Skip to content

Area Quests

Kit Oliynyk edited this page Nov 2, 2026 · 2 revisions

TL;DR: An area quest is a list of steps stored in the area file. Each step names a begin trigger and an end trigger on a mob, object or room, and the game finds the Fenia handlers by method name. The engine tracks every player's step, runs the handlers, pays the declared rewards and calls global scripts at start, step end and completion. Use when: you create or edit an area quest, write step handlers, wonder why a quest never starts or never advances, or read the quest data from a script. Source of truth: plug-ins/loadsave/areaquestutils.cpp (the engine), src/core/areaquest.h (the data), plug-ins/olc/qedit.cpp and aedit.cpp (the editors), plug-ins/feniaroot/areaquestwrapper.cpp (the script API), dreamland_world/fenia.examples/queststep/ (handler templates). Verified: 2026-11-02 against dreamland_code 88da8302, dreamland_world 43880467, dreamland_fenia_public c28874b

What the engine does

A quest has two parts.

  1. Data in the <quests> section of the area file: title, description, who may take it, and the ordered steps. The data is static. A script can read it and cannot change it.
  2. Handlers, which are Fenia methods on the prototype of a mob, object or room, named after the quest, the step and the trigger.

On every player event that has a quest trigger (a player greets a mob, speaks, gives an item and so on), the engine looks at the methods defined on the target of the event.

For each quest that has a method on that target:

  1. The player is not on the quest. The engine does something only if this target has a step 0 begin method. It checks who may take the quest (see Quest data). If the player passes, it calls that begin method, marks the quest as started (step 0) and calls the global script onQuestStart. The start does not depend on what the begin method returns.
  2. The player is on the quest. The engine calls the end method of the player's current step. If that method returns true, the engine calls the global script onQuestStepEnd (which pays the step rewards), moves the player to the next step, and then calls the begin method of that next step. After the last step it marks the quest as completed and calls onQuestComplete.

Facts that follow from this:

  • Only an end method that returns true advances. A false or missing return keeps the player where they are.
  • The begin of step N+1 is called from inside the end of step N, on the same mob, object or room, and only if it is the post form of the same trigger. If it is not, the next step never gets its begin call. When the next begin has the same trigger type but is not the post form (for example onSpeech instead of postSpeech), the log gets aquest: begin/end trigger type mismatch. A different trigger type or a different target is skipped with no log line at all. The quest still advances.
  • Handlers are found by method name. The trigger fields in the XML are only data for the editor, so renaming a method in a script changes behaviour at once.
  • Eligibility (level, class and so on) is checked once, at the start. A player who changes between steps stays on the quest. If a step must re-check something, check it in the handler.
  • The quest data of a player (thisLife, step, times) is read-only from scripts.
  • NPCs never take part: the engine ignores events whose actor is an NPC. A pet that kills for its master does not count.
  • A quest that nobody has touched for the configured number of days is cancelled when its player logs in, through quest cancel so that onCancel runs. The flags onboarding and noexpire exempt a quest.

Quest data

<quests>
  <node type="AreaQuest">
    <vnum>9901</vnum>
    <title l="en">The Lost Lantern</title>
    <description l="en">The lamplighter has lost his lantern.</description>
    <minLevel>5</minLevel>
    <maxLevel>101</maxLevel>
    <limitPerLife>1</limitPerLife>
    <oncePerDay>false</oncePerDay>
    <flags></flags>
    <prereq>0</prereq>
    <steps>
      <node type="QuestStep">
        <beginType>mob</beginType>
        <beginValue>9910</beginValue>
        <beginTrigger>postGreet</beginTrigger>
        <endType>mob</endType>
        <endValue>9910</endValue>
        <endTrigger>onSpeech</endTrigger>
        <info l="en">The lamplighter asks for help. Tell him you will help.</info>
      </node>
      <node type="QuestStep">
        <beginType>mob</beginType>
        <beginValue>9910</beginValue>
        <beginTrigger>postSpeech</beginTrigger>
        <endType>mob</endType>
        <endValue>9910</endValue>
        <endTrigger>onGive</endTrigger>
        <info l="en">Find the lantern and give it to the lamplighter.</info>
        <rewardExp>200</rewardExp>
      </node>
    </steps>
  </node>
