Old School Ops

Dungeon Scripts

Writing and changing instanced dungeons in the server's scripts/dungeons folder. The parts of a dungeon, a worked example, and every field.

Applies to RF Online 4.55RF Online 4.15RF Online 1.5RF Online 2.2.3.2 · updated 2026-11-06
On this page

Every file in scripts/dungeons describes one instanced dungeon: a private copy of a map that a party opens with a key item, fights through, and gets paid for. The files are Lua, written as lists and tables.

What a Dungeon Is Made Of

  • The dungeon is the whole run: which map, who may enter, which key opens it, and what it pays.
  • A mission is one stage of the run, roughly one room or one wave: how long the party has, what to put on the map, and what ends the stage. Missions run in the order they are listed.
  • A goal is one thing the party has to do inside a mission: kill twenty of something, kill one boss, collect seven of an item. A mission can have several.
  • Reactions happen while a mission runs: reinforcements when somebody walks into a room, a kill that buys more time, a gate that opens when a boss falls.

A Worked Example

This is hora_infernal_50.lua, cut down: five bosses in a row, seven minutes each and ten for the last.

dungeon "hora_infernal_50" {
  title      = "Hora Infernal",
  keys       = { "ixkey27" },        -- the key item that opens it
  map        = "Dungeon00",
  spawn      = "dpstart",            -- where the party lands
  party_only = true,
  max_party  = 8,
  level      = { min = 50, max = 55 },

  blocks = {
    -- a pool of markers; block("BOSS") picks one of these at random
    BOSS    = { "dm00m50_0", "dm00m54_0", "dm00m56_0", "dm00m53_0" },
    PitBOSS = { "dm00m51_0", "dm00m52_0", "dm00m55_0", "dm00m57_0" },
  },

  reward = {
    exp    = 218039766451,
    dalant = 150000,
    items  = {
      { to = "one",   code = "iyust03", count = 1, chance = 0.3 },
      { to = "other", code = "iyust03", count = 1, chance = 0.3 },
    },
  },

  missions = {
    {
      name        = "m01_hora_infernal_lava",
      description = "a50",                 -- the message the client shows
      time_limit  = 420,                   -- seconds
      result      = "next",                -- when it is done, run the next mission
      spawn = {
        { monster = "05BA3", count = 1, at = block("BOSS") },
      },
      add_time = {
        { monster = "05BA3", seconds = 0 },
      },
      goals = {
        hunt("05BA3", 1),                  -- kill one of it
      },
    },

    {
      name         = "m05_hora_infernal_draco",
      description  = "a51",
      complete_msg = "a52",                -- shown when the whole run is won
      time_limit   = 600,
      result       = "complete",           -- last stage: the run is over
      spawn = {
        { monster = "0BBA3", count = 1, at = block("PitBOSS") },
      },
      goals = {
        hunt("0BBA3", 1),
      },
    },
  },
}

Adding a Dungeon

  1. Copy the closest file. hora_infernal_50.lua is the simplest complete one, and dark_hole_01_4549.lua uses every feature.
  2. Rename the file and change the name in the dungeon "..." line to match. The name is unique in the folder, lower case, with words joined by underscores.
  3. Point map at a map and spawn at a marker on it. Reuse the marker names of a dungeon on the same map, in lower case. The load refuses a marker that is not on that map.
  4. Add the key to _index.lua if a key item should open it. Without a key, nothing in a player's bag opens it.
  5. Fill in the missions.

Codes and marker names belong to your version's content and change between RF versions, so check any value you copy from another version.

Checking and Reloading

Everything is checked when the server loads it. An unknown monster or item code, a marker that is not on the map, a mission nothing can reach, a next that names no mission, a gate that leads nowhere or an empty goal list each name the file in the log and skip that dungeon. The others keep working.

There is no build step. Change a file, then restart the server. On Dragonborn and Golden Age you can instead send reload-content from GoMMO Manager, and a load that fails keeps the dungeons already loaded.

Field Reference

Dungeon

FieldTypeMeaning
titletextDisplay name for logs and the admin panel. Players do not see it.
keyslist of textKey item codes that open this dungeon.
maptextMap code, for example Dungeon00.
spawntextMarker where entrants land.
party_onlytrue or falseWhether a party is required to open it.
max_partynumberHow many members may be inside.
level{ min =, max = }Level band allowed in. Omit for no limit.
descriptiontextA client message code shown when the run starts. Usually absent.
blockstable of listsNamed pools of markers.
rewardtableWhat the run pays out.
missionslistThe stages, in run order.

Blocks

A block is a named pool of markers. at = block("BOSS") puts the monster at one of the pool's markers at random, and at = "dm00m50_0" puts it at exactly that marker.

Reward

FieldTypeMeaning
expnumberExperience for the whole run.
dalantnumberMoney.
pvpnumberContribution points.
itemslistWeighted item rolls.

exp, dalant and pvp are usually one number that everyone gets. When the finisher and the rest differ, write exp = { one = 10, other = 4 }: one is the player who finished the run, and other is everyone else.

