mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-06 01:53:56 -06:00
feat: apply the board a mounted card names
This commit is contained in:
1 parent
ddafc2ff42
commit
9c128e46b2
8 files changed
+177
-20
No files matched your search
+1
-1
@@ -16,7 +16,7 @@
|
||||
# and Invoke-WebRequest throws on a cut transfer before the hash is read:
|
||||
# the site's build rewrites this line there (scripts/sitebootstrap.py).
|
||||
$mismatchAdvice = "The download was most likely cut short: try again. Within minutes of an update of this project the two files can come from caches of different ages. Waiting five minutes settles that."
|
||||
$defaultInstallSha256 = "d32691fb483d73a2845911957c2ce0fb18c0e7990605b3b466ef936ef8cb882e"
|
||||
$defaultInstallSha256 = "993f6128d67d32a96bcacb83ca542ef745701a2f98eb348330a1e86728ba597a"
|
||||
$maximumInstallerBytes = 2MB
|
||||
$installerExit = 0
|
||||
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
|
||||
|
||||
+59
-7
@@ -676,6 +676,12 @@ EVENT_KEYS: dict[str, tuple[str, ...]] = {
|
||||
# message says it and what it means. Not written when either version
|
||||
# is unknown, and never a refusal.
|
||||
"version": ("platform", "list_version", "installed_version", "message"),
|
||||
# Before choices: the board the system or its card names, read with no
|
||||
# --target given. applied is whether the run keeps its cores' files
|
||||
# only, false for a board the platform's list does not name. message
|
||||
# says it. A kind of its own: the full-screen interface always gives
|
||||
# --dest, and the board was never applied there.
|
||||
"board": ("platform", "board", "applied", "message"),
|
||||
# Before the file events of a check: the systems the run's list lets
|
||||
# one choose among, --system aside, in id order (other for the files
|
||||
# no system names). What Choose systems lists, and what the plan counts
|
||||
@@ -1471,6 +1477,33 @@ def derivative_board(platform: str) -> tuple[str, str] | None:
|
||||
return (name, board) if board else None
|
||||
|
||||
|
||||
# The board a card names on a partition a computer reads. Batocera writes
|
||||
# its image's board to boot/batocera.board on the BATOCERA boot partition,
|
||||
# the FAT one beside SHARE (batocera-43.1, board/batocera/scripts/
|
||||
# post-image-script.sh:60), and reads it there itself (package/batocera/
|
||||
# core/batocera-scripts/scripts/batocera-info). batocera.arch is inside the
|
||||
# image, out of a computer's reach.
|
||||
_CARD_BOARD_FILES: dict[str, str] = {"batocera": "boot/batocera.board"}
|
||||
|
||||
|
||||
def card_board(platform: str) -> str | None:
|
||||
"""The board the card mounted here names, None where no card or two
|
||||
cards of different boards do: a card is most often prepared on a
|
||||
computer, where the system's own board file does not exist."""
|
||||
where = _CARD_BOARD_FILES.get(platform)
|
||||
if where is None:
|
||||
return None
|
||||
boards: set[str] = set()
|
||||
for mount in _mounted_cards():
|
||||
try:
|
||||
value = (mount / where).read_text(encoding="utf-8", errors="replace")
|
||||
except OSError:
|
||||
continue
|
||||
if value.strip() and not _CONTROLS.search(value.strip()):
|
||||
boards.add(value.strip().lower())
|
||||
return boards.pop() if len(boards) == 1 else None
|
||||
|
||||
|
||||
def detect_board(platform: str) -> str | None:
|
||||
"""The board this system was built for, or None where it names none."""
|
||||
kind, where = _BOARD_SOURCES.get(platform, ("", ""))
|
||||
@@ -15001,19 +15034,28 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
|
||||
|
||||
# Target filtering. With no --target, a system that names its board
|
||||
# narrows to it: a Batocera RPi4 took 8.6 GiB where its board needs
|
||||
# 5.5. Only on the device itself, where the board file exists.
|
||||
# 5.5. Read from the device itself, else from the card mounted here:
|
||||
# a card prepared on a computer took every board's files, and the
|
||||
# full-screen interface, which always gives --dest, never applied one.
|
||||
target = args.target
|
||||
if (not target and not args.every_board and args.dest is None
|
||||
if (not target and not args.every_board
|
||||
and (not LOCAL.offline or local_targets(plat_name) is not None)):
|
||||
board = detect_board(plat_name)
|
||||
where = "detected"
|
||||
if not board and args.dest is not None:
|
||||
board, where = card_board(plat_name), "found on the card"
|
||||
if board:
|
||||
known = fetch_targets(plat_name) or {}
|
||||
if board in known:
|
||||
target = board
|
||||
print(f" Board {board} detected (--every-board installs all boards)")
|
||||
words = f"Board {board} {where} (--every-board installs all boards)"
|
||||
else:
|
||||
print(f" Board {board} has no {PLATFORM_NAMES.get(plat_name, plat_name)} "
|
||||
"core list: installing the files of every board")
|
||||
words = (f"Board {board} {where} has no "
|
||||
f"{PLATFORM_NAMES.get(plat_name, plat_name)} core list: "
|
||||
"installing the files of every board")
|
||||
print(f" {words}")
|
||||
EVENTS.emit("board", platform=plat_name, board=board,
|
||||
applied=target == board, message=words)
|
||||
else:
|
||||
derived = derivative_board(plat_name)
|
||||
if derived:
|
||||
@@ -16428,7 +16470,8 @@ def _folded(text: str) -> str:
|
||||
# --system plan: Back with the same ticks gives that plan back.
|
||||
_CHECK_FIELDS = ("check", "files", "systems", "choices", "set_aside", "found", "scanned",
|
||||
"unreadable", "saves", "check_summary", "checked_flags", "cli_systems",
|
||||
"facts", "ticked", "version", "checked_dests", "games", "games_unavailable")
|
||||
"facts", "ticked", "version", "board", "checked_dests", "games",
|
||||
"games_unavailable")
|
||||
|
||||
|
||||
class TuiState:
|
||||
@@ -16595,6 +16638,9 @@ class TuiState:
|
||||
self.version: str = ""
|
||||
# The note in one line, for a plan with no room for its sentences.
|
||||
self.version_short: str = ""
|
||||
# The board event of the check: the board read from the system or
|
||||
# its card, said on the plan.
|
||||
self.board: str = ""
|
||||
# The same of the confirm or preview run, and of the install: a
|
||||
# choice of systems can leave the save file out, or keep it.
|
||||
self.dry_saves: dict | None = None
|
||||
@@ -18120,7 +18166,7 @@ def _start_run(state: TuiState, run: str) -> list[tuple]:
|
||||
state.choices = state.set_aside = None
|
||||
state.found, state.scanned, state.unreadable = [], [], []
|
||||
state.check_summary = state.saves = None
|
||||
state.version = state.version_short = ""
|
||||
state.version = state.version_short = state.board = ""
|
||||
state.games = state.games_reading = state.games_unavailable = None
|
||||
state.checked_flags = tui_flags(state)
|
||||
elif run in ("confirm", "preview"):
|
||||
@@ -20042,6 +20088,8 @@ def _take_event(state: TuiState, event: dict) -> None:
|
||||
state.choices = list(event["systems"])
|
||||
elif kind == "set_aside" and state.run == "check":
|
||||
state.set_aside = set(event["dests"])
|
||||
elif kind == "board" and state.run == "check":
|
||||
state.board = str(event["message"])
|
||||
elif kind == "version" and state.run == "check":
|
||||
state.version = str(event["message"])
|
||||
state.version_short = (f"List for {display_name(state.platform, state.manifest)} "
|
||||
@@ -21256,6 +21304,10 @@ def _view_plan(state: TuiState, width: int, height: int) -> tuple:
|
||||
free = next((index for index, line in enumerate(lines)
|
||||
if str(line).startswith("Free space:")), len(lines))
|
||||
lines.insert(free, state.version)
|
||||
if state.board:
|
||||
free = next((index for index, line in enumerate(lines)
|
||||
if str(line).startswith("Free space:")), len(lines))
|
||||
lines.insert(free, state.board)
|
||||
if state.status:
|
||||
lines.append(state.status)
|
||||
if defined:
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/ins
|
||||
# curl or wget stop on a cut transfer before the hash is read: the site's
|
||||
# build rewrites this line there (scripts/sitebootstrap.py).
|
||||
MISMATCH_ADVICE="The download was most likely cut short: try again. Within minutes of an update of this project the two files can come from caches of different ages. Waiting five minutes settles that."
|
||||
DEFAULT_INSTALL_SHA256="d32691fb483d73a2845911957c2ce0fb18c0e7990605b3b466ef936ef8cb882e"
|
||||
DEFAULT_INSTALL_SHA256="993f6128d67d32a96bcacb83ca542ef745701a2f98eb348330a1e86728ba597a"
|
||||
MAX_INSTALLER_BYTES=2097152
|
||||
|
||||
cleanup() {
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
{
|
||||
"dest": "roms/ports/retrobios/install.py",
|
||||
"repo_path": "install.py",
|
||||
"size": 1091372,
|
||||
"sha256": "d32691fb483d73a2845911957c2ce0fb18c0e7990605b3b466ef936ef8cb882e",
|
||||
"size": 1093946,
|
||||
"sha256": "993f6128d67d32a96bcacb83ca542ef745701a2f98eb348330a1e86728ba597a",
|
||||
"executable": false
|
||||
},
|
||||
{
|
||||
@@ -71,8 +71,8 @@
|
||||
"muos": {
|
||||
"archive": {
|
||||
"dest": "ARCHIVE/RetroBIOS.muxapp",
|
||||
"size": 1144999,
|
||||
"sha256": "eaa8cf2c295c6d57b349ef7b28fcb48cb47a7a13c251b13ff66dedbc5764bfd6"
|
||||
"size": 1147573,
|
||||
"sha256": "e0bc43ae6a7ce3e0db3b19392eba9dc1561db3a0c95bfe3933dd7c870bdf19a8"
|
||||
},
|
||||
"members": [
|
||||
{
|
||||
@@ -92,8 +92,8 @@
|
||||
{
|
||||
"name": "RetroBIOS/install.py",
|
||||
"repo_path": "install.py",
|
||||
"size": 1091372,
|
||||
"sha256": "d32691fb483d73a2845911957c2ce0fb18c0e7990605b3b466ef936ef8cb882e",
|
||||
"size": 1093946,
|
||||
"sha256": "993f6128d67d32a96bcacb83ca542ef745701a2f98eb348330a1e86728ba597a",
|
||||
"executable": false
|
||||
},
|
||||
{
|
||||
@@ -166,8 +166,8 @@
|
||||
{
|
||||
"dest": ".config/modules/retrobios/install.py",
|
||||
"repo_path": "install.py",
|
||||
"size": 1091372,
|
||||
"sha256": "d32691fb483d73a2845911957c2ce0fb18c0e7990605b3b466ef936ef8cb882e",
|
||||
"size": 1093946,
|
||||
"sha256": "993f6128d67d32a96bcacb83ca542ef745701a2f98eb348330a1e86728ba597a",
|
||||
"executable": false
|
||||
},
|
||||
{
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"error",
|
||||
"scanned",
|
||||
"version",
|
||||
"board",
|
||||
"choices",
|
||||
"set_aside",
|
||||
"file",
|
||||
@@ -59,6 +60,9 @@
|
||||
{
|
||||
"$ref": "#/$defs/version"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/board"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/choices"
|
||||
},
|
||||
@@ -292,6 +296,34 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"board": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"event",
|
||||
"platform",
|
||||
"board",
|
||||
"applied",
|
||||
"message"
|
||||
],
|
||||
"properties": {
|
||||
"event": {
|
||||
"const": "board"
|
||||
},
|
||||
"platform": {
|
||||
"type": "string"
|
||||
},
|
||||
"board": {
|
||||
"type": "string"
|
||||
},
|
||||
"applied": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"choices": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -35,7 +35,7 @@ KEY_TYPES: dict[str, dict] = {
|
||||
**dict.fromkeys((
|
||||
"installer", "os", "os_name", "stage", "reason", "platform", "message",
|
||||
"folder", "dest", "system", "name", "bios_dir", "path", "member",
|
||||
"list_version", "installed_version",
|
||||
"list_version", "installed_version", "board",
|
||||
), _TEXT),
|
||||
"mode": {"enum": ["install", "check", "dry-run", "list"]},
|
||||
"state": {"enum": ["present", "wrong_hash", "missing"]},
|
||||
@@ -45,7 +45,7 @@ KEY_TYPES: dict[str, dict] = {
|
||||
"bytes_needed", "data_missing", "not_installable", "installed", "up_to_date",
|
||||
"pending", "code", "games", "recognised", "not_games",
|
||||
), _COUNT),
|
||||
**dict.fromkeys(("truncated", "ok", "interrupted", "required_only"), _FLAG),
|
||||
**dict.fromkeys(("truncated", "ok", "interrupted", "required_only", "applied"), _FLAG),
|
||||
**dict.fromkeys(("systems", "other_drives", "dests", "written"), _NAMES),
|
||||
**dict.fromkeys(("platforms", "failed", "skipped", "left_out", "kept", "found",
|
||||
"fallback"), _RECORDS),
|
||||
|
||||
@@ -3190,6 +3190,78 @@ class AndroidEmulatorsOutOfReachAreSaid(unittest.TestCase):
|
||||
self.assertIn("DraStic is installed", err.getvalue())
|
||||
|
||||
|
||||
class ACardPreparedOnAComputerGetsItsBoard(unittest.TestCase):
|
||||
"""A Batocera RPi4 card prepared on a PC took every board's files (8.9 GB
|
||||
where bcm2711 reads 5.8): the board was read only on the device, and
|
||||
only without --dest, which the full-screen interface always gives."""
|
||||
|
||||
def setUp(self):
|
||||
self.root = Path(_tempdir(self))
|
||||
self.boot = self.root / "BATOCERA"
|
||||
(self.boot / "boot").mkdir(parents=True)
|
||||
(self.boot / "boot" / "batocera.board").write_text("bcm2711\n")
|
||||
self.share = self.root / "SHARE"
|
||||
(self.share / "system").mkdir(parents=True)
|
||||
(self.share / "system" / "batocera.conf").write_text("system.hostname=BATOCERA\n")
|
||||
lists = self.root / "lists"
|
||||
(lists / "targets").mkdir(parents=True)
|
||||
one, two = b"pi core file", b"pc core file"
|
||||
self.list = lists / "batocera.json"
|
||||
self.list.write_text(json.dumps({
|
||||
"manifest_version": 2, "platform": "batocera", "base_destination": "bios",
|
||||
"files": [
|
||||
{"dest": "pi.bin", "sha1": hashlib.sha1(one).hexdigest(), "size": len(one),
|
||||
"repo_path": "bios/pi.bin", "cores": ["picore"], "systems": ["pi"]},
|
||||
{"dest": "pc.bin", "sha1": hashlib.sha1(two).hexdigest(), "size": len(two),
|
||||
"repo_path": "bios/pc.bin", "cores": ["pccore"], "systems": ["pc"]},
|
||||
],
|
||||
"system_cores": {"pi": ["picore"], "pc": ["pccore"]},
|
||||
}))
|
||||
(lists / "targets" / "batocera.json").write_text(
|
||||
json.dumps({"bcm2711": ["picore"], "x86_64": ["pccore"]}))
|
||||
|
||||
def run_main(self, *extra: str) -> tuple[str, list[dict]]:
|
||||
out = io.StringIO()
|
||||
argv = ["install.py", "--platform", "batocera", "--dest", str(self.share),
|
||||
"--manifest", str(self.list), "--offline", "--check", "--events", *extra]
|
||||
with unittest.mock.patch.object(install, "_mounted_cards", return_value=[self.boot]), \
|
||||
unittest.mock.patch.object(sys, "argv", argv), \
|
||||
unittest.mock.patch.dict(os.environ), \
|
||||
contextlib.redirect_stdout(out), contextlib.redirect_stderr(io.StringIO()), \
|
||||
contextlib.suppress(SystemExit):
|
||||
install.main()
|
||||
events = [json.loads(line) for line in out.getvalue().splitlines()
|
||||
if line.startswith("{")]
|
||||
return out.getvalue(), events
|
||||
|
||||
def test_the_boot_partition_names_the_board(self):
|
||||
with unittest.mock.patch.object(install, "_mounted_cards", return_value=[self.boot]):
|
||||
self.assertEqual(install.card_board("batocera"), "bcm2711")
|
||||
self.assertIsNone(install.card_board("retroarch"))
|
||||
|
||||
def test_the_run_keeps_that_boards_files(self):
|
||||
_out, events = self.run_main()
|
||||
[board] = [event for event in events if event["event"] == "board"]
|
||||
self.assertEqual((board["board"], board["applied"]), ("bcm2711", True))
|
||||
self.assertIn("found on the card", board["message"])
|
||||
files = {event["dest"] for event in events if event["event"] == "file"}
|
||||
self.assertEqual(files, {"pi.bin"})
|
||||
|
||||
def test_every_board_keeps_them_all(self):
|
||||
_out, events = self.run_main("--every-board")
|
||||
self.assertFalse([event for event in events if event["event"] == "board"])
|
||||
files = {event["dest"] for event in events if event["event"] == "file"}
|
||||
self.assertEqual(files, {"pi.bin", "pc.bin"})
|
||||
|
||||
def test_the_plan_says_it(self):
|
||||
state = install.TuiState()
|
||||
state.run = "check"
|
||||
install._take_event(state, {"event": "board", "platform": "batocera",
|
||||
"board": "bcm2711", "applied": True,
|
||||
"message": "Board bcm2711 found on the card (x)"})
|
||||
self.assertEqual(state.board, "Board bcm2711 found on the card (x)")
|
||||
|
||||
|
||||
class TheBoardTheSystemNamesIsTheTarget(unittest.TestCase):
|
||||
"""Batocera, Recalbox and ROCKNIX each write the board they were built
|
||||
for, but the installer read none of them, so a Batocera RPi4 installed
|
||||
|
||||
+2
-1
@@ -956,7 +956,7 @@ what the options name, everything by default.
|
||||
|--------|--------|
|
||||
| <span id="option-platform"></span>`--platform NAME` | Install for this platform instead of the detected one. A system `--list-platforms` names under its own files (`muos`, `arkos`) stands for the file list it gets, and the run says which. ES-DE, LaunchBox and Android, which keep no BIOS folder, are answered with the run that fills each emulator's folder. Unknown names are refused with the closest ones |
|
||||
| <span id="option-dest"></span>`--dest PATH` | Destination directory, overriding detection. Without `--platform`, the file list is the platform detected here: at a terminal the installer asks, Enter taking the one found, and otherwise it uses the one found, RetroArch's when nothing is found, and refuses when several are. The run prints whose list it is. A folder that does not exist is named with the nearest one that does, then created, except for a system that runs from its SD card, which refuses it. When more than its last level is missing (a mistyped path), a run at a terminal asks first, and the answer defaults to no |
|
||||
| <span id="option-target"></span>`--target NAME` | Keep only the files the cores of that hardware target need, and the systems they emulate. An unknown target is refused rather than ignored, since carrying on would install everything. On Batocera, Recalbox, ROCKNIX and Lakka the board is read from the system (`batocera.arch`, `recalbox.arch`, `HW_DEVICE`, `LIBREELEC_DEVICE` or `LIBREELEC_PROJECT`) and applied when no `--target` is given, from a pack alone too when it carries the core list per board |
|
||||
| <span id="option-target"></span>`--target NAME` | Keep only the files the cores of that hardware target need, and the systems they emulate. An unknown target is refused rather than ignored, since carrying on would install everything. On Batocera, Recalbox, ROCKNIX and Lakka the board is read from the system (`batocera.arch`, `recalbox.arch`, `HW_DEVICE`, `LIBREELEC_DEVICE` or `LIBREELEC_PROJECT`) and applied when no `--target` is given, from a pack alone too when it carries the core list per board. On a computer, the board a Batocera card names on its BATOCERA partition (`boot/batocera.board`) is applied the same way, in line mode and on the full screen |
|
||||
| <span id="option-every-board"></span>`--every-board` | Install the files of every board instead of the one the system names, for a card that moves between devices |
|
||||
| <span id="option-installed-cores"></span>`--installed-cores` | Keep only what the cores already installed read (RetroArch, Lakka, RetroPie). See [Installed cores](#installed-cores) |
|
||||
| <span id="option-cores-dir"></span>`--cores-dir PATH` | The folder RetroArch loads its cores from, for `--installed-cores` when it is not found. Implies `--installed-cores` |
|
||||
@@ -1774,6 +1774,7 @@ other schemas under `api/v1/schemas/`.
|
||||
| `error` | `stage`, `reason`, `platform`, `message` | The file list could not be read (`stage` is `manifest`): `reason` is `network`, `not_found` or `invalid`, `clock` when the server's certificate was refused and the system clock reads a date before the installer was written, `no_ca` when the machine holds no list of trusted certificates (`SSL_CERT_FILE` names one), `no_ssl` when this Python was built without its `ssl` module and cannot use HTTPS, or `offline` when `--offline` forbids the request. A folder named with `--from` cannot be read (`stage` is `from`, `reason` `not_found`, `not_a_folder` for a file that is no ZIP, `unreadable`, or `system` for the root of the system, `/proc`, `/sys` or `/dev`). The BIOS folder sits under a folder this user cannot open (`stage` is `dest`, `reason` `unreadable`), and the run ends with code 1 before anything is written. A platform, target, system, core or emulator named on the command line is unknown or cannot be used here (`stage` is `option`, `reason` `unknown`, `platform` the platform it was checked against or the name typed), refused before any download with code 2. The BIOS folder cannot be created (`stage` is `dest`, `reason` `not_writable`): a `summary` follows with what the run installed before it. Two systems are detected on one machine and no `--dest` names the folder (`stage` is `detect`, `reason` `conflict`). The platform named with `--platform` is found nowhere and no folder can be assumed for it: a card system with no card named, a portable emulator, Batocera or Recalbox named on a computer (their folder is the `bios` folder of their SHARE drive), or no usual folder on this system or one set aside behind a folder this user cannot open (`stage` is `detect`, `reason` `not_found`). So is a platform named by a file list or a pack, in a run without questions where it is found nowhere here. A `--dest` refused as the card of the system named carries the same `stage` and `reason`: another system's card, a folder inside a card, a drive with none of the files its card always has, or a folder that does not exist. `message` says it in words, and is the only thing that tells these cases apart |
|
||||
| `scanned` | `folder`, `files`, `truncated`, `other_drives` | Each folder named with `--from`, once read and before `detect`. `files` counts the files found, `truncated` is true when the search stopped at 200,000 of them, and `other_drives` lists the folders below it on another drive, which were not entered |
|
||||
| `version` | `platform`, `list_version`, `installed_version`, `message` | Before `choices`, when the system here runs another version than the one the list was built for: `list_version` is the list's, `installed_version` the one the system's version file states, and `message` says it and what it means. Not written when either version is unknown. The run goes on |
|
||||
| `board` | `platform`, `board`, `applied`, `message` | Before `choices`, with no `--target` and no `--every-board`, when the system names its board or a Batocera card mounted here does (`boot/batocera.board` on its BATOCERA partition): `applied` is true when the run keeps that board's files only, false for a board the platform's list does not name, which keeps every board's files. `message` says it |
|
||||
| `choices` | `platform`, `systems` | Before `checking`: the systems the run's list lets one choose among, sorted by id, `other` last for the files no system names. The run's other options apply (`--core`, `--region`, `--no-game-data`, the files left out for an unknown save directory), `--system` does not: these are the systems a screen lists to choose from, and the count a `--system` is out of |
|
||||
| `set_aside` | `platform`, `dests` | Right after `choices`: the files of the list the run sets aside before any choice, in list order, those read from a save directory whose place is not known among them. A screen chooses its cores among the rest, the cores `--core` accepts |
|
||||
| `file` | `platform`, `dest`, `systems`, `size`, `state` | Each file of the list after the check. `state` is `present` (hash verified), `wrong_hash` or `missing` |
|
||||
|
||||
Reference in new issue
Block a user