A game in this family is a DotModule the server loads. Before this addon,
every one of them contained the same two hundred lines.
Requires dot-core, dot-server and
dot-net — all three, and it names all three
freely.
The sequence, written once
Find the game object, build a DotNetManager with the same four non-obvious
settings, construct a bridge, attach it, open its link under the server node,
seal the message schema, build an identity layer and await it, load the
platform module by path, build a services layer and hand it the link, keep
track of who is actually playing, drive one authoritative tick, and take it all
down in the reverse order.
The parts that genuinely differed between five games were the class names and a
handful of numbers. DotGameModule is that sequence; what is left for a game
to write is what is actually its own:
extends DotGameModule
const MyBridge := preload("net/my_bridge.gd")
func _module_name() -> String: return "mygame"
func _game_service() -> StringName: return MyGame.SERVICE
func _net_config() -> DotNetConfig:
var config := DotNetConfig.new()
config.tick_rate = game.tick_rate
config.snapshot_rate = 32
return config
func _make_bridge() -> Node: return MyBridge.new()
func _make_identity() -> Node: return MyIdentity.new()
func _make_services() -> Node: return MyServices.new()
func _game_load() -> DotResult:
add_command("mygame_status", _cmd_status, "Show the match state")
return DotResult.success(null)
What is in it
DotGameModule |
The sequence, the tick, the teardown. Subclass it. |
DotGameNetcode |
Builds a DotNetManager and attaches a game’s bridge. Usable on its own. |
DotGameRoster |
Who is in the game as opposed to who is connected, and everything that has to be told when that changes — including telling followers to forget a player who leaves. |
DotGameServices |
Chat, a relay, moderation and voice, built in the one order that works. Subclass it for a game’s channels, rules and voice range. |
The identity layer _make_identity() returns is not this addon’s: it is dot-platform’s DotPlatformIdentity, because every type it builds is dot-platform’s dependency and none is this one’s. A game returns one configured with its avatar schema, and with it on the module loads dot-platform’s own module beside it.
What uses it: every minigame (mg-buses-from-hell was the first), hide-n-seek, murder-mystery, zombie-zone and the starter game in dot-game-template subclass both DotGameModule and DotGameServices. game-playground subclasses only DotGameServices so far. arena, g2gfast and hungario are not converted yet and still carry their own copies of both.
The services layer
Five games had services layers of five to seven hundred lines each, differing in
their channels, their rules and a voice range. The relay was identical in all
five, comments included; so were the admission check, the peer fan-out, the
punishment subject — and the order, which is the part with a bug behind it.
DotGameServices builds chat, a relay, moderation and voice, and a subclass
answers only what is its own.
Moderation is built before chat, or no gag is ever enforced
dot-moderation publishes
dot_mute_source, and both the chat and the voice router look that name up
when they start. A chat router built first finds nothing, warns once, and
then enforces no gag for the life of the server. That is why the order is
written down once here rather than left to each game.
It also builds dot-moderation’s live tools — noclip, freeze, slay, blind and
the rest — and binds their commands to the server’s console, so a game gets the
community admin set by answering _mod_abilities with a table of callables.
Reasons for what it refuses, the teleport verbs, and whether a target is alive or
on a team are virtuals too. The commands are unbound when the layer leaves the
tree, because the console outlives it.
dot-chat, dot-voice and dot-moderation are not dependencies, and must not
become them: a game with no chat is a legitimate game, and one with no voice is
most of them. Each layer is loaded by path and driven through set() and
call(), so a missing addon is a layer skipped with a log line rather than a
server that will not boot. What a subclass hands back — channels, rules, a voice
config — it may name freely, because a game that configures chat has dot-chat.
Hooks, each defaulting to the old behaviour
Moving the first hand-written game onto both bases found five places the base had no question to ask:
| Hook | What it is for |
|---|---|
DotGameServices.setup(null, …) |
Offline play. Only what is bound to a server is skipped — the tool commands, the relay telling clients, the admin and audit seams. |
_can_hear(listener, speaker, listener_at, speaker_at) |
One rule for who hears whom, wired into both chat’s and voice’s can_hear_fn. Asked only of listeners already in a proximity channel’s range. |
_mod_configure_tools(tools) |
Adjust the live tools after their handlers are set, before they enter the tree or a command is bound. |
_peer_can_receive(peer) and send_backlog(peer) |
Hold the chat backlog for a client that builds its scene after it is seated — a line sent before then is a “Node not found”. |
DotGameModule._can_tick() |
Skip the tick, asked before the tick counter moves — for instance while a game change has freed the world. |
Telling the server which map you are on
A server listing — A2S, DQP and the backbone’s stats report — prints a map. Only the game knows what it calls one (a course, an arena from a rotation, a round’s layout), so the module says it: call report_map("surf_mesa") whenever your map changes, and report_map("") to clear it. A game that never calls it is listed under its own display name, which is right for a game with one map. The server forgets the map whenever a game unloads, so report it on every change, not once at load. On a server too old to take a map the call returns false and logs one line; nothing breaks.
The client half of the same extraction is DotGameClient: see Moving a client onto DotGameClient.
What is deliberately not in it
No rules. Whether a round has ended, what a team is, what the objective is, who won — those belong to dot-match and dot-objective, and they stay there. A game is free to use neither and still use this.
The test for whether something belongs here is whether every game would
write it the same way with only the class names changed. A map rotation would
not: some games have one, some do not, and the ones that do wire it
differently. So DotGameRoster will tell a rotation that somebody left — by
duck-typed follow() — and this addon does not know what a rotation is.
The four netcode settings, and why they are not defaults
DotGameNetcode fixes four things and leaves the rest to the game:
- auto_tick = false
A game’s tick happens inside the netcode’s, between applying each peer’s inputs and building the snapshot, which is what the bridge arranges. A manager ticking itself as well moves every player twice — and what that looks like is a game running at double speed only when somebody is connected.
- config_file = ""
The manager reads a JSON file by default, so a stale one in
user://sets a tick rate the game did not choose and nothing says so.- local_peer_id = 1
The authority’s id in Godot’s multiplayer, which the bridge routes by.
- messages.seal() last
Wire ids come from a sort of the registered type names, so a type registered after the seal renumbers every id above it and silently reinterprets every message.
A client whose message schema this server cannot play with is kicked.
dot-net refuses the pair but owns no socket, so DotGameNetcode connects its
peer_schema_refused to the server’s refusal, and the client is kicked with
dot-net’s sentence rather than left to fail on the first message it misreads.
Tick rate, snapshot rate and world extent have no default here, because there is no default that is right for two different games — and a game running at a rate it did not choose is the kind of wrong that nothing reports.
Everything optional is reached without naming it
A map director and a vote are duck-typed; dot-platform’s module and the services
layer’s chat, voice and moderation are loaded by path. In GDScript a script that so much as mentions a
class_name the project does not have fails to parse and takes down
everything referencing it, so an addon that named an optional dependency
would make it mandatory. See
Conventions.
