From 8044c88373dfad5ae54385ce2df26bedad8e65a7 Mon Sep 17 00:00:00 2001 From: AyeZeeBB Date: Sun, 11 Oct 2026 03:07:32 -0500 Subject: [PATCH] Hold skateboards to their size, and catch one kept on another player A hacked game was resizing its skateboard and reporting it at another player's position, so everyone saw an oversized board riding on them, and with player collision on it knocked them about. A skateboard is now held to within 10% of its size, apart from the limit on resized body parts: on a dedicated server when its scale limit is on, and in every player's game whatever sent the pose. Another player's board is solid only within 3 m of its own skater; one further off is seen and not felt. Dedicated servers also check for a board kept on another player (Server/board_check.h): one away from its own skater that stays beside one other player while that player travels 8 m over at least 3 seconds. The board is then no longer passed on to the others until it has behaved for half a minute, and the log and the admins in the game are told who. New setting anti_cheat.pinned_board and command board-check: off | warn (default) | kick. --- CMakeLists.txt | 3 + Extension/Multiplayer/Net/protocol.cpp | 10 +- Extension/Multiplayer/Net/protocol.h | 7 ++ .../Multiplayer/Remote/remote_collision.cpp | 9 +- .../Multiplayer/Test/bandwidth_tests.cpp | 10 ++ Server/README-linux.md | 4 +- Server/README.txt | 8 ++ Server/Test/board_check_tests.cpp | 95 +++++++++++++++++++ Server/board_check.h | 76 +++++++++++++++ Server/server_commands.cpp | 55 ++++++++++- Server/server_config.cpp | 5 +- Server/server_config.h | 4 + Server/server_host.cpp | 11 ++- Server/server_host.h | 5 + 14 files changed, 289 insertions(+), 13 deletions(-) create mode 100644 Server/Test/board_check_tests.cpp create mode 100644 Server/board_check.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 6b1ac582..caa7715a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -81,6 +81,9 @@ if(DINGOSDK_BUILD_MULTIPLAYER_TESTS) add_executable(dingosdk_speed_check_tests Server/Test/speed_check_tests.cpp) target_include_directories(dingosdk_speed_check_tests PRIVATE "${PROJECT_SOURCE_DIR}") add_test(NAME server_speed_check COMMAND dingosdk_speed_check_tests) + add_executable(dingosdk_board_check_tests Server/Test/board_check_tests.cpp) + target_include_directories(dingosdk_board_check_tests PRIVATE "${PROJECT_SOURCE_DIR}") + add_test(NAME server_board_check COMMAND dingosdk_board_check_tests) add_executable(dingosdk_server_config_tests Server/Test/server_config_tests.cpp Server/server_config.cpp Extension/Multiplayer/Net/protocol.cpp) target_include_directories(dingosdk_server_config_tests PRIVATE "${PROJECT_SOURCE_DIR}") diff --git a/Extension/Multiplayer/Net/protocol.cpp b/Extension/Multiplayer/Net/protocol.cpp index f51612a7..bc58aada 100644 --- a/Extension/Multiplayer/Net/protocol.cpp +++ b/Extension/Multiplayer/Net/protocol.cpp @@ -345,12 +345,12 @@ void coarsen(Transform &t, unsigned bits) noexcept { } // 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; + const auto hold = [](Transform &t, float most) { + for (float &axis : t.scale) axis = std::isfinite(axis) ? std::clamp(axis, 1.f / most, most) : 1.f; }; - hold(pose.root); - for (auto &t : pose.skater) hold(t); - for (auto &t : pose.board) hold(t); + hold(pose.root, limit); + for (auto &t : pose.skater) hold(t, limit); + for (auto &t : pose.board) hold(t, std::min(limit, board_scale_limit)); } void limit_bone_reach(Pose &pose, float limit) noexcept { if (!(limit > 0.f)) return; diff --git a/Extension/Multiplayer/Net/protocol.h b/Extension/Multiplayer/Net/protocol.h index 69b2eea6..7aefd642 100644 --- a/Extension/Multiplayer/Net/protocol.h +++ b/Extension/Multiplayer/Net/protocol.h @@ -318,6 +318,13 @@ 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. +// The skateboard is held closer, to board_scale_limit: nothing in the game resizes one, and a +// hacked client that does shows everyone a board the size of a car. +inline constexpr float board_scale_limit = 1.1f; +// How far from its skater a skateboard still stops other players (remote_collision.cpp). One +// left lying further off is seen and not felt, so a hacked client that keeps its board on +// another player cannot knock them about with it. +inline constexpr float board_solid_reach = 3.f; void limit_bone_scale(Pose &pose, float limit) noexcept; // Keeps every bone of the body and the board within `limit` metres of the bone it hangs from // (0: no limit). A bone's place is relative to its parent and, on the game's rigs, never diff --git a/Extension/Multiplayer/Remote/remote_collision.cpp b/Extension/Multiplayer/Remote/remote_collision.cpp index 74990c0e..a19509f7 100644 --- a/Extension/Multiplayer/Remote/remote_collision.cpp +++ b/Extension/Multiplayer/Remote/remote_collision.cpp @@ -266,7 +266,14 @@ void update_remote_collision(std::uintptr_t base, std::uintptr_t context, const // Upright at the skater's root, as retail's proxy stands. if (s.skater.created) move_part(n, s.skater, pose.root.position, {0, 0, 0, 1}, now); if (s.board.created) { - if (pose.board.empty()) switch_off(n, s.board); + // Solid under and beside its skater only (board_solid_reach). + float apart{}; + if (!pose.board.empty()) + for (unsigned i = 0; i < 3; ++i) { + const float d = pose.board.front().position[i] - pose.root.position[i]; + apart += d * d; + } + if (pose.board.empty() || !(apart <= board_solid_reach * board_solid_reach)) switch_off(n, s.board); else move_part(n, s.board, pose.board.front().position, pose.board.front().rotation, now); } } catch (...) {} diff --git a/Extension/Multiplayer/Test/bandwidth_tests.cpp b/Extension/Multiplayer/Test/bandwidth_tests.cpp index d1e5b67a..5d0ae20e 100644 --- a/Extension/Multiplayer/Test/bandwidth_tests.cpp +++ b/Extension/Multiplayer/Test/bandwidth_tests.cpp @@ -213,6 +213,16 @@ void coarse_checks() { 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"); + big.pose.board.resize(3); + big.pose.board[0].scale = {3, 3, 3}; + big.pose.board[2].scale = {.2f, 1, 1}; + auto board_held = big.pose, board_free = big.pose; + limit_bone_scale(board_held, 2.f); + limit_bone_scale(board_free, 0.f); + check(board_held.board[0].scale == std::array{board_scale_limit, board_scale_limit, board_scale_limit} && + std::abs(board_held.board[2].scale[0] - 1.f / board_scale_limit) < 1e-5f && board_held.skater[5].scale == std::array{2, 2, 2} && + board_free.board[0].scale == std::array{3, 3, 3}, + "A skateboard's scale was not held to its own limit"); std::cout << "Coarse rotations: valid poses, within a step, small turns unchanged.\n"; } void rate_checks() { diff --git a/Server/README-linux.md b/Server/README-linux.md index 1534c083..f848eaf7 100644 --- a/Server/README-linux.md +++ b/Server/README-linux.md @@ -69,8 +69,8 @@ cmake --build --preset linux-release -j$(nproc) ctest --test-dir build/linux --output-on-failure ``` -Expected: 9 passed (`multiplayer_parties`, `server_activity`, `server_speed_check`, -`server_config`, `server_global_bans`, `word_filter`, `multiplayer_bandwidth`, `multiplayer_lanes`, +Expected: 10 passed (`multiplayer_parties`, `server_activity`, `server_speed_check`, +`server_board_check`, `server_config`, `server_global_bans`, `word_filter`, `multiplayer_bandwidth`, `multiplayer_lanes`, `multiplayer_prediction`). Windows-only tests (voice, lobbies, UI) stay `WIN32`-gated. ## Run diff --git a/Server/README.txt b/Server/README.txt index d2a51389..088b8d11 100644 --- a/Server/README.txt +++ b/Server/README.txt @@ -169,6 +169,12 @@ speed_hack Catch players whose game runs faster than normal (Cheat throwdowns and coop challenges until their speed is normal again and tells them so (the log has it for the admins), "kick" removes them from the server, "off" does not check. +pinned_board Catch players whose skateboard stays on another player as + that player moves (a hacked game placing its board there): + "warn" (default) stops showing their board to everyone else + until it has behaved for half a minute and tells the admins + in the game and the log who it is, "kick" removes them from + the server, "off" does not check. modified_scoring Catch players whose mods change how many points tricks score (per-trick points, the scoring multipliers, the throwdown scoring logic) or how the skater handles (core @@ -412,6 +418,8 @@ and change voice, distances, placement and kicks from the Multiplayer menu. party-size <2-8> Most players in one party. afk-kick |off Remove players who have been away that long. speed-check off|warn|kick What happens to players whose game runs fast. + board-check off|warn|kick What happens to players who keep their skateboard + on another player. score-check [off|warn|kick] What happens to players whose mods change scoring or physics; with no argument, every player's result. score-allow [|remove ] Accept a scoring mod's diff --git a/Server/Test/board_check_tests.cpp b/Server/Test/board_check_tests.cpp new file mode 100644 index 00000000..60c49613 --- /dev/null +++ b/Server/Test/board_check_tests.cpp @@ -0,0 +1,95 @@ +#include "Server/board_check.h" +#include +#include +#include +#include +#include + +using dingosdk::server::BoardCheck; +namespace { +void check(bool ok, const std::string &message) { + if (ok) return; + std::cerr << message << '\n'; + std::exit(1); +} +constexpr std::uint64_t step_us = 50000; // 20 poses a second +} + +int main() { + using Position = BoardCheck::Position; + { + // Ridden: the board is under its skater whoever is near. + BoardCheck board; + for (unsigned i = 0; i < 400; ++i) { + const float x = i * 0.3f; + const Position skater{x, 0, 0}, deck{x, -0.1f, 0}; + const std::vector others{{2, {x + 1, 0, 0}}}; + check(!board.sample(i * step_us, skater, &deck, others) && !board.flagged(), "A ridden board was flagged"); + } + } + { + // Left lying: another player skates past it, and one stands on it for a minute. + BoardCheck board; + const Position skater{0, 0, 0}, deck{20, 0, 0}; + for (unsigned i = 0; i < 400; ++i) { + const std::vector others{{2, {i * 0.3f - 20, 0, 0}}}; + check(!board.sample(i * step_us, skater, &deck, others), "A board skated past was flagged"); + } + for (unsigned i = 400; i < 1600; ++i) { + const std::vector others{{2, {20.3f, 0, 0}}}; + check(!board.sample(i * step_us, skater, &deck, others), "A board stood beside was flagged"); + } + check(!board.flagged(), "A board left lying was flagged"); + } + { + // Kept on another player as they skate: flagged once they have gone 8 m with it over 3 s, + // and not before. + BoardCheck board; + const Position skater{0, 0, 0}; + unsigned flagged_at{}; + for (unsigned i = 0; i < 200 && !flagged_at; ++i) { + const float x = 10 + i * 0.15f; // 3 m/s + const Position deck{x, 0.5f, 0}; + const std::vector others{{2, {x, 0, 0}}, {3, {-30, 0, 0}}}; + if (board.sample(i * step_us, skater, &deck, others)) flagged_at = i; + } + check(flagged_at && board.flagged() && board.victim() == 2, "A board kept on another player was not flagged"); + check(flagged_at * step_us >= BoardCheck::held_us && flagged_at * 0.15f >= BoardCheck::travel - 0.2f, + "A board kept on another player was flagged too soon"); + // Back under its own skater: still flagged for a while, then cleared. + const Position home{0, -0.1f, 0}; + const std::vector far{{2, {40, 0, 0}}}; + auto now = flagged_at * step_us; + bool cleared{}; + for (unsigned i = 0; i < 20 * 29 && !cleared; ++i) cleared = board.sample(now += step_us, skater, &home, far); + check(!cleared && board.flagged(), "The flag cleared too soon"); + for (unsigned i = 0; i < 20 * 3 && !cleared; ++i) cleared = board.sample(now += step_us, skater, &home, far); + check(cleared && !board.flagged(), "The flag did not clear after the board behaved"); + } + { + // Circling a player who stays within a few metres counts as travel too; a dropped pose + // or two does not start it over. + BoardCheck board; + const Position skater{0, 0, 0}; + bool flagged{}; + for (unsigned i = 0; i < 400 && !flagged; ++i) { + if (i % 7 == 3) continue; // a lost pose + const float angle = i * 0.05f; + const Position them{20 + 2 * std::cos(angle), 0, 2 * std::sin(angle)}, deck{them[0], 1, them[2]}; + const std::vector others{{5, them}}; + flagged = board.sample(i * step_us, skater, &deck, others); + } + check(flagged, "A board kept on a circling player was not flagged"); + } + { + // No board out at all. + BoardCheck board; + const Position skater{0, 0, 0}; + for (unsigned i = 0; i < 200; ++i) { + const std::vector others{{2, {10 + i * 0.15f, 0, 0}}}; + check(!board.sample(i * step_us, skater, nullptr, others), "A player with no board out was flagged"); + } + } + std::cout << "Board check: ridden, left lying, kept on a player, circling, clearing and no board passed.\n"; + return 0; +} diff --git a/Server/board_check.h b/Server/board_check.h new file mode 100644 index 00000000..69ae3d34 --- /dev/null +++ b/Server/board_check.h @@ -0,0 +1,76 @@ +#pragma once +#include +#include +#include +#include + +namespace dingosdk::server { +// Skateboard check for one player. A player's game says where its skateboard is, and one left +// lying can be anywhere, so a hacked game can say its board is wherever another player is and +// keep it on them. Nothing a real board does looks like that: one away from its skater lies +// where it stopped. This flags a board that is away from its own skater and stays beside one +// other player while that player travels. +class BoardCheck { + public: + using Position = std::array; + struct Other { std::uint64_t id{}; Position position{}; }; + static constexpr float away = 3.f; // further than this from its skater: not ridden, held or just dropped + static constexpr float beside = 1.5f; // within this of another player + static constexpr float travel = 8.f; // that player has gone this far with it beside them... + static constexpr std::uint64_t held_us = 3000000; // ...for at least this long + static constexpr std::uint64_t slack_us = 500000; // a lost pose or two does not start it over + static constexpr std::uint64_t clear_us = 30000000; // flagged until it has behaved for this long + + // One pose of the player: their skater, their board (null when they have none out) and the + // other players on the map. Returns true when the flag changed (see flagged()). + bool sample(std::uint64_t now, const Position &skater, const Position *board, std::span others) { + const Other *with{}; + if (board && apart(*board, skater) > away * away) + for (const auto &other : others) + if (apart(*board, other.position) <= beside * beside && (!with || other.id == following_)) with = &other; + if (with && with->id == following_ && now - seen_ <= slack_us) { + seen_ = now; + // How far they have carried it: summed step by step, so circling counts as travel + // and standing still with it lying beside them does not. + const float step = std::sqrt(apart(with->position, last_)); + if (step >= 0.05f) { travelled_ += step; last_ = with->position; } + } else if (with) { + following_ = with->id; + since_ = seen_ = now; + last_ = with->position; + travelled_ = 0; + } else if (now - seen_ > slack_us) { + following_ = 0; + travelled_ = 0; + } + const bool pinned = following_ && travelled_ >= travel && now - since_ >= held_us; + if (pinned) { + behaved_since_ = 0; + if (flagged_) return false; + flagged_ = true; + victim_ = following_; + return true; + } + if (!flagged_) return false; + if (following_) { behaved_since_ = 0; return false; } // still beside someone: not yet behaving + if (!behaved_since_) behaved_since_ = now; + if (now - behaved_since_ < clear_us) return false; + flagged_ = false; + return true; + } + bool flagged() const { return flagged_; } + std::uint64_t victim() const { return victim_; } // the player it was on when flagged + void restart() { *this = {}; } + + private: + static float apart(const Position &a, const Position &b) { + float squared{}; + for (unsigned i = 0; i < 3; ++i) squared += (a[i] - b[i]) * (a[i] - b[i]); + return squared; + } + std::uint64_t following_{}, victim_{}, since_{}, seen_{}, behaved_since_{}; + Position last_{}; + float travelled_{}; + bool flagged_{}; +}; +} // namespace dingosdk::server diff --git a/Server/server_commands.cpp b/Server/server_commands.cpp index 9fb8b685..daba574b 100644 --- a/Server/server_commands.cpp +++ b/Server/server_commands.cpp @@ -24,7 +24,7 @@ constexpr std::string_view help_text = "announce | announce-to | announcements [list|add |remove |clear|interval |off]\n" "map-pool [add|remove |clear] | rotation [|off]\n" "park | park random | layer-sync on|off | layer default|on|off | tod \n" - "activity-log on|off | announce-throwdowns on|off | parties [on|off] | party-size <2-8> | afk-kick |off | speed-check off|warn|kick\n" + "activity-log on|off | announce-throwdowns on|off | parties [on|off] | party-size <2-8> | afk-kick |off | speed-check off|warn|kick | board-check off|warn|kick\n" "score-check [off|warn|kick] | score-allow [|remove ]\n" "reserved [slots | add|remove ] | admin add|remove | admins | update | quit"; } // namespace @@ -279,6 +279,20 @@ std::string Host::command(std::string_view line, std::uint64_t admin) { : value == "kick" ? "Players whose game runs fast are kicked." : "Players whose game runs fast are taken out of throwdowns and challenges."); } + if (name == "board-check") { + const auto value = lower(argument); + if (value != "off" && value != "warn" && value != "kick") + return "board-check off|warn|kick (now " + config_.board_check + ")"; + config_.board_check = value; + if (value == "off") + for (auto &[id, guest] : guests_) { + guest->board.restart(); + guest->board_hidden = false; + } + return changed(value == "off" ? "Skateboards kept on other players are no longer checked for." + : value == "kick" ? "Players who keep their skateboard on another player are kicked." + : "A skateboard kept on another player is hidden from the others, and the admins are told."); + } if (name == "score-check") { const auto value = lower(argument); if (value.empty()) { @@ -804,4 +818,43 @@ bool Host::check_speed(Guest &guest, std::uint64_t sent) { } else guest.speed_normal_since = 0; return true; } +void Host::tell_admins(const std::string &text) { + for (auto &[id, guest] : guests_) + if (guest->handshaken && is_admin(id)) send_chat(text, guest.get()); +} +// A hacked game can say its skateboard is wherever another player is, and keep it on them +// (board_check.h). The board of a player doing that is no longer passed on to the others, the +// log and the admins in the game are told who, and with "kick" the player is removed. It is +// shown again once it has behaved for half a minute. +bool Host::check_board(Guest &guest, const Pose &pose) { + if (config_.board_check == "off" || !guest.handshaken || !guest.world_ready) return true; + // The other players whose place is known now (a pose within the last second). + std::array others; + std::size_t count{}; + for (const auto &[id, other] : guests_) + if (other.get() != &guest && other->handshaken && other->world_ready && other->latest_root && other->pose_arrival && + now_ >= other->pose_arrival && now_ - other->pose_arrival <= 1000000 && count < others.size()) + others[count++] = {id, other->latest_root->position}; + if (!guest.board.sample(now_, pose.root.position, pose.board.empty() ? nullptr : &pose.board.front().position, + std::span(others.data(), count))) + return true; + const auto name = guest_name(guest); + if (!guest.board.flagged()) { + guest.board_hidden = false; + log_("[anticheat] " + name + "'s skateboard is shown to the others again."); + return true; + } + const auto *victim = find(guest.board.victim()); + const auto text = name + " (" + std::to_string(guest.member.id) + ") is keeping their skateboard on " + + (victim ? guest_name(*victim) : std::string("another player")) + " (a hacked game?)."; + log_("[anticheat] " + text); + if (config_.board_check == "kick") { + tell_admins("[anticheat] " + text + " Kicked."); + drop(guest.member.id, "Your skateboard was being kept on another player. Turn off cheats to play here."); + return false; + } + guest.board_hidden = true; + tell_admins("[anticheat] " + text + " Their board is hidden from everyone else."); + return true; +} } // namespace dingosdk::server diff --git a/Server/server_config.cpp b/Server/server_config.cpp index 1c6de464..7296af17 100644 --- a/Server/server_config.cpp +++ b/Server/server_config.cpp @@ -111,6 +111,7 @@ Layout layout(const ServerConfig &c) { auto &anti_cheat = root.section("anti_cheat"); anti_cheat.set("speed_hack", c.speed_check); + anti_cheat.set("pinned_board", c.board_check); anti_cheat.set("modified_scoring", c.score_check); auto allowed = Json::array(); for (const auto fingerprint : c.score_allow) allowed.push_back(scoring_text(fingerprint)); @@ -312,6 +313,8 @@ ServerConfig load_config(const std::filesystem::path &file, std::vectoris_array()) @@ -534,7 +537,7 @@ bool custom_command_name_free(std::string_view name) noexcept { for (const std::string_view taken : {"help", "party", "p", "w", "whisper", "tell", "poll", "vote", "yes", "y", "no", "n", "tp", "activity-log", "admin", "admins", "afk-kick", "announce", "announce-throwdowns", "announce-to", "announcements", - "ban", "bans", "bone-scale", "boosts", "boosts-allow", "chat-color", "chat-colour", "clear-objects", "crowd", + "ban", "bans", "board-check", "bone-scale", "boosts", "boosts-allow", "chat-color", "chat-colour", "clear-objects", "crowd", "distances", "effects", "kick", "layer", "layer-sync", "layers", "listed", "map", "map-pool", "maps", "msg", "msg-admins", "msg-party", "name", "net", "nobail", "nobail-allow", "noclip", "noclip-allow", "object-limit", "object-placement", "object-scaling", "objects", "park", "parties", "party-size", "password", "placement", diff --git a/Server/server_config.h b/Server/server_config.h index a3d57633..5daf244c 100644 --- a/Server/server_config.h +++ b/Server/server_config.h @@ -130,6 +130,10 @@ struct ServerConfig { // Players whose game runs fast (a speedhack; Server/speed_check.h): "warn" takes them out of // throwdowns and coop challenges and tells the admins, "kick" also removes them, "off" does not check. std::string speed_check = "warn"; + // Players whose skateboard stays on another player as they move (a hacked game; Server/ + // board_check.h): "warn" stops showing their board to the others and tells the admins, + // "kick" removes them, "off" does not check. + std::string board_check = "warn"; // Players whose mods change how tricks score (Engine/Vfs/mod_scoring.h): "warn" takes them out // of throwdowns and coop challenges (the server stops relaying theirs) and tells everyone, // "kick" removes them, "off" does not check. diff --git a/Server/server_host.cpp b/Server/server_host.cpp index e284fabe..05249276 100644 --- a/Server/server_host.cpp +++ b/Server/server_host.cpp @@ -299,9 +299,11 @@ Host::Guest::KeptPose *Host::keep_pose(Guest &from, const Packet &packet) { std::erase_if(kept, [&](const Guest::KeptPose &pose) { return now_ - pose.kept_at > pose_kept_for[std::min(pose.keep, 3)]; }); while (kept.size() >= 96) kept.pop_front(); if (!kept.empty() && kept.back().sequence == packet.sequence) return &kept.back(); - // 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 || config_.bone_reach_limit > 0.f) { + // What a mod resized on its player's skater reaches the others only as far as the server allows, + // and a board kept on another player (check_board) not at all. + if (config_.bone_scale_limit >= 1.f || config_.bone_reach_limit > 0.f || from.board_hidden) { auto pose = packet.pose; + if (from.board_hidden) pose.board.clear(); limit_bone_scale(pose, config_.bone_scale_limit); limit_bone_reach(pose, config_.bone_reach_limit); kept.push_back({packet.sequence, packet.time_us, now_, 0, pose_codec::quantize(pose)}); @@ -552,6 +554,7 @@ void Host::broadcast(const Packet &packet, bool reliable, bool fresh, std::uint6 data.packet.pose_interval_us = interval; 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 (source && source->board_hidden) data.packet.pose.board.clear(); if (config_.bone_scale_limit >= 1.f) limit_bone_scale(data.packet.pose, config_.bone_scale_limit); limit_bone_reach(data.packet.pose, config_.bone_reach_limit); } @@ -804,6 +807,7 @@ void Host::change_map(std::string_view map) { g.party_budget = old.party_budget; g.scoring_budget = old.scoring_budget; g.speeding = old.speeding; + g.board_hidden = old.board_hidden; g.scoring = old.scoring; g.scoring_mods = std::move(old.scoring_mods); g.scoring_flagged = old.scoring_flagged; @@ -892,7 +896,8 @@ bool Host::accept_data(Guest &source, const Packet &p) { active(source); } } - return check_speed(source, p.time_us); // last: a speed-check kick frees `source` + // Last: a kick by either check frees `source`. + return check_board(source, p.pose) && check_speed(source, p.time_us); } return true; } diff --git a/Server/server_host.h b/Server/server_host.h index ddc08802..29fd6a51 100644 --- a/Server/server_host.h +++ b/Server/server_host.h @@ -2,6 +2,7 @@ #include "server_activity.h" #include "worker_pool.h" #include "server_config.h" +#include "board_check.h" #include "speed_check.h" #include "Engine/Game/Multiplayer/chat_rate.h" #include "Extension/Multiplayer/Net/delta_codec.h" @@ -160,6 +161,8 @@ class Host { SpeedCheck speed; // how fast their game runs, from their pose timestamps bool speeding{}; // flagged: out of linked activities (config speed_check) std::uint64_t speed_normal_since{}; // flagged, but measuring normal again since then + BoardCheck board; // whether their skateboard is being kept on another player + bool board_hidden{}; // flagged: their board is not shown to the others (config board_check) // How their mods change trick scoring, as they reported it (Engine/Vfs/mod_scoring.h): // nothing until the report arrives, 0 for the game's own. std::optional scoring; @@ -379,6 +382,8 @@ class Host { void party_chat(Guest &, std::string_view text); // "/p ": to the sender's party only void party_left(std::uint64_t id, const std::string &name); // a player left the server bool check_speed(Guest &, std::uint64_t sent); // a pose they sent at `sent` (their clock); false: kicked + bool check_board(Guest &, const Pose &); // a pose of theirs; false: kicked + void tell_admins(const std::string &text); // a chat line to every admin in the game void check_scoring(Guest &); // after their report, or a score_check change void tick_parties(); void send_party(Guest &to, PartyAction, std::uint64_t player);