Compare commits
160 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 70d7b6383e | |||
| 087a275189 | |||
| 63b31d234c | |||
| 44f4680b24 | |||
| 06e06e305b | |||
| 464fb47756 | |||
| fe3746fd1a | |||
| 0e407dca7a | |||
| b0ff1552f2 | |||
| d191aaa34d | |||
| a1a70540b8 | |||
| 5c1837b1eb | |||
| bb0f156497 | |||
| 4b0496bad1 | |||
| a56add6d17 | |||
| 48b39c8519 | |||
| decaa006b2 | |||
| c0bd4a35be | |||
| 8c0d0ace4d | |||
| c11c762f15 | |||
| c465f58006 | |||
| 780246c4f6 | |||
| 5714555847 | |||
| 738f7317d7 | |||
| 5e514335c1 | |||
| f5b8b6c85f | |||
| db25c14dfb | |||
| 90163a3ff2 | |||
| f63707c45b | |||
| 7756fdc3cb | |||
| 4bdb9435a4 | |||
| 5cbde96177 | |||
| 03c5a1eddf | |||
| ada0d8abe1 | |||
| dbecc345e3 | |||
| 0f8f6d0e4f | |||
| 911e11a372 | |||
| 2d28d18bf6 | |||
| 0b70d6c535 | |||
| 1e6613e2de | |||
| debfaf28e6 | |||
| 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 | |||
| 28f741f72f | |||
| 22bcd95da1 | |||
| ea28f886f3 | |||
| 6cd8f0ddea | |||
| 7e25da70f0 | |||
| df3d3e7600 | |||
| 8c9af95598 | |||
| 45519ad550 | |||
| 3cca70608f | |||
| fb4eaeda10 |
@@ -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"
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
# AI Disclosure
|
||||||
|
|
||||||
|
This is a disclosure of the use of AI in this project.
|
||||||
|
|
||||||
|
## AI Use
|
||||||
|
|
||||||
|
Anyone who demands the dislosure of how AI was used in an engineering project,
|
||||||
|
has no idea what AI is, or how it works.
|
||||||
|
|
||||||
|
AI was used in this project as a tool. Several contributors used AI in their
|
||||||
|
commits, and so you will see like 7 commits by Claude or Codex or Cursor.
|
||||||
|
|
||||||
|
However those commits were reviewed by human beings, and it was declared that
|
||||||
|
the exact same fix would have been done by a human, so they were accepted.
|
||||||
|
|
||||||
|
AI was not used to make decisions, or to create the project.
|
||||||
|
|
||||||
|
If you would like to read more, well then continue reading:
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque ante leo, luctus in semper a, maximus ut est. Vivamus nec magna vitae quam luctus suscipit nec eu orci. Vestibulum ut felis a dolor cursus vulputate. Phasellus pharetra elementum sollicitudin. Aenean elementum imperdiet ultrices. In risus mauris, scelerisque sed viverra in, iaculis non eros. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin quam justo, maximus sit amet fringilla in, tristique eget sapien.
|
||||||
|
|
||||||
|
Pellentesque rhoncus, erat nec elementum ultricies, augue purus suscipit augue, in condimentum nisl enim vel velit. Etiam at semper turpis. Vestibulum ipsum magna, ultrices non sem ut, hendrerit bibendum mi. Curabitur in varius sapien. Morbi posuere bibendum ex, at ultrices orci. Fusce feugiat ultrices varius. Suspendisse sed ante ligula. Sed dignissim lorem est, nec tristique arcu commodo sed. Cras consectetur sapien dolor, vitae finibus enim lacinia id.
|
||||||
|
|
||||||
|
Donec quis magna est. Maecenas dui arcu, venenatis sit amet libero nec, lacinia eleifend leo. Quisque lobortis vulputate lacus a elementum. Proin nec metus lectus. Donec eu auctor sem, at finibus ipsum. Curabitur eget dignissim justo. Donec lobortis leo eu arcu tristique, in volutpat augue eleifend. Morbi lacinia a risus in suscipit. Maecenas suscipit est eu interdum dictum. Cras in nulla imperdiet, dapibus mauris posuere, facilisis velit. Nunc dapibus, leo quis interdum tempor, elit mi mattis dolor, sagittis dictum mi urna sed lectus. Maecenas elementum, mauris id molestie dapibus, diam arcu egestas erat, at tempor justo orci vitae nibh. Etiam sagittis facilisis erat a vulputate. Praesent condimentum ac odio quis sollicitudin.
|
||||||
|
|
||||||
|
Fusce vitae orci vestibulum, sagittis dolor non, cursus urna. Morbi eleifend pretium pellentesque. Pellentesque ornare elementum sem in imperdiet. Maecenas dapibus, erat et lobortis porttitor, velit magna auctor odio, quis interdum elit est eu justo. In posuere euismod odio, in porttitor magna iaculis eget. In id quam pulvinar, ultrices dolor in, pellentesque dolor. Nunc varius ante at felis dictum, id porttitor sem efficitur. Integer pretium dignissim commodo. Suspendisse in est a arcu blandit faucibus. Donec quis lacus mollis, tincidunt nunc quis, suscipit nunc. Nunc non arcu dignissim, dignissim sem in, finibus neque. Aliquam non porta eros. Donec et pretium augue, non cursus eros.
|
||||||
|
|
||||||
|
Nunc at dignissim nisi. Nam nec metus augue. Proin nulla sapien, tristique a purus vel, vulputate commodo mi. Sed id erat leo. Quisque ullamcorper a nisl id molestie. Aliquam erat volutpat. Donec eget hendrerit mauris. Fusce tincidunt nisl a lorem tincidunt dapibus. Nam volutpat rhoncus tortor.
|
||||||
|
|
||||||
|
Nunc et sapien enim. Proin at nunc a nulla maximus consectetur nec eget tortor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Mauris orci odio, sodales et elementum laoreet, porta at lacus. Maecenas vestibulum lectus risus, pulvinar scelerisque dolor posuere viverra. Proin gravida tellus vitae accumsan dignissim. Nunc non sapien aliquet ex cursus ultricies ac quis diam. Sed luctus feugiat risus eu tincidunt. Duis auctor lacinia fringilla. Donec pretium cursus magna a feugiat. Duis tristique, leo vulputate semper iaculis, est ipsum dapibus lacus, et molestie nulla enim nec ex. Nunc non feugiat neque.
|
||||||
|
|
||||||
|
Fusce euismod egestas elit ut pretium. Nulla eros quam, auctor sit amet faucibus eu, scelerisque eu neque. Sed nisi felis, lobortis in sapien a, tempor efficitur nunc. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque ut justo est. Sed maximus, tellus in bibendum posuere, quam augue finibus eros, sed gravida arcu diam quis lectus. Donec eu placerat ligula. Ut quis imperdiet lorem. Maecenas a mi ac augue semper sodales. Curabitur in justo velit. Praesent et felis quis enim porttitor sagittis.
|
||||||
|
|
||||||
|
Nulla sed sagittis felis, sit amet placerat tortor. Ut metus est, sollicitudin ac turpis quis, aliquet congue lorem. Fusce auctor erat non convallis aliquet. Nullam sodales rutrum tellus ac malesuada. Quisque sem diam, iaculis in ultricies sit amet, fermentum quis sem. Integer condimentum placerat purus non lacinia. Integer hendrerit ultricies tellus, at dignissim nibh. Suspendisse accumsan eget tortor nec cursus. Proin accumsan rhoncus leo, eget pretium est tristique ac.
|
||||||
|
|
||||||
|
Sed feugiat sed diam a porta. Nullam varius lacus at fermentum fringilla. Morbi pharetra scelerisque pharetra. Nulla placerat vitae ligula non efficitur. Suspendisse quam dui, rutrum eget nulla eu, semper eleifend ante. Aenean ut condimentum arcu. Suspendisse auctor metus non sem ornare, vel tincidunt odio vehicula. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.
|
||||||
|
|
||||||
|
Donec vel metus ut libero sollicitudin posuere a nec nulla. Vivamus a scelerisque nisi. Aliquam eu sollicitudin tortor. Aliquam purus eros, blandit et quam et, pretium porttitor sem. Nunc iaculis arcu enim, et maximus sem malesuada in. Morbi nec nunc volutpat, semper diam sit amet, gravida elit. Vestibulum eu turpis vel lacus imperdiet congue. Donec rhoncus auctor sem.
|
||||||
|
|
||||||
|
Suspendisse eu lorem non dolor pretium finibus euismod quis dolor. Cras finibus egestas velit, commodo rutrum est placerat sit amet. Pellentesque vitae semper diam, sit amet auctor metus. Sed porttitor porttitor nunc, vel imperdiet neque volutpat quis. Sed hendrerit sapien et lacus imperdiet, nec hendrerit lorem sodales. Integer lobortis rutrum odio at ullamcorper. Aliquam tincidunt magna a tellus suscipit, at porta turpis dapibus. Ut ultricies auctor felis eu feugiat. Sed tempus sem et dictum fringilla. Nunc non pellentesque tortor. Suspendisse pulvinar, arcu ut imperdiet gravida, eros ex mattis mauris, vel ultricies est erat et dui. Praesent porttitor tortor et erat interdum efficitur. Phasellus et luctus lectus, et egestas ante. Praesent ex ipsum, rutrum id efficitur et, vulputate non tortor. Aenean maximus nunc ac purus sodales, et venenatis lacus laoreet.
|
||||||
|
|
||||||
|
Cras egestas ultrices dui, at tempor leo varius vitae. Donec porta, nisl nec ornare maximus, est arcu auctor mauris, varius elementum nisl arcu venenatis neque. Aenean metus quam, vestibulum eget justo non, hendrerit dapibus nunc. Vivamus diam ante, mattis sed nulla at, iaculis elementum magna. Sed massa diam, efficitur vel nunc sed, malesuada interdum tortor. Aliquam non neque aliquet ex imperdiet finibus eget ac neque. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae;
|
||||||
|
|
||||||
|
Pellentesque fringilla tortor metus, luctus commodo leo gravida et. Fusce nec turpis at lorem rutrum porta vel in justo. Curabitur mattis suscipit felis, id dapibus arcu ornare ut. Proin sapien felis, pulvinar ut tristique vitae, aliquet ac dui. Vestibulum a erat tellus. Vestibulum sagittis dolor eget augue egestas fringilla. Fusce et purus a nunc auctor dapibus vel sed dui. Fusce interdum, libero vel pellentesque rutrum, urna massa iaculis tellus, et aliquam lectus diam vel sem. Donec a nunc et dui semper gravida. Donec posuere, eros eu consequat efficitur, justo metus ullamcorper lectus, et molestie massa ipsum eu sapien. Mauris eu suscipit neque. Morbi convallis sit amet leo a scelerisque. Cras ultrices libero ac mattis accumsan. Nam gravida ligula id erat semper ornare. Duis consequat ut ipsum eu volutpat. Quisque egestas sollicitudin ullamcorper.
|
||||||
|
|
||||||
|
Cras pellentesque quam non neque porta fringilla. Integer elementum, augue mattis blandit consequat, enim ipsum finibus ex, quis finibus neque eros eu ex. Fusce at urna justo. Donec erat eros, maximus id mauris vel, rutrum rutrum sapien. Morbi sed rutrum ex. Suspendisse lacus velit, varius ut elementum vitae, finibus non enim. Suspendisse vehicula euismod ipsum, id consequat nulla sodales vel. Morbi eu sem id leo congue dapibus a nec velit. Sed nec neque quam. Etiam rhoncus id nulla id volutpat. Nulla facilisi. Donec non maximus enim. Aenean consequat, sapien sit amet malesuada rutrum, erat sem euismod sapien, et feugiat lectus mauris id velit.
|
||||||
|
|
||||||
|
Fusce sodales porttitor gravida. Proin placerat ante nec nibh tempor aliquam. Sed ut diam eu sem fringilla malesuada. Maecenas aliquam risus vel quam dictum, at iaculis nibh pretium. Mauris convallis quam vitae dolor varius suscipit. Etiam nec fermentum dui. Aliquam in magna tincidunt, consectetur quam eget, aliquam purus. Ut dictum aliquet finibus. Pellentesque vel lacinia felis. Nulla malesuada vestibulum varius. Sed quam diam, efficitur id felis in, volutpat bibendum erat. Praesent luctus vulputate urna at interdum. Aenean ac aliquam eros. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Morbi mollis, nisl vel consequat vehicula, massa tellus porta arcu, vel dictum sapien mauris sit amet nunc. Integer sed maximus neque, ac iaculis urna.
|
||||||
|
|
||||||
|
Integer non erat a leo euismod convallis quis eget magna. Morbi gravida ac urna sed ornare. Nunc vehicula mauris accumsan, ornare sem in, egestas mauris. Vestibulum vel vulputate felis. Nulla eu scelerisque diam. Suspendisse ac odio tempor nunc pellentesque hendrerit at a magna. Vivamus ultrices nunc ut orci fermentum pharetra. Nullam laoreet hendrerit ligula ut gravida. Proin scelerisque magna sit amet arcu malesuada, pharetra ultrices est molestie. Nullam pulvinar placerat dui, vitae hendrerit tortor luctus sed. Pellentesque elementum tellus eget arcu pulvinar varius.
|
||||||
|
|
||||||
|
Ut placerat, magna vitae tincidunt ultricies, est orci aliquet urna, at luctus augue erat vel ipsum. Fusce odio sem, venenatis vel consequat nec, bibendum sed dolor. Cras a sodales eros. Nullam eget dui congue, vehicula purus ut, condimentum dui. Maecenas libero ipsum, condimentum tincidunt nisi in, sodales lacinia ex. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.
|
||||||
|
|
||||||
|
Duis gravida velit ac euismod suscipit. Proin sed ligula erat. Nullam eu ornare massa, non fringilla felis. Curabitur eu erat ex. Quisque sit amet dolor id arcu mattis scelerisque at at eros. In arcu nulla, fermentum non maximus eget, rhoncus in lacus. Nulla sit amet augue eu tortor vulputate congue. Vivamus laoreet condimentum tempus. Maecenas tempor, diam sed laoreet venenatis, mauris arcu lacinia enim, quis facilisis nunc turpis a ligula. Pellentesque quis placerat nisi, sit amet ullamcorper diam. Suspendisse a elementum elit, vel tristique dolor. Nam pretium ante tortor, vel tristique ipsum ultricies ut. Quisque non lectus imperdiet, placerat erat ac, pharetra tellus. In condimentum at magna a posuere. Aliquam et fringilla ipsum. Sed facilisis, nulla a finibus gravida, elit elit vulputate velit, dictum ornare est sapien a nisl.
|
||||||
|
|
||||||
|
Mauris eleifend vulputate felis sed mattis. Praesent id velit vitae ex porta pretium. Cras mollis malesuada justo, ut ornare quam placerat ac. Donec lobortis arcu tellus, luctus tempor mi malesuada quis. Maecenas condimentum libero vitae finibus malesuada. Vestibulum sollicitudin fringilla diam eget egestas. Sed vulputate urna nec ipsum maximus hendrerit. Maecenas blandit ex ut massa sodales, vitae tincidunt lorem ullamcorper. Phasellus vitae nisl ornare, cursus sem a, pulvinar arcu. Vestibulum faucibus risus nec tincidunt pellentesque. Pellentesque vel porttitor ex. Vivamus sollicitudin gravida lacus in suscipit. Aliquam urna neque, sodales quis quam ac, suscipit condimentum ante.
|
||||||
|
|
||||||
|
Morbi id arcu sit amet sapien ornare gravida eget quis sapien. In hac habitasse platea dictumst. In quis interdum ligula. Donec sed mi vulputate, scelerisque turpis vitae, interdum odio. Proin tristique condimentum arcu, et malesuada tellus convallis in. Fusce egestas maximus magna, sit amet convallis velit porttitor ut. Curabitur venenatis lacus ut blandit convallis. Phasellus scelerisque congue turpis eget vehicula. Nam venenatis mi sit amet rhoncus pretium. Nulla sed odio purus. Phasellus cursus id sapien ut feugiat. Duis et ipsum vel dui tempus porta. Curabitur non tortor consectetur, sollicitudin tellus mollis, sollicitudin lorem.
|
||||||
|
|
||||||
|
Ut quis ornare justo. Nunc aliquam, leo sit amet placerat placerat, dui nulla luctus dui, ac iaculis nisl orci id metus. Vestibulum nunc nunc, porta nec dictum id, feugiat et ante. Quisque lobortis, lacus tristique vestibulum rhoncus, massa nulla dignissim massa, at scelerisque massa nunc at velit. Donec eu ipsum nec dui luctus pulvinar et et turpis. Quisque eu neque erat. Donec varius egestas nunc, ut pretium libero tempor ac. Vestibulum pellentesque mi erat, et semper dolor semper vitae. Morbi enim dui, laoreet non venenatis sit amet, dignissim a orci. Sed id odio turpis. Phasellus non rutrum magna. Maecenas placerat arcu ultricies ultrices congue. Nulla quis neque ligula. Etiam in diam commodo, pharetra mauris ac, pretium nisi. Praesent sed nibh nec odio condimentum commodo vel vel lacus.
|
||||||
|
|
||||||
|
Pellentesque id libero vitae ex egestas pharetra placerat nec augue. Ut eget lobortis lorem, at vehicula sapien. Aliquam eu tincidunt ligula. Aenean et vestibulum dui, quis porttitor dui. Nullam quis dolor libero. Sed accumsan eros vitae nisi ornare congue. Aliquam nisi sapien, sollicitudin quis odio vel, pharetra maximus urna.
|
||||||
|
|
||||||
|
Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Morbi accumsan felis id urna malesuada, non vulputate velit maximus. Proin urna velit, viverra a metus vel, sollicitudin faucibus nulla. Vivamus ipsum lectus, pharetra sit amet varius vel, ullamcorper nec neque. Suspendisse potenti. Vivamus sit amet justo ac augue placerat hendrerit in eu felis. Integer luctus ex quam, in pellentesque eros interdum sed. Fusce finibus quis neque vitae efficitur. Pellentesque vulputate consectetur egestas. Integer a neque fermentum, tincidunt ligula id, gravida urna. Pellentesque ultrices, leo et suscipit accumsan, lorem nunc porta dui, non congue ligula leo ut urna. Duis vehicula risus in mi eleifend luctus. Duis convallis, mi faucibus pellentesque cursus, libero mauris varius sem, sit amet fermentum massa metus nec tellus. In tortor ligula, faucibus eu nibh id, lobortis viverra erat. Morbi non nisi suscipit, mollis enim at, convallis velit.
|
||||||
|
|
||||||
|
Pellentesque consequat imperdiet felis quis scelerisque. Duis aliquam mollis nibh quis tincidunt. Vivamus elit odio, blandit quis volutpat at, blandit nec tortor. Cras maximus ex at odio maximus, dapibus condimentum risus malesuada. Pellentesque viverra orci at ante commodo, quis posuere sapien efficitur. Nunc tristique imperdiet diam elementum lobortis. Fusce velit dui, ultrices id ante pharetra, fermentum egestas augue. Curabitur ante augue, vestibulum non magna quis, feugiat pulvinar diam. Suspendisse sagittis dui a tellus scelerisque, ut tincidunt neque accumsan. Cras pharetra metus vel eros tincidunt, vel tincidunt lacus egestas. Donec eget pellentesque sapien. Cras condimentum in justo pulvinar feugiat. Quisque malesuada ac odio eget rhoncus. Fusce posuere justo sed finibus ornare.
|
||||||
|
|
||||||
|
Duis a auctor tortor. Pellentesque lobortis auctor risus, ultrices varius mi cursus quis. Sed dui nulla, mattis sit amet justo mattis, condimentum commodo justo. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Pellentesque hendrerit diam vel lacinia volutpat. Sed et luctus nunc, vitae congue arcu. Aenean placerat tincidunt ipsum. Ut molestie orci eu dapibus viverra. Cras sodales ullamcorper augue, at aliquam ex. Sed et justo augue.
|
||||||
|
|
||||||
|
Nullam feugiat risus et turpis faucibus, vel tincidunt nulla consequat. Aliquam libero erat, pellentesque sit amet tellus in, tempor ornare nisl. Donec viverra eget magna non pharetra. Cras sollicitudin, justo ut porttitor venenatis, risus nulla auctor est, at commodo sem urna in mi. Vestibulum mattis sapien vel nibh pulvinar, vel dignissim lacus cursus. Maecenas vel dictum tortor, ac ultricies justo. Praesent quis venenatis nisl. Morbi a diam fringilla, auctor lectus sed, varius est. Praesent faucibus auctor dolor, a commodo nisi mattis id. Fusce porta molestie ultrices. Sed pulvinar, leo ac consectetur hendrerit, erat velit gravida enim, eu blandit ligula justo sit amet neque.
|
||||||
|
|
||||||
|
Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Aenean libero risus, porttitor in congue vel, consequat sed felis. Vivamus eget blandit ante. Vivamus vitae mattis massa. In ut dolor sit amet tellus sollicitudin mattis. Mauris iaculis nisl neque, in gravida lectus ultricies nec. Fusce vehicula vehicula lacinia. Fusce viverra sed nisl id rhoncus. Donec sed porta mi. Nam tempus purus non massa tincidunt iaculis. Morbi viverra massa ut gravida vestibulum.
|
||||||
|
|
||||||
|
Proin dapibus mi a libero sagittis, id vehicula nulla iaculis. Proin a enim in tortor tincidunt egestas. Integer finibus neque eu nibh pretium, et pellentesque urna finibus. Integer a sollicitudin mauris, at convallis erat. Nullam sit amet lectus sed turpis commodo efficitur. Nulla nec turpis dapibus, suscipit diam quis, vulputate urna. Aenean sed posuere justo.
|
||||||
|
|
||||||
|
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Sed luctus purus nibh, id imperdiet risus pulvinar in. Quisque auctor sem lacinia turpis mollis, nec pretium ipsum suscipit. Aliquam sed metus sagittis, mollis nisi eget, hendrerit libero. Nulla sodales erat semper nisl condimentum, ultricies rhoncus lacus commodo. Ut suscipit libero augue, non vulputate dui tristique vel. Praesent convallis efficitur est, sed tincidunt mauris aliquet in. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.
|
||||||
|
|
||||||
|
Etiam a vehicula purus. Curabitur lacus erat, ultrices et dictum id, posuere ut risus. Etiam at auctor leo. Pellentesque eleifend est a metus lacinia rutrum. Nullam justo ligula, tempor non facilisis vel, volutpat sed nulla. Integer non dolor consequat, dapibus lectus eu, luctus turpis. Aenean enim erat, sagittis vitae ornare bibendum, faucibus sit amet magna. Phasellus suscipit ultrices faucibus. Sed at risus molestie, viverra ipsum vel, bibendum lectus. Pellentesque molestie vitae risus non viverra. Phasellus eleifend massa id odio sagittis mattis. Nullam velit mauris, viverra quis fermentum sit amet, vestibulum ut ex. Suspendisse imperdiet, sapien sed sagittis pharetra, nisi nibh vulputate metus, quis mattis dolor nisi ut lorem. Praesent vestibulum nibh vulputate lacus pulvinar tempor. Vestibulum vulputate diam ligula, vitae efficitur enim dapibus non. Etiam at ornare enim.
|
||||||
|
|
||||||
|
Curabitur aliquet velit enim, euismod faucibus urna euismod sit amet. Vivamus viverra vulputate nulla, ut gravida neque rutrum in. Suspendisse potenti. Nulla vitae neque felis. Etiam eu erat ac nulla ornare volutpat. Quisque ut diam dui. Sed ut massa quis dolor volutpat eleifend. Duis posuere dolor sit amet varius auctor. Donec mollis malesuada erat, eu luctus libero viverra feugiat. Curabitur fermentum velit eu purus fringilla, consequat tincidunt diam rutrum. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc volutpat vel est in vehicula. Morbi euismod, tortor ut posuere ullamcorper, velit justo ultricies lorem, vitae tincidunt erat ante vitae sem. Praesent semper, quam eget condimentum finibus, metus leo imperdiet augue, nec fringilla sem nunc id sapien.
|
||||||
|
|
||||||
|
Aliquam vestibulum ante porta sem finibus, ut rhoncus elit sodales. Phasellus a lacus congue, sagittis nulla eget, cursus libero. Duis laoreet fringilla faucibus. Aenean gravida lorem sed fringilla facilisis. Pellentesque sodales urna lorem, non rutrum tortor vulputate eget. Duis a enim semper, iaculis sem ac, facilisis urna. Donec iaculis nulla sit amet dignissim volutpat. Mauris cursus dui id feugiat suscipit. Fusce tempor placerat nulla vitae vestibulum. Vivamus imperdiet blandit nulla, in aliquet justo viverra in. Cras malesuada molestie ligula sit amet volutpat. Praesent ornare orci sit amet rutrum eleifend. Ut placerat metus felis, id malesuada justo mollis eget. Etiam mi turpis, pulvinar in pulvinar in, tincidunt in neque.
|
||||||
|
|
||||||
|
In venenatis euismod neque, eu convallis diam semper ac. Mauris auctor mi non massa vestibulum viverra. Aenean non turpis sapien. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Praesent gravida feugiat interdum. Vivamus sit amet consequat ligula. Praesent dictum nunc ac sapien ullamcorper consectetur. Ut malesuada blandit neque.
|
||||||
|
|
||||||
|
Duis tincidunt mauris sit amet odio dictum accumsan. Suspendisse efficitur nibh magna, ac dapibus augue vulputate nec. Etiam lectus neque, sollicitudin quis sapien vitae, aliquet fermentum mi. Aenean interdum interdum rhoncus. Donec eu libero urna. Donec semper lacus eu nunc scelerisque, vitae viverra quam consectetur. Integer sagittis, nulla congue venenatis auctor, purus justo mollis metus, at rutrum magna arcu nec est. Maecenas felis lorem, consequat non cursus vitae, lobortis vitae nisi. Cras eget magna justo. Nullam sagittis tellus id luctus ultricies. Etiam a arcu efficitur, consectetur libero non, imperdiet turpis. Donec ac velit et nisl semper semper. Duis iaculis interdum nunc sed tempor.
|
||||||
|
|
||||||
|
Sed diam odio, sagittis non dignissim nec, accumsan ac diam. Fusce sit amet dui sit amet justo ultrices viverra. Sed vel massa suscipit nibh porttitor laoreet in id nunc. Nam quis libero vitae nunc blandit sollicitudin et a lorem. Duis urna arcu, accumsan sed dignissim sit amet, vulputate at ex. Mauris porttitor libero mauris, vel fringilla diam euismod quis. Sed varius placerat tellus vel efficitur. Phasellus pulvinar gravida magna. Nulla dignissim consectetur finibus.
|
||||||
|
|
||||||
|
Nunc quis aliquet nisi. Cras luctus bibendum eros ac dignissim. Aenean suscipit felis vitae elementum eleifend. Proin commodo nunc non diam dignissim, in tincidunt nulla ultrices. Vivamus faucibus quam scelerisque interdum finibus. Sed porttitor vehicula urna, in laoreet arcu condimentum non. Praesent ac lacus diam. Vivamus aliquam euismod risus, luctus dictum lectus sodales et. Proin quis velit ac massa tristique scelerisque.
|
||||||
|
|
||||||
|
Sed non dolor efficitur, tincidunt mi eget, sagittis tortor. Quisque at varius felis, at finibus sem. Vestibulum vel lectus tincidunt, pharetra diam sit amet, interdum nulla. Sed ut elit tortor. Nam tincidunt tempus aliquam. Vivamus rhoncus faucibus sapien eget facilisis. Aliquam erat volutpat. Phasellus placerat aliquam lacus, eget ultrices orci pretium hendrerit. Fusce vitae dolor sit amet ante condimentum placerat. Nunc varius risus id tellus mollis, euismod luctus sapien viverra. Donec sodales est vel massa suscipit, eu sollicitudin ante convallis. Curabitur eu condimentum velit. Sed pharetra euismod tincidunt.
|
||||||
|
|
||||||
|
Nam at libero eros. Quisque bibendum, ligula quis sagittis ullamcorper, eros leo consectetur ex, quis elementum dolor justo vitae mi. Maecenas et elementum erat, et auctor enim. Nunc at nibh fermentum, ullamcorper mi elementum, facilisis erat. Vestibulum vestibulum leo ut pellentesque placerat. Suspendisse imperdiet nisl vitae justo sodales pellentesque. Interdum et malesuada fames ac ante ipsum primis in faucibus. In faucibus pretium nunc, sed interdum lectus vestibulum quis. Vestibulum luctus viverra ex at efficitur. Etiam ac est lorem. Maecenas mollis, orci at rhoncus congue, nulla leo rutrum dui, et pellentesque orci ligula eget ipsum. Suspendisse fermentum nisi turpis, ut sollicitudin purus imperdiet non. Maecenas vitae quam ornare, porta sem quis, rhoncus neque. Donec mattis purus a erat tristique, ac mollis est convallis. Duis vitae ipsum viverra, condimentum ante vel, sagittis ex. Maecenas placerat odio libero, id interdum turpis fermentum at.
|
||||||
|
|
||||||
|
Praesent faucibus nulla eget vehicula accumsan. Nulla elementum ante a nibh venenatis hendrerit. Proin nec nunc mattis, imperdiet nisl rutrum, sollicitudin libero. Nullam bibendum dignissim faucibus. Sed eget tortor vitae sapien cursus faucibus nec et lectus. Duis pharetra non odio id consectetur. Suspendisse at est sem. Nunc mauris ligula, ultrices id ante non, venenatis mattis erat. Vivamus sit amet viverra nisl.
|
||||||
|
|
||||||
|
Sed cursus vel nisi in mattis. Nunc porttitor dictum leo ac euismod. Sed blandit ornare nunc id lobortis. Aenean convallis ligula at volutpat commodo. Vestibulum sit amet laoreet urna. Donec et pellentesque orci, ac egestas nulla. In accumsan venenatis porta. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Donec scelerisque, metus nec viverra pharetra, dolor libero dictum velit, id ullamcorper enim nisi eu nibh. Aliquam nec dapibus quam. Curabitur vulputate, libero sit amet tempor ullamcorper, libero purus congue quam, nec sollicitudin orci erat non massa. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.
|
||||||
|
|
||||||
|
Aenean faucibus mollis placerat. Praesent lacinia venenatis turpis eu scelerisque. Nam tempus tortor a varius posuere. Cras ut viverra tortor. Cras facilisis mauris ut ante imperdiet, a malesuada justo luctus. Ut eu enim sit amet arcu porttitor pulvinar ac ac odio. Ut odio neque, molestie vitae ligula quis, dignissim viverra erat. In ullamcorper erat sed elementum varius. Sed in lacus maximus, euismod nisi vitae, tempus mi. Nunc tellus justo, auctor at luctus ac, feugiat sit amet dui.
|
||||||
|
|
||||||
|
Donec imperdiet purus lorem, sed venenatis dolor finibus non. Aenean lacus nunc, elementum nec arcu eget, faucibus elementum turpis. Aliquam lacinia massa ac quam efficitur, et tincidunt eros pretium. Fusce condimentum mi vel pharetra egestas. Quisque consectetur nibh vel leo dignissim sollicitudin. Duis ultrices felis ipsum, sed maximus arcu ornare vitae. Curabitur porttitor ligula in turpis facilisis, id venenatis augue ultricies. Phasellus vel dolor id tellus finibus sodales ut quis nisi. Integer id orci cursus erat tincidunt sagittis non in nunc. Pellentesque ligula lacus, vestibulum eu ante vel, facilisis viverra massa. Sed ut tincidunt metus, vel tristique est. Ut et cursus justo. Ut ac porttitor eros, at dictum felis. Phasellus ornare nisi sit amet risus varius, sed sollicitudin nulla ornare. Donec aliquam ipsum urna. Aliquam id bibendum magna, quis venenatis diam.
|
||||||
|
|
||||||
|
Duis tempor odio id iaculis egestas. Cras consequat neque ac posuere iaculis. Nulla tempus et nisi eu auctor. Vestibulum metus massa, dignissim ut metus eget, ullamcorper consectetur turpis. Integer vel est tellus. Ut ac vestibulum massa. Pellentesque nec venenatis erat. Nam vel pellentesque lectus.
|
||||||
|
|
||||||
|
Morbi a placerat est. Ut eleifend ante ut placerat porta. Donec sagittis semper leo, ut scelerisque nisi imperdiet feugiat. Mauris purus turpis, consequat ut fringilla ac, cursus eget augue. Fusce arcu dolor, sagittis et facilisis ut, scelerisque non lacus. Aliquam sit amet eleifend tellus. Mauris id est luctus, iaculis tortor eget, gravida justo. Suspendisse at tellus nisl. Nullam felis erat, vehicula eu porttitor bibendum, pulvinar et dui. Sed molestie lacus nec sagittis rutrum. Aliquam erat volutpat. Nullam ut aliquet eros. Sed feugiat, massa id pharetra auctor, leo turpis condimentum purus, sit amet volutpat sem nunc sed nisi.
|
||||||
|
|
||||||
|
Pellentesque feugiat ipsum at accumsan iaculis. Morbi et dui in lorem commodo hendrerit. Mauris tempor ex mollis mollis blandit. Cras eu turpis feugiat, suscipit velit quis, volutpat magna. Vestibulum varius ligula ut quam mollis, a volutpat nibh lobortis. Sed sodales euismod leo non suscipit. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nullam ultricies sagittis justo, sit amet lacinia eros tincidunt tristique. Etiam faucibus turpis in lacus efficitur, vitae rutrum magna porttitor. Cras finibus eros vel ante semper, facilisis vestibulum erat accumsan. Ut mollis dui ut commodo varius. In et sem malesuada erat egestas pellentesque. Maecenas pulvinar sodales risus, at euismod ligula aliquet in. Quisque fringilla malesuada dui vel cursus. Curabitur eu ex vulputate, pretium elit non, sodales sapien. Integer egestas facilisis odio et pretium.
|
||||||
|
|
||||||
|
Integer eleifend, felis vitae faucibus tempus, tellus lectus placerat nunc, eu efficitur lacus mi vel nisl. Mauris commodo pretium feugiat. In aliquet nibh diam, ac egestas mauris consequat ut. Integer cursus, tortor pharetra pellentesque pulvinar, neque risus ultricies felis, et consequat felis eros at eros. Pellentesque fermentum velit ac sodales facilisis. Suspendisse vestibulum metus quis convallis lacinia. Donec in pharetra magna. Proin gravida dolor eget ligula lobortis sagittis.
|
||||||
|
|
||||||
|
Nullam consectetur ut massa id ultrices. Fusce consectetur at eros at mollis. Donec nec nibh fringilla, porttitor ipsum eget, aliquam neque. Quisque suscipit tortor in dui commodo, sed venenatis augue cursus. Etiam feugiat purus id justo elementum placerat. Sed interdum dictum nibh at sodales. Maecenas lobortis, metus ac sagittis lacinia, elit arcu varius felis, quis facilisis magna elit in leo. Proin condimentum orci sit amet dignissim imperdiet. Fusce sed iaculis felis. Maecenas sodales non magna vitae rutrum.
|
||||||
|
|
||||||
|
Nulla id ex massa. Sed vehicula sed quam non elementum. Aliquam luctus, enim vel molestie posuere, sem arcu laoreet justo, quis finibus nisl justo et magna. Vivamus malesuada elit in aliquam dignissim. Sed at tellus in orci vulputate ullamcorper. Integer magna sem, mattis id hendrerit non, tincidunt in est. Praesent posuere aliquet lobortis. Quisque euismod leo ut nisl pellentesque, et imperdiet dui dapibus.
|
||||||
|
|
||||||
|
Sed a erat nec risus pulvinar venenatis. Integer ultrices eros at aliquet efficitur. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Quisque ac maximus ante. Sed sodales, nisi et sagittis accumsan, diam odio consequat elit, et mollis dolor turpis tincidunt metus. Proin justo quam, tincidunt id convallis a, molestie hendrerit massa. Etiam pharetra turpis eu ultrices mollis.
|
||||||
|
|
||||||
|
Proin fermentum libero in purus cursus molestie. In varius magna eu ante maximus, eget rutrum felis iaculis. Maecenas hendrerit, diam eget vestibulum vehicula, est quam porta magna, a dictum urna mi ut libero. Vestibulum dictum quis lacus vitae eleifend. Integer sapien libero, pretium vitae euismod eget, semper eget ante. Vivamus mollis elementum odio vel hendrerit. Phasellus tristique, metus eget luctus tincidunt, ex enim faucibus ipsum, at dictum eros urna ac mi. Aenean imperdiet felis eu ultricies egestas. Vivamus fermentum convallis nisi, non sollicitudin felis posuere nec. Ut commodo sit amet felis semper dictum. Aenean accumsan, tellus id blandit aliquet, lorem nibh pellentesque mi, sit amet volutpat erat ligula ut est. Vivamus non posuere velit. Sed vel rutrum diam, non pulvinar nulla. Suspendisse quis gravida lectus, ac accumsan justo. Sed lobortis neque ante, a imperdiet nisl iaculis nec.
|
||||||
@@ -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 44 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
|
||||||
|
|||||||
@@ -4,8 +4,12 @@ A native LÖVE2D recreation of Poke Red, Blue and Yellow. The engine and map
|
|||||||
behavior are hand-written Lua; game data and graphics are decoded from a ROM
|
behavior are hand-written Lua; game data and graphics are decoded from a ROM
|
||||||
supplied by the player.
|
supplied by the player.
|
||||||
|
|
||||||
|
And before you say, "that's not a recomp", you're wrong. Recomp is an acronym. ***Reverse Engineering Causes Obsessive Mental Problems***
|
||||||
|
|
||||||
|
[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>
|
||||||
|
|
||||||
@@ -49,18 +53,17 @@ supplied by the player.
|
|||||||
|
|
||||||
### 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
|
||||||
|
|
||||||
@@ -68,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
|
||||||
@@ -215,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 |
|
||||||
|
|
||||||
@@ -328,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
|
||||||
@@ -337,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)
|
||||||
@@ -350,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 |
@@ -0,0 +1,10 @@
|
|||||||
|
# gb_anim -- bundled touch skin
|
||||||
|
|
||||||
|
Bezel art and overlay layout from libretro's `common-overlays`
|
||||||
|
(`gamepads/gb_anim_portrait`), licensed CC-BY-4.0:
|
||||||
|
https://github.com/libretro/common-overlays
|
||||||
|
|
||||||
|
`overlay.cfg` is the upstream `gb_big.cfg`, unmodified. It ships as the
|
||||||
|
reference skin for the RetroArch-overlay loader in
|
||||||
|
`src/core/TouchSkin.lua`: a full-device bezel, per-button press art, a
|
||||||
|
screen viewport, and page switching between the DMG and Color shells.
|
||||||
|
After Width: | Height: | Size: 973 B |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 649 B |
|
After Width: | Height: | Size: 645 B |
|
After Width: | Height: | Size: 636 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,89 @@
|
|||||||
|
overlays = 2
|
||||||
|
|
||||||
|
overlay0_name = "GameBoy"
|
||||||
|
overlay0_overlay = img/gb_back.png
|
||||||
|
overlay0_full_screen = true
|
||||||
|
overlay0_normalized = true
|
||||||
|
overlay0_range_mod = 1.0
|
||||||
|
overlay0_alpha_mod = 0.001
|
||||||
|
overlay0_viewport = "0.0,0.0,1.0,0.505"
|
||||||
|
overlay0_viewport_fill = true
|
||||||
|
|
||||||
|
overlay1_name = "GameBoyColor"
|
||||||
|
overlay1_overlay = img/gbc_back.png
|
||||||
|
overlay1_full_screen = true
|
||||||
|
overlay1_normalized = true
|
||||||
|
overlay1_range_mod = 1.0
|
||||||
|
overlay1_alpha_mod = 0.001
|
||||||
|
overlay1_viewport = "0.0,0.0,1.0,0.505"
|
||||||
|
overlay1_viewport_fill = true
|
||||||
|
|
||||||
|
# GameBoy
|
||||||
|
overlay0_descs = 18
|
||||||
|
|
||||||
|
overlay0_desc0 = "left,0.12778,0.73417,radial,0.09630,0.04635"
|
||||||
|
overlay0_desc0_overlay = img/gb_left.png
|
||||||
|
overlay0_desc1 = "right,0.35370,0.73417,radial,0.09630,0.04635"
|
||||||
|
overlay0_desc1_overlay = img/gb_right.png
|
||||||
|
overlay0_desc2 = "up,0.24074,0.67063,radial,0.08241,0.05417"
|
||||||
|
overlay0_desc2_overlay = img/gb_up.png
|
||||||
|
overlay0_desc3 = "down,0.24074,0.79771,radial,0.08241,0.05417"
|
||||||
|
overlay0_desc3_overlay = img/gb_down.png
|
||||||
|
overlay0_desc4 = "left|up,0.09259,0.65188,rect,0.06481,0.03646"
|
||||||
|
overlay0_desc5 = "right|up,0.38704,0.65188,rect,0.06481,0.03646"
|
||||||
|
overlay0_desc6 = "left|down,0.09259,0.81750,rect,0.06481,0.03646"
|
||||||
|
overlay0_desc7 = "right|down,0.38704,0.81750,rect,0.06481,0.03646"
|
||||||
|
overlay0_desc8 = "a,0.87407,0.72417,radial,0.08889,0.05000"
|
||||||
|
overlay0_desc8_overlay = img/gb_a_b.png
|
||||||
|
overlay0_desc9 = "b,0.68148,0.76584,radial,0.08889,0.05000"
|
||||||
|
overlay0_desc9_overlay = img/gb_a_b.png
|
||||||
|
overlay0_desc10 = "a|b,0.77037,0.73417,radial,0.02963,0.01667"
|
||||||
|
overlay0_desc11 = "a|b,0.78518,0.75584,radial,0.02963,0.01667"
|
||||||
|
overlay0_desc12 = "start,0.66666,0.93000,radial,0.07037,0.03958"
|
||||||
|
overlay0_desc12_overlay = img/gb_start_select.png
|
||||||
|
overlay0_desc13 = "select,0.33333,0.93000,radial,0.07037,0.03958"
|
||||||
|
overlay0_desc13_overlay = img/gb_start_select.png
|
||||||
|
overlay0_desc14 = "menu_toggle,0.05000,0.52800,radial,0.041296,0.02323"
|
||||||
|
overlay0_desc14_overlay = img/menu.png
|
||||||
|
overlay0_desc15 = "overlay_next,0.95000,0.52800,radial,0.041296,0.02323"
|
||||||
|
overlay0_desc15_overlay = img/rotate.png
|
||||||
|
overlay0_desc15_next_target = "GameBoyColor"
|
||||||
|
overlay0_desc16 = "rewind,0.05000,0.97500,radial,0.041296,0.02323"
|
||||||
|
overlay0_desc16_overlay =
|
||||||
|
overlay0_desc17 = "hold_fast_forward,0.95000,0.97500,radial,0.041296,0.02323"
|
||||||
|
overlay0_desc17_overlay =
|
||||||
|
|
||||||
|
# GameBoyColor
|
||||||
|
overlay1_descs = 18
|
||||||
|
|
||||||
|
overlay1_desc0 = "left,0.14078,0.73417,radial,0.08530,0.04635"
|
||||||
|
overlay1_desc0_overlay = img/gbc_left.png
|
||||||
|
overlay1_desc1 = "right,0.34270,0.73417,radial,0.08530,0.04635"
|
||||||
|
overlay1_desc1_overlay = img/gbc_right.png
|
||||||
|
overlay1_desc2 = "up,0.24074,0.67863,radial,0.08241,0.04617"
|
||||||
|
overlay1_desc2_overlay = img/gbc_up.png
|
||||||
|
overlay1_desc3 = "down,0.24074,0.78971,radial,0.08241,0.04617"
|
||||||
|
overlay1_desc3_overlay = img/gbc_down.png
|
||||||
|
overlay1_desc4 = "left|up,0.09259,0.65188,rect,0.06481,0.03646"
|
||||||
|
overlay1_desc5 = "right|up,0.38704,0.65188,rect,0.06481,0.03646"
|
||||||
|
overlay1_desc6 = "left|down,0.09259,0.81750,rect,0.06481,0.03646"
|
||||||
|
overlay1_desc7 = "right|down,0.38704,0.81750,rect,0.06481,0.03646"
|
||||||
|
overlay1_desc8 = "a,0.87407,0.72417,radial,0.08889,0.05000"
|
||||||
|
overlay1_desc8_overlay = img/gbc_a.png
|
||||||
|
overlay1_desc9 = "b,0.68148,0.76584,radial,0.08889,0.05000"
|
||||||
|
overlay1_desc9_overlay = img/gbc_b.png
|
||||||
|
overlay1_desc10 = "a|b,0.77037,0.73417,radial,0.02963,0.01667"
|
||||||
|
overlay1_desc11 = "a|b,0.78518,0.75584,radial,0.02963,0.01667"
|
||||||
|
overlay1_desc12 = "start,0.66666,0.93000,radial,0.07037,0.03958"
|
||||||
|
overlay1_desc12_overlay = img/gbc_start_select.png
|
||||||
|
overlay1_desc13 = "select,0.33333,0.93000,radial,0.07037,0.03958"
|
||||||
|
overlay1_desc13_overlay = img/gbc_start_select.png
|
||||||
|
overlay1_desc14 = "menu_toggle,0.05000,0.52800,radial,0.041296,0.02323"
|
||||||
|
overlay1_desc14_overlay = img/menu.png
|
||||||
|
overlay1_desc15 = "overlay_next,0.95000,0.52800,radial,0.041296,0.02323"
|
||||||
|
overlay1_desc15_overlay = img/rotate.png
|
||||||
|
overlay1_desc15_next_target = "GameBoy"
|
||||||
|
overlay1_desc16 = "rewind,0.05000,0.97500,radial,0.041296,0.02323"
|
||||||
|
overlay1_desc16_overlay =
|
||||||
|
overlay1_desc17 = "hold_fast_forward,0.95000,0.97500,radial,0.041296,0.02323"
|
||||||
|
overlay1_desc17_overlay =
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# tv_crt -- bundled desktop bezel
|
||||||
|
|
||||||
|
CRT television border from libretro's `common-overlays`
|
||||||
|
(`borders/tv-integer.cfg` + `borders/img/tv-integer.png`), licensed
|
||||||
|
CC-BY-4.0: https://github.com/libretro/common-overlays
|
||||||
|
|
||||||
|
`overlay.cfg` is the upstream file, unmodified. It is the reference
|
||||||
|
DESKTOP skin: 1920x1080, `descs = 0` (pure decoration, no touch buttons),
|
||||||
|
and a `viewport` naming the transparent screen hole, so the Game Boy
|
||||||
|
picture is fitted into the TV's tube instead of the whole window.
|
||||||
|
After Width: | Height: | Size: 1.9 MiB |
@@ -0,0 +1,6 @@
|
|||||||
|
overlays = 1
|
||||||
|
overlay0_overlay = img/tv-integer.png
|
||||||
|
overlay0_full_screen = true
|
||||||
|
overlay0_descs = 0
|
||||||
|
overlay0_viewport = "0.2335,0.0855,0.5335,0.830"
|
||||||
|
overlay0_viewport_fill = true
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -702,6 +702,23 @@ M.MT_MOON_B2F = {
|
|||||||
return false
|
return false
|
||||||
end,
|
end,
|
||||||
talk = {
|
talk = {
|
||||||
|
-- MtMoonB2FSuperNerdText: once beaten his line turns on the fossils
|
||||||
|
-- (scripts/MtMoonB2F.asm:187), which the header's flat `after` can't hold
|
||||||
|
TEXT_MTMOONB2F_SUPER_NERD = function(game, ow, npc, done)
|
||||||
|
if not superNerdBeaten(ow) then
|
||||||
|
engageSuperNerd(game, ow, done)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local TextBox = require("src.render.TextBox")
|
||||||
|
local t = game.data.text
|
||||||
|
local flags = game.save.flags
|
||||||
|
local line = (flags.EVENT_GOT_DOME_FOSSIL or flags.EVENT_GOT_HELIX_FOSSIL)
|
||||||
|
and (t._MtMoonB2FSuperNerdTheresAPokemonLabText
|
||||||
|
or "Far away, on\nCINNABAR ISLAND,\nthere's a POKéMON\nLAB.")
|
||||||
|
or (t._MtMoonB2fSuperNerdEachTakeOneText
|
||||||
|
or "We'll each take\none!\nNo being greedy!")
|
||||||
|
game.stack:push(TextBox.new(game, line, done))
|
||||||
|
end,
|
||||||
TEXT_MTMOONB2F_DOME_FOSSIL = mtMoonFossil(
|
TEXT_MTMOONB2F_DOME_FOSSIL = mtMoonFossil(
|
||||||
"DOME_FOSSIL", "MTMOONB2F_HELIX_FOSSIL", "EVENT_GOT_DOME_FOSSIL"),
|
"DOME_FOSSIL", "MTMOONB2F_HELIX_FOSSIL", "EVENT_GOT_DOME_FOSSIL"),
|
||||||
TEXT_MTMOONB2F_HELIX_FOSSIL = mtMoonFossil(
|
TEXT_MTMOONB2F_HELIX_FOSSIL = mtMoonFossil(
|
||||||
@@ -716,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
|
||||||
@@ -734,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
|
||||||
|
|||||||
@@ -118,33 +118,36 @@ M.ROUTE_15_GATE_2F = {
|
|||||||
|
|
||||||
M.MT_MOON_POKECENTER = {
|
M.MT_MOON_POKECENTER = {
|
||||||
talk = {
|
talk = {
|
||||||
TEXT_MTMOONPOKECENTER_MAGIKARP_SALESMAN = function(game, ow, npc, done)
|
-- command rows, not a Lua handler: give_pokemon needs a runner to AskName (#1407)
|
||||||
local t = text(game)
|
TEXT_MTMOONPOKECENTER_MAGIKARP_SALESMAN = {
|
||||||
if game.save.flags.EVENT_BOUGHT_MAGIKARP then
|
{ "check_flag", "EVENT_BOUGHT_MAGIKARP" },
|
||||||
push(game, t._MtMoonPokecenterMagikarpSalesmanNoRefundsText
|
{ "jump_if_true", "no_refunds" },
|
||||||
or "Well, I don't\ngive refunds!", done)
|
-- MONEY_BOX goes up between the offer and YesNoChoice -- MtMoonPokecenter.asm:31
|
||||||
return
|
{ "text_opts", { money = true } },
|
||||||
end
|
{ "ask", "_MtMoonPokecenterMagikarpSalesmanIGotADealText" },
|
||||||
ask(game, t._MtMoonPokecenterMagikarpSalesmanOfferText
|
{ "jump_if_false", "declined" },
|
||||||
or "MAGIKARP! A\nsteal at ¥500!\nWant one?", function(yes)
|
{ "check_money", 500 },
|
||||||
if not yes then
|
{ "jump_if_false", "no_money" },
|
||||||
push(game, t._MtMoonPokecenterMagikarpSalesmanNoText
|
{ "give_pokemon", "MAGIKARP", 5 },
|
||||||
or "No? I'm only\nselling today!", done)
|
-- MtMoonPokecenter.asm:49 `jr nc, .done`: a refused gift is never charged
|
||||||
return
|
{ "jump_if_false", "box_full" },
|
||||||
end
|
{ "take_money", 500 },
|
||||||
if game.save.money < 500 then
|
{ "set_flag", "EVENT_BOUGHT_MAGIKARP" },
|
||||||
push(game, t._MtMoonPokecenterMagikarpSalesmanNoMoneyText
|
{ "text_sound", "Get_Item1" },
|
||||||
or "You'll need more\nmoney than that!", done)
|
{ "show_text", "_GotMonText", { RAM = "MAGIKARP" } },
|
||||||
return
|
{ "jump", "end" },
|
||||||
end
|
{ "label", "box_full" },
|
||||||
game.save.money = game.save.money - 500
|
{ "show_text", "_BoxIsFullText" },
|
||||||
game.save.flags.EVENT_BOUGHT_MAGIKARP = true
|
{ "jump", "end" },
|
||||||
local Commands = require("src.script.Commands")
|
{ "label", "declined" },
|
||||||
Commands.give_pokemon({ save = game.save, game = game, overworld = ow },
|
{ "show_text", "_MtMoonPokecenterMagikarpSalesmanNoText" },
|
||||||
"MAGIKARP", 5)
|
{ "jump", "end" },
|
||||||
push(game, t._GotMonText or "{PLAYER} got\n{RAM:wNameBuffer}!", done)
|
{ "label", "no_money" },
|
||||||
end)
|
{ "show_text", "_MtMoonPokecenterMagikarpSalesmanNoMoneyText" },
|
||||||
end,
|
{ "jump", "end" },
|
||||||
|
{ "label", "no_refunds" },
|
||||||
|
{ "show_text", "_MtMoonPokecenterMagikarpSalesmanNoRefundsText" },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,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
|
||||||
|
|
||||||
@@ -513,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.
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ The short version, for an author deciding what to write:
|
|||||||
merged.** The write is taken, dropped, and named once per mod in the same
|
merged.** The write is taken, dropped, and named once per mod in the same
|
||||||
error feed the mod manager shows -- in both directions, so a Red boot writing
|
error feed the mod manager shows -- in both directions, so a Red boot writing
|
||||||
to `decorations` is told exactly as a Gold boot writing to `map_scripts` is.
|
to `decorations` is told exactly as a Gold boot writing to `map_scripts` is.
|
||||||
- **40 event names and 43 hook names have a call site in both generations**, so
|
- **40 event names and 44 hook names have a call site in both generations**, so
|
||||||
one subscription serves both games. `tests/engine/gate_gen2_mod_api.lua`
|
one subscription serves both games. `tests/engine/gate_gen2_mod_api.lua`
|
||||||
reads those names back out of the source and fails if a site is renamed or
|
reads those names back out of the source and fails if a site is renamed or
|
||||||
deleted on either side, and fails again if a new shared site appears without
|
deleted on either side, and fails again if a new shared site appears without
|
||||||
@@ -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.
|
||||||
@@ -537,11 +539,13 @@ gains a field instead of the name gaining a prefix.
|
|||||||
`battle.damage_dealt`, `battle.fainted`, `battle.status_inflicted`,
|
`battle.damage_dealt`, `battle.fainted`, `battle.status_inflicted`,
|
||||||
`battle.battler_switched`, `battle.ball_thrown`, `battle.exp_gained`,
|
`battle.battler_switched`, `battle.ball_thrown`, `battle.exp_gained`,
|
||||||
`pokemon.level_up`, `pokemon.move_learned`; hooks `battle.damage`,
|
`pokemon.level_up`, `pokemon.move_learned`; hooks `battle.damage`,
|
||||||
`battle.crit`, `battle.accuracy`, `battle.turn_order`,
|
`battle.crit`, `battle.accuracy`, `battle.charge_required`,
|
||||||
|
`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. |
|
||||||
@@ -122,21 +122,40 @@ Each object requires a stable `id`, a display `name`, a destination `file`
|
|||||||
digests. `format` is either `"raw"` (the default) or `"n64"`. An optional
|
digests. `format` is either `"raw"` (the default) or `"n64"`. An optional
|
||||||
`description` gives players dump or region guidance in the import panel.
|
`description` gives players dump or region guidance in the import panel.
|
||||||
`size` declares the exact canonical byte length; `max_size` declares a smaller
|
`size` declares the exact canonical byte length; `max_size` declares a smaller
|
||||||
per-import ceiling when an exact size is not appropriate. Every import also
|
per-import ceiling when an exact size is not appropriate. The engine hard limit
|
||||||
has an engine-enforced 128 MiB ceiling and is rejected before hashing when its
|
is 2 GiB. Imports above 128 MiB receive an explicit free-space confirmation and
|
||||||
filesystem reports an invalid size.
|
use the launcher's streaming large-file path rather than being materialized as
|
||||||
|
one Lua string.
|
||||||
|
|
||||||
For `"n64"`, the launcher recognizes `.z64`, `.v64`, and `.n64` byte orders,
|
For `"n64"`, the launcher recognizes `.z64`, `.v64`, and `.n64` byte orders,
|
||||||
strips a recognized 512-byte copier header, converts the bytes to canonical
|
strips a recognized 512-byte copier header, converts the bytes to canonical
|
||||||
big-endian `.z64` order, and then checks MD5. The canonical bytes are written
|
big-endian `.z64` order, and then checks MD5. The canonical bytes are written
|
||||||
to `mods/<mod-id>/baseroms/<file>`. Each selection is a private grant to that
|
to `mods/<mod-id>/baseroms/<file>`. Each selection is a private grant to that
|
||||||
mod: the launcher never scans or copies another mod's imported files merely
|
mod: the launcher never scans or copies another mod's imported files merely
|
||||||
because its manifest names the same digest. Mods read the result with their existing scoped `mod:read` API, for
|
because its manifest names the same digest. Small sources can still be read
|
||||||
example `mod:read("baseroms/stadium2.z64")`; no host path or new filesystem
|
with the existing scoped `mod:read` API, for example
|
||||||
|
`mod:read("baseroms/stadium2.z64")`. For large sources, prefer the bounded
|
||||||
|
`mod.imports` facade described below; no host path or new general filesystem
|
||||||
permission is exposed. Missing `required_imports` block the mod before its
|
permission is exposed. Missing `required_imports` block the mod before its
|
||||||
entry chunk runs; missing `optional_imports` remain visible in the same
|
entry chunk runs; missing `optional_imports` remain visible in the same
|
||||||
launcher panel but do not block loading.
|
launcher panel but do not block loading.
|
||||||
|
|
||||||
|
#### Bounded access to validated imports
|
||||||
|
|
||||||
|
A loaded mod can address only ids declared by its own `required_imports` or
|
||||||
|
`optional_imports` arrays:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local info, err = mod.imports:info("stadium2")
|
||||||
|
local header, err = mod.imports:read("stadium2", 0, 4096)
|
||||||
|
```
|
||||||
|
|
||||||
|
`read` uses zero-based offsets and is capped at 8 MiB per call. The engine
|
||||||
|
rechecks the stored import before exposing it, seeks into the engine-owned
|
||||||
|
copy, and never gives the mod a host path or file handle. This is intended for
|
||||||
|
large source formats whose table/index can be parsed with small reads before
|
||||||
|
selectively reading the payloads a transform actually needs.
|
||||||
|
|
||||||
MD5 here identifies a known dump because ROM databases commonly publish it;
|
MD5 here identifies a known dump because ROM databases commonly publish it;
|
||||||
it is not a security or authenticity guarantee. Do not paste the SHA-1 used by
|
it is not a security or authenticity guarantee. Do not paste the SHA-1 used by
|
||||||
Gen1Recomp's own game-ROM importer into an import's `md5` field. Mod archives
|
Gen1Recomp's own game-ROM importer into an import's `md5` field. Mod archives
|
||||||
@@ -268,10 +287,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
|
||||||
|
|
||||||
@@ -441,6 +460,28 @@ default** (1x front, 2x back).
|
|||||||
ball-to-pic grow multiplies your scale through each stage, so a rescaled
|
ball-to-pic grow multiplies your scale through each stage, so a rescaled
|
||||||
mon still grows into place from the ball, grounded the whole way.
|
mon still grows into place from the ball, grounded the whole way.
|
||||||
|
|
||||||
|
## Installation-scoped generated cache
|
||||||
|
|
||||||
|
Generated data derived from a validated user source often belongs to the mod
|
||||||
|
installation rather than to one Pokémon save. `mod.cache` is that namespace:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local ok, err = mod.cache:write("extract/v1/arena.bin", encodedArena)
|
||||||
|
local bytes, err = mod.cache:read("extract/v1/arena.bin")
|
||||||
|
local info = mod.cache:info("extract/v1/arena.bin")
|
||||||
|
mod.cache:delete("extract/v1/arena.bin")
|
||||||
|
```
|
||||||
|
|
||||||
|
The physical root is engine-owned (`mod_cache/<mod-id>/`) and never exposed to
|
||||||
|
the mod. Keys are safe relative paths and a single write is capped at 64 MiB.
|
||||||
|
The cache does not rewind with checkpoints and is not scoped to game version,
|
||||||
|
slot, or playthrough. The mod owns its generated format, fingerprints, rebuild
|
||||||
|
policy, and completion marker; the engine treats the bytes as opaque data.
|
||||||
|
|
||||||
|
Use `mod.storage` instead when the data belongs to one playthrough. Use
|
||||||
|
`mod.cache` when it is a reproducible installation artifact that can be rebuilt
|
||||||
|
from a declared user source.
|
||||||
|
|
||||||
## Durable tool storage and runtime checkpoints
|
## Durable tool storage and runtime checkpoints
|
||||||
|
|
||||||
`mod.save` remains the right place for state that should travel with the next
|
`mod.save` remains the right place for state that should travel with the next
|
||||||
@@ -633,6 +674,17 @@ the selected indices. Mods remain responsible for selection policy and should
|
|||||||
use only public `mod.ui`, hook, and save APIs. See RFC 0010 for the exact
|
use only public `mod.ui`, hook, and save APIs. See RFC 0010 for the exact
|
||||||
contract and compatibility guarantees.
|
contract and compatibility guarantees.
|
||||||
|
|
||||||
|
Both battle engines expose the guarded `battle.charge_required` hook when a
|
||||||
|
charge-capable move is selected for its initial turn and the active ruleset
|
||||||
|
would otherwise charge it. The wrapper receives `(next, ctx)`, where `ctx` is
|
||||||
|
`{ battle, user, target, move, charge = true, isCalled }`. Return `false` to
|
||||||
|
skip only that initial charge and continue through the ordinary move pipeline;
|
||||||
|
call `next(ctx)` to keep it. The hook does not run for the release turn or when
|
||||||
|
the active ruleset already skips charging (for example, Gold Solarbeam in
|
||||||
|
sun). PP use, accuracy, damage, animation, and secondary effects remain owned
|
||||||
|
by the engine. With no subscriber, the vanilla decision runs without building
|
||||||
|
the hook context.
|
||||||
|
|
||||||
## Developer console
|
## Developer console
|
||||||
|
|
||||||
Boot with developer mode on to unlock the in-game console and hot-reload
|
Boot with developer mode on to unlock the in-game console and hot-reload
|
||||||
|
|||||||
@@ -11,28 +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
|
||||||
* **Translation and custom font support**
|
* **Screen position setting** (center, upper, top) shared across all games, for clamp-on controllers that cover the lower screen
|
||||||
* **Built-in save editor** for parties, boxes, items, events, maps, and Pokédex data
|
* **Touch skins** in RetroArch overlay format and Delta `.deltaskin` (including PDF-wrapped bezel art), with per-button press states and Super Game Boy borders
|
||||||
* **Tiled map editing tools** for mod authors
|
|
||||||
* **Pokédex diploma and printer image exports**
|
* **Pokédex diploma and printer image exports**
|
||||||
* **Community mod browser**
|
* **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
|
||||||
* **Soft reset button combination**
|
|
||||||
* **Keyboard and controller rebinding**
|
|
||||||
* **Mod profiles** with separate mod settings and save slots
|
|
||||||
* **Sandboxed mods**: an installed mod can read only its own folder and write only its own storage
|
|
||||||
* **Improved launcher and save editor UI**, including background downloads and update checks
|
|
||||||
* **Direct-launch options** for shortcuts, Steam entries, and handheld frontends
|
|
||||||
|
|
||||||
## Pokémon Gold (Gen 2)
|
## Gen 2 Specifics
|
||||||
|
|
||||||
* **COLOR, zoom, tilt, GBC FX, and quick save/load**
|
* **Pokémon Silver** as an importable, launcher-selectable version alongside Gold
|
||||||
* **UI that stays fixed while the overworld zooms**
|
|
||||||
* **Border-block surrounds** for maps smaller than the screen
|
|
||||||
* **Gold-specific launcher options**
|
|
||||||
* **Optional widescreen battle layout**
|
|
||||||
* **Skippable trade animation** with B or START
|
|
||||||
* **QUIT and EXIT GAME** from the menus
|
|
||||||
* **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
|
||||||
* **On-screen touch pad** and controller SELECT for registered items
|
|
||||||
* **Older mods keep loading** after the sandbox change, through per-mod compat stand-ins for the pre-sandbox globals
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
# RFC 0008 — Streamed mod imports and installation-scoped generated cache
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
|
||||||
|
`required_imports`/`optional_imports` can now describe files up to 2 GiB, but
|
||||||
|
the existing launcher and public mod API still assume imported bytes are small:
|
||||||
|
|
||||||
|
* the Windows desktop picker stages a selected required import through a fixed
|
||||||
|
`%TEMP%/pokeport_required_import.bin` path before validation;
|
||||||
|
* the fallback import path materializes the selected file as one Lua string;
|
||||||
|
* after validation a mod can only use `mod:read("baseroms/...")`, which also
|
||||||
|
materializes the whole file;
|
||||||
|
* `mod.storage` is intentionally scoped to one Pokémon playthrough, so it is
|
||||||
|
not an appropriate home for a one-time generated asset cache shared by every
|
||||||
|
save using the same installed mod.
|
||||||
|
|
||||||
|
This makes optical-disc-sized user sources impractical even though the manifest
|
||||||
|
schema already accepts them. A failed temporary staging copy can also turn a
|
||||||
|
valid large source into a smaller temporary file and produce a misleading
|
||||||
|
"wrong file size" rejection.
|
||||||
|
|
||||||
|
A mod should be able to consume its own already-validated source incrementally
|
||||||
|
and compile derived runtime data once, without receiving a host path or general
|
||||||
|
filesystem access.
|
||||||
|
|
||||||
|
## Decision being extended
|
||||||
|
|
||||||
|
This extends the same legal/sandbox direction as **D11 asset transforms**
|
||||||
|
(`src/mods/AssetTransform.lua`): mods distribute recipes and derive bytes from
|
||||||
|
user-owned sources rather than shipping ROM-derived data. It also follows the
|
||||||
|
**D14 parity-gate** contract referenced by `tests/harness.lua` and
|
||||||
|
`tests/engine/gate_meta_coverage.lua` (the `21-testing-and-ci` plan): additive
|
||||||
|
extension points ship public-API coverage, no-mod parity coverage, and docs in
|
||||||
|
the same change.
|
||||||
|
|
||||||
|
The historical D11 plan document is referenced by source comments but is not
|
||||||
|
present in the current repository tree; this RFC is the checked-in design
|
||||||
|
record for the new surface.
|
||||||
|
|
||||||
|
## Exact API delta
|
||||||
|
|
||||||
|
No manifest field changes. Existing `required_imports` and `optional_imports`
|
||||||
|
remain the declaration/validation authority.
|
||||||
|
|
||||||
|
Two additive facades are added to the `mod` object.
|
||||||
|
|
||||||
|
### `mod.imports`
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local info, err = mod.imports:info("source_id")
|
||||||
|
local bytes, err = mod.imports:read("source_id", offset, length)
|
||||||
|
```
|
||||||
|
|
||||||
|
* `source_id` must name an import declared by the calling mod.
|
||||||
|
* the import is rechecked through `RequiredImports.validateStored` before it is
|
||||||
|
exposed, so missing, replaced, or invalid optional imports are not readable;
|
||||||
|
* `offset` and `length` are zero-based byte coordinates;
|
||||||
|
* one read is capped at 8 MiB;
|
||||||
|
* no host path or file handle is returned;
|
||||||
|
* production reads seek into the engine-owned stored copy instead of reading
|
||||||
|
the whole source.
|
||||||
|
|
||||||
|
`info()` returns declaration metadata plus stored size. It does not expose a
|
||||||
|
host path.
|
||||||
|
|
||||||
|
### `mod.cache`
|
||||||
|
|
||||||
|
```lua
|
||||||
|
mod.cache:write("extract/v1/model.bin", bytes)
|
||||||
|
local bytes = mod.cache:read("extract/v1/model.bin")
|
||||||
|
local info = mod.cache:info("extract/v1/model.bin")
|
||||||
|
mod.cache:delete("extract/v1/model.bin")
|
||||||
|
```
|
||||||
|
|
||||||
|
The cache is rooted at `mod_cache/<mod-id>/`, follows the engine persistence
|
||||||
|
backend, and is independent of game version, launcher slot, and playthrough.
|
||||||
|
Paths are checked with `SafePath`; `..`, absolute paths, drive paths, and other
|
||||||
|
escapes remain unavailable. A single cache write is capped at 64 MiB so large
|
||||||
|
generated datasets are naturally split into independently replaceable files.
|
||||||
|
|
||||||
|
The engine does not interpret cache bytes. Mods own generated-format versioning,
|
||||||
|
fingerprints, transactional completion markers, and rebuild policy.
|
||||||
|
|
||||||
|
## Launcher/import transport delta
|
||||||
|
|
||||||
|
For large raw required imports:
|
||||||
|
|
||||||
|
1. desktop pickers return the original selected path instead of staging it
|
||||||
|
through a fixed temporary file;
|
||||||
|
2. the engine opens that source itself;
|
||||||
|
3. bytes are copied directly to the existing engine-owned
|
||||||
|
`mods/<id>/baseroms/<file>` destination in 4 MiB chunks;
|
||||||
|
4. MD5 is updated incrementally during the copy;
|
||||||
|
5. the normal size/MD5 validation receipt is written only after the complete
|
||||||
|
destination passes validation;
|
||||||
|
6. partial destinations are removed on short reads, write failure, size
|
||||||
|
mismatch, or digest mismatch.
|
||||||
|
|
||||||
|
N64 imports stay on the existing canonicalization path because byte-order and
|
||||||
|
copier-header normalization require transformation rather than a raw copy.
|
||||||
|
|
||||||
|
If a validation receipt for an already-stored large raw import is missing, the
|
||||||
|
engine rebuilds it with streaming MD5 rather than a whole-file read.
|
||||||
|
|
||||||
|
## Backward compatibility / migration
|
||||||
|
|
||||||
|
**Existing mods do nothing.** This is additive.
|
||||||
|
|
||||||
|
* manifest v1/v2 fields are unchanged;
|
||||||
|
* `mod:read`, `mod.storage`, registries, events, hooks, and legacy compatibility
|
||||||
|
retain their existing behavior;
|
||||||
|
* small required imports retain the existing in-memory validation path;
|
||||||
|
* N64 imports retain canonicalization and existing accepted byte orders;
|
||||||
|
* a mod that never touches `mod.imports` or `mod.cache` creates no new cache
|
||||||
|
files and observes no new behavior.
|
||||||
|
|
||||||
|
The mod API integer is not bumped because no existing member changes meaning or
|
||||||
|
shape.
|
||||||
|
|
||||||
|
## Security and legal posture
|
||||||
|
|
||||||
|
The launcher remains the authority that validates user-supplied bytes. The new
|
||||||
|
facade narrows access rather than widening it: a mod can read only ids declared
|
||||||
|
in its own manifest, only after validation, and only in bounded ranges. It does
|
||||||
|
not receive host paths, `io`, or a raw filesystem handle.
|
||||||
|
|
||||||
|
`mod.cache` is writable only beneath the calling mod's generated-cache root.
|
||||||
|
Nothing in this RFC permits packaged ROM-derived bytes; `modkit lint/pack`
|
||||||
|
continue to enforce the existing legal posture.
|
||||||
|
|
||||||
|
## Parity guarantee
|
||||||
|
|
||||||
|
The change ships with:
|
||||||
|
|
||||||
|
* a no-mod/API-v1 parity test proving an empty load and an existing v1-style
|
||||||
|
`mod:read` load do not create cache data or change the old surface;
|
||||||
|
* a public mod-API test that reaches `mod.imports` and `mod.cache` through a
|
||||||
|
real `Loader` load, including bounded reads, undeclared/missing imports,
|
||||||
|
cache isolation, and traversal rejection;
|
||||||
|
* incremental MD5 vectors and a large-import streaming regression test;
|
||||||
|
* the existing engine suite, required-import suite, and mod lint gates.
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# RFC 0011: Charge-required battle hook
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Proposed.
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
|
||||||
|
A battle-mechanics mod can change damage through `battle.damage` and register
|
||||||
|
move effects, but it cannot conditionally skip the first turn of an existing
|
||||||
|
charge move. In Gen 1, the engine decides and stores the charge continuation
|
||||||
|
before any public effect callback can run. Reaching into `user.charging`,
|
||||||
|
`user.chargeReady`, or generation-specific volatile state is private,
|
||||||
|
checkpoint-fragile, and would require a mod to duplicate move-pipeline policy.
|
||||||
|
|
||||||
|
Weather is the immediate example: a portable sun rule needs Solarbeam to
|
||||||
|
resolve on selection while leaving Fly, Dig, PP use, hit resolution, animation,
|
||||||
|
and secondary effects to the engine. The capability is generic and useful to
|
||||||
|
other ruleset and move-mechanics mods.
|
||||||
|
|
||||||
|
## Decision and plan extended
|
||||||
|
|
||||||
|
This implements **D-AT-002: charge-stage policy remains mod authority through a
|
||||||
|
generic guarded engine decision seam**. The consuming design is tracked in the
|
||||||
|
Adaptive Trainers implementation plan,
|
||||||
|
[`docs/superpowers/plans/2026-08-14-adaptive-trainers.md`](https://github.com/MaxTomahawk/gen1recomp-adaptive-trainers/blob/main/docs/superpowers/plans/2026-08-14-adaptive-trainers.md),
|
||||||
|
Task 8. The delta follows the additive, guarded hook convention documented by
|
||||||
|
Route B in `CONTRIBUTING-mods.md`; it contains no weather, move-id, trainer, or
|
||||||
|
Adaptive Trainers policy.
|
||||||
|
|
||||||
|
## Exact API delta
|
||||||
|
|
||||||
|
Both the Gen 1 and Gen 2 battle engines add this guarded hook:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
mod.hooks:wrap("battle.charge_required", function(next, ctx)
|
||||||
|
-- ctx = {
|
||||||
|
-- battle = live battle controller,
|
||||||
|
-- user = attacking battler,
|
||||||
|
-- target = defending battler,
|
||||||
|
-- move = merged move record,
|
||||||
|
-- charge = true,
|
||||||
|
-- isCalled = false,
|
||||||
|
-- }
|
||||||
|
if should_resolve_now(ctx) then return false end
|
||||||
|
return next(ctx)
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
The call site is the initial-use charge decision, after announcement and PP
|
||||||
|
handling but before charge state, invulnerability, charge animation, or charge
|
||||||
|
text is created. It runs only when the active engine rules would otherwise
|
||||||
|
require a charge. It does not run on the release turn. Returning exactly
|
||||||
|
`false` skips that initial charge and continues through the engine-owned move
|
||||||
|
pipeline. Any other downstream return preserves the charge. `isCalled` is true
|
||||||
|
when Metronome or Mirror Move selected the move.
|
||||||
|
|
||||||
|
Gold keeps its native sun decision first, so Solarbeam in native sun already
|
||||||
|
requires no charge and does not invoke the hook. Gen 1 link battles use the
|
||||||
|
shared Gen 1 move pipeline and therefore receive the same seam; normal link
|
||||||
|
mod-compatibility rules continue to govern deterministic peers.
|
||||||
|
|
||||||
|
The hot path first calls `Runtime.wantsHook("battle.charge_required")`. With no
|
||||||
|
subscriber, no hook payload table is allocated and the existing branch runs
|
||||||
|
unchanged.
|
||||||
|
|
||||||
|
## Migration and compatibility
|
||||||
|
|
||||||
|
Existing mods change nothing. The hook name and payload are additive. With no
|
||||||
|
wrapper installed, Red, Blue, Yellow, Gold, and Silver retain their previous
|
||||||
|
charge state, PP use, text, animation, accuracy, damage, and native weather
|
||||||
|
behavior. Existing charge-move data and effect records require no migration.
|
||||||
|
|
||||||
|
A mod adopting the seam should call `next(ctx)` unless it deliberately wants to
|
||||||
|
skip this charge. It should not mutate private charge fields or re-run the move.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- `tests/engine/battle_charge_required.lua` exercises the real Gen 1 and Gen 2
|
||||||
|
engines through a sandboxed public mod, including false-to-skip, next-to-keep,
|
||||||
|
release-turn behavior, called-move PP semantics, shared payload shape, and
|
||||||
|
native Gold sun behavior.
|
||||||
|
- The same test proves no-mod charge/release parity and replaces
|
||||||
|
`Runtime.call` with a sentinel behind a false `Runtime.wantsHook` guard.
|
||||||
|
- `tests/engine/gate_hooks.lua` discovers the new catalog name and proves empty
|
||||||
|
chains preserve vanilla values and allocation behavior.
|
||||||
|
- `tests/engine/gate_gen2_mod_api.lua` requires a guarded site in both
|
||||||
|
generations and keeps the compatibility reference list complete.
|
||||||
|
|
||||||
|
## Deprecation etiquette
|
||||||
|
|
||||||
|
Nothing is removed, renamed, superseded, or deprecated.
|
||||||
@@ -0,0 +1,317 @@
|
|||||||
|
# Touch skins and the Skin Studio
|
||||||
|
|
||||||
|
A **skin** replaces the on-screen controls wholesale: a bezel image, a
|
||||||
|
control layout, and a screen-placement anchor. Engine:
|
||||||
|
`src/core/TouchSkin.lua` (model, parsers, zip export), `src/core/TouchControls.lua`
|
||||||
|
(draw and input), `src/render/Renderer.lua` (screen placement),
|
||||||
|
`src/core/DeltaSkin.lua` (Delta `.deltaskin` import and export),
|
||||||
|
`src/ui/SkinStudio.lua` (the responsive skin editor). Tests:
|
||||||
|
`tests/engine/touch_skin_test.lua`, `tests/engine/skin_studio_test.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`.
|
||||||
|
|
||||||
|
The launcher's **Skins** tab imports skins, shows the enabled skin, exports it,
|
||||||
|
and is the one place that turns skin use off. **My Skins** holds the visual
|
||||||
|
grid, pagination, edit, delete and per-skin export actions.
|
||||||
|
`options.touchControls.skin` holds the folder name.
|
||||||
|
|
||||||
|
## Formats
|
||||||
|
|
||||||
|
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
|
||||||
|
as-is. Supported keys:
|
||||||
|
|
||||||
|
| Key | Meaning |
|
||||||
|
| --- | --- |
|
||||||
|
| `overlays` | page count |
|
||||||
|
| `overlayN_name` | page name, the target of `next_target` |
|
||||||
|
| `overlayN_overlay` | bezel image |
|
||||||
|
| `overlayN_full_screen` | cover the window with the page without deforming its artwork |
|
||||||
|
| `overlayN_rect` | page placement, default `0,0,1,1` |
|
||||||
|
| `overlayN_aspect_ratio` | design aspect; the overlay letterboxes to it even when full screen |
|
||||||
|
| `overlayN_range_mod`, `overlayN_alpha_mod` | desc defaults |
|
||||||
|
| `overlayN_viewport` | `x,y,w,h`, the screen cutout |
|
||||||
|
| `overlayN_viewport_fill` | parsed; the engine always fits, see below |
|
||||||
|
| `overlayN_descM` | `binds,x,y,shape,range_x,range_y` |
|
||||||
|
| `overlayN_descM_overlay` | control art |
|
||||||
|
| `overlayN_descM_next_target` | page to switch to |
|
||||||
|
| `overlayN_descM_range_mod`, `_alpha_mod` | per-control overrides |
|
||||||
|
| `overlayN_descM_reach_x/_y/_up/_down/_left/_right` | hitbox reach |
|
||||||
|
|
||||||
|
`x,y` is the centre and `range_x,range_y` are half extents, both normalized.
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
fades it out, and both directions read as a press animation.
|
||||||
|
|
||||||
|
**Native `skin.lua`.** This module's own model written back out: one Lua
|
||||||
|
table, no flat key space, and a separate `imagePressed` per control that a
|
||||||
|
`.cfg` cannot express. Loaded with an empty environment, so a skin authored by
|
||||||
|
a stranger cannot reach `love` or `io`. Sizes here are full width and height
|
||||||
|
rather than RetroArch's half extents, because that is what an editor's numeric
|
||||||
|
fields mean.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
return {
|
||||||
|
name = "my_skin",
|
||||||
|
pages = {
|
||||||
|
{
|
||||||
|
name = "main",
|
||||||
|
image = "img/bezel.png",
|
||||||
|
fullScreen = true,
|
||||||
|
viewport = { x = 0.0, y = 0.0, w = 1.0, h = 0.5, fill = false },
|
||||||
|
controls = {
|
||||||
|
{ bind = "a", x = 0.87, y = 0.72, w = 0.18, h = 0.10,
|
||||||
|
shape = "radial", image = "img/a.png", imagePressed = "img/a_down.png" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**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 fit the
|
||||||
|
game into it. 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
|
||||||
|
|
||||||
|
The eight Game Boy buttons: `a`, `b`, `start`, `select`, `up`, `down`,
|
||||||
|
`left`, `right`.
|
||||||
|
|
||||||
|
Engine hotkeys, handled in `Game:touchSkinHotkey`:
|
||||||
|
|
||||||
|
| Bind | Effect |
|
||||||
|
| --- | --- |
|
||||||
|
| `overlay_next`, `overlay_previous` | switch page, honouring `next_target` |
|
||||||
|
| `hold_fast_forward`, `fast_forward` | fast forward while held |
|
||||||
|
| `toggle_fast_forward` | step the speed option |
|
||||||
|
| `reset` | soft reset to the title |
|
||||||
|
| `menu_toggle` | open OPTIONS |
|
||||||
|
|
||||||
|
`screenshot`, `pause_toggle` and `exit_emulator` are recognised but have no
|
||||||
|
handler yet: a control bound to them draws and does nothing. Anything else,
|
||||||
|
`rewind` included, is not in the bind table at all, so the control falls back
|
||||||
|
to decoration and never captures a touch.
|
||||||
|
|
||||||
|
As an extension to the format, `key:<name>` presses any keyboard key, which is
|
||||||
|
how a skin button reaches a mod hotkey.
|
||||||
|
|
||||||
|
## Screen placement
|
||||||
|
|
||||||
|
`overlayN_viewport` is the cutout the picture is fitted into. The Game Boy
|
||||||
|
screen keeps its whole-pixel scale and letterboxes inside that rect rather than
|
||||||
|
stretching to it, so a bezel gets an exact 160x144 picture; `viewport_fill` is
|
||||||
|
parsed but does not stretch. `overlayN_viewport_expand = true` is an extension
|
||||||
|
that lets a widescreen bezel take the filling survey-zoom world view instead.
|
||||||
|
|
||||||
|
A viewport also implies the faithful-ratio lock. Without it the world pass
|
||||||
|
expands to fill the cutout and you get more map instead of a Game Boy screen.
|
||||||
|
|
||||||
|
Zoom still steps around that hole: OUT shows more map inside it, IN enlarges
|
||||||
|
the world, and the start menu stays at the hole's fit scale instead of
|
||||||
|
shrinking with the map.
|
||||||
|
|
||||||
|
An image-backed portrait overlay that has no explicit vertical anchor is treated
|
||||||
|
as a controller deck: it is contained without deformation and pinned to the
|
||||||
|
bottom on taller screens. The spare space belongs to the game above it.
|
||||||
|
|
||||||
|
When a skin is active, **SCREEN POS** reads **SKIN**: placement comes from the
|
||||||
|
skin rather than the normal Center / Upper / Top setting.
|
||||||
|
|
||||||
|
Border art often ships with a transparent hole and no `viewport` key. **Detect
|
||||||
|
screen from bezel** in the studio measures the hole out of the art's alpha
|
||||||
|
channel and writes the rect.
|
||||||
|
|
||||||
|
## Bezels versus pads
|
||||||
|
|
||||||
|
A skin whose active page binds nothing is a frame rather than a pad: a TV
|
||||||
|
surround, a handheld shell, a Super Game Boy border. Selected skins draw on
|
||||||
|
**desktop** as well as mobile; a gamepad does not hide them.
|
||||||
|
|
||||||
|
## Installing
|
||||||
|
|
||||||
|
Four roads, all of them landing in `skins/` in the save directory:
|
||||||
|
|
||||||
|
* **Import** on the Skins tab opens the host file picker for a `.zip` or a
|
||||||
|
`.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:
|
||||||
|
|
||||||
|
| Skin | Source | Shape |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `gb_anim` | `gamepads/gb_anim_portrait` | handheld shell, working buttons, two pages |
|
||||||
|
| `tv_crt` | `borders/tv-integer` | CRT television frame, no buttons |
|
||||||
|
|
||||||
|
Attribution lives in each folder's `README.md`. `tv_crt` is a photograph of a
|
||||||
|
real television: CC-BY-4.0 upstream, but treat it as a test asset rather than
|
||||||
|
shipping branding.
|
||||||
|
|
||||||
|
## The studio
|
||||||
|
|
||||||
|
Launcher, Skins tab, **My Skins** opens the Studio library on desktop and
|
||||||
|
mobile. **My Skins** is the only visual grid: real bezel previews plus create
|
||||||
|
and import actions, with each card owning Edit, Export and (for installed
|
||||||
|
skins) Delete. Choosing Edit opens a separate, canvas-first editor; the old
|
||||||
|
New/Load workspace controls are deliberately not duplicated inside that editor.
|
||||||
|
|
||||||
|
The editor keeps its canvas unobstructed and puts the contextual actions in a
|
||||||
|
compact lower tray: add/control binding, button and bezel artwork, pages,
|
||||||
|
screen placement, freeform/10:9 screen shape and deletion. **Screen** opens
|
||||||
|
cutout, bezel-hole detect, **Detect this screen**, and the canvas presets.
|
||||||
|
**Detect this screen** (also on the tray) sets the mock device to the live
|
||||||
|
window size so a phone skin is authored at that phone's form factor rather
|
||||||
|
than a generic 1080x1920 16:9. **Zoom −** shrinks the mock device inside the
|
||||||
|
workspace so the screen hole can be dragged larger than the bezel while the
|
||||||
|
handles stay grabable; **Fit** restores contain. The mouse wheel over the
|
||||||
|
canvas, and `-` / `=` / `0` on a keyboard, do the same. Touches select, drag and resize the
|
||||||
|
same controls that a mouse edits on desktop.
|
||||||
|
|
||||||
|
My Skins and the editor chrome sit inside the platform safe area (notch,
|
||||||
|
status bar, home indicator), the same inset the launcher uses. The mock
|
||||||
|
device still represents the full window, because a skin covers the whole
|
||||||
|
screen at play time.
|
||||||
|
|
||||||
|
The launcher’s **Turn skins off** button clears the selected skin and disables
|
||||||
|
skin use. With no skin enabled, mobile falls back to the built-in pad; that pad
|
||||||
|
is not itself a skin card.
|
||||||
|
|
||||||
|
**Canvas.** A mock device at a chosen preset, so a phone skin is authored at
|
||||||
|
phone proportions on a desktop monitor.
|
||||||
|
|
||||||
|
| Preset | Size |
|
||||||
|
| --- | --- |
|
||||||
|
| Phone portrait / landscape | 1080x1920, 1920x1080 |
|
||||||
|
| This screen | the live window, so a phone is authored at its own height |
|
||||||
|
| Tablet portrait / landscape | 1536x2048, 2048x1536 |
|
||||||
|
| Steam Deck | 1280x800 |
|
||||||
|
| Desktop 1080p | 1920x1080 |
|
||||||
|
| Ultrawide 21:9 | 2560x1080 |
|
||||||
|
| Super Game Boy border | 256x224 |
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
**Editing.** Click a control to select it, drag to move, eight handles to
|
||||||
|
resize. Arrow keys nudge the selection one canvas pixel, shift-arrow ten. While
|
||||||
|
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 anchor are
|
||||||
|
per page. The SCREEN anchor is itself draggable and resizable; its default
|
||||||
|
shape is freeform, with an optional 10:9 lock.
|
||||||
|
|
||||||
|
**Bind** opens a grid of every bind the engine understands: the eight Game Boy
|
||||||
|
buttons, the diagonal pairs, every hotkey, desktop hotkeys and decoration.
|
||||||
|
The desktop section exposes `-` / `=`, `1` through `5`, `F1`, `F2` and `F10`
|
||||||
|
as `key:` controls, so a mobile button invokes the exact same game path as
|
||||||
|
its desktop shortcut. 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 anchor: press **Detect screen from bezel** to measure it out of
|
||||||
|
the art's alpha.
|
||||||
|
|
||||||
|
**Testing.** **Test** renders a game-composition preview behind the live overlay:
|
||||||
|
the 160x144 picture letterboxes inside the screen cutout, matching gameplay.
|
||||||
|
Clicking presses real Game Boy buttons and the footer reports what is held.
|
||||||
|
**Play** saves the skin, selects it, and boots the game with it.
|
||||||
|
|
||||||
|
**Saving.** **Save** writes `skins/<name>/skin.lua` and copies every image the
|
||||||
|
skin names, so the folder stands alone. **Export** offers three formats, and
|
||||||
|
the Skins tab's gear offers the same three for any installed skin:
|
||||||
|
|
||||||
|
| 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
|
||||||
|
|
||||||
|
True vector Delta skins (PDF artwork with no embedded JPEG). Those still need
|
||||||
|
a PDF renderer this engine does not carry, so they are refused with a message
|
||||||
|
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,9 +121,18 @@ 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
|
||||||
|
|
||||||
|
|
||||||
- **`love.run` persists across handoff.** By the time `chainload` runs, the
|
- **`love.run` persists across handoff.** By the time `chainload` runs, the
|
||||||
bundled `love.run` has already returned its stepper to LOVE; redefining the
|
bundled `love.run` has already returned its stepper to LOVE; redefining the
|
||||||
global `love.run` from the payload's `main.lua` does not affect the loop
|
global `love.run` from the payload's `main.lua` does not affect the loop
|
||||||
@@ -139,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
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local Game, EditorApp, Importer, TouchEditor
|
local Game, EditorApp, Importer, TouchEditor, Studio
|
||||||
|
|
||||||
-- #887: quit-to-launcher state, shared by love.load and love.quit (both need
|
-- #887: quit-to-launcher state, shared by love.load and love.quit (both need
|
||||||
-- it, so it is declared here rather than next to love.quit).
|
-- it, so it is declared here rather than next to love.quit).
|
||||||
@@ -255,7 +255,118 @@ function closeTouchControlsEditor()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function bootGame(version)
|
-- ------------------------------------------------------------ skin studio
|
||||||
|
local studioHost
|
||||||
|
local closeSkinStudio
|
||||||
|
local bootGame
|
||||||
|
|
||||||
|
local function openSkinStudio(version, skinId)
|
||||||
|
local SkinStudio = require("src.ui.SkinStudio")
|
||||||
|
if not SkinStudio.available_desktop() then return end
|
||||||
|
studioHost = Importer
|
||||||
|
if Importer and Importer.prepareOverlayHandoff then
|
||||||
|
Importer:prepareOverlayHandoff()
|
||||||
|
end
|
||||||
|
Importer = nil
|
||||||
|
Studio = SkinStudio
|
||||||
|
Studio.load({
|
||||||
|
version = version,
|
||||||
|
skinId = skinId,
|
||||||
|
onClose = function() closeSkinStudio() end,
|
||||||
|
onPlay = function(v)
|
||||||
|
closeSkinStudio()
|
||||||
|
Importer = nil
|
||||||
|
bootGame(v or version)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function closeSkinStudio()
|
||||||
|
if Studio and Studio.unload then Studio.unload() end
|
||||||
|
Studio = nil
|
||||||
|
Importer = studioHost
|
||||||
|
studioHost = nil
|
||||||
|
if Importer and Importer.resumeAfterOverlay then
|
||||||
|
Importer:resumeAfterOverlay()
|
||||||
|
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,
|
||||||
|
-- Skin Studio owns a touch-first layout as well as the desktop workspace.
|
||||||
|
-- Keep the compatibility predicate so external hosts using it still work.
|
||||||
|
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)
|
||||||
-- 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.
|
||||||
-- Set the active version and overlay its extracted cache BEFORE anything
|
-- Set the active version and overlay its extracted cache BEFORE anything
|
||||||
@@ -278,11 +389,12 @@ local 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
|
||||||
@@ -346,7 +458,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())
|
||||||
@@ -406,8 +518,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
|
||||||
@@ -448,15 +560,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,
|
|
||||||
})
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function love.update(dt)
|
function love.update(dt)
|
||||||
@@ -466,7 +570,9 @@ function love.update(dt)
|
|||||||
NxDisplay.sync()
|
NxDisplay.sync()
|
||||||
if editorMode then return EditorApp.update(dt) end
|
if editorMode then return EditorApp.update(dt) end
|
||||||
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 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
|
||||||
@@ -522,6 +628,12 @@ function love.draw()
|
|||||||
HostDisplay.endFrame("touch_editor", TouchEditor)
|
HostDisplay.endFrame("touch_editor", TouchEditor)
|
||||||
return result
|
return result
|
||||||
end
|
end
|
||||||
|
if Studio then
|
||||||
|
HostDisplay.beginFrame("skin_studio", Studio)
|
||||||
|
local result = Studio.draw()
|
||||||
|
HostDisplay.endFrame("skin_studio", Studio)
|
||||||
|
return result
|
||||||
|
end
|
||||||
if Importer then
|
if Importer then
|
||||||
GameViewport.reset()
|
GameViewport.reset()
|
||||||
HostDisplay.beginFrame("launcher", Importer)
|
HostDisplay.beginFrame("launcher", Importer)
|
||||||
@@ -555,13 +667,16 @@ end
|
|||||||
function love.keypressed(key, scancode, isrepeat)
|
function love.keypressed(key, scancode, isrepeat)
|
||||||
if editorMode then return EditorApp.keypressed(key) end
|
if editorMode then return EditorApp.keypressed(key) end
|
||||||
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 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 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
|
||||||
|
|
||||||
@@ -579,7 +694,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
|
||||||
|
|
||||||
@@ -597,7 +714,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
|
||||||
|
|
||||||
@@ -615,7 +734,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
|
||||||
|
|
||||||
@@ -633,7 +754,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
|
||||||
|
|
||||||
@@ -651,7 +774,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
|
||||||
|
|
||||||
@@ -669,7 +794,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
|
||||||
|
|
||||||
@@ -687,21 +814,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
|
||||||
|
|
||||||
@@ -710,29 +841,81 @@ 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
|
||||||
|
local Sound = package.loaded["src.core.Sound"]
|
||||||
|
if Sound then pcall(Sound.onDeviceReset) 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
|
||||||
@@ -750,12 +933,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 Studio.touchpressed(id, x, y) 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
|
||||||
|
|
||||||
@@ -765,9 +950,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 Studio.touchmoved(id, x, y) 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
|
||||||
|
|
||||||
@@ -777,9 +964,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 Studio.touchreleased(id, x, y) 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
|
||||||
|
|
||||||
@@ -789,7 +978,9 @@ function love.wheelmoved(x, y)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
if TouchEditor then return end
|
if TouchEditor then return 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
|
||||||
|
|
||||||
@@ -826,6 +1017,12 @@ function love.mousepressed(x, y, button, istouch)
|
|||||||
if love.system.getOS() == "Android" then return end
|
if love.system.getOS() == "Android" then return end
|
||||||
return TouchEditor.mousepressed(x, y, button)
|
return TouchEditor.mousepressed(x, y, button)
|
||||||
end
|
end
|
||||||
|
if Studio then
|
||||||
|
-- Mobile LÖVE sends both a touch event and an `istouch` mouse twin.
|
||||||
|
-- Studio consumes the real finger stream above, so discard the twin.
|
||||||
|
if istouch and (love.system.getOS() == "Android" or love.system.getOS() == "iOS") then return end
|
||||||
|
return Studio.mousepressed(x, y, button)
|
||||||
|
end
|
||||||
if Importer then
|
if Importer then
|
||||||
-- love.touchpressed already forwards the primary touch into FlexLove for
|
-- love.touchpressed already forwards the primary touch into FlexLove for
|
||||||
-- scroll. LÖVE ALSO synthesizes a mouse press for that same touch; if both
|
-- scroll. LÖVE ALSO synthesizes a mouse press for that same touch; if both
|
||||||
@@ -860,6 +1057,10 @@ function love.mousereleased(x, y, button, istouch)
|
|||||||
if love.system.getOS() == "Android" then return end
|
if love.system.getOS() == "Android" then return end
|
||||||
return TouchEditor.mousereleased(x, y, button)
|
return TouchEditor.mousereleased(x, y, button)
|
||||||
end
|
end
|
||||||
|
if Studio then
|
||||||
|
if istouch and (love.system.getOS() == "Android" or love.system.getOS() == "iOS") then return end
|
||||||
|
return Studio.mousereleased(x, y, button)
|
||||||
|
end
|
||||||
if Importer then return end
|
if Importer then return end
|
||||||
if editorMode and EditorApp.mousereleased then
|
if editorMode and EditorApp.mousereleased then
|
||||||
return EditorApp.mousereleased(x, y, button)
|
return EditorApp.mousereleased(x, y, button)
|
||||||
@@ -877,6 +1078,10 @@ function love.mousemoved(x, y, dx, dy, istouch)
|
|||||||
if love.system.getOS() == "Android" then return end
|
if love.system.getOS() == "Android" then return end
|
||||||
return TouchEditor.mousemoved(x, y)
|
return TouchEditor.mousemoved(x, y)
|
||||||
end
|
end
|
||||||
|
if Studio then
|
||||||
|
if istouch and (love.system.getOS() == "Android" or love.system.getOS() == "iOS") then return end
|
||||||
|
return Studio.mousemoved(x, y)
|
||||||
|
end
|
||||||
if editorMode or Importer then return end
|
if editorMode or Importer then return end
|
||||||
if mouseTouch then
|
if mouseTouch then
|
||||||
if Game and love.mouse.isDown(1) then Game:touchmoved("mouse", x, y) end
|
if Game and love.mouse.isDown(1) then Game:touchmoved("mouse", x, y) end
|
||||||
@@ -887,6 +1092,7 @@ end
|
|||||||
|
|
||||||
function love.textinput(text)
|
function love.textinput(text)
|
||||||
if TouchEditor then return end
|
if TouchEditor then return end
|
||||||
|
if Studio then return Studio.textinput(text) end
|
||||||
if Importer then return Importer:textinput(text) end
|
if Importer then return Importer:textinput(text) end
|
||||||
if editorMode and EditorApp.textinput then
|
if editorMode and EditorApp.textinput then
|
||||||
return EditorApp.textinput(text)
|
return EditorApp.textinput(text)
|
||||||
@@ -926,11 +1132,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
|
||||||
@@ -964,6 +1175,7 @@ function love.filedropped(file)
|
|||||||
if editorMode and EditorApp and EditorApp.filedropped then
|
if editorMode and EditorApp and EditorApp.filedropped then
|
||||||
return EditorApp.filedropped(file)
|
return EditorApp.filedropped(file)
|
||||||
end
|
end
|
||||||
|
if Studio then return Studio.filedropped(file) end
|
||||||
if Importer then Importer:filedropped(file) end
|
if Importer then Importer:filedropped(file) end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -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|uiMode|density|fontScale|locale|layoutDirection|colorMode"
|
||||||
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" >
|
||||||
@@ -54,7 +71,7 @@
|
|||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name="org.love2d.android.GameActivity$SecondaryActivity"
|
android:name="org.love2d.android.GameActivity$SecondaryActivity"
|
||||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation"
|
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation|uiMode|density|fontScale|locale|layoutDirection|colorMode"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
|
|||||||
|
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>
|
||||||