This commit is contained in:
bryanthaboi
2026-09-22 17:05:12 -04:00
parent 8ad3a53770
commit d920f7acdb
236 changed files with 805 additions and 2181 deletions
+1 -1
View File
@@ -1 +1 @@
{"schema_version":1,"mods":[]}
{"mods":[],"schema_version":1}
+57 -143
View File
@@ -23,19 +23,6 @@
# POKEPORT_IDENTITY=pokeport-test-caches POKEPORT_VERSION=<version> \
# POKEPORT_IMPORT_ONLY=1 POKEPORT_IMPORT_ROM="<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" <suite> 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"
-6
View File
@@ -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
-- ---------------------------------------------------------------------------
+1 -17
View File
@@ -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
-9
View File
@@ -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"]
-13
View File
@@ -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)
-9
View File
@@ -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
-5
View File
@@ -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
+1 -4
View File
@@ -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
+1 -4
View File
@@ -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
-3
View File
@@ -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
-1
View File
@@ -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)
@@ -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)
@@ -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 = {}
@@ -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 }
+1 -3
View File
@@ -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)
-1
View File
@@ -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
+1 -13
View File
@@ -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 "<port>_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
-1
View File
@@ -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
-2
View File
@@ -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 }
+1 -3
View File
@@ -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
-2
View File
@@ -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))
+2 -5
View File
@@ -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
+12 -20
View File
@@ -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
+1 -3
View File
@@ -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
+2 -22
View File
@@ -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
+12 -5
View File
@@ -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
+2 -6
View File
@@ -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
+8 -13
View File
@@ -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
+3 -7
View File
@@ -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
-5
View File
@@ -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,
+2 -7
View File
@@ -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
-3
View File
@@ -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)
-2
View File
@@ -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
-2
View File
@@ -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
-14
View File
@@ -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"
+4 -49
View File
@@ -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
-2
View File
@@ -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
-1
View File
@@ -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))
-3
View File
@@ -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
-4
View File
@@ -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 = {}
+1 -2
View File
@@ -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)
-2
View File
@@ -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
+6 -13
View File
@@ -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
+3 -20
View File
@@ -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
-2
View File
@@ -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
-4
View File
@@ -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
-4
View File
@@ -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
+2 -6
View File
@@ -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 }
-2
View File
@@ -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
-4
View File
@@ -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)
+1 -4
View File
@@ -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)
-2
View File
@@ -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
+2 -7
View File
@@ -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
-3
View File
@@ -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
+13 -32
View File
@@ -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,
-5
View File
@@ -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
+2 -8
View File
@@ -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)
-2
View File
@@ -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
-2
View File
@@ -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
-3
View File
@@ -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])
-4
View File
@@ -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
-18
View File
@@ -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 = {}
+9 -45
View File
@@ -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",
+5
View File
@@ -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
-4
View File
@@ -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
+11 -17
View File
@@ -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 = {} },
-6
View File
@@ -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)
+1 -3
View File
@@ -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
+1 -4
View File
@@ -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
-10
View File
@@ -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
+16 -47
View File
@@ -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)
+21 -63
View File
@@ -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
+4 -13
View File
@@ -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
+26 -70
View File
@@ -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
+2 -9
View File
@@ -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,
+2 -6
View File
@@ -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
@@ -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
+16 -61
View File
@@ -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
+4 -10
View File
@@ -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 }),
+22 -51
View File
@@ -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
+1 -2
View File
@@ -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
+8 -7
View File
@@ -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 = {}
+1 -2
View File
@@ -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
+6 -29
View File
@@ -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 = {}
-1
View File
@@ -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
-18
View File
@@ -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)
+2 -8
View File
@@ -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)
+1 -16
View File
@@ -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
-2
View File
@@ -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
-17
View File
@@ -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)
+1 -5
View File
@@ -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)
-1
View File
@@ -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
-8
View File
@@ -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)
-1
View File
@@ -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 = {
+47 -34
View File
@@ -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.
+1 -3
View File
@@ -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
+2 -6
View File
@@ -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
+1 -17
View File
@@ -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 = {}
-7
View File
@@ -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" },

Some files were not shown because too many files have changed in this diff Show More