dot-timer

Speedrun timers for bunny-hop, surf, KZ and anything against the clock — zones a mapper draws in the world, tracks, stages, styles, practice checkpoints, replays and records, counted in ticks with sub-tick crossings.

dot-timer times a run against the clock: zones drawn in the world, tracks, stages, styles, practice checkpoints, replays and records, for bunny-hop, surf, KZ or anything else that is raced.

Requires dot-core.

A time is a tick count plus two sub-tick fractions, and the whole design follows from that. A float accumulator drifts. A tick counter alone quantises every run to the server’s tickrate, so the same play is worth up to 15 ms more at 64 Hz than at 128 Hz — and on a leaderboard sorted to the millisecond, the tickrate then decides the ordering. Two servers cannot share a records table until that is gone, and a shared records table is the whole social object of this genre. Measured: 64 Hz and 128 Hz agree to under a millisecond, where counting whole ticks puts them 7.8 ms apart.

var manager := DotTimerManager.new()
manager.authoritative = true          # on the server. A client leaves this false.
manager.store = DotTimerStoreFile.at("user://records")
add_child(manager)

manager.set_styles(DotTimerStyle.defaults())
manager.adopt_engine_tick_rate()      # whatever sv_tickrate says
manager.load_zones("res://maps/surf_beginner.zones.json")
manager.add_player(&"p1", "Christian")

manager.record_accepted.connect(func(record, previous, rank):
    print("%s — rank %d" % [record.formatted_time(), rank]))

and once per simulated tick, from your movement loop:

manager.tick_player(&"p1", state.position, state.velocity,
                    state.is_grounded(), alive, state.yaw, state.pitch)

It depends on nothing but dot-core

Deliberately. DotTimerSample asks for a position, a velocity and a grounded flag — naming DotFpsState would make the addon fail to parse in a project without it. That is what lets the same timer, records and replay format serve a 3D surf map and a 2D racing course, into the same table.

Where a game plugs in

DotTimerZone.Kind

What the timer reacts to in the world. START, END, STAGE, CHECKPOINT, RESPAWN, STOP, SLAY, TELEPORT, SPAWN, SPEED_LIMIT, GRAVITY, AIR_ACCELERATE, PUSH, NO_JUMP, AUTO_HOP, EASY_BHOP, FREESTYLE, SLIDE, and CUSTOM with a payload dictionary for anything a game invents.

Numbering is deliberately compatible with the zone types the genre’s existing tools use, because zones are traded between admins as files.

DotTimerZonePainter

Drawing zones from inside the game, the way these maps have been zoned for twenty years: walk to one corner, run a command, walk to the other, run it again. It adds height above the marked corners for you, because both marks are taken at your feet and a zone with no height is one nothing ever enters.

DotTimerZoneVolume2D / DotTimerZoneVolume3D

Or draw them in the editor. flatten_for_2d() makes a box unbounded on Z, which a 2D game needs — its positions are (x, y, 0) and a box authored with a zero-thickness third axis contains nothing at all.

DotTimerStyle

A ranking weight, a minimum time and the rules for a named style. Pair it with a DotFpsStyle of the same id and the movement transform follows.

DotTimerStore subclass

put, top, best_for, rank_of, count_on, remove. File and memory ship; past a few thousand rows per board, point it at a database.

DotTimerRules

What can_record refuses, and why. Each refusal is defending against a specific exploit.

DotTimerConfig

Layered like a server config, including max_replay_seconds — a recorder with no ceiling is a player idling in a run for hours.

DotTimerHud

Clock, split against a personal best or the record, speedometer, strafe statistics. No art, no theme.

Signals

run_started, run_finished, run_stopped, stage_reached, zone_entered, zone_exited, zones_changed, player_started, player_finished, player_staged, player_stopped, stage_requested, record_accepted, record_refused, effects_changed, and effect_requested.

Replays and practice

Replays are quantised and delta-encoded to under 12 bytes a frame, with playback sampled at a time so it runs at the right speed on any monitor.

Practice mode is +cp / +tp checkpoints with the taint rules the genre expects — saving is free, restoring costs you the run. DotTimerStyle.allow_checkpoints gates whether a practised run can be ranked, not whether practice exists at all: a ranked style nobody can learn the map on is the opposite of what a records server wants.

A thin zone is the classic way to lose a run

A player crossing a finish line at 30 m/s covers 23 cm in one tick at 128 Hz and 47 cm at 64 Hz. A finish volume thinner than that is not sampled on the tick the player is inside it, and the run never ends. min_thickness() is advisory and is the first thing to check when a zone “does not work”.