暂存文档

This commit is contained in:
2025-09-02 15:48:16 +08:00
parent d335b2008c
commit 6129bb1b50

View File

@@ -0,0 +1,227 @@
# 技术文档RimWorld 中 `specialTrainables` 的工作机制解析 (V6 - 终极证据版)
## 1. 目标
本文档旨在深入剖析 RimWorld 中 `<specialTrainables>` 标签的底层工作机制,并最终得出一套可复用的“配方”,用于让动物学会在无需训练、无衰减的情况下,执行任何原版已存在的工作。本文档包含所有必要的、完整的原版代码引用作为证据,以确保其自包含性、正确性和健壮性,可供任何人在无上下文的情况下阅读并执行。
## 2. 核心机制深度解析
`specialTrainables` 的功能是一套精巧的、深度集成在 AI 思维逻辑中的 **条件行为分支**
### 2.1. 玩家授权机制
**关键发现**: 游戏通过一个统一的 UI 列 (`PawnColumnWorker_Trainable_Special`) 来管理所有被标记为 `specialTrainable` 的技能。当玩家点击该列的复选框时,会调用 `pawn.training.SetWantedRecursive()` 来设置 `Pawn_TrainingTracker``wantedTrainables` 字段的值。
* **证据**: 以下是 `PawnColumnWorker_Trainable_Special.txt` 的完整内容,它清晰地展示了 `SetWantedRecursive` 是如何被调用的。
```csharp
using System.Text;
using UnityEngine;
using Verse;
namespace RimWorld
{
public class PawnColumnWorker_Trainable_Special : PawnColumnWorker
{
public override void DoHeader(Rect rect, PawnTable table)
{
base.DoHeader(rect, table);
MouseoverSounds.DoRegion(rect);
}
public override void DoCell(Rect rect, Pawn pawn, PawnTable table)
{
if (pawn.training != null && !pawn.RaceProps.specialTrainables.NullOrEmpty())
{
int num = (int)((rect.width - 24f) / 2f);
int num2 = Mathf.Max(3, 0);
Rect rect2 = new Rect(rect.x + (float)num, rect.y + (float)num2, 24f, 24f);
DoSpecialTrainableCheckbox(rect2, pawn, doTooltip: true);
}
}
private void DoSpecialTrainableCheckbox(Rect rect, Pawn pawn, bool doTooltip)
{
GetStatus(pawn, out var learned, out var checkOn, out var canTrain, out var _);
bool flag = checkOn;
Texture2D texChecked = (learned ? TrainingCardUtility.LearnedTrainingTex : null);
Texture2D texUnchecked = (learned ? TrainingCardUtility.LearnedNotTrainingTex : null);
Widgets.Checkbox(rect.position, ref checkOn, rect.width, !canTrain, paintable: true, texChecked, texUnchecked);
if (checkOn != flag)
{
PlayerKnowledgeDatabase.KnowledgeDemonstrated(ConceptDefOf.AnimalTraining, KnowledgeAmount.Total);
foreach (TrainableDef specialTrainable in pawn.RaceProps.specialTrainables)
{
pawn.training.SetWantedRecursive(specialTrainable, checkOn);
}
}
if (doTooltip)
{
DoSpecialTrainableTooltip(rect, pawn);
}
}
private void DoSpecialTrainableTooltip(Rect rect, Pawn pawn)
{
if (!Mouse.IsOver(rect))
{
return;
}
TooltipHandler.TipRegion(rect, delegate
{
StringBuilder stringBuilder = new StringBuilder();
foreach (TrainableDef specialTrainable in pawn.RaceProps.specialTrainables)
{
bool visible;
AcceptanceReport acceptanceReport = pawn.training.CanAssignToTrain(specialTrainable, out visible);
stringBuilder.AppendLineIfNotEmpty();
stringBuilder.AppendLine(specialTrainable.LabelCap + "\n\n" + specialTrainable.description);
if (!acceptanceReport.Accepted)
{
stringBuilder.AppendLine().AppendLine(acceptanceReport.Reason);
}
else if (!specialTrainable.prerequisites.NullOrEmpty())
{
stringBuilder.AppendLine();
foreach (TrainableDef prerequisite in specialTrainable.prerequisites)
{
if (!pawn.training.HasLearned(prerequisite))
{
stringBuilder.AppendLine("TrainingNeedsPrerequisite".Translate(prerequisite.LabelCap));
}
}
}
}
return stringBuilder.ToString();
}, (int)(rect.y * 511f + rect.x));
}
public override int GetMinWidth(PawnTable table)
{
return Mathf.Max(base.GetMinWidth(table), 24);
}
public override int GetMaxWidth(PawnTable table)
{
return Mathf.Min(base.GetMaxWidth(table), GetMinWidth(table));
}
public override int GetMinCellHeight(Pawn pawn)
{
return Mathf.Max(base.GetMinCellHeight(pawn), 24);
}
public override int Compare(Pawn a, Pawn b)
{
return GetValueToCompare(a).CompareTo(GetValueToCompare(b));
}
private int GetValueToCompare(Pawn pawn)
{
if (pawn.training == null || pawn.RaceProps.specialTrainables.NullOrEmpty())
{
return int.MinValue;
}
GetStatus(pawn, out var learned, out var checkOn, out var canTrain, out var visible);
if (learned)
{
return 4;
}
if (!visible)
{
return 0;
}
if (!canTrain)
{
return 1;
}
if (!checkOn)
{
return 2;
}
return 3;
}
private static void GetStatus(Pawn pawn, out bool learned, out bool checkOn, out bool canTrain, out bool visible)
{
learned = true;
checkOn = true;
canTrain = true;
visible = false;
foreach (TrainableDef specialTrainable in pawn.RaceProps.specialTrainables)
{
if (!pawn.training.HasLearned(specialTrainable))
{
learned = false;
}
if (!pawn.training.GetWanted(specialTrainable))
{
checkOn = false;
}
if (!pawn.training.CanAssignToTrain(specialTrainable, out var visible2))
{
canTrain = false;
}
if (visible2)
{
visible = true;
}
}
}
}
}
```
### 2.2. AI 决策机制
* **关键发现**: 我们可以为每个技能创建一个继承自 `ThinkNode_Conditional` 的决策节点,在 `Satisfied` 方法中通过检查 `pawn.training.GetWanted(def)` 来判断玩家是否授权。
## 3. 最终实施蓝图
### 阶段一:核心机制实现 (通用)
* **1.1: 实现瞬间训练 (`CompInstantTrain.cs`)**: 创建一个 `ThingComp`,在 `PostSpawnSetup` 中调用 `pawn.training.Train(def, null, true)`。
* **1.2: 引入 Harmony**: 在项目中添加 `0Harmony.dll` 引用并创建 `MainHarmony.cs` 初始化补丁。
* **1.3: 阻止训练衰减 (`Patch_TrainingTracker_TickRare.cs`)**: 创建一个对 `Pawn_TrainingTracker.TrainingTrackerTickRare` 的前缀补丁,对特殊动物返回 `false`。
### 阶段二:实现“种植” (`Growing`) 功能
* **2.1: (XML) 创建 `TrainableDef`**: 创建 `Defs/TrainableDefs/ARA_Sowing.xml`,定义 `ARA_Sowing`,并设 `<specialTrainable>true</specialTrainable>`。
* **2.2: (C#) 创建 AI 决策节点**: 创建 `Source/ArachnaeSwarm/ThinkNode_ConditionalAnimalShouldSow.cs`,并使用静态缓存 Def。
```csharp
using Verse;
using Verse.AI;
namespace ArachnaeSwarm
{
[DefOf]
public static class ARA_TrainableDefOf
{
public static TrainableDef ARA_Sowing;
public static TrainableDef ARA_PlantCutting;
static ARA_TrainableDefOf() { DefOfHelper.EnsureInitializedInCtor(typeof(ARA_TrainableDefOf)); }
}
public class ThinkNode_ConditionalAnimalShouldSow : ThinkNode_Conditional
{
protected override bool Satisfied(Pawn pawn)
{
if (pawn.training == null) return false;
return pawn.training.HasLearned(ARA_TrainableDefOf.ARA_Sowing) &&
pawn.training.GetWanted(ARA_TrainableDefOf.ARA_Sowing);
}
}
}
```
* **2.3: (XML) 关联组件**:
* **PawnKindDef**: 在 `comps` 列表中添加 `CompProperties_InstantTrain` 并配置 `trainables`。
* **ThingDef**: 在 `specialTrainables` 列表中添加 `<li>ARA_Sowing</li>`。
* **ThinkTreeDef**: 添加 `<li Class="ArachnaeSwarm.ThinkNode_ConditionalAnimalShouldSow">` 节点,其子节点为 `<li Class="RimWorld.JobGiver_Work"><workType>Growing</workType></li>`。
### 阶段三:实现“植物割除” (`PlantCutting`) 功能
此阶段完全重复阶段二的模式,仅替换相应的名称 (`ARA_PlantCutting`, `ThinkNode_ConditionalAnimalShouldPlantCut`, `workType: PlantCutting`)。
### 阶段四:最终审查与打包
* **4.1**: 审查所有代码和 XML。
* **4.2**: 编译并进行游戏内测试。