// Exercises the real session admission/routing implementation with an in-memory // Steam transport. Native actors/UI are stubbed; no game process is opened. #include "Extension/Multiplayer/Session/session.cpp" #include "Extension/Multiplayer/Session/session_view.cpp" #include "Extension/Multiplayer/Session/session_send.cpp" #include "Extension/Multiplayer/Session/session_receive.cpp" #include "Extension/Multiplayer/Session/session_commands.cpp" #include "Extension/Multiplayer/Session/session_party.cpp" #include "Engine/Game/World/park_randomization.h" #include #include #include namespace dingosdk { bool read_local_camera_transform(std::uintptr_t, std::uintptr_t, std::array&) noexcept { return false; } ParksModel simulated_parks; std::vector>* simulated_park_receives{}; std::uint64_t simulated_park_receiver{}; ParksModel local_profile_parks() { return simulated_parks; } void set_lobby_park_mode(bool, bool) {} bool simulated_object_guest{}; void set_lobby_object_guest(bool guest) { simulated_object_guest = guest; } WorldLayersModel simulated_host_layers; WorldLayersModel local_profile_world_layers() { return simulated_host_layers; } void apply_host_world_layers(bool, const WorldLayerChoices &) {} void apply_host_park_choices(const ParkChoices& choices) { if (simulated_park_receives) simulated_park_receives->emplace_back(simulated_park_receiver, choices); } std::optional capture_local_network_objects() { return {}; } void set_remote_network_objects(std::string_view, std::span) {} void clear_remote_network_objects() {} void set_network_object_owner_solid(std::uint64_t, bool) {} std::vector> removed_object_owners; void remove_remote_network_objects(std::uint64_t owner, std::uint64_t epoch) { if (owner && epoch) removed_object_owners.emplace_back(owner, epoch); } std::string network_object_status() { return {}; } bool simulated_placement_allowed = true; void set_lobby_object_placement_allowed(bool allowed) { simulated_placement_allowed = allowed; } bool simulated_scaling_allowed = true; void set_lobby_object_scaling_allowed(bool allowed) noexcept { simulated_scaling_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; } } namespace dingosdk { // (The session groups its hook installs; no hook is installed here.) HookBatch::HookBatch() noexcept {} HookBatch::~HookBatch() {} bool teleport_local_skater(const std::array&) { return true; } ControllerBindingsModel local_profile_controller_bindings() { return {}; } void update_board_lock(std::uintptr_t, std::uintptr_t, bool) noexcept {} void update_developer_hoodie(std::uintptr_t, std::uintptr_t, std::uint64_t, std::uint64_t, DeveloperHoodieState &, const multiplayer::MarkStyles &) noexcept {} void update_developer_board(std::uintptr_t, std::uintptr_t, std::uint64_t, std::uint64_t, DeveloperBoardState &, const multiplayer::MarkStyles &) noexcept {} } namespace dingosdk::multiplayer { // The backend's lists are not read here: a check lists the players it means. std::set> simulated_identities; bool identity_listed(std::uint64_t id, IdentityList list) noexcept { return simulated_identities.contains({id, list}); } bool local_allows_player_collision(std::uintptr_t, std::uintptr_t) noexcept { return false; } void update_remote_collision(std::uintptr_t, std::uintptr_t, const Pose &, bool, std::uint64_t) noexcept {} void expire_remote_collision(std::uintptr_t, std::uint64_t) noexcept {} } namespace dingosdk::mods { // Every simulated player runs the same process, so they share one scoring state. ScoringState simulated_scoring{true, 0, {}}; ScoringState scoring_state() noexcept { return simulated_scoring; } } namespace dingosdk::physics_tuning { void prepare() noexcept {} std::optional local_differences(std::uintptr_t, std::size_t) { return {}; } void enforce(std::uintptr_t, std::uintptr_t, std::span) noexcept {} void release(std::uintptr_t) noexcept {} std::string status() { return {}; } } namespace dingosdk::profile_runtime { void install_script_error_log(std::uintptr_t) noexcept {} void install_board_wear_hold(std::uintptr_t) noexcept {} bool simulated_profile_store_ready = true; std::map> simulated_profile; bool local_profile_store_ready() noexcept { return simulated_profile_store_ready; } std::optional local_preference(std::string_view key) noexcept { const auto value = local_value(key); if (value && value->is_boolean()) return value->get(); return {}; } void set_local_preference(std::string_view key, bool value) noexcept { set_local_values({{std::string(key), value}}); } std::optional local_value(std::string_view key) noexcept { if (!simulated_profile_store_ready) return {}; const auto it = simulated_profile.find("ReSkate." + std::string(key)); if (it == simulated_profile.end()) return {}; return it->second; } void set_local_values(const std::vector>& values) noexcept { if (!simulated_profile_store_ready) return; for (const auto& [key, value] : values) simulated_profile["ReSkate." + key] = value; } } namespace dingosdk::multiplayer { // Voice capture and playback need audio devices; the mesh carries no voice here. struct VoiceChat::Impl {}; VoiceChat::VoiceChat() = default; VoiceChat::~VoiceChat() = default; void VoiceChat::configure(VoiceSettings) {} VoiceModel VoiceChat::model() const { return {}; } void VoiceChat::mute(std::uint64_t, bool) {} void VoiceChat::volume(std::uint64_t, float) {} void VoiceChat::update(VoiceScene) {} void VoiceChat::reset() {} void VoiceChat::process_native(std::uintptr_t) noexcept {} void VoiceChat::receive(const Packet&) {} std::vector VoiceChat::take_capture() { return {}; } } namespace dingosdk::multiplayer { struct SimulatedNetwork { static inline std::set> blocked; static inline bool lose_unreliable{}, lose_reports{}; static inline bool slow_join{}; static inline bool cosmetics_first{}, audit_lanes{}; static inline std::uint64_t hold_control_to{}; static inline std::array lane_sends{}; static inline std::map queues; // What a message's arrival time is (TransportMessage::arrived); 0: the transport does not say. static inline std::uint64_t clock{}; static std::pair pair(std::uint64_t a, std::uint64_t b) { return {std::min(a, b), std::max(a, b)}; } }; struct SteamTransport::Impl { static inline std::uint64_t next_id = 76561198000000000ULL; static inline std::map bus; TransportStatus state; unsigned capacity = max_players; std::set allowed; struct Queued { TransportMessage message; TrafficLane lane{}; }; std::vector inbox; std::uint64_t host{}, unreliable{}; void link(std::uint64_t id) { if (std::none_of(state.peers.begin(), state.peers.end(), [&](const auto &p) { return p.id == id; })) state.peers.push_back({id, true}); } }; SteamTransport::SteamTransport() : impl_(std::make_unique()) { impl_->state.local_id = ++Impl::next_id; Impl::bus[impl_->state.local_id] = impl_.get(); } SteamTransport::~SteamTransport() { stop(); Impl::bus.erase(impl_->state.local_id); } bool SteamTransport::open() { return impl_->state.ready = true; } bool SteamTransport::host(unsigned capacity) { stop(); open(); if (capacity < 2 || capacity > max_players) return false; impl_->capacity = capacity; impl_->state.hosting = true; return true; } bool SteamTransport::join(std::uint64_t id, std::uint32_t, std::uint16_t) { if (SimulatedNetwork::slow_join) Sleep(2); stop(); open(); impl_->host = id; impl_->allowed.insert(id); return connect_peer(id); } bool SteamTransport::connect_peer(std::uint64_t id) { auto &p = *impl_; const auto other = Impl::bus.find(id); if (!p.allowed.contains(id) || other == Impl::bus.end() || SimulatedNetwork::blocked.contains(SimulatedNetwork::pair(id, p.state.local_id))) return false; auto &q = *other->second; if (q.state.hosting && q.state.peers.size() >= q.capacity - 1 && std::none_of(q.state.peers.begin(), q.state.peers.end(), [&](const auto &peer) { return peer.id == p.state.local_id; })) return false; if (!q.state.hosting && !q.allowed.contains(p.state.local_id)) return false; p.link(id); q.link(p.state.local_id); return true; } void SteamTransport::allow_peers(std::span members) { impl_->allowed.clear(); impl_->allowed.insert(impl_->host); for (const auto &m : members) if (m.id != impl_->state.local_id) impl_->allowed.insert(m.id); auto peers = impl_->state.peers; for (const auto &p : peers) if (!impl_->allowed.contains(p.id)) disconnect(p.id, "Removed"); } void SteamTransport::disconnect(std::uint64_t id, const char *reason) { impl_->state.detail = reason; if (auto it = Impl::bus.find(id); it != Impl::bus.end()) it->second->state.detail = reason; std::erase_if(impl_->state.peers, [&](const auto &p) { return p.id == id; }); if (auto it = Impl::bus.find(id); it != Impl::bus.end()) std::erase_if(it->second->state.peers, [&](const auto &p) { return p.id == impl_->state.local_id; }); } void SteamTransport::stop() { auto peers = impl_->state.peers; for (const auto &p : peers) disconnect(p.id, ""); impl_->allowed.clear(); impl_->host = 0; impl_->state.hosting = false; impl_->inbox.clear(); } void SteamTransport::poll() { impl_->state.telemetry = true; impl_->state.queue_us = SimulatedNetwork::queues[impl_->state.local_id]; } bool SteamTransport::send(std::uint64_t id, std::span bytes, bool reliable, bool, TrafficLane lane) { auto &p = *impl_; if (std::none_of(p.state.peers.begin(), p.state.peers.end(), [&](const auto &v) { return v.id == id; })) return false; ++p.state.sent; p.state.sent_bytes += bytes.size(); const auto decoded = decode_wire(bytes); if (SimulatedNetwork::audit_lanes) { auto kind = decoded ? std::optional(decoded->kind) : std::nullopt; if (bytes.size() > 4 && std::equal(bytes.begin(), bytes.begin() + 4, "RMB1")) { if (const auto baseline = decode_wire(bytes.subspan(4))) kind = baseline->kind; } else if (bytes.size() >= 30 && bytes[0] == 'R' && bytes[1] == 'M' && (bytes[2] == 'D' || bytes[2] == 'S')) kind = static_cast(bytes[20] | (unsigned{bytes[21]} << 8)); if (!kind || lane != traffic_lane(*kind)) throw std::runtime_error("Stream/reference sent on the wrong Steam lane"); ++SimulatedNetwork::lane_sends[static_cast(lane)]; } if (SimulatedNetwork::lose_reports && decoded && decoded->kind == PacketKind::routes) return true; if (!reliable && SimulatedNetwork::lose_unreliable && ++p.unreliable % 5 == 0) return true; Impl::bus.at(id)->inbox.push_back({{p.state.local_id, {bytes.begin(), bytes.end()}, SimulatedNetwork::clock}, lane}); return true; } void SteamTransport::send_batch(std::span messages) { for (auto &message : messages) message.sent = send(message.id, message.bytes, message.reliable, message.fresh, message.lane); } std::vector SteamTransport::receive() { auto queued = std::exchange(impl_->inbox, {}); if (SimulatedNetwork::cosmetics_first) std::stable_sort(queued.begin(), queued.end(), [](const auto &a, const auto &b) { return a.lane == TrafficLane::cosmetics && b.lane != TrafficLane::cosmetics; }); std::vector messages; for (auto &item : queued) { if (SimulatedNetwork::hold_control_to == impl_->state.local_id && item.lane == TrafficLane::control) impl_->inbox.push_back(std::move(item)); else messages.push_back(std::move(item.message)); } return messages; } const TransportStatus &SteamTransport::status() const { return impl_->state; } std::string SteamTransport::name(std::uint64_t id) { return "Player " + std::to_string(id); } bool SteamTransport::socket_test() { return true; } std::vector SteamTransport::take_direct_notes() { return {}; } // No native/game/UI calls in this harness. NativeFrame tick_frame; unsigned capture_delay_ms{}; bool captured_full_pose{}; float shown_root{}; std::uint64_t shown_at{}; NativeFrame capture_local(std::uintptr_t, std::uintptr_t) { if (capture_delay_ms) Sleep(capture_delay_ms); return tick_frame; } NativeFrame capture_local(std::uintptr_t base, std::uintptr_t client, bool full_pose) { captured_full_pose = full_pose; return capture_local(base, client); } bool show_remote(std::uintptr_t, std::uintptr_t, const NativeFrame &, const Pose &pose, std::string &) { shown_root = pose.root.position[0]; shown_at = now_us(); return true; } void remove_remote(std::uintptr_t) noexcept {} void update_remote_cosmetics(std::uintptr_t, const NativeFrame &, const Appearance &, std::string &) {} std::uint64_t remote_pose_updates() noexcept { return 0; } std::uint64_t remote_board_pose_updates() noexcept { return 0; } NativeAnimationStats remote_animation_stats() noexcept { return {}; } std::uintptr_t remote_skater_entity() noexcept { return 0; } std::uintptr_t remote_board_entity() noexcept { return 0; } std::uint64_t remote_skater_generation() noexcept { return 0; } void note_remote_distance(float) noexcept {} void update_native_party(std::uintptr_t, const PartyRoster &, unsigned, bool) noexcept {} void set_native_party_server(std::uint64_t, std::string_view) noexcept {} void set_native_party_changes(bool) noexcept {} bool post_native_party_invite(std::uint64_t) noexcept { return false; } void set_native_nametags_enabled(bool) noexcept {} void set_native_compass_enabled(bool) noexcept {} void prepare_native_indicators(std::uintptr_t) noexcept {} void prepare_player_ui(std::uintptr_t) noexcept {} void prepare_remote_audio(std::uintptr_t) noexcept {} void prepare_effects(std::uintptr_t) noexcept {} std::vector drain_impacts() { return {}; } void play_impact(const Impact &) noexcept {} void note_remote_outfit(std::uint64_t) noexcept {} void tick_remote_effects(std::uint64_t) noexcept {} void reset_effects() noexcept {} bool install_entity_hooks(std::uintptr_t, std::string &) noexcept { return true; } void publish_custom_nametags(std::uintptr_t, std::vector, std::optional>, bool, bool, float, float, bool) noexcept {} void set_custom_nametags_enabled(bool) noexcept {} GameUiState sample_game_ui_state(std::uintptr_t) noexcept { return {}; } void note_local_skater(const Transform &) noexcept {} void initialize_native_throwdowns(std::uintptr_t) noexcept {} // The relay itself needs the game; routing is checked through each node's inbox. std::vector> tick_throwdown_relay(std::uintptr_t, const ThrowdownRelayInput &) { return {}; } std::vector take_throwdown_relay_notices() { return {}; } void receive_throwdown_relay(std::uint64_t, std::span) {} bool throwdown_relay_hides(std::uint64_t) noexcept { return false; } std::optional> throwdown_relay_celebration_offset(std::uint64_t, const std::array &) noexcept { return std::nullopt; } bool throwdown_relay_waits_offboard() noexcept { return false; } std::shared_ptr steam_social_snapshot() { static const auto snapshot = std::make_shared(); return snapshot; } std::optional capture_cosmetics(std::uintptr_t, const NativeFrame &, std::string &) { return {}; } void prepare_audio_capture(std::uintptr_t, const NativeFrame &) noexcept {} std::vector drain_audio_capture(std::uint64_t) { return {}; } void update_remote_audio(std::uintptr_t, const NativeFrame &, const Pose &, const AudioState &) noexcept {} void stop_remote_audio() noexcept {} void reset_audio() noexcept {} std::string native_audio_status() { return {}; } std::uint64_t captured_audio_frames() noexcept { return 0; } std::uint64_t played_audio_frames() noexcept { return 0; } void update_player_ui(std::uintptr_t, const Pose *, std::string) noexcept {} void update_party_position(const Pose *) noexcept {} std::string player_ui_status() { return {}; } std::uint64_t player_map_updates() noexcept { return 0; } std::unique_ptr make_steam_lobby_api() { return {}; } namespace { void check(bool value, const char *why) { if (!value) throw std::runtime_error(why); } // Exercise the public UI command queue and Steam lobby completion through tick(), // rather than starting a simulated Session after admission has already begun. struct JoinLobby final : LobbyApi { std::uint64_t host_id{}; bool completed{}; std::map metadata; void open() override { Sleep(2); } std::uint64_t create(unsigned) override { return 0; } std::uint64_t search() override { return 1; } std::uint64_t join(std::uint64_t) override { return 2; } std::optional poll(std::uint64_t, LobbyCall kind) override { if (kind == LobbyCall::list) return LobbyResult{true, 0, 1, 1}; if (kind == LobbyCall::join && completed) return LobbyResult{true, 9001, 0, 1}; return {}; } void leave(std::uint64_t) override {} std::uint64_t at(int) override { return 9001; } std::uint64_t owner(std::uint64_t) override { return host_id; } std::string data(std::uint64_t, const char *key) override { return metadata[key]; } bool data(std::uint64_t, const char *, const std::string &) override { return true; } bool joinable(std::uint64_t, bool) override { return true; } bool visibility(std::uint64_t, bool) override { return true; } std::string name() override { return "Host"; } }; void join_tick_checks() { auto &guest = session(); SimulatedNetwork::slow_join = true; tick_frame.ready = true; tick_frame.pose.skater.resize(1); const std::string map = "Levels/Root/Root|Levels/Beach/Beach"; auto host_storage = std::make_unique(); auto &host = *host_storage; host.mode = Mode::host; host.secret = 73; host.epoch = 100; host.map = map_hash(map); host.map_name = map; host.started_map = true; host.transport.host(8); host.host_id = host.transport.status().local_id; const auto code = format_invite({host.host_id, host.secret}); for (unsigned route = 0; route < 3; ++route) for (bool protected_lobby : {false, true}) { const bool browser = route != 0, friend_join = route == 2; if (friend_join && protected_lobby) continue; // Password rejection is covered by the lobby adapter tests. stop(guest, "Next join test"); check(!simulated_object_guest, "Leaving retained the guest object layout"); check(!guest.joined_public_lobby, "Stopping must clear the public friend-join target"); const auto password = protected_lobby ? "test password" : ""; host.password = password_key(password, host.secret); networking(host, tick_frame, now_us()); JoinLobby *api{}; if (browser) { auto backend = std::make_unique(); api = backend.get(); api->host_id = host.host_id; api->metadata = {{"rs_kind", "reskate-co-skate"}, {"rs_protocol", std::to_string(protocol_version)}, {"rs_build", std::string(supported_build::game_sha256)}, {"rs_name", "Host"}, {"rs_map", map}, {"rs_code", code}, {"rs_players", "1"}, {"rs_capacity", "8"}, {"rs_open", "1"}, {"rs_password", protected_lobby ? "1" : "0"}}; guest.lobbies = SteamLobbies(std::move(backend)); if (!friend_join) { guest.lobbies.refresh(1000000); guest.lobbies.tick(1100000); } } check(queue_command(friend_join ? "join-friend-lobby" : browser ? "join-lobby" : "join", browser ? "9001" : code, password), "UI join request was not queued"); tick(0, 0, true, map, nullptr); if (browser) { check(guest.lobbies.status().joining, "Fresh browser request timed out in its starting tick"); api->completed = true; Sleep(110); // The real lobby adapter polls at 100 ms intervals. tick(0, 0, true, map, nullptr); } check(guest.mode == Mode::join && guest.awaiting_map, "Fresh join timed out in its starting tick"); check(simulated_object_guest, "Joining did not isolate the guest's personal object layout"); for (unsigned frame = 0; frame < 50 && !guest.peers[0].handshaken; ++frame) { networking(host, tick_frame, now_us()); tick(0, 0, true, map, nullptr); Sleep(1); } check(guest.mode == Mode::join && player_count(guest) == 2 && !guest.awaiting_map, "Queued join did not complete map/password/admission handshake"); check(guest.joined_public_lobby == (browser ? 9001ULL : 0ULL), "Only verified lobby joins retain a public presence target"); // Party requests (menus and the game's own party buttons) go through the same queue. check(queue_command("party", "status", {}), "A party request was not queued"); tick(0, 0, true, map, nullptr); check(guest.requests.empty(), "A queued party request was not run"); // So do the host's switches in both menus: each has to be let into the queue. The // physics tuning one was left out, and its switch did nothing in either menu. for (const char *setting : {"object-placement", "noclip-allow", "nobail-allow", "boosts-allow", "tuning-enforce", "world-layer-sync", "clear-objects"}) { const bool queued = queue_command(setting, "toggle", {}); if (!queued) std::cerr << "Refused: " << setting << "\n"; check(queued, "A menu's host setting was refused before it reached the session"); tick(0, 0, true, map, nullptr); check(guest.requests.empty(), "A queued host setting was not run"); } if (friend_join) { check(queue_command("join-friend-lobby", "9002", {}), "Steam friend join request should enter the normal queue"); tick(0, 0, true, map, nullptr); check(guest.mode == Mode::join && guest.host_id == host.host_id && guest.joined_public_lobby == 9001, "A Steam join click must not replace an active session"); // Picking the server they are on in the browser changes nothing either; picking // another one leaves this session for it. check(queue_command("join-lobby", "9001", {}), "A browser pick should enter the normal queue"); tick(0, 0, true, map, nullptr); check(guest.mode == Mode::join && guest.host_id == host.host_id && guest.joined_public_lobby == 9001, "Picking the current server replaced the session"); check(queue_command("join-lobby", "9002", {}), "A browser hop should enter the normal queue"); tick(0, 0, true, map, nullptr); check(guest.joined_public_lobby != 9001 && guest.status != "Leave your current session before joining another lobby.", "A guest could not hop to another server from the browser"); } } stop(guest, "Timeout checks"); networking(host, tick_frame, now_us()); check(queue_command("join", code, {}), "Timeout join was not queued"); tick(0, 0, true, map, nullptr); check(guest.mode == Mode::join, "Fresh timeout-check join failed"); guest.join_started = now_us() - 180000001; tick(0, 0, true, map, nullptr); check(guest.mode == Mode::off && guest.status.find("timed out") != std::string::npos, "Real three-minute join timeout was disabled"); check(queue_command("join", code, {}), "Stale join was not queued"); guest.requests.back()->queued = now_us() - 10000001; tick(0, 0, true, map, nullptr); check(guest.mode == Mode::off, "Expired UI command was executed"); stop(guest, "Join tick checks complete"); guest.lobbies = SteamLobbies(nullptr); tick_frame = {}; SimulatedNetwork::slow_join = false; std::cout << "Join tick checks: queued code/browser joins, password admission, pending Steam calls, " "real timeout and stale commands passed.\n"; } void client_tick_checks() { auto &s = session(); const std::string map = "Levels/Root/Root|Levels/Beach/Beach"; tick_frame = {}; tick_frame.ready = true; tick_frame.pose.skater.resize(395); tick_frame.pose.board.resize(17); command("echo", {}, {}); tick(0, 0, true, map, nullptr); auto &peer = s.peers[0]; peer.poses.clear(); const auto base = now_us(); auto pose = packet(s, PacketKind::pose, base - 200000); pose.pose = tick_frame.pose; check(peer.poses.push(pose, base - 200000), "Client timing reference rejected"); ++pose.sequence; pose.time_us = base; pose.pose.root.position[0] = 2; check(peer.poses.push(pose, base), "Client timing latest pose rejected"); auto outfit = packet(s, PacketKind::cosmetics, base); outfit.appearance = {{skater_recipe_key, 2, {}, {{1, "Outfit", {}}}}, {board_recipe_key, 1, {}, {{2, "Deck", {}}}}}; check(peer.appearance.push(outfit), "Client timing appearance rejected"); s.next_send = base + 1000000; capture_delay_ms = 40; shown_at = 0; tick(0, 0, true, map, nullptr); capture_delay_ms = 0; check(!captured_full_pose, "Client captured a complete skeleton before the send deadline"); const auto expected = 1.f + static_cast(shown_at - base) / 100000.f; check(shown_at >= base && shown_root > 1.35f && std::abs(shown_root - std::min(expected, 2.f)) < .05f, "Client capture work left playback on the old frame timestamp"); const auto timing = s.client_timing.snapshot(now_us()); check(timing.peak_ms[ClientTiming::capture] >= 35 && timing.work_max_ms >= 35, "Actual client capture stall was absent from diagnostics"); stop(s, "Client timing checks complete"); tick_frame = {}; std::cout << "Client tick: injected capture stall measured; playback uses fresh wall time while Local Echo remains buffered.\n"; } struct Simulation { std::vector> nodes; std::uint64_t now = 10000000; NativeFrame local; std::vector positions; unsigned capacity, tps; std::size_t held = static_cast(-1); // a node that is not run: held up, reading nothing bool stamp{}; // messages carry when they arrived explicit Simulation(unsigned limit = max_players, unsigned rate = 20) : capacity(limit), tps(rate) { local.ready = true; local.pose.skater.resize(395); local.pose.board.resize(17); } Session &add(bool correct_password = true) { auto s = std::make_unique(); s->capacity = capacity; s->secret = 73; s->epoch = nodes.size() + 100; s->map = 42; s->started_map = true; s->password = password_key(correct_password ? "test room" : "wrong password", s->secret); if (nodes.empty()) { s->tps = tps; s->mode = Mode::host; s->transport.host(capacity); s->host_id = s->transport.status().local_id; } else { s->mode = Mode::join; s->host_id = nodes.front()->host_id; s->transport.join(s->host_id); } auto cosmetics = packet(*s, PacketKind::cosmetics, now); cosmetics.appearance = { {skater_recipe_key, 2, {}, {{1, "Outfit" + std::to_string(nodes.size()), {}}}}, {board_recipe_key, 1, {}, {{2, "Deck", {}}}}}; s->cosmetic_packet = encode_wire(cosmetics); nodes.push_back(std::move(s)); return *nodes.back(); } void run(unsigned frames) { for (unsigned i = 0; i < frames; ++i) { now += network_tick_us; SimulatedNetwork::clock = stamp ? now : 0; for (std::size_t n = 0; n < nodes.size(); ++n) { auto &s = *nodes[n]; if (s.mode == Mode::off || n == held) continue; local.pose.root.position[0] = static_cast(now - 10000000) / 1000000; local.pose.root.position[2] = n < positions.size() ? positions[n] : static_cast(n); refresh_host_choices(s, now); simulated_park_receiver = s.transport.status().local_id; networking(s, local, now); if (s.mode != Mode::off) { sync_objects(s, local, now); send_local(s, local, now); auto sound = packet(s, PacketKind::audio, now); sound.audio = {AudioSample{}}; sound.audio[0].state.values[1] = static_cast(n) + .5f; broadcast(s, sound, true, true, now); } } } } void fresh(unsigned count) { for (unsigned i = 0; i < count; ++i) for (unsigned j = 0; j < count; ++j) if (i != j) { auto *p = find_peer(*nodes[i], nodes[j]->transport.status().local_id); if (!p || !p->handshaken) { std::cerr << "Missing source " << j << " at node " << i << " count=" << count << " time=" << now << " status=" << nodes[i]->status << "\n"; for (std::size_t k = 0; k < nodes.size(); ++k) std::cerr << k << ": " << nodes[k]->status << "\n"; throw std::runtime_error("Admitted player missing from roster"); } check(p->appearance.value().has_value(), "Player cosmetic recipe missing after route transition"); const auto sound = p->audio.sample(now); check(sound && sound->values[1] == static_cast(j) + .5f, "Player sound missing or attributed to wrong source"); const auto pose = p->poses.sample(now); const auto lag = pose ? local.pose.root.position[0] - pose->root.position[0] : 999.f; const auto bound = .25f + 3.f * static_cast(p->received_pose_interval) / 1000000.f; if (lag >= bound) std::cerr << "Lag: receiver=" << i << " sender=" << j << " seconds=" << lag << " cadence=" << p->received_pose_interval << " arrival_age=" << (now - p->pose_arrival) << "\n"; check(pose && lag < bound, "Remote pose fell behind sender time"); } } }; // Linked throwdowns travel through the host like chat: each message reaches every // other player exactly once, never its sender, and never another world. void throwdown_routing_checks() { Simulation sim; for (unsigned i = 0; i < 3; ++i) sim.add(); sim.run(60); sim.fresh(3); auto &host = *sim.nodes[0], &first = *sim.nodes[1], &second = *sim.nodes[2]; const auto received = [](const Session &s, std::uint64_t from, const std::vector &message) { return std::count_if(s.throwdown_inbox.begin(), s.throwdown_inbox.end(), [&](const auto &entry) { return entry.first == from && entry.second == message; }); }; const auto clear = [&] { for (auto &node : sim.nodes) node->throwdown_inbox.clear(); }; clear(); const std::vector from_guest{3, 1, 2, 3}, from_host{5, 9, 9}; send_throwdown(first, from_guest); sim.run(10); const auto guest_id = first.transport.status().local_id; check(received(host, guest_id, from_guest) == 1 && received(second, guest_id, from_guest) == 1 && first.throwdown_inbox.empty(), "A guest's throwdown message did not reach every other player exactly once"); clear(); send_throwdown(host, from_host); sim.run(10); const auto host_id = host.transport.status().local_id; check(received(first, host_id, from_host) == 1 && received(second, host_id, from_host) == 1 && host.throwdown_inbox.empty(), "The host's throwdown message did not reach every guest exactly once"); clear(); auto stale = packet(first, PacketKind::throwdown, sim.now); stale.throwdown = from_guest; stale.world = first.world + 1; check(first.transport.send(first.host_id, encode_wire(stale), true), "Could not queue a wrong-world throwdown fixture"); sim.run(10); check(host.throwdown_inbox.empty() && second.throwdown_inbox.empty(), "A throwdown message from another world was relayed"); } // A lobby's parties are the ones its players form, kept by the host the way a dedicated // server keeps them: nobody is in a party for being in the lobby. void party_checks() { Simulation sim; for (unsigned i = 0; i < 4; ++i) sim.add(); sim.run(60); sim.fresh(4); auto &host = *sim.nodes[0], &first = *sim.nodes[1], &second = *sim.nodes[2], &third = *sim.nodes[3]; const auto id = [](const Session &s) { return s.transport.status().local_id; }; for (const auto &node : sim.nodes) check(!node->local_party && node->party_invites.empty(), "A lobby put a player in a party nobody formed"); check(send_party_request(first, PartyAction::invite, id(second)).empty(), "A guest's party invite was not sent"); sim.run(20); check(second.party_invites.size() == 1 && second.party_invites.front().from == id(first), "The host did not pass a guest's invite on to the invited guest"); check(!first.local_party && !second.local_party, "An unanswered invite made a party"); check(send_party_request(second, PartyAction::accept, id(first)).empty(), "Accepting an invite was not sent"); sim.run(40); check(first.local_party && first.local_party == second.local_party && first.local_party_leader && !second.local_party_leader, "An accepted invite did not put both guests in one party led by the inviter"); check(party_member(first, id(second)) && party_member(second, id(first)), "Party members do not see each other as members"); check(!host.local_party && !third.local_party && !party_member(third, id(first)) && party_of(third, id(first)) == first.local_party, "Players outside a party were put in it, or cannot see who is in it"); // The host's own requests are answered on the spot, and make a second party. check(send_party_request(host, PartyAction::invite, id(third)).empty(), "The host's own party invite was refused"); sim.run(20); check(third.party_invites.size() == 1 && third.party_invites.front().from == id(host), "The host's invite did not reach the guest"); check(send_party_request(third, PartyAction::accept, id(host)).empty(), "Accepting the host's invite was not sent"); sim.run(40); check(host.local_party && host.local_party == third.local_party && host.local_party_leader && host.local_party != first.local_party, "The host and its guest are not in a party of their own"); check(party_of(first, id(third)) == host.local_party && !party_member(first, id(third)), "A guest does not see the other party"); // Party chat reaches the party and nobody else. const auto heard = [](const Session &s, std::string_view words) { return std::any_of(s.chat.begin(), s.chat.end(), [&](const auto &line) { return line.text.find(words) != std::string::npos; }); }; check(send_party_chat(first, "meet at the bowl").empty(), "A guest's party chat was refused"); sim.run(20); check(heard(first, "meet at the bowl") && heard(second, "meet at the bowl"), "Party chat did not reach the party"); check(!heard(host, "meet at the bowl") && !heard(third, "meet at the bowl"), "Party chat reached players outside the party"); check(send_party_chat(host, "host party only").empty(), "The host's party chat was refused"); sim.run(20); check(heard(third, "host party only") && !heard(first, "host party only") && !heard(second, "host party only"), "The host's party chat did not stay in its party"); // Leaving: a party left with one member is no party. check(send_party_request(second, PartyAction::leave, 0).empty(), "Leaving a party was not sent"); sim.run(40); check(!first.local_party && !second.local_party, "A party of one was kept"); check(send_party_chat(first, "anyone").starts_with("You're not in a party"), "Party chat was sent without a party"); // A guest leaving the lobby is out of their party as well. stop(third, "Left"); sim.run(40); check(!host.local_party && host.parties.parties().empty(), "A guest who left the lobby stayed in the host's party"); std::cout << "Lobby parties: nobody by default, invites, two parties, party chat, leaving and departures passed.\n"; } // A player's badge and colour, in chat and on their nametag: who the backend lists them as // comes before what they are in the lobby, and a developer before the other lists. void role_checks() { Simulation sim; for (unsigned i = 0; i < 4; ++i) sim.add(); sim.run(60); sim.fresh(4); auto &host = *sim.nodes[0], &first = *sim.nodes[1], &second = *sim.nodes[2], &third = *sim.nodes[3]; const auto id = [](const Session &s) { return s.transport.status().local_id; }; using Role = std::pair; using L = IdentityList; check(player_role(first, id(host), false) == Role{nametag_host, "Host"} && player_role(host, id(host), true) == Role{nametag_host, "Host"} && player_role(host, id(first), false) == Role{nametag_white, {}}, "Players the backend does not list did not get their lobby roles"); simulated_identities = {{id(host), L::homie}, {id(first), L::content_creator}, {id(second), L::developer}, {id(second), L::content_creator}, {id(second), L::homie}, {id(third), L::content_creator}, {id(third), L::homie}}; check(player_role(first, id(host), false) == Role{nametag_homie, "Homie"} && player_role(host, id(host), true) == Role{nametag_homie, "Homie"}, "A homie who hosts is not shown as a homie"); check(player_role(host, id(first), false) == Role{nametag_creator, "Content Creator"} && player_role(first, id(first), true) == Role{nametag_creator, "Content Creator"}, "A content creator is not shown as one"); check(player_role(host, id(second), false) == Role{nametag_developer, "Dev"}, "A developer on every list is not shown as a developer"); check(player_role(host, id(third), false) == Role{nametag_creator, "Content Creator"}, "A content creator who is also a homie is not shown as a creator"); // A special tag comes before every lobby role: a Centrix player who hosts is Centrix, not Host. simulated_identities.insert({id(host), L::centrix}); check(player_role(first, id(host), false) == Role{nametag_centrix, "Centrix"} && player_role(host, id(host), true) == Role{nametag_centrix, "Centrix"}, "A Centrix player who hosts is not shown as Centrix"); simulated_identities.erase({id(host), L::centrix}); // A player who has turned their tag off (the Special page) is whatever they are in the lobby, // to everyone: their appearance carries the choice through the host. Their items are a // separate choice, which leaves the tag alone. auto plain = packet(host, PacketKind::cosmetics, sim.now); plain.appearance = {{skater_recipe_key, 2, {}, {{1, "Outfit0", {}}}}, {board_recipe_key, 1, {}, {{2, "Deck", {}}}}}; plain.appearance.hide_items = true; host.cosmetic_packet = encode_wire(plain); broadcast(host, plain, true, false, sim.now); sim.run(20); const auto told = [&](const Session &s) { const auto *peer = find_peer(const_cast(s), id(host)); return peer && !shows_items(*peer) && shows_tag(*peer); }; check(told(first) && told(second) && player_role(first, id(host), false) == Role{nametag_homie, "Homie"}, "A homie who turned their items off was not seen to, or lost their tag"); // A later packet, as the host's game would send after the change. const auto look = plain.appearance; plain = packet(host, PacketKind::cosmetics, sim.now); plain.appearance = look; plain.appearance.hide_items = false, plain.appearance.hide_tag = true; host.cosmetic_packet = encode_wire(plain); broadcast(host, plain, true, false, sim.now); sim.run(20); check(player_role(first, id(host), false) == Role{nametag_host, "Host"} && player_role(second, id(host), false) == Role{nametag_host, "Host"}, "A homie who turned their tag off still shows as a homie"); const auto *shown = find_peer(first, id(host)); check(shown && shows_items(*shown), "Turning their tag off turned a player's items off"); check(player_role(host, id(first), false) == Role{nametag_creator, "Content Creator"}, "One player's choice hid another's tag"); show_own_items(false); check(player_role(first, id(first), true) == Role{nametag_creator, "Content Creator"}, "Turning their items off hid a player's own tag"); show_own_items(true); show_own_tag(false); check(player_role(first, id(first), true) == Role{nametag_white, {}} && player_role(host, id(first), false) == Role{nametag_creator, "Content Creator"}, "A player's own choice did not hide their tag from themselves, or hid it from others before they were told"); show_own_tag(true); // A chat line carries its sender's role. check(send_chat(first, "new video is up").empty(), "A guest's chat was refused"); sim.run(20); const auto said = std::find_if(host.chat.begin(), host.chat.end(), [](const auto &line) { return line.text == "new video is up"; }); check(said != host.chat.end() && said->color == nametag_creator && said->tag == "Content Creator", "A content creator's chat line does not carry their role"); // None of it can be claimed. A badge goes to a Steam identity this PC is itself connected to: // a guest knows another guest only from the host's roster until Steam connects the two, and // a roster alone, which a host fills as it likes, gives nobody a badge. const Role developer{nametag_developer, "Dev"}; auto *seen = find_peer(first, id(second)); check(seen && seen->direct_ready && player_role(first, id(second), false) == developer && player_role(third, id(second), false) == developer, "Guests connected to a developer do not see them as one"); seen->direct_ready = false; check(!steam_vouched(first, *seen) && player_role(first, id(second), false) == Role{nametag_white, {}}, "A guest took a developer's identity from the host's roster alone"); seen->direct_ready = true; // In chat a guest's badge rests on their own copy of the line, sent straight to the players // Steam connects them to: the host passes lines on, and could pass on anything under // anyone's name. const auto line_of = [](const Session &s, std::string_view text) { const auto line = std::find_if(s.chat.begin(), s.chat.end(), [&](const auto &v) { return v.text == text; }); return line == s.chat.end() ? nullptr : &*line; }; check(send_chat(second, "patch notes are out").empty(), "A developer's chat was refused"); sim.run(6); const auto *real = line_of(third, "patch notes are out"), *at_host = line_of(host, "patch notes are out"); check(real && real->tag == "Dev" && real->color == nametag_developer && at_host && at_host->tag == "Dev", "A developer's own line lost its badge on the way, or waited"); check(std::count_if(third.chat.begin(), third.chat.end(), [](const auto &v) { return v.text == "patch notes are out"; }) == 1, "A line and its sender's own copy of it were both shown"); // A line the host makes up under a developer's name waits for their copy, then shows as // any other player's would. auto lie = packet(host, PacketKind::chat, sim.now); lie.source = id(second), lie.epoch = second.epoch; lie.text = "send me your password"; check(send_packet(host, id(third), lie, true, false), "The made-up line could not be sent"); sim.run(4); check(!line_of(third, "send me your password"), "A line without its sender's copy did not wait for it"); sim.run(20); const auto *made_up = line_of(third, "send me your password"); check(made_up && made_up->tag.empty() && made_up->color == nametag_white, "A line the host made up carries a developer's badge"); // So does one from a build that sends no copy; the host, who has it from the developer // themselves, shows the badge. auto old = packet(second, PacketKind::chat, sim.now); old.text = "from an older build"; check(send_packet(second, id(host), old, true, false), "The older build's line could not be sent"); sim.run(30); const auto *bare = line_of(third, "from an older build"), *first_hand = line_of(host, "from an older build"); check(bare && bare->tag.empty() && first_hand && first_hand->tag == "Dev", "A line with no copy from its sender shows a badge to a guest, or lost it at the host"); // The other lists' badges travel the same way, guest to guest. check(send_chat(third, "clip is on my channel").empty(), "A content creator's chat was refused"); sim.run(6); const auto *clip = line_of(first, "clip is on my channel"); check(clip && clip->color == nametag_creator && clip->tag == "Content Creator", "A content creator's own line lost its badge"); // A player on no list stays plain whatever their own packets ask for: the styles only shape // what a list already gives. simulated_identities.erase({id(first), L::content_creator}); auto wish = packet(first, PacketKind::cosmetics, sim.now); wish.appearance = look; wish.appearance.marks.fill({MarkMode::gradient, {255, 0, 0}, {0, 0, 255}, 2}); first.cosmetic_packet = encode_wire(wish); broadcast(first, wish, true, false, sim.now); sim.run(20); const auto *wisher = find_peer(host, id(first)); check(wisher && wisher->appearance.value() && wisher->appearance.value()->marks[0].mode == MarkMode::gradient && !identity_mark(id(first)) && player_role(host, id(first), false) == Role{nametag_white, {}} && player_role(third, id(first), false) == Role{nametag_white, {}}, "A player on no list got a badge by sending styles"); // And their chat is as it always was: shown as it arrives, with no copy sent or waited for. check(send_chat(first, "anyone at the plaza").empty(), "A guest's chat was refused"); sim.run(3); const auto *ordinary = line_of(third, "anyone at the plaza"); const auto *said_to = find_peer(third, id(first)); check(ordinary && ordinary->tag.empty() && ordinary->color == nametag_white && said_to && said_to->chat_proofs.empty() && said_to->chat_waiting.empty(), "An ordinary player's line was held up, or a copy of it was kept"); // Nor can anyone name someone else as the sender. What does not come over that player's own // Steam connection is refused, and whoever sent it is out of the session. const auto heard = [](const Session &s) { return std::any_of(s.chat.begin(), s.chat.end(), [](const auto &line) { return line.text == "free decks at my link"; }); }; auto forged = packet(first, PacketKind::chat, sim.now); forged.source = id(second), forged.epoch = second.epoch; forged.text = "free decks at my link"; check(send_packet(first, id(host), forged, true, false) && send_packet(first, id(third), forged, true, false), "The forged chat line could not be sent"); sim.run(20); check(!heard(host) && !heard(second) && !heard(third), "A guest spoke as a developer"); check(!find_peer(host, id(first)) && !find_peer(third, id(first)), "A guest who forged a sender stayed in the session"); // The same for how a developer looks: a guest cannot hide or restyle their tag and items. auto costume = packet(third, PacketKind::cosmetics, sim.now); costume.source = id(second), costume.epoch = second.epoch; costume.sequence += 1000; costume.appearance = look; costume.appearance.hide_tag = costume.appearance.hide_items = true; check(send_packet(third, id(host), costume, true, false), "The forged outfit could not be sent"); sim.run(20); const auto *theirs = find_peer(host, id(second)); check(player_role(host, id(second), false) == developer && theirs && shows_tag(*theirs) && shows_items(*theirs) && !find_peer(host, id(third)), "A guest changed how a developer shows, or stayed in the session after trying"); simulated_identities.clear(); std::cout << "Roles: lobby roles, homie, content creator, developer first, chat lines and claimed identities passed.\n"; } // The backend's bans hold in every session, and reach one that is running: a banned guest is // out and cannot come back, the others stay, and nobody stays with a banned host. void global_ban_checks() { Simulation sim; for (unsigned i = 0; i < 3; ++i) sim.add(); sim.run(60); sim.fresh(3); auto &host = *sim.nodes[0], &first = *sim.nodes[1], &second = *sim.nodes[2]; const auto id = [](const Session &s) { return s.transport.status().local_id; }; const auto first_id = id(first), host_id = id(host); simulated_identities = {{first_id, IdentityList::banned}}; sim.run(40); check(first.mode == Mode::off && first.status == banned_notice, "A banned guest stayed in the session"); check(!find_peer(host, first_id) && !find_peer(second, first_id), "The session kept a banned guest"); check(second.mode == Mode::join && find_peer(host, id(second)), "Banning one guest took another out"); // A banned player whose game does not stop itself is turned away by the host all the same. first.mode = Mode::join; first.host_id = host_id; first.transport.join(host_id); host.transport.poll(); networking(host, sim.local, sim.now); const auto &links = host.transport.status().peers; check(std::none_of(links.begin(), links.end(), [&](const auto &link) { return link.id == first_id; }) && !find_peer(host, first_id), "The host let a banned player back in"); stop(first, "Left"); simulated_identities = {{host_id, IdentityList::banned}}; sim.run(40); check(host.mode == Mode::off && host.status == banned_notice, "A banned host kept hosting"); check(second.mode == Mode::off, "A guest stayed with a banned host"); simulated_identities.clear(); std::cout << "Global bans: a banned guest, a guest who comes back, the other guests and a banned host passed.\n"; } // A host that is kept from reading for a few seconds (a server whose console held it up) then // reads everything its guests sent meanwhile in one go. That is not a flood: nobody is dropped // for it, while a real flood still is. void stall_checks() { { // 16 s of a player's ordinary traffic read at once, counted by when it arrived. ReceiveBudget backlog; bool kept = true; for (std::uint64_t i = 0; i < 16 * 90; ++i) kept = kept && backlog.accept(5000000 + i * (1000000 / 90), 300); check(kept, "A backlog read after a stall was taken for a flood"); ReceiveBudget flood; bool refused = false; for (std::uint64_t i = 0; i < 2000; ++i) refused = refused || !flood.accept(5000000 + i * 100, 300); check(refused, "A flood was let through the packet limit"); // Lanes are read one after another, so arrival times step back and forth. ReceiveBudget lanes; refused = false; for (std::uint64_t i = 0; i < 2000; ++i) refused = refused || !lanes.accept(i % 2 ? 9000000 : 9500000, 300); check(refused, "Packets read out of arrival order started the count again"); } Simulation sim(max_players, 120); sim.stamp = true; for (unsigned i = 0; i < 4; ++i) sim.add(); sim.run(240); auto &host = *sim.nodes[0]; const auto connected = [&] { unsigned count{}; for (std::size_t n = 1; n < sim.nodes.size(); ++n) { const auto *peer = find_peer(host, sim.nodes[n]->transport.status().local_id); count += peer && peer->handshaken && sim.nodes[n]->mode == Mode::join; } return count; }; check(connected() == 3, "The stall fixture did not connect its guests"); // Nine seconds unread: under the ten after which a silent player is given up on, and more // than a second's allowance from every guest. const auto sent = sim.nodes[1]->transport.status().sent; sim.held = 0; sim.run(static_cast(9000000 / network_tick_us)); sim.held = static_cast(-1); check(sim.nodes[1]->transport.status().sent - sent > 2ULL * multiplayer_tick_rates.back() + 80 + 32, "The stall fixture did not queue more than the packet limit"); sim.run(240); if (host.mode != Mode::host || connected() != 3) for (std::size_t n = 0; n < sim.nodes.size(); ++n) std::cerr << " node " << n << " mode " << static_cast(sim.nodes[n]->mode) << ": " << sim.nodes[n]->status << "\n"; check(host.mode == Mode::host && connected() == 3, "Guests were dropped after their host was held up for a few seconds"); SimulatedNetwork::clock = 0; std::cout << "Stalls: a held-up host keeps its guests; floods and out-of-order arrival are still limited.\n"; } // What a host changes outside its physics tuning (the trainer's class values and trick // multipliers) reaches its guests while it sets everyone's physics: when it changes, to a // player who joins later, and from nobody but the host. void physics_extras_checks() { Simulation sim; for (unsigned i = 0; i < 3; ++i) sim.add(); sim.run(60); sim.fresh(3); auto &host = *sim.nodes[0], &first = *sim.nodes[1], &second = *sim.nodes[2]; // The simulated players share one process, so they share what "the local player" changed: // only the host's session sends it. const std::vector boosted{1, 2, 3, 4, 5}, calmer{9, 8}; const auto physics = [&] { for (auto &node : sim.nodes) update_physics_tuning(*node, sim.local, sim.now); }; dingosdk::set_local_physics_extras(boosted); physics(); sim.run(10); check(first.host_extras == boosted && second.host_extras == boosted, "The host's physics extras did not reach its guests"); check(!host.host_extras, "The host took physics extras for itself"); update_physics_tuning(first, sim.local, sim.now); std::uint64_t seen{}; std::vector handed; check(dingosdk::session_tuning_enforced() && dingosdk::host_physics_extras(seen, handed) && handed == boosted, "A guest's game was not handed the host's physics extras"); update_physics_tuning(host, sim.local, sim.now); check(!dingosdk::session_tuning_enforced(), "A host was told another player sets its physics"); auto &late = sim.add(); sim.run(60); sim.fresh(4); check(late.host_extras == boosted, "A player who joined later did not get the host's physics extras"); dingosdk::set_local_physics_extras(calmer); physics(); sim.run(10); check(first.host_extras == calmer && second.host_extras == calmer && late.host_extras == calmer, "A change to the host's physics extras did not reach its guests"); // A guest cannot set the others' physics. auto forged = packet(first, PacketKind::physics_extras, sim.now); forged.extras = {6, 6, 6}; broadcast(first, forged, true, false, sim.now); sim.run(10); check(second.host_extras == calmer && late.host_extras == calmer && !host.host_extras, "A guest's physics extras were taken for the host's"); // Back to the game's own: sent, so no guest keeps what it had. dingosdk::set_local_physics_extras({}); physics(); sim.run(10); check(first.host_extras && first.host_extras->empty() && second.host_extras && second.host_extras->empty(), "Guests kept physics extras the host no longer has"); // Enforcement off: nothing kept for late joiners, and a guest's game is its own again. host.enforce_tuning = false; host.roster_dirty = true; physics(); sim.run(20); check(host.extras_packet.empty() && !host.sent_extras, "A host that stopped setting everyone's physics kept its extras packet"); check(!first.enforce_tuning, "The guest was not told the host stopped setting everyone's physics"); update_physics_tuning(first, sim.local, sim.now); check(!dingosdk::session_tuning_enforced(), "A guest stayed under the host's physics after the host let go"); dingosdk::set_session_tuning_enforced(false); dingosdk::set_host_physics_extras({}); std::cout << "Physics extras: host to guests, late joiners, changes, forged senders and enforcement off passed.\n"; } // A lobby host keeps a guest whose mods change scoring or physics out of linked activities: the roster // flags them for everyone and the host passes none of their throwdown messages on. void scoring_checks() { Simulation sim; for (unsigned i = 0; i < 3; ++i) sim.add(); sim.run(60); sim.fresh(3); auto &host = *sim.nodes[0], &first = *sim.nodes[1], &second = *sim.nodes[2]; const auto first_id = first.transport.status().local_id; auto *first_at_host = find_peer(host, first_id); check(first_at_host && first_at_host->scoring == 0 && !first_at_host->member.scoring, "A guest with the game's own scoring did not report it, or was flagged"); check(first.scoring_sent && first.scoring_sent->first == 0, "The guest's report was not remembered"); auto report = packet(first, PacketKind::scoring, sim.now); report.scoring = 0xbadbadbadULL; report.text = "BigPoints"; check(first.transport.send(first.host_id, encode_wire(report), true), "Could not queue a scoring report fixture"); sim.run(40); first_at_host = find_peer(host, first_id); const auto *first_at_second = find_peer(second, first_id); check(first_at_host && first_at_host->member.scoring && first_at_host->scoring_mods == "BigPoints", "The host did not flag a guest whose mods change scoring or physics"); check(first.local_scoring, "The flagged guest does not know it was flagged"); check(first_at_second && first_at_second->member.scoring, "Other guests do not see the flag"); check(std::any_of(second.chat.begin(), second.chat.end(), [](const auto &line) { return line.text.find("change scoring or physics") != std::string::npos; }), "Other guests were not told in chat"); for (auto &node : sim.nodes) node->throwdown_inbox.clear(); send_throwdown(first, {3, 1, 2, 3}); sim.run(10); check(host.throwdown_inbox.empty() && second.throwdown_inbox.empty(), "A flagged guest's throwdown message was passed on"); send_throwdown(second, {4, 5, 6}); sim.run(10); check(host.throwdown_inbox.size() == 1 && first.throwdown_inbox.size() == 1, "An unflagged guest's throwdown message stopped reaching the others"); check(edit_score_check(host, "off") == "Mods that change scoring or physics are no longer checked.", "score-check off refused"); sim.run(40); check(!find_peer(host, first_id)->member.scoring && !first.local_scoring && !find_peer(second, first_id)->member.scoring, "Turning the check off did not clear the flag"); for (auto &node : sim.nodes) node->throwdown_inbox.clear(); send_throwdown(first, {3, 1, 2, 3}); sim.run(10); check(host.throwdown_inbox.size() == 1 && second.throwdown_inbox.size() == 1, "With the check off, the guest's throwdown messages still did not go through"); check(edit_score_check(first, "on") != "Mods that change scoring or physics are no longer checked." && first.score_check, "A guest changed the host's scoring check"); // The host's own mods count too. check(edit_score_check(host, "on").find("kept out") != std::string::npos, "score-check on refused"); mods::simulated_scoring = {true, 0x1234, {"HostPoints"}}; sim.run(40); check(host.local_scoring && find_peer(second, host.transport.status().local_id)->member.scoring, "The host's own scoring mod was not flagged"); mods::simulated_scoring = {true, 0, {}}; } void object_sync_checks() { Simulation sim(32); sim.local.pose.skater.resize(1); sim.local.pose.board.resize(1); for (unsigned i = 0; i < 31; ++i) { auto &node = sim.add(); const NetworkObject object{1, "own_bk_ramp", {static_cast(i), 2, 5}, {0, 0, 0, 1}}; node.local_objects.replace(std::span(&object, 1)); } sim.run(90); sim.fresh(31); auto &last = sim.add(); const NetworkObject last_object{1, "own_bk_ramp", {32, 3, 5}, {0, 0, 0, 1}}; last.local_objects.replace(std::span(&last_object, 1)); sim.run(80); sim.fresh(32); for (const auto &node : sim.nodes) { check(player_count(*node) == 32, "A full 32-player roster was not admitted"); for (const auto &peer : node->peers) if (peer.handshaken && peer.objects.objects().size() != 1) throw std::runtime_error("Object sync: receiver " + std::to_string(node->transport.status().local_id) + ", owner " + std::to_string(peer.member.id) + ", count " + std::to_string(peer.objects.objects().size()) + ", status " + node->status); } auto &owner = *sim.nodes[1]; const auto owner_id = owner.transport.status().local_id; std::vector park; for (unsigned i = 0; i < 1024; ++i) park.push_back({i + 1ULL, "own_bk_ramp", {static_cast(i), 2, 5}, {0, 0, 0, 1}}); owner.local_objects.replace(park); sim.run(80); for (const auto &node : sim.nodes) if (node.get() != &owner) check(find_peer(*node, owner_id)->objects.layout() == park, "Chunked full park did not reach every player"); park[0].position[0] = -15; park[1].rotation = {0, 1, 0, 0}; park.erase(park.begin() + 2); const auto before = owner.local_objects.revision(); owner.local_objects.replace(park); const auto delta = owner.local_objects.updates(before); check(delta.size() == 1 && delta[0].objects.size() == 2 && delta[0].removed == std::vector{3}, "Moving/deleting a few objects resent the whole park"); sim.run(30); for (const auto &node : sim.nodes) if (node.get() != &owner) check(find_peer(*node, owner_id)->objects.layout() == park, "Live object moves/rotations/deletion failed through the host"); SteamTransport extra; check(!extra.join(sim.nodes[0]->host_id), "A 33rd player bypassed the advertised capacity"); auto stale = packet(owner, PacketKind::objects, sim.now); stale.objects = owner.local_objects.updates(0).front(); stale.world = owner.world + 1; check(owner.transport.send(owner.host_id, encode_wire(stale), true), "Could not queue a stale-world fixture"); sim.run(5); check(find_peer(*sim.nodes[0], owner_id)->objects.layout() == park, "Wrong-world object packet changed the live park"); auto spoof = packet(*sim.nodes[2], PacketKind::objects, sim.now); spoof.source = owner_id; spoof.epoch = owner.epoch; spoof.objects = delta[0]; sim.nodes[2]->transport.send(owner.host_id, encode_wire(spoof), true); sim.run(6); check(sim.nodes[2]->mode == Mode::off && find_peer(*sim.nodes[0], owner_id)->objects.layout() == park, "A guest could publish another player's object edits"); stop(owner, "Owner left"); sim.run(8); for (const auto &node : sim.nodes) if (node->mode != Mode::off) { check(!find_peer(*node, owner_id), "A departed owner's object state remained in the roster"); check(node->local_objects.objects().size() == 1, "Another owner's edits changed a local layout"); clear_world(*node, sim.now); check(!node->local_objects.revision() && std::all_of(node->peers.begin(), node->peers.end(), [](const auto &peer) { return !peer.objects.revision() && peer.object_delivery.chunks.empty(); }), "Map travel retained object state or queued packets from the previous world"); } std::cout << "32-player objects: late join, matching owner IDs, full parks, deltas, delete, spoof rejection, disconnect and world reset passed.\n"; } void tick_settings_checks() { auto& configured = session(); stop(configured, "TPS fixture"); check(configured.tps == 30, "Default session TPS is not 30"); for (const auto rate : multiplayer_tick_rates) { const auto result = command("host-config", "code 8 " + std::to_string(rate) + " Test lobby"); check(configured.mode == Mode::host && configured.tps == rate && configured.lobby_name == "Test lobby", "Host form did not apply TPS and lobby name"); (void)result; const auto epoch = configured.epoch; (void)command("host-config", "code 8 45 Invalid"); check(configured.epoch == epoch && configured.tps == rate, "Invalid TPS replaced an active session"); check(configured.status == "Choose 20, 30, 60, or 120 TPS before hosting.", "A refused host left no status for the menu"); stop(configured, "TPS fixture"); Simulation sim(4, rate); sim.add(); sim.add(); sim.run(70); check(sim.nodes[1]->tps == rate, "Joining guest did not adopt host TPS"); sim.add(); sim.run(70); check(sim.nodes.back()->tps == rate, "Late joiner did not adopt host TPS"); for (const auto& node : sim.nodes) { clear_world(*node, sim.now); check(node->tps == rate, "Map transition reset host TPS"); } } (void)command("host", "code 8 120 is a lobby name"); check(configured.tps == 30 && configured.lobby_name == "120 is a lobby name", "Legacy host command changed meaning"); profile_runtime::simulated_profile.clear(); stop(configured, "Lobby name persistence"); (void)command("host-config", "code 8 30 Persisted lobby"); check(configured.lobby_name == "Persisted lobby", "Host did not use the requested lobby name"); const auto stored = profile_runtime::local_value("Host.LobbyName"); check(stored && stored->is_string() && stored->string() == "Persisted lobby", "Host did not save the lobby name to the profile"); stop(configured, "Lobby name persistence"); configured.host_preferences.loaded = false; (void)command("host-config", "code 8 30"); check(configured.lobby_name == "Persisted lobby", "Empty rehost did not reuse the saved lobby name"); const auto still_stored = profile_runtime::local_value("Host.LobbyName"); check(still_stored && still_stored->is_string() && still_stored->string() == "Persisted lobby", "Empty rehost wiped the saved lobby name"); // Whose physics guests skate with is the host's to switch, and is remembered for next time. check(configured.enforce_tuning, "A new lobby did not start with the host's physics for everyone"); (void)command("tuning-enforce", "off"); check(!configured.enforce_tuning && !configured.host_preferences.enforce_tuning && configured.roster_dirty, "The host could not let guests skate with their own physics"); (void)command("tuning-enforce", "toggle"); check(configured.enforce_tuning && configured.host_preferences.enforce_tuning, "The host could not switch its physics for everyone back on"); stop(configured, "TPS fixture complete"); } void pacing_checks() { bool passed = true; // The native callback runs once per rendered frame, not on a 50 ms timer. for (const auto tps : multiplayer_tick_rates) for (const unsigned fps : {30U, 59U, 60U, 75U, 144U}) { auto echo_storage = std::make_unique(); auto &echo = *echo_storage; echo.mode = Mode::echo; echo.tps = tps; echo.secret = 73; echo.epoch = 91; echo.map = 42; echo.transport.open(); NativeFrame local; local.ready = true; local.pose.skater.resize(395); local.pose.board.resize(17); for (unsigned frame = 0; frame < fps * 6; ++frame) { const auto now = 10000000ULL + std::uint64_t{frame} * 1000000 / fps; send_local(echo, local, now); } const auto count = echo.peers[0].pose_count; std::cout << "Pacing " << tps << " TPS at " << fps << " FPS: " << count << " poses / 6 seconds\n"; const auto expected = std::min(tps, fps) * 6; passed &= count >= expected - 1 && count <= expected; const auto stalled = 20000000ULL; send_local(echo, local, stalled); send_local(echo, local, stalled); passed &= echo.peers[0].pose_count == count + 1; } // Relay arrivals alternate 35/65 ms, even though the sender is exactly 20 TPS. // Distance thinning must use source time; nearby delivery must not thin again. for (const bool uneven_source : {false, true}) for (const auto distance : {1.f, 100.f, 250.f}) { Simulation sim; auto &host = sim.add(); sim.add(); sim.add(); sim.run(30); const auto source_id = sim.nodes[1]->transport.status().local_id; const auto target_id = sim.nodes[2]->transport.status().local_id; auto *target = find_peer(host, target_id); target->pose_delivery = {}; target->direct_routes.clear(); target->route_reported = 0; auto pose = packet(*sim.nodes[1], PacketKind::pose, sim.now); pose.pose = sim.local.pose; const auto before = host.transport.status().sent; for (unsigned tick = 0; tick < 120; ++tick) { pose.time_us = sim.now + (uneven_source ? ((tick * 3ULL + 1) / 2) * 1000000 / 30 : tick * 50000ULL); ++pose.sequence; const auto arrival = pose.time_us + 100000 + (tick % 2 ? 15000 : 0); target->latest_root = pose.pose.root; target->latest_root->position[2] += distance; target->pose_arrival = arrival; broadcast(host, pose, false, true, arrival, source_id); } const auto count = host.transport.status().sent - before; const auto expected = distance < 60 ? 120U : distance < 170 ? 60U : 30U; std::cout << "Jittered relay " << distance << " m, source FPS=" << (uneven_source ? 30 : 20) << ": " << count << " / " << expected << " poses\n"; passed &= count == expected; } check(passed, "Frame/arrival timing reduced pose TPS or caused a catch-up burst"); std::cout << "Pacing: render-frame cadence, stalls and jittered 20/10/5 TPS relay checks passed.\n"; } void priority_mesh_checks() { SimulatedNetwork::audit_lanes = SimulatedNetwork::cosmetics_first = true; SimulatedNetwork::lane_sends = {}; Simulation sim; auto &host = sim.add(); auto &guest = sim.add(); erase_key(host.password); erase_key(guest.password); SimulatedNetwork::hold_control_to = guest.transport.status().local_id; sim.run(6); const auto *peer = find_peer(guest, host.host_id); check(peer && !peer->handshaken && !peer->pending_cosmetics.empty() && !peer->appearance.value(), "Early cosmetic was lost or applied before welcome/admission"); SimulatedNetwork::hold_control_to = 0; sim.run(20); sim.fresh(2); for (unsigned i = 2; i < 8; ++i) { auto &node = sim.add(); erase_key(node.password); } sim.run(80); sim.fresh(8); auto outfit = packet(host, PacketKind::cosmetics, sim.now); outfit.appearance = {{skater_recipe_key, 2, {}, {{1, "Updated outfit", {}}}}, {board_recipe_key, 1, {}, {{2, "Updated deck", {}}}}}; broadcast(host, outfit, true, false, sim.now); SimulatedNetwork::lose_unreliable = true; sim.run(80); sim.fresh(8); for (std::size_t i = 1; i < sim.nodes.size(); ++i) check(find_peer(*sim.nodes[i], host.host_id)->appearance.value() == outfit.appearance, "Cosmetic update failed after prioritized baseline/delta delivery"); auto &target = *sim.nodes[1]; auto *host_link = find_peer(target, host.host_id); auto spoof = outfit; spoof.epoch += 1000; spoof.sequence += 10; defer_cosmetics(*host_link, spoof, sim.now); receive_cosmetics(target, sim.local, sim.now); check(host_link->appearance.value() == outfit.appearance, "Deferred outfit bypassed roster epoch validation"); receive_cosmetics(target, sim.local, sim.now + 10000001); check(host_link->pending_cosmetics.empty(), "Unadmitted cosmetics retained beyond deadline"); for (unsigned i = 0; i < max_players + 5; ++i) { spoof.source = i + 1; defer_cosmetics(*host_link, spoof, sim.now); } check(host_link->pending_cosmetics.size() == max_players, "Pending cosmetics exceeded source bound"); clear_world(target, sim.now); check(find_peer(target, host.host_id)->pending_cosmetics.empty(), "Old outfits survived world reset"); check(std::all_of(SimulatedNetwork::lane_sends.begin(), SimulatedNetwork::lane_sends.end(), [](auto count) { return count > 0; }), "Not all Steam traffic lanes were exercised"); SimulatedNetwork::lose_unreliable = false; std::cout << "Priority mesh: early cosmetics before admission, eight-player reordered delivery, " "stream/reference lanes, packet loss, outfit updates, epoch validation and bounded reset passed.\n"; } struct MapPair { static inline unsigned loads{}; static inline bool missing{}; // Match runtime ownership: large session buffers do not fit on the // default Windows stack when two map-transition peers are nested. std::unique_ptr host_storage = std::make_unique(); std::unique_ptr guest_storage = std::make_unique(); Session &host = *host_storage, &guest = *guest_storage; NativeFrame host_frame; std::string current = "Levels/Root/Root|Levels/Other/Other"; bool ready = true; std::uint64_t now = 10000000; static MapLoadResult loader(std::string_view, bool submitted, std::string &detail) { if (missing) { detail = "Map not installed."; return MapLoadResult::missing; } if (!submitted) ++loads; return submitted ? MapLoadResult::waiting : MapLoadResult::queued; } MapPair(bool password = false, bool correct = true) { loads = 0; missing = false; host_frame.ready = true; host_frame.pose.skater.resize(1); host.mode = Mode::host; host.secret = guest.secret = 789; host.epoch = 101; guest.epoch = 102; host.map_name = "Levels/Root/Root|Levels/Beach/Beach"; host.map = map_hash(host.map_name); host.transport.host(8); host.host_id = guest.host_id = host.transport.status().local_id; guest.transport.join(host.host_id); guest.mode = Mode::join; guest.awaiting_map = true; guest.join_started = now; if (password) { host.password = password_key("test room", host.secret); guest.password = password_key(correct ? "test room" : "wrong password", guest.secret); } } void run(unsigned frames) { for (unsigned i = 0; i < frames; ++i) { now += network_tick_us; if (host.mode != Mode::off) networking(host, host_frame, now); if (guest.mode == Mode::off) continue; NativeFrame local; if (prepare_join_map(guest, ready, current, loader, now)) { local = host_frame; guest.map = map_hash(current); guest.map_name = current; } if (guest.mode != Mode::off) networking(guest, local, now); } } }; void map_checks() { Packet offer; offer.kind = PacketKind::map_offer; offer.session = 1; offer.epoch = 2; offer.destination = "Levels/Root/Root|Levels/Beach/Beach"; offer.map = map_hash(offer.destination); offer.map_authorized = true; const auto bytes = encode(offer); const auto decoded = decode(bytes); check(decoded && decoded->destination == offer.destination && decoded->map_authorized, "Host map offer did not round trip"); for (std::size_t i = 0; i < bytes.size(); ++i) check(!decode(std::span(bytes.data(), i)), "Truncated map offer accepted"); auto invalid = bytes; invalid[packet_header_size + 72] = 2; check(!decode(invalid), "Invalid map authorization flag accepted"); invalid = bytes; invalid.back() ^= 1; check(!decode(invalid), "Map destination/hash mismatch accepted"); check(!valid_map_destination("Levels/Root/Root") && !valid_map_destination("Levels/Root|Bad\nLevel") && !valid_map_destination("Levels/Root|Level|Other") && !valid_map_destination("|Level") && valid_map_destination("Levels/Root/Root|"), "Invalid destination syntax accepted"); for (bool password : {false, true}) { MapPair pair(password); pair.run(65); check(pair.guest.join_map_authorized && pair.guest.map_load_submitted && MapPair::loads == 1, "Authorized cross-map join did not queue exactly one load"); check(!find_peer(pair.host, pair.guest.transport.status().local_id)->handshaken, "Guest admitted before host map loaded"); pair.ready = false; pair.run(400); // Well beyond the old eight-second handshake timeout. check(pair.guest.mode == Mode::join && MapPair::loads == 1, "Loading disconnected the guest or queued duplicate loads"); pair.current = pair.host.map_name; pair.run(4); check(pair.guest.awaiting_map, "Map identity alone bypassed skater readiness"); pair.ready = true; pair.run(4); check(find_peer(pair.guest, pair.host.host_id)->handshaken && find_peer(pair.host, pair.guest.transport.status().local_id)->handshaken, "Guest failed to complete admission after native loading"); } { MapPair pair; pair.current = pair.host.map_name; pair.run(10); check(MapPair::loads == 0 && find_peer(pair.guest, pair.host.host_id)->handshaken, "Same-map join unnecessarily reloaded the level"); } { MapPair pair(true, false); pair.run(65); check(pair.guest.mode == Mode::off && MapPair::loads == 0, "Wrong password triggered a map load"); } { MapPair pair(true); erase_key(pair.guest.password); pair.run(20); check(pair.guest.mode == Mode::off && MapPair::loads == 0, "Missing password triggered a map load"); } { MapPair pair; MapPair::missing = true; pair.run(20); const auto said = "The host is on Beach, which is not installed on this PC. Install its map mod and join again."; check(pair.guest.mode == Mode::off && pair.guest.status == said && pair.guest.leave_notice == said, "Missing map did not end the pending join naming the map"); } { MapPair pair; pair.run(20); stop(pair.guest, "Cancelled"); pair.current = pair.host.map_name; pair.run(20); check(pair.guest.mode == Mode::off && !pair.guest.awaiting_map && pair.guest.join_destination.empty(), "Cancelled map load rejoined the server"); } { MapPair pair; pair.run(20); pair.now += 180000001; pair.run(1); check(pair.guest.mode == Mode::off, "Loading join did not have a bounded deadline"); } { MapPair pair; pair.run(20); stop(pair.host, "Host left"); pair.run(1); check(pair.guest.mode == Mode::off, "Host departure left a pending map join active"); } { map_package_lookup.store(+[](std::string_view) { return std::string("Team-Map-1.0.0"); }); MapPair pair; MapPair::missing = true; pair.run(20); const auto deadline = pair.guest.map_fetch_deadline; check(deadline && pair.guest.map_need && pair.guest.map_need->deadline_us == deadline, "Downloader did not receive the session's authoritative deadline"); pair.now += 180000001; auto request = packet(pair.guest, PacketKind::map_request, pair.now); request.map = pair.host.map; request.map_fetching = true; send_required(pair.guest, pair.host.host_id, encode_wire(request)); networking(pair.host, pair.host_frame, pair.now); networking(pair.guest, NativeFrame{}, pair.now); check(pair.guest.mode == Mode::join && pair.guest.map_fetch_deadline == deadline, "An active download was disconnected by the ordinary host-link handshake timeout"); check(!prepare_join_map(pair.guest, false, pair.current, MapPair::loader, deadline - 1) && pair.guest.mode == Mode::join, "Map fetch expired before its deadline"); check(!prepare_join_map(pair.guest, false, pair.current, MapPair::loader, deadline) && pair.guest.mode == Mode::off, "Map fetch survived its exact deadline"); map_package_lookup.store(nullptr); } { map_package_lookup.store(+[](std::string_view) { return std::string("Team-Map-1.0.0"); }); MapPair pair; MapPair::missing = true; pair.run(20); const auto deadline = pair.guest.map_fetch_deadline; MapPair::missing = false; pair.current = pair.host.map_name; const auto now = deadline - 1; check(prepare_join_map(pair.guest, true, pair.current, MapPair::loader, now), "Installed map could not finish loading within the deadline"); pair.guest.map = map_hash(pair.current); pair.guest.map_name = pair.current; find_peer(pair.guest, pair.host.host_id)->last_packet = now; // ongoing metadata heartbeats check(pair.guest.map_fetch_deadline == deadline, "Native loading completion prematurely discarded the join deadline"); networking(pair.guest, pair.host_frame, now); networking(pair.host, pair.host_frame, now); networking(pair.guest, pair.host_frame, now); check(find_peer(pair.guest, pair.host.host_id)->handshaken && !pair.guest.map_fetch_deadline, "Welcome did not retire a successfully completed download deadline"); check(prepare_join_map(pair.guest, true, pair.current, MapPair::loader, deadline + 1), "A successfully joined player was disconnected by a retired deadline"); map_package_lookup.store(nullptr); } { MapPair pair; pair.run(20); hold_map_load(true, "Team-Unrelated-1.0.0"); check(!prepare_join_map(pair.guest, false, pair.current, MapPair::loader, pair.now + 500000) && pair.guest.mode == Mode::join && pair.guest.map_load_submitted, "An abandoned download blocked an unrelated ordinary map load"); hold_map_load(false, ""); } { auto &waiting = session(); stop(waiting, "Map cancellation fixture"); waiting.mode = Mode::join; waiting.map_fetch_deadline = 100; check(queue_command("stop-map-fetch", "100", ""), "Map cancellation command was not queued"); std::unique_ptr cancellation; { std::lock_guard lock(waiting.request_mutex); cancellation = std::move(waiting.requests.back()); waiting.requests.pop_back(); } // A fresh join executes before the old worker's cancellation is dispatched. waiting.map_fetch_deadline = 200; command(cancellation->action, cancellation->argument, cancellation->password); check(waiting.mode == Mode::join && waiting.map_fetch_deadline == 200, "A queued cancellation stopped a fresh map-fetch attempt"); command("stop-map-fetch", "200"); check(waiting.mode == Mode::off, "An owned cancellation did not stop its pending join"); } std::cout << "Automatic map joining: codec, same/different maps, password-before-load, slow loading, " "skater readiness, missing maps, cancellation, timeout and host departure passed.\n"; } struct TravelSimulation { Simulation sim; std::vector current, queued; std::vector ready; std::vector loads; static inline TravelSimulation *active{}; static inline std::size_t node{}; const std::string beach = "Levels/Root/Root|Levels/Beach/Beach"; const std::string city = "Levels/Root/Root|Levels/City/City"; explicit TravelSimulation(unsigned count) { current.resize(count, beach); queued.resize(count); ready.resize(count, true); loads.resize(count); for (unsigned i = 0; i < count; ++i) { auto &s = sim.add(); s.map = map_hash(beach); s.map_name = beach; s.invite = format_invite({s.host_id, s.secret}); auto cosmetic = *decode_wire(s.cosmetic_packet); cosmetic.map = s.map; s.cosmetic_packet = encode_wire(cosmetic); } run(80); sim.fresh(count); } static MapLoadResult loader(std::string_view destination, bool submitted, std::string &) { if (submitted) return MapLoadResult::waiting; active->queued[node] = destination; active->ready[node] = false; ++active->loads[node]; return MapLoadResult::queued; } void finish(std::size_t i) { if (!queued[i].empty()) current[i] = queued[i]; ready[i] = true; } void run(unsigned frames) { active = this; for (unsigned frame = 0; frame < frames; ++frame) { sim.now += network_tick_us; for (node = 0; node < sim.nodes.size(); ++node) { auto &s = *sim.nodes[node]; if (s.mode == Mode::off) continue; refresh_host_choices(s, sim.now); NativeFrame local; if (prepare_join_map(s, ready[node], current[node], loader, sim.now) && ready[node]) { local = sim.local; local.pose.root.position[0] = static_cast(sim.now - 10000000) / 1000000.f; local.pose.root.position[2] = static_cast(node); sim.local.pose.root.position[0] = local.pose.root.position[0]; } if (s.mode == Mode::off || !observe_local_world(s, local, current[node], sim.now)) continue; networking(s, local, sim.now); if (s.mode == Mode::off || !world_playing(s, local)) continue; if (s.cosmetic_packet.empty()) { auto c = packet(s, PacketKind::cosmetics, sim.now); c.appearance = {{skater_recipe_key, 2, {}, {{1, "Outfit" + std::to_string(node), {}}}}, {board_recipe_key, 1, {}, {{2, "Deck", {}}}}}; s.cosmetic_packet = encode_wire(c); broadcast(s, c, true, false, sim.now); } send_local(s, local, sim.now); auto sound = packet(s, PacketKind::audio, sim.now); sound.audio = {AudioSample{}}; sound.audio[0].state.values[1] = static_cast(node) + .5f; broadcast(s, sound, true, true, sim.now); } } } void connected() { const auto count = static_cast(sim.nodes.size()); for (const auto &s : sim.nodes) { check(s->mode != Mode::off && player_count(*s) == count, "Map change lost room membership"); check(s->secret == 73 && s->password.has_value(), "Map change replaced the invitation/password"); } check(sim.nodes[0]->transport.status().peers.size() == count - 1, "Map change closed a guest's host connection"); } }; void travel_checks() { Packet state; state.kind = PacketKind::world_state; state.session = 1; state.epoch = 2; state.world = 3; state.map = 0; auto encoded = encode(state); check(decode(encoded)->destination.empty(), "Unknown-destination loading notice rejected"); state.destination = "Levels/Root/Root|Levels/Beach/Beach"; state.map = map_hash(state.destination); state.world_ready = true; encoded = encode(state); check(decode(encoded)->world == 3 && decode(encoded)->world_ready, "Transition state lost generation/readiness"); for (std::size_t i = 0; i < encoded.size(); ++i) check(!decode(std::span(encoded.data(), i)), "Truncated transition accepted"); encoded[packet_header_size + 32] = 2; check(!decode(encoded), "Invalid transition flag accepted"); for (const unsigned count : {2U, 8U}) { TravelSimulation test(count); auto &host = *test.sim.nodes[0]; const auto invite = host.invite; const auto old_pose = packet(host, PacketKind::pose, test.sim.now); auto stale = old_pose; stale.pose = test.sim.local.pose; stale.pose.root.position[0] = 9999; const auto stale_wire = encode_wire(stale); begin_host_world(host, test.city, test.sim.now); test.ready[0] = false; test.run(240); test.connected(); for (unsigned i = 1; i < count; ++i) check(test.loads[i] == 1 && test.queued[i] == test.city, "Guest did not queue the host map exactly once"); test.current[0] = test.city; test.ready[0] = true; for (unsigned i = 1; i + 1 < count; ++i) test.finish(i); test.run(240); // One guest still loading, others can skate. test.connected(); check(!host.travelling && !test.sim.nodes.back()->started_map, "Slow guest blocked the host or used an unloaded world"); test.finish(count - 1); test.run(100); test.connected(); test.sim.fresh(count); check(host.invite == invite, "Host map switch changed the share code"); // Same-map reload must preserve admission without an unnecessary guest load. const auto previous_loads = test.loads; begin_host_world(host, test.city, test.sim.now); test.ready[0] = false; test.run(240); test.connected(); check(test.loads == previous_loads, "Same-map host reload reloaded guests unnecessarily"); test.ready[0] = true; test.run(100); test.sim.fresh(count); // Returning to A cannot revive packets from the first A world. begin_host_world(host, test.beach, test.sim.now); test.ready[0] = false; test.run(30); test.current[0] = test.beach; test.ready[0] = true; for (unsigned i = 1; i < count; ++i) test.finish(i); test.run(100); test.connected(); test.sim.fresh(count); for (unsigned i = 1; i < count; ++i) host.transport.send(test.sim.nodes[i]->transport.status().local_id, stale_wire, false, false, TrafficLane::gameplay); test.run(2); test.sim.fresh(count); check(host.world == 4, "World generation did not advance on every host load"); } { TravelSimulation test(2); auto &host = *test.sim.nodes[0]; begin_host_world(host, {}, test.sim.now); // Native load outside the menu. test.ready[0] = false; test.run(240); test.connected(); check(test.loads[1] == 0, "Unknown host destination queued an invalid native load"); test.current[0] = test.city; test.ready[0] = true; test.run(30); test.finish(1); test.run(100); test.sim.fresh(2); } { TravelSimulation test(2); auto &host = *test.sim.nodes[0]; begin_host_world(host, test.city, test.sim.now); test.ready[0] = false; test.run(30); // The native host load failed and returned to its old map. test.ready[0] = true; test.run(10); check(host.world == 3 && host.map == map_hash(test.beach), "Failed host load did not publish a rollback"); test.current[1] = test.city; // Finish the superseded guest request first. test.ready[1] = true; test.run(20); check(test.queued[1] == test.beach && test.loads[1] == 2, "New destination failed to replace a guest's in-flight load"); test.finish(1); test.run(100); test.connected(); test.sim.fresh(2); } { TravelSimulation test(3); auto &host = *test.sim.nodes[0]; begin_host_world(host, test.city, test.sim.now); test.ready[0] = false; test.run(30); stop(*test.sim.nodes[2], "Cancelled during loading"); test.current[0] = test.city; test.ready[0] = true; test.finish(1); test.run(100); check(player_count(host) == 2 && player_count(*test.sim.nodes[1]) == 2 && test.sim.nodes[2]->mode == Mode::off, "One cancelled guest disrupted the remaining room"); test.sim.fresh(2); auto spoof = packet(*test.sim.nodes[1], PacketKind::world_state, test.sim.now); spoof.world = host.world + 1; spoof.destination = test.beach; spoof.map = map_hash(test.beach); test.sim.nodes[1]->transport.send(host.host_id, encode_wire(spoof), true, false); test.run(2); check(host.mode == Mode::host && host.world == 2 && host.map == map_hash(test.city), "Guest was able to redirect the host to another map"); } std::cout << "Host travel: 2/8 players, retained membership/password/code, staggered loading, " "same-map reload, A-B-A stale packets, poses/board/audio/cosmetics, native-load fallback, " "failed-load rollback, superseded loads, cancellation and host-only authority passed.\n"; } void session_controls_checks() { Simulation sim; for (unsigned i = 0; i < 4; ++i) sim.add(); sim.run(80); auto &host = *sim.nodes[0]; auto &guest = *sim.nodes[1]; const auto roster = [&] { auto p = packet(host, PacketKind::roster, sim.now); p.capacity = host.capacity; p.object_placement = host.object_placement; p.members.push_back({p.source, host.epoch, "Host"}); for (const auto &peer : host.peers) if (peer.handshaken) p.members.push_back(peer.member); // Everyone in one party led by the host, as a roster may list them. for (auto &m : p.members) { m.party = p.members.size() > 1 ? lobby_party : 0; m.party_leader = m.party && m.id == p.source; } return p; }; const auto stale = roster(); constexpr auto everyone = ObjectPlacement::everyone, host_only = ObjectPlacement::host_only; check(edit_object_placement(guest, "off").starts_with("Only the session host") && guest.object_placement == everyone, "Guest changed object placement"); (void)edit_object_placement(host, "off"); sim.run(4); for (const auto &s : sim.nodes) check(s->object_placement == host_only, "Live placement lock was not delivered"); check(object_placement_allowed(host.object_placement, true) && !object_placement_allowed(guest.object_placement, false), "Host-only placement did not exempt the host alone"); (void)edit_object_placement(host, "invalid"); check(host.object_placement == host_only, "Invalid permission changed the policy"); send_required(host, guest.transport.status().local_id, encode_wire(stale)); sim.run(4); check(guest.object_placement == host_only, "Stale roster reenabled placement"); sim.add(); sim.run(70); check(sim.nodes.back()->object_placement == host_only, "Late joiner missed the placement policy"); auto bytes = encode(roster()); check(decode(bytes) && decode(bytes)->object_placement == host_only, "Placement policy did not round trip"); // Roster tail: tps, placement, forced layers, layer count (2 bytes), layer // modes, voice policy (5 bytes), voice range (2 bytes). const auto placement_at = bytes.size() - 2 - 5 - world_layers().size() - 2 - 2; check(!decode(std::span(bytes).first(placement_at)), "Missing permission byte accepted"); bytes[placement_at] = 3; check(!decode(bytes), "Invalid permission byte accepted"); auto forged = roster(); forged.source = sim.nodes[2]->transport.status().local_id; forged.epoch = sim.nodes[2]->epoch; forged.object_placement = everyone; check(send_packet(*sim.nodes[2], guest.transport.status().local_id, forged, true, false), "Permission spoof fixture could not send"); sim.run(3); check(guest.object_placement == host_only, "Guest spoof replaced host placement policy"); (void)edit_object_placement(host, "on"); sim.run(4); for (const auto &s : sim.nodes) check(s->object_placement == everyone, "Placement enable was not delivered"); check(edit_object_placement(guest, "next").starts_with("Only the session host") && guest.object_placement == everyone, "Guest cycled object placement"); for (const auto policy : {host_only, ObjectPlacement::nobody, everyone, host_only}) { (void)edit_object_placement(host, "next"); sim.run(4); for (const auto &s : sim.nodes) check(s->object_placement == policy, "Repeated placement cycle reused a stale value"); } (void)edit_object_placement(host, "next"); (void)edit_object_placement(host, "next"); (void)edit_object_placement(host, "next"); check(host.object_placement == host_only, "Back-to-back placement cycles did not use live state"); (void)edit_object_placement(host, "on"); const auto victim = sim.nodes.back()->transport.status().local_id; const auto epoch = sim.nodes.back()->epoch; const auto target = std::to_string(victim) + " " + std::to_string(epoch); check(kick_player(guest, target).starts_with("Only the session host"), "Guest could kick a player"); (void)kick_player(host, std::to_string(host.host_id) + " " + std::to_string(host.epoch)); check(host.mode == Mode::host, "Host kicked itself"); (void)kick_player(host, std::to_string(victim) + " " + std::to_string(epoch + 1)); check(find_peer(host, victim), "Stale kick removed a player with a different epoch"); (void)edit_object_placement(host, "off"); sim.run(3); removed_object_owners.clear(); check(kick_player(host, target).find("was kicked") != std::string::npos, "Host kick failed"); sim.run(10); check(std::count(removed_object_owners.begin(), removed_object_owners.end(), std::pair{victim, epoch}) >= 4, "Kick did not clean the departing owner's objects on every remaining peer"); check(sim.nodes.back()->mode == Mode::off && sim.nodes.back()->object_placement == everyone, "Kicked guest did not disconnect and restore offline placement"); for (std::size_t i = 0; i + 1 < sim.nodes.size(); ++i) { const auto &s = *sim.nodes[i]; check(!find_peer(*sim.nodes[i], victim), "Kicked player survived in the roster"); check(std::none_of(s.transport.status().peers.begin(), s.transport.status().peers.end(), [&](const auto &p) { return p.id == victim; }), "Kick retained a direct transport"); check(s.mode != Mode::off && s.object_placement == host_only, "Kick changed another player's session"); } publish(host); check(host.view.host_id == host.host_id && !host.view.local_name.empty() && host.view.roster.front().epoch != 0, "Session view omitted player identity"); std::cout << "Session controls: live policy, late join, malformed/stale/spoof rejection, host-only kick, epoch checks and peer cleanup passed.\n"; } void guest_building_checks() { Simulation sim; for (unsigned i = 0; i < 3; ++i) sim.add(); sim.run(80); auto &host = *sim.nodes[0]; auto &builder = *sim.nodes[1]; auto &viewer = *sim.nodes[2]; const auto builder_id = builder.transport.status().local_id; const auto host_id = host.host_id; const auto seen = [&](Session &node, std::uint64_t owner) { return find_peer(node, owner)->objects.objects().size(); }; const auto place = [](Session &node, unsigned count) { std::vector layout; for (unsigned i = 0; i < count; ++i) layout.push_back({i + 1ULL, "own_bk_ramp", {static_cast(i), 2, 5}, {0, 0, 0, 1}}); node.local_objects.replace(layout); }; place(builder, 1); sim.run(10); check(seen(viewer, builder_id) == 1, "Guest objects did not reach other guests while building was allowed"); (void)edit_object_placement(host, "host"); sim.run(4); // A modified guest ignores its own placement lock and keeps publishing. place(builder, 3); sim.run(10); check(find_peer(host, builder_id)->objects.objects().size() == 3, "Host lost the guest's upload, so its revisions would desync on re-enable"); check(seen(viewer, builder_id) == 1 && find_peer(host, builder_id)->shared.objects().size() == 1, "A guest bypassed disabled building and published objects"); place(host, 2); sim.run(10); check(seen(viewer, host_id) == 2 && seen(builder, host_id) == 2, "Disabling guest building blocked the host"); auto &late = sim.add(); sim.run(70); check(seen(late, builder_id) == 1, "A late joiner received objects placed while building was disabled"); (void)edit_object_placement(host, "nobody"); sim.run(4); check(!simulated_placement_allowed, "Session-wide lock did not lock the host's own tools"); place(builder, 5); sim.run(10); check(seen(viewer, builder_id) == 1, "A guest bypassed a session-wide placement lock"); (void)edit_object_placement(host, "everyone"); sim.run(10); for (auto *node : {&viewer, &late}) check(seen(*node, builder_id) == 5, "Re-enabling guest building did not publish the current layout"); place(builder, 6); sim.run(10); check(seen(viewer, builder_id) == 6, "Guest edits stopped syncing after building was re-enabled"); place(viewer, 2); sim.run(10); check(clear_guest_objects(viewer, {}).starts_with("Only the session host") && seen(builder, viewer.transport.status().local_id) == 2, "A guest deleted other players' objects"); simulated_guest_wipes = 0; check(clear_guest_objects(host, {}) == "Deleted 8 guest objects.", "Host delete reported the wrong object count"); sim.run(10); check(seen(viewer, builder_id) == 0 && seen(builder, viewer.transport.status().local_id) == 0 && seen(late, builder_id) == 0, "Deleting guest objects did not remove them for everyone"); check(seen(viewer, host_id) == 2, "Deleting guest objects removed the host's own objects"); check(simulated_guest_wipes >= 3 && !builder.clear_pending && !viewer.clear_pending, "Guests were not told to delete their own session objects"); // A modified guest ignores the wipe and re-sends (and edits) its old objects. std::vector stale; for (unsigned i = 0; i < 6; ++i) stale.push_back({i + 1ULL, "own_bk_ramp", {static_cast(i), 9, 5}, {0, 0, 0, 1}}); builder.local_objects.replace(stale); sim.run(10); check(seen(viewer, builder_id) == 0, "A guest restored objects the host deleted"); stale.push_back({100, "own_bk_ramp", {40, 2, 5}, {0, 0, 0, 1}}); builder.local_objects.replace(stale); sim.run(10); check(seen(viewer, builder_id) == 1, "New guest objects stopped syncing after a delete"); const auto wipes = simulated_guest_wipes; auto &after = sim.add(); sim.run(70); check(simulated_guest_wipes == wipes && !after.clear_pending && after.object_clears == host.object_clears, "A player joining after a delete wiped their own objects"); const auto late_id = late.transport.status().local_id; (void)kick_player(host, std::to_string(late_id) + " " + std::to_string(late.epoch)); sim.run(6); check(late.mode == Mode::off, "Kicked guest stayed connected"); // Rejoin with the same Steam identity and a fresh session epoch. late.mode = Mode::join; late.epoch += 1000; late.started_map = true; late.map = 42; late.host_id = host_id; late.password = password_key("test room", late.secret); late.transport.join(host_id); sim.run(60); check(late.mode == Mode::off && (!find_peer(host, late_id) || !find_peer(host, late_id)->handshaken), "A kicked player rejoined the session"); check(player_count(host) == 4, "Rejecting a kicked player disturbed the other guests"); stop(host, "Host restarted"); check(host.banned.empty(), "Kick bans outlived the session"); std::cout << "Guest building: host-enforced freeze, patched-guest rejection, host exemption, late join, re-enable, guest object deletion and " "kick bans passed.\n"; } void world_layer_sync_checks() { WorldLayersModel model; model.choices.front() = "on"; const auto own = model.choices; auto host_choices = default_world_layers(); host_choices.front() = "off"; host_choices.back() = "on"; WorldLayerOverride override; check(override.apply(model, true, host_choices) && model.controlled_by_host && model.choices == host_choices, "Host layer override did not apply"); host_choices.front() = "default"; check(override.apply(model, true, host_choices) && override.local == own, "Repeated host snapshot replaced local layer preferences"); auto invalid = host_choices; invalid.back() = "invalid"; check(!override.apply(model, true, invalid) && model.choices == host_choices, "Invalid layer override partially changed the world"); model.map = WorldMap::none; model.ready = false; check(override.apply(model, false, {}) && !model.controlled_by_host && model.choices == own && !override.local, "Disabling sync during travel did not restore local choices"); check(unpack_world_layers(pack_world_layers(host_choices)) == host_choices, "World layer modes did not round trip"); Simulation sim; for (unsigned i = 0; i < 3; ++i) sim.add(); sim.run(80); auto &host = *sim.nodes[0]; auto &guest = *sim.nodes[1]; const auto roster = [&] { auto p = packet(host, PacketKind::roster, sim.now); p.capacity = host.capacity; p.force_world_layers = host.force_world_layers; p.layers = pack_world_layers(host.layers); p.members.push_back({p.source, host.epoch, "Host"}); for (const auto &peer : host.peers) if (peer.handshaken) p.members.push_back(peer.member); // Everyone in one party led by the host, as a roster may list them. for (auto &m : p.members) { m.party = p.members.size() > 1 ? lobby_party : 0; m.party_leader = m.party && m.id == p.source; } return p; }; const auto stale = roster(); simulated_host_layers.choices = host_choices; check(edit_world_layer_sync(guest, "on").starts_with("Only the session host") && !guest.force_world_layers, "Guest could force world layer sync"); (void)edit_world_layer_sync(host, "on"); sim.run(5); check(guest.force_world_layers && guest.layers == host_choices, "Host layers did not synchronize"); (void)edit_world_layer_sync(host, "invalid"); check(host.force_world_layers, "Invalid toggle changed world layer policy"); send_required(host, guest.transport.status().local_id, encode_wire(stale)); sim.run(4); check(guest.force_world_layers && guest.layers == host_choices, "Stale roster changed layer policy"); simulated_host_layers.choices.front() = "off"; sim.run(4); check(guest.layers == simulated_host_layers.choices, "Live layer changes did not propagate"); sim.add(); sim.run(70); check(sim.nodes.back()->force_world_layers && sim.nodes.back()->layers == host.layers, "Late joiner missed forced world layers"); auto bytes = encode(roster()); check(decode(bytes) && decode(bytes)->force_world_layers && unpack_world_layers(decode(bytes)->layers) == host.layers, "Roster lost world layer settings"); // The voice policy (allowed byte, 4-byte revision), voice range (2 bytes) and the guest // noclip / No Bail byte follow the layer modes, then what a dedicated server adds, here // none: polls (1 byte), custom votes (1) and an announcement (4). const auto layers_end = bytes.size() - 5 - 2 - 1 - 6; for (std::size_t n = layers_end - world_layers().size() - 1; n < bytes.size(); ++n) check(!decode(std::span(bytes).first(n)), "Truncated world layer settings accepted"); auto malformed = bytes; malformed[layers_end - 1] = 3; check(!decode(malformed), "Invalid layer mode accepted"); malformed = bytes; malformed[layers_end - world_layers().size() - 1] = 2; check(!decode(malformed), "Invalid force-sync flag accepted"); auto forged = roster(); forged.source = sim.nodes[2]->transport.status().local_id; forged.epoch = sim.nodes[2]->epoch; forged.force_world_layers = false; check(send_packet(*sim.nodes[2], guest.transport.status().local_id, forged, true, false), "World layer spoof fixture failed to send"); sim.run(4); check(guest.force_world_layers, "Guest disabled host's forced world layers"); (void)edit_world_layer_sync(host, "off"); sim.run(5); for (const auto &s : sim.nodes) check(!s->force_world_layers, "World layer unlock did not propagate"); check(edit_world_layer_sync(guest, "toggle").starts_with("Only the session host") && !guest.force_world_layers, "Guest toggled world layer sync"); for (const bool forced : {true, false, true, false}) { (void)edit_world_layer_sync(host, "toggle"); sim.run(5); for (const auto &s : sim.nodes) check(s->force_world_layers == forced, "Repeated world-layer toggle reused a stale value"); } (void)edit_world_layer_sync(host, "toggle"); (void)edit_world_layer_sync(host, "toggle"); check(!host.force_world_layers, "Back-to-back world-layer toggles did not use live state"); (void)edit_world_layer_sync(host, "on"); sim.run(4); stop(guest, "Leave layer session"); check(!guest.force_world_layers, "Disconnected guest kept layer lock"); TravelSimulation travel(2); auto &travelling_host = *travel.sim.nodes[0]; (void)edit_object_placement(travelling_host, "off"); (void)edit_world_layer_sync(travelling_host, "on"); travel.run(4); begin_host_world(travelling_host, travel.city, travel.sim.now); travel.ready[0] = false; travel.run(5); simulated_host_layers.choices.back() = "off"; travel.current[0] = travel.city; travel.ready[0] = true; travel.run(8); travel.finish(1); travel.run(12); travel.connected(); check(travel.sim.nodes[1]->object_placement == ObjectPlacement::host_only && travel.sim.nodes[1]->force_world_layers && travel.sim.nodes[1]->layers == simulated_host_layers.choices, "Map travel lost host controls or world layer changes"); simulated_host_layers = {}; std::cout << "World layers: live host sync, local preference restoration, late joins, stale/spoof rejection and bounded modes passed.\n"; } void distance_settings_checks() { Simulation sim; sim.positions = {0, 25, 100, 250, 500, 750, 1000, 1250}; for (unsigned i = 0; i < 7; ++i) sim.add(); sim.run(80); auto &host = *sim.nodes[0]; const auto host_roster = [&] { auto p = packet(host, PacketKind::roster, sim.now); p.distances = host.distances; p.capacity = host.capacity; p.members.push_back({p.source, host.epoch, "Host"}); for (const auto &peer : host.peers) if (peer.handshaken) p.members.push_back(peer.member); // Everyone in one party led by the host, as a roster may list them. for (auto &m : p.members) { m.party = p.members.size() > 1 ? lobby_party : 0; m.party_leader = m.party && m.id == p.source; } return p; }; auto stale = host_roster(); const auto message = edit_distances(host, "15 20 70 80"); const MultiplayerDistances custom{15, 20, 70, 80}; check(message.starts_with("TPS distances applied") && host.distances == custom, "Host edit did not apply"); sim.run(4); for (const auto &s : sim.nodes) check(s->distances == custom, "Host edit did not synchronize to a guest"); auto *medium = find_peer(*sim.nodes[1], host.host_id); auto *distant = find_peer(*sim.nodes[2], host.host_id); const auto before_medium = medium->pose_count, before_distant = distant->pose_count; sim.run(40); check(medium->pose_count - before_medium >= 19 && medium->pose_count - before_medium <= 21 && distant->pose_count - before_distant >= 9 && distant->pose_count - before_distant <= 11, "New policy did not change actual host delivery rates"); const auto nearby_id = sim.nodes[2]->transport.status().local_id; auto *guest_stream = find_peer(*sim.nodes[1], nearby_id); check(guest_stream->received_pose_interval == 100000, "New policy did not change guest-to-guest delivery"); for (const auto *invalid : {"", "10 20 30", "10 20 30 40 extra", "-1 20 30 40", "20 20 30 40", "10 40 30 50", "10 20 40 40", "10 20 30 10001", "0 2147483648 30 40", "1.5 20 30 40"}) { (void)edit_distances(host, invalid); check(host.distances == custom, "Invalid host edit changed the policy"); } check(edit_distances(*sim.nodes[1], "1 2 3 4").starts_with("Only the lobby host") && sim.nodes[1]->distances == custom, "Guest could edit its lobby policy"); send_required(host, sim.nodes[1]->transport.status().local_id, encode_wire(stale)); sim.run(4); check(sim.nodes[1]->distances == custom, "Old reliable roster rolled back the policy"); sim.add(); sim.run(70); check(sim.nodes.back()->distances == custom, "Late joiner missed host policy"); const auto fresh = host_roster(); const auto bytes = encode(fresh); const auto decoded = decode(bytes); check(decoded && decoded->distances == custom, "Distance policy lost values in the codec"); for (std::size_t n = bytes.size() - 8; n < bytes.size(); ++n) check(!decode(std::span(bytes).first(n)), "Truncated roster policy accepted"); // The tick rate sits before placement, the forced flag, the layers and the voice policy. auto malformed = bytes; malformed[bytes.size() - 5 - world_layers().size() - 3] = 255; check(!decode(malformed), "Out-of-range wire policy accepted"); auto invalid_roster = fresh; invalid_roster.distances = {20, 10, 30, 40}; bool rejected{}; try { (void)encode(invalid_roster); } catch (...) { rejected = true; } check(rejected, "Encoder accepted invalid distance order"); // An admitted direct peer still cannot publish the host's policy. auto forged = fresh; forged.source = sim.nodes[2]->transport.status().local_id; forged.epoch = sim.nodes[2]->epoch; forged.distances = {1, 2, 3, 4}; check(send_packet(*sim.nodes[2], sim.nodes[1]->transport.status().local_id, forged, true, false), "Spoof fixture did not reach a direct path"); sim.run(3); check(sim.nodes[1]->mode == Mode::join && sim.nodes[1]->distances == custom, "A direct guest replaced host distance settings"); // Increasing the radii must immediately clear previous low-rate hysteresis. (void)edit_distances(host, "2000 2100 3000 3100"); sim.run(6); for (const auto &s : sim.nodes) check(s->distances == host.distances, "Larger distances did not propagate"); check(find_peer(*sim.nodes[1], host.host_id)->received_pose_interval == 50000, "Increased distances retained the previous slow band"); (void)edit_distances(host, "50 60 150 170"); sim.run(5); check(host.distances == MultiplayerDistances{} && sim.nodes.back()->distances == MultiplayerDistances{}, "Default values did not restore across the lobby"); // Settings remain part of the room across native map teardown and late loads. TravelSimulation travel(2); auto &owner = *travel.sim.nodes[0]; (void)edit_distances(owner, "15 20 70 80"); travel.run(4); begin_host_world(owner, travel.city, travel.sim.now); travel.ready[0] = false; travel.run(5); (void)edit_distances(owner, "30 40 90 110"); // Applied when gameplay is ready again. travel.current[0] = travel.city; travel.ready[0] = true; travel.run(8); travel.finish(1); travel.run(12); travel.connected(); check(owner.distances == MultiplayerDistances{30, 40, 90, 110} && travel.sim.nodes[1]->distances == owner.distances, "Map travel lost updated host settings"); stop(*travel.sim.nodes[1], "Disconnect settings check"); check(travel.sim.nodes[1]->distances == MultiplayerDistances{}, "Disconnected guest retained the old lobby policy"); std::cout << "Host distance settings: eight-player sync, live host/direct rates, late joins, reset, stale/spoof rejection, invalid inputs, and map travel passed.\n"; } void distance_mesh_checks() { Simulation sim; sim.positions = {0, 250, 251, 500, 750, 1000, 1250, 1500}; for (unsigned i = 0; i < 8; ++i) sim.add(); sim.run(90); sim.fresh(8); const auto measure = [&] { std::array, 8> counts{}; for (unsigned i = 0; i < 8; ++i) for (unsigned j = 0; j < 8; ++j) if (i != j) counts[i][j] = find_peer(*sim.nodes[i], sim.nodes[j]->transport.status().local_id)->pose_count; sim.run(80); for (unsigned i = 0; i < 8; ++i) for (unsigned j = 0; j < 8; ++j) { if (i == j) continue; auto *peer = find_peer(*sim.nodes[i], sim.nodes[j]->transport.status().local_id); const bool nearby = i == 0 || std::abs(sim.positions[i] - sim.positions[j]) < 50; const auto received = peer->pose_count - counts[i][j]; check(received >= (nearby ? 78U : 18U) && received <= (nearby ? 82U : 22U), "Distance policy rate differs on direct/forwarded/upstream path"); } sim.fresh(8); }; measure(); const auto a = sim.nodes[1]->transport.status().local_id, b = sim.nodes[2]->transport.status().local_id; SimulatedNetwork::blocked.insert(SimulatedNetwork::pair(a, b)); sim.nodes[1]->transport.disconnect(b, "Force nearby relay with distant host"); sim.run(80); measure(); SimulatedNetwork::blocked.clear(); sim.positions[2] = 350; // The nearby pair moves to the 10 TPS band. sim.run(90); auto *peer = find_peer(*sim.nodes[1], b); const auto before = peer->pose_count; sim.run(80); check(peer->pose_count - before >= 38 && peer->pose_count - before <= 42, "Middle band is not 10 TPS"); sim.positions[2] = 251; sim.run(90); measure(); SimulatedNetwork::lose_unreliable = true; sim.run(80); sim.fresh(8); SimulatedNetwork::lose_unreliable = false; std::cout << "Eight-player distance delivery: 20/10/5 TPS, full upstream, nearby relay through a distant host, approach recovery and loss passed.\n"; } void mesh_checks() { Packet report; report.kind = PacketKind::routes; report.session = 1; report.epoch = 2; report.map = 42; report.members = {{76561198000000321ULL, 9, {}}}; const auto encoded = encode(report); check(decode(encoded)->members == report.members, "Route report codec changed membership"); for (std::size_t n = 0; n < encoded.size(); ++n) check(!decode(std::span(encoded.data(), n)), "Truncated route report accepted"); auto duplicate = report; duplicate.members.push_back(report.members.front()); check(!valid_routes(duplicate.members), "Duplicate route identity accepted"); Simulation sim; for (unsigned i = 0; i < 5; ++i) { sim.add(); sim.run(70); sim.fresh(i + 1); } auto &host = *sim.nodes.front(); auto bytes = host.transport.status().sent_bytes; sim.run(40); sim.fresh(5); const auto direct_bytes = host.transport.status().sent_bytes - bytes; for (std::size_t i = 1; i < 5; ++i) for (std::size_t j = 1; j < 5; ++j) if (i != j) check(find_peer(*sim.nodes[i], sim.nodes[j]->transport.status().local_id)->direct_ready, "Guest pair failed to establish direct Steam route"); // Expiring reports must restore every relay stream even while physical links remain up. SimulatedNetwork::lose_reports = true; sim.run(40); bytes = host.transport.status().sent_bytes; sim.run(40); const auto relay_bytes = host.transport.status().sent_bytes - bytes; sim.fresh(5); check(direct_bytes * 2 < relay_bytes, "Host upload did not fall substantially with direct routes"); SimulatedNetwork::lose_reports = false; std::cout << "Five players: host bytes over two seconds, direct=" << direct_bytes << ", forced relay=" << relay_bytes << "\n"; const auto a = sim.nodes[1]->transport.status().local_id, b = sim.nodes[2]->transport.status().local_id; SimulatedNetwork::blocked.insert(SimulatedNetwork::pair(a, b)); sim.nodes[1]->transport.disconnect(b, "Simulated route loss"); sim.run(50); sim.fresh(5); check(!find_peer(*sim.nodes[1], b)->direct_ready, "Broken direct route still active"); const auto *destination = find_peer(host, a); const auto *source = find_peer(host, b); check(needs_relay(destination->direct_routes, destination->route_reported, source->member, sim.now), "Lost route did not restore host forwarding"); SimulatedNetwork::blocked.clear(); sim.run(90); sim.fresh(5); check(find_peer(*sim.nodes[1], b)->direct_ready, "Direct path did not recover"); SimulatedNetwork::lose_unreliable = true; sim.run(100); sim.fresh(5); SimulatedNetwork::lose_unreliable = false; // Late joins up to the supported maximum. for (unsigned i = 5; i < 8; ++i) { sim.add(); sim.run(70); sim.fresh(i + 1); } // A direct guest cannot impersonate the host or another roster member. auto forged = packet(*sim.nodes[2], PacketKind::pose, sim.now); forged.source = host.host_id; send_packet(*sim.nodes[2], sim.nodes[1]->transport.status().local_id, forged, false, false); sim.run(70); sim.fresh(8); check(sim.nodes[1]->mode == Mode::join, "Invalid direct packet ended the host session"); // A limited-upload guest reduces optional fan-out; receivers restore relay. const auto limited_id = sim.nodes[1]->transport.status().local_id; SimulatedNetwork::queues[limited_id] = 90000; sim.run(180); sim.fresh(8); check(sim.nodes[1]->direct_upload.limit == 0, "Congested guest did not reduce direct uploads"); const auto *limited = find_peer(host, limited_id); for (unsigned i = 2; i < 8; ++i) { const auto *target = find_peer(host, sim.nodes[i]->transport.status().local_id); check(needs_relay(target->direct_routes, target->route_reported, limited->member, sim.now), "Paused direct uploads did not restore relay"); } SimulatedNetwork::queues.clear(); sim.run(900); sim.fresh(8); check(sim.nodes[1]->direct_upload.limit >= 6 && sim.nodes[1]->direct_upload.limit < max_remote_players - 1, "Direct uploads did not recover gradually"); // A guest reconnect uses a new epoch; old direct routes cannot suppress its data. auto &guest = *sim.nodes[3]; const auto id = guest.transport.status().local_id; stop(guest, "Test departure"); sim.run(10); check(!find_peer(host, id), "Departed player retained by host"); guest.mode = Mode::join; guest.started_map = true; // The simulation reconnects while its local map remains loaded. guest.secret = 73; guest.epoch = 900; guest.map = 42; guest.host_id = host.host_id; guest.password = password_key("test room", 73); guest.transport.join(host.host_id); auto cosmetics = packet(guest, PacketKind::cosmetics, sim.now); cosmetics.appearance = {{skater_recipe_key, 2, {}, {{1, "Rejoined outfit", {}}}}, {board_recipe_key, 1, {}, {{2, "Deck", {}}}}}; guest.cosmetic_packet = encode_wire(cosmetics); sim.run(100); sim.fresh(8); // Password admission is still exclusively controlled by the host. stop(*sim.nodes.back(), "Make room"); sim.run(10); auto &wrong = sim.add(false); sim.run(30); check(wrong.mode == Mode::off, "Wrong password entered mesh"); SteamTransport outsider; outsider.open(); const Member victim{sim.nodes[1]->transport.status().local_id, 101, {}}; outsider.allow_peers(std::span(&victim, 1)); check(!outsider.connect_peer(victim.id), "Unknown identity connected to an admitted guest"); stop(host, "Host departure"); sim.run(5); for (const auto &s : sim.nodes) check(s->mode == Mode::off, "Guest remained after host departure"); } void random_park_sync_checks() { std::mt19937 generator{7821}; std::vector> received; simulated_park_receives = &received; simulated_parks.choices = random_park_choices(generator); Simulation sim; for (unsigned i = 0; i < 3; ++i) sim.add(); sim.run(60); sim.fresh(3); const auto all_received = [&] { return !received.empty() && std::all_of(received.begin(), received.end(), [](const auto& entry) { return entry.second == simulated_parks.choices; }); }; const auto received_by = [&](const Session& node) { return std::any_of(received.begin(), received.end(), [&](const auto& entry) { return entry.first == node.transport.status().local_id && entry.second == simulated_parks.choices; }); }; check(received_by(*sim.nodes[1]) && received_by(*sim.nodes[2]) && all_received(), "Guests did not receive the host's initial random park choices"); received.clear(); simulated_parks.choices = random_park_choices(generator); sim.run(12); sim.fresh(3); check(received_by(*sim.nodes[1]) && received_by(*sim.nodes[2]) && all_received(), "A new host roll did not reach existing guests intact"); received.clear(); sim.add(); sim.run(30); sim.fresh(4); check(received_by(*sim.nodes.back()) && all_received(), "A late joiner did not receive the host's current random parks"); auto& host = *sim.nodes[0]; auto& sender = *sim.nodes[2]; auto forged = packet(host, PacketKind::roster, sim.now); forged.parks = random_park_choices(generator); check(forged.parks != simulated_parks.choices, "Spoof fixture must use different park choices"); forged.source = sender.transport.status().local_id; forged.epoch = sender.epoch; for (const auto& node : sim.nodes) forged.members.push_back({node->transport.status().local_id, node->epoch, "Player"}); received.clear(); check(send_packet(sender, sim.nodes[1]->transport.status().local_id, forged, true, false), "Guest park spoof fixture failed to send"); sim.run(2); sim.fresh(4); check(std::all_of(received.begin(), received.end(), [&](const auto& entry) { return entry.second == simulated_parks.choices; }) && host.parks == simulated_parks.choices, "A guest's forged roster replaced the host's park choices"); simulated_park_receives = nullptr; simulated_parks = {}; std::cout << "Random parks: host rolls, current guests, late join and guest spoof rejection passed.\n"; } // Typing dots: the host relays an on notice, an off notice clears it, and a missed off expires. void typing_checks() { Simulation sim; for (unsigned i = 0; i < 3; ++i) sim.add(); sim.run(60); sim.fresh(3); auto &host = *sim.nodes[0], &first = *sim.nodes[1], &second = *sim.nodes[2]; const auto id = [](const Session &s) { return s.transport.status().local_id; }; const auto until = [](Session &s, std::uint64_t who) { const auto *peer = find_peer(s, who); return peer ? peer->typing_until : 0; }; send_typing(first, true); sim.run(4); check(until(host, id(first)) > sim.now && until(second, id(first)) > sim.now && first.local_typing_until != 0, "A guest's typing notice did not reach the host and the other guest"); check(until(first, id(host)) == 0 && second.local_typing_until == 0, "A typing notice was applied to the wrong player"); check(std::none_of(host.chat.begin(), host.chat.end(), [](const auto &line) { return line.text == "." || line.text == "..."; }) && std::none_of(second.chat.begin(), second.chat.end(), [](const auto &line) { return line.text == "." || line.text == "..."; }), "A typing notice was written into chat"); send_typing(first, false); sim.run(4); check(until(host, id(first)) == 0 && until(second, id(first)) == 0 && first.local_typing_until == 0, "Stopping typing left the dots up"); // A stop with no dots up is not passed on: the other guest would lose this marker if it were. find_peer(second, id(first))->typing_until = sim.now + typing_hold_us; send_typing(first, false); sim.run(4); check(until(second, id(first)) != 0, "A stop notice with no dots up was relayed"); find_peer(second, id(first))->typing_until = 0; send_typing(host, true); sim.run(4); check(until(first, id(host)) > sim.now && until(second, id(host)) > sim.now && host.local_typing_until != 0, "The host's typing notice did not reach both guests"); send_typing(host, false); sim.run(2); check(until(first, id(host)) == 0 && until(second, id(host)) == 0 && host.local_typing_until == 0, "The host stopping left the dots up"); send_typing(first, true); sim.run(4); const auto held = until(second, id(first)); check(held > sim.now, "Typing did not start again"); sim.run(static_cast(typing_hold_us / network_tick_us) + 8); check(until(second, id(first)) < sim.now && until(host, id(first)) < sim.now && until(second, id(first)) != 0, "A missed stop notice left the dots up, or cleared the deadline"); std::cout << "Typing notices: relay, stop and expiry passed.\n"; } } // namespace } // namespace dingosdk::multiplayer int main(int argc, char **argv) { try { // A small stand-in for the catalog read from the level data at startup. dingosdk::install_world_layer_catalog({ {{"Levels/Game/BAM_LevelRoot/Root", dingosdk::WorldMap::bam, -1, true}, {"Levels/Game/BAM_LevelRoot/TOD_1_Morning", dingosdk::WorldMap::bam, 0, false}, {"Levels/Game/BAM_LevelRoot/Season_01", dingosdk::WorldMap::bam, 0, false}}, {0}, {{"bam_root", "Root", "", dingosdk::WorldMap::bam, 0, 0, "World"}, {"bam_tod_1_morning", "TOD 1 Morning", "", dingosdk::WorldMap::bam, 1, 1, "Lighting"}, {"bam_season_01", "Season 01", "", dingosdk::WorldMap::bam, 2, 2, "Seasonal"}}}); dingosdk::simulated_host_layers = {}; dingosdk::multiplayer::join_tick_checks(); dingosdk::multiplayer::tick_settings_checks(); if (argc == 2 && std::string_view(argv[1]) == "--parks-only") { dingosdk::multiplayer::random_park_sync_checks(); return 0; } if (argc == 2 && std::string_view(argv[1]) == "--objects-only") { dingosdk::multiplayer::object_sync_checks(); return 0; } if (argc == 2 && std::string_view(argv[1]) == "--session-controls-only") { dingosdk::multiplayer::session_controls_checks(); dingosdk::multiplayer::guest_building_checks(); dingosdk::multiplayer::world_layer_sync_checks(); return 0; } if (argc == 2 && std::string_view(argv[1]) == "--join-tick-only") return 0; if (argc == 2 && std::string_view(argv[1]) == "--pacing-only") { dingosdk::multiplayer::pacing_checks(); dingosdk::multiplayer::client_tick_checks(); return 0; } if (argc == 2 && std::string_view(argv[1]) == "--priorities-only") { dingosdk::multiplayer::priority_mesh_checks(); dingosdk::multiplayer::map_checks(); dingosdk::multiplayer::travel_checks(); return 0; } if (argc == 2 && std::string_view(argv[1]) == "--distance-settings-only") { dingosdk::multiplayer::distance_settings_checks(); return 0; } if (argc == 2 && std::string_view(argv[1]) == "--bandwidth-only") { dingosdk::multiplayer::distance_mesh_checks(); return 0; } dingosdk::multiplayer::map_checks(); if (argc == 2 && std::string_view(argv[1]) == "--map-only") return 0; dingosdk::multiplayer::travel_checks(); if (argc == 2 && std::string_view(argv[1]) == "--travel-only") return 0; dingosdk::multiplayer::mesh_checks(); dingosdk::multiplayer::throwdown_routing_checks(); dingosdk::multiplayer::typing_checks(); dingosdk::multiplayer::party_checks(); dingosdk::multiplayer::role_checks(); dingosdk::multiplayer::global_ban_checks(); dingosdk::multiplayer::physics_extras_checks(); dingosdk::multiplayer::stall_checks(); dingosdk::multiplayer::scoring_checks(); dingosdk::multiplayer::object_sync_checks(); dingosdk::multiplayer::session_controls_checks(); dingosdk::multiplayer::guest_building_checks(); dingosdk::multiplayer::world_layer_sync_checks(); dingosdk::multiplayer::random_park_sync_checks(); std::cout << "Mesh admission, 2-8 players, relay fallback, route loss, packet loss, reconnect and " "password checks passed.\n"; return 0; } catch (const std::exception &e) { std::cerr << e.what() << '\n'; return 1; } }