Compare commits
119 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c11c762f15 | |||
| 780246c4f6 | |||
| 5714555847 | |||
| f5b8b6c85f | |||
| db25c14dfb | |||
| 90163a3ff2 | |||
| 4bdb9435a4 | |||
| ada0d8abe1 | |||
| dbecc345e3 | |||
| 0f8f6d0e4f | |||
| 83463a5a59 | |||
| d66a72ac95 | |||
| 1b659dab01 | |||
| 9e01fe2c2c | |||
| 5fa5005786 | |||
| 1ac5b867bb | |||
| 69ef1bfc77 | |||
| 9ed7e05dc1 | |||
| 25166ff3a1 | |||
| c777e85641 | |||
| 06299328f5 | |||
| 5b19259928 | |||
| c2b6a7b937 | |||
| 7c9c2380d2 | |||
| 2468d5042d | |||
| 51c4766ead | |||
| ec9dc29646 | |||
| 934a4c55ca | |||
| 72592665d7 | |||
| 8f88d01cf2 | |||
| 2279617b29 | |||
| 17fbf6cec4 | |||
| 34c4481f96 | |||
| bff40a5d90 | |||
| 354a8b476d | |||
| f0d3c014a7 | |||
| 0dd889b35b | |||
| 032f894f7f | |||
| 9922e235c6 | |||
| 667267d9bb | |||
| ecdea61cfd | |||
| 872d6b4516 | |||
| def270f7c7 | |||
| 93e336b7cb | |||
| 4c8c1cf36b | |||
| 7d9e99ea18 | |||
| b27e5ab017 | |||
| fd9f3da91a | |||
| a7c19be88f | |||
| 9ab80adaca | |||
| 518d61e039 | |||
| 4349a1142f | |||
| 9713977755 | |||
| 63448ca640 | |||
| b36d38815f | |||
| e24f812475 | |||
| fddf619ed2 | |||
| bf83509ef2 | |||
| 6c05b854c4 | |||
| 2baafab027 | |||
| 813f9d959b | |||
| fba87f028c | |||
| cb4647daf0 | |||
| 93374fbbbb | |||
| abe176b26c | |||
| 085180992d | |||
| 9984958193 | |||
| 5871469002 | |||
| 302b2c9591 | |||
| 67a170fd6e | |||
| 66079686fc | |||
| cc5ff987ac | |||
| f8ba51636b | |||
| fb97318e87 | |||
| 580449b8df | |||
| fd73ab2a11 | |||
| 48f710c3d6 | |||
| 794a5fc6cb | |||
| 286988a1e3 | |||
| 48a3140a28 | |||
| f7bdaa81f8 | |||
| a1ab5e2cff | |||
| 99806ead25 | |||
| def967a8f8 | |||
| 7583ba8729 | |||
| fc83ecd52f | |||
| 5ba49bdf3f | |||
| abf95ce1c4 | |||
| bd1046f398 | |||
| d5ad830fb8 | |||
| c2c7fdafcf | |||
| c01bda3570 | |||
| 74e04cb086 | |||
| 25ec896545 | |||
| 8240205254 | |||
| 55616fc03d | |||
| 675971068e | |||
| cfa8406306 | |||
| 6588901e9a | |||
| 0ea224d5db | |||
| c2e1db0f89 | |||
| 7b1e796c48 | |||
| 4c13770e70 | |||
| cf45cbbf92 | |||
| 4e1ab1879b | |||
| 917735a41c | |||
| c877ea80a7 | |||
| 7d1ddf9b7c | |||
| faf82c2cec | |||
| 5bc6036735 | |||
| 809628f0fa | |||
| 4817ff8bf9 | |||
| 4f8739c029 | |||
| 142d1358dd | |||
| c655217120 | |||
| fbdfc1c053 | |||
| 72c244b433 | |||
| e0e030003b | |||
| ce2afb83f1 |
@@ -0,0 +1 @@
|
|||||||
|
* @bryanthaboi
|
||||||
@@ -5,6 +5,10 @@ body:
|
|||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
|
**Turn off all mods before filing.** Disable everything in the launcher's MODS
|
||||||
|
tab, confirm the bug still happens, then open this. Bugs that only show up with
|
||||||
|
mods on belong with the mod author, not here.
|
||||||
|
|
||||||
A screenshot is worth more than any description. If you can grab one, grab one.
|
A screenshot is worth more than any description. If you can grab one, grab one.
|
||||||
If you genuinely can't, that's fine, but then the details below need to be thorough
|
If you genuinely can't, that's fine, but then the details below need to be thorough
|
||||||
enough that someone can find the bug without ever seeing your screen.
|
enough that someone can find the bug without ever seeing your screen.
|
||||||
@@ -51,22 +55,24 @@ body:
|
|||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: dropdown
|
- type: checkboxes
|
||||||
id: mods_enabled
|
id: mods_off
|
||||||
attributes:
|
attributes:
|
||||||
label: Were any mods on
|
label: Mods off
|
||||||
description: Check the MODS tab in the launcher if you're not sure.
|
description: >
|
||||||
|
Turn off every mod in the launcher's MODS tab and reproduce the bug
|
||||||
|
before submitting. Do not file vanilla bugs with mods still enabled.
|
||||||
options:
|
options:
|
||||||
- "No"
|
- label: I turned off all mods and can still reproduce this
|
||||||
- "Yes"
|
|
||||||
validations:
|
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: input
|
- type: input
|
||||||
id: mods_which
|
id: mods_which
|
||||||
attributes:
|
attributes:
|
||||||
label: Which mods (if any were on)
|
label: Which mods (if you first noticed this with any on)
|
||||||
description: List the enabled mods. Leave blank if none were on.
|
description: >
|
||||||
|
Optional. If you originally hit this with mods enabled, list them —
|
||||||
|
but only after you've confirmed it still happens with all of them off.
|
||||||
placeholder: nuzlocke 1.0.0, running-shoes 0.3
|
placeholder: nuzlocke 1.0.0, running-shoes 0.3
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
|
|||||||
@@ -1,118 +0,0 @@
|
|||||||
name: Feature request
|
|
||||||
description: Ask for something new in the engine, launcher, or platform — not a content/gameplay mod.
|
|
||||||
labels: ["enhancement"]
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
Use this for **engine / launcher / platform** work (ports, video options, save
|
|
||||||
tooling, networking, mod API seams, docs).
|
|
||||||
|
|
||||||
If what you want is a gameplay, cosmetic, audio, or QoL change that a Lua mod
|
|
||||||
could ship — running shoes, alternate sprites, day/night, shiny indicators,
|
|
||||||
Gen 2-like battle toggles, soundtrack packs — open a
|
|
||||||
**[Mod request](https://github.com/bryanthaboi/gen1recomp/issues/new?template=mod_request.yml)**
|
|
||||||
instead.
|
|
||||||
|
|
||||||
"Can we add X" on its own is hard to act on. Say what you want, why you want it,
|
|
||||||
and how you picture it working.
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: summary
|
|
||||||
attributes:
|
|
||||||
label: One line summary
|
|
||||||
description: What you want, in a sentence.
|
|
||||||
placeholder: Add Linux AppImage releases next to the macOS and Windows builds
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: dropdown
|
|
||||||
id: game
|
|
||||||
attributes:
|
|
||||||
label: Which game is this about
|
|
||||||
description: Pick every version it applies to. Use N/A if it isn't game-specific.
|
|
||||||
multiple: true
|
|
||||||
options:
|
|
||||||
- Red
|
|
||||||
- Blue
|
|
||||||
- Yellow
|
|
||||||
- Gold
|
|
||||||
- N/A
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: discord
|
|
||||||
attributes:
|
|
||||||
label: Discord username (optional)
|
|
||||||
description: >
|
|
||||||
So maintainers can ping you on Discord if they need a quick follow-up.
|
|
||||||
Leave blank if you'd rather keep everything on GitHub.
|
|
||||||
placeholder: yourname
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: what
|
|
||||||
attributes:
|
|
||||||
label: What do you want
|
|
||||||
description: >
|
|
||||||
Describe it properly. What is it, where does it live (launcher, options,
|
|
||||||
engine), what does the player see or do. If it changes something that already
|
|
||||||
exists, say what it does today and what it should do instead.
|
|
||||||
placeholder: |
|
|
||||||
Ship a Linux AppImage on each release, same version as the macOS/Windows builds,
|
|
||||||
with the same save folder layout and mod discovery path.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: why
|
|
||||||
attributes:
|
|
||||||
label: Why is this worth doing
|
|
||||||
description: >
|
|
||||||
What's annoying or missing right now. What does this fix. If it's just because you
|
|
||||||
think it would be fun, say that, it's a real answer.
|
|
||||||
placeholder: |
|
|
||||||
LÖVE already runs on Linux; without a packaged build, players have to assemble
|
|
||||||
it themselves and miss release notes / update checks.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: how
|
|
||||||
attributes:
|
|
||||||
label: How should it work
|
|
||||||
description: >
|
|
||||||
The specifics. Which menu, what happens in the edge cases. If you don't
|
|
||||||
know, say what you'd expect as a player and leave the rest open.
|
|
||||||
placeholder: |
|
|
||||||
- GitHub Releases asset next to the .dmg / .exe
|
|
||||||
- Same options.lua / mods/ layout as desktop
|
|
||||||
- Documented in the README install section
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: dropdown
|
|
||||||
id: scope
|
|
||||||
attributes:
|
|
||||||
label: Does this change how the original game plays
|
|
||||||
description: >
|
|
||||||
Some requests are quality of life, some change the actual game. Both are fine,
|
|
||||||
it just helps to know which one you're asking for.
|
|
||||||
options:
|
|
||||||
- Quality of life, original game is untouched
|
|
||||||
- Changes how the game plays
|
|
||||||
- Not sure
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: extra
|
|
||||||
attributes:
|
|
||||||
label: Anything else
|
|
||||||
description: >
|
|
||||||
Reference screenshots, how another game does it, related issues. Leave blank
|
|
||||||
if nothing comes to mind.
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
name: Mod request
|
|
||||||
description: Ask for a gameplay, cosmetic, audio, or QoL change that belongs as a Lua mod.
|
|
||||||
labels: ["mod request"]
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
This tracker is for ideas that should ship as **mods**, not as core engine
|
|
||||||
features — alternate sprites, running shoes, day/night, shiny indicators,
|
|
||||||
soundtrack packs, Gen 2-like battle toggles, map cosmetics, bag QoL, etc.
|
|
||||||
|
|
||||||
The engine already exposes a lot of this through registries and hooks
|
|
||||||
([modding wiki](https://github.com/bryanthaboi/gen1recomp/wiki)).
|
|
||||||
Use a **Feature request** instead for launcher / ports / video options /
|
|
||||||
networking / save tooling / new API seams.
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: summary
|
|
||||||
attributes:
|
|
||||||
label: One line summary
|
|
||||||
description: What the mod should do, in a sentence.
|
|
||||||
placeholder: Hold B to run at bike speed on the overworld
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: dropdown
|
|
||||||
id: game
|
|
||||||
attributes:
|
|
||||||
label: Which game is this for
|
|
||||||
description: Pick every version the mod should cover. Use N/A if it isn't game-specific.
|
|
||||||
multiple: true
|
|
||||||
options:
|
|
||||||
- Red
|
|
||||||
- Blue
|
|
||||||
- Yellow
|
|
||||||
- Gold
|
|
||||||
- N/A
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: discord
|
|
||||||
attributes:
|
|
||||||
label: Discord username (optional)
|
|
||||||
description: >
|
|
||||||
So maintainers or mod authors can ping you on Discord if they pick this up.
|
|
||||||
Leave blank if you'd rather keep everything on GitHub.
|
|
||||||
placeholder: yourname
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: what
|
|
||||||
attributes:
|
|
||||||
label: What should the mod do
|
|
||||||
description: >
|
|
||||||
Describe the player-facing behavior. What changes, where, what does the
|
|
||||||
player see or press. If it toggles from Options or a START-menu entry, say so.
|
|
||||||
placeholder: |
|
|
||||||
Hold B while walking outdoors to move at bike speed. Release to walk again.
|
|
||||||
Same places the bike is allowed; no effect in battles or menus.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: why
|
|
||||||
attributes:
|
|
||||||
label: Why is this worth doing as a mod
|
|
||||||
description: >
|
|
||||||
Why optional/modded rather than a core option. Who wants it on, who wants
|
|
||||||
vanilla left alone.
|
|
||||||
placeholder: |
|
|
||||||
Great for replaying and backtracking, but some people want a strict Gen 1
|
|
||||||
pace. A mod (or an opt-in mod option) keeps both camps happy.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: how
|
|
||||||
attributes:
|
|
||||||
label: How should it work
|
|
||||||
description: >
|
|
||||||
Buttons, menus, edge cases, whether it needs new art/audio. If you know a
|
|
||||||
hook or registry that fits (movement.speed, pokemon.sprite, rulesets, …),
|
|
||||||
mention it — otherwise leave it open.
|
|
||||||
placeholder: |
|
|
||||||
- Hold B on the overworld
|
|
||||||
- Same step timing as the bike
|
|
||||||
- Disabled where the bike is disabled
|
|
||||||
- Prefer hooks:wrap("movement.speed") if that still fits
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: dropdown
|
|
||||||
id: vanilla
|
|
||||||
attributes:
|
|
||||||
label: With the mod off, is vanilla unchanged
|
|
||||||
options:
|
|
||||||
- Yes — parity when disabled
|
|
||||||
- No — it would replace something always-on
|
|
||||||
- Not sure
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: dropdown
|
|
||||||
id: category
|
|
||||||
attributes:
|
|
||||||
label: Best-fit mod category
|
|
||||||
description: Same taxonomy as example mods (BALANCE, GRAPHICS, AUDIO, …).
|
|
||||||
options:
|
|
||||||
- GAMEPLAY / QoL
|
|
||||||
- GRAPHICS
|
|
||||||
- AUDIO
|
|
||||||
- BALANCE / ruleset
|
|
||||||
- CONTENT (maps, encounters, trainers)
|
|
||||||
- UI / TOOL
|
|
||||||
- TOTAL_CONVERSION-ish
|
|
||||||
- Not sure
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: extra
|
|
||||||
attributes:
|
|
||||||
label: Anything else
|
|
||||||
description: >
|
|
||||||
Reference screenshots, other games/hacks that do it, related issues, or
|
|
||||||
"I'd like to try writing this myself." Leave blank if nothing comes to mind.
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
@@ -12,10 +12,11 @@ name: ci
|
|||||||
#
|
#
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
# Integration branch + release branch. PRs already run via pull_request
|
|
||||||
# (any base); this list is only for post-merge push runs.
|
|
||||||
branches: [dev, main]
|
branches: [dev, main]
|
||||||
|
# PRs into dev only: a dev -> main ship PR reuses the required checks the
|
||||||
|
# dev push already put on the same head SHA, so it needs no second run.
|
||||||
pull_request:
|
pull_request:
|
||||||
|
branches: [dev]
|
||||||
|
|
||||||
# a force-push while CI is mid-run should cancel the stale run, not queue
|
# a force-push while CI is mid-run should cancel the stale run, not queue
|
||||||
concurrency:
|
concurrency:
|
||||||
@@ -120,7 +121,7 @@ jobs:
|
|||||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
if git diff --name-only "$BASE_SHA" "$HEAD_SHA" | grep -Eq '^(scripts/build_switch\.sh$|scripts/switch/|docs/switch-.*\.md$|tests/switch_ci_workflows_test\.lua$|tests/switch_transfer_docs_test\.lua$|\.github/workflows/(ci|release|switch-artifact-comment)\.yml$|src/core/(NxAssetOverlay|Platform|GameVersion)\.lua$|src/import/CacheFs\.lua$|tests/engine/(assets_version_fallback|nx_generated_guard|nx_yellow_boot|switch_diagnostics)_test\.lua$|tests/engine/platform_nx)'; then
|
if git diff --name-only "$BASE_SHA" "$HEAD_SHA" | grep -Eq '^(scripts/build_switch\.sh$|scripts/switch/|docs/switch-.*\.md$|tests/switch_ci_workflows_test\.lua$|tests/switch_transfer_docs_test\.lua$|\.github/workflows/(ci|release|switch-artifact-comment)\.yml$|src/core/(NxAssetOverlay|Platform|GameVersion)\.lua$|src/import/CacheFs\.lua$|tests/engine/(assets_version_fallback|nx_generated_guard|nx_yellow_boot|switch_diagnostics|cache_fs_gold_nx_load)_test\.lua$|tests/engine/platform_nx)'; then
|
||||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||||
else
|
else
|
||||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||||
@@ -151,6 +152,9 @@ jobs:
|
|||||||
luajit tests/engine/assets_version_fallback_test.lua
|
luajit tests/engine/assets_version_fallback_test.lua
|
||||||
luajit tests/engine/nx_generated_guard_test.lua
|
luajit tests/engine/nx_generated_guard_test.lua
|
||||||
luajit tests/engine/nx_yellow_boot_test.lua
|
luajit tests/engine/nx_yellow_boot_test.lua
|
||||||
|
luajit tests/engine/cache_fs_gold_nx_load_test.lua
|
||||||
|
luajit tests/engine/cache_fs_blue_mount_test.lua
|
||||||
|
luajit tests/engine/switch_diagnostics_test.lua
|
||||||
|
|
||||||
switch-build:
|
switch-build:
|
||||||
name: Switch fused build
|
name: Switch fused build
|
||||||
@@ -315,52 +319,10 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
scripts/build_linux_arm64.sh --version 0.0.0
|
scripts/build_linux_arm64.sh --version 0.0.0
|
||||||
|
# Shared with the release workflow so shipped images get the same
|
||||||
|
# self-contained / glibc-floor checks as PR builds.
|
||||||
- name: Verify the AppImage is self-contained and bullseye-compatible
|
- name: Verify the AppImage is self-contained and bullseye-compatible
|
||||||
run: |
|
run: bash scripts/linux-arm64/verify_appimage.sh dist/linux-arm64/gen1recomp-0.0.0-linux-arm64.AppImage
|
||||||
set -euo pipefail
|
|
||||||
image="dist/linux-arm64/gen1recomp-0.0.0-linux-arm64.AppImage"
|
|
||||||
|
|
||||||
# --appimage-extract needs no FUSE, so this works on a runner
|
|
||||||
# without /dev/fuse and still exercises the real payload.
|
|
||||||
"$image" --appimage-extract >/dev/null
|
|
||||||
for required in AppRun bin/love game.love lib/liblove-11.5.so; do
|
|
||||||
[ -e "squashfs-root/$required" ] \
|
|
||||||
|| { echo "::error::AppImage is missing $required"; exit 1; }
|
|
||||||
done
|
|
||||||
|
|
||||||
# Every bundled object must resolve once AppRun's LD_LIBRARY_PATH is
|
|
||||||
# applied; an unresolved soname here is a user-visible launch crash.
|
|
||||||
#
|
|
||||||
# This runs on a HEADLESS runner on purpose, and that is the point.
|
|
||||||
# The first version of this build bundled Debian's SDL2, which
|
|
||||||
# hard-links libpulse/libasound/libX11/libwayland, so it only ever
|
|
||||||
# started on a full desktop -- a bare runner is what exposed it.
|
|
||||||
missing="$(LD_LIBRARY_PATH="$PWD/squashfs-root/lib" \
|
|
||||||
ldd squashfs-root/bin/love squashfs-root/lib/*.so* 2>/dev/null \
|
|
||||||
| grep 'not found' || true)"
|
|
||||||
[ -z "$missing" ] || { echo "::error::unresolved deps:"; echo "$missing"; exit 1; }
|
|
||||||
|
|
||||||
# Nothing may hard-link a driver, session or audio-stack library:
|
|
||||||
# those must be reached through dlopen so the AppImage runs on a box
|
|
||||||
# with only ALSA, only Wayland, or only KMSDRM.
|
|
||||||
linked="$(for f in squashfs-root/bin/love squashfs-root/lib/*.so*; do
|
|
||||||
objdump -p "$f" 2>/dev/null | awk '/NEEDED/{print $2}'
|
|
||||||
done | sort -u | grep -E '^lib(pulse|asound|X11|wayland|GL|EGL|drm|gbm|xcb|cairo|sndio|dbus)' || true)"
|
|
||||||
[ -z "$linked" ] \
|
|
||||||
|| { echo "::error::these must be dlopened, not linked:"; echo "$linked"; exit 1; }
|
|
||||||
|
|
||||||
# The whole point of compiling on bullseye. If a future change moves
|
|
||||||
# the builder to a newer base, the glibc floor silently rises and
|
|
||||||
# every user on an older distro gets "GLIBC_2.xx not found" -- catch
|
|
||||||
# it here instead of in a release.
|
|
||||||
floor="$(objdump -T squashfs-root/bin/love squashfs-root/lib/*.so* 2>/dev/null \
|
|
||||||
| grep -o 'GLIBC_[0-9.]*' | sort -V | tail -1)"
|
|
||||||
echo "highest required glibc symbol version: $floor"
|
|
||||||
[ -n "$floor" ] \
|
|
||||||
|| { echo "::error::found no versioned glibc symbols -- objdump read nothing"; exit 1; }
|
|
||||||
highest="$(printf '%s\n' "$floor" "GLIBC_2.31" | sort -V | tail -1)"
|
|
||||||
[ "$highest" = "GLIBC_2.31" ] \
|
|
||||||
|| { echo "::error::AppImage requires $floor, above the bullseye 2.31 floor"; exit 1; }
|
|
||||||
- name: Upload the AppImage
|
- name: Upload the AppImage
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
@@ -397,7 +359,6 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
- run: sudo apt-get update && sudo apt-get install -y luajit
|
|
||||||
- run: python3 -m pip install --upgrade pillow
|
- run: python3 -m pip install --upgrade pillow
|
||||||
|
|
||||||
# the fixture PNGs are committed (they are 8x8 placeholders, not
|
# the fixture PNGs are committed (they are 8x8 placeholders, not
|
||||||
@@ -418,13 +379,8 @@ jobs:
|
|||||||
print(f"\n{len(paths)} fixture assets valid")
|
print(f"\n{len(paths)} fixture assets valid")
|
||||||
PY
|
PY
|
||||||
|
|
||||||
# the fingerprint golden is the parity tripwire; prove it still
|
# the fingerprint parity gates (gate_fingerprint / gate_meta_coverage)
|
||||||
# matches the dataset on a clean checkout
|
# run in the headless job via run_engine; this job only guards the PNGs
|
||||||
- name: fingerprint gate
|
|
||||||
run: luajit tests/engine/gate_fingerprint.lua
|
|
||||||
|
|
||||||
- name: parity-guarantee meta-test
|
|
||||||
run: luajit tests/engine/gate_meta_coverage.lua
|
|
||||||
|
|
||||||
# Only the differ is under test here, and the job is named for that. The
|
# Only the differ is under test here, and the job is named for that. The
|
||||||
# capture half of the golden pipeline does not exist: a POKEPORT_DRIVER
|
# capture half of the golden pipeline does not exist: a POKEPORT_DRIVER
|
||||||
|
|||||||
@@ -161,6 +161,8 @@ jobs:
|
|||||||
scripts/build_linux_arm64.sh \
|
scripts/build_linux_arm64.sh \
|
||||||
--version "${{ needs.version.outputs.version }}" \
|
--version "${{ needs.version.outputs.version }}" \
|
||||||
--game-love .bazinga/work/game.love
|
--game-love .bazinga/work/game.love
|
||||||
|
- name: Verify the AppImage is self-contained and bullseye-compatible
|
||||||
|
run: bash scripts/linux-arm64/verify_appimage.sh "dist/linux-arm64/gen1recomp-${{ needs.version.outputs.version }}-linux-arm64.AppImage"
|
||||||
- name: Upload Linux arm64 release
|
- name: Upload Linux arm64 release
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
@@ -285,7 +287,7 @@ jobs:
|
|||||||
retention-days: 1
|
retention-days: 1
|
||||||
|
|
||||||
release:
|
release:
|
||||||
needs: [version, xbox-uwp, linux-arm64, native-tls-win]
|
needs: [version, love-payload, xbox-uwp, linux-arm64, native-tls-win]
|
||||||
runs-on: ${{ fromJSON(github.repository == 'bryanthaboi/gen1recomp' && '["self-hosted", "macOS"]' || '"macos-latest"') }}
|
runs-on: ${{ fromJSON(github.repository == 'bryanthaboi/gen1recomp' && '["self-hosted", "macOS"]' || '"macos-latest"') }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -307,6 +309,15 @@ jobs:
|
|||||||
name: gen1tls-win-x64
|
name: gen1tls-win-x64
|
||||||
path: dist/native/win-x64
|
path: dist/native/win-x64
|
||||||
|
|
||||||
|
# The same game.love the arm64 AppImage and Xbox UWP builds fused, so
|
||||||
|
# every release asset ships one identical payload (build.sh's own pack
|
||||||
|
# would omit PATCH_NOTES.md and mobile/ios/app-repo.json).
|
||||||
|
- name: Download shared payload
|
||||||
|
uses: actions/download-artifact@v8
|
||||||
|
with:
|
||||||
|
name: gen1recomp-release-love
|
||||||
|
path: dist/payload
|
||||||
|
|
||||||
- name: Import signing certificate into a temporary keychain
|
- name: Import signing certificate into a temporary keychain
|
||||||
if: github.repository == 'bryanthaboi/gen1recomp'
|
if: github.repository == 'bryanthaboi/gen1recomp'
|
||||||
run: |
|
run: |
|
||||||
@@ -357,14 +368,36 @@ jobs:
|
|||||||
echo "::error::gen1tls.dll missing at $GEN1TLS_DLL (native-tls-win job)"
|
echo "::error::gen1tls.dll missing at $GEN1TLS_DLL (native-tls-win job)"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
scripts/build.sh all --version "${{ needs.version.outputs.version }}" --no-notarize
|
scripts/build.sh all --version "${{ needs.version.outputs.version }}" --no-notarize \
|
||||||
|
--game-love dist/payload/game.love
|
||||||
unzip -l dist/win/gen1recomp-win64.zip | grep -F gen1tls.dll \
|
unzip -l dist/win/gen1recomp-win64.zip | grep -F gen1tls.dll \
|
||||||
|| { echo "::error::Windows zip is missing gen1tls.dll"; exit 1; }
|
|| { echo "::error::Windows zip is missing gen1tls.dll"; exit 1; }
|
||||||
|
|
||||||
- name: Build Android
|
- name: Materialize Android release signing key
|
||||||
|
env:
|
||||||
|
KEYSTORE_B64: ${{ secrets.ANDROID_RELEASE_KEYSTORE_B64 }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
scripts/build_android.sh --version "${{ needs.version.outputs.version }}"
|
[ -n "$KEYSTORE_B64" ] || {
|
||||||
|
echo "::error::ANDROID_RELEASE_KEYSTORE_B64 is required for a publishable Android update"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
python3 - <<'PY'
|
||||||
|
import base64, os, pathlib
|
||||||
|
encoded = os.environ["KEYSTORE_B64"]
|
||||||
|
path = pathlib.Path(os.environ["RUNNER_TEMP"]) / "gen1recomp-android-release.keystore"
|
||||||
|
path.write_bytes(base64.b64decode(encoded, validate=True))
|
||||||
|
PY
|
||||||
|
|
||||||
|
- name: Build Android
|
||||||
|
env:
|
||||||
|
GEN1RECOMP_ANDROID_KEYSTORE: ${{ runner.temp }}/gen1recomp-android-release.keystore
|
||||||
|
GEN1RECOMP_ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_RELEASE_KEYSTORE_PASSWORD }}
|
||||||
|
GEN1RECOMP_ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_RELEASE_KEY_ALIAS }}
|
||||||
|
GEN1RECOMP_ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_RELEASE_KEY_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
scripts/build_android.sh --release --version "${{ needs.version.outputs.version }}"
|
||||||
|
|
||||||
- name: Install xcbeautify
|
- name: Install xcbeautify
|
||||||
run: |
|
run: |
|
||||||
@@ -486,8 +519,8 @@ jobs:
|
|||||||
[ -f "$arm64_appimage" ] || { echo "::error::$arm64_appimage not found (expected from the linux-arm64 job)"; exit 1; }
|
[ -f "$arm64_appimage" ] || { echo "::error::$arm64_appimage not found (expected from the linux-arm64 job)"; exit 1; }
|
||||||
cp "$arm64_appimage" "$outdir/gen1recomp-${v}-linux-arm64.AppImage"
|
cp "$arm64_appimage" "$outdir/gen1recomp-${v}-linux-arm64.AppImage"
|
||||||
chmod +x "$outdir/gen1recomp-${v}-linux-arm64.AppImage"
|
chmod +x "$outdir/gen1recomp-${v}-linux-arm64.AppImage"
|
||||||
apk="$(find dist/android/debug -name '*.apk' | head -1)"
|
apk="$(find dist/android/release -name '*.apk' | head -1)"
|
||||||
[ -n "$apk" ] || { echo "::error::no Android APK found under dist/android/debug"; exit 1; }
|
[ -n "$apk" ] || { echo "::error::no Android APK found under dist/android/release"; exit 1; }
|
||||||
cp "$apk" "$outdir/gen1recomp-${v}-android.apk"
|
cp "$apk" "$outdir/gen1recomp-${v}-android.apk"
|
||||||
|
|
||||||
ipa="dist/ios/gen1recomp++.ipa"
|
ipa="dist/ios/gen1recomp++.ipa"
|
||||||
|
|||||||
@@ -140,17 +140,17 @@ ship text.
|
|||||||
|
|
||||||
### 4. `games` (and the legacy `gen2compat`)
|
### 4. `games` (and the legacy `gen2compat`)
|
||||||
|
|
||||||
Pokemon Gold is Gen 2, and it runs its own battle engine, overworld, script
|
Pokemon Gold and Silver are Gen 2, and they run their own battle engine,
|
||||||
VM and save format. The mod API is shared across both generations (same hook
|
overworld, script VM and save format. The mod API is shared across both
|
||||||
names, same event names, same registry names) but Gold cannot serve all of it
|
generations (same hook names, same event names, same registry names) but Gen 2
|
||||||
yet, so Gen 2 is opt-in. Say which games the mod is for:
|
cannot serve all of it yet, so it is opt-in. Say which games the mod is for:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"games": ["gen1", "gen2"]
|
"games": ["gen1", "gen2"]
|
||||||
```
|
```
|
||||||
|
|
||||||
Each entry is a version id (`"red"`, `"blue"`, `"yellow"`, `"gold"`), a
|
Each entry is a version id (`"red"`, `"blue"`, `"yellow"`, `"gold"`,
|
||||||
generation (`"gen1"`, `"gen2"`) or `"all"`;
|
`"silver"`), a generation (`"gen1"`, `"gen2"`) or `"all"`;
|
||||||
`src/mods/ModTargets.lua` resolves them off `GameVersion.ORDER` so nothing
|
`src/mods/ModTargets.lua` resolves them off `GameVersion.ORDER` so nothing
|
||||||
restates the game list. `python3 tools/modkit.py scaffold my_mod --games
|
restates the game list. `python3 tools/modkit.py scaffold my_mod --games
|
||||||
gen1,gen2` writes the key for you. The mod still installs to one directory,
|
gen1,gen2` writes the key for you. The mod still installs to one directory,
|
||||||
@@ -159,10 +159,10 @@ gen1,gen2` writes the key for you. The mod still installs to one directory,
|
|||||||
Absent means Gen 1 only, which is what every mod written before the key existed
|
Absent means Gen 1 only, which is what every mod written before the key existed
|
||||||
was tested as. `"gen2compat": true` is the legacy spelling, still accepted and
|
was tested as. `"gen2compat": true` is the legacy spelling, still accepted and
|
||||||
purely additive (it *adds* the Gen 2 games), so no manifest can lose a game it
|
purely additive (it *adds* the Gen 2 games), so no manifest can lose a game it
|
||||||
already ran on. On a Gold boot a mod claiming no Gen 2 game is not loaded at
|
already ran on. On a Gold or Silver boot a mod claiming no Gen 2 game is not
|
||||||
all: the manager lists it as `ENABLED (NOT THIS GAME)` and says why, because a
|
loaded at all: the manager lists it as `ENABLED (NOT THIS GAME)` and says why,
|
||||||
mod that half-applies reads as a broken mod. Claim Gen 2 once you have actually
|
because a mod that half-applies reads as a broken mod. Claim Gen 2 once you
|
||||||
run your mod on Gold.
|
have actually run your mod on Gold or Silver.
|
||||||
|
|
||||||
Every token is enforced, per game: the loader gates on the same
|
Every token is enforced, per game: the loader gates on the same
|
||||||
`ModTargets.supports` answer both mod surfaces draw, so `"games": ["blue"]`
|
`ModTargets.supports` answer both mod surfaces draw, so `"games": ["blue"]`
|
||||||
@@ -173,7 +173,7 @@ before the key existed changes behavior; list both generations or say `"all"`
|
|||||||
when you mean everywhere.
|
when you mean everywhere.
|
||||||
|
|
||||||
`docs/mod-api-gen2-compat.md` is the compatibility matrix: what works on Gold
|
`docs/mod-api-gen2-compat.md` is the compatibility matrix: what works on Gold
|
||||||
today (40 of the 46 registries, 40 event and 43 hook names shared with Gen 1,
|
and Silver today (40 of the 46 registries, 40 event and 43 hook names shared with Gen 1,
|
||||||
and 24 Gen 2-only ones), which registries have no Gen 2 home and drop their
|
and 24 Gen 2-only ones), which registries have no Gen 2 home and drop their
|
||||||
writes with a report, and which hooks and events are still to come.
|
writes with a report, and which hooks and events are still to come.
|
||||||
`docs/preparing-your-mod-for-gen2.md` is the step-by-step migration guide for a
|
`docs/preparing-your-mod-for-gen2.md` is the step-by-step migration guide for a
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ And before you say, "that's not a recomp", you're wrong. Recomp is an acronym. *
|
|||||||
[Click Here for the AI Use Disclosure!](AIDisclosure.md)
|
[Click Here for the AI Use Disclosure!](AIDisclosure.md)
|
||||||
|
|
||||||
> [!CAUTION]
|
> [!CAUTION]
|
||||||
> **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.
|
> **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. Also, as I assumed would eventually happen, the idiot that made that website now pumped it full of adware. Please stay away from that website.
|
||||||
|
|
||||||
<p align="center"><img src="https://raw.githubusercontent.com/bryanthaboi/gen1recomp/refs/heads/dev/assets/logo/logo.png"></p>
|
<p align="center"><img src="https://raw.githubusercontent.com/bryanthaboi/gen1recomp/refs/heads/dev/assets/logo/logo.png"></p>
|
||||||
|
|
||||||
@@ -53,18 +53,17 @@ And before you say, "that's not a recomp", you're wrong. Recomp is an acronym. *
|
|||||||
|
|
||||||
### Watch the latest update video
|
### Watch the latest update video
|
||||||
|
|
||||||
[](https://www.youtube.com/watch?v=8IOgqbe4YvA)
|
[](https://youtu.be/yi7LkWQPKKM)
|
||||||
|
|
||||||
|
|
||||||
This project does not include a ROM, emulate the Game Boy, transpile assembly,
|
This project does not include a ROM, emulate the Game Boy, transpile assembly,
|
||||||
or download a disassembly. A canonical US Poke Red, Blue, Yellow, or Gold ROM
|
or download a disassembly. A canonical US Poke Red, Blue, Yellow, Gold, or
|
||||||
is the only game content input.
|
Silver ROM is the only game content input.
|
||||||
|
|
||||||
The ROM is verified, used during import, and then released from memory. It is
|
The ROM is verified, used during import, and then released from memory. It is
|
||||||
not copied into the cache. Later launches load the private generated cache and
|
not copied into the cache. Later launches load the private generated cache and
|
||||||
do not ask for the ROM again. Red, Blue, Yellow, and Gold can all be imported
|
do not ask for the ROM again. Red, Blue, Yellow, Gold, and Silver can all be
|
||||||
side by side. Gold is Gen 2 Phase 1 (import + launcher; see
|
imported side by side. Gold and Silver are Gen 2 Phase 1 (import + launcher;
|
||||||
`docs/gold-phase1.md`): the Gen 2 engine is still under construction.
|
see `docs/gold-phase1.md`): the Gen 2 engine is still under construction.
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
@@ -72,13 +71,14 @@ Open the desktop app. On first boot, choose your legally obtained `.gb` /
|
|||||||
`.gbc` file or drop it onto the window. Import takes a few seconds and the
|
`.gbc` file or drop it onto the window. Import takes a few seconds and the
|
||||||
game starts automatically.
|
game starts automatically.
|
||||||
|
|
||||||
Only the canonical US Red, Blue, Yellow (1 MiB), and Gold (2 MiB) ROMs are
|
Only the canonical US Red, Blue, Yellow (1 MiB), Gold, and Silver (2 MiB)
|
||||||
accepted. The importer verifies SHA-1 before creating any game data:
|
ROMs are accepted. The importer verifies SHA-1 before creating any game data:
|
||||||
|
|
||||||
- Red: `ea9bcae617fdf159b045185467ae58b2e4a48b9a`
|
- Red: `ea9bcae617fdf159b045185467ae58b2e4a48b9a`
|
||||||
- Blue: `d7037c83e1ae5b39bde3c30787637ba1d4c48ce2`
|
- Blue: `d7037c83e1ae5b39bde3c30787637ba1d4c48ce2`
|
||||||
- Yellow: `cc7d03262ebfaf2f06772c1a480c7d9d5f4a38e1`
|
- Yellow: `cc7d03262ebfaf2f06772c1a480c7d9d5f4a38e1`
|
||||||
- Gold: `d8b8a3600a465308c9953dfa04f0081c05bdcb94`
|
- Gold: `d8b8a3600a465308c9953dfa04f0081c05bdcb94`
|
||||||
|
- Silver: `49b163f7e57702bc939d642a18f591de55d92dae`
|
||||||
|
|
||||||
The packaged app contains neither a ROM nor pre-extracted game data. Music,
|
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
|
sound effects, and cries are synthesized while the game runs from compact
|
||||||
@@ -219,7 +219,7 @@ entry: a desktop shortcut per game, a Steam entry, or a handheld frontend.
|
|||||||
|
|
||||||
| Option | Effect |
|
| Option | Effect |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `--game=red` | boot Red, skipping the launcher (`blue` and `yellow` too, or just `r` / `b` / `y`) |
|
| `--game=red` | boot Red, skipping the launcher (`blue`, `yellow`, `gold` and `silver` too, or just `r` / `b` / `y` / `g` / `s`) |
|
||||||
| `--slot=2` | load that save slot; takes a slot number or a slot id |
|
| `--slot=2` | load that save slot; takes a slot number or a slot id |
|
||||||
| `--launcher` | open the launcher anyway, so you can edit a shortcut you already made |
|
| `--launcher` | open the launcher anyway, so you can edit a shortcut you already made |
|
||||||
|
|
||||||
@@ -332,7 +332,7 @@ Maps can be edited in our own build of [Tiled](https://www.mapeditor.org),
|
|||||||
and exported back out as a mod; see
|
and exported back out as a mod; see
|
||||||
[docs/tiled-map-editing.md](docs/tiled-map-editing.md).
|
[docs/tiled-map-editing.md](docs/tiled-map-editing.md).
|
||||||
|
|
||||||
## Bugs and Ideas
|
## Bugs
|
||||||
|
|
||||||
Found a bug? A warp dropping you somewhere it shouldn't, a battle doing math
|
Found a bug? A warp dropping you somewhere it shouldn't, a battle doing math
|
||||||
that looks wrong, text in the wrong box, anything that does not match the
|
that looks wrong, text in the wrong box, anything that does not match the
|
||||||
@@ -341,12 +341,6 @@ original game.
|
|||||||
Attach a screenshot if you can. It saves a lot of back and forth, and if you
|
Attach a screenshot if you can. It saves a lot of back and forth, and if you
|
||||||
can't get one, the form asks you to describe what you saw instead.
|
can't get one, the form asks you to describe what you saw instead.
|
||||||
|
|
||||||
Thought of a feature that could be good, or a way to improve one that already
|
|
||||||
exists?
|
|
||||||
[Open a feature request](https://github.com/bryanthaboi/gen1recomp/issues/new?template=feature_request.yml).
|
|
||||||
Say what you want, why it is worth doing, and how you picture it working. A
|
|
||||||
request with real detail is one that can actually get built.
|
|
||||||
|
|
||||||
## More
|
## More
|
||||||
|
|
||||||
- [Link play](https://github.com/bryanthaboi/gen1recomp/wiki/Guide-Link-Play)
|
- [Link play](https://github.com/bryanthaboi/gen1recomp/wiki/Guide-Link-Play)
|
||||||
@@ -354,7 +348,8 @@ request with real detail is one that can actually get built.
|
|||||||
- [Save editor](https://github.com/bryanthaboi/gen1recomp/wiki/Guide-Save-Editor)
|
- [Save editor](https://github.com/bryanthaboi/gen1recomp/wiki/Guide-Save-Editor)
|
||||||
— edit party, boxes, items, events, and Pokédex flags outside the game.
|
— edit party, boxes, items, events, and Pokédex flags outside the game.
|
||||||
- `docs/architecture.md` — runtime details;
|
- `docs/architecture.md` — runtime details;
|
||||||
`docs/behavior-porting-notes.md` — formula provenance.
|
`docs/behavior-porting-notes.md` — formula provenance;
|
||||||
|
`docs/link-security.md` — what link play defends against, and what it doesn't.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
|
After Width: | Height: | Size: 2.7 KiB |
@@ -133,6 +133,8 @@ mkdir -p "$GAME_SRC"
|
|||||||
(cd "$SOURCE_DIR" && zip -q -9 -r "$WORK/game-payload.zip" \
|
(cd "$SOURCE_DIR" && zip -q -9 -r "$WORK/game-payload.zip" \
|
||||||
main.lua conf.lua src libs data assets tools/save-editor \
|
main.lua conf.lua src libs data assets tools/save-editor \
|
||||||
tools/rom_manifest.json tools/rom_manifest_blue.json \
|
tools/rom_manifest.json tools/rom_manifest_blue.json \
|
||||||
|
tools/rom_manifest_yellow.json tools/rom_manifest_gold.json \
|
||||||
|
tools/rom_manifest_silver.json \
|
||||||
-x '*.DS_Store' 'data/generated/*' 'assets/generated/*')
|
-x '*.DS_Store' 'data/generated/*' 'assets/generated/*')
|
||||||
if unzip -Z1 "$WORK/game-payload.zip" \
|
if unzip -Z1 "$WORK/game-payload.zip" \
|
||||||
| grep -Eq '^(data|assets)/generated/[^/]+|^(data|assets)/generated/.+/'; then
|
| grep -Eq '^(data|assets)/generated/[^/]+|^(data|assets)/generated/.+/'; then
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ mkdir -p "$GAME_SRC"
|
|||||||
main.lua conf.lua src libs data assets tools/save-editor \
|
main.lua conf.lua src libs data assets tools/save-editor \
|
||||||
tools/rom_manifest.json tools/rom_manifest_blue.json \
|
tools/rom_manifest.json tools/rom_manifest_blue.json \
|
||||||
tools/rom_manifest_yellow.json tools/rom_manifest_gold.json \
|
tools/rom_manifest_yellow.json tools/rom_manifest_gold.json \
|
||||||
|
tools/rom_manifest_silver.json \
|
||||||
-x '*.DS_Store' 'data/generated/*' 'assets/generated/*')
|
-x '*.DS_Store' 'data/generated/*' 'assets/generated/*')
|
||||||
payload_list="$(unzip -Z1 "$WORK/game-payload.zip")"
|
payload_list="$(unzip -Z1 "$WORK/game-payload.zip")"
|
||||||
printf '%s\n' "$payload_list" \
|
printf '%s\n' "$payload_list" \
|
||||||
@@ -99,6 +100,8 @@ printf '%s\n' "$payload_list" \
|
|||||||
&& fail "payload unexpectedly contains generated ROM data"
|
&& fail "payload unexpectedly contains generated ROM data"
|
||||||
printf '%s\n' "$payload_list" | grep -qxF "tools/rom_manifest_gold.json" \
|
printf '%s\n' "$payload_list" | grep -qxF "tools/rom_manifest_gold.json" \
|
||||||
|| fail "payload is missing tools/rom_manifest_gold.json"
|
|| fail "payload is missing tools/rom_manifest_gold.json"
|
||||||
|
printf '%s\n' "$payload_list" | grep -qxF "tools/rom_manifest_silver.json" \
|
||||||
|
|| fail "payload is missing tools/rom_manifest_silver.json"
|
||||||
unzip -q "$WORK/game-payload.zip" -d "$GAME_SRC"
|
unzip -q "$WORK/game-payload.zip" -d "$GAME_SRC"
|
||||||
rm -f "$WORK/game-payload.zip"
|
rm -f "$WORK/game-payload.zip"
|
||||||
|
|
||||||
@@ -193,6 +196,26 @@ get_controls
|
|||||||
[ -f "${controlfolder}/mod_${CFW_NAME}.txt" ] && source "${controlfolder}/mod_${CFW_NAME}.txt"
|
[ -f "${controlfolder}/mod_${CFW_NAME}.txt" ] && source "${controlfolder}/mod_${CFW_NAME}.txt"
|
||||||
|
|
||||||
GAMEDIR="$SHDIR/gen1recomp"
|
GAMEDIR="$SHDIR/gen1recomp"
|
||||||
|
# Anbernic stock keeps the launcher and the game folder side by side, so the
|
||||||
|
# SHDIR-relative path above is correct there and is tried first.
|
||||||
|
#
|
||||||
|
# Other firmwares (muOS, and PortMaster's layout on several devices) keep
|
||||||
|
# launcher scripts and port data in SEPARATE trees -- scripts under roms/ports,
|
||||||
|
# data under ports -- so the sibling folder holds no game.
|
||||||
|
#
|
||||||
|
# Probe for the BINARY, not the directory: on a split layout this script has
|
||||||
|
# usually already created "$SHDIR/gen1recomp/conf" and log.txt on an earlier
|
||||||
|
# failed run (see mkdir/tee below), so an existence test matches a decoy of our
|
||||||
|
# own making. Stock is unaffected -- its sibling holds the real binary and wins
|
||||||
|
# on the first test.
|
||||||
|
if [ ! -f "$GAMEDIR/bin/love.aarch64" ]; then
|
||||||
|
for candidate in "/$directory/ports/gen1recomp" \
|
||||||
|
"/mnt/sdcard/ports/gen1recomp" \
|
||||||
|
"/mnt/mmc/ports/gen1recomp" \
|
||||||
|
"/roms/ports/gen1recomp"; do
|
||||||
|
if [ -f "$candidate/bin/love.aarch64" ]; then GAMEDIR="$candidate"; break; fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
CONFDIR="$GAMEDIR/conf"
|
CONFDIR="$GAMEDIR/conf"
|
||||||
mkdir -p "$CONFDIR"
|
mkdir -p "$CONFDIR"
|
||||||
|
|
||||||
|
|||||||
@@ -25,8 +25,10 @@
|
|||||||
local Menu = require("src.ui.Menu")
|
local Menu = require("src.ui.Menu")
|
||||||
local TextBox = require("src.render.TextBox")
|
local TextBox = require("src.render.TextBox")
|
||||||
|
|
||||||
-- TMNotebookText (data/text/text_2.asm) has no leading underscore, so the
|
-- TMNotebookText (data/text/text_2.asm) has no leading underscore, but the
|
||||||
-- extractor never collects it and the pamphlet's text is inlined.
|
-- extractor now collects any top-level label in a dedicated text file
|
||||||
|
-- regardless (tools/extract/text.py), so this is the real ROM label --
|
||||||
|
-- the literal below is only the fallback for a catalog without it.
|
||||||
local TM_NOTEBOOK_TEXT = "It's a pamphlet\non TMs.\f...\f"
|
local TM_NOTEBOOK_TEXT = "It's a pamphlet\non TMs.\f...\f"
|
||||||
.. "There are 50 TMs\nin all.\f"
|
.. "There are 50 TMs\nin all.\f"
|
||||||
.. "There are also 5\nHMs that can be\vused repeatedly.\f"
|
.. "There are also 5\nHMs that can be\vused repeatedly.\f"
|
||||||
@@ -70,7 +72,8 @@ return {
|
|||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
if fx == 3 and fy == 4 then
|
if fx == 3 and fy == 4 then
|
||||||
game.stack:push(TextBox.new(game, TM_NOTEBOOK_TEXT))
|
local text = game.data.text or {}
|
||||||
|
game.stack:push(TextBox.new(game, text.TMNotebookText or TM_NOTEBOOK_TEXT))
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -5,8 +5,27 @@
|
|||||||
-- voucher exchange and the BICYCLE/CANCEL price window need more than
|
-- voucher exchange and the BICYCLE/CANCEL price window need more than
|
||||||
-- command rows (#568).
|
-- command rows (#568).
|
||||||
|
|
||||||
|
local TextBox = require("src.render.TextBox")
|
||||||
|
|
||||||
|
-- data/events/hidden_events.asm:542
|
||||||
|
local BIKE_DISPLAYS = {
|
||||||
|
{ 1, 0 }, { 2, 1 }, { 1, 2 }, { 3, 2 }, { 0, 4 }, { 1, 5 },
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
BIKE_SHOP = {
|
BIKE_SHOP = {
|
||||||
|
-- engine/events/hidden_events/new_bike.asm:1
|
||||||
|
onInteract = function(game, ow, fx, fy)
|
||||||
|
for _, c in ipairs(BIKE_DISPLAYS) do
|
||||||
|
if c[1] == fx and c[2] == fy then
|
||||||
|
game.stack:push(TextBox.new(game,
|
||||||
|
(game.data.text or {})._NewBicycleText or "A shiny new\nBICYCLE!"))
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end,
|
||||||
|
|
||||||
talk = {
|
talk = {
|
||||||
-- BikeShopMiddleAgedWomanText (pokered/scripts/BikeShop.asm):
|
-- BikeShopMiddleAgedWomanText (pokered/scripts/BikeShop.asm):
|
||||||
-- always shows the same flavor line, no branching.
|
-- always shows the same flavor line, no branching.
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ return {
|
|||||||
-- pick the dish: bit 7 set (~50%) -> Salmon du Salad, else bit 4
|
-- pick the dish: bit 7 set (~50%) -> Salmon du Salad, else bit 4
|
||||||
-- set (~25%) -> Eels au Barbecue, else (~25%) -> Prime Beef Steak.
|
-- set (~25%) -> Eels au Barbecue, else (~25%) -> Prime Beef Steak.
|
||||||
-- The three dish texts (SSAnneKitchenCook7SalmonDuSaladText /
|
-- The three dish texts (SSAnneKitchenCook7SalmonDuSaladText /
|
||||||
-- ...EelsAuBarbecueText / ...PrimeBeefSteakText) aren't extracted
|
-- ...EelsAuBarbecueText / ...PrimeBeefSteakText) have no leading
|
||||||
-- into data/generated/text.lua (no leading underscore in
|
-- underscore in pokered/text/SSAnneKitchen.asm, but the extractor
|
||||||
-- pokered/text/SSAnneKitchen.asm), so their literal strings are
|
-- collects them regardless (tools/extract/text.py); the literals
|
||||||
-- ported here verbatim.
|
-- below are only the fallback for a catalog without them.
|
||||||
TEXT_SSANNEKITCHEN_COOK7 = function(game, ow, npc, done)
|
TEXT_SSANNEKITCHEN_COOK7 = function(game, ow, npc, done)
|
||||||
local t = game.data.text
|
local t = game.data.text
|
||||||
push(game, t._SSAnneKitchenCook7MainCourseIsText
|
push(game, t._SSAnneKitchenCook7MainCourseIsText
|
||||||
@@ -27,13 +27,16 @@ return {
|
|||||||
local dish
|
local dish
|
||||||
if roll <= 2 then
|
if roll <= 2 then
|
||||||
-- bit 7 of hRandomAdd set (~50%)
|
-- bit 7 of hRandomAdd set (~50%)
|
||||||
dish = "Salmon du Salad!\fLes guests may\ngripe it's fish\vagain, however!"
|
dish = t.SSAnneKitchenCook7SalmonDuSaladText
|
||||||
|
or "Salmon du Salad!\fLes guests may\ngripe it's fish\vagain, however!"
|
||||||
elseif roll == 3 then
|
elseif roll == 3 then
|
||||||
-- bit 4 set, bit 7 clear (~25%)
|
-- bit 4 set, bit 7 clear (~25%)
|
||||||
dish = "Eels au Barbecue!\fLes guests will\nmutiny, I fear."
|
dish = t.SSAnneKitchenCook7EelsAuBarbecueText
|
||||||
|
or "Eels au Barbecue!\fLes guests will\nmutiny, I fear."
|
||||||
else
|
else
|
||||||
-- neither bit set (~25%)
|
-- neither bit set (~25%)
|
||||||
dish = "Prime Beef Steak!\fBut, have I enough\nfillets du beef?"
|
dish = t.SSAnneKitchenCook7PrimeBeefSteakText
|
||||||
|
or "Prime Beef Steak!\fBut, have I enough\nfillets du beef?"
|
||||||
end
|
end
|
||||||
push(game, dish, done)
|
push(game, dish, done)
|
||||||
end)
|
end)
|
||||||
|
|||||||
@@ -60,22 +60,23 @@ M.VIRIDIAN_CITY = {
|
|||||||
-- you want to know about the two kinds of caterpillar Pokemon;
|
-- you want to know about the two kinds of caterpillar Pokemon;
|
||||||
-- YES -> CATERPIE/WEEDLE description, NO -> "Oh, OK then!".
|
-- YES -> CATERPIE/WEEDLE description, NO -> "Oh, OK then!".
|
||||||
-- ViridianCityYoungster2OkThenText and
|
-- ViridianCityYoungster2OkThenText and
|
||||||
-- ViridianCityYoungster2CaterpieAndWeedleDescriptionText are
|
-- ViridianCityYoungster2CaterpieAndWeedleDescriptionText are defined
|
||||||
-- defined without a leading underscore in pokered/text/ViridianCity.asm
|
-- without a leading underscore in pokered/text/ViridianCity.asm, but
|
||||||
-- and aren't present in data/generated/text.lua, so we fall back to
|
-- tools/extract/text.py now collects them regardless -- the literal
|
||||||
-- the literal strings from pokered. Those fallbacks have to carry the
|
-- strings below are only the fallback for a catalog without them.
|
||||||
-- extractor's markers, not plain newlines: line -> \n, cont -> \v,
|
-- Those fallbacks have to carry the extractor's markers, not plain
|
||||||
-- para -> \f. Spelling cont/para as \n and \n\n put all six lines on
|
-- newlines: line -> \n, cont -> \v, para -> \f. Spelling cont/para as
|
||||||
-- one page with nothing to wait on, so the whole speech scrolled past
|
-- \n and \n\n put all six lines on one page with nothing to wait on,
|
||||||
-- without a button press (#250).
|
-- so the whole speech scrolled past without a button press (#250).
|
||||||
TEXT_VIRIDIANCITY_YOUNGSTER2 = function(game, ow, npc, done)
|
TEXT_VIRIDIANCITY_YOUNGSTER2 = function(game, ow, npc, done)
|
||||||
local t = text(game)
|
local t = text(game)
|
||||||
ask(game, t._ViridianCityYoungster2YouWantToKnowAboutText
|
ask(game, t._ViridianCityYoungster2YouWantToKnowAboutText
|
||||||
or "You want to know\nabout the 2 kinds\vof caterpillar\vPOKéMON?", function(yes)
|
or "You want to know\nabout the 2 kinds\vof caterpillar\vPOKéMON?", function(yes)
|
||||||
if yes then
|
if yes then
|
||||||
push(game, "CATERPIE has no\npoison, but\vWEEDLE does.\fWatch out for its\nPOISON STING!", done)
|
push(game, t.ViridianCityYoungster2CaterpieAndWeedleDescriptionText
|
||||||
|
or "CATERPIE has no\npoison, but\vWEEDLE does.\fWatch out for its\nPOISON STING!", done)
|
||||||
else
|
else
|
||||||
push(game, "Oh, OK then!", done)
|
push(game, t.ViridianCityYoungster2OkThenText or "Oh, OK then!", done)
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end,
|
end,
|
||||||
|
|||||||
@@ -38,6 +38,22 @@ local function retryTmGive(game, ow, victoryKey, done)
|
|||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- The badge line + its jingle, armed for the battle screen the way
|
||||||
|
-- SaveEndBattleTextPointers does (PewterGym.asm:117-119) (#1606)
|
||||||
|
local function badgeEndBattleText(game, victoryKey)
|
||||||
|
local reward = victoryKey and require("data.scripts.victories")[victoryKey]
|
||||||
|
if not (reward and reward.dialogue) then return nil end
|
||||||
|
local text = game.data.text or {}
|
||||||
|
local pages = {}
|
||||||
|
for _, label in ipairs(reward.dialogue) do
|
||||||
|
if text[label] and text[label] ~= "" then
|
||||||
|
pages[#pages + 1] = text[label]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if #pages == 0 then return nil end
|
||||||
|
return table.concat(pages, "\f"), reward.badgeSound
|
||||||
|
end
|
||||||
|
|
||||||
-- scripts/PewterGym.asm PewterGymBrockText (text_asm): CheckEvent
|
-- scripts/PewterGym.asm PewterGymBrockText (text_asm): CheckEvent
|
||||||
-- EVENT_BEAT_BROCK branches his dialogue. Before the badge he prints
|
-- EVENT_BEAT_BROCK branches his dialogue. Before the badge he prints
|
||||||
-- _PewterGymBrockPreBattleText and engages the leader battle
|
-- _PewterGymBrockPreBattleText and engages the leader battle
|
||||||
@@ -58,7 +74,8 @@ M.PEWTER_GYM.talk = {
|
|||||||
game.data.text._PewterGymBrockPostBattleAdviceText
|
game.data.text._PewterGymBrockPostBattleAdviceText
|
||||||
or "Go to the GYM in\nCERULEAN and test\nyour abilities!", done))
|
or "Go to the GYM in\nCERULEAN and test\nyour abilities!", done))
|
||||||
else
|
else
|
||||||
ow:engageTrainer(npc, done)
|
local text, sound = badgeEndBattleText(game, "OPP_BROCK#1")
|
||||||
|
ow:engageTrainer(npc, done, text, nil, sound)
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
}
|
}
|
||||||
@@ -91,7 +108,8 @@ local function leaderTalk(beatFlag, adviceLabel, fallback, afterAdvice, victoryK
|
|||||||
game.stack:push(TextBox.new(game,
|
game.stack:push(TextBox.new(game,
|
||||||
game.data.text[adviceLabel] or fallback, finish))
|
game.data.text[adviceLabel] or fallback, finish))
|
||||||
else
|
else
|
||||||
ow:engageTrainer(npc, done)
|
local text, sound = badgeEndBattleText(game, victoryKey)
|
||||||
|
ow:engageTrainer(npc, done, text, nil, sound)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -168,6 +168,7 @@ return {
|
|||||||
{ "jump_if_true", "come_see" },
|
{ "jump_if_true", "come_see" },
|
||||||
{ "set_flag", "EVENT_GOT_POKEBALLS_FROM_OAK" },
|
{ "set_flag", "EVENT_GOT_POKEBALLS_FROM_OAK" },
|
||||||
{ "give_item", "POKE_BALL", 5, false },
|
{ "give_item", "POKE_BALL", 5, false },
|
||||||
|
{ "text_sound", "Get_Key_Item" }, -- OaksLab.asm:1060
|
||||||
{ "show_text", "_OaksLabOak1ReceivedPokeballsText" },
|
{ "show_text", "_OaksLabOak1ReceivedPokeballsText" },
|
||||||
{ "show_text", "_OaksLabGivePokeballsExplanationText" },
|
{ "show_text", "_OaksLabGivePokeballsExplanationText" },
|
||||||
{ "jump", "end" },
|
{ "jump", "end" },
|
||||||
|
|||||||
@@ -837,13 +837,14 @@ M.SILPH_CO_11F = {
|
|||||||
-- every Silph rocket leaves off-screen (the street rockets are
|
-- every Silph rocket leaves off-screen (the street rockets are
|
||||||
-- handled by M.SAFFRON_CITY.onEnter in story4.lua). Queued, not
|
-- handled by M.SAFFRON_CITY.onEnter in story4.lua). Queued, not
|
||||||
-- run here: the battle's own callbacks are still unwinding, so
|
-- run here: the battle's own callbacks are still unwinding, so
|
||||||
-- queueScript starts it on the first idle overworld frame --
|
-- queueScript starts it on the first idle overworld frame (#722).
|
||||||
-- after the end-battle "Arrgh!!" box victories.lua OPP_GIOVANNI#2
|
|
||||||
-- pushes (#722).
|
|
||||||
if game.save.flags.EVENT_BEAT_SILPH_CO_GIOVANNI then
|
if game.save.flags.EVENT_BEAT_SILPH_CO_GIOVANNI then
|
||||||
ow:queueScript(silphAftermathRows())
|
ow:queueScript(silphAftermathRows())
|
||||||
end
|
end
|
||||||
end, nil, true)
|
end,
|
||||||
|
-- "Arrgh!!" is armed for the battle screen, not the map
|
||||||
|
-- (scripts/SilphCo11F.asm:264-266 SaveEndBattleTextPointers) #1606
|
||||||
|
game.data.text._SilphCo10FGiovanniILostAgainText, true)
|
||||||
end)
|
end)
|
||||||
end))
|
end))
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -733,7 +733,8 @@ local function museumClerk(game, ow, done, onDecline)
|
|||||||
local t = game.data.text or {}
|
local t = game.data.text or {}
|
||||||
if game.save.flags.EVENT_BOUGHT_MUSEUM_TICKET then
|
if game.save.flags.EVENT_BOUGHT_MUSEUM_TICKET then
|
||||||
game.stack:push(TextBox.new(game,
|
game.stack:push(TextBox.new(game,
|
||||||
"Take your time,\nand enjoy it all!", done))
|
t._Museum1FScientist1TakePlentyOfTimeText
|
||||||
|
or "Take your time,\nand enjoy it all!", done))
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
-- scripts/Museum1F.asm:72
|
-- scripts/Museum1F.asm:72
|
||||||
@@ -751,10 +752,12 @@ local function museumClerk(game, ow, done, onDecline)
|
|||||||
{ money = money }))
|
{ money = money }))
|
||||||
elseif yes then
|
elseif yes then
|
||||||
game.stack:push(TextBox.new(game,
|
game.stack:push(TextBox.new(game,
|
||||||
"You don't have\nenough money.", onDecline or done, { money = money }))
|
t._Museum1FScientist1DontHaveEnoughMoneyText
|
||||||
|
or "You don't have\nenough money.", onDecline or done, { money = money }))
|
||||||
else
|
else
|
||||||
game.stack:push(TextBox.new(game,
|
game.stack:push(TextBox.new(game,
|
||||||
"Come again!", onDecline or done, { money = money }))
|
t._Museum1FScientist1ComeAgainText
|
||||||
|
or "Come again!", onDecline or done, { money = money }))
|
||||||
end
|
end
|
||||||
end }))
|
end }))
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -216,7 +216,10 @@ local function dojoMasterGate(game, ow, x, y)
|
|||||||
if not master or ow:trainerDefeated(master) then return false end
|
if not master or ow:trainerDefeated(master) then return false end
|
||||||
ow.player.facing = "right"
|
ow.player.facing = "right"
|
||||||
master:facePlayer(ow.player)
|
master:facePlayer(ow.player)
|
||||||
ow:engageTrainer(master)
|
-- scripts/FightingDojo.asm:117-119 SaveEndBattleTextPointers (#1606)
|
||||||
|
ow:engageTrainer(master, nil,
|
||||||
|
((game.data or {}).text or {})._FightingDojoKarateMasterDefeatedText,
|
||||||
|
nil, nil, false)
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -516,7 +519,17 @@ M.ROUTE_24 = {
|
|||||||
push(game, text(game)._Route24CooltrainerM1YouCouldBecomeATopLeaderText,
|
push(game, text(game)._Route24CooltrainerM1YouCouldBecomeATopLeaderText,
|
||||||
done)
|
done)
|
||||||
else
|
else
|
||||||
ow:engageTrainer(npc, done)
|
-- scripts/Route24.asm:125
|
||||||
|
ow:engageTrainer(npc, function()
|
||||||
|
if ow:trainerDefeated(npc) then
|
||||||
|
-- scripts/Route24.asm:62
|
||||||
|
push(game,
|
||||||
|
text(game)._Route24CooltrainerM1YouCouldBecomeATopLeaderText,
|
||||||
|
done)
|
||||||
|
else
|
||||||
|
done()
|
||||||
|
end
|
||||||
|
end, text(game)._Route24CooltrainerM1DefeatedText, true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if not flags.EVENT_GOT_NUGGET then
|
if not flags.EVENT_GOT_NUGGET then
|
||||||
|
|||||||
@@ -122,9 +122,8 @@ M.CINNABAR_LAB_METRONOME_ROOM = {
|
|||||||
-- TM42 Dream Eater (scripts/ViridianCity.asm, the fisher). The fisher's
|
-- TM42 Dream Eater (scripts/ViridianCity.asm, the fisher). The fisher's
|
||||||
-- YouCanHaveThisText prints before GiveItem, so this gift needs a pre
|
-- YouCanHaveThisText prints before GiveItem, so this gift needs a pre
|
||||||
-- text (#775). Like the SilphCo2F worker (#393) that label carries no
|
-- text (#775). Like the SilphCo2F worker (#393) that label carries no
|
||||||
-- leading underscore, and on Red it sits outside the extractor's symbol
|
-- leading underscore; tools/extract/text.py now collects it regardless,
|
||||||
-- set, so the literal from text/ViridianCity.asm rides along as the
|
-- so preFallback below is just the safety net for a catalog without it.
|
||||||
-- fallback; Yellow resolves the ROM string instead.
|
|
||||||
M.VIRIDIAN_CITY = {
|
M.VIRIDIAN_CITY = {
|
||||||
talk = {
|
talk = {
|
||||||
TEXT_VIRIDIANCITY_FISHER = gift({
|
TEXT_VIRIDIANCITY_FISHER = gift({
|
||||||
@@ -146,9 +145,11 @@ M.SILPH_CO_2F = {
|
|||||||
talk = {
|
talk = {
|
||||||
TEXT_SILPHCO2F_SILPH_WORKER_F = gift({
|
TEXT_SILPHCO2F_SILPH_WORKER_F = gift({
|
||||||
flag = "EVENT_GOT_TM36", item = "TM_SELFDESTRUCT",
|
flag = "EVENT_GOT_TM36", item = "TM_SELFDESTRUCT",
|
||||||
-- the label carries no leading underscore: pokered keeps this one in
|
-- the label carries no leading underscore (#393); collected like any
|
||||||
-- the script bank, not the far-text bank (#393)
|
-- other text/*.asm label now, preFallback is just the safety net
|
||||||
pre = "SilphCo2FSilphWorkerFPleaseTakeThisText",
|
pre = "SilphCo2FSilphWorkerFPleaseTakeThisText",
|
||||||
|
preFallback = "Eeek!\nNo! Stop! Help!\fOh, you're not\nwith TEAM ROCKET."
|
||||||
|
.. "\vI thought...\vI'm sorry. Here,\vplease take this!",
|
||||||
received = "_SilphCo2FSilphWorkerFReceivedTM36Text",
|
received = "_SilphCo2FSilphWorkerFReceivedTM36Text",
|
||||||
explain = "_SilphCo2FSilphWorkerFTM36ExplanationText",
|
explain = "_SilphCo2FSilphWorkerFTM36ExplanationText",
|
||||||
noRoom = "_SilphCo2FSilphWorkerFTM36NoRoomText",
|
noRoom = "_SilphCo2FSilphWorkerFTM36NoRoomText",
|
||||||
@@ -646,27 +647,29 @@ end
|
|||||||
local rocketRows = {
|
local rocketRows = {
|
||||||
{ "face_player" }, -- 1
|
{ "face_player" }, -- 1
|
||||||
{ "check_flag", "EVENT_GOT_TM28" }, -- 2
|
{ "check_flag", "EVENT_GOT_TM28" }, -- 2
|
||||||
{ "jump_if_true", 15 }, -- 3 → CeruleanHideRocket
|
{ "jump_if_true", 16 }, -- 3 → CeruleanHideRocket
|
||||||
{ "check_flag", "EVENT_BEAT_CERULEAN_ROCKET_THIEF" }, -- 4
|
{ "check_flag", "EVENT_BEAT_CERULEAN_ROCKET_THIEF" }, -- 4
|
||||||
{ "jump_if_true", 9 }, -- 5
|
{ "jump_if_true", 10 }, -- 5
|
||||||
{ "show_text", "_CeruleanCityRocketText" }, -- 6
|
{ "show_text", "_CeruleanCityRocketText" }, -- 6
|
||||||
{ "start_battle", "trainer", "OPP_ROCKET", 5 }, -- 7
|
-- scripts/CeruleanCity.asm:297 SaveEndBattleTextPointers
|
||||||
{ "jump_if_false", "end" }, -- 8
|
{ "save_end_battle_text", "_CeruleanCityRocketIGiveUpText" }, -- 7
|
||||||
{ "show_text", "_CeruleanCityRocketIllReturnTheTMText" }, -- 9
|
{ "start_battle", "trainer", "OPP_ROCKET", 5 }, -- 8
|
||||||
{ "set_flag", "EVENT_BEAT_CERULEAN_ROCKET_THIEF" }, -- 10
|
{ "jump_if_false", "end" }, -- 9
|
||||||
{ "give_item", "TM_DIG", 1, false }, -- 11 (row 13 prints)
|
{ "show_text", "_CeruleanCityRocketIllReturnTheTMText" }, -- 10
|
||||||
{ "set_flag", "EVENT_GOT_TM28" }, -- 12
|
{ "set_flag", "EVENT_BEAT_CERULEAN_ROCKET_THIEF" }, -- 11
|
||||||
{ "show_text", "_CeruleanCityRocketReceivedTM28Text" }, -- 13
|
{ "give_item", "TM_DIG", 1, false }, -- 12 (row 14 prints)
|
||||||
{ "show_text", "_CeruleanCityRocketIBetterGetMovingText" }, -- 14
|
{ "set_flag", "EVENT_GOT_TM28" }, -- 13
|
||||||
{ "fade", "out" }, -- 15 GBFadeOutToBlack
|
{ "show_text", "_CeruleanCityRocketReceivedTM28Text" }, -- 14
|
||||||
|
{ "show_text", "_CeruleanCityRocketIBetterGetMovingText" }, -- 15
|
||||||
|
{ "fade", "out" }, -- 16 GBFadeOutToBlack
|
||||||
-- CeruleanHideRocket while black: GUARD1 (28,12) appears, GUARD2
|
-- CeruleanHideRocket while black: GUARD1 (28,12) appears, GUARD2
|
||||||
-- (27,12) and the ROCKET go. GUARD2 blocks the trashed-house south
|
-- (27,12) and the ROCKET go. GUARD2 blocks the trashed-house south
|
||||||
-- door neighbour -- the swap reconnects the city (Bill's ticket does
|
-- door neighbour -- the swap reconnects the city (Bill's ticket does
|
||||||
-- the same in story.lua; either route is enough).
|
-- the same in story.lua; either route is enough).
|
||||||
{ "show_object", "CERULEAN_CITY", "CERULEANCITY_GUARD1" }, -- 16
|
{ "show_object", "CERULEAN_CITY", "CERULEANCITY_GUARD1" }, -- 17
|
||||||
{ "hide_object", "CERULEAN_CITY", "CERULEANCITY_GUARD2" }, -- 17
|
{ "hide_object", "CERULEAN_CITY", "CERULEANCITY_GUARD2" }, -- 18
|
||||||
{ "hide_object", "CERULEAN_CITY", "CERULEANCITY_ROCKET" }, -- 18
|
{ "hide_object", "CERULEAN_CITY", "CERULEANCITY_ROCKET" }, -- 19
|
||||||
{ "fade", "in" }, -- 19 GBFadeInFromBlack
|
{ "fade", "in" }, -- 20 GBFadeInFromBlack
|
||||||
}
|
}
|
||||||
|
|
||||||
M.CERULEAN_CITY = {
|
M.CERULEAN_CITY = {
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ local M = {}
|
|||||||
|
|
||||||
local function text(game) return game.data.text end
|
local function text(game) return game.data.text end
|
||||||
|
|
||||||
local function push(game, s, done)
|
local function push(game, s, done, opts)
|
||||||
local TextBox = require("src.render.TextBox")
|
local TextBox = require("src.render.TextBox")
|
||||||
game.stack:push(TextBox.new(game, s, done))
|
game.stack:push(TextBox.new(game, s, done, opts))
|
||||||
end
|
end
|
||||||
|
|
||||||
-- PrintText on a text_end string returns with the box still drawn and
|
-- PrintText on a text_end string returns with the box still drawn and
|
||||||
@@ -236,7 +236,6 @@ M.CINNABAR_GYM = {
|
|||||||
if yes == machine.yes then
|
if yes == machine.yes then
|
||||||
-- CinnabarGymQuizCorrectText: item jingle, then the gate
|
-- CinnabarGymQuizCorrectText: item jingle, then the gate
|
||||||
-- slides open (SFX_GO_INSIDE) if it was still locked
|
-- slides open (SFX_GO_INSIDE) if it was still locked
|
||||||
Sound.play(game.data, "Get_Item1")
|
|
||||||
push(game, t._CinnabarGymQuizCorrectText
|
push(game, t._CinnabarGymQuizCorrectText
|
||||||
or "You're absolutely\ncorrect!\fGo on through!", function()
|
or "You're absolutely\ncorrect!\fGo on through!", function()
|
||||||
if not game.save.flags[gymGateFlag(index)] then
|
if not game.save.flags[gymGateFlag(index)] then
|
||||||
@@ -244,7 +243,9 @@ M.CINNABAR_GYM = {
|
|||||||
Sound.play(game.data, "Go_Inside")
|
Sound.play(game.data, "Go_Inside")
|
||||||
end
|
end
|
||||||
applyGymGates(game, ow)
|
applyGymGates(game, ow)
|
||||||
end)
|
end, { preSound = function()
|
||||||
|
return Sound.play(game.data, "Get_Item1")
|
||||||
|
end })
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
Sound.play(game.data, "Denied")
|
Sound.play(game.data, "Denied")
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ local function surfingPikachu(game)
|
|||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
local function push(game, text, done)
|
local function push(game, text, done, opts)
|
||||||
local TextBox = require("src.render.TextBox")
|
local TextBox = require("src.render.TextBox")
|
||||||
game.stack:push(TextBox.new(game, text, done))
|
game.stack:push(TextBox.new(game, text, done, opts))
|
||||||
end
|
end
|
||||||
|
|
||||||
-- the two-variant posters: the surf-capable line once a surfing
|
-- the two-variant posters: the surf-capable line once a surfing
|
||||||
@@ -69,11 +69,11 @@ return {
|
|||||||
|
|
||||||
TEXT_SUMMERBEACHHOUSE_PIKACHU = function(game, ow, npc, done)
|
TEXT_SUMMERBEACHHOUSE_PIKACHU = function(game, ow, npc, done)
|
||||||
local t = game.data.text
|
local t = game.data.text
|
||||||
|
-- scripts/SummerBeachHouse.asm:68
|
||||||
push(game, t._SummerBeachHousePikachuText or "PIKACHU: Pikaa!",
|
push(game, t._SummerBeachHousePikachuText or "PIKACHU: Pikaa!",
|
||||||
function()
|
done, { auto = { wait = true, delay = 0, sound = function()
|
||||||
require("src.core.Sound").playCry(game.data, "PIKACHU")
|
return require("src.core.Sound").playCry(game.data, "PIKACHU")
|
||||||
done()
|
end } })
|
||||||
end)
|
|
||||||
end,
|
end,
|
||||||
|
|
||||||
TEXT_SUMMERBEACHHOUSE_POSTER1 = poster(1),
|
TEXT_SUMMERBEACHHOUSE_POSTER1 = poster(1),
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
# Link play: threat model and what the code actually guarantees
|
||||||
|
|
||||||
|
Link play is the only part of this game that reads bytes written by
|
||||||
|
somebody else. This is what it defends against, what it does not, and
|
||||||
|
where each guarantee lives.
|
||||||
|
|
||||||
|
## The boundary
|
||||||
|
|
||||||
|
Everything a peer or the relay sends arrives as one JSON object per line.
|
||||||
|
There is exactly one place it becomes a message:
|
||||||
|
|
||||||
|
src/link/Net.lua reads bytes, frames lines, decodes JSON
|
||||||
|
src/link/Wire.lua rebuilds each line as a typed message
|
||||||
|
src/link/Session.lua the only path from a transport into a mode
|
||||||
|
|
||||||
|
`Session:update` runs `Wire.sanitize` on every message before anything
|
||||||
|
else sees it. A schema returns a **new** table holding only the fields it
|
||||||
|
names, at the Lua types it names, so the rest of `src/link/` can read
|
||||||
|
`msg.slot`, `msg.parts.actives` or `msg.mons[i].dvs.hp` directly and be
|
||||||
|
right by construction. A message with no schema (a mod's, or a future
|
||||||
|
build's) keeps a bounded, scalar-only copy of its payload instead of
|
||||||
|
being dropped.
|
||||||
|
|
||||||
|
A message that fails its schema is **dropped and logged**, never fatal.
|
||||||
|
Latching a terminal failure would hand a hostile peer a cheaper
|
||||||
|
disconnect than sending nothing at all.
|
||||||
|
|
||||||
|
### Why the bounds are loose
|
||||||
|
|
||||||
|
Wire's numeric bounds are deliberately wider than the game's own clamps in
|
||||||
|
`Protocol.unpackMon`. Both peers run identical clamps over identical
|
||||||
|
packets; a bound that bit an honest value would change one side's copy of
|
||||||
|
a mon and desync the lockstep. Wire's job is types and sizes. Rules are
|
||||||
|
`Protocol`'s job, and it keeps its own clamps for the callers that reach
|
||||||
|
it without a Session (the mod API, `tests/`).
|
||||||
|
|
||||||
|
### Containment behind it
|
||||||
|
|
||||||
|
Assume something still gets through:
|
||||||
|
|
||||||
|
- `Game:step` pcalls the link pump, and pcalls `stack:update` **only
|
||||||
|
while a link session is active**. On a throw, `Game:breakLink` closes
|
||||||
|
the connection, unwinds to the overworld and says "The link was
|
||||||
|
broken." Outside link play the stack is unguarded on purpose: a blanket
|
||||||
|
pcall would swallow real engine bugs and leave the game silently wrong
|
||||||
|
instead of loudly broken.
|
||||||
|
- `Net` caps `rxBuf` at 256KB and its per-frame read at 512KB, so a peer
|
||||||
|
that never sends a newline ends as a clean disconnect.
|
||||||
|
- `Json.decode` refuses documents nested past 64 levels, and takes an
|
||||||
|
optional length cap that the link path passes and the mod-manifest path
|
||||||
|
does not.
|
||||||
|
|
||||||
|
## The relay (`../pokeserver`)
|
||||||
|
|
||||||
|
- A line that is not a JSON **object** with a string `type` is dropped
|
||||||
|
before any handler runs, and `onLine` is wrapped in try/catch.
|
||||||
|
`server.js` installs `uncaughtException`/`unhandledRejection` handlers:
|
||||||
|
one bad packet must never take every live match down with the process.
|
||||||
|
- Line buffers are capped, lines per second are capped, connections per
|
||||||
|
IP and in total are capped, and an unbound connection that never hosts
|
||||||
|
or joins is swept after 30s.
|
||||||
|
- `SERVER_ONLY` is the set of message types the server is the only
|
||||||
|
legitimate author of (`peer_gone`, `bracket_update`, `match_start`,
|
||||||
|
`tournament_over`, `spectate`, ...). A peer that sends one has them
|
||||||
|
dropped rather than forwarded, so a bracket opponent cannot forge a
|
||||||
|
tournament result or fake "your opponent left".
|
||||||
|
- Trainer names are reduced to a printable subset and capped at the same
|
||||||
|
10 characters the game enforces, on the way in, because they are
|
||||||
|
rendered by the dashboard and broadcast to every participant.
|
||||||
|
|
||||||
|
`pokeserver/test/hostile.js` is the regression net for all of that.
|
||||||
|
|
||||||
|
## What is NOT defended
|
||||||
|
|
||||||
|
**Party legality is trust-the-client.** Online play meets strangers, and
|
||||||
|
`Handshake.onlineAllowed` is a Lua function in the same VM the mods load
|
||||||
|
into. It cannot be made tamper-proof in-process, and pretending otherwise
|
||||||
|
would only cost honest mod authors. What lockstep and
|
||||||
|
`Protocol.unpackMon`'s recompute-from-species-data *do* guarantee is that
|
||||||
|
a cheater cannot invent stats, moves, or a shiny: every derived value is
|
||||||
|
rebuilt locally from real species data. They can send a legal party they
|
||||||
|
farmed or edited. That is the honest boundary.
|
||||||
|
|
||||||
|
What the relay does instead is **observe and record**. It already sees
|
||||||
|
every `hello`, so it keeps each connection's self-reported
|
||||||
|
`engineVersion`, `fingerprint` and `linkModified`, compares the two sides
|
||||||
|
of a room or a live tournament match, and logs and surfaces a
|
||||||
|
`modded` / `fingerprint_mismatch` / `version_skew` flag on the dashboard.
|
||||||
|
A patched client can still lie; what it cannot do is lie without the
|
||||||
|
tournament organizer having a record of it.
|
||||||
|
|
||||||
|
Client-side attestation is deliberately not built. This is an
|
||||||
|
open-source Lua game: it would be theater, and it would break honest
|
||||||
|
mods.
|
||||||
|
|
||||||
|
**The relay has no TLS.** Port 7778 is plaintext, so party contents,
|
||||||
|
trades and trainer names are visible to anyone on the network path. There
|
||||||
|
is nothing secret in a Pokemon party, but it is a real property of the
|
||||||
|
system and not an oversight. Fixing it means a TLS terminator in front of
|
||||||
|
the relay and a client that speaks it, which is a version break for every
|
||||||
|
shipped build.
|
||||||
|
|
||||||
|
**The dashboard has no default password.** `DASHBOARD_PASSWORD` is
|
||||||
|
required; with it unset the relay runs and the dashboard simply does not
|
||||||
|
start. It is still Basic Auth over plain HTTP, so it belongs behind an
|
||||||
|
IP restriction or an SSH tunnel (`pokeserver/DEPLOY.md`).
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
luajit tests/link_hostile.lua every message type x every wrong type
|
||||||
|
luajit tests/link_desync_fuzz.lua lockstep fuzz, plus a mutation mode
|
||||||
|
luajit tests/run_link_tests.lua both of the above, plus the rest
|
||||||
|
cd ../pokeserver && npm test relay smoke, 16-player bracket, hostile
|
||||||
|
|
||||||
|
`tests/link_hostile.lua` builds its corpus from a template per message
|
||||||
|
type, replaces each field (and several nested ones) with every wrong Lua
|
||||||
|
type, and drives the survivors through the real trade session, a real
|
||||||
|
lockstep battle, a real spectator battle, and the tournament screen
|
||||||
|
**including its draw** -- because the two nastiest payloads are
|
||||||
|
delayed-fuse ones that crash on render rather than on receipt.
|
||||||
@@ -105,8 +105,9 @@ trixie.
|
|||||||
This is a statement about the *compile environment*, not about where the
|
This is a statement about the *compile environment*, not about where the
|
||||||
artifact runs — building on your own newer distro would silently raise that
|
artifact runs — building on your own newer distro would silently raise that
|
||||||
floor and strand every user on an older one, with no symptom until they
|
floor and strand every user on an older one, with no symptom until they
|
||||||
download it. CI enforces the floor: `linux-arm64-build` fails if the highest
|
download it. `scripts/linux-arm64/verify_appimage.sh` enforces the floor in
|
||||||
required glibc symbol version climbs above 2.31.
|
both CI (`linux-arm64-build`) and the release workflow: the build fails if
|
||||||
|
the highest required glibc symbol version climbs above 2.31.
|
||||||
|
|
||||||
### Why five libraries are built from source
|
### Why five libraries are built from source
|
||||||
|
|
||||||
@@ -172,13 +173,15 @@ Three jobs, path-gated on `scripts/build_linux_arm64.sh`,
|
|||||||
exclude list still classifies known sonames correctly, that AppRun still
|
exclude list still classifies known sonames correctly, that AppRun still
|
||||||
launches `game.love` with `--fused`, and that the host-arch guard actually
|
launches `game.love` with `--fused`, and that the host-arch guard actually
|
||||||
fires. Needs no container and no arm64 machine.
|
fires. Needs no container and no arm64 machine.
|
||||||
- **`linux-arm64-build`** (`ubuntu-24.04-arm`) — the real build, then extracts
|
- **`linux-arm64-build`** (`ubuntu-24.04-arm`) — the real build, then
|
||||||
the artifact and asserts the layout, that every bundled object resolves
|
`scripts/linux-arm64/verify_appimage.sh` extracts the artifact and asserts
|
||||||
under AppRun's `LD_LIBRARY_PATH`, and that the glibc floor is still ≤ 2.31.
|
the layout, that every bundled object resolves under AppRun's
|
||||||
Uploads the AppImage for 7 days.
|
`LD_LIBRARY_PATH`, and that the glibc floor is still ≤ 2.31. Uploads the
|
||||||
|
AppImage for 7 days.
|
||||||
- **release** — `linux-arm64` runs on `ubuntu-24.04-arm`, reuses the shared
|
- **release** — `linux-arm64` runs on `ubuntu-24.04-arm`, reuses the shared
|
||||||
`game.love` from the `love-payload` job, and the AppImage is staged and
|
`game.love` from the `love-payload` job, runs the same
|
||||||
published like every other release asset.
|
`verify_appimage.sh` checks on the shipped image, and the AppImage is
|
||||||
|
staged and published like every other release asset.
|
||||||
|
|
||||||
Unlike the Switch job, none of this needs secrets or self-hosted hardware, so
|
Unlike the Switch job, none of this needs secrets or self-hosted hardware, so
|
||||||
it runs on fork PRs too.
|
it runs on fork PRs too.
|
||||||
|
|||||||
@@ -55,9 +55,10 @@ The short version, for an author deciding what to write:
|
|||||||
```
|
```
|
||||||
|
|
||||||
`games` is an optional array of version ids (`"red"`, `"blue"`, `"yellow"`,
|
`games` is an optional array of version ids (`"red"`, `"blue"`, `"yellow"`,
|
||||||
`"gold"`), generations (`"gen1"`, `"gen2"`, case-insensitive) or `"all"`.
|
`"gold"`, `"silver"`), generations (`"gen1"`, `"gen2"`, case-insensitive) or
|
||||||
`src/mods/ModTargets.lua` resolves the tokens off `GameVersion.ORDER` and
|
`"all"`. `src/mods/ModTargets.lua` resolves the tokens off `GameVersion.ORDER`
|
||||||
`GameVersion.generation`, so nothing anywhere restates the game list.
|
and `GameVersion.generation`, so nothing anywhere restates the game list.
|
||||||
|
`"gen2"` now expands to both Gold and Silver.
|
||||||
`Manifest.validate` stores the resolved, ORDER-sorted ids on `manifest.games`
|
`Manifest.validate` stores the resolved, ORDER-sorted ids on `manifest.games`
|
||||||
and **derives** `manifest.gen2compat` from them, which is the one field the
|
and **derives** `manifest.gen2compat` from them, which is the one field the
|
||||||
loader's gate reads.
|
loader's gate reads.
|
||||||
@@ -512,8 +513,9 @@ gains a field instead of the name gaining a prefix.
|
|||||||
id under Gen 1's `name` key, which is the one payload difference the
|
id under Gen 1's `name` key, which is the one payload difference the
|
||||||
numeric flag space forces.
|
numeric flag space forces.
|
||||||
- *Menus (`src/ui/gen2/`):* `ui.start_menu.items`, `ui.title_menu.items`,
|
- *Menus (`src/ui/gen2/`):* `ui.start_menu.items`, `ui.title_menu.items`,
|
||||||
`ui.options.rows`, `ui.party.submenu`, `ui.naming.grid`, `ui.pc.items`,
|
`ui.options.rows`, `ui.party.submenu`, `ui.party.grid_navigation`,
|
||||||
`ui.list_menu`, `transition.style`. `ui.list_menu` covers Gold's script
|
`ui.naming.grid`, `ui.pc.items`, `ui.list_menu`, `transition.style`.
|
||||||
|
`ui.list_menu` covers Gold's script
|
||||||
menus (`ScriptMenu.lua`); the `Chrome.List` widget the START and title
|
menus (`ScriptMenu.lua`); the `Chrome.List` widget the START and title
|
||||||
menus draw with does not raise it yet, so those two are composed through
|
menus draw with does not raise it yet, so those two are composed through
|
||||||
their own hooks only.
|
their own hooks only.
|
||||||
@@ -540,8 +542,9 @@ gains a field instead of the name gaining a prefix.
|
|||||||
`battle.crit`, `battle.accuracy`, `battle.turn_order`,
|
`battle.crit`, `battle.accuracy`, `battle.turn_order`,
|
||||||
`battle.enemy_action`, `battle.run`, `battle.exp_award`, `exp.gain`,
|
`battle.enemy_action`, `battle.run`, `battle.exp_award`, `exp.gain`,
|
||||||
`catch.rate`, `trainer.party`, `battle.overlay`, `battle.low_health_alarm`,
|
`catch.rate`, `trainer.party`, `battle.overlay`, `battle.low_health_alarm`,
|
||||||
`battle.catch_exp`, `battle.bottom_ui_visible` and
|
`battle.catch_exp`, `battle.bottom_ui_visible`,
|
||||||
`battle.status_hud_visible`. One payload difference: Gen 1's vanilla
|
`battle.status_hud_visible` and `battle.move_grid_navigation`. One payload
|
||||||
|
difference: Gen 1's vanilla
|
||||||
`battle.low_health_alarm` link reads `ctx.battle.data`, and Gold's battle
|
`battle.low_health_alarm` link reads `ctx.battle.data`, and Gold's battle
|
||||||
screen has no `.data` field, so the Gen 2 site **adds** `ctx.data` beside the
|
screen has no `.data` field, so the Gen 2 site **adds** `ctx.data` beside the
|
||||||
Gen 1 keys. A mod that calls `nextFn` is unaffected; one that reaches through
|
Gen 1 keys. A mod that calls `nextFn` is unaffected; one that reaches through
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ Every mod contains a root `manifest.json` defining its metadata, supported games
|
|||||||
| `entry` | `string` | Entry Lua file path relative to mod root (usually `"main.lua"`). |
|
| `entry` | `string` | Entry Lua file path relative to mod root (usually `"main.lua"`). |
|
||||||
| `profile` | `string` | Mod profile: `"content"`, `"overhaul"`, or `"total_conversion"`. |
|
| `profile` | `string` | Mod profile: `"content"`, `"overhaul"`, or `"total_conversion"`. |
|
||||||
| `category` | `string` | Categorization chip (e.g. `"GAMEPLAY"`, `"CONTENT"`, `"UI"`, `"AUDIO"`). |
|
| `category` | `string` | Categorization chip (e.g. `"GAMEPLAY"`, `"CONTENT"`, `"UI"`, `"AUDIO"`). |
|
||||||
| `games` | `array` | Supported game versions: `["gen1"]`, `["gen2"]`, `["red"]`, `["blue"]`, `["yellow"]`, `["gold"]`, or `["all"]`. |
|
| `games` | `array` | Supported game versions: `["gen1"]`, `["gen2"]`, `["red"]`, `["blue"]`, `["yellow"]`, `["gold"]`, `["silver"]`, or `["all"]`. |
|
||||||
| `game_version`| `string` | Semver range of required engine version (e.g. `">=0.0.0-dev <2.0.0"`). |
|
| `game_version`| `string` | Semver range of required engine version (e.g. `">=0.0.0-dev <2.0.0"`). |
|
||||||
| `priority` | `integer` | Load priority order (lower numbers load earlier; dependencies always precede dependents regardless of priority). |
|
| `priority` | `integer` | Load priority order (lower numbers load earlier; dependencies always precede dependents regardless of priority). |
|
||||||
| `dependencies` | `array` | Hard required dependencies. A mod will not load if a required dependency is missing or disabled for the active game. |
|
| `dependencies` | `array` | Hard required dependencies. A mod will not load if a required dependency is missing or disabled for the active game. |
|
||||||
@@ -268,10 +268,10 @@ an optional stock `catchChance` percentage.
|
|||||||
`prompt` describes the currently visible choice (`menu`, `moves`, `party`,
|
`prompt` describes the currently visible choice (`menu`, `moves`, `party`,
|
||||||
`advance`, `safari`, or `mimic`) and is `locked` when another screen or battle
|
`advance`, `safari`, or `mimic`) and is `locked` when another screen or battle
|
||||||
phase owns input. Generation-specific features remain optional: Gen 1 includes
|
phase owns input. Generation-specific features remain optional: Gen 1 includes
|
||||||
battle medicine, balls, catch previews, Safari balls, and Mimic choices;
|
battle medicine, balls, catch previews, Safari balls, and Mimic choices. Gold
|
||||||
Gold currently returns an empty `items` list rather than guessing at its
|
exposes balls and their exact stock catch previews; targeted medicine remains
|
||||||
pocketed PACK flow. Callers should ignore unknown fields and tolerate absent
|
screen-owned and is omitted rather than guessing at its pocketed PACK flow.
|
||||||
optional ones.
|
Callers should ignore unknown fields and tolerate absent optional ones.
|
||||||
|
|
||||||
## Battle menu intents
|
## Battle menu intents
|
||||||
|
|
||||||
|
|||||||
@@ -11,10 +11,13 @@ Features intentionally added beyond the original Pokémon Red, Blue, and Yellow
|
|||||||
* **Persistent custom options** stored separately from game saves
|
* **Persistent custom options** stored separately from game saves
|
||||||
* **Optional widescreen battle layout**
|
* **Optional widescreen battle layout**
|
||||||
* **Mobile touch controls** with editable layouts, vibration, and orientation settings
|
* **Mobile touch controls** with editable layouts, vibration, and orientation settings
|
||||||
* **Touch skins** in RetroArch overlay format, with bezel art, per-button press states, and Super Game Boy borders
|
* **Screen position setting** (center, upper, top) shared across all games, for clamp-on controllers that cover the lower screen
|
||||||
|
* **Touch skins** in RetroArch overlay format and Delta `.deltaskin` (including PDF-wrapped bezel art), with per-button press states and Super Game Boy borders
|
||||||
* **Pokédex diploma and printer image exports**
|
* **Pokédex diploma and printer image exports**
|
||||||
|
* **Shareable mod lists** over save sync, optionally carrying the options set for those mods, which the receiving device is asked about before anything is changed
|
||||||
|
|
||||||
## Gen 2 Specifics
|
## Gen 2 Specifics
|
||||||
|
|
||||||
|
* **Pokémon Silver** as an importable, launcher-selectable version alongside Gold
|
||||||
* **Mod manager** with Gen 1 mod adapters, per-game targeting, and `modkit gen2check`
|
* **Mod manager** with Gen 1 mod adapters, per-game targeting, and `modkit gen2check`
|
||||||
* **Followers** for mods, plus Gen 2-only registries and hooks
|
* **Followers** for mods, plus Gen 2-only registries and hooks
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ something the filesystem encodes.
|
|||||||
|
|
||||||
| token | means |
|
| token | means |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `"red"`, `"blue"`, `"yellow"`, `"gold"` | that one game (a version id from `GameVersion.ORDER`) |
|
| `"red"`, `"blue"`, `"yellow"`, `"gold"`, `"silver"` | that one game (a version id from `GameVersion.ORDER`) |
|
||||||
| `"gen1"`, `"gen2"` | every game of that generation (case-insensitive; `"gen 2"` also parses) |
|
| `"gen1"`, `"gen2"` | every game of that generation (case-insensitive; `"gen 2"` also parses) |
|
||||||
| `"all"` | every game this engine has |
|
| `"all"` | every game this engine has |
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
# What This Port Requires
|
# What This Port Requires
|
||||||
|
|
||||||
The packaged desktop app requires one user-supplied input on first boot: a
|
The packaged desktop app requires one user-supplied input on first boot: a
|
||||||
canonical 1 MiB US Pokemon Red, Blue, or Yellow ROM.
|
canonical 1 MiB US Pokemon Red, Blue, or Yellow ROM, or a canonical 2 MiB US
|
||||||
|
Pokemon Gold or Silver ROM.
|
||||||
|
|
||||||
The importer verifies the SHA-1 for the game (see `src/core/GameVersion.lua`
|
The importer verifies the SHA-1 for the game (see `src/core/GameVersion.lua`
|
||||||
for specific hashes). Other revisions and Virtual Console releases are rejected
|
for specific hashes). Other revisions and Virtual Console releases are rejected
|
||||||
@@ -15,7 +16,8 @@ Python and Pillow are not required by the packaged app.
|
|||||||
|
|
||||||
Assembly removes high-level names and some relationships that the Lua port
|
Assembly removes high-level names and some relationships that the Lua port
|
||||||
needs. The version-specific files `tools/rom_manifest.json`,
|
needs. The version-specific files `tools/rom_manifest.json`,
|
||||||
`tools/rom_manifest_blue.json`, and `tools/rom_manifest_yellow.json` therefore
|
`tools/rom_manifest_blue.json`, `tools/rom_manifest_yellow.json`,
|
||||||
|
`tools/rom_manifest_gold.json`, and `tools/rom_manifest_silver.json` therefore
|
||||||
contain:
|
contain:
|
||||||
|
|
||||||
- the ROM symbol addresses actually read by the extractor
|
- the ROM symbol addresses actually read by the extractor
|
||||||
|
|||||||
@@ -4,16 +4,23 @@ A **skin** replaces the on-screen controls wholesale: a bezel image, a
|
|||||||
control layout, and the rectangle the Game Boy screen is drawn into. Engine:
|
control layout, and the rectangle the Game Boy screen is drawn into. Engine:
|
||||||
`src/core/TouchSkin.lua` (model, parsers, zip export), `src/core/TouchControls.lua`
|
`src/core/TouchSkin.lua` (model, parsers, zip export), `src/core/TouchControls.lua`
|
||||||
(draw and input), `src/render/Renderer.lua` (the screen viewport),
|
(draw and input), `src/render/Renderer.lua` (the screen viewport),
|
||||||
|
`src/core/DeltaSkin.lua` (Delta `.deltaskin` import and export),
|
||||||
`src/ui/SkinStudio.lua` (the desktop editor). Tests:
|
`src/ui/SkinStudio.lua` (the desktop editor). Tests:
|
||||||
`tests/engine/touch_skin_test.lua`, `tests/engine/skin_studio_test.lua`,
|
`tests/engine/touch_skin_test.lua`, `tests/engine/skin_studio_test.lua`,
|
||||||
`tests/engine/launcher_skins_tab.lua`.
|
`tests/engine/skin_studio_ux.lua`,
|
||||||
|
`tests/engine/skin_studio_image_import.lua`,
|
||||||
|
`tests/engine/skin_format_import_test.lua`,
|
||||||
|
`tests/engine/launcher_skins_tab.lua`,
|
||||||
|
`tests/engine/launcher_skins_ux.lua`.
|
||||||
|
|
||||||
Skins are picked in the launcher's **Skins** tab, which also imports them and
|
Skins are picked in the launcher's **Skins** tab, which also imports them and
|
||||||
opens the studio. `options.touchControls.skin` holds the folder name.
|
opens the studio. `options.touchControls.skin` holds the folder name.
|
||||||
|
|
||||||
## Formats
|
## Formats
|
||||||
|
|
||||||
Two load. `skin.lua` wins when a folder has both.
|
Three load: the native `skin.lua`, a RetroArch overlay `.cfg`, and a Delta
|
||||||
|
`.deltaskin`. `skin.lua` wins when a folder has more than one. The launcher
|
||||||
|
badges each installed skin with the format it was read from.
|
||||||
|
|
||||||
**RetroArch overlay `.cfg`.** The libretro `common-overlays` collection loads
|
**RetroArch overlay `.cfg`.** The libretro `common-overlays` collection loads
|
||||||
as-is. Supported keys:
|
as-is. Supported keys:
|
||||||
@@ -25,7 +32,7 @@ as-is. Supported keys:
|
|||||||
| `overlayN_overlay` | bezel image |
|
| `overlayN_overlay` | bezel image |
|
||||||
| `overlayN_full_screen` | stretch the page to the window |
|
| `overlayN_full_screen` | stretch the page to the window |
|
||||||
| `overlayN_rect` | page placement, default `0,0,1,1` |
|
| `overlayN_rect` | page placement, default `0,0,1,1` |
|
||||||
| `overlayN_aspect_ratio` | fallback aspect when not full screen |
|
| `overlayN_aspect_ratio` | design aspect; the overlay letterboxes to it even when full screen |
|
||||||
| `overlayN_range_mod`, `overlayN_alpha_mod` | desc defaults |
|
| `overlayN_range_mod`, `overlayN_alpha_mod` | desc defaults |
|
||||||
| `overlayN_viewport` | `x,y,w,h`, the screen cutout |
|
| `overlayN_viewport` | `x,y,w,h`, the screen cutout |
|
||||||
| `overlayN_viewport_fill` | parsed; the engine always fits, see below |
|
| `overlayN_viewport_fill` | parsed; the engine always fits, see below |
|
||||||
@@ -40,6 +47,15 @@ Hitboxes are `radial` or `rect`. Pipe-separated binds (`left|down`) are one
|
|||||||
control that holds both. A `nul` desc is decoration: it draws and never
|
control that holds both. A `nul` desc is decoration: it draws and never
|
||||||
captures a touch.
|
captures a touch.
|
||||||
|
|
||||||
|
The area desc types are expanded rather than ignored: `dpad_area`,
|
||||||
|
`abxy_area`, `analog_left` and `analog_right` each become eight hitboxes over
|
||||||
|
the same area, one per 45 degree sector measured from its centre, the way
|
||||||
|
RetroArch resolves them: there is no neutral middle, and the four corner
|
||||||
|
sectors fire two inputs. Any `_up` / `_down` / `_left` / `_right` override and
|
||||||
|
the per-side reach are honoured, and the desc's own art is kept as decoration
|
||||||
|
over the top. Exporting a cfg folds the eight back into the one area desc they
|
||||||
|
came from. `retrok_<key>` is a keyboard bind.
|
||||||
|
|
||||||
Alpha follows RetroArch (`input_driver.c`, `input_overlay_post_poll`): every
|
Alpha follows RetroArch (`input_driver.c`, `input_overlay_post_poll`): every
|
||||||
image sits at the overlay opacity, and a pressed control's image swaps to
|
image sits at the overlay opacity, and a pressed control's image swaps to
|
||||||
`opacity * alpha_mod`. So `alpha_mod` above 1 lights a control up and below 1
|
`opacity * alpha_mod`. So `alpha_mod` above 1 lights a control up and below 1
|
||||||
@@ -70,6 +86,32 @@ return {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Delta `.deltaskin`.** A zip (any wrapping folder is stripped) holding an
|
||||||
|
`info.json` plus its art. The `representations` tree is walked
|
||||||
|
device / display type / orientation, and every orientation that exists becomes
|
||||||
|
a page; `page.orient` is the orientation key, so a portrait/landscape pair
|
||||||
|
auto-rotates like a RetroArch one. Item `frame` rects are top-left plus size in
|
||||||
|
`mappingSize` points and are converted to the native centre plus half extent;
|
||||||
|
`extendedEdges` merge per key into the reach fields; `mask: "circle"` becomes a
|
||||||
|
radial hitbox. A `dpad` or `thumbstick` item expands into the 3x3 grid, so the
|
||||||
|
corners fire two directions. `screens[1].outputFrame` (or the legacy
|
||||||
|
`gameScreenFrame`) becomes the screen cutout. A portrait page with neither
|
||||||
|
keeps `mappingSize` as the overlay aspect, sits at the bottom of the
|
||||||
|
window, and puts the Game Boy picture in the leftover space above -- the
|
||||||
|
usual GBA4iOS controller-deck layout. Pages that name a screen rect still
|
||||||
|
stretch to the window the way Delta does. Host functions map to
|
||||||
|
engine hotkeys: `menu` to `menu_toggle`, `fastForward` to
|
||||||
|
`hold_fast_forward`, `toggleFastForward` to `toggle_fast_forward`;
|
||||||
|
`quickSave` and `quickLoad` have nothing to bind to and drop to decoration.
|
||||||
|
Both `com.rileytestut.delta.game.*` and Manic's `public.aoshuang.game.*`
|
||||||
|
identifiers are accepted, and a non Game Boy system warns instead of failing.
|
||||||
|
|
||||||
|
PDF artwork is usually a JPEG wrapped so iOS can scale it (Delta's
|
||||||
|
Image-to-PDF skins, Preview exports, and the like). Import extracts that
|
||||||
|
JPEG and draws it; a true vector PDF with no embedded image is still refused,
|
||||||
|
with a message asking for a PNG version. GBA4iOS `.gbcskin` / `.gbaskin` files
|
||||||
|
are an older, incompatible schema and are refused by name.
|
||||||
|
|
||||||
## Bindable actions
|
## Bindable actions
|
||||||
|
|
||||||
The eight Game Boy buttons: `a`, `b`, `start`, `select`, `up`, `down`,
|
The eight Game Boy buttons: `a`, `b`, `start`, `select`, `up`, `down`,
|
||||||
@@ -118,10 +160,22 @@ them. Anything that binds a button still follows the usual mobile /
|
|||||||
|
|
||||||
## Installing
|
## Installing
|
||||||
|
|
||||||
Drop a folder or a `.zip` into `skins/` in the save directory, or drop a zip on
|
Four roads, all of them landing in `skins/` in the save directory:
|
||||||
the launcher window while the Skins tab is open. A zip is mounted in place, so
|
|
||||||
there is nothing to unpack. The folder needs one `skin.lua` or `.cfg`
|
* **Import** on the Skins tab opens the host file picker for a `.zip` or a
|
||||||
(`overlay.cfg` is preferred when there are several) and the images it names.
|
`.deltaskin`.
|
||||||
|
* **Paste a skin link** in the tab's URL row, then **Add**. The download runs
|
||||||
|
on the fetch pool (`src/net/Fetch.lua`), so the launcher stays live, and the
|
||||||
|
row shows a spinner until it lands. A link to a bare `overlay.cfg` is wrapped
|
||||||
|
into an archive on the way in. This is the road that works on a phone, where
|
||||||
|
there is no file picker to speak of.
|
||||||
|
* Drop a `.zip` or `.deltaskin` on the launcher window while the Skins tab is
|
||||||
|
open.
|
||||||
|
* Copy a folder or archive into `skins/` by hand.
|
||||||
|
|
||||||
|
An archive is mounted in place, so there is nothing to unpack. It needs one
|
||||||
|
`skin.lua`, `.cfg` (`overlay.cfg` is preferred when there are several) or
|
||||||
|
`info.json`, plus the images it names.
|
||||||
|
|
||||||
Two ship bundled, both from libretro's `common-overlays` under CC-BY-4.0:
|
Two ship bundled, both from libretro's `common-overlays` under CC-BY-4.0:
|
||||||
|
|
||||||
@@ -156,24 +210,69 @@ The Super Game Boy preset locks the viewport to the real screen window,
|
|||||||
160x144 at (48,40), so an SGB border cannot be drawn out of register.
|
160x144 at (48,40), so an SGB border cannot be drawn out of register.
|
||||||
|
|
||||||
**Editing.** Click a control to select it, drag to move, eight handles to
|
**Editing.** Click a control to select it, drag to move, eight handles to
|
||||||
resize. X / Y / W / H are in canvas pixels, so a control can be typed to the
|
resize. Arrow keys nudge the selection one canvas pixel, shift-arrow ten. While
|
||||||
coordinate its art was drawn at. Bind, hitbox shape, hit reach and idle and
|
a control is dragged it snaps to the centres and edges of the other controls
|
||||||
|
and of the page itself when it comes within a few pixels, and the guide it
|
||||||
|
snapped to is drawn. X / Y / W / H are in canvas pixels, so a control can be
|
||||||
|
typed to the coordinate its art was drawn at. **Back** and **Front** move the
|
||||||
|
selection through the draw order. Bind, hitbox shape, hit reach and idle and
|
||||||
pressed images are per control; the bezel, the pages and the screen cutout are
|
pressed images are per control; the bezel, the pages and the screen cutout are
|
||||||
per page. The cutout is itself a draggable element with a 10:9 lock. Drop a PNG
|
per page. The cutout is itself a draggable element with a 10:9 lock.
|
||||||
or JPG on the window to import art into the skin.
|
|
||||||
|
**Bind** opens a grid of every bind the engine understands: the eight Game Boy
|
||||||
|
buttons, the diagonal pairs, every hotkey, a few `key:` entries, and
|
||||||
|
decoration. The COMBINE chips at the top toggle one part at a time, which is
|
||||||
|
how a pipe bind like `left|down` is built without typing it.
|
||||||
|
|
||||||
|
**Undo** and **Redo** in the top bar cover every edit (ctrl+Z / ctrl+Y, or
|
||||||
|
`u` / shift+`u` without a keyboard modifier). The stack holds the last 50
|
||||||
|
actions. `L` toggles the bind captions drawn on the canvas.
|
||||||
|
|
||||||
|
Each page can **Lock** to portrait or landscape. With **Match canvas** on
|
||||||
|
(the default), the page list picks a matching mock device and the canvas preset
|
||||||
|
picks a matching page. Turn Match canvas off to look at a portrait page on a
|
||||||
|
landscape device. **Pages** opens the page list, where a page is selected,
|
||||||
|
renamed or deleted.
|
||||||
|
|
||||||
|
Starting a new skin, opening another one or closing the studio with unsaved
|
||||||
|
edits prompts first, with Save first / Discard / Cancel.
|
||||||
|
|
||||||
|
A RetroArch overlay whose pages are already named portrait / landscape
|
||||||
|
(the auto-rotate convention) locks those pages and turns Match canvas on
|
||||||
|
when you open it. You do not have to click Lock first.
|
||||||
|
|
||||||
|
**Art.** The **Bezel**, **Idle art** and **Pressed art** rows open a
|
||||||
|
thumbnail grid of the images already in the skin folder, with `(none)` first;
|
||||||
|
the **Import** button there and beside each row opens the host file picker (`src/core/FilePicker.lua`: osascript, PowerShell,
|
||||||
|
zenity/kdialog) and copies the chosen PNG or JPG into `img/` under the name in
|
||||||
|
the SKIN field, then assigns it to that slot. Dropping a PNG or JPG on the
|
||||||
|
window does the same for whichever slot was last touched. A new bezel does not
|
||||||
|
move the screen cutout: press **Detect screen from bezel** to measure it out of
|
||||||
|
the art's alpha.
|
||||||
|
|
||||||
**Testing.** **Test** makes the canvas live: clicking presses real Game Boy
|
**Testing.** **Test** makes the canvas live: clicking presses real Game Boy
|
||||||
buttons and the footer reports what is held. **Play** saves the skin, selects
|
buttons and the footer reports what is held. **Play** saves the skin, selects
|
||||||
it, and boots the game with it.
|
it, and boots the game with it.
|
||||||
|
|
||||||
**Saving.** **Save** writes `skins/<name>/skin.lua` and copies every image the
|
**Saving.** **Save** writes `skins/<name>/skin.lua` and copies every image the
|
||||||
skin names, so the folder stands alone. **Export** packs it as one zip
|
skin names, so the folder stands alone. **Export** offers three formats, and
|
||||||
(`src/core/SkinZip.lua`, store-only) carrying the native `skin.lua`, the
|
the Skins tab's gear offers the same three for any installed skin:
|
||||||
images, and the original `.cfg` when it came from one. An exported skin drops
|
|
||||||
straight back into `skins/` and still opens in RetroArch.
|
| Export | Contents |
|
||||||
|
| --- | --- |
|
||||||
|
| gen1recomp `.zip` | the native `skin.lua`, the images, and the original `.cfg` when it came from one |
|
||||||
|
| RetroArch `.zip` | an `overlay.cfg` generated from the model, plus the images |
|
||||||
|
| Delta `.deltaskin` | an `info.json` generated from the model, plus the images |
|
||||||
|
|
||||||
|
All three are written store-only (`src/core/SkinZip.lua`) into `skins/_export/`
|
||||||
|
in the save directory, which is outside the folder the skin list scans, so an
|
||||||
|
export can never shadow the skin it came from. The notice names the full path
|
||||||
|
so a phone can find the file in its own file manager. On desktop **Show the
|
||||||
|
exported file** opens that folder.
|
||||||
|
|
||||||
## Not implemented
|
## Not implemented
|
||||||
|
|
||||||
RetroArch's `analog_*`, `dpad_area`, `abxy_area` and `retrok_*` desc types.
|
True vector Delta skins (PDF artwork with no embedded JPEG). Those still need
|
||||||
Image assignment cycles through art already in the skin folder; there is no
|
a PDF renderer this engine does not carry, so they are refused with a message
|
||||||
file browser, so new art arrives by drag and drop.
|
rather than imported half-drawn. PDF files that wrap a JPEG, the usual Delta
|
||||||
|
skin case, extract on import.
|
||||||
|
|||||||
@@ -169,6 +169,7 @@ the NX runtime modules `src/core/NxAssetOverlay.lua`, `src/core/Platform.lua`,
|
|||||||
`tests/engine/assets_version_fallback_test.lua`,
|
`tests/engine/assets_version_fallback_test.lua`,
|
||||||
`tests/engine/nx_generated_guard_test.lua`,
|
`tests/engine/nx_generated_guard_test.lua`,
|
||||||
`tests/engine/nx_yellow_boot_test.lua`,
|
`tests/engine/nx_yellow_boot_test.lua`,
|
||||||
|
`tests/engine/cache_fs_gold_nx_load_test.lua`,
|
||||||
`tests/engine/switch_diagnostics_test.lua`, `tests/engine/platform_nx_*`,
|
`tests/engine/switch_diagnostics_test.lua`, `tests/engine/platform_nx_*`,
|
||||||
or the Switch-related workflow YAML), CI runs:
|
or the Switch-related workflow YAML), CI runs:
|
||||||
|
|
||||||
@@ -179,7 +180,8 @@ or the Switch-related workflow YAML), CI runs:
|
|||||||
`luajit tests/switch_transfer_docs_test.lua`, and the NX engine suites
|
`luajit tests/switch_transfer_docs_test.lua`, and the NX engine suites
|
||||||
headlessly (`luajit tests/engine/assets_version_fallback_test.lua`,
|
headlessly (`luajit tests/engine/assets_version_fallback_test.lua`,
|
||||||
`luajit tests/engine/nx_generated_guard_test.lua`,
|
`luajit tests/engine/nx_generated_guard_test.lua`,
|
||||||
`luajit tests/engine/nx_yellow_boot_test.lua`).
|
`luajit tests/engine/nx_yellow_boot_test.lua`,
|
||||||
|
`luajit tests/engine/cache_fs_gold_nx_load_test.lua`).
|
||||||
2. **Fused NRO build** only on the **main** repository
|
2. **Fused NRO build** only on the **main** repository
|
||||||
(`bryanthaboi/gen1recomp`), on the self-hosted Mac runner
|
(`bryanthaboi/gen1recomp`), on the self-hosted Mac runner
|
||||||
(`scripts/build_switch.sh --fetch --fused`), and only when the workflow
|
(`scripts/build_switch.sh --fetch --fused`), and only when the workflow
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
Every GitHub Release that includes Switch support ships an SD-ready zip:
|
Every GitHub Release that includes Switch support ships an SD-ready zip:
|
||||||
`gen1recomp-*-switch.zip`. Extract it at the root of your microSD (install
|
`gen1recomp-*-switch.zip`. Extract it at the root of your microSD (install
|
||||||
or update, same steps), launch with **title override**, then import your
|
or update, same steps), launch with **title override**, then import your
|
||||||
own legal `.gb` ROM.
|
own legal `.gb` / `.gbc` ROM.
|
||||||
|
|
||||||
> You need a console that can run Switch homebrew (custom firmware / hbmenu).
|
> You need a console that can run Switch homebrew (custom firmware / hbmenu).
|
||||||
> This project does not help you set that up.
|
> This project does not help you set that up.
|
||||||
@@ -91,13 +91,14 @@ Do **not** launch from the Album applet path for normal play.
|
|||||||
|
|
||||||
This project ships **no** game data. On first launch:
|
This project ships **no** game data. On first launch:
|
||||||
|
|
||||||
1. Put your own legally obtained Pokémon Red, Blue (`.gb`), or Yellow
|
1. Put your own legally obtained Pokémon Red, Blue (`.gb`), Yellow, Gold, or
|
||||||
(`.gbc`) dump into `switch/gen1recomp/pokemon-love2d/imports/` (the
|
Silver (`.gbc`) dump into `switch/gen1recomp/pokemon-love2d/imports/` (the
|
||||||
launcher also shows the live save-dir path). All three can sit in the
|
launcher also shows the live save-dir path). All five can sit in the
|
||||||
same folder.
|
same folder.
|
||||||
2. Use **Scan again** on that game's tab (Red / Blue / Yellow). Rescan
|
2. Use **Scan again** on that game's tab (Red / Blue / Yellow / Gold /
|
||||||
matches by ROM SHA-1 for the open tab only. A Red dump never imports
|
Silver). Rescan matches by ROM SHA-1 for the open tab only. A Red dump
|
||||||
from the Yellow tab (and vice versa).
|
never imports from the Yellow tab (and vice versa). Gold and Silver are
|
||||||
|
Beta in the launcher; a clean US dump of either is enough to Play.
|
||||||
|
|
||||||
## 5. Import / Export a raw `.sav`
|
## 5. Import / Export a raw `.sav`
|
||||||
|
|
||||||
@@ -109,10 +110,15 @@ SD / FTP, same transfer methods as ROMs. Paths are **per game**:
|
|||||||
| Red | `imports/saves/red/` | `exports/red/` |
|
| Red | `imports/saves/red/` | `exports/red/` |
|
||||||
| Blue | `imports/saves/blue/` | `exports/blue/` |
|
| Blue | `imports/saves/blue/` | `exports/blue/` |
|
||||||
| Yellow | `imports/saves/yellow/` | `exports/yellow/` |
|
| Yellow | `imports/saves/yellow/` | `exports/yellow/` |
|
||||||
|
| Gold | `imports/saves/gold/` | `exports/gold/` |
|
||||||
|
| Silver | `imports/saves/silver/` | `exports/silver/` |
|
||||||
|
|
||||||
(Under the save dir `pokemon-love2d/`. The zip already creates these folders.)
|
(Under the save dir `pokemon-love2d/`. The zip already creates these folders.
|
||||||
|
Gold and Silver cart `.sav` import/export is not supported yet -- the folders
|
||||||
|
exist so MTP browsing matches the other games. Gold and Silver progress still
|
||||||
|
saves in-engine.)
|
||||||
|
|
||||||
1. Copy a Gen1 `.sav` (32 KB) into that game's inbox under the save dir
|
1. Copy a Gen 1 `.sav` (32 KB) into that game's inbox under the save dir
|
||||||
([switch-transfer.md](switch-transfer.md)).
|
([switch-transfer.md](switch-transfer.md)).
|
||||||
2. With the game's ROM already imported, open **that game's tab** →
|
2. With the game's ROM already imported, open **that game's tab** →
|
||||||
**SAVE FILES** → **Import save**. Only that folder is scanned.
|
**SAVE FILES** → **Import save**. Only that folder is scanned.
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ Player install (what to download, title override) stays in
|
|||||||
| Loose iteration pair | `sdmc:/switch/gen1recomp/gen1recomp.nro` **and** `game.love` beside it |
|
| Loose iteration pair | `sdmc:/switch/gen1recomp/gen1recomp.nro` **and** `game.love` beside it |
|
||||||
| ROM inbox | LÖVE save dir → `imports/` (launcher shows the live `getSaveDirectory()` path; under MTP often `1: SD Card/<save identity>/imports/`) |
|
| ROM inbox | LÖVE save dir → `imports/` (launcher shows the live `getSaveDirectory()` path; under MTP often `1: SD Card/<save identity>/imports/`) |
|
||||||
| Mod zip inbox | Same save dir → `imports/mods/` then MODS → **Scan again** |
|
| Mod zip inbox | Same save dir → `imports/mods/` then MODS → **Scan again** |
|
||||||
| Save `.sav` inbox | Same save dir → `imports/saves/red\|blue\|yellow/` then that game's SAVE FILES → **Import save** |
|
| Save `.sav` inbox | Same save dir → `imports/saves/red\|blue\|yellow\|gold\|silver/` then that game's SAVE FILES → **Import save** (Gold / Silver cart `.sav` not supported yet) |
|
||||||
| Save exports | Same save dir → `exports/red\|blue\|yellow/` (pull after **Export save**; MTP / SD / FTP) |
|
| Save exports | Same save dir → `exports/red\|blue\|yellow\|gold\|silver/` (pull after **Export save**; Gold / Silver cart `.sav` not supported yet) |
|
||||||
| Opt-in diagnostics | Empty `switch-debug.txt` in the save dir → `switch.log` |
|
| Opt-in diagnostics | Empty `switch-debug.txt` in the save dir → `switch.log` |
|
||||||
| Lua error log | `lua-error.log` in the save dir |
|
| Lua error log | `lua-error.log` in the save dir |
|
||||||
|
|
||||||
@@ -54,8 +54,9 @@ macOS, not a Mac-only requirement.
|
|||||||
3. Create `switch/gen1recomp/` if needed; extract the release zip at SD root
|
3. Create `switch/gen1recomp/` if needed; extract the release zip at SD root
|
||||||
(or copy NRO / `game.love` for loose).
|
(or copy NRO / `game.love` for loose).
|
||||||
4. For ROMs/mods/saves, open the save-dir `imports/`, `imports/mods/`,
|
4. For ROMs/mods/saves, open the save-dir `imports/`, `imports/mods/`,
|
||||||
`imports/saves/<red|blue|yellow>/`, or `exports/<red|blue|yellow>/` path the
|
`imports/saves/<red|blue|yellow|gold|silver>/`, or
|
||||||
launcher prints.
|
`exports/<red|blue|yellow|gold|silver>/`
|
||||||
|
path the launcher prints.
|
||||||
5. Wait for the queue; refresh; exit MTP responder; title-override launch.
|
5. Wait for the queue; refresh; exit MTP responder; title-override launch.
|
||||||
|
|
||||||
macOS clients often create AppleDouble sidecars (`._Something.zip`,
|
macOS clients often create AppleDouble sidecars (`._Something.zip`,
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# Tiled map editing (mod authoring)
|
||||||
|
|
||||||
|
`tools/tiled_export.py` turns the imported ROM cache into a
|
||||||
|
[Tiled](https://www.mapeditor.org) workspace, so maps can be edited in a
|
||||||
|
real map editor and exported back out as a mod. The original had no map
|
||||||
|
editor at all; the port's own map data is plain Lua, which is what makes
|
||||||
|
this a data path rather than an asset path.
|
||||||
|
|
||||||
|
Editing is done in our own Tiled build,
|
||||||
|
[bryanthaboi/tiled_gen1recomp](https://github.com/bryanthaboi/tiled_gen1recomp/releases),
|
||||||
|
which ships the `gen1-mod-export` extension the workspace relies on. Grab it
|
||||||
|
from that repo's releases; upstream Tiled opens the workspace but cannot
|
||||||
|
export a mod out of it.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 tools/tiled_export.py # -> build/tiled/ (gitignored)
|
||||||
|
```
|
||||||
|
|
||||||
|
Then open `build/tiled/gen1.tiled-project` in that build of Tiled.
|
||||||
|
|
||||||
|
- **The overworld is one surface.** All 222 maps become `maps/*.tmj`, and
|
||||||
|
`kanto.world` places the 36 connected overworld maps at their real
|
||||||
|
connection offsets. That world is pre-loaded (seeded into the workspace's
|
||||||
|
Tiled session), so opening any one overworld map draws its neighbors around
|
||||||
|
it and you scroll and edit straight across the seams. Everything else is a
|
||||||
|
double-click away in Tiled's project panel.
|
||||||
|
- **Extending Kanto wires both ends.** A connection lives on both maps, so
|
||||||
|
hooking a new map onto a base map also emits the return connection as a
|
||||||
|
patch on that base map, keeping its other directions intact. The return
|
||||||
|
offset is derived, not guessed: all 78 vanilla reciprocal pairs satisfy
|
||||||
|
`back.offset == -offset`.
|
||||||
|
- **A Tiled tile is a gen1 block.** Each of the 24 tilesets becomes a Tiled
|
||||||
|
tileset whose tiles are its 32x32 blocks, composited from the 8x8 sheet,
|
||||||
|
so a tile layer *is* the map's `blocks` array. Warps, signs and objects
|
||||||
|
sit on the 16px cell grid in object layers, which is the grid the engine
|
||||||
|
addresses them on.
|
||||||
|
- **Collision is visible.** View > Show Tile Collision Shapes draws the real
|
||||||
|
walkability: a rectangle covers each cell whose feet tile is not in the
|
||||||
|
tileset's `walkable` list, which is the rule `src/world/Map.lua` applies.
|
||||||
|
- **Maps are shown in their real colors.** Each map is atlased in the SGB
|
||||||
|
palette it renders with, so Cerulean is blue and Lavender is purple in the
|
||||||
|
editor exactly as in game. Vanilla resolves that through a cascade with
|
||||||
|
interiors inheriting the last outdoor map, so the workspace mirrors the
|
||||||
|
cascade and walks the warp graph to colour interiors. Changing a map's
|
||||||
|
`palette` exports `palette = "..."` on the record, which beats the cascade,
|
||||||
|
and the editor offers the real palette names as a dropdown.
|
||||||
|
- **New blocks and new tilesets.** `blocksets/*.tmj` show a tileset's blocks
|
||||||
|
as raw 8x8 tiles, four by four, so new blocks can be composed there;
|
||||||
|
per-tile flags on `tilesets/tiles_*.tsj` become `walkable`, `waterTiles`,
|
||||||
|
`doorTiles` and the rest.
|
||||||
|
- **Export is a diff, not a fork of the data.** The `gen1-mod-export`
|
||||||
|
extension (shipped in `tiled_gen1recomp`) writes either one map file or a whole
|
||||||
|
loadable mod folder. An edited vanilla map diffs against the imported data
|
||||||
|
and emits `mod.content.maps:patch` carrying *only* the fields that moved, so
|
||||||
|
a mod covers the parts it changes and leaves the rest to the base game; a
|
||||||
|
new map gets `:register` at an index of 1000 or above. An unchanged map
|
||||||
|
exports nothing at all. Exports pass `tools/modkit.py validate` and `lint`.
|
||||||
|
- **Or the whole record, on request.** Ticking `exactExport` on a map switches
|
||||||
|
it to `mod.content.maps:override`, pinning the map to exactly what the
|
||||||
|
editor shows. It is off by default because an override wins outright over
|
||||||
|
any other mod patching that map, where a patch composes.
|
||||||
|
|
||||||
|
No ROM-derived art travels into an exported mod: a tileset still drawing on
|
||||||
|
the player's own imported sheet references that path rather than shipping the
|
||||||
|
pixels, and only a sheet the author supplied is copied in.
|
||||||
@@ -64,7 +64,8 @@ mounted or deleted as stale; the launcher directs the player to a full package.
|
|||||||
|
|
||||||
Each tagged release `vX.Y.Z` carries the existing per-platform archives
|
Each tagged release `vX.Y.Z` carries the existing per-platform archives
|
||||||
(`gen1recomp-X.Y.Z-macos.zip`, `-windows.zip`, `-linux.zip`,
|
(`gen1recomp-X.Y.Z-macos.zip`, `-windows.zip`, `-linux.zip`,
|
||||||
`-android.apk`) plus two assets the updater itself consumes:
|
`-linux-arm64.AppImage`, `-android.apk`, `-ios.ipa`, `-switch.zip`, Xbox and
|
||||||
|
PortMaster archives) plus two assets the updater itself consumes:
|
||||||
|
|
||||||
- `gen1recomp-X.Y.Z.love` - the payload, matched by the exact pattern
|
- `gen1recomp-X.Y.Z.love` - the payload, matched by the exact pattern
|
||||||
`gen1recomp-<version>.love` (see `isPayloadName` in `Boot.lua` and
|
`gen1recomp-<version>.love` (see `isPayloadName` in `Boot.lua` and
|
||||||
@@ -75,8 +76,9 @@ Each tagged release `vX.Y.Z` carries the existing per-platform archives
|
|||||||
filename otherwise to match the asset name exactly.
|
filename otherwise to match the asset name exactly.
|
||||||
|
|
||||||
A release missing either asset is treated as "no in-place update available":
|
A release missing either asset is treated as "no in-place update available":
|
||||||
`Check` reports `needs_full` and sends the player to `Check.releaseUrl()`
|
`Check` reports `needs_full`. It also selects the exact current platform asset
|
||||||
(`https://github.com/bryanthaboi/gen1recomp/releases/latest`).
|
from the same release and persists the requirement, so it is visible again on
|
||||||
|
every launch, including offline launches.
|
||||||
|
|
||||||
## Save-directory layout
|
## Save-directory layout
|
||||||
|
|
||||||
@@ -85,6 +87,7 @@ Under the save directory (identity `pokemon-love2d`):
|
|||||||
```
|
```
|
||||||
updates/gen1recomp-<X.Y.Z>.love downloaded payload(s)
|
updates/gen1recomp-<X.Y.Z>.love downloaded payload(s)
|
||||||
updates/pending.txt crash-guard marker
|
updates/pending.txt crash-guard marker
|
||||||
|
updates/full-update.json persistent native-package requirement
|
||||||
```
|
```
|
||||||
|
|
||||||
`pending.txt` holds the filename of the payload currently being chainloaded.
|
`pending.txt` holds the filename of the payload currently being chainloaded.
|
||||||
@@ -106,7 +109,8 @@ bundled game, in that case.
|
|||||||
against the GitHub releases API; safe to call every frame, it is a no-op
|
against the GitHub releases API; safe to call every frame, it is a no-op
|
||||||
once a check is in flight or has reached a terminal state. `Check.state()`
|
once a check is in flight or has reached a terminal state. `Check.state()`
|
||||||
reports `idle | checking | uptodate | available | downloading | ready |
|
reports `idle | checking | uptodate | available | downloading | ready |
|
||||||
needs_full | error` plus the latest version and download progress.
|
needs_full | full_downloading | full_ready | error` plus the latest version,
|
||||||
|
download progress, and (when applicable) the selected full-package asset.
|
||||||
3. **Download + verify**: on `available`, `Check.download()` tells the
|
3. **Download + verify**: on `available`, `Check.download()` tells the
|
||||||
worker to fetch the payload, polling the growing `.part` file for
|
worker to fetch the payload, polling the growing `.part` file for
|
||||||
progress. On completion the worker re-fetches `sha256sums.txt`, verifies
|
progress. On completion the worker re-fetches `sha256sums.txt`, verifies
|
||||||
@@ -117,6 +121,14 @@ bundled game, in that case.
|
|||||||
4. **Restart to apply**: a `ready` payload just sits in `updates/` until the
|
4. **Restart to apply**: a `ready` payload just sits in `updates/` until the
|
||||||
player relaunches; the next launch's Boot step (1) is what actually
|
player relaunches; the next launch's Boot step (1) is what actually
|
||||||
mounts and runs it. There is no in-session hot-swap.
|
mounts and runs it. There is no in-session hot-swap.
|
||||||
|
5. **Native-package requirement**: when `minShell` or `payloadHost` is
|
||||||
|
incompatible, the worker writes `full-update.json` and surfaces a
|
||||||
|
persistent launcher control. Android downloads the release APK, verifies
|
||||||
|
its SHA-256 entry from `sha256sums.txt`, then invokes Android's Package
|
||||||
|
Installer. The installer asks the user for consent and enforces package,
|
||||||
|
version-code, and signing-certificate compatibility. iOS links the
|
||||||
|
sideload repository for a re-sideload; Xbox, desktop, and PortMaster builds
|
||||||
|
link their correctly named full package. Switch keeps its native OTA flow.
|
||||||
|
|
||||||
## Known limitations
|
## Known limitations
|
||||||
|
|
||||||
@@ -140,6 +152,13 @@ bundled game, in that case.
|
|||||||
still need a full reinstall (`minShell` / `payloadHost` gate →
|
still need a full reinstall (`minShell` / `payloadHost` gate →
|
||||||
`needs_full`). Applying a downloaded payload on Android relaunches via
|
`needs_full`). Applying a downloaded payload on Android relaunches via
|
||||||
`love.system.restartApp`; iOS still uses in-process `quit("restart")`.
|
`love.system.restartApp`; iOS still uses in-process `quit("restart")`.
|
||||||
|
- **Android full updates are user-confirmed and certificate-bound.** The app
|
||||||
|
uses a private `FileProvider` cache path plus
|
||||||
|
`Intent.ACTION_INSTALL_PACKAGE`, checks Android 8+'s per-app
|
||||||
|
"install unknown apps" setting, and never requests a silent install. The
|
||||||
|
release job must use the original long-lived Android signing key; a new key
|
||||||
|
causes Android to reject an in-place update and requires a one-time manual
|
||||||
|
reinstall. See [mobile/ANDROID.md](../mobile/ANDROID.md).
|
||||||
- **Dev/source runs never self-update.** `Boot.run` returns immediately when
|
- **Dev/source runs never self-update.** `Boot.run` returns immediately when
|
||||||
`love.filesystem.isFused()` is false, and a working tree's `engine` is the
|
`love.filesystem.isFused()` is false, and a working tree's `engine` is the
|
||||||
`"0.0.0-dev"` placeholder that always reports up to date, so a source
|
`"0.0.0-dev"` placeholder that always reports up to date, so a source
|
||||||
|
|||||||
@@ -291,6 +291,79 @@ function closeSkinStudio()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function makeLauncher()
|
||||||
|
local RomImporter = require("src.import.RomImporter")
|
||||||
|
local forceImport = os.getenv("POKEPORT_FORCE_IMPORT") == "1"
|
||||||
|
return RomImporter.new(function(version)
|
||||||
|
Importer = nil
|
||||||
|
bootGame(version)
|
||||||
|
end, {
|
||||||
|
launcher = true,
|
||||||
|
forceImport = forceImport,
|
||||||
|
onEditSave = openEditor,
|
||||||
|
onEditTouchControls = openTouchControlsEditor,
|
||||||
|
onOpenSkinStudio = require("src.ui.SkinStudio").available_desktop()
|
||||||
|
and openSkinStudio or nil,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
local function returnToLauncher()
|
||||||
|
if not Game then return end
|
||||||
|
|
||||||
|
pcall(function() require("src.core.Music").stop() end)
|
||||||
|
pcall(function() require("src.core.Sound").stop() end)
|
||||||
|
if package.loaded["src.core.ChipAudio"] then
|
||||||
|
pcall(package.loaded["src.core.ChipAudio"].shutdown)
|
||||||
|
end
|
||||||
|
if package.loaded["src.core.DiscordPresence"] then
|
||||||
|
pcall(package.loaded["src.core.DiscordPresence"].shutdown)
|
||||||
|
end
|
||||||
|
if package.loaded["src.core.gen2.Clock"] then
|
||||||
|
pcall(package.loaded["src.core.gen2.Clock"].shutdown)
|
||||||
|
end
|
||||||
|
if package.loaded["src.net.Gen1Tls"] then
|
||||||
|
pcall(package.loaded["src.net.Gen1Tls"].shutdown)
|
||||||
|
end
|
||||||
|
if love.audio and love.audio.stop then
|
||||||
|
pcall(love.audio.stop)
|
||||||
|
end
|
||||||
|
pcall(function() require("src.render.SecondScreen").setEnabled(false) end)
|
||||||
|
|
||||||
|
local GameVersion = require("src.core.GameVersion")
|
||||||
|
local currentVersion = GameVersion.get()
|
||||||
|
if currentVersion then
|
||||||
|
require("src.import.CacheFs").unmountVersion(currentVersion)
|
||||||
|
end
|
||||||
|
require("src.core.Data"):unloadGenerated()
|
||||||
|
|
||||||
|
local Runtime = require("src.mods.Runtime")
|
||||||
|
if Runtime.reset then
|
||||||
|
Runtime.reset()
|
||||||
|
end
|
||||||
|
|
||||||
|
Game = nil
|
||||||
|
autopilot = nil
|
||||||
|
driverCo = nil
|
||||||
|
|
||||||
|
local Input = require("src.core.Input")
|
||||||
|
local TouchControls = require("src.core.TouchControls")
|
||||||
|
Input:reset()
|
||||||
|
TouchControls:reset()
|
||||||
|
|
||||||
|
require("src.core.Orientation").applyOptions(
|
||||||
|
require("src.core.SaveData").loadOptions())
|
||||||
|
|
||||||
|
local preload = require("src.mods.LauncherMods").translationStrings()
|
||||||
|
if preload then require("src.core.Strings").load({ strings = preload }) end
|
||||||
|
|
||||||
|
if love.window and love.window.setTitle then
|
||||||
|
local Version = require("src.core.Version")
|
||||||
|
love.window.setTitle(Version.title("Gen 1 Recompilation Project"))
|
||||||
|
end
|
||||||
|
|
||||||
|
Importer = makeLauncher()
|
||||||
|
end
|
||||||
|
|
||||||
function bootGame(version)
|
function bootGame(version)
|
||||||
-- The launcher hands us the chosen game (Red / Blue / Yellow / Gold);
|
-- The launcher hands us the chosen game (Red / Blue / Yellow / Gold);
|
||||||
-- scripted and headless runs fall back to POKEPORT_VERSION, then Red.
|
-- scripted and headless runs fall back to POKEPORT_VERSION, then Red.
|
||||||
@@ -314,11 +387,12 @@ function bootGame(version)
|
|||||||
love.window.setTitle(Version.title(
|
love.window.setTitle(Version.title(
|
||||||
GameVersion.info().displayName .. " (Gen 1 Recompilation Project)"))
|
GameVersion.info().displayName .. " (Gen 1 Recompilation Project)"))
|
||||||
end
|
end
|
||||||
-- Gold: Gen 1 Game:load cannot consume a Gen 2 cache -- different generated
|
-- Gen 2: Gen 1 Game:load cannot consume a Gen 2 cache -- different generated
|
||||||
-- tables, save shape and screen registry -- so Gold boots its own service
|
-- tables, save shape and screen registry -- so Gold and Silver boot their
|
||||||
-- owner, which mounts src/world/gen2 (walk / warps / connections) and the
|
-- own service owner, which mounts src/world/gen2 (walk / warps /
|
||||||
-- Gen 2 screens instead of src/core/Game.lua's Gen 1 wiring.
|
-- connections) and the Gen 2 screens instead of src/core/Game.lua's Gen 1
|
||||||
if GameVersion.isGold() then
|
-- wiring.
|
||||||
|
if GameVersion.generation() == 2 then
|
||||||
Game = require("src.core.Game2").new()
|
Game = require("src.core.Game2").new()
|
||||||
Game:load()
|
Game:load()
|
||||||
else
|
else
|
||||||
@@ -382,7 +456,7 @@ function love.load(args)
|
|||||||
|
|
||||||
-- Apply the persisted Android orientation lock (#592) before the launcher
|
-- Apply the persisted Android orientation lock (#592) before the launcher
|
||||||
-- shows: SDL created the window with no orientation hint, so without this
|
-- shows: SDL created the window with no orientation hint, so without this
|
||||||
-- the launcher would rotate freely until Game:applyOptions runs at boot.
|
-- the launcher would rotate freely until options are applied at boot.
|
||||||
-- No-op on desktop / iOS / when options.lua does not exist yet.
|
-- No-op on desktop / iOS / when options.lua does not exist yet.
|
||||||
require("src.core.Orientation").applyOptions(
|
require("src.core.Orientation").applyOptions(
|
||||||
require("src.core.SaveData").loadOptions())
|
require("src.core.SaveData").loadOptions())
|
||||||
@@ -442,8 +516,8 @@ function love.load(args)
|
|||||||
-- (#767) only pays off if something fills that catalog this early, and no
|
-- (#767) only pays off if something fills that catalog this early, and no
|
||||||
-- restart could: the ordering is the same on every launch. Read the
|
-- restart could: the ordering is the same on every launch. Read the
|
||||||
-- enabled mods' string catalogs -- data only, no entry chunk -- so a
|
-- enabled mods' string catalogs -- data only, no entry chunk -- so a
|
||||||
-- translation reaches the launcher too. Game:load replaces this with the
|
-- translation reaches the launcher too. The active game's loader replaces
|
||||||
-- real merged catalog once a version boots.
|
-- this with the real merged catalog once a version boots.
|
||||||
do
|
do
|
||||||
local preload = require("src.mods.LauncherMods").translationStrings()
|
local preload = require("src.mods.LauncherMods").translationStrings()
|
||||||
if preload then require("src.core.Strings").load({ strings = preload }) end
|
if preload then require("src.core.Strings").load({ strings = preload }) end
|
||||||
@@ -484,17 +558,7 @@ function love.load(args)
|
|||||||
-- by its SHA-1 (GameVersion.forSha1); pressing Play boots that game (Gold
|
-- by its SHA-1 (GameVersion.forSha1); pressing Play boots that game (Gold
|
||||||
-- goes to its own service owner, src/core/Game2.lua -- docs/gold-phase1.md).
|
-- goes to its own service owner, src/core/Game2.lua -- docs/gold-phase1.md).
|
||||||
-- Edit on a save row opens the bundled editor on that slot (openEditor).
|
-- Edit on a save row opens the bundled editor on that slot (openEditor).
|
||||||
Importer = RomImporter.new(function(version)
|
Importer = makeLauncher()
|
||||||
Importer = nil
|
|
||||||
bootGame(version)
|
|
||||||
end, {
|
|
||||||
launcher = true,
|
|
||||||
forceImport = forceImport,
|
|
||||||
onEditSave = openEditor,
|
|
||||||
onEditTouchControls = openTouchControlsEditor,
|
|
||||||
onOpenSkinStudio = require("src.ui.SkinStudio").available_desktop()
|
|
||||||
and openSkinStudio or nil,
|
|
||||||
})
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function love.update(dt)
|
function love.update(dt)
|
||||||
@@ -506,6 +570,7 @@ function love.update(dt)
|
|||||||
if TouchEditor then return TouchEditor.update(dt) end
|
if TouchEditor then return TouchEditor.update(dt) end
|
||||||
if Studio then return Studio.update(dt) end
|
if Studio then return Studio.update(dt) end
|
||||||
if Importer then return Importer:update(dt) end
|
if Importer then return Importer:update(dt) end
|
||||||
|
if not Game then return end
|
||||||
|
|
||||||
-- Scripted runs (autopilot / POKEPORT_DRIVER) observe and act exactly
|
-- Scripted runs (autopilot / POKEPORT_DRIVER) observe and act exactly
|
||||||
-- once per Game:update, so they must keep a 1:1 relationship with the
|
-- once per Game:update, so they must keep a 1:1 relationship with the
|
||||||
@@ -602,12 +667,14 @@ function love.keypressed(key, scancode, isrepeat)
|
|||||||
if TouchEditor then return TouchEditor.keypressed(key) end
|
if TouchEditor then return TouchEditor.keypressed(key) end
|
||||||
if Studio then return Studio.keypressed(key) end
|
if Studio then return Studio.keypressed(key) end
|
||||||
if Importer then return Importer:keypressed(key) end
|
if Importer then return Importer:keypressed(key) end
|
||||||
|
if not Game then return end
|
||||||
Game:keypressed(key)
|
Game:keypressed(key)
|
||||||
end
|
end
|
||||||
|
|
||||||
function love.keyreleased(key)
|
function love.keyreleased(key)
|
||||||
if editorMode or TouchEditor or Studio then return end
|
if editorMode or TouchEditor or Studio then return end
|
||||||
if Importer then return end
|
if Importer then return end
|
||||||
|
if not Game then return end
|
||||||
Game:keyreleased(key)
|
Game:keyreleased(key)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -625,7 +692,9 @@ function love.gamepadpressed(joystick, button)
|
|||||||
end
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
if Studio then return end
|
||||||
if Importer then return Importer:gamepadpressed(joystick, button) end
|
if Importer then return Importer:gamepadpressed(joystick, button) end
|
||||||
|
if not Game then return end
|
||||||
Game:gamepadpressed(joystick, button)
|
Game:gamepadpressed(joystick, button)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -643,7 +712,9 @@ function love.gamepadreleased(joystick, button)
|
|||||||
end
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
if Studio then return end
|
||||||
if Importer then return Importer:gamepadreleased(joystick, button) end
|
if Importer then return Importer:gamepadreleased(joystick, button) end
|
||||||
|
if not Game then return end
|
||||||
Game:gamepadreleased(joystick, button)
|
Game:gamepadreleased(joystick, button)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -661,7 +732,9 @@ function love.gamepadaxis(joystick, axis, value)
|
|||||||
end
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
if Studio then return end
|
||||||
if Importer then return Importer:gamepadaxis(joystick, axis, value) end
|
if Importer then return Importer:gamepadaxis(joystick, axis, value) end
|
||||||
|
if not Game then return end
|
||||||
Game:gamepadaxis(joystick, axis, value)
|
Game:gamepadaxis(joystick, axis, value)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -679,7 +752,9 @@ function love.joystickpressed(joystick, button)
|
|||||||
end
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
if Studio then return end
|
||||||
if Importer then return Importer:joystickpressed(joystick, button) end
|
if Importer then return Importer:joystickpressed(joystick, button) end
|
||||||
|
if not Game then return end
|
||||||
Game:joystickpressed(joystick, button)
|
Game:joystickpressed(joystick, button)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -697,7 +772,9 @@ function love.joystickreleased(joystick, button)
|
|||||||
end
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
if Studio then return end
|
||||||
if Importer then return Importer:joystickreleased(joystick, button) end
|
if Importer then return Importer:joystickreleased(joystick, button) end
|
||||||
|
if not Game then return end
|
||||||
Game:joystickreleased(joystick, button)
|
Game:joystickreleased(joystick, button)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -715,7 +792,9 @@ function love.joystickaxis(joystick, axis, value)
|
|||||||
end
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
if Studio then return end
|
||||||
if Importer then return Importer:joystickaxis(joystick, axis, value) end
|
if Importer then return Importer:joystickaxis(joystick, axis, value) end
|
||||||
|
if not Game then return end
|
||||||
Game:joystickaxis(joystick, axis, value)
|
Game:joystickaxis(joystick, axis, value)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -733,21 +812,25 @@ function love.joystickhat(joystick, hat, direction)
|
|||||||
end
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
if Studio then return end
|
||||||
if Importer then return Importer:joystickhat(joystick, hat, direction) end
|
if Importer then return Importer:joystickhat(joystick, hat, direction) end
|
||||||
|
if not Game then return end
|
||||||
Game:joystickhat(joystick, hat, direction)
|
Game:joystickhat(joystick, hat, direction)
|
||||||
end
|
end
|
||||||
|
|
||||||
function love.joystickadded(joystick)
|
function love.joystickadded(joystick)
|
||||||
SwitchDiagnostics.onJoystickEvent("joystickadded", joystick)
|
SwitchDiagnostics.onJoystickEvent("joystickadded", joystick)
|
||||||
if editorMode or TouchEditor then return end
|
if editorMode or TouchEditor or Studio then return end
|
||||||
if Importer then return end
|
if Importer then return end
|
||||||
|
if not Game then return end
|
||||||
Game:joystickadded(joystick)
|
Game:joystickadded(joystick)
|
||||||
end
|
end
|
||||||
|
|
||||||
function love.joystickremoved(joystick)
|
function love.joystickremoved(joystick)
|
||||||
SwitchDiagnostics.onJoystickEvent("joystickremoved", joystick)
|
SwitchDiagnostics.onJoystickEvent("joystickremoved", joystick)
|
||||||
if editorMode or TouchEditor then return end
|
if editorMode or TouchEditor or Studio then return end
|
||||||
if Importer then return end
|
if Importer then return end
|
||||||
|
if not Game then return end
|
||||||
Game:joystickremoved(joystick)
|
Game:joystickremoved(joystick)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -756,29 +839,79 @@ end
|
|||||||
-- unfocused, so reset input on either transition rather than trust it.
|
-- unfocused, so reset input on either transition rather than trust it.
|
||||||
function love.focus(f)
|
function love.focus(f)
|
||||||
if editorMode or TouchEditor then return end
|
if editorMode or TouchEditor then return end
|
||||||
|
if Studio then
|
||||||
|
if Studio.focus then Studio.focus(f) end
|
||||||
|
return
|
||||||
|
end
|
||||||
if Importer then
|
if Importer then
|
||||||
require("src.core.Input"):reset()
|
require("src.core.Input"):reset()
|
||||||
if Importer.focus then Importer:focus(f) end
|
if Importer.focus then Importer:focus(f) end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
if not Game then return end
|
||||||
Game:focus(f)
|
Game:focus(f)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- v is true when the window becomes visible again, false on minimize.
|
-- v is true when the window becomes visible again, false on minimize.
|
||||||
function love.visible(v)
|
function love.visible(v)
|
||||||
if editorMode or TouchEditor then return end
|
if editorMode or TouchEditor then return end
|
||||||
|
if Studio then
|
||||||
|
if Studio.visible then Studio.visible(v) end
|
||||||
|
return
|
||||||
|
end
|
||||||
if Importer then
|
if Importer then
|
||||||
require("src.core.Input"):reset()
|
require("src.core.Input"):reset()
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
if not Game then return end
|
||||||
Game:visible(v)
|
Game:visible(v)
|
||||||
end
|
end
|
||||||
|
|
||||||
function love.lowmemory()
|
function love.lowmemory()
|
||||||
if editorMode or TouchEditor or Importer then return end
|
if editorMode or TouchEditor or Studio or Importer then return end
|
||||||
if Game then Game:onResume() end
|
if Game then Game:onResume() end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
love.handlers = love.handlers or {}
|
||||||
|
|
||||||
|
function love.handlers.audiosuspend()
|
||||||
|
local ChipAudio = package.loaded["src.core.ChipAudio"]
|
||||||
|
if ChipAudio then pcall(ChipAudio.setSuspended, true) end
|
||||||
|
end
|
||||||
|
|
||||||
|
function love.handlers.audioreset()
|
||||||
|
local ChipAudio = package.loaded["src.core.ChipAudio"]
|
||||||
|
if ChipAudio then
|
||||||
|
pcall(ChipAudio.setSuspended, false)
|
||||||
|
pcall(ChipAudio.rebuildPlayback)
|
||||||
|
end
|
||||||
|
local Music = package.loaded["src.core.Music"]
|
||||||
|
if Music then pcall(Music.onDeviceReset) end
|
||||||
|
local Sound = package.loaded["src.core.Sound"]
|
||||||
|
if Sound then pcall(Sound.onDeviceReset) end
|
||||||
|
end
|
||||||
|
|
||||||
|
function love.handlers.intent_game(version)
|
||||||
|
if type(version) ~= "string" or version == "" then return end
|
||||||
|
version = version:lower():gsub("^%s+", ""):gsub("%s+$", "")
|
||||||
|
local GameVersion = require("src.core.GameVersion")
|
||||||
|
if GameVersion.VERSIONS and not GameVersion.VERSIONS[version] then return end
|
||||||
|
|
||||||
|
local RomImporter = require("src.import.RomImporter")
|
||||||
|
if not RomImporter.isReady(version) then return end
|
||||||
|
|
||||||
|
local currentVersion = GameVersion.get()
|
||||||
|
if Game and currentVersion == version then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if Game then
|
||||||
|
returnToLauncher()
|
||||||
|
end
|
||||||
|
Importer = nil
|
||||||
|
bootGame(version)
|
||||||
|
end
|
||||||
|
|
||||||
function love.touchpressed(id, x, y, dx, dy, pressure)
|
function love.touchpressed(id, x, y, dx, dy, pressure)
|
||||||
if editorMode then
|
if editorMode then
|
||||||
-- iOS synthesizes mousepressed for the primary touch; forwarding here
|
-- iOS synthesizes mousepressed for the primary touch; forwarding here
|
||||||
@@ -796,12 +929,14 @@ function love.touchpressed(id, x, y, dx, dy, pressure)
|
|||||||
if love.system.getOS() == "iOS" then return end
|
if love.system.getOS() == "iOS" then return end
|
||||||
return TouchEditor.touchpressed(id, x, y)
|
return TouchEditor.touchpressed(id, x, y)
|
||||||
end
|
end
|
||||||
|
if Studio then return end
|
||||||
if Importer then
|
if Importer then
|
||||||
-- Both mobiles: FlexLove scroll needs the real touch stream. Clicks are
|
-- Both mobiles: FlexLove scroll needs the real touch stream. Clicks are
|
||||||
-- polled inside the view; the istouch filter on mousepressed still drops
|
-- polled inside the view; the istouch filter on mousepressed still drops
|
||||||
-- Android's synthesized mouse twin so Import cannot double-fire (#553).
|
-- Android's synthesized mouse twin so Import cannot double-fire (#553).
|
||||||
return Importer:touchpressed(id, x, y, dx, dy, pressure)
|
return Importer:touchpressed(id, x, y, dx, dy, pressure)
|
||||||
end
|
end
|
||||||
|
if not Game then return end
|
||||||
Game:touchpressed(id, x, y, dx, dy, pressure)
|
Game:touchpressed(id, x, y, dx, dy, pressure)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -811,9 +946,11 @@ function love.touchmoved(id, x, y, dx, dy, pressure)
|
|||||||
if love.system.getOS() == "iOS" then return end
|
if love.system.getOS() == "iOS" then return end
|
||||||
return TouchEditor.touchmoved(id, x, y)
|
return TouchEditor.touchmoved(id, x, y)
|
||||||
end
|
end
|
||||||
|
if Studio then return end
|
||||||
if Importer then
|
if Importer then
|
||||||
return Importer:touchmoved(id, x, y, dx, dy, pressure)
|
return Importer:touchmoved(id, x, y, dx, dy, pressure)
|
||||||
end
|
end
|
||||||
|
if not Game then return end
|
||||||
Game:touchmoved(id, x, y, dx, dy, pressure)
|
Game:touchmoved(id, x, y, dx, dy, pressure)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -823,9 +960,11 @@ function love.touchreleased(id, x, y, dx, dy, pressure)
|
|||||||
if love.system.getOS() == "iOS" then return end
|
if love.system.getOS() == "iOS" then return end
|
||||||
return TouchEditor.touchreleased(id, x, y)
|
return TouchEditor.touchreleased(id, x, y)
|
||||||
end
|
end
|
||||||
|
if Studio then return end
|
||||||
if Importer then
|
if Importer then
|
||||||
return Importer:touchreleased(id, x, y, dx, dy, pressure)
|
return Importer:touchreleased(id, x, y, dx, dy, pressure)
|
||||||
end
|
end
|
||||||
|
if not Game then return end
|
||||||
Game:touchreleased(id, x, y, dx, dy, pressure)
|
Game:touchreleased(id, x, y, dx, dy, pressure)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -837,6 +976,7 @@ function love.wheelmoved(x, y)
|
|||||||
if TouchEditor then return end
|
if TouchEditor then return end
|
||||||
if Studio then return Studio.wheelmoved(x, y) end
|
if Studio then return Studio.wheelmoved(x, y) end
|
||||||
if Importer then return end
|
if Importer then return end
|
||||||
|
if not Game then return end
|
||||||
Game:wheelmoved(x, y)
|
Game:wheelmoved(x, y)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -977,11 +1117,16 @@ function love.quit()
|
|||||||
-- docs/modding.md's core.quit_to_launcher entry) may veto returning to
|
-- docs/modding.md's core.quit_to_launcher entry) may veto returning to
|
||||||
-- this Lua launcher via that hook. Vanilla behavior (used when no mod
|
-- this Lua launcher via that hook. Vanilla behavior (used when no mod
|
||||||
-- claims the hook) is exactly the condition below.
|
-- claims the hook) is exactly the condition below.
|
||||||
|
local isAndroid = (love.system and love.system.getOS and love.system.getOS() == "Android")
|
||||||
local wouldReturnToLauncher = PlatformHooks.quitToLauncher(function()
|
local wouldReturnToLauncher = PlatformHooks.quitToLauncher(function()
|
||||||
return Game and not Importer and not quitToLauncher and not scripted
|
return Game and not Importer and not quitToLauncher and not scripted
|
||||||
and not launchedIntoGame
|
and (isAndroid or not launchedIntoGame)
|
||||||
end)
|
end)
|
||||||
if wouldReturnToLauncher then
|
if wouldReturnToLauncher then
|
||||||
|
if isAndroid then
|
||||||
|
returnToLauncher()
|
||||||
|
return true -- abort this quit; the restart lands back in the launcher
|
||||||
|
end
|
||||||
quitToLauncher = true
|
quitToLauncher = true
|
||||||
-- Tell the fresh boot to ignore any boot-straight-into-a-game option this
|
-- Tell the fresh boot to ignore any boot-straight-into-a-game option this
|
||||||
-- once, so the restart really does land in the launcher (#887). A failed
|
-- once, so the restart really does land in the launcher (#887). A failed
|
||||||
|
|||||||
@@ -93,8 +93,9 @@ transport, exactly as a missing curl does.
|
|||||||
love-android 11.5a expects:
|
love-android 11.5a expects:
|
||||||
|
|
||||||
- **JDK 17**
|
- **JDK 17**
|
||||||
- Android SDK with **API 34**
|
- Android SDK with **API 36** (Android 16; latest 36.x Build-Tools)
|
||||||
- NDK **25.2.9519653** (Apple Silicon host supported)
|
- NDK **25.2.9519653** (Apple Silicon host supported)
|
||||||
|
- **minSdk 19** (Android 4.4), **targetSdk 36** (Android 16)
|
||||||
|
|
||||||
Set `ANDROID_SDK_ROOT` (or `ANDROID_HOME`), or let the script write
|
Set `ANDROID_SDK_ROOT` (or `ANDROID_HOME`), or let the script write
|
||||||
`local.properties` when it finds `~/Library/Android/sdk`.
|
`local.properties` when it finds `~/Library/Android/sdk`.
|
||||||
@@ -109,10 +110,10 @@ The APK lands under `app/build/outputs/apk/embedNoRecord/debug/`.
|
|||||||
|
|
||||||
`app/src/embed/assets/game.love` - zip of `main.lua`, `conf.lua`, `src/`,
|
`app/src/embed/assets/game.love` - zip of `main.lua`, `conf.lua`, `src/`,
|
||||||
`libs/` (the vendored FlexLove toolkit the launcher UI needs), `data/`,
|
`libs/` (the vendored FlexLove toolkit the launcher UI needs), `data/`,
|
||||||
`assets/`, and the Red, Blue, and Yellow ROM manifests. The Android
|
`assets/`, and the Red, Blue, Yellow, Gold, and Silver ROM manifests. The
|
||||||
packer verifies the Yellow manifest before it packages; if a partial source
|
Android packer verifies the Yellow, Gold, and Silver manifests before it
|
||||||
export omitted it, it restores the file from this checkout's Git data and then
|
packages; if a partial source export omitted one, it restores the file from
|
||||||
falls back to the project's GitHub copy. Generated game data,
|
this checkout's Git data and then falls back to the project's GitHub copy. Generated game data,
|
||||||
scripts, tests, and mobile build sources are excluded.
|
scripts, tests, and mobile build sources are excluded.
|
||||||
|
|
||||||
## Branding (applied by the build script)
|
## Branding (applied by the build script)
|
||||||
@@ -122,15 +123,24 @@ scripts, tests, and mobile build sources are excluded.
|
|||||||
| `app.application_id` | `com.theboisclub.pokemonred` |
|
| `app.application_id` | `com.theboisclub.pokemonred` |
|
||||||
| `app.name` | Pokemon Red |
|
| `app.name` | Pokemon Red |
|
||||||
| `app.orientation` | `fullUser`. This is only the manifest default: SDL requests FULL_SENSOR at window creation (resizable window, no `SDL_HINT_ORIENTATIONS`), and `GameActivity.setOrientationBis` remaps that to FULL_USER so the device's rotation lock is honoured. |
|
| `app.orientation` | `fullUser`. This is only the manifest default: SDL requests FULL_SENSOR at window creation (resizable window, no `SDL_HINT_ORIENTATIONS`), and `GameActivity.setOrientationBis` remaps that to FULL_USER so the device's rotation lock is honoured. |
|
||||||
| `app.version_name` / `app.version_code` | set from `--version X.Y.Z` (code = major*10000 + minor*100 + patch); left as-is if `--version` is omitted |
|
| `app.version_name` / `app.version_code` | set from `--version X.Y.Z` (code = major*1,000,000 + minor*1,000 + patch); left as-is if `--version` is omitted |
|
||||||
| Permissions | RECORD_AUDIO / WRITE_EXTERNAL_STORAGE stripped; VIBRATE + BLUETOOTH + INTERNET (link play, mod index) + ACTIVITY_RECOGNITION (step bridge) kept |
|
| Permissions | RECORD_AUDIO / WRITE_EXTERNAL_STORAGE stripped; VIBRATE + BLUETOOTH + INTERNET (link play, mod index) + ACTIVITY_RECOGNITION (step bridge) kept; REQUEST_INSTALL_PACKAGES is limited to the user-confirmed full-update installer |
|
||||||
|
|
||||||
## Releases
|
## Releases
|
||||||
|
|
||||||
`.github/workflows/release.yml` builds the APK with `--version` set to the
|
`.github/workflows/release.yml` builds the APK with `--version` set to the
|
||||||
release version and publishes it alongside the macOS/Windows/Linux builds as
|
release version and publishes it alongside the macOS/Windows/Linux builds as
|
||||||
`PokemonRed-<version>-android.apk`.
|
`gen1recomp-<version>-android.apk`.
|
||||||
|
|
||||||
## Signing
|
## Signing
|
||||||
|
|
||||||
Signed with the default Android keystore (no setup required).
|
Production APKs are built with `scripts/build_android.sh --release`. They must
|
||||||
|
be signed with the same long-lived certificate as the currently installed app:
|
||||||
|
Android's Package Installer rejects an update with a different signing
|
||||||
|
certificate. Store that keystore and its passwords only in CI secrets, expose
|
||||||
|
them as `GEN1RECOMP_ANDROID_KEYSTORE`,
|
||||||
|
`GEN1RECOMP_ANDROID_KEYSTORE_PASSWORD`, `GEN1RECOMP_ANDROID_KEY_ALIAS`, and
|
||||||
|
`GEN1RECOMP_ANDROID_KEY_PASSWORD`, and never commit the keystore. A newly
|
||||||
|
created certificate cannot update users who have an APK signed by a different
|
||||||
|
legacy key; those users need one final manual reinstall before in-app updates
|
||||||
|
can take over.
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ Quick Start:
|
|||||||
Before you start, install JDK 17 (not later not earlier). If you intend to build from Android Studio, skip this step as
|
Before you start, install JDK 17 (not later not earlier). If you intend to build from Android Studio, skip this step as
|
||||||
Android Studio bundles its own JDK 17.
|
Android Studio bundles its own JDK 17.
|
||||||
|
|
||||||
Install Android SDK with SDK API 34 (34.x.y) and Android NDK 25.2.9519653, set the environment variable
|
Install Android SDK with SDK API 36 (latest 36.x Build-Tools) and Android NDK 25.2.9519653, set the environment variable
|
||||||
`ANDROID_SDK_ROOT` to your Android SDK location and run:
|
`ANDROID_SDK_ROOT` to your Android SDK location and run:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -10,9 +10,12 @@ android {
|
|||||||
applicationId project.properties["app.application_id"]
|
applicationId project.properties["app.application_id"]
|
||||||
versionCode project.properties["app.version_code"].toInteger()
|
versionCode project.properties["app.version_code"].toInteger()
|
||||||
versionName project.properties["app.version_name"]
|
versionName project.properties["app.version_name"]
|
||||||
minSdk 16
|
// NDK r25 no longer supports API 16; API 19 is Android 4.4 and keeps
|
||||||
compileSdk 34
|
// the native toolchain and package-installer bridge on a supported ABI.
|
||||||
targetSdk 34
|
minSdk 19
|
||||||
|
// Android 16 / API 36: current Android distribution target.
|
||||||
|
compileSdk 36
|
||||||
|
targetSdk 36
|
||||||
|
|
||||||
def getAppName = {
|
def getAppName = {
|
||||||
def nameArray = project.properties["app.name_byte_array"]
|
def nameArray = project.properties["app.name_byte_array"]
|
||||||
@@ -38,10 +41,31 @@ android {
|
|||||||
ORIENTATION:project.properties["app.orientation"],
|
ORIENTATION:project.properties["app.orientation"],
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
// Release signing lives outside the repository. The release build script
|
||||||
|
// requires all five values below, while debug builds intentionally remain
|
||||||
|
// usable without them.
|
||||||
|
def releaseStore = System.getenv("GEN1RECOMP_ANDROID_KEYSTORE")
|
||||||
|
def releaseStorePassword = System.getenv("GEN1RECOMP_ANDROID_KEYSTORE_PASSWORD")
|
||||||
|
def releaseKeyAlias = System.getenv("GEN1RECOMP_ANDROID_KEY_ALIAS")
|
||||||
|
def releaseKeyPassword = System.getenv("GEN1RECOMP_ANDROID_KEY_PASSWORD")
|
||||||
|
def hasReleaseSigning = releaseStore && releaseStorePassword && releaseKeyAlias && releaseKeyPassword
|
||||||
|
|
||||||
|
if (hasReleaseSigning) {
|
||||||
|
signingConfigs {
|
||||||
|
release {
|
||||||
|
storeFile file(releaseStore)
|
||||||
|
storePassword releaseStorePassword
|
||||||
|
keyAlias releaseKeyAlias
|
||||||
|
keyPassword releaseKeyPassword
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
minifyEnabled true
|
minifyEnabled true
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
|
if (hasReleaseSigning) signingConfig signingConfigs.release
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
flavorDimensions = ['mode', 'recording']
|
flavorDimensions = ['mode', 'recording']
|
||||||
|
|||||||
@@ -8,6 +8,10 @@
|
|||||||
the link screen shows as "(Operation not permitted)" (issue #287).
|
the link screen shows as "(Operation not permitted)" (issue #287).
|
||||||
scripts/build_android.sh must not strip this again. -->
|
scripts/build_android.sh must not strip this again. -->
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<!-- Required only to hand a checksum-verified, user-selected GitHub release
|
||||||
|
APK to Android's own Package Installer. Android still shows the install
|
||||||
|
confirmation and enforces package/signing-key/version compatibility. -->
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
<!-- Step bridge: love.system.syncHealthSteps reads the hardware step
|
<!-- Step bridge: love.system.syncHealthSteps reads the hardware step
|
||||||
counter, which Android 10+ gates behind this runtime permission.
|
counter, which Android 10+ gates behind this runtime permission.
|
||||||
Requested only on the first sync call (the Pokéwalker mod's SYNC
|
Requested only on the first sync call (the Pokéwalker mod's SYNC
|
||||||
@@ -29,17 +33,30 @@
|
|||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@drawable/love"
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:label="${NAME}" >
|
android:label="${NAME}" >
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.allow_multiple_resumed_activities"
|
android:name="android.allow_multiple_resumed_activities"
|
||||||
android:value="true" />
|
android:value="true" />
|
||||||
|
<!-- The full-update APK is copied into this small cache subdirectory
|
||||||
|
before it is handed to Package Installer. Keep the provider private
|
||||||
|
and expose only that directory, never a storage root. -->
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.full_update_provider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/full_update_paths" />
|
||||||
|
</provider>
|
||||||
<activity
|
<activity
|
||||||
android:name="org.love2d.android.GameActivity"
|
android:name="org.love2d.android.GameActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation"
|
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation"
|
||||||
android:label="${NAME}"
|
android:label="${NAME}"
|
||||||
android:launchMode="singleInstance"
|
android:launchMode="singleTask"
|
||||||
android:screenOrientation="${ORIENTATION}"
|
android:screenOrientation="${ORIENTATION}"
|
||||||
android:resizeableActivity="false"
|
android:resizeableActivity="false"
|
||||||
android:theme="@android:style/Theme.NoTitleBar.Fullscreen" >
|
android:theme="@android:style/Theme.NoTitleBar.Fullscreen" >
|
||||||
|
|||||||
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 32 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background" />
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
</adaptive-icon>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background" />
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
</adaptive-icon>
|
||||||
@@ -3,4 +3,10 @@
|
|||||||
<color name="colorPrimary">#3F51B5</color>
|
<color name="colorPrimary">#3F51B5</color>
|
||||||
<color name="colorPrimaryDark">#303F9F</color>
|
<color name="colorPrimaryDark">#303F9F</color>
|
||||||
<color name="colorAccent">#FF4081</color>
|
<color name="colorAccent">#FF4081</color>
|
||||||
|
<color name="ic_launcher_background">#FFFFFF</color>
|
||||||
|
<color name="shortcut_red">#E53935</color>
|
||||||
|
<color name="shortcut_blue">#1E88E5</color>
|
||||||
|
<color name="shortcut_yellow">#FDD835</color>
|
||||||
|
<color name="shortcut_gold">#D4AF37</color>
|
||||||
|
<color name="shortcut_silver">#BEC6D2</color>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Deliberately narrow: FileProvider may grant only the staged APK, never
|
||||||
|
arbitrary app, external, or shared storage. -->
|
||||||
|
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<cache-path name="full_update" path="full-update/" />
|
||||||
|
</paths>
|
||||||
@@ -18,7 +18,8 @@ buildscript {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:8.1.1'
|
// Android 16 / API 36 requires Android Gradle Plugin 8.9+.
|
||||||
|
classpath 'com.android.tools.build:gradle:8.9.2'
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ app.version_name=11.5a
|
|||||||
# No need to modify anything past this line!
|
# No need to modify anything past this line!
|
||||||
android.enableJetifier=false
|
android.enableJetifier=false
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.defaults.buildfeatures.buildconfig=true
|
|
||||||
android.nonTransitiveRClass=true
|
android.nonTransitiveRClass=true
|
||||||
android.nonFinalResIds=true
|
android.nonFinalResIds=true
|
||||||
app.name=gen1recomp
|
app.name=gen1recomp
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ android {
|
|||||||
ndkVersion '25.2.9519653'
|
ndkVersion '25.2.9519653'
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdk 16
|
minSdk 19
|
||||||
compileSdk 34
|
compileSdk 36
|
||||||
targetSdk 34
|
targetSdk 36
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
ndkBuild {
|
ndkBuild {
|
||||||
arguments "-j" + Runtime.runtime.availableProcessors()
|
arguments "-j" + Runtime.runtime.availableProcessors()
|
||||||
@@ -63,8 +63,7 @@ android {
|
|||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
minifyEnabled true
|
minifyEnabled false
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
|
||||||
}
|
}
|
||||||
debug {
|
debug {
|
||||||
ndk {
|
ndk {
|
||||||
|
|||||||
@@ -45,6 +45,11 @@
|
|||||||
// own, which can name a different volume on merged / adopted-SD storage.
|
// own, which can name a different volume on merged / adopted-SD storage.
|
||||||
#include "filesystem/Filesystem.h"
|
#include "filesystem/Filesystem.h"
|
||||||
|
|
||||||
|
#include "common/Module.h"
|
||||||
|
#include "audio/Audio.h"
|
||||||
|
#include "audio/openal/Audio.h"
|
||||||
|
#include "event/Event.h"
|
||||||
|
|
||||||
namespace love
|
namespace love
|
||||||
{
|
{
|
||||||
namespace android
|
namespace android
|
||||||
@@ -278,6 +283,104 @@ bool restartApp()
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool installApk(const char *path)
|
||||||
|
{
|
||||||
|
if (path == nullptr || path[0] == '\0')
|
||||||
|
return false;
|
||||||
|
|
||||||
|
JNIEnv *env = (JNIEnv*) SDL_AndroidGetJNIEnv();
|
||||||
|
// This may be called from Lua's main thread, but use the activity object
|
||||||
|
// class just like httpDownload so a future worker caller does not depend on
|
||||||
|
// the system JNI class loader finding the app class.
|
||||||
|
void *rawActivity = SDL_AndroidGetActivity();
|
||||||
|
if (rawActivity == nullptr)
|
||||||
|
return false;
|
||||||
|
jobject activityObj = (jobject) rawActivity;
|
||||||
|
jclass activity = env->GetObjectClass(activityObj);
|
||||||
|
env->DeleteLocalRef(activityObj);
|
||||||
|
|
||||||
|
jmethodID method = env->GetStaticMethodID(activity, "installApk",
|
||||||
|
"(Ljava/lang/String;Ljava/lang/String;)Z");
|
||||||
|
if (method == nullptr)
|
||||||
|
{
|
||||||
|
env->ExceptionClear();
|
||||||
|
env->DeleteLocalRef(activity);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
jstring jpath = env->NewStringUTF(path);
|
||||||
|
jstring jroot = env->NewStringUTF(bridgeSaveDirectory());
|
||||||
|
jboolean result = env->CallStaticBooleanMethod(activity, method, jpath, jroot);
|
||||||
|
env->DeleteLocalRef(jroot);
|
||||||
|
env->DeleteLocalRef(jpath);
|
||||||
|
env->DeleteLocalRef(activity);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool updateAppShortcuts(const std::vector<std::string> &versions)
|
||||||
|
{
|
||||||
|
JNIEnv *env = (JNIEnv*) SDL_AndroidGetJNIEnv();
|
||||||
|
jclass activity = env->FindClass("org/love2d/android/GameActivity");
|
||||||
|
if (activity == nullptr)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
jmethodID method = env->GetStaticMethodID(activity, "updateAppShortcuts", "([Ljava/lang/String;)Z");
|
||||||
|
if (method == nullptr)
|
||||||
|
{
|
||||||
|
env->ExceptionClear();
|
||||||
|
env->DeleteLocalRef(activity);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
jclass stringClass = env->FindClass("java/lang/String");
|
||||||
|
jobjectArray array = env->NewObjectArray((jsize) versions.size(), stringClass, nullptr);
|
||||||
|
for (size_t i = 0; i < versions.size(); ++i)
|
||||||
|
{
|
||||||
|
jstring jstr = env->NewStringUTF(versions[i].c_str());
|
||||||
|
env->SetObjectArrayElement(array, (jsize) i, jstr);
|
||||||
|
env->DeleteLocalRef(jstr);
|
||||||
|
}
|
||||||
|
|
||||||
|
jboolean result = env->CallStaticBooleanMethod(activity, method, array);
|
||||||
|
|
||||||
|
env->DeleteLocalRef(array);
|
||||||
|
env->DeleteLocalRef(stringClass);
|
||||||
|
env->DeleteLocalRef(activity);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string getLaunchGame()
|
||||||
|
{
|
||||||
|
JNIEnv *env = (JNIEnv*) SDL_AndroidGetJNIEnv();
|
||||||
|
jclass activity = env->FindClass("org/love2d/android/GameActivity");
|
||||||
|
if (activity == nullptr)
|
||||||
|
return "";
|
||||||
|
|
||||||
|
jmethodID method = env->GetStaticMethodID(activity, "getLaunchGame", "()Ljava/lang/String;");
|
||||||
|
if (method == nullptr)
|
||||||
|
{
|
||||||
|
env->ExceptionClear();
|
||||||
|
env->DeleteLocalRef(activity);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
jstring jgame = (jstring) env->CallStaticObjectMethod(activity, method);
|
||||||
|
if (jgame == nullptr)
|
||||||
|
{
|
||||||
|
env->DeleteLocalRef(activity);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *str = env->GetStringUTFChars(jgame, nullptr);
|
||||||
|
std::string result = (str != nullptr) ? str : "";
|
||||||
|
if (str != nullptr)
|
||||||
|
env->ReleaseStringUTFChars(jgame, str);
|
||||||
|
|
||||||
|
env->DeleteLocalRef(jgame);
|
||||||
|
env->DeleteLocalRef(activity);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
bool httpDownload(const char *url, const char *destPath, const char *userAgent, const char *accept)
|
bool httpDownload(const char *url, const char *destPath, const char *userAgent, const char *accept)
|
||||||
{
|
{
|
||||||
if (url == nullptr || destPath == nullptr)
|
if (url == nullptr || destPath == nullptr)
|
||||||
@@ -374,6 +477,104 @@ bool httpPost(const char *url, const char *body, int bodyLen, const char *conten
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool httpRequest(const char *url, const char *method,
|
||||||
|
const char *const *headerPairs, int headerPairCount,
|
||||||
|
const char *body, int bodyLen, const char *userAgent, std::string &out)
|
||||||
|
{
|
||||||
|
out.clear();
|
||||||
|
if (url == nullptr)
|
||||||
|
return false;
|
||||||
|
if (headerPairCount < 0 || (headerPairCount > 0 && headerPairs == nullptr))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
JNIEnv *env = (JNIEnv*) SDL_AndroidGetJNIEnv();
|
||||||
|
// Same resolution rule as httpDownload: the activity's own class via
|
||||||
|
// SDL_AndroidGetActivity, never FindClass for an app class -- save sync
|
||||||
|
// runs on a love.thread worker, whose class loader cannot see them.
|
||||||
|
jobject activityObj = (jobject) SDL_AndroidGetActivity();
|
||||||
|
if (activityObj == nullptr)
|
||||||
|
return false;
|
||||||
|
jclass activity = env->GetObjectClass(activityObj);
|
||||||
|
env->DeleteLocalRef(activityObj);
|
||||||
|
|
||||||
|
// Old APK / new liblove skew: report "no transport" instead of aborting
|
||||||
|
// on a missing method (#597).
|
||||||
|
jmethodID method_id = env->GetStaticMethodID(activity, "httpRequest",
|
||||||
|
"(Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;[BLjava/lang/String;)[B");
|
||||||
|
if (method_id == nullptr)
|
||||||
|
{
|
||||||
|
env->ExceptionClear();
|
||||||
|
env->DeleteLocalRef(activity);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
jobjectArray jheaders = nullptr;
|
||||||
|
if (headerPairCount > 0)
|
||||||
|
{
|
||||||
|
// java/lang/String, unlike an app class, resolves from any thread.
|
||||||
|
jclass stringClass = env->FindClass("java/lang/String");
|
||||||
|
if (stringClass == nullptr)
|
||||||
|
{
|
||||||
|
env->ExceptionClear();
|
||||||
|
env->DeleteLocalRef(activity);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
jheaders = env->NewObjectArray((jsize) headerPairCount, stringClass, nullptr);
|
||||||
|
env->DeleteLocalRef(stringClass);
|
||||||
|
if (jheaders == nullptr)
|
||||||
|
{
|
||||||
|
env->ExceptionClear();
|
||||||
|
env->DeleteLocalRef(activity);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < headerPairCount; i++)
|
||||||
|
{
|
||||||
|
jstring field = env->NewStringUTF(headerPairs[i] != nullptr ? headerPairs[i] : "");
|
||||||
|
env->SetObjectArrayElement(jheaders, (jsize) i, field);
|
||||||
|
if (field != nullptr)
|
||||||
|
env->DeleteLocalRef(field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jstring jurl = env->NewStringUTF(url);
|
||||||
|
jstring jmethod = env->NewStringUTF(method != nullptr ? method : "GET");
|
||||||
|
// raw bytes across the bridge, as httpPost does: a request body is JSON
|
||||||
|
// carrying a base64 save, and a jstring would run it through modified UTF-8
|
||||||
|
jbyteArray jbody = nullptr;
|
||||||
|
if (body != nullptr && bodyLen >= 0)
|
||||||
|
{
|
||||||
|
jbody = env->NewByteArray((jsize) bodyLen);
|
||||||
|
if (jbody != nullptr && bodyLen > 0)
|
||||||
|
env->SetByteArrayRegion(jbody, 0, (jsize) bodyLen, (const jbyte*) body);
|
||||||
|
}
|
||||||
|
jstring jua = env->NewStringUTF(userAgent != nullptr ? userAgent : "gen1recomp");
|
||||||
|
|
||||||
|
jobject result = env->CallStaticObjectMethod(activity, method_id, jurl, jmethod,
|
||||||
|
jheaders, jbody, jua);
|
||||||
|
|
||||||
|
env->DeleteLocalRef(jurl);
|
||||||
|
env->DeleteLocalRef(jmethod);
|
||||||
|
if (jheaders != nullptr)
|
||||||
|
env->DeleteLocalRef(jheaders);
|
||||||
|
if (jbody != nullptr)
|
||||||
|
env->DeleteLocalRef(jbody);
|
||||||
|
env->DeleteLocalRef(jua);
|
||||||
|
env->DeleteLocalRef(activity);
|
||||||
|
|
||||||
|
if (result == nullptr)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
jbyteArray bytes = (jbyteArray) result;
|
||||||
|
jsize length = env->GetArrayLength(bytes);
|
||||||
|
if (length > 0)
|
||||||
|
{
|
||||||
|
out.resize((size_t) length);
|
||||||
|
env->GetByteArrayRegion(bytes, 0, length, (jbyte*) &out[0]);
|
||||||
|
}
|
||||||
|
env->DeleteLocalRef(result);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* TLS sockets. Same resolution rule as httpDownload above -- the activity's
|
* TLS sockets. Same resolution rule as httpDownload above -- the activity's
|
||||||
* own class, never FindClass -- and the same tolerance for an old APK: a
|
* own class, never FindClass -- and the same tolerance for an old APK: a
|
||||||
@@ -1320,4 +1521,98 @@ const char *love_android_poll_secondary_touch()
|
|||||||
return event.empty() ? nullptr : event.c_str();
|
return event.empty() ? nullptr : event.c_str();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static love::audio::openal::Audio *love_android_openal_audio()
|
||||||
|
{
|
||||||
|
love::audio::Audio *audio = love::Module::getInstance<love::audio::Audio>(love::Module::M_AUDIO);
|
||||||
|
|
||||||
|
if (audio == nullptr)
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
const char *name = audio->getName();
|
||||||
|
|
||||||
|
if (name == nullptr || strcmp(name, "love.audio.openal") != 0)
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
return (love::audio::openal::Audio *) audio;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" JNIEXPORT void JNICALL
|
||||||
|
Java_org_love2d_android_GameActivity_nativeAudioFocusLost(JNIEnv *env, jclass cls)
|
||||||
|
{
|
||||||
|
(void) env;
|
||||||
|
(void) cls;
|
||||||
|
|
||||||
|
love::audio::openal::pushAudioSuspendEvent();
|
||||||
|
|
||||||
|
love::audio::openal::Audio *audio = love_android_openal_audio();
|
||||||
|
|
||||||
|
if (audio != nullptr)
|
||||||
|
audio->pauseContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" JNIEXPORT void JNICALL
|
||||||
|
Java_org_love2d_android_GameActivity_nativeAudioFocusGained(JNIEnv *env, jclass cls)
|
||||||
|
{
|
||||||
|
(void) env;
|
||||||
|
(void) cls;
|
||||||
|
|
||||||
|
love::audio::openal::Audio *audio = love_android_openal_audio();
|
||||||
|
|
||||||
|
if (audio == nullptr)
|
||||||
|
return;
|
||||||
|
|
||||||
|
audio->resumeContext();
|
||||||
|
|
||||||
|
if (!audio->isDeviceConnected())
|
||||||
|
audio->reopenDevice();
|
||||||
|
|
||||||
|
love::audio::openal::pushAudioResetEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" JNIEXPORT void JNICALL
|
||||||
|
Java_org_love2d_android_GameActivity_nativeAudioDeviceChanged(JNIEnv *env, jclass cls)
|
||||||
|
{
|
||||||
|
(void) env;
|
||||||
|
(void) cls;
|
||||||
|
|
||||||
|
love::audio::openal::Audio *audio = love_android_openal_audio();
|
||||||
|
|
||||||
|
if (audio == nullptr)
|
||||||
|
return;
|
||||||
|
|
||||||
|
audio->pauseContext();
|
||||||
|
audio->reopenDevice();
|
||||||
|
audio->resumeContext();
|
||||||
|
|
||||||
|
love::audio::openal::pushAudioResetEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void pushGameIntentEvent(const char *game)
|
||||||
|
{
|
||||||
|
auto eventmodule = love::Module::getInstance<love::event::Event>(love::Module::M_EVENT);
|
||||||
|
if (eventmodule == nullptr || game == nullptr)
|
||||||
|
return;
|
||||||
|
|
||||||
|
std::vector<love::Variant> args;
|
||||||
|
args.push_back(love::Variant(std::string(game)));
|
||||||
|
|
||||||
|
love::event::Message *msg = new love::event::Message("intent_game", args);
|
||||||
|
eventmodule->push(msg);
|
||||||
|
msg->release();
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" JNIEXPORT void JNICALL
|
||||||
|
Java_org_love2d_android_GameActivity_nativeOnGameIntent(JNIEnv *env, jclass cls, jstring game)
|
||||||
|
{
|
||||||
|
(void) cls;
|
||||||
|
if (game == nullptr)
|
||||||
|
return;
|
||||||
|
const char *str = env->GetStringUTFChars(game, nullptr);
|
||||||
|
if (str != nullptr)
|
||||||
|
{
|
||||||
|
pushGameIntentEvent(str);
|
||||||
|
env->ReleaseStringUTFChars(game, str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#endif // LOVE_ANDROID
|
#endif // LOVE_ANDROID
|
||||||
|
|||||||
@@ -90,6 +90,22 @@ bool syncHealthSteps();
|
|||||||
**/
|
**/
|
||||||
bool restartApp();
|
bool restartApp();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stages a checksum-verified APK from the current save directory and starts
|
||||||
|
* Android's user-confirmed Package Installer flow. Android-only.
|
||||||
|
**/
|
||||||
|
bool installApk(const char *path);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dynamic App Shortcuts: updates Android ShortcutManager with ready game versions.
|
||||||
|
**/
|
||||||
|
bool updateAppShortcuts(const std::vector<std::string> &versions);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the game version requested via initial launch Intent (if any).
|
||||||
|
**/
|
||||||
|
std::string getLaunchGame();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Blocking HTTPS GET into destPath (GameActivity.httpDownload). Android has
|
* Blocking HTTPS GET into destPath (GameActivity.httpDownload). Android has
|
||||||
* no curl binary, so this is the transport src/core/HostShell.lua uses there
|
* no curl binary, so this is the transport src/core/HostShell.lua uses there
|
||||||
@@ -106,6 +122,21 @@ bool httpDownload(const char *url, const char *destPath, const char *userAgent,
|
|||||||
**/
|
**/
|
||||||
bool httpPost(const char *url, const char *body, int bodyLen, const char *contentType, const char *userAgent);
|
bool httpPost(const char *url, const char *body, int bodyLen, const char *contentType, const char *userAgent);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blocking HTTPS request with a method, headers and a byte body
|
||||||
|
* (GameActivity.httpRequest). What save sync needs and neither of the two
|
||||||
|
* above can give it: PUT, per-request auth headers, and the response body of
|
||||||
|
* a 4xx as well as a 2xx. headerPairs is a flat name, value array of
|
||||||
|
* headerPairCount entries; body/userAgent may be null. `out` receives the
|
||||||
|
* Java side's envelope -- a head line of "STATUS <code>" or "ERROR <text>",
|
||||||
|
* a newline, then the raw response bytes. False means the platform has no
|
||||||
|
* such bridge at all (an old APK under a newer liblove), which the Lua side
|
||||||
|
* reports as "update the app" rather than as a failed request.
|
||||||
|
**/
|
||||||
|
bool httpRequest(const char *url, const char *method,
|
||||||
|
const char *const *headerPairs, int headerPairCount,
|
||||||
|
const char *body, int bodyLen, const char *userAgent, std::string &out);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TLS client sockets (GameActivity.tls*, implemented by TlsSocket.java).
|
* TLS client sockets (GameActivity.tls*, implemented by TlsSocket.java).
|
||||||
* LuaSocket, which is what LOVE ships, does TCP only, so wss:// is otherwise
|
* LuaSocket, which is what LOVE ships, does TCP only, so wss:// is otherwise
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
#include "common/delay.h"
|
#include "common/delay.h"
|
||||||
#include "RecordingDevice.h"
|
#include "RecordingDevice.h"
|
||||||
#include "sound/Decoder.h"
|
#include "sound/Decoder.h"
|
||||||
|
#include "event/Event.h"
|
||||||
|
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
@@ -30,6 +31,10 @@
|
|||||||
#include "common/ios.h"
|
#include "common/ios.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifndef ALC_CONNECTED
|
||||||
|
#define ALC_CONNECTED 0x313
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace love
|
namespace love
|
||||||
{
|
{
|
||||||
namespace audio
|
namespace audio
|
||||||
@@ -37,9 +42,35 @@ namespace audio
|
|||||||
namespace openal
|
namespace openal
|
||||||
{
|
{
|
||||||
|
|
||||||
Audio::PoolThread::PoolThread(Pool *pool)
|
static const int DISCONNECT_CHECK_INTERVAL = 200;
|
||||||
: pool(pool)
|
|
||||||
|
static void pushAudioEvent(const char *name)
|
||||||
|
{
|
||||||
|
auto eventmodule = Module::getInstance<event::Event>(Module::M_EVENT);
|
||||||
|
|
||||||
|
if (eventmodule == nullptr)
|
||||||
|
return;
|
||||||
|
|
||||||
|
event::Message *msg = new event::Message(name);
|
||||||
|
eventmodule->push(msg);
|
||||||
|
msg->release();
|
||||||
|
}
|
||||||
|
|
||||||
|
void pushAudioSuspendEvent()
|
||||||
|
{
|
||||||
|
pushAudioEvent("audiosuspend");
|
||||||
|
}
|
||||||
|
|
||||||
|
void pushAudioResetEvent()
|
||||||
|
{
|
||||||
|
pushAudioEvent("audioreset");
|
||||||
|
}
|
||||||
|
|
||||||
|
Audio::PoolThread::PoolThread(Audio *audio, Pool *pool)
|
||||||
|
: audio(audio)
|
||||||
|
, pool(pool)
|
||||||
, finish(false)
|
, finish(false)
|
||||||
|
, paused(false)
|
||||||
{
|
{
|
||||||
threadName = "AudioPool";
|
threadName = "AudioPool";
|
||||||
}
|
}
|
||||||
@@ -51,6 +82,8 @@ Audio::PoolThread::~PoolThread()
|
|||||||
|
|
||||||
void Audio::PoolThread::threadFunction()
|
void Audio::PoolThread::threadFunction()
|
||||||
{
|
{
|
||||||
|
int disconnectCheck = 0;
|
||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
{
|
{
|
||||||
@@ -61,7 +94,23 @@ void Audio::PoolThread::threadFunction()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (paused.load())
|
||||||
|
{
|
||||||
|
disconnectCheck = 0;
|
||||||
|
sleep(5);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
pool->update();
|
pool->update();
|
||||||
|
|
||||||
|
if (audio != nullptr && ++disconnectCheck >= DISCONNECT_CHECK_INTERVAL)
|
||||||
|
{
|
||||||
|
disconnectCheck = 0;
|
||||||
|
|
||||||
|
if (!audio->isDeviceConnected() && audio->reopenDevice())
|
||||||
|
pushAudioResetEvent();
|
||||||
|
}
|
||||||
|
|
||||||
sleep(5);
|
sleep(5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,6 +121,11 @@ void Audio::PoolThread::setFinish()
|
|||||||
finish = true;
|
finish = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Audio::PoolThread::setPaused(bool paused)
|
||||||
|
{
|
||||||
|
this->paused.store(paused);
|
||||||
|
}
|
||||||
|
|
||||||
ALenum Audio::getFormat(int bitDepth, int channels)
|
ALenum Audio::getFormat(int bitDepth, int channels)
|
||||||
{
|
{
|
||||||
if (bitDepth != 8 && bitDepth != 16)
|
if (bitDepth != 8 && bitDepth != 16)
|
||||||
@@ -99,6 +153,8 @@ Audio::Audio()
|
|||||||
, pool(nullptr)
|
, pool(nullptr)
|
||||||
, poolThread(nullptr)
|
, poolThread(nullptr)
|
||||||
, distanceModel(DISTANCE_INVERSE_CLAMPED)
|
, distanceModel(DISTANCE_INVERSE_CLAMPED)
|
||||||
|
, alcReopenDeviceSOFT(nullptr)
|
||||||
|
, reopenChecked(false)
|
||||||
{
|
{
|
||||||
// Before opening new device, check if recording
|
// Before opening new device, check if recording
|
||||||
// is requested.
|
// is requested.
|
||||||
@@ -189,13 +245,6 @@ Audio::Audio()
|
|||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
poolThread = new PoolThread(pool);
|
|
||||||
poolThread->start();
|
|
||||||
|
|
||||||
#ifdef LOVE_IOS
|
|
||||||
love::ios::initAudioSessionInterruptionHandler();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef LOVE_ANDROID
|
#ifdef LOVE_ANDROID
|
||||||
bool hasPauseDeviceExt = alcIsExtensionPresent(device, "ALC_SOFT_pause_device") == ALC_TRUE;
|
bool hasPauseDeviceExt = alcIsExtensionPresent(device, "ALC_SOFT_pause_device") == ALC_TRUE;
|
||||||
alcDevicePauseSOFT = hasPauseDeviceExt
|
alcDevicePauseSOFT = hasPauseDeviceExt
|
||||||
@@ -205,6 +254,13 @@ Audio::Audio()
|
|||||||
? (LPALCDEVICERESUMESOFT) alcGetProcAddress(device, "alcDeviceResumeSOFT")
|
? (LPALCDEVICERESUMESOFT) alcGetProcAddress(device, "alcDeviceResumeSOFT")
|
||||||
: nullptr;
|
: nullptr;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
poolThread = new PoolThread(this, pool);
|
||||||
|
poolThread->start();
|
||||||
|
|
||||||
|
#ifdef LOVE_IOS
|
||||||
|
love::ios::initAudioSessionInterruptionHandler();
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
Audio::~Audio()
|
Audio::~Audio()
|
||||||
@@ -314,6 +370,9 @@ std::vector<love::audio::Source*> Audio::pause()
|
|||||||
|
|
||||||
void Audio::pauseContext()
|
void Audio::pauseContext()
|
||||||
{
|
{
|
||||||
|
if (poolThread != nullptr)
|
||||||
|
poolThread->setPaused(true);
|
||||||
|
|
||||||
#ifdef LOVE_ANDROID
|
#ifdef LOVE_ANDROID
|
||||||
if (alcDevicePauseSOFT)
|
if (alcDevicePauseSOFT)
|
||||||
alcDevicePauseSOFT(device);
|
alcDevicePauseSOFT(device);
|
||||||
@@ -350,6 +409,52 @@ void Audio::resumeContext()
|
|||||||
if (context && alcGetCurrentContext() != context)
|
if (context && alcGetCurrentContext() != context)
|
||||||
alcMakeContextCurrent(context);
|
alcMakeContextCurrent(context);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
if (poolThread != nullptr)
|
||||||
|
poolThread->setPaused(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Audio::reopenDevice()
|
||||||
|
{
|
||||||
|
if (device == nullptr)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
thread::Lock lock(deviceMutex);
|
||||||
|
|
||||||
|
if (!reopenChecked)
|
||||||
|
{
|
||||||
|
reopenChecked = true;
|
||||||
|
|
||||||
|
if (alcIsExtensionPresent(device, "ALC_SOFT_reopen_device") == ALC_TRUE)
|
||||||
|
alcReopenDeviceSOFT = (LPALCREOPENDEVICESOFT) alcGetProcAddress(device, "alcReopenDeviceSOFT");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alcReopenDeviceSOFT == nullptr)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
alcGetError(device);
|
||||||
|
|
||||||
|
return alcReopenDeviceSOFT(device, nullptr, nullptr) == ALC_TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Audio::isDeviceConnected()
|
||||||
|
{
|
||||||
|
if (device == nullptr)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
thread::Lock lock(deviceMutex);
|
||||||
|
|
||||||
|
if (alcIsExtensionPresent(device, "ALC_EXT_disconnect") != ALC_TRUE)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
ALCint connected = 1;
|
||||||
|
alcGetError(device);
|
||||||
|
alcGetIntegerv(device, ALC_CONNECTED, 1, &connected);
|
||||||
|
|
||||||
|
if (alcGetError(device) != ALC_NO_ERROR)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return connected != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Audio::setVolume(float volume)
|
void Audio::setVolume(float volume)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
#define LOVE_AUDIO_OPENAL_AUDIO_H
|
#define LOVE_AUDIO_OPENAL_AUDIO_H
|
||||||
|
|
||||||
// STD
|
// STD
|
||||||
|
#include <atomic>
|
||||||
#include <queue>
|
#include <queue>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -97,6 +98,8 @@ public:
|
|||||||
std::vector<love::audio::Source*> pause();
|
std::vector<love::audio::Source*> pause();
|
||||||
void pauseContext();
|
void pauseContext();
|
||||||
void resumeContext();
|
void resumeContext();
|
||||||
|
bool reopenDevice();
|
||||||
|
bool isDeviceConnected();
|
||||||
void setVolume(float volume);
|
void setVolume(float volume);
|
||||||
float getVolume() const;
|
float getVolume() const;
|
||||||
|
|
||||||
@@ -155,6 +158,7 @@ private:
|
|||||||
class PoolThread: public thread::Threadable
|
class PoolThread: public thread::Threadable
|
||||||
{
|
{
|
||||||
protected:
|
protected:
|
||||||
|
Audio *audio;
|
||||||
Pool *pool;
|
Pool *pool;
|
||||||
|
|
||||||
// Set this to true when the thread should finish.
|
// Set this to true when the thread should finish.
|
||||||
@@ -162,13 +166,16 @@ private:
|
|||||||
// will read from it.
|
// will read from it.
|
||||||
volatile bool finish;
|
volatile bool finish;
|
||||||
|
|
||||||
|
std::atomic<bool> paused;
|
||||||
|
|
||||||
// finish lock
|
// finish lock
|
||||||
love::thread::MutexRef mutex;
|
love::thread::MutexRef mutex;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
PoolThread(Pool *pool);
|
PoolThread(Audio *audio, Pool *pool);
|
||||||
virtual ~PoolThread();
|
virtual ~PoolThread();
|
||||||
void setFinish();
|
void setFinish();
|
||||||
|
void setPaused(bool paused);
|
||||||
void threadFunction();
|
void threadFunction();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -177,6 +184,13 @@ private:
|
|||||||
DistanceModel distanceModel;
|
DistanceModel distanceModel;
|
||||||
//float metersPerUnit = 1.0;
|
//float metersPerUnit = 1.0;
|
||||||
|
|
||||||
|
#ifndef ALC_SOFT_reopen_device
|
||||||
|
typedef ALCboolean (ALC_APIENTRY*LPALCREOPENDEVICESOFT)(ALCdevice *device, const ALCchar *deviceName, const ALCint *attribs);
|
||||||
|
#endif
|
||||||
|
LPALCREOPENDEVICESOFT alcReopenDeviceSOFT;
|
||||||
|
bool reopenChecked;
|
||||||
|
love::thread::MutexRef deviceMutex;
|
||||||
|
|
||||||
#ifdef LOVE_ANDROID
|
#ifdef LOVE_ANDROID
|
||||||
# ifndef ALC_SOFT_pause_device
|
# ifndef ALC_SOFT_pause_device
|
||||||
typedef void (ALC_APIENTRY*LPALCDEVICEPAUSESOFT)(ALCdevice *device);
|
typedef void (ALC_APIENTRY*LPALCDEVICEPAUSESOFT)(ALCdevice *device);
|
||||||
@@ -188,6 +202,9 @@ private:
|
|||||||
#endif
|
#endif
|
||||||
}; // Audio
|
}; // Audio
|
||||||
|
|
||||||
|
void pushAudioSuspendEvent();
|
||||||
|
void pushAudioResetEvent();
|
||||||
|
|
||||||
#ifdef ALC_EXT_EFX
|
#ifdef ALC_EXT_EFX
|
||||||
// Effect objects
|
// Effect objects
|
||||||
extern LPALGENEFFECTS alGenEffects;
|
extern LPALGENEFFECTS alGenEffects;
|
||||||
|
|||||||
@@ -245,6 +245,35 @@ bool System::restartApp() const
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool System::installApk(const char *path) const
|
||||||
|
{
|
||||||
|
#ifdef LOVE_ANDROID
|
||||||
|
return love::android::installApk(path);
|
||||||
|
#else
|
||||||
|
LOVE_UNUSED(path);
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool System::updateShortcuts(const std::vector<std::string> &versions) const
|
||||||
|
{
|
||||||
|
#ifdef LOVE_ANDROID
|
||||||
|
return love::android::updateAppShortcuts(versions);
|
||||||
|
#else
|
||||||
|
LOVE_UNUSED(versions);
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string System::getLaunchGame() const
|
||||||
|
{
|
||||||
|
#ifdef LOVE_ANDROID
|
||||||
|
return love::android::getLaunchGame();
|
||||||
|
#else
|
||||||
|
return "";
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
bool System::httpDownload(const char *url, const char *destPath,
|
bool System::httpDownload(const char *url, const char *destPath,
|
||||||
const char *userAgent, const char *accept) const
|
const char *userAgent, const char *accept) const
|
||||||
{
|
{
|
||||||
@@ -274,6 +303,27 @@ bool System::httpPost(const char *url, const char *body, int bodyLen,
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool System::httpRequest(const char *url, const char *method,
|
||||||
|
const char *const *headerPairs, int headerPairCount,
|
||||||
|
const char *body, int bodyLen, const char *userAgent,
|
||||||
|
std::string &out) const
|
||||||
|
{
|
||||||
|
#ifdef LOVE_ANDROID
|
||||||
|
return love::android::httpRequest(url, method, headerPairs, headerPairCount,
|
||||||
|
body, bodyLen, userAgent, out);
|
||||||
|
#else
|
||||||
|
LOVE_UNUSED(url);
|
||||||
|
LOVE_UNUSED(method);
|
||||||
|
LOVE_UNUSED(headerPairs);
|
||||||
|
LOVE_UNUSED(headerPairCount);
|
||||||
|
LOVE_UNUSED(body);
|
||||||
|
LOVE_UNUSED(bodyLen);
|
||||||
|
LOVE_UNUSED(userAgent);
|
||||||
|
out.clear();
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
int System::tlsOpen(const char *host, int port) const
|
int System::tlsOpen(const char *host, int port) const
|
||||||
{
|
{
|
||||||
#ifdef LOVE_ANDROID
|
#ifdef LOVE_ANDROID
|
||||||
|
|||||||
@@ -143,6 +143,12 @@ public:
|
|||||||
**/
|
**/
|
||||||
virtual bool restartApp() const;
|
virtual bool restartApp() const;
|
||||||
|
|
||||||
|
/** Starts Android's user-confirmed install flow for a verified APK. */
|
||||||
|
virtual bool installApk(const char *path) const;
|
||||||
|
|
||||||
|
virtual bool updateShortcuts(const std::vector<std::string> &versions) const;
|
||||||
|
virtual std::string getLaunchGame() const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Blocking HTTPS GET into an absolute host path (Android only; false
|
* Blocking HTTPS GET into an absolute host path (Android only; false
|
||||||
* elsewhere). Android has no curl, which is what every other platform
|
* elsewhere). Android has no curl, which is what every other platform
|
||||||
@@ -159,6 +165,18 @@ public:
|
|||||||
virtual bool httpPost(const char *url, const char *body, int bodyLen,
|
virtual bool httpPost(const char *url, const char *body, int bodyLen,
|
||||||
const char *contentType = nullptr, const char *userAgent = nullptr) const;
|
const char *contentType = nullptr, const char *userAgent = nullptr) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blocking HTTPS request with a method, headers and a byte body (Android
|
||||||
|
* only; false elsewhere). Save sync needs PUT, auth headers and the body
|
||||||
|
* of a 4xx, none of which the two bridges above can express. headerPairs
|
||||||
|
* is a flat name, value array; `out` receives the response envelope
|
||||||
|
* ("STATUS <code>" or "ERROR <text>", a newline, then the raw body).
|
||||||
|
**/
|
||||||
|
virtual bool httpRequest(const char *url, const char *method,
|
||||||
|
const char *const *headerPairs, int headerPairCount,
|
||||||
|
const char *body, int bodyLen, const char *userAgent,
|
||||||
|
std::string &out) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TLS client sockets (Android only; every call fails elsewhere, where
|
* TLS client sockets (Android only; every call fails elsewhere, where
|
||||||
* LuaSec or another provider is the answer). Non-blocking by contract:
|
* LuaSec or another provider is the answer). Non-blocking by contract:
|
||||||
|
|||||||
@@ -22,6 +22,9 @@
|
|||||||
#include "wrap_System.h"
|
#include "wrap_System.h"
|
||||||
#include "sdl/System.h"
|
#include "sdl/System.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace love
|
namespace love
|
||||||
{
|
{
|
||||||
namespace system
|
namespace system
|
||||||
@@ -129,6 +132,13 @@ int w_restartApp(lua_State *L)
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int w_installApk(lua_State *L)
|
||||||
|
{
|
||||||
|
const char *path = luaL_checkstring(L, 1);
|
||||||
|
luax_pushboolean(L, instance()->installApk(path));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
int w_httpDownload(lua_State *L)
|
int w_httpDownload(lua_State *L)
|
||||||
{
|
{
|
||||||
const char *url = luaL_checkstring(L, 1);
|
const char *url = luaL_checkstring(L, 1);
|
||||||
@@ -150,6 +160,57 @@ int w_httpPost(lua_State *L)
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* love.system.httpRequest(url, method, headers, body, userAgent) -> envelope
|
||||||
|
*
|
||||||
|
* `headers` is a flat array of alternating header name and value strings, so
|
||||||
|
* it maps straight onto the Java bridge's String[] without any parsing here.
|
||||||
|
* The single return is the response envelope -- a head line of
|
||||||
|
* "STATUS <code>" or "ERROR <text>", a newline, then the raw body -- or nil
|
||||||
|
* where the build has no bridge, which src/core/HostShell.lua turns into an
|
||||||
|
* "update the app" notice rather than a failed request.
|
||||||
|
*/
|
||||||
|
int w_httpRequest(lua_State *L)
|
||||||
|
{
|
||||||
|
const char *url = luaL_checkstring(L, 1);
|
||||||
|
const char *method = luaL_optstring(L, 2, "GET");
|
||||||
|
|
||||||
|
std::vector<std::string> fields;
|
||||||
|
if (!lua_isnoneornil(L, 3))
|
||||||
|
{
|
||||||
|
luaL_checktype(L, 3, LUA_TTABLE);
|
||||||
|
size_t count = luax_objlen(L, 3);
|
||||||
|
for (size_t i = 1; i <= count; i++)
|
||||||
|
{
|
||||||
|
lua_rawgeti(L, 3, (int) i);
|
||||||
|
const char *field = lua_tostring(L, -1);
|
||||||
|
fields.push_back(field != nullptr ? field : "");
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::vector<const char *> pairs;
|
||||||
|
for (size_t i = 0; i < fields.size(); i++)
|
||||||
|
pairs.push_back(fields[i].c_str());
|
||||||
|
|
||||||
|
size_t bodyLen = 0;
|
||||||
|
const char *body = nullptr;
|
||||||
|
if (!lua_isnoneornil(L, 4))
|
||||||
|
body = luaL_checklstring(L, 4, &bodyLen);
|
||||||
|
const char *ua = luaL_optstring(L, 5, nullptr);
|
||||||
|
|
||||||
|
std::string out;
|
||||||
|
bool ok = instance()->httpRequest(url, method,
|
||||||
|
pairs.empty() ? nullptr : &pairs[0], (int) pairs.size(),
|
||||||
|
body, (int) bodyLen, ua, out);
|
||||||
|
if (!ok)
|
||||||
|
{
|
||||||
|
lua_pushnil(L);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
lua_pushlstring(L, out.data(), out.size());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
int w_hasBackgroundMusic(lua_State *L)
|
int w_hasBackgroundMusic(lua_State *L)
|
||||||
{
|
{
|
||||||
lua_pushboolean(L, instance()->hasBackgroundMusic());
|
lua_pushboolean(L, instance()->hasBackgroundMusic());
|
||||||
@@ -229,6 +290,34 @@ int w_tlsClose(lua_State *L)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int w_updateShortcuts(lua_State *L)
|
||||||
|
{
|
||||||
|
if (!lua_istable(L, 1))
|
||||||
|
return luaL_error(L, "Expected table of game version strings");
|
||||||
|
|
||||||
|
std::vector<std::string> versions;
|
||||||
|
int len = (int) luax_objlen(L, 1);
|
||||||
|
for (int i = 1; i <= len; ++i)
|
||||||
|
{
|
||||||
|
lua_rawgeti(L, 1, i);
|
||||||
|
if (lua_isstring(L, -1))
|
||||||
|
versions.push_back(lua_tostring(L, -1));
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
luax_pushboolean(L, instance()->updateShortcuts(versions));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int w_getLaunchGame(lua_State *L)
|
||||||
|
{
|
||||||
|
std::string game = instance()->getLaunchGame();
|
||||||
|
if (game.empty())
|
||||||
|
lua_pushnil(L);
|
||||||
|
else
|
||||||
|
luax_pushstring(L, game);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
static const luaL_Reg functions[] =
|
static const luaL_Reg functions[] =
|
||||||
{
|
{
|
||||||
{ "getOS", w_getOS },
|
{ "getOS", w_getOS },
|
||||||
@@ -243,8 +332,12 @@ static const luaL_Reg functions[] =
|
|||||||
{ "createFile", w_createFile },
|
{ "createFile", w_createFile },
|
||||||
{ "syncHealthSteps", w_syncHealthSteps },
|
{ "syncHealthSteps", w_syncHealthSteps },
|
||||||
{ "restartApp", w_restartApp },
|
{ "restartApp", w_restartApp },
|
||||||
|
{ "installApk", w_installApk },
|
||||||
|
{ "updateShortcuts", w_updateShortcuts },
|
||||||
|
{ "getLaunchGame", w_getLaunchGame },
|
||||||
{ "httpDownload", w_httpDownload },
|
{ "httpDownload", w_httpDownload },
|
||||||
{ "httpPost", w_httpPost },
|
{ "httpPost", w_httpPost },
|
||||||
|
{ "httpRequest", w_httpRequest },
|
||||||
{ "tlsOpen", w_tlsOpen },
|
{ "tlsOpen", w_tlsOpen },
|
||||||
{ "tlsStatus", w_tlsStatus },
|
{ "tlsStatus", w_tlsStatus },
|
||||||
{ "tlsSend", w_tlsSend },
|
{ "tlsSend", w_tlsSend },
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve Oboe
|
||||||
|
title: ''
|
||||||
|
labels: bug
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Android version(s):
|
||||||
|
Android device(s):
|
||||||
|
Oboe version:
|
||||||
|
App name used for testing:
|
||||||
|
(Please try to reproduce the issue using the OboeTester or an Oboe sample.)
|
||||||
|
|
||||||
|
**Short description**
|
||||||
|
(Please only report one bug per Issue. Do not combine multiple bugs.)
|
||||||
|
|
||||||
|
**Steps to reproduce**
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
|
||||||
|
**Actual behavior**
|
||||||
|
|
||||||
|
**Device**
|
||||||
|
|
||||||
|
Please list which devices have this bug.
|
||||||
|
If device specific, and you are on Linux or a Macintosh, connect the device and please share the result for the following script. This gets properties of the device.
|
||||||
|
|
||||||
|
```
|
||||||
|
for p in \
|
||||||
|
ro.product.brand ro.product.manufacturer ro.product.model \
|
||||||
|
ro.product.device ro.product.cpu.abi ro.build.description \
|
||||||
|
ro.hardware ro.hardware.chipname ro.arch "| grep aaudio";
|
||||||
|
do echo "$p = $(adb shell getprop $p)"; done
|
||||||
|
```
|
||||||
|
|
||||||
|
**Any additional context**
|
||||||
|
|
||||||
|
If applicable, please attach a few seconds of an uncompressed recording of the sound in a WAV or AIFF file.
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# To get started with Dependabot version updates, you'll need to specify which
|
||||||
|
# package ecosystems to update and where the package manifests are located.
|
||||||
|
# Please see the documentation for all configuration options:
|
||||||
|
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
#Workaround for https://github.com/dependabot/dependabot-core/issues/6888#issuecomment-1539501116
|
||||||
|
registries:
|
||||||
|
maven-google:
|
||||||
|
type: maven-repository
|
||||||
|
url: "https://dl.google.com/dl/android/maven2/"
|
||||||
|
|
||||||
|
updates:
|
||||||
|
#Check for updates to Github Actions
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/" #Location of package manifests
|
||||||
|
target-branch: "main"
|
||||||
|
open-pull-requests-limit: 5
|
||||||
|
labels:
|
||||||
|
- "dependencies"
|
||||||
|
- "dependencies/github-actions"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
|
||||||
|
#Check updates for Gradle dependencies
|
||||||
|
- package-ecosystem: "gradle"
|
||||||
|
registries:
|
||||||
|
- maven-google
|
||||||
|
directory: "/" #Location of package manifests
|
||||||
|
target-branch: "main"
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
labels:
|
||||||
|
- "dependencies"
|
||||||
|
- "dependencies/gradle"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
name: Build CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: set up JDK 17
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'temurin'
|
||||||
|
java-version: 17
|
||||||
|
- name: build samples and apps
|
||||||
|
uses: github/codeql-action/init@v3
|
||||||
|
with:
|
||||||
|
languages: cpp
|
||||||
|
- run: |
|
||||||
|
pushd samples
|
||||||
|
chmod +x gradlew
|
||||||
|
./gradlew -q clean bundleDebug
|
||||||
|
popd
|
||||||
|
pushd apps/OboeTester
|
||||||
|
chmod +x gradlew
|
||||||
|
./gradlew -q clean bundleDebug
|
||||||
|
popd
|
||||||
|
pushd apps/fxlab
|
||||||
|
chmod +x gradlew
|
||||||
|
./gradlew -q clean bundleDebug
|
||||||
|
popd
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v3
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
name: Update Docs
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Doxygen Action
|
||||||
|
uses: mattnotmitt/doxygen-action@v1.9.8
|
||||||
|
with:
|
||||||
|
doxyfile-path: "./Doxyfile"
|
||||||
|
working-directory: "."
|
||||||
|
|
||||||
|
- name: Deploy
|
||||||
|
uses: peaceiris/actions-gh-pages@v4
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
publish_dir: ./docs/reference
|
||||||
@@ -4,3 +4,5 @@
|
|||||||
.cxx/
|
.cxx/
|
||||||
.idea
|
.idea
|
||||||
build
|
build
|
||||||
|
.logpile
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +1,21 @@
|
|||||||
LOCAL_PATH:= $(call my-dir)
|
LOCAL_PATH:= $(call my-dir)
|
||||||
include $(CLEAR_VARS)
|
include $(CLEAR_VARS)
|
||||||
|
|
||||||
#
|
|
||||||
# Module
|
|
||||||
#
|
|
||||||
LOCAL_MODULE := oboe
|
LOCAL_MODULE := oboe
|
||||||
LOCAL_ARM_NEON := true
|
LOCAL_ARM_NEON := true
|
||||||
|
|
||||||
#
|
|
||||||
# Flags
|
|
||||||
#
|
|
||||||
LOCAL_CFLAGS := -Wall -Wextra-semi -Wshadow -Wshadow-field
|
LOCAL_CFLAGS := -Wall -Wextra-semi -Wshadow -Wshadow-field
|
||||||
LOCAL_CPPFLAGS := -std=c++14
|
LOCAL_CPPFLAGS := -std=c++17
|
||||||
|
|
||||||
#
|
|
||||||
# Include paths
|
|
||||||
#
|
|
||||||
LOCAL_C_INCLUDES := \
|
LOCAL_C_INCLUDES := \
|
||||||
$(LOCAL_PATH)/include \
|
$(LOCAL_PATH)/include \
|
||||||
$(LOCAL_PATH)/src
|
$(LOCAL_PATH)/src
|
||||||
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
|
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
|
||||||
|
|
||||||
#
|
|
||||||
# Source files
|
|
||||||
#
|
|
||||||
LOCAL_SRC_FILES := \
|
LOCAL_SRC_FILES := \
|
||||||
src/aaudio/AAudioLoader.cpp \
|
src/aaudio/AAudioLoader.cpp \
|
||||||
src/aaudio/AudioStreamAAudio.cpp \
|
src/aaudio/AudioStreamAAudio.cpp \
|
||||||
|
src/common/AdpfWrapper.cpp \
|
||||||
src/common/AudioSourceCaller.cpp \
|
src/common/AudioSourceCaller.cpp \
|
||||||
src/common/AudioStream.cpp \
|
src/common/AudioStream.cpp \
|
||||||
src/common/AudioStreamBuilder.cpp \
|
src/common/AudioStreamBuilder.cpp \
|
||||||
@@ -36,8 +25,11 @@ LOCAL_SRC_FILES := \
|
|||||||
src/common/FixedBlockReader.cpp \
|
src/common/FixedBlockReader.cpp \
|
||||||
src/common/FixedBlockWriter.cpp \
|
src/common/FixedBlockWriter.cpp \
|
||||||
src/common/LatencyTuner.cpp \
|
src/common/LatencyTuner.cpp \
|
||||||
|
src/common/OboeExtensions.cpp \
|
||||||
src/common/SourceFloatCaller.cpp \
|
src/common/SourceFloatCaller.cpp \
|
||||||
src/common/SourceI16Caller.cpp \
|
src/common/SourceI16Caller.cpp \
|
||||||
|
src/common/SourceI24Caller.cpp \
|
||||||
|
src/common/SourceI32Caller.cpp \
|
||||||
src/common/Utilities.cpp \
|
src/common/Utilities.cpp \
|
||||||
src/common/QuirksManager.cpp \
|
src/common/QuirksManager.cpp \
|
||||||
src/fifo/FifoBuffer.cpp \
|
src/fifo/FifoBuffer.cpp \
|
||||||
@@ -45,17 +37,26 @@ LOCAL_SRC_FILES := \
|
|||||||
src/fifo/FifoControllerBase.cpp \
|
src/fifo/FifoControllerBase.cpp \
|
||||||
src/fifo/FifoControllerIndirect.cpp \
|
src/fifo/FifoControllerIndirect.cpp \
|
||||||
src/flowgraph/FlowGraphNode.cpp \
|
src/flowgraph/FlowGraphNode.cpp \
|
||||||
|
src/flowgraph/ChannelCountConverter.cpp \
|
||||||
src/flowgraph/ClipToRange.cpp \
|
src/flowgraph/ClipToRange.cpp \
|
||||||
|
src/flowgraph/Limiter.cpp \
|
||||||
src/flowgraph/ManyToMultiConverter.cpp \
|
src/flowgraph/ManyToMultiConverter.cpp \
|
||||||
|
src/flowgraph/MonoBlend.cpp \
|
||||||
src/flowgraph/MonoToMultiConverter.cpp \
|
src/flowgraph/MonoToMultiConverter.cpp \
|
||||||
|
src/flowgraph/MultiToManyConverter.cpp \
|
||||||
|
src/flowgraph/MultiToMonoConverter.cpp \
|
||||||
src/flowgraph/RampLinear.cpp \
|
src/flowgraph/RampLinear.cpp \
|
||||||
src/flowgraph/SampleRateConverter.cpp \
|
src/flowgraph/SampleRateConverter.cpp \
|
||||||
src/flowgraph/SinkFloat.cpp \
|
src/flowgraph/SinkFloat.cpp \
|
||||||
src/flowgraph/SinkI16.cpp \
|
src/flowgraph/SinkI16.cpp \
|
||||||
src/flowgraph/SinkI24.cpp \
|
src/flowgraph/SinkI24.cpp \
|
||||||
|
src/flowgraph/SinkI32.cpp \
|
||||||
|
src/flowgraph/SinkI8_24.cpp \
|
||||||
src/flowgraph/SourceFloat.cpp \
|
src/flowgraph/SourceFloat.cpp \
|
||||||
src/flowgraph/SourceI16.cpp \
|
src/flowgraph/SourceI16.cpp \
|
||||||
src/flowgraph/SourceI24.cpp \
|
src/flowgraph/SourceI24.cpp \
|
||||||
|
src/flowgraph/SourceI32.cpp \
|
||||||
|
src/flowgraph/SourceI8_24.cpp \
|
||||||
src/flowgraph/resampler/IntegerRatio.cpp \
|
src/flowgraph/resampler/IntegerRatio.cpp \
|
||||||
src/flowgraph/resampler/LinearResampler.cpp \
|
src/flowgraph/resampler/LinearResampler.cpp \
|
||||||
src/flowgraph/resampler/MultiChannelResampler.cpp \
|
src/flowgraph/resampler/MultiChannelResampler.cpp \
|
||||||
@@ -75,10 +76,6 @@ LOCAL_SRC_FILES := \
|
|||||||
src/common/Trace.cpp \
|
src/common/Trace.cpp \
|
||||||
src/common/Version.cpp
|
src/common/Version.cpp
|
||||||
|
|
||||||
#
|
|
||||||
# Libraries related
|
|
||||||
#
|
|
||||||
LOCAL_LDLIBS := -llog
|
LOCAL_LDLIBS := -llog
|
||||||
|
|
||||||
# Build
|
|
||||||
include $(BUILD_STATIC_LIBRARY)
|
include $(BUILD_STATIC_LIBRARY)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ project(oboe)
|
|||||||
set (oboe_sources
|
set (oboe_sources
|
||||||
src/aaudio/AAudioLoader.cpp
|
src/aaudio/AAudioLoader.cpp
|
||||||
src/aaudio/AudioStreamAAudio.cpp
|
src/aaudio/AudioStreamAAudio.cpp
|
||||||
|
src/common/AdpfWrapper.cpp
|
||||||
src/common/AudioSourceCaller.cpp
|
src/common/AudioSourceCaller.cpp
|
||||||
src/common/AudioStream.cpp
|
src/common/AudioStream.cpp
|
||||||
src/common/AudioStreamBuilder.cpp
|
src/common/AudioStreamBuilder.cpp
|
||||||
@@ -18,8 +19,11 @@ set (oboe_sources
|
|||||||
src/common/FixedBlockReader.cpp
|
src/common/FixedBlockReader.cpp
|
||||||
src/common/FixedBlockWriter.cpp
|
src/common/FixedBlockWriter.cpp
|
||||||
src/common/LatencyTuner.cpp
|
src/common/LatencyTuner.cpp
|
||||||
|
src/common/OboeExtensions.cpp
|
||||||
src/common/SourceFloatCaller.cpp
|
src/common/SourceFloatCaller.cpp
|
||||||
src/common/SourceI16Caller.cpp
|
src/common/SourceI16Caller.cpp
|
||||||
|
src/common/SourceI24Caller.cpp
|
||||||
|
src/common/SourceI32Caller.cpp
|
||||||
src/common/Utilities.cpp
|
src/common/Utilities.cpp
|
||||||
src/common/QuirksManager.cpp
|
src/common/QuirksManager.cpp
|
||||||
src/fifo/FifoBuffer.cpp
|
src/fifo/FifoBuffer.cpp
|
||||||
@@ -27,17 +31,26 @@ set (oboe_sources
|
|||||||
src/fifo/FifoControllerBase.cpp
|
src/fifo/FifoControllerBase.cpp
|
||||||
src/fifo/FifoControllerIndirect.cpp
|
src/fifo/FifoControllerIndirect.cpp
|
||||||
src/flowgraph/FlowGraphNode.cpp
|
src/flowgraph/FlowGraphNode.cpp
|
||||||
|
src/flowgraph/ChannelCountConverter.cpp
|
||||||
src/flowgraph/ClipToRange.cpp
|
src/flowgraph/ClipToRange.cpp
|
||||||
|
src/flowgraph/Limiter.cpp
|
||||||
src/flowgraph/ManyToMultiConverter.cpp
|
src/flowgraph/ManyToMultiConverter.cpp
|
||||||
|
src/flowgraph/MonoBlend.cpp
|
||||||
src/flowgraph/MonoToMultiConverter.cpp
|
src/flowgraph/MonoToMultiConverter.cpp
|
||||||
|
src/flowgraph/MultiToManyConverter.cpp
|
||||||
|
src/flowgraph/MultiToMonoConverter.cpp
|
||||||
src/flowgraph/RampLinear.cpp
|
src/flowgraph/RampLinear.cpp
|
||||||
src/flowgraph/SampleRateConverter.cpp
|
src/flowgraph/SampleRateConverter.cpp
|
||||||
src/flowgraph/SinkFloat.cpp
|
src/flowgraph/SinkFloat.cpp
|
||||||
src/flowgraph/SinkI16.cpp
|
src/flowgraph/SinkI16.cpp
|
||||||
src/flowgraph/SinkI24.cpp
|
src/flowgraph/SinkI24.cpp
|
||||||
|
src/flowgraph/SinkI32.cpp
|
||||||
|
src/flowgraph/SinkI8_24.cpp
|
||||||
src/flowgraph/SourceFloat.cpp
|
src/flowgraph/SourceFloat.cpp
|
||||||
src/flowgraph/SourceI16.cpp
|
src/flowgraph/SourceI16.cpp
|
||||||
src/flowgraph/SourceI24.cpp
|
src/flowgraph/SourceI24.cpp
|
||||||
|
src/flowgraph/SourceI32.cpp
|
||||||
|
src/flowgraph/SourceI8_24.cpp
|
||||||
src/flowgraph/resampler/IntegerRatio.cpp
|
src/flowgraph/resampler/IntegerRatio.cpp
|
||||||
src/flowgraph/resampler/LinearResampler.cpp
|
src/flowgraph/resampler/LinearResampler.cpp
|
||||||
src/flowgraph/resampler/MultiChannelResampler.cpp
|
src/flowgraph/resampler/MultiChannelResampler.cpp
|
||||||
@@ -70,18 +83,23 @@ target_include_directories(oboe
|
|||||||
# Enable -Ofast
|
# Enable -Ofast
|
||||||
target_compile_options(oboe
|
target_compile_options(oboe
|
||||||
PRIVATE
|
PRIVATE
|
||||||
-std=c++14
|
-std=c++17
|
||||||
-Wall
|
-Wall
|
||||||
-Wextra-semi
|
-Wextra-semi
|
||||||
-Wshadow
|
-Wshadow
|
||||||
-Wshadow-field
|
-Wshadow-field
|
||||||
-Ofast
|
"$<$<CONFIG:RELEASE>:-Ofast>"
|
||||||
|
"$<$<CONFIG:DEBUG>:-O3>"
|
||||||
"$<$<CONFIG:DEBUG>:-Werror>")
|
"$<$<CONFIG:DEBUG>:-Werror>")
|
||||||
|
|
||||||
# Enable logging of D,V for debug builds
|
# Enable logging of D,V for debug builds
|
||||||
target_compile_definitions(oboe PUBLIC $<$<CONFIG:DEBUG>:OBOE_ENABLE_LOGGING=1>)
|
target_compile_definitions(oboe PUBLIC $<$<CONFIG:DEBUG>:OBOE_ENABLE_LOGGING=1>)
|
||||||
|
|
||||||
|
option(OBOE_DO_NOT_DEFINE_OPENSL_ES_CONSTANTS "Do not define OpenSLES constants" OFF)
|
||||||
|
target_compile_definitions(oboe PRIVATE $<$<BOOL:${OBOE_DO_NOT_DEFINE_OPENSL_ES_CONSTANTS}>:DO_NOT_DEFINE_OPENSL_ES_CONSTANTS=1>)
|
||||||
|
|
||||||
target_link_libraries(oboe PRIVATE log OpenSLES)
|
target_link_libraries(oboe PRIVATE log OpenSLES)
|
||||||
|
target_link_options(oboe PRIVATE "-Wl,-z,max-page-size=16384")
|
||||||
|
|
||||||
# When installing oboe put the libraries in the lib/<ABI> folder e.g. lib/arm64-v8a
|
# When installing oboe put the libraries in the lib/<ABI> folder e.g. lib/arm64-v8a
|
||||||
install(TARGETS oboe
|
install(TARGETS oboe
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
Please see the CONTRIBUTING.md file for more information.
|
|
||||||
@@ -38,7 +38,7 @@ PROJECT_NAME = "Oboe"
|
|||||||
# could be handy for archiving the generated documentation or if some version
|
# could be handy for archiving the generated documentation or if some version
|
||||||
# control system is used.
|
# control system is used.
|
||||||
|
|
||||||
PROJECT_NUMBER = 1.2
|
PROJECT_NUMBER =
|
||||||
|
|
||||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
||||||
# for a project that appears at the top of each page and should give viewer a
|
# for a project that appears at the top of each page and should give viewer a
|
||||||
@@ -58,7 +58,7 @@ PROJECT_LOGO =
|
|||||||
# entered, it will be relative to the location where doxygen was started. If
|
# entered, it will be relative to the location where doxygen was started. If
|
||||||
# left blank the current directory will be used.
|
# left blank the current directory will be used.
|
||||||
|
|
||||||
OUTPUT_DIRECTORY = docs
|
OUTPUT_DIRECTORY = ./docs
|
||||||
|
|
||||||
# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub-
|
# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub-
|
||||||
# directories (in 2 levels) under the output directory of each output format and
|
# directories (in 2 levels) under the output directory of each output format and
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
Please see the README.md file for more information.
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Oboe [](https://travis-ci.org/google/oboe)
|
# Oboe [](https://github.com/google/oboe/actions)
|
||||||
|
|
||||||
[](https://www.youtube.com/watch?v=csfHAbr5ilI&list=PLWz5rJ2EKKc_duWv9IPNvx9YBudNMmLSa)
|
[](https://www.youtube.com/watch?v=csfHAbr5ilI&list=PLWz5rJ2EKKc_duWv9IPNvx9YBudNMmLSa)
|
||||||
|
|
||||||
@@ -9,35 +9,36 @@ Oboe is a C++ library which makes it easy to build high-performance audio apps o
|
|||||||
- Chooses the audio API (OpenSL ES on API 16+ or AAudio on API 27+) which will give the best audio performance on the target Android device
|
- Chooses the audio API (OpenSL ES on API 16+ or AAudio on API 27+) which will give the best audio performance on the target Android device
|
||||||
- Automatic latency tuning
|
- Automatic latency tuning
|
||||||
- Modern C++ allowing you to write clean, elegant code
|
- Modern C++ allowing you to write clean, elegant code
|
||||||
- [Used by popular apps and frameworks](docs/AppsUsingOboe.md)
|
- Workarounds for some known issues
|
||||||
|
- [Used by popular apps and frameworks](https://github.com/google/oboe/wiki/AppsUsingOboe)
|
||||||
|
|
||||||
## Requirements
|
## Documentation
|
||||||
To build Oboe you'll need a compiler which supports C++14 and the Android header files. The easiest way to obtain these is by downloading the Android NDK r17 or above. It can be installed using Android Studio's SDK manager, or via [direct download](https://developer.android.com/ndk/downloads/).
|
|
||||||
|
|
||||||
## API Documentation
|
|
||||||
- [Getting Started Guide](docs/GettingStarted.md)
|
- [Getting Started Guide](docs/GettingStarted.md)
|
||||||
- [Full Guide to Oboe](docs/FullGuide.md)
|
- [Full Guide to Oboe](docs/FullGuide.md)
|
||||||
- [API reference](https://google.github.io/oboe/reference)
|
- [API reference](https://google.github.io/oboe)
|
||||||
- [Tech Notes](docs/notes/)
|
|
||||||
- [History of Audio features/bugs by Android version](docs/AndroidAudioHistory.md)
|
- [History of Audio features/bugs by Android version](docs/AndroidAudioHistory.md)
|
||||||
|
- [Migration guide for apps using OpenSL ES](docs/OpenSLESMigration.md)
|
||||||
- [Frequently Asked Questions](docs/FAQ.md) (FAQ)
|
- [Frequently Asked Questions](docs/FAQ.md) (FAQ)
|
||||||
|
- [Wiki](https://github.com/google/oboe/wiki)
|
||||||
- [Our roadmap](https://github.com/google/oboe/milestones) - Vote on a feature/issue by adding a thumbs up to the first comment.
|
- [Our roadmap](https://github.com/google/oboe/milestones) - Vote on a feature/issue by adding a thumbs up to the first comment.
|
||||||
|
|
||||||
|
### Community
|
||||||
|
- Reddit: [r/androidaudiodev](https://www.reddit.com/r/androidaudiodev/)
|
||||||
|
- StackOverflow: [#oboe](https://stackoverflow.com/questions/tagged/oboe)
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
- [**OboeTester** app for measuring latency, glitches, etc.](https://github.com/google/oboe/tree/master/apps/OboeTester/docs)
|
- [**OboeTester** app for measuring latency, glitches, etc.](apps/OboeTester/docs)
|
||||||
- [Oboe unit tests](https://github.com/google/oboe/tree/master/tests)
|
- [Oboe unit tests](tests)
|
||||||
|
|
||||||
## Videos
|
## Videos
|
||||||
- [Getting started with Oboe](https://www.youtube.com/playlist?list=PLWz5rJ2EKKc_duWv9IPNvx9YBudNMmLSa)
|
- [Getting started with Oboe](https://www.youtube.com/playlist?list=PLWz5rJ2EKKc_duWv9IPNvx9YBudNMmLSa)
|
||||||
- [Low Latency Audio - Because Your Ears Are Worth It](https://www.youtube.com/watch?v=8vOf_fDtur4) (Android Dev Summit '18)
|
- [Low Latency Audio - Because Your Ears Are Worth It](https://www.youtube.com/watch?v=8vOf_fDtur4) (Android Dev Summit '18)
|
||||||
- [Real-time audio with the 100 oscillator synthesizer](https://www.youtube.com/watch?v=J04iPJBkAKs) (DroidCon Berlin '18)
|
|
||||||
- [Winning on Android](https://www.youtube.com/watch?v=tWBojmBpS74) - How to optimize an Android audio app. (ADC '18)
|
- [Winning on Android](https://www.youtube.com/watch?v=tWBojmBpS74) - How to optimize an Android audio app. (ADC '18)
|
||||||
- [Real-Time Processing on Android](https://youtu.be/hY9BrS2uX-c) (ADC '19)
|
|
||||||
|
|
||||||
## Sample code and apps
|
## Sample code and apps
|
||||||
- Sample apps can be found in the [samples directory](samples).
|
- Sample apps can be found in the [samples directory](samples).
|
||||||
- A complete "effects processor" app called FXLab can be found in the [apps/fxlab folder](apps/fxlab).
|
- A complete "effects processor" app called FXLab can be found in the [apps/fxlab folder](apps/fxlab).
|
||||||
- Also check out the [Rhythm Game codelab](https://codelabs.developers.google.com/codelabs/musicalgame-using-oboe/index.html#0).
|
- Also check out the [Rhythm Game codelab](https://developer.android.com/codelabs/musicalgame-using-oboe?hl=en#0).
|
||||||
|
|
||||||
### Third party sample code
|
### Third party sample code
|
||||||
- [Ableton Link integration demo](https://github.com/jbloit/AndroidLinkAudio) (author: jbloit)
|
- [Ableton Link integration demo](https://github.com/jbloit/AndroidLinkAudio) (author: jbloit)
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
/build/
|
/build/
|
||||||
.idea/
|
.idea/
|
||||||
/app/build/
|
/app/build/
|
||||||
|
/app/release/
|
||||||
|
/app/debug/
|
||||||
/app/app.iml
|
/app/app.iml
|
||||||
*.iml
|
*.iml
|
||||||
/app/externalNativeBuild/
|
/app/externalNativeBuild/
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
cmake_minimum_required(VERSION 3.4.1)
|
cmake_minimum_required(VERSION 3.4.1)
|
||||||
|
|
||||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror -Wall -std=c++14")
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror -Wall -std=c++17 -fvisibility=hidden")
|
||||||
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -O2")
|
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -O2")
|
||||||
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3")
|
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3")
|
||||||
|
|
||||||
link_directories(${CMAKE_CURRENT_LIST_DIR}/..)
|
link_directories(${CMAKE_CURRENT_LIST_DIR}/..)
|
||||||
|
|
||||||
|
# Increment this number when adding files to OboeTester => 105
|
||||||
|
# The change in this file will help Android Studio resync
|
||||||
|
# and generate new build files that reference the new code.
|
||||||
file(GLOB_RECURSE app_native_sources src/main/cpp/*)
|
file(GLOB_RECURSE app_native_sources src/main/cpp/*)
|
||||||
|
|
||||||
### Name must match loadLibrary() call in MainActivity.java
|
### Name must match loadLibrary() call in MainActivity.java
|
||||||
@@ -30,5 +33,4 @@ include_directories(
|
|||||||
|
|
||||||
# link to oboe
|
# link to oboe
|
||||||
target_link_libraries(oboetester log oboe atomic)
|
target_link_libraries(oboetester log oboe atomic)
|
||||||
|
target_link_options(oboetester PRIVATE "-Wl,-z,max-page-size=16384")
|
||||||
# bump 2 to resync CMake
|
|
||||||
|
|||||||