|
| 1 | +# AGENTS.md — RimMind-Bridge-RimChat |
| 2 | + |
| 3 | +本文件供 AI 编码助手阅读,描述 RimMind-Bridge-RimChat 的架构、代码约定和扩展模式。 |
| 4 | + |
| 5 | +## 项目定位 |
| 6 | + |
| 7 | +RimMind-Bridge-RimChat 是 RimMind 套件与 RimChat 模组之间的协调层。当两个模组同时激活时,本模组负责: |
| 8 | + |
| 9 | +1. **对话门控**:避免 RimMind-Dialogue 和 RimChat 重复触发玩家对话 |
| 10 | +2. **动作门控**:避免 RimMind-Actions 和 RimChat 重复执行外交、社交、招募等动作;避免 RimMind-Storyteller 和 RimChat 重复触发事件 |
| 11 | +3. **上下文拉取**:从 RimChat 拉取外交/RPG对话历史,注册为 RimMind Provider,使 RimMind 能感知 RimChat 的对话内容 |
| 12 | + |
| 13 | +本模组通过 `RimMindAPI` 注册 SkipCheck 和 Provider,不依赖 RimChat 的编译期引用,因此 RimChat 未安装时不会报错。 |
| 14 | + |
| 15 | +## 源码结构 |
| 16 | + |
| 17 | +``` |
| 18 | +Source/ |
| 19 | +├── RimMindBridgeRimChatMod.cs Mod 入口,注册 Harmony、Settings Tab,按条件注册桥接模块 |
| 20 | +├── Bridge/ |
| 21 | +│ ├── DialogueGate.cs 对话门控,注册 SkipCheck 防止重复触发 |
| 22 | +│ ├── ActionGate.cs 动作门控,注册 ActionSkipCheck + IncidentSkipCheck + IncidentCallback |
| 23 | +│ └── ContextPullBridge.cs 上下文拉取,从 RimChat 读取对话历史注册为 RimMind Provider |
| 24 | +├── Cooldown/ |
| 25 | +│ └── SharedIncidentCooldown.cs 事件触发共享冷却,防止 RimMind-Storyteller 与 RimChat 短时间内重复触发事件 |
| 26 | +├── Detection/ |
| 27 | +│ └── RimChatDetector.cs 检测 RimChat 是否激活(带缓存) |
| 28 | +└── Settings/ |
| 29 | + └── BridgeRimChatSettings.cs 模组设置(对话门控 + 动作门控 + 上下文拉取) |
| 30 | +``` |
| 31 | + |
| 32 | +## 关键类与 API |
| 33 | + |
| 34 | +### RimChatDetector |
| 35 | + |
| 36 | +检测 RimChat 模组状态,带缓存: |
| 37 | + |
| 38 | +```csharp |
| 39 | +static class RimChatDetector { |
| 40 | + const string RimChatPackageId = "yancy.rimchat"; |
| 41 | + |
| 42 | + bool IsRimChatActive // RimChat 模组是否激活(6000 tick 缓存) |
| 43 | + void InvalidateCache() // 手动刷新缓存 |
| 44 | +} |
| 45 | +``` |
| 46 | + |
| 47 | +### DialogueGate |
| 48 | + |
| 49 | +对话门控,防止 RimMind-Dialogue 和 RimChat 同时触发玩家对话: |
| 50 | + |
| 51 | +```csharp |
| 52 | +static class DialogueGate { |
| 53 | + bool ShouldSkipDialogue(Pawn pawn, string triggerType) |
| 54 | + // triggerType: "PlayerInput" 时检查 skipPlayerDialogue |
| 55 | +
|
| 56 | + bool ShouldSkipFloatMenuOption() |
| 57 | + // 判断是否跳过 RimMind 的"与X对话"浮动菜单 |
| 58 | +
|
| 59 | + void RegisterSkipChecks() |
| 60 | + // 注册到 RimMindAPI.RegisterDialogueSkipCheck / RegisterFloatMenuSkipCheck |
| 61 | +
|
| 62 | + void UnregisterSkipChecks() |
| 63 | + // 清理注册 |
| 64 | +} |
| 65 | +``` |
| 66 | + |
| 67 | +门控逻辑: |
| 68 | + |
| 69 | +| 条件 | 跳过条件 | |
| 70 | +|------|---------| |
| 71 | +| `"PlayerInput"` 对话 | `enableDialogueGate && skipPlayerDialogue && !forceRimMindPlayerDialogue` | |
| 72 | +| 浮动菜单 | `enableDialogueGate && skipPlayerDialogue && !forceRimMindPlayerDialogue` | |
| 73 | + |
| 74 | +### ActionGate |
| 75 | + |
| 76 | +动作门控,防止 RimMind-Actions/Storyteller 和 RimChat 重复执行动作: |
| 77 | + |
| 78 | +```csharp |
| 79 | +static class ActionGate { |
| 80 | + bool ShouldSkipAction(string intentId) |
| 81 | + // 检查动作是否应被跳过 |
| 82 | +
|
| 83 | + bool ShouldSkipStorytellerIncident() |
| 84 | + // 检查 Storyteller 事件是否应被跳过(含冷却检查) |
| 85 | +
|
| 86 | + void Register() |
| 87 | + // 注册到 RimMindAPI.RegisterActionSkipCheck / RegisterIncidentExecutedCallback / RegisterStorytellerIncidentSkipCheck |
| 88 | +
|
| 89 | + void Unregister() |
| 90 | + // 清理注册 |
| 91 | +} |
| 92 | +``` |
| 93 | + |
| 94 | +动作分类: |
| 95 | + |
| 96 | +| 分类 | 动作 ID | 设置开关 | |
| 97 | +|------|---------|---------| |
| 98 | +| 外交 | `adjust_faction`, `trigger_incident` | skipDiplomacyActions | |
| 99 | +| 社交 | `romance_accept`, `romance_breakup` | skipSocialActions | |
| 100 | +| 招募 | `recruit_agree` | skipRecruitAgree | |
| 101 | +| 叙事者事件 | Storyteller incident | skipTriggerIncident + SharedIncidentCooldown | |
| 102 | + |
| 103 | +> **注意**:`trigger_incident` 同时出现在 DiplomacyActions(RimMind-Actions 的动作ID)和 Storyteller 事件跳过(skipTriggerIncident)中。前者由 `skipDiplomacyActions` 控制,后者由 `skipTriggerIncident` 控制,两者互不干扰。 |
| 104 | + |
| 105 | +门控逻辑: |
| 106 | + |
| 107 | +``` |
| 108 | +ShouldSkipAction: |
| 109 | + RimChat 未激活 → false |
| 110 | + enableActionGate 关闭 → false |
| 111 | + forceRimMindActions 开启 → false(强制 RimMind-Actions 执行所有动作,不影响 Storyteller) |
| 112 | + skipDiplomacyActions && intentId ∈ DiplomacyActions → true |
| 113 | + skipSocialActions && intentId ∈ SocialActions → true |
| 114 | + skipRecruitAgree && intentId == "recruit_agree" → true |
| 115 | + 否则 → false |
| 116 | + |
| 117 | +ShouldSkipStorytellerIncident: |
| 118 | + RimChat 未激活 → false |
| 119 | + enableActionGate 关闭 → false |
| 120 | + skipTriggerIncident 关闭 → false |
| 121 | + SharedIncidentCooldown.IsOnCooldown(incidentCooldownTicks) → true/false |
| 122 | +``` |
| 123 | + |
| 124 | +### SharedIncidentCooldown |
| 125 | + |
| 126 | +事件触发共享冷却,防止 RimMind-Storyteller 和 RimChat 短时间内重复触发事件: |
| 127 | + |
| 128 | +```csharp |
| 129 | +static class SharedIncidentCooldown { |
| 130 | + void RecordIncident() // 记录事件触发时间 |
| 131 | + bool IsOnCooldown(int cooldownTicks) // 检查是否在冷却中 |
| 132 | + int LastIncidentTick { get; } // 上次事件触发 tick |
| 133 | + void Reset() // 重置冷却 |
| 134 | +} |
| 135 | +``` |
| 136 | + |
| 137 | +### ContextPullBridge |
| 138 | + |
| 139 | +从 RimChat 拉取对话历史,注册为 RimMind Provider: |
| 140 | + |
| 141 | +```csharp |
| 142 | +static class ContextPullBridge { |
| 143 | + void Register() // 根据设置注册各 Provider |
| 144 | + void Unregister() // 清理所有注册(RimMindAPI.UnregisterModProviders) |
| 145 | +} |
| 146 | +``` |
| 147 | + |
| 148 | +注册的 Provider: |
| 149 | + |
| 150 | +| Category | 数据来源 | 类型 | 优先级 | 设置开关 | |
| 151 | +|----------|---------|------|--------|---------| |
| 152 | +| `rimchat_diplomacy` | GameComponent_DiplomacyManager.dialogueSessions(反射) | StaticProvider | PriorityAuxiliary | pullDiplomacyHistory | |
| 153 | +| `rimchat_rpg_history` | RpgNpcDialogueArchiveManager._archiveCache(反射) | PawnContextProvider | PriorityMemory | pullRpgHistory | |
| 154 | + |
| 155 | +所有 Provider 注册时使用 ModId `"RimMind.BridgeRimChat"`,卸载时通过 `UnregisterModProviders` 一次性清理。 |
| 156 | + |
| 157 | +### BridgeRimChatSettings |
| 158 | + |
| 159 | +```csharp |
| 160 | +class BridgeRimChatSettings : ModSettings { |
| 161 | + // 对话门控 |
| 162 | + bool enableDialogueGate; // 默认 true |
| 163 | + bool skipPlayerDialogue; // 默认 true |
| 164 | + bool forceRimMindPlayerDialogue; // 默认 false |
| 165 | +
|
| 166 | + // 动作门控 |
| 167 | + bool enableActionGate; // 默认 true |
| 168 | + bool skipDiplomacyActions; // 默认 true(跳过 adjust_faction, trigger_incident) |
| 169 | + bool skipTriggerIncident; // 默认 true(跳过 Storyteller 事件触发) |
| 170 | + bool skipSocialActions; // 默认 false(跳过 romance_accept, romance_breakup) |
| 171 | + bool skipRecruitAgree; // 默认 false(跳过 recruit_agree) |
| 172 | + int incidentCooldownTicks; // 默认 60000(1 游戏天),Slider 6000~180000,步进 1500 |
| 173 | + bool forceRimMindActions; // 默认 false(仅影响 RimMind-Actions,不影响 Storyteller) |
| 174 | +
|
| 175 | + // 上下文拉取 |
| 176 | + bool enableContextPull; // 默认 true |
| 177 | + bool pullDiplomacyHistory; // 默认 true |
| 178 | + bool pullRpgHistory; // 默认 false |
| 179 | +
|
| 180 | + static BridgeRimChatSettings Get(); |
| 181 | + static void DrawSettingsContent(Rect inRect); |
| 182 | +} |
| 183 | +``` |
| 184 | + |
| 185 | +## 数据流 |
| 186 | + |
| 187 | +``` |
| 188 | +RimChat 对话数据 RimMind 上下文系统 |
| 189 | +┌──────────────────┐ ┌──────────────────┐ |
| 190 | +│ 外交对话历史 │──反射读取──→ │ rimchat_diplomacy (Static) |
| 191 | +│ RPG对话历史 │──反射读取──→ │ rimchat_rpg_history (Pawn) |
| 192 | +└──────────────────┘ └──────────────────┘ |
| 193 | + |
| 194 | +RimMind-Dialogue 触发 ──DialogueGate──→ 跳过/放行 |
| 195 | +RimMind-Actions 执行 ──ActionGate────→ 跳过/放行 |
| 196 | +RimMind-Storyteller ──ActionGate────→ 跳过/放行(含冷却) |
| 197 | +``` |
| 198 | + |
| 199 | +## 初始化流程 |
| 200 | + |
| 201 | +``` |
| 202 | +RimMindBridgeRimChatMod 构造函数 |
| 203 | + │ |
| 204 | + ├── GetSettings<BridgeRimChatSettings>() |
| 205 | + ├── Harmony("mcocdaa.RimMindBridgeRimChat").PatchAll() |
| 206 | + ├── RimMindAPI.RegisterSettingsTab("bridge_rimchat", ...) |
| 207 | + │ |
| 208 | + ├── RimChatDetector.IsRimChatActive? |
| 209 | + │ │ |
| 210 | + │ ├── No → Log + 跳过所有桥接模块 |
| 211 | + │ │ |
| 212 | + │ └── Yes → DialogueGate.RegisterSkipChecks() |
| 213 | + │ ActionGate.Register() |
| 214 | + │ ContextPullBridge.Register() |
| 215 | +``` |
| 216 | + |
| 217 | +## 代码约定 |
| 218 | + |
| 219 | +### 命名空间 |
| 220 | + |
| 221 | +| 命名空间 | 目录 | 职责 | |
| 222 | +|---------|------|------| |
| 223 | +| `RimMind.Bridge.RimChat` | Source/ 根目录 | Mod 入口 | |
| 224 | +| `RimMind.Bridge.RimChat.Bridge` | Bridge/ | 桥接模块 | |
| 225 | +| `RimMind.Bridge.RimChat.Cooldown` | Cooldown/ | 冷却管理 | |
| 226 | +| `RimMind.Bridge.RimChat.Detection` | Detection/ | RimChat 检测 | |
| 227 | +| `RimMind.Bridge.RimChat.Settings` | Settings/ | 设置 | |
| 228 | + |
| 229 | +### ModId |
| 230 | + |
| 231 | +所有 RimMindAPI 注册使用统一 ModId:`"RimMind.BridgeRimChat"` |
| 232 | + |
| 233 | +### Harmony |
| 234 | + |
| 235 | +- Harmony ID:`mcocdaa.RimMindBridgeRimChat` |
| 236 | +- 当前无 Harmony Patch(预留) |
| 237 | + |
| 238 | +### 构建 |
| 239 | + |
| 240 | +| 配置项 | 值 | |
| 241 | +|--------|-----| |
| 242 | +| 目标框架 | `net48` | |
| 243 | +| C# 语言版本 | 9.0 | |
| 244 | +| Nullable | enable | |
| 245 | +| RimWorld 版本 | 1.6 | |
| 246 | +| 输出路径 | `../1.6/Assemblies/` | |
| 247 | +| 部署 | 设置 `RIMWORLD_DIR` 环境变量后自动部署 | |
| 248 | +| NuGet 依赖 | `Krafs.Rimworld.Ref 1.6.*-*`, `Lib.Harmony.Ref 2.*`, `Newtonsoft.Json 13.0.*` | |
| 249 | +| 编译期引用 | RimMindCore, RimMindDialogue, RimMindPersonality, RimMindMemory, RimMindStoryteller, RimMindAdvisor, RimMindActions(均为 Private=false) | |
| 250 | +| 无编译期引用 | RimChat(纯反射读取,不调用 RimChat API) | |
| 251 | + |
| 252 | +### 加载顺序 |
| 253 | + |
| 254 | +``` |
| 255 | +Harmony → yancy.rimchat → RimMind-Core → RimMind 子模组 → RimMind-Bridge-RimChat |
| 256 | +``` |
| 257 | + |
| 258 | +### UI 本地化 |
| 259 | + |
| 260 | +所有 UI 文本通过 `Languages/ChineseSimplified/Keyed/RimMind_BridgeRimChat.xml` 和 `Languages/English/Keyed/RimMind_BridgeRimChat.xml` 的 Keyed 翻译,禁止硬编码中文。 |
| 261 | + |
| 262 | +翻译键共 22 个,覆盖 3 个设置分区(DialogueGate / ActionGate / ContextPull)+ Mod 入口。 |
| 263 | + |
| 264 | +### 设置 UI |
| 265 | + |
| 266 | +通过 `RimMindAPI.RegisterSettingsTab` 注册到 Core 的多分页设置界面,Tab 标签为 "Bridge (RimChat)"。使用 `SettingsUIHelper` 辅助工具类绘制。 |
| 267 | + |
| 268 | +设置项条件嵌套: |
| 269 | +- `skipPlayerDialogue` 仅在 `enableDialogueGate` 为 true 时显示 |
| 270 | +- `forceRimMindPlayerDialogue` 仅在 `enableDialogueGate && skipPlayerDialogue` 为 true 时显示 |
| 271 | +- 4 个 skip 选项 + `forceRimMindActions` 仅在 `enableActionGate` 为 true 时显示 |
| 272 | +- `incidentCooldownTicks` 滑块仅在 `enableActionGate && skipTriggerIncident` 为 true 时显示 |
| 273 | +- `pullDiplomacyHistory` / `pullRpgHistory` 仅在 `enableContextPull` 为 true 时显示 |
| 274 | + |
| 275 | +## 与 RimTalk Bridge 的区别 |
| 276 | + |
| 277 | +| 特性 | RimChat Bridge | RimTalk Bridge | |
| 278 | +|------|---------------|----------------| |
| 279 | +| 目标模组 | RimChat (`yancy.rimchat`) | RimTalk (`cj.rimtalk`) | |
| 280 | +| 对话门控 | 仅 PlayerInput | Chitchat + Auto + PlayerInput | |
| 281 | +| 动作门控 | 有(外交/社交/招募/叙事者事件) | 无 | |
| 282 | +| 事件冷却 | 有(SharedIncidentCooldown) | 无 | |
| 283 | +| 上下文推送 | 无(RimChat 无公开API) | 有(变量/PromptEntry/Hook) | |
| 284 | +| 上下文拉取 | 有(反射读取外交/RPG对话历史) | 有(反射读取对话历史) | |
| 285 | +| RimChat API 调用 | 无(仅检测激活状态+反射读取数据) | 有(反射调用 RimTalkPromptAPI) | |
| 286 | +| 人格推送 | 无 | 有(PersonaPushBridge + Hook) | |
| 287 | + |
| 288 | +## 扩展指南 |
| 289 | + |
| 290 | +### 新增动作门控 |
| 291 | + |
| 292 | +1. 在 `ActionGate` 中添加动作 ID 到对应 HashSet,或新增 HashSet |
| 293 | +2. 在 `BridgeRimChatSettings` 中添加对应开关 |
| 294 | +3. 在语言文件中添加翻译键 |
| 295 | +4. 更新 `EstimateHeight` 中的高度计算 |
| 296 | + |
| 297 | +### 新增上下文拉取 |
| 298 | + |
| 299 | +1. 在 `ContextPullBridge` 中添加反射读取方法 |
| 300 | +2. 使用 `RimMindAPI.RegisterPawnContextProvider` 或 `RegisterStaticProvider` 注册 |
| 301 | +3. 在 `BridgeRimChatSettings` 中添加对应开关 |
| 302 | +4. 在语言文件中添加翻译键 |
| 303 | + |
| 304 | +### 新增对话门控类型 |
| 305 | + |
| 306 | +1. 在 `DialogueGate.ShouldSkipDialogue` 中添加 triggerType 分支 |
| 307 | +2. 在 `BridgeRimChatSettings` 中添加对应开关 |
| 308 | +3. 在语言文件中添加翻译键 |
| 309 | +4. 更新 `EstimateHeight` 中的高度计算 |
0 commit comments