mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-23 13:05:54 +02:00
Server: tools can be locked for single players, and for a throwdown's players
The server can take noclip (with teleporting), No Bail, the boosts and the player's own physics tuning from one player while everyone else keeps what the server allows: for fair throwdowns on a server that allows them for free skating. Server only: a locked player gets a roster of their own with the tool off (players with the same locks share one) and permissions without "movement", so their role cannot give it back. Players without locks get exactly the roster they got before. - lock <player|SteamID64> <tool[,tool...]|all>, unlock <player|SteamID64> [<tools>|all], locks: from the console, or an admin in chat. In memory only; locks end when the player leaves. Logged as "[lock] <name> (<SteamID64>): nobail, boosts locked by <who>". - players.throwdown_locks (default []): tools locked for the players of a throwdown from its start until they quit, it ends or they leave (console: throwdown-locks <tools>|off). - Tests: Server/Test/server_lock_tests.cpp, and the key in the config tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PreNfxwh21xyPp8EA72cUN
This commit is contained in:
1 parent
0f8af62710
commit
3bfabed705
13 files changed
+720
-18
No files matched your search
+55
-13
@@ -112,6 +112,8 @@ void Host::send_permissions(Guest &guest) {
|
||||
p.role_all_commands = role->granted_all;
|
||||
p.role_commands = role->granted_commands;
|
||||
p.role_permissions = role->granted;
|
||||
// A lock on noclip, No Bail or the boosts is not lifted by the role (their roster has the rest).
|
||||
if (locks_of(guest.member.id) & lock_movement) p.role_permissions &= ~multiplayer::role_movement;
|
||||
}
|
||||
if (send_packet(guest, p, true, false)) guest.permissions_sent = roles_revision_;
|
||||
}
|
||||
@@ -500,7 +502,8 @@ void Host::flush_poses() {
|
||||
}
|
||||
// A player said which messages of poses they read in full: the poses in those are ones they hold.
|
||||
void Host::pose_ack(Guest &g, const pose_batch::Ack &ack) { g.pose_sender.ack(ack); }
|
||||
void Host::broadcast(const Packet &packet, bool reliable, bool fresh, std::uint64_t except) {
|
||||
void Host::broadcast(const Packet &packet, bool reliable, bool fresh, std::uint64_t except, const std::set<std::uint64_t> *only,
|
||||
const std::set<std::uint64_t> *skip) {
|
||||
// A pose is rounded once and kept (keep_pose); each recipient is then sent its differences
|
||||
// from a pose of this player they hold, several players to a message (flush_poses).
|
||||
auto *kept = packet.kind == PacketKind::pose ? [&]() -> Guest::KeptPose * {
|
||||
@@ -522,6 +525,7 @@ void Host::broadcast(const Packet &packet, bool reliable, bool fresh, std::uint6
|
||||
for (auto &[id, guest] : guests_) {
|
||||
auto &p = *guest;
|
||||
if (!p.handshaken || id == except || (gameplay && !p.world_ready)) continue;
|
||||
if ((only && !only->contains(id)) || (skip && skip->contains(id))) continue;
|
||||
if (packet.kind == PacketKind::voice) {
|
||||
if (!voice_policy_.accepts(packet.voice)) continue;
|
||||
// Each listener fades voices by their own hearing distance; the server
|
||||
@@ -791,17 +795,44 @@ void Host::send_roster() {
|
||||
if (m.party && std::none_of(p.members.begin(), p.members.end(),
|
||||
[&](const Member &o) { return o.party == m.party && o.party_leader; }))
|
||||
m.party_leader = true; // the first listed member of a party missing its leader
|
||||
// An announcement for one player: everyone else's roster goes without it.
|
||||
auto *only = announcement_for_ ? find(announcement_for_) : nullptr;
|
||||
if (only && !only->handshaken) only = nullptr;
|
||||
if (announcement_for_) {
|
||||
const auto shown = p.announcement;
|
||||
p.announcement = {};
|
||||
broadcast(p, true, false, announcement_for_);
|
||||
p.announcement = shown;
|
||||
if (only) send_packet(*only, p, true, false);
|
||||
} else {
|
||||
broadcast(p, true, false);
|
||||
// An announcement for one player: everyone else's roster goes without it. And a player with
|
||||
// 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{};
|
||||
auto operator<=>(const Variant &) const = default;
|
||||
};
|
||||
const Variant common{config_.noclip, config_.no_bail, config_.boosts, config_.enforce_tuning, !announcement_for_};
|
||||
std::map<Variant, std::set<std::uint64_t>> variants;
|
||||
std::set<std::uint64_t> others;
|
||||
for (const auto &[id, guest] : guests_) {
|
||||
if (!guest->handshaken) continue;
|
||||
auto variant = common;
|
||||
variant.announcement = !announcement_for_ || id == announcement_for_;
|
||||
if (const auto locks = locks_of(id)) {
|
||||
// Without "movement" (send_permissions), the tools their role gives are said here instead.
|
||||
const bool movement = (locks & lock_movement) && has(id, multiplayer::role_movement);
|
||||
if (locks & lock_movement) {
|
||||
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.enforce_tuning = config_.enforce_tuning || (locks & lock_tuning);
|
||||
}
|
||||
if (variant == common) continue;
|
||||
variants[variant].insert(id);
|
||||
others.insert(id);
|
||||
}
|
||||
const auto shown = p.announcement;
|
||||
if (announcement_for_) p.announcement = {};
|
||||
broadcast(p, true, false, 0, nullptr, others.empty() ? nullptr : &others);
|
||||
for (const auto &[variant, players] : variants) {
|
||||
p.guest_noclip = variant.noclip;
|
||||
p.guest_no_bail = variant.no_bail;
|
||||
p.guest_boosts = variant.boosts;
|
||||
p.enforce_tuning = variant.enforce_tuning;
|
||||
p.announcement = variant.announcement ? shown : multiplayer::ServerAnnouncement{};
|
||||
broadcast(p, true, false, 0, &players);
|
||||
}
|
||||
roster_dirty_ = false;
|
||||
last_roster_ = now_;
|
||||
@@ -914,6 +945,7 @@ void Host::change_map(std::string_view map) {
|
||||
}
|
||||
if (++world_ == 0) throw std::runtime_error("Map transition counter exhausted.");
|
||||
activity_.clear(); // throwdowns end with the world
|
||||
update_throwdown_locks();
|
||||
config_.map = map_setting(map);
|
||||
map_ = map_hash(map_destination(config_.map));
|
||||
map_since_ = now_;
|
||||
@@ -931,6 +963,13 @@ void Host::drop(std::uint64_t id, const std::string &reason, const std::string &
|
||||
const auto found = guests_.find(id);
|
||||
if (found == guests_.end()) return;
|
||||
const auto name = guest_name(*found->second);
|
||||
// Locks end with the visit.
|
||||
in_throwdown_.erase(id);
|
||||
if (const auto locks = locks_.find(id); locks != locks_.end()) {
|
||||
if (locks->second.all())
|
||||
log_("[lock] " + name + " (" + std::to_string(id) + "): " + lock_tools_text(locks->second.all()) + " unlocked (left the server)");
|
||||
locks_.erase(locks);
|
||||
}
|
||||
if (found->second->handshaken) {
|
||||
roster_dirty_ = true;
|
||||
log_(name + " left (" + reason + ")" + (detail.empty() ? std::string{} : " [" + detail + "]"));
|
||||
@@ -1275,8 +1314,10 @@ void Host::receive(std::uint64_t peer, std::span<const std::uint8_t> bytes, std:
|
||||
// attempts and drops go nowhere, whatever the other players' games would do with them.
|
||||
if (link->scoring_flagged) return;
|
||||
link->last_packet = now_;
|
||||
if (config_.activity_log || config_.announce_throwdowns)
|
||||
if (config_.activity_log || config_.announce_throwdowns || config_.throwdown_locks) {
|
||||
activity_.throwdown(peer, p.throwdown, link->latest_root ? &link->latest_root->position : nullptr, now_);
|
||||
update_throwdown_locks();
|
||||
}
|
||||
broadcast(p, true, false, p.source);
|
||||
return;
|
||||
}
|
||||
@@ -1861,6 +1902,7 @@ void Host::tick(std::uint64_t now) {
|
||||
tick_parks();
|
||||
sync_objects();
|
||||
activity_.tick(now_);
|
||||
update_throwdown_locks();
|
||||
if (std::exchange(vote_recount_, false)) check_vote(false);
|
||||
if (vote_ && now_ >= vote_->ends) check_vote(true);
|
||||
// A finished vote has been shown long enough.
|
||||
|
||||
Reference in new issue
Block a user