Quest authoring
Status: experimental.
What it is
The game calls a quest a task. A task has a title, a description and a list of objectives, which are the steps the player works through. The player sees tasks in the task panel. This API lets your mod make a new task while the game runs, add objectives to it, move its counters and end it as finished or failed. A task made here is an ordinary game task, so the player sees it in the task panel, and Quests.All, Quests.Get, Quests.TaskChanged and Quests.ObjectiveChanged see it too. An objective can also ask the player for items, which the framework takes in one step, and a quest can name a follow-up quest that starts when it finishes.
Reading and changing the game’s own tasks stays on the Quests page, in the Story API. You can only change the tasks your own mod made through QuestAuthoring.
When to use it
- Give the player a new task from your mod, with a title, a description and steps.
- Count progress on a step, for example “find 3 keys”, and finish the task when it is done.
- End a task as finished or failed from your own code.
- Ids. A quest has a key you choose (letters, digits,
_and-, up to 48 characters, no dot). The task id ismodId + "." + key, so two mods never share an id.QuestAuthoring.TaskId(modId, key)builds it. An objective id is its key, and it must be unique inside the quest. The game’s Lua functions (TaskActive("<taskId>"),ObjectiveActive, and the others) use these ids. - Owner. Only the mod whose id is the prefix of the task id can change a quest through
QuestAuthoring.AddObjective,SetObjective,SetCounter,FinishandFailreturnfalsefor another mod’s quest or for a game quest. The owner is read from the id, so it still works after a restart or a load. - Titles. The game finds a started task again with
TaskInfo.Compare, which falls back to the title. The framework patchesCompareso a quest made here (an id of the formmodId.keyand no addressable path) is compared by id only, against game tasks and other quests alike. A game task with the same title therefore never joins your quest, and your quest never takes over a game task’s objectives, status or “new task” check.Createstill refuses a title that another started task or a loaded task asset has (DuplicateTitle), because two quests with one title are confusing in the task panel. A game task that is not loaded and not started cannot be seen, so a clash with it is possible. It only shows as two equal titles in the panel. If the patch cannot be installed, quest authoring is switched off. - Create.
Createruns the game’s own create path,TaskController.EditTaskwithcreateNewTask. The game shows the “New Task” notification, adds the task to the task panel, and queuesQuests.TaskChangedandQuests.ObjectiveChanged. Every objective in the definition starts Active, unless it setsHidden(see Hidden objectives and reveal). A quest has 1 to 12 objectives. - Counters. An objective with
Targetabove 0 has a counter. When the counter reaches the target, the game finishes the objective.SetCountersets an exact value from 0 to the target. For a relative change useQuests.AddProgress. A counter that reaches its target while the objective is still hidden does not finish anything: the game’s finish call does nothing for a non-Active objective, so the counter sits at the target.Revealfinishes such an objective itself (see Hidden objectives and reveal). - Ending.
FinishandFailneed an Active task. The game marks each still-active objective Abandoned.Finishdoes not run the game’s own follow-up tasks (seeQuests.SetTask), but a finished quest does start the framework follow-up named byNext. UseQuests.SetTaskwithAbandonedto abandon a quest. - Item hand-in. An objective can ask the player for items: set
ItemIdandCountonQuestObjectiveDefinition.QuestAuthoring.HandIntakes those items and moves that objective on by the count. The take is all or nothing, so a refused move gives the items back. See Handing in items. - Follow-up quest. Set
QuestDefinition.Nextto the key of another quest of yours and give that quest’s definition inQuestDefinition.NextDefinition, and the framework starts that quest when the first one finishes. See Follow-up quest. - Testing. With
Debug.QuestAuthoringCommandon, the console commandscore_quest reset <taskId>andcore_quest skip <taskId> [key]start a quest over or finish a quest or one objective. See Console commands. - Refusals.
Createreturnsnulland setsrefusal. The other writers returnfalse. A write is also refused during a save or a load, while one of the game’s quest hooks is running, and when the task panel or notifications are not loaded (outside a game scene). A refusal that is a bad argument or a conflict is logged once per reason. - Mod removed. The quest stays in the save and in the task panel. While the framework is loaded and the quest is in the live task list,
Removedeletes it, and the next game save no longer holds it; a save written after the framework is gone, or a quest already written beforeRemovewas called, still has it until a later save drops it. Without the framework theComparepatch is gone too, so the game compares the leftover quest by title again. A game task with the same title (even one that was never loaded when the quest was made) can then land on that entry, and its objectives join the leftover quest. Give your quests titles that no game quest uses, and prefer a new key over reusing a title.
Hidden objectives and reveal
An objective can start hidden and show its real text later.
public sealed class QuestObjectiveDefinition
{
public bool Hidden { get; set; }
}
public static class QuestAuthoring
{
public static bool Reveal(string modId, string taskId, string key, string title, string description);
}
- A placeholder is created, not your text. A hidden objective is created with the title
???and an empty description, whateverTitleandDescriptionhold, and its text is validated as usual but not written. This is deliberate: the game saves each objective’s title and description inside the save file, so a spoiler written at creation would sit in the save from the moment the quest starts. - The player sees the placeholder. The game’s task panel lists every objective of the task, hidden or not, with its title, and it adds each objective’s description under the task description.
Hiddenonly keeps the objective out of the game’s Lua status lists, soObjectiveActiveandQuests.Reacheddo not report it. The framework does not patch the panel. - Reveal writes the text and activates the objective.
Revealneeds your own Active quest and a hidden objective. It writes the real title and description onto the objective and then activates it through the game’sTaskController.EditTaskObjective, which shows the “New Objective” notification with the real title and adds the objective to the game’s active list.EditTaskcannot do this, because it refusesActivefor an objective that already exists. The reveal is the moment the real text reaches the save. Two things follow from the game’s activation call. The “New Objective” notification always reads0/<target>even when the counter is already higher, because the game builds that text from the target and not from the current count. And when the counter is already at or past the target and the objective is not marked to skip self completion by counter,Revealfinishes the objective immediately after the activation, through the same game call the counter completion uses, so a revealed objective can never stay Active at n of n.Revealthen runs the task’s own finish rule with the game’sFinishIfNoActiveObjectives, the call the counter completion makes next, so a task withfinishTaskWhenTheresNoActiveTasksfinishes at that moment instead of staying Active. - Panel refresh.
Revealcalls the game’sTaskPanel.UpdateCurrentTaskInfoafter the activation, the same refreshEditTaskmakes, so the revealed text shows at once instead of on the next quest change. - Refusals.
Revealreturnsfalsewhen the quest is not yours, a save or load is running, the quest is not Active, the objective is missing or is not hidden (already revealed, finished, failed or abandoned), when the title is empty or over 100 characters, or when the description is over 1000 characters. A refused or throwing activation puts the placeholder back, so a hidden objective never keeps spoiler text. A failure of the panel refresh does not turn a successful reveal into afalse: once the objective is Active,Revealreturnstrueand logs that failure on its own. - Creation raises the event. A hidden objective raises
Quests.ObjectiveChangedwithObjectiveState.Hiddenwhen it is created, andRevealraises another withActiveand the live counts. - Counters keep working while hidden. A counter change on a hidden objective raises
Quests.ObjectiveChangedwithObjectiveState.Hiddenand the newCurrentandTarget, which is how a mod can show progress before the reveal. - No re-hiding.
SetObjectivedoes not acceptHidden, and a revealed objective cannot be hidden again. The game’s own status writers only move an objective forward.
Rewinding a quest
A finished, failed or abandoned quest of yours can go back to its start, be taken out of the game, or be relabelled. Every call needs your own quest that is in the live task list, is refused while a save or a load is running, and returns false when it cannot do what you asked.
public static class QuestAuthoring
{
public static bool SetTitle(string modId, string taskId, string title);
public static bool SetTitle(string modId, string taskId, string objectiveKey, string title);
public static bool SetDescription(string modId, string taskId, string description);
public static bool Restart(string modId, string taskId);
public static bool Reset(string modId, string taskId);
public static bool Remove(string modId, string taskId);
public static bool JumpTo(string modId, string taskId, string objectiveKey);
public static bool Checkpoint(string modId, string taskId, string name, bool restore);
public static bool Checkpoint(string modId, string taskId, string name, Action restoreCallback);
}
- The reopen bug is not used. The game only ever adds an id to its five task lists and its five objective lists. It never takes an id out of the finished, failed or abandoned list when a task starts again, so a task the game reopens is reported as both Active and Finished, the panel can draw it twice, and
TaskFinished("<taskId>")stays true while the player plays it again. The framework never calls that path. Instead it writes the ten lists itself from the live task and objective states, so an id sits in exactly the lists its state belongs to and nowhere else.Quests.SetTaskstill refuses to reopen a task by itself: useRestartorReset. - Restart. Sends one of your finished, failed or abandoned quests back to its start. Every objective returns to its start: its state is Active, or Hidden for an objective created hidden, its counter is 0, and its text is the text from your definition, so a hidden objective shows the placeholder again. The quest is Active again, it returns to the active list, and it leaves the finished, failed and abandoned lists.
Restartreturnsfalsewhile the quest is still Active, because there is nothing to restart. - Reset. The same as
Restart, from any state, including Active. Use it to start a quest over that the player has already begun. - JumpTo. Rewinds the quest to one objective: the objectives before it are Finished and their counters sit at their target, the chosen objective is Active with its real text even when it was created hidden, the ones after it go back to their start, and the quest is Active. Returns
falsewhen the objective is not on the quest. - Remove. Deletes one of your quests from the live task list, the task panel and the framework’s record of its definition, so
Createaccepts the same key again in the same session. The next game save no longer holds it.Removeonly sees the live task list, so it cannot delete a quest that is not there. - SetTitle and SetDescription. Change the title of the quest, the title of one of its objectives, or the description of the quest, and refresh the task panel at once. A title is 1 to 100 characters and a description is at most 1000. The three calls return
falseon a bad length, a missing objective, or a quest that is not yours. LikeReveal, a failure of the panel refresh does not turn a successful write intofalse; it is logged on its own. - Checkpoint.
Checkpoint(modId, taskId, name, restore)keeps one named capture of the live state of the quest in memory whenrestoreis false, and puts that state back whenrestoreis true. A capture holds the quest state and every objective’s state and count, a quest can hold 16 captures, and a name is 1 to 32 characters of letters, digits and-. Captures do not outlive the game session andRemovedrops them. Returnsfalsefor a bad name, a quest that is not yours, or a name that was never captured whenrestoreis true. - A checkpoint can run code when it is put back.
Checkpoint(modId, taskId, name, restoreCallback)keeps the capture exactly as the call above does, and remembersrestoreCallbackunder that name. The callback runs once when that name is restored, after the state is back and afterQuests.Rewoundis raised for the restore, so a handler on the event sees the quest before the callback runs. An exception in the callback is logged once and swallowed, and the restore still returnstrue. The callback is kept, not used up, so restoring the same name again runs it again. Capturing the same name without a callback clears it, a second callback for that name replaces it, and a capture that is refused neither sets nor clears the callback remembered for that name. Callbacks are dropped with the captures at the framework’s session point, so they do not outlive the game session either. The call returnsfalsefor the same reasons the capture does.
string id = QuestAuthoring.TaskId(Mod.Id, "lost_key");
// The capture holds the quest and its objectives. The callback puts the rest back.
QuestAuthoring.Checkpoint(Mod.Id, id, "before_talk", () => PutSignBack());
// Later, when the player asks to undo the talk. PutSignBack runs at the end of this call.
QuestAuthoring.Checkpoint(Mod.Id, id, "before_talk", true);
- What a rewind can restore. The framework keeps its own record of every quest it created in a file of its own in the save folder, holding the start state of each objective:
Hidden,Target,Optionaland the text from your definition.Createfills the record andAddObjectiveadds to it. A quest that was in the save before this version, or one created outsideQuestAuthoring, has no record, so a rewind starts each of its objectives Active with its counter at 0 and leaves its text alone. The framework never writes that record into the game’s save. - The event.
Quests.Rewoundreports every one of these calls after the change, with the task id and aReasonofSetTitle,SetDescription,Restart,Reset,Remove,JumpToorCheckpoint.FromandToare objective keys: the objective the quest was on before the call, and the one it is on after.FromandToarenullwhen no objective isActiveat that moment:Fromwhen the quest had no active objective before the call, andTowhen it has none after it. Both are alsonullforSetTitle,SetDescriptionandRemove, which move no objective. - Reading one state.
Quests.State(taskId, objectiveKey)returns the state of a single objective, ornullwhen the task or the objective is missing. It builds no objects, so it is safe to call every frame.Quests.State(taskId)still returns the quest state. - Refusals. A call on a quest that is not yours returns
falsewithout a log, as the other writers do. A call on a quest that is not in the live task list, or aJumpToon a missing objective, logs once and returnsfalse.
Handing in items
An objective can ask the player for items, as well as or instead of a counter. Set ItemId and Count on the objective, then call QuestAuthoring.HandIn(modId, taskId, objectiveKey) from your own code, for example when the player talks to the quest giver.
- The pair.
ItemId0 needsCount0, and anItemIdabove 0 needs aCountof 1 to 9999. Any other pair is refused withBadInputwhen the quest is created, andAddObjectivereturnsfalsefor it. - What it does.
HandInneeds your own Active quest, an Active objective on it that asks for items, and the player holding all of them. It takes the items in one step, then moves the objective on: byCountwhen the objective has a counter, and to Finished when it has none. It returnstruewhen the objective moved. - All or nothing. When the move fails or throws, the items are given back, so the player either keeps every item or loses exactly the ones the objective asked for. A failure of the give-back is logged once on its own.
- The whole count is taken. When the counter has less room left than the count, the counter stops at its target and the rest of the items are still taken. Keep
Countthe same as the room the objective has. - Refusals.
HandInreturnsfalsewhen the quest is not yours, a save or a load is running, the quest is not Active, the objective is missing or not Active, the objective asks for no items, or the player does not hold all of them. The last three are logged once each. - The item id is the game’s.
ItemIdis the game’s own item id, the number on the item’s asset. The framework does not check that the item exists. - A load keeps the pair.
ItemIdandCountare kept in the framework’s own file in the save folder with the rest of the definition, so a hand-in still knows what to take after the save is loaded again.
The example below asks for the key back, and takes it only when the player says yes.
using ObenseuerCore;
[ModInfo("example.keyrun", "Key Run", "0.1.0")]
[ModDependency("obenseuer.moddingframework", "0.11.0")]
public sealed class KeyRun : FrameworkMod
{
protected override void OnLoad()
{
// Fires at each save load, once the task panel and the notifications are up.
Quests.Ready += Register;
}
private void Register()
{
string id = QuestAuthoring.TaskId(Mod.Id, "lost_key");
if (Quests.Get(id) != null) return;
QuestDefinition quest = new QuestDefinition { Key = "lost_key", Title = "The lost key", Description = "Find the key and bring it back." };
quest.Objectives.Add(new QuestObjectiveDefinition { Key = "search", Title = "Search the rooms", Target = 3 });
quest.Objectives.Add(new QuestObjectiveDefinition { Key = "return", Title = "Bring the key back", ItemId = 1, Count = 1 });
QuestRefusal refusal;
if (QuestAuthoring.Create(Mod.Id, quest, out refusal) == null)
{
Log.Warn("The quest was refused: " + refusal);
}
}
/// <summary>Call this when the player offers the key.</summary>
public void HandInKey()
{
string id = QuestAuthoring.TaskId(Mod.Id, "lost_key");
if (!QuestAuthoring.HandIn(Mod.Id, id, "return"))
{
Log.Warn("HandIn was refused for " + id);
}
}
}
Follow-up quest
QuestDefinition.Next names the quest to start when this one finishes. It is a quest key of your own mod, with the same rules as Key: letters, digits, _ and -, up to 48 characters, and no dot. A value that is not a valid key is refused with BadInput when the quest is created.
The framework starts that quest from its definition, so give it in QuestDefinition.NextDefinition on the same object: its Key is the quest key, and its Title, Description and Objectives are the ones the follow-up is made with, Hidden, Target, ItemId and Count included. When you set NextDefinition you can leave Next empty, because the framework then takes the key from NextDefinition.Key. When you set both they have to agree, or Create is refused with BadInput.
- It is stored with the parent.
Createcopies the follow-up’s definition into the framework’s own file in the save folder along with the parent’s, so a save and a load keep it, and the follow-up does not depend on your mod callingCreatefor it later. A chain holds at most 8 quests: a longer chain is refused withBadInput, while a definition that names itself or that loops is accepted, because the rules below stop it. - When it starts. When a quest of yours is finished, the framework builds the task id
modId + "." + Nextand starts that quest the wayCreatedoes, from the stored definition. - When it is skipped. The framework does not start it when a task with that id already exists, when
Nextnames the quest that just finished, when the same follow-up was already started in this session, when this session has already started 8 follow-ups, or when no definition for it was stored. A chain therefore cannot loop, and one quest is never added twice. The self reference, the repeat, the cap and a missing definition are logged once, and so is a refusal fromCreate; the other two skips are silent. - An older save. A parent quest that an earlier version of the framework created has no stored follow-up definition, so nothing starts and the skip is logged. The same happens for a
Nextthat names a key you gave noNextDefinitionfor. - Only a finished quest. The follow-up starts from
Quests.TaskChangedwithTaskState.Finished. A failed or abandoned quest does not start one, and neither does a rewind, becauseRestart,Reset,JumpTo,Remove, the checkpoint restore and the relabel calls do not raise that event. Nothing starts a follow-up while a save or a load is running, or when quest authoring is not available. - The follow-up is a quest of its own. It obeys every rule on this page, so it needs a title no other started task uses. Two quests with one title are refused with
DuplicateTitle.
Console commands
The framework registers these command forms when Debug.QuestAuthoringCommand is true in the framework’s config and the game console is available.
| Command | What it does |
|---|---|
core_quest reset <taskId> | Starts your quest over from any state. |
core_quest skip <taskId> | Finishes the whole quest. |
core_quest skip <taskId> <key> | Finishes the one objective named by key. |
resettakes no key, andskiptakes at most one. Anything else printscore_quest reset <taskId> or core_quest skip <taskId> [key].resetneeds a task id of one of your quests, and the quest must be in the live task list. The mod id is read from the task id.skipgoes through the game’s own writers, soskip <taskId>needs an Active quest andskip <taskId> <key>needs an Active objective.- Every command answers with a line that says what happened.
- Both change the live save, so use a scratch save. The same config also registers
core_quests_check, which creates, advances and finishes a test quest.
Quick example
This mod registers one quest each time the game is ready, unless the save already has it.
using ObenseuerCore;
[ModInfo("example.questy", "Quest Maker", "0.1.0")]
[ModDependency("obenseuer.moddingframework", "0.11.0")]
public sealed class QuestMaker : FrameworkMod
{
protected override void OnLoad()
{
Quests.Ready += MakeQuest;
}
private void MakeQuest()
{
string id = QuestAuthoring.TaskId(Mod.Id, "lost_key");
if (Quests.Get(id) != null) return;
QuestDefinition quest = new QuestDefinition { Key = "lost_key", Title = "The lost key", Description = "Find the key." };
quest.Objectives.Add(new QuestObjectiveDefinition { Key = "search", Title = "Search the rooms", Target = 3 });
quest.Objectives.Add(new QuestObjectiveDefinition { Key = "return", Title = "Bring it back" });
QuestRefusal refusal;
if (QuestAuthoring.Create(Mod.Id, quest, out refusal) == null) Log.Warn("The quest was refused: " + refusal);
}
}
The check Quests.Get(id) != null matters. The game saves the task, so on the next start the task is already in the save, and Create would be refused. Quests.Ready fires at every save load and every new game, once the task panel and the notifications are up, so it is the place to register quests; the handler has to be safe to run again, and the check makes it so. Log.Warn prints the reason a create was refused, which is the first thing to look at when a quest does not appear. Mod.Id is your mod’s own id; it becomes the start of the task id. See Using events.
QuestDefinition the quest you want to make
A quest has a key, a title, a description, an optional follow-up quest and 1 to 12 objectives.
public sealed class QuestDefinition
{
public string Key { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string Next { get; set; } // the key of the quest to start when this one finishes
public QuestDefinition NextDefinition { get; set; } // that quest's definition, stored with this one
public List<QuestObjectiveDefinition> Objectives { get; set; }
}
QuestObjectiveDefinition one step of a quest
Each step in Objectives is one of these. Target gives the step a counter, and ItemId and Count make it ask the player for items.
public sealed class QuestObjectiveDefinition
{
public string Key { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public int Target { get; set; } // counter target, 0 for none, at most 9999
public bool Optional { get; set; }
public bool Hidden { get; set; }
public int ItemId { get; set; } // the item the objective asks for, 0 for none
public int Count { get; set; } // how many, 1 to 9999 when ItemId is above 0
}
QuestRefusal why a quest was not made
Create sets this through its out refusal parameter. None means nothing was refused.
public enum QuestRefusal
{
None,
NotReady,
SaveBusy,
BadInput,
DuplicateTitle,
AlreadyExists,
Rejected,
Failed,
PartiallyCreated
}
QuestAuthoring makes and changes your quests
The calls that make a quest, change it, rewind it and take it out. Each one returns false, or null for Create, when it refuses, and the reasons are in the sections above.
public static class QuestAuthoring
{
public static bool IsAvailable { get; }
public static string TaskId(string modId, string key); // null when either is invalid
public static string Create(string modId, QuestDefinition definition, out QuestRefusal refusal);
public static bool AddObjective(string modId, string taskId, QuestObjectiveDefinition objective);
public static bool SetObjective(string modId, string taskId, string objectiveKey, ObjectiveState state);
public static bool SetCounter(string modId, string taskId, string objectiveKey, int amount);
public static bool HandIn(string modId, string taskId, string objectiveKey);
public static bool Finish(string modId, string taskId);
public static bool Fail(string modId, string taskId);
public static bool Reveal(string modId, string taskId, string key, string title, string description);
public static bool SetTitle(string modId, string taskId, string title);
public static bool SetTitle(string modId, string taskId, string objectiveKey, string title);
public static bool SetDescription(string modId, string taskId, string description);
public static bool Restart(string modId, string taskId);
public static bool Reset(string modId, string taskId);
public static bool Remove(string modId, string taskId);
public static bool JumpTo(string modId, string taskId, string objectiveKey);
public static bool Checkpoint(string modId, string taskId, string name, bool restore);
public static bool Checkpoint(string modId, string taskId, string name, Action restoreCallback);
}
Createreturns the task id, ornullwith the reason inrefusal.AlreadyExistsmeans the save already has a task with that id, for example from an earlier session, so callQuests.Getfirst and create only when it returnsnull.Rejectedmeans the game refused the first objective and no task was made.Failedmeans a game call threw before the task was made.PartiallyCreatedis the one refusal that comes with a non-null return: the task was made, but the game refused or threw on a later objective, so the quest exists with fewer objectives than the definition. Add the missing ones withAddObjective, or finish or fail the quest.AddObjectiveadds an Active objective to an Active quest of yours. It returnsfalsewhen the key is already used in that quest or the quest already has 12 objectives.SetObjectiveacceptsFailed,FinishedandAbandonedfor an Active objective, asQuests.SetObjectivedoes.SetCounterreturnstruewhen the counter changed. An amount outside 0 to the target is refused. Setting the target finishes the objective.Revealreturnstruewhen the objective is Active afterwards, even when the panel refresh after the activation throws; that failure is logged on its own. Its rules are in Hidden objectives and reveal.HandInreturnstruewhen the objective moved. It takes the items the objective asks for first, and gives them back when the move fails, so a refused call never costs the player an item. Its rules are in Handing in items.Next,NextDefinition,ItemIdandCountare checked when the definition is created. ANextthat is not a valid key, aNextDefinitionwhoseKeydoes not matchNext, a chain longer than 8 quests, or an item pair that does not fit, is refused withBadInput.Restart,Reset,Remove,JumpTo,Checkpointand the twoSetTitlecalls, withSetDescription, are in Rewinding a quest.
Complete example
This mod makes one quest with a counter, a hand-in and a follow-up quest. It also gives you keys to test the rewind calls. Every call that can refuse is checked, and the warning says why.
using ObenseuerCore;
using UnityEngine;
[ModInfo("example.lostbottle", "Lost Bottle", "0.1.0")]
[ModDependency("obenseuer.moddingframework", "0.11.0")]
public sealed class LostBottle : FrameworkMod
{
private const string QuestKey = "lost_bottle";
private const string ItemTitle = "Empty Glass Bottle";
private string taskId;
private ModKeybind searchKey;
private ModKeybind handInKey;
private ModKeybind jumpKey;
private ModKeybind restartKey;
private ModKeybind resetKey;
private ModKeybind removeKey;
protected override void OnLoad()
{
taskId = QuestAuthoring.TaskId(Mod.Id, QuestKey);
searchKey = Mod.Key("Search", "Count one search", KeyCode.F6);
handInKey = Mod.Key("HandIn", "Hand in the bottle", KeyCode.F7);
jumpKey = Mod.Key("Jump", "Jump to the hand-in step", KeyCode.F8);
restartKey = Mod.Key("Restart", "Restart a finished quest", KeyCode.F9);
resetKey = Mod.Key("Reset", "Reset the quest", KeyCode.F10);
removeKey = Mod.Key("Remove", "Remove the quest and make it again", KeyCode.F11);
Quests.Ready += Register;
Quests.TaskChanged += OnTaskChanged;
}
private void Register()
{
if (Quests.Get(taskId) != null) return;
ItemDef bottle = Items.Find(ItemTitle);
if (bottle == null)
{
Log.Warn("No item is named " + ItemTitle + ", so the quest was not made");
return;
}
QuestDefinition quest = new QuestDefinition { Key = QuestKey, Title = "The lost bottle", Description = "Find the bottle and hand it back." };
quest.Objectives.Add(new QuestObjectiveDefinition { Key = "search", Title = "Search the rooms", Target = 3 });
quest.Objectives.Add(new QuestObjectiveDefinition { Key = "return", Title = "Hand in the bottle", ItemId = bottle.Id, Count = 1 });
quest.NextDefinition = new QuestDefinition { Key = "reward", Title = "Collect the reward", Description = "The bottle was returned." };
quest.NextDefinition.Objectives.Add(new QuestObjectiveDefinition { Key = "claim", Title = "Claim the reward" });
QuestRefusal refusal;
if (QuestAuthoring.Create(Mod.Id, quest, out refusal) == null) Log.Warn("The quest was refused: " + refusal);
}
private void Update()
{
if (UiState.IsMenuOpen || UiState.IsPaused || UiState.IsInputBlocked) return;
if (Pressed(searchKey)) Quests.AddProgress(taskId, "search", 1);
if (Pressed(handInKey)) HandIn();
if (Pressed(jumpKey)) Log.Info("JumpTo: " + QuestAuthoring.JumpTo(Mod.Id, taskId, "return"));
if (Pressed(restartKey)) Log.Info("Restart: " + QuestAuthoring.Restart(Mod.Id, taskId));
if (Pressed(resetKey)) Log.Info("Reset: " + QuestAuthoring.Reset(Mod.Id, taskId));
if (Pressed(removeKey) && QuestAuthoring.Remove(Mod.Id, taskId)) Register();
}
private void HandIn()
{
if (!QuestAuthoring.HandIn(Mod.Id, taskId, "return"))
{
Log.Warn("The bottle was not handed in. The step must be active and the bottle must be in the inventory.");
return;
}
if (Quests.State(taskId, "search") == ObjectiveState.Finished) QuestAuthoring.Finish(Mod.Id, taskId);
}
private void OnTaskChanged(TaskChangedArgs args)
{
if (args.TaskId.StartsWith(Mod.Id + ".")) Log.Info(args.TaskId + " is " + args.State);
}
private static bool Pressed(ModKeybind key)
{
return key != null && key.Down();
}
}
The keys are listed in the Mods menu under Mod Keybinds. F6 counts one search, F7 hands in the bottle and finishes the quest once the search is done, F8 jumps to the hand-in step, F9 restarts a finished quest, F10 resets it from any state, and F11 removes it and makes it again. Items.Find returns null for a title the game does not have, so the quest is not made and the log says so. Register runs at every load, and the Quests.Get check keeps it from being made twice.
The follow-up quest is stored with the bottle quest through NextDefinition, so it starts by itself when the bottle quest finishes, even after a load. Quests.TaskChanged logs each change of your quests, which is how you see the follow-up start.
Self-check
Set Debug.QuestsSelfCheck in the framework’s config to log a check of the counter event rule and the ready retry bookkeeping at start. Set Debug.QuestAuthoringSelfCheck in the framework’s config to log a check of the definition rules, the hidden objective text rule, the reveal state rule and the owner rule at start. Set Debug.QuestAuthoringCommand to register the core_quests_check console command, which creates, advances and finishes a test quest in the loaded save and logs the result, and the core_quest reset and core_quest skip commands in Console commands. It adds a finished task to that save, so use a scratch save. The rule that rebuilds the game’s ten id lists is checked offline in the framework’s own test harness, before a release.