mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-25 23:44:37 +01:00
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:
1 parent
4d77b859bc
commit
c996fbda13
20 files changed
+1079
-110
No files matched your search
@@ -616,9 +616,14 @@ std::vector<std::uint8_t> encode(const Packet &p, bool compact_pose, std::uint32
|
||||
w.integer(p.tps, 1);
|
||||
w.integer(p.chat_badge & 0xffffff, 3); // red, green, blue
|
||||
w.integer(p.chat_text & 0xffffff, 3);
|
||||
// Short text: its length in `width` bytes, then the text.
|
||||
const auto text = [&w](std::string_view value, unsigned width) {
|
||||
w.integer(value.size(), width);
|
||||
w.bytes.insert(w.bytes.end(), value.begin(), value.end());
|
||||
};
|
||||
w.integer(p.vote.id, 4);
|
||||
if (p.vote.id) {
|
||||
if (p.vote.label.size() > max_vote_label || p.vote.outcome > vote_cancelled) throw std::invalid_argument("Invalid server vote");
|
||||
if (!valid_server_vote(p.vote) || p.vote.outcome > vote_cancelled) throw std::invalid_argument("Invalid server vote");
|
||||
w.integer(p.vote.kind, 1);
|
||||
w.integer(p.vote.outcome, 1);
|
||||
w.integer(p.vote.yes, 2);
|
||||
@@ -629,6 +634,11 @@ std::vector<std::uint8_t> encode(const Packet &p, bool compact_pose, std::uint32
|
||||
w.integer(p.vote.target, 8);
|
||||
w.integer(p.vote.label.size(), 1);
|
||||
w.bytes.insert(w.bytes.end(), p.vote.label.begin(), p.vote.label.end());
|
||||
w.integer(p.vote.answers.size(), 1);
|
||||
for (std::size_t i = 0; i < p.vote.answers.size(); ++i) {
|
||||
text(p.vote.answers[i], 1);
|
||||
w.integer(p.vote.counts[i], 2);
|
||||
}
|
||||
}
|
||||
w.integer(static_cast<std::uint8_t>(p.object_placement), 1);
|
||||
if (!valid_object_limit(p.object_limit)) throw std::invalid_argument("Invalid object limit");
|
||||
@@ -652,6 +662,23 @@ std::vector<std::uint8_t> encode(const Packet &p, bool compact_pose, std::uint32
|
||||
if (p.server_votes > 7) throw std::invalid_argument("Invalid server votes");
|
||||
w.integer((p.guest_noclip ? 1U : 0U) | (p.guest_no_bail ? 2U : 0U) | (p.guest_boosts ? 4U : 0U) |
|
||||
(static_cast<unsigned>(p.server_votes) << 3) | (p.enforce_tuning ? 64U : 0U), 1);
|
||||
if (p.server_polls > static_cast<std::uint8_t>(ServerPolls::everyone) || p.server_custom_votes.size() > server_custom_vote_limit ||
|
||||
!std::all_of(p.server_custom_votes.begin(), p.server_custom_votes.end(), [](const auto &v) { return valid_server_custom_vote(v); }))
|
||||
throw std::invalid_argument("Invalid server votes");
|
||||
w.integer(p.server_polls, 1);
|
||||
w.integer(p.server_custom_votes.size(), 1);
|
||||
for (const auto &vote : p.server_custom_votes) {
|
||||
text(vote.name, 1);
|
||||
text(vote.description, 1);
|
||||
w.integer(vote.choices.size(), 1);
|
||||
for (const auto &choice : vote.choices) text(choice, 1);
|
||||
}
|
||||
w.integer(p.announcement.id, 4);
|
||||
if (p.announcement.id) {
|
||||
if (!valid_chat_text(p.announcement.text)) throw std::invalid_argument("Invalid server announcement");
|
||||
w.integer(p.announcement.seconds, 2);
|
||||
text(p.announcement.text, 1);
|
||||
}
|
||||
}
|
||||
if (p.kind == PacketKind::routes) {
|
||||
w.integer(p.members.size(), 1);
|
||||
@@ -686,6 +713,14 @@ static bool read_map_label(Reader &r, std::span<const std::uint8_t> bytes, Packe
|
||||
r.at += static_cast<std::size_t>(length);
|
||||
return valid_map_label(p.map_label);
|
||||
}
|
||||
// Short text written by encode's `text`: nothing when it is longer than `limit` or the packet.
|
||||
static std::optional<std::string> read_text(Reader &r, std::span<const std::uint8_t> bytes, unsigned width, std::size_t limit) {
|
||||
const auto length = r.integer(width);
|
||||
if (length > limit || length > bytes.size() - r.at) return {};
|
||||
std::string value(reinterpret_cast<const char *>(bytes.data() + r.at), static_cast<std::size_t>(length));
|
||||
r.at += static_cast<std::size_t>(length);
|
||||
return value;
|
||||
}
|
||||
std::optional<Packet> decode(std::span<const std::uint8_t> bytes) noexcept {
|
||||
try {
|
||||
if (bytes.size() < header_size || bytes.size() > max_packet)
|
||||
@@ -968,6 +1003,15 @@ std::optional<Packet> decode(std::span<const std::uint8_t> bytes) noexcept {
|
||||
if (p.vote.outcome > vote_cancelled || length > max_vote_label || length > bytes.size() - r.at) return {};
|
||||
p.vote.label.assign(reinterpret_cast<const char *>(bytes.data() + r.at), static_cast<std::size_t>(length));
|
||||
r.at += static_cast<std::size_t>(length);
|
||||
const auto answers = r.integer(1);
|
||||
if (answers > max_vote_answers) return {};
|
||||
for (std::uint64_t i = 0; i < answers; ++i) {
|
||||
auto answer = read_text(r, bytes, 1, max_vote_answer);
|
||||
if (!answer) return {};
|
||||
p.vote.answers.push_back(std::move(*answer));
|
||||
p.vote.counts.push_back(static_cast<std::uint16_t>(r.integer(2)));
|
||||
}
|
||||
if (!valid_server_vote(p.vote)) return {};
|
||||
}
|
||||
const auto placement = r.integer(1);
|
||||
if (!valid_object_placement(placement)) return {};
|
||||
@@ -1006,6 +1050,33 @@ std::optional<Packet> decode(std::span<const std::uint8_t> bytes) noexcept {
|
||||
p.guest_boosts = (tools & 4) != 0;
|
||||
p.server_votes = static_cast<std::uint8_t>((tools >> 3) & 7);
|
||||
p.enforce_tuning = (tools & 64) != 0;
|
||||
const auto polls = r.integer(1), custom = r.integer(1);
|
||||
if (polls > static_cast<std::uint8_t>(ServerPolls::everyone) || custom > server_custom_vote_limit) return {};
|
||||
p.server_polls = static_cast<std::uint8_t>(polls);
|
||||
for (std::uint64_t i = 0; i < custom; ++i) {
|
||||
ServerCustomVote vote;
|
||||
auto name = read_text(r, bytes, 1, server_vote_name_bytes);
|
||||
auto description = name ? read_text(r, bytes, 1, server_vote_description_bytes) : std::nullopt;
|
||||
if (!description) return {};
|
||||
vote.name = std::move(*name);
|
||||
vote.description = std::move(*description);
|
||||
const auto choices = r.integer(1);
|
||||
if (choices > server_vote_max_choices) return {};
|
||||
for (std::uint64_t c = 0; c < choices; ++c) {
|
||||
auto choice = read_text(r, bytes, 1, server_vote_name_bytes);
|
||||
if (!choice) return {};
|
||||
vote.choices.push_back(std::move(*choice));
|
||||
}
|
||||
if (!valid_server_custom_vote(vote)) return {};
|
||||
p.server_custom_votes.push_back(std::move(vote));
|
||||
}
|
||||
p.announcement.id = static_cast<std::uint32_t>(r.integer(4));
|
||||
if (p.announcement.id) {
|
||||
p.announcement.seconds = static_cast<std::uint16_t>(r.integer(2));
|
||||
auto line = read_text(r, bytes, 1, multiplayer_chat_max_bytes);
|
||||
if (!line || !valid_chat_text(*line)) return {};
|
||||
p.announcement.text = std::move(*line);
|
||||
}
|
||||
if (!valid_roster(p.members, p.capacity) || !p.distances.valid())
|
||||
return {};
|
||||
} else if (p.kind == PacketKind::routes) {
|
||||
@@ -1085,6 +1156,28 @@ bool valid_chat_text(std::string_view text) noexcept {
|
||||
}
|
||||
return visible;
|
||||
}
|
||||
bool valid_server_vote_name(std::string_view name) noexcept {
|
||||
return !name.empty() && name.size() <= server_vote_name_bytes && std::all_of(name.begin(), name.end(), [](char c) {
|
||||
return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '_';
|
||||
});
|
||||
}
|
||||
bool valid_server_custom_vote(const ServerCustomVote &vote) noexcept {
|
||||
if (!valid_server_vote_name(vote.name) || vote.choices.size() > server_vote_max_choices) return false;
|
||||
if (!vote.description.empty() &&
|
||||
(vote.description.size() > server_vote_description_bytes || !valid_chat_text(vote.description)))
|
||||
return false;
|
||||
return std::all_of(vote.choices.begin(), vote.choices.end(), [](const std::string &c) { return valid_server_vote_name(c); });
|
||||
}
|
||||
bool valid_server_vote(const ServerVote &vote) noexcept {
|
||||
if (vote.label.size() > max_vote_label || vote.answers.size() > max_vote_answers || vote.answers.size() != vote.counts.size())
|
||||
return false;
|
||||
// A poll has two answers at least; nothing else has any.
|
||||
if ((vote.kind == server_vote_poll) != !vote.answers.empty() || (vote.kind == server_vote_poll && vote.answers.size() < 2))
|
||||
return false;
|
||||
return std::all_of(vote.answers.begin(), vote.answers.end(), [](const std::string &answer) {
|
||||
return answer.size() <= max_vote_answer && valid_chat_text(answer);
|
||||
});
|
||||
}
|
||||
bool valid_member_name(std::string_view text) noexcept {
|
||||
if (text.size() > max_member_name) return false;
|
||||
for (std::size_t at = 0; at < text.size();) {
|
||||
|
||||
Reference in new issue
Block a user