Repository navigation
Engine Time and Pulses
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
| 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.
DreamLand::run repeats three steps until shutdown:
-
scheduler->tick()runs every scheduled task, ordered by priority. -
pluginManager->checkReloadRequest()applies a pending plugin reload. Reloads happen between ticks, never inside one. -
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.
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
onSpecand behaviorspecapply to NPCs only. For a player,mobile_updatefires the global triggeronSpec(throughgprog) after the affect handlers. - Behavior
onSpecandpostSpecon 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-indexonSpecscripts are not gated and always run. - Auction: a running auction advances one step ("going once", "going twice", "sold") every 45 seconds.
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_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.
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.
| 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 and daze are counted in pulses.
- A connected player: both counters go down once per pulse in the input phase. While
waitis 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
waitfrom 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.
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, andchar_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.
Start here
Get running
Contribute
- Contribute
- Contributing Workflow
- Code Style
- Accessibility
- Writing Player Text
- Game Design Principles
Build areas
- Building
- OLC
- OLC Rooms
- OLC Mobiles
- OLC Objects
- OLC Resets
- OLC Line Editor
- OLC Other Editors
- Area Format
- Area Quests
- Mob Forms and Parts
Fenia
- Fenia
- Fenia Language
- Fenia Triggers
- Fenia Gotchas
- Fenia API
- Fenia Scripts
- Fenia Behaviors
- Fenia Skills and Spells
- Fenia Cookbook
- Fenia Grammar
Engine
- Engine Architecture
- Engine Time and Pulses
- Output Format
- Mechanics Combat
- Mechanics World
- Data Configs
- Item Power
Localization
Clients
More