Updated

dot-cloud

Runtime content delivery for Godot — signed manifests, a content-addressed cache, resumable parallel downloads and version-namespaced mounting, in the browser as well as on desktop.

A server hands a client a manifest; the client downloads what it is missing, verifies it, and mounts it — on Windows, macOS, Linux, Android, iOS and in the browser.

Requires dot-core.

var cloud := DotCloudClient.new()
add_child(cloud)

cloud.config = DotCloudConfig.new()
cloud.config.trusted_keys = {"default": PUBLIC_KEY_PEM}
await cloud.start()

cloud.phase_changed.connect(func(_p, text): status_label.text = text)
cloud.progress_changed.connect(func(p): bar.value = p.fraction * 100.0)

var res := await cloud.acquire("https://cdn.example.com/dm_arena/manifest.json")
if res.ok:
    get_tree().change_scene_to_file(res.value)

Publishing is a headless CLI that produces a static directory — a manifest.json plus an objects/ tree — that any web server or CDN hosts with no configuration:

godot --headless --path . --script addons/dot_cloud/publish/dot_cloud_cli.gd -- \
    keygen --private keys/content.key --public keys/content.pub

godot --headless --path . --script addons/dot_cloud/publish/dot_cloud_cli.gd -- \
    publish --source content/dm_arena --out dist/dm_arena \
    --id dm_arena --version 1.2.0 --entry arena.tscn --key keys/content.key

An imported asset’s source file (the .glb or .png behind its imported output) is left out of a pack by default, since the pack already ships the output; --with-sources puts it back. A file that is not under the source, such as one a build step derives, goes in through DotCloudPublisher.extra_files (pack path to file) rather than being written into someone’s checkout.

The two constraints that shaped everything

Godot cannot unmount a resource pack. Not slowly, not with a workaround — the API does not exist. So every content set gets its own versioned path, res://<root>/<content_id>/<version>/, and nothing ever needs replacing. “Unloading” means dropping references, not reclaiming the file table. That one fact is why swapping games at run time works at all.

Unsigned content is remote code execution. A Godot pack can contain scripts, so a manifest decides what gets executed. require_signed_manifests defaults to on and the config refuses to validate without a trusted key. You can turn it off for a LAN or a first-party server; it warns every time.

What it does

  • Content-addressed cache. Files are stored under their SHA-256, so two games sharing an asset download it once and republishing re-downloads only what changed.
  • Resumable parallel downloads. Mirror failover, HTTP range resume, a circuit breaker per source, bandwidth throttling, honest progress and ETA.
  • Three sources. HTTP/CDN, a local directory (development, LAN, bundled content), and in-band over the game connection for a server with no web host.
  • Quota awareness. Browser storage estimates, mobile ceilings, and least-recently-used eviction that never evicts content currently in use.

What mounting checks

