mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-06 01:53:56 -06:00
feat: install for the games on the card
--for-games reads the card's rom folders through install/games/ and keeps what the games need: per system what its default core reads, one region where the content proves it (crc32 and size, a disc serial, a header the core reads), an arcade set's BIOS parents. A game not recognised gets every BIOS of its system and is named. --required-only keeps only what starts. The plan and the line menu open on it when the card holds games, everything one press away. A cache keeps what reading a card found.
This commit is contained in:
1 parent
b69b33178b
commit
2e9175a8e9
28 files changed
+4030
-36
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 = "5b8bd7b486d236ee1d00439b2bdd4f6fefcf20112abe3b0f1037e38426d80c15"
|
||||
$defaultInstallSha256 = "8cf9ac83b620610cb469e65158e9e7f4efd0509242db86820403457c69ab3628"
|
||||
$maximumInstallerBytes = 2MB
|
||||
$installerExit = 0
|
||||
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
|
||||
|
||||
+1803
-23
File diff suppressed because it is too large.
Load diff
+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="5b8bd7b486d236ee1d00439b2bdd4f6fefcf20112abe3b0f1037e38426d80c15"
|
||||
DEFAULT_INSTALL_SHA256="8cf9ac83b620610cb469e65158e9e7f4efd0509242db86820403457c69ab3628"
|
||||
MAX_INSTALLER_BYTES=2097152
|
||||
|
||||
cleanup() {
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
{
|
||||
"dest": "roms/ports/retrobios/install.py",
|
||||
"repo_path": "install.py",
|
||||
"size": 996397,
|
||||
"sha256": "5b8bd7b486d236ee1d00439b2bdd4f6fefcf20112abe3b0f1037e38426d80c15",
|
||||
"size": 1077524,
|
||||
"sha256": "8cf9ac83b620610cb469e65158e9e7f4efd0509242db86820403457c69ab3628",
|
||||
"executable": false
|
||||
},
|
||||
{
|
||||
@@ -71,8 +71,8 @@
|
||||
"muos": {
|
||||
"archive": {
|
||||
"dest": "ARCHIVE/RetroBIOS.muxapp",
|
||||
"size": 1049574,
|
||||
"sha256": "30bbe8a0f78cc7b6bb37b9290c69e2244e0b9217339d9ea695f45c0d7815a263"
|
||||
"size": 1130701,
|
||||
"sha256": "4381cfaa7fe2363671f334f1567e31e763df78d11fbadab3de135d7937878715"
|
||||
},
|
||||
"members": [
|
||||
{
|
||||
@@ -92,8 +92,8 @@
|
||||
{
|
||||
"name": "RetroBIOS/install.py",
|
||||
"repo_path": "install.py",
|
||||
"size": 996397,
|
||||
"sha256": "5b8bd7b486d236ee1d00439b2bdd4f6fefcf20112abe3b0f1037e38426d80c15",
|
||||
"size": 1077524,
|
||||
"sha256": "8cf9ac83b620610cb469e65158e9e7f4efd0509242db86820403457c69ab3628",
|
||||
"executable": false
|
||||
},
|
||||
{
|
||||
@@ -166,8 +166,8 @@
|
||||
{
|
||||
"dest": ".config/modules/retrobios/install.py",
|
||||
"repo_path": "install.py",
|
||||
"size": 996397,
|
||||
"sha256": "5b8bd7b486d236ee1d00439b2bdd4f6fefcf20112abe3b0f1037e38426d80c15",
|
||||
"size": 1077524,
|
||||
"sha256": "8cf9ac83b620610cb469e65158e9e7f4efd0509242db86820403457c69ab3628",
|
||||
"executable": false
|
||||
},
|
||||
{
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
"file",
|
||||
"system",
|
||||
"checking",
|
||||
"games_reading",
|
||||
"games",
|
||||
"games_unavailable",
|
||||
"check",
|
||||
"declared",
|
||||
"data",
|
||||
@@ -71,6 +74,15 @@
|
||||
{
|
||||
"$ref": "#/$defs/checking"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/games_reading"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/games"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/games_unavailable"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/check"
|
||||
},
|
||||
@@ -437,6 +449,105 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"games_reading": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"event",
|
||||
"files_done",
|
||||
"files_total"
|
||||
],
|
||||
"properties": {
|
||||
"event": {
|
||||
"const": "games_reading"
|
||||
},
|
||||
"files_done": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"files_total": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"games": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"event",
|
||||
"platform",
|
||||
"folder",
|
||||
"games",
|
||||
"recognised",
|
||||
"found",
|
||||
"fallback",
|
||||
"not_games",
|
||||
"required_only"
|
||||
],
|
||||
"properties": {
|
||||
"event": {
|
||||
"const": "games"
|
||||
},
|
||||
"platform": {
|
||||
"type": "string"
|
||||
},
|
||||
"folder": {
|
||||
"type": "string"
|
||||
},
|
||||
"games": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"recognised": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"found": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"fallback": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"not_games": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"required_only": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"games_unavailable": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"event",
|
||||
"platform",
|
||||
"reason",
|
||||
"message"
|
||||
],
|
||||
"properties": {
|
||||
"event": {
|
||||
"const": "games_unavailable"
|
||||
},
|
||||
"platform": {
|
||||
"type": "string"
|
||||
},
|
||||
"reason": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"check": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -43,11 +43,12 @@ KEY_TYPES: dict[str, dict] = {
|
||||
"files", "files_done", "files_total", "bytes", "bytes_done", "bytes_total",
|
||||
"bytes_local", "bytes_network", "size", "present", "missing", "wrong_hash",
|
||||
"bytes_needed", "data_missing", "not_installable", "installed", "up_to_date",
|
||||
"pending", "code",
|
||||
"pending", "code", "games", "recognised", "not_games",
|
||||
), _COUNT),
|
||||
**dict.fromkeys(("truncated", "ok", "interrupted"), _FLAG),
|
||||
**dict.fromkeys(("truncated", "ok", "interrupted", "required_only"), _FLAG),
|
||||
**dict.fromkeys(("systems", "other_drives", "dests", "written"), _NAMES),
|
||||
**dict.fromkeys(("platforms", "failed", "skipped", "left_out", "kept"), _RECORDS),
|
||||
**dict.fromkeys(("platforms", "failed", "skipped", "left_out", "kept", "found",
|
||||
"fallback"), _RECORDS),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -25,3 +25,9 @@ os.environ.setdefault(
|
||||
(Path(__file__).resolve().parent.parent / "tmp" / "tests").mkdir(
|
||||
parents=True, exist_ok=True
|
||||
)
|
||||
# The installer's cache (the games index, what reading a card found) is the
|
||||
# suite's own: a run never reads nor fills the one of the person running it.
|
||||
os.environ.setdefault(
|
||||
"RETROBIOS_CACHE",
|
||||
str(Path(__file__).resolve().parent.parent / "tmp" / "tests" / "cache"),
|
||||
)
|
||||
Vendored
+82
@@ -900,3 +900,85 @@
|
||||
|
||||
|
||||
Any key closes this help
|
||||
=== plan-games
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: RetroArch^[[0m
|
||||
|
||||
^[[0;1m ^[[0;1;35m4^[[0;1m files to download, ^[[0;1;35m1.0 MB^[[0m
|
||||
BIOS folder: ^[[0;1m~/.config/retroarch/system^[[0m
|
||||
For the 412 games on this card.
|
||||
One region where the games prove it: nintendo-gba Europe.
|
||||
2 games get every BIOS of their system: Show the games.
|
||||
^[[0;32m1 in place^[[0m, ^[[0;33m3 missing^[[0m, ^[[0;33m1 wrong copy^[[0m
|
||||
The ^[[0;33m1 wrong copy^[[0m is replaced, the old one kept in a backup folder.
|
||||
Free space: 50.0 GB
|
||||
|
||||
^[[0;7;35m > Install^[[0m
|
||||
For the games on this card: yes 412 games
|
||||
Only what starts: no
|
||||
Show the games
|
||||
Choose systems
|
||||
Copy the BIOS files from your old card or a folder
|
||||
Options
|
||||
Quit
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;1;35mA/Enter^[[0m Install ^[[0;2mB/Esc^[[0m Back ^[[0;2mY/o^[[0m Options ^[[0;2m?^[[0m Help
|
||||
=== games
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Games on this card^[[0m
|
||||
|
||||
412 games in /media/user/SDCARD/Roms, 398 recognised by their content.
|
||||
|
||||
sony-playstation (PS): 400 games, 390 recognised, every region
|
||||
nintendo-gba (GBA): 12 games, 8 recognised, Europe only
|
||||
|
||||
Every BIOS of their system kept, so that they start:
|
||||
GBA/demo.gba: not recognised
|
||||
GBA/hack.gba: not recognised
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;1;35mA/Enter^[[0m Back
|
||||
Vendored
+26
@@ -284,3 +284,29 @@
|
||||
BIOS folder:
|
||||
^[[0;1m/home/user/.config/retroarch/system^[[0m
|
||||
Any key closes this help
|
||||
=== plan-games
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: RetroArch^[[0m
|
||||
|
||||
^[[0;1m ^[[0;1;35m4^[[0;1m files to download, ^[[0;1;35m1.0 MB^[[0m
|
||||
BIOS folder:
|
||||
^[[0;1m~/.config/retroarch/system^[[0m
|
||||
|
||||
^[[0;7;35m > Install^[[0m
|
||||
For the games on this card: yes 412
|
||||
Only what starts: no
|
||||
Show the games
|
||||
^[[0;2m 1-4 of 8^[[0m
|
||||
^[[0;1;35mA^[[0m Install ^[[0;2mB^[[0m Back ^[[0;2mY^[[0m Options ^[[0;2m?^[[0m Help
|
||||
=== games
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Games on this card^[[0m
|
||||
|
||||
412 games in /media/user/SDCARD/Roms,
|
||||
398 recognised by their content.
|
||||
|
||||
sony-playstation (PS): 400 games, 390
|
||||
recognised, every region
|
||||
nintendo-gba (GBA): 12 games, 8
|
||||
recognised, Europe only
|
||||
|
||||
^[[0;2m 1-5 of 9^[[0m
|
||||
^[[0;1;35mA^[[0m Back ^[[0;2m^v^[[0m Scroll
|
||||
Vendored
+32
@@ -350,3 +350,35 @@
|
||||
|
||||
|
||||
Any key closes this help
|
||||
=== plan-games
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: RetroArch^[[0m
|
||||
|
||||
^[[0;1m ^[[0;1;35m4^[[0;1m files to download, ^[[0;1;35m1.0 MB^[[0m
|
||||
BIOS folder:
|
||||
^[[0;1m~/.config/retroarch/system^[[0m
|
||||
For the 412 games on this card.
|
||||
^[[0;7;35m > Install^[[0m
|
||||
For the games on this card: yes 412
|
||||
Only what starts: no
|
||||
Show the games
|
||||
Choose systems
|
||||
Copy from your old card or a folder
|
||||
Options
|
||||
Quit
|
||||
^[[0;1;35mA^[[0m Install ^[[0;2mB^[[0m Back ^[[0;2mY^[[0m Options ^[[0;2m?^[[0m Help
|
||||
=== games
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Games on this card^[[0m
|
||||
|
||||
412 games in /media/user/SDCARD/Roms,
|
||||
398 recognised by their content.
|
||||
|
||||
sony-playstation (PS): 400 games, 390
|
||||
recognised, every region
|
||||
nintendo-gba (GBA): 12 games, 8
|
||||
recognised, Europe only
|
||||
|
||||
Every BIOS of their system kept, so
|
||||
that they start:
|
||||
GBA/demo.gba: not recognised
|
||||
GBA/hack.gba: not recognised
|
||||
^[[0;1;35mA^[[0m Back
|
||||
Vendored
+50
@@ -548,3 +548,53 @@
|
||||
|
||||
|
||||
Any key closes this help
|
||||
=== plan-games
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: RetroArch^[[0m
|
||||
|
||||
^[[0;1m ^[[0;1;35m4^[[0;1m files to download, ^[[0;1;35m1.0 MB^[[0m
|
||||
BIOS folder: ^[[0;1m~/.config/retroarch/system^[[0m
|
||||
For the 412 games on this card.
|
||||
One region where the games prove it: nintendo-gba Europe.
|
||||
2 games get every BIOS of their system: Show the games.
|
||||
^[[0;32m1 in place^[[0m, ^[[0;33m3 missing^[[0m, ^[[0;33m1 wrong copy^[[0m
|
||||
The ^[[0;33m1 wrong copy^[[0m is replaced, the old one kept in a backup folder.
|
||||
Free space: 50.0 GB
|
||||
|
||||
^[[0;7;35m > Install^[[0m
|
||||
For the games on this card: yes 412 games
|
||||
Only what starts: no
|
||||
Show the games
|
||||
Choose systems
|
||||
Copy the BIOS files from your old card or a folder
|
||||
Options
|
||||
Quit
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;1;35mA/Enter^[[0m Install ^[[0;2mB/Esc^[[0m Back ^[[0;2mY/o^[[0m Options ^[[0;2m?^[[0m Help
|
||||
=== games
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Games on this card^[[0m
|
||||
|
||||
412 games in /media/user/SDCARD/Roms, 398 recognised by their content.
|
||||
|
||||
sony-playstation (PS): 400 games, 390 recognised, every region
|
||||
nintendo-gba (GBA): 12 games, 8 recognised, Europe only
|
||||
|
||||
Every BIOS of their system kept, so that they start:
|
||||
GBA/demo.gba: not recognised
|
||||
GBA/hack.gba: not recognised
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
^[[0;1;35mA/Enter^[[0m Back
|
||||
Vendored
+40
@@ -0,0 +1,40 @@
|
||||
RetroBIOS: Games on this card
|
||||
|
||||
412 games in /media/user/SDCARD/Roms, 398 recognised by their content.
|
||||
|
||||
sony-playstation (PS): 400 games, 390 recognised, every region
|
||||
nintendo-gba (GBA): 12 games, 8 recognised, Europe only
|
||||
|
||||
Every BIOS of their system kept, so that they start:
|
||||
GBA/demo.gba: not recognised
|
||||
GBA/hack.gba: not recognised
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
A/Enter Back
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
RetroBIOS: Games on this card
|
||||
|
||||
412 games in /media/user/SDCARD/Roms,
|
||||
398 recognised by their content.
|
||||
|
||||
sony-playstation (PS): 400 games, 390
|
||||
recognised, every region
|
||||
nintendo-gba (GBA): 12 games, 8
|
||||
recognised, Europe only
|
||||
|
||||
1-5 of 9
|
||||
A Back ^v Scroll
|
||||
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
RetroBIOS: Games on this card
|
||||
|
||||
412 games in /media/user/SDCARD/Roms,
|
||||
398 recognised by their content.
|
||||
|
||||
sony-playstation (PS): 400 games, 390
|
||||
recognised, every region
|
||||
nintendo-gba (GBA): 12 games, 8
|
||||
recognised, Europe only
|
||||
|
||||
Every BIOS of their system kept, so
|
||||
that they start:
|
||||
GBA/demo.gba: not recognised
|
||||
GBA/hack.gba: not recognised
|
||||
A Back
|
||||
Vendored
+24
@@ -0,0 +1,24 @@
|
||||
RetroBIOS: Games on this card
|
||||
|
||||
412 games in /media/user/SDCARD/Roms, 398 recognised by their content.
|
||||
|
||||
sony-playstation (PS): 400 games, 390 recognised, every region
|
||||
nintendo-gba (GBA): 12 games, 8 recognised, Europe only
|
||||
|
||||
Every BIOS of their system kept, so that they start:
|
||||
GBA/demo.gba: not recognised
|
||||
GBA/hack.gba: not recognised
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
A/Enter Back
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
4 files to download, 1.0 MB
|
||||
BIOS folder: ~/.config/retroarch/system
|
||||
For the 412 games on this card.
|
||||
One region where the games prove it: nintendo-gba Europe.
|
||||
2 games get every BIOS of their system: Show the games.
|
||||
1 in place, 3 missing, 1 wrong copy
|
||||
The 1 wrong copy is replaced, the old one kept in a backup folder.
|
||||
Free space: 50.0 GB
|
||||
|
||||
> Install
|
||||
For the games on this card: yes 412 games
|
||||
Only what starts: no
|
||||
Show the games
|
||||
Choose systems
|
||||
Copy the BIOS files from your old card or a folder
|
||||
Options
|
||||
Quit
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
A/Enter Install B/Esc Back Y/o Options ? Help
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
4 files to download, 1.0 MB
|
||||
BIOS folder:
|
||||
~/.config/retroarch/system
|
||||
|
||||
> Install
|
||||
For the games on this card: yes 412
|
||||
Only what starts: no
|
||||
Show the games
|
||||
1-4 of 8
|
||||
A Install B Back Y Options ? Help
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
4 files to download, 1.0 MB
|
||||
BIOS folder:
|
||||
~/.config/retroarch/system
|
||||
For the 412 games on this card.
|
||||
> Install
|
||||
For the games on this card: yes 412
|
||||
Only what starts: no
|
||||
Show the games
|
||||
Choose systems
|
||||
Copy from your old card or a folder
|
||||
Options
|
||||
Quit
|
||||
A Install B Back Y Options ? Help
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
4 files to download, 1.0 MB
|
||||
BIOS folder: ~/.config/retroarch/system
|
||||
For the 412 games on this card.
|
||||
One region where the games prove it: nintendo-gba Europe.
|
||||
2 games get every BIOS of their system: Show the games.
|
||||
1 in place, 3 missing, 1 wrong copy
|
||||
The 1 wrong copy is replaced, the old one kept in a backup folder.
|
||||
Free space: 50.0 GB
|
||||
|
||||
> Install
|
||||
For the games on this card: yes 412 games
|
||||
Only what starts: no
|
||||
Show the games
|
||||
Choose systems
|
||||
Copy the BIOS files from your old card or a folder
|
||||
Options
|
||||
Quit
|
||||
|
||||
|
||||
|
||||
|
||||
A/Enter Install B/Esc Back Y/o Options ? Help
|
||||
Vendored
+79
@@ -1054,3 +1054,82 @@
|
||||
|
||||
|
||||
Any key closes this help
|
||||
=== plan-games 40x12
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
4 files to download, 1.0 MB
|
||||
BIOS folder:
|
||||
~/.config/retroarch/system
|
||||
|
||||
> Install
|
||||
For the games on this card: yes 412
|
||||
Only what starts: no
|
||||
Show the games
|
||||
1-4 of 8
|
||||
A Install B Back Y Options
|
||||
=== plan-games 40x15
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
4 files to download, 1.0 MB
|
||||
BIOS folder:
|
||||
~/.config/retroarch/system
|
||||
For the 412 games on this card.
|
||||
> Install
|
||||
For the games on this card: yes 412
|
||||
Only what starts: no
|
||||
Show the games
|
||||
Choose systems
|
||||
Copy from your old card or a folder
|
||||
Options
|
||||
Quit
|
||||
A Install B Back Y Options
|
||||
=== plan-games 80x24
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
4 files to download, 1.0 MB
|
||||
BIOS folder: ~/.config/retroarch/system
|
||||
For the 412 games on this card.
|
||||
One region where the games prove it: nintendo-gba Europe.
|
||||
2 games get every BIOS of their system: Show the games.
|
||||
1 in place, 3 missing, 1 wrong copy
|
||||
The 1 wrong copy is replaced, the old one kept in a backup folder.
|
||||
Free space: 50.0 GB
|
||||
|
||||
> Install
|
||||
For the games on this card: yes 412 games
|
||||
Only what starts: no
|
||||
Show the games
|
||||
Choose systems
|
||||
Copy the BIOS files from your old card or a folder
|
||||
Options
|
||||
Quit
|
||||
|
||||
|
||||
|
||||
|
||||
A Install B Back Y Options
|
||||
=== games 80x24
|
||||
RetroBIOS: Games on this card
|
||||
|
||||
412 games in /media/user/SDCARD/Roms, 398 recognised by their content.
|
||||
|
||||
sony-playstation (PS): 400 games, 390 recognised, every region
|
||||
nintendo-gba (GBA): 12 games, 8 recognised, Europe only
|
||||
|
||||
Every BIOS of their system kept, so that they start:
|
||||
GBA/demo.gba: not recognised
|
||||
GBA/hack.gba: not recognised
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
A Back
|
||||
+79
@@ -1054,3 +1054,82 @@
|
||||
|
||||
|
||||
Any key closes this help
|
||||
=== plan-games 40x12
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
4 files to download, 1.0 MB
|
||||
BIOS folder:
|
||||
~/.config/retroarch/system
|
||||
|
||||
> Install
|
||||
For the games on this card: yes 412
|
||||
Only what starts: no
|
||||
Show the games
|
||||
1-4 of 8
|
||||
A Install B Back Y Options
|
||||
=== plan-games 40x15
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
4 files to download, 1.0 MB
|
||||
BIOS folder:
|
||||
~/.config/retroarch/system
|
||||
For the 412 games on this card.
|
||||
> Install
|
||||
For the games on this card: yes 412
|
||||
Only what starts: no
|
||||
Show the games
|
||||
Choose systems
|
||||
Copy from your old card or a folder
|
||||
Options
|
||||
Quit
|
||||
A Install B Back Y Options
|
||||
=== plan-games 80x24
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
4 files to download, 1.0 MB
|
||||
BIOS folder: ~/.config/retroarch/system
|
||||
For the 412 games on this card.
|
||||
One region where the games prove it: nintendo-gba Europe.
|
||||
2 games get every BIOS of their system: Show the games.
|
||||
1 in place, 3 missing, 1 wrong copy
|
||||
The 1 wrong copy is replaced, the old one kept in a backup folder.
|
||||
Free space: 50.0 GB
|
||||
|
||||
> Install
|
||||
For the games on this card: yes 412 games
|
||||
Only what starts: no
|
||||
Show the games
|
||||
Choose systems
|
||||
Copy the BIOS files from your old card or a folder
|
||||
Options
|
||||
Quit
|
||||
|
||||
|
||||
|
||||
|
||||
A Install B Back Y Options
|
||||
=== games 80x24
|
||||
RetroBIOS: Games on this card
|
||||
|
||||
412 games in /media/user/SDCARD/Roms, 398 recognised by their content.
|
||||
|
||||
sony-playstation (PS): 400 games, 390 recognised, every region
|
||||
nintendo-gba (GBA): 12 games, 8 recognised, Europe only
|
||||
|
||||
Every BIOS of their system kept, so that they start:
|
||||
GBA/demo.gba: not recognised
|
||||
GBA/hack.gba: not recognised
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
A Back
|
||||
+79
@@ -1054,3 +1054,82 @@
|
||||
|
||||
|
||||
Any key closes this help
|
||||
=== plan-games 40x12
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
4 files to download, 1.0 MB
|
||||
BIOS folder:
|
||||
~/.config/retroarch/system
|
||||
|
||||
> Install
|
||||
For the games on this card: yes 412
|
||||
Only what starts: no
|
||||
Show the games
|
||||
1-4 of 8
|
||||
A Install B Back Y Options
|
||||
=== plan-games 40x15
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
4 files to download, 1.0 MB
|
||||
BIOS folder:
|
||||
~/.config/retroarch/system
|
||||
For the 412 games on this card.
|
||||
> Install
|
||||
For the games on this card: yes 412
|
||||
Only what starts: no
|
||||
Show the games
|
||||
Choose systems
|
||||
Copy from your old card or a folder
|
||||
Options
|
||||
Quit
|
||||
A Install B Back Y Options
|
||||
=== plan-games 80x24
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
4 files to download, 1.0 MB
|
||||
BIOS folder: ~/.config/retroarch/system
|
||||
For the 412 games on this card.
|
||||
One region where the games prove it: nintendo-gba Europe.
|
||||
2 games get every BIOS of their system: Show the games.
|
||||
1 in place, 3 missing, 1 wrong copy
|
||||
The 1 wrong copy is replaced, the old one kept in a backup folder.
|
||||
Free space: 50.0 GB
|
||||
|
||||
> Install
|
||||
For the games on this card: yes 412 games
|
||||
Only what starts: no
|
||||
Show the games
|
||||
Choose systems
|
||||
Copy the BIOS files from your old card or a folder
|
||||
Options
|
||||
Quit
|
||||
|
||||
|
||||
|
||||
|
||||
A Install B Back Y Options
|
||||
=== games 80x24
|
||||
RetroBIOS: Games on this card
|
||||
|
||||
412 games in /media/user/SDCARD/Roms, 398 recognised by their content.
|
||||
|
||||
sony-playstation (PS): 400 games, 390 recognised, every region
|
||||
nintendo-gba (GBA): 12 games, 8 recognised, Europe only
|
||||
|
||||
Every BIOS of their system kept, so that they start:
|
||||
GBA/demo.gba: not recognised
|
||||
GBA/hack.gba: not recognised
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
A Back
|
||||
Vendored
+108
@@ -1132,3 +1132,111 @@
|
||||
|
||||
|
||||
Any key closes this help
|
||||
=== plan-games 40x12
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
4 files to download, 1.0 MB
|
||||
BIOS folder:
|
||||
~/.config/retroarch/system
|
||||
|
||||
> Install
|
||||
For the games on this card: yes 412
|
||||
Only what starts: no
|
||||
Show the games
|
||||
1-4 of 8
|
||||
Enter Install Esc Back o Options
|
||||
=== plan-games 40x15
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
4 files to download, 1.0 MB
|
||||
BIOS folder:
|
||||
~/.config/retroarch/system
|
||||
For the 412 games on this card.
|
||||
> Install
|
||||
For the games on this card: yes 412
|
||||
Only what starts: no
|
||||
Show the games
|
||||
Choose systems
|
||||
Copy from your old card or a folder
|
||||
Options
|
||||
Quit
|
||||
Enter Install Esc Back o Options
|
||||
=== plan-games 80x24
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
4 files to download, 1.0 MB
|
||||
BIOS folder: ~/.config/retroarch/system
|
||||
For the 412 games on this card.
|
||||
One region where the games prove it: nintendo-gba Europe.
|
||||
2 games get every BIOS of their system: Show the games.
|
||||
1 in place, 3 missing, 1 wrong copy
|
||||
The 1 wrong copy is replaced, the old one kept in a backup folder.
|
||||
Free space: 50.0 GB
|
||||
|
||||
> Install
|
||||
For the games on this card: yes 412 games
|
||||
Only what starts: no
|
||||
Show the games
|
||||
Choose systems
|
||||
Copy the BIOS files from your old card or a folder
|
||||
Options
|
||||
Quit
|
||||
|
||||
|
||||
|
||||
|
||||
Enter Install Esc Back o Options ? Help
|
||||
=== games 40x12
|
||||
RetroBIOS: Games on this card
|
||||
|
||||
412 games in /media/user/SDCARD/Roms,
|
||||
398 recognised by their content.
|
||||
|
||||
sony-playstation (PS): 400 games, 390
|
||||
recognised, every region
|
||||
nintendo-gba (GBA): 12 games, 8
|
||||
recognised, Europe only
|
||||
|
||||
1-5 of 9
|
||||
Enter Back ^v Scroll
|
||||
=== games 40x15
|
||||
RetroBIOS: Games on this card
|
||||
|
||||
412 games in /media/user/SDCARD/Roms,
|
||||
398 recognised by their content.
|
||||
|
||||
sony-playstation (PS): 400 games, 390
|
||||
recognised, every region
|
||||
nintendo-gba (GBA): 12 games, 8
|
||||
recognised, Europe only
|
||||
|
||||
Every BIOS of their system kept, so
|
||||
that they start:
|
||||
GBA/demo.gba: not recognised
|
||||
GBA/hack.gba: not recognised
|
||||
Enter Back
|
||||
=== games 80x24
|
||||
RetroBIOS: Games on this card
|
||||
|
||||
412 games in /media/user/SDCARD/Roms, 398 recognised by their content.
|
||||
|
||||
sony-playstation (PS): 400 games, 390 recognised, every region
|
||||
nintendo-gba (GBA): 12 games, 8 recognised, Europe only
|
||||
|
||||
Every BIOS of their system kept, so that they start:
|
||||
GBA/demo.gba: not recognised
|
||||
GBA/hack.gba: not recognised
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Enter Back
|
||||
Vendored
+79
@@ -1128,3 +1128,82 @@
|
||||
|
||||
|
||||
Any key closes this help
|
||||
=== plan-games 40x12
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
4 files to download, 1.0 MB
|
||||
BIOS folder:
|
||||
~/.config/retroarch/system
|
||||
|
||||
> Install
|
||||
For the games on this card: yes 412
|
||||
Only what starts: no
|
||||
Show the games
|
||||
1-4 of 8
|
||||
A Install R2 Back L2 Options
|
||||
=== plan-games 40x15
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
4 files to download, 1.0 MB
|
||||
BIOS folder:
|
||||
~/.config/retroarch/system
|
||||
For the 412 games on this card.
|
||||
> Install
|
||||
For the games on this card: yes 412
|
||||
Only what starts: no
|
||||
Show the games
|
||||
Choose systems
|
||||
Copy from your old card or a folder
|
||||
Options
|
||||
Quit
|
||||
A Install R2 Back L2 Options
|
||||
=== plan-games 80x24
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
4 files to download, 1.0 MB
|
||||
BIOS folder: ~/.config/retroarch/system
|
||||
For the 412 games on this card.
|
||||
One region where the games prove it: nintendo-gba Europe.
|
||||
2 games get every BIOS of their system: Show the games.
|
||||
1 in place, 3 missing, 1 wrong copy
|
||||
The 1 wrong copy is replaced, the old one kept in a backup folder.
|
||||
Free space: 50.0 GB
|
||||
|
||||
> Install
|
||||
For the games on this card: yes 412 games
|
||||
Only what starts: no
|
||||
Show the games
|
||||
Choose systems
|
||||
Copy the BIOS files from your old card or a folder
|
||||
Options
|
||||
Quit
|
||||
|
||||
|
||||
|
||||
|
||||
A Install R2 Back L2 Options
|
||||
=== games 80x24
|
||||
RetroBIOS: Games on this card
|
||||
|
||||
412 games in /media/user/SDCARD/Roms, 398 recognised by their content.
|
||||
|
||||
sony-playstation (PS): 400 games, 390 recognised, every region
|
||||
nintendo-gba (GBA): 12 games, 8 recognised, Europe only
|
||||
|
||||
Every BIOS of their system kept, so that they start:
|
||||
GBA/demo.gba: not recognised
|
||||
GBA/hack.gba: not recognised
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
A Back
|
||||
Vendored
+108
@@ -1186,3 +1186,111 @@
|
||||
|
||||
|
||||
Any key closes this help
|
||||
=== plan-games 40x12
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
4 files to download, 1.0 MB
|
||||
BIOS folder:
|
||||
~/.config/retroarch/system
|
||||
|
||||
> Install
|
||||
For the games on this card: yes 412
|
||||
Only what starts: no
|
||||
Show the games
|
||||
1-4 of 8
|
||||
Start Install B Back
|
||||
=== plan-games 40x15
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
4 files to download, 1.0 MB
|
||||
BIOS folder:
|
||||
~/.config/retroarch/system
|
||||
For the 412 games on this card.
|
||||
> Install
|
||||
For the games on this card: yes 412
|
||||
Only what starts: no
|
||||
Show the games
|
||||
Choose systems
|
||||
Copy from your old card or a folder
|
||||
Options
|
||||
Quit
|
||||
Start Install B Back
|
||||
=== plan-games 80x24
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
4 files to download, 1.0 MB
|
||||
BIOS folder: ~/.config/retroarch/system
|
||||
For the 412 games on this card.
|
||||
One region where the games prove it: nintendo-gba Europe.
|
||||
2 games get every BIOS of their system: Show the games.
|
||||
1 in place, 3 missing, 1 wrong copy
|
||||
The 1 wrong copy is replaced, the old one kept in a backup folder.
|
||||
Free space: 50.0 GB
|
||||
|
||||
> Install
|
||||
For the games on this card: yes 412 games
|
||||
Only what starts: no
|
||||
Show the games
|
||||
Choose systems
|
||||
Copy the BIOS files from your old card or a folder
|
||||
Options
|
||||
Quit
|
||||
|
||||
|
||||
|
||||
|
||||
Start Install B Back
|
||||
=== games 40x12
|
||||
RetroBIOS: Games on this card
|
||||
|
||||
412 games in /media/user/SDCARD/Roms,
|
||||
398 recognised by their content.
|
||||
|
||||
sony-playstation (PS): 400 games, 390
|
||||
recognised, every region
|
||||
nintendo-gba (GBA): 12 games, 8
|
||||
recognised, Europe only
|
||||
|
||||
1-5 of 9
|
||||
Start Back ^v Scroll
|
||||
=== games 40x15
|
||||
RetroBIOS: Games on this card
|
||||
|
||||
412 games in /media/user/SDCARD/Roms,
|
||||
398 recognised by their content.
|
||||
|
||||
sony-playstation (PS): 400 games, 390
|
||||
recognised, every region
|
||||
nintendo-gba (GBA): 12 games, 8
|
||||
recognised, Europe only
|
||||
|
||||
Every BIOS of their system kept, so
|
||||
that they start:
|
||||
GBA/demo.gba: not recognised
|
||||
GBA/hack.gba: not recognised
|
||||
Start Back
|
||||
=== games 80x24
|
||||
RetroBIOS: Games on this card
|
||||
|
||||
412 games in /media/user/SDCARD/Roms, 398 recognised by their content.
|
||||
|
||||
sony-playstation (PS): 400 games, 390 recognised, every region
|
||||
nintendo-gba (GBA): 12 games, 8 recognised, Europe only
|
||||
|
||||
Every BIOS of their system kept, so that they start:
|
||||
GBA/demo.gba: not recognised
|
||||
GBA/hack.gba: not recognised
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Start Back
|
||||
@@ -0,0 +1,947 @@
|
||||
"""install.py --for-games: what the games on a card need.
|
||||
|
||||
The games index (install/games/) is a fixture written per test, read from
|
||||
the folder beside --manifest as a run offline reads it, so a test pins the
|
||||
rules and not the state of the data. Two tests read the repository's own
|
||||
index, published by scripts/generate_games_index.py.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import functools
|
||||
import gzip
|
||||
import hashlib
|
||||
import http.server
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import unittest
|
||||
import zipfile
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
import yaml
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO_ROOT))
|
||||
|
||||
import install # noqa: E402
|
||||
|
||||
TMP_ROOT = REPO_ROOT / "tmp" / "tests"
|
||||
TMP_ROOT.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def _crc(data: bytes) -> str:
|
||||
return f"{zlib.crc32(data) & 0xFFFFFFFF:08x}"
|
||||
|
||||
|
||||
MD_EU = b"md europe game " * 64
|
||||
MD_JP = b"md japan game " * 64
|
||||
MD_ZIPPED = b"md zipped usa game " * 64
|
||||
GC_HEADER = 0x458
|
||||
|
||||
# Every file the fixture list carries: dest -> (size, regions, cores, systems).
|
||||
FILES = {
|
||||
"scph5500.bin": (512, ["japan"], None, ["sony-playstation"]),
|
||||
"scph5501.bin": (512, ["north-america"], None, ["sony-playstation"]),
|
||||
"scph5502.bin": (512, ["europe"], None, ["sony-playstation"]),
|
||||
"bios_CD_E.bin": (128, ["europe"], None, ["sega-megacd"]),
|
||||
"bios_CD_U.bin": (128, ["north-america"], None, ["sega-megacd"]),
|
||||
"bios_CD_J.bin": (128, ["japan"], None, ["sega-megacd"]),
|
||||
"cd_extra.bin": (16, None, ["picodrive"], None),
|
||||
"md_eu.bin": (64, ["europe"], None, ["sega-megadrive"]),
|
||||
"md_us.bin": (64, ["north-america"], None, ["sega-megadrive"]),
|
||||
"md_jp.bin": (64, ["japan"], None, ["sega-megadrive"]),
|
||||
"md_tmss.bin": (32, None, None, ["sega-megadrive"]),
|
||||
"dolphin/GC/USA/IPL.bin": (256, ["north-america"], ["dolphin"], None),
|
||||
"dolphin/GC/EUR/IPL.bin": (256, ["europe"], ["dolphin"], None),
|
||||
"dolphin/GC/JAP/IPL.bin": (256, ["japan"], ["dolphin"], None),
|
||||
"neogeo.zip": (300, None, None, ["snk-neogeo"]),
|
||||
"fbneo/neogeo.zip": (300, None, ["fbneo"], None),
|
||||
"fbneo/pgm.zip": (200, None, ["fbneo"], None),
|
||||
"fbneo/samples/big.zip": (9000, None, ["fbneo"], None),
|
||||
"gba_bios.bin": (100, None, None, ["nintendo-gba"]),
|
||||
}
|
||||
# The fallback sets: key -> (files, optional).
|
||||
SETS = {
|
||||
"sony-playstation|beetle_psx": (["scph5500.bin", "scph5501.bin", "scph5502.bin"], []),
|
||||
"sega-megacd|picodrive": (["bios_CD_E.bin", "bios_CD_U.bin", "bios_CD_J.bin",
|
||||
"cd_extra.bin"], ["cd_extra.bin"]),
|
||||
"sega-megadrive|fixcore": (["md_eu.bin", "md_us.bin", "md_jp.bin", "md_tmss.bin"],
|
||||
["md_tmss.bin"]),
|
||||
"nintendo-gamecube|dolphin": (["dolphin/GC/USA/IPL.bin", "dolphin/GC/EUR/IPL.bin",
|
||||
"dolphin/GC/JAP/IPL.bin"], []),
|
||||
"arcade|fbneo": (["fbneo/samples/big.zip"], ["fbneo/samples/big.zip"]),
|
||||
"nintendo-gba|mgba": (["gba_bios.bin"], ["gba_bios.bin"]),
|
||||
"nintendo-gba|gpsp": (["gba_bios.bin"], []),
|
||||
}
|
||||
|
||||
REGION_READER_MCD = {
|
||||
"applies_to": {"extensions": [".cue", ".bin", ".iso"], "media": "cd"},
|
||||
"decode": {"kind": "integer", "otherwise": ["north-america"],
|
||||
"values": [{"regions": ["europe"], "value": 100},
|
||||
{"regions": ["japan"], "value": 161}]},
|
||||
"location": {"encoding": "u8", "length": 1, "offset": 523, "sector": 0},
|
||||
"magic": {"bytes": "SEGADISCSYSTEM", "offset": 0, "sector": 0},
|
||||
}
|
||||
|
||||
SYSTEMS = {
|
||||
"sony-playstation": {
|
||||
"identify": {"sources": [{"source": "redump", "dat": "PS"}], "regions": [["europe"]],
|
||||
"games": [], "serials": {"SLES-00001": [0]}},
|
||||
"headers": {"readers": {"psx-serial-prefix": {
|
||||
"location": {"iso_file": "SYSTEM.CNF", "key": "BOOT", "take": "basename"},
|
||||
"decode": {"kind": "prefix-nocase", "values": [
|
||||
{"value": "sles", "regions": ["europe"]}], "otherwise": "unknown"}}},
|
||||
"cores": {"beetle_psx": {"order": ["psx-serial-prefix"],
|
||||
"otherwise": ["north-america"]}}},
|
||||
"region_rules": {"beetle_psx": {"keep_all_regions": [
|
||||
{"case": "psx-override-bios", "condition": "option", "keeps": "region-free-files"}]}},
|
||||
},
|
||||
"sega-megacd": {
|
||||
"identify": {"sources": [{"source": "redump", "dat": "MCD"}], "regions": [],
|
||||
"games": [], "serials": {}},
|
||||
"headers": {"readers": {"mcd-security-code": REGION_READER_MCD},
|
||||
"cores": {"picodrive": {"order": ["mcd-security-code"],
|
||||
"otherwise": ["north-america"]}}},
|
||||
"region_rules": {"picodrive": {"default_region": "north-america"}},
|
||||
"extras": [{"system": "sega-megacd", "profile": "picodrive",
|
||||
"trigger": {"every_game": True}, "files": ["cd_extra.bin"],
|
||||
"required": True}],
|
||||
},
|
||||
"sega-megadrive": {
|
||||
"identify": {"sources": [{"source": "no-intro", "dat": "MD"}],
|
||||
"regions": [["europe"], ["japan"], ["north-america"]],
|
||||
"games": sorted([[_crc(MD_EU), len(MD_EU), 0, "Game (Europe)"],
|
||||
[_crc(MD_JP), len(MD_JP), 1, "Game (Japan)"],
|
||||
[_crc(MD_ZIPPED), len(MD_ZIPPED), 2, "Zipped (USA)"]]),
|
||||
"serials": {}},
|
||||
"headers": {"readers": {"md-country": {
|
||||
"location": {"offset": 0x1F0, "length": 3, "encoding": "ascii"},
|
||||
"decode": {"kind": "letters", "otherwise": "unknown", "values": [
|
||||
{"value": "J", "regions": ["japan"]}, {"value": "E", "regions": ["europe"]},
|
||||
{"value": "U", "regions": ["north-america"]}]}}},
|
||||
"cores": {"fixcore": {"order": ["md-country"], "otherwise": "unknown"}}},
|
||||
"region_rules": {"fixcore": {}},
|
||||
},
|
||||
"nintendo-gamecube": {
|
||||
"identify": {"sources": [{"source": "redump", "dat": "GC"}],
|
||||
"regions": [["europe", "north-america"], ["japan"]],
|
||||
"games": [], "serials": {"GALE01": [0], "GJPN01": [1]}},
|
||||
"headers": {"readers": {
|
||||
"gc-game-id": {"location": {"container": "gamecube-disc", "disc_offset": 0,
|
||||
"encoding": "ascii", "length": 6},
|
||||
"decode": {"kind": "serial"}},
|
||||
"gc-region": {"location": {"container": "gamecube-disc", "disc_offset": GC_HEADER,
|
||||
"encoding": "u32be", "length": 4},
|
||||
"decode": {"kind": "integer", "otherwise": "unknown", "values": [
|
||||
{"value": 0, "regions": ["japan"]},
|
||||
{"value": 1, "regions": ["north-america"]},
|
||||
{"value": 2, "regions": ["europe"]}]}}},
|
||||
"cores": {"dolphin": {"order": ["gc-region"], "otherwise": "unknown"}}},
|
||||
"region_rules": {"dolphin": {"keep_all_regions": [
|
||||
{"case": "gamecube-unknown-region", "condition": "disc"}]}},
|
||||
},
|
||||
"nintendo-gba": {},
|
||||
}
|
||||
|
||||
ARCADE = {
|
||||
"bios": {"neogeo": ["mslug", "mslug2"], "pgm": ["orlegend"], "nobios": ["lost"]},
|
||||
"devices": {}, "chd": [], "missing_parents": {},
|
||||
"no_bios": ["pacman"], "bios_sets": ["neogeo", "pgm", "nobios"],
|
||||
}
|
||||
|
||||
SETUP = {
|
||||
"format_version": 1, "setup": "retroarch", "name": "Fixture", "platform": "retroarch",
|
||||
"sources": {}, "roms_root": {"path": "/mnt/SDCARD/Roms", "from_bios": "../Roms"},
|
||||
"case_sensitive": False,
|
||||
"media": [{"name": "Imgs"}],
|
||||
"folders": [
|
||||
{"folder": "PS", "systems": ["sony-playstation"], "basis": "id",
|
||||
"profile": "beetle_psx", "extensions": [".cue", ".bin", ".chd", ".m3u", ".iso"],
|
||||
"fallback": ["sony-playstation|beetle_psx"], "source_ref": "x"},
|
||||
{"folder": "SEGACD", "systems": ["sega-megacd"], "basis": "id",
|
||||
"profile": "picodrive", "extensions": [".cue", ".iso", ".bin", ".chd", ".m3u"],
|
||||
"fallback": ["sega-megacd|picodrive"], "source_ref": "x"},
|
||||
{"folder": "MD", "systems": ["sega-megadrive"], "basis": "id", "profile": "fixcore",
|
||||
"extensions": [".md", ".bin", ".zip", ".7z"],
|
||||
"fallback": ["sega-megadrive|fixcore"], "source_ref": "x"},
|
||||
{"folder": "GC", "systems": ["nintendo-gamecube"], "basis": "id",
|
||||
"profile": "dolphin", "extensions": [".iso", ".rvz"],
|
||||
"fallback": ["nintendo-gamecube|dolphin"], "source_ref": "x"},
|
||||
{"folder": "FBNEO", "systems": ["arcade"], "basis": "id", "profile": "fbneo",
|
||||
"arcade": "fbneo", "extensions": [".zip", ".7z"], "fallback": ["arcade|fbneo"],
|
||||
"source_ref": "x"},
|
||||
{"folder": "GBA", "systems": ["nintendo-gba"], "basis": "id", "profile": "mgba",
|
||||
"extensions": [".gba"], "fallback": ["nintendo-gba|mgba"],
|
||||
"board_defaults": {"rpi": {"core": "gpsp", "profile": "gpsp",
|
||||
"fallback": ["nintendo-gba|gpsp"]}},
|
||||
"source_ref": "x"},
|
||||
{"folder": "PORTS", "systems": [], "basis": "unresolved", "source_ref": "x"},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _dumps(doc: object) -> bytes:
|
||||
return (json.dumps(doc, sort_keys=True, separators=(",", ":")) + "\n").encode()
|
||||
|
||||
|
||||
def write_index(folder: Path, systems: dict | None = None, arcade: dict | None = None,
|
||||
setup: dict | None = None, extra_sets: dict | None = None) -> None:
|
||||
"""install/games/ as the generator lays it out, every digest right."""
|
||||
rows = sorted({(dest, FILES[dest][0], "bios") for files, _o in SETS.values()
|
||||
for dest in files})
|
||||
position = {row[0]: i for i, row in enumerate(rows)}
|
||||
sets = {**SETS, **(extra_sets or {})}
|
||||
fallback = {"format_version": 1, "platform": "retroarch", "files": [list(r) for r in rows],
|
||||
"sets": {key: {"files": [position[d] for d in files],
|
||||
"optional": [position[d] for d in optional],
|
||||
"bytes": 0, "required_bytes": 0}
|
||||
for key, (files, optional) in sets.items()}}
|
||||
docs = {"setups": {"retroarch": setup or SETUP}, "fallback": {"retroarch": fallback},
|
||||
"systems": {key: {"format_version": 1, "system": key, **value}
|
||||
for key, value in (systems or SYSTEMS).items()},
|
||||
"arcade": {"fbneo": {"profile": "fbneo", **(arcade or ARCADE)}}}
|
||||
index: dict = {"format_version": 1, "data_version": "0" * 16, "licences": [],
|
||||
"folders": {"ps": ["sony-playstation"], "psx": ["sony-playstation"],
|
||||
"md": ["sega-megadrive"], "megadrive": ["sega-megadrive"]}}
|
||||
for kind, items in docs.items():
|
||||
index[kind] = {}
|
||||
for key, doc in items.items():
|
||||
data = _dumps(doc)
|
||||
rel = f"{kind}/{key}.json"
|
||||
(folder / kind).mkdir(parents=True, exist_ok=True)
|
||||
(folder / rel).write_bytes(data)
|
||||
index[kind][key] = {"file": rel, "size": len(data),
|
||||
"gzip_size": len(gzip.compress(data)),
|
||||
"sha256": hashlib.sha256(data).hexdigest()}
|
||||
(folder / "index.json").write_bytes(_dumps(index))
|
||||
|
||||
|
||||
def manifest() -> dict:
|
||||
files = []
|
||||
for dest, (size, regions, cores, systems) in FILES.items():
|
||||
payload = dest.encode().ljust(size, b".")[:size]
|
||||
entry = {"dest": dest, "size": size, "sha1": hashlib.sha1(payload).hexdigest(),
|
||||
"sha256": hashlib.sha256(payload).hexdigest(),
|
||||
"repo_path": f"bios/{dest.replace('/', '_')}", "cores": cores}
|
||||
if systems:
|
||||
entry["systems"] = systems
|
||||
if regions:
|
||||
entry.update(regions=regions)
|
||||
files.append(entry)
|
||||
return {"manifest_version": 2, "platform": "retroarch", "files": files,
|
||||
"omitted_files": []}
|
||||
|
||||
|
||||
def mcd_iso(code: int, magic: bool = True) -> bytes:
|
||||
data = bytearray(2048 * 18)
|
||||
if magic:
|
||||
data[0:14] = b"SEGADISCSYSTEM"
|
||||
data[523] = code
|
||||
return bytes(data)
|
||||
|
||||
|
||||
def gc_iso(game_id: bytes, region: int) -> bytes:
|
||||
data = bytearray(0x500)
|
||||
data[0:6] = game_id
|
||||
data[GC_HEADER:GC_HEADER + 4] = region.to_bytes(4, "big")
|
||||
return bytes(data)
|
||||
|
||||
|
||||
def md_rom(content: bytes, country: bytes) -> bytes:
|
||||
data = bytearray(content.ljust(0x200, b"\0"))
|
||||
data[0x1F0:0x1F3] = country
|
||||
return bytes(data)
|
||||
|
||||
|
||||
def ps_iso(boot: str) -> bytes:
|
||||
"""A 2048-byte ISO 9660 image whose root holds SYSTEM.CNF."""
|
||||
sectors = [bytearray(2048) for _ in range(20)]
|
||||
volume = sectors[16]
|
||||
volume[0] = 1
|
||||
volume[1:6] = b"CD001"
|
||||
root = bytearray(34)
|
||||
root[0] = 34
|
||||
root[2:6] = (17).to_bytes(4, "little")
|
||||
root[10:14] = (2048).to_bytes(4, "little")
|
||||
volume[156:190] = root
|
||||
name = b"SYSTEM.CNF;1"
|
||||
record = bytearray(33 + len(name))
|
||||
record[0] = len(record)
|
||||
record[2:6] = (18).to_bytes(4, "little")
|
||||
text = f"BOOT = cdrom:\\{boot};1\r\n".encode()
|
||||
record[10:14] = len(text).to_bytes(4, "little")
|
||||
record[32] = len(name)
|
||||
record[33:] = name
|
||||
sectors[17][0:len(record)] = record
|
||||
sectors[18][0:len(text)] = text
|
||||
return b"".join(bytes(s) for s in sectors)
|
||||
|
||||
|
||||
class Card:
|
||||
"""A card in tmp/: BIOS/ and Roms/ at its root, the games index beside
|
||||
a saved file list."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.root = Path(tempfile.mkdtemp(dir=TMP_ROOT))
|
||||
self.bios = self.root / "card" / "BIOS"
|
||||
self.roms = self.root / "card" / "Roms"
|
||||
self.bios.mkdir(parents=True)
|
||||
self.roms.mkdir()
|
||||
self.lists = self.root / "lists"
|
||||
(self.lists / "games").mkdir(parents=True)
|
||||
self.manifest_path = self.lists / "retroarch.json"
|
||||
self.manifest_path.write_text(json.dumps(manifest()))
|
||||
write_index(self.lists / "games")
|
||||
self.cache = self.root / "cache"
|
||||
|
||||
def game(self, folder: str, name: str, data: bytes = b"game") -> Path:
|
||||
path = self.roms / folder / name
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_bytes(data)
|
||||
return path
|
||||
|
||||
def zipped(self, folder: str, name: str, member: str, data: bytes) -> Path:
|
||||
path = self.roms / folder / name
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with zipfile.ZipFile(path, "w") as archive:
|
||||
archive.writestr(member, data)
|
||||
return path
|
||||
|
||||
def close(self) -> None:
|
||||
shutil.rmtree(self.root, ignore_errors=True)
|
||||
|
||||
|
||||
class GamesTestCase(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.card = Card()
|
||||
self.addCleanup(self.card.close)
|
||||
patches = [mock.patch.dict(os.environ, {"RETROBIOS_CACHE": str(self.card.cache),
|
||||
install.OFFLINE_VARIABLE: "1"}),
|
||||
mock.patch.object(install.LOCAL, "manifest", self.card.manifest_path),
|
||||
mock.patch.object(install, "GAMES", install.GamesData())]
|
||||
for patcher in patches:
|
||||
patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
|
||||
def plan(self, folder: Path | None = None) -> install.GamesPlan:
|
||||
plan = install.plan_games("retroarch", self.card.bios, manifest(), folder,
|
||||
os_type="linux")
|
||||
self.assertIsNotNone(plan)
|
||||
return plan
|
||||
|
||||
def kept(self, plan: install.GamesPlan, prefix: str = "", required: bool = False) -> list:
|
||||
return sorted(dest for dest in plan.wanted(required) if dest.startswith(prefix))
|
||||
|
||||
|
||||
class Identification(GamesTestCase):
|
||||
def test_a_cartridge_is_known_by_crc32_and_size(self):
|
||||
self.card.game("MD", "anything.md", MD_EU)
|
||||
plan = self.plan()
|
||||
game = plan.games[0]
|
||||
self.assertTrue(game.recognised)
|
||||
self.assertEqual(game.title, "Game (Europe)")
|
||||
self.assertEqual(game.dat_regions, frozenset({"europe"}))
|
||||
|
||||
def test_a_zip_member_is_known_from_the_central_directory(self):
|
||||
self.card.zipped("MD", "z.zip", "rom.md", MD_ZIPPED)
|
||||
with mock.patch.object(install, "_crc32_file") as hashed:
|
||||
plan = self.plan()
|
||||
hashed.assert_not_called()
|
||||
self.assertTrue(plan.games[0].recognised)
|
||||
self.assertEqual(plan.games[0].dat_regions, frozenset({"north-america"}))
|
||||
|
||||
def test_a_disc_serial_from_system_cnf(self):
|
||||
self.card.game("PS", "game.iso", ps_iso("SLES_000.01"))
|
||||
game = self.plan().games[0]
|
||||
self.assertTrue(game.recognised)
|
||||
self.assertEqual(game.title, "SLES_000.01")
|
||||
|
||||
def test_a_chd_is_not_read(self):
|
||||
self.card.game("PS", "game.chd", b"MComprHD")
|
||||
game = self.plan().games[0]
|
||||
self.assertFalse(game.read)
|
||||
self.assertFalse(game.recognised)
|
||||
|
||||
def test_files_a_cue_names_are_part_of_its_game(self):
|
||||
self.card.game("SEGACD", "g.cue", b'FILE "g.bin" BINARY\n TRACK 01 MODE1/2048\n')
|
||||
self.card.game("SEGACD", "g.bin", mcd_iso(100))
|
||||
plan = self.plan()
|
||||
self.assertEqual([game.shown for game in plan.games], ["SEGACD/g.cue"])
|
||||
self.assertEqual(install.core_regions(plan.games[0], "sega-megacd",
|
||||
install.SystemData(SYSTEMS["sega-megacd"]),
|
||||
"picodrive"), frozenset({"europe"}))
|
||||
|
||||
def test_hidden_media_and_other_files_are_not_games(self):
|
||||
self.card.game("MD", ".hidden.md", MD_EU)
|
||||
self.card.game("MD", "Imgs/cover.md", MD_EU)
|
||||
self.card.game("MD", "notes.txt", b"x")
|
||||
self.card.game("MD", "game.md", MD_EU)
|
||||
plan = self.plan()
|
||||
self.assertEqual([game.shown for game in plan.games], ["MD/game.md"])
|
||||
self.assertEqual(plan.not_games, 1)
|
||||
|
||||
def test_a_folder_the_setup_does_not_list_holds_no_game(self):
|
||||
self.card.game("UNKNOWN", "game.md", MD_EU)
|
||||
self.card.game("PORTS", "game.sh", b"x")
|
||||
self.assertEqual(self.plan().games, [])
|
||||
|
||||
def test_a_raw_cd_image_with_its_mode(self):
|
||||
user = mcd_iso(161)
|
||||
raw = bytearray()
|
||||
for index in range(len(user) // 2048):
|
||||
sector = bytearray(2352)
|
||||
sector[0:12] = b"\x00" + b"\xff" * 10 + b"\x00"
|
||||
sector[15] = 1
|
||||
sector[16:16 + 2048] = user[index * 2048:(index + 1) * 2048]
|
||||
raw += sector
|
||||
self.card.game("SEGACD", "g.bin", bytes(raw))
|
||||
plan = self.plan()
|
||||
self.assertEqual(self.kept(plan, "bios_CD"), ["bios_CD_J.bin"])
|
||||
|
||||
|
||||
class RegionMatrix(GamesTestCase):
|
||||
"""What each kind of evidence lets the run remove (research section 2)."""
|
||||
|
||||
def test_a_header_read_keeps_the_region_it_proves(self):
|
||||
self.card.game("SEGACD", "sonic.iso", mcd_iso(100))
|
||||
plan = self.plan()
|
||||
self.assertEqual(self.kept(plan, "bios_CD"), ["bios_CD_E.bin"])
|
||||
self.assertEqual(plan.systems["sega-megacd"]["regions"], ["europe"])
|
||||
self.assertEqual(plan.fallback, [])
|
||||
|
||||
def test_a_value_the_code_reads_as_its_default_is_a_proof(self):
|
||||
self.card.game("SEGACD", "us.iso", mcd_iso(7))
|
||||
self.assertEqual(self.kept(self.plan(), "bios_CD"), ["bios_CD_U.bin"])
|
||||
|
||||
def test_two_games_keep_both_regions(self):
|
||||
self.card.game("SEGACD", "a.iso", mcd_iso(100))
|
||||
self.card.game("SEGACD", "b.iso", mcd_iso(161))
|
||||
self.assertEqual(self.kept(self.plan(), "bios_CD"), ["bios_CD_E.bin", "bios_CD_J.bin"])
|
||||
|
||||
def test_a_hash_match_keeps_the_catalog_region(self):
|
||||
self.card.game("MD", "eu.md", MD_EU)
|
||||
self.assertEqual(self.kept(self.plan(), "md_"), ["md_eu.bin", "md_tmss.bin"])
|
||||
|
||||
def test_a_filename_tag_alone_keeps_every_region(self):
|
||||
self.card.game("SEGACD", "Sonic CD (Europe).iso", b"no header here" * 200)
|
||||
plan = self.plan()
|
||||
self.assertEqual(self.kept(plan, "bios_CD"),
|
||||
["bios_CD_E.bin", "bios_CD_J.bin", "bios_CD_U.bin"])
|
||||
self.assertEqual([(g.shown, g.reason) for g in plan.fallback],
|
||||
[("SEGACD/Sonic CD (Europe).iso", "not_recognised")])
|
||||
|
||||
def test_folder_only_keeps_every_region_and_names_the_game(self):
|
||||
self.card.game("MD", "unknown.md", b"not in any catalog")
|
||||
plan = self.plan()
|
||||
self.assertEqual(self.kept(plan, "md_"),
|
||||
["md_eu.bin", "md_jp.bin", "md_tmss.bin", "md_us.bin"])
|
||||
self.assertEqual(plan.fallback[0].reason, "not_recognised")
|
||||
lines = install.games_lines(plan, None)
|
||||
self.assertIn("1 game gets every BIOS of its system: MD/unknown.md (not recognised)",
|
||||
"\n".join(lines))
|
||||
|
||||
def test_one_unproven_game_keeps_every_region_for_all(self):
|
||||
self.card.game("SEGACD", "a.iso", mcd_iso(100))
|
||||
self.card.game("SEGACD", "b.chd", b"MComprHD")
|
||||
plan = self.plan()
|
||||
self.assertEqual(len(self.kept(plan, "bios_CD")), 3)
|
||||
self.assertEqual([(g.shown, g.reason) for g in plan.fallback],
|
||||
[("SEGACD/b.chd", "not_read")])
|
||||
self.assertIsNone(plan.systems["sega-megacd"]["regions"])
|
||||
|
||||
def test_a_catalog_and_a_header_that_disagree_keep_every_region(self):
|
||||
self.card.game("MD", "jp.md", md_rom(MD_JP, b"E "))
|
||||
# The catalog matches the whole file: write the header into it first.
|
||||
path = self.card.roms / "MD" / "jp.md"
|
||||
data = path.read_bytes()
|
||||
rows = [[_crc(data), len(data), 1, "Game (Japan)"]]
|
||||
systems = json.loads(json.dumps(SYSTEMS))
|
||||
systems["sega-megadrive"]["identify"]["games"] = rows
|
||||
write_index(self.card.lists / "games", systems=systems)
|
||||
plan = self.plan()
|
||||
self.assertTrue(plan.games[0].recognised)
|
||||
self.assertEqual(plan.games[0].readings["md-country"], frozenset({"europe"}))
|
||||
self.assertEqual(len(self.kept(plan, "md_")), 4)
|
||||
self.assertEqual(plan.fallback[0].reason, "region_unknown")
|
||||
|
||||
def test_a_multi_region_serial_keeps_its_regions(self):
|
||||
self.card.game("GC", "game.iso", gc_iso(b"GALE01", 1))
|
||||
plan = self.plan()
|
||||
self.assertEqual(self.kept(plan, "dolphin"),
|
||||
["dolphin/GC/EUR/IPL.bin", "dolphin/GC/USA/IPL.bin"])
|
||||
|
||||
def test_an_unknown_gamecube_region_keeps_every_ipl(self):
|
||||
self.card.game("GC", "odd.iso", gc_iso(b"XXXX01", 9))
|
||||
self.assertEqual(len(self.kept(self.plan(), "dolphin")), 3)
|
||||
|
||||
def test_a_compressed_gamecube_disc_is_not_read(self):
|
||||
self.card.game("GC", "game.rvz", b"RVZ\x01")
|
||||
plan = self.plan()
|
||||
self.assertEqual(len(self.kept(plan, "dolphin")), 3)
|
||||
self.assertEqual(plan.fallback[0].reason, "not_read")
|
||||
|
||||
def test_a_core_with_a_keep_all_case_never_narrows(self):
|
||||
self.card.game("PS", "game.iso", ps_iso("SLES_000.01"))
|
||||
plan = self.plan()
|
||||
self.assertTrue(plan.games[0].recognised)
|
||||
self.assertEqual(self.kept(plan, "scph"),
|
||||
["scph5500.bin", "scph5501.bin", "scph5502.bin"])
|
||||
# Nothing the game could have proven changes that: it is not named.
|
||||
self.assertEqual(plan.fallback, [])
|
||||
|
||||
def test_a_game_region_no_file_matches_keeps_the_whole_set(self):
|
||||
dests = ["a.bin", "b.bin"]
|
||||
entries = {"a.bin": {"regions": ["japan"]}, "b.bin": {"regions": ["north-america"]}}
|
||||
self.assertEqual(install.keep_regions(dests, entries, frozenset({"europe"}), set()),
|
||||
dests)
|
||||
|
||||
def test_a_country_matches_its_region_and_region_free_files_stay(self):
|
||||
dests = ["eu.bin", "us.bin", "free.bin", "world.bin"]
|
||||
entries = {"eu.bin": {"regions": ["europe"]}, "us.bin": {"regions": ["north-america"]},
|
||||
"free.bin": {"regions": ["japan"]}, "world.bin": {"regions": ["world"]}}
|
||||
self.assertEqual(install.keep_regions(dests, entries, frozenset({"germany"}),
|
||||
{"free.bin"}),
|
||||
["eu.bin", "free.bin", "world.bin"])
|
||||
|
||||
def test_routes_send_a_region_to_another_regions_file(self):
|
||||
rule = {"routes": {"south-korea": "japan"}}
|
||||
self.assertEqual(install.needed_regions(rule, [frozenset({"south-korea"})]),
|
||||
frozenset({"japan"}))
|
||||
|
||||
|
||||
class KeepEveryRegionCases(unittest.TestCase):
|
||||
"""Every case of games/regions.yml keeps every region where it applies."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
with open(REPO_ROOT / "games" / "regions.yml", encoding="utf-8") as handle:
|
||||
cls.rules = yaml.safe_load(handle)
|
||||
|
||||
def facts(self, name: str) -> install.GameFacts:
|
||||
folder = install.GameFolder(Path("x"), {})
|
||||
return install.GameFacts(Path(name), name, folder)
|
||||
|
||||
def test_there_are_fourteen_cases_and_each_is_decided(self):
|
||||
cases = self.rules["cases"]
|
||||
self.assertEqual(len(cases), 14)
|
||||
disc_cases = {"first-disc-region", "multi-area-disc", "gamecube-unknown-region",
|
||||
"saturn-kr-ipl"}
|
||||
game = [self.facts("game.iso")]
|
||||
for name in cases:
|
||||
with self.subTest(case=name):
|
||||
conditions = {case.get("condition")
|
||||
for system in self.rules["systems"].values()
|
||||
for rule in system.values()
|
||||
for case in rule.get("keep_all_regions") or []
|
||||
if case.get("case") == name}
|
||||
self.assertTrue(conditions, f"{name} is named by no rule")
|
||||
for condition in conditions:
|
||||
rule = {"keep_all_regions": [{"case": name, "condition": condition}]}
|
||||
keeps = install.region_rule_keeps_all(rule, game)
|
||||
if condition in ("always", "option", "user-state"):
|
||||
self.assertTrue(keeps, (name, condition))
|
||||
else:
|
||||
self.assertIn(name, disc_cases)
|
||||
|
||||
def test_first_disc_region_keeps_every_region_for_a_playlist(self):
|
||||
rule = {"keep_all_regions": [{"case": "first-disc-region", "condition": "disc"}]}
|
||||
self.assertTrue(install.region_rule_keeps_all(rule, [self.facts("g.m3u")]))
|
||||
self.assertFalse(install.region_rule_keeps_all(rule, [self.facts("g.cue")]))
|
||||
|
||||
def test_an_asia_ntsc_or_korean_saturn_disc_keeps_every_region(self):
|
||||
rule = {"keep_all_regions": [{"case": "saturn-kr-ipl", "condition": "disc"}]}
|
||||
self.assertIsNone(install.needed_regions(rule, [frozenset({"asia-ntsc"})]))
|
||||
self.assertIsNone(install.needed_regions(rule, [frozenset({"south-korea"})]))
|
||||
self.assertEqual(install.needed_regions(rule, [frozenset({"japan"})]),
|
||||
frozenset({"japan"}))
|
||||
|
||||
def test_a_multi_area_disc_keeps_every_area_it_lists(self):
|
||||
rule = {"keep_all_regions": [{"case": "multi-area-disc", "condition": "disc"}]}
|
||||
self.assertFalse(install.region_rule_keeps_all(rule, [self.facts("g.iso")]))
|
||||
self.assertEqual(install.needed_regions(rule, [frozenset({"japan", "europe"})]),
|
||||
frozenset({"japan", "europe"}))
|
||||
|
||||
def test_an_unknown_gamecube_region_is_no_proof(self):
|
||||
system = install.SystemData({"headers": {"cores": {"dolphin": {
|
||||
"order": ["gc-region"], "otherwise": "unknown"}}}})
|
||||
facts = self.facts("g.iso")
|
||||
facts.system = "nintendo-gamecube"
|
||||
facts.readings["gc-region"] = "unknown"
|
||||
self.assertIsNone(install.core_regions(facts, "nintendo-gamecube", system, "dolphin"))
|
||||
|
||||
def test_no_rule_read_and_a_case_not_known_keep_every_region(self):
|
||||
game = [self.facts("g.iso")]
|
||||
self.assertTrue(install.region_rule_keeps_all(None, game))
|
||||
self.assertTrue(install.region_rule_keeps_all(
|
||||
{"keep_all_regions": [{"case": "new-case", "condition": "disc"}]}, game))
|
||||
|
||||
def test_every_rule_of_the_repository_is_read_without_error(self):
|
||||
game = [self.facts("g.iso")]
|
||||
for system, cores in self.rules["systems"].items():
|
||||
for core, rule in cores.items():
|
||||
with self.subTest(system=system, core=core):
|
||||
install.region_rule_keeps_all(rule, game)
|
||||
|
||||
|
||||
class Arcade(GamesTestCase):
|
||||
def test_a_set_gets_its_bios_parent_from_the_whole_list(self):
|
||||
self.card.game("FBNEO", "mslug.zip", b"PK")
|
||||
plan = self.plan()
|
||||
self.assertEqual(self.kept(plan), ["fbneo/neogeo.zip", "neogeo.zip"])
|
||||
self.assertEqual(self.kept(plan, required=True), ["fbneo/neogeo.zip", "neogeo.zip"])
|
||||
|
||||
def test_a_set_with_no_bios_needs_nothing(self):
|
||||
self.card.game("FBNEO", "pacman.zip", b"PK")
|
||||
plan = self.plan()
|
||||
self.assertEqual(self.kept(plan), [])
|
||||
self.assertTrue(plan.games[0].recognised)
|
||||
|
||||
def test_a_bios_set_on_the_card_is_not_a_game(self):
|
||||
self.card.game("FBNEO", "neogeo.zip", b"PK")
|
||||
plan = self.plan()
|
||||
self.assertEqual(plan.games, [])
|
||||
self.assertEqual(plan.support, 1)
|
||||
|
||||
def test_an_unknown_set_gets_every_bios_set_of_the_core(self):
|
||||
self.card.game("FBNEO", "zzzz.zip", b"PK")
|
||||
plan = self.plan()
|
||||
self.assertEqual(self.kept(plan),
|
||||
["fbneo/neogeo.zip", "fbneo/pgm.zip", "fbneo/samples/big.zip"])
|
||||
self.assertEqual([(g.shown, g.reason) for g in plan.fallback],
|
||||
[("FBNEO/zzzz.zip", "unknown_set")])
|
||||
|
||||
def test_a_parent_the_list_does_not_hold_falls_back(self):
|
||||
self.card.game("FBNEO", "lost.zip", b"PK")
|
||||
plan = self.plan()
|
||||
self.assertEqual(plan.fallback[0].reason, "no_parent")
|
||||
self.assertIn("fbneo/neogeo.zip", plan.keep)
|
||||
|
||||
|
||||
class RequiredOnlyAndExtras(GamesTestCase):
|
||||
def test_required_only_leaves_out_what_the_set_marks_optional(self):
|
||||
self.card.game("MD", "eu.md", MD_EU)
|
||||
plan = self.plan()
|
||||
self.assertEqual(self.kept(plan, "md_"), ["md_eu.bin", "md_tmss.bin"])
|
||||
self.assertEqual(self.kept(plan, "md_", required=True), ["md_eu.bin"])
|
||||
|
||||
def test_a_required_extra_the_game_triggers_stays(self):
|
||||
self.card.game("SEGACD", "sonic.iso", mcd_iso(100))
|
||||
plan = self.plan()
|
||||
self.assertIn("cd_extra.bin", plan.required)
|
||||
|
||||
def test_apply_keeps_the_entries_and_the_omissions_of_the_games(self):
|
||||
self.card.game("MD", "eu.md", MD_EU)
|
||||
plan = self.plan()
|
||||
omitted = [{"dest": "md_missing.bin", "systems": ["sega-megadrive"], "required": False},
|
||||
{"dest": "other.bin", "systems": ["nintendo-gba"], "required": True}]
|
||||
files, kept_omitted = plan.apply(manifest()["files"], omitted, False)
|
||||
self.assertEqual(sorted(e["dest"] for e in files), ["md_eu.bin", "md_tmss.bin"])
|
||||
self.assertEqual([o["dest"] for o in kept_omitted], ["md_missing.bin"])
|
||||
_files, kept_omitted = plan.apply(manifest()["files"], omitted, True)
|
||||
self.assertEqual(kept_omitted, [])
|
||||
|
||||
def test_a_board_default_core_adds_its_set(self):
|
||||
self.card.game("GBA", "game.gba", b"gba game")
|
||||
plan = self.plan()
|
||||
# mgba marks the BIOS optional, gpsp (a board's default) requires it.
|
||||
self.assertEqual(self.kept(plan, "gba", required=True), ["gba_bios.bin"])
|
||||
|
||||
|
||||
class Folders(GamesTestCase):
|
||||
def test_a_games_folder_named_on_a_computer_is_read_by_folder_names(self):
|
||||
games = self.card.root / "pc games"
|
||||
(games / "megadrive").mkdir(parents=True)
|
||||
(games / "megadrive" / "eu.md").write_bytes(MD_EU)
|
||||
setup = dict(SETUP, folders=[])
|
||||
write_index(self.card.lists / "games", setup=setup)
|
||||
plan = self.plan(games)
|
||||
self.assertFalse(plan.card)
|
||||
self.assertEqual(plan.where(), "in this folder")
|
||||
self.assertEqual(plan.unknown_core, ["sega-megadrive"])
|
||||
# The core is not known: every set of the system is read.
|
||||
self.assertIn("md_eu.bin", plan.keep)
|
||||
|
||||
def test_a_folder_of_one_system_named_alone(self):
|
||||
folder = self.card.roms / "MD"
|
||||
folder.mkdir()
|
||||
(folder / "eu.md").write_bytes(MD_EU)
|
||||
plan = self.plan(folder)
|
||||
self.assertEqual(len(plan.games), 1)
|
||||
|
||||
def test_no_rom_folder_beside_the_bios_folder(self):
|
||||
shutil.rmtree(self.card.roms)
|
||||
self.assertIsNone(install.plan_games("retroarch", self.card.bios, manifest(),
|
||||
os_type="linux"))
|
||||
|
||||
def test_the_probe_counts_folders_holding_a_game(self):
|
||||
self.card.game("MD", "eu.md", MD_EU)
|
||||
self.card.game("PS", "a.chd", b"x")
|
||||
(self.card.roms / "GBA").mkdir()
|
||||
self.assertEqual(install.games_probe("retroarch", self.card.bios, network=False,
|
||||
os_type="linux"), 2)
|
||||
|
||||
def test_the_probe_says_nothing_without_a_rom_folder(self):
|
||||
shutil.rmtree(self.card.roms)
|
||||
self.assertIsNone(install.games_probe("retroarch", self.card.bios, network=False,
|
||||
os_type="linux"))
|
||||
|
||||
|
||||
class Cache(GamesTestCase):
|
||||
def test_a_second_run_hashes_nothing_that_did_not_change(self):
|
||||
game = self.card.game("MD", "eu.md", MD_EU)
|
||||
counted = []
|
||||
real = install._crc32_file
|
||||
|
||||
def counting(path):
|
||||
counted.append(path)
|
||||
return real(path)
|
||||
|
||||
with mock.patch.object(install, "_crc32_file", side_effect=counting):
|
||||
self.plan()
|
||||
self.assertEqual(len(counted), 1)
|
||||
install.GAMES.reset()
|
||||
plan = self.plan()
|
||||
self.assertEqual(len(counted), 1)
|
||||
self.assertTrue(plan.games[0].recognised)
|
||||
game.write_bytes(MD_JP)
|
||||
plan = self.plan()
|
||||
self.assertEqual(len(counted), 2)
|
||||
self.assertEqual(plan.games[0].dat_regions, frozenset({"japan"}))
|
||||
|
||||
def test_the_index_is_cached_and_read_offline_without_the_list_beside_it(self):
|
||||
self.card.game("MD", "eu.md", MD_EU)
|
||||
index = (self.card.lists / "games" / "index.json").read_bytes()
|
||||
cached = self.card.cache / "games"
|
||||
cached.mkdir(parents=True)
|
||||
(cached / "index.json").write_bytes(index)
|
||||
for path in (self.card.lists / "games").rglob("*.json"):
|
||||
if path.name != "index.json":
|
||||
data = path.read_bytes()
|
||||
(cached / f"{hashlib.sha256(data).hexdigest()}.json").write_bytes(data)
|
||||
shutil.rmtree(self.card.lists / "games")
|
||||
install.GAMES.reset()
|
||||
self.assertTrue(self.plan().games[0].recognised)
|
||||
|
||||
|
||||
class Unavailable(GamesTestCase):
|
||||
def test_offline_with_no_index_is_said(self):
|
||||
shutil.rmtree(self.card.lists / "games")
|
||||
with self.assertRaises(install.GamesUnavailable) as caught:
|
||||
install.plan_games("retroarch", self.card.bios, manifest(), os_type="linux")
|
||||
self.assertEqual(caught.exception.reason, "offline")
|
||||
|
||||
def test_a_document_that_is_not_the_one_listed_is_refused(self):
|
||||
(self.card.lists / "games" / "setups" / "retroarch.json").write_text("{}")
|
||||
with self.assertRaises(install.GamesUnavailable) as caught:
|
||||
install.plan_games("retroarch", self.card.bios, manifest(), os_type="linux")
|
||||
self.assertEqual(caught.exception.reason, "offline")
|
||||
|
||||
def test_an_index_that_names_a_file_the_list_lacks_is_stale(self):
|
||||
self.card.game("MD", "x.md", b"unknown")
|
||||
listed = manifest()
|
||||
listed["files"] = [e for e in listed["files"] if e["dest"] != "md_us.bin"]
|
||||
with self.assertRaises(install.GamesUnavailable) as caught:
|
||||
install.plan_games("retroarch", self.card.bios, listed, os_type="linux")
|
||||
self.assertEqual(caught.exception.reason, "stale")
|
||||
|
||||
|
||||
def run_cli(card: Card, *options: str, offline: bool = True,
|
||||
base: str | None = None) -> subprocess.CompletedProcess:
|
||||
env = {**os.environ, "RETROBIOS_CACHE": str(card.cache)}
|
||||
env.pop(install.OFFLINE_VARIABLE, None)
|
||||
if base:
|
||||
env["RETROBIOS_BASE_URL"] = base
|
||||
command = [sys.executable, str(REPO_ROOT / "install.py"), "--platform", "retroarch",
|
||||
"--dest", str(card.bios), *options]
|
||||
if offline:
|
||||
command += ["--offline", "--manifest", str(card.manifest_path)]
|
||||
return subprocess.run(command, env=env, capture_output=True, text=True, timeout=120,
|
||||
check=False, stdin=subprocess.DEVNULL)
|
||||
|
||||
|
||||
class CommandLine(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.card = Card()
|
||||
self.addCleanup(self.card.close)
|
||||
self.card.game("SEGACD", "sonic.iso", mcd_iso(100))
|
||||
self.card.game("MD", "odd.md", b"not in the catalog")
|
||||
|
||||
def events(self, proc: subprocess.CompletedProcess) -> list[dict]:
|
||||
return [json.loads(line) for line in proc.stdout.splitlines() if line.startswith("{")]
|
||||
|
||||
def test_check_narrows_to_what_the_games_need(self):
|
||||
proc = run_cli(self.card, "--for-games", "--check", "--events")
|
||||
events = self.events(proc)
|
||||
games = next(e for e in events if e["event"] == "games")
|
||||
self.assertEqual(games["games"], 2)
|
||||
self.assertEqual(games["fallback"], [{"system": "sega-megadrive", "folder": "MD",
|
||||
"name": "MD/odd.md",
|
||||
"reason": "not_recognised"}])
|
||||
self.assertEqual(sorted(set(games)), sorted(["event", *install.EVENT_KEYS["games"]]))
|
||||
dests = sorted(e["dest"] for e in events if e["event"] == "file")
|
||||
self.assertEqual(dests, ["bios_CD_E.bin", "cd_extra.bin", "md_eu.bin", "md_jp.bin",
|
||||
"md_tmss.bin", "md_us.bin"])
|
||||
self.assertIn("1 game gets every BIOS of its system", proc.stderr)
|
||||
|
||||
def test_required_only_leaves_out_the_optional_files(self):
|
||||
proc = run_cli(self.card, "--for-games", "--required-only", "--check", "--events")
|
||||
dests = sorted(e["dest"] for e in self.events(proc) if e["event"] == "file")
|
||||
self.assertEqual(dests, ["bios_CD_E.bin", "cd_extra.bin", "md_eu.bin", "md_jp.bin",
|
||||
"md_us.bin"])
|
||||
|
||||
def test_required_only_alone_is_refused(self):
|
||||
proc = run_cli(self.card, "--required-only", "--check")
|
||||
self.assertEqual(proc.returncode, 2)
|
||||
self.assertIn("--required-only applies to --for-games", proc.stderr)
|
||||
|
||||
def test_a_listing_refuses_it(self):
|
||||
proc = run_cli(self.card, "--for-games", "--list-systems")
|
||||
self.assertEqual(proc.returncode, 2)
|
||||
|
||||
def test_a_games_folder_that_does_not_exist_stops_the_run(self):
|
||||
proc = run_cli(self.card, "--for-games", str(self.card.root / "nowhere"), "--check")
|
||||
self.assertEqual(proc.returncode, 1)
|
||||
self.assertIn("does not exist", proc.stderr)
|
||||
|
||||
def test_offline_without_the_index_keeps_every_file_and_says_so(self):
|
||||
shutil.rmtree(self.card.lists / "games")
|
||||
proc = run_cli(self.card, "--for-games", "--check", "--events")
|
||||
events = self.events(proc)
|
||||
unavailable = next(e for e in events if e["event"] == "games_unavailable")
|
||||
self.assertEqual(unavailable["reason"], "offline")
|
||||
self.assertEqual(len([e for e in events if e["event"] == "file"]), len(FILES))
|
||||
self.assertIn("Every file of the list is kept", proc.stderr)
|
||||
|
||||
def test_a_system_choice_narrows_what_the_games_keep(self):
|
||||
proc = run_cli(self.card, "--for-games", "--system", "sega-megacd", "--check",
|
||||
"--events")
|
||||
dests = sorted(e["dest"] for e in self.events(proc) if e["event"] == "file")
|
||||
# cd_extra.bin names no system: a system choice leaves it, as without
|
||||
# --for-games.
|
||||
self.assertEqual(dests, ["bios_CD_E.bin"])
|
||||
|
||||
def test_the_install_writes_only_what_the_games_need(self):
|
||||
self.card.game("MD", "eu.md", MD_EU)
|
||||
root = self.card.root / "repo"
|
||||
(root / "install").mkdir(parents=True)
|
||||
shutil.copytree(self.card.lists / "games", root / "install" / "games")
|
||||
listed = manifest()
|
||||
(root / "bios").mkdir()
|
||||
for entry in listed["files"]:
|
||||
payload = entry["dest"].encode().ljust(entry["size"], b".")[:entry["size"]]
|
||||
(root / entry["repo_path"]).write_bytes(payload)
|
||||
(root / "install" / "retroarch.json").write_text(json.dumps(listed))
|
||||
handler = functools.partial(_Quiet, directory=str(root))
|
||||
server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), handler)
|
||||
threading.Thread(target=server.serve_forever, daemon=True).start()
|
||||
self.addCleanup(server.server_close)
|
||||
self.addCleanup(server.shutdown)
|
||||
base = f"http://127.0.0.1:{server.server_address[1]}"
|
||||
proc = run_cli(self.card, "--for-games", "--required-only", "--yes", offline=False,
|
||||
base=base)
|
||||
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
|
||||
written = sorted(p.relative_to(self.card.bios).as_posix()
|
||||
for p in self.card.bios.rglob("*") if p.is_file())
|
||||
self.assertEqual(written, ["bios_CD_E.bin", "cd_extra.bin", "md_eu.bin", "md_jp.bin",
|
||||
"md_us.bin"])
|
||||
self.assertIn("1 game gets every BIOS of its system", proc.stdout)
|
||||
# Cached for the next run, offline.
|
||||
self.assertTrue((self.card.cache / "games" / "index.json").is_file())
|
||||
self.assertTrue((self.card.cache / install.GAMES_SCAN_FILE).is_file())
|
||||
|
||||
|
||||
class _Quiet(http.server.SimpleHTTPRequestHandler):
|
||||
def log_message(self, format, *args): # noqa: A002 - stdlib signature
|
||||
return
|
||||
|
||||
|
||||
class LineMenu(GamesTestCase):
|
||||
def menu(self, answers: list[str], given: object = None) -> tuple[object, str]:
|
||||
self.card.game("SEGACD", "sonic.iso", mcd_iso(100))
|
||||
plan = self.plan()
|
||||
listed = manifest()
|
||||
replies = iter(answers)
|
||||
out = io.StringIO()
|
||||
with mock.patch.object(install, "_ask", side_effect=lambda _p: next(replies)), \
|
||||
contextlib.redirect_stdout(out):
|
||||
choice = install._prompt_custom_selection(
|
||||
"retroarch", listed["files"], [], manifest=listed, played=plan, given=given,
|
||||
command=lambda choice: install._INVOCATION.command("retroarch", "<dest>"))
|
||||
return choice, out.getvalue()
|
||||
|
||||
def test_the_games_are_the_first_choice(self):
|
||||
choice, text = self.menu([""])
|
||||
self.assertIsInstance(choice, install.GamesChoice)
|
||||
self.assertFalse(choice.required)
|
||||
self.assertIn("for the 1 game on this card", text)
|
||||
self.assertIn("e) everything, whatever the games", text)
|
||||
self.assertIn(f"r) {install.GAMES_REQUIRED_WORDS}", text)
|
||||
|
||||
def test_e_installs_everything_and_h_goes_back(self):
|
||||
choice, text = self.menu(["e", ""])
|
||||
self.assertIsNone(choice)
|
||||
self.assertIn("h) only for the games on this card", text)
|
||||
choice, _text = self.menu(["e", "h", ""])
|
||||
self.assertIsInstance(choice, install.GamesChoice)
|
||||
|
||||
def test_r_keeps_only_what_starts(self):
|
||||
choice, text = self.menu(["r", ""])
|
||||
self.assertTrue(choice.required)
|
||||
self.assertIn("r) every file the games read", text)
|
||||
self.assertEqual(choice.flags(), [("--for-games", None), ("--required-only", None)])
|
||||
|
||||
def test_the_choice_is_given_back_as_a_command(self):
|
||||
self.card.game("SEGACD", "sonic.iso", mcd_iso(100))
|
||||
plan = self.plan()
|
||||
self.assertEqual(install.chosen_flags(install.GamesChoice(plan)),
|
||||
[("--for-games", None)])
|
||||
named = install.plan_games("retroarch", self.card.bios, manifest(), self.card.roms,
|
||||
os_type="linux")
|
||||
self.assertEqual(install.GamesChoice(named).flags(),
|
||||
[("--for-games", str(self.card.roms))])
|
||||
|
||||
|
||||
class Published(unittest.TestCase):
|
||||
"""The repository's own index, as a run reads it."""
|
||||
|
||||
def test_a_card_reads_with_the_published_index(self):
|
||||
if not (REPO_ROOT / "install" / "games" / "index.json").exists():
|
||||
self.skipTest("install/games/ not generated")
|
||||
root = Path(tempfile.mkdtemp(dir=TMP_ROOT))
|
||||
self.addCleanup(shutil.rmtree, root, True)
|
||||
bios = root / "userdata" / "bios"
|
||||
bios.mkdir(parents=True)
|
||||
(root / "userdata" / "roms" / "fbneo").mkdir(parents=True)
|
||||
(root / "userdata" / "roms" / "fbneo" / "mslug.zip").write_bytes(b"PK")
|
||||
(root / "userdata" / "roms" / "fbneo" / "pacman.zip").write_bytes(b"PK")
|
||||
with mock.patch.object(install.LOCAL, "manifest", REPO_ROOT / "install" / "batocera.json"), \
|
||||
mock.patch.object(install, "GAMES", install.GamesData()), \
|
||||
mock.patch.dict(os.environ, {"RETROBIOS_CACHE": str(root / "cache"),
|
||||
install.OFFLINE_VARIABLE: "1"}):
|
||||
listed = json.loads((REPO_ROOT / "install" / "batocera.json").read_text())
|
||||
plan = install.plan_games("batocera", bios, listed, os_type="linux")
|
||||
self.assertEqual(len(plan.games), 2)
|
||||
self.assertEqual(plan.fallback, [])
|
||||
self.assertIn("neogeo.zip", plan.required)
|
||||
self.assertTrue(all(dest.lower().endswith("neogeo.zip") for dest in plan.required))
|
||||
|
||||
def test_every_published_setup_resolves_a_root_layout(self):
|
||||
index = json.loads((REPO_ROOT / "install" / "games" / "index.json").read_text())
|
||||
for key in index["setups"]:
|
||||
doc = json.loads((REPO_ROOT / "install" / "games" / "setups"
|
||||
/ f"{key}.json").read_text())
|
||||
with self.subTest(setup=key):
|
||||
self.assertIn("from_bios", doc["roms_root"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -371,6 +371,36 @@ def fresh_state() -> install.TuiState:
|
||||
return state
|
||||
|
||||
|
||||
GAMES = {"event": "games", "platform": "retroarch", "folder": "/media/user/SDCARD/Roms",
|
||||
"games": 412, "recognised": 398,
|
||||
"found": [{"system": "sony-playstation", "folder": "PS", "games": 400,
|
||||
"recognised": 390, "regions": []},
|
||||
{"system": "nintendo-gba", "folder": "GBA", "games": 12,
|
||||
"recognised": 8, "regions": ["europe"]}],
|
||||
"fallback": [{"system": "nintendo-gba", "folder": "GBA", "name": "GBA/demo.gba",
|
||||
"reason": "not_recognised"},
|
||||
{"system": "nintendo-gba", "folder": "GBA", "name": "GBA/hack.gba",
|
||||
"reason": "not_recognised"}],
|
||||
"not_games": 3, "required_only": False}
|
||||
|
||||
|
||||
def games_state() -> install.TuiState:
|
||||
"""The plan of a card whose games the check read (--for-games)."""
|
||||
state = fresh_state()
|
||||
state.stack = ["setup"]
|
||||
with mock.patch.object(install.Path, "exists", return_value=True):
|
||||
install.tui_update(state, Key("enter"))
|
||||
install.tui_receive(state, ("games_here", "retroarch", SYSTEM_DIR, 2))
|
||||
effects = install.tui_receive(state, ("manifest", "retroarch", MANIFEST))
|
||||
assert effects == [("run", "check")], effects
|
||||
for event in [GAMES, *FILES, *SYSTEMS, CHECK]:
|
||||
install.tui_receive(state, ("event", state.run_id, event))
|
||||
with mock.patch.object(install, "_tui_folder_facts", return_value={
|
||||
"free": 50 * 1024 ** 3, "writable": True, "cores": ["pcsx_rearmed"]}):
|
||||
install.tui_receive(state, ("ended", state.run_id, 1, []))
|
||||
return state
|
||||
|
||||
|
||||
def checked_state(facts: dict | None = None) -> install.TuiState:
|
||||
"""The plan screen of a RetroArch folder with files missing."""
|
||||
state = fresh_state()
|
||||
@@ -989,6 +1019,10 @@ def golden_states() -> dict:
|
||||
helped = checked_state()
|
||||
helped.help = True
|
||||
states["help"] = helped
|
||||
states["plan-games"] = games_state()
|
||||
listed = games_state()
|
||||
listed.stack.append("games")
|
||||
states["games"] = listed
|
||||
return states
|
||||
|
||||
|
||||
@@ -14490,5 +14524,186 @@ class AuditTwentyFourAndSeventeen(unittest.TestCase):
|
||||
self.assertFalse(os.path.ismount("/media/x"))
|
||||
|
||||
|
||||
class GamesOnTheCard(unittest.TestCase):
|
||||
"""For the games on this card: the first choice where the card holds
|
||||
games, the whole list one press away (--for-games)."""
|
||||
|
||||
def test_a_card_with_games_opens_on_them(self):
|
||||
state = games_state()
|
||||
self.assertTrue(state.for_games)
|
||||
self.assertEqual(state.games_choice, "probe")
|
||||
argv = install.tui_argv(state, "check", "py", "i.py")
|
||||
self.assertEqual(argv[4:], ["--for-games", "--check", "--events"])
|
||||
rows = install.tui_rows(state)
|
||||
self.assertEqual([row.action for row in rows[:4]],
|
||||
["install", "games", "startonly", "gameslist"])
|
||||
self.assertEqual(rows[1].label, "For the games on this card: yes")
|
||||
self.assertEqual(rows[1].right, "412 games")
|
||||
text = "\n".join(install.tui_view(state, 80, 24))
|
||||
self.assertIn("For the 412 games on this card.", text)
|
||||
self.assertIn("2 games get every BIOS of their system: Show the games.", text)
|
||||
|
||||
def test_a_card_without_games_keeps_the_whole_list(self):
|
||||
state = fresh_state()
|
||||
with mock.patch.object(install.Path, "exists", return_value=True):
|
||||
install.tui_update(state, Key("enter"))
|
||||
install.tui_receive(state, ("games_here", "retroarch", SYSTEM_DIR, 0))
|
||||
install.tui_receive(state, ("manifest", "retroarch", MANIFEST))
|
||||
self.assertFalse(state.for_games)
|
||||
self.assertNotIn("--for-games", install.tui_argv(state, "check", "py", "i.py"))
|
||||
self.assertNotIn("games", [row.action for row in install.tui_rows(checked_state())])
|
||||
|
||||
def test_a_look_at_another_folder_is_ignored(self):
|
||||
state = fresh_state()
|
||||
with mock.patch.object(install.Path, "exists", return_value=True):
|
||||
install.tui_update(state, Key("enter"))
|
||||
install.tui_receive(state, ("games_here", "retroarch", HOME / "other", 5))
|
||||
self.assertIsNone(state.for_games)
|
||||
|
||||
def test_everything_is_one_press_away(self):
|
||||
state = games_state()
|
||||
state.focus["plan"] = 1
|
||||
self.assertEqual(press(state, "enter"), [("run", "check")])
|
||||
self.assertFalse(state.for_games)
|
||||
self.assertEqual(state.games_choice, "user")
|
||||
self.assertNotIn("--for-games", install.tui_argv(state, "check", "py", "i.py"))
|
||||
self.assertEqual(state.refocus, "games")
|
||||
|
||||
def test_only_what_starts_adds_its_flag(self):
|
||||
state = games_state()
|
||||
state.focus["plan"] = 2
|
||||
self.assertEqual(press(state, "enter"), [("run", "check")])
|
||||
self.assertIn("--required-only", install.tui_argv(state, "check", "py", "i.py"))
|
||||
self.assertEqual(install.tui_command(state).split()[-2:],
|
||||
["--for-games", "--required-only"])
|
||||
|
||||
def test_show_the_games_names_those_that_fell_back(self):
|
||||
state = games_state()
|
||||
state.focus["plan"] = 3
|
||||
press(state, "enter")
|
||||
self.assertEqual(state.screen, "games")
|
||||
items = install._pager_items(state)
|
||||
self.assertTrue(any(line.endswith("(GBA): 12 games, 8 recognised, Europe only")
|
||||
for line in items), items)
|
||||
self.assertIn("GBA/demo.gba: not recognised", items)
|
||||
press(state, "enter")
|
||||
self.assertEqual(state.screen, "plan")
|
||||
|
||||
def test_the_result_names_them_too(self):
|
||||
state = games_state()
|
||||
state.stack.append("result")
|
||||
state.code = 0
|
||||
state.download = {"event": "download", "files": 4, "bytes": 1064964}
|
||||
state.summary = {"event": "summary", "mode": "install", "installed": 4,
|
||||
"up_to_date": 1, "pending": 0, "failed": [], "skipped": [],
|
||||
"left_out": [], "kept": [], "interrupted": False}
|
||||
lines = install.tui_outcome(state)[1]
|
||||
self.assertIn("Not recognised, every BIOS of their system kept: GBA/demo.gba, "
|
||||
"GBA/hack.gba.", lines)
|
||||
|
||||
def test_the_command_line_decides_and_names_a_folder(self):
|
||||
args = _args()
|
||||
args.for_games, args.required_only = Path("games"), True
|
||||
with mock.patch.object(install, "_mounted_cards", return_value=[]):
|
||||
state, _effects = install.tui_start(args, "linux", detect=False)
|
||||
self.assertEqual(state.games_choice, "cli")
|
||||
flags = install.tui_flags(state)
|
||||
self.assertIn(("--for-games", str(Path("games").absolute())), flags)
|
||||
self.assertIn(("--required-only", None), flags)
|
||||
state.platform, state.dest = "retroarch", SYSTEM_DIR
|
||||
install.tui_receive(state, ("games_here", "retroarch", SYSTEM_DIR, 0))
|
||||
self.assertTrue(state.for_games)
|
||||
|
||||
def test_another_folder_is_looked_at_from_what_was_read(self):
|
||||
state = games_state()
|
||||
state.dest = HOME / "card" / "BIOS"
|
||||
with mock.patch.object(install, "games_probe", return_value=0) as probe:
|
||||
self.assertEqual(install._start_check(state), [("run", "check")])
|
||||
probe.assert_called_once_with("retroarch", HOME / "card" / "BIOS", network=False,
|
||||
os_type="linux")
|
||||
self.assertFalse(state.for_games)
|
||||
self.assertEqual(state.games_probed, ("retroarch", HOME / "card" / "BIOS"))
|
||||
|
||||
def test_the_fetch_looks_at_the_card_before_the_list_arrives(self):
|
||||
inbox: queue.Queue = queue.Queue()
|
||||
|
||||
def fetched(plat, held, log):
|
||||
held.put(("manifest", plat, MANIFEST))
|
||||
|
||||
with mock.patch.object(install, "_tui_fetch", side_effect=fetched), \
|
||||
mock.patch.object(install, "games_probe", return_value=3) as probe:
|
||||
install._tui_fetch_games("retroarch", inbox, io.StringIO(), SYSTEM_DIR, True)
|
||||
probe.assert_called_once_with("retroarch", SYSTEM_DIR, attempts=1)
|
||||
self.assertEqual(inbox.get()[0], "games_here")
|
||||
self.assertEqual(inbox.get()[0], "manifest")
|
||||
|
||||
def test_reading_the_games_is_counted_while_the_check_runs(self):
|
||||
state = fresh_state()
|
||||
state.platform, state.dest, state.stack = "retroarch", SYSTEM_DIR, ["checking"]
|
||||
state.manifest, state.run, state.running = MANIFEST, "check", True
|
||||
install.tui_receive(state, ("event", state.run_id, {
|
||||
"event": "games_reading", "files_done": 40, "files_total": 412}))
|
||||
self.assertIn("Reading the games: 40 of 412",
|
||||
"\n".join(install.tui_view(state, 80, 24)))
|
||||
|
||||
def test_what_the_games_need_not_known_is_said(self):
|
||||
state = games_state()
|
||||
state.games = None
|
||||
state.games_unavailable = {"event": "games_unavailable", "platform": "retroarch",
|
||||
"reason": "offline", "message": "the games index is "
|
||||
"not on this machine"}
|
||||
self.assertIn("What the games need is not known: the games index is not on this "
|
||||
"machine. Every file is kept.", install.games_plan_words(state))
|
||||
|
||||
def test_every_size_draws_the_rows(self):
|
||||
state = games_state()
|
||||
for cols, rows in SIZES:
|
||||
with self.subTest(size=f"{cols}x{rows}"):
|
||||
state.cols, state.rows = cols, rows
|
||||
lines = install.tui_view(state, cols, rows)
|
||||
self.assertTrue(any("For the games" in line for line in lines), lines)
|
||||
|
||||
|
||||
@unittest.skipUnless(pyte and pty, "the end-to-end screens need pyte and a pty")
|
||||
class GamesEndToEnd(unittest.TestCase):
|
||||
"""The real --tui on a card with games, against a loopback repository."""
|
||||
|
||||
def test_the_plan_opens_on_the_games_and_installs_only_them(self):
|
||||
import test_install_games as games # noqa: PLC0415
|
||||
|
||||
card = games.Card()
|
||||
self.addCleanup(card.close)
|
||||
card.game("SEGACD", "sonic.iso", games.mcd_iso(100))
|
||||
root = card.root / "repo"
|
||||
(root / "install").mkdir(parents=True)
|
||||
shutil.copytree(card.lists / "games", root / "install" / "games")
|
||||
listed = games.manifest()
|
||||
(root / "bios").mkdir()
|
||||
for entry in listed["files"]:
|
||||
payload = entry["dest"].encode().ljust(entry["size"], b".")[:entry["size"]]
|
||||
(root / entry["repo_path"]).write_bytes(payload)
|
||||
(root / "install" / "retroarch.json").write_text(json.dumps(listed))
|
||||
handler = functools.partial(_QuietHandler, directory=str(root))
|
||||
server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), handler)
|
||||
threading.Thread(target=server.serve_forever, daemon=True).start()
|
||||
self.addCleanup(server.server_close)
|
||||
self.addCleanup(server.shutdown)
|
||||
env = {**os.environ, "TERM": "xterm", "HOME": str(card.root), "RETROBIOS_OS": "linux",
|
||||
"RETROBIOS_BASE_URL": f"http://127.0.0.1:{server.server_address[1]}",
|
||||
"RETROBIOS_CACHE": str(card.cache)}
|
||||
for name in ("DISPLAY", "WAYLAND_DISPLAY", "SSH_CONNECTION", "SSH_TTY", "NO_COLOR",
|
||||
install.TUI_PAD_ENV, install.OFFLINE_VARIABLE, *install._STEAM_MARKERS):
|
||||
env.pop(name, None)
|
||||
term = Pty(["--platform", "retroarch", "--dest", str(card.bios)], env)
|
||||
self.addCleanup(lambda: term.status is None and os.kill(term.pid, 9))
|
||||
screen = term.wait("For the games on this card: yes", timeout=40)
|
||||
self.assertIn("For the 1 game on this card.", screen)
|
||||
term.send("\r")
|
||||
term.wait("A/Enter Exit", timeout=40)
|
||||
term.send("\r")
|
||||
self.assertEqual(term.close(), 0)
|
||||
written = sorted(p.name for p in card.bios.rglob("*") if p.is_file())
|
||||
self.assertEqual(written, ["bios_CD_E.bin", "cd_extra.bin"])
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -839,6 +839,31 @@ python install.py --platform recalbox --region us,eu,jp
|
||||
python install.py --platform emuelec --no-game-data
|
||||
```
|
||||
|
||||
### For the games on this card { #for-the-games-on-this-card }
|
||||
|
||||
When the card holds games in its rom folders, the plan opens on **For the
|
||||
games on this card: yes**, and Install takes only what those games need. One
|
||||
press on the row installs everything instead. Line mode asks the same: Enter
|
||||
installs for the games, `e` everything, `r` only what starts. On the command
|
||||
line it is `--for-games`, and `--for-games FOLDER` reads a games folder on a
|
||||
computer, one subfolder per system.
|
||||
|
||||
The rule is that every game must start. Per system, the run keeps what the
|
||||
system's default core reads. One region is kept only where the game's own
|
||||
content proves it: a cartridge's crc32 found in No-Intro, a disc serial found
|
||||
in Redump, or a header the core reads. A file name such as `(Europe)` proves
|
||||
nothing. Where a core forces a region by an option, falls back to another
|
||||
region, or uses its own disc list, every region stays. An arcade set gets the
|
||||
BIOS sets of its parent chain (`neogeo.zip` for `mslug.zip`).
|
||||
|
||||
A game that is not recognised gets every BIOS of its system, and the run names
|
||||
it. So does an arcade set the core does not know. **Only what starts**
|
||||
(`--required-only`) leaves out the files the cores mark optional.
|
||||
|
||||
The games are read without hashing whole discs, and a cache keeps what was
|
||||
read, so a second run over the same card reads only new files. Offline, with
|
||||
no games index on this machine, the run keeps every file and says so.
|
||||
|
||||
### Firmware without game data
|
||||
|
||||
Some platform lists name the files a game reads beside the firmware its
|
||||
@@ -932,6 +957,8 @@ what the options name, everything by default.
|
||||
| <span id="option-core"></span>`--core NAMES` | Keep the platform's own list and only the extra files these cores load. `--list-cores` prints the names |
|
||||
| <span id="option-emulator"></span>`--emulator NAMES` | Install what these emulators read on the detected platform, and nothing else: their own files and the platform's files of the systems they run. Each emulator page on the site gives this command. `--list-emulators` prints the names, buildbot names such as `mednafen_psx` included |
|
||||
| <span id="option-region"></span>`--region LIST` | Preferred regions, best first (`us,eu,jp`): keep one BIOS per region where a system has several. Accepts the names and aliases of `generate_pack.py --region` |
|
||||
| <span id="option-for-games"></span>`--for-games FOLDER` | Install only what the games on the card need. See [For the games on this card](#for-the-games-on-this-card) |
|
||||
| <span id="option-required-only"></span>`--required-only` | With `--for-games`, only the files the games cannot start without |
|
||||
| <span id="option-no-game-data"></span>`--no-game-data` | Leave out game data (a game's own files, not firmware) and install the firmware only. What it leaves out is named. See [Firmware without game data](#firmware-without-game-data) |
|
||||
| <span id="option-declared-only"></span>`--declared-only` | Keep only the files the platform's own list names (`cores: null` in the manifest), not the extra files cores load. With `--check`, the answer is about the list the platform itself checks. Refused with `--core`, the listings it does not change, `--standalone-copies` and `--tui` |
|
||||
| <span id="option-list-systems"></span>`--list-systems` | Print a platform's systems with the files and size each holds, the files other systems share too counted apart |
|
||||
@@ -1739,6 +1766,9 @@ other schemas under `api/v1/schemas/`.
|
||||
| `file` | `platform`, `dest`, `systems`, `size`, `state` | Each file of the list after the check. `state` is `present` (hash verified), `wrong_hash` or `missing` |
|
||||
| `system` | `platform`, `system`, `name`, `files`, `present`, `missing`, `wrong_hash`, `bytes_needed` | Each system, sorted by id, and with `--system` only the systems named. Files with no system count under `other`. `name` is the platform's own name for it |
|
||||
| `checking` | `files_done`, `files_total` | While the files already in the folder are read and hashed, at most every half second and once at the end. `files_total` counts the files found in place |
|
||||
| `games_reading` | `files_done`, `files_total` | With `--for-games`, while the games of the card are read (a cartridge hashed, a disc's header read), at most every half second and once at the end |
|
||||
| `games` | `platform`, `folder`, `games`, `recognised`, `found`, `fallback`, `not_games`, `required_only` | With `--for-games`, once the games are read, before `choices`. `found` lists per system its `folder`, `games`, `recognised` and the `regions` kept (empty for every region). `fallback` names each game that got every BIOS of its system, with a `reason` (`not_recognised`, `not_read`, `region_unknown`, `unknown_set`, `no_parent`) |
|
||||
| `games_unavailable` | `platform`, `reason`, `message` | With `--for-games`, what the games need could not be read (`offline`, `network`, `invalid`, `stale`, `no_sets`, `no_folder`): the run keeps every file of the list, except for a `FOLDER` that does not exist, where it stops |
|
||||
| `check` | `platform`, `name`, `bios_dir`, `files`, `present`, `missing`, `wrong_hash`, `bytes_needed`, `data_missing`, `not_installable` | The platform's totals. `data_missing` counts data-tree files to fetch, `not_installable` the files the collection cannot supply |
|
||||
| `declared` | `platform`, `files`, `present`, `missing`, `wrong_hash`, `bytes_needed`, `dests` | Right after `check`, the same counts over the files the platform's own list names (`cores: null`), and their `dests` in the order of the `file` events. A setup that checks its own list reads its answer here, with or without `--declared-only` |
|
||||
| `data` | `platform`, `files`, `bytes` | Right after `declared`: the data-tree files to fetch (the `data_missing` of `check`) and the bytes they write once in place |
|
||||
|
||||
Reference in new issue
Block a user