diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..f67560db --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "mcp__rimworld-code-rag__rough_search" + ] + } +} diff --git a/1.6/1.6/Assemblies/WulaFallenEmpire.dll b/1.6/1.6/Assemblies/WulaFallenEmpire.dll index a4920647..b5f2825b 100644 Binary files a/1.6/1.6/Assemblies/WulaFallenEmpire.dll and b/1.6/1.6/Assemblies/WulaFallenEmpire.dll differ diff --git a/1.6/1.6/Assemblies/WulaFallenEmpire.pdb b/1.6/1.6/Assemblies/WulaFallenEmpire.pdb index a2df47d7..4599713a 100644 Binary files a/1.6/1.6/Assemblies/WulaFallenEmpire.pdb and b/1.6/1.6/Assemblies/WulaFallenEmpire.pdb differ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..9b51885c --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,129 @@ +# AGENTS.md - WulaFallenEmpire RimWorld Mod + +## Build Commands + +### Primary Build Command +```bash +dotnet build "C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\WulaFallenEmpire.csproj" +``` + +### Clean Build +```bash +dotnet clean "C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\WulaFallenEmpire.csproj" +dotnet build "C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\WulaFallenEmpire.csproj" +``` + +### Output Location +- Debug builds: `C:\Steam\steamapps\common\RimWorld\Mods\3516260226\1.6\1.6\Assemblies\` +- Release builds: Same as Debug (optimized) + +### Testing +This project does not have automated unit tests. Manual testing is done through RimWorld gameplay. + +## Project Structure + +``` +C:\Steam\steamapps\common\RimWorld\Mods\3516260226\ +├── Source\WulaFallenEmpire\ # C# source code +├── 1.6\1.6\ # RimWorld 1.6 mod files +│ ├── Assemblies\ # Compiled DLL output +│ ├── Defs\ # XML definitions +│ ├── Languages\ # Translation files +│ ├── Patches\ # XML patch operations +│ └── Textures\ # Visual assets +└── LoadFolders.xml # Mod loading configuration +``` + +## Code Style Guidelines + +### Imports & Formatting +- Group RimWorld imports: `Verse`, `RimWorld`, `Verse.Sound`, `UnityEngine` +- Group mod imports after RimWorld imports +- 4-space indentation, curly braces on new lines +- Use `var` when type is obvious, explicit types when clarity matters +- C# 11.0, .NET Framework 4.8 + +### Naming Conventions +- Classes/Methods/Properties: PascalCase (e.g., `WulaFallenEmpireMod`, `TryCastShot`) +- Fields: camelCase (e.g., `explosionShotCounter`), private: `_scrollPosition` +- Harmony patches: `Patch_` prefix (e.g., `Patch_CaravanFormingUtility_AllSendablePawns`) + +### Harmony Patches +```csharp +[HarmonyPatch(typeof(RimWorld.Planet.CaravanFormingUtility), "AllSendablePawns")] +public static class Patch_CaravanFormingUtility_AllSendablePawns +{ + [HarmonyPostfix] + public static void Postfix(Map map, ref List __result) + { + WulaLog.Debug("[WULA] Patch executed"); + } +} +``` + +### DefOf Pattern +```csharp +[DefOf] +public static class ThingDefOf_WULA +{ + public static ThingDef WULA_MaintenancePod; + static ThingDefOf_WULA() => DefOfHelper.EnsureInitializedInCtor(typeof(ThingDefOf_WULA)); +} +``` + +### Mod Initialization +```csharp +[StaticConstructorOnStartup] +public class WulaFallenEmpireMod : Mod +{ + public WulaFallenEmpireMod(ModContentPack content) : base(content) + { + new Harmony("tourswen.wulafallenempire").PatchAll(Assembly.GetExecutingAssembly()); + } +} +``` + +### Error Handling +Check null before access, use `WulaLog.Debug()` for logging (controlled by mod setting). + +### Signing Convention +沐雪写的代码会加上可爱的署名注释,例如: +```csharp +// ✨ 沐雪写的哦~ +``` + +## Important Rules + +### Knowledge Base Usage +When working on RimWorld modding, ALWAYS use the `rimworld-knowledge-base` tool to: +- Search for correct class names, method signatures, and enum values +- Verify game mechanics and API usage +- Access decompiled RimWorld 1.6 source code +- **Do not rely on external memory or searches** + +### Critical Paths +- Local C# Knowledge Base: `C:\Steam\steamapps\common\RimWorld\dll1.6` +- Mod Project: `C:\Steam\steamapps\common\RimWorld\Mods\3516260226` +- C# Project: `C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire` + +### Project File Sync +When renaming, moving, or deleting C# files, **MUST** update `.csproj` file's `` entries. + +### Dependencies +- RimWorld Assembly-CSharp +- UnityEngine modules (Core, IMGUIModule, etc.) +- Harmony (0Harmony.dll) +- AlienRace (AlienRace.dll) + +## Additional Notes + +### Logging +Use `WulaLog.Debug(string message)` for all debug output. Controlled by mod setting `enableDebugLogs`. Independent of DevMode. + +### Serialization +Use `Scribe_Values.Look()` for primitive types, `Scribe_Collections.Look()` for collections in `ExposeData()` methods. + +### Comments +- Use Chinese comments for Chinese-language code +- Use English comments for general API documentation +- XML documentation (`///`) for public APIs diff --git a/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs b/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs index dca3b0da..1dfa5ec2 100644 --- a/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs +++ b/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs @@ -141,25 +141,32 @@ For each function call, return a JSON object within tags if (_overlayWindowOpen && !string.IsNullOrEmpty(_overlayWindowEventDefName)) { string eventDefNameToRestore = _overlayWindowEventDefName; + float savedX = _overlayWindowX; + float savedY = _overlayWindowY; LongEventHandler.ExecuteWhenFinished(() => { try { - var existingWindow = Find.WindowStack?.Windows?.OfType().FirstOrDefault(); + // Additional safety checks for load scenarios + if (Find.WindowStack == null || Find.World == null) + { + WulaLog.Debug("[WulaAI] Skipping overlay restore: game not fully loaded."); + return; + } + + var existingWindow = Find.WindowStack.Windows?.OfType().FirstOrDefault(); if (existingWindow == null) { var eventDef = DefDatabase.GetNamedSilentFail(eventDefNameToRestore); if (eventDef != null) { var newWindow = new WulaFallenEmpire.EventSystem.AI.UI.Overlay_WulaLink(eventDef); + if (savedX >= 0f && savedY >= 0f) + { + newWindow.SetInitialPosition(savedX, savedY); + } Find.WindowStack.Add(newWindow); newWindow.ToggleMinimize(); // Start minimized - // Force position after everything else - if (_overlayWindowX >= 0f && _overlayWindowY >= 0f) - { - newWindow.windowRect.x = _overlayWindowX; - newWindow.windowRect.y = _overlayWindowY; - } } } } diff --git a/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs b/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs index 28c5ea99..e95f4c57 100644 --- a/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs +++ b/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs @@ -195,9 +195,9 @@ namespace WulaFallenEmpire.EventSystem.AI.UI // Switch to Small UI Button Rect switchBtnRect = new Rect(0f, 0f, 25f, 25f); - if (DrawHeaderButton(switchBtnRect, "-")) + if (DrawHeaderButton(switchBtnRect, "-")) { - if (def != null) + if (def != null && Find.WindowStack != null) { var existing = Find.WindowStack.WindowOfType(); if (existing != null) existing.Expand(); @@ -210,7 +210,7 @@ namespace WulaFallenEmpire.EventSystem.AI.UI Rect personalityBtnRect = new Rect(0f, 30f, 25f, 25f); if (DrawHeaderButton(personalityBtnRect, "P")) { - Find.WindowStack.Add(new Dialog_ExtraPersonalityPrompt()); + Find.WindowStack?.Add(new Dialog_ExtraPersonalityPrompt()); } float margin = 15f; @@ -854,7 +854,7 @@ namespace WulaFallenEmpire.EventSystem.AI.UI private string BuildThinkingStatus() { if (_core == null) return "Thinking..."; - float elapsedSeconds = Mathf.Max(0f, Time.realtimeSinceStartup - _core.ThinkingStartTime); + float elapsedSeconds = Mathf.Max(0f, Time.realtimeSinceStartup - (_core.ThinkingStartTime)); string elapsedText = elapsedSeconds.ToString("0.0", CultureInfo.InvariantCulture); return $"P.I.A is thinking... ({elapsedText}s Loop {_core.ThinkingPhaseIndex})"; } @@ -945,22 +945,22 @@ namespace WulaFallenEmpire.EventSystem.AI.UI { if (string.IsNullOrWhiteSpace(text)) return; if (_core == null) return; - + if (string.Equals(text.Trim(), "/clear", StringComparison.OrdinalIgnoreCase)) { _isThinking = false; _options.Clear(); _inputText = ""; // Core functionality for clear if implemented, or just UI clear - // For now, Dialog doesn't manage history, Core does. - // Core should handle /clear command via SendUserMessage theoretically, + // For now, Dialog doesn't manage history, Core does. + // Core should handle /clear command via SendUserMessage theoretically, // or we call a hypothetical _core.ClearHistory(). // Based on previous code, SendUserMessage handles /clear logic inside Core. } _scrollToBottom = true; _core.SendUserMessage(text); - _history = _core.GetHistorySnapshot(); + _history = _core.GetHistorySnapshot() ?? new List<(string role, string message)>(); } private void DrawConversationOptions(Rect rect, List options) diff --git a/Source/WulaFallenEmpire/EventSystem/AI/UI/Overlay_WulaLink.cs b/Source/WulaFallenEmpire/EventSystem/AI/UI/Overlay_WulaLink.cs index 680b7d72..cc5c6551 100644 --- a/Source/WulaFallenEmpire/EventSystem/AI/UI/Overlay_WulaLink.cs +++ b/Source/WulaFallenEmpire/EventSystem/AI/UI/Overlay_WulaLink.cs @@ -129,14 +129,14 @@ namespace WulaFallenEmpire.EventSystem.AI.UI public void Expand() { if (_isMinimized) ToggleMinimize(); - Find.WindowStack.Notify_ManuallySetFocus(this); + Find.WindowStack?.Notify_ManuallySetFocus(this); } public override void PreOpen() { base.PreOpen(); - // Connect to Core - _core = Find.World.GetComponent(); + // Connect to Core - with null safety for load scenarios + _core = Find.World?.GetComponent(); if (_core != null) { _core.InitializeConversation(_eventDefName); @@ -167,7 +167,7 @@ namespace WulaFallenEmpire.EventSystem.AI.UI { _unreadCount++; // Spawn Notification Bubble - Find.WindowStack.Add(new Overlay_WulaLink_Notification(msg)); + Find.WindowStack?.Add(new Overlay_WulaLink_Notification(msg)); } } @@ -221,15 +221,15 @@ namespace WulaFallenEmpire.EventSystem.AI.UI private void DrawMinimized(Rect rect) { // AI 核心挂件背景 - Widgets.DrawBoxSolid(rect, new Color(0.1f, 0.1f, 0.1f, 0.9f)); + Widgets.DrawBoxSolid(rect, new Color(0.1f, 0.1f, 0.1f, 0.9f)); GUI.color = WulaLinkStyles.HeaderColor; - Widgets.DrawBox(rect, 2); + Widgets.DrawBox(rect, 2); GUI.color = Color.white; - + // 左侧:大型方形头像 float avaSize = rect.height - 16f; Rect avatarRect = new Rect(8f, 8f, avaSize, avaSize); - + int expId = _core?.ExpressionId ?? 1; string portraitPath = "Wula/Storyteller/WULA_Legion_TINY"; Texture2D portrait = ContentFinder.Get(portraitPath, false); @@ -245,17 +245,18 @@ namespace WulaFallenEmpire.EventSystem.AI.UI // 右侧:状态展示 float rightContentX = avatarRect.xMax + 12f; float btnWidth = 30f; - - // Status Info - string status = _core.IsThinking ? "Thinking..." : "Standby"; - Color statusColor = _core.IsThinking ? Color.yellow : Color.green; + + // Status Info - with null safety + bool isThinking = _core?.IsThinking ?? false; + string status = isThinking ? "Thinking..." : "Standby"; + Color statusColor = isThinking ? Color.yellow : Color.green; // 绘制状态文字 Rect textRect = new Rect(rightContentX, 0, rect.width - rightContentX - btnWidth - 5f, rect.height); Text.Anchor = TextAnchor.MiddleLeft; Text.Font = GameFont.Small; GUI.color = statusColor; - Widgets.Label(textRect, _core.IsThinking ? BuildThinkingStatus() : "Standby"); + Widgets.Label(textRect, isThinking ? BuildThinkingStatus() : "Standby"); GUI.color = Color.white; // 右侧:小巧的展开按钮 @@ -290,13 +291,14 @@ namespace WulaFallenEmpire.EventSystem.AI.UI Widgets.DrawLineHorizontal(rect.x, rect.y, rect.width); string contextInfo = "Context: None"; - if (Find.Selector.SingleSelectedThing != null) + var selector = Find.Selector; + if (selector?.SingleSelectedThing != null) { - contextInfo = $"Context: [{Find.Selector.SingleSelectedThing.LabelCap}]"; + contextInfo = $"Context: [{selector.SingleSelectedThing.LabelCap}]"; } - else if (Find.Selector.SelectedObjects.Count > 1) + else if (selector?.SelectedObjects?.Count > 1) { - contextInfo = $"Context: {Find.Selector.SelectedObjects.Count} objects selected"; + contextInfo = $"Context: {selector.SelectedObjects.Count} objects selected"; } Text.Anchor = TextAnchor.MiddleLeft; @@ -917,12 +919,12 @@ namespace WulaFallenEmpire.EventSystem.AI.UI bool sendClicked = DrawCustomButton(btnRect, ">", !string.IsNullOrWhiteSpace(_inputText)); if (sendClicked || enterPressed) { - if (!string.IsNullOrWhiteSpace(_inputText)) + if (!string.IsNullOrWhiteSpace(_inputText) && _core != null) { bool wasFocused = GUI.GetNameOfFocusedControl() == "WulaInput"; _core.SendUserMessage(_inputText); _inputText = ""; - if (wasFocused) GUI.FocusControl("WulaInput"); + if (wasFocused) GUI.FocusControl("WulaInput"); } }