diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5892cd7a..984bff2a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -46,6 +46,12 @@ jobs: runs-on: [self-hosted, macOS] steps: + # The self-hosted runner lives under the machine owner's home + # directory; mask it first so absolute paths in every later step's + # output show up as *** in the public workflow logs. + - name: Mask runner paths + run: echo "::add-mask::$HOME" + - name: Checkout uses: actions/checkout@v4 with: @@ -171,6 +177,11 @@ jobs: set -euo pipefail scripts/build_android.sh --version "${{ steps.ver.outputs.version }}" + - name: Install xcbeautify + run: | + set -euo pipefail + brew list xcbeautify >/dev/null 2>&1 || brew install xcbeautify + - name: Build iOS run: | set -euo pipefail diff --git a/data/scripts/oaks_lab.lua b/data/scripts/oaks_lab.lua index 2105ae85..82ebb4bb 100644 --- a/data/scripts/oaks_lab.lua +++ b/data/scripts/oaks_lab.lua @@ -6,6 +6,8 @@ -- takes it ("I'll take this one, then!") and both balls disappear. -- Source: scripts/OaksLab.asm OaksLabCharmanderPokeBallText / -- OaksLabRivalTakePokeBallScript. +-- * Leftover ball (after the pick): Oak turns and reads the last-mon +-- line instead of re-offering the starter (OaksLabLastMonScript, #601). -- * Rival (object 1): before starter -> "go ahead and choose" once Oak -- has walked you in, else "gramps isn't around" (#218); with -- starter -> taunt + battle OPP_RIVAL1 with the counter-pick party @@ -23,7 +25,7 @@ local function starterBall(askText, species, choseFlag, ownBall, { "jump_if_true", 20 }, -- 2 -- no picking until Oak has walked you in (OaksLabScript gating) { "check_flag", "EVENT_FOLLOWED_OAK_INTO_LAB" }, -- 3 - { "jump_if_false", 20 }, -- 4 + { "jump_if_false", 22 }, -- 4 -- the Pokédex "new species" entry shows before the ask (predef -- StarterDex ahead of OaksLabYouWant...Text). StarterDex temporarily -- sets the owned bits so ShowPokedexData prints height/weight/text; @@ -31,7 +33,7 @@ local function starterBall(askText, species, choseFlag, ownBall, { "push_screen", "DexEntryMenu", { species = species, forceOwned = true } }, -- 5 { "ask", askText }, -- 6 - { "jump_if_false", 21 }, -- 7 + { "jump_if_false", "end" }, -- 7 -- OaksLab.asm prints ReceivedMon then AddPartyMon (AskName lives -- inside give_pokemon). Show the received text first so the -- nickname prompt follows "you got X", matching Gen1. @@ -52,9 +54,16 @@ local function starterBall(askText, species, choseFlag, ownBall, { RAM = rivalBall == "OAKSLAB_CHARMANDER_POKE_BALL" and "CHARMANDER" or rivalBall == "OAKSLAB_SQUIRTLE_POKE_BALL" and "SQUIRTLE" or "BULBASAUR" } }, -- 17 - { "jump", 21 }, -- 18 - { "jump", 21 }, -- 19 (spacer) - { "show_text", "_OaksLabThoseArePokeBallsText" }, -- 20 + { "jump", "end" }, -- 18 + { "jump", "end" }, -- 19 (spacer) + -- a leftover ball after the player's pick: Oak turns to face the + -- player and reads the last-mon line instead of re-offering the + -- starter (scripts/OaksLab.asm OaksLabSelectedPokeBallScript -> + -- OaksLabLastMonScript; #601). The ROM's "#MON" ligature is spelled + -- out as Pokémon here. + { "face_object", 5, "down" }, -- 20 + { "show_text", "That's PROF.OAK's\nlast Pokémon!" }, -- 21 + { "show_text", "_OaksLabThoseArePokeBallsText" }, -- 22 } end diff --git a/mobile/ios/LOVE_VERSION b/mobile/ios/LOVE_VERSION index 8204473e..d7213f31 100644 --- a/mobile/ios/LOVE_VERSION +++ b/mobile/ios/LOVE_VERSION @@ -1 +1 @@ -11.5 +12.0 diff --git a/mobile/ios/README.md b/mobile/ios/README.md index 25a0f0b6..e1a9f48a 100644 --- a/mobile/ios/README.md +++ b/mobile/ios/README.md @@ -1,4 +1,4 @@ -# iOS build (LÖVE 11.5) +# iOS build (LÖVE 12.0) > **Native ROM/mod/save import.** The iOS build ships a Swift > document-picker bridge (`native/GRPickerBridge.swift` + `GRBootstrap.m`) @@ -20,20 +20,17 @@ > The note below about a missing "UIDocumentPicker handoff" is > resolved by this bridge. -macOS + Xcode only. Pins the official **LÖVE 11.5** iOS Xcode tree -(`love-11.5-ios-source.zip` from [love2d/love releases](https://github.com/love2d/love/releases/tag/11.5)), -matching `conf.lua`'s `t.version = "11.5"`. +macOS + Xcode only. Fetches the **LÖVE 12.0** source tree and matching Apple +dependencies from the official [LÖVE source](https://github.com/love2d/love) +and [Apple dependencies](https://github.com/love2d/love-apple-dependencies) +repositories. `conf.lua` declares LÖVE 12.0 on iOS and 11.5 elsewhere. -There is no separate `love2d/love-ios` GitHub repo for 11.5; the release zip -**is** the vendored iOS project (Xcode project under -`love-src/platform/xcode/love.xcodeproj`, target `love-ios`). - -Pin file: [`LOVE_VERSION`](./LOVE_VERSION) → `11.5`. +Pin file: [`LOVE_VERSION`](./LOVE_VERSION) → `12.0`. ## Quick start (simulator) ```bash -# Fetch LÖVE 11.5 iOS sources (once) + build for Simulator +# Fetch LÖVE 12.0 iOS sources and dependencies (once) + build for Simulator scripts/build_ios.sh --fetch ``` @@ -80,10 +77,10 @@ Manual out-of-band steps: | Path | Role | |------|------| -| `LOVE_VERSION` | Engine pin (`11.5`) | +| `LOVE_VERSION` | Engine pin (`12.0`) | | `overlays/love-ios.plist` | Portrait-only Info.plist + display name **Pokemon Red** (copied over the upstream plist every build) | -| `love-src/` | Downloaded `love-11.5-ios-source` tree (**gitignored**, do not commit) | -| `cache/` | Downloaded zips (**gitignored**) | +| `love-src/` | Downloaded LÖVE 12.0 source tree (**gitignored**, do not commit) | +| `cache/` | Temporary source and dependency checkout data (**gitignored**) | | `build/` | `xcodebuild` derived data (**gitignored**) | Game payload lands at: @@ -94,28 +91,10 @@ and is fused into the built `.app` (LÖVE auto-runs any bundled `*.love`). ## Apple libraries dependency -The official `love-11.5-ios-source.zip` already ships prebuilt iOS -xcframeworks under `platform/xcode/ios/libraries/` (SDL2, LuaJIT, freetype, -ogg, vorbis, theora, modplug). - -If that folder is missing or incomplete (e.g. you cloned sources without -libs), download the matching prebuilts and install them: - -```bash -curl -fL -o mobile/ios/cache/love-11.5-apple-libraries.zip \ - https://github.com/love2d/love/releases/download/11.5/love-11.5-apple-libraries.zip -unzip -q mobile/ios/cache/love-11.5-apple-libraries.zip -d mobile/ios/cache -rm -rf mobile/ios/love-src/platform/xcode/ios/libraries -cp -R mobile/ios/cache/love-apple-dependencies/iOS/libraries \ - mobile/ios/love-src/platform/xcode/ios/libraries -``` - -`scripts/build_ios.sh` checks for `libraries/SDL2.xcframework` and fails with -these instructions if it is absent. - -Upstream also documents -[love-apple-dependencies](https://github.com/love2d/love-apple-dependencies) -as an alternate source of the same libraries. +`scripts/build_ios.sh --fetch` retrieves the matching iOS libraries and the +SDL3 framework from +[love-apple-dependencies](https://github.com/love2d/love-apple-dependencies). +Re-run it if either dependency directory is absent. ## App identity @@ -134,7 +113,7 @@ so refreshing `love-src/` does not lose branding. | Flag | Meaning | |------|---------| | *(default)* | Simulator, Debug, no signing | -| `--fetch` | Download/extract `love-11.5-ios-source.zip` if `love-src/` is missing | +| `--fetch` | Fetch the LÖVE 12.0 source tree and Apple dependencies if `love-src/` is missing | | `--device` | Build against `iphoneos` instead of `iphonesimulator` | | `--release` | `Release` configuration instead of `Debug` | | `--package-only` | Zip `game.love` + apply plist overlay; skip `xcodebuild` | @@ -147,5 +126,5 @@ Also: `scripts/build.sh ios` delegates here (`--release` is forwarded). - iOS platform installed in Xcode (Settings → Platforms). `xcodebuild -showsdks` should list `iphonesimulator` / `iphoneos`. A partial install can fail IB/xib compiles with `iOS … Platform Not Installed` even when the SDK name appears. -- `love-src/` present (`--fetch` or manual unzip of `love-11.5-ios-source.zip`) -- iOS libraries under `love-src/platform/xcode/ios/libraries/` (see above) +- `love-src/` present (`--fetch`) +- iOS libraries under `love-src/platform/xcode/ios/libraries/` and SDL3 under `love-src/platform/xcode/shared/Frameworks/` diff --git a/mobile/ios/patch_love_src.py b/mobile/ios/patch_love_src.py index 6ea38c83..9c0fa0ac 100644 --- a/mobile/ios/patch_love_src.py +++ b/mobile/ios/patch_love_src.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Applies gen1recomp's iOS native-bridge patches to the fetched LÖVE 11.5 +"""Applies gen1recomp's iOS native-bridge patches to the fetched LÖVE 12.0 source tree (mobile/ios/love-src/). Idempotent AND re-appliable: the first run stashes a pristine `.orig` copy of every file it rewrites, and later runs always start over from that copy — so editing the patch content here @@ -39,6 +39,7 @@ WRAP_INCLUDES = """ #ifdef LOVE_IOS #include #include +#include #include "filesystem/Filesystem.h" #endif """ % MARKER @@ -53,8 +54,12 @@ WRAP_FUNCS = """ #ifdef LOVE_IOS static const char *gr_saveDirectory() { + static std::string saveDirectory; auto fs = Module::getInstance(Module::M_FILESYSTEM); - return fs != nullptr ? fs->getSaveDirectory() : ""; + if (fs == nullptr) + return ""; + saveDirectory = fs->getSaveDirectory(); + return saveDirectory.c_str(); } static int gr_callBridge(lua_State *L, const char *className, diff --git a/scripts/build_ios.sh b/scripts/build_ios.sh index 70311d34..5e8adc9d 100755 --- a/scripts/build_ios.sh +++ b/scripts/build_ios.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Packages the LÖVE2D Pokémon Red port into an iOS app via LÖVE 11.5's -# official iOS Xcode project (love-11.5-ios-source.zip). +# Packages the LÖVE2D Pokémon Red port into an iOS app via LÖVE 12.0's +# iOS Xcode project. # # Usage: scripts/build_ios.sh [--fetch] [--device] [--release] [--install] # [--version X.Y.Z] [--package-only] @@ -12,13 +12,12 @@ # first connected iPhone/iPad (unlock it first) # --release Release configuration # --version X.Y.Z stamp MARKETING_VERSION / CURRENT_PROJECT_VERSION -# --fetch Download love-11.5-ios-source.zip into mobile/ios/love-src/ +# --fetch Fetch LÖVE 12.0 sources and Apple dependencies into mobile/ios/love-src/ # --package-only Zip game.love + apply plist overlay; skip xcodebuild # # Prerequisites: # - macOS + Xcode (xcodebuild) # - mobile/ios/love-src/ (see --fetch / mobile/ios/README.md) -# - prebuilt iOS libraries under love-src/platform/xcode/ios/libraries/ # # Output: dist/ios/-/gen1recomp.app (convenience copy) # dist/ios/gen1recomp.ipa (device builds only) @@ -54,11 +53,11 @@ BUNDLE_ID="${GEN1_BUNDLE_ID:-com.theboisclub.gen1recomp}" if [ -z "$BUNDLE_ID" ] && [ -f "$IOS_DIR/bundle_id.local" ]; then BUNDLE_ID="$(tr -d '[:space:]' < "$IOS_DIR/bundle_id.local")" fi -LOVE_VERSION="$(tr -d '[:space:]' < "$IOS_DIR/LOVE_VERSION" 2>/dev/null || echo 11.5)" -IOS_SOURCE_ZIP="love-${LOVE_VERSION}-ios-source.zip" -APPLE_LIBS_ZIP="love-${LOVE_VERSION}-apple-libraries.zip" -IOS_SOURCE_URL="https://github.com/love2d/love/releases/download/${LOVE_VERSION}/${IOS_SOURCE_ZIP}" -APPLE_LIBS_URL="https://github.com/love2d/love/releases/download/${LOVE_VERSION}/${APPLE_LIBS_ZIP}" +LOVE_VERSION="$(tr -d '[:space:]' < "$IOS_DIR/LOVE_VERSION" 2>/dev/null || echo 12.0)" +LOVE_SOURCE_REF="${LOVE_SOURCE_REF:-main}" +APPLE_DEPENDENCIES_REF="${APPLE_DEPENDENCIES_REF:-main}" +LOVE_SOURCE_REPO="https://github.com/love2d/love.git" +APPLE_DEPENDENCIES_REPO="https://github.com/love2d/love-apple-dependencies.git" FETCH=false DEVICE=false @@ -160,25 +159,19 @@ fi # --------------------------------------------------------------- fetch love-src fetch_love_ios() { mkdir -p "$CACHE" - local zip_path="$CACHE/$IOS_SOURCE_ZIP" - if [ ! -f "$zip_path" ]; then - say "downloading $IOS_SOURCE_ZIP (LÖVE $LOVE_VERSION iOS sources)" - curl -fL --progress-bar "$IOS_SOURCE_URL" -o "$zip_path" \ - || fail "download failed: $IOS_SOURCE_URL" - else - say "using cached $zip_path" - fi - - say "extracting into $LOVE_SRC" - rm -rf "$LOVE_SRC" local tmp tmp="$(mktemp -d "$CACHE/extract.XXXXXX")" - unzip -q "$zip_path" -d "$tmp" - # Zip root is love--ios-source/ - local extracted - extracted="$(find "$tmp" -maxdepth 1 -mindepth 1 -type d ! -name '__MACOSX' | head -1)" - [ -n "$extracted" ] || fail "unexpected layout inside $IOS_SOURCE_ZIP" - mv "$extracted" "$LOVE_SRC" + say "fetching LÖVE $LOVE_VERSION sources ($LOVE_SOURCE_REF)" + git clone --depth 1 --branch "$LOVE_SOURCE_REF" "$LOVE_SOURCE_REPO" "$tmp/love" \ + || fail "failed to fetch LÖVE sources from $LOVE_SOURCE_REPO" + say "fetching Apple dependencies ($APPLE_DEPENDENCIES_REF)" + git clone --depth 1 --branch "$APPLE_DEPENDENCIES_REF" "$APPLE_DEPENDENCIES_REPO" "$tmp/dependencies" \ + || fail "failed to fetch Apple dependencies from $APPLE_DEPENDENCIES_REPO" + rm -rf "$LOVE_SRC" + mv "$tmp/love" "$LOVE_SRC" + mkdir -p "$LIBS_DIR" "$XCODE_DIR/shared" + cp -R "$tmp/dependencies/iOS/libraries/." "$LIBS_DIR" + cp -R "$tmp/dependencies/shared/." "$XCODE_DIR/shared" rm -rf "$tmp" say "love-src ready (LÖVE $LOVE_VERSION)" } @@ -188,18 +181,12 @@ if [ ! -d "$XCODE_DIR/love.xcodeproj" ]; then fetch_love_ios else fail "LÖVE $LOVE_VERSION iOS sources not found at mobile/ios/love-src/. - Fetch them (documented download of love-${LOVE_VERSION}-ios-source.zip): + Fetch them: scripts/build_ios.sh --fetch - Or manually: - mkdir -p mobile/ios/cache - curl -fL -o mobile/ios/cache/$IOS_SOURCE_ZIP \\ - $IOS_SOURCE_URL - unzip -q mobile/ios/cache/$IOS_SOURCE_ZIP -d mobile/ios/cache - mv mobile/ios/cache/love-${LOVE_VERSION}-ios-source mobile/ios/love-src See mobile/ios/README.md." fi elif $FETCH; then - say "love-src already present; skipping download (delete mobile/ios/love-src to refresh)" + say "love-src already present; skipping fetch (delete mobile/ios/love-src to refresh)" fi [ -d "$XCODE_DIR/love.xcodeproj" ] \ @@ -207,25 +194,16 @@ fi # --------------------------------------------------------------- apple libraries require_ios_libraries() { - if [ -d "$LIBS_DIR/SDL2.xcframework" ]; then + if [ -d "$LIBS_DIR/SDL2.xcframework" ] && [ -d "$XCODE_DIR/shared/Frameworks/SDL3.xcframework" ]; then return 0 fi fail "prebuilt iOS libraries missing at: $LIBS_DIR - love-ios expects SDL2.xcframework (and friends) there. + and shared/Frameworks. - The official love-${LOVE_VERSION}-ios-source.zip normally includes them. - If they are absent, install love-${LOVE_VERSION}-apple-libraries.zip: + Re-fetch the LÖVE $LOVE_VERSION source tree and its Apple dependencies: - mkdir -p mobile/ios/cache - curl -fL -o mobile/ios/cache/$APPLE_LIBS_ZIP \\ - $APPLE_LIBS_URL - unzip -q mobile/ios/cache/$APPLE_LIBS_ZIP -d mobile/ios/cache - rm -rf mobile/ios/love-src/platform/xcode/ios/libraries - cp -R mobile/ios/cache/love-apple-dependencies/iOS/libraries \\ - mobile/ios/love-src/platform/xcode/ios/libraries - - See mobile/ios/README.md (Apple libraries dependency)." + scripts/build_ios.sh --fetch" } require_ios_libraries @@ -565,11 +543,27 @@ run_xcodebuild() { say "xcodebuild love-ios ($config / $sdk)" set +e - ( - cd "$XCODE_DIR" - xcodebuild "${args[@]}" - ) - local xc_status=$? + local xc_status + if command -v xcbeautify >/dev/null 2>&1; then + ( + cd "$XCODE_DIR" + xcodebuild "${args[@]}" + ) 2>&1 | xcbeautify + local pipeline_status=("${PIPESTATUS[@]}") + local xcode_status=${pipeline_status[0]} + local beautify_status=${pipeline_status[1]} + if [ "$xcode_status" -ne 0 ]; then + xc_status=$xcode_status + else + xc_status=$beautify_status + fi + else + ( + cd "$XCODE_DIR" + xcodebuild "${args[@]}" + ) + xc_status=$? + fi set -e if [ "$xc_status" -ne 0 ]; then fail "xcodebuild failed (exit $xc_status). diff --git a/src/render/Renderer.lua b/src/render/Renderer.lua index 8c5557d8..e3513c1d 100644 --- a/src/render/Renderer.lua +++ b/src/render/Renderer.lua @@ -641,7 +641,8 @@ function Renderer:endFrame(zones, worldZones) -- runs, so dialogs, menus and the HUD sit on top as usual. love.graphics.setColor(1, 1, 1, 1) love.graphics.setScissor(0, 0, ww, wh) - if love.system and love.system.getOS and love.system.getOS() == "iOS" then + local loveMajor = love.getVersion() + if love.system and love.system.getOS and love.system.getOS() == "iOS" and loveMajor >= 12 then love.graphics.draw(self.worldOverride, 0, wh, 0, 1 / dpiX, -1 / dpiY) else love.graphics.draw(self.worldOverride, 0, 0, 0, 1 / dpiX, 1 / dpiY) diff --git a/tests/drivers/oak_last_ball_bug601_test.lua b/tests/drivers/oak_last_ball_bug601_test.lua new file mode 100644 index 00000000..2c3e6485 --- /dev/null +++ b/tests/drivers/oak_last_ball_bug601_test.lua @@ -0,0 +1,112 @@ +-- Driver: regression coverage for #601 "Wrong dialogue when interacting +-- with Prof. Oak's last ball". +-- +-- After the player picks a starter and the rival takes his, the leftover +-- ball on the lab table must show "That's PROF.OAK's last Pokémon!" -- +-- pret/pokered scripts/OaksLab.asm OaksLabSelectedPokeBallScript jumps +-- every ball handler to OaksLabLastMonScript once EVENT_GOT_STARTER is +-- set (Oak turns to face the player first). The buggy port fell through +-- to _OaksLabThoseArePokeBallsText ("Those are POKé BALLs...") instead. +-- +-- Scenario A (the #601 regression): with a starter already picked, talk +-- to the leftover ball -> Oak faces down, box says "last Pokémon!", +-- and no starter offer/dex appears. Fails before the fix (the box +-- says "Those are POKé BALLs"). +-- Scenario B (guard): with NO starter and not escorted in, the ball still +-- says "Those are POKé BALLs". Passes before and after the fix. +-- +-- Setup: flags are set directly (pick flow never runs), so all three +-- balls stay visible; the player stands left of the Charmander ball +-- (cell 6,3), the leftover slot for the Squirtle pick (rival took the +-- Bulbasaur ball). The lab battle flag is set so the rival is gone and +-- cannot intercept the talk. TextBox.new is hooked to capture the raw +-- box text. + +return function(game) + local U = dofile("tests/drivers/util.lua") + local DIR = os.getenv("SHOT_DIR") or "/tmp/shots" + + local TextBox = require("src.render.TextBox") + local origNew = TextBox.new + local lastText + TextBox.new = function(g, text, ...) + lastText = text + return origNew(g, text, ...) + end + + local function restore() + TextBox.new = origNew + end + + local function setFlags(postPick) + local flags = game.save.flags or {} + game.save.flags = flags + flags.EVENT_FOLLOWED_OAK_INTO_LAB = true + if postPick then + flags.EVENT_GOT_STARTER = true + flags.EVENT_CHOSE_SQUIRTLE = true + else + flags.EVENT_GOT_STARTER = nil + end + -- rival already fought + gone, so he cannot intercept the talk + flags.EVENT_BATTLED_RIVAL_IN_OAKS_LAB = true + end + + -- Talk to the ball at cell 6,3 (Charmander slot): stand one cell left + -- facing right and press A. Returns once a TextBox has been built. + local function talkToBall() + lastText = nil + U.teleport(game, "OAKS_LAB", 5, 3, "right") + U.wait(6) + for _ = 1, 8 do + U.tap(game, "a") + for _ = 1, 30 do + if lastText then return true end + U.wait(1) + end + end + return lastText ~= nil + end + + -- ---- Scenario A: leftover ball after the pick + setFlags(true) + local aBoxOpened = talkToBall() + U.wait(30) -- let the typewriter reveal the line + U.shot(game, DIR .. "/a_last_ball.png") + local aText = lastText or "" + local aPass = aBoxOpened + and aText:find("last Pokémon!", 1, true) ~= nil + and aText:find("Those are", 1, true) == nil + U.log("SCENARIO A box:", aText) + U.log("SCENARIO A", aPass and "PASS" or "FAIL") + + -- close the box + for _ = 1, 10 do + if game.stack:top() == game.overworld then break end + U.tap(game, "a") + U.wait(2) + end + + -- ---- Scenario B: pre-escort ball text unchanged + setFlags(false) + local bBoxOpened = talkToBall() + U.wait(30) + U.shot(game, DIR .. "/b_pre_escort.png") + local bText = lastText or "" + local bPass = bBoxOpened + and bText:find("ThoseArePokeBalls", 1, true) ~= nil + or bText:find("Those are", 1, true) ~= nil + U.log("SCENARIO B box:", bText) + U.log("SCENARIO B", bPass and "PASS" or "FAIL") + + -- restore hooks before any assert so a failure can't leave them installed + restore() + + U.log("RESULT bug601", (aPass and bPass) and "PASS" or "FAIL") + assert(aPass, + "Leftover ball after the pick must say 'That's PROF.OAK's last " + .. "Pokémon!' (no 'Those are POKé BALLs'); got: " .. aText) + assert(bPass, + "Pre-escort balls must keep the 'Those are POKé BALLs' line; got: " + .. bText) +end diff --git a/tests/engine/oaks_lab_last_ball_bug601.lua b/tests/engine/oaks_lab_last_ball_bug601.lua new file mode 100644 index 00000000..9320da0b --- /dev/null +++ b/tests/engine/oaks_lab_last_ball_bug601.lua @@ -0,0 +1,129 @@ +-- Regression coverage for #601 "Wrong dialogue when interacting with Prof. +-- Oak's last ball" (T2, ROM-free). +-- +-- pret/pokered scripts/OaksLab.asm OaksLabSelectedPokeBallScript: once +-- EVENT_GOT_STARTER is set, EVERY ball's text handler jumps to +-- OaksLabLastMonScript -- Oak turns to face the player and reads +-- "_OaksLabLastMonText" ("That's PROF.OAK's last #MON!") instead of +-- re-offering the starter. The buggy port fell through to +-- _OaksLabThoseArePokeBallsText ("Those are POKé BALLs...") on every ball +-- once a starter had been picked. The fix also spells the ROM's "#MON" +-- ligature out as "Pokémon". +-- +-- The three ball scripts share one table (starterBall), so this suite +-- drives that table through a mini ScriptRunner-compatible executor: +-- flag checks, jumps, "end" halts and text rows are executed, UI-heavy +-- commands (push_screen, ask, give_pokemon, npc moves) are no-ops with +-- ask recording the offer text. It then asserts the whole flow: +-- * GOT_STARTER + talk -> Oak faces down + "last Pokémon!" line, ends +-- * no GOT_STARTER, not escorted in -> "Those are POKé BALLs" +-- * no GOT_STARTER, escorted in -> the dex/ask offer (unchanged path) +-- plus MapScripts.validateContribution stays clean (the pre-fix table +-- carried nine out-of-range "jump 21" findings -- its run-time "end"). + +package.path = "./?.lua;./?/init.lua;" .. package.path + +local T = require("tests.harness") +local MapScripts = require("src.script.MapScripts") + +local contribution = dofile("data/scripts/oaks_lab.lua") +local problems = MapScripts.validateContribution(contribution) +T.eq(#problems, 0, "oaks_lab contribution validates cleanly") +for _, p in ipairs(problems) do + T.check(false, "unexpected finding: " .. p) +end + +local BALL = "TEXT_OAKSLAB_CHARMANDER_POKE_BALL" + +-- ---- mini executor over the talk rows (ScriptRunner semantics: a jump +-- command returns the next row index or "end" to halt) + +local function run(script, flags, answer) + local pc, texts, offers = 1, {}, {} + local lastCheck = nil + while pc <= #script do + local row = script[pc] + local verb = row[1] + if verb == "check_flag" then + lastCheck = flags[row[2]] == true + elseif verb == "jump_if_true" then + if lastCheck then + if row[2] == "end" then break end + pc = row[2] goto next + end + elseif verb == "jump_if_false" then + if not lastCheck then + if row[2] == "end" then break end + pc = row[2] goto next + end + elseif verb == "jump" then + if row[2] == "end" then break end + pc = row[2] + goto next + elseif verb == "show_text" then + texts[#texts + 1] = row[2] + elseif verb == "ask" then + offers[#offers + 1] = row[2] + if answer == false then + pc = pc + 1 -- decline: the next row's jump_if_false decides + goto next + end + end + -- push_screen / give_pokemon / set_flag / hide_object / move_npc_to / + -- face_object: no-op here (set_flag is exercised via the fixture + -- flags table instead of being run) + pc = pc + 1 + ::next:: + end + return texts, offers +end + +local function concat(list) + return table.concat(list, "\n") +end + +-- ---- leftover ball after the pick: Oak faces down + the last-mon line +local got = { EVENT_GOT_STARTER = true, EVENT_FOLLOWED_OAK_INTO_LAB = true } +local texts, offers = run(contribution.talk[BALL], got, true) +T.eq(#offers, 0, "no starter offer after the pick") +local box = concat(texts) +T.check(box:find("last Pokémon!", 1, true) ~= nil, + "leftover ball says the last-mon line (got: " .. box .. ")") +T.check(box:find("Those are", 1, true) == nil, + "leftover ball no longer says 'Those are POKé BALLs'") +T.check(box:find("#MON", 1, true) == nil, + "the ROM #MON ligature is spelled out as Pokémon") + +-- the pokered beat also turns Oak to face the player +T.check(contribution.talk[BALL][20][1] == "face_object" + and contribution.talk[BALL][20][2] == 5 + and contribution.talk[BALL][20][3] == "down", + "row 20 faces Oak down before the line (OaksLabLastMonScript)") + +-- ---- pre-escort: still the vanilla "Those are POKé BALLs" line +local pre = { EVENT_GOT_STARTER = false, EVENT_FOLLOWED_OAK_INTO_LAB = false } +local t2, o2 = run(contribution.talk[BALL], pre, true) +T.eq(#o2, 0, "no offer before Oak escorts the player in") +T.check(concat(t2):find("ThoseArePokeBalls", 1, true) ~= nil, + "pre-escort balls keep the 'Those are POKé BALLs' line") + +-- ---- escorted in but no pick yet: the dex + "You want X?" offer +local mid = { EVENT_GOT_STARTER = false, EVENT_FOLLOWED_OAK_INTO_LAB = true } +local t3, o3 = run(contribution.talk[BALL], mid, true) +T.eq(#o3, 1, "the starter offer still runs before the pick") +T.check(concat(t3):find("last Pokémon!", 1, true) == nil, + "no last-mon line before the pick") + +-- ---- all three balls share the same table shape (last-mon beat present) +for _, key in ipairs({ + "TEXT_OAKSLAB_CHARMANDER_POKE_BALL", + "TEXT_OAKSLAB_SQUIRTLE_POKE_BALL", + "TEXT_OAKSLAB_BULBASAUR_POKE_BALL", +}) do + local script = contribution.talk[key] + T.check(script and script[21] and script[21][2] and + script[21][2]:find("Pokémon", 1, true) ~= nil, + key .. " carries the last-mon line") +end + +T.finish("oaks_lab_last_ball_bug601")