mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-13 00:40:59 +02:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ac01135ca8 | |||
| 5e89e35e02 | |||
| 7e7afeaf82 | |||
| c6fa6d294b | |||
| fc2d17fc78 | |||
| 9f072285a6 | |||
| c58ebe4b9b | |||
| 0dd187fe30 | |||
| ea78792c03 | |||
| 9954225701 | |||
| 6bb2e078c0 |
@@ -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
|
||||
|
||||
@@ -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 @@
|
||||
11.5
|
||||
12.0
|
||||
|
||||
+17
-38
@@ -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/`
|
||||
|
||||
@@ -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
@@ -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).
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
Reference in New Issue
Block a user