mirror of
https://github.com/mihawk-99/PS5_RetroArch.git
synced 2026-10-21 08:10:25 -05:00
Manual Scan: choosing Content Directory again no longer crashes
A tester's v0.5.5-alpha.5 crashed when Import Content -> Manual Scan ->
Content Directory was pressed a second time after a playlist had been
made. On that second visit RetroArch opens the browser on the parent of
the folder chosen before, with that folder pending, and finds the entry
to select with strcasestr. The title linked strcasestr against
libScePosixForWebKit's stub, and at run time the import was null: the
call jumped to address 0 inside menu_entries_list_search. The first
visit has no pending selection, so it never made the call.
Six imports had this fault: strcasestr and mkstemp from the WebKit
POSIX stub, and link, symlink, readlink and pathconf from libkernel_sys's.
src/platform_wraps.c now binds all six to the platform layer (pathconf
as the frontends answer it), and tools/build-title.sh adds the --wrap
flags. tools/build.sh refuses a title that imports anything only those
two stubs define: it finds all six in the old link and none in this one.
Every other strcasestr caller in the menu (list search, Explore
filtering, Load Content's last folder) had the same fault and is fixed
too.
Tooling: tools/run-title.sh --pad-script=FILE uploads a pad script for
one run and collects its pictures. A new MENU pad action prints the list
on top of the menu stack and its selected entry, because SCREENSHOT
leaves the XMB out when no content is running.
Evidence: evidence/manual-scan-reentry/ (capture.json, expectation.json).
Base PS5 13.40, tests/pad/manual-scan-reentry.txt:
before (build 71f4bedc): signal 11, rip 0, return address in
menu_entries_list_search after callq *0x2306c40 (GLOB_DAT strcasestr)
after (build 10a05e61): the browser opened /app0/ with "assets"
selected, quit status 0, no fatal signal
Command:
PS5_FRONTENDS='es-de picker' tools/build-title.sh
tools/run-title.sh --no-build --pad-script=tests/pad/manual-scan-reentry.txt --watch 40
Gates: tools/verify.sh PASS (format unit build integration evidence).
This commit is contained in:
1 parent
f35915abca
commit
1b4ba51b74
8 files changed
+236
-4
No files matched your search
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"step": "manual-scan-reentry",
|
||||
"recorded": "2026-10-04T23:33:20Z",
|
||||
"console_run": "base PS5 13.40, runs 192114 (before) and 193202 (after)",
|
||||
"revision": "83ba60f + manual scan re-entry fix",
|
||||
"command": "PS5_FRONTENDS='es-de picker' tools/build-title.sh; tools/run-title.sh --no-build --pad-script=tests/pad/manual-scan-reentry.txt --watch 40",
|
||||
"raw_capture": "klog/manual-scan/summary.txt",
|
||||
"raw_lines": 15,
|
||||
"raw_bytes": 1364,
|
||||
"lines": [
|
||||
"==> tools/run-title.sh --no-build --pad-script=tests/pad/manual-scan-reentry.txt --watch 40 (base PS5, 13.40)",
|
||||
"before (83ba60f + MENU action, build 71f4bedc, klog/run-PPSA99169-192114.log):",
|
||||
" MENU at 17.01 s: depth 4, list 'manual_content_scan_dir' '/app0/assets', entry 1 of 7 '<Scan This Directory>'",
|
||||
" MENU at 19.01 s: depth 2, list 'deferred_manual_content_scan_list' 'Manual Scan', entry 0 of 12 'Content Directory'",
|
||||
" crash: signal 11 fault=0x0 rip=0x0 [rsp]=0x100e207 handler=0x4a3c50 (slide 0x400000)",
|
||||
" 0xc0e207 = menu_entries_list_search, after callq *0x2306c40; 0x2306c40 = R_X86_64_GLOB_DAT strcasestr",
|
||||
" kernel: # signal: 11 (SIGSEGV), # fault address: 0000000000000000",
|
||||
" old build, imports no loaded module exports: link mkstemp pathconf readlink strcasestr symlink",
|
||||
"after (this commit, build 10a05e61, klog/run-PPSA99169-193202.log):",
|
||||
" fixed build, imports no loaded module exports: none; libScePosixForWebKit.prx no longer needed",
|
||||
" MENU at 17.00 s: depth 4, list 'manual_content_scan_dir' '/app0/assets', entry 1 of 7 '<Scan This Directory>'",
|
||||
" MENU at 19.01 s: depth 2, list 'deferred_manual_content_scan_list' 'Manual Scan', entry 0 of 12 'Content Directory'",
|
||||
" MENU at 21.01 s: depth 3, list 'manual_content_scan_dir' '/app0/', entry 2 of 23 'assets', pending ''",
|
||||
" STOP at 22.01 s; native quit: frontend status = 0",
|
||||
" fatal signals in the fixed run: 0"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"step": "manual-scan-reentry",
|
||||
"note": "Import Content -> Manual Scan -> Content Directory, a folder chosen, then Content Directory again: the second visit selects the folder with strcasestr, whose import was null in the title (libScePosixForWebKit's stub), and jumped to address 0. The title now binds strcasestr, mkstemp, link, symlink, readlink and pathconf to the platform layer, and tools/build.sh refuses an import no loaded module exports.",
|
||||
"must_contain": [
|
||||
"0xc0e207 = menu_entries_list_search, after callq *0x2306c40; 0x2306c40 = R_X86_64_GLOB_DAT strcasestr",
|
||||
"fixed build, imports no loaded module exports: none",
|
||||
"MENU at 21.01 s: depth 3, list 'manual_content_scan_dir' '/app0/', entry 2 of 23 'assets'",
|
||||
"native quit: frontend status = 0",
|
||||
"fatal signals in the fixed run: 0"
|
||||
],
|
||||
"must_not_contain": [],
|
||||
"human_check": ""
|
||||
}
|
||||
+39
-2
@@ -42,7 +42,10 @@
|
||||
* prints how many new frames the core has made and the longest gap between
|
||||
* two since the last MARK, so a run's frame rate is read over exact script
|
||||
* times. RUMBLE_STRONG and RUMBLE_WEAK take a strength (0..65535) and exercise
|
||||
* controller feedback; send 0 to stop. Testing only: the file is never shipped.
|
||||
* controller feedback; send 0 to stop. `<seconds> MENU` prints where the menu
|
||||
* is: the list on top of its stack (label and path), the selected entry and the
|
||||
* pending selection, which pictures cannot show (SCREENSHOT leaves the XMB out
|
||||
* when no content runs). Testing only: the file is never shipped.
|
||||
*
|
||||
* Reference: docs/REFERENCE.md, "Input".
|
||||
*/
|
||||
@@ -68,6 +71,7 @@
|
||||
#include "command.h"
|
||||
#include "configuration.h"
|
||||
#include "core_option_manager.h"
|
||||
#include "menu/menu_driver.h"
|
||||
#if defined(HAVE_SLANG)
|
||||
#include "gfx/video_shader_parse.h"
|
||||
#include "menu/menu_shader.h"
|
||||
@@ -243,6 +247,7 @@ enum class ScriptActionKind
|
||||
effect,
|
||||
rumble_strong,
|
||||
rumble_weak,
|
||||
menu,
|
||||
};
|
||||
// The core's new frames (gfx/video_driver.c, patches/series 0101), the
|
||||
// longest time between two of them since the last MARK and how many came more
|
||||
@@ -315,7 +320,8 @@ void load_script() noexcept
|
||||
{"SCREENSHOT_FULL", ScriptActionKind::screenshot_full},
|
||||
{"MARK", ScriptActionKind::mark},
|
||||
{"RUMBLE_STRONG", ScriptActionKind::rumble_strong},
|
||||
{"RUMBLE_WEAK", ScriptActionKind::rumble_weak}};
|
||||
{"RUMBLE_WEAK", ScriptActionKind::rumble_weak},
|
||||
{"MENU", ScriptActionKind::menu}};
|
||||
// OPTION key value: set a core option as the Quick Menu does (the core
|
||||
// sees it as an update at its next frame)
|
||||
if (std::strcmp(buttons, "OPTION") == 0 && action_count < action_capacity)
|
||||
@@ -926,6 +932,37 @@ void run_script_actions() noexcept
|
||||
ps5_input_trace(note);
|
||||
continue;
|
||||
}
|
||||
if (action.kind == ScriptActionKind::menu)
|
||||
{
|
||||
char note[768];
|
||||
#ifdef HAVE_MENU
|
||||
struct menu_state *menu_st = menu_state_get_ptr();
|
||||
menu_list_t *menu_list = menu_st->entries.list;
|
||||
const file_list_t *stack = MENU_LIST_GET(menu_list, 0);
|
||||
const file_list_t *entries = MENU_LIST_GET_SELECTION(menu_list, 0);
|
||||
const char *label = "", *path = "", *selected = "";
|
||||
if (stack != nullptr && stack->size > 0)
|
||||
{
|
||||
const struct item_file &top = stack->list[stack->size - 1];
|
||||
label = top.label != nullptr ? top.label : "";
|
||||
path = top.path != nullptr ? top.path : "";
|
||||
}
|
||||
const std::size_t selection = menu_st->selection_ptr;
|
||||
if (entries != nullptr && selection < entries->size &&
|
||||
entries->list[selection].path != nullptr)
|
||||
selected = entries->list[selection].path;
|
||||
std::snprintf(note, sizeof(note),
|
||||
"input: pad script MENU at %.2f s: depth %zu, list '%.120s' '%.200s', "
|
||||
"entry %zu of %zu '%.200s', pending '%.120s'",
|
||||
seconds, stack != nullptr ? stack->size : 0, label, path, selection,
|
||||
entries != nullptr ? entries->size : 0, selected,
|
||||
menu_st->pending_selection);
|
||||
#else
|
||||
std::snprintf(note, sizeof(note), "input: pad script MENU at %.2f s: no menu", seconds);
|
||||
#endif
|
||||
ps5_input_trace(note);
|
||||
continue;
|
||||
}
|
||||
if (action.kind == ScriptActionKind::rumble_strong ||
|
||||
action.kind == ScriptActionKind::rumble_weak)
|
||||
{
|
||||
|
||||
@@ -12,8 +12,12 @@
|
||||
* tools/core-imports.py.
|
||||
*/
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <limits.h>
|
||||
#include <stdarg.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <ps5platform/libc.h>
|
||||
|
||||
@@ -76,3 +80,55 @@ int __wrap_closedir(DIR *directory)
|
||||
{
|
||||
return ps5_closedir(directory);
|
||||
}
|
||||
|
||||
/* No module a title loads exports these, so each import was null at run time:
|
||||
* strcasestr and mkstemp linked against libScePosixForWebKit's stub, link,
|
||||
* symlink, readlink and pathconf against libkernel_sys's. RetroArch's menu
|
||||
* finds the entry it should select with strcasestr: Import Content -> Manual
|
||||
* Scan -> Content Directory, opened a second time, starts on the parent of the
|
||||
* folder chosen before with that folder pending, and jumped to address 0
|
||||
* (2026-10-04). tools/build.sh refuses a title that imports one again. */
|
||||
char *__wrap_strcasestr(const char *haystack, const char *needle)
|
||||
{
|
||||
return ps5_strcasestr(haystack, needle);
|
||||
}
|
||||
|
||||
int __wrap_mkstemp(char *path_template)
|
||||
{
|
||||
return ps5_mkstemp(path_template);
|
||||
}
|
||||
|
||||
int __wrap_link(const char *existing, const char *name)
|
||||
{
|
||||
return ps5_link(existing, name);
|
||||
}
|
||||
|
||||
int __wrap_symlink(const char *target, const char *name)
|
||||
{
|
||||
return ps5_symlink(target, name);
|
||||
}
|
||||
|
||||
ssize_t __wrap_readlink(const char *path, char *buffer, size_t size)
|
||||
{
|
||||
return ps5_readlink(path, buffer, size);
|
||||
}
|
||||
|
||||
/* libc++'s std::filesystem::current_path sizes its buffer with pathconf; the
|
||||
* platform layer has none, so the limits are the console's FreeBSD ones for a
|
||||
* path that exists, as the frontends answer (frontends/common/frontend_shims.c). */
|
||||
long __wrap_pathconf(const char *path, int name)
|
||||
{
|
||||
struct stat status;
|
||||
if (!path || stat(path, &status) != 0)
|
||||
return -1;
|
||||
switch (name)
|
||||
{
|
||||
case _PC_PATH_MAX:
|
||||
return PATH_MAX;
|
||||
case _PC_NAME_MAX:
|
||||
return NAME_MAX;
|
||||
default:
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
# Import Content -> Manual Scan -> Content Directory, a folder chosen, then
|
||||
# Content Directory again: the second visit opens the browser on the folder's
|
||||
# parent with the folder selected, which a tester's v0.5.5-alpha.5 crashed on.
|
||||
# The menu's OK is B on this console (menu_swap_ok_cancel_buttons), and XMB's
|
||||
# tabs are Main Menu, Settings, History, Favourites, Images, Import Content.
|
||||
# The folder browser lists Parent Directory, <Use this directory>, then the
|
||||
# folders. Each MENU line prints the list on top and the selected entry.
|
||||
4.0 RIGHT
|
||||
4.5 RIGHT
|
||||
5.0 RIGHT
|
||||
5.5 RIGHT
|
||||
6.0 RIGHT
|
||||
7.5 MENU # Import Content, Manual Scan
|
||||
8.0 B # Manual Scan
|
||||
9.5 MENU # Manual Scan, Content Directory first
|
||||
10.0 B # Content Directory: the browser at /app0, first visit
|
||||
11.5 MENU
|
||||
12.0 DOWN
|
||||
12.5 DOWN
|
||||
13.5 MENU # /app0's first folder
|
||||
14.0 B # into it
|
||||
15.5 MENU
|
||||
16.0 DOWN
|
||||
17.0 MENU # <Use this directory>
|
||||
17.5 B # use it: back on Manual Scan with the folder set
|
||||
19.0 MENU
|
||||
19.5 B # Content Directory again: the parent, the folder pending
|
||||
21.0 MENU # the browser on the parent, the folder selected
|
||||
22.0 STOP
|
||||
@@ -311,6 +311,11 @@ directory_wrap_flags+=" --wrap=getcwd"
|
||||
# Folders the title or a core creates are 0777 and files at least 0666, so FTP,
|
||||
# which runs as another user, can reach them (src/permissions_ps5.cpp).
|
||||
directory_wrap_flags+=" --wrap=mkdir --wrap=open --wrap=fopen"
|
||||
# No module a title loads exports these: each import was null at run time, and
|
||||
# RetroArch's menu search (strcasestr) jumped to address 0 from Manual Scan's
|
||||
# Content Directory (src/platform_wraps.c). tools/build.sh refuses the title
|
||||
# should one be imported again.
|
||||
directory_wrap_flags+=" --wrap=strcasestr --wrap=mkstemp --wrap=link --wrap=symlink --wrap=readlink --wrap=pathconf"
|
||||
echo "==> [title] step 2/3: the title"
|
||||
# Large frontend/core buffers use mapped memory; wrap all ownership operations.
|
||||
PS5_PAYLOAD_SDK="$sdk" \
|
||||
|
||||
@@ -298,6 +298,22 @@ fi
|
||||
--exclude-libs=ALL -L "$build/obj" \
|
||||
-e _start -o "$build/llvm-pie.elf" "${link_inputs[@]}" \
|
||||
--as-needed "$sdk_root"/target/lib/*.so
|
||||
# A title loads neither libkernel_sys's exports nor libScePosixForWebKit's: an
|
||||
# import only their stubs define links, and is null at run time, so its first
|
||||
# call jumps to address 0 (getcwd and the *at functions; strcasestr from the
|
||||
# menu's list search, 2026-10-04). Each is bound to the platform layer instead
|
||||
# (src/platform_wraps.c, the --wrap flags in tools/build-title.sh).
|
||||
mapfile -t null_imports < <(comm -23 \
|
||||
<("$sdk_root/bin/llvm-nm" -D --undefined-only "$build/llvm-pie.elf" |
|
||||
awk '$1 == "U" { sub(/@.*/, "", $2); print $2 }' | sort -u) \
|
||||
<(for stub in "$sdk_root"/target/lib/*.so "$agc_stub" "$agc_driver_stub"; do
|
||||
case $(basename "$stub") in libkernel_sys.so | libScePosixForWebKit.so) continue ;; esac
|
||||
"$sdk_root/bin/llvm-nm" -D --defined-only "$stub" 2>/dev/null | awk '{ print $NF }'
|
||||
done | sort -u))
|
||||
((${#null_imports[@]} == 0)) || {
|
||||
echo "error: the title imports what no module it loads exports (null at run time): ${null_imports[*]}" >&2
|
||||
exit 1
|
||||
}
|
||||
"$tool" link --in "$build/llvm-pie.elf" --out "$build/eboot.elf" \
|
||||
--stub-dir "$sdk_root/target/lib" --stub "$agc_stub" \
|
||||
--stub "$agc_driver_stub" --module-sdk "$module_sdk" \
|
||||
|
||||
+51
-2
@@ -27,6 +27,9 @@
|
||||
# tools/run-title.sh --picker-test=300:es-de --frontend-capture=20 --watch 60
|
||||
# the picker, as the home screen starts it: 300 frames, a
|
||||
# picture of the last, then it chooses EmulationStation
|
||||
# tools/run-title.sh --pad-script=tests/pad/manual-scan-reentry.txt --watch 40
|
||||
# RetroArch with the pad scripted (src/input_ps5.cpp says
|
||||
# how); its SCREENSHOT pictures are collected afterwards
|
||||
#
|
||||
# Why this exists. Every earlier round of the console loop was four hand-driven
|
||||
# steps that needed a person: build, upload, launch, read. Two things went wrong
|
||||
@@ -68,6 +71,7 @@ frontend_capture=
|
||||
frontend_scroll=
|
||||
picker_test=
|
||||
retroarch_frames=0
|
||||
pad_script=
|
||||
while (( $# )); do
|
||||
case "$1" in
|
||||
--no-build) build=0 ;;
|
||||
@@ -89,7 +93,8 @@ while (( $# )); do
|
||||
--frontend-launch) frontend_scroll=${frontend_scroll:+$frontend_scroll,}launch ;;
|
||||
--picker-test=*) picker_test=${1#*=} ;;
|
||||
--retroarch-frames=*) retroarch_frames=${1#*=} ;;
|
||||
*) echo "usage: ${0##*/} [--no-build] [--no-deploy] [--watch SECONDS] [--gpu-profile 1..60] [--audio-test] [--core-test[=fceumm|mgba|snes9x|fbneo|genesis_plus_gx|ppsspp]] [--relaunch-test[=1..20] [--relaunch-image=PATH]] [--display-modes-test[=frames]] [--frontend-capture=SECONDS[,SECONDS...] [--frontend-scroll] [--frontend-profile] [--frontend-quit] [--frontend-launch]] [--picker-test=FRAMES:retroarch|es-de|none] [--retroarch-frames=N]" >&2; exit 2 ;;
|
||||
--pad-script=*) pad_script=${1#*=} ;;
|
||||
*) echo "usage: ${0##*/} [--no-build] [--no-deploy] [--watch SECONDS] [--gpu-profile 1..60] [--audio-test] [--core-test[=fceumm|mgba|snes9x|fbneo|genesis_plus_gx|ppsspp]] [--relaunch-test[=1..20] [--relaunch-image=PATH]] [--display-modes-test[=frames]] [--frontend-capture=SECONDS[,SECONDS...] [--frontend-scroll] [--frontend-profile] [--frontend-quit] [--frontend-launch]] [--picker-test=FRAMES:retroarch|es-de|none] [--retroarch-frames=N] [--pad-script=FILE]" >&2; exit 2 ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
@@ -146,6 +151,14 @@ fi
|
||||
[[ $retroarch_frames =~ ^[0-9]+$ ]] && (( retroarch_frames <= 100000 )) ||
|
||||
{ echo "--retroarch-frames takes 1..100000 frames" >&2; exit 2; }
|
||||
|
||||
# The pad script (src/input_ps5.cpp): presses and actions timed from RetroArch's
|
||||
# first poll, at most 128 lines.
|
||||
if [[ -n $pad_script ]]; then
|
||||
[[ -f $pad_script ]] || { echo "--pad-script: no file $pad_script" >&2; exit 2; }
|
||||
(( $(grep -cvE '^[[:space:]]*(#|$)' "$pad_script") <= 128 )) ||
|
||||
{ echo "--pad-script holds more than 128 presses and actions" >&2; exit 2; }
|
||||
fi
|
||||
|
||||
if (( audio_test && watch < 20 )); then
|
||||
echo "--audio-test requires --watch of at least 20 seconds" >&2
|
||||
exit 2
|
||||
@@ -236,7 +249,7 @@ fi
|
||||
# appeared. A run that does not ask for extras must not inherit them, so the file
|
||||
# is removed on every run - before the launch, because deleting it afterwards
|
||||
# would leave it for the next one if this run dies.
|
||||
python3 - "$title_id" "$profile" "$audio_test" "$core_test" "$relaunch_test" "$relaunch_run" "$display_modes_test" "$relaunch_image" "$frontend_capture" "$capture_run" "$frontend_scroll" "$picker_test" "$retroarch_frames" <<'PY'
|
||||
python3 - "$title_id" "$profile" "$audio_test" "$core_test" "$relaunch_test" "$relaunch_run" "$display_modes_test" "$relaunch_image" "$frontend_capture" "$capture_run" "$frontend_scroll" "$picker_test" "$retroarch_frames" "$pad_script" <<'PY'
|
||||
import importlib.util, sys
|
||||
spec = importlib.util.spec_from_file_location("dt", "tools/deploy-title.py")
|
||||
dt = importlib.util.module_from_spec(spec); spec.loader.exec_module(dt)
|
||||
@@ -313,6 +326,17 @@ with connect(**dt.load_settings()) as ftp:
|
||||
ftp.storbinary(f"STOR /data/homebrew/{sys.argv[1]}/args.txt",
|
||||
io.BytesIO(f"--max-frames={sys.argv[13]}\n".encode()))
|
||||
print(f" RetroArch quits after {sys.argv[13]} frames")
|
||||
# The pad script, and the pictures an earlier one took: a test launch keeps
|
||||
# /app0/pad-script.txt, so one left behind would drive this run.
|
||||
remove_if_present(ftp, f"/data/homebrew/{sys.argv[1]}/pad-script.txt")
|
||||
from ps5_ftp import list_names
|
||||
for name in sorted(list_names(ftp, f"/data/homebrew/{sys.argv[1]}")):
|
||||
if name.startswith("pad-shot-") and name.endswith(".png"):
|
||||
remove_if_present(ftp, f"/data/homebrew/{sys.argv[1]}/{name}")
|
||||
if sys.argv[14]:
|
||||
with open(sys.argv[14], "rb") as script:
|
||||
ftp.storbinary(f"STOR /data/homebrew/{sys.argv[1]}/pad-script.txt", script)
|
||||
print(f" armed pad script {sys.argv[14]}")
|
||||
# The launch is a test run's (src/main.cpp): it keeps the test files this run
|
||||
# armed, and starts RetroArch rather than the frontend picker
|
||||
# (src/frontend_mode_ps5.cpp) unless a picker test is armed.
|
||||
@@ -427,6 +451,31 @@ else:
|
||||
PY
|
||||
fi
|
||||
|
||||
# --- the pad script's pictures ------------------------------------------------
|
||||
if [[ -n $pad_script ]]; then
|
||||
python3 - "$title_id" "$stamp" "$pad_script" <<'PY'
|
||||
import importlib.util, shutil, sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, "tools")
|
||||
from ps5_ftp import connect, list_names, remove_if_present
|
||||
spec = importlib.util.spec_from_file_location("dt", "tools/deploy-title.py")
|
||||
dt = importlib.util.module_from_spec(spec); spec.loader.exec_module(dt)
|
||||
base = f"/data/homebrew/{sys.argv[1]}"
|
||||
out = Path("klog") / f"pad-{sys.argv[2]}"
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(sys.argv[3], out / "pad-script.txt")
|
||||
with connect(**dt.load_settings()) as ftp:
|
||||
remove_if_present(ftp, f"{base}/pad-script.txt")
|
||||
shots = sorted((name for name in list_names(ftp, base) if name.startswith("pad-shot-") and name.endswith(".png")),
|
||||
key=lambda name: int(name[9:-4]) if name[9:-4].isdigit() else 0)
|
||||
for name in shots:
|
||||
with (out / name).open("wb") as stream:
|
||||
ftp.retrbinary(f"RETR {base}/{name}", stream.write)
|
||||
remove_if_present(ftp, f"{base}/{name}")
|
||||
print(f" pad script: {len(shots)} picture(s) saved to {out}")
|
||||
PY
|
||||
fi
|
||||
|
||||
# --- the frontend's pictures and logs ----------------------------------------
|
||||
# Collected before RetroArch's logs, whose checks stop the script on a failure.
|
||||
if [[ -n $frontend_capture ]]; then
|
||||
|
||||
Reference in new issue
Block a user