feat(runtime): MEMCARD_SLOTS for titles that only use port 1 (#704)

This commit is contained in:
Zap0xfce2
2026-10-21 23:45:02 +02:00
committed by GitHub
parent e3ea739d7c
commit da0c98b936
4 changed files with 97 additions and 1 deletions
+4
View File
@@ -2127,6 +2127,10 @@ if(BUILD_TESTING)
COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_game_runtime_pgxp.py
--cmake ${CMAKE_COMMAND})
add_test(NAME game_runtime_memcard_slots_test
COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_game_runtime_memcard_slots.py
--cmake ${CMAKE_COMMAND})
# tools/mod_channel_filter.py strips developer features from a staged
# catalog; every entry that served one ([[requirement]] included) must
# leave with it.
+17
View File
@@ -1453,6 +1453,18 @@ function(_psxrt_stage_mod_catalog target preloaded_dir)
endif()
endfunction()
# Memory-card slots a title uses (MEMCARD_SLOTS, default 2). 1 for games that
# only talk to port 1: the launcher hides slot 2 and the runtime leaves it empty.
function(psxrt_resolve_memcard_slots out_var value)
if(value STREQUAL "")
set(value 2)
endif()
if(NOT "${value}" MATCHES "^[12]$")
message(FATAL_ERROR "MEMCARD_SLOTS must be 1 or 2 (got ${value})")
endif()
set(${out_var} ${value} PARENT_SCOPE)
endfunction()
function(psxrecomp_add_runtime_target target)
# PGXP: build this target's objects with -DPSX_PGXP=1 so the PGXP_*()
# hook macros the emitter writes into ALL generated C become real calls
@@ -1479,6 +1491,7 @@ function(psxrecomp_add_runtime_target target)
EXE_NAME
GAME_VERSION
MAX_PLAYERS
MEMCARD_SLOTS
APP_ICON
# The title's own mod catalog source directory -- the one shaped like
# <repo>/mods/preloaded, holding packages/<id>/<version>/manifest.toml
@@ -2010,6 +2023,8 @@ function(psxrecomp_add_runtime_target target)
endif()
message(STATUS "psxrecomp ${target}: PSX_MAX_PLAYERS=${PSXRT_MAX_PLAYERS}")
psxrt_resolve_memcard_slots(PSXRT_MEMCARD_SLOTS "${PSXRT_MEMCARD_SLOTS}")
target_compile_definitions(${target} PRIVATE
DEFAULT_DEBUG_PORT=${PSXRT_DEBUG_PORT}
PSX_DEFAULT_BIOS_PATH="${PSXRT_DEFAULT_BIOS_PATH}"
@@ -2025,6 +2040,7 @@ function(psxrecomp_add_runtime_target target)
PSX_DEFAULT_GAME_CONFIG_PATH="${PSXRT_DEFAULT_GAME_CONFIG_PATH}"
PSX_WINDOW_TITLE="${PSXRT_WINDOW_TITLE}"
PSX_MAX_PLAYERS=${PSXRT_MAX_PLAYERS}
PSX_MEMCARD_SLOTS=${PSXRT_MEMCARD_SLOTS}
FMT_HEADER_ONLY=1
$<$<PLATFORM_ID:Windows>:NOMINMAX>
$<$<BOOL:${PSX_SDL3}>:PSX_SDL3=1>
@@ -2630,6 +2646,7 @@ endfunction()
# LAUNCHER_BOXART "${CMAKE_CURRENT_SOURCE_DIR}/launcher_assets/img/boxart.tga"
# APP_ICON "${CMAKE_CURRENT_SOURCE_DIR}/assets/psxrecomp.ico"
# MAX_PLAYERS 2
# MEMCARD_SLOTS 1 # optional; title only uses memory card port 1
# ENABLE_NETPLAY_IF_PRESENT
# ENABLE_SETUP_WIZARD
# PRELOADED_MODS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/mods/preloaded"
+10 -1
View File
@@ -82,6 +82,9 @@ extern "C" uint32_t g_guest_last_store_addr, g_guest_last_store_val;
#ifndef PSX_MAX_PLAYERS
#define PSX_MAX_PLAYERS 2
#endif
#ifndef PSX_MEMCARD_SLOTS
#define PSX_MEMCARD_SLOTS 2 /* runtime.cmake MEMCARD_SLOTS */
#endif
#include "psx_netplay.h"
#include "psx_stick.h" /* radial SDL-stick -> DualShock response transform */
#include "psx_trigger.h" /* continuous SDL trigger -> 0..255 magnitude */
@@ -16552,6 +16555,9 @@ namespace {
* the generic Display -> View mode launcher row. */
gi->widescreen_supported = 0;
gi->num_players = game_players_n;
#if defined(RECOMP_LAUNCHER_HAS_MEMCARD_SLOTS)
gi->memcard_slots = PSX_MEMCARD_SLOTS;
#endif
#if defined(RECOMP_LAUNCHER_HAS_SHARED_GAMEPAD) && !defined(PSX_NO_DEBUG_TOOLS)
gi->allow_shared_gamepad = 1;
#endif
@@ -20715,9 +20721,12 @@ session_reboot:
{
std::string mc1 = memcard1_path.string();
std::string mc2 = memcard2_path.string();
/* A one-slot title (MEMCARD_SLOTS 1) never reads port 2: leave it empty
* whatever settings.toml or the launcher stored. */
const bool slot2_on = memcard2_enabled && PSX_MEMCARD_SLOTS >= 2;
const MemcardSlotConfig slots[2] = {
{ mc1.empty() ? nullptr : mc1.c_str(), memcard1_enabled ? 1 : 0 },
{ mc2.empty() ? nullptr : mc2.c_str(), memcard2_enabled ? 1 : 0 },
{ mc2.empty() ? nullptr : mc2.c_str(), slot2_on ? 1 : 0 },
};
memcard_init_slots(memcard_dir_str.c_str(), slots);
}
@@ -0,0 +1,66 @@
#!/usr/bin/env python3
"""psxrecomp_add_game_runtime(... MEMCARD_SLOTS n): runs the REAL
psxrt_resolve_memcard_slots from runtime.cmake under `cmake -P`. Unset keeps
both slots, 1 and 2 pass through, anything else fails the configure.
"""
import argparse
import re
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
REPO = Path(__file__).resolve().parents[2]
RUNTIME_CMAKE = REPO / "runtime" / "runtime.cmake"
def extract_function(text: str, name: str) -> str:
m = re.search(r"^function\(%s\b.*?^endfunction\(\)" % re.escape(name),
text, re.S | re.M)
if not m:
raise SystemExit("FAIL: %s not found in runtime.cmake" % name)
return m.group(0)
def resolve(cmake: str, body: str, value: str):
with tempfile.TemporaryDirectory() as td:
driver = Path(td) / "driver.cmake"
driver.write_text(body + '\npsxrt_resolve_memcard_slots(_n "%s")\n'
'message(STATUS "SLOTS=${_n}")\n' % value)
r = subprocess.run([cmake, "-P", str(driver)], capture_output=True, text=True)
m = re.search(r"SLOTS=(\S+)", r.stdout + r.stderr)
return (m.group(1) if r.returncode == 0 and m else None)
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--cmake", default=shutil.which("cmake") or "cmake")
args = ap.parse_args()
if shutil.which(args.cmake) is None and not Path(args.cmake).is_file():
print("SKIP: no cmake executable available (%s)" % args.cmake)
return 0
body = extract_function(RUNTIME_CMAKE.read_text(encoding="utf-8"),
"psxrt_resolve_memcard_slots")
cases = (("", "2", "unset keeps both slots"),
("1", "1", "a port-1-only title gets one slot"),
("2", "2", "two slots pass through"),
("3", None, "three slots fail the configure"),
("0", None, "zero slots fail the configure"),
("1.5", None, "fractional slots fail the configure"),
("one", None, "non-numeric slots fail the configure"),
("1;2", None, "lists fail the configure"))
failures = 0
for value, want, what in cases:
ok = resolve(args.cmake, body, value) == want
print(("ok " if ok else "FAIL ") + what)
failures += not ok
if failures:
print("test_game_runtime_memcard_slots: %d FAILURES" % failures)
return 1
print("test_game_runtime_memcard_slots: all checks passed")
return 0
if __name__ == "__main__":
sys.exit(main())