diff --git a/Engine/Game/Multiplayer/session_model.h b/Engine/Game/Multiplayer/session_model.h index 1bcf7a1..04ed311 100644 --- a/Engine/Game/Multiplayer/session_model.h +++ b/Engine/Game/Multiplayer/session_model.h @@ -108,6 +108,9 @@ struct MultiplayerChatLine { // the overlay can keep emote names the filter caught; empty when nothing was masked. std::string unmasked; bool server{}; // said by the dedicated server itself: its console, welcome or answers + // The colour of the line's own text when it is not the usual one (IM_COL32 layout, 0 = usual): + // a dedicated server's lines, in the colour its owner chose. + std::uint32_t text_color{}; }; // A command typed into chat with a leading "/" (shown as the player types "/"). struct MultiplayerChatCommand { diff --git a/Extension/Multiplayer/Net/protocol.cpp b/Extension/Multiplayer/Net/protocol.cpp index a5555a2..f897297 100644 --- a/Extension/Multiplayer/Net/protocol.cpp +++ b/Extension/Multiplayer/Net/protocol.cpp @@ -597,6 +597,8 @@ std::vector encode(const Packet &p, bool compact_pose, std::uint32 w.integer(p.object_clears, 4); if (!valid_multiplayer_tps(p.tps)) throw std::invalid_argument("Invalid session TPS"); w.integer(p.tps, 1); + w.integer(p.chat_badge & 0xffffff, 3); // red, green, blue + w.integer(p.chat_text & 0xffffff, 3); w.integer(static_cast(p.object_placement), 1); if (!valid_object_limit(p.object_limit)) throw std::invalid_argument("Invalid object limit"); w.integer(p.object_limit, 2); @@ -903,6 +905,8 @@ std::optional decode(std::span bytes) noexcept { p.object_clears = static_cast(r.integer(4)); p.tps = static_cast(r.integer(1)); if (!valid_multiplayer_tps(p.tps)) return {}; + p.chat_badge = 0xff000000U | static_cast(r.integer(3)); + p.chat_text = 0xff000000U | static_cast(r.integer(3)); const auto placement = r.integer(1); if (!valid_object_placement(placement)) return {}; p.object_placement = static_cast(placement); diff --git a/Extension/Multiplayer/Net/protocol.h b/Extension/Multiplayer/Net/protocol.h index c634e1f..7410fbd 100644 --- a/Extension/Multiplayer/Net/protocol.h +++ b/Extension/Multiplayer/Net/protocol.h @@ -25,7 +25,9 @@ namespace dingosdk::multiplayer { constexpr std::size_t max_skater_bones = 512, max_board_bones = 64; constexpr std::size_t max_packet = 24576; constexpr std::size_t packet_header_size = 64; -constexpr std::uint16_t protocol_version = 44; +constexpr std::uint16_t protocol_version = 45; +// A dedicated server's chat lines unless its owner says otherwise: violet (#8E5CFF) and lavender (#D9C8FF). +inline constexpr std::uint32_t default_server_chat_badge = 0xffff5c8eU, default_server_chat_text = 0xffffc8d9U; constexpr std::size_t max_throwdown_message = 4096; // Packet::tuning: the host's SkatePhysicsTuning differences (Extension/Skater/physics_tuning.h). constexpr std::size_t max_physics_tuning = 16384; @@ -176,6 +178,9 @@ struct Packet { // Bumped each time the host deletes all guest objects. Guests delete their // own session objects when it changes after their first roster. std::uint32_t object_clears{}; + // Roster: the colours of a dedicated server's own chat lines, its badge and name and the + // text after them (IM_COL32 layout; the server's owner chooses them). + std::uint32_t chat_badge = default_server_chat_badge, chat_text = default_server_chat_text; bool force_world_layers{}; WorldLayerState layers = WorldLayerState(world_layers().size()); // all "default" ParkChoices parks; // One allowlisted selection byte per shared native park slot. diff --git a/Extension/Multiplayer/Session/session.cpp b/Extension/Multiplayer/Session/session.cpp index 7e9978e..ffd344d 100644 --- a/Extension/Multiplayer/Session/session.cpp +++ b/Extension/Multiplayer/Session/session.cpp @@ -155,6 +155,8 @@ void stop(Session &s, std::string reason) { s.next_send = s.last_hello = s.last_roster = s.last_cosmetic_capture = s.last_routes = s.last_network_log = 0; s.sequence = s.roster_sequence = 0; + s.server_chat_badge = default_server_chat_badge; + s.server_chat_text = default_server_chat_text; s.pose_streams.clear(); s.pose_ack = {}; s.pose_ack_due = false; diff --git a/Extension/Multiplayer/Session/session_internal.h b/Extension/Multiplayer/Session/session_internal.h index 2f92778..ad9b297 100644 --- a/Extension/Multiplayer/Session/session_internal.h +++ b/Extension/Multiplayer/Session/session_internal.h @@ -212,6 +212,8 @@ struct Session { // and the server's voice range from it. bool server_admin{}; float roster_voice_range = default_voice_range; + // The colours of the dedicated server's own chat lines, as its roster gives them. + std::uint32_t server_chat_badge = default_server_chat_badge, server_chat_text = default_server_chat_text; // The server's ban list, as sent to us while we are one of its admins. std::vector server_bans; std::uint32_t server_ban_total{}; diff --git a/Extension/Multiplayer/Session/session_receive.cpp b/Extension/Multiplayer/Session/session_receive.cpp index 2eea1c5..ffabd9a 100644 --- a/Extension/Multiplayer/Session/session_receive.cpp +++ b/Extension/Multiplayer/Session/session_receive.cpp @@ -307,6 +307,8 @@ void apply_roster(Session &s, const Packet &p, std::uint64_t now) { } s.capacity = p.capacity; s.roster_voice_range = p.voice_range; + s.server_chat_badge = p.chat_badge; + s.server_chat_text = p.chat_text; ++s.party_revision; // anyone's party may have changed // A dedicated server knows players only by the name each sent in their hello. for (auto &peer : active_peers(s)) diff --git a/Extension/Multiplayer/Session/session_view.cpp b/Extension/Multiplayer/Session/session_view.cpp index 1eaeb0d..2b8900c 100644 --- a/Extension/Multiplayer/Session/session_view.cpp +++ b/Extension/Multiplayer/Session/session_view.cpp @@ -579,7 +579,7 @@ std::pair mark_role(IdentityList list) { std::pair player_role(Session &s, std::uint64_t sender, bool local, bool marks) { if (!sender) return {}; const bool dedicated = dedicated_host(s); - if (dedicated && sender == s.host_id) return {nametag_server, "Server"}; // the server itself + if (dedicated && sender == s.host_id) return {s.server_chat_badge, "Server"}; // the server itself const auto *peer = local ? nullptr : find_peer(s, sender); const bool vouched = local || (peer && steam_vouched(s, *peer)); // Who the backend says a player is comes before what they are in this lobby, unless they @@ -600,10 +600,11 @@ void add_chat(Session &s, std::uint64_t sender, std::string name, std::string te // The dedicated server's own lines (its chat, and its answers sent to this player alone) stand out. const bool server = dedicated_host(s) && (sender ? sender == s.host_id : name == "Server"); if (server && !sender) { - color = nametag_server; + color = s.server_chat_badge; tag = "Server"; } - s.chat.push_back({++s.chat_sequence, sender, now_us(), std::move(name), std::move(text), local, color, std::move(tag), {}, server}); + s.chat.push_back({++s.chat_sequence, sender, now_us(), std::move(name), std::move(text), local, color, std::move(tag), {}, server, + server ? s.server_chat_text : 0U}); while (s.chat.size() > multiplayer_chat_history) s.chat.pop_front(); publish_chat(s); } diff --git a/Extension/UI/Overlay/chat_overlay.cpp b/Extension/UI/Overlay/chat_overlay.cpp index 70e48f2..7b39066 100644 --- a/Extension/UI/Overlay/chat_overlay.cpp +++ b/Extension/UI/Overlay/chat_overlay.cpp @@ -92,7 +92,7 @@ ImU32 name_colour(const MultiplayerChatLine& line) { // A message's own colour: the server's lines in lavender, everyone else's white. ImU32 text_colour(const MultiplayerChatLine& line) { - return line.server ? multiplayer::nametag_server_text : theme::paper; + return line.text_color ? line.text_color : line.server ? multiplayer::nametag_server_text : theme::paper; } // "name:"; a role tag is drawn in its own box before it (role_badge.h). diff --git a/Server/README.txt b/Server/README.txt index 4feebf0..5b74e0c 100644 --- a/Server/README.txt +++ b/Server/README.txt @@ -51,6 +51,11 @@ time the server starts. name Shown in the browser: 1-64 letters, numbers, spaces and - _ / [ ] ( ). password Empty for anyone; otherwise players type it to join. welcome_message A chat line sent to each player as they join. +chat_color The colour of the server's own lines in chat: its "Server" badge + and name, as "#RRGGBB" (default "#8E5CFF", violet). +chat_text_color The colour of the text of those lines (default "#D9C8FF", + lavender). Pick one that reads on a dark background. + Console: chat-color <#badge> [<#text>]. listed false hides the server; players then need the code. max_players 1-249. port, query_port Steam game server ports (default 27015, 27016). diff --git a/Server/Test/server_config_tests.cpp b/Server/Test/server_config_tests.cpp index 2a7e331..7322324 100644 --- a/Server/Test/server_config_tests.cpp +++ b/Server/Test/server_config_tests.cpp @@ -192,6 +192,15 @@ int run() { "A config in sections did not read back as it was written"); std::filesystem::remove_all(folder / "data"); } + // The server's chat colours: violet and lavender unless set, red first as written. + { + ServerConfig colours; + check(colours.chat_color == "#8E5CFF" && parse_colour(colours.chat_color) == 0xffff5c8eU && parse_colour("d9c8ff") == 0xffffc8d9U, + "The default chat colours are not violet and lavender"); + check(!parse_colour("#12345") && !parse_colour("#12345G") && !parse_colour("") && !parse_colour("#1234567"), "A bad colour was read"); + colours.chat_text_color = "blue"; + check(config_error(colours).find("chat_text_color") != std::string::npos, "A chat colour that is not one was accepted"); + } // Bans have a file of their own; a config that still holds them has them moved there. { std::filesystem::remove_all(folder / "data"); diff --git a/Server/server_config.cpp b/Server/server_config.cpp index 6a51456..2b06693 100644 --- a/Server/server_config.cpp +++ b/Server/server_config.cpp @@ -64,6 +64,8 @@ Layout layout(const ServerConfig &c) { server.set("name", c.name); server.set("password", c.password); server.set("welcome_message", c.welcome); + server.set("chat_color", c.chat_color); + server.set("chat_text_color", c.chat_text_color); server.set("listed", c.listed); server.set("max_players", c.max_players); server.set("port", static_cast(c.port)); @@ -207,6 +209,8 @@ ServerConfig load_config(const std::filesystem::path &file, std::vector &ids) { return std::find(ids.begin(), ids.end(), id) != ids.end(); }; return (listed(config.reserved) || listed(config.admins)) && on < config.max_players + extra_slots(config); } +std::optional parse_colour(std::string_view text) noexcept { + if (!text.empty() && text.front() == '#') text.remove_prefix(1); + if (text.size() != 6) return std::nullopt; + std::uint32_t rgb{}; + for (const char c : text) { + const int digit = c >= '0' && c <= '9' ? c - '0' : c >= 'a' && c <= 'f' ? c - 'a' + 10 : c >= 'A' && c <= 'F' ? c - 'A' + 10 : -1; + if (digit < 0) return std::nullopt; + rgb = rgb << 4 | static_cast(digit); + } + // Written red first; held with red lowest and opaque. + return 0xff000000U | (rgb & 0xff) << 16 | (rgb & 0xff00) | rgb >> 16; +} std::string config_error(const ServerConfig &c) { using namespace multiplayer; + if (!parse_colour(c.chat_color)) return "chat_color must be a colour like #8E5CFF."; + if (!parse_colour(c.chat_text_color)) return "chat_text_color must be a colour like #D9C8FF."; if (!valid_server_name(c.name)) return std::string("name must be ") + server_name_rule + "."; for (const auto id : c.reserved) if (!individual_steam_id(id)) return "reserved_players_slots must be SteamID64s (17 digits starting 7656119)."; diff --git a/Server/server_config.h b/Server/server_config.h index 58d3ce1..18e370d 100644 --- a/Server/server_config.h +++ b/Server/server_config.h @@ -62,6 +62,9 @@ struct ServerConfig { std::vector reserved; std::string password; // empty: anyone may join std::string welcome; // sent to each player as they join + // The colours of the server's own lines in chat, as "#RRGGBB": its badge and name, and the + // text after them. + std::string chat_color = "#8E5CFF", chat_text_color = "#D9C8FF"; bool listed = true; // shown in the in-game server browser // A Steam game server login token (steamcommunity.com/dev/managegameservers, app 3354750). // With one the server signs in to its own account and keeps the same Steam ID every start, @@ -123,6 +126,8 @@ std::size_t extra_slots(const ServerConfig &config) noexcept; // max_players are on; the reserved players and the admins after that too, in the extra slots // (a full server of 32 shows 33/32 with one of them on). bool may_join(const ServerConfig &config, std::uint64_t id, std::size_t on) noexcept; +// "#RRGGBB" (or "RRGGBB") as a colour in the layout the protocol and the overlay use, or nothing. +std::optional parse_colour(std::string_view text) noexcept; // Why `config` cannot run, or empty. std::string config_error(const ServerConfig &config); // A scoring fingerprint as the config and console write it (16 hex digits), and read back diff --git a/Server/server_host.cpp b/Server/server_host.cpp index f708315..bc1ed0b 100644 --- a/Server/server_host.cpp +++ b/Server/server_host.cpp @@ -578,6 +578,8 @@ void Host::send_roster() { auto p = packet(PacketKind::roster, now_); p.voice_policy = voice_policy_; p.voice_range = config_.voice_range; + p.chat_badge = parse_colour(config_.chat_color).value_or(multiplayer::default_server_chat_badge); + p.chat_text = parse_colour(config_.chat_text_color).value_or(multiplayer::default_server_chat_text); p.distances = config_.distances; p.object_placement = config_.object_placement; p.object_limit = config_.object_limit; @@ -1727,6 +1729,15 @@ std::string Host::command(std::string_view line, std::uint64_t admin) { config_.welcome = argument == "off" ? std::string{} : std::string(argument); return changed(config_.welcome.empty() ? "Welcome message removed." : "Welcome message set."); } + if (name == "chat-color" || name == "chat-colour") { + // chat-color <#badge> [<#text>]: the server's own lines in chat. + const auto [badge, text] = split(argument); + if (badge.empty() || !parse_colour(badge) || (!text.empty() && !parse_colour(text))) + return "chat-color <#RRGGBB badge> [<#RRGGBB text>] (now " + config_.chat_color + " " + config_.chat_text_color + ")"; + config_.chat_color = std::string(badge); + if (!text.empty()) config_.chat_text_color = std::string(text); + return changed("The server's chat lines are " + config_.chat_color + " with " + config_.chat_text_color + " text."); + } if (name == "announce-throwdowns") { const auto value = on_off(argument); if (!value) return std::string("announce-throwdowns on|off (now ") + (config_.announce_throwdowns ? "on" : "off") + ")";