mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 21:35:42 -04:00
clean up
This commit is contained in:
@@ -1 +1 @@
|
||||
{"schema_version":1,"mods":[]}
|
||||
{"mods":[],"schema_version":1}
|
||||
+57
-143
@@ -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"
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 = {} },
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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 = {}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user