Files
AyeZeeBB 07fef3820d 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)
2026-10-07 10:03:54 -05:00

204 lines
8.0 KiB
C++

#pragma once
#include "Engine/Game/World/network_objects.h"
#include <algorithm>
#include <cmath>
#include <map>
#include <optional>
#include <set>
#include <span>
#include <stdexcept>
namespace dingosdk::multiplayer {
inline constexpr std::size_t max_owned_objects = 1024, object_chunk_entries = 64, max_object_parts = 32;
struct ObjectChunk {
std::uint64_t base{}, revision{}; // base=0 replaces the owner's complete layout.
std::uint16_t part{}, parts{1};
std::vector<NetworkObject> objects;
std::vector<std::uint64_t> removed;
};
inline bool valid_network_object(const NetworkObject &object) {
if (!object.id || object.item.size() > 256 || !object.item.starts_with("own_bk") ||
!std::all_of(object.item.begin(), object.item.end(), [](unsigned char c) {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_';
}))
return false;
for (float value : object.position)
if (!std::isfinite(value) || std::abs(value) > 100000)
return false;
if (!std::isfinite(object.scale) || object.scale < .01f || object.scale > 100.0f)
return false;
float norm{};
for (float value : object.rotation) {
if (!std::isfinite(value))
return false;
norm += value * value;
}
return std::isfinite(norm) && norm > .98f && norm < 1.02f;
}
inline bool valid_object_chunk(const ObjectChunk &chunk) {
if (!chunk.revision || chunk.base >= chunk.revision || !chunk.parts || chunk.parts > max_object_parts ||
chunk.part >= chunk.parts || chunk.objects.size() + chunk.removed.size() > object_chunk_entries ||
(!chunk.base && !chunk.removed.empty()))
return false;
std::set<std::uint64_t> ids;
for (const auto &object : chunk.objects)
if (!valid_network_object(object) || !ids.insert(object.id).second)
return false;
for (const auto id : chunk.removed)
if (!id || !ids.insert(id).second)
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.
class ObjectState {
using Layout = std::map<std::uint64_t, NetworkObject>;
Layout objects_;
std::uint64_t revision_{};
ObjectChunk latest_;
struct Pending {
ObjectChunk change;
Layout layout;
std::set<std::uint64_t> touched;
unsigned next{};
};
std::optional<Pending> pending_;
public:
std::uint64_t revision() const {
return revision_;
}
const Layout &objects() const {
return objects_;
}
std::vector<NetworkObject> layout() const {
std::vector<NetworkObject> result;
result.reserve(objects_.size());
for (const auto &[id, object] : objects_) {
(void)id;
result.push_back(object);
}
return result;
}
void replace(std::span<const NetworkObject> objects) {
if (objects.size() > max_owned_objects)
throw std::invalid_argument("Too many owned objects");
Layout next;
for (const auto &object : objects)
if (!valid_network_object(object) || !next.emplace(object.id, object).second)
throw std::invalid_argument("Invalid owned object");
if (revision_ && next == objects_)
return;
if (revision_ == UINT64_MAX)
throw std::runtime_error("Object revision exhausted");
latest_ = {};
latest_.base = revision_;
latest_.revision = ++revision_;
for (const auto &[id, object] : next) {
const auto found = objects_.find(id);
if (found == objects_.end() || found->second != object)
latest_.objects.push_back(object);
}
for (const auto &[id, object] : objects_) {
(void)object;
if (!next.contains(id))
latest_.removed.push_back(id);
}
objects_ = std::move(next);
}
std::vector<ObjectChunk> updates(std::uint64_t since) const {
if (!revision_ || since == revision_)
return {};
ObjectChunk change;
if (since && since == latest_.base)
change = latest_;
else {
change.revision = revision_;
change.objects = layout();
}
const auto count = change.objects.size() + change.removed.size();
const auto parts =
std::max<std::size_t>(1, (count + object_chunk_entries - 1) / object_chunk_entries);
std::vector<ObjectChunk> result(parts);
for (std::size_t part = 0; part < parts; ++part) {
auto &chunk = result[part];
chunk.base = change.base;
chunk.revision = revision_;
chunk.part = static_cast<std::uint16_t>(part);
chunk.parts = static_cast<std::uint16_t>(parts);
for (auto index = part * object_chunk_entries;
index < std::min(count, (part + 1) * object_chunk_entries); ++index)
if (index < change.objects.size())
chunk.objects.push_back(change.objects[index]);
else
chunk.removed.push_back(change.removed[index - change.objects.size()]);
}
return result;
}
enum class Result { ignored, pending, applied, invalid };
Result receive(const ObjectChunk &chunk) {
if (!valid_object_chunk(chunk))
return Result::invalid;
if (chunk.revision <= revision_)
return Result::ignored;
if (!chunk.part) {
if (chunk.base && chunk.base != revision_)
return Result::invalid;
Pending next;
next.change.base = chunk.base;
next.change.revision = chunk.revision;
next.change.parts = chunk.parts;
if (chunk.base)
next.layout = objects_;
pending_ = std::move(next);
}
if (!pending_ || pending_->next != chunk.part || pending_->change.base != chunk.base ||
pending_->change.revision != chunk.revision || pending_->change.parts != chunk.parts)
return Result::invalid;
auto &next = *pending_;
for (const auto &object : chunk.objects) {
if (!next.touched.insert(object.id).second) {
pending_.reset();
return Result::invalid;
}
next.layout[object.id] = object;
next.change.objects.push_back(object);
}
for (const auto id : chunk.removed) {
if (!next.touched.insert(id).second) {
pending_.reset();
return Result::invalid;
}
next.layout.erase(id);
next.change.removed.push_back(id);
}
if (next.touched.size() > max_owned_objects * 2) {
pending_.reset();
return Result::invalid;
}
if (++next.next != chunk.parts)
return Result::pending;
if (next.layout.size() > max_owned_objects) {
pending_.reset();
return Result::invalid;
}
objects_ = std::move(next.layout);
latest_ = std::move(next.change);
revision_ = chunk.revision;
pending_.reset();
return Result::applied;
}
};
} // namespace dingosdk::multiplayer