Merge branch 'my-uncles-neighbor' into dev

# Conflicts:
#	src/import/RomImporter.lua
This commit is contained in:
bryanthaboi
2026-08-20 08:28:19 -04:00
68 changed files with 18617 additions and 466 deletions
+11 -11
View File
@@ -140,17 +140,17 @@ ship text.
### 4. `games` (and the legacy `gen2compat`)
Pokemon Gold is Gen 2, and it runs its own battle engine, overworld, script
VM and save format. The mod API is shared across both generations (same hook
names, same event names, same registry names) but Gold cannot serve all of it
yet, so Gen 2 is opt-in. Say which games the mod is for:
Pokemon Gold and Silver are Gen 2, and they run their own battle engine,
overworld, script VM and save format. The mod API is shared across both
generations (same hook names, same event names, same registry names) but Gen 2
cannot serve all of it yet, so it is opt-in. Say which games the mod is for:
```json
"games": ["gen1", "gen2"]
```
Each entry is a version id (`"red"`, `"blue"`, `"yellow"`, `"gold"`), a
generation (`"gen1"`, `"gen2"`) or `"all"`;
Each entry is a version id (`"red"`, `"blue"`, `"yellow"`, `"gold"`,
`"silver"`), a generation (`"gen1"`, `"gen2"`) or `"all"`;
`src/mods/ModTargets.lua` resolves them off `GameVersion.ORDER` so nothing
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,
@@ -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
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
already ran on. On a Gold boot a mod claiming no Gen 2 game is not loaded at
all: the manager lists it as `ENABLED (NOT THIS GAME)` and says why, because a
mod that half-applies reads as a broken mod. Claim Gen 2 once you have actually
run your mod on Gold.
already ran on. On a Gold or Silver boot a mod claiming no Gen 2 game is not
loaded at all: the manager lists it as `ENABLED (NOT THIS GAME)` and says why,
because a mod that half-applies reads as a broken mod. Claim Gen 2 once you
have actually run your mod on Gold or Silver.
Every token is enforced, per game: the loader gates on the same
`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.
`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
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
+9 -8
View File
@@ -57,14 +57,14 @@ And before you say, "that's not a recomp", you're wrong. Recomp is an acronym. *
This project does not include a ROM, emulate the Game Boy, transpile assembly,
or download a disassembly. A canonical US Poke Red, Blue, Yellow, or Gold ROM
is the only game content input.
or download a disassembly. A canonical US Poke Red, Blue, Yellow, Gold, or
Silver ROM is the only game content input.
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
do not ask for the ROM again. Red, Blue, Yellow, and Gold can all be imported
side by side. Gold is Gen 2 Phase 1 (import + launcher; see
`docs/gold-phase1.md`): the Gen 2 engine is still under construction.
do not ask for the ROM again. Red, Blue, Yellow, Gold, and Silver can all be
imported side by side. Gold and Silver are Gen 2 Phase 1 (import + launcher;
see `docs/gold-phase1.md`): the Gen 2 engine is still under construction.
## 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
game starts automatically.
Only the canonical US Red, Blue, Yellow (1 MiB), and Gold (2 MiB) ROMs are
accepted. The importer verifies SHA-1 before creating any game data:
Only the canonical US Red, Blue, Yellow (1 MiB), Gold, and Silver (2 MiB)
ROMs are accepted. The importer verifies SHA-1 before creating any game data:
- Red: `ea9bcae617fdf159b045185467ae58b2e4a48b9a`
- Blue: `d7037c83e1ae5b39bde3c30787637ba1d4c48ce2`
- Yellow: `cc7d03262ebfaf2f06772c1a480c7d9d5f4a38e1`
- Gold: `d8b8a3600a465308c9953dfa04f0081c05bdcb94`
- Silver: `49b163f7e57702bc939d642a18f591de55d92dae`
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
@@ -219,7 +220,7 @@ entry: a desktop shortcut per game, a Steam entry, or a handheld frontend.
| 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 |
| `--launcher` | open the launcher anyway, so you can edit a shortcut you already made |
+2
View File
@@ -133,6 +133,8 @@ mkdir -p "$GAME_SRC"
(cd "$SOURCE_DIR" && zip -q -9 -r "$WORK/game-payload.zip" \
main.lua conf.lua src libs data assets tools/save-editor \
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/*')
if unzip -Z1 "$WORK/game-payload.zip" \
| grep -Eq '^(data|assets)/generated/[^/]+|^(data|assets)/generated/.+/'; then
+3
View File
@@ -92,6 +92,7 @@ mkdir -p "$GAME_SRC"
main.lua conf.lua src libs data assets tools/save-editor \
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/*')
payload_list="$(unzip -Z1 "$WORK/game-payload.zip")"
printf '%s\n' "$payload_list" \
@@ -99,6 +100,8 @@ printf '%s\n' "$payload_list" \
&& fail "payload unexpectedly contains generated ROM data"
printf '%s\n' "$payload_list" | grep -qxF "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"
rm -f "$WORK/game-payload.zip"
+4 -3
View File
@@ -55,9 +55,10 @@ The short version, for an author deciding what to write:
```
`games` is an optional array of version ids (`"red"`, `"blue"`, `"yellow"`,
`"gold"`), generations (`"gen1"`, `"gen2"`, case-insensitive) or `"all"`.
`src/mods/ModTargets.lua` resolves the tokens off `GameVersion.ORDER` and
`GameVersion.generation`, so nothing anywhere restates the game list.
`"gold"`, `"silver"`), generations (`"gen1"`, `"gen2"`, case-insensitive) or
`"all"`. `src/mods/ModTargets.lua` resolves the tokens off `GameVersion.ORDER`
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`
and **derives** `manifest.gen2compat` from them, which is the one field the
loader's gate reads.
+1 -1
View File
@@ -81,7 +81,7 @@ Every mod contains a root `manifest.json` defining its metadata, supported games
| `entry` | `string` | Entry Lua file path relative to mod root (usually `"main.lua"`). |
| `profile` | `string` | Mod profile: `"content"`, `"overhaul"`, or `"total_conversion"`. |
| `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"`). |
| `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. |
+1
View File
@@ -16,5 +16,6 @@ Features intentionally added beyond the original Pokémon Red, Blue, and Yellow
## 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`
* **Followers** for mods, plus Gen 2-only registries and hooks
+1 -1
View File
@@ -176,7 +176,7 @@ something the filesystem encodes.
| 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) |
| `"all"` | every game this engine has |
+4 -2
View File
@@ -1,7 +1,8 @@
# What This Port Requires
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`
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
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:
- the ROM symbol addresses actually read by the extractor
+11 -9
View File
@@ -91,14 +91,14 @@ Do **not** launch from the Album applet path for normal play.
This project ships **no** game data. On first launch:
1. Put your own legally obtained Pokémon Red, Blue (`.gb`), Yellow, or
Gold (`.gbc`) dump into `switch/gen1recomp/pokemon-love2d/imports/` (the
launcher also shows the live save-dir path). All four can sit in the
1. Put your own legally obtained Pokémon Red, Blue (`.gb`), Yellow, Gold, or
Silver (`.gbc`) dump into `switch/gen1recomp/pokemon-love2d/imports/` (the
launcher also shows the live save-dir path). All five can sit in the
same folder.
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
imports from the Yellow tab (and vice versa). Gold is Beta in the
launcher; a clean US Gold dump is enough to Play.
2. Use **Scan again** on that game's tab (Red / Blue / Yellow / Gold /
Silver). Rescan matches by ROM SHA-1 for the open tab only. A Red dump
never imports from the Yellow tab (and vice versa). Gold and Silver are
Beta in the launcher; a clean US dump of either is enough to Play.
## 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/` |
| Yellow | `imports/saves/yellow/` | `exports/yellow/` |
| Gold | `imports/saves/gold/` | `exports/gold/` |
| Silver | `imports/saves/silver/` | `exports/silver/` |
(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
MTP browsing matches the other games. Gold progress still saves in-engine.)
Gold and Silver cart `.sav` import/export is not supported yet -- the folders
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
([switch-transfer.md](switch-transfer.md)).
+4 -3
View File
@@ -22,8 +22,8 @@ Player install (what to download, title override) stays in
| Loose iteration pair | `sdmc:/switch/gen1recomp/gen1recomp.nro` **and** `game.love` beside it |
| 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** |
| 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 exports | Same save dir → `exports/red\|blue\|yellow\|gold/` (pull after **Export 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\|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` |
| 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
(or copy NRO / `game.love` for loose).
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.
5. Wait for the queue; refresh; exit MTP responder; title-override launch.
+6 -5
View File
@@ -386,11 +386,12 @@ function bootGame(version)
love.window.setTitle(Version.title(
GameVersion.info().displayName .. " (Gen 1 Recompilation Project)"))
end
-- Gold: Gen 1 Game:load cannot consume a Gen 2 cache -- different generated
-- tables, save shape and screen registry -- so Gold boots its own service
-- owner, which mounts src/world/gen2 (walk / warps / connections) and the
-- Gen 2 screens instead of src/core/Game.lua's Gen 1 wiring.
if GameVersion.isGold() then
-- Gen 2: Gen 1 Game:load cannot consume a Gen 2 cache -- different generated
-- tables, save shape and screen registry -- so Gold and Silver boot their
-- own service owner, which mounts src/world/gen2 (walk / warps /
-- connections) and the Gen 2 screens instead of src/core/Game.lua's Gen 1
-- wiring.
if GameVersion.generation() == 2 then
Game = require("src.core.Game2").new()
Game:load()
else
+4 -4
View File
@@ -109,10 +109,10 @@ The APK lands under `app/build/outputs/apk/embedNoRecord/debug/`.
`app/src/embed/assets/game.love` - zip of `main.lua`, `conf.lua`, `src/`,
`libs/` (the vendored FlexLove toolkit the launcher UI needs), `data/`,
`assets/`, and the Red, Blue, and Yellow ROM manifests. The Android
packer verifies the Yellow manifest before it packages; if a partial source
export omitted it, it restores the file from this checkout's Git data and then
falls back to the project's GitHub copy. Generated game data,
`assets/`, and the Red, Blue, Yellow, Gold, and Silver ROM manifests. The
Android packer verifies the Yellow, Gold, and Silver manifests before it
packages; if a partial source export omitted one, it restores the file from
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.
## 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_yellow">#FDD835</color>
<color name="shortcut_gold">#D4AF37</color>
<color name="shortcut_silver">#BEC6D2</color>
</resources>
+3 -1
View File
@@ -71,6 +71,7 @@ rm -f "$LOVE_FILE"
main.lua conf.lua src data assets tools/save-editor \
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/*')
# 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
@@ -90,7 +91,8 @@ for required in tools/save-editor/App.lua tools/save-editor/Kit.lua \
tools/save-editor/panels/Party.lua \
src/ui/kit/Kit.lua \
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" \
|| fail "game.love is missing $required"
done
+54
View File
@@ -30,6 +30,8 @@ YELLOW_MANIFEST_RELATIVE="tools/rom_manifest_yellow.json"
YELLOW_MANIFEST_URL="${YELLOW_MANIFEST_URL:-https://raw.githubusercontent.com/bryanthaboi/gen1recomp/main/tools/rom_manifest_yellow.json}"
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}"
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=""
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"
}
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
# love-android 11.5+ reads app id / name / orientation from gradle.properties.
# 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"
ensure_yellow_manifest
ensure_gold_manifest
ensure_silver_manifest
mkdir -p "$EMBED_ASSETS"
rm -f "$LOVE_FILE"
# 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 \
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' -x '*/.git/*' -x '*/.DS_Store' \
-x 'data/generated/*' -x 'assets/generated/*')
# 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"
grep -qx 'tools/rom_manifest_gold.json' <<< "$archive_entries" \
|| 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
# 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
+2
View File
@@ -366,6 +366,8 @@ cp "$IN/game.love" "$APPDIR/game.love"
unzip -Z1 "$APPDIR/game.love" > "$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"
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
# .DirIcon is what appimaged and file-manager thumbnailers read.
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"
grep -qxF "tools/rom_manifest_gold.json" "$temp_dir/love-listing.txt" \
|| 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"
+2
View File
@@ -50,6 +50,7 @@ rm -f "$OUTPUT"
main.lua conf.lua src data assets tools/save-editor \
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/*')
if [ -f "$ROOT/PATCH_NOTES.md" ]; then
(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/rom_manifest.json tools/rom_manifest_blue.json \
tools/rom_manifest_yellow.json tools/rom_manifest_gold.json \
tools/rom_manifest_silver.json \
src/ui/kit/Kit.lua \
src/import/LauncherView.lua; do
grep -qxF "$required" "$LISTING" \
+3 -1
View File
@@ -17,7 +17,9 @@ fail() { printf '\033[1;31merror:\033[0m %s\n' "$*" >&2; exit 1; }
if [ ! -f "$ROOT/data/generated/maps.lua" ] \
&& [ ! -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"
fi
+10 -4
View File
@@ -71,15 +71,15 @@ First install or update (same steps):
your saves, imported ROMs, mods, and options. Re-extracting only
replaces the NRO(s) and these help files.
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/
then use Scan again in the launcher if needed.
Inboxes (drop files here via MTP / SD / FTP):
imports/ — ROM .gb / .gbc
imports/mods/ — community mod .zip
imports/saves/red|blue|yellow|gold/ — raw .sav import (Gold cart .sav not yet)
exports/red|blue|yellow|gold/ — pull after Export save (Gold not yet)
imports/saves/red|blue|yellow|gold|silver/ — raw .sav import (Gold/Silver cart .sav 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
EOF
@@ -92,7 +92,7 @@ write_readme() {
}
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" \
"Put community mod .zip files here, then MODS → Scan again."
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."
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."
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" \
"After Export save (Red), copy the .sav out of this folder via MTP / SD / FTP."
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."
write_readme "$SAVE_ROOT/exports/gold/README.txt" \
"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"
(
@@ -140,10 +144,12 @@ REQUIRED=(
"switch/gen1recomp/pokemon-love2d/imports/saves/blue/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/silver/README.txt"
"switch/gen1recomp/pokemon-love2d/exports/red/README.txt"
"switch/gen1recomp/pokemon-love2d/exports/blue/README.txt"
"switch/gen1recomp/pokemon-love2d/exports/yellow/README.txt"
"switch/gen1recomp/pokemon-love2d/exports/gold/README.txt"
"switch/gen1recomp/pokemon-love2d/exports/silver/README.txt"
)
for rel in "${REQUIRED[@]}"; do
printf '%s\n' "$LISTING" | grep -Fq "$rel" || fail "zip missing $rel"
+3 -1
View File
@@ -271,10 +271,12 @@ for rel in \
"switch/gen1recomp/pokemon-love2d/imports/saves/blue/README.txt" \
"switch/gen1recomp/pokemon-love2d/imports/saves/yellow/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/blue/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
printf '%s\n' "$ZIP_LIST" | grep -Fq "$rel" || PACK_MISSING="${PACK_MISSING} ${rel}"
done
+2 -2
View File
@@ -3,7 +3,7 @@
-- everything under src/*/gen2 reaches shared services through here. Gen 1
-- 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
-- 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
-- (tilemap + Ho-Oh flap / clouds / trails) → Oak speech (Marill + shrink)
@@ -1702,7 +1702,7 @@ function Game2:hotkey(key)
self:writeSave()
return true
elseif key == "f2" then
local loaded = Save.load("gold")
local loaded = Save.load()
if loaded then self:continueGame(loaded) end
return true
elseif key == "1" then
+18 -4
View File
@@ -1,5 +1,5 @@
-- 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
-- extracted cache lives, and the save-file suffix -- so the importer,
-- 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,
-- Yellow, and Gold (issue #899); a legacy root cache is moved into red/ once
-- 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
-- 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",
cachePrefix = "gold/", -- gold/data/generated, gold/assets/generated
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,
},
}
-- Launcher column order.
GameVersion.ORDER = { "red", "blue", "yellow", "gold" }
GameVersion.ORDER = { "red", "blue", "yellow", "gold", "silver" }
GameVersion.current = "red"
+1
View File
@@ -39,6 +39,7 @@ local function normalizeVersion(v)
b = "blue", blue = "blue",
y = "yellow", yellow = "yellow",
g = "gold", gold = "gold",
s = "silver", silver = "silver",
}
v = alias[v] or v
if GameVersion.VERSIONS and not GameVersion.VERSIONS[v] then return nil end
+1 -1
View File
@@ -218,7 +218,7 @@ function TouchControls.defaultLayout(ww, wh, ox, oy, scale)
local ssW = dpadW * 0.30
local margin = dpadW * 0.12
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))
end
return {
+24 -12
View File
@@ -103,7 +103,7 @@ Save.PLAYER_STATES = {
}
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
-- the flat save_<version>.lua when no slot is registered.
--
@@ -133,16 +133,22 @@ local function fs()
return love.filesystem
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.
function Save.newGame(opts)
opts = opts or {}
local save = {
format = Save.FORMAT,
version = "gold",
version = GameVersion.get(),
generation = 2,
player = {
name = opts.playerName or "GOLD",
name = opts.playerName or Save.defaultPlayerName(),
-- _ResetWRAM rolls wPlayerID out of hRandomSub/hRandomAdd
-- (engine/menus/intro_menu.asm:41-49).
id = opts.trainerId or rand(0, 65535),
@@ -374,10 +380,13 @@ end
function Save.normalize(save)
if type(save) ~= "table" then return nil end
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.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.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))
@@ -753,21 +762,24 @@ end
-- copy is the witness that survives a crash mid-replace.
function Save.save(save)
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")
if ok and SaveData.activeSlot and not SaveData.activeSlot("gold") then
local id = SaveData.createSlot and SaveData.createSlot("gold")
if id and SaveData.setActiveSlot then SaveData.setActiveSlot("gold", id) end
if ok and SaveData.activeSlot and not SaveData.activeSlot(version) then
local id = SaveData.createSlot and SaveData.createSlot(version)
if id and SaveData.setActiveSlot then
SaveData.setActiveSlot(version, id)
end
end
end
local main, backup, tmp = saveNames(save.version)
local main, backup, tmp = saveNames(version)
local f = fs()
if not f then return false, "no filesystem" end
-- saveNames may now return a saves/<version>/<slot>.lua path, and
-- love.filesystem.write does not create missing parent directories.
local dir = main:match("^(.*)/[^/]+$")
if dir and f.createDirectory then f.createDirectory(dir) end
Save.normalize(save)
save.savedAt = os.time()
local encoded = SaveSerializer.encode(save)
if f.getInfo(main) then
+4 -2
View File
@@ -249,10 +249,12 @@ function SwitchDiagnostics.probeAssets(version)
end
-- 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",
"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
local info = filesystem.getInfo(dir)
if info and info.type == "directory" and filesystem.getDirectoryItems then
+2 -1
View File
@@ -27,7 +27,8 @@ local ok, err = pcall(function()
CacheFs.prefix = prefix
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")
or require("src.import.RomExtractor")
+7 -4
View File
@@ -550,9 +550,10 @@ end
-- opened on the Gold tab used to offer a dozen controls that did nothing
-- and hide the seven that the cart itself has.
--
-- The block lives in options.lua under `gold`, which is 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
-- The block lives in options.lua under `gold` (the historical key; Gold and
-- Silver share it the way the Gen 1 games share the flat namespace), which is
-- 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.
local GEN2_KEY = "gold"
@@ -699,7 +700,9 @@ end
function LauncherSettings.open(hooks, version)
local opts = SaveData.loadOptions()
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]
if type(block) ~= "table" then
block = {}
+128 -179
View File
@@ -14,9 +14,9 @@
-- height, and flex-shrink compressing text until it overlapped.
--
-- THE RULES THIS FILE FOLLOWS:
-- * Lists paginate (Kit.pager). The installed-mod list also scrolls inside
-- its viewport, so each of its pages can hold at least ten entries without
-- requiring a tall window. Pages still bound how many mod rows we visit.
-- * Short lists paginate (Kit.pager, perPage from Kit.rowsThatFit); the
-- installed-mod list is one continuous scroll instead, drawing only the
-- rows inside the region viewport so the window bounds the frame cost.
-- * Every click handler only QUEUES work (imp._uiActions); update() drains
-- the queue, so an action that tears the view down (Play, Edit save)
-- never runs inside the frame that dispatched it.
@@ -45,9 +45,6 @@ local COMMUNITY_URL = "https://bois.icu"
local ACT_DEDUP = 0.35
-- Finger travel past this (px) is a drag, not a tap.
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 SKIN_FORMAT_LABEL = {
native = "GEN1",
@@ -81,15 +78,6 @@ local function setTabScroll(imp, value)
imp._tabScroll[tabKeyOf(imp)] = clamp(value, 0, tabScrollMax(imp))
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
local function ensureState(imp)
@@ -192,9 +180,6 @@ function LauncherView.touchpressed(imp, id, x, y)
imp._touchAt = imp._touchAt or {}
imp._touchAt[tostring(id)] = {
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),
}
end
@@ -207,19 +192,9 @@ function LauncherView.touchmoved(imp, id, x, y)
if ddx * ddx + ddy * ddy > TAP_SLOP2 then
start.dragged = true
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
local last = start.lastY or start.y
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
local at, leftover = Kit.scrollHandoff(tabScrollAt(imp),
tabScrollMax(imp), move)
@@ -343,7 +318,7 @@ end
local CART_COLOR = {
red = PAL.railRed, blue = PAL.railBlue, yellow = PAL.railGold,
gold = PAL.railAmber,
gold = PAL.railAmber, silver = PAL.railSilver,
}
local function cartColor(version)
return CART_COLOR[version] or PAL.green
@@ -942,6 +917,8 @@ local GAME_TABS = {
label = "Yellow" },
{ id = "gold", key = "tab-gold", letter = "G", color = PAL.railAmber,
label = "Gold" },
{ id = "silver", key = "tab-silver", letter = "S", color = PAL.railSilver,
label = "Silver" },
}
local HEADER_TABS = {
@@ -1114,16 +1091,11 @@ local function buildHeader(imp, m)
local tabGap = math.floor(6 * m.s)
local tabRowGap = math.floor(4 * m.s)
-- the cartridge dropdown, sized to its longest label so switching games
-- never reflows the row
-- the cartridge dropdown: just the game's initial and the caret; the
-- popup list carries the full names
local chrome0 = headerChrome(imp)
local game = currentGame(imp)
local labelW = 0
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))
local dropW = math.min(tabRight - tabLeft, tabH + math.floor(24 * m.s))
chrome0.game.color = game.color
chrome0.game.letter = game.letter
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
local gameInvert = chrome0.game.active or gameHot
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
local cw = math.floor(7 * 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)
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())
return (cy - y) + math.floor(110 * m.s)
end
@@ -2005,159 +1976,139 @@ local function buildModsPanel(imp, x, y, w, availH, m)
end
-- 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
-- what lets perPage come from the viewport.
-- line of per-game checkboxes. Fixed row heights are what make the
-- cull below plain arithmetic.
local togH = math.floor(26 * m.s)
local gamesLabel = Strings("Enable for:")
local textH = Kit.textHeight("button") + math.floor(4 * m.s)
+ 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
+ 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 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)
for i = first, last do
-- One continuous list: every row is laid out, the region scroll moves
-- 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 ry = listTop + (i - first) * (rowH + gap) - scroll
local rowKey = rowKeyFor(imp, "mod-row-", mod.id)
local isFullyDisabled = true
if mod.enabledByVersion then
for _, on in pairs(mod.enabledByVersion) do
if on then isFullyDisabled = false; break end
local ry = listTop + (i - 1) * (rowH + gap)
if ry + rowH >= viewTop and ry <= viewBot then
local rowKey = rowKeyFor(imp, "mod-row-", mod.id)
local isFullyDisabled = true
if mod.enabledByVersion then
for _, on in pairs(mod.enabledByVersion) do
if on then isFullyDisabled = false; break end
end
else
isFullyDisabled = not mod.enabled
end
else
isFullyDisabled = not mod.enabled
end
local focused = Kit.focusable(rowKey, x, ry, w, rowH)
local hot = focused or Kit.hover(x, ry, w, rowH)
if isFullyDisabled then
Kit.card(x, ry, w, rowH, hot and "mutedHot" or "muted")
else
Kit.card(x, ry, w, rowH, hot)
end
local pad = math.floor(12 * m.s)
local px, inner = x + pad, w - 2 * pad
local ly = ry + math.floor(10 * m.s)
local togGap = math.floor(5 * m.s) + 1
local info = mod.github and mod.github ~= "" and imp:_modUpdateInfo(mod.id)
-- These answer separate games, not a single shared install flag. The
-- importer receives the game id so an experimental confirmation also
-- applies only to the checkbox the player pressed.
local flipped = false
local gamesY = ry + math.floor(8 * m.s) + textH + math.floor(8 * m.s)
Kit.text("micro", gamesLabel, px,
gamesY + (togH - Kit.textHeight("micro")) / 2, PAL.muted)
local tx = px + Kit.textWidth("micro", gamesLabel) + math.floor(10 * m.s)
for _, game in ipairs(GameVersion.ORDER) do
local togKey = "mod-toggle-" .. mod.id .. "-" .. game
if modGameCheckbox(tx, gamesY, togH,
mod.enabledByVersion and mod.enabledByVersion[game] == true,
game, togKey, not safeMode) then
local version = game
queueAction(imp, togKey, function() imp:_toggleMod(mod.id, nil, version) end)
flipped = true
local focused = Kit.focusable(rowKey, x, ry, w, rowH)
local hot = focused or Kit.hover(x, ry, w, rowH)
if isFullyDisabled then
Kit.card(x, ry, w, rowH, hot and "mutedHot" or "muted")
else
Kit.card(x, ry, w, rowH, hot)
end
tx = tx + togH + togGap
end
-- The checkboxes sit inside the row's rect, so their press also passes the
-- row hit test; `flipped` gates the row action to everywhere else.
if not flipped
and (Kit.press(x, ry, w, rowH) or Kit._activateId == rowKey) then
local id = mod.id
queueAction(imp, rowKey, function() imp._modActions = id end)
end
local textW = inner
local pad = math.floor(12 * m.s)
local px, inner = x + pad, w - 2 * pad
local ly = ry + math.floor(10 * m.s)
local badgeW = Kit.textWidth("micro", mod.badge) + math.floor(12 * m.s)
-- the games the mod is for, beside its category: the same chip the
-- in-game manager shows (src/mods/ModTargets.lua)
local gamesW = mod.targets
and Kit.textWidth("micro", mod.targets) + math.floor(12 * m.s) or 0
local nameShown = Kit.ellipsize("button", mod.name,
textW - badgeW - gamesW - math.floor(12 * m.s))
local headingCol = isFullyDisabled and PAL.muted or PAL.heading
Kit.text("button", nameShown, px, ly, headingCol)
local tagX = px + Kit.textWidth("button", nameShown) + math.floor(8 * m.s)
Kit.tag(tagX, ly, badgeW, Kit.textHeight("button"), mod.badge,
mod.experimental and PAL.yellow or PAL.muted)
if mod.targets then
Kit.tag(tagX + badgeW + math.floor(4 * m.s), ly, gamesW,
Kit.textHeight("button"), mod.targets,
mod.targetsHere == false and PAL.steel or PAL.blue)
end
ly = ly + Kit.textHeight("button") + math.floor(4 * m.s)
local togGap = math.floor(5 * m.s) + 1
local info = mod.github and mod.github ~= "" and imp:_modUpdateInfo(mod.id)
-- version + status + update state
local statusText, statusCol = modStatusColor(mod.status)
local line = "v" .. tostring(mod.version or "?") .. " " .. statusText
Kit.text("small", line, px, ly, statusCol)
local lx = px + Kit.textWidth("small", line) + math.floor(12 * m.s)
if imp:_modInfoPending(mod.id) then
-- An inline spinner, because this row's release check is genuinely in
-- flight -- the list stays usable while it resolves.
Loader.dot(lx, ly, Kit.textHeight("small"))
Kit.text("small", Strings("Checking..."),
lx + Kit.textHeight("small") + math.floor(6 * m.s), ly, PAL.muted)
elseif info and info.status == "available" then
Kit.text("small", Strings("v%s available", tostring(info.latest)),
lx, ly, PAL.yellow)
elseif info and info.status == "current" then
Kit.text("small", Strings("up to date"), lx, ly, PAL.muted)
elseif info and info.status == "error" then
Kit.text("small", Strings("check failed"), lx, ly, PAL.red)
end
ly = ly + Kit.textHeight("small") + math.floor(2 * m.s)
-- one line of description, or the download stats when we have them
-- (download count in green so popularity reads at a glance)
if info and info.downloads then
local d = info.dates
local dl = ModUpdate.downloadsLine(info.downloads.total)
local dates = ModUpdate.datesLine(d and d.first, d and d.latest)
local segs = {}
if dl then segs[#segs + 1] = { dl, PAL.green } end
if dates then
segs[#segs + 1] = { (dl and " - " or "") .. dates, PAL.detail }
-- These answer separate games, not a single shared install flag. The
-- importer receives the game id so an experimental confirmation also
-- applies only to the checkbox the player pressed.
local flipped = false
local gamesY = ry + math.floor(8 * m.s) + textH + math.floor(8 * m.s)
Kit.text("micro", gamesLabel, px,
gamesY + (togH - Kit.textHeight("micro")) / 2, PAL.muted)
local tx = px + Kit.textWidth("micro", gamesLabel) + math.floor(10 * m.s)
for _, game in ipairs(GameVersion.ORDER) do
local togKey = "mod-toggle-" .. mod.id .. "-" .. game
if modGameCheckbox(tx, gamesY, togH,
mod.enabledByVersion and mod.enabledByVersion[game] == true,
game, togKey, not safeMode) then
local version = game
queueAction(imp, togKey, function() imp:_toggleMod(mod.id, nil, version) end)
flipped = true
end
tx = tx + togH + togGap
end
-- The checkboxes sit inside the row's rect, so their press also passes the
-- row hit test; `flipped` gates the row action to everywhere else.
if not flipped
and (Kit.press(x, ry, w, rowH) or Kit._activateId == rowKey) then
local id = mod.id
queueAction(imp, rowKey, function() imp._modActions = id end)
end
local textW = inner
local badgeW = Kit.textWidth("micro", mod.badge) + math.floor(12 * m.s)
-- the games the mod is for, beside its category: the same chip the
-- in-game manager shows (src/mods/ModTargets.lua)
local gamesW = mod.targets
and Kit.textWidth("micro", mod.targets) + math.floor(12 * m.s) or 0
local nameShown = Kit.ellipsize("button", mod.name,
textW - badgeW - gamesW - math.floor(12 * m.s))
local headingCol = isFullyDisabled and PAL.muted or PAL.heading
Kit.text("button", nameShown, px, ly, headingCol)
local tagX = px + Kit.textWidth("button", nameShown) + math.floor(8 * m.s)
Kit.tag(tagX, ly, badgeW, Kit.textHeight("button"), mod.badge,
mod.experimental and PAL.yellow or PAL.muted)
if mod.targets then
Kit.tag(tagX + badgeW + math.floor(4 * m.s), ly, gamesW,
Kit.textHeight("button"), mod.targets,
mod.targetsHere == false and PAL.steel or PAL.blue)
end
ly = ly + Kit.textHeight("button") + math.floor(4 * m.s)
-- version + status + update state
local statusText, statusCol = modStatusColor(mod.status)
local line = "v" .. tostring(mod.version or "?") .. " " .. statusText
Kit.text("small", line, px, ly, statusCol)
local lx = px + Kit.textWidth("small", line) + math.floor(12 * m.s)
if imp:_modInfoPending(mod.id) then
-- An inline spinner, because this row's release check is genuinely in
-- flight -- the list stays usable while it resolves.
Loader.dot(lx, ly, Kit.textHeight("small"))
Kit.text("small", Strings("Checking..."),
lx + Kit.textHeight("small") + math.floor(6 * m.s), ly, PAL.muted)
elseif info and info.status == "available" then
Kit.text("small", Strings("v%s available", tostring(info.latest)),
lx, ly, PAL.yellow)
elseif info and info.status == "current" then
Kit.text("small", Strings("up to date"), lx, ly, PAL.muted)
elseif info and info.status == "error" then
Kit.text("small", Strings("check failed"), lx, ly, PAL.red)
end
ly = ly + Kit.textHeight("small") + math.floor(2 * m.s)
-- one line of description, or the download stats when we have them
-- (download count in green so popularity reads at a glance)
if info and info.downloads then
local d = info.dates
local dl = ModUpdate.downloadsLine(info.downloads.total)
local dates = ModUpdate.datesLine(d and d.first, d and d.latest)
local segs = {}
if dl then segs[#segs + 1] = { dl, PAL.green } end
if dates then
segs[#segs + 1] = { (dl and " - " or "") .. dates, PAL.detail }
end
segLine("small", segs, px, ly, textW)
elseif (mod.description or "") ~= "" then
Kit.text("small", Kit.ellipsize("small", mod.description, textW),
px, ly, PAL.detail)
end
segLine("small", segs, px, ly, textW)
elseif (mod.description or "") ~= "" then
Kit.text("small", Kit.ellipsize("small", mod.description, textW),
px, ly, PAL.detail)
end
end
Kit.popClip()
local pagerY = listTop + listH + gap
local newPage, newPagerH = Kit.pager(x, pagerY, w, cur, #mods, perPage, "mods")
if newPage ~= cur then imp.modScroll = 0 end
setPage(imp, "mods", newPage)
return pagerY + newPagerH - y
local contentH = #mods * rowH + (#mods - 1) * gap
return (listTop + contentH + gap) - y
end
-- ---------------------------------------------------------- find mods panel
@@ -4851,8 +4802,7 @@ function LauncherView.draw(imp)
Kit.blockClicks = modalUp(imp)
local step = Kit.scrollStep(m.s)
local nested = modListWantsWheel(imp, Kit.wheelY or 0)
if not nested then
do
local rect = imp._tabRegionRect
if rect then
setTabScroll(imp, (Kit.scrollWheel(tabScrollAt(imp), tabScrollMax(imp),
@@ -4860,8 +4810,7 @@ function LauncherView.draw(imp)
end
end
local scroll = math.max(0, math.min(imp._pageScroll or 0, scrollMax))
if scrollMax > 0 and (Kit.wheelY or 0) ~= 0 and not nested
and not Kit.blockClicks then
if scrollMax > 0 and (Kit.wheelY or 0) ~= 0 and not Kit.blockClicks then
local moved = math.max(0, math.min(scroll - Kit.wheelY * step, scrollMax))
if moved ~= scroll then
scroll = moved
@@ -4869,7 +4818,7 @@ function LauncherView.draw(imp)
end
end
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
local was = tabScrollAt(imp)
local to = Kit.scrollClamp(was - Kit.wheelY * step, tabScrollMax(imp))
+122 -27
View File
@@ -4,6 +4,7 @@
-- (species order IS dex order, pics/tilesets are lz3-compressed rather than
-- pkmncompress'd, maps are grouped instead of flat). See docs/gold-phase1.md.
local bit = require("bit")
local GameVersion = require("src.core.GameVersion")
local ImageWriter = require("src.import.ImageWriter")
local LuaWriter = require("src.import.LuaWriter")
local Rom = require("src.import.Rom")
@@ -168,6 +169,10 @@ function RomExtractorGen2.new(romData, manifest, progress)
symbols = manifest.symbols,
progress = progress,
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)
end
@@ -1641,22 +1646,40 @@ function RomExtractorGen2:extractTitle()
return 3
end
-- pret gfx/title/title_bg_gold.pal / title_fg.pal (5 BG pals, 2 OBJ pals).
local BG_PALS = {
local silver = self.edition == "silver"
-- 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, 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 }, { 29, 25, 0 }, { 15, 20, 31 }, { 17, 10, 1 } },
{ { 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);
-- pal 1 = gold trail sparks (OAM_PAL1 on GSTitleTrail).
-- title_fg.pal, shared (GSTitleOBPals, engine/gfx/color.asm:1241): pal 0 =
-- Ho-Oh silhouette; pal 1 = gold trail sparks (OAM_PAL1 on GSTitleTrail).
local OBJ_HOOH = {
{ 31, 31, 31 }, { 7, 6, 3 }, { 7, 6, 3 }, { 7, 6, 3 },
}
local OBJ_TRAIL = {
{ 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 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
-- a straight decode gives. rOBP1 (%11111000) carries the gold trail.
local DMG_BGP = { 0, 2, 1, 3 }
local DMG_OBP0 = { 3, 3, 3, 3 }
local DMG_OBP1 = { 0, 2, 3, 3 }
-- engine/movie/title.asm:105-115: Silver writes %11110000 to both OBPs.
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.
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).
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 = {
{ -- 1
{ -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 },
},
}
-- Frameset_GSIntroHoOhLugia (Gold): 1,2,3,4,3,5 with these durations.
local HOOH_SEQUENCE = {
-- Silver's five oamsets, as {layout, vtile base} (oam.asm:103-107).
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 },
}
local hoohPaths, hoohGrayPaths = {}, {}
local originX, originY = 32, 24
for fi, oam in ipairs(HOOH_FRAMES) do
-- Lugia1/2 span x tiles -5..4, four tiles wider than Ho-Oh's -4..3.
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
-- 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.
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
local px = originX + spr[1] * 8 + spr[3]
local py = originY + spr[2] * 8 + spr[4]
blitSprite(pose, hoohTiles[spr[5] + 1], px, py)
blitSprite(pose, hoohTiles[spr[5] + 2], px, py + 8)
blitSprite(pose, hoohTiles[base + spr[5] + 1], px, py)
blitSprite(pose, hoohTiles[base + spr[5] + 2], px, py + 8)
end
local tinted = colorize(pose, function() return OBJ_HOOH end)
local rel = ("title/hooh_%d.png"):format(fi)
@@ -1855,14 +1915,20 @@ function RomExtractorGen2:extractTitle()
end
self:tick("Title screen", 3, 5)
-- Trail: TitleScreenGFX3 is raw 2bpp (8 tiles); Gold OAM uses one 8x16
-- on OAM_PAL1 (gold), not the Ho-Oh silhouette pal.
-- Trail: TitleScreenGFX3 is raw 2bpp; Gold's OAM is one 8x16, Silver's two
-- side by side, and only 4 of Silver's 8 copied tiles exist (title.asm:43).
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 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[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)
self:save(trailTint, "title/trail.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",
hoohFramesGray = hoohGrayPaths,
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,
-- 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
-- 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
@@ -1919,19 +1988,45 @@ function RomExtractorGen2:extractTitle()
-- 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
-- (x - 8, y - 16) on screen.
-- The pose canvas holds its own origin at (32, 24), so the sheet's corner
-- is (88 - 8 - 32, 96 - 16 - 24) -- and the bird's 64px width then lands
-- centred on the screen, 48 to 112.
hoohX = 48,
hoohY = 56,
-- The pose canvas holds its own origin, so the sheet's corner is
-- (88 - 8 - originX, 96 - 16 - originY) -- and the pose's width then lands
-- centred on the screen (Ho-Oh 48..112, Lugia 40..120).
hoohX = 80 - originX,
hoohY = 80 - originY,
trail = "assets/generated/title/trail.png",
copyright = "assets/generated/title/copyright.png",
copyrightSplash = "assets/generated/title/copyright_splash.png",
-- ScrollTitleScreenClouds: Gold decrements the cloud-band SCX every
-- 8 vblanks, so the strip slides 1px right. Silver does the same
-- decrement every frame.
cloudScrollEvery = 8,
-- ScrollTitleScreenClouds (engine/menus/intro_menu.asm:917-928): Gold
-- decrements the cloud-band SCX every 8 vblanks, so the strip slides 1px
-- right. Silver does the same decrement every frame.
cloudScrollEvery = silver and 1 or 8,
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)
return data
+13 -10
View File
@@ -142,6 +142,8 @@ local VERSION_REQUIRED_FILES_OVERRIDE = {
"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):
-- a dark neon arcade panel, one column per game.
@@ -335,7 +337,7 @@ function RomImporter.syncAndroidShortcuts(activeVersion)
return false
end
local allVersions = { "red", "blue", "yellow", "gold" }
local allVersions = GameVersion.ORDER
local ready = {}
local seen = {}
@@ -1361,11 +1363,10 @@ function RomImporter.new(onComplete, opts)
saveNotice = {},
-- MODS panel state (pass 3): mods is the cached LauncherMods.list() array
-- (refreshed lazily on first draw and after any toggle/install/delete);
-- modScroll is the current paged list's inner scroll offset (px, clamped
-- in draw); modNotice is the last install/delete result { ok, text }.
-- modNotice is the last install/delete result { ok, text }.
-- requiredImportNotice stays inside the imported-files modal so validation
-- 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,
-- Which game the MODS panel is answering for (a GameVersion id, nil =
-- every game). Rows resolve their enable-state and their "runs here"
@@ -1422,7 +1423,8 @@ function RomImporter.new(onComplete, opts)
self.returning[version] =
(not ready) and marker ~= nil and marker ~= markerFor(version)
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
RomImporter.syncAndroidShortcuts()
self:_applyLastVersionTab()
@@ -1687,7 +1689,7 @@ function RomImporter:startData(data, displayName)
end
if not isAcceptedRomSize(#data) then
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))
return
end
@@ -1695,7 +1697,8 @@ function RomImporter:startData(data, displayName)
local version = GameVersion.forSha1(actualHash)
if not version then
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))
return
end
@@ -1772,7 +1775,7 @@ function RomImporter:_startExtractCoroutine(version, info, prefix, displayName)
local CacheFs = require("src.import.CacheFs")
CacheFs.prefix = prefix
local manifest = require("src.import.RomManifest").decode(version)
local RomExtractor = version == "gold"
local RomExtractor = GameVersion.generation(version) == 2
and require("src.import.RomExtractorGen2")
or require("src.import.RomExtractor")
local extractor = RomExtractor.new(self.romData, manifest,
@@ -2757,7 +2760,8 @@ function RomImporter:resumeAfterOverlay()
end
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
for i, id in ipairs(order) do
if id == self.tab then idx = i; break end
@@ -3123,7 +3127,6 @@ function RomImporter:_switchTab(id)
self.tab = id
self._findSearchFocus = false
self._skinUrlFocus = false
self._modScrollMax, self._modListRect = 0, nil
self:_disarmTextInput()
-- 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
+4 -1
View File
@@ -228,7 +228,10 @@ SaveConvert.mergeDefaults = mergeDefaults
-- 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
-- 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
-- bytes: the raw 32768-byte SRAM string. Validates size and the main-data
+1 -1
View File
@@ -5,7 +5,7 @@ SyncMods.REV = 1
local function versions()
local ok, GameVersion = pcall(require, "src.core.GameVersion")
if ok and GameVersion and GameVersion.ORDER then return GameVersion.ORDER end
return { "red", "blue", "yellow", "gold" }
return { "red", "blue", "yellow", "gold", "silver" }
end
local function defaultDeps()
+8 -3
View File
@@ -73,7 +73,10 @@ function Editor.load(opts)
}
local optsTbl = SaveData.loadOptions()
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 {}
applied = {
touchControls = gold.touchControls,
@@ -151,7 +154,9 @@ local function persist()
skin = cfg.skin,
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.touchControls = block
else
@@ -569,7 +574,7 @@ function Editor.new(game)
local state = { game = game, isOpaque = true }
Editor.hostPoll = true
Editor.load({
version = "gold",
version = require("src.core.GameVersion").get(),
hostPoll = true,
onClose = function()
Editor.hostPoll = false
+12 -5
View File
@@ -189,11 +189,18 @@ defineString("CREDIT_END", "END")
-- STAFF and everything after it is a heading. Anything BELOW this id is a
-- person as far as ParseCredits is concerned.
Credits.STAFF = defineString("STAFF", {
" #MON",
" GOLD VERSION",
" PORT 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",
" GOLD VERSION",
" PORT STAFF",
})
defineString("DIRECTOR", " DIRECTOR")
defineString("PROGRAMMING", " PROGRAMMING")
defineString("ENGINE_DESIGN", " ENGINE DESIGN")
+2 -2
View File
@@ -147,8 +147,8 @@ function GoldSilverIntro.new(game, opts)
-- the movie's full 2335 frames over an empty screen and reads exactly
-- 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.
Logger.warn("gold intro: no intro.lua in the cache -- re-import Gold "
.. "or the movie plays blank")
Logger.warn("gen2 intro: no intro.lua in the cache -- re-import "
.. "this version or the movie plays blank")
end
self.images = {}
self.sheets = {}
+1 -1
View File
@@ -56,7 +56,7 @@ function MainMenu.new(game, opts)
self.save = opts.save
if self.save == nil and opts.hasSave ~= false then
local loaded = Save.load("gold")
local loaded = Save.load()
self.save = loaded
end
self.hasSave = opts.hasSave
+12 -4
View File
@@ -34,8 +34,14 @@ local NamePick = {}
NamePick.__index = NamePick
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_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).
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.onDone = opts.onDone
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
end
-- `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
self:choose(name)
else
self:choose(self.items[2] or "GOLD")
self:choose(self.items[2] or presetsFor()[1])
end
end,
})
@@ -146,7 +152,7 @@ function NamePick:update(_dt)
if self.fontOk then
self:openNaming()
else
self:choose(self.items[2] or "GOLD")
self:choose(self.items[2] or presetsFor()[1])
end
else
-- A preset returns through MovePlayerPicLeft, so the pic walks back
@@ -227,5 +233,7 @@ function NamePick:drawWidescreen(winW, winH)
end
NamePick.PRESETS = PRESETS
NamePick.PRESETS_SILVER = PRESETS_SILVER
NamePick.presetsFor = presetsFor
return NamePick
+4 -4
View File
@@ -72,8 +72,8 @@ end
-- Naming presets are boot config the same way Gen 1 reads them
-- (field.boot.namePresets), so a total conversion that replaces the list once
-- replaces it for both games; NamePick.PRESETS (data/player_names.asm
-- PlayerNameArray) is Gold's fallback.
-- replaces it for both games; NamePick.presetsFor (data/player_names.asm
-- PlayerNameArray) is the running edition's fallback.
local function namePresets(game, who, fallback)
local boot = game and game.data and game.data.field
and game.data.field.boot
@@ -351,9 +351,9 @@ function OakSpeech:openNamePick(step)
picColors = self.playerColors,
presets = step.presets
or namePresets(self.game, step.presetsWho or step.who or "player",
step.presetsFallback or NamePick.PRESETS),
step.presetsFallback or NamePick.presetsFor()),
onDone = function(name)
name = name or "GOLD"
name = name or NamePick.presetsFor()[1]
self.game.save.player.name = name
self.game.stack:pop() -- NamePick
self.busy = false
+1 -1
View File
@@ -181,7 +181,7 @@ function PcMenu:beginChangeBox(index)
self.saveTimer = 0
self.saved = nil
local existed = self.saveExists
if existed == nil then existed = Save.exists("gold") end
if existed == nil then existed = Save.exists() end
self.existed = existed
end
+1 -1
View File
@@ -71,7 +71,7 @@ function SaveMenu.new(game, opts)
self.onDone = opts.onDone
self.writer = opts.writer or Save.save
local existed = opts.existed
if existed == nil then existed = Save.exists("gold") end
if existed == nil then existed = Save.exists() end
self.existed = existed
-- confirm -> overwrite (only when a file exists) -> saving -> done
self.phase = "confirm"
+60 -25
View File
@@ -2,6 +2,7 @@
-- wing-flap (Frameset_GSIntroHoOhLugia), spark trails, A/Start to continue.
-- drawWidescreen fills the window with sky/clouds so widescreen has no
-- 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
-- love.graphics.newImage skips overrides/ and AssetTransform output.
@@ -16,7 +17,7 @@ local TitleState = {}
TitleState.__index = TitleState
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 }
-- ...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
@@ -61,6 +62,15 @@ function TitleState.new(game, opts)
self.hoohY = tonumber(title.hoohY) or 56
self.cloudY = tonumber(title.cloudY) or 88
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 = {}, {}
local paths = title.hoohFrames
@@ -80,8 +90,12 @@ function TitleState.new(game, opts)
self.sequence = title.hoohSequence or {
{ 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.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
-- AnimSeq_GSIntroHoOhLugia's SPRITEANIMSTRUCT_VAR1.
@@ -89,12 +103,21 @@ function TitleState.new(game, opts)
self.frameCounter = 0
self.cloudScroll = 0
self.trails = {}
-- UpdateTitleTrailSprite / TitleTrailCoords (intro_menu.asm), in pixels.
self.trailSpawns = {
-- UpdateTitleTrailSprite / TitleTrailCoords (intro_menu.asm:1069-1124), in
-- pixels.
self.trailSpawns = title.trailSpawns or {
{ 80, 88 }, { 104, 88 }, { 104, 88 }, { 120, 88 },
{ 120, 88 }, { 88, 88 },
}
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.
self.trailMaxX = 200
self.musicStarted = false
@@ -119,47 +142,56 @@ function TitleState:enter()
end
end
-- AnimSeq_GSIntroHoOhLugia (engine/sprite_anims/functions.asm): VAR1 counts up
-- one per frame and the struct's Y offset becomes `d * sin(VAR1 * pi/32)` with
-- d = 2 on Gold (Silver counts DOWN with d = 8). Sprites_Sine hands back the
-- byte the ASM leaves in a, so the down half of the wave arrives in two's
-- 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
-- Sprites_Sine hands back the byte the ASM leaves in a, so the down half of
-- the wave arrives in two's complement and is a signed pixel delta here.
local function signed(value)
if value >= 0x80 then return value - 0x100 end
return value
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()
self.hoohPhase = (self.hoohPhase + 1) % 256
self.hoohPhase = (self.hoohPhase + self.hoohBobStep) % 256
self.seqLeft = self.seqLeft - 1
if self.seqLeft > 0 then return end
self.seqIndex = self.seqIndex + 1
if self.seqIndex > #self.sequence then self.seqIndex = 1 end
local step = self.sequence[self.seqIndex]
self.frame = step[1]
self.seqLeft = step[2]
self.seqLeft = step[2] + 1
end
function TitleState:spawnTrail()
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]
self.trailSpawnIndex = self.trailSpawnIndex % #self.trailSpawns + 1
if not spawn then return end
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
function TitleState:stepTrails()
local alive = {}
local maxX = self.trailMaxX or 200
local silver = self.trailMode == "silver"
for _, t in ipairs(self.trails) do
t.x = t.x + 4
t.y = t.y + 1
t.phase = t.phase + 3
t.drawY = t.y + math.floor(math.sin(t.phase / 16) * 2)
t.x = t.x + self.trailStepX
t.y = t.y + self.trailStepY
t.phase = t.phase + self.trailPhaseStep
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
end
self.trails = alive
@@ -254,14 +286,17 @@ function TitleState:drawWidescreen(winW, winH)
-- Let trails fly into the side bands.
self.trailMaxX = math.ceil((winW - ox) / scale) + 16
-- Sky above the cloud line, paper white below : edge to edge. The fill has
-- to match whichever baked set is showing, or the surround would stay blue
-- around a grey screen.
local sky = self:gray() and SKY_GRAY or SKY
-- Sky above the cloud line, title.below under it (Gold's white cloud
-- field, Silver's black sea) : edge to edge. The fill has to match
-- whichever baked set is showing, or the surround would stay blue around a
-- grey screen.
local sky = self:gray() and SKY_GRAY or self.sky
G.setColor(sky[1], sky[2], sky[3], 1)
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.setColor(1, 1, 1, 1)
-- Clouds across the full window width, aligned to the GB cloud band.
G.push()
+5 -2
View File
@@ -64,6 +64,7 @@ local PAL = {
railBlue = { 70, 150, 255 },
railGold = { 255, 203, 5 }, -- Yellow cartridge (bright)
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.
PAL.cardBorder = PAL.line
@@ -321,10 +322,12 @@ function Theme.meter(x, y, w, h, pct, c)
end
-- 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)
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
for i, c in ipairs(bars) do
Theme.fill(x + (i - 1) * seg, y, seg, h, c, 1)
+16 -10
View File
@@ -1569,11 +1569,11 @@ function World:readVar(varId)
end
-- 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
-- read it, and the port has no Silver cache yet, so an unset version is Gold.
-- GS_VERSION).
function World:gsVersion()
local GameVersion = require("src.core.GameVersion")
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
end
@@ -6633,8 +6633,9 @@ function World:nameRival(onDone)
if save then
save.rival = save.rival or {}
-- NameRival ends on `ld hl, wRivalName / ld de, .DefaultName / call
-- InitName`, and .DefaultName is "SILVER@" on Gold
-- (engine/events/specials.asm:80-91). The default has to be written
-- InitName`, and .DefaultName is "SILVER@" on Gold and "GOLD@" on
-- Silver (engine/events/specials.asm:80-94). The default has to be
-- written
-- HERE rather than ridden in from the seed: wRivalName starts as
-- InitializeNPCNames' "???" (src/core/gen2/Save.lua), which is what the
-- pre-naming Cherrygrove battle prints.
@@ -6646,7 +6647,7 @@ function World:nameRival(onDone)
if name and name:gsub(" ", "") ~= "" then
save.rival.name = name
else
save.rival.name = "SILVER"
save.rival.name = self:gsVersion() == 1 and "GOLD" or "SILVER"
end
end
if onDone then onDone(name) end
@@ -10026,8 +10027,11 @@ function World:draw()
G.clear(0.07, 0.05, 0.02, 1)
if not self.mapImage or not self.player then
G.setColor(0.85, 0.57, 0.13, 1)
G.printf("POKEMON GOLD", 0, math.floor(h * 0.38), w, "center")
local silver = self:gsVersion() == 1
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.printf(self.status or "No map.",
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
-- (or the F3 toggle) shows it, a normal boot does not.
if self.showDebugHud then
G.setColor(0.85, 0.57, 0.13, 1)
G.printf("POKEMON GOLD", 0, 10, w, "center")
local silver = self:gsVersion() == 1
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)
local label = string.format("%s (%d,%d) %s · %s · zoom %s",
self.map.id, p.cellX, p.cellY, p.facing,
@@ -41,12 +41,21 @@ end
-- Mock isReady
RomImporter.isReady = function(v)
return v == "red" or v == "gold" or v == "blue" or v == "yellow"
or v == "silver"
end
local ok = RomImporter.syncAndroidShortcuts("gold")
check(ok == true, "syncAndroidShortcuts returns true on Android")
check(#capturedShortcuts == 4, "syncAndroidShortcuts caps at 4 items")
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)
RomImporter.isReady = function(v)
@@ -78,4 +87,4 @@ end
love.system.getLaunchGame = savedGetLaunchGame
love.system.updateShortcuts = nil
print("8/8 checks passed (android_shortcuts_payload_test)")
T.finish("android_shortcuts_payload_test")
+1
View File
@@ -23,6 +23,7 @@ T.eq(GameVersion.generation("red"), 1, "Red is Gen 1")
T.eq(GameVersion.generation("blue"), 1, "Blue is Gen 1")
T.eq(GameVersion.generation("yellow"), 1, "Yellow is Gen 1")
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
+45 -34
View File
@@ -39,45 +39,54 @@ end
local edited = 0
local hooks = { editTouchControls = function() edited = edited + 1 end }
local gold = LauncherSettings.open(hooks, "gold")
check(has(gold, "TOUCH PAD"), "Gold's gear offers TOUCH PAD")
check(has(gold, "VIBRATION"), "and VIBRATION")
check(has(gold, "TOUCH CONTROLS"), "and the layout editor")
check(has(gold, "VOID FILL"), "and VOID FILL")
for _, version in ipairs({ "gold", "silver" }) do
local model = LauncherSettings.open(hooks, version)
check(has(model, "TOUCH PAD"), version .. " gear offers TOUCH PAD")
check(has(model, "VIBRATION"), version .. " and VIBRATION")
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")
eq(voidFill.value(), "FADE ", "VOID FILL defaults to FADE")
voidFill.step(1)
eq(gold.opts.gold.voidFill, "water", "right stores water in the gold block")
eq(voidFill.value(), "WATER", "and the row reads WATER")
voidFill.step(-1)
eq(gold.opts.gold.voidFill, "fade", "left restores fade")
local voidFill = findRow(model, "VOID FILL")
eq(voidFill.value(), "FADE ", version .. " VOID FILL defaults to FADE")
voidFill.step(1)
eq(model.opts.gold.voidFill, "water",
version .. " right stores water in the gold block")
eq(voidFill.value(), "WATER", version .. " and the row reads WATER")
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
-- Red's, and Gold's boot never reads them (src/core/gen2/Save.lua:299).
-- loadOptions seeds the flat Gen 1 defaults, so the check is that the Gold
-- rows leave them exactly as they found them.
local flatPad = gold.opts.touchControls
local flatBuzz = gold.opts.haptics
-- Every write has to land in the gen2 block: the flat keys beside it are
-- 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 Gen 2
-- rows leave them exactly as they found them.
local flatPad = model.opts.touchControls
local flatBuzz = model.opts.haptics
local pad = findRow(gold, "TOUCH PAD")
local before = pad.value()
pad.step(1)
check(pad.value() ~= before, "stepping TOUCH PAD flips it")
check(type(gold.opts.gold) == "table", "into the gold block")
eq(gold.opts.gold.touchControls.enabled, false, "which now carries enabled")
eq(gold.opts.touchControls, flatPad, "leaving the flat Gen 1 key alone")
local pad = findRow(model, "TOUCH PAD")
local before = pad.value()
pad.step(1)
check(pad.value() ~= before, version .. " stepping TOUCH PAD flips it")
check(type(model.opts.gold) == "table", version .. " into the gold block")
eq(model.opts.gold.touchControls.enabled, false,
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 buzzBefore = buzz.value()
buzz.step(1)
check(buzz.value() ~= buzzBefore, "stepping VIBRATION moves the level")
eq(gold.opts.gold.haptics, TouchControls.normalizeHaptics(gold.opts.gold.haptics),
"VIBRATION stores a level the shared module knows")
eq(gold.opts.haptics, flatBuzz, "also without touching Red's")
local buzz = findRow(model, "VIBRATION")
local buzzBefore = buzz.value()
buzz.step(1)
check(buzz.value() ~= buzzBefore, version .. " stepping VIBRATION moves the level")
eq(model.opts.gold.haptics,
TouchControls.normalizeHaptics(model.opts.gold.haptics),
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()
eq(edited, 1, "the editor row reaches the host hook")
edited = 0
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 flat table.
@@ -94,6 +103,8 @@ eq(findRow(red, "TOUCH PAD").value() ~= nil, true, "and reading it back")
-- rather than dead, on both sides.
eq(has(LauncherSettings.open(nil, "gold"), "TOUCH CONTROLS"), false,
"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,
"nor on Red")
-- The Edit row hands the screen to the host, and the host has to know WHICH
+41 -38
View File
@@ -155,28 +155,34 @@ modImp.mods = mods
modImp._ensureMods = function() return mods end
LauncherView.draw(modImp)
LauncherView.draw(modImp)
check((modImp._modScrollMax or 0) > 0,
"60 mods overflow the list viewport inside the panel")
local list = modImp._modListRect
check(list.x + list.w
<= modImp._tabRegionRect.x + modImp._tabRegionRect.w - Kit.scrollBarW(),
"the rows stop short of the region's scrollbar gutter")
pointer(list.x + 10, list.y + 10)
local reg = modImp._tabRegionRect
check((modImp._tabScrollMax.mods or 0) > reg.h,
"60 installed mods are ONE continuous list: the region's travel spans "
.. "the whole list, not one page of it")
eq(modImp._pages.mods, nil, "and no page state is ever minted for it")
pointer(reg.x + 10, reg.y + 40)
modImp._wheelY = -1
LauncherView.draw(modImp)
check((modImp.modScroll or 0) > 0, "a notch over the mod list scrolls the list")
eq(modImp._tabScroll.mods or 0, 0, "not the panel region around it")
eq(modImp._pageScroll, 0, "and not the page behind that")
check((modImp._tabScroll.mods or 0) > 0,
"a notch over the region scrolls the list like any other tab")
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"
local shielded = modImp.modScroll
local shieldedPage = modImp._pageScroll
pointer(list.x + 10, list.y + 10)
local shieldedAt = modImp._tabScroll.mods
modImp._wheelY = -1
LauncherView.draw(modImp)
eq(modImp.modScroll, shielded, "a shielded mod list ignores the notch")
eq(modImp._tabScroll.mods or 0, 0, "and so does the region under the scrim")
eq(modImp._pageScroll, shieldedPage, "and the page behind that")
eq(modImp._tabScroll.mods, shieldedAt,
"a shielded mod list ignores the notch under the scrim")
modImp._modActions = nil
modImp._wheelY = 0
LauncherView.draw(modImp)
@@ -225,32 +231,27 @@ dragMods.mods = mods
dragMods._ensureMods = function() return mods end
LauncherView.draw(dragMods)
LauncherView.draw(dragMods)
local dlist = dragMods._modListRect
local dListMax = dragMods._modScrollMax
local dreg = dragMods._tabRegionRect
local dRegionMax = dragMods._tabScrollMax.mods
check(dListMax > 0 and dRegionMax > 0,
"the mods tab has both an inner list and a region to scroll")
LauncherView.touchpressed(dragMods, 7, dlist.x + 20, dlist.y + 30)
LauncherView.touchmoved(dragMods, 7, dlist.x + 20, dlist.y + 30 - 60)
eq(dragMods.modScroll, math.min(60, dListMax),
"the first pixels of the drag move the list")
eq(dragMods._tabScroll.mods or 0, 0, "and nothing else")
LauncherView.touchmoved(dragMods, 7, dlist.x + 20,
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(dRegionMax > 0, "the mods region scrolls its overscan like any tab")
LauncherView.touchpressed(dragMods, 7, dreg.x + 20, dreg.y + 30)
LauncherView.touchmoved(dragMods, 7, dreg.x + 20, dreg.y + 30 - 60)
eq(dragMods._tabScroll.mods, math.min(60, dRegionMax),
"a drag moves the region by the finger's travel")
LauncherView.touchmoved(dragMods, 7, dreg.x + 20,
dreg.y + 30 - 60 - dRegionMax * 2)
eq(dragMods._tabScroll.mods, dRegionMax, "carrying on saturates the region")
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 } }
for i = 2, 12 do
dragMods._skins[i] = { id = "s" .. i, source = "user", controls = 8, pages = 1 }
end
dragMods._ensureSkins = function() return dragMods._skins end
dragMods.modScroll = 0
local heldModScroll = dragMods.modScroll
local overList = dlist.y + 30
local heldModsPage = dragMods._pages.mods or 1
local heldModsAt = dragMods._tabScroll.mods
local overList = dreg.y + 60
dragMods:_switchTab("skins")
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)
check((dragMods._tabScroll.skins or 0) > 0,
"a drag on the skins tab scrolls the skins tab")
eq(dragMods.modScroll, heldModScroll,
"and leaves the mod list where the player parked it")
eq(dragMods._pages.mods or 1, heldModsPage,
"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)
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,
"the panel dispatch opens a scroll region")
check(view:find("Kit.scrollEnd(", 1, true) ~= nil, "and closes it")
check(view:find("modListWantsWheel", 1, true) ~= nil,
"the nested mod list is asked before the region takes a notch")
check(view:find("modListWantsWheel", 1, true) == nil,
"no nested list steals the wheel from the region any more")
check(view:find("start.region", 1, true) ~= nil,
"a touch drag that began in the region scrolls the region")
check(view:find("Kit.scrollGutter(", 1, true) ~= nil,
+12 -8
View File
@@ -32,11 +32,13 @@ do
"a version id names exactly that game")
eq(table.concat(ModTargets.expand("GEN1"), ","), "red,blue,yellow",
"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")
eq(table.concat(ModTargets.expand("silver"), ","), "silver",
"and each of them names itself")
eq(table.concat(ModTargets.expand("all"), ","),
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(7), nil, "a non-string token is not a game")
end
@@ -56,7 +58,7 @@ end
do
eq(list(mf({})), "red,blue,yellow",
"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")
eq(mf({}).gen2compat, false, "and the derived flag agrees")
eq(mf({ gen2compat = true }).gen2compat, true, "both ways")
@@ -67,14 +69,14 @@ end
do
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")
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" } })
eq(list(one), "blue", "or one single game")
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")
end
@@ -224,10 +226,12 @@ do
local bad = ModProfile.decode(require("src.core.SaveSerializer").encode({
format = "g1rmodlist", formatVersion = 1,
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.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")
end
@@ -59,7 +59,7 @@ package.loaded["src.import.RomImporter"] = nil
RomImporter = require("src.import.RomImporter")
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
for _, name in ipairs(love.filesystem.getDirectoryItems(dir) or {}) do
love.filesystem.remove(dir .. "/" .. name)
@@ -137,6 +137,8 @@ check(createdDirs["imports/saves/yellow"] == true,
"RES-01: ensureSavesInboxDir creates imports/saves/yellow/")
check(createdDirs["imports/saves/gold"] == true,
"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
ri = freshImporter()
+4 -2
View File
@@ -81,7 +81,8 @@ local function importer(ready)
}, RomImporter)
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()
eq(allReady.baseRomScan.state, "done", "ready launcher skips discovery")
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")
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:reimport("red")
check(rescanned.baseRoms.red == nil, "re-import clears the detected ROM")
+6
View File
@@ -31,10 +31,14 @@ require("src.core.Logger").warn = function() end
local Core = require("src.core.gen2.HallOfFame")
local Credits = require("src.ui.gen2.Credits")
local GameVersion = require("src.core.GameVersion")
local HallOfFame = require("src.ui.gen2.HallOfFame")
local Save = require("src.core.gen2.Save")
local Screens = require("src.ui.Screens")
local priorVersion = GameVersion.get()
GameVersion.set("gold")
-- ---- fixtures -------------------------------------------------------------
local POKEMON = {
@@ -711,4 +715,6 @@ else
eq(creditsData.palettes[1][1][1], 255, "and set 1 colour 0 is white")
end
GameVersion.set(priorVersion)
S.finish()
+2
View File
@@ -76,6 +76,8 @@ local function makeWorld()
local game = {
data = { audio = {}, screens = registry },
save = {
version = "gold",
generation = 2,
player = { name = "GOLD" },
party = { { species = "TYPHLOSION", level = 50, hp = 120,
otId = 33333 } },
+55 -1
View File
@@ -28,6 +28,9 @@ love.filesystem = {
local GameVersion = require("src.core.GameVersion")
local Save = require("src.core.gen2.Save")
local priorVersion = GameVersion.get()
GameVersion.set("gold")
local failures, checks = 0, 0
local function check(name, got, want)
checks = checks + 1
@@ -48,6 +51,11 @@ check("main file", main, "save_gold.lua")
check("backup file", backup, "save_gold.lua.bak")
check("staged file", tmp, "save_gold.lua.tmp")
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.
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
-- the first screen that reads it.
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("inventory exists", type(sparse.inventory), "table")
check("pokedex seen exists", type(sparse.pokedex.seen), "table")
@@ -478,6 +492,46 @@ do
files = {}
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))
-- 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
+35 -21
View File
@@ -71,13 +71,17 @@ local data = {
maps = {},
}
local function newState()
local function newState(version)
version = version or "gold"
local prior = GameVersion.get()
GameVersion.set(version)
local S = State.new()
S.data = data
S.cat = Catalog.build(data)
S.save = Save2.newGame()
S.version = "gold"
S.version = version
Gen.ensureBoxes(S.save)
GameVersion.set(prior)
return S
end
@@ -85,10 +89,19 @@ do
GameVersion.set("gold")
eq(Gen.of({ generation = 2 }), 2, "Gen.of generation field")
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(Save2.newGame()), 2, "Gen.of gold newGame")
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
local S = newState()
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")
end
do
local S = newState()
for _, version in ipairs({ "gold", "silver" }) do
local S = newState(version)
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]
eq(mon.species, "CYNDAQUIL", "first catalog species")
check(mon.experience ~= nil, "gold mon has experience")
check(mon.exp == nil or mon.experience ~= nil, "does not rely on gen1 exp")
eq(mon.species, "CYNDAQUIL", version .. " first catalog species")
check(mon.experience ~= nil, version .. " mon has experience")
check(mon.exp == nil or mon.experience ~= nil,
version .. " does not rely on gen1 exp")
check(mon.stats.specialAttack and mon.stats.specialDefense,
"gold stats have spa/spd")
check(mon.happiness ~= nil, "gold mon has happiness")
eq(mon.ot, S.save.player.name, "stampOT copies player name")
version .. " stats have spa/spd")
check(mon.happiness ~= nil, version .. " mon has happiness")
eq(mon.ot, S.save.player.name, version .. " stampOT copies player name")
Ops.setLevel(S, mon, 20)
eq(mon.level, 20, "setLevel 20")
check(mon.experience > 0, "experience resynced")
eq(mon.level, 20, version .. " setLevel 20")
check(mon.experience > 0, version .. " experience resynced")
Ops.setHappiness(S, mon, 200)
eq(mon.happiness, 200, "happiness 200")
eq(mon.happiness, 200, version .. " happiness 200")
Ops.setPokerus(S, mon, 15)
eq(mon.pokerus, 15, "pokerus byte")
eq(mon.pokerus, 15, version .. " pokerus byte")
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")
eq(mon.species, "TOTODILE", "setSpecies id")
eq(mon.name, "TOTODILE", "setSpecies rewrites the Gold display name")
check(mon.nickname == nil, "setSpecies does not invent a nickname")
eq(mon.types[1], "WATER", "setSpecies rewrites copied types")
eq(mon.species, "TOTODILE", version .. " setSpecies id")
eq(mon.name, "TOTODILE", version .. " setSpecies rewrites the display name")
check(mon.nickname == nil, version .. " setSpecies does not invent a nickname")
eq(mon.types[1], "WATER", version .. " setSpecies rewrites copied types")
end
do
+10
View File
@@ -149,12 +149,22 @@ local ok, err = pcall(function()
local Ops = require("Ops")
check(not ModTargets.supports({}, "gold"),
"legacy gen1-only fixture does not support gold")
check(not ModTargets.supports({}, "silver"),
"nor silver")
check(not ModTargets.supports({ games = { "red" } }, "gold"),
"explicit gen1 games list does not support gold")
check(not ModTargets.supports({ games = { "red" } }, "silver"),
"nor silver")
check(ModTargets.supports({ games = { "gold" } }, "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"),
"gen2compat legacy still supports gold")
check(ModTargets.supports({ gen2compat = true }, "silver"),
"and silver with it")
local goldS = {
data = {
+2 -1
View File
@@ -25,6 +25,7 @@ SHELL_COLORS = {
"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)},
"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():
@@ -186,7 +187,7 @@ def main():
fg = create_adaptive_foreground(sizes["adaptive"])
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.save(os.path.join(drawable_dir, f"ic_shortcut_{ver}.png"), "PNG")
+94
View File
@@ -0,0 +1,94 @@
#!/usr/bin/env python3
"""Derive the Pokemon Silver import manifest from the shipped Gold manifest.
Gold and Silver are assembled from one pokegold source tree; every constant
block, charmap entry, map id, and symbol NAME is identical between the two
builds, and the edition differences (wild encounter tables, the alternate
front pics in gfx/pics_silver.asm, the title screen art, preset player
names, a handful of map texts) are all ROM data that the importer decodes
from the Silver cart at import time. So rather than re-parsing pokegold
from scratch like tools/make_gold_manifest.py does, this takes the shipped
Gold manifest verbatim and overrides only what genuinely differs:
* romSha1 -- Silver's ROM hash (pret/pokegold's byte-exact build).
* symbols -- every symbol re-resolved from pokesilver.sym, because the
edition-selected data blocks have different sizes and shift
their neighbours (e.g. the compressed front pics).
Usage: python3 tools/make_silver_manifest.py
Default paths: Gold manifest beside this script; symbols at
/Users/bryanbassett/Documents/development/pokegold-symbols/pokesilver.sym
(a pokegold checkout's own `make silver` build emits an identical one).
"""
from __future__ import annotations
import argparse
import copy
import json
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from rom_data import CANONICAL_SILVER_SHA1, SymbolTable # noqa: E402
DEV = "/Users/bryanbassett/Documents/development"
DEFAULT_GOLD = os.path.join(os.path.dirname(__file__), "rom_manifest_gold.json")
DEFAULT_OUT = os.path.join(
os.path.dirname(__file__), "rom_manifest_silver.json")
DEFAULT_SYMBOLS = os.path.join(DEV, "pokegold-symbols/pokesilver.sym")
def derive(gold, symbols_path):
"""Return the Silver manifest derived from the Gold manifest dict."""
silver = copy.deepcopy(gold)
silver["romSha1"] = CANONICAL_SILVER_SHA1
silver_symbols = SymbolTable(symbols_path)
resolved, missing = {}, []
for name in gold["symbols"]:
symbol = silver_symbols.by_name.get(name)
if symbol is None:
missing.append(name)
continue
resolved[name] = [symbol.bank, symbol.address]
if missing:
raise SystemExit(
"pokesilver.sym is missing symbols the manifest needs: "
+ ", ".join(sorted(missing)[:10])
+ (" ..." if len(missing) > 10 else ""))
silver["symbols"] = resolved
# Sanity: the edition pic banks must actually have moved something. If
# every address matches Gold's, the --symbols file is almost certainly
# pokegold.sym, and the import would decode Gold data out of a Silver ROM.
if all(resolved[n] == gold["symbols"][n] for n in resolved):
raise SystemExit(
f"{symbols_path} resolves every symbol to Gold's address; "
"is it really pokesilver.sym?")
return silver
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--gold", default=DEFAULT_GOLD,
help="shipped Gold manifest to derive from")
parser.add_argument("--symbols", default=DEFAULT_SYMBOLS,
help="pokesilver.sym symbol file")
parser.add_argument("--out", default=DEFAULT_OUT)
args = parser.parse_args()
with open(args.gold, encoding="utf-8") as f:
gold = json.load(f)
silver = derive(gold, os.path.abspath(args.symbols))
with open(args.out, "w", encoding="utf-8", newline="\n") as f:
json.dump(silver, f, ensure_ascii=False, indent=2, sort_keys=True)
f.write("\n")
print(f"wrote {args.out}")
if __name__ == "__main__":
main()
+3 -2
View File
@@ -741,7 +741,7 @@ def imported_data_dir(repo):
return path if _generated_data_dir_ok(path) else None
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"
candidates = [
@@ -2404,7 +2404,8 @@ UPVALUE_CALL = re.compile(
UPVALUE_ARGS = re.compile(
r"""^\s*([A-Za-z_]\w*)\s*\.\s*(\w+)\s*,\s*["'](\w+)["']""")
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):
+2 -1
View File
@@ -11,8 +11,9 @@ from dataclasses import dataclass
CANONICAL_RED_SHA1 = "ea9bcae617fdf159b045185467ae58b2e4a48b9a"
CANONICAL_BLUE_SHA1 = "d7037c83e1ae5b39bde3c30787637ba1d4c48ce2"
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_SILVER_SHA1 = "49b163f7e57702bc939d642a18f591de55d92dae"
ROM_BANK_SIZE = 0x4000
File diff suppressed because it is too large Load Diff