diff --git a/Engine/Game/Multiplayer/session_tools.h b/Engine/Game/Multiplayer/session_tools.h index 3363ff62..4048e405 100644 --- a/Engine/Game/Multiplayer/session_tools.h +++ b/Engine/Game/Multiplayer/session_tools.h @@ -2,24 +2,26 @@ #include namespace dingosdk { -// Noclip (and teleporting), No Bail and the forward / up boosts as the session's host lets +// Noclip (and teleporting), No Bail, the forward / up boosts and ollies as the session's host lets // the local player use them. All are allowed outside a session, and always for the host // itself and a dedicated server's admins. namespace session_tools_detail { -inline std::atomic noclip{true}, no_bail{true}, boosts{true}; +inline std::atomic noclip{true}, no_bail{true}, boosts{true}, ollies{true}; inline std::atomic session{}; } inline bool session_noclip_allowed() noexcept { return session_tools_detail::noclip.load(std::memory_order_acquire); } inline bool session_no_bail_allowed() noexcept { return session_tools_detail::no_bail.load(std::memory_order_acquire); } inline bool session_boosts_allowed() noexcept { return session_tools_detail::boosts.load(std::memory_order_acquire); } +inline bool session_ollies_allowed() noexcept { return session_tools_detail::ollies.load(std::memory_order_acquire); } // Hosting or joined to a multiplayer session (set by the session every tick). Any thread. inline bool multiplayer_session_active() noexcept { return session_tools_detail::session.load(std::memory_order_acquire); } inline void set_multiplayer_session_active(bool active) noexcept { session_tools_detail::session.store(active, std::memory_order_release); } -inline void set_session_tools_allowed(bool noclip, bool no_bail, bool boosts) noexcept { +inline void set_session_tools_allowed(bool noclip, bool no_bail, bool boosts, bool ollies) noexcept { session_tools_detail::noclip.store(noclip, std::memory_order_release); session_tools_detail::no_bail.store(no_bail, std::memory_order_release); session_tools_detail::boosts.store(boosts, std::memory_order_release); + session_tools_detail::ollies.store(ollies, std::memory_order_release); } } // namespace dingosdk diff --git a/Extension/Multiplayer/Net/protocol.cpp b/Extension/Multiplayer/Net/protocol.cpp index edf875a8..274fe8a2 100644 --- a/Extension/Multiplayer/Net/protocol.cpp +++ b/Extension/Multiplayer/Net/protocol.cpp @@ -766,7 +766,7 @@ std::vector encode(const Packet &p, bool compact_pose, std::uint32 w.integer(static_cast(std::lround(p.voice_range)), 2); if (p.server_votes > 7) throw std::invalid_argument("Invalid server votes"); w.integer((p.guest_noclip ? 1U : 0U) | (p.guest_no_bail ? 2U : 0U) | (p.guest_boosts ? 4U : 0U) | - (static_cast(p.server_votes) << 3) | (p.enforce_tuning ? 64U : 0U), 1); + (static_cast(p.server_votes) << 3) | (p.enforce_tuning ? 64U : 0U) | (p.guest_ollies ? 128U : 0U), 1); if (p.server_polls > static_cast(ServerPolls::everyone) || p.server_custom_votes.size() > server_custom_vote_limit || !std::all_of(p.server_custom_votes.begin(), p.server_custom_votes.end(), [](const auto &v) { return valid_server_custom_vote(v); })) throw std::invalid_argument("Invalid server votes"); @@ -1223,12 +1223,12 @@ std::optional decode(std::span bytes) noexcept { p.voice_range = static_cast(r.integer(2)); if (!valid_voice_range(p.voice_range)) return {}; const auto tools = r.integer(1); - if (tools > 127) return {}; p.guest_noclip = (tools & 1) != 0; p.guest_no_bail = (tools & 2) != 0; p.guest_boosts = (tools & 4) != 0; p.server_votes = static_cast((tools >> 3) & 7); p.enforce_tuning = (tools & 64) != 0; + p.guest_ollies = (tools & 128) != 0; const auto polls = r.integer(1), custom = r.integer(1); if (polls > static_cast(ServerPolls::everyone) || custom > server_custom_vote_limit) return {}; p.server_polls = static_cast(polls); diff --git a/Extension/Multiplayer/Net/protocol.h b/Extension/Multiplayer/Net/protocol.h index 99be6cf3..a9581f7d 100644 --- a/Extension/Multiplayer/Net/protocol.h +++ b/Extension/Multiplayer/Net/protocol.h @@ -27,7 +27,7 @@ namespace dingosdk::multiplayer { constexpr std::size_t max_skater_bones = 512, max_board_bones = 64; constexpr std::size_t max_packet = 24576; constexpr std::size_t packet_header_size = 64; -constexpr std::uint16_t protocol_version = 52; +constexpr std::uint16_t protocol_version = 53; // How long a receiver keeps typing dots after the last "still typing" notice. The sender // clears them after three seconds without input; this only covers a lost "stopped" packet. constexpr std::uint64_t typing_hold_us = 4000000; @@ -349,8 +349,9 @@ struct Packet { std::string text; float voice_range = default_voice_range; // roster: how far the host forwards proximity voice // roster: whether guests may use noclip (and teleport) / No Bail / the forward and up boosts - // (the host and a dedicated server's admins always may) - bool guest_noclip = true, guest_no_bail = true, guest_boosts = true; + // (the host and a dedicated server's admins always may); and ollies, which only a dedicated + // server's lock takes away + bool guest_noclip = true, guest_no_bail = true, guest_boosts = true, guest_ollies = true; // roster: guests skate with the host's physics tuning (a dedicated server's: the game's own) bool enforce_tuning = true; // roster: the votes a dedicated server lets players start (server_vote_* bits) diff --git a/Extension/Multiplayer/Session/session.cpp b/Extension/Multiplayer/Session/session.cpp index a85d8136..bb858f17 100644 --- a/Extension/Multiplayer/Session/session.cpp +++ b/Extension/Multiplayer/Session/session.cpp @@ -103,7 +103,7 @@ void stop(Session &s, std::string reason) { s.server_map_rotation = 0; set_lobby_object_placement_allowed(true); set_lobby_object_scaling_allowed(true); - s.guest_noclip = s.guest_no_bail = s.guest_boosts = true; + s.guest_noclip = s.guest_no_bail = s.guest_boosts = s.guest_ollies = true; s.enforce_tuning = true; s.score_check = true; s.scoring_sent.reset(); @@ -122,7 +122,7 @@ void stop(Session &s, std::string reason) { s.server_votes = 0; s.server_polls = 0; s.server_custom_votes.clear(); - set_session_tools_allowed(true, true, true); + set_session_tools_allowed(true, true, true, true); s.object_clears.reset(); s.clear_pending = false; s.force_world_layers = false; diff --git a/Extension/Multiplayer/Session/session_commands.cpp b/Extension/Multiplayer/Session/session_commands.cpp index 2ae2f3f9..7fc0429f 100644 --- a/Extension/Multiplayer/Session/session_commands.cpp +++ b/Extension/Multiplayer/Session/session_commands.cpp @@ -51,12 +51,13 @@ void apply_nametags(const Session &s) { set_native_nametags_enabled(false); set_native_compass_enabled(!s.nametags); } -void apply_guest_tools(Session &s, bool noclip, bool no_bail, bool boosts) { +void apply_guest_tools(Session &s, bool noclip, bool no_bail, bool boosts, bool ollies) { s.guest_noclip = noclip; s.guest_no_bail = no_bail; s.guest_boosts = boosts; + s.guest_ollies = ollies; const bool exempt = s.mode == Mode::host || (s.role_permissions & role_movement); - set_session_tools_allowed(noclip || exempt, no_bail || exempt, boosts || exempt); + set_session_tools_allowed(noclip || exempt, no_bail || exempt, boosts || exempt, ollies || exempt); } } // namespace session_detail namespace { diff --git a/Extension/Multiplayer/Session/session_internal.h b/Extension/Multiplayer/Session/session_internal.h index 1c8fd018..7d18344e 100644 --- a/Extension/Multiplayer/Session/session_internal.h +++ b/Extension/Multiplayer/Session/session_internal.h @@ -173,6 +173,7 @@ struct Session { bool sync_effects{true}; // the dedicated server has players see each other's skater effects // What guests may use: the host's choice, or the host's roster for a guest. bool guest_noclip = true, guest_no_bail = true, guest_boosts = true; + bool guest_ollies = true; // a dedicated server's lock only: a lobby host has no such choice // Host: guests skate with its physics tuning. Guest: the host's roster says so (a // dedicated server's guests skate with the game's own). bool enforce_tuning = true; @@ -588,7 +589,7 @@ void apply_object_limit(Session &s, unsigned limit); void apply_object_scaling(Session &s, bool allowed); // Stores what guests may use and applies it to the local player (the host and a dedicated // server's admins are exempt). -void apply_guest_tools(Session &s, bool noclip, bool no_bail, bool boosts); +void apply_guest_tools(Session &s, bool noclip, bool no_bail, bool boosts, bool ollies = true); // Shows the chosen nametags: ReSkate's (and none of the game's nametags or compass // arrows), the game's own, or none. void apply_nametags(const Session &s); diff --git a/Extension/Multiplayer/Session/session_receive.cpp b/Extension/Multiplayer/Session/session_receive.cpp index ebc078db..b776143e 100644 --- a/Extension/Multiplayer/Session/session_receive.cpp +++ b/Extension/Multiplayer/Session/session_receive.cpp @@ -286,7 +286,7 @@ void apply_roster(Session &s, const Packet &p, std::uint64_t now) { apply_object_limit(s, p.object_limit); // after server_admin, which exempts an admin apply_object_scaling(s, !dedicated_host(s) || p.object_scaling); s.sync_effects = !dedicated_host(s) || p.sync_effects; - apply_guest_tools(s, p.guest_noclip, p.guest_no_bail, p.guest_boosts); + apply_guest_tools(s, p.guest_noclip, p.guest_no_bail, p.guest_boosts, p.guest_ollies); s.enforce_tuning = p.enforce_tuning; s.server_votes = dedicated_host(s) ? p.server_votes : 0; s.server_polls = dedicated_host(s) ? p.server_polls : 0; @@ -970,7 +970,7 @@ void networking(Session &s, const NativeFrame &local, std::uint64_t now) { apply_object_limit(s, s.object_limit); apply_object_placement(s, s.object_placement); apply_object_scaling(s, s.object_scaling); - apply_guest_tools(s, s.guest_noclip, s.guest_no_bail, s.guest_boosts); + apply_guest_tools(s, s.guest_noclip, s.guest_no_bail, s.guest_boosts, s.guest_ollies); publish(s); publish_chat(s); } diff --git a/Extension/Multiplayer/Test/protocol_tests.cpp b/Extension/Multiplayer/Test/protocol_tests.cpp index f331c2c4..40ba4aad 100644 --- a/Extension/Multiplayer/Test/protocol_tests.cpp +++ b/Extension/Multiplayer/Test/protocol_tests.cpp @@ -928,6 +928,13 @@ void dedicated_server_codec() { auto no_boosts = roster; no_boosts.guest_boosts = false; check(decode(encode(no_boosts)) && !decode(encode(no_boosts))->guest_boosts, "Guest boosts permission lost"); + auto no_ollies = voting; + no_ollies.guest_ollies = false; + const auto ollies_decoded = decode(encode(no_ollies)); + check(ollies_decoded && !ollies_decoded->guest_ollies && ollies_decoded->enforce_tuning && + ollies_decoded->server_votes == (server_vote_map | server_vote_time), + "Guest ollies permission lost, or it spilled into the other roster bits"); + check(decoded && decoded->guest_ollies, "Guest ollies should be allowed by default"); check(decoded && decoded->enforce_tuning, "Physics tuning enforcement lost"); auto own_tuning = voting; own_tuning.enforce_tuning = false; diff --git a/Extension/Plugins/plugin_api_world.cpp b/Extension/Plugins/plugin_api_world.cpp index 7bf62d41..4365ae1b 100644 --- a/Extension/Plugins/plugin_api_world.cpp +++ b/Extension/Plugins/plugin_api_world.cpp @@ -995,9 +995,9 @@ struct WorldApi { } // A player's permissions: player.permissions is what the server holds for them now, read-only; // permissions:clone() is a copy to change; player:apply_permissions(copy) makes it theirs. - static constexpr std::array, 5> permission_fields{{ + static constexpr std::array, 6> permission_fields{{ {"noclip", &Game::Permissions::noclip}, {"nobail", &Game::Permissions::nobail}, {"boosts", &Game::Permissions::boosts}, - {"tuning", &Game::Permissions::tuning}, {"objects", &Game::Permissions::objects}}}; + {"tuning", &Game::Permissions::tuning}, {"objects", &Game::Permissions::objects}, {"ollies", &Game::Permissions::ollies}}}; static void push_permissions(lua_State* L, const Game::Permissions& permissions, bool readonly) { lua_createtable(L, 0, static_cast(permission_fields.size())); for (const auto& [name, field] : permission_fields) { diff --git a/Extension/Plugins/plugin_game.h b/Extension/Plugins/plugin_game.h index 8b007bd9..c2ac1993 100644 --- a/Extension/Plugins/plugin_game.h +++ b/Extension/Plugins/plugin_game.h @@ -292,7 +292,7 @@ public: // A player's permissions, true where they may: what the server holds for them now, or nothing // for one it cannot hold any for. Applying makes them exactly these. struct Permissions { - bool noclip = true, nobail = true, boosts = true, tuning = true, objects = true; + bool noclip = true, nobail = true, boosts = true, tuning = true, objects = true, ollies = true; }; virtual std::optional permissions(std::uint64_t) { return std::nullopt; } virtual std::string apply_permissions(std::string_view /*plugin*/, std::uint64_t, const Permissions&) { return unavailable; } diff --git a/Extension/Trainer/trainer_jump.cpp b/Extension/Trainer/trainer_jump.cpp index f62ae921..3f80391d 100644 --- a/Extension/Trainer/trainer_jump.cpp +++ b/Extension/Trainer/trainer_jump.cpp @@ -1,6 +1,7 @@ #include "trainer_jump.h" #include "Engine/Core/Hooks/hooks.h" #include "Engine/Core/Log/logging.h" +#include "Engine/Game/Multiplayer/session_tools.h" #include "Extension/Skater/no_bail.h" #include #include @@ -15,8 +16,9 @@ namespace { // the vector at parameters + 0x80. Measured with a scripted controller: // - a no comply calls it from 0x482f646 on entering the pop (state 200) after a crouch // (state 103) of about 0.20 s; an ollie crouches for 0.08 s and launches elsewhere; -// - a boneless calls it from 0x47691f8 while the foot is planted (state 602). -constexpr std::uintptr_t set_trajectory = 0x4752aa0, no_comply_call = 0x482f646, boneless_call = 0x47691f8; +// - a boneless calls it from 0x47691f8 while the foot is planted (state 602); +// - a flat-ground ollie calls it from 0x4824f7f (Engine/Game/Build/20260929/grind_exit.h). +constexpr std::uintptr_t set_trajectory = 0x4752aa0, no_comply_call = 0x482f646, boneless_call = 0x47691f8, ollie_call = 0x4824f7f; constexpr std::array set_trajectory_prefix{0x40, 0x53, 0x48, 0x83, 0xec, 0x20, 0x80, 0xb9, 0x88, 0xbb, 0x00, 0x00, 0x00, 0x48, 0x8b, 0xd9, 0x0f, 0x85, 0xf6, 0x00, 0x00, 0x00, 0xc5, 0xfc, 0x28, 0x05, 0x62, 0x7d, 0xe8, 0x01, 0xc5, 0xfc}; @@ -53,6 +55,9 @@ void set_trajectory_hook(std::uintptr_t trajectory, float *parameters) { parameters[velocity_y + 1] = velocity[2]; } } + // A dedicated server's ollie lock (session_tools.h): the ollie launches with no upward speed, + // so the skater stays on the ground. + if (parameters && caller == ollie_call + 5 && !session_ollies_allowed() && parameters[velocity_y] > 0) parameters[velocity_y] = 0; if (parameters && (caller == no_comply_call + 5 || caller == boneless_call + 5)) { const auto watch = watched_physics_state(); // the local skater's Trick trick{}; diff --git a/Server/Test/server_config_tests.cpp b/Server/Test/server_config_tests.cpp index fd878eae..4df8a2d0 100644 --- a/Server/Test/server_config_tests.cpp +++ b/Server/Test/server_config_tests.cpp @@ -183,10 +183,10 @@ int run() { check(load_config(file).throwdown_locks == (lock_nobail | lock_tuning), "Throwdown locks lost"); std::ofstream(file, std::ios::binary) << R"({"players": {"throwdown_locks": ["boosts", "jetpack", 3, "teleport"]}})"; check(load_config(file).throwdown_locks == (lock_boosts | lock_noclip), "Bad throwdown locks not cleaned"); - check(parse_lock_tools("all") == lock_all && parse_lock_tools(" NoBail , tuning") == (lock_nobail | lock_tuning) && + check(parse_lock_tools("all") == lock_all && parse_lock_tools(" NoBail , tuning") == (lock_nobail | lock_tuning) && parse_lock_tools("ollies") == lock_ollies && !parse_lock_tools("") && !parse_lock_tools("nobail,") && !parse_lock_tools("fly"), "Lock tool names not read"); - check(lock_tools_text(lock_all) == "noclip, nobail, boosts, tuning" && lock_tools_text(0) == "none", "Lock tool names not written"); + check(lock_tools_text(lock_all) == "noclip, nobail, boosts, tuning, ollies" && lock_tools_text(0) == "none", "Lock tool names not written"); // A config from before the sections: every setting is read from where it was, under the name // it had, and the file is written back in sections, in reading order, with nothing called new diff --git a/Server/Test/server_lock_tests.cpp b/Server/Test/server_lock_tests.cpp index a9b81510..195b603f 100644 --- a/Server/Test/server_lock_tests.cpp +++ b/Server/Test/server_lock_tests.cpp @@ -331,7 +331,7 @@ int run() { check(has(b.chat, "The server unlocked for you: nobail, boosts."), "Bailey was not told of the unlock"); command("unlock Dana all"); check(free(d) && d.permissions && (*d.permissions & role_movement), "Dana did not get their roster and movement back"); - check(command("unlock Dana") == "Dana has none of noclip, nobail, boosts, tuning, objects locked.", "unlock with nothing locked"); + check(command("unlock Dana") == "Dana has none of noclip, nobail, boosts, tuning, ollies, objects locked.", "unlock with nothing locked"); check(command("lock Dana objects") == "Locked for Dana: objects.", "objects did not lock by name"); check(command("unlock Dana") == "Unlocked for Dana: objects.", "a bare unlock did not take back objects"); diff --git a/Server/server_commands.cpp b/Server/server_commands.cpp index f0dad033..15280049 100644 --- a/Server/server_commands.cpp +++ b/Server/server_commands.cpp @@ -954,7 +954,7 @@ std::string Host::command(std::string_view line, std::uint64_t admin) { who = trim(who.substr(0, space)); } if (who.empty() || !tools) - return lock ? "lock (tools: noclip, nobail, boosts, tuning, objects)" + return lock ? "lock (tools: noclip, nobail, boosts, tuning, ollies, objects)" : "unlock []"; auto *guest = target(who); if (guest && !guest->handshaken) guest = nullptr; @@ -996,7 +996,7 @@ std::string Host::command(std::string_view line, std::uint64_t admin) { if (argument.empty()) return "throwdown-locks |off (now " + (config_.throwdown_locks ? lock_tools_text(config_.throwdown_locks) : std::string("off")) + ")"; const auto tools = lower(argument) == "off" ? std::optional(0) : parse_lock_tools(argument); - if (!tools) return "throwdown-locks |off (tools: noclip, nobail, boosts, tuning, objects)"; + if (!tools) return "throwdown-locks |off (tools: noclip, nobail, boosts, tuning, ollies, objects)"; // The players of a running throwdown get the new set at once: what it no longer locks // unlocked, what it now locks locked (none of it when it is turned off or on: then // update_throwdown_locks ends or starts theirs). diff --git a/Server/server_config.cpp b/Server/server_config.cpp index e62fb9f0..8a432c69 100644 --- a/Server/server_config.cpp +++ b/Server/server_config.cpp @@ -132,7 +132,7 @@ Layout layout(const ServerConfig &c) { players.set("sync_effects", c.sync_effects); players.set("announce_throwdowns", c.announce_throwdowns); auto locks = Json::array(); - for (const auto bit : {lock_noclip, lock_nobail, lock_boosts, lock_tuning}) + for (const auto bit : {lock_noclip, lock_nobail, lock_boosts, lock_tuning, lock_ollies}) if (c.throwdown_locks & bit) locks.push_back(lock_tools_text(bit)); players.set("throwdown_locks", std::move(locks)); @@ -716,6 +716,7 @@ std::optional parse_lock_tools(std::string_view text) { else if (lower == "nobail") tools |= lock_nobail; else if (lower == "boosts") tools |= lock_boosts; else if (lower == "tuning") tools |= lock_tuning; + else if (lower == "ollies") tools |= lock_ollies; else if (lower == "objects") tools |= lock_objects; else if (lower == "all") tools |= lock_all; else return std::nullopt; @@ -727,7 +728,7 @@ std::string lock_tools_text(std::uint8_t tools) { std::string text; for (const auto &[bit, name] : {std::pair{lock_noclip, "noclip"}, std::pair{lock_nobail, "nobail"}, std::pair{lock_boosts, "boosts"}, std::pair{lock_tuning, "tuning"}, - std::pair{lock_objects, "objects"}}) + std::pair{lock_ollies, "ollies"}, std::pair{lock_objects, "objects"}}) if (tools & bit) text += (text.empty() ? "" : ", ") + std::string(name); return text.empty() ? "none" : text; } diff --git a/Server/server_config.h b/Server/server_config.h index d29aafc2..65e47662 100644 --- a/Server/server_config.h +++ b/Server/server_config.h @@ -90,8 +90,8 @@ inline constexpr unsigned dedicated_tps = 20; // The tools the server can take away from single players (lock, throwdown_locks): noclip with // teleporting, No Bail, the boosts, their own physics tuning, and placing objects. "all" is the // first four, as throwdown_locks has always read it: objects are only ever locked by name. -inline constexpr std::uint8_t lock_noclip = 1, lock_nobail = 2, lock_boosts = 4, lock_tuning = 8, lock_objects = 16; -inline constexpr std::uint8_t lock_movement = lock_noclip | lock_nobail | lock_boosts; // what the role's "movement" gives +inline constexpr std::uint8_t lock_noclip = 1, lock_nobail = 2, lock_boosts = 4, lock_tuning = 8, lock_objects = 16, lock_ollies = 32; +inline constexpr std::uint8_t lock_movement = lock_noclip | lock_nobail | lock_boosts | lock_ollies; // what the role's "movement" gives inline constexpr std::uint8_t lock_all = lock_movement | lock_tuning; struct ServerConfig { std::filesystem::path file; diff --git a/Server/server_host.cpp b/Server/server_host.cpp index 4d3d6eb0..bbef2e72 100644 --- a/Server/server_host.cpp +++ b/Server/server_host.cpp @@ -814,10 +814,10 @@ void Host::send_roster() { // locked tools is told those are off for them. Everyone else gets the same roster; players // whose roster differs in the same way share one. struct Variant { - bool noclip{}, no_bail{}, boosts{}, enforce_tuning{}, announcement{}; + bool noclip{}, no_bail{}, boosts{}, ollies{}, enforce_tuning{}, announcement{}; auto operator<=>(const Variant &) const = default; }; - const Variant common{config_.noclip, config_.no_bail, config_.boosts, config_.enforce_tuning, !announcement_for_}; + const Variant common{config_.noclip, config_.no_bail, config_.boosts, true, config_.enforce_tuning, !announcement_for_}; std::map> variants; std::set others; for (const auto &[id, guest] : guests_) { @@ -831,6 +831,7 @@ void Host::send_roster() { variant.noclip = !(locks & lock_noclip) && (config_.noclip || movement); variant.no_bail = !(locks & lock_nobail) && (config_.no_bail || movement); variant.boosts = !(locks & lock_boosts) && (config_.boosts || movement); + variant.ollies = !(locks & lock_ollies); // no setting turns ollies off: only a lock } variant.enforce_tuning = config_.enforce_tuning || (locks & lock_tuning); } @@ -845,6 +846,7 @@ void Host::send_roster() { p.guest_noclip = variant.noclip; p.guest_no_bail = variant.no_bail; p.guest_boosts = variant.boosts; + p.guest_ollies = variant.ollies; p.enforce_tuning = variant.enforce_tuning; p.announcement = variant.announcement ? shown : multiplayer::ServerAnnouncement{}; broadcast(p, true, false, 0, &players); diff --git a/Server/server_plugins.cpp b/Server/server_plugins.cpp index edd1a643..790bfa60 100644 --- a/Server/server_plugins.cpp +++ b/Server/server_plugins.cpp @@ -70,12 +70,12 @@ class ServerGame final : public Game { if (!multiplayer::individual_steam_id(player)) return std::nullopt; const auto locks = host_.player_locks(player); return Permissions{!(locks & lock_noclip), !(locks & lock_nobail), !(locks & lock_boosts), !(locks & lock_tuning), - !(locks & lock_objects)}; + !(locks & lock_objects), !(locks & lock_ollies)}; } std::string apply_permissions(std::string_view plugin, std::uint64_t player, const Permissions &p) override { if (!multiplayer::individual_steam_id(player)) return "not a player's SteamID64"; const auto locks = static_cast((p.noclip ? 0 : lock_noclip) | (p.nobail ? 0 : lock_nobail) | (p.boosts ? 0 : lock_boosts) | - (p.tuning ? 0 : lock_tuning) | (p.objects ? 0 : lock_objects)); + (p.tuning ? 0 : lock_tuning) | (p.objects ? 0 : lock_objects) | (p.ollies ? 0 : lock_ollies)); host_.set_locks(player, locks, "plugin " + std::string(plugin)); return {}; }