</quests>
Field Meaning
vnum Quest number, inside the vnum range of the area.
title, description Per language. Shown in the quest list.
minLevel, maxLevel Level limits. A maxLevel of 100 or more means no upper limit.
align Allowed alignments (good, neutral, evil).
classes, hometowns Allowed professions and home towns. Empty means all.
limitPerLife How many times per rebirth. 0 or less means no limit.
oncePerDay At most once in 24 hours.
prereq Another quest that must have been completed at least once in this life.
flags hidden (left out of the area help and the quest help listing), onboarding (newbie chain: not offered as a later quest to look for, and no pause after cancelling), nocancel (quest cancel is refused while the quest fits the player's level), noexpire (not cancelled for inactivity).
lootVnums Optional list of item vnums the quest hands out, used by the gear advisor.
steps The steps, in order. Each has the begin and end target, the info text and the rewards.

A step:

Field Meaning
beginType, beginValue, beginTrigger The target kind (mob, obj, room), its vnum and the trigger that starts the step.
endType, endValue, endTrigger The same for the end of the step.
info The hint the player reads in quest info, per language.
rewardExp, rewardGold, rewardQp, rewardVnum Paid when the step ends: experience, gold, quest points, an item.

Rules the editor warns about:

  • The end target of step N and the begin target of step N+1 should be the same mob, object or room.
  • The end trigger onX of step N pairs with the begin trigger postX of step N+1: onSpeech with postSpeech, onGive with postGive.

An eligible player has to pass these gates to start: level range, alignment, home town, profession, limitPerLife, oncePerDay, completed prereq, a one hour pause after cancelling the same quest (not for onboarding quests), and the optional quest hook onCanStart. The prereq test is "completed at least once in this life", and since the count resets at rebirth, prereq with limitPerLife 1 gives a clean once-per-life chain.

Creating a quest

The quest data cannot be created from a script. Use OLC or edit the area file and boot.

aedit <area number from alist>
quest create

quest create [vnum] takes the first free vnum of the area (or the one you give) and opens qedit. quest delete <vnum> removes a quest. quest <vnum> opens one. qedit <vnum> and qedit list also work from outside.

qedit commands:

Command Meaning
title, uatitle, rutitle Name.
desc, uadesc, rudesc Description.
minlevel, maxlevel Level limits.
align, classes, hometowns Eligibility.
perlife <number> Times per rebirth. -1 means no limit.
perday y|n Once per day.
prereq <vnum> The quest that must be done first.
flags <names> Toggles quest flags (? areaquest_flags).
fenia <hook> Opens the script editor for a quest-level hook.
step ... Step editor, below.
show, done Redraw, leave. done marks the area changed, so also run asave changed.

The step command:

step add
step <n> begin|end type mob|obj|room
step <n> begin|end vnum <vnum>
step <n> begin|end trigger <name>
step <n> begin|end fenia clear
step <n> begin|end fenia
step <n> info|uainfo|ruinfo <text>
step <n> qp|gold|exp|item <number>
step <n> up
step <n> down
step <n> del
  • step add makes a step whose begin copies the end of the previous step with on changed to post. A first step starts with a greeting from the first mob of the area.
  • trigger <name> accepts the triggers that exist for that target kind. The clickable menu offers only the sensible ones: all of them for the begin of step 0, the post ones for the begin of later steps and the on ones for an end.
  • fenia opens the script editor for the handler of that step and stage, and writes the code to the script store.
  • Changing type, vnum or trigger of a stage deletes the script that was attached to it. up and down do that for both swapped steps.
  • step <n> del removes the step. It does not remove the scripts: the editor reminds you to clean them up.

Triggers and handlers

A handler is a method on the prototype of the begin or end target, named:

q<quest vnum>_step<step number>_<begin|end>_<trigger>

for example q9901_step0_end_onSpeech.

The triggers below are the ones qedit offers. The arguments are those of the handler.

Target Trigger pair Arguments When
mob onGreet / postGreet (mob, ch) A player enters the room, and the mob is awake and can see the player.
mob onSpeech / postSpeech (mob, ch, speech) A player says something in the room of an awake mob, or tells it something from the same room.
mob onGive / postGive (mob, ch, obj) A player gives the mob an item.
mob onSocial / postSocial (mob, ch, victim, social) A player does a social on the mob.
mob onWake / postWake (mob, ch) A player wakes the mob.
mob onDeath / postDeath (mob, ch) The mob dies, ch is the killer.
mob onSell / postSell (mob, ch, obj, cost, number) A player buys from the mob.
mob onTrain / postTrain (mob, ch, stat) A player trains with the mob.
mob onTeach / postTeach (mob, ch, arg) A player practices with the mob.
mob onPourOut / postPourOut (mob, ch, container, liqname, amount) A player pours a liquid on the mob.
obj onGet / postGet (obj, ch) A player picks the item up.
obj onGive / postGive (obj, ch, mob) A player gives the item away.
obj onPut / postPut (container, ch, item) A player puts an item into the container.
obj onUse / postUse (obj, ch, args) A player uses the item.
obj onSpeech / postSpeech (obj, ch, speech) A player says something in a room where the item lies on the floor or is carried (in an inventory or worn).
room onGreet / postGreet (ch, fromRoom, moveType) A player enters the room.
room onCommand / postCommand (ch, cmdName, cmdArgs) A player types any command in the room.

The engine itself fires get, give, put, use, speech, greet, social, wake, command and death. The sell, train, teach and pour-out triggers are fired by game scripts through the character's trigger method.

How on and post differ:

  • on... runs at once and the engine reads the result: true means the step is done. Every end handler is an on..., and so is the begin of step 0 if you choose that form.
  • post... is postponed until the current event is finished and its result is ignored. Use it for the begin of steps 1 and up, so that an NPC can speak after the player's own words were already shown.

Rules for handlers:

  • A handler has to check its own conditions. The engine calls it for every player event of that kind on that target.
  • Return true from an end handler only when the step is really done.
  • A post handler cannot stop the quest from starting. To refuse a player, use onCanStart.
  • A post handler may pause for dramatic effect, but a pause means the mob or player may be gone when it resumes: check again.

A small example for the quest above:

.get_mob_index(9910).q9901_step0_end_onSpeech = function(mob, ch, speech) {
    if (speech.match("help|yes|ok"))
        return true;
    mob.say("Will you help me find my lantern?");
    return false;
};

.get_mob_index(9910).q9901_step1_end_onGive = function(mob, ch, obj) {
    if (obj.vnum != 9920) {
        mob.say("That is not my lantern.");
        mob.giveBack(ch, obj);
        return false;
    }
    obj.extract(true);
    mob.say("My lantern! Thank you!");
    return true;
};

Two things to remember for greet-based quests:

  • A mob greet runs only for an awake mob that can see the player. A giver in the dark, or one lying down (position mort, sleep), never starts the quest. Give such a quest to a room greet instead.
  • The engine marks the quest as started before the mob's own onGreet runs. A plain onGreet on the quest giver runs together with the quest's postGreet, so give it a check that it should stay silent.

Only one trigger swallows the plain prototype trigger: when a quest step consumes a give, the prototype's own onGive is skipped. Greet and speech run both.

Quest-level hooks

Methods on the quest object, .AreaQuest(<vnum>). The engine calls them if they exist.

Hook Called Result
onCanStart(ch) After all other start checks. A non-empty string refuses the start and is the reason. An empty string allows it.
onCanEverStart(ch) When listing quests. A non-empty string says the quest is closed for the rest of this life (for example the one slot is taken).
onCancel(ch) The player cancels the quest. Clean up temporary mobs or items.
onInfo(ch) quest info shows the current step. A string to show instead of the step's info.

qedit fenia offers onCanStart and onCancel as templates.

Rewards and global scripts

Rewards are data. The step fields rewardExp, rewardGold, rewardQp and rewardVnum are paid by the game's global script onQuestStepEnd, after the end handler returns true. A handler does not pay them. For a reward that the player chooses or that depends on a condition, leave the XML field at 0 and give it in the handler.

The engine calls these global scripts: onQuestStart(ch, quest), onQuestStepEnd(ch, quest, stepIndex), onQuestComplete(ch, quest) and onQuestCooldown(ch, quest) (a player tried to start a quest in the one hour pause after cancelling). The shipped scripts show hints, pay rewards and announce completion. A quest with the onboarding flag skips the announcements.

Script API

.AreaQuest(vnum) returns the quest and raises an error for an unknown vnum. It only looks quests up. There is no call that creates a quest or changes a step.

Member Meaning
vnum, title, description, minLevel, maxLevel, limitPerLife, oncePerDay, align, classes, hometowns, flags, area The data, read-only.
getTitle(lang), getDescription(lang) By language: 0 English, 1 Russian, 2 Ukrainian.
step(n) The step as a structure.
data(ch) The player's progress: step, times, thisLife. A step of -1 means not on the quest. Both completing and cancelling set it to -1.
cancel(ch) Cancels for the player and calls onCancel. Returns true if the quest was running.
info(ch) The hint for the player's current step.
canParticipate(ch), canParticipateEver(ch) Whether the player can start it now, or at any point in life.

The game's script library also has helpers for the usual handler chores: .tmp.quest.active, .tmp.quest.cancelled, .tmp.quest.completed, .tmp.quest.had, .tmp.quest.lastStep, .tmp.quest.canStart (the standard check at the top of a greeting) and .tmp.quest.cutscene (a pause between lines that cancels the quest if player or mob leave). They are not part of the public Fenia repository. The public utilities include .tmp.mob.canGreet and .tmp.mob.canSpeak, which a giver handler usually calls first. canSpeak is true for an NPC only if its form is sentient or magical.

Pitfalls

  • Annul before delete. Handlers are stored on the prototype in the script database. Deleting a handler's source file without first setting the method to null leaves the old method behind, and Fenia duplicates the file. Details: Fenia-Scripts.
  • Handlers before data. Posting q<N>_step... handlers before the quest exists in the area file logs AreaQuest: unknown quest id at every matching event.
  • The area file is read at boot. A new quest appears after a reboot. Handlers can be hot-loaded once the quest data exists.
  • Quest start is not cancellable by a handler. qdata.start() runs after the begin method whatever it returns.
  • Eligibility is checked at the start only.
  • step del and qedit do not delete handlers. Clean up by searching the script store for the quest vnum.
  • Charmed pets do not count. A killing blow by a pet earns the master nothing through a quest step.

See also: OLC, OLC-Other-Editors, Fenia-Triggers, Fenia-Scripts, Area-Format.

Clone this wiki locally