The location header drew `session.mapName or session.map`, and nothing in
src/ ever assigns session.mapName, so the fallback always won and the
screen printed the engine's internal id ("FR_ROUTE_22").
pret prints the sMapNames place name resolved from the map header:
start_menu.c PrintSaveStats -> SAVE_STAT_LOCATION
save_menu_util.c GetMapNameGeneric(dest, gMapHeader.regionMapSectionId)
region_map.c GetMapName(dst, mapsec, 0)
and centres it in the 14-tile stats window. Do the same via the existing
MapSectionsExtract.getInfo() lookup, and centre the run instead of
drawing it at a fixed x.
getInfo() also gets two fixes on the path the save screen now depends on:
- the fuzzy id match picked whichever section pairs() happened to visit
first, so "ROUTE_22" could resolve to "ROUTE 2". Prefer the exact match,
else the longest.
- return `resolved` so callers can tell a real section from the Pallet
Town placeholder the function falls back to for unknown maps.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The FRLG level-up stat window opened and the sequence kept pumping, so the
box outlived its own step: it stopped taking input (routing is phase-gated),
never closed, and drew over the win/money text.
- exp_seq: update() now waits while the window is open (mirroring the
already-correct busy()), and finish()/reset() tear the window down silently
so a stale onDone cannot advance a sequence that no longer owns it.
- stat_growth: close(opts) gains opts.silent for teardown callers.
- init: the input-routing phase list moves into STAT_WINDOW_PHASES behind
Battle.statWindowPhase(), and Battle.update() tears down a window whose
phase can no longer dismiss it.
- ui: the stat-window draw keys off the same predicate, so input and drawing
can never disagree.
Tests: the "In-Battle Level Up Stat Growth Window" block in
tests/game3_battle_switch_and_faint_test.lua now pumps ExpSeq.update() while
the box is open, and a new CI-covered tests/engine suite pins the same
regression.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Follow-up to #2319, found while testing it: the catch flow hand-wrote
"YOUR POKEMON'S NICKNAME?" into the text-entry window. That string is
141px wide, but sWindowTemplates[WIN_TEXT_ENTRY_BOX] only gives 127px
from x=73, so the title ran ~12px over the frame's right edge.
pret composes the title instead -- DrawMonTextEntryBox prints
gSpeciesNames[mon] followed by gText_PkmnsNickname ("'s nickname?"),
which fits for every species (worst case 123px).
- Naming.monTitle() is now the single source of that string, used by
both the catch flow and special:158.
- Naming.draw clamps the title to L.titleMaxW, mirroring the GBA's
per-window glyph clip, so an over-long name can never escape the frame
(the default 240px maxWidth is wider than the box itself).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The mon branch of drawPlayerIcon reused the player overworld sprite's
16x32 box to letterbox the icon, so a 32x32 Pokemon.icon came out at
scale 0.5 -- half size inside the frame baked into bg.png. The 64x64
frontPic fallback was worse still, at 0.25.
Give the mon its own box in the layout table (pret naming_screen.c:1422
CreateMonIcon(species, SpriteCallbackDummy, 56, 40) draws it unscaled on
that centre), so the icon fills the frame 1:1 and the fallback shrinks to
fit the same box. Also pin the frame to quads[0]: pret's dummy callback
leaves the icon on its first frame, not the 1px-shifted second pose.
The player/rival overworld slot keeps its 16x32 box untouched.
Fixes#2319
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The Gen 3 extractor emitted eggCycles/eggGroups but never egg moves, so the
gen3_dexnav hidden-mon roll was inert on FireRed: a hidden mon kept its
level-up moves and could never gain an egg move.
Decode the ROM's own gEggMoves table (pokefirered/src/data/pokemon/
egg_moves.h) into pokemon/egg_moves.lua and expose it the same way as
learnsets/tmhm:
- Versions: EGG_MOVES = 0x25EF0C plus the 20000 species offset, the 0xFFFF
run terminator and a defensive per-species cap. The table is one flat u16
stream of `{ species + 20000, move..., 0xFFFF }` runs with no final
terminator, so the first word that is neither a header nor a plausible move
id ends the scan.
- PokemonExtract: extract_egg_moves + write_egg_moves_lua, written to the
cache by run(), required by ready() so a stale cache re-extracts, and
returned as pack.eggMoves. FORMAT_VERSION 3 -> 4.
- Pokemon.eggMoves(species): runtime accessor with the same species coercion
as Pokemon.learnset; nil for a species with no egg move.
- Schemas: eggMoves on monTables/monRecord/writeMon and the Gen 3 species
field list, so a mod reads record.eggMoves as move ids and writes names back.
- Versions.CACHE_VERSION 98 -> 99 to force re-extraction.
The table is sparse: species without egg moves are absent rather than an
empty list, all the way from the ROM to the mod record.
Validated against a supported FireRed USA 1.0 dump: 165 species, 973 moves,
<=8 per species, Bulbasaur {113,130,219,204,80,345,320,174}, Mankey
{157,193,96,68,179,251,279,265} (no Toxic), Mew nil.
Tests: new tests/engine/game3_egg_moves.lua (31 checks ROM-free, 39 with a
ROM), gate_gen3_mod_api 734/734, engine tier 587/587, modkit tier 37/37,
lint --gate clean.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
dev added the public Encounters.tableFor export (MapCatalog resolution,
prefix stripping, route normalization) at the same point in
src/core/game3/encounters.lua where this branch inserts the cooldown
block. The two changes are purely additive, so both are kept: dev's
export sits next to table_for where it belongs, followed by the
cooldown/rate-test block.
scripts/lint.sh auto-merged cleanly, keeping both dev's PATH export for
node_modules/.bin and ~/.luarocks/bin and this branch's luacheck --version
probe.
Re-verified after the merge that dev's RNG-parity commit does not shift
the encounter goldens: 88/88 cooldown checks still pass, including the
Route 1 4000-step counts (401 with the grace period vs 858 without).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Completes the pret/pokefirered parity for wild encounter rate gating,
on top of the cooldown port in b84c2c15.
Ports DoWildEncounterRateTest as Encounters.encounterRate(): *16, bike
*80/100, banked failure rate +buff*16/200, flute, Cleanse Tag *2/3,
ability (Stench /2, Illuminate *2), then the 1600 clamp last -- pret's
exact order, which the modifier-order tests pin down.
Ports AddToWildEncounterRateBuff: a failed rate test (or a rate test
that passes but picks no slot) banks the area rate; an active Repel
zeroes the bank. Without this, encounters were still flat-rate rather
than ramping after near-misses.
Also ports the Mach/Acro bike *80/100 modifier.
End-to-end goldens on Route 1 (rate 21, 4000 steps) shift 397->401
with the cooldown and 843->858 without. That matches pret's steady
state analytically: a 6-step cooldown followed by p~0.112 as the bank
ramps gives a cycle of (1-0.94^6)/0.112+6 ~ 10.3 steps, ~10%/step.
The observed 401/4000 = 10.03% confirms it.
tests/game3_rng_test.lua now resets rate modifiers per replay, since
the banked rate makes sequences history-dependent; a map load resets
in-game.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Gen3 (FireRed) had no cooldown between wild battles, so every step on an
encounter tile rolled at the area's full rate -- Route 1 (~21%) landed a
battle on roughly one step in five. Gen2 already had this via
World:wildCooldownStep(); game3 never did.
Port pret/pokefirered wild_encounter.c:
- GetMapBaseEncounterCooldown -> Encounters.mapBaseCooldown: steps of
immunity derived from the area's own rate (rate >= 80 -> none, < 10 ->
8, else 8 - rate/10).
- HandleWildEncounterCooldown -> Encounters.handleCooldown: a soft floor,
not a hard gate -- once the minimum elapses a 5%/step leak lets a battle
through anyway. Includes the White/Black Flute, Cleanse Tag, Stench and
Illuminate modifiers in pret's application order (Cleanse Tag before the
ability mod, which changes the result).
- ResetEncounterRateModifiers -> Encounters.resetRateModifiers, wired to
the two places pret resets: map load (Map.load, including seamless
connection crossings) and battle start (BattleBridge.startWild). The
latter is the bug-#1229 class -- scripted battles and fishing re-arm the
grace period even though no step rolled.
Measured on Route 1 over 4000 steps: 843 encounters before, 397 after;
first encounter never lands before step 7.
Not ported (makes encounters more likely, so it does not affect the
reported symptom): the encounterRateBuff anti-frustration ramp and the
Mach/Acro bike rate modifier.
tests/engine/wild_encounter_cooldown.lua covers the cooldown golden table,
the counter/leak mechanics, all five modifiers, both re-arm paths and the
end-to-end step count.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Collision.installWarps force-marked every map-header warp cell walkable. That
is right for the outdoor MB_WARP_DOOR tiles extract can leave solid, but it also
opened cells that only carry a dead warp event. PalletTown_PlayersHouse_1F has
one at (3,9): the wall tile directly left of the door mat, mid 26, coll 1,
behavior 0x00 MB_NORMAL. pret never lets the player stand there, so the warp
never fires; the forced COLL_DOOR let the player walk out of the house through
the wall and land in PalletTown.
The repair now fires only when the behavior is unreadable (tileset attrs stopped
before that mid, so Collision.behavior returns nil) or is a behavior pret would
actually warp on. Collision.isWarpMetatileBehavior covers 0x60-0x6F plus 0x71,
matching field_control_avatar.c: the arrow warps (TryArrowWarp), the directional
stair warps (IsDirectionalStairWarpMetatileBehavior), and everything
IsWarpMetatileBehavior accepts (cave door, ladder, fall warp, regular warp,
Lavaridge 1F warp, warp door, escalators, union-room warp).
Warp indexing is unchanged: a dead warp on a wall is still registered, only the
grid write is gated.
tests/engine/firered_house_wall_warp_bug2297.lua is ROM-free: it embeds the pret
13x10 map.bin grid and the `building` metatile attributes, translates them with
the real ScriptingCollision.fromCell, binds the real Versions.WARPS table, and
asserts the wall stays solid, the four warp cells still behave, the repair still
applies for unreadable attrs and live warp behaviors, and an exhaustive
0x00-0xFF table pins the predicate. It fails 6 checks against the pre-fix file.
luajit tests/run_engine.lua: 585/585 suites passed.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
RFC 0021 gave a mode back the bag on the link cable: opts.items makes an
item the turn, and `{ type = "action", kind = "item", item, index, move }`
rides the wire so the peer -- and a spectator -- can apply the same effect
to their copies before the moves. The peer never got the item.
Wire.sanitize rebuilds every inbound message field by field from
SCHEMAS[type] and drops anything the schema does not name. SCHEMAS.action
names kind, slot and index; `item` and `move` are not among them, so they
were stripped on the way in. Added beside the others, clamped the same
way (a string id, a move slot in 1..MAX_MOVES). SCHEMAS.spectate calls
the same sanitize, so a spectator is fixed by the same line.
Why the tests did not see it. tests/engine/link_items.lua -- and every
in-process link test -- pairs the two sides with Net.loopbackPair, which
hands the table straight over; sanitize only runs on the way through
Session, which is every REAL transport. So the failure needed two
machines to show up, and when it did it was the quietest kind: the peer
receives an item action with no item in it, LinkItems.apply returns
nothing, the turn is still spent, nothing is printed, and the two
simulations part by one heal with nothing on either screen to say so --
until a hash several turns later blames the wrong turn. Found by a
downstream mod's two-client harness on its first real duel: the guest
healed 12 -> 18 and the host watched its copy of that mon go 12 -> 6.
link_items.lua now pins the schema directly (an item action through
sanitize, through a spectate wrapper, and a move action carrying no
item), which is a check a loopback pair cannot make. docs/rfcs/0021
gains the section saying a new field on an existing message type is
invisible until SCHEMAS knows about it.
tests/run_link_tests.lua green; the engine tier matches stock's reds
(572/584, the same twelve).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A link battle runs under cable rules and LinkBattle keeps them: openItems
prints "Items can't be used in a link battle!" and the wire knows move,
struggle, locked, switch and RUN. Right for the Cable Club, wrong for a
mode that is not the Cable Club -- a battle royale played over
LinkState.newFromSession fights with real, damaged parties, and the
potions and X items on the ground are its whole economy; against a bot
the bag works, against a person it says no. The mode cannot fix it:
submit, resolveLockstep and the decoder are closures, and there is no
action that means "nothing" to spend the turn on.
opts.items = true on newHost/newGuest (off by default, set on both
machines like turnLimit): the bag is BattleState.openItems -- the vanilla
BagMenu against this battle, whose picker already offers the clamped
copies -- and the effect is ItemEffects.use as in any fight. What
changes is what spending the turn means: itemUsed puts { kind = "item",
item, index, move } on the wire as the turn's action instead of running
the AI's reply. Both machines and a spectator resolve it before switches
and moves: the user's side is already applied (the bag did it), the other
side applies the same effect to its own copies of that side through
src/link/LinkItems.lua -- ItemEffects.use behind a battle whose player is
the user's battler and a save whose party is the user's copies, nothing
consumed -- and prints "<name> used <ITEM>!" plus the effect's lines.
Every effect reachable in a battle is deterministic, so the per-turn
hash still agrees. BagMenu hands itemUsed the item, target and move it
used; a local battle ignores them. A ball on the cable is refused
(ItemEffects), the way the doll is in any trainer battle.
tests/engine/link_items.lua: cable rules by default; opted in, the host's
POTION heals the guest's copy before the moves, both print the lines,
one turn, hashes agree; a spectator fed the same two messages heals its
copy too. docs/modding.md and docs/rfcs/0021-link-battle-items.md.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- Align CacheContract firered overrides with pure ROM extraction
- Fix door animation extraction tile sizes, palettes, and strides
- Update storage chrome, pokedex, summary, and battle chrome extractors
- Ensure 100% self-contained ROM extraction without external dependencies
Kanto Companion's Edit Mode can't run during battle -- there's no way
to stop a D-pad press from also driving the battle menu, so the only
option today is pausing the game outright. This adds the same input
precedence a mod had before the sandbox changes: vanilla is the whole
existing callback body, and the hook fires before any of it runs.
input.wheel stays a plain observer like input.pointer, since nothing
depends on suppressing it.