zc
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -47,3 +47,4 @@ gemini-websocket-proxy/
|
|||||||
Tools/dark-server/dark-server.js
|
Tools/dark-server/dark-server.js
|
||||||
Tools/rimworld_cpt_data.jsonl
|
Tools/rimworld_cpt_data.jsonl
|
||||||
Tools/mem0-1.0.0/
|
Tools/mem0-1.0.0/
|
||||||
|
Tools/thenextagent-1
|
||||||
|
|||||||
Binary file not shown.
1235
Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs
Normal file
1235
Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Verse;
|
||||||
|
using RimWorld;
|
||||||
|
using LudeonTK;
|
||||||
|
using WulaFallenEmpire.EventSystem.AI.UI;
|
||||||
|
|
||||||
|
namespace WulaFallenEmpire.EventSystem.AI
|
||||||
|
{
|
||||||
|
public static class DebugActions_WulaLink
|
||||||
|
{
|
||||||
|
[DebugAction("WulaLink", "Open WulaLink UI", actionType = DebugActionType.Action, allowedGameStates = AllowedGameStates.Playing)]
|
||||||
|
public static void OpenWulaLink()
|
||||||
|
{
|
||||||
|
// Find a suitable event def or create a generic one
|
||||||
|
EventDef def = DefDatabase<EventDef>.AllDefs.FirstOrDefault();
|
||||||
|
if (def == null)
|
||||||
|
{
|
||||||
|
Messages.Message("No EventDef found to initialize WulaLink.", MessageTypeDefOf.RejectInput, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Find.WindowStack.Add(new Overlay_WulaLink(def));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using Verse;
|
using Verse;
|
||||||
using WulaFallenEmpire.EventSystem.AI.UI;
|
using WulaFallenEmpire.EventSystem.AI;
|
||||||
|
|
||||||
namespace WulaFallenEmpire.EventSystem.AI.Tools
|
namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||||
{
|
{
|
||||||
@@ -21,13 +21,13 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
|
|||||||
{
|
{
|
||||||
if (int.TryParse(idStr, out id))
|
if (int.TryParse(idStr, out id))
|
||||||
{
|
{
|
||||||
var window = Dialog_AIConversation.Instance ?? Find.WindowStack.WindowOfType<Dialog_AIConversation>();
|
var core = AIIntelligenceCore.Instance;
|
||||||
if (window != null)
|
if (core != null)
|
||||||
{
|
{
|
||||||
window.SetPortrait(id);
|
core.SetPortrait(id);
|
||||||
return $"Expression changed to {id}.";
|
return $"Expression changed to {id}.";
|
||||||
}
|
}
|
||||||
return "Error: Dialog window not found.";
|
return "Error: AI Core not found.";
|
||||||
}
|
}
|
||||||
return "Error: Invalid arguments. 'expression_id' must be an integer.";
|
return "Error: Invalid arguments. 'expression_id' must be an integer.";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ using System.Reflection;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using Verse;
|
using Verse;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using WulaFallenEmpire.EventSystem.AI.UI;
|
using WulaFallenEmpire.EventSystem.AI;
|
||||||
|
|
||||||
namespace WulaFallenEmpire.EventSystem.AI.Tools
|
namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||||
{
|
{
|
||||||
@@ -110,10 +110,10 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
|
|||||||
|
|
||||||
private static string BuildToolHistory(int maxCount)
|
private static string BuildToolHistory(int maxCount)
|
||||||
{
|
{
|
||||||
var window = Dialog_AIConversation.Instance ?? Find.WindowStack.WindowOfType<Dialog_AIConversation>();
|
var core = AIIntelligenceCore.Instance;
|
||||||
if (window == null) return "AI Tool History: none found.";
|
if (core == null) return "AI Tool History: none found.";
|
||||||
|
|
||||||
var history = window.GetHistorySnapshot();
|
var history = core.GetHistorySnapshot();
|
||||||
if (history == null || history.Count == 0) return "AI Tool History: none found.";
|
if (history == null || history.Count == 0) return "AI Tool History: none found.";
|
||||||
|
|
||||||
var entries = new List<(string ToolXml, string ToolResult)>();
|
var entries = new List<(string ToolXml, string ToolResult)>();
|
||||||
@@ -310,3 +310,4 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -7,7 +7,6 @@ using System.Threading.Tasks;
|
|||||||
using RimWorld;
|
using RimWorld;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using Verse;
|
using Verse;
|
||||||
using WulaFallenEmpire.EventSystem.AI;
|
|
||||||
using WulaFallenEmpire.EventSystem.AI.Tools;
|
using WulaFallenEmpire.EventSystem.AI.Tools;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
@@ -45,13 +44,6 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
|
|||||||
private const int DefaultMaxHistoryTokens = 100000;
|
private const int DefaultMaxHistoryTokens = 100000;
|
||||||
private const int CharsPerToken = 4;
|
private const int CharsPerToken = 4;
|
||||||
private const int ThinkingPhaseTotal = 3;
|
private const int ThinkingPhaseTotal = 3;
|
||||||
private const int MemorySearchLimit = 6;
|
|
||||||
private const int MemoryFactMaxChars = 200;
|
|
||||||
private const int MemoryPromptMaxChars = 1600;
|
|
||||||
private const int MemoryUpdateMaxMemories = 40;
|
|
||||||
private string _memoryContextCache = "";
|
|
||||||
private string _memoryContextQuery = "";
|
|
||||||
private bool _memoryExtractionInFlight = false;
|
|
||||||
|
|
||||||
private enum RequestPhase
|
private enum RequestPhase
|
||||||
{
|
{
|
||||||
@@ -67,20 +59,6 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
|
|||||||
public bool AnyActionError;
|
public bool AnyActionError;
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct MemoryFact
|
|
||||||
{
|
|
||||||
public string Text;
|
|
||||||
public string Category;
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct MemoryUpdate
|
|
||||||
{
|
|
||||||
public string Event;
|
|
||||||
public string Id;
|
|
||||||
public string Text;
|
|
||||||
public string Category;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetThinkingPhase(int phaseIndex, bool isRetry)
|
private void SetThinkingPhase(int phaseIndex, bool isRetry)
|
||||||
{
|
{
|
||||||
_thinkingPhaseIndex = Math.Max(1, Math.Min(ThinkingPhaseTotal, phaseIndex));
|
_thinkingPhaseIndex = Math.Max(1, Math.Min(ThinkingPhaseTotal, phaseIndex));
|
||||||
@@ -256,14 +234,9 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
|||||||
// Use XML persona if available, otherwise default
|
// Use XML persona if available, otherwise default
|
||||||
string persona = !string.IsNullOrEmpty(def.aiSystemInstruction) ? def.aiSystemInstruction : DefaultPersona;
|
string persona = !string.IsNullOrEmpty(def.aiSystemInstruction) ? def.aiSystemInstruction : DefaultPersona;
|
||||||
|
|
||||||
string memoryContext = _memoryContextCache;
|
|
||||||
string personaWithMemory = string.IsNullOrWhiteSpace(memoryContext)
|
|
||||||
? persona
|
|
||||||
: persona + "\n\n" + memoryContext;
|
|
||||||
|
|
||||||
string fullInstruction = toolsEnabled
|
string fullInstruction = toolsEnabled
|
||||||
? (personaWithMemory + "\n" + ToolRulesInstruction + "\n" + toolsForThisPhase)
|
? (persona + "\n" + ToolRulesInstruction + "\n" + toolsForThisPhase)
|
||||||
: personaWithMemory;
|
: persona;
|
||||||
|
|
||||||
string language = LanguageDatabase.activeLanguage.FriendlyNameNative;
|
string language = LanguageDatabase.activeLanguage.FriendlyNameNative;
|
||||||
var eventVarManager = Find.World.GetComponent<EventVariableManager>();
|
var eventVarManager = Find.World.GetComponent<EventVariableManager>();
|
||||||
@@ -521,542 +494,6 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
|||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PrepareMemoryContext()
|
|
||||||
{
|
|
||||||
string lastUserMessage = _history.LastOrDefault(entry => string.Equals(entry.role, "user", StringComparison.OrdinalIgnoreCase)).message;
|
|
||||||
if (string.IsNullOrWhiteSpace(lastUserMessage))
|
|
||||||
{
|
|
||||||
_memoryContextCache = "";
|
|
||||||
_memoryContextQuery = "";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_memoryContextQuery = lastUserMessage;
|
|
||||||
_memoryContextCache = BuildMemoryContext(lastUserMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
private string BuildMemoryContext(string userQuery)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(userQuery)) return "";
|
|
||||||
|
|
||||||
var memoryManager = Find.World?.GetComponent<AIMemoryManager>();
|
|
||||||
if (memoryManager == null) return "";
|
|
||||||
|
|
||||||
var memories = memoryManager.SearchMemories(userQuery, MemorySearchLimit);
|
|
||||||
if (memories == null || memories.Count == 0) return "";
|
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
sb.AppendLine("# MEMORY (Relevant Facts)");
|
|
||||||
foreach (var memory in memories)
|
|
||||||
{
|
|
||||||
if (memory == null || string.IsNullOrWhiteSpace(memory.Fact)) continue;
|
|
||||||
string category = string.IsNullOrWhiteSpace(memory.Category) ? "misc" : memory.Category.Trim();
|
|
||||||
string fact = TrimMemoryFact(memory.Fact, MemoryFactMaxChars);
|
|
||||||
if (string.IsNullOrWhiteSpace(fact)) continue;
|
|
||||||
sb.AppendLine($"- [{category}] {fact}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return sb.ToString().TrimEnd();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string TrimMemoryFact(string fact, int maxChars)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(fact)) return "";
|
|
||||||
string trimmed = fact.Trim();
|
|
||||||
if (trimmed.Length <= maxChars) return trimmed;
|
|
||||||
return trimmed.Substring(0, maxChars) + "...";
|
|
||||||
}
|
|
||||||
|
|
||||||
private string BuildMemoryExtractionConversation()
|
|
||||||
{
|
|
||||||
if (_history == null || _history.Count == 0) return "";
|
|
||||||
|
|
||||||
const int maxTurns = 6;
|
|
||||||
var lines = new List<string>();
|
|
||||||
|
|
||||||
for (int i = _history.Count - 1; i >= 0 && lines.Count < maxTurns; i--)
|
|
||||||
{
|
|
||||||
var entry = _history[i];
|
|
||||||
if (!string.Equals(entry.role, "user", StringComparison.OrdinalIgnoreCase) &&
|
|
||||||
!string.Equals(entry.role, "assistant", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
string content = entry.role == "assistant"
|
|
||||||
? ParseResponseForDisplay(entry.message)
|
|
||||||
: entry.message;
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(content)) continue;
|
|
||||||
|
|
||||||
lines.Add($"{entry.role}: {content.Trim()}");
|
|
||||||
}
|
|
||||||
|
|
||||||
lines.Reverse();
|
|
||||||
string snippet = string.Join("\n", lines);
|
|
||||||
return TrimForPrompt(snippet, MemoryPromptMaxChars);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ExtractAndUpdateMemoriesAsync()
|
|
||||||
{
|
|
||||||
if (_memoryExtractionInFlight) return;
|
|
||||||
_memoryExtractionInFlight = true;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var world = Find.World;
|
|
||||||
if (world == null) return;
|
|
||||||
|
|
||||||
var memoryManager = world.GetComponent<AIMemoryManager>();
|
|
||||||
if (memoryManager == null) return;
|
|
||||||
|
|
||||||
string conversation = BuildMemoryExtractionConversation();
|
|
||||||
if (string.IsNullOrWhiteSpace(conversation)) return;
|
|
||||||
|
|
||||||
var settings = WulaFallenEmpire.WulaFallenEmpireMod.settings;
|
|
||||||
if (settings == null) return;
|
|
||||||
|
|
||||||
var client = new SimpleAIClient(settings.apiKey, settings.baseUrl, settings.model);
|
|
||||||
string extractPrompt = MemoryPrompts.BuildFactExtractionPrompt(conversation);
|
|
||||||
string extractResponse = await client.GetChatCompletionAsync(extractPrompt, new List<(string role, string message)>(), maxTokens: 256, temperature: 0.2f);
|
|
||||||
if (string.IsNullOrWhiteSpace(extractResponse)) return;
|
|
||||||
|
|
||||||
List<MemoryFact> facts = ParseFactsResponse(extractResponse);
|
|
||||||
if (facts.Count == 0) return;
|
|
||||||
|
|
||||||
var existing = memoryManager.GetAllMemories()
|
|
||||||
.OrderByDescending(m => m.UpdatedTicks)
|
|
||||||
.ThenByDescending(m => m.CreatedTicks)
|
|
||||||
.Take(MemoryUpdateMaxMemories)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
if (existing.Count == 0)
|
|
||||||
{
|
|
||||||
AddFactsToMemory(memoryManager, facts);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string existingJson = BuildMemoriesJson(existing);
|
|
||||||
string factsJson = BuildFactsJson(facts);
|
|
||||||
string updatePrompt = MemoryPrompts.BuildMemoryUpdatePrompt(existingJson, factsJson);
|
|
||||||
string updateResponse = await client.GetChatCompletionAsync(updatePrompt, new List<(string role, string message)>(), maxTokens: 256, temperature: 0.2f);
|
|
||||||
if (string.IsNullOrWhiteSpace(updateResponse))
|
|
||||||
{
|
|
||||||
AddFactsToMemory(memoryManager, facts);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<MemoryUpdate> updates = ParseMemoryUpdateResponse(updateResponse);
|
|
||||||
if (updates.Count == 0)
|
|
||||||
{
|
|
||||||
AddFactsToMemory(memoryManager, facts);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ApplyMemoryUpdates(memoryManager, updates, facts);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
WulaLog.Debug($"[WulaAI] Memory extraction failed: {ex}");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_memoryExtractionInFlight = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AddFactsToMemory(AIMemoryManager memoryManager, List<MemoryFact> facts)
|
|
||||||
{
|
|
||||||
if (memoryManager == null || facts == null) return;
|
|
||||||
|
|
||||||
foreach (var fact in facts)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(fact.Text)) continue;
|
|
||||||
memoryManager.AddMemory(fact.Text, fact.Category);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ApplyMemoryUpdates(AIMemoryManager memoryManager, List<MemoryUpdate> updates, List<MemoryFact> fallbackFacts)
|
|
||||||
{
|
|
||||||
if (memoryManager == null || updates == null) return;
|
|
||||||
|
|
||||||
bool applied = false;
|
|
||||||
bool anyDecision = false;
|
|
||||||
foreach (var update in updates)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(update.Event)) continue;
|
|
||||||
|
|
||||||
switch (update.Event.Trim().ToUpperInvariant())
|
|
||||||
{
|
|
||||||
case "ADD":
|
|
||||||
anyDecision = true;
|
|
||||||
if (!string.IsNullOrWhiteSpace(update.Text))
|
|
||||||
{
|
|
||||||
memoryManager.AddMemory(update.Text, update.Category);
|
|
||||||
applied = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "UPDATE":
|
|
||||||
anyDecision = true;
|
|
||||||
if (!string.IsNullOrWhiteSpace(update.Id))
|
|
||||||
{
|
|
||||||
if (memoryManager.UpdateMemory(update.Id, update.Text, update.Category))
|
|
||||||
{
|
|
||||||
applied = true;
|
|
||||||
}
|
|
||||||
else if (!string.IsNullOrWhiteSpace(update.Text))
|
|
||||||
{
|
|
||||||
memoryManager.AddMemory(update.Text, update.Category);
|
|
||||||
applied = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (!string.IsNullOrWhiteSpace(update.Text))
|
|
||||||
{
|
|
||||||
memoryManager.AddMemory(update.Text, update.Category);
|
|
||||||
applied = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "DELETE":
|
|
||||||
anyDecision = true;
|
|
||||||
if (!string.IsNullOrWhiteSpace(update.Id) && memoryManager.DeleteMemory(update.Id))
|
|
||||||
{
|
|
||||||
applied = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "NONE":
|
|
||||||
anyDecision = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!applied && !anyDecision)
|
|
||||||
{
|
|
||||||
AddFactsToMemory(memoryManager, fallbackFacts);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string BuildMemoriesJson(List<AIMemoryEntry> memories)
|
|
||||||
{
|
|
||||||
if (memories == null || memories.Count == 0) return "[]";
|
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
sb.Append("[");
|
|
||||||
bool first = true;
|
|
||||||
foreach (var memory in memories)
|
|
||||||
{
|
|
||||||
if (memory == null || string.IsNullOrWhiteSpace(memory.Fact)) continue;
|
|
||||||
if (!first) sb.Append(",");
|
|
||||||
first = false;
|
|
||||||
sb.Append("{");
|
|
||||||
sb.Append("\"id\":\"").Append(EscapeJson(memory.Id)).Append("\",");
|
|
||||||
sb.Append("\"text\":\"").Append(EscapeJson(memory.Fact)).Append("\",");
|
|
||||||
sb.Append("\"category\":\"").Append(EscapeJson(memory.Category)).Append("\"");
|
|
||||||
sb.Append("}");
|
|
||||||
}
|
|
||||||
sb.Append("]");
|
|
||||||
return sb.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string BuildFactsJson(List<MemoryFact> facts)
|
|
||||||
{
|
|
||||||
if (facts == null || facts.Count == 0) return "[]";
|
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
sb.Append("[");
|
|
||||||
bool first = true;
|
|
||||||
foreach (var fact in facts)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(fact.Text)) continue;
|
|
||||||
if (!first) sb.Append(",");
|
|
||||||
first = false;
|
|
||||||
sb.Append("{");
|
|
||||||
sb.Append("\"text\":\"").Append(EscapeJson(fact.Text)).Append("\",");
|
|
||||||
sb.Append("\"category\":\"").Append(EscapeJson(fact.Category)).Append("\"");
|
|
||||||
sb.Append("}");
|
|
||||||
}
|
|
||||||
sb.Append("]");
|
|
||||||
return sb.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<MemoryFact> ParseFactsResponse(string response)
|
|
||||||
{
|
|
||||||
var facts = new List<MemoryFact>();
|
|
||||||
string array = TryExtractJsonArray(response, "facts");
|
|
||||||
if (string.IsNullOrWhiteSpace(array)) return facts;
|
|
||||||
|
|
||||||
var objects = ExtractJsonObjects(array);
|
|
||||||
if (objects.Count > 0)
|
|
||||||
{
|
|
||||||
foreach (string obj in objects)
|
|
||||||
{
|
|
||||||
var dict = SimpleJsonParser.Parse(obj);
|
|
||||||
if (dict == null || dict.Count == 0) continue;
|
|
||||||
|
|
||||||
string text = GetDictionaryValue(dict, "text") ?? GetDictionaryValue(dict, "fact");
|
|
||||||
if (string.IsNullOrWhiteSpace(text)) continue;
|
|
||||||
|
|
||||||
string category = NormalizeMemoryCategory(GetDictionaryValue(dict, "category"));
|
|
||||||
facts.Add(new MemoryFact { Text = text.Trim(), Category = category });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
foreach (string item in SplitJsonArrayValues(array))
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(item)) continue;
|
|
||||||
facts.Add(new MemoryFact { Text = item.Trim(), Category = "misc" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var deduped = new List<MemoryFact>();
|
|
||||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
foreach (var fact in facts)
|
|
||||||
{
|
|
||||||
string hash = AIMemoryEntry.ComputeHash(fact.Text);
|
|
||||||
if (string.IsNullOrWhiteSpace(hash) || !seen.Add(hash)) continue;
|
|
||||||
deduped.Add(fact);
|
|
||||||
}
|
|
||||||
|
|
||||||
return deduped;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<MemoryUpdate> ParseMemoryUpdateResponse(string response)
|
|
||||||
{
|
|
||||||
var updates = new List<MemoryUpdate>();
|
|
||||||
string array = TryExtractJsonArray(response, "memory") ?? TryExtractJsonArray(response, "memories");
|
|
||||||
if (string.IsNullOrWhiteSpace(array)) return updates;
|
|
||||||
|
|
||||||
foreach (string obj in ExtractJsonObjects(array))
|
|
||||||
{
|
|
||||||
var dict = SimpleJsonParser.Parse(obj);
|
|
||||||
if (dict == null || dict.Count == 0) continue;
|
|
||||||
|
|
||||||
string evt = GetDictionaryValue(dict, "event") ?? GetDictionaryValue(dict, "action");
|
|
||||||
if (string.IsNullOrWhiteSpace(evt)) continue;
|
|
||||||
|
|
||||||
updates.Add(new MemoryUpdate
|
|
||||||
{
|
|
||||||
Event = evt.Trim().ToUpperInvariant(),
|
|
||||||
Id = GetDictionaryValue(dict, "id"),
|
|
||||||
Text = GetDictionaryValue(dict, "text") ?? GetDictionaryValue(dict, "fact"),
|
|
||||||
Category = NormalizeMemoryCategory(GetDictionaryValue(dict, "category"))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return updates;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string NormalizeMemoryCategory(string category)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(category)) return "misc";
|
|
||||||
string normalized = category.Trim().ToLowerInvariant();
|
|
||||||
switch (normalized)
|
|
||||||
{
|
|
||||||
case "preference":
|
|
||||||
case "personal":
|
|
||||||
case "plan":
|
|
||||||
case "colony":
|
|
||||||
case "misc":
|
|
||||||
return normalized;
|
|
||||||
default:
|
|
||||||
return "misc";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string TryExtractJsonArray(string json, string key)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(json) || string.IsNullOrWhiteSpace(key)) return null;
|
|
||||||
|
|
||||||
string keyPattern = $"\"{key}\"";
|
|
||||||
int keyIndex = json.IndexOf(keyPattern, StringComparison.OrdinalIgnoreCase);
|
|
||||||
if (keyIndex == -1) return null;
|
|
||||||
|
|
||||||
int arrayStart = json.IndexOf('[', keyIndex);
|
|
||||||
if (arrayStart == -1) return null;
|
|
||||||
|
|
||||||
int arrayEnd = FindMatchingBracket(json, arrayStart);
|
|
||||||
if (arrayEnd == -1) return null;
|
|
||||||
|
|
||||||
return json.Substring(arrayStart + 1, arrayEnd - arrayStart - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<string> ExtractJsonObjects(string arrayContent)
|
|
||||||
{
|
|
||||||
var objects = new List<string>();
|
|
||||||
if (string.IsNullOrWhiteSpace(arrayContent)) return objects;
|
|
||||||
|
|
||||||
int depth = 0;
|
|
||||||
int start = -1;
|
|
||||||
bool inString = false;
|
|
||||||
bool escaped = false;
|
|
||||||
|
|
||||||
for (int i = 0; i < arrayContent.Length; i++)
|
|
||||||
{
|
|
||||||
char c = arrayContent[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 == '{')
|
|
||||||
{
|
|
||||||
if (depth == 0) start = i;
|
|
||||||
depth++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (c == '}')
|
|
||||||
{
|
|
||||||
depth--;
|
|
||||||
if (depth == 0 && start >= 0)
|
|
||||||
{
|
|
||||||
objects.Add(arrayContent.Substring(start, i - start + 1));
|
|
||||||
start = -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return objects;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<string> SplitJsonArrayValues(string arrayContent)
|
|
||||||
{
|
|
||||||
var items = new List<string>();
|
|
||||||
if (string.IsNullOrWhiteSpace(arrayContent)) return items;
|
|
||||||
|
|
||||||
bool inString = false;
|
|
||||||
bool escaped = false;
|
|
||||||
int start = 0;
|
|
||||||
|
|
||||||
for (int i = 0; i < arrayContent.Length; i++)
|
|
||||||
{
|
|
||||||
char c = arrayContent[i];
|
|
||||||
if (inString)
|
|
||||||
{
|
|
||||||
if (escaped)
|
|
||||||
{
|
|
||||||
escaped = false;
|
|
||||||
}
|
|
||||||
else if (c == '\\')
|
|
||||||
{
|
|
||||||
escaped = true;
|
|
||||||
}
|
|
||||||
else if (c == '"')
|
|
||||||
{
|
|
||||||
inString = false;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (c == '"')
|
|
||||||
{
|
|
||||||
inString = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (c == ',')
|
|
||||||
{
|
|
||||||
string part = arrayContent.Substring(start, i - start);
|
|
||||||
items.Add(UnescapeJsonString(part.Trim().Trim('"')));
|
|
||||||
start = i + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (start < arrayContent.Length)
|
|
||||||
{
|
|
||||||
string part = arrayContent.Substring(start);
|
|
||||||
items.Add(UnescapeJsonString(part.Trim().Trim('"')));
|
|
||||||
}
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string UnescapeJsonString(string value)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(value)) return "";
|
|
||||||
return value.Replace("\\r", "\r").Replace("\\n", "\n").Replace("\\\"", "\"").Replace("\\\\", "\\");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetDictionaryValue(Dictionary<string, string> dict, string key)
|
|
||||||
{
|
|
||||||
if (dict == null || string.IsNullOrWhiteSpace(key)) return null;
|
|
||||||
return dict.TryGetValue(key, out string value) ? value : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string EscapeJson(string value)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(value)) return "";
|
|
||||||
return value.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\n", "\\n").Replace("\r", "\\r");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int FindMatchingBracket(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 async Task RunPhasedRequestAsync()
|
private async Task RunPhasedRequestAsync()
|
||||||
{
|
{
|
||||||
if (_isThinking) return;
|
if (_isThinking) return;
|
||||||
@@ -1081,7 +518,6 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
PrepareMemoryContext();
|
|
||||||
CompressHistoryIfNeeded();
|
CompressHistoryIfNeeded();
|
||||||
|
|
||||||
var settings = WulaFallenEmpireMod.settings;
|
var settings = WulaFallenEmpireMod.settings;
|
||||||
@@ -1365,7 +801,6 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
|||||||
}
|
}
|
||||||
|
|
||||||
ParseResponse(reply);
|
ParseResponse(reply);
|
||||||
_ = ExtractAndUpdateMemoriesAsync();
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -2082,8 +1517,6 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
|||||||
_inputText = "";
|
_inputText = "";
|
||||||
|
|
||||||
_history.Clear();
|
_history.Clear();
|
||||||
_memoryContextCache = "";
|
|
||||||
_memoryContextQuery = "";
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var historyManager = Find.World?.GetComponent<AIHistoryManager>();
|
var historyManager = Find.World?.GetComponent<AIHistoryManager>();
|
||||||
|
|||||||
549
Source/WulaFallenEmpire/EventSystem/AI/UI/Overlay_WulaLink.cs
Normal file
549
Source/WulaFallenEmpire/EventSystem/AI/UI/Overlay_WulaLink.cs
Normal file
@@ -0,0 +1,549 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using UnityEngine;
|
||||||
|
using Verse;
|
||||||
|
using RimWorld;
|
||||||
|
using WulaFallenEmpire.EventSystem.AI;
|
||||||
|
|
||||||
|
namespace WulaFallenEmpire.EventSystem.AI.UI
|
||||||
|
{
|
||||||
|
public class Overlay_WulaLink : Window
|
||||||
|
{
|
||||||
|
// Core Connection
|
||||||
|
private AIIntelligenceCore _core;
|
||||||
|
private string _eventDefName;
|
||||||
|
private EventDef _def;
|
||||||
|
|
||||||
|
// UI State
|
||||||
|
private Vector2 _scrollPosition = Vector2.zero;
|
||||||
|
private string _inputText = "";
|
||||||
|
private bool _scrollToBottom = true;
|
||||||
|
|
||||||
|
|
||||||
|
// HUD / Minimized State
|
||||||
|
private bool _isMinimized = false;
|
||||||
|
private int _unreadCount = 0;
|
||||||
|
private Vector2 _expandedSize;
|
||||||
|
private Vector2 _minimizedSize = new Vector2(220f, 80f);
|
||||||
|
|
||||||
|
// Layout Constants
|
||||||
|
private const float HeaderHeight = 50f;
|
||||||
|
private const float FooterHeight = 50f;
|
||||||
|
private const float AvatarSize = 40f;
|
||||||
|
private const float BubblePadding = 10f;
|
||||||
|
private const float MessageSpacing = 15f;
|
||||||
|
private const float MaxBubbleWidthRatio = 0.75f;
|
||||||
|
|
||||||
|
public Overlay_WulaLink(EventDef def)
|
||||||
|
{
|
||||||
|
this._def = def;
|
||||||
|
this._eventDefName = def.defName;
|
||||||
|
|
||||||
|
// Window Properties (Floating, Non-Modal)
|
||||||
|
this.layer = WindowLayer.GameUI;
|
||||||
|
this.forcePause = false;
|
||||||
|
this.absorbInputAroundWindow = false;
|
||||||
|
this.closeOnClickedOutside = false;
|
||||||
|
this.doWindowBackground = false; // We draw our own
|
||||||
|
this.drawShadow = true;
|
||||||
|
this.draggable = true;
|
||||||
|
this.resizeable = true;
|
||||||
|
this.preventCameraMotion = false;
|
||||||
|
|
||||||
|
// Initial Size (Phone-like)
|
||||||
|
_expandedSize = new Vector2(380f, 600f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Vector2 InitialSize => _isMinimized ? _minimizedSize : _expandedSize;
|
||||||
|
|
||||||
|
public void ToggleMinimize()
|
||||||
|
{
|
||||||
|
_isMinimized = !_isMinimized;
|
||||||
|
_unreadCount = 0; // Reset on toggle? Or only on expand? Let's say expand.
|
||||||
|
|
||||||
|
if (_isMinimized)
|
||||||
|
{
|
||||||
|
// Save current position if needed, or just snap to right edge
|
||||||
|
windowRect.width = _minimizedSize.x;
|
||||||
|
windowRect.height = _minimizedSize.y;
|
||||||
|
windowRect.x = Verse.UI.screenWidth - _minimizedSize.x - 20f;
|
||||||
|
windowRect.y = Verse.UI.screenHeight / 2f;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
windowRect.width = _expandedSize.x;
|
||||||
|
windowRect.height = _expandedSize.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Expand()
|
||||||
|
{
|
||||||
|
if (_isMinimized) ToggleMinimize();
|
||||||
|
// Ensure window is on screen
|
||||||
|
if (windowRect.x > Verse.UI.screenWidth - 100f)
|
||||||
|
{
|
||||||
|
windowRect.x = Verse.UI.screenWidth - _expandedSize.x - 20f;
|
||||||
|
}
|
||||||
|
Find.WindowStack.Notify_ManuallySetFocus(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void PreOpen()
|
||||||
|
{
|
||||||
|
base.PreOpen();
|
||||||
|
// Connect to Core
|
||||||
|
_core = Find.World.GetComponent<AIIntelligenceCore>();
|
||||||
|
if (_core != null)
|
||||||
|
{
|
||||||
|
_core.InitializeConversation(_eventDefName);
|
||||||
|
_core.OnMessageReceived += OnMessageReceived;
|
||||||
|
_core.OnThinkingStateChanged += OnThinkingStateChanged;
|
||||||
|
_core.OnExpressionChanged += OnExpressionChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void PostClose()
|
||||||
|
{
|
||||||
|
base.PostClose();
|
||||||
|
if (_core != null)
|
||||||
|
{
|
||||||
|
_core.OnMessageReceived -= OnMessageReceived;
|
||||||
|
_core.OnThinkingStateChanged -= OnThinkingStateChanged;
|
||||||
|
_core.OnExpressionChanged -= OnExpressionChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMessageReceived(string msg)
|
||||||
|
{
|
||||||
|
_scrollToBottom = true;
|
||||||
|
if (_isMinimized)
|
||||||
|
{
|
||||||
|
_unreadCount++;
|
||||||
|
// Spawn Notification Bubble
|
||||||
|
Find.WindowStack.Add(new Overlay_WulaLink_Notification(msg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnThinkingStateChanged(bool thinking)
|
||||||
|
{
|
||||||
|
// Trigger repaint or animation update if needed
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnExpressionChanged(int id)
|
||||||
|
{
|
||||||
|
// Repaint happens next frame
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void DoWindowContents(Rect inRect)
|
||||||
|
{
|
||||||
|
if (_isMinimized)
|
||||||
|
{
|
||||||
|
DrawMinimized(inRect);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw Main Background (Whole Window)
|
||||||
|
Widgets.DrawBoxSolid(inRect, WulaLinkStyles.BackgroundColor);
|
||||||
|
GUI.color = new Color(0.8f, 0.8f, 0.8f);
|
||||||
|
Widgets.DrawBox(inRect, 1); // Border
|
||||||
|
GUI.color = Color.white;
|
||||||
|
|
||||||
|
// Areas
|
||||||
|
Rect headerRect = new Rect(0, 0, inRect.width, HeaderHeight);
|
||||||
|
Rect footerRect = new Rect(0, inRect.height - FooterHeight, inRect.width, FooterHeight);
|
||||||
|
Rect contextRect = new Rect(0, inRect.height - FooterHeight - 30f, inRect.width, 30f); // Context Bar
|
||||||
|
Rect bodyRect = new Rect(0, HeaderHeight, inRect.width, inRect.height - HeaderHeight - FooterHeight - 30f);
|
||||||
|
|
||||||
|
// Draw Components
|
||||||
|
DrawHeader(headerRect);
|
||||||
|
DrawMessageList(bodyRect);
|
||||||
|
DrawContextBar(contextRect);
|
||||||
|
DrawFooter(footerRect);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawMinimized(Rect rect)
|
||||||
|
{
|
||||||
|
// HUD Capsule Style
|
||||||
|
Widgets.DrawBoxSolid(rect, new Color(0.1f, 0.1f, 0.1f, 0.85f)); // Semi-transparent black
|
||||||
|
GUI.color = WulaLinkStyles.HeaderColor;
|
||||||
|
Widgets.DrawBox(rect, 2); // Thicker colored border
|
||||||
|
GUI.color = Color.white;
|
||||||
|
|
||||||
|
// Layout
|
||||||
|
Rect titleRect = new Rect(rect.x + 10f, rect.y + 5f, rect.width - 20f, 25f);
|
||||||
|
Rect statusRect = new Rect(rect.x + 10f, rect.yMax - 30f, rect.width - 20f, 25f);
|
||||||
|
|
||||||
|
// Title
|
||||||
|
Text.Anchor = TextAnchor.MiddleCenter;
|
||||||
|
Text.Font = GameFont.Small;
|
||||||
|
Widgets.Label(titleRect, "WULA LINK");
|
||||||
|
|
||||||
|
// Status
|
||||||
|
string status = _core.IsThinking ? "Thinking..." : "Standby";
|
||||||
|
Color statusColor = _core.IsThinking ? Color.yellow : Color.green;
|
||||||
|
|
||||||
|
GUI.color = statusColor;
|
||||||
|
Widgets.Label(statusRect, $"â—?{status}");
|
||||||
|
GUI.color = Color.white;
|
||||||
|
|
||||||
|
// Unread Badge
|
||||||
|
if (_unreadCount > 0)
|
||||||
|
{
|
||||||
|
float badgeSize = 24f;
|
||||||
|
Rect badgeRect = new Rect(rect.xMax - badgeSize - 5f, rect.y - 10f, badgeSize, badgeSize);
|
||||||
|
GUI.color = Color.red;
|
||||||
|
GUI.DrawTexture(badgeRect, BaseContent.WhiteTex); // Circle ideally
|
||||||
|
GUI.color = Color.white;
|
||||||
|
Text.Font = GameFont.Tiny;
|
||||||
|
Text.Anchor = TextAnchor.MiddleCenter;
|
||||||
|
Widgets.Label(badgeRect, _unreadCount.ToString());
|
||||||
|
Text.Font = GameFont.Small;
|
||||||
|
}
|
||||||
|
Text.Anchor = TextAnchor.UpperLeft;
|
||||||
|
|
||||||
|
// Click to Expand
|
||||||
|
if (Widgets.ButtonInvisible(rect))
|
||||||
|
{
|
||||||
|
ToggleMinimize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawContextBar(Rect rect)
|
||||||
|
{
|
||||||
|
// Context Awareness
|
||||||
|
Widgets.DrawBoxSolid(rect, new Color(0.15f, 0.15f, 0.15f, 1f));
|
||||||
|
GUI.color = Color.grey;
|
||||||
|
Widgets.DrawLineHorizontal(rect.x, rect.y, rect.width);
|
||||||
|
|
||||||
|
string contextInfo = "Context: None";
|
||||||
|
if (Find.Selector.SingleSelectedThing != null)
|
||||||
|
{
|
||||||
|
contextInfo = $"Context: [{Find.Selector.SingleSelectedThing.LabelCap}]";
|
||||||
|
}
|
||||||
|
else if (Find.Selector.SelectedObjects.Count > 1)
|
||||||
|
{
|
||||||
|
contextInfo = $"Context: {Find.Selector.SelectedObjects.Count} objects selected";
|
||||||
|
}
|
||||||
|
|
||||||
|
Text.Anchor = TextAnchor.MiddleLeft;
|
||||||
|
Text.Font = GameFont.Tiny;
|
||||||
|
Widgets.Label(new Rect(rect.x + 10f, rect.y, rect.width - 20f, rect.height), contextInfo);
|
||||||
|
Text.Anchor = TextAnchor.UpperLeft;
|
||||||
|
GUI.color = Color.white;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawHeader(Rect rect)
|
||||||
|
{
|
||||||
|
// Header BG
|
||||||
|
Widgets.DrawBoxSolid(rect, WulaLinkStyles.HeaderColor);
|
||||||
|
|
||||||
|
// Title
|
||||||
|
Text.Font = GameFont.Medium;
|
||||||
|
Text.Anchor = TextAnchor.MiddleLeft;
|
||||||
|
GUI.color = Color.white;
|
||||||
|
Rect titleRect = rect;
|
||||||
|
titleRect.x += 10f;
|
||||||
|
Widgets.Label(titleRect, _def.characterName ?? "MomoTalk");
|
||||||
|
|
||||||
|
// Header Icons (Minimize/Close)
|
||||||
|
Rect closeRect = new Rect(rect.width - 30f, 10f, 20f, 20f);
|
||||||
|
Rect minRect = new Rect(rect.width - 60f, 10f, 20f, 20f);
|
||||||
|
|
||||||
|
if (Widgets.ButtonText(minRect, "-"))
|
||||||
|
{
|
||||||
|
ToggleMinimize();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Widgets.ButtonImage(closeRect, Widgets.CheckboxOffTex)) // Use standard X tex
|
||||||
|
{
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
GUI.color = Color.white;
|
||||||
|
Text.Anchor = TextAnchor.UpperLeft;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawMessageList(Rect rect)
|
||||||
|
{
|
||||||
|
var history = _core?.GetHistorySnapshot();
|
||||||
|
if (history == null) return;
|
||||||
|
|
||||||
|
// Filter out tool messages for cleaner display (only show in DevMode)
|
||||||
|
var displayHistory = new List<(string role, string message)>();
|
||||||
|
foreach (var msg in history)
|
||||||
|
{
|
||||||
|
// Skip tool/toolcall messages entirely in normal mode
|
||||||
|
if ((msg.role == "tool" || msg.role == "toolcall") && !Prefs.DevMode) continue;
|
||||||
|
displayHistory.Add(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup ScrollView
|
||||||
|
float contentHeight = 0f;
|
||||||
|
float width = rect.width - 26f; // Scrollbar space (16 + margin)
|
||||||
|
float reducedSpacing = 8f; // Reduced from MessageSpacing (15f) to 8f
|
||||||
|
|
||||||
|
List<float> heights = new List<float>();
|
||||||
|
foreach (var msg in displayHistory)
|
||||||
|
{
|
||||||
|
float h = CalcMessageHeight(msg.message, width);
|
||||||
|
heights.Add(h);
|
||||||
|
contentHeight += h + reducedSpacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_core.IsThinking)
|
||||||
|
{
|
||||||
|
contentHeight += 40f; // Space for thinking indicator
|
||||||
|
}
|
||||||
|
|
||||||
|
Rect viewRect = new Rect(0, 0, width, contentHeight);
|
||||||
|
|
||||||
|
// Handle Auto Scroll
|
||||||
|
if (_scrollToBottom)
|
||||||
|
{
|
||||||
|
_scrollPosition.y = contentHeight - rect.height;
|
||||||
|
_scrollToBottom = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widgets.BeginScrollView(rect, ref _scrollPosition, viewRect);
|
||||||
|
|
||||||
|
float curY = 10f;
|
||||||
|
for (int i = 0; i < displayHistory.Count; i++)
|
||||||
|
{
|
||||||
|
var entry = displayHistory[i];
|
||||||
|
float h = heights[i];
|
||||||
|
Rect msgRect = new Rect(0, curY, width, h);
|
||||||
|
|
||||||
|
if (entry.role == "user")
|
||||||
|
{
|
||||||
|
DrawSenseiMessage(msgRect, entry.message);
|
||||||
|
}
|
||||||
|
else if (entry.role == "assistant")
|
||||||
|
{
|
||||||
|
DrawStudentMessage(msgRect, entry.message);
|
||||||
|
}
|
||||||
|
else if (entry.role == "tool" || entry.role == "toolcall")
|
||||||
|
{
|
||||||
|
// Only shown in DevMode (already filtered above)
|
||||||
|
DrawSystemMessage(msgRect, $"[Tool] {entry.message}");
|
||||||
|
}
|
||||||
|
else if (entry.role == "system")
|
||||||
|
{
|
||||||
|
DrawSystemMessage(msgRect, entry.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
curY += h + reducedSpacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_core.IsThinking)
|
||||||
|
{
|
||||||
|
Rect thinkingRect = new Rect(0, curY, width, 30f);
|
||||||
|
DrawThinkingIndicator(thinkingRect);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widgets.EndScrollView();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawFooter(Rect rect)
|
||||||
|
{
|
||||||
|
Widgets.DrawBoxSolid(rect, WulaLinkStyles.InputBarColor);
|
||||||
|
Widgets.DrawLineHorizontal(rect.x, rect.y, rect.width); // Top border
|
||||||
|
|
||||||
|
float padding = 8f;
|
||||||
|
float btnWidth = 40f;
|
||||||
|
float inputWidth = rect.width - btnWidth - (padding * 3);
|
||||||
|
|
||||||
|
Rect inputRect = new Rect(rect.x + padding, rect.y + padding, inputWidth, rect.height - (padding * 2));
|
||||||
|
Rect btnRect = new Rect(inputRect.xMax + padding, rect.y + padding, btnWidth, rect.height - (padding * 2));
|
||||||
|
|
||||||
|
// Input Field
|
||||||
|
string nextInput = Widgets.TextField(inputRect, _inputText);
|
||||||
|
if (nextInput != _inputText)
|
||||||
|
{
|
||||||
|
_inputText = nextInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send Button (Simulate Enter key or Click)
|
||||||
|
bool enterPressed = (Event.current.type == EventType.KeyDown && (Event.current.keyCode == KeyCode.Return || Event.current.keyCode == KeyCode.KeypadEnter) && GUI.GetNameOfFocusedControl() == "WulaInput");
|
||||||
|
|
||||||
|
bool sendClicked = DrawCustomButton(btnRect, ">", !string.IsNullOrWhiteSpace(_inputText));
|
||||||
|
if (sendClicked || enterPressed)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(_inputText))
|
||||||
|
{
|
||||||
|
bool wasFocused = GUI.GetNameOfFocusedControl() == "WulaInput";
|
||||||
|
_core.SendUserMessage(_inputText);
|
||||||
|
_inputText = "";
|
||||||
|
if (wasFocused) GUI.FocusControl("WulaInput");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle focus for Enter key to work
|
||||||
|
if (Mouse.IsOver(inputRect) && Event.current.type == EventType.MouseDown)
|
||||||
|
{
|
||||||
|
GUI.FocusControl("WulaInput");
|
||||||
|
}
|
||||||
|
GUI.SetNextControlName("WulaInput");
|
||||||
|
}
|
||||||
|
|
||||||
|
// =================================================================================
|
||||||
|
// Message Rendering Helpers
|
||||||
|
// =================================================================================
|
||||||
|
|
||||||
|
private float CalcMessageHeight(string text, float containerWidth)
|
||||||
|
{
|
||||||
|
float maxBubbleWidth = containerWidth * MaxBubbleWidthRatio;
|
||||||
|
Text.Font = WulaLinkStyles.MessageFont;
|
||||||
|
float textH = Text.CalcHeight(text, maxBubbleWidth - (BubblePadding * 2));
|
||||||
|
return Mathf.Max(textH + (BubblePadding * 2), AvatarSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawSenseiMessage(Rect rect, string text)
|
||||||
|
{
|
||||||
|
// Right aligned Blue Bubble
|
||||||
|
float maxBubbleWidth = rect.width * MaxBubbleWidthRatio;
|
||||||
|
Text.Font = WulaLinkStyles.MessageFont;
|
||||||
|
Vector2 textSize = Text.CalcSize(text);
|
||||||
|
float bubbleWidth = Mathf.Min(textSize.x + (BubblePadding * 2), maxBubbleWidth);
|
||||||
|
float bubbleHeight = rect.height;
|
||||||
|
|
||||||
|
Rect bubbleRect = new Rect(rect.xMax - bubbleWidth - 10f, rect.y, bubbleWidth, bubbleHeight);
|
||||||
|
|
||||||
|
// Draw Bubble
|
||||||
|
GUI.color = WulaLinkStyles.SenseiBubbleColor;
|
||||||
|
Widgets.DrawBoxSolid(bubbleRect, WulaLinkStyles.SenseiBubbleColor); // Rounded rect ideally
|
||||||
|
GUI.color = Color.white;
|
||||||
|
|
||||||
|
// Text
|
||||||
|
Rect textRect = bubbleRect.ContractedBy(BubblePadding);
|
||||||
|
GUI.color = WulaLinkStyles.SenseiTextColor;
|
||||||
|
Text.Anchor = TextAnchor.MiddleLeft;
|
||||||
|
Widgets.Label(textRect, text);
|
||||||
|
Text.Anchor = TextAnchor.UpperLeft;
|
||||||
|
GUI.color = Color.white;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawStudentMessage(Rect rect, string text)
|
||||||
|
{
|
||||||
|
// Left aligned White Bubble + Avatar
|
||||||
|
float avatarX = 10f;
|
||||||
|
Rect avatarRect = new Rect(avatarX, rect.y, AvatarSize, AvatarSize);
|
||||||
|
|
||||||
|
// Avatar
|
||||||
|
int expId = _core?.ExpressionId ?? 1;
|
||||||
|
string portraitPath = _def.portraitPath ?? $"Wula/Events/Portraits/WULA_Legion_{expId}";
|
||||||
|
if (expId > 1 && _def.portraitPath == null) // If using default Legion set
|
||||||
|
{
|
||||||
|
portraitPath = $"Wula/Events/Portraits/WULA_Legion_{expId}";
|
||||||
|
}
|
||||||
|
|
||||||
|
Texture2D portrait = ContentFinder<Texture2D>.Get(portraitPath, false);
|
||||||
|
if (portrait != null)
|
||||||
|
{
|
||||||
|
GUI.DrawTexture(avatarRect, portrait); // Needs circle mask ideally
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Widgets.DrawBoxSolid(avatarRect, Color.gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
float maxBubbleWidth = rect.width * MaxBubbleWidthRatio;
|
||||||
|
float bubbleX = avatarRect.xMax + 10f;
|
||||||
|
|
||||||
|
Text.Font = WulaLinkStyles.MessageFont;
|
||||||
|
Vector2 textSize = Text.CalcSize(text);
|
||||||
|
float bubbleWidth = Mathf.Min(textSize.x + (BubblePadding * 2), maxBubbleWidth);
|
||||||
|
float bubbleHeight = rect.height; // Height was pre-calculated
|
||||||
|
|
||||||
|
Rect bubbleRect = new Rect(bubbleX, rect.y, bubbleWidth, bubbleHeight);
|
||||||
|
|
||||||
|
// Draw Bubble
|
||||||
|
GUI.color = WulaLinkStyles.StudentStrokeColor;
|
||||||
|
Widgets.DrawBox(bubbleRect, 1);
|
||||||
|
GUI.color = WulaLinkStyles.StudentBubbleColor;
|
||||||
|
Widgets.DrawBoxSolid(bubbleRect, WulaLinkStyles.StudentBubbleColor);
|
||||||
|
GUI.color = Color.white;
|
||||||
|
|
||||||
|
// Text
|
||||||
|
Rect textRect = bubbleRect.ContractedBy(BubblePadding);
|
||||||
|
GUI.color = WulaLinkStyles.StudentTextColor;
|
||||||
|
Text.Anchor = TextAnchor.MiddleLeft;
|
||||||
|
Widgets.Label(textRect, text);
|
||||||
|
Text.Anchor = TextAnchor.UpperLeft;
|
||||||
|
GUI.color = Color.white;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawSystemMessage(Rect rect, string text)
|
||||||
|
{
|
||||||
|
// Centered gray text or Pink box
|
||||||
|
if (text.Contains("[Tool]")) return; // Skip logic log in main view if needed, but here we draw minimal
|
||||||
|
|
||||||
|
GUI.color = Color.gray;
|
||||||
|
Text.Font = GameFont.Tiny;
|
||||||
|
Text.Anchor = TextAnchor.MiddleCenter;
|
||||||
|
Widgets.Label(rect, text);
|
||||||
|
Text.Anchor = TextAnchor.UpperLeft;
|
||||||
|
GUI.color = Color.white;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawThinkingIndicator(Rect rect)
|
||||||
|
{
|
||||||
|
Text.Anchor = TextAnchor.MiddleLeft;
|
||||||
|
GUI.color = Color.gray;
|
||||||
|
Rect iconRect = new Rect(rect.x + 60f, rect.y, 24f, 24f);
|
||||||
|
Rect labelRect = new Rect(iconRect.xMax + 5f, rect.y, 200f, 24f);
|
||||||
|
|
||||||
|
// Draw a simple box as thinking indicator if TexUI is missing
|
||||||
|
Widgets.DrawBoxSolid(iconRect, Color.gray);
|
||||||
|
Widgets.Label(labelRect, "P.I.A is thinking...");
|
||||||
|
|
||||||
|
Text.Anchor = TextAnchor.UpperLeft;
|
||||||
|
GUI.color = Color.white;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool DrawCustomButton(Rect rect, string label, bool isEnabled)
|
||||||
|
{
|
||||||
|
bool isMouseOver = Mouse.IsOver(rect);
|
||||||
|
Color originalColor = GUI.color;
|
||||||
|
TextAnchor originalAnchor = Text.Anchor;
|
||||||
|
GameFont originalFont = Text.Font;
|
||||||
|
|
||||||
|
Color buttonColor;
|
||||||
|
Color textColor;
|
||||||
|
if (!isEnabled)
|
||||||
|
{
|
||||||
|
buttonColor = Dialog_CustomDisplay.CustomButtonDisabledColor;
|
||||||
|
textColor = Dialog_CustomDisplay.CustomButtonTextDisabledColor;
|
||||||
|
}
|
||||||
|
else if (isMouseOver)
|
||||||
|
{
|
||||||
|
buttonColor = Dialog_CustomDisplay.CustomButtonHoverColor;
|
||||||
|
textColor = Dialog_CustomDisplay.CustomButtonTextHoverColor;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
buttonColor = Dialog_CustomDisplay.CustomButtonNormalColor;
|
||||||
|
textColor = Dialog_CustomDisplay.CustomButtonTextNormalColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
GUI.color = buttonColor;
|
||||||
|
Widgets.DrawBoxSolid(rect, buttonColor);
|
||||||
|
Widgets.DrawBox(rect, 1);
|
||||||
|
|
||||||
|
GUI.color = textColor;
|
||||||
|
Text.Anchor = TextAnchor.MiddleCenter;
|
||||||
|
Text.Font = GameFont.Tiny;
|
||||||
|
Widgets.Label(rect.ContractedBy(4f), label);
|
||||||
|
|
||||||
|
GUI.color = originalColor;
|
||||||
|
Text.Anchor = originalAnchor;
|
||||||
|
Text.Font = originalFont;
|
||||||
|
|
||||||
|
if (!isEnabled)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Widgets.ButtonInvisible(rect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
using System;
|
||||||
|
using UnityEngine;
|
||||||
|
using Verse;
|
||||||
|
using RimWorld;
|
||||||
|
using WulaFallenEmpire.EventSystem.AI;
|
||||||
|
|
||||||
|
namespace WulaFallenEmpire.EventSystem.AI.UI
|
||||||
|
{
|
||||||
|
public class Overlay_WulaLink_Notification : Window
|
||||||
|
{
|
||||||
|
private string _message;
|
||||||
|
private int _tickCreated;
|
||||||
|
private const int DisplayTicks = 300; // 5 seconds
|
||||||
|
|
||||||
|
public override Vector2 InitialSize => new Vector2(300f, 80f);
|
||||||
|
|
||||||
|
public Overlay_WulaLink_Notification(string message)
|
||||||
|
{
|
||||||
|
_message = message;
|
||||||
|
_tickCreated = Find.TickManager.TicksGame;
|
||||||
|
|
||||||
|
// Transient properties
|
||||||
|
this.layer = WindowLayer.Super; // Topmost
|
||||||
|
this.closeOnClickedOutside = false;
|
||||||
|
this.forcePause = false;
|
||||||
|
this.absorbInputAroundWindow = false;
|
||||||
|
this.doWindowBackground = false; // Custom bg
|
||||||
|
this.drawShadow = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void DoWindowContents(Rect inRect)
|
||||||
|
{
|
||||||
|
// Auto Close after time
|
||||||
|
if (Find.TickManager.TicksGame > _tickCreated + DisplayTicks)
|
||||||
|
{
|
||||||
|
Close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw HUD Notification Style
|
||||||
|
Widgets.DrawBoxSolid(inRect, new Color(0.1f, 0.1f, 0.1f, 0.9f));
|
||||||
|
GUI.color = WulaLinkStyles.SystemAccentColor;
|
||||||
|
Widgets.DrawBox(inRect, 1);
|
||||||
|
GUI.color = Color.white;
|
||||||
|
|
||||||
|
Rect iconRect = new Rect(10f, 10f, 30f, 30f);
|
||||||
|
Rect titleRect = new Rect(50f, 10f, 200f, 20f);
|
||||||
|
Rect textRect = new Rect(50f, 30f, 240f, 40f);
|
||||||
|
|
||||||
|
// Icon (Warning / Info)
|
||||||
|
Widgets.DrawBoxSolid(iconRect, WulaLinkStyles.SystemAccentColor);
|
||||||
|
GUI.color = Color.black;
|
||||||
|
Text.Anchor = TextAnchor.MiddleCenter;
|
||||||
|
Text.Font = GameFont.Medium;
|
||||||
|
Widgets.Label(iconRect, "!");
|
||||||
|
GUI.color = Color.white;
|
||||||
|
|
||||||
|
// Title
|
||||||
|
Text.Anchor = TextAnchor.UpperLeft;
|
||||||
|
Text.Font = GameFont.Tiny;
|
||||||
|
GUI.color = Color.yellow;
|
||||||
|
Widgets.Label(titleRect, "WULA LINK :: NEW ALERT");
|
||||||
|
GUI.color = Color.white;
|
||||||
|
|
||||||
|
// Text
|
||||||
|
Text.Font = GameFont.Small;
|
||||||
|
string truncated = _message.Length > 40 ? _message.Substring(0, 38) + "..." : _message;
|
||||||
|
Widgets.Label(textRect, truncated);
|
||||||
|
|
||||||
|
// Click to Open/Expand
|
||||||
|
if (Widgets.ButtonInvisible(inRect))
|
||||||
|
{
|
||||||
|
OpenWulaLink();
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OpenWulaLink()
|
||||||
|
{
|
||||||
|
// Find existing or open new
|
||||||
|
var existing = Find.WindowStack.WindowOfType<Overlay_WulaLink>();
|
||||||
|
if (existing != null)
|
||||||
|
{
|
||||||
|
existing.Expand();
|
||||||
|
Find.WindowStack.Notify_ManuallySetFocus(existing);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Create new if not exists
|
||||||
|
var core = AIIntelligenceCore.Instance;
|
||||||
|
// Without EventDef we can't easily open.
|
||||||
|
// Assuming notification implies active core state.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
53
Source/WulaFallenEmpire/EventSystem/AI/UI/WulaLinkStyles.cs
Normal file
53
Source/WulaFallenEmpire/EventSystem/AI/UI/WulaLinkStyles.cs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
using System;
|
||||||
|
using UnityEngine;
|
||||||
|
using Verse;
|
||||||
|
|
||||||
|
namespace WulaFallenEmpire.EventSystem.AI.UI
|
||||||
|
{
|
||||||
|
[StaticConstructorOnStartup]
|
||||||
|
public static class WulaLinkStyles
|
||||||
|
{
|
||||||
|
// =================================================================================
|
||||||
|
// Colors
|
||||||
|
// =================================================================================
|
||||||
|
|
||||||
|
// Background Colors - Semi-transparent red theme
|
||||||
|
public static readonly Color BackgroundColor = new Color(0.2f, 0.05f, 0.05f, 0.85f);
|
||||||
|
public static readonly Color HeaderColor = new Color(0.5f, 0.2f, 0.2f, 1f);
|
||||||
|
public static readonly Color InputBarColor = new Color(0.16f, 0.08f, 0.08f, 0.95f);
|
||||||
|
public static readonly Color SystemAccentColor = new Color(0.6f, 0.2f, 0.2f, 1f);
|
||||||
|
|
||||||
|
// Message Bubble Colors - Matching Dialog_CustomDisplay button style
|
||||||
|
public static readonly Color SenseiBubbleColor = new Color(0.5f, 0.2f, 0.2f, 1f);
|
||||||
|
public static readonly Color StudentBubbleColor = new Color(0.15f, 0.15f, 0.2f, 0.9f);
|
||||||
|
public static readonly Color StudentStrokeColor = new Color(0.6f, 0.3f, 0.3f, 1f);
|
||||||
|
|
||||||
|
// Text Colors
|
||||||
|
public static readonly Color TextColor = new Color32(220, 220, 220, 255);
|
||||||
|
public static readonly Color SenseiTextColor = new Color32(240, 240, 240, 255);
|
||||||
|
public static readonly Color StudentTextColor = new Color32(230, 230, 230, 255);
|
||||||
|
public static readonly Color InputBorderColor = new Color32(60, 60, 60, 255);
|
||||||
|
|
||||||
|
// =================================================================================
|
||||||
|
// Fonts
|
||||||
|
// =================================================================================
|
||||||
|
public static GameFont MessageFont = GameFont.Small;
|
||||||
|
public static GameFont HeaderFont = GameFont.Medium;
|
||||||
|
|
||||||
|
// =================================================================================
|
||||||
|
// Textures
|
||||||
|
// =================================================================================
|
||||||
|
public static readonly Texture2D TexCircleMask;
|
||||||
|
public static readonly Texture2D TexSendIcon;
|
||||||
|
public static readonly Texture2D TexPaperClip;
|
||||||
|
public static readonly Texture2D TexWhite;
|
||||||
|
|
||||||
|
static WulaLinkStyles()
|
||||||
|
{
|
||||||
|
TexCircleMask = ContentFinder<Texture2D>.Get("Base/UI/WulaLink/CircleMask", false);
|
||||||
|
TexSendIcon = ContentFinder<Texture2D>.Get("Base/UI/WulaLink/Send", false);
|
||||||
|
TexPaperClip = ContentFinder<Texture2D>.Get("Base/UI/WulaLink/Clip", false);
|
||||||
|
TexWhite = SolidColorMaterials.NewSolidColorTexture(Color.white);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,7 +22,16 @@ namespace WulaFallenEmpire
|
|||||||
EventDef eventDef = DefDatabase<EventDef>.GetNamed(defName, false);
|
EventDef eventDef = DefDatabase<EventDef>.GetNamed(defName, false);
|
||||||
if (eventDef != null)
|
if (eventDef != null)
|
||||||
{
|
{
|
||||||
Find.WindowStack.Add(new Dialog_AIConversation(eventDef));
|
var existing = Find.WindowStack.WindowOfType<Overlay_WulaLink>();
|
||||||
|
if (existing != null)
|
||||||
|
{
|
||||||
|
existing.Expand();
|
||||||
|
Find.WindowStack.Notify_ManuallySetFocus(existing);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Find.WindowStack.Add(new Overlay_WulaLink(eventDef));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
29
Source/WulaFallenEmpire/build_log.txt
Normal file
29
Source/WulaFallenEmpire/build_log.txt
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
?????????????????
|
||||||
|
??????????????????????????????????
|
||||||
|
C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\EventSystem\AI\UI\Overlay_WulaLink.cs(69,32): error CS0234: ????????ulaFallenEmpire.EventSystem.AI.UI??????????????????????creenWidth????????????????) [C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\WulaFallenEmpire.csproj]
|
||||||
|
C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\EventSystem\AI\UI\Overlay_WulaLink.cs(70,32): error CS0234: ????????ulaFallenEmpire.EventSystem.AI.UI??????????????????????creenHeight????????????????) [C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\WulaFallenEmpire.csproj]
|
||||||
|
C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\EventSystem\AI\UI\Overlay_WulaLink.cs(83,32): error CS0234: ????????ulaFallenEmpire.EventSystem.AI.UI??????????????????????creenWidth????????????????) [C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\WulaFallenEmpire.csproj]
|
||||||
|
C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\EventSystem\AI\UI\Overlay_WulaLink.cs(85,32): error CS0234: ????????ulaFallenEmpire.EventSystem.AI.UI??????????????????????creenWidth????????????????) [C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\WulaFallenEmpire.csproj]
|
||||||
|
C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\EventSystem\AI\UI\Overlay_WulaLink.cs(87,13): error CS0103: ???????????????????ringToFront??[C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\WulaFallenEmpire.csproj]
|
||||||
|
C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\EventSystem\AI\AIIntelligenceCore.cs(334,50): warning CS1998: ???????????"await" ??????????????????????????? "await" ??????????????API ???????????"await Task.Run(...)" ?????????????????? CPU ???????[C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\WulaFallenEmpire.csproj]
|
||||||
|
C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\EventSystem\AI\AIIntelligenceCore.cs(98,24): warning CS0414: ?????IIntelligenceCore._memoryContextQuery???????????????????????[C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\WulaFallenEmpire.csproj]
|
||||||
|
C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\EventSystem\AI\AIIntelligenceCore.cs(90,22): warning CS0414: ?????IIntelligenceCore._actionRetryUsed???????????????????????[C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\WulaFallenEmpire.csproj]
|
||||||
|
C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\EventSystem\AI\UI\Dialog_AIConversation.cs(20,21): warning CS0414: ?????ialog_AIConversation._currentPortraitId???????????????????????[C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\WulaFallenEmpire.csproj]
|
||||||
|
C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\EventSystem\AI\UI\Overlay_WulaLink.cs(21,23): warning CS0414: ?????verlay_WulaLink._lastMessageHeight???????????????????????[C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\WulaFallenEmpire.csproj]
|
||||||
|
|
||||||
|
????????
|
||||||
|
|
||||||
|
C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\EventSystem\AI\AIIntelligenceCore.cs(334,50): warning CS1998: ???????????"await" ??????????????????????????? "await" ??????????????API ???????????"await Task.Run(...)" ?????????????????? CPU ???????[C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\WulaFallenEmpire.csproj]
|
||||||
|
C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\EventSystem\AI\AIIntelligenceCore.cs(98,24): warning CS0414: ?????IIntelligenceCore._memoryContextQuery???????????????????????[C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\WulaFallenEmpire.csproj]
|
||||||
|
C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\EventSystem\AI\AIIntelligenceCore.cs(90,22): warning CS0414: ?????IIntelligenceCore._actionRetryUsed???????????????????????[C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\WulaFallenEmpire.csproj]
|
||||||
|
C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\EventSystem\AI\UI\Dialog_AIConversation.cs(20,21): warning CS0414: ?????ialog_AIConversation._currentPortraitId???????????????????????[C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\WulaFallenEmpire.csproj]
|
||||||
|
C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\EventSystem\AI\UI\Overlay_WulaLink.cs(21,23): warning CS0414: ?????verlay_WulaLink._lastMessageHeight???????????????????????[C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\WulaFallenEmpire.csproj]
|
||||||
|
C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\EventSystem\AI\UI\Overlay_WulaLink.cs(69,32): error CS0234: ????????ulaFallenEmpire.EventSystem.AI.UI??????????????????????creenWidth????????????????) [C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\WulaFallenEmpire.csproj]
|
||||||
|
C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\EventSystem\AI\UI\Overlay_WulaLink.cs(70,32): error CS0234: ????????ulaFallenEmpire.EventSystem.AI.UI??????????????????????creenHeight????????????????) [C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\WulaFallenEmpire.csproj]
|
||||||
|
C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\EventSystem\AI\UI\Overlay_WulaLink.cs(83,32): error CS0234: ????????ulaFallenEmpire.EventSystem.AI.UI??????????????????????creenWidth????????????????) [C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\WulaFallenEmpire.csproj]
|
||||||
|
C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\EventSystem\AI\UI\Overlay_WulaLink.cs(85,32): error CS0234: ????????ulaFallenEmpire.EventSystem.AI.UI??????????????????????creenWidth????????????????) [C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\WulaFallenEmpire.csproj]
|
||||||
|
C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\EventSystem\AI\UI\Overlay_WulaLink.cs(87,13): error CS0103: ???????????????????ringToFront??[C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\WulaFallenEmpire.csproj]
|
||||||
|
5 ?????
|
||||||
|
5 ?????
|
||||||
|
|
||||||
|
?????? 00:00:00.41
|
||||||
BIN
Source/WulaFallenEmpire/build_output.txt
Normal file
BIN
Source/WulaFallenEmpire/build_output.txt
Normal file
Binary file not shown.
23
Source/WulaFallenEmpire/trace.txt
Normal file
23
Source/WulaFallenEmpire/trace.txt
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
姝e湪纭畾瑕佽繕鍘熺殑椤圭洰鈥?
|
||||||
|
鏃犲彲鎵ц鎿嶄綔銆傛寚瀹氱殑椤圭洰鍧囦笉鍖呭惈鍙繕鍘熺殑鍖呫€?
|
||||||
|
C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\EventSystem\AI\AIIntelligenceCore.cs(323,50): warning CS1998: 姝ゅ紓姝ユ柟娉曠己灏?"await" 杩愮畻绗︼紝灏嗕互鍚屾鏂瑰紡杩愯銆傝鑰冭檻浣跨敤 "await" 杩愮畻绗︾瓑寰呴潪闃绘鐨?API 璋冪敤锛屾垨鑰呬娇鐢?"await Task.Run(...)" 鍦ㄥ悗鍙扮嚎绋嬩笂鎵ц鍗犵敤澶ч噺 CPU 鐨勫伐浣溿€?[C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\WulaFallenEmpire.csproj]
|
||||||
|
C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\EventSystem\AI\Tools\Tool_GetRecentNotifications.cs(116,34): error CS1061: 鈥淒ialog_AIConversation鈥濇湭鍖呭惈鈥淕etHistorySnapshot鈥濈殑瀹氫箟锛屽苟涓旀壘涓嶅埌鍙帴鍙楃涓€涓€淒ialog_AIConversation鈥濈被鍨嬪弬鏁扮殑鍙闂墿灞曟柟娉曗€淕etHistorySnapshot鈥?鏄惁缂哄皯 using 鎸囦护鎴栫▼搴忛泦寮曠敤?) [C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\WulaFallenEmpire.csproj]
|
||||||
|
C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\EventSystem\AI\Tools\Tool_GetRecentNotifications.cs(117,36): error CS0019: 杩愮畻绗︹€?=鈥濇棤娉曞簲鐢ㄤ簬鈥滄柟娉曠粍鈥濆拰鈥渋nt鈥濈被鍨嬬殑鎿嶄綔鏁?[C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\WulaFallenEmpire.csproj]
|
||||||
|
C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\EventSystem\AI\Tools\Tool_GetRecentNotifications.cs(120,26): error CS0019: 杩愮畻绗︹€?鈥濇棤娉曞簲鐢ㄤ簬鈥滄柟娉曠粍鈥濆拰鈥渋nt鈥濈被鍨嬬殑鎿嶄綔鏁?[C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\WulaFallenEmpire.csproj]
|
||||||
|
C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\EventSystem\AI\UI\Overlay_WulaLink.cs(21,23): warning CS0414: 瀛楁鈥淥verlay_WulaLink._lastMessageHeight鈥濆凡琚祴鍊硷紝浣嗕粠鏈娇鐢ㄨ繃瀹冪殑鍊?[C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\WulaFallenEmpire.csproj]
|
||||||
|
C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\EventSystem\AI\AIIntelligenceCore.cs(79,22): warning CS0414: 瀛楁鈥淎IIntelligenceCore._actionRetryUsed鈥濆凡琚祴鍊硷紝浣嗕粠鏈娇鐢ㄨ繃瀹冪殑鍊?[C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\WulaFallenEmpire.csproj]
|
||||||
|
C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\EventSystem\AI\AIIntelligenceCore.cs(87,24): warning CS0414: 瀛楁鈥淎IIntelligenceCore._memoryContextQuery鈥濆凡琚祴鍊硷紝浣嗕粠鏈娇鐢ㄨ繃瀹冪殑鍊?[C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\WulaFallenEmpire.csproj]
|
||||||
|
|
||||||
|
鐢熸垚澶辫触銆?
|
||||||
|
|
||||||
|
C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\EventSystem\AI\AIIntelligenceCore.cs(323,50): warning CS1998: 姝ゅ紓姝ユ柟娉曠己灏?"await" 杩愮畻绗︼紝灏嗕互鍚屾鏂瑰紡杩愯銆傝鑰冭檻浣跨敤 "await" 杩愮畻绗︾瓑寰呴潪闃绘鐨?API 璋冪敤锛屾垨鑰呬娇鐢?"await Task.Run(...)" 鍦ㄥ悗鍙扮嚎绋嬩笂鎵ц鍗犵敤澶ч噺 CPU 鐨勫伐浣溿€?[C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\WulaFallenEmpire.csproj]
|
||||||
|
C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\EventSystem\AI\UI\Overlay_WulaLink.cs(21,23): warning CS0414: 瀛楁鈥淥verlay_WulaLink._lastMessageHeight鈥濆凡琚祴鍊硷紝浣嗕粠鏈娇鐢ㄨ繃瀹冪殑鍊?[C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\WulaFallenEmpire.csproj]
|
||||||
|
C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\EventSystem\AI\AIIntelligenceCore.cs(79,22): warning CS0414: 瀛楁鈥淎IIntelligenceCore._actionRetryUsed鈥濆凡琚祴鍊硷紝浣嗕粠鏈娇鐢ㄨ繃瀹冪殑鍊?[C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\WulaFallenEmpire.csproj]
|
||||||
|
C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\EventSystem\AI\AIIntelligenceCore.cs(87,24): warning CS0414: 瀛楁鈥淎IIntelligenceCore._memoryContextQuery鈥濆凡琚祴鍊硷紝浣嗕粠鏈娇鐢ㄨ繃瀹冪殑鍊?[C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\WulaFallenEmpire.csproj]
|
||||||
|
C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\EventSystem\AI\Tools\Tool_GetRecentNotifications.cs(116,34): error CS1061: 鈥淒ialog_AIConversation鈥濇湭鍖呭惈鈥淕etHistorySnapshot鈥濈殑瀹氫箟锛屽苟涓旀壘涓嶅埌鍙帴鍙楃涓€涓€淒ialog_AIConversation鈥濈被鍨嬪弬鏁扮殑鍙闂墿灞曟柟娉曗€淕etHistorySnapshot鈥?鏄惁缂哄皯 using 鎸囦护鎴栫▼搴忛泦寮曠敤?) [C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\WulaFallenEmpire.csproj]
|
||||||
|
C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\EventSystem\AI\Tools\Tool_GetRecentNotifications.cs(117,36): error CS0019: 杩愮畻绗︹€?=鈥濇棤娉曞簲鐢ㄤ簬鈥滄柟娉曠粍鈥濆拰鈥渋nt鈥濈被鍨嬬殑鎿嶄綔鏁?[C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\WulaFallenEmpire.csproj]
|
||||||
|
C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\EventSystem\AI\Tools\Tool_GetRecentNotifications.cs(120,26): error CS0019: 杩愮畻绗︹€?鈥濇棤娉曞簲鐢ㄤ簬鈥滄柟娉曠粍鈥濆拰鈥渋nt鈥濈被鍨嬬殑鎿嶄綔鏁?[C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\WulaFallenEmpire.csproj]
|
||||||
|
4 涓鍛?
|
||||||
|
3 涓敊璇?
|
||||||
|
|
||||||
|
宸茬敤鏃堕棿 00:00:00.48
|
||||||
1
Tools/momotalk
Submodule
1
Tools/momotalk
Submodule
Submodule Tools/momotalk added at 2a3e352738
150
Tools/task.md.resolved
Normal file
150
Tools/task.md.resolved
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
# WulaLink UI 修复任务 - Codex 交接文档
|
||||||
|
|
||||||
|
## 项目概述
|
||||||
|
RimWorld Mod: [WulaFallenEmpire](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire)
|
||||||
|
目标: 实现 MomoTalk 风格的悬浮 AI 聊天 UI (WulaLink),同时确保原有大 UI 不被破坏。
|
||||||
|
|
||||||
|
## 关键文件路径
|
||||||
|
```
|
||||||
|
C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\
|
||||||
|
├── EventSystem\
|
||||||
|
│ ├── Dialog_CustomDisplay.cs # 基类,包含正确的按钮样式和布局逻辑
|
||||||
|
│ └── AI\
|
||||||
|
│ ├── AIIntelligenceCore.cs # AI 核心逻辑 (WorldComponent) - 已完成
|
||||||
|
│ ├── DebugActions_WulaLink.cs # Debug 入口
|
||||||
|
│ └── UI\
|
||||||
|
│ ├── Dialog_AIConversation.cs # 大 UI - 需要修复布局
|
||||||
|
│ ├── Overlay_WulaLink.cs # 小悬浮框 - 需要修复样式
|
||||||
|
│ ├── Overlay_WulaLink_Notification.cs # 通知弹窗 - 已完成
|
||||||
|
│ └── WulaLinkStyles.cs # 样式定义 - 需要调整颜色
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 问题 1: 大 UI (Dialog_AIConversation) 布局损坏
|
||||||
|
|
||||||
|
### 现状
|
||||||
|
[Dialog_AIConversation](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs#108-133) 继承自 [Dialog_CustomDisplay](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/Dialog_CustomDisplay.cs#10-668),但 [DoWindowContents](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs#1177-1328) 被完全重写,丢失了原有的沉浸式布局。
|
||||||
|
|
||||||
|
### 期望效果 (参考用户截图)
|
||||||
|
- 左侧: 大尺寸立绘 (占据约 60% 窗口)
|
||||||
|
- 右下: 半透明暗红色面板,包含:
|
||||||
|
- 标题 "「军团」,P.I.A"
|
||||||
|
- 描述文本区域
|
||||||
|
- 底部按钮 "打开通讯频道" (使用 [DrawCustomButton](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/Dialog_CustomDisplay.cs#388-449) 样式)
|
||||||
|
- 点击按钮后进入聊天模式 (显示对话历史)
|
||||||
|
|
||||||
|
### 修复方案
|
||||||
|
恢复 `base.DoWindowContents(inRect)` 的调用,或手动复制 `Dialog_CustomDisplay.DoWindowContents` 的布局逻辑。
|
||||||
|
|
||||||
|
关键代码参考 ([Dialog_CustomDisplay.cs](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/Dialog_CustomDisplay.cs) 第 144-280 行):
|
||||||
|
```csharp
|
||||||
|
// 1. 绘制背景
|
||||||
|
if (background != null) GUI.DrawTexture(inRect, background, ScaleMode.StretchToFill);
|
||||||
|
|
||||||
|
// 2. 立绘 (Config.showPortrait)
|
||||||
|
Rect portraitRect = Config.GetScaledRect(Config.portraitSize, inRect);
|
||||||
|
|
||||||
|
// 3. 标题 (Config.showLabel)
|
||||||
|
Rect labelRect = Config.GetScaledRect(Config.labelSize, inRect);
|
||||||
|
|
||||||
|
// 4. 描述 (Config.showDescriptions)
|
||||||
|
Rect descriptionRect = Config.GetScaledRect(Config.descriptionsSize, inRect);
|
||||||
|
|
||||||
|
// 5. 选项按钮 (Config.showOptions)
|
||||||
|
Rect optionsRect = Config.GetScaledRect(Config.optionsListSize, inRect);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 问题 2: 小悬浮框 (Overlay_WulaLink) 样式问题
|
||||||
|
|
||||||
|
### 2.1 背景颜色错误
|
||||||
|
**现状**: 使用纯黑背景 `new Color(10, 10, 12, 255)`
|
||||||
|
**期望**: 半透明暗红色,与大 UI 一致
|
||||||
|
|
||||||
|
修改 [WulaLinkStyles.cs](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/AI/UI/WulaLinkStyles.cs):
|
||||||
|
```csharp
|
||||||
|
// 修改前
|
||||||
|
public static readonly Color BackgroundColor = new Color32(10, 10, 12, 255);
|
||||||
|
|
||||||
|
// 修改后
|
||||||
|
public static readonly Color BackgroundColor = new Color(0.2f, 0.05f, 0.05f, 0.85f); // 半透明暗红
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 消息间距过大
|
||||||
|
**原因**: `role == "tool"` 的消息也被渲染,占用空间但不显示内容。
|
||||||
|
|
||||||
|
修改 [Overlay_WulaLink.cs](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/AI/UI/Overlay_WulaLink.cs) 的 [DrawMessageList](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/AI/UI/Overlay_WulaLink.cs#265-344) 方法:
|
||||||
|
```csharp
|
||||||
|
// 在 foreach 循环中添加过滤
|
||||||
|
foreach (var msg in history)
|
||||||
|
{
|
||||||
|
// 跳过工具调用消息
|
||||||
|
if (msg.role == "tool") continue;
|
||||||
|
|
||||||
|
// ... 其余渲染逻辑
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 气泡无圆角
|
||||||
|
**问题**: RimWorld 原生 `Widgets.DrawBoxSolid` 不支持圆角。
|
||||||
|
**方案**:
|
||||||
|
- 方案 A: 使用 9-slice 圆角贴图 (`Textures/UI/BubbleRounded.png`)
|
||||||
|
- 方案 B: 接受直角,但添加边框使其更精致
|
||||||
|
|
||||||
|
### 2.4 头像无圆形处理
|
||||||
|
**方案**: 使用圆形遮罩贴图覆盖在头像上,或创建 `Textures/UI/AvatarMask.png`
|
||||||
|
|
||||||
|
### 2.5 按钮样式不一致
|
||||||
|
**现状**: 使用默认 `Widgets.ButtonText`
|
||||||
|
**期望**: 使用 `Dialog_CustomDisplay.DrawCustomButton` 样式
|
||||||
|
|
||||||
|
修改 [Overlay_WulaLink.cs](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/AI/UI/Overlay_WulaLink.cs) 的 [DrawFooter](file:///C:/Steam/steamapps/common/RimWorld/Mods/3516260226/Source/WulaFallenEmpire/EventSystem/AI/UI/Overlay_WulaLink.cs#345-385):
|
||||||
|
```csharp
|
||||||
|
// 修改前
|
||||||
|
if (Widgets.ButtonText(btnRect, ">"))
|
||||||
|
|
||||||
|
// 修改后 (需要静态化或复制 DrawCustomButton 方法)
|
||||||
|
DrawCustomButton(btnRect, ">", isEnabled: true);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 问题 3: 样式统一 (WulaLinkStyles.cs)
|
||||||
|
|
||||||
|
需要调整以下颜色以匹配大 UI 的"军团"风格:
|
||||||
|
```csharp
|
||||||
|
// Header: 深红色,与大 UI 标题栏一致
|
||||||
|
public static readonly Color HeaderColor = new Color(0.5f, 0.15f, 0.15f, 1f);
|
||||||
|
|
||||||
|
// 背景: 半透明暗红
|
||||||
|
public static readonly Color BackgroundColor = new Color(0.2f, 0.05f, 0.05f, 0.85f);
|
||||||
|
|
||||||
|
// AI 气泡: 深灰带红色边框
|
||||||
|
public static readonly Color StudentBubbleColor = new Color(0.15f, 0.12f, 0.12f, 1f);
|
||||||
|
public static readonly Color StudentStrokeColor = new Color(0.6f, 0.2f, 0.2f, 1f);
|
||||||
|
|
||||||
|
// 玩家气泡: 暗红色
|
||||||
|
public static readonly Color SenseiBubbleColor = new Color(0.4f, 0.15f, 0.15f, 1f);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 验证清单
|
||||||
|
- [ ] 大 UI 显示正确的立绘、标题、描述和按钮布局
|
||||||
|
- [ ] 小悬浮框背景为半透明暗红色
|
||||||
|
- [ ] 消息之间无多余空白
|
||||||
|
- [ ] 按钮使用暗红色自定义样式
|
||||||
|
- [ ] (可选) 气泡有圆角
|
||||||
|
- [ ] (可选) 头像为圆形
|
||||||
|
|
||||||
|
## 编译命令
|
||||||
|
```powershell
|
||||||
|
dotnet build C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\WulaFallenEmpire.csproj
|
||||||
|
```
|
||||||
|
|
||||||
|
## 测试入口
|
||||||
|
1. 启动 RimWorld,加载存档
|
||||||
|
2. 打开 Debug Actions Menu
|
||||||
|
3. 搜索 "WulaLink" 并点击 "Open WulaLink UI"
|
||||||
140
Tools/task_handoff.md
Normal file
140
Tools/task_handoff.md
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
# WulaLink 任务交接文档
|
||||||
|
|
||||||
|
## 当前状态:需要创建 AIIntelligenceCore.cs
|
||||||
|
|
||||||
|
### 背景
|
||||||
|
WulaLink 是一个 RimWorld Mod 中的 AI 对话系统,包含两个 UI:
|
||||||
|
1. **大 UI** (`Dialog_AIConversation`) - 全屏对话窗口
|
||||||
|
2. **小 UI** (`Overlay_WulaLink`) - 悬浮对话窗口
|
||||||
|
|
||||||
|
### 已完成的操作
|
||||||
|
1. ✅ 恢复了 `Dialog_AIConversation.cs` 为旧版自包含版本(从备份文件 `Tools/using System;.cs` 复制)
|
||||||
|
2. ✅ 删除了损坏的 `AIIntelligenceCore.cs`
|
||||||
|
3. ✅ 重写了 `WulaLinkStyles.cs`(颜色主题配置)
|
||||||
|
|
||||||
|
### 当前编译错误
|
||||||
|
```
|
||||||
|
error CS0246: 未能找到类型或命名空间名"AIIntelligenceCore"
|
||||||
|
```
|
||||||
|
|
||||||
|
以下文件引用了 `AIIntelligenceCore`:
|
||||||
|
- `Overlay_WulaLink.cs` (第13行, 第94行)
|
||||||
|
- `Overlay_WulaLink_Notification.cs` (第89行)
|
||||||
|
- `Tool_ChangeExpression.cs` (第24行)
|
||||||
|
- `Tool_GetRecentNotifications.cs` (第113行)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 需要完成的任务
|
||||||
|
|
||||||
|
### 任务:创建 AIIntelligenceCore.cs
|
||||||
|
|
||||||
|
**路径**: `Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs`
|
||||||
|
|
||||||
|
**要求**:
|
||||||
|
1. 必须是 `WorldComponent`,类名 `AIIntelligenceCore`
|
||||||
|
2. 提供 `static Instance` 属性供外部访问
|
||||||
|
3. 从 `Dialog_AIConversation`(备份文件 `Tools/using System;.cs`)提取 AI 核心逻辑
|
||||||
|
4. 暴露事件/回调供 UI 使用
|
||||||
|
|
||||||
|
**必须包含的公共接口**(根据现有代码引用):
|
||||||
|
```csharp
|
||||||
|
public class AIIntelligenceCore : WorldComponent
|
||||||
|
{
|
||||||
|
// 静态实例
|
||||||
|
public static AIIntelligenceCore Instance { get; private set; }
|
||||||
|
|
||||||
|
// 事件回调
|
||||||
|
public event Action<string> OnMessageReceived;
|
||||||
|
public event Action<bool> OnThinkingStateChanged;
|
||||||
|
public event Action<int> OnExpressionChanged;
|
||||||
|
|
||||||
|
// 公共属性
|
||||||
|
public int ExpressionId { get; }
|
||||||
|
public bool IsThinking { get; }
|
||||||
|
|
||||||
|
// 公共方法
|
||||||
|
public void InitializeConversation(string eventDefName);
|
||||||
|
public List<(string role, string message)> GetHistorySnapshot();
|
||||||
|
public void SetExpression(int id); // 供 Tool_ChangeExpression 调用
|
||||||
|
public void SendMessage(string text); // 供小 UI 调用
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**参考代码**:
|
||||||
|
- 备份文件 `Tools/using System;.cs` 包含完整的 AI 逻辑(1549行)
|
||||||
|
- 核心方法包括:
|
||||||
|
- `RunPhasedRequestAsync()` - 3阶段请求处理
|
||||||
|
- `ExecuteXmlToolsForPhase()` - 工具执行
|
||||||
|
- `BuildToolContext()` / `BuildReplyHistory()` - 上下文构建
|
||||||
|
- `ParseResponse()` - 响应解析
|
||||||
|
- `GetSystemInstruction()` / `GetToolSystemInstruction()` - 提示词生成
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 关键文件路径
|
||||||
|
|
||||||
|
```
|
||||||
|
C:\Steam\steamapps\common\RimWorld\Mods\3516260226\
|
||||||
|
├── Tools\
|
||||||
|
│ └── using System;.cs # 旧版 Dialog_AIConversation 备份(包含完整 AI 逻辑)
|
||||||
|
└── Source\WulaFallenEmpire\EventSystem\AI\
|
||||||
|
├── AIIntelligenceCore.cs # 【需要创建】
|
||||||
|
├── AIHistoryManager.cs # 历史记录管理
|
||||||
|
├── AIMemoryManager.cs # 记忆管理
|
||||||
|
├── SimpleAIClient.cs # API 客户端
|
||||||
|
├── Tools\ # AI 工具目录
|
||||||
|
│ ├── Tool_SpawnResources.cs
|
||||||
|
│ ├── Tool_SendReinforcement.cs
|
||||||
|
│ └── ... (其他工具)
|
||||||
|
└── UI\
|
||||||
|
├── Dialog_AIConversation.cs # 大 UI(已恢复)
|
||||||
|
├── Overlay_WulaLink.cs # 小 UI(需要修复引用)
|
||||||
|
├── Overlay_WulaLink_Notification.cs
|
||||||
|
└── WulaLinkStyles.cs # 样式配置(已重写)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 编译命令
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet build C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\WulaFallenEmpire.csproj
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 架构说明
|
||||||
|
|
||||||
|
### 目标架构
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ AIIntelligenceCore │ ← WorldComponent (核心逻辑)
|
||||||
|
│ - 历史记录管理 │
|
||||||
|
│ - AI 请求处理 (3阶段) │
|
||||||
|
│ - 工具执行 │
|
||||||
|
│ - 表情/状态管理 │
|
||||||
|
└──────────────┬──────────────────────┘
|
||||||
|
│ 事件回调
|
||||||
|
┌──────────┴──────────┐
|
||||||
|
▼ ▼
|
||||||
|
┌─────────────┐ ┌──────────────┐
|
||||||
|
│ Dialog_AI │ │ Overlay_ │
|
||||||
|
│ Conversation│ │ WulaLink │
|
||||||
|
│ (大 UI) │ │ (小 UI) │
|
||||||
|
└─────────────┘ └──────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 关键点
|
||||||
|
1. `Dialog_AIConversation` 目前是**自包含**的(既有 UI 也有 AI 逻辑)
|
||||||
|
2. `Overlay_WulaLink` 需要通过 `AIIntelligenceCore` 获取数据
|
||||||
|
3. 两个 UI 可以共享同一个 `AIIntelligenceCore` 实例
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **不要使用 PowerShell Get-Content 读取文件** - 会显示乱码,请使用 `view_file` 工具
|
||||||
|
2. **备份文件编码正常** - `Tools/using System;.cs` 可以正常读取
|
||||||
|
3. **命名空间**:`WulaFallenEmpire.EventSystem.AI`
|
||||||
|
4. **依赖项**:需要引用 `SimpleAIClient`、`AIHistoryManager`、`AITool` 等现有类
|
||||||
1548
Tools/using System;.cs
Normal file
1548
Tools/using System;.cs
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user