This is the vote asset for TMC's Dot collection. It is what you add when the players, rather than the rotation, should decide what runs next.
This collection of assets provides modular building blocks for creating games and applications within the TMC ecosystem, ensuring consistency and interoperability across all dot-* assets. This includes core functionality, networking, authentication, cloud integration, and more.
These assets are COMPLETELY OPEN SOURCE. You are free to use, modify, and distribute them under the terms of the MIT license. The only thing not open source is the back-end web infrastructure. So if you opt into using your own authentication backend instead of integrating with TMC, you will need to build and integrate your own back-end infrastructure.
This asset, along with all the others, was built initially with Claude Code and will continue to be maintained and extended using it. This is because I (gamemann) cannot build the entire TMC platform alone (I wish I could lol).
Please treat this as partially tested. Every asset has its own headless test suite and those suites pass, but very little of this has been in front of real players yet. Expect rough edges, and please report anything you run into.
I intend on reviewing code, testing, and editing documentation regularly. If you're interested in helping out, please let me know!
An asset that allows users to vote for what plays next, whether games, maps or modes, and every part of it is a setting.
Rock the vote, nominations, per-choice time limits, a ballot, and the change at the end of it. The shape every server in this genre has had since 2005 (rtv, nominate, mapchooser, timeleft, extend), rebuilt so that a community that wants it to work differently changes a number rather than forking it.
var votes := DotVoteDirector.new()
votes.rules = DotVoteRules.new()
votes.source = DotVoteGameSource.of(server.games) # or a map catalogue, or your own
votes.player_count_fn = func() -> int: return players.size()
add_child(votes)
votes.begin(&"lobby") # what is running now
# once per tick
votes.advance(delta)That is the whole integration. Everything else is configuration.
| Rock the vote | A fraction of the players, with a minimum player count, a delay at the start of every map, idempotent per player, and votes withdrawn when their owner disconnects. |
| Nominations | Per-player caps, a total cap, seconding (so "most nominated" means something), admin bypass, and reserved places on the ballot so three organised players cannot decide every map. |
| Time limits | In seconds, in rounds, in score, or all three, per choice, so a forty-minute surf map and a ten-minute bhop map are not forced under one number. Multiple warnings, extending with a cap. |
| The end-of-map vote | One switch (end_vote). The ballot opens a fixed lead before the end, or at a fraction of the limit, with an optional counted-down warning. "Extend" adds a configurable amount of time, rounds and score, and leaves the ballot once the extensions are used up. |
| The ballot | Up to N options, filled five ways, with "extend", "don't change" and "no vote" as options a server can turn on or off, placed first or last, optionally shuffled, and custom maps marked. Opens a configurable lead time before the map ends, so the change happens on time. |
| Counting | Plurality, approval, instant runoff, or a majority runoff. Quorums, weighted ballots, and five tie-breaks, four of which a player watching can predict. |
| Cooldowns | In plays or in wall-clock minutes, per choice, clamped against the pool so a long cooldown on a short rotation cannot exclude everything. |
| Applying | Immediately, at the end of the round, or when the clock runs out — separately for the end-of-map ballot and for a rock-the-vote one — with a delay so players can read the result. |
| Countdowns and cues | A per-second countdown signal before a ballot and before a runoff, and sound cue ids for the start, the end, the warning and each second. Ids only: the host plays them, through dot-audio or anything else. |
| Commands | nominate, rtv, votefor, timeleft, nextmap, revote, extend, endvote, setnextmap, nominate_addmap, forcertv, votereload, and more, on a dot-server console and in chat. Every name configurable; the admin ones need the changemap flag. |
| Asking | Whether a player may nominate and why not, what may be nominated, what is excluded, what is nominated and by whom, whether the end vote has finished, whether a vote could start, whether a choice is official. |
DotVoteRules |
Every policy decision, as one DotConfig. Layers defaults < file < DOT_VOTE_* < --vote-*, and enum settings are written by name (method: instant_runoff). |
DotVoteChoice |
One thing that can be voted for: an id, a name, and the few facts a ballot needs: player counts, its own time limit, its own cooldown and its weight. |
DotVoteSource |
Where the choices come from and what "play this" means. The one seam. |
DotVoteDirector |
The Node that joins them, driven by one advance(delta) per tick. |
DotVoteClock |
The limit, the warnings, the extends and the rock-the-vote. |
DotVoteClockView |
The clock as a client shows it: a state a server sends when it changes, counted down in between, and empty when there is no limit. |
DotVoteBallot |
The open ballot and the four counting methods. |
DotVoteNominations |
What players have asked for, in order. |
DotVoteHistory |
What has been played, and what is still on cooldown. |
DotVoteResult |
What was decided, and how, including which tie-break, so the announcement can explain itself. |
Three sources ship with it, and none of them names a class outside dot-core:
DotVoteGameSource.of(server.games) # dot-server's games
DotVoteMapSource.of(catalogue, session) # dot-map's maps
DotVoteListSource.of(my_choices) # anything else, with a CallableThe two integrations are duck-typed, so this addon installs in a project that has never heard of dot-server or dot-map. Per-choice settings live in the thing's own metadata, so a game's time limit is written beside the game:
# content/arena/game.yml
metadata:
vote:
time_limit_sec: 2400
weight: 2.0A source that cannot change anything is legitimate: the director runs the whole vote and emits change_due for the host to act on, which is what a client mirroring a server's ballot does.
trigger: time_limit # round_end holds a time or score ballot until the round ends
duration_sec: 1800
end_vote: true # a vote for the next map when this one runs out
vote_lead_sec: 120 # the ballot opens two minutes before the end
vote_warning_sec: 10 # after a ten-second countdown
warn_at_sec: "300,60,30"
include_extend: true # "extend this map" is on the ballot
extend_seconds: 900 # by fifteen minutes
max_extends: 2 # at most twice
rtv_fraction: 0.6
rtv_min_players: 2
rtv_delay_sec: 120 # no rtv in the first two minutes
max_options: 6
nomination_slots: 4
fill: least_recently_played
method: instant_runoff
tie_break: nomination_order
quorum: 0.4
on_no_quorum: keep
cooldown: 5
apply: end_of_round
apply_delay_sec: 5Eighty settings, and the self-test fails if any one of them is read by nothing. docs/parity.md maps every setting, command and hook of the long-standing community map-chooser plugins onto these, row by row.
A game that builds its rules in code layers an operator's file over them in one call — its own defaults, then the running game's game.yml metadata, then the file, then DOT_VOTE_*, then --vote-*:
var rules := my_default_rules()
var layered := rules.layer_over_defaults(
"user://cfg/my_game_vote.json",
DotVoteGameSource.running_game_metadata("map_vote"),
)A result that does not validate is refused whole, and the game keeps its own defaults.
godot --headless --path . --import
find . -name '*.gd' -not -path './.godot/*' | while read f; do
godot --headless --path . --check-only --script "res://${f#./}"
done
godot --headless --path . res://examples/vote_selftest.tscn374 checks. The last two sections run this addon against a real DotGameManager and a real DotMapCatalogue and change what they are running, because "the two ends have never met" is how the expensive bugs in this family start.
dot-core, and nothing else. dot-server and dot-map are optional and are reached by duck typing.
ln -s ../../dot-core/addons/dot_core addons/dot_core
# optional, and only so the self-test can run the integrations for real
ln -s ../../dot-map/addons/dot_map addons/dot_map
ln -s ../../dot-server/addons/dot_server addons/dot_serverMIT. See LICENSE.