mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-17 13:57:05 +02:00
Limit how many objects each player can place (protocol 43)
Limit how many objects each player can place (protocol 43) - Servers and lobby hosts set how many objects each player may have placed: object_limit in ReSkateServer.json and the "objects" command, "Objects per player" in the host options. Default 100; 0 is no limit - The host and a server's admins are not limited - At the limit a player's game refuses another object, from the native object tool and the park editor, and the menu shows how many they have placed - Enforced by the host or server, which shows everyone no more than the limit of each player's layout; objects already shown keep their place - The limit travels in the roster, which takes the protocol to version 43: builds before this cannot join sessions with builds after it - The protocol tests link again (the server browser needs the identity lists since the official servers change)
This commit is contained in:
1 parent
3d259667d7
commit
07fef3820d
26 files changed
+233
-9
No files matched your search
+2
-2
@@ -142,8 +142,8 @@ if(DINGOSDK_BUILD_MULTIPLAYER_TESTS AND WIN32)
|
||||
add_executable(dingosdk_multiplayer_tests
|
||||
Extension/Multiplayer/Test/protocol_tests.cpp Extension/Multiplayer/Net/protocol.cpp Extension/Multiplayer/Remote/playback_buffers.cpp
|
||||
Extension/Multiplayer/Net/wire_codec.cpp Extension/Multiplayer/Steam/steam_server_browser.cpp Server/steam_server.cpp
|
||||
Extension/Throwdowns/throwdown_wire.cpp)
|
||||
target_link_libraries(dingosdk_multiplayer_tests PRIVATE dingosdk_lz4 dingosdk_zstd dingosdk_logging dingosdk_word_filter)
|
||||
Extension/Throwdowns/throwdown_wire.cpp Extension/Multiplayer/developer_identity.cpp)
|
||||
target_link_libraries(dingosdk_multiplayer_tests PRIVATE dingosdk_lz4 dingosdk_zstd dingosdk_logging dingosdk_word_filter dingosdk_json)
|
||||
add_test(NAME multiplayer_protocol COMMAND dingosdk_multiplayer_tests)
|
||||
add_executable(dingosdk_throwdown_relay_tests Extension/Throwdowns/Test/throwdown_relay_tests.cpp
|
||||
Extension/Throwdowns/throwdown_relay.cpp Extension/Throwdowns/throwdown_wire.cpp)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#pragma once
|
||||
#include <charconv>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
@@ -27,4 +28,21 @@ inline std::optional<ObjectPlacement> parse_object_placement(std::string_view ar
|
||||
: current == ObjectPlacement::host_only ? ObjectPlacement::nobody : ObjectPlacement::everyone;
|
||||
return {};
|
||||
}
|
||||
// How many objects each player may have placed in a session, set by the host or the server:
|
||||
// 0 is no limit beyond the protocol's own (max_object_limit). The host and a server's admins
|
||||
// are not held to it. A player over it keeps what everyone already sees of theirs; only the
|
||||
// objects past the limit are left out.
|
||||
inline constexpr unsigned max_object_limit = 1024;
|
||||
// What a server or a host starts with, until they choose otherwise.
|
||||
inline constexpr unsigned default_object_limit = 100;
|
||||
inline constexpr bool valid_object_limit(std::uint64_t value) noexcept { return value <= max_object_limit; }
|
||||
// Accepts a number from 1 to max_object_limit, and off|none|0 for no limit.
|
||||
inline std::optional<unsigned> parse_object_limit(std::string_view argument) noexcept {
|
||||
if (argument == "off" || argument == "none") return 0U;
|
||||
unsigned value{};
|
||||
const auto parsed = std::from_chars(argument.data(), argument.data() + argument.size(), value);
|
||||
if (argument.empty() || parsed.ec != std::errc{} || parsed.ptr != argument.data() + argument.size() || !valid_object_limit(value))
|
||||
return {};
|
||||
return value;
|
||||
}
|
||||
} // namespace dingosdk
|
||||
@@ -130,6 +130,9 @@ struct MultiplayerModel {
|
||||
std::vector<MultiplayerBan> bans;
|
||||
unsigned tps = multiplayer_default_tps;
|
||||
ObjectPlacement object_placement = ObjectPlacement::everyone;
|
||||
// Objects each player may have placed in this session (0: no limit), the limit this
|
||||
// player is held to (0 for the host and a server's admins), and how many they have placed.
|
||||
unsigned object_limit{}, object_limit_own{}, objects_placed{};
|
||||
// Host setting: whether guests may use noclip / No Bail (the host and server admins always may).
|
||||
bool guest_noclip{true}, guest_no_bail{true}, guest_boosts{true};
|
||||
// Host setting: guests skate with the host's physics tuning (on a dedicated server: the
|
||||
|
||||
@@ -558,6 +558,8 @@ std::vector<std::uint8_t> encode(const Packet &p, bool compact_pose, std::uint32
|
||||
if (!valid_multiplayer_tps(p.tps)) throw std::invalid_argument("Invalid session TPS");
|
||||
w.integer(p.tps, 1);
|
||||
w.integer(static_cast<std::uint8_t>(p.object_placement), 1);
|
||||
if (!valid_object_limit(p.object_limit)) throw std::invalid_argument("Invalid object limit");
|
||||
w.integer(p.object_limit, 2);
|
||||
w.integer(p.force_world_layers, 1);
|
||||
// One mode per world-layer catalog row; both peers read the same catalog
|
||||
// from the same game build.
|
||||
@@ -864,6 +866,9 @@ std::optional<Packet> decode(std::span<const std::uint8_t> bytes) noexcept {
|
||||
const auto placement = r.integer(1);
|
||||
if (!valid_object_placement(placement)) return {};
|
||||
p.object_placement = static_cast<ObjectPlacement>(placement);
|
||||
const auto object_limit = r.integer(2);
|
||||
if (!valid_object_limit(object_limit)) return {};
|
||||
p.object_limit = static_cast<unsigned>(object_limit);
|
||||
const auto forced = r.integer(1);
|
||||
if (forced > 1) return {};
|
||||
p.force_world_layers = forced != 0;
|
||||
|
||||
@@ -25,7 +25,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 = 42;
|
||||
constexpr std::uint16_t protocol_version = 43;
|
||||
constexpr std::size_t max_throwdown_message = 4096;
|
||||
// Packet::tuning: the host's SkatePhysicsTuning differences (Extension/Skater/physics_tuning.h).
|
||||
constexpr std::size_t max_physics_tuning = 16384;
|
||||
@@ -171,6 +171,8 @@ struct Packet {
|
||||
// lock their editor and native tools from it; the host enforces it by
|
||||
// freezing guest layouts (see publish_guest_objects).
|
||||
ObjectPlacement object_placement = ObjectPlacement::everyone;
|
||||
// Objects each player may have placed (object_placement.h); 0: no limit.
|
||||
unsigned object_limit{};
|
||||
// Bumped each time the host deletes all guest objects. Guests delete their
|
||||
// own session objects when it changes after their first roster.
|
||||
std::uint32_t object_clears{};
|
||||
|
||||
@@ -49,6 +49,16 @@ inline bool valid_object_chunk(const ObjectChunk &chunk) {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
// What of an owner's uploaded `layout` everyone else is shown under a limit of `limit` objects
|
||||
// (0: all of it). The objects already shown (`shared`) keep their place, so placing one too
|
||||
// many never takes away one that is there; the rest fill what room is left, oldest ID first.
|
||||
inline std::vector<NetworkObject> limited_layout(std::vector<NetworkObject> layout, const std::map<std::uint64_t, NetworkObject> &shared,
|
||||
std::size_t limit) {
|
||||
if (!limit || layout.size() <= limit) return layout;
|
||||
std::stable_partition(layout.begin(), layout.end(), [&](const NetworkObject &object) { return shared.contains(object.id); });
|
||||
layout.resize(limit);
|
||||
return layout;
|
||||
}
|
||||
// Per-owner state. A multipart replacement/delta becomes visible only after all
|
||||
// parts validate. Reliable delivery keeps parts ordered; stale revisions cannot
|
||||
// resurrect deleted objects. No wire IDs are native entity handles.
|
||||
|
||||
@@ -73,6 +73,8 @@ void stop(Session &s, std::string reason) {
|
||||
s.voice_policy = {};
|
||||
s.tps = multiplayer_default_tps;
|
||||
s.object_placement = ObjectPlacement::everyone;
|
||||
s.object_limit = 0;
|
||||
set_lobby_object_limit(0);
|
||||
s.server_admin = false;
|
||||
s.server_bans.clear();
|
||||
s.server_ban_total = 0;
|
||||
|
||||
@@ -27,6 +27,10 @@ void apply_distances(Session &s, const MultiplayerDistances &distances) {
|
||||
// previous band's hysteresis or waiting for its old send deadline.
|
||||
for (auto &peer : active_peers(s)) peer.pose_delivery = {};
|
||||
}
|
||||
void apply_object_limit(Session &s, unsigned limit) {
|
||||
s.object_limit = limit;
|
||||
set_lobby_object_limit(s.mode == Mode::host || s.server_admin ? 0 : limit);
|
||||
}
|
||||
void apply_object_placement(Session &s, ObjectPlacement policy) {
|
||||
s.object_placement = policy;
|
||||
// On a dedicated server "host only" means its admins.
|
||||
@@ -288,6 +292,21 @@ std::string edit_object_placement(Session &s, std::string_view argument) {
|
||||
: *policy == ObjectPlacement::host_only ? "Only you can place objects. Guests' objects are frozen."
|
||||
: "Object placement is disabled for everyone. Existing objects stay.";
|
||||
}
|
||||
std::string edit_object_limit(Session &s, std::string_view argument) {
|
||||
if (s.mode != Mode::host) return "Only the session host can change the object limit.";
|
||||
const auto limit = parse_object_limit(argument);
|
||||
if (!limit) return "Use a number of objects from 1 to " + std::to_string(max_object_limit) + ", or off.";
|
||||
// Guests' games stop them at the limit when the roster arrives; enforcement is the host
|
||||
// showing everyone no more than that of each guest's layout (publish_guest_objects).
|
||||
apply_object_limit(s, *limit);
|
||||
for (auto &peer : active_peers(s)) peer.shared_from = 0; // look at every layout again
|
||||
s.roster_dirty = true;
|
||||
load_host_preferences(s);
|
||||
s.host_preferences.object_limit = *limit;
|
||||
save_host_preferences(s);
|
||||
return *limit ? "Each guest can place up to " + std::to_string(*limit) + " objects."
|
||||
: std::string("Guests can place as many objects as they like.");
|
||||
}
|
||||
// Removes every guest's objects for everyone, whatever the placement policy.
|
||||
// The host's own objects are its saved park and are left alone.
|
||||
std::string clear_guest_objects(Session &s, std::string_view) {
|
||||
@@ -541,7 +560,7 @@ bool own_mark_command(std::string_view action) {
|
||||
bool queue_command(std::string_view action, std::string_view argument, std::string_view password) {
|
||||
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 != "kick" && action != "clear-objects" &&
|
||||
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 != "chat-bubbles" && action != "chat-bubbles-own" && action != "chat-bubbles-distance" &&
|
||||
action != "chat-bubbles-duration" && action != "chat-bubbles-history" &&
|
||||
@@ -581,7 +600,7 @@ std::string command(std::string_view action, std::string_view argument, std::str
|
||||
// their own: the same menu actions, sent to the server. "server" sends
|
||||
// any server console command.
|
||||
if (dedicated_host(s) && (action == "server" || (s.server_admin &&
|
||||
(action == "distances" || action == "object-placement" || action == "voice-allow" || action == "voice-range" ||
|
||||
(action == "distances" || action == "object-placement" || action == "object-limit" || action == "voice-allow" || action == "voice-range" ||
|
||||
action == "clear-objects" || action == "kick" || action == "ban" || action == "unban" ||
|
||||
action == "world-layer-sync" || action == "noclip-allow" || action == "nobail-allow" ||
|
||||
action == "tpall" || action == "tphere" || action == "boosts-allow" || action == "tuning-enforce")))) {
|
||||
@@ -716,7 +735,7 @@ std::string command(std::string_view action, std::string_view argument, std::str
|
||||
// Host-only settings check the mode themselves; guests get a refusal.
|
||||
using Setting = std::string (*)(Session &, std::string_view);
|
||||
static constexpr std::pair<std::string_view, Setting> settings[] = {
|
||||
{"object-placement", edit_object_placement}, {"kick", kick_player},
|
||||
{"object-placement", edit_object_placement}, {"object-limit", edit_object_limit}, {"kick", kick_player},
|
||||
{"noclip-allow", edit_guest_noclip}, {"nobail-allow", edit_guest_no_bail},
|
||||
{"boosts-allow", edit_guest_boosts}, {"tuning-enforce", edit_enforce_tuning},
|
||||
{"score-check", edit_score_check},
|
||||
@@ -919,6 +938,7 @@ std::string command(std::string_view action, std::string_view argument, std::str
|
||||
apply_distances(s, remembered.distances);
|
||||
s.voice_range = remembered.voice_range;
|
||||
apply_object_placement(s, remembered.placement);
|
||||
apply_object_limit(s, remembered.object_limit);
|
||||
apply_guest_tools(s, remembered.guest_noclip, remembered.guest_no_bail, remembered.guest_boosts);
|
||||
s.enforce_tuning = remembered.enforce_tuning;
|
||||
s.score_check = remembered.score_check;
|
||||
|
||||
@@ -146,6 +146,7 @@ struct Session {
|
||||
std::uint64_t next_publish{}, last_client_log{}, next_party_update{};
|
||||
MultiplayerDistances distances;
|
||||
ObjectPlacement object_placement = ObjectPlacement::everyone;
|
||||
unsigned object_limit{}; // objects each player may have placed; 0: no limit
|
||||
// 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;
|
||||
// Host: guests skate with its physics tuning. Guest: the host's roster says so (a
|
||||
@@ -242,6 +243,7 @@ struct Session {
|
||||
std::string lobby_name;
|
||||
MultiplayerDistances distances;
|
||||
ObjectPlacement placement = ObjectPlacement::everyone;
|
||||
unsigned object_limit = default_object_limit;
|
||||
float voice_range = default_voice_range;
|
||||
bool guest_noclip = true, guest_no_bail = true, guest_boosts = true;
|
||||
bool enforce_tuning = true;
|
||||
@@ -433,6 +435,8 @@ void networking(Session &s, const NativeFrame &local, std::uint64_t now);
|
||||
// session_commands.cpp
|
||||
void apply_distances(Session &s, const MultiplayerDistances &distances);
|
||||
void apply_object_placement(Session &s, ObjectPlacement policy);
|
||||
// The session's limit, and this game's own share of it: none while hosting or as a server's admin.
|
||||
void apply_object_limit(Session &s, unsigned limit);
|
||||
// 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);
|
||||
|
||||
@@ -202,6 +202,7 @@ void apply_roster(Session &s, const Packet &p, std::uint64_t now) {
|
||||
for (auto &peer : active_peers(s)) peer.pose_delivery = {};
|
||||
}
|
||||
apply_object_placement(s, p.object_placement);
|
||||
apply_object_limit(s, p.object_limit); // after server_admin, which exempts an admin
|
||||
apply_guest_tools(s, p.guest_noclip, p.guest_no_bail, p.guest_boosts);
|
||||
s.enforce_tuning = p.enforce_tuning;
|
||||
s.server_votes = dedicated_host(s) ? p.server_votes : 0;
|
||||
|
||||
@@ -26,6 +26,8 @@ void publish_guest_objects(Session &s) {
|
||||
if (peer.handshaken && peer.objects.revision() != peer.shared_from) {
|
||||
auto layout = peer.objects.layout();
|
||||
std::erase_if(layout, [&](const auto &object) { return peer.cleared.contains(object.id); });
|
||||
// The host's limit on each guest's objects, which a patched client cannot place past.
|
||||
layout = limited_layout(std::move(layout), peer.shared.objects(), s.object_limit);
|
||||
peer.shared.replace(layout);
|
||||
peer.shared_from = peer.objects.revision();
|
||||
}
|
||||
@@ -429,6 +431,7 @@ void send_roster(Session &s, std::uint64_t now) {
|
||||
p.voice_range = s.voice_range;
|
||||
p.distances = s.distances;
|
||||
p.object_placement = s.object_placement;
|
||||
p.object_limit = s.object_limit;
|
||||
p.guest_noclip = s.guest_noclip;
|
||||
p.guest_no_bail = s.guest_no_bail;
|
||||
p.guest_boosts = s.guest_boosts;
|
||||
|
||||
@@ -50,6 +50,8 @@ void load_host_preferences(Session &s) {
|
||||
p.voice_range = value->get<float>();
|
||||
if (const auto value = number("Host.ObjectPlacement"); value && *value >= 0 && valid_object_placement(static_cast<std::uint64_t>(*value)))
|
||||
p.placement = static_cast<ObjectPlacement>(*value);
|
||||
if (const auto value = number("Host.ObjectLimit"); value && *value >= 0 && valid_object_limit(static_cast<std::uint64_t>(*value)))
|
||||
p.object_limit = static_cast<unsigned>(*value);
|
||||
p.guest_noclip = profile_runtime::local_preference("Host.GuestNoclip").value_or(true);
|
||||
p.guest_no_bail = profile_runtime::local_preference("Host.GuestNoBail").value_or(true);
|
||||
p.guest_boosts = profile_runtime::local_preference("Host.GuestBoosts").value_or(true);
|
||||
@@ -70,6 +72,7 @@ void save_host_preferences(const Session &s) {
|
||||
{"Host.Distance.HalfReturn", static_cast<std::int64_t>(p.distances.half_rate_return)},
|
||||
{"Host.Distance.LowStart", static_cast<std::int64_t>(p.distances.low_rate_start)},
|
||||
{"Host.ObjectPlacement", static_cast<std::int64_t>(p.placement)},
|
||||
{"Host.ObjectLimit", static_cast<std::int64_t>(p.object_limit)},
|
||||
{"Host.GuestNoclip", p.guest_noclip},
|
||||
{"Host.GuestNoBail", p.guest_no_bail},
|
||||
{"Host.GuestBoosts", p.guest_boosts},
|
||||
@@ -147,6 +150,9 @@ void publish(Session &s, const NativeFrame *local) {
|
||||
view.tps = s.tps;
|
||||
view.distances = s.distances;
|
||||
view.object_placement = s.object_placement;
|
||||
view.object_limit = s.object_limit;
|
||||
view.object_limit_own = s.mode == Mode::host || s.server_admin ? 0 : s.object_limit; // as apply_object_limit gives this game
|
||||
view.objects_placed = s.mode == Mode::off ? 0 : static_cast<unsigned>(s.local_objects.objects().size());
|
||||
view.guest_noclip = s.guest_noclip;
|
||||
view.guest_no_bail = s.guest_no_bail;
|
||||
view.guest_boosts = s.guest_boosts;
|
||||
|
||||
@@ -29,6 +29,9 @@ void remove_remote_network_objects(std::uint64_t owner, std::uint64_t epoch) {
|
||||
std::string network_object_status() { return {}; }
|
||||
bool simulated_placement_allowed = true;
|
||||
void set_lobby_object_placement_allowed(bool allowed) { simulated_placement_allowed = allowed; }
|
||||
unsigned simulated_object_limit{};
|
||||
void set_lobby_object_limit(unsigned limit) noexcept { simulated_object_limit = limit; }
|
||||
unsigned lobby_object_limit() noexcept { return simulated_object_limit; }
|
||||
unsigned simulated_guest_wipes{};
|
||||
bool clear_lobby_guest_objects() { ++simulated_guest_wipes; return true; }
|
||||
}
|
||||
|
||||
@@ -721,6 +721,7 @@ void dedicated_server_codec() {
|
||||
roster.members = {{server, 10, "My server"}, {player, 20, "Skater", true}, {other, 30, "Other"}};
|
||||
roster.voice_range = 450;
|
||||
roster.guest_noclip = false;
|
||||
roster.object_limit = 50;
|
||||
Packet teleport;
|
||||
teleport.kind = PacketKind::teleport; teleport.session = 9; teleport.epoch = 10; teleport.map = 11; teleport.source = server;
|
||||
teleport.teleport = {612.5f, 199.25f, -1075.75f};
|
||||
@@ -744,6 +745,47 @@ void dedicated_server_codec() {
|
||||
"Server roster, admin flags or voice range lost");
|
||||
check(decoded && !decoded->guest_noclip && decoded->guest_no_bail && decoded->guest_boosts,
|
||||
"Guest noclip / No Bail / boost permissions lost");
|
||||
// The limit on each player's objects rides in the roster too; one past the protocol's own is refused.
|
||||
check(decoded && decoded->object_limit == 50, "The object limit was lost");
|
||||
{
|
||||
auto unlimited = roster;
|
||||
unlimited.object_limit = 0;
|
||||
const auto back = decode(encode(unlimited));
|
||||
check(back && back->object_limit == 0, "No object limit did not stay none");
|
||||
auto over = roster;
|
||||
over.object_limit = dingosdk::max_object_limit + 1;
|
||||
bool refused{};
|
||||
try {
|
||||
encode(over);
|
||||
} catch (const std::exception &) {
|
||||
refused = true;
|
||||
}
|
||||
check(refused, "An object limit past the protocol's was sent");
|
||||
check(dingosdk::parse_object_limit("25") == 25U && dingosdk::parse_object_limit("off") == 0U &&
|
||||
dingosdk::parse_object_limit("1024") == 1024U && !dingosdk::parse_object_limit("1025") &&
|
||||
!dingosdk::parse_object_limit("") && !dingosdk::parse_object_limit("-1") && !dingosdk::parse_object_limit("ten"),
|
||||
"Object limits were not read");
|
||||
// What a host shows of a layout under a limit: what is already shown stays, the rest fills up in order.
|
||||
const auto object = [](std::uint64_t id) {
|
||||
dingosdk::NetworkObject value;
|
||||
value.id = id;
|
||||
value.item = "own_bk_test";
|
||||
return value;
|
||||
};
|
||||
const auto ids = [](const std::vector<dingosdk::NetworkObject> &layout) {
|
||||
std::vector<std::uint64_t> out;
|
||||
for (const auto &entry : layout) out.push_back(entry.id);
|
||||
return out;
|
||||
};
|
||||
const std::vector<dingosdk::NetworkObject> uploaded{object(1), object(2), object(3), object(4), object(5)};
|
||||
std::map<std::uint64_t, dingosdk::NetworkObject> shown{{4, object(4)}, {5, object(5)}};
|
||||
check(ids(limited_layout(uploaded, {}, 0)) == std::vector<std::uint64_t>{1, 2, 3, 4, 5} &&
|
||||
ids(limited_layout(uploaded, {}, 9)) == std::vector<std::uint64_t>{1, 2, 3, 4, 5},
|
||||
"A layout within the limit was cut");
|
||||
check(ids(limited_layout(uploaded, {}, 3)) == std::vector<std::uint64_t>{1, 2, 3}, "A layout over the limit was not cut to it");
|
||||
check(ids(limited_layout(uploaded, shown, 3)) == std::vector<std::uint64_t>{4, 5, 1},
|
||||
"Objects already shown lost their place to newer ones");
|
||||
}
|
||||
// Parties ride in the roster: each has one leader and at least two members; a server is in none.
|
||||
auto party = roster;
|
||||
party.members[1].party = party.members[2].party = 7;
|
||||
|
||||
@@ -5,10 +5,26 @@
|
||||
#include <atomic>
|
||||
|
||||
namespace dingosdk {
|
||||
namespace { std::atomic<bool> placement_allowed{true}; }
|
||||
namespace {
|
||||
std::atomic<bool> placement_allowed{true};
|
||||
std::atomic<unsigned> object_limit{};
|
||||
}
|
||||
bool lobby_object_placement_allowed() noexcept { return placement_allowed.load(std::memory_order_acquire); }
|
||||
void set_lobby_object_limit(unsigned limit) noexcept { object_limit.store(limit, std::memory_order_release); }
|
||||
unsigned lobby_object_limit() noexcept { return object_limit.load(std::memory_order_acquire); }
|
||||
using namespace profile_runtime;
|
||||
using namespace profile_runtime::park_editor_detail;
|
||||
// Not for callers that hold the native mutex: they have the layout, and count it themselves.
|
||||
std::size_t lobby_object_count() {
|
||||
std::lock_guard lock(local_runtime().native_mutex);
|
||||
auto &r = placements_runtime();
|
||||
const auto found = r.document.maps.find(r.map);
|
||||
return found == r.document.maps.end() ? 0 : found->second.size();
|
||||
}
|
||||
bool lobby_object_limit_reached() {
|
||||
const auto limit = lobby_object_limit();
|
||||
return limit && lobby_object_count() >= limit;
|
||||
}
|
||||
void set_lobby_object_guest(bool guest) {
|
||||
std::lock_guard lock(local_runtime().native_mutex);
|
||||
auto &r = placements_runtime();
|
||||
|
||||
@@ -105,6 +105,9 @@ std::string edit_local_park(std::string_view operation, std::string_view map, st
|
||||
auto object = supplied;
|
||||
if (r.next_id == UINT64_MAX || target.size() >= 1024)
|
||||
throw std::runtime_error("Park object limit reached.");
|
||||
// The session's own limit on each player (the mutex is held: count here).
|
||||
if (const auto limit = lobby_object_limit(); limit && target.size() >= limit)
|
||||
throw std::runtime_error(object_limit_notice);
|
||||
object.id = r.next_id++;
|
||||
if (!e.assets || std::none_of(e.assets->begin(), e.assets->end(),
|
||||
[&](const auto &item) { return item.key == object.item; }))
|
||||
|
||||
@@ -66,6 +66,11 @@ bool queue_local_park_paste(const EditorPasteRequest &request) {
|
||||
auto target = r.document.maps[r.map];
|
||||
if (target.size() + request.objects.size() > 1024 || r.next_id > UINT64_MAX - request.objects.size())
|
||||
return false;
|
||||
// The session's own limit on each player: copies that would pass it are not made.
|
||||
if (const auto limit = lobby_object_limit(); limit && target.size() + request.objects.size() > limit) {
|
||||
e.status = object_limit_notice;
|
||||
return false;
|
||||
}
|
||||
auto next_id = r.next_id;
|
||||
for (const auto © : request.objects) {
|
||||
if (std::none_of(e.assets->begin(), e.assets->end(),
|
||||
@@ -103,6 +108,10 @@ bool queue_local_park_preview(const EditorPreviewRequest &request) {
|
||||
profile::PlacedObject object;
|
||||
std::uint64_t entity{};
|
||||
if (placing) {
|
||||
if (const auto limit = lobby_object_limit(); limit && layout.size() >= limit) {
|
||||
e.status = object_limit_notice;
|
||||
return false;
|
||||
}
|
||||
if (layout.size() >= 1024 || r.next_id == UINT64_MAX || !e.assets ||
|
||||
std::none_of(e.assets->begin(), e.assets->end(),
|
||||
[&](const auto &asset) { return asset.key == request.item; }))
|
||||
|
||||
@@ -12,6 +12,14 @@ void set_park_mod_list_visible(bool visible) noexcept;
|
||||
// on disconnect; existing placed objects remain in the world.
|
||||
void set_lobby_object_placement_allowed(bool enabled);
|
||||
bool lobby_object_placement_allowed() noexcept;
|
||||
// The session's limit on this player's objects (object_placement.h), 0 for none: the host's
|
||||
// own game and a server admin's are given none. Adding an object is refused at the limit.
|
||||
void set_lobby_object_limit(unsigned limit) noexcept;
|
||||
unsigned lobby_object_limit() noexcept;
|
||||
// Objects this player has on the current map, and whether one more would pass the limit.
|
||||
std::size_t lobby_object_count();
|
||||
bool lobby_object_limit_reached();
|
||||
inline constexpr char object_limit_notice[] = "You have placed as many objects as this session allows each player.";
|
||||
bool queue_local_park_surface(const EditorSurfaceRequest &);
|
||||
bool queue_local_park_preview(const EditorPreviewRequest &);
|
||||
bool queue_local_park_selection(const EditorSelectionRequest &);
|
||||
|
||||
@@ -311,6 +311,14 @@ void placement_message_hook(std::uintptr_t manager, const void* message) {
|
||||
PlacementBatch batch;
|
||||
batch.native = true;
|
||||
{ PreserveError preserve; read(reinterpret_cast<std::uintptr_t>(message) + 0xc, batch.type); }
|
||||
if (placement_adds_objects(batch.type) && lobby_object_placement_allowed() && lobby_object_limit_reached()) {
|
||||
// As below: the request never reaches the server, so nothing is created.
|
||||
PreserveError preserve;
|
||||
placements_runtime().status = object_limit_notice;
|
||||
dingosdk::logging::event(dingosdk::logging::Channel::objects, dingosdk::Json{
|
||||
{"event", "local_placement_over_limit"}, {"type", batch.type}, {"limit", lobby_object_limit()}}.dump().c_str());
|
||||
return;
|
||||
}
|
||||
if (placement_adds_objects(batch.type) && !lobby_object_placement_allowed()) {
|
||||
// Drop the request before the server sees it: no entity is created.
|
||||
PreserveError preserve;
|
||||
|
||||
@@ -168,7 +168,7 @@ void process_actions(const Context& context, const MultiplayerModel& model) {
|
||||
SecureZeroMemory(password.data(), password.size());
|
||||
s.feedback = "Password is too long. Use a shorter password."; continue;
|
||||
}
|
||||
if ((cmd == "kick" || cmd == "object-placement" || cmd == "clear-objects" || cmd == "world-layer-sync" ||
|
||||
if ((cmd == "kick" || cmd == "object-placement" || cmd == "object-limit" || cmd == "clear-objects" || cmd == "world-layer-sync" ||
|
||||
cmd == "voice-allow" || cmd == "noclip-allow" || cmd == "nobail-allow" || cmd == "boosts-allow" ||
|
||||
cmd == "tuning-enforce") &&
|
||||
!model.hosting) {
|
||||
@@ -382,6 +382,15 @@ void render_section(const Context& context, const MultiplayerModel& model, Secti
|
||||
? std::string("Admins only") : std::string(object_placement_name(model.object_placement));
|
||||
add_button(context, side, "editor", "Object placement: " + placement,
|
||||
model.hosting ? "object-placement" : "", "next", false, 136.f);
|
||||
{
|
||||
// The same round numbers as the overlay's list, one press each.
|
||||
static constexpr std::array<unsigned, 7> limits{0, 10, 25, 50, 100, 250, 500};
|
||||
const auto found = std::find(limits.begin(), limits.end(), model.object_limit);
|
||||
const auto next = limits[found == limits.end() ? 0 : static_cast<std::size_t>(found - limits.begin() + 1) % limits.size()];
|
||||
add_button(context, side, "object-limit",
|
||||
"Objects per player: " + (model.object_limit ? std::to_string(model.object_limit) : std::string("No limit")),
|
||||
model.hosting ? "object-limit" : "", next ? std::to_string(next) : std::string("off"), false, 136.f);
|
||||
}
|
||||
if (model.hosting)
|
||||
add_button(context, side, "clear-objects", "Delete all guest objects", "clear-objects", {}, false, 136.f);
|
||||
add_button(context, side, "guest-noclip", std::string("Guest noclip: ") + (model.guest_noclip ? "Allowed" : "Off"),
|
||||
|
||||
@@ -608,6 +608,28 @@ void session_page(SkateMenu &menu, const Model &model) {
|
||||
: "Only the host can build. Your editor and object tools are locked.")
|
||||
: mp.dedicated ? "Nobody can build. Existing objects stay."
|
||||
: "Nobody can build, including the host. Existing objects stay.");
|
||||
// How many objects each player may have placed: a few round numbers, and whatever the
|
||||
// server's file or console set that is not one of them.
|
||||
{
|
||||
static constexpr std::array<unsigned, 7> limits{0, 10, 25, 50, 100, 250, 500};
|
||||
const auto found = std::find(limits.begin(), limits.end(), mp.object_limit);
|
||||
const std::string custom = std::to_string(mp.object_limit);
|
||||
std::array<const char *, 8> labels{"No limit", "10", "25", "50", "100", "250", "500", custom.c_str()};
|
||||
int picked = static_cast<int>(found - limits.begin());
|
||||
field(menu, "Objects per player");
|
||||
ImGui::BeginDisabled(!controls_session(mp));
|
||||
ImGui::SetNextItemWidth(-FLT_MIN);
|
||||
if (ImGui::Combo("##object-limit", &picked, labels.data(), found == limits.end() ? 8 : 7) && picked < 7)
|
||||
send_private(menu, "object-limit", picked ? std::to_string(limits[static_cast<std::size_t>(picked)]) : std::string("off"), unused, false);
|
||||
ImGui::EndDisabled();
|
||||
const char *exempt = mp.dedicated ? "Admins" : "The host";
|
||||
if (!mp.object_limit) note("Everyone can place as many objects as they like.");
|
||||
else if (mp.object_limit_own)
|
||||
note(("You have placed " + std::to_string(mp.objects_placed) + " of " + std::to_string(mp.object_limit_own) +
|
||||
" objects. Delete one to place another once you reach the limit.").c_str());
|
||||
else note((std::string("Each player can have ") + std::to_string(mp.object_limit) + " objects placed. " + exempt +
|
||||
(mp.dedicated ? " are not limited." : " is not limited.")).c_str());
|
||||
}
|
||||
if (controls_session(mp)) {
|
||||
note(mp.dedicated ? "Enforced by the server, so modified clients cannot get around it."
|
||||
: "Enforced by the host, so modified clients cannot get around it.");
|
||||
|
||||
@@ -119,6 +119,9 @@ voice_chat Allow voice chat.
|
||||
voice_range How far proximity voice reaches, 50-1000 m.
|
||||
distances When far-away players update less often (metres).
|
||||
object_placement everyone, admins (only admins can build), or nobody.
|
||||
object_limit How many objects each player may have placed, 1-1024
|
||||
(default 100), or 0 for no limit. Admins are not limited.
|
||||
A player at the limit deletes one to place another.
|
||||
noclip, no_bail, Let players use noclip (and tp) / No Bail / the forward and up
|
||||
boosts boosts (default true; admins always can).
|
||||
enforce_tuning Players skate with the game's own Gameplay/SkatePhysicsTuning,
|
||||
@@ -163,6 +166,7 @@ and change voice, distances, placement and kicks from the Multiplayer menu.
|
||||
tps 20|30|60|120 voice on|off voice-range <m>
|
||||
distances <full> <half> <half-return> <low>
|
||||
placement everyone|admins|nobody clear-objects
|
||||
objects <number>|off How many objects each player may have placed.
|
||||
noclip on|off nobail on|off boosts on|off
|
||||
What players may use (admins always can).
|
||||
tuning on|off Everyone on the game's own physics tuning.
|
||||
|
||||
@@ -60,6 +60,15 @@ int run() {
|
||||
check(token_refused(tokened), "A steam_token with other characters was accepted");
|
||||
tokened.steam_token = std::string(65, 'A');
|
||||
check(token_refused(tokened), "An overlong steam_token was accepted");
|
||||
// object_limit: 100 unless set (0 is no limit), and kept on a rewrite.
|
||||
check(has("object_limit") && config.object_limit == 100, "object_limit is not a new setting of 100");
|
||||
auto limited = reloaded;
|
||||
limited.object_limit = 50;
|
||||
save_config(limited);
|
||||
check(load_config(file).object_limit == 50, "object_limit was not kept");
|
||||
limited.object_limit = 0;
|
||||
save_config(limited);
|
||||
check(load_config(file).object_limit == 0, "No object limit became the default again");
|
||||
save_config(reloaded);
|
||||
|
||||
// Server names: letters, digits, spaces and - _ / [ ] ( ) only.
|
||||
|
||||
@@ -50,6 +50,7 @@ Json to_json(const ServerConfig &c) {
|
||||
distances["low_rate_start"] = c.distances.low_rate_start;
|
||||
root["distances"] = std::move(distances);
|
||||
root["object_placement"] = placement_text(c.object_placement);
|
||||
root["object_limit"] = c.object_limit;
|
||||
root["noclip"] = c.noclip;
|
||||
root["no_bail"] = c.no_bail;
|
||||
root["boosts"] = c.boosts;
|
||||
@@ -179,6 +180,7 @@ ServerConfig load_config(const std::filesystem::path &file, std::vector<std::str
|
||||
c.votes.seconds = std::clamp(votes.value("seconds", c.votes.seconds), 10U, 300U);
|
||||
c.votes.cooldown = std::clamp(votes.value("cooldown_seconds", c.votes.cooldown), 0U, 3600U);
|
||||
}
|
||||
c.object_limit = root.value("object_limit", c.object_limit);
|
||||
const auto placement = root.value("object_placement", placement_text(c.object_placement));
|
||||
// On a dedicated server the protocol's "host only" means its admins.
|
||||
c.object_placement = placement == "nobody" ? ObjectPlacement::nobody
|
||||
@@ -250,6 +252,7 @@ std::string config_error(const ServerConfig &c) {
|
||||
if (!c.welcome.empty() && !valid_chat_text(c.welcome)) return "welcome must be one chat line (at most 200 bytes).";
|
||||
if (!valid_multiplayer_tps(c.tps)) return "tps must be 20, 30, 60 or 120.";
|
||||
if (!valid_voice_range(c.voice_range)) return "voice_range must be 50 to 1000.";
|
||||
if (!valid_object_limit(c.object_limit)) return "object_limit must be 0 (no limit) to " + std::to_string(max_object_limit) + ".";
|
||||
if (!c.distances.valid()) return "distances must be ordered: full_rate_return < half_rate_start <= half_rate_return < low_rate_start <= 10000.";
|
||||
for (unsigned lot = 0; lot < park_lots.size(); ++lot)
|
||||
if (c.parks[lot].empty() || !valid_park(lot, c.parks[lot]))
|
||||
|
||||
@@ -68,6 +68,8 @@ struct ServerConfig {
|
||||
MultiplayerDistances distances;
|
||||
// everyone, admins (only the admins below may build) or nobody.
|
||||
ObjectPlacement object_placement = ObjectPlacement::everyone;
|
||||
// Objects each player may have placed (object_placement.h); 0: no limit. Admins are not held to it.
|
||||
unsigned object_limit = default_object_limit;
|
||||
// Whether players may use noclip (and teleport) / No Bail / the boosts (admins always may).
|
||||
bool noclip = true, no_bail = true, boosts = true;
|
||||
// Players skate with the game's own physics tuning, not copies they edited.
|
||||
|
||||
+13
-1
@@ -45,7 +45,7 @@ constexpr std::string_view help_text =
|
||||
"status | players | say <text> | msg <player> <text> | msg-party <player> <text> | msg-admins <text> | kick <player> | ban <player or SteamID64> [name] | unban <SteamID64> | bans\n"
|
||||
"map <name, e.g. San Vansterdam> | maps | name <text> | password <text|off> | welcome <text|off> | listed on|off\n"
|
||||
"tps 20|30|60|120 | voice on|off | voice-range <50-1000> | distances <full> <half> <half-return> <low>\n"
|
||||
"placement everyone|admins|nobody | clear-objects | noclip on|off | nobail on|off | boosts on|off | tuning on|off\n"
|
||||
"placement everyone|admins|nobody | objects <number>|off | clear-objects | noclip on|off | nobail on|off | boosts on|off | tuning on|off\n"
|
||||
"tpall [player] | tphere <player> | votes [map|kick|tod on|off|<percent>] | vote-cancel\n"
|
||||
"map-pool [add|remove <map>|clear] | rotation [<minutes>|off]\n"
|
||||
"park <lot> <layout> | layer-sync on|off | layer <key> default|on|off | tod <time|default>\n"
|
||||
@@ -261,6 +261,7 @@ void Host::send_roster() {
|
||||
p.voice_range = config_.voice_range;
|
||||
p.distances = config_.distances;
|
||||
p.object_placement = config_.object_placement;
|
||||
p.object_limit = config_.object_limit;
|
||||
p.guest_noclip = config_.noclip;
|
||||
p.guest_no_bail = config_.no_bail;
|
||||
p.guest_boosts = config_.boosts;
|
||||
@@ -698,6 +699,8 @@ void Host::sync_objects() {
|
||||
(config_.object_placement == ObjectPlacement::host_only && is_admin(id)))) {
|
||||
auto layout = guest->objects.layout();
|
||||
std::erase_if(layout, [&](const auto &object) { return guest->cleared.contains(object.id); });
|
||||
// No more of a player's objects than the server allows each of them; admins are not limited.
|
||||
if (!is_admin(id)) layout = limited_layout(std::move(layout), guest->shared.objects(), config_.object_limit);
|
||||
if (config_.activity_log) activity_.objects(id, guest->shared.layout(), layout);
|
||||
guest->shared.replace(layout);
|
||||
guest->shared_from = guest->objects.revision();
|
||||
@@ -840,6 +843,7 @@ std::string Host::command(std::string_view line, std::uint64_t admin) {
|
||||
const auto verb = lower(action);
|
||||
// The in-game menu's names for the same settings.
|
||||
const std::string name = verb == "voice-allow" ? "voice" : verb == "object-placement" ? "placement"
|
||||
: verb == "object-limit" ? "objects"
|
||||
: verb == "world-layer-sync" ? "layer-sync" : verb == "noclip-allow" ? "noclip"
|
||||
: verb == "nobail-allow" ? "nobail" : verb == "boosts-allow" ? "boosts"
|
||||
: verb == "tuning-enforce" ? "tuning" : verb;
|
||||
@@ -1190,6 +1194,14 @@ std::string Host::command(std::string_view line, std::uint64_t admin) {
|
||||
: *policy == ObjectPlacement::host_only ? "Only admins can place objects. Everyone else's are frozen."
|
||||
: "Object placement is off. Existing objects stay.");
|
||||
}
|
||||
if (name == "objects") {
|
||||
const auto limit = parse_object_limit(argument);
|
||||
if (!limit) return "objects <1-" + std::to_string(max_object_limit) + ">|off";
|
||||
config_.object_limit = *limit;
|
||||
for (auto &[id, guest] : guests_) guest->shared_from = 0; // look at every layout again
|
||||
return changed(*limit ? "Each player can place up to " + std::to_string(*limit) + " objects. Admins are not limited."
|
||||
: std::string("Players can place as many objects as they like."));
|
||||
}
|
||||
if (name == "votes") {
|
||||
// votes | votes <map|kick|tod> on|off|<percent> | votes seconds|cooldown <n>
|
||||
const auto [what_text, value_text] = split(argument);
|
||||
|
||||
Reference in new issue
Block a user