Questing & Storylines with Quest scripting engines
Implementing branching quests in MUDs requires managing player state through text interactions. This guide covers building a state-driven fetch quest with dialogue trees using standard MUD trigger systems, applicable to CircleMUD, ROM, and similar C-based codebases.

Define quest state storage in player data
Add quest state tracking to your player structure or use an external quest log system. For a simple fetch quest, track quest_started, has_item, and quest_completed states. In C-based MUDs, add these to struct player_data or use a bitvector flag system to ensure persistence across reboots.
/* In structs.h */
#define QUEST_OLD_MAN_HEALING 1
struct player_data {
/* existing fields */
int quest_flags[32]; /* Bitvector for active quests */
int quest_state[32]; /* 0=inactive, 1=active, 2=complete */
};⚠ Common Pitfalls
- •Do not use global variables for quest state
- •Ensure state persists through player logout
Create the quest giver NPC trigger procedure
Implement a special procedure (spec_proc) or mob program that activates when a player speaks specific keywords to the NPC. Parse the player's input for 'quest' or 'help' and check current quest state before responding to prevent duplicate quest acceptance.
SPECIAL(quest_giver_old_man) {
char buf[MAX_INPUT_LENGTH];
if (CMD_IS("say") || CMD_IS("'")) {
skip_spaces(&argument);
strcpy(buf, argument);
if (!str_cmp(buf, "quest")) {
if (GET_QUEST_STATE(ch, QUEST_OLD_MAN_HEALING) == 0) {
act("The old man says, 'Please bring me a healing potion.'",
FALSE, mob, 0, ch, TO_VICT);
SET_QUEST_STATE(ch, QUEST_OLD_MAN_HEALING, 1);
return TRUE;
}
}
}
return FALSE;
}⚠ Common Pitfalls
- •Avoid hardcoding player names in trigger logic
- •Check for NULL character pointers before string comparison
Build branching dialogue state machine
Structure the NPC's responses using a switch statement or if-else chain based on quest_state. State 0 shows initial greeting, State 1 shows reminder text for active quests, State 2 handles item acceptance dialogue, and State 3 prevents re-completion.
void handle_quest_dialogue(struct char_data *ch, struct char_data *npc) {
int state = GET_QUEST_STATE(ch, QUEST_OLD_MAN_HEALING);
switch(state) {
case 0:
send_to_char(ch, "The old man looks sick and pale.\r\n");
break;
case 1:
send_to_char(ch, "The old man asks, 'Did you bring the potion?'\r\n");
break;
case 2:
send_to_char(ch, "The old man smiles weakly.\r\n");
break;
case 3:
send_to_char(ch, "The old man ignores you, looking healthier now.\r\n");
break;
}
}⚠ Common Pitfalls
- •Missing state transitions can lock players out of completion
- •Text output must respect 80-character line limits for standard MUD clients
Implement quest item handling and validation
Create an object with a special procedure that triggers when given to the NPC via the 'give' command. Validate that the giver is the quest holder and state equals 1 (accepted). Atomically remove the item from inventory and update state to 2 before triggering completion.
SPECIAL(healing_potion_quest) {
char arg1[MAX_INPUT_LENGTH], arg2[MAX_INPUT_LENGTH];
struct obj_data *obj;
if (CMD_IS("give")) {
two_arguments(argument, arg1, arg2);
if (!(obj = get_obj_in_list_vis(ch, arg1, ch->carrying)))
return FALSE;
if (isname("potion", arg1) && isname("old man", arg2)) {
if (GET_QUEST_STATE(ch, QUEST_OLD_MAN_HEALING) == 1) {
obj_from_char(obj);
extract_obj(obj);
SET_QUEST_STATE(ch, QUEST_OLD_MAN_HEALING, 2);
act("You give $p to the old man.", FALSE, ch, obj, 0, TO_CHAR);
complete_quest(ch); /* Call reward function */
return TRUE;
}
}
}
return FALSE;
}⚠ Common Pitfalls
- •Players may attempt to exploit by giving item multiple times if state check fails
- •Ensure item is removed from inventory before giving reward to prevent duplication bugs
Add completion validation and reward distribution
Finalize the quest by setting completion flags and delivering rewards. Use atomic operations: verify state equals 2, set state to 3 (completed), then distribute experience points or items. This prevents race conditions in multi-threaded environments and stops double-dipping.
void complete_quest(struct char_data *ch) {
if (GET_QUEST_STATE(ch, QUEST_OLD_MAN_HEALING) != 2)
return;
/* Atomic: Check, Set, Reward */
SET_QUEST_STATE(ch, QUEST_OLD_MAN_HEALING, 3);
GET_EXP(ch) += 500;
send_to_char(ch, "Quest complete! You gain 500 experience.\r\n");
act("$n looks more experienced.", TRUE, ch, 0, 0, TO_ROOM);
}⚠ Common Pitfalls
- •Race conditions in multi-threaded MUDs can cause double rewards
- •Always validate player level or prerequisites before final reward distribution
Test state transitions and edge cases
Walk through every state permutation using a test character: accepting quest, logging out mid-quest, attempting to complete without item, trying to restart after completion, and verifying database persistence across copyover or reboot. Check that quest logs display correctly to players.
⚠ Common Pitfalls
- •Incomplete testing leads to stuck quest states in production
- •Player file corruption can reset quest progress causing duplicate completions
What you built
State-driven quest systems require rigorous validation at each transition point. Deploy changes to a test port first, verify persistence across reboots by checking player file saves, and monitor for player reports of stuck states before promoting to production. Consider implementing a 'quest reset' admin command for emergency fixes.