Compare commits

...

11 Commits

Author SHA1 Message Date
bryanthaboi ac01135ca8 Merge pull request #615 from bryanthaboi/dev 2026-08-01 16:53:54 -04:00
bryanthaboi 5e89e35e02 Merge pull request #614 from castdrian/metal 2026-08-01 16:52:41 -04:00
bryanthaboi 7e7afeaf82 Mask the self-hosted runner's home path in release logs 2026-08-01 16:45:37 -04:00
Adrian Castro c6fa6d294b fix(ios): support LÖVE 12 file bridge 2026-08-01 22:22:49 +02:00
bryanthaboi fc2d17fc78 Merge pull request #612 from bryanthaboi/dev 2026-08-01 16:03:28 -04:00
bryanthaboi 9f072285a6 Merge pull request #611 from ShaneMcGovernIE/fix/oak-last-ball-601 2026-08-01 16:00:52 -04:00
bryanthaboi c58ebe4b9b Merge pull request #609 from castdrian/metal 2026-08-01 15:59:33 -04:00
Shane McGovern 0dd187fe30 Fix wrong dialogue on Prof. Oak's leftover ball (#601)
After the player picks a starter and the rival takes his, every ball on
the lab table fell through to 'Those are POKé BALLs' instead of the
leftover-mon line.  Port the pokered OaksLabSelectedPokeBallScript ->
OaksLabLastMonScript beat: with EVENT_GOT_STARTER set, Oak turns to face
the player and reads 'That's PROF.OAK's last Pokémon!'.  The ROM's
'#MON' ligature is spelled out as Pokémon.

The renumbering also drops the table's nine out-of-range 'jump 21'
rows (run-time 'end' idioms) for explicit "end" targets, so the
script now validates cleanly.

Tests: T2 suite drives the ball talk table through a ScriptRunner-
compatible executor (leftover text, pre-escort text, pre-pick offer)
plus a T3 driver that talks to the leftover ball in a real game.
2026-08-01 20:55:31 +01:00
Adrian Castro ea78792c03 fix(ios): build releases with LÖVE 12 2026-08-01 21:45:23 +02:00
bryanthaboi 9954225701 Merge pull request #603 from bryanthaboi/dev 2026-08-01 14:32:23 -04:00
bryanthaboi 6bb2e078c0 Scrub mod manifest strings to valid UTF-8
A manifest whose name, version, description, or category carries invalid
UTF-8 (a BOM, Latin-1 bytes) crashed the launcher's MODS panel, since
love.graphics.printf raises on invalid UTF-8. Manifest.validate now drops
invalid bytes and a leading BOM from those strings, in place so the
badge's raw.category read agrees.
2026-08-01 14:22:30 -04:00
11 changed files with 420 additions and 99 deletions
+11
View File
@@ -45,6 +45,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:
@@ -170,6 +176,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
+14 -5
View File
@@ -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
+1 -1
View File
@@ -1 +1 @@
11.5
12.0
+17 -38
View File
@@ -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/`
+7 -2
View File
@@ -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 <objc/runtime.h>
#include <objc/message.h>
#include <string>
#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<love::filesystem::Filesystem>(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,
+46 -52
View File
@@ -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/<Config>-<sdk>/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-<version>-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).
+50
View File
@@ -86,8 +86,58 @@ local function mergeConflictLists(conflicts, incompatible)
return out
end
-- Drop bytes that are not valid UTF-8 (malformed sequences, overlongs,
-- surrogates, > U+10FFFF) and a leading BOM. LÖVE's text renderer raises
-- "Invalid UTF-8" from love.graphics.print/printf, so any manifest string a
-- panel may draw must be scrubbed here -- the one place every mod manifest
-- passes through -- or a single mangled description crashes the whole MODS
-- panel instead of misrendering one card.
local function scrubUtf8(s)
if type(s) ~= "string" then return s end
s = s:gsub("^\239\187\191", "")
local out, i, n = {}, 1, #s
while i <= n do
local b = s:byte(i)
local len
if b < 0x80 then len = 1
elseif b >= 0xC2 and b <= 0xDF then len = 2
elseif b >= 0xE0 and b <= 0xEF then len = 3
elseif b >= 0xF0 and b <= 0xF4 then len = 4
end
local ok = len ~= nil and i + len - 1 <= n
if ok and len > 1 then
for j = i + 1, i + len - 1 do
local c = s:byte(j)
if c < 0x80 or c > 0xBF then ok = false; break end
end
if ok then
-- boundary lead bytes narrow their second byte: no overlongs
-- (E0/F0), no surrogates (ED), nothing past U+10FFFF (F4)
local b2 = s:byte(i + 1)
if (b == 0xE0 and b2 < 0xA0) or (b == 0xED and b2 > 0x9F)
or (b == 0xF0 and b2 < 0x90) or (b == 0xF4 and b2 > 0x8F) then
ok = false
end
end
end
if ok then
out[#out + 1] = s:sub(i, i + len - 1)
i = i + len
else
i = i + 1
end
end
return table.concat(out)
end
function Manifest.validate(raw, path)
assert(type(raw) == "table", "manifest must be an object")
-- scrubbed in place so every later reader agrees, including the launcher's
-- badge derivation, which reads raw.category rather than the validated copy
raw.name = scrubUtf8(raw.name)
raw.version = scrubUtf8(raw.version)
raw.description = scrubUtf8(raw.description)
raw.category = scrubUtf8(raw.category)
assert(type(raw.id) == "string" and raw.id:match("^[%w_%-]+$"),
"manifest id must contain only letters, numbers, _ or -")
assert(type(raw.name) == "string" and raw.name ~= "", "manifest name is required")
+2 -1
View File
@@ -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)
+112
View File
@@ -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 "<none>"
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 "<none>"
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
+31
View File
@@ -297,4 +297,35 @@ do
eq(rows[1].name, "bare", "a nameless row falls back to its id")
end
-- ------- manifest strings are scrubbed to valid UTF-8 (MODS panel crash:
-- LÖVE's printf raises "Invalid UTF-8" on a mangled name/description, so
-- validate must drop bad bytes before any panel draws them)
do
local m = mf({ id = "utf", entry = "m.lua",
-- BOM-prefixed name (a real manifest shipped this way), a Latin-1 e-acute
-- (\233, invalid as UTF-8) in the description, and a lone continuation
-- byte in the version
name = "\239\187\191Run Mode",
version = "1.0\128.0",
description = "caf\233 latt\233",
category = "UI\255" })
eq(m.name, "Run Mode", "a leading BOM is stripped from the name")
eq(m.version, "1.0.0", "invalid bytes are dropped from the version")
eq(m.description, "caf latt", "Latin-1 bytes are dropped, not replaced")
eq(m.raw.category, "UI", "raw.category is scrubbed in place for the badge")
local ok2 = mf({ id = "utf2", name = "Vers\195\163oVermelha", version = "1.0.0",
entry = "m.lua", description = "Pok\195\169mon \240\159\148\165" })
eq(ok2.name, "Vers\195\163oVermelha", "valid two-byte sequences survive")
eq(ok2.description, "Pok\195\169mon \240\159\148\165",
"valid three- and four-byte sequences survive")
-- surrogate half (ED A0 80) and overlong slash (C0 AF) are invalid even
-- though their lead bytes look plausible
local bad = mf({ id = "utf3", name = "a\237\160\128b\192\175c",
version = "1.0.0", entry = "m.lua" })
eq(bad.name, "abc", "surrogates and overlongs are dropped")
end
T.finish("launcher_mods")
+129
View File
@@ -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")