diff --git a/main.lua b/main.lua index 17a1c65a..c33d63d4 100644 --- a/main.lua +++ b/main.lua @@ -461,14 +461,27 @@ function love.wheelmoved(x, y) Game:wheelmoved(x, y) end -function love.mousepressed(x, y, button) +function love.mousepressed(x, y, button, istouch) if TouchEditor then -- Android primary touch already arrived via love.touchpressed; a second -- mouse path would double-fire Done / begin a second drag. if love.system.getOS() == "Android" then return end return TouchEditor.mousepressed(x, y, button) end - if Importer then return Importer:mousepressed(x, y, button) end + if Importer then + -- The same double-fire TouchEditor guards against, which the launcher was + -- missing: love.touchpressed above forwards the primary touch to the + -- Importer on Android, and LÖVE ALSO synthesizes a mouse press for that + -- same touch, so one tap ran every launcher button twice. On Import that + -- meant two choose() calls and two stacked SAF picker activities: the + -- player picked their ROM, the top picker closed, and the second was still + -- underneath asking for it again, which is the "import the file twice" + -- in #553. Filtering on istouch rather than on the OS keeps a real mouse + -- (DeX, a Chromebook, a USB mouse) working, which an Android-wide return + -- would have broken. + if istouch then return end + return Importer:mousepressed(x, y, button) + end if editorMode and EditorApp.mousepressed then return EditorApp.mousepressed(x, y, button) end diff --git a/scripts/build_ios.sh b/scripts/build_ios.sh index 30d3d08f..869287d1 100755 --- a/scripts/build_ios.sh +++ b/scripts/build_ios.sh @@ -65,6 +65,10 @@ DEVICE=false RELEASE=false PACKAGE_ONLY=false INSTALL=false +# Last resort for an incomplete source export, mirroring build_android.sh. +MANIFEST_BASE_URL="${MANIFEST_BASE_URL:-https://raw.githubusercontent.com/bryanthaboi/gen1recomp/main}" +MANIFESTS="" + VERSION="" say() { printf '\033[1;32m==>\033[0m %s\n' "$*"; } @@ -229,6 +233,68 @@ apply_ios_branding() { } # --------------------------------------------------------------- game.love +# Every version's import manifest has to ship or that game's ROM import fails in +# the built app: decodeManifest (src/import/RomImporter.lua) errors outright when +# one is absent, and dev reads them off the source tree, so the miss only ever +# shows up in a build. iOS shipped without the Yellow one in 0.1.45 to 0.1.47 +# for exactly that reason. +# +# The list is READ OUT OF src/core/GameVersion.lua rather than hand-kept here, so +# a fourth version cannot silently ship without its manifest, and a missing file +# is recovered from Git or the project repo the same way build_android.sh already +# recovers Yellow's. Recovery is a last resort for an incomplete source export: +# a manifest carries extraction metadata only, never a ROM or game data. +manifest_paths() { + python3 - "$ROOT/src/core/GameVersion.lua" <<'PY' +import re, sys +src = open(sys.argv[1]).read() +print(" ".join(dict.fromkeys(re.findall(r'manifest\s*=\s*"([^"]+)"', src)))) +PY +} + +manifest_is_valid() { + python3 - "$1" <<'PY' +import json, pathlib, sys +try: + m = json.loads(pathlib.Path(sys.argv[1]).read_text()) +except (OSError, ValueError): + raise SystemExit(1) +sha = m.get("romSha1") +raise SystemExit(0 if isinstance(sha, str) and len(sha) == 40 else 1) +PY +} + +ensure_manifests() { + MANIFESTS="$(manifest_paths)" + [ -n "$MANIFESTS" ] \ + || fail "could not read any manifest path out of src/core/GameVersion.lua" + local rel staged + for rel in $MANIFESTS; do + if manifest_is_valid "$ROOT/$rel"; then continue; fi + warn "$rel is missing or invalid; recovering it before packaging" + staged="$(mktemp)" + if git -C "$ROOT" show "HEAD:$rel" > "$staged" 2>/dev/null \ + && manifest_is_valid "$staged"; then + mkdir -p "$ROOT/$(dirname "$rel")" + mv "$staged" "$ROOT/$rel" + say "restored $rel from this checkout's Git data" + continue + fi + if command -v curl >/dev/null 2>&1 \ + && curl --fail --location --retry 2 --connect-timeout 15 \ + --output "$staged" "$MANIFEST_BASE_URL/$rel" \ + && manifest_is_valid "$staged"; then + mkdir -p "$ROOT/$(dirname "$rel")" + mv "$staged" "$ROOT/$rel" + say "downloaded $rel from the project repository" + continue + fi + rm -f "$staged" + fail "$rel is unavailable: Git recovery failed and $MANIFEST_BASE_URL/$rel could not be downloaded" + done + say "import manifests: $MANIFESTS" +} + pack_game_love() { say "packing game.love for love-ios resources" mkdir -p "$RESOURCES_DIR" @@ -240,10 +306,10 @@ pack_game_love() { # it reappears every launch. Mods install as .zips at runtime instead # (launcher -> MODS -> Import mod .zip), the same lifecycle as every # other platform. + # shellcheck disable=SC2086 # MANIFESTS is a deliberate word list (cd "$ROOT" && zip -q -9 -r "$LOVE_FILE" \ main.lua conf.lua src data assets tools/save-editor \ - tools/rom_manifest.json tools/rom_manifest_blue.json \ - tools/rom_manifest_yellow.json \ + $MANIFESTS \ -x '*.DS_Store' -x '*/.git/*' -x '*/.DS_Store' \ -x 'data/generated/*' -x 'assets/generated/*') # NOTE: grep -q here would race pipefail — it exits on first match, unzip @@ -259,10 +325,9 @@ pack_game_love() { # outright when a version's manifest is absent, so Import ROM on Yellow died # in the built app while dev, which reads the source tree, stayed green. archive_entries="$(unzip -Z1 "$LOVE_FILE")" + # shellcheck disable=SC2086 # MANIFESTS is a deliberate word list for required in tools/save-editor/App.lua tools/save-editor/Kit.lua \ - tools/save-editor/panels/Party.lua \ - tools/rom_manifest.json tools/rom_manifest_blue.json \ - tools/rom_manifest_yellow.json; do + tools/save-editor/panels/Party.lua $MANIFESTS; do printf '%s\n' "$archive_entries" | grep -qx "$required" \ || fail "game.love is missing $required" done @@ -577,6 +642,7 @@ install_to_device() { apply_ios_branding say "applying iOS native bridge patches (picker/Files support)" python3 "$IOS_DIR/patch_love_src.py" || fail "patch_love_src.py failed" +ensure_manifests pack_game_love ensure_game_love_in_xcode diff --git a/tests/rom_importer_double_pick_test.lua b/tests/rom_importer_double_pick_test.lua index 56f7e3a2..3e1efe54 100644 --- a/tests/rom_importer_double_pick_test.lua +++ b/tests/rom_importer_double_pick_test.lua @@ -109,6 +109,28 @@ ri4.workState = "working" ri4:_pollPickedFiles(0.6) check(not fired4, "the poll stands down while an import is already running") +-- 6. THE ACTUAL #553 CAUSE lives in main.lua, not here. On Android +-- love.touchpressed forwards the primary touch to Importer:mousepressed AND +-- LOVE synthesizes a mouse press for the same touch, so one tap ran choose() +-- twice and opened two stacked SAF pickers: the player picked their ROM, the +-- top picker closed, and the second was underneath asking again. main.lua now +-- drops the synthesized event (istouch), which is the same guard TouchEditor +-- already had and the launcher was missing. +-- +-- Deliberately NOT deduped here: tests/engine/save_import_retry_bug420.lua +-- and rom_pick_error_bug442.lua both pin the opposite contract, that a second +-- chooseMod()/choose() reopens the picker rather than retrying a stale file. +-- Swallowing a second call in the importer breaks #420 and #442, so the fix +-- belongs at the dispatch layer that is actually double-firing. +saveDir = {} +local picks = 0 +love.system.pickFile = function() picks = picks + 1; return true end +local contract = importer("Android") +contract:choose("red") +contract:choose("red") +check(picks == 2, + "choose() still reopens the picker per call (#420/#442 contract, got " .. picks .. ")") + love.system.getOS = saved.getOS love.system.pickFile = saved.pickFile love.filesystem.getDirectoryItems = saved.getDirectoryItems