From 7ee82b2771d96de011511e4a71d4770cf7d4cc67 Mon Sep 17 00:00:00 2001 From: AyeZeeBB Date: Wed, 7 Oct 2026 16:43:18 -0500 Subject: [PATCH] Player distance and nametag settings; far players sent less and more coarsely; profiler addresses Game - Player distance (Settings > Interface, "player-distance"): how far away another player is still built as a skater, 50 to 1000 m, default 120. Past it they keep their nametag or dot. All the way up shows everyone. A player already shown stays until 15% further out, so one skating along the edge is not built and taken down over and over. The game ran a full skater for every player on the server wherever they were, which is most of a frame on a full one - Nametags are always ReSkate's own. The "ReSkate nametags" toggle, the "Nametag style" button and the nametag-style command are gone; the game's own nametags are never shown - Nametag distance ("nametag-distance"): how far away a name still shows before it becomes a dot, 10 to 500 m. Default 120, was a fixed 150, so by default the name and the skater go at the same distance - Nametag dots ("nametag-dots"): off hides the dots for far players and the ones at the screen's edge for players off screen - Friends' nametags only ("nametags-friends"): only Steam friends get a name or dot; chat bubbles still show for everyone Server - A player out of sight of another (half as far again as the low rate starts, 255 m by default) is sent to them once a second instead of five times. On a full server most players are out of each other's sight, and at five a second each they were most of what was sent - Rotations are rounded before a pose is passed on, more coarsely the further away the recipient is: 0.04 degrees within the full rate's reach, 0.16 within the half rate's, a third of a degree past that, about 5 degrees out of sight. The pose is still an ordinary one. A bone that turned by less than a step is the same bytes as before, so the difference from the reference leaves it out, and the low bits of the ones that did turn pack away. By distance only: a player the crowd limit slowed keeps the fine rotations - A recipient crossing from one precision to another is sent a whole state of that player, since a difference from a reference of another precision would carry every bone again - "net" shows the size of the poses sent at each precision Profiler - A function in ReSkate.dll is reported by its address when the DLL has no symbols. A release build named every one of ours as the nearest of three exports, so a report could not say which was slow; the release's symbols name the address afterwards --- Engine/Core/Profiling/sampler.cpp | 13 +++- Engine/Game/Multiplayer/session_model.h | 14 ++++- Extension/Multiplayer/Hud/custom_nametags.cpp | 11 +++- Extension/Multiplayer/Hud/custom_nametags.h | 8 ++- Extension/Multiplayer/Net/delta_codec.h | 2 + Extension/Multiplayer/Net/protocol.cpp | 31 ++++++++++ Extension/Multiplayer/Net/protocol.h | 7 +++ Extension/Multiplayer/Session/room.h | 1 + Extension/Multiplayer/Session/session.cpp | 40 ++++++++++--- .../Multiplayer/Session/session_commands.cpp | 60 ++++++++++++++----- .../Multiplayer/Session/session_internal.h | 8 ++- .../Multiplayer/Session/session_view.cpp | 5 +- .../Multiplayer/Test/bandwidth_tests.cpp | 47 ++++++++++++++- Extension/Multiplayer/Test/mesh_tests.cpp | 2 +- Extension/Multiplayer/console_commands.cpp | 12 +++- .../UI/NativeMenu/native_menu_multiplayer.cpp | 2 - Extension/UI/Overlay/multiplayer_menu.cpp | 52 +++++++++++++--- Extension/UI/Overlay/nametag_overlay.cpp | 15 +++-- Extension/UI/Overlay/overlay.h | 3 + Extension/UI/Overlay/skate_menu.h | 5 +- Server/server_host.cpp | 49 ++++++++++++--- Server/server_host.h | 3 +- 22 files changed, 325 insertions(+), 65 deletions(-) diff --git a/Engine/Core/Profiling/sampler.cpp b/Engine/Core/Profiling/sampler.cpp index 3d93b7d..fc62615 100644 --- a/Engine/Core/Profiling/sampler.cpp +++ b/Engine/Core/Profiling/sampler.cpp @@ -186,6 +186,7 @@ const HANDLE symbol_session = reinterpret_cast(static_cast symbols_loaded; +const char waiting_marker{}; // an address inside this module, to tell it from the others std::string symbol_name(const Module& module, std::uintptr_t address) { std::lock_guard lock(symbols_mutex); if (!symbols_ready) { @@ -200,8 +201,16 @@ std::string symbol_name(const Module& module, std::uintptr_t address) { info->SizeOfStruct = sizeof(SYMBOL_INFO); info->MaxNameLen = 511; DWORD64 displacement{}; - if (symbols_ready && SymFromAddr(symbol_session, address, &displacement, info) && info->Name[0]) - return module.name + "!" + info->Name; + if (symbols_ready && SymFromAddr(symbol_session, address, &displacement, info) && info->Name[0]) { + // A release ReSkate.dll ships without its symbols, so all this finds in it is the nearest + // export, megabytes away: every function of ours then reads as one of three exports and + // a report cannot say which was slow. Unless the address is inside a symbol with a size + // (the real ones have one), give our own module's address instead: the release's symbols + // (build/release/ReSkate--symbols) name it afterwards. + const auto here = reinterpret_cast(&waiting_marker); + const bool own = module.base <= here && here < module.end; + if (!own || (info->Size && displacement < info->Size)) return module.name + "!" + info->Name; + } return std::format("{}+0x{:x}", module.name, address - module.base); } diff --git a/Engine/Game/Multiplayer/session_model.h b/Engine/Game/Multiplayer/session_model.h index 9ef549f..35c36b6 100644 --- a/Engine/Game/Multiplayer/session_model.h +++ b/Engine/Game/Multiplayer/session_model.h @@ -125,6 +125,7 @@ struct MultiplayerBan { std::string name; // as they were known when banned; may be empty std::int64_t added{}; // Unix time }; +inline constexpr float player_distance_least = 50.f, player_distance_unlimited = 1000.f; struct MultiplayerModel { VoiceModel voice; std::vector bans; @@ -141,12 +142,21 @@ struct MultiplayerModel { std::string tuning_status; // Local display preference: the floating name label above each peer. bool nametags{true}; - // Nametag style: ReSkate's own (name, distance, role colours, dots) or the game's. - bool custom_nametags{true}; // Local: whether session text chat shows at all (and T opens it). bool chat_visible{true}; // Local: bad words in chat names and messages show as **** (on by default). bool chat_filter{true}; + // Local, for ReSkate's nametags: `nametag_distance` is how far away a player's name still + // shows (past it they are a dot), `nametag_dots` whether those dots and the ones at the + // screen's edge for off-screen players show at all, `nametags_friends` names only the + // player's Steam friends. + // Local: how far away another player still gets a skater built for them, in metres. Past + // it they keep their nametag or dot. Each skater costs memory and frame time, in view or + // not. `player_distance_unlimited` and above: every player, however far. + float player_distance{120.f}; + float nametag_distance{120.f}; + bool nametag_dots{true}; + bool nametags_friends{}; // Local: chat bubbles above each skater's head. `chat_bubbles_distance` is how far away // a player may be and still show one, `chat_bubbles_duration` how many seconds a line // stays before it fades, `chat_bubbles_history` how many recent lines stack up. diff --git a/Extension/Multiplayer/Hud/custom_nametags.cpp b/Extension/Multiplayer/Hud/custom_nametags.cpp index a2f67de..8c4dadd 100644 --- a/Extension/Multiplayer/Hud/custom_nametags.cpp +++ b/Extension/Multiplayer/Hud/custom_nametags.cpp @@ -26,8 +26,8 @@ struct State { std::vector tags; Clock::time_point published; bool hidden{}; // the game hides its own nametags (menus, hidden UI; game_ui_state.h) - bool show_names{true}, show_bubbles{}; - float bubble_distance{40.f}; + bool show_names{true}, show_bubbles{}, dots{true}; + float bubble_distance{40.f}, name_distance{120.f}; }; State &state() { static auto *value = new State; @@ -37,7 +37,7 @@ State &state() { void publish_custom_nametags(std::uintptr_t base, std::vector players, std::optional> local, bool show_names, - bool show_bubbles, float bubble_distance) noexcept { + bool show_bubbles, float bubble_distance, float name_distance, bool dots) noexcept { auto &s = state(); if (!s.enabled.load(std::memory_order_acquire)) return; s.base.store(base, std::memory_order_release); @@ -61,6 +61,7 @@ void publish_custom_nametags(std::uintptr_t base, std::vector pla for (auto &line : player.bubbles) tag.bubbles.push_back({std::move(line.text), std::move(line.raw), line.appear, line.fade}); tag.self = player.self; + tag.nameless = player.nameless; if (local) { const float dx = player.head[0] - (*local)[0], dy = player.head[1] - (*local)[1], dz = player.head[2] - (*local)[2]; tag.distance = std::sqrt(dx * dx + dy * dy + dz * dz); @@ -72,6 +73,8 @@ void publish_custom_nametags(std::uintptr_t base, std::vector pla s.show_names = show_names; s.show_bubbles = show_bubbles; s.bubble_distance = bubble_distance; + s.name_distance = name_distance; + s.dots = dots; if (!view) { s.tags.clear(); return; @@ -126,6 +129,8 @@ overlay::Nametags custom_nametags() { result.show_names = s.show_names; result.show_bubbles = s.show_bubbles; result.bubble_distance = s.bubble_distance; + result.name_distance = s.name_distance; + result.dots = s.dots; view = s.view; } read_live_view(s.base.load(std::memory_order_acquire), view); diff --git a/Extension/Multiplayer/Hud/custom_nametags.h b/Extension/Multiplayer/Hud/custom_nametags.h index face9f8..4320cfb 100644 --- a/Extension/Multiplayer/Hud/custom_nametags.h +++ b/Extension/Multiplayer/Hud/custom_nametags.h @@ -28,6 +28,8 @@ struct NametagPlayer { std::vector bubbles; // The local player's own tag: only ever draws bubbles, never a name or dot. bool self{}; + // Another player whose name is not shown (friends-only nametags): bubbles only, like `self`. + bool nameless{}; }; // Role colours; friends are the viewer's own Steam friends. inline constexpr std::uint32_t nametag_white = 0xffffffffU, nametag_developer = 0xffff78b4U, // purple @@ -49,10 +51,12 @@ inline constexpr std::uint32_t nametag_developer_start = 0xffff206eU, // #6E20FF // Client thread, once per rendered frame: everyone to label and the local skater's position // (for distances). Also reads whether the game is hiding its own nametags right now. // `show_names` draws the name, distance and role badge; `show_bubbles` draws chat bubbles -// within `bubble_distance` metres. +// within `bubble_distance` metres. Names show within `name_distance` metres; past it, and for +// players off screen, a dot when `dots` is on. void publish_custom_nametags(std::uintptr_t base, std::vector players, std::optional> local, bool show_names, - bool show_bubbles, float bubble_distance) noexcept; + bool show_bubbles, float bubble_distance, float name_distance = 120.f, + bool dots = true) noexcept; void set_custom_nametags_enabled(bool enabled) noexcept; bool custom_nametags_enabled() noexcept; // The overlay's feed (any thread): nothing while off, stale, or while the game hides nametags. diff --git a/Extension/Multiplayer/Net/delta_codec.h b/Extension/Multiplayer/Net/delta_codec.h index 456ced8..ba43e47 100644 --- a/Extension/Multiplayer/Net/delta_codec.h +++ b/Extension/Multiplayer/Net/delta_codec.h @@ -46,6 +46,8 @@ class DeltaSender { WireUpdate prepare(const Packet &, std::span raw, std::span wire, DeltaCache &cache) const; void sent(const Packet &, WireUpdate &&); + // Drops a stream's reference: the next packet of it goes as a whole state. + void forget(std::uint64_t source, PacketKind kind) { bases_.erase(StreamKey{source, kind}); } // How old a pose or sound reference may get before a whole state replaces it. Each one // is sent reliably, so Steam resends it until it arrives: a dedicated server, with a // stream for every pair of players, keeps them rarer than the two seconds a game does. diff --git a/Extension/Multiplayer/Net/protocol.cpp b/Extension/Multiplayer/Net/protocol.cpp index 26c7b57..9b636af 100644 --- a/Extension/Multiplayer/Net/protocol.cpp +++ b/Extension/Multiplayer/Net/protocol.cpp @@ -315,6 +315,37 @@ std::string_view greeting_error(const Packet &p, std::uint64_t session, std::uin return "Peer changed level or session. Join again."; return {}; } +namespace { +void coarsen(Transform &t, unsigned bits) noexcept { + if (!bits) return; + unsigned largest{}; + float norm{}; + for (unsigned i = 0; i < 4; ++i) { + norm += t.rotation[i] * t.rotation[i]; + if (std::abs(t.rotation[i]) > std::abs(t.rotation[largest])) largest = i; + } + if (!(norm > 0.f) || !std::isfinite(norm)) return; + // As Writer::compact_transform packs it: the three smaller components, the largest positive. + constexpr float scale = 46339.5358f; + const float factor = (t.rotation[largest] < 0 ? -1.f : 1.f) * scale / std::sqrt(norm); + const float step = static_cast(1U << bits); + const float most = std::floor(32767.f / step) * step; // a rounded value must still fit 16 bits + float sum{}; + for (unsigned i = 0; i < 4; ++i) { + if (i == largest) continue; + const float packed = std::clamp(std::round(t.rotation[i] * factor / step) * step, -most, most); + t.rotation[i] = packed / scale; + sum += t.rotation[i] * t.rotation[i]; + } + t.rotation[largest] = std::sqrt(std::max(0.f, 1.f - sum)); +} +} // namespace +void coarsen_rotations(Pose &pose, unsigned bits) noexcept { + bits = std::min(bits, 12U); + coarsen(pose.root, std::min(bits, 6U)); // which way they face matters from further off than a finger does + for (auto &t : pose.skater) coarsen(t, bits); + for (auto &t : pose.board) coarsen(t, bits); +} std::vector encode(const Packet &p, bool compact_pose) { return encode(p, compact_pose, p.pose_interval_us); } diff --git a/Extension/Multiplayer/Net/protocol.h b/Extension/Multiplayer/Net/protocol.h index 85792c6..a84c6d5 100644 --- a/Extension/Multiplayer/Net/protocol.h +++ b/Extension/Multiplayer/Net/protocol.h @@ -225,6 +225,13 @@ std::string clean_chat_text(std::string_view); std::string clean_roster_name(std::string_view); bool valid_pose(const Pose &) noexcept; std::vector encode(const Packet &, bool compact_pose = false); +// Rounds every rotation in a pose to a multiple of 2^bits in the 16-bit form a compact pose +// packs it in (the root to at most 2^6). The pose still encodes and decodes as any other; +// what changes is that a bone turning by less than a step is the same bytes as before, so a +// difference from a reference leaves it out, and the low bits of the ones that did change are +// zero and pack away. One step is about 0.0025 degrees times 2^bits: 4 bits is 0.04 degrees, +// 7 bits a third of a degree. +void coarsen_rotations(Pose &pose, unsigned bits) noexcept; // The same, with a pose encoded at another update interval (a recipient thinned by // distance) instead of the packet's own, so the packet need not be copied for it. std::vector encode(const Packet &, bool compact_pose, std::uint32_t pose_interval_us); diff --git a/Extension/Multiplayer/Session/room.h b/Extension/Multiplayer/Session/room.h index 3b38d1c..3d95151 100644 --- a/Extension/Multiplayer/Session/room.h +++ b/Extension/Multiplayer/Session/room.h @@ -79,6 +79,7 @@ struct PoseDelivery { std::uint32_t interval_us = 50000; std::uint64_t next_source_time{}; std::uint32_t by_distance{}; // what the distance alone asked for, before any crowd limit + std::uint8_t precision{}; // how finely the source's rotations are sent to them (a dedicated server's pose_precision) }; // A crowd in one place: every player there is within full-rate distance of every other, and // what one player is sent grows with the crowd until their connection cannot carry it. The diff --git a/Extension/Multiplayer/Session/session.cpp b/Extension/Multiplayer/Session/session.cpp index 08304d2..cc2859b 100644 --- a/Extension/Multiplayer/Session/session.cpp +++ b/Extension/Multiplayer/Session/session.cpp @@ -261,7 +261,12 @@ void publish_party(Session &s) { if (!s.display_preferences_loaded) { s.display_preferences_loaded = true; s.nametags = profile_runtime::local_preference("Nametags").value_or(true); - s.custom_nametags = profile_runtime::local_preference("CustomNametags").value_or(true); + if (const auto saved = profile_runtime::local_value("NametagDistance"); saved && saved->is_number()) + s.nametag_distance = std::clamp(saved->get(), 10.f, 500.f); + s.nametag_dots = profile_runtime::local_preference("NametagDots").value_or(true); + if (const auto saved = profile_runtime::local_value("PlayerDistance"); saved && saved->is_number()) + s.player_distance = std::clamp(saved->get(), player_distance_least, player_distance_unlimited); + s.nametags_friends = profile_runtime::local_preference("NametagsFriendsOnly").value_or(false); s.chat_visible = profile_runtime::local_preference("ChatVisible").value_or(true); s.chat_filter = profile_runtime::local_preference("ChatFilter").value_or(true); s.chat_bubbles = profile_runtime::local_preference("ChatBubbles").value_or(true); @@ -400,7 +405,7 @@ void render(Session &s, std::uintptr_t client, const NativeFrame &local, std::ui // each frame. std::vector nametags; const bool bubbles = s.chat_bubbles; - const bool labels = (s.nametags && s.custom_nametags) || bubbles; + const bool labels = s.nametags || bubbles; if (labels) refresh_friends(s); // The newest chat lines from `sender` still inside the bubble duration, oldest first, // filtered the same way the chat panel filters them, each with the opacity it has left @@ -442,6 +447,8 @@ void render(Session &s, std::uintptr_t client, const NativeFrame &local, std::ui tag.name = p.member.name.empty() ? s.transport.name(p.member.id) : p.member.name; // The same role colour and badge the player's chat lines get. std::tie(tag.color, tag.tag) = player_role(s, p.member.id, false); + // Friends only: everyone else keeps their chat bubbles and loses the name and dot. + tag.nameless = s.nametags_friends && !std::binary_search(s.friend_ids.begin(), s.friend_ids.end(), p.member.id); tag.talking = std::any_of(voices.begin(), voices.end(), [&](const auto &v) { return v.id == p.member.id && v.speaking; }); tag.bubbles = recent_bubbles(p.member.id); nametags.push_back(std::move(tag)); @@ -482,6 +489,20 @@ void render(Session &s, std::uintptr_t client, const NativeFrame &local, std::ui const auto developer_id = [&](const Peer &p) { return steam_vouched(s, p) && shows_items(p) ? p.member.id : std::uint64_t{}; }; + // Only players within `player_distance` get a skater: each one costs memory and frame time + // whether or not it is in view, and on a full server most are far across the map, too small + // to see. Checked twice a second; a player already shown stays until a little further + // out, so one skating along the edge is not built and taken down over and over (building + // a skater is the expensive part). Before a pose has placed them, nobody is too far. + if (now >= s.next_shown_rank) { + s.next_shown_rank = now + 500000; + const bool everyone = s.player_distance >= player_distance_unlimited; + for (auto &p : active_peers(s)) { + if (!p.member.id || (dedicated_host(s) && p.member.id == s.host_id)) continue; + p.shown_wanted = everyone || !p.placed || + nearest_distance(p, local, view) <= s.player_distance * (p.visible ? 1.15f : 1.f); + } + } each_active_peer(s, [&](Peer &p) { // A dedicated server has no skater to show. if (!p.member.id || (dedicated_host(s) && p.member.id == s.host_id)) @@ -510,12 +531,13 @@ void render(Session &s, std::uintptr_t client, const NativeFrame &local, std::ui // the next frames slower still. const bool held = !sampled && was_visible && p.poses.heard_within(now, 8000000); // Showing a player without an actor spawns one (native_skater_spawn.cpp). - const bool spawning = sampled && p.appearance.value() && !was_visible && !remote_skater_entity(); + if (sampled) p.placed = true; + const bool spawning = sampled && p.shown_wanted && p.appearance.value() && !was_visible && !remote_skater_entity(); if (spawning && now < p.next_spawn) { p.native_status = "Waiting to show the player again."; } else if (spawning && native_pass) { p.native_status = "Waiting for another player's skater to finish spawning."; - } else if ((sampled || held) && p.appearance.value()) { + } else if ((sampled || held) && p.shown_wanted && p.appearance.value()) { if (spawning) { native_pass = true; p.applied_cosmetics = 0; // the new actor wears no recipe yet @@ -571,8 +593,11 @@ void render(Session &s, std::uintptr_t client, const NativeFrame &local, std::ui remove_remote(s.base); p.next_spawn = now + 3000000; } - p.native_status = - sampled ? "Waiting for the player's cosmetic recipe." : "Waiting for player poses."; + p.native_status = !p.shown_wanted ? "Further away than players are shown." + : sampled ? "Waiting for the player's cosmetic recipe." + : "Waiting for player poses."; + // No skater for them, but still their name or dot where they are. + if (!p.shown_wanted && sampled && labels) label(p); } } update_developer_hoodie(s.base, p.visible ? remote_skater_entity() : 0, developer_id(p), @@ -605,7 +630,8 @@ void render(Session &s, std::uintptr_t client, const NativeFrame &local, std::ui if (labels) publish_custom_nametags(s.base, std::move(nametags), local.ready ? std::optional(local.pose.root.position) : std::nullopt, - s.nametags && s.custom_nametags, bubbles, s.chat_bubbles_distance); + s.nametags, bubbles, s.chat_bubbles_distance, s.nametag_distance, + s.nametag_dots); } } // namespace MultiplayerModel model() { diff --git a/Extension/Multiplayer/Session/session_commands.cpp b/Extension/Multiplayer/Session/session_commands.cpp index 5177954..5476346 100644 --- a/Extension/Multiplayer/Session/session_commands.cpp +++ b/Extension/Multiplayer/Session/session_commands.cpp @@ -39,9 +39,11 @@ void apply_object_placement(Session &s, ObjectPlacement policy) { void apply_nametags(const Session &s) { // The feed also runs when only chat bubbles are on: the overlay draws whichever of the // two is enabled from the flags it is handed. - set_custom_nametags_enabled((s.nametags && s.custom_nametags) || s.chat_bubbles); - set_native_nametags_enabled(s.nametags && !s.custom_nametags); - set_native_compass_enabled(!(s.nametags && s.custom_nametags)); + // Nametags are ReSkate's own (Hud/custom_nametags.h); the game's are never shown. Its + // compass comes back when they are off, since they are what points at the other players. + set_custom_nametags_enabled(s.nametags || s.chat_bubbles); + set_native_nametags_enabled(false); + set_native_compass_enabled(!s.nametags); } void apply_guest_tools(Session &s, bool noclip, bool no_bail, bool boosts) { s.guest_noclip = noclip; @@ -337,16 +339,6 @@ std::string edit_nametags(Session &s, std::string_view argument) { profile_runtime::set_local_preference("Nametags", s.nametags); return s.nametags ? "Peer nametags shown." : "Peer nametags hidden."; } -std::string edit_nametag_style(Session &s, std::string_view argument) { - const auto custom = argument == "reskate" ? std::optional(true) : argument == "game" ? std::optional(false) - : parse_switch(argument, s.custom_nametags); - if (!custom) return "Use reskate, game, or toggle for the nametag style."; - s.custom_nametags = *custom; - s.display_preferences_loaded = true; - apply_nametags(s); - profile_runtime::set_local_preference("CustomNametags", s.custom_nametags); - return s.custom_nametags ? "ReSkate nametags: names, distances and dots." : "The game's own nametags."; -} // The Special page: a player on one of the backend's lists going without their tag, or // without the animation on their items. Their next appearance packet tells everyone // (session_send.cpp). @@ -410,6 +402,42 @@ std::string edit_chat_filter(Session &s, std::string_view argument) { publish_chat(s); return s.chat_filter ? "Bad words in chat are hidden." : "Chat is shown unfiltered."; } +std::string edit_player_distance(Session &s, std::string_view argument) { + float value{}; + const auto parsed = std::from_chars(argument.data(), argument.data() + argument.size(), value); + if (parsed.ec != std::errc{} || parsed.ptr != argument.data() + argument.size() || !std::isfinite(value) || + value < player_distance_least || value > player_distance_unlimited) + return "Player distance is a number of metres from 50 to 1000 (1000: every player)."; + s.player_distance = value; + s.next_shown_rank = 0; + profile_runtime::set_local_values({{"PlayerDistance", static_cast(value)}}); + return value >= player_distance_unlimited ? std::string("Every player is shown as a skater, however far.") + : "Players within " + std::to_string(static_cast(value)) + " m are shown as skaters."; +} +std::string edit_nametag_distance(Session &s, std::string_view argument) { + float value{}; + const auto parsed = std::from_chars(argument.data(), argument.data() + argument.size(), value); + if (parsed.ec != std::errc{} || parsed.ptr != argument.data() + argument.size() || !std::isfinite(value) || + value < 10.f || value > 500.f) + return "Nametag distance is a number of metres from 10 to 500."; + s.nametag_distance = value; + profile_runtime::set_local_values({{"NametagDistance", static_cast(value)}}); + return "Names show within " + std::to_string(static_cast(value)) + " m."; +} +std::string edit_nametag_dots(Session &s, std::string_view argument) { + const auto enabled = parse_switch(argument, s.nametag_dots); + if (!enabled) return "Use on, off, or toggle for nametag dots."; + s.nametag_dots = *enabled; + profile_runtime::set_local_preference("NametagDots", s.nametag_dots); + return s.nametag_dots ? "Far and off-screen players show as dots." : "No dots for far and off-screen players."; +} +std::string edit_nametags_friends(Session &s, std::string_view argument) { + const auto enabled = parse_switch(argument, s.nametags_friends); + if (!enabled) return "Use on, off, or toggle for friends-only nametags."; + s.nametags_friends = *enabled; + profile_runtime::set_local_preference("NametagsFriendsOnly", s.nametags_friends); + return s.nametags_friends ? "Only your Steam friends have nametags." : "Every player has a nametag."; +} std::string edit_chat_bubbles(Session &s, std::string_view argument) { const auto enabled = parse_switch(argument, s.chat_bubbles); if (!enabled) return "Use on, off, or toggle for chat bubbles."; @@ -561,7 +589,8 @@ bool queue_command(std::string_view action, std::string_view argument, std::stri if (launcher::offline_mode() && !own_mark_command(action)) return false; if ((action != "host" && action != "host-config" && action != "join" && action != "join-lobby" && action != "join-friend-lobby" && action != "stop" && action != "distances" && action != "object-placement" && action != "object-limit" && action != "kick" && action != "clear-objects" && - action != "nametags" && action != "nametag-style" && action != "chat-visible" && action != "chat-filter" && + action != "nametags" && action != "chat-visible" && action != "chat-filter" && + action != "nametag-distance" && action != "nametag-dots" && action != "nametags-friends" && action != "player-distance" && action != "chat-bubbles" && action != "chat-bubbles-own" && action != "chat-bubbles-distance" && action != "chat-bubbles-duration" && action != "chat-bubbles-history" && !own_mark_command(action) && @@ -743,7 +772,8 @@ std::string command(std::string_view action, std::string_view argument, std::str {"clear-objects", clear_guest_objects}, {"world-layer-sync", edit_world_layer_sync}, {"distances", edit_distances}, {"nametags", edit_nametags}, - {"nametag-style", edit_nametag_style}, + {"nametag-distance", edit_nametag_distance}, {"nametag-dots", edit_nametag_dots}, + {"nametags-friends", edit_nametags_friends}, {"player-distance", edit_player_distance}, {"mark-tag", edit_own_tag}, {"mark-items", edit_own_items}, {"mark-style", edit_mark_style}, {"chat-visible", edit_chat_visible}, {"chat-filter", edit_chat_filter}, {"chat-bubbles", edit_chat_bubbles}, {"chat-bubbles-own", edit_chat_bubbles_own}, diff --git a/Extension/Multiplayer/Session/session_internal.h b/Extension/Multiplayer/Session/session_internal.h index 343bd1f..8ce5b2b 100644 --- a/Extension/Multiplayer/Session/session_internal.h +++ b/Extension/Multiplayer/Session/session_internal.h @@ -92,6 +92,8 @@ struct Peer { // A skater removed for lack of poses is not spawned again before this: a spawn is a heavy // native pass, and a player's poses may stop and start again and again. std::uint64_t next_spawn{}; + // Near enough to be shown as a skater; and whether a pose has placed them yet. + bool shown_wanted = true, placed{}; ReceiveBudget budget; PoseBuffer poses; Pose render_pose; @@ -229,7 +231,11 @@ struct Session { unsigned capacity = max_players; // Local display preferences, loaded once from the profile. Never sent to peers. bool nametags = true, chat_visible = true, display_preferences_loaded{}; - bool custom_nametags = true; // ReSkate's nametags instead of the game's (Hud/custom_nametags.h) + float nametag_distance = 120.f; // names within this many metres, dots past it + // Only players within this many metres have a skater (session.cpp); checked a few times a second. + float player_distance = 120.f; + std::uint64_t next_shown_rank{}; + bool nametag_dots = true, nametags_friends{}; bool chat_filter = true; // bad words in chat show as **** (Engine/Core/Text/word_filter.h) // Chat bubbles above each skater's head (Hud/custom_nametags.h, nametag_overlay.cpp). bool chat_bubbles = true, chat_bubbles_own{}; diff --git a/Extension/Multiplayer/Session/session_view.cpp b/Extension/Multiplayer/Session/session_view.cpp index 7eff34d..4e5a156 100644 --- a/Extension/Multiplayer/Session/session_view.cpp +++ b/Extension/Multiplayer/Session/session_view.cpp @@ -177,7 +177,6 @@ void publish(Session &s, const NativeFrame *local) { view.saved_host = {true, s.host_preferences.public_lobby, s.host_preferences.password_required, static_cast(s.host_preferences.capacity), s.host_preferences.tps, s.host_preferences.lobby_name}; view.nametags = s.nametags; - view.custom_nametags = s.custom_nametags; if (const auto social = steam_social_snapshot()) if (const auto mark = identity_mark(social->local.id)) { std::tie(view.identity_tag_colour, view.identity_tag) = mark_role(*mark); @@ -229,6 +228,10 @@ void publish(Session &s, const NativeFrame *local) { view.server_map_rotation = s.server_map_rotation; view.server_map_votes = (s.server_votes & server_vote_map) != 0; } + view.player_distance = s.player_distance; + view.nametag_distance = s.nametag_distance; + view.nametag_dots = s.nametag_dots; + view.nametags_friends = s.nametags_friends; view.chat_visible = s.chat_visible; view.chat_filter = s.chat_filter; view.chat_bubbles = s.chat_bubbles; diff --git a/Extension/Multiplayer/Test/bandwidth_tests.cpp b/Extension/Multiplayer/Test/bandwidth_tests.cpp index 8280ade..a59258e 100644 --- a/Extension/Multiplayer/Test/bandwidth_tests.cpp +++ b/Extension/Multiplayer/Test/bandwidth_tests.cpp @@ -130,6 +130,51 @@ void audio_checks() { check(raw.size() < packet_header_size + 2 + p.audio.size() * 383, "Sparse audio failed to shrink fixture"); std::cout << "Audio: continuous coalescing, contact/selector edges, exact values, idle heartbeat, reordering, deduplication and disconnect passed.\n"; } +void coarse_checks() { + // Coarser rotations still encode and decode as any pose, land within a step of the + // original, and make a bone that barely turned the same bytes as before. + std::mt19937 random(11); + std::uniform_real_distribution any(-1.f, 1.f); + auto p = fixture(); + p.kind = PacketKind::pose; + for (auto &t : p.pose.skater) { + t.rotation = {any(random), any(random), any(random), any(random)}; + float norm{}; + for (float v : t.rotation) norm += v * v; + for (float &v : t.rotation) v /= std::sqrt(norm); + } + // The awkward ones: two components equal and as large as a smaller one can be, and an exact axis. + p.pose.skater[0].rotation = {.70710678f, .70710678f, 0, 0}; + p.pose.skater[1].rotation = {0, 0, 0, -1}; + p.pose.skater[2].rotation = {.5f, .5f, .5f, .5f}; + for (const unsigned bits : {4U, 6U, 7U, 11U}) { + auto coarse = p; + coarsen_rotations(coarse.pose, bits); + const auto decoded = decode(encode(coarse, true)); + check(decoded && decoded->pose.skater.size() == p.pose.skater.size(), "A coarsened pose did not decode"); + const float step = static_cast(1U << bits) / 46339.5358f; + for (std::size_t i = 0; decoded && i < p.pose.skater.size(); ++i) { + const auto &a = p.pose.skater[i].rotation, &b = decoded->pose.skater[i].rotation; + const float same = std::abs(a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3]); + // Each of three components is off by at most half a step. + check(same > 1.f - 2.f * step * step - 1e-4f, "A coarsened rotation moved by more than its step"); + } + // A turn of a fortieth of a step is lost: the same bytes, so a difference leaves the bone out. + auto nudged = p; + for (auto &t : nudged.pose.skater) { + t.rotation[0] += step * .025f; + float norm{}; + for (float v : t.rotation) norm += v * v; + for (float &v : t.rotation) v /= std::sqrt(norm); + } + coarsen_rotations(nudged.pose, bits); + const auto first = encode(coarse, true), second = encode(nudged, true); + std::size_t different{}; + for (std::size_t i = 0; i < std::min(first.size(), second.size()); ++i) different += first[i] != second[i]; + check(first.size() == second.size() && different < first.size() / 20, "A turn far below the step still changed the pose's bytes"); + } + std::cout << "Coarse rotations: valid poses, within a step, small turns unchanged.\n"; +} void rate_checks() { check(pose_interval(61*61, 50000) == 100000 && pose_interval(56*56, 100000) == 100000 && pose_interval(49*49, 100000) == 50000, "Near-rate hysteresis failed"); @@ -219,6 +264,6 @@ void block_checks() { } } int main() { - try { pose_checks(); audio_checks(); rate_checks(); block_checks(); } + try { pose_checks(); audio_checks(); coarse_checks(); rate_checks(); block_checks(); } catch (const std::exception &e) { std::cerr << e.what() << '\n'; return 1; } } diff --git a/Extension/Multiplayer/Test/mesh_tests.cpp b/Extension/Multiplayer/Test/mesh_tests.cpp index 20e7a40..e9b3893 100644 --- a/Extension/Multiplayer/Test/mesh_tests.cpp +++ b/Extension/Multiplayer/Test/mesh_tests.cpp @@ -272,7 +272,7 @@ void prepare_native_indicators(std::uintptr_t) noexcept {} void prepare_player_ui(std::uintptr_t) noexcept {} void prepare_remote_audio(std::uintptr_t) noexcept {} bool install_entity_hooks(std::uintptr_t, std::string &) noexcept { return true; } -void publish_custom_nametags(std::uintptr_t, std::vector, std::optional>, bool, bool, float) noexcept {} +void publish_custom_nametags(std::uintptr_t, std::vector, std::optional>, bool, bool, float, float, bool) noexcept {} void set_custom_nametags_enabled(bool) noexcept {} GameUiState sample_game_ui_state(std::uintptr_t) noexcept { return {}; } void note_local_skater(const Transform &) noexcept {} diff --git a/Extension/Multiplayer/console_commands.cpp b/Extension/Multiplayer/console_commands.cpp index a87af0c..2acc6bc 100644 --- a/Extension/Multiplayer/console_commands.cpp +++ b/Extension/Multiplayer/console_commands.cpp @@ -31,7 +31,10 @@ void register_multiplayer_commands(Commands ®istry) { Command{"tpall", "Host or server admin: teleport everyone to you"}, Command{"tphere", "Host or server admin: teleport one player to you"}, Command{"nametags", "Show or hide player nametags (on, off, toggle)"}, - Command{"nametag-style", "ReSkate nametags or the game's own (reskate, game, toggle)"}, + Command{"player-distance", "How far away another player still gets a skater, in metres (50-1000; 1000: every player). Past it: a nametag or dot"}, + Command{"nametag-distance", "How far away a player's name still shows, in metres (10-500); past it they are a dot"}, + Command{"nametag-dots", "Show far and off-screen players as dots (on, off, toggle)"}, + Command{"nametags-friends", "Only your Steam friends have nametags (on, off, toggle)"}, Command{"chat-bubbles", "Show or hide chat bubbles above skaters (on, off, toggle)"}, Command{"chat-bubbles-own", "Also show your own chat messages above your skater (on, off, toggle)"}, Command{"chat-bubbles-distance", "How far away a player may be and still show a chat bubble, in metres (5-500)"}, @@ -51,14 +54,17 @@ void register_multiplayer_commands(Commands ®istry) { } if (std::string_view(c.name) == "join-lobby") args.push_back(argument("lobby_id")); - if (std::string_view(c.name) == "nametags" || std::string_view(c.name) == "nametag-style" || + if (std::string_view(c.name) == "nametags" || std::string_view(c.name) == "chat-bubbles" || std::string_view(c.name) == "chat-bubbles-own" || + std::string_view(c.name) == "nametag-dots" || std::string_view(c.name) == "nametags-friends" || std::string_view(c.name) == "score-check") args.push_back(argument("choice")); - if (std::string_view(c.name) == "chat-bubbles-distance") + if (std::string_view(c.name) == "chat-bubbles-distance" || std::string_view(c.name) == "nametag-distance") args.push_back(argument("metres")); if (std::string_view(c.name) == "chat-bubbles-duration") args.push_back(argument("seconds")); + if (std::string_view(c.name) == "player-distance") + args.push_back(argument("metres")); if (std::string_view(c.name) == "chat-bubbles-history") args.push_back(argument("lines")); if (std::string_view(c.name) == "chat" || std::string_view(c.name) == "server" || std::string_view(c.name) == "party" || diff --git a/Extension/UI/NativeMenu/native_menu_multiplayer.cpp b/Extension/UI/NativeMenu/native_menu_multiplayer.cpp index 25db9a2..8db8a94 100644 --- a/Extension/UI/NativeMenu/native_menu_multiplayer.cpp +++ b/Extension/UI/NativeMenu/native_menu_multiplayer.cpp @@ -406,8 +406,6 @@ void render_section(const Context& context, const MultiplayerModel& model, Secti model.hosting ? "world-layer-sync" : "", "toggle", false, 136.f); add_button(context, side, "nametags", std::string("Nametags: ") + (model.nametags ? "On" : "Off"), "nametags", "toggle", false, 136.f); - add_button(context, side, "nametag-style", std::string("Nametag style: ") + (model.custom_nametags ? "ReSkate" : "Game"), - "nametag-style", "toggle", false, 136.f); add_button(context, side, "chat-bubbles", std::string("Chat bubbles: ") + (model.chat_bubbles ? "On" : "Off"), "chat-bubbles", "toggle", false, 136.f); add_button(context, side, "chat-bubbles-own", std::string("Own chat bubbles: ") + (model.chat_bubbles_own ? "On" : "Off"), diff --git a/Extension/UI/Overlay/multiplayer_menu.cpp b/Extension/UI/Overlay/multiplayer_menu.cpp index a6c19c4..0c8412b 100644 --- a/Extension/UI/Overlay/multiplayer_menu.cpp +++ b/Extension/UI/Overlay/multiplayer_menu.cpp @@ -240,19 +240,55 @@ void debug_page(SkateMenu &menu, const MultiplayerModel &mp, const CallbacksV3 & void multiplayer_display_settings(SkateMenu &menu, const Model &model) { const auto &mp = model.multiplayer; begin_card(menu, "multiplayer-display", "MULTIPLAYER"); + { + std::array unused{}; + field(menu, "Player distance", "How far away other players are still shown as skaters. Past it they keep their nametag " + "or dot. Lower it on a busy server for more frames and less memory; all the way up shows everyone."); + float shown = menu.player_distance_pending.value_or(mp.player_distance); + ImGui::SliderFloat("##player-distance", &shown, player_distance_least, player_distance_unlimited, + shown >= player_distance_unlimited ? "Everyone" : "%.0f m", ImGuiSliderFlags_AlwaysClamp); + if (ImGui::IsItemActive()) menu.player_distance_pending = shown; + if (ImGui::IsItemDeactivatedAfterEdit()) { + send_private(menu, "player-distance", std::to_string(static_cast(shown)), unused, false); + menu.player_distance_until = ImGui::GetTime() + 2; + } + if (!ImGui::IsItemActive() && menu.player_distance_pending && + (*menu.player_distance_pending == mp.player_distance || ImGui::GetTime() >= menu.player_distance_until)) + menu.player_distance_pending.reset(); + } bool nametags = mp.nametags; - if (toggle_row(menu, "Player nametags", "The name above each skater.", nametags)) { + if (toggle_row(menu, "Player nametags", + "The name above each skater, with their distance: purple for ReSkate developers, red for content creators, " + "gold for homies, pink for server admins, blue for the host, green for your Steam friends.", + nametags)) { std::array unused{}; send_private(menu, "nametags", nametags ? "on" : "off", unused, false); } - bool custom = mp.custom_nametags; - if (toggle_row(menu, "ReSkate nametags", - "Names with distance: purple for ReSkate developers, red for content creators, gold for homies, " - "pink for server admins, blue for the host, green for your Steam friends; far and off-screen players as dots. " - "Off: the game's own nametags and arrows.", - custom, mp.nametags, "OFF")) { + if (mp.nametags) { std::array unused{}; - send_private(menu, "nametag-style", custom ? "reskate" : "game", unused, false); + field(menu, "Nametag distance", "How far away a player's name still shows. Past it they are a dot."); + float distance = menu.nametag_distance_pending.value_or(mp.nametag_distance); + ImGui::SliderFloat("##nametag-distance", &distance, 10.f, 500.f, "%.0f m", ImGuiSliderFlags_AlwaysClamp); + if (ImGui::IsItemActive()) menu.nametag_distance_pending = distance; + if (ImGui::IsItemDeactivatedAfterEdit()) { + send_private(menu, "nametag-distance", std::to_string(static_cast(distance)), unused, false); + menu.nametag_distance_until = ImGui::GetTime() + 2; + } + if (!ImGui::IsItemActive() && menu.nametag_distance_pending && + (*menu.nametag_distance_pending == mp.nametag_distance || ImGui::GetTime() >= menu.nametag_distance_until)) + menu.nametag_distance_pending.reset(); + } + bool dots = mp.nametag_dots; + if (toggle_row(menu, "Nametag dots", "Show players past the nametag distance, and players off screen, as dots.", dots, + mp.nametags, "OFF")) { + std::array unused{}; + send_private(menu, "nametag-dots", dots ? "on" : "off", unused, false); + } + bool friends_only = mp.nametags_friends; + if (toggle_row(menu, "Friends' nametags only", "Only your Steam friends have a name or dot. Chat bubbles still show for everyone.", + friends_only, mp.nametags, "OFF")) { + std::array unused{}; + send_private(menu, "nametags-friends", friends_only ? "on" : "off", unused, false); } bool chat = mp.chat_visible; if (toggle_row(menu, "Text chat", "Show session chat in the bottom-right corner; T opens it. Hidden, nothing shows and T does nothing.", diff --git a/Extension/UI/Overlay/nametag_overlay.cpp b/Extension/UI/Overlay/nametag_overlay.cpp index 2ab43c3..4cd0123 100644 --- a/Extension/UI/Overlay/nametag_overlay.cpp +++ b/Extension/UI/Overlay/nametag_overlay.cpp @@ -24,7 +24,6 @@ using namespace dingosdk::overlay::detail; namespace dingosdk::overlay::detail { namespace { // Nearer than this a player gets their name; further, only a dot. -constexpr float name_range = 150.0f; // How far in from the screen's edge the dots of players off screen sit (1080p pixels). constexpr float edge_margin = 28.0f; @@ -221,8 +220,10 @@ void draw_nametags() { const auto frame = static_cast(ImGui::GetFrameCount()); // Far first, so nearer names draw over them. std::sort(value.tags.begin(), value.tags.end(), [](const Nametag &a, const Nametag &b) { return a.distance > b.distance; }); + const float name_range = std::max(1.0f, value.name_distance); for (const auto &tag : value.tags) { const auto animated_colour = animated_nametag_colour(tag.color, animation_time); + const bool unnamed = tag.self || tag.nameless; // bubbles only: no name, no dot const Vec3 delta{tag.position[0] - origin[0], tag.position[1] - origin[1], tag.position[2] - origin[2]}; const float depth = -dot(delta, back), side = dot(delta, right), height = dot(delta, up); bool on_screen = false; @@ -233,7 +234,7 @@ void draw_nametags() { } if (!on_screen) { // The local player has no name or edge dot, only a bubble when they are visible. - if (!tag.self && value.show_names) { + if (!unnamed && value.show_names && value.dots) { // The direction to them from the middle of the screen, pushed out to the edge. float dx = depth > 0.1f ? at.x - centre.x : side, dy = depth > 0.1f ? at.y - centre.y : -height; if (depth <= 0.1f && std::abs(dx) < 1e-3f && std::abs(dy) < 1e-3f) dy = 1.0f; // straight behind @@ -246,10 +247,12 @@ void draw_nametags() { } // Where a bubble's tail should point: the top of the name, or the head itself. float bubble_bottom = at.y; - if (!tag.self && value.show_names) { + if (!unnamed && value.show_names) { if (tag.distance > name_range) { - outlined_dot(draw, at, 4.0f * k, animated_colour); - bubble_bottom = at.y - 6.0f * k; + if (value.dots) { + outlined_dot(draw, at, 4.0f * k, animated_colour); + bubble_bottom = at.y - 6.0f * k; + } } else { // Names shrink a little with distance and fade slightly towards the name range. const float nearness = 1.0f - std::clamp(tag.distance / name_range, 0.0f, 1.0f); @@ -287,7 +290,7 @@ void draw_nametags() { animated_colour, frame); // When the name itself is not shown (nametags off, or a player past the name range), // label the stack so each bubble is still attributed. - const bool named = !tag.self && value.show_names && tag.distance <= name_range; + const bool named = !unnamed && value.show_names && tag.distance <= name_range; if (!named && !tag.name.empty() && stack_top < bubble_bottom - 1.0f) { const float nearness = 1.0f - std::clamp(tag.distance / name_range, 0.0f, 1.0f); const float label_size = std::clamp((12.0f + 3.0f * nearness) * k, 9.0f, 30.0f); diff --git a/Extension/UI/Overlay/overlay.h b/Extension/UI/Overlay/overlay.h index db36708..16a9d52 100644 --- a/Extension/UI/Overlay/overlay.h +++ b/Extension/UI/Overlay/overlay.h @@ -318,6 +318,7 @@ struct Nametag { // Recent chat lines to show as bubbles above the head, oldest first ("" = none). std::vector bubbles; bool self{}; // the local player: bubbles only, never a name or dot + bool nameless{}; // another player whose name is not shown: bubbles only too }; struct Nametags { std::array camera{}; // world matrix: right, up, back, position rows @@ -325,6 +326,8 @@ struct Nametags { bool show_names{true}; // draw the name, distance and role badge bool show_bubbles{}; // draw chat bubbles above the heads float bubble_distance{40.f}; // furthest a player may be and still show a bubble (metres) + float name_distance{120.f}; // furthest a player's name shows; past it they are a dot + bool dots{true}; // draw those dots, and the ones at the edge for off-screen players std::vector tags; }; using NametagFeed = Nametags (*)(); diff --git a/Extension/UI/Overlay/skate_menu.h b/Extension/UI/Overlay/skate_menu.h index 9fd1223..99feff1 100644 --- a/Extension/UI/Overlay/skate_menu.h +++ b/Extension/UI/Overlay/skate_menu.h @@ -55,7 +55,10 @@ struct SkateMenu { std::optional voice_range_pending; // the host's range slider while dragged / until the model agrees double voice_range_until{}; // Chat bubble distance/duration/history sliders, likewise held while dragged. - std::optional chat_bubbles_distance_pending, chat_bubbles_duration_pending; + std::optional chat_bubbles_distance_pending, chat_bubbles_duration_pending, nametag_distance_pending; + double nametag_distance_until{}; + std::optional player_distance_pending; + double player_distance_until{}; std::optional chat_bubbles_history_pending; double chat_bubbles_distance_until{}, chat_bubbles_duration_until{}, chat_bubbles_history_until{}; std::map> voice_volume_pending; diff --git a/Server/server_host.cpp b/Server/server_host.cpp index 2a40532..4eca32e 100644 --- a/Server/server_host.cpp +++ b/Server/server_host.cpp @@ -181,6 +181,13 @@ namespace { // for a difference from one, however old the reference (net's pose size line), so they are // kept rare: at every 8 seconds they were a seventh of everything a busy server sent. constexpr std::uint64_t whole_state_refresh = 60000000; +// How finely a player's rotations are sent, by how far away the recipient is (coarsen_rotations): +// within the full rate's reach, the half rate's, the low rate's, and out of sight. The +// nearest is 0.04 degrees a step, which nothing shows; the low rate's a third of a degree, +// at 170 m and more; out of sight only where the skater is matters. A bone that turns by less +// than a step is not sent at all, and a far skater's fingers and face never turn by more. +constexpr std::array pose_precision{4, 6, 7, 11}; +constexpr std::array pose_precision_names{"near", "mid", "far", "out of sight"}; constexpr std::array traffic_names{"poses", "sound", "voice", "outfits", "objects", "other"}; constexpr std::size_t traffic_kind(PacketKind kind) noexcept { return kind == PacketKind::pose ? 0 : kind == PacketKind::audio ? 1 : kind == PacketKind::voice ? 2 @@ -239,7 +246,7 @@ void Host::broadcast(const Packet &packet, bool reliable, bool fresh, std::uint6 // Recipients holding the same delta reference share one patch and compression // (DeltaCache): every update is built first, then all are sent and recorded. struct Encoded { Packet packet; std::vector raw, wire; DeltaCache deltas; }; - std::array encoded; + std::array encoded; // by the interval the pose carries, then by precision struct Outgoing { Guest *guest; std::uint64_t id; WireUpdate update; PoseDelivery *delivery; std::uint32_t interval; unsigned variant; }; std::vector outgoing; outgoing.reserve(guests_.size()); @@ -282,6 +289,9 @@ void Host::broadcast(const Packet &packet, bool reliable, bool fresh, std::uint6 !needs_relay(p.direct_routes, p.route_reported, source->member, now_)) continue; PoseDelivery *delivery{}; + std::uint32_t slot{}; // how often this source's poses go to them, when not every `interval` + bool out_of_sight{}; + unsigned precision{}; std::uint32_t interval = multiplayer_pose_interval(config_.tps); if (packet.kind == PacketKind::pose) { auto entry = std::find_if(p.pose_delivery.begin(), p.pose_delivery.end(), @@ -300,8 +310,21 @@ void Host::broadcast(const Packet &packet, bool reliable, bool fresh, std::uint6 } delivery->by_distance = pose_interval(distance, delivery->by_distance, config_.distances, config_.tps); interval = crowd_interval(delivery->by_distance, distance, p.crowd); + // Half as far again as the low rate starts: nothing of a skater can be made out + // there, only where they are (their dot, the map). + // (A little nearer to come back into sight than to leave it.) + const auto sight = static_cast(config_.distances.low_rate_start) * (delivery->precision == 3 ? 1.4f : 1.5f); + out_of_sight = config_.distances.valid() && distance > sight * sight; + precision = out_of_sight ? 3U : delivery->by_distance == 200000 ? 2U : delivery->by_distance == 100000 ? 1U : 0U; } else { delivery->by_distance = 0; + precision = delivery->precision; + } + // By distance only, which changes seldom: a difference from a reference of another + // precision carries every bone again, so the reference is renewed with it. + if (precision != delivery->precision) { + p.sender.forget(packet.source, PacketKind::pose); + delivery->precision = static_cast(precision); } // Standing still for a few seconds: five poses a second carry it to anyone. The // rate is back at once when they move (the change of rate sends the next pose). @@ -309,16 +332,21 @@ void Host::broadcast(const Packet &packet, bool reliable, bool fresh, std::uint6 if (delivery->interval_us != interval) delivery->next_source_time = 0; delivery->interval_us = interval; if (interval > multiplayer_pose_interval(config_.tps) && packet.time_us < delivery->next_source_time) continue; + // Out of sight, one pose a second is sent (as low-rate poses, a game holds a skater + // it hears nothing of for several seconds). On a full server most players are out + // of each other's sight, and at five a second each they were most of what was sent. + slot = out_of_sight && interval == 200000 ? 1000000U : interval; } - const auto variant = interval == 200000 ? 2U : interval == 100000 ? 1U : 0U; + const auto variant = (interval == 200000 ? 2U : interval == 100000 ? 1U : 0U) * 4 + precision; auto &data = encoded[variant]; if (data.raw.empty()) { data.packet = packet; data.packet.pose_interval_us = interval; + if (packet.kind == PacketKind::pose) coarsen_rotations(data.packet.pose, pose_precision[precision]); data.raw = encode(data.packet, true); data.wire = encode_wire_bytes(data.raw); } - outgoing.push_back({&p, id, p.sender.prepare(data.packet, data.raw, data.wire, data.deltas), delivery, interval, variant}); + outgoing.push_back({&p, id, p.sender.prepare(data.packet, data.raw, data.wire, data.deltas), delivery, slot ? slot : interval, variant}); } for (auto &out : outgoing) { // A packet that cannot be built for anyone is its source's fault, never this @@ -334,8 +362,8 @@ void Host::broadcast(const Packet &packet, bool reliable, bool fresh, std::uint6 counted->total.snapshots += whole; } if (sending.kind == PacketKind::pose && !whole) { - ++pose_sizes_.sent; - pose_sizes_.sent_bytes += out.update.bytes.size(); + ++pose_sizes_.sent[out.variant % 4]; + pose_sizes_.sent_bytes[out.variant % 4] += out.update.bytes.size(); } out.guest->sender.sent(sending, std::move(out.update)); } @@ -1003,11 +1031,14 @@ std::string Host::network_report(std::string_view player, bool console) { } text += "\nloop: " + loop_report(); text += "\n" + by_kind(traffic_); - if (pose_sizes_.samples && pose_sizes_.sent) { + if (pose_sizes_.samples) { const auto each = [&](std::uint64_t sum) { return std::to_string(sum / pose_sizes_.samples) + " B"; }; - text += "\npose size: " + std::to_string(pose_sizes_.sent_bytes / pose_sizes_.sent) + " B as sent (whole " + each(pose_sizes_.whole) + - "); it would be " + each(pose_sizes_.second) + " against a reference renewed every second, " + each(pose_sizes_.quarter) + - " every quarter second, " + each(pose_sizes_.last) + " against the pose before (" + std::to_string(pose_sizes_.samples) + " measured)"; + text += "\npose size as sent:"; + for (std::size_t i = 0; i < pose_sizes_.sent.size(); ++i) + if (pose_sizes_.sent[i]) + text += std::string(" ") + pose_precision_names[i] + " " + std::to_string(pose_sizes_.sent_bytes[i] / pose_sizes_.sent[i]) + " B x" + + std::to_string(pose_sizes_.sent[i]); + text += "; as it arrives " + each(pose_sizes_.last) + " against the pose before, whole " + each(pose_sizes_.whole); } { // Who uploads most, and what most of it is: one player's outfit or objects can cost diff --git a/Server/server_host.h b/Server/server_host.h index 4d35af4..c5b641e 100644 --- a/Server/server_host.h +++ b/Server/server_host.h @@ -177,7 +177,8 @@ class Host { // every fourth pose that arrives (net). A nearer reference changes less, so it packs // smaller: this says by how much, before the server is made to send them that way. struct PoseSizes { - std::uint64_t samples{}, whole{}, last{}, quarter{}, second{}, sent{}, sent_bytes{}; + std::uint64_t samples{}, whole{}, last{}, quarter{}, second{}; + std::array sent{}, sent_bytes{}; // differences sent, by precision (pose_precision) } pose_sizes_; void measure_pose(Guest &from, const Packet &packet); std::uint64_t traffic_mark_{}, traffic_window_us_{}; // when `mark` was taken, and how long `last` covers