Game loop & time¶
The server has no global tick counter and no loop running at a fixed interval. All game time is managed by a single component, the Scheduler, a queue of dated events. This page explains how time is measured, how the -f (frequency) parameter affects speed, and why this design avoids any busy-waiting.
The Scheduler: a heap of dated events¶
The Scheduler is a min-heap (std::priority_queue ordered by ascending deadline) of events. Each event pairs an absolute deadline (steady_clock) with a callback.
// scheduler/Event.hpp:16-21
struct Event {
std::chrono::steady_clock::time_point deadline;
std::function<void()> callback;
bool operator>(const Event& o) const { return deadline > o.deadline; }
};
// scheduler/Scheduler.hpp:32-35 (excerpt)
int _freq;
std::priority_queue<Event, std::vector<Event>, std::greater<Event>> _queue;
Using std::greater makes the top of the queue the event closest in time — exactly what we want to consult continuously.
Scheduling an action: schedule(cb, delayTicks)¶
A deferred action (the completion of a command after its delay, death by starvation, resource respawn) is registered via schedule. The delay is expressed in time units (ticks); the Scheduler converts them into milliseconds according to the frequency.
// scheduler/Scheduler.cpp:9-18
void Scheduler::schedule(std::function<void()> cb, int delayTicks)
{
Event event;
event.deadline = std::chrono::steady_clock::now()
+ std::chrono::milliseconds((delayTicks * 1000) / _freq);
event.callback = cb;
_queue.push(event);
}
The conversion is (delayTicks * 1000) / freq milliseconds. In other words:
$$ \text{1 time unit} = \frac{1000}{freq}\ \text{ms} $$
Effect of the frequency
With -f 100 (default value), one time unit lasts 1000 / 100 = 10 ms. A Forward command (delay 7) therefore completes after 70 ms.
With -f 1000, a unit lasts only 1 ms, and the same Forward completes after 7 ms. The higher -f, the faster the game: the frequency is a game-time acceleration factor. For the list of per-command delays, see Commands & delays.
Running due actions: tick()¶
On each poll() wake-up, the main loop calls Scheduler::tick(), which pops and runs all events whose deadline has passed:
// scheduler/Scheduler.cpp:20-30
void Scheduler::tick()
{
auto now = std::chrono::steady_clock::now();
while (!_queue.empty() && _queue.top().deadline <= now) {
Event event = _queue.top();
_queue.pop();
event.callback();
}
}
Computing the poll() timeout: nextDeadlineMs()¶
This is the heart of the design. Rather than polling in a loop, the server asks the Scheduler how long it can sleep before something must happen, and passes that value as the poll() timeout.
// scheduler/Scheduler.cpp:32-44
int Scheduler::nextDeadlineMs() const
{
auto now = std::chrono::steady_clock::now();
if (_queue.empty())
return -1; // no event → poll() blocks indefinitely
auto diff = std::chrono::duration_cast<std::chrono::milliseconds>(
_queue.top().deadline - now
).count();
return std::max(0, static_cast<int>(diff));
}
- Empty queue →
-1:poll()blocks indefinitely until network activity (the server truly sleeps, 0% CPU). - Otherwise → the number of milliseconds until the next event (never negative).
A full iteration¶
sequenceDiagram
participant Loop as Main loop
participant Sched as Scheduler
participant Poll as poll()
participant Disp as AICommandDispatcher
Loop->>Loop: updatePollEvents()
Loop->>Sched: nextDeadlineMs()
Sched-->>Loop: timeout (ms)
Loop->>Poll: poll(_pollFds, timeout)
Poll-->>Loop: ready fds (or timeout)
Loop->>Loop: readClient / writeClient / accept
Loop->>Sched: tick() (run due events)
Note over Sched: completed commands,<br/>hunger, respawn…
Loop->>Disp: for each idle AI, popNextCommand()
Loop->>Sched: schedule(response, cmd->getDelay())
Note over Loop: back to top of loop
Concretely, the loop (Server.cpp:111-...):
- recomputes the
pollevents (POLLINalways,POLLOUTon demand); - fetches the timeout via
nextDeadlineMs()and callspoll(); - handles the ready descriptors (accept, read, write);
- calls
scheduler->tick()to run the actions that have reached their deadline; - for each idle AI client (neither busy nor incanting), pops one command and schedules its completion after
cmd->getDelay()ticks viaschedule.
// core/Server.cpp:114-115 and 161
int timeout = _scheduler->nextDeadlineMs();
int pollResult = poll(_pollFds.data(), _pollFds.size(), timeout);
...
_scheduler->tick();
Why this design avoids busy-waiting¶
No CPU wasted
A naive game loop would poll the clock continuously (while (true) { if (now >= deadline) … }), burning CPU for nothing. Here, the server sleeps inside poll() exactly until the next deadline — or indefinitely if there is none. It wakes up for only two reasons: network activity, or a game action to run. The result is a server that is both frugal (0% CPU at rest) and accurate to the millisecond.
Note the absence of a global tick counter: the game's "time" exists only as absolute deadlines stored in the heap. It is purely deadline-driven, not clocked.
Changing the frequency on the fly: setFrequency¶
The graphical client can change the game speed mid-game via the sst command (see Protocol). The server then applies Scheduler::setFrequency, which rescales all in-flight deadlines to preserve the remaining duration expressed in ticks:
// scheduler/Scheduler.cpp:46-79 (excerpt)
void Scheduler::setFrequency(int freq)
{
...
while (!_queue.empty()) {
Event event = _queue.top();
_queue.pop();
// remaining time converted to ticks at the old frequency...
int remainingTicks = static_cast<int>((remainingMs * _freq) / 1000);
// ...then re-expressed in ms at the new frequency
event.deadline = now +
std::chrono::milliseconds((remainingTicks * 1000) / freq);
newQueue.push(event);
}
_freq = freq;
_queue = newQueue;
}
Game consistency
Without this rescaling, speeding up the game would only affect new actions; those already scheduled would keep their old duration in milliseconds. By reconverting ticks → ms at the new frequency, a command that had, say, "5 ticks" left to wait will indeed wait 5 × (1000/freq) ms after the change.
For the detail of the delays associated with each command, see Commands & delays.