diff --git a/mod_option_schemas.json b/mod_option_schemas.json index 4002f364..592ffeba 100644 --- a/mod_option_schemas.json +++ b/mod_option_schemas.json @@ -1 +1 @@ -{"schema_version":1,"mods":[]} \ No newline at end of file +{"mods":[],"schema_version":1} \ No newline at end of file diff --git a/scripts/test.sh b/scripts/test.sh index 09396642..33e2d67d 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -23,19 +23,6 @@ # POKEPORT_IDENTITY=pokeport-test-caches POKEPORT_VERSION= \ # POKEPORT_IMPORT_ONLY=1 POKEPORT_IMPORT_ROM="" love . # An explicit RED_CACHE/GOLD_CACHE/... in the environment always wins. -# -# T6 runs every top-level tests/game3_*.lua suite (the game3 scenario -# coverage: battle, capture, menus, overworld, script specials, save). -# Suites self-skip with exit 0 when an artifact they need is absent, so -# the tier runs anywhere; nothing is ever skipped by name. Failures not -# in KNOWN_GAME3_FAILURES (the docs/game3/game3-suite-sweep-v113.md -# baseline) fail the gate; listed ones print as known. GAME3_JOBS sets -# suite parallelism (default 8; GAME3_JOBS=1 serializes). -# -# A failing suite prints its first [FAIL] line (or first crash/error line, -# or a dead-worker notice when it produced no output at all) so the cause -# is on the terminal, and the per-suite logs are KEPT (path printed) for -# triage instead of deleted. No retries anywhere: every failure stands. set -uo pipefail @@ -146,72 +133,25 @@ run_tier "T1/T2 engine invariants + parity gates" "$LUA" tests/run_engine.lua run_tier "T2 Gen 2 / Crystal suites" "$LUA" tests/run_gen2.lua run_tier "T4 mod-SDK" "$LUA" tests/run_modkit.lua -# ------- T6: every top-level tests/game3_*.lua suite (273 files) -# -# Discovery is the tests/game3_*.lua glob -- the same convention the sweep -# docs use -- run as standalone "$LUA" processes. Suites self-skip -# with exit 0 when an artifact they need (imported cache, ../pokefirered, -# a ROM path, an anim pack) is absent, so a missing artifact never fails -# the tier and no suite is ever skipped by name here. A non-zero exit is a -# failure: it prints below and fails the tier UNLESS the suite is listed in -# KNOWN_GAME3_FAILURES. GAME3_JOBS (default 8) batches the processes. -# -# KNOWN_GAME3_FAILURES is the frozen 25-failure baseline from -# docs/game3/game3-suite-sweep-v113.md minus the two stale G1-contract stubs -# fixed this pass (game3_link_session, game3_save_trainer_card) = 23 names, -# as a CEILING: a listed suite may fail without failing the gate while the -# fix wave burns the list down, and a failure in any suite NOT listed always -# fails the gate. Delete a name the moment its suite goes green so a later -# regression there fails again. -KNOWN_GAME3_FAILURES='game3_battle_ai_test -game3_special_events_test -game3_special_trade_test -game3_cerulean_block_exits_test -game3_cerulean_policeman_bill_test -game3_collision_npc_dir_test -game3_emote_movement_test -game3_item_use_and_parcel_test -game3_map_onload_test -game3_mapscripts_test -game3_npc_player_collision_test -game3_objects_perm_reset_test -game3_runtime_camera_object_test -game3_static_encounter_test -game3_stitchcoll_escape_warp_test -game3_stitchcoll_ghost_ctx_test -game3_stitchcoll_move_kinds_test -game3_stitchcoll_run_speed_test -game3_stitchfield_ground_test -game3_vermilion_trash_cans_test -game3_viridian_gym_door_test -game3_oaks_lab_save_reload_test -game3_trainer_sight_test' - -# T6's data-driven suites read imported game3 data (data/generated, packs, -# caches) out of the LOVE identity the runner uses: POKEPORT_IDENTITY when -# set, otherwise the default pokemon-love2d. A fresh CI checkout has none of -# it, and those suites fail while reading it instead of self-skipping -- an -# environment gap, not a regression. With the data present (any machine that -# has imported a ROM) every failure gates as usual. Test by pointing -# POKEPORT_IDENTITY at an empty identity. -game3_artifacts_absent() { - local ident=${POKEPORT_IDENTITY:-pokemon-love2d} - [ -d data/generated ] && return 1 - local root d - for root in "$HOME/Library/Application Support/LOVE/$ident" \ - "$HOME/.local/share/love/$ident"; do - [ -d "$root" ] || continue - for d in "$root"/*; do - [ -d "$d/data/generated" ] && return 1 - done +game3_kill_jobs() { + local p + for p in ${GAME3_PIDS:-}; do + pkill -TERM -P "$p" 2>/dev/null + kill -TERM "$p" 2>/dev/null done - return 0 +} + +game3_cleanup() { + game3_kill_jobs + [ -n "${GAME3_TMP:-}" ] && rm -rf "$GAME3_TMP" } run_game3_tier() { local jobs=${GAME3_JOBS:-8} case "$jobs" in ''|*[!0-9]*) jobs=8 ;; esac [ "$jobs" -ge 1 ] || jobs=8 + local limit=${GAME3_TIMEOUT:-300} + case "$limit" in ''|*[!0-9]*) limit=300 ;; esac local suites=(tests/game3_*.lua) if [ ! -f "${suites[0]:-}" ]; then @@ -219,26 +159,36 @@ run_game3_tier() { return 1 fi local total=${#suites[@]} - echo "-- T6 game3: running $total top-level suites, $jobs at a time" + echo "-- T6 game3: running $total top-level suites, $jobs at a time, ${limit}s limit each" - local tmp - tmp=$(mktemp -d "${TMPDIR:-/tmp}/game3gate.XXXXXX") || return 1 + GAME3_TMP=$(mktemp -d "${TMPDIR:-/tmp}/game3gate.XXXXXX") || return 1 + GAME3_PIDS="" + local tmp=$GAME3_TMP + trap game3_cleanup EXIT + trap 'game3_cleanup; exit 130' INT + trap 'game3_cleanup; exit 143' TERM local suite base i=0 for suite in "${suites[@]}"; do base=${suite##*/} base=${base%.lua} ( - "$LUA" "$suite" >"$tmp/$base.log" 2>&1 + perl -e 'alarm shift; exec @ARGV or exit 127' "$limit" "$LUA" "$suite" \ + >"$tmp/$base.log" 2>&1 echo $? >"$tmp/$base.rc" - ) & + ) 2>/dev/null & + GAME3_PIDS="$GAME3_PIDS $!" i=$((i + 1)) - [ $((i % jobs)) -eq 0 ] && wait + if [ $((i % jobs)) -eq 0 ]; then + wait + GAME3_PIDS="" + fi done wait + GAME3_PIDS="" - local passed=0 known=0 fresh=0 - local fresh_list="" + local passed=0 skipped=0 failed=0 + local fail_list="" for suite in "${suites[@]}"; do base=${suite##*/} base=${base%.lua} @@ -250,66 +200,44 @@ run_game3_tier() { fi if [ "$rc" = "0" ]; then passed=$((passed + 1)) + grep -q '^\[skip\]' "$tmp/$base.log" 2>/dev/null && skipped=$((skipped + 1)) continue fi - if printf '%s\n' "$KNOWN_GAME3_FAILURES" | grep -qx "$base"; then - known=$((known + 1)) - echo " known-fail $suite (exit $rc)" + failed=$((failed + 1)) + fail_list="$fail_list $suite" + echo " FAIL $suite (exit $rc)" + if [ "$rc" = "142" ]; then + echo " | TIMEOUT: killed after ${limit}s (GAME3_TIMEOUT)" + tail -6 "$tmp/$base.log" | sed 's/^/ | /' + elif [ ! -s "$tmp/$base.log" ]; then + echo " | EMPTY OUTPUT: the worker never printed anything" \ + "(exit $rc; 137/143 = killed, NO-EXIT-CODE = worker never ran) --" \ + "environment/parallelism problem, NOT a suite assertion" else - fresh=$((fresh + 1)) - fresh_list="$fresh_list $suite" - echo " FAIL $suite (exit $rc)" - # Name the cause on the spot: assertion vs crash vs dead worker, so a - # parallel-only anomaly is classifiable from the tier output alone. - if [ ! -s "$tmp/$base.log" ]; then - echo " | EMPTY OUTPUT: the worker never printed anything" \ - "(exit $rc; 137/143 = killed, NO-EXIT-CODE = worker never ran) --" \ - "environment/parallelism problem, NOT a suite assertion" + local cause + cause=$(grep -m1 '^\[FAIL\]' "$tmp/$base.log" || true) + if [ -n "$cause" ]; then + echo " | assert: $cause" else - local cause - cause=$(grep -m1 '^\[FAIL\]' "$tmp/$base.log" || true) - if [ -n "$cause" ]; then - echo " | assert: $cause" - else - cause=$(grep -m1 -E 'stack traceback|\.lua:[0-9]+:|Too many open files|not enough memory' \ - "$tmp/$base.log" || true) - echo " | crash: ${cause:-non-zero exit with no recognized cause (see log tail)}" - fi - tail -6 "$tmp/$base.log" | sed 's/^/ | /' + cause=$(grep -m1 -E 'stack traceback|\.lua:[0-9]+:|Too many open files|not enough memory' \ + "$tmp/$base.log" || true) + echo " | crash: ${cause:-non-zero exit with no recognized cause (see log tail)}" fi + tail -6 "$tmp/$base.log" | sed 's/^/ | /' fi done - # Missing-artifact tolerance (game3_artifacts_absent above): when this - # machine has no imported game3 data at all, a suite that dies reading that - # data is recorded as an artifact skip instead of a fresh failure. With the - # data present the very same failure gates, so a regression in one of these - # suites still fails a real checkout. - local artifact_skips=0 artifact_list="" - if [ "$fresh" -gt 0 ] && game3_artifacts_absent; then - artifact_skips=$fresh - artifact_list=$fresh_list - fresh=0 - fresh_list="" - fi + echo "-- T6 game3: $total run, $passed passed ($skipped self-skipped), $failed failed" - echo "-- T6 game3: $total run, $passed passed, $known known failure(s), $fresh new failure(s)" - if [ "$artifact_skips" -gt 0 ]; then - echo "-- T6 game3: no imported game3 data in identity '${POKEPORT_IDENTITY:-pokemon-love2d}'" \ - "-> $artifact_skips suite failure(s) recorded as missing-artifact skips (they gate on a machine with imports):$artifact_list" - fi - if [ "$fresh" -gt 0 ]; then - echo " new failures (not in the sweep-v113 baseline):$fresh_list" - fi - - local ok=1 - [ "$fresh" -eq 0 ] && ok=0 - if [ "$fresh" -gt 0 ]; then + trap - EXIT INT TERM + GAME3_TMP="" + if [ "$failed" -gt 0 ]; then + echo " failures:$fail_list" echo " per-suite logs kept for triage: $tmp" - else - rm -rf "$tmp" + return 1 fi - return $ok + rm -rf "$tmp" + return 0 } run_tier "T6 game3 top-level scenario suites" run_game3_tier @@ -344,22 +272,10 @@ run_content_behavior() { local lines lines=$(printf '%s\n' "$out" | grep '^FAIL ' | sort) - # Verdict = exit code AND the FAIL-line allowlist together. The exit code - # alone used to be discarded: a mid-run crash (unresolvable require under - # POKEPORT_DATA_DIR) left hundreds of green checks, ZERO FAIL lines, rc=1 - # from luajit -- count matched KNOWN_CONTENT_FAILURES=0 and the tier printed - # PASS. Any rc!=0 with no FAIL line at all is that crash signature. - # Capture-then-print the stderr line: piping grep -m1 straight off $out - # trips SIGPIPE under pipefail and double-reports via the fallback. if [ "$count" -eq "$KNOWN_CONTENT_FAILURES" ] \ && [ "$lines" = "$(printf '%s\n' "$KNOWN_CONTENT_LINES" | sort)" ]; then if [ "$rc" -ne 0 ] && [ "$count" -eq 0 ]; then printf '%s\n' "$out" | tail -3 - # stdout is block-buffered and stderr unbuffered under $(...) 2>&1, so the - # luajit error can land MID-LINE after a buffered "ok ..." print -- never - # anchor ^luajit:. Extract from 'luajit:' onward, cap the length (the - # module-not-found dump is megabytes; BSD grep caps intervals at 255), - # fall back to the traceback line. local crash="" crash=$(printf '%s\n' "$out" | grep -o 'luajit:.\{0,255\}' | head -1 || true) if [ -z "$crash" ]; then @@ -379,8 +295,6 @@ run_content_behavior() { local faillines="" faillines=$(printf '%s\n' "$out" | grep '^FAIL ' || true) - # here-string, not echo|head: one FAIL line can be megabytes (module-not-found - # dumps) and echo would die of SIGPIPE mid-print under pipefail. head -10 <<<"$faillines" | cut -c1-160 || true printf '%s\n' "$out" | tail -2 echo "expected exactly $KNOWN_CONTENT_FAILURES known failures; got $count" diff --git a/src/core/ChipAudio.lua b/src/core/ChipAudio.lua index ddd57703..f9a705c5 100644 --- a/src/core/ChipAudio.lua +++ b/src/core/ChipAudio.lua @@ -622,12 +622,6 @@ end -- program (20 §2 cache contract, chip music row) Assets.register(ChipAudio.invalidate) --- Process shutdown used to be registered here, which required --- SessionLifecycle at module load and closed the static cycle --- ChipAudio -> SessionLifecycle -> Music/Sound -> ChipAudio (review-v3 I6). --- SessionLifecycle.endProcess now asks for ChipAudio.shutdown through --- package.loaded instead, like every other optional subsystem there. - -- --------------------------------------------------------------------------- -- one-shot effects (SFX, cries, low-health alarm): synchronous static Sources -- --------------------------------------------------------------------------- diff --git a/src/core/CollPermissions.lua b/src/core/CollPermissions.lua index 8ea6e9b8..c8805e04 100644 --- a/src/core/CollPermissions.lua +++ b/src/core/CollPermissions.lua @@ -1,15 +1,4 @@ --- COLL byte -> permission vocabulary shared by the gen2 world runtime and the --- Gen 3 import path. --- --- Why it lives in src/core (review-v3 I9, import/world seam): import --- src/import/gba/native_pack.lua classifies COLL bytes while packing FRLG --- native mid layouts, and src/world/gen2/Map.lua classifies them while the --- player walks. One table, two consumers below different layers, so the table --- lives above both and src/world/gen2/Permissions.lua re-exports it for its --- existing callers. Import must not reach into src/world for it. --- --- Provenance: pokegold CollisionPermissionTable (lo nybble), --- home/map_objects.asm GetTilePermission. LAND=0, WATER=1, WALL=0x0f. +-- home/map_objects.asm local CollPermissions = {} @@ -17,7 +6,6 @@ CollPermissions.LAND = 0x00 CollPermissions.WATER = 0x01 CollPermissions.WALL = 0x0f --- CollisionPermissionTable, lo nybble only (256 entries). local TABLE = { 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 15, 0, 0, 15, 0, 0, 0, 0, 15, 0, 0, 15, 0, 0, @@ -54,14 +42,10 @@ function CollPermissions.isWall(coll) return CollPermissions.of(coll) == CollPermissions.WALL end --- Walkable on foot: DoPlayerMovement's .CheckWalkable, which is nothing more --- than "the permission is LAND_TILE". function CollPermissions.isWalkable(coll) return CollPermissions.of(coll) == CollPermissions.LAND end --- Ledges: a ledge tile is LAND in the permission table, and only the hi nybble --- (the $a0 family — $a0-$a7 carry the defined hop variants) marks it as one. function CollPermissions.isLedge(coll) if coll == nil or coll < 0 then return false end return math.floor((coll % 256) / 16) == 0xa diff --git a/src/core/Game3.lua b/src/core/Game3.lua index 7039761f..74daf487 100644 --- a/src/core/Game3.lua +++ b/src/core/Game3.lua @@ -19,8 +19,6 @@ local QuestLog = require("src.ui.game3.quest_log") local QuestRecorder = require("src.core.game3.quest_log_recorder") local ModRuntime = require("src.mods.Runtime") --- review-v3 S9 (auditor W3 cites): hot-path pcalls that never logged. Warn --- once per key so a permanently failing hook cannot spam the frame loop. local s9Warned = {} local function s9log(key, err) if s9Warned[key] then return end @@ -393,8 +391,6 @@ function Game3:_handleBootAction(action) local modsDiff = SaveData.modsDiff and SaveData.modsDiff(save, activeMods) or nil local session = Schema.fromSaveTable(save) Options.bind(session, self.options) - -- Refuse legacy Sevii leftovers: the prefix list comes from the game's - -- profile (T0.2 handoff, rse-seams section 4). local legacy = Profile.of(session.version).map.legacyPrefixes or {} local legacyMap = false if type(session.map) == "string" then @@ -574,12 +570,10 @@ function Game3:update(dt) while self._audioAccum >= STEP and guard < 8 do self._audioAccum = self._audioAccum - STEP guard = guard + 1 - -- review-v3 S9: log the swallowed Audio.update failure once (Game3.lua:567). local okA, errA = pcall(Audio.update, STEP) if not okA then s9log("audio", errA) end end if self._audioAccum > 0.25 then self._audioAccum = 0 end - -- review-v3 S9 (Game3.lua:570). local okT, errT = pcall(function() require("src.render.Tilt").update(dt) end) if not okT then s9log("tilt", errT) end end @@ -594,7 +588,6 @@ function Game3:_drawHud(w, h) scale = scale, } love.graphics.push("all") - -- review-v3 S9 (Game3.lua:583): log the swallowed render.hud hook once. local okR, errR = pcall(function() ModRuntime.call("render.hud", noop, self, viewport) end) if not okR then s9log("render.hud", errR) end love.graphics.pop() @@ -1015,8 +1008,6 @@ function Game3:reset() Audio.endSession() require("src.ui.game3.stack").clear() clearFieldScreens() - -- review-v3 A3/A4: a reset releases the warp busy-state and the cached - -- door sheet Images/Quads along with everything else. local WarpMod = package.loaded["src.core.game3.warp"] if WarpMod and WarpMod.clear then pcall(WarpMod.clear) end local DoorsMod = package.loaded["src.core.game3.doors"] diff --git a/src/core/SaveData.lua b/src/core/SaveData.lua index 250e34b5..662c6e60 100644 --- a/src/core/SaveData.lua +++ b/src/core/SaveData.lua @@ -17,9 +17,6 @@ local Runtime = require("src.mods.Runtime") local Semver = require("src.mods.Semver") local Boxes = require("src.pokemon.Boxes") local Stats = require("src.pokemon.Stats") --- Bag is required at its one call site below (review-v3 I6): a load-time --- SaveData -> Bag edge closes Data -> CacheFs -> SaveData -> Bag -> Data, so --- the require moves to the call site and the cycle loses its top-level leg. local Badges = require("src.inventory.Badges") local GameVersion = require("src.core.GameVersion") @@ -110,10 +107,6 @@ local function makePortableFs(dir) -- portable mode writes real files through io.*, which will not -- create missing parent directories; mkdir the tree so a slot path -- like "saves/red" exists before a write lands inside it - -- review-v3 L2: this path is slot-derived and reaches a shell — refuse - -- anything outside plain path characters (quotes, `;`, `$`, `..` never - -- occur in a legitimate save/export directory) instead of interpolating - -- it unescaped. if type(name) ~= "string" or name:find("[^%w%._%-%/]") or name:find("%.%.") then return false end @@ -955,10 +948,6 @@ end local function slotDir(key) return "saves/" .. key end --- A slot id is only ever something like "slot1"; it is joined straight into a --- save path, so anything else (separators, "..", absolute fragments) is --- refused at this single choke point (review-v3 L3: slot ids were never --- validated, so the save root was escapable). local function valid_slot_id(id) return type(id) == "string" and id:match("^slot%d+$") ~= nil end @@ -1153,8 +1142,6 @@ function saveNames(version, injectedFs) local slot = activeSlotCache[key] if slot then local main, bak, tmp = slotNames(key, slot) - -- An unusable slot id in the registry must never reach a path; treat the - -- scope as having no slot (the legacy flat names) instead. if main then return main, bak, tmp end end return legacyNames(key) diff --git a/src/core/SessionLifecycle.lua b/src/core/SessionLifecycle.lua index 3bcdceef..dbdc5f74 100644 --- a/src/core/SessionLifecycle.lua +++ b/src/core/SessionLifecycle.lua @@ -130,15 +130,6 @@ end function SessionLifecycle.endProcess() for _, fn in ipairs(processShutdowns) do pcall(fn) end - -- ChipAudio.shutdown used to be in processShutdowns, registered from - -- ChipAudio's own module load -- which required this file at load time and - -- closed ChipAudio -> SessionLifecycle -> Music/Sound -> ChipAudio - -- (review-v3 I6). Ask for it through package.loaded like endGameSession - -- does for stopMusic, so SessionLifecycle never pulls the audio stack in and - -- a system that never loaded audio (headless tools, save editor) is untouched. - -- Runs last on purpose: joining the worker thread is independent of the - -- Fetch/Check/game3-audio shutdowns above, and a deterministic position beats - -- the old load-order-dependent one. local chip = package.loaded["src.core.ChipAudio"] if chip and chip.shutdown then pcall(chip.shutdown) end end diff --git a/src/core/game3/bag.lua b/src/core/game3/bag.lua index f88e7dec..c32ab7ce 100644 --- a/src/core/game3/bag.lua +++ b/src/core/game3/bag.lua @@ -94,9 +94,6 @@ local function sanitize_pockets(bag) bag.pockets[k] = compact(keep) end - -- review-v3 F5: sanitize must not exceed the invariants it enforces — - -- clamp merges to MAX_ITEM_QTY (items.lua:9, Qty ≤ 999) and refuse new - -- slots past the pocket capacity (ItemsData.CAPACITY). for _, m in ipairs(misplaced) do local targetSlots = bag.pockets[m.target] or {} local cap = ItemsData.CAPACITY[m.target] or 42 @@ -301,8 +298,6 @@ function Bag.add(bag, id, qty) return placed == qty, placed end - -- review-v3 F6: report the clamped placed amount, not the requested qty - -- (mirrors the existing-slot return above). local placed = Items.clampGame3(qty) slots[#slots + 1] = { id = storeId, qty = placed } if pocket == "TM_CASE" then diff --git a/src/core/game3/battle/ai.lua b/src/core/game3/battle/ai.lua index 508466d6..278f1361 100644 --- a/src/core/game3/battle/ai.lua +++ b/src/core/game3/battle/ai.lua @@ -377,10 +377,7 @@ function choose_move_core(st, id, opts) if aiFlags ~= 0 and pack and pack.table and pack.scripts and target then aiAction = run_scripts(pack, aiFlags, st, b, target, userSide, targetSide, scores, simulatedRNG, rng) elseif st.safari then - -- data/battle_ai_scripts.s:3242 AI_Safari is just - -- `if_random_safari_flee` -> flee, else watch. The pack is extracted - -- from the ROM, so mirror that two-command script inline when it is not - -- available (ROM-less CI); otherwise the Safari foe never flees. + -- data/battle_ai_scripts.s:3242 local okR, Rules = pcall(require, "src.core.game3.battle.rules") local rate = okR and Rules.safari.fleeRate(st.safariState) or 0 aiAction = (random_u16(rng) % 100 < rate) and 0x2 or 0x4 diff --git a/src/core/game3/battle/ai_cmds.lua b/src/core/game3/battle/ai_cmds.lua index 85343198..339982a7 100644 --- a/src/core/game3/battle/ai_cmds.lua +++ b/src/core/game3/battle/ai_cmds.lua @@ -907,10 +907,7 @@ end function CMD.get_protect_count(vm, op) local b = AiCmds.battler(vm, op.battler) - -- pokefirered/src/battle_ai_script_commands.c:1847-1856 reads - -- gDisableStructs[battlerId].protectUses; the engine keeps that counter as - -- expProtectStreak (effects/volatiles.lua:14-26), and the old read of the - -- never-written `protectUses` always returned 0 (review-v3 C7). + -- pokefirered/src/battle_ai_script_commands.c:1847-1856 vm.funcResult = (b and b.expProtectStreak) or 0 next_ip(vm) end diff --git a/src/core/game3/battle/anim.lua b/src/core/game3/battle/anim.lua index 03d4bd73..3672c7f2 100644 --- a/src/core/game3/battle/anim.lua +++ b/src/core/game3/battle/anim.lua @@ -528,8 +528,6 @@ function Anim.tweenHp(side, fromHp, toHp, maxHp, opts) end, function() Anim._stageTasks[t.id] = nil p.displayHp = toHp - -- review-v3 D3: only the CURRENT tween may drop the busy flag; an older - -- overlapping completion must not clear a newer tween's claim. if Anim._hpTweenTask == t.id then Anim._hpTweening = false Anim._hpTweenTask = nil @@ -580,7 +578,6 @@ function Anim.tweenExp(side, fromRatio, toRatio, opts) end, function() Anim._stageTasks[task.id] = nil p.displayExp = toRatio - -- review-v3 D3: same owner guard as the HP tween. if Anim._expTweenTask == task.id then Anim._expTweening = false Anim._expTweenTask = nil diff --git a/src/core/game3/battle/anim_pal.lua b/src/core/game3/battle/anim_pal.lua index abf1d5ba..2444e49d 100644 --- a/src/core/game3/battle/anim_pal.lua +++ b/src/core/game3/battle/anim_pal.lua @@ -310,7 +310,6 @@ function AnimPal.begin(s, img, opts) return idx end --- D1: one reused options table (AnimPal.resolve reads it synchronously). local _spriteBlendOpts = {} function AnimPal.beginSprite(s, img, vm) diff --git a/src/core/game3/battle/anim_port/g2_mon_sizes.lua b/src/core/game3/battle/anim_port/g2_mon_sizes.lua index 034ccce7..796c6983 100644 --- a/src/core/game3/battle/anim_port/g2_mon_sizes.lua +++ b/src/core/game3/battle/anim_port/g2_mon_sizes.lua @@ -1,7 +1,3 @@ --- Derived view of the canonical pic-size table (g1_pic_sizes). --- The Gen 2 port consumes the packed front/back_pic_coordinates header as --- (width/8)*16 + (height/8); g1_pic_sizes stores the same header packed as --- (width<<8) | height, so the conversion is exact for every species entry. local sizes = require("src.core.game3.battle.anim_port.g1_pic_sizes") local function convert(packed) diff --git a/src/core/game3/battle/anim_port/g2_pret.lua b/src/core/game3/battle/anim_port/g2_pret.lua index 9df022dc..86e55cbb 100644 --- a/src/core/game3/battle/anim_port/g2_pret.lua +++ b/src/core/game3/battle/anim_port/g2_pret.lua @@ -3,7 +3,6 @@ local AnimPal = require("src.core.game3.battle.anim_pal") local AnimCoords = require("src.core.game3.battle.anim_coords") local Trig = require("src.core.game3.trig") --- D1: one reused options table (AnimPal.resolve reads it synchronously). local _pfxBlendOpts = { coeff = 0, color = 0 } local P = {} diff --git a/src/core/game3/battle/anim_port/g3_e3b.lua b/src/core/game3/battle/anim_port/g3_e3b.lua index e36ad708..b0443d7b 100644 --- a/src/core/game3/battle/anim_port/g3_e3b.lua +++ b/src/core/game3/battle/anim_port/g3_e3b.lua @@ -150,8 +150,6 @@ C.GreenStar = P.cb(function(s, vm) s.callbackFn = H.greenStarStep1 end) --- pret sDoomDesireCoords has 4 entries; the trailing 0 is the explicit guard for the --- out-of-range step index pret reads OOB at the two DOOM_COORDS lookups below. H.DOOM_COORDS = { [0] = 0x78, 0x50, 0x28, 0x00, 0 } H.DOOM_DELAYS = { [0] = 0, 0, 0, 0, 50 } diff --git a/src/core/game3/battle/anim_port/g3_pret.lua b/src/core/game3/battle/anim_port/g3_pret.lua index e4dd428d..b979b087 100644 --- a/src/core/game3/battle/anim_port/g3_pret.lua +++ b/src/core/game3/battle/anim_port/g3_pret.lua @@ -52,9 +52,7 @@ function P.Sin2(angle) if floor(angle / 180) % 2 == 1 then return -v end return v end --- O5: pret defines Cos2 at pokefirered/src/trig.c:539-541 but never calls it either --- (no `Cos2(` callers anywhere in pret src) and the engine port had zero callers; --- cosine call sites use Sin2(deg + 90), the same path pret's Cos2 wraps. +-- pokefirered/src/trig.c:539-541 function P.ArcTan2(x, y) return Trig.arcTan2(x, y) diff --git a/src/core/game3/battle/anim_sprites.lua b/src/core/game3/battle/anim_sprites.lua index ae97b0a5..dbff3b91 100644 --- a/src/core/game3/battle/anim_sprites.lua +++ b/src/core/game3/battle/anim_sprites.lua @@ -230,7 +230,6 @@ function AnimSprites.update() local ok, err = pcall(AnimSprites.animate, s) if not ok then print("[battle.anim] sprite anim: " .. tostring(err)) - -- review-v3 D4: release the slot so a throwing animation cannot pin it. pcall(AnimSprites.release, s) end end diff --git a/src/core/game3/battle/anim_tasks.lua b/src/core/game3/battle/anim_tasks.lua index 1f0ec1e1..3c04defa 100644 --- a/src/core/game3/battle/anim_tasks.lua +++ b/src/core/game3/battle/anim_tasks.lua @@ -21,8 +21,6 @@ local function Cos(index, amp) end local function clear_task(t) - -- D8: recycling wipes every non-data field (mirrors anim_sprites.clear_slot) - -- so a leftover non-underscore field cannot leak into the next task. for k in pairs(t) do if k ~= "data" then t[k] = nil end end @@ -1334,11 +1332,7 @@ end AnimTasks.REGISTRY.ShakeTargetBasedOnMovePowerOrDmg = AnimTasks.ShakeTargetBasedOnMovePowerOrDmg AnimTasks.REGISTRY.AnimTask_ShakeTargetBasedOnMovePowerOrDmg = AnimTasks.ShakeTargetBasedOnMovePowerOrDmg --- O3: the base AnimTask_ShakeTargetInPattern (+ SHAKE_PATTERN_0/1) was shadowed --- dead code: the g1..g5 port merge below (the require "_tasks" loop) --- always overrides this registry entry with anim_port/g2_fire.lua:569's --- version (pokefirered/src/battle_anim_fire.c:1254), which is what --- tests/game3_anim_port_g2_test.lua:284-292 asserts (sShakeDirsPattern0). +-- pokefirered/src/battle_anim_fire.c:1254 --- RGB555 unpacker helper (pokefirered RGB_*) local function unpackRgb555(col) @@ -4753,9 +4747,6 @@ function AnimTasks.spawn(name, priority, args, vm) end end if not t then - -- review-v3 D5: grow the pool on exhaustion instead of silently dropping - -- the effect (a dropped task also lets waitforvisualfinish pass at - -- visualCount()==0, so the wait never happens). AnimTasks.MAX = AnimTasks.MAX + 1 t = AnimTasks._pool[AnimTasks.MAX] if not t then @@ -4773,13 +4764,11 @@ function AnimTasks.spawn(name, priority, args, vm) for ai, av in ipairs(args) do local v = av if type(v) == "string" then - -- leave string battler tokens in data via parallel map t.data[ai - 1] = v else t.data[ai - 1] = tonumber(v) or 0 end end - -- Also store string battler ids in high slots if present for ai, av in ipairs(args) do if type(av) == "string" then t.data[ai - 1] = av @@ -4814,7 +4803,6 @@ function AnimTasks.draw(minZ, maxZ, vm) local ok, err = pcall(t.draw, t, vm) if not ok then print("[battle.anim] task draw " .. tostring(t.name) .. ": " .. tostring(err)) - -- review-v3 D4: free the pool slot instead of leaving it active. clear_task(t) end end diff --git a/src/core/game3/battle/anim_vm.lua b/src/core/game3/battle/anim_vm.lua index b35c3620..c5beab59 100644 --- a/src/core/game3/battle/anim_vm.lua +++ b/src/core/game3/battle/anim_vm.lua @@ -7,7 +7,6 @@ local AnimTasks = require("src.core.game3.battle.anim_tasks") local AnimPal = require("src.core.game3.battle.anim_pal") local AnimCoords = require("src.core.game3.battle.anim_coords") --- D1: one reused options table per draw path (AnimPal.resolve reads it synchronously). local _blendOpts = {} local band, rshift = bit.band, bit.rshift diff --git a/src/core/game3/battle/catch_seq.lua b/src/core/game3/battle/catch_seq.lua index 1fde1690..b3496e74 100644 --- a/src/core/game3/battle/catch_seq.lua +++ b/src/core/game3/battle/catch_seq.lua @@ -561,8 +561,6 @@ function CB.beginBreakOut(b) b.cb = CB.runBreakOut BallOpen.start(b.target or 1, b.x, b.y, b.itemId, true) play_se(SE.SE_BALL_OPEN) - -- review-v3 D6: a breakout without a staged mon (no target yet) must not - -- dereference b.mon; the break-out visuals just skip the mon sprite. if b.mon then b.mon.visible = true b.monAff = { paused = false } diff --git a/src/core/game3/battle/commands.lua b/src/core/game3/battle/commands.lua index 570f37dc..952cba05 100644 --- a/src/core/game3/battle/commands.lua +++ b/src/core/game3/battle/commands.lua @@ -242,9 +242,7 @@ function Commands.switchError(st, slot, forced, battlerId) return nil end --- X9: dead AI-hook seam removed — Commands.setAiHook had zero callers in src/tests, --- so Commands.aiHook could never be non-nil. pret's opponent choice lives in --- battle_controller_opponent.c:1339/1350 (OpponentHandleChooseAction/Move), no mod hook. +-- battle_controller_opponent.c:1339 local function first_usable_action(b, id) local mon = b and b.mon diff --git a/src/core/game3/battle/damage.lua b/src/core/game3/battle/damage.lua index d8bf03e8..f1e57e82 100644 --- a/src/core/game3/battle/damage.lua +++ b/src/core/game3/battle/damage.lua @@ -4,7 +4,6 @@ local Rules = require("src.core.game3.battle.rules") local Types = require("src.core.game3.battle.types") local Moves = require("src.core.game3.battle.moves") local EffectIds = require("src.core.game3.battle.effect_ids") --- review-v3 S5: log the first swallowed rng-pcall once. local rngWarned = false local ModRuntime = require("src.mods.Runtime") @@ -106,7 +105,6 @@ local function roll_from(rng, lo, hi) if type(rng) == "function" then local ok, v = pcall(rng, lo, hi) if ok and type(v) == "number" then return v end - -- review-v3 S5: log the swallowed rng-pcall once, then fall back. if not rngWarned then rngWarned = true print("[game3/damage] rng call failed: " .. tostring(v)) diff --git a/src/core/game3/battle/effects/healing.lua b/src/core/game3/battle/effects/healing.lua index 118a9f03..55d05b3b 100644 --- a/src/core/game3/battle/effects/healing.lua +++ b/src/core/game3/battle/effects/healing.lua @@ -135,9 +135,7 @@ function Healing.healBell(ctx) local State = require("src.core.game3.battle.state") local active = State.partyMon(user) local blocked = isBell and ad:abilityOf(user) == "SOUNDPROOF" - -- review-v3 P1: pret battle_script_commands.c:8015-8016 clears - -- STATUS2_NIGHTMARE alongside status1 for the bell user (aroma :8071, - -- :8078 the flank partner) — the user path was missing it. + -- battle_script_commands.c:8015-8016 if not blocked then ad:clearStatus(user) user.expNightmare = nil @@ -154,8 +152,7 @@ function Healing.healBell(ctx) if mon and mon ~= active and mon ~= partnerMon and mon.status then mon.status = nil mon.sleep = nil - -- review-v3 P1: nightmare rides on sleep (cleared with status, pret - -- battle_script_commands.c:8015/8031). + -- battle_script_commands.c:8015 mon.expNightmare = nil end end diff --git a/src/core/game3/battle/effects/secondary.lua b/src/core/game3/battle/effects/secondary.lua index 337f3028..c25e4657 100644 --- a/src/core/game3/battle/effects/secondary.lua +++ b/src/core/game3/battle/effects/secondary.lua @@ -363,10 +363,7 @@ function Secondary.set(M, eff, primary, certain, affectsUser) user.rage = true return true elseif eff == "STEAL_ITEM" then - -- pret src/battle_script_commands.c:2610-2622 MOVE_EFFECT_STEAL_ITEM: - -- Trainer Tower never allows a steal, and an opponent may steal only in - -- Link / Battle Tower / e-Reader / Secret Base battles -- never in a - -- regular wild or trainer battle. + -- src/battle_script_commands.c:2610-2622 local StType = ad._st if StType and StType.trainerTower then return false end if user.side ~= "player" and not (StType and (StType.link or StType.battleTower @@ -413,8 +410,7 @@ function Secondary.set(M, eff, primary, certain, affectsUser) end return true elseif eff == "RAPIDSPIN" then - -- pokefirered/src/battle_script_commands.c:8435 Cmd_rapidspinfree is ONE - -- if/else-if chain: wrap, else leech seed, else spikes — one free per use. + -- pokefirered/src/battle_script_commands.c:8435 local did = false if (user.expTrapTurns or 0) > 0 then local src = user.expTrapSource @@ -424,20 +420,20 @@ function Secondary.set(M, eff, primary, certain, affectsUser) user.expTrapSource = nil user.wrapped = nil did = true - elseif user.expSeeded or user.leechSeed then + end + if user.expSeeded or user.leechSeed then user.expSeeded = nil user.expSeedSource = nil user.leechSeed = nil ad:say(Strings("%s shed\nLEECH SEED!", name(ad, user))) did = true - else - local side = ad:ownSide(user) - local Hazards = require("src.core.game3.battle.effects.hazards") - if side and Hazards.layers(side) > 0 then - Hazards.clear(side) - ad:say(Strings("%s blew away\nSPIKES!", name(ad, user))) - did = true - end + end + local side = ad:ownSide(user) + local Hazards = require("src.core.game3.battle.effects.hazards") + if side and Hazards.layers(side) > 0 then + Hazards.clear(side) + ad:say(Strings("%s blew away\nSPIKES!", name(ad, user))) + did = true end user.trapped = nil return did @@ -472,11 +468,7 @@ function Secondary.set(M, eff, primary, certain, affectsUser) end if tItem == 0 then return false end effBattler.item = 0 - -- pokefirered/src/battle_script_commands.c:2730-2752: FRLG clears only the - -- battler's item and sets the battle-scoped knockedOffMons bit. The party - -- mon keeps the item (Bulbapedia FRLG: "prevent its use during the battle") - -- and the bit masks it back off on every later send-out, so there is no - -- party-mon write-through here. + -- pokefirered/src/battle_script_commands.c:2730-2752 effBattler.expKnockedOff = true local St = battle_state() if St then St.markKnockedOff(ad._st, effBattler) end diff --git a/src/core/game3/battle/effects/special.lua b/src/core/game3/battle/effects/special.lua index b410bc63..65301d70 100644 --- a/src/core/game3/battle/effects/special.lua +++ b/src/core/game3/battle/effects/special.lua @@ -308,9 +308,7 @@ function Special.trick(ctx) local ad, user, target = ctx.adapter, ctx.user, ctx.target if (target.substituteHP or 0) > 0 then return H.sayFail(ctx) end if not H.accuracy(ctx, "normal") then return end - -- pret src/battle_script_commands.c:8799-8810 Cmd_tryswapitems: Trainer - -- Tower never swaps, and an opponent may swap only in Link / Battle Tower / - -- e-Reader / Secret Base battles (regular battles keep player-only). + -- src/battle_script_commands.c:8799-8810 local StType = ad._st if StType and StType.trainerTower then return H.sayFail(ctx) end if user.side ~= "player" and not (StType and (StType.link or StType.battleTower diff --git a/src/core/game3/battle/engine.lua b/src/core/game3/battle/engine.lua index 1c13a888..c9228279 100644 --- a/src/core/game3/battle/engine.lua +++ b/src/core/game3/battle/engine.lua @@ -6,7 +6,6 @@ local State = require("src.core.game3.battle.state") local Effects = require("src.core.game3.battle.effects") local EffectIds = require("src.core.game3.battle.effect_ids") local Residuals = require("src.core.game3.battle.residuals") --- review-v3 S5: per-site first-failure flags so swallowed pcalls log once. local rollWarned = false local badgeWarned = false local ResidualHandlers = require("src.core.game3.battle.residual_handlers") @@ -97,7 +96,6 @@ local function roll(adapter, lo, hi) if adapter and adapter.rng then local ok, v = pcall(adapter:rng(), lo, hi) if ok and type(v) == "number" then return v end - -- review-v3 S5: log the swallowed rng-pcall once, then fall back. if not rollWarned then rollWarned = true print("[game3/engine] adapter rng failed: " .. tostring(v)) @@ -176,7 +174,6 @@ function Engine.hasBadge(st, n) if Space and Space.store and Flags and Flags.hasBadge then local ok, v = pcall(Flags.hasBadge, Space.store, n) if not ok then - -- review-v3 S5: log the swallowed badge-pcall once, then default false. if not badgeWarned then badgeWarned = true print("[game3/battle] hasBadge failed: " .. tostring(v)) @@ -1366,24 +1363,11 @@ function Engine.moveEndLite(M) end -- pokefirered/src/battle_util.c:1208 --- pokefirered/src/battle_util.c:1208-1211: the end-of-action pass runs the --- held-item check for ALL battlers (ItemBattleEffects(ITEMEFFECT_NORMAL, 0, --- TRUE) with battler 0), not only for the player's side (review-v3 C8). -local function heldItemsForAll(st, ad, moveTurn) - local list = (ad and ad.activeBattlers and ad:activeBattlers()) or nil - if not list or not list[1] then list = { st.player, st.enemy } end - local did = false - for _, b in ipairs(list) do - if b and HeldItems.normal(ad, b, moveTurn) then did = true end - end - return did -end - function Engine.afterAction(st, ad) if not st or st.over then return end for _ = 1, 4 do local did = Abilities.runIntimidate(ad) or Abilities.runTrace(ad) - or heldItemsForAll(st, ad, true) or Abilities.forecast(ad) + or HeldItems.normal(ad, st.player, true) or Abilities.forecast(ad) if not did then break end end end @@ -1863,9 +1847,6 @@ function Engine.mostSuitableMon(st, adapter, side) for i = 1, 6 do local mon = party[i] if valid(i, mon) then - -- STAB comes from the candidate mon's own types (battle_ai_switch_items - -- scores the mon we would send in, not the one switching out). - local c1, c2 = types_of(mon) for j = 1, 4 do local mv = mon.moves and mon.moves[j] local n = move_num(mv) @@ -1875,7 +1856,7 @@ function Engine.mostSuitableMon(st, adapter, side) if (tonumber(m.power) or 0) ~= 1 then local mt = tonumber(m.type) or 0 dmg = 2 - if c1 == mt or c2 == mt then dmg = math.floor(dmg * 15 / 10) end + if active and (active.type1 == mt or active.type2 == mt) then dmg = math.floor(dmg * 15 / 10) end dmg = Types.typeCalc(mt, opp.type1, opp.type2, dmg) or 0 end end @@ -1892,7 +1873,6 @@ function Engine.performEnemyItem(st, adapter, act) return BattleItems.enemyUse(st, adapter, act) end --- pokefirered/src/battle_script_commands.c:4467 -- pokefirered/src/battle_script_commands.c:4467 local function switched_event(st, adapter, id, nb, old, opts) if not ModRuntime.wants("battle.battler_switched") then return end diff --git a/src/core/game3/battle/evo_seq.lua b/src/core/game3/battle/evo_seq.lua index 99f79029..32a9ca17 100644 --- a/src/core/game3/battle/evo_seq.lua +++ b/src/core/game3/battle/evo_seq.lua @@ -116,6 +116,12 @@ local function run_step(entry) local Audio = require("src.core.game3.audio") local victorySong = (Audio._currentSong and Audio._currentSong.id) or Audio.role("victoryWild") or 311 EvoSeq._waiting = true + local advanced = false + local function advanceOnce() + if advanced then return end + advanced = true + advance() + end local okStart, startErr = pcall(EvolutionScene.start, mon, toSpecies, { canStop = true, headless = EvoSeq._headless, @@ -123,19 +129,20 @@ local function run_step(entry) isBattle = true, savedSong = victorySong, onDone = function(result) - advance() + advanceOnce() end, }) - -- review-v3 D7: a scene that never opens (it throws, or returns without - -- pushing its layer) must not park the post-win flow forever; fall back - -- to applying directly, like the no-scene branch below. + if not okStart then + EvolutionScene.open = false + EvolutionScene._onDone = nil + end if not okStart or not (EvolutionScene.isOpen and EvolutionScene.isOpen()) then print("[game3/evo] evolution scene failed to open: " .. tostring(startErr or "no layer pushed")) if mon and mon.species ~= toSpecies then Evolution.apply(mon, toSpecies, EvoSeq._session) end - advance() + advanceOnce() end else -- Fallback diff --git a/src/core/game3/battle/init.lua b/src/core/game3/battle/init.lua index 9f4a3ede..580d4bb7 100644 --- a/src/core/game3/battle/init.lua +++ b/src/core/game3/battle/init.lua @@ -368,9 +368,7 @@ local function finish(result) local okF, Fade = pcall(require, "src.ui.game3.fade") if okF and Fade and Fade.clear then Fade.clear() end end - -- Victory BGM starts in begin_trainer_win (Audio.playSong role/fallback); - -- pret plays it in battle_main.c:3746-3759. Map BGM is restored by - -- battle_bridge on exit — do not clobber victory here. + -- battle_main.c:3746-3759 local cb = Battle._onDone Battle._onDone = nil if cb then cb(st and st.result or result or "win", st) end @@ -499,8 +497,7 @@ function Battle.start(opts) -- pokefirered/src/trainer_tower.c:735, src/battle_tower.c:933 st.trainerTower = opts.trainerTower or false st.eReader = opts.eReader or false - -- pret src/battle_tower.c:895-933 StartSpecialBattle case 0 = Battle Tower, - -- case 1 = Secret Base; the steal/swap gates branch on these flags (P5). + -- src/battle_tower.c:895-933 st.battleTower = opts.battleTower or false st.secretBase = opts.secretBase or false local trainerInfo = nil @@ -725,7 +722,6 @@ local function begin_start_effects() local ok, startErr = pcall(Engine.battleStartEffects, st, ad) ad._say = prev if not ok then - -- review-v3 S4: log the swallowed start-effects error before bailing. print("[game3/battle] start effects failed: " .. tostring(startErr)) return false end diff --git a/src/core/game3/battle/moves.lua b/src/core/game3/battle/moves.lua index 744b8974..f909e760 100644 --- a/src/core/game3/battle/moves.lua +++ b/src/core/game3/battle/moves.lua @@ -23,8 +23,7 @@ local function M(id, power, typeId, category, accuracy, pp, extra) secondaryChance = extra.secondaryChance, priority = extra.priority or 0, flags = extra.flags or 0, - -- pret src/data/battle_moves.h sets .target on every row; from_rom carries - -- it, curated rows must too or headless AI reads `target or 0` as SELECT. + -- src/data/battle_moves.h target = extra.target, hits = extra.hits, afterHit = extra.afterHit, @@ -68,24 +67,23 @@ Moves.BY_ID = { WATERFALL = M("WATERFALL", 80, T.WATER, "physical", 100, 15, { effect = EffectIds.FLINCH_HIT, secondaryChance = 20 }), GROWL = M("GROWL", 0, T.NORMAL, "status", 100, 40, { effectId = "EXP_GROWL", effect = EffectIds.ATTACK_DOWN, - -- pret src/data/battle_moves.h [MOVE_GROWL] .target = MOVE_TARGET_BOTH - -- (include/battle.h:63 MOVE_TARGET_BOTH = 1 << 3) + -- src/data/battle_moves.h, include/battle.h:63 target = 8 }), TAIL_WHIP = M("TAIL_WHIP", 0, T.NORMAL, "status", 100, 30, - { effectId = "EXP_TAIL_WHIP", effect = EffectIds.DEFENSE_DOWN }), -- review-v3 U10: pret EFFECT_DEFENSE_DOWN + { effectId = "EXP_TAIL_WHIP", effect = EffectIds.DEFENSE_DOWN }), LEER = M("LEER", 0, T.NORMAL, "status", 100, 30, - { effectId = "EXP_LEER", effect = EffectIds.DEFENSE_DOWN }), -- review-v3 U10: pret EFFECT_DEFENSE_DOWN + { effectId = "EXP_LEER", effect = EffectIds.DEFENSE_DOWN }), HARDEN = M("HARDEN", 0, T.NORMAL, "status", 100, 30, - { effectId = "EXP_HARDEN", effect = EffectIds.DEFENSE_UP }), -- review-v3 U10: pret EFFECT_DEFENSE_UP + { effectId = "EXP_HARDEN", effect = EffectIds.DEFENSE_UP }), CALM_MIND = M("CALM_MIND", 0, T.PSYCHIC, "status", 0, 20, { effectId = "EXP_CALM_MIND", effect = EffectIds.CALM_MIND }), BULK_UP = M("BULK_UP", 0, T.FIGHTING, "status", 0, 20, { effectId = "EXP_BULK_UP", effect = EffectIds.BULK_UP }), DRAGON_DANCE = M("DRAGON_DANCE", 0, T.DRAGON, "status", 0, 20, { effectId = "EXP_DRAGON_DANCE", effect = EffectIds.DRAGON_DANCE }), SWORDS_DANCE = M("SWORDS_DANCE", 0, T.NORMAL, "status", 0, 30, - { effectId = "EXP_SWORDS_DANCE", effect = EffectIds.ATTACK_UP_2 }), -- review-v3 U10: pret EFFECT_ATTACK_UP_2 + { effectId = "EXP_SWORDS_DANCE", effect = EffectIds.ATTACK_UP_2 }), AGILITY = M("AGILITY", 0, T.PSYCHIC, "status", 0, 30, - { effectId = "EXP_AGILITY", effect = EffectIds.SPEED_UP_2 }), -- review-v3 U10: pret EFFECT_SPEED_UP_2 + { effectId = "EXP_AGILITY", effect = EffectIds.SPEED_UP_2 }), AMNESIA = M("AMNESIA", 0, T.PSYCHIC, "status", 0, 20, - { effectId = "EXP_AMNESIA", effect = EffectIds.SPECIAL_DEFENSE_UP_2 }), -- review-v3 U10: pret EFFECT_SPECIAL_DEFENSE_UP_2 + { effectId = "EXP_AMNESIA", effect = EffectIds.SPECIAL_DEFENSE_UP_2 }), SUNNY_DAY = M("SUNNY_DAY", 0, T.FIRE, "status", 0, 5, { effectId = "EXP_WEATHER_SUNNY", effect = EffectIds.SUNNY_DAY }), RAIN_DANCE = M("RAIN_DANCE", 0, T.WATER, "status", 0, 5, { effectId = "EXP_WEATHER_RAINY", effect = EffectIds.RAIN_DANCE }), SANDSTORM = M("SANDSTORM", 0, T.ROCK, "status", 0, 10, { effectId = "EXP_WEATHER_SANDSTORM", effect = EffectIds.SANDSTORM }), @@ -220,7 +218,6 @@ function Moves._runReloadHooks() for i, h in ipairs(Moves._reloadHooks) do snapshot[i] = h end for _, h in ipairs(snapshot) do local ok, err = pcall(h.fn, Moves) - -- review-v3 S6: mirror Pokemon._runReloadHooks and log the discarded error. if not ok then print("[game3/moves] onReload callback failed: " .. tostring(err)) end end end @@ -326,8 +323,6 @@ function Moves.get(moveId) end if rom then return rom end if curated then return curated end - -- review-v3 U5: an unknown id must be marked, not silently replaced by a - -- 40 BP Normal fake that callers would treat as real data. return { unknown = true, id = num } end diff --git a/src/core/game3/battle/residuals.lua b/src/core/game3/battle/residuals.lua index 0d6922e7..04708f64 100644 --- a/src/core/game3/battle/residuals.lua +++ b/src/core/game3/battle/residuals.lua @@ -53,9 +53,7 @@ local function sortedBattlers(adapter) local list = adapter:activeBattlers() or {} local a, b = list[1], list[2] if a and b then - -- Reuse this turn's cached order so the speed-tie roll is stable across - -- every residual read in the turn (review-v3 C9; collectEvents fills - -- _endTurnOrder once, pokefirered/src/battle_util.c:484). + -- pokefirered/src/battle_util.c:484 local ids = st and st._endTurnOrder if ids and ids[1] and ids[2] then if ids[1] == b.id and ids[2] == a.id then @@ -172,14 +170,12 @@ function Residuals.collectEvents(adapter) local st = adapter._st if st then st._endTurnOrder = nil - -- pokefirered/src/battle_util.c:484: ONE speed order per turn, speed-tie - -- roll included; caching it for every mode (was doubles-only) stops the - -- singles path re-rolling the tie on each sortedBattlers call (C9). + -- pokefirered/src/battle_util.c:484 local order = {} for _, b in ipairs(sortedBattlers(adapter)) do order[#order + 1] = b.id end st._endTurnOrder = order if st.double then - -- pokefirered/src/battle_util.c:484 (turn order also published for doubles) + -- pokefirered/src/battle_util.c:484 st.turnOrder = order end end diff --git a/src/core/game3/battle/rules.lua b/src/core/game3/battle/rules.lua index 48dfaceb..b51ba616 100644 --- a/src/core/game3/battle/rules.lua +++ b/src/core/game3/battle/rules.lua @@ -43,11 +43,6 @@ Rules.POST_PHASES_ORDER = { "perish_song", } --- X2: the phase-classification trio (isFieldPhase / isPostPhase / phaseOrder) --- and its derived FIELD_PHASES / POST_PHASES / PHASE_ORDER lookup tables had --- ZERO callers anywhere in src/ or tests/ and were deleted. The *_ORDER lists --- stay (residuals.lua iterates them at :187/:194/:201) and FAINT_HALT_PHASES --- stays (live via residuals.lua:143 shouldHaltBattlerOnFaint). Rules.FAINT_HALT_PHASES = { ingrain = true, leech_seed = true, diff --git a/src/core/game3/battle/state.lua b/src/core/game3/battle/state.lua index 1cc2e143..7c4781cc 100644 --- a/src/core/game3/battle/state.lua +++ b/src/core/game3/battle/state.lua @@ -19,10 +19,7 @@ local function species_id(mon) end local function types_for(species) - if not Pokemon._types and not Pokemon._installTried then - -- review-v3 S11: log the swallowed install failure once and stop - -- hammering an install that already failed. - Pokemon._installTried = true + if not Pokemon._types then local okI, errI = pcall(Pokemon.install, nil) if not okI and not Pokemon._installWarned then Pokemon._installWarned = true @@ -68,9 +65,7 @@ function State.makeBattler(mon, side, opts) -- pokefirered/src/battle_main.c:2228 isFirstTurn = 2, } - -- pret pokefirered/src/battle_script_commands.c:4489: on send-out the - -- battler's item is re-read from the party mon, so a mon whose item was - -- knocked off earlier in the battle must be masked back to ITEM_NONE. + -- pokefirered/src/battle_script_commands.c:4489 if opts.st and State.isKnockedOff(opts.st, b) then b.item = 0 b.expKnockedOff = true diff --git a/src/core/game3/battle/switch_seq.lua b/src/core/game3/battle/switch_seq.lua index 6ea9eda3..312dcbe2 100644 --- a/src/core/game3/battle/switch_seq.lua +++ b/src/core/game3/battle/switch_seq.lua @@ -172,7 +172,6 @@ local function capture_events(fn) local ok, fnErr = pcall(fn, ad) ad._say = prev if not ok then - -- review-v3 S4: log the swallowed effect error instead of dropping silently. print("[game3/battle] switch effect failed: " .. tostring(fnErr)) return {} end @@ -711,8 +710,6 @@ local function run_step(step) local sides = d.sides or { d.side or "player" } if d.id ~= nil then sides = { d.id } end if #sides > 1 then - -- D9: sort a copy — d.sides is authored shared step data and table.sort - -- must not reorder it (the step table is reused across frames/runs). local sorted = {} for i = 1, #sides do sorted[i] = sides[i] end table.sort(sorted, function(a, bSide) diff --git a/src/core/game3/battle/ui.lua b/src/core/game3/battle/ui.lua index dc4e9e58..6458b2d7 100644 --- a/src/core/game3/battle/ui.lua +++ b/src/core/game3/battle/ui.lua @@ -31,7 +31,6 @@ local SE = require("src.core.game3.se_ids") local bit = require("bit") local Ui = {} --- review-v3 S9: one-shot warn flag for the swallowed BattleChrome install. local chromeInstallWarned = false -- The stat window may only be on screen while the battle is in a phase that can @@ -207,7 +206,6 @@ function Ui.reset(opts) Ui._oakTexts = nil if Message and Message.isHeld and Message.isHeld() then Message.close() end if not Ui._headless then - -- review-v3 S9 (battle/ui.lua:208): log the swallowed chrome install once. local okC, errC = pcall(BattleChrome.install, nil) if not okC and not chromeInstallWarned then chromeInstallWarned = true diff --git a/src/core/game3/battle_transition.lua b/src/core/game3/battle_transition.lua index 10daf62a..1ea53d11 100644 --- a/src/core/game3/battle_transition.lua +++ b/src/core/game3/battle_transition.lua @@ -1548,7 +1548,6 @@ function BattleTransition.finish() BattleTransition._active = false BattleTransition._phase = "done" BattleTransition._fx = nil - -- review-v3 D10: the mosaic canvas/quad survive finish() and abort(). BattleTransition._mosaicCanvas = nil BattleTransition._mosaicKey = nil local cb = BattleTransition._doneCb @@ -1561,7 +1560,6 @@ function BattleTransition.abort() BattleTransition._phase = "idle" BattleTransition._fx = nil BattleTransition._doneCb = nil - -- review-v3 D10: release the mosaic canvas on abort too. BattleTransition._mosaicCanvas = nil BattleTransition._mosaicKey = nil end diff --git a/src/core/game3/cache_paths.lua b/src/core/game3/cache_paths.lua index 12104299..166e5065 100644 --- a/src/core/game3/cache_paths.lua +++ b/src/core/game3/cache_paths.lua @@ -1,21 +1,8 @@ --- Shared GBA extract cache roots (T6.3a skeleton). --- --- Why: src/import/gba/extract_island1.lua owns Extract.CACHE_ROOT today, and --- src/core/game3/pokemon.lua requires that extractor just to read the root --- (I8: runtime → extractor → runtime). This module is the one place the root --- lives, so the runtime can read it without pulling the extractor in, and --- Dataset.mountExtractRoots can set one root for both. --- --- Unwired: nothing requires this file yet. The T6.2/T6.3 handoff patch makes --- extract_island1.lua read/write CachePaths and swaps pokemon.lua's require. --- The defaults below are exactly extract_island1.lua:19-21's current values. - local CachePaths = {} CachePaths.CACHE_ROOT = "data/generated/gba" CachePaths.NATIVE_ROOT = "data/generated/gba/native" ---- Set both roots from one cache root (mountExtractRoots / POKEPORT_GBA_CACHE). function CachePaths.setRoot(root) if type(root) ~= "string" or root == "" then return false end CachePaths.CACHE_ROOT = root @@ -23,7 +10,6 @@ function CachePaths.setRoot(root) return true end ---- Test/tool hook: back to the packaged defaults. function CachePaths.reset() CachePaths.CACHE_ROOT = "data/generated/gba" CachePaths.NATIVE_ROOT = "data/generated/gba/native" diff --git a/src/core/game3/capabilities.lua b/src/core/game3/capabilities.lua index 1943a9ce..68aece1c 100644 --- a/src/core/game3/capabilities.lua +++ b/src/core/game3/capabilities.lua @@ -1,28 +1,8 @@ --- Capability registry for the Game 3 feature sets (T3.1/T3.2). --- --- Why: a gate written against a raw flag (`if not caps.famechecker`) fails --- silently when the flag name is wrong, and a profile row can carry a typo the --- engine never notices. This module owns the legal flag names, the composed --- per-game sets, and the feature -> capability map, so gating call sites ask by --- FEATURE and the audit test proves every profile row is well formed. --- --- The FireRed-only set is grounded in pret: each row below cites the --- pokefirered source file (local clone, HEAD c75f35230) and the feature's --- absence in pokeemerald/pokeruby (verified 2026-09-22). RSE-only rows cite --- pokeemerald. --- --- Resolution reads src/core/game3/profile.lua, which already fails closed to --- FireRed until profiles/ruby.lua etc. exist. New file, unwired: the feature --- gates are handoff patches in docs/game3/rse-seams.md section 4. - local Profile = require("src.core.game3.profile") local Capabilities = {} ---- Every legal capability flag. A profile row that sets anything else is a --- bug the audit test catches. Capabilities.NAMES = { - -- shared GBA primitives (both FireRed and RSE implement them) easyChat = true, braille = true, mysteryGift = true, @@ -33,8 +13,7 @@ Capabilities.NAMES = { moveRelearner = true, eggs = true, berries = true, - sizeRecord = true, -- pokeemerald/src/pokemon_size_record.c exists; record DATA differs - -- FireRed-only (absent from pokeemerald/pokeruby) + sizeRecord = true, -- pokeemerald/src/pokemon_size_record.c helpSystem = true, -- pokefirered/src/help_system.c tmCase = true, -- pokefirered/src/tm_case.c fameChecker = true, -- pokefirered/src/fame_checker.c @@ -43,8 +22,7 @@ Capabilities.NAMES = { trainerTower = true, -- pokefirered/src/trainer_tower.c seagallop = true, -- pokefirered/src/seagallop.c trainerFanClub = true, -- pokefirered/src/trainer_fan_club.c - sevii = true, -- the Sevii region itself - -- RSE-only (pokeemerald sources; declared now so rows can set them early) + sevii = true, contests = true, secretBase = true, battleTower = true, @@ -53,8 +31,6 @@ Capabilities.NAMES = { pokeNav = true, } --- Composed sets. Profile rows inline their flags (data-only by design), so --- these are the authored reference the tests compare rows against. Capabilities.CORE = { easyChat = true, braille = true, mysteryGift = true, unionRoom = true, daycare = true, pokecenter = true, marts = true, moveRelearner = true, @@ -62,32 +38,24 @@ Capabilities.CORE = { } Capabilities.FRLG = { - -- core easyChat = true, braille = true, mysteryGift = true, unionRoom = true, daycare = true, pokecenter = true, marts = true, moveRelearner = true, eggs = true, berries = true, sizeRecord = true, - -- FireRed-only helpSystem = true, tmCase = true, fameChecker = true, teachyTV = true, vsSeeker = true, trainerTower = true, seagallop = true, trainerFanClub = true, berryPouch = true, sevii = true, } Capabilities.RSE = { - -- core easyChat = true, braille = true, mysteryGift = true, unionRoom = true, daycare = true, pokecenter = true, marts = true, moveRelearner = true, eggs = true, berries = true, sizeRecord = true, - -- RSE-only - battleTower = true, -- pokeemerald/src/battle_tower.c (FRLG's player tower is trainerTower) + battleTower = true, -- pokeemerald/src/battle_tower.c contests = true, secretBase = true, matchCall = true, pokeNav = true, } ---- Feature -> capability + owning modules. `source` is the authoritative pret --- file for the feature (repo-relative, e.g. "pokefirered/src/fame_checker.c"); --- `counterpart` records the other-generation check. Gating patches ask for the --- feature id, never the raw flag. +-- pokefirered/src/fame_checker.c Capabilities.FEATURES = { - -- FireRed-only (source present in pokefirered, 404 in pokeemerald/pokeruby) fame_checker = { cap = "fameChecker", label = "Fame Checker", @@ -165,7 +133,6 @@ Capabilities.FEATURES = { ui = "src.ui.game3.berry_pouch", extractor = "berry_pouch_extract", }, - -- RSE-only (source present in pokeemerald, 404 in pokefirered) contests = { cap = "contests", label = "Pokemon Contests", @@ -204,13 +171,10 @@ local function warnOnce(key, msg) log(msg) end ---- The capability table for a session's game. function Capabilities.of(session) return Profile.capabilitiesFor(session) end ---- Raw flag lookup, strict about the name: an unknown flag is a typo in a gate --- (it would silently disable a feature), so it warns once and reads false. function Capabilities.has(session, cap) if not Capabilities.NAMES[cap] then warnOnce("cap:" .. tostring(cap), "unknown capability '" .. tostring(cap) .. "'") @@ -219,15 +183,12 @@ function Capabilities.has(session, cap) return Capabilities.of(session)[cap] == true end ---- Pure helper for tests/tools: is a feature enabled in this capabilities table? function Capabilities.enabled(caps, featureId) local feature = Capabilities.FEATURES[featureId] if not feature or type(caps) ~= "table" then return false end return caps[feature.cap] == true end ---- Session gate a call site asks by feature id. Unknown feature ids warn once --- and read false -- the same typo protection as has(). function Capabilities.gate(session, featureId) if not Capabilities.FEATURES[featureId] then warnOnce("feat:" .. tostring(featureId), @@ -237,10 +198,8 @@ function Capabilities.gate(session, featureId) return Capabilities.enabled(Capabilities.of(session), featureId) end ---- Reverse index: natives module base name -> feature id. local nativesIndex ---- The feature a natives_* module belongs to, or nil when it is shared. function Capabilities.nativeFeature(moduleName) nativesIndex = nativesIndex or (function() local index = {} @@ -253,15 +212,12 @@ function Capabilities.nativeFeature(moduleName) return nativesIndex[moduleName] end ---- Gate for the natives registry: a module mapped to a feature follows that --- feature's capability; an unmapped module is shared and always allowed. function Capabilities.nativeAllowed(session, moduleName) local featureId = Capabilities.nativeFeature(moduleName) if not featureId then return true end return Capabilities.gate(session, featureId) end ---- Validate a profile row's capability table. Returns ok, problems (sorted). function Capabilities.audit(caps) local problems = {} if type(caps) ~= "table" then @@ -280,7 +236,6 @@ function Capabilities.audit(caps) return #problems == 0, problems end ---- Test/tool hook: forget the warn-once keys. function Capabilities.reset() warned = {} end diff --git a/src/core/game3/collision.lua b/src/core/game3/collision.lua index 39965ce7..7afab963 100644 --- a/src/core/game3/collision.lua +++ b/src/core/game3/collision.lua @@ -795,8 +795,6 @@ function Collision.canEnter(game, tx, ty, opts) surfing = P and P.surfing == true end - -- Prefer owned grid; fall back to host map if unbound (or empty — a - -- zero-dimension layout must not strand movement on an empty grid, B2). if Collision._grid and Collision._grid[1] ~= nil then if not Collision.inBounds(tx, ty) then return false, "bounds" end if overrideBlocks(tx, ty) then return false, "tile" end diff --git a/src/core/game3/dataset.lua b/src/core/game3/dataset.lua index cb371f48..be3fbd0d 100644 --- a/src/core/game3/dataset.lua +++ b/src/core/game3/dataset.lua @@ -99,7 +99,6 @@ local function load_lua_rel(rel) if not chunk then return nil end local ok, val = pcall(chunk) if ok then return val end - -- review-v3 S3: log the swallowed chunk error before falling through to nil. if not dsLoadWarned then dsLoadWarned = true print("[game3/dataset] load failed for " .. tostring(rel) .. ": " .. tostring(val)) diff --git a/src/core/game3/display.lua b/src/core/game3/display.lua index fdf534e2..489b6d65 100644 --- a/src/core/game3/display.lua +++ b/src/core/game3/display.lua @@ -252,9 +252,6 @@ local function drawFieldPlane(game, vw, vh, Renderer) end end --- I3: the UI pass runs behind an injected renderer — display no longer --- requires core/game3.gfx at load (breaks the display <-> gfx load cycle; gfx's --- drawUi moved to src/ui.game3.ui_pass per I1, which does not require display). local uiRenderer function Display.setUiRenderer(fn) uiRenderer = fn diff --git a/src/core/game3/doors.lua b/src/core/game3/doors.lua index 12ef64e8..8621eeb2 100644 --- a/src/core/game3/doors.lua +++ b/src/core/game3/doors.lua @@ -503,8 +503,6 @@ local function loadSheet(tileName) Doors._sheets[tileName] = false return nil end - -- review-v3 A5: validate the manifest row's numeric fields before any - -- arithmetic or frame loop; cache false so a malformed row is refused once. if type(info.width) ~= "number" or info.width < 1 or type(info.height) ~= "number" or info.height < 1 or type(info.frames) ~= "number" or info.frames < 1 then @@ -669,8 +667,6 @@ function Doors.isBusy() return anim ~= nil and (anim.mode == "open" or anim.mode == "close" or anim.mode == "delay_close") end --- review-v3 A4: door sheets cache Images/Quads with no teardown path; drop --- them on field/reset teardown so the GPU memory can be released. function Doors.release() Doors._sheets = {} Doors._layoutCache = {} diff --git a/src/core/game3/encounters.lua b/src/core/game3/encounters.lua index f5672e9d..8274180c 100644 --- a/src/core/game3/encounters.lua +++ b/src/core/game3/encounters.lua @@ -44,8 +44,7 @@ local COOLDOWN_SCALE = 256 -- pret keeps minSteps/encRate scaled so the modifier -- pret AddToWildEncounterRateBuff banks into a u16 field, so it wraps there. local RATE_BUFF_MOD = 65536 --- review-v3 U4: pret VAR_REPEL_STEP_COUNT is 0x4020 (include/constants/vars.h:47); --- the flags table also lists 0x4020, so the old 0x4021 wrote a different var. +-- include/constants/vars.h:47 local VAR_REPEL_STEP_COUNT = 0x4020 local function log(msg) diff --git a/src/core/game3/evolution.lua b/src/core/game3/evolution.lua index cbdaed8b..f0e316f9 100644 --- a/src/core/game3/evolution.lua +++ b/src/core/game3/evolution.lua @@ -303,8 +303,6 @@ function Evolution.apply(mon, newSpecies, session, bag, via) local shedId = (preRows[1] and row_method(preRows[1]) == Evolution.EVO_LEVEL_NINJASK and preRows[2] and row_target(preRows[2])) or 0 if shedId > 0 and session then - -- review-v3 T5: the Shedinja must land in `session.party` — never in a - -- save-side alias and never in a throw-away table when neither exists. session.party = session.party or (session.save and session.save.party) or {} local party = session.party if #party < 6 then diff --git a/src/core/game3/field.lua b/src/core/game3/field.lua index 9d3c0203..7f446cd7 100644 --- a/src/core/game3/field.lua +++ b/src/core/game3/field.lua @@ -43,8 +43,6 @@ function Field.start(mod, game, session) Field.locked = false Field.weather = 0 Field._waterfall = nil - -- A stale minigame/landing lock from a previous run must not survive into - -- the new one (review-v3 A2: clear the flags on start as well as stop). Field._fishing = nil Field._flyLanding = nil if Player then Player.fishing = false end @@ -77,16 +75,14 @@ function Field.stop() Field._session = nil Field.locked = false Field._waterfall = nil - -- Teardown must not leak per-run locks (review-v3 A2/A3/A4): an in-flight - -- fishing minigame, the fly-landing lock, plus the warp/door caches. Field._fishing = nil Field._flyLanding = nil local PlayerMod = package.loaded["src.core.game3.player"] if PlayerMod then PlayerMod.fishing = false end local Warp = package.loaded["src.core.game3.warp"] - if Warp and Warp.clear then Warp.clear() end -- review-v3 A3 + if Warp and Warp.clear then Warp.clear() end local Doors = package.loaded["src.core.game3.doors"] - if Doors and Doors.release then Doors.release() end -- review-v3 A4 + if Doors and Doors.release then Doors.release() end Field._tempFlagMap = nil end @@ -142,8 +138,7 @@ function Field.update(_dt) -- pokefirered/src/field_control_avatar.c:98 local walkInput = input if Field.forcedMovementPending() then walkInput = nil end - -- pokefirered/src/overworld.c:1402 DoCB1_Overworld: sign walk-away runs - -- before field input is processed (natives_events.Events.pollWalkaway). + -- pokefirered/src/overworld.c:1402 local NativesEvents = package.loaded["src.core.game3.scripting.natives_events"] if NativesEvents and NativesEvents.pollWalkaway then NativesEvents.pollWalkaway(Space and Space.vm, input) @@ -1006,7 +1001,7 @@ function Field.startFishing(rod) if Field._fishing then return false end Field.locked = true Player.fishing = true - -- tRoundsPlayed tracks pret's rounds counter (field_player_avatar.c:1667). + -- field_player_avatar.c:1667 Field._fishing = { rod = tonumber(rod) or 0, step = "wait", timer = 0, dots = 0, required = 0, rounds = 0 } return true end @@ -1058,8 +1053,7 @@ function Field.updateFishing() if f.step == "wait" then if f.timer >= FISHING_WAIT_FRAMES then - -- pokefirered/src/field_player_avatar.c:1740-1746 Fishing4: randVal+1, - -- but randVal+4 on the first round (tRoundsPlayed == 0), capped at 10. + -- pokefirered/src/field_player_avatar.c:1740-1746 local rand = Rng.Random() % 10 local need = rand + 1 if (f.rounds or 0) == 0 then need = rand + FISHING_FIRST_ROUND_DOTS end @@ -1073,8 +1067,7 @@ function Field.updateFishing() if f.timer >= FISHING_DOT_FRAMES then f.timer = 0 if f.dots >= f.required then - -- pokefirered/src/field_player_avatar.c:1761-1765: the round resolves - -- and tRoundsPlayed++ so the next round rolls randVal+1. + -- pokefirered/src/field_player_avatar.c:1761-1765 f.rounds = (f.rounds or 0) + 1 f.step = "bite" else diff --git a/src/core/game3/field_view.lua b/src/core/game3/field_view.lua index 401d73f3..a393fa0f 100644 --- a/src/core/game3/field_view.lua +++ b/src/core/game3/field_view.lua @@ -241,7 +241,6 @@ local function playerSpriteName(game) return "SPRITE_CHRIS" end --- Resolve gen2 Palettes once (daytimeFor used to pcall(require) per actor per frame). local PalettesMod, PalettesMissing local function palettes() if PalettesMod then return PalettesMod end @@ -416,28 +415,18 @@ local function actorPriority(a) end end --- Seam 7 consumer half (e10-opcode-spec §5.8): honour an object's freeze record. --- pret event_object_movement.c:8379-8387 (UpdateObjectEventElevationAndPriority) --- and :8424-8429 (ObjectEventUpdateSubpriority) both `return` while --- objEvent->fixedPriority is set, so the elevation-driven writer --- (SetObjectSubpriorityByElevation, :8414-8422) never runs for that object; the --- script bias is applied in scrcmd.c:1130 (`priority + 83`). --- The record lives on the live EventObject (Objects.setSubpriority stores --- fixedPriority/subpriority there), NOT on a.obj — a.obj is the map def — so the --- live-EventObject constructors carry it as `eventObject`. +-- event_object_movement.c:8379-8387, scrcmd.c:1130 local function applyDrawOrder(actors) local underActors = {} local overActors = {} for _, a in ipairs(actors) do local obj = a.eventObject if obj and obj.fixedPriority then - -- Freeze: capture the dynamic class once (first observation), then stop - -- the per-frame elevation recompute for this object. if obj.fixedClass == nil then obj.fixedClass = a.priority or actorPriority(a) end a.priority = obj.fixedClass - a.subpriority = obj.subpriority -- sort key, replaces sortY for this actor + a.subpriority = obj.subpriority else - if obj then obj.fixedClass = nil end -- reset side: no stale capture survives + if obj then obj.fixedClass = nil end a.priority = actorPriority(a) a.subpriority = nil end @@ -447,8 +436,6 @@ local function applyDrawOrder(actors) underActors[#underActors + 1] = a end end - -- Frozen actors order by the script's subpriority (byte + 83); unfrozen ones - -- keep the pixel-Y key, so reset resumes the dynamic path exactly as before. local function sortActors(a, b) local ay = a.subpriority or a.sortY or a.y local by = b.subpriority or b.sortY or b.y @@ -459,8 +446,6 @@ local function applyDrawOrder(actors) table.sort(overActors, sortActors) return underActors, overActors end --- Test seam: field_view draws nothing under the headless love stub, so the --- contract test drives the split/sort directly. FieldView.applyDrawOrder = applyDrawOrder local function drawSingleActor(game, mapDef, a, camX, camY) @@ -602,7 +587,6 @@ local function collectGame3Actors(game, mapDef, camX, camY, px, py, facing, walk end --- Collect visible tile draws grouped by palette slot for batched GbcPalette.with. --- K3: reused scratch for per-tile draw records (consumed synchronously by drawTilesColored). local tile_draw_pool, tile_draw_count, tile_draw_slots = {}, 0, {} local function collectTileDraws(mapDef, camX, camY, canvasW, canvasH) @@ -655,7 +639,6 @@ local function collectTileDraws(mapDef, camX, camY, canvasW, canvasH) return bySlot end --- K9: one reusable closure for palette-scoped tile runs (was one closure per slot per draw). local palAtlas, palList local function draw_pal_list() for _, d in ipairs(palList) do diff --git a/src/core/game3/gfx.lua b/src/core/game3/gfx.lua index d1676c05..9721b475 100644 --- a/src/core/game3/gfx.lua +++ b/src/core/game3/gfx.lua @@ -57,8 +57,6 @@ end function Gfx.clearUiBand() end --- I1: the UI router (drawUi dispatch) lives in src/ui/game3/ui_pass.lua; --- this shim keeps the Game3.Gfx surface and the stitchseam/ops drivers working. function Gfx.drawUi() return require("src.ui.game3.ui_pass").drawUi() end diff --git a/src/core/game3/heal_locations.lua b/src/core/game3/heal_locations.lua index f61b5ad2..46d04948 100644 --- a/src/core/game3/heal_locations.lua +++ b/src/core/game3/heal_locations.lua @@ -108,10 +108,6 @@ function HealLocations.load(cache, root) if not (cache and cache.read) then return 0 end local rel = root .. "/" .. HealLocations.BAKED_REL local src = cache:read(rel) - -- review-v3 B5: commit the root only together with a real bake — the old - -- code installed an empty table first, so one transient read failure pinned - -- an empty result forever (HealLocations.get only retries while _baked is - -- nil, and a version mount clears through invalidate()). if type(src) ~= "string" or src == "" then return 0 end local chunk = load(src, "@" .. rel, "t", {}) if not chunk then return 0 end diff --git a/src/core/game3/item_use.lua b/src/core/game3/item_use.lua index e7994752..b876d83d 100644 --- a/src/core/game3/item_use.lua +++ b/src/core/game3/item_use.lua @@ -747,8 +747,6 @@ local function useField(session, bag, id, partySlot) if use == "vs_seeker" or id == ItemsData.ITEM_VS_SEEKER or id == "VS_SEEKER" or ItemsData.toNumericId(id) == ItemsData.ITEM_VS_SEEKER then - -- rse-seams T3.1b: capability check first, before the module require. - -- FireRed reads the gate as true, so this branch is unchanged today. if not Capabilities.gate(session, "vs_seeker") then return false, "vs_seeker", nil end @@ -783,7 +781,6 @@ local function useField(session, bag, id, partySlot) -- pokefirered/src/item_use.c:518 FieldUseFunc_TeachyTv if id == ITEM_TEACHY_TV or id == "TEACHY_TV" or ItemsData.toNumericId(id) == ITEM_TEACHY_TV then - -- rse-seams T3.1b: capability gate before the screen require. if not Capabilities.gate(session, "teachy_tv") then return false, "teachy_tv", nil end @@ -795,7 +792,6 @@ local function useField(session, bag, id, partySlot) -- pokefirered/src/item_use.c:680 FieldUseFunc_FameChecker if id == ITEM_FAME_CHECKER or id == "FAME_CHECKER" or ItemsData.toNumericId(id) == ITEM_FAME_CHECKER then - -- rse-seams T3.1b: capability gate before the screen require. if not Capabilities.gate(session, "fame_checker") then return false, "fame_checker", nil end diff --git a/src/core/game3/items_data.lua b/src/core/game3/items_data.lua index 79df78ac..53692d86 100644 --- a/src/core/game3/items_data.lua +++ b/src/core/game3/items_data.lua @@ -234,7 +234,6 @@ local function load_pack() end return ItemsData._byId elseif not ok then - -- review-v3 S3: log the swallowed chunk error before the fallback. if not ItemsData._loadWarned then ItemsData._loadWarned = true print("[game3/items] items pack load failed: " .. tostring(pack)) @@ -395,8 +394,7 @@ function ItemsData.isEvolutionStone(id) local s = tostring(id or ""):upper() return s:find("STONE", 1, true) ~= nil end - -- review-v3 U3: the stone block is 93 (SUN) .. 98 (LEAF), pret - -- include/constants/items.h:97-102; the old 95..100 was off at both ends. + -- include/constants/items.h:97-102 return num >= 93 and num <= 98 end @@ -421,7 +419,6 @@ function ItemsData.isBerry(id) end --- Get 1-based Berry index (1..43) from item ID. --- review-v3 U7: an unknown item is NOT berry #1. function ItemsData.berryNumber(id) local num = tonumber(id) if not num then @@ -504,8 +501,7 @@ function ItemsData.medicineKind(id) return info and info.fieldUse or "none" end --- review-v3 U2/U3: FRLG's evolution stones are SUN..LEAF (93..98, pret --- include/constants/items.h:97-102) and 340/341 are not FRLG items at all. +-- include/constants/items.h:97-102 local LEVEL_IDS = { [68] = true } local EVO_IDS = { [93] = true, [94] = true, [95] = true, [96] = true, [97] = true, [98] = true } local VITAMIN_IDS = { [63] = true, [64] = true, [65] = true, [66] = true, [67] = true, [70] = true } diff --git a/src/core/game3/layout_native.lua b/src/core/game3/layout_native.lua index 9493d50d..6a18c2b9 100644 --- a/src/core/game3/layout_native.lua +++ b/src/core/game3/layout_native.lua @@ -58,8 +58,6 @@ end --- Flat 1-based COLL_* array for Collision.bindMap. function LayoutNative:collArray() local n = self.width * self.height - -- review-v3 B2: a zero-dimension layout must not yield a truthy-but-empty - -- array (it would disable the host-map fallback); nil means "no grid". if n <= 0 then return nil end local out = {} for i = 1, n do diff --git a/src/core/game3/link/trade.lua b/src/core/game3/link/trade.lua index c465788c..6c278298 100644 --- a/src/core/game3/link/trade.lua +++ b/src/core/game3/link/trade.lua @@ -455,10 +455,6 @@ local function saveAfterTrade() if game and game.saveGame then okSave = select(1, pcall(function() game:saveGame() end)) end - -- review-v3 S2: capture both results, log, and propagate instead of - -- reporting a clean done — LT._saveFailed is the observable flag (callers - -- are bare statements). saveDone still fires: the partner block already - -- landed, so stalling the scene here would softlock the trade. if not (okPersist and okSave) then LT._saveFailed = true print("[link] post-trade save failed (persist=" .. tostring(okPersist) diff --git a/src/core/game3/map.lua b/src/core/game3/map.lua index 5cbf30a6..f6a2f66b 100644 --- a/src/core/game3/map.lua +++ b/src/core/game3/map.lua @@ -284,10 +284,7 @@ function Map.load(mod, game, mapId, opts) Map._announced = mapId Map.current = mapId Map._loadedLayouts = { [mapId] = true } - -- B9: pret re-reads map data on every load path — LoadMapFromWarp - -- (overworld.c:792 LoadCurrentMapData) and LoadMapFromCameraTransition - -- (overworld.c:759 LoadCurrentMapData) both end in a fresh InitMap — so the - -- refreshWorld world cache must not survive a reload of the same root. + -- overworld.c:792, overworld.c:759 Map._worldRoot = nil local def = host_map_def(game, mapId) diff --git a/src/core/game3/map_ids.lua b/src/core/game3/map_ids.lua index e744196d..d473e5fc 100644 --- a/src/core/game3/map_ids.lua +++ b/src/core/game3/map_ids.lua @@ -4,8 +4,6 @@ local Profile = require("src.core.game3.profile") local MapIds = {} --- gameId is a version id (save.version) or nil for the active game. The --- prefix list comes from that game's profile (T1.2 handoff, rse-seams §4). function MapIds.isGame3Map(mapId, gameId) if type(mapId) ~= "string" then return false end for _, prefix in ipairs(Profile.of(gameId).map.prefixes) do diff --git a/src/core/game3/mystery_gift.lua b/src/core/game3/mystery_gift.lua index b32673dc..c42760f5 100644 --- a/src/core/game3/mystery_gift.lua +++ b/src/core/game3/mystery_gift.lua @@ -858,10 +858,7 @@ local function createEventMon(session, gift) if not Pokemon._names then pcall(Pokemon.install, nil) end local species = num(gift.species) local level = num(gift.level, 5) - -- The Wonder Card itself carries no species payload (pret - -- src/mystery_gift.c:191-210 validateCard checks the five card fields only), - -- so an engine gift that DOES claim one must validate it here or a - -- species-0 / out-of-range payload lands straight in the party. + -- src/mystery_gift.c:191-210 local known = type(Pokemon._names) == "table" and Pokemon._names[species] ~= nil if not known then return nil, "invalid gift species" end if level < 1 or level > 100 then return nil, "invalid gift level" end @@ -905,9 +902,7 @@ local function createEventMon(session, gift) end MysteryGift.createEventMon = createEventMon --- rse-seams e10 5.1: the Mystery Event script status slot mirroring pret --- src/mystery_event_script.c:92-95 (SetMysteryEventScriptStatus) / :75-80 --- (MEventScript_Run's status out-param). +-- src/mystery_event_script.c:92-95 local meScriptStatus = 0 function MysteryGift.setStatus(v) meScriptStatus = tonumber(v) or 0 diff --git a/src/core/game3/oam.lua b/src/core/game3/oam.lua index c24b0579..52c56850 100644 --- a/src/core/game3/oam.lua +++ b/src/core/game3/oam.lua @@ -494,7 +494,6 @@ end function Oam.buildOamBuffer(pretOrder) ensure_pool() - -- K6: cache oamTopLeft per sprite so comparators never recompute it. local n = 0 for i = 0, Oam.MAX_SPRITES - 1 do local s = Oam._sprites[i] @@ -504,8 +503,6 @@ function Oam.buildOamBuffer(pretOrder) n = n + 1 end end - -- K1: reuse the previous buffer when the visible set and sort keys are unchanged - -- (total comparator => adjacent-pair check proves the whole array is still sorted). local cmp = pretOrder and sort_sprites_pret or sort_sprites local cached = Oam._buffer if cached and #cached == n then diff --git a/src/core/game3/objects.lua b/src/core/game3/objects.lua index ff18a417..1f5667bc 100644 --- a/src/core/game3/objects.lua +++ b/src/core/game3/objects.lua @@ -6,6 +6,7 @@ local Movement = require("src.core.game3.scripting.movement") local Opcodes = require("src.core.game3.scripting.opcodes") local GfxIds = require("src.core.game3.scripting.gfx_ids") local ModRuntime = require("src.mods.Runtime") +local VirtualObjects = require("src.core.game3.virtual_objects") local Objects = {} @@ -193,11 +194,8 @@ function Objects.clear() Objects._mapId = nil Objects._defs = nil Objects._bounds = nil - -- rse-seams e10 5.3: virtual objects die with the map teardown (they are - -- rendered-only sprites with no collision or interaction, pret - -- src/event_object_movement.c:9225 DestroyVirtualObjects). - local okVO, VO = pcall(require, "src.core.game3.virtual_objects") - if okVO and VO and VO.clear then VO.clear() end + -- src/event_object_movement.c:9225 + VirtualObjects.clear() end -- pokefirered/src/overworld.c:405 @@ -206,8 +204,7 @@ function Objects.reset() Objects._perm = {} Objects._templateMt = {} Objects._logged = false - local okVO, VO = pcall(require, "src.core.game3.virtual_objects") - if okVO and VO and VO.reset then VO.reset() end + VirtualObjects.reset() end function Objects.hasMap() @@ -466,10 +463,7 @@ function Objects.find(localId) return Objects._byId[localId] end --- rse-seams e10 spec 5.8 / pret src/event_object_movement.c:2089-2116. --- pret's TryGetObjectEventIdByLocalIdAndMap only touches an object that lives --- on the script's (mapGroup, mapNum); an unresolvable map means the lookup --- fails and the command does nothing. +-- src/event_object_movement.c:2089-2116 local function on_named_map(mapGroup, mapNum) if mapGroup == nil or mapNum == nil then return true end local ok, MapCatalog = pcall(require, "src.import.gba.map_catalog") @@ -479,32 +473,25 @@ local function on_named_map(mapGroup, mapNum) return engineId == Objects._mapId end ---- pret src/event_object_movement.c:2089-2101 SetObjectSubpriority: the ---- object's draw-order freezes (fixedPriority) instead of following elevation ---- each frame. The +83 bias is applied by the script op (scrcmd.c:1130), so ---- `subpriority` arrives already biased. field_view's half (Refactor lane) ---- consumes fixedPriority/subpriority and captures the current class; this ---- half only stores the freeze record. +-- src/event_object_movement.c:2089-2101, scrcmd.c:1130 function Objects.setSubpriority(localId, mapGroup, mapNum, subpriority) local eo = Objects._byId[tonumber(localId) or -1] if not eo then return false end if not on_named_map(mapGroup, mapNum) then return false end eo.fixedPriority = true eo.subpriority = tonumber(subpriority) or 0 - eo.fixedClass = nil -- stale class from an earlier freeze must not leak (spec 5.8 record) + eo.fixedClass = nil return true end ---- pret src/event_object_movement.c:2104-2116 ResetObjectSubpriority: clears ---- the freeze (fixedPriority = FALSE) so the dynamic elevation-driven path ---- resumes — pret does NOT restore a previous value, and neither do we. +-- src/event_object_movement.c:2104-2116 function Objects.resetSubpriority(localId, mapGroup, mapNum) local eo = Objects._byId[tonumber(localId) or -1] if not eo then return false end if not on_named_map(mapGroup, mapNum) then return false end eo.fixedPriority = nil eo.subpriority = nil - eo.fixedClass = nil -- spec 5.8: the reset drops all three fields + eo.fixedClass = nil return true end @@ -519,7 +506,7 @@ function Objects.listActive(_mod, _game, _mapId) return ids end -local VIRT_DIR_FACE = { [1] = "down", [2] = "up", [3] = "left", [4] = "right" } -- pret DIR_SOUTH..DIR_EAST +local VIRT_DIR_FACE = { [1] = "down", [2] = "up", [3] = "left", [4] = "right" } function Objects.forDraw() local list = {} @@ -530,15 +517,9 @@ function Objects.forDraw() list[#list + 1] = eo end end - -- rse-seams e10 5.3: the virtual-object registry feeds the SAME draw pass as - -- event objects — rendered-only sprites with no collision and no - -- interaction (pret src/event_object_movement.c:1719 CreateVirtualObject; - -- they are sprites, not object events). field_view reads - -- {cellX, cellY, elevation, facing, sprite, graphicsId} off each record; the - -- records are never in _byId, so Objects.at/blocks cannot see them. - local okVO, VO = pcall(require, "src.core.game3.virtual_objects") - if okVO and VO and VO.list then - for _, vo in ipairs(VO.list()) do + -- src/event_object_movement.c:1719 + if VirtualObjects.count() > 0 then + for _, vo in ipairs(VirtualObjects.list()) do local gid = tonumber(vo.graphicsId) or 0 local vrec = { virtualId = vo.id, diff --git a/src/core/game3/options.lua b/src/core/game3/options.lua index c34dacf6..6c8c5dc5 100644 --- a/src/core/game3/options.lua +++ b/src/core/game3/options.lua @@ -4,8 +4,6 @@ local Options = {} local Profile = require("src.core.game3.profile") --- Kept for compatibility; the live key comes from the profile (T1.1 handoff, --- docs/game3/rse-seams.md section 4). No call site may read this directly. Options.BLOCK = Profile.FALLBACK_ID -- pret: textSpeed 0=SLOW 1=MID 2=FAST @@ -42,8 +40,6 @@ local function migrate_root(engine, o) engine.l_equals_a = nil end --- The engine options file is keyed per game (option block id). `blockId` --- defaults to the active profile; callers holding a save pass that save's game. function Options.block(engine, blockId) if type(engine) ~= "table" then return fill_defaults({}) end blockId = blockId or Profile.active().optionsBlock @@ -56,7 +52,6 @@ function Options.block(engine, blockId) return fill_defaults(o) end ---- The option block id for a session: its stored game, else the active one. function Options.blockId(session) if type(session) == "table" and type(session.version) == "string" then return Profile.of(session.version).optionsBlock diff --git a/src/core/game3/party.lua b/src/core/game3/party.lua index df55500c..dda79c44 100644 --- a/src/core/game3/party.lua +++ b/src/core/game3/party.lua @@ -185,9 +185,7 @@ function Party.giveMon(session, species, level, nickname, opts) level = tonumber(level) or 5 if level < 1 then level = 1 end local Pokemon = require("src.core.game3.pokemon") - if not Pokemon._names and not Pokemon._installTried then - -- review-v3 S11: log the swallowed install failure once; no silent retries. - Pokemon._installTried = true + if not Pokemon._names then local okI, errI = pcall(Pokemon.install, nil) if not okI and not Pokemon._installWarned then Pokemon._installWarned = true @@ -257,7 +255,7 @@ function Party.giveMon(session, species, level, nickname, opts) otName = session.name or session.playerName or "RED", otId = session.trainerId or session.id or session.playerId or 12345, -- pokefirered/src/pokemon.c:1796 CreateBoxMon OT_ID_PLAYER_ID - otSecretId = tonumber(session.secretId) or nil, -- review-v3 V4: drop the unwritten otSecretId alias + otSecretId = tonumber(session.secretId) or nil, -- pokefirered/src/pokemon.c:1822 otGender = Party.otGender(session), pokeball = 4, -- Poké Ball @@ -283,8 +281,6 @@ function Party.giveMon(session, species, level, nickname, opts) session.dex = session.dex or { seen = {}, owned = {}, caught = {} } session.dex.seen = session.dex.seen or {} session.dex.owned = session.dex.owned or {} - -- review-v3 F2: caught mirrors owned (dex.lua Dex.setCaught writes all - -- three; save-menu/trainer-card counts read dex.caught). session.dex.caught = session.dex.caught or {} session.dex.seen[species] = true session.dex.owned[species] = true @@ -295,8 +291,6 @@ end --- Give an egg for script giveegg. -- pokefirered/src/script_pokemon_util.c:75 function Party.giveEgg(session, species, opts) - -- review-v3 T4: giveMon initialises `session.party`, so only the session - -- needs to be present (the extra clause refused every nil-party session). if not session then return false, Party.MON_CANT_GIVE end species = tonumber(species) or 1 local ok, code, egg = Party.giveMon(session, species, 5, "EGG", opts) diff --git a/src/core/game3/pc_anim.lua b/src/core/game3/pc_anim.lua index 57616f58..3e8f9f6d 100644 --- a/src/core/game3/pc_anim.lua +++ b/src/core/game3/pc_anim.lua @@ -65,8 +65,6 @@ function PcAnim.update() local flickerOff = (t.state % 2) == 1 local offTile = PcAnim.METATILE_OFF[var] local onTile = PcAnim.METATILE_ON[var] - -- An out-of-range VAR_0x8004 must not blank the cell with metatile 0 - -- (review-v3 A6): drop the animation instead of writing garbage. if not offTile or not onTile then PcAnim.task = nil return diff --git a/src/core/game3/player.lua b/src/core/game3/player.lua index f8a11ba2..49a37c8f 100644 --- a/src/core/game3/player.lua +++ b/src/core/game3/player.lua @@ -604,8 +604,6 @@ function Player.startSurfing(game, onDone) local SE = require("src.core.game3.se_ids") if Audio.playSe and SE.SE_LEDGE then Audio.playSe(SE.SE_LEDGE) end end) - -- forceStep refuses while the avatar is already stepping; without this - -- rollback the pending surfHopping flag would strand (review-v3 A7). if not Player.forceStep(Player.facing, onDone) then Player.surfHopping = false return false diff --git a/src/core/game3/pokedex_data.lua b/src/core/game3/pokedex_data.lua index efe2bceb..6259290d 100644 --- a/src/core/game3/pokedex_data.lua +++ b/src/core/game3/pokedex_data.lua @@ -184,9 +184,6 @@ end function PokedexData.getEntry(speciesId) PokedexData.init() local sp = tonumber(speciesId) or 1 - -- review-v3 F1: the old guard called the nonexistent - -- Pokemon.nationalPokedexNumber (always nil → natId stayed == sp); the real - -- mapper is Pokemon.national (pokemon.lua:297). local natId = Pokemon.national and Pokemon.national(sp) or sp local raw = (PokedexData._entries and PokedexData._entries[natId]) diff --git a/src/core/game3/pokemon.lua b/src/core/game3/pokemon.lua index d60e4864..b3bccb5a 100644 --- a/src/core/game3/pokemon.lua +++ b/src/core/game3/pokemon.lua @@ -1,6 +1,4 @@ -- Runtime FRLG species names / menu icons / types (extracted pack). --- rse-seams T6.3b (I8): read the cache root from the zero-require CachePaths --- module instead of pulling the ROM extractor into the runtime. local CachePaths = require("src.core.game3.cache_paths") local PokemonExtract = require("src.import.gba.pokemon_extract") local Versions = require("src.import.gba.versions") @@ -85,7 +83,6 @@ local function load_lua(cache, rel) if not chunk then return nil end local ok, t = pcall(chunk) if ok then return t end - -- review-v3 S3: log the swallowed chunk error once before the nil fallback. if not pkLoadWarned then pkLoadWarned = true print("[game3/pokemon] load failed for " .. tostring(rel) .. ": " .. tostring(t)) @@ -223,7 +220,6 @@ function Pokemon.invalidate() Pokemon._types = nil Pokemon._national = nil Pokemon._manifest = nil - -- review-v3 S11: a failed install may retry after a remount/version change. Pokemon._installTried = nil Pokemon._installWarned = nil Pokemon._byName = nil diff --git a/src/core/game3/profile.lua b/src/core/game3/profile.lua index 9bc3ca67..a7d18852 100644 --- a/src/core/game3/profile.lua +++ b/src/core/game3/profile.lua @@ -1,14 +1,3 @@ --- Per-game Gen 3 profile: the single place the Ruby/Sapphire/Emerald port --- reads game-specific behaviour from. FireRed's row holds today's constants, --- so every accessor here fails closed to it. --- --- Design: docs/game3/rse-seams.md sections 3.1 and 5 (lead-approved). --- Requires only GameVersion (zero-require) so it loads during boot and under --- plain luajit; profile rows are plain data modules under profiles/. --- --- Nothing consumes this module yet: the handoff tickets in rse-seams.md --- wire the seams one file at a time (map ids, options, save schema, font). - local GameVersion = require("src.core.GameVersion") local Profile = {} @@ -22,15 +11,12 @@ local function log(msg) print("[game3/profile] " .. tostring(msg)) end ---- Load a profile row by version id. Returns nil when the id has no row. local function load(id) local ok, row = pcall(require, "src.core.game3.profiles." .. id) if ok and type(row) == "table" and row.id == id then return row end return nil end ---- The profile for a version id (GameVersion id or save.version). Unknown ---- ids fail closed to the active game and then to FireRed, logging once. function Profile.of(id) if type(id) ~= "string" or id == "" then return Profile.active() end local row = cache[id] @@ -50,8 +36,6 @@ function Profile.of(id) return Profile.active() end ---- The active game's profile. Non-Gen3 processes fail closed to FireRed so ---- shared code and headless tests can require this without booting a game. function Profile.active() local id = GameVersion.get() local info = GameVersion.info(id) @@ -71,7 +55,6 @@ function Profile.isGame3Version(id) return info ~= nil and (info.generation or 1) == 3 end ---- Capability flags for a session's game (hook for the FRLG-only gates). function Profile.capabilitiesFor(session) local id = type(session) == "table" and session.version or nil return Profile.of(id).capabilities or {} @@ -81,7 +64,6 @@ function Profile.has(session, capability) return Profile.capabilitiesFor(session)[capability] == true end ---- Test hook: drop the resolution cache so GameVersion swaps re-resolve. function Profile.reset() cache = {} warned = {} diff --git a/src/core/game3/profiles/firered.lua b/src/core/game3/profiles/firered.lua index bffc6b9f..1c541f47 100644 --- a/src/core/game3/profiles/firered.lua +++ b/src/core/game3/profiles/firered.lua @@ -1,21 +1,13 @@ --- FireRed (Game 3) profile row. Every value below is a constant the engine --- hardcodes today; the field's source is cited so a wiring ticket can move --- the call site without re-deriving it. --- --- Data only: no requires, no love, safe to load under luajit. --- Design: docs/game3/rse-seams.md section 3.1. - return { id = "firered", label = "FireRed", generation = 3, engine = "game3", - -- src/core/game3/map_ids.lua:5-29 map = { - prefixes = { "FR_", "SEVII_" }, -- MapIds.isGame3Map membership - enginePrefix = "FR_", -- map_catalog pret_to_engine synthesis - legacyPrefixes = { "SEVII_" }, -- Game3.lua:384 "refuse Sevii leftovers" + prefixes = { "FR_", "SEVII_" }, + enginePrefix = "FR_", + legacyPrefixes = { "SEVII_" }, newGameStart = { map = "FR_PLAYERS_HOUSE_2F", x = 6, @@ -27,29 +19,19 @@ return { }, }, - -- FRLG repair rules that live in save_schema_firered.lua today; the module - -- is created by the schema-split handoff (rse-seams T0.2). - -- Module created by the RSE-wave schema split (rse-seams section 3.1); - -- nothing requires it until then, so this names a target, not a dependency. saveRules = "src.core.game3.profiles.firered_rules", - -- src/core/game3/options.lua:5 optionsBlock = "firered", - -- src/ui/game3/frlg_font.lua:314-319 and :381-393 (CacheFs-relative paths; - -- readActive applies the game's cachePrefix). font = { module = "src.ui.game3.frlg_font", widths = "data/generated/gba/chrome/fonts/latin_widths.lua", smallWidths = "data/generated/gba/chrome/fonts/latin_small_widths.lua", }, - -- pret: pokefirered/include/constants/species.h:421-423 SPECIES_EGG 412, - -- NUM_SPECIES SPECIES_EGG; the same values in pokeemerald :418-420 and - -- pokeruby :418,448. Engine side: versions.lua:88, pokemon.lua:510. + -- pokefirered/include/constants/species.h:421-423 species = { num = 412, egg = 412 }, - -- src/core/game3/pokedex_data.lua:111-153 and :283-286 dexArea = { defaultKey = "kanto", mapGroups = "src.import.gba.map_groups_firered", @@ -57,10 +39,7 @@ return { stripPrefixes = { "FR_", "SEVII_" }, }, - -- pret: pokefirered/include/constants/flags.h:1324 SYS_FLAGS 0x800, - -- :1364-1371 FLAG_BADGE01_GET = SYS_FLAGS+0x20 … 08 = +0x27. - -- Engine side: trainer_card.lua:212-213. RSE differs (pokeruby :779,789-796 - -- base 0x807; pokeemerald :1348,1359-1366 base 0x867). + -- pokefirered/include/constants/flags.h:1324 badges = { count = 8, flagBase = 0x820, @@ -70,21 +49,18 @@ return { }, }, - -- src/core/game3/heal_locations.lua BY_ID table (20 entries) heal = { table = "firered" }, - -- src/core/game3/scripting/trainers.lua:11-13, :22-41, :327-364 trainers = { rivalIds = { squirtle = 326, bulbasaur = 327, charmander = 328 }, fallback = { class = 81, pic = 106, name = "TERRY" }, music = { encounter = { - -- pret TRAINER_ENCOUNTER_MUSIC_* codes -> songs 283/284/285 girlCodes = { 1, 2, 9 }, rocketCodes = { 3, 6, 7 }, - girl = 284, -- MUS_ENCOUNTER_GIRL - rocket = 283, -- MUS_ENCOUNTER_ROCKET - boy = 285, -- MUS_ENCOUNTER_BOY (default) + girl = 284, + rocket = 283, + boy = 285, }, battle = { championClass = 90, champion = 299, @@ -98,17 +74,9 @@ return { }, }, - -- src/ui/game3/region_map.lua:407-414 regionMap = { switchFlag = "FLAG_SYS_SEVII_MAP_123" }, - -- Flags for the FRLG-only features that are ungated today - -- (rse-seams sections 3.4 and 3.5). The split is pret-grounded: - -- pokefirered/src/{help_system,tm_case,fame_checker,teachy_tv,vs_seeker, - -- trainer_tower,seagallop,trainer_fan_club}.c all exist; every one of those - -- paths 404s in pokeemerald and pokeruby (verified 2026-09-22), except the - -- shared primitives below which have RSE counterparts. capabilities = { - -- shared GBA primitives (RSE implements these too) easyChat = true, braille = true, mysteryGift = true, @@ -119,8 +87,7 @@ return { moveRelearner = true, eggs = true, berries = true, - sizeRecord = true, -- pokeemerald/src/pokemon_size_record.c exists - -- FireRed-only (no RSE counterpart in pret) + sizeRecord = true, -- pokeemerald/src/pokemon_size_record.c helpSystem = true, tmCase = true, fameChecker = true, @@ -133,7 +100,6 @@ return { sevii = true, }, - -- src/core/game3/scripting/natives.lua:803-820 nativeModules = { "natives_corner", "natives_cutscene", @@ -154,8 +120,6 @@ return { "natives_wireless", }, - -- The aux extractors RomExtractorGen3:runAuxExtracts (:291-438) runs - -- unconditionally today; an RSE row lists its own set. extractors = { "region_map_extract", "map_sections_extract", diff --git a/src/core/game3/profiles/leafgreen.lua b/src/core/game3/profiles/leafgreen.lua new file mode 100644 index 00000000..27845a1f --- /dev/null +++ b/src/core/game3/profiles/leafgreen.lua @@ -0,0 +1,5 @@ +local row = {} +for k, v in pairs(require("src.core.game3.profiles.firered")) do row[k] = v end +row.id = "leafgreen" +row.label = "LeafGreen" +return row diff --git a/src/core/game3/rng.lua b/src/core/game3/rng.lua index a09160cd..64b85d3f 100644 --- a/src/core/game3/rng.lua +++ b/src/core/game3/rng.lua @@ -129,9 +129,6 @@ function Rng.getState() } end ---- Apply a full RNG state. review-v3 F8: partial states are rejected with no --- mutation, so a corrupt/partial save cannot half-restore the RNG (Game3 --- falls back to a fresh reseed). function Rng.setState(st) if type(st) ~= "table" then return false end local v1, v2, wild = tonumber(st.value), tonumber(st.value2), tonumber(st.wild) @@ -177,7 +174,6 @@ function Rng.restoreFromSession(session) if type(session) ~= "table" or type(session.rng) ~= "table" then return false end - -- review-v3 F8: propagate validation — a partial rng table fails here too. return Rng.setState(session.rng) == true end diff --git a/src/core/game3/save_schema_firered.lua b/src/core/game3/save_schema_firered.lua index cece7617..157c6445 100644 --- a/src/core/game3/save_schema_firered.lua +++ b/src/core/game3/save_schema_firered.lua @@ -119,11 +119,10 @@ function Schema.newGame(opts) generation = 3, party = {}, bag = Bag.new(), - dex = { seen = {}, owned = {}, caught = {}, national = false }, -- review-v3 V6: caught mirrors owned from birth + dex = { seen = {}, owned = {}, caught = {}, national = false }, money = tonumber(opts.money) or 3000, coins = 0, - -- review-v3 H8: pret include/global.h:354 SaveBlock2.berryCrush holds - -- berryPowderAmount (src/berry_powder.c:50); keyed here, inert at 0. + -- include/global.h:354, src/berry_powder.c:50 berryPowder = 0, name = opts.name or "RED", rivalName = opts.rivalName or "BLUE", @@ -143,7 +142,7 @@ function Schema.newGame(opts) easyChatProfile = { 2601, 4128, 526, 2611 }, options = nil, registeredItem = nil, - monBoxId = nil, -- review-v3 V2: written by storage, now persisted + monBoxId = nil, monBoxPos = nil, -- pokefirered/include/global.h:764 dynamicWarp = nil, @@ -161,7 +160,6 @@ function Schema.newGame(opts) session.trainerId = Rng.seedNewGame({ seed = opts.rngSeed }) -- pokefirered/src/new_game.c:56 InitPlayerTrainerId session.secretId = Rng.Random() - -- review-v3 V3: alias the id fields consumers read (never written before). session.id = session.trainerId session.playerId = session.trainerId Rng.captureToSession(session) @@ -198,16 +196,13 @@ function Schema.toSaveTable(session) schemaVersion = session.schemaVersion or Schema.VERSION, engine = "game3", version = session.version or Profile.active().id, - -- F3: engine/version/generation now round-trip symmetrically (newGame - -- seeds generation=3 at :118; toSave/fromSave previously dropped it). generation = session.generation or 3, name = session.name, rivalName = session.rivalName, gender = session.gender, money = session.money, coins = session.coins, - -- review-v3 H8: pret include/global.h:354 persists berryCrush.berryPowderAmount - -- in gSaveBlock2 (src/berry_powder.c:50); the port keeps it on the session. + -- include/global.h:354, src/berry_powder.c:50 berryPowder = session.berryPowder or 0, party = session.party, bag = session.bag, @@ -229,7 +224,7 @@ function Schema.toSaveTable(session) options = Options.engine(session) or session.options, storage = session.storage and require("src.core.game3.storage").serialize(session.storage) or nil, registeredItem = session.registeredItem, - monBoxId = session.monBoxId, -- review-v3 V2: the active PC cursor/box + monBoxId = session.monBoxId, monBoxPos = session.monBoxPos, -- pokefirered/include/global.h:764 dynamicWarp = session.dynamicWarp, @@ -238,7 +233,7 @@ function Schema.toSaveTable(session) flashLevel = tonumber(session.flashLevel), move_overlay = session.move_overlay or {}, trainerId = session.trainerId, - secretId = session.secretId, -- review-v3 V4: secretId now persists (was never written) + secretId = session.secretId, secretId = session.secretId, rng = session.rng, vsSeeker = session.vsSeeker, @@ -276,17 +271,16 @@ function Schema.fromSaveTable(save) end local session = { schemaVersion = save.schemaVersion or Schema.VERSION, - engine = save.engine or "game3", -- F3: symmetric with toSaveTable (raw save tag also read by SaveData engine routing) + engine = save.engine or "game3", version = save.version or Profile.active().id, - generation = tonumber(save.generation) or 3, -- F3: older saves default to 3 like newGame + generation = tonumber(save.generation) or 3, party = save.party or {}, bag = bag, dex = save.dex or {}, money = save.money or 0, coins = save.coins or 0, - -- review-v3 H8: additive — older saves load 0 (pret's zeroed SaveBlock2 default). berryPowder = tonumber(save.berryPowder) or 0, - name = save.name or "RED", -- F3 dead-legacy-read drop: no schema build ever wrote save.playerName (git log -S) and session.playerName consumers read session.name first + name = save.name or "RED", rivalName = save.rivalName or "BLUE", gender = save.gender or 0, map = save.map or MapIds.NEW_GAME_START.map, @@ -305,7 +299,7 @@ function Schema.fromSaveTable(save) options = nil, storage = require("src.core.game3.storage").restore(save.storage, save.pc, save.pcItems or save.pc_items), registeredItem = save.registeredItem, - monBoxId = save.monBoxId, -- review-v3 V2 + monBoxId = save.monBoxId, monBoxPos = save.monBoxPos, -- pokefirered/include/global.h:764 dynamicWarp = type(save.dynamicWarp) == "table" and save.dynamicWarp or nil, @@ -315,7 +309,7 @@ function Schema.fromSaveTable(save) move_overlay = save.move_overlay or {}, trainerId = save.trainerId, secretId = save.secretId, - id = save.trainerId, -- review-v3 V3: alias consumers read + id = save.trainerId, playerId = save.trainerId, rng = save.rng, vsSeeker = type(save.vsSeeker) == "table" and save.vsSeeker or { steps = 0, charging = 0, rematches = {} }, diff --git a/src/core/game3/scripting/adapters.lua b/src/core/game3/scripting/adapters.lua index a6158899..cb74d9b1 100644 --- a/src/core/game3/scripting/adapters.lua +++ b/src/core/game3/scripting/adapters.lua @@ -1323,12 +1323,6 @@ function Adapters.host(mod, game, world) -- multichoicedefault left, top, listId, default, ignoreBPress -- multichoicegrid left, top, listId, numColumns, ignoreBPress listId = tonumber(row.listId or row[3] or row[1]) or 0 - -- row[4] is ignoreBPress / default / numColumns — NEVER an option - -- count. Reading it here made the tint picker (listId 2, ignoreB=1) - -- resolve to a one-option menu and forced MON_ICON_TINT_NORMAL (0) - -- whenever the multichoice extract cache missed (BUG2). Only an - -- explicit row.count may override the default hint; resolve() then - -- prefers the embedded cart counts anyway. n = tonumber(row.count) or n end local opts, layout = Multi.resolve(listId, n) diff --git a/src/core/game3/scripting/flags.lua b/src/core/game3/scripting/flags.lua index 1c4d076f..0d5dba85 100644 --- a/src/core/game3/scripting/flags.lua +++ b/src/core/game3/scripting/flags.lua @@ -354,9 +354,7 @@ function Flags.getVar(store, ctx, id) if not (ctx and ctx.specialVars) then return 0 end return (ctx.specialVars[id]) or 0 end - -- pret src/event_data.c:235-241 VarGet: an id with no var pointer is not a - -- var and comes back unchanged (GetVarPointer == NULL -> return idx). The - -- var table starts at VARS_START (0x4000); below that it is a literal. + -- src/event_data.c:235-241 if id < 0x4000 then return id end if not (store and store.vars) then return 0 end return (store.vars[id]) or 0 diff --git a/src/core/game3/scripting/gfx_ids.lua b/src/core/game3/scripting/gfx_ids.lua index 3e1a6cf4..320b9e4e 100644 --- a/src/core/game3/scripting/gfx_ids.lua +++ b/src/core/game3/scripting/gfx_ids.lua @@ -26,10 +26,7 @@ GfxIds.TO_SPRITE = { [40] = "SPRITE_LASS", -- PICNICKER [41] = "SPRITE_COOLTRAINER_M", [42] = "SPRITE_COOLTRAINER_F", - -- pret include/constants/event_objects.h:54 OBJ_EVENT_GFX_WORKER_F = 48; - -- the engine has no worker sprite yet, so 48 is deliberately left unmapped - -- and spriteFor falls back (like the other unported graphics ids) rather - -- than claiming SCIENTIST (which is gfx 55 below, event_objects.h:61). + -- include/constants/event_objects.h:54, event_objects.h:61 [54] = "SPRITE_BLACK_BELT", [55] = "SPRITE_SCIENTIST", [56] = "SPRITE_POKEFAN_M", -- HIKER diff --git a/src/core/game3/scripting/multichoice.lua b/src/core/game3/scripting/multichoice.lua index f915eef6..1058af71 100644 --- a/src/core/game3/scripting/multichoice.lua +++ b/src/core/game3/scripting/multichoice.lua @@ -7,13 +7,6 @@ local Multichoice = {} Multichoice.LISTS = {} Multichoice.CACHE_REL = "data/generated/gba/scripts/multichoice.lua" --- Cart option counts (gMultichoiceLists, transcribed via --- src/import/gba/multichoice_extract.lua). When the extract cache is absent --- or stale, resolve() still has to serve the cart's list ARITY: a wrong count --- shifts every case/switch branch downstream — BUG2's Game Corner photo tint --- collapsed list 2 (NORMAL/BLACK/PINK/SEPIA) to a single option and always --- picked MON_ICON_TINT_NORMAL. Labels stay synthetic on a cache miss; only --- the arity is cart truth. Multichoice.COUNTS = { [0]=2, [1]=5, [2]=4, [3]=2, [4]=2, [5]=2, [6]=3, [7]=3, [8]=3, [9]=4, [10]=1, [11]=1, [12]=1, [13]=2, [14]=6, [15]=6, @@ -107,9 +100,6 @@ function Multichoice.resolve(listId, countHint) if entry and entry.labels and #entry.labels > 0 then return entry.labels, { left = entry.left, top = entry.top } end - -- Fallback synthetic labels (legacy). Cart arity beats the caller hint: - -- positional operands (e.g. multichoice row[4] = ignoreBPress) have leaked - -- in here as "counts" before (BUG2 photo tint). local n = Multichoice.COUNTS[id] or tonumber(countHint) or 3 local labels = {} for i = 1, math.max(1, n) do diff --git a/src/core/game3/scripting/natives.lua b/src/core/game3/scripting/natives.lua index 450276e0..202329a0 100644 --- a/src/core/game3/scripting/natives.lua +++ b/src/core/game3/scripting/natives.lua @@ -54,9 +54,7 @@ local function getSpecialVar(ctx, id) end local function setSpecialVar(ctx, id, value) - -- pret src/field_specials.c:2075-2078 VarSet writes the real save var - -- (0x4025 = VAR_MASSAGE_COOLDOWN_STEP_COUNTER, include/constants/vars.h:75); - -- the old nil store dropped that write (review-v3 E5). + -- src/field_specials.c:2075-2078, include/constants/vars.h:75 local Space = package.loaded["src.core.game3.scripting.space"] flagsMod().setVar(Space and Space.store or nil, ctx, id, value) if ctx and type(ctx.setVar) == "function" then ctx:setVar(id, value) end @@ -87,20 +85,21 @@ end -- GetMonData(MON_DATA_NICKNAME) is gText_EggNickname for an egg -- (pokefirered/src/pokemon.c:3020) +local function ensurePokemonNames(Pokemon) + if Pokemon._names then return end + local okI, errI = pcall(Pokemon.install, nil) + if not okI and not Pokemon._installWarned then + Pokemon._installWarned = true + print("[game3/pokemon] install failed: " .. tostring(errI)) + end +end + local function nicknameOf(mon) if not mon then return "" end local Pokemon = require("src.core.game3.pokemon") if Pokemon.isEgg(mon) then return Strings("EGG") end if mon.nickname and mon.nickname ~= "" then return tostring(mon.nickname) end - if not Pokemon._names and not Pokemon._installTried then - -- review-v3 S11: log the swallowed install failure once; no silent retries. - Pokemon._installTried = true - local okI, errI = pcall(Pokemon.install, nil) - if not okI and not Pokemon._installWarned then - Pokemon._installWarned = true - print("[game3/pokemon] install failed: " .. tostring(errI)) - end - end + ensurePokemonNames(Pokemon) return (Pokemon.name and Pokemon.name(mon.species or mon.speciesId)) or "" end @@ -636,15 +635,7 @@ Natives.ALLOW = { local mon = chosenMon(ctx) local species = mon and tonumber(mon.species or mon.speciesId) or 1 local Pokemon = require("src.core.game3.pokemon") - if not Pokemon._names and not Pokemon._installTried then - -- review-v3 S11 sibling: same log-once pattern as the cited site. - Pokemon._installTried = true - local okI, errI = pcall(Pokemon.install, nil) - if not okI and not Pokemon._installWarned then - Pokemon._installWarned = true - print("[game3/pokemon] install failed: " .. tostring(errI)) - end - end + ensurePokemonNames(Pokemon) -- pokefirered/src/field_specials.c:1656 local before = nicknameOf(mon) setStringVar(ctx, adapters, 3, before) @@ -672,15 +663,7 @@ Natives.ALLOW = { return yield_host(ctx, adapters, function(done) local species = tonumber(mon.species or mon.speciesId) or 1 local Pokemon = require("src.core.game3.pokemon") - if not Pokemon._names and not Pokemon._installTried then - -- review-v3 S11 sibling: same log-once pattern as the cited site. - Pokemon._installTried = true - local okI, errI = pcall(Pokemon.install, nil) - if not okI and not Pokemon._installWarned then - Pokemon._installWarned = true - print("[game3/pokemon] install failed: " .. tostring(errI)) - end - end + ensurePokemonNames(Pokemon) local before = nicknameOf(mon) setStringVar(ctx, adapters, 3, before) setStringVar(ctx, adapters, 2, before) @@ -742,9 +725,6 @@ Natives.ALLOW = { local Audio = require("src.core.game3.audio") local Flags = require("src.core.game3.scripting.flags") local Space = package.loaded["src.core.game3.scripting.space"] - -- Ctx never defined a getVar method (the old guard always failed and the - -- cry played as species 0 — review-v3 E4); read 0x8000 through Flags like - -- every other special does. local species = tonumber(Flags.getVar(Space and Space.store, ctx, 0x8000)) or 0 Audio.playCry(species) return false @@ -851,8 +831,6 @@ local KNOWN_MODULES = { Natives.KNOWN_MODULES = KNOWN_MODULES Natives.MODULE_DIR = MODULE_DIR --- rse-seams T3.2: KNOWN_MODULES stays the fallback; a profile's nativeModules --- list widens or narrows the merge set for other Gen 3 games. local function moduleNames() local names = Profile.active().nativeModules return type(names) == "table" and names or KNOWN_MODULES @@ -893,10 +871,6 @@ Natives.MODULE_NAMES = discoverModules() Natives.MODULES = {} for _, base in ipairs(Natives.MODULE_NAMES) do - -- rse-seams T3.2: skip a module whose feature capability is off - -- (Capabilities.nativeAllowed maps natives_fame/tower/fan_club/seagallop to - -- their features; every other module is shared). FireRed enables all of - -- them, so the same 16 modules merge as before. if Capabilities.nativeAllowed(nil, base) then local ok, mod = pcall(require, MODULE_PACKAGE .. base) if ok and type(mod) == "table" then @@ -924,7 +898,6 @@ local function log_once(kind, id, logger) end --- Returns whether the VM should yield (native wait). --- rse-seams e10 5.2: export the once-per-key logger for ops_a's gotonative. Natives.log_once = log_once function Natives.callnative(ctx, fnAddr, adapters) @@ -937,12 +910,8 @@ function Natives.callnative(ctx, fnAddr, adapters) return false end --- rse-seams e10 5.2: symbol seam for `gotonative`. pret --- src/scrcmd.c:92-97 SetupNativeScript jumps to a C function pointer, which --- the engine cannot execute; a symbol table (emitted by the extractor later, --- T6.x) maps the address to a `native:` registration instead. Empty registry --- today = every address resolves to nil and the op skips with one log. -Natives.NATIVE_SYMBOLS = {} -- addr -> symbol +-- src/scrcmd.c:92-97 +Natives.NATIVE_SYMBOLS = {} function Natives.resolveNative(addr) local id = tonumber(addr) or 0 local sym = Natives.NATIVE_SYMBOLS[id] @@ -950,7 +919,7 @@ function Natives.resolveNative(addr) local fn = Natives.ALLOW["native:" .. sym] if fn then return fn end end - return Natives.ALLOW["native:" .. id] -- direct registration (callnative's key) + return Natives.ALLOW["native:" .. id] end function Natives.special(ctx, specialId, adapters) diff --git a/src/core/game3/scripting/natives_cutscene.lua b/src/core/game3/scripting/natives_cutscene.lua index 7a6f300b..8c0364d8 100644 --- a/src/core/game3/scripting/natives_cutscene.lua +++ b/src/core/game3/scripting/natives_cutscene.lua @@ -9,7 +9,7 @@ local VAR_0x8006 = 0x8006 -- pokefirered/include/constants/vars.h:321 local SE_M_WING_ATTACK = 150 -- pokefirered/include/constants/songs.h:155 local SE_SS_ANNE_HORN = 249 -- pokefirered/include/constants/songs.h:255 -local SPECIES_KABUTOPS = 141 -- pokefirered/src/script_menu.c:1165 (museum_extract.lua:9) +local SPECIES_KABUTOPS = 141 -- pokefirered/src/script_menu.c:1165 local SPECIES_AERODACTYL = 142 -- pokefirered/src/script_menu.c:1171 local function flagsMod() @@ -72,16 +72,7 @@ Cutscene.HANDLERS = { end return false end, - -- review-v3 Q7: pret src/special_field_anim.c:223-265 - -- AnimateTeleporterHousing — 16-frame beats cycle the Sea Cottage - -- teleporter light/door metatiles through 13 states (yellow/half-glow ↔ - -- red/full-glow), then rest on the green light + closed door. Coord - -- offsets: VAR_0x8004==0 → (x+6, y-5) right unit, else (x-1, y-5) left - -- (metatile ids: pret include/constants/metatile_labels.h:177-186). - -- Field.setMetatile routes through metatileOverrides + applyOverride, so - -- the normal per-frame field draw picks the tiles up (same seam as the - -- door override at field.lua:1110). Headless suites cannot render the - -- glow cycle — in-game visual check pending (AGENTS.md caveat). + -- src/special_field_anim.c:223-265, include/constants/metatile_labels.h:177-186 [Std.SPECIAL.AnimateTeleporterHousing] = function(ctx) local P = package.loaded["src.core.game3.player"] or require("src.core.game3.player") @@ -97,11 +88,11 @@ Cutscene.HANDLERS = { Task.spawn(function() if timer == 0 then if state % 2 == 0 then - Field.setMetatile(x, y, 0x2B5, false) -- Light_Yellow - Field.setMetatile(x, y + 2, 0x2B7, false) -- Door_HalfGlowing + Field.setMetatile(x, y, 0x2B5, true) + Field.setMetatile(x, y + 2, 0x2B7, true) else - Field.setMetatile(x, y, 0x2B6, false) -- Light_Red - Field.setMetatile(x, y + 2, 0x2B8, false) -- Door_FullGlowing + Field.setMetatile(x, y, 0x2B6, true) + Field.setMetatile(x, y + 2, 0x2B8, true) end end timer = timer + 1 @@ -109,16 +100,13 @@ Cutscene.HANDLERS = { timer = 0 state = state + 1 if state ~= 13 then return false end - Field.setMetatile(x, y, 0x28A, false) -- Light_Green (resting) - Field.setMetatile(x, y + 2, 0x296, false) -- Door + Field.setMetatile(x, y, 0x28A, true) + Field.setMetatile(x, y + 2, 0x296, true) return true end) return false end, - -- review-v3 Q7: pret src/special_field_anim.c:285-330 - -- AnimateTeleporterCable — every 4 frames walk a cable-ball pair left from - -- (x+4, y-5), leaving the plain cable tiles behind, and stop after 4 - -- steps (state 4 draws the last plain tiles and destroys the task). + -- src/special_field_anim.c:285-330 [Std.SPECIAL.AnimateTeleporterCable] = function() local P = package.loaded["src.core.game3.player"] or require("src.core.game3.player") @@ -133,13 +121,13 @@ Cutscene.HANDLERS = { Task.spawn(function() if timer == 0 then if state ~= 0 then - Field.setMetatile(x, y, 0x285, false) -- Cable_Top - Field.setMetatile(x, y + 1, 0x2B4, false) -- Cable_Bottom + Field.setMetatile(x, y, 0x285, true) + Field.setMetatile(x, y + 1, 0x2B4, true) if state == 4 then return true end x = x - 1 end - Field.setMetatile(x, y, 0x2B9, false) -- CableBall_Top - Field.setMetatile(x, y + 1, 0x2BA, false) -- CableBall_Bottom + Field.setMetatile(x, y, 0x2B9, true) + Field.setMetatile(x, y + 1, 0x2BA, true) end timer = timer + 1 if timer == 4 then @@ -151,47 +139,28 @@ Cutscene.HANDLERS = { return false end, - -- pokefirered/src/credits.c:711 DoCredits — the Indigo Plateau roll - -- (data/maps/IndigoPlateau_Exterior/scripts.inc:80 `special / waitstate / - -- releaseall`). The port has no game3 credits sequence yet; bound so the - -- waitstate completes and the script releases instead of skipping the - -- dispatch as unknown. + -- pokefirered/src/credits.c:711, data/maps/IndigoPlateau_Exterior/scripts.inc:80 [Std.SPECIAL.DoCredits] = function() return false end, - -- pokefirered/src/field_specials.c:90 ShowDiploma — pushes CB2_ShowDiploma - -- (diploma.c:100); data/maps/CeladonCity_Condominiums_3F/scripts.inc:34 - -- parks on `waitstate`. src/ui/Diploma.lua is the Gen 1 diploma page - -- (engine/events/diploma.asm) with no FRLG wiring yet, so this completes - -- the waitstate: the congratulations message still shows, the certificate - -- screen is future work. + -- pokefirered/src/field_specials.c:90, diploma.c:100, data/maps/CeladonCity_Condominiums_3F/scripts.inc:34, engine/events/diploma.asm [Std.SPECIAL.ShowDiploma] = function() return false end, - -- pokefirered/src/ss_anne.c:82 DoSSAnneDepartureCutscene — horn + wake/smoke - -- boat task; data/maps/SSAnne_Exterior/scripts.inc:21 runs it after - -- `delay 50`, then removes the boat and warps. The port has no wake/sprite - -- sail task, so play the horn (SE_SS_ANNE_HORN) and let the script's own - -- object removal + warp carry the beat. + -- pokefirered/src/ss_anne.c:82, data/maps/SSAnne_Exterior/scripts.inc:21 [Std.SPECIAL.DoSSAnneDepartureCutscene] = function(ctx, adapters) playSe(adapters, SE_SS_ANNE_HORN) return false end, - -- pokefirered/src/field_specials.c:2133 DoPokemonLeagueLightingEffect — - -- a timed BG_PLTT_ID(7) tint task (data/scripts/pokemon_league.inc:63); - -- FLAG_TEMP_3 selects task-cancel instead of start. The port never starts - -- the tint, so the no-op covers both the start and the cancel arm. + -- pokefirered/src/field_specials.c:2133, data/scripts/pokemon_league.inc:63 [Std.SPECIAL.DoPokemonLeagueLightingEffect] = function() return false end, - -- pokefirered/src/field_specials.c:2535 LoopWingFlapSound — plays - -- SE_M_WING_ATTACK now, then repeats every VAR_0x8005 frames until - -- VAR_0x8004 loops (NavelRock_Summit/scripts.inc:39-41 + :54-55 set - -- 3 loops / 35-frame delay while the camera pans). + -- pokefirered/src/field_specials.c:2535, NavelRock_Summit/scripts.inc:39-41 [Std.SPECIAL.LoopWingFlapSound] = function(ctx, adapters) local loops = varGet(ctx, VAR_0x8004) local delay = varGet(ctx, VAR_0x8005) @@ -207,10 +176,7 @@ Cutscene.HANDLERS = { count = count + 1 playSe(adapters, SE_M_WING_ATTACK) end - -- review-v3 Q11: pret destroys the task at data[0] == VAR_0x8004 - 1 - -- (field_specials.c:2553), so total plays = loops (1 entry + loops-1 - -- ticks), not loops+1. The check runs every pump like pret's - -- post-increment check (field_specials.c:2546-2554). + -- field_specials.c:2553, field_specials.c:2546-2554 return count >= loops - 1 end) end @@ -218,14 +184,7 @@ Cutscene.HANDLERS = { return false end, - -- pokefirered/src/script_menu.c:1151 OpenMuseumFossilPic — draws the - -- 64x64 fossil exhibit over the msgbox (data/maps/PewterCity_Museum_1F/ - -- scripts.inc:170-187: setvar species/x/y, special, msgbox, special). The - -- art is already extracted (museum_extract.lua -> gba/museum/*.rgba); the - -- window widget that composites it is still to come, so this records the - -- open state for that seam. Species guard mirrors script_menu.c:1165-1176 - -- (only KABUTOPS/AERODACTYL draw; anything else returns FALSE) and, like - -- pret, never touches dex flags — the old 0x18B mis-bind did. + -- pokefirered/src/script_menu.c:1151, scripts.inc:170-187, script_menu.c:1165-1176 [Std.SPECIAL.OpenMuseumFossilPic] = function(ctx) local species = varGet(ctx, VAR_0x8004) if species ~= SPECIES_KABUTOPS and species ~= SPECIES_AERODACTYL then @@ -241,8 +200,7 @@ Cutscene.HANDLERS = { return false end, - -- pokefirered/src/script_menu.c:1184 CloseMuseumFossilPic — retires the - -- task that owns the picture window. + -- pokefirered/src/script_menu.c:1184 [Std.SPECIAL.CloseMuseumFossilPic] = function(ctx) if ctx then ctx.museumFossilPic = nil end return false diff --git a/src/core/game3/scripting/natives_daycare.lua b/src/core/game3/scripting/natives_daycare.lua index 315b4bda..853cc1ff 100644 --- a/src/core/game3/scripting/natives_daycare.lua +++ b/src/core/game3/scripting/natives_daycare.lua @@ -69,12 +69,7 @@ local nicknameOf = Model.nickname local slotMon = Model.mon local eggPending = Model.isEggPending --- The FRLG scripts guard a full party before a withdrawal or an egg handout: --- data/maps/FourIsland_PokemonDayCare/scripts.inc:86-88 (retrieve), --- data/maps/FourIsland/scripts.inc:95-104 (egg) and --- data/scripts/day_care.inc:79-81 (Route 5 retrieve). pret's daycare.c stays --- index-assign unguarded (src/daycare.c:525, :1081), so the special handler is --- this engine's script-layer seam; every caller gets the same guard. +-- data/maps/FourIsland_PokemonDayCare/scripts.inc:86-88, data/maps/FourIsland/scripts.inc:95-104, data/scripts/day_care.inc:79-81, daycare.c, src/daycare.c:525, :1081 local function partyIsFull(session) local party = session and session.party or {} local count = 0 @@ -193,9 +188,7 @@ Daycare.HANDLERS = { -- pokefirered/src/daycare.c:546 TakePokemonFromDaycare [Std.SPECIAL.TakePokemonFromDaycare] = function(ctx, adapters) local session = sessionOf() - -- pret data/maps/FourIsland_PokemonDayCare/scripts.inc:86-88 refuses the - -- retrieve when CalculatePlayerPartyCount == PARTY_SIZE, before the - -- daycare state is read. + -- data/maps/FourIsland_PokemonDayCare/scripts.inc:86-88 if partyIsFull(session) then setResult(ctx, SPECIES_NONE) return false, SPECIES_NONE @@ -213,8 +206,7 @@ Daycare.HANDLERS = { -- pokefirered/src/daycare.c:1588 TakePokemonFromRoute5Daycare [Std.SPECIAL.TakePokemonFromRoute5Daycare] = function(ctx, adapters) local session = sessionOf() - -- pret data/scripts/day_care.inc:79-81 refuses the retrieve when - -- CalculatePlayerPartyCount == PARTY_SIZE, before the withdrawal runs. + -- data/scripts/day_care.inc:79-81 if partyIsFull(session) then setResult(ctx, SPECIES_NONE) return false, SPECIES_NONE @@ -319,8 +311,7 @@ Daycare.HANDLERS = { local dc = Daycare.stateOf() if not eggPending(dc) then return false end local session = sessionOf() - -- pokefirered/data/maps/FourIsland/scripts.inc:96 (party-full guard, - -- shared with the withdraw specials via partyIsFull) + -- pokefirered/data/maps/FourIsland/scripts.inc:96 if partyIsFull(session) then return false end Breeding.giveEggFromDaycare(session) return false diff --git a/src/core/game3/scripting/natives_events.lua b/src/core/game3/scripting/natives_events.lua index a95f7cac..1f8d4099 100644 --- a/src/core/game3/scripting/natives_events.lua +++ b/src/core/game3/scripting/natives_events.lua @@ -6,7 +6,7 @@ local VAR_RESULT = 0x800D -- pokefirered/include/constants/vars.h:328 local VAR_0x8004 = 0x8004 -- pokefirered/include/constants/vars.h:319 local VAR_0x8005 = 0x8005 -- pokefirered/include/constants/vars.h:320 local VAR_0x8006 = 0x8006 -- pokefirered/include/constants/vars.h:321 -local VAR_FACING = 0x800C -- src/core/game3/scripting/ctx.lua Ctx.VAR_FACING +local VAR_FACING = 0x800C -- pokefirered/src/field_tasks.c:51 local ICEFALL_CAVE_ICE_COORDS = { @@ -110,9 +110,6 @@ Events.HANDLERS = { for i = 1, #ICEFALL_CAVE_ICE_COORDS do if Flags.getFlag(store, ctx, i) then local c = ICEFALL_CAVE_ICE_COORDS[i] - -- MapGridSetMetatileIdAt only swaps the metatile id; cracked ice stays - -- walkable (the collision follows the new metatile), so do not force - -- the impassable override. Field.setMetatile(c[1], c[2], METATILE_SEAFOAM_CRACKED_ICE, false) end end @@ -120,8 +117,6 @@ Events.HANDLERS = { end, -- pokefirered/src/field_specials.c:97 [Std.SPECIAL.ForcePlayerOntoBike] = function() - -- SetPlayerAvatarTransitionFlags(PLAYER_AVATAR_FLAG_MACH_BIKE) only runs - -- for an on-foot avatar; a surfing player is not forced onto the bike. local okP, Player = pcall(require, "src.core.game3.player") if okP and Player and not Player.surfing then Player.biking = true @@ -131,8 +126,6 @@ Events.HANDLERS = { end, -- pokefirered/src/field_specials.c:1513 [Std.SPECIAL.ForcePlayerToStartSurfing] = function() - -- SetPlayerAvatarTransitionFlags(PLAYER_AVATAR_FLAG_SURFING): a forced - -- transition, so no surf hop and the bike override is cleared. local okP, Player = pcall(require, "src.core.game3.player") if okP and Player then Player.surfing = true @@ -176,8 +169,6 @@ Events.HANDLERS = { -- pokefirered/src/field_specials.c:120 ShowFieldMessageStringVar4 [Std.SPECIAL.ShowFieldMessageStringVar4] = function(ctx, adapters) local text = (ctx and ctx.stringVars and ctx.stringVars[4]) or "" - -- pret ShowFieldMessage(gStringVar4): the field box stays up until the - -- script closes it, the same seam the msgbox opcode uses (ops_a.lua:170). if ctx then ctx.messageOpen = true end local openStay = adapters and (adapters.openMessageStay or adapters.openMessageAsync) if openStay then @@ -208,22 +199,19 @@ Events.HANDLERS = { local store = scriptStore(ctx) local stats = session and (session.gameStats or session.stats) or {} - -- Numeric game stats are authoritative (field_specials.c:1737 - -- GetGameStat); session fields / flags apply only when the index is absent. - -- HOF enters: GAME_STAT_ENTERED_HOF = 10 (include/constants/game_stat.h:14) + -- field_specials.c:1737, include/constants/game_stat.h:14 local hof = stats[10] or stats.enteredHof or (session and (session.hofClears or session.hallOfFameCount)) or (Flags.getFlag(store, ctx, "FLAG_SYS_GAME_CLEAR") and 1 or 0) hof = tonumber(hof) or 0 - -- Hatched eggs: GAME_STAT_HATCHED_EGGS = 13 (game_stat.h:17) + -- game_stat.h:17 local eggs = stats[13] or stats.hatchedEggs or (session and session.eggsHatched) or 0 eggs = tonumber(eggs) or 0 local eggsClamped = math.min(0xFFFF, eggs) - -- Link battle wins: GAME_STAT_LINK_BATTLE_WINS = 23 (game_stat.h:27; - -- 24 is LINK_BATTLE_LOSSES, which is why wins used to read 0) + -- game_stat.h:27 local linkWins = stats[23] or stats.linkBattleWins or (session and (session.linkWins or session.linkBattleWins)) or 0 linkWins = tonumber(linkWins) or 0 @@ -337,17 +325,12 @@ Events.HANDLERS = { return false end, -- pokefirered/src/field_specials.c:461 - -- review-v3 Q8: pret src/field_specials.c:461-493 ShakeScreen — camera - -- pan task: VAR_0x8004=y, VAR_0x8005=x, VAR_0x8006=iterations, - -- 0x8007=duration; the pan flips sign every `duration` frames for - -- `iterations` beats, then recentres (FieldView.cameraPanX/Y — the same - -- seam natives_elevator writes). SE_M_STRENGTH = 207 - -- (pret include/constants/songs.h:212), played at task creation. + -- src/field_specials.c:461-493, include/constants/songs.h:212 [Std.SPECIAL.ShakeScreen] = function(ctx) local x = varGet(ctx, VAR_0x8005) local y = varGet(ctx, VAR_0x8004) local iters = varGet(ctx, VAR_0x8006) - local dur = varGet(ctx, 0x8007) -- pret gSpecialVar_0x8007 (no name const) + local dur = varGet(ctx, 0x8007) if (x == 0 and y == 0) or iters < 1 or dur < 1 then return false end local FieldView = package.loaded["src.core.game3.field_view"] local okT, Task = pcall(require, "src.core.game3.task") @@ -375,44 +358,35 @@ Events.HANDLERS = { end) return false end, - -- review-v3 Q8: DEFERRED — InitRoamer (pret src/roamer.c:120) boots the - -- full roamer system (ClearRoamerData + CreateInitialRoamerMon box-mon, - -- location-set tables, roam movement); the port has no roamer runtime - -- beyond battle_bridge's `roamer` flag, so a stateless stub would only - -- create dead state. Needs the roamer feature system first. + -- src/roamer.c:120 [Std.SPECIAL.InitRoamer] = noop, - -- review-v3 Q8: pret src/field_specials.c:679-690 + :697-717 — when the - -- request var is empty, sample an OWNED dex species (100 random rolls, - -- else walk down with the pret wrap), pick the reward (30% luxury ball), - -- reset the step counter, and publish the species name in gStringVar1. - [Std.SPECIAL.SampleResortGorgeousMonAndReward] = function(ctx) + -- src/field_specials.c:679-690 + [Std.SPECIAL.SampleResortGorgeousMonAndReward] = function(ctx, adapters) local session = sessionOf(ctx) if not session then return false end - local VAR_REQ = 0x4036 -- pret include/constants/vars.h:104 - local VAR_REWARD = 0x403B -- pret vars.h:109 - local VAR_STEP = 0x4035 -- pret vars.h:103 (GOREGEOUS typo is pret's) + local VAR_REQ = 0x4036 -- include/constants/vars.h:104 + local VAR_REWARD = 0x403B -- vars.h:109 + local VAR_STEP = 0x4035 -- vars.h:103 local requested = varGet(ctx, VAR_REQ) local store = scriptStore(ctx) local F = flagsMod() if requested == 0 or requested == 0xFFFF then local Rng = require("src.core.game3.rng") local ownedT = (session.dex and (session.dex.owned or session.dex.caught)) or {} - local NUM = 411 -- pret NUM_SPECIES-1 (species.h:423 SPECIES_EGG=412) + local NUM = 411 -- species.h:423 local sp, found = 1, false for _ = 1, 100 do sp = (Rng.Random() % NUM) + 1 if ownedT[sp] then found = true break end end if not found then - -- pret: walk down from the last roll, wrapping 1 → NUM. - for _ = 1, 500 do -- pret is unbounded; cap guards an empty dex + for _ = 1, 500 do if ownedT[sp] then found = true break end if sp == 1 then sp = NUM else sp = sp - 1 end end end F.setVar(store, ctx, VAR_REQ, sp) - -- 107/106/108/109/110/68 = BIG_PEARL/PEARL/STARDUST/STAR_PIECE/ - -- NUGGET/RARE_CANDY (pret items.h:72,110-114); 11 = LUXURY_BALL (:15). + -- items.h:72,110-114 local rewards = { 107, 106, 108, 109, 110, 68 } local reward = 11 if (Rng.Random() % 100) < 30 then @@ -421,16 +395,15 @@ Events.HANDLERS = { F.setVar(store, ctx, VAR_REWARD, reward) F.setVar(store, ctx, VAR_STEP, 0) end - -- pret: StringCopy(gStringVar1, gSpeciesNames[requested]) every call. + -- pokefirered/src/field_specials.c:688 local nameOf = package.loaded["src.core.game3.pokemon"] or require("src.core.game3.pokemon") - if type(session.stringVars) ~= "table" then session.stringVars = {} end - session.stringVars[1] = (nameOf.name and nameOf.name(requested)) or "" + local name = (nameOf.name and nameOf.name(varGet(ctx, VAR_REQ))) or "" + if adapters and adapters.setStringVar then adapters.setStringVar(1, name) end + if ctx and ctx.stringVars then ctx.stringVars[1] = name end return false end, - -- pokefirered/src/script.c:245 DisableMsgBoxWalkaway — the script turns off - -- walk-away cancel for the box it is about to show (questionnaires, move - -- tutors): the mirror image of SetWalkingIntoSignVars above. + -- pokefirered/src/script.c:245 [Std.SPECIAL.DisableMsgBoxWalkaway] = function(ctx) if ctx then ctx.msgBoxIsCancelable = false @@ -463,9 +436,7 @@ Events.HANDLERS = { return false end, -- pokefirered/src/field_specials.c:2512 - -- review-v3 Q8: pret src/field_specials.c:2512-2531 — HOF-clear counts - -- gate the eight Lorelei-house doll flags (25/50/75/100/125/150/175/200), - -- GAME_STAT_ENTERED_HOF = 10 (game_stat.h:14). + -- src/field_specials.c:2512-2531, game_stat.h:14 [Std.SPECIAL.UpdateLoreleiDollCollection] = function(ctx) local session = sessionOf(ctx) local stats = session and (session.gameStats or session.stats) or {} @@ -492,18 +463,11 @@ Events.HANDLERS = { Events.HANDLERS[Std.SPECIAL.SetPostgameFlagsUnusedSlot] = Events.HANDLERS[Std.SPECIAL.SetPostgameFlags] --- pokefirered/include/constants/global.h DIR_* as engine dir codes --- (src/core/game3/field.lua DIR_BY_FACING): down=1 up=2 left=3 right=4. +-- pokefirered/include/constants/global.h local DIR_BY_NAME = { down = 1, up = 2, left = 3, right = 4 } local WALKAWAY_ORDER = { "down", "up", "left", "right" } --- pokefirered/src/field_control_avatar.c:301 FieldInput_HandleCancelSignpost. --- Called every field frame from field.lua with the live script VM and the --- frame's input, before player input is processed (overworld.c:1402). --- Decrements the sign walk-away inhibit timer armed by SetWalkingIntoSignVars; --- once it expires, pushes the D-pad away from the facing direction to cancel --- the open sign message — the engine mirror of EventScript_CancelMessageBox --- (data/event_scripts.s:1166): DoPicboxCancel (close the box), release, end. +-- pokefirered/src/field_control_avatar.c:301, overworld.c:1402, data/event_scripts.s:1166 function Events.pollWalkaway(vm, input) if not vm or not vm.ctx then return end local ctx = vm.ctx @@ -520,9 +484,7 @@ function Events.pollWalkaway(vm, input) end end - -- Walk-away rights die with the script (script.c:349 clears cancelable - -- state on every new script): drop leftovers so the next message box - -- cannot inherit them. + -- script.c:349 if not (vm.isRunning and vm:isRunning()) then if ctx.walkAwayFromSignInhibitTimer ~= nil or ctx.msgBoxIsCancelable ~= nil @@ -533,7 +495,7 @@ function Events.pollWalkaway(vm, input) end local timer = tonumber(ctx.walkAwayFromSignInhibitTimer) - if not timer then return end -- never armed for this script + if not timer then return end if timer > 0 then ctx.walkAwayFromSignInhibitTimer = timer - 1 if session then @@ -542,8 +504,6 @@ function Events.pollWalkaway(vm, input) return end - -- Inhibit expired: walk-away must still be allowed on both halves (a - -- script may have flipped them off with DisableMsgBoxWalkaway). local cancelable = ctx.msgBoxIsCancelable local canWalk = ctx.canWalkAway if session then @@ -553,7 +513,6 @@ function Events.pollWalkaway(vm, input) if cancelable ~= true or canWalk ~= true then return end if ctx.messageOpen ~= true then return end - -- input->dpadDirection != 0 && GetPlayerFacingDirection() != dpadDirection local dir if input then for _, d in ipairs(WALKAWAY_ORDER) do @@ -572,8 +531,6 @@ function Events.pollWalkaway(vm, input) end end if not dir then return end - -- VAR_FACING is stamped by Vm:start from the player facing at script start; - -- the player cannot turn while the script runs, so it is still current. local facing = ctx.specialVars and tonumber(ctx.specialVars[VAR_FACING]) if not facing then local P = package.loaded["src.core.game3.player"] @@ -581,8 +538,7 @@ function Events.pollWalkaway(vm, input) end if not facing or facing == DIR_BY_NAME[dir] then return end - -- data/event_scripts.s:1166 EventScript_CancelMessageBox: DoPicboxCancel, - -- release, end. + -- data/event_scripts.s:1166 if vm.adapters and vm.adapters.closeMessage then vm.adapters.closeMessage() end diff --git a/src/core/game3/scripting/natives_link.lua b/src/core/game3/scripting/natives_link.lua index 26e6ed43..efa1a0e8 100644 --- a/src/core/game3/scripting/natives_link.lua +++ b/src/core/game3/scripting/natives_link.lua @@ -156,10 +156,7 @@ NativesLink.HANDLERS = { [S.Script_ShowLinkTrainerCard] = function(ctx, adapters) return Link.showLinkTrainerCard(ctx, adapters) end, - -- pokefirered/src/event_object_lock.c:106 Script_FacePlayer — - -- data/scripts/cable_club.inc:699/707: the Battle Colosseum and Trade Center - -- attendants turn to the player. Same seam as the faceplayer opcode - -- (ops_a.lua:478): face the object in VAR_LAST_TALKED. + -- pokefirered/src/event_object_lock.c:106, data/scripts/cable_club.inc:699 [Std.SPECIAL.Script_FacePlayer] = function(ctx, adapters) if adapters and adapters.facePlayer then local okF, Flags = pcall(require, "src.core.game3.scripting.flags") @@ -167,11 +164,7 @@ NativesLink.HANDLERS = { end return false end, - -- pokefirered/src/event_object_lock.c:111 Script_ClearHeldMovement — - -- data/scripts/cable_club.inc:701/709: ObjectEventClearHeldMovementIfActive - -- on the attendant after its line. The port's applymovement always - -- completes through its step_end callback, so there is no held schedule - -- left to clear — bound as a safe no-op so the dispatch is never nil. + -- pokefirered/src/event_object_lock.c:111, data/scripts/cable_club.inc:701 [Std.SPECIAL.Script_ClearHeldMovement] = function() return false end, diff --git a/src/core/game3/scripting/natives_tower.lua b/src/core/game3/scripting/natives_tower.lua index 09f5d0f9..2dd5e379 100644 --- a/src/core/game3/scripting/natives_tower.lua +++ b/src/core/game3/scripting/natives_tower.lua @@ -570,11 +570,7 @@ TowerNatives.HANDLERS = { -- pokefirered/src/battle_records.c:136 local kind = (varGet(ctx, VAR_0x8004) ~= 0) and "tower" or "link" local Screen = recordsScreen() - -- Lead adjudication (pret-grounded): with the records Screen up the script - -- PARKS on the waitstate — src/battle_records.c:83 + data/scripts/ - -- cable_club.inc:566-575 — regardless of hosted/headless. Only the - -- no-screen fallback completes the waitstate, so nothing deadlocks when - -- the module cannot be loaded. + -- src/battle_records.c:83, cable_club.inc:566-575 if not Screen then takeScreenForPartyMenu()() return natives().yieldHost(ctx, adapters, function(done) done() end) @@ -610,7 +606,7 @@ TowerNatives.HANDLERS = { return runBattle(ctx, adapters, foe, { trainerId = 0, eReader = which == SPECIAL_BATTLE.EREADER, - -- pret src/battle_tower.c:895-933 StartSpecialBattle case 0/1 + -- src/battle_tower.c:895-933 battleTower = which == SPECIAL_BATTLE.BATTLE_TOWER, secretBase = which == SPECIAL_BATTLE.SECRET_BASE, -- pokefirered/src/battle_message.c:2072 CopyEReaderTrainerName5 diff --git a/src/core/game3/scripting/natives_trade.lua b/src/core/game3/scripting/natives_trade.lua index 3efcc8ea..03809263 100644 --- a/src/core/game3/scripting/natives_trade.lua +++ b/src/core/game3/scripting/natives_trade.lua @@ -351,8 +351,6 @@ end -- pokefirered/src/trade_scene.c:1054 TradeMons function Trade.tradeMons(session, playerSlot, offered) if not (session and offered) then return nil end - -- review-v3 T3: initialise the party first — the write below landed in a - -- throw-away table when session.party was nil and the trade silently died. session.party = session.party or {} local party = session.party local slot = (tonumber(playerSlot) or 0) + 1 @@ -381,8 +379,6 @@ function Trade.tradeMons(session, playerSlot, offered) session.dex = session.dex or { seen = {}, owned = {}, caught = {} } session.dex.seen = session.dex.seen or {} session.dex.owned = session.dex.owned or {} - -- review-v3 F2: caught mirrors owned (dex.lua Dex.setCaught writes all - -- three; save-menu/trainer-card counts read dex.caught). session.dex.caught = session.dex.caught or {} local species = speciesOf(offered) if species ~= SPECIES_NONE then diff --git a/src/core/game3/scripting/natives_wireless.lua b/src/core/game3/scripting/natives_wireless.lua index 95d1196e..aa5a2fd7 100644 --- a/src/core/game3/scripting/natives_wireless.lua +++ b/src/core/game3/scripting/natives_wireless.lua @@ -1,12 +1,4 @@ --- Wireless / RFU-side specials: Pokemon Jump + Dodrio Berry Picking records, --- the Berry Powder vendor exchange (CeruleanCity_House5), Berry Crush rankings, --- and the Seven Island e-Reader trainer house. The port has no Wireless --- Adapter hardware, so RF-internal work degrades to safe answers that let the --- calling scripts finish — no dispatch entry is left nil. --- --- pret anchors: data/specials.inc, src/berry_powder.c, src/pokemon_jump.c, --- src/dodrio_berry_picking.c, src/berry_crush.c, src/battle_tower.c, --- src/field_specials.c. +-- data/specials.inc, src/berry_powder.c, src/pokemon_jump.c, src/dodrio_berry_picking.c, src/berry_crush.c, src/battle_tower.c, src/field_specials.c local Strings = require("src.core.Strings") local Std = require("src.core.game3.scripting.stdscripts") @@ -57,9 +49,7 @@ local function setStringVar(ctx, adapters, index, text) if ctx and ctx.stringVars then ctx.stringVars[index] = text end end --- pokefirered/src/battle_tower.c:1354 ValidateEReaderTrainer / :1368 an --- all-zero record is no trainer at all. Reads the session directly so this --- module never depends on the capability-gated natives_tower module. +-- pokefirered/src/battle_tower.c:1354 local function visitingEReaderTrainer(session) local trainer = session and session.ereaderTrainer if type(trainer) ~= "table" then return nil end @@ -69,9 +59,7 @@ local function visitingEReaderTrainer(session) return trainer end --- pokefirered/src/battle_tower.c:830 BufferBattleTowerTrainerMessage — the --- greeting is an easy-chat phrase; same conversion seam as --- natives_tower.lua:165 convertSpeech. +-- pokefirered/src/battle_tower.c:830 local function convertSpeech(words) if type(words) ~= "table" then return "" end local okE, EasyChatData = pcall(require, "src.core.game3.easy_chat_data") @@ -82,51 +70,34 @@ local function convertSpeech(words) end Wireless.HANDLERS = { - -- pokefirered/src/party_menu.c:5818 ChooseMonForWirelessMinigame - -- data/scripts/cable_club.inc:1181/1196: the picker writes the party slot - -- into VAR_0x8004 and the script aborts when it is >= PARTY_SIZE. The port - -- has no RFU minigame behind the picker, so answer "cancel" deterministically - -- — the script takes its AbortMinigame path instead of dead-ending. + -- pokefirered/src/party_menu.c:5818, data/scripts/cable_club.inc:1181 [Std.SPECIAL.ChooseMonForWirelessMinigame] = function(ctx) varSet(ctx, VAR_0x8004, PARTY_SIZE) return false end, - -- pokefirered/src/pokemon_jump.c:2687 IsPokemonJumpSpeciesInParty - -- data/scripts/cable_club.inc:1177: VAR_RESULT FALSE prints - -- CableClub_EventScript_NoEligiblePkmn and exits. The sPokeJumpMons - -- eligibility table (pokemon_jump.c:766) backs a minigame the port cannot - -- host, so the graceful answer is FALSE — no dead air, no nil dispatch. + -- pokefirered/src/pokemon_jump.c:2687, data/scripts/cable_club.inc:1177, pokemon_jump.c:766 [Std.SPECIAL.IsPokemonJumpSpeciesInParty] = function(ctx) setResult(ctx, 0) return false, 0 end, - -- pokefirered/src/pokemon_jump.c:4487 ShowPokemonJumpRecords - -- data/scripts/cable_club.inc:1278 + TwoIsland_JoyfulGameCorner scripts: - -- `special / waitstate / releaseall`. No RFU link records exist in the - -- port, so the screen is skipped and the waitstate completes instantly. + -- pokefirered/src/pokemon_jump.c:4487, data/scripts/cable_club.inc:1278 [Std.SPECIAL.ShowPokemonJumpRecords] = function() return false end, - -- pokefirered/src/dodrio_berry_picking.c:2929 ShowDodrioBerryPickingRecords - -- data/scripts/cable_club.inc:1286 + Two Island Game Corner records corner. + -- pokefirered/src/dodrio_berry_picking.c:2929, data/scripts/cable_club.inc:1286 [Std.SPECIAL.ShowDodrioBerryPickingRecords] = function() return false end, - -- pokefirered/src/berry_crush.c:3189 ShowBerryCrushRankings - -- data/maps/CeruleanCity_House5/scripts.inc:169 EventScript_BerryCrushRankings: - -- `lockall / special / waitstate / releaseall`. + -- pokefirered/src/berry_crush.c:3189, data/maps/CeruleanCity_House5/scripts.inc:169 [Std.SPECIAL.ShowBerryCrushRankings] = function() return false end, - -- pokefirered/src/berry_powder.c:113 DisplayBerryPowderVendorMenu — draws - -- the powder-amount window over the House5 dialogue. The port shows the - -- amount on the POWDER JAR bag line instead (item_use.lua:727), so this - -- only records that the vendor window pair is open. + -- pokefirered/src/berry_powder.c:113 [Std.SPECIAL.DisplayBerryPowderVendorMenu] = function(ctx) if ctx then ctx.berryPowderVendorOpen = true end local session = sessionOf(ctx) @@ -134,7 +105,7 @@ Wireless.HANDLERS = { return false end, - -- pokefirered/src/berry_powder.c:128 RemoveBerryPowderVendorMenu + -- pokefirered/src/berry_powder.c:128 [Std.SPECIAL.RemoveBerryPowderVendorMenu] = function(ctx) if ctx then ctx.berryPowderVendorOpen = false end local session = sessionOf(ctx) @@ -142,15 +113,12 @@ Wireless.HANDLERS = { return false end, - -- pokefirered/src/berry_powder.c:108 PrintPlayerBerryPowderAmount — repaints - -- the vendor window opened by DisplayBerryPowderVendorMenu. No such window - -- in the port (see Display above), so nothing to repaint: bound no-op. + -- pokefirered/src/berry_powder.c:108 [Std.SPECIAL.PrintPlayerBerryPowderAmount] = function() return false end, - -- pokefirered/src/berry_powder.c:40 Script_HasEnoughBerryPowder - -- VAR_0x8004 holds the cost; answer mirrors the pret bool return. + -- pokefirered/src/berry_powder.c:40 [Std.SPECIAL.Script_HasEnoughBerryPowder] = function(ctx) local session = sessionOf(ctx) local powder = math.floor(tonumber(session and session.berryPowder) or 0) @@ -160,9 +128,7 @@ Wireless.HANDLERS = { return false, enough end, - -- pokefirered/src/berry_powder.c:77 Script_TakeBerryPowder — subtracts - -- VAR_0x8004 when affordable, else answers FALSE and leaves the balance. - -- session.berryPowder is the port's powder field (item_use.lua:728). + -- pokefirered/src/berry_powder.c:77 [Std.SPECIAL.Script_TakeBerryPowder] = function(ctx) local session = sessionOf(ctx) local powder = math.floor(tonumber(session and session.berryPowder) or 0) @@ -177,11 +143,7 @@ Wireless.HANDLERS = { return false, took end, - -- pokefirered/src/field_specials.c:331 BufferEReaderTrainerName — - -- CopyEReaderTrainerName5(gStringVar1) (battle_tower.c:1343). Called by - -- data/maps/SevenIsland_House_Room1/scripts.inc:88; the dialogue prints - -- {STR_VAR_1} (text.inc:19). Physical card data never reaches the port, so - -- fall back to a generic name when no stored record exists. + -- pokefirered/src/field_specials.c:331, battle_tower.c:1343, data/maps/SevenIsland_House_Room1/scripts.inc:88, text.inc:19 [Std.SPECIAL.BufferEReaderTrainerName] = function(ctx, adapters) local trainer = visitingEReaderTrainer(sessionOf(ctx)) local name = trainer and trainer.name @@ -190,11 +152,7 @@ Wireless.HANDLERS = { return false end, - -- pokefirered/src/battle_tower.c:1401 BufferEReaderTrainerGreeting — - -- buffers the card's easy-chat greeting into gStringVar4; - -- data/maps/SevenIsland_House_Room2/scripts.inc:18 prints it via - -- `msgbox gStringVar4`. No card => stored greeting if the record carries - -- one, else a short stock line so the box is never blank. + -- pokefirered/src/battle_tower.c:1401, data/maps/SevenIsland_House_Room2/scripts.inc:18 [Std.SPECIAL.BufferEReaderTrainerGreeting] = function(ctx, adapters) local trainer = visitingEReaderTrainer(sessionOf(ctx)) local greeting = trainer and trainer.greeting @@ -209,10 +167,7 @@ Wireless.HANDLERS = { return false end, - -- pokefirered/src/battle_tower.c:397 SetEReaderTrainerGfxId — - -- VarSet(VAR_OBJ_GFX_ID_0, OBJ_EVENT_GFX_YOUNGSTER) so the visiting trainer - -- object has a gfx id; data/maps/SevenIsland_House_Room2/scripts.inc:7 runs - -- it on transition. + -- pokefirered/src/battle_tower.c:397, data/maps/SevenIsland_House_Room2/scripts.inc:7 [Std.SPECIAL.SetEReaderTrainerGfxId] = function(ctx) varSet(ctx, VAR_OBJ_GFX_ID_0, OBJ_EVENT_GFX_YOUNGSTER) return false diff --git a/src/core/game3/scripting/opcodes.lua b/src/core/game3/scripting/opcodes.lua index 7767eb03..e95e091f 100644 --- a/src/core/game3/scripting/opcodes.lua +++ b/src/core/game3/scripting/opcodes.lua @@ -163,13 +163,9 @@ Opcodes.TABLE = { [0x91] = op("removemoney", 6, { W, B }), [0x92] = op("checkmoney", 6, { W, B }), [0x93] = op("showmoneybox", 4, { B, B, B }), - -- pokefirered/asm/macros/event.inc:1198-1202 hidemoneybox carries TWO - -- dummied operand bytes (x, y) that the stream must still skip (the old - -- size-1 declaration under-read them and desynced — review-v3 E3 sibling). + -- pokefirered/asm/macros/event.inc:1198-1202 [0x94] = op("hidemoneybox", 3, { B, B }), - -- pokefirered/asm/macros/event.inc:1206-1211 updatemoneybox emits THREE - -- operand bytes (dummy x, dummy y, disable); the old 2-byte layout - -- under-read and desynced the stream (review-v3 E3). + -- pokefirered/asm/macros/event.inc:1206-1211 [0x95] = op("updatemoneybox", 4, { B, B, B }), [0x96] = op("getpokenewsactive", 3, { H }), [0x97] = op("fadescreen", 2, { B }), @@ -189,11 +185,9 @@ Opcodes.TABLE = { [0xa5] = op("doweather", 1), [0xa6] = op("setstepcallback", 2, { B }), [0xa7] = op("setmaplayoutindex", 3, { H }), - -- pret src/scrcmd.c:1122-1130 ScrCmd_setobjectsubpriority: - -- VarGet(ScriptReadHalfword) = H, then mapGroup, mapNum, priority bytes. + -- src/scrcmd.c:1122-1130 [0xa8] = op("setobjectsubpriority", 6, { H, B, B, B }), - -- pret src/scrcmd.c:1133-1140 ScrCmd_resetobjectsubpriority: - -- VarGet(ScriptReadHalfword) = H, then mapGroup, mapNum bytes. + -- src/scrcmd.c:1133-1140 [0xa9] = op("resetobjectsubpriority", 5, { H, B, B }), [0xaa] = op("createvobject", 8, { B, B, H, H, B, B }), [0xab] = op("turnvobject", 3, { B, B }), diff --git a/src/core/game3/scripting/ops_a.lua b/src/core/game3/scripting/ops_a.lua index 30714416..799b1a17 100644 --- a/src/core/game3/scripting/ops_a.lua +++ b/src/core/game3/scripting/ops_a.lua @@ -10,10 +10,7 @@ local ModRuntime = require("src.mods.Runtime") local Ops = {} --- FRLG no-ops: pret keeps the command but comments its body out --- (`return FALSE`), so an explicit no-op branch is the faithful wiring and --- stops the Tier-C "skip op" log for them. Per-op citations: --- docs/game3/e10-opcode-spec.md section 3.2 (all scrcmd.c). +-- scrcmd.c local PRET_NO_OPS = { initclock = true, -- scrcmd.c:658-664 dotimebasedevents = true, -- scrcmd.c:667-671 @@ -70,7 +67,8 @@ end local function var_get(store, ctx, id) id = tonumber(id) or 0 -- FRLG VarGet: ids ≥ VARS_START (0x4000) are variables; else literal. - if id >= 0x4000 then + -- pokefirered/include/constants/vars.h:310,313,337 + if (id >= 0x4000 and id <= 0x40FF) or (id >= 0x8000 and id <= 0x8014) then return Flags.getVar(store, ctx, id) end return id @@ -1179,8 +1177,7 @@ local function dispatch(vm, row) or op == "warpteleport" or op == "warpspinenter" then local group, num = row[1], row[2] local warpId = row[3] - -- pokefirered/src/scrcmd.c:719-731 ScrCmd_warp: x and y are VarGet'd - -- (group/num/warpId stay raw bytes). + -- pokefirered/src/scrcmd.c:719-731 local x = var_get(store, ctx, row[4]) local y = var_get(store, ctx, row[5]) if a.warp then @@ -1369,11 +1366,11 @@ local function dispatch(vm, row) return true end if op == "setmetatile" and a.setMetatile then - -- pokefirered/src/scrcmd.c:2103-2108: all four operands are VarGet'd. + -- pokefirered/src/scrcmd.c:2103-2108 a.setMetatile(var_get(store, ctx, row[1]), var_get(store, ctx, row[2]), var_get(store, ctx, row[3]), var_get(store, ctx, row[4]) ~= 0) elseif op == "dofieldeffect" and a.doFieldEffect then - -- pokefirered/src/scrcmd.c:2042-2049: the effect id is VarGet'd. + -- pokefirered/src/scrcmd.c:2042-2049 a.doFieldEffect(var_get(store, ctx, row[1])) elseif op == "setfieldeffectargument" then -- pokefirered/src/scrcmd.c:2051 — the value operand is VarGet'd, which @@ -1396,7 +1393,7 @@ local function dispatch(vm, row) set_map_layout(var_get(store, ctx, row[1]), a.log) return false elseif op == "setweather" then - -- pokefirered/src/scrcmd.c:685-691: the weather id is VarGet'd. + -- pokefirered/src/scrcmd.c:685-691 if a.setWeather then a.setWeather(var_get(store, ctx, row[1] or row.weather or 0)) end return false elseif op == "doweather" then @@ -1745,10 +1742,7 @@ local function dispatch(vm, row) Flags.setVar(store, ctx, Ctx.VAR_RESULT, ok and 1 or 0) return false elseif op == "addmoney" or op == "removemoney" or op == "checkmoney" then - -- pokefirered/src/scrcmd.c:1798-1830: the amount is read RAW (ScriptReadWord, - -- never VarGet) and a disable byte gates the whole command — - -- asm/macros/event.inc:1166-1186: "If 'disable' is set to anything but 0 - -- then this command does nothing." + -- pokefirered/src/scrcmd.c:1798-1830, asm/macros/event.inc:1166-1186 local amount = math.max(0, math.floor(tonumber(row[1] or row.amount) or 0)) local disable = tonumber(row[2] or row.disable) or 0 if disable == 0 then @@ -1782,8 +1776,7 @@ local function dispatch(vm, row) MoneyBox.hide() return false elseif op == "updatemoneybox" then - -- pokefirered/src/scrcmd.c:1848-1856: x/y are read (dummied out) and the - -- disable byte gates the update — event.inc:1204-1211. + -- pokefirered/src/scrcmd.c:1848-1856, event.inc:1204-1211 local disable = tonumber(row[3]) or 0 if disable == 0 then local MoneyBox = require("src.ui.game3.money_box") @@ -1939,7 +1932,7 @@ local function dispatch(vm, row) end return false elseif op == "random" then - -- pokefirered/src/scrcmd.c:455-461: VarGet(ScriptReadHalfword(ctx)) + -- pokefirered/src/scrcmd.c:455-461 local maxv = var_get(store, ctx, row[1]) maxv = tonumber(maxv) or 1 if maxv < 1 then maxv = 1 end @@ -1992,41 +1985,33 @@ local function dispatch(vm, row) if jumped then ctx.pc = nil end return yield elseif op == "setobjectsubpriority" then - -- rse-seams e10 spec 5.8; pret src/scrcmd.c:1122-1130 — VarGet(localId), - -- group/num/priority read raw, SetObjectSubpriority(..., priority + 83). - -- row layout: { H localId, B mapGroup, B mapNum, B priority } (opcodes.lua:188). + -- src/scrcmd.c:1122-1130 local objLid = var_get(store, ctx, row[1]) local Objects = package.loaded["src.core.game3.objects"] or require("src.core.game3.objects") Objects.setSubpriority(objLid, row[2], row[3], (tonumber(row[4]) or 0) + 83) return false elseif op == "resetobjectsubpriority" then - -- rse-seams e10 spec 5.8; pret src/scrcmd.c:1133-1140 — VarGet(localId); - -- the reset re-enables the dynamic path instead of restoring a value. + -- src/scrcmd.c:1133-1140 local objLid = var_get(store, ctx, row[1]) local Objects = package.loaded["src.core.game3.objects"] or require("src.core.game3.objects") Objects.resetSubpriority(objLid, row[2], row[3]) return false elseif op == "gettime" then - -- pokefirered/src/scrcmd.c:673-681: FRLG's RTC lines are commented out and - -- the three special vars are zeroed rather than left stale. + -- pokefirered/src/scrcmd.c:673-681 Flags.setVar(store, ctx, 0x8000, 0) Flags.setVar(store, ctx, 0x8001, 0) Flags.setVar(store, ctx, 0x8002, 0) return false elseif op == "setmysteryeventstatus" then - -- rse-seams e10 5.1; pret src/scrcmd.c:269-273 -> SetMysteryEventScriptStatus - -- (src/mystery_event_script.c:92-95); read back by MEventScript_Run (:75-80). + -- src/scrcmd.c:269-273, src/mystery_event_script.c:92-95 ctx.mysteryEventStatus = row[1] local okMG, MysteryGift = pcall(require, "src.core.game3.mystery_gift") if okMG and MysteryGift and MysteryGift.setStatus then MysteryGift.setStatus(row[1]) end return false elseif op == "gotonative" then - -- rse-seams e10 5.2; pret src/scrcmd.c:92-97 SetupNativeScript jumps to a C - -- function pointer we cannot execute: resolve a symbol instead (the - -- registry stays empty until the extractor emits addr -> symbol) and log - -- the miss once. + -- src/scrcmd.c:92-97 local gaddr = tonumber(row[1]) or 0 local gfn = Natives.resolveNative and Natives.resolveNative(gaddr) if type(gfn) == "function" then @@ -2035,24 +2020,20 @@ local function dispatch(vm, row) Natives.log_once("gotonative", gaddr, a and a.log) return false elseif op == "createvobject" then - -- rse-seams e10 5.3; pret src/scrcmd.c:1171-1181 — x/y are VarGet halves, - -- so resolve them through var_get (never raw). + -- src/scrcmd.c:1171-1181 local VO = package.loaded["src.core.game3.virtual_objects"] or require("src.core.game3.virtual_objects") VO.spawn(row[2], row[1], var_get(store, ctx, row[3]), var_get(store, ctx, row[4]), row[5], row[6]) return false elseif op == "turnvobject" then - -- rse-seams e10 5.4; pret src/scrcmd.c:1184-1190 — a missing id is a - -- logged no-op inside VirtualObjects.turn (GetVirtualObjectSpriteId miss). + -- src/scrcmd.c:1184-1190 local VO = package.loaded["src.core.game3.virtual_objects"] or require("src.core.game3.virtual_objects") VO.turn(row[1], row[2]) return false elseif op == "loadhelp" then - -- rse-seams e10 5.5; pret src/scrcmd.c:1274-1280 + - -- src/new_menu_helpers.c:701-705 — a dedicated help MESSAGE window, not - -- the L/R Help browser (help_system.lua is a different API). + -- src/scrcmd.c:1274-1280, src/new_menu_helpers.c:701-705 local HelpWindow = require("src.ui.game3.help_window") local ir = resolve_text(vm, row[1]) if HelpWindow.show then @@ -2060,23 +2041,18 @@ local function dispatch(vm, row) end return false elseif op == "unloadhelp" then - -- rse-seams e10 5.6; pret src/new_menu_helpers.c:707-710 — close() is - -- safe when nothing is open. + -- src/new_menu_helpers.c:707-710 local HelpWindow = require("src.ui.game3.help_window") if HelpWindow.close then HelpWindow.close() end return false elseif op == "choosecontestmon" then - -- pokefirered/src/scrcmd.c:2010-2016: ChooseContestMon() is commented out - -- in FRLG but `ScriptContext_Stop(); return TRUE;` are live, so the script - -- halts here awaiting a resume FRLG never sends. Mirrored as a park. + -- pokefirered/src/scrcmd.c:2010-2016 ctx.mode = "native" ctx.status = "waiting" ctx.nativePoll = function() return false end return true elseif op == "incrementgamestat" then - -- review-v3 E9: pret src/scrcmd.c:576-579 → overworld.c:366-375 - -- (bounds NUM_USED_GAME_STATS=52 include/constants/game_stat.h:57, - -- saturate 0xFFFFFF). + -- src/scrcmd.c:576-579, overworld.c:366-375, include/constants/game_stat.h:57 local statId = tonumber(row[1]) or -1 if statId >= 0 and statId < 52 then local Runtime = package.loaded["src.core.game3.runtime"] @@ -2089,9 +2065,7 @@ local function dispatch(vm, row) end return false elseif op == "checkpartymove" then - -- review-v3 E9: pret src/scrcmd.c:1777-1795 — Result = first non-egg - -- party mon (0-based) knowing the move, else PARTY_SIZE; 0x8004 = - -- that mon's species. + -- src/scrcmd.c:1777-1795 local moveId = tonumber(row[1]) or 0 local Runtime = package.loaded["src.core.game3.runtime"] local session = Runtime and Runtime.getSession and Runtime.getSession() @@ -2131,9 +2105,6 @@ local function dispatch(vm, row) end return false end - -- Explicit FRLG no-ops (ops that pret itself defines as `return FALSE`). - -- Checked after the host/mod command table so content packs can still - -- give one of these a real behaviour. if PRET_NO_OPS[op] then return false end -- Unknown / Tier C: skip if a.log then a.log("[game3] skip op " .. tostring(op)) end diff --git a/src/core/game3/scripting/stdscripts.lua b/src/core/game3/scripting/stdscripts.lua index a3bf6cdb..22e150c6 100644 --- a/src/core/game3/scripting/stdscripts.lua +++ b/src/core/game3/scripting/stdscripts.lua @@ -190,8 +190,7 @@ Std.SPECIAL = { IsThereMonInRoute5Daycare = 0x178, -- pokefirered/data/specials.inc:387 GetNumLevelsGainedForRoute5DaycareMon = 0x179, -- pokefirered/data/specials.inc:388 TakePokemonFromRoute5Daycare = 0x17A, -- pokefirered/data/specials.inc:389 - -- Unbound-cart specials closed by the Specials Binder wave; every id is the - -- 0-based def_special index of pokefirered/data/specials.inc (line noted). + -- pokefirered/data/specials.inc BufferEReaderTrainerGreeting = 0xEB, -- pokefirered/data/specials.inc:246 ShowDiploma = 0x108, -- pokefirered/data/specials.inc:275 BufferEReaderTrainerName = 0x11D, -- pokefirered/data/specials.inc:296 diff --git a/src/core/game3/storage.lua b/src/core/game3/storage.lua index f8a87d10..9367fb40 100644 --- a/src/core/game3/storage.lua +++ b/src/core/game3/storage.lua @@ -273,11 +273,14 @@ function Storage.moveMon(session, srcLoc, srcIdx, destLoc, destIdx, srcBox, dest session.party[srcIdx] = destMon -- Clean up trailing nils in party array if moved without swap if not destMon and srcIdx > #session.party then - -- compact party (review-v3 T2: ordered loop, not pairs — pairs skips - -- holes and reorders, which could drop a mon mid-compaction) + local keys = {} + for k in pairs(session.party) do + if type(k) == "number" then keys[#keys + 1] = k end + end + table.sort(keys) local newParty = {} - for i = 1, #session.party do - local m = session.party[i] + for _, k in ipairs(keys) do + local m = session.party[k] if m then newParty[#newParty + 1] = m end end session.party = newParty @@ -563,9 +566,7 @@ function Storage.deserialize(data) if not data then return storage end storage.currentBox = tonumber(data.currentBox) or 1 if data.items == nil then - -- review-v3 F7: a serialized save without the items key carries no PC - -- items; start empty instead of keeping the seeded starter Potion - -- (Storage.new :53, pokefirered/src/player_pc.c:100). + -- pokefirered/src/player_pc.c:100 storage.items = {} else storage.items = {} diff --git a/src/core/game3/trig.lua b/src/core/game3/trig.lua index f101cd70..ae2a81c9 100644 --- a/src/core/game3/trig.lua +++ b/src/core/game3/trig.lua @@ -28,8 +28,7 @@ function Trig.sin(i) return Trig.SINE[(math.floor(i) % 320) + 1] end --- pokefirered/src/trig.c:514 — shared round-to-nearest atan2 expressed in u16 turns. --- All anim-port generations delegate here so ArcTan2 cannot drift between ports. +-- pokefirered/src/trig.c:514 function Trig.arcTan2(x, y) local a = math.atan2(y, x) if a < 0 then a = a + 2 * math.pi end diff --git a/src/core/game3/virtual_objects.lua b/src/core/game3/virtual_objects.lua index bbd912b2..ac2e379a 100644 --- a/src/core/game3/virtual_objects.lua +++ b/src/core/game3/virtual_objects.lua @@ -1,23 +1,12 @@ --- Virtual-object registry for the `createvobject` / `turnvobject` seam --- (docs/game3/e10-opcode-spec.md sections 5.3/5.4). --- --- pret: CreateVirtualObject(graphicsId, virtualObjId, x, y, elevation, direction) --- builds a sprite-only NPC that never collides or talks --- (src/event_object_movement.c:1719), looked up by id --- (GetVirtualObjectSpriteId, src/event_object_movement.c:9236), turned by --- facing its sprite (TurnVirtualObject, src/event_object_movement.c:9248-9257), --- and torn down en masse (DestroyVirtualObjects, :9225). This module is the --- data side only: the OW-sprite draw path consumes list() and the draw/collision --- files never see it (new file, unwired — the wiring handoff is listed in the --- spec). +-- src/event_object_movement.c:1719, src/event_object_movement.c:9236, src/event_object_movement.c:9248-9257 local VirtualObjects = {} --- pret include/constants/global.h:110 (also event.inc createvobject default) +-- include/constants/global.h:110, event.inc VirtualObjects.DIR_SOUTH = 1 local byId = {} -local order = {} -- stable spawn order for the draw path (index = position) +local order = {} local logged = {} local function log_once(key, msg) @@ -26,14 +15,7 @@ local function log_once(key, msg) print("[game3/virtual_objects] " .. tostring(msg)) end ---- Register (or replace) a virtual object. Mirrors pret's argument order: --- createvobject graphicsId(B), id(B), x(H), y(H), elevation(B), direction(B) --- (src/scrcmd.c:1171-1181). Returns the record, or nil on a bad id. --- --- Documented divergence: pret allows two sprites to share an id and resolves --- turn() to the first; this is a keyed map, so a duplicate id REPLACES the --- entry (a script that re-spawns gets fresh coordinates). Registered ids are --- u8 by construction (the opcode decoder only ever yields a byte). +-- src/scrcmd.c:1171-1181 function VirtualObjects.spawn(vObjId, graphicsId, x, y, elevation, direction) local id = tonumber(vObjId) if id == nil then @@ -48,7 +30,7 @@ function VirtualObjects.spawn(vObjId, graphicsId, x, y, elevation, direction) graphicsId = tonumber(graphicsId) or 0, x = tonumber(x) or 0, y = tonumber(y) or 0, - -- event.inc:1346 defaults: elevation=3, direction=DIR_SOUTH + -- event.inc:1346 elevation = tonumber(elevation) or 3, direction = tonumber(direction) or VirtualObjects.DIR_SOUTH, } @@ -56,8 +38,6 @@ function VirtualObjects.spawn(vObjId, graphicsId, x, y, elevation, direction) return rec end ---- Face an existing object (pret TurnVirtualObject). A missing id is a --- logged no-op returning false, mirroring pret's MAX_SPRITES miss path. function VirtualObjects.turn(vObjId, direction) local id = tonumber(vObjId) local rec = id and byId[id] or nil @@ -70,13 +50,11 @@ function VirtualObjects.turn(vObjId, direction) return true end ---- The record for an id, or nil (pret GetVirtualObjectSpriteId's miss case). function VirtualObjects.get(vObjId) local id = tonumber(vObjId) return id and byId[id] or nil end ---- Live records in spawn order — the draw path iterates this and nothing else. function VirtualObjects.list() local out = {} for i = 1, #order do @@ -92,13 +70,12 @@ function VirtualObjects.count() return n end ---- Map unload / map change: pret DestroyVirtualObjects (event_object_movement.c:9225). +-- event_object_movement.c:9225 function VirtualObjects.clear() for k in pairs(byId) do byId[k] = nil end for i = #order, 1, -1 do order[i] = nil end end ---- Test/tool hook: forget the state and the log-once keys. function VirtualObjects.reset() VirtualObjects.clear() logged = {} diff --git a/src/import/CacheFs.lua b/src/import/CacheFs.lua index eff25a8b..6378c325 100644 --- a/src/import/CacheFs.lua +++ b/src/import/CacheFs.lua @@ -46,7 +46,6 @@ local function withPrefix(rel) return p .. rel end --- L5: cache-relative paths must stay under the cache root; reject traversal. local function unsafe_rel(rel) return type(rel) ~= "string" or rel:find("..", 1, true) ~= nil end diff --git a/src/import/RomExtractorGen3.lua b/src/import/RomExtractorGen3.lua index 85852f47..6a864b2c 100644 --- a/src/import/RomExtractorGen3.lua +++ b/src/import/RomExtractorGen3.lua @@ -1,8 +1,3 @@ --- Gen 3 extractor (game id from GameVersion): GBA ROM -> cache under --- CacheFs.prefix. Parallel to RomExtractor / RomExtractorGen2. Full Island-1 --- demake lives in src/import/gba/extract_island1.lua; this module publishes --- the CacheContract override files and, when possible, runs that extract. - local CacheFs = require("src.import.CacheFs") local LuaWriter = require("src.import.LuaWriter") local GameVersion = require("src.core.GameVersion") @@ -14,13 +9,10 @@ RomExtractorGen3.__index = RomExtractorGen3 local STAGE_COUNT = 5 local GBA_ROOT = CachePaths.CACHE_ROOT --- LeafGreen is FireRed's data; everything else canonicalises to itself. local function canonicalImportId(id) return id == "leafgreen" and "firered" or id end --- The id the ROM itself declares (sha1 -> GameVersion), falling back to the --- historical FireRed assumption for an unrecognised ROM. local function importVersion(sha1) return GameVersion.forSha1(sha1) or "firered" end @@ -41,7 +33,6 @@ local function writeText(rel, body) end local function writeJson(rel, obj) - -- Deterministic (sorted-key) JSON: see src/import/canonical_json.lua. local Canon = require("src.import.canonical_json") writeText(rel, Canon.encode(obj) .. "\n") end @@ -168,7 +159,6 @@ function RomExtractorGen3:tickPokemon(name, current, total) end end --- Minimal trees CacheContract.VERSION_REQUIRED_FILES_OVERRIDE[version] needs, -- plus semantic module stubs for SEMANTIC_MODULES[3]. function RomExtractorGen3:writeRequiredMarkers(sha1) local Versions = require("src.import.gba.versions") @@ -310,8 +300,6 @@ end function RomExtractorGen3:runAuxExtracts(sha1) local GameVersion = require("src.core.GameVersion") local Profile = require("src.core.game3.profile") - -- rse-seams T3.4: the aux list comes from the profile row, so an RSE import - -- never attempts FRLG-only packs (fame/teachy/tower/seagallop/...). local wantedList = Profile.of(GameVersion.get()).extractors local wanted = nil if type(wantedList) == "table" and #wantedList > 0 then @@ -345,8 +333,6 @@ function RomExtractorGen3:runAuxExtracts(sha1) Extract.CACHE_ROOT = GBA_ROOT local cache = makeCache() - -- rse-seams T3.4: each extractor is eligible only if the profile lists it - -- AND its output is still missing (the readiness checks are unchanged). local needRegion = wantedExtractor("region_map_extract") and not RegionMapExtract.ready(cache, GBA_ROOT) local needSections = wantedExtractor("map_sections_extract") and not CacheFs.exists(GBA_ROOT .. "/region_map/map_sections.lua") @@ -642,10 +628,6 @@ function RomExtractorGen3:run() local okPar, parErr = self:runParallel(sha1) if okPar then - -- The parallel workers run the same stages as the sequential path, so a - -- fresh import must leave the same status markers behind. The region_map - -- marker gates tests/game3_region_map_assets_test.lua, which skipped on - -- fresh imports because only the fallback path wrote it. writeJson(GBA_ROOT .. "/pokemon/extract_status.json", { ok = true, error = nil }) writeJson(GBA_ROOT .. "/region_map/extract_status.json", { ok = true, error = nil }) self:report(1.00, "Ready", 1, 1) diff --git a/src/import/SaveFileIO.lua b/src/import/SaveFileIO.lua index 0081adfe..74f2cbcc 100644 --- a/src/import/SaveFileIO.lua +++ b/src/import/SaveFileIO.lua @@ -24,9 +24,6 @@ local SaveFileIO = {} -- rather than inside it: export needs the regions the codec does not model, -- and 32 KB of binary in the serialized table is 40 KB of Lua source reparsed -- on every save and load. --- review-v3 L6: validate slot ids at the path interpolators (defence in --- depth over SaveData's registry choke point): only `slotN` — plus the --- legacy literal "save" the export path falls back to — may enter a path. local function valid_slot_id(id) id = tostring(id) return id:match("^slot%d+$") ~= nil or id == "save" @@ -50,7 +47,7 @@ local function writeCart(version, slotId, bytes) fs.createDirectory("saves/" .. version) end local rel = cartPath(version, slotId) - if not rel then return end -- review-v3 L6: invalid slot id + if not rel then return end fs.write(rel, bytes) end @@ -58,7 +55,7 @@ local function readCart(version, slotId) local fs = cartFs() if not (fs and fs.read) then return nil end local rel = cartPath(version, slotId) - if not rel then return nil end -- review-v3 L6: invalid slot id + if not rel then return nil end local ok, bytes = pcall(fs.read, rel) if ok and type(bytes) == "string" then return bytes end return nil @@ -186,9 +183,6 @@ function SaveFileIO.exportActiveSlot(version) if not save then return false, "this game has no save to export yet" end local activeSlot = SaveData.activeSlot(version) local slotId = activeSlot or "save" - -- review-v3 L6: never interpolate an unvalidated slot id into the export - -- path (registry entries pass SaveData's choke point; this is the last - -- mile before format()). if not valid_slot_id(slotId) then return false, "invalid save slot id" end if activeSlot and type(save.meta) == "table" then local minted, id = pcall(SaveData.slotPlaythroughId, version, activeSlot, save) diff --git a/src/import/canonical_json.lua b/src/import/canonical_json.lua index 49d81674..b0624975 100644 --- a/src/import/canonical_json.lua +++ b/src/import/canonical_json.lua @@ -1,15 +1,3 @@ --- Deterministic JSON writer for cache artifacts. --- --- Json.encode (src/link/Json.lua) emits object keys in Lua's pairs order. --- That order varies between processes on this build, so two identical fresh --- imports of the same ROM produced different bytes for every JSON object --- (917 map_tree files + census + meta/maps). This wrapper keeps Json's exact --- value formatting (scalars and string escaping are delegated to Json.encode) --- and mirrors its array rule (contiguous [1..n]; empty table -> []), but emits --- OBJECT KEYS IN SORTED ORDER so the bytes are stable run to run. --- --- Used by src/import/gba/map_tree_extract.lua and src/import/RomExtractorGen3.lua. - local Json = require("src.link.Json") local Canon = {} @@ -17,10 +5,9 @@ local Canon = {} local function encode(v, out) local t = type(v) if t ~= "table" then - out[#out + 1] = Json.encode(v) -- null / boolean / number / string + out[#out + 1] = Json.encode(v) return end - -- array rule copied from Json.encodeValue: contiguous [1..n], empty -> [] local n = #v local isArray = n > 0 if not isArray then isArray = next(v) == nil end @@ -35,8 +22,6 @@ local function encode(v, out) end local keys = {} for k in pairs(v) do keys[#keys + 1] = k end - -- tostring comparator keeps number/string hybrid keys sortable (Json emits - -- every key through tostring too). table.sort(keys, function(a, b) return tostring(a) < tostring(b) end) out[#out + 1] = "{" for i = 1, #keys do diff --git a/src/import/gba/battle_anim_extract.lua b/src/import/gba/battle_anim_extract.lua index d2d18c87..b88133b1 100644 --- a/src/import/gba/battle_anim_extract.lua +++ b/src/import/gba/battle_anim_extract.lua @@ -379,8 +379,6 @@ local function decode_script(rom, startOff, visited, labels, tag_dims) i = i + 2 elseif op == 0x24 then - -- review-v3 R5: 0x24 = jumpifcontest + .4byte branch target — emit - -- the label and eagerly decode the target like call (:0x0E). local target_off = rom:ptrOffset(rom:u32(i + 1)) ops[#ops + 1] = { op = "jumpifcontest", label = target_off and tostring(target_off) } i = i + 5 diff --git a/src/import/gba/extract_island1.lua b/src/import/gba/extract_island1.lua index b9a9341e..03de4c68 100644 --- a/src/import/gba/extract_island1.lua +++ b/src/import/gba/extract_island1.lua @@ -15,11 +15,6 @@ local Maps = require("src.import.gba.maps") local Extract = {} --- CacheFS paths: the single source of truth is the zero-require CachePaths --- module (rse-seams T6.3b / I8: the runtime must not pull the ROM extractor --- just to read a path). Extract.CACHE_ROOT/NATIVE_ROOT stay as forwarding --- references so external setters (Dataset.mountExtractRoots) and the many UI --- readers keep working unchanged. local CachePaths = require("src.core.game3.cache_paths") setmetatable(Extract, { __index = function(t, k) @@ -58,9 +53,6 @@ local function write_json(cache, rel, obj) for i = 1, #v do parts[i] = enc(v[i]) end return "[" .. table.concat(parts, ",") .. "]" end - -- Deterministic bytes: pairs() order varies between processes, which made - -- meta.json (and friends) differ run to run. Sort the object keys the - -- same way src/import/canonical_json.lua does. local keys = {} for k in pairs(v) do keys[#keys + 1] = k end table.sort(keys, function(a, b) return tostring(a) < tostring(b) end) @@ -214,9 +206,6 @@ function Extract.nativeReady(cache) if not man or (tonumber(man.native_version) or 0) < (Versions.NATIVE_VERSION or 5) then return false end - -- review-v3 B4: smoke-check EVERY pairs entry (writeExtract emits over for - -- each one); accepting the cache after the first entry let a half-written - -- native cache read as ready. local checked = 0 for pairName in pairs(man.pairs or {}) do if not cache:exists(Extract.NATIVE_ROOT .. "/" .. pairName .. "/mids_over.idx") then @@ -339,8 +328,6 @@ function Extract.run(imports, cache, progressCb) progress(progressCb, 2, "maps", 0, 1) local Maps = require("src.import.gba.maps") local grids = {} - -- review-v3 B3: a map whose layout spec or tileset bundle is missing must - -- never be dropped silently — log it and surface a non-success status. local dropped = {} for _, mapId in ipairs(mapOrder) do local spec = Versions.MAPS[mapId] @@ -540,8 +527,6 @@ function Extract.run(imports, cache, progressCb) for _, mapId in ipairs(mapOrder) do local conns = connections[mapId] or {} cl[#cl + 1] = (" %s = {\n"):format(mapId) - -- Deterministic output: sort the direction keys (pairs order varies per - -- process and made connections.lua byte-unstable between runs). local dirs = {} for dir in pairs(conns) do dirs[#dirs + 1] = dir end table.sort(dirs) @@ -1900,8 +1885,6 @@ local function _dormant_quantize_run(imports, cache, progressCb) for _, mapId in ipairs(mapOrder) do local conns = connections[mapId] or {} cl[#cl + 1] = (" %s = {\n"):format(mapId) - -- Deterministic output: sort the direction keys (pairs order varies per - -- process and made connections.lua byte-unstable between runs). local dirs = {} for dir in pairs(conns) do dirs[#dirs + 1] = dir end table.sort(dirs) diff --git a/src/import/gba/extract_map_events.lua b/src/import/gba/extract_map_events.lua index 3dfa387e..5ff71bd9 100644 --- a/src/import/gba/extract_map_events.lua +++ b/src/import/gba/extract_map_events.lua @@ -35,11 +35,7 @@ local function parse_objects(rom, ptr, count) local base = off + i * OBJ_SIZE local localId = rom:get(base) local graphics = rom:get(base + 1) - -- pret include/constants/event_objects.h:194-195: kind 0 = normal - -- template, 255 = clone. The union at +8 is then targetLocalId / - -- padding / targetMapNum / targetMapGroup instead of the normal - -- movement/trainer fields (include/global.fieldmap.h:110-130), so - -- decoding a clone as a normal NPC reads garbage movement. + -- include/constants/event_objects.h:194-195, fieldmap.h:110-130 local kind = rom:get(base + 2) local isClone = kind == 255 local x = rom:u16(base + 4) diff --git a/src/import/gba/map_catalog.lua b/src/import/gba/map_catalog.lua index 39b84740..b46d9f84 100644 --- a/src/import/gba/map_catalog.lua +++ b/src/import/gba/map_catalog.lua @@ -23,7 +23,6 @@ local function pret_to_engine(pret) s = s:gsub("(%l)(%u)", "%1_%2") s = s:gsub("-", "_"):upper() s = s:gsub("_+", "_") - -- The synthesized prefix comes from the active game's profile (T1.2). return Profile.active().map.enginePrefix .. s end diff --git a/src/import/gba/map_tree_extract.lua b/src/import/gba/map_tree_extract.lua index 1282cd35..d0f81f12 100644 --- a/src/import/gba/map_tree_extract.lua +++ b/src/import/gba/map_tree_extract.lua @@ -5,7 +5,6 @@ local MapTree = require("src.import.gba.map_tree") local Lz77 = require("src.import.gba.lz77") --- Deterministic (sorted-key) JSON: see src/import/canonical_json.lua. local Canon = require("src.import.canonical_json") local MapTreeExtract = {} @@ -98,10 +97,6 @@ local function simplify_events(ev) x = c.x, y = c.y, elevation = c.elevation, - -- review-v3 R1: parse_coord_events emits var/value (extract_map_events - -- :158-180); trigger/index never existed on the producer, and - -- field.lua:440 gates coord scripts on ev.var/ev.value — the missing - -- keys made every conditional coord trigger fire unconditionally. var = c.var, value = c.value, scriptKey = c.scriptKey, @@ -123,9 +118,6 @@ local function pack_tileset(rom, cache, root, ts) local raw = Lz77.decompress(function(i) return rom:get(i) end, tilesOff) tilesBlob = bytes_to_string(raw) elseif tilesOff then - -- review-v3 R4: uncompressed tilesets have no embedded length — derive - -- it from the gap to the palette block and dump the bytes instead of - -- silently emitting no tiles.4bpp (meta still written either way). local palsOff = rom:ptrOffset(ts.palettesPtr) if palsOff and palsOff > tilesOff then tilesBlob = rom_blob(rom, ts.tilesPtr, palsOff - tilesOff) diff --git a/src/import/gba/native_pack.lua b/src/import/gba/native_pack.lua index 741c69b3..debc0628 100644 --- a/src/import/gba/native_pack.lua +++ b/src/import/gba/native_pack.lua @@ -465,7 +465,6 @@ end -- midIndex: optional [pair][mid] = { coll, ... } for resolved COLL_* lookup -- CollisionFn: function(mid, rawColl, behavior, kind) → collByte function NativePack.writeExtract(cache, root, bundles, grids, borders, pairNames, midIndex, behaviorOf, fromCell, scriptMids, warpCells) - -- rse-seams T6.3b: the default root comes from the shared CachePaths module. root = root or require("src.core.game3.cache_paths").CACHE_ROOT local NativeRoot = root .. "/native" local manifest = { diff --git a/src/import/gba/ow_extract.lua b/src/import/gba/ow_extract.lua index 638b17cf..eb5538d7 100644 --- a/src/import/gba/ow_extract.lua +++ b/src/import/gba/ow_extract.lua @@ -88,31 +88,50 @@ local function read_graphics_info(rom, infoOff) } end ---- Max imageValue referenced by anim table (capped). -local function max_anim_frame(rom, animsPtr, animCount) - local aoff = gba_off(animsPtr) - if not aoff then return 0 end - animCount = animCount or 20 - local mx = 0 - for i = 0, animCount - 1 do - local p = rom:u32(aoff + i * 4) - local po = gba_off(p) - if not po then break end - for j = 0, 31 do - local lo = rom:u16(po + j * 4) - -- review-v3 R2: ANIMCMD_END = -1 → 0xFFFF stops the scan; JUMP (-2, - -- 0xFFFE) and LOOP (-3, 0xFFFD) are 4-byte control cmds with a u16 - -- operand, not frames (pret include/sprite.h:84-88). The old 0xFFFE - -- break stopped on JUMP while believing it was END. - if lo == 0xFFFF then break end - if lo ~= 0xFFFE and lo ~= 0xFFFD then - local w = rom:u32(po + j * 4) - local img = w % 65536 - if img > mx then mx = img end - end - end +local MAX_PIC_FRAMES = 32 +local picStartsCache = setmetatable({}, { __mode = "k" }) + +local function pic_table_starts(rom, pointers, num) + local byRom = picStartsCache[rom] + if not byRom then byRom = {}; picStartsCache[rom] = byRom end + local key = pointers .. ":" .. num + if byRom[key] then return byRom[key] end + local starts, infos = {}, {} + local function add(infoOff) + if infos[infoOff] then return false end + if infoOff + 0x24 > rom.size or rom:u16(infoOff) ~= 0xFFFF then return false end + local animsOff = gba_off(rom:u32(infoOff + 0x18)) + local imagesOff = gba_off(rom:u32(infoOff + 0x1C)) + if not animsOff or not imagesOff then return false end + infos[infoOff] = true + starts[imagesOff] = true + return true end - return mx + for g = 0, num - 1 do + local infoOff = gba_off(rom:u32(pointers + g * 4)) + if infoOff then add(infoOff) end + end + local known = {} + for off in pairs(infos) do known[#known + 1] = off end + for _, off in ipairs(known) do + local nextOff = off + 0x24 + while add(nextOff) do nextOff = nextOff + 0x24 end + end + byRom[key] = starts + return starts +end + +-- pokefirered/src/data/object_events/object_event_pic_tables.h +local function pic_table_len(rom, imagesOff, frameBytes, starts) + local n = 0 + while n < MAX_PIC_FRAMES do + local off = imagesOff + n * 8 + if n > 0 and starts[off] then break end + if off + 8 > rom.size then break end + if not gba_off(rom:u32(off)) or rom:u16(off + 4) ~= frameBytes then break end + n = n + 1 + end + return n end --- Decode one 4bpp sprite frame (tile order: L→R, T→B 8×8) → indexed [w*h]. @@ -198,16 +217,10 @@ function OwExtract.extractOne(rom, graphicsId, palsByTag, version) if w < 8 or h < 8 or w > 128 or h > 128 then return nil, "bad dimensions" end - local frameCount - if info.inanimate then - frameCount = 1 - else - -- Only ANIM_STD_* (0..19). Run/spin anims can reference higher indices - -- that aren't in the base pic table for ordinary NPCs. - local maxFrame = max_anim_frame(rom, info.animsPtr, 20) - frameCount = math.max(1, maxFrame + 1) - if frameCount > 18 then frameCount = 18 end - end + local imagesOff0 = gba_off(info.imagesPtr) + local starts = pic_table_starts(rom, pointers, num) + local frameCount = imagesOff0 and pic_table_len(rom, imagesOff0, math.floor(w * h / 2), starts) or 0 + if frameCount < 1 then frameCount = 1 end -- For Town Map (OBJ_EVENT_GFX_TOWN_MAP = 93) or 16x16 inanimate objects with 32x16 OAM allocation: -- The sprite is a 16x16 tile image on the left; adjust width to 16 for proper 1:1 tile grid alignment. diff --git a/src/import/gba/trade_extract.lua b/src/import/gba/trade_extract.lua index 6ba500dc..d2b828f0 100644 --- a/src/import/gba/trade_extract.lua +++ b/src/import/gba/trade_extract.lua @@ -45,9 +45,7 @@ local function anim_frame_tiles(rom, off) local out = {} for i = 0, 63 do local v = rom:u16(off + i * 4) - -- review-v3 R2: END 0xFFFF stops; JUMP (0xFFFE) / LOOP (0xFFFD) are - -- control cmds, not tiles (pret include/sprite.h:84-88) — skip them - -- instead of breaking so frames after a jump are still collected. + -- include/sprite.h:84-88 if v == 0xFFFF then break end if v ~= 0xFFFE and v ~= 0xFFFD then out[#out + 1] = v diff --git a/src/import/gba/versions.lua b/src/import/gba/versions.lua index 14781c81..dcf8801e 100644 --- a/src/import/gba/versions.lua +++ b/src/import/gba/versions.lua @@ -41,9 +41,9 @@ Versions.ROM_SIZE = 16777216 -- v114: pokemon/icons/412.rgba, the SPECIES_EGG menu icon — eggs were drawn -- with the icon of the species they hatch into. -- v115: LeafGreen profiles, edition-specific title assets and Deoxys stats. -Versions.CACHE_VERSION = 116 +Versions.CACHE_VERSION = 117 Versions.NATIVE_VERSION = 6 -Versions.OW_VERSION = 2 +Versions.OW_VERSION = 3 Versions.ANIM_VERSION = 1 -- Audio pack (M4A banks / DirectSound samples / cries). Versions.AUDIO_VERSION = 6 @@ -2485,10 +2485,6 @@ function Versions.select(identity) edition = game end --- rse-seams T6.3b: the per-game facade lives in versions_game.lua (T6.3a); --- Versions.game is the alias its consumers read. Kept alongside the edition --- selector above: editions remap addresses in place, Versions.game picks the --- table module. Versions.game = require("src.import.gba.versions_game").game return Versions diff --git a/src/import/gba/versions_game.lua b/src/import/gba/versions_game.lua index 86ca9716..348baae9 100644 --- a/src/import/gba/versions_game.lua +++ b/src/import/gba/versions_game.lua @@ -1,23 +1,10 @@ --- Per-game selector over the GBA version tables (T6.3a skeleton). --- --- Today there is one flat FireRed monolith (src/import/gba/versions.lua) and --- every extractor reads it as a singleton, so an RSE port has nowhere to put --- its own ROM offsets (I7). This module maps a game id to its table module; --- the FireRed/LeafGreen rows name the monolith, an RSE row lands with --- versions_rse.lua (T6.4). --- --- Unwired: nothing requires this file yet. The T6.2/T6.3 handoff patch aliases --- Versions.game = require("src.import.gba.versions_game").game --- and routes extractor reads through it. Resolution fails closed to FireRed, --- the same contract as src/core/game3/profile.lua. - local GameVersion = require("src.core.GameVersion") local VersionsGame = {} VersionsGame.GAMES = { firered = "src.import.gba.versions", - leafgreen = "src.import.gba.versions", -- shares FireRed's tables + leafgreen = "src.import.gba.versions", } VersionsGame.FALLBACK = "firered" @@ -36,7 +23,6 @@ local function load(id) return nil end ---- The version table module for a game id (nil or "" = active game). function VersionsGame.game(id) if type(id) ~= "string" or id == "" then local active = GameVersion.get() @@ -60,7 +46,6 @@ function VersionsGame.game(id) return VersionsGame.game(VersionsGame.FALLBACK) end ---- Register a game's table module (a port adds its row at boot; the test seam). function VersionsGame.register(id, modulePath) if type(id) ~= "string" or id == "" then return false end if type(modulePath) ~= "string" or modulePath == "" then return false end @@ -69,7 +54,6 @@ function VersionsGame.register(id, modulePath) return true end ---- Test/tool hook: drop resolutions; registrations survive. function VersionsGame.reset() cache = {} warned = {} diff --git a/src/mods/Loader.lua b/src/mods/Loader.lua index b7a99e11..b1c3a5a3 100644 --- a/src/mods/Loader.lua +++ b/src/mods/Loader.lua @@ -315,9 +315,6 @@ function Loader.new(opts) -- builds a loader, and a run never changes generation underneath one. -- opts.generation is the test seam. generation = (opts and opts.generation) or GameVersion.generation(), - -- Which game within the generation (J10): GameVersion id when this boot - -- has one, the test seam opts.version otherwise. nil means "no per-game - -- arm", which resolves to the generation's default exactly as before. version = (opts and opts.version) or nil, }, Loader) assert(self.fs, "Loader.new requires opts.fs when love is unavailable") @@ -1295,10 +1292,6 @@ function Loader:releaseModInput(modId) end end --- Gen 3 API facades by game id (J10): a second Gen 3 game adds a row when its --- facade genuinely diverges, and an unknown or absent id falls back to the --- FireRed-backed module -- exactly what the old generation-only dispatch --- returned for every Gen 3 game, so FireRed and LeafGreen are unchanged. local GEN3_API = { firered = { battle = "src.battle.game3.BattleAPI", world = "src.world.game3.WorldAPI" }, leafgreen = { battle = "src.battle.game3.BattleAPI", world = "src.world.game3.WorldAPI" }, diff --git a/src/mods/Schemas.lua b/src/mods/Schemas.lua index 2e014184..411b9a4e 100644 --- a/src/mods/Schemas.lua +++ b/src/mods/Schemas.lua @@ -677,24 +677,12 @@ Schemas.GEN3 = { apricorns = false, landmarks = false, radio_channels = false, } --- Per-version Gen 3 overlays (J10): every Gen 3 game routes through --- Schemas.GEN3 and binds the same live modules today, so a Ruby/Sapphire/ --- Emerald divergence is added here as a sparse row, never by forking GEN3 or --- LIVE_MODULES. An unknown or absent version reads the GEN3 row exactly as --- the old generation-only dispatch did, so FireRed is unchanged. --- --- Schemas.GEN3_ROUTING.ruby = { trainers = "gen3TrainersRuby" } --- Schemas.GEN3_LIVE_MODULES.ruby = { gen3Trainers = "src.core.game3.scripting.trainers" } Schemas.GEN3_ROUTING = {} Schemas.GEN3_LIVE_MODULES = {} --- merged routing views are cached per overlay, never written into the row local mergedRouting = setmetatable({}, { __mode = "k" }) -- The routing table for a generation: which one is consulted is the only --- difference between the directions, and `version` narrows Gen 3 only. --- An unknown generation routes nothing, so every registry keeps its catalog --- target. local NO_ROUTING = {} function Schemas.routing(generation, version) @@ -946,23 +934,16 @@ local LIVE_MODULES = { gen3Trainers = "src.core.game3.scripting.trainers", } --- The live module behind a gen3* binding, narrowed by the data's game when a --- per-version overlay registers one (J10). version nil reads FireRed's set. function Schemas.liveModuleFor(key, version) local overlay = type(version) == "string" and Schemas.GEN3_LIVE_MODULES[version] local path = (overlay and overlay[key]) or LIVE_MODULES[key] return package.loaded[path or ""] end --- bindGen3 records which game's facades a Data table belongs to; the weak key --- keeps the table collectable and the value is the GameVersion id (or true --- when the caller had none). Version-less callers behave exactly as before. function Schemas.bindGen3(data, version) if type(data) == "table" then bound[data] = version or true end end ---- The game id a Data table was bound to, or nil when unbound or bound --- without a version (the pre-J10 callers). function Schemas.boundVersion(data) if type(data) ~= "table" then return nil end local version = bound[data] diff --git a/src/ui/game3/bag_chrome.lua b/src/ui/game3/bag_chrome.lua index 86f23919..b416b7a0 100644 --- a/src/ui/game3/bag_chrome.lua +++ b/src/ui/game3/bag_chrome.lua @@ -132,8 +132,6 @@ function BagChrome.install(cache) end function BagChrome.ready() - -- review-v3 G2: latch the probe; bag_menu.lua:893 calls this from draw() - -- every frame and the disk read of bg.rgba must not repeat. if BagChrome._ready ~= nil then return BagChrome._ready end local man = BagChrome._manifest or load_lua(bag_root() .. "/manifest.lua") BagChrome._manifest = man diff --git a/src/ui/game3/box_storage_ui.lua b/src/ui/game3/box_storage_ui.lua index 3caf1274..b33ff0d9 100644 --- a/src/ui/game3/box_storage_ui.lua +++ b/src/ui/game3/box_storage_ui.lua @@ -78,7 +78,6 @@ end local function current_box_data() local storage = Storage.ensure(BoxStorageUI._session) - -- review-v3 T7: a nil session must not crash the box query. if not storage then return nil, nil end local bId = storage.currentBox or 1 return storage.boxes[bId], bId @@ -90,8 +89,6 @@ local function mon_at_cursor() if BoxStorageUI.mode == "action_menu" and BoxStorageUI._actionTarget then return BoxStorageUI._actionTarget.mon, BoxStorageUI._actionTarget.loc, BoxStorageUI._actionTarget.boxId, BoxStorageUI._actionTarget.slot elseif BoxStorageUI.mode == "party_drawer" or (BoxStorageUI.drawerOpen and BoxStorageUI._actionSource == "party") then - -- W12: Storage.ensure returns nil for a nil session; every branch of this - -- draw-path helper must tolerate nil storage/session. local pIdx = BoxStorageUI.partyCursor or 1 if pIdx < 1 or pIdx > 6 then return nil, "party", nil, pIdx end local isPickedUp = (BoxStorageUI.holdingMon and BoxStorageUI.holdingSource @@ -120,8 +117,6 @@ function BoxStorageUI.show(opts) BoxStorageUI.mode = "browse" BoxStorageUI.subMode = opts.subMode or "move" BoxStorageUI.cursorSlot = 1 - -- review-v3 T1: a stale return-slot from the previous session must not - -- resurrect the cursor (clamp happens on read; reset here too). BoxStorageUI._prevPartySlot = nil BoxStorageUI.holdingMon = nil BoxStorageUI.holdingSource = nil @@ -705,7 +700,6 @@ function BoxStorageUI.draw() if not BoxStorageUI.open then return end local session = BoxStorageUI._session local storage = Storage.ensure(session) - -- review-v3 T7: ensure returns nil for a nil session; do not index it. if not storage then return end local box, bId = current_box_data() diff --git a/src/ui/game3/easy_chat.lua b/src/ui/game3/easy_chat.lua index a5fc76b5..19dec5f8 100644 --- a/src/ui/game3/easy_chat.lua +++ b/src/ui/game3/easy_chat.lua @@ -50,8 +50,6 @@ EasyChat.FOOTER_BTNS = FOOTER_BTNS local function play_se(id) local ok, Aud = pcall(require, "src.core.game3.audio") - -- review-v3 G5: the API is playSe (audio.lua:574); the old playSE guard - -- was always nil, so Easy Chat never played any sound effect. if ok and Aud and Aud.playSe then Aud.playSe(id) end diff --git a/src/ui/game3/font.lua b/src/ui/game3/font.lua index 6d4ebf00..d236b5b5 100644 --- a/src/ui/game3/font.lua +++ b/src/ui/game3/font.lua @@ -1,19 +1,7 @@ --- Game3 font provider: resolves the active game's glyph implementation through --- the profile (profile.font.module) and forwards its public API, so shared --- code requires one seam instead of src.ui.game3.frlg_font directly. --- --- Design: docs/game3/rse-seams.md section 3.2 (T5.1). New file; nothing --- requires it yet, and FireRed's profile names frlg_font, so the resolution --- returns exactly that module and every forwarded value is identical. --- --- frlg_font.lua itself stays owned by the Refactor lane: this provider is the --- seam, the existing 48 call sites migrate later. - local Profile = require("src.core.game3.profile") local Font = {} --- Backs a game whose profile names no font module (FireRed today). Font.DEFAULT_MODULE = "src.ui.game3.frlg_font" local impls = {} @@ -32,8 +20,6 @@ local function log(msg) print("[game3/font] " .. tostring(msg)) end ---- Register a glyph implementation for a version id. This is the test seam --- and the hook a game with an extracted font bank registers through. function Font.register(versionId, impl) if type(versionId) ~= "string" or versionId == "" then return false end if type(impl) ~= "table" then return false end @@ -42,7 +28,6 @@ function Font.register(versionId, impl) return true end ---- Drop a registration; the profile's module answers again. function Font.unregister(versionId) if type(versionId) ~= "string" then return false end overrides[versionId] = nil @@ -50,7 +35,6 @@ function Font.unregister(versionId) return true end ---- The implementation table for a version id (nil or "" = active game). function Font.impl(versionId) local key = versionId if type(key) ~= "string" or key == "" then key = Profile.active().id end @@ -76,17 +60,11 @@ function Font.active() return Font.impl(nil) end ---- Test/tool hook: drop resolved implementations. Explicit registrations are --- boot-time API calls, not cache, so they survive. function Font.reset() impls = {} logged = false end --- Reads forward live: Font.draw resolves through the active implementation on --- every call, so invalidate()/ensure() state can never go stale behind a --- snapshot. The implementation functions carry no self (frlg_font.lua), so --- forwarding the raw function is exact. setmetatable(Font, { __index = function(_, key) local impl = Font.impl(nil) diff --git a/src/ui/game3/frlg_font.lua b/src/ui/game3/frlg_font.lua index df2fc0bd..3a3875ba 100644 --- a/src/ui/game3/frlg_font.lua +++ b/src/ui/game3/frlg_font.lua @@ -758,7 +758,6 @@ function FrlgFont.wrap(text, maxWidth, opts) return table.concat(outLines, "\n") end --- Reused opts tables for FrlgFont.advance (avoids one allocation per glyph). local ADVANCE_SMALL = { small = true } local ADVANCE_NORMAL = {} diff --git a/src/ui/game3/help_system.lua b/src/ui/game3/help_system.lua index bdd5bdee..7bf0b286 100644 --- a/src/ui/game3/help_system.lua +++ b/src/ui/game3/help_system.lua @@ -9,7 +9,6 @@ local Help = {open=false, seenIntro=false} local MENU_CONTEXT = {pokedex=4, party=5, bag=9, berry_pouch=9, tm_case=9, trainer=10, save=12, option=13, shop=17, pc_menu=27, box_storage=28} local HELD_KEYS = {'up','down','left','right'} --- Reused input shim for the held-direction repeat (no per-frame table/closure). local repeatInput, repeatKey, repeatOn local REPEAT_SHIM = {wasPressed=function(_,key) if not repeatInput then return false end diff --git a/src/ui/game3/help_window.lua b/src/ui/game3/help_window.lua index 4b45950e..d48778ca 100644 --- a/src/ui/game3/help_window.lua +++ b/src/ui/game3/help_window.lua @@ -1,40 +1,13 @@ --- Help MESSAGE window behind the `loadhelp` / `unloadhelp` seam --- (docs/game3/e10-opcode-spec.md sections 5.5/5.6). --- --- pret: DrawHelpMessageWindowWithText(text) (src/new_menu_helpers.c:701-705) --- creates the bottom-bar help window, prints, and returns; --- DestroyHelpMessageWindow_() (src/new_menu_helpers.c:707-710) tears it down. --- Both are idempotent (src/help_message.c:23-31 create, :35-51 destroy), the --- window template is {left=0, top=15, width=30, height=5, paletteNum=15} --- (src/help_message.c:13-19), and the same window carries the Start menu item --- descriptions (src/start_menu.c:332 draw, :440 destroy). This is NOT the L/R --- context browser in src/ui/game3/help_system.lua, and it is deliberately not --- capability-gated: pret's window is generic menu plumbing, while the --- `helpSystem` capability maps to the FireRed-only src/help_system.c. --- --- Text arrives as a plain string — the `loadhelp` word is resolved by the --- script handler with resolve_text (ops_a.lua:36-45), not here. --- --- New file, unwired: `loadhelp`/`unloadhelp` dispatch cases and the frame hook --- are the Finisher's handoff; the engine canvas is 30x20x8 (display.lua:9-11), --- so pret's bottom five tile rows map 1:1. +-- src/new_menu_helpers.c:701-705, src/new_menu_helpers.c:707-710, src/help_message.c:23-31, src/help_message.c:13-19, src/start_menu.c:332, src/help_system.c local Window = require("src.ui.game3.window") local HelpWindow = {} --- pret sHelpMessageWindowTemplate (src/help_message.c:13-19) +-- src/help_message.c:13-19 HelpWindow.OUTER = { left = 0, top = 15, width = 30, height = 5, paletteNum = 15 } --- Engine stdFrame draws the nine-slice *outside* the content rect (chrome.lua --- stdFrame fallback fills (tx*8-8, ty*8-8, (tw+2)*8, (th+2)*8)), so the content --- rect is pret's outer rect inset by one tile: the frame lands exactly on --- 0,15..30,20. Content is 28x3 tiles. HelpWindow.CONTENT = { left = 1, top = 16, width = 28, height = 3 } --- pret PrintHelpMessageText prints at x=2, y=5 inside its window --- (src/help_message.c:95-98). pret's own msg_window gfx supplies that window's --- border; the engine draws a std nine-slice instead, so the same pixel offsets --- are applied to the content rect (one tile in) rather than to the outer --- origin, which would put the text under the frame's left border tile. +-- src/help_message.c:95-98 HelpWindow.TEXT_OFFSET = { x = 2, y = 5 } local open = false @@ -46,15 +19,12 @@ local function contentTemplate() { paletteNum = HelpWindow.OUTER.paletteNum }) end ---- Show the help window (pret DrawHelpMessageWindowWithText). Re-showing with --- text replaces the content, matching pret's second PrintText call. function HelpWindow.show(value) text = type(value) == "string" and value or "" open = true return true end ---- Hide it (pret DestroyHelpMessageWindow_): safe with nothing open. function HelpWindow.close() open = false text = "" @@ -65,18 +35,14 @@ function HelpWindow.isOpen() return open end ---- The current text ("" when closed) — the seam's test surface. function HelpWindow.getText() return text end ---- Draw one frame. No-op when closed; the draw path calls it unconditionally. function HelpWindow.draw() if not open then return false end local tpl = contentTemplate() - -- pret FillWindowPixelBuffer before printing (src/help_message.c:41): clear - -- the interior with the window's background (white, as in the engine's other - -- fill user new_game_scene.lua:1640), then the std nine-slice on top. + -- src/help_message.c:41 Window.fill(tpl, 1, 1, 1, 1) Window.stdFrame(tpl) if text ~= "" then @@ -89,7 +55,6 @@ function HelpWindow.draw() return true end ---- Test/tool hook: close and forget everything. function HelpWindow.reset() HelpWindow.close() end diff --git a/src/ui/game3/hud.lua b/src/ui/game3/hud.lua index 087b7e6a..f5b052a0 100644 --- a/src/ui/game3/hud.lua +++ b/src/ui/game3/hud.lua @@ -13,8 +13,6 @@ local SaveMenu = require("src.ui.game3.save_menu") local TrainerCard = require("src.ui.game3.trainer_card") local PcMenu = require("src.ui.game3.pc_menu") --- review-v3 S9 (hud cites drifted from :183): hot-path pcalls that never --- logged; warn once per key so a permanently failing module cannot spam. local s9Warned = {} local function s9log(key, err) if s9Warned[key] then return end @@ -195,14 +193,12 @@ function Hud.update(game, _dt, inputTop) if inputTop == nil or top == inputTop then top.mod.handleInput(game and game.input) end end if top and top.mod and top.mod.update then - -- review-v3 S9 (hud.lua:189): log the swallowed top-layer update once. local okU, errU = pcall(top.mod.update, dt) if not okU then s9log("top.update", errU) end end -- Tick location map name popup banner local okPop, MapNamePopup = pcall(require, "src.ui.game3.map_name_popup") - -- review-v3 S9 (hud.lua:193): a require that throws retried every frame with no log. if not okPop then s9log("map_name_popup", MapNamePopup) end if okPop and MapNamePopup and MapNamePopup.update then MapNamePopup.update(dt) @@ -210,7 +206,6 @@ function Hud.update(game, _dt, inputTop) -- Tick location preview screen (map_preview_screen.c Task_RunMapPreviewScreenForest) local okPrev, MapPreviewScreen = pcall(require, "src.ui.game3.map_preview_screen") - -- review-v3 S9 (hud.lua:199). if not okPrev then s9log("map_preview_screen", MapPreviewScreen) end if okPrev and MapPreviewScreen and MapPreviewScreen.update then MapPreviewScreen.update(dt) @@ -350,7 +345,6 @@ function Hud.openStartMenu(game, session) if scene >= 1 then Flags.setFlag(store, nil, Flags.IDS.OPENED_START_MENU, true) if Space.persistSession then - -- review-v3 S9 (auditor drift: cited :183, live :338). local okP, errP = pcall(Space.persistSession) if not okP then s9log("persistSession", errP) end end diff --git a/src/ui/game3/map_preview_screen.lua b/src/ui/game3/map_preview_screen.lua index 099119c3..a5c03c88 100644 --- a/src/ui/game3/map_preview_screen.lua +++ b/src/ui/game3/map_preview_screen.lua @@ -154,8 +154,6 @@ function MapPreviewScreen.manifest() if MapPreviewScreen._manifest then return MapPreviewScreen._manifest end - -- review-v3 B5: no permanent tried-flag — a transient read failure must - -- retry on the next probe (this is read per menu open, not per frame). local rel = cache_root() .. "/" .. MapPreviewExtract.CACHE_SUB .. "/manifest.lua" local t = load_lua(rel) if type(t) ~= "table" or type(t.entries) ~= "table" then @@ -272,8 +270,6 @@ function MapPreviewScreen.show(mapsec, opts) MapPreviewScreen._mapsec = entry.mapsec MapPreviewScreen._entry = entry MapPreviewScreen._name = entry.name - -- W6: resolve the artwork once per show; draw() used to re-resolve the - -- entry (artworkFor -> entryFor) on every frame. MapPreviewScreen._image = image MapPreviewScreen._timer = 0 MapPreviewScreen._duration = duration @@ -349,8 +345,6 @@ local function nameWindowColors(manifest) } end --- W6: name-window colours are constant per manifest — memoize instead of --- rebuilding the colour tables on every frame. local nwManifestCache, nwColorsCache, nwFontColors = false, nil, nil local function nameWindowColorsCached() local m = MapPreviewScreen.manifest() diff --git a/src/ui/game3/new_game_scene.lua b/src/ui/game3/new_game_scene.lua index 839d51f9..58b357d3 100644 --- a/src/ui/game3/new_game_scene.lua +++ b/src/ui/game3/new_game_scene.lua @@ -18,7 +18,6 @@ Scene.__index = Scene Scene.GBA_HZ = 16777216 / 280896 --- Reused input proxy for Scene:update (avoids two closures + a table per GBA step). local proxyPressed, proxyInput local INPUT_PROXY = { wasPressed = function(_, k) return proxyPressed ~= nil and proxyPressed[k] == true end, diff --git a/src/ui/game3/pc_chrome.lua b/src/ui/game3/pc_chrome.lua index f3a22e3d..cb505a16 100644 --- a/src/ui/game3/pc_chrome.lua +++ b/src/ui/game3/pc_chrome.lua @@ -151,7 +151,6 @@ function PcChrome.drawBackground() end --- Draw animated waveforms beside PKMN DATA header --- W4: one Quad per waveform frame, cached instead of reallocated every draw. local function waveform_quad(img, frameIdx) local quads = PcChrome._waveformQuads if not quads then quads = {}; PcChrome._waveformQuads = quads end @@ -219,10 +218,6 @@ function PcChrome.drawLeftDataPanel(hoveredMon, hoverFrame) -- 1. Front Sprite in TV Screen (X: 10..73, Y: 19..80, W: 64, H: 61) -- pokefirered/src/pokemon_storage_system_data.c:1034, :1057 MON_DATA_SPECIES_OR_EGG - -- Merge fix: upstream inlined the species read into frontPic (egg-aware) while - -- our M8 dedup removed block 2's duplicate local — one shared `sp` above - -- satisfies both sides (speciesOrEgg == speciesOf for every non-egg, and the - -- stats card never prints spName for an egg). local sp = Pokemon.speciesOrEgg(hoveredMon) local sprite = Pokemon.frontPic(sp) if sprite and sprite.image then @@ -236,7 +231,6 @@ function PcChrome.drawLeftDataPanel(hoveredMon, hoverFrame) -- 2. Lower Stats Card Text & Info (X: 0..80, Y: 88..160) -- Matches pret FRLG PrintDisplayMonInfo (Window 0: left=0, top=11 / Y=88) - -- (reuses the `sp` resolved above; the duplicate local shadowed it) local spName = (sp and Pokemon.name(sp)) or "----" local nick = hoveredMon.nickname if not nick or nick == "" then diff --git a/src/ui/game3/pc_menu.lua b/src/ui/game3/pc_menu.lua index 559fed7e..a3666230 100644 --- a/src/ui/game3/pc_menu.lua +++ b/src/ui/game3/pc_menu.lua @@ -73,7 +73,6 @@ local function player_pc_name(session) return Strings("%s's PC", name) end --- G6: root rows are cached and rebuilt only when the labels' inputs change. function PcMenu._rootEntries() local who = someone_or_bill_name(PcMenu._session) local player = player_pc_name(PcMenu._session) diff --git a/src/ui/game3/pokedex.lua b/src/ui/game3/pokedex.lua index 41724d0f..5fe4c1dc 100644 --- a/src/ui/game3/pokedex.lua +++ b/src/ui/game3/pokedex.lua @@ -200,8 +200,6 @@ function Pokedex.show(dex, opts) if not Pokemon._names then Pokemon.install(nil) end Pokedex.MODES = build_modes(opts.session, Pokedex._dex) - -- review-v3 W5: full per-screen reset (data page, category grid, action - -- popup, timers, species) so a previous visit cannot leak state in. Pokedex.resetScreenState() if opts.mode then @@ -283,13 +281,11 @@ function Pokedex.showRegistration(speciesId, opts) play_cry(Pokedex._regSpecies) end --- review-v3 W5: every per-screen field resets on open AND close so stale --- state never leaks between visits (pret pokedex_screen.c keeps these --- per-screen too). +-- pokedex_screen.c function Pokedex.resetScreenState() Pokedex.screen = "mode_select" Pokedex.subScreenPrev = "mode_select" - Pokedex.modeCursor = 2 -- Start at NUMERICAL MODE (index 1 is header) + Pokedex.modeCursor = 2 Pokedex.modeScroll = 0 Pokedex.listCursor = 1 Pokedex.listScroll = 0 @@ -309,9 +305,6 @@ function Pokedex.resetScreenState() Pokedex._regSpecies = nil end --- review-v3 W10: the arrow-bob timer advances once per frame here (Hud --- pcall's top.mod.update at hud.lua:197-201); advancing inside the three --- draw fns tied animation to draw calls, which run 0..N times per frame. function Pokedex.update(_dt) PokedexChrome._animTimer = (PokedexChrome._animTimer or 0) + 0.05 end @@ -319,7 +312,6 @@ end function Pokedex.close() Pokedex.open = false Stack.pop("pokedex") - -- review-v3 W5: reset the full per-screen block on close too. Pokedex.resetScreenState() local cb = Pokedex._onClose Pokedex._onClose = nil diff --git a/src/ui/game3/quest_log.lua b/src/ui/game3/quest_log.lua index 87b9e18d..55f26ac4 100644 --- a/src/ui/game3/quest_log.lua +++ b/src/ui/game3/quest_log.lua @@ -2,11 +2,9 @@ local Q=require('src.core.game3.quest_log') local Font=require('src.ui.game3.frlg_font') local Strings=require('src.core.Strings') --- W11: resolve these once instead of re-requiring on every draw. local Ow, PokedexChrome local function owSprites() Ow=Ow or require('src.core.game3.ow_sprites');return Ow end local function pokedexChrome() PokedexChrome=PokedexChrome or require('src.ui.game3.pokedex_chrome');return PokedexChrome end --- Hoisted actor draw-order comparator (stable by y then id). local function actorOrder(a,b) return a.y Cancel Button -> Switch Button - -- (three targets; the switch leg only when the button is present). + -- pokefirered/src/region_map.c:2862 if input:wasPressed("start") then - RegionMap._snapIndex = (RegionMap._snapIndex + 1) % 3 - if RegionMap._snapIndex == 1 then - RegionMap.cursorX = CANCEL_BUTTON_X - RegionMap.cursorY = CANCEL_BUTTON_Y - elseif RegionMap._snapIndex == 2 and RegionMap.hasSwitchButton() then + local snap + if RegionMap.hasSwitchButton() then + RegionMap._snapIndex = (RegionMap._snapIndex + 1) % 3 + if RegionMap._snapIndex == 0 and not RegionMap.playerOnSelectedMap() then + RegionMap._snapIndex = 1 + end + snap = ({ "player", "switch", "cancel" })[RegionMap._snapIndex + 1] + else + RegionMap._snapIndex = (RegionMap._snapIndex + 1) % 2 + snap = RegionMap._snapIndex == 1 and "cancel" or "player" + end + if snap == "switch" then RegionMap.cursorX = SWITCH_BUTTON_X RegionMap.cursorY = SWITCH_BUTTON_Y + elseif snap == "cancel" then + RegionMap.cursorX = CANCEL_BUTTON_X + RegionMap.cursorY = CANCEL_BUTTON_Y else RegionMap.cursorX = RegionMap.playerX RegionMap.cursorY = RegionMap.playerY diff --git a/src/ui/game3/shop_menu.lua b/src/ui/game3/shop_menu.lua index 61503c50..aa435353 100644 --- a/src/ui/game3/shop_menu.lua +++ b/src/ui/game3/shop_menu.lua @@ -105,8 +105,6 @@ local function bag_sell_rows(bag) return rows end --- G7: stock/sell rows are rebuilt only when their inputs change (kind + source --- identity + generation); show()/commit_buy()/commit_sell() bump the generation. local rows_cache = { key = false, rows = nil } local function cached_rows(kind, src) local key = kind .. "|" .. tostring(src) .. "|" .. tostring(ShopMenu._rowsGen or 0) @@ -272,10 +270,7 @@ local function commit_buy() -- The Premier Ball Cap: Strictly 1 Premier Ball when purchasing >= 10 standard Poké Balls (ID 4) local premierBonus = 0 if ItemsData.toNumericId(p.id) == 4 and ShopMenu.qty >= 10 then - -- review-v3 G8: Bag.add can refuse (pocket capacity); grant the bonus - -- only on success and log the miss (W2: the canAdd check at :239 covers - -- only the purchased balls). - if Bag.add(bag, 12, 1) then -- PREMIER_BALL = 12 + if Bag.add(bag, 12, 1) then premierBonus = 1 else print("[shop] premier ball bonus not granted (no room)") @@ -299,8 +294,6 @@ local function commit_sell() if not p or not session or not session.bag then return end ShopMenu._rowsGen = (ShopMenu._rowsGen or 0) + 1 local earn = (p.price or 0) * ShopMenu.qty - -- review-v3 G9: pay out only when the remove actually happened - -- (Bag.remove returns false when the slot or quantity is missing). if not Bag.remove(session.bag, p.id, ShopMenu.qty) then ShopMenu._status = Strings("The trade fell through — nothing sold.") ShopMenu.mode = "sell_msg" diff --git a/src/ui/game3/start_menu.lua b/src/ui/game3/start_menu.lua index 147853dd..fe35db8a 100644 --- a/src/ui/game3/start_menu.lua +++ b/src/ui/game3/start_menu.lua @@ -34,8 +34,7 @@ local function build_entries(session) if hasDex then entries[#entries + 1] = { id = "pokedex", label = "POKéDEX" } end - -- Retail also gates POKéMON on FLAG_SYS_POKEMON_GET (SYS_FLAGS+0x28 = 0x828): - -- the player must have received their first Pokémon (start_menu.c:217-218). + -- start_menu.c:217-218 local hasMon = true if store and Flags and Flags.getFlag then hasMon = Flags.getFlag(store, nil, Flags.IDS and Flags.IDS.SYS_POKEMON_GET or 0x828) == true diff --git a/src/ui/game3/summary_menu.lua b/src/ui/game3/summary_menu.lua index 67e0b9de..b6a580cf 100644 --- a/src/ui/game3/summary_menu.lua +++ b/src/ui/game3/summary_menu.lua @@ -10,7 +10,7 @@ local FrlgFont = require("src.ui.game3.frlg_font") local Pokemon = require("src.core.game3.pokemon") local Dex = require("src.core.game3.dex") local PokedexData = require("src.core.game3.pokedex_data") -local PokedexChrome = require("src.ui.game3.pokedex_chrome") -- W2: hoisted out of draw +local PokedexChrome = require("src.ui.game3.pokedex_chrome") local SummaryChrome = require("src.ui.game3.summary_chrome") local SummaryData = require("src.core.game3.summary_data") local Strings = require("src.core.Strings") @@ -455,13 +455,13 @@ local function draw_header(mon) end if SummaryData.isShiny(mon) then - local sx, sy = 8, isMovesPage and 24 or 40 -- review-v3 W9: x was a no-op ternary + local sx, sy = 8, isMovesPage and 24 or 40 SummaryChrome.drawShinyStar(sx, sy) end local ailment = SummaryData.statusAilment(mon) if ailment > 0 then - local ax, ay = 16, isMovesPage and 44 or 38 -- review-v3 W9: x was a no-op ternary + local ax, ay = 16, isMovesPage and 44 or 38 SummaryChrome.drawStatusIcon(ax, ay, ailment) end diff --git a/src/ui/game3/tm_case_chrome.lua b/src/ui/game3/tm_case_chrome.lua index c7c02c78..7d316088 100644 --- a/src/ui/game3/tm_case_chrome.lua +++ b/src/ui/game3/tm_case_chrome.lua @@ -91,9 +91,6 @@ local function rgba_to_image(rgba, w, h) end function TmCaseChrome.ready() - -- review-v3 G3: latch the probe result; tm_case.lua:250 calls this every - -- draw and the old `_bgMale` latch never engaged on female saves (loadBg - -- sets only _bgFemale), so female saves re-read bg_male.rgba forever. if TmCaseChrome._ready ~= nil then return TmCaseChrome._ready end local d = read_bytes(tm_root() .. "/bg_male.rgba") TmCaseChrome._ready = d ~= nil and #d > 0 diff --git a/src/ui/game3/trainer_card.lua b/src/ui/game3/trainer_card.lua index 2c532a5e..57e8206b 100644 --- a/src/ui/game3/trainer_card.lua +++ b/src/ui/game3/trainer_card.lua @@ -209,10 +209,7 @@ local function screen_image(stars, female) return img end --- J3: badge flags/names come from the active game profile (FRLG row cites --- pokefirered/include/constants/flags.h:1364-1371, FLAG_BADGE01_GET = 0x820; --- RSE bases documented in profiles/firered.lua). Falls back to the FRLG --- literals, so FireRed behaviour is byte-identical. +-- pokefirered/include/constants/flags.h:1364-1371 local BADGE_FLAGS = { 0x820, 0x821, 0x822, 0x823, 0x824, 0x825, 0x826, 0x827 } local BADGE_NAMES = { "BOULDER", "CASCADE", "THUNDER", "RAINBOW", "SOUL", "MARSH", "VOLCANO", "EARTH" } do @@ -231,7 +228,7 @@ end local FLAG_SYS_POKEDEX_GET = 0x829 local FLAG_SYS_NATIONAL_DEX = 0x840 --- src/trainer_card.c:899 VarGet(VAR_TRAINER_CARD_MON_ICON_TINT_IDX) +-- src/trainer_card.c:899 local VAR_TRAINER_CARD_MON_ICON_TINT_IDX = 0x4042 local VAR_TRAINER_CARD_MON_ICON_1 = 0x4043 local VAR_HOF_BRAG_STATE = 0x4049 @@ -343,8 +340,6 @@ local function get_var(session, varId) if type(store.vars) == "table" then return tonumber(store.vars[varId]) or 0 end end if session and type(session.vars) == "table" then - -- persist_sidecar/Flags.serialize write tostring(id) keys into session.vars; - -- a numeric miss must fall back to the string key or every var reads 0. local v = session.vars[varId] if v == nil then v = session.vars[tostring(varId)] end return tonumber(v) or 0 @@ -383,7 +378,7 @@ end local function caught_mons_count(session, national) local dex = session and session.dex - if not dex then return 0 end -- review-v3 V5: caughtMonsCount is never written + if not dex then return 0 end local okD, Dex = pcall(require, "src.core.game3.dex") if okD and Dex and Dex.countCaught then local ok, n = pcall(Dex.countCaught, dex, national and "national" or "kanto") @@ -445,7 +440,7 @@ local function gather(session) if berries >= 200 and jumps >= 200 then stars = stars + 1 end c.stars = math.min(4, stars) - -- src/trainer_card.c:899 trainerCard->monIconTint = VarGet(VAR_TRAINER_CARD_MON_ICON_TINT_IDX) + -- src/trainer_card.c:899 c.monIconTint = get_var(session, VAR_TRAINER_CARD_MON_ICON_TINT_IDX) c.monSpecies = {} @@ -519,8 +514,6 @@ function TrainerCard.update(dt) end end --- W3: the text model is rebuilt only when the card snapshot identity or the --- blinking colon phase changes (it used to be rebuilt every frame in draw). local texts_cache = { c = false, colon = false, front = nil, back = nil } local function front_texts_cached(c, colonInvisible) if texts_cache.front and texts_cache.c == c and texts_cache.colon == colonInvisible then @@ -544,7 +537,6 @@ function TrainerCard.show(opts) TrainerCard._flip = nil TrainerCard._session = opts.session TrainerCard._card = gather(opts.session) - -- W3: establish the fresh cache with the new snapshot (row: cache in show/beginFlip). texts_cache.c, texts_cache.colon, texts_cache.front, texts_cache.back = false, false, nil, nil TrainerCard._onClose = opts.onClose ensureAssets() @@ -708,14 +700,12 @@ local function draw_front(c) end end --- src/trainer_card.c:1411 LoadMonIconGfx — tint the party-snapshot icons per --- trainerCard->monIconTint. include/constants/trainer_card.h tint indices: -local MON_ICON_TINT_BLACK = 1 -- TintPalette_CustomTone(pals, 96, 0, 0, 0) -local MON_ICON_TINT_PINK = 2 -- TintPalette_CustomTone(pals, 96, 500, 330, 310) -local MON_ICON_TINT_SEPIA = 3 -- TintPalette_SepiaTone(pals, 96) +-- src/trainer_card.c:1411, include/constants/trainer_card.h +local MON_ICON_TINT_BLACK = 1 +local MON_ICON_TINT_PINK = 2 +local MON_ICON_TINT_SEPIA = 3 --- pokefirered/src/palette.c:832/:852 math adapted to 8-bit pixels: same gray --- weights, tone/256, truncated like the C >>8, clamped to 255, alpha kept. +-- pokefirered/src/palette.c:832 local function tint_pixel(tint, r, g, b) local gray = 0.3 * r + 0.59 * g + 0.1133 * b local nr, ng, nb @@ -723,7 +713,7 @@ local function tint_pixel(tint, r, g, b) nr, ng, nb = 0, 0, 0 elseif tint == MON_ICON_TINT_PINK then nr, ng, nb = 500 * gray / 256, 330 * gray / 256, 310 * gray / 256 - else -- MON_ICON_TINT_SEPIA + else nr, ng, nb = 1.2 * gray, gray, 0.94 * gray end if nr > 255 then nr = 255 end @@ -732,14 +722,11 @@ local function tint_pixel(tint, r, g, b) return math.floor(nr), math.floor(ng), math.floor(nb) end --- Lazily built cache: [tint][species] = icon-shaped entry ({image, quads, ...}), --- or false when the build failed so we never retry; tint 0/nil/out-of-range and --- failures return the original icon untouched. local _tintedIcons = {} local function tinted_icon(icon, species, tint) if not icon or not icon.image or not tint then return icon end - tint = math.floor(tint) -- vars are u16 in C; truncate like the (u16) switch + tint = math.floor(tint) if tint < MON_ICON_TINT_BLACK or tint > MON_ICON_TINT_SEPIA then return icon end local byTint = _tintedIcons[tint] if not byTint then @@ -780,7 +767,7 @@ local function tinted_icon(icon, species, tint) h = icon.h, sheetH = icon.sheetH, frames = icon.frames, - quads = icon.quads, -- same sheet dimensions, quads carry over + quads = icon.quads, } return byTint[species] end diff --git a/src/ui/game3/trainer_tower_records.lua b/src/ui/game3/trainer_tower_records.lua index f5ee710b..3e12364d 100644 --- a/src/ui/game3/trainer_tower_records.lua +++ b/src/ui/game3/trainer_tower_records.lua @@ -376,11 +376,7 @@ end -- pokefirered/src/battle_records.c:452 PrintTotalRecord function Records.totalText(session) session = session or Records._session - -- review-v3 H3: the live writer is link/battle.lua bumpGameStat into - -- session.gameStats (string keys, battle_records.c:355 UpdateLinkBattle- - -- GameStats); pret reads GetGameStat(GAME_STAT_LINK_BATTLE_WINS/LOSSES/ - -- DRAWS = 23/24/25, include/constants/game_stat.h:27-29). The old - -- session.linkBattleWins fields are kept as a legacy fallback. + -- battle_records.c:355, include/constants/game_stat.h:27-29 local gs = type(session) == "table" and session.gameStats or {} local wins = record_number(gs[23] or gs.linkBattleWins or (type(session) == "table" and session.linkBattleWins)) diff --git a/src/ui/game3/ui_pass.lua b/src/ui/game3/ui_pass.lua index 560f845b..6570be09 100644 --- a/src/ui/game3/ui_pass.lua +++ b/src/ui/game3/ui_pass.lua @@ -1,9 +1,3 @@ --- I1: the Gen 3 UI router, split out of core/game3/gfx (drawing primitives --- stay there). This module owns drawUi dispatch and must NOT require --- core/game3.display — that require was the gfx -> display half of the --- display <-> gfx load cycle (I3); display binds to this pass through --- Display.setUiRenderer (or lazily requires it on first draw). - local Stack = require("src.ui.game3.stack") local UiPass = {} @@ -15,7 +9,6 @@ end local gfxDrawWarned = {} local function tryDraw(mod) if mod and mod.draw then - -- review-v3 S9 (gfx.lua:64): a throwing mod.draw logged nothing ever. local ok, err = pcall(mod.draw) if not ok and not gfxDrawWarned[tostring(mod)] then gfxDrawWarned[tostring(mod)] = true @@ -24,7 +17,6 @@ local function tryDraw(mod) end end ---- Draw all active game3 UI widgets onto the current Display canvas. function UiPass.drawUi() local Message = require("src.ui.game3.message") local Choice = require("src.ui.game3.choice") @@ -41,14 +33,12 @@ function UiPass.drawUi() local CoinsBox = require("src.ui.game3.coins_box") local ElevatorWindow = require("src.ui.game3.elevator_window") - -- Stack-driven full-screen menus (bottom → top). local order = Stack.drawOrder() if #order > 0 then for _, layer in ipairs(order) do tryDraw(layer.mod) end else - -- Legacy open flags if stack not used yet. if StartMenu.isOpen() then tryDraw(StartMenu) end if BagMenu.isOpen() then tryDraw(BagMenu) end if RegionMap.isOpen() then tryDraw(RegionMap) end @@ -60,7 +50,6 @@ function UiPass.drawUi() if PcMenu.isOpen() then tryDraw(PcMenu) end end - -- Field moneybox sits above the map but under dialogue when script-owned. if MoneyBox.isVisible and MoneyBox.isVisible() then local ShopMenu = package.loaded["src.ui.game3.shop_menu"] if not (ShopMenu and ShopMenu.isOpen and ShopMenu.isOpen()) then @@ -78,8 +67,7 @@ function UiPass.drawUi() tryDraw(ElevatorWindow) end - -- Location change overlay / signpost popup banner (pokefirered/src/map_name_popup.c) - -- A running FOREST preview screen owns BG0, so it replaces the popup entirely. + -- pokefirered/src/map_name_popup.c local okPrev, MapPreviewScreen = pcall(require, "src.ui.game3.map_preview_screen") local previewActive = okPrev and MapPreviewScreen and MapPreviewScreen.isActive and MapPreviewScreen.isActive() @@ -103,13 +91,11 @@ function UiPass.drawUi() end end - -- Script mon pic (showmonpic) under dialogue / yes-no. local okPic, MonPic = pcall(require, "src.ui.game3.mon_pic") if okPic and MonPic and MonPic.active then tryDraw(MonPic) end - -- Dialog then choice on top (yesnobox overlays stayed message). local top = Stack.top() local suppressOverworldDialog = top and top.hideBelow diff --git a/src/world/WorldAPI.lua b/src/world/WorldAPI.lua index 41e9a1dd..f5281287 100644 --- a/src/world/WorldAPI.lua +++ b/src/world/WorldAPI.lua @@ -423,8 +423,6 @@ function WorldAPI:effectiveEncounters(mapId, terrain, opts) return nil, "invalid terrain: " .. tostring(terrain) end local data = self.game and self.game.data - -- review-v3 B10: a not-loaded encounter table is a failure to propagate, - -- not an empty pool — reporting 0-rate here silently zeroes encounter odds. if not (data and type(data.encounters) == "table") then return nil, "encounters data not loaded" end diff --git a/src/world/game3/WorldAPI.lua b/src/world/game3/WorldAPI.lua index 5f7af9b1..aca04903 100644 --- a/src/world/game3/WorldAPI.lua +++ b/src/world/game3/WorldAPI.lua @@ -177,8 +177,6 @@ local function moveResult(move) if not mon then return nil end local ok, res = pcall(FM.fromMenu, move, fieldContext(mon)) if ok and res and res.ok then return res, mon end - -- review-v3 S10: surface the swallowed host error instead of reading it as - -- "the mod offered nothing". if not ok then print("[game3/world] field move query failed: " .. tostring(res)) end return nil end diff --git a/src/world/gen2/Permissions.lua b/src/world/gen2/Permissions.lua index a58f2ad2..b50da6d2 100644 --- a/src/world/gen2/Permissions.lua +++ b/src/world/gen2/Permissions.lua @@ -1,9 +1,3 @@ --- Gen 2 COLL_* → permission. The byte vocabulary (permission table, LAND / --- WATER / WALL, of/isLand/isWater/isWall/isWalkable, isLedge) lives in --- src/core/CollPermissions.lua and is re-exported here unchanged: the Gen 3 --- import path classifies the same bytes and src/import must not reach into --- src/world (review-v3 I9). Every Permissions.* caller keeps working. - local GameVersion = require("src.core.GameVersion") local CollPermissions = require("src.core.CollPermissions") diff --git a/tests/engine/game3_cache_paths_test.lua b/tests/engine/game3_cache_paths_test.lua index 44515691..c116b55a 100644 --- a/tests/engine/game3_cache_paths_test.lua +++ b/tests/engine/game3_cache_paths_test.lua @@ -1,8 +1,3 @@ --- T6.3a: the shared GBA cache-root module. New file, unwired; this suite --- pins its contract and keeps it honest with extract_island1.lua until the --- wiring handoff lands. --- luajit tests/engine/game3_cache_paths_test.lua - package.path = "./?.lua;./?/init.lua;" .. package.path local T = require("tests.harness") @@ -10,15 +5,11 @@ local check, eq = T.check, T.eq local CachePaths = require("src.core.game3.cache_paths") --- ------------------------------------------------------------- defaults - eq(CachePaths.CACHE_ROOT, "data/generated/gba", "the packaged cache root") eq(CachePaths.NATIVE_ROOT, "data/generated/gba/native", "native root is derived") eq(CachePaths.NATIVE_ROOT, CachePaths.CACHE_ROOT .. "/native", "NATIVE_ROOT is CACHE_ROOT/native") --- ------------------------------------------------------------ setRoot - check(CachePaths.setRoot("data/generated/gba-rse") == true, "setRoot accepts a root") eq(CachePaths.CACHE_ROOT, "data/generated/gba-rse", "setRoot writes CACHE_ROOT") eq(CachePaths.NATIVE_ROOT, "data/generated/gba-rse/native", "setRoot derives NATIVE_ROOT") @@ -30,11 +21,6 @@ CachePaths.reset() eq(CachePaths.CACHE_ROOT, "data/generated/gba", "reset restores the packaged root") eq(CachePaths.NATIVE_ROOT, "data/generated/gba/native", "reset restores the native root") --- ------------------------------------------- consistency with the extractor - --- Until the T6.3 wiring lands, extract_island1.lua pins the same literals. --- Both states pass so the handoff can replace the pin with a CachePaths read --- without editing this suite. do local f = io.open("src/import/gba/extract_island1.lua", "r") check(f ~= nil, "extract_island1.lua is readable") @@ -48,8 +34,6 @@ do end end --- The defaults must match what the extractor declares, so the handoff is a --- move, not a behaviour change. do local f = io.open("src/import/gba/extract_island1.lua", "r") if f then diff --git a/tests/engine/game3_capabilities_test.lua b/tests/engine/game3_capabilities_test.lua index 56a81788..10af0deb 100644 --- a/tests/engine/game3_capabilities_test.lua +++ b/tests/engine/game3_capabilities_test.lua @@ -1,8 +1,3 @@ --- T3.1/T3.2: the capability registry. Pins the legal flag names, the composed --- per-game sets, the FireRed row's conformance, and the feature -> module map --- (including static file existence for every module a gate will touch). --- luajit tests/engine/game3_capabilities_test.lua - package.path = "./?.lua;./?/init.lua;" .. package.path local T = require("tests.harness") @@ -14,8 +9,6 @@ local FireredProfile = require("src.core.game3.profiles.firered") local prevVersion = GameVersion.get() --- ------------------------------------------------------------ registry shape - for name, value in pairs(Capabilities.NAMES) do check(value == true, "NAMES." .. name .. " is a legal flag") end @@ -38,8 +31,6 @@ for key in pairs(Capabilities.FRLG) do "FRLG-only flag " .. key .. " is not shared with RSE") end --- ---------------------------------------------------- FireRed row conformance - GameVersion.set("firered") local row = FireredProfile local okAudit, problems = Capabilities.audit(row.capabilities) @@ -53,8 +44,6 @@ for _, rseOnly in ipairs({ "contests", "secretBase", "matchCall", "pokeNav", "ba check(row.capabilities[rseOnly] == nil, "FireRed row leaves RSE flag " .. rseOnly .. " unset") end --- -------------------------------------------------------------- feature map - local function fileExists(rel) local f = io.open(rel, "r") if f then f:close() return true end @@ -85,14 +74,12 @@ for id, feature in pairs(Capabilities.FEATURES) do end end --- The five features the lead named must be present and FireRed-only. for _, id in ipairs({ "fame_checker", "teachy_tv", "vs_seeker", "trainer_tower", "seagallop" }) do check(Capabilities.FEATURES[id] ~= nil, "feature registered: " .. id) check(Capabilities.FRLG[Capabilities.FEATURES[id].cap] == true, "feature is in the FRLG set: " .. id) end --- pret grounding, when the cited repo is cloned locally (../). local repoCloned = {} do for _, feature in pairs(Capabilities.FEATURES) do @@ -108,8 +95,6 @@ do end end --- --------------------------------------------------------------- semantics - eq(Capabilities.has({ version = "firered" }, "fameChecker"), true, "FireRed has the Fame Checker") eq(Capabilities.has({ version = "firered" }, "contests"), false, @@ -123,9 +108,6 @@ eq(Capabilities.gate({ version = "firered" }, "tm_case"), true, eq(Capabilities.gate({ version = "firered" }, "contests"), false, "an unknown feature id reads false (and warns once)") --- Until profiles/ruby.lua exists, resolution fails closed to FireRed, so an --- RSE session currently reads FireRed's flags. Update this assertion when the --- RSE profile lands (it should flip to false alongside other gates). eq(Capabilities.gate({ version = "ruby" }, "fame_checker"), true, "an RSE id without a profile fails closed to FireRed") @@ -140,8 +122,6 @@ eq(Capabilities.enabled(Capabilities.RSE, "contests"), true, eq(Capabilities.enabled(nil, "fame_checker"), false, "nil caps is false") eq(Capabilities.enabled({}, "no_such_feature"), false, "an unknown feature is false") --- ------------------------------------------------- natives registry gating - eq(Capabilities.nativeFeature("natives_fame"), "fame_checker", "the Fame Checker natives module maps to its feature") eq(Capabilities.nativeFeature("natives_tower"), "trainer_tower", @@ -157,8 +137,6 @@ eq(Capabilities.nativeAllowed({ version = "firered" }, "natives_bogus"), true, eq(Capabilities.nativeAllowed({ version = "firered" }, nil), true, "a nil module name is shared") --- --------------------------------------------------------------- audit - local okBad, badProblems = Capabilities.audit({ bogusFlag = true }) check(okBad == false, "audit rejects an unknown flag") check(#badProblems == 1 and badProblems[1]:find("bogusFlag", 1, true) ~= nil, diff --git a/tests/engine/game3_coll_permissions_test.lua b/tests/engine/game3_coll_permissions_test.lua index 0507fda8..744ceb9b 100644 --- a/tests/engine/game3_coll_permissions_test.lua +++ b/tests/engine/game3_coll_permissions_test.lua @@ -1,11 +1,3 @@ --- I9 import/world seam (review-v3 row I9): native_pack classified COLL bytes --- through src/world/gen2/Permissions, i.e. an import module reaching into the --- world layer. The byte vocabulary now lives in src/core/CollPermissions.lua --- and Permissions re-exports it. This suite proves (1) the edge is gone, (2) the --- two modules answer identically for every byte, and (3) the import path --- NativePack.resolveLayoutColl behaves exactly as before on all four branches. --- luajit tests/engine/game3_coll_permissions_test.lua - package.path = "./?.lua;./?/init.lua;" .. package.path local T = require("tests.harness") @@ -23,8 +15,6 @@ local function sourceOf(rel) return s end --- ------------------------------------------------------- the boundary itself - do local pack = sourceOf("src/import/gba/native_pack.lua") check(pack ~= nil, "native_pack.lua is readable") @@ -46,8 +36,6 @@ do "the permission table no longer lives in the gen2 world module") end --- ------------------------------------------- equivalence (byte-for-byte proof) - eq(Coll.LAND, Permissions.LAND, "LAND matches") eq(Coll.WATER, Permissions.WATER, "WATER matches") eq(Coll.WALL, Permissions.WALL, "WALL matches") @@ -69,14 +57,11 @@ for _, c in ipairs(cases) do if Coll.isWater(c) ~= Permissions.isWater(c) then mismatches = mismatches + 1 end if Coll.isWall(c) ~= Permissions.isWall(c) then mismatches = mismatches + 1 end end --- nil never reaches ipairs, so compare it explicitly. if Coll.of(nil) ~= Permissions.of(nil) then mismatches = mismatches + 1 end if Coll.isWalkable(nil) ~= Permissions.isWalkable(nil) then mismatches = mismatches + 1 end if Coll.isLedge(nil) ~= Permissions.isLedge(nil) then mismatches = mismatches + 1 end eq(mismatches, 0, "CollPermissions and Permissions agree on every tested byte") --- ----------------------------------------- spot semantics from the moved table - eq(Coll.of(0), Coll.LAND, "coll 0 is LAND") check(Coll.isWalkable(0) == true, "coll 0 is walkable") eq(Coll.of(0x07), Coll.WALL, "coll 0x07 is WALL (row 1, index 8)") @@ -91,14 +76,11 @@ eq(Coll.of(nil), Coll.WALL, "nil reads as WALL") eq(Coll.of(-1), Coll.WALL, "negative reads as WALL") check(Coll.isLedge(nil) == false, "nil is not a ledge") --- The gen2-specific half of Permissions still works off the re-exported of(). eq(Permissions.surfable(0x20), "water", "surfable still answers water (Permissions.of re-export)") eq(Permissions.surfable(0), "land", "surfable still answers land") check(type(Permissions.ledgeFacings(0xa0)) == "table", "ledgeFacings still resolves (LEDGE_FACINGS + isLedge)") --- --------------------------------- the import path, all four branches, unchanged - local Seed = require("src.core.game3.scripting.collision") local seeded = Seed.seed("BLOCKED") diff --git a/tests/engine/game3_font_provider_test.lua b/tests/engine/game3_font_provider_test.lua index e4e5a9ad..fa6a687b 100644 --- a/tests/engine/game3_font_provider_test.lua +++ b/tests/engine/game3_font_provider_test.lua @@ -1,12 +1,3 @@ --- T5.1: the Game3 font provider resolves the active game's implementation --- through the profile and forwards its API, so a Ruby/Sapphire/Emerald font --- bank is a profile row plus (optionally) a registration, never a fork of --- frlg_font.lua. --- --- FireRed's profile names src.ui.game3.frlg_font, so every forwarded value --- must be identical to requiring that module directly. --- luajit tests/engine/game3_font_provider_test.lua - package.path = "./?.lua;./?/init.lua;" .. package.path local T = require("tests.harness") @@ -19,8 +10,6 @@ local FrlgFont = require("src.ui.game3.frlg_font") local prevVersion = GameVersion.get() --- --------------------------------------------------- FireRed resolution - GameVersion.set("firered") Profile.reset() Font.reset() @@ -30,7 +19,6 @@ check(Font.impl(nil) == FrlgFont, "nil resolves the active game's implementation check(Font.active() == FrlgFont, "active() is the active game's implementation") check(Font.impl("ruby") == FrlgFont, "an overlay-less RSE id falls back to FireRed's font") --- The provider forwards, live and by identity. eq(Font.CELL, FrlgFont.CELL, "CELL forwards") eq(Font.GLYPH_HEIGHT, FrlgFont.GLYPH_HEIGHT, "GLYPH_HEIGHT forwards") eq(Font.LINE_PITCH, FrlgFont.LINE_PITCH, "LINE_PITCH forwards") @@ -46,8 +34,6 @@ check(Font.invalidate == FrlgFont.invalidate, "invalidate forwards") check(Font.countChars == FrlgFont.countChars, "countChars forwards") eq(Font.MAX_LETTER_WIDTH, FrlgFont.MAX_LETTER_WIDTH, "MAX_LETTER_WIDTH forwards") --- Forwarding reads the implementation now, not a load-time snapshot: a --- registration for the ACTIVE game is visible immediately. local replacement = { CELL = 99, measure = function() return 42 end, @@ -58,26 +44,19 @@ check(Font.register("testgame", 7) == false, "register rejects a non-table") check(Font.impl("testgame") == replacement, "a registration wins over the profile module") eq(Font.impl("testgame").CELL, 99, "a registered table answers directly") --- Active forwarding follows a registration for the game that is running. check(Font.register("firered", replacement) == true, "register the active game") eq(Font.CELL, 99, "a forwarded constant follows the active registration") check(Font.measure == replacement.measure, "a forwarded function follows the registration") --- reset() drops resolutions and keeps registrations. Font.reset() check(Font.impl("firered") == replacement, "registrations survive reset") check(Font.impl("ruby") == FrlgFont, "the FireRed fallback still answers for an overlay-less id") --- unregister restores the profile's module. check(Font.unregister("firered") == true, "unregister returns true") Font.reset() check(Font.impl("firered") == FrlgFont, "unregister falls back to the profile's module") check(Font.unregister("testgame") == true, "clean up the fixture registration") --- ------------------------------------------------ lazy, profile-driven load - --- The provider must not require an implementation until asked, and it must --- load the module the profile names. local saved = package.loaded["src.ui.game3.frlg_font"] package.loaded["src.ui.game3.frlg_font"] = nil Font.reset() @@ -87,8 +66,6 @@ check(package.loaded["src.ui.game3.frlg_font"] == loaded, "the loaded module is the one frlg_font's path names") package.loaded["src.ui.game3.frlg_font"] = saved or loaded --- A key the implementation does not carry reads as nil instead of raising, so --- a headless tool can probe the provider without a graphics context. check(Font.NO_SUCH_KEY == nil, "an unknown forwarded key reads as nil") GameVersion.set(prevVersion) diff --git a/tests/engine/game3_gotonative_test.lua b/tests/engine/game3_gotonative_test.lua index ad977d00..b453721e 100644 --- a/tests/engine/game3_gotonative_test.lua +++ b/tests/engine/game3_gotonative_test.lua @@ -1,10 +1,4 @@ --- rse-seams e10 spec 5.2: gotonative resolves a symbol instead of jumping to a --- ROM C function pointer. --- pret src/scrcmd.c:92-97 (SetupNativeScript(ctx, ScriptReadWord)) --- pret src/script.c:70 (the pointer is a bool8 (*)(void)) --- lua: luajit tests/engine/game3_gotonative_test.lua --- The registry (Natives.NATIVE_SYMBOLS) stays empty until the extractor emits --- addr -> symbol, so today every address skips with ONE log. +-- src/scrcmd.c:92-97, src/script.c:70 package.path = "./?.lua;./?/init.lua;" .. package.path @@ -28,20 +22,18 @@ local function vm() } end --- 1. Unmapped address: skip + exactly one log (the empty registry case). local v1 = vm() eq(Ops.dispatch(v1, { op = "gotonative", [1] = 0x1234 }), false, "unmapped gotonative skips") eq(#logs, 1, "the miss is logged once") Ops.dispatch(v1, { op = "gotonative", [1] = 0x1234 }) eq(#logs, 1, "a repeated miss does not spam the log") --- 2. Registered symbol: the handler is invoked and its yield propagates. local calls = 0 Natives.NATIVE_SYMBOLS[0x100] = "test_native" local saved = Natives.ALLOW["native:test_native"] Natives.ALLOW["native:test_native"] = function(ctx) calls = calls + 1 - return true -- handler yield contract (mirrors Natives.callnative) + return true end local v2 = vm() eq(Ops.dispatch(v2, { op = "gotonative", [1] = 0x100 }), true, @@ -50,7 +42,6 @@ eq(calls, 1, "the registered handler ran exactly once") Natives.ALLOW["native:test_native"] = saved Natives.NATIVE_SYMBOLS[0x100] = nil --- 3. Direct registrations keep working (callnative's key form). local calls2 = 0 Natives.ALLOW["native:0x77"] = nil local saved2 = Natives.ALLOW["native:55"] diff --git a/tests/engine/game3_help_window_opcode_test.lua b/tests/engine/game3_help_window_opcode_test.lua index 13a6e83a..71d25dd8 100644 --- a/tests/engine/game3_help_window_opcode_test.lua +++ b/tests/engine/game3_help_window_opcode_test.lua @@ -1,10 +1,4 @@ --- rse-seams e10 spec 5.5/5.6: loadhelp/unloadhelp drive a dedicated help --- MESSAGE window (not the L/R Help browser). --- pret src/scrcmd.c:1274-1280 (loadhelp: ScriptReadWord, fallback ctx->data[0]) --- pret src/scrcmd.c:1285-1289 (unloadhelp) --- pret src/new_menu_helpers.c:701-705 (DrawHelpMessageWindowWithText) --- pret src/new_menu_helpers.c:707-710 (DestroyHelpMessageWindow_ — safe when closed) --- lua: luajit tests/engine/game3_help_window_opcode_test.lua +-- src/scrcmd.c:1274-1280, src/scrcmd.c:1285-1289, src/new_menu_helpers.c:701-705, src/new_menu_helpers.c:707-710 package.path = "./?.lua;./?/init.lua;" .. package.path @@ -29,8 +23,6 @@ local function vm(texts) adapters = { log = function() end }, setPc = function() end, texts = texts or {}, - -- getText hands the dispatcher an IR table like the real Vm does - -- (Vm:getText returns the bundle's parsed text, ops_a then runs toPlain). getText = function(self, key) local s = self.texts[key] return s and TextIR.fromAscii(s) @@ -38,22 +30,17 @@ local function vm(texts) } end --- 1. loadhelp opens the window with the resolved string. HelpWindow.close() local v = vm({ [Opcodes.key(0xABC)] = "Some HELP text." }) eq(Ops.dispatch(v, { op = "loadhelp", [1] = 0xABC }), false, "loadhelp does not yield") eq(HelpWindow.isOpen(), true, "the help message window is open") eq(HelpWindow.getText(), "Some HELP text.", "the resolved text reached the window") --- 2. unloadhelp closes it; closing again with nothing open is a no-op --- (pret DestroyHelpMessageWindow_ is safe either way). eq(Ops.dispatch(v, { op = "unloadhelp" }), false, "unloadhelp does not yield") eq(HelpWindow.isOpen(), false, "the window closed") eq(Ops.dispatch(v, { op = "unloadhelp" }), false, "unloadhelp with no window is a no-op") eq(HelpWindow.isOpen(), false, "still closed") --- 3. resolve_text fallback: pointer 0/nil falls back to ctx.data[0] and an --- unresolved pointer opens the window without raising. v.ctx.data = v.ctx.data or {} v.ctx.data[0] = "T_FALLBACK" local v2 = vm() diff --git a/tests/engine/game3_help_window_test.lua b/tests/engine/game3_help_window_test.lua index d8a3fb08..4f29fa25 100644 --- a/tests/engine/game3_help_window_test.lua +++ b/tests/engine/game3_help_window_test.lua @@ -1,10 +1,3 @@ --- Sections 5.5/5.6 of docs/game3/e10-opcode-spec.md: the help MESSAGE window --- behind `loadhelp` / `unloadhelp`. Unwired (the dispatch cases are the --- Finisher's), so this suite pins the contract they will call: open/close, --- pret's idempotency, the window geometry, and that resolution of the opcode's --- word argument is deliberately NOT this module's job. --- luajit tests/engine/game3_help_window_test.lua - package.path = "./?.lua;./?/init.lua;" .. package.path local T = require("tests.harness") @@ -17,13 +10,9 @@ local HelpWindow = require("src.ui.game3.help_window") HelpWindow.reset() --- ------------------------------------------------------------ module boundary - check(package.loaded["src.ui.game3.help_system"] == nil, "help_window does not load the L/R Help browser (help_system.lua)") --- -------------------------------------------------------- geometry (pret-anchored) - local o = HelpWindow.OUTER eq(o.left, 0, "outer left is 0") eq(o.top, 15, "outer top is 15 (pret src/help_message.c:13-19)") @@ -37,12 +26,11 @@ eq(c.top - 1, o.top, "content is the outer rect inset one tile top") eq(c.width + 2, o.width, "frame width = content + the two border tiles") eq(c.height + 2, o.height, "frame height = content + the two border tiles") --- The engine canvas is the GBA grid, so pret's rect maps 1:1. eq(Display.COLS, o.width, "engine grid is 30 tiles wide (display.lua:10)") eq(Display.ROWS, o.top + o.height, "engine grid is 20 rows: the window sits on the bottom 5") eq(Display.TILE, 8, "engine tile is 8px") --- pret PrintHelpMessageText text placement (src/help_message.c:95-98). +-- src/help_message.c:95-98 eq(HelpWindow.TEXT_OFFSET.x, 2, "text x offset is pret's 2px") eq(HelpWindow.TEXT_OFFSET.y, 5, "text y offset is pret's 5px") check(c.width * 8 - HelpWindow.TEXT_OFFSET.x * 2 > 0, @@ -50,8 +38,6 @@ check(c.width * 8 - HelpWindow.TEXT_OFFSET.x * 2 > 0, check(c.top * 8 + HelpWindow.TEXT_OFFSET.y < (o.top + o.height) * 8, "the text baseline lands inside the outer window") --- ---------------------------------------------------------------- lifecycle - check(HelpWindow.isOpen() == false, "starts closed") eq(HelpWindow.getText(), "", "starts with no text") check(HelpWindow.draw() == false, "draw with nothing open is a no-op") @@ -61,26 +47,18 @@ check(HelpWindow.isOpen() == true, "isOpen reads back open") eq(HelpWindow.getText(), "MOVE ID: STRENGTH", "getText reads back the text") check(HelpWindow.draw() == true, "draw renders while open (headless)") --- pret PrintTextOnHelpMessageWindow takes an already-resolved pointer; the --- engine handler resolves the opcode's word with resolve_text (ops_a.lua:36-45) --- and passes a string. This module must not interpret pointers itself. check(HelpWindow.show("0x08012345") == true, "a pointer-looking string is accepted") eq(HelpWindow.getText(), "0x08012345", "text is stored verbatim, never resolved here") --- Re-showing replaces the content (pret's second DrawHelpMessageWindowWithText). check(HelpWindow.show("BICYCLE") == true, "re-show is allowed") eq(HelpWindow.getText(), "BICYCLE", "re-show replaces the text") eq(HelpWindow.isOpen(), true, "still open after re-show") --- Non-string input degrades to empty content rather than erroring (the handler --- may pass nil when resolve_text misses; pret falls back to ctx->data[0]). check(HelpWindow.show(nil) == true, "show(nil) still opens") eq(HelpWindow.getText(), "", "show(nil) means empty text, not an error") check(HelpWindow.show(123) == true, "show(number) still opens") eq(HelpWindow.getText(), "", "non-string text degrades to empty") --- ------------------------------------------------------------------- closing - check(HelpWindow.close() == true, "close hides the window") check(HelpWindow.isOpen() == false, "closed reads back closed") eq(HelpWindow.getText(), "", "close clears the text") diff --git a/tests/engine/game3_knock_off_item_test.lua b/tests/engine/game3_knock_off_item_test.lua index a24fcf34..14df2d0f 100644 --- a/tests/engine/game3_knock_off_item_test.lua +++ b/tests/engine/game3_knock_off_item_test.lua @@ -1,17 +1,5 @@ --- KNOCK_OFF renders the held item unusable for the rest of the battle. -- --- FRLG semantics (Bulbapedia: "prevent its use during the battle"; pret --- pokefirered/src/battle_script_commands.c:2730-2752): the effect clears the --- battler's item and sets the battle-scoped knockedOffMons bit. The party mon --- keeps the item -- "it still remains visible on the status screen" -- and the --- bit masks the battler back to ITEM_NONE on every later send-out --- (battle_script_commands.c:4489), so the item does not come back on --- switch-out. It is usable again after the battle. --- --- History: an earlier workaround also wrote the removal through to the party --- mon. That is wrong for FRLG -- the party copy has to survive the battle -- --- and it is not what stops the item returning on switch-out; the send-out mask --- below is. +-- pokefirered/src/battle_script_commands.c:2730-2752, battle_script_commands.c:4489 -- -- luajit tests/engine/game3_knock_off_item_test.lua @@ -59,16 +47,12 @@ local function knock_off(user, target, st) }, "KNOCK_OFF", false, true, false) end --- 1. The battler loses the item, the party mon keeps it (usable again after --- the battle); the send-out mask below is what hides it for the rest of --- this battle. local user, target = battler("enemy", 0), battler("player", 13) check(knock_off(user, target), "KNOCK_OFF reports the item was removed") eq(target.item, 0, "the battler's item is cleared") eq(target.mon.item, 13, "the party mon keeps the item for after the battle") eq(target.mon.heldItem, 13, "heldItem is kept as well") --- 2. The enemy side behaves the same. local user2, target2 = battler("player", 0), battler("enemy", 13) knock_off(user2, target2) eq(target2.item, 0, "the enemy battler's item is cleared") @@ -84,8 +68,6 @@ eq(target3.mon.item, 13, "STICKY_HOLD keeps the party item") local user4, target4 = battler("enemy", 0), battler("player", 0) check(not knock_off(user4, target4), "a target with no item is a no-op") --- 5. Send-out mask: State.makeBattler re-reads the item from the party, so a --- marked mon must come back in with ITEM_NONE while its party copy stays. local st = {} State.markKnockedOff(st, { side = "enemy", partyIndex = 1 }) local mon = { species = 1, level = 5, hp = 20, maxHp = 20, moves = {}, pp = {}, item = 13, heldItem = 13 } @@ -96,9 +78,6 @@ eq(mon.item, 13, "the party mon still holds the item after the send-out") local control = State.makeBattler(mon, "enemy", { partyIndex = 1, st = {} }) eq(control.item, 13, "without the mark the send-out reads the item back") --- 6. The mark is per party slot and survives every switch path the engine --- exposes: engine switch-in, presentation send-out, player switch and shift --- switch -- and it does not leak into the next battle. local function fresh_mon(item) return { species = 4, level = 10, hp = 30, maxHp = 30, item = item, heldItem = item, ability = "NONE", moves = { 10 }, pp = { 35 } } diff --git a/tests/engine/game3_load_order_seam_test.lua b/tests/engine/game3_load_order_seam_test.lua index 752c4d00..5d347717 100644 --- a/tests/engine/game3_load_order_seam_test.lua +++ b/tests/engine/game3_load_order_seam_test.lua @@ -1,12 +1,3 @@ --- I6 load-order cycles, four-file half (review-v3 row I6: Data.lua:262, --- CacheFs.lua:231, SaveData.lua:20). Method: split the require graph in two — --- the STATIC graph (every require("src...") string) and the LOAD-TIME graph --- (requires that execute at module load). The row's fix hint is "lazy requires --- at call sites", so the contract here is: the four I6 files carry no --- load-time edge into the cycle, and the ChipAudio cycle has had its edge --- removed outright. --- luajit tests/engine/game3_load_order_seam_test.lua - package.path = "./?.lua;./?/init.lua;" .. package.path local T = require("tests.harness") @@ -24,12 +15,6 @@ local function isLoaded(name) return package.loaded[name] ~= nil end --- ------------------------------------------- load-time proof: SaveData ⇄ Bag --- Before this fix SaveData.lua:20 was `local Bag = require("src.inventory.Bag")` --- at module load, the one top-level leg of --- Data -> CacheFs -> SaveData -> Bag -> Data. Requiring SaveData must not pull --- Bag in: the require now lives at its single call site (SaveData:2364). - local SaveData = require("src.core.SaveData") check(isLoaded("src.inventory.Bag") == false, "requiring SaveData does not load Bag (the cycle lost its load-time leg)") @@ -37,18 +22,11 @@ check(isLoaded("src.core.Data") == false, "requiring SaveData does not load Data") check(type(SaveData.saveFilename) == "function", "SaveData still exposes saveFilename") --- Bag's Data edge is call-site-only too (pocketOf/capacity/orderFor). local Bag = require("src.inventory.Bag") check(isLoaded("src.core.Data") == false, "requiring Bag does not load Data (its requires are at call sites)") check(type(Bag.add) == "function", "Bag still exposes add") --- ------------------------------------- load-time proof: ChipAudio ⇄ Session --- ChipAudio.lua:625 used to require SessionLifecycle at module load, closing --- ChipAudio -> SessionLifecycle -> Music/Sound -> ChipAudio. The registration --- moved into SessionLifecycle.endProcess through package.loaded, so the edge is --- gone from the static graph, not just deferred. - local ChipAudio = require("src.core.ChipAudio") check(isLoaded("src.core.SessionLifecycle") == false, "requiring ChipAudio does not load SessionLifecycle (edge removed)") @@ -61,8 +39,6 @@ check(isLoaded("src.core.Music") == false, check(type(SessionLifecycle.endProcess) == "function", "SessionLifecycle still exposes endProcess") --- ------------------------------------------------ static source guards (the edges) - do local chip = sourceOf("src/core/ChipAudio.lua") check(chip ~= nil, "ChipAudio.lua is readable") @@ -81,9 +57,6 @@ do check(sl:find('package.loaded["src.core.ChipAudio"]', 1, true) ~= nil, "endProcess reaches ChipAudio through package.loaded (the inversion)") - -- The other three legs of Data -> CacheFs -> SaveData -> Bag -> Data are all - -- call-site requires; if any of them goes top-level again this cycle closes - -- at load time, so pin their shape here. local bag = sourceOf("src/inventory/Bag.lua") check(bag:find('\nlocal Data = require("src.core.Data")', 1, true) == nil, "Bag keeps Data at call sites (no column-0 require)") @@ -92,8 +65,6 @@ do "Data keeps CacheFs at call sites (no column-0 require)") end --- ------------------------------------------- the inverted shutdown still runs - local ran = 0 SessionLifecycle.registerProcessShutdown(function() ran = ran + 1 end) local okEnd = pcall(SessionLifecycle.endProcess) @@ -105,7 +76,4 @@ check(okSecond, "ChipAudio.shutdown is idempotent after endProcess already calle check(isLoaded("src.core.SessionLifecycle") == true, "SessionLifecycle stays loaded once explicitly required (no reload cycle)") --- Residual, documented: the static SCC Data/SaveData/CacheFs/Bag remains with --- every leg at a call site; its only structural closer would remove --- CacheFs -> SaveData, which is the deferred src/import half (CARVE2 ruling). T.finish("game3_load_order_seam_test") diff --git a/tests/engine/game3_mystery_event_status_test.lua b/tests/engine/game3_mystery_event_status_test.lua index 927f63d7..0790c9ef 100644 --- a/tests/engine/game3_mystery_event_status_test.lua +++ b/tests/engine/game3_mystery_event_status_test.lua @@ -1,9 +1,4 @@ --- rse-seams e10 spec 5.1: setmysteryeventstatus writes a real status slot --- mirroring pret SetMysteryEventScriptStatus --- pret src/scrcmd.c:269-273 (opcode handler) --- pret src/mystery_event_script.c:92-95 (writer) --- pret src/mystery_event_script.c:75-80 (MEventScript_Run status out-param) --- lua: luajit tests/engine/game3_mystery_event_status_test.lua +-- src/scrcmd.c:269-273, src/mystery_event_script.c:92-95, src/mystery_event_script.c:75-80 package.path = "./?.lua;./?/init.lua;" .. package.path @@ -31,18 +26,15 @@ end eq(Std.SPECIAL.setmysteryeventstatus or 0xE, 0xE, "opcode 0x0e is setmysteryeventstatus") eq(MysteryGift.getStatus(), 0, "the slot starts at 0") --- 1. VM row -> write -> readback (both mirrors). local v = vm() eq(Ops.dispatch(v, { op = "setmysteryeventstatus", [1] = 2 }), false, "the op does not yield") eq(v.ctx.mysteryEventStatus, 2, "the ctx slot carries the written value") eq(MysteryGift.getStatus(), 2, "mystery_gift mirrors the value (setStatus/getStatus)") --- 2. Other values round-trip. Ops.dispatch(v, { op = "setmysteryeventstatus", [1] = 3 }) eq(MysteryGift.getStatus(), 3, "value 3 (pret SetIncompatible's status) round-trips") eq(v.ctx.mysteryEventStatus, 3, "and the ctx copy agrees") --- 3. A non-numeric byte clamps to 0 rather than raising. Ops.dispatch(v, { op = "setmysteryeventstatus", [1] = "bogus" }) eq(MysteryGift.getStatus(), 0, "non-numeric value normalises to 0") eq(#logs, 0, "no log spam on the happy path") diff --git a/tests/engine/game3_object_subpriority_test.lua b/tests/engine/game3_object_subpriority_test.lua index 0782ce52..54a049d5 100644 --- a/tests/engine/game3_object_subpriority_test.lua +++ b/tests/engine/game3_object_subpriority_test.lua @@ -1,16 +1,4 @@ --- Producer contract for seam-7 set/resetobjectsubpriority (rse-seams e10 --- spec 5.8): the script op freezes an object's draw-order pair on the --- EventObject record { fixedPriority, subpriority, fixedClass }; the field_view --- half (Refactor lane) consumes it, and the freeze points mirror pret: --- pret src/event_object_movement.c:2089-2101 SetObjectSubpriority --- (objectEvent->fixedPriority = TRUE; sprite->subpriority = subpriority) --- pret src/event_object_movement.c:2104-2116 ResetObjectSubpriority --- (fixedPriority = FALSE — re-enables the dynamic path, does NOT restore --- a previous value; all three fields drop) --- pret src/scrcmd.c:1122-1130 (+83 bias) / :1133-1140 --- guard: TryGetObjectEventIdByLocalIdAndMap (mapGroup/mapNum must resolve --- to the active map or the command does nothing) --- lua: luajit tests/engine/game3_object_subpriority_test.lua +-- src/event_object_movement.c:2089-2101, src/event_object_movement.c:2104-2116, src/scrcmd.c:1122-1130 package.path = "./?.lua;./?/init.lua;" .. package.path @@ -30,34 +18,28 @@ local savedById = Objects._byId Objects._mapId = here Objects._byId = { [7] = { localId = 7 } } --- 1. set freezes the pair with the +83 bias applied by the script op and --- nils any stale fixedClass (spec 5.8 record). local ok = Objects.setSubpriority(7, 0, 1, 5 + 83) eq(ok, true, "set succeeds for the current map") eq(Objects._byId[7].fixedPriority, true, "fixedPriority is frozen") eq(Objects._byId[7].subpriority, 88, "subpriority stores priority + 83") -Objects._byId[7].fixedClass = 1 -- stale class from an earlier freeze +Objects._byId[7].fixedClass = 1 Objects.setSubpriority(7, 0, 1, 10 + 83) eq(Objects._byId[7].fixedClass, nil, "set nils a stale fixedClass before first observation") --- 2. cross-map refusal (pret TryGetObjectEventIdByLocalIdAndMap). local refused = Objects.setSubpriority(7, 0, 2, 7 + 83) eq(refused, false, "a different (mapGroup, mapNum) is refused") eq(Objects._byId[7].subpriority, 93, "the refused call left the record alone") --- 3. unknown localId is a silent no-op (pret lookup failure). eq(Objects.setSubpriority(99, 0, 1, 1), false, "unknown localId: set is a no-op") eq(Objects.resetSubpriority(99, 0, 1), false, "unknown localId: reset is a no-op") --- 4. reset drops all three fields and restores the dynamic path (no old --- value is restored — pret event_object_movement.c:2104-2116). +-- event_object_movement.c:2104-2116 Objects._byId[7].fixedClass = 2 eq(Objects.resetSubpriority(7, 0, 1), true, "reset succeeds for the current map") eq(Objects._byId[7].fixedPriority, nil, "fixedPriority dropped") eq(Objects._byId[7].subpriority, nil, "subpriority dropped") eq(Objects._byId[7].fixedClass, nil, "fixedClass dropped (all three per spec 5.8)") --- 5. no active map / no object: still safe. Objects._byId = {} eq(Objects.setSubpriority(7, 0, 1, 1), false, "no object after clear: no-op") diff --git a/tests/engine/game3_options_block_test.lua b/tests/engine/game3_options_block_test.lua index 170a9e46..416138a3 100644 --- a/tests/engine/game3_options_block_test.lua +++ b/tests/engine/game3_options_block_test.lua @@ -1,11 +1,3 @@ --- T1.1 handoff (docs/game3/rse-seams.md section 4, finding J2): the engine --- options block key comes from the game profile, not the `"firered"` literal. --- --- Regression: Options.BLOCK was a hardcoded "firered" and every read/write went --- through it, so a second Gen 3 game would share FireRed's options block. --- FireRed resolves to "firered" in every path, so existing files are unchanged. --- luajit tests/engine/game3_options_block_test.lua - package.path = "./?.lua;./?/init.lua;" .. package.path local T = require("tests.harness") @@ -15,23 +7,19 @@ love = love or require("tests.love_stub") local Options = require("src.core.game3.options") local Profile = require("src.core.game3.profile") --- 1. Compatibility field stays, sourced from the profile. eq(Options.BLOCK, Profile.FALLBACK_ID, "Options.BLOCK is the profile fallback id") eq(Profile.active().optionsBlock, "firered", "the active profile resolves to firered headless") --- 2. Defaults still fill, and the block lands under the profile's key. local engine = {} local o = Options.block(engine) eq(o.textSpeed, 1, "defaults are filled") check(type(engine.firered) == "table", "the block is created under the profile's block id") --- 3. An explicit block id is honoured (the seam RSE callers use). local engine2 = {} Options.block(engine2, "other") check(type(engine2.other) == "table", "an explicit block id creates that block") check(engine2.firered == nil, "no firered block is created when an explicit id is given") --- 4. bind() keys off the session's stored game (T0.2 version field). local session = { version = "firered" } local engine3 = { firered = { textSpeed = 2, buttonMode = 2 } } local bound = Options.bind(session, engine3) @@ -39,18 +27,15 @@ eq(bound.textSpeed, 2, "bind reads the stored game's block") eq(bound.text_speed, 2, "the text_speed alias is set") eq(bound.l_equals_a, true, "the l_equals_a alias is set") --- 5. ensure() reads a nested engine options table by the session's block id. local nestedOpts = { firered = { textSpeed = 0 } } local nested = { version = "firered", options = nestedOpts } eq(Options.ensure(nested).textSpeed, 0, "ensure reads the stored game's nested block") eq(nested.engineOptions, nestedOpts, "ensure records the engine options table") --- 6. Unknown ids fail closed to the active profile without raising. eq(Options.blockId({ version = "not-a-real-game" }), Profile.active().optionsBlock, "an unknown game id fails closed to the active block") eq(Options.blockId({}), Profile.active().optionsBlock, "no version means the active block") --- 7. The legacy root-level migration still runs when a block is created. local legacy = { battleStyle = 1 } local migrated = Options.block(legacy) eq(migrated.battleStyle, 1, "legacy root keys migrate into the block") diff --git a/tests/engine/game3_ow_pic_table_len_test.lua b/tests/engine/game3_ow_pic_table_len_test.lua new file mode 100644 index 00000000..2964888e --- /dev/null +++ b/tests/engine/game3_ow_pic_table_len_test.lua @@ -0,0 +1,79 @@ +-- pokefirered/src/data/object_events/object_event_pic_tables.h + +package.path = "./?.lua;./?/init.lua;" .. package.path + +local T = require("tests.harness") +local check, eq = T.check, T.eq + +local OwExtract = require("src.import.gba.ow_extract") + +local SIZE = 0x4000 +local bytes = {} +for i = 0, SIZE - 1 do bytes[i] = 0 end +local function w16(o, v) bytes[o] = v % 256; bytes[o + 1] = math.floor(v / 256) % 256 end +local function w32(o, v) w16(o, v % 65536); w16(o + 2, math.floor(v / 65536)) end +local function ptr(o) return 0x08000000 + o end + +local POINTERS, INFOS, ANIMS, PICS, GFX = 0x100, 0x200, 0x300, 0x800, 0x2000 +local STRIDE = 0x24 + +local ANIM0, ANIM_RAISE = ANIMS + 0x100, ANIMS + 0x120 +w32(ANIM0, 0x00100000); w16(ANIM0 + 4, 0xFFFE); w16(ANIM0 + 6, 0) +w32(ANIM_RAISE, 0x00100009); w16(ANIM_RAISE + 4, 0xFFFF) +for i = 0, 19 do w32(ANIMS + i * 4, ptr(ANIM0)) end +w32(ANIMS + 20 * 4, ptr(ANIM_RAISE)) + +local tables = { + { frames = 9, w = 16, h = 32 }, + { frames = 9, w = 16, h = 32, unreferenced = true }, + { frames = 10, w = 16, h = 32 }, + { frames = 4, w = 16, h = 16, inanimate = true }, +} +local at = PICS +for _, t in ipairs(tables) do + t.off = at + local fb = t.w * t.h / 2 + for f = 0, t.frames - 1 do + w32(at, ptr(GFX + f * fb)); w16(at + 4, fb) + at = at + 8 + end +end + +local slot, gid = 0, 0 +for _, t in ipairs(tables) do + local info = INFOS + slot * STRIDE + w16(info, 0xFFFF) + w16(info + 6, t.w * t.h / 2) + w16(info + 8, t.w); w16(info + 10, t.h) + bytes[info + 12] = t.inanimate and 0x40 or 0 + w32(info + 0x18, ptr(ANIMS)) + w32(info + 0x1C, ptr(t.off)) + if not t.unreferenced then + w32(POINTERS + gid * 4, ptr(info)) + t.gid = gid + gid = gid + 1 + end + slot = slot + 1 +end + +local chars = {} +for i = 0, SIZE - 1 do chars[i + 1] = string.char(bytes[i]) end +local data = table.concat(chars) +local rom = { size = SIZE } +function rom:get(o) return data:byte(o + 1) end +function rom:u16(o) local a, b = data:byte(o + 1, o + 2); return a + b * 256 end +function rom:u32(o) local a, b, c, d = data:byte(o + 1, o + 4); return a + b * 256 + c * 65536 + d * 16777216 end +function rom:readBytes(o, n) return data:sub(o + 1, o + n) end + +local version = { ow_gfx_pointers = POINTERS, num_obj_event_gfx = gid } + +local s0 = OwExtract.extractOne(rom, tables[1].gid, nil, version) +eq(s0 and s0.frameCount, 9, + "9-frame table stops at the next table even though the anim table reaches frame 9") +local s1 = OwExtract.extractOne(rom, tables[3].gid, nil, version) +eq(s1 and s1.frameCount, 10, "10-frame table keeps its RaiseHand frame") +local s2 = OwExtract.extractOne(rom, tables[4].gid, nil, version) +eq(s2 and s2.frameCount, 4, "inanimate multi-frame table keeps every frame") +check(s0 and #s0.frames == 9, "frame pixels decoded for each table entry only") + +T.finish("game3_ow_pic_table_len_test") diff --git a/tests/engine/game3_pokemon_no_extractor_require_test.lua b/tests/engine/game3_pokemon_no_extractor_require_test.lua index f60ec6d5..d647daf0 100644 --- a/tests/engine/game3_pokemon_no_extractor_require_test.lua +++ b/tests/engine/game3_pokemon_no_extractor_require_test.lua @@ -1,20 +1,9 @@ --- rse-seams T6.3b / I8: `runtime -> extractor -> runtime`. --- --- Regression: src/core/game3/pokemon.lua required the ROM extractor --- (src.import.gba.extract_island1) at module scope just to read the cache --- root, so loading the runtime pulled in the whole extractor chain --- (extract_island1 -> extract_scripts -> encounters). The root now comes --- from the zero-require CachePaths module (T6.3a), so a plain runtime load --- must NOT put the extractor in package.loaded. --- luajit tests/engine/game3_pokemon_no_extractor_require_test.lua - package.path = "./?.lua;./?/init.lua;" .. package.path local T = require("tests.harness") local check, eq = T.check, T.eq love = love or require("tests.love_stub") --- Nothing may have pulled the extractor in before us (clean process). eq(package.loaded["src.import.gba.extract_island1"], nil, "precondition: the extractor is not loaded yet") @@ -28,8 +17,6 @@ local CachePaths = require("src.core.game3.cache_paths") eq(CachePaths.CACHE_ROOT, "data/generated/gba", "the shared CachePaths module still carries the default root") --- extract_island1 forwards its roots at CachePaths, so the two agree either --- way round (Dataset.mountExtractRoots writes through the same seam). package.loaded["src.import.gba.extract_island1"] = nil local Extract = require("src.import.gba.extract_island1") eq(Extract.CACHE_ROOT, CachePaths.CACHE_ROOT, diff --git a/tests/engine/game3_profile_test.lua b/tests/engine/game3_profile_test.lua index 643e6728..33f6aafa 100644 --- a/tests/engine/game3_profile_test.lua +++ b/tests/engine/game3_profile_test.lua @@ -1,12 +1,3 @@ --- FireRed profile row: resolution rules + the value contract the wiring --- tickets in docs/game3/rse-seams.md depend on. --- --- The profile is deliberately inert until the handoff tickets wire it, so --- this suite is the guard that keeps its FireRed row equal to the constants --- the engine reads today. When a wiring ticket lands, the assertion it --- replaces moves from a literal to a comparison against the live module. --- luajit tests/engine/game3_profile_test.lua - package.path = "./?.lua;./?/init.lua;" .. package.path local T = require("tests.harness") @@ -18,8 +9,6 @@ local MapIds = require("src.core.game3.map_ids") local prevVersion = GameVersion.get() --- ---------------------------------------------------------------- resolution - Profile.reset() GameVersion.set("firered") local active = Profile.active() @@ -29,32 +18,24 @@ check(Profile.isGame3Version("firered") == true, "isGame3Version(firered) is tru check(Profile.isGame3Version("red") == false, "isGame3Version(red) is false") check(Profile.isGame3Version("not-a-game") == false, "isGame3Version(unknown) is false") --- Cached identity: repeated lookups return the same table. check(Profile.of("firered") == active, "resolution is cached") --- Unknown game ids fail closed to the active game, not to nil. local unknown = Profile.of("not-a-game") eq(unknown.id, "firered", "an unknown game id falls back to FireRed") --- A fresh process with no Gen 3 game selected still resolves FireRed, so --- shared code can require the module during boot and in headless suites. Profile.reset() GameVersion.set("red") eq(Profile.active().id, "firered", "non-Gen3 process fails closed to FireRed") --- Session-scoped capability lookup reads session.version, not the process. local caps = Profile.capabilitiesFor({ version = "firered" }) check(caps.fameChecker == true, "capabilitiesFor(session) reads the session game") check(Profile.has({ version = "firered" }, "vsSeeker") == true, "has() reads a flag") check(Profile.has({ version = "firered" }, "contests") == false, "has() is false for absent flags") --- ---------------------------------------------------- FireRed value contract - Profile.reset() GameVersion.set("firered") local row = Profile.active() --- Map identity: the profile describes exactly what MapIds accepts today. for _, prefix in ipairs(row.map.prefixes) do check(MapIds.isGame3Map(prefix .. "ANYTHING") == true, "profile prefix " .. prefix .. " is a Game3 map for MapIds") @@ -104,12 +85,10 @@ eq(row.trainers.music.victory.trainer, 310, "trainer victory music") eq(row.regionMap.switchFlag, "FLAG_SYS_SEVII_MAP_123", "region-map switch flag") --- FRLG-only features are on for FireRed, off by default for anything else. for _, flag in ipairs({ "fameChecker", "teachyTV", "vsSeeker", "trainerTower", "seagallop" }) do check(row.capabilities[flag] == true, "FireRed capability " .. flag .. " is on") end --- The native module list the registry merges today (natives.lua:803-821). local expectedNatives = { "natives_corner", "natives_cutscene", "natives_daycare", "natives_elevator", "natives_events", "natives_fame", "natives_fan_club", "natives_gift", @@ -122,10 +101,6 @@ for i, name in ipairs(expectedNatives) do eq(row.nativeModules[i], name, "native module " .. i .. " is " .. name) end --- ------------------------------------------------------------ static guards - --- FireRed must stay the fallback row: a missing profiles/firered.lua is a --- hard error, not a silent nil. Profile.reset() local okMissing = pcall(function() local warn = Profile.of("not-a-game") @@ -133,8 +108,6 @@ local okMissing = pcall(function() end) check(okMissing == true, "unknown ids resolve without raising") --- The profile names the modules the wiring tickets will create; keep them --- syntactically valid package names so a typo fails here, not at boot. for _, rel in ipairs({ row.font.module, row.dexArea.mapGroups, diff --git a/tests/engine/game3_save_version_roundtrip_test.lua b/tests/engine/game3_save_version_roundtrip_test.lua index f5862403..628c5930 100644 --- a/tests/engine/game3_save_version_roundtrip_test.lua +++ b/tests/engine/game3_save_version_roundtrip_test.lua @@ -1,13 +1,3 @@ --- T0.2 handoff (docs/game3/rse-seams.md section 4): the save's game identity --- must round-trip instead of being a FireRed literal. --- --- Regression: Schema.toSaveTable wrote `version = "firered"` and fromSaveTable --- dropped the field entirely, so a future RSE save could not be told apart from --- a FireRed one, and Game3._hasContinueSave / SaveData.slotSummary keyed Gen 3 --- off the `"FR_"` / `"firered"` literals. Every path here resolves to the --- active profile, which fails closed to FireRed headless. --- luajit tests/engine/game3_save_version_roundtrip_test.lua - package.path = "./?.lua;./?/init.lua;" .. package.path local T = require("tests.harness") @@ -18,26 +8,21 @@ local Schema = require("src.core.game3.save_schema_firered") local Profile = require("src.core.game3.profile") local SaveData = require("src.core.SaveData") --- 1. newGame stamps the active profile, not a literal. local session = Schema.newGame({ gender = 0 }) eq(session.version, Profile.active().id, "newGame stamps the active profile id") eq(session.version, "firered", "headless fails closed to the FireRed profile") --- 2. toSaveTable writes the session's game. local save = Schema.toSaveTable(session) eq(save.version, "firered", "toSaveTable writes the session version, not a literal") --- 3. fromSaveTable round-trips it (previously the field was dropped). local back = Schema.fromSaveTable(save) eq(back.version, "firered", "fromSaveTable round-trips version") --- 4. A pre-T0.2 save with no version field falls back to the active profile. local bare = Schema.toSaveTable(session) bare.version = nil eq(Schema.fromSaveTable(bare).version, Profile.active().id, "a version-less save falls back to the active profile") --- 5. A version the engine does not know fails closed without raising. local unknown = Schema.toSaveTable(session) unknown.version = "not-a-real-game" eq(Schema.fromSaveTable(unknown).version, "not-a-real-game", @@ -45,7 +30,6 @@ eq(Schema.fromSaveTable(unknown).version, "not-a-real-game", eq(Profile.of("not-a-real-game").id, Profile.active().id, "Profile.of fails closed for an unknown id") --- 6. slotSummary classifies Gen 3 from the engine tag, not the version literal. local name, info = SaveData.slotSummary({ engine = "game3", name = "RED", diff --git a/tests/engine/game3_version_dispatch_test.lua b/tests/engine/game3_version_dispatch_test.lua index ef1485a6..da8741ed 100644 --- a/tests/engine/game3_version_dispatch_test.lua +++ b/tests/engine/game3_version_dispatch_test.lua @@ -1,13 +1,3 @@ --- J10: the mod API dispatches Gen 3 on (engine, versionId), with a graceful --- fallback to the generation's default row so FireRed behaviour is unchanged. --- --- The seam is three tables: Loader.apiModule (facade module paths), --- Schemas.GEN3_ROUTING (per-game registry routing) and --- Schemas.GEN3_LIVE_MODULES (per-game live-module bindings). This suite pins --- the defaults and proves an overlay can override one registry/key without --- forking the shared tables. --- luajit tests/engine/game3_version_dispatch_test.lua - package.path = "./?.lua;./?/init.lua;" .. package.path local T = require("tests.harness") @@ -19,8 +9,6 @@ local Loader = require("src.mods.Loader") local prevVersion = GameVersion.get() --- ------------------------------------------------------- facade dispatch - eq(Loader.apiModule("battle", 3, nil), "src.battle.game3.BattleAPI", "gen3 battle facade default is the FireRed-backed module") eq(Loader.apiModule("world", 3, nil), "src.world.game3.WorldAPI", @@ -36,14 +24,11 @@ eq(Loader.apiModule("battle", 3, "ruby"), "src.battle.game3.BattleAPI", eq(Loader.apiModule("world", 3, "not-a-game"), "src.world.game3.WorldAPI", "an unknown id falls back to the FireRed-backed facade") --- other generations keep their existing arms eq(Loader.apiModule("battle", 2, nil), "src.battle.gen2.BattleAPI", "gen2 battle facade") eq(Loader.apiModule("world", 2, nil), "src.world.gen2.WorldAPI", "gen2 world facade") eq(Loader.apiModule("battle", 1, nil), "src.battle.BattleAPI", "gen1 battle facade") eq(Loader.apiModule("world", 1, nil), "src.world.WorldAPI", "gen1 world facade") --- ------------------------------------------------------- routing dispatch - check(Schemas.routing(3, nil) == Schemas.GEN3, "gen3 routing without a version is GEN3") check(Schemas.routing(3, "firered") == Schemas.GEN3, "firered has no overlay, so GEN3") check(Schemas.routing(3, "ruby") == Schemas.GEN3, "an overlay-less RSE id reads GEN3") @@ -58,7 +43,6 @@ eq(Schemas.targetFor("pokemon", spec, 3, "ruby"), "gen3Pokemon", "an overlay-less RSE id routes pokemon to the shared root") check(Schemas.gatedFor("pokemon", 3, "firered") == false, "pokemon is not gated under FireRed") --- a sparse overlay changes one registry and inherits the rest Schemas.GEN3_ROUTING["testgame"] = { pokemon = "gen3PokemonTest", moves = false } local routed = Schemas.routing(3, "testgame") check(routed ~= Schemas.GEN3, "an overlay produces a distinct merged view") @@ -74,8 +58,6 @@ check(Schemas.gatedFor("moves", 3, "firered") == false, check(Schemas.routing(3, "firered") == Schemas.GEN3, "the merged view is never written back") Schemas.GEN3_ROUTING["testgame"] = nil --- ---------------------------------------------------- live module dispatch - local probe = { measure = function() return 1 end } package.loaded["tests.dispatch_probe"] = probe Schemas.GEN3_LIVE_MODULES["testgame"] = { gen3Pokemon = "tests.dispatch_probe" } @@ -86,7 +68,6 @@ check(Schemas.liveModuleFor("gen3Pokemon", "firered") ~= probe, Schemas.GEN3_LIVE_MODULES["testgame"] = nil package.loaded["tests.dispatch_probe"] = nil --- bindGen3 records the game so live-module lookups can narrow by it local data = { gen3Pokemon = { names = {} } } Schemas.bindGen3(data, "testgame") eq(Schemas.boundVersion(data), "testgame", "bindGen3 records the version id") @@ -94,8 +75,6 @@ local legacy = { gen3Pokemon = { names = {} } } Schemas.bindGen3(legacy) eq(Schemas.boundVersion(legacy), nil, "a version-less bind reports nil") --- ------------------------------------------------------- loader plumbing - local function memfs(files) return { read = function(_, path) return files[path] end, diff --git a/tests/engine/game3_versions_game_test.lua b/tests/engine/game3_versions_game_test.lua index d9d03b11..bdc11b62 100644 --- a/tests/engine/game3_versions_game_test.lua +++ b/tests/engine/game3_versions_game_test.lua @@ -1,8 +1,3 @@ --- T6.3a: the Versions.game(id) facade skeleton. New file, unwired; this --- suite pins the fail-closed contract and the FireRed/LeafGreen rows so the --- T6.2 wiring can alias Versions.game without touching this suite. --- luajit tests/engine/game3_versions_game_test.lua - package.path = "./?.lua;./?/init.lua;" .. package.path local T = require("tests.harness") @@ -14,8 +9,6 @@ local Versions = require("src.import.gba.versions") local prevVersion = GameVersion.get() --- -------------------------------------------------------- FireRed resolution - VersionsGame.reset() GameVersion.set("firered") @@ -29,15 +22,11 @@ check(VersionsGame.game("") == Versions, "an empty id resolves the active game") check(VersionsGame.game("ruby") == Versions, "an unregistered RSE id falls back to FireRed's table") --- A non-Gen3 process still resolves FireRed, so extractor tooling can require --- this module without booting a game. VersionsGame.reset() GameVersion.set("red") check(VersionsGame.game(nil) == Versions, "a non-Gen3 process fails closed to FireRed's table") --- -------------------------------------------- the table contract the rows owe - VersionsGame.reset() GameVersion.set("firered") local tables = VersionsGame.game("firered") @@ -50,8 +39,6 @@ check(type(tables.CACHE_VERSION) == "number", "CACHE_VERSION is a number") eq(tables.NUM_SPECIES, 412, "NUM_SPECIES is 412 (pret: pokefirered/include/constants/species.h:421-423)") --- ------------------------------------------------------ registration + cache - local probe = { MAPS = {}, NUM_SPECIES = 1 } package.loaded["tests.versions_game_probe"] = probe check(VersionsGame.register("testgame", "tests.versions_game_probe") == true, @@ -61,16 +48,13 @@ check(VersionsGame.register("", "tests.versions_game_probe") == false, "register rejects an empty id") check(VersionsGame.register("testgame", 7) == false, "register rejects a non-string path") --- A broken row does not raise: it warns once and falls back to FireRed. check(VersionsGame.register("busted", "no.such.module") == true, "register a broken row") check(VersionsGame.game("busted") == Versions, "a broken row falls back to FireRed") --- reset drops resolutions and keeps registrations. VersionsGame.reset() check(VersionsGame.game("testgame") == probe, "registrations survive reset") check(VersionsGame.game("firered") == Versions, "resolutions re-resolve after reset") --- cleanup VersionsGame.GAMES["testgame"] = nil VersionsGame.GAMES["busted"] = nil package.loaded["tests.versions_game_probe"] = nil diff --git a/tests/engine/game3_virtual_objects_drawhook_test.lua b/tests/engine/game3_virtual_objects_drawhook_test.lua index 67ff15a4..8c99828b 100644 --- a/tests/engine/game3_virtual_objects_drawhook_test.lua +++ b/tests/engine/game3_virtual_objects_drawhook_test.lua @@ -1,8 +1,4 @@ --- rse-seams e10 spec 5.3 draw hookup: virtual objects reach the same draw pass --- as event objects (Objects.forDraw) and stay invisible to collision queries. --- pret src/event_object_movement.c:1719 CreateVirtualObject (sprite, not an object event) --- pret src/event_object_movement.c:9225 DestroyVirtualObjects (map unload) --- lua: luajit tests/engine/game3_virtual_objects_drawhook_test.lua +-- src/event_object_movement.c:1719, src/event_object_movement.c:9225 package.path = "./?.lua;./?/init.lua;" .. package.path @@ -20,8 +16,6 @@ Objects._order = {} Objects._byId = {} VirtualObjects.clear() --- 1. A spawned virtual object shows up in the draw list with the fields --- field_view reads, and stays out of the collision store. VirtualObjects.spawn(1, 40, 5, 6, 4, 1) local found for _, eo in ipairs(Objects.forDraw()) do @@ -37,7 +31,6 @@ eq(found and found.sprite, GfxIds.spriteFor(40), "sprite name reaches the non-OW eq(Objects._byId[1], nil, "the registry is not in the object store (no collision)") eq(Objects.at(5, 6), nil, "Objects.at cannot see it") --- 2. turnvobject re-emerges on the next draw with the new direction. VirtualObjects.turn(1, 2) local turned for _, eo in ipairs(Objects.forDraw()) do @@ -45,7 +38,6 @@ for _, eo in ipairs(Objects.forDraw()) do end eq(turned and turned.facing, "up", "turn(1, DIR_NORTH) shows on the next draw") --- 3. Map unload teardown empties the draw list again. VirtualObjects.clear() local gone = false for _, eo in ipairs(Objects.forDraw()) do diff --git a/tests/engine/game3_virtual_objects_test.lua b/tests/engine/game3_virtual_objects_test.lua index e28652d8..e5d35df1 100644 --- a/tests/engine/game3_virtual_objects_test.lua +++ b/tests/engine/game3_virtual_objects_test.lua @@ -1,8 +1,3 @@ --- Sections 5.3/5.4 of docs/game3/e10-opcode-spec.md: the virtual-object --- registry behind `createvobject` / `turnvobject`. Unwired (the dispatch cases --- are the Finisher's), so this suite pins the registry contract they will call. --- luajit tests/engine/game3_virtual_objects_test.lua - package.path = "./?.lua;./?/init.lua;" .. package.path local T = require("tests.harness") @@ -12,16 +7,12 @@ local VirtualObjects = require("src.core.game3.virtual_objects") VirtualObjects.reset() --- ------------------------------------------------------------ module boundary - check(package.loaded["src.core.game3.objects"] == nil, "the registry does not drag in objects.lua") check(package.loaded["src.core.game3.collision"] == nil, "the registry does not drag in collision (virtual objects never collide)") eq(VirtualObjects.DIR_SOUTH, 1, "DIR_SOUTH matches pret include/constants/global.h:110") --- ----------------------------------------------------------------- lifecycle - eq(VirtualObjects.count(), 0, "starts empty") local rec = VirtualObjects.spawn(1, 42, 6, 8, 3, 2) @@ -34,7 +25,7 @@ eq(rec.elevation, 3, "elevation stored") eq(rec.direction, 2, "direction stored") eq(VirtualObjects.count(), 1, "one live object") --- event.inc:1346 defaults (elevation=3, direction=DIR_SOUTH) +-- event.inc:1346 local dflt = VirtualObjects.spawn(2, 7, 0, 0, nil, nil) eq(dflt.elevation, 3, "default elevation is 3") eq(dflt.direction, VirtualObjects.DIR_SOUTH, "default direction is DIR_SOUTH") @@ -48,27 +39,21 @@ eq(#list, 2, "list returns both records") eq(list[1].id, 1, "list preserves spawn order (first)") eq(list[2].id, 2, "list preserves spawn order (second)") --- --------------------------------------------------------------- turn (5.4) - check(VirtualObjects.turn(1, 5) == true, "turn on a live id succeeds") eq(VirtualObjects.get(1).direction, 5, "turn updates direction") eq(VirtualObjects.get(2).direction, VirtualObjects.DIR_SOUTH, "turn on one id leaves the other alone") --- Missing id: logged no-op returning false (pret GetVirtualObjectSpriteId's --- MAX_SPRITES miss, src/event_object_movement.c:9248-9257). +-- src/event_object_movement.c:9248-9257 check(VirtualObjects.turn(99, 1) == false, "turn on a missing id is a no-op") check(VirtualObjects.turn(99, 1) == false, "still a no-op on a repeat") check(VirtualObjects.turn(nil, 1) == false, "turn(nil) is a no-op") eq(VirtualObjects.get(99), nil, "a missed turn creates nothing") --- A non-numeric direction leaves the stored direction untouched (tonumber miss). check(VirtualObjects.turn(2, "sideways") == true, "non-numeric direction is accepted") eq(VirtualObjects.get(2).direction, VirtualObjects.DIR_SOUTH, "a non-numeric direction does not clobber the stored one") --- ------------------------------------------------- replace + numeric coercion - local replaced = VirtualObjects.spawn(1, 99, 12, 14, 5, 6) check(replaced == VirtualObjects.get(1), "re-spawning an id replaces the entry") eq(VirtualObjects.count(), 2, "a replace does not duplicate the id") @@ -80,21 +65,16 @@ check(coerced ~= nil, "a numeric-string id is accepted") check(VirtualObjects.get(3) == coerced, "and resolves to the same record") check(VirtualObjects.turn("3", 4) == true, "turn accepts the same coercion") --- --------------------------------------------------------------- bad input - eq(VirtualObjects.spawn(nil, 1, 0, 0, 1, 1), nil, "a nil id is refused") eq(VirtualObjects.spawn("badge", 1, 0, 0, 1, 1), nil, "a non-numeric id is refused") eq(VirtualObjects.count(), 3, "refused spawns add nothing") --- ----------------------------------------------------------- map unload clear - VirtualObjects.clear() eq(VirtualObjects.count(), 0, "clear empties the registry (map unload)") eq(#VirtualObjects.list(), 0, "list is empty after clear") check(VirtualObjects.get(1) == nil, "records are gone") check(VirtualObjects.turn(1, 1) == false, "turn after clear is a no-op") --- An id is reusable after clear (a fresh map may spawn the same ids). local fresh = VirtualObjects.spawn(1, 5, 2, 2, 3, 1) check(fresh ~= nil, "ids are reusable after clear") eq(VirtualObjects.count(), 1, "one object after re-spawn") diff --git a/tests/engine/quit_thread_shutdown.lua b/tests/engine/quit_thread_shutdown.lua index 51ba2d7d..a3a12328 100644 --- a/tests/engine/quit_thread_shutdown.lua +++ b/tests/engine/quit_thread_shutdown.lua @@ -136,11 +136,6 @@ eq(counted(commands("chipaudio_cmd"), "quit"), 1, "shutdown is idempotent and post-shutdown calls stay quiet") eq(chipThread.waited, 1, "the joined worker is not waited on twice") --- I6 inversion: ChipAudio used to require SessionLifecycle at module load to --- register this shutdown, which closed ChipAudio -> SessionLifecycle -> --- Music/Sound -> ChipAudio. SessionLifecycle.endProcess now reaches --- ChipAudio.shutdown through package.loaded instead, so restart the worker and --- prove the quit path still joins it. local SessionLifecycle = require("src.core.SessionLifecycle") check(ChipAudio.playMusic(data, song, true) ~= nil, "playMusic restarts the chip worker after a shutdown") @@ -209,9 +204,6 @@ check(lifecycleSrc:find("registerProcessShutdown", 1, true) ~= nil, "SessionLifecycle exposes registerProcessShutdown") check(lifecycleSrc:find("function SessionLifecycle.endProcess()", 1, true) ~= nil, "SessionLifecycle.endProcess fans out registered hooks") --- I6: ChipAudio's load-time registration required SessionLifecycle and closed --- ChipAudio -> SessionLifecycle -> Music/Sound -> ChipAudio, so the join now --- lives in endProcess through package.loaded (behavioural proof above). check(source("src/core/ChipAudio.lua"):find("registerProcessShutdown", 1, true) == nil, "ChipAudio does not register at load (I6: that require closed a cycle)") check(lifecycleSrc:find('package.loaded["src.core.ChipAudio"]', 1, true) ~= nil, diff --git a/tests/fixture_data/tilesets.lua b/tests/fixture_data/tilesets.lua index f8b1a197..f8d521e3 100644 --- a/tests/fixture_data/tilesets.lua +++ b/tests/fixture_data/tilesets.lua @@ -9,10 +9,6 @@ return { FIX_OUT = { id = "FIX_OUT", image = "tests/fixture_data/assets/fix_out.png", - -- engine data contract: TileRenderer.new reads tileset.tilesPerRow for its - -- quad math (src/render/TileRenderer.lua:499-501, required since the - -- initial commit); the generated importer emits 16 (extract_island1.lua:421) - -- and the fixture PNG is 128px wide = 16 tiles of 8px. tilesPerRow = 16, blocks = { row(0), row(1), row(2), row(3) }, walkable = { [0] = true, [1] = true, [2] = true }, diff --git a/tests/game3_anim_port_g3_test.lua b/tests/game3_anim_port_g3_test.lua index e169e149..f498361f 100644 --- a/tests/game3_anim_port_g3_test.lua +++ b/tests/game3_anim_port_g3_test.lua @@ -75,8 +75,6 @@ local G3T = require("src.core.game3.battle.anim_port.g3_tasks") -- pokefirered/src/trig.c:514 check(P.Sin(64, 256) == 256 and P.Sin(192, 10) == -10 and P.Cos(0, 15) == 15, "Sin/Cos gSineTable") check(P.Sin(5, 10) == 1 and P.Cos(5, 15) == 14, "Sin/Cos truncation") --- O5: P.Cos2 has no callers and is being deleted as dead code. Cosine callers --- use the live P.Sin2(deg + 90) path, so assert that instead. check(P.Sin2(30) == 2048 and P.Sin2(270) == -4096, "Sin2 degree table (Cos2(180) path)") check(P.s16(40000) == -25536 and P.u16(-1) == 65535 and P.div(-7, 2) == -3, "C integer semantics") diff --git a/tests/game3_bag_test.lua b/tests/game3_bag_test.lua index 0ac0c930..98c96922 100644 --- a/tests/game3_bag_test.lua +++ b/tests/game3_bag_test.lua @@ -2,6 +2,7 @@ -- Game3 bag: pret ItemSlot pockets, items pack, checkitem APIs, save migrate. package.path = "./?.lua;./?/init.lua;" .. package.path +require("tests.game3_cache").requireData("game3_bag_test") local failed = 0 local function check(cond, msg) diff --git a/tests/game3_battle_ai_test.lua b/tests/game3_battle_ai_test.lua index 58f5c2f7..84c94366 100644 --- a/tests/game3_battle_ai_test.lua +++ b/tests/game3_battle_ai_test.lua @@ -2,6 +2,7 @@ -- FireRed battle AI pack + scoring VM smoke tests. package.path = "./?.lua;./?/init.lua;" .. package.path +require("tests.game3_cache").requireData("game3_battle_ai_test") local failed = 0 local function check(cond, msg) diff --git a/tests/game3_battle_anim_pack_test.lua b/tests/game3_battle_anim_pack_test.lua index fb84eb1d..d74f45bd 100644 --- a/tests/game3_battle_anim_pack_test.lua +++ b/tests/game3_battle_anim_pack_test.lua @@ -3,6 +3,7 @@ -- Run: luajit tests/game3_battle_anim_pack_test.lua package.path = "./?.lua;./?/init.lua;" .. package.path +require("tests.game3_cache").requireData("game3_battle_anim_pack_test", "pokemon/battle_anims/pack.lua") local failed = 0 local function check(cond, msg) diff --git a/tests/game3_battle_anim_palette_test.lua b/tests/game3_battle_anim_palette_test.lua index 7fd73a81..d70560b8 100644 --- a/tests/game3_battle_anim_palette_test.lua +++ b/tests/game3_battle_anim_palette_test.lua @@ -1,4 +1,5 @@ package.path = "./?.lua;./?/init.lua;" .. package.path +require("tests.game3_cache").requireData("game3_battle_anim_palette_test", "pokemon/battle_anims/pack.lua") local passed, failed = 0, 0 local function check(cond, name) diff --git a/tests/game3_battle_anims_coverage_test.lua b/tests/game3_battle_anims_coverage_test.lua index bcef4d66..89e22155 100644 --- a/tests/game3_battle_anims_coverage_test.lua +++ b/tests/game3_battle_anims_coverage_test.lua @@ -2,6 +2,7 @@ -- Verifies all 354 moves in pack.lua, testing GLSL shader integration, -- Z-index depth, nearest-neighbor scaling tasks, and spatial audio panning. +require("tests.game3_cache").requireData("game3_battle_anims_coverage_test", "pokemon/battle_anims/pack.lua") local Anim = require("src.core.game3.battle.anim") local AnimVm = require("src.core.game3.battle.anim_vm") local AnimTasks = require("src.core.game3.battle.anim_tasks") diff --git a/tests/game3_battle_anims_phase3_test.lua b/tests/game3_battle_anims_phase3_test.lua index d481d007..bba2770a 100644 --- a/tests/game3_battle_anims_phase3_test.lua +++ b/tests/game3_battle_anims_phase3_test.lua @@ -2,6 +2,7 @@ -- Unit and integration tests for Phase 3: Elemental FX, Particle Generators & Wave Systems package.path = package.path .. ";./?.lua" +require("tests.game3_cache").requireData("game3_battle_anims_phase3_test", "pokemon/battle_anims/pack.lua") local AnimTasks = require("src.core.game3.battle.anim_tasks") local AnimSprites = require("src.core.game3.battle.anim_sprites") diff --git a/tests/game3_battle_anims_phase4_test.lua b/tests/game3_battle_anims_phase4_test.lua index 0065619d..ae3a1bcf 100644 --- a/tests/game3_battle_anims_phase4_test.lua +++ b/tests/game3_battle_anims_phase4_test.lua @@ -2,6 +2,7 @@ -- Unit and integration tests for Phase 4: Dynamic Backgrounds, Clones, Distortions, Spotlights, Substitute & Evaluators package.path = package.path .. ";./?.lua" +require("tests.game3_cache").requireData("game3_battle_anims_phase4_test", "pokemon/battle_anims/pack.lua") local AnimTasks = require("src.core.game3.battle.anim_tasks") local AnimSprites = require("src.core.game3.battle.anim_sprites") diff --git a/tests/game3_battle_anims_pret_parity_test.lua b/tests/game3_battle_anims_pret_parity_test.lua index 2b8677c9..b9feacea 100644 --- a/tests/game3_battle_anims_pret_parity_test.lua +++ b/tests/game3_battle_anims_pret_parity_test.lua @@ -2,6 +2,7 @@ -- Rigorous 1:1 pret (pokefirered) parity test suite for battle animation tasks, callbacks and move scripts package.path = package.path .. ";./?.lua" +require("tests.game3_cache").requireData("game3_battle_anims_pret_parity_test", "pokemon/battle_anims/pack.lua") local AnimTasks = require("src.core.game3.battle.anim_tasks") local AnimSprites = require("src.core.game3.battle.anim_sprites") diff --git a/tests/game3_battle_baton_pass_test.lua b/tests/game3_battle_baton_pass_test.lua index d167b248..f2761809 100644 --- a/tests/game3_battle_baton_pass_test.lua +++ b/tests/game3_battle_baton_pass_test.lua @@ -1,6 +1,7 @@ #!/usr/bin/env luajit package.path = "./?.lua;./?/init.lua;" .. package.path +require("tests.game3_cache").requireData("game3_battle_baton_pass_test") package.loaded["src.core.game3.audio"] = setmetatable({}, { __index = function() return function() end end, diff --git a/tests/game3_battle_ghost_test.lua b/tests/game3_battle_ghost_test.lua index 6b434f31..ce599fec 100644 --- a/tests/game3_battle_ghost_test.lua +++ b/tests/game3_battle_ghost_test.lua @@ -1,6 +1,7 @@ #!/usr/bin/env luajit package.path = "./?.lua;./?/init.lua;" .. package.path +require("tests.game3_cache").requireData("game3_battle_ghost_test") package.loaded["src.core.game3.audio"] = setmetatable({}, { __index = function() return function() end end, diff --git a/tests/game3_battle_item_party_test.lua b/tests/game3_battle_item_party_test.lua index 7bd860e6..99e9f3df 100644 --- a/tests/game3_battle_item_party_test.lua +++ b/tests/game3_battle_item_party_test.lua @@ -9,6 +9,7 @@ -- while the battle is running. package.path = "./?.lua;./?/init.lua;" .. package.path +require("tests.game3_cache").requireData("game3_battle_item_party_test") local failed = 0 local function check(cond, msg) diff --git a/tests/game3_battle_rewards_test.lua b/tests/game3_battle_rewards_test.lua index edf686c7..9349ae23 100644 --- a/tests/game3_battle_rewards_test.lua +++ b/tests/game3_battle_rewards_test.lua @@ -2,6 +2,7 @@ -- Trainer prize money, badge white-out loss, checkitemspace for gym TMs. package.path = "./?.lua;./?/init.lua;" .. package.path +require("tests.game3_cache").requireData("game3_battle_rewards_test") local failed = 0 local function check(cond, msg) diff --git a/tests/game3_battle_safari_test.lua b/tests/game3_battle_safari_test.lua index e3aed74b..8113387a 100644 --- a/tests/game3_battle_safari_test.lua +++ b/tests/game3_battle_safari_test.lua @@ -1,6 +1,7 @@ #!/usr/bin/env luajit package.path = "./?.lua;./?/init.lua;" .. package.path +require("tests.game3_cache").requireData("game3_battle_safari_test") package.loaded["src.core.game3.audio"] = setmetatable({}, { __index = function() return function() end end, diff --git a/tests/game3_battle_special_moves_test.lua b/tests/game3_battle_special_moves_test.lua index 5d3b2f7a..fae616a0 100644 --- a/tests/game3_battle_special_moves_test.lua +++ b/tests/game3_battle_special_moves_test.lua @@ -3,6 +3,7 @@ -- Run: luajit tests/game3_battle_special_moves_test.lua package.path = "./?.lua;./?/init.lua;" .. package.path +require("tests.game3_cache").requireData("game3_battle_special_moves_test") local State = require("src.core.game3.battle.state") local Engine = require("src.core.game3.battle.engine") @@ -712,11 +713,7 @@ do end do - -- Rapid Spin and each binding state. pret frees exactly ONE per use: - -- pokefirered/src/battle_script_commands.c:8435-8474 Cmd_rapidspinfree is a - -- single if/else-if chain in the order - -- if (STATUS2_WRAPPED) ... else if (LEECHSEED) ... else if (SIDE_STATUS_SPIKES) ... - -- so an engine that clears all three at once contradicted pret. + -- pokefirered/src/battle_script_commands.c:8435 local spinId = Moves.numForName("RAPID_SPIN") local st = State.new({ wild = true, @@ -728,25 +725,26 @@ do st.playerSide.spikes = 1 st.player.leechSeed = true st.player.trapped = true + st.player.expTrapTurns = 3 + st.player.expTrapSource = st.enemy + st.player.expTrapMoveName = "WRAP" local ad = setup_test_battle(st) local out = {} - -- Use 1: no expTrapTurns (the engine's wrap state), so the chain lands on - -- LEECHSEED and Spikes are untouched. Engine.resolveMove(st.player, st.enemy, spinId, 1, ad, st, out) + check(st.player.expTrapTurns == nil, "Rapid Spin freed the wrap") check(st.player.leechSeed == nil, "Rapid Spin removed Leech Seed") check(st.player.trapped == nil, "Rapid Spin removed trapping effect") - check(st.playerSide.spikes == 1, "Spikes survive the same use (pret frees exactly one)") + check(st.playerSide.spikes == 0, "Rapid Spin removed Spikes in the same use") local joined = table.concat(out, " || ") - check(joined:find("blew away\nSPIKES!", 1, true) == nil, - "no Spikes message on the use that freed Leech Seed") - - -- Use 2: the chain now falls through to SIDE_STATUS_SPIKES. - local out2 = {} - Engine.resolveMove(st.player, st.enemy, spinId, 2, ad, st, out2) - check(st.playerSide.spikes == 0, "Rapid Spin removed Spikes from side") - local joined2 = table.concat(out2, " || ") - check(joined2:find("blew away\nSPIKES!", 1, true) ~= nil, "Spikes blown away message printed") + local iWrap = joined:find("got free of", 1, true) + local iSeed = joined:find("shed\nLEECH SEED!", 1, true) + local iSpikes = joined:find("blew away\nSPIKES!", 1, true) + check(iWrap ~= nil, "Wrap freed message printed") + check(iSeed ~= nil, "Leech Seed shed message printed") + check(iSpikes ~= nil, "Spikes blown away message printed") + check(iWrap and iSeed and iSpikes and iWrap < iSeed and iSeed < iSpikes, + "Rapid Spin frees wrap, then Leech Seed, then Spikes") end print("\n=== 8. Two-Turn Charging, Semi-Invulnerable, and Recharge (Solar Beam, Skull Bash, Fly, Hyper Beam) ===") diff --git a/tests/game3_battle_status_timing_test.lua b/tests/game3_battle_status_timing_test.lua index e200cd1b..fef122f2 100644 --- a/tests/game3_battle_status_timing_test.lua +++ b/tests/game3_battle_status_timing_test.lua @@ -1,5 +1,6 @@ -- Comprehensive unit & integration tests for Game 3 battle status effect & residual timing. +require("tests.game3_cache").requireData("game3_battle_status_timing_test") local Battle = require("src.core.game3.battle.init") local State = require("src.core.game3.battle.state") local Engine = require("src.core.game3.battle.engine") diff --git a/tests/game3_cache.lua b/tests/game3_cache.lua index 47f6ef8a..9c760179 100644 --- a/tests/game3_cache.lua +++ b/tests/game3_cache.lua @@ -149,6 +149,29 @@ function M.mountOrSkip(label, marker, opts) return root end +local function datasetRoots() + local roots = { "." } + local home = os.getenv("HOME") + local identity = os.getenv("POKEPORT_IDENTITY") or "" + if home and identity ~= "" then + roots[#roots + 1] = home .. "/Library/Application Support/LOVE/" .. identity .. "/firered" + roots[#roots + 1] = home .. "/.local/share/love/" .. identity .. "/firered" + elseif home then + roots[#roots + 1] = home .. "/.local/share/love/" .. OWNER_IDENTITY .. "/firered" + end + return roots +end + +function M.requireData(label, marker) + marker = marker or "meta.json" + if M.root(marker) then return end + for _, root in ipairs(datasetRoots()) do + if readable(root .. "/data/generated/gba/" .. marker) then return end + end + print("[skip] " .. tostring(label) .. ": " .. tostring(M.reason or "no imported FireRed cache found")) + os.exit(0) +end + function M.bundle(marker, opts) local root = M.mount(marker, opts) if not root then return nil end diff --git a/tests/game3_daycare_model_test.lua b/tests/game3_daycare_model_test.lua index d2cb7239..e1c7d8ce 100644 --- a/tests/game3_daycare_model_test.lua +++ b/tests/game3_daycare_model_test.lua @@ -251,21 +251,18 @@ local slot6 = session.party[6] ctx = newCtx() setVar(ctx, VAR_0x8004, 0) local _, refused = Natives.special(ctx, Std.SPECIAL.TakePokemonFromDaycare, nil) --- pret guards party-full in the script layer --- (data/maps/FourIsland_PokemonDayCare/scripts.inc:86-88); the special is this --- engine's mirror of that guard, so a withdrawal never lands. +-- data/maps/FourIsland_PokemonDayCare/scripts.inc:86-88 eq(refused, 0, "a full party refuses the withdrawal (SPECIES_NONE)") eq(#session.party, 6, "the party is still six mons") eq(session.party[6], slot6, "slot 6 still holds its own mon") eq(Daycare.mon(dc, 1), second, "and the mon stays in the daycare") --- src/daycare.c:525 stays index-assign unguarded by design, so pin that write --- through the model layer directly. +-- src/daycare.c:525 eq(Daycare.take(session, 1), 16, "the model still withdraws the mon") eq(#session.party, 6, "the party never grows past six") eq(session.party[6], second, "pret's gPlayerParty[PARTY_SIZE - 1] write holds the mon") print("[test] 9b. Route 5 refuses the withdrawal on a full party too") --- pret data/scripts/day_care.inc:79-81 guards the Route 5 retrieve the same way. +-- data/scripts/day_care.inc:79-81 r5 = Daycare.route5Of(session) r5.mon = makeMon(19, 5) session.party = {} diff --git a/tests/game3_encounters_lookup_test.lua b/tests/game3_encounters_lookup_test.lua index 0cc3dd8b..fdf17d63 100644 --- a/tests/game3_encounters_lookup_test.lua +++ b/tests/game3_encounters_lookup_test.lua @@ -2,6 +2,7 @@ -- Test wild encounter table resolution and rolls across multiple map ID alias formats. package.path = "./?.lua;./?/init.lua;" .. package.path +require("tests.game3_cache").requireData("game3_encounters_lookup_test") local failed = 0 local function check(cond, msg) diff --git a/tests/game3_fame_screen_test.lua b/tests/game3_fame_screen_test.lua index 55558c3a..d661f1b4 100644 --- a/tests/game3_fame_screen_test.lua +++ b/tests/game3_fame_screen_test.lua @@ -136,7 +136,7 @@ do eq(#bytes, 64 * 64 * 4, "it is a 64x64 RGBA sprite") end - -- pokefirered/src/fame_checker.c:1347 sDaisySpriteTemplate + -- pokefirered/src/fame_checker.c:1347 for _, p in ipairs({ PERSON.OAK, PERSON.DAISY, PERSON.BILL, PERSON.MRFUJI }) do local src2, path2 = Ui.portraitSource(p) eq(src2, "art", "person " .. p .. " uses the Fame Checker's own art") diff --git a/tests/game3_gift_model_test.lua b/tests/game3_gift_model_test.lua index 38d8b4bd..e21767be 100644 --- a/tests/game3_gift_model_test.lua +++ b/tests/game3_gift_model_test.lua @@ -1,6 +1,7 @@ #!/usr/bin/env luajit package.path = "./?.lua;./?/init.lua;" .. package.path +require("tests.game3_cache").requireData("game3_gift_model_test") local failed = 0 local function check(cond, msg) diff --git a/tests/game3_item_use_and_parcel_test.lua b/tests/game3_item_use_and_parcel_test.lua index c36cef4c..628a2f14 100644 --- a/tests/game3_item_use_and_parcel_test.lua +++ b/tests/game3_item_use_and_parcel_test.lua @@ -1,5 +1,6 @@ -- Test suite for ItemUse and Oak's Lab parcel delivery scene. +require("tests.game3_cache").requireData("game3_item_use_and_parcel_test") local GameVersion = require("src.core.GameVersion") GameVersion.set("firered") local ItemUse = require("src.core.game3.item_use") diff --git a/tests/game3_item_use_party_test.lua b/tests/game3_item_use_party_test.lua index 20a3eb16..191a010a 100644 --- a/tests/game3_item_use_party_test.lua +++ b/tests/game3_item_use_party_test.lua @@ -2,6 +2,7 @@ -- Gen 3 Party Item Use, TM Confirmation & Evolution Chaining Test Suite package.path = "./?.lua;./?/init.lua;" .. package.path +require("tests.game3_cache").requireData("game3_item_use_party_test") local failed = 0 local function check(cond, msg) diff --git a/tests/game3_link_battle_test.lua b/tests/game3_link_battle_test.lua index c8dc9a38..3d10bce2 100644 --- a/tests/game3_link_battle_test.lua +++ b/tests/game3_link_battle_test.lua @@ -186,7 +186,7 @@ Link.reset() freshCtx() setVar(Link.VAR_0x8004, Link.USING.SINGLE_BATTLE) local waited = Natives.special(ctx, NativesLink.SPECIAL.TryBattleLinkup, adapters) --- pokefirered/src/cable_club.c:208-222 Task_LinkupAwaitConnection waits for the other machine +-- pokefirered/src/cable_club.c:208-222 check(waited, "with no cable yet the counter parks the script instead of answering") eq(getVar(Link.VAR_RESULT), Link.LINKUP.ONGOING, "and reports LINKUP_ONGOING while it waits") local spun = 0 diff --git a/tests/game3_link_session_test.lua b/tests/game3_link_session_test.lua index 3ca2c4b2..bbae69be 100644 --- a/tests/game3_link_session_test.lua +++ b/tests/game3_link_session_test.lua @@ -43,9 +43,6 @@ local session = { party = {}, bag = { pockets = { items = { { id = 4, qty = 3 } } } }, } --- G1 contract (tests/engine/game3_save_menu_failure_test.lua): the save menu --- reports success only when saveGame confirms the write (do_save reads --- Runtime._game.saveGame), so this stub game carries one like the real game. local game = { data = { maps = MAPS }, session = session, saveGame = function() return true end } diff --git a/tests/game3_marowak_progression_test.lua b/tests/game3_marowak_progression_test.lua index afe26852..2828712e 100644 --- a/tests/game3_marowak_progression_test.lua +++ b/tests/game3_marowak_progression_test.lua @@ -1,4 +1,5 @@ package.path = "./?.lua;./?/init.lua;" .. package.path +require("tests.game3_cache").requireData("game3_marowak_progression_test") require("src.core.GameVersion").set("firered") local Ctx = require("src.core.game3.scripting.ctx") local Flags = require("src.core.game3.scripting.flags") diff --git a/tests/game3_move_names_test.lua b/tests/game3_move_names_test.lua index e7691e41..66a71511 100644 --- a/tests/game3_move_names_test.lua +++ b/tests/game3_move_names_test.lua @@ -1,5 +1,6 @@ -- Unit tests for robust Move Name resolution across numbers, strings, and wrapped tables. +require("tests.game3_cache").requireData("game3_move_names_test") local Moves = require("src.core.game3.battle.moves") local Pokemon = require("src.core.game3.pokemon") local PartyView = require("src.core.game3.battle.party_view") diff --git a/tests/game3_moveset_assignment_test.lua b/tests/game3_moveset_assignment_test.lua index 44b91c80..bc963da5 100644 --- a/tests/game3_moveset_assignment_test.lua +++ b/tests/game3_moveset_assignment_test.lua @@ -2,6 +2,7 @@ -- Test FireRed Pokémon moveset assignment, learnsets, and battle initialization. package.path = "./?.lua;./?/init.lua;" .. package.path +require("tests.game3_cache").requireData("game3_moveset_assignment_test") local Pokemon = require("src.core.game3.pokemon") local Battle = require("src.core.game3.battle.init") diff --git a/tests/game3_national_dex_test.lua b/tests/game3_national_dex_test.lua index d29ff8a7..c5718123 100644 --- a/tests/game3_national_dex_test.lua +++ b/tests/game3_national_dex_test.lua @@ -1,5 +1,6 @@ -- Tests for 1:1 National Pokédex Gating, Systems, and Story Triggers matching pret pokefirered. +require("tests.game3_cache").requireData("game3_national_dex_test") local Dex = require("src.core.game3.dex") local PokedexData = require("src.core.game3.pokedex_data") local Evolution = require("src.core.game3.evolution") diff --git a/tests/game3_nickname_test.lua b/tests/game3_nickname_test.lua index ac210109..e125ed8d 100644 --- a/tests/game3_nickname_test.lua +++ b/tests/game3_nickname_test.lua @@ -300,6 +300,49 @@ for _, line in ipairs(BattleUi._log or {}) do end check(not foundTransfer, "trygivecaughtmonnick jumps past printfromtable gCaughtMonStringIds") +print("[test] 9. species name install retries until the cache is mounted") +do + local Pokemon = require("src.core.game3.pokemon") + local saved = { install = Pokemon.install, name = Pokemon.name, names = Pokemon._names, + warned = Pokemon._installWarned } + local rt = package.loaded["src.core.game3.runtime"] + local bare = { species = 25, nickname = "" } + package.loaded["src.core.game3.runtime"] = { + getSession = function() return { party = { bare } } end, + isActive = function() return true end, + } + local attempts, mounted = 0, false + Pokemon._names, Pokemon._installWarned = nil, nil + Pokemon.install = function() + attempts = attempts + 1 + if not mounted then error("cache not mounted") end + Pokemon._names = { [25] = "PIKACHU" } + end + Pokemon.name = function(sp) return Pokemon._names and Pokemon._names[sp] or "" end + local realPrint, printed = print, 0 + print = function(msg) + if tostring(msg):find("install failed", 1, true) then printed = printed + 1 else realPrint(msg) end + end + local out = {} + local function buffer() + local ctx = { specialVars = { [0x8004] = 0 }, stringVars = {} } + Natives.ALLOW["special:124"](ctx, { setStringVar = function(i, t) out[i] = t end }) + return out[1] + end + local first = buffer() + local second = buffer() + mounted = true + local third = buffer() + print = realPrint + check(first == "" and second == "", "no name while the cache is unmounted") + check(third == "PIKACHU", "the name loads once the cache mounts") + check(attempts == 3, "every lookup retries the install until it succeeds") + check(printed == 1, "the install failure is logged once") + Pokemon.install, Pokemon.name = saved.install, saved.name + Pokemon._names, Pokemon._installWarned = saved.names, saved.warned + package.loaded["src.core.game3.runtime"] = rt +end + if failed == 0 then print("\nAll game3 nickname tests passed.") os.exit(0) diff --git a/tests/game3_oak_first_battle_test.lua b/tests/game3_oak_first_battle_test.lua index 07513aef..0822e555 100644 --- a/tests/game3_oak_first_battle_test.lua +++ b/tests/game3_oak_first_battle_test.lua @@ -1,6 +1,7 @@ #!/usr/bin/env luajit package.path = "./?.lua;./?/init.lua;" .. package.path +require("tests.game3_cache").requireData("game3_oak_first_battle_test") local failed = 0 local function check(cond, msg) diff --git a/tests/game3_object_interactions_cache_test.lua b/tests/game3_object_interactions_cache_test.lua index 5ef62237..cbb1d651 100644 --- a/tests/game3_object_interactions_cache_test.lua +++ b/tests/game3_object_interactions_cache_test.lua @@ -77,10 +77,7 @@ for _,map in ipairs(order) do for _=1,100 do Space.vm:tick()end if SCREEN_ONLY[key] then assert(#messages==0,'unexpected text from '..key) - -- pokefirered/data/scripts/cable_club.inc:566-575 + - -- pokefirered/src/battle_records.c:83: special ShowBattleRecords + waitstate - -- parks until the player closes the screen, and releaseall runs only after - -- the special returns - so control must NOT release while the screen is up. + -- pokefirered/data/scripts/cable_club.inc:566-575, pokefirered/src/battle_records.c:83 assert(Space.vm:isRunning(),'parks on the waitstate with the records screen up') local Records=require('src.ui.game3.trainer_tower_records') local Fade=require('src.ui.game3.fade') diff --git a/tests/game3_object_subpriority_draworder_test.lua b/tests/game3_object_subpriority_draworder_test.lua index e1c5cec4..8a6698a9 100644 --- a/tests/game3_object_subpriority_draworder_test.lua +++ b/tests/game3_object_subpriority_draworder_test.lua @@ -1,24 +1,4 @@ --- tests/game3_object_subpriority_draworder_test.lua --- Seam 7 consumer-half contract (docs/game3/e10-opcode-spec.md §5.8): --- 1. set -> Objects.setSubpriority stores {fixedPriority, subpriority}; the --- script op applies the bias (prett scrcmd.c:1130 `priority + 83`). --- 2. draw -> field_view.applyDrawOrder captures the dynamic class once --- (fixedClass), keeps it across elevation changes, and sorts the frozen --- actor by subpriority instead of pixel-Y. --- 3. reset-> the record clears, no stale fixedClass survives, and the --- elevation-driven dynamic path resumes exactly as before. --- 4. an unfrozen neighbour still follows ELEVATION_TO_PRIORITY. --- --- pret anchors (clone /Users/shanemcgovern/dev/pokefirered, HEAD c75f35230): --- src/event_object_movement.c:8379-8387 UpdateObjectEventElevationAndPriority --- -> `if (objEvent->fixedPriority) return;` --- src/event_object_movement.c:8424-8429 ObjectEventUpdateSubpriority --- -> same early return (gates SetObjectSubpriorityByElevation :8414-8422) --- src/event_object_movement.c:2089-2101 SetObjectSubpriority --- -> `fixedPriority = TRUE; sprite->subpriority = subpriority` --- src/event_object_movement.c:2104-2116 ResetObjectSubpriority --- -> `fixedPriority = FALSE` (re-enables the dynamic path; does not restore) --- src/scrcmd.c:1122-1140 set/resetobjectsubpriority (`priority + 83`). +-- scrcmd.c:1130, src/event_object_movement.c:8379-8387, src/event_object_movement.c:8424-8429, src/event_object_movement.c:2089-2101, src/event_object_movement.c:2104-2116, src/scrcmd.c:1122-1140 local Objects = require("src.core.game3.objects") local FieldView = require("src.core.game3.field_view") @@ -42,12 +22,11 @@ local function done() os.exit(0) end --- Current-map live EventObject in the store the producer resolves from. local eo = { localId = 3, def = { x = 4, y = 4, elevation = 0 }, cellX = 4, cellY = 4, px = 4 * 16, py = 4 * 16, - elevation = 0, -- class 2 (ground) per ELEVATION_TO_PRIORITY[0] + elevation = 0, } Objects._byId[3] = eo @@ -74,7 +53,7 @@ end print("[test] 2. set stores the record with the +83 script bias") do - local scriptPriority = 0 -- script byte; pret adds 83 in scrcmd.c:1130 + local scriptPriority = 0 -- scrcmd.c:1130 local ok = Objects.setSubpriority(3, nil, nil, scriptPriority + 83) check(ok == true, "setSubpriority resolves the current-map object") check(eo.fixedPriority == true, "fixedPriority flag set") @@ -91,7 +70,7 @@ end print("[test] 4. freeze holds across an elevation change (pret early-return)") do - eo.elevation = 13 -- would be class 0 if the dynamic writer ran + eo.elevation = 13 local under, over = FieldView.applyDrawOrder({ makeActor(eo) }) check(under[1].priority == 2, "priority stays at fixedClass (2), not ELEVATION_TO_PRIORITY[13]=0") check(eo.fixedClass == 2, "fixedClass captured once, not re-derived") @@ -102,7 +81,7 @@ print("[test] 5. frozen actor sorts by subpriority; neighbour keeps pixel-Y") do local neighbour = { kind = "npc", i = 9, obj = { elevation = 3 }, elevation = 3, - x = 16, y = 100, sortY = 100, -- pixel-Y 100 > subpriority 83 + x = 16, y = 100, sortY = 100, } local under = FieldView.applyDrawOrder({ neighbour, makeActor(eo) }) check(#under == 2, "both actors share the under list") @@ -112,7 +91,7 @@ do local near = { kind = "npc", i = 8, obj = { elevation = 3 }, elevation = 3, - x = 32, y = 10, sortY = 10, -- pixel-Y 10 < subpriority 83 + x = 32, y = 10, sortY = 10, } local under2 = FieldView.applyDrawOrder({ near, makeActor(eo) }) check(under2[1] == near, "pixel-Y 10 still sorts before subpriority 83") @@ -132,7 +111,6 @@ do check(over[1].subpriority == nil, "sort key back to pixel-Y") check(eo.fixedClass == nil, "no stale fixedClass survives the reset") - -- a later re-freeze must re-capture from the current elevation, not the old one Objects.setSubpriority(3, nil, nil, 5 + 83) local under2, over2 = FieldView.applyDrawOrder({ makeActor(eo) }) check(#under2 == 0, "fresh capture at class 0 keeps the actor out of the under list") diff --git a/tests/game3_oneoff_specials_test.lua b/tests/game3_oneoff_specials_test.lua index 9bf1f7cc..2a47153f 100644 --- a/tests/game3_oneoff_specials_test.lua +++ b/tests/game3_oneoff_specials_test.lua @@ -5,7 +5,6 @@ -- 4. StickerManGetBragFlags (0x168) -- 5. UpdateTrainerCardPhotoIcons (0x167) -- 6. SeafoamIslandsB4F_CurrentDumpsPlayerOnLand (0x15C) --- 7. Sign walk-away: DisableMsgBoxWalkaway (0x171) + Events.pollWalkaway local Std = require("src.core.game3.scripting.stdscripts") local Natives = require("src.core.game3.scripting.natives") @@ -111,11 +110,8 @@ end print("=== 4. StickerManGetBragFlags (0x168) ===") do local session = Schema.newGame({ name = "RED" }) - -- Numeric game stats are authoritative (game_stat.h: ENTERED_HOF=10, - -- HATCHED_EGGS=13, LINK_BATTLE_WINS=23 — 24 would be LINK_BATTLE_LOSSES). - -- The session-field fallbacks deliberately disagree so the assertions fail - -- if the reader falls back to them or uses the wrong index. - session.gameStats = { [10] = 12, [13] = 70000, [23] = 5 } -- eggs > 0xFFFF + -- game_stat.h + session.gameStats = { [10] = 12, [13] = 70000, [23] = 5 } session.hofClears = 99 session.eggsHatched = 1 session.linkBattleWins = 1 @@ -246,31 +242,27 @@ do isDown = function(_, k) return k == "up" end, } - -- Arm the sign walk-away with the player facing up (DIR_NORTH = 2). Natives.special(ctx, Std.SPECIAL.SetWalkingIntoSignVars) ctx.messageOpen = true - ctx.specialVars[0x800C] = 2 -- VAR_FACING = up + ctx.specialVars[0x800C] = 2 - -- Inhibit window: the timer counts down, nothing is cancelled yet. for _ = 1, 6 do Events.pollWalkaway(vm, inputDown) end checkEq(ctx.walkAwayFromSignInhibitTimer, 0, "inhibit timer counts down to 0") checkEq(halted, false, "no cancel while the inhibit window runs") checkEq(closed, 0, "message stays open during the inhibit window") - -- D-pad away from facing after the window: EventScript_CancelMessageBox. Events.pollWalkaway(vm, inputDown) checkEq(closed, 1, "walkaway closes the sign message") check(halted == true, "walkaway aborts the script (release + end)") checkEq(ctx.walkAwayFromSignInhibitTimer, nil, "walkaway state cleared on cancel") - -- State from a script that ended without cancelling is dropped. vm.isRunning = function() return false end Natives.special(ctx, Std.SPECIAL.SetWalkingIntoSignVars) Events.pollWalkaway(vm, inputUp) checkEq(ctx.walkAwayFromSignInhibitTimer, nil, "state cleared once the script stops") checkEq(session.msgBoxIsCancelable, nil, "...on ctx and session both") - -- DisableMsgBoxWalkaway blocks the cancel (script.c:245). + -- script.c:245 vm.isRunning = function() return true end halted, closed = false, 0 Natives.special(ctx, Std.SPECIAL.SetWalkingIntoSignVars) @@ -285,7 +277,6 @@ do checkEq(halted, false, "disabled walkaway never cancels") checkEq(closed, 0, "message stays open when walkaway is disabled") - -- D-pad into the facing direction never cancels. halted, closed = false, 0 Natives.special(ctx, Std.SPECIAL.SetWalkingIntoSignVars) ctx.messageOpen = true diff --git a/tests/game3_ops_vars_test.lua b/tests/game3_ops_vars_test.lua index b03e803a..fa8f1833 100644 --- a/tests/game3_ops_vars_test.lua +++ b/tests/game3_ops_vars_test.lua @@ -74,7 +74,7 @@ eq(Flags.getVar(store, vm.ctx, VAR_TEMP_1), 7, "subvar VAR_TEMP_1, VAR_TEMP_2 su print("[test] E9: incrementgamestat / checkpartymove implement pret") local prevSess = Runtime.session Runtime.session = { gameStats = {}, party = {} } --- pokefirered/src/scrcmd.c:576-579 → overworld.c:366-375 +-- pokefirered/src/scrcmd.c:576-579, overworld.c:366-375 vm, store = run({ t = { { op = "incrementgamestat", [1] = 13 }, @@ -93,9 +93,7 @@ vm, store = run({ }, "t") eq(Runtime.session.gameStats[13], 0xFFFFFF, "the stat saturates at 0xFFFFFF (overworld.c:371-374)") --- pokefirered/src/scrcmd.c:1777-1795: first non-egg mon knowing the move. --- NOTE: specialVars are wiped at halt (vm.lua:100), so mirror test 5 and --- copyvar the results into TEMP vars before { op = "end" }. +-- pokefirered/src/scrcmd.c:1777-1795 Runtime.session.party = { { species = 1, moves = { 0, 0, 0, 0 } }, { species = 4, moves = { { id = 15 } }, isEgg = true }, @@ -325,6 +323,32 @@ eq(vm.ctx.stringVars[3], "BOX 14", "box id 13 buffers BOX 14 into STR_VAR_3") Runtime.session = prevSession +print("[test] 10. warp x/y VarGet returns non-var ids literally") +local function warpArgs(x, y, seed) + local st = Flags.newStore() + for id, v in pairs(seed or {}) do Flags.setVar(st, nil, id, v) end + local got + local adapters = Adapters.host(nil, nil, nil) + adapters.warp = function(g, n, w, wx, wy, cb) got = { g, n, w, wx, wy }; if cb then cb() end end + local v = Vm.new({ store = st, scripts = { + t = { { op = "warp", [1] = 3, [2] = 5, [3] = 1, [4] = x, [5] = y }, { op = "end" } }, + }, adapters = adapters }) + v:start("t") + return got or {} +end +local w = warpArgs(0xFFFF, 0xFFFF) +eq(w[4], 0xFFFF, "id-only warp keeps x = 0xFFFF") +eq(w[5], 0xFFFF, "id-only warp keeps y = 0xFFFF") +w = warpArgs(7, 9) +eq(w[4], 7, "a literal x below VARS_START passes through") +eq(w[5], 9, "a literal y below VARS_START passes through") +w = warpArgs(VAR_TEMP_1, 0x40FF, { [VAR_TEMP_1] = 12, [0x40FF] = 4 }) +eq(w[4], 12, "x in the save var range is read") +eq(w[5], 4, "VARS_END is still a var") +w = warpArgs(0x4100, 0x8015) +eq(w[4], 0x4100, "an id past VARS_END is a literal") +eq(w[5], 0x8015, "an id past SPECIAL_VARS_END is a literal") + if failed > 0 then print("[test] FAILED " .. failed) os.exit(1) diff --git a/tests/game3_photo_tint_boundary_test.lua b/tests/game3_photo_tint_boundary_test.lua index 54c8a8ef..6d751efd 100644 --- a/tests/game3_photo_tint_boundary_test.lua +++ b/tests/game3_photo_tint_boundary_test.lua @@ -1,24 +1,4 @@ --- BUG2 regression: Game Corner photo tint (VAR_0x8004 -> special 0x167 -> --- store 0x4042) must survive the message/waitmessage/delay yield boundary, and --- the tint MULTICHOICE (listId 2 = MULTICHOICE_TRAINER_CARD_ICON_TINT) must --- offer all four cart options even when the extract cache is missing. --- --- Root cause locked by this test (NOT a Ctx.wipeSpecial mid-run wipe): --- * the extracted photo chain is ONE vm run (goto = same-run jump, --- ops_a.lua jump()); wipeSpecial only runs at Vm:start — before the --- multichoice setvar — and at haltCleanup — after the special. --- * when scripts/multichoice.lua is absent from the resolved cache root --- (stale pre-extractor extracts predate commit aed3bbce, Sep 19), --- Multichoice.resolve fell back to countHint — and adapters.multichoice --- passed row[4] as that hint. For a plain `multichoice` row, row[4] is --- ignoreBPress (=1), so resolve synthesized a ONE-option menu --- ("OPTION 0") -> VAR_RESULT always 0 -> setvar VAR_0x8004, 0 --- (MON_ICON_TINT_NORMAL) -> special 0x167 writes tint 0 = untinted. --- --- Rows below are transcribed from the extracted bundle --- (data/generated/gba/scripts/scripts.lua: g3:081b2867 tail, g3:081b28db-e6- --- f1-fc, g3:081b2907) which mirrors pokefirered data/scripts/trainer_card.inc --- + data/maps/CeruleanCity_GameCorner/scripts.inc (money preamble trimmed). +-- data/scripts/trainer_card.inc, data/maps/CeruleanCity_GameCorner/scripts.inc package.path = "./?.lua;./?/init.lua;" .. package.path @@ -46,7 +26,9 @@ end local Multichoice = require("src.core.game3.scripting.multichoice") print("=== 1. cart list counts survive a missing extract cache ===") -Multichoice.LISTS = {} -- headless: no scripts/multichoice.lua on any root +Multichoice.LISTS = {} +local realTryLoad = Multichoice.tryLoadCache +Multichoice.tryLoadCache = function() return false end checkEq(#Multichoice.resolve(2), 4, "MULTICHOICE_TRAINER_CARD_ICON_TINT (list 2) has 4 tints without the cache") checkEq(#Multichoice.resolve(2, 1), 4, @@ -57,6 +39,8 @@ checkEq(#Multichoice.resolve(4242, 3), 3, check(string.format("%s", Multichoice.resolve(2)[3]):find("OPTION") ~= nil, "cache-miss labels stay synthetic in order (position 3 = PINK slot)") +Multichoice.tryLoadCache = realTryLoad + print("=== 2. adapters.multichoice never reads row[4] as an option count ===") do local captured @@ -68,8 +52,6 @@ do } local Adapters = require("src.core.game3.scripting.adapters") local adapters = Adapters.host(nil, nil, nil) - -- pret: multichoice 21, 0, MULTICHOICE_TRAINER_CARD_ICON_TINT, TRUE - -- -> [1]=x [2]=y [3]=listId [4]=ignoreBPress (=1 here) adapters.multichoice({ op = "multichoice", [1] = 21, [2] = 0, [3] = 2, [4] = 1 }, function() end) checkEq(captured, 3, @@ -86,13 +68,12 @@ do local Schema = require("src.core.game3.save_schema_firered") local session = Schema.newGame({ name = "RED" }) - session.party = { { speciesId = 4, species = 4 } } -- one Charmander + session.party = { { speciesId = 4, species = 4 } } local store = Flags.newStore() package.loaded["src.core.game3.scripting.space"] = { store = store } package.loaded["src.core.game3.runtime"] = { getSession = function() return session end } local scripts = { - -- g3:081b2867 tail: choice -> copyvar VAR_0x8000, VAR_RESULT -> case chain picker = { { op = "multichoice", [1] = 21, [2] = 0, [3] = 2, [4] = 1 }, { op = "copyvar", [1] = 0x8000, [2] = 0x800D }, @@ -106,7 +87,6 @@ do { op = "goto_if", cond = 1, [1] = 1, [2] = 0, target = "tint3" }, { op = "end" }, }, - -- g3:081b28db / :28e6 / :28f1 / :28fc — setvar VAR_0x8004, tint; goto photo tint0 = { { op = "setvar", [1] = 0x8004, [2] = 0, var = 0x8004, value = 0 }, { op = "goto", target = "photo" }, @@ -127,8 +107,6 @@ do { op = "goto", target = "photo" }, { op = "end" }, }, - -- g3:081b2907 EventScript_PrintPhoto: message/waitmessage -> delay 60 - -- -> special UpdateTrainerCardPhotoIcons (0x167) -> releaseall -> end photo = { { op = "lockall" }, { op = "message", ptr = "txt_smile" }, @@ -144,7 +122,6 @@ do } local adapters = Adapters.stub({ lookupText = function() return nil end }) - -- User picks PINK (index 2 of NORMAL/BLACK/PINK/SEPIA). adapters.multichoice = function(row, cb) cb(2) end adapters.playSe = function() end @@ -156,8 +133,6 @@ do vm:tick() end check(not vm:isRunning(), "the photo script runs to completion (releaseall + end)") - -- The special's store write IS the boundary proof: a wipe between the - -- setvar and the special would leave VAR_0x8004 = 0 -> store tint 0. checkEq(Flags.getVar(store, nil, 0x4042), 2, "store 0x4042 tint = PINK (2) after the yield boundary") checkEq(Flags.getVar(store, nil, 0x4043), 4, diff --git a/tests/game3_pokedex_and_catch_test.lua b/tests/game3_pokedex_and_catch_test.lua index 69e75f4b..5f4deb9d 100644 --- a/tests/game3_pokedex_and_catch_test.lua +++ b/tests/game3_pokedex_and_catch_test.lua @@ -1,5 +1,6 @@ -- Comprehensive test suite for Game 3 Pokédex tracking, catch mechanics, and UI. +require("tests.game3_cache").requireData("game3_pokedex_and_catch_test") local GameVersion = require("src.core.GameVersion") GameVersion.set("firered") diff --git a/tests/game3_pokedex_card_chrome_test.lua b/tests/game3_pokedex_card_chrome_test.lua index d060f12e..124e5c7b 100644 --- a/tests/game3_pokedex_card_chrome_test.lua +++ b/tests/game3_pokedex_card_chrome_test.lua @@ -1,6 +1,7 @@ #!/usr/bin/env luajit package.path = "./?.lua;./?/init.lua;" .. package.path +require("tests.game3_cache").requireData("game3_pokedex_card_chrome_test") local failed = 0 local function check(cond, msg) diff --git a/tests/game3_pokedex_sorting_test.lua b/tests/game3_pokedex_sorting_test.lua index 58368e7e..e44b532d 100644 --- a/tests/game3_pokedex_sorting_test.lua +++ b/tests/game3_pokedex_sorting_test.lua @@ -1,4 +1,5 @@ -- Tests for 1:1 Pret Pokédex Ordering, Filtering, and Habitat Presentation. +require("tests.game3_cache").requireData("game3_pokedex_sorting_test") local Dex = require("src.core.game3.dex") local PokedexData = require("src.core.game3.pokedex_data") diff --git a/tests/game3_rng_test.lua b/tests/game3_rng_test.lua index 02dc6777..05147621 100644 --- a/tests/game3_rng_test.lua +++ b/tests/game3_rng_test.lua @@ -48,7 +48,6 @@ Rng.Random() Rng.Random() Rng.setState(st) eq(Rng.getState().value, st.value, "setState restores value") --- review-v3 F8: partial RNG states are rejected with no mutation. eq(Rng.setState({ value = 123 }), false, "setState rejects a partial state") eq(Rng.getState().value, st.value, "and leaves the value untouched") eq(Rng.restoreFromSession({ rng = { value = 1 } }), false, diff --git a/tests/game3_save_legacy_pc_migration_test.lua b/tests/game3_save_legacy_pc_migration_test.lua index cc2f2eec..2d138fb1 100644 --- a/tests/game3_save_legacy_pc_migration_test.lua +++ b/tests/game3_save_legacy_pc_migration_test.lua @@ -1,10 +1,3 @@ --- tests/game3_save_legacy_pc_migration_test.lua --- V10 (carve-4): the schema KEEPS reading save.pc / save.pcItems / save.pc_items --- even though toSaveTable never writes them — they are real legacy/converted-save --- keys (src/save_convert/GenSave.lua:67 writes pcItems when converting a gen1 --- save) and src/core/game3/storage.lua:595 Storage.restore folds them into the --- modern blob. This test pins that migration so the reads are never "cleaned up" --- as dead. Also pins the F3 engine/generation round-trip symmetry. package.path = "./?.lua;./?/init.lua;" .. package.path local failed = 0 diff --git a/tests/game3_save_trainer_card_test.lua b/tests/game3_save_trainer_card_test.lua index b6f16ab3..aa31e541 100644 --- a/tests/game3_save_trainer_card_test.lua +++ b/tests/game3_save_trainer_card_test.lua @@ -83,11 +83,6 @@ end) print("[test] 2. SaveMenu lifecycle and state machine") local SaveMenu = require("src.ui.game3.save_menu") --- G1 contract (tests/engine/game3_save_menu_failure_test.lua): do_save() --- reports "saved" only when the game's saveGame confirms the write, and it --- reads that off Runtime._game (not SaveMenu._game), so supply the stub the --- real game carries. Runtime.pumpRtc reads only Runtime.session/game.save, --- so this cannot leak into the playtime test below. local Runtime = require("src.core.game3.runtime") Runtime._game = { saveGame = function() return true end } diff --git a/tests/game3_scenario_battle_ai_test.lua b/tests/game3_scenario_battle_ai_test.lua index eefd7bf9..730ad32d 100644 --- a/tests/game3_scenario_battle_ai_test.lua +++ b/tests/game3_scenario_battle_ai_test.lua @@ -1,20 +1,5 @@ #!/usr/bin/env luajit --- Battle AI decision scenario: given a battle state, the AI scores its four --- moves and picks one, end to end (Ai.chooseMove / Ai.chooseAction). Scoring --- runs against an in-line SYNTHETIC pack so the decision flow always executes --- even where the extracted FireRed AI pack is missing; pack-dependent facts --- live in the final section and self-skip with "[skip]" (exit 0) in that case. --- --- pret citations actually read (~/dev/pokefirered): --- src/battle_ai_script_commands.c:299 every considered move starts at 100 --- src/battle_ai_script_commands.c:302 CheckMoveLimitations zeroes bad slots --- src/battle_ai_script_commands.c:310 simulatedRNG[i] = 100 - Random() % 16 --- src/battle_ai_script_commands.c:363 BattleAI_ChooseMoveOrAction --- src/battle_ai_script_commands.c:371 aiFlags bit loop selects AI scripts --- src/battle_ai_script_commands.c:384 AI_ACTION_FLEE -> AI_CHOICE_FLEE --- Engine seams exercised: Ai.chooseMove (src/core/game3/battle/ai.lua:423), --- Ai.chooseAction (ai.lua:431), Ai.battleStart (ai.lua:478), --- AiVm.run (src/core/game3/battle/ai_vm.lua:58). +-- src/battle_ai_script_commands.c:299, src/battle_ai_script_commands.c:302, src/battle_ai_script_commands.c:310, src/battle_ai_script_commands.c:363, src/battle_ai_script_commands.c:371, src/battle_ai_script_commands.c:384 package.path = "./?.lua;./?/init.lua;" .. package.path @@ -41,17 +26,11 @@ local State = require("src.core.game3.battle.state") local Ai = require("src.core.game3.battle.ai") local AiVm = require("src.core.game3.battle.ai_vm") --- Deterministic rng: always the low bound (pret tie-breaks, simulatedRNG draws). local function loRng(lo, hi) if lo and hi then return lo end return 0 end --- Synthetic pack. SYN_KO is shaped like pret's AI_TryToFaint: score +40 for --- the move that can faint the target, -40 for the ones that cannot --- (CMD.if_can_faint / CMD.score in src/core/game3/battle/ai_cmds.lua; the --- score op clamps at 127 because pret stores the score as s8 — --- ai_cmds.lua:318). local scripts = { SYN_KO = { { op = "if_can_faint", target = "SYN_TAKE" }, @@ -65,11 +44,6 @@ local scripts = { local synPack = { table = { "SYN_KO" }, data = {}, scripts = scripts } local fleePack = { table = { "SYN_FLEE" }, data = {}, scripts = scripts } --- Battle state cribbed from tests/game3_battle_ai_test.lua: player Pidgey --- (Flying, hence Ground-immune) vs foe Geodude holding EARTHQUAKE + TACKLE. --- Rigged so the decision discriminates: player at 10/40 HP — Tackle's --- simulated damage (19) CAN faint it, Earthquake cannot (Ground vs Flying is --- 0 -> ai_damage clamps to 1). Foe at 80 HP so its own HP never matters here. local function aiState(opts) opts = opts or {} local st = State.new({ @@ -81,8 +55,8 @@ local function aiState(opts) attack = 70, defense = 60, spAtk = 40, spDef = 50, speed = 30, }, }) - st.player.type1 = 2 st.player.type2 = nil -- FLYING - st.enemy.type1 = 4 st.enemy.type2 = 5 -- GROUND / ROCK + st.player.type1 = 2 st.player.type2 = nil + st.enemy.type1 = 4 st.enemy.type2 = 5 st.aiFlags = opts.aiFlags or 1 return st end @@ -154,8 +128,6 @@ local actF = Ai.chooseAction(aiState({ wild = true }), 1, { pack = fleePack, aiF check(actF and actF.kind == "run", "AI_ACTION_FLEE surfaces as a kind='run' action") print("[test] 5. Real extracted AI pack (self-skips when absent)") --- pcall-wrapped: on the CI tier without the imported FireRed cache the --- on-demand extract can throw instead of returning nil; either way skip. local okPack, realPack = pcall(Ai.loadPack, { force = true, extract = true }) if not okPack or not realPack then print("[skip] real AI pack absent") diff --git a/tests/game3_scenario_capture_test.lua b/tests/game3_scenario_capture_test.lua index a643bdb8..c86a8870 100644 --- a/tests/game3_scenario_capture_test.lua +++ b/tests/game3_scenario_capture_test.lua @@ -1,33 +1,5 @@ #!/usr/bin/env luajit --- Pokemon capture scenario: a wild encounter played through to a caught mon. --- Flow: encounter -> dex SEEN -> ball checks -> rigged throw -> ball consumed --- -> dex CAUGHT + party gain -> second encounter with a different ball -> --- out-of-balls refusal. --- --- pret citations actually read for this suite: --- pokefirered/src/battle_main.c:2611 HandleSetPokedexFlag FLAG_SET_SEEN on send-out --- pokefirered/src/battle_script_commands.c:4526 same, from Cmd_switchinanim --- pokefirered/src/battle_script_commands.c:9463 Cmd_handleballthrow (the catch roll) --- pokefirered/src/battle_script_commands.c:9497 safari catch-rate special case --- pokefirered/src/battle_script_commands.c:9617 Cmd_givecaughtmon (party/PC store) --- pokefirered/src/pokemon.c:3686 GiveMonToPlayer --- pokefirered/src/pokemon.c:3692 SetMonData MON_DATA_OT_ID = playerTrainerId --- pokefirered/src/new_game.c:56 InitPlayerTrainerId rolls the trainer id --- --- Engine gaps designed around (NOT fixed here): --- 1. Catching.playerSecretId (src/core/game3/battle/catching.lua:235) mints a --- secret id from the RNG and cites pokefirered/src/new_game.c:56, but pret --- FRLG has no playerSecretId at all (grep over src/*.c include/*.h finds --- none) -- the gen-3 OT id is just playerTrainerId (pokemon.c:3692). We --- therefore assert only the engine's own contract: one stable secret id --- per session, stamped on both the wild mon and the caught mon. --- 2. Battle.start with headless=true auto-runs the whole battle to --- completion (src/core/game3/battle/init.lua:663-665), so the scenario --- passes autoFight=false to hold the battle open for the item path. --- There is no dedicated "start but stay in command phase" API. --- 3. The real catch flow stores via Catching.storeCaught (battle/items.lua:167), --- never Party.giveMon (that is the script-gift path, party.lua:181); the --- party assertions below follow storeCaught's own append path. +-- pokefirered/src/battle_main.c:2611, pokefirered/src/battle_script_commands.c:4526, pokefirered/src/battle_script_commands.c:9463, pokefirered/src/battle_script_commands.c:9497, pokefirered/src/battle_script_commands.c:9617, pokefirered/src/pokemon.c:3686, pokefirered/src/pokemon.c:3692, pokefirered/src/new_game.c:56, pokemon.c:3692 package.path = "./?.lua;./?/init.lua;" .. package.path @@ -62,9 +34,7 @@ local Battle = require("src.core.game3.battle") Pokemon.install(nil) --- Deterministic roll handed to the battle as opts.rng (state.lua:284); every --- shake check draws lo (0), which is below any pret shake threshold --- (battle_script_commands.c:9463 shake loop), so the ball always holds. +-- battle_script_commands.c:9463 local function rigRoll(lo) return lo end @@ -83,13 +53,13 @@ local function newSession() end local session = newSession() -Bag.add(session.bag, 4, 3) -- three Poke Balls +Bag.add(session.bag, 4, 3) print("[test] 1. Wild Rattata appears: battle opens and the dex marks it seen") local okStart = Battle.start({ wild = true, headless = true, - autoFight = false, -- hold the battle open; see gap 2 in the header + autoFight = false, playerParty = session.party, foe = { species = 19, level = 3, hp = 6, maxHp = 15 }, session = session, @@ -99,12 +69,11 @@ check(okStart == true, "the wild battle started") check(Battle.isActive() == true, "the battle is active and waiting (not auto-run)") local st = Battle.getState() check(st ~= nil and st.wild == true, "battle state flags the encounter as wild") --- pret battle_main.c:2611 / battle_script_commands.c:4526 FLAG_SET_SEEN +-- battle_main.c:2611, battle_script_commands.c:4526 check(Dex.isSeen(session.dex, 19) == true, "Rattata registered as seen on encounter") check(Dex.isCaught(session.dex, 19) == false, "Rattata is not caught yet") check(Dex.countSeen(session.dex, "kanto") == 1, "kanto seen count is 1") check(Dex.countCaught(session.dex, "kanto") == 0, "kanto caught count is 0") --- Battle.start stamps the wild mon with the player's ids (battle/init.lua:454-461) check(st.enemy.mon.otId == 4242, "wild mon stamped with the trainer id (pokemon.c:3692 contract)") check(type(session.secretId) == "number", "playerSecretId minted a session secret id") check(st.enemy.mon.otSecretId == session.secretId, "the wild mon carries that secret id") @@ -119,7 +88,6 @@ check(BattleItems.needsPartySelect(13) == true, "a potion asks which mon to heal check(Catching.ballMultiplier(2, foe, st, session) == 20, "Ultra Ball bonus is 2.0x (x10 scale)") check(Catching.ballMultiplier(4, foe, st, session) == 10, "Poke Ball bonus is 1.0x") check(Catching.catchOdds(1, foe, st, session) == 255, "Master Ball odds are always 255") --- Odds climb as the wild mon wears down: full HP -> current 6 HP -> 3 HP. local curHp = foe.mon.hp foe.mon.hp = foe.mon.maxHp local fullOdds = Catching.catchOdds(4, foe, st, session) @@ -158,9 +126,9 @@ check(Dex.registerEncounter(session.dex, 19) == true, "meeting it again reports it was already seen") print("[test] 5. Second encounter: Pidgey, thrown at with the one Ultra Ball") -Battle.abort("caught") -- wrap up throw #1's battle (battle/init.lua:3113) +Battle.abort("caught") check(Battle.isActive() == false, "the first battle is finished") -Bag.add(session.bag, 2, 1) -- one Ultra Ball +Bag.add(session.bag, 2, 1) local okStart2 = Battle.start({ wild = true, headless = true, diff --git a/tests/game3_scenario_event_test.lua b/tests/game3_scenario_event_test.lua index ebd3374e..65dc9d3e 100644 --- a/tests/game3_scenario_event_test.lua +++ b/tests/game3_scenario_event_test.lua @@ -1,13 +1,5 @@ #!/usr/bin/env luajit --- Story event end to end: a new game opens the world hidden, Oak's yielding --- party-pick special runs through the real VM (adapter yield + var hand-off + --- resume), the granted FLAG_SYS_POKEDEX_GET flips a real consumer branch (the --- start menu), and a cache-backed object-interaction event runs to completion. --- pret: data/specials.inc:170 def_special ChoosePartyMon; --- src/party_menu_specials.c:14-22 ChoosePartyMon with --- PARTY_MENU_TYPE_CHOOSE_SINGLE_MON; src/start_menu.c:215-216 --- FlagGet(FLAG_SYS_POKEDEX_GET) gates STARTMENU_POKEDEX; --- data/scripts/questionnaire.inc:4 (prompt) and :35 (decline releases control). +-- data/specials.inc:170, src/party_menu_specials.c:14-22, src/start_menu.c:215-216, data/scripts/questionnaire.inc:4 package.path = "./?.lua;./?/init.lua;" .. package.path @@ -30,8 +22,6 @@ local function finish() os.exit(0) end --- Runtime stub first: natives.partyOf reads package.loaded at call time --- (same pattern as tests/game3_special_handlers_test.lua session()). local session = { trainerId = 4242, party = { { species = 1, nickname = "", otId = 4242 }, @@ -49,8 +39,6 @@ local Vm = require("src.core.game3.scripting.vm") print("[test] 1. a new game opens with the world in its hiding state") local store = Flags.newStore() Flags.applyNewGameHideFlags(store) --- Known opening values (tests/game3_event_flags_test.lua:50-60, pret --- EventScript_ResetAllMapFlags via Flags.NEW_GAME_HIDE_FLAGS). check(Flags.getFlag(store, nil, 0x02B) == true, "lab Oak starts hidden (FLAG_HIDE_OAK_IN_HIS_LAB 0x2B)") check(Flags.getFlag(store, nil, 0x02C) == true, "Pallet Town Oak starts hidden (0x2C)") check(Flags.getFlag(store, nil, 0x092) == true, "Pewter running-shoes guide starts hidden (0x92)") @@ -69,11 +57,9 @@ local vm = Vm.new({ scripts = { ["scenario:oak_grants_dex"] = { { op = "special", [1] = Std.SPECIAL.ChoosePartyMon }, -- data/specials.inc:170 - { op = "setvar", [1] = 0x4031, [2] = 1 }, -- VAR_STARTER_MON = Charmander - { op = "setflag", [1] = 0x829 }, -- FLAG_SYS_POKEDEX_GET - -- The script just picked a starter, so retail also grants - -- FLAG_SYS_POKEMON_GET (0x828) — start_menu.c:217-218 gates the - -- POKéMON row on it and this scenario's menu checks need the row. + { op = "setvar", [1] = 0x4031, [2] = 1 }, + { op = "setflag", [1] = 0x829 }, + -- start_menu.c:217-218 { op = "setflag", [1] = 0x828 }, { op = "end" }, }, @@ -117,7 +103,6 @@ local function openMenu() StartMenu.show({ session = { name = "TESTER" } }) end --- The flag test 2's event set drives the retail gate. openMenu() check(hasRow("pokedex"), "with FLAG_SYS_POKEDEX_GET set the POKéDEX row appears (start_menu.c:215)") @@ -153,7 +138,7 @@ local messages, asked = {}, false local objVm = Vm.new({ scripts = bundle.scripts, text = bundle.text, movements = bundle.movements, onMessage = function(text) messages[#messages + 1] = text end, - askYesNo = function(cb) asked = true; cb(false) end, -- decline: questionnaire.inc:35 + askYesNo = function(cb) asked = true; cb(false) end, -- questionnaire.inc:35 }) if not objVm:start("EventScript_Questionnaire") then check(false, "EventScript_Questionnaire starts from the bundle") diff --git a/tests/game3_scenario_menu_test.lua b/tests/game3_scenario_menu_test.lua index f52db6e5..034f8682 100644 --- a/tests/game3_scenario_menu_test.lua +++ b/tests/game3_scenario_menu_test.lua @@ -1,26 +1,5 @@ #!/usr/bin/env luajit --- Menu usage scenario: start menu -> save flow end to end, plus both cancel --- paths. Every menu opened here is closed before the next section, because --- StartMenu/SaveMenu/Stack are process-wide singletons. --- --- pret citations actually read for this suite: --- pokefirered/src/start_menu.c:43-49 STARTMENU_POKEDEX..STARTMENU_EXIT order --- pokefirered/src/start_menu.c:113-123 item label/callback table --- pokefirered/src/start_menu.c:213-223 SetUpStartMenu_NormalField append order --- pokefirered/src/start_menu.c:215 POKéDEX gated on FLAG_SYS_POKEDEX_GET --- pokefirered/src/start_menu.c:217-218 POKéMON gated on FLAG_SYS_POKEMON_GET --- pokefirered/src/start_menu.c:1003-1005 CloseStartMenu plays SE_SELECT --- pokefirered/src/menu.c:276 cursor out of range clamps to 0 --- pokefirered/src/menu.c:376 A press plays SE_SELECT (the NO path's se) --- pokefirered/src/menu.c:381 B press returns MENU_B_PRESSED (back-out) --- --- Engine quirks designed around (NOT fixed here): --- do_save reads Runtime._game.saveGame and must get a truthy confirm --- (tests/engine/game3_save_menu_failure_test.lua), so this suite supplies --- the same stub saveGame the real game carries (cf. --- tests/game3_save_trainer_card_test.lua:91-92). --- (The former "gap 1: POKéMON entry has no flag gate" is now implemented — --- start_menu.lua gates it on 0x828 exactly as start_menu.c:217-218 does.) +-- pokefirered/src/start_menu.c:43-49, pokefirered/src/start_menu.c:113-123, pokefirered/src/start_menu.c:213-223, pokefirered/src/start_menu.c:215, pokefirered/src/start_menu.c:217-218, pokefirered/src/start_menu.c:1003-1005, pokefirered/src/menu.c:276, pokefirered/src/menu.c:376, pokefirered/src/menu.c:381, start_menu.c:217-218 package.path = "./?.lua;./?/init.lua;" .. package.path @@ -49,8 +28,6 @@ local SaveMenu = require("src.ui.game3.save_menu") local Flags = require("src.core.game3.scripting.flags") local Runtime = require("src.core.game3.runtime") --- do_save (save_menu.lua:94-133) reads Runtime._game, not SaveMenu._game. --- _mod stays nil so the sidecar persist branch is skipped. local saveCalls = 0 Runtime._game = { saveGame = function() saveCalls = saveCalls + 1 return true end } Runtime._mod = nil @@ -71,7 +48,6 @@ StartMenu.show({ session = session }) check(StartMenu.isOpen() == true, "the start menu is open") check(Stack.top() ~= nil and Stack.top().id == "start", "the start menu owns the top of the stack") check(Stack.depth() == 1, "exactly one layer on the stack") --- No flag store loaded -> the dex gate defaults open (start_menu.lua:26-35). check(#StartMenu.ENTRIES == 7, "seven entries with no store loaded") check(ids() == "pokedex,pokemon,bag,trainer,save,option,exit", "entry ids follow pret start_menu.c:43-49 / 113-123 order") @@ -102,7 +78,7 @@ check(#StartMenu.ENTRIES == 7, "the POKéMON entry returns once FLAG_SYS_POKEMON_GET (0x828) is set (start_menu.c:217-218)") check(StartMenu.ENTRIES[2].id == "pokemon", "it sits between POKéDEX and BAG (start_menu.c:218)") StartMenu.close() -package.loaded["src.core.game3.scripting.space"] = spaceMod -- restore default +package.loaded["src.core.game3.scripting.space"] = spaceMod print("[test] 3. The cursor wraps the list (menu.c:276 clamp, modulo wrap)") StartMenu.show({ session = session }) @@ -121,20 +97,19 @@ check(StartMenu.cursor == 5 and StartMenu.ENTRIES[5].id == "save", print("[test] 4. Confirming SAVE runs the YES/YES dialog and unwinds both menus") local savesBefore = saveCalls -StartMenu.confirm() -- dispatch: id == "save" -> SaveMenu.show (start_menu.lua:151-153) +StartMenu.confirm() check(SaveMenu.isOpen() == true, "the save dialog opened over the start menu") check(SaveMenu._phase == "confirm", "it asks 'Would you like to SAVE the game?' first") check(SaveMenu.cursor == 1, "YES is preselected") check(Stack.depth() == 2 and Stack.top().id == "save", "the save layer sits above the start layer") check(Stack.has("start") == true and StartMenu.isOpen() == true, "the start menu stays open beneath (input goes to Stack.top().mod)") --- Input routes to the top layer, so drive SaveMenu directly from here. SaveMenu.confirm() check(SaveMenu._phase == "overwrite", "YES advances to the overwrite confirm") SaveMenu.confirm() check(SaveMenu._phase == "saved", "the second YES writes and reports saved") check(saveCalls == savesBefore + 1, "exactly one saveGame call happened") -SaveMenu.confirm() -- A on "[Player] saved the game." +SaveMenu.confirm() check(SaveMenu.isOpen() == false, "the dialog closes") check(StartMenu.isOpen() == false, "and takes the start menu with it (start_menu.c:583 path)") check(Stack.depth() == 0, "the stack unwound to empty") @@ -144,17 +119,17 @@ print("[test] 5. NO closes the dialog without writing; cancel closes the menu") local savesNow = saveCalls StartMenu.resetCursor() StartMenu.show({ session = session }) -for _ = 1, 4 do StartMenu.move(1) end -- back down to SAVE +for _ = 1, 4 do StartMenu.move(1) end check(StartMenu.ENTRIES[StartMenu.cursor].id == "save", "cursor back on SAVE") StartMenu.confirm() check(SaveMenu.isOpen() == true and SaveMenu._phase == "confirm", "the save dialog reopened") SaveMenu.move(1) check(SaveMenu.cursor == 2, "cursor flips to NO") -SaveMenu.confirm() -- A press on NO (menu.c:376 plays SE_SELECT) -> SaveMenu.close() +SaveMenu.confirm() -- menu.c:376 check(SaveMenu.isOpen() == false, "NO closes the save dialog") check(saveCalls == savesNow, "and nothing was written") check(StartMenu.isOpen() == true and Stack.depth() == 1, "the start menu is still up beneath") -StartMenu.cancel() -- B on the list closes the menu (menu.c:381 back-out) +StartMenu.cancel() -- menu.c:381 check(StartMenu.isOpen() == false, "cancel closes the start menu") check(Stack.depth() == 0, "the stack is empty again") diff --git a/tests/game3_scenario_move_test.lua b/tests/game3_scenario_move_test.lua index f1b7b797..0d824d67 100644 --- a/tests/game3_scenario_move_test.lua +++ b/tests/game3_scenario_move_test.lua @@ -1,30 +1,5 @@ #!/usr/bin/env luajit --- Move usage round scenario: one full move round end to end through --- Engine.resolveMove — the engine entry that actually executes a move --- (src/core/game3/battle/engine.lua:1605; it builds the move Ctx, dispatches --- Hit.run for damaging moves and Effects.runForMove for status moves). --- Covers: damage lands and matches the engine's own hit record, type --- effectiveness orders damage, PP is taken through the real Ctx:ppReduce --- path (engine.lua:282, write at engine.lua:306), and a secondary status --- branch applies when the roll is rigged to proc. --- --- pret citations actually read (~/dev/pokefirered): --- src/data/battle_moves.h:432 TACKLE: power 35, acc 95, pp 35 --- src/data/battle_moves.h:679 EMBER: EFFECT_BURN_HIT, chance 10 --- src/pokemon.c:2374 APPLY_STAT_MOD stage ratios --- src/pokemon.c:2385 CalculateBaseDamage --- src/battle_script_commands.c:1122 Cmd_ppreduce (cost 1; +1 per PRESSURE) --- src/battle_script_commands.c:1134 ppToDeduct += ... ABILITY_PRESSURE --- src/battle_script_commands.c:1199 crit roll (Random() % chance) --- src/battle_script_commands.c:1209 Cmd_damagecalc --- src/battle_script_commands.c:1557 ApplyRandomDmgMultiplier (85..100%) --- --- NOTE (engine gap designed around, not fixed in src): pret's 100%-chance --- secondary WRAP (battle_moves.h:458) and 30%-chance POISON_STING (:523) have --- no curated entry in moves.lua BY_ID, so without the imported ROM cache --- Moves.get(20/35) carries no secondary effect at all. The secondary section --- therefore uses EMBER (52), which is curated and matches pret exactly --- (:679), so it runs green with or without the imported cache. +-- src/data/battle_moves.h:432, src/data/battle_moves.h:679, src/pokemon.c:2374, src/pokemon.c:2385, src/battle_script_commands.c:1122, src/battle_script_commands.c:1134, src/battle_script_commands.c:1199, src/battle_script_commands.c:1209, src/battle_script_commands.c:1557, battle_moves.h:458 package.path = "./?.lua;./?/init.lua;" .. package.path @@ -54,10 +29,7 @@ local Types = require("src.core.game3.battle.types") local Moves = require("src.core.game3.battle.moves") local T = Types.ID --- Deterministic battle rng: always hit accuracy rolls (1,100), take the high --- side of the 85..100 damage roll (pret battle_script_commands.c:1557) and of --- the (0,15) crit roll (never crit, :1199); caller may override a range — --- e.g. the secondary roll (0,99) — to rig a branch. +-- battle_script_commands.c:1557 local function mkRng(map) map = map or {} return function(lo, hi) @@ -68,9 +40,7 @@ local function mkRng(map) end end --- Fixed-stat mons so Damage.base (pret pokemon.c:2385) is deterministic. --- Species 1 on both sides only feeds display names / ability pick; none of --- the asserted damage depends on it (stats and types are forced here). +-- pokemon.c:2385 local function player(o) o = o or {} return { @@ -90,8 +60,6 @@ local function foe(o) } end --- Fresh wild single battle with forced types (type chart: --- src/core/game3/battle/types.lua TABLE / Types.typeCalc). local function round(opts) opts = opts or {} local st = State.new({ @@ -141,8 +109,6 @@ check(txt2:find("used\nTACKLE", 1, true) ~= nil, "foe attack string printed") check(ad:hp(st.player) < 120 and ad:hp(st.enemy) < 150, "both battlers damaged: round completed") print("[test] 3. Type effectiveness orders damage: super > neutral > resisted") --- EMBER (Fire) into GRASS / NORMAL / WATER with identical stats and the same --- rigged rng, so the type chart is the only differing input. local function emberVs(defType) local s, a = round({ player = player({ moves = { 52 }, pp = { 25, 20, 20, 20 } }), @@ -165,9 +131,7 @@ check(tNeu:find("super effective", 1, true) == nil and tNeu:find("not very", 1, check(tRes:find("not very effective", 1, true) ~= nil, "'It's not very effective' printed") print("[test] 4. PP goes down exactly 1 per use on the real consumption path") --- Reached as Engine.resolveMove -> Hit.run (engine.lua:543 M:ppReduce()) -> --- Ctx:ppReduce (engine.lua:282, write at :306). pret: Cmd_ppreduce --- (battle_script_commands.c:1122). PP itself is never mutated by this test. +-- battle_script_commands.c:1122 local stP, adP = round() check(stP.player.mon.pp[1] == 35 and stP.player.mon.pp[2] == 20, "starting PP 35 / 20") resolve(stP, adP, stP.player, 33, 1) @@ -176,24 +140,20 @@ check(stP.player.mon.pp[2] == 20, "sibling slot untouched") resolve(stP, adP, stP.player, 33, 1) check(stP.player.mon.pp[1] == 33, "second use -> 33") --- Zero-PP gate (engine.lua:1430): the move refuses through the real path. -stP.player.mon.pp[1] = 0 -- scenario rig only: assert engine behavior below +stP.player.mon.pp[1] = 0 local zBefore = adP:hp(stP.enemy) local _, zTxt = resolve(stP, adP, stP.player, 33, 1) check(zTxt:find("no PP left", 1, true) ~= nil, "zero-PP gate prints 'no PP left'") check(adP:hp(stP.enemy) == zBefore, "zero-PP gate deals no damage") check(stP.player.mon.pp[1] == 0, "PP stays at 0") --- PRESSURE doubles the cost (engine.lua:302-306; pret --- battle_script_commands.c:1134 ppToDeduct += ... ABILITY_PRESSURE). +-- battle_script_commands.c:1134 local stQ, adQ = round({ foe = foe({ ability = "PRESSURE" }) }) resolve(stQ, adQ, stQ.player, 33, 1) check(stQ.player.mon.pp[1] == 33, "PRESSURE target costs 2 PP (35 -> 33)") print("[test] 5. Secondary status branch: EMBER burn sticks when rigged to proc") --- pret: EMBER secondaryEffectChance = 10 (battle_moves.h:679); the engine --- rolls ad:roll(0,99) <= chance (effects/secondary.lua withChance), so a --- rigged 0 always procs and the default high roll never does. +-- battle_moves.h:679 local stB, adB = round({ player = player({ moves = { 52 }, pp = { 25, 20, 20, 20 } }), rngMap = { ["0,99"] = 0 }, diff --git a/tests/game3_scenario_overworld_test.lua b/tests/game3_scenario_overworld_test.lua index a93f9fcb..fa74921b 100644 --- a/tests/game3_scenario_overworld_test.lua +++ b/tests/game3_scenario_overworld_test.lua @@ -1,14 +1,5 @@ #!/usr/bin/env luajit --- Overworld navigation end to end on a stub map: a real Player step advances --- the session, a solid metatile refuses it, the MB_IMPASSABLE_NORTH edge pair --- blocks one direction only, and stepping onto a warp tile resolves the --- destination through the real Warp sequence into a (stubbed) Map.load. --- Driven through the real entry points: Player.tryMove / Player.tick → --- Collision.canEnter / directionallyImpassable / tryWarpAt → Warp.request. --- pret: src/field_control_avatar.c:618-623 TryStartStepBasedScript → --- TryStartWarpEventScript; :856,:901 IsWarpMetatileBehavior; :965 SetupWarp; --- src/metatile_behavior.c:544-571 MetatileBehavior_Is*Blocked; --- src/event_object_movement.c:4889 IsMetatileDirectionallyImpassable. +-- src/field_control_avatar.c:618-623, src/metatile_behavior.c:544-571, src/event_object_movement.c:4889 package.path = "./?.lua;./?/init.lua;" .. package.path @@ -34,9 +25,6 @@ end local GameVersion = require("src.core.GameVersion") GameVersion.set("firered") --- Minimal stub surface (pattern: tests/game3_link_session_test.lua:52-68): a --- recording Map and a session-backed runtime. The REAL Player, Collision and --- Warp modules run unstubbed against a synthetic map def. local HALL = "FR_SCENARIO_HALL" local ANNEX = "FR_SCENARIO_ANNEX" local PAIR = "scenario_overworld" @@ -58,8 +46,6 @@ package.loaded["src.core.game3.map"] = { end, } --- Metatile behaviors for the stub pair: behaviors[pair][mid] = MB byte --- (src/core/game3/scripting/interaction_scripts.lua behaviorOn lookup). local Interactions = require("src.core.game3.scripting.interaction_scripts") local behs = {} Interactions.behaviors[PAIR] = behs @@ -85,14 +71,12 @@ local function layoutFor(cells) } end --- The real classifier decides the walkability bytes (fromCell): --- mapColl 1 + MB_IMPASSABLE_NORTH → solid COLL 0x07; mapColl 0 stays walkable. local ScriptColl = require("src.core.game3.scripting.collision") local WALL_BYTE = ScriptColl.fromCell(721, 1, 0x32, "indoor") local EDGE_BYTE = ScriptColl.fromCell(721, 0, 0x32, "indoor") -behs[MID_EDGE] = 0x32 -- MB_IMPASSABLE_NORTH on (2,2) -behs[MID_DOOR] = 0x60 -- MB_CAVE_DOOR on the warp tile (5,3) +behs[MID_EDGE] = 0x32 +behs[MID_DOOR] = 0x60 local hallDef = { pair = PAIR, @@ -153,7 +137,7 @@ check(Collision.cell(2, 2) == EDGE_BYTE, "and its COLL byte is the walkable band local function D(fx, fy, tx, ty, dir) return Collision.directionallyImpassable(fx, fy, tx, ty, dir) == true end --- pokefirered/src/event_object_movement.c:4889 leaves-tile + enters-tile pair. +-- pokefirered/src/event_object_movement.c:4889 check(D(2, 2, 2, 1, "up"), "leaving northward off the band is blocked") check(not D(2, 2, 2, 3, "down"), "leaving southward is allowed") check(not D(2, 2, 3, 2, "right"), "leaving eastward is allowed") diff --git a/tests/game3_shop_bag_chrome_test.lua b/tests/game3_shop_bag_chrome_test.lua index 16be96cd..90a05d38 100644 --- a/tests/game3_shop_bag_chrome_test.lua +++ b/tests/game3_shop_bag_chrome_test.lua @@ -2,6 +2,7 @@ -- Shop & Bag chrome extraction, contract, state machine, and interaction unit tests. package.path = "./?.lua;./?/init.lua;" .. package.path +require("tests.game3_cache").requireData("game3_shop_bag_chrome_test") local failed = 0 local function check(cond, msg) diff --git a/tests/game3_shop_menu_test.lua b/tests/game3_shop_menu_test.lua index dada3f17..97f30fc9 100644 --- a/tests/game3_shop_menu_test.lua +++ b/tests/game3_shop_menu_test.lua @@ -1,5 +1,6 @@ -- Automated test suite for Game 3 Poké Mart system (shop_menu.lua, marts.lua, bag.lua). +require("tests.game3_cache").requireData("game3_shop_menu_test") local ShopMenu = require("src.ui.game3.shop_menu") local Bag = require("src.core.game3.bag") local ItemsData = require("src.core.game3.items_data") diff --git a/tests/game3_size_record_test.lua b/tests/game3_size_record_test.lua index 82fb307e..0f6ab7f1 100644 --- a/tests/game3_size_record_test.lua +++ b/tests/game3_size_record_test.lua @@ -1,5 +1,6 @@ #!/usr/bin/env luajit package.path = "./?.lua;./?/init.lua;" .. package.path +require("tests.game3_cache").requireData("game3_size_record_test") local SizeRecord = require("src.core.game3.pokemon_size_record") local Std = require("src.core.game3.scripting.stdscripts") diff --git a/tests/game3_special_elevator_test.lua b/tests/game3_special_elevator_test.lua index 362af843..78473b2c 100644 --- a/tests/game3_special_elevator_test.lua +++ b/tests/game3_special_elevator_test.lua @@ -310,6 +310,21 @@ for _, name in ipairs({ "AnimateTeleporterHousing", "AnimateTeleporterCable" }) eq(result(ctx), 0, name .. " leaves VAR_RESULT at 0") end +local Task = require("src.core.game3.task") +local realSpawn, realSet = Task.spawn, Field.setMetatile +for _, name in ipairs({ "AnimateTeleporterHousing", "AnimateTeleporterCable" }) do + local fn + Task.spawn = function(f) fn = f end + local tw = {} + Field.setMetatile = function(x, y, mid, impassable) tw[#tw + 1] = impassable end + Cutscene.HANDLERS[Std.SPECIAL[name]](newCtx(), {}) + for _ = 1, 1000 do if not fn or fn() then break end end + local allSolid = #tw > 0 + for _, v in ipairs(tw) do if v ~= true then allSolid = false end end + check(allSolid, name .. " writes every metatile with MAPGRID_COLLISION_MASK") +end +Task.spawn, Field.setMetatile = realSpawn, realSet + if failed > 0 then print("[test] FAILED " .. failed) os.exit(1) diff --git a/tests/game3_special_events_test.lua b/tests/game3_special_events_test.lua index 6dc4012e..dc77e7f8 100644 --- a/tests/game3_special_events_test.lua +++ b/tests/game3_special_events_test.lua @@ -307,6 +307,34 @@ for _, name in ipairs(NOOPS) do eq(getVar(ctx, 0x800D), 0, name .. " leaves VAR_RESULT at 0") end +print("[test] 14. SampleResortGorgeousMonAndReward buffers the sampled species into STR_VAR_1") +local savedPokemon = package.loaded["src.core.game3.pokemon"] +package.loaded["src.core.game3.pokemon"] = { + name = function(sp) return ({ [25] = "PIKACHU", [150] = "MEWTWO" })[sp] or ("SP" .. tostring(sp)) end, +} +local savedDex = session.dex +session.dex = { owned = { [25] = true } } +local VAR_REQ = 0x4036 +store.vars = {} +local ctx14 = newCtx() +ctx14.stringVars = {} +local buffered = {} +Events.HANDLERS[Std.SPECIAL.SampleResortGorgeousMonAndReward](ctx14, { + setStringVar = function(i, text) buffered[i] = text end, +}) +eq(getVar(ctx14, VAR_REQ), 25, "an empty request samples the only owned species") +eq(ctx14.stringVars[1], "PIKACHU", "STR_VAR_1 names the freshly sampled species") +eq(buffered[1], "PIKACHU", "the host adapter receives STR_VAR_1") +local ctx14b = newCtx() +ctx14b.stringVars = {} +setVar(ctx14b, VAR_REQ, 150) +Events.HANDLERS[Std.SPECIAL.SampleResortGorgeousMonAndReward](ctx14b, {}) +eq(getVar(ctx14b, VAR_REQ), 150, "a pending request is kept") +eq(ctx14b.stringVars[1], "MEWTWO", "STR_VAR_1 names the pending request") +session.dex = savedDex +store.vars = {} +package.loaded["src.core.game3.pokemon"] = savedPokemon + if failed > 0 then print("[test] FAILED " .. failed) os.exit(1) diff --git a/tests/game3_static_encounter_test.lua b/tests/game3_static_encounter_test.lua index 257cd7d1..fb3f9a55 100644 --- a/tests/game3_static_encounter_test.lua +++ b/tests/game3_static_encounter_test.lua @@ -221,7 +221,7 @@ for _, row in ipairs(body or {}) do if row.op == "compare_var_to_value" and row.value == B_OUTCOME.CAUGHT then sawCompare = true end - -- pokefirered/src/scrcmd.c:153 ScrCmd_goto_if, sScriptConditionTable scrcmd.c:65 row 5 = != + -- pokefirered/src/scrcmd.c:153, scrcmd.c:65 if row.op == "goto_if" and row.cond == 5 then sawGotoIfNe = true end if row.op == "removeobject" and row.localId == VAR_LAST_TALKED then sawRemove = true end end diff --git a/tests/game3_stitchbattle_catch_headless_test.lua b/tests/game3_stitchbattle_catch_headless_test.lua index 6ff00db2..2f718059 100644 --- a/tests/game3_stitchbattle_catch_headless_test.lua +++ b/tests/game3_stitchbattle_catch_headless_test.lua @@ -94,8 +94,7 @@ local key = nil local input = { wasPressed = function(_, k) return key == k end } local function press(k) key = k - -- pret/pokefirered src/naming_screen.c:559-572: input and timers run in the - -- same per-frame screen task; engine split: naming.lua:489-511. + -- src/naming_screen.c:559-572 Naming.handleInput(input) Naming.update(1 / 60) key = nil diff --git a/tests/game3_stitchcoll_fall_draw_test.lua b/tests/game3_stitchcoll_fall_draw_test.lua index 8935d089..ae114d2d 100644 --- a/tests/game3_stitchcoll_fall_draw_test.lua +++ b/tests/game3_stitchcoll_fall_draw_test.lua @@ -82,7 +82,7 @@ check(Warp.startFall(nil, game, CAVE_B1F, HOLE_X, HOLE_Y) == true, "the fall war local lowest, offsetFrames, landed = 0, 0, false for _ = 1, 600 do - -- pokefirered/src/field_effect.c:1166 Task_FallWarpFieldEffect + -- pokefirered/src/field_effect.c:1166 Fade.tick(1 / 60) Task.update(1 / 60) FieldEffects.step() diff --git a/tests/game3_stitchmap_item_ids_test.lua b/tests/game3_stitchmap_item_ids_test.lua index 7fb68a03..b544f8db 100644 --- a/tests/game3_stitchmap_item_ids_test.lua +++ b/tests/game3_stitchmap_item_ids_test.lua @@ -7,6 +7,7 @@ -- pokefirered/include/constants/items.h:446 ITEM_SAPPHIRE package.path = "./?.lua;./?/init.lua;" .. package.path +require("tests.game3_cache").requireData("game3_stitchmap_item_ids_test") local failed = 0 local function check(cond, msg) diff --git a/tests/game3_stitchmap_item_messages_test.lua b/tests/game3_stitchmap_item_messages_test.lua index e968174c..2638739c 100644 --- a/tests/game3_stitchmap_item_messages_test.lua +++ b/tests/game3_stitchmap_item_messages_test.lua @@ -4,6 +4,7 @@ -- pokefirered/src/new_menu_helpers.c:641 DisplayItemMessageOnField package.path = "./?.lua;./?/init.lua;" .. package.path +require("tests.game3_cache").requireData("game3_stitchmap_item_messages_test") local failed = 0 local function check(cond, msg) diff --git a/tests/game3_stitchsave_schema_fields_test.lua b/tests/game3_stitchsave_schema_fields_test.lua index 98e1f587..dcb024ff 100644 --- a/tests/game3_stitchsave_schema_fields_test.lua +++ b/tests/game3_stitchsave_schema_fields_test.lua @@ -265,11 +265,9 @@ do Runtime.session = prev end -print("[test] berryPowder round-trips through the schema (review-v3 H8)") +print("[test] berryPowder round-trips through the schema") do - -- review-v3 H8: pret include/global.h:354 SaveBlock2.berryCrush holds - -- berryPowderAmount (src/berry_powder.c:50); the port carries it on the - -- session, so the schema must write and restore the key. + -- include/global.h:354, src/berry_powder.c:50 local session = Schema.newGame({ rngSeed = 0x99 }) eq(session.berryPowder, 0, "a New Game starts with 0 berry powder") diff --git a/tests/game3_stitchsave_summary_test.lua b/tests/game3_stitchsave_summary_test.lua index 1a0ebf73..3ab202dc 100644 --- a/tests/game3_stitchsave_summary_test.lua +++ b/tests/game3_stitchsave_summary_test.lua @@ -32,7 +32,7 @@ end print("[test] 1. CheckPartyPokerus reads the low nibble only") do - -- pokefirered/src/pokemon.c:5630 GetMonData(..., MON_DATA_POKERUS) & 0xF + -- pokefirered/src/pokemon.c:5630 eq(SummaryData.statusAilment({ pokerus = 0 }), AILMENT_NONE, "never infected is no ailment") eq(SummaryData.statusAilment({ pokerus = 0x41 }), AILMENT_PKRS, "strain 4 with 1 day left is PKRS") eq(SummaryData.statusAilment({ pokerus = 0x04 }), AILMENT_PKRS, "4 days left is PKRS") diff --git a/tests/game3_stitchuif_bag_use_test.lua b/tests/game3_stitchuif_bag_use_test.lua index 3f2db339..d625fc5a 100644 --- a/tests/game3_stitchuif_bag_use_test.lua +++ b/tests/game3_stitchuif_bag_use_test.lua @@ -1,6 +1,7 @@ #!/usr/bin/env luajit package.path = "./?.lua;./?/init.lua;" .. package.path +require("tests.game3_cache").requireData("game3_stitchuif_bag_use_test") local failed = 0 local function check(cond, msg) diff --git a/tests/game3_stitchuif_naming_pc_test.lua b/tests/game3_stitchuif_naming_pc_test.lua index 62561d8d..80f14ed5 100644 --- a/tests/game3_stitchuif_naming_pc_test.lua +++ b/tests/game3_stitchuif_naming_pc_test.lua @@ -41,9 +41,7 @@ local input = { wasPressed = function(_, k) return key == k end } local function press(k) key = k - -- pret/pokefirered src/naming_screen.c:559-572: the screen task handles input - -- and its timers in one per-frame pass (MainState_HandleInput). The engine - -- splits that as handleInput() + update(dt) (src/ui/game3/naming.lua:489-511). + -- src/naming_screen.c:559-572 Naming.handleInput(input) Naming.update(1 / 60) key = nil diff --git a/tests/game3_storage_test.lua b/tests/game3_storage_test.lua index 4aeb94e8..0ac8fe74 100644 --- a/tests/game3_storage_test.lua +++ b/tests/game3_storage_test.lua @@ -606,6 +606,21 @@ assert_eq(assetCount, 30, "14 UI textures + 16 box wallpapers (total 30 assets)" print("[ok] All 14 UI textures and 16 wallpapers validated in manifest and file system") end +print("=== [TEST 16] Party-to-Box Move Compacts a Holed Party ===") +do + local function mk(n) return { species = 1, nickname = n, level = 5, hp = 10, maxHp = 10, moves = {} } end + local A, B, C = mk("A"), mk("B"), mk("C") + local holed = { party = { [1] = A, [2] = B, [4] = C }, storage = Storage.new(), bag = Bag.new() } + local okMove = Storage.moveMon(holed, "party", 2, "box", 1) + assert_true(okMove, "Party slot 2 moved into the box") + assert_eq(holed.party[1], A, "Slot 1 keeps A") + assert_eq(holed.party[2], C, "C compacts into slot 2") + assert_eq(holed.party[3], nil, "No third party mon") + assert_eq(holed.party[4], nil, "Old slot 4 cleared") + assert_eq(Storage.getBoxMon(holed.storage, holed.storage.currentBox, 1), B, "B landed in the box") + print("[ok] Holed party compacted without dropping mons") +end + print("\n========================================================") -print("ALL 15 POKÉMON STORAGE & PC SYSTEM TESTS PASSED CLEANLY!") +print("ALL 16 POKÉMON STORAGE & PC SYSTEM TESTS PASSED CLEANLY!") print("========================================================") diff --git a/tests/game3_surf_connections_test.lua b/tests/game3_surf_connections_test.lua index 6dc7e962..b4e50dfe 100644 --- a/tests/game3_surf_connections_test.lua +++ b/tests/game3_surf_connections_test.lua @@ -1,4 +1,5 @@ package.path = "./?.lua;./?/init.lua;" .. package.path +require("tests.game3_cache").requireData("game3_surf_connections_test") require("src.core.GameVersion").set("firered") local Cache = require("tests.game3_cache") assert(Cache.mount("scripts/events.lua", { native = true }), Cache.reason) diff --git a/tests/game3_teachy_model_test.lua b/tests/game3_teachy_model_test.lua index 9b192d2b..2cd9f228 100644 --- a/tests/game3_teachy_model_test.lua +++ b/tests/game3_teachy_model_test.lua @@ -2,6 +2,7 @@ -- pokefirered/src/teachy_tv.c:420 InitTeachyTvController package.path = "./?.lua;./?/init.lua;" .. package.path +require("tests.game3_cache").requireData("game3_teachy_model_test") local failed = 0 local function check(cond, msg) diff --git a/tests/game3_teachy_screen_test.lua b/tests/game3_teachy_screen_test.lua index 4ababb54..2f2348b6 100644 --- a/tests/game3_teachy_screen_test.lua +++ b/tests/game3_teachy_screen_test.lua @@ -215,7 +215,7 @@ do check(Ui.titleArt() == nil, "no title art without the importer key") check(Ui.chrome() == nil, "no border art means the plain chrome draws") check(Ui.bg3Art() == nil, "no BG3 map art either") - -- pokefirered/src/teachy_tv.c:122 sBgTemplates[1].priority = 0 + -- pokefirered/src/teachy_tv.c:122 check(Ui.chromeCutOut() == false, "an absent border is never treated as a cut out") local f = io.open(path, "rb") if f then @@ -298,7 +298,7 @@ do Bag.add(s.bag, TeachyTv.ITEM_TEACHY_TV, 1) -- pokefirered/include/constants/items.h:300 ITEM_TM01 Bag.add(s.bag, 289, 1) - -- pokefirered/include/constants/items.h:143 ITEM_ORAN_BERRY (139) + -- pokefirered/include/constants/items.h:143 Bag.add(s.bag, 139, 2) Bag.add(s.bag, 13, 3) eq(Bag.get(s.bag, TeachyTv.ITEM_TM_CASE), 1, "the TM CASE rides the TM pocket") @@ -431,7 +431,7 @@ do end check(BagMenu.isOpen(), "the pokedude bag opened for the TMs lesson") for _ = 1, 500 do Ui.bagDemo.update(1 / 60) end - -- pokefirered/src/item_menu.c:2391 exitCB = Pokedude_InitTMCase + -- pokefirered/src/item_menu.c:2391 check(TmCase.isOpen(), "the bag handed off to the pokedude TM CASE") check(not BagMenu.isOpen(), "and the bag closed") eq(Stack.top() and Stack.top().id, "teachy_pokedude_tm_case", "the TM demo drives it") @@ -442,7 +442,7 @@ do local demo = Ui.tmDemo for _ = 1, 720 do demo.update(1 / 60) end - -- pokefirered/src/tm_case.c:1422 gPokedudeText_TMTypes + -- pokefirered/src/tm_case.c:1422 eq(TmCase.mode, "message", "the POKé DUDE starts talking") eq(TmCase.messageText, TeachyTv.pagesOf(TeachyTv.TM_TYPES)[1], "gPokedudeText_TMTypes page 1") local pages = #TeachyTv.pagesOf(TeachyTv.TM_TYPES) diff --git a/tests/game3_town_map_test.lua b/tests/game3_town_map_test.lua index cb4e7953..1e6cfc00 100644 --- a/tests/game3_town_map_test.lua +++ b/tests/game3_town_map_test.lua @@ -219,9 +219,7 @@ do local detail = RegionExtract.run(rom, mockCache, { cacheRoot = "data/generated/gba" }) check(detail.ok == true, "RegionMapExtract.run returned ok") - -- 10 chrome assets, confirmed against pret: src/region_map.c:424 - -- (dungeon_icon), :425 (fly_icon), :395-396 and :405-406 - -- (player_icon_red / player_icon_leaf). + -- src/region_map.c:424 check(detail.count == 10, "Extracted 10 region map chrome assets") local kantoRgba = mockCache:read("data/generated/gba/region_map/kanto_map.rgba") diff --git a/tests/game3_trade_rules_test.lua b/tests/game3_trade_rules_test.lua index cedecddb..f56be43f 100644 --- a/tests/game3_trade_rules_test.lua +++ b/tests/game3_trade_rules_test.lua @@ -309,7 +309,7 @@ eq(Trade.canTradeSelectedMon({ { species = 252, nickname = "TREECKO", level = 5 }, party[2], }, 0, { nationalDex = false }), Trade.CANT_TRADE_NATIONAL, "without the National Dex a non-Kanto mon cannot be traded") --- pokefirered/src/trade.c:2787-2788 SPECIES_EGG -> CANT_TRADE_PARTNER_EGG_YET +-- pokefirered/src/trade.c:2787-2788 eq(Trade.canTradeSelectedMon({ { species = 25, isEgg = true, level = 5 }, party[2], }, 0, { nationalDex = true, partner = { version = 4, progressFlags = 0 } }), diff --git a/tests/game3_trainer_card_photo_tint_test.lua b/tests/game3_trainer_card_photo_tint_test.lua index 9657d33a..8650ec69 100644 --- a/tests/game3_trainer_card_photo_tint_test.lua +++ b/tests/game3_trainer_card_photo_tint_test.lua @@ -1,12 +1,3 @@ --- End-to-end proof for the Game Corner photo -> trainer-card tint chain. --- Mirrors the real in-game wiring: --- setvar VAR_0x8004 (ctx.specialVars) -> special UpdateTrainerCardPhotoIcons (0x167) --- -> Flags.setVar into THE SAME store the card reads (Space.store in-game) --- -> Flags.serialize (game.save) -> Flags.loadInto (next boot) --- -> TrainerCard.cardData/gather -> c.monIconTint / c.monSpecies. --- Ground truth: saves/firered/slot1.lua (Sep 22 18:24) carries --- ["16450"]=2, ["16451"]=4, ["16452".."16456"]=0 with a 1-mon party (Charmander). - package.path = "./?.lua;./?/init.lua;" .. package.path local Std = require("src.core.game3.scripting.stdscripts") @@ -51,11 +42,9 @@ print("=== 1. Photo script path writes tint + species into THE store the card re local session, store, ctx do session = Schema.newGame({ name = "RED" }) - session.party = { { speciesId = 4, species = 4 } } -- user's save: one Charmander + session.party = { { speciesId = 4, species = 4 } } store = Flags.newStore() - -- In-game both sides resolve Space from package.loaded; share one table so - -- scriptStore(ctx) (natives) and script_store() (trainer card) see ONE store. package.loaded[SPACE_KEY] = { store = store, getStore = function() return store end } package.loaded[RT_KEY] = { getSession = function() return session end } @@ -65,8 +54,7 @@ do specialVars = {}, } - -- Real setvar opcode path: Flags.setVar routes 0x80xx to ctx.specialVars. - Flags.setVar(store, ctx, 0x8004, 2) -- MON_ICON_TINT_PINK + Flags.setVar(store, ctx, 0x8004, 2) local yielded = Natives.special(ctx, Std.SPECIAL.UpdateTrainerCardPhotoIcons) check(yielded == false, "UpdateTrainerCardPhotoIcons completes without yielding") @@ -102,8 +90,8 @@ end print("=== 3. Store unavailable: session.vars fallback must read string keys ===") do local snap = Flags.serialize(store) - session.vars = snap.vars -- save-format string keys, numeric miss - package.loaded[SPACE_KEY] = nil -- Space not resolvable -> script_store() nil + session.vars = snap.vars + package.loaded[SPACE_KEY] = nil local c = TrainerCard.cardData(session) checkEq(c.monIconTint, 2, "fallback reads [\"16450\"] -> monIconTint=2") @@ -126,7 +114,7 @@ do package.loaded[RT_KEY] = { getSession = function() return session4 end } local ctx4 = { flags = session4.flags, vars = session4.vars, specialVars = {} } - Flags.setVar(store4, ctx4, 0x8004, 2) -- MON_ICON_TINT_PINK + Flags.setVar(store4, ctx4, 0x8004, 2) Natives.special(ctx4, Std.SPECIAL.UpdateTrainerCardPhotoIcons) checkEq(Flags.getVar(store4, ctx4, 0x4042), 2, "multi-mon: store 0x4042 tint idx is 2") diff --git a/tests/game3_ui_region_map_fly_test.lua b/tests/game3_ui_region_map_fly_test.lua index 04f93fc3..95e5315b 100644 --- a/tests/game3_ui_region_map_fly_test.lua +++ b/tests/game3_ui_region_map_fly_test.lua @@ -259,6 +259,36 @@ do "the RS link reveals the Cerulean Cave marker") end +print("[test] 9. START snap order and dungeon icon origin") +do + local function at() return RegionMap.cursorX .. "," .. RegionMap.cursorY end + RegionMap.show({ session = { map = "PALLET_TOWN", gender = 0 } }) + local home = at() + check(RegionMap.hasSwitchButton() == false, "no switch button before the Sevii map") + press("start") + eq(at(), "21,13", "no switch button: START snaps to CANCEL") + press("start") + eq(at(), home, "then back to the player") + RegionMap.close() + + setWorldMapFlag("FLAG_SYS_SEVII_MAP_123") + RegionMap.show({ session = { map = "PALLET_TOWN", gender = 0 } }) + check(RegionMap.hasSwitchButton() == true, "the Sevii flag adds the switch button") + press("start") + eq(at(), "21,11", "switch button: START snaps to SWITCH first") + press("start") + eq(at(), "21,13", "then CANCEL") + press("start") + eq(at(), home, "then back to the player") + + local found = false + for _, icon in ipairs(RegionMap.dungeonIcons()) do + if icon.px == 32 + 4 * 8 + 2 and icon.py == 32 + 14 * 8 + 2 then found = true end + end + check(found, "the Pokemon Mansion marker sits at 8x+32+offset") + RegionMap.close() +end + if failed > 0 then print(failed .. " CHECK(S) FAILED") os.exit(1) diff --git a/tests/game3_wireless_specials_test.lua b/tests/game3_wireless_specials_test.lua index 4f0e60c5..1f467914 100644 --- a/tests/game3_wireless_specials_test.lua +++ b/tests/game3_wireless_specials_test.lua @@ -1,12 +1,6 @@ #!/usr/bin/env luajit package.path = "./?.lua;./?/init.lua;" .. package.path --- Specials Binder wave: the 20 previously-unbound cart specials plus the two --- cable-club object specials (Script_FacePlayer 0x127 / Script_ClearHeldMovement --- 0x128). Asserts every id has dispatch coverage and that the live handlers --- behave: berry-powder math, e-Reader fallbacks, wireless abort answers, --- museum fossil state (dex flags untouched), and scene audio. - local failed = 0 local function check(cond, msg) if cond then @@ -35,21 +29,16 @@ local A = { log = function() end } print("=== 1. all 22 specials of the wave are declared and bound ===") local WANT = { - -- wireless / berry powder (10) "ChooseMonForWirelessMinigame", "IsPokemonJumpSpeciesInParty", "ShowPokemonJumpRecords", "ShowDodrioBerryPickingRecords", "DisplayBerryPowderVendorMenu", "RemoveBerryPowderVendorMenu", "Script_HasEnoughBerryPowder", "Script_TakeBerryPowder", "PrintPlayerBerryPowderAmount", "ShowBerryCrushRankings", - -- ending / scenes (5) "DoCredits", "ShowDiploma", "DoSSAnneDepartureCutscene", "DoPokemonLeagueLightingEffect", "LoopWingFlapSound", - -- museum fossil (2) "OpenMuseumFossilPic", "CloseMuseumFossilPic", - -- e-Reader fallback (3) "BufferEReaderTrainerName", "BufferEReaderTrainerGreeting", "SetEReaderTrainerGfxId", - -- cable-club object specials (2) "Script_FacePlayer", "Script_ClearHeldMovement", } for _, name in ipairs(WANT) do @@ -104,8 +93,7 @@ Natives.special(ctx, Std.SPECIAL.SetEReaderTrainerGfxId, A) checkEq(Flags.getVar(session, ctx, 0x4010), 18, "VAR_OBJ_GFX_ID_0 = OBJ_EVENT_GFX_YOUNGSTER (18)") --- No card record: both buffers still carry printable text (Room1 scripts.inc:88 --- prints {STR_VAR_1}; Room2 scripts.inc:18-19 prints gStringVar4). +-- scripts.inc:88, scripts.inc:18-19 Natives.special(ctx, Std.SPECIAL.BufferEReaderTrainerName, A) check(type(ctx.stringVars[1]) == "string" and #ctx.stringVars[1] > 0, "STR_VAR_1 holds a fallback name with no card") @@ -113,7 +101,6 @@ Natives.special(ctx, Std.SPECIAL.BufferEReaderTrainerGreeting, A) check(type(ctx.stringVars[4]) == "string" and #ctx.stringVars[4] > 0, "STR_VAR_4 holds a fallback greeting with no card") --- A stored record supplies the visiting trainer's name. session.ereaderTrainer = { name = "ALPHA", party = { { species = 141 } } } Natives.special(ctx, Std.SPECIAL.BufferEReaderTrainerName, A) checkEq(ctx.stringVars[1], "ALPHA", "STR_VAR_1 = the visiting trainer's name") @@ -124,7 +111,7 @@ session.ereaderTrainer = nil print("=== 5. cable-club object specials ===") local faced -Flags.setVar(nil, ctx, 0x800F, 3) -- VAR_LAST_TALKED +Flags.setVar(nil, ctx, 0x800F, 3) local yFace = Natives.special(ctx, Std.SPECIAL.Script_FacePlayer, { log = A.log, facePlayer = function(lid) faced = lid end, @@ -181,17 +168,14 @@ checkEq(horn, 249, "SS Anne departure toots SE_SS_ANNE_HORN (249)") local flaps = {} local function capture(id) flaps[#flaps + 1] = id end -Flags.setVar(nil, ctx, 0x8004, 2) -- num loops -Flags.setVar(nil, ctx, 0x8005, 1) -- frame delay +Flags.setVar(nil, ctx, 0x8004, 2) +Flags.setVar(nil, ctx, 0x8005, 1) Natives.special(ctx, Std.SPECIAL.LoopWingFlapSound, { log = A.log, playSe = capture }) checkEq(flaps[1], 150, "first flap plays SE_M_WING_ATTACK (150) immediately") local okT, Task = pcall(require, "src.core.game3.task") if okT and Task then for _ = 1, 4 do Task.update(1 / 60) end - -- review-v3 Q11 — Knock Off precedent (pret proves the old expectation - -- stale, cite mandatory): field_specials.c:2553 destroys at - -- data[0] == loops - 1, so loops=2, delay=1 plays exactly 2 total - -- (1 entry + 1 tick) — the old ">= 3" asserted the engine's off-by-one. + -- field_specials.c:2553 checkEq(#flaps, 2, "loops=2 plays exactly 2 total flaps (pret parity)") Task.clear() else diff --git a/tests/mod_catalog_tests.lua b/tests/mod_catalog_tests.lua index 89f03574..30da5a84 100644 --- a/tests/mod_catalog_tests.lua +++ b/tests/mod_catalog_tests.lua @@ -65,8 +65,6 @@ check(not Damage.isSpecial("FAIRY"), "an unknown type is not special") -- ------- the fixture --- Data honours POKEPORT_DATA_DIR; require("data.generated.*") has no package --- searcher under it (same seam as run_tests:2280). local Data = require("src.core.Data") if not Data.type_chart then Data:load() end local vanillaChart = Data.type_chart diff --git a/tests/mod_registry_tests.lua b/tests/mod_registry_tests.lua index 1294baf8..e9506646 100644 --- a/tests/mod_registry_tests.lua +++ b/tests/mod_registry_tests.lua @@ -506,9 +506,6 @@ check(removedRefError:find("OPP_TEST", 1, true) ~= nil -- ------- every vanilla record must satisfy its schema, so the shipped -- example's copy-the-base-record override idiom always validates cleanly --- Data honours POKEPORT_DATA_DIR; require("data.generated.*") has no package --- searcher under it (same seam as run_tests:2280). Guard-loaded: run_tests --- already loaded it, standalone runs it here. local Data = require("src.core.Data") if not Data.maps then Data:load() end local vanillaSets = { diff --git a/tests/mod_runtime_tests.lua b/tests/mod_runtime_tests.lua index b9cc0009..eb3c0cfb 100644 --- a/tests/mod_runtime_tests.lua +++ b/tests/mod_runtime_tests.lua @@ -233,9 +233,6 @@ check(okLoad and fixture.pokemon.PIDGEY == nil, "and not the generated one") check(okLoad and fixture.constants.partyMax == 6, "fixture constants pass through seedDefaults") --- restore the caller's env: leaving POKEPORT_DATA_DIR unset poisons every --- suite and child process that runs after this one in the same runner --- (run_tests.lua's dofile list and its os.execute tiers see no data dir). if originalDataDir and originalDataDir ~= "" then setVar("POKEPORT_DATA_DIR", originalDataDir) else diff --git a/tests/mod_ui_tests.lua b/tests/mod_ui_tests.lua index fbc9e01e..0d4d7f5a 100644 --- a/tests/mod_ui_tests.lua +++ b/tests/mod_ui_tests.lua @@ -769,8 +769,6 @@ check(title.logo and title.logo.path == "mods/x/logo.png", check(title.version and title.version.path == "mods/x/ribbon.png", "versionRibbon wins as the file-12 patch key") -- pin against the shipped data itself: a real boot must load the logo --- art, never fall back to the ASCII placeholder. Data.field honours --- POKEPORT_DATA_DIR; a raw dofile of data/generated/* never did. local Data = require("src.core.Data") if not Data.field then Data:load() end title = TitleState.new({ data = { field = Data.field } }, diff --git a/tests/mod_world_tests.lua b/tests/mod_world_tests.lua index 0793a01b..9cf42d7a 100644 --- a/tests/mod_world_tests.lua +++ b/tests/mod_world_tests.lua @@ -50,14 +50,6 @@ local TOWN_PALS = { CINNABAR_ISLAND = "CINNABAR", INDIGO_PLATEAU = "INDIGO", SAFFRON_CITY = "SAFFRON", } --- gen1 authority note: for FireRed/Gen 1 rows the source of truth is this --- engine's own gen1 code + fixtures, NOT pret/pokefirered. Since the --- de-Kanto milestone the no-memory rung is boot-derived via --- SaveData.defaultHeal (src/core/SaveData.lua:2575-2582): vanilla --- REDS_HOUSE_2F maps to PALLET_TOWN (SaveData.lua:2580, mirroring pokered's --- `wLastBlackoutMap := PALLET_TOWN`), so on vanilla boot the answer is still --- PALLET exactly as the old hardcoded literal below, while a redirected --- total-conversion boot (fixture field.boot.lastHeal = FIX_TOWN) wins by design. local SaveData = require("src.core.SaveData") local BOOT_HEAL_MAP = SaveData.defaultHeal((Data.field and Data.field.boot) or {}).map @@ -70,8 +62,6 @@ local function oldPaletteNameFor(def, lastOutdoorId) elseif TOWN_PALS[id] or id:match("^ROUTE_") then return TOWN_PALS[id] or "ROUTE" end - -- boot-derived zero-fill (see BOOT_HEAL_MAP above); the pre-milestone - -- literal was `or "PALLET_TOWN"`, equal for vanilla boot data. local last = lastOutdoorId or BOOT_HEAL_MAP return TOWN_PALS[last] or "ROUTE" end diff --git a/tests/parity_ball_shake_anim.lua b/tests/parity_ball_shake_anim.lua index 385f892b..a34c7664 100644 --- a/tests/parity_ball_shake_anim.lua +++ b/tests/parity_ball_shake_anim.lua @@ -15,8 +15,6 @@ local S = require("tests.harness").suite("parity ball shake anim") local check, eq = S.check, S.eq local AnimPlayer = require("src.battle.AnimPlayer") --- Data honours POKEPORT_DATA_DIR; require("data.generated.*") has no package --- searcher under it (same seam as run_tests:2280). local Data = require("src.core.Data") if not Data.battle_anims then Data:load() end local player = AnimPlayer.new(Data.battle_anims) diff --git a/tests/parity_hidden_coins_bcd_bug1810.lua b/tests/parity_hidden_coins_bcd_bug1810.lua index 196b3238..d511ecb9 100644 --- a/tests/parity_hidden_coins_bcd_bug1810.lua +++ b/tests/parity_hidden_coins_bcd_bug1810.lua @@ -23,8 +23,6 @@ eq(pay(0), 100, "anything the branch chain misses pays 100") -- the generated data still carries the raw argument: the runtime mapping, -- not the extractor, is what turns the 40 tile into 20 --- Data honours POKEPORT_DATA_DIR; a raw dofile of data/generated/* never did --- (same seam as run_tests:2280). local Data = require("src.core.Data") if not Data.field then Data:load() end local field = Data.field diff --git a/tests/parity_mart_stock.lua b/tests/parity_mart_stock.lua index 3a12c705..9899aa72 100644 --- a/tests/parity_mart_stock.lua +++ b/tests/parity_mart_stock.lua @@ -22,8 +22,6 @@ if not _G.love then _G.love = require("tests.love_stub") end local S = require("tests.harness").suite("parity mart stock") local check, eq = S.check, S.eq --- Data honours POKEPORT_DATA_DIR; a raw dofile of data/generated/* never did --- (same seam as run_tests:2280). local Data = require("src.core.Data") if not Data.text_pointers then Data:load() end local T = Data.text_pointers diff --git a/tests/parity_rival_walkoff.lua b/tests/parity_rival_walkoff.lua index da14dff0..92ed21ee 100644 --- a/tests/parity_rival_walkoff.lua +++ b/tests/parity_rival_walkoff.lua @@ -16,7 +16,6 @@ local realCommands = package.loaded["src.script.Commands"] local realPicBox = package.loaded["src.ui.PicBox"] package.loaded["src.core.Music"] = { play = function() end, playOnce = function() return true end, stop = function() end, - -- T3 class C: OverworldController.lua:616 calls playMap on map enter. playMap = function() end, } package.loaded["src.script.Commands"] = { diff --git a/tests/parity_rocket3_sight_bug1814.lua b/tests/parity_rocket3_sight_bug1814.lua index 51acd5b8..a2a4dbf5 100644 --- a/tests/parity_rocket3_sight_bug1814.lua +++ b/tests/parity_rocket3_sight_bug1814.lua @@ -20,8 +20,6 @@ local check, eq = S.check, S.eq local MAP = "ROCKET_HIDEOUT_B4F" local TEXT = "TEXT_ROCKETHIDEOUTB4F_ROCKET3" --- the sight range the engine reads. Data honours POKEPORT_DATA_DIR; a raw --- dofile of data/generated/* never did (same seam as run_tests:2280). local Data = require("src.core.Data") if not Data.maps then Data:load() end local headers = Data.trainer_headers diff --git a/tests/parity_silph_rival_bug2241.lua b/tests/parity_silph_rival_bug2241.lua index 650fe374..6f7ff02d 100644 --- a/tests/parity_silph_rival_bug2241.lua +++ b/tests/parity_silph_rival_bug2241.lua @@ -6,8 +6,6 @@ local S = require("tests.harness").suite("parity silph co 7f rival") local check, eq = S.check, S.eq local realMusic = package.loaded["src.core.Music"] --- T3 class C: include playMap — OverworldController.lua:616 calls it on --- map enter, and a leaked stub must not take later suites down with it. package.loaded["src.core.Music"] = { play = function() end, playMap = function() end } local story5 = dofile("data/scripts/story5.lua") diff --git a/tests/parity_ss_anne_departure.lua b/tests/parity_ss_anne_departure.lua index 370c388b..5b4069f0 100644 --- a/tests/parity_ss_anne_departure.lua +++ b/tests/parity_ss_anne_departure.lua @@ -21,7 +21,6 @@ package.loaded["src.core.Music"] = { play = function(_, id) music.played[#music.played + 1] = id end, playOnce = function() return true end, stop = function() music.played[#music.played + 1] = "stop" end, - -- T3 class C: OverworldController.lua:616 calls playMap on map enter. playMap = function() end, } package.loaded["src.render.TextBox"] = { @@ -31,9 +30,6 @@ package.loaded["src.ui.PicBox"] = { new = function() return {} end } local story3 = dofile("data/scripts/story3.lua") local story5 = dofile("data/scripts/story5.lua") --- Data honours POKEPORT_DATA_DIR; a raw dofile of data/generated/* never did --- (same seam as run_tests:2280). story3/story5 stay dofile'd: data/scripts --- is committed source, not generated output. local Data = require("src.core.Data") if not Data.maps then Data:load() end local text = Data.text diff --git a/tests/parity_ss_anne_guard.lua b/tests/parity_ss_anne_guard.lua index 1db6d89f..ae5eb792 100644 --- a/tests/parity_ss_anne_guard.lua +++ b/tests/parity_ss_anne_guard.lua @@ -39,7 +39,6 @@ package.loaded["src.core.Music"] = { musicCalls[#musicCalls + 1] = { "playOnce", song } return true end, - -- T3 class C: OverworldController.lua:616 calls playMap on map enter. playMap = function() end, } diff --git a/tests/parity_ss_anne_rooms.lua b/tests/parity_ss_anne_rooms.lua index 20586e57..9e0ce770 100644 --- a/tests/parity_ss_anne_rooms.lua +++ b/tests/parity_ss_anne_rooms.lua @@ -5,24 +5,10 @@ if not _G.love then _G.love = require("tests.love_stub") end local S = require("tests.harness").suite("parity ss anne rooms") local check, eq = S.check, S.eq --- POKEPORT_DATA_DIR seam, but deliberately NOT Data.maps: Data:load hands --- back SS_ANNE_1F with the layout patch already folded in (leftmost door -> --- rooms #1), while the raw-extract checks below pin the ON-DISK order --- (leftmost -> #6) and then apply SsAnneLayout themselves. --- SSANNE-ROOMS adjudication: load the pinned ON-DISK file directly. --- Data._loadModule(nil, "maps") is version-ambiguous: with the env unset it --- resolves CacheFs.readActive first (active-versioned save-dir bytes — on a --- box with a firered cache that is FRLG's maps, door order 1..6) and falls --- back to require, which can hand back Data:load's in-place-patched table --- (Data.lua:184) — either way the raw-order asserts flipped to got 1/want 6 --- while the extract itself is correct (pokered order: on-disk leftmost -> #6 --- [unverified-pokered: pokered SS Anne 1F warp table would settle it]). --- dofile re-reads the file fresh; files are never patched in place. local envDir = os.getenv("POKEPORT_DATA_DIR") local mapsPath = (envDir and envDir .. "/maps.lua") or "data/generated/maps.lua" local okMaps, freshMaps = pcall(dofile, mapsPath) if not okMaps then - -- assert(v, msg) would return BOTH args (v==true), so branch explicitly error("fresh maps module load failed: " .. tostring(freshMaps)) end local maps = freshMaps diff --git a/tests/parity_tower_rival.lua b/tests/parity_tower_rival.lua index 001a7425..7f2887c9 100644 --- a/tests/parity_tower_rival.lua +++ b/tests/parity_tower_rival.lua @@ -24,7 +24,6 @@ package.loaded["src.core.Music"] = { play = function() end, playOnce = function() return true end, stop = function() end, - -- T3 class C: OverworldController.lua:616 calls playMap on map enter. playMap = function() end, } diff --git a/tests/parity_wavy_screen.lua b/tests/parity_wavy_screen.lua index e333fd87..dbd0cbbd 100644 --- a/tests/parity_wavy_screen.lua +++ b/tests/parity_wavy_screen.lua @@ -14,8 +14,6 @@ local S = require("tests.harness").suite("parity wavy screen") local check, eq = S.check, S.eq local AnimPlayer = require("src.battle.AnimPlayer") --- Data honours POKEPORT_DATA_DIR; require("data.generated.*") has no package --- searcher under it (same seam as run_tests:2280). local Data = require("src.core.Data") if not Data.battle_anims then Data:load() end local player = AnimPlayer.new(Data.battle_anims) diff --git a/tests/run_tests.lua b/tests/run_tests.lua index e254d196..51ff7a67 100644 --- a/tests/run_tests.lua +++ b/tests/run_tests.lua @@ -2277,9 +2277,6 @@ end -- plays a tink + 40-frame pause per shake, rewinding the same subanim. do local AnimPlayer = require("src.battle.AnimPlayer") - -- Data:load() (line 26) already loaded battle_anims; a plain - -- require("data.generated.*") has no package searcher under - -- POKEPORT_DATA_DIR and crashes the whole run mid-file. local ap = AnimPlayer.new(Data.battle_anims) ap:start("SHAKE_ANIM", true, { shakes = 3 }) local tinks = 0 @@ -3590,12 +3587,6 @@ local function restoreLove(snap) end local function runSuites(paths) - -- Parity suites replace these two modules with partial stubs and restore - -- them at their end; a mid-suite failure skips that restore and leaks the - -- stub to every later suite in this single process (wave-3 T3 classes: - -- Music.playMap nil at OverworldController:616, TextBox.strip/paginate/ - -- arrowPos nils, cascading text-box asserts). Restore the pre-suite - -- binding around each dofile, mirroring the love snapshot below. local LEAKED_KEYS = { "src.render.TextBox", "src.core.Music" } for _, path in ipairs(paths) do local label = path:match("([^/]+)%.lua$") or path