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