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.jsonis 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_rolenames the side. - A content id may name its owner:
<owner>/<name>./cannot occur in either half, so a key scopedalice/*is exact — a flattenedalice_bobcould not tellalice+bob_xfromalice_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@versionfrom a different publisher is refused withCODE_CONFLICTrather 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 everyload("uid://…")in the build could be re-pointed at downloaded content.
Imported outputs ship under `_imported/`, never `.godot/`
A published pack carries the engine’s imported outputs, and they used to sit at
their real path, .godot/imported/. Linux reads that back perfectly; a web
export cannot, and the whole game refused to start in the browser. Outputs now
ship under _imported/, with each .import marker rewritten to name where the
output landed. Missing art in a mounted pack otherwise shows up only as
warnings — the pack verifies, mounts and loads with its textures absent.
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
fetchmust write toDotCloudStore.partial_pathfor the file’s hash, resume frompartial_sizewhere it can, and finish throughcommit_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_urlsagainst a manifest carrying no mirrors. Returning a failure fromcan_servemeans “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 everyDotConfig.- 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_fallbackslists 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.
An absent cloud client is a legitimate configuration
Every call site treats “no dot_cloud_client” as “this deployment ships its
content inside the build” — which is a legitimate choice for your own game, and is therefore
indistinguishable from a client that failed to register. TMC’s own deployment ships no content in its build at all: every game, every imported map and the lobby is a pack this addon fetches. When a delivered map or
a delivered game silently does nothing, check the registry first.
