#include "server_host.h" #include "server_text.h" #include #include #include // Player votes (map, kick, time of day, and the owner's own custom votes), polls, // announcements, and the chat commands that run them. Each vote is switched on and given its // pass percentage in ReSkateServer.json ("votes"); also the map pool and rotation. Games show // the running vote or poll, and each announcement, as a card (the roster carries them). namespace dingosdk::server { namespace { constexpr std::array times{"default", "morning", "noon", "afternoon", "evening", "night", "weatherday", "weathernight"}; const char *vote_name(Host::VoteKind kind) { switch (kind) { case Host::VoteKind::map: return "Map"; case Host::VoteKind::kick: return "Kick"; case Host::VoteKind::time: return "Time of day"; case Host::VoteKind::custom: return "Custom"; case Host::VoteKind::poll: return "Poll"; case Host::VoteKind::extend: return "Extend"; } return ""; } std::uint8_t vote_bit(Host::VoteKind kind) { switch (kind) { case Host::VoteKind::map: return server_vote_map; case Host::VoteKind::kick: return server_vote_kick; case Host::VoteKind::time: return server_vote_time; case Host::VoteKind::custom: return multiplayer::server_vote_custom; case Host::VoteKind::poll: return multiplayer::server_vote_poll; case Host::VoteKind::extend: return multiplayer::server_vote_custom; // a yes or no vote, as the owner's are } return 0; } std::uint64_t microseconds(unsigned seconds) { return static_cast(seconds) * 1000000; } // `text` with each `key` replaced by `value`. std::string fill(std::string text, std::string_view key, std::string_view value) { for (auto at = text.find(key); at != std::string::npos; at = text.find(key, at + value.size())) text.replace(at, key.size(), value); return text; } std::string joined(const std::vector &items, std::string_view between) { std::string text; for (const auto &item : items) text += (text.empty() ? "" : std::string(between)) + item; return text; } // "Grom 3, San Vansterdam 1" std::string answer_count(const std::vector &answers, const std::vector &count) { std::string text; for (std::size_t i = 0; i < answers.size() && i < count.size(); ++i) text += (i ? ", " : "") + answers[i] + " " + std::to_string(count[i]); return text; } // "The vote to change the map to Grom", "The poll \"Next map?\"" std::string vote_title(Host::VoteKind kind, const std::string &label) { return kind == Host::VoteKind::poll ? "The poll \"" + label + "\"" : "The vote to " + label; } } // namespace const VoteSetting &Host::vote_setting(VoteKind kind, std::size_t custom) const { static const VoteSetting none; switch (kind) { case VoteKind::map: return config_.votes.map; case VoteKind::kick: return config_.votes.kick; case VoteKind::time: return config_.votes.time; case VoteKind::custom: return custom < config_.votes.custom.size() ? config_.votes.custom[custom].setting : none; case VoteKind::extend: return config_.votes.extend; case VoteKind::poll: break; } return none; } std::uint8_t Host::enabled_votes() const { std::uint8_t bits{}; if (config_.votes.map.enabled) bits |= server_vote_map; if (config_.votes.kick.enabled) bits |= server_vote_kick; // Time of day is a world layer choice: it only reaches players while layer sync is on. if (config_.votes.time.enabled && config_.world_layer_sync && !world_layers().empty()) bits |= server_vote_time; return bits; } multiplayer::ServerPolls Host::enabled_polls() const { using multiplayer::ServerPolls; return config_.votes.polls == "everyone" ? ServerPolls::everyone : config_.votes.polls == "admins" ? ServerPolls::admins : ServerPolls::off; } std::vector Host::custom_votes() const { std::vector list; for (const auto &v : config_.votes.custom) if (v.setting.enabled) list.push_back({v.name, v.description, v.choices}); // The server's own "extend" goes with them: games list it in their "/" menu as they do those. if (extend_vote_on() && list.size() < multiplayer::server_custom_vote_limit) list.push_back({"extend", "Give this map " + std::to_string(config_.rotation_extend) + " more minutes", {}}); return list; } bool Host::extend_vote_on() const { return config_.votes.extend.enabled && config_.map_rotation && config_.rotation_extend; } void Host::reply(Guest &guest, std::string_view text, unsigned max_lines) { // Chat lines are single lines: longer answers (admin commands) arrive a line at a time. unsigned lines{}; for (std::size_t at = 0; at <= text.size() && lines < max_lines;) { const auto end = text.find('\n', at); const auto line = text.substr(at, end == std::string_view::npos ? std::string_view::npos : end - at); if (!trim(line).empty()) { send_chat(line, &guest); ++lines; } if (end == std::string_view::npos) break; at = end + 1; } } Host::Guest *Host::match_player(std::string_view text) { // A SteamID64, or the start of one connected player's name. text = trim(text); if (text.empty()) return nullptr; if (const auto id = number(split(text).first)) { auto *guest = find(*id); return guest && guest->handshaken ? guest : nullptr; } Guest *match{}; for (auto &[id, guest] : guests_) if (guest->handshaken && lower(guest_name(*guest)).starts_with(lower(text))) { if (match) return nullptr; match = guest.get(); } return match; } void Host::chat_command(Guest &guest, std::string_view line) { using multiplayer::ServerPolls; const auto [first, rest] = split(line); const auto verb = lower(first); const auto custom = custom_votes(); const auto polls = enabled_polls(); if (verb.empty() || verb == "help" || verb == "?") { std::string text; const auto votes = enabled_votes(); if (votes & server_vote_map) text += "/vote map : start a vote to change the map\n"; if (votes & server_vote_kick) text += "/vote kick : start a vote to kick a player\n"; if (votes & server_vote_time) text += "/vote tod