What it is. The common layer under everything else, and the only hard dependency in the collection: a result type for operations that can fail, logging, configuration that layers file, environment and command line, transports, HTTP, paths and hashing.
When to use it. Always: copy it first, whatever else you take. You can also use it alone, as a set of small tools for a Godot project of any kind.
Smallest use. Return a DotResult instead of null, and branch on its code rather than its message:
func load_save(path: String) -> DotResult:
if not FileAccess.file_exists(path):
return DotResult.fail(DotError.CODE_IO, "no save at %s" % path)
return DotResult.success(FileAccess.get_file_as_string(path))
var res := load_save("user://save.json")
if res.ok:
print(res.value)
elif res.error.code == DotError.CODE_IO:
start_new_game()
In more detail: 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. DotTransportDual serves browsers over WebSocket and native clients over ENet in one server; DotTransportAuto picks one of the two for a client, 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. dot-log is the sink layer for everything beyond a file. |
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. |
DotContent |
The one place the family asks for downloadable content. See below. |
DotAddonApi |
Each addon’s API level, and a pack’s requires.json checked against it. See below. |
DotRenderWarmup |
Draws a scene’s meshes and materials once, out of sight, so their shaders compile under a loading screen rather than on the first shot. It removes most first-use stalls, not all (a light that appears later adds variants nobody drew). Finishes after timeout_sec even when nothing can be drawn (headless). |
DotCollisionAudit |
audit(root) lists the visible geometry under a node that nothing collides with: the level pieces a player walks straight through. |
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.
CODE_CONFLICT means two different things claim one identity — a pack wanting
a uid another pack already holds, say. It is neither CODE_STATE (“not
downloaded yet”) nor CODE_INTEGRITY (bytes that disagree with a hash), and it
is not retryable: retrying produces the same conflict, so the caller has to
change what it asks for.
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.
Downloadable content: DotContent
Every addon that lets a developer deliver something — an avatar part, a prop,
an NPC, a vehicle, an item, a map — declares a content_id. DotContent is
the one shared caller that fetches it: client(), available(), ensure(),
ensure_all() and resolve(), duck-typed through DotRegistry, so no addon
gains a dependency on dot-cloud and a build
that ships all its content still works.
Falling back is the designed behaviour, which is why nobody noticed
Before this, only dot-map ever fetched its content. The other addons declared
the id, put it on the wire and resolved it against content nothing had
downloaded — so they fell back forever, and falling back is exactly what
content that has not arrived is supposed to do. Nothing errored, nothing
warned, and no suite could see it.
Addon API levels: DotAddonApi
A delivered pack’s scripts compile against the host’s addons. A pack built
against a newer dot-net than the client or server has does not fail with a
sentence — it fails to parse, mid-load, with an identifier “not declared in the
current scope”. DotAddonApi turns that into “This game needs dot-net API
level 3 or newer; this server has level 2.”, asked before a single script in
the pack loads (dot-cloud asks between download and mount).
- Every addon may carry
addons/<addon>/<addon>_api.gdwithconst LEVELandconst OLDEST. BumpLEVELwhen you add anything a game could call; raiseOLDESTto it when you remove or change something, because every pack built before no longer compiles. An addon with no api file is level 1. - A pack states what it needs in
requires.jsonat its root —{"format": 1, "addons": {"dot_net": 3}}— and nobody writes it by hand:addons/dot_core/tools/dot_requires.gdderives it from the game’s own files at the levels of the checkout that built it, and dot-ci writes it into the pack. A game that commits its own has it audited instead.
An API level, not the release version
A developer checkout’s plugin.cfg says 0.1.0 whatever it holds, an export does
not carry plugin.cfg at all, and a tag moves for fixes that change no API — so
“needs 0.4.1” would refuse a 0.4.0 host that runs the pack perfectly. A level
committed in the addon’s own source is the same number everywhere, because it
is code.
Transports
- 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, so a server for both audiences listens twice.
DotTransportDualputs WebSocket on TCP and ENet on UDP (the same port number by default) behind oneMultiplayerPeer, so browsers and native clients play in the same match and nothing above the transport knows which a peer came in on. It is dot-server’s default. If this build has no ENet, or the UDP port is taken, it serves WebSocket only and logs a warning rather than failing to start. - A TLS hello at a plain WebSocket listener is closed at once. A native client tries
wss://first at a publichost:port; without a screen, every wrong guess against a server with no certificate cost the client its TLS timeout.DotTcpScreenreads the first byte, closes anything that is not an HTTP request, and relays the rest to the WebSocket server on loopback. It is skipped for a listener bound to loopback (a TLS terminator’s backend) and costs a little script time per busy link. - The UDP port can be shared with the query protocols. A2S and DQP answer on the game port too, and ENet cannot share a socket with them, so
DotUdpDemuxowns the port: a query goes to its handler and everything else is relayed to ENet on loopback, with the client’s real address kept for bans and rate limits. - A client uses one transport.
DotTransportAutopicks WebSocket in a browser and whenrequire_web_clientsis set (the default, for projects that have not decided); dot-server’s client link tries ENet first on a native build and falls back to WebSocket.
`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.
