From 91c939ea4f35bc1aa99ea16158d01599dec060de Mon Sep 17 00:00:00 2001 From: AyeZeeBB Date: Wed, 7 Oct 2026 19:42:48 -0500 Subject: [PATCH] Server: limit how far a mod may resize part of a skater for other players - bone_scale_limit in ReSkateServer.json, and the bone-scale command: the most a bone may be scaled in the poses the server passes on, 1 to 8, and its inverse the least. 1 shows every skater at the game's own proportions; 0 (the default, "bone-scale off") is no limit - A "big head" mod sets one joint's scale in the body's bind pose (animation/dingo/cas_rsp_body_bind, the head from 1 to 4). The game applies it to the bone, and the bone's scale travels in every pose, which is how players without the mod see it. The game's own skeleton has no scaled joint, so 1 changes nothing for anyone else - The player with the mod still sees it on their own screen: the server decides only what the others are sent - Changing it sends whole states again, so nobody keeps a reference with the old sizes in it - Dedicated servers only; a lobby's host has no such setting --- Extension/Multiplayer/Net/protocol.cpp | 9 +++++++ Extension/Multiplayer/Net/protocol.h | 4 ++++ .../Multiplayer/Test/bandwidth_tests.cpp | 12 ++++++++++ Server/README.txt | 6 +++++ Server/Test/server_config_tests.cpp | 4 ++++ Server/server_config.cpp | 4 ++++ Server/server_config.h | 3 +++ Server/server_host.cpp | 24 +++++++++++++++++-- 8 files changed, 64 insertions(+), 2 deletions(-) diff --git a/Extension/Multiplayer/Net/protocol.cpp b/Extension/Multiplayer/Net/protocol.cpp index 9b636af..a5555a2 100644 --- a/Extension/Multiplayer/Net/protocol.cpp +++ b/Extension/Multiplayer/Net/protocol.cpp @@ -340,6 +340,15 @@ void coarsen(Transform &t, unsigned bits) noexcept { t.rotation[largest] = std::sqrt(std::max(0.f, 1.f - sum)); } } // namespace +void limit_bone_scale(Pose &pose, float limit) noexcept { + if (!(limit >= 1.f)) return; + const auto hold = [&](Transform &t) { + for (float &axis : t.scale) axis = std::isfinite(axis) ? std::clamp(axis, 1.f / limit, limit) : 1.f; + }; + hold(pose.root); + for (auto &t : pose.skater) hold(t); + for (auto &t : pose.board) hold(t); +} 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 diff --git a/Extension/Multiplayer/Net/protocol.h b/Extension/Multiplayer/Net/protocol.h index a84c6d5..b847e74 100644 --- a/Extension/Multiplayer/Net/protocol.h +++ b/Extension/Multiplayer/Net/protocol.h @@ -232,6 +232,10 @@ std::vector encode(const Packet &, bool compact_pose = false); // 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; +// Keeps every bone's scale within 1/limit to limit on each axis (1: no scaling at all). A mod +// that resizes part of a skater (a head four times the size) does it with a bone's scale, +// which travels in the pose and so shows to everyone, mod or not. +void limit_bone_scale(Pose &pose, float limit) 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/Test/bandwidth_tests.cpp b/Extension/Multiplayer/Test/bandwidth_tests.cpp index a59258e..586a36c 100644 --- a/Extension/Multiplayer/Test/bandwidth_tests.cpp +++ b/Extension/Multiplayer/Test/bandwidth_tests.cpp @@ -173,6 +173,18 @@ void coarse_checks() { 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"); } + // A resized bone is held to the limit both ways, and at 1 is not resized at all. + auto big = fixture(); + big.pose.skater[5].scale = {4, 4, 4}; + big.pose.skater[6].scale = {.1f, 1, 1}; + auto held = big.pose, plain = big.pose, free = big.pose; + limit_bone_scale(held, 1.5f); + limit_bone_scale(plain, 1.f); + limit_bone_scale(free, 0.f); + check(held.skater[5].scale == std::array{1.5f, 1.5f, 1.5f} && std::abs(held.skater[6].scale[0] - 1.f / 1.5f) < 1e-5f && + plain.skater[5].scale == std::array{1, 1, 1} && plain.skater[6].scale == std::array{1, 1, 1} && + free.skater[5].scale == std::array{4, 4, 4}, + "A bone's scale was not held to the limit"); std::cout << "Coarse rotations: valid poses, within a step, small turns unchanged.\n"; } void rate_checks() { diff --git a/Server/README.txt b/Server/README.txt index dd5a177..0de4121 100644 --- a/Server/README.txt +++ b/Server/README.txt @@ -60,6 +60,12 @@ send_rate The most the server sends one player, in KB/s (default 900, Steam's relays: set higher, what is lost is resent until the connection is full of resends and half of everything is lost. Console: rate , also for the players already on. +bone_scale_limit How far a mod may resize part of a skater for the other + players (a "big head" mod and the like): the most a bone may + be scaled, 1 to 8. 1 shows every skater at the game's own + proportions; 0 (default) is no limit. The player with the + mod still sees it on their own screen. Console: + bone-scale <1-8>|off. crowd_budget The most position updates a second one player is sent (default 600, 0 for no limit). Players near each other are sent at the full rate (tps); this only matters once more are diff --git a/Server/Test/server_config_tests.cpp b/Server/Test/server_config_tests.cpp index 2312f0f..71dfd19 100644 --- a/Server/Test/server_config_tests.cpp +++ b/Server/Test/server_config_tests.cpp @@ -66,6 +66,10 @@ int run() { ServerConfig rate; rate.send_rate = 64; check(config_error(rate).find("send_rate") != std::string::npos, "A send rate too low to play with was accepted"); + check(has("bone_scale_limit") && config.bone_scale_limit == 0, "bone_scale_limit is not a new setting of no limit"); + ServerConfig scaled; + scaled.bone_scale_limit = 0.5f; + check(config_error(scaled).find("bone_scale_limit") != std::string::npos, "A bone scale limit under 1 was accepted"); ServerConfig crowd; crowd.crowd_budget = 50; check(config_error(crowd).find("crowd_budget") != std::string::npos, "A crowd budget too small to play with was accepted"); diff --git a/Server/server_config.cpp b/Server/server_config.cpp index 3c64b41..cf0a3f4 100644 --- a/Server/server_config.cpp +++ b/Server/server_config.cpp @@ -25,6 +25,7 @@ Json to_json(const ServerConfig &c) { root["max_players"] = c.max_players; root["reserved_slots"] = c.reserved_slots; root["crowd_budget"] = c.crowd_budget; + root["bone_scale_limit"] = c.bone_scale_limit; root["send_rate"] = c.send_rate; auto reserved = Json::array(); for (const auto id : c.reserved) reserved.push_back(std::to_string(id)); // as strings, like the admins @@ -132,6 +133,7 @@ ServerConfig load_config(const std::filesystem::path &file, std::vector= c.max_players) return "reserved_slots must be less than max_players, so that anyone can join at all."; if (c.reserved.size() > 1024) return "reserved holds at most 1024 players."; if (c.send_rate < 128 || c.send_rate > 16384) return "send_rate must be 128 to 16384 (KB/s for each player)."; + if (c.bone_scale_limit != 0 && !(c.bone_scale_limit >= 1.f && c.bone_scale_limit <= 8.f)) + return "bone_scale_limit must be 0 (no limit) or 1 to 8 (1: no resized body parts at all)."; if (!valid_crowd_budget(c.crowd_budget)) return "crowd_budget must be 0 (no limit) or " + std::to_string(min_crowd_budget) + " to " + std::to_string(max_crowd_budget) + "."; diff --git a/Server/server_config.h b/Server/server_config.h index 3bb7502..d1748ac 100644 --- a/Server/server_config.h +++ b/Server/server_config.h @@ -42,6 +42,9 @@ struct ServerConfig { unsigned reserved_slots = 0; // The most poses a second one player is sent (crowd_limits); 0: no limit. unsigned crowd_budget = crowd_pose_budget; + // The most a mod may resize part of a skater for the other players, as a factor (and its + // inverse the least): 1 shows every skater at the game's own proportions, 0 is no limit. + float bone_scale_limit = 0; // What the server may send each player, in KB/s (128-16384). unsigned send_rate = 900; std::vector reserved; diff --git a/Server/server_host.cpp b/Server/server_host.cpp index 4eca32e..a85a31f 100644 --- a/Server/server_host.cpp +++ b/Server/server_host.cpp @@ -46,7 +46,7 @@ std::uint64_t nonce() { constexpr std::string_view help_text = "status | net [player] | players | say | msg | msg-party | msg-admins | kick | ban [name] | unban | bans\n" "map | maps | name | password | welcome | listed on|off\n" - "voice on|off | voice-range <50-1000> | distances | crowd |off | rate \n" + "voice on|off | voice-range <50-1000> | distances | crowd |off | rate | bone-scale <1-8>|off\n" "placement everyone|admins|nobody | objects |off | clear-objects | noclip on|off | nobail on|off | boosts on|off | tuning on|off\n" "tpall [player] | tphere | votes [map|kick|tod on|off|] | vote-cancel\n" "map-pool [add|remove |clear] | rotation [|off]\n" @@ -342,7 +342,11 @@ void Host::broadcast(const Packet &packet, bool reliable, bool fresh, std::uint6 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]); + if (packet.kind == PacketKind::pose) { + // What a mod resized on its player's skater reaches the others only as far as the server allows. + if (config_.bone_scale_limit >= 1.f) limit_bone_scale(data.packet.pose, config_.bone_scale_limit); + coarsen_rotations(data.packet.pose, pose_precision[precision]); + } data.raw = encode(data.packet, true); data.wire = encode_wire_bytes(data.raw); } @@ -1570,6 +1574,22 @@ std::string Host::command(std::string_view line, std::uint64_t admin) { return changed("Each player is sent at most " + std::to_string(config_.send_rate) + " KB/s" + (applied ? "." : ": players who join from now on. Steam did not change the connections already open.")); } + if (name == "bone-scale") { + // bone-scale <1-8>|off: how far a mod may resize part of a skater for the other players. + float value{}; + const bool off = argument == "off" || argument == "0"; + const auto parsed = std::from_chars(argument.data(), argument.data() + argument.size(), value); + if (!off && (parsed.ec != std::errc{} || parsed.ptr != argument.data() + argument.size() || !(value >= 1.f && value <= 8.f))) + return "bone-scale <1-8>|off: 1 shows every skater at the game's own proportions, off allows anything (now " + + (config_.bone_scale_limit >= 1.f ? std::to_string(config_.bone_scale_limit).substr(0, 4) : std::string("off")) + ")"; + config_.bone_scale_limit = off ? 0.f : value; + // Whole states go again so that nobody keeps a reference with the old sizes in it. + for (auto &[id, guest] : guests_) + for (const auto &[other, unused] : guests_) guest->sender.forget(other, PacketKind::pose); + return changed(off ? std::string("Mods may resize skaters' body parts freely.") + : value == 1.f ? std::string("Skaters show at the game's own proportions: resized body parts are not passed on.") + : "Resized body parts show at up to " + std::to_string(value).substr(0, 4) + "x."); + } if (name == "crowd") { // crowd |off: the most one player is sent (crowd_limits). auto value = number(argument);