dot-core

The shared foundation for every Dot addon — results, capability detection, node references, layered configuration, frame-budgeted jobs, swappable transports, HTTP and logging.

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 @export properties and the layering, the reflection and the settings screen all follow.

DotLogSink, or DotLog.add_sink(callable)

Where records go. The Callable form 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().

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.

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 ENetMultiplayerPeer is absent from the web template — so DotTransportENet reaches ENet through ClassDB.instantiate and Object.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_clients defaults to true for 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.