整理
507
Source/Documentation/EventSystem_Documentation.md
Normal file
@@ -0,0 +1,507 @@
|
||||
# Wula Fallen Empire - 事件系统文档
|
||||
|
||||
这是一个用于在RimWorld中创建复杂、带选项的事件和对话框的强大系统。它由两个主要部分组成:**任务事件** 和 **EventDef事件**。
|
||||
|
||||
## 核心概念
|
||||
|
||||
- **Effect(效果)**: 一个原子操作,例如生成一个Pawn、给予一个物品、改变派系关系或打开另一个UI。
|
||||
- **Condition(条件)**: 一个用于决定一个选项是否可用的逻辑检查(例如,检查一个变量的值)。
|
||||
- **EventContext(事件上下文)**: 一个全局的静态类,用于存储和检索变量,允许在不同的事件和UI之间传递数据。
|
||||
|
||||
---
|
||||
|
||||
## 1. 任务事件 (`QuestNode_Root_EventLetter`)
|
||||
|
||||
这是通过RimWorld的原版任务系统触发的事件。它会生成一个带有选项的信件。
|
||||
|
||||
### 如何使用
|
||||
|
||||
1. 在你的 `QuestScriptDef` 中,使用 `WulaFallenEmpire.QuestNode_Root_EventLetter` 作为根节点。
|
||||
2. 在XML中定义 `letterLabel`, `letterTitle`, `letterText`。
|
||||
3. 在 `<options>` 列表中定义多个选项。每个选项都有一个 `label` 和一个或多个 `effects`。
|
||||
|
||||
### 示例 (`QuestScriptDef`)
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Defs>
|
||||
<QuestScriptDef>
|
||||
<defName>Wula_ExampleQuestEvent</defName>
|
||||
<root Class="WulaFallenEmpire.QuestNode_Root_EventLetter">
|
||||
<letterLabel>一个抉择</letterLabel>
|
||||
<letterTitle>远方的呼唤</letterTitle>
|
||||
<letterText>一个来自遥远星系的信号抵达了你们的通讯站。他们似乎想和你们谈谈。</letterText>
|
||||
<options>
|
||||
<li>
|
||||
<label>接受通讯</label>
|
||||
<optionEffects>
|
||||
<li Class="WulaFallenEmpire.Effect_OpenCustomUI">
|
||||
<defName>Wula_ExampleEvent</defName>
|
||||
</li>
|
||||
</optionEffects>
|
||||
</li>
|
||||
<li>
|
||||
<label>忽略他们</label>
|
||||
<optionEffects>
|
||||
<li Class="WulaFallenEmpire.Effect_ShowMessage">
|
||||
<message>你决定无视这个信号。宇宙的寂静再次笼罩着你。</message>
|
||||
<messageTypeDef>NeutralEvent</messageTypeDef>
|
||||
</li>
|
||||
</optionEffects>
|
||||
</li>
|
||||
</options>
|
||||
</root>
|
||||
</QuestScriptDef>
|
||||
</Defs>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. EventDef事件 (`Dialog_CustomDisplay`)
|
||||
|
||||
这是一个高度可定制的对话框窗口,可以显示角色肖像、背景、文本和多个带条件的选项。
|
||||
|
||||
### 如何使用
|
||||
|
||||
1. 创建一个 `EventDef`。
|
||||
2. 定义 `label`, `characterName`, `portraitPath`, `descriptions` 等。
|
||||
3. 在 `<options>` 列表中定义选项。每个选项可以有关联的 `effects` 和 `conditions`。
|
||||
4. 你可以通过 `Effect_OpenCustomUI` 效果来打开这个UI(从任务事件或其他EventDef)。
|
||||
5. 你也可以通过将 `CompOpenCustomUI` 附加到一个建筑上来从游戏中直接打开它。
|
||||
|
||||
### `EventDef` 参数
|
||||
|
||||
- **label**: (string) 窗口的标题。
|
||||
- **characterName**: (string) 显示在肖像下方的角色名称。
|
||||
- **portraitPath**: (string) 角色肖像的纹理路径。
|
||||
- **descriptions**: (List<string>) 一个描述文本列表。可以通过 `descriptionMode` 控制是随机选择一个还是按顺序显示。
|
||||
- **options**: (List<EventOption>) 对话框中显示的选项列表。
|
||||
- **immediateEffects**: (List<ConditionalEffects>) 当对话框打开时立即执行的效果列表。
|
||||
- **dismissEffects**: (List<ConditionalEffects>) 当对话框关闭时(通过关闭按钮或`Effect_CloseDialog`)执行的效果列表。
|
||||
- **backgroundImagePath**: (string) (可选) 对话框的背景图片路径。
|
||||
- **windowSize**: (Vector2) (可选) 自定义窗口大小。
|
||||
- **hiddenWindow**: (bool) (可选) 如果为 `true`,则不会显示窗口。在这种模式下,`immediateEffects` 的内容会在加载时自动合并到 `dismissEffects` 中,然后在事件触发时作为单个效果链统一执行。这对于创建纯粹的后台“效果链”事件非常有用。默认为 `false`。
|
||||
|
||||
### `EventDef` 示例
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Defs>
|
||||
<WulaFallenEmpire.EventDef>
|
||||
<defName>Wula_ExampleEvent</defName>
|
||||
<label>神秘的通讯</label>
|
||||
<characterName>特使</characterName>
|
||||
<portraitPath>Textures/Wula/Events/Portraits/Envoy</portraitPath>
|
||||
<descriptions>
|
||||
<li>“你好,来自边缘世界的陌生人。我们观察你很久了。你的挣扎……很有趣。”</li>
|
||||
</descriptions>
|
||||
<immediateEffects>
|
||||
<!-- 这是一个无条件的ConditionalEffects块 -->
|
||||
<li>
|
||||
<!-- 没有<conditions>,所以总是执行 -->
|
||||
<effects>
|
||||
<li Class="WulaFallenEmpire.Effect_SetVariable">
|
||||
<name>MetTheEnvoy</name>
|
||||
<value>true</value>
|
||||
</li>
|
||||
</effects>
|
||||
</li>
|
||||
</immediateEffects>
|
||||
<dismissEffects>
|
||||
<!-- 这是一个有条件的ConditionalEffects块 -->
|
||||
<li>
|
||||
<conditions>
|
||||
<li Class="WulaFallenEmpire.Condition_VariableEquals">
|
||||
<name>PlayerMadeChoice</name>
|
||||
<value>false</value>
|
||||
</li>
|
||||
</conditions>
|
||||
<effects>
|
||||
<li Class="WulaFallenEmpire.Effect_ShowMessage">
|
||||
<message>你没有做出选择就关闭了通讯。</message>
|
||||
</li>
|
||||
</effects>
|
||||
</li>
|
||||
</dismissEffects>
|
||||
<options>
|
||||
<li>
|
||||
<label>“你是谁?”</label>
|
||||
<optionEffects>
|
||||
<li>
|
||||
<effects>
|
||||
<li Class="WulaFallenEmpire.Effect_ShowMessage">
|
||||
<message>“我们是观察者。我们是见证者。现在,我们是你的未来。”</message>
|
||||
</li>
|
||||
<li Class="WulaFallenEmpire.Effect_CloseDialog" />
|
||||
</effects>
|
||||
</li>
|
||||
</optionEffects>
|
||||
</li>
|
||||
<li>
|
||||
<label>“给我们一些东西来证明你的诚意。”</label>
|
||||
<disabledReason>他们似乎对你不够信任。</disabledReason>
|
||||
<!-- 这个conditions块现在只用于决定选项是否可点击 -->
|
||||
<conditions>
|
||||
<li Class="WulaFallenEmpire.Condition_VariableGreaterThanOrEqual">
|
||||
<name>EmpireGoodwill</name>
|
||||
<value>50</value>
|
||||
</li>
|
||||
</conditions>
|
||||
<optionEffects>
|
||||
<li>
|
||||
<!-- 你甚至可以在选项的效果内部再次添加条件 -->
|
||||
<conditions>
|
||||
<li Class="WulaFallenEmpire.Condition_VariableEquals">
|
||||
<name>IsGenerous</name>
|
||||
<value>true</value>
|
||||
</li>
|
||||
</conditions>
|
||||
<effects>
|
||||
<li Class="WulaFallenEmpire.Effect_GiveThing">
|
||||
<thingDef>Gold</thingDef>
|
||||
<count>200</count> <!-- 如果IsGenerous为true,则给予更多 -->
|
||||
</li>
|
||||
<li Class="WulaFallenEmpire.Effect_CloseDialog" />
|
||||
</effects>
|
||||
</li>
|
||||
<li>
|
||||
<effects>
|
||||
<li Class="WulaFallenEmpire.Effect_GiveThing">
|
||||
<thingDef>Gold</thingDef>
|
||||
<count>100</count>
|
||||
</li>
|
||||
<li Class="WulaFallenEmpire.Effect_CloseDialog" />
|
||||
</effects>
|
||||
</li>
|
||||
</optionEffects>
|
||||
</li>
|
||||
</options>
|
||||
</WulaFallenEmpire.EventDef>
|
||||
</Defs>
|
||||
```
|
||||
|
||||
### UI 布局配置 (`EventUIConfigDef`)
|
||||
|
||||
你可以在 `1.6/Defs/WulaMiscSettingDefs/EventUIConfig.xml` 中调整所有EventDef窗口的默认外观和布局。
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心结构: 条件化效果 (`ConditionalEffects`)
|
||||
|
||||
所有执行效果的地方 (`immediateEffects`, `dismissEffects`, 以及每个选项的 `optionEffects`) 都是一个 `ConditionalEffects` 块的列表。
|
||||
|
||||
这允许你将一组效果与一组条件绑定在一起。
|
||||
|
||||
### `ConditionalEffects` 结构
|
||||
|
||||
每个 `<li>` 代表一个 `ConditionalEffects` 块。它包含两个可选部分:
|
||||
- **`<conditions>`**: 一个条件列表。只有当这里的所有条件都满足时,对应的效果才会执行。如果省略这个部分,效果将总是执行。
|
||||
- **`<effects>`**: 一个效果列表。当条件满足时,这些效果会被执行。
|
||||
|
||||
```xml
|
||||
<!-- 示例: 一个ConditionalEffects块 -->
|
||||
<li>
|
||||
<conditions>
|
||||
<li Class="WulaFallenEmpire.Condition_VariableEquals">
|
||||
<name>PlayerChoice</name>
|
||||
<value>AcceptedOffer</value>
|
||||
</li>
|
||||
</conditions>
|
||||
<effects>
|
||||
<li Class="WulaFallenEmpire.Effect_ShowMessage">
|
||||
<message>你接受了提议!</message>
|
||||
</li>
|
||||
<li Class="WulaFallenEmpire.Effect_GiveThing">
|
||||
<thingDef>Silver</thingDef>
|
||||
<count>500</count>
|
||||
</li>
|
||||
</effects>
|
||||
</li>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 可用的效果 (`Effect`)
|
||||
|
||||
这些是可以在 `ConditionalEffects` 块的 `<effects>` 列表中使用的类。
|
||||
|
||||
### `Effect_OpenCustomUI`
|
||||
打开一个指定的 `EventDef`。
|
||||
- **defName**: (string) 要打开的 `EventDef` 的 `defName`。
|
||||
```xml
|
||||
<li Class="WulaFallenEmpire.Effect_OpenCustomUI">
|
||||
<defName>Wula_AnotherEvent</defName>
|
||||
</li>
|
||||
```
|
||||
|
||||
### `Effect_CloseDialog`
|
||||
关闭当前的EventDef窗口。没有参数。
|
||||
```xml
|
||||
<li Class="WulaFallenEmpire.Effect_CloseDialog" />
|
||||
```
|
||||
|
||||
### `Effect_ShowMessage`
|
||||
在屏幕上显示一条消息。
|
||||
- **message**: (string) 要显示的消息文本。
|
||||
- **messageTypeDef**: (MessageTypeDef) 消息的类型 (例如 `PositiveEvent`, `NegativeEvent`, `NeutralEvent`)。默认为 `PositiveEvent`。
|
||||
```xml
|
||||
<li Class="WulaFallenEmpire.Effect_ShowMessage">
|
||||
<message>你获得了一个新的盟友。</message>
|
||||
<messageTypeDef>PositiveEvent</messageTypeDef>
|
||||
</li>
|
||||
```
|
||||
|
||||
### `Effect_FireIncident`
|
||||
触发一个指定的事件。
|
||||
- **incident**: (IncidentDef) 要触发的事件的 `defName`。
|
||||
```xml
|
||||
<li Class="WulaFallenEmpire.Effect_FireIncident">
|
||||
<incident>RaidEnemy</incident>
|
||||
</li>
|
||||
```
|
||||
|
||||
### `Effect_ChangeFactionRelation`
|
||||
改变玩家与某个派系的关系。
|
||||
- **faction**: (FactionDef) 目标派系的 `defName`。
|
||||
- **goodwillChange**: (int) 关系值的变化量(可以是负数)。
|
||||
```xml
|
||||
<li Class="WulaFallenEmpire.Effect_ChangeFactionRelation">
|
||||
<faction>WulaFallenEmpire_Player</faction>
|
||||
<goodwillChange>15</goodwillChange>
|
||||
</li>
|
||||
```
|
||||
|
||||
### `Effect_ChangeFactionRelation_FromVariable`
|
||||
根据一个变量的值改变派系关系。
|
||||
- **faction**: (FactionDef) 目标派系的 `defName`。
|
||||
- **goodwillVariableName**: (string) 存储关系变化值的变量名。
|
||||
```xml
|
||||
<li Class="WulaFallenEmpire.Effect_ChangeFactionRelation_FromVariable">
|
||||
<faction>WulaFallenEmpire_Player</faction>
|
||||
<goodwillVariableName>ReputationChange</goodwillVariableName>
|
||||
</li>
|
||||
```
|
||||
|
||||
### `Effect_GiveThing`
|
||||
给玩家一些物品(通过空投)。
|
||||
- **thingDef**: (ThingDef) 要给予的物品的 `defName`。
|
||||
- **count**: (int) 给予的数量。默认为 1。
|
||||
```xml
|
||||
<li Class="WulaFallenEmpire.Effect_GiveThing">
|
||||
<thingDef>Plasteel</thingDef>
|
||||
<count>150</count>
|
||||
</li>
|
||||
```
|
||||
|
||||
### `Effect_SpawnPawn`
|
||||
生成一个Pawn。
|
||||
- **kindDef**: (PawnKindDef) 要生成的Pawn的 `defName`。
|
||||
- **count**: (int) 生成的数量。默认为 1。
|
||||
- **joinPlayerFaction**: (bool) 是否加入玩家派系。默认为 `true`。
|
||||
- **letterLabel**: (string) 可选,生成时附带的信件标题。
|
||||
- **letterText**: (string) 可选,生成时附带的信件内容。
|
||||
- **letterDef**: (LetterDef) 可选,信件的类型。
|
||||
```xml
|
||||
<li Class="WulaFallenEmpire.Effect_SpawnPawn">
|
||||
<kindDef>Colonist</kindDef>
|
||||
<count>1</count>
|
||||
<joinPlayerFaction>true</joinPlayerFaction>
|
||||
<letterLabel>一个新人加入了!</letterLabel>
|
||||
<letterText>一个流浪者被你们的善举所吸引,决定加入你们的殖民地。</letterText>
|
||||
</li>
|
||||
```
|
||||
|
||||
### `Effect_SpawnPawnAndStore`
|
||||
生成一个Pawn并将其存储在一个变量中以备后用。
|
||||
- **kindDef**: (PawnKindDef) 要生成的Pawn的 `defName`。
|
||||
- **count**: (int) 生成的数量。默认为 1。
|
||||
- **storeAs**: (string) 用于存储生成Pawn的变量名。如果 `count` 大于1,则存储一个Pawn列表。
|
||||
```xml
|
||||
<li Class="WulaFallenEmpire.Effect_SpawnPawnAndStore">
|
||||
<kindDef>Wula_Elite_Warrior</kindDef>
|
||||
<storeAs>spawnedWarrior</storeAs>
|
||||
</li>
|
||||
```
|
||||
|
||||
### `Effect_AddQuest`
|
||||
触发一个新的任务。
|
||||
- **quest**: (QuestScriptDef) 要开始的任务的 `defName`。
|
||||
```xml
|
||||
<li Class="WulaFallenEmpire.Effect_AddQuest">
|
||||
<quest>Wula_AnotherQuest</quest>
|
||||
</li>
|
||||
```
|
||||
|
||||
### `Effect_FinishResearch`
|
||||
立即完成一个研究项目。
|
||||
- **research**: (ResearchProjectDef) 要完成的研究的 `defName`。
|
||||
```xml
|
||||
<li Class="WulaFallenEmpire.Effect_FinishResearch">
|
||||
<research>MicroelectronicsBasics</research>
|
||||
</li>
|
||||
```
|
||||
|
||||
### `Effect_TriggerRaid`
|
||||
触发一次袭击。这个效果有两种模式:
|
||||
1. **简单模式**: 使用派系默认的袭击队伍。
|
||||
2. **高级模式**: 使用动态定义的 `pawnGroupMakers` 来生成自定义的袭击队伍。
|
||||
|
||||
- **points**: (float) 袭击的点数。
|
||||
- **faction**: (FactionDef) 袭击者的派系 `defName`。
|
||||
- **raidStrategy**: (RaidStrategyDef) 袭击策略的 `defName` (例如 `ImmediateAttack`)。
|
||||
- **raidArrivalMode**: (PawnsArrivalModeDef) 袭击者到达方式的 `defName` (例如 `EdgeWalkIn`)。
|
||||
- **groupKind**: (PawnGroupKindDef) (高级模式) 定义队伍类型,例如 `Combat` 或 `Trader`。默认为 `Combat`。
|
||||
- **pawnGroupMakers**: (List<PawnGroupMaker>) (高级模式) 一个 `PawnGroupMaker` 列表,用于动态定义袭击队伍的构成。
|
||||
- **letterLabel**: (string) (可选) 自定义袭击信件的标题。如果提供,将覆盖默认的 "Raid" 标题。
|
||||
- **letterText**: (string) (可选) 自定义袭击信件的内容。如果提供,将覆盖默认的袭击描述文本。
|
||||
|
||||
**简单模式示例:**
|
||||
```xml
|
||||
<li Class="WulaFallenEmpire.Effect_TriggerRaid">
|
||||
<points>500</points>
|
||||
<faction>Pirate</faction>
|
||||
<raidStrategy>ImmediateAttack</raidStrategy>
|
||||
<raidArrivalMode>EdgeWalkIn</raidArrivalMode>
|
||||
<letterLabel>侦测到威胁!</letterLabel>
|
||||
<letterText>我们的传感器侦测到一伙来自 {FACTION_name} 的袭击者!他们看起来充满敌意,正朝着我们的殖民地前进。</letterText>
|
||||
</li>
|
||||
```
|
||||
|
||||
**高级模式示例:**
|
||||
```xml
|
||||
<li Class="WulaFallenEmpire.Effect_TriggerRaid">
|
||||
<points>1000</points>
|
||||
<faction>WulaFallenEmpire_Player</faction>
|
||||
<raidStrategy>ImmediateAttack</raidStrategy>
|
||||
<raidArrivalMode>EdgeWalkIn</raidArrivalMode>
|
||||
<groupKind>Combat</groupKind>
|
||||
<pawnGroupMakers>
|
||||
<li>
|
||||
<kindDef>Combat</kindDef>
|
||||
<commonality>100</commonality>
|
||||
<options>
|
||||
<Mech_WULA_Cat_Constructor>20</Mech_WULA_Cat_Constructor>
|
||||
<Mech_WULA_Cat_Assault>20</Mech_WULA_Cat_Assault>
|
||||
<Wula_Broken_Personality_Pawn_7>2</Wula_Broken_Personality_Pawn_7>
|
||||
<Wula_Broken_Personality_Pawn_5>1</Wula_Broken_Personality_Pawn_5>
|
||||
</options>
|
||||
</li>
|
||||
</pawnGroupMakers>
|
||||
</li>
|
||||
```
|
||||
|
||||
### `Effect_SetVariable`
|
||||
设置一个 `EventContext` 变量的值。
|
||||
- **name**: (string) 变量名。
|
||||
- **value**: (string) 变量的值。系统会自动尝试将其解析为 `int` 或 `float`,如果失败则作为 `string` 存储。
|
||||
```xml
|
||||
<li Class="WulaFallenEmpire.Effect_SetVariable">
|
||||
<name>PlayerChoice</name>
|
||||
<value>AcceptedOffer</value>
|
||||
</li>
|
||||
```
|
||||
|
||||
### `Effect_ModifyVariable`
|
||||
对一个数字变量进行加、减、乘、除操作。
|
||||
- **name**: (string) 变量名。
|
||||
- **value**: (float) 用于操作的数值。
|
||||
- **operation**: (VariableOperation) 操作类型,可以是 `Add`, `Subtract`, `Multiply`, `Divide`。
|
||||
```xml
|
||||
<li Class="WulaFallenEmpire.Effect_ModifyVariable">
|
||||
<name>ResourceCount</name>
|
||||
<value>-10</value>
|
||||
<operation>Add</operation> <!-- This will subtract 10 -->
|
||||
</li>
|
||||
```
|
||||
|
||||
### `Effect_ClearVariable`
|
||||
从 `EventContext` 中移除一个变量。
|
||||
- **name**: (string) 要清除的变量名。
|
||||
```xml
|
||||
<li Class="WulaFallenEmpire.Effect_ClearVariable">
|
||||
<name>PlayerChoice</name>
|
||||
</li>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 可用的条件 (`Condition`)
|
||||
|
||||
这些是可以在 `conditions` 列表中使用的类,用于控制选项的可用性。
|
||||
|
||||
### `Condition_VariableEquals`
|
||||
检查一个变量是否等于一个特定值。
|
||||
- **name**: (string) 要检查的变量名。
|
||||
- **value**: (string) 要比较的字面值。
|
||||
- **valueVariableName**: (string) (可选) 要比较的另一个变量的名称。如果提供此项,则忽略 `value`。
|
||||
```xml
|
||||
<li Class="WulaFallenEmpire.Condition_VariableEquals">
|
||||
<name>PlayerChoice</name>
|
||||
<value>AcceptedOffer</value>
|
||||
</li>
|
||||
```
|
||||
### `Condition_VariableNotEqual`
|
||||
检查一个变量是否 **不等于** 一个特定值。
|
||||
```xml
|
||||
<li Class="WulaFallenEmpire.Condition_VariableNotEqual">
|
||||
<name>QuestStage</name>
|
||||
<value>3</value>
|
||||
</li>
|
||||
```
|
||||
|
||||
### `Condition_CompareVariable` (基类)
|
||||
这是一个抽象基类,不应直接使用。以下所有比较条件(大于、小于等)都继承自这个基类,并共享其参数。
|
||||
|
||||
**基类参数:**
|
||||
- **name**: (string) 要检查的变量名。
|
||||
- **value**: (float) 要比较的字面数值。
|
||||
- **valueVariableName**: (string) (可选) 要比较的另一个变量的名称。如果提供此项,则会忽略 `value` 字段。
|
||||
|
||||
**工作原理:**
|
||||
当你使用例如 `Condition_VariableGreaterThan` 时,你实际上是在使用一个 `Condition_CompareVariable` 的特定版本。你可以提供 `value` 来与一个固定的数字比较,或者提供 `valueVariableName` 来与另一个变量的值进行比较。
|
||||
|
||||
**变量与变量比较示例:**
|
||||
下面的例子使用了 `Condition_VariableGreaterThanOrEqual`(它是 `Condition_CompareVariable` 的子类),来检查 `PlayerWealth` 变量是否大于或等于 `RequiredWealth` 变量。
|
||||
```xml
|
||||
<li Class="WulaFallenEmpire.Condition_VariableGreaterThanOrEqual">
|
||||
<name>PlayerWealth</name>
|
||||
<valueVariableName>RequiredWealth</valueVariableName>
|
||||
</li>
|
||||
```
|
||||
|
||||
### `Condition_VariableGreaterThan`
|
||||
检查一个变量是否 **大于** 一个特定值。
|
||||
```xml
|
||||
<li Class="WulaFallenEmpire.Condition_VariableGreaterThan">
|
||||
<name>ColonistCount</name>
|
||||
<value>5</value>
|
||||
</li>
|
||||
```
|
||||
|
||||
### `Condition_VariableLessThan`
|
||||
检查一个变量是否 **小于** 一个特定值。
|
||||
```xml
|
||||
<li Class="WulaFallenEmpire.Condition_VariableLessThan">
|
||||
<name>ThreatPoints</name>
|
||||
<value>1000</value>
|
||||
</li>
|
||||
```
|
||||
|
||||
### `Condition_VariableGreaterThanOrEqual`
|
||||
检查一个变量是否 **大于或等于** 一个特定值。
|
||||
```xml
|
||||
<li Class="WulaFallenEmpire.Condition_VariableGreaterThanOrEqual">
|
||||
<name>EmpireGoodwill</name>
|
||||
<value>50</value>
|
||||
</li>
|
||||
```
|
||||
|
||||
### `Condition_VariableLessThanOrEqual`
|
||||
检查一个变量是否 **小于或等于** 一个特定值。
|
||||
```xml
|
||||
<li Class="WulaFallenEmpire.Condition_VariableLessThanOrEqual">
|
||||
<name>YearsPassed</name>
|
||||
<value>2</value>
|
||||
</li>
|
||||
```
|
||||
---
|
||||
186
Source/Documentation/layout_preview.html
Normal file
@@ -0,0 +1,186 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>事件UI布局预览 (动态版)</title>
|
||||
<style>
|
||||
body {
|
||||
background-color: #333;
|
||||
color: white;
|
||||
font-family: sans-serif;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
margin: 0;
|
||||
}
|
||||
.controls {
|
||||
margin-bottom: 20px;
|
||||
width: 1000px;
|
||||
}
|
||||
textarea {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
background-color: #222;
|
||||
color: #ddd;
|
||||
border: 1px solid #555;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.window {
|
||||
width: 1000px;
|
||||
height: 750px;
|
||||
background-color: #555;
|
||||
border: 2px solid #ccc;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
.element {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
border: 2px solid white;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
text-shadow: 1px 1px 2px black;
|
||||
}
|
||||
.lihui { background-color: rgba(255, 0, 0, 0.4); }
|
||||
.name { background-color: rgba(0, 255, 0, 0.4); }
|
||||
.text { background-color: rgba(0, 0, 255, 0.4); }
|
||||
.options { background-color: rgba(255, 255, 0, 0.4); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="controls">
|
||||
<h3>动态布局预览</h3>
|
||||
<p>请将您的 <code>EventUIConfig.xml</code> 文件内容完整粘贴到下面的文本框中,然后点击“生成预览”按钮。</p>
|
||||
<textarea id="xmlInput" placeholder="在这里粘贴 EventUIConfig.xml 的内容..."></textarea>
|
||||
<button onclick="generatePreview()">生成预览</button>
|
||||
</div>
|
||||
<div class="window" id="window">
|
||||
<!-- 布局将在这里生成 -->
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function parseVector2(str) {
|
||||
const match = str.match(/\((\s*[\d.]+)\s*,\s*([\d.]+)\s*\)/);
|
||||
return match ? { x: parseFloat(match[1]), y: parseFloat(match[2]) } : { x: 0, y: 0 };
|
||||
}
|
||||
|
||||
function generatePreview() {
|
||||
const xmlString = document.getElementById('xmlInput').value;
|
||||
if (!xmlString) {
|
||||
alert('请先粘贴XML内容!');
|
||||
return;
|
||||
}
|
||||
|
||||
const parser = new DOMParser();
|
||||
const xmlDoc = parser.parseFromString(xmlString, "text/xml");
|
||||
|
||||
// Need to escape the dot in the class name for querySelector
|
||||
const configDef = xmlDoc.querySelector('WulaFallenEmpire\\.EventUIConfigDef');
|
||||
if (!configDef) {
|
||||
alert('无法找到 WulaFallenEmpire.EventUIConfigDef 节点,请检查XML内容是否正确。');
|
||||
return;
|
||||
}
|
||||
|
||||
const config = {
|
||||
lihuiSize: parseVector2(configDef.querySelector('lihuiSize')?.textContent || '(0,0)'),
|
||||
nameSize: parseVector2(configDef.querySelector('nameSize')?.textContent || '(0,0)'),
|
||||
textSize: parseVector2(configDef.querySelector('textSize')?.textContent || '(0,0)'),
|
||||
optionsWidth: parseFloat(configDef.querySelector('optionsWidth')?.textContent || '0'),
|
||||
textNameOffset: parseFloat(configDef.querySelector('textNameOffset')?.textContent || '0'),
|
||||
optionsTextOffset: parseFloat(configDef.querySelector('optionsTextOffset')?.textContent || '0')
|
||||
};
|
||||
|
||||
const windowRect = { width: 1000, height: 750 };
|
||||
const windowDiv = document.getElementById('window');
|
||||
windowDiv.innerHTML = ''; // Clear previous preview
|
||||
|
||||
// --- Calculation logic from Dialog_CustomDisplay.cs ---
|
||||
const virtualWidth = config.lihuiSize.x + config.textSize.x;
|
||||
const virtualHeight = config.lihuiSize.y;
|
||||
|
||||
const scaleX = windowRect.width / virtualWidth;
|
||||
const scaleY = windowRect.height / virtualHeight;
|
||||
const scale = Math.min(scaleX, scaleY) * 0.95;
|
||||
|
||||
const scaledLihuiWidth = config.lihuiSize.x * scale;
|
||||
const scaledLihuiHeight = config.lihuiSize.y * scale;
|
||||
const scaledNameWidth = config.nameSize.x * scale;
|
||||
const scaledNameHeight = config.nameSize.y * scale;
|
||||
const scaledTextWidth = config.textSize.x * scale;
|
||||
const scaledTextHeight = config.textSize.y * scale;
|
||||
const scaledOptionsWidth = config.optionsWidth * scale;
|
||||
|
||||
const totalContentWidth = scaledLihuiWidth + scaledTextWidth;
|
||||
const totalContentHeight = scaledLihuiHeight;
|
||||
const startX = (windowRect.width - totalContentWidth) / 2;
|
||||
const startY = (windowRect.height - totalContentHeight) / 2;
|
||||
|
||||
// --- Create and position elements ---
|
||||
|
||||
// Lihui (Portrait)
|
||||
const lihuiRect = { left: startX, top: startY, width: scaledLihuiWidth, height: scaledLihuiHeight };
|
||||
createDiv('lihui', '立绘 (Portrait)', lihuiRect);
|
||||
|
||||
// Name
|
||||
const nameRect = { left: lihuiRect.left + lihuiRect.width, top: lihuiRect.top, width: scaledNameWidth, height: scaledNameHeight };
|
||||
createDiv('name', '名称 (Name)', nameRect);
|
||||
|
||||
// Text (Description)
|
||||
const textRect = { left: nameRect.left, top: nameRect.top + nameRect.height + (config.textNameOffset * scale), width: scaledTextWidth, height: scaledTextHeight };
|
||||
createDiv('text', '描述 (Description)', textRect);
|
||||
|
||||
// Options
|
||||
const optionRect = { left: nameRect.left, top: textRect.top + textRect.height + (config.optionsTextOffset * scale), width: scaledOptionsWidth, height: lihuiRect.height - nameRect.height - textRect.height - ((config.textNameOffset + config.optionsTextOffset) * scale) };
|
||||
createDiv('options', '选项 (Options)', optionRect);
|
||||
}
|
||||
|
||||
function createDiv(className, text, rect) {
|
||||
const div = document.createElement('div');
|
||||
div.className = `element ${className}`;
|
||||
div.textContent = text;
|
||||
div.style.left = `${rect.left}px`;
|
||||
div.style.top = `${rect.top}px`;
|
||||
div.style.width = `${rect.width}px`;
|
||||
div.style.height = `${rect.height}px`;
|
||||
document.getElementById('window').appendChild(div);
|
||||
}
|
||||
|
||||
// Auto-populate with example content
|
||||
document.getElementById('xmlInput').value = `<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Defs>
|
||||
<WulaFallenEmpire.EventUIConfigDef>
|
||||
<defName>Wula_EventUIConfig</defName>
|
||||
|
||||
<!-- General Style -->
|
||||
<labelFont>Small</labelFont>
|
||||
<drawBorders>true</drawBorders>
|
||||
<defaultBackgroundImagePath></defaultBackgroundImagePath>
|
||||
|
||||
<!-- Virtual Layout Dimensions -->
|
||||
<lihuiSize>(500, 800)</lihuiSize>
|
||||
<nameSize>(260, 130)</nameSize>
|
||||
<textSize>(650, 500)</textSize>
|
||||
<optionsWidth>610</optionsWidth>
|
||||
|
||||
<!-- Virtual Layout Offsets -->
|
||||
<textNameOffset>20</textNameOffset>
|
||||
<optionsTextOffset>20</optionsTextOffset>
|
||||
|
||||
</WulaFallenEmpire.EventUIConfigDef>
|
||||
</Defs>`;
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,441 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
import json
|
||||
import re
|
||||
|
||||
# 1. --- 导入库 ---
|
||||
# mcp 库已通过 'pip install -e' 安装,无需修改 sys.path
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
# 新增:阿里云模型服务和向量计算库
|
||||
import dashscope
|
||||
from dashscope.api_entities.dashscope_response import Role
|
||||
from tenacity import retry, stop_after_attempt, wait_random_exponential
|
||||
from sklearn.metrics.pairwise import cosine_similarity
|
||||
import numpy as np
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# 2. --- 日志、缓存和知识库配置 ---
|
||||
MCP_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
LOG_FILE_PATH = os.path.join(MCP_DIR, 'mcpserver.log')
|
||||
CACHE_DIR = os.path.join(MCP_DIR, 'vector_cache')
|
||||
CACHE_FILE_PATH = os.path.join(CACHE_DIR, 'knowledge_cache.json')
|
||||
os.makedirs(CACHE_DIR, exist_ok=True)
|
||||
|
||||
logging.basicConfig(filename=LOG_FILE_PATH, level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
encoding='utf-8')
|
||||
|
||||
# 新增: 加载 .env 文件并设置 API Key
|
||||
# 指定 .env 文件的确切路径,以确保脚本在任何工作目录下都能正确加载
|
||||
env_path = os.path.join(MCP_DIR, '.env')
|
||||
load_dotenv(dotenv_path=env_path)
|
||||
|
||||
dashscope.api_key = os.getenv("DASHSCOPE_API_KEY")
|
||||
|
||||
if not dashscope.api_key:
|
||||
logging.error("错误:未在 .env 文件中找到或加载 DASHSCOPE_API_KEY。")
|
||||
# 如果没有Key,服务器无法工作,可以选择退出或继续运行但功能受限
|
||||
# sys.exit("错误:API Key 未配置。")
|
||||
else:
|
||||
logging.info("成功加载 DASHSCOPE_API_KEY。")
|
||||
|
||||
# 定义知识库路径
|
||||
KNOWLEDGE_BASE_PATHS = [
|
||||
r"C:\Steam\steamapps\common\RimWorld\Data"
|
||||
]
|
||||
|
||||
# 3. --- 缓存管理 (分文件存储) ---
|
||||
def load_cache_for_keyword(keyword: str):
|
||||
"""为指定关键词加载缓存文件。"""
|
||||
# 清理关键词,使其适合作为文件名
|
||||
safe_filename = "".join(c for c in keyword if c.isalnum() or c in ('_', '-')).rstrip()
|
||||
cache_file = os.path.join(CACHE_DIR, f"{safe_filename}.txt")
|
||||
|
||||
if os.path.exists(cache_file):
|
||||
try:
|
||||
with open(cache_file, 'r', encoding='utf-8') as f:
|
||||
return f.read()
|
||||
except IOError as e:
|
||||
logging.error(f"读取缓存文件 {cache_file} 失败: {e}")
|
||||
return None
|
||||
return None
|
||||
|
||||
def save_cache_for_keyword(keyword: str, data: str):
|
||||
"""为指定关键词保存缓存到单独的文件。"""
|
||||
safe_filename = "".join(c for c in keyword if c.isalnum() or c in ('_', '-')).rstrip()
|
||||
cache_file = os.path.join(CACHE_DIR, f"{safe_filename}.txt")
|
||||
|
||||
try:
|
||||
with open(cache_file, 'w', encoding='utf-8') as f:
|
||||
f.write(data)
|
||||
except IOError as e:
|
||||
logging.error(f"写入缓存文件 {cache_file} 失败: {e}")
|
||||
|
||||
# 4. --- 向量化与相似度计算 ---
|
||||
@retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6))
|
||||
def get_embedding(text: str):
|
||||
"""获取文本的向量嵌入"""
|
||||
try:
|
||||
# 根据用户文档,选用v4模型,更适合代码和文本
|
||||
response = dashscope.TextEmbedding.call(
|
||||
model='text-embedding-v4',
|
||||
input=text
|
||||
)
|
||||
if response.status_code == 200:
|
||||
return response.output['embeddings'][0]['embedding']
|
||||
else:
|
||||
logging.error(f"获取向量失败: {response.message}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logging.error(f"调用向量API时出错: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
def find_most_similar_files(question_embedding, file_embeddings, top_n=3, min_similarity=0.5):
|
||||
"""在文件向量中找到与问题向量最相似的 top_n 个文件。"""
|
||||
if not question_embedding or not file_embeddings:
|
||||
return []
|
||||
|
||||
file_vectors = np.array([emb['embedding'] for emb in file_embeddings])
|
||||
question_vector = np.array(question_embedding).reshape(1, -1)
|
||||
|
||||
similarities = cosine_similarity(question_vector, file_vectors)[0]
|
||||
|
||||
# 获取排序后的索引
|
||||
sorted_indices = np.argsort(similarities)[::-1]
|
||||
|
||||
# 筛选出最相关的结果
|
||||
results = []
|
||||
for i in sorted_indices:
|
||||
similarity_score = similarities[i]
|
||||
if similarity_score >= min_similarity and len(results) < top_n:
|
||||
results.append({
|
||||
'path': file_embeddings[i]['path'],
|
||||
'similarity': similarity_score
|
||||
})
|
||||
else:
|
||||
break
|
||||
|
||||
return results
|
||||
|
||||
def extract_relevant_code(file_path, keyword):
|
||||
"""从文件中智能提取包含关键词的完整代码块 (C#类 或 XML Def)。"""
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
lines = content.split('\n')
|
||||
keyword_lower = keyword.lower()
|
||||
|
||||
found_line_index = -1
|
||||
for i, line in enumerate(lines):
|
||||
if keyword_lower in line.lower():
|
||||
found_line_index = i
|
||||
break
|
||||
|
||||
if found_line_index == -1:
|
||||
return ""
|
||||
|
||||
# 根据文件类型选择提取策略
|
||||
if file_path.endswith(('.cs', '.txt')):
|
||||
# C# 提取策略:寻找完整的类
|
||||
return extract_csharp_class(lines, found_line_index)
|
||||
elif file_path.endswith('.xml'):
|
||||
# XML 提取策略:寻找完整的 Def
|
||||
return extract_xml_def(lines, found_line_index)
|
||||
else:
|
||||
return "" # 不支持的文件类型
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"提取代码时出错 {file_path}: {e}")
|
||||
return f"# Error reading file: {e}"
|
||||
|
||||
def extract_csharp_class(lines, start_index):
|
||||
"""从C#代码行中提取完整的类定义。"""
|
||||
# 向上找到 class 声明
|
||||
class_start_index = -1
|
||||
brace_level_at_class_start = -1
|
||||
for i in range(start_index, -1, -1):
|
||||
line = lines[i]
|
||||
if 'class ' in line:
|
||||
class_start_index = i
|
||||
brace_level_at_class_start = line.count('{') - line.count('}')
|
||||
break
|
||||
|
||||
if class_start_index == -1: return "" # 没找到类
|
||||
|
||||
# 从 class 声明开始,向下找到匹配的 '}'
|
||||
brace_count = brace_level_at_class_start
|
||||
class_end_index = -1
|
||||
for i in range(class_start_index + 1, len(lines)):
|
||||
line = lines[i]
|
||||
brace_count += line.count('{')
|
||||
brace_count -= line.count('}')
|
||||
if brace_count <= 0: # 找到匹配的闭合括号
|
||||
class_end_index = i
|
||||
break
|
||||
|
||||
if class_end_index != -1:
|
||||
return "\n".join(lines[class_start_index:class_end_index+1])
|
||||
return "" # 未找到完整的类块
|
||||
|
||||
def extract_xml_def(lines, start_index):
|
||||
"""从XML行中提取完整的Def块。"""
|
||||
import re
|
||||
# 向上找到 <DefName> 或 <defName>
|
||||
def_start_index = -1
|
||||
def_tag = ""
|
||||
for i in range(start_index, -1, -1):
|
||||
line = lines[i].strip()
|
||||
match = re.match(r'<(\w+)\s+.*>', line) or re.match(r'<(\w+)>', line)
|
||||
if match and ('Def' in match.group(1) or 'def' in match.group(1)):
|
||||
# 这是一个简化的判断,实际中可能需要更复杂的逻辑
|
||||
def_start_index = i
|
||||
def_tag = match.group(1)
|
||||
break
|
||||
|
||||
if def_start_index == -1: return ""
|
||||
|
||||
# 向下找到匹配的 </DefName>
|
||||
def_end_index = -1
|
||||
for i in range(def_start_index + 1, len(lines)):
|
||||
if f'</{def_tag}>' in lines[i]:
|
||||
def_end_index = i
|
||||
break
|
||||
|
||||
if def_end_index != -1:
|
||||
return "\n".join(lines[def_start_index:def_end_index+1])
|
||||
return ""
|
||||
|
||||
# 5. --- 核心功能函数 ---
|
||||
def find_files_with_keyword(roots, keywords: list[str], extensions=['.xml', '.cs', '.txt']):
|
||||
"""在指定目录中查找包含任何一个关键字的文件。"""
|
||||
found_files = set()
|
||||
keywords_lower = [k.lower() for k in keywords]
|
||||
for root_path in roots:
|
||||
if not os.path.isdir(root_path):
|
||||
logging.warning(f"知识库路径不存在或不是一个目录: {root_path}")
|
||||
continue
|
||||
for dirpath, _, filenames in os.walk(root_path):
|
||||
for filename in filenames:
|
||||
if any(filename.lower().endswith(ext) for ext in extensions):
|
||||
file_path = os.path.join(dirpath, filename)
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content_lower = f.read().lower()
|
||||
# 如果任何一个关键词在内容中,就添加文件
|
||||
if any(kw in content_lower for kw in keywords_lower):
|
||||
found_files.add(file_path)
|
||||
except Exception as e:
|
||||
logging.error(f"读取文件时出错 {file_path}: {e}")
|
||||
return list(found_files)
|
||||
|
||||
def find_keywords_in_question(question: str) -> list[str]:
|
||||
"""从问题中提取所有可能的关键词 (类型名, defName等)。"""
|
||||
# 正则表达式优先,用于精确匹配定义
|
||||
# 匹配 C# class, struct, enum, interface 定义, 例如 "public class MyClass : Base"
|
||||
csharp_def_pattern = re.compile(r'\b(?:public|private|internal|protected|sealed|abstract|static|new)\s+(?:class|struct|enum|interface)\s+([A-Za-z_][A-Za-z0-9_]*)')
|
||||
# 匹配 XML Def, 例如 "<ThingDef Name="MyDef">" or "<MyCustomDef>"
|
||||
xml_def_pattern = re.compile(r'<([A-Za-z_][A-Za-z0-9_]*Def)\b')
|
||||
|
||||
# 启发式规则,用于匹配独立的关键词
|
||||
# 规则1: 包含下划线 (很可能是 defName)
|
||||
# 规则2: 混合大小写 (很可能是 C# 类型名)
|
||||
# 规则3: 多个大写字母(例如 CompPsychicScaling,但要排除纯大写缩写词)
|
||||
|
||||
# 排除常见但非特定的术语
|
||||
excluded_keywords = {"XML", "C#", "DEF", "CS", "CLASS", "PUBLIC"}
|
||||
|
||||
found_keywords = set()
|
||||
|
||||
# 1. 正则匹配
|
||||
csharp_matches = csharp_def_pattern.findall(question)
|
||||
xml_matches = xml_def_pattern.findall(question)
|
||||
|
||||
for match in csharp_matches:
|
||||
found_keywords.add(match)
|
||||
for match in xml_matches:
|
||||
found_keywords.add(match)
|
||||
|
||||
# 2. 启发式单词匹配
|
||||
parts = re.split(r'[\s,.:;\'"`()<>]+', question)
|
||||
|
||||
for part in parts:
|
||||
if not part or part.upper() in excluded_keywords:
|
||||
continue
|
||||
|
||||
# 规则1: 包含下划线
|
||||
if '_' in part:
|
||||
found_keywords.add(part)
|
||||
# 规则2: 驼峰命名或混合大小写
|
||||
elif any(c.islower() for c in part) and any(c.isupper() for c in part) and len(part) > 3:
|
||||
found_keywords.add(part)
|
||||
# 规则3: 多个大写字母
|
||||
elif sum(1 for c in part if c.isupper()) > 1 and not part.isupper():
|
||||
found_keywords.add(part)
|
||||
# 备用规则: 大写字母开头且较长
|
||||
elif part[0].isupper() and len(part) > 4:
|
||||
found_keywords.add(part)
|
||||
|
||||
if not found_keywords:
|
||||
logging.warning(f"在 '{question}' 中未找到合适的关键词。")
|
||||
return []
|
||||
|
||||
logging.info(f"找到的潜在关键词: {list(found_keywords)}")
|
||||
return list(found_keywords)
|
||||
|
||||
|
||||
# 5. --- 创建和配置 MCP 服务器 ---
|
||||
# 使用 FastMCP 创建服务器实例
|
||||
mcp = FastMCP(
|
||||
"rimworld-knowledge-base",
|
||||
"1.0.0-fastmcp",
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
def get_context(question: str) -> str:
|
||||
"""
|
||||
根据问题中的关键词和向量相似度,在RimWorld知识库中搜索最相关的多个代码片段,
|
||||
并将其整合后返回。
|
||||
"""
|
||||
logging.info(f"收到问题: {question}")
|
||||
keywords = find_keywords_in_question(question)
|
||||
if not keywords:
|
||||
logging.warning("无法从问题中提取关键词。")
|
||||
return "无法从问题中提取关键词,请提供更具体的信息。"
|
||||
|
||||
logging.info(f"提取到关键词: {keywords}")
|
||||
|
||||
# 基于所有关键词创建缓存键
|
||||
cache_key = "-".join(sorted(keywords))
|
||||
|
||||
# 1. 检查缓存
|
||||
cached_result = load_cache_for_keyword(cache_key)
|
||||
if cached_result:
|
||||
logging.info(f"缓存命中: 关键词 '{cache_key}'")
|
||||
return cached_result
|
||||
|
||||
logging.info(f"缓存未命中,开始实时搜索: {cache_key}")
|
||||
|
||||
# 2. 关键词文件搜索 (分层智能筛选)
|
||||
try:
|
||||
# 优先使用最长的(通常最具体)的关键词进行搜索
|
||||
specific_keywords = sorted(keywords, key=len, reverse=True)
|
||||
candidate_files = find_files_with_keyword(KNOWLEDGE_BASE_PATHS, [specific_keywords[0]])
|
||||
|
||||
# 如果最具体的关键词找不到文件,再尝试所有关键词
|
||||
if not candidate_files and len(keywords) > 1:
|
||||
logging.info(f"使用最具体的关键词 '{specific_keywords[0]}' 未找到文件,尝试所有关键词...")
|
||||
candidate_files = find_files_with_keyword(KNOWLEDGE_BASE_PATHS, keywords)
|
||||
|
||||
if not candidate_files:
|
||||
logging.info(f"未找到与 '{keywords}' 相关的文件。")
|
||||
return f"未在知识库中找到与 '{keywords}' 相关的文件定义。"
|
||||
|
||||
logging.info(f"找到 {len(candidate_files)} 个候选文件,开始向量化处理...")
|
||||
|
||||
# 新增:文件名精确匹配优先
|
||||
priority_results = []
|
||||
remaining_files = []
|
||||
for file_path in candidate_files:
|
||||
filename_no_ext = os.path.splitext(os.path.basename(file_path))[0]
|
||||
is_priority = False
|
||||
for keyword in keywords:
|
||||
if filename_no_ext.lower() == keyword.lower():
|
||||
logging.info(f"文件名精确匹配: {file_path}")
|
||||
code_block = extract_relevant_code(file_path, keyword)
|
||||
if code_block:
|
||||
lang = "csharp" if file_path.endswith(('.cs', '.txt')) else "xml"
|
||||
priority_results.append(
|
||||
f"---\n"
|
||||
f"**文件路径 (精确匹配):** `{file_path}`\n\n"
|
||||
f"```{lang}\n"
|
||||
f"{code_block}\n"
|
||||
f"```"
|
||||
)
|
||||
is_priority = True
|
||||
break # 已处理该文件,跳出内层循环
|
||||
if not is_priority:
|
||||
remaining_files.append(file_path)
|
||||
|
||||
candidate_files = remaining_files # 更新候选文件列表,排除已优先处理的文件
|
||||
|
||||
# 3. 向量化和相似度计算 (精准筛选)
|
||||
# 增加超时保护:限制向量化的文件数量
|
||||
MAX_FILES_TO_VECTORIZE = 25
|
||||
if len(candidate_files) > MAX_FILES_TO_VECTORIZE:
|
||||
logging.warning(f"候选文件过多 ({len(candidate_files)}),仅处理前 {MAX_FILES_TO_VECTORIZE} 个。")
|
||||
candidate_files = candidate_files[:MAX_FILES_TO_VECTORIZE]
|
||||
|
||||
question_embedding = get_embedding(question)
|
||||
if not question_embedding:
|
||||
return "无法生成问题向量,请检查API连接或问题内容。"
|
||||
|
||||
file_embeddings = []
|
||||
for file_path in candidate_files:
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
file_embedding = get_embedding(content[:8000]) # 限制内容长度以提高效率
|
||||
if file_embedding:
|
||||
file_embeddings.append({'path': file_path, 'embedding': file_embedding})
|
||||
except Exception as e:
|
||||
logging.error(f"处理文件 {file_path} 时出错: {e}")
|
||||
|
||||
if not file_embeddings:
|
||||
return "无法为任何候选文件生成向量。"
|
||||
|
||||
# 找到最相似的多个文件
|
||||
best_matches = find_most_similar_files(question_embedding, file_embeddings, top_n=5) # 增加返回数量
|
||||
|
||||
if not best_matches:
|
||||
return "计算向量相似度失败或没有找到足够相似的文件。"
|
||||
|
||||
# 4. 提取代码并格式化输出
|
||||
output_parts = [f"根据向量相似度分析,与 '{', '.join(keywords)}' 最相关的代码定义如下:\n"]
|
||||
output_parts.extend(priority_results) # 将优先结果放在最前面
|
||||
|
||||
extracted_blocks = set() # 用于防止重复提取相同的代码块
|
||||
|
||||
for match in best_matches:
|
||||
file_path = match['path']
|
||||
similarity = match['similarity']
|
||||
|
||||
# 对每个关键词都尝试提取代码
|
||||
for keyword in keywords:
|
||||
code_block = extract_relevant_code(file_path, keyword)
|
||||
|
||||
if code_block and code_block not in extracted_blocks:
|
||||
extracted_blocks.add(code_block)
|
||||
lang = "csharp" if file_path.endswith(('.cs', '.txt')) else "xml"
|
||||
output_parts.append(
|
||||
f"---\n"
|
||||
f"**文件路径:** `{file_path}`\n"
|
||||
f"**相似度:** {similarity:.4f}\n\n"
|
||||
f"```{lang}\n"
|
||||
f"{code_block}\n"
|
||||
f"```"
|
||||
)
|
||||
|
||||
if len(output_parts) <= 1:
|
||||
return f"虽然找到了相似的文件,但无法在其中提取到关于 '{', '.join(keywords)}' 的完整代码块。"
|
||||
|
||||
final_output = "\n".join(output_parts)
|
||||
|
||||
# 5. 更新缓存并返回结果
|
||||
logging.info(f"向量搜索完成。找到了 {len(best_matches)} 个匹配项并成功提取了代码。")
|
||||
save_cache_for_keyword(cache_key, final_output)
|
||||
|
||||
return final_output
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"处理请求时发生意外错误: {e}", exc_info=True)
|
||||
return f"处理您的请求时发生错误: {e}"
|
||||
|
||||
# 6. --- 启动服务器 ---
|
||||
# FastMCP 实例可以直接运行
|
||||
if __name__ == "__main__":
|
||||
logging.info("RimWorld 向量知识库 (FastMCP版, v2.1-v4-model) 正在启动...")
|
||||
# 使用 'stdio' 传输协议
|
||||
mcp.run(transport="stdio")
|
||||
BIN
Source/美术与文本源文件/Wula/Apparel/WULA_Body_Suit_Thin_south.sai2
Normal file
BIN
Source/美术与文本源文件/Wula/Apparel/WULA_Bodystocking_Thin_south.sai2
Normal file
BIN
Source/美术与文本源文件/Wula/Apparel/WULA_Knight_Helmet_south.sai2
Normal file
BIN
Source/美术与文本源文件/Wula/Apparel/WULA_Maid_Uniform_Headband.sai2
Normal file
BIN
Source/美术与文本源文件/Wula/Apparel/WULA_Maid_Uniform_Thin_south.sai2
Normal file
BIN
Source/美术与文本源文件/Wula/Apparel/WULA_Nun_Uniform_Thin_south.sai2
Normal file
BIN
Source/美术与文本源文件/Wula/Apparel/WULA_Nun_veil_south.sai2
Normal file
BIN
Source/美术与文本源文件/Wula/Apparel/WULA_Nurse_Uniform_Thin_south.sai2
Normal file
BIN
Source/美术与文本源文件/Wula/Apparel/WULA_Priest_Hat_south.sai2
Normal file
BIN
Source/美术与文本源文件/Wula/Apparel/WULA_Priest_Uniform_Thin_south.sai2
Normal file
BIN
Source/美术与文本源文件/Wula/Apparel/WULA_Qipao_Thin_south.sai2
Normal file
BIN
Source/美术与文本源文件/Wula/Apparel/WULA_Sailor_Dress_Thin_east.sai2
Normal file
BIN
Source/美术与文本源文件/Wula/Apparel/WULA_Sailor_Dress_Thin_north.sai2
Normal file
BIN
Source/美术与文本源文件/Wula/Apparel/WULA_Sailor_Dress_Thin_south.sai2
Normal file
BIN
Source/美术与文本源文件/Wula/Apparel/WULA_Shield_Base_Thin_south.sai2
Normal file
BIN
Source/美术与文本源文件/Wula/Apparel/WULA_Skitarii_veil_south.sai2
Normal file
BIN
Source/美术与文本源文件/Wula/Building/Door/WulaAutodoor.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
Source/美术与文本源文件/Wula/Building/Door/WulaAutodoor_BluePrint.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
Source/美术与文本源文件/Wula/Building/Door/WulaAutodoor_Mover.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
Source/美术与文本源文件/Wula/Building/Door/WulaAutodoor_Mover.sai2
Normal file
BIN
Source/美术与文本源文件/Wula/Building/Door/WulaAutodoor_Mover_east.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
Source/美术与文本源文件/Wula/Building/Door/WulaAutodoor_Mover_south.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
Source/美术与文本源文件/Wula/Building/Door/WulaAutodoor_east.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
Source/美术与文本源文件/Wula/Building/Door/WulaAutodoor_south.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 722 B |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
BIN
Source/美术与文本源文件/Wula/Building/Linked/WulaWall/WulaWall_Atlas.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
Source/美术与文本源文件/Wula/Building/WULA_Chair.sai2
Normal file
BIN
Source/美术与文本源文件/Wula/Building/WULA_Communicator_Station.sai2
Normal file
BIN
Source/美术与文本源文件/Wula/Building/WULA_Cube_Productor_BIO.sai2
Normal file
BIN
Source/美术与文本源文件/Wula/Building/WULA_Cube_Productor_Component.sai2
Normal file
BIN
Source/美术与文本源文件/Wula/Building/WULA_Nourishment_Center_Unit.sai2
Normal file
BIN
Source/美术与文本源文件/Wula/Building/WULA_Synth_Maintainer_south.sai2
Normal file
BIN
Source/美术与文本源文件/Wula/Building/WULA_Synth_Server_south.sai2
Normal file
BIN
Source/美术与文本源文件/Wula/Building/WULA_Turret_Component.sai2
Normal file
BIN
Source/美术与文本源文件/Wula/Building/Wula_DarkEnergy_FuelTank.sai2
Normal file
BIN
Source/美术与文本源文件/Wula/Hair/WULA_Double_Ponytail_Long_east.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
Source/美术与文本源文件/Wula/Hair/WULA_Double_Ponytail_Long_north.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
Source/美术与文本源文件/Wula/Hair/WULA_Double_Ponytail_Long_south.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
Source/美术与文本源文件/Wula/Hair/WULA_Double_Ponytail_Long_south.sai2
Normal file
BIN
Source/美术与文本源文件/Wula/Hair/WULA_High_Ponytail_east.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
Source/美术与文本源文件/Wula/Hair/WULA_High_Ponytail_north.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
Source/美术与文本源文件/Wula/Hair/WULA_High_Ponytail_south.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
Source/美术与文本源文件/Wula/Hair/WULA_High_Ponytail_south.sai2
Normal file
BIN
Source/美术与文本源文件/Wula/Hair/WULA_One_Side_Ponytail_east.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
Source/美术与文本源文件/Wula/Hair/WULA_One_Side_Ponytail_north.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
Source/美术与文本源文件/Wula/Hair/WULA_One_Side_Ponytail_south.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
Source/美术与文本源文件/Wula/Hair/WULA_One_Side_Ponytail_south.sai2
Normal file
BIN
Source/美术与文本源文件/Wula/Hair/WULA_One_Side_Ponytail_west.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
Source/美术与文本源文件/Wula/Hair/WULA_Rotational_Ponytail_east.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
Source/美术与文本源文件/Wula/Hair/WULA_Rotational_Ponytail_north.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
Source/美术与文本源文件/Wula/Hair/WULA_Rotational_Ponytail_south.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
Source/美术与文本源文件/Wula/Hair/WULA_Rotational_Ponytail_south.sai2
Normal file
BIN
Source/美术与文本源文件/Wula/Hair/WULA_Scattered_Hair_Long_east.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
Source/美术与文本源文件/Wula/Hair/WULA_Scattered_Hair_Long_north.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
Source/美术与文本源文件/Wula/Hair/WULA_Scattered_Hair_Long_south.png
Normal file
|
After Width: | Height: | Size: 34 KiB |