launcher: integrate recomp-ui netplay flow

This commit is contained in:
Matthew Stanley
2026-07-21 21:33:33 -07:00
parent 0527806ce3
commit 87491cb035
12 changed files with 568 additions and 136 deletions
+1
View File
@@ -108,3 +108,4 @@ runtime/tests/*.exe
# transient split-gen build dirs
recompiler/build-splitgen/
recompiler/build-*/
+6 -7
View File
@@ -1,9 +1,8 @@
[submodule "lib/RmlUi"]
path = lib/RmlUi
url = https://github.com/mikke89/RmlUi.git
[submodule "lib/freetype"]
path = lib/freetype
url = https://github.com/freetype/freetype.git
[submodule "lib/recomp-net"]
path = lib/recomp-net
url = https://github.com/TechnicallyComputers/recomp-net.git
url = https://github.com/mstan/recomp-net.git
branch = feat/direct-host-peer-discovery
[submodule "lib/recomp-ui"]
path = lib/recomp-ui
url = https://github.com/mstan/recomp-ui.git
branch = feat/netplay-lobby-ui
Submodule
+1
Submodule lib/recomp-ui added at cdcee8d197
+12 -2
View File
@@ -1457,6 +1457,10 @@ UserSettings load_user_settings(const fs::path& path) {
const auto v = toml::find<std::string>(n, "player_name");
if (!v.empty()) { s.netplay_player_name = v; s.has_netplay_player_name = true; }
});
if (n.contains("lobby_url")) try_get([&]{
const auto v = toml::find<std::string>(n, "lobby_url");
if (!v.empty()) { s.netplay_lobby_url = v; s.has_netplay_lobby_url = true; }
});
}
if (doc.contains("bios")) {
const toml::value& b = toml::find(doc, "bios");
@@ -1601,8 +1605,14 @@ bool save_user_settings(const fs::path& path, const UserSettings& s) {
f << "spu_hq = " << (s.spu_hq ? "true" : "false") << "\n";
if (s.has_skip_launcher)
f << "\n[launcher]\nskip_launcher = " << (s.skip_launcher ? "true" : "false") << "\n";
if (s.has_netplay_player_name && !s.netplay_player_name.empty())
f << "\n[netplay]\nplayer_name = \"" << s.netplay_player_name << "\"\n";
if ((s.has_netplay_player_name && !s.netplay_player_name.empty()) ||
(s.has_netplay_lobby_url && !s.netplay_lobby_url.empty())) {
f << "\n[netplay]\n";
if (s.has_netplay_player_name && !s.netplay_player_name.empty())
f << "player_name = \"" << s.netplay_player_name << "\"\n";
if (s.has_netplay_lobby_url && !s.netplay_lobby_url.empty())
f << "lobby_url = \"" << s.netplay_lobby_url << "\"\n";
}
if (s.has_bios_path)
f << "\n[bios]\npath = \"" << fwd(s.bios_path) << "\"\n";
if (s.has_disc_path)
+1
View File
@@ -807,6 +807,7 @@ struct UserSettings {
// [netplay] — display name for lobby browser / room. Prompted once on first
// Netplay Lobbies visit; Change Player Name updates it.
bool has_netplay_player_name = false; std::string netplay_player_name;
bool has_netplay_lobby_url = false; std::string netplay_lobby_url;
bool has_aspect_ratio = false; int aspect_num = 4; // display aspect W:H
int aspect_den = 3; // (4:3 = native)
// [audio]
+3 -3
View File
@@ -352,7 +352,7 @@ int build_project(const Options& options, const fs::path& exe_dir) {
"set(CMAKE_C_STANDARD 99)\n"
"set(CMAKE_CXX_STANDARD 17)\n"
"set(PSXRECOMP_ROOT \"${{CMAKE_CURRENT_SOURCE_DIR}}/psxrecomp\")\n"
"set(PSX_LAUNCHER OFF CACHE BOOL \"\" FORCE)\n"
"set(PSX_RECOMP_UI OFF CACHE BOOL \"\" FORCE)\n"
"include(\"${{PSXRECOMP_ROOT}}/runtime/runtime.cmake\")\n"
"file(GLOB GAME_FULL CONFIGURE_DEPENDS\n"
" \"${{CMAKE_CURRENT_SOURCE_DIR}}/generated/*_full.c\"\n"
@@ -372,7 +372,7 @@ int build_project(const Options& options, const fs::path& exe_dir) {
write_file(options.output / "build.ps1",
"$ErrorActionPreference = 'Stop'\n"
"$Root = Split-Path -Parent $MyInvocation.MyCommand.Path\n"
"cmake -S $Root -B (Join-Path $Root 'build') -G Ninja -DCMAKE_BUILD_TYPE=Release -DPSX_LAUNCHER=OFF\n"
"cmake -S $Root -B (Join-Path $Root 'build') -G Ninja -DCMAKE_BUILD_TYPE=Release -DPSX_RECOMP_UI=OFF\n"
"if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }\n"
"cmake --build (Join-Path $Root 'build') --config Release --parallel\n"
"exit $LASTEXITCODE\n");
@@ -380,7 +380,7 @@ int build_project(const Options& options, const fs::path& exe_dir) {
"#!/usr/bin/env sh\n"
"set -eu\n"
"ROOT=$(CDPATH= cd -- \"$(dirname -- \"$0\")\" && pwd)\n"
"cmake -S \"$ROOT\" -B \"$ROOT/build\" -G Ninja -DCMAKE_BUILD_TYPE=Release -DPSX_LAUNCHER=OFF\n"
"cmake -S \"$ROOT\" -B \"$ROOT/build\" -G Ninja -DCMAKE_BUILD_TYPE=Release -DPSX_RECOMP_UI=OFF\n"
"cmake --build \"$ROOT/build\" --config Release --parallel\n");
write_file(options.output / ".gitignore",
"build/\ninput/\ngenerated/\nbios-generated/\n*.mcr\nsaves/\n");
+2 -1
View File
@@ -18,7 +18,8 @@ extern "C" {
*
* Input ownership:
* - Each peer stages one local device sample; recomp-net maps it onto
* that peer's local_slot (host 0 = sim P1, guest 1 = sim P2).
* that peer's local_slot (slot 0 = sim P1, slot 1 = sim P2). Transport
* host/client role is determined by peer_hostport, not by local_slot.
* - input_player selects which host PlayerInput (0/1) to sample; -1 = auto
* (prefer g_players[local_slot] if assigned, else player 0).
* - While active, publish / release_pads is the sole SIO writer.
+21 -4
View File
@@ -95,10 +95,8 @@ else()
option(PSX_STATIC_RUNTIME "Statically link SDL2 + libgcc/libstdc++ for a self-contained exe" OFF)
endif()
# PSX_LAUNCHER remains a game-level opt-in used by downstream game CMake files
# to include recomp-ui. Targets gain launcher code only when recomp-ui defines
# RECOMP_LAUNCHER on that target.
option(PSX_LAUNCHER "Allow downstream game projects to wire recomp-ui launcher support" ON)
# PSX_RECOMP_UI: build the shared Dear ImGui launcher from lib/recomp-ui.
option(PSX_RECOMP_UI "Build the shared recomp-ui Dear ImGui launcher" ON)
set(PSXRECOMP_RUNTIME_SOURCES
${PSXRECOMP_ROOT}/runtime/src/main.cpp
@@ -281,6 +279,7 @@ function(psxrecomp_add_runtime_target target)
WINDOW_TITLE
DEFAULT_BIOS_PATH
DEFAULT_GAME_CONFIG_PATH
LAUNCHER_BOXART
EXE_NAME
GAME_VERSION
)
@@ -533,6 +532,24 @@ function(psxrecomp_add_runtime_target target)
target_compile_definitions(${target} PRIVATE PSX_COSIM=1 PSX_NO_DEBUG_TOOLS=1)
endif()
# Shared recomp-ui Dear ImGui launcher (not in the oracle build — that's headless).
if(PSX_RECOMP_UI AND NOT PSXRT_ORACLE)
if(NOT EXISTS "${PSXRECOMP_ROOT}/lib/recomp-ui/recomp_ui.cmake")
message(FATAL_ERROR
"PSX_RECOMP_UI=ON but lib/recomp-ui is missing. "
"Run: git submodule update --init --recursive")
endif()
set(RECOMP_UI_ROOT "${PSXRECOMP_ROOT}/lib/recomp-ui" CACHE PATH
"Root directory of recomp-ui" FORCE)
include("${PSXRECOMP_ROOT}/lib/recomp-ui/recomp_ui.cmake")
set(_psx_recomp_ui_args)
if(PSXRT_LAUNCHER_BOXART)
list(APPEND _psx_recomp_ui_args BOXART "${PSXRT_LAUNCHER_BOXART}")
endif()
recomp_target_launcher_ui(${target} ${_psx_recomp_ui_args})
endif()
if(WIN32 OR MINGW)
# opengl32: GL backend (gpu_gl_renderer.c). GL 1.x is exported directly
# by opengl32; Phase 2b will load modern GL via SDL_GL_GetProcAddress.
+519 -117
View File
@@ -59,7 +59,7 @@ extern "C" void psx_event_step_conservative_env_init(void);
#include "iso_reader.h" /* text-image guard: extract the boot EXE from the disc */
#include "psx_keybinds.h" /* configurable keyboard->DualShock keybinds (keybinds.ini) */
#if defined(RECOMP_LAUNCHER)
#include "recomp_launcher.h" /* shared recomp-ui Dear ImGui launcher (prototype) */
#include "recomp_launcher.h" /* shared recomp-ui Dear ImGui launcher */
#include "launcher_profile.h" /* per-system variant profile (theme/caps bundle) */
#endif
#include <SDL.h>
@@ -85,6 +85,8 @@ extern "C" void psx_event_step_conservative_env_init(void);
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#include <commdlg.h>
#endif
@@ -3753,7 +3755,7 @@ static void sdl_vblank_present(void) {
#if defined(RECOMP_LAUNCHER)
// Host verification/inspection callbacks for the shared recomp-ui launcher.
// The launcher re-runs these on every disc/memory-card change so the "Disc
// verified" verdict + memcard block grids reflect the REAL images. The expected
// verified" verdict + memcard block grids reflect the real images. The expected
// serial/CRC are passed via these file-scope statics (set just before
// recomp_launcher_run_window) because the C-ABI callback can't capture locals.
namespace {
@@ -3771,7 +3773,7 @@ namespace {
std::snprintf(out->serial, sizeof(out->serial), "%s", serial.c_str());
std::snprintf(out->region, sizeof(out->region), "%s", id.region.c_str());
out->iso_ok = id.has_header ? 1 : 0;
// Verdict state consumed by the launcher UI.
// Verdict shown by the launcher.
if (!id.opened || !id.has_header) out->verdict = 3; // bad
else if (id.expected_serial_given && !id.serial_matches) out->verdict = 3; // wrong disc
else if (id.expected_crc_given && id.crc_computed && !id.crc_matches) out->verdict = 2; // warn
@@ -3788,6 +3790,454 @@ namespace {
for (int i = 0; i < 15; ++i) out->block_used[i] = s.block_used[i];
return 1;
}
std::string g_lnch_netplay_game_name;
std::filesystem::path g_lnch_settings_path;
std::string g_lnch_lobby_url;
RecompLauncherCNetplayLaunch g_lnch_pending_direct_launch{};
bool g_lnch_hosting_lan = false;
bool g_lnch_joined_lan = false;
std::string g_lnch_lan_endpoint;
struct AeLanLobbyState {
std::string name;
std::string game;
std::string endpoint;
std::string host_name;
std::string joiner_name;
std::string password;
bool started = false;
int host_slot = 0;
};
std::filesystem::path ae_np_lan_file() {
return std::filesystem::current_path() / "netplay_lan_lobby.txt";
}
bool ae_np_read_lan_state(AeLanLobbyState* state) {
if (!state) return false;
std::ifstream f(ae_np_lan_file());
if (!f) return false;
std::string started, host_slot;
std::getline(f, state->name);
std::getline(f, state->game);
std::getline(f, state->endpoint);
std::getline(f, state->host_name);
std::getline(f, state->joiner_name);
std::getline(f, started);
std::getline(f, host_slot);
std::getline(f, state->password);
state->started = started == "1";
state->host_slot = host_slot == "1" ? 1 : 0;
return !state->endpoint.empty();
}
bool ae_np_write_lan_state(const AeLanLobbyState& state) {
std::ofstream f(ae_np_lan_file(), std::ios::trunc);
if (!f) return false;
f << state.name << "\n"
<< state.game << "\n"
<< state.endpoint << "\n"
<< state.host_name << "\n"
<< state.joiner_name << "\n"
<< (state.started ? "1" : "0") << "\n"
<< state.host_slot << "\n"
<< state.password << "\n";
return (bool)f;
}
void ae_np_write_lan_lobby(const char* name, const char* endpoint,
const char* password) {
AeLanLobbyState state;
state.name = name && name[0] ? name : "LAN Lobby";
state.game = g_lnch_netplay_game_name.empty() ? "PSX" : g_lnch_netplay_game_name;
state.endpoint = endpoint && endpoint[0] ? endpoint : "127.0.0.1:7777";
state.host_name = psx_lobby_display_name();
if (state.host_name.empty()) state.host_name = "Host";
state.password = password ? password : "";
ae_np_write_lan_state(state);
g_lnch_hosting_lan = true;
g_lnch_joined_lan = false;
g_lnch_lan_endpoint = state.endpoint;
}
int ae_np_read_lan_lobby(RecompLauncherCNetplayLobby* out) {
if (!out) return 0;
AeLanLobbyState state;
if (!ae_np_read_lan_state(&state)) return 0;
std::snprintf(out->lobby_id, sizeof(out->lobby_id), "lan:%s", state.endpoint.c_str());
std::snprintf(out->name, sizeof(out->name), "LAN - %s",
state.name.empty() ? "Lobby" : state.name.c_str());
std::snprintf(out->game_name, sizeof(out->game_name), "%s",
state.game.empty() ? "PSX" : state.game.c_str());
out->player_count = state.joiner_name.empty() ? 1 : 2;
out->max_slots = 2;
out->has_password = state.password.empty() ? 0 : 1;
return 1;
}
PsxLobbyMatchCaps ae_netplay_caps_from_settings(const RecompLauncherCSettings* s) {
PsxLobbyMatchCaps caps{};
caps.valid = 1;
switch (s ? s->aspect_index : 0) {
case 2: caps.aspect_num = 21; caps.aspect_den = 9; break;
case 1: caps.aspect_num = 16; caps.aspect_den = 9; break;
default: caps.aspect_num = 4; caps.aspect_den = 3; break;
}
caps.turbo_loads = s ? (s->turbo_loads != 0) : 0;
caps.auto_skip_fmv = s ? (s->auto_skip_fmv != 0) : 0;
caps.input_delay = 2;
return caps;
}
const char* ae_np_default_url(void*) {
return g_lnch_lobby_url.empty() ? psx_lobby_default_url() : g_lnch_lobby_url.c_str();
}
void ae_np_save_identity(const char* player_name, const char* lobby_url) {
if (g_lnch_settings_path.empty()) return;
PSXRecompV4::UserSettings settings =
PSXRecompV4::load_user_settings(g_lnch_settings_path);
if (settings.parse_error) return;
if (player_name && player_name[0]) {
settings.netplay_player_name = player_name;
settings.has_netplay_player_name = true;
}
if (lobby_url && lobby_url[0]) {
settings.netplay_lobby_url = lobby_url;
settings.has_netplay_lobby_url = true;
}
PSXRecompV4::save_user_settings(g_lnch_settings_path, settings);
}
void ae_np_set_lobby_url(void*, const char* url) {
g_lnch_lobby_url = url && url[0] ? url : psx_lobby_default_url();
ae_np_save_identity(nullptr, g_lnch_lobby_url.c_str());
}
int ae_np_connect(void*) {
psx_lobby_set_game_identity(g_lnch_netplay_game_name.c_str(), PSX_GAME_VERSION);
return psx_lobby_connect(ae_np_default_url(nullptr));
}
int ae_np_connected(void*) {
return psx_lobby_connected();
}
void ae_np_pump(void*) {
psx_lobby_pump();
}
void ae_np_set_player_name(void*, const char* name) {
psx_lobby_set_display_name(name && name[0] ? name : "Player");
if (name && name[0]) ae_np_save_identity(name, nullptr);
}
const char* ae_np_player_name(void*) {
return psx_lobby_display_name();
}
void ae_np_request_list(void*) {
psx_lobby_request_list();
}
int ae_np_list_count(void*) {
RecompLauncherCNetplayLobby lan{};
return psx_lobby_list_count() + (ae_np_read_lan_lobby(&lan) ? 1 : 0);
}
int ae_np_list_get(void*, int index, RecompLauncherCNetplayLobby* out) {
if (!out) return 0;
const int remote_count = psx_lobby_list_count();
if (index >= remote_count)
return ae_np_read_lan_lobby(out);
PsxLobbyRow row{};
if (!psx_lobby_list_get(index, &row)) return 0;
std::snprintf(out->lobby_id, sizeof(out->lobby_id), "%s", row.lobby_id);
std::snprintf(out->name, sizeof(out->name), "%s", row.name);
std::snprintf(out->game_name, sizeof(out->game_name), "%s", row.game_name);
std::snprintf(out->game_version, sizeof(out->game_version), "%s", row.game_version);
out->player_count = row.player_count;
out->max_slots = row.max_slots;
out->has_password = row.has_password;
return 1;
}
int ae_np_local_ip(void*, char* out, size_t out_len) {
if (!out || out_len == 0) return 0;
#ifdef _WIN32
WSADATA wsa;
WSAStartup(MAKEWORD(2, 2), &wsa);
SOCKET s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (s == INVALID_SOCKET) return 0;
sockaddr_in dst{};
dst.sin_family = AF_INET;
dst.sin_port = htons(80);
inet_pton(AF_INET, "8.8.8.8", &dst.sin_addr);
if (connect(s, (sockaddr*)&dst, sizeof(dst)) == SOCKET_ERROR) {
closesocket(s);
return 0;
}
sockaddr_in local{};
int len = sizeof(local);
if (getsockname(s, (sockaddr*)&local, &len) == SOCKET_ERROR) {
closesocket(s);
return 0;
}
char buf[64] = {};
const char* ip = inet_ntop(AF_INET, &local.sin_addr, buf, sizeof(buf));
closesocket(s);
if (!ip || !buf[0]) return 0;
std::snprintf(out, out_len, "%s", buf);
return 1;
#else
std::snprintf(out, out_len, "Unavailable");
return 0;
#endif
}
int ae_np_external_ip(void*, char* out, size_t out_len) {
if (!out || out_len == 0) return 0;
#ifdef _WIN32
WSADATA wsa;
WSAStartup(MAKEWORD(2, 2), &wsa);
addrinfo hints{};
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
addrinfo* res = nullptr;
if (getaddrinfo("api.ipify.org", "80", &hints, &res) != 0 || !res)
return 0;
SOCKET s = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (s == INVALID_SOCKET) {
freeaddrinfo(res);
return 0;
}
int ok = 0;
if (connect(s, res->ai_addr, (int)res->ai_addrlen) != SOCKET_ERROR) {
const char req[] =
"GET / HTTP/1.1\r\n"
"Host: api.ipify.org\r\n"
"Connection: close\r\n\r\n";
(void)send(s, req, (int)strlen(req), 0);
char resp[1024];
int n = recv(s, resp, sizeof(resp) - 1, 0);
if (n > 0) {
resp[n] = '\0';
char* body = strstr(resp, "\r\n\r\n");
body = body ? body + 4 : resp;
char ip[64] = {};
int j = 0;
for (int i = 0; body[i] && j < (int)sizeof(ip) - 1; ++i) {
if ((body[i] >= '0' && body[i] <= '9') || body[i] == '.')
ip[j++] = body[i];
else if (j > 0)
break;
}
if (j > 0) {
std::snprintf(out, out_len, "%s", ip);
ok = 1;
}
}
}
closesocket(s);
freeaddrinfo(res);
return ok;
#else
std::snprintf(out, out_len, "Unavailable");
return 0;
#endif
}
int ae_np_create(void*, const char* lobby_name, const char* host_port,
const char* password,
const RecompLauncherCSettings* settings) {
PsxLobbyMatchCaps caps = ae_netplay_caps_from_settings(settings);
const char* endpoint = host_port && host_port[0] ? host_port : "0.0.0.0:7777";
ae_np_write_lan_lobby(lobby_name, endpoint, password);
return psx_lobby_create(lobby_name && lobby_name[0] ? lobby_name : "Netplay Lobby",
g_lnch_netplay_game_name.c_str(), PSX_GAME_VERSION,
password ? password : "", endpoint, &caps);
}
int ae_np_join(void*, const char* lobby_id, const char* password) {
if (lobby_id && strncmp(lobby_id, "lan:", 4) == 0) {
AeLanLobbyState state;
if (!ae_np_read_lan_state(&state) || !state.joiner_name.empty()) return -1;
if (state.password != (password ? password : "")) return -2;
state.joiner_name = psx_lobby_display_name();
if (state.joiner_name.empty()) state.joiner_name = "Player";
state.started = false;
if (!ae_np_write_lan_state(state)) return -1;
g_lnch_hosting_lan = false;
g_lnch_joined_lan = true;
g_lnch_lan_endpoint = state.endpoint;
return 0;
}
return psx_lobby_join(lobby_id, password ? password : "", "0.0.0.0:0");
}
int ae_np_leave(void*) {
if (g_lnch_hosting_lan) {
std::error_code ec;
std::filesystem::remove(ae_np_lan_file(), ec);
g_lnch_hosting_lan = false;
} else if (g_lnch_joined_lan) {
AeLanLobbyState state;
if (ae_np_read_lan_state(&state)) {
state.joiner_name.clear();
state.started = false;
ae_np_write_lan_state(state);
}
}
g_lnch_joined_lan = false;
g_lnch_lan_endpoint.clear();
g_lnch_pending_direct_launch = {};
return psx_lobby_leave();
}
int ae_np_in_lobby(void*) {
return g_lnch_hosting_lan || g_lnch_joined_lan || psx_lobby_in_lobby();
}
int ae_np_is_host(void*) {
if (g_lnch_hosting_lan || g_lnch_joined_lan) return g_lnch_hosting_lan ? 1 : 0;
return psx_lobby_is_host();
}
int ae_np_member_count(void*) {
if (g_lnch_hosting_lan || g_lnch_joined_lan) return 2;
return psx_lobby_member_count();
}
int ae_np_member_get(void*, int index, RecompLauncherCNetplayMember* out) {
if (!out) return 0;
if (g_lnch_hosting_lan || g_lnch_joined_lan) {
if (index < 0 || index > 1) return 0;
AeLanLobbyState state;
if (!ae_np_read_lan_state(&state)) return 0;
const bool host = index == 0;
out->slot = host ? state.host_slot : 1 - state.host_slot;
const std::string& name = host ? state.host_name : state.joiner_name;
std::snprintf(out->display_name, sizeof(out->display_name), "%s", name.c_str());
out->ready = !name.empty();
out->is_host = host ? 1 : 0;
return 1;
}
PsxLobbyMember mem{};
if (!psx_lobby_member_get(index, &mem)) return 0;
out->slot = mem.slot;
std::snprintf(out->display_name, sizeof(out->display_name), "%s", mem.display_name);
out->ready = mem.ready;
out->is_host = mem.slot == 0;
return 1;
}
int ae_np_move_member(void*, int from_slot, int to_slot) {
if (!g_lnch_hosting_lan || from_slot < 0 || from_slot > 1 ||
to_slot < 0 || to_slot > 1 || from_slot == to_slot) return -1;
AeLanLobbyState state;
if (!ae_np_read_lan_state(&state)) return -1;
state.host_slot = 1 - state.host_slot;
state.started = false;
return ae_np_write_lan_state(state) ? 0 : -1;
}
int ae_np_local_ready(void*) { return psx_lobby_local_ready(); }
int ae_np_all_ready(void*) { return psx_lobby_all_ready(); }
int ae_np_set_ready(void*, int ready) { return psx_lobby_set_ready(ready); }
int ae_np_request_start(void*, const RecompLauncherCSettings* settings) {
if (g_lnch_hosting_lan) {
AeLanLobbyState state;
if (!ae_np_read_lan_state(&state) || state.joiner_name.empty()) return -1;
state.started = true;
return ae_np_write_lan_state(state) ? 0 : -1;
}
PsxLobbyMatchCaps caps = ae_netplay_caps_from_settings(settings);
return psx_lobby_request_start(&caps);
}
int ae_np_launch_pending(void*) {
if ((g_lnch_hosting_lan || g_lnch_joined_lan) &&
!g_lnch_pending_direct_launch.enabled) {
AeLanLobbyState state;
if (ae_np_read_lan_state(&state) && state.started) {
g_lnch_pending_direct_launch = {};
g_lnch_pending_direct_launch.enabled = 1;
g_lnch_pending_direct_launch.local_slot = g_lnch_hosting_lan
? state.host_slot : 1 - state.host_slot;
g_lnch_pending_direct_launch.input_player = 0;
g_lnch_pending_direct_launch.session_id = 1;
g_lnch_pending_direct_launch.input_delay = 2;
if (g_lnch_hosting_lan) {
const size_t colon = state.endpoint.rfind(':');
const char* port = colon == std::string::npos
? "7777" : state.endpoint.c_str() + colon + 1;
std::snprintf(g_lnch_pending_direct_launch.bind_hostport,
sizeof(g_lnch_pending_direct_launch.bind_hostport),
"0.0.0.0:%s", port);
} else {
std::snprintf(g_lnch_pending_direct_launch.bind_hostport,
sizeof(g_lnch_pending_direct_launch.bind_hostport), "0.0.0.0:0");
std::snprintf(g_lnch_pending_direct_launch.peer_hostport,
sizeof(g_lnch_pending_direct_launch.peer_hostport), "%s",
state.endpoint.c_str());
}
}
}
return g_lnch_pending_direct_launch.enabled || psx_lobby_launch_pending();
}
void ae_np_clear_launch_pending(void*) {
g_lnch_pending_direct_launch = {};
psx_lobby_clear_launch_pending();
}
int ae_np_fill_launch(void*, RecompLauncherCNetplayLaunch* out) {
if (!out) return 0;
if (g_lnch_pending_direct_launch.enabled) {
*out = g_lnch_pending_direct_launch;
return 1;
}
const PsxLobbyJoinInfo* ji = psx_lobby_join_info();
if (!ji || !ji->ok) return 0;
const PsxLobbyMatchCaps* caps = psx_lobby_match_caps();
out->enabled = 1;
out->local_slot = ji->local_slot;
out->input_player = 0;
std::snprintf(out->bind_hostport, sizeof(out->bind_hostport), "%s", ji->bind_hostport);
std::snprintf(out->peer_hostport, sizeof(out->peer_hostport), "%s", ji->peer_hostport);
out->session_id = ji->session_id;
out->input_delay = (caps && caps->valid) ? caps->input_delay : 2;
return 1;
}
RecompLauncherCNetplayCallbacks g_lnch_netplay_callbacks = {
nullptr,
ae_np_default_url,
ae_np_set_lobby_url,
ae_np_connect,
ae_np_connected,
ae_np_pump,
ae_np_set_player_name,
ae_np_player_name,
ae_np_request_list,
ae_np_list_count,
ae_np_list_get,
ae_np_local_ip,
ae_np_external_ip,
ae_np_create,
ae_np_join,
ae_np_leave,
ae_np_in_lobby,
ae_np_is_host,
ae_np_member_count,
ae_np_member_get,
ae_np_move_member,
ae_np_local_ready,
ae_np_all_ready,
ae_np_set_ready,
ae_np_request_start,
ae_np_launch_pending,
ae_np_clear_launch_pending,
ae_np_fill_launch,
};
} // namespace
#endif
@@ -4384,6 +4834,9 @@ int main(int argc, char** argv) {
{
std::filesystem::path settings_path =
exe_dir_from_argv(argv[0]) / "settings.toml";
#if defined(RECOMP_LAUNCHER)
g_lnch_settings_path = settings_path;
#endif
const PSXRecompV4::UserSettings us =
PSXRecompV4::load_user_settings(settings_path);
if (us.parse_error) {
@@ -4422,6 +4875,10 @@ int main(int argc, char** argv) {
netplay_player_name = us.netplay_player_name;
has_netplay_player_name = true;
}
#if defined(RECOMP_LAUNCHER)
if (us.has_netplay_lobby_url && !us.netplay_lobby_url.empty())
g_lnch_lobby_url = us.netplay_lobby_url;
#endif
if (us.has_supersampling) g_video_scale = us.supersampling;
if (us.has_window_width) g_video_win_w = us.window_width;
if (us.has_antialiasing) g_video_aa = us.antialiasing;
@@ -4589,9 +5046,9 @@ int main(int argc, char** argv) {
const bool want_launcher =
force_launcher ||
(!std::getenv("PSX_NO_LAUNCHER") && !force_no_launcher && !skip_launcher_setting);
psx_launcher::NetplayLaunch launcher_netplay{};
if (want_launcher) {
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) == 0) {
int lr = 2; /* 0 = launch, 1 = quit, 2 = unavailable */
PSXRecompV4::UserSettings seed;
seed.renderer = g_video_renderer; seed.has_renderer = true;
seed.supersampling = g_video_scale; seed.has_supersampling = true;
@@ -4615,6 +5072,10 @@ int main(int argc, char** argv) {
seed.netplay_player_name = netplay_player_name;
seed.has_netplay_player_name = true;
}
if (!g_lnch_lobby_url.empty()) {
seed.netplay_lobby_url = g_lnch_lobby_url;
seed.has_netplay_lobby_url = true;
}
if (bios_path && bios_path[0]) { seed.bios_path = bios_path; seed.has_bios_path = true; }
if (!resolved_disc.empty()) { seed.disc_path = resolved_disc; seed.has_disc_path = true; }
seed.memcard_dir = memcard_dir; seed.has_memcard_dir = true;
@@ -4659,6 +5120,8 @@ int main(int argc, char** argv) {
ls.skip_launcher = seed.skip_launcher ? 1 : 0;
ls.msu1_enabled = 0;
ls.msu1_dir[0] = '\0';
std::snprintf(ls.netplay_player_name, sizeof(ls.netplay_player_name), "%s",
has_netplay_player_name ? netplay_player_name.c_str() : "");
ls.pad_mode[0] = seed.p1_mode;
ls.pad_mode[1] = seed.p2_mode;
/* aspect_index: 0 = 4:3, 1 = 16:9, 2 = 21:9 (see RecompLauncherCSettings). */
@@ -4772,15 +5235,22 @@ int main(int argc, char** argv) {
g_lnch_has_crc = game_has_disc_crc;
gi.disc_verify = ae_disc_verify;
gi.memcard_inspect = ae_memcard_inspect;
#if defined(PSX_HAS_RECOMP_NET) && defined(PSX_HAS_LOBBY_CLIENT)
g_lnch_netplay_game_name = game_name.empty() ? "PSX" : game_name;
gi.netplay_supported = (game_players == 2) ? 1 : 0;
gi.netplay = &g_lnch_netplay_callbacks;
#endif
char rui_out_disc[1024] = {0};
int rui_rc = recomp_launcher_run_window(
rui_title.c_str(), &ls, &gi, assets_dir_str.c_str(),
rui_initial_disc.c_str(), rui_out_disc, sizeof(rui_out_disc));
int lr = rui_rc; /* 0=Launch, 1=Quit, other=Unavailable */
lr = rui_rc;
if (lr == 0) {
seed.netplay_player_name = ls.netplay_player_name;
seed.has_netplay_player_name = true;
if (rui_out_disc[0]) {
seed.disc_path = rui_out_disc;
seed.has_disc_path = true;
@@ -4838,6 +5308,24 @@ int main(int argc, char** argv) {
seed.language = lang_menu_options[ls.language_index].code;
seed.has_language = true;
}
#if defined(PSX_HAS_LOBBY_CLIENT)
if (ls.netplay_launch.enabled) {
const PsxLobbyMatchCaps* caps = psx_lobby_match_caps();
if (caps && caps->valid) {
seed.aspect_num = caps->aspect_num ? caps->aspect_num : seed.aspect_num;
seed.aspect_den = caps->aspect_den ? caps->aspect_den : seed.aspect_den;
seed.has_aspect_ratio = true;
seed.turbo_loads = caps->turbo_loads != 0;
seed.has_turbo_loads = true;
seed.auto_skip_fmv = caps->auto_skip_fmv != 0;
seed.has_auto_skip_fmv = true;
if (caps->language[0]) {
seed.language = caps->language;
seed.has_language = true;
}
}
}
#endif
}
if (lr == 1) {
@@ -4845,6 +5333,25 @@ int main(int argc, char** argv) {
return 0;
}
if (lr == 0) {
if (ls.netplay_launch.enabled) {
net_cfg.enabled = 1;
net_cfg.local_slot = ls.netplay_launch.local_slot;
net_cfg.input_player = ls.netplay_launch.input_player;
net_cfg.session_id = ls.netplay_launch.session_id;
net_cfg.input_delay = ls.netplay_launch.input_delay;
std::snprintf(net_cfg.bind_hostport, sizeof(net_cfg.bind_hostport), "%s",
ls.netplay_launch.bind_hostport);
std::snprintf(net_cfg.peer_hostport, sizeof(net_cfg.peer_hostport), "%s",
ls.netplay_launch.peer_hostport);
g_netplay_from_lobby = 1;
std::fprintf(stdout,
"psxrecomp: launcher netplay slot=%d bind=%s peer=%s session=%u\n",
net_cfg.local_slot, net_cfg.bind_hostport, net_cfg.peer_hostport,
(unsigned)net_cfg.session_id);
std::fflush(stdout);
} else {
g_netplay_from_lobby = 0;
}
g_video_renderer = seed.renderer;
g_video_scale = seed.supersampling;
g_video_aa = seed.antialiasing;
@@ -5338,9 +5845,12 @@ session_reboot:
* Must start after SDL so local pad capture has devices; before the guest
* fiber so the first vblanks already pump HELLO/START. */
if (net_cfg.enabled) {
if (!net_cfg.peer_hostport[0] || !net_cfg.bind_hostport[0]) {
/* Transport role and gameplay slot are independent. An empty peer
* means this process listens for the first peer, even when the lobby
* owner was reordered into gameplay slot 1 (Player 2). */
if (!net_cfg.bind_hostport[0]) {
std::fprintf(stderr,
"psxrecomp: netplay refused — empty bind/peer "
"psxrecomp: netplay refused — empty bind "
"(bind='%s' peer='%s' session=%u)\n",
net_cfg.bind_hostport, net_cfg.peer_hostport,
(unsigned)net_cfg.session_id);
@@ -5686,118 +6196,10 @@ session_reboot:
return 0;
soft_return_lobby:
/* Netplay soft-exit: tear down the match window, keep lobby WS, reopen room. */
/* Netplay soft-exit: tear down the match window. Lobby resume will be
* restored through recomp-ui. */
teardown_game_session_keep_lobby();
#if defined(PSX_LAUNCHER)
{
configure_core_gl_context_attributes();
int lwin_w = g_video_win_w < 1280 ? 1280 : g_video_win_w;
int lwin_h = 0;
clamp_window_aspect(&lwin_w, &lwin_h, 4, 3);
std::string lwin_title = (game_name.empty() ? std::string("PSX") : game_name)
+ " \xE2\x80\x94 Lobby";
SDL_Window* lwin = SDL_CreateWindow(
lwin_title.c_str(), SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
lwin_w, lwin_h, SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE);
if (!lwin) {
std::fprintf(stderr, "psxrecomp: resume lobby window failed: %s\n",
SDL_GetError());
psx_lobby_disconnect();
SDL_Quit();
return 1;
}
SDL_GLContext lctx = SDL_GL_CreateContext(lwin);
if (!lctx) {
std::fprintf(stderr, "psxrecomp: resume lobby GL context failed: %s\n",
SDL_GetError());
SDL_DestroyWindow(lwin);
psx_lobby_disconnect();
SDL_Quit();
return 1;
}
SDL_GL_MakeCurrent(lwin, lctx);
SDL_GL_SetSwapInterval(1);
PSXRecompV4::UserSettings seed;
seed.renderer = g_video_renderer; seed.has_renderer = true;
seed.supersampling = g_video_scale; seed.has_supersampling = true;
seed.antialiasing = g_video_aa; seed.has_antialiasing = true;
seed.texture_filter = g_video_texfilter; seed.has_texture_filter = true;
seed.screen_kind = g_video_screen; seed.has_screen_kind = true;
seed.auto_skip_fmv = (g_auto_skip_fmv != 0); seed.has_auto_skip_fmv = true;
seed.turbo_loads = (g_turbo_loads_enabled != 0); seed.has_turbo_loads = true;
seed.fullscreen = (g_fullscreen != 0); seed.has_fullscreen = true;
seed.frame_interpolation = (g_frame_interpolation != 0);
seed.has_frame_interpolation = true;
seed.frame_interpolation_fps = g_frame_interpolation_fps;
seed.has_frame_interpolation_fps = true;
seed.aspect_num = g_video_aspect_num;
seed.aspect_den = g_video_aspect_den; seed.has_aspect_ratio = true;
seed.spu_hq = g_audio_spu_hq; seed.has_spu_hq = true;
seed.bios_path = bios_path_str; seed.has_bios_path = true;
if (!disc_path_str.empty()) {
seed.disc_path = disc_path_str; seed.has_disc_path = true;
}
seed.memcard_dir = memcard_dir; seed.has_memcard_dir = true;
seed.p1_device = p1_device; seed.has_p1_device = true;
seed.p2_device = p2_device; seed.has_p2_device = true;
seed.p1_mode = p1_mode; seed.has_p1_mode = true;
seed.p2_mode = p2_mode; seed.has_p2_mode = true;
if (has_netplay_player_name) {
seed.netplay_player_name = netplay_player_name;
seed.has_netplay_player_name = true;
}
psx_launcher::GameInfo ginfo;
ginfo.name = game_name.empty() ? nullptr : game_name.c_str();
ginfo.expected_serial = game_id.empty() ? nullptr : game_id.c_str();
ginfo.expected_crc = game_disc_crc;
ginfo.has_expected_crc = game_has_disc_crc;
ginfo.allow_hybrid = ctrl_allow_hybrid;
ginfo.lock_mode = ctrl_lock_mode;
ginfo.locked_mode = p1_mode;
ginfo.lock_device = ctrl_lock_device;
ginfo.ws_offered = ws_offered;
ginfo.ws_ultrawide_offered = ws_ultrawide_offered;
for (const auto& lo : lang_menu_options)
ginfo.languages.push_back({ lo.code, lo.label });
psx_launcher::NetplayLaunch resume_net{};
psx_launcher::RunOptions ropts;
ropts.resume_netplay_room = true;
std::string assets = exe_dir_from_argv(argv[0]).string();
psx_launcher::Result lr = psx_launcher::run(
lwin, lctx, seed, ginfo, assets.c_str(), &resume_net, ropts);
SDL_GL_DeleteContext(lctx);
SDL_DestroyWindow(lwin);
SDL_GL_ResetAttributes();
if (lr == psx_launcher::Result::Quit) {
std::fprintf(stdout, "psxrecomp: lobby closed after match; exiting.\n");
psx_lobby_disconnect();
SDL_Quit();
return 0;
}
if (lr == psx_launcher::Result::Launch && resume_net.enabled) {
net_cfg = resume_net.cfg;
net_cfg.enabled = 1;
g_netplay_from_lobby = 1;
std::fprintf(stdout,
"psxrecomp: rematch netplay slot=%d bind=%s peer=%s session=%u\n",
net_cfg.local_slot, net_cfg.bind_hostport, net_cfg.peer_hostport,
(unsigned)net_cfg.session_id);
std::fflush(stdout);
goto session_reboot;
}
std::fprintf(stdout, "psxrecomp: resume lobby ended without rematch; exiting.\n");
psx_lobby_disconnect();
SDL_Quit();
return 0;
}
#else
psx_lobby_disconnect();
SDL_Quit();
return 0;
#endif
}
+1 -1
View File
@@ -30,7 +30,7 @@ void psx_netplay_config_defaults(PsxNetplayConfig *cfg)
cfg->input_delay = 2;
cfg->session_id = 1;
strncpy(cfg->bind_hostport, "0.0.0.0:7777", sizeof(cfg->bind_hostport) - 1);
strncpy(cfg->peer_hostport, "127.0.0.1:7778", sizeof(cfg->peer_hostport) - 1);
cfg->peer_hostport[0] = '\0';
}
static unsigned env_u(const char *name, unsigned def)