Between download and mount, a pack is held to four rules that each came from a failure somebody measured:

  • Its requires.json is read first. A pack’s scripts compile against the host’s addons, so one needing a newer addon used to fail to parse mid-load. Now it is refused before a script loads, with “This game needs dot-net API level 3 or newer; this build has level 2.” — see addon API levels. host_role names the side.
  • A content id may name its owner: <owner>/<name>. / cannot occur in either half, so a key scoped alice/* is exact — a flattened alice_bob could not tell alice + bob_x from alice_bob + x. Each segment must be a slug; a bare id, which every first-party pack is, still validates.
  • One id cannot mean two things. A second manifest for the same content_id@version from a different publisher is refused with CODE_CONFLICT rather than handed the first one’s mount — a pack can contain scripts, and “the first one’s” is somebody else’s code. An equal manifest re-read off the wire is still a cheap no-op.
  • A mounted pack registers its own uids, and may not take anybody else’s. An imported asset (a .glb, a .png) refers to its siblings by UID, which does not survive being mounted elsewhere; the mounter points each UID at the mounted copy. A UID the host already holds is refused and reported, or every load("uid://…") in the build could be re-pointed at downloaded content.

Provides, session tags and the Workshop

A pack can say what it contributes. A provides.json at the root of its source (kinds such as maps, weapons, props, NPCs, sounds and scripts, each with entries, and optionally the games it is for) is validated by the publisher and signed into the manifest as provides. When such a pack is mounted, the client registers its entries in DotContentIndex (in dot-core), which is how a map menu or spawn menu finds delivered content without ever naming a pack path. A game pack declares an accepts block listing the kinds it takes; a game with none takes no subscriptions.

The publisher also sets runs_code in provides, computed by DotCloudScriptScan from the files rather than taken from the author. Treat it as a label for honest packs, not a sandbox.

Every pack in the index carries the sessions it was mounted for, solo or online, and only entries of the current session are visible. DotCloudClient.session_tag (default online) tags what ensure() and acquire() mount, and ensure_for_session(content_id, version, session, source, options) passes the tag and the mount options explicitly. A subscription mount uses uid_policy = "add_only", so a downloaded pack can never re-point a UID the game already holds, and an addon overlay is refused from a subscription as CODE_FORBIDDEN.

DotCloudWorkshop (registry name dot_workshop) is the solo-play sync client: it asks the site for the packs the player subscribed to, downloads and mounts them for a solo launch, and never runs for an online session. If a pack that runs code was mounted, joining an online server restarts the process first, because resident scripts cannot be unloaded. Its DotCloudWorkshopConfig sets, among others, workshop_script_packs (never, approved or any; default approved), workshop_metered (ask, allow, never) and workshop_key_ids, the signing keys a subscription must carry, which are deliberately separate from the keys trusted for a server’s content. The host supplies token_fn for the signed-in player’s bearer and ask_fn for the “download N MB?” prompt.

Where a game plugs in

DotCloudSource subclass

source_name(), is_supported(), can_serve(manifest), fetch(...). Three ship — DotCloudSourceHttp, DotCloudSourceLocal, DotCloudSourceNetchan — and a fourth is yours: a torrent, a platform-specific CDN, a signed S3 URL minter.

A fetch must write to DotCloudStore.partial_path for the file’s hash, resume from partial_size where it can, and finish through commit_partial. That is the only place the hash is verified, and writing into the object directory directly is exactly how unverified content gets mounted.

can_serve() vs is_supported()

The distinction earns its keep. A source can be perfectly healthy and still have no way to reach this manifest — an HTTP source with no base_urls against a manifest carrying no mirrors. Returning a failure from can_serve means “skip me for this manifest, and say why”, and is not recorded against the circuit breaker.

priorityint

On every source. Lower runs first; the in-band fallback deliberately sits last.

DotCloudConfig

trusted_keys, require_signed_manifests, cache_bytes, mobile_cache_bytes, web_quota_fraction, parallel_downloads, max_attempts_per_file, throttle_bytes_per_sec, file_timeout_sec, cache_dir, verify_before_mount, verify_cache_on_start, resume_downloads, enforce_mount_prefix. Layered like every DotConfig.

manifest_url_template / manifest_url_fallbacksDotCloudClient

Where a manifest is asked for, from a base URL, an id and a version. The default is {base}/{id}/{version}/manifest.json, which is what the site publishes; manifest_url_fallbacks lists other shapes tried after it, per base, because one origin can hold packs published under two layouts and re-publishing all of them is not a precondition for starting.

Signals

On DotCloudClient: phase_changed, progress_changed, content_ready, content_released, failed. Per-file detail is on the downloader: file_finished, sync_finished.

The content interface other addons expect

Register a DotCloudClient and it publishes itself under dot_cloud_client. dot-map’s loader and dot-server’s game change both reach it through that name and call:

ensure(content_id: StringName, version: String = "", groups := PackedStringArray(), manifest_url := "") -> DotResult
is_mounted(content_id: StringName, version: String = "") -> bool

Most addons reach them through DotContent in dot-core, the one shared caller — which is how avatar parts, props, NPCs, vehicles and items that name content now actually fetch it.