Skip to content

Engine Time and Pulses

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

TL;DR: The server runs one loop at 4 pulses per second. Everything periodic hangs off a pulse counter: a 3 second combat round, a 4 second mobile round, a 60 second tick (one game hour), and a few slower sweeps. Intervals are config values, the sweeps are in plug-ins/updates. Use when: you need to know how often something runs, which trigger fires on which cadence, why a log says "pulse overflow", or you are adding periodic work. Source of truth: src/dreamland.cpp (DreamLand::run, pulseEnd), plug-ins/updates/update.cpp (update_handler), etc/dreamland.xml.template, src/core/scheduler/dlscheduler.h. Verified: 2026-11-02 against dreamland_code 88da8302, dreamland_world 43880467, dreamland_fenia_public c28874b

Units

Unit Length Where it comes from
Pulse 250 ms pulsePerSecond = 4 in etc/dreamland.xml
Second 4 pulses
Combat round 12 pulses, 3 s pulseViolence
Mobile round 16 pulses, 4 s pulseMobile
Tick 240 pulses, 60 s pulseTick
Game hour one tick
Game day 24 ticks, 24 min
Game month 35 days, 14 h
Game year 17 months, 238 h

All four pulse* values are fields of the DreamLand config object and can be overridden in the config file. Counters in code that are written as seconds multiply by pulsePerSecond, so changing it rescales them too. The combat round, mobile round and tick are not scaled that way, they are separate numbers.

A pulse is 250 ms on purpose. Anyone whose ping is below that gets commands processed in the order they arrived.

Fenia .scheduler.sleep(n) takes pulses, not seconds. Sleep 4 for one second.

The pulse loop

DreamLand::run repeats three steps until shutdown:

  1. scheduler->tick() runs every scheduled task, ordered by priority.
  2. pluginManager->checkReloadRequest() applies a pending plugin reload. Reloads happen between ticks, never inside one.
  3. pulseEnd() first saves changed Fenia objects to the database until 40 ms before the pulse deadline. Then it serves sockets (telnet, WebSocket, HTTP API) until the deadline.

Scheduler priorities, lowest runs first (SCDP_* in dlscheduler.h):

Priority Phase What runs
1000 initial first pulse after boot only: weather init and the forced first area reset
2000 to 4000 I/O init, poll, read read input, count down wait and daze of connected players, parse and run commands
5000 auto update_handler (all the periodic sweeps below), auction (5010), clan tasks
6500 round per-entity Fenia and plugin round tasks, global quests
7000 I/O write flush output to players
10000 final closes and frees descriptors marked closed

A task that re-queues itself with putTaskInSecond(n) runs after n seconds (n * pulsePerSecond pulses). The hourly task works this way with 3600.

What runs when

update_handler is called once per pulse. Each sweep has its own countdown. The first values are offsets chosen so that sweeps do not land on the same pulse.

Cadence Sweep Notes
Every pulse Remove dead NPCs
char_update_lostlink quits link-dead players after the lostlink config timeout, and removes newbies that lost their connection
aggr_update mob aggression. Also counts down wait and daze for NPCs and link-dead players
4 pulses (1 s) hit point, mana and move regeneration CharacterParamsUpdateTask, a per-character round task that re-queues itself every second
12 pulses (3 s) violence_update one round of blows for every fighter, then onFight on characters and worn items
16 pulses (4 s) mobile_update onSpec on every character, mob behavior spec and the legacy spec function
16 pulses (4 s) player_update PK, ghost and death timers of connected players, light sources
16 pulses (4 s) diving_update onSpec and onDiveUpdate on rooms, and room affect onSpec
16 pulses (4 s) water_float_update onSpec on every object, then floating and sinking in water rooms
20 pulses (5 s) room_saving writes the items and saved mobs of rooms marked dirty
28 pulses (7 s) track_update one step for each mob that is following a trail
49 pulses (about 12 s) room_affect_update onUpdate of room affects (the plague spreading and similar)
240 pulses (1 tick) tick sweeps see the next section
440 pulses (110 s) area_update area onUpdate, reset bookkeeping, see Area resets
2400 pulses (10 min) perf dump per-block timing counters to the log
3600 s (real) hourly task hourly() of object and mob behaviors

