Server votes: custom votes, polls, more vote settings and announcements

Builds on the 2.0.0 vote card. Protocol 46: a server's vote can now be a
poll, and the roster carries who may start polls, the owner's own votes
and the current announcement.

Custom votes (votes.custom): the server owner defines /vote <name> [choice]
in ReSkateServer.json. When one passes, the server runs its command as the
console; {map} is the current map and {arg} the player's choice, which must
be one of the vote's "choices", so players never put free text into a
command. They show in /help, /vote list and the "/" menu.

Polls: /poll <question> | <answer> | ... with 2 to 6 answers. A poll runs
nothing. Players answer by clicking a row on the vote card or with /1, /2...;
the card shows each answer's count, then the result for a few seconds.
votes.polls says who may start one (off, admins, everyone), poll_seconds how
long it runs, and /poll end ends it early (its starter or an admin).

More vote settings: each vote may have its own seconds, cooldown_seconds and
min_players; starter_votes_yes says whether starting a vote counts as a yes.
The "votes" admin command sets all of them.

Announcements: "announce <text>" posts a chat line and shows a card at the
top of every player's screen. The new "announcements" section holds messages
the server posts in turn every interval_minutes while players are on;
"card": false keeps them to chat. Managed with the "announcements" command.

Older configs gain the new settings with their defaults on first start.
Server/README.txt documents them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WbSRfNf5T9wX4XMMQ5kwia
This commit is contained in:
Robin MöllerandClaude Opus 5.5 committed 2026-10-09 21:01:40 +00:00
1 parent 4d77b859bc
commit c996fbda13
20 files changed
+1079 -110

No files matched your search

+6
View File
@@ -593,6 +593,11 @@ void Host::send_roster() {
p.guest_boosts = config_.boosts;
p.enforce_tuning = config_.enforce_tuning;
p.server_votes = enabled_votes();
p.server_polls = static_cast<std::uint8_t>(enabled_polls());
p.server_custom_votes = custom_votes();
p.announcement = announcement_;
if (announcement_.id)
p.announcement.seconds = static_cast<std::uint16_t>(announcement_until_ > now_ ? (announcement_until_ - now_ + 999999) / 1000000 : 0);
p.object_clears = object_clears_;
// Without the catalog (or with sync off) no layers are sent: every player keeps their own.
p.force_world_layers = config_.world_layer_sync && !world_layers().empty();
@@ -1595,6 +1600,7 @@ void Host::tick(std::uint64_t now) {
roster_dirty_ = true;
}
tick_rotation();
tick_announcements();
remove_away();
std::erase_if(vote_cooldowns_, [&](const auto &entry) { return now_ >= entry.second; });
join_backoff_.prune(now_);