Pokemon Silver as a full launcher version, plus launcher mods-list and title-tempo fixes

Silver: derived import manifest (tools/make_silver_manifest.py re-resolves
the Gold manifest's symbols from pokesilver.sym), silver GameVersion row,
generation-keyed extractor routing, required-files override, edition save
stamping (a Silver playthrough no longer writes into the Gold save),
checkver-driven edition data, SILVER/KAMON/OSCAR/MAX presets, GOLD rival
default, edition credits banner, Lugia title screen (OAM layouts, bob,
trail, palettes as title.lua data keys with Gold defaults so old caches
need no re-import), packaging for every build target, docs, and tests.

Launcher: the installed-mods list is one continuous scroll (rows culled to
the viewport) instead of a pager with an inner scroll viewport; the pad
cursor's edge-scroll no longer runs it to the bottom. The game dropdown
shows just the initial and caret. Find-tab behavior unchanged.

Title tempo: a sprite-anim frame shows duration+1 ticks
(engine/sprite_anims/core.asm GetSpriteAnimFrame), which locks both
editions' 64-tick wing beat to the 64-tick sine bob; the title screens no
longer run fast and out of phase.
This commit is contained in:
bryanthaboi
2026-08-20 08:27:25 -04:00
parent 4c8c1cf36b
commit ec9dc29646
68 changed files with 18617 additions and 466 deletions
+11 -11
View File
@@ -140,17 +140,17 @@ ship text.
### 4. `games` (and the legacy `gen2compat`) ### 4. `games` (and the legacy `gen2compat`)
Pokemon Gold is Gen 2, and it runs its own battle engine, overworld, script Pokemon Gold and Silver are Gen 2, and they run their own battle engine,
VM and save format. The mod API is shared across both generations (same hook overworld, script VM and save format. The mod API is shared across both
names, same event names, same registry names) but Gold cannot serve all of it generations (same hook names, same event names, same registry names) but Gen 2
yet, so Gen 2 is opt-in. Say which games the mod is for: cannot serve all of it yet, so it is opt-in. Say which games the mod is for:
```json ```json
"games": ["gen1", "gen2"] "games": ["gen1", "gen2"]
``` ```
Each entry is a version id (`"red"`, `"blue"`, `"yellow"`, `"gold"`), a Each entry is a version id (`"red"`, `"blue"`, `"yellow"`, `"gold"`,
generation (`"gen1"`, `"gen2"`) or `"all"`; `"silver"`), a generation (`"gen1"`, `"gen2"`) or `"all"`;
`src/mods/ModTargets.lua` resolves them off `GameVersion.ORDER` so nothing `src/mods/ModTargets.lua` resolves them off `GameVersion.ORDER` so nothing
restates the game list. `python3 tools/modkit.py scaffold my_mod --games restates the game list. `python3 tools/modkit.py scaffold my_mod --games
gen1,gen2` writes the key for you. The mod still installs to one directory, gen1,gen2` writes the key for you. The mod still installs to one directory,
@@ -159,10 +159,10 @@ gen1,gen2` writes the key for you. The mod still installs to one directory,
Absent means Gen 1 only, which is what every mod written before the key existed Absent means Gen 1 only, which is what every mod written before the key existed
was tested as. `"gen2compat": true` is the legacy spelling, still accepted and was tested as. `"gen2compat": true` is the legacy spelling, still accepted and
purely additive (it *adds* the Gen 2 games), so no manifest can lose a game it purely additive (it *adds* the Gen 2 games), so no manifest can lose a game it
already ran on. On a Gold boot a mod claiming no Gen 2 game is not loaded at already ran on. On a Gold or Silver boot a mod claiming no Gen 2 game is not
all: the manager lists it as `ENABLED (NOT THIS GAME)` and says why, because a loaded at all: the manager lists it as `ENABLED (NOT THIS GAME)` and says why,
mod that half-applies reads as a broken mod. Claim Gen 2 once you have actually because a mod that half-applies reads as a broken mod. Claim Gen 2 once you
run your mod on Gold. have actually run your mod on Gold or Silver.
Every token is enforced, per game: the loader gates on the same Every token is enforced, per game: the loader gates on the same
`ModTargets.supports` answer both mod surfaces draw, so `"games": ["blue"]` `ModTargets.supports` answer both mod surfaces draw, so `"games": ["blue"]`
@@ -173,7 +173,7 @@ before the key existed changes behavior; list both generations or say `"all"`
when you mean everywhere. when you mean everywhere.
`docs/mod-api-gen2-compat.md` is the compatibility matrix: what works on Gold `docs/mod-api-gen2-compat.md` is the compatibility matrix: what works on Gold
today (40 of the 46 registries, 40 event and 43 hook names shared with Gen 1, and Silver today (40 of the 46 registries, 40 event and 43 hook names shared with Gen 1,
and 24 Gen 2-only ones), which registries have no Gen 2 home and drop their and 24 Gen 2-only ones), which registries have no Gen 2 home and drop their
writes with a report, and which hooks and events are still to come. writes with a report, and which hooks and events are still to come.
`docs/preparing-your-mod-for-gen2.md` is the step-by-step migration guide for a `docs/preparing-your-mod-for-gen2.md` is the step-by-step migration guide for a
+9 -8
View File
@@ -57,14 +57,14 @@ And before you say, "that's not a recomp", you're wrong. Recomp is an acronym. *
This project does not include a ROM, emulate the Game Boy, transpile assembly, This project does not include a ROM, emulate the Game Boy, transpile assembly,
or download a disassembly. A canonical US Poke Red, Blue, Yellow, or Gold ROM or download a disassembly. A canonical US Poke Red, Blue, Yellow, Gold, or
is the only game content input. Silver ROM is the only game content input.
The ROM is verified, used during import, and then released from memory. It is The ROM is verified, used during import, and then released from memory. It is
not copied into the cache. Later launches load the private generated cache and not copied into the cache. Later launches load the private generated cache and
do not ask for the ROM again. Red, Blue, Yellow, and Gold can all be imported do not ask for the ROM again. Red, Blue, Yellow, Gold, and Silver can all be
side by side. Gold is Gen 2 Phase 1 (import + launcher; see imported side by side. Gold and Silver are Gen 2 Phase 1 (import + launcher;
`docs/gold-phase1.md`): the Gen 2 engine is still under construction. see `docs/gold-phase1.md`): the Gen 2 engine is still under construction.
## Quick Start ## Quick Start
@@ -72,13 +72,14 @@ Open the desktop app. On first boot, choose your legally obtained `.gb` /
`.gbc` file or drop it onto the window. Import takes a few seconds and the `.gbc` file or drop it onto the window. Import takes a few seconds and the
game starts automatically. game starts automatically.
Only the canonical US Red, Blue, Yellow (1 MiB), and Gold (2 MiB) ROMs are Only the canonical US Red, Blue, Yellow (1 MiB), Gold, and Silver (2 MiB)
accepted. The importer verifies SHA-1 before creating any game data: ROMs are accepted. The importer verifies SHA-1 before creating any game data:
- Red: `ea9bcae617fdf159b045185467ae58b2e4a48b9a` - Red: `ea9bcae617fdf159b045185467ae58b2e4a48b9a`
- Blue: `d7037c83e1ae5b39bde3c30787637ba1d4c48ce2` - Blue: `d7037c83e1ae5b39bde3c30787637ba1d4c48ce2`
- Yellow: `cc7d03262ebfaf2f06772c1a480c7d9d5f4a38e1` - Yellow: `cc7d03262ebfaf2f06772c1a480c7d9d5f4a38e1`
- Gold: `d8b8a3600a465308c9953dfa04f0081c05bdcb94` - Gold: `d8b8a3600a465308c9953dfa04f0081c05bdcb94`
- Silver: `49b163f7e57702bc939d642a18f591de55d92dae`
The packaged app contains neither a ROM nor pre-extracted game data. Music, The packaged app contains neither a ROM nor pre-extracted game data. Music,
sound effects, and cries are synthesized while the game runs from compact sound effects, and cries are synthesized while the game runs from compact
@@ -219,7 +220,7 @@ entry: a desktop shortcut per game, a Steam entry, or a handheld frontend.
| Option | Effect | | Option | Effect |
| --- | --- | | --- | --- |
| `--game=red` | boot Red, skipping the launcher (`blue` and `yellow` too, or just `r` / `b` / `y`) | | `--game=red` | boot Red, skipping the launcher (`blue`, `yellow`, `gold` and `silver` too, or just `r` / `b` / `y` / `g` / `s`) |
| `--slot=2` | load that save slot; takes a slot number or a slot id | | `--slot=2` | load that save slot; takes a slot number or a slot id |
| `--launcher` | open the launcher anyway, so you can edit a shortcut you already made | | `--launcher` | open the launcher anyway, so you can edit a shortcut you already made |
+2
View File
@@ -133,6 +133,8 @@ mkdir -p "$GAME_SRC"
(cd "$SOURCE_DIR" && zip -q -9 -r "$WORK/game-payload.zip" \ (cd "$SOURCE_DIR" && zip -q -9 -r "$WORK/game-payload.zip" \
main.lua conf.lua src libs data assets tools/save-editor \ main.lua conf.lua src libs data assets tools/save-editor \
tools/rom_manifest.json tools/rom_manifest_blue.json \ tools/rom_manifest.json tools/rom_manifest_blue.json \
tools/rom_manifest_yellow.json tools/rom_manifest_gold.json \
tools/rom_manifest_silver.json \
-x '*.DS_Store' 'data/generated/*' 'assets/generated/*') -x '*.DS_Store' 'data/generated/*' 'assets/generated/*')
if unzip -Z1 "$WORK/game-payload.zip" \ if unzip -Z1 "$WORK/game-payload.zip" \
| grep -Eq '^(data|assets)/generated/[^/]+|^(data|assets)/generated/.+/'; then | grep -Eq '^(data|assets)/generated/[^/]+|^(data|assets)/generated/.+/'; then
+3
View File
@@ -92,6 +92,7 @@ mkdir -p "$GAME_SRC"
main.lua conf.lua src libs data assets tools/save-editor \ main.lua conf.lua src libs data assets tools/save-editor \
tools/rom_manifest.json tools/rom_manifest_blue.json \ tools/rom_manifest.json tools/rom_manifest_blue.json \
tools/rom_manifest_yellow.json tools/rom_manifest_gold.json \ tools/rom_manifest_yellow.json tools/rom_manifest_gold.json \
tools/rom_manifest_silver.json \
-x '*.DS_Store' 'data/generated/*' 'assets/generated/*') -x '*.DS_Store' 'data/generated/*' 'assets/generated/*')
payload_list="$(unzip -Z1 "$WORK/game-payload.zip")" payload_list="$(unzip -Z1 "$WORK/game-payload.zip")"
printf '%s\n' "$payload_list" \ printf '%s\n' "$payload_list" \
@@ -99,6 +100,8 @@ printf '%s\n' "$payload_list" \
&& fail "payload unexpectedly contains generated ROM data" && fail "payload unexpectedly contains generated ROM data"
printf '%s\n' "$payload_list" | grep -qxF "tools/rom_manifest_gold.json" \ printf '%s\n' "$payload_list" | grep -qxF "tools/rom_manifest_gold.json" \
|| fail "payload is missing tools/rom_manifest_gold.json" || fail "payload is missing tools/rom_manifest_gold.json"
printf '%s\n' "$payload_list" | grep -qxF "tools/rom_manifest_silver.json" \
|| fail "payload is missing tools/rom_manifest_silver.json"
unzip -q "$WORK/game-payload.zip" -d "$GAME_SRC" unzip -q "$WORK/game-payload.zip" -d "$GAME_SRC"
rm -f "$WORK/game-payload.zip" rm -f "$WORK/game-payload.zip"
+4 -3
View File
@@ -55,9 +55,10 @@ The short version, for an author deciding what to write:
``` ```
`games` is an optional array of version ids (`"red"`, `"blue"`, `"yellow"`, `games` is an optional array of version ids (`"red"`, `"blue"`, `"yellow"`,
`"gold"`), generations (`"gen1"`, `"gen2"`, case-insensitive) or `"all"`. `"gold"`, `"silver"`), generations (`"gen1"`, `"gen2"`, case-insensitive) or
`src/mods/ModTargets.lua` resolves the tokens off `GameVersion.ORDER` and `"all"`. `src/mods/ModTargets.lua` resolves the tokens off `GameVersion.ORDER`
`GameVersion.generation`, so nothing anywhere restates the game list. and `GameVersion.generation`, so nothing anywhere restates the game list.
`"gen2"` now expands to both Gold and Silver.
`Manifest.validate` stores the resolved, ORDER-sorted ids on `manifest.games` `Manifest.validate` stores the resolved, ORDER-sorted ids on `manifest.games`
and **derives** `manifest.gen2compat` from them, which is the one field the and **derives** `manifest.gen2compat` from them, which is the one field the
loader's gate reads. loader's gate reads.
+1 -1
View File
@@ -81,7 +81,7 @@ Every mod contains a root `manifest.json` defining its metadata, supported games
| `entry` | `string` | Entry Lua file path relative to mod root (usually `"main.lua"`). | | `entry` | `string` | Entry Lua file path relative to mod root (usually `"main.lua"`). |
| `profile` | `string` | Mod profile: `"content"`, `"overhaul"`, or `"total_conversion"`. | | `profile` | `string` | Mod profile: `"content"`, `"overhaul"`, or `"total_conversion"`. |
| `category` | `string` | Categorization chip (e.g. `"GAMEPLAY"`, `"CONTENT"`, `"UI"`, `"AUDIO"`). | | `category` | `string` | Categorization chip (e.g. `"GAMEPLAY"`, `"CONTENT"`, `"UI"`, `"AUDIO"`). |
| `games` | `array` | Supported game versions: `["gen1"]`, `["gen2"]`, `["red"]`, `["blue"]`, `["yellow"]`, `["gold"]`, or `["all"]`. | | `games` | `array` | Supported game versions: `["gen1"]`, `["gen2"]`, `["red"]`, `["blue"]`, `["yellow"]`, `["gold"]`, `["silver"]`, or `["all"]`. |
| `game_version`| `string` | Semver range of required engine version (e.g. `">=0.0.0-dev <2.0.0"`). | | `game_version`| `string` | Semver range of required engine version (e.g. `">=0.0.0-dev <2.0.0"`). |
| `priority` | `integer` | Load priority order (lower numbers load earlier; dependencies always precede dependents regardless of priority). | | `priority` | `integer` | Load priority order (lower numbers load earlier; dependencies always precede dependents regardless of priority). |
| `dependencies` | `array` | Hard required dependencies. A mod will not load if a required dependency is missing or disabled for the active game. | | `dependencies` | `array` | Hard required dependencies. A mod will not load if a required dependency is missing or disabled for the active game. |
+1
View File
@@ -16,5 +16,6 @@ Features intentionally added beyond the original Pokémon Red, Blue, and Yellow
## Gen 2 Specifics ## Gen 2 Specifics
* **Pokémon Silver** as an importable, launcher-selectable version alongside Gold
* **Mod manager** with Gen 1 mod adapters, per-game targeting, and `modkit gen2check` * **Mod manager** with Gen 1 mod adapters, per-game targeting, and `modkit gen2check`
* **Followers** for mods, plus Gen 2-only registries and hooks * **Followers** for mods, plus Gen 2-only registries and hooks
+1 -1
View File
@@ -176,7 +176,7 @@ something the filesystem encodes.
| token | means | | token | means |
| --- | --- | | --- | --- |
| `"red"`, `"blue"`, `"yellow"`, `"gold"` | that one game (a version id from `GameVersion.ORDER`) | | `"red"`, `"blue"`, `"yellow"`, `"gold"`, `"silver"` | that one game (a version id from `GameVersion.ORDER`) |
| `"gen1"`, `"gen2"` | every game of that generation (case-insensitive; `"gen 2"` also parses) | | `"gen1"`, `"gen2"` | every game of that generation (case-insensitive; `"gen 2"` also parses) |
| `"all"` | every game this engine has | | `"all"` | every game this engine has |
+4 -2
View File
@@ -1,7 +1,8 @@
# What This Port Requires # What This Port Requires
The packaged desktop app requires one user-supplied input on first boot: a The packaged desktop app requires one user-supplied input on first boot: a
canonical 1 MiB US Pokemon Red, Blue, or Yellow ROM. canonical 1 MiB US Pokemon Red, Blue, or Yellow ROM, or a canonical 2 MiB US
Pokemon Gold or Silver ROM.
The importer verifies the SHA-1 for the game (see `src/core/GameVersion.lua` The importer verifies the SHA-1 for the game (see `src/core/GameVersion.lua`
for specific hashes). Other revisions and Virtual Console releases are rejected for specific hashes). Other revisions and Virtual Console releases are rejected
@@ -15,7 +16,8 @@ Python and Pillow are not required by the packaged app.
Assembly removes high-level names and some relationships that the Lua port Assembly removes high-level names and some relationships that the Lua port
needs. The version-specific files `tools/rom_manifest.json`, needs. The version-specific files `tools/rom_manifest.json`,
`tools/rom_manifest_blue.json`, and `tools/rom_manifest_yellow.json` therefore `tools/rom_manifest_blue.json`, `tools/rom_manifest_yellow.json`,
`tools/rom_manifest_gold.json`, and `tools/rom_manifest_silver.json` therefore
contain: contain:
- the ROM symbol addresses actually read by the extractor - the ROM symbol addresses actually read by the extractor
+11 -9
View File
@@ -91,14 +91,14 @@ Do **not** launch from the Album applet path for normal play.
This project ships **no** game data. On first launch: This project ships **no** game data. On first launch:
1. Put your own legally obtained Pokémon Red, Blue (`.gb`), Yellow, or 1. Put your own legally obtained Pokémon Red, Blue (`.gb`), Yellow, Gold, or
Gold (`.gbc`) dump into `switch/gen1recomp/pokemon-love2d/imports/` (the Silver (`.gbc`) dump into `switch/gen1recomp/pokemon-love2d/imports/` (the
launcher also shows the live save-dir path). All four can sit in the launcher also shows the live save-dir path). All five can sit in the
same folder. same folder.
2. Use **Scan again** on that game's tab (Red / Blue / Yellow / Gold). 2. Use **Scan again** on that game's tab (Red / Blue / Yellow / Gold /
Rescan matches by ROM SHA-1 for the open tab only. A Red dump never Silver). Rescan matches by ROM SHA-1 for the open tab only. A Red dump
imports from the Yellow tab (and vice versa). Gold is Beta in the never imports from the Yellow tab (and vice versa). Gold and Silver are
launcher; a clean US Gold dump is enough to Play. Beta in the launcher; a clean US dump of either is enough to Play.
## 5. Import / Export a raw `.sav` ## 5. Import / Export a raw `.sav`
@@ -111,10 +111,12 @@ SD / FTP, same transfer methods as ROMs. Paths are **per game**:
| Blue | `imports/saves/blue/` | `exports/blue/` | | Blue | `imports/saves/blue/` | `exports/blue/` |
| Yellow | `imports/saves/yellow/` | `exports/yellow/` | | Yellow | `imports/saves/yellow/` | `exports/yellow/` |
| Gold | `imports/saves/gold/` | `exports/gold/` | | Gold | `imports/saves/gold/` | `exports/gold/` |
| Silver | `imports/saves/silver/` | `exports/silver/` |
(Under the save dir `pokemon-love2d/`. The zip already creates these folders. (Under the save dir `pokemon-love2d/`. The zip already creates these folders.
Gold cart `.sav` import/export is not supported yet -- the folders exist so Gold and Silver cart `.sav` import/export is not supported yet -- the folders
MTP browsing matches the other games. Gold progress still saves in-engine.) exist so MTP browsing matches the other games. Gold and Silver progress still
saves in-engine.)
1. Copy a Gen 1 `.sav` (32 KB) into that game's inbox under the save dir 1. Copy a Gen 1 `.sav` (32 KB) into that game's inbox under the save dir
([switch-transfer.md](switch-transfer.md)). ([switch-transfer.md](switch-transfer.md)).
+4 -3
View File
@@ -22,8 +22,8 @@ Player install (what to download, title override) stays in
| Loose iteration pair | `sdmc:/switch/gen1recomp/gen1recomp.nro` **and** `game.love` beside it | | Loose iteration pair | `sdmc:/switch/gen1recomp/gen1recomp.nro` **and** `game.love` beside it |
| ROM inbox | LÖVE save dir → `imports/` (launcher shows the live `getSaveDirectory()` path; under MTP often `1: SD Card/<save identity>/imports/`) | | ROM inbox | LÖVE save dir → `imports/` (launcher shows the live `getSaveDirectory()` path; under MTP often `1: SD Card/<save identity>/imports/`) |
| Mod zip inbox | Same save dir → `imports/mods/` then MODS → **Scan again** | | Mod zip inbox | Same save dir → `imports/mods/` then MODS → **Scan again** |
| Save `.sav` inbox | Same save dir → `imports/saves/red\|blue\|yellow\|gold/` then that game's SAVE FILES → **Import save** (Gold cart `.sav` not supported yet) | | Save `.sav` inbox | Same save dir → `imports/saves/red\|blue\|yellow\|gold\|silver/` then that game's SAVE FILES → **Import save** (Gold / Silver cart `.sav` not supported yet) |
| Save exports | Same save dir → `exports/red\|blue\|yellow\|gold/` (pull after **Export save**; Gold cart `.sav` not supported yet) | | Save exports | Same save dir → `exports/red\|blue\|yellow\|gold\|silver/` (pull after **Export save**; Gold / Silver cart `.sav` not supported yet) |
| Opt-in diagnostics | Empty `switch-debug.txt` in the save dir → `switch.log` | | Opt-in diagnostics | Empty `switch-debug.txt` in the save dir → `switch.log` |
| Lua error log | `lua-error.log` in the save dir | | Lua error log | `lua-error.log` in the save dir |
@@ -54,7 +54,8 @@ macOS, not a Mac-only requirement.
3. Create `switch/gen1recomp/` if needed; extract the release zip at SD root 3. Create `switch/gen1recomp/` if needed; extract the release zip at SD root
(or copy NRO / `game.love` for loose). (or copy NRO / `game.love` for loose).
4. For ROMs/mods/saves, open the save-dir `imports/`, `imports/mods/`, 4. For ROMs/mods/saves, open the save-dir `imports/`, `imports/mods/`,
`imports/saves/<red|blue|yellow|gold>/`, or `exports/<red|blue|yellow|gold>/` `imports/saves/<red|blue|yellow|gold|silver>/`, or
`exports/<red|blue|yellow|gold|silver>/`
path the launcher prints. path the launcher prints.
5. Wait for the queue; refresh; exit MTP responder; title-override launch. 5. Wait for the queue; refresh; exit MTP responder; title-override launch.
+6 -5
View File
@@ -386,11 +386,12 @@ function bootGame(version)
love.window.setTitle(Version.title( love.window.setTitle(Version.title(
GameVersion.info().displayName .. " (Gen 1 Recompilation Project)")) GameVersion.info().displayName .. " (Gen 1 Recompilation Project)"))
end end
-- Gold: Gen 1 Game:load cannot consume a Gen 2 cache -- different generated -- Gen 2: Gen 1 Game:load cannot consume a Gen 2 cache -- different generated
-- tables, save shape and screen registry -- so Gold boots its own service -- tables, save shape and screen registry -- so Gold and Silver boot their
-- owner, which mounts src/world/gen2 (walk / warps / connections) and the -- own service owner, which mounts src/world/gen2 (walk / warps /
-- Gen 2 screens instead of src/core/Game.lua's Gen 1 wiring. -- connections) and the Gen 2 screens instead of src/core/Game.lua's Gen 1
if GameVersion.isGold() then -- wiring.
if GameVersion.generation() == 2 then
Game = require("src.core.Game2").new() Game = require("src.core.Game2").new()
Game:load() Game:load()
else else
+4 -4
View File
@@ -109,10 +109,10 @@ The APK lands under `app/build/outputs/apk/embedNoRecord/debug/`.
`app/src/embed/assets/game.love` - zip of `main.lua`, `conf.lua`, `src/`, `app/src/embed/assets/game.love` - zip of `main.lua`, `conf.lua`, `src/`,
`libs/` (the vendored FlexLove toolkit the launcher UI needs), `data/`, `libs/` (the vendored FlexLove toolkit the launcher UI needs), `data/`,
`assets/`, and the Red, Blue, and Yellow ROM manifests. The Android `assets/`, and the Red, Blue, Yellow, Gold, and Silver ROM manifests. The
packer verifies the Yellow manifest before it packages; if a partial source Android packer verifies the Yellow, Gold, and Silver manifests before it
export omitted it, it restores the file from this checkout's Git data and then packages; if a partial source export omitted one, it restores the file from
falls back to the project's GitHub copy. Generated game data, this checkout's Git data and then falls back to the project's GitHub copy. Generated game data,
scripts, tests, and mobile build sources are excluded. scripts, tests, and mobile build sources are excluded.
## Branding (applied by the build script) ## Branding (applied by the build script)
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

@@ -8,4 +8,5 @@
<color name="shortcut_blue">#1E88E5</color> <color name="shortcut_blue">#1E88E5</color>
<color name="shortcut_yellow">#FDD835</color> <color name="shortcut_yellow">#FDD835</color>
<color name="shortcut_gold">#D4AF37</color> <color name="shortcut_gold">#D4AF37</color>
<color name="shortcut_silver">#BEC6D2</color>
</resources> </resources>
+3 -1
View File
@@ -71,6 +71,7 @@ rm -f "$LOVE_FILE"
main.lua conf.lua src data assets tools/save-editor \ main.lua conf.lua src data assets tools/save-editor \
tools/rom_manifest.json tools/rom_manifest_blue.json \ tools/rom_manifest.json tools/rom_manifest_blue.json \
tools/rom_manifest_yellow.json tools/rom_manifest_gold.json \ tools/rom_manifest_yellow.json tools/rom_manifest_gold.json \
tools/rom_manifest_silver.json \
-x '*.DS_Store' 'data/generated/*' 'assets/generated/*') -x '*.DS_Store' 'data/generated/*' 'assets/generated/*')
# Materialize the listing once and grep the file: piping unzip straight into # Materialize the listing once and grep the file: piping unzip straight into
# grep -q under `set -o pipefail` SIGPIPEs unzip when grep exits early on a # grep -q under `set -o pipefail` SIGPIPEs unzip when grep exits early on a
@@ -90,7 +91,8 @@ for required in tools/save-editor/App.lua tools/save-editor/Kit.lua \
tools/save-editor/panels/Party.lua \ tools/save-editor/panels/Party.lua \
src/ui/kit/Kit.lua \ src/ui/kit/Kit.lua \
tools/rom_manifest.json tools/rom_manifest_blue.json \ tools/rom_manifest.json tools/rom_manifest_blue.json \
tools/rom_manifest_yellow.json tools/rom_manifest_gold.json; do tools/rom_manifest_yellow.json tools/rom_manifest_gold.json \
tools/rom_manifest_silver.json; do
grep -qxF "$required" "$LOVE_LISTING" \ grep -qxF "$required" "$LOVE_LISTING" \
|| fail "game.love is missing $required" || fail "game.love is missing $required"
done done
+54
View File
@@ -30,6 +30,8 @@ YELLOW_MANIFEST_RELATIVE="tools/rom_manifest_yellow.json"
YELLOW_MANIFEST_URL="${YELLOW_MANIFEST_URL:-https://raw.githubusercontent.com/bryanthaboi/gen1recomp/main/tools/rom_manifest_yellow.json}" YELLOW_MANIFEST_URL="${YELLOW_MANIFEST_URL:-https://raw.githubusercontent.com/bryanthaboi/gen1recomp/main/tools/rom_manifest_yellow.json}"
GOLD_MANIFEST_RELATIVE="tools/rom_manifest_gold.json" GOLD_MANIFEST_RELATIVE="tools/rom_manifest_gold.json"
GOLD_MANIFEST_URL="${GOLD_MANIFEST_URL:-https://raw.githubusercontent.com/bryanthaboi/gen1recomp/main/tools/rom_manifest_gold.json}" GOLD_MANIFEST_URL="${GOLD_MANIFEST_URL:-https://raw.githubusercontent.com/bryanthaboi/gen1recomp/main/tools/rom_manifest_gold.json}"
SILVER_MANIFEST_RELATIVE="tools/rom_manifest_silver.json"
SILVER_MANIFEST_URL="${SILVER_MANIFEST_URL:-https://raw.githubusercontent.com/bryanthaboi/gen1recomp/main/tools/rom_manifest_silver.json}"
VERSION="" VERSION=""
PACKAGE_ONLY=false PACKAGE_ONLY=false
@@ -177,6 +179,54 @@ ensure_gold_manifest() {
fail "Gold import manifest is unavailable. Git recovery failed and could not download $GOLD_MANIFEST_URL" fail "Gold import manifest is unavailable. Git recovery failed and could not download $GOLD_MANIFEST_URL"
} }
silver_manifest_is_valid() {
local path="$1"
python3 - "$path" <<'PY'
import json, pathlib, sys
try:
manifest = json.loads(pathlib.Path(sys.argv[1]).read_text())
except (OSError, ValueError):
raise SystemExit(1)
raise SystemExit(0 if manifest.get("romSha1") ==
"49b163f7e57702bc939d642a18f591de55d92dae" else 1)
PY
}
ensure_silver_manifest() {
local manifest="$ROOT/$SILVER_MANIFEST_RELATIVE"
local staged
staged="$(mktemp)"
if silver_manifest_is_valid "$manifest"; then
rm -f "$staged"
return
fi
warn "Silver import manifest is missing or invalid; recovering it before packaging"
if git -C "$ROOT" show "HEAD:$SILVER_MANIFEST_RELATIVE" > "$staged" 2>/dev/null \
&& silver_manifest_is_valid "$staged"; then
mkdir -p "$(dirname "$manifest")"
mv "$staged" "$manifest"
say "restored Silver import manifest from this checkout's Git data"
return
fi
if command -v curl >/dev/null 2>&1 \
&& curl --fail --location --retry 2 --connect-timeout 15 \
--output "$staged" "$SILVER_MANIFEST_URL" \
&& silver_manifest_is_valid "$staged"; then
mkdir -p "$(dirname "$manifest")"
mv "$staged" "$manifest"
say "downloaded Silver import manifest from the project repository"
return
fi
rm -f "$staged"
fail "Silver import manifest is unavailable. Git recovery failed and could not download $SILVER_MANIFEST_URL"
}
# --------------------------------------------------------------- branding # --------------------------------------------------------------- branding
# love-android 11.5+ reads app id / name / orientation from gradle.properties. # love-android 11.5+ reads app id / name / orientation from gradle.properties.
# Manifest still gets permission trims. Re-applied every build so refreshing # Manifest still gets permission trims. Re-applied every build so refreshing
@@ -242,6 +292,7 @@ pack_game_love() {
say "packing game.love for love-android embed flavor" say "packing game.love for love-android embed flavor"
ensure_yellow_manifest ensure_yellow_manifest
ensure_gold_manifest ensure_gold_manifest
ensure_silver_manifest
mkdir -p "$EMBED_ASSETS" mkdir -p "$EMBED_ASSETS"
rm -f "$LOVE_FILE" rm -f "$LOVE_FILE"
# tools/save-editor ships with the app: the launcher's Edit button on a save # tools/save-editor ships with the app: the launcher's Edit button on a save
@@ -256,6 +307,7 @@ pack_game_love() {
main.lua conf.lua src data assets tools/save-editor \ main.lua conf.lua src data assets tools/save-editor \
tools/rom_manifest.json tools/rom_manifest_blue.json \ tools/rom_manifest.json tools/rom_manifest_blue.json \
tools/rom_manifest_yellow.json tools/rom_manifest_gold.json \ tools/rom_manifest_yellow.json tools/rom_manifest_gold.json \
tools/rom_manifest_silver.json \
-x '*.DS_Store' -x '*/.git/*' -x '*/.DS_Store' \ -x '*.DS_Store' -x '*/.git/*' -x '*/.DS_Store' \
-x 'data/generated/*' -x 'assets/generated/*') -x 'data/generated/*' -x 'assets/generated/*')
# List once and match against the captured text: piping unzip straight into # List once and match against the captured text: piping unzip straight into
@@ -277,6 +329,8 @@ pack_game_love() {
|| fail "game.love is missing the Yellow ROM import manifest" || fail "game.love is missing the Yellow ROM import manifest"
grep -qx 'tools/rom_manifest_gold.json' <<< "$archive_entries" \ grep -qx 'tools/rom_manifest_gold.json' <<< "$archive_entries" \
|| fail "game.love is missing the Gold ROM import manifest" || fail "game.love is missing the Gold ROM import manifest"
grep -qx 'tools/rom_manifest_silver.json' <<< "$archive_entries" \
|| fail "game.love is missing the Silver ROM import manifest"
# This gate exists because the launcher's UI toolkit once lived outside # This gate exists because the launcher's UI toolkit once lived outside
# src/ (libs/flexlove) and was added to scripts/build.sh's payload and to # src/ (libs/flexlove) and was added to scripts/build.sh's payload and to
# no other packager, so Android and iOS built an APK/IPA whose launcher # no other packager, so Android and iOS built an APK/IPA whose launcher
+2
View File
@@ -366,6 +366,8 @@ cp "$IN/game.love" "$APPDIR/game.love"
unzip -Z1 "$APPDIR/game.love" > "$WORK/love-listing.txt" unzip -Z1 "$APPDIR/game.love" > "$WORK/love-listing.txt"
grep -qxF "tools/rom_manifest_gold.json" "$WORK/love-listing.txt" \ grep -qxF "tools/rom_manifest_gold.json" "$WORK/love-listing.txt" \
|| fail "game.love is missing tools/rom_manifest_gold.json" || fail "game.love is missing tools/rom_manifest_gold.json"
grep -qxF "tools/rom_manifest_silver.json" "$WORK/love-listing.txt" \
|| fail "game.love is missing tools/rom_manifest_silver.json"
# The .desktop's Icon= resolves against the AppDir root by basename, and # The .desktop's Icon= resolves against the AppDir root by basename, and
# .DirIcon is what appimaged and file-manager thumbnailers read. # .DirIcon is what appimaged and file-manager thumbnailers read.
cp "$IN/icon.png" "$APPDIR/$APP_NAME.png" cp "$IN/icon.png" "$APPDIR/$APP_NAME.png"
@@ -169,5 +169,7 @@ unzip -p "$temp_dir/game.love" src/core/Version.lua \
|| fail "shared payload version was not stamped" || fail "shared payload version was not stamped"
grep -qxF "tools/rom_manifest_gold.json" "$temp_dir/love-listing.txt" \ grep -qxF "tools/rom_manifest_gold.json" "$temp_dir/love-listing.txt" \
|| fail "shared payload is missing tools/rom_manifest_gold.json" || fail "shared payload is missing tools/rom_manifest_gold.json"
grep -qxF "tools/rom_manifest_silver.json" "$temp_dir/love-listing.txt" \
|| fail "shared payload is missing tools/rom_manifest_silver.json"
say "Linux arm64 self-test passed" say "Linux arm64 self-test passed"
+2
View File
@@ -50,6 +50,7 @@ rm -f "$OUTPUT"
main.lua conf.lua src data assets tools/save-editor \ main.lua conf.lua src data assets tools/save-editor \
tools/rom_manifest.json tools/rom_manifest_blue.json \ tools/rom_manifest.json tools/rom_manifest_blue.json \
tools/rom_manifest_yellow.json tools/rom_manifest_gold.json \ tools/rom_manifest_yellow.json tools/rom_manifest_gold.json \
tools/rom_manifest_silver.json \
-x '*.DS_Store' 'data/generated/*' 'assets/generated/*') -x '*.DS_Store' 'data/generated/*' 'assets/generated/*')
if [ -f "$ROOT/PATCH_NOTES.md" ]; then if [ -f "$ROOT/PATCH_NOTES.md" ]; then
(cd "$ROOT" && zip -q "$OUTPUT" PATCH_NOTES.md) (cd "$ROOT" && zip -q "$OUTPUT" PATCH_NOTES.md)
@@ -94,6 +95,7 @@ for required in tools/save-editor/App.lua tools/save-editor/Kit.lua \
tools/save-editor/panels/Party.lua \ tools/save-editor/panels/Party.lua \
tools/rom_manifest.json tools/rom_manifest_blue.json \ tools/rom_manifest.json tools/rom_manifest_blue.json \
tools/rom_manifest_yellow.json tools/rom_manifest_gold.json \ tools/rom_manifest_yellow.json tools/rom_manifest_gold.json \
tools/rom_manifest_silver.json \
src/ui/kit/Kit.lua \ src/ui/kit/Kit.lua \
src/import/LauncherView.lua; do src/import/LauncherView.lua; do
grep -qxF "$required" "$LISTING" \ grep -qxF "$required" "$LISTING" \
+3 -1
View File
@@ -17,7 +17,9 @@ fail() { printf '\033[1;31merror:\033[0m %s\n' "$*" >&2; exit 1; }
if [ ! -f "$ROOT/data/generated/maps.lua" ] \ if [ ! -f "$ROOT/data/generated/maps.lua" ] \
&& [ ! -f "$ROOT/blue/data/generated/maps.lua" ] \ && [ ! -f "$ROOT/blue/data/generated/maps.lua" ] \
&& [ ! -f "$ROOT/yellow/data/generated/maps.lua" ]; then && [ ! -f "$ROOT/yellow/data/generated/maps.lua" ] \
&& [ ! -f "$ROOT/gold/data/generated/maps.lua" ] \
&& [ ! -f "$ROOT/silver/data/generated/maps.lua" ]; then
fail "generated data missing, run scripts/setup.sh first" fail "generated data missing, run scripts/setup.sh first"
fi fi
+10 -4
View File
@@ -71,15 +71,15 @@ First install or update (same steps):
your saves, imported ROMs, mods, and options. Re-extracting only your saves, imported ROMs, mods, and options. Re-extracting only
replaces the NRO(s) and these help files. replaces the NRO(s) and these help files.
3. Launch with title override (hold R on HOME, open any title → hbmenu). 3. Launch with title override (hold R on HOME, open any title → hbmenu).
4. Copy a legal Pokemon Red/Blue .gb or Yellow/Gold .gbc into: 4. Copy a legal Pokemon Red/Blue .gb or Yellow/Gold/Silver .gbc into:
switch/gen1recomp/pokemon-love2d/imports/ switch/gen1recomp/pokemon-love2d/imports/
then use Scan again in the launcher if needed. then use Scan again in the launcher if needed.
Inboxes (drop files here via MTP / SD / FTP): Inboxes (drop files here via MTP / SD / FTP):
imports/ — ROM .gb / .gbc imports/ — ROM .gb / .gbc
imports/mods/ — community mod .zip imports/mods/ — community mod .zip
imports/saves/red|blue|yellow|gold/ — raw .sav import (Gold cart .sav not yet) imports/saves/red|blue|yellow|gold|silver/ — raw .sav import (Gold/Silver cart .sav not yet)
exports/red|blue|yellow|gold/ — pull after Export save (Gold not yet) exports/red|blue|yellow|gold|silver/ — pull after Export save (Gold/Silver not yet)
Full guide: https://github.com/bryanthaboi/gen1recomp/blob/main/docs/switch-install.md Full guide: https://github.com/bryanthaboi/gen1recomp/blob/main/docs/switch-install.md
EOF EOF
@@ -92,7 +92,7 @@ write_readme() {
} }
write_readme "$SAVE_ROOT/imports/README.txt" \ write_readme "$SAVE_ROOT/imports/README.txt" \
"Put a legal Pokemon Red/Blue .gb or Yellow/Gold .gbc here, then Scan again in the launcher." "Put a legal Pokemon Red/Blue .gb or Yellow/Gold/Silver .gbc here, then Scan again in the launcher."
write_readme "$SAVE_ROOT/imports/mods/README.txt" \ write_readme "$SAVE_ROOT/imports/mods/README.txt" \
"Put community mod .zip files here, then MODS → Scan again." "Put community mod .zip files here, then MODS → Scan again."
write_readme "$SAVE_ROOT/imports/saves/red/README.txt" \ write_readme "$SAVE_ROOT/imports/saves/red/README.txt" \
@@ -103,6 +103,8 @@ write_readme "$SAVE_ROOT/imports/saves/yellow/README.txt" \
"Put a Yellow .sav (32 KB) here, then Yellow tab → SAVE FILES → Import save." "Put a Yellow .sav (32 KB) here, then Yellow tab → SAVE FILES → Import save."
write_readme "$SAVE_ROOT/imports/saves/gold/README.txt" \ write_readme "$SAVE_ROOT/imports/saves/gold/README.txt" \
"Gold cart .sav import is not supported yet. Folder reserved so MTP matches the other games." "Gold cart .sav import is not supported yet. Folder reserved so MTP matches the other games."
write_readme "$SAVE_ROOT/imports/saves/silver/README.txt" \
"Silver cart .sav import is not supported yet. Folder reserved so MTP matches the other games."
write_readme "$SAVE_ROOT/exports/red/README.txt" \ write_readme "$SAVE_ROOT/exports/red/README.txt" \
"After Export save (Red), copy the .sav out of this folder via MTP / SD / FTP." "After Export save (Red), copy the .sav out of this folder via MTP / SD / FTP."
write_readme "$SAVE_ROOT/exports/blue/README.txt" \ write_readme "$SAVE_ROOT/exports/blue/README.txt" \
@@ -111,6 +113,8 @@ write_readme "$SAVE_ROOT/exports/yellow/README.txt" \
"After Export save (Yellow), copy the .sav out of this folder via MTP / SD / FTP." "After Export save (Yellow), copy the .sav out of this folder via MTP / SD / FTP."
write_readme "$SAVE_ROOT/exports/gold/README.txt" \ write_readme "$SAVE_ROOT/exports/gold/README.txt" \
"Gold cart .sav export is not supported yet. Folder reserved so MTP matches the other games." "Gold cart .sav export is not supported yet. Folder reserved so MTP matches the other games."
write_readme "$SAVE_ROOT/exports/silver/README.txt" \
"Silver cart .sav export is not supported yet. Folder reserved so MTP matches the other games."
rm -f "$OUT_ZIP" rm -f "$OUT_ZIP"
( (
@@ -140,10 +144,12 @@ REQUIRED=(
"switch/gen1recomp/pokemon-love2d/imports/saves/blue/README.txt" "switch/gen1recomp/pokemon-love2d/imports/saves/blue/README.txt"
"switch/gen1recomp/pokemon-love2d/imports/saves/yellow/README.txt" "switch/gen1recomp/pokemon-love2d/imports/saves/yellow/README.txt"
"switch/gen1recomp/pokemon-love2d/imports/saves/gold/README.txt" "switch/gen1recomp/pokemon-love2d/imports/saves/gold/README.txt"
"switch/gen1recomp/pokemon-love2d/imports/saves/silver/README.txt"
"switch/gen1recomp/pokemon-love2d/exports/red/README.txt" "switch/gen1recomp/pokemon-love2d/exports/red/README.txt"
"switch/gen1recomp/pokemon-love2d/exports/blue/README.txt" "switch/gen1recomp/pokemon-love2d/exports/blue/README.txt"
"switch/gen1recomp/pokemon-love2d/exports/yellow/README.txt" "switch/gen1recomp/pokemon-love2d/exports/yellow/README.txt"
"switch/gen1recomp/pokemon-love2d/exports/gold/README.txt" "switch/gen1recomp/pokemon-love2d/exports/gold/README.txt"
"switch/gen1recomp/pokemon-love2d/exports/silver/README.txt"
) )
for rel in "${REQUIRED[@]}"; do for rel in "${REQUIRED[@]}"; do
printf '%s\n' "$LISTING" | grep -Fq "$rel" || fail "zip missing $rel" printf '%s\n' "$LISTING" | grep -Fq "$rel" || fail "zip missing $rel"
+3 -1
View File
@@ -271,10 +271,12 @@ for rel in \
"switch/gen1recomp/pokemon-love2d/imports/saves/blue/README.txt" \ "switch/gen1recomp/pokemon-love2d/imports/saves/blue/README.txt" \
"switch/gen1recomp/pokemon-love2d/imports/saves/yellow/README.txt" \ "switch/gen1recomp/pokemon-love2d/imports/saves/yellow/README.txt" \
"switch/gen1recomp/pokemon-love2d/imports/saves/gold/README.txt" \ "switch/gen1recomp/pokemon-love2d/imports/saves/gold/README.txt" \
"switch/gen1recomp/pokemon-love2d/imports/saves/silver/README.txt" \
"switch/gen1recomp/pokemon-love2d/exports/red/README.txt" \ "switch/gen1recomp/pokemon-love2d/exports/red/README.txt" \
"switch/gen1recomp/pokemon-love2d/exports/blue/README.txt" \ "switch/gen1recomp/pokemon-love2d/exports/blue/README.txt" \
"switch/gen1recomp/pokemon-love2d/exports/yellow/README.txt" \ "switch/gen1recomp/pokemon-love2d/exports/yellow/README.txt" \
"switch/gen1recomp/pokemon-love2d/exports/gold/README.txt" "switch/gen1recomp/pokemon-love2d/exports/gold/README.txt" \
"switch/gen1recomp/pokemon-love2d/exports/silver/README.txt"
do do
printf '%s\n' "$ZIP_LIST" | grep -Fq "$rel" || PACK_MISSING="${PACK_MISSING} ${rel}" printf '%s\n' "$ZIP_LIST" | grep -Fq "$rel" || PACK_MISSING="${PACK_MISSING} ${rel}"
done done
+2 -2
View File
@@ -3,7 +3,7 @@
-- everything under src/*/gen2 reaches shared services through here. Gen 1 -- everything under src/*/gen2 reaches shared services through here. Gen 1
-- Game:load cannot consume a Gen 2 cache -- different generated tables, save -- Game:load cannot consume a Gen 2 cache -- different generated tables, save
-- shape and screen registry -- so main.lua's bootGame picks this owner when -- shape and screen registry -- so main.lua's bootGame picks this owner when
-- GameVersion.isGold(), and the two never branch into each other. -- GameVersion.generation() == 2, and the two never branch into each other.
-- --
-- Boot: copyright → GameFreak Presents → GS intro stub → title -- Boot: copyright → GameFreak Presents → GS intro stub → title
-- (tilemap + Ho-Oh flap / clouds / trails) → Oak speech (Marill + shrink) -- (tilemap + Ho-Oh flap / clouds / trails) → Oak speech (Marill + shrink)
@@ -1702,7 +1702,7 @@ function Game2:hotkey(key)
self:writeSave() self:writeSave()
return true return true
elseif key == "f2" then elseif key == "f2" then
local loaded = Save.load("gold") local loaded = Save.load()
if loaded then self:continueGame(loaded) end if loaded then self:continueGame(loaded) end
return true return true
elseif key == "1" then elseif key == "1" then
+18 -4
View File
@@ -1,5 +1,5 @@
-- Which game this process is running: Red (the historical default), Blue, -- Which game this process is running: Red (the historical default), Blue,
-- Yellow, or Gold. One source of truth for everything that differs by -- Yellow, Gold, or Silver. One source of truth for everything that differs by
-- version -- the accepted ROM hash, the import manifest, where the -- version -- the accepted ROM hash, the import manifest, where the
-- extracted cache lives, and the save-file suffix -- so the importer, -- extracted cache lives, and the save-file suffix -- so the importer,
-- cache mount, SaveData, title screen and palette all agree. -- cache mount, SaveData, title screen and palette all agree.
@@ -8,7 +8,8 @@
-- saves are untouched, but its extracted cache lives under red/ like Blue, -- saves are untouched, but its extracted cache lives under red/ like Blue,
-- Yellow, and Gold (issue #899); a legacy root cache is moved into red/ once -- Yellow, and Gold (issue #899); a legacy root cache is moved into red/ once
-- by CacheFs.migrateLegacyRedCache. All supported versions can be imported -- by CacheFs.migrateLegacyRedCache. All supported versions can be imported
-- and selected side by side. Gold is Gen 2 (see docs/gold-phase1.md). -- and selected side by side. Gold and Silver are Gen 2 (see
-- docs/gold-phase1.md).
-- --
-- Zero requires, so it loads during love.conf and under plain Lua for tools -- Zero requires, so it loads during love.conf and under plain Lua for tools
-- and tests. The active version is a process-global set once at boot from -- and tests. The active version is a process-global set once at boot from
@@ -61,13 +62,26 @@ GameVersion.VERSIONS = {
manifest = "tools/rom_manifest_gold.json", manifest = "tools/rom_manifest_gold.json",
cachePrefix = "gold/", -- gold/data/generated, gold/assets/generated cachePrefix = "gold/", -- gold/data/generated, gold/assets/generated
saveSuffix = "_gold", -- save_gold.lua / .bak / .tmp saveSuffix = "_gold", -- save_gold.lua / .bak / .tmp
-- The only row that carries one; absent reads as 1 (GameVersion.generation) -- Absent reads as 1 (GameVersion.generation)
generation = 2,
},
-- Gold's engine with edition-selected data; the manifest is derived from
-- Gold's by tools/make_silver_manifest.py.
silver = {
id = "silver",
label = "Silver",
displayName = "Pokemon Silver",
launcherName = "Silver (Beta)",
sha1 = "49b163f7e57702bc939d642a18f591de55d92dae",
manifest = "tools/rom_manifest_silver.json",
cachePrefix = "silver/", -- silver/data/generated, silver/assets/generated
saveSuffix = "_silver", -- save_silver.lua / .bak / .tmp
generation = 2, generation = 2,
}, },
} }
-- Launcher column order. -- Launcher column order.
GameVersion.ORDER = { "red", "blue", "yellow", "gold" } GameVersion.ORDER = { "red", "blue", "yellow", "gold", "silver" }
GameVersion.current = "red" GameVersion.current = "red"
+1
View File
@@ -39,6 +39,7 @@ local function normalizeVersion(v)
b = "blue", blue = "blue", b = "blue", blue = "blue",
y = "yellow", yellow = "yellow", y = "yellow", yellow = "yellow",
g = "gold", gold = "gold", g = "gold", gold = "gold",
s = "silver", silver = "silver",
} }
v = alias[v] or v v = alias[v] or v
if GameVersion.VERSIONS and not GameVersion.VERSIONS[v] then return nil end if GameVersion.VERSIONS and not GameVersion.VERSIONS[v] then return nil end
+1 -1
View File
@@ -218,7 +218,7 @@ function TouchControls.defaultLayout(ww, wh, ox, oy, scale)
local ssW = dpadW * 0.30 local ssW = dpadW * 0.30
local margin = dpadW * 0.12 local margin = dpadW * 0.12
local ok, GameVersion = pcall(require, "src.core.GameVersion") local ok, GameVersion = pcall(require, "src.core.GameVersion")
if ok and GameVersion.isGold and GameVersion.isGold() then if ok and GameVersion.generation and GameVersion.generation() == 2 then
margin = math.max(margin, math.min(ww * 0.10, 72)) margin = math.max(margin, math.min(ww * 0.10, 72))
end end
return { return {
+24 -12
View File
@@ -103,7 +103,7 @@ Save.PLAYER_STATES = {
} }
local function saveNames(version) local function saveNames(version)
version = version or "gold" version = version or GameVersion.get()
-- Resolve the ACTIVE SLOT the same way SaveData does, and only fall back to -- Resolve the ACTIVE SLOT the same way SaveData does, and only fall back to
-- the flat save_<version>.lua when no slot is registered. -- the flat save_<version>.lua when no slot is registered.
-- --
@@ -133,16 +133,22 @@ local function fs()
return love.filesystem return love.filesystem
end end
-- A fresh Gold save. `opts` carries what the intro collected: player name, -- The blank-name fallback is the first PlayerNameArray row, which differs
-- per edition -- data/player_names.asm:12-23.
function Save.defaultPlayerName(version)
return (version or GameVersion.get()) == "silver" and "SILVER" or "GOLD"
end
-- A fresh Gen 2 save. `opts` carries what the intro collected: player name,
-- rival name, and the options the OPTION screen was left on. -- rival name, and the options the OPTION screen was left on.
function Save.newGame(opts) function Save.newGame(opts)
opts = opts or {} opts = opts or {}
local save = { local save = {
format = Save.FORMAT, format = Save.FORMAT,
version = "gold", version = GameVersion.get(),
generation = 2, generation = 2,
player = { player = {
name = opts.playerName or "GOLD", name = opts.playerName or Save.defaultPlayerName(),
-- _ResetWRAM rolls wPlayerID out of hRandomSub/hRandomAdd -- _ResetWRAM rolls wPlayerID out of hRandomSub/hRandomAdd
-- (engine/menus/intro_menu.asm:41-49). -- (engine/menus/intro_menu.asm:41-49).
id = opts.trainerId or rand(0, 65535), id = opts.trainerId or rand(0, 65535),
@@ -374,10 +380,13 @@ end
function Save.normalize(save) function Save.normalize(save)
if type(save) ~= "table" then return nil end if type(save) ~= "table" then return nil end
save.format = save.format or Save.FORMAT save.format = save.format or Save.FORMAT
save.version = "gold" if not (GameVersion.VERSIONS[save.version]
and GameVersion.generation(save.version) == 2) then
save.version = GameVersion.get()
end
save.generation = 2 save.generation = 2
save.player = save.player or {} save.player = save.player or {}
save.player.name = save.player.name or "GOLD" save.player.name = save.player.name or Save.defaultPlayerName(save.version)
save.player.id = save.player.id or rand(0, 65535) save.player.id = save.player.id or rand(0, 65535)
save.player.money = math.max(0, math.min(save.player.money or 0, Save.MAX_MONEY)) save.player.money = math.max(0, math.min(save.player.money or 0, Save.MAX_MONEY))
save.player.coins = math.max(0, math.min(save.player.coins or 0, Save.MAX_COINS)) save.player.coins = math.max(0, math.min(save.player.coins or 0, Save.MAX_COINS))
@@ -753,21 +762,24 @@ end
-- copy is the witness that survives a crash mid-replace. -- copy is the witness that survives a crash mid-replace.
function Save.save(save) function Save.save(save)
if type(save) ~= "table" then return false, "no save" end if type(save) ~= "table" then return false, "no save" end
if (save.version or "gold") == "gold" then Save.normalize(save)
local version = save.version
do
local ok, SaveData = pcall(require, "src.core.SaveData") local ok, SaveData = pcall(require, "src.core.SaveData")
if ok and SaveData.activeSlot and not SaveData.activeSlot("gold") then if ok and SaveData.activeSlot and not SaveData.activeSlot(version) then
local id = SaveData.createSlot and SaveData.createSlot("gold") local id = SaveData.createSlot and SaveData.createSlot(version)
if id and SaveData.setActiveSlot then SaveData.setActiveSlot("gold", id) end if id and SaveData.setActiveSlot then
SaveData.setActiveSlot(version, id)
end
end end
end end
local main, backup, tmp = saveNames(save.version) local main, backup, tmp = saveNames(version)
local f = fs() local f = fs()
if not f then return false, "no filesystem" end if not f then return false, "no filesystem" end
-- saveNames may now return a saves/<version>/<slot>.lua path, and -- saveNames may now return a saves/<version>/<slot>.lua path, and
-- love.filesystem.write does not create missing parent directories. -- love.filesystem.write does not create missing parent directories.
local dir = main:match("^(.*)/[^/]+$") local dir = main:match("^(.*)/[^/]+$")
if dir and f.createDirectory then f.createDirectory(dir) end if dir and f.createDirectory then f.createDirectory(dir) end
Save.normalize(save)
save.savedAt = os.time() save.savedAt = os.time()
local encoded = SaveSerializer.encode(save) local encoded = SaveSerializer.encode(save)
if f.getInfo(main) then if f.getInfo(main) then
+4 -2
View File
@@ -249,10 +249,12 @@ function SwitchDiagnostics.probeAssets(version)
end end
-- Shallow listing so we can see if the extract tree exists at all. -- Shallow listing so we can see if the extract tree exists at all.
local roots = { "yellow", "blue", "gold", "assets", "yellow/assets/generated", local roots = { "yellow", "blue", "gold", "silver", "assets",
"yellow/assets/generated",
"yellow/assets/generated/sprites", "blue/assets/generated/sprites", "yellow/assets/generated/sprites", "blue/assets/generated/sprites",
"gold/assets/generated", "gold/assets/generated/sprites", "gold/assets/generated", "gold/assets/generated/sprites",
"gold/data/generated" } "gold/data/generated", "silver/assets/generated",
"silver/data/generated" }
for _, dir in ipairs(roots) do for _, dir in ipairs(roots) do
local info = filesystem.getInfo(dir) local info = filesystem.getInfo(dir)
if info and info.type == "directory" and filesystem.getDirectoryItems then if info and info.type == "directory" and filesystem.getDirectoryItems then
+2 -1
View File
@@ -27,7 +27,8 @@ local ok, err = pcall(function()
CacheFs.prefix = prefix CacheFs.prefix = prefix
local manifest = require("src.import.RomManifest").decode(version) local manifest = require("src.import.RomManifest").decode(version)
local RomExtractor = version == "gold" local RomExtractor =
require("src.core.GameVersion").generation(version) == 2
and require("src.import.RomExtractorGen2") and require("src.import.RomExtractorGen2")
or require("src.import.RomExtractor") or require("src.import.RomExtractor")
+7 -4
View File
@@ -573,9 +573,10 @@ end
-- opened on the Gold tab used to offer a dozen controls that did nothing -- opened on the Gold tab used to offer a dozen controls that did nothing
-- and hide the seven that the cart itself has. -- and hide the seven that the cart itself has.
-- --
-- The block lives in options.lua under `gold`, which is exactly where -- The block lives in options.lua under `gold` (the historical key; Gold and
-- src/core/gen2/Save.lua loadOptions reads it, so an edit here is live on the -- Silver share it the way the Gen 1 games share the flat namespace), which is
-- next boot the same way a Gen 1 edit is. Ladders mirror -- exactly where src/core/gen2/Save.lua loadOptions reads it, so an edit here
-- is live on the next boot the same way a Gen 1 edit is. Ladders mirror
-- src/ui/gen2/OptionsMenu.lua's ROWS; when editing one, keep the two in sync. -- src/ui/gen2/OptionsMenu.lua's ROWS; when editing one, keep the two in sync.
local GEN2_KEY = "gold" local GEN2_KEY = "gold"
@@ -722,7 +723,9 @@ end
function LauncherSettings.open(hooks, version) function LauncherSettings.open(hooks, version)
local opts = SaveData.loadOptions() local opts = SaveData.loadOptions()
local sections local sections
if version == "gold" then local GameVersion = require("src.core.GameVersion")
if GameVersion.VERSIONS[version]
and GameVersion.generation(version) == 2 then
local block = opts[GEN2_KEY] local block = opts[GEN2_KEY]
if type(block) ~= "table" then if type(block) ~= "table" then
block = {} block = {}
+128 -179
View File
@@ -14,9 +14,9 @@
-- height, and flex-shrink compressing text until it overlapped. -- height, and flex-shrink compressing text until it overlapped.
-- --
-- THE RULES THIS FILE FOLLOWS: -- THE RULES THIS FILE FOLLOWS:
-- * Lists paginate (Kit.pager). The installed-mod list also scrolls inside -- * Short lists paginate (Kit.pager, perPage from Kit.rowsThatFit); the
-- its viewport, so each of its pages can hold at least ten entries without -- installed-mod list is one continuous scroll instead, drawing only the
-- requiring a tall window. Pages still bound how many mod rows we visit. -- rows inside the region viewport so the window bounds the frame cost.
-- * Every click handler only QUEUES work (imp._uiActions); update() drains -- * Every click handler only QUEUES work (imp._uiActions); update() drains
-- the queue, so an action that tears the view down (Play, Edit save) -- the queue, so an action that tears the view down (Play, Edit save)
-- never runs inside the frame that dispatched it. -- never runs inside the frame that dispatched it.
@@ -45,9 +45,6 @@ local COMMUNITY_URL = "https://bois.icu"
local ACT_DEDUP = 0.35 local ACT_DEDUP = 0.35
-- Finger travel past this (px) is a drag, not a tap. -- Finger travel past this (px) is a drag, not a tap.
local TAP_SLOP2 = 16 * 16 local TAP_SLOP2 = 16 * 16
-- Installed mods should not turn into a one- or two-item pager on a compact
-- display. Keep a useful page size, then let the list viewport scroll.
local MIN_MODS_PER_PAGE = 10
local MIN_SKIN_ROWS = 4 local MIN_SKIN_ROWS = 4
local SKIN_FORMAT_LABEL = { local SKIN_FORMAT_LABEL = {
native = "GEN1", native = "GEN1",
@@ -81,15 +78,6 @@ local function setTabScroll(imp, value)
imp._tabScroll[tabKeyOf(imp)] = clamp(value, 0, tabScrollMax(imp)) imp._tabScroll[tabKeyOf(imp)] = clamp(value, 0, tabScrollMax(imp))
end end
local function modListWantsWheel(imp, wheel)
if imp.tab ~= "mods" or (imp._modScrollMax or 0) <= 0 then return false end
if not inRect(imp._modListRect, Kit.mouseX, Kit.mouseY) then return false end
if not inRect(imp._tabRegionRect, Kit.mouseX, Kit.mouseY) then return false end
local at = clamp(imp.modScroll or 0, 0, imp._modScrollMax)
if wheel < 0 then return at < imp._modScrollMax end
return at > 0
end
-- ------------------------------------------------------------- lifecycle -- ------------------------------------------------------------- lifecycle
local function ensureState(imp) local function ensureState(imp)
@@ -192,9 +180,6 @@ function LauncherView.touchpressed(imp, id, x, y)
imp._touchAt = imp._touchAt or {} imp._touchAt = imp._touchAt or {}
imp._touchAt[tostring(id)] = { imp._touchAt[tostring(id)] = {
x = x, y = y, x = x, y = y,
modsList = imp.tab == "mods" and (imp._modScrollMax or 0) > 0
and inRect(imp._modListRect, x, y)
and inRect(imp._tabRegionRect, x, y),
region = tabScrollMax(imp) > 0 and inRect(imp._tabRegionRect, x, y), region = tabScrollMax(imp) > 0 and inRect(imp._tabRegionRect, x, y),
} }
end end
@@ -207,19 +192,9 @@ function LauncherView.touchmoved(imp, id, x, y)
if ddx * ddx + ddy * ddy > TAP_SLOP2 then if ddx * ddx + ddy * ddy > TAP_SLOP2 then
start.dragged = true start.dragged = true
end end
-- A drag that began in the installed-mod viewport scrolls that page's
-- rows. Its pager remains available for moving to the next ten-plus
-- entries; a drag elsewhere keeps the normal short-window page scroll.
if start.dragged then if start.dragged then
local last = start.lastY or start.y local last = start.lastY or start.y
local move = -(y - last) local move = -(y - last)
if start.modsList then
local listMax = imp._modScrollMax or 0
local at, leftover = Kit.scrollHandoff(
clamp(imp.modScroll or 0, 0, listMax), listMax, move)
imp.modScroll = at
move = leftover
end
if move ~= 0 and start.region then if move ~= 0 and start.region then
local at, leftover = Kit.scrollHandoff(tabScrollAt(imp), local at, leftover = Kit.scrollHandoff(tabScrollAt(imp),
tabScrollMax(imp), move) tabScrollMax(imp), move)
@@ -343,7 +318,7 @@ end
local CART_COLOR = { local CART_COLOR = {
red = PAL.railRed, blue = PAL.railBlue, yellow = PAL.railGold, red = PAL.railRed, blue = PAL.railBlue, yellow = PAL.railGold,
gold = PAL.railAmber, gold = PAL.railAmber, silver = PAL.railSilver,
} }
local function cartColor(version) local function cartColor(version)
return CART_COLOR[version] or PAL.green return CART_COLOR[version] or PAL.green
@@ -942,6 +917,8 @@ local GAME_TABS = {
label = "Yellow" }, label = "Yellow" },
{ id = "gold", key = "tab-gold", letter = "G", color = PAL.railAmber, { id = "gold", key = "tab-gold", letter = "G", color = PAL.railAmber,
label = "Gold" }, label = "Gold" },
{ id = "silver", key = "tab-silver", letter = "S", color = PAL.railSilver,
label = "Silver" },
} }
local HEADER_TABS = { local HEADER_TABS = {
@@ -1108,16 +1085,11 @@ local function buildHeader(imp, m)
local tabGap = math.floor(6 * m.s) local tabGap = math.floor(6 * m.s)
local tabRowGap = math.floor(4 * m.s) local tabRowGap = math.floor(4 * m.s)
-- the cartridge dropdown, sized to its longest label so switching games -- the cartridge dropdown: just the game's initial and the caret; the
-- never reflows the row -- popup list carries the full names
local chrome0 = headerChrome(imp) local chrome0 = headerChrome(imp)
local game = currentGame(imp) local game = currentGame(imp)
local labelW = 0 local dropW = math.min(tabRight - tabLeft, tabH + math.floor(24 * m.s))
for _, g in ipairs(GAME_TABS) do
labelW = math.max(labelW, Kit.textWidth("tab", Strings(g.label)))
end
local dropW = math.min(tabRight - tabLeft,
tabH + labelW + math.floor(34 * m.s))
chrome0.game.color = game.color chrome0.game.color = game.color
chrome0.game.letter = game.letter chrome0.game.letter = game.letter
chrome0.game.active = imp.tab == game.id chrome0.game.active = imp.tab == game.id
@@ -1127,7 +1099,7 @@ local function buildHeader(imp, m)
-- flip with it or it vanishes into the cartridge colour -- flip with it or it vanishes into the cartridge colour
local gameInvert = chrome0.game.active or gameHot local gameInvert = chrome0.game.active or gameHot
chrome0.game.ring = gameHot and not chrome0.game.active or nil chrome0.game.ring = gameHot and not chrome0.game.active or nil
btn(imp, tx, ty, dropW, tabH, "tab-game", Strings(game.label), chrome0.game) btn(imp, tx, ty, dropW, tabH, "tab-game", "", chrome0.game)
do do
local cw = math.floor(7 * m.s) local cw = math.floor(7 * m.s)
local ccx = tx + dropW - math.floor(14 * m.s) local ccx = tx + dropW - math.floor(14 * m.s)
@@ -1958,7 +1930,6 @@ local function buildModsPanel(imp, x, y, w, availH, m)
cy = cy + buildModScopeRow(imp, x, cy, w, m) cy = cy + buildModScopeRow(imp, x, cy, w, m)
if #mods == 0 then if #mods == 0 then
imp.modScroll, imp._modScrollMax, imp._modListRect = 0, 0, nil
Kit.emptyBox(x, cy, w, math.floor(110 * m.s), imp:_modsEmptyHint()) Kit.emptyBox(x, cy, w, math.floor(110 * m.s), imp:_modsEmptyHint())
return (cy - y) + math.floor(110 * m.s) return (cy - y) + math.floor(110 * m.s)
end end
@@ -1999,159 +1970,139 @@ local function buildModsPanel(imp, x, y, w, availH, m)
end end
-- A mod row is a fixed height: its details first, then a dedicated second -- A mod row is a fixed height: its details first, then a dedicated second
-- line of per-game checkboxes. Fixed because a page of uniform rows is -- line of per-game checkboxes. Fixed row heights are what make the
-- what lets perPage come from the viewport. -- cull below plain arithmetic.
local togH = math.floor(26 * m.s) local togH = math.floor(26 * m.s)
local gamesLabel = Strings("Enable for:") local gamesLabel = Strings("Enable for:")
local textH = Kit.textHeight("button") + math.floor(4 * m.s) local textH = Kit.textHeight("button") + math.floor(4 * m.s)
+ Kit.textHeight("small") + math.floor(2 * m.s) + Kit.textHeight("small") + Kit.textHeight("small") + math.floor(2 * m.s) + Kit.textHeight("small")
local rowH = math.floor(8 * m.s) + textH + math.floor(8 * m.s) + togH local rowH = math.floor(8 * m.s) + textH + math.floor(8 * m.s) + togH
+ math.floor(8 * m.s) + math.floor(8 * m.s)
local pagerH = math.max(Kit.tapMin(), math.floor(30 * m.s))
local listH = availH - (cy - y) - pagerH - gap
local perPage = Kit.rowsThatFit(listH, rowH, gap, MIN_MODS_PER_PAGE, 20)
local first, last, cur, pages = Kit.pageBounds(page(imp, "mods"), #mods, perPage)
setPage(imp, "mods", cur)
local listTop = cy local listTop = cy
local shown = math.max(0, last - first + 1)
local contentH = shown * rowH + math.max(0, shown - 1) * gap
local scrollMax = math.max(0, contentH - listH)
local scroll = clamp(imp.modScroll or 0, 0, scrollMax)
local lr = imp._modListRect
if not lr then lr = {}; imp._modListRect = lr end
lr.x, lr.y, lr.w, lr.h = x, listTop, w, listH
imp._modScrollMax = scrollMax
if scrollMax > 0 and (Kit.wheelY or 0) ~= 0 and not Kit.blockClicks
and Kit.hit(x, listTop, w, listH) then
scroll = clamp(scroll - Kit.wheelY * math.floor(48 * m.s), 0, scrollMax)
Kit.wheelY = 0
elseif scrollMax == 0 then
local wheelPage = Kit.wheelPage(x, listTop, w, listH, cur, #mods, perPage)
if wheelPage ~= cur then imp.modScroll = 0 end
setPage(imp, "mods", wheelPage)
end
imp.modScroll = scroll
Kit.pushClip(x, listTop, w, listH) -- One continuous list: every row is laid out, the region scroll moves
for i = first, last do -- through all of it, and only rows inside the region's viewport draw --
-- so the per-frame cost stays bounded by the window, not the list.
local view = imp._tabRegionRect
local viewTop = view and view.y or listTop
local viewBot = view and (view.y + view.h) or (listTop + availH)
for i = 1, #mods do
local mod = mods[i] local mod = mods[i]
local ry = listTop + (i - first) * (rowH + gap) - scroll local ry = listTop + (i - 1) * (rowH + gap)
local rowKey = rowKeyFor(imp, "mod-row-", mod.id) if ry + rowH >= viewTop and ry <= viewBot then
local isFullyDisabled = true local rowKey = rowKeyFor(imp, "mod-row-", mod.id)
if mod.enabledByVersion then local isFullyDisabled = true
for _, on in pairs(mod.enabledByVersion) do if mod.enabledByVersion then
if on then isFullyDisabled = false; break end for _, on in pairs(mod.enabledByVersion) do
if on then isFullyDisabled = false; break end
end
else
isFullyDisabled = not mod.enabled
end end
else
isFullyDisabled = not mod.enabled
end
local focused = Kit.focusable(rowKey, x, ry, w, rowH) local focused = Kit.focusable(rowKey, x, ry, w, rowH)
local hot = focused or Kit.hover(x, ry, w, rowH) local hot = focused or Kit.hover(x, ry, w, rowH)
if isFullyDisabled then if isFullyDisabled then
Kit.card(x, ry, w, rowH, hot and "mutedHot" or "muted") Kit.card(x, ry, w, rowH, hot and "mutedHot" or "muted")
else else
Kit.card(x, ry, w, rowH, hot) Kit.card(x, ry, w, rowH, hot)
end
local pad = math.floor(12 * m.s)
local px, inner = x + pad, w - 2 * pad
local ly = ry + math.floor(10 * m.s)
local togGap = math.floor(5 * m.s) + 1
local info = mod.github and mod.github ~= "" and imp:_modUpdateInfo(mod.id)
-- These answer separate games, not a single shared install flag. The
-- importer receives the game id so an experimental confirmation also
-- applies only to the checkbox the player pressed.
local flipped = false
local gamesY = ry + math.floor(8 * m.s) + textH + math.floor(8 * m.s)
Kit.text("micro", gamesLabel, px,
gamesY + (togH - Kit.textHeight("micro")) / 2, PAL.muted)
local tx = px + Kit.textWidth("micro", gamesLabel) + math.floor(10 * m.s)
for _, game in ipairs(GameVersion.ORDER) do
local togKey = "mod-toggle-" .. mod.id .. "-" .. game
if modGameCheckbox(tx, gamesY, togH,
mod.enabledByVersion and mod.enabledByVersion[game] == true,
game, togKey, not safeMode) then
local version = game
queueAction(imp, togKey, function() imp:_toggleMod(mod.id, nil, version) end)
flipped = true
end end
tx = tx + togH + togGap local pad = math.floor(12 * m.s)
end local px, inner = x + pad, w - 2 * pad
-- The checkboxes sit inside the row's rect, so their press also passes the local ly = ry + math.floor(10 * m.s)
-- row hit test; `flipped` gates the row action to everywhere else.
if not flipped
and (Kit.press(x, ry, w, rowH) or Kit._activateId == rowKey) then
local id = mod.id
queueAction(imp, rowKey, function() imp._modActions = id end)
end
local textW = inner
local badgeW = Kit.textWidth("micro", mod.badge) + math.floor(12 * m.s) local togGap = math.floor(5 * m.s) + 1
-- the games the mod is for, beside its category: the same chip the local info = mod.github and mod.github ~= "" and imp:_modUpdateInfo(mod.id)
-- in-game manager shows (src/mods/ModTargets.lua)
local gamesW = mod.targets
and Kit.textWidth("micro", mod.targets) + math.floor(12 * m.s) or 0
local nameShown = Kit.ellipsize("button", mod.name,
textW - badgeW - gamesW - math.floor(12 * m.s))
local headingCol = isFullyDisabled and PAL.muted or PAL.heading
Kit.text("button", nameShown, px, ly, headingCol)
local tagX = px + Kit.textWidth("button", nameShown) + math.floor(8 * m.s)
Kit.tag(tagX, ly, badgeW, Kit.textHeight("button"), mod.badge,
mod.experimental and PAL.yellow or PAL.muted)
if mod.targets then
Kit.tag(tagX + badgeW + math.floor(4 * m.s), ly, gamesW,
Kit.textHeight("button"), mod.targets,
mod.targetsHere == false and PAL.steel or PAL.blue)
end
ly = ly + Kit.textHeight("button") + math.floor(4 * m.s)
-- version + status + update state -- These answer separate games, not a single shared install flag. The
local statusText, statusCol = modStatusColor(mod.status) -- importer receives the game id so an experimental confirmation also
local line = "v" .. tostring(mod.version or "?") .. " " .. statusText -- applies only to the checkbox the player pressed.
Kit.text("small", line, px, ly, statusCol) local flipped = false
local lx = px + Kit.textWidth("small", line) + math.floor(12 * m.s) local gamesY = ry + math.floor(8 * m.s) + textH + math.floor(8 * m.s)
if imp:_modInfoPending(mod.id) then Kit.text("micro", gamesLabel, px,
-- An inline spinner, because this row's release check is genuinely in gamesY + (togH - Kit.textHeight("micro")) / 2, PAL.muted)
-- flight -- the list stays usable while it resolves. local tx = px + Kit.textWidth("micro", gamesLabel) + math.floor(10 * m.s)
Loader.dot(lx, ly, Kit.textHeight("small")) for _, game in ipairs(GameVersion.ORDER) do
Kit.text("small", Strings("Checking..."), local togKey = "mod-toggle-" .. mod.id .. "-" .. game
lx + Kit.textHeight("small") + math.floor(6 * m.s), ly, PAL.muted) if modGameCheckbox(tx, gamesY, togH,
elseif info and info.status == "available" then mod.enabledByVersion and mod.enabledByVersion[game] == true,
Kit.text("small", Strings("v%s available", tostring(info.latest)), game, togKey, not safeMode) then
lx, ly, PAL.yellow) local version = game
elseif info and info.status == "current" then queueAction(imp, togKey, function() imp:_toggleMod(mod.id, nil, version) end)
Kit.text("small", Strings("up to date"), lx, ly, PAL.muted) flipped = true
elseif info and info.status == "error" then end
Kit.text("small", Strings("check failed"), lx, ly, PAL.red) tx = tx + togH + togGap
end end
ly = ly + Kit.textHeight("small") + math.floor(2 * m.s) -- The checkboxes sit inside the row's rect, so their press also passes the
-- row hit test; `flipped` gates the row action to everywhere else.
-- one line of description, or the download stats when we have them if not flipped
-- (download count in green so popularity reads at a glance) and (Kit.press(x, ry, w, rowH) or Kit._activateId == rowKey) then
if info and info.downloads then local id = mod.id
local d = info.dates queueAction(imp, rowKey, function() imp._modActions = id end)
local dl = ModUpdate.downloadsLine(info.downloads.total) end
local dates = ModUpdate.datesLine(d and d.first, d and d.latest) local textW = inner
local segs = {}
if dl then segs[#segs + 1] = { dl, PAL.green } end local badgeW = Kit.textWidth("micro", mod.badge) + math.floor(12 * m.s)
if dates then -- the games the mod is for, beside its category: the same chip the
segs[#segs + 1] = { (dl and " - " or "") .. dates, PAL.detail } -- in-game manager shows (src/mods/ModTargets.lua)
local gamesW = mod.targets
and Kit.textWidth("micro", mod.targets) + math.floor(12 * m.s) or 0
local nameShown = Kit.ellipsize("button", mod.name,
textW - badgeW - gamesW - math.floor(12 * m.s))
local headingCol = isFullyDisabled and PAL.muted or PAL.heading
Kit.text("button", nameShown, px, ly, headingCol)
local tagX = px + Kit.textWidth("button", nameShown) + math.floor(8 * m.s)
Kit.tag(tagX, ly, badgeW, Kit.textHeight("button"), mod.badge,
mod.experimental and PAL.yellow or PAL.muted)
if mod.targets then
Kit.tag(tagX + badgeW + math.floor(4 * m.s), ly, gamesW,
Kit.textHeight("button"), mod.targets,
mod.targetsHere == false and PAL.steel or PAL.blue)
end
ly = ly + Kit.textHeight("button") + math.floor(4 * m.s)
-- version + status + update state
local statusText, statusCol = modStatusColor(mod.status)
local line = "v" .. tostring(mod.version or "?") .. " " .. statusText
Kit.text("small", line, px, ly, statusCol)
local lx = px + Kit.textWidth("small", line) + math.floor(12 * m.s)
if imp:_modInfoPending(mod.id) then
-- An inline spinner, because this row's release check is genuinely in
-- flight -- the list stays usable while it resolves.
Loader.dot(lx, ly, Kit.textHeight("small"))
Kit.text("small", Strings("Checking..."),
lx + Kit.textHeight("small") + math.floor(6 * m.s), ly, PAL.muted)
elseif info and info.status == "available" then
Kit.text("small", Strings("v%s available", tostring(info.latest)),
lx, ly, PAL.yellow)
elseif info and info.status == "current" then
Kit.text("small", Strings("up to date"), lx, ly, PAL.muted)
elseif info and info.status == "error" then
Kit.text("small", Strings("check failed"), lx, ly, PAL.red)
end
ly = ly + Kit.textHeight("small") + math.floor(2 * m.s)
-- one line of description, or the download stats when we have them
-- (download count in green so popularity reads at a glance)
if info and info.downloads then
local d = info.dates
local dl = ModUpdate.downloadsLine(info.downloads.total)
local dates = ModUpdate.datesLine(d and d.first, d and d.latest)
local segs = {}
if dl then segs[#segs + 1] = { dl, PAL.green } end
if dates then
segs[#segs + 1] = { (dl and " - " or "") .. dates, PAL.detail }
end
segLine("small", segs, px, ly, textW)
elseif (mod.description or "") ~= "" then
Kit.text("small", Kit.ellipsize("small", mod.description, textW),
px, ly, PAL.detail)
end end
segLine("small", segs, px, ly, textW)
elseif (mod.description or "") ~= "" then
Kit.text("small", Kit.ellipsize("small", mod.description, textW),
px, ly, PAL.detail)
end end
end end
Kit.popClip()
local pagerY = listTop + listH + gap local contentH = #mods * rowH + (#mods - 1) * gap
local newPage, newPagerH = Kit.pager(x, pagerY, w, cur, #mods, perPage, "mods") return (listTop + contentH + gap) - y
if newPage ~= cur then imp.modScroll = 0 end
setPage(imp, "mods", newPage)
return pagerY + newPagerH - y
end end
-- ---------------------------------------------------------- find mods panel -- ---------------------------------------------------------- find mods panel
@@ -4782,8 +4733,7 @@ function LauncherView.draw(imp)
Kit.blockClicks = modalUp(imp) Kit.blockClicks = modalUp(imp)
local step = Kit.scrollStep(m.s) local step = Kit.scrollStep(m.s)
local nested = modListWantsWheel(imp, Kit.wheelY or 0) do
if not nested then
local rect = imp._tabRegionRect local rect = imp._tabRegionRect
if rect then if rect then
setTabScroll(imp, (Kit.scrollWheel(tabScrollAt(imp), tabScrollMax(imp), setTabScroll(imp, (Kit.scrollWheel(tabScrollAt(imp), tabScrollMax(imp),
@@ -4791,8 +4741,7 @@ function LauncherView.draw(imp)
end end
end end
local scroll = math.max(0, math.min(imp._pageScroll or 0, scrollMax)) local scroll = math.max(0, math.min(imp._pageScroll or 0, scrollMax))
if scrollMax > 0 and (Kit.wheelY or 0) ~= 0 and not nested if scrollMax > 0 and (Kit.wheelY or 0) ~= 0 and not Kit.blockClicks then
and not Kit.blockClicks then
local moved = math.max(0, math.min(scroll - Kit.wheelY * step, scrollMax)) local moved = math.max(0, math.min(scroll - Kit.wheelY * step, scrollMax))
if moved ~= scroll then if moved ~= scroll then
scroll = moved scroll = moved
@@ -4800,7 +4749,7 @@ function LauncherView.draw(imp)
end end
end end
imp._pageScroll, imp._pageScrollMax = scroll, scrollMax imp._pageScroll, imp._pageScrollMax = scroll, scrollMax
if (Kit.wheelY or 0) ~= 0 and not nested and not Kit.blockClicks if (Kit.wheelY or 0) ~= 0 and not Kit.blockClicks
and tabScrollMax(imp) > 0 then and tabScrollMax(imp) > 0 then
local was = tabScrollAt(imp) local was = tabScrollAt(imp)
local to = Kit.scrollClamp(was - Kit.wheelY * step, tabScrollMax(imp)) local to = Kit.scrollClamp(was - Kit.wheelY * step, tabScrollMax(imp))
+122 -27
View File
@@ -4,6 +4,7 @@
-- (species order IS dex order, pics/tilesets are lz3-compressed rather than -- (species order IS dex order, pics/tilesets are lz3-compressed rather than
-- pkmncompress'd, maps are grouped instead of flat). See docs/gold-phase1.md. -- pkmncompress'd, maps are grouped instead of flat). See docs/gold-phase1.md.
local bit = require("bit") local bit = require("bit")
local GameVersion = require("src.core.GameVersion")
local ImageWriter = require("src.import.ImageWriter") local ImageWriter = require("src.import.ImageWriter")
local LuaWriter = require("src.import.LuaWriter") local LuaWriter = require("src.import.LuaWriter")
local Rom = require("src.import.Rom") local Rom = require("src.import.Rom")
@@ -168,6 +169,10 @@ function RomExtractorGen2.new(romData, manifest, progress)
symbols = manifest.symbols, symbols = manifest.symbols,
progress = progress, progress = progress,
stage = 0, stage = 0,
-- _GOLD / _SILVER: the labels are shared, the data behind a handful of
-- them is not (gfx/misc.asm:9-20 vs :46-57).
edition = GameVersion.forSha1(manifest.romSha1) == "silver"
and "silver" or "gold",
}, RomExtractorGen2) }, RomExtractorGen2)
end end
@@ -1641,22 +1646,40 @@ function RomExtractorGen2:extractTitle()
return 3 return 3
end end
-- pret gfx/title/title_bg_gold.pal / title_fg.pal (5 BG pals, 2 OBJ pals). local silver = self.edition == "silver"
local BG_PALS = {
-- pret gfx/title/title_bg_gold.pal / title_bg_silver.pal (5 BG pals);
-- GSTitleBGPals is the edition-selected include (engine/gfx/color.asm:1234).
local BG_PALS = silver and {
{ { 31, 31, 31 }, { 0, 12, 15 }, { 4, 8, 21 }, { 0, 0, 0 } },
{ { 31, 21, 0 }, { 15, 17, 15 }, { 4, 8, 21 }, { 0, 0, 17 } },
{ { 31, 31, 31 }, { 31, 0, 0 }, { 4, 8, 21 }, { 0, 0, 0 } },
{ { 31, 31, 31 }, { 24, 23, 25 }, { 4, 8, 21 }, { 8, 8, 9 } },
{ { 31, 31, 31 }, { 5, 10, 11 }, { 0, 12, 15 }, { 0, 0, 0 } },
} or {
{ { 31, 31, 31 }, { 18, 23, 31 }, { 15, 20, 31 }, { 0, 0, 0 } }, { { 31, 31, 31 }, { 18, 23, 31 }, { 15, 20, 31 }, { 0, 0, 0 } },
{ { 31, 21, 0 }, { 12, 14, 12 }, { 15, 20, 31 }, { 0, 0, 17 } }, { { 31, 21, 0 }, { 12, 14, 12 }, { 15, 20, 31 }, { 0, 0, 17 } },
{ { 31, 31, 31 }, { 31, 0, 0 }, { 15, 20, 31 }, { 0, 0, 0 } }, { { 31, 31, 31 }, { 31, 0, 0 }, { 15, 20, 31 }, { 0, 0, 0 } },
{ { 31, 31, 31 }, { 29, 25, 0 }, { 15, 20, 31 }, { 17, 10, 1 } }, { { 31, 31, 31 }, { 29, 25, 0 }, { 15, 20, 31 }, { 17, 10, 1 } },
{ { 31, 31, 31 }, { 23, 26, 31 }, { 18, 23, 31 }, { 0, 0, 0 } }, { { 31, 31, 31 }, { 23, 26, 31 }, { 18, 23, 31 }, { 0, 0, 0 } },
} }
-- title_fg.pal: pal 0 = Ho-Oh silhouette (shades 1-3 are the same brown); -- title_fg.pal, shared (GSTitleOBPals, engine/gfx/color.asm:1241): pal 0 =
-- pal 1 = gold trail sparks (OAM_PAL1 on GSTitleTrail). -- Ho-Oh silhouette; pal 1 = gold trail sparks (OAM_PAL1 on GSTitleTrail).
local OBJ_HOOH = { local OBJ_HOOH = {
{ 31, 31, 31 }, { 7, 6, 3 }, { 7, 6, 3 }, { 7, 6, 3 }, { 31, 31, 31 }, { 7, 6, 3 }, { 7, 6, 3 }, { 7, 6, 3 },
} }
local OBJ_TRAIL = { local OBJ_TRAIL = {
{ 31, 31, 31 }, { 31, 31, 0 }, { 26, 22, 0 }, { 0, 0, 0 }, { 31, 31, 31 }, { 31, 31, 0 }, { 26, 22, 0 }, { 0, 0, 0 },
} }
-- engine/movie/title.asm:134-141 + CopyPals (home/palettes.asm:190):
-- DmgToCgbObjPal0 %11100000 makes Silver's OBJ pal 0 {c0, c0, c2, c3}.
if silver then
OBJ_HOOH = {
OBJ_HOOH[1], OBJ_HOOH[1], OBJ_HOOH[3], OBJ_HOOH[4],
}
-- .OAMData_GSTitleTrail is attribute 0, not OAM_PAL1 (oam.asm:834-837).
OBJ_TRAIL = OBJ_HOOH
end
local function palColor(pal, shade) local function palColor(pal, shade)
local c = pal[shade + 1] or pal[4] local c = pal[shade + 1] or pal[4]
@@ -1671,8 +1694,9 @@ function RomExtractorGen2:extractTitle()
-- solid BLACK silhouette on a monochrome screen rather than the shaded pose -- solid BLACK silhouette on a monochrome screen rather than the shaded pose
-- a straight decode gives. rOBP1 (%11111000) carries the gold trail. -- a straight decode gives. rOBP1 (%11111000) carries the gold trail.
local DMG_BGP = { 0, 2, 1, 3 } local DMG_BGP = { 0, 2, 1, 3 }
local DMG_OBP0 = { 3, 3, 3, 3 } -- engine/movie/title.asm:105-115: Silver writes %11110000 to both OBPs.
local DMG_OBP1 = { 0, 2, 3, 3 } local DMG_OBP0 = silver and { 0, 0, 3, 3 } or { 3, 3, 3, 3 }
local DMG_OBP1 = silver and { 0, 0, 3, 3 } or { 0, 2, 3, 3 }
-- ImageWriter's four hardware shades, by shade number. -- ImageWriter's four hardware shades, by shade number.
local DMG_SHADE = { 1, 2 / 3, 1 / 3, 0 } local DMG_SHADE = { 1, 2 / 3, 1 / 3, 0 }
@@ -1776,6 +1800,28 @@ function RomExtractorGen2:extractTitle()
-- Ho-Oh frames from OAMData_GSIntroHoOh1..5 (data/sprite_anims/oam.asm). -- Ho-Oh frames from OAMData_GSIntroHoOh1..5 (data/sprite_anims/oam.asm).
local hoohTiles = tilesFrom2bpp(self:decompressLz3Symbol("TitleScreenGFX4"), true) local hoohTiles = tilesFrom2bpp(self:decompressLz3Symbol("TitleScreenGFX4"), true)
-- .OAMData_GSIntroLugia1 / 2 (data/sprite_anims/oam.asm:736-773); the
-- spriteanimoam vtile offset is added per frame (core.asm:224-227).
local LUGIA_1 = {
{ -5, -2, 0, 0, 0x00 }, { -5, 0, 0, 0, 0x02 },
{ -4, -2, 0, 0, 0x04 }, { -4, 0, 0, 0, 0x06 },
{ -3, -1, 0, 0, 0x08 }, { -2, -1, 0, 0, 0x0a },
{ -1, -2, 0, 0, 0x0c }, { -1, 0, 0, 0, 0x0e },
{ 0, -2, 0, 0, 0x10 }, { 0, 0, 0, 0, 0x12 },
{ 1, -2, 0, 0, 0x14 }, { 1, 0, 0, 0, 0x16 },
{ 2, -2, 0, 0, 0x18 }, { 2, 0, 0, 0, 0x1a },
{ 3, -1, 0, 0, 0x1c }, { 4, -1, 0, 0, 0x1e },
}
local LUGIA_2 = {
{ -5, -2, 0, 0, 0x00 }, { -5, 0, 0, 0, 0x02 },
{ -4, -2, 0, 0, 0x04 }, { -4, 0, 0, 0, 0x06 },
{ -3, -1, 0, 0, 0x08 }, { -2, -1, 0, 0, 0x0a },
{ -1, -2, 0, 0, 0x0c }, { -1, 0, 0, 0, 0x0e },
{ 0, -2, 0, 0, 0x10 }, { 0, 0, 0, 0, 0x12 },
{ 1, -2, 0, 0, 0x14 }, { 1, 0, 0, 0, 0x16 },
{ 2, -2, 0, 0, 0x18 }, { 2, 0, 0, 0, 0x1a },
{ 3, -2, 0, 0, 0x1c }, { 4, -2, 0, 0, 0x1e },
}
local HOOH_FRAMES = { local HOOH_FRAMES = {
{ -- 1 { -- 1
{ -4, -1, 0, 0, 0x00 }, { -3, -2, 0, 0, 0x02 }, { -3, 0, 0, 0, 0x04 }, { -4, -1, 0, 0, 0x00 }, { -3, -2, 0, 0, 0x02 }, { -3, 0, 0, 0, 0x04 },
@@ -1823,22 +1869,36 @@ function RomExtractorGen2:extractTitle()
{ 3, -2, 0, 0, 0x22 }, { 3, 0, 0, 0, 0x24 }, { 3, -2, 0, 0, 0x22 }, { 3, 0, 0, 0, 0x24 },
}, },
} }
-- Frameset_GSIntroHoOhLugia (Gold): 1,2,3,4,3,5 with these durations. -- Silver's five oamsets, as {layout, vtile base} (oam.asm:103-107).
local HOOH_SEQUENCE = { local LUGIA_FRAMES = {
{ LUGIA_1, 0x00 }, { LUGIA_1, 0x20 }, { LUGIA_2, 0x40 },
{ LUGIA_2, 0x60 }, { LUGIA_1, 0x00 },
}
-- Frameset_GSIntroHoOhLugia (data/sprite_anims/framesets.asm:376-396):
-- Gold 1,2,3,4,3,5; Silver 2,1,2,3,3,4,4,3,2 on a faster clock.
local HOOH_SEQUENCE = silver and {
{ 2, 3 }, { 1, 7 }, { 2, 7 }, { 3, 7 }, { 3, 7 },
{ 4, 7 }, { 4, 7 }, { 3, 7 }, { 2, 3 },
} or {
{ 1, 10 }, { 2, 9 }, { 3, 10 }, { 4, 10 }, { 3, 9 }, { 5, 10 }, { 1, 10 }, { 2, 9 }, { 3, 10 }, { 4, 10 }, { 3, 9 }, { 5, 10 },
} }
local hoohPaths, hoohGrayPaths = {}, {} local hoohPaths, hoohGrayPaths = {}, {}
local originX, originY = 32, 24 -- Lugia1/2 span x tiles -5..4, four tiles wider than Ho-Oh's -4..3.
for fi, oam in ipairs(HOOH_FRAMES) do local originX, originY = silver and 40 or 32, 24
local poseW = silver and 80 or 64
local frames = silver and LUGIA_FRAMES or HOOH_FRAMES
for fi, entry in ipairs(frames) do
local oam = silver and entry[1] or entry
local base = silver and entry[2] or 0
-- The pose starts EMPTY, not white: an OBJ's colour 0 is transparent -- The pose starts EMPTY, not white: an OBJ's colour 0 is transparent
-- wherever it falls, so a gap enclosed by the bird shows the sky through -- wherever it falls, so a gap enclosed by the bird shows the sky through
-- exactly like one outside it, and there is no matte to flood-fill. -- exactly like one outside it, and there is no matte to flood-fill.
local pose = ImageWriter.blank(64, 64, 0, 0, 0, 0) local pose = ImageWriter.blank(poseW, 64, 0, 0, 0, 0)
for _, spr in ipairs(oam) do for _, spr in ipairs(oam) do
local px = originX + spr[1] * 8 + spr[3] local px = originX + spr[1] * 8 + spr[3]
local py = originY + spr[2] * 8 + spr[4] local py = originY + spr[2] * 8 + spr[4]
blitSprite(pose, hoohTiles[spr[5] + 1], px, py) blitSprite(pose, hoohTiles[base + spr[5] + 1], px, py)
blitSprite(pose, hoohTiles[spr[5] + 2], px, py + 8) blitSprite(pose, hoohTiles[base + spr[5] + 2], px, py + 8)
end end
local tinted = colorize(pose, function() return OBJ_HOOH end) local tinted = colorize(pose, function() return OBJ_HOOH end)
local rel = ("title/hooh_%d.png"):format(fi) local rel = ("title/hooh_%d.png"):format(fi)
@@ -1855,14 +1915,20 @@ function RomExtractorGen2:extractTitle()
end end
self:tick("Title screen", 3, 5) self:tick("Title screen", 3, 5)
-- Trail: TitleScreenGFX3 is raw 2bpp (8 tiles); Gold OAM uses one 8x16 -- Trail: TitleScreenGFX3 is raw 2bpp; Gold's OAM is one 8x16, Silver's two
-- on OAM_PAL1 (gold), not the Ho-Oh silhouette pal. -- side by side, and only 4 of Silver's 8 copied tiles exist (title.asm:43).
local trailSym = self:symbol("TitleScreenGFX3") local trailSym = self:symbol("TitleScreenGFX3")
local trailRaw = self.rom:bytes(trailSym.bank, trailSym.address, 8 * 16) local trailTileCount = silver and 4 or 8
local trailRaw =
self.rom:bytes(trailSym.bank, trailSym.address, trailTileCount * 16)
local trailTiles = tilesFrom2bpp(trailRaw, true) local trailTiles = tilesFrom2bpp(trailRaw, true)
local trail = ImageWriter.blank(8, 16, 0, 0, 0, 0) local trail = ImageWriter.blank(silver and 16 or 8, 16, 0, 0, 0, 0)
blitSprite(trail, trailTiles[1], 0, 0) blitSprite(trail, trailTiles[1], 0, 0)
blitSprite(trail, trailTiles[2], 0, 8) blitSprite(trail, trailTiles[2], 0, 8)
if silver then
blitSprite(trail, trailTiles[3], 8, 0)
blitSprite(trail, trailTiles[4], 8, 8)
end
local trailTint = colorize(trail, function() return OBJ_TRAIL end) local trailTint = colorize(trail, function() return OBJ_TRAIL end)
self:save(trailTint, "title/trail.png") self:save(trailTint, "title/trail.png")
self:save(throughRegister(trail, DMG_OBP1), "title/trail_gray.png") self:save(throughRegister(trail, DMG_OBP1), "title/trail_gray.png")
@@ -1908,8 +1974,11 @@ function RomExtractorGen2:extractTitle()
cloudsGray = "assets/generated/title/clouds_gray.png", cloudsGray = "assets/generated/title/clouds_gray.png",
hoohFramesGray = hoohGrayPaths, hoohFramesGray = hoohGrayPaths,
trailGray = "assets/generated/title/trail_gray.png", trailGray = "assets/generated/title/trail_gray.png",
-- Frameset_GSIntroHoOhLugia (Gold), frame index 1-based + duration frames. -- Frameset_GSIntroHoOhLugia, frame index 1-based + duration frames.
hoohSequence = HOOH_SEQUENCE, hoohSequence = HOOH_SEQUENCE,
-- AnimSeq_GSIntroHoOhLugia (engine/sprite_anims/functions.asm:820-838).
hoohBobAmplitude = silver and 8 or 2,
hoohBobStep = silver and -1 or 1,
-- `depixel 12, 11` (engine/movie/title.asm). Two traps, and the port had -- `depixel 12, 11` (engine/movie/title.asm). Two traps, and the port had
-- fallen into both, which is what put Ho-Oh off-centre: -- fallen into both, which is what put Ho-Oh off-centre:
-- * ldpixel's own comment calls its first tile argument the X one and is -- * ldpixel's own comment calls its first tile argument the X one and is
@@ -1919,19 +1988,45 @@ function RomExtractorGen2:extractTitle()
-- the cursor two rows up on the box screen. So this is x 88, y 96. -- the cursor two rows up on the box screen. So this is x 88, y 96.
-- * those are OAM coordinates, which are biased; a drawn object sits at -- * those are OAM coordinates, which are biased; a drawn object sits at
-- (x - 8, y - 16) on screen. -- (x - 8, y - 16) on screen.
-- The pose canvas holds its own origin at (32, 24), so the sheet's corner -- The pose canvas holds its own origin, so the sheet's corner is
-- is (88 - 8 - 32, 96 - 16 - 24) -- and the bird's 64px width then lands -- (88 - 8 - originX, 96 - 16 - originY) -- and the pose's width then lands
-- centred on the screen, 48 to 112. -- centred on the screen (Ho-Oh 48..112, Lugia 40..120).
hoohX = 48, hoohX = 80 - originX,
hoohY = 56, hoohY = 80 - originY,
trail = "assets/generated/title/trail.png", trail = "assets/generated/title/trail.png",
copyright = "assets/generated/title/copyright.png", copyright = "assets/generated/title/copyright.png",
copyrightSplash = "assets/generated/title/copyright_splash.png", copyrightSplash = "assets/generated/title/copyright_splash.png",
-- ScrollTitleScreenClouds: Gold decrements the cloud-band SCX every -- ScrollTitleScreenClouds (engine/menus/intro_menu.asm:917-928): Gold
-- 8 vblanks, so the strip slides 1px right. Silver does the same -- decrements the cloud-band SCX every 8 vblanks, so the strip slides 1px
-- decrement every frame. -- right. Silver does the same decrement every frame.
cloudScrollEvery = 8, cloudScrollEvery = silver and 1 or 8,
cloudY = 88, cloudY = 88,
-- BG pal 0 colour 2: the sky the widescreen bands have to match.
sky = {
BG_PALS[1][3][1] / 31, BG_PALS[1][3][2] / 31, BG_PALS[1][3][3] / 31,
},
-- The fill under the cloud/wave band: Gold's cloud field is BG pal 0
-- colour 0 (white), Silver's sea floor is colour 3 (black).
below = silver and {
BG_PALS[1][4][1] / 31, BG_PALS[1][4][2] / 31, BG_PALS[1][4][3] / 31,
} or {
BG_PALS[1][1][1] / 31, BG_PALS[1][1][2] / 31, BG_PALS[1][1][3] / 31,
},
-- UpdateTitleTrailSprite (engine/menus/intro_menu.asm:1069-1124). Silver's
-- `depixel 15, 11, 4, 0` is OAM (88, 124), less the bias and the (-16, -8)
-- corner .OAMData_GSTitleTrail draws from.
trailMode = silver and "silver" or "gold",
trailSpawns = silver and { { 72, 100 } } or {
{ 80, 88 }, { 104, 88 }, { 104, 88 }, { 120, 88 },
{ 120, 88 }, { 88, 88 },
},
trailSpawnEvery = 4,
trailStepX = 4,
trailStepY = silver and 0 or 1,
-- AnimSeq_GSTitleTrail (functions.asm:784-813) with wIntroSceneTimer 0.
trailBobAmplitude = silver and 3 or 2,
trailPhaseStep = silver and 7 or 3,
trailPhase = silver and 0 or nil,
} }
self:write("title", data) self:write("title", data)
return data return data
+13 -10
View File
@@ -142,6 +142,8 @@ local VERSION_REQUIRED_FILES_OVERRIDE = {
"assets/generated/audio/programs.bin", "assets/generated/audio/programs.bin",
}, },
} }
-- Same Gen 2 extract, so a Silver cache is complete when the same files exist.
VERSION_REQUIRED_FILES_OVERRIDE.silver = VERSION_REQUIRED_FILES_OVERRIDE.gold
-- "Split-screen ROM selector" first-run palette (matches the FirstRun mockup): -- "Split-screen ROM selector" first-run palette (matches the FirstRun mockup):
-- a dark neon arcade panel, one column per game. -- a dark neon arcade panel, one column per game.
@@ -335,7 +337,7 @@ function RomImporter.syncAndroidShortcuts(activeVersion)
return false return false
end end
local allVersions = { "red", "blue", "yellow", "gold" } local allVersions = GameVersion.ORDER
local ready = {} local ready = {}
local seen = {} local seen = {}
@@ -1360,11 +1362,10 @@ function RomImporter.new(onComplete, opts)
saveNotice = {}, saveNotice = {},
-- MODS panel state (pass 3): mods is the cached LauncherMods.list() array -- MODS panel state (pass 3): mods is the cached LauncherMods.list() array
-- (refreshed lazily on first draw and after any toggle/install/delete); -- (refreshed lazily on first draw and after any toggle/install/delete);
-- modScroll is the current paged list's inner scroll offset (px, clamped -- modNotice is the last install/delete result { ok, text }.
-- in draw); modNotice is the last install/delete result { ok, text }.
-- requiredImportNotice stays inside the imported-files modal so validation -- requiredImportNotice stays inside the imported-files modal so validation
-- failures are visible beside the file picker that caused them. -- failures are visible beside the file picker that caused them.
mods = nil, modScroll = 0, modNotice = nil, requiredImportNotice = nil, mods = nil, modNotice = nil, requiredImportNotice = nil,
-- Which game the MODS panel is answering for (a GameVersion id, nil = -- Which game the MODS panel is answering for (a GameVersion id, nil =
-- every game). Rows resolve their enable-state and their "runs here" -- every game). Rows resolve their enable-state and their "runs here"
-- verdict against it (src/mods/ModTargets.lua). -- verdict against it (src/mods/ModTargets.lua).
@@ -1420,7 +1421,8 @@ function RomImporter.new(onComplete, opts)
self.returning[version] = self.returning[version] =
(not ready) and marker ~= nil and marker ~= markerFor(version) (not ready) and marker ~= nil and marker ~= markerFor(version)
self.romName[version] = "pokemon_" .. info.id self.romName[version] = "pokemon_" .. info.id
.. ((info.id == "yellow" or info.id == "gold") and ".gbc" or ".gb") .. ((info.id == "yellow" or GameVersion.generation(version) == 2)
and ".gbc" or ".gb")
end end
RomImporter.syncAndroidShortcuts() RomImporter.syncAndroidShortcuts()
self:_applyLastVersionTab() self:_applyLastVersionTab()
@@ -1685,7 +1687,7 @@ function RomImporter:startData(data, displayName)
end end
if not isAcceptedRomSize(#data) then if not isAcceptedRomSize(#data) then
self:setError(("Expected a 1 MiB Game Boy ROM (Red/Blue/Yellow) or a " self:setError(("Expected a 1 MiB Game Boy ROM (Red/Blue/Yellow) or a "
.. "2 MiB Game Boy Color ROM (Gold); this file is %.2f MiB.") .. "2 MiB Game Boy Color ROM (Gold/Silver); this file is %.2f MiB.")
:format(#data / 1024 / 1024)) :format(#data / 1024 / 1024))
return return
end end
@@ -1693,7 +1695,8 @@ function RomImporter:startData(data, displayName)
local version = GameVersion.forSha1(actualHash) local version = GameVersion.forSha1(actualHash)
if not version then if not version then
self:setError(("Unsupported ROM (SHA-1 %s). This needs a clean US Pokemon " self:setError(("Unsupported ROM (SHA-1 %s). This needs a clean US Pokemon "
.. "Red, Blue, Yellow, or Gold dump; patched, trimmed or \"fixed\" dumps " .. "Red, Blue, Yellow, Gold, or Silver dump; patched, trimmed or "
.. "\"fixed\" dumps "
.. "(tagged [b] or [BF]) never verify."):format(actualHash)) .. "(tagged [b] or [BF]) never verify."):format(actualHash))
return return
end end
@@ -1770,7 +1773,7 @@ function RomImporter:_startExtractCoroutine(version, info, prefix, displayName)
local CacheFs = require("src.import.CacheFs") local CacheFs = require("src.import.CacheFs")
CacheFs.prefix = prefix CacheFs.prefix = prefix
local manifest = require("src.import.RomManifest").decode(version) local manifest = require("src.import.RomManifest").decode(version)
local RomExtractor = version == "gold" local RomExtractor = GameVersion.generation(version) == 2
and require("src.import.RomExtractorGen2") and require("src.import.RomExtractorGen2")
or require("src.import.RomExtractor") or require("src.import.RomExtractor")
local extractor = RomExtractor.new(self.romData, manifest, local extractor = RomExtractor.new(self.romData, manifest,
@@ -2755,7 +2758,8 @@ function RomImporter:resumeAfterOverlay()
end end
function RomImporter:_cycleTab(delta) function RomImporter:_cycleTab(delta)
local order = { "red", "blue", "yellow", "gold", "mods", "find", "skins" } local order = { "red", "blue", "yellow", "gold", "silver",
"mods", "find", "skins" }
local idx = 1 local idx = 1
for i, id in ipairs(order) do for i, id in ipairs(order) do
if id == self.tab then idx = i; break end if id == self.tab then idx = i; break end
@@ -3121,7 +3125,6 @@ function RomImporter:_switchTab(id)
self.tab = id self.tab = id
self._findSearchFocus = false self._findSearchFocus = false
self._skinUrlFocus = false self._skinUrlFocus = false
self._modScrollMax, self._modListRect = 0, nil
self:_disarmTextInput() self:_disarmTextInput()
-- the skins list is cheap and can change behind the launcher's back -- the skins list is cheap and can change behind the launcher's back
-- (an export, a hand-dropped folder), so re-read it on every visit -- (an export, a hand-dropped folder), so re-read it on every visit
+4 -1
View File
@@ -228,7 +228,10 @@ SaveConvert.mergeDefaults = mergeDefaults
-- codec exists yet. Both directions answer with a plain message the -- codec exists yet. Both directions answer with a plain message the
-- launcher's save card renders as-is, instead of pushing a Gen 2 save -- launcher's save card renders as-is, instead of pushing a Gen 2 save
-- table through Gen 1 offsets and surfacing a codec traceback. -- table through Gen 1 offsets and surfacing a codec traceback.
local GEN2_SAV_UNSUPPORTED = { gold = "Pokemon Gold" } local GEN2_SAV_UNSUPPORTED = {
gold = "Pokemon Gold",
silver = "Pokemon Silver",
}
-- importSav(bytes, version, gameVersion) -> saveTable, err -- importSav(bytes, version, gameVersion) -> saveTable, err
-- bytes: the raw 32768-byte SRAM string. Validates size and the main-data -- bytes: the raw 32768-byte SRAM string. Validates size and the main-data
+1 -1
View File
@@ -5,7 +5,7 @@ SyncMods.REV = 1
local function versions() local function versions()
local ok, GameVersion = pcall(require, "src.core.GameVersion") local ok, GameVersion = pcall(require, "src.core.GameVersion")
if ok and GameVersion and GameVersion.ORDER then return GameVersion.ORDER end if ok and GameVersion and GameVersion.ORDER then return GameVersion.ORDER end
return { "red", "blue", "yellow", "gold" } return { "red", "blue", "yellow", "gold", "silver" }
end end
local function defaultDeps() local function defaultDeps()
+8 -3
View File
@@ -73,7 +73,10 @@ function Editor.load(opts)
} }
local optsTbl = SaveData.loadOptions() local optsTbl = SaveData.loadOptions()
local applied = optsTbl local applied = optsTbl
if opts.version == "gold" then local GameVersion = require("src.core.GameVersion")
local gen2 = GameVersion.VERSIONS[opts.version]
and GameVersion.generation(opts.version) == 2
if gen2 then
local gold = type(optsTbl.gold) == "table" and optsTbl.gold or {} local gold = type(optsTbl.gold) == "table" and optsTbl.gold or {}
applied = { applied = {
touchControls = gold.touchControls, touchControls = gold.touchControls,
@@ -151,7 +154,9 @@ local function persist()
skin = cfg.skin, skin = cfg.skin,
layouts = cfg.layouts, layouts = cfg.layouts,
} }
if Editor.version == "gold" then local GameVersion = require("src.core.GameVersion")
if GameVersion.VERSIONS[Editor.version]
and GameVersion.generation(Editor.version) == 2 then
opts.gold = type(opts.gold) == "table" and opts.gold or {} opts.gold = type(opts.gold) == "table" and opts.gold or {}
opts.gold.touchControls = block opts.gold.touchControls = block
else else
@@ -569,7 +574,7 @@ function Editor.new(game)
local state = { game = game, isOpaque = true } local state = { game = game, isOpaque = true }
Editor.hostPoll = true Editor.hostPoll = true
Editor.load({ Editor.load({
version = "gold", version = require("src.core.GameVersion").get(),
hostPoll = true, hostPoll = true,
onClose = function() onClose = function()
Editor.hostPoll = false Editor.hostPoll = false
+12 -5
View File
@@ -189,11 +189,18 @@ defineString("CREDIT_END", "END")
-- STAFF and everything after it is a heading. Anything BELOW this id is a -- STAFF and everything after it is a heading. Anything BELOW this id is a
-- person as far as ParseCredits is concerned. -- person as far as ParseCredits is concerned.
Credits.STAFF = defineString("STAFF", { -- Credits_Staff differs per edition, including the centring spaces
" #MON", -- (data/credits_strings.asm:54-60).
" GOLD VERSION", Credits.STAFF = defineString("STAFF",
" PORT STAFF", require("src.core.GameVersion").get() == "silver" and {
}) " #MON",
" SILVER VERSION",
" PORT STAFF",
} or {
" #MON",
" GOLD VERSION",
" PORT STAFF",
})
defineString("DIRECTOR", " DIRECTOR") defineString("DIRECTOR", " DIRECTOR")
defineString("PROGRAMMING", " PROGRAMMING") defineString("PROGRAMMING", " PROGRAMMING")
defineString("ENGINE_DESIGN", " ENGINE DESIGN") defineString("ENGINE_DESIGN", " ENGINE DESIGN")
+2 -2
View File
@@ -147,8 +147,8 @@ function GoldSilverIntro.new(game, opts)
-- the movie's full 2335 frames over an empty screen and reads exactly -- the movie's full 2335 frames over an empty screen and reads exactly
-- like "the intro does not work". Say so instead: the fix is a -- like "the intro does not work". Say so instead: the fix is a
-- re-import, and nothing else in the boot chain will mention it. -- re-import, and nothing else in the boot chain will mention it.
Logger.warn("gold intro: no intro.lua in the cache -- re-import Gold " Logger.warn("gen2 intro: no intro.lua in the cache -- re-import "
.. "or the movie plays blank") .. "this version or the movie plays blank")
end end
self.images = {} self.images = {}
self.sheets = {} self.sheets = {}
+1 -1
View File
@@ -56,7 +56,7 @@ function MainMenu.new(game, opts)
self.save = opts.save self.save = opts.save
if self.save == nil and opts.hasSave ~= false then if self.save == nil and opts.hasSave ~= false then
local loaded = Save.load("gold") local loaded = Save.load()
self.save = loaded self.save = loaded
end end
self.hasSave = opts.hasSave self.hasSave = opts.hasSave
+12 -4
View File
@@ -34,8 +34,14 @@ local NamePick = {}
NamePick.__index = NamePick NamePick.__index = NamePick
NamePick.isOpaque = true NamePick.isOpaque = true
-- data/player_names.asm PlayerNameArray, Gold's half of the IF. -- data/player_names.asm PlayerNameArray, one half of the IF per edition.
local PRESETS = { "GOLD", "HIRO", "TAYLOR", "KARL" } local PRESETS = { "GOLD", "HIRO", "TAYLOR", "KARL" }
local PRESETS_SILVER = { "SILVER", "KAMON", "OSCAR", "MAX" }
local function presetsFor()
local silver = require("src.core.GameVersion").get() == "silver"
return silver and PRESETS_SILVER or PRESETS
end
-- menu_coords 0, 0, 10, TEXTBOX_Y - 1 (TEXTBOX_Y = 12). -- menu_coords 0, 0, 10, TEXTBOX_Y - 1 (TEXTBOX_Y = 12).
local BOX_X1, BOX_Y1, BOX_X2, BOX_Y2 = 0, 0, 10, 11 local BOX_X1, BOX_Y1, BOX_X2, BOX_Y2 = 0, 0, 10, 11
@@ -60,7 +66,7 @@ function NamePick.new(game, opts)
self.game = game self.game = game
self.onDone = opts.onDone self.onDone = opts.onDone
self.items = { "NEW NAME" } self.items = { "NEW NAME" }
for _, name in ipairs(opts.presets or PRESETS) do for _, name in ipairs(opts.presets or presetsFor()) do
self.items[#self.items + 1] = name self.items[#self.items + 1] = name
end end
-- `db 1 ; default option`: the cursor starts on NEW NAME, not on a preset. -- `db 1 ; default option`: the cursor starts on NEW NAME, not on a preset.
@@ -106,7 +112,7 @@ function NamePick:openNaming()
if name and #name > 0 then if name and #name > 0 then
self:choose(name) self:choose(name)
else else
self:choose(self.items[2] or "GOLD") self:choose(self.items[2] or presetsFor()[1])
end end
end, end,
}) })
@@ -146,7 +152,7 @@ function NamePick:update(_dt)
if self.fontOk then if self.fontOk then
self:openNaming() self:openNaming()
else else
self:choose(self.items[2] or "GOLD") self:choose(self.items[2] or presetsFor()[1])
end end
else else
-- A preset returns through MovePlayerPicLeft, so the pic walks back -- A preset returns through MovePlayerPicLeft, so the pic walks back
@@ -227,5 +233,7 @@ function NamePick:drawWidescreen(winW, winH)
end end
NamePick.PRESETS = PRESETS NamePick.PRESETS = PRESETS
NamePick.PRESETS_SILVER = PRESETS_SILVER
NamePick.presetsFor = presetsFor
return NamePick return NamePick
+4 -4
View File
@@ -72,8 +72,8 @@ end
-- Naming presets are boot config the same way Gen 1 reads them -- Naming presets are boot config the same way Gen 1 reads them
-- (field.boot.namePresets), so a total conversion that replaces the list once -- (field.boot.namePresets), so a total conversion that replaces the list once
-- replaces it for both games; NamePick.PRESETS (data/player_names.asm -- replaces it for both games; NamePick.presetsFor (data/player_names.asm
-- PlayerNameArray) is Gold's fallback. -- PlayerNameArray) is the running edition's fallback.
local function namePresets(game, who, fallback) local function namePresets(game, who, fallback)
local boot = game and game.data and game.data.field local boot = game and game.data and game.data.field
and game.data.field.boot and game.data.field.boot
@@ -351,9 +351,9 @@ function OakSpeech:openNamePick(step)
picColors = self.playerColors, picColors = self.playerColors,
presets = step.presets presets = step.presets
or namePresets(self.game, step.presetsWho or step.who or "player", or namePresets(self.game, step.presetsWho or step.who or "player",
step.presetsFallback or NamePick.PRESETS), step.presetsFallback or NamePick.presetsFor()),
onDone = function(name) onDone = function(name)
name = name or "GOLD" name = name or NamePick.presetsFor()[1]
self.game.save.player.name = name self.game.save.player.name = name
self.game.stack:pop() -- NamePick self.game.stack:pop() -- NamePick
self.busy = false self.busy = false
+1 -1
View File
@@ -181,7 +181,7 @@ function PcMenu:beginChangeBox(index)
self.saveTimer = 0 self.saveTimer = 0
self.saved = nil self.saved = nil
local existed = self.saveExists local existed = self.saveExists
if existed == nil then existed = Save.exists("gold") end if existed == nil then existed = Save.exists() end
self.existed = existed self.existed = existed
end end
+1 -1
View File
@@ -71,7 +71,7 @@ function SaveMenu.new(game, opts)
self.onDone = opts.onDone self.onDone = opts.onDone
self.writer = opts.writer or Save.save self.writer = opts.writer or Save.save
local existed = opts.existed local existed = opts.existed
if existed == nil then existed = Save.exists("gold") end if existed == nil then existed = Save.exists() end
self.existed = existed self.existed = existed
-- confirm -> overwrite (only when a file exists) -> saving -> done -- confirm -> overwrite (only when a file exists) -> saving -> done
self.phase = "confirm" self.phase = "confirm"
+60 -25
View File
@@ -2,6 +2,7 @@
-- wing-flap (Frameset_GSIntroHoOhLugia), spark trails, A/Start to continue. -- wing-flap (Frameset_GSIntroHoOhLugia), spark trails, A/Start to continue.
-- drawWidescreen fills the window with sky/clouds so widescreen has no -- drawWidescreen fills the window with sky/clouds so widescreen has no
-- pillarbox voids; the 160x144 art stays aspect-centered on top. -- pillarbox voids; the 160x144 art stays aspect-centered on top.
-- Every Gold/Silver difference arrives as a title.lua key, defaulted to Gold.
-- src/render/Assets.lua is the mod-override choke point: a raw -- src/render/Assets.lua is the mod-override choke point: a raw
-- love.graphics.newImage skips overrides/ and AssetTransform output. -- love.graphics.newImage skips overrides/ and AssetTransform output.
@@ -16,7 +17,7 @@ local TitleState = {}
TitleState.__index = TitleState TitleState.__index = TitleState
TitleState.isOpaque = true TitleState.isOpaque = true
-- title_bg_gold.pal mid-sky shade (sampled from composed title_screen.png). -- title_bg_gold.pal mid-sky shade, for a cache built before title.sky existed.
local SKY = { 123 / 255, 165 / 255, 255 / 255, 1 } local SKY = { 123 / 255, 165 / 255, 255 / 255, 1 }
-- ...and its grey stand-in, for when COLOR is not GBC. The title art is the -- ...and its grey stand-in, for when COLOR is not GBC. The title art is the
-- one thing in the port baked with its colours in (see the extractor), so the -- one thing in the port baked with its colours in (see the extractor), so the
@@ -61,6 +62,15 @@ function TitleState.new(game, opts)
self.hoohY = tonumber(title.hoohY) or 56 self.hoohY = tonumber(title.hoohY) or 56
self.cloudY = tonumber(title.cloudY) or 88 self.cloudY = tonumber(title.cloudY) or 88
self.cloudScrollEvery = tonumber(title.cloudScrollEvery) or 8 self.cloudScrollEvery = tonumber(title.cloudScrollEvery) or 8
-- AnimSeq_GSIntroHoOhLugia (engine/sprite_anims/functions.asm:820-838).
self.hoohBobAmplitude = tonumber(title.hoohBobAmplitude) or 2
self.hoohBobStep = tonumber(title.hoohBobStep) or 1
local sky = title.sky
self.sky = (type(sky) == "table" and #sky >= 3)
and { sky[1], sky[2], sky[3], 1 } or SKY
local below = title.below
self.below = (type(below) == "table" and #below >= 3)
and { below[1], below[2], below[3], 1 } or { 1, 1, 1, 1 }
self.hoohColor, self.hoohGray = {}, {} self.hoohColor, self.hoohGray = {}, {}
local paths = title.hoohFrames local paths = title.hoohFrames
@@ -80,8 +90,12 @@ function TitleState.new(game, opts)
self.sequence = title.hoohSequence or { self.sequence = title.hoohSequence or {
{ 1, 10 }, { 2, 9 }, { 3, 10 }, { 4, 10 }, { 3, 9 }, { 5, 10 }, { 1, 10 }, { 2, 9 }, { 3, 10 }, { 4, 10 }, { 3, 9 }, { 5, 10 },
} }
-- A frame shows duration + 1 ticks: GetSpriteAnimFrame stores the byte on
-- the advancing tick and only decrements on the ones after
-- (engine/sprite_anims/core.asm:400-434). Both editions' framesets total
-- 64 ticks with it, locking the wing beat to the 64-tick sine bob.
self.seqIndex = 1 self.seqIndex = 1
self.seqLeft = self.sequence[1] and self.sequence[1][2] or 10 self.seqLeft = (self.sequence[1] and self.sequence[1][2] or 10) + 1
self.frame = 1 self.frame = 1
-- AnimSeq_GSIntroHoOhLugia's SPRITEANIMSTRUCT_VAR1. -- AnimSeq_GSIntroHoOhLugia's SPRITEANIMSTRUCT_VAR1.
@@ -89,12 +103,21 @@ function TitleState.new(game, opts)
self.frameCounter = 0 self.frameCounter = 0
self.cloudScroll = 0 self.cloudScroll = 0
self.trails = {} self.trails = {}
-- UpdateTitleTrailSprite / TitleTrailCoords (intro_menu.asm), in pixels. -- UpdateTitleTrailSprite / TitleTrailCoords (intro_menu.asm:1069-1124), in
self.trailSpawns = { -- pixels.
self.trailSpawns = title.trailSpawns or {
{ 80, 88 }, { 104, 88 }, { 104, 88 }, { 120, 88 }, { 80, 88 }, { 104, 88 }, { 104, 88 }, { 120, 88 },
{ 120, 88 }, { 88, 88 }, { 120, 88 }, { 88, 88 },
} }
self.trailSpawnIndex = 1 self.trailSpawnIndex = 1
-- AnimSeq_GSTitleTrail (engine/sprite_anims/functions.asm:720-818).
self.trailMode = title.trailMode or "gold"
self.trailSpawnEvery = tonumber(title.trailSpawnEvery) or 4
self.trailStepX = tonumber(title.trailStepX) or 4
self.trailStepY = tonumber(title.trailStepY) or 1
self.trailBobAmplitude = tonumber(title.trailBobAmplitude) or 2
self.trailPhaseStep = tonumber(title.trailPhaseStep) or 3
self.trailPhase = tonumber(title.trailPhase)
-- How far past the 160px frame trails may fly (GB pixels); set each draw. -- How far past the 160px frame trails may fly (GB pixels); set each draw.
self.trailMaxX = 200 self.trailMaxX = 200
self.musicStarted = false self.musicStarted = false
@@ -119,47 +142,56 @@ function TitleState:enter()
end end
end end
-- AnimSeq_GSIntroHoOhLugia (engine/sprite_anims/functions.asm): VAR1 counts up -- Sprites_Sine hands back the byte the ASM leaves in a, so the down half of
-- one per frame and the struct's Y offset becomes `d * sin(VAR1 * pi/32)` with -- the wave arrives in two's complement and is a signed pixel delta here.
-- d = 2 on Gold (Silver counts DOWN with d = 8). Sprites_Sine hands back the local function signed(value)
-- byte the ASM leaves in a, so the down half of the wave arrives in two's if value >= 0x80 then return value - 0x100 end
-- complement and has to be read as a signed pixel delta here.
function TitleState:hoohBob()
local value = SpriteAnims.sine(self.hoohPhase, 2)
if value >= 0x80 then value = value - 0x100 end
return value return value
end end
-- AnimSeq_GSIntroHoOhLugia (engine/sprite_anims/functions.asm:820-838).
function TitleState:hoohBob()
return signed(SpriteAnims.sine(self.hoohPhase, self.hoohBobAmplitude))
end
function TitleState:advanceHooh() function TitleState:advanceHooh()
self.hoohPhase = (self.hoohPhase + 1) % 256 self.hoohPhase = (self.hoohPhase + self.hoohBobStep) % 256
self.seqLeft = self.seqLeft - 1 self.seqLeft = self.seqLeft - 1
if self.seqLeft > 0 then return end if self.seqLeft > 0 then return end
self.seqIndex = self.seqIndex + 1 self.seqIndex = self.seqIndex + 1
if self.seqIndex > #self.sequence then self.seqIndex = 1 end if self.seqIndex > #self.sequence then self.seqIndex = 1 end
local step = self.sequence[self.seqIndex] local step = self.sequence[self.seqIndex]
self.frame = step[1] self.frame = step[1]
self.seqLeft = step[2] self.seqLeft = step[2] + 1
end end
function TitleState:spawnTrail() function TitleState:spawnTrail()
if not (self.trailColor or self.trailGray) then return end if not (self.trailColor or self.trailGray) then return end
if self.frameCounter % 4 ~= 0 then return end if #self.trailSpawns == 0 then return end
if self.frameCounter % self.trailSpawnEvery ~= 0 then return end
local spawn = self.trailSpawns[self.trailSpawnIndex] local spawn = self.trailSpawns[self.trailSpawnIndex]
self.trailSpawnIndex = self.trailSpawnIndex % #self.trailSpawns + 1 self.trailSpawnIndex = self.trailSpawnIndex % #self.trailSpawns + 1
if not spawn then return end if not spawn then return end
self.trails[#self.trails + 1] = { self.trails[#self.trails + 1] = {
x = spawn[1], y = spawn[2], phase = love.math.random(0, 255), x = spawn[1], y = spawn[2],
phase = self.trailPhase or love.math.random(0, 255),
} }
end end
function TitleState:stepTrails() function TitleState:stepTrails()
local alive = {} local alive = {}
local maxX = self.trailMaxX or 200 local maxX = self.trailMaxX or 200
local silver = self.trailMode == "silver"
for _, t in ipairs(self.trails) do for _, t in ipairs(self.trails) do
t.x = t.x + 4 t.x = t.x + self.trailStepX
t.y = t.y + 1 t.y = t.y + self.trailStepY
t.phase = t.phase + 3 t.phase = t.phase + self.trailPhaseStep
t.drawY = t.y + math.floor(math.sin(t.phase / 16) * 2) if silver then
t.drawY = t.y + signed(SpriteAnims.sine(t.phase, self.trailBobAmplitude))
else
t.drawY = t.y
+ math.floor(math.sin(t.phase / 16) * self.trailBobAmplitude)
end
if t.x < maxX then alive[#alive + 1] = t end if t.x < maxX then alive[#alive + 1] = t end
end end
self.trails = alive self.trails = alive
@@ -254,14 +286,17 @@ function TitleState:drawWidescreen(winW, winH)
-- Let trails fly into the side bands. -- Let trails fly into the side bands.
self.trailMaxX = math.ceil((winW - ox) / scale) + 16 self.trailMaxX = math.ceil((winW - ox) / scale) + 16
-- Sky above the cloud line, paper white below : edge to edge. The fill has -- Sky above the cloud line, title.below under it (Gold's white cloud
-- to match whichever baked set is showing, or the surround would stay blue -- field, Silver's black sea) : edge to edge. The fill has to match
-- around a grey screen. -- whichever baked set is showing, or the surround would stay blue around a
local sky = self:gray() and SKY_GRAY or SKY -- grey screen.
local sky = self:gray() and SKY_GRAY or self.sky
G.setColor(sky[1], sky[2], sky[3], 1) G.setColor(sky[1], sky[2], sky[3], 1)
G.rectangle("fill", 0, 0, winW, math.max(0, cloudTop)) G.rectangle("fill", 0, 0, winW, math.max(0, cloudTop))
G.setColor(1, 1, 1, 1) local below = self.below
G.setColor(below[1], below[2], below[3], 1)
G.rectangle("fill", 0, cloudTop, winW, winH - cloudTop) G.rectangle("fill", 0, cloudTop, winW, winH - cloudTop)
G.setColor(1, 1, 1, 1)
-- Clouds across the full window width, aligned to the GB cloud band. -- Clouds across the full window width, aligned to the GB cloud band.
G.push() G.push()
+5 -2
View File
@@ -64,6 +64,7 @@ local PAL = {
railBlue = { 70, 150, 255 }, railBlue = { 70, 150, 255 },
railGold = { 255, 203, 5 }, -- Yellow cartridge (bright) railGold = { 255, 203, 5 }, -- Yellow cartridge (bright)
railAmber = { 218, 145, 32 }, -- Gold cartridge (deeper metal) railAmber = { 218, 145, 32 }, -- Gold cartridge (deeper metal)
railSilver = { 190, 198, 210 }, -- Silver cartridge (cool light metal)
} }
-- Semantic aliases kept so ported call sites read the same as before. -- Semantic aliases kept so ported call sites read the same as before.
PAL.cardBorder = PAL.line PAL.cardBorder = PAL.line
@@ -321,10 +322,12 @@ function Theme.meter(x, y, w, h, pct, c)
end end
-- The 4px version rail across the top of both windows: the only brand -- The 4px version rail across the top of both windows: the only brand
-- colour on screen (Red / Blue / Yellow / Gold cartridge colours). -- colour on screen (Red / Blue / Yellow / Gold / Silver cartridge colours).
function Theme.versionRail(x, y, w, h) function Theme.versionRail(x, y, w, h)
if not G then return end if not G then return end
local bars = { PAL.railRed, PAL.railBlue, PAL.railGold, PAL.railAmber } local bars = {
PAL.railRed, PAL.railBlue, PAL.railGold, PAL.railAmber, PAL.railSilver,
}
local seg = w / #bars local seg = w / #bars
for i, c in ipairs(bars) do for i, c in ipairs(bars) do
Theme.fill(x + (i - 1) * seg, y, seg, h, c, 1) Theme.fill(x + (i - 1) * seg, y, seg, h, c, 1)
+16 -10
View File
@@ -1569,11 +1569,11 @@ function World:readVar(varId)
end end
-- Script_checkver: 0 for Gold, 1 for Silver (constants/misc_constants.asm -- Script_checkver: 0 for Gold, 1 for Silver (constants/misc_constants.asm
-- GS_VERSION). Only the Goldenrod prize counters and a handful of gift mons -- GS_VERSION).
-- read it, and the port has no Silver cache yet, so an unset version is Gold.
function World:gsVersion() function World:gsVersion()
local GameVersion = require("src.core.GameVersion")
local save = self.game and self.game.save local save = self.game and self.game.save
local version = (save and save.version) or "gold" local version = (save and save.version) or GameVersion.get()
return version == "silver" and 1 or 0 return version == "silver" and 1 or 0
end end
@@ -6633,8 +6633,9 @@ function World:nameRival(onDone)
if save then if save then
save.rival = save.rival or {} save.rival = save.rival or {}
-- NameRival ends on `ld hl, wRivalName / ld de, .DefaultName / call -- NameRival ends on `ld hl, wRivalName / ld de, .DefaultName / call
-- InitName`, and .DefaultName is "SILVER@" on Gold -- InitName`, and .DefaultName is "SILVER@" on Gold and "GOLD@" on
-- (engine/events/specials.asm:80-91). The default has to be written -- Silver (engine/events/specials.asm:80-94). The default has to be
-- written
-- HERE rather than ridden in from the seed: wRivalName starts as -- HERE rather than ridden in from the seed: wRivalName starts as
-- InitializeNPCNames' "???" (src/core/gen2/Save.lua), which is what the -- InitializeNPCNames' "???" (src/core/gen2/Save.lua), which is what the
-- pre-naming Cherrygrove battle prints. -- pre-naming Cherrygrove battle prints.
@@ -6646,7 +6647,7 @@ function World:nameRival(onDone)
if name and name:gsub(" ", "") ~= "" then if name and name:gsub(" ", "") ~= "" then
save.rival.name = name save.rival.name = name
else else
save.rival.name = "SILVER" save.rival.name = self:gsVersion() == 1 and "GOLD" or "SILVER"
end end
end end
if onDone then onDone(name) end if onDone then onDone(name) end
@@ -10026,8 +10027,11 @@ function World:draw()
G.clear(0.07, 0.05, 0.02, 1) G.clear(0.07, 0.05, 0.02, 1)
if not self.mapImage or not self.player then if not self.mapImage or not self.player then
G.setColor(0.85, 0.57, 0.13, 1) local silver = self:gsVersion() == 1
G.printf("POKEMON GOLD", 0, math.floor(h * 0.38), w, "center") if silver then G.setColor(0.74, 0.78, 0.83, 1)
else G.setColor(0.85, 0.57, 0.13, 1) end
G.printf(silver and "POKEMON SILVER" or "POKEMON GOLD",
0, math.floor(h * 0.38), w, "center")
G.setColor(0.92, 0.90, 0.82, 1) G.setColor(0.92, 0.90, 0.82, 1)
G.printf(self.status or "No map.", G.printf(self.status or "No map.",
0, math.floor(h * 0.48), w, "center") 0, math.floor(h * 0.48), w, "center")
@@ -10140,8 +10144,10 @@ function World:draw()
-- The survey overlay is a developer aid, not part of the game: POKEPORT_DEV -- The survey overlay is a developer aid, not part of the game: POKEPORT_DEV
-- (or the F3 toggle) shows it, a normal boot does not. -- (or the F3 toggle) shows it, a normal boot does not.
if self.showDebugHud then if self.showDebugHud then
G.setColor(0.85, 0.57, 0.13, 1) local silver = self:gsVersion() == 1
G.printf("POKEMON GOLD", 0, 10, w, "center") if silver then G.setColor(0.74, 0.78, 0.83, 1)
else G.setColor(0.85, 0.57, 0.13, 1) end
G.printf(silver and "POKEMON SILVER" or "POKEMON GOLD", 0, 10, w, "center")
G.setColor(0.92, 0.90, 0.82, 1) G.setColor(0.92, 0.90, 0.82, 1)
local label = string.format("%s (%d,%d) %s · %s · zoom %s", local label = string.format("%s (%d,%d) %s · %s · zoom %s",
self.map.id, p.cellX, p.cellY, p.facing, self.map.id, p.cellX, p.cellY, p.facing,
@@ -41,12 +41,21 @@ end
-- Mock isReady -- Mock isReady
RomImporter.isReady = function(v) RomImporter.isReady = function(v)
return v == "red" or v == "gold" or v == "blue" or v == "yellow" return v == "red" or v == "gold" or v == "blue" or v == "yellow"
or v == "silver"
end end
local ok = RomImporter.syncAndroidShortcuts("gold") local ok = RomImporter.syncAndroidShortcuts("gold")
check(ok == true, "syncAndroidShortcuts returns true on Android") check(ok == true, "syncAndroidShortcuts returns true on Android")
check(#capturedShortcuts == 4, "syncAndroidShortcuts caps at 4 items") check(#capturedShortcuts == 4, "syncAndroidShortcuts caps at 4 items")
check(capturedShortcuts[1] == "gold", "activeVersion 'gold' is placed first") check(capturedShortcuts[1] == "gold", "activeVersion 'gold' is placed first")
check(capturedShortcuts[2] == "red" and capturedShortcuts[3] == "blue"
and capturedShortcuts[4] == "yellow",
"the rest follow GameVersion.ORDER until the cap")
capturedShortcuts = nil
RomImporter.syncAndroidShortcuts("silver")
check(#capturedShortcuts == 4, "a fifth ready game does not widen the payload")
check(capturedShortcuts[1] == "silver", "activeVersion 'silver' is placed first")
-- Test with subset of ready games (e.g. only Red and Gold) -- Test with subset of ready games (e.g. only Red and Gold)
RomImporter.isReady = function(v) RomImporter.isReady = function(v)
@@ -78,4 +87,4 @@ end
love.system.getLaunchGame = savedGetLaunchGame love.system.getLaunchGame = savedGetLaunchGame
love.system.updateShortcuts = nil love.system.updateShortcuts = nil
print("8/8 checks passed (android_shortcuts_payload_test)") T.finish("android_shortcuts_payload_test")
+1
View File
@@ -23,6 +23,7 @@ T.eq(GameVersion.generation("red"), 1, "Red is Gen 1")
T.eq(GameVersion.generation("blue"), 1, "Blue is Gen 1") T.eq(GameVersion.generation("blue"), 1, "Blue is Gen 1")
T.eq(GameVersion.generation("yellow"), 1, "Yellow is Gen 1") T.eq(GameVersion.generation("yellow"), 1, "Yellow is Gen 1")
T.eq(GameVersion.generation("gold"), 2, "Gold is Gen 2") T.eq(GameVersion.generation("gold"), 2, "Gold is Gen 2")
T.eq(GameVersion.generation("silver"), 2, "Silver is Gen 2")
-- ------- 2. manifest: gen2compat is opt-in and defaults off -- ------- 2. manifest: gen2compat is opt-in and defaults off
+45 -34
View File
@@ -39,45 +39,54 @@ end
local edited = 0 local edited = 0
local hooks = { editTouchControls = function() edited = edited + 1 end } local hooks = { editTouchControls = function() edited = edited + 1 end }
local gold = LauncherSettings.open(hooks, "gold") for _, version in ipairs({ "gold", "silver" }) do
check(has(gold, "TOUCH PAD"), "Gold's gear offers TOUCH PAD") local model = LauncherSettings.open(hooks, version)
check(has(gold, "VIBRATION"), "and VIBRATION") check(has(model, "TOUCH PAD"), version .. " gear offers TOUCH PAD")
check(has(gold, "TOUCH CONTROLS"), "and the layout editor") check(has(model, "VIBRATION"), version .. " and VIBRATION")
check(has(gold, "VOID FILL"), "and VOID FILL") check(has(model, "TOUCH CONTROLS"), version .. " and the layout editor")
check(has(model, "VOID FILL"), version .. " and VOID FILL")
local voidFill = findRow(gold, "VOID FILL") local voidFill = findRow(model, "VOID FILL")
eq(voidFill.value(), "FADE ", "VOID FILL defaults to FADE") eq(voidFill.value(), "FADE ", version .. " VOID FILL defaults to FADE")
voidFill.step(1) voidFill.step(1)
eq(gold.opts.gold.voidFill, "water", "right stores water in the gold block") eq(model.opts.gold.voidFill, "water",
eq(voidFill.value(), "WATER", "and the row reads WATER") version .. " right stores water in the gold block")
voidFill.step(-1) eq(voidFill.value(), "WATER", version .. " and the row reads WATER")
eq(gold.opts.gold.voidFill, "fade", "left restores fade") voidFill.step(-1)
eq(model.opts.gold.voidFill, "fade", version .. " left restores fade")
-- Every write has to land in the gold block: the flat keys beside it are -- Every write has to land in the gen2 block: the flat keys beside it are
-- Red's, and Gold's boot never reads them (src/core/gen2/Save.lua:299). -- Red's, and no Gen 2 boot reads them (src/core/gen2/Save.lua:299).
-- loadOptions seeds the flat Gen 1 defaults, so the check is that the Gold -- loadOptions seeds the flat Gen 1 defaults, so the check is that the Gen 2
-- rows leave them exactly as they found them. -- rows leave them exactly as they found them.
local flatPad = gold.opts.touchControls local flatPad = model.opts.touchControls
local flatBuzz = gold.opts.haptics local flatBuzz = model.opts.haptics
local pad = findRow(gold, "TOUCH PAD") local pad = findRow(model, "TOUCH PAD")
local before = pad.value() local before = pad.value()
pad.step(1) pad.step(1)
check(pad.value() ~= before, "stepping TOUCH PAD flips it") check(pad.value() ~= before, version .. " stepping TOUCH PAD flips it")
check(type(gold.opts.gold) == "table", "into the gold block") check(type(model.opts.gold) == "table", version .. " into the gold block")
eq(gold.opts.gold.touchControls.enabled, false, "which now carries enabled") eq(model.opts.gold.touchControls.enabled, false,
eq(gold.opts.touchControls, flatPad, "leaving the flat Gen 1 key alone") version .. " which now carries enabled")
eq(model.opts.touchControls, flatPad,
version .. " leaving the flat Gen 1 key alone")
eq(model.opts.silver, nil,
version .. " and inventing no second Gen 2 block beside it")
local buzz = findRow(gold, "VIBRATION") local buzz = findRow(model, "VIBRATION")
local buzzBefore = buzz.value() local buzzBefore = buzz.value()
buzz.step(1) buzz.step(1)
check(buzz.value() ~= buzzBefore, "stepping VIBRATION moves the level") check(buzz.value() ~= buzzBefore, version .. " stepping VIBRATION moves the level")
eq(gold.opts.gold.haptics, TouchControls.normalizeHaptics(gold.opts.gold.haptics), eq(model.opts.gold.haptics,
"VIBRATION stores a level the shared module knows") TouchControls.normalizeHaptics(model.opts.gold.haptics),
eq(gold.opts.haptics, flatBuzz, "also without touching Red's") version .. " VIBRATION stores a level the shared module knows")
eq(model.opts.haptics, flatBuzz, version .. " also without touching Red's")
findRow(gold, "TOUCH CONTROLS").action() edited = 0
eq(edited, 1, "the editor row reaches the host hook") findRow(model, "TOUCH CONTROLS").action()
eq(edited, 1, version .. " the editor row reaches the host hook")
end
-- The Gen 1 gear is untouched by the extraction: same three rows, still on -- The Gen 1 gear is untouched by the extraction: same three rows, still on
-- the flat table. -- the flat table.
@@ -94,6 +103,8 @@ eq(findRow(red, "TOUCH PAD").value() ~= nil, true, "and reading it back")
-- rather than dead, on both sides. -- rather than dead, on both sides.
eq(has(LauncherSettings.open(nil, "gold"), "TOUCH CONTROLS"), false, eq(has(LauncherSettings.open(nil, "gold"), "TOUCH CONTROLS"), false,
"no hook, no editor row on Gold") "no hook, no editor row on Gold")
eq(has(LauncherSettings.open(nil, "silver"), "TOUCH CONTROLS"), false,
"nor on Silver")
eq(has(LauncherSettings.open(nil, "red"), "TOUCH CONTROLS"), false, eq(has(LauncherSettings.open(nil, "red"), "TOUCH CONTROLS"), false,
"nor on Red") "nor on Red")
-- The Edit row hands the screen to the host, and the host has to know WHICH -- The Edit row hands the screen to the host, and the host has to know WHICH
+41 -38
View File
@@ -155,28 +155,34 @@ modImp.mods = mods
modImp._ensureMods = function() return mods end modImp._ensureMods = function() return mods end
LauncherView.draw(modImp) LauncherView.draw(modImp)
LauncherView.draw(modImp) LauncherView.draw(modImp)
check((modImp._modScrollMax or 0) > 0, local reg = modImp._tabRegionRect
"60 mods overflow the list viewport inside the panel") check((modImp._tabScrollMax.mods or 0) > reg.h,
local list = modImp._modListRect "60 installed mods are ONE continuous list: the region's travel spans "
check(list.x + list.w .. "the whole list, not one page of it")
<= modImp._tabRegionRect.x + modImp._tabRegionRect.w - Kit.scrollBarW(), eq(modImp._pages.mods, nil, "and no page state is ever minted for it")
"the rows stop short of the region's scrollbar gutter") pointer(reg.x + 10, reg.y + 40)
pointer(list.x + 10, list.y + 10)
modImp._wheelY = -1 modImp._wheelY = -1
LauncherView.draw(modImp) LauncherView.draw(modImp)
check((modImp.modScroll or 0) > 0, "a notch over the mod list scrolls the list") check((modImp._tabScroll.mods or 0) > 0,
eq(modImp._tabScroll.mods or 0, 0, "not the panel region around it") "a notch over the region scrolls the list like any other tab")
eq(modImp._pageScroll, 0, "and not the page behind that") eq(modImp._pageScroll, 0, "without reaching the page behind it")
modImp._tabScroll.mods = modImp._tabScrollMax.mods
LauncherView.draw(modImp)
pointer(reg.x + 10, reg.y + reg.h * 0.5)
modImp._wheelY = -1
LauncherView.draw(modImp)
eq(modImp._pages.mods, nil,
"bottoming the list out never auto-advances any pager")
eq(modImp._tabScroll.mods, modImp._tabScrollMax.mods,
"the list just rests at its end")
modImp._modActions = "mod1" modImp._modActions = "mod1"
local shielded = modImp.modScroll local shieldedAt = modImp._tabScroll.mods
local shieldedPage = modImp._pageScroll
pointer(list.x + 10, list.y + 10)
modImp._wheelY = -1 modImp._wheelY = -1
LauncherView.draw(modImp) LauncherView.draw(modImp)
eq(modImp.modScroll, shielded, "a shielded mod list ignores the notch") eq(modImp._tabScroll.mods, shieldedAt,
eq(modImp._tabScroll.mods or 0, 0, "and so does the region under the scrim") "a shielded mod list ignores the notch under the scrim")
eq(modImp._pageScroll, shieldedPage, "and the page behind that")
modImp._modActions = nil modImp._modActions = nil
modImp._wheelY = 0 modImp._wheelY = 0
LauncherView.draw(modImp) LauncherView.draw(modImp)
@@ -225,32 +231,27 @@ dragMods.mods = mods
dragMods._ensureMods = function() return mods end dragMods._ensureMods = function() return mods end
LauncherView.draw(dragMods) LauncherView.draw(dragMods)
LauncherView.draw(dragMods) LauncherView.draw(dragMods)
local dlist = dragMods._modListRect local dreg = dragMods._tabRegionRect
local dListMax = dragMods._modScrollMax
local dRegionMax = dragMods._tabScrollMax.mods local dRegionMax = dragMods._tabScrollMax.mods
check(dListMax > 0 and dRegionMax > 0, check(dRegionMax > 0, "the mods region scrolls its overscan like any tab")
"the mods tab has both an inner list and a region to scroll") LauncherView.touchpressed(dragMods, 7, dreg.x + 20, dreg.y + 30)
LauncherView.touchpressed(dragMods, 7, dlist.x + 20, dlist.y + 30) LauncherView.touchmoved(dragMods, 7, dreg.x + 20, dreg.y + 30 - 60)
LauncherView.touchmoved(dragMods, 7, dlist.x + 20, dlist.y + 30 - 60) eq(dragMods._tabScroll.mods, math.min(60, dRegionMax),
eq(dragMods.modScroll, math.min(60, dListMax), "a drag moves the region by the finger's travel")
"the first pixels of the drag move the list") LauncherView.touchmoved(dragMods, 7, dreg.x + 20,
eq(dragMods._tabScroll.mods or 0, 0, "and nothing else") dreg.y + 30 - 60 - dRegionMax * 2)
LauncherView.touchmoved(dragMods, 7, dlist.x + 20, eq(dragMods._tabScroll.mods, dRegionMax, "carrying on saturates the region")
dlist.y + 30 - 60 - dListMax - dRegionMax * 2)
eq(dragMods.modScroll, dListMax, "carrying on saturates the list")
eq(dragMods._tabScroll.mods, dRegionMax,
"then the same gesture walks the region to its bottom")
check((dragMods._pageScroll or 0) > 0, "and only then reaches the page") check((dragMods._pageScroll or 0) > 0, "and only then reaches the page")
LauncherView.touchreleased(dragMods, 7, dlist.x + 20, dlist.y - 900) LauncherView.touchreleased(dragMods, 7, dreg.x + 20, dreg.y - 900)
dragMods._skins = { { id = "s1", source = "user", controls = 8, pages = 1 } } dragMods._skins = { { id = "s1", source = "user", controls = 8, pages = 1 } }
for i = 2, 12 do for i = 2, 12 do
dragMods._skins[i] = { id = "s" .. i, source = "user", controls = 8, pages = 1 } dragMods._skins[i] = { id = "s" .. i, source = "user", controls = 8, pages = 1 }
end end
dragMods._ensureSkins = function() return dragMods._skins end dragMods._ensureSkins = function() return dragMods._skins end
dragMods.modScroll = 0 local heldModsPage = dragMods._pages.mods or 1
local heldModScroll = dragMods.modScroll local heldModsAt = dragMods._tabScroll.mods
local overList = dlist.y + 30 local overList = dreg.y + 60
dragMods:_switchTab("skins") dragMods:_switchTab("skins")
LauncherView.draw(dragMods) LauncherView.draw(dragMods)
LauncherView.draw(dragMods) LauncherView.draw(dragMods)
@@ -260,8 +261,10 @@ LauncherView.touchpressed(dragMods, 9, sreg.x + 20, overList)
LauncherView.touchmoved(dragMods, 9, sreg.x + 20, overList - 200) LauncherView.touchmoved(dragMods, 9, sreg.x + 20, overList - 200)
check((dragMods._tabScroll.skins or 0) > 0, check((dragMods._tabScroll.skins or 0) > 0,
"a drag on the skins tab scrolls the skins tab") "a drag on the skins tab scrolls the skins tab")
eq(dragMods.modScroll, heldModScroll, eq(dragMods._pages.mods or 1, heldModsPage,
"and leaves the mod list where the player parked it") "and leaves the mod list on the page the player parked it")
eq(dragMods._tabScroll.mods, heldModsAt,
"with its region offset held for the return trip")
LauncherView.touchreleased(dragMods, 9, sreg.x + 20, sreg.y - 400) LauncherView.touchreleased(dragMods, 9, sreg.x + 20, sreg.y - 400)
love.graphics.polygon = love.graphics.polygon or function() end love.graphics.polygon = love.graphics.polygon or function() end
@@ -308,8 +311,8 @@ local view = read("src/import/LauncherView.lua")
check(view:find("Kit.scrollBegin(", 1, true) ~= nil, check(view:find("Kit.scrollBegin(", 1, true) ~= nil,
"the panel dispatch opens a scroll region") "the panel dispatch opens a scroll region")
check(view:find("Kit.scrollEnd(", 1, true) ~= nil, "and closes it") check(view:find("Kit.scrollEnd(", 1, true) ~= nil, "and closes it")
check(view:find("modListWantsWheel", 1, true) ~= nil, check(view:find("modListWantsWheel", 1, true) == nil,
"the nested mod list is asked before the region takes a notch") "no nested list steals the wheel from the region any more")
check(view:find("start.region", 1, true) ~= nil, check(view:find("start.region", 1, true) ~= nil,
"a touch drag that began in the region scrolls the region") "a touch drag that began in the region scrolls the region")
check(view:find("Kit.scrollGutter(", 1, true) ~= nil, check(view:find("Kit.scrollGutter(", 1, true) ~= nil,
+12 -8
View File
@@ -32,11 +32,13 @@ do
"a version id names exactly that game") "a version id names exactly that game")
eq(table.concat(ModTargets.expand("GEN1"), ","), "red,blue,yellow", eq(table.concat(ModTargets.expand("GEN1"), ","), "red,blue,yellow",
"gen1 is every Gen 1 game, case-insensitive") "gen1 is every Gen 1 game, case-insensitive")
eq(table.concat(ModTargets.expand("gen2"), ","), "gold", eq(table.concat(ModTargets.expand("gen2"), ","), "gold,silver",
"gen2 is every Gen 2 game") "gen2 is every Gen 2 game")
eq(table.concat(ModTargets.expand("silver"), ","), "silver",
"and each of them names itself")
eq(table.concat(ModTargets.expand("all"), ","), eq(table.concat(ModTargets.expand("all"), ","),
table.concat(GameVersion.ORDER, ","), "all is the launcher order itself") table.concat(GameVersion.ORDER, ","), "all is the launcher order itself")
eq(ModTargets.expand("silver"), nil, "a game this engine has no cache for") eq(ModTargets.expand("crystal"), nil, "a game this engine has no cache for")
eq(ModTargets.expand("gen9"), nil, "a generation with no games is unknown") eq(ModTargets.expand("gen9"), nil, "a generation with no games is unknown")
eq(ModTargets.expand(7), nil, "a non-string token is not a game") eq(ModTargets.expand(7), nil, "a non-string token is not a game")
end end
@@ -56,7 +58,7 @@ end
do do
eq(list(mf({})), "red,blue,yellow", eq(list(mf({})), "red,blue,yellow",
"a manifest with no games key is Gen 1, which is what it was tested as") "a manifest with no games key is Gen 1, which is what it was tested as")
eq(list(mf({ gen2compat = true })), "red,blue,yellow,gold", eq(list(mf({ gen2compat = true })), "red,blue,yellow,gold,silver",
"gen2compat keeps Gen 1 and adds Gen 2") "gen2compat keeps Gen 1 and adds Gen 2")
eq(mf({}).gen2compat, false, "and the derived flag agrees") eq(mf({}).gen2compat, false, "and the derived flag agrees")
eq(mf({ gen2compat = true }).gen2compat, true, "both ways") eq(mf({ gen2compat = true }).gen2compat, true, "both ways")
@@ -67,14 +69,14 @@ end
do do
local gen2 = mf({ games = { "gen2" } }) local gen2 = mf({ games = { "gen2" } })
eq(list(gen2), "gold", "games can name Gen 2 alone") eq(list(gen2), "gold,silver", "games can name Gen 2 alone")
eq(gen2.gen2compat, true, "which IS the gen2compat claim the gate reads") eq(gen2.gen2compat, true, "which IS the gen2compat claim the gate reads")
local both = mf({ games = { "gen1", "gen2" } }) local both = mf({ games = { "gen1", "gen2" } })
eq(list(both), "red,blue,yellow,gold", "or both generations") eq(list(both), "red,blue,yellow,gold,silver", "or both generations")
local one = mf({ games = { "blue" } }) local one = mf({ games = { "blue" } })
eq(list(one), "blue", "or one single game") eq(list(one), "blue", "or one single game")
eq(one.gen2compat, false, "a Gen 1 game is not a Gen 2 claim") eq(one.gen2compat, false, "a Gen 1 game is not a Gen 2 claim")
eq(list(mf({ games = { "red" }, gen2compat = true })), "red,gold", eq(list(mf({ games = { "red" }, gen2compat = true })), "red,gold,silver",
"an old gen2compat beside a new games list still adds its game") "an old gen2compat beside a new games list still adds its game")
end end
@@ -224,10 +226,12 @@ do
local bad = ModProfile.decode(require("src.core.SaveSerializer").encode({ local bad = ModProfile.decode(require("src.core.SaveSerializer").encode({
format = "g1rmodlist", formatVersion = 1, format = "g1rmodlist", formatVersion = 1,
profile = { name = "P", enabledByVersion = { profile = { name = "P", enabledByVersion = {
gold = { a = true }, silver = { a = true }, red = "nope" } }, gold = { a = true }, silver = { a = true }, crystal = { a = true },
red = "nope" } },
})) }))
eq(bad.enabledByVersion.gold.a, true, "a shared file's known game is kept") eq(bad.enabledByVersion.gold.a, true, "a shared file's known game is kept")
eq(bad.enabledByVersion.silver, nil, "an unknown game is dropped on read") eq(bad.enabledByVersion.silver.a, true, "every one of them, not just the first")
eq(bad.enabledByVersion.crystal, nil, "an unknown game is dropped on read")
eq(bad.enabledByVersion.red, nil, "and so is a bucket that is not a table") eq(bad.enabledByVersion.red, nil, "and so is a bucket that is not a table")
end end
@@ -59,7 +59,7 @@ package.loaded["src.import.RomImporter"] = nil
RomImporter = require("src.import.RomImporter") RomImporter = require("src.import.RomImporter")
local function clearSavesInbox() local function clearSavesInbox()
for _, ver in ipairs({ "red", "blue", "yellow", "gold" }) do for _, ver in ipairs({ "red", "blue", "yellow", "gold", "silver" }) do
local dir = "imports/saves/" .. ver local dir = "imports/saves/" .. ver
for _, name in ipairs(love.filesystem.getDirectoryItems(dir) or {}) do for _, name in ipairs(love.filesystem.getDirectoryItems(dir) or {}) do
love.filesystem.remove(dir .. "/" .. name) love.filesystem.remove(dir .. "/" .. name)
@@ -137,6 +137,8 @@ check(createdDirs["imports/saves/yellow"] == true,
"RES-01: ensureSavesInboxDir creates imports/saves/yellow/") "RES-01: ensureSavesInboxDir creates imports/saves/yellow/")
check(createdDirs["imports/saves/gold"] == true, check(createdDirs["imports/saves/gold"] == true,
"RES-01: ensureSavesInboxDir creates imports/saves/gold/") "RES-01: ensureSavesInboxDir creates imports/saves/gold/")
check(createdDirs["imports/saves/silver"] == true,
"RES-01: ensureSavesInboxDir creates imports/saves/silver/")
-- NXSAV-02: notice/hint includes save dir + per-game imports/saves/<version>/ MTP path -- NXSAV-02: notice/hint includes save dir + per-game imports/saves/<version>/ MTP path
ri = freshImporter() ri = freshImporter()
+4 -2
View File
@@ -81,7 +81,8 @@ local function importer(ready)
}, RomImporter) }, RomImporter)
end end
local allReady = importer({ red = true, blue = true, yellow = true, gold = true }) local allReady = importer({ red = true, blue = true, yellow = true, gold = true,
silver = true })
allReady:_queueBaseRomScan() allReady:_queueBaseRomScan()
eq(allReady.baseRomScan.state, "done", "ready launcher skips discovery") eq(allReady.baseRomScan.state, "done", "ready launcher skips discovery")
eq(listings, 0, "ready launcher does not enumerate baseroms") eq(listings, 0, "ready launcher does not enumerate baseroms")
@@ -123,7 +124,8 @@ eq(picks, 0, "missing detected ROM does not open the picker unexpectedly")
missing:choose("red") missing:choose("red")
eq(picks, 1, "the next import attempt falls back to the native picker") eq(picks, 1, "the next import attempt falls back to the native picker")
local rescanned = importer({ red = true, blue = true, yellow = true, gold = true }) local rescanned = importer({ red = true, blue = true, yellow = true, gold = true,
silver = true })
rescanned.baseRoms.red = { path = "baseroms/z-red.gb", name = "z-red.gb" } rescanned.baseRoms.red = { path = "baseroms/z-red.gb", name = "z-red.gb" }
rescanned:reimport("red") rescanned:reimport("red")
check(rescanned.baseRoms.red == nil, "re-import clears the detected ROM") check(rescanned.baseRoms.red == nil, "re-import clears the detected ROM")
+6
View File
@@ -31,10 +31,14 @@ require("src.core.Logger").warn = function() end
local Core = require("src.core.gen2.HallOfFame") local Core = require("src.core.gen2.HallOfFame")
local Credits = require("src.ui.gen2.Credits") local Credits = require("src.ui.gen2.Credits")
local GameVersion = require("src.core.GameVersion")
local HallOfFame = require("src.ui.gen2.HallOfFame") local HallOfFame = require("src.ui.gen2.HallOfFame")
local Save = require("src.core.gen2.Save") local Save = require("src.core.gen2.Save")
local Screens = require("src.ui.Screens") local Screens = require("src.ui.Screens")
local priorVersion = GameVersion.get()
GameVersion.set("gold")
-- ---- fixtures ------------------------------------------------------------- -- ---- fixtures -------------------------------------------------------------
local POKEMON = { local POKEMON = {
@@ -711,4 +715,6 @@ else
eq(creditsData.palettes[1][1][1], 255, "and set 1 colour 0 is white") eq(creditsData.palettes[1][1][1], 255, "and set 1 colour 0 is white")
end end
GameVersion.set(priorVersion)
S.finish() S.finish()
+2
View File
@@ -76,6 +76,8 @@ local function makeWorld()
local game = { local game = {
data = { audio = {}, screens = registry }, data = { audio = {}, screens = registry },
save = { save = {
version = "gold",
generation = 2,
player = { name = "GOLD" }, player = { name = "GOLD" },
party = { { species = "TYPHLOSION", level = 50, hp = 120, party = { { species = "TYPHLOSION", level = 50, hp = 120,
otId = 33333 } }, otId = 33333 } },
+55 -1
View File
@@ -28,6 +28,9 @@ love.filesystem = {
local GameVersion = require("src.core.GameVersion") local GameVersion = require("src.core.GameVersion")
local Save = require("src.core.gen2.Save") local Save = require("src.core.gen2.Save")
local priorVersion = GameVersion.get()
GameVersion.set("gold")
local failures, checks = 0, 0 local failures, checks = 0, 0
local function check(name, got, want) local function check(name, got, want)
checks = checks + 1 checks = checks + 1
@@ -48,6 +51,11 @@ check("main file", main, "save_gold.lua")
check("backup file", backup, "save_gold.lua.bak") check("backup file", backup, "save_gold.lua.bak")
check("staged file", tmp, "save_gold.lua.tmp") check("staged file", tmp, "save_gold.lua.tmp")
check("gold suffix", GameVersion.saveSuffix("gold"), "_gold") check("gold suffix", GameVersion.saveSuffix("gold"), "_gold")
local sMain, sBackup, sTmp = Save.filenames("silver")
check("silver main file", sMain, "save_silver.lua")
check("silver backup file", sBackup, "save_silver.lua.bak")
check("silver staged file", sTmp, "save_silver.lua.tmp")
check("silver suffix", GameVersion.saveSuffix("silver"), "_silver")
-- Red keeps its historical un-suffixed name, so the two can never collide. -- Red keeps its historical un-suffixed name, so the two can never collide.
check("red is unsuffixed", GameVersion.saveSuffix("red"), "") check("red is unsuffixed", GameVersion.saveSuffix("red"), "")
@@ -104,7 +112,13 @@ check("defaults are copied", Save.defaultOptions().textSpeed, "MID")
-- A sparse or hand-edited save must come back indexable rather than crashing -- A sparse or hand-edited save must come back indexable rather than crashing
-- the first screen that reads it. -- the first screen that reads it.
local sparse = Save.normalize({ player = {} }) local sparse = Save.normalize({ player = {} })
check("normalized version", sparse.version, "gold") check("normalized version", sparse.version, GameVersion.get())
check("a Gen 2 version already on the table is kept",
Save.normalize({ version = "silver", player = {} }).version, "silver")
check("and a Gen 1 one is replaced by the running edition",
Save.normalize({ version = "red", player = {} }).version, "gold")
check("as is a version this engine does not have",
Save.normalize({ version = "crystal", player = {} }).version, "gold")
check("party exists", type(sparse.party), "table") check("party exists", type(sparse.party), "table")
check("inventory exists", type(sparse.inventory), "table") check("inventory exists", type(sparse.inventory), "table")
check("pokedex seen exists", type(sparse.pokedex.seen), "table") check("pokedex seen exists", type(sparse.pokedex.seen), "table")
@@ -478,6 +492,46 @@ do
files = {} files = {}
end end
-- ------- the same save, run as Silver
--
-- The blank name is the edition's own first PlayerNameArray row
-- (data/player_names.asm:12-23).
do
GameVersion.set("silver")
files = {}
local born = Save.newGame({})
check("a silver boot stamps silver", born.version, "silver")
check("still generation 2", born.generation, 2)
check("with the edition's own preset name", born.player.name, "SILVER")
check("gold's preset is unchanged", Save.defaultPlayerName("gold"), "GOLD")
check("and silver's is its own", Save.defaultPlayerName("silver"), "SILVER")
check("the running edition answers with no argument",
Save.defaultPlayerName(), "SILVER")
check("a version-less save takes the running edition",
Save.normalize({ player = {} }).version, "silver")
check("and a gold save keeps gold on a silver boot",
Save.normalize({ version = "gold", player = {} }).version, "gold")
check("no silver save yet", Save.exists("silver"), false)
check("silver save wrote", Save.save(born), true)
check("silver save exists now", Save.exists("silver"), true)
check("gold is untouched by it", Save.exists("gold"), false)
local main = Save.filenames("silver")
check("the silver file is silver's own", main, "saves/silver/slot1.lua")
check("and it is the file on disk", files[main] ~= nil, true)
local back = Save.load("silver")
check("silver round-trips", back and back.player.name, "SILVER")
check("keeping its stamp", back.version, "silver")
files = {}
GameVersion.set("gold")
end
GameVersion.set(priorVersion)
print(("gen2 save: %d checks, %d failures"):format(checks, failures)) print(("gen2 save: %d checks, %d failures"):format(checks, failures))
-- Raise rather than os.exit: tests/run_tests.lua dofiles this file, so an -- Raise rather than os.exit: tests/run_tests.lua dofiles this file, so an
-- exit here takes the whole tier down with it and silently skips every -- exit here takes the whole tier down with it and silently skips every
+35 -21
View File
@@ -71,13 +71,17 @@ local data = {
maps = {}, maps = {},
} }
local function newState() local function newState(version)
version = version or "gold"
local prior = GameVersion.get()
GameVersion.set(version)
local S = State.new() local S = State.new()
S.data = data S.data = data
S.cat = Catalog.build(data) S.cat = Catalog.build(data)
S.save = Save2.newGame() S.save = Save2.newGame()
S.version = "gold" S.version = version
Gen.ensureBoxes(S.save) Gen.ensureBoxes(S.save)
GameVersion.set(prior)
return S return S
end end
@@ -85,10 +89,19 @@ do
GameVersion.set("gold") GameVersion.set("gold")
eq(Gen.of({ generation = 2 }), 2, "Gen.of generation field") eq(Gen.of({ generation = 2 }), 2, "Gen.of generation field")
eq(Gen.of({ version = "gold" }), 2, "Gen.of version gold") eq(Gen.of({ version = "gold" }), 2, "Gen.of version gold")
eq(Gen.of({ version = "silver" }), 2, "Gen.of version silver")
eq(Gen.of(SaveData.newGame()), 1, "Gen.of gen1 newGame") eq(Gen.of(SaveData.newGame()), 1, "Gen.of gen1 newGame")
eq(Gen.of(Save2.newGame()), 2, "Gen.of gold newGame") eq(Gen.of(Save2.newGame()), 2, "Gen.of gold newGame")
end end
do
GameVersion.set("silver")
eq(Save2.newGame().version, "silver", "a silver boot stamps silver")
eq(Save2.newGame().player.name, "SILVER", "with silver's preset name")
eq(Gen.of(Save2.newGame()), 2, "Gen.of silver newGame")
GameVersion.set("gold")
end
do do
local S = newState() local S = newState()
check(Ops.speciesUsable(S, "CYNDAQUIL"), "spa/spd species is usable") check(Ops.speciesUsable(S, "CYNDAQUIL"), "spa/spd species is usable")
@@ -103,36 +116,37 @@ do
check(not Ops.speciesUsable(S1, "BROKEN"), "partial record is not usable") check(not Ops.speciesUsable(S1, "BROKEN"), "partial record is not usable")
end end
do for _, version in ipairs({ "gold", "silver" }) do
local S = newState() local S = newState(version)
Ops.partyAdd(S) Ops.partyAdd(S)
eq(#S.save.party, 1, "partyAdd on gold") eq(#S.save.party, 1, "partyAdd on " .. version)
local mon = S.save.party[1] local mon = S.save.party[1]
eq(mon.species, "CYNDAQUIL", "first catalog species") eq(mon.species, "CYNDAQUIL", version .. " first catalog species")
check(mon.experience ~= nil, "gold mon has experience") check(mon.experience ~= nil, version .. " mon has experience")
check(mon.exp == nil or mon.experience ~= nil, "does not rely on gen1 exp") check(mon.exp == nil or mon.experience ~= nil,
version .. " does not rely on gen1 exp")
check(mon.stats.specialAttack and mon.stats.specialDefense, check(mon.stats.specialAttack and mon.stats.specialDefense,
"gold stats have spa/spd") version .. " stats have spa/spd")
check(mon.happiness ~= nil, "gold mon has happiness") check(mon.happiness ~= nil, version .. " mon has happiness")
eq(mon.ot, S.save.player.name, "stampOT copies player name") eq(mon.ot, S.save.player.name, version .. " stampOT copies player name")
Ops.setLevel(S, mon, 20) Ops.setLevel(S, mon, 20)
eq(mon.level, 20, "setLevel 20") eq(mon.level, 20, version .. " setLevel 20")
check(mon.experience > 0, "experience resynced") check(mon.experience > 0, version .. " experience resynced")
Ops.setHappiness(S, mon, 200) Ops.setHappiness(S, mon, 200)
eq(mon.happiness, 200, "happiness 200") eq(mon.happiness, 200, version .. " happiness 200")
Ops.setPokerus(S, mon, 15) Ops.setPokerus(S, mon, 15)
eq(mon.pokerus, 15, "pokerus byte") eq(mon.pokerus, 15, version .. " pokerus byte")
Ops.setHeldItem(S, mon, "POTION") Ops.setHeldItem(S, mon, "POTION")
eq(mon.item, "POTION", "held item") eq(mon.item, "POTION", version .. " held item")
eq(mon.name, "CYNDAQUIL", "new mon copies species display name") eq(mon.name, "CYNDAQUIL", version .. " new mon copies species display name")
Ops.setSpecies(S, mon, "TOTODILE") Ops.setSpecies(S, mon, "TOTODILE")
eq(mon.species, "TOTODILE", "setSpecies id") eq(mon.species, "TOTODILE", version .. " setSpecies id")
eq(mon.name, "TOTODILE", "setSpecies rewrites the Gold display name") eq(mon.name, "TOTODILE", version .. " setSpecies rewrites the display name")
check(mon.nickname == nil, "setSpecies does not invent a nickname") check(mon.nickname == nil, version .. " setSpecies does not invent a nickname")
eq(mon.types[1], "WATER", "setSpecies rewrites copied types") eq(mon.types[1], "WATER", version .. " setSpecies rewrites copied types")
end end
do do
+10
View File
@@ -149,12 +149,22 @@ local ok, err = pcall(function()
local Ops = require("Ops") local Ops = require("Ops")
check(not ModTargets.supports({}, "gold"), check(not ModTargets.supports({}, "gold"),
"legacy gen1-only fixture does not support gold") "legacy gen1-only fixture does not support gold")
check(not ModTargets.supports({}, "silver"),
"nor silver")
check(not ModTargets.supports({ games = { "red" } }, "gold"), check(not ModTargets.supports({ games = { "red" } }, "gold"),
"explicit gen1 games list does not support gold") "explicit gen1 games list does not support gold")
check(not ModTargets.supports({ games = { "red" } }, "silver"),
"nor silver")
check(ModTargets.supports({ games = { "gold" } }, "gold"), check(ModTargets.supports({ games = { "gold" } }, "gold"),
"gold-targeted manifest supports gold") "gold-targeted manifest supports gold")
check(not ModTargets.supports({ games = { "gold" } }, "silver"),
"and names one edition, not the generation")
check(ModTargets.supports({ games = { "gold", "silver" } }, "silver"),
"a whole-Gen-2 games list supports silver")
check(ModTargets.supports({ gen2compat = true }, "gold"), check(ModTargets.supports({ gen2compat = true }, "gold"),
"gen2compat legacy still supports gold") "gen2compat legacy still supports gold")
check(ModTargets.supports({ gen2compat = true }, "silver"),
"and silver with it")
local goldS = { local goldS = {
data = { data = {
+2 -1
View File
@@ -25,6 +25,7 @@ SHELL_COLORS = {
"blue": {"main": (35, 125, 235), "dark": (20, 85, 175), "light": (80, 165, 255)}, "blue": {"main": (35, 125, 235), "dark": (20, 85, 175), "light": (80, 165, 255)},
"yellow": {"main": (255, 205, 10), "dark": (210, 160, 0), "light": (255, 230, 80)}, "yellow": {"main": (255, 205, 10), "dark": (210, 160, 0), "light": (255, 230, 80)},
"gold": {"main": (225, 170, 40), "dark": (170, 120, 20), "light": (245, 200, 80)}, "gold": {"main": (225, 170, 40), "dark": (170, 120, 20), "light": (245, 200, 80)},
"silver": {"main": (185, 190, 200), "dark": (130, 135, 145), "light": (225, 230, 240)},
} }
def extract_cleaned_love_emblem(): def extract_cleaned_love_emblem():
@@ -186,7 +187,7 @@ def main():
fg = create_adaptive_foreground(sizes["adaptive"]) fg = create_adaptive_foreground(sizes["adaptive"])
fg.save(os.path.join(drawable_dir, "ic_launcher_foreground.png"), "PNG") fg.save(os.path.join(drawable_dir, "ic_launcher_foreground.png"), "PNG")
for ver in ("red", "blue", "yellow", "gold"): for ver in ("red", "blue", "yellow", "gold", "silver"):
cart = render_3d_cartridge(ver, sizes["shortcut"]) cart = render_3d_cartridge(ver, sizes["shortcut"])
cart.save(os.path.join(drawable_dir, f"ic_shortcut_{ver}.png"), "PNG") cart.save(os.path.join(drawable_dir, f"ic_shortcut_{ver}.png"), "PNG")
+94
View File
@@ -0,0 +1,94 @@
#!/usr/bin/env python3
"""Derive the Pokemon Silver import manifest from the shipped Gold manifest.
Gold and Silver are assembled from one pokegold source tree; every constant
block, charmap entry, map id, and symbol NAME is identical between the two
builds, and the edition differences (wild encounter tables, the alternate
front pics in gfx/pics_silver.asm, the title screen art, preset player
names, a handful of map texts) are all ROM data that the importer decodes
from the Silver cart at import time. So rather than re-parsing pokegold
from scratch like tools/make_gold_manifest.py does, this takes the shipped
Gold manifest verbatim and overrides only what genuinely differs:
* romSha1 -- Silver's ROM hash (pret/pokegold's byte-exact build).
* symbols -- every symbol re-resolved from pokesilver.sym, because the
edition-selected data blocks have different sizes and shift
their neighbours (e.g. the compressed front pics).
Usage: python3 tools/make_silver_manifest.py
Default paths: Gold manifest beside this script; symbols at
/Users/bryanbassett/Documents/development/pokegold-symbols/pokesilver.sym
(a pokegold checkout's own `make silver` build emits an identical one).
"""
from __future__ import annotations
import argparse
import copy
import json
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from rom_data import CANONICAL_SILVER_SHA1, SymbolTable # noqa: E402
DEV = "/Users/bryanbassett/Documents/development"
DEFAULT_GOLD = os.path.join(os.path.dirname(__file__), "rom_manifest_gold.json")
DEFAULT_OUT = os.path.join(
os.path.dirname(__file__), "rom_manifest_silver.json")
DEFAULT_SYMBOLS = os.path.join(DEV, "pokegold-symbols/pokesilver.sym")
def derive(gold, symbols_path):
"""Return the Silver manifest derived from the Gold manifest dict."""
silver = copy.deepcopy(gold)
silver["romSha1"] = CANONICAL_SILVER_SHA1
silver_symbols = SymbolTable(symbols_path)
resolved, missing = {}, []
for name in gold["symbols"]:
symbol = silver_symbols.by_name.get(name)
if symbol is None:
missing.append(name)
continue
resolved[name] = [symbol.bank, symbol.address]
if missing:
raise SystemExit(
"pokesilver.sym is missing symbols the manifest needs: "
+ ", ".join(sorted(missing)[:10])
+ (" ..." if len(missing) > 10 else ""))
silver["symbols"] = resolved
# Sanity: the edition pic banks must actually have moved something. If
# every address matches Gold's, the --symbols file is almost certainly
# pokegold.sym, and the import would decode Gold data out of a Silver ROM.
if all(resolved[n] == gold["symbols"][n] for n in resolved):
raise SystemExit(
f"{symbols_path} resolves every symbol to Gold's address; "
"is it really pokesilver.sym?")
return silver
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--gold", default=DEFAULT_GOLD,
help="shipped Gold manifest to derive from")
parser.add_argument("--symbols", default=DEFAULT_SYMBOLS,
help="pokesilver.sym symbol file")
parser.add_argument("--out", default=DEFAULT_OUT)
args = parser.parse_args()
with open(args.gold, encoding="utf-8") as f:
gold = json.load(f)
silver = derive(gold, os.path.abspath(args.symbols))
with open(args.out, "w", encoding="utf-8", newline="\n") as f:
json.dump(silver, f, ensure_ascii=False, indent=2, sort_keys=True)
f.write("\n")
print(f"wrote {args.out}")
if __name__ == "__main__":
main()
+3 -2
View File
@@ -741,7 +741,7 @@ def imported_data_dir(repo):
return path if _generated_data_dir_ok(path) else None return path if _generated_data_dir_ok(path) else None
version = (os.environ.get("POKEPORT_VERSION") or "red").lower() version = (os.environ.get("POKEPORT_VERSION") or "red").lower()
if version not in ("red", "blue", "yellow"): if version not in ("red", "blue", "yellow", "gold", "silver"):
version = "red" version = "red"
candidates = [ candidates = [
@@ -2393,7 +2393,8 @@ UPVALUE_CALL = re.compile(
UPVALUE_ARGS = re.compile( UPVALUE_ARGS = re.compile(
r"""^\s*([A-Za-z_]\w*)\s*\.\s*(\w+)\s*,\s*["'](\w+)["']""") r"""^\s*([A-Za-z_]\w*)\s*\.\s*(\w+)\s*,\s*["'](\w+)["']""")
VERSION_MATCH = re.compile( VERSION_MATCH = re.compile(
r"""[=~]=\s*["'](red|blue|yellow)["']|["'](red|blue|yellow)["']\s*[=~]=""") r"""[=~]=\s*["'](red|blue|yellow|gold|silver)["']"""
r"""|["'](red|blue|yellow|gold|silver)["']\s*[=~]=""")
def _line_of(body, offset): def _line_of(body, offset):
+2 -1
View File
@@ -11,8 +11,9 @@ from dataclasses import dataclass
CANONICAL_RED_SHA1 = "ea9bcae617fdf159b045185467ae58b2e4a48b9a" CANONICAL_RED_SHA1 = "ea9bcae617fdf159b045185467ae58b2e4a48b9a"
CANONICAL_BLUE_SHA1 = "d7037c83e1ae5b39bde3c30787637ba1d4c48ce2" CANONICAL_BLUE_SHA1 = "d7037c83e1ae5b39bde3c30787637ba1d4c48ce2"
CANONICAL_YELLOW_SHA1 = "cc7d03262ebfaf2f06772c1a480c7d9d5f4a38e1" CANONICAL_YELLOW_SHA1 = "cc7d03262ebfaf2f06772c1a480c7d9d5f4a38e1"
# Gold is Gen 2: a 2 MiB cart, twice the size of the Gen 1 ROMs above. # Gold and Silver are Gen 2: 2 MiB carts, twice the size of the Gen 1 ROMs above.
CANONICAL_GOLD_SHA1 = "d8b8a3600a465308c9953dfa04f0081c05bdcb94" CANONICAL_GOLD_SHA1 = "d8b8a3600a465308c9953dfa04f0081c05bdcb94"
CANONICAL_SILVER_SHA1 = "49b163f7e57702bc939d642a18f591de55d92dae"
ROM_BANK_SIZE = 0x4000 ROM_BANK_SIZE = 0x4000
File diff suppressed because it is too large Load Diff