Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a2aa9ce637 | |||
| d683be205b | |||
| d2f6735b0a | |||
| bfa68e6f7d |
@@ -18,59 +18,17 @@ body:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: game
|
||||
attributes:
|
||||
label: Which game were you playing
|
||||
description: Pick every version you saw the bug in. Use N/A if it isn't game-specific.
|
||||
multiple: true
|
||||
options:
|
||||
- Red
|
||||
- Blue
|
||||
- Yellow
|
||||
- Gold
|
||||
- N/A
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: os
|
||||
attributes:
|
||||
label: Which build are you running
|
||||
description: Official release targets. Pick Multiple platforms if you saw it on more than one.
|
||||
options:
|
||||
- macOS
|
||||
- Windows
|
||||
- Linux
|
||||
- Android
|
||||
- iOS
|
||||
- Nintendo Switch
|
||||
- Xbox
|
||||
- Anbernic RG34XXSP
|
||||
- Multiple platforms
|
||||
- Both
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: mods_enabled
|
||||
attributes:
|
||||
label: Were any mods on
|
||||
description: Check the MODS tab in the launcher if you're not sure.
|
||||
options:
|
||||
- "No"
|
||||
- "Yes"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: mods_which
|
||||
attributes:
|
||||
label: Which mods (if any were on)
|
||||
description: List the enabled mods. Leave blank if none were on.
|
||||
placeholder: nuzlocke 1.0.0, running-shoes 0.3
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
@@ -80,17 +38,6 @@ body:
|
||||
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: input
|
||||
id: location
|
||||
attributes:
|
||||
|
||||
@@ -1,68 +1,35 @@
|
||||
name: Feature request
|
||||
description: Ask for something new in the engine, launcher, or platform — not a content/gameplay mod.
|
||||
description: Ask for something new, or a change to how something already works.
|
||||
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.
|
||||
and how you picture it working. A request with real detail is one someone can
|
||||
actually build without guessing at what you meant.
|
||||
|
||||
- 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
|
||||
placeholder: Add a run toggle so you can move at bike speed without the bike
|
||||
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.
|
||||
Describe it properly. What is it, where does it live in the game, 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.
|
||||
A hold-to-run button, the way later Pokemon games do it. Hold B while moving on
|
||||
foot and the player moves at bike speed. Release and you go back to walking.
|
||||
Doesn't work indoors where the bike doesn't work, doesn't work in battles or menus.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -74,8 +41,9 @@ body:
|
||||
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.
|
||||
Backtracking across routes you've already cleared is slow, and the bike is a menu
|
||||
dive away and doesn't work in a lot of places. Running would cut a lot of dead time
|
||||
without making the game easier.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -84,12 +52,14 @@ body:
|
||||
attributes:
|
||||
label: How should it work
|
||||
description: >
|
||||
The specifics. Which menu, what happens in the edge cases. If you don't
|
||||
The specifics. Which button, 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
|
||||
- Hold B on the overworld to run
|
||||
- Same speed as the bike
|
||||
- Disabled anywhere the bike is disabled
|
||||
- Sprite should probably need a running animation, not sure what that takes
|
||||
- No effect on encounter rates
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -1,6 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
|
Before Width: | Height: | Size: 626 KiB |
|
Before Width: | Height: | Size: 582 KiB |
|
Before Width: | Height: | Size: 472 KiB |
|
Before Width: | Height: | Size: 730 KiB |
@@ -10,11 +10,11 @@ name: ci
|
||||
# The T3 content tier asserts Pokemon Red facts; scripts/test.sh detects
|
||||
# data/generated/ is absent and skips it rather than failing.
|
||||
#
|
||||
# Runs alongside release.yml, which is untouched by this file.
|
||||
|
||||
on:
|
||||
push:
|
||||
# Integration branch + release branch. PRs already run via pull_request
|
||||
# (any base); this list is only for post-merge push runs.
|
||||
branches: [dev, main]
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
# a force-push while CI is mid-run should cancel the stale run, not queue
|
||||
@@ -22,360 +22,12 @@ concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
ios-changes:
|
||||
name: detect iOS changes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
changed: ${{ steps.paths.outputs.changed }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- id: paths
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
|
||||
HEAD_SHA: ${{ github.sha }}
|
||||
run: |
|
||||
if [ -z "$BASE_SHA" ] || [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]; then
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
if git diff --name-only "$BASE_SHA" "$HEAD_SHA" | grep -Eq '^(mobile/ios/|scripts/build_ios\.sh$)'; then
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
ios-build:
|
||||
name: iOS build
|
||||
needs: ios-changes
|
||||
if: needs.ios-changes.outputs.changed == 'true'
|
||||
runs-on: ${{ fromJSON(github.repository == 'bryanthaboi/gen1recomp' && '["self-hosted", "macOS"]' || '"macos-latest"') }}
|
||||
outputs:
|
||||
ipa_url: ${{ steps.upload-ipa.outputs.artifact-url }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: import signing certificate
|
||||
if: github.repository == 'bryanthaboi/gen1recomp'
|
||||
run: |
|
||||
keychain_path="$RUNNER_TEMP/gen1recomp-ci-signing.keychain-db"
|
||||
ci_dir="${POKEMON_CI_DIR:-$HOME/.config/pokemon-ci}"
|
||||
p12="$ci_dir/signing.p12"
|
||||
passfile="$ci_dir/signing.pass"
|
||||
[ -f "$p12" ] && [ -f "$passfile" ] || exit 1
|
||||
p12pw="$(cat "$passfile")"
|
||||
kcpw="$(openssl rand -base64 24)"
|
||||
echo "::add-mask::$kcpw"
|
||||
security delete-keychain "$keychain_path" 2>/dev/null || true
|
||||
security create-keychain -p "$kcpw" "$keychain_path"
|
||||
security set-keychain-settings "$keychain_path"
|
||||
security unlock-keychain -p "$kcpw" "$keychain_path"
|
||||
security import "$p12" -P "$p12pw" -k "$keychain_path" -T /usr/bin/codesign -T /usr/bin/security
|
||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$kcpw" "$keychain_path" >/dev/null
|
||||
existing="$(security list-keychains -d user | sed -e 's/^[[:space:]]*//' -e 's/"//g')"
|
||||
security list-keychains -d user -s "$keychain_path" $existing
|
||||
- name: install xcbeautify
|
||||
run: brew list xcbeautify >/dev/null 2>&1 || brew install xcbeautify
|
||||
- name: build iOS release
|
||||
env:
|
||||
CANONICAL_REPOSITORY: ${{ github.repository == 'bryanthaboi/gen1recomp' }}
|
||||
run: |
|
||||
if [ "$CANONICAL_REPOSITORY" = true ]; then
|
||||
scripts/build_ios.sh --fetch --device --release
|
||||
else
|
||||
scripts/build_ios.sh --fetch --release
|
||||
fi
|
||||
- name: upload iOS release artifact
|
||||
id: upload-ipa
|
||||
if: github.repository == 'bryanthaboi/gen1recomp'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: gen1recomp++-ios-ipa
|
||||
path: dist/ios/gen1recomp++.ipa
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
- name: clean up signing keychain
|
||||
if: ${{ always() && github.repository == 'bryanthaboi/gen1recomp' }}
|
||||
run: security delete-keychain "$RUNNER_TEMP/gen1recomp-ci-signing.keychain-db" 2>/dev/null || true
|
||||
|
||||
switch-changes:
|
||||
name: detect Switch changes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
changed: ${{ steps.paths.outputs.changed }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- id: paths
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
|
||||
HEAD_SHA: ${{ github.sha }}
|
||||
run: |
|
||||
if [ -z "$BASE_SHA" ] || [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]; then
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
if git diff --name-only "$BASE_SHA" "$HEAD_SHA" | grep -Eq '^(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
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
switch-selftest:
|
||||
name: Switch offline selftest
|
||||
needs: switch-changes
|
||||
if: needs.switch-changes.outputs.changed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: install luajit
|
||||
run: sudo apt-get update && sudo apt-get install -y luajit
|
||||
- name: Switch offline selftest
|
||||
run: bash scripts/switch/selftest_build_switch.sh
|
||||
- name: verify_payload self-test
|
||||
run: bash scripts/switch/verify_payload.sh --self-test
|
||||
- name: Switch CI workflow content gate
|
||||
run: luajit tests/switch_ci_workflows_test.lua
|
||||
- name: Switch transfer docs content gate
|
||||
run: luajit tests/switch_transfer_docs_test.lua
|
||||
# NX runtime regressions gate this job via switch-changes; run the NX
|
||||
# engine suites here too so a PR touching them gets feedback on the
|
||||
# fork-safe ubuntu runner before the self-hosted Mac build.
|
||||
- name: NX engine suites (headless)
|
||||
run: |
|
||||
luajit tests/engine/assets_version_fallback_test.lua
|
||||
luajit tests/engine/nx_generated_guard_test.lua
|
||||
luajit tests/engine/nx_yellow_boot_test.lua
|
||||
|
||||
switch-build:
|
||||
name: Switch fused build
|
||||
needs: [switch-changes, switch-selftest]
|
||||
if: |
|
||||
always()
|
||||
&& needs.switch-changes.outputs.changed == 'true'
|
||||
&& needs.switch-selftest.result == 'success'
|
||||
&& github.repository == 'bryanthaboi/gen1recomp'
|
||||
&& (github.event_name != 'pull_request'
|
||||
|| github.event.pull_request.head.repo.full_name == github.repository)
|
||||
runs-on: ["self-hosted", "macOS"]
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Build Switch fused NRO
|
||||
run: |
|
||||
set -euo pipefail
|
||||
VER="$(printf '%s' "$GITHUB_SHA" | cut -c1-7)"
|
||||
scripts/build_switch.sh --fetch --fused --version "$VER"
|
||||
echo "SWITCH_VER=$VER" >> "$GITHUB_ENV"
|
||||
- name: upload Switch NRO artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: gen1recomp-switch-nro
|
||||
path: |
|
||||
dist/switch/gen1recomp-${{ env.SWITCH_VER }}-switch.nro
|
||||
dist/switch/gen1recomp-${{ env.SWITCH_VER }}-switch.nro.sha256
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
xbox-uwp-changes:
|
||||
name: detect Xbox UWP changes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
changed: ${{ steps.paths.outputs.changed }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- id: paths
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
|
||||
HEAD_SHA: ${{ github.sha }}
|
||||
run: |
|
||||
if [ -z "$BASE_SHA" ] || [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]; then
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
if git diff --name-only "$BASE_SHA" "$HEAD_SHA" | grep -Eq '^(ports/uwp/|scripts/build_xbox_uwp\.sh$|scripts/xbox-uwp/|scripts/pack_love\.sh$|\.github/workflows/(ci|release)\.yml$|src/core/Platform\.lua$|src/import/(CacheFs|LauncherView|RomImporter)\.lua$|src/update/Check\.lua$|tests/engine/(platform_nx|uwp_baseroms|uwp_native_picker)_test\.lua$|tests/rom_importer_double_pick_test\.lua$)'; then
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
xbox-uwp-selftest:
|
||||
name: Xbox UWP offline selftest
|
||||
needs: xbox-uwp-changes
|
||||
if: needs.xbox-uwp-changes.outputs.changed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Xbox UWP offline selftest
|
||||
run: bash scripts/xbox-uwp/selftest_build_xbox_uwp.sh
|
||||
- name: Build shared payload
|
||||
run: |
|
||||
scripts/pack_love.sh \
|
||||
--output .bazinga/work/ci-game.love \
|
||||
--listing .bazinga/work/ci-love-listing.txt \
|
||||
--version 0.0.0
|
||||
- name: Upload shared payload
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: gen1recomp-xbox-uwp-payload
|
||||
path: .bazinga/work/ci-game.love
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
xbox-uwp-build:
|
||||
name: Xbox UWP build
|
||||
needs: [xbox-uwp-changes, xbox-uwp-selftest]
|
||||
if: |
|
||||
always()
|
||||
&& needs.xbox-uwp-changes.outputs.changed == 'true'
|
||||
&& needs.xbox-uwp-selftest.result == 'success'
|
||||
runs-on: windows-2022
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Download shared payload
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: gen1recomp-xbox-uwp-payload
|
||||
path: .bazinga/work
|
||||
- name: Build Xbox UWP package
|
||||
shell: bash
|
||||
run: |
|
||||
bash scripts/build_xbox_uwp.sh \
|
||||
--release \
|
||||
--version 0.0.0 \
|
||||
--game-love .bazinga/work/ci-game.love
|
||||
- name: Upload Xbox UWP package
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: gen1recomp-xbox-uwp
|
||||
path: |
|
||||
dist/xbox-uwp/gen1recomp-0.0.0-xbox-uwp.zip
|
||||
dist/xbox-uwp/gen1recomp-0.0.0-xbox-uwp.zip.sha256
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
linux-arm64-changes:
|
||||
name: detect Linux arm64 changes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
changed: ${{ steps.paths.outputs.changed }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- id: paths
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
|
||||
HEAD_SHA: ${{ github.sha }}
|
||||
run: |
|
||||
if [ -z "$BASE_SHA" ] || [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]; then
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
if git diff --name-only "$BASE_SHA" "$HEAD_SHA" | grep -Eq '^(scripts/build_linux_arm64\.sh$|scripts/linux-arm64/|scripts/pack_love\.sh$|docs/linux-arm64-build\.md$|\.github/workflows/(ci|release)\.yml$)'; then
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
linux-arm64-selftest:
|
||||
name: Linux arm64 offline selftest
|
||||
needs: linux-arm64-changes
|
||||
if: needs.linux-arm64-changes.outputs.changed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
# Deliberately on x86_64: everything this gate checks (pins, the
|
||||
# host-arch guard, the dependency exclude list, the AppRun fusion
|
||||
# contract) is answerable without a container or an aarch64 machine,
|
||||
# so the slow native job below only ever starts on a sane tree.
|
||||
- name: Linux arm64 offline selftest
|
||||
run: bash scripts/linux-arm64/selftest_build_linux_arm64.sh
|
||||
|
||||
linux-arm64-build:
|
||||
name: Linux arm64 AppImage build
|
||||
needs: [linux-arm64-changes, linux-arm64-selftest]
|
||||
if: |
|
||||
always()
|
||||
&& needs.linux-arm64-changes.outputs.changed == 'true'
|
||||
&& needs.linux-arm64-selftest.result == 'success'
|
||||
# No fork restriction, unlike switch-build: this needs no secrets and no
|
||||
# self-hosted hardware, just GitHub's free arm64 runner for public repos,
|
||||
# so contributors get the same coverage on their own PRs.
|
||||
runs-on: ubuntu-24.04-arm
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Build the aarch64 AppImage
|
||||
run: |
|
||||
set -euo pipefail
|
||||
scripts/build_linux_arm64.sh --version 0.0.0
|
||||
- name: Verify the AppImage is self-contained and bullseye-compatible
|
||||
run: |
|
||||
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
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: gen1recomp-linux-arm64
|
||||
path: |
|
||||
dist/linux-arm64/gen1recomp-0.0.0-linux-arm64.AppImage
|
||||
dist/linux-arm64/gen1recomp-0.0.0-linux-arm64.AppImage.sha256
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
headless:
|
||||
name: headless suites (no ROM)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# LuaJIT, not lua5.4: LOVE 11.x embeds LuaJIT 2.1 and the engine is
|
||||
# written to Lua 5.1 semantics, so CI must run the interpreter the
|
||||
@@ -383,9 +35,6 @@ jobs:
|
||||
- name: install luajit
|
||||
run: sudo apt-get update && sudo apt-get install -y luajit
|
||||
|
||||
- name: install Pillow
|
||||
run: python3 -m pip install --upgrade pillow
|
||||
|
||||
- name: interpreter version
|
||||
run: luajit -v
|
||||
|
||||
@@ -396,7 +45,7 @@ jobs:
|
||||
name: fixture dataset integrity
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
- run: sudo apt-get update && sudo apt-get install -y luajit
|
||||
- run: python3 -m pip install --upgrade pillow
|
||||
|
||||
@@ -438,7 +87,7 @@ jobs:
|
||||
name: screenshot differ (capture not yet wired)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
- run: python3 -m pip install --upgrade pillow
|
||||
|
||||
# 21-testing-and-ci §"Testing & acceptance criteria": compare_shots
|
||||
@@ -486,7 +135,7 @@ jobs:
|
||||
name: mod lint (no ROM-derived content)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# the MK305 dump check key-diffs shipped tables through luajit, and
|
||||
# modkit treats a missing interpreter as a fatal MK100 -- without
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
name: iOS artifact comment
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [ci]
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: artifact
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RUN_ID: ${{ github.event.workflow_run.id }}
|
||||
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
|
||||
HEAD_REPOSITORY: ${{ github.event.workflow_run.head_repository.full_name }}
|
||||
run: |
|
||||
artifact_id="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$RUN_ID/artifacts" --jq '.artifacts[] | select(.name == "gen1recomp++-ios-ipa") | .id')"
|
||||
[ -n "$artifact_id" ] || exit 0
|
||||
head_owner="${HEAD_REPOSITORY%%/*}"
|
||||
pr_number="$(gh api "repos/$GITHUB_REPOSITORY/pulls?state=open&head=$head_owner:$HEAD_BRANCH" --jq '.[0].number // empty')"
|
||||
[ -n "$pr_number" ] || exit 0
|
||||
echo "artifact_url=https://github.com/$GITHUB_REPOSITORY/actions/runs/$RUN_ID/artifacts/$artifact_id" >> "$GITHUB_OUTPUT"
|
||||
echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT"
|
||||
# Upsert via comment-tag only — do not delete-all bot comments (clobbers Switch).
|
||||
- name: Get build info
|
||||
id: build-info
|
||||
env:
|
||||
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
|
||||
run: |
|
||||
commit_hash="$(printf '%s' "$HEAD_SHA" | cut -c1-7)"
|
||||
build_time="$(date "+%Y-%m-%d %H:%M:%S")"
|
||||
echo "hash=$commit_hash" >> "$GITHUB_OUTPUT"
|
||||
echo "time=$build_time" >> "$GITHUB_OUTPUT"
|
||||
- name: comment iOS artifact
|
||||
if: steps.artifact.outputs.pr_number != ''
|
||||
uses: thollander/actions-comment-pull-request@v3
|
||||
with:
|
||||
message: |
|
||||
[gen1recomp++.ipa](${{ steps.artifact.outputs.artifact_url }})
|
||||
|
||||
**Commit**: [#${{ steps.build-info.outputs.hash }}](https://github.com/${{ github.event.workflow_run.head_repository.full_name }}/commit/${{ github.event.workflow_run.head_sha }})
|
||||
**Build Time**: `${{ steps.build-info.outputs.time }}`
|
||||
|
||||
<sub>This comment was automatically generated. [View workflow run](https://github.com/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }})</sub>
|
||||
pr-number: ${{ steps.artifact.outputs.pr_number }}
|
||||
comment-tag: ios-build-result
|
||||
github-token: ${{ github.token }}
|
||||
@@ -1,10 +1,7 @@
|
||||
name: Release
|
||||
|
||||
# Builds the macOS, Windows, and Linux desktop apps, an Android APK, an iOS
|
||||
# IPA, a Nintendo Switch SD-ready zip (experimental), Xbox UWP, the Anbernic
|
||||
# RG34XXSP (Stock OS 64-bit MOD / PortMaster) and Linux ARM SBC PortMaster
|
||||
# handheld ports on the self-hosted Mac runner, and publishes them as a
|
||||
# GitHub Release.
|
||||
# Builds the macOS, Windows, and Linux desktop apps plus an Android APK
|
||||
# on the self-hosted Mac runner, and publishes them as a GitHub Release.
|
||||
#
|
||||
# Versioning:
|
||||
# - First ever release is 0.1.0.
|
||||
@@ -13,9 +10,6 @@ name: Release
|
||||
# - To force a specific version, either:
|
||||
# * run this workflow manually (Actions tab) and type it into "version", or
|
||||
# * put "[release X.Y.Z]" anywhere in the commit message.
|
||||
#
|
||||
# Branch model: day-to-day work merges to `dev`. Releases stay on `main` only
|
||||
# so promoting `dev` -> `main` is the ship gate that cuts a build.
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -26,7 +20,6 @@ on:
|
||||
paths-ignore:
|
||||
- '.github/**'
|
||||
- '**.md'
|
||||
- 'mobile/ios/app-repo.json'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
@@ -44,17 +37,16 @@ concurrency:
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
version:
|
||||
name: determine release version
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.ver.outputs.version }}
|
||||
tag: ${{ steps.ver.outputs.tag }}
|
||||
release:
|
||||
runs-on: [self-hosted, macOS]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Determine version
|
||||
id: ver
|
||||
env:
|
||||
@@ -62,6 +54,7 @@ jobs:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
semver_re='^[0-9]+\.[0-9]+\.[0-9]+$'
|
||||
|
||||
# 1) Explicit override from a manual run.
|
||||
@@ -91,6 +84,7 @@ jobs:
|
||||
| grep -E "$semver_re" \
|
||||
| sort -t. -k1,1n -k2,2n -k3,3n \
|
||||
| tail -1 || true)"
|
||||
|
||||
if [ -z "$latest" ]; then
|
||||
version="0.1.0"
|
||||
echo "No existing release tag; starting at $version"
|
||||
@@ -118,197 +112,11 @@ jobs:
|
||||
echo "::error::Release $tag already exists. Pick a different version."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||
|
||||
love-payload:
|
||||
name: build release game.love
|
||||
needs: version
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Build shared payload
|
||||
run: |
|
||||
scripts/pack_love.sh \
|
||||
--output dist/payload/game.love \
|
||||
--listing dist/payload/love-listing.txt \
|
||||
--version "${{ needs.version.outputs.version }}"
|
||||
- name: Upload shared payload
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: gen1recomp-release-love
|
||||
path: dist/payload/game.love
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
linux-arm64:
|
||||
name: build Linux arm64 AppImage
|
||||
needs: [version, love-payload]
|
||||
# GitHub's free arm64 runner for public repos. It has to be arm64: the
|
||||
# AppImage compiles LÖVE natively inside a Debian bullseye arm64
|
||||
# container, and the qemu-emulated alternative takes hours.
|
||||
runs-on: ubuntu-24.04-arm
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Download shared payload
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: gen1recomp-release-love
|
||||
path: .bazinga/work
|
||||
- name: Build Linux arm64 AppImage
|
||||
run: |
|
||||
set -euo pipefail
|
||||
scripts/build_linux_arm64.sh \
|
||||
--version "${{ needs.version.outputs.version }}" \
|
||||
--game-love .bazinga/work/game.love
|
||||
- name: Upload Linux arm64 release
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: gen1recomp-linux-arm64-release
|
||||
path: |
|
||||
dist/linux-arm64/gen1recomp-${{ needs.version.outputs.version }}-linux-arm64.AppImage
|
||||
dist/linux-arm64/gen1recomp-${{ needs.version.outputs.version }}-linux-arm64.AppImage.sha256
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
xbox-uwp:
|
||||
name: build Xbox UWP release
|
||||
needs: [version, love-payload]
|
||||
runs-on: windows-2022
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Download shared payload
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: gen1recomp-release-love
|
||||
path: .bazinga/work
|
||||
- name: Prepare signing certificate
|
||||
shell: pwsh
|
||||
env:
|
||||
CERTIFICATE_BASE64: ${{ secrets.XBOX_UWP_SIGNING_CERTIFICATE }}
|
||||
CERTIFICATE_PASSWORD: ${{ secrets.XBOX_UWP_SIGNING_PASSWORD }}
|
||||
CANONICAL_REPOSITORY: ${{ github.repository == 'bryanthaboi/gen1recomp' }}
|
||||
run: |
|
||||
if ($env:CANONICAL_REPOSITORY -eq 'true' -and
|
||||
[string]::IsNullOrWhiteSpace($env:CERTIFICATE_BASE64)) {
|
||||
throw 'XBOX_UWP_SIGNING_CERTIFICATE is not configured.'
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($env:CERTIFICATE_BASE64)) {
|
||||
"UWP_PUBLISHER=CN=Gen1Recomp" | Out-File $env:GITHUB_ENV -Append
|
||||
exit 0
|
||||
}
|
||||
$pfx = Join-Path $env:RUNNER_TEMP 'gen1recomp-uwp.pfx'
|
||||
[IO.File]::WriteAllBytes($pfx, [Convert]::FromBase64String($env:CERTIFICATE_BASE64))
|
||||
$flags = [Security.Cryptography.X509Certificates.X509KeyStorageFlags]::EphemeralKeySet
|
||||
$cert = [Security.Cryptography.X509Certificates.X509Certificate2]::new(
|
||||
$pfx, $env:CERTIFICATE_PASSWORD, $flags)
|
||||
$cer = Join-Path $env:RUNNER_TEMP 'gen1recomp-uwp.cer'
|
||||
[IO.File]::WriteAllBytes(
|
||||
$cer,
|
||||
$cert.Export([Security.Cryptography.X509Certificates.X509ContentType]::Cert))
|
||||
Import-Certificate -FilePath $cer -CertStoreLocation Cert:\LocalMachine\TrustedPeople | Out-Null
|
||||
"UWP_PFX=$pfx" | Out-File $env:GITHUB_ENV -Append
|
||||
"UWP_CERT_THUMBPRINT=$($cert.Thumbprint)" | Out-File $env:GITHUB_ENV -Append
|
||||
"UWP_PUBLISHER=$($cert.Subject)" | Out-File $env:GITHUB_ENV -Append
|
||||
- name: Build Xbox UWP package
|
||||
shell: bash
|
||||
run: |
|
||||
bash scripts/build_xbox_uwp.sh \
|
||||
--release \
|
||||
--version "${{ needs.version.outputs.version }}" \
|
||||
--publisher "$UWP_PUBLISHER" \
|
||||
--game-love .bazinga/work/game.love
|
||||
- name: Sign and stage Xbox UWP release
|
||||
shell: pwsh
|
||||
env:
|
||||
CERTIFICATE_PASSWORD: ${{ secrets.XBOX_UWP_SIGNING_PASSWORD }}
|
||||
run: |
|
||||
if (-not $env:UWP_PFX) {
|
||||
exit 0
|
||||
}
|
||||
scripts/xbox-uwp/stage_release.ps1 `
|
||||
-Version '${{ needs.version.outputs.version }}' `
|
||||
-Configuration Release `
|
||||
-BuildInfo .bazinga/work/xbox-uwp-build-info.json `
|
||||
-CertificatePath $env:UWP_PFX `
|
||||
-CertificatePassword $env:CERTIFICATE_PASSWORD
|
||||
- name: Upload Xbox UWP release
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: gen1recomp-xbox-uwp-release
|
||||
path: |
|
||||
dist/xbox-uwp/gen1recomp-${{ needs.version.outputs.version }}-xbox-uwp.zip
|
||||
dist/xbox-uwp/gen1recomp-${{ needs.version.outputs.version }}-xbox-uwp.zip.sha256
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
- name: Remove signing certificate
|
||||
if: always()
|
||||
shell: pwsh
|
||||
run: |
|
||||
if ($env:UWP_CERT_THUMBPRINT) {
|
||||
Remove-Item "Cert:\LocalMachine\TrustedPeople\$env:UWP_CERT_THUMBPRINT" -ErrorAction SilentlyContinue
|
||||
}
|
||||
if ($env:UWP_PFX) {
|
||||
Remove-Item $env:UWP_PFX -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# Windows Native AOT TLS dialer. The Mac release runner fuses the win64 zip
|
||||
# from LÖVE's prebuilt binaries and cannot cross-compile this DLL, so build
|
||||
# it here and inject it in the release job before scripts/build.sh win.
|
||||
native-tls-win:
|
||||
name: build Windows gen1tls.dll
|
||||
needs: version
|
||||
runs-on: windows-2022
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Setup .NET 8
|
||||
uses: actions/setup-dotnet@v6
|
||||
with:
|
||||
dotnet-version: "8.0.x"
|
||||
- name: Publish gen1tls (win-x64 Native AOT)
|
||||
shell: pwsh
|
||||
run: |
|
||||
$out = "dist/native/win-x64"
|
||||
New-Item -ItemType Directory -Force -Path $out | Out-Null
|
||||
dotnet publish native/tls_dial/Gen1Tls.csproj `
|
||||
-c Release -r win-x64 -o $out
|
||||
if (-not (Test-Path "$out/gen1tls.dll")) {
|
||||
throw "gen1tls.dll missing after publish"
|
||||
}
|
||||
Get-Item "$out/gen1tls.dll" | Format-List Name, Length, LastWriteTime
|
||||
- name: Upload gen1tls.dll
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: gen1tls-win-x64
|
||||
path: dist/native/win-x64/gen1tls.dll
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
release:
|
||||
needs: [version, xbox-uwp, linux-arm64, native-tls-win]
|
||||
runs-on: ${{ fromJSON(github.repository == 'bryanthaboi/gen1recomp' && '["self-hosted", "macOS"]' || '"macos-latest"') }}
|
||||
|
||||
steps:
|
||||
# The self-hosted runner lives under the machine owner's home
|
||||
# directory; mask it first so absolute paths in every later step's
|
||||
# output show up as *** in the public workflow logs.
|
||||
- name: Mask runner paths
|
||||
run: echo "::add-mask::$HOME"
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Download Windows gen1tls dialer
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: gen1tls-win-x64
|
||||
path: dist/native/win-x64
|
||||
|
||||
- name: Import signing certificate into a temporary keychain
|
||||
if: github.repository == 'bryanthaboi/gen1recomp'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
KEYCHAIN_PATH="$RUNNER_TEMP/pokemon-signing.keychain-db"
|
||||
@@ -345,79 +153,20 @@ jobs:
|
||||
security find-identity -v -p codesigning "$KEYCHAIN_PATH"
|
||||
|
||||
- name: Build macOS + Windows + Linux
|
||||
env:
|
||||
GEN1TLS_DLL: ${{ github.workspace }}/dist/native/win-x64/gen1tls.dll
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Sign in-build (identity auto-detected from the temp keychain);
|
||||
# notarize separately below so it uses secret credentials, not a
|
||||
# login-keychain profile. "all" also builds the Linux AppImage,
|
||||
# which needs no signing/notarization.
|
||||
if [ ! -f "$GEN1TLS_DLL" ]; then
|
||||
echo "::error::gen1tls.dll missing at $GEN1TLS_DLL (native-tls-win job)"
|
||||
exit 1
|
||||
fi
|
||||
scripts/build.sh all --version "${{ needs.version.outputs.version }}" --no-notarize
|
||||
unzip -l dist/win/gen1recomp-win64.zip | grep -F gen1tls.dll \
|
||||
|| { echo "::error::Windows zip is missing gen1tls.dll"; exit 1; }
|
||||
scripts/build.sh all --version "${{ steps.ver.outputs.version }}" --no-notarize
|
||||
|
||||
- name: Build Android
|
||||
run: |
|
||||
set -euo pipefail
|
||||
scripts/build_android.sh --version "${{ needs.version.outputs.version }}"
|
||||
|
||||
- name: Install xcbeautify
|
||||
run: |
|
||||
set -euo pipefail
|
||||
brew list xcbeautify >/dev/null 2>&1 || brew install xcbeautify
|
||||
|
||||
- name: Build iOS
|
||||
env:
|
||||
CANONICAL_REPOSITORY: ${{ github.repository == 'bryanthaboi/gen1recomp' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ "$CANONICAL_REPOSITORY" = true ]; then
|
||||
scripts/build_ios.sh --fetch --device --release \
|
||||
--version "${{ needs.version.outputs.version }}"
|
||||
else
|
||||
scripts/build_ios.sh --fetch --release \
|
||||
--version "${{ needs.version.outputs.version }}"
|
||||
fi
|
||||
|
||||
- name: Build Switch
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Hard-fail gate: Switch ships with every release (never soft-fail).
|
||||
# PR CI is path-gated (ubuntu selftest + canonical fused); release
|
||||
# always builds Switch regardless of which files changed.
|
||||
# Needs native switch-tools (nacptool/elf2nro) and/or Docker on the
|
||||
# Mac self-hosted runner; see docs/switch-build.md.
|
||||
scripts/build_switch.sh --fetch --fused \
|
||||
--version "${{ needs.version.outputs.version }}"
|
||||
|
||||
- name: Build Anbernic RG34XXSP port
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Self-contained aarch64 PortMaster-style pack; pulls the LÖVE 11.5
|
||||
# runtime from PortMaster-GUI, so it needs no signing/notarization.
|
||||
./build-rg34xxsp.sh --version "${{ needs.version.outputs.version }}"
|
||||
|
||||
- name: Build Linux ARM SBC PortMaster port
|
||||
env:
|
||||
# The release workflow must package the commit being released. The
|
||||
# script defaults to the latest published release for standalone
|
||||
# builds, while this explicit local override keeps CI source-aligned.
|
||||
GEN1RECOMP_SOURCE_DIR: ${{ github.workspace }}
|
||||
GEN1RECOMP_RELEASE_TAG: v${{ needs.version.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Same aarch64 PortMaster-style pack for Linux ARM SBC PortMaster. The build
|
||||
# keeps its own cache because the two scripts use different staging
|
||||
# layouts and runtime package paths.
|
||||
./build-linux-arm-sbc.sh --version "${{ needs.version.outputs.version }}"
|
||||
scripts/build_android.sh --version "${{ steps.ver.outputs.version }}"
|
||||
|
||||
- name: Notarize & staple macOS app
|
||||
if: github.repository == 'bryanthaboi/gen1recomp'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ci_dir="${POKEMON_CI_DIR:-$HOME/.config/pokemon-ci}"
|
||||
@@ -451,124 +200,43 @@ jobs:
|
||||
ditto -c -k --sequesterRsrc --keepParent "$app" "$zip"
|
||||
echo "Notarized + stapled ✓"
|
||||
|
||||
- name: Download Xbox UWP release
|
||||
if: github.repository == 'bryanthaboi/gen1recomp'
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: gen1recomp-xbox-uwp-release
|
||||
path: dist/xbox-uwp
|
||||
|
||||
- name: Download Linux arm64 release
|
||||
if: github.repository == 'bryanthaboi/gen1recomp'
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: gen1recomp-linux-arm64-release
|
||||
path: dist/linux-arm64
|
||||
|
||||
- name: Stage release assets
|
||||
if: github.repository == 'bryanthaboi/gen1recomp'
|
||||
id: assets
|
||||
run: |
|
||||
set -euo pipefail
|
||||
v="${{ needs.version.outputs.version }}"
|
||||
v="${{ steps.ver.outputs.version }}"
|
||||
outdir="dist/release"
|
||||
rm -rf "$outdir"
|
||||
mkdir -p "$outdir"
|
||||
cp "dist/mac/gen1recomp-macos.zip" "$outdir/gen1recomp-${v}-macos.zip"
|
||||
cp "dist/win/gen1recomp-win64.zip" "$outdir/gen1recomp-${v}-windows.zip"
|
||||
cp "dist/linux/gen1recomp-linux.zip" "$outdir/gen1recomp-${v}-linux.zip"
|
||||
|
||||
# arm64 desktop Linux (Raspberry Pi, Armbian, arm64 VMs). Built on
|
||||
# its own runner because LÖVE publishes no aarch64 binary and the
|
||||
# AppImage has to be compiled natively; ships as a runnable
|
||||
# AppImage rather than a zip so `chmod +x && ./it` just works.
|
||||
arm64_appimage="dist/linux-arm64/gen1recomp-${v}-linux-arm64.AppImage"
|
||||
[ -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"
|
||||
chmod +x "$outdir/gen1recomp-${v}-linux-arm64.AppImage"
|
||||
apk="$(find dist/android/debug -name '*.apk' | head -1)"
|
||||
[ -n "$apk" ] || { echo "::error::no Android APK found under dist/android/debug"; exit 1; }
|
||||
cp "$apk" "$outdir/gen1recomp-${v}-android.apk"
|
||||
|
||||
ipa="dist/ios/gen1recomp++.ipa"
|
||||
[ -f "$ipa" ] || { echo "::error::$ipa not found (expected from scripts/build_ios.sh --device)"; exit 1; }
|
||||
cp "$ipa" "$outdir/gen1recomp++-${v}-ios.ipa"
|
||||
|
||||
swzip="dist/switch/gen1recomp-${v}-switch.zip"
|
||||
[ -f "$swzip" ] || { echo "::error::$swzip not found (expected from scripts/build_switch.sh --fused → pack_sd_zip.sh)"; exit 1; }
|
||||
cp "$swzip" "$outdir/gen1recomp-${v}-switch.zip"
|
||||
# Local fused .nro stays under dist/switch/ for PR CI / debug; release
|
||||
# publishes the SD-ready zip only.
|
||||
|
||||
uwp="dist/xbox-uwp/gen1recomp-${v}-xbox-uwp.zip"
|
||||
[ -f "$uwp" ] || { echo "::error::$uwp not found (expected from the Xbox UWP job)"; exit 1; }
|
||||
cp "$uwp" "$outdir/gen1recomp-${v}-xbox-uwp.zip"
|
||||
|
||||
# Anbernic handheld port (suffix names the CFW it targets, so a
|
||||
# future RG35XX/other-CFW pack can ship alongside it).
|
||||
rg34="dist/rg34xxsp/gen1recomp-rg34xxsp-stockos64-mod.zip"
|
||||
[ -f "$rg34" ] || { echo "::error::$rg34 not found (expected from ./build-rg34xxsp.sh)"; exit 1; }
|
||||
cp "$rg34" "$outdir/gen1recomp-${v}-rg34xxsp-stockos64-mod.zip"
|
||||
|
||||
# Linux ARM SBC PortMaster handheld port.
|
||||
sbc="dist/linux-arm-sbc/gen1recomp-sbc-portmaster.zip"
|
||||
[ -f "$sbc" ] || { echo "::error::$sbc not found (expected from ./build-linux-arm-sbc.sh)"; exit 1; }
|
||||
cp "$sbc" "$outdir/gen1recomp-${v}-sbc-portmaster.zip"
|
||||
|
||||
# Platform-independent update payload, built alongside the desktop
|
||||
# apps above (same game.love that gets fused into each of them).
|
||||
love_file=".bazinga/work/game.love"
|
||||
[ -f "$love_file" ] || { echo "::error::$love_file not found (expected from scripts/build.sh)"; exit 1; }
|
||||
cp "$love_file" "$outdir/gen1recomp-${v}.love"
|
||||
|
||||
ls -lh "$outdir"
|
||||
|
||||
# Checksums for every staged release asset (sums file itself is
|
||||
# written after this and named outside the gen1recomp-* glob, so it
|
||||
# never lists itself).
|
||||
(cd "$outdir" && shasum -a 256 gen1recomp-* > sha256sums.txt)
|
||||
cat "$outdir/sha256sums.txt"
|
||||
|
||||
- name: Publish GitHub Release
|
||||
if: github.repository == 'bryanthaboi/gen1recomp'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
v="${{ needs.version.outputs.version }}"
|
||||
tag="${{ needs.version.outputs.tag }}"
|
||||
v="${{ steps.ver.outputs.version }}"
|
||||
tag="${{ steps.ver.outputs.tag }}"
|
||||
|
||||
# Issues this release closes. Three sources, deduped by number:
|
||||
# 1. GitHub's own "closing issues" links on every PR whose
|
||||
# commits are in the range (works for squash, rebase, and
|
||||
# merge commits alike) -- including PRs that were merged
|
||||
# into a branch this release PR is itself merging in.
|
||||
# 2. CLOSES/fixes/resolves text in those PRs' titles + bodies
|
||||
# that GitHub didn't turn into a closing link (a PR into a
|
||||
# non-default branch never gets one).
|
||||
# 3. The same text in raw commit messages, so a direct push
|
||||
# with "CLOSES #N #M" still lands in the notes.
|
||||
# Scans every commit since the previous tag so a skipped release
|
||||
# run doesn't drop issues on the floor.
|
||||
# Issues this release closes, per GitHub's own "closing issues" links
|
||||
# (works for squash, rebase, and merge commits alike). Scans every
|
||||
# commit since the previous tag so a skipped release run doesn't drop
|
||||
# issues on the floor.
|
||||
prev_tag="$(git tag -l 'v*' --sort=-v:refname | grep -v "^${tag}$" | head -1 || true)"
|
||||
range="${prev_tag:+${prev_tag}..}$GITHUB_SHA"
|
||||
|
||||
# every #N on a line that carries a closing keyword (handles the
|
||||
# multi-issue "CLOSES #1 #2 #3" form the tracker uses)
|
||||
scan_closes() {
|
||||
grep -iE '\b(close[sd]?|fix(es|ed)?|resolve[sd]?)\b' \
|
||||
| grep -oE '#[0-9]+' | tr -d '#' || true
|
||||
}
|
||||
|
||||
nums=""
|
||||
nums+=" $(git log --pretty=%B "$range" | scan_closes | tr '\n' ' ')"
|
||||
|
||||
prs="$(git log --pretty=%H "$range" \
|
||||
| xargs -I{} gh api "repos/$GITHUB_REPOSITORY/commits/{}/pulls" \
|
||||
--jq '.[].number' 2>/dev/null \
|
||||
| sort -un || true)"
|
||||
for pr in $prs; do
|
||||
nums+=" $(gh api graphql \
|
||||
closed=""
|
||||
for pr in $(git log --pretty=%H "$range" \
|
||||
| xargs -I{} gh api "repos/$GITHUB_REPOSITORY/commits/{}/pulls" \
|
||||
--jq '.[].number' 2>/dev/null \
|
||||
| sort -un || true); do
|
||||
closed+="$(gh api graphql \
|
||||
-f owner="${GITHUB_REPOSITORY%/*}" \
|
||||
-f name="${GITHUB_REPOSITORY#*/}" \
|
||||
-F pr="$pr" \
|
||||
@@ -576,140 +244,33 @@ jobs:
|
||||
query($owner:String!, $name:String!, $pr:Int!) {
|
||||
repository(owner:$owner, name:$name) {
|
||||
pullRequest(number:$pr) {
|
||||
closingIssuesReferences(first:50) { nodes { number } }
|
||||
closingIssuesReferences(first:50) { nodes { number title } }
|
||||
}
|
||||
}
|
||||
}' \
|
||||
--jq '.data.repository.pullRequest.closingIssuesReferences.nodes[].number' \
|
||||
2>/dev/null | grep -E '^[0-9]+$' | tr '\n' ' ' || true)"
|
||||
nums+=" $(gh api "repos/$GITHUB_REPOSITORY/pulls/$pr" \
|
||||
--jq '.title + " " + (.body // "")' 2>/dev/null \
|
||||
| scan_closes | tr '\n' ' ' || true)"
|
||||
done
|
||||
|
||||
# dedupe, drop anything that is a PR or does not exist, keep
|
||||
# titles (gh api prints the response body to stdout on an HTTP
|
||||
# error, so only a zero exit counts)
|
||||
closed=""
|
||||
for n in $(printf '%s' "$nums" | tr ' ' '\n' | grep -E '^[0-9]+$' | sort -un); do
|
||||
if title="$(gh api "repos/$GITHUB_REPOSITORY/issues/$n" \
|
||||
--jq 'if .pull_request then empty else .title end' \
|
||||
2>/dev/null)"; then
|
||||
[ -n "$title" ] && closed+="- #$n $title"$'\n'
|
||||
fi
|
||||
--jq '.data.repository.pullRequest.closingIssuesReferences.nodes[]
|
||||
| "- #\(.number) \(.title)"' 2>/dev/null || true)"$'\n'
|
||||
done
|
||||
closed="$(printf '%s' "$closed" | grep . | sort -t'#' -k2 -n || true)"
|
||||
|
||||
# Everyone whose commits are in the range: GitHub login when the
|
||||
# commit is linked to an account, the raw git author name when not;
|
||||
# CI bots filtered out.
|
||||
base="${prev_tag:-$(git rev-list --max-parents=0 "$GITHUB_SHA" | tail -1)}"
|
||||
contributors="$(gh api --paginate \
|
||||
"repos/$GITHUB_REPOSITORY/compare/${base}...${GITHUB_SHA}" \
|
||||
--jq '.commits[] | if .author and .author.login
|
||||
then "@" + .author.login
|
||||
else .commit.author.name end' 2>/dev/null \
|
||||
| grep -viE '\[bot\]$' | sort -uf | sed 's/^/- /' || true)"
|
||||
|
||||
notes="Download the correct version for your computer below."
|
||||
if [ -n "$closed" ]; then
|
||||
notes+=$'\n\n## Issues closed\n\n'"$closed"
|
||||
fi
|
||||
if [ -n "$contributors" ]; then
|
||||
notes+=$'\n\n## Contributors\n\n'"$contributors"
|
||||
fi
|
||||
printf 'Release notes:\n%s\n' "$notes"
|
||||
|
||||
release_files=(
|
||||
"dist/release/gen1recomp-${v}-macos.zip"
|
||||
"dist/release/gen1recomp-${v}-windows.zip"
|
||||
"dist/release/gen1recomp-${v}-linux.zip"
|
||||
"dist/release/gen1recomp-${v}-linux-arm64.AppImage"
|
||||
"dist/release/gen1recomp-${v}-android.apk"
|
||||
"dist/release/gen1recomp++-${v}-ios.ipa"
|
||||
"dist/release/gen1recomp-${v}-switch.zip"
|
||||
"dist/release/gen1recomp-${v}-xbox-uwp.zip"
|
||||
"dist/release/gen1recomp-${v}-rg34xxsp-stockos64-mod.zip"
|
||||
"dist/release/gen1recomp-${v}-sbc-portmaster.zip"
|
||||
"dist/release/gen1recomp-${v}.love"
|
||||
"dist/release/sha256sums.txt"
|
||||
)
|
||||
|
||||
gh release create "$tag" \
|
||||
--target "$GITHUB_SHA" \
|
||||
--title "$v" \
|
||||
--notes "$notes" \
|
||||
"${release_files[@]}"
|
||||
"dist/release/gen1recomp-${v}-macos.zip" \
|
||||
"dist/release/gen1recomp-${v}-windows.zip" \
|
||||
"dist/release/gen1recomp-${v}-linux.zip" \
|
||||
"dist/release/gen1recomp-${v}-android.apk"
|
||||
|
||||
echo "Published release $tag"
|
||||
|
||||
- name: Update iOS app repository
|
||||
if: github.repository == 'bryanthaboi/gen1recomp'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
v="${{ needs.version.outputs.version }}"
|
||||
ipa="dist/release/gen1recomp++-${v}-ios.ipa"
|
||||
app_repo="mobile/ios/app-repo.json"
|
||||
[ -f "$ipa" ] || { echo "::error::$ipa not found"; exit 1; }
|
||||
[ -f "$app_repo" ] || { echo "::error::$app_repo not found"; exit 1; }
|
||||
|
||||
date="$(date -u +"%Y-%m-%d")"
|
||||
size="$(wc -c < "$ipa" | tr -d '[:space:]')"
|
||||
download_url="https://github.com/${GITHUB_REPOSITORY}/releases/download/v${v}/gen1recomp++-${v}-ios.ipa"
|
||||
bundle_id="com.theboisclub.gen1recompplusplus"
|
||||
localized_description="Gen1Recomp - A native Lua / LÖVE2D recreation of Gen 1 Poke"
|
||||
release_notes="$(GH_TOKEN="${{ github.token }}" gh release view "v${v}" --json body --jq '.body // ""' 2>/dev/null || true)"
|
||||
if [ -n "$release_notes" ]; then
|
||||
localized_description="$release_notes"
|
||||
fi
|
||||
entry="$(jq -n \
|
||||
--arg version "$v" \
|
||||
--arg date "$date" \
|
||||
--arg download_url "$download_url" \
|
||||
--arg localized_description "$localized_description" \
|
||||
--argjson size "$size" \
|
||||
'{version: $version, date: $date, size: $size, downloadURL: $download_url, localizedDescription: $localized_description}')"
|
||||
|
||||
if jq -e --arg bundle_id "$bundle_id" --arg version "$v" \
|
||||
'any(.apps[] | select(.bundleIdentifier == $bundle_id).versions[]?; .version == $version)' \
|
||||
"$app_repo" >/dev/null; then
|
||||
jq --arg bundle_id "$bundle_id" --arg version "$v" --argjson entry "$entry" \
|
||||
'(.apps[] | select(.bundleIdentifier == $bundle_id).versions) |= map(if .version == $version then $entry else . end)' \
|
||||
"$app_repo" > "$app_repo.tmp"
|
||||
else
|
||||
jq --arg bundle_id "$bundle_id" --argjson entry "$entry" \
|
||||
'(.apps[] | select(.bundleIdentifier == $bundle_id).versions) |= [$entry] + .' \
|
||||
"$app_repo" > "$app_repo.tmp"
|
||||
fi
|
||||
mv "$app_repo.tmp" "$app_repo"
|
||||
|
||||
# main is PR-only for everyone except deploy keys (the "main protection"
|
||||
# ruleset's bypass actor), so this push must authenticate with the
|
||||
# RELEASE_DEPLOY_KEY deploy key over SSH; the workflow's GITHUB_TOKEN
|
||||
# would be rejected by the branch protection.
|
||||
- name: Commit iOS app repository
|
||||
if: github.repository == 'bryanthaboi/gen1recomp'
|
||||
env:
|
||||
DEPLOY_KEY: ${{ secrets.RELEASE_DEPLOY_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git add mobile/ios/app-repo.json
|
||||
if git diff --cached --quiet; then
|
||||
echo "app-repo.json unchanged; nothing to push"
|
||||
exit 0
|
||||
fi
|
||||
key="$RUNNER_TEMP/release-deploy-key"
|
||||
printf '%s\n' "$DEPLOY_KEY" > "$key"
|
||||
chmod 600 "$key"
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git commit -m "chore(ios): update app-repo.json [skip ci]"
|
||||
git -c core.sshCommand="ssh -i $key -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new" \
|
||||
push "git@github.com:${GITHUB_REPOSITORY}.git" "HEAD:${GITHUB_REF_NAME}"
|
||||
rm -f "$key"
|
||||
|
||||
- name: Clean up signing keychain
|
||||
if: ${{ always() && github.repository == 'bryanthaboi/gen1recomp' }}
|
||||
if: always()
|
||||
run: |
|
||||
security delete-keychain "$RUNNER_TEMP/pokemon-signing.keychain-db" 2>/dev/null || true
|
||||
rm -f "$RUNNER_TEMP/release-deploy-key"
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
name: Switch artifact comment
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [ci]
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: artifact
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RUN_ID: ${{ github.event.workflow_run.id }}
|
||||
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
|
||||
HEAD_REPOSITORY: ${{ github.event.workflow_run.head_repository.full_name }}
|
||||
run: |
|
||||
artifact_id="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$RUN_ID/artifacts" --jq '.artifacts[] | select(.name == "gen1recomp-switch-nro") | .id')"
|
||||
[ -n "$artifact_id" ] || exit 0
|
||||
head_owner="${HEAD_REPOSITORY%%/*}"
|
||||
pr_number="$(gh api "repos/$GITHUB_REPOSITORY/pulls?state=open&head=$head_owner:$HEAD_BRANCH" --jq '.[0].number // empty')"
|
||||
[ -n "$pr_number" ] || exit 0
|
||||
echo "artifact_url=https://github.com/$GITHUB_REPOSITORY/actions/runs/$RUN_ID/artifacts/$artifact_id" >> "$GITHUB_OUTPUT"
|
||||
echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT"
|
||||
# Upsert via comment-tag only — do not delete-all bot comments (clobbers iOS).
|
||||
- name: Get build info
|
||||
id: build-info
|
||||
env:
|
||||
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
|
||||
run: |
|
||||
commit_hash="$(printf '%s' "$HEAD_SHA" | cut -c1-7)"
|
||||
build_time="$(date "+%Y-%m-%d %H:%M:%S")"
|
||||
echo "hash=$commit_hash" >> "$GITHUB_OUTPUT"
|
||||
echo "time=$build_time" >> "$GITHUB_OUTPUT"
|
||||
- name: comment Switch artifact
|
||||
if: steps.artifact.outputs.pr_number != ''
|
||||
uses: thollander/actions-comment-pull-request@v3
|
||||
with:
|
||||
message: |
|
||||
[gen1recomp-switch.nro](${{ steps.artifact.outputs.artifact_url }})
|
||||
|
||||
**Commit**: [#${{ steps.build-info.outputs.hash }}](https://github.com/${{ github.event.workflow_run.head_repository.full_name }}/commit/${{ github.event.workflow_run.head_sha }})
|
||||
**Build Time**: `${{ steps.build-info.outputs.time }}`
|
||||
|
||||
<sub>This comment was automatically generated. [View workflow run](https://github.com/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }})</sub>
|
||||
pr-number: ${{ steps.artifact.outputs.pr_number }}
|
||||
comment-tag: switch-build-result
|
||||
github-token: ${{ github.token }}
|
||||
@@ -3,9 +3,8 @@
|
||||
data/generated/
|
||||
assets/generated/
|
||||
|
||||
# LÖVE packages & archives
|
||||
# LÖVE packages
|
||||
*.love
|
||||
*.zip
|
||||
|
||||
# Local saves (LÖVE writes to its save dir, but keep the repo clean anyway)
|
||||
save/
|
||||
@@ -17,7 +16,6 @@ __pycache__/
|
||||
.*
|
||||
!.github/
|
||||
!.gitignore
|
||||
!.luacheckrc
|
||||
|
||||
# Android build outputs / local SDK path / packaged payload (keep love-android sources)
|
||||
mobile/android/app/build/
|
||||
@@ -32,56 +30,8 @@ mobile/ios/love-src/
|
||||
mobile/ios/cache/
|
||||
mobile/ios/build/
|
||||
|
||||
# love-nx vendor binaries (fetch per docs/switch-build.md; also covered by .*)
|
||||
.bazinga/love-nx/
|
||||
|
||||
# Final packaged build artifacts (mac/win/web/android/ios/switch) — see scripts/build.sh
|
||||
# Final packaged build artifacts (mac/win/web/android/ios) — see scripts/build.sh
|
||||
/dist/
|
||||
|
||||
# Switch OTA launcher build outputs
|
||||
ports/switch/ota-launcher/build/
|
||||
ports/switch/ota-launcher/build-host/
|
||||
ports/switch/ota-launcher/*.nro
|
||||
ports/switch/ota-launcher/*.nacp
|
||||
ports/switch/ota-launcher/*.elf
|
||||
ports/switch/ota-launcher/*.map
|
||||
ports/switch/ota-launcher/romfs/logo.rgba
|
||||
ports/switch/ota-launcher/romfs/logo.png
|
||||
ports/switch/ota-launcher/romfs/cacert.pem
|
||||
ports/switch/ota-launcher/romfs/ota-bootstrap.nro
|
||||
ports/switch/ota-bootstrap/build/
|
||||
ports/switch/ota-bootstrap/*.nro
|
||||
ports/switch/ota-bootstrap/*.nacp
|
||||
ports/switch/ota-bootstrap/*.elf
|
||||
ports/switch/ota-bootstrap/*.map
|
||||
|
||||
# Legacy manual convenience-copy location (superseded by /dist/android/)
|
||||
mobile/dist/
|
||||
|
||||
# Tiled map-editing workspace (tools/tiled_export.py). Derived from the imported
|
||||
# ROM cache exactly like data/generated/, so it is never committable.
|
||||
/build/tiled/
|
||||
|
||||
# per-machine iOS bundle-id pin (see scripts/build_ios.sh)
|
||||
mobile/ios/bundle_id.local
|
||||
|
||||
# Xbox UWP build output
|
||||
/ports/uwp/build/
|
||||
/ports/uwp/third_party/*/source/
|
||||
/ports/uwp/third_party/angle/depot_tools/
|
||||
|
||||
# Native TLS dialer build output (dotnet publish)
|
||||
/native/tls_dial/bin/
|
||||
/native/tls_dial/obj/
|
||||
/dist/native/
|
||||
/dist/win/
|
||||
/.bazinga/
|
||||
|
||||
# Local options / preferences
|
||||
/options.lua*
|
||||
|
||||
# User-owned ROMs imported for individual mods. Manifests declare the
|
||||
# destinations, but source checkouts and packaged mods never ship the files.
|
||||
/mods/*/baseroms/
|
||||
/imports/baseroms/
|
||||
/imports/baseroms-recovery/
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
-- Static-analysis config for `luacheck` (https://luacheck.readthedocs.io).
|
||||
--
|
||||
-- Run it over the engine with: luacheck src (or scripts/lint.sh)
|
||||
--
|
||||
-- The point is a high-signal baseline: the categories left on are the ones
|
||||
-- that catch real defects -- undefined globals/locals (the class that hid a
|
||||
-- `music.volume` crash: applyVolume read a `state` that was still the nil
|
||||
-- global), unused values, unreachable code, redefinitions. The cosmetic
|
||||
-- categories the codebase deliberately lives with (a `self`/`dt` an
|
||||
-- interface requires but a given method ignores, documented empty
|
||||
-- fall-through branches, the odd long line) are muted so they don't drown
|
||||
-- the signal.
|
||||
|
||||
std = "luajit"
|
||||
|
||||
-- LÖVE exposes `love` as a mutable table: games assign their callbacks onto
|
||||
-- it (love.wheelmoved, love.run, ...), so it is a regular global, not
|
||||
-- read-only -- otherwise every callback registration reads as a violation.
|
||||
globals = { "love" }
|
||||
|
||||
read_globals = {
|
||||
"jit",
|
||||
-- LuaJIT 2.1 ships table.unpack even though the bare 5.1 `table` std lacks
|
||||
-- it; without this, every `table.unpack` reads as an undefined field.
|
||||
table = { fields = { "unpack" } },
|
||||
}
|
||||
|
||||
-- Vendored/native trees and the test suites have their own conventions.
|
||||
exclude_files = {
|
||||
"mobile/",
|
||||
"tests/",
|
||||
"tools/save-editor/",
|
||||
}
|
||||
|
||||
ignore = {
|
||||
"212", -- unused argument -- self/dt kept for a shared method signature
|
||||
"213", -- unused loop variable -- `for _, v in` where only v is wanted
|
||||
"421", -- shadowing a local -- deliberate re-use in a few tight scopes
|
||||
"431", -- shadowing an upvalue
|
||||
"432", -- shadowing an argument
|
||||
"542", -- empty if branch -- documented fall-throughs, not gaps
|
||||
"631", -- line is too long
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
# 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.
|
||||
@@ -118,7 +118,6 @@ not a hard error, so the list can grow without breaking old mods.
|
||||
| `QUEST` | New story, NPCs, dialogue, cutscenes | content |
|
||||
| `MECHANIC` | New or changed battle/field mechanics via hooks/effects | overhaul |
|
||||
| `GRAPHICS` | Sprite / tileset / palette / font changes | content |
|
||||
| `LANGUAGE` | A translation: `text`, `strings` and the glyphs it needs | content |
|
||||
| `AUDIO` | Music, sfx, cries | content |
|
||||
| `UI` | New or modified screens, menus, overlays | content / overhaul |
|
||||
| `TOOL` | Dev/QoL utilities, overlays, inter-mod libraries | content |
|
||||
@@ -129,195 +128,10 @@ not a hard error, so the list can grow without breaking old mods.
|
||||
keeps validating with the value it has shipped since before the taxonomy
|
||||
existed.
|
||||
|
||||
A translation may also set `"language": true` in the manifest. That is the
|
||||
one claim online play acts on: an install running nothing but verified
|
||||
translations may take an ONLINE MATCH or a TOURNAMENT instead of being
|
||||
asked to restart vanilla. The claim is checked, not taken -- the mod
|
||||
qualifies only if every record it writes lands in `text`, `strings` or
|
||||
`font`, it wraps no hook, subscribes to no event and requests no
|
||||
permission. Anything else and it is an ordinary content mod that happens to
|
||||
ship text.
|
||||
|
||||
### 4. `games` (and the legacy `gen2compat`)
|
||||
|
||||
Pokemon Gold is Gen 2, and it runs its own battle engine, overworld, script
|
||||
VM and save format. The mod API is shared across both generations (same hook
|
||||
names, same event names, same registry names) but Gold cannot serve all of it
|
||||
yet, so Gen 2 is opt-in. Say which games the mod is for:
|
||||
|
||||
```json
|
||||
"games": ["gen1", "gen2"]
|
||||
```
|
||||
|
||||
Each entry is a version id (`"red"`, `"blue"`, `"yellow"`, `"gold"`), a
|
||||
generation (`"gen1"`, `"gen2"`) or `"all"`;
|
||||
`src/mods/ModTargets.lua` resolves them off `GameVersion.ORDER` so nothing
|
||||
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,
|
||||
`mods/<id>/`, shared by every game -- targeting is declared, never filed.
|
||||
|
||||
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
|
||||
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
|
||||
all: the manager lists it as `ENABLED (NOT THIS GAME)` and says why, because a
|
||||
mod that half-applies reads as a broken mod. Claim Gen 2 once you have actually
|
||||
run your mod on Gold.
|
||||
|
||||
Every token is enforced, per game: the loader gates on the same
|
||||
`ModTargets.supports` answer both mod surfaces draw, so `"games": ["blue"]`
|
||||
really does not load on Red and the skip line is the launcher's line, `For
|
||||
Blue, not Red`, and `"games": ["gold"]` alone does not load on Red either. A
|
||||
manifest with neither key still covers every Gen 1 game, so nothing written
|
||||
before the key existed changes behavior; list both generations or say `"all"`
|
||||
when you mean everywhere.
|
||||
|
||||
`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 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.
|
||||
`docs/preparing-your-mod-for-gen2.md` is the step-by-step migration guide for a
|
||||
Gen 1 mod, and it is the one to start from.
|
||||
|
||||
Two consequences worth knowing before you claim Gen 2.
|
||||
|
||||
**Dependencies are contagious.** A mod whose hard dependency does not run here
|
||||
is left out too, with the dependency's own wording (`depends on X, which does
|
||||
not run here (For Blue, not Red)`). It is reported as a skip, not as a failure,
|
||||
and neither mod lands on the boot error list, but the mod does not run, so
|
||||
every hard dependency has to cover the same games.
|
||||
|
||||
**The player can override you.** The claim is yours, and a mod written before
|
||||
the key existed can never carry one, so the manager's detail pane offers
|
||||
`TRY HERE ANYWAY` for any mod that does not claim the game being played. It
|
||||
persists per game in `options.modsGen2[id][version]` and takes effect on the
|
||||
next boot; forcing a mod onto Red does not force it onto Gold. A forced mod
|
||||
loads normally and keeps a note saying its author never verified it here.
|
||||
|
||||
**Prefer the API on Gold, but the Gen 1 names still work.** Gen 2 is a
|
||||
parallel module tree behind `src/core/Game2.lua`. In new code take the live
|
||||
game from `mod.game` (or the `game.ready` payload, or any `ui.*` hook's first
|
||||
argument) and the world from `mod.world`; both resolve per generation, and
|
||||
neither needs `engine_internals`.
|
||||
|
||||
For the mods written before Gold existed, a require made from a mod's own file
|
||||
is answered on a Gold boot by an adapter presenting the Gen 1 API over Gen 2
|
||||
internals. Fifteen names are served -- `src.core.Game`,
|
||||
`src.world.OverworldController`, `src.world.Map`, `src.world.NPC`,
|
||||
`src.world.Collision`, `src.world.WorldAPI`, `src.world.PikachuFollower`,
|
||||
`src.world.FieldDefaults`, `src.pokemon.Boxes`, `src.script.ScriptRunner`,
|
||||
`src.ui.PartyMenu`, `src.ui.StartMenu`, `src.ui.OptionsMenu`, `src.ui.BoxMenu`
|
||||
and `src.battle.BattleState`. `src/mods/Gen2Compat.lua` is the full table and
|
||||
publishes what it covers through `Gen2Compat.coverage(name)`, whose members are
|
||||
`backed`, `warned` or `absent`. A name with no adapter (`src.script.Commands`,
|
||||
`src.ui.OptionRows`) is reported against the mod that required it, and a member
|
||||
an adapter cannot back is absent or logs once rather than answering wrongly.
|
||||
|
||||
Things no adapter can fix, all mod-side: a hardcoded version allow-list
|
||||
(`GameVersion.get() == "red" or ...`) excludes you from Gold by construction;
|
||||
Gold's builtin screen ids carry a `Gen2` prefix, so a string match on
|
||||
`"BoxMenu"` matches nothing there; a write to a field on a live Gen 2 menu
|
||||
instance is inert; and `map.warpAt` is a table on Gen 1 and a method on Gold,
|
||||
so indexing it raises. Each has a route that works on both generations, in
|
||||
`docs/preparing-your-mod-for-gen2.md`.
|
||||
|
||||
Check it statically, then load it headless:
|
||||
|
||||
```sh
|
||||
python3 tools/modkit.py gen2check mods/my_mod
|
||||
```
|
||||
|
||||
```lua
|
||||
local run = T.sdk.loadMod("mods/my_mod", { generation = 2 })
|
||||
T.eq(run.mod and run.mod.state, "loaded",
|
||||
"runs on gen 2: " .. tostring(run.mod and run.mod.skipReason))
|
||||
T.eq(#run.errors, 0, "and loads with no boot errors")
|
||||
```
|
||||
|
||||
Assert the state, not only the error count: a gate skip is deliberately not an
|
||||
error, so `#run.errors == 0` passes for a mod that never ran a line.
|
||||
|
||||
`gen2check` answers `will load`, `will load but degrade` or `will not work`,
|
||||
with a `MK4xx` finding per site and an `unresolved:` note, with a file and a
|
||||
line, for every reach a static scan could not follow. Neither substitutes for a
|
||||
real Gold boot.
|
||||
|
||||
### 5. What a mod's code can reach
|
||||
|
||||
Your code runs in a sandbox (`src/mods/Sandbox.lua`), not against the
|
||||
engine's globals. Every chunk you author gets it: `main.lua`, your
|
||||
`options_schema`, and anything you `load()` yourself.
|
||||
|
||||
The globals the sandbox took away are still *reachable*, as compat
|
||||
stand-ins (`src/mods/LegacyCompat.lua`) that answer with the new API
|
||||
underneath. A mod written before the sandbox keeps working; it logs one
|
||||
warning per call it should migrate, and the mod manager lists them. What
|
||||
each stand-in actually does:
|
||||
|
||||
| Pre-sandbox call | What it does now | Migrate to |
|
||||
| --- | --- | --- |
|
||||
| `io.open`, `io.lines`, `love.filesystem.read`/`lines`/`newFile` | reads your own shipped files, then your overlay, then `mod.storage` | `mod:read`, `mod.storage` |
|
||||
| `love.filesystem.write`/`append`, `io.open(…, "w")`, `os.remove`, `os.rename` | writes to a private per-mod overlay under `mod_compat/<your id>/` | `mod.storage` |
|
||||
| `love.filesystem.getDirectoryItems`/`getInfo` | your own directory plus your overlay | `mod:list`, `mod:info` |
|
||||
| `love.filesystem.getSaveDirectory` and friends | a virtual root; anything joined to it lands in your overlay | `mod.storage` |
|
||||
| `os.getenv` | `nil`, except home-like names, which answer with that same virtual root | nothing |
|
||||
| `love.filesystem.load`, `dofile`, `loadfile` | compiles the chunk into your sandbox | `require`, `mod:read` plus `load` |
|
||||
| `love.system` | `getOS`/`getPowerInfo`/`getProcessorCount` read through; clipboard and `openURL` do nothing | `mod.device:powerInfo()`, `mod.steps` |
|
||||
| `love.event` | passes through, except `quit`, which does nothing | `mod.events`, `mod.hooks` |
|
||||
| `love.mousemoved = fn` and the other callbacks | installs on the real `love` table, the way it always did | `mod.hooks`, `mod.events` |
|
||||
| `package` | an inert stub, so `package.path = …` does not crash | `require` |
|
||||
|
||||
What has no stand-in, because there is nothing honest to reroute it to:
|
||||
|
||||
| Still refused | Why |
|
||||
| --- | --- |
|
||||
| `love.thread` | a LÖVE thread is a fresh Lua state with the full standard library, which no environment-based sandbox in this state can reach. Use `mod.fetch` for background HTTP (`network`) or `mod.job` for background compute (`background`) — both run your code inside the sandbox instead of outside it |
|
||||
| `require("ffi")` | arbitrary C |
|
||||
| `debug`, `getfenv`, `setfenv` | each one undoes the sandbox from inside |
|
||||
| `io.popen`, `os.execute` | spawning a process |
|
||||
| `love.run`, `love.errorhandler` | the engine's own loop and its crash path |
|
||||
| replacing a `love` module table (`love.filesystem = {}`) | the engine reads those tables too |
|
||||
|
||||
The rest of `love` passes through unchanged, so graphics, audio, timers and
|
||||
input work as they always have.
|
||||
|
||||
Three consequences worth knowing before you write against it:
|
||||
|
||||
- **Your globals are yours.** `_G` inside a mod is that mod's own table. Two
|
||||
mods no longer share a namespace, and neither can reach the engine's. To
|
||||
publish something to another mod, put it on `mod.exports` and let them
|
||||
`mod.find("your_id").exports` — the channel that was always the intended
|
||||
one. The same goes for the standard library: `string`, `table` and `math`
|
||||
are per-mod copies, so patching one is a local decision.
|
||||
- **Paths cannot climb.** `mod:read`, `mod:list`, `mod:info`, `mod.assets:path`
|
||||
and `mod.assets:image` join to your own directory, and `..`, absolute paths
|
||||
and drive letters are refused. So are `entry` and `options_schema` in your
|
||||
manifest. `mod:list("assets")` is the sandboxed `getDirectoryItems` for a
|
||||
folder you shipped; `mod:info` tells file from directory so a walk can
|
||||
recurse.
|
||||
- **Ship source, not bytecode.** A precompiled entry file is refused.
|
||||
|
||||
`permissions` in the manifest is still a disclosure the manager shows the
|
||||
player. `network` gates `require("socket")` and friends plus `mod.fetch`
|
||||
(non-blocking HTTP), and `background` gates `mod.job` (compute on a worker
|
||||
thread). Those two are the sanctioned ways to work off the main thread now
|
||||
that `love.thread` is refused. There is no
|
||||
permission that grants raw filesystem access, because no mod needs one:
|
||||
everything a mod legitimately writes is already scoped by
|
||||
`mod.storage` or the asset-transform derived root.
|
||||
|
||||
If your mod used one of the rerouted globals, the fix is almost always
|
||||
`mod.storage`. The overlay is a compatibility floor, not a second storage
|
||||
system: it is not scoped per playthrough, it does not migrate, and it is
|
||||
the first thing that will be dropped once the mods on the index have
|
||||
moved off it. Open an issue if you have a case `mod.storage` does not
|
||||
cover.
|
||||
|
||||
### 6. `mod.card`
|
||||
### 4. `mod.card`
|
||||
|
||||
The manifest is the *engine's* contract: identity, load order, dependencies,
|
||||
permissions, profile (see [Manifest specification](docs/modding.md#manifest-specification-manifestjson)).
|
||||
The card is the *human-facing* one: who made this,
|
||||
permissions, profile. The card is the *human-facing* one: who made this,
|
||||
what it changes, what it does not do yet. It is never read by the loader's
|
||||
merge — only by tooling and the manager's detail pane — so an absent or
|
||||
malformed card can never break a load.
|
||||
@@ -335,7 +149,7 @@ Two fields deserve their own note:
|
||||
distributed mod never carries ROM-derived bytes, not even in its preview
|
||||
images.
|
||||
|
||||
### 7. Tags
|
||||
### 5. Tags
|
||||
|
||||
Lowercase kebab strings, open vocabulary. The showcase generator
|
||||
lowercases and de-dupes. A recommended starting set: `beginner`,
|
||||
@@ -393,15 +207,12 @@ registry or a new schema field lands with its catalog entry in the same PR
|
||||
and the generator runs clean:
|
||||
|
||||
```sh
|
||||
luajit tools/gen_registry_docs.lua # docs/modding/reference/registries.md
|
||||
luajit tools/gen_registry_docs.lua ../project.wiki # Reference-Registries.md in a wiki checkout
|
||||
luajit tools/gen_registry_docs.lua # in-repo default
|
||||
luajit tools/gen_registry_docs.lua ../project.wiki # the wiki checkout
|
||||
```
|
||||
|
||||
With no argument it writes inside the repo, which is the copy `python3
|
||||
tools/modkit.py docs` regenerates and `--out` copies from. Pass a directory
|
||||
(or set `POKEPORT_DOCS_DIR`) to write the wiki's flat page name into a wiki
|
||||
checkout instead. The prose reference lives in the GitHub wiki; both copies
|
||||
come off `src/mods/Schemas.lua`, so neither can drift from the engine.
|
||||
The prose reference lives in the GitHub wiki; the generated pages are
|
||||
written into a checkout of it, so they cannot drift from the engine.
|
||||
|
||||
### 5. Deprecation etiquette
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
Copyright 2026 BOIS CLUB GAMES, LLC
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -1,192 +1,49 @@
|
||||
# Gen1Recomp
|
||||
# Pokemon Gen 1 Recompilation Project
|
||||
|
||||
A native LÖVE2D recreation of Poke Red, Blue and Yellow. The engine and map
|
||||
behavior are hand-written Lua; game data and graphics are decoded from a ROM
|
||||
supplied by the player.
|
||||
|
||||
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]
|
||||
> **We are NOT affiliated with the website `gen1recomp[.]com`** That website is not run by this project, was not authorized by us, and we have no idea who operates it. It is impersonating this project; do not download anything from it, and treat anything it hosts or claims as untrustworthy. Even if the site currently links back to this repository, the people behind it can change its content at any time, so nothing on it should ever be trusted. This GitHub repository and the Discord linked below are the only official sources for this project.
|
||||
|
||||
<p align="center"><img src="https://raw.githubusercontent.com/bryanthaboi/gen1recomp/refs/heads/dev/assets/logo/logo.png"></p>
|
||||
|
||||
**SUPPORT / ANNOUNCEMENTS / MODS:** [Discord](https://bois.icu)
|
||||
|
||||
<p align="center">
|
||||
|
||||
<a href="https://www.youtube.com/@bryanthaboi">
|
||||
<img src="https://img.shields.io/badge/YouTube-FF0000?style=for-the-badge&logo=youtube&logoColor=white" alt="YouTube">
|
||||
</a>
|
||||
<a href="https://www.tiktok.com/@bryanthaboi">
|
||||
<img src="https://img.shields.io/badge/TikTok-000000?style=for-the-badge&logo=tiktok&logoColor=white" alt="TikTok">
|
||||
</a>
|
||||
<a href="https://x.com/bryanthaboi">
|
||||
<img src="https://img.shields.io/badge/X-000000?style=for-the-badge&logo=x&logoColor=white" alt="X">
|
||||
</a>
|
||||
<a href="https://bsky.app/profile/bryanthaboi.live">
|
||||
<img src="https://img.shields.io/badge/Bluesky-0285FF?style=for-the-badge&logo=bluesky&logoColor=white" alt="Bluesky">
|
||||
</a>
|
||||
<a href="https://www.instagram.com/bryanthaboi">
|
||||
<img src="https://img.shields.io/badge/Instagram-E4405F?style=for-the-badge&logo=instagram&logoColor=white" alt="Instagram">
|
||||
</a>
|
||||
|
||||
</p>
|
||||
|
||||
|
||||
<p align="center"> <a href="https://www.polygon.com/pokemon-red-blue-3d-voxel-mod-battle-pixels-gameplay-footage-remake/"> <img src="https://img.shields.io/badge/AS%20SEEN%20ON-POLYGON-ea2e49?style=for-the-badge" alt="As seen on Polygon"> </a>
|
||||
<a href="https://kotaku.com/pokemon-red-blue-recompilation-project-voxel-3d-mod-2000720281"> <img src="https://img.shields.io/badge/AS%20SEEN%20ON-KOTAKU-ea2e49?style=for-the-badge" alt="As seen on KOTAKU"> </a>
|
||||
|
||||
<a href="https://www.digitalfoundry.net/news/2026/07/pokemon-yellow-voxel-mod-turns-the-original-gameboy-code-into-a-stunning-world">
|
||||
<img src="https://img.shields.io/badge/AS%20SEEN%20ON-DIGITAL%20FOUNDRY-ea2e49?style=for-the-badge" alt="As seen on Digital Foundry">
|
||||
</a>
|
||||
<a href="https://www.androidauthority.com/unofficial-android-port-pokemon-red-blue-yellow-3692724/">
|
||||
<img src="https://img.shields.io/badge/AS%20SEEN%20ON-ANDROID%20AUTHORITY-ea2e49?style=for-the-badge" alt="As seen on Android Authority">
|
||||
</a>
|
||||
|
||||
<a href="https://www.xda-developers.com/this-amazing-pokemon-red-and-blue-voxel-mod-adds-a-3d-perspective-without-an-emulator/">
|
||||
<img src="https://img.shields.io/badge/AS%20SEEN%20ON-XDA%20DEVELOPERS-ea2e49?style=for-the-badge" alt="As seen on XDA Developers">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
### Watch the latest update video
|
||||
|
||||
[](https://www.youtube.com/watch?v=8IOgqbe4YvA)
|
||||
A native LÖVE2D recreation of Pokemon Red. The engine and map behavior are
|
||||
hand-written Lua; game data and graphics are decoded from a ROM supplied by
|
||||
the player.
|
||||
|
||||
SUPPORT AND ANNOUNCEMENTS: [Discord](https://bois.icu)
|
||||
|
||||
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
|
||||
is the only game content input.
|
||||
or download a disassembly. A canonical US Pokemon Red ROM is the only game
|
||||
content input.
|
||||
|
||||
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
|
||||
do not ask for the ROM again. Red, Blue, Yellow, and Gold can all be imported
|
||||
side by side. Gold is Gen 2 Phase 1 (import + launcher; see
|
||||
`docs/gold-phase1.md`): the Gen 2 engine is still under construction.
|
||||
do not ask for the ROM again.
|
||||
|
||||
## Quick Start
|
||||
|
||||
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
|
||||
game starts automatically.
|
||||
|
||||
Only the canonical US Red, Blue, Yellow (1 MiB), and Gold (2 MiB) ROMs are
|
||||
accepted. The importer verifies SHA-1 before creating any game data:
|
||||
|
||||
- Red: `ea9bcae617fdf159b045185467ae58b2e4a48b9a`
|
||||
- Blue: `d7037c83e1ae5b39bde3c30787637ba1d4c48ce2`
|
||||
- Yellow: `cc7d03262ebfaf2f06772c1a480c7d9d5f4a38e1`
|
||||
- Gold: `d8b8a3600a465308c9953dfa04f0081c05bdcb94`
|
||||
Open the desktop app. On first boot, choose your legally obtained `.gb` file
|
||||
or drop it onto the window. Import takes a few seconds and the game starts
|
||||
automatically.
|
||||
|
||||
Only the canonical 1 MiB US Red ROM is accepted. The importer verifies SHA-1
|
||||
`ea9bcae617fdf159b045185467ae58b2e4a48b9a` before creating any game data.
|
||||
The packaged app contains neither a ROM nor pre-extracted game data. Music,
|
||||
sound effects, and cries are synthesized while the game runs from compact
|
||||
audio channel programs copied out of the verified ROM.
|
||||
|
||||
### A note on Windows Defender warnings
|
||||
|
||||
Windows Defender sometimes flags the Windows build with a generic
|
||||
machine-learning detection such as `Trojan:Win32/Wacatac!ml` (#621). This is
|
||||
a known false positive: the exe is the official LÖVE runtime with the game
|
||||
archive appended (the standard way LÖVE games ship), and Defender's
|
||||
heuristics distrust unsigned executables with appended data. Every release
|
||||
publishes SHA-256 checksums (`sha256sums.txt`) so you can verify your
|
||||
download, and you can confirm a flagged file yourself on
|
||||
[VirusTotal](https://www.virustotal.com), where these builds come back clean
|
||||
on every engine except Defender's heuristic. False positives are reported to
|
||||
Microsoft as they come up.
|
||||
|
||||
## Controls
|
||||
|
||||
|
||||
| Action | Keyboard | Controller |
|
||||
| ------ | ----------------- | ------------------ |
|
||||
| Move | Arrow keys / WASD | D-pad / left stick |
|
||||
| A | Z / Enter / Space | A |
|
||||
| B | X / Backspace | B |
|
||||
| Start | Escape | Start |
|
||||
| Select | Tab / Shift | Back / Select |
|
||||
|
||||
|
||||
Rebind any of these in-game under **OPTIONS → CONTROLS**. Controllers are
|
||||
supported out of the box.
|
||||
|
||||
### Hotkeys
|
||||
|
||||
|
||||
| Key | What it does |
|
||||
| --------- | ---------------------------------------------------- |
|
||||
| `-` / `=` | Zoom out / in (overworld; also mouse wheel) |
|
||||
| `1` | Cycle GAME SPEED up (controller: R2 faster, L2 slower) |
|
||||
| `2` | Cycle COLORS |
|
||||
| `3` | Cycle TILT (free-roam overworld) |
|
||||
| `4` | Cycle ZOOM through every level (free-roam overworld) |
|
||||
| `5` | Cycle GBC FX |
|
||||
| `F1` | Save |
|
||||
| `F2` | Load |
|
||||
| `F10` | Open / close the mod manager |
|
||||
|
||||
|
||||
COLORS, TILT, ZOOM, GBC FX, GAME SPEED, and VOID FILL are also in the
|
||||
Options menu and persist in `options.lua`.
|
||||
|
||||
### Low-end devices
|
||||
|
||||
**OPTIONS → PERFORMANCE** scales the port's optional extras for weaker
|
||||
hardware: **HIGH** (everything on), **BALANCED** (no 3D tilt or GBC FX),
|
||||
**LOW** (also no survey zoom, FPS capped), or **AUTO** — the default, which
|
||||
picks a tier from your device (ARM handhelds → LOW, phones → BALANCED,
|
||||
normal desktops → HIGH, unchanged). It only scales presentation; the
|
||||
fixed-step game logic is identical on every tier, and a lower tier hides
|
||||
your tilt/zoom/GBC-FX preferences without forgetting them. Details in
|
||||
[docs/new-features.md](docs/new-features.md#performance-tier-low-end-devices).
|
||||
|
||||
### Rulesets
|
||||
|
||||
**OPTIONS → RULESET** picks which set of Gen 1 battle behaviors to run.
|
||||
Both rulesets share the same damage formulas; they differ only in whether
|
||||
the original's quirks are kept. The setting persists in `options.lua`, and
|
||||
mods can register their own.
|
||||
|
||||
`gen1_faithful` is the default and reproduces the original cartridge,
|
||||
famous bugs included:
|
||||
|
||||
| Rule | Behavior |
|
||||
| --------------------------- | ----------------------------------------------------- |
|
||||
| `oneIn256Miss` | A 100%-accurate move still misses on a roll of 255 |
|
||||
| `critUsesBaseSpeed` | Crit rate reads base speed, not the current stat |
|
||||
| `critIgnoresStages` | Crit rate ignores stat stages |
|
||||
| `focusEnergyBug` | FOCUS ENERGY quarters the crit rate instead of x4 |
|
||||
| `enemyUnlimitedPP` | Enemies never spend PP, so they never Struggle |
|
||||
| `hyperBeamSkipRechargeOnKO` | HYPER BEAM skips its recharge when the target faints |
|
||||
| `randMin` / `randMax` | Damage random factor 217-255 |
|
||||
|
||||
`modern_clean` keeps the formulas but removes the notorious quirks:
|
||||
|
||||
| Rule | Behavior |
|
||||
| --------------------------- | ----------------------------------------------------- |
|
||||
| `oneIn256Miss` | Off: a 100%-accurate move always hits |
|
||||
| `critUsesBaseSpeed` | Unchanged: crit rate still reads base speed |
|
||||
| `critIgnoresStages` | Off: stat stages count toward the crit rate |
|
||||
| `focusEnergyBug` | Off: FOCUS ENERGY raises the crit rate as intended |
|
||||
| `enemyUnlimitedPP` | Off: enemies deplete PP and Struggle when empty |
|
||||
| `hyperBeamSkipRechargeOnKO` | Off: HYPER BEAM always recharges, like Gen 2+ |
|
||||
| `randMin` / `randMax` | Damage random factor 217-255, same as faithful |
|
||||
arrow keys or WASD move; Z, Enter, or Space is A; X or Backspace is
|
||||
B; Escape opens START. F1 saves and F2 loads. Controllers are supported.
|
||||
|
||||
## Running From Source
|
||||
|
||||
Requires LÖVE 11.x. Place a Red, Blue, or Yellow ROM in the project folder and
|
||||
double-click `Play-Mac.command` or `Play-Windows.bat`, or run:
|
||||
Requires LÖVE 11.x. Place the ROM in the project folder and double-click
|
||||
`Play-Mac.command` or `Play-Windows.bat`, or run:
|
||||
|
||||
```sh
|
||||
scripts/setup.sh --rom "/path/to/Poke Red.gb" # or Blue.gb / Yellow.gbc
|
||||
scripts/setup.sh --rom "/path/to/Pokemon Red.gb"
|
||||
scripts/run.sh
|
||||
```
|
||||
|
||||
then `love .` for later launches. Windows PowerShell scripts, the optional
|
||||
developer data build, test suites, and cache management are covered in
|
||||
[Developer Setup](https://github.com/bryanthaboi/gen1recomp/wiki/Guide-Developer-Setup).
|
||||
[Developer Setup](https://github.com/bryanthaboi/pokemon-gen1-recomp-project/wiki/Guide-Developer-Setup).
|
||||
|
||||
## Portable Mode
|
||||
|
||||
@@ -194,7 +51,7 @@ By default the game keeps your save, options, and the private ROM-derived
|
||||
data cache in your OS's normal per-user app data folder. To keep everything
|
||||
next to the game instead (handy for a USB stick or portable drive you carry
|
||||
between computers), drop an empty file named `portable.txt` next to the app
|
||||
(next to `gen1recomp.app`/`.exe`, or next to `main.lua`/`conf.lua` when
|
||||
(next to `PokemonRed.app`/`.exe`, or next to `main.lua`/`conf.lua` when
|
||||
running from source), then launch the game. Portable mode is desktop-only
|
||||
(Windows, Linux, macOS); it has no effect on Android or iOS, where the app
|
||||
runs from a read-only package.
|
||||
@@ -202,120 +59,14 @@ runs from a read-only package.
|
||||
With `portable.txt` present:
|
||||
|
||||
- `save.lua`, `save.lua.bak`, and `options.lua` are read from and written to
|
||||
that same folder instead of the OS save directory.
|
||||
that same folder instead of the OS save directory.
|
||||
- A ROM import writes the generated `data/generated` and `assets/generated`
|
||||
cache straight into that folder too (nothing is left in the OS save
|
||||
directory), so a later launch reuses it without asking for the ROM again
|
||||
even on a different computer, as long as the same folder comes along.
|
||||
cache straight into that folder too (nothing is left in the OS save
|
||||
directory), so a later launch reuses it without asking for the ROM again
|
||||
even on a different computer, as long as the same folder comes along.
|
||||
- Deleting `portable.txt` switches back to the normal OS save directory; nothing
|
||||
already written to either location is touched automatically, so copy files
|
||||
over yourself if you want to carry existing progress across the switch.
|
||||
|
||||
## Launch Options
|
||||
|
||||
By default the app opens the launcher so you can pick a game. Launch options
|
||||
skip it and start one game directly, which is what you want for a one-click
|
||||
entry: a desktop shortcut per game, a Steam entry, or a handheld frontend.
|
||||
|
||||
| Option | Effect |
|
||||
| --- | --- |
|
||||
| `--game=red` | boot Red, skipping the launcher (`blue` and `yellow` too, or just `r` / `b` / `y`) |
|
||||
| `--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 |
|
||||
|
||||
|
||||
## Linux on arm64 (Raspberry Pi)
|
||||
|
||||
Alongside the x86_64 `gen1recomp-*-linux.zip`, every release ships
|
||||
`gen1recomp-*-linux-arm64.AppImage` for 64-bit ARM desktop Linux — Raspberry
|
||||
Pi 4/5, Armbian and other SBC distros, and arm64 VMs on Apple Silicon:
|
||||
|
||||
```sh
|
||||
chmod +x gen1recomp-*-linux-arm64.AppImage
|
||||
./gen1recomp-*-linux-arm64.AppImage
|
||||
```
|
||||
|
||||
LÖVE publishes no aarch64 binary of any kind, so this artifact compiles the
|
||||
engine — and SDL2, OpenAL and the codecs — from source inside a Debian
|
||||
bullseye arm64 container. It needs only glibc 2.29+, libstdc++, freetype and
|
||||
zlib on the host; OpenGL, X11, Wayland, KMSDRM, ALSA and PulseAudio are all
|
||||
dlopened, so the same image runs on a full desktop, a Wayland-only session or
|
||||
a KMSDRM handheld with no X server. Build instructions and the reasoning are
|
||||
in [docs/linux-arm64-build.md](docs/linux-arm64-build.md).
|
||||
|
||||
|
||||
## iOS
|
||||
|
||||
Every release ships `gen1recomp++-*-ios.ipa`. Sideload it with AltStore
|
||||
(Windows or Mac) — see [docs/ios-sideload.md](docs/ios-sideload.md). To
|
||||
build and install from source on a Mac instead, see
|
||||
[docs/ios-install.md](docs/ios-install.md).
|
||||
|
||||
<div>
|
||||
<a href="https://intradeus.github.io/http-protocol-redirector?r=sidestore://source?url=https://github.com/bryanthaboi/gen1recomp/raw/refs/heads/main/mobile/ios/app-repo.json"><img src="./.github/resources/sidestore-badge.png" alt="Add to SideStore" height="60"></a>
|
||||
|
||||
<a href="https://intradeus.github.io/http-protocol-redirector?r=feather://source/https://github.com/bryanthaboi/gen1recomp/raw/refs/heads/main/mobile/ios/app-repo.json"><img src="./.github/resources/feather-badge.png" alt="Add to Feather" height="60"></a>
|
||||
|
||||
<a href="https://intradeus.github.io/http-protocol-redirector?r=altstore://source?url=https://github.com/bryanthaboi/gen1recomp/raw/refs/heads/main/mobile/ios/app-repo.json"><img src="./.github/resources/altstore-badge.png" alt="Add to AltStore" height="60"></a>
|
||||
|
||||
<a href="https://github.com/bryanthaboi/gen1recomp/releases/latest"><img src="./.github/resources/github-badge.png" alt="Download from GitHub" height="60"></a>
|
||||
</div>
|
||||
|
||||
## Xbox Dev Mode
|
||||
|
||||
Every release ships `gen1recomp-*-xbox-uwp.zip` for Xbox One and Xbox Series
|
||||
consoles in Developer Mode. It cannot be installed in retail mode.
|
||||
|
||||
Extract the archive, then use Xbox Device Portal to install the `.msix` and
|
||||
the x64 package under `Dependencies`.
|
||||
|
||||
### External setup
|
||||
|
||||
1. Put your legally obtained Red, Blue, or Yellow ROMs on an external drive.
|
||||
Mod ZIPs can go on the same drive.
|
||||
2. Connect the drive to the Xbox and open Gen1Recomp.
|
||||
3. Select **Import ROM** or **Import Mod**, then choose the file with the Xbox
|
||||
file picker.
|
||||
4. Repeat the ROM import for each version you want to use.
|
||||
|
||||
### Internal setup
|
||||
|
||||
1. Create a folder named `baseroms` on your PC and place your legally obtained
|
||||
Red, Blue, or Yellow ROMs inside it.
|
||||
2. ZIP the folder, keeping `baseroms` at the top level of the archive.
|
||||
3. Launch Gen1Recomp once, then close it.
|
||||
4. Open Xbox Device Portal and upload the ZIP to
|
||||
`Gen1Recomp/LocalState/pokemon-love2d/`.
|
||||
5. Choose **Yes** when Device Portal asks whether to extract the archive.
|
||||
6. Open Gen1Recomp. The launcher checks baseroms once at startup. When it finds a compatible ROM, that game’s tab shows ROM FOUND and an Import detected ROM button.
|
||||
|
||||
ROMs, generated game data, saves, and mods remain in LocalState and are not
|
||||
included in the app.
|
||||
|
||||
Source builds and package details are covered in
|
||||
[the Xbox UWP build notes](ports/uwp/BUILD.md).
|
||||
|
||||
## Handhelds
|
||||
|
||||
A PortMaster-style port for the **Anbernic RG34XXSP** on Stock OS 64-bit MOD
|
||||
ships with every release as `gen1recomp-*-rg34xxsp-stockos64-mod.zip`.
|
||||
Install steps, controls, and troubleshooting live in
|
||||
[docs/anbernic-rg34xxsp.md](docs/anbernic-rg34xxsp.md).
|
||||
|
||||
## Nintendo Switch
|
||||
|
||||
Releases ship an SD-ready `gen1recomp-*-switch.zip`. Runtime target is pinned
|
||||
[love-nx](https://github.com/retronx-team/love-nx) `11.5-nx1`. Requires a
|
||||
console that can run Switch homebrew.
|
||||
|
||||
- Players: [docs/switch-install.md](docs/switch-install.md). Download the
|
||||
zip, extract at the microSD root (install or update), title-override
|
||||
launch, import your own legal ROM, Joy-Con controls and shortcuts.
|
||||
- Builders: [docs/switch-build.md](docs/switch-build.md). `--fetch` /
|
||||
`--loose` / `--fused`, toolchain, Docker fallback, and CI vs release
|
||||
(path-gated ubuntu selftest, fused PR artifact on the main repo, release
|
||||
hard-fail).
|
||||
- File transfer (MTP / SD / FTP): [docs/switch-transfer.md](docs/switch-transfer.md).
|
||||
already written to either location is touched automatically, so copy files
|
||||
over yourself if you want to carry existing progress across the switch.
|
||||
|
||||
## Modding
|
||||
|
||||
@@ -323,40 +74,33 @@ The game ships a native mod platform: content registries, events and hooks,
|
||||
per-mod saves and options, and an in-game manager. The full modding book —
|
||||
getting started, a twelve-rung tutorial ladder, a cookbook, and the generated
|
||||
reference — lives on the
|
||||
[project wiki](https://github.com/bryanthaboi/gen1recomp/wiki).
|
||||
[project wiki](https://github.com/bryanthaboi/pokemon-gen1-recomp-project/wiki).
|
||||
|
||||
Shipped example mods, one per kind of author, live in `[mods/](mods/)`.
|
||||
|
||||
Maps can be edited in our own build of [Tiled](https://www.mapeditor.org),
|
||||
[bryanthaboi/tiled_gen1recomp](https://github.com/bryanthaboi/tiled_gen1recomp/releases),
|
||||
and exported back out as a mod; see
|
||||
[docs/tiled-map-editing.md](docs/tiled-map-editing.md).
|
||||
Shipped example mods, one per kind of author, live in [`mods/`](mods/).
|
||||
|
||||
## Bugs and Ideas
|
||||
|
||||
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
|
||||
original game.
|
||||
[Open a bug report](https://github.com/bryanthaboi/gen1recomp/issues/new?template=bug_report.yml).
|
||||
[Open a bug report](https://github.com/bryanthaboi/pokemon-gen1-recomp-project/issues/new?template=bug_report.yml).
|
||||
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.
|
||||
|
||||
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).
|
||||
[Open a feature request](https://github.com/bryanthaboi/pokemon-gen1-recomp-project/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
|
||||
|
||||
- [Link play](https://github.com/bryanthaboi/gen1recomp/wiki/Guide-Link-Play)
|
||||
— START > LINK connects two copies directly over UDP.
|
||||
- [Save editor](https://github.com/bryanthaboi/gen1recomp/wiki/Guide-Save-Editor)
|
||||
— edit party, boxes, items, events, and Pokédex flags outside the game.
|
||||
- [Link play](https://github.com/bryanthaboi/pokemon-gen1-recomp-project/wiki/Guide-Link-Play)
|
||||
— START > LINK connects two copies directly over UDP.
|
||||
- [Save editor](https://github.com/bryanthaboi/pokemon-gen1-recomp-project/wiki/Guide-Save-Editor)
|
||||
— edit party, boxes, items, events, and Pokédex flags outside the game.
|
||||
- `docs/architecture.md` — runtime details;
|
||||
`docs/behavior-porting-notes.md` — formula provenance.
|
||||
|
||||
|
||||
`docs/behavior-porting-notes.md` — formula provenance.
|
||||
|
||||
## Special Thanks
|
||||
|
||||
@@ -364,4 +108,6 @@ This project would not be possible without [pret](https://github.com/pret) >
|
||||
the pret band of decompiling maniacs > and their
|
||||
[pokered](https://github.com/pret/pokered) disassembly.
|
||||
|
||||
<p align="center"><a href="https://boisclub.games"><img src="https://raw.githubusercontent.com/bryanthaboi/gen1recomp/refs/heads/dev/assets/logo/bcg.png"></a></p>
|
||||
## Wanna Support My Work?
|
||||
[](https://buymeacoffee.com/bryanthaboi)
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
# Plain Pixel font
|
||||
|
||||
"Plain Pixel Font" by Douglas Vautour (Burpy Fresh) is licensed under
|
||||
CC-BY 4.0: https://burpyfresh.itch.io
|
||||
|
||||
Version 0.009 (CJK character additions), unmodified. Characters for most
|
||||
languages have a 5x11 base but can extend vertically; double-width
|
||||
characters such as Hiragana and Katakana are 11x11.
|
||||
|
||||
Bundled so a translation mod can opt into TTF text rendering
|
||||
(`mod.content.font:register("ttf", {})`; see the Translation support
|
||||
section of docs/new-features.md) instead of drawing hundreds of glyph-page
|
||||
tiles. The tile font extracted from the player's ROM stays the default.
|
||||
|
Before Width: | Height: | Size: 243 KiB |
|
Before Width: | Height: | Size: 217 KiB |
|
Before Width: | Height: | Size: 236 KiB |
|
Before Width: | Height: | Size: 242 KiB |
|
Before Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 251 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 18 KiB |
@@ -1,10 +0,0 @@
|
||||
# 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.
|
||||
|
Before Width: | Height: | Size: 973 B |
|
Before Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 649 B |
|
Before Width: | Height: | Size: 645 B |
|
Before Width: | Height: | Size: 636 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
@@ -1,89 +0,0 @@
|
||||
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 =
|
||||
@@ -1,10 +0,0 @@
|
||||
# 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.
|
||||
|
Before Width: | Height: | Size: 1.9 MiB |
@@ -1,6 +0,0 @@
|
||||
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
|
||||
@@ -1,9 +0,0 @@
|
||||
# On-screen touch control art
|
||||
|
||||
From Xelu's "Free Controller & Keyboard Prompts" pack (Nicolae Berbece /
|
||||
Those Awesome Guys), public domain under CC0:
|
||||
https://thoseawesomeguys.com/prompts/
|
||||
|
||||
256px Nintendo Switch set, renamed for src/core/TouchControls.lua:
|
||||
d-pad (neutral + per-direction highlight), A, B, plus (START) and
|
||||
minus (SELECT).
|
||||
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 751 B |
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,366 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build a PortMaster aarch64 port of gen1recomp for Linux ARM SBC handhelds.
|
||||
# The package uses PortMaster control hooks and a self-contained LÖVE runtime,
|
||||
# while keeping paths relative to the launcher for broad CFW compatibility.
|
||||
#
|
||||
# The launcher uses SHDIR-relative paths and bundles the LÖVE 11.5 aarch64
|
||||
# runtime so the device does not need a separate runtime download on first launch.
|
||||
#
|
||||
# Usage:
|
||||
# ./build-linux-arm-sbc.sh [--version X.Y.Z]
|
||||
# GEN1RECOMP_SOURCE_DIR="$PWD" ./build-linux-arm-sbc.sh --version X.Y.Z
|
||||
# ./build-linux-arm-sbc.sh --source /path/to/gen1recomp --version X.Y.Z
|
||||
#
|
||||
# Output:
|
||||
# dist/linux-arm-sbc/gen1recomp-sbc-portmaster.zip
|
||||
#
|
||||
# Install on device:
|
||||
# 1. Install PortMaster for the handheld firmware.
|
||||
# 2. Unzip into the device's PortMaster ports folder so you have:
|
||||
# Roms/Ports (PORTS)/gen1recomp-sbc.sh
|
||||
# Roms/Ports (PORTS)/gen1recomp-sbc/...
|
||||
# 3. Copy a legal US Red or Blue .gb into Roms/Ports (PORTS)/gen1recomp-sbc/lovegame/
|
||||
# 4. Launch "gen1recomp-sbc" from the Ports list; press Choose ROM (scans that
|
||||
# folder when zenity is missing).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||
HERE="$ROOT/.bazinga"
|
||||
CACHE="$HERE/cache/linux-arm-sbc"
|
||||
WORK="$HERE/work/linux-arm-sbc"
|
||||
DIST="$ROOT/dist/linux-arm-sbc"
|
||||
|
||||
APP_NAME="gen1recomp-sbc"
|
||||
# Artifact suffix identifies this as the generic PortMaster SBC package.
|
||||
# Release uploads stage it as gen1recomp-<ver>-sbc-portmaster.zip.
|
||||
ARTIFACT_SUFFIX="portmaster"
|
||||
PORT_DIR_NAME="gen1recomp-sbc"
|
||||
LAUNCHER_NAME="gen1recomp-sbc.sh"
|
||||
LOVE_VERSION="11.5"
|
||||
# By default the pack is reproducible from the latest published GitHub release,
|
||||
# not whatever happens to be in the caller's checkout. Development builds can
|
||||
# point this at a local checkout with GEN1RECOMP_SOURCE_DIR=/path/to/repo.
|
||||
SOURCE_DIR_OVERRIDE="${GEN1RECOMP_SOURCE_DIR:-}"
|
||||
SOURCE_TAG_OVERRIDE="${GEN1RECOMP_RELEASE_TAG:-}"
|
||||
VERSION="${GEN1RECOMP_VERSION:-}"
|
||||
|
||||
# Official PortMaster LÖVE 11.5 aarch64 runtime (small love stub + liblove).
|
||||
PM_RUNTIME_BASE="https://raw.githubusercontent.com/PortsMaster/PortMaster-GUI/main/PortMaster/runtimes/love_${LOVE_VERSION}"
|
||||
RELEASES_LATEST_URL="https://github.com/bryanthaboi/gen1recomp/releases/latest"
|
||||
RELEASE_TARBALL_BASE="https://github.com/bryanthaboi/gen1recomp/archive/refs/tags"
|
||||
|
||||
say() { printf '\033[1;32m==>\033[0m %s\n' "$*"; }
|
||||
warn() { printf '\033[1;33mwarn:\033[0m %s\n' "$*" >&2; }
|
||||
fail() { printf '\033[1;31merror:\033[0m %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--version) [ $# -ge 2 ] || fail "--version needs X.Y.Z"; VERSION="$2"; shift ;;
|
||||
--source) [ $# -ge 2 ] || fail "--source needs a directory"; SOURCE_DIR_OVERRIDE="$2"; shift ;;
|
||||
--release-tag) [ $# -ge 2 ] || fail "--release-tag needs a tag"; SOURCE_TAG_OVERRIDE="$2"; shift ;;
|
||||
-h|--help)
|
||||
sed -n '2,24p' "$0"
|
||||
exit 0
|
||||
;;
|
||||
*) fail "unknown argument: $1" ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
command -v curl >/dev/null || fail "curl is required"
|
||||
command -v zip >/dev/null || fail "zip is required"
|
||||
command -v unzip >/dev/null || fail "unzip is required"
|
||||
command -v tar >/dev/null || fail "tar is required"
|
||||
|
||||
mkdir -p "$CACHE" "$WORK" "$DIST"
|
||||
|
||||
download() {
|
||||
local url="$1" dest="$2"
|
||||
if [ -f "$dest" ] && [ -s "$dest" ]; then
|
||||
return 0
|
||||
fi
|
||||
say "downloading $(basename "$dest")"
|
||||
curl -fL --progress-bar "$url" -o "$dest.tmp" \
|
||||
|| fail "download failed: $url"
|
||||
mv "$dest.tmp" "$dest"
|
||||
}
|
||||
|
||||
# --------------------------------------------------------------- source + game tree
|
||||
# Release builds use the latest published source archive. A local checkout is
|
||||
# an explicit override for development and for CI's just-built release source.
|
||||
if [ -n "$SOURCE_DIR_OVERRIDE" ]; then
|
||||
SOURCE_DIR_OVERRIDE="$(cd "$SOURCE_DIR_OVERRIDE" 2>/dev/null && pwd)" \
|
||||
|| fail "source directory does not exist: $SOURCE_DIR_OVERRIDE"
|
||||
SOURCE_DIR="$SOURCE_DIR_OVERRIDE"
|
||||
SOURCE_TAG="${SOURCE_TAG_OVERRIDE:-local}"
|
||||
if [ "$SOURCE_TAG" != "local" ]; then
|
||||
printf '%s' "$SOURCE_TAG" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$' \
|
||||
|| fail "release tag must look like vX.Y.Z: $SOURCE_TAG"
|
||||
fi
|
||||
if [ -z "$VERSION" ]; then
|
||||
VERSION="$(git -C "$SOURCE_DIR" rev-parse --short HEAD 2>/dev/null || echo dev)"
|
||||
fi
|
||||
else
|
||||
if [ -z "$SOURCE_TAG_OVERRIDE" ]; then
|
||||
latest_location="$(curl -fsSI "$RELEASES_LATEST_URL" \
|
||||
| awk 'tolower($1) == "location:" { print $2 }' | tail -1 | tr -d '\r')" \
|
||||
|| fail "could not resolve latest published release"
|
||||
SOURCE_TAG_OVERRIDE="${latest_location##*/}"
|
||||
fi
|
||||
printf '%s' "$SOURCE_TAG_OVERRIDE" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$' \
|
||||
|| fail "release tag must look like vX.Y.Z: $SOURCE_TAG_OVERRIDE"
|
||||
SOURCE_TAG="$SOURCE_TAG_OVERRIDE"
|
||||
SOURCE_ARCHIVE="$CACHE/gen1recomp-${SOURCE_TAG}.tar.gz"
|
||||
download "$RELEASE_TARBALL_BASE/$SOURCE_TAG.tar.gz" "$SOURCE_ARCHIVE"
|
||||
SOURCE_EXTRACT="$WORK/source-$SOURCE_TAG"
|
||||
rm -rf "$SOURCE_EXTRACT"
|
||||
mkdir -p "$SOURCE_EXTRACT"
|
||||
tar -xzf "$SOURCE_ARCHIVE" -C "$SOURCE_EXTRACT"
|
||||
SOURCE_DIR="$(find "$SOURCE_EXTRACT" -mindepth 1 -maxdepth 1 -type d -print -quit)"
|
||||
[ -n "$SOURCE_DIR" ] || fail "release archive had no source directory"
|
||||
if [ -z "$VERSION" ]; then VERSION="${SOURCE_TAG#v}"; fi
|
||||
fi
|
||||
|
||||
say "staging lovegame/ from $SOURCE_TAG"
|
||||
GAME_SRC="$WORK/lovegame"
|
||||
rm -rf "$GAME_SRC"
|
||||
mkdir -p "$GAME_SRC"
|
||||
|
||||
# Same payload as scripts/build.sh's game.love — never ship ROM-derived cache.
|
||||
# tools/save-editor is part of that payload: the launcher's Edit button on a
|
||||
# save row opens it in-process (main.lua).
|
||||
(cd "$SOURCE_DIR" && zip -q -9 -r "$WORK/game-payload.zip" \
|
||||
main.lua conf.lua src libs data assets tools/save-editor \
|
||||
tools/rom_manifest.json tools/rom_manifest_blue.json \
|
||||
-x '*.DS_Store' 'data/generated/*' 'assets/generated/*')
|
||||
if unzip -Z1 "$WORK/game-payload.zip" \
|
||||
| grep -Eq '^(data|assets)/generated/[^/]+|^(data|assets)/generated/.+/'; then
|
||||
fail "payload unexpectedly contains generated ROM data"
|
||||
fi
|
||||
unzip -q "$WORK/game-payload.zip" -d "$GAME_SRC"
|
||||
rm -f "$WORK/game-payload.zip"
|
||||
|
||||
# Stamp release version into the staged tree only (never the working tree).
|
||||
if printf '%s' "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||||
say "stamping engine version $VERSION"
|
||||
sed -E "s/(engine[[:space:]]*=[[:space:]]*\")[^\"]*(\")/\1$VERSION\2/" \
|
||||
"$SOURCE_DIR/src/core/Version.lua" > "$GAME_SRC/src/core/Version.lua"
|
||||
version_re="$(printf '%s' "$VERSION" | sed 's/\./\\./g')"
|
||||
grep -Eq "engine[[:space:]]*=[[:space:]]*\"$version_re\"" \
|
||||
"$GAME_SRC/src/core/Version.lua" \
|
||||
|| fail "version stamp failed"
|
||||
else
|
||||
say "version '$VERSION' is not X.Y.Z — shipping default engine (no stamp)"
|
||||
fi
|
||||
|
||||
# Portable marker: saves + ROM cache live next to the game on the SD card.
|
||||
: > "$GAME_SRC/portable.txt"
|
||||
|
||||
# --------------------------------------------------------------- love runtime
|
||||
say "fetching LÖVE $LOVE_VERSION aarch64 runtime"
|
||||
LOVE_BIN="$CACHE/love.aarch64"
|
||||
LOVE_LIB="$CACHE/liblove-11.5.so"
|
||||
LUAJIT_LIB="$CACHE/libluajit-5.1.so.2"
|
||||
MODPLUG_LIB="$CACHE/libmodplug.so.1"
|
||||
OGG_LIB="$CACHE/libogg.so.0"
|
||||
|
||||
download "$PM_RUNTIME_BASE/love.aarch64" "$LOVE_BIN"
|
||||
download "$PM_RUNTIME_BASE/libs.aarch64/liblove-11.5.so" "$LOVE_LIB"
|
||||
download "$PM_RUNTIME_BASE/libs.aarch64/libluajit-5.1.so.2" "$LUAJIT_LIB"
|
||||
download "$PM_RUNTIME_BASE/libs.aarch64/libmodplug.so.1" "$MODPLUG_LIB"
|
||||
download "$PM_RUNTIME_BASE/libs.aarch64/libogg.so.0" "$OGG_LIB"
|
||||
|
||||
# Sanity: love stub must be an aarch64 ELF.
|
||||
file "$LOVE_BIN" | grep -qi 'aarch64\|ARM aarch64' \
|
||||
|| fail "love.aarch64 does not look like an aarch64 ELF (got: $(file "$LOVE_BIN"))"
|
||||
|
||||
# --------------------------------------------------------------- port tree
|
||||
say "assembling port package"
|
||||
PORT_ROOT="$WORK/port"
|
||||
rm -rf "$PORT_ROOT"
|
||||
mkdir -p "$PORT_ROOT/$PORT_DIR_NAME/bin" \
|
||||
"$PORT_ROOT/$PORT_DIR_NAME/libs.aarch64" \
|
||||
"$PORT_ROOT/$PORT_DIR_NAME/licenses" \
|
||||
"$PORT_ROOT/$PORT_DIR_NAME/conf"
|
||||
|
||||
cp -R "$GAME_SRC" "$PORT_ROOT/$PORT_DIR_NAME/lovegame"
|
||||
cp "$LOVE_BIN" "$PORT_ROOT/$PORT_DIR_NAME/bin/love.aarch64"
|
||||
chmod +x "$PORT_ROOT/$PORT_DIR_NAME/bin/love.aarch64"
|
||||
cp "$LOVE_LIB" "$LUAJIT_LIB" "$MODPLUG_LIB" "$OGG_LIB" \
|
||||
"$PORT_ROOT/$PORT_DIR_NAME/libs.aarch64/"
|
||||
|
||||
# Drop a short license pointer for the bundled LÖVE bits.
|
||||
cat > "$PORT_ROOT/$PORT_DIR_NAME/licenses/LICENSE.love2d.txt" <<'EOF'
|
||||
This port bundles the LÖVE 11.5 aarch64 runtime from PortMaster
|
||||
(https://github.com/PortsMaster/PortMaster-GUI). LÖVE is zlib-licensed;
|
||||
see https://love2d.org/ for full terms.
|
||||
EOF
|
||||
|
||||
# --------------------------------------------------------------- launcher
|
||||
# Resolve the game directory from the launcher so this works with both
|
||||
# PortMaster-managed ports directories.
|
||||
cat > "$PORT_ROOT/$LAUNCHER_NAME" <<'EOF'
|
||||
#!/bin/bash
|
||||
# gen1recomp-sbc — Linux ARM SBC / PortMaster launcher
|
||||
# Uses SHDIR-relative paths so firmware-specific mount points do not matter.
|
||||
|
||||
export HOME="${HOME:-/root}"
|
||||
XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}"
|
||||
SHDIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
if [ -d "/mnt/SDCARD/Apps/PortMaster/PortMaster/" ]; then
|
||||
controlfolder="/mnt/SDCARD/Apps/PortMaster/PortMaster"
|
||||
elif [ -d "/mnt/SDCARD/Roms/ports/PortMaster" ]; then
|
||||
controlfolder="/mnt/SDCARD/Roms/ports/PortMaster"
|
||||
elif [ -d "/mnt/SDCARD/Data/PortMaster/" ]; then
|
||||
controlfolder="/mnt/SDCARD/Data/PortMaster"
|
||||
elif [ -d "$SHDIR/PortMaster" ]; then
|
||||
controlfolder="$SHDIR/PortMaster"
|
||||
elif [ -d "/opt/system/Tools/PortMaster/" ]; then
|
||||
controlfolder="/opt/system/Tools/PortMaster"
|
||||
elif [ -d "/opt/tools/PortMaster/" ]; then
|
||||
controlfolder="/opt/tools/PortMaster"
|
||||
elif [ -d "$XDG_DATA_HOME/PortMaster/" ]; then
|
||||
controlfolder="$XDG_DATA_HOME/PortMaster"
|
||||
elif [ -d "/roms/ports/PortMaster" ]; then
|
||||
controlfolder="/roms/ports/PortMaster"
|
||||
else
|
||||
controlfolder="/mnt/SDCARD/Roms/PORTS/PortMaster"
|
||||
fi
|
||||
|
||||
if [ ! -f "$controlfolder/control.txt" ]; then
|
||||
echo "PortMaster control.txt not found under $controlfolder" >&2
|
||||
exit 1
|
||||
fi
|
||||
# shellcheck disable=SC1090
|
||||
source "$controlfolder/control.txt"
|
||||
get_controls
|
||||
if [ -n "${CFW_NAME:-}" ] && [ -f "${controlfolder}/mod_${CFW_NAME}.txt" ]; then
|
||||
# shellcheck disable=SC1090
|
||||
source "${controlfolder}/mod_${CFW_NAME}.txt"
|
||||
fi
|
||||
|
||||
GAMEDIR="$SHDIR/gen1recomp-sbc"
|
||||
CONFDIR="$GAMEDIR/conf"
|
||||
mkdir -p "$CONFDIR"
|
||||
|
||||
cd "$GAMEDIR" || exit 1
|
||||
> "$GAMEDIR/log.txt" && exec > >(tee "$GAMEDIR/log.txt") 2>&1
|
||||
|
||||
export XDG_DATA_HOME="$CONFDIR"
|
||||
export XDG_CONFIG_HOME="$CONFDIR"
|
||||
export LD_LIBRARY_PATH="$GAMEDIR/libs.aarch64:${LD_LIBRARY_PATH:-}"
|
||||
export SDL_GAMECONTROLLERCONFIG="${sdl_controllerconfig:-}"
|
||||
# GLES is the common path on ARM SBC handhelds; firmware may override it.
|
||||
export LOVE_GRAPHICS_USE_OPENGLES="${LOVE_GRAPHICS_USE_OPENGLES:-1}"
|
||||
|
||||
$ESUDO chmod a+x ./bin/love.aarch64 2>/dev/null || chmod a+x ./bin/love.aarch64
|
||||
$ESUDO chmod 666 /dev/uinput 2>/dev/null || true
|
||||
|
||||
if [ -n "${GPTOKEYB:-}" ]; then
|
||||
$GPTOKEYB "love.aarch64" &
|
||||
fi
|
||||
if type pm_platform_helper >/dev/null 2>&1; then
|
||||
pm_platform_helper "$GAMEDIR/bin/love.aarch64"
|
||||
fi
|
||||
|
||||
./bin/love.aarch64 "$GAMEDIR/lovegame"
|
||||
|
||||
if type pm_finish >/dev/null 2>&1; then
|
||||
pm_finish
|
||||
else
|
||||
if [ -n "${ESUDO:-}" ]; then
|
||||
$ESUDO kill -9 $(pidof gptokeyb) 2>/dev/null || true
|
||||
else
|
||||
kill -9 $(pidof gptokeyb) 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
EOF
|
||||
chmod +x "$PORT_ROOT/$LAUNCHER_NAME"
|
||||
|
||||
# --------------------------------------------------------------- metadata
|
||||
cat > "$PORT_ROOT/port.json" <<EOF
|
||||
{
|
||||
"version": 2,
|
||||
"name": "gen1recomp-sbc.zip",
|
||||
"items": [
|
||||
"$LAUNCHER_NAME",
|
||||
"$PORT_DIR_NAME"
|
||||
],
|
||||
"items_opt": null,
|
||||
"attr": {
|
||||
"title": "gen1recomp-sbc",
|
||||
"desc": "Native LÖVE2D recreation of Pokemon Red and Blue. Supply your own legal US Red or Blue ROM.",
|
||||
"source": "https://github.com/bryanthaboi/gen1recomp/releases/tag/$SOURCE_TAG",
|
||||
"inst": "Requires a 64-bit Linux ARM handheld with PortMaster. Copy a canonical US Red or Blue .gb into gen1recomp-sbc/lovegame/, then launch and press Choose ROM.",
|
||||
"genres": ["adventure", "rpg"],
|
||||
"porter": ["gen1recomp-sbc"],
|
||||
"image": {},
|
||||
"rtr": true,
|
||||
"runtime": null,
|
||||
"reqs": [],
|
||||
"arch": ["aarch64"]
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
cat > "$PORT_ROOT/gameinfo.xml" <<EOF
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<gameList>
|
||||
<game>
|
||||
<path>./$LAUNCHER_NAME</path>
|
||||
<name>gen1recomp-sbc</name>
|
||||
<desc>Native LÖVE2D recreation of Pokemon Red and Blue. Requires your own legal US Red or Blue ROM.</desc>
|
||||
<releasedate>20250101T000000</releasedate>
|
||||
<developer>the bois club</developer>
|
||||
<publisher>the bois club</publisher>
|
||||
<genre>RPG</genre>
|
||||
</game>
|
||||
</gameList>
|
||||
EOF
|
||||
|
||||
cat > "$PORT_ROOT/README.md" <<'EOF'
|
||||
## gen1recomp-sbc (Linux ARM SBC / PortMaster)
|
||||
|
||||
Native LÖVE 11.5 aarch64 PortMaster port of gen1recomp for compatible Linux ARM SBC handhelds, including H700-class devices. This pack was built from source release **__SOURCE_TAG__**.
|
||||
|
||||
### Install
|
||||
|
||||
1. Install PortMaster for your handheld firmware.
|
||||
2. Unzip so `gen1recomp-sbc.sh` and the `gen1recomp-sbc/` folder are siblings in the device's PortMaster ports directory.
|
||||
3. Copy a legal US Pokémon Red or Blue `.gb` into `gen1recomp-sbc/lovegame/`.
|
||||
4. Refresh the launcher and launch **gen1recomp-sbc** from Ports.
|
||||
|
||||
### Controls
|
||||
|
||||
| Input | Action |
|
||||
|--|--|
|
||||
| D-pad | Move cursor |
|
||||
| A | Click |
|
||||
| L1 / R1 | Switch tabs |
|
||||
| Start / Select | Play or choose ROM |
|
||||
|
||||
Controls use the normal PortMaster / SDL pad map. Device-specific power/suspend behavior is supplied by the firmware and PortMaster runtime.
|
||||
|
||||
### First run
|
||||
|
||||
Put the `.gb` in `lovegame/`, then press **Choose ROM**. After import, the ROM-derived cache and saves stay beside the game (`portable.txt`).
|
||||
|
||||
### Thanks
|
||||
|
||||
LÖVE runtime binaries from [PortMaster](https://portmaster.games/). PortMaster device support and runtime integration are maintained by the PortMaster team.
|
||||
EOF
|
||||
sed -i.bak "s/__SOURCE_TAG__/$SOURCE_TAG/g" "$PORT_ROOT/README.md"
|
||||
rm -f "$PORT_ROOT/README.md.bak"
|
||||
|
||||
# --------------------------------------------------------------- zip
|
||||
ZIP_OUT="$DIST/$APP_NAME-$ARTIFACT_SUFFIX.zip"
|
||||
rm -f "$ZIP_OUT"
|
||||
say "packing $ZIP_OUT"
|
||||
(cd "$PORT_ROOT" && zip -q -9 -r "$ZIP_OUT" \
|
||||
"$LAUNCHER_NAME" "$PORT_DIR_NAME" port.json gameinfo.xml README.md)
|
||||
|
||||
say "done."
|
||||
say "artifact: $ZIP_OUT ($(du -h "$ZIP_OUT" | cut -f1))"
|
||||
say "copy into the device PortMaster ports folder, then drop your .gb into gen1recomp-sbc/lovegame/"
|
||||
@@ -1,336 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build a PortMaster-style aarch64 port of gen1recomp for Anbernic RG34XXSP
|
||||
# on Stock OS 64-bit MOD (H700; cbepx-me StockOS MOD / official V1.0.6 base
|
||||
# + PortMaster).
|
||||
#
|
||||
# The stock Anbernic firmware resolves ports relative to the launch script
|
||||
# (roms/PORTS/...), not PortMaster's /$directory/ports/... path. This pack
|
||||
# uses SHDIR-relative paths and bundles the LÖVE 11.5 aarch64 runtime so the
|
||||
# device does not need a separate love_11.5 runtime download on first launch.
|
||||
#
|
||||
# Usage:
|
||||
# ./build-rg34xxsp.sh [--version X.Y.Z]
|
||||
#
|
||||
# Output:
|
||||
# dist/rg34xxsp/gen1recomp-rg34xxsp-stockos64-mod.zip
|
||||
#
|
||||
# Install on device:
|
||||
# 1. Flash / run Stock OS 64-bit MOD with PortMaster installed.
|
||||
# 2. Unzip into the SD card's roms/PORTS/ folder so you have:
|
||||
# roms/PORTS/Gen1recomp.sh
|
||||
# roms/PORTS/gen1recomp/...
|
||||
# 3. Copy a legal US Red or Blue .gb into roms/PORTS/gen1recomp/lovegame/
|
||||
# 4. Launch "Gen1recomp" from the Ports list; press Choose ROM (scans that
|
||||
# folder when zenity is missing).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||
HERE="$ROOT/.bazinga"
|
||||
CACHE="$HERE/cache/rg34xxsp"
|
||||
WORK="$HERE/work/rg34xxsp"
|
||||
DIST="$ROOT/dist/rg34xxsp"
|
||||
|
||||
APP_NAME="gen1recomp"
|
||||
# Artifact suffix matches the CFW this port targets (Anbernic H700 Stock OS
|
||||
# 64-bit MOD for RG34XXSP). Release uploads stage it as
|
||||
# gen1recomp-<ver>-rg34xxsp-stockos64-mod.zip.
|
||||
ARTIFACT_SUFFIX="rg34xxsp-stockos64-mod"
|
||||
PORT_DIR_NAME="gen1recomp"
|
||||
LAUNCHER_NAME="Gen1recomp.sh"
|
||||
LOVE_VERSION="11.5"
|
||||
VERSION="$(git -C "$ROOT" rev-parse --short HEAD 2>/dev/null || echo dev)"
|
||||
|
||||
# Official PortMaster LÖVE 11.5 aarch64 runtime (small love stub + liblove).
|
||||
PM_RUNTIME_BASE="https://raw.githubusercontent.com/PortsMaster/PortMaster-GUI/main/PortMaster/runtimes/love_${LOVE_VERSION}"
|
||||
|
||||
say() { printf '\033[1;32m==>\033[0m %s\n' "$*"; }
|
||||
warn() { printf '\033[1;33mwarn:\033[0m %s\n' "$*" >&2; }
|
||||
fail() { printf '\033[1;31merror:\033[0m %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--version) VERSION="$2"; shift ;;
|
||||
-h|--help)
|
||||
sed -n '2,24p' "$0"
|
||||
exit 0
|
||||
;;
|
||||
*) fail "unknown argument: $1" ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
command -v curl >/dev/null || fail "curl is required"
|
||||
command -v zip >/dev/null || fail "zip is required"
|
||||
command -v unzip >/dev/null || fail "unzip is required"
|
||||
|
||||
mkdir -p "$CACHE" "$WORK" "$DIST"
|
||||
|
||||
download() {
|
||||
local url="$1" dest="$2"
|
||||
if [ -f "$dest" ] && [ -s "$dest" ]; then
|
||||
return 0
|
||||
fi
|
||||
say "downloading $(basename "$dest")"
|
||||
curl -fL --progress-bar "$url" -o "$dest.tmp" \
|
||||
|| fail "download failed: $url"
|
||||
mv "$dest.tmp" "$dest"
|
||||
}
|
||||
|
||||
# --------------------------------------------------------------- game tree
|
||||
# Unpacked directory (not a .love zip) so the player can drop a .gb next to
|
||||
# main.lua and RomImporter can scan it without zenity/kdialog.
|
||||
say "staging lovegame/"
|
||||
GAME_SRC="$WORK/lovegame"
|
||||
rm -rf "$GAME_SRC"
|
||||
mkdir -p "$GAME_SRC"
|
||||
|
||||
# Same payload as scripts/build.sh's game.love — never ship ROM-derived cache.
|
||||
# tools/save-editor is part of that payload: the launcher's Edit button on a
|
||||
# save row opens it in-process (main.lua).
|
||||
(cd "$ROOT" && zip -q -9 -r "$WORK/game-payload.zip" \
|
||||
main.lua conf.lua src libs data assets tools/save-editor \
|
||||
tools/rom_manifest.json tools/rom_manifest_blue.json \
|
||||
tools/rom_manifest_yellow.json tools/rom_manifest_gold.json \
|
||||
-x '*.DS_Store' 'data/generated/*' 'assets/generated/*')
|
||||
payload_list="$(unzip -Z1 "$WORK/game-payload.zip")"
|
||||
printf '%s\n' "$payload_list" \
|
||||
| grep -Eq '^(data|assets)/generated/[^/]+|^(data|assets)/generated/.+/' \
|
||||
&& fail "payload unexpectedly contains generated ROM data"
|
||||
printf '%s\n' "$payload_list" | grep -qxF "tools/rom_manifest_gold.json" \
|
||||
|| fail "payload is missing tools/rom_manifest_gold.json"
|
||||
unzip -q "$WORK/game-payload.zip" -d "$GAME_SRC"
|
||||
rm -f "$WORK/game-payload.zip"
|
||||
|
||||
# Stamp release version into the staged tree only (never the working tree).
|
||||
if printf '%s' "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||||
say "stamping engine version $VERSION"
|
||||
sed -E "s/(engine[[:space:]]*=[[:space:]]*\")[^\"]*(\")/\1$VERSION\2/" \
|
||||
"$ROOT/src/core/Version.lua" > "$GAME_SRC/src/core/Version.lua"
|
||||
version_re="$(printf '%s' "$VERSION" | sed 's/\./\\./g')"
|
||||
grep -Eq "engine[[:space:]]*=[[:space:]]*\"$version_re\"" \
|
||||
"$GAME_SRC/src/core/Version.lua" \
|
||||
|| fail "version stamp failed"
|
||||
else
|
||||
say "version '$VERSION' is not X.Y.Z — shipping default engine (no stamp)"
|
||||
fi
|
||||
|
||||
# Portable marker: saves + ROM cache live next to the game on the SD card.
|
||||
: > "$GAME_SRC/portable.txt"
|
||||
|
||||
# --------------------------------------------------------------- love runtime
|
||||
say "fetching LÖVE $LOVE_VERSION aarch64 runtime"
|
||||
LOVE_BIN="$CACHE/love.aarch64"
|
||||
LOVE_LIB="$CACHE/liblove-11.5.so"
|
||||
LUAJIT_LIB="$CACHE/libluajit-5.1.so.2"
|
||||
MODPLUG_LIB="$CACHE/libmodplug.so.1"
|
||||
OGG_LIB="$CACHE/libogg.so.0"
|
||||
|
||||
download "$PM_RUNTIME_BASE/love.aarch64" "$LOVE_BIN"
|
||||
download "$PM_RUNTIME_BASE/libs.aarch64/liblove-11.5.so" "$LOVE_LIB"
|
||||
download "$PM_RUNTIME_BASE/libs.aarch64/libluajit-5.1.so.2" "$LUAJIT_LIB"
|
||||
download "$PM_RUNTIME_BASE/libs.aarch64/libmodplug.so.1" "$MODPLUG_LIB"
|
||||
download "$PM_RUNTIME_BASE/libs.aarch64/libogg.so.0" "$OGG_LIB"
|
||||
|
||||
# Sanity: love stub must be an aarch64 ELF.
|
||||
file "$LOVE_BIN" | grep -qi 'aarch64\|ARM aarch64' \
|
||||
|| fail "love.aarch64 does not look like an aarch64 ELF (got: $(file "$LOVE_BIN"))"
|
||||
|
||||
# --------------------------------------------------------------- port tree
|
||||
say "assembling port package"
|
||||
PORT_ROOT="$WORK/port"
|
||||
rm -rf "$PORT_ROOT"
|
||||
mkdir -p "$PORT_ROOT/$PORT_DIR_NAME/bin" \
|
||||
"$PORT_ROOT/$PORT_DIR_NAME/libs.aarch64" \
|
||||
"$PORT_ROOT/$PORT_DIR_NAME/licenses" \
|
||||
"$PORT_ROOT/$PORT_DIR_NAME/conf"
|
||||
|
||||
cp -R "$GAME_SRC" "$PORT_ROOT/$PORT_DIR_NAME/lovegame"
|
||||
cp "$LOVE_BIN" "$PORT_ROOT/$PORT_DIR_NAME/bin/love.aarch64"
|
||||
chmod +x "$PORT_ROOT/$PORT_DIR_NAME/bin/love.aarch64"
|
||||
cp "$LOVE_LIB" "$LUAJIT_LIB" "$MODPLUG_LIB" "$OGG_LIB" \
|
||||
"$PORT_ROOT/$PORT_DIR_NAME/libs.aarch64/"
|
||||
|
||||
# Drop a short license pointer for the bundled LÖVE bits.
|
||||
cat > "$PORT_ROOT/$PORT_DIR_NAME/licenses/LICENSE.love2d.txt" <<'EOF'
|
||||
This port bundles the LÖVE 11.5 aarch64 runtime from PortMaster
|
||||
(https://github.com/PortsMaster/PortMaster-GUI). LÖVE is zlib-licensed;
|
||||
see https://love2d.org/ for full terms.
|
||||
EOF
|
||||
|
||||
# --------------------------------------------------------------- launcher
|
||||
# Stock OS fix: resolve GAMEDIR from this script's directory (SHDIR), not
|
||||
# PortMaster's $directory variable — Anbernic uses roms/PORTS with different
|
||||
# casing and mount points (mmc / sdcard / TF1 / TF2).
|
||||
cat > "$PORT_ROOT/$LAUNCHER_NAME" <<'EOF'
|
||||
#!/bin/bash
|
||||
# gen1recomp — Anbernic RG34XXSP stock OS / PortMaster launcher
|
||||
# Uses SHDIR-relative paths so stock firmware finds the game folder.
|
||||
|
||||
export HOME="${HOME:-/root}"
|
||||
XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}"
|
||||
|
||||
if [ -d "/opt/system/Tools/PortMaster/" ]; then
|
||||
controlfolder="/opt/system/Tools/PortMaster"
|
||||
elif [ -d "/opt/tools/PortMaster/" ]; then
|
||||
controlfolder="/opt/tools/PortMaster"
|
||||
elif [ -d "$XDG_DATA_HOME/PortMaster/" ]; then
|
||||
controlfolder="$XDG_DATA_HOME/PortMaster"
|
||||
elif [ -d "/mnt/mmc/Roms/PORTS/PortMaster" ]; then
|
||||
controlfolder="/mnt/mmc/Roms/PORTS/PortMaster"
|
||||
elif [ -d "/mnt/sdcard/Roms/PORTS/PortMaster" ]; then
|
||||
controlfolder="/mnt/sdcard/Roms/PORTS/PortMaster"
|
||||
elif [ -d "/roms/ports/PortMaster" ]; then
|
||||
controlfolder="/roms/ports/PortMaster"
|
||||
else
|
||||
controlfolder="/roms/PORTS/PortMaster"
|
||||
fi
|
||||
|
||||
SHDIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
# shellcheck disable=SC1090
|
||||
source "$controlfolder/control.txt"
|
||||
get_controls
|
||||
[ -f "${controlfolder}/mod_${CFW_NAME}.txt" ] && source "${controlfolder}/mod_${CFW_NAME}.txt"
|
||||
|
||||
GAMEDIR="$SHDIR/gen1recomp"
|
||||
CONFDIR="$GAMEDIR/conf"
|
||||
mkdir -p "$CONFDIR"
|
||||
|
||||
cd "$GAMEDIR" || exit 1
|
||||
> "$GAMEDIR/log.txt" && exec > >(tee "$GAMEDIR/log.txt") 2>&1
|
||||
|
||||
export XDG_DATA_HOME="$CONFDIR"
|
||||
export XDG_CONFIG_HOME="$CONFDIR"
|
||||
export LD_LIBRARY_PATH="$GAMEDIR/libs.aarch64:${LD_LIBRARY_PATH:-}"
|
||||
export SDL_GAMECONTROLLERCONFIG="${sdl_controllerconfig:-}"
|
||||
# Mali / H700: prefer GLES where available
|
||||
export LOVE_GRAPHICS_USE_OPENGLES="${LOVE_GRAPHICS_USE_OPENGLES:-1}"
|
||||
# Same GPU class as a phone, but getOS() here says "Linux", so the Android
|
||||
# gate (issue #136) would not fire on its own: refuse GBC FX explicitly.
|
||||
# Hides the OPTIONS row, pins the level to OFF, and heals a level already
|
||||
# persisted in options.lua.
|
||||
export POKEPORT_GBCFX="${POKEPORT_GBCFX:-0}"
|
||||
|
||||
$ESUDO chmod a+x ./bin/love.aarch64 2>/dev/null || chmod a+x ./bin/love.aarch64
|
||||
$ESUDO chmod 666 /dev/uinput 2>/dev/null || true
|
||||
|
||||
if [ -n "${GPTOKEYB:-}" ]; then
|
||||
$GPTOKEYB "love.aarch64" &
|
||||
fi
|
||||
if type pm_platform_helper >/dev/null 2>&1; then
|
||||
pm_platform_helper "$GAMEDIR/bin/love.aarch64"
|
||||
fi
|
||||
|
||||
./bin/love.aarch64 "$GAMEDIR/lovegame"
|
||||
|
||||
if type pm_finish >/dev/null 2>&1; then
|
||||
pm_finish
|
||||
else
|
||||
if [ -n "${ESUDO:-}" ]; then
|
||||
$ESUDO kill -9 $(pidof gptokeyb) 2>/dev/null || true
|
||||
else
|
||||
kill -9 $(pidof gptokeyb) 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
EOF
|
||||
chmod +x "$PORT_ROOT/$LAUNCHER_NAME"
|
||||
|
||||
# --------------------------------------------------------------- metadata
|
||||
cat > "$PORT_ROOT/port.json" <<EOF
|
||||
{
|
||||
"version": 2,
|
||||
"name": "gen1recomp.zip",
|
||||
"items": [
|
||||
"$LAUNCHER_NAME",
|
||||
"$PORT_DIR_NAME"
|
||||
],
|
||||
"items_opt": null,
|
||||
"attr": {
|
||||
"title": "gen1recomp",
|
||||
"desc": "Native LÖVE2D recreation of Pokemon Red and Blue. Supply your own legal US Red or Blue ROM.",
|
||||
"inst": "Requires Anbernic RG34XXSP Stock OS 64-bit MOD with PortMaster. Copy a canonical US Red or Blue .gb into gen1recomp/lovegame/, then launch and press Choose ROM.",
|
||||
"genres": ["adventure", "rpg"],
|
||||
"porter": ["gen1recomp"],
|
||||
"image": {},
|
||||
"rtr": true,
|
||||
"runtime": null,
|
||||
"reqs": [],
|
||||
"arch": ["aarch64"]
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
cat > "$PORT_ROOT/gameinfo.xml" <<EOF
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<gameList>
|
||||
<game>
|
||||
<path>./$LAUNCHER_NAME</path>
|
||||
<name>gen1recomp</name>
|
||||
<desc>Native LÖVE2D recreation of Pokemon Red and Blue. Requires your own legal US Red or Blue ROM.</desc>
|
||||
<releasedate>20250101T000000</releasedate>
|
||||
<developer>the bois club</developer>
|
||||
<publisher>the bois club</publisher>
|
||||
<genre>RPG</genre>
|
||||
</game>
|
||||
</gameList>
|
||||
EOF
|
||||
|
||||
cat > "$PORT_ROOT/README.md" <<'EOF'
|
||||
## gen1recomp (RG34XXSP / Stock OS 64-bit MOD)
|
||||
|
||||
Native LÖVE 11.5 port of gen1recomp for Anbernic RG34XXSP on
|
||||
**Stock OS 64-bit MOD** (H700, PortMaster).
|
||||
|
||||
### Install
|
||||
|
||||
1. Use **Stock OS 64-bit MOD** with PortMaster installed (TF1).
|
||||
2. Unzip so `Gen1recomp.sh` and the `gen1recomp/` folder sit in `roms/PORTS/`.
|
||||
3. Copy a legal US Pokemon Red or Blue `.gb` into `gen1recomp/lovegame/`.
|
||||
4. Refresh Ports / restart EmulationStation and launch **Gen1recomp**.
|
||||
|
||||
### Controls (launcher)
|
||||
|
||||
| Input | Action |
|
||||
|--|--|
|
||||
| D-pad / left stick | Move cursor |
|
||||
| A | Click |
|
||||
| L1 / R1 | Switch tabs |
|
||||
| Right stick | Scroll lists |
|
||||
| Start / Select | Play or Choose ROM |
|
||||
|
||||
In-game controls use the normal PortMaster / SDL pad map (rebind under OPTIONS → CONTROLS).
|
||||
|
||||
### Display options
|
||||
|
||||
GBC FX is disabled on this device (the H700's Mali GPU compiles that present
|
||||
pass and then shows a black frame), so the OPTIONS row is hidden. COLORS,
|
||||
TILT, ZOOM, VOID FILL and MAX FPS all work. To try it anyway, launch with
|
||||
`POKEPORT_GBCFX=1`.
|
||||
|
||||
### First run
|
||||
|
||||
Stock OS has no zenity file picker. Put the `.gb` in `lovegame/`, then press
|
||||
**Choose ROM** — the game scans that folder. After import, the ROM-derived
|
||||
cache and saves stay beside the game (`portable.txt`).
|
||||
|
||||
Canonical SHA-1 (1 MiB US carts only):
|
||||
|
||||
- Red: `ea9bcae617fdf159b045185467ae58b2e4a48b9a`
|
||||
- Blue: `d7037c83e1ae5b39bde3c30787637ba1d4c48ce2`
|
||||
|
||||
### Thanks
|
||||
|
||||
LÖVE runtime binaries from [PortMaster](https://portmaster.games/).
|
||||
Stock OS 64-bit MOD: [cbepx-me](https://github.com/cbepx-me/Anbernic-H700-RG-xx-StockOS-Modification).
|
||||
EOF
|
||||
|
||||
# --------------------------------------------------------------- zip
|
||||
ZIP_OUT="$DIST/$APP_NAME-$ARTIFACT_SUFFIX.zip"
|
||||
rm -f "$ZIP_OUT" "$DIST/$APP_NAME-rg34xxsp.zip"
|
||||
say "packing $ZIP_OUT"
|
||||
(cd "$PORT_ROOT" && zip -q -9 -r "$ZIP_OUT" \
|
||||
"$LAUNCHER_NAME" "$PORT_DIR_NAME" port.json gameinfo.xml README.md)
|
||||
|
||||
say "done."
|
||||
say "artifact: $ZIP_OUT ($(du -h "$ZIP_OUT" | cut -f1))"
|
||||
say "copy onto the RG34XXSP SD card under roms/PORTS/ (Stock OS 64-bit MOD), then drop your .gb into gen1recomp/lovegame/"
|
||||
@@ -1,41 +1,19 @@
|
||||
function love.conf(t)
|
||||
-- PhysFS ignores symlinks unless told otherwise, so a mod dev-linked into
|
||||
-- mods/ (ln -s, matching the mklink /J workflow on Windows) is invisible
|
||||
-- to love.filesystem.getDirectoryItems without this.
|
||||
love.filesystem.setSymlinksEnabled(true)
|
||||
|
||||
local editor = os.getenv("POKEPORT_EDITOR") == "1"
|
||||
local developer = os.getenv("POKEPORT_DEV") == "1"
|
||||
local companion = nil
|
||||
if arg then
|
||||
for _, a in ipairs(arg) do
|
||||
if a == "--editor" then editor = true end
|
||||
if a == "--developer" then developer = true end
|
||||
local port, token = a:match("^%-%-display%-companion=(%d+),([%w]+)$")
|
||||
if port then companion = { port = tonumber(port), token = token } end
|
||||
end
|
||||
end
|
||||
-- main.lua runs in the same Lua state right after conf.lua; stash the
|
||||
-- decision in a global so it doesn't need to reparse `arg`.
|
||||
_G.POKEPORT_EDITOR_MODE = editor
|
||||
_G.POKEPORT_DEV_MODE = developer
|
||||
_G.POKEPORT_DISPLAY_COMPANION = companion
|
||||
|
||||
if companion then
|
||||
t.identity = "pokemon-love2d-companion"
|
||||
t.window.title = "gen1recomp Secondary Display"
|
||||
t.window.width = 640
|
||||
t.window.height = 576
|
||||
t.window.minwidth = 160
|
||||
t.window.minheight = 144
|
||||
t.window.resizable = true
|
||||
elseif editor then
|
||||
-- Same identity as the game, deliberately: the editor edits the game's
|
||||
-- saves and reads the game's ROM cache, both of which live under this
|
||||
-- folder. A private editor identity would point love.filesystem at an
|
||||
-- empty directory in a packaged build, so `--editor` could not find
|
||||
-- data/generated at all (SaveIO.defaultPath already assumed this name).
|
||||
t.identity = os.getenv("POKEPORT_IDENTITY") or "pokemon-love2d"
|
||||
if editor then
|
||||
t.identity = os.getenv("POKEPORT_IDENTITY") or "pokemon-love2d-editor"
|
||||
t.window.title = "Pokemon Save Editor"
|
||||
t.window.width = 1280
|
||||
t.window.height = 800
|
||||
@@ -52,52 +30,25 @@ function love.conf(t)
|
||||
-- starting size, not the game's resolution.
|
||||
t.window.width = 1024
|
||||
t.window.height = 768
|
||||
-- Floor for the resizable desktop window. The launcher's single-column
|
||||
-- layout is laid out against ~420 logical px of content, and the game
|
||||
-- canvas letterboxes fine below that, so this only stops a drag that would
|
||||
-- squeeze the cards past the point where their buttons still read. Kept
|
||||
-- well under the smallest supported desktop display; mobile ignores it
|
||||
-- (fullscreen), so the handheld ports are unaffected.
|
||||
t.window.minwidth = 480
|
||||
t.window.minheight = 360
|
||||
end
|
||||
t.version = love._os == "iOS" and "12.0" or "11.5"
|
||||
t.version = "11.5"
|
||||
t.window.vsync = 1
|
||||
t.modules.audio = not companion
|
||||
t.modules.joystick = not companion
|
||||
t.modules.joystick = true
|
||||
t.modules.physics = false
|
||||
|
||||
-- love.system is not loaded during love.conf; love._os is set by the
|
||||
-- engine before conf runs (LÖVE 11.x / 11.5).
|
||||
local osName = love._os
|
||||
local mobile = osName == "Android" or osName == "iOS"
|
||||
local nx = osName == "NX"
|
||||
if nx then
|
||||
-- Switch (love-nx): hint handheld 720p. SDL auto-switches portable↔dock
|
||||
-- (720p↔1080p) only when the window is resizable and not exclusive
|
||||
-- fullscreen; NxDisplay.sync also applies the size on boot and dock change.
|
||||
t.window.width = 1280
|
||||
t.window.height = 720
|
||||
t.window.fullscreen = false
|
||||
t.window.resizable = true
|
||||
t.window.highdpi = false
|
||||
elseif mobile then
|
||||
if mobile then
|
||||
-- resizable is what unlocks orientation. SDL's Android backend, given no
|
||||
-- SDL_HINT_ORIENTATIONS (LÖVE sets none), calls setRequestedOrientation
|
||||
-- at window creation -- FULL_SENSOR when the window is resizable (rotates
|
||||
-- freely to portrait or landscape), otherwise locked to the window's w/h
|
||||
-- aspect. So a non-resizable tall window forced portrait; resizable lets
|
||||
-- the game follow the device. The renderer letterboxes the 160x144
|
||||
-- viewport into whatever size results, and the on-screen touch controls
|
||||
-- re-lay themselves out from the new window size, so both orientations
|
||||
-- just work. FULL_SENSOR ignores the device's rotation lock, so
|
||||
-- GameActivity.setOrientationBis remaps it to FULL_USER after SDL has
|
||||
-- run: same orientations allowed, but auto-rotate being off now wins.
|
||||
-- A persisted ORIENTATION lock (#592) overrides all of this after boot:
|
||||
-- src/core/Orientation.lua sets SDL_HINT_ORIENTATIONS over the FFI and
|
||||
-- re-triggers the request, from main.lua for the launcher and from
|
||||
-- Game:applyOptions in game.
|
||||
-- iOS follows the Info.plist orientations
|
||||
-- viewport into whatever size results, and touch input is gesture-based,
|
||||
-- so both orientations just work. iOS follows the Info.plist orientations
|
||||
-- (see mobile/ios/overlays/love-ios.plist, now portrait + landscape).
|
||||
t.window.resizable = true
|
||||
-- Starting size is a tall portrait hint; the OS resizes to the real
|
||||
@@ -113,14 +64,6 @@ function love.conf(t)
|
||||
-- player to copy their ROM there instead of needing a native file
|
||||
-- picker (LOVE 11.5 on Android has none -- see src/import/RomImporter.lua).
|
||||
t.externalstorage = osName == "Android"
|
||||
-- LOVE 11.x exposes the phone's accelerometer as a 3-axis joystick by
|
||||
-- default. Gravity keeps one axis pinned near +/-1.0 whenever the phone
|
||||
-- is held upright, so it streams past-deadzone joystickaxis events that
|
||||
-- both hid the touch overlay every instant (it reads as "a controller is
|
||||
-- in use") and, via the generic-joystick axis mapping (#459), walked the
|
||||
-- player around by tilt. Nothing in the game reads the accelerometer,
|
||||
-- so drop the device entirely (#468).
|
||||
t.accelerometerjoystick = false
|
||||
else
|
||||
t.window.resizable = true
|
||||
end
|
||||
|
||||
@@ -1683,172 +1683,158 @@ return {
|
||||
{ 0, 0, 0 },
|
||||
},
|
||||
},
|
||||
-- Species -> palette NAME, taken from pokered-gbc data/pokemon/palettes.asm
|
||||
-- ELSE branch -- the one that is NOT gated on GEN_2_GRAPHICS.
|
||||
--
|
||||
-- That file has two tables. The IF GEN_2_GRAPHICS branch assigns a
|
||||
-- PER-SPECIES palette (PAL_BULBASAUR, PAL_SQUIRTLE, ...) authored for Gen 2
|
||||
-- sprite art; the ELSE branch keeps Gen 1's own assignments (GREENMON,
|
||||
-- CYANMON, ...). This port extracts Gen 1 pics from the ROM, so the Gen 1
|
||||
-- branch is the matching one. The per-species table was imported here by
|
||||
-- mistake, which pointed every mon at colours shaded for different art:
|
||||
-- Bulbasaur wore PAL_BULBASAUR's red-orange over a sprite that has no red
|
||||
-- on it, and Squirtle wore PAL_SQUIRTLE's shell brown on his head.
|
||||
--
|
||||
-- The palette VALUES below are still pokered-gbc's, so ADVANCED keeps its
|
||||
-- richer colours -- only which palette each species points at changed.
|
||||
pokemon = {
|
||||
ABRA = "YELLOWMON",
|
||||
AERODACTYL = "GRAYMON",
|
||||
ALAKAZAM = "YELLOWMON",
|
||||
ARBOK = "PURPLEMON",
|
||||
ARCANINE = "REDMON",
|
||||
ARTICUNO = "BLUEMON",
|
||||
BEEDRILL = "YELLOWMON",
|
||||
BELLSPROUT = "GREENMON",
|
||||
BLASTOISE = "CYANMON",
|
||||
BULBASAUR = "GREENMON",
|
||||
BUTTERFREE = "CYANMON",
|
||||
CATERPIE = "GREENMON",
|
||||
CHANSEY = "PINKMON",
|
||||
CHARIZARD = "REDMON",
|
||||
CHARMANDER = "REDMON",
|
||||
CHARMELEON = "REDMON",
|
||||
CLEFABLE = "PINKMON",
|
||||
CLEFAIRY = "PINKMON",
|
||||
CLOYSTER = "GRAYMON",
|
||||
CUBONE = "GRAYMON",
|
||||
DEWGONG = "BLUEMON",
|
||||
DIGLETT = "BROWNMON",
|
||||
DITTO = "GRAYMON",
|
||||
DODRIO = "BROWNMON",
|
||||
DODUO = "BROWNMON",
|
||||
DRAGONAIR = "BLUEMON",
|
||||
DRAGONITE = "BROWNMON",
|
||||
DRATINI = "GRAYMON",
|
||||
DROWZEE = "YELLOWMON",
|
||||
DUGTRIO = "BROWNMON",
|
||||
EEVEE = "GRAYMON",
|
||||
EKANS = "PURPLEMON",
|
||||
ELECTABUZZ = "YELLOWMON",
|
||||
ELECTRODE = "YELLOWMON",
|
||||
EXEGGCUTE = "PINKMON",
|
||||
EXEGGUTOR = "GREENMON",
|
||||
FARFETCHD = "BROWNMON",
|
||||
FEAROW = "BROWNMON",
|
||||
FLAREON = "REDMON",
|
||||
GASTLY = "PURPLEMON",
|
||||
GENGAR = "PURPLEMON",
|
||||
GEODUDE = "GRAYMON",
|
||||
GLOOM = "REDMON",
|
||||
GOLBAT = "BLUEMON",
|
||||
GOLDEEN = "REDMON",
|
||||
GOLDUCK = "CYANMON",
|
||||
GOLEM = "GRAYMON",
|
||||
GRAVELER = "GRAYMON",
|
||||
GRIMER = "PURPLEMON",
|
||||
GROWLITHE = "BROWNMON",
|
||||
GYARADOS = "BLUEMON",
|
||||
HAUNTER = "PURPLEMON",
|
||||
HITMONCHAN = "BROWNMON",
|
||||
HITMONLEE = "BROWNMON",
|
||||
HORSEA = "CYANMON",
|
||||
HYPNO = "YELLOWMON",
|
||||
IVYSAUR = "GREENMON",
|
||||
JIGGLYPUFF = "PINKMON",
|
||||
JOLTEON = "YELLOWMON",
|
||||
JYNX = "MEWMON",
|
||||
KABUTO = "BROWNMON",
|
||||
KABUTOPS = "BROWNMON",
|
||||
KADABRA = "YELLOWMON",
|
||||
KAKUNA = "YELLOWMON",
|
||||
KANGASKHAN = "BROWNMON",
|
||||
KINGLER = "REDMON",
|
||||
KOFFING = "PURPLEMON",
|
||||
KRABBY = "REDMON",
|
||||
LAPRAS = "CYANMON",
|
||||
LICKITUNG = "PINKMON",
|
||||
MACHAMP = "GRAYMON",
|
||||
MACHOKE = "GRAYMON",
|
||||
MACHOP = "GRAYMON",
|
||||
MAGIKARP = "REDMON",
|
||||
MAGMAR = "REDMON",
|
||||
MAGNEMITE = "GRAYMON",
|
||||
MAGNETON = "GRAYMON",
|
||||
MANKEY = "BROWNMON",
|
||||
MAROWAK = "GRAYMON",
|
||||
MEOWTH = "YELLOWMON",
|
||||
METAPOD = "GREENMON",
|
||||
MEW = "MEWMON",
|
||||
MEWTWO = "MEWMON",
|
||||
MOLTRES = "REDMON",
|
||||
MR_MIME = "PINKMON",
|
||||
MUK = "PURPLEMON",
|
||||
NIDOKING = "PURPLEMON",
|
||||
NIDOQUEEN = "BLUEMON",
|
||||
NIDORAN_F = "BLUEMON",
|
||||
NIDORAN_M = "PURPLEMON",
|
||||
NIDORINA = "BLUEMON",
|
||||
NIDORINO = "PURPLEMON",
|
||||
NINETALES = "YELLOWMON",
|
||||
ODDISH = "GREENMON",
|
||||
OMANYTE = "BLUEMON",
|
||||
OMASTAR = "BLUEMON",
|
||||
ONIX = "GRAYMON",
|
||||
PARAS = "REDMON",
|
||||
PARASECT = "REDMON",
|
||||
PERSIAN = "YELLOWMON",
|
||||
PIDGEOT = "BROWNMON",
|
||||
PIDGEOTTO = "BROWNMON",
|
||||
PIDGEY = "BROWNMON",
|
||||
PIKACHU = "YELLOWMON",
|
||||
PINSIR = "BROWNMON",
|
||||
POLIWAG = "BLUEMON",
|
||||
POLIWHIRL = "BLUEMON",
|
||||
POLIWRATH = "BLUEMON",
|
||||
PONYTA = "REDMON",
|
||||
PORYGON = "GRAYMON",
|
||||
PRIMEAPE = "BROWNMON",
|
||||
PSYDUCK = "YELLOWMON",
|
||||
RAICHU = "YELLOWMON",
|
||||
RAPIDASH = "REDMON",
|
||||
RATICATE = "GRAYMON",
|
||||
RATTATA = "GRAYMON",
|
||||
RHYDON = "GRAYMON",
|
||||
RHYHORN = "GRAYMON",
|
||||
SANDSHREW = "BROWNMON",
|
||||
SANDSLASH = "BROWNMON",
|
||||
SCYTHER = "GREENMON",
|
||||
SEADRA = "CYANMON",
|
||||
SEAKING = "REDMON",
|
||||
SEEL = "BLUEMON",
|
||||
SHELLDER = "GRAYMON",
|
||||
SLOWBRO = "PINKMON",
|
||||
SLOWPOKE = "PINKMON",
|
||||
SNORLAX = "PINKMON",
|
||||
SPEAROW = "BROWNMON",
|
||||
SQUIRTLE = "CYANMON",
|
||||
STARMIE = "GRAYMON",
|
||||
STARYU = "REDMON",
|
||||
TANGELA = "BLUEMON",
|
||||
TAUROS = "GRAYMON",
|
||||
TENTACOOL = "CYANMON",
|
||||
TENTACRUEL = "CYANMON",
|
||||
VAPOREON = "CYANMON",
|
||||
VENOMOTH = "PURPLEMON",
|
||||
VENONAT = "PURPLEMON",
|
||||
VENUSAUR = "GREENMON",
|
||||
VICTREEBEL = "GREENMON",
|
||||
VILEPLUME = "REDMON",
|
||||
VOLTORB = "YELLOWMON",
|
||||
VULPIX = "REDMON",
|
||||
WARTORTLE = "CYANMON",
|
||||
WEEDLE = "YELLOWMON",
|
||||
WEEPINBELL = "GREENMON",
|
||||
WEEZING = "PURPLEMON",
|
||||
WIGGLYTUFF = "PINKMON",
|
||||
ZAPDOS = "YELLOWMON",
|
||||
ZUBAT = "BLUEMON",
|
||||
ABRA = "ABRA",
|
||||
AERODACTYL = "AERODACTYL",
|
||||
ALAKAZAM = "ALAKAZAM",
|
||||
ARBOK = "ARBOK",
|
||||
ARCANINE = "ARCANINE",
|
||||
ARTICUNO = "ARTICUNO",
|
||||
BEEDRILL = "BEEDRILL",
|
||||
BELLSPROUT = "BELLSPROUT",
|
||||
BLASTOISE = "BLASTOISE",
|
||||
BULBASAUR = "BULBASAUR",
|
||||
BUTTERFREE = "BUTTERFREE",
|
||||
CATERPIE = "CATERPIE",
|
||||
CHANSEY = "CHANSEY",
|
||||
CHARIZARD = "CHARIZARD",
|
||||
CHARMANDER = "CHARMANDER",
|
||||
CHARMELEON = "CHARMELEON",
|
||||
CLEFABLE = "CLEFABLE",
|
||||
CLEFAIRY = "CLEFAIRY",
|
||||
CLOYSTER = "CLOYSTER",
|
||||
CUBONE = "CUBONE",
|
||||
DEWGONG = "DEWGONG",
|
||||
DIGLETT = "DIGLETT",
|
||||
DITTO = "DITTO",
|
||||
DODRIO = "DODRIO",
|
||||
DODUO = "DODUO",
|
||||
DRAGONAIR = "DRAGONAIR",
|
||||
DRAGONITE = "DRAGONITE",
|
||||
DRATINI = "DRATINI",
|
||||
DROWZEE = "DROWZEE",
|
||||
DUGTRIO = "DUGTRIO",
|
||||
EEVEE = "EEVEE",
|
||||
EKANS = "EKANS",
|
||||
ELECTABUZZ = "ELECTABUZZ",
|
||||
ELECTRODE = "ELECTRODE",
|
||||
EXEGGCUTE = "EXEGGCUTE",
|
||||
EXEGGUTOR = "EXEGGUTOR",
|
||||
FARFETCHD = "FARFETCH_D",
|
||||
FEAROW = "FEAROW",
|
||||
FLAREON = "FLAREON",
|
||||
GASTLY = "GASTLY",
|
||||
GENGAR = "GENGAR",
|
||||
GEODUDE = "GEODUDE",
|
||||
GLOOM = "GLOOM",
|
||||
GOLBAT = "GOLBAT",
|
||||
GOLDEEN = "GOLDEEN",
|
||||
GOLDUCK = "GOLDUCK",
|
||||
GOLEM = "GOLEM",
|
||||
GRAVELER = "GRAVELER",
|
||||
GRIMER = "GRIMER",
|
||||
GROWLITHE = "GROWLITHE",
|
||||
GYARADOS = "GYARADOS",
|
||||
HAUNTER = "HAUNTER",
|
||||
HITMONCHAN = "HITMONCHAN",
|
||||
HITMONLEE = "HITMONLEE",
|
||||
HORSEA = "HORSEA",
|
||||
HYPNO = "HYPNO",
|
||||
IVYSAUR = "IVYSAUR",
|
||||
JIGGLYPUFF = "JIGGLYPUFF",
|
||||
JOLTEON = "JOLTEON",
|
||||
JYNX = "JYNX",
|
||||
KABUTO = "KABUTO",
|
||||
KABUTOPS = "KABUTOPS",
|
||||
KADABRA = "KADABRA",
|
||||
KAKUNA = "KAKUNA",
|
||||
KANGASKHAN = "KANGASKHAN",
|
||||
KINGLER = "KINGLER",
|
||||
KOFFING = "KOFFING",
|
||||
KRABBY = "KRABBY",
|
||||
LAPRAS = "LAPRAS",
|
||||
LICKITUNG = "LICKITUNG",
|
||||
MACHAMP = "MACHAMP",
|
||||
MACHOKE = "MACHOKE",
|
||||
MACHOP = "MACHOP",
|
||||
MAGIKARP = "MAGIKARP",
|
||||
MAGMAR = "MAGMAR",
|
||||
MAGNEMITE = "MAGNEMITE",
|
||||
MAGNETON = "MAGNETON",
|
||||
MANKEY = "MANKEY",
|
||||
MAROWAK = "MAROWAK",
|
||||
MEOWTH = "MEOWTH",
|
||||
METAPOD = "METAPOD",
|
||||
MEW = "MEW",
|
||||
MEWTWO = "MEWTWO",
|
||||
MOLTRES = "MOLTRES",
|
||||
MR_MIME = "MR_MIME",
|
||||
MUK = "MUK",
|
||||
NIDOKING = "NIDOKING",
|
||||
NIDOQUEEN = "NIDOQUEEN",
|
||||
NIDORAN_F = "NIDORAN_F",
|
||||
NIDORAN_M = "NIDORAN_M",
|
||||
NIDORINA = "NIDORINA",
|
||||
NIDORINO = "NIDORINO",
|
||||
NINETALES = "NINETALES",
|
||||
ODDISH = "ODDISH",
|
||||
OMANYTE = "OMANYTE",
|
||||
OMASTAR = "OMASTAR",
|
||||
ONIX = "ONIX",
|
||||
PARAS = "PARAS",
|
||||
PARASECT = "PARASECT",
|
||||
PERSIAN = "PERSIAN",
|
||||
PIDGEOT = "PIDGEOT",
|
||||
PIDGEOTTO = "PIDGEOTTO",
|
||||
PIDGEY = "PIDGEY",
|
||||
PIKACHU = "PIKACHU",
|
||||
PINSIR = "PINSIR",
|
||||
POLIWAG = "POLIWAG",
|
||||
POLIWHIRL = "POLIWHIRL",
|
||||
POLIWRATH = "POLIWRATH",
|
||||
PONYTA = "PONYTA",
|
||||
PORYGON = "PORYGON",
|
||||
PRIMEAPE = "PRIMEAPE",
|
||||
PSYDUCK = "PSYDUCK",
|
||||
RAICHU = "RAICHU",
|
||||
RAPIDASH = "RAPIDASH",
|
||||
RATICATE = "RATICATE",
|
||||
RATTATA = "RATTATA",
|
||||
RHYDON = "RHYDON",
|
||||
RHYHORN = "RHYHORN",
|
||||
SANDSHREW = "SANDSHREW",
|
||||
SANDSLASH = "SANDSLASH",
|
||||
SCYTHER = "SCYTHER",
|
||||
SEADRA = "SEADRA",
|
||||
SEAKING = "SEAKING",
|
||||
SEEL = "SEEL",
|
||||
SHELLDER = "SHELLDER",
|
||||
SLOWBRO = "SLOWBRO",
|
||||
SLOWPOKE = "SLOWPOKE",
|
||||
SNORLAX = "SNORLAX",
|
||||
SPEAROW = "SPEAROW",
|
||||
SQUIRTLE = "SQUIRTLE",
|
||||
STARMIE = "STARMIE",
|
||||
STARYU = "STARYU",
|
||||
TANGELA = "TANGELA",
|
||||
TAUROS = "TAUROS",
|
||||
TENTACOOL = "TENTACOOL",
|
||||
TENTACRUEL = "TENTACRUEL",
|
||||
VAPOREON = "VAPOREON",
|
||||
VENOMOTH = "VENOMOTH",
|
||||
VENONAT = "VENONAT",
|
||||
VENUSAUR = "VENUSAUR",
|
||||
VICTREEBEL = "VICTREEBEL",
|
||||
VILEPLUME = "VILEPLUME",
|
||||
VOLTORB = "VOLTORB",
|
||||
VULPIX = "VULPIX",
|
||||
WARTORTLE = "WARTORTLE",
|
||||
WEEDLE = "WEEDLE",
|
||||
WEEPINBELL = "WEEPINBELL",
|
||||
WEEZING = "WEEZING",
|
||||
WIGGLYTUFF = "WIGGLYTUFF",
|
||||
ZAPDOS = "ZAPDOS",
|
||||
ZUBAT = "ZUBAT",
|
||||
},
|
||||
source = "pokered-gbc data/super_palettes.asm + data/mon_palettes.asm + color/**",
|
||||
world = {
|
||||
|
||||
@@ -1,686 +0,0 @@
|
||||
-- Generated by tools/build_data.py. DO NOT EDIT.
|
||||
-- Pokemon Yellow palettes (COLORS=OG YELLOW): SuperPalettes +
|
||||
-- CGBBasePalettes (authentic GBC look) as 4 8-bit RGB colors
|
||||
-- per palette (color 0 first), plus MonsterPalettes.
|
||||
return {
|
||||
cgbBase = {
|
||||
["0F"] = {
|
||||
{ 255, 255, 255 },
|
||||
{ 107, 8, 255 },
|
||||
{ 0, 74, 255 },
|
||||
{ 8, 8, 8 },
|
||||
},
|
||||
BADGE = {
|
||||
{ 255, 255, 255 },
|
||||
{ 189, 66, 0 },
|
||||
{ 140, 115, 90 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
BLACK = {
|
||||
{ 255, 255, 255 },
|
||||
{ 25, 25, 25 },
|
||||
{ 25, 25, 25 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
BLUEMON = {
|
||||
{ 255, 255, 255 },
|
||||
{ 132, 148, 255 },
|
||||
{ 0, 8, 206 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
BROWNMON = {
|
||||
{ 255, 255, 255 },
|
||||
{ 239, 148, 82 },
|
||||
{ 140, 74, 41 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
CAVE = {
|
||||
{ 255, 255, 255 },
|
||||
{ 189, 66, 0 },
|
||||
{ 140, 115, 90 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
CELADON = {
|
||||
{ 255, 255, 255 },
|
||||
{ 41, 255, 41 },
|
||||
{ 90, 189, 255 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
CERULEAN = {
|
||||
{ 255, 255, 255 },
|
||||
{ 41, 66, 255 },
|
||||
{ 90, 189, 255 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
CINNABAR = {
|
||||
{ 255, 255, 255 },
|
||||
{ 255, 66, 66 },
|
||||
{ 90, 189, 255 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
CYANMON = {
|
||||
{ 255, 255, 255 },
|
||||
{ 132, 214, 255 },
|
||||
{ 0, 140, 255 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
FUCHSIA = {
|
||||
{ 255, 255, 255 },
|
||||
{ 255, 123, 123 },
|
||||
{ 90, 189, 255 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
GAMEFREAK = {
|
||||
{ 255, 255, 255 },
|
||||
{ 255, 156, 0 },
|
||||
{ 156, 156, 0 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
GRAYMON = {
|
||||
{ 255, 255, 255 },
|
||||
{ 165, 189, 82 },
|
||||
{ 90, 90, 41 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
GREENBAR = {
|
||||
{ 255, 255, 255 },
|
||||
{ 255, 255, 0 },
|
||||
{ 0, 255, 0 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
GREENMON = {
|
||||
{ 255, 255, 255 },
|
||||
{ 140, 255, 90 },
|
||||
{ 8, 181, 49 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
INDIGO = {
|
||||
{ 255, 255, 255 },
|
||||
{ 90, 66, 255 },
|
||||
{ 90, 189, 255 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
LAVENDER = {
|
||||
{ 255, 255, 255 },
|
||||
{ 206, 33, 255 },
|
||||
{ 90, 189, 255 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
LOGO1 = {
|
||||
{ 255, 255, 255 },
|
||||
{ 255, 255, 0 },
|
||||
{ 255, 0, 0 },
|
||||
{ 255, 0, 0 },
|
||||
},
|
||||
LOGO2 = {
|
||||
{ 255, 255, 255 },
|
||||
{ 255, 255, 0 },
|
||||
{ 58, 58, 206 },
|
||||
{ 0, 0, 140 },
|
||||
},
|
||||
MEWMON = {
|
||||
{ 255, 255, 255 },
|
||||
{ 255, 255, 0 },
|
||||
{ 255, 8, 8 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
PALLET = {
|
||||
{ 255, 255, 255 },
|
||||
{ 189, 140, 255 },
|
||||
{ 90, 189, 255 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
PEWTER = {
|
||||
{ 255, 255, 255 },
|
||||
{ 148, 148, 123 },
|
||||
{ 90, 189, 255 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
PIKACHUS_BEACH = {
|
||||
{ 255, 255, 255 },
|
||||
{ 255, 255, 0 },
|
||||
{ 90, 189, 255 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
PIKACHUS_BEACH_TITLE = {
|
||||
{ 255, 255, 255 },
|
||||
{ 74, 74, 74 },
|
||||
{ 255, 173, 0 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
PIKACHU_PORTRAIT = {
|
||||
{ 255, 255, 255 },
|
||||
{ 255, 148, 0 },
|
||||
{ 156, 58, 8 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
PINKMON = {
|
||||
{ 255, 255, 255 },
|
||||
{ 255, 123, 148 },
|
||||
{ 255, 0, 49 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
PURPLEMON = {
|
||||
{ 255, 255, 255 },
|
||||
{ 206, 123, 255 },
|
||||
{ 156, 0, 181 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
REDBAR = {
|
||||
{ 255, 255, 255 },
|
||||
{ 255, 255, 0 },
|
||||
{ 255, 0, 0 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
REDMON = {
|
||||
{ 255, 255, 255 },
|
||||
{ 255, 140, 0 },
|
||||
{ 255, 0, 0 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
ROUTE = {
|
||||
{ 255, 255, 255 },
|
||||
{ 132, 255, 33 },
|
||||
{ 90, 189, 255 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
SAFFRON = {
|
||||
{ 255, 255, 255 },
|
||||
{ 255, 255, 0 },
|
||||
{ 90, 189, 255 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
SLOTS1 = {
|
||||
{ 255, 255, 255 },
|
||||
{ 206, 8, 255 },
|
||||
{ 255, 0, 0 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
SLOTS2 = {
|
||||
{ 255, 255, 255 },
|
||||
{ 255, 33, 156 },
|
||||
{ 255, 255, 0 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
SLOTS3 = {
|
||||
{ 255, 255, 255 },
|
||||
{ 66, 255, 0 },
|
||||
{ 255, 255, 0 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
SLOTS4 = {
|
||||
{ 255, 255, 255 },
|
||||
{ 0, 255, 255 },
|
||||
{ 255, 255, 0 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
TOWNMAP = {
|
||||
{ 255, 255, 255 },
|
||||
{ 0, 173, 255 },
|
||||
{ 82, 230, 0 },
|
||||
{ 8, 8, 8 },
|
||||
},
|
||||
VERMILION = {
|
||||
{ 255, 255, 255 },
|
||||
{ 255, 156, 0 },
|
||||
{ 90, 189, 255 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
VIRIDIAN = {
|
||||
{ 255, 255, 255 },
|
||||
{ 156, 255, 0 },
|
||||
{ 90, 189, 255 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
YELLOWBAR = {
|
||||
{ 255, 255, 255 },
|
||||
{ 255, 255, 0 },
|
||||
{ 255, 148, 0 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
YELLOWMON = {
|
||||
{ 255, 255, 255 },
|
||||
{ 255, 255, 0 },
|
||||
{ 230, 115, 0 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
},
|
||||
order = {
|
||||
"ROUTE",
|
||||
"PALLET",
|
||||
"VIRIDIAN",
|
||||
"PEWTER",
|
||||
"CERULEAN",
|
||||
"LAVENDER",
|
||||
"VERMILION",
|
||||
"CELADON",
|
||||
"FUCHSIA",
|
||||
"CINNABAR",
|
||||
"INDIGO",
|
||||
"SAFFRON",
|
||||
"TOWNMAP",
|
||||
"LOGO1",
|
||||
"LOGO2",
|
||||
"0F",
|
||||
"MEWMON",
|
||||
"BLUEMON",
|
||||
"REDMON",
|
||||
"CYANMON",
|
||||
"PURPLEMON",
|
||||
"BROWNMON",
|
||||
"GREENMON",
|
||||
"PINKMON",
|
||||
"YELLOWMON",
|
||||
"GRAYMON",
|
||||
"SLOTS1",
|
||||
"SLOTS2",
|
||||
"SLOTS3",
|
||||
"SLOTS4",
|
||||
"BLACK",
|
||||
"GREENBAR",
|
||||
"YELLOWBAR",
|
||||
"REDBAR",
|
||||
"BADGE",
|
||||
"CAVE",
|
||||
"GAMEFREAK",
|
||||
"PIKACHUS_BEACH",
|
||||
"PIKACHU_PORTRAIT",
|
||||
"PIKACHUS_BEACH_TITLE",
|
||||
},
|
||||
palettes = {
|
||||
["0F"] = {
|
||||
{ 255, 255, 247 },
|
||||
{ 197, 165, 247 },
|
||||
{ 90, 165, 247 },
|
||||
{ 25, 16, 16 },
|
||||
},
|
||||
BADGE = {
|
||||
{ 255, 255, 247 },
|
||||
{ 165, 123, 90 },
|
||||
{ 181, 173, 165 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
BLACK = {
|
||||
{ 255, 255, 247 },
|
||||
{ 49, 49, 49 },
|
||||
{ 49, 49, 49 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
BLUEMON = {
|
||||
{ 255, 255, 247 },
|
||||
{ 173, 181, 255 },
|
||||
{ 74, 82, 165 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
BROWNMON = {
|
||||
{ 255, 255, 247 },
|
||||
{ 214, 189, 148 },
|
||||
{ 148, 115, 82 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
CAVE = {
|
||||
{ 255, 255, 247 },
|
||||
{ 165, 123, 90 },
|
||||
{ 181, 173, 165 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
CELADON = {
|
||||
{ 255, 255, 247 },
|
||||
{ 181, 255, 181 },
|
||||
{ 189, 222, 255 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
CERULEAN = {
|
||||
{ 255, 255, 247 },
|
||||
{ 181, 189, 255 },
|
||||
{ 189, 222, 255 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
CINNABAR = {
|
||||
{ 255, 255, 247 },
|
||||
{ 255, 123, 115 },
|
||||
{ 189, 222, 255 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
CYANMON = {
|
||||
{ 255, 255, 247 },
|
||||
{ 214, 230, 255 },
|
||||
{ 58, 197, 230 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
FUCHSIA = {
|
||||
{ 255, 255, 247 },
|
||||
{ 255, 214, 214 },
|
||||
{ 189, 222, 255 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
GAMEFREAK = {
|
||||
{ 255, 255, 247 },
|
||||
{ 230, 197, 115 },
|
||||
{ 165, 165, 90 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
GRAYMON = {
|
||||
{ 255, 255, 247 },
|
||||
{ 206, 206, 148 },
|
||||
{ 132, 132, 115 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
GREENBAR = {
|
||||
{ 255, 255, 247 },
|
||||
{ 255, 255, 156 },
|
||||
{ 0, 173, 0 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
GREENMON = {
|
||||
{ 255, 255, 247 },
|
||||
{ 197, 230, 148 },
|
||||
{ 107, 173, 123 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
INDIGO = {
|
||||
{ 255, 255, 247 },
|
||||
{ 140, 140, 206 },
|
||||
{ 189, 222, 255 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
LAVENDER = {
|
||||
{ 255, 255, 247 },
|
||||
{ 222, 189, 239 },
|
||||
{ 189, 222, 255 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
LOGO1 = {
|
||||
{ 255, 255, 247 },
|
||||
{ 247, 247, 140 },
|
||||
{ 173, 0, 33 },
|
||||
{ 173, 0, 33 },
|
||||
},
|
||||
LOGO2 = {
|
||||
{ 255, 255, 247 },
|
||||
{ 247, 247, 140 },
|
||||
{ 148, 148, 197 },
|
||||
{ 58, 58, 132 },
|
||||
},
|
||||
MEWMON = {
|
||||
{ 255, 255, 247 },
|
||||
{ 255, 247, 181 },
|
||||
{ 222, 132, 132 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
PALLET = {
|
||||
{ 255, 255, 247 },
|
||||
{ 230, 222, 255 },
|
||||
{ 189, 222, 255 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
PEWTER = {
|
||||
{ 255, 255, 247 },
|
||||
{ 189, 189, 181 },
|
||||
{ 189, 222, 255 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
PIKACHUS_BEACH = {
|
||||
{ 255, 255, 247 },
|
||||
{ 255, 247, 181 },
|
||||
{ 189, 222, 255 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
PIKACHUS_BEACH_TITLE = {
|
||||
{ 255, 255, 247 },
|
||||
{ 132, 132, 132 },
|
||||
{ 255, 206, 74 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
PIKACHU_PORTRAIT = {
|
||||
{ 255, 255, 247 },
|
||||
{ 230, 189, 74 },
|
||||
{ 148, 115, 82 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
PINKMON = {
|
||||
{ 255, 255, 247 },
|
||||
{ 255, 197, 214 },
|
||||
{ 255, 148, 173 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
PURPLEMON = {
|
||||
{ 255, 255, 247 },
|
||||
{ 222, 181, 247 },
|
||||
{ 181, 123, 189 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
REDBAR = {
|
||||
{ 255, 255, 247 },
|
||||
{ 255, 255, 156 },
|
||||
{ 214, 74, 49 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
REDMON = {
|
||||
{ 255, 255, 247 },
|
||||
{ 255, 197, 90 },
|
||||
{ 214, 74, 49 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
ROUTE = {
|
||||
{ 255, 255, 247 },
|
||||
{ 189, 214, 156 },
|
||||
{ 189, 222, 255 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
SAFFRON = {
|
||||
{ 255, 255, 247 },
|
||||
{ 255, 255, 156 },
|
||||
{ 189, 222, 255 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
SLOTS1 = {
|
||||
{ 255, 255, 247 },
|
||||
{ 222, 181, 247 },
|
||||
{ 214, 74, 49 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
SLOTS2 = {
|
||||
{ 255, 255, 247 },
|
||||
{ 255, 189, 214 },
|
||||
{ 239, 239, 66 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
SLOTS3 = {
|
||||
{ 255, 255, 247 },
|
||||
{ 189, 255, 165 },
|
||||
{ 239, 239, 66 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
SLOTS4 = {
|
||||
{ 255, 255, 247 },
|
||||
{ 189, 239, 255 },
|
||||
{ 239, 239, 66 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
TOWNMAP = {
|
||||
{ 255, 255, 247 },
|
||||
{ 165, 214, 255 },
|
||||
{ 140, 189, 82 },
|
||||
{ 25, 16, 16 },
|
||||
},
|
||||
VERMILION = {
|
||||
{ 255, 255, 247 },
|
||||
{ 255, 206, 132 },
|
||||
{ 189, 222, 255 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
VIRIDIAN = {
|
||||
{ 255, 255, 247 },
|
||||
{ 214, 255, 173 },
|
||||
{ 189, 222, 255 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
YELLOWBAR = {
|
||||
{ 255, 255, 247 },
|
||||
{ 255, 255, 156 },
|
||||
{ 230, 189, 74 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
YELLOWMON = {
|
||||
{ 255, 255, 247 },
|
||||
{ 255, 255, 156 },
|
||||
{ 230, 189, 74 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
},
|
||||
pokemon = {
|
||||
ABRA = "YELLOWMON",
|
||||
AERODACTYL = "GRAYMON",
|
||||
ALAKAZAM = "YELLOWMON",
|
||||
ARBOK = "PURPLEMON",
|
||||
ARCANINE = "REDMON",
|
||||
ARTICUNO = "BLUEMON",
|
||||
BEEDRILL = "YELLOWMON",
|
||||
BELLSPROUT = "GREENMON",
|
||||
BLASTOISE = "CYANMON",
|
||||
BULBASAUR = "GREENMON",
|
||||
BUTTERFREE = "CYANMON",
|
||||
CATERPIE = "GREENMON",
|
||||
CHANSEY = "PINKMON",
|
||||
CHARIZARD = "REDMON",
|
||||
CHARMANDER = "REDMON",
|
||||
CHARMELEON = "REDMON",
|
||||
CLEFABLE = "PINKMON",
|
||||
CLEFAIRY = "PINKMON",
|
||||
CLOYSTER = "GRAYMON",
|
||||
CUBONE = "GRAYMON",
|
||||
DEWGONG = "BLUEMON",
|
||||
DIGLETT = "BROWNMON",
|
||||
DITTO = "GRAYMON",
|
||||
DODRIO = "BROWNMON",
|
||||
DODUO = "BROWNMON",
|
||||
DRAGONAIR = "BLUEMON",
|
||||
DRAGONITE = "BROWNMON",
|
||||
DRATINI = "GRAYMON",
|
||||
DROWZEE = "YELLOWMON",
|
||||
DUGTRIO = "BROWNMON",
|
||||
EEVEE = "GRAYMON",
|
||||
EKANS = "PURPLEMON",
|
||||
ELECTABUZZ = "YELLOWMON",
|
||||
ELECTRODE = "YELLOWMON",
|
||||
EXEGGCUTE = "PINKMON",
|
||||
EXEGGUTOR = "GREENMON",
|
||||
FARFETCHD = "BROWNMON",
|
||||
FEAROW = "BROWNMON",
|
||||
FLAREON = "REDMON",
|
||||
GASTLY = "PURPLEMON",
|
||||
GENGAR = "PURPLEMON",
|
||||
GEODUDE = "GRAYMON",
|
||||
GLOOM = "REDMON",
|
||||
GOLBAT = "BLUEMON",
|
||||
GOLDEEN = "REDMON",
|
||||
GOLDUCK = "CYANMON",
|
||||
GOLEM = "GRAYMON",
|
||||
GRAVELER = "GRAYMON",
|
||||
GRIMER = "PURPLEMON",
|
||||
GROWLITHE = "BROWNMON",
|
||||
GYARADOS = "BLUEMON",
|
||||
HAUNTER = "PURPLEMON",
|
||||
HITMONCHAN = "BROWNMON",
|
||||
HITMONLEE = "BROWNMON",
|
||||
HORSEA = "CYANMON",
|
||||
HYPNO = "YELLOWMON",
|
||||
IVYSAUR = "GREENMON",
|
||||
JIGGLYPUFF = "PINKMON",
|
||||
JOLTEON = "YELLOWMON",
|
||||
JYNX = "MEWMON",
|
||||
KABUTO = "BROWNMON",
|
||||
KABUTOPS = "BROWNMON",
|
||||
KADABRA = "YELLOWMON",
|
||||
KAKUNA = "YELLOWMON",
|
||||
KANGASKHAN = "BROWNMON",
|
||||
KINGLER = "REDMON",
|
||||
KOFFING = "PURPLEMON",
|
||||
KRABBY = "REDMON",
|
||||
LAPRAS = "CYANMON",
|
||||
LICKITUNG = "PINKMON",
|
||||
MACHAMP = "GRAYMON",
|
||||
MACHOKE = "GRAYMON",
|
||||
MACHOP = "GRAYMON",
|
||||
MAGIKARP = "REDMON",
|
||||
MAGMAR = "REDMON",
|
||||
MAGNEMITE = "GRAYMON",
|
||||
MAGNETON = "GRAYMON",
|
||||
MANKEY = "BROWNMON",
|
||||
MAROWAK = "GRAYMON",
|
||||
MEOWTH = "YELLOWMON",
|
||||
METAPOD = "GREENMON",
|
||||
MEW = "MEWMON",
|
||||
MEWTWO = "MEWMON",
|
||||
MOLTRES = "REDMON",
|
||||
MR_MIME = "PINKMON",
|
||||
MUK = "PURPLEMON",
|
||||
NIDOKING = "PURPLEMON",
|
||||
NIDOQUEEN = "BLUEMON",
|
||||
NIDORAN_F = "BLUEMON",
|
||||
NIDORAN_M = "PURPLEMON",
|
||||
NIDORINA = "BLUEMON",
|
||||
NIDORINO = "PURPLEMON",
|
||||
NINETALES = "YELLOWMON",
|
||||
ODDISH = "GREENMON",
|
||||
OMANYTE = "BLUEMON",
|
||||
OMASTAR = "BLUEMON",
|
||||
ONIX = "GRAYMON",
|
||||
PARAS = "REDMON",
|
||||
PARASECT = "REDMON",
|
||||
PERSIAN = "YELLOWMON",
|
||||
PIDGEOT = "BROWNMON",
|
||||
PIDGEOTTO = "BROWNMON",
|
||||
PIDGEY = "BROWNMON",
|
||||
PIKACHU = "YELLOWMON",
|
||||
PINSIR = "BROWNMON",
|
||||
POLIWAG = "BLUEMON",
|
||||
POLIWHIRL = "BLUEMON",
|
||||
POLIWRATH = "BLUEMON",
|
||||
PONYTA = "REDMON",
|
||||
PORYGON = "GRAYMON",
|
||||
PRIMEAPE = "BROWNMON",
|
||||
PSYDUCK = "YELLOWMON",
|
||||
RAICHU = "YELLOWMON",
|
||||
RAPIDASH = "REDMON",
|
||||
RATICATE = "GRAYMON",
|
||||
RATTATA = "GRAYMON",
|
||||
RHYDON = "GRAYMON",
|
||||
RHYHORN = "GRAYMON",
|
||||
SANDSHREW = "BROWNMON",
|
||||
SANDSLASH = "BROWNMON",
|
||||
SCYTHER = "GREENMON",
|
||||
SEADRA = "CYANMON",
|
||||
SEAKING = "REDMON",
|
||||
SEEL = "BLUEMON",
|
||||
SHELLDER = "GRAYMON",
|
||||
SLOWBRO = "PINKMON",
|
||||
SLOWPOKE = "PINKMON",
|
||||
SNORLAX = "PINKMON",
|
||||
SPEAROW = "BROWNMON",
|
||||
SQUIRTLE = "CYANMON",
|
||||
STARMIE = "GRAYMON",
|
||||
STARYU = "REDMON",
|
||||
TANGELA = "BLUEMON",
|
||||
TAUROS = "GRAYMON",
|
||||
TENTACOOL = "CYANMON",
|
||||
TENTACRUEL = "CYANMON",
|
||||
VAPOREON = "CYANMON",
|
||||
VENOMOTH = "PURPLEMON",
|
||||
VENONAT = "PURPLEMON",
|
||||
VENUSAUR = "GREENMON",
|
||||
VICTREEBEL = "GREENMON",
|
||||
VILEPLUME = "REDMON",
|
||||
VOLTORB = "YELLOWMON",
|
||||
VULPIX = "REDMON",
|
||||
WARTORTLE = "CYANMON",
|
||||
WEEDLE = "YELLOWMON",
|
||||
WEEPINBELL = "GREENMON",
|
||||
WEEZING = "PURPLEMON",
|
||||
WIGGLYTUFF = "PINKMON",
|
||||
ZAPDOS = "YELLOWMON",
|
||||
ZUBAT = "BLUEMON",
|
||||
},
|
||||
source = "pokeyellow data/sgb/sgb_palettes.asm + data/pokemon/palettes.asm",
|
||||
}
|
||||
@@ -11,70 +11,8 @@
|
||||
-- equivalent, kept for save compatibility: rows 1-4 also self-heal
|
||||
-- older saves (flag set before the port hid the ball) by hiding the
|
||||
-- leftover ball on the next interaction.
|
||||
--
|
||||
-- The room's two readables (data/events/hidden_events.asm,
|
||||
-- hidden_events_for CELADON_MANSION_ROOF_HOUSE):
|
||||
-- hidden_text_predef 3, 0 / 4, 0 PrintBlackboardLinkCableText, LinkCableHelp
|
||||
-- hidden_text_predef 3, 4 PrintNotebookText, TMNotebook
|
||||
-- tools/extract/field.py only parses `hidden_event` rows, so no
|
||||
-- hidden_text_predef row reaches data/generated/field.lua and both tiles
|
||||
-- were dead A presses (#391). Same hook shape as the bedroom SNES in
|
||||
-- data/scripts/flavor/reds_house_2f.lua (#135). hidden_text_predef puts
|
||||
-- the predef id in the facing byte, so neither tile gates on facing.
|
||||
|
||||
local Menu = require("src.ui.Menu")
|
||||
local TextBox = require("src.render.TextBox")
|
||||
|
||||
-- TMNotebookText (data/text/text_2.asm) has no leading underscore, so the
|
||||
-- extractor never collects it and the pamphlet's text is inlined.
|
||||
local TM_NOTEBOOK_TEXT = "It's a pamphlet\non TMs.\f...\f"
|
||||
.. "There are 50 TMs\nin all.\f"
|
||||
.. "There are also 5\nHMs that can be\vused repeatedly.\f"
|
||||
.. "SILPH CO."
|
||||
|
||||
-- LinkCableHelp (engine/events/hidden_events/school_blackboard.asm):
|
||||
-- HowToLinkText's four headings in the 15x10 box at the top left; picking
|
||||
-- a heading prints its LinkCableInfoText and returns to the menu, B or
|
||||
-- STOP READING closes.
|
||||
local LINK_HEADINGS = { "HOW TO LINK", "COLOSSEUM", "TRADE CENTER" }
|
||||
|
||||
local function linkCableHelp(game)
|
||||
local text = game.data.text or {}
|
||||
local items, showMenu, askHeading
|
||||
function showMenu()
|
||||
game.stack:push(Menu.new(game, items,
|
||||
{ tx = 0, ty = 0, tw = 15, th = 10, rowStep = 1 }))
|
||||
end
|
||||
function askHeading()
|
||||
game.stack:push(TextBox.new(game,
|
||||
text._LinkCableHelpText2 or "Which heading do\nyou want to read?",
|
||||
showMenu))
|
||||
end
|
||||
items = {}
|
||||
for i, label in ipairs(LINK_HEADINGS) do
|
||||
items[i] = { label = label, onSelect = function()
|
||||
game.stack:push(TextBox.new(game,
|
||||
text["_LinkCableInfoText" .. i] or label, askHeading))
|
||||
end }
|
||||
end
|
||||
items[#items + 1] = { label = "STOP READING" }
|
||||
game.stack:push(TextBox.new(game,
|
||||
text._LinkCableHelpText1 or "TRAINER TIPS\fUsing a Game Link\nCable",
|
||||
askHeading))
|
||||
end
|
||||
|
||||
return {
|
||||
onInteract = function(game, ow, fx, fy)
|
||||
if fy == 0 and (fx == 3 or fx == 4) then
|
||||
linkCableHelp(game)
|
||||
return true
|
||||
end
|
||||
if fx == 3 and fy == 4 then
|
||||
game.stack:push(TextBox.new(game, TM_NOTEBOOK_TEXT))
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end,
|
||||
talk = {
|
||||
TEXT_CELADONMANSION_ROOF_HOUSE_EEVEE_POKEBALL = {
|
||||
{ "check_flag", "EVENT_GOT_EEVEE" }, -- 1
|
||||
@@ -84,16 +22,11 @@ return {
|
||||
{ "jump", 13 }, -- 4
|
||||
{ "give_pokemon", "EEVEE", 25 }, -- 5
|
||||
{ "jump_if_false", 12 }, -- 6 (party+box full)
|
||||
-- flag + HideObject before the jingle and GotMonText: the nickname
|
||||
-- prompt inside give_pokemon and the text row both yield, and a
|
||||
-- script that dies there would leave the EEVEE taken with the ball
|
||||
-- still on the table and the gift claimable again (#426). The row
|
||||
-- count is unchanged, so the numeric jump targets still hold.
|
||||
{ "set_flag", "EVENT_GOT_EEVEE" }, -- 7
|
||||
{ "play_sound", "Get_Item1" }, -- 7 (GotMonText jingle)
|
||||
{ "show_text", "_GotMonText", { RAM = "EEVEE" } }, -- 8
|
||||
{ "set_flag", "EVENT_GOT_EEVEE" }, -- 9
|
||||
{ "hide_object", "CELADON_MANSION_ROOF_HOUSE",
|
||||
"CELADONMANSION_ROOF_HOUSE_EEVEE_POKEBALL" }, -- 8
|
||||
{ "text_sound", "Get_Item1" }, -- 9 (GotMonText jingle)
|
||||
{ "show_text", "_GotMonText", { RAM = "EEVEE" } }, -- 10
|
||||
"CELADONMANSION_ROOF_HOUSE_EEVEE_POKEBALL" }, -- 10
|
||||
{ "jump", 13 }, -- 11
|
||||
{ "show_text", "_BoxIsFullText" }, -- 12
|
||||
},
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
-- BikeShop (BIKE_SHOP) flavor dialogue
|
||||
-- Source: pokered/scripts/BikeShop.asm, pokered/text/BikeShop.asm
|
||||
--
|
||||
-- TEXT_BIKESHOP_CLERK lives in data/scripts/story2.lua (M.BIKE_SHOP): the
|
||||
-- voucher exchange and the BICYCLE/CANCEL price window need more than
|
||||
-- command rows (#568).
|
||||
-- TEXT_BIKESHOP_CLERK is skipped: it drives the actual voucher-for-bicycle
|
||||
-- exchange (YesNoChoice purchase menu, GiveItem, RemoveItemByID, SetEvent
|
||||
-- EVENT_GOT_BICYCLE). That is a significant standalone feature outside the
|
||||
-- scope of these two flavor NPCs and is left unported here.
|
||||
|
||||
return {
|
||||
BIKE_SHOP = {
|
||||
@@ -19,15 +20,12 @@ return {
|
||||
-- CheckEvent EVENT_GOT_BICYCLE ; jr nz, .gotBike
|
||||
-- before the player owns a bike -> TheseBikesAreExpensiveText
|
||||
-- after the player owns a bike -> CoolBikeText
|
||||
-- The check reads the bag, not the event: the port hands out the
|
||||
-- BICYCLE itself (a key item, so it cannot be tossed), and that also
|
||||
-- reads right on saves written before the clerk set the event (#567).
|
||||
TEXT_BIKESHOP_YOUNGSTER = {
|
||||
{ "face_player" }, -- 1
|
||||
{ "check_item", "BICYCLE" }, -- 2
|
||||
{ "jump_if_true", 6 }, -- 3
|
||||
{ "check_flag", "EVENT_GOT_BICYCLE" }, -- 2
|
||||
{ "jump_if_true", 5 }, -- 3
|
||||
{ "show_text", "_BikeShopYoungsterTheseBikesAreExpensiveText" }, -- 4
|
||||
{ "jump", "end" }, -- 5
|
||||
{ "jump", 6 }, -- 5
|
||||
{ "show_text", "_BikeShopYoungsterCoolBikeText" }, -- 6
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,88 +1,33 @@
|
||||
-- Celadon Mansion 3F, the GAME FREAK dev floor
|
||||
-- (pokered+pokeyellow/scripts/CeladonMansion3F.asm). Every dev's
|
||||
-- text_asm counts set bits in wPokedexOwned against NUM_POKEMON - 1
|
||||
-- (150, discounting Mew). The game designer shows the diploma
|
||||
-- (DisplayDiploma -> src/ui/Diploma.lua) on a completed dex; Yellow's
|
||||
-- graphic artist then offers the printed copy (PrintDiploma, stood in
|
||||
-- by src/core/Printer.lua's PNG export like the Pokédex PRNT item).
|
||||
|
||||
local function ownedCount(game)
|
||||
local dex = game.save.pokedex
|
||||
local owned = 0
|
||||
if dex and dex.owned then
|
||||
for _ in pairs(dex.owned) do owned = owned + 1 end
|
||||
end
|
||||
return owned
|
||||
end
|
||||
|
||||
-- Celadon Mansion 3F Game Designer (pokered/scripts/CeladonMansion3F.asm
|
||||
-- CeladonMansion3FGameDesignerText): text_asm counts set bits in
|
||||
-- wPokedexOwned and compares against NUM_POKEMON - 1 (discounts Mew).
|
||||
-- If the player owns >= 150 species, shows the "completed" text
|
||||
-- (originally followed by DisplayDiploma, which has no equivalent UI
|
||||
-- in this port, so we just show the congratulatory text); otherwise
|
||||
-- shows the normal encouragement text.
|
||||
return {
|
||||
CELADON_MANSION_3F = {
|
||||
talk = {
|
||||
TEXT_CELADONMANSION3F_GAME_DESIGNER = function(game, ow, npc, done)
|
||||
local t = game.data.text
|
||||
local TextBox = require("src.render.TextBox")
|
||||
if ownedCount(game) < 150 then
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._CeladonMansion3FGameDesignerText
|
||||
or "Is that right?\nI'm the game\ndesigner!\fFilling up your\nPOKéDEX is tough,\nbut don't quit!",
|
||||
done))
|
||||
return
|
||||
local dex = game.save.pokedex
|
||||
local owned = 0
|
||||
if dex and dex.owned then
|
||||
for _ in pairs(dex.owned) do
|
||||
owned = owned + 1
|
||||
end
|
||||
end
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._CeladonMansion3FGameDesignerCompletedDexText
|
||||
or "Wow! Excellent!\nYou completed\nyour POKéDEX!\nCongratulations!",
|
||||
function()
|
||||
local Diploma = require("src.ui.Diploma")
|
||||
game.stack:push(Diploma.new(game, function()
|
||||
-- Yellow tags on the unlocked-printing line (CompletedDexText2)
|
||||
local after = require("src.core.GameVersion").isYellow()
|
||||
and (t._CeladonMansion3FGameDesignerCompletedDexText2
|
||||
or "You can print out\nyour diploma with\nthe GAME BOY\nPrinter!")
|
||||
or nil
|
||||
if after then
|
||||
game.stack:push(TextBox.new(game, after, done))
|
||||
else
|
||||
done()
|
||||
end
|
||||
end))
|
||||
end))
|
||||
end,
|
||||
|
||||
TEXT_CELADONMANSION3F_GRAPHIC_ARTIST = function(game, ow, npc, done)
|
||||
local t = game.data.text
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local yellow = require("src.core.GameVersion").isYellow()
|
||||
if not (yellow and ownedCount(game) >= 150) then
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._CeladonMansion3FGraphicArtistText
|
||||
or "I'm the graphic\nartist!\nI drew you!", done))
|
||||
return
|
||||
-- NUM_POKEMON - 1 = 150 (discounts Mew, per pokered)
|
||||
local label, fallback
|
||||
if owned >= 150 then
|
||||
label, fallback = "_CeladonMansion3FGameDesignerCompletedDexText",
|
||||
"Wow! Excellent!\nYou completed\nyour POKeDEX!\nCongratulations!"
|
||||
else
|
||||
label, fallback = "_CeladonMansion3FGameDesignerText",
|
||||
"Is that right?\nI'm the game\ndesigner!\nFilling up your\nPOKeDEX is tough,\nbut don't quit!\nWhen you finish,\ncome tell me!"
|
||||
end
|
||||
-- _CeladonMansion3FGraphicArtistText2: offer to print the diploma
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._CeladonMansion3FGraphicArtistText2
|
||||
or "I'm the graphic\nartist!\fShould I print\nyour diploma?",
|
||||
function()
|
||||
local ChoiceBox = require("src.ui.ChoiceBox")
|
||||
game.stack:push(ChoiceBox.new(game, function(yes)
|
||||
if not yes then
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._CeladonMansion3FGraphicArtistText3
|
||||
or "Oh. But it's a\nspecial diploma!", done))
|
||||
return
|
||||
end
|
||||
local Printer = require("src.core.Printer")
|
||||
local Diploma = require("src.ui.Diploma")
|
||||
local Strings = require("src.core.Strings")
|
||||
local saved, err = Printer.save("diploma", 160, 144, function()
|
||||
Diploma.render(game)
|
||||
end)
|
||||
-- the PRNT stand-in always reports where the PNG landed
|
||||
game.stack:push(TextBox.new(game, saved
|
||||
and Strings("There you go!\fSaved as\n%s\vin the save\nfolder.", saved)
|
||||
or Strings("Printer error!\n%s", tostring(err)), done))
|
||||
end))
|
||||
end))
|
||||
local TextBox = require("src.render.TextBox")
|
||||
game.stack:push(TextBox.new(game, t[label] or fallback, done))
|
||||
end,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -35,25 +35,13 @@ local function middleAgedMan(game, ow, npc, done)
|
||||
-- .loop: print WhichBadgeText, then show the badge list menu again
|
||||
push(game, t._CeruleanBadgeHouseMiddleAgedManWhichBadgeText, function()
|
||||
local ListMenu = require("src.ui.ListMenu")
|
||||
local Strings = require("src.core.Strings")
|
||||
local items = {}
|
||||
for _, id in ipairs(BADGE_ORDER) do
|
||||
local idef = game.data.items[id]
|
||||
items[#items + 1] = { label = idef and idef.name or id, value = id }
|
||||
end
|
||||
-- PrintListMenuEntries prints ListMenuCancelText once it hits the
|
||||
-- list's $FF terminator (home/list_menu.asm), so every
|
||||
-- DisplayListMenuID list ends in an on-screen CANCEL row; picking it
|
||||
-- takes DisplayListMenuIDLoop's ExitListMenu path, the same .done
|
||||
-- exit as B (#569)
|
||||
items[#items + 1] = { label = Strings("CANCEL") }
|
||||
local menu = ListMenu.new(game, "", items, {
|
||||
onChoose = function(item)
|
||||
game.stack:pop()
|
||||
if not item.value then
|
||||
push(game, t._CeruleanBadgeHouseMiddleAgedManVisitAnyTimeText, done)
|
||||
return
|
||||
end
|
||||
push(game, t[BADGE_TEXT[item.value]], loop)
|
||||
end,
|
||||
onCancel = function()
|
||||
|
||||
@@ -3,10 +3,6 @@
|
||||
-- Both NPCs use text_asm with an hRandomAdd roll to pick one of several
|
||||
-- flavor lines (no flags, no branching outcome) -- ported as a weighted
|
||||
-- math.random pick each time the NPC is talked to.
|
||||
--
|
||||
-- Yellow renames Slowbro -> Electrode in the text labels (see
|
||||
-- tools/yellow_symbol_aliases.py); look up both so Red/Blue and Yellow
|
||||
-- share this script.
|
||||
|
||||
local M = {}
|
||||
|
||||
@@ -16,29 +12,6 @@ local function push(game, ow, npc, done, text)
|
||||
game.stack:push(TextBox.new(game, text, done))
|
||||
end
|
||||
|
||||
-- CeruleanCitySlowbroText / CeruleanCityElectrodeText
|
||||
-- cp 180 -> 76/256 chance of 1st; cp 120 -> 60/256 chance of 2nd;
|
||||
-- cp 60 -> 60/256 chance of 3rd; else 60/256 chance of 4th.
|
||||
local function talkMon(game, ow, npc, done)
|
||||
local t = game.data.text
|
||||
local roll = math.random(0, 255)
|
||||
local text
|
||||
if roll >= 180 then
|
||||
text = t._CeruleanCitySlowbroTookASnoozeText
|
||||
or t._CeruleanCityElectrodeTookASnoozeText
|
||||
elseif roll >= 120 then
|
||||
text = t._CeruleanCitySlowbroIsLoafingAroundText
|
||||
or t._CeruleanCityElectrodeIsLoafingAroundText
|
||||
elseif roll >= 60 then
|
||||
text = t._CeruleanCitySlowbroTurnedAwayText
|
||||
or t._CeruleanCityElectrodeTurnedAwayText
|
||||
else
|
||||
text = t._CeruleanCitySlowbroIgnoredOrdersText
|
||||
or t._CeruleanCityElectrodeIgnoredOrdersText
|
||||
end
|
||||
push(game, ow, npc, done, text)
|
||||
end
|
||||
|
||||
M.CERULEAN_CITY = {
|
||||
talk = {
|
||||
-- CeruleanCityCooltrainerF1Text (scripts/CeruleanCity.asm:362-393)
|
||||
@@ -50,20 +23,32 @@ M.CERULEAN_CITY = {
|
||||
local text
|
||||
if roll >= 180 then
|
||||
text = t._CeruleanCityCooltrainerF1SlowbroUseSonicboomText
|
||||
or t._CeruleanCityCooltrainerF1ElectrodeUseSonicboomText
|
||||
elseif roll >= 100 then
|
||||
text = t._CeruleanCityCooltrainerF1SlowbroPunchText
|
||||
or t._CeruleanCityCooltrainerF1ElectrodePunchText
|
||||
else
|
||||
text = t._CeruleanCityCooltrainerF1SlowbroWithdrawText
|
||||
or t._CeruleanCityCooltrainerF1ElectrodeWithdrawText
|
||||
end
|
||||
push(game, ow, npc, done, text)
|
||||
end,
|
||||
|
||||
TEXT_CERULEANCITY_SLOWBRO = talkMon,
|
||||
-- Yellow map object uses TEXT_CERULEANCITY_ELECTRODE
|
||||
TEXT_CERULEANCITY_ELECTRODE = talkMon,
|
||||
-- CeruleanCitySlowbroText (scripts/CeruleanCity.asm:395-436)
|
||||
-- cp 180 -> 76/256 chance of 1st; cp 120 -> 60/256 chance of 2nd;
|
||||
-- cp 60 -> 60/256 chance of 3rd; else 60/256 chance of 4th.
|
||||
TEXT_CERULEANCITY_SLOWBRO = function(game, ow, npc, done)
|
||||
local t = game.data.text
|
||||
local roll = math.random(0, 255)
|
||||
local text
|
||||
if roll >= 180 then
|
||||
text = t._CeruleanCitySlowbroTookASnoozeText
|
||||
elseif roll >= 120 then
|
||||
text = t._CeruleanCitySlowbroIsLoafingAroundText
|
||||
elseif roll >= 60 then
|
||||
text = t._CeruleanCitySlowbroTurnedAwayText
|
||||
else
|
||||
text = t._CeruleanCitySlowbroIgnoredOrdersText
|
||||
end
|
||||
push(game, ow, npc, done, text)
|
||||
end,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -14,9 +14,9 @@ return {
|
||||
-- else -> .WhatsLostIsLostText (player has TM_DIG)
|
||||
TEXT_CERULEANTRASHEDHOUSE_FISHING_GURU = {
|
||||
{ "check_item", "TM_DIG" },
|
||||
{ "jump_if_true", 5 },
|
||||
{ "jump_if_true", 4 },
|
||||
{ "show_text", "_CeruleanTrashedHouseFishingGuruTheyStoleATMText" },
|
||||
{ "jump", "end" },
|
||||
{ "jump", 5 },
|
||||
{ "show_text", "_CeruleanTrashedHouseFishingGuruWhatsLostIsLostText" },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
-- Flavor talk scripts for CopycatsHouse2F (registry id COPYCATS_HOUSE_2F).
|
||||
-- Source: pokered/scripts/CopycatsHouse2F.asm, pokered/text/CopycatsHouse2F.asm
|
||||
--
|
||||
-- TEXT_COPYCATSHOUSE2F_COPYCAT (POKE DOLL -> TM31 MIMIC) lives in
|
||||
-- data/scripts/story4.lua.
|
||||
-- TEXT_COPYCATSHOUSE2F_COPYCAT is already ported (with the POKE DOLL /
|
||||
-- TM31 MIMIC trade) in data/scripts/story4.lua, so it is intentionally
|
||||
-- omitted here.
|
||||
|
||||
return {
|
||||
COPYCATS_HOUSE_2F = {
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
-- Fuchsia City (pokered/scripts/FuchsiaCity.asm)
|
||||
--
|
||||
-- The exhibit signs are text_asm bodies: PrintText of a single text_far,
|
||||
-- then DisplayPokedex for the exhibited species. The preview marks the
|
||||
-- species seen but not owned, same as the S.S. Anne passenger's Snorlax.
|
||||
--
|
||||
-- The fossil sign branches on the Mt. Moon fossil events. The exhibit
|
||||
-- holds the fossil the player did NOT take: taking the Dome Fossil puts
|
||||
-- Omanyte on display, taking the Helix Fossil puts Kabuto. With neither
|
||||
-- event set the sign only prints its undetermined line and no dex entry
|
||||
-- opens.
|
||||
--
|
||||
-- The other text pointers (city sign, Safari Game signs, mart/center/gym
|
||||
-- and warden signs, the four NPCs, the exhibited-mon FuchsiaCityPokemonText
|
||||
-- rows) are plain text_far wrappers that resolve through Data:resolveText,
|
||||
-- so they are not ported here.
|
||||
return {
|
||||
FUCHSIA_CITY = {
|
||||
talk = {
|
||||
-- FuchsiaCityChanseySignText: PrintText(_FuchsiaCityChanseySignText),
|
||||
-- then DisplayPokedex CHANSEY.
|
||||
TEXT_FUCHSIACITY_CHANSEY_SIGN = {
|
||||
{ "show_text", "_FuchsiaCityChanseySignText" },
|
||||
{ "mark_seen", "CHANSEY" },
|
||||
{ "push_screen", "DexEntryMenu", "CHANSEY" },
|
||||
},
|
||||
|
||||
-- FuchsiaCityVoltorbSignText: PrintText(_FuchsiaCityVoltorbSignText),
|
||||
-- then DisplayPokedex VOLTORB.
|
||||
TEXT_FUCHSIACITY_VOLTORB_SIGN = {
|
||||
{ "show_text", "_FuchsiaCityVoltorbSignText" },
|
||||
{ "mark_seen", "VOLTORB" },
|
||||
{ "push_screen", "DexEntryMenu", "VOLTORB" },
|
||||
},
|
||||
|
||||
-- FuchsiaCityKangaskhanSignText: PrintText(_FuchsiaCityKangaskhanSignText),
|
||||
-- then DisplayPokedex KANGASKHAN.
|
||||
TEXT_FUCHSIACITY_KANGASKHAN_SIGN = {
|
||||
{ "show_text", "_FuchsiaCityKangaskhanSignText" },
|
||||
{ "mark_seen", "KANGASKHAN" },
|
||||
{ "push_screen", "DexEntryMenu", "KANGASKHAN" },
|
||||
},
|
||||
|
||||
-- FuchsiaCitySlowpokeSignText: PrintText(_FuchsiaCitySlowpokeSignText),
|
||||
-- then DisplayPokedex SLOWPOKE.
|
||||
TEXT_FUCHSIACITY_SLOWPOKE_SIGN = {
|
||||
{ "show_text", "_FuchsiaCitySlowpokeSignText" },
|
||||
{ "mark_seen", "SLOWPOKE" },
|
||||
{ "push_screen", "DexEntryMenu", "SLOWPOKE" },
|
||||
},
|
||||
|
||||
-- FuchsiaCityLaprasSignText: PrintText(_FuchsiaCityLaprasSignText),
|
||||
-- then DisplayPokedex LAPRAS.
|
||||
TEXT_FUCHSIACITY_LAPRAS_SIGN = {
|
||||
{ "show_text", "_FuchsiaCityLaprasSignText" },
|
||||
{ "mark_seen", "LAPRAS" },
|
||||
{ "push_screen", "DexEntryMenu", "LAPRAS" },
|
||||
},
|
||||
|
||||
-- FuchsiaCityFossilSignText: CheckEvent EVENT_GOT_DOME_FOSSIL /
|
||||
-- CheckEventReuseA EVENT_GOT_HELIX_FOSSIL pick the text and the
|
||||
-- displayed entry; with neither set only the undetermined line prints.
|
||||
TEXT_FUCHSIACITY_FOSSIL_SIGN = {
|
||||
{ "check_flag", "EVENT_GOT_DOME_FOSSIL" }, -- 1
|
||||
{ "jump_if_true", 7 }, -- 2
|
||||
{ "check_flag", "EVENT_GOT_HELIX_FOSSIL" }, -- 3
|
||||
{ "jump_if_true", 11 }, -- 4
|
||||
{ "show_text", "_FuchsiaCityFossilSignUndeterminedText" }, -- 5
|
||||
{ "jump", "end" }, -- 6
|
||||
{ "show_text", "_FuchsiaCityFossilSignOmanyteText" }, -- 7
|
||||
{ "mark_seen", "OMANYTE" }, -- 8
|
||||
{ "push_screen", "DexEntryMenu", "OMANYTE" }, -- 9
|
||||
{ "jump", "end" }, -- 10
|
||||
{ "show_text", "_FuchsiaCityFossilSignKabutoText" }, -- 11
|
||||
{ "mark_seen", "KABUTO" }, -- 12
|
||||
{ "push_screen", "DexEntryMenu", "KABUTO" }, -- 13
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -10,10 +10,10 @@
|
||||
local function coinGiver(opts)
|
||||
return function(game, ow, npc, done)
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local Sound = require("src.core.Sound")
|
||||
local t = game.data.text
|
||||
local function push(label, fallback, onDone, popts)
|
||||
game.stack:push(TextBox.new(game, t[label] or fallback, onDone or done,
|
||||
popts))
|
||||
local function push(label, fallback, onDone)
|
||||
game.stack:push(TextBox.new(game, t[label] or fallback, onDone or done))
|
||||
end
|
||||
if game.save.flags[opts.event] then
|
||||
push(opts.alreadyGotLabel, opts.alreadyGotFallback)
|
||||
@@ -30,10 +30,9 @@ local function coinGiver(opts)
|
||||
end
|
||||
game.save.coins = math.min(9999, (game.save.coins or 0) + opts.amount)
|
||||
game.save.flags[opts.event] = true
|
||||
-- the ReceivedNCoinsText strings carry sound_get_item_1
|
||||
Sound.play(game.data, "Get_Item1")
|
||||
push(opts.receivedLabel,
|
||||
("{PLAYER} received\n%d coins!"):format(opts.amount), nil,
|
||||
TextBox.soundOpts(game, "Get_Item1"))
|
||||
("{PLAYER} received\n%d coins!"):format(opts.amount))
|
||||
end)
|
||||
end
|
||||
end
|
||||
@@ -86,47 +85,6 @@ return {
|
||||
alreadyGotLabel = "_GameCornerGentlemanCloselyWatchTheReelsText",
|
||||
alreadyGotFallback = "The trick is to\nwatch the reels\vclosely!",
|
||||
}),
|
||||
|
||||
-- Yellow keeps all three giveaways with the same events and amounts
|
||||
-- but renames the objects and their text labels: FISHING_GURU ->
|
||||
-- FISHING_GURU1, CLERK2 -> MIDDLE_AGED_MAN2, GENTLEMAN ->
|
||||
-- FISHING_GURU2 (pokeyellow/scripts/GameCorner.asm). The Red/Blue
|
||||
-- keys above never match there, so Yellow needs its own three (#552).
|
||||
TEXT_GAMECORNER_FISHING_GURU1 = coinGiver({
|
||||
event = "EVENT_GOT_10_COINS",
|
||||
amount = 10,
|
||||
askLabel = "_GameCornerFishingGuru1WantToPlayText",
|
||||
askFallback = "Kid, do you want\nto play?",
|
||||
receivedLabel = "_GameCornerFishingGuru1Received10CoinsText",
|
||||
coinCaseFullLabel = "_GameCornerFishingGuru1DontNeedMyCoinsText",
|
||||
coinCaseFullFallback = "You don't need my\ncoins!",
|
||||
alreadyGotLabel = "_GameCornerFishingGuru1WinsComeAndGoText",
|
||||
alreadyGotFallback = "Wins seem to come\nand go.",
|
||||
}),
|
||||
|
||||
TEXT_GAMECORNER_MIDDLE_AGED_MAN2 = coinGiver({
|
||||
event = "EVENT_GOT_20_COINS_2",
|
||||
amount = 20,
|
||||
askLabel = "_GameCornerMiddleAgedMan2WantSomeCoinsText",
|
||||
askFallback = "What's up? Want\nsome coins?",
|
||||
receivedLabel = "_GameCornerMiddleAgedMan2Received20CoinsText",
|
||||
coinCaseFullLabel = "_GameCornerMiddleAgedMan2YouHaveLotsOfCoinsText",
|
||||
coinCaseFullFallback = "You have lots of\ncoins!",
|
||||
alreadyGotLabel = "_GameCornerMiddleAgedMan2INeedMoreCoinsText",
|
||||
alreadyGotFallback = "Darn! I need more\ncoins for the\vPOKéMON I want!",
|
||||
}),
|
||||
|
||||
TEXT_GAMECORNER_FISHING_GURU2 = coinGiver({
|
||||
event = "EVENT_GOT_20_COINS",
|
||||
amount = 20,
|
||||
askLabel = "_GameCornerFishingGuru2ThrowingMeOffText",
|
||||
askFallback = "Hey, what? You're\nthrowing me off!\vHere are some\vcoins, shoo!",
|
||||
receivedLabel = "_GameCornerFishingGuru2Received20CoinsText",
|
||||
coinCaseFullLabel = "_GameCornerFishingGuru2YouGotYourOwnCoinsText",
|
||||
coinCaseFullFallback = "You've got your\nown coins!",
|
||||
alreadyGotLabel = "_GameCornerFishingGuru2CloselyWatchTheReelsText",
|
||||
alreadyGotFallback = "The trick is to\nwatch the reels\vclosely!",
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@ return {
|
||||
TEXT_LAVENDERMART_COOLTRAINER_M = {
|
||||
{ "face_player" },
|
||||
{ "check_flag", "EVENT_RESCUED_MR_FUJI" },
|
||||
{ "jump_if_true", 6 },
|
||||
{ "jump_if_true", 5 },
|
||||
{ "show_text", "_LavenderMartCooltrainerMReviveText" },
|
||||
{ "jump", "end" },
|
||||
{ "jump", 6 },
|
||||
{ "show_text", "_LavenderMartCooltrainerMNuggetText" },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -13,9 +13,9 @@ M.MR_FUJIS_HOUSE = {
|
||||
TEXT_MRFUJISHOUSE_SUPER_NERD = {
|
||||
{ "face_player" }, -- 1
|
||||
{ "check_flag", "EVENT_RESCUED_MR_FUJI" }, -- 2
|
||||
{ "jump_if_true", 6 }, -- 3
|
||||
{ "jump_if_true", 5 }, -- 3
|
||||
{ "show_text", "_MrFujisHouseSuperNerdMrFujiIsntHereText" }, -- 4
|
||||
{ "jump", "end" }, -- 5
|
||||
{ "jump", 6 }, -- 5
|
||||
{ "show_text", "_MrFujisHouseSuperNerdMrFujiHadBeenPrayingText" }, -- 6
|
||||
},
|
||||
|
||||
@@ -25,9 +25,9 @@ M.MR_FUJIS_HOUSE = {
|
||||
TEXT_MRFUJISHOUSE_LITTLE_GIRL = {
|
||||
{ "face_player" }, -- 1
|
||||
{ "check_flag", "EVENT_RESCUED_MR_FUJI" }, -- 2
|
||||
{ "jump_if_true", 6 }, -- 3
|
||||
{ "jump_if_true", 5 }, -- 3
|
||||
{ "show_text", "_MrFujisHouseLittleGirlThisIsMrFujisHouseText" }, -- 4
|
||||
{ "jump", "end" }, -- 5
|
||||
{ "jump", 6 }, -- 5
|
||||
{ "show_text", "_MrFujisHouseLittleGirlPokemonAreNiceToHugText" }, -- 6
|
||||
},
|
||||
|
||||
|
||||
@@ -22,8 +22,8 @@ return {
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local Commands = require("src.script.Commands")
|
||||
local t = game.data.text
|
||||
local function say(label, cb, sopts)
|
||||
game.stack:push(TextBox.new(game, t[label] or label, cb, sopts))
|
||||
local function say(label, cb)
|
||||
game.stack:push(TextBox.new(game, t[label] or label, cb))
|
||||
end
|
||||
|
||||
if game.save.flags.EVENT_GOT_OLD_AMBER then
|
||||
@@ -39,9 +39,8 @@ return {
|
||||
game.save.flags.EVENT_GOT_OLD_AMBER = true
|
||||
Commands.hide_object({ save = game.save, overworld = ow, game = game },
|
||||
"MUSEUM_1F", "MUSEUM1F_OLD_AMBER")
|
||||
-- .ReceivedOldAmberText carries sound_get_item_1
|
||||
say("_Museum1FScientist2ReceivedOldAmberText", done,
|
||||
TextBox.soundOpts(game, "Get_Item1"))
|
||||
require("src.core.Sound").play(game.data, "Get_Item1")
|
||||
say("_Museum1FScientist2ReceivedOldAmberText", done)
|
||||
end)
|
||||
end,
|
||||
|
||||
|
||||
@@ -1,39 +1,13 @@
|
||||
-- Hand-ported OAKS_LAB flavor: the simple talk texts (scripts/OaksLab.asm;
|
||||
-- OAK1, the starter balls and RIVAL live in data/scripts/oaks_lab.lua).
|
||||
|
||||
local TextBox = require("src.render.TextBox")
|
||||
-- Hand-ported flavor text for OaksLab (registry id OAKS_LAB).
|
||||
-- Source: pokered/scripts/OaksLab.asm. These five text_asm bodies are
|
||||
-- all simple "PrintText; jp TextScriptEnd" -- no flag branches, no
|
||||
-- YES/NO menu -- so a one-row talk script showing the real extracted
|
||||
-- text is a faithful port. (The rest of OaksLab.asm's TEXT_OAKSLAB_*
|
||||
-- constants -- OAK1, the three starter poke balls, RIVAL -- are already
|
||||
-- ported with full branching logic in data/scripts/oaks_lab.lua.)
|
||||
|
||||
return {
|
||||
OAKS_LAB = {
|
||||
-- data/events/hidden_events.asm:147
|
||||
onInteract = function(game, ow, fx, fy)
|
||||
local t = game.data.text or {}
|
||||
-- engine/events/hidden_events/oaks_lab_posters.asm:1
|
||||
if fy == 0 and fx == 4 then
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._PushStartText or "Push START to\nopen the MENU!"))
|
||||
return true
|
||||
end
|
||||
if fy == 0 and fx == 5 then
|
||||
local owned = 0
|
||||
for _ in pairs(game.save.pokedex.owned or {}) do owned = owned + 1 end
|
||||
game.stack:push(TextBox.new(game,
|
||||
owned >= 2
|
||||
and (t._StrengthsAndWeaknessesText
|
||||
or "All POKéMON types\nhave strong and\vweak points\vagainst others.")
|
||||
or (t._SaveOptionText
|
||||
or "The SAVE option is\non the MENU\vscreen.")))
|
||||
return true
|
||||
end
|
||||
-- engine/events/hidden_events/oaks_lab_email.asm:1
|
||||
if fy == 1 and (fx == 0 or fx == 1) then
|
||||
if ow.player.facing ~= "up" then return false end
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._OakLabEmailText or "There's an e-mail\nmessage here!"))
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end,
|
||||
talk = {
|
||||
-- OaksLabGirlText (scripts/OaksLab.asm)
|
||||
TEXT_OAKSLAB_GIRL = {
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
-- guide and SUPER_NERD2 garden nerd.
|
||||
--
|
||||
-- The YOUNGSTER's gym escort (talk + east-exit onStep) lives in
|
||||
-- story5.lua; SUPER_NERD1's museum escort (scripts/PewterCity.asm:47-113)
|
||||
-- is below.
|
||||
-- story5.lua so the lockstep RLE walk is not overwritten by this
|
||||
-- flavor merge. SUPER_NERD1's museum escort is not ported; only the
|
||||
-- YES/NO-branched flavor text is here.
|
||||
|
||||
local M = {}
|
||||
|
||||
@@ -15,147 +16,16 @@ local function push(game, s, done)
|
||||
game.stack:push(TextBox.new(game, s, done))
|
||||
end
|
||||
|
||||
-- PrintText on a text_end string returns with the box still drawn and
|
||||
-- YesNoChoice then draws the menu above it (InitYesNoTextBoxParameters,
|
||||
-- engine/menus/text_box.asm); no A press clears the question first. Ride
|
||||
-- TextBox's opts.choice, the same as Commands.ask (#854).
|
||||
local function ask(game, s, cb)
|
||||
local TextBox = require("src.render.TextBox")
|
||||
game.stack:push(TextBox.new(game, s, nil, { choice = cb }))
|
||||
end
|
||||
|
||||
-- RLEList_PewterMuseumGuy (engine/overworld/auto_movement.asm:199-204)
|
||||
local museumGuySteps = {
|
||||
"up", "up", "up", "up", "up", "up",
|
||||
"left", "left", "left", "left", "left", "left", "left", "left",
|
||||
"left", "left", "left", "left", "left",
|
||||
"up", "up", "up",
|
||||
"left",
|
||||
}
|
||||
|
||||
-- RLEList_PewterMuseumPlayer (engine/overworld/auto_movement.asm:192-197)
|
||||
local museumPlayerRle = {
|
||||
"NO",
|
||||
"up", "up", "up",
|
||||
"left", "left", "left", "left", "left", "left", "left", "left",
|
||||
"left", "left", "left", "left", "left",
|
||||
"up", "up", "up", "up", "up", "up",
|
||||
}
|
||||
|
||||
-- PewterMuseumGuyCoords (engine/events/pewter_guys.asm:58-75)
|
||||
local museumPreambles = {
|
||||
["27,18"] = { "up", "up" },
|
||||
["27,16"] = { "right", "left" },
|
||||
["26,17"] = { "up", "right" },
|
||||
["28,17"] = { "up", "left" },
|
||||
}
|
||||
|
||||
-- PewterGuys (engine/events/pewter_guys.asm:1-49), same transform as
|
||||
-- pewterEscort.playerPlan in story5.lua
|
||||
local function museumPlan(x, y)
|
||||
local pre = museumPreambles[x .. "," .. y]
|
||||
if not pre then return nil end
|
||||
local buf = {}
|
||||
for i, d in ipairs(museumPlayerRle) do buf[i] = d end
|
||||
buf[#buf] = pre[1]
|
||||
for i = 2, #pre do buf[#buf + 1] = pre[i] end
|
||||
local path = {}
|
||||
for i = #buf, 1, -1 do path[#path + 1] = buf[i] end
|
||||
local head = 0
|
||||
while path[head + 1] == "NO" do head = head + 1 end
|
||||
local tail = #path
|
||||
while tail > head and path[tail] == "NO" do tail = tail - 1 end
|
||||
local steps = {}
|
||||
for i = head + 1, tail do steps[#steps + 1] = path[i] end
|
||||
return { steps = steps, guyHeadStart = math.floor(head / 8) }
|
||||
end
|
||||
|
||||
-- PewterCitySuperNerd1ShowsPlayerMuseumScript (scripts/PewterCity.asm:47-113)
|
||||
local function museumEscortWalk(game, ow)
|
||||
if ow.runner:isRunning() or #ow.scriptMoves > 0 then return false end
|
||||
local plan = museumPlan(ow.player.cellX, ow.player.cellY)
|
||||
if not plan then return false end
|
||||
local Music = require("src.core.Music")
|
||||
local t = text(game)
|
||||
local guy = ow:npcByIndex(3) -- PEWTERCITY_SUPER_NERD1
|
||||
local head = plan.guyHeadStart
|
||||
|
||||
-- SetSpritePosition2 + ShowObject back on his spawn (27,17), the same
|
||||
-- snap walkHome does in story5.lua (scripts/PewterCity.asm:102-113)
|
||||
local function walkOut()
|
||||
if not guy then return end
|
||||
local i = 0
|
||||
local function tick()
|
||||
i = i + 1
|
||||
if i > 4 then
|
||||
guy.cellX, guy.cellY = 27, 17
|
||||
guy.px, guy.py = 27 * 16, 17 * 16
|
||||
guy.moving = false
|
||||
guy.targetX, guy.targetY = nil, nil
|
||||
guy.facing = "down"
|
||||
return
|
||||
end
|
||||
ow:scriptMove(guy, "down", 1, tick)
|
||||
end
|
||||
tick()
|
||||
end
|
||||
|
||||
-- SetSpritePosition1 pins him beside the museum door (map (17,12) minus
|
||||
-- the +4 border offset = (13,8)), then MovementData_PewterMuseumGuyExit
|
||||
local function afterWalk()
|
||||
if guy then
|
||||
guy.stepFrames = nil
|
||||
guy.cellX, guy.cellY = 13, 8
|
||||
guy.px, guy.py = 13 * 16, 8 * 16
|
||||
guy.moving = false
|
||||
guy.targetX, guy.targetY = nil, nil
|
||||
guy.facing = "up"
|
||||
end
|
||||
Music.playMap(game.data, "PEWTER_CITY")
|
||||
push(game, t._PewterCitySuperNerd1ItsRightHereText
|
||||
or "It's right here!", walkOut)
|
||||
end
|
||||
|
||||
local function lockstep()
|
||||
local i = 0
|
||||
local function tick()
|
||||
i = i + 1
|
||||
local ps = plan.steps[i]
|
||||
if not ps then
|
||||
afterWalk()
|
||||
return
|
||||
end
|
||||
local gs = museumGuySteps[head + i]
|
||||
if guy and gs then ow:scriptMove(guy, gs, 1) end
|
||||
ow:scriptMove(ow.player, ps, 1, tick)
|
||||
end
|
||||
tick()
|
||||
end
|
||||
|
||||
-- engine/overworld/movement.asm:737 (DoScriptedNPCMovement)
|
||||
if guy then
|
||||
guy.stepFrames = ow.player.stepFramesCur or ow.player.stepFrames
|
||||
end
|
||||
Music.play(game.data, "Music_MuseumGuy")
|
||||
if guy and head > 0 then
|
||||
local h = 0
|
||||
local function headTick()
|
||||
h = h + 1
|
||||
if h > head then lockstep(); return end
|
||||
ow:scriptMove(guy, museumGuySteps[h], 1, headTick)
|
||||
end
|
||||
headTick()
|
||||
else
|
||||
lockstep()
|
||||
end
|
||||
return true
|
||||
local ChoiceBox = require("src.ui.ChoiceBox")
|
||||
push(game, s, function() game.stack:push(ChoiceBox.new(game, cb)) end)
|
||||
end
|
||||
|
||||
M.PEWTER_CITY = {
|
||||
museumEscort = { plan = museumPlan, guySteps = museumGuySteps },
|
||||
talk = {
|
||||
-- PewterCitySuperNerd1Text (scripts/PewterCity.asm:209-237): YES ->
|
||||
-- fossils comment, NO -> "you have to go" and the museum escort
|
||||
-- PewterCitySuperNerd1Text (scripts/PewterCity.asm): asks if you
|
||||
-- checked out the museum; YES -> fossils comment, NO -> "you have
|
||||
-- to go" (which in pokered also kicks off the escort script).
|
||||
TEXT_PEWTERCITY_SUPER_NERD1 = function(game, ow, npc, done)
|
||||
local t = text(game)
|
||||
ask(game, t._PewterCitySuperNerd1DidYouCheckOutMuseumText
|
||||
@@ -165,10 +35,7 @@ M.PEWTER_CITY = {
|
||||
or "Weren't those\nfossils from MT.\nMOON amazing?", done)
|
||||
else
|
||||
push(game, t._PewterCitySuperNerd1YouHaveToGoText
|
||||
or "Really?\nYou absolutely\nhave to go!", function()
|
||||
museumEscortWalk(game, ow)
|
||||
if done then done() end
|
||||
end)
|
||||
or "Really?\nYou absolutely\nhave to go!", done)
|
||||
end
|
||||
end)
|
||||
end,
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
-- pokered/scripts/PewterNidoranHouse.asm
|
||||
-- PewterNidoranHouseNidoranText: text_far _PewterNidoranHouseNidoranText, then
|
||||
-- text_asm plays the NIDORAN_M cry (PlayCry/WaitForSoundToFinish) before ending.
|
||||
-- play_cry only stashes ctx.pendingCry for the very next show_text row, so it
|
||||
-- has to sit immediately before that row (src/script/Commands.lua); the cry
|
||||
-- then sounds once the box has typed out, matching the asm's print-then-cry
|
||||
-- order. Its waitForButton form keeps DisplayTextID's trailing
|
||||
-- WaitForTextScrollButtonPress -- PewterNidoranHouse_Script is
|
||||
-- `jp EnableAutoTextBoxDrawing`, which zeroes
|
||||
-- wDoNotWaitForButtonPressAfterDisplayingText -- so the box still holds until
|
||||
-- A/B. The cry row was missing, leaving the NIDORAN silent (#247).
|
||||
-- No sound-effect command exists in Commands.lua, so we port the dialogue line
|
||||
-- only; the cry SFX is cosmetic and has no gameplay effect.
|
||||
return {
|
||||
PEWTER_NIDORAN_HOUSE = {
|
||||
talk = {
|
||||
TEXT_PEWTERNIDORANHOUSE_NIDORAN = {
|
||||
{ "face_player" },
|
||||
-- text_asm: ld a, NIDORAN_M / call PlayCry
|
||||
{ "play_cry", "NIDORAN_M", true },
|
||||
{ "show_text", "_PewterNidoranHouseNidoranText" },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
-- Red's House 2F (pokered data/events/hidden_events.asm +
|
||||
-- engine/events/hidden_objects PrintRedSNESText).
|
||||
--
|
||||
-- The bedroom SNES is a hidden_event at (3, 5) with ANY_FACING that
|
||||
-- prints _RedBedroomSNESText. field.py extracts OpenRedsPC on this map
|
||||
-- but skips PrintRedSNESText, so interaction was a no-op (#135).
|
||||
|
||||
local TextBox = require("src.render.TextBox")
|
||||
|
||||
return {
|
||||
REDS_HOUSE_2F = {
|
||||
-- hidden_events.asm:
|
||||
-- hidden_event 3, 5, PrintRedSNESText, ANY_FACING
|
||||
onInteract = function(game, ow, fx, fy)
|
||||
if fx ~= 3 or fy ~= 5 then return false end
|
||||
local text = game.data.text._RedBedroomSNESText
|
||||
or "{PLAYER} is\nplaying the SNES!"
|
||||
game.stack:push(TextBox.new(game, text))
|
||||
return true
|
||||
end,
|
||||
},
|
||||
}
|
||||
@@ -12,9 +12,9 @@ return {
|
||||
TEXT_ROUTE16GATE1F_GUARD = {
|
||||
{ "face_player" },
|
||||
{ "check_item", "BICYCLE" },
|
||||
{ "jump_if_true", 6 },
|
||||
{ "jump_if_true", 5 },
|
||||
{ "show_text", "_Route16Gate1FGuardNoPedestriansAllowedText" },
|
||||
{ "jump", "end" },
|
||||
{ "jump", 6 },
|
||||
{ "show_text", "_Route16Gate1FGuardCyclingRoadExplanationText" },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -14,9 +14,9 @@ return {
|
||||
TEXT_ROUTE18GATE1F_GUARD = {
|
||||
{ "face_player" },
|
||||
{ "check_item", "BICYCLE" },
|
||||
{ "jump_if_true", 6 },
|
||||
{ "jump_if_true", 5 },
|
||||
{ "show_text", "_Route18Gate1FGuardYouNeedABicycleText" },
|
||||
{ "jump", "end" },
|
||||
{ "jump", 6 },
|
||||
{ "show_text", "_Route18Gate1FGuardCyclingRoadUphillText" },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -28,29 +28,7 @@ local function badgeGuard(badge, passFlag)
|
||||
}
|
||||
end
|
||||
|
||||
-- Route23SetVictoryRoadBoulders (pokered scripts/Route23.asm:8): every entry
|
||||
-- to Route 23 resets the Victory Road boulder puzzle behind you. The 2F/3F
|
||||
-- switch events clear (so those barriers are closed again on the next floor
|
||||
-- load) and the boulder that fell through 3F's hole goes back upstairs:
|
||||
-- ShowObject TOGGLE_VICTORY_ROAD_3F_BOULDER / HideObject
|
||||
-- TOGGLE_VICTORY_ROAD_2F_BOULDER, which are VICTORYROAD3F_BOULDER4 and
|
||||
-- VICTORYROAD2F_BOULDER3 (data/maps/toggleable_objects.asm). 1F's event is
|
||||
-- reset by the Indigo Plateau lobby and by Victory Road 2F, not here. This
|
||||
-- is what makes the puzzle "completely reset" on a return trip (#258).
|
||||
-- onEnter is the port's BIT_CUR_MAP_LOADED_2 equivalent: setMap runs it on
|
||||
-- every map entry, connection crossings included, like EnterMap.
|
||||
M.ROUTE_23 = {
|
||||
onEnter = function(game, ow)
|
||||
local f = game.save.flags
|
||||
f.EVENT_VICTORY_ROAD_2_BOULDER_ON_SWITCH1 = nil
|
||||
f.EVENT_VICTORY_ROAD_2_BOULDER_ON_SWITCH2 = nil
|
||||
f.EVENT_VICTORY_ROAD_3_BOULDER_ON_SWITCH1 = nil
|
||||
f.EVENT_VICTORY_ROAD_3_BOULDER_ON_SWITCH2 = nil
|
||||
local Commands = require("src.script.Commands")
|
||||
local ctx = { save = game.save, overworld = ow, game = game }
|
||||
Commands.show_object(ctx, "VICTORY_ROAD_3F", "VICTORYROAD3F_BOULDER4")
|
||||
Commands.hide_object(ctx, "VICTORY_ROAD_2F", "VICTORYROAD2F_BOULDER3")
|
||||
end,
|
||||
talk = {
|
||||
-- Route23Guard1Text: EventFlagBit ..., EVENT_PASSED_EARTHBADGE_CHECK
|
||||
-- -> wWhichBadge = EARTHBADGE
|
||||
|
||||
@@ -12,9 +12,9 @@ return {
|
||||
TEXT_SILPHCO10F_SILPH_WORKER_F = {
|
||||
{ "face_player" },
|
||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||
{ "jump_if_true", 6 },
|
||||
{ "jump_if_true", 5 },
|
||||
{ "show_text", "_SilphCo10FSilphWorkerFImScaredText" },
|
||||
{ "jump", "end" },
|
||||
{ "jump", 6 },
|
||||
{ "show_text", "_SilphCo10FSilphWorkerFQuietAboutMyCryingText" },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -10,9 +10,9 @@ return {
|
||||
-- not set: _SilphCo3FSilphWorkerMWhatShouldIDoText
|
||||
TEXT_SILPHCO3F_SILPH_WORKER_M = {
|
||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||
{ "jump_if_true", 5 },
|
||||
{ "jump_if_true", 4 },
|
||||
{ "show_text", "_SilphCo3FSilphWorkerMWhatShouldIDoText" },
|
||||
{ "jump", "end" },
|
||||
{ "jump", 5 },
|
||||
{ "show_text", "_SilphCo3FSilphWorkerMYouSavedUsText" },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -7,9 +7,9 @@ return {
|
||||
TEXT_SILPHCO4F_SILPH_WORKER_M = {
|
||||
{"face_player"},
|
||||
{"check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI"},
|
||||
{"jump_if_true", 6},
|
||||
{"jump_if_true", 5},
|
||||
{"show_text", "_SilphCo4FSilphWorkerMImHidingText"},
|
||||
{"jump", "end"},
|
||||
{"jump", 6},
|
||||
{"show_text", "_SilphCo4FSilphWorkerMTeamRocketIsGoneText"},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -7,9 +7,9 @@ return {
|
||||
TEXT_SILPHCO5F_SILPH_WORKER_M = {
|
||||
{"face_player"},
|
||||
{"check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI"},
|
||||
{"jump_if_true", 6},
|
||||
{"jump_if_true", 5},
|
||||
{"show_text", "_SilphCo5FSilphWorkerMThatsYouRightText"},
|
||||
{"jump", "end"},
|
||||
{"jump", 6},
|
||||
{"show_text", "_SilphCo5FSilphWorkerMYoureOurHeroText"},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -11,9 +11,9 @@ return {
|
||||
TEXT_SILPHCO6F_SILPH_WORKER_M1 = {
|
||||
{ "face_player" },
|
||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||
{ "jump_if_true", 6 },
|
||||
{ "jump_if_true", 5 },
|
||||
{ "show_text", "_SilphCo6FSilphWorkerM1TookOverTheBuildingText" },
|
||||
{ "jump", "end" },
|
||||
{ "jump", 6 },
|
||||
{ "show_text", "_SilphCo6FSilphWorkerM1BackToWorkText" },
|
||||
},
|
||||
|
||||
@@ -21,9 +21,9 @@ return {
|
||||
TEXT_SILPHCO6F_SILPH_WORKER_M2 = {
|
||||
{ "face_player" },
|
||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||
{ "jump_if_true", 6 },
|
||||
{ "jump_if_true", 5 },
|
||||
{ "show_text", "_SilphCo6FSilphWorkerMHelpMePleaseText" },
|
||||
{ "jump", "end" },
|
||||
{ "jump", 6 },
|
||||
{ "show_text", "_SilphCo6FSilphWorkerMWeGotEngagedText" },
|
||||
},
|
||||
|
||||
@@ -31,9 +31,9 @@ return {
|
||||
TEXT_SILPHCO6F_SILPH_WORKER_F1 = {
|
||||
{ "face_player" },
|
||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||
{ "jump_if_true", 6 },
|
||||
{ "jump_if_true", 5 },
|
||||
{ "show_text", "_SilphCo6FSilphWorkerF1SuchACowardText" },
|
||||
{ "jump", "end" },
|
||||
{ "jump", 6 },
|
||||
{ "show_text", "_SilphCo6FSilphWorkerF1HaveToMarryHimText" },
|
||||
},
|
||||
|
||||
@@ -41,9 +41,9 @@ return {
|
||||
TEXT_SILPHCO6F_SILPH_WORKER_F2 = {
|
||||
{ "face_player" },
|
||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||
{ "jump_if_true", 6 },
|
||||
{ "jump_if_true", 5 },
|
||||
{ "show_text", "_SilphCo6FSilphWorkerF2TeamRocketConquerWorldText" },
|
||||
{ "jump", "end" },
|
||||
{ "jump", 6 },
|
||||
{ "show_text", "_SilphCo6FSilphWorkerF2TeamRocketRanText" },
|
||||
},
|
||||
|
||||
@@ -51,9 +51,9 @@ return {
|
||||
TEXT_SILPHCO6F_SILPH_WORKER_M3 = {
|
||||
{ "face_player" },
|
||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||
{ "jump_if_true", 6 },
|
||||
{ "jump_if_true", 5 },
|
||||
{ "show_text", "_SilphCo6FSilphWorkerM3TargetedSilphText" },
|
||||
{ "jump", "end" },
|
||||
{ "jump", 6 },
|
||||
{ "show_text", "_SilphCo6FSilphWorkerM3WorkForSilphText" },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -10,9 +10,9 @@ return {
|
||||
-- set: _SilphCo7FSilphWorkerM2CancelledMasterBallText
|
||||
TEXT_SILPHCO7F_SILPH_WORKER_M2 = {
|
||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||
{ "jump_if_true", 5 },
|
||||
{ "jump_if_true", 4 },
|
||||
{ "show_text", "_SilphCo7FSilphWorkerM2AfterTheMasterBallText" },
|
||||
{ "jump", "end" },
|
||||
{ "jump", 5 },
|
||||
{ "show_text", "_SilphCo7FSilphWorkerM2CancelledMasterBallText" },
|
||||
},
|
||||
|
||||
@@ -22,9 +22,9 @@ return {
|
||||
-- set: _SilphCo7FSilphWorkerM3YouChasedOffTeamRocketText
|
||||
TEXT_SILPHCO7F_SILPH_WORKER_M3 = {
|
||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||
{ "jump_if_true", 5 },
|
||||
{ "jump_if_true", 4 },
|
||||
{ "show_text", "_SilphCo7FSilphWorkerM3ItWouldBeBadText" },
|
||||
{ "jump", "end" },
|
||||
{ "jump", 5 },
|
||||
{ "show_text", "_SilphCo7FSilphWorkerM3YouChasedOffTeamRocketText" },
|
||||
},
|
||||
|
||||
@@ -34,9 +34,9 @@ return {
|
||||
-- set: _SilphCo7FSilphWorkerM4SafeAtLastText
|
||||
TEXT_SILPHCO7F_SILPH_WORKER_M4 = {
|
||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||
{ "jump_if_true", 5 },
|
||||
{ "jump_if_true", 4 },
|
||||
{ "show_text", "_SilphCo7FSilphWorkerM4ItsReallyDangerousHereText" },
|
||||
{ "jump", "end" },
|
||||
{ "jump", 5 },
|
||||
{ "show_text", "_SilphCo7FSilphWorkerM4SafeAtLastText" },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -10,9 +10,9 @@ return {
|
||||
-- set: _SilphCo8FSilphWorkerMThanksForSavingUsText
|
||||
TEXT_SILPHCO8F_SILPH_WORKER_M = {
|
||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||
{ "jump_if_true", 5 },
|
||||
{ "jump_if_true", 4 },
|
||||
{ "show_text", "_SilphCo8FSilphWorkerMSilphIsFinishedText" },
|
||||
{ "jump", "end" },
|
||||
{ "jump", 5 },
|
||||
{ "show_text", "_SilphCo8FSilphWorkerMThanksForSavingUsText" },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -7,19 +7,19 @@
|
||||
-- CheckFightingMapTrainers/TalkToTrainer via the existing trainer
|
||||
-- system, and the item pickups carry no talk text of their own here.
|
||||
--
|
||||
-- TEXT_SSANNE2FROOMS_GENTLEMAN3 opens SNORLAX's POKéDEX entry after its
|
||||
-- text box, like DisplayPokedex in the original. The preview marks it seen
|
||||
-- but does not mark it owned.
|
||||
-- TEXT_SSANNE2FROOMS_GENTLEMAN3 also opens the POKéDEX entry for
|
||||
-- SNORLAX after the text box (DisplayPokedex in the original), which
|
||||
-- has no equivalent talk-script hook in this port (DexEntryMenu has
|
||||
-- no done-callback the way other pushed UI states do) -- only the
|
||||
-- flavor line is ported here.
|
||||
return {
|
||||
SS_ANNE_2F_ROOMS = {
|
||||
talk = {
|
||||
-- SSAnne2FRoomsGentleman3Text: PrintText(_SSAnne2FRoomsGentleman3Text),
|
||||
-- then DisplayPokedex SNORLAX.
|
||||
-- SSAnne2FRoomsGentleman3Text: PrintText(_SSAnne2FRoomsGentleman3Text)
|
||||
-- (then DisplayPokedex SNORLAX -- not ported, see note above)
|
||||
TEXT_SSANNE2FROOMS_GENTLEMAN3 = {
|
||||
{ "face_player" },
|
||||
{ "show_text", "_SSAnne2FRoomsGentleman3Text" },
|
||||
{ "mark_seen", "SNORLAX" },
|
||||
{ "push_screen", "DexEntryMenu", "SNORLAX" },
|
||||
},
|
||||
|
||||
-- SSAnne2FRoomsGentleman4Text: PrintText(_SSAnne2FRoomsGentleman4Text)
|
||||
|
||||
@@ -23,13 +23,9 @@ local function push(game, s, done)
|
||||
game.stack:push(TextBox.new(game, s, done))
|
||||
end
|
||||
|
||||
-- PrintText on a text_end string returns with the box still drawn and
|
||||
-- YesNoChoice then draws the menu above it (InitYesNoTextBoxParameters,
|
||||
-- engine/menus/text_box.asm); no A press clears the question first. Ride
|
||||
-- TextBox's opts.choice, the same as Commands.ask (#854).
|
||||
local function ask(game, s, cb)
|
||||
local TextBox = require("src.render.TextBox")
|
||||
game.stack:push(TextBox.new(game, s, nil, { choice = cb }))
|
||||
local ChoiceBox = require("src.ui.ChoiceBox")
|
||||
push(game, s, function() game.stack:push(ChoiceBox.new(game, cb)) end)
|
||||
end
|
||||
|
||||
M.VIRIDIAN_CITY = {
|
||||
@@ -63,17 +59,13 @@ M.VIRIDIAN_CITY = {
|
||||
-- ViridianCityYoungster2CaterpieAndWeedleDescriptionText are
|
||||
-- defined without a leading underscore in pokered/text/ViridianCity.asm
|
||||
-- and aren't present in data/generated/text.lua, so we fall back to
|
||||
-- the literal strings from pokered. Those fallbacks have to carry the
|
||||
-- extractor's markers, not plain newlines: line -> \n, cont -> \v,
|
||||
-- para -> \f. Spelling cont/para as \n and \n\n put all six lines on
|
||||
-- one page with nothing to wait on, so the whole speech scrolled past
|
||||
-- without a button press (#250).
|
||||
-- the literal strings from pokered.
|
||||
TEXT_VIRIDIANCITY_YOUNGSTER2 = function(game, ow, npc, done)
|
||||
local t = text(game)
|
||||
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\nof caterpillar\nPOKéMON?", function(yes)
|
||||
if yes then
|
||||
push(game, "CATERPIE has no\npoison, but\vWEEDLE does.\fWatch out for its\nPOISON STING!", done)
|
||||
push(game, "CATERPIE has no\npoison, but\nWEEDLE does.\n\nWatch out for its\nPOISON STING!", done)
|
||||
else
|
||||
push(game, "Oh, OK then!", done)
|
||||
end
|
||||
|
||||
@@ -3,16 +3,12 @@
|
||||
return {
|
||||
VIRIDIAN_NICKNAME_HOUSE = {
|
||||
talk = {
|
||||
-- ViridianNicknameHouseSpearowText: text_asm that PrintTexts the
|
||||
-- line, then plays the SPEAROW cry (PlayCry/WaitForSoundToFinish)
|
||||
-- before TextScriptEnd. play_cry only arms the show_text row that
|
||||
-- follows it, so the cry sounds once the box has typed out; its
|
||||
-- waitForButton form keeps DisplayTextID's trailing
|
||||
-- WaitForTextScrollButtonPress, so the box then holds until A/B like
|
||||
-- any other NPC line rather than popping itself (#251).
|
||||
-- ViridianNicknameHouseSpearowText: text_asm that shows the text
|
||||
-- then plays a SPEAROW cry (PlayCry/WaitForSoundToFinish). The
|
||||
-- cry playback isn't in the talk-script command vocabulary, so
|
||||
-- only the flavor line is ported here.
|
||||
TEXT_VIRIDIANNICKNAMEHOUSE_SPEAROW = {
|
||||
{ "play_cry", "SPEAROW", true }, -- 1 PlayCry
|
||||
{ "show_text", "_ViridianNicknameHouseSpearowText" }, -- 2 PrintText
|
||||
{ "show_text", "_ViridianNicknameHouseSpearowText" },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,175 +0,0 @@
|
||||
-- Viridian School House's two readables (data/events/hidden_events.asm,
|
||||
-- hidden_events_for VIRIDIAN_SCHOOL_HOUSE):
|
||||
-- hidden_text_predef 3, 0 PrintBlackboardLinkCableText, ViridianSchoolBlackboard
|
||||
-- hidden_text_predef 3, 4 PrintNotebookText, ViridianSchoolNotebook
|
||||
-- tools/extract/field.py only parses `hidden_event` rows, so neither
|
||||
-- hidden_text_predef row reaches data/generated/field.lua and both tiles
|
||||
-- were dead A presses (#503). Same hook shape, and the same sibling asm
|
||||
-- file, as the Celadon roof house in data/scripts/celadon_eevee.lua (#391);
|
||||
-- hidden_text_predef spends the facing byte on the tx_pre id, so neither
|
||||
-- tile gates on facing.
|
||||
|
||||
local Font = require("src.render.Font")
|
||||
local Theme = require("src.ui.Theme")
|
||||
local TextBox = require("src.render.TextBox")
|
||||
|
||||
-- ViridianSchoolBlackboard (engine/events/hidden_events/school_blackboard.asm):
|
||||
-- StatusAilmentText1/2 are the two columns of the 12x8 box at the top left
|
||||
-- (hlcoord 0, 0 + `lb bc, 6, 10`); picking a status prints its
|
||||
-- ViridianBlackboardStatusPointers entry and jumps back to .blackboardLoop,
|
||||
-- QUIT or B falls through to .exitBlackboard.
|
||||
local STATUS_LABELS = {
|
||||
{ " SLP", "_ViridianBlackboardSleepText" },
|
||||
{ " PSN", "_ViridianBlackboardPoisonText" },
|
||||
{ " PAR", "_ViridianBlackboardPrlzText" },
|
||||
{ " BRN", "_ViridianBlackboardBurnText" },
|
||||
{ " FRZ", "_ViridianBlackboardFrozenText" },
|
||||
}
|
||||
|
||||
-- The headings list is a two-column menu, which src/ui/Menu.lua does not do
|
||||
-- (it stacks one column), so the layout lives here (#591). .blackboardLoop:
|
||||
-- TextBoxBorder at hlcoord 0, 0 with `lb bc, 6, 10` is the 12x8 box,
|
||||
-- StatusAilmentText1 (" SLP"/" PSN"/" PAR") is placed at hlcoord 1, 2 and
|
||||
-- StatusAilmentText2 (" BRN"/" FRZ"/" QUIT") at hlcoord 6, 2. LEFT/RIGHT
|
||||
-- move wTopMenuItemX between those two columns and swap wMenuItemOffset
|
||||
-- between 0 and 3 while leaving wCurrentMenuItem (the row) alone; UP/DOWN
|
||||
-- are not in wMenuWatchedKeys, so they only slide the cursor and loop.
|
||||
local BOARD_LABELS = {}
|
||||
for i, row in ipairs(STATUS_LABELS) do BOARD_LABELS[i] = row[1] end
|
||||
BOARD_LABELS[#BOARD_LABELS + 1] = " QUIT"
|
||||
local BOARD_COL_X = { 1, 6 }
|
||||
local BOARD_ROW_Y = 2
|
||||
local BOARD_ROWS = 3
|
||||
|
||||
local StatusBoard = {}
|
||||
StatusBoard.__index = StatusBoard
|
||||
|
||||
function StatusBoard.new(game, onPick, onQuit)
|
||||
return setmetatable({ game = game, col = 1, row = 1, labels = BOARD_LABELS,
|
||||
onPick = onPick, onQuit = onQuit }, StatusBoard)
|
||||
end
|
||||
|
||||
-- flat index = pokered's wMenuItemOffset (0 or 3) + wCurrentMenuItem (0..2),
|
||||
-- so 1..5 are the statuses in ViridianBlackboardStatusPointers order and 6
|
||||
-- is QUIT
|
||||
function StatusBoard:selection()
|
||||
return (self.col - 1) * BOARD_ROWS + self.row
|
||||
end
|
||||
|
||||
function StatusBoard:update()
|
||||
local input = self.game.input
|
||||
if input:wasPressed("up") then
|
||||
-- wMenuWrappingEnabled is never set here, so both ends are hard stops
|
||||
if self.row > 1 then self.row = self.row - 1 end
|
||||
elseif input:wasPressed("down") then
|
||||
if self.row < BOARD_ROWS then self.row = self.row + 1 end
|
||||
elseif input:wasPressed("left") then
|
||||
self.col = 1
|
||||
elseif input:wasPressed("right") then
|
||||
self.col = 2
|
||||
elseif input:wasPressed("a") or input:wasPressed("b") then
|
||||
-- HandleMenuInput_ (home/window.asm) beeps for the PAD_A | PAD_B branch,
|
||||
-- and B and QUIT share .exitBlackboard
|
||||
require("src.core.Sound").play(self.game.data, "Press_AB")
|
||||
local sel = self:selection()
|
||||
if input:wasPressed("b") or sel > #STATUS_LABELS then
|
||||
self.onQuit()
|
||||
else
|
||||
self.onPick(sel)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function StatusBoard:draw()
|
||||
Font.drawBox(0, 0, 12, 8)
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
for i, label in ipairs(BOARD_LABELS) do
|
||||
local col = i <= BOARD_ROWS and 1 or 2
|
||||
local row = i - (col - 1) * BOARD_ROWS
|
||||
Font.draw(label, BOARD_COL_X[col] * 8, (BOARD_ROW_Y + row - 1) * 8)
|
||||
end
|
||||
-- wTopMenuItemX equals the column PlaceString started at, so the cursor
|
||||
-- covers the blank each label leads with
|
||||
Font.drawCode(Theme.cursor, BOARD_COL_X[self.col] * 8,
|
||||
(BOARD_ROW_Y + self.row - 1) * 8)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end
|
||||
|
||||
local function blackboard(game)
|
||||
local text = game.data.text or {}
|
||||
local openBoard
|
||||
-- wCurrentMenuItem / wMenuItemOffset are zeroed once, above .blackboardLoop,
|
||||
-- and nothing inside the loop clears them again: after a status blurb
|
||||
-- `jp .blackboardLoop` comes back with the cursor still on the row and
|
||||
-- column the player just picked. One StatusBoard lives for the whole
|
||||
-- reading and is re-pushed each pass, so only entering the blackboard
|
||||
-- resets to the left column / top row (#591).
|
||||
local board
|
||||
-- .blackboardLoop reprints ViridianSchoolBlackboardText2 and only then
|
||||
-- calls HandleMenuInput, so the prompt is on screen for exactly as long as
|
||||
-- the headings list is. That text ends in `done`, not `prompt`
|
||||
-- (data/text/text_2.asm:646), so PrintText returns with the box still up
|
||||
-- and never waits for a button: TextBox opts.stay holds it open under the
|
||||
-- list and these callbacks pop the pair together (#591).
|
||||
local function closeBoard()
|
||||
game.stack:pop() -- the headings list
|
||||
game.stack:pop() -- the held "Which heading" box under it
|
||||
end
|
||||
local function pick(i)
|
||||
closeBoard()
|
||||
game.stack:push(TextBox.new(game,
|
||||
text[STATUS_LABELS[i][2]] or STATUS_LABELS[i][1], openBoard))
|
||||
end
|
||||
function openBoard()
|
||||
game.stack:push(TextBox.new(game,
|
||||
text._ViridianSchoolBlackboardText2 or "Which heading do\nyou want to read?",
|
||||
nil, { stay = { onShown = function()
|
||||
board = board or StatusBoard.new(game, pick, closeBoard)
|
||||
game.stack:push(board)
|
||||
end } }))
|
||||
end
|
||||
game.stack:push(TextBox.new(game,
|
||||
text._ViridianSchoolBlackboardText1
|
||||
or "The blackboard\ndescribes POKéMON\vSTATUS changes\vduring battles.",
|
||||
openBoard))
|
||||
end
|
||||
|
||||
-- ViridianSchoolNotebook (engine/events/hidden_events/school_notebooks.asm):
|
||||
-- pages 1-3 each end in TurnPageSchoolNotebook (TurnPageText + YesNoChoice)
|
||||
-- and NO stops the read; page 4 turns without asking and runs straight into
|
||||
-- page 5, the girl catching you at it.
|
||||
local function notebook(game)
|
||||
local text = game.data.text or {}
|
||||
local function page(n, after)
|
||||
return TextBox.new(game, text["_ViridianSchoolNotebookText" .. n] or "", after)
|
||||
end
|
||||
local function turnPage(nextPage)
|
||||
return function()
|
||||
game.stack:push(TextBox.new(game, text._TurnPageText or "Turn the page?",
|
||||
nil, { choice = function(yes)
|
||||
if yes then game.stack:push(nextPage()) end
|
||||
end }))
|
||||
end
|
||||
end
|
||||
local function page5() return page(5) end
|
||||
local function page4() return page(4, function() game.stack:push(page5()) end) end
|
||||
local function page3() return page(3, turnPage(page4)) end
|
||||
local function page2() return page(2, turnPage(page3)) end
|
||||
game.stack:push(page(1, turnPage(page2)))
|
||||
end
|
||||
|
||||
return {
|
||||
VIRIDIAN_SCHOOL_HOUSE = {
|
||||
onInteract = function(game, ow, fx, fy)
|
||||
if fx == 3 and fy == 0 then
|
||||
blackboard(game)
|
||||
return true
|
||||
end
|
||||
if fx == 3 and fy == 4 then
|
||||
notebook(game)
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end,
|
||||
},
|
||||
}
|
||||
@@ -15,7 +15,6 @@ local files = {
|
||||
"data.scripts.flavor.cerulean_trashed_house",
|
||||
"data.scripts.flavor.copycats_house_1f",
|
||||
"data.scripts.flavor.copycats_house_2f",
|
||||
"data.scripts.flavor.fuchsia_city",
|
||||
"data.scripts.flavor.game_corner",
|
||||
"data.scripts.flavor.lavender_cubone_house",
|
||||
"data.scripts.flavor.lavender_mart",
|
||||
@@ -29,7 +28,6 @@ local files = {
|
||||
"data.scripts.flavor.pokemon_fan_club",
|
||||
"data.scripts.flavor.power_plant",
|
||||
"data.scripts.flavor.reds_house_1f",
|
||||
"data.scripts.flavor.reds_house_2f",
|
||||
"data.scripts.flavor.route11_gate_2f",
|
||||
"data.scripts.flavor.route18_gate_2f",
|
||||
"data.scripts.flavor.route_12_gate_2f",
|
||||
@@ -61,7 +59,6 @@ local files = {
|
||||
"data.scripts.flavor.victory_road_2f",
|
||||
"data.scripts.flavor.viridian_city",
|
||||
"data.scripts.flavor.viridian_nickname_house",
|
||||
"data.scripts.flavor.viridian_school_house", -- #503
|
||||
"data.scripts.flavor.wardens_house",
|
||||
}
|
||||
|
||||
|
||||
@@ -18,26 +18,6 @@ local M = {
|
||||
VIRIDIAN_GYM = { city = "VIRIDIAN CITY", leader = "GIOVANNI", badge = "EARTHBADGE" },
|
||||
}
|
||||
|
||||
-- The originals' middle branch shared by every leader's text_asm: beaten
|
||||
-- but EVENT_GOT_TM* unset means the bag was full when the victory script
|
||||
-- ran GiveItem, so talking to the leader re-runs the ReceiveTM script.
|
||||
-- Returns true when the retry took over the talk. A save from before
|
||||
-- #797 already holds the TM without the flag; treat the owned TM as
|
||||
-- received so those saves fall through to the advice text instead of
|
||||
-- collecting a second copy.
|
||||
local function retryTmGive(game, ow, victoryKey, done)
|
||||
local reward = require("data.scripts.victories")[victoryKey]
|
||||
if not (reward and reward.gotFlag) then return false end
|
||||
if game.save.flags[reward.gotFlag] then return false end
|
||||
local owned = game.save.inventory and game.save.inventory[reward.item] or 0
|
||||
if owned > 0 then
|
||||
game.save.flags[reward.gotFlag] = true
|
||||
return false
|
||||
end
|
||||
ow:offerGymTm(reward, done)
|
||||
return true
|
||||
end
|
||||
|
||||
-- scripts/PewterGym.asm PewterGymBrockText (text_asm): CheckEvent
|
||||
-- EVENT_BEAT_BROCK branches his dialogue. Before the badge he prints
|
||||
-- _PewterGymBrockPreBattleText and engages the leader battle
|
||||
@@ -45,14 +25,12 @@ end
|
||||
-- badge/TM34 rewards and EVENT_BEAT_BROCK come from
|
||||
-- data/scripts/victories.lua OPP_BROCK#1). After the badge his
|
||||
-- .afterBeat branch prints _PewterGymBrockPostBattleAdviceText ("Go to
|
||||
-- the GYM in CERULEAN..."). The middle branch (beat but TM34 not yet
|
||||
-- handed over, CheckEventReuseA EVENT_GOT_TM34 -> call
|
||||
-- PewterGymScriptReceiveTM34) retries the TM give when the bag was full
|
||||
-- at the victory (#797).
|
||||
-- the GYM in CERULEAN..."). The original's middle branch (beat but
|
||||
-- TM34 not yet handed over, CheckEventReuseA EVENT_GOT_TM34) is
|
||||
-- unreachable in the port: the TM is granted with the victory.
|
||||
M.PEWTER_GYM.talk = {
|
||||
TEXT_PEWTERGYM_BROCK = function(game, ow, npc, done)
|
||||
if game.save.flags.EVENT_BEAT_BROCK then
|
||||
if retryTmGive(game, ow, "OPP_BROCK#1", done) then return end
|
||||
local TextBox = require("src.render.TextBox")
|
||||
game.stack:push(TextBox.new(game,
|
||||
game.data.text._PewterGymBrockPostBattleAdviceText
|
||||
@@ -70,17 +48,16 @@ M.PEWTER_GYM.talk = {
|
||||
-- (engageTrainer shows that same pre-battle text via resolveText; the
|
||||
-- badge/TM rewards and the beat flag come from data/scripts/victories.lua)
|
||||
-- -- and once beaten print the post-battle advice text. As with Brock,
|
||||
-- the middle branch (beaten but the TM not yet handed over,
|
||||
-- CheckEventReuseA EVENT_GOT_TM*) retries the TM give when the bag was
|
||||
-- full at the victory.
|
||||
-- the originals' middle branch (beaten but the TM not yet handed over,
|
||||
-- CheckEventReuseA EVENT_GOT_TM*) is unreachable in the port: the TM is
|
||||
-- granted with the victory.
|
||||
-- afterAdvice, when given, takes over `done`: it is handed (game, ow, npc,
|
||||
-- done) and must call done() itself once whatever it's doing (e.g. a fade
|
||||
-- around a HideObject) finishes, rather than having it invoked
|
||||
-- automatically. Only Giovanni's farewell uses this.
|
||||
local function leaderTalk(beatFlag, adviceLabel, fallback, afterAdvice, victoryKey)
|
||||
local function leaderTalk(beatFlag, adviceLabel, fallback, afterAdvice)
|
||||
return function(game, ow, npc, done)
|
||||
if game.save.flags[beatFlag] then
|
||||
if victoryKey and retryTmGive(game, ow, victoryKey, done) then return end
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local finish = done
|
||||
if afterAdvice then
|
||||
@@ -102,42 +79,42 @@ end
|
||||
M.CERULEAN_GYM.talk = {
|
||||
TEXT_CERULEANGYM_MISTY = leaderTalk("EVENT_BEAT_MISTY",
|
||||
"_CeruleanGymMistyTM11ExplanationText",
|
||||
"TM11 teaches\nBUBBLEBEAM!", nil, "OPP_MISTY#1"),
|
||||
"TM11 teaches\nBUBBLEBEAM!"),
|
||||
}
|
||||
|
||||
-- scripts/VermilionGym.asm VermilionGymLTSurgeText .got_tm24_already
|
||||
M.VERMILION_GYM.talk = {
|
||||
TEXT_VERMILIONGYM_LT_SURGE = leaderTalk("EVENT_BEAT_LT_SURGE",
|
||||
"_VermilionGymLTSurgePostBattleAdviceText",
|
||||
"A little word of\nadvice, kid!", nil, "OPP_LT_SURGE#1"),
|
||||
"A little word of\nadvice, kid!"),
|
||||
}
|
||||
|
||||
-- scripts/CeladonGym.asm CeladonGymErikaText .afterBeat
|
||||
M.CELADON_GYM.talk = {
|
||||
TEXT_CELADONGYM_ERIKA = leaderTalk("EVENT_BEAT_ERIKA",
|
||||
"_CeladonGymErikaPostBattleAdviceText",
|
||||
"You are cataloging\nPOKéMON? I must\nsay I'm impressed.", nil, "OPP_ERIKA#1"),
|
||||
"You are cataloging\nPOKéMON? I must\nsay I'm impressed."),
|
||||
}
|
||||
|
||||
-- scripts/FuchsiaGym.asm FuchsiaGymKogaText .afterBeat
|
||||
M.FUCHSIA_GYM.talk = {
|
||||
TEXT_FUCHSIAGYM_KOGA = leaderTalk("EVENT_BEAT_KOGA",
|
||||
"_FuchsiaGymKogaPostBattleAdviceText",
|
||||
"When afflicted by\nTOXIC, POKéMON\nsuffer more and\nmore as battle\nprogresses!", nil, "OPP_KOGA#1"),
|
||||
"When afflicted by\nTOXIC, POKéMON\nsuffer more and\nmore as battle\nprogresses!"),
|
||||
}
|
||||
|
||||
-- scripts/SaffronGym.asm SaffronGymSabrinaText .afterBeat
|
||||
M.SAFFRON_GYM.talk = {
|
||||
TEXT_SAFFRONGYM_SABRINA = leaderTalk("EVENT_BEAT_SABRINA",
|
||||
"_SaffronGymSabrinaPostBattleAdviceText",
|
||||
"Everyone has\npsychic power!\nPeople just don't\nrealize it!", nil, "OPP_SABRINA#1"),
|
||||
"Everyone has\npsychic power!\nPeople just don't\nrealize it!"),
|
||||
}
|
||||
|
||||
-- scripts/CinnabarGym.asm CinnabarGymBlaineText .afterBeat
|
||||
M.CINNABAR_GYM.talk = {
|
||||
TEXT_CINNABARGYM_BLAINE = leaderTalk("EVENT_BEAT_BLAINE",
|
||||
"_CinnabarGymBlainePostBattleAdviceText",
|
||||
"FIRE BLAST is the\nultimate fire\ntechnique!", nil, "OPP_BLAINE#1"),
|
||||
"FIRE BLAST is the\nultimate fire\ntechnique!"),
|
||||
}
|
||||
|
||||
-- scripts/ViridianGym.asm ViridianGymGiovanniText .afterBeat: after the
|
||||
@@ -165,7 +142,7 @@ M.VIRIDIAN_GYM.talk = {
|
||||
"VIRIDIAN_GYM", "VIRIDIANGYM_GIOVANNI")
|
||||
end
|
||||
end, done))
|
||||
end, "OPP_GIOVANNI#3"),
|
||||
end),
|
||||
}
|
||||
|
||||
return M
|
||||
|
||||