Metin2 Quest Scripting Guide: How Dungeon Quests Work
Writing a Metin2 dungeon quest from scratch is one of the most involved tasks a private server admin can face. Between the event-driven Lua structure, the dungeon instance API, server timers, state flags, and per-floor sequencing — there is a lot to get right. This guide walks through every piece with real examples.
If you want to skip the theory and generate a working quest immediately, use the Metin2 Dungeon Quest Generator.
1. The Quest File Structure
Every Metin2 quest follows the same outer structure:
quest questname_zone begin
state start begin
-- event handlers here
end -- state
end -- quest
All event logic lives inside state start begin ... end. Each handler is a when ... begin ... end block. Dungeon quests typically use several handler types:
- when login begin — fires when a player logs in, used to initialise state and warp the player out if the dungeon is already expired
- when kill begin — fires when the player kills something; filtered by
npc.get_race()to target specific mobs or metins - when [vnum].chat."text" begin — fires when a player clicks a chat option on an NPC
- when [vnum].take begin — fires when a player hands an item to an NPC
- when [name].server_timer begin — fires when a named server timer expires
- when [name].server_timer begin (loop) — repeating timer, set up with
server_loop_timer()
2. Dungeon State with d.setf() and d.getf()
Dungeon quests use d.setf(key, value) and d.getf(key) to store per-instance state. These are scoped to the dungeon instance — every party has its own values. Common state variables:
d.setf("dungeon_floor", 1); -- which floor is active
d.setf("initialized", 1); -- prevent re-init on login
d.setf("dungeon_complete", 0); -- set to 1 when final boss dies
d.setf("start_time", get_time()); -- used for completion time display
Unlike pc.setf(), dungeon flags are lost when the instance is cleaned up — they don't persist to the database.
3. Creating the Dungeon Instance
When a player or party leader clicks the entrance NPC chat, you create an instance with d.make_dungeon(map_index):
when 30000.chat."Enter Dungeon" begin
local d_index = d.make_dungeon(353);
if (d_index == 0) then
say_reward("Could not create dungeon instance.[ENTER]")
return;
end
if (party.is_party()) then
party.warp(11500, 10500, d_index);
else
pc.warp(11500, 10500, d_index);
end
end
Note that coordinates passed to warp() are in engine units — you multiply your map-relative coordinates by 100. A position of (115, 105) on the map becomes warp(11500, 10500, d_index).
4. Server Timers for Floor Transitions
Floor transitions use server_timer(name, delay, map_index). When the timer fires, the handler receives the map index via get_server_timer_arg() and uses d.select(arg) to scope the logic to the correct instance:
-- start floor 2 after a 5-second delay
server_timer("mydungeon_floor2", 5, d.get_map_index());
-- the timer handler
when mydungeon_floor2.server_timer begin
if (d.select(get_server_timer_arg())) then
d.setf("dungeon_floor", 2);
d.notice("Moving to the next area...")
-- spawn floor 2 content
d.spawn_mob(6051, 150, 150);
end
end
Always pass d.get_map_index() as the third argument so the timer fires in the correct instance. Without it, all instances share the same timer — leading to race conditions across parties.
5. Metin Wave Floor
A metin wave floor spawns several metin stones and advances when all are destroyed. The kill counter is tracked via a dungeon flag:
-- spawn phase: called from StartFloor()
d.spawn_mob(8100, 100, 120);
d.spawn_mob(8100, 150, 180);
d.spawn_mob(8100, 200, 140);
-- kill handler (3 metins total)
when kill with npc.get_race() == 8100 and d.getf("dungeon_floor") == 1 begin
d.setf("floor1_metin_count", d.getf("floor1_metin_count") + 1);
if (d.getf("floor1_metin_count") == 3) then
d.notice("All metins destroyed!")
server_timer("mydungeon_floor2", 5, d.get_map_index());
else
d.notice(string.format("Metins remaining: %d.", 3 - d.getf("floor1_metin_count")))
end
end
6. Monster Wave Floor with Regen File
Monster wave floors use d.set_regen_file() to load a regen file and server_loop_timer() to poll when all monsters are dead:
-- spawn phase
d.set_regen_file("dungeon/mydungeon_floor2.txt");
d.set_regen_time(0);
server_loop_timer("mydungeon_floor2_check", 5, d.get_map_index());
-- loop timer handler
when mydungeon_floor2_check.server_timer begin
if (d.select(get_server_timer_arg())) then
if (d.count_monster() == 0) then
clear_server_timer("mydungeon_floor2_check", get_server_timer_arg());
d.notice("All monsters defeated!")
server_timer("mydungeon_floor3", 5, get_server_timer_arg());
end
end
end
The regen file itself uses the standard format:
r 167 150 5 5 0 0 90s 100 1 6051
Columns: type, X, Y, sizeX, sizeY, direction, isAggressive, respawnTime, maxCount, count, mobVnum.
7. Final Boss and Completion
when kill with not npc.is_pc() and npc.get_race() == 6051
and d.getf("dungeon_floor") == 3 and d.getf("dungeon_complete") == 0 begin
d.setf("dungeon_complete", 1);
d.setf("completion_time", get_time());
notice_all(string.format("[Dungeon] %s cleared the dungeon in %s!",
pc.get_name(),
get_time_format(d.getf("completion_time") - d.getf("start_time"))
))
d.notice("The dungeon is complete!")
server_timer("mydungeon_out_room", 30, d.get_map_index());
end
when mydungeon_out_room.server_timer begin
if (d.select(get_server_timer_arg())) then
d.notice("Teleporting out...")
d.exit_all();
end
end
8. Generate It Instead of Writing It
All of the above patterns — initialisation, floor sequencing, metin waves, monster waves, boss kills, item delivery, time limits, exit timers — are handled automatically by the Metin2 Dungeon Quest Generator. Configure your dungeon visually, preview the generated Lua in real time, and download a zip containing the quest file, the Lib.lua, and all regen files. The smart extraction system includes only the functions.lua helper functions your quest actually needs.