mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-23 18:45:57 +02:00
Merge pull request #310 from moelrobi/fix/poll-answer-repeat
Polls: answers no longer refused as repeats, and less chat noise
This commit is contained in:
7 files changed
+79
-23
No files matched your search
@@ -7,7 +7,9 @@
|
||||
namespace dingosdk {
|
||||
// Chat flood control, the same for the sender, the host, a dedicated server and every
|
||||
// receiver: a few lines at once, then one every 1.5 s, and never the same line twice within
|
||||
// 15 s. Receivers allow one line of slack for lines that arrive bunched up.
|
||||
// 15 s. Receivers allow one line of slack for lines that arrive bunched up. An answer to the
|
||||
// running vote or poll may repeat the line before it (`answer`): the same /2 is a new answer
|
||||
// in the next poll, and the vote itself counts one answer per player. The pace still holds.
|
||||
struct ChatRate {
|
||||
static constexpr double burst = 4, interval_us = 1500000;
|
||||
static constexpr std::uint64_t repeat_us = 15000000;
|
||||
@@ -15,7 +17,7 @@ struct ChatRate {
|
||||
double tokens = burst;
|
||||
std::uint64_t refilled{}, previous_at{};
|
||||
std::string previous;
|
||||
Verdict accept(std::uint64_t now, std::string_view text, double slack = 0) {
|
||||
Verdict accept(std::uint64_t now, std::string_view text, double slack = 0, bool answer = false) {
|
||||
const double limit = burst + slack;
|
||||
if (!refilled || now < refilled) {
|
||||
tokens = limit;
|
||||
@@ -23,7 +25,7 @@ struct ChatRate {
|
||||
tokens = std::min(limit, tokens + static_cast<double>(now - refilled) / interval_us);
|
||||
}
|
||||
refilled = now;
|
||||
if (!previous.empty() && text == previous && now >= previous_at && now - previous_at < repeat_us)
|
||||
if (!answer && !previous.empty() && text == previous && now >= previous_at && now - previous_at < repeat_us)
|
||||
return Verdict::repeated;
|
||||
if (tokens < 1) return Verdict::too_fast;
|
||||
tokens -= 1;
|
||||
|
||||
@@ -217,8 +217,21 @@ std::string send_chat_command(Session &s, std::string_view typed) {
|
||||
if (!dedicated_host(s)) return "That command needs a dedicated server.";
|
||||
auto text = clean_chat_text(typed);
|
||||
if (text.size() < 2 || text.front() != '/') return "Type a command after the /.";
|
||||
// An answer to the running vote: it shows on the vote card like one given there, and may
|
||||
// repeat the line before it (the same /2 in the next poll; the server counts one per player).
|
||||
std::uint8_t answer{};
|
||||
if (s.vote.id && s.vote.outcome == vote_running) {
|
||||
const std::string_view line = text;
|
||||
if (s.vote.kind == server_vote_poll) {
|
||||
// "/2" or "/vote 2"
|
||||
const auto number = line.starts_with("/vote ") ? line.substr(6) : line.substr(1);
|
||||
if (number.size() == 1 && number[0] >= '1' && static_cast<std::size_t>(number[0] - '0') <= s.vote.answers.size())
|
||||
answer = static_cast<std::uint8_t>(number[0] - '0');
|
||||
} else if (line == "/yes" || line == "/y" || line == "/vote yes" || line == "/vote y") answer = 1;
|
||||
else if (line == "/no" || line == "/n" || line == "/vote no" || line == "/vote n") answer = 2;
|
||||
}
|
||||
const auto now = now_us();
|
||||
switch (s.local_chat_rate.accept(now, text)) {
|
||||
switch (s.local_chat_rate.accept(now, text, 0, answer != 0)) {
|
||||
case ChatRate::Verdict::repeated: return "You just sent that.";
|
||||
case ChatRate::Verdict::too_fast: return "Slow down: one message every couple of seconds.";
|
||||
case ChatRate::Verdict::accepted: break;
|
||||
@@ -226,18 +239,9 @@ std::string send_chat_command(Session &s, std::string_view typed) {
|
||||
auto message = packet(s, PacketKind::chat, now);
|
||||
message.text = std::move(text);
|
||||
if (!send_packet(s, s.host_id, message, true, false)) return "Could not reach the server.";
|
||||
if (answer) s.vote_mine = answer;
|
||||
// Party chat: the server relays it to the rest of the party, not back to us.
|
||||
const std::string_view sent = message.text;
|
||||
// An answer to the vote typed in chat shows on the vote card like one given there.
|
||||
if (s.vote.id && s.vote.outcome == vote_running) {
|
||||
if (s.vote.kind == server_vote_poll) {
|
||||
// "/2" or "/vote 2"
|
||||
const auto number = sent.starts_with("/vote ") ? sent.substr(6) : sent.substr(1);
|
||||
if (number.size() == 1 && number[0] >= '1' && static_cast<std::size_t>(number[0] - '0') <= s.vote.answers.size())
|
||||
s.vote_mine = static_cast<std::uint8_t>(number[0] - '0');
|
||||
} else if (sent == "/yes" || sent == "/y" || sent == "/vote yes" || sent == "/vote y") s.vote_mine = 1;
|
||||
else if (sent == "/no" || sent == "/n" || sent == "/vote no" || sent == "/vote n") s.vote_mine = 2;
|
||||
}
|
||||
if (sent.starts_with("/p ") && s.local_party) {
|
||||
const auto local = s.transport.status().local_id;
|
||||
add_chat(s, local, s.transport.name(local), "[Party] " + std::string(sent.substr(3)), true);
|
||||
|
||||
@@ -126,6 +126,11 @@ int main() {
|
||||
check(rate.accept(2'600'000, "line 5") == V::accepted, "one more line is allowed after 1.5 s");
|
||||
check(rate.accept(2'700'000, "line 5") == V::repeated, "the same line straight after is refused");
|
||||
check(rate.accept(30'000'000, "line 5") == V::accepted, "the same line later is allowed");
|
||||
// An answer to a vote may repeat the line before it (the same /2 in the next poll), at the same pace.
|
||||
ChatRate answers;
|
||||
for (int i = 0; i < 4; ++i) check(answers.accept(1'000'000, "/2", 0, true) == V::accepted, "an answer to a vote may repeat the line before it");
|
||||
check(answers.accept(1'000'000, "/3", 0, true) == V::too_fast, "an answer is still held to the pace");
|
||||
check(answers.accept(2'600'000, "/2") == V::repeated, "the same line, not an answer, is still refused");
|
||||
ChatRate relayed;
|
||||
for (int i = 0; i < 5; ++i)
|
||||
check(relayed.accept(1'000'000, std::to_string(i), 1) == V::accepted, "receivers allow one line of slack");
|
||||
|
||||
@@ -517,6 +517,41 @@ int run(const std::filesystem::path &fixtures) {
|
||||
run_for(1000000);
|
||||
check(said("passed (2 yes, 0 no, 2 needed of 2 votes)") == 1, "A vote nobody could change any more was not decided at once");
|
||||
}
|
||||
// Poll answers: the same /2 in two polls back to back is an answer in each (chat turns
|
||||
// repeats away, answers not), twice in one poll it is still one; a chat line said twice is
|
||||
// still turned away. The card shows the question and the result, so chat names neither; the
|
||||
// log has the result.
|
||||
{
|
||||
const auto said = [&](std::string_view text) {
|
||||
return std::count_if(a.chat.begin(), a.chat.end(), [&](const std::string &line) { return line.find(text) != std::string::npos; });
|
||||
};
|
||||
const auto logged = [&](std::string_view text) {
|
||||
return std::count_if(log.begin(), log.end(), [&](const std::string &line) { return line.find(text) != std::string::npos; });
|
||||
};
|
||||
auto &c = games.at(76561198000000003ULL);
|
||||
command("poll First? | yes | no");
|
||||
a.say("/2", now);
|
||||
c.say("/2", now);
|
||||
run_for(1000000);
|
||||
a.say("/2", now);
|
||||
run_for(1000000);
|
||||
check(said("You already answered no.") == 1, "A second answer in the same poll was not answered as one");
|
||||
command("poll end");
|
||||
run_for(1000000);
|
||||
check(logged("Poll \"First?\" ended: yes 0, no 2 (no wins).") == 1, "A poll counted an answer given twice twice");
|
||||
check(!said("First?"), "A poll's question or result was repeated in chat");
|
||||
command("poll Second? | yes | no");
|
||||
a.say("/2", now);
|
||||
run_for(1000000);
|
||||
command("poll end");
|
||||
run_for(1000000);
|
||||
check(logged("Poll \"Second?\" ended: yes 0, no 1 (no wins).") == 1, "The same answer in the next poll was turned away");
|
||||
c.say("hello again", now);
|
||||
run_for(1000000);
|
||||
c.say("hello again", now);
|
||||
run_for(1000000);
|
||||
check(said("hello again") == 1, "The same chat line twice in a row was not turned away");
|
||||
}
|
||||
// The rotation: the minutes left are announced, the vote's maps are drawn a minute ahead,
|
||||
// and the vote decides: staying adds time, a map changes to it.
|
||||
{
|
||||
|
||||
@@ -1318,8 +1318,10 @@ void Host::receive(std::uint64_t peer, std::span<const std::uint8_t> bytes, std:
|
||||
if (p.kind == PacketKind::physics_tuning || p.kind == PacketKind::physics_extras) return;
|
||||
if (p.kind == PacketKind::chat) {
|
||||
active(*link);
|
||||
// An answer to the running vote may repeat the line before it: the same /2 in the next poll.
|
||||
const bool answer = p.text.starts_with('/') && answers_vote(*link, std::string_view(p.text).substr(1));
|
||||
if (!routed_source(p, link->member, peer, true, id_) ||
|
||||
link->chat_rate.accept(now_, p.text, 1) != ChatRate::Verdict::accepted)
|
||||
link->chat_rate.accept(now_, p.text, 1, answer) != ChatRate::Verdict::accepted)
|
||||
return;
|
||||
link->last_packet = now_;
|
||||
if (!allowed_words(*link, p.text)) return;
|
||||
|
||||
@@ -472,6 +472,8 @@ class Host {
|
||||
std::string start_poll(Guest *starter, std::string_view text, std::string run = {});
|
||||
void cast_vote(Guest &, bool yes);
|
||||
void answer_poll(Guest &, std::size_t answer);
|
||||
// Whether a chat command (without its "/") answers the running vote or poll for this player.
|
||||
bool answers_vote(const Guest &, std::string_view line) const;
|
||||
std::string end_poll(Guest *by);
|
||||
void check_vote(bool expired);
|
||||
void cancel_vote(const std::string &why);
|
||||
|
||||
+14
-8
@@ -372,13 +372,9 @@ std::string Host::start_poll(Guest *starter, std::string_view text, std::string
|
||||
poll.id = vote_ids_;
|
||||
poll.ends = now_ + microseconds(config_.votes.poll_seconds);
|
||||
if (!admin) vote_cooldowns_[starter->member.id] = now_ + microseconds(config_.votes.cooldown);
|
||||
std::string choices;
|
||||
for (std::size_t i = 0; i < poll.answers.size(); ++i) choices += (i ? " " : "") + std::string("/") + std::to_string(i + 1) + " " + poll.answers[i];
|
||||
vote_ = std::move(poll);
|
||||
// The card shows the question and its answers; chat does not repeat them.
|
||||
const auto who = starter ? guest_name(*starter) : std::string("The server");
|
||||
send_chat(who + " asks: " + vote_->label + " (" + std::to_string(config_.votes.poll_seconds) +
|
||||
" s). Answer on the card at the right of your screen, or type:");
|
||||
send_chat(choices);
|
||||
log_("[poll] " + who + " started a poll: " + vote_->label + " " + joined(vote_->answers, " | "));
|
||||
check_vote(false);
|
||||
return {};
|
||||
@@ -408,6 +404,17 @@ void Host::answer_poll(Guest &guest, std::size_t answer) {
|
||||
check_vote(false);
|
||||
}
|
||||
|
||||
bool Host::answers_vote(const Guest &guest, std::string_view line) const {
|
||||
if (!vote_) return false;
|
||||
// "/2", "/yes", or the same after "/vote", as chat_command takes them.
|
||||
const auto [first, rest] = split(line);
|
||||
auto verb = lower(first);
|
||||
if (verb == "vote") verb = lower(split(rest).first);
|
||||
if (vote_->kind == VoteKind::poll)
|
||||
return verb.size() == 1 && verb[0] >= '1' && static_cast<std::size_t>(verb[0] - '0') <= vote_->answers.size();
|
||||
return guest.member.id != vote_->target && (verb == "yes" || verb == "y" || verb == "no" || verb == "n");
|
||||
}
|
||||
|
||||
// `by` nullptr: the server console, which may always end it.
|
||||
std::string Host::end_poll(Guest *by) {
|
||||
if (!vote_ || vote_->kind != VoteKind::poll) return "No poll is running.";
|
||||
@@ -496,9 +503,8 @@ void Host::check_vote(bool expired) {
|
||||
if (top && count[i] == top) leaders.push_back(done.answers[i]);
|
||||
const auto outcome = !top ? std::string("nobody answered") : leaders.size() > 1 ? "a tie: " + joined(leaders, ", ")
|
||||
: leaders.front() + " wins";
|
||||
const auto line = "Poll \"" + done.label + "\" ended: " + answer_count(done.answers, count) + " (" + outcome + ").";
|
||||
send_chat(line);
|
||||
log_("[poll] " + line);
|
||||
// The card shows the result for a while; chat does not repeat it.
|
||||
log_("[poll] Poll \"" + done.label + "\" ended: " + answer_count(done.answers, count) + " (" + outcome + ").");
|
||||
// A console poll-run: the winning answer's command, as the console. A tie or no answer runs nothing.
|
||||
if (!done.run.empty() && top && leaders.size() == 1) log_(command(fill(done.run, "{answer}", leaders.front())));
|
||||
return;
|
||||
|
||||
Reference in new issue
Block a user