Map downloads on join, faster mod merging, connect cards in the server browser

Map downloader
- A host or dedicated server now tells joiners which Thunderstore package
  its map comes from (the Mods folder name plus manifest.json).
- A player without the map gets a card: icon, name, author, version and
  description, with DOWNLOAD and CANCEL. It works with mouse, keyboard and
  controller.
- Only packages listed as Maps in the ReSkate community are offered, and
  never deprecated, rejected or NSFW ones.
- On yes the package is downloaded, installed into Mods and applied as a
  live mod, then the session loads the map. A small card on the right
  shows the progress, so the player can keep skating.
- The player stays connected while it downloads: the host gives a player
  who says they are fetching the map up to 45 minutes instead of the
  usual load time.
- When the server changes map with the player in it, the card says so.
- Protocol version 47: map package on map_offer and world_state, and a
  "fetching the map" flag on map_request and world_ready.

Joining
- The pause-menu server browser shows a CONNECTING card over the page
  while a join is under way, and a COULDN'T CONNECT card with the reason
  when it does not get in.
- The session and voice tabs no longer show before the player is in.
- Leaving without the map (declined or cancelled download) no longer
  counts towards the failed-join limit, on servers and player hosts.

Input
- A card that asks something holds the game's input until the buttons
  are released.
- Steam's overlay hooks XInput in front of ReSkate and answers the game
  itself, so a controller reached the game behind menus. ReSkate now
  hooks that hook as well.

Mod merging
- Mods are read on several threads, at launch and for live merges. A live
  merge runs below the game's priority.
- The paid-cosmetics check reads the game's files and the mods on several
  threads.
- A live merge reuses the paid-cosmetics verdicts it already has and
  reports its progress and timings.

Launcher
- NSFW mods and their categories are not shown in the mod browser.
This commit is contained in:
AyeZeeBB committed 2026-10-10 13:15:04 -05:00
1 parent b7936a36c6
commit 0ae0e71f0f
46 files changed
+2448 -275

No files matched your search

+30 -4
View File
@@ -118,6 +118,21 @@ void Host::save() {
log_(std::string("Could not save the config: ") + e.what());
}
}
// A player without the map is downloading it: the clock they are loading against starts over
// each time they say so, for as long as map_fetch_limit_us from the first time. A slot is still
// never held for good, and a player who stops saying it has the usual time to load.
void Host::fetching(Guest &guest) {
// Only for a player who is past the password: anyone else is held to the short time a
// connection gets to show it belongs here.
if (!guest.handshaken && !guest.map_authorized) return;
if (!guest.fetching_since) {
guest.fetching_since = now_;
log_("[map] " + guest_name(guest) + " is downloading the map");
}
if (now_ - guest.fetching_since > map_fetch_limit_us) return;
if (!guest.handshaken) guest.connected_at = now_;
else if (guest.travel_since) guest.travel_since = now_;
}
std::string Host::invite() const { return format_invite({id_, secret_}); }
std::string Host::map_name() const { return map_label(config_.map); }
std::string Host::wire_map_label() const { // players without the map's mod still see its name
@@ -698,6 +713,7 @@ void Host::send_world_state() {
auto state = packet(PacketKind::world_state, now_);
state.destination = map_destination(config_.map);
state.map_label = wire_map_label();
state.map_package = map_package(config_.map);
state.world_ready = true; // the server has nothing to load
const auto bytes = encode_wire(state);
for (auto &[id, guest] : guests_)
@@ -793,6 +809,7 @@ void Host::change_map(std::string_view map) {
g.scoring_flagged = old.scoring_flagged;
g.world_ready = false;
g.travel_since = g.handshaken ? now_ : 0;
g.fetching_since = 0;
g.last_packet = now_;
}
if (++world_ == 0) throw std::runtime_error("Map transition counter exhausted.");
@@ -818,10 +835,16 @@ void Host::drop(std::uint64_t id, const std::string &reason, const std::string &
} else {
// Never admitted: it held a player slot meanwhile, so it shows in the log, and an ID
// that keeps failing waits longer each time before its connection is taken again.
const auto failures = join_backoff_.failed(id, now_);
log_("[join] " + std::to_string(id) + " did not finish joining (" + reason + ")" +
(failures > 1 ? ", attempt " + std::to_string(failures) : std::string{}) +
(detail.empty() ? std::string{} : " [" + detail + "]"));
// Not one that was let in (past the password) and left while fetching the map: turning
// a download down, or stopping one, is no failed attempt, and they may come straight back.
if (found->second->map_authorized && found->second->fetching_since) {
log_("[join] " + std::to_string(id) + " left without the map (" + reason + ")");
} else {
const auto failures = join_backoff_.failed(id, now_);
log_("[join] " + std::to_string(id) + " did not finish joining (" + reason + ")" +
(failures > 1 ? ", attempt " + std::to_string(failures) : std::string{}) +
(detail.empty() ? std::string{} : " [" + detail + "]"));
}
}
guests_.erase(found);
for (auto &[other, guest] : guests_) {
@@ -987,6 +1010,7 @@ void Host::receive(std::uint64_t peer, std::span<const std::uint8_t> bytes, std:
return;
link->ready_sequence = p.sequence;
link->last_packet = now_;
if (p.map_fetching) fetching(*link);
const bool arrived = p.world_ready && !link->world_ready;
if (!p.world_ready && link->world_ready) link->loading_since = now_;
if (arrived && config_.activity_log)
@@ -1005,6 +1029,7 @@ void Host::receive(std::uint64_t peer, std::span<const std::uint8_t> bytes, std:
return drop(peer, "Invalid map request. Update ReSkate to the server's version and join again.");
if (link->handshaken) return;
if (p.map && p.map != map_) return drop(peer, "The server changed maps while you were joining. Join again.");
if (p.map_fetching) fetching(*link);
link->member.epoch = p.epoch;
bool authorized = !password_;
if (password_) {
@@ -1021,6 +1046,7 @@ void Host::receive(std::uint64_t peer, std::span<const std::uint8_t> bytes, std:
auto offer = packet(PacketKind::map_offer, now_);
offer.destination = map_destination(config_.map);
offer.map_label = wire_map_label();
offer.map_package = map_package(config_.map);
offer.challenge = link->password_challenge;
offer.map_authorized = authorized;
send_required(*link, encode_wire(offer));