Server: its chat lines in colours the owner chooses; protocol 45

Server: its chat lines in colours the owner chooses; protocol 45

A dedicated server's own lines in chat (its welcome, console messages and
answers) were made violet in #129. The owner can now pick the colours.

- "chat_color": the "Server" badge and name, as "#RRGGBB" (default
  "#8E5CFF", violet).
- "chat_text_color": the text of those lines (default "#D9C8FF", lavender).
- Both are in the config's "server" section; a value that is not a colour
  stops the server from starting and names the setting.
- Console: chat-color <#badge> [<#text>]. Saved, and sent to everyone on
  the server, so the next line uses it.
- With the defaults, chat looks as it did.

The colours travel in the roster, so this is protocol 45: servers and
games must both update. Lobbies hosted by a player are unchanged.
This commit is contained in:
AyeZeeBB committed 2026-10-08 20:42:51 -05:00
1 parent e57bae499a
commit 0b58163de5
13 files changed
+72 -5

No files matched your search

+3
View File
@@ -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 {
+4
View File
@@ -597,6 +597,8 @@ std::vector<std::uint8_t> 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<std::uint8_t>(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<Packet> decode(std::span<const std::uint8_t> bytes) noexcept {
p.object_clears = static_cast<std::uint32_t>(r.integer(4));
p.tps = static_cast<unsigned>(r.integer(1));
if (!valid_multiplayer_tps(p.tps)) return {};
p.chat_badge = 0xff000000U | static_cast<std::uint32_t>(r.integer(3));
p.chat_text = 0xff000000U | static_cast<std::uint32_t>(r.integer(3));
const auto placement = r.integer(1);
if (!valid_object_placement(placement)) return {};
p.object_placement = static_cast<ObjectPlacement>(placement);
+6 -1
View File
@@ -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.
@@ -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;
@@ -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<MultiplayerBan> server_bans;
std::uint32_t server_ban_total{};
@@ -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))
@@ -579,7 +579,7 @@ std::pair<std::uint32_t, std::string> mark_role(IdentityList list) {
std::pair<std::uint32_t, std::string> 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);
}
+1 -1
View File
@@ -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).
+5
View File
@@ -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).
+9
View File
@@ -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");
+18
View File
@@ -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<unsigned>(c.port));
@@ -207,6 +209,8 @@ ServerConfig load_config(const std::filesystem::path &file, std::vector<std::str
c.name = get("server", "name", c.name);
c.password = get("server", "password", c.password);
c.welcome = get("server", "welcome_message", c.welcome, {"welcome"});
c.chat_color = get("server", "chat_color", c.chat_color);
c.chat_text_color = get("server", "chat_text_color", c.chat_text_color);
c.listed = get("server", "listed", c.listed);
c.max_players = get("server", "max_players", c.max_players);
// Checked here, not in config_error: once narrowed, 70000 is just port 4464, and the
@@ -368,8 +372,22 @@ bool may_join(const ServerConfig &config, std::uint64_t id, std::size_t on) noex
const auto listed = [&](const std::vector<std::uint64_t> &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<std::uint32_t> 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<std::uint32_t>(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).";
+5
View File
@@ -62,6 +62,9 @@ struct ServerConfig {
std::vector<std::uint64_t> 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<std::uint32_t> 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
+11
View File
@@ -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") + ")";