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);