Each row of items is { to = "one", code = "iyust03", count = 1, chance = 0.3 }. The engine picks one row, weighted by chance, and grants that item, so a chance of 1 is not a guarantee when the list holds other rows.

Mission

FieldTypeMeaning
nametextUnique within the file. Other missions refer to it by this name.
descriptiontextClient message code shown when the stage starts.
complete_msgtextClient message code shown when the stage is won.
when_morphedtableReplaces description and complete_msg when a morphs roll fired.
time_limitnumberSeconds. 0 means untimed.
accept_attextThe room this stage belongs to, in the dungeons built around gates.
starttextMarker where the party is placed for this stage, when it differs from spawn.
resulttextWhat finishing does.
nexttextThe mission to run next, written only when it is not the next one listed.
goal_modetext"any" means one goal is enough. Absent means all are needed.
gateslist of textGates this stage opens when it is done.
spawnlistWhat to put on the map when the stage starts.
repoplistA population that keeps refilling while the stage runs.
add_timelistSeconds a kill of a given code adds to the stage's clock.
triggerslistWhat happens when a player walks into a marker.
respondslistWhat happens when a particular monster is killed.
morphslistA spawned monster may turn out to be a different one.
dropslistAn extra item drop bound to one monster code.
goalslistWhat the party has to do. Required.

result is one of three words:

  • "complete" ends the whole run as a win. The last stage of a linear dungeon has this.
  • "next" moves on to the following mission.
  • "portal" opens this stage's gates and waits for the party. The Dark Hole dungeons are built this way: rooms behind gates rather than a queue of waves.

Gates

A gate is one of the map's own portals. Each gate leads to a spot on the same map, and the stage that runs next is the first one listed whose accept_at holds that spot. So you choose where a gate goes by where its room is, and next does not come into it.

When a "portal" stage ends, nothing else counts until the party moves on. The leader takes the gate, and every member has to be standing at it. The stage's clock keeps running while they walk, so time left over is time to reach the gate. When a gate is taken, everyone arrives in the next room together.

The stage's monsters leave a few seconds after it ends.

A stage does not have to open everything. Leave gates out, give each goal its own opens, and the party's choice of what to kill decides which way they go. This is how the last room of a Dark Hole picks its final boss.

A file that opens no gate at all, names a marker that is not a portal, or has a gate leading to no stage's room is refused when the server loads it.

Spawning

spawn = {
  { monster = "05BA3", count = 1,  at = block("BOSS") },
  { monster = "001A0", count = 40, at = "dm00m50_0" },
},

monster is a monster code, count is how many, and at is a marker name or a block(...).

repop rows carry two more numbers, period and limit. Their units are not established, so leave those values as they are.

add_time rows give the party more time. Each kill of that code adds that many seconds to the stage's clock, and the party sees the clock change:

add_time = {
  { monster = "06A03", seconds = 2 },
},

A negative number takes time away, and zero changes nothing. Files that still say respawn load with a warning and act the same as add_time.

Triggers and Responds

A trigger fires the first time a player walks into a named marker:

triggers = {
  { at = "dq_event01",
    spawn = {
      { monster = "06203", count = 5, at = "dmm01_0" },
    },
  },
},

A respond fires when a named monster is killed. at = HERE puts the reinforcements where that kill happened:

responds = {
  { on_kill = "0A50B",
    spawn = {
      { monster = "06403", count = 2, at = HERE },
    },
  },
},

Goals

goals = {
  hunt("05BA3", 1),        -- kill 1 of monster 05BA3
  hunt(ANY, 220),          -- kill 220 of anything
  hunt("006A3", ALL),      -- kill every one of them on the map
  take("evddd01", 7),      -- pick up 7 event objects, dropped through `drops`
  loot("iyust03", 3),      -- collect 3 of an item
},

ANY and ALL are words the scripting layer provides, so do not put them in quotes.

A goal can carry an options table as a third argument:

hunt("0A70B", 1, { opens = { "dp_gate04" } }),
hunt(ANY, ALL, { description = "a8" }),

opens is a list of gates that unlock the moment that one goal is met. description is a client message code for that goal on its own.

The Key Table

_index.lua records which key item opens which dungeon, and lists every key on your server. Each dungeon file repeats its own keys in keys, and that field is the authority: if the two disagree, the dungeon file wins and the index is stale.

return {
  keys = {
    ["ixkey25"] = "dark_hole_01_4549",
    ["ixkey27"] = "hora_infernal_50",
  },
  unkeyed = { "_template" },
}

Files Beginning With an Underscore

_index.lua is the key table and declares no dungeon. _template.lua is a complete example to copy; no key opens it and its rewards are placeholders. The loader skips both, so start the name of anything in this folder that is not a real dungeon with _.

Something here wrong?

Out of date, incomplete, or assuming something it should not. Documentation errors are bugs, and this one arrives tagged and pointed at this page.

Top xp sources

How xp works
Loading…

Open the full search page