The common layer under everything else, and the only hard dependency in the collection. It targets desktop, mobile and the browser from one codebase, and encodes the browser’s limitations rather than pretending they do not exist.
cp -r dot-core/addons/dot_core my-game/addons/
godot --headless --path . res://examples/capability_report.tscn
What is in it
DotResult / DotError |
The return type of everything fallible. Stable CODE_* values callers branch on; reading .value on a failure errors instead of handing back a default. |
DotPlatform |
Cached capability detection — threads, UDP, listening, pack mounting, storage family, unique id. Ask about capabilities, not platform names. |
DotNodeRef |
An inspector-editable description of which node: relative, absolute, group, registry, ancestor or descendant by type, or create-if-missing. |
DotConfig |
Layered configuration: exported defaults < file < environment < command line, with reflective key discovery and secret-key protection. |
DotRegistry / DotRegistryBus |
Name-based lookup, and the signals a static class cannot declare. |
DotScheduler / DotJob |
Long work in slices. Worker threads where available, a per-frame time budget where not. |
DotTransport |
Swappable multiplayer transport. DotTransportAuto picks between WebSocket and ENet, and logs why. |
DotHttp |
An HTTPRequest-based client with retries, jittered backoff, Retry-After, range-resumed downloads and connection pooling. |
DotLog / DotLogBus / DotLogSink |
Levelled, channelled logging with structured fields, rotating files and UDP forwarding. |
DotPaths |
Path sanitisation that refuses traversal rather than cleaning it, atomic writes, and web filesystem flushing. |
DotHash / DotHashJob |
SHA-256, HMAC, constant-time comparison, base64url, CSPRNG tokens. |
DotRateLimiter |
A token bucket, used on every path a client can drive. |
DotSemVer |
Version comparison, for content and engine minimums. |
DotWeb |
The browser-only calls: filesystem sync, secure-context detection, storage estimates. |
DotRandomStream / DotRandomTable / DotRandomSchedule |
Randomness a second machine can reproduce. See below. |
Where a game plugs in
- DotTransport subclass
_create_server,_create_client,_transport_name,_is_available,supports_web_clients,scheme. Availability is checked before use, so an unusable transport fails at boot with a reason rather than as a null peer three calls later.- DotJob subclass
_step(a bounded slice; return true if more remains),_progress,_setup,_teardown,is_thread_safe. Hash a chunk, not a file — the scheduler can stop calling_step, but it cannot interrupt one.- DotConfig subclass
env_prefix(),cli_prefix(),sensitive_keys(),validate(). Declare@exportproperties and the layering, the reflection and the settings screen all follow.- DotLogSink, or DotLog.add_sink(callable)
Where records go. The
Callableform is one line; the class handles files, rotation and forwarding.- DotNodeRef on every component
The collection-wide answer to “which node”. Every addon’s node exports at least one.
DotResult is not optional politeness
var res := await cloud.acquire(url)
if not res.ok:
if res.error.code == DotError.CODE_RATE_LIMITED:
retry_after(res.error.retry_after)
return
var scene_path: String = res.value
Branch on DotError.CODE_*; never on the message. res.wrap(...) adds context
without discarding the cause, so a failure three layers down still says what it
was doing at each level.
DotScheduler and the browser
DotScheduler runs jobs on worker threads where the build has them and slices
them on the main thread inside a frame budget where it does not — because a web
export has no threads unless the template was built for them, and
DotPlatform.has_threads() is the question to ask rather than
OS.get_name().
A job's `finished` signal is always delivered on the main thread
It was not, once: await resumed on a worker thread and the next line touched
the scene tree off-thread. _teardown runs on whichever thread settled the job
and is for thread-safe APIs only; anything touching the tree belongs in a
finished handler.
Reproducible randomness
dot-core’s random/ module replaces RandomNumberGenerator with named
streams whose draws are a pure function of (key, index). It shipped as a
separate addon, dot-randomness, and folded in here: every other addon that
wanted determinism had to take it, which made it a second hard dependency in a
collection that has exactly one.
Other addons still find it through
DotRegistry under
dot_random_source and call stream(name), so anything with that method can
stand in.
var world := DotRandomStream.new(seed)
var spread := world.stream(&"spread")
var loot := world.stream(&"loot")
spread.at(tick) # the same number on every machine, for ever
loot.next_range_i(1, 6) # and drawing it does not move `spread`
at() advances nothing, so a client can know what the server will do on a tick
it has not simulated, and a replay can be scrubbed backwards.
A single shared generator is mutable state every caller advances, which is three bugs in a networked game:
- Two peers that draw in a different order diverge. A server that rolls weapon spread and then a loot drop, and a client that predicts the shot and never sees the loot, are one draw apart for the rest of the session — and the visible symptom is that the shooting stops matching, which points at the netcode.
- Adding a feature changes the past. One extra draw shifts every later number, so yesterday’s replay plays back differently and a shared seed stops producing the world it was shared for.
- A stream nobody can resume. A receiving peer has to adopt an index, not allocate one.
A seed is not a secret
It is meant to be shareable, and a client can compute every number a server will. Anything that must be hidden from a player belongs behind a value the client does not have.
Tables
DotRandomTable draws ids and knows nothing about what they are, so a loot
table validates and draws on a server without the content.
WITH_REPLACEMENT |
Independent draws. The honest one, and the one that makes a player who has opened forty crates conclude the game is broken. |
WITHOUT_REPLACEMENT |
A shuffled deck, reshuffled when empty. What a rotation and a card game want; not a loot table, because a player can count it. |
PITY |
The weight climbs on a miss and resets on a hit, cutting off the long tail. |
`pity_after` is not optional
Measured by this addon’s own suite: a 2% entry whose weight starts climbing on the first miss comes out at 14.5%, because most droughts are short and the ramp was inflating all of them. Start the ramp past the stated mean interval and the ordinary case is untouched.
Even then the realised rate sits above the base — removing the tail of a
distribution raises its mean — and measure_rate() is the only honest way to
find out by how much.
Events in ticks
var s := DotRandomSchedule.new()
s.mean_interval_ticks = 64 * 30 # about every thirty seconds at 64 Hz
s.min_gap_ticks = 64 * 8 # never two in eight seconds
s.max_gap_ticks = 64 * 90 # and never a gap longer than ninety
if s.fires_at(stream, tick, last_fired_tick):
...
fires_at is pure, so it answers for tick N without having run ticks 0..N-1 —
which is what lets a client anticipate a server, a replay scrub, and a restarted
server land on the schedule it was already running. A stateful countdown can do
none of those, and its failure mode latches: a due event dropped while a cooldown
runs, and nothing offered again.
Transports
DotTransportAuto is the default and picks for you. The choice is not a
preference:
- A browser has no UDP, and
ENetMultiplayerPeeris absent from the web template — soDotTransportENetreaches ENet throughClassDB.instantiateandObject.call, never by name. A script that so much as mentions the identifier fails to compile on a web export. - One listening socket speaks one protocol. A server that wants browser
clients listens on WebSocket, and then all its clients do.
require_web_clientsdefaults totruefor that reason.
`DotWeb.is_secure_context()` is not "the page is HTTPS"
A browser treats http://localhost and http://127.0.0.1 as trustworthy
origins, so it is true on a plain HTTP page — while mixed-content blocking, the
rule that actually decides whether ws:// is allowed, exempts exactly those
origins. Upgrading on the wrong question is why nothing here had ever loaded in
a browser.
