Multiplayer: dedicated servers require plugins, and players get them and the map on one card

A server lists Thunderstore packages in plugins.required ("Owner-Name", or "Owner-Name-1.2.3"
for at least that version, at most 16). It checks each on Thunderstore as it starts and leaves
out one not listed as a plugin, and sends the rest with its map offer (protocol 55).

A joining player's game checks them before the server's map loads. Those missing, switched off
or too old go on one card with the map when that is missing too, so the player is asked once.
GET ALL downloads and installs each, switches on any that were off, runs one live merge when
any brings game data, and joins; CANCEL leaves. The game fetches a package only when
Thunderstore lists it as a plugin and it installs as one (a reskate-plugin.json naming Luau
code), whatever a server asks; anything else is taken out again, so a cosmetic can never be
required.

map_download handles a list of packages instead of one map, and the Thunderstore chooser takes
the category it needs. A map alone keeps its card as it was.
This commit is contained in:
AyeZeeBB committed 2026-10-24 12:00:09 -05:00
1 parent e2d12a701d
commit 55a7342dc6
26 files changed
+1051 -255

No files matched your search

+31
View File
@@ -480,6 +480,8 @@ std::vector<std::uint8_t> encode(const Packet &p, bool compact_pose, std::uint32
throw std::invalid_argument("Invalid map name");
if ((p.kind == PacketKind::map_offer || p.kind == PacketKind::world_state) && !valid_map_package(p.map_package))
throw std::invalid_argument("Invalid map package");
if (!p.required_plugins.empty() && (p.kind != PacketKind::map_offer || !valid_required_plugins(p.required_plugins)))
throw std::invalid_argument("Invalid required plugins");
if (p.map_fetching && (p.world_ready || (p.kind != PacketKind::map_request && p.kind != PacketKind::world_ready)))
throw std::invalid_argument("Invalid map fetch notice");
if (p.kind == PacketKind::world_state &&
@@ -530,6 +532,11 @@ std::vector<std::uint8_t> encode(const Packet &p, bool compact_pose, std::uint32
w.bytes.insert(w.bytes.end(), p.map_label.begin(), p.map_label.end());
w.integer(p.map_package.size(), 1);
w.bytes.insert(w.bytes.end(), p.map_package.begin(), p.map_package.end());
w.integer(p.required_plugins.size(), 1);
for (const auto &plugin : p.required_plugins) {
w.integer(plugin.size(), 1);
w.bytes.insert(w.bytes.end(), plugin.begin(), plugin.end());
}
}
if (p.kind == PacketKind::map_request) w.integer(p.map_fetching ? 1 : 0, 1);
if (p.kind == PacketKind::hello) {
@@ -874,6 +881,14 @@ std::optional<Packet> decode(std::span<const std::uint8_t> bytes) noexcept {
r.at += static_cast<std::size_t>(length);
if (!valid_map_destination(p.destination) || p.map != map_hash(p.destination) || !read_map_label(r, bytes, p))
return {};
const auto plugins = r.integer(1);
if (plugins > max_required_plugins) return {};
for (std::uint64_t i = 0; i < plugins; ++i) {
auto plugin = read_text(r, bytes, 1, max_map_package);
if (!plugin) return {};
p.required_plugins.push_back(std::move(*plugin));
}
if (!valid_required_plugins(p.required_plugins)) return {};
}
if (p.kind == PacketKind::map_request) {
const auto fetching = r.integer(1);
@@ -1422,6 +1437,22 @@ bool valid_map_package(std::string_view package) noexcept {
if (!package_word(package.substr(0, first)) || !package_word(rest.substr(0, second))) return false;
return second == std::string_view::npos || package_version(rest.substr(second + 1));
}
bool valid_required_plugins(const std::vector<std::string> &packages) noexcept {
if (packages.size() > max_required_plugins) return false;
for (std::size_t i = 0; i < packages.size(); ++i) {
if (packages[i].empty() || !valid_map_package(packages[i])) return false;
// (Twice is twice whatever the version or the case: "A-B" and "a-b-1.0.0" are one package.)
const auto name = packages[i].substr(0, packages[i].size() - map_package_version(packages[i]).size());
for (std::size_t k = 0; k < i; ++k) {
const auto other = packages[k].substr(0, packages[k].size() - map_package_version(packages[k]).size());
const auto bare = [](std::string_view text) { return text.ends_with('-') ? text.substr(0, text.size() - 1) : text; };
const auto a = bare(name), b = bare(other);
if (a.size() == b.size() && std::equal(a.begin(), a.end(), b.begin(), [](char x, char y) { return (x | 0x20) == (y | 0x20); }))
return false;
}
}
return true;
}
std::string map_package_name(std::string_view folder, std::string_view version) {
const auto dash = folder.find('-');
if (dash == std::string_view::npos || !package_word(folder.substr(0, dash))) return {};