diff --git a/README.md b/README.md index 4cd31e7c0..314902878 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ items, and the Gold/Silver title sequence. It has since grown well past Generation II. The same launcher now imports and runs **FireRed** and **Emerald** (Gen 3), **Platinum**, **HeartGold** and **SoulSilver** (Gen 4), and **Black** and **White** (Gen 5), plus the -**Polished Crystal** and **Prism** romhacks — all side by side, all from your +**Polished Crystal**, **Prism** and **Renegade Platinum** romhacks — all side by side, all from your own cartridge dumps. Around the games sit the **Monster Walker** (a next-gen Pokéwalker with minigames and a Watt Shop), a **Map Editor**, a **Save Editor**, machine **translation** into 25 languages, **cloud saves**, @@ -80,10 +80,18 @@ revision) so you know why — see [docs/rom-revisions.md](docs/rom-revisions.md) | Emerald | GBA | Alpha | `f3ae088181bf583e55daf962a92bb46f4f1d07b7` | | Platinum (Rev 1) | DS | Pre-alpha | `0862ec35b24de5c7e2dcb88c9eea0873110d755c` | | Platinum (Rev 0) | DS | Pre-alpha | `ce81046eda7d232513069519cb2085349896dec7` | +| Platinum (Europe, English) | DS | Pre-alpha | `8d15cd443d4e83e20493c79d3f9dc408c4f51ae0` | +| Platin Edition (Germany) | DS | Pre-alpha | `5d009a175c77d4387cde26d34efb71aee825a938` | | HeartGold (USA) | DS | Pre-alpha | `4fcded0e2713dc03929845de631d0932ea2b5a37` | | SoulSilver (USA) | DS | Pre-alpha | `f8dc38ea20c17541a43b58c5e6d18c1732c7e582` | | Black (USA/EU) | DS | Pre-alpha | `26ad0b9967aa279c4a266ee69f52b9b2332399a5` | | White (USA/EU) | DS | Pre-alpha | `bc696a0dfb448c7b3a8a206f0f8214411a039208` | +| Renegade Platinum (over Platinum USA Rev 1) | DS | Pre-alpha | `381f9eef44564602a5f6469ee8c390fbb8ebac3a` | + +All four Platinum dumps import as the same game and share one save. The +German cartridge plays in German (its own text, names and localized art); the +European one is the US game with the European Game Corner. See +[docs/rom-revisions.md](docs/rom-revisions.md#platinum-europe-and-germany). HeartGold, SoulSilver, Black and White dumps that have had their trailing `FF` padding trimmed are also accepted, as long as every game byte is intact — see @@ -123,6 +131,10 @@ Everything below arrived since this README was last updated. including Professor Juniper's intro in BW — as **PRE-ALPHA** builds. - **Polished Crystal** (beta) and **Prism** (experimental) romhacks, with audio, overworld rendering and feature-parity fixes. +- **Renegade Platinum** (pre-alpha) imports as its own game on the Platinum + engine. It has the hack's species, moves, trainers and encounters, the + Fairy type (its chart, names and icons) and its 1/512 shiny odds. See + [docs/renegade-platinum.md](docs/renegade-platinum.md). - **Mass ROM import**: pick a folder's worth of dumps at once. ### Monster Walker diff --git a/docs/controller.md b/docs/controller.md new file mode 100644 index 000000000..2ec074b71 --- /dev/null +++ b/docs/controller.md @@ -0,0 +1,61 @@ +# Xbox controller (Gen 4 and the launcher) + +SDL game-controller mapping through `love.gamepad*` (an "Xbox One For Windows" +pad reports as a gamepad; the triggers arrive as axes and are edge-detected at +0.5 / 0.3 in `Input.AXIS_BUTTONS`). Raw `joystick*` events are used only for +sticks SDL has no mapping for. + +| Xbox | Action | Gen 4 meaning | +|---|---|---| +| A | A | confirm / talk / tap (cursor mode) | +| B | B | cancel; hold to run in the field | +| Y (north) | X | field start menu (DS X) | +| X (west) | Y | registered key item (DS Y) | +| Start | Start | start menu | +| Back/View | Select | Select (reorder moves / bag items, Pokétch clear) | +| LB | L | raises the Pokétch from the field (L or B lowers it) | +| RB | R | next Pokétch app; camera tilt in the field | +| D-pad / left stick | directions | move, menus (held = repeat) | +| RT | TOUCH | toggle the lower-screen cursor (below) | +| LT | CHAT | online chat only | +| L3 / R3 | zoom out / in | field zoom | + +All rows are rebindable in the CONTROLS screen (pad capture works for buttons, +triggers and right-stick directions; the TOUCH row is new). + +## Left stick + +Press at 0.5, release under 0.3. Eight-way input maps to four directions with +a 1.3x diagonal bias: a held direction keeps its axis until the other axis is +clearly stronger, so a 45-degree wobble does not flip between two directions. +With two controllers the one deflected furthest drives the stick; each pad has +its own button sources, so two pads holding a button are one press and the +button stays held until both release. + +## Lower-screen cursor (the DS touch screen) + +RT toggles a crosshair in the bottom screen's 256x192 space (dual-screen games +only). While it is on: left stick and D-pad move it (the stick is quadratic for +fine control), A presses and drags (hold A and move), and every other button +keeps its normal job (B still backs out). It maps through +`SecondScreen.fromLocal`, the inverse of what every screen's touch handler +already does, so it works in swap, inset, vertical/horizontal and `display` +modes and drives every pointer-only screen: Pokétch apps (drawing pad, +dowsing, calendar, calculator, roulette, move tester, party apps), the battle +bag/party touch screens, Underground, contest and photo dress-up, Poffin +cooking, mining, naming-screen touch keys. It switches itself off whenever the +top screen changes (so the stick can never stay stolen), and refuses to start +over the field when no lower screen is showing. + +Pokétch without the cursor: LB opens it, D-pad left/right or RB change app, A +presses the app's main button, B/L/Start close. + +## Hotplug + +`love.joystickadded` is forwarded (the touch overlay steps aside); removal +resets input and lifts the virtual finger. Controllers added or reconnected +mid-game send ordinary gamepad events. + +## Tests + +`python tools/run_lua_check.py tools/gen4_xbox_pad_check.lua` diff --git a/docs/gen4-platinum.md b/docs/gen4-platinum.md index 82af339fb..0169eacbe 100644 --- a/docs/gen4-platinum.md +++ b/docs/gen4-platinum.md @@ -106,6 +106,12 @@ Rev 0 (`ce81046eda7d232513069519cb2085349896dec7`) is registered as an alternate hash. pokeplatinum builds both and the filesystem layout is the same in each, which is what the extractor reads. +The English European cartridge (`8d15cd44…`, CPUE rev 10) and the German +Platin Edition (`5d009a17…`, CPUD) are alternates too. The German build moves +its 32 language archives under `/resource/ger/`, which +`src/import/Gen4Regions.lua` answers at the filesystem. See +[rom-revisions.md](rom-revisions.md#platinum-europe-and-germany). + ### Files that will matter | Path | Bytes | What | diff --git a/docs/platinum-parity-progress.md b/docs/platinum-parity-progress.md index eda68b8a3..4eb676138 100644 --- a/docs/platinum-parity-progress.md +++ b/docs/platinum-parity-progress.md @@ -2135,3 +2135,58 @@ it is the clock exclusion); nothing animating that is not on those lists, since the union only ever adds; no prop drawn twice, since twelve models have just changed pass; and a stutter check on a waterfall route or in Canalave, for the 14 extra chunks today and 41 after the flipbooks land. + +## Story playthrough pass (November 2026) + +The automated playthrough (`tools/gen4_playthrough/platinum.lua`, 78 beats from +the bedroom to the Hall of Fame) passed every beat on a USA Rev 1 cache before +this pass. The fixes below come from what the log showed while the beats still +passed — scenes that played to missing actors, no-op commands, stalls — and +from the beats added past the credits. Each is checked against pret's +pokeplatinum. + +- **Cynthia rides the Distortion World lifts.** 1F's lift (progress 2) and + B6F's (progress 7) carry Cynthia down with the player + (`DistWorldElevatorPlatform_BeginMovement` / `_ChangeMaps`): the same map + object, renumbered for the next floor (B1F 128; B7F 128 with script 6). The + next floor's object table has no Cynthia for that moment, so B1F's and B7F's + first-entry scenes moved a local id that was not there ("no object with + localId 128") and played to an empty platform. `Gen4DistPlatforms` now + takes the passenger at the start of the ride, lifts her with the platform, + and places her on the arrival floor at the world tile she rode on (B7F: she + then walks to exactly the tile B7F's own table gives her at progress 8). + Check: `tools/gen4_dist_surface_check.lua /data/generated` (53). +- **The playthrough now goes past the credits** (91 beats, all passing in one + unbroken run on USA Rev 1). New beats: Eterna Forest with Cheryl as the + partner (her two-on-two trainers, the Soothe Bell); the Elite Four and the + Champion walked room to room — out by the door each win removes, up each + lift room's `TriggerPlatformLift`, walked in by the next room's frame + script — instead of teleported into; the credits to the title and a + CONTINUE at the house 2F (Mom's post-game scene); the National Dex at + Rowan's lab (a cheat, said as one: the Sinnoh dex marked seen); the + S.S. Spiral (Cynthia's send-off) to the Fight Area; Barry, Palmer and Buck + there; Route 227 (Crasher Wake and Barry, Buck's patrol request); Stark + Mountain — the grunts, Mars and Jupiter, Buck as the partner through room 2 + (Rock Smash at (41,15), the Strength boulder at (29,12) pushed to (48,12), + his multi battles), Charon's arrest and Looker outside; the Battleground's + Buck; Heatran. Trainer-defeated flags are read from `save.gen4TrainerFlags` + (where the port keeps TRAINER_DEFEATED_FLAGS_START + id), not `save.flags`. +- **`PT_CONTINUE=1`** (tools/gen4_playthrough/runner.lua): save and continue + between every two beats — the field's `captureSave`, the save through the + codec, and `Game:restoreSave` as the title's CONTINUE — so each beat runs on + what a reload brings back. Off by default; shared by every game's beats. +- **Platinum Pokémon have a met location again.** The cartridge stamps a + caught, given or hatched Pokémon with the map's label + (`MapHeader_GetMapLabelTextID`, a place-name bank 433 index; egg_hatch.c, + field_battle_data_transfer.c). The port passed `BattleState.metHere`, a + Gen 3 region-map section that is nil on every Gen 4 map, so every Platinum + Pokémon was met nowhere: the Hall of Fame at the end of the story printed + "Met at ." and the summary left the line out. `Gen4Origin.stamp` now takes + the label of the map the player is on (the live field map — `save.player.map` + is only written by a save, so it named wherever the last save was: the + starter came out "met in Twinleaf Town"), and the Hall of Fame and the + summary name a numeric location from bank 433 (which also names the + locations NDS-imported Pokémon carry). HeartGold/SoulSilver, whose place + names are another bank, are left as they were. The starter now reads + "Route 201, Lv. 5". Check: `tools/gen4_met_location_check.lua + [/data/generated]` (13). diff --git a/docs/renegade-platinum.md b/docs/renegade-platinum.md new file mode 100644 index 000000000..8ebf0a5a9 --- /dev/null +++ b/docs/renegade-platinum.md @@ -0,0 +1,234 @@ +# Renegade Platinum + +Drayano's **Renegade Platinum** is a ROM hack of Pokemon Platinum (USA) Rev 1. +It imports as its own version, `renegadeplatinum`. It has its own cache +(`renegadeplatinum/`), its own saves (`save_renegadeplatinum.lua`) and its own +launcher card ("RP", under *Pokemon ROM hacks*). It runs on the Platinum +engine: `GameVersion.family("renegadeplatinum") == "platinum"`, and +`GameVersion.isPlatinumFamily()` is what Platinum-only paths check. + +Stage: **PRE-ALPHA**, the same as Platinum. It imports, boots to the field +and fights battles, but nobody has played it through. + +## The accepted file + +| | | +|---|---| +| SHA-1 | `381f9eef44564602a5f6469ee8c390fbb8ebac3a` | +| Size | 104,922,516 bytes (trimmed) | +| Header | `POKEMON PL`, game code `CPUE`, revision 1 (the base cartridge's) | +| Base | Platinum (USA) Rev 1, `0862ec35b24de5c7e2dcb88c9eea0873110d755c` | + +The header is the retail one, so the hack can only be told apart by its hash. +A different build of the hack has a different hash. The launcher then +describes it as a modified Platinum dump, and it does not import. + +## What the hack changes, measured against Platinum Rev 1 + +Filesystem: the paths are all the same. One DP leftover, +`/poketool/personal/personal.narc`, is gone, and nothing reads it. The +archives that differ: + +| Archive | Change | Handled by | +|---|---|---| +| `pl_personal.narc` | 507 of 508 records (stats, types, abilities) | the species stage, unchanged | +| `wotbl.narc`, `evo.narc` | learnsets (503), evolutions (40) | unchanged | +| `pl_waza_tbl.narc` | 117 moves (type, power, new Fairy moves in old slots) | unchanged | +| `pl_item_data.narc` | 108 items | unchanged | +| `pl_enc_data.narc` | 118 of 183 encounter tables | unchanged | +| `trdata.narc` / `trpoke.narc` | 928 -> **1066** trainers, 698 parties rewritten | already sized by the archive; `constants.trainerOrder` now is too | +| `scr_seq.narc`, `zone_event.narc` | 215 scripts, 163 event files | unchanged (no new script commands: the ARM9 command table is untouched) | +| `pl_msg.narc` | 222 banks, trainer names 928 -> 1201 | unchanged | +| `pl_poke_icon.narc` | 539 icons redrawn | see *Icon palettes* | +| `pl_batt_obj.narc` | `type_icons/mystery` redrawn as FAIRY, `type_icons/shared.NCLR` | see *Fairy* | +| `zukan.narc` | a 19th Pokedex type icon | see *Fairy* | + +Code: the ARM9 differs in 266 bytes, and overlays 11, 16, 21 and 73 differ. +No overlay was added and none changed its load address. + +## Fairy + +Renegade turns the unused `???` type (slot 9) into **FAIRY**. Everything the +extractor reads agrees on that: + +- The type-name bank (624) says `FAIRY` at 9. `Gen4Versions.TYPE_SLOTS` + renames slot 9 in `Gen4TypeChart.TYPES` for this version. Species types, + move types and the chart's matchups are therefore written as `"FAIRY"`, not + as `"MYSTERY"` (Gen4Damage treats `MYSTERY` as typeless and deals it no + damage). +- 20 species carry the type: the Clefairy, Jigglypuff, Togepi, Marill, + Snubbull and Ralts lines, Mr. Mime, Mime Jr. and Mawile. 8 moves carry it: + Play Rough, Draining Kiss, Sweet Kiss, Charm, Moonlight, Dazzling Gleam, + Disarming Voice and Moonblast. +- **The chart moved.** Overlay 16 still holds Platinum's 110-row chart, but + every literal that pointed at it (`0x0226ECD4/5/6`) now points at + `0x021E9F80/1/2`. That address is a dense 18x18 chart, Fairy row and + column included, which the hack appended to overlay 21. The chart also has + Gen 6's Steel changes, so Ghost and Dark now hit Steel neutrally. + `Gen4TypeChart.follow` handles this generically. If the overlay no longer + references the chart `find` located, it resolves the `X, X+1, X+2` literal + triple, parses there (1x rows allowed) and drops the neutral rows. On + retail cartridges the pointer is still present, so nothing changes. +- Icons: the battle and summary extractors also save the slot-9 member under + the slot's name (`type_icons_fairy`, `type_fairy`). The Pokedex maps FAIRY + to the hack's new 19th type-icon sequence (18), as the hack's overlay 21 + switch does. +- Move buttons: overlay 11's `sMovePaletteTable` entry for slot 9 is + re-coloured. It is read by type id, so Fairy moves get the hack's colours + without any code change. +- The AI: `Gen4AI.TYPE.FAIRY = 9` resolves the chart's names to ids. + Hidden Power still skips slot 9. The hack changes nothing in overlay 16 + except the chart pointers, so its Hidden Power can never be Fairy either. + +Verified in the battle harness: Moonblast against Dragonair is "super +effective", and Dragon Pulse against Clefairy "doesn't affect" it. + +## Other ARM9 / overlay changes and how each is handled + +| Where | What | Status | +|---|---|---| +| ARM9 `0x02075E5x` | `Pokemon_IsPersonalityShiny` compares against 128, not 8 (1/512) | extracted as `constants.shinyUnder` (found by its instruction run); `Stats.setShinyUnder` applies it | +| ARM9 `sPokemonIconPaletteIndex` | 86 icon palette assignments changed for the new icon art | `Gen4Icons.VERIFIED_BY_VERSION.renegadeplatinum`: the table is where Platinum keeps it, and 11 of the 14 anchors still hold | +| ARM9 `sGameCornerPrizeData` | the first prize (Silk Scarf) replaced; others re-priced | `Gen4GameCorner.SIGNATURE_ROW1` finds the table by rows 1..3 | +| ARM9 TM/HM table | several TMs changed | already found by its HM tail | +| ARM9 mart tables | Veilstone floors restocked | already found by shape (`martSpecialties`) | +| ARM9 map headers | flags / weather bytes on some maps | read at import, as before | +| Overlay 16 | the chart pointers, nothing else | handled (above) | +| Overlay 21 | the appended chart, the Pokedex type-icon switch | handled (above) | +| Overlay 11 | the Fairy move-button palette | read by type id | +| Overlay 73 | two literals change species 427 (Buneary) to 133 (Eevee) | the opening's own Pokemon (Rowan's Poke Ball), NOT Amity Square: `Gen4IntroScene.monSpecies` reads it into `gen4_intro.monSpecies` (written only when it is not Buneary) | + +## Trainers past the offset table + +The hack grows trdata to 1,066 trainers and rewrites 136 `trtbl` records IN +PLACE (blank placeholder lines of dummy trainers become real lines, retagged +with ids 928..1065), but `trtblofs` keeps its 928 entries (the file is +byte-identical to Platinum's). The cartridge's walk reads the offset of +trainer 928 from past the end of that file (zero padding), starts at record 0 +and stops at once, so by the letter of the walk those trainers are silent. +`Gen4TrainerMessages.all` therefore also scans the table for the runs of ids +PAST the offset table (`scanUncovered`; records of one trainer are +contiguous). A retail cartridge has no such ids and is unchanged. 135 +trainers get lines (e.g. the counterpart's Route 202 battle, trainers +928..933: "Wow! You're that strong already?"). A trainer with no line has no +row: `g4_print_trainer_dialogue` returns without printing, so neither a script +nor a battle end waits on it. It is not known whether the real hack shows +these lines (it reads the same padding); the cache carries what the hack +authored. + +## Other differences the survey found + +- Route 202: the catching tutorial (`startcatchingtutorial`) is gone. The + coordinate trigger runs a real trainer battle against the counterpart + (trainers 928..933, level 9 against a level-5 starter), so a lone starter + loses and blacks out. The playthrough beat trains the lead up first. +- Jubilife: the Pokétch clowns' quizzes are removed (persons 24 and 25 are + parked at 0,0 without a script); the president gives the Pokétch himself. + +## The story, as the hack changes it + +All of these are script and event-data edits; the port runs them with no +engine change. `tools/gen4_playthrough/platinum.lua` walks each one when +`VERSION=renegadeplatinum` (retail's beats are untouched). + +- Sandgem: Rowan's first lab scene enables the **National Dex** at once + (`getsetnationaldexenabled 1`), so Mom's post-game scene takes its National + Dex branch (flag 0x15C, not 0x15D). +- Floaroma Meadow: the two grunts are one double battle (296 + 297) that + checks `getpartycount` and pushes a party of one back. +- Pastoria: the Gym Guide (person 29, flag 0xA08) stands on Barry's coord + cell in front of the gym; Crasher Wake sings on Route 212 south (R212B + coord (10..13,5), 0x406E): meeting him sets 0xA08 and clears the way. +- Route 218 gate: the Pokédex-upgrade scientist is gone; a thirsty + policeman's coord trigger ((2,5..9), 0x406C) turns the player back unless + the bag holds the **Tea** (item 456, the old S.S. Ticket slot). The Tea is + Pal Park's: talk to Prof. Oak (flag 0xA0B), then Darach battles the player + (trainer 969) and Caitlin hands it over (flag 0xA0C). +- Iron Island: Riley at the entrance battles the player (trainer 970); + HM04 Strength comes after his tag battle inside (D24R0105, 0x4092 = 2). +- Mt. Coronet 2F, Victory Road 2F and Stark Mountain's second room: the + Strength boulders and Rock Smash rocks are parked at (999,999). +- Victory Road 1F: a collector and a coord trigger ((37..39,18), 0x403E) + hold the way north until the counterpart is beaten on **Route 224**, which + is open before the League (talk twice, battle 975..977 by starter; flag + 0xA12). The hack also puts two nurses inside Victory Road. +- Route 227: Buck moved to (22,1); his trigger ((22,2..4), 0x403D) wants the + six **Battle Zone leaders**' unofficial badges first (Heather R225 0x8C9, + Wyver W226 0x8CA, Slythe R227 0x8CB, Shade R228 0x908, Gene R229 0x909, + Luna W230 0x8CE), then he battles (trainer 1023) and leaves (flag 0xA18). + Retail's patrol question (0x4119) is unreferenced. +- Battleground: the room holds the gym leaders; its frame table sets + FLAG_TALKED_TO_BATTLEGROUND_BUCK (0x125) as the player walks in. +- Trainers are much stronger throughout (full level-appropriate teams with + coverage, e.g. Volkner's two Rotom, Jupiter's Sableye). +- Evolutions: 40 records change (trade evolutions gain a level, Eevee uses + Sun/Moon/Ice/Leaf stones, and about a dozen trade-with-item species can also evolve by + using the item, whose records the hack flags as party items); the cache + matches the cartridge's `evo.narc` record for record, and + `Gen45Evolution.itemTarget` evolves Poliwhirl with King's Rock, Rhydon with + the Protector, Eevee with the Sun/Moon/Ice/Leaf stone. +- Amity Square's accessory pools are ARM9 data (six 10-entry u16 pools at + `0x020EBF84` on Platinum) and are byte-identical in the hack, so the + hard-coded `PtMiscCommands.AMITY_POOLS` is right for both. +- Type logic that lives in code the hack leaves alone behaves as retail: the + Battle Hall's grid and opponent-type table (overlay 104 is identical), + Hidden Power (never Fairy), Judgment / Natural Gift (no item carries + Natural Gift type 9; there is no Pixie Plate), Camouflage, Weather Ball, + the Pokedex search type list and the Pokétch move tester (their overlays + are identical apart from the icon switch). Natural Gift's type name, + the Battle Factory's type counts and the Battle Hall's slot-9 cell + now read slot 9's name from the cache instead of assuming "???". + +## ARM9 and overlay differences, all accounted for + +The ARM9 differs in 136 runs; every one is inside a table the extractor finds +by content. Map headers `0x020E6BA0..0x020E9468` (weather / flags), the mart +specialty lists `0x020EBA19`, `0x020EBA66` (Veilstone floors), the Game +Corner prize table `0x020EC018` (first prize replaced, TM prizes replaced and +re-priced), the icon palette table `0x020F079D..0x020F096D`, the TM/HM list +`0x020F0C6C..0x020F0CAA`, and the shiny comparison `0x02075E50`. Overlays 11 +(Fairy move-button palette), 16 (chart pointers), 21 (appended chart, Pokedex +type-icon switch) and 73 (the intro Pokemon); no other overlay differs, so +overlay-resident tables (move tutors, starters, the Frontier, Pickup, Amity, +trainer prize money) read as in Platinum. Starters are Turtwig, Chimchar and +Piplup in both. + +## Not verified yet + +- The playthrough walks the whole story to Heatran (100 beats, retail's 91 + plus the hack's nine) with the runner's training cheat (a level-100 + Mewtwo team, healed between Victory Road's legs). Optional content the + hack adds (gift Pokémon, the Battleground's leaders, new item balls) is + not walked. +- Trainer Pokémon abilities. The hack reorders some ability pairs (Toxicroak + is DRY_SKIN first). The cartridge builds a trainer mon's personality from + a seed (`TrainerData_BuildParty`: dv + level + species + trainer id, the + LCRNG run `class` times, `<< 8` plus 0x88 / 0x78), so its low bit, and + with it the ability slot, is always 0: every trainer Toxicroak has Dry + Skin. The port's `buildTrainerParty` (BattleState) still rolls a random + personality (nature, ability, gender) for both games, so half the time it + gets Anticipation. Not fixed here; it changes retail's battles too. +- Whether the real hack prints the lines of trainers 928..1065 (see above). +- Pickup, Pokemart common stock and the Frontier were not diffed at the data + level, only shown unchanged by the ARM9/overlay survey. + +## Checking it + +``` +POKEPORT_IDENTITY=pl_rp POKEPORT_VERSION=renegadeplatinum \ + POKEPORT_IMPORT_ROM= POKEPORT_IMPORT_ONLY=1 \ + POKEPORT_NO_BOOT_REPORT=1 lovec . +INSTALL=/renegadeplatinum VERSION=renegadeplatinum MAP=T01 \ + lovec tools/gen4_hgss_overworld_harness +POKEPORT_DATA_DIR=/renegadeplatinum/data/generated \ + POKEPORT_ASSET_ROOT=/renegadeplatinum VERSION=renegadeplatinum \ + SPECIES=148 LEVEL=30 PARTY=282 PARTYLEVEL=50 SLOT=2 TICKS=1500 PRESS=15 \ + MSGTRACE=1 lovec tools/gen4_battle_harness +python tools/run_lua_check.py tools/rom_revisions_check.lua +``` + +The battle harness's `SLOT=n` keeps the cursor on move *n*, so a chosen +matchup can be fought. + +A Platinum USA import made with these changes is byte-identical to one made +without them, apart from the new `constants.shinyUnder = 8`. diff --git a/docs/rom-revisions.md b/docs/rom-revisions.md index 843c10ba1..d696333f5 100644 --- a/docs/rom-revisions.md +++ b/docs/rom-revisions.md @@ -30,9 +30,10 @@ other revisions. | Crystal | Rev 0 (USA) `f4cd194b`, Rev 1 (USA, Europe) `f2f52230` | (Australia) BYTU `a0fc810f` | It is built with `_CRYSTAL_AU`, which rewrites the Celadon/Goldenrod Game Corner and Buena scripts (15 sites in 5 files). The map-script banks then no longer match `pokecrystal11.sym`, and no AU symbol table has been built. | | Prism | the one mapped release `75207669` | other Prism releases | Romhack tables were found one by one for this file | | Polished Crystal | 3.2.3 `6930b48a` | other releases | Same reason as Prism | +| Renegade Platinum | the trimmed patched Rev 1 file `381f9eef` (header CPUE rev 1) | other builds of the hack | Measured against this one file ([renegade-platinum.md](renegade-platinum.md)); a different build reads as a modified Platinum dump | | FireRed | Rev 0 (USA, Europe) `41cb23d8` | Rev 1 `dd5945db`, Switch Online build (header rev 10) `baa452d0` | See below | | Emerald | (USA, Europe) `f3ae0881` | none (one release) | n/a | -| Platinum | Rev 0 `ce81046e`, Rev 1 `0862ec35` (USA) | (Europe) CPUP | No dump or symbols to map it against | +| Platinum | Rev 0 `ce81046e`, Rev 1 `0862ec35` (USA); (Europe) CPUE rev 10 `8d15cd44`; (Germany) CPUD `5d009a17` | French, Italian, Spanish (CPUF/CPUI/CPUS) | No dump to map against; see below for how the German one was mapped | | HeartGold | (USA) IPKE `4fcded0e` | (Europe) IPKP | pokeheartgold builds the US version only | | SoulSilver | (USA, Australia) IPGE `f8dc38ea` | (Europe) IPGP | Same as HeartGold | | Black | (USA, Europe) IRBO `26ad0b99` | none known | n/a | @@ -41,9 +42,136 @@ other revisions. LeafGreen (`574fa542`, Rev 1 `7862c67b`, Switch `62b9fc77`) is recognised by hash and named, but this build does not import LeafGreen at all. -No revision moved from "refused" to "accepted" in this pass. Each candidate -either has no extractor mapping to back it (FireRed Rev 1, Crystal AU) or has -neither a dump nor a symbol table to check against (the European DS carts). +Platinum (Europe) and (Germany) moved from "refused" to "accepted" once the +user's own dumps were there to map against (next section). The rest still +either have no extractor mapping to back them (FireRed Rev 1, Crystal AU) or +have neither a dump nor a symbol table to check against (the other European +DS carts). + +## Platinum (Europe) and (Germany) + +Both import as `platinum`, into the same cache folder and with the same save +(`_platinum`). A save started on one cartridge's cache keeps working on +another's: the only language-bearing value a Gen 4 mon stores that the engine +computes with is its nature NAME, and `SaveData` re-derives an unknown one from +the personality (`personality % 25`) against the loaded cache. + +**(Europe), English, `8d15cd44`.** The header says CPUE, revision 10. The +ARM9 and all 122 overlays are byte-identical to US Rev 1, and so is every file +but five. Member by member: + +| File | Members | What | +|---|---|---| +| `/data/slot.narc` | 0, 8, 11, 21 | the Game Corner machines' art | +| `/demo/title/op_demo.narc` | 113 | the opening's legal overlay | +| `/fielddata/land_data/land_data.narc` | 164 | Route 225, four collision bytes | +| `/fielddata/script/scr_seq.narc` | 141 | the Veilstone Game Corner's script | +| `/msgdata/pl_msg.narc` | 143, 147, 366, 417, 544, 623 | 28 strings, mostly the Game Corner's | + +That is the European Game Corner: its machines are not slots. Its staff talk +about Coins and a TM campaign instead, and the script is the European one. The +importer needs nothing for this cartridge beyond its hash. + +**(Germany), Platin Edition, `5d009a17`.** This is a separate build: + +- The ARM9 is 416 bytes longer, and every overlay loads 0x1A0 or 0x1C0 higher. +- The 724 text banks keep the US bank numbers and hold German text. +- The 32 language-specific archives moved from paths like `/graphic/box.narc` + to `/resource/ger/box/box.narc`. The file name never changed. `op_demo.narc` + is three members shorter because it has no US legal overlay. + +What the extractor needed: + +- **The archive paths.** `tools/gen4_region_map_gen.py ` + pairs each US path with the one German file of the same name and writes + `src/import/Gen4RegionMap.lua`. A name with zero or two candidates stops the + tool; it does not guess. `NdsRom:index()` then lets the US path name the + German file, so no stage changed its paths. +- **The code tables.** No address map was needed. Every ARM9 and overlay table + the extractor reads is found by content (a signature, a structural search + or a known first row). Two were not, and both were fixed: + - The area-name popup read the map headers at the fixed US offset + `0xE601C` and got plausible garbage. It now takes the headers the maps + stage located. + - The staff roll's signature included a y coordinate and a row count of + 237. The German roll is spaced differently and has 224 rows. +- **English used as a key.** These were rekeyed to ids: + - Poketch app pictures and behaviour + - the starter cross-check + - TM/HM detection (German HMs are `VM01`) + - the Plate range + - the orb flag + - Repels and Flutes + - the Journal, Poke Radar, Town Map, Coin, Poffin and Seal Cases, Point Card + and Fashion Case + - the Underground's Mystery Zone test, which now uses the label text id + - Underground treasures into the bag + - the berry-soil bag pocket + - the first PC Potion + - GTS mail +- **File names.** Battle sprites are `001.png` rather than `001_bisasam.png` on + a non-English cartridge. + +`tools/gen4_region_check.lua ` proves the result. It +walks every generated table of both imports and sorts each difference into one +of four kinds: + +- **Text:** the cartridge's language. +- **Known:** listed in the tool with the reason. +- **Address:** where a table was found. +- **Data:** anything else. The check fails on any data difference. + +Both cartridges pass with zero data differences. The known ones are: + +- **Both cartridges:** + - the Route 225 collision fix + - the rewritten scripts. German Platinum also rewrites script files 31 + (Trainers' School), 148 (Veilstone Store), 367 and 376..379 (Battle Tower + and the Frontier facilities) and 500. + - the slot-machine art +- **German cartridge only:** + - the Pokedex A-Z order and its initial-letter groups + - the wallpaper-password word list + - the in-game trades' language field + - the title logo + - the missing legal overlay + - the staff roll + - overlay words the trainer-AI reader keeps past its script. They are RAM + pointers, so they move. + +The art with baked-in text is German as well. That covers the summary, Pokedex, +party, box, bag, trainer card, naming, contest and touch screens, the type +icons, the "KP" healthbox, and one map texture set (21, its `m_depanel` signs). + +**Fixed since (runtime on a German cache):** + +- Pal Park, Poke Transfer and the Monster Walker reach Gen IV items and moves + by cartridge id through `src/pokemon/Gen4EnglishRef.lua` (the table is + `Gen4EnglishNames.lua`, written by `tools/gen4_english_ref_gen.lua` from an + English import). Held items survive a Gen 3 migration. +- The whiteout's nurse/Mom scripts, the loss bank and the scurry line are found + by shape when the English words are absent. +- The randomizer's protected move/item lists read English names through the + same reference, and the kept trainer classes are class ids. +- The hero's default names come from the cartridge's name bank (text bank 427). +- A non-English Gen 4 import writes `gen4_language.lua`; the translation layer + reads it, so German ROM text is not sent for translation as English. +- The battle prints the cartridge's own lines (`src/battle/Gen4BattleText.lua`, + bank 368 and bank 0) for the common ones: wild/trainer intro, "Go!", move + use, fainted, effectiveness, critical hit, misses, EXP and level-up, stat + changes, catch, escape. + +**Still English on a German cartridge:** + +- the engine's own strings that are not mapped above (the less common battle + lines: statuses, weather, abilities, items; some menu words the port draws) +- the translation layer's engine-string catalog is English-keyed + +Checks that assert English text now take the language from the cache +(`gen4_item_use_check`, `gen4_area_popup_check`, `gen4_underground_check`, +`gen4_machine_check`, `gen4_mining_screen_check`, `gen4_met_location_check`, +`gen4_blackout_check`). Others still assert English (contest, safari, seals, +journal, tablet, tag battle, town map, field queries, fly, daily slots). ## Evidence for the revisions already accepted as alternates diff --git a/main.lua b/main.lua index 4b21efad4..d9a9491dc 100644 --- a/main.lua +++ b/main.lua @@ -884,6 +884,12 @@ function love.joystickhat(joystick, hat, direction) Game:joystickhat(joystick, hat, direction) end +function love.joystickadded(joystick) + if editorMode or TouchEditor or Importer then return end + if not Game then return end + if Game.joystickadded then Game:joystickadded(joystick) end +end + function love.joystickremoved(joystick) if editorMode or TouchEditor then return end if Importer then return end diff --git a/src/battle/BattleState.lua b/src/battle/BattleState.lua index cbcdbc1d1..ee4974470 100644 --- a/src/battle/BattleState.lua +++ b/src/battle/BattleState.lua @@ -1192,7 +1192,11 @@ BattleState.stampOT = stampOT -- internal id of whichever screen the ball happened to land on. function BattleState.metHere(game) local save = game and game.save - local mapId = save and save.player and save.player.map + -- THE MAP THE PLAYER IS ON, not the one last saved: save.player.map is only + -- written by OverworldState:captureSave, so it named wherever the last save + -- happened. Falls back to it when no overworld is live (tools, tests). + local ow = game and game.overworld + local mapId = (ow and ow.map and ow.map.id) or (save and save.player and save.player.map) local def = mapId and game.data and game.data.maps and game.data.maps[mapId] return def and def.regionMapSection or nil end @@ -1741,6 +1745,10 @@ local function buildTrainerParty(game, oppClass, partyIndex, partyDef) -- other trainer in the game is unchanged. local dvs = slot.dvs or trainerDvs mon.dvs = dvs + -- GEN 4 (Platinum family): TrainerData_BuildParty's own personality, so + -- nature / ability slot / gender are the cartridge's, not rolled + local cartridgePid = require("src.battle.Gen4TrainerPersonality").apply( + game, game.data.trainers and game.data.trainers[oppClass], slot, mon) if slot.statExp then mon.statExp = slot.statExp end if slot.happiness then mon.happiness = slot.happiness end if slot.stats then @@ -1753,7 +1761,7 @@ local function buildTrainerParty(game, oppClass, partyIndex, partyDef) else local def=require('src.pokemon.Gen4Forms').definition(game.data,mon) if (game.data.constants or {}).gen==4 then - mon.ivs=slot.dvs or mon.ivs + if not cartridgePid then mon.ivs=slot.dvs or mon.ivs end mon.stats=require('src.pokemon.Stats').calc( def,slot.level,mon.ivs,nil,mon.evs,mon.nature) else @@ -2668,6 +2676,11 @@ function BattleState:startMessage(item) if self.bw and text ~= "" then text = require("src.battle.Gen5BattleText").translate(self, text) end + -- A NON-ENGLISH GEN 4 CARTRIDGE (German Platinum) prints its own battle + -- lines (text bank 368 / 0) in place of the engine's English + if self.data and self.data.gen4_language and not self.bw and text ~= "" then + text = require("src.battle.Gen4BattleText").translate(self, text) + end local pos, cont = 1, false while true do local npos = text:find("[\n\v]", pos) @@ -4137,6 +4150,11 @@ function BattleState:fightLockedAction(battler) if battler.encoreTurns and battler.encoreMove then for _, m in ipairs(battler.curMoves or battler.mon.moves or {}) do if m.id == battler.encoreMove and (m.pp or 0) > 0 then + -- PLATINUM / HGSS: the move itself, which is what every other + -- chosen action is. The `{ move = m }` wrapper has no `id`, so + -- performMove logged "unknown move instance nil" and an Encored + -- Pokemon lost its turn. + if self.g4fx and not self.bw then return m end return { move = m } end end @@ -4181,6 +4199,16 @@ function BattleState:menuBattler() return self:choosingBattler() or self.player end +-- PLATINUM / HGSS: CHECK_INVALID_ALL also counts the TAUNTED and TORMENTED +-- slots (BattleSystem_CheckInvalidMoves), and Struggle is forced when every +-- slot is invalid, so a Torment on a Pokemon whose only usable move was just +-- used -- or a Taunt on one that knows nothing but status moves -- is a +-- Struggle turn. Without this the move menu refused every pick for ever. +function BattleState:g4StatusBlocksMove(who, mvDef) + if not (self.g4fx and not self.bw) then return false end + return self:tauntBlocks(who, mvDef) or self:tormentBlocks(who, mvDef) +end + function BattleState:playerHasPP(who) who = who or self:menuBattler() if not (who and who.curMoves) then return false end @@ -4188,6 +4216,7 @@ function BattleState:playerHasPP(who) -- (PLATINUM / HGSS: a move Gravity or Heal Block refuses is no more a -- choice than a disabled one -- CHECK_INVALID_ALL) if mv.pp > 0 and who.disabledSlot ~= i + and not self:g4StatusBlocksMove(who, self.data.moves[mv.id]) and not (self.g4fx and require("src.battle.Gen4MoveEffects") .menuRefusal(self, who, self.data.moves[mv.id])) and not (self.g3x and require("src.battle.Gen4MoveEffectsExtra").menuRefusal(self, who, self.data.moves[mv.id])) then @@ -4611,7 +4640,12 @@ function BattleState:update(dt) -- activeOn answers the question this menu actually means: somebody on the -- player's side, left flank first. A single battle and an ordinary -- double both get exactly what they got before. - local acting = self.player or self:activeOn(1) + -- PLATINUM / HGSS DOUBLES: the slot being asked, not always the left one. + -- A locked move (Bide, a charge, recharge, Rage, a thrash) belongs to ITS + -- Pokemon; reading the left flank here handed the left's Bide to the + -- right-hand slot, which crashed continueBide (`bideTurns` nil). + local acting = (self.g4fx and self:isDouble() and self:choosingBattler()) + or self.player or self:activeOn(1) if not (acting and acting.mon) then -- nobody left on this side at all: that is the faint flow's business, -- not the menu's, and it is already running. Returning is what keeps @@ -4632,7 +4666,8 @@ function BattleState:update(dt) -- victims (and wrappers) still get FIGHT/PKMN/ITEM/RUN (core.asm:312) local locked = self:menuLockedAction(acting) if locked then - self:resolveTurn(locked) + -- (a double files it under its own slot and goes on to ask the other) + if self.g4fx and self:isDouble() then self:chooseAction(locked) else self:resolveTurn(locked) end return end -- PLATINUM'S ACTION MENU IS NOT A 2x2, and walking it as one is what a @@ -4706,9 +4741,10 @@ function BattleState:update(dt) elseif choice == "fight" then -- After the menu: own trapping/Bide or foe Wrap skips the move -- list and forces the locked action (core.asm:320-329) - local fightLock = self:fightLockedAction(self.player) + local g4Double = self.g4fx and self:isDouble() + local fightLock = self:fightLockedAction(g4Double and self:menuBattler() or self.player) if fightLock then - self:resolveTurn(fightLock) + if g4Double then self:chooseAction(fightLock) else self:resolveTurn(fightLock) end return end local who = self:menuBattler() @@ -5006,7 +5042,7 @@ function BattleState:gen3TutorialBag() if require("src.core.GameVersion").isGen4() then self.phase, self.afterQueue = "messages", "menu" self:ui(function() - local bag = require("src.ui.Gen4BagMenu").new(self.game, { pocket = "POKé BALLS" }) + local bag = require("src.ui.Gen4BagMenu").new(self.game, { pocket = "POKE_BALLS" }) bag.rows = { { id = 4, label = "POKé BALL", qty = 20 } } bag.update = function(b) b.scriptTimer = (b.scriptTimer or 0) + 1 @@ -5914,7 +5950,7 @@ function BattleState:resolveTurn(playerAction) local switches, moves = {}, {} for _, e in ipairs(entries) do local sp = e.action and e.action.special - if sp == "playerSwitch" or sp == "aiSwitch" then + if sp == "playerSwitch" or sp == "aiSwitch" or sp == "playerItem" then switches[#switches + 1] = e else moves[#moves + 1] = e @@ -6804,7 +6840,20 @@ function BattleState:endOfTurn() -- faint on the first action and its partner still take the third. Does -- nothing at all in a single battle, where the faint path fills the slot -- itself and there is nothing pending. - self:fillEmptySlots() + -- + -- PLATINUM / HGSS DOUBLES: ...BUT AFTER THE FAINTS THIS SWEEP QUEUED. A + -- burn / poison / sandstorm / Leech Seed faint inside the residual above + -- queues its `doubleFaint` with actNext, which lands AFTER this running + -- queue item; filling right here ran first, found the slot still occupied, + -- and left the replacement pending until the NEXT turn's end -- with both + -- slots empty (a living bench mon, e.g. a Revived one) the battle sat in + -- an unanswerable command menu forever. Queueing the fill behind the + -- faints is HandleFaintedMonActions' order. + if self.g4fx and not self.bw and self:isDouble() then + self:actNext(function() self:fillEmptySlots() end) + else + self:fillEmptySlots() + end -- BATTLE PALACE: the first time a Pokemon is at half HP or below (and -- awake), its nature shows (VARIOUS_PALACE_FLAVOR_TEXT) if self.palace then @@ -8003,6 +8052,7 @@ function BattleState:executeAction(user, target, action) -- resolve it to. local isSwitch = action.special == "playerSwitch" or action.special == "aiSwitch" + or action.special == "playerItem" if not isSwitch and not (target and target.mon and target.mon.hp > 0) then return end @@ -8103,6 +8153,11 @@ function BattleState:executeAction(user, target, action) -- actions rather than the whole turn. A single battle never reaches -- here: resolveSwitch does the send-out itself and gives the foe its -- free move, which is the cartridge's single-battle flow. + if action.special == "playerItem" then + for _, m in ipairs(action.messages or {}) do self:sayNext(m) end + self:drainNext() -- potions animate the bar + return + end if action.special == "playerSwitch" then if action.mon and action.mon.hp and action.mon.hp > 0 then self:switchPlayerInto(user.position or BattleState.POS.PLAYER_LEFT, @@ -8182,11 +8237,15 @@ function BattleState:executeAction(user, target, action) end end local animationPlayed = false + local announced = false for i, one in ipairs(list) do if one and one.mon and (one.mon.hp or 0) > 0 then -- only the first announces; the rest are the same move landing again + -- (Platinum / HGSS print the line once, not once per target) self:performMove(user, one, action, i > 1, - spreadAnimation and animationPlayed) + spreadAnimation and animationPlayed, + (self.g4fx and announced) or nil) + announced = true -- FireRed's Cmd_attackanimation increments animTargetsHit only when an -- animation actually survives the no-effect/failure path. If the -- first target cancels it, a later spread target is still allowed to @@ -8469,7 +8528,11 @@ local WEATHER_BALL_ANIM = { SUN = 1, RAIN = 2, SANDSTORM = 3, HAIL = 4, } -function BattleState:performMove(user, target, moveInst, isCalled, suppressAnimation) +-- `suppressAnnounce` (PLATINUM / HGSS, a spread move's later targets): the +-- "X used Y!" line belongs to the move, not to each target -- the cartridge +-- shows it once (SYSCTL_ATTACK_MESSAGE_SHOWN) and loops the hit over the other +-- battlers (BattleControllerPlayer_MoveEnd's RANGE_ALL_ADJACENT arm). +function BattleState:performMove(user, target, moveInst, isCalled, suppressAnimation, suppressAnnounce) local move = self:moveDef(moveInst) if not move then Logger.warn("unknown move instance %s", tostring(moveInst.id)) @@ -8548,7 +8611,9 @@ function BattleState:performMove(user, target, moveInst, isCalled, suppressAnima self.moveAnimRow = nil if not (user.thrashTurns and moveInst == user.thrashMove and user.thrashAnnounced) then - self:sayNext(self:romText("_ItemUseText001", "%s\nused %s!", displayName(user), move.name)) + if not suppressAnnounce then + self:sayNext(self:romText("_ItemUseText001", "%s\nused %s!", displayName(user), move.name)) + end -- the move's animation plays right after the announcement; the -- damage path attaches the target's hit blink to this row so the -- blink follows the animation (pokered's order). Mimic is the @@ -8646,6 +8711,14 @@ function BattleState:performMove(user, target, moveInst, isCalled, suppressAnima self:animNext(chargeAnim, user.isPlayer) end end + -- a non-English Gen 4 cartridge has the charge line of each move itself + if self.data and self.data.gen4_language and not self.bw then + local own = require("src.battle.Gen4BattleText").chargeLine(self, move, user) + if own then + self:sayNext(own) + return + end + end local chargeText = move.chargeText or CHARGE_TEXT[move.id] or Strings.source("%s\nis charging up!") -- the template is a source string (a move record may supply its own), @@ -9184,6 +9257,18 @@ end -- before the first one has played would queue the whole thing again. function BattleState:checkSideBeaten() if self.sideBeaten then return end + -- PLATINUM / HGSS: BOTH SIDES OUT AT ONCE IS A DRAW, AND A DRAW IS PLAYED AS + -- A LOSS. CheckBattleOver ORs BATTLE_RESULT_WIN and _LOSE when every + -- battler's party is empty (Explosion taking the last Pokemon of both + -- sides), and BattleControllerPlayer_HandleResult runs subscript_battle_lost + -- for LOSE and DRAW alike. Checking the foes first made it a win -- paid + -- out, then forced to a blackout by finish() with a warning. + if self.g4fx and not self.bw and not self:sideCanContinue(false) + and not self:sideCanContinue(true) then + self.sideBeaten = "player" + self:act(function() self:playerMonFainted() end) + return + end if not self:sideCanContinue(false) then self.sideBeaten = "enemy" self:act(function() self:enemyMonFainted() end) @@ -9292,7 +9377,13 @@ function BattleState:awardExp(fallen) -- new current HP (house convention: potions drain the bar too, see -- itemUsed) so the bar grows instead. Only the active player battler -- shares its table with the HUD; other party mons (EXP.ALL) have no bar. - if mon == self.player.mon then self:drainNext() end + -- (`self.player` is the LEFT flank and is nil in a double between its + -- faint and the replacement -- the exp is paid right then) + local onField = self.player and mon == self.player.mon + for _, b in activeBattlers(self) do + if b and b.isPlayer and b.mon == mon then onField = true end + end + if onField then self:drainNext() end for _, moveId in ipairs(Experience.movesLearnedAt( self.data.pokemon[mon.species], lv)) do self:learnMove(mon, moveId) @@ -9665,6 +9756,13 @@ function BattleState:enemyMonFainted() end end self.result = "win" + -- PLATINUM / HGSS: the last foe going down in the same breath as the last + -- of the player's own Pokemon (recoil, Explosion, a sandstorm tick) is a + -- DRAW, and the cartridge plays a draw as a loss (HandleResult). playerMonFainted + -- queues the blackout; the result has to agree with it. + if self.g4fx and not self.bw and not self.ally and not Party.firstHealthy(self.game.save.party) then + self.result = "lose" + end self.afterQueue = "finish" end @@ -10308,6 +10406,17 @@ function BattleState:itemUsed(messages) -- bag cures clear mon.status before the message UI; refresh the HUD -- once control returns (pokered DrawHUDsAndHPBars after item use) self:syncShownStatus() + -- PLATINUM / HGSS DOUBLES: an item is one of the four actions, not the whole + -- turn. It is filed under the slot that used it and run with the switches + -- (BattleControllerPlayer_CalcTurnOrder: items and party first, fights + -- after), while the other three slots still choose and move. Running it as + -- a turn of its own lost the partner's move and the right foe's, and left + -- the half-collected `pendingActions` behind for the NEXT turn to execute + -- (a stale Bide on a Pokemon that never used it crashed continueBide). + if self.g4fx and not self.bw and self:isDouble() then + self:chooseAction({ special = "playerItem", messages = messages or {} }) + return + end for _, m in ipairs(messages or {}) do self:say(m) end table.insert(self.queue, { drain = true }) -- potions animate the bar self:act(function() diff --git a/src/battle/Gen45Abilities.lua b/src/battle/Gen45Abilities.lua index ea9fdd05c..47dd27f9f 100644 --- a/src/battle/Gen45Abilities.lua +++ b/src/battle/Gen45Abilities.lua @@ -610,7 +610,10 @@ function M.unovaAfterHit(battle, attacker, target, move, contact) end end -local function isBerry(def) return def and type(def.name) == "string" and def.name:find("Berry", 1, true) ~= nil end +local function isBerry(def) + -- the Berries pocket is the cartridge's own answer in any language; the name is the older test + return def and (def.pocket == "BERRIES" or (type(def.name) == "string" and def.name:find("Berry", 1, true) ~= nil)) or false +end -- ZEN MODE: Darmanitan below half HP is Fire / Psychic with its Zen stats, -- and in its Zen picture (form 1, gen5-form-sprites-v1) -- shown through a diff --git a/src/battle/Gen4AI.lua b/src/battle/Gen4AI.lua index e71f4b7c1..ca0a9d1dc 100644 --- a/src/battle/Gen4AI.lua +++ b/src/battle/Gen4AI.lua @@ -67,6 +67,9 @@ local TYPE = { NORMAL = 0, FIGHTING = 1, FLYING = 2, POISON = 3, GROUND = 4, ROC ELECTRIC = 13, PSYCHIC = 14, ICE = 15, DRAGON = 16, DARK = 17 } local TYPE_NAME = {} for k, v in pairs(TYPE) do TYPE_NAME[v] = k end +-- Renegade Platinum names slot 9 FAIRY (its species, moves and chart say so); +-- an alias by name only, so TYPE_NAME[9] stays Platinum's +TYPE.FAIRY = 9 G.TYPE, G.TYPE_NAME = TYPE, TYPE_NAME local MOVE = { NATURAL_GIFT = 363, JUDGMENT = 449, HIDDEN_POWER = 237, GYRO_BALL = 360, diff --git a/src/battle/Gen4BattleText.lua b/src/battle/Gen4BattleText.lua new file mode 100644 index 000000000..68edcff08 --- /dev/null +++ b/src/battle/Gen4BattleText.lua @@ -0,0 +1,401 @@ +-- PLATINUM'S BATTLE LINES IN THE CARTRIDGE'S OWN LANGUAGE. +-- +-- The Gen 4 battle prints the engine's English ("Wild X\nappeared!", "Enemy X +-- used Y!"). On an English cartridge that is the game's own wording; on a +-- German one it is English in the middle of a German game. The cartridge's +-- lines are in text bank 368 (the battle) and bank 0 (a line per move: +-- 3 * move + side), three variants per line like Black / White's: the +-- player's Pokemon (+0), the wild one (+1) and the foe's (+2). +-- +-- Like src/battle/Gen5BattleText.lua this is applied where a battle message +-- starts (BattleState:startMessage), and only on a non-English cartridge +-- (`data.gen4_language`), so an English cache prints exactly what it did. A +-- line no pattern knows is left as the engine wrote it. +-- +-- THREE WAYS IN, tried in this order: +-- 1. T.PATTERNS: the lines worked out first (move used, appearing, fainted, +-- the trainer's challenge ...), read off an imported US cache by the line +-- they start with (grep TEXT_B0368_). +-- 2. stat changes ("X's ATTACK rose!"): the stat's name is the battle's own +-- (bank 551), the verb picks the entry. +-- 3. src/battle/Gen4BattleTextMap.lua, GENERATED by tools/gen4_battle_text_map.py: +-- every engine template that is also a line of bank 368 (matched by its +-- wording against the USA cartridge's English) plus a hand table of the +-- lines the engine words differently. Only entry NUMBERS are in it; the +-- text is read from the running cache. +-- +-- The placeholders ({STRVAR_1 0}) are filled from the printed +-- names by type: 1 a Pokemon (nickname), 3 a trainer, 5 an ability, 6 a move, +-- 8 an item, 13 a stat, 14 a trainer class, 50/52/54 a number. + +local T = {} + +T.BANK = 368 +T.BANK_MOVE_USED = 0 + +local function key(bank, index) return ("TEXT_B%04d_%05d"):format(bank, index) end + +local function line(data, bank, index) + local s = data and data.text and data.text[key(bank, index)] + return type(s) == "string" and s ~= "" and s or nil +end + +-- the cartridge's page breaks and controls as this engine's line breaks +local function tidy(out) + out = out:gsub("{[^}]*}", "") + out = out:gsub("\012", "\n") -- a page break: the box scrolls + out = out:gsub("[\011]+$", "") -- the ROM's trailing wait marker is the box's own concern + return out +end + +-- fill a cartridge line: vals[type] = { slot0, slot1, ... } +local function fill(raw, vals) + local out = raw:gsub("{WAIT %d+}", ""):gsub("{STRVAR_1 (%d+) (%d+) %d+}", function(kind, slot) + local list = vals[tonumber(kind)] + local v = list and list[tonumber(slot) + 1] + return v ~= nil and tostring(v) or "" + end) + return tidy(out) +end + +-- which of the three variants a printed name belongs to +local function side(battle, enemyPrefix) + if not enemyPrefix or enemyPrefix == "" then return 0 end + return battle.trainer and 2 or 1 +end + +local function moveIdByName(battle, name) + local map = battle._gen4MoveIdByName + if not map then + map = {} + for id, def in pairs((battle.data and battle.data.moves) or {}) do + if type(id) == "number" and type(def) == "table" and def.name and map[def.name] == nil then map[def.name] = id end + end + battle._gen4MoveIdByName = map + end + return map[name] +end + +-- the three-variant line (index = base + side) with the Pokemon name in slot 0 +local function three(base, extra) + return function(battle, prefix, name, ...) + local raw = line(battle.data, T.BANK, base + side(battle, prefix)) + if not raw then return nil end + local vals = { [1] = { name } } + if extra then extra(vals, ...) end + return fill(raw, vals) + end +end + +local function plain(index) + return function(battle) local raw = line(battle.data, T.BANK, index) return raw and fill(raw, {}) end +end + +-- move -> the "was trapped" entry +T.TRAP = { [250] = 242, [83] = 242, [328] = 245, [463] = 1247 } + +-- patterns: { lua pattern over the engine's text, handler(battle, captures...) } +T.PATTERNS = { + { "^(Enemy )(.-)\nused (.-)!$", function(battle, prefix, name, move) + local id = moveIdByName(battle, move) + local raw = id and line(battle.data, T.BANK_MOVE_USED, 3 * id + side(battle, prefix)) + return raw and fill(raw, { [1] = { name } }) end }, + { "^(.-)\nused (.-)!$", function(battle, name, move) + local id = moveIdByName(battle, move) + local raw = id and line(battle.data, T.BANK_MOVE_USED, 3 * id) + return raw and fill(raw, { [1] = { name } }) end }, + { "^Wild (.-)\nappeared!$", function(battle, name) + local raw = line(battle.data, T.BANK, 1246) + return raw and fill(raw, { [1] = { name } }) end }, + { "^Go! (.-)!$", function(battle, name) + local raw = line(battle.data, T.BANK, 979) + return raw and fill(raw, { [1] = { name } }) end }, + { "^(Enemy )(.-)\nfainted!$", three(30) }, + { "^(.-)\nfainted!$", function(battle, name) return three(30)(battle, "", name) end }, + { "^It's super\neffective!$", plain(780) }, + { "^It's not very\neffective%.%.%.$", plain(779) }, + { "^Critical hit!$", plain(774) }, + { "^One%-hit KO!$", plain(775) }, + { "^(Enemy )(.-)'s\nattack missed!$", three(12) }, + { "^(.-)'s\nattack missed!$", function(battle, name) return three(12)(battle, "", name) end }, + { "^It doesn't affect\n(Enemy )(.-)%.%.%.$", three(27) }, + { "^It doesn't affect\n(.-)%.%.%.$", function(battle, name) return three(27)(battle, "", name) end }, + { "^(.-) gained\n(%d+) EXP%. Points!$", function(battle, name, n) + local raw = line(battle.data, T.BANK, 1) + return raw and fill(raw, { [1] = { name }, [54] = { nil, n } }) end }, + { "^(.-) grew\nto level (%d+)!$", function(battle, name, n) + local raw = line(battle.data, T.BANK, 3) + return raw and fill(raw, { [1] = { name }, [52] = { nil, n } }) end }, + { "^Gotcha!\n(.-) was caught!$", function(battle, name) + local raw = line(battle.data, T.BANK, 867) + return raw and fill(raw, { [1] = { name } }) end }, + { "^Got away safely!$", plain(781) }, + { "^Can't escape!$", plain(42) }, + { "^(.-) sent\nout (.-)!$", function(battle, who, mon) + local raw = line(battle.data, T.BANK, 974) + return raw and fill(raw, { [3] = { who }, [1] = { nil, mon } }) end }, + -- "Bug Catcher Jack wants to fight!": the class and the name are the + -- trainer record's, which the cartridge prints from two slots + -- "X was trapped by !": the vortex lines (Whirlpool, Fire Spin 242, Sand + -- Tomb 245, Magma Storm 1247), each three variants; the others name the + -- attacker, which the engine's line does not + { "^(Enemy )(.-) was trapped\nby (.-)!$", function(battle, prefix, name, move) + local at = T.TRAP[moveIdByName(battle, move) or 0] + return at and three(at)(battle, prefix, name) end }, + { "^(.-) was trapped\nby (.-)!$", function(battle, name, move) + local at = T.TRAP[moveIdByName(battle, move) or 0] + return at and three(at)(battle, "", name) end }, + { "^(.-) wants\nto fight!$", function(battle) + local t = battle.trainer + local raw = t and line(battle.data, T.BANK, 969) + return raw and fill(raw, { [14] = { t.className }, [3] = { nil, t.name } }) end }, +} + +-- --------------------------------------------------------------------------- +-- A STAT CHANGE names the stat in the cartridge's language: the battle's own +-- stat-name bank (551: HP, attack, defense, speed, sp. atk, sp. def, accuracy, +-- evasiveness). Entries: 750 rose, 753 sharply rose, 762 fell, 765 harshly +-- fell, 142 won't go higher, 145 won't go lower; each three variants (ally, +-- wild, foe). +-- --------------------------------------------------------------------------- +T.STAT_BANK = 551 +T.STAT_INDEX = { ["HP"] = 0, ["ATTACK"] = 1, ["DEFENSE"] = 2, ["SPEED"] = 3, ["SP. ATK"] = 4, ["SPCL.ATK"] = 4, + ["SP. DEF"] = 5, ["SPCL.DEF"] = 5, ["ACCURACY"] = 6, ["accuracy"] = 6, ["EVASION"] = 7, ["evasiveness"] = 7, + ["EVASIVENESS"] = 7 } +T.STAT_VERB = { ["rose"] = 750, ["greatly rose"] = 753, ["fell"] = 762, ["greatly fell"] = 765, + ["won't go higher"] = 142, ["won't go lower"] = 145 } + +local function statName(data, label) + local at = T.STAT_INDEX[label] + return at and line(data, T.STAT_BANK, at) or nil +end + +local function statChange(battle, text) + local flat = text:gsub("[\n\v]+", " ") + local best + for verb, base in pairs(T.STAT_VERB) do + local tail = " " .. verb .. "!" + if flat:sub(-#tail) == tail and (not best or #verb > #best[1]) then + local head = flat:sub(1, #flat - #tail) + local name, stat = head:match("^(.-)'s (.+)$") + if name and statName(battle.data, stat) then best = { verb, base, name, stat } end + end + end + if not best then return nil end + local prefix = best[3]:match("^Enemy ") and "Enemy " or "" + local label = statName(battle.data, best[4]) + return three(best[2], function(vals) vals[13] = { label, label, label, label } end)(battle, prefix, (best[3]:gsub("^Enemy ", ""))) +end + +-- --------------------------------------------------------------------------- +-- THE GENERATED TABLE (Gen4BattleTextMap.lua) +-- +-- Per row: the line as the engine prints it (%s for each filled-in name), what +-- each %s is ("type.slot" of the bank entry; "side" only picks the variant; "-" +-- is dropped), the entry of each variant by the sides of the pokemon names +-- (a ally, w wild, f foe), and options: fixed (constants the engine prints as +-- English: "move:", "ability: