Three follow-ups to the merge snapshot, kept as their own commit so the
review of the wave commit stays intact:
- The PC panel's left data panel broke in the merge: upstream inlines the
egg-aware species read into frontPic while the M8 dedup had removed the
duplicate local, so `sp` was referenced with no binding and the luacheck
gate failed. One shared local sp = Pokemon.speciesOrEgg(hoveredMon) serves
both the TV sprite and the stats card
(pokefirered/src/pokemon_storage_system_data.c:1034, :1057,
MON_DATA_SPECIES_OR_EGG).
- Trainer-card vars fall back to the stringified key: persist_sidecar and
Flags.serialize write tostring(id) keys into session.vars, so a numeric miss
used to read every var as 0.
- tests/game3_trainer_card_photo_tint_test.lua drives the whole Game Corner
photo -> card tint chain (setvar -> UpdateTrainerCardPhotoIcons ->
Flags.serialize/loadInto -> cardData) and pins c.monIconTint/c.monSpecies.
Second batch of the Gen 3 review work: the review lanes closed their queues
and the test gate grew with them.
Review census: 139 routed findings -- 118 fixed, 14 invalid (the report had
Gen 4+ semantics in more than one place), 6 struck as stale after
re-verification, 1 re-routed. Every fix carries its own pret citation in
docs/game3/review-v3-triage.md; the E10 opcode closure is itemised in
docs/game3/e10-opcode-spec.md (18 ops wired, 2 reclassified to the seam class).
Representative ROM-grounded changes:
- The POKéMON start-menu entry is now gated on FLAG_SYS_POKEMON_GET the way
retail does it (pokefirered/src/start_menu.c:217-218, flag 0x828).
- Money ops read their amount raw and gate the change on the disable byte
(pokefirered/src/scrcmd.c:1798-1830, pokefirered/asm/macros/event.inc:1166-1186);
random and the warp family VarGet theirs
(pokefirered/src/scrcmd.c:455-461, :719-731).
- The HM table matches FRLG: there is no Whirlpool HM
(pokefirered/include/constants/items.h:411-418).
- The day-care party-full guard follows src/daycare.c:525, :1081.
- Knock Off keeps its battle-scoped send-out mask
(pokefirered/src/battle_script_commands.c:2730-2752, :4489), carried from
the first batch.
Structural work: the I6/I9/J1 architecture items landed as seams (profile
selector, capability flags, font provider, virtual-object layer), the adopted
footprint register keeps a KEEP verdict, the quantizer target is met, and the
30 drain items plus carves 1-4 are folded in.
Tests: T6 now runs 282 top-level suites (273 + 9 new), the re-sweep ends at
267 pass / 0 fail, the engine tier is 634 suites, modkit 37, and the full
gate ran green twice with T3 active on an imported Red cache. Documented
skips are the lua5.4 oversize-save oracle where lua5.4 is absent and the
config-partials listed in docs/game3/game3-artifact-conversions-v3.md.
Docs shipped: review-v3-triage.md, e10-opcode-spec.md, rse-seams.md,
game3-suite-sweep-v113.md, game3-artifact-conversions-v3.md,
test-baseline-v3.md (plus the first-batch docs already on the branch).
Braille.encode spells a braille message from Latin letters, through
Braille.CODE and the Latin charmap byte of any other character. That
covers the US cart, but the European carts' braille lines use cells no
character reaches: German ä (dots 3-4-5), the full cell, dot 5 alone. A
translation mod therefore cannot hand over the German, Spanish or Italian
braille, and the 39 lines of the Dotted Hole, Ruin Valley and Mt. Ember
stay in English in every language.
pret's braille font holds every dot combination
(pokefirered/include/characters.h:282), so Braille.encode now draws a
Unicode braille character (U+2800-U+283F) as that very cell, mapping
Unicode's dot bits to the cart's cell numbering (dot 1 = 0x01, 4 = 0x02,
2 = 0x04, 5 = 0x08, 3 = 0x10, 6 = 0x20). A cell spells its own number sign,
as the carts' braille does. Latin text is spelled as before.
Second wave of fixes from the Gen 3 codebase review (210 findings), the
regressions the full-suite sweep surfaced, and a T6 tier so the top-level
tests/game3_*.lua suites run inside ./scripts/test.sh instead of outside it.
Battle: Knock Off and Thief/Trick item persistence follow the ROM, not the
review - the party slot is never written through during battle, and the
knocked-off mask suppresses the item on later send-outs
(pokefirered/src/battle_script_commands.c:2731 MOVE_EFFECT_KNOCK_OFF with the
STICKY_HOLD guard at :2732, opponent-steal guard at :2610-2622, mask use at
:4489). Rapid Spin keeps its one-per-use chain order (:8435-8474); Growl
keeps pret's target (src/data/battle_moves.h, include/battle.h:63).
Scripting: operand layouts match pret/asm/macros/event.inc - givemon 15 bytes
(:989-997), comparestat {B,W} (:1573-1576), setptr/loadbytefromptr/setptrbyte
take a word pointer (:118-137). 18 of the 20 E10 ops are wired per
src/scrcmd.c (per-op citations in docs/game3/e10-opcode-spec.md), money and
random and the warp family read their operands as VarGets
(src/scrcmd.c:1798-1830, :455-461, :719-731), and the day-care party-full
guard is in place (src/daycare.c:525,:1081).
Field and UI: fishing counts rounds as the ROM does
(src/field_player_avatar.c:1740-1765), an unresolved map section no longer
reports Pallet Town (src/region_map.c:3782), and a font provider sits behind
a FireRed capability/profile seam.
Save data: profile-driven Options.block, a save version round-trip, and slot
id validation that blocks path traversal (slotDiskPath("firered", "../evil")
returns nil).
Importer: the parallel import path now writes both completion markers,
object kind/clone bytes decode as pret defines them
(include/global.fieldmap.h:110-130), and the HM table drops Whirlpool
(include/constants/items.h:411-418).
Tests: six new scenario suites (battle_ai, capture, event, menu, move,
overworld) and eight engine suites (profile, capabilities, cache paths, font
provider, options block, save version round-trip, version dispatch,
versions_game). T6 runs every top-level game3 suite at GAME3_JOBS default 8
with per-suite logs, failure cause classification and a KNOWN_GAME3_FAILURES
ceiling of 23 as a shrinking guard.
Docs: the working notes under docs/game3 (triage ledger, pret citation audit,
e10 opcode spec, RSE seams, sweep v3/v113, test baseline, artifact
conversions, merge trial).
Full gate at this snapshot: exit 0, all tiers passed - engine 624/624,
T6 279/279 (0 known, 0 fresh failures), gen2 146/146, modkit 37/37, luacheck
clean, privacy gate 3/3 over a 9,711-file publication set.
The party menu drew an egg's slot like any other mon's: level, gender, HP
numbers and HP bar. pret's DisplayPartyPokemonData prints only the nickname
for an egg and blits the slot without its HP frame (sSlotTilemap_MainNoHP /
WideNoHP; pokefirered/src/party_menu.c:781, :2197), and
DisplayPartyPokemonDescriptionData likewise skips the level and gender of an
egg (party_menu.c:804). The slot now does the same, reusing the no-HP frame
the description slots already draw.
Three screens that show a single mon still drew an egg as the species it
will hatch into, and printed its stored nickname untranslated:
- The PC's hovered-mon panel drew the hatch species' front pic, the stored
"EGG" nickname, "/PICHU", gender, level and held item. pret draws the
SPECIES_OR_EGG pic and, for an egg, prints only gText_EggNickname with the
other lines blank (pokemon_storage_system_data.c:1034, :1091).
- The summary's egg page drew the hatch species' front pic, where pret uses
SPECIES_OR_EGG (pokemon_summary_screen.c:4016). It also recognised an egg by
mon.isEgg alone; it now uses Pokemon.isEgg like the rest of the engine.
- The trade scene drew a traded egg as its species and named it by the stored
nickname (trade_scene.c:757, :1239).
The script string buffers (BufferMonNickname and the other nicknameOf
helpers) now also give the language's EGG for an egg, as GetMonData
(MON_DATA_NICKNAME) does (pokemon.c:3020).
The party, the PC boxes and the release animation drew an egg with the icon
of the species it will hatch into, so a Pichu egg showed as a Pichu. pret's
party menu picks the icon from GetMonData(MON_DATA_SPECIES_OR_EGG), which is
SPECIES_EGG for any egg (pokefirered/src/pokemon.c:3245, party_menu.c:2655).
Pokemon.speciesOrEgg does the same, and the icon call sites use it. The
species pass only extracted icons up to NUM_SPECIES - 1, so the egg extractor
now also bakes gMonIconTable[SPECIES_EGG] as pokemon/icons/412.rgba. Cache
version 114 re-imports existing caches so the icon appears.
The rest of the Gen 3 review fixes, each with a gated suite in tests/engine/.
ROM semantics were checked against pret/pokefirered.
Scripts
- givemon carried the wrong operand layout, found earlier; four more layout
desyncs came out of pret asm/macros/event.inc: comparestat is {byte,word},
and setptr / loadbytefromptr / setptrbyte each carry a leading byte plus a
word. A wrong size mis-decodes every instruction after the bad one, so
Versions.CACHE_VERSION moves to 113 and existing caches re-import.
- handlers for previously handler-less verbs: comparestat,
bufferitemnameplural, setmonmove, setmonmetlocation, the modern
fateful-encounter pair, the script-locals family (copylocal, setptr,
loadbytefromptr, setptrbyte, copybyte, compare_local_to_* and
compare_ptr_to_*), the RAM-script family (setvaddress, vgoto, vcall,
vgoto_if, vcall_if, vmessage, vbuffermessage, vbufferstring, endram,
returnram) and the same-map forms of the *at verbs.
- setdooropen / setdoorclosed read their coordinates through VarGet.
Battles
- Knock Off and Thief / Trick persist the item change instead of only
touching the in-battle copy.
- knocked-off party slots are tracked in a bitmask, so a slot reused later
does not inherit the flag.
Field and UI
- Player.reset restores facing and clears the surf flags.
- a definition-less Map.load no longer leaves collision unbound.
- an unresolved region-map section no longer reports PALLET TOWN.
- the naming screen splits input from the timer, so update(dt) stops
indexing a number.
- the hall of fame commits through the engine save path and serializes its
fields.
Persistence
- gameStats, the link-battle records and the trainer card are serialized.
- the PC deposit refuses at the 999 cap instead of destroying the overflow.
Robustness
- Data.load runs cached modules sandboxed.
- the file browser quotes shell arguments.
- .meta dimensions are bounds-checked and mids.idx validates its header.
do_save() discarded both pcall results and set _phase = "saved" unconditionally,
so a refused or failed save still showed "<player> saved the game." and closed the
menu. Only a truthy saveGame now reports success; a raised error, an explicit
false, or a missing saveGame enters a new save_failed phase, logs the reason via
Logger, and shows "The game could not be saved."
Test: tests/engine/game3_save_menu_failure_test.lua (8/10 fail on the previous
code, 10/10 pass here).
The Easy Chat screen picks its words from src/core/game3/easy_chat_data.lua, the ROM's own word tables as the extractor writes them: 1,813 English words across 22 groups, plus the group names the picker lists. It drew them straight from that table, so every word the player composes with stayed English, and the screen's own prompts -- routed through Strings() already -- sat in a frame of untranslated vocabulary.
src/core/game3/easy_chat_text.lua is the seam. The data module stays exactly what the extractor produced, because it is regenerated from the ROM and anything written into it would be lost on the next import; the words are translated where they are drawn instead, the same split the rest of game3 uses for extracted text. Five sites go through it: the group list, its header, the word list, the word sitting in a slot, and the saved profile a message box prints (ShowEasyChatMessage). The ids the save file stores are untouched.
Each word carries its group's context ("easyChat.FEELINGS"), because the same English word means different things in different groups and 155 of them collide with labels this engine already translates elsewhere (ATTACK, BAG, CANCEL, BACK...). Strings() falls back to the plain key, so a catalog that does not need the distinction lands with one entry -- with the consequence that a mod which already translates BAG for the menus now sees that wording in the Easy Chat list too, until it adds a context-specific entry.
A seam nothing can discover is not much use: tools/modkit.py's catalog generator emits these keys too. The literal harvester cannot see them (the picker looks each word up at draw time, not at a Strings("...") call site), so the dataset dump walks EasyChatData.GROUPS and writes 1,835 rows into lang/strings.lua, where a translator finds them beside the rest of the engine text.
Each word is clipped to the box it sits in: in the picker, the red selection rectangle; in the phrase frame, the slot's own frame. The cart's words fit those boxes (72 px at most); a longer translation is cut at the box edge instead of running past the highlight, into the next slot's cursor, or off screen. Group names get the room up to the scroll arrows instead, because an official translation already needs more than the rectangle: the French cart's VIE QUOTIDIEN. is 83 px.
tests/engine/game3_easy_chat_text_test.lua covers the context lookup, the fallback to a plain key, the untouched English with no catalog, and that the extracted data itself is never rewritten. It lives in tests/engine so the T1/T2 tier actually runs it.
The summary's MOVES page drew each move name with a fixed 64 px limit.
pret prints the names at x 3 of POKESUM_WIN_MOVES_3, a 10-tile window
that starts at tile 20 (pokemon_summary_screen.c:857, :2543), so a name
has up to the right edge of the screen: 77 px from the pen at 163. The
cart's own longest names are 72 px -- SKY UPPERCUT, FRENZY PLANT, HYDRO
CANNON -- and lost their last letters, as did translated names that the
cart's window would hold (RUGISSEMENT for GROWL).
Clip to the window's right edge instead, measured from wherever the
chrome manifest puts the pen.
The word picker pages eight words at a time and its navigation moves through four rows of two: `row < 3` when moving down, `3 * 2 + col + 1` when stepping up into the previous page. The draw loop rendered three rows, so the seventh and eighth word of every page were selectable -- the cursor sat on them, and pressing A picked them -- while nothing was drawn there. A group's last words could be chosen only blind.
Eight is the cart's own page: easy_chat_2.c scrolls selectWordRowsAbove by four, and easy_chat_3.c's PrintECRowsWin2 prints row * 16 + 96 for each row. The loop now draws four, starting 2 px higher so the fourth row's 14 px of glyphs stop exactly at the bottom of the 78 px frame.
tests/engine/game3_easy_chat_page_test.lua drives the screen with a group larger than one page and checks all eight words reach the font, inside the frame. It reports the two missing words before this change.
- Doors:
- Fix Viridian City Gym and Dojo warp doors registering as wooden house doors; map metatile 0x15B to SlidingDouble animation and SE_SLIDING_DOOR (18).
- Embed full FRLG metatile fallback lookup table and support CacheFs/Dataset.cache() for door textures and manifests.
- Scripting & Movement (Fixes#2356):
- Add support for jump movement opcodes (jump_2_down, jump_in_place, etc.) for both player and NPC objects.
- Implement scriptJump routines updating cell coords and triggering jump trajectory animations.
- CacheFS & GBA ROM Extraction:
- Wire CacheFS persistence across storage, region map, battle AI/moves, items, pokedex chrome, multichoice, and easy chat extractors for Android portability.
- Fix box storage chrome extraction palette/shadow colors and duplicate label bug.
- Add region map chrome extraction and interactive Town Map rendering.
- Performance & Android Optimization:
- Eliminate dynamic per-frame require/pcall lookups in battle render/update loops.
- Pre-allocate particle sort comparator in AnimVm to avoid per-frame closures.
- Cache palette shader uniform uploads and blend state in AnimPal/AnimVm.
- Pool scratch color tables in FrlgFont to remove garbage generation during text token scanning.
- Harden audio FFI caching and m4a worker initialization for sandboxed environments.
- Tests:
- Add test suites for Viridian Gym doors, jump opcodes, storage chrome, and town map caching.
fixes#2356fixes#2357
The box above the menu during a Bug-Catching Contest (pokecrystal ContestStatus) drew its three labels and the empty-slot placeholder as bare literals: CAUGHT, LEVEL, BALLS: and "None". They were the last raw player-facing sinks in this file, which tests/gen2_ui_translation_harvest_test.py already lists among the modules that should have none.
The values keep the cart's own columns (x=8, 7, 8) only while the label fits them: "ATTRAPÉ" fills the seven tiles CAUGHT leaves, and "GEFANGEN" or "ATRAPADO" one more, so each value starts after its label instead of being drawn over by it.
"None" carries a context: the bare key is also the import wizard's "no limit" label (src/import/online/Wizard.lua), and one catalog entry cannot serve both -- which is also why the harvest gate asks for the three labels and not for it.
The caught mon's own name is left alone: it is a nickname, or a species the `pokemon` registry renames.
The Bug-Catching Contest confirmation was drawn as two bare literals, one per row, with no hook a mod could reach. It becomes one catalog key, "Would you like to\nend the Contest?", the way every other confirmation prompt in this port is written (src/ui/StartMenu.lua, src/ui/gen2/SaveMenu.lua): "Would you like to" alone has no stable translation out of context, and the cart writes it as one text (data/text/common_2.asm).
Both prompts in this file now share one helper. It draws with Chrome.printWrapped, which wraps on the translated line break AND on pixel width, and reads the line count back from it to warn -- once per prompt -- when a translation needs a third row the box cannot hold. The title-screen prompt went through a hand-rolled splitter on the first line break, which handed Font.draw an embedded break, rendered as a space: a translation needing three rows printed two of them joined and ran past the box.
The official wordings fit: "Voulez-vous arrê- / ter le concours?" and "Möchtest du den / Wettkampf beenden?".
tests/engine/gen2_contest_coins_translation_test.lua drives StartMenu's confirmContest arm and StartGameCornerGame's no-coins branch with a mod-loaded catalog and checks the translated wording reaches the screen, plus the English case. On dev the three prompt checks fail; the refusal's own check passes there too, because that lookup already ran at runtime -- what dev was missing is the key, not the call.
The Easy Chat editor (profile, questionnaire, battle-start message) drew all of its own text as literals: the screen's title and its two instruction lines, the "is as shown. Okay?" confirmation, the quit and delete-all confirmations, YES/NO, "Select a group." and the DEL. ALL / CANCEL / OK footer.
The per-screen strings are chosen when the screen opens, so they go through Strings() there. FOOTER_BTNS is a module-level table built before any catalog exists, so its labels are Strings.source and the draw loop looks each one up, the same shape as ItemsData.POCKET_LABEL; tests/game3_strings_module_tables_test.lua covers it.
The words the player picks are not touched: they come from the ROM's own Easy Chat word groups.
The title is still centred the way the cart centres it, but no longer starts left of the ribbon: the longest English title fills 118 of the header's 128 px, so a longer translation would have spilled out of the blue fill.
The save window draws its stat values at a fixed x, 56 px into the window, which pret places for the English labels. The translated labels are wider in several languages ("DUREE JEU", "SPIELZEIT", "TIEMPO J."), and the time then prints over its label.
The value column now starts past the widest label, with the English gap between them. English labels leave it where pret has it.
The popup shown on entering an area takes its text from MapSectionsExtract.SECTIONS, the cart's English section names, with the floor suffix map_name_popup.c adds (" 3F", " B1F", " ROOFTOP") glued on in English. Nothing in a translation mod could reach either.
getInfo now also returns the name before the suffix (baseName), and the popup passes it through Strings() and translates the floor as a whole label ("3F", "B1F", "ROOFTOP"): the European carts number floors differently (3F is "2E" in French, "2S" in German), so a template around the number could not say it. The forest preview's name window (MapPreviewScreen), which shows the ROM's English section name, and the summary's met-location fallback go through Strings() too.
Species, move, item and trainer names reach a translation mod through registries, but nothing renames abilities: battle messages ("<POKéMON>'s <ability> prevents ...") and the summary page printed the ROM's English name whatever the language.
Abilities.name and the summary's ability line now pass the name through Strings(), keyed by the cart's English name ("SPEED BOOST", "COMPOUNDEYES"). The identifiers the battle code compares (SPEED_BOOST) and the description lookup are unchanged.
The new-game intro (controls guide, Pikachu pages, Oak's speech, name choices), the naming screen titles, the title menu, Pokédex headers and labels, the Town Map names and dungeon descriptions, option values and hints, the Help System bar, the Hall of Fame and the Trainer Card were plain literals.
The intro text tables stay English sources and are passed to Strings() when shown. Option values go through Strings() with the option key as context ("option.battleStyle"), so a mod can tell the SHIFT battle style apart from other uses of the word.
The bag, TM case, Berry Pouch, party, PC, box storage, shop, summary, save and yes/no menus drew their labels, prompts and result messages as literals, so a translation mod could not reach them.
Messages built at call time go through Strings() with whole-sentence templates. Action lists the menus compare against ("CANCEL", "EXIT", "SWITCH BOX"…) keep their English identifiers and are translated where they are drawn, as are the module-level PC labels, descriptions and TEXT_* strings, now marked with Strings.source(). Default box names are stored in English ("BOX 3") and shown translated, while names the player typed are drawn as they are.
- Hidden Items & Itemfinder (fixes#2314):
* Extract BG_EVENT_HIDDEN_ITEM (kind 7) from GBA ROM with proper flag calculation (0x3E8 + id), underfoot flags, and quantities.
* Implement overworld Itemfinder scan logic (7-tile radius, directional response, underfoot detection, SFX 65, and OEM text).
* Wire up Bag Menu Itemfinder dispatch to overworld field runtime.
- Opponent Battle HUD Caught Marker (fixes#2313):
* Add authentic Pokédex caught Poké Ball icon (B_INTERFACE_GFX_BALL_CAUGHT / tile 70) to opponent healthbox.
* Match pokefirered parity in TryAddPokeballIconToHealthbox (wild encounters only, non-tutorial, non-ghost, checked against Dex).
* Ensure status ailment icons (PSN, PAR, SLP, FRZ, BRN) take rendering priority over the caught ball marker.
- Level-Up Learnset & Movepool Assignment (fixes#2315):
* Fix wild and generated Pokémon moveset assignment to assign the 4 most recent level-up moves up to the current level instead of getting stuck on basic moves (e.g. Tackle).
- Easy Chat / Profile Quick Chat:
* Extract full word groups, categories, and dictionary entries from ROM.
* Build Easy Chat interface with group selection, paginated word lists, and alphabetized filtering.
* Fix dialog text overflowing when exiting profile / quick chat prompts.
- Nurse Joy & PC Storage / Map Polishing:
* Implement Nurse Joy healing tray animation flow and audio cues in standard scripts.
* Fix PC storage box header/wallpaper rendering and storage data extraction.
* Correct Sevii Islands / Island 1 map section lookups and region map coordinate handling.
* Fix circular dependency in battle residual handlers (partialTrap).
fixes#2325fixes#2322fixes#2315fixes#2314fixes#2313