From c502092a743c4b5a9d8fcaaa486bdeb1aa4554df Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Fri, 30 Oct 2026 22:37:25 -0700 Subject: [PATCH 1/3] Settle host match options before netplay --- CMakeLists.txt | 4 +++ docs/NETPLAY_DIRECT.md | 2 +- src/hub/hub_main.cpp | 20 ++++++++++-- src/hub/hub_netplay.cpp | 58 +++++++++++++++++++++++++++++++-- src/hub/hub_netplay.hpp | 8 +++++ src/hub/hub_netplay_options.hpp | 49 ++++++++++++++++++++++++++++ tests/netplay_options_test.cpp | 54 ++++++++++++++++++++++++++++++ 7 files changed, 189 insertions(+), 6 deletions(-) create mode 100644 src/hub/hub_netplay_options.hpp create mode 100644 tests/netplay_options_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 926fbf3..6ee7922 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -553,6 +553,10 @@ if(RETCOMM_HAVE_SDL3 AND RETCOMM_IMGUI_READY) ${RETCOMM_HUB_SDL_TARGET}) add_test(NAME hub_core_settings COMMAND retro-hub-settings-test ${CMAKE_CURRENT_BINARY_DIR}/hub_core_settings_test.d) + add_executable(retro-hub-netplay-options-test tests/netplay_options_test.cpp) + target_include_directories(retro-hub-netplay-options-test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) + target_link_libraries(retro-hub-netplay-options-test PRIVATE retcomm_core retro_corelink) + add_test(NAME netplay_options COMMAND retro-hub-netplay-options-test) # Romhack bases (romhacks.hpp): what the Base picker lists, a base's save # dir, and a mod's standing on a base. add_executable(retro-hub-romhack-test tests/hub_romhack_test.cpp) diff --git a/docs/NETPLAY_DIRECT.md b/docs/NETPLAY_DIRECT.md index 2e58269..7bf40fd 100644 --- a/docs/NETPLAY_DIRECT.md +++ b/docs/NETPLAY_DIRECT.md @@ -46,7 +46,7 @@ contract across the four repositories involved. | Host Lobby: "Transfer Paks" on/off (`match_caps.tpak`); a Transfer Pak lobby needs all three ROMs set and rechecked, to host and to join (a join without them leaves, saying why) | **built**; PLAY in a Transfer Pak lobby waits until every seated player's pak has arrived | | Pak exchange: each seated player's pak (which of the three carts, its save) goes to every peer before start, over the lobby's opaque `signal` (type `0x5450`, base64 chunks of 12 KiB, sha256 checked on arrival). A remote pak is kept under `/netplay//paks/<player id>.sav`. At launch each seat's save is copied into the match's sandbox (`session-<id>/saves/tpakN.sav`) with the local ROM for that cart, so the runner's match key holds every seat's ROM and save hash | **built** | | Host relay: a Direct-mode room asks for it (`match_caps.relay = "host"`). The host's hub holds the game port while the room waits (`netplay_nat` `HostPort`: UPnP IGD, else NAT-PMP, else STUN), advertises it with `set_host_endpoint` and answers probes; each guest probes it and sends `path_report`; the room says which way it went. At launch the host frees the port and its runner binds it | **built**; UPnP checked read-only against Alex's router (discovery, the WANIPConnection:1 control URL, GetExternalIPAddress); no mapping was added from here | -| PLAY -> `start` -> `launch` -> a PlaySession in netplay mode: `netplay_runner_args` turns the launch into `--net-*` (transport `sfu`: every peer dials the relay; `host`: guests dial the host); a fresh save sandbox per match (`<data>/netplay/<title>/session-<id>/saves`); the player's NETPLAY options dropped for the core's defaults; the game never pauses (the menu opens over it, the local pad neutral); no save states, no turbo; a runner exit 4 is "The match ended", not a fault | **built** | +| PLAY -> `start` -> `launch` -> a PlaySession in netplay mode: `netplay_runner_args` turns the launch into `--net-*` (transport `sfu`: every peer dials the relay; `host`: guests dial the host); a fresh save sandbox per match (`<data>/netplay/<title>/session-<id>/saves`); the host-selected fixed match choices (declared netplay.* enums) carried in the room and adopted before boot, other NETPLAY options using core defaults, and local display options preserved; the game never pauses (the menu opens over it, the local pad neutral); no save states, no turbo; a runner exit 4 is "The match ended", not a fault | **built** | ### 2. Runner (Retro-Runtime) diff --git a/src/hub/hub_main.cpp b/src/hub/hub_main.cpp index e3dc586..25294bc 100644 --- a/src/hub/hub_main.cpp +++ b/src/hub/hub_main.cpp @@ -19,6 +19,7 @@ #include "hub/hub_theme.hpp" #include "hub/hub_widgets.hpp" #include "hub/hub_netplay.hpp" +#include "hub/hub_netplay_options.hpp" #if !defined(RETCOMM_COMMIT) #define RETCOMM_COMMIT "" @@ -17648,6 +17649,18 @@ void prepare_net_scope(DirectHome& h, HubModel& hub) { (pkg_sha.empty() ? std::string() : ".p" + pkg_sha.substr(0, 10)); sc.max_slots = 4; sc.tpak_supported = h.tpak_supported; + sc.room_choices = [&h, &hub] { + retcomm::hub::PlayArgs args = h.d.args; + apply_player_settings(args, hub.paths.data_dir, hub.exe_dir, h.platform, h.title_key); + std::vector<retcomm::hub::NetplayChoice> choices; + for (const auto& o : h.desc.options) { + if (!retcomm::hub::room_option(o)) continue; + const auto selected = args.options.find(o.key); + choices.push_back({o.key, o.label, + selected == args.options.end() ? o.default_value : selected->second, o.values}); + } + return choices; + }; const fs::path data = hub.paths.data_dir; const std::string platform = h.platform; sc.tpak_problem = [data, platform]() -> std::string { @@ -18183,9 +18196,12 @@ bool start_net_session(retcomm::hub::PlaySession& play, const DirectHome& h, con // Except the BASE (system.romhack): the stock game in a romhack player's // place would be a silent substitution. It stays, so the match key admits // peers on the same base and refuses peers on another. + if (!l.core_options.empty() && (!h.desc_ready || !h.desc.ok)) { + *why = "The core description is unavailable; the room settings cannot be validated."; + return false; + } if (h.desc_ready && h.desc.ok) { - for (const auto& o : h.desc.options) - if (o.netplay && o.key != retcomm::kRomhackOption) args.options.erase(o.key); + if (!retcomm::hub::apply_room_options(h.desc.options, l.core_options, &args.options, why)) return false; } args.tpak_rom = {}; args.tpak_save = {}; diff --git a/src/hub/hub_netplay.cpp b/src/hub/hub_netplay.cpp index f937ddd..f708b7a 100644 --- a/src/hub/hub_netplay.cpp +++ b/src/hub/hub_netplay.cpp @@ -1,4 +1,5 @@ #include "hub/hub_netplay.hpp" +#include "hub/hub_netplay_options.hpp" #include "hub/hub_overlay.hpp" #include "hub/hub_model.hpp" @@ -374,6 +375,7 @@ struct Page { int host_max = 2; bool host_spectators = false; std::string host_error; + std::map<std::string, std::string> host_core_options; char join_pw[64]{}; std::string join_pw_lobby, join_pw_game, join_pw_version; char name_buf[64]{}; @@ -508,6 +510,34 @@ std::string local_name(const HubModel& hub) { return hub.cfg.netplay.display_name; } +void draw_host_room_choices(Page& p) { + if (!p.scope || !p.scope->room_choices) return; + for (const auto& choice : p.scope->room_choices()) { + if (choice.values.empty()) continue; + auto [it, inserted] = p.host_core_options.emplace(choice.key, choice.value); + if (std::find(choice.values.begin(), choice.values.end(), it->second) == choice.values.end()) + it->second = choice.values.front(); + ImGui::PushID(choice.key.c_str()); + ImGui::TextUnformatted(choice.label.c_str()); + if (ImGui::BeginCombo("##room_option", it->second.c_str())) { + for (const auto& value : choice.values) + if (ImGui::Selectable(value.c_str(), value == it->second)) it->second = value; + ImGui::EndCombo(); + } + ImGui::PopID(); + } +} + +std::map<std::string, std::string> chosen_room_options(const Page& p) { + std::map<std::string, std::string> result; + if (!p.scope || !p.scope->room_choices) return result; + for (const auto& choice : p.scope->room_choices()) { + const auto selected = p.host_core_options.find(choice.key); + result[choice.key] = selected == p.host_core_options.end() ? choice.value : selected->second; + } + return result; +} + void ensure_init(HubModel& hub) { Page& p = page(); if (p.init) return; @@ -1580,6 +1610,7 @@ void draw_modals(HubModel& hub, const Theme& th, const std::vector<Game>& games) ImGui::TextUnformatted("Lobby name"); ImGui::SetNextItemWidth(430.f); ImGui::InputText("##host_name", p.host_name, sizeof(p.host_name)); + draw_host_room_choices(p); ImGui::TextUnformatted("Max Players"); const int top = std::min(8, g.max_slots); p.host_max = std::clamp(p.host_max, 2, std::max(2, top)); @@ -1623,6 +1654,7 @@ void draw_modals(HubModel& hub, const Theme& th, const std::vector<Game>& games) json caps = json::object(); caps["tpak"] = p.host_tpak; caps["relay"] = "host"; // the lobby server's relay only as a fallback + caps["core_options"] = chosen_room_options(p); p.client.create_for(g.game_name, g.pin, name, p.host_pw, p.host_max, p.host_spectators, caps); if (p.filter.empty()) p.scope_dirty = true; @@ -1854,8 +1886,15 @@ void draw_modals(HubModel& hub, const Theme& th, const std::vector<Game>& games) ImGui::TextColored(th.accent, "MATCH SETTINGS"); ImGui::SameLine(); ImGui::TextColored(th.text_muted, "(set by the host)"); + if (p.scope && p.scope->room_choices && s.match_caps.contains("core_options")) { + for (const auto& choice : p.scope->room_choices()) { + const auto& selected = s.match_caps["core_options"]; + if (selected.is_object() && selected.contains(choice.key) && selected[choice.key].is_string()) + ImGui::Text("%s: %s", choice.label.c_str(), selected[choice.key].get_ref<const std::string&>().c_str()); + } + } for (auto it = s.match_caps.begin(); it != s.match_caps.end(); ++it) { - if (it.key() == "v" || it.key() == "mods") continue; + if (it.key() == "v" || it.key() == "mods" || it.key() == "core_options") continue; ImGui::Text("%s: %s", it.key().c_str(), it->dump().c_str()); } } @@ -2402,6 +2441,7 @@ void ov_host(HubModel& hub, const Theme& th, const ov::Metrics& m, const std::ve if (p.scope) ov::value_row(m, "game", "Game", g.title + " \xC2\xB7 " + g.pin); else if (ov::setting_row(m, "game", "Game", g.title, "The game the lobby is for. Guests need the same build.", games.size() > 1)) ov::open_picker("##hostgame"); + draw_host_room_choices(p); { std::vector<ov::PickerChoice> choices; for (const Game& x : games) choices.push_back({x.title, "Version " + x.pin}); @@ -2449,6 +2489,7 @@ void ov_host(HubModel& hub, const Theme& th, const ov::Metrics& m, const std::ve json caps = json::object(); caps["tpak"] = p.host_tpak; caps["relay"] = "host"; // the lobby server's relay only as a fallback + caps["core_options"] = chosen_room_options(p); p.client.create_for(g.game_name, g.pin, name, p.host_pw, p.host_max, p.host_spectators, caps); if (p.filter.empty()) p.scope_dirty = true; p.status = "Creating lobby..."; @@ -2682,9 +2723,16 @@ void ov_room(HubModel& hub, const Theme& th, const ov::Metrics& m, const std::ve } else { ov::value_row(m, "srv", "Lobby server", p.client_url); ov::value_row(m, "lid", "Lobby id", s.lobby_id); + if (p.scope && p.scope->room_choices && s.match_caps.is_object() && s.match_caps.contains("core_options")) { + for (const auto& choice : p.scope->room_choices()) { + const auto& selected = s.match_caps["core_options"]; + if (selected.is_object() && selected.contains(choice.key) && selected[choice.key].is_string()) + ov::value_row(m, choice.key.c_str(), choice.label.c_str(), selected[choice.key].get<std::string>()); + } + } if (s.match_caps.is_object() && !s.match_caps.empty()) for (auto it = s.match_caps.begin(); it != s.match_caps.end(); ++it) { - if (it.key() == "v" || it.key() == "mods") continue; + if (it.key() == "v" || it.key() == "mods" || it.key() == "core_options") continue; ov::value_row(m, it.key().c_str(), it.key().c_str(), it->dump()); } } @@ -3115,6 +3163,7 @@ void ingest(HubModel& hub) { l.host_endpoint = s.launch.value("host_endpoint", std::string()); l.host = s.is_host; l.tpak = s.match_caps.is_object() && s.match_caps.value("tpak", false); + const bool options_ok = read_room_options(s.match_caps, &l.core_options); l.seat = -1; const auto slots = s.launch.find("slots"); if (slots != s.launch.end() && slots->is_array()) { @@ -3139,7 +3188,10 @@ void ingest(HubModel& hub) { p.host_port.stop(); p.host_port_open = false; } - if (l.seat < 0) { + if (!options_ok) { + p.room_status = "The room supplied invalid match options."; + p.client.ack_launch(); + } else if (l.seat < 0) { p.room_status = "The match started without you in a player seat " "(spectating is not built yet)."; } else { diff --git a/src/hub/hub_netplay.hpp b/src/hub/hub_netplay.hpp index 2f8baed..8e7c96a 100644 --- a/src/hub/hub_netplay.hpp +++ b/src/hub/hub_netplay.hpp @@ -12,6 +12,7 @@ #include <array> #include <cstdint> #include <functional> +#include <map> #include <string> #include <vector> @@ -37,6 +38,12 @@ struct NetplayLaunch { // Transfer Pak lobbies: every seat's cartridge (this machine's copy of // it) and that seat's save as the room exchanged it. Empty = no pak. std::array<std::string, 4> tpak_rom, tpak_save; + std::map<std::string, std::string> core_options; +}; + +struct NetplayChoice { + std::string key, label, value; + std::vector<std::string> values; }; // One player's Transfer Pak, as brought to a match: which of the supported @@ -60,6 +67,7 @@ struct NetplayScope { std::string pin; // the lobby's wire version: this build, exactly int max_slots = 4; bool tpak_supported = false; + std::function<std::vector<NetplayChoice>()> room_choices; // "" when all three Transfer Pak cartridges are set and check out. std::function<std::string()> tpak_problem; // "" when this machine's seats can join a match at all; else why not diff --git a/src/hub/hub_netplay_options.hpp b/src/hub/hub_netplay_options.hpp new file mode 100644 index 0000000..3fb9bd4 --- /dev/null +++ b/src/hub/hub_netplay_options.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include "hub/hub_core_settings.hpp" +#include <algorithm> +#include <map> +#include <nlohmann/json.hpp> + +namespace retcomm::hub { + +// Explicit room controls use the netplay.* namespace. Other simulation +// options retain their existing common defaults; display options stay local. +inline bool room_option(const CoreOptionDecl& o) { + return o.netplay && o.key.rfind("netplay.", 0) == 0 && o.type == "enum" && !o.values.empty(); +} + +inline bool read_room_options(const nlohmann::json& caps, + std::map<std::string, std::string>* out) { + out->clear(); + if (!caps.is_object() || !caps.contains("core_options")) return true; + const auto& options = caps["core_options"]; + if (!options.is_object() || options.size() > 32) return false; + for (auto it = options.begin(); it != options.end(); ++it) { + if (!it.value().is_string() || it.key().size() > 128 || it.value().get_ref<const std::string&>().size() > 128) + return false; + out->emplace(it.key(), it.value().get<std::string>()); + } + return true; +} + +inline bool apply_room_options(const std::vector<CoreOptionDecl>& decl, + const std::map<std::string, std::string>& room, + std::map<std::string, std::string>* local, + std::string* error) { + for (const auto& [key, value] : room) { + const auto found = std::find_if(decl.begin(), decl.end(), [&](const auto& o) { return o.key == key; }); + if (found == decl.end() || !room_option(*found) || + std::find(found->values.begin(), found->values.end(), value) == found->values.end()) { + *error = "The room selected an unsupported match option: " + key; + return false; + } + } + for (const auto& o : decl) { + if (o.netplay && o.key != "system.romhack") local->erase(o.key); + } + local->insert(room.begin(), room.end()); + return true; +} + +} // namespace retcomm::hub diff --git a/tests/netplay_options_test.cpp b/tests/netplay_options_test.cpp new file mode 100644 index 0000000..a9b2a8f --- /dev/null +++ b/tests/netplay_options_test.cpp @@ -0,0 +1,54 @@ +#include "hub/hub_netplay_options.hpp" +#include <cstdio> + +using namespace retcomm::hub; + +int main() { + CoreOptionDecl aspect; + aspect.key = "netplay.aspect"; + aspect.type = "enum"; + aspect.netplay = aspect.restart = true; + aspect.values = {"16:9", "21:9", "32:9"}; + CoreOptionDecl scale; + scale.key = "video.internal_resolution"; + scale.type = "enum"; + scale.values = {"1x", "4x", "16x"}; + CoreOptionDecl cpu; + cpu.key = "system.cpu_mode"; + cpu.netplay = true; + const std::vector<CoreOptionDecl> decl = {aspect, scale, cpu}; + int failed = 0; + const auto check = [&](bool ok, const char* what) { + if (!ok) { std::fprintf(stderr, "FAIL: %s\n", what); ++failed; } + }; + for (const auto& selected : aspect.values) { + nlohmann::json caps = {{"core_options", {{"netplay.aspect", selected}}}}; + std::map<std::string, std::string> room; + check(read_room_options(caps, &room), "read host room options"); + std::map<std::string, std::string> local = {{"netplay.aspect", "16:9"}, + {"video.internal_resolution", "16x"}, {"video.aspect", "fit"}, + {"engine.internal_scale", "8"}, {"system.cpu_mode", "Fast"}, + {"system.romhack", "same-base"}}; + std::string error; + check(apply_room_options(decl, room, &local, &error), "adopt host aspect"); + check(local["netplay.aspect"] == selected, "host overrides guest camera choice"); + check(local["video.internal_resolution"] == "16x" && local["engine.internal_scale"] == "8", + "peer scale remains local"); + check(local["video.aspect"] == "fit", "offline preference is preserved"); + check(!local.count("system.cpu_mode"), "other simulation settings retain common defaults"); + check(local["system.romhack"] == "same-base", "romhack identity preserved"); + } + for (const auto& room : std::vector<std::map<std::string, std::string>>{ + {{"netplay.aspect", "fit"}}, {{"video.internal_resolution", "1x"}}, {{"unknown", "x"}}}) { + std::map<std::string, std::string> local = {{"video.internal_resolution", "16x"}}; + const auto before = local; + std::string error; + check(!apply_room_options(decl, room, &local, &error) && local == before, + "invalid room cannot alter options"); + } + std::map<std::string, std::string> room; + check(!read_room_options({{"core_options", {{"netplay.aspect", 1}}}}, &room), "reject non-string value"); + check(!read_room_options({{"core_options", "16:9"}}, &room), "reject non-object options"); + check(read_room_options(nlohmann::json::object(), &room) && room.empty(), "old room uses shared defaults"); + return failed ? 1 : 0; +} From e5ece7d137245678820be83c85fe4a03b277931c Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Fri, 30 Oct 2026 23:04:17 -0700 Subject: [PATCH 2/3] Fix Linux local packaging builds --- CMakeLists.txt | 3 ++- scripts/build-local.sh | 28 +++++++++++++++++++++------- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6ee7922..0aef04a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -555,7 +555,8 @@ if(RETCOMM_HAVE_SDL3 AND RETCOMM_IMGUI_READY) ${CMAKE_CURRENT_BINARY_DIR}/hub_core_settings_test.d) add_executable(retro-hub-netplay-options-test tests/netplay_options_test.cpp) target_include_directories(retro-hub-netplay-options-test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) - target_link_libraries(retro-hub-netplay-options-test PRIVATE retcomm_core retro_corelink) + target_link_libraries(retro-hub-netplay-options-test PRIVATE retcomm_core retro_corelink + ${RETCOMM_HUB_SDL_TARGET}) add_test(NAME netplay_options COMMAND retro-hub-netplay-options-test) # Romhack bases (romhacks.hpp): what the Base picker lists, a base's save # dir, and a mod's standing on a base. diff --git a/scripts/build-local.sh b/scripts/build-local.sh index 197c73d..a8035c9 100755 --- a/scripts/build-local.sh +++ b/scripts/build-local.sh @@ -135,8 +135,16 @@ cmake_args=( "-DCMAKE_BUILD_TYPE=${CONFIG}" "-DRETCOMM_COMMIT=${COMMIT}" ) -[[ -z "${RUNTIME}" ]] || cmake_args+=("-DRETCOMM_RUNTIME_DIR=${RUNTIME}") -[[ -z "${RELEASE_VERSION}" ]] || cmake_args+=("-DRETCOMM_VERSION=${RELEASE_VERSION}" -DRETCOMM_RELEASE_BUILD=ON) +if [[ -n "${RUNTIME}" ]]; then + cmake_args+=("-DRETCOMM_RUNTIME_DIR=${RUNTIME}") +else + cmake_args+=(-URETCOMM_RUNTIME_DIR) +fi +if [[ -n "${RELEASE_VERSION}" ]]; then + cmake_args+=("-DRETCOMM_VERSION=${RELEASE_VERSION}" -DRETCOMM_RELEASE_BUILD=ON) +else + cmake_args+=(-URETCOMM_VERSION -URETCOMM_RELEASE_BUILD) +fi command -v ninja >/dev/null && cmake_args+=(-G Ninja) [[ -z "${CMAKE_PREFIX_PATH:-}" ]] || cmake_args+=("-DCMAKE_PREFIX_PATH=${CMAKE_PREFIX_PATH}") # The runner finds SDL3 beside itself in the flat prefix (the hub already @@ -145,11 +153,15 @@ case "${OS}" in linux) cmake_args+=("-DCMAKE_INSTALL_RPATH=\$ORIGIN") ;; macos) cmake_args+=("-DCMAKE_INSTALL_RPATH=@loader_path") ;; esac -# Another generator or other choices in an existing cache: start it afresh. +# Keep compiled objects and caller-supplied dependency paths when definitions +# change. Only a generator transition needs a fresh CMake cache. STAMP="${BUILD}/build-local.args" if [[ -f "${STAMP}" && "$(cat "${STAMP}")" != "$(printf '%s\n' "${cmake_args[@]}")" ]]; then - say "configuration changed since the last build in ${BUILD}; reconfiguring from scratch" - rm -rf "${BUILD}/CMakeCache.txt" "${BUILD}/CMakeFiles" + say "configuration changed since the last build in ${BUILD}; reconfiguring" + old_generator="$(sed -n 's/^CMAKE_GENERATOR:INTERNAL=//p' "${BUILD}/CMakeCache.txt" 2>/dev/null || true)" + if [[ -n "${old_generator}" && "${old_generator}" != Ninja ]] && command -v ninja >/dev/null; then + rm -rf "${BUILD}/CMakeCache.txt" "${BUILD}/CMakeFiles" + fi fi say "${PLATFORM}, ${CONFIG}, build ${BUILD}" cmake "${cmake_args[@]}" >&2 @@ -196,7 +208,8 @@ done # title-app kit's bundle_dylibs.sh copies it into an app. SDL_NOTE="SDL3 from the system install name" if [[ "${OS}" == linux ]]; then - sdl="$(ldd "${BUILD}/retro-hub" | awk '$1 ~ /^libSDL3\.so/ {print $1 " " $3; exit}')" + imports="$(ldd "${BUILD}/retro-hub")" || die "cannot inspect the built hub's imports" + sdl="$(awk '$1 ~ /^libSDL3\.so/ {print $1 " " $3; exit}' <<<"${imports}")" [[ -n "${sdl}" ]] || die "retro-hub does not import libSDL3" soname="${sdl%% *}" path="${sdl#* }" [[ -f "${path}" ]] || die "retro-hub needs ${soname}, which does not resolve" @@ -212,7 +225,8 @@ field() { sed -n "s/^$1 //p" <<<"${report}"; } [[ "$(field title_app)" == 1 ]] || die "${HUB} --version has no 'title_app 1'" [[ " $(field direct_mode_flags) " == *" --package "* ]] || die "${HUB} --version lists no --package" if [[ "${OS}" == linux ]]; then - env -u LD_LIBRARY_PATH ldd "${HUB}" | grep -qF "${soname} => ${OUT}/${soname}" || + imports="$(env -u LD_LIBRARY_PATH ldd "${HUB}")" || die "cannot inspect the flat hub's imports" + grep -qF "${soname} => ${OUT}/${soname}" <<<"${imports}" || die "${HUB} does not resolve ${soname} from the prefix" fi runner_report="$(env -u LD_LIBRARY_PATH "${OUT}/retro-core-runner" --version)" || From 05196d5d5ff6e450a43f4a5cf15d573863f67807 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sat, 31 Oct 2026 00:32:13 -0700 Subject: [PATCH 3/3] Keep Windows portable startup self-contained --- CMakeLists.txt | 4 +++- src/hub/hub_main.cpp | 4 ++-- src/portable/win_portable_main.cpp | 16 +++++++--------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0aef04a..ac15059 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -215,7 +215,9 @@ if(WIN32) if(MINGW) target_compile_options(retcomm-portable PRIVATE $<$<COMPILE_LANGUAGE:CXX>:-municode>) - target_link_options(retcomm-portable PRIVATE -municode) + # The stub starts before its payload DLLs have been extracted. Keep + # the compiler runtimes inside it as well as miniz. + target_link_options(retcomm-portable PRIVATE -municode -static) endif() target_link_libraries(retcomm-portable PRIVATE shell32 retcomm_miniz) install(TARGETS retcomm-portable RUNTIME DESTINATION bin) diff --git a/src/hub/hub_main.cpp b/src/hub/hub_main.cpp index 25294bc..288a2ed 100644 --- a/src/hub/hub_main.cpp +++ b/src/hub/hub_main.cpp @@ -18191,8 +18191,8 @@ bool start_net_session(retcomm::hub::PlaySession& play, const DirectHome& h, con std::string err; retcomm::hub::PlayArgs args = h.d.args; apply_player_settings(args, hub.paths.data_dir, hub.exe_dir, h.platform, h.title_key); - // NETPLAY-flagged options join the match key: each peer's own choice - // would only get the match refused, so every peer takes the core's default. + // NETPLAY-flagged options join the match key. Every peer adopts the room's + // validated host choices; other shared options take the core's defaults. // Except the BASE (system.romhack): the stock game in a romhack player's // place would be a silent substitution. It stays, so the match key admits // peers on the same base and refuses peers on another. diff --git a/src/portable/win_portable_main.cpp b/src/portable/win_portable_main.cpp index f4a33d2..7721ed9 100644 --- a/src/portable/win_portable_main.cpp +++ b/src/portable/win_portable_main.cpp @@ -173,15 +173,13 @@ bool holds_only_runtime(const fs::path& dir) { } fs::path local_app_data() { - wchar_t* raw = nullptr; - size_t len = 0; - if (_wdupenv_s(&raw, &len, L"LOCALAPPDATA") == 0 && raw && len > 1) { - fs::path p(raw); - free(raw); - return p; - } - if (raw) free(raw); - return {}; + const DWORD capacity = GetEnvironmentVariableW(L"LOCALAPPDATA", nullptr, 0); + if (!capacity) return {}; + std::wstring value(capacity, L'\0'); + const DWORD length = GetEnvironmentVariableW(L"LOCALAPPDATA", value.data(), capacity); + if (!length || length >= capacity) return {}; + value.resize(length); + return fs::path(value); } // A title app's payload: its title.json id, and a fingerprint of the whole