Files
ArachnaeSwarm/Source/Documents/How_SpecialTrainables_Work.md
2025-09-02 15:48:16 +08:00

9.9 KiB
Raw Blame History

技术文档RimWorld 中 specialTrainables 的工作机制解析 (V6 - 终极证据版)

1. 目标

本文档旨在深入剖析 RimWorld 中 <specialTrainables> 标签的底层工作机制,并最终得出一套可复用的“配方”,用于让动物学会在无需训练、无衰减的情况下,执行任何原版已存在的工作。本文档包含所有必要的、完整的原版代码引用作为证据,以确保其自包含性、正确性和健壮性,可供任何人在无上下文的情况下阅读并执行。

2. 核心机制深度解析

specialTrainables 的功能是一套精巧的、深度集成在 AI 思维逻辑中的 条件行为分支

2.1. 玩家授权机制

关键发现: 游戏通过一个统一的 UI 列 (PawnColumnWorker_Trainable_Special) 来管理所有被标记为 specialTrainable 的技能。当玩家点击该列的复选框时,会调用 pawn.training.SetWantedRecursive() 来设置 Pawn_TrainingTrackerwantedTrainables 字段的值。

  • 证据: 以下是 PawnColumnWorker_Trainable_Special.txt 的完整内容,它清晰地展示了 SetWantedRecursive 是如何被调用的。
    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。
    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: 编译并进行游戏内测试。