Quest framework tutorial - Taranchuk/RW-Modding-Tutorials GitHub Wiki
Quests are one of the most interesting features of the game, but their creation is considered difficult, but this is mainly due to the lack of documentation and tutorials. When all the main parts of quest creation are mastered, then a quest takes about 3-9 hours to complete (depending on the complexity of the quest). Now, let's try to figure out how to do the quests and we'll start with the vanilla quest Bandit Camp as an example to dissect, because it contains almost all parts of quest creation process. First, let's find the quest file at this path (Depends on the location of the game, of course)
c:\Program Files (x86)\Steam\steamapps\common\RimWorld\Data\Core\Defs\QuestScriptDefs\Script_BanditCamp.xml
Opening the file, we find the following fields such as
defName
rootSelectionWeight
rootMinPoints
canGiveRoyalFavor
expireDaysRange
successHistoryEvent
questNameRules
questDescriptionRules
root Class="QuestNode_Sequence"
The first field indicates the internal name of the quest, it is used for technical purposes and is not displayed in the game. The actual names and descriptions are done with questNameRules and questDescriptionRules, which are sets of rules, there is nothing complicated about them and can be easily copied and replaced with values. The rootSelectionWeight field is responsible for how often the quest is issued in the game, rootMinPoints determines the minimum points at which the storyteller issues the quest. These points are generated by the storyteller and are based on the wealth of your colony or other location or other incident targets. The expireDaysRange field defines the quest expiration days, successHistoryEvent is a historical event that is used in Ideology and in faction relations. There is also an autoAccept field that automatically sets the quest as active, at the time of quest initialization.
Now let's move on to the root field, which is the entry point for the quest logic generation. The root field is a quest node and this is usually QuestNode_Sequence, which is a class derived from QuestNode done to store a list of quest nodes and iterate sequentially through them. A quest node is a building block of a quest that can perform different functions, accept arguments, and do absolutely anything. In general, a quest script is a tree of quest nodes that are executed sequentially or in a random order (depending on the parent node that stores them) and they perform different functions, for example, some collect information and store them in a special container called slate (to we will return to this later) for further processing by next quest nodes, and some create new things necessary for the functioning of the quest (like new pawns, quest sites etc). Let's take a look at the body of the BanditCamp quest:
<root Class="QuestNode_Sequence">
<nodes>
<li Class="QuestNode_SubScript">
<def>Util_RandomizePointsChallengeRating</def>
<parms>
<pointsFactorTwoStar>1.5</pointsFactorTwoStar>
<pointsFactorThreeStar>2</pointsFactorThreeStar>
</parms>
</li>
<li Class="QuestNode_SubScript">
<def>Util_AdjustPointsForDistantFight</def>
</li>
<li Class="QuestNode_GetMap" />
<li Class="QuestNode_GetPawn">
<storeAs>asker</storeAs>
<mustBeFactionLeader>true</mustBeFactionLeader>
<allowPermanentEnemyFaction>false</allowPermanentEnemyFaction>
<hostileWeight>0.15</hostileWeight>
</li>
... // further quest nodes
</root>
As we can see, the root quest node stores various quest nodes and iterates them sequentially. The first QuestNode_SubScript node executes Util_RandomizePointsChallengeRating subscript (another QuestScriptDef with its root value) and it determines the random value of the quest points (which is mainly used to determine the threat level), Util_AdjustPointsForDistantFight modifies the points based on distance on the world map, QuestNode_GetMap stores local map in the slate, it will be used as an example in the spawn of drop pods on this map or for spawning incidents or raids on this map. Next, we have a QuestNode_GetPawn node with its own parameters, which in this case iterates through the entire list of faction leaders, gets a pawn and stores it as asker. asker is stored in a special container called a slate, and subsequent quest nodes can refer to this slate to get the asker value. Let's now take a look at the next part of the quest.
<li Class="QuestNode_GetSiteTile">
<storeAs>siteTile</storeAs>
<preferCloserTiles>true</preferCloserTiles>
</li>
<li Class="QuestNode_GetSitePartDefsByTagsAndFaction">
<storeAs>sitePartDefs</storeAs>
<storeFactionAs>siteFaction</storeFactionAs>
<sitePartsTags>
<li><tag>BanditCamp</tag></li>
</sitePartsTags>
<mustBeHostileToFactionOf>$asker</mustBeHostileToFactionOf>
</li>
<li Class="QuestNode_GetDefaultSitePartsParams">
<tile>$siteTile</tile>
<faction>$siteFaction</faction>
<sitePartDefs>$sitePartDefs</sitePartDefs>
<storeSitePartsParamsAs>sitePartsParams</storeSitePartsParamsAs>
</li>
<li Class="QuestNode_GetSiteThreatPoints">
<storeAs>sitePoints</storeAs>
<sitePartsParams>$sitePartsParams</sitePartsParams>
</li>
<li Class="QuestNode_SubScript">
<def>Util_GetDefaultRewardValueFromPoints</def>
<parms>
<!-- Use the actual threat points generated (some site parts define a minimum threshold) -->
<points>$sitePoints</points>
</parms>
</li>
<!-- Inflate reward value. Since we're basing the reward value on the threat points generated, we need to do this
even though the threat points was deflated from the input points already. -->
<li Class="QuestNode_Multiply">
<value1>$rewardValue</value1>
<value2>1.75</value2>
<storeAs>rewardValue</storeAs>
</li>
<li Class="QuestNode_SubScript">
<def>Util_GenerateSite</def>
</li>
<li Class="QuestNode_SpawnWorldObjects">
<worldObjects>$site</worldObjects>
</li>
Okay, this is a lot of logic, but just by reading the names of the nodes, everything becomes clear. First, the quest node QuestNode_GetSiteTile gets a tile in the world map and stores it in the slate container as siteTile. It has a preferCloserTiles parameter sets to true that counts in searches. Next we have QuestNode_GetSitePartDefsByTagsAndFaction, which performs a more complex function and it is just collecting SitePartDefs based on the list of tags and the faction that will belong to the site. It will be the faction of the quest site and in the case of the bandit camp quest, it is the enemy faction that is located in the camp. The next node, QuestNode_GetDefaultSitePartsParams, forms the parameters for creating a site, QuestNode_GetSiteThreatPoints determines the number of threat points, the next nodes modify the reward points (which is used when the quest is completed successfully and rewards are issued). Next this node
<li Class="QuestNode_SubScript">
<def>Util_GenerateSite</def>
</li>
Executes a subscript for spawning the site based on collected varialbes in the slate. Let's take a loot at this utility:
<QuestScriptDef>
<defName>Util_GenerateSite</defName>
<root Class="QuestNode_GenerateSite">
<sitePartsParams>$sitePartsParams</sitePartsParams>
<hiddenSitePartsPossible>$hiddenSitePartsPossible</hiddenSitePartsPossible>
<storeAs>site</storeAs>
<faction>$siteFaction</faction>
<tile>$siteTile</tile>
<singleSitePartRules>
<rulesStrings>
<li>root(priority=1,sitePart==ClimateAdjuster)->there's (*Threat)a climate adjuster machine(/Threat) there shifting the regional temperature by [temperatureOffset]</li>
<li>root(priority=1,sitePart==PsychicDroner)->there's (*Threat)a psychic droner machine(/Threat) there tuned to the [affectedGender] gender</li>
<li>root(priority=1,sitePart==PsychicSuppressor)->there's (*Threat)a psychic suppressor machine(/Threat) there tuned to the [affectedGender] gender</li>
<li>root(priority=1,sitePart==WeatherController)->there's (*Threat)a weather controller machine(/Threat) there forcing weather in the whole region to [weather_label]</li>
<li>root(priority=1,sitePart==SmokeSpewer)->there's (*Threat)a smoke spewer machine(/Threat) there belching smoke over the whole region</li>
<li>root(priority=1,sitePart==SunBlocker)->there's (*Threat)a sun blocker machine(/Threat) there shadowing the whole region</li>
<li>root(priority=1,sitePart==EMIDynamo)->there's (*Threat)an EMI dynamo machine(/Threat) there which can disable electrical devices across the region</li>
<li>root(priority=1,sitePart==ToxicSpewer)->there's (*Threat)a toxic spewer machine(/Threat) there blanketing the whole region in poison</li>
<li>root(priority=1,sitePart==RaidSource)->there's a military staging area guarded by (*Threat)[enemiesCount] [enemiesLabel](/Threat) that will launch raids on you every [mtbDays]</li>
<li>root(priority=1,sitePart==Outpost)->there's an enemy outpost at the site guarded by (*Threat)[enemiesCount] [enemiesLabel](/Threat)</li>
<li>root(priority=1,sitePart==BanditCamp)->there's a bandit camp at the site guarded by (*Threat)[enemiesCount] [enemiesLabel](/Threat)</li>
<li>root(priority=1,sitePart==Manhunters,count==1)->(*Threat)a manhunting [kindLabel](/Threat) is wandering nearby</li>
<li>root(priority=1,sitePart==Manhunters,count>1)->(*Threat)[count] manhunting [kindLabel](/Threat) are wandering nearby</li>
<li>root(priority=1,sitePart==SleepingMechanoids,count==1)->(*Threat)a mechanoid(/Threat) is sleeping nearby</li>
<li>root(priority=1,sitePart==SleepingMechanoids,count>1)->(*Threat)[count] mechanoids(/Threat) are sleeping nearby</li>
<li>root(priority=1,sitePart==Turrets)->(*Threat)[threatsInfo](/Threat) defend the site</li>
<li>root(priority=1,sitePart==AmbushEdge)->(*Threat)an enemy force(/Threat) is waiting to ambush anyone who comes near</li>
<li>root(priority=1,sitePart==AmbushHidden)->(*Threat)an enemy force(/Threat) is waiting to ambush anyone who comes near</li>
<li>root(priority=1,sitePart==MechCluster)->there's (*Threat)a cluster of hostile mechanoid structures(/Threat)</li>
<li>root(priority=1,sitePart==PossibleUnknownThreatMarker)->there may be an (*Threat)unknown threat(/Threat)</li>
<li>root->there's a [label]</li>
</rulesStrings>
</singleSitePartRules>
</root>
</QuestScriptDef>
As you can see, this is another quest node that accepts parameters and performs the task of spawning the site. Starting from this, a new world object is created on the world map, on which the bandit camp will be placed.
Now let's move on to the next node
<li Class="QuestNode_WorldObjectTimeout">
<worldObject>$site</worldObject>
<isQuestTimeout>true</isQuestTimeout>
<delayTicks>$(randInt(12,28)*60000)</delayTicks>
<inSignalDisable>site.MapGenerated</inSignalDisable>
<node Class="QuestNode_Sequence">
<nodes>
<li Class="QuestNode_Letter">
<label TKey="LetterLabelQuestExpired">Quest expired: [resolvedQuestName]</label>
<text TKey="LetterTextQuestExpired">The bandit camp has packed up and moved on. The quest [resolvedQuestName] has expired.</text>
</li>
<li Class="QuestNode_End">
<outcome>Fail</outcome>
</li>
</nodes>
</node>
</li>
This and the following nodes are important because they form the logic that exists throughout the life of the quest. To understand how they work, we need to look at their internal code through a decompiler. Let's take a look at QuestNode_WorldObjectTimeout
public class QuestNode_WorldObjectTimeout : QuestNode_Delay
{
public SlateRef<WorldObject> worldObject;
protected override QuestPart_Delay MakeDelayQuestPart()
{
return new QuestPart_WorldObjectTimeout
{
worldObject = worldObject.GetValue(QuestGen.slate)
};
}
}
As we can see, this is a class inherited from QuestNode_Delay. We look at QuestNode_Delay:
public class QuestNode_Delay : QuestNode
{
[NoTranslate]
public SlateRef<string> inSignalEnable;
[NoTranslate]
public SlateRef<string> inSignalDisable;
[NoTranslate]
public SlateRef<string> outSignalComplete;
public SlateRef<string> expiryInfoPart;
public SlateRef<string> expiryInfoPartTip;
public SlateRef<string> inspectString;
public SlateRef<IEnumerable<ISelectable>> inspectStringTargets;
public SlateRef<int> delayTicks;
public SlateRef<IntRange?> delayTicksRange;
public SlateRef<bool> isQuestTimeout;
public SlateRef<bool> reactivatable;
public QuestNode node;
protected override bool TestRunInt(Slate slate)
{
if (node != null)
{
return node.TestRun(slate);
}
return true;
}
protected override void RunInt()
{
Slate slate = QuestGen.slate;
QuestPart_Delay questPart_Delay;
if (delayTicksRange.GetValue(slate).HasValue)
{
questPart_Delay = new QuestPart_DelayRandom();
((QuestPart_DelayRandom)questPart_Delay).delayTicksRange = delayTicksRange.GetValue(slate).Value;
}
else
{
questPart_Delay = MakeDelayQuestPart();
questPart_Delay.delayTicks = delayTicks.GetValue(slate);
}
questPart_Delay.inSignalEnable = QuestGenUtility.HardcodedSignalWithQuestID(inSignalEnable.GetValue(slate)) ?? QuestGen.slate.Get<string>("inSignal");
questPart_Delay.inSignalDisable = QuestGenUtility.HardcodedSignalWithQuestID(inSignalDisable.GetValue(slate));
questPart_Delay.reactivatable = reactivatable.GetValue(slate);
if (!inspectStringTargets.GetValue(slate).EnumerableNullOrEmpty())
{
questPart_Delay.inspectString = inspectString.GetValue(slate);
questPart_Delay.inspectStringTargets = new List<ISelectable>();
questPart_Delay.inspectStringTargets.AddRange(inspectStringTargets.GetValue(slate));
}
if (isQuestTimeout.GetValue(slate))
{
questPart_Delay.isBad = true;
questPart_Delay.expiryInfoPart = "QuestExpiresIn".Translate();
questPart_Delay.expiryInfoPartTip = "QuestExpiresOn".Translate();
}
else
{
questPart_Delay.expiryInfoPart = expiryInfoPart.GetValue(slate);
questPart_Delay.expiryInfoPartTip = expiryInfoPartTip.GetValue(slate);
}
if (node != null)
{
QuestGenUtility.RunInnerNode(node, questPart_Delay);
}
if (!outSignalComplete.GetValue(slate).NullOrEmpty())
{
questPart_Delay.outSignalsCompleted.Add(QuestGenUtility.HardcodedSignalWithQuestID(outSignalComplete.GetValue(slate)));
}
QuestGen.quest.AddPart(questPart_Delay);
}
protected virtual QuestPart_Delay MakeDelayQuestPart()
{
return new QuestPart_Delay();
}
}
Let's take a look at what this class does. First, all quest nodes have TestRunInt and RunInt methods, which are executed by the game. The first TestRunInt method is executed to determine if the quest can run in the game. It returns false if the quest cannot be started (for example, the colony is not well developed or a certain technology is missing) and true if all conditions are met. Each time the game goes through a quest node, the RunInt method of the iterated quest node starts up and executes a specific code. Some quest nodes are quite simple and they just collect information into the slate, and some nodes are more complicated and they create active components of the quest, which are called Quest Parts and which provide logic throughout the life of the quest. Quest Parts usually exist to react to a specific signal (which is set by the quest node that creates the quest part). Let's take a look at the RunInt code. The first line contains the slate variable, which stores all the variables that the quest nodes have transferred to the slate. Next, a new quest part QuestPart_Delay is created and its internal fields are being initialized, mainly from the internal fields of the SlateRef fields of the QuestNode. Slate ref is just a wrapper to store any arbitrary type of data, it is retrieved from Slate and stored there by a string key. Example for retrieving inSignal string
slate.Get<string>("inSignal")
and for storing pawn as asker
slate.Set("asker", pawn);
The class also contains an internal node field, which is executed if not empty
QuestGenUtility.RunInnerNode(node, questPart_Delay);
Let's take a look what RunInnerNode does:
public static void RunInnerNode(QuestNode node, QuestPartActivable outerQuestPart)
{
string text = QuestGen.GenerateNewSignal("OuterNodeCompleted");
outerQuestPart.outSignalsCompleted.Add(text);
RunInnerNode(node, text);
}
public static void RunInnerNode(QuestNode node, string innerNodeInSignal)
{
Slate.VarRestoreInfo restoreInfo = QuestGen.slate.GetRestoreInfo("inSignal");
QuestGen.slate.Set("inSignal", innerNodeInSignal);
try
{
node.Run();
}
finally
{
QuestGen.slate.Restore(restoreInfo);
}
}
As we can see, it generates a new signal OuterNodeCompleted, which is assigned to the outSignalsCompleted list in outerQuestPart. It also exists in the slate as inSignal and the quest part can receive it this way
questPart.inSignal = QuestGenUtility.HardcodedSignalWithQuestID(inSignal) ?? QuestGen.slate.Get<string>("inSignal");
After that, the internal node starts up. In this case it is
<node Class="QuestNode_Sequence">
<nodes>
<li Class="QuestNode_Letter">
<label TKey="LetterLabelQuestExpired">Quest expired: [resolvedQuestName]</label>
<text TKey="LetterTextQuestExpired">The bandit camp has packed up and moved on. The quest [resolvedQuestName] has expired.</text>
</li>
<li Class="QuestNode_End">
<outcome>Fail</outcome>
</li>
</nodes>
</node>
This node is a QuestNode_Sequence, which is a collection of quest nodes (same as with the root field). All children's nodes are executed right there. But we digress, we will come back to the moment with later.
In general, the quest part variables are initialized and now it is passed to the quest via
QuestGen.quest.AddPart (questPart_Delay);
Now let's see how the QuestPart_Delay is done.
public class QuestPart_Delay : QuestPartActivable
{
public int delayTicks;
public string expiryInfoPart;
public string expiryInfoPartTip;
public string inspectString;
public List<ISelectable> inspectStringTargets;
public bool isBad;
public string alertLabel;
public string alertExplanation;
public List<GlobalTargetInfo> alertCulprits = new List<GlobalTargetInfo>();
public int ticksLeftAlertCritical;
public int TicksLeft
{
get
{
if (base.State != QuestPartState.Enabled)
{
return 0;
}
return enableTick + delayTicks - Find.TickManager.TicksGame;
}
}
public override string ExpiryInfoPart
{
get
{
if (quest.Historical)
{
return null;
}
return expiryInfoPart.Formatted(TicksLeft.ToStringTicksToPeriod());
}
}
public override IEnumerable<GlobalTargetInfo> QuestLookTargets
{
get
{
foreach (GlobalTargetInfo questLookTarget in base.QuestLookTargets)
{
yield return questLookTarget;
}
if (inspectStringTargets == null)
{
yield break;
}
for (int i = 0; i < inspectStringTargets.Count; i++)
{
ISelectable selectable = inspectStringTargets[i];
if (selectable is Thing)
{
yield return (Thing)selectable;
}
else if (selectable is WorldObject)
{
yield return (WorldObject)selectable;
}
}
}
}
public override void QuestPartTick()
{
base.QuestPartTick();
if (Find.TickManager.TicksGame >= enableTick + delayTicks)
{
DelayFinished();
}
}
protected virtual void DelayFinished()
{
Complete();
}
public override string ExtraInspectString(ISelectable target)
{
if (inspectStringTargets != null && inspectStringTargets.Contains(target))
{
return inspectString.Formatted(TicksLeft.ToStringTicksToPeriod());
}
return null;
}
public override void ExposeData()
{
base.ExposeData();
Scribe_Values.Look(ref delayTicks, "delayTicks", 0);
Scribe_Values.Look(ref expiryInfoPart, "expiryInfoPart");
Scribe_Values.Look(ref expiryInfoPartTip, "expiryInfoPartTip");
Scribe_Values.Look(ref inspectString, "inspectString");
Scribe_Collections.Look(ref inspectStringTargets, "inspectStringTargets", LookMode.Reference);
Scribe_Values.Look(ref isBad, "isBad", defaultValue: false);
Scribe_Values.Look(ref alertLabel, "alertLabel");
Scribe_Values.Look(ref alertExplanation, "alertExplanation");
Scribe_Values.Look(ref ticksLeftAlertCritical, "ticksLeftAlertCritical", 0);
Scribe_Collections.Look(ref alertCulprits, "alertCulprits", LookMode.GlobalTargetInfo);
if (alertCulprits == null)
{
alertCulprits = new List<GlobalTargetInfo>();
}
if (Scribe.mode == LoadSaveMode.PostLoadInit)
{
alertCulprits.RemoveAll((GlobalTargetInfo x) => !x.IsValid);
}
}
}
Some parts of the class have been removed so as not to clutter up the tutorial, the most important parts are placed here. As we can see, the quest part has an important method QuestPartTick, which is executed every tick and which checks if the current tick is equal to or greater than enableTick + delayTicks values. In this case, the Complete method is executed. See what it is
protected void Complete()
{
Complete(default(SignalArgs));
}
protected virtual void Complete(SignalArgs signalArgs)
{
if (state != QuestPartState.Enabled)
{
Log.Error("Tried to end QuestPart but its state is not Active. part=" + this);
return;
}
state = QuestPartState.Disabled;
if (outcomeCompletedSignalArg != 0)
{
signalArgs.Add(outcomeCompletedSignalArg.Named("OUTCOME"));
}
Find.SignalManager.SendSignal(new Signal(OutSignalCompleted, signalArgs));
if (outSignalsCompleted.NullOrEmpty())
{
return;
}
for (int i = 0; i < outSignalsCompleted.Count; i++)
{
if (!outSignalsCompleted[i].NullOrEmpty())
{
Find.SignalManager.SendSignal(new Signal(outSignalsCompleted[i], signalArgs));
}
}
}
As we can see, the Complete method sets the internal state of the quest part and sends outSignalsCompleted signals. Now let's go back to the node that stores the QuestNode_Letter and QuestNode_End. Let's look at the QuestNode_Letter code
public class QuestNode_Letter : QuestNode
{
[NoTranslate]
public SlateRef<string> inSignal;
public SlateRef<Faction> relatedFaction;
public SlateRef<LetterDef> letterDef;
public SlateRef<string> label;
public SlateRef<string> text;
public SlateRef<RulePack> labelRules;
public SlateRef<RulePack> textRules;
public SlateRef<IEnumerable<object>> lookTargets;
public SlateRef<QuestPart.SignalListenMode?> signalListenMode;
[NoTranslate]
public SlateRef<string> chosenPawnSignal;
public SlateRef<MapParent> useColonistsOnMap;
public SlateRef<bool> useColonistsFromCaravanArg;
[NoTranslate]
public SlateRef<string> acceptedVisitorsSignal;
public SlateRef<List<Pawn>> visitors;
public SlateRef<bool> filterDeadPawnsFromLookTargets;
private const string RootSymbol = "root";
protected override bool TestRunInt(Slate slate)
{
return true;
}
protected override void RunInt()
{
Slate slate = QuestGen.slate;
QuestPart_Letter questPart_Letter = new QuestPart_Letter();
questPart_Letter.inSignal = QuestGenUtility.HardcodedSignalWithQuestID(inSignal.GetValue(slate)) ?? slate.Get<string>("inSignal");
LetterDef letterDef = this.letterDef.GetValue(slate) ?? LetterDefOf.NeutralEvent;
if (typeof(ChoiceLetter).IsAssignableFrom(letterDef.letterClass))
{
ChoiceLetter choiceLetter = LetterMaker.MakeLetter("error", "error", letterDef, QuestGenUtility.ToLookTargets(lookTargets, slate), relatedFaction.GetValue(slate), QuestGen.quest);
questPart_Letter.letter = choiceLetter;
QuestGen.AddTextRequest("root", delegate(string x)
{
choiceLetter.label = x;
}, QuestGenUtility.MergeRules(labelRules.GetValue(slate), label.GetValue(slate), "root"));
QuestGen.AddTextRequest("root", delegate(string x)
{
choiceLetter.text = x;
}, QuestGenUtility.MergeRules(textRules.GetValue(slate), text.GetValue(slate), "root"));
}
else
{
questPart_Letter.letter = LetterMaker.MakeLetter(letterDef);
questPart_Letter.letter.lookTargets = QuestGenUtility.ToLookTargets(lookTargets, slate);
questPart_Letter.letter.relatedFaction = relatedFaction.GetValue(slate);
}
questPart_Letter.chosenPawnSignal = QuestGenUtility.HardcodedSignalWithQuestID(chosenPawnSignal.GetValue(slate));
questPart_Letter.useColonistsOnMap = useColonistsOnMap.GetValue(slate);
questPart_Letter.useColonistsFromCaravanArg = useColonistsFromCaravanArg.GetValue(slate);
questPart_Letter.acceptedVisitorsSignal = QuestGenUtility.HardcodedSignalWithQuestID(acceptedVisitorsSignal.GetValue(slate));
questPart_Letter.visitors = visitors.GetValue(slate);
questPart_Letter.signalListenMode = signalListenMode.GetValue(slate) ?? QuestPart.SignalListenMode.OngoingOnly;
questPart_Letter.filterDeadPawnsFromLookTargets = filterDeadPawnsFromLookTargets.GetValue(slate);
QuestGen.quest.AddPart(questPart_Letter);
}
}
QuestNode_Letter logic is almost like in QuestNode_Delay, it creates a new quest part and assigns internal fields and passes it to the quest. Pay attention to this line
questPart_Letter.inSignal = QuestGenUtility.HardcodedSignalWithQuestID(inSignal.GetValue(slate)) ?? slate.Get<string>("inSignal");
this is the field for storing the inSignal signal and in this case it is the OuterNodeCompleted string that was created by RunInnerNode from the parent node. QuestPart_Letter class has a very important method Notify_QuestSignalReceived. This method runs every time a signal is sent. So if the signal matches the stored inSignal, the following code is run, which issues a letter. The code is not full shown here, in the compiler you can see how the letter was sent.
public override void Notify_QuestSignalReceived(Signal signal)
{
base.Notify_QuestSignalReceived(signal);
if (!string.IsNullOrEmpty(getColonistsFromSignal) && signal.tag == getColonistsFromSignal)
{
if (signal.args.TryGetArg("SUBJECT", out var arg))
{
ReadPawns(arg.arg);
}
if (signal.args.TryGetArg("SENT", out var arg2))
{
ReadPawns(arg2.arg);
}
}
if (!(signal.tag == inSignal))
{
return;
}
Letter letter = Gen.MemberwiseClone(this.letter);
letter.ID = Find.UniqueIDsManager.GetNextLetterID();
... following code you can see in a compiler
}
Now we have the next node, QuestNode_End, which has almost the same logic, but in this case it ends the quest with a fail outcome So let's summarize everything we've seen. Let's take another look at this xml block:
<li Class="QuestNode_WorldObjectTimeout">
<worldObject>$site</worldObject>
<isQuestTimeout>true</isQuestTimeout>
<delayTicks>$(randInt(12,28)*60000)</delayTicks>
<inSignalDisable>site.MapGenerated</inSignalDisable>
<node Class="QuestNode_Sequence">
<nodes>
<li Class="QuestNode_Letter">
<label TKey="LetterLabelQuestExpired">Quest expired: [resolvedQuestName]</label>
<text TKey="LetterTextQuestExpired">The bandit camp has packed up and moved on. The quest [resolvedQuestName] has expired.</text>
</li>
<li Class="QuestNode_End">
<outcome>Fail</outcome>
</li>
</nodes>
</node>
</li>
From what we have seen, QuestNode_WorldObjectTimeout creates quest part QuestPart_WorldObjectTimeout and assigns the internal fields to this quest part and passes it on to the quest. The next nodes QuestNode_Letter and QuestNode_End are iterated over and their RunInt is executed, creating next quest parts that store inSignal fields (in this case outSignalsCompleted) and they execute certain code in Notify_QuestSignalReceived method if the signal matches the inSignal string. When the delay is done, QuestPart_WorldObjectTimeout executes the Complete method and it sends signals through Find.SignalManager and it iterates through the whole list of quest parts and QuestNode_Letter and QuestNode_End are executed because their signals match. In general, this was a long read, I recommend taking a break and re-reading the tutorial several times and dissecting the code and it will make sense. Now let's move on to the next nodes.
<!-- If we enter and leave, the map is destroyed. Fail the quest. -->
<li Class="QuestNode_Signal">
<inSignal>site.Destroyed</inSignal>
<node Class="QuestNode_Sequence">
<nodes>
<li Class="QuestNode_Letter">
<label TKey="LetterLabelQuestFailed">Quest failed: [resolvedQuestName]</label>
<text TKey="LetterTextQuestFailed">After being discovered, the bandit camp has dispersed. The quest [resolvedQuestName] has ended.</text>
</li>
<li Class="QuestNode_End">
<outcome>Fail</outcome>
</li>
</nodes>
</node>
</li>
<li Class="QuestNode_Signal">
<inSignal>site.AllEnemiesDefeated</inSignal>
<node Class="QuestNode_Sequence">
<nodes>
<li Class="QuestNode_Notify_PlayerRaidedSomeone">
<getRaidersFromMapParent>$site</getRaidersFromMapParent>
</li>
<li Class="QuestNode_GiveRewards">
<parms>
<allowGoodwill>true</allowGoodwill>
<allowRoyalFavor>true</allowRoyalFavor>
<chosenPawnSignal>ChosenPawnForReward</chosenPawnSignal>
</parms>
<addCampLootReward>true</addCampLootReward>
<customLetterLabel TKey="LetterLabelPaymentArrived">Payment arrived</customLetterLabel>
<customLetterText TKey="LetterTextPaymentArrived">You have defeated the bandit camp!\n\nThe payment from [asker_faction_name] has arrived.</customLetterText>
<nodeIfChosenPawnSignalUsed Class="QuestNode_Letter">
<letterDef>ChoosePawn</letterDef>
<label TKey="LetterLabelFavorReceiver">[asker_faction_royalFavorLabel]</label>
<text TKey="LetterTextFavorReceiver">These colonists participated in the victory for the quest [resolvedQuestName]. [asker_definite] wants to know who should receive the [royalFavorReward_amount] [asker_faction_royalFavorLabel] for this service.</text>
<useColonistsOnMap>$site</useColonistsOnMap>
<chosenPawnSignal>ChosenPawnForReward</chosenPawnSignal>
</nodeIfChosenPawnSignalUsed>
</li>
</nodes>
</node>
</li>
<li Class="QuestNode_End">
<inSignal>site.AllEnemiesDefeated</inSignal>
<outcome>Success</outcome>
</li>
As you can see, these three nodes are signal based. They respond to signals and execute specific code if inSignal matches the signal. Let's see what the signals mean. We have the following signals
<inSignal>site.Destroyed</inSignal>
<inSignal>site.AllEnemiesDefeated</inSignal>
As we can see, the signals here consist of several parts. The first is an argument (SignalArgs). It can mean a world object, a pawn, a quest node, or something else. The second part is a string. How does the code send these signals? This is done via QuestUtility.SendQuestTargetSignals or Find.SignalManager. Let's take a look at the code from World.Destroy method:
public virtual void Destroy()
{
if (Destroyed)
{
Log.Error("Tried to destroy already-destroyed world object " + this);
return;
}
if (Spawned)
{
Find.WorldObjects.Remove(this);
}
destroyed = true;
Find.FactionManager.Notify_WorldObjectDestroyed(this);
for (int i = 0; i < comps.Count; i++)
{
comps[i].PostDestroy();
}
QuestUtility.SendQuestTargetSignals(questTags, "Destroyed", this.Named("SUBJECT"));
}
as we can see, this signal is sent at the moment of destroying the site (the moment of exiting the map) and it activates the QuestNode_Letter and QuestNode_End parts through their Notify_QuestSignalReceived methods. And we have site.AllEnemiesDefeated signal. When we look in the code where it sends a signal, we find it in Site.CheckAllEnemiesDefeated
private void CheckAllEnemiesDefeated()
{
if (!allEnemiesDefeatedSignalSent && base.HasMap && !GenHostility.AnyHostileActiveThreatToPlayer_NewTemp(base.Map, countDormantPawnsAsHostile: true))
{
QuestUtility.SendQuestTargetSignals(questTags, "AllEnemiesDefeated", this.Named("SUBJECT"));
allEnemiesDefeatedSignalSent = true;
}
}
In this case, if the signal is sent, then the quest parts QuestNode_Notify_PlayerRaidedSomeone, QuestNode_GiveRewards and QuestNode_End are executed, completing the quest with success.
How rewards are done? Surprisingly (or not), it's hardcoded in QuestGen_Rewards.GiveRewards, but there are ways to provide custom rewards, using custom quest parts. You could create your custom QuestNode_Rewards and do this like that:
public class QuestNode_Rewards : QuestNode
{
public List<RewardList> nodes = new List<RewardList>();
protected override bool TestRunInt(Slate slate)
{
if (!slate.Exists("map"))
{
return false;
}
return true;
}
protected override void RunInt()
{
Slate slate = QuestGen.slate;
QuestPart_Choice questPart_Choice = new QuestPart_Choice();
foreach (var node in nodes)
{
QuestPart_Choice.Choice choice = new QuestPart_Choice.Choice();
foreach (var rewardNode in node.rewards)
{
var rewards = rewardNode.GenerateRewards(slate);
foreach (var reward in rewards)
{
choice.rewards.Add(reward);
}
foreach (var item in rewardNode.GenerateQuestParts(slate))
{
QuestGen.quest.AddPart(item);
choice.questParts.Add(item);
}
}
questPart_Choice.choices.Add(choice);
}
QuestGen.quest.AddPart(questPart_Choice);
}
}
where RewardList is done as a class to store own list of rewards (in order to allow multiple rewards)
public class RewardList
{
public List<RewardNode> rewards;
}
and then make an abstract class
public abstract class RewardNode
{
public string inSignal;
public string outSignalChoiceAccepted;
public abstract IEnumerable<Reward> GenerateRewards(Slate slate);
public abstract IEnumerable<QuestPart> GenerateQuestParts(Slate slate);
}
and make a class inheriting from it
public class RewardNode_ItemsReward : RewardNode
{
public ThingDef thingDef;
public int stackCount;
public List<Thing> items;
public RewardNode_ItemsReward()
{
}
public float TotalMarketValue
{
get
{
float num = 0f;
for (int i = 0; i < items.Count; i++)
{
Thing innerIfMinified = items[i].GetInnerIfMinified();
num += innerIfMinified.MarketValue * (float)items[i].stackCount;
}
return num;
}
}
public override IEnumerable<Reward> GenerateRewards(Slate slate)
{
var thing = ThingMaker.MakeThing(thingDef);
thing.stackCount = stackCount;
items = new List<Thing>
{
thing
};
var reward = new Reward_Items()
{
items = items
};
yield return reward;
}
public override IEnumerable<QuestPart> GenerateQuestParts(Slate slate)
{
QuestPart_DropPods dropPods = new QuestPart_DropPods();
dropPods.inSignal = (QuestGenUtility.HardcodedSignalWithQuestID(inSignal) ?? QuestGen.slate.Get<string>("inSignal"));
dropPods.outSignalResult = (QuestGenUtility.HardcodedSignalWithQuestID(outSignalChoiceAccepted) ?? QuestGen.slate.Get<string>("outSignalChoiceAccepted"));
dropPods.mapParent = slate.Get<Map>("map").Parent;
dropPods.useTradeDropSpot = true;
dropPods.Things = items;
slate.Set("itemsReward_items", items);
slate.Set("itemsReward_totalMarketValue", TotalMarketValue);
yield return dropPods;
}
}
then in xml it will look like that
<li Class="YourMod.QuestNode_Rewards">
<nodes>
<li Class="YourMod.RewardList">
<rewards>
<li Class="YourMod.RewardNode_ItemsReward">
<thingDef>Axe</thingDef>
<stackCount>1</stackCount>
</li>
<li Class="YourMod.RewardNode_ItemsReward">
<thingDef>Pistol</thingDef>
<stackCount>1</stackCount>
</li>
</rewards>
</li>
<li Class="YourMod.RewardList">
<rewards>
<li Class="YourMod.RewardNode_ItemsReward">
<thingDef>Sword</thingDef>
<stackCount>1</stackCount>
</li>
</rewards>
</li>
</nodes>
</li>
Well, that's all I have to say about quest creation process. Good luck with that!