mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-21 21:16:28 +02:00
Merge branch 'my-uncles-neighbor' into dev
# Conflicts: # src/import/RomImporter.lua
This commit is contained in:
+11
-11
@@ -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
|
||||||
|
|||||||
@@ -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 |
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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. |
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 |
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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)).
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
local main, backup, tmp = saveNames(save.version)
|
end
|
||||||
|
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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -550,9 +550,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"
|
||||||
|
|
||||||
@@ -699,7 +700,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 = {}
|
||||||
|
|||||||
+27
-78
@@ -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 = {
|
||||||
@@ -1114,16 +1091,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
|
||||||
@@ -1133,7 +1105,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)
|
||||||
@@ -1964,7 +1936,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
|
||||||
@@ -2005,43 +1976,26 @@ 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)
|
||||||
|
if ry + rowH >= viewTop and ry <= viewBot then
|
||||||
local rowKey = rowKeyFor(imp, "mod-row-", mod.id)
|
local rowKey = rowKeyFor(imp, "mod-row-", mod.id)
|
||||||
local isFullyDisabled = true
|
local isFullyDisabled = true
|
||||||
if mod.enabledByVersion then
|
if mod.enabledByVersion then
|
||||||
@@ -2151,13 +2105,10 @@ local function buildModsPanel(imp, x, y, w, availH, m)
|
|||||||
px, ly, PAL.detail)
|
px, ly, PAL.detail)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
Kit.popClip()
|
end
|
||||||
|
|
||||||
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
|
||||||
@@ -4851,8 +4802,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),
|
||||||
@@ -4860,8 +4810,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
|
||||||
@@ -4869,7 +4818,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
@@ -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
@@ -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 = {}
|
||||||
|
|
||||||
@@ -1361,11 +1363,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, issueNotice = nil,
|
mods = nil, modNotice = nil, issueNotice = nil,
|
||||||
requiredImportNotice = 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"
|
||||||
@@ -1422,7 +1423,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()
|
||||||
@@ -1687,7 +1689,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
|
||||||
@@ -1695,7 +1697,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
|
||||||
@@ -1772,7 +1775,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,
|
||||||
@@ -2757,7 +2760,8 @@ function RomImporter:resumeAfterOverlay()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function RomImporter:_cycleTab(delta)
|
function RomImporter:_cycleTab(delta)
|
||||||
local order = { "red", "blue", "yellow", "gold", "mods", "find", "skins", "bug" }
|
local order = { "red", "blue", "yellow", "gold", "silver",
|
||||||
|
"mods", "find", "skins", "bug" }
|
||||||
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
|
||||||
@@ -3123,7 +3127,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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
-- (data/credits_strings.asm:54-60).
|
||||||
|
Credits.STAFF = defineString("STAFF",
|
||||||
|
require("src.core.GameVersion").get() == "silver" and {
|
||||||
|
" #MON",
|
||||||
|
" SILVER VERSION",
|
||||||
|
" PORT STAFF",
|
||||||
|
} or {
|
||||||
" #MON",
|
" #MON",
|
||||||
" GOLD VERSION",
|
" GOLD VERSION",
|
||||||
" PORT STAFF",
|
" PORT STAFF",
|
||||||
})
|
})
|
||||||
defineString("DIRECTOR", " DIRECTOR")
|
defineString("DIRECTOR", " DIRECTOR")
|
||||||
defineString("PROGRAMMING", " PROGRAMMING")
|
defineString("PROGRAMMING", " PROGRAMMING")
|
||||||
defineString("ENGINE_DESIGN", " ENGINE DESIGN")
|
defineString("ENGINE_DESIGN", " ENGINE DESIGN")
|
||||||
|
|||||||
@@ -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 = {}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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()
|
||||||
|
|||||||
@@ -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
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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 } },
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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 = [
|
||||||
@@ -2404,7 +2404,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
@@ -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
Reference in New Issue
Block a user