Notes:

  • The mobile, player, diving and water sweeps have the same period but different offsets (3, 11, 7 and 8). They were once phase-locked on one pulse and overflowed together.
  • Mob-index onSpec and behavior spec apply to NPCs only. For a player, mobile_update fires the global trigger onSpec (through gprog) after the affect handlers.
  • Behavior onSpec and postSpec on a mob are skipped while its area is flagged empty (no player there since the last reset, cleared when someone enters). Instance wrappers and mob-index onSpec scripts are not gated and always run.
  • Auction: a running auction advances one step ("going once", "going twice", "sold") every 45 seconds.

Tick sweeps

The tick is staggered over four pulses so the heavy sweeps do not share one. In order:

Where Sweep
3rd pulse before the tick weather and sunlight. The world clock moves one hour here, the calendar rolls over, new-day bonuses are announced, and the config XML is saved to disk
2nd pulse before the tick obj_update: item timers, item affect countdown, onArea on items, key and consumable decay
1st pulse before the tick room_update: room affect countdown
the tick pulse char_update, then char_update_prog, char_update_autosave, check_reboot

char_update per character: restores racial and granted permanent affects, hunger and thirst (the desire system), idle timer, NPC despawn timers, affect countdown, and the "dying" checks. char_update_prog then fires onArea on every character. char_update_autosave saves every online player, at most once per 60 seconds. check_reboot counts down a pending reboot and warns players at 15, 10, 5, 4, 3, 2 and 1 minutes.

A mortal who has been idle for 12 ticks gets a "dissolves into the air" message. Idle players are quit by the autosave pass once their idle timer passes the autoquit value of the config JSON, and link-dead players once it passes lostlink. Immortals are exempt.

Area resets

area_update runs every 110 seconds, and only on a pulse where no other heavy sweep ran. It calls the area onUpdate, ages every area by one step and queues areas that are due. area_update_next then resets one queued area per quiet pulse, so resets never stack on one pulse.

An area is due when it has aged at least 3 steps and one of these holds:

  • it was visited recently and either no player is inside or it is 15 steps old
  • it is 31 steps old, whatever happens
  • the reset was forced (boot)

After a reset a popular area is put back at age 13, so it is due again two steps later. Any other area gets a random age from 0 to 3, and if no player is inside it is also marked empty, which means only the 31-step rule applies until someone visits.

Calendar

One tick adds one hour to the stored worldTime. Hour 0 to 23, day 0 to 34, month 0 to 16. Sunrise and sunset hours depend on the month. On a fresh config where worldTime is 0 the clock is seeded from the real time. The value is saved to the main config file every tick, so a crash loses at most one hour.

Triggers by cadence

Fenia trigger Fires from Cadence
onSpec on mobs mobile_update 4 s
onSpec on objects water_float_update 4 s
onSpec and onDiveUpdate on rooms diving_update 4 s
onFight violence_update 3 s
onArea on mobs and players char_update_prog 60 s
onArea on objects obj_update 60 s
area onUpdate area_update 110 s
room affect onUpdate room_affect_update about 12 s

The full trigger list with arguments is in Fenia-Triggers.

Wait states

wait and daze are counted in pulses.

  • A connected player: both counters go down once per pulse in the input phase. While wait is above 0 the player's input is not parsed at all.
  • An NPC or link-dead player: they go down in aggr_update, also once per pulse.
  • Setting wait from Fenia assigns the value directly. It does not take the maximum of the old and new value.

See Mechanics-Combat for what the numbers do to a fight.

Pulse overflow

If everything scheduled for a pulse takes more than 250 ms, pulseEnd logs pulse overflow N msec as an error and returns at once without serving sockets. The next pulse starts late, so game time drifts behind wall time. A game hour is then slightly longer than a minute. The Fenia database sync and socket service are the first to be starved on an overrun, because they run last and have no minimum budget.

Typical causes, all visible as ProfilerBlock lines in the log (a block logs only when it exceeds its threshold, 100 ms for most blocks and 200 ms for update_handler):

  • sweeps that walk every character, every object or every room in the world: mobile_update, water_float_update, diving_update, obj_update, char_update_prog
  • synchronous disk writes inside the pulse: room_saving, and char_update_autosave, which saves all players in one pulse
  • an expensive Fenia handler behind a trigger that fires for everyone
  • a bulk reset

A wrapper per sweep counts calls and time and dumps cumulative statistics every 10 minutes. Read those before guessing which sweep is slow. When you add periodic work, prefer a registry of the few entities that need it over a new walk of the whole world, and give the walk an offset pulse of its own.

See also

Clone this wiki locally