Compare commits

...

26 Commits

Author SHA1 Message Date
bryanthaboi fea81f03fc Merge branch 'dev' 2026-08-01 21:10:08 -04:00
bryanthaboi b549e319c2 Update README.md 2026-08-01 20:54:08 -04:00
bryanthaboi 378246728b Merge pull request #628 from spiritsnails/fix/oaks-lab-rival-music 2026-08-01 19:57:10 -04:00
bryanthaboi f9adeca5ca Merge pull request #618 from castdrian/metal 2026-08-01 19:56:29 -04:00
bryanthaboi 838f87c224 Merge pull request #619 from ShaneMcGovernIE/fix/viridian-old-man-yellow-617 2026-08-01 19:55:49 -04:00
spiritsnails 5b519165fc Fix #596: missing rival battle music in Oak's Lab
The Yellow port (oaks_lab_yellow.lua) already swaps in Music_MeetRival
for the "Hey wait! Let's check out our Pokemon!" challenge and restores
Music_OaksLab once the rival walks out, but the Red/Blue script never
did, leaving the lab battle silent before and after the fight.
2026-08-01 17:41:35 -06:00
Adrian Castro 1a2b23a066 ci: configure dependabot for actions 2026-08-02 00:26:48 +02:00
Adrian Castro fe16b3259c ci(ios): comment artifacts after pull request builds 2026-08-02 00:01:53 +02:00
Adrian Castro 2e46954fe0 ci(ios): use release signing in verification 2026-08-01 23:56:04 +02:00
Adrian Castro 00d13d3c35 ci(ios): comment release artifacts on pull requests 2026-08-01 23:43:12 +02:00
Adrian Castro 1b8b3ad538 ci(ios): build device release on canonical repo 2026-08-01 23:43:11 +02:00
Adrian Castro 5f75cfd691 ci: verify release builds on fork runners 2026-08-01 23:43:11 +02:00
Adrian Castro 5f89def2ce ci: select hosted runners for forks 2026-08-01 23:43:08 +02:00
Adrian Castro 7c26eb9a24 ci(ios): gate hosted build on iOS changes 2026-08-01 23:43:07 +02:00
Shane McGovern d951fe8fc5 Fix Viridian old man tutorial dialog in Yellow (#617) 2026-08-01 22:38:09 +01:00
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
21 changed files with 1035 additions and 112 deletions
+6
View File
@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
+79 -2
View File
@@ -10,8 +10,6 @@ name: ci
# The T3 content tier asserts Pokemon Red facts; scripts/test.sh detects
# data/generated/ is absent and skips it rather than failing.
#
# Runs alongside release.yml, which is untouched by this file.
on:
push:
# Integration branch + release branch. PRs already run via pull_request
@@ -24,7 +22,86 @@ concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
ios-changes:
name: detect iOS changes
runs-on: ubuntu-latest
outputs:
changed: ${{ steps.paths.outputs.changed }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- id: paths
env:
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
HEAD_SHA: ${{ github.sha }}
run: |
if [ -z "$BASE_SHA" ] || [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]; then
echo "changed=true" >> "$GITHUB_OUTPUT"
exit 0
fi
if git diff --name-only "$BASE_SHA" "$HEAD_SHA" | grep -Eq '^(mobile/ios/|scripts/build_ios\.sh$|\.github/workflows/(ci|release)\.yml$)'; then
echo "changed=true" >> "$GITHUB_OUTPUT"
else
echo "changed=false" >> "$GITHUB_OUTPUT"
fi
ios-build:
name: iOS build
needs: ios-changes
if: needs.ios-changes.outputs.changed == 'true'
runs-on: ${{ fromJSON(github.repository == 'bryanthaboi/gen1recomp' && '["self-hosted", "macOS"]' || '"macos-latest"') }}
outputs:
ipa_url: ${{ steps.upload-ipa.outputs.artifact-url }}
steps:
- uses: actions/checkout@v4
- name: import signing certificate
if: github.repository == 'bryanthaboi/gen1recomp'
run: |
keychain_path="$RUNNER_TEMP/gen1recomp-ci-signing.keychain-db"
ci_dir="${POKEMON_CI_DIR:-$HOME/.config/pokemon-ci}"
p12="$ci_dir/signing.p12"
passfile="$ci_dir/signing.pass"
[ -f "$p12" ] && [ -f "$passfile" ] || exit 1
p12pw="$(cat "$passfile")"
kcpw="$(openssl rand -base64 24)"
echo "::add-mask::$kcpw"
security delete-keychain "$keychain_path" 2>/dev/null || true
security create-keychain -p "$kcpw" "$keychain_path"
security set-keychain-settings "$keychain_path"
security unlock-keychain -p "$kcpw" "$keychain_path"
security import "$p12" -P "$p12pw" -k "$keychain_path" -T /usr/bin/codesign -T /usr/bin/security
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$kcpw" "$keychain_path" >/dev/null
existing="$(security list-keychains -d user | sed -e 's/^[[:space:]]*//' -e 's/"//g')"
security list-keychains -d user -s "$keychain_path" $existing
- name: install xcbeautify
run: brew list xcbeautify >/dev/null 2>&1 || brew install xcbeautify
- name: build iOS release
env:
CANONICAL_REPOSITORY: ${{ github.repository == 'bryanthaboi/gen1recomp' }}
run: |
if [ "$CANONICAL_REPOSITORY" = true ]; then
scripts/build_ios.sh --fetch --device --release
else
scripts/build_ios.sh --fetch --release
fi
- name: upload iOS release artifact
id: upload-ipa
if: github.repository == 'bryanthaboi/gen1recomp'
uses: actions/upload-artifact@v4
with:
name: gen1recomp-ios-ipa
path: dist/ios/gen1recomp.ipa
if-no-files-found: error
retention-days: 7
- name: clean up signing keychain
if: ${{ always() && github.repository == 'bryanthaboi/gen1recomp' }}
run: security delete-keychain "$RUNNER_TEMP/gen1recomp-ci-signing.keychain-db" 2>/dev/null || true
headless:
name: headless suites (no ROM)
runs-on: ubuntu-latest
@@ -0,0 +1,43 @@
name: iOS artifact comment
on:
workflow_run:
workflows: [ci]
types: [completed]
permissions:
contents: read
pull-requests: write
jobs:
comment:
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
steps:
- id: artifact
env:
GH_TOKEN: ${{ github.token }}
RUN_ID: ${{ github.event.workflow_run.id }}
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
HEAD_REPOSITORY: ${{ github.event.workflow_run.head_repository.full_name }}
run: |
artifact_id="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$RUN_ID/artifacts" --jq '.artifacts[] | select(.name == "gen1recomp-ios-ipa") | .id')"
[ -n "$artifact_id" ] || exit 0
head_owner="${HEAD_REPOSITORY%%/*}"
pr_number="$(gh api "repos/$GITHUB_REPOSITORY/pulls?state=open&head=$head_owner:$HEAD_BRANCH" --jq '.[0].number // empty')"
[ -n "$pr_number" ] || exit 0
echo "artifact_url=https://github.com/$GITHUB_REPOSITORY/actions/runs/$RUN_ID/artifacts/$artifact_id" >> "$GITHUB_OUTPUT"
echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT"
- name: comment iOS artifact
if: steps.artifact.outputs.pr_number != ''
uses: thollander/actions-comment-pull-request@v3
with:
message: |
#### iOS Release IPA
- [Download gen1recomp.ipa](${{ steps.artifact.outputs.artifact_url }})
<sub>Automatically generated. [View workflow run](https://github.com/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }})</sub>
pr-number: ${{ steps.artifact.outputs.pr_number }}
comment-tag: ios-build-result
github-token: ${{ github.token }}
+27 -8
View File
@@ -42,9 +42,15 @@ concurrency:
jobs:
release:
runs-on: [self-hosted, macOS]
runs-on: ${{ fromJSON(github.repository == 'bryanthaboi/gen1recomp' && '["self-hosted", "macOS"]' || '"macos-latest"') }}
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:
@@ -121,6 +127,7 @@ jobs:
echo "tag=$tag" >> "$GITHUB_OUTPUT"
- name: Import signing certificate into a temporary keychain
if: github.repository == 'bryanthaboi/gen1recomp'
run: |
set -euo pipefail
KEYCHAIN_PATH="$RUNNER_TEMP/pokemon-signing.keychain-db"
@@ -170,14 +177,23 @@ jobs:
set -euo pipefail
scripts/build_android.sh --version "${{ steps.ver.outputs.version }}"
- name: Build iOS
- name: Install xcbeautify
run: |
set -euo pipefail
# Device Release IPA; signs with the Apple Development identity on
# the runner (auto team detection). Users on other Apple IDs still
# re-sign or build via docs/ios-install.md.
scripts/build_ios.sh --fetch --device --release \
--version "${{ steps.ver.outputs.version }}"
brew list xcbeautify >/dev/null 2>&1 || brew install xcbeautify
- name: Build iOS
env:
CANONICAL_REPOSITORY: ${{ github.repository == 'bryanthaboi/gen1recomp' }}
run: |
set -euo pipefail
if [ "$CANONICAL_REPOSITORY" = true ]; then
scripts/build_ios.sh --fetch --device --release \
--version "${{ steps.ver.outputs.version }}"
else
scripts/build_ios.sh --fetch --release \
--version "${{ steps.ver.outputs.version }}"
fi
- name: Build Anbernic RG34XXSP port
run: |
@@ -187,6 +203,7 @@ jobs:
./build-rg34xxsp.sh --version "${{ steps.ver.outputs.version }}"
- name: Notarize & staple macOS app
if: github.repository == 'bryanthaboi/gen1recomp'
run: |
set -euo pipefail
ci_dir="${POKEMON_CI_DIR:-$HOME/.config/pokemon-ci}"
@@ -221,6 +238,7 @@ jobs:
echo "Notarized + stapled ✓"
- name: Stage release assets
if: github.repository == 'bryanthaboi/gen1recomp'
id: assets
run: |
set -euo pipefail
@@ -260,6 +278,7 @@ jobs:
cat "$outdir/sha256sums.txt"
- name: Publish GitHub Release
if: github.repository == 'bryanthaboi/gen1recomp'
env:
GH_TOKEN: ${{ github.token }}
run: |
@@ -365,6 +384,6 @@ jobs:
echo "Published release $tag"
- name: Clean up signing keychain
if: always()
if: ${{ always() && github.repository == 'bryanthaboi/gen1recomp' }}
run: |
security delete-keychain "$RUNNER_TEMP/pokemon-signing.keychain-db" 2>/dev/null || true
+16
View File
@@ -4,6 +4,9 @@ A native LÖVE2D recreation of Poke Red, Blue and Yellow. The engine and map
behavior are hand-written Lua; game data and graphics are decoded from a ROM
supplied by the player.
> [!WARNING]
> **We are NOT affiliated with the website `gen1recomp[.]com`** That website is not run by this project, was not authorized by us, and we have no idea who operates it. It is impersonating this project; do not download anything from it, and treat anything it hosts or claims as untrustworthy. Even if the site currently links back to this repository, the people behind it can change its content at any time, so nothing on it should ever be trusted. This GitHub repository and the Discord linked below are the only official sources for this project.
<p align="center"><img src="https://raw.githubusercontent.com/bryanthaboi/gen1recomp/refs/heads/dev/assets/logo/logo.png"></p>
**SUPPORT / ANNOUNCEMENTS / MODS:** [Discord](https://bois.icu)
@@ -75,6 +78,19 @@ The packaged app contains neither a ROM nor pre-extracted game data. Music,
sound effects, and cries are synthesized while the game runs from compact
audio channel programs copied out of the verified ROM.
### A note on Windows Defender warnings
Windows Defender sometimes flags the Windows build with a generic
machine-learning detection such as `Trojan:Win32/Wacatac!ml` (#621). This is
a known false positive: the exe is the official LÖVE runtime with the game
archive appended (the standard way LÖVE games ship), and Defender's
heuristics distrust unsigned executables with appended data. Every release
publishes SHA-256 checksums (`sha256sums.txt`) so you can verify your
download, and you can confirm a flagged file yourself on
[VirusTotal](https://www.virustotal.com), where these builds come back clean
on every engine except Defender's heuristic. False positives are reported to
Microsoft as they come up.
## Controls
+2 -1
View File
@@ -48,7 +48,8 @@ end
if GameVersion.isYellow() then
for _, file in ipairs({ "data.scripts.yellow_gifts",
"data.scripts.yellow_jessie_james",
"data.scripts.yellow_beach_house" }) do
"data.scripts.yellow_beach_house",
"data.scripts.yellow_viridian_old_man" }) do
for mapId, mod in pairs(require(file)) do
MapScripts.attachBase(mapId, mod)
end
+21 -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
@@ -245,7 +254,12 @@ return {
and y >= 6 then
local rival = ow:npcByIndex(1)
if not rival then return false end
-- OaksLabRivalChallengesPlayerScript swaps in the rival encounter
-- fanfare for the taunt/challenge exchange, same as the Yellow port
-- (oaks_lab_yellow.lua); it was silently dropped here (#596).
local rows = {
{ "stop_music" },
{ "play_music", "Music_MeetRival" },
{ "show_text", "_OaksLabRivalIllTakeYouOnText" }, -- 1
}
-- the rival routes to a free cell beside the player
@@ -285,6 +299,8 @@ return {
table.insert(rows, { "show_text", "_OaksLabRivalSmellYouLaterText" })
table.insert(rows, { "move_npc_to", 1, 4, 11 })
table.insert(rows, { "hide_object", "OAKS_LAB", "OAKSLAB_RIVAL" })
-- restore the lab theme once he's walked out, same as the Yellow port
table.insert(rows, { "play_music", "Music_OaksLab" })
ow.runner:run(rows, { npc = rival })
return true
end
+5 -1
View File
@@ -71,8 +71,12 @@ return {
{ "show_text", "_OaksLabRivalLeaveItAllToMeText" },
{ "set_flag", "EVENT_GOT_POKEDEX" },
{ "set_flag", "EVENT_OAK_GOT_PARCEL" },
-- OaksLabOakGivesPokedexScript: HideObject TOGGLE_LYING_OLD_MAN /
-- ShowObject TOGGLE_OLD_MAN_2 -- Yellow's tutorial old man stands
-- on the sleeper's cell (18,9); the Red/Blue walker OLD_MAN at
-- (17,5) never appears in Yellow (#617)
{ "hide_object", "VIRIDIAN_CITY", "VIRIDIANCITY_OLD_MAN_SLEEPY" },
{ "show_object", "VIRIDIAN_CITY", "VIRIDIANCITY_OLD_MAN" },
{ "show_object", "VIRIDIAN_CITY", "VIRIDIANCITY_OLD_MAN2" },
{ "stop_music" },
{ "play_music", "Music_MeetRival" },
{ "move_npc_to", RIVAL, 4, 7 },
+145
View File
@@ -0,0 +1,145 @@
-- The Viridian City old-man catch tutorial, Yellow's way
-- (pokeyellow scripts/ViridianCity.asm, scripts/ViridianCity_2.asm,
-- scripts/OaksLab.asm). Registered on top of the shared tables by
-- data/scripts/init.lua on a Yellow boot.
--
-- Yellow has TWO gambler objects where Red/Blue have one:
-- * VIRIDIANCITY_OLD_MAN at (17,5) -- a Red/Blue leftover; its
-- toggle stays OFF forever in Yellow, no script ever shows it.
-- * VIRIDIANCITY_OLD_MAN2 at (18,9) -- replaces the sleeper the
-- moment the Pokédex is given (OaksLabOakGivesPokedexScript:
-- HideObject TOGGLE_LYING_OLD_MAN / ShowObject TOGGLE_OLD_MAN_2).
--
-- This is the tutorial old man. The Red/Blue "Are you in a hurry?"
-- yes/no script must NOT run against him: Yellow's
-- _ViridianCityOldManHadMyCoffeeNowText is the apology speech ("I've had
-- my coffee now ... I'll show you how to catch POKéMON as my apology"),
-- and the shared story.lua TEXT_VIRIDIANCITY_OLD_MAN rows hang an
-- invented yes/no over it -- YES printed the TimeIsMoney alias
-- (_ViridianCityOldManLosingMyTouchText) and NO ran the demo, every
-- talk, forever (#617).
--
-- The real flow (ViridianCityCheckWaitingOldMan + ViridianCityOldMan2Text
-- + ViridianCityOldManInitialCatchTrainingScript + ...EndInitial... +
-- ViridianCityPostInitialCatchTraining): stepping into (19,9) -- the gap
-- east of the sleeper's cell -- faces the old man right and the player
-- left, prints the apology, and without any choice runs the demo battle
-- (BATTLE_TYPE_OLD_MAN, RATTATA lvl 5). After it, the same text pointer
-- now prints _ViridianCityOldManLosingMyTouchText ("That didn't work!
-- I must be losing my touch."), the old man walks off (down 6 with the
-- player on (19,9), right 1 otherwise, Pikachu nudged out of the way
-- first) and TOGGLE_OLD_MAN_2 hides. A direct talk does the same.
local M = {}
local OLD_MAN2 = "VIRIDIANCITY_OLD_MAN2"
-- Capture the FUNCTION, not the table: attachBase stores the module
-- table itself, so once this file's onStep is attached the table's slot
-- points back here -- delegating through the table would self-recurse.
-- story5's VIRIDIAN_CITY.onStep chains story.lua's sleeping-old-man
-- gate and its own gym-lock step (same pattern as yellow_jessie_james).
local baseViridianStep = require("data.scripts.story5").VIRIDIAN_CITY.onStep
-- pokeyellow text/ViridianCity.asm, _ViridianCityOldManHadMyCoffeeNowText
-- and _ViridianCityOldManLosingMyTouchText, spelled with the extractor's
-- markers (line -> \n, cont -> \v, para -> \f)
local function text(game)
return {
apology = game.data.text._ViridianCityOldManHadMyCoffeeNowText
or "Ahh, I've had my\ncoffee now and I\vfeel great!\fSure, you can go\n"
.. "through!\fI'm sorry I was\nso rude to you!\fI see you're using\n"
.. "a POKéDEX.\fI'll show you how\nto catch POKéMON\vas my apology.",
losingMyTouch = game.data.text._ViridianCityOldManLosingMyTouchText
or "That didn't work!\nI must be losing\vmy touch.\fI've run out of\n"
.. "POKé BALLs too.\fI have to get some\nat POKéMON MART.",
}
end
-- The row list for the initial-tutorial branch, keyed by where the
-- player stands when the battle ends (ViridianCityPostInitialCatchTraining
-- reads wXCoord: (19,9) walks the old man down the corridor, anywhere
-- else walks him right 1 after moving the follower Pikachu aside).
local function oldMan2Rows(game, ow, npc)
local rows = {
{ "show_text", "_ViridianCityOldManHadMyCoffeeNowText" },
{ "old_man_demo" },
{ "set_flag", "EVENT_COMPLETED_CATCH_TRAINING" },
{ "show_text", "_ViridianCityOldManLosingMyTouchText" },
}
if ow.player and ow.player.cellX == 19 then
rows[#rows + 1] =
{ "walk_npc", npc.def.index,
{ "down", "down", "down", "down", "down", "down" } }
else
-- ViridianCityMovePikachu (scripts/ViridianCity_2.asm): Pikachu
-- steps out of the old man's way before he turns right
local PikachuFollower = require("src.world.PikachuFollower")
local pika = ow and PikachuFollower.current(ow)
if pika then
rows[#rows + 1] = { "walk_npc", pika.def.index, { "right" } }
end
rows[#rows + 1] = { "walk_npc", npc.def.index, { "right" } }
end
rows[#rows + 1] =
{ "hide_object", "VIRIDIAN_CITY", OLD_MAN2 }
return rows
end
-- The shared talk handler: TEXT_VIRIDIANCITY_OLD_MAN2's text_asm branch
-- (ViridianCityOldMan2Text) on EVENT_COMPLETED_CATCH_TRAINING.
local function oldMan2Talk(game, ow, npc, done)
if game.save.flags and game.save.flags.EVENT_COMPLETED_CATCH_TRAINING then
local TextBox = require("src.render.TextBox")
game.stack:push(TextBox.new(game, text(game).losingMyTouch, done))
return
end
ow.runner:run(oldMan2Rows(game, ow, npc), { npc = npc, onDone = done })
end
M.VIRIDIAN_CITY = {
talk = {
TEXT_VIRIDIANCITY_OLD_MAN2 = oldMan2Talk,
},
-- Re-apply the Pokédex swap for a save that already holds the flag but
-- was never standing here when it fired (converted .sav imports, same
-- shape as story.lua's VIRIDIAN_CITY.onEnter). Yellow shows OLD_MAN2,
-- not the Red/Blue OLD_MAN at (17,5), and also puts away a stray
-- OLD_MAN a save made by the pre-#617 build left standing.
onEnter = function(game, ow)
if not (game.save.flags and game.save.flags.EVENT_GOT_POKEDEX) then
return
end
local Commands = require("src.script.Commands")
local ctx = { save = game.save, game = game, overworld = ow }
Commands.hide_object(ctx, "VIRIDIAN_CITY", "VIRIDIANCITY_OLD_MAN_SLEEPY")
Commands.hide_object(ctx, "VIRIDIAN_CITY", "VIRIDIANCITY_OLD_MAN")
Commands.show_object(ctx, "VIRIDIAN_CITY", OLD_MAN2)
end,
-- ViridianCityCheckWaitingOldMan: with the Pokédex held and the
-- tutorial undone, (19,9) -- the gap east of the old man, the same
-- cell the sleeper used to gate -- faces him right, turns the player
-- left and starts the OLD_MAN2 flow with no choice.
onStep = function(game, ow, x, y)
if baseViridianStep and baseViridianStep(game, ow, x, y) then
return true
end
local flags = game.save.flags
if not flags.EVENT_GOT_POKEDEX then return false end
if flags.EVENT_COMPLETED_CATCH_TRAINING then return false end
if x ~= 19 or y ~= 9 then return false end
local man
for _, npc in ipairs(ow.npcs) do
if npc.def and npc.def.name == OLD_MAN2 then man = npc break end
end
if not man then return false end
man.facing = "right"
ow.player.facing = "left"
oldMan2Talk(game, ow, man, nil)
return true
end,
}
return M
+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).
+7
View File
@@ -77,6 +77,13 @@ end
function Data:applyVersionedFieldData()
if require("src.core.GameVersion").isYellow() then
self.field.trades = copy(YELLOW_TRADES)
-- The old man's catch demo is a RATTATA in Yellow
-- (scripts/ViridianCity.asm ViridianCityOldManStartCatchTrainingScript
-- .SetupBattle: ld a, RATTATA / ld [wCurOpponent], a) but the Yellow
-- manifest inherited Red's WEEDLE field.oldManBattle (#617), so old
-- Yellow caches carry the wrong demo species too. The fixed import
-- manifest below stamps RATTATA for fresh imports.
self.field.oldManBattle = { species = "RATTATA", level = 5 }
end
end
+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")
+288
View File
@@ -0,0 +1,288 @@
-- Parity (#617): Yellow's Viridian old man is the OLD_MAN2 at (18,9),
-- not the Red/Blue OLD_MAN at (17,5), and his dialog has no yes/no
-- choice -- the apology speech runs the RATTATA demo battle straight
-- away, the post-battle line is the losing-my-touch text, and he walks
-- off and hides.
--
-- Oracle: pokeyellow scripts/OaksLab.asm (OaksLabOakGivesPokedexScript:
-- HideObject TOGGLE_LYING_OLD_MAN / ShowObject TOGGLE_OLD_MAN_2),
-- scripts/ViridianCity.asm (ViridianCityCheckWaitingOldMan,
-- ViridianCityOldMan2Text, ...InitialCatchTrainingScript,
-- ...PostInitialCatchTraining) and scripts/ViridianCity_2.asm
-- (ViridianCityPrintOldManText). The Red/Blue "Are you in a hurry?"
-- script was running against Yellow's text: YES printed the TimeIsMoney
-- alias (_ViridianCityOldManLosingMyTouchText) and NO ran the demo --
-- every talk, forever.
--
-- Self-contained: `luajit tests/parity_yellow_old_man.lua`; also globbed
-- by tests/run_tests.lua.
package.path = "./?.lua;./?/init.lua;" .. package.path
if not _G.love then _G.love = require("tests.love_stub") end
local Data = require("src.core.Data")
if not (Data.maps and Data.maps.PALLET_TOWN) then Data:load() end
local GameVersion = require("src.core.GameVersion")
local SaveData = require("src.core.SaveData")
local ScriptRunner = require("src.script.ScriptRunner")
local TextBox = require("src.render.TextBox")
local BattleState = require("src.battle.BattleState")
local Pokemon = require("src.pokemon.Pokemon")
local S = require("tests.harness").suite("parity Yellow old man (#617)")
local check, eq = S.check, S.eq
local oldVersion = GameVersion.get()
local MAP = "VIRIDIAN_CITY"
local SLEEPER = "VIRIDIANCITY_OLD_MAN_SLEEPY"
local WALKER = "VIRIDIANCITY_OLD_MAN"
local OLD_MAN2 = "VIRIDIANCITY_OLD_MAN2"
local DONE_FLAG = "EVENT_COMPLETED_CATCH_TRAINING"
-- The Yellow wiring must be attached before anything else caches the
-- map-script registry: data.scripts.init branches on GameVersion at
-- load, so flip it first (this file owns its own process when run
-- standalone). Under tests/run_tests.lua the registry is already
-- cached with the Red wiring, so attach the Yellow modules directly
-- afterwards -- attachBase merges per TEXT constant and replaces hooks,
-- which is a no-op on a fresh process and the fix on a shared one.
GameVersion.set("yellow")
local mapScripts = require("data.scripts.init")
local MapScripts = require("src.script.MapScripts")
MapScripts.attachBase(MAP,
require("data.scripts.yellow_viridian_old_man").VIRIDIAN_CITY)
MapScripts.attachBase("OAKS_LAB",
require("data.scripts.oaks_lab_yellow"))
local oldManMod = require("data.scripts.yellow_viridian_old_man")
-- ------------------------------------------------------- the demo species
-- The catch demo is a RATTATA in Yellow (SetupBattle sets wCurOpponent
-- = RATTATA) but the Yellow manifest inherited Red's WEEDLE; the runtime
-- override in Data:applyVersionedFieldData repairs old caches. Kept
-- active until the end of this file so the demo-battle assertions below
-- run against the Yellow value; restored before S.finish() like
-- parity_yellow_trades does for its trades table.
local originalOldManBattle = Data.field.oldManBattle
or { species = "WEEDLE", level = 5 } -- the fixture carries no oldManBattle
local originalTrades = Data.field.trades
eq(originalOldManBattle.species, "WEEDLE",
"Red/Blue's old man still demos a Weedle")
GameVersion.set("yellow")
Data:applyVersionedFieldData()
eq(Data.field.oldManBattle.species, "RATTATA",
"Yellow's old man demos a Rattata (#617)")
local manifestFile = assert(io.open("tools/rom_manifest_yellow.json", "r"))
local yellowManifest = manifestFile:read("*a")
manifestFile:close()
check(yellowManifest:find('"species": "RATTATA"', 1, true) ~= nil,
"the Yellow manifest stamps RATTATA for fresh imports")
local redManifestFile = assert(io.open("tools/rom_manifest.json", "r"))
local redManifest = redManifestFile:read("*a")
redManifestFile:close()
check(redManifest:find('"species": "WEEDLE"', 1, true) ~= nil,
"and the Red/Blue manifest keeps WEEDLE")
-- ------------------------------------------------------- the Pokedex swap
-- OaksLabOakGivesPokedexScript shows TOGGLE_OLD_MAN_2 (the tutorial old
-- man standing on the sleeper's cell), never the Red/Blue walker
local oaksRows = mapScripts.talkScript("OAKS_LAB", "TEXT_OAKSLAB_OAK1")
check(type(oaksRows) == "table",
"the Yellow OaksLab Oak talk resolves to rows")
local sawSleepHide, sawOldMan2Show, sawOldManShow = false, false, false
for _, row in ipairs(oaksRows or {}) do
if row[1] == "hide_object" and row[3] == SLEEPER then sawSleepHide = true end
if row[1] == "show_object" and row[3] == OLD_MAN2 then sawOldMan2Show = true end
if row[1] == "show_object" and row[3] == WALKER then sawOldManShow = true end
end
check(sawSleepHide, "the Pokédex hand-over hides the lying old man")
check(sawOldMan2Show, "it shows OLD_MAN2 on the sleeper's cell")
check(not sawOldManShow, "it never shows the Red/Blue OLD_MAN (#617)")
-- both Yellow gamblers default hidden (toggle OFF), like pokeyellow
-- data/maps/toggleable_objects.asm. OLD_MAN2 only exists in a Yellow
-- import -- a Red-imported checkout carries just OLD_MAN -- so the
-- dataset checks tolerate its absence and the Yellow manifest carries
-- the OLD_MAN2 default instead.
local walkerDef, oldMan2Def
if Data.maps[MAP] then
for _, o in ipairs(Data.maps[MAP].objects or {}) do
if o.name == WALKER then walkerDef = o end
if o.name == OLD_MAN2 then oldMan2Def = o end
end
end
check(walkerDef == nil or walkerDef.hidden == true,
"VIRIDIANCITY_OLD_MAN defaults hidden in Yellow")
check(oldMan2Def == nil or oldMan2Def.hidden == true,
"VIRIDIANCITY_OLD_MAN2 defaults hidden in Yellow")
local om2Name = yellowManifest:find('"name": "VIRIDIANCITY_OLD_MAN2"', 1, true)
local om2Hidden = om2Name and yellowManifest:sub(
math.max(1, om2Name - 40), om2Name):find('"hidden": true', 1, true)
check(om2Hidden ~= nil,
"the Yellow manifest ships OLD_MAN2 with the toggle OFF")
-- ------------------------------------------------------- script registry
local talk = mapScripts.talkScript(MAP, "TEXT_VIRIDIANCITY_OLD_MAN2")
check(type(talk) == "function",
"TEXT_VIRIDIANCITY_OLD_MAN2 resolves to the Yellow handler")
check(type(mapScripts.talkScript(MAP, "TEXT_VIRIDIANCITY_OLD_MAN")) == "table",
"the Red/Blue OLD_MAN talk is still registered (unreachable in Yellow)")
local hooks = mapScripts.get(MAP)
check(hooks and type(hooks.onEnter) == "function",
"VIRIDIAN_CITY.onEnter is the Yellow swap")
check(hooks and type(hooks.onStep) == "function",
"VIRIDIAN_CITY.onStep chains the gym lock and sleeper gate")
check(oldManMod.VIRIDIAN_CITY and oldManMod.VIRIDIAN_CITY.talk
and oldManMod.VIRIDIAN_CITY.talk.TEXT_VIRIDIANCITY_OLD_MAN2 == talk,
"the handler is the module's own, not a leftover merge")
-- ------------------------------------------------------- completed branch
do
local pushed = {}
local game = {
data = Data,
save = SaveData.newGame(),
stack = { push = function(_, s) pushed[#pushed + 1] = s end },
}
game.save.flags.EVENT_COMPLETED_CATCH_TRAINING = true
local done = false
talk(game, nil, {}, function() done = true end)
eq(#pushed, 1, "a second talk only prints one box")
eq(getmetatable(pushed[1]), TextBox, "the losing-my-touch line, in a box")
pushed[1].onDone()
check(done, "closing it hands input back")
end
-- ------------------------------- the initial tutorial, end to end
-- Needs real species in the dataset (the fixture carries only FIX_*);
-- the engine's old-man demo machinery itself is parity_J's territory.
if Data.pokemon.RATTATA and Data.pokemon.PIKACHU then
do
require("src.render.Font").load(Data)
local pushed = {}
local save = SaveData.newGame()
save.party = { Pokemon.new(Data, "PIKACHU", 12) }
local moves = {}
local man = { def = { index = 8, name = OLD_MAN2 } }
local ow = {
map = { id = MAP, def = { label = "ViridianCity" } },
npcs = { man }, entities = { man },
player = { cellX = 19, cellY = 9, facing = "left" },
scriptMove = function(_, _, dir, _, cb) moves[#moves + 1] = dir; cb() end,
npcByIndex = function(_, i) if i == 8 then return man end end,
}
local game = {
data = Data,
save = save,
stack = { push = function(_, s) pushed[#pushed + 1] = s end },
}
local runner = ScriptRunner.new(game, ow)
ow.runner = runner
local done = false
talk(game, ow, man, function() done = true end)
eq(#pushed, 1, "the initial talk opens the apology speech")
eq(getmetatable(pushed[1]), TextBox, "in a text box")
pushed[1].onDone() -- A: the apology closes, the demo battle starts
eq(#pushed, 2, "the demo battle starts with no choice in between")
local battle = pushed[2]
check(battle and battle.demo, "it is the old-man demo battle")
eq(battle and battle.enemy and battle.enemy.mon.species, "RATTATA",
"the demo is a RATTATA in Yellow (#617)")
eq(save.flags[DONE_FLAG], nil, "the flag is still clear mid-demo")
battle.onFinish() -- the battle ends, the post-battle text prints
eq(save.flags[DONE_FLAG], true, "EVENT_COMPLETED_CATCH_TRAINING is set")
eq(#pushed, 3, "the losing-my-touch line follows the demo")
pushed[3].onDone() -- A: the old man walks off
eq(#moves, 6, "with the player on (19,9) he walks down 6 tiles")
check(moves[1] == "down" and moves[6] == "down",
"all six steps are the ViridianCityOldManMovementData2 walk")
eq(save.objectToggles[MAP] and save.objectToggles[MAP][OLD_MAN2], false,
"TOGGLE_OLD_MAN_2 hides once the walk finishes")
check(done, "and the talk hands input back")
end
-- ---------------------------------- side talk: player not on (19,9) cell
do
local pushed = {}
local save = SaveData.newGame()
save.party = { Pokemon.new(Data, "PIKACHU", 12) }
local moves = {}
local man = { def = { index = 8, name = OLD_MAN2 } }
local pika = { def = { index = 99, name = "PIKACHU_FOLLOWER" },
pikachuFollower = true }
local ow = {
map = { id = MAP, def = { label = "ViridianCity" } },
npcs = { man, pika }, entities = { man, pika },
player = { cellX = 18, cellY = 8, facing = "down" },
scriptMove = function(_, _, dir, _, cb) moves[#moves + 1] = dir; cb() end,
npcByIndex = function(_, i) if i == 8 then return man elseif i == 99 then return pika end end,
}
local game = {
data = Data,
save = save,
stack = { push = function(_, s) pushed[#pushed + 1] = s end },
}
local runner = ScriptRunner.new(game, ow)
ow.runner = runner
talk(game, ow, man, function() end)
pushed[1].onDone()
pushed[2].onFinish()
pushed[3].onDone()
eq(moves[1], "right", "Pikachu steps aside first (ViridianCityMovePikachu)")
eq(moves[2], "right", "then the old man turns right one tile")
eq(#moves, 2, "and no more")
end
else
check(true, "fixture dataset: demo-battle flow skipped (no RATTATA)")
end
-- --------------------------------------------------------- the (19,9) step
do
local pushed = {}
local save = SaveData.newGame()
local man = { def = { index = 8, name = OLD_MAN2 } }
local ow = {
map = { id = MAP, def = { label = "ViridianCity" } },
npcs = { man }, entities = { man },
player = { cellX = 19, cellY = 9, facing = "down" },
scriptMove = function(_, _, _, _, cb) cb() end,
npcByIndex = function() end,
}
local game = {
data = Data,
save = save,
stack = { push = function(_, s) pushed[#pushed + 1] = s end },
}
local runner = ScriptRunner.new(game, ow)
ow.runner = runner
check(not hooks.onStep(game, ow, 5, 5),
"off the trigger cell the step passes through")
check(hooks.onStep(game, ow, 19, 9),
"pre-Pokedex the sleeper gate owns (19,9)")
eq(#pushed, 1, "with the sleepy text box")
check(save.flags[DONE_FLAG] ~= true, "the tutorial is not running")
save.flags.EVENT_GOT_POKEDEX = true
check(hooks.onStep(game, ow, 19, 9),
"with the Pokedex, (19,9) starts the tutorial")
eq(man.facing, "right", "the old man faces the player")
eq(ow.player.facing, "left", "and the player turns to face him")
eq(#pushed, 2, "the apology box is up")
check(save.flags[DONE_FLAG] ~= true,
"no flag until the demo battle actually runs")
save.flags.EVENT_COMPLETED_CATCH_TRAINING = true
check(not hooks.onStep(game, ow, 19, 9),
"once the tutorial is done the cell is quiet again")
end
Data.field.trades = originalTrades
Data.field.oldManBattle = originalOldManBattle
GameVersion.set(oldVersion)
S.finish()
+1 -1
View File
@@ -6574,7 +6574,7 @@
"battleType": "BATTLE_TYPE_OLD_MAN",
"level": 5,
"map": "VIRIDIAN_CITY",
"species": "WEEDLE",
"species": "RATTATA",
"text": "ViridianCityOldManText"
},
"overworldFx": {