dot-auth says who you are. dot-user says what we know about you.
dot-user-avatar says what you look like. None of them knows about the others —
that is what makes them adoptable one at a time — and so nothing joined them up.
This does.
Requires dot-core and dot-server.
It requires dot-core and dot-server, and
finds the other three at run time. Any subset works, including none.
var hub := DotPlatformHub.new()
add_child(hub)
var admitted := await hub.admit(identity)
var player: DotPlatformPlayer = admitted.value
player.key() # scoped id — what everything else uses
player.display_name() # from the profile, falling back to the identity
player.avatar # resolved, conformed, entitlement-checked
On a dot-server, load the module instead and it does that per connection:
server.modules.load_module("res://addons/dot_platform/dot_platform_module.gd")
It finds the hub through DotRegistry, admits every authenticated client, puts
the result on session.data["platform"], applies the profile’s name, releases
the state when they leave, and adds platform_status and platform_name to the
console.
Admission gates the spawn on a dot-server that has the PROFILE signon stage: the module holds each authenticated player there while the hub admits them, then releases them, so the game seats somebody whose name and avatar are already resolved. A required admission (require_avatar or require_profile) keeps holding until it finishes, or refuses the player in the stage with refuse_profile; an optional one lets the player in after admission_timeout_sec and finishes behind them. The hold is taken synchronously inside dot-server’s own client_state_changed, because the server looks at its holds as soon as the handlers return. gates_spawn() says whether the server in front of the module has the stage; on an older dot-server, or for a session adopted past the stage, admission finishes after the spawn and a refusal is a kick.
Because admission can still finish late, the module declares three server events, each carrying userid and peer_id: player_admitted, once the profile and avatar are resolved (gated, it usually fires before the spawn, so a redraw handler finds nobody seated and does nothing); player_renamed, when an operator’s platform_name changes a connected player’s name (it sets the session’s display name too); and player_avatar_changed. A game hook_posts them and redraws by whatever path it already has. The client is not told about the hold: it waits in its own signing-in phase.
The identity layer: DotPlatformIdentity
Every game used to carry its own copy of the chain that builds content delivery, backbone reporting, profiles, avatars and the hub in the order they depend on each other — two of them, 262 and 215 lines, differed in one line. DotPlatformIdentity is that chain once. A game hands it an avatar schema and a stock-avatar function:
var identity := DotPlatformIdentity.new()
identity.avatar_schema = MyAvatars.schema()
identity.stock_avatar_fn = MyAvatars.stock_avatar # what a first-time player wears
identity.avatar_translate_fn = MyAvatars.from_site # optional: the site's avatar, in this game's schema
add_child(identity)
await identity.setup()
await server.modules.load_module("res://addons/dot_platform/dot_platform_module.gd")
A dot-game module returns one from _make_identity() and skips the last line — DotGameModule loads the platform module itself. Authentication is not in it: proving who somebody is happens in dot-server’s handshake.
With a dot_backbone_client registered (dot-server-deploy registers one from its listing credential), the avatar store is the site’s avatar route, read only, instead of a local file the site never sees; avatar_translate_fn maps the site’s builtin schema into the game’s, and unset, a member wears the game’s stock look.
Profiles follow the same rule. With use_backbone_profiles on (the default) and a backbone client holding an integration credential, a player the site issued a key for (the scoped_key claim) has their profile read from and saved to the site, through DotPlatformUsers (still a DotUserManager) and DotPlatformProfileStore. A guest, a local account or any key derived from the server’s own secret stays on the server’s disk, because the site never minted that key and would answer every read with 404 and fail every save. A credential the site refuses (401 or 403, for example one enrolled before it carried the profile permissions) falls back to the local store for the rest of the process, said once. How far a profile follows a player is the credential’s scope: an app-bound credential shares profiles across every server holding it, a server’s own credential keeps them on that server.
The translation it exists to do
identity.uid "backbone:acc-1" global; a server must never see this
-> profile.user_key "sWrM4ym4beWFH0Nx" scoped; what everything else keys on
-> avatar filed under the SAME scoped key
Keying the avatar on the account id instead works perfectly in testing and
quietly hands every server operator a global identifier for every player.
DotPlatformHub is the only place that translation happens, and
DotPlatformPlayer.account_uid() is named to be conspicuous at the call site.
Everything optional stays optional
| Installed | What happens |
|---|---|
| all three | profile, avatar, entitlements, onboarding flag |
dot-user only |
a profile and a scoped key; nobody has an avatar and nothing asks |
| neither | a player with a name from the identity and no scoped key |
| a broken store | the player still gets in, visible, with nothing persisted over |
A sandbox that required all three to start would be unusable on the first day of a project, which is the day people decide whether to adopt something.
Where a game plugs in
- DotPlatformModule
Load it into a
dot-serverand admission is wired. Subclass it, or callDotPlatformHub.admitdirectly, for a different join flow.- DotPlatformHub.entitlement_sourceCallable
Where “what does this player own” comes from — a purchase, a season pass, a site group. Unset, a player owns nothing, which is the loud default rather than the silent one.
- DotPlatformConfig
Timeouts, whether onboarding is required, and what an absent subsystem means.
- Signals
player_admitted,player_refused,player_released,avatar_changed.
Running the seams is what finds the bugs
seam_selftest puts all four addons through one process with real RSA keys, a
real ticket and real stores; sandbox_server boots a real DotServer, loads
the module and connects a real DotClientLink over a real socket. Doing that
found two bugs in dot-server on the first run, both of which made joining
impossible in every configuration, and neither of which was reachable from
dot-server’s own 300-check suite.
