diff --git a/Engine/Game/Multiplayer/session_model.h b/Engine/Game/Multiplayer/session_model.h index a2f7ef5..be3aeda 100644 --- a/Engine/Game/Multiplayer/session_model.h +++ b/Engine/Game/Multiplayer/session_model.h @@ -11,6 +11,7 @@ #include namespace dingosdk { +constexpr unsigned max_map_rotation = 1440; // minutes a server's rotation keeps one map, at most struct MultiplayerLobby { std::uint64_t id{}, owner{}; std::string name, map, code; @@ -140,6 +141,9 @@ struct MultiplayerModel { unsigned server_ban_total{}; // For its admins: the levels (assets) the dedicated server can switch to. std::vector server_maps; + std::vector server_map_pool; // in rotation order; empty: every map + unsigned server_map_rotation{}; // minutes per map (0: off) + bool server_map_votes{}; // players may vote for a map MultiplayerHostPreferences saved_host; bool force_world_layers{}; std::uint64_t host_id{}; diff --git a/Extension/Multiplayer/Net/protocol.cpp b/Extension/Multiplayer/Net/protocol.cpp index 559ab56..545ae59 100644 --- a/Extension/Multiplayer/Net/protocol.cpp +++ b/Extension/Multiplayer/Net/protocol.cpp @@ -356,7 +356,8 @@ std::vector encode(const Packet &p, bool compact_pose, std::uint32 }))) throw std::invalid_argument("Invalid ban list"); if (p.kind == PacketKind::maps && - (!p.source || p.maps.size() > max_server_maps || + (!p.source || p.maps.size() > max_server_maps || !valid_map_pool(p.map_pool, p.maps.size()) || + p.map_rotation > max_map_rotation || std::any_of(p.maps.begin(), p.maps.end(), [](const auto &asset) { return !valid_map_asset(asset); }))) throw std::invalid_argument("Invalid server map list"); if (p.kind == PacketKind::hello && !p.text.empty() && !valid_member_name(p.text)) @@ -515,6 +516,9 @@ std::vector encode(const Packet &p, bool compact_pose, std::uint32 w.integer(asset.size(), 1); w.bytes.insert(w.bytes.end(), asset.begin(), asset.end()); } + w.integer(p.map_pool.size(), 2); + for (const auto index : p.map_pool) w.integer(index, 2); + w.integer(p.map_rotation, 2); } if (p.kind == PacketKind::bans) { w.integer(p.ban_total, 4); @@ -789,6 +793,11 @@ std::optional decode(std::span bytes) noexcept { if (!valid_map_asset(asset)) return {}; p.maps.push_back(std::move(asset)); } + const auto pooled = r.integer(2); + if (pooled > p.maps.size()) return {}; + for (std::uint64_t i = 0; i < pooled; ++i) p.map_pool.push_back(static_cast(r.integer(2))); + p.map_rotation = static_cast(r.integer(2)); + if (!valid_map_pool(p.map_pool, p.maps.size()) || p.map_rotation > max_map_rotation) return {}; } else if (p.kind == PacketKind::bans) { p.ban_total = static_cast(r.integer(4)); const auto count = r.integer(2); @@ -958,6 +967,12 @@ bool valid_map_asset(std::string_view asset) noexcept { return !asset.empty() && asset.size() <= max_map_asset && std::all_of(asset.begin(), asset.end(), [](char c) { return c > 32 && c < 127 && c != '|'; }); } +bool valid_map_pool(std::span pool, std::size_t maps) noexcept { + if (pool.size() > maps) return false; + for (std::size_t i = 0; i < pool.size(); ++i) + if (pool[i] >= maps || std::find(pool.begin(), pool.begin() + i, pool[i]) != pool.begin() + i) return false; + return true; +} bool valid_admin_text(std::string_view text) noexcept { if (text.empty() || text.size() > max_admin_text) return false; for (std::size_t at = 0; at < text.size();) { diff --git a/Extension/Multiplayer/Net/protocol.h b/Extension/Multiplayer/Net/protocol.h index a86d70f..aa1e2e7 100644 --- a/Extension/Multiplayer/Net/protocol.h +++ b/Extension/Multiplayer/Net/protocol.h @@ -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 = 41; +constexpr std::uint16_t protocol_version = 42; 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; @@ -110,6 +110,7 @@ constexpr std::size_t max_ban_rows = 256; constexpr std::size_t max_server_maps = 128, max_map_asset = 128; // A level asset as a server's map list carries it: printable ASCII, no '|'. bool valid_map_asset(std::string_view asset) noexcept; +bool valid_map_pool(std::span pool, std::size_t maps) noexcept; // distinct indices below `maps` struct Transform { std::array position{}; std::array rotation{0, 0, 0, 1}; @@ -187,6 +188,8 @@ struct Packet { std::vector bans; // bans: newest first std::uint32_t ban_total{}; // bans: how many the server has in all std::vector maps; // maps: level assets + std::vector map_pool; // maps: the pool as indices into `maps`, rotation order (empty: every map) + std::uint16_t map_rotation{}; // maps: minutes per map (0: off) std::vector throwdown; // throwdown: one encoded message (1..max_throwdown_message bytes) std::array teleport{}; // teleport: where the receiver goes (world position) std::vector tuning; // physics_tuning: 0..max_physics_tuning bytes diff --git a/Extension/Multiplayer/Session/session.cpp b/Extension/Multiplayer/Session/session.cpp index 2500dff..7661906 100644 --- a/Extension/Multiplayer/Session/session.cpp +++ b/Extension/Multiplayer/Session/session.cpp @@ -75,6 +75,8 @@ void stop(Session &s, std::string reason) { s.server_bans.clear(); s.server_ban_total = 0; s.server_maps.clear(); + s.server_map_pool.clear(); + s.server_map_rotation = 0; set_lobby_object_placement_allowed(true); s.guest_noclip = s.guest_no_bail = s.guest_boosts = true; s.enforce_tuning = true; diff --git a/Extension/Multiplayer/Session/session_internal.h b/Extension/Multiplayer/Session/session_internal.h index 46ca45a..3ac17dc 100644 --- a/Extension/Multiplayer/Session/session_internal.h +++ b/Extension/Multiplayer/Session/session_internal.h @@ -200,6 +200,8 @@ struct Session { std::vector server_bans; std::uint32_t server_ban_total{}; std::vector server_maps; // level assets the dedicated server allows + std::vector server_map_pool; // admins: the pool's assets in rotation order (empty: every map) + unsigned server_map_rotation{}; // minutes per map (0: off) std::uint64_t joined_public_lobby{}; std::array peers; // Players take the lowest free slots, so every one sits below this mark diff --git a/Extension/Multiplayer/Session/session_receive.cpp b/Extension/Multiplayer/Session/session_receive.cpp index a9d69f3..092b3fb 100644 --- a/Extension/Multiplayer/Session/session_receive.cpp +++ b/Extension/Multiplayer/Session/session_receive.cpp @@ -682,6 +682,9 @@ void networking(Session &s, const NativeFrame &local, std::uint64_t now) { if (p.kind == PacketKind::maps) { if (dedicated_host(s) && message.peer == s.host_id && p.source == s.host_id) { s.server_maps = p.maps; + s.server_map_pool.clear(); + for (const auto entry : p.map_pool) s.server_map_pool.push_back(p.maps[entry]); + s.server_map_rotation = p.map_rotation; publish(s); publish_chat(s); } diff --git a/Extension/Multiplayer/Session/session_view.cpp b/Extension/Multiplayer/Session/session_view.cpp index 21c9ac7..44dd90a 100644 --- a/Extension/Multiplayer/Session/session_view.cpp +++ b/Extension/Multiplayer/Session/session_view.cpp @@ -214,6 +214,9 @@ void publish(Session &s, const NativeFrame *local) { view.server_bans = true; view.server_ban_total = s.server_ban_total; view.server_maps = s.server_maps; + view.server_map_pool = s.server_map_pool; + view.server_map_rotation = s.server_map_rotation; + view.server_map_votes = (s.server_votes & server_vote_map) != 0; } view.chat_visible = s.chat_visible; view.chat_filter = s.chat_filter; @@ -447,6 +450,8 @@ std::vector chat_commands(const Session &s) { list.push_back({"/tod", "/tod