One Godot game, a hundred maps, switched under live players.
dot-map treats maps as content rather than as builds: a map is an id, a
version, a scene path and optionally a
dot-cloud pack, and one game switches between
a hundred of them under live players.
Requires dot-core. dot-cloud is optional and is never imported.
The obvious alternative — a hundred Godot projects — is a hundred export pipelines, a hundred copies of every addon, a player who downloads a whole game to try one map, and records that cannot be compared because each project has its own.
var session := DotMapSession.new()
session.world_ref = DotNodeRef.of_path(^"../World")
add_child(session)
session.load_catalogue("res://maps/catalogue.json")
await session.change_to(&"surf_beginner")
{
"format": 1,
"maps": [
{
"id": "surf_beginner", "version": "1.0.0", "name": "Surf Beginner",
"kind": "surf", "tier": 1,
"scene": "res://maps/surf_beginner/map.tscn",
"zones": "res://maps/surf_beginner/zones.json"
},
{
"id": "surf_kitsune", "version": "2.1.0", "kind": "surf", "tier": 6,
"content": "surf_pack_2024",
"scene": "res://packs/surf_pack_2024/kitsune/map.tscn"
}
]
}
The second map is delivered: its scene lives inside a pack that is fetched and mounted before the scene is loaded.
Rotation, time limit and voting
session.rotation.mode = DotMapRotation.Mode.RANDOM
session.rotation.cooldown = 5 # recently played, off the menu
session.map_seconds = 1800.0
session.time_limit.rtv_fraction = 0.6
session.map_over.connect(func(map, reason):
await session.change_to(session.rotation.choose(player_count).id))
session.advance(delta) # once per simulated tick
session.rock_the_vote(player_id, player_count)
The catalogue is deliberately not the rotation: what maps exist and what plays next are different questions, and a server that answers them with one list cannot have a map installed but out of rotation.
Where a game plugs in
- DotMapDef in a catalogue
Id, version, name, kind, tier, scene, zones, content pack, and a
metadictionary for whatever your game needs. A JSON file an operator edits.- DotMapRotation / DotMapVote
What plays next: modes, cooldowns, a pool that respects player count, nominations and a tally. Or hand the whole question to
dot-votethroughDotVoteMapSource.- DotMapTimeLimit
Seconds, warnings, extending with a cap, and the rock-the-vote fraction.
- DotMapLoader
Resolves a map to a scene, reaching
dot_cloud_clientthrough the registry rather than by name — so a project with nodot-cloudstill parses and still loads maps off its disk.- DotMapSyncHost.send_fn / DotMapSyncClient.send_fnCallable
How a map change reaches clients: announce, progress, ready, load, abort.
on_timeout_fndecides what to do about a straggler.- DotMapSyncClient.accept_unknown_maps
What a client will load on a host’s say-so. Off is the safe default.
- Signals
changing,changed,change_failed,change_finished,change_aborted,fetching,fetch_progress,fetch_failed,content_ready,sync_progress,peer_progress,peer_timed_out,map_over,time_warning,extended,rocked,opened,voted,closed.
A map change is not a game change
Changing a game replaces the module, the netcode and the client’s scene and
puts everybody through signon. Changing a map replaces the world and nothing
else, and happens every few minutes. addons/dot_map/net/ is deliberately its
own protocol rather than dot-server’s game change.
Before it existed, DotMapSession swapped the world in its own process and said
nothing to any peer — so a dedicated server changing map left every client
playing a world that no longer existed.
`ensure` and `is_mounted`, not `acquire` and `is_ready`
The loader has asked a cloud client for ensure(content_id, version) and
is_mounted(content_id, version) since the day it was written. When dot-cloud
offered a different pair, every delivered map failed with “the registered
cloud client does not speak the content interface” — and the only loader test
ran with no cloud client at all, taking the branch that falls back to the disk
and passes.
