mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-27 08:51:27 +02:00
Compare commits
65 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a231657093 | |||
| 6f587520d4 | |||
| e41f2c528a | |||
| 7797962f66 | |||
| 017eafc51b | |||
| c1b7980b4a | |||
| 3e4772abf4 | |||
| 522d8cecf3 | |||
| 40075ae469 | |||
| dce0dd32db | |||
| 88e24f2c0c | |||
| b7b96cd9ef | |||
| 9886920c7a | |||
| f5f9e60b3c | |||
| 247ec919ea | |||
| fe338f47f2 | |||
| 7e4c4eb46e | |||
| 1d735c3291 | |||
| dce0f92c2d | |||
| f1bc392664 | |||
| ee427cd9eb | |||
| 592daafa42 | |||
| b78ab6fb50 | |||
| 6a02a13f22 | |||
| 0e8c93ec75 | |||
| 7b77dbe3ea | |||
| 0c6c813e87 | |||
| a6c914ecad | |||
| 1f12e4838f | |||
| 7fa762871f | |||
| 4d805abb9c | |||
| 51908faabf | |||
| 54c7a3aeae | |||
| 5cc33af354 | |||
| 93c7dff4d6 | |||
| b796c30278 | |||
| ec6b03f84b | |||
| 69c7ee89b5 | |||
| 28dc9b9bb0 | |||
| 2ca07cfd09 | |||
| b3159c19fd | |||
| 08121faa3d | |||
| bbe0f0d9ae | |||
| b35fab845a | |||
| d622e83a8e | |||
| 9dd38e06a5 | |||
| 6b4796b08f | |||
| ab61d08fe9 | |||
| c94ff016d9 | |||
| 85e47bc422 | |||
| 1850b50ee6 | |||
| ef62962010 | |||
| ba201aa4ad | |||
| 7d6566fa51 | |||
| 1ad6c810fa | |||
| 233082967c | |||
| d76d173b60 | |||
| bc7b96c963 | |||
| 56fc4653d7 | |||
| 68fc01dd1c | |||
| cec19db5e0 | |||
| 38d515f547 | |||
| 7e069df740 | |||
| ca700c44d7 | |||
| b286e6584f |
@@ -33,6 +33,8 @@ body:
|
|||||||
- Blue
|
- Blue
|
||||||
- Yellow
|
- Yellow
|
||||||
- Gold
|
- Gold
|
||||||
|
- Silver
|
||||||
|
- Crystal
|
||||||
- N/A
|
- N/A
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -333,6 +333,56 @@ jobs:
|
|||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
|
||||||
|
linux-flatpak-changes:
|
||||||
|
name: detect Linux Flatpak 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_flatpak\.sh$|flatpak/|scripts/pack_love\.sh$|docs/linux-flatpak\.md$|\.github/workflows/(ci|release)\.yml$)'; then
|
||||||
|
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
linux-flatpak-build:
|
||||||
|
name: Linux Flatpak build
|
||||||
|
needs: linux-flatpak-changes
|
||||||
|
if: needs.linux-flatpak-changes.outputs.changed == 'true'
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
- name: Install flatpak-builder deps
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y flatpak flatpak-builder elfutils imagemagick
|
||||||
|
- name: Build Flatpak bundle
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
scripts/build_flatpak.sh --version 0.0.0
|
||||||
|
- name: Upload the Flatpak
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: gen1recomp-linux-flatpak
|
||||||
|
path: |
|
||||||
|
dist/flatpak/gen1recomp-0.0.0-linux.flatpak
|
||||||
|
dist/flatpak/gen1recomp-0.0.0-linux.flatpak.sha256
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 7
|
||||||
|
|
||||||
headless:
|
headless:
|
||||||
name: headless suites (no ROM)
|
name: headless suites (no ROM)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ jobs:
|
|||||||
|
|
||||||
linux-arm64:
|
linux-arm64:
|
||||||
name: build Linux arm64 AppImage
|
name: build Linux arm64 AppImage
|
||||||
needs: [version, love-payload]
|
needs: [version, love-payload, shaderfx-bridge]
|
||||||
# GitHub's free arm64 runner for public repos. It has to be arm64: the
|
# GitHub's free arm64 runner for public repos. It has to be arm64: the
|
||||||
# AppImage compiles LÖVE natively inside a Debian bullseye arm64
|
# AppImage compiles LÖVE natively inside a Debian bullseye arm64
|
||||||
# container, and the qemu-emulated alternative takes hours.
|
# container, and the qemu-emulated alternative takes hours.
|
||||||
@@ -155,13 +155,22 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: gen1recomp-release-love
|
name: gen1recomp-release-love
|
||||||
path: .bazinga/work
|
path: .bazinga/work
|
||||||
|
- name: Download the ShaderFX bridge
|
||||||
|
uses: actions/download-artifact@v8
|
||||||
|
with:
|
||||||
|
name: shaderfx-bridge-linux-arm64
|
||||||
|
path: dist/native/linux-arm64
|
||||||
- name: Build Linux arm64 AppImage
|
- name: Build Linux arm64 AppImage
|
||||||
|
env:
|
||||||
|
SHADERFX_BRIDGE_REQUIRED: "1"
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
scripts/build_linux_arm64.sh \
|
scripts/build_linux_arm64.sh \
|
||||||
--version "${{ needs.version.outputs.version }}" \
|
--version "${{ needs.version.outputs.version }}" \
|
||||||
--game-love .bazinga/work/game.love
|
--game-love .bazinga/work/game.love
|
||||||
- name: Verify the AppImage is self-contained and bullseye-compatible
|
- name: Verify the AppImage is self-contained and bullseye-compatible
|
||||||
|
env:
|
||||||
|
SHADERFX_BRIDGE_REQUIRED: "1"
|
||||||
run: bash scripts/linux-arm64/verify_appimage.sh "dist/linux-arm64/gen1recomp-${{ needs.version.outputs.version }}-linux-arm64.AppImage"
|
run: bash scripts/linux-arm64/verify_appimage.sh "dist/linux-arm64/gen1recomp-${{ needs.version.outputs.version }}-linux-arm64.AppImage"
|
||||||
- name: Upload Linux arm64 release
|
- name: Upload Linux arm64 release
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
@@ -173,6 +182,45 @@ jobs:
|
|||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
|
|
||||||
|
linux-flatpak:
|
||||||
|
name: build Linux Flatpak
|
||||||
|
needs: [version, love-payload, shaderfx-bridge]
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
- name: Install flatpak-builder deps
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y flatpak flatpak-builder elfutils imagemagick
|
||||||
|
- name: Download shared payload
|
||||||
|
uses: actions/download-artifact@v8
|
||||||
|
with:
|
||||||
|
name: gen1recomp-release-love
|
||||||
|
path: .bazinga/work
|
||||||
|
- name: Download the ShaderFX bridge
|
||||||
|
uses: actions/download-artifact@v8
|
||||||
|
with:
|
||||||
|
name: shaderfx-bridge-linux-x64
|
||||||
|
path: dist/native/linux-x64
|
||||||
|
- name: Build Flatpak bundle
|
||||||
|
env:
|
||||||
|
SHADERFX_BRIDGE_REQUIRED: "1"
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
scripts/build_flatpak.sh \
|
||||||
|
--version "${{ needs.version.outputs.version }}" \
|
||||||
|
--game-love .bazinga/work/game.love
|
||||||
|
- name: Upload Flatpak release
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: gen1recomp-linux-flatpak-release
|
||||||
|
path: |
|
||||||
|
dist/flatpak/gen1recomp-${{ needs.version.outputs.version }}-linux.flatpak
|
||||||
|
dist/flatpak/gen1recomp-${{ needs.version.outputs.version }}-linux.flatpak.sha256
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
xbox-uwp:
|
xbox-uwp:
|
||||||
name: build Xbox UWP release
|
name: build Xbox UWP release
|
||||||
needs: [version, love-payload]
|
needs: [version, love-payload]
|
||||||
@@ -286,8 +334,85 @@ jobs:
|
|||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
|
|
||||||
|
shaderfx-bridge:
|
||||||
|
name: build ShaderFX bridge (${{ matrix.plat }})
|
||||||
|
needs: version
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- plat: win-x64
|
||||||
|
runs-on: windows-2022
|
||||||
|
lib: librashader_bridge.dll
|
||||||
|
- plat: mac
|
||||||
|
runs-on: macos-latest
|
||||||
|
lib: liblibrashader_bridge.dylib
|
||||||
|
- plat: linux-x64
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
lib: liblibrashader_bridge.so
|
||||||
|
glibc_triple: x86_64-unknown-linux-gnu.2.17
|
||||||
|
- plat: linux-arm64
|
||||||
|
runs-on: ubuntu-24.04-arm
|
||||||
|
lib: liblibrashader_bridge.so
|
||||||
|
glibc_triple: aarch64-unknown-linux-gnu.2.17
|
||||||
|
runs-on: ${{ matrix.runs-on }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
- name: Setup Rust
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
- name: Cache cargo registry and build dir
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: tools/shaderfx-bridge
|
||||||
|
key: ${{ matrix.plat }}-${{ matrix.glibc_triple }}
|
||||||
|
- name: Build the bridge
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
PLAT: ${{ matrix.plat }}
|
||||||
|
LIB: ${{ matrix.lib }}
|
||||||
|
GLIBC_TRIPLE: ${{ matrix.glibc_triple }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
out="dist/native/$PLAT"
|
||||||
|
mkdir -p "$out"
|
||||||
|
cd tools/shaderfx-bridge
|
||||||
|
case "$PLAT" in
|
||||||
|
win-x64)
|
||||||
|
rustup target add x86_64-pc-windows-msvc
|
||||||
|
cargo build --release --target x86_64-pc-windows-msvc
|
||||||
|
cp "target/x86_64-pc-windows-msvc/release/$LIB" "$GITHUB_WORKSPACE/$out/$LIB"
|
||||||
|
;;
|
||||||
|
mac)
|
||||||
|
rustup target add x86_64-apple-darwin aarch64-apple-darwin
|
||||||
|
cargo build --release --target x86_64-apple-darwin
|
||||||
|
cargo build --release --target aarch64-apple-darwin
|
||||||
|
lipo -create -output "$GITHUB_WORKSPACE/$out/$LIB" \
|
||||||
|
"target/x86_64-apple-darwin/release/$LIB" \
|
||||||
|
"target/aarch64-apple-darwin/release/$LIB"
|
||||||
|
lipo -info "$GITHUB_WORKSPACE/$out/$LIB"
|
||||||
|
;;
|
||||||
|
linux-x64|linux-arm64)
|
||||||
|
pipx install cargo-zigbuild
|
||||||
|
pipx inject cargo-zigbuild ziglang
|
||||||
|
zigpy="$(pipx environment --value PIPX_LOCAL_VENVS)/cargo-zigbuild/bin/python"
|
||||||
|
"$zigpy" -m ziglang version
|
||||||
|
export CARGO_ZIGBUILD_PYTHON_PATH="$zigpy"
|
||||||
|
triple="$GLIBC_TRIPLE"
|
||||||
|
rustup target add "${triple%%.*}"
|
||||||
|
cargo zigbuild --release --target "$triple"
|
||||||
|
cp "target/${triple%%.*}/release/$LIB" "$GITHUB_WORKSPACE/$out/$LIB"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
- name: Upload the bridge
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: shaderfx-bridge-${{ matrix.plat }}
|
||||||
|
path: dist/native/${{ matrix.plat }}/${{ matrix.lib }}
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
release:
|
release:
|
||||||
needs: [version, love-payload, xbox-uwp, linux-arm64, native-tls-win]
|
needs: [version, love-payload, xbox-uwp, linux-arm64, linux-flatpak, native-tls-win, shaderfx-bridge]
|
||||||
runs-on: ${{ fromJSON(github.repository == 'bryanthaboi/gen1recomp' && '["self-hosted", "macOS"]' || '"macos-latest"') }}
|
runs-on: ${{ fromJSON(github.repository == 'bryanthaboi/gen1recomp' && '["self-hosted", "macOS"]' || '"macos-latest"') }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -309,6 +434,28 @@ jobs:
|
|||||||
name: gen1tls-win-x64
|
name: gen1tls-win-x64
|
||||||
path: dist/native/win-x64
|
path: dist/native/win-x64
|
||||||
|
|
||||||
|
- name: Download ShaderFX bridge libraries
|
||||||
|
uses: actions/download-artifact@v8
|
||||||
|
with:
|
||||||
|
pattern: shaderfx-bridge-*
|
||||||
|
path: dist/native
|
||||||
|
- name: Flatten the ShaderFX bridge artifact layout
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
shopt -s nullglob
|
||||||
|
for d in dist/native/shaderfx-bridge-*/; do
|
||||||
|
d="${d%/}"
|
||||||
|
plat="${d#dist/native/shaderfx-bridge-}"
|
||||||
|
mkdir -p "dist/native/$plat"
|
||||||
|
for f in "$d"/*; do mv "$f" "dist/native/$plat/"; done
|
||||||
|
rmdir "$d"
|
||||||
|
done
|
||||||
|
for plat in mac win-x64 linux-x64 linux-arm64; do
|
||||||
|
ls "dist/native/$plat" >/dev/null \
|
||||||
|
|| { echo "::error::no ShaderFX bridge staged for $plat"; exit 1; }
|
||||||
|
done
|
||||||
|
ls -lR dist/native
|
||||||
|
|
||||||
# The same game.love the arm64 AppImage and Xbox UWP builds fused, so
|
# The same game.love the arm64 AppImage and Xbox UWP builds fused, so
|
||||||
# every release asset ships one identical payload (build.sh's own pack
|
# every release asset ships one identical payload (build.sh's own pack
|
||||||
# would omit PATCH_NOTES.md and mobile/ios/app-repo.json).
|
# would omit PATCH_NOTES.md and mobile/ios/app-repo.json).
|
||||||
@@ -358,6 +505,7 @@ jobs:
|
|||||||
- name: Build macOS + Windows + Linux
|
- name: Build macOS + Windows + Linux
|
||||||
env:
|
env:
|
||||||
GEN1TLS_DLL: ${{ github.workspace }}/dist/native/win-x64/gen1tls.dll
|
GEN1TLS_DLL: ${{ github.workspace }}/dist/native/win-x64/gen1tls.dll
|
||||||
|
SHADERFX_BRIDGE_REQUIRED: "1"
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
# Sign in-build (identity auto-detected from the temp keychain);
|
# Sign in-build (identity auto-detected from the temp keychain);
|
||||||
@@ -372,6 +520,8 @@ jobs:
|
|||||||
--game-love dist/payload/game.love
|
--game-love dist/payload/game.love
|
||||||
unzip -l dist/win/gen1recomp-win64.zip | grep -F gen1tls.dll \
|
unzip -l dist/win/gen1recomp-win64.zip | grep -F gen1tls.dll \
|
||||||
|| { echo "::error::Windows zip is missing gen1tls.dll"; exit 1; }
|
|| { echo "::error::Windows zip is missing gen1tls.dll"; exit 1; }
|
||||||
|
unzip -l dist/win/gen1recomp-win64.zip | grep -F librashader_bridge.dll \
|
||||||
|
|| { echo "::error::Windows zip is missing librashader_bridge.dll"; exit 1; }
|
||||||
|
|
||||||
- name: Materialize Android release signing key
|
- name: Materialize Android release signing key
|
||||||
env:
|
env:
|
||||||
@@ -429,6 +579,8 @@ jobs:
|
|||||||
--version "${{ needs.version.outputs.version }}"
|
--version "${{ needs.version.outputs.version }}"
|
||||||
|
|
||||||
- name: Build Anbernic RG34XXSP port
|
- name: Build Anbernic RG34XXSP port
|
||||||
|
env:
|
||||||
|
SHADERFX_BRIDGE_REQUIRED: "1"
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
# Self-contained aarch64 PortMaster-style pack; pulls the LÖVE 11.5
|
# Self-contained aarch64 PortMaster-style pack; pulls the LÖVE 11.5
|
||||||
@@ -442,6 +594,7 @@ jobs:
|
|||||||
# builds, while this explicit local override keeps CI source-aligned.
|
# builds, while this explicit local override keeps CI source-aligned.
|
||||||
GEN1RECOMP_SOURCE_DIR: ${{ github.workspace }}
|
GEN1RECOMP_SOURCE_DIR: ${{ github.workspace }}
|
||||||
GEN1RECOMP_RELEASE_TAG: v${{ needs.version.outputs.version }}
|
GEN1RECOMP_RELEASE_TAG: v${{ needs.version.outputs.version }}
|
||||||
|
SHADERFX_BRIDGE_REQUIRED: "1"
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
# Same aarch64 PortMaster-style pack for Linux ARM SBC PortMaster. The build
|
# Same aarch64 PortMaster-style pack for Linux ARM SBC PortMaster. The build
|
||||||
@@ -498,6 +651,13 @@ jobs:
|
|||||||
name: gen1recomp-linux-arm64-release
|
name: gen1recomp-linux-arm64-release
|
||||||
path: dist/linux-arm64
|
path: dist/linux-arm64
|
||||||
|
|
||||||
|
- name: Download Linux Flatpak release
|
||||||
|
if: github.repository == 'bryanthaboi/gen1recomp'
|
||||||
|
uses: actions/download-artifact@v8
|
||||||
|
with:
|
||||||
|
name: gen1recomp-linux-flatpak-release
|
||||||
|
path: dist/flatpak
|
||||||
|
|
||||||
- name: Stage release assets
|
- name: Stage release assets
|
||||||
if: github.repository == 'bryanthaboi/gen1recomp'
|
if: github.repository == 'bryanthaboi/gen1recomp'
|
||||||
id: assets
|
id: assets
|
||||||
@@ -509,16 +669,24 @@ jobs:
|
|||||||
mkdir -p "$outdir"
|
mkdir -p "$outdir"
|
||||||
cp "dist/mac/gen1recomp-macos.zip" "$outdir/gen1recomp-${v}-macos.zip"
|
cp "dist/mac/gen1recomp-macos.zip" "$outdir/gen1recomp-${v}-macos.zip"
|
||||||
cp "dist/win/gen1recomp-win64.zip" "$outdir/gen1recomp-${v}-windows.zip"
|
cp "dist/win/gen1recomp-win64.zip" "$outdir/gen1recomp-${v}-windows.zip"
|
||||||
cp "dist/linux/gen1recomp-linux.zip" "$outdir/gen1recomp-${v}-linux.zip"
|
# x86_64 desktop Linux: raw AppImage (no zip wrapper).
|
||||||
|
x64_appimage="dist/linux/gen1recomp-linux-x86_64.AppImage"
|
||||||
|
[ -f "$x64_appimage" ] || { echo "::error::$x64_appimage not found (expected from scripts/build.sh linux)"; exit 1; }
|
||||||
|
cp "$x64_appimage" "$outdir/gen1recomp-${v}-linux-x86_64.AppImage"
|
||||||
|
chmod +x "$outdir/gen1recomp-${v}-linux-x86_64.AppImage"
|
||||||
|
|
||||||
# arm64 desktop Linux (Raspberry Pi, Armbian, arm64 VMs). Built on
|
# arm64 desktop Linux (Raspberry Pi, Armbian, arm64 VMs). Built on
|
||||||
# its own runner because LÖVE publishes no aarch64 binary and the
|
# its own runner because LÖVE publishes no aarch64 binary and the
|
||||||
# AppImage has to be compiled natively; ships as a runnable
|
# AppImage has to be compiled natively; ships as a runnable
|
||||||
# AppImage rather than a zip so `chmod +x && ./it` just works.
|
# AppImage so `chmod +x && ./it` just works.
|
||||||
arm64_appimage="dist/linux-arm64/gen1recomp-${v}-linux-arm64.AppImage"
|
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; }
|
[ -f "$arm64_appimage" ] || { echo "::error::$arm64_appimage not found (expected from the linux-arm64 job)"; exit 1; }
|
||||||
cp "$arm64_appimage" "$outdir/gen1recomp-${v}-linux-arm64.AppImage"
|
cp "$arm64_appimage" "$outdir/gen1recomp-${v}-linux-arm64.AppImage"
|
||||||
chmod +x "$outdir/gen1recomp-${v}-linux-arm64.AppImage"
|
chmod +x "$outdir/gen1recomp-${v}-linux-arm64.AppImage"
|
||||||
|
|
||||||
|
flatpak_bundle="dist/flatpak/gen1recomp-${v}-linux.flatpak"
|
||||||
|
[ -f "$flatpak_bundle" ] || { echo "::error::$flatpak_bundle not found (expected from the linux-flatpak job)"; exit 1; }
|
||||||
|
cp "$flatpak_bundle" "$outdir/gen1recomp-${v}-linux.flatpak"
|
||||||
apk="$(find dist/android/release -name '*.apk' | head -1)"
|
apk="$(find dist/android/release -name '*.apk' | head -1)"
|
||||||
[ -n "$apk" ] || { echo "::error::no Android APK found under dist/android/release"; exit 1; }
|
[ -n "$apk" ] || { echo "::error::no Android APK found under dist/android/release"; exit 1; }
|
||||||
cp "$apk" "$outdir/gen1recomp-${v}-android.apk"
|
cp "$apk" "$outdir/gen1recomp-${v}-android.apk"
|
||||||
@@ -656,8 +824,9 @@ jobs:
|
|||||||
release_files=(
|
release_files=(
|
||||||
"dist/release/gen1recomp-${v}-macos.zip"
|
"dist/release/gen1recomp-${v}-macos.zip"
|
||||||
"dist/release/gen1recomp-${v}-windows.zip"
|
"dist/release/gen1recomp-${v}-windows.zip"
|
||||||
"dist/release/gen1recomp-${v}-linux.zip"
|
"dist/release/gen1recomp-${v}-linux-x86_64.AppImage"
|
||||||
"dist/release/gen1recomp-${v}-linux-arm64.AppImage"
|
"dist/release/gen1recomp-${v}-linux-arm64.AppImage"
|
||||||
|
"dist/release/gen1recomp-${v}-linux.flatpak"
|
||||||
"dist/release/gen1recomp-${v}-android.apk"
|
"dist/release/gen1recomp-${v}-android.apk"
|
||||||
"dist/release/gen1recomp++-${v}-ios.ipa"
|
"dist/release/gen1recomp++-${v}-ios.ipa"
|
||||||
"dist/release/gen1recomp-${v}-switch.zip"
|
"dist/release/gen1recomp-${v}-switch.zip"
|
||||||
|
|||||||
+9
-2
@@ -1,5 +1,9 @@
|
|||||||
# Generated from a user-provided Pokemon Red ROM.
|
# Generated from a user-provided Pokemon ROM (Red/Blue/Yellow/Gold/Silver/Crystal).
|
||||||
# Regenerate with: python3 tools/build_data.py --rom /path/to/pokemon-red.gb --clean
|
# Regenerate with: python3 tools/build_data.py --rom /path/to/rom.gb --clean
|
||||||
|
# Yellow Surfing Pikachu minigame art also lands here on import:
|
||||||
|
# assets/generated/minigame/surf_1{a,b,c}.png
|
||||||
|
# assets/generated/minigame/title_bg.png
|
||||||
|
# assets/generated/minigame/intro_pika_{0,1,2,3}.png
|
||||||
data/generated/
|
data/generated/
|
||||||
assets/generated/
|
assets/generated/
|
||||||
|
|
||||||
@@ -62,6 +66,9 @@ mobile/dist/
|
|||||||
# ROM cache exactly like data/generated/, so it is never committable.
|
# ROM cache exactly like data/generated/, so it is never committable.
|
||||||
/build/tiled/
|
/build/tiled/
|
||||||
|
|
||||||
|
# Local visual scratch from minigame development (not wired to CI).
|
||||||
|
/tests/fixtures/
|
||||||
|
|
||||||
# per-machine iOS bundle-id pin (see scripts/build_ios.sh)
|
# per-machine iOS bundle-id pin (see scripts/build_ios.sh)
|
||||||
mobile/ios/bundle_id.local
|
mobile/ios/bundle_id.local
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
# Gen1Recomp
|
# Gen1Recomp
|
||||||
|
|
||||||
A native LÖVE2D recreation of Poke Red, Blue and Yellow. The engine and map
|
A native LÖVE2D recreation of Poke Red, Blue, Gold, Silver, and Crystal. The
|
||||||
behavior are hand-written Lua; game data and graphics are decoded from a ROM
|
engine and map behavior are hand-written Lua; game data and graphics are
|
||||||
supplied by the player.
|
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***
|
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)
|
[Click Here for the AI Use Disclosure!](AIDisclosure.md)
|
||||||
|
|
||||||
> [!CAUTION]
|
> [!CAUTION]
|
||||||
> **We are NOT affiliated with the website `gen1recomp[.]com`** That website is not run by this project, was not authorized by us, and we have no idea who operates it. It is impersonating this project; do not download anything from it, and treat anything it hosts or claims as untrustworthy. Even if the site currently links back to this repository, the people behind it can change its content at any time, so nothing on it should ever be trusted. This GitHub repository and the Discord linked below are the only official sources for this project. Also, as I assumed would eventually happen, the idiot that made that website now pumped it full of adware. Please stay away from that website.
|
> **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, the Discord, and https://gen1re.com are the only official sources for this project. Also, as I assumed would eventually happen, the idiot that made that website now pumped it full of adware. Please stay away from that website.
|
||||||
|
|
||||||
<p align="center"><img src="https://raw.githubusercontent.com/bryanthaboi/gen1recomp/refs/heads/dev/assets/logo/logo.png"></p>
|
<p align="center"><img src="https://raw.githubusercontent.com/bryanthaboi/gen1recomp/refs/heads/dev/assets/logo/logo.png"></p>
|
||||||
|
|
||||||
**SUPPORT / ANNOUNCEMENTS / MODS:** [Discord](https://bois.icu)
|
# [SUPPORT / ANNOUNCEMENTS / MODS ALL FOUND ON THE DISCORD](https://bois.icu)
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ And before you say, "that's not a recomp", you're wrong. Recomp is an acronym. *
|
|||||||
|
|
||||||
### Watch the latest update video
|
### Watch the latest update video
|
||||||
|
|
||||||
[](https://youtu.be/yi7LkWQPKKM)
|
[](https://youtu.be/0hgtLDNGpdo)
|
||||||
|
|
||||||
This project does not include a ROM, emulate the Game Boy, transpile assembly,
|
This project does not include a ROM, emulate the Game Boy, transpile assembly,
|
||||||
or download a disassembly. A canonical US Poke Red, Blue, Yellow, Gold,
|
or download a disassembly. A canonical US Poke Red, Blue, Yellow, Gold,
|
||||||
@@ -228,18 +228,27 @@ entry: a desktop shortcut per game, a Steam entry, or a handheld frontend.
|
|||||||
| `--launcher` | open the launcher anyway, so you can edit a shortcut you already made |
|
| `--launcher` | open the launcher anyway, so you can edit a shortcut you already made |
|
||||||
|
|
||||||
|
|
||||||
## Linux on arm64 (Raspberry Pi)
|
## Linux desktop (AppImage / Flatpak)
|
||||||
|
|
||||||
Alongside the x86_64 `gen1recomp-*-linux.zip`, every release ships
|
Releases ship raw AppImages (no zip wrapper) plus an optional Flatpak bundle:
|
||||||
`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:
|
- `gen1recomp-*-linux-x86_64.AppImage`
|
||||||
|
- `gen1recomp-*-linux-arm64.AppImage` (Raspberry Pi 4/5, Armbian, arm64 VMs)
|
||||||
|
- `gen1recomp-*-linux.flatpak` (see [docs/linux-flatpak.md](docs/linux-flatpak.md))
|
||||||
|
|
||||||
|
```sh
|
||||||
|
chmod +x gen1recomp-*-linux-x86_64.AppImage
|
||||||
|
./gen1recomp-*-linux-x86_64.AppImage
|
||||||
|
```
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
chmod +x gen1recomp-*-linux-arm64.AppImage
|
chmod +x gen1recomp-*-linux-arm64.AppImage
|
||||||
./gen1recomp-*-linux-arm64.AppImage
|
./gen1recomp-*-linux-arm64.AppImage
|
||||||
```
|
```
|
||||||
|
|
||||||
LÖVE publishes no aarch64 binary of any kind, so this artifact compiles the
|
Shared troubleshooting (FUSE, curl, portable mode): [docs/linux-appimage.md](docs/linux-appimage.md).
|
||||||
|
|
||||||
|
LÖVE publishes no aarch64 binary of any kind, so the arm64 artifact compiles the
|
||||||
engine — and SDL2, OpenAL and the codecs — from source inside a Debian
|
engine — and SDL2, OpenAL and the codecs — from source inside a Debian
|
||||||
bullseye arm64 container. It needs only glibc 2.29+, libstdc++, freetype and
|
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
|
zlib on the host; OpenGL, X11, Wayland, KMSDRM, ALSA and PulseAudio are all
|
||||||
|
|||||||
+186
-50
@@ -192,6 +192,20 @@ chmod +x "$PORT_ROOT/$PORT_DIR_NAME/bin/love.aarch64"
|
|||||||
cp "$LOVE_LIB" "$LUAJIT_LIB" "$MODPLUG_LIB" "$OGG_LIB" \
|
cp "$LOVE_LIB" "$LUAJIT_LIB" "$MODPLUG_LIB" "$OGG_LIB" \
|
||||||
"$PORT_ROOT/$PORT_DIR_NAME/libs.aarch64/"
|
"$PORT_ROOT/$PORT_DIR_NAME/libs.aarch64/"
|
||||||
|
|
||||||
|
BRIDGE_LIB="liblibrashader_bridge.so"
|
||||||
|
BRIDGE_SRC="${SHADERFX_BRIDGE_LINUX_ARM64:-}"
|
||||||
|
if [ -z "$BRIDGE_SRC" ] && [ -f "$ROOT/dist/native/linux-arm64/$BRIDGE_LIB" ]; then
|
||||||
|
BRIDGE_SRC="$ROOT/dist/native/linux-arm64/$BRIDGE_LIB"
|
||||||
|
fi
|
||||||
|
if [ -n "$BRIDGE_SRC" ] && [ -f "$BRIDGE_SRC" ]; then
|
||||||
|
cp "$BRIDGE_SRC" "$PORT_ROOT/$PORT_DIR_NAME/libs.aarch64/$BRIDGE_LIB"
|
||||||
|
say "bundled $BRIDGE_LIB for SHADER FX preset conversion"
|
||||||
|
elif [ "${SHADERFX_BRIDGE_REQUIRED:-}" = "1" ]; then
|
||||||
|
fail "$BRIDGE_LIB not found: set SHADERFX_BRIDGE_LINUX_ARM64 or stage it at dist/native/linux-arm64/$BRIDGE_LIB"
|
||||||
|
else
|
||||||
|
warn "$BRIDGE_LIB not found: this port can run converted presets but not CONVERT new ones"
|
||||||
|
fi
|
||||||
|
|
||||||
# Drop a short license pointer for the bundled LÖVE bits.
|
# Drop a short license pointer for the bundled LÖVE bits.
|
||||||
cat > "$PORT_ROOT/$PORT_DIR_NAME/licenses/LICENSE.love2d.txt" <<'EOF'
|
cat > "$PORT_ROOT/$PORT_DIR_NAME/licenses/LICENSE.love2d.txt" <<'EOF'
|
||||||
This port bundles the LÖVE 11.5 aarch64 runtime from PortMaster
|
This port bundles the LÖVE 11.5 aarch64 runtime from PortMaster
|
||||||
@@ -203,81 +217,203 @@ EOF
|
|||||||
# Resolve the game directory from the launcher so this works with both
|
# Resolve the game directory from the launcher so this works with both
|
||||||
# PortMaster-managed ports directories.
|
# PortMaster-managed ports directories.
|
||||||
cat > "$PORT_ROOT/$LAUNCHER_NAME" <<'EOF'
|
cat > "$PORT_ROOT/$LAUNCHER_NAME" <<'EOF'
|
||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
# gen1recomp-sbc — Linux ARM SBC / PortMaster launcher
|
# gen1recomp-sbc — High-Performance Native ARM64 Handheld Port Launcher
|
||||||
# Uses SHDIR-relative paths so firmware-specific mount points do not matter.
|
# Compatible with TrimUI Brick, TrimUI Smart Pro, Anbernic (H700/RK3566), muOS, Knulli, ArkOS, Stock OS
|
||||||
|
|
||||||
export HOME="${HOME:-/root}"
|
export HOME="${HOME:-/root}"
|
||||||
XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}"
|
XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}"
|
||||||
SHDIR="$(cd "$(dirname "$0")" && pwd)"
|
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"
|
GAMEDIR="$SHDIR/gen1recomp-sbc"
|
||||||
CONFDIR="$GAMEDIR/conf"
|
CONFDIR="$GAMEDIR/conf"
|
||||||
|
|
||||||
mkdir -p "$CONFDIR"
|
mkdir -p "$CONFDIR"
|
||||||
|
|
||||||
cd "$GAMEDIR" || exit 1
|
cd "$GAMEDIR" || exit 1
|
||||||
> "$GAMEDIR/log.txt" && exec > >(tee "$GAMEDIR/log.txt") 2>&1
|
|
||||||
|
|
||||||
|
# Setup POSIX logging (compatible with busybox ash/dash/bash)
|
||||||
|
exec 1>"$GAMEDIR/log.txt" 2>&1
|
||||||
|
|
||||||
|
echo "========================================="
|
||||||
|
echo "gen1recomp Handheld Native ARM64 Launcher"
|
||||||
|
echo "SHDIR: $SHDIR"
|
||||||
|
echo "GAMEDIR: $GAMEDIR"
|
||||||
|
echo "Date: $(date 2>/dev/null || echo 'N/A')"
|
||||||
|
echo "========================================="
|
||||||
|
|
||||||
|
# PortMaster's control.txt wants a home tree under /mnt/SDCARD/Data; on a
|
||||||
|
# fresh card that tree may not exist yet and its mkdir fails noisily (log.txt:
|
||||||
|
# "mkdir: can't create directory '/mnt/SDCARD/Data/home'"). Pre-create it
|
||||||
|
# before any PortMaster script runs so the failure cannot occur.
|
||||||
|
if [ -d /mnt/SDCARD ]; then
|
||||||
|
mkdir -p /mnt/SDCARD/Data/home 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 1. Locate PortMaster control directory if present
|
||||||
|
controlfolder=""
|
||||||
|
if [ -d "/mnt/SDCARD/Apps/PortMaster/PortMaster" ]; then
|
||||||
|
controlfolder="/mnt/SDCARD/Apps/PortMaster/PortMaster"
|
||||||
|
elif [ -d "/mnt/SDCARD/PortMaster/PortMaster" ]; then
|
||||||
|
controlfolder="/mnt/SDCARD/PortMaster/PortMaster"
|
||||||
|
elif [ -d "/mnt/SDCARD/PortMaster" ]; then
|
||||||
|
controlfolder="/mnt/SDCARD/PortMaster"
|
||||||
|
elif [ -d "/mnt/SDCARD/Apps/PortMaster" ]; then
|
||||||
|
controlfolder="/mnt/SDCARD/Apps/PortMaster"
|
||||||
|
elif [ -d "/mnt/SDCARD/Emus/PORTS/PortMaster" ]; then
|
||||||
|
controlfolder="/mnt/SDCARD/Emus/PORTS/PortMaster"
|
||||||
|
elif [ -d "/mnt/SDCARD/Emus/PORTMASTER" ]; then
|
||||||
|
controlfolder="/mnt/SDCARD/Emus/PORTMASTER"
|
||||||
|
elif [ -d "/mnt/SDCARD/Roms/ports/PortMaster" ]; then
|
||||||
|
controlfolder="/mnt/SDCARD/Roms/ports/PortMaster"
|
||||||
|
elif [ -d "/mnt/SDCARD/Roms/PORTS/PortMaster" ]; then
|
||||||
|
controlfolder="/mnt/SDCARD/Roms/PORTS/PortMaster"
|
||||||
|
elif [ -d "/mnt/SDCARD/Ports/PortMaster" ]; then
|
||||||
|
controlfolder="/mnt/SDCARD/Ports/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 "/mnt/SDCARD/Emus/tg5040/PORTS.pak/PortMaster" ]; then
|
||||||
|
controlfolder="/mnt/SDCARD/Emus/tg5040/PORTS.pak/PortMaster"
|
||||||
|
elif [ -d "/roms/PORTS/PortMaster" ]; then
|
||||||
|
controlfolder="/roms/PORTS/PortMaster"
|
||||||
|
elif [ -d "/roms/ports/PortMaster" ]; then
|
||||||
|
controlfolder="/roms/ports/PortMaster"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$controlfolder" ] && [ -f "$controlfolder/control.txt" ]; then
|
||||||
|
echo "Found PortMaster control folder: $controlfolder"
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "$controlfolder/control.txt"
|
||||||
|
if type get_controls >/dev/null 2>&1; then
|
||||||
|
get_controls
|
||||||
|
fi
|
||||||
|
if [ -n "${CFW_NAME:-}" ] && [ -f "${controlfolder}/mod_${CFW_NAME}.txt" ]; then
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "${controlfolder}/mod_${CFW_NAME}.txt"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "PortMaster control.txt not found, using standalone handheld configuration."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. CPU governor
|
||||||
|
# Pinning every core to performance keeps handhelds at peak clock even when gameplay
|
||||||
|
# uses only a fraction of CPU. Schedutil is the default; set POKEPORT_CPU_GOVERNOR=performance
|
||||||
|
# only for profiling or devices needing an extra boost.
|
||||||
|
CPU_GOVERNOR_OVERRIDE="${POKEPORT_CPU_GOVERNOR:-schedutil}"
|
||||||
|
CPU_GOVERNOR_STATE=""
|
||||||
|
|
||||||
|
write_cpu_governor() {
|
||||||
|
local governor_path="$1"
|
||||||
|
local governor="$2"
|
||||||
|
if [ -n "${ESUDO:-}" ]; then
|
||||||
|
printf '%s\n' "$governor" | $ESUDO tee "$governor_path" >/dev/null 2>&1 || true
|
||||||
|
else
|
||||||
|
printf '%s\n' "$governor" > "$governor_path" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
restore_cpu_governors() {
|
||||||
|
[ -n "$CPU_GOVERNOR_STATE" ] || return 0
|
||||||
|
while IFS='|' read -r governor_path governor; do
|
||||||
|
[ -n "$governor_path" ] || continue
|
||||||
|
write_cpu_governor "$governor_path" "$governor"
|
||||||
|
done <<< "$CPU_GOVERNOR_STATE"
|
||||||
|
CPU_GOVERNOR_STATE=""
|
||||||
|
}
|
||||||
|
|
||||||
|
apply_cpu_governors() {
|
||||||
|
[ -n "$CPU_GOVERNOR_OVERRIDE" ] || return 0
|
||||||
|
for governor_path in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do
|
||||||
|
[ -f "$governor_path" ] || continue
|
||||||
|
old_governor=$(cat "$governor_path" 2>/dev/null || true)
|
||||||
|
[ -n "$old_governor" ] || continue
|
||||||
|
CPU_GOVERNOR_STATE="${CPU_GOVERNOR_STATE}${governor_path}|${old_governor}"$'\n'
|
||||||
|
write_cpu_governor "$governor_path" "$CPU_GOVERNOR_OVERRIDE"
|
||||||
|
done
|
||||||
|
echo "CPU governor requested: $CPU_GOVERNOR_OVERRIDE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 3. Environment & Low-Level OS Tuning
|
||||||
|
export POKEPORT_HANDHELD=1
|
||||||
|
export HANDHELD=1
|
||||||
|
export PORTMASTER=1
|
||||||
|
export NINTENDO_LAYOUT=1
|
||||||
export XDG_DATA_HOME="$CONFDIR"
|
export XDG_DATA_HOME="$CONFDIR"
|
||||||
export XDG_CONFIG_HOME="$CONFDIR"
|
export XDG_CONFIG_HOME="$CONFDIR"
|
||||||
export LD_LIBRARY_PATH="$GAMEDIR/libs.aarch64:${LD_LIBRARY_PATH:-}"
|
export LD_LIBRARY_PATH="$GAMEDIR/libs.aarch64:$GAMEDIR/libs:/usr/trimui/lib:/mnt/SDCARD/System/lib:/usr/lib64:/usr/lib:${LD_LIBRARY_PATH:-}"
|
||||||
export SDL_GAMECONTROLLERCONFIG="${sdl_controllerconfig:-}"
|
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}"
|
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
|
# Audio buffer size
|
||||||
$ESUDO chmod 666 /dev/uinput 2>/dev/null || true
|
export SDL_AUDIO_SAMPLES=1024
|
||||||
|
|
||||||
if [ -n "${GPTOKEYB:-}" ]; then
|
# Chip-synth sample rate. The audio worker is the dominant CPU cost while
|
||||||
$GPTOKEYB "love.aarch64" &
|
# idle (music keeps playing when the screen is static), and synthesis cost
|
||||||
|
# scales linearly with the rate. 22050 Hz roughly halves audio CPU vs the
|
||||||
|
# 44100 Hz default; the Game Boy's own DAC content is well below 11 kHz, so
|
||||||
|
# the handheld speaker sounds effectively identical.
|
||||||
|
export POKEPORT_AUDIO_RATE=22050
|
||||||
|
|
||||||
|
# Idle-power render governor: after 10s with no input on static screens,
|
||||||
|
# presentation drops to IDLE_FPS, cutting idle CPU/GPU compositing ~6x.
|
||||||
|
# Any button press restores full framerate next frame.
|
||||||
|
export POKEPORT_IDLE_FPS=6
|
||||||
|
|
||||||
|
# Memory allocator tuning (limit arena fragmentation on 1GB RAM SBCs)
|
||||||
|
export MALLOC_ARENA_MAX=2
|
||||||
|
export MALLOC_TRIM_THRESHOLD_=131072
|
||||||
|
|
||||||
|
# Process scheduling & CPU affinity
|
||||||
|
renice -n -5 -p $$ >/dev/null 2>&1 || true
|
||||||
|
taskset -cp 0-3 $$ >/dev/null 2>&1 || true
|
||||||
|
ionice -c 3 -p $$ >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
# Ensure executable permissions
|
||||||
|
if [ -n "${ESUDO:-}" ]; then
|
||||||
|
$ESUDO chmod a+x ./bin/love.aarch64 2>/dev/null || true
|
||||||
|
$ESUDO chmod 666 /dev/uinput 2>/dev/null || true
|
||||||
|
else
|
||||||
|
chmod a+x ./bin/love.aarch64 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# 4. Platform helper
|
||||||
if type pm_platform_helper >/dev/null 2>&1; then
|
if type pm_platform_helper >/dev/null 2>&1; then
|
||||||
|
echo "Running pm_platform_helper..."
|
||||||
pm_platform_helper "$GAMEDIR/bin/love.aarch64"
|
pm_platform_helper "$GAMEDIR/bin/love.aarch64"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Apply after PortMaster's helper: some firmware helpers reset cpufreq policy
|
||||||
|
# while preparing the runtime. The saved values are restored on exit.
|
||||||
|
apply_cpu_governors
|
||||||
|
trap restore_cpu_governors EXIT
|
||||||
|
|
||||||
|
# PulseAudio runtime: PortMaster's helper points it at conf/pulse on the vfat
|
||||||
|
# SD card, where the runtime symlink fails ("Operation not permitted" -- vfat
|
||||||
|
# has no symlinks). Force the runtime onto tmpfs (real dirs, no symlinks).
|
||||||
|
export PULSE_RUNTIME_PATH="/tmp/pulse-$PPID"
|
||||||
|
mkdir -p "$PULSE_RUNTIME_PATH" 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "Launching ./bin/love.aarch64 $GAMEDIR/lovegame..."
|
||||||
|
echo "LD_LIBRARY_PATH: $LD_LIBRARY_PATH"
|
||||||
|
|
||||||
|
# 5. Launch
|
||||||
./bin/love.aarch64 "$GAMEDIR/lovegame"
|
./bin/love.aarch64 "$GAMEDIR/lovegame"
|
||||||
|
EXIT_CODE=$?
|
||||||
|
|
||||||
|
echo "Game exited with code: $EXIT_CODE"
|
||||||
|
|
||||||
|
# 6. Cleanup
|
||||||
|
restore_cpu_governors
|
||||||
|
|
||||||
if type pm_finish >/dev/null 2>&1; then
|
if type pm_finish >/dev/null 2>&1; then
|
||||||
pm_finish
|
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
|
fi
|
||||||
|
|
||||||
|
exit $EXIT_CODE
|
||||||
EOF
|
EOF
|
||||||
chmod +x "$PORT_ROOT/$LAUNCHER_NAME"
|
chmod +x "$PORT_ROOT/$LAUNCHER_NAME"
|
||||||
|
|
||||||
|
|||||||
@@ -156,6 +156,20 @@ chmod +x "$PORT_ROOT/$PORT_DIR_NAME/bin/love.aarch64"
|
|||||||
cp "$LOVE_LIB" "$LUAJIT_LIB" "$MODPLUG_LIB" "$OGG_LIB" \
|
cp "$LOVE_LIB" "$LUAJIT_LIB" "$MODPLUG_LIB" "$OGG_LIB" \
|
||||||
"$PORT_ROOT/$PORT_DIR_NAME/libs.aarch64/"
|
"$PORT_ROOT/$PORT_DIR_NAME/libs.aarch64/"
|
||||||
|
|
||||||
|
BRIDGE_LIB="liblibrashader_bridge.so"
|
||||||
|
BRIDGE_SRC="${SHADERFX_BRIDGE_LINUX_ARM64:-}"
|
||||||
|
if [ -z "$BRIDGE_SRC" ] && [ -f "$ROOT/dist/native/linux-arm64/$BRIDGE_LIB" ]; then
|
||||||
|
BRIDGE_SRC="$ROOT/dist/native/linux-arm64/$BRIDGE_LIB"
|
||||||
|
fi
|
||||||
|
if [ -n "$BRIDGE_SRC" ] && [ -f "$BRIDGE_SRC" ]; then
|
||||||
|
cp "$BRIDGE_SRC" "$PORT_ROOT/$PORT_DIR_NAME/libs.aarch64/$BRIDGE_LIB"
|
||||||
|
say "bundled $BRIDGE_LIB for SHADER FX preset conversion"
|
||||||
|
elif [ "${SHADERFX_BRIDGE_REQUIRED:-}" = "1" ]; then
|
||||||
|
fail "$BRIDGE_LIB not found: set SHADERFX_BRIDGE_LINUX_ARM64 or stage it at dist/native/linux-arm64/$BRIDGE_LIB"
|
||||||
|
else
|
||||||
|
warn "$BRIDGE_LIB not found: this port can run converted presets but not CONVERT new ones"
|
||||||
|
fi
|
||||||
|
|
||||||
# Drop a short license pointer for the bundled LÖVE bits.
|
# Drop a short license pointer for the bundled LÖVE bits.
|
||||||
cat > "$PORT_ROOT/$PORT_DIR_NAME/licenses/LICENSE.love2d.txt" <<'EOF'
|
cat > "$PORT_ROOT/$PORT_DIR_NAME/licenses/LICENSE.love2d.txt" <<'EOF'
|
||||||
This port bundles the LÖVE 11.5 aarch64 runtime from PortMaster
|
This port bundles the LÖVE 11.5 aarch64 runtime from PortMaster
|
||||||
|
|||||||
@@ -62,7 +62,16 @@ function love.conf(t)
|
|||||||
t.window.minheight = 360
|
t.window.minheight = 360
|
||||||
end
|
end
|
||||||
t.version = love._os == "iOS" and "12.0" or "11.5"
|
t.version = love._os == "iOS" and "12.0" or "11.5"
|
||||||
|
-- On Linux handhelds with KMSDRM/EGL, driver vsync blocks the CPU thread with
|
||||||
|
-- busy-waiting spinloops inside eglSwapBuffers. Disabling driver vsync allows
|
||||||
|
-- love.run to perform kernel nanosleep(), dropping idle CPU from 25% to 4%.
|
||||||
|
if os.getenv("HANDHELD") == "1" or os.getenv("PORTMASTER") == "1"
|
||||||
|
or os.getenv("POKEPORT_HANDHELD") == "1" or os.getenv("TRIMUI") == "1"
|
||||||
|
or os.getenv("MUOS") == "1" or os.getenv("KNULLI") == "1" then
|
||||||
|
t.window.vsync = 0
|
||||||
|
else
|
||||||
t.window.vsync = 1
|
t.window.vsync = 1
|
||||||
|
end
|
||||||
t.modules.audio = not companion
|
t.modules.audio = not companion
|
||||||
t.modules.joystick = not companion
|
t.modules.joystick = not companion
|
||||||
t.modules.physics = false
|
t.modules.physics = false
|
||||||
|
|||||||
@@ -0,0 +1,765 @@
|
|||||||
|
return {
|
||||||
|
{ name = "BGB", palettes = {
|
||||||
|
{ "BGB_v0.3", "142c3c548c74acd494ecfccc" },
|
||||||
|
{ "Blue", "0000600000c05f5fffc0c0ff" },
|
||||||
|
{ "GB_Kiosk", "1434046c6c04bcbc1cececb4" },
|
||||||
|
{ "GB_Light", "004f3b00694a009a7100b581" },
|
||||||
|
{ "Grey", "1414145c5c5ca4a4a4ececec" },
|
||||||
|
{ "Greys_High_Contrast", "000000686868b0b0b0ffffff" },
|
||||||
|
{ "KIG_Green", "20402030703040a04050d050" },
|
||||||
|
{ "LCD_Green", "0c1c24346c548cc474e4fcd4" },
|
||||||
|
{ "Red", "600000c00000ff6060ffc0c0" },
|
||||||
|
{ "Super_GB", "311852ad2921de944affefce" },
|
||||||
|
{ "Yellow", "202010686830b0a848f8f078" },
|
||||||
|
{ "Yellow_BGB", "556f3593a73bd2cf4df4ebbf" },
|
||||||
|
{ "NGM_Brown", "483818987838d8b058f8e088" },
|
||||||
|
} },
|
||||||
|
{ name = "BGB/Inverted", palettes = {
|
||||||
|
{ "BGB_v0.3 - Inverted", "ecfcccacd494548c74142c3c" },
|
||||||
|
{ "Blue - Inverted", "c0c0ff5f5fff0000c0000060" },
|
||||||
|
{ "GB_Kiosk - Inverted", "ececb4bcbc1c6c6c04143404" },
|
||||||
|
{ "GB_Light - Inverted", "00b581009a7100694a004f3b" },
|
||||||
|
{ "Grey - Inverted", "ecececa4a4a45c5c5c141414" },
|
||||||
|
{ "Greys_High_Contrast - Inverted", "ffffffb0b0b0686868000000" },
|
||||||
|
{ "KIG_Green - Inverted", "50d05040a040307030204020" },
|
||||||
|
{ "LCD_Green - Inverted", "e4fcd48cc474346c540c1c24" },
|
||||||
|
{ "Red - Inverted", "ffc0c0ff6060c00000600000" },
|
||||||
|
{ "Super_GB - Inverted", "ffefcede944aad2921311852" },
|
||||||
|
{ "Yellow - Inverted", "f8f078b0a848686830202010" },
|
||||||
|
{ "Yellow_BGB- Inverted", "f4ebbfd2cf4d93a73b556f35" },
|
||||||
|
{ "NGM_Brown - Inverted", "f8e088d8b058987838483818" },
|
||||||
|
} },
|
||||||
|
{ name = "GalleryWebApp Palettes", palettes = {
|
||||||
|
{ "Artistic Caffeinated Lactose", "2416069e754fdea963fdfef5" },
|
||||||
|
{ "AQP Pikes Peak", "2a201efa2b25868779ebeee7" },
|
||||||
|
{ "Azure Clouds", "00000012412732b66d47ff99" },
|
||||||
|
{ "BGB Emulator", "08182034685688c070e0f8d0" },
|
||||||
|
{ "Black & White", "000000555555aaaaaaffffff" },
|
||||||
|
{ "Black Zero", "2e463d385d49577b467e8416" },
|
||||||
|
{ "CGA Palette Crush 1", "000000ff55ff55ffffffffff" },
|
||||||
|
{ "CGA Palette Crush 2", "000000ff555555ffffffffff" },
|
||||||
|
{ "CMYKeystone", "373737fb00fa0be8fdffff00" },
|
||||||
|
{ "Candy Cotton Tower Raid", "3800168f0039e65790e6aec4" },
|
||||||
|
{ "Caramel Fudge Paranoia", "3f1711b01553cf7163cf9255" },
|
||||||
|
{ "Childhood in Greenland", "405010708028a0a840d0d058" },
|
||||||
|
{ "Cyanide Blues", "0e1e3d1e479321aff59efbe3" },
|
||||||
|
{ "Deep Haze Green", "00000027421f467818a1d909" },
|
||||||
|
{ "Dies ist meine Wassermelone", "222903558429f27d7affdbcb" },
|
||||||
|
{ "Drowning at Night", "030c2220293f586164a9b0b3" },
|
||||||
|
{ "DUN 2000 Remastered", "000000725441c09e7dfbf1cd" },
|
||||||
|
{ "Flowerfeldstrasse", "171f6275868fc5c5cee9d9cc" },
|
||||||
|
{ "Floyd Steinberg in Love", "4c1c2dd23c4e5fb1f5eaf5fa" },
|
||||||
|
{ "GB Light", "0b7a6d16a59619c7b31ddece" },
|
||||||
|
{ "GB Pocket", "1f1f1f4d533c8b956dc4cfa1" },
|
||||||
|
{ "Glowing Mountains", "2f1c35514f6ca1a8b8ffbf98" },
|
||||||
|
{ "Golden Elephant Curry", "0000004f3000c27600ff9c00" },
|
||||||
|
{ "Grafixkid Gray", "2b2b26706b66a89f94e0dbcd" },
|
||||||
|
{ "Grafixkid Green", "4c625a7b9278abc396dbf4b4" },
|
||||||
|
{ "Knee-Deep in the Wood", "2c24103c3ca9d5690ffffe6e" },
|
||||||
|
{ "LNK late Awakening", "5a39216b8c427bc67bffffb5" },
|
||||||
|
{ "MTR Aran Remixed", "2c1700b62558047e60aedf1e" },
|
||||||
|
{ "My Friend from Bavaria", "382977b60077df7925feda1b" },
|
||||||
|
{ "Nortorious Comadante", "0402ac04aaac54fefcfcfe54" },
|
||||||
|
{ "Original GB", "0f380f30623077a1129bbc0f" },
|
||||||
|
{ "Purple Rain", "68006aff00848570b2adfffc" },
|
||||||
|
{ "Romeros Garden", "323727574431649a57ebc4ab" },
|
||||||
|
{ "Rusted City Rain", "2c2137764462a96868edb4a1" },
|
||||||
|
{ "Space Haze Overload", "0b06306b1fb1cc3495f8e3c4" },
|
||||||
|
{ "Starlit Memories", "4f133f6f20966d53bd869ad9" },
|
||||||
|
{ "Sunflower Holidays", "000000881400ff5555ffff55" },
|
||||||
|
{ "Super Hyper Mega GB", "331e50a63725d68e49f7e7c6" },
|
||||||
|
{ "The death of Yung Columbus", "1d1414462917ff2261b5ff32" },
|
||||||
|
{ "The starry knight", "123f770f86b637cae5f5db37" },
|
||||||
|
{ "There is Always Money", "221a09977b25fed638fdfe0a" },
|
||||||
|
{ "Tramonto al Parco degli Acquedotti", "0c0a3e912978e64a4ef3c677" },
|
||||||
|
{ "VTB 1985", "000000520000db0000ff0000" },
|
||||||
|
{ "Waterfront Plaza", "10253342678e6f9edfcecece" },
|
||||||
|
{ "Youth Ikarus reloaded", "1e00009e0000f78e50cef7f7" },
|
||||||
|
} },
|
||||||
|
{ name = "GB Color (Combo Palettes)", palettes = {
|
||||||
|
{ "GBC - Down + A", "000000ff0000ffff00ffffff" },
|
||||||
|
{ "GBC - Down + B", "0000007b4a00ffff00ffffff", "0000000000ff63a5ffffffff", "0000000084007bff31ffffff" },
|
||||||
|
{ "GBC - Down", "0000009494ffff9494ffffa5" },
|
||||||
|
{ "GBC - Left + A", "00000052528c8c8cdeffffff", "000000943a3aff8484ffffff", "000000843100ffad63ffffff" },
|
||||||
|
{ "GBC - Left + B", "000000525252a5a5a5ffffff" },
|
||||||
|
{ "GBC - Left", "0000000000ff63a5ffffffff", "000000943a3aff8484ffffff", "0000000084007bff31ffffff" },
|
||||||
|
{ "GBC - Right + A", "0000000063c57bff31ffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
|
||||||
|
{ "GBC - Right + B", "ffffffffde00008484000000" },
|
||||||
|
{ "GBC - Right", "000000ff420052ff00ffffff" },
|
||||||
|
{ "GBC - Up + A", "000000943a3aff8484ffffff", "0000000084007bff31ffffff", "0000000000ff63a5ffffffff" },
|
||||||
|
{ "GBC - Up + B", "5a3108846b29ce9c84ffe6c5", "000000843100ffad63ffffff", "000000843100ffad63ffffff" },
|
||||||
|
{ "GBC - Up", "000000843100ffad63ffffff" },
|
||||||
|
} },
|
||||||
|
{ name = "GB Color (Unique Palettes)", palettes = {
|
||||||
|
{ "GBC Palette 001 (Europe)", "000000843100ffad63ffffff", "0000000000ff63a5ffffffff", "0000000084007bff31ffffff" },
|
||||||
|
{ "GBC Palette 002 (World)", "000000006300ffff00a59cff" },
|
||||||
|
{ "GBC Palette 003 (USA, Europe)", "0000000084007bff31ffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
|
||||||
|
{ "GBC Palette 004 (USA, Europe)", "0000000063c57bff31ffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
|
||||||
|
{ "GBC Palette 005 (USA)", "ffffffffde00008484000000" },
|
||||||
|
{ "GBC Palette 006 (USA, Europe)", "000000843100ffad63ffffff", "0000000000ff63a5ffffffff", "0000000084007bff31ffffff" },
|
||||||
|
{ "GBC Palette 007 (USA, Europe)", "000000ff0000ff9c00ffffff" },
|
||||||
|
{ "GBC Palette 008 (World)", "ffffffffff00ff840052de00", "0000ff63a5ffffffffffffff", "000000943a3aff8484ffffff" },
|
||||||
|
{ "GBC Palette 009 (USA)", "000000843100ffad63ffffff", "0000000000ff63a5ffffffff", "0000000084007bff31ffffff" },
|
||||||
|
{ "GBC Palette 010 (Europe)", "000000843100ffad63ffffff", "0000000084007bff31ffffff", "0000000084007bff31ffffff" },
|
||||||
|
{ "GBC Palette 011 (Europe)", "000000843100ffad63ffffff", "0000000084007bff31ffffff", "0000000000ff63a5ffffffff" },
|
||||||
|
{ "GBC Palette 012 (Europe)", "000000843100ffad63ffffff", "0000000000ff63a5ffffffff", "0000000000ff63a5ffffffff" },
|
||||||
|
{ "GBC Palette 013 (World) (Rev A)", "000000ff0000ff9c00ffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
|
||||||
|
{ "GBC Palette 014 (World)", "000000ff0000ff9c00ffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
|
||||||
|
{ "GBC Palette 015 (Japan)", "00000052528c8c8cdeffffff", "4a0000943a00ffd600ffc542", "0000ffff00005abdffffffff" },
|
||||||
|
{ "GBC Palette 016 (USA, Europe)", "003a3a63947394b5ffffff9c", "4a0000943a00ffd600ffc542", "000000943a3aff8484ffffff" },
|
||||||
|
{ "GBC Palette 017 (USA, Europe)", "00000052528c8c8cdeffffff", "4a0000943a00ffd600ffc542", "0000ffff00005abdffffffff" },
|
||||||
|
{ "GBC Palette 018 (USA, Europe) (Rev A)", "00000052528c8c8cdeffffff", "4a0000943a00ffd600ffc542", "0000ffff00005abdffffffff" },
|
||||||
|
{ "GBC Palette 019 (USA, Europe)", "00000052528c8c8cdeffffff", "4a0000943a00ffd600ffc542", "0000ffff00005abdffffffff" },
|
||||||
|
{ "GBC Palette 020 (World) (Rev A)", "0000000000ff63a5ffffffff", "0000000000ff63a5ffffffff", "000000943a3aff8484ffffff" },
|
||||||
|
{ "GBC Palette 021 (World)", "0000000000ff63a5ffffffff", "0000000000ff63a5ffffffff", "000000943a3aff8484ffffff" },
|
||||||
|
{ "GBC Palette 022 (Europe)", "000000843100ffad63ffffff", "0000000000ff63a5ffffffff", "0000000000ff63a5ffffffff" },
|
||||||
|
{ "GBC Palette 023 (World) (Rev A)", "000000843100ffad63ffffff" },
|
||||||
|
{ "GBC Palette 024 (World)", "000000843100ffad63ffffff" },
|
||||||
|
{ "GBC Palette 025 (USA)", "000000943a3aff8484ffffff", "0000000084007bff31ffffff" },
|
||||||
|
{ "GBC Palette 026 (Europe)", "000000843100ffad63ffffff" },
|
||||||
|
{ "GBC Palette 027 (Japan)", "000000b573007bff00ffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
|
||||||
|
{ "GBC Palette 028 (Australia)", "000000b573007bff00ffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
|
||||||
|
{ "GBC Palette 029 (Japan)", "000000b573007bff00ffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
|
||||||
|
{ "GBC Palette 030 (Japan)", "00000042737badad84ffffff", "000000944200ff7300ffffff", "0000ffff00005abdffffffff" },
|
||||||
|
{ "GBC Palette 031 (Europe)", "000000b573007bff00ffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
|
||||||
|
{ "GBC Palette 032 (USA) (Rev A)", "000000b573007bff00ffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
|
||||||
|
{ "GBC Palette 033 (USA)", "000000b573007bff00ffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
|
||||||
|
{ "GBC Palette 034 (World)", "0000000084007bff31ffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
|
||||||
|
{ "GBC Palette 035 (Japan) (Rev A)", "000000006300ffff00a59cff", "000000630000d60000ff6352", "0084ffffff7bffffff0000ff" },
|
||||||
|
{ "GBC Palette 036 (Japan)", "000000006300ffff00a59cff", "000000630000d60000ff6352", "0084ffffff7bffffff0000ff" },
|
||||||
|
{ "GBC Palette 037 (Japan)", "000000006300ffff00a59cff", "000000630000d60000ff6352", "0084ffffff7bffffff0000ff" },
|
||||||
|
{ "GBC Palette 038 (USA, Europe)", "0000000063c57bff31ffffff", "000000943a3aff8484ffffff" },
|
||||||
|
{ "GBC Palette 039 (Japan)", "00000052528c8c8cdeffffff", "000000943a3aff8484ffffff" },
|
||||||
|
{ "GBC Palette 040 (USA, Europe)", "0000000063c57bff31ffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
|
||||||
|
{ "GBC Palette 041 (USA, Europe)", "00000052528c8c8cdeffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
|
||||||
|
{ "GBC Palette 042 (USA, Europe)", "00000052528c8c8cdeffffff", "000000943a3aff8484ffffff", "000000843100ffad63ffffff" },
|
||||||
|
{ "GBC Palette 043 (USA)", "000000843100ffad63ffffff", "0000000000ff63a5ffffffff", "0000000084007bff31ffffff" },
|
||||||
|
{ "GBC Palette 044 (Europe)", "000000843100ffad63ffffff", "0000000000ff63a5ffffffff", "0000000000ff63a5ffffffff" },
|
||||||
|
{ "GBC Palette 045 (Japan)", "000000006300ffff00a59cff", "000000630000d60000ff6352", "0084ffffff7bffffff0000ff" },
|
||||||
|
{ "GBC Palette 046 (Japan)", "000000843100ffad63ffffff" },
|
||||||
|
{ "GBC Palette 047 (Japan)", "000000006300ffff00a59cff", "000000630000d60000ff6352", "000000630000d60000ff6352" },
|
||||||
|
{ "GBC Palette 048 (USA, Europe)", "000000006300ffff00a59cff", "000000630000d60000ff6352", "0084ffffff7bffffff0000ff" },
|
||||||
|
{ "GBC Palette 049 (USA, Europe)", "000000006300ffff00a59cff", "000000630000d60000ff6352", "0084ffffff7bffffff0000ff" },
|
||||||
|
{ "GBC Palette 050 (USA, Europe)", "000000006300ffff00a59cff", "000000630000d60000ff6352", "0084ffffff7bffffff0000ff" },
|
||||||
|
{ "GBC Palette 051 (USA, Europe)", "000000006300ffff00a59cff", "000000630000d60000ff6352", "000000630000d60000ff6352" },
|
||||||
|
{ "GBC Palette 052 (USA, Europe)", "000000843100ffad63ffffff" },
|
||||||
|
{ "GBC Palette 053 (France)", "000000943a3aff8484ffffff", "004a0031840000ff00ffffff", "0000000000ff63a5ffffffff" },
|
||||||
|
{ "GBC Palette 054 (Germany)", "000000943a3aff8484ffffff", "004a0031840000ff00ffffff", "0000000000ff63a5ffffffff" },
|
||||||
|
{ "GBC Palette 055 (USA, Europe) (Rev A)", "000000943a3aff8484ffffff", "004a0031840000ff00ffffff", "0000000000ff63a5ffffffff" },
|
||||||
|
{ "GBC Palette 056 (USA, Europe) (Rev B)", "000000943a3aff8484ffffff", "004a0031840000ff00ffffff", "0000000000ff63a5ffffffff" },
|
||||||
|
{ "GBC Palette 057 (USA, Europe)", "000000943a3aff8484ffffff", "004a0031840000ff00ffffff", "0000000000ff63a5ffffffff" },
|
||||||
|
{ "GBC Palette 058 (Europe)", "0000000084007bff31ffffff", "000000943a3aff8484ffffff", "0000000000ff63a5ffffffff" },
|
||||||
|
{ "GBC Palette 059 (Europe)", "000000ff420052ff00ffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
|
||||||
|
{ "GBC Palette 060 (Japan)", "000000843100ffad63ffffff" },
|
||||||
|
{ "GBC Palette 061 (USA, Europe)", "000000843100ffad63ffffff" },
|
||||||
|
{ "GBC Palette 062 (Europe)", "000000843100ffad63ffffff", "0000000000ff63a5ffffffff", "0000000084007bff31ffffff" },
|
||||||
|
{ "GBC Palette 063 (Europe)", "000000843100ffad63ffffff", "0000000000ff63a5ffffffff", "0000000084007bff31ffffff" },
|
||||||
|
{ "GBC Palette 064 (Europe)", "000000843100ffad63ffffff", "0000000000ff63a5ffffffff", "0000000084007bff31ffffff" },
|
||||||
|
{ "GBC Palette 065 (World)", "0000000000ff63a5ffffffff", "000000630000ff0000ffff00", "0000000084007bff31ffffff" },
|
||||||
|
{ "GBC Palette 066 (Japan)", "00000042737badad84ffffff", "000000944200ff7300ffffff", "000000944200ff7300ffffff" },
|
||||||
|
{ "GBC Palette 067 (USA, Europe)", "00000042737badad84ffffff", "000000944200ff7300ffffff", "000000944200ff7300ffffff" },
|
||||||
|
{ "GBC Palette 068 (Europe)", "0000000084007bff31ffffff", "000000943a3aff8484ffffff", "0000000000ff63a5ffffffff" },
|
||||||
|
{ "GBC Palette 069 (France)", "0000000084007bff31ffffff", "000000943a3aff8484ffffff", "0000000000ff63a5ffffffff" },
|
||||||
|
{ "GBC Palette 070 (Germany)", "0000000084007bff31ffffff", "000000943a3aff8484ffffff", "0000000000ff63a5ffffffff" },
|
||||||
|
{ "GBC Palette 071 (Europe)", "000000843100ffad63ffffff" },
|
||||||
|
{ "GBC Palette 072 (USA, Europe)", "0000000084007bff31ffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
|
||||||
|
{ "GBC Palette 073 (Europe)", "0000000084007bff31ffffff", "000000943a3aff8484ffffff", "0000000000ff63a5ffffffff" },
|
||||||
|
{ "GBC Palette 074 (USA)", "0000000063c57bff31ffffff", "000000943a3aff8484ffffff", "0000000000ff63a5ffffffff" },
|
||||||
|
{ "GBC Palette 075 (Japan)", "000000843100ffad63ffffff" },
|
||||||
|
{ "GBC Palette 076 (Europe)", "00000052528c8c8cdeffffff", "00000052528c8c8cdeffffff", "4a0000943a00ffd600ffc542" },
|
||||||
|
{ "GBC Palette 077 (USA)", "0000000084007bff31ffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
|
||||||
|
{ "GBC Palette 078 (Europe)", "00000052528c8c8cdeffffff", "4a0000943a00ffd600ffc542", "4a0000943a00ffd600ffc542" },
|
||||||
|
{ "GBC Palette 079 (Japan) (Rev A)", "0000009c6300ffce00ffffff" },
|
||||||
|
{ "GBC Palette 080 (Japan) (Rev 0A)", "000000ff0000ffff00ffffff" },
|
||||||
|
{ "GBC Palette 081 (Japan) (Rev B)", "000000ff0000ffff00ffffff" },
|
||||||
|
{ "GBC Palette 082 (Japan) (Rev C)", "000000ff0000ffff00ffffff" },
|
||||||
|
{ "GBC Palette 083 (Japan) (Rev D)", "000000ff0000ffff00ffffff" },
|
||||||
|
{ "GBC Palette 084 (Japan) (Rev A)", "000000943a3aff8484ffffff", "0000000084007bff31ffffff" },
|
||||||
|
{ "GBC Palette 085 (Japan)", "000000943a3aff8484ffffff", "0000000084007bff31ffffff" },
|
||||||
|
{ "GBC Palette 086 (Japan)", "0000000000ff63a5ffffffff", "000000943a3aff8484ffffff" },
|
||||||
|
{ "GBC Palette 087 (Japan) (Rev A)", "0000000063c57bff31ffffff", "000000943a3aff8484ffffff" },
|
||||||
|
{ "GBC Palette 088 (Japan)", "0000000063c57bff31ffffff", "000000943a3aff8484ffffff" },
|
||||||
|
{ "GBC Palette 089 (Germany)", "0000000000ff63a5ffffffff", "000000943a3aff8484ffffff" },
|
||||||
|
{ "GBC Palette 090 (USA, Europe)", "0000000000ff63a5ffffffff", "000000943a3aff8484ffffff" },
|
||||||
|
{ "GBC Palette 091 (Spain)", "0000000000ff63a5ffffffff", "000000943a3aff8484ffffff" },
|
||||||
|
{ "GBC Palette 092 (Spain)", "000000943a3aff8484ffffff", "0000000084007bff31ffffff" },
|
||||||
|
{ "GBC Palette 093 (USA, Europe)", "000000943a3aff8484ffffff", "0000000084007bff31ffffff" },
|
||||||
|
{ "GBC Palette 094 (Germany)", "000000943a3aff8484ffffff", "0000000084007bff31ffffff" },
|
||||||
|
{ "GBC Palette 095 (France)", "0000000000ff63a5ffffffff", "000000943a3aff8484ffffff" },
|
||||||
|
{ "GBC Palette 096 (France)", "000000943a3aff8484ffffff", "0000000084007bff31ffffff" },
|
||||||
|
{ "GBC Palette 097 (Italy)", "0000000000ff63a5ffffffff", "000000943a3aff8484ffffff" },
|
||||||
|
{ "GBC Palette 098 (Italy)", "000000943a3aff8484ffffff", "0000000084007bff31ffffff" },
|
||||||
|
{ "GBC Palette 099 (World)", "000000ff0000ffff00ffffff", "000000ff0000ffff00ffffff", "0000ffff00005abdffffffff" },
|
||||||
|
{ "GBC Palette 100 (Japan)", "003a3a63947394b5ffffff9c" },
|
||||||
|
{ "GBC Palette 101 (USA, Europe)", "003a3a63947394b5ffffff9c" },
|
||||||
|
{ "GBC Palette 102 (Europe) (En,Fr,De)", "000000ff524affffff6bff00", "0000ff63a5ffffffffffffff", "000000843100ffad63ffffff" },
|
||||||
|
{ "GBC Palette 103 (World)", "ffffffffde00008484000000" },
|
||||||
|
{ "GBC Palette 104 (Europe)", "ffffffffde00008484000000" },
|
||||||
|
{ "GBC Palette 105 (USA)", "ffffffffde00008484000000" },
|
||||||
|
{ "GBC Palette 106 (USA, Europe) (Rev A)", "000000843100ffad63ffffff", "0000000084007bff31ffffff", "0000000000ff63a5ffffffff" },
|
||||||
|
{ "GBC Palette 107 (USA, Europe) (Rev A)", "000000843100ffad63ffffff", "0000000000ff63a5ffffffff", "0000000084007bff31ffffff" },
|
||||||
|
{ "GBC Palette 108 (Japan)", "003a3a63947394b5ffffff9c", "4a0000943a00ffd600ffc542", "000000943a3aff8484ffffff" },
|
||||||
|
{ "GBC Palette 109 (World) (Rev A)", "000000ad5a42ffff94b5b5ff", "943a3aff8484ffffff000000", "943a3aff8484ffffff000000" },
|
||||||
|
{ "GBC Palette 110 (World)", "000000ad5a42ffff94b5b5ff", "943a3aff8484ffffff000000", "943a3aff8484ffffff000000" },
|
||||||
|
{ "GBC Palette 111 (USA, Europe) (Rev A)", "5a5a5a9c843163efefffffce", "000000944200ff7300ffffff", "0000000000ff63a5ffffffff" },
|
||||||
|
{ "GBC Palette 112 (USA, Europe) (Rev B)", "5a5a5a9c843163efefffffce", "000000944200ff7300ffffff", "0000000000ff63a5ffffffff" },
|
||||||
|
{ "GBC Palette 113 (USA, Europe)", "5a5a5a9c843163efefffffce", "000000944200ff7300ffffff", "0000000000ff63a5ffffffff" },
|
||||||
|
{ "GBC Palette 114 (Japan) (Rev B)", "5a5a5a9c843163efefffffce", "000000944200ff7300ffffff", "0000000000ff63a5ffffffff" },
|
||||||
|
{ "GBC Palette 115 (Japan)", "5a5a5a9c843163efefffffce", "000000944200ff7300ffffff", "0000000000ff63a5ffffffff" },
|
||||||
|
{ "GBC Palette 116 (USA, Europe)", "000000843100ffad63ffffff", "0000000000ff63a5ffffffff", "0000000084007bff31ffffff" },
|
||||||
|
{ "GBC Palette 117 (World)", "000000ff524affffff6bff00", "0000ff63a5ffffffffffffff", "000000843100ffad63ffffff" },
|
||||||
|
{ "GBC Palette 118 (World) (Rev A)", "000000ff0000ffff00ffffff" },
|
||||||
|
{ "GBC Palette 119 (World)", "000000ff0000ffff00ffffff" },
|
||||||
|
{ "GBC Palette 120 (Europe)", "000000ff0000ffff00ffffff", "000000ff0000ffff00ffffff", "0000ffff00005abdffffffff" },
|
||||||
|
{ "GBC Palette 121 (USA)", "000000ff0000ffff00ffffff", "000000ff0000ffff00ffffff", "0000ffff00005abdffffffff" },
|
||||||
|
{ "GBC Palette 122 (USA)", "000000ff420052ff00ffffff", "000000ff420052ff00ffffff", "0000ffff00005abdffffffff" },
|
||||||
|
{ "GBC Palette 123 (USA, Europe) (Rev A)", "000000ff420052ff00ffffff", "000000ff420052ff00ffffff", "0000ffff00005abdffffffff" },
|
||||||
|
{ "GBC Palette 124 (USA, Europe)", "000000ff0000ff9c00ffffff" },
|
||||||
|
{ "GBC Palette 125 (Japan)", "000000ff0000ffff00ffffff", "000000ff0000ffff00ffffff", "0000ffff00005abdffffffff" },
|
||||||
|
{ "GBC Palette 126 (USA, Europe)", "0000000063c57bff31ffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
|
||||||
|
{ "GBC Palette 127 (USA)", "000000ff524affffff6bff00", "0000ff63a5ffffffffffffff", "000000843100ffad63ffffff" },
|
||||||
|
{ "GBC Palette 128 (Europe)", "000000ff524affffff6bff00", "0000ff63a5ffffffffffffff", "000000843100ffad63ffffff" },
|
||||||
|
{ "GBC Palette 129 (Europe)", "0000000084007bff31ffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
|
||||||
|
{ "GBC Palette 130 (USA, Europe)", "0000000084007bff31ffffff", "000000943a3aff8484ffffff", "0000000000ff63a5ffffffff" },
|
||||||
|
{ "GBC Palette 131 (USA, Europe)", "0000000063c57bff31ffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
|
||||||
|
{ "GBC Palette 132 (World)", "00000042737badad84ffffff", "000000944200ff7300ffffff", "0000ffff00005abdffffffff" },
|
||||||
|
{ "GBC Palette 133 (USA, Europe)", "00000042737badad84ffffff", "000000843100ffad63ffffff", "0000000000ff63a5ffffffff" },
|
||||||
|
{ "GBC Palette 134 (USA, Europe)", "0000000000ff63a5ffffffff", "000000943a3aff8484ffffff", "ff00000084ffffff7bffffff" },
|
||||||
|
{ "GBC Palette 135 (Japan)", "000000525252a5a5a5ffffff" },
|
||||||
|
{ "GBC Palette 136 (Japan) (Rev A)", "000000843100ffad63ffffff" },
|
||||||
|
{ "GBC Palette 137 (Japan)", "000000843100ffad63ffffff" },
|
||||||
|
{ "GBC Palette 138 (USA)", "000000ff420052ff00ffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
|
||||||
|
{ "GBC Palette 139 (Japan)", "000000ff0000ff9c00ffffff", "000000ff0000ff9c00ffffff", "0000ffff00005abdffffffff" },
|
||||||
|
{ "GBC Palette 140 (Japan)", "000000ff420052ff00ffffff", "000000ff420052ff00ffffff", "0000ffff00005abdffffffff" },
|
||||||
|
{ "GBC Palette 141 (Japan)", "000000ff420052ff00ffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
|
||||||
|
{ "GBC Palette 142 (USA, Europe)", "000000ff0000ff9c00ffffff", "000000ff0000ff9c00ffffff", "0000ffff00005abdffffffff" },
|
||||||
|
{ "GBC Palette 143 (Japan) (Rev A)", "000000943a3aff8484ffffff", "004a0031840000ff00ffffff", "0000000000ff63a5ffffffff" },
|
||||||
|
{ "GBC Palette 144 (Japan)", "000000943a3aff8484ffffff", "004a0031840000ff00ffffff", "0000000000ff63a5ffffffff" },
|
||||||
|
} },
|
||||||
|
{ name = "GB Color (Unused Palettes)", palettes = {
|
||||||
|
{ "Unused0", "000000b573007bff00ffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
|
||||||
|
{ "Unused1", "000000ff0000ffff00ffffff", "0000000000ff63a5ffffffff", "0000ffff00005abdffffffff" },
|
||||||
|
{ "Unused2", "000000ad5a42ffff94b5b5ff", "943a3aff8484ffffff000000", "943a3aff8484ffffff000000" },
|
||||||
|
{ "Unused3", "ffffffffde00008484000000", "000000943a3aff8484ffffff", "0000000084007bff31ffffff" },
|
||||||
|
{ "Unused4", "000000525252a5a5a5ffffff" },
|
||||||
|
{ "Unused5", "0000009c6300ffce00ffffff" },
|
||||||
|
} },
|
||||||
|
{ name = "R.A.Helllord/Limited Edition Pockets", palettes = {
|
||||||
|
{ "Classic Blue LE", "0000000232741b559e5775af" },
|
||||||
|
{ "Classic Green LE", "00000001633f0b8a68549d80" },
|
||||||
|
{ "Classic Indigo LE", "0000002631725457a47b80b4" },
|
||||||
|
{ "Classic Pink LE", "000000d8648beb8cb3efa1bf" },
|
||||||
|
{ "Classic Red LE", "00000095030dbc2b2bcb5d5b" },
|
||||||
|
{ "Classic Silver LE", "000000908a85cececee0dfde" },
|
||||||
|
{ "Classic Spice Orange LE", "000000c96722e38f48e8a36c" },
|
||||||
|
{ "Classic Yellow LE", "000000cf8b08d2921de8be63" },
|
||||||
|
{ "Glow in the Dark LE", "00000005a56d4dcc808cf68a" },
|
||||||
|
{ "Transparent Blue LE", "0000001e124f361aa6646dcb", "1e124f361aa6646dcb7f8cff", "1e124f361aa6646dcb7f8cff" },
|
||||||
|
{ "Transparent Clear LE", "0000003e3b395959599c9a99", "3e3b395959599c9a99d7d7d7", "3e3b395959599c9a99d7d7d7" },
|
||||||
|
{ "Transparent Green LE", "000000003c0600b10003ce09", "003c0600b10003ce09afe4ab", "003c0600b10003ce09afe4ab" },
|
||||||
|
{ "Transparent Orange LE", "0000003c1405ad2d01e56400", "3c1405ad2d01e56400ebbf8f", "3c1405ad2d01e56400ebbf8f" },
|
||||||
|
{ "Transparent Purple LE", "0000003c2e498d6ea7a986bb", "3c2e498d6ea7a986bbe0d6e5", "3c2e498d6ea7a986bbe0d6e5" },
|
||||||
|
{ "Transparent Red LE", "0000006d1a1a991019c51d21", "6d1a1a991019c51d21eab5b1", "6d1a1a991019c51d21eab5b1" },
|
||||||
|
{ "Transparent Smoke LE", "000000282421363333686464", "282421363333686464bfbfbe", "282421363333686464bfbfbe" },
|
||||||
|
} },
|
||||||
|
{ name = "R.A.Helllord/Limited Edition Pockets Inverted", palettes = {
|
||||||
|
{ "Classic Blue LE - inverted", "5775af1b559e023274000000" },
|
||||||
|
{ "Classic Green LE - inverted", "549d800b8a6801633f000000" },
|
||||||
|
{ "Classic Indigo LE - inverted", "7b80b45457a4263172000000" },
|
||||||
|
{ "Classic Pink LE - inverted", "efa1bfeb8cb3d8648b000000" },
|
||||||
|
{ "Classic Red LE - inverted", "cb5d5bbc2b2b95030d000000" },
|
||||||
|
{ "Classic Silver LE - inverted", "e0dfdececece908a85000000" },
|
||||||
|
{ "Classic Spice Orange LE - inverted", "e8a36ce38f48c96722000000" },
|
||||||
|
{ "Classic Yellow LE - inverted", "e8be63d2921dcf8b08000000" },
|
||||||
|
{ "Glow in the Dark LE - inverted", "8cf68a4dcc8005a56d000000" },
|
||||||
|
{ "Transparent Blue LE - inverted", "646dcb361aa61e124f000000", "7f8cff646dcb361aa61e124f", "7f8cff646dcb361aa61e124f" },
|
||||||
|
{ "Transparent Clear LE - inverted", "9c9a995959593e3b39000000", "d7d7d79c9a995959593e3b39", "d7d7d79c9a995959593e3b39" },
|
||||||
|
{ "Transparent Green LE - inverted", "03ce0900b100003c06000000", "afe4ab03ce0900b100003c06", "afe4ab03ce0900b100003c06" },
|
||||||
|
{ "Transparent Orange LE - inverted", "e56400ad2d013c1405000000", "ebbf8fe56400ad2d013c1405", "ebbf8fe56400ad2d013c1405" },
|
||||||
|
{ "Transparent Purple LE - inverted", "a986bb8d6ea73c2e49000000", "e0d6e5a986bb8d6ea73c2e49", "e0d6e5a986bb8d6ea73c2e49" },
|
||||||
|
{ "Transparent Red LE - inverted", "c51d219910196d1a1a000000", "eab5b1c51d219910196d1a1a", "eab5b1c51d219910196d1a1a" },
|
||||||
|
{ "Transparent Smoke LE - inverted", "686464363333282421000000", "bfbfbe686464363333282421", "bfbfbe686464363333282421" },
|
||||||
|
} },
|
||||||
|
{ name = "R.A.Helllord/PPB", palettes = {
|
||||||
|
{ "PPB - Amber", "ffb13b8c61216043172d1f0b" },
|
||||||
|
{ "PPB - Blue", "28ccff16708c0f4b5e07242d" },
|
||||||
|
{ "PPB - Green", "00ee00008e00005f00002f00" },
|
||||||
|
{ "PPB - White", "c1ffff6a8c8c496060202b2b" },
|
||||||
|
{ "PPB inverted - Amber", "2d1f0b6043178c6121ffb13b" },
|
||||||
|
{ "PPB inverted - Blue", "07242d0f4b5e16708c28ccff" },
|
||||||
|
{ "PPB inverted - Green", "002f00005f00008e0000ee00" },
|
||||||
|
{ "PPB inverted - White", "202b2b4960606a8c8cc1ffff" },
|
||||||
|
} },
|
||||||
|
{ name = "SMY", palettes = {
|
||||||
|
{ "Desert", "3e2f30746657a49b83d2d0b1" },
|
||||||
|
{ "Evening", "012636185569539889aee4a6" },
|
||||||
|
{ "Fake Crystal", "21293129317b4a73bd9ce7f7" },
|
||||||
|
{ "Fog", "343c3756727386a39dbfd2c3" },
|
||||||
|
{ "Magic Eggplant", "36213c842e949d69c7b0e4f1" },
|
||||||
|
{ "Radioactive Pea", "00522106731f349e16b8ce03" },
|
||||||
|
{ "Seaweed", "15003f32654278a758dfe095" },
|
||||||
|
{ "Twilight", "15003f861246bd5462e9d397" },
|
||||||
|
} },
|
||||||
|
{ name = "Super GB", palettes = {
|
||||||
|
{ "1-A (Default)", "311852ad2921de944affefce" },
|
||||||
|
{ "1-B", "000000b55210ceb573dedec6" },
|
||||||
|
{ "1-C", "39399c9c3963ef9c52ffc6ff" },
|
||||||
|
{ "1-D", "521800ff0000c6844affffad" },
|
||||||
|
{ "1-E", "5a39216b8c427bc67bffdeb5" },
|
||||||
|
{ "1-F", "004210ad0000e78c52deefff" },
|
||||||
|
{ "1-G", "ffff5a7b7b0000a5ef000052" },
|
||||||
|
{ "1-H", "311800844200ffbd8cffefe7" },
|
||||||
|
{ "2-A", "000000297b00c68c4af7cea5" },
|
||||||
|
{ "2-B", "52005aff3100ffef52ffffff" },
|
||||||
|
{ "2-C", "29299c7b31efef8c8cffc6ff" },
|
||||||
|
{ "2-D", "000052ff310000ff00ffffa5" },
|
||||||
|
{ "2-E", "10081029106394b5e7ffce84" },
|
||||||
|
{ "2-F", "180000a50000ff9452d6ffff" },
|
||||||
|
{ "2-G", "001800e7bd84e752426bbd39" },
|
||||||
|
{ "2-H", "000000737373bdbdbdffffff" },
|
||||||
|
{ "3-A", "314a63ff632973c6c6ffd69c" },
|
||||||
|
{ "3-B", "001010005200e78421dedec6" },
|
||||||
|
{ "3-C", "21215a00bdffffff7be7adce" },
|
||||||
|
{ "3-D", "00000008ce00e7ad7bf7ffbd" },
|
||||||
|
{ "3-E", "524a73b57b21e7b56bffffc6" },
|
||||||
|
{ "3-F", "424242ffd600ff6bff7b7bce" },
|
||||||
|
{ "3-G", "390000ce3139ffffff63de52" },
|
||||||
|
{ "3-H", "0818004a8c187bce39e7ffa5" },
|
||||||
|
{ "4-A", "00007bd600d67badfff7ad6b" },
|
||||||
|
{ "4-B", "180808427b39efa563f7eff7" },
|
||||||
|
{ "4-C", "0800009ca5e7dea5d6ffe7e7" },
|
||||||
|
{ "4-D", "08214a4a6b7b94ceceffffbd" },
|
||||||
|
{ "4-E", "0021317b5a8ce7ad7bffdead" },
|
||||||
|
{ "4-F", "3900008400a5de84debdd6d6" },
|
||||||
|
{ "4-G", "008463291000bd215ab5e718" },
|
||||||
|
{ "4-H", "425229848c42bdc65affffce" },
|
||||||
|
} },
|
||||||
|
{ name = "Super GB 2 - Vaporwave Edition", palettes = {
|
||||||
|
{ "SGB2VE 1-A Sunset", "1039adef2994f7a552f7f794" },
|
||||||
|
{ "SGB2VE 1-B Kimochi WRO", "4a3973bd63a5f7a56bf7efbd" },
|
||||||
|
{ "SGB2VE 1-C Pachinko", "42427b9c42eff7947bf7deef" },
|
||||||
|
{ "SGB2VE 1-D Cotton Candy", "1039adffa5ff84e7eff7f7f7" },
|
||||||
|
{ "SGB2VE 1-E Absynthe", "7b4aada5b5ffadff5affff8c" },
|
||||||
|
{ "SGB2VE 1-F Arizona", "52529cff4273ffcecedefff7" },
|
||||||
|
{ "SGB2VE 1-G Hot Vibes", "8c18f7f72973f78c18f7ce18" },
|
||||||
|
{ "SGB2VE 1-H Sorbet Whip", "63184aff6b8cffc684f7ffc6" },
|
||||||
|
{ "SGB2VE 2-A Bufotoxin", "104a63009c84ef8c84f7de9c" },
|
||||||
|
{ "SGB2VE 2-B Dank", "4a184263528463b5bdf7f794" },
|
||||||
|
{ "SGB2VE 2-C Creampuff", "632963a573f7f79cadffdef7" },
|
||||||
|
{ "SGB2VE 2-D Modern Computing", "101018ff849c6bffbdf7f7f7" },
|
||||||
|
{ "SGB2VE 2-E Uranium Glass", "4a08ef9c42ef00deded6ff9c" },
|
||||||
|
{ "SGB2VE 2-F. Tenshi", "2142e7de5aceefadceadf7c6" },
|
||||||
|
{ "SGB2VE 2-G 87 Ninjas", "42427bd63994bdb5f7c6f7de" },
|
||||||
|
{ "SGB2VE 2-H Floral", "08101852b59cff849cefe7e7" },
|
||||||
|
{ "SGB2VE 3-A Mind Game", "4200f700b5f7f784f7f7f7f7" },
|
||||||
|
{ "SGB2VE 3-B Sewer Magic", "5a00848c7be784f7c6f7f794" },
|
||||||
|
{ "SGB2VE 3-C Galactic Rabbit Trip", "4a527352a5cebdceeff7f7ef" },
|
||||||
|
{ "SGB2VE 3-D Frenemy", "1039ad42d6d6ffd6b5bdffe7" },
|
||||||
|
{ "SGB2VE 3-E Agent Orange", "632963e76b84f7a56bffe742" },
|
||||||
|
{ "SGB2VE 3-F Backstreet", "294a8cff8cbdffceb5dedeff" },
|
||||||
|
{ "SGB2VE 3-G Eccojam", "8c42a5ad7be7946cff9cefff" },
|
||||||
|
{ "SGB2VE 3-H Sakura", "4a39b5ef73d6efb5bdf7efb5" },
|
||||||
|
{ "SGB2VE 4-A Vector", "1039ad9400f700b5f7f7d684" },
|
||||||
|
{ "SGB2VE 4-B Slumber", "944ab58c8cf794ceffd6ffff" },
|
||||||
|
{ "SGB2VE 4-C Lingonberry Butter", "21529c7b84e7ffadcefff7de" },
|
||||||
|
{ "SGB2VE 4-D Zangetsu", "104a6331a5c684e7eff7efbd" },
|
||||||
|
{ "SGB2VE 4-E Icy Hot", "8c00ffff39ced6c6ffbdfff7" },
|
||||||
|
{ "SGB2VE 4-F Night Driver", "214aff6b00c6ff31ef10006b" },
|
||||||
|
{ "SGB2VE 4-G Bug Hunt", "0839633952bdf7738cf7e7ad" },
|
||||||
|
{ "SGB2VE 4-H MGB-101", "004a84107ba500bdc621ffde" },
|
||||||
|
} },
|
||||||
|
{ name = "TheWolfBunny64/American Pop Culture Pack", palettes = {
|
||||||
|
{ "Absorbent and Yellow", "343b00687600aec600fff752" },
|
||||||
|
{ "BMO Ver.", "394c3d607f6699cca3c0ffcc" },
|
||||||
|
{ "BRB Pink", "480030790050c10080f200a1" },
|
||||||
|
{ "Bedrock Caveman Vision", "002f37005e6e009eb8ff7f00" },
|
||||||
|
{ "BKB Ver.", "6060002098f048f8e0f8f880" },
|
||||||
|
{ "BLS Pink", "492e357a4d59c47c8ef59bb2" },
|
||||||
|
{ "BBS Blue", "1e3a45326274509cba64c4e9" },
|
||||||
|
{ "BTC Green", "39422a5f6e4698b070bedc8d" },
|
||||||
|
{ "DNP Silver", "33383d555d668895a3abbbcc" },
|
||||||
|
{ "Fairly OddPalette", "3d1b2d7b365bce5a997bb850" },
|
||||||
|
{ "GRF Vision", "2d1309964220e59436f5ea8b" },
|
||||||
|
{ "HGW Goldius", "3631215b523891845ab6a571" },
|
||||||
|
{ "Invincible Yellow and Blue", "113c4622788d39c9ebfee566" },
|
||||||
|
{ "NCK Orange", "4c1e007f3300cc5200ff6700" },
|
||||||
|
{ "NJT Green", "28380b435e126b961d86bc25" },
|
||||||
|
{ "OPP Palette", "0015330047abd92121d3d3d3" },
|
||||||
|
{ "PTS Pink", "4c26297f4045cc666fff808b" },
|
||||||
|
{ "Retro Bogeda", "0000006408ffff6cfffbfd1b" },
|
||||||
|
{ "Rocket Portable Power", "09122d213f99bdd539fee998" },
|
||||||
|
{ "RGR Playtime Palette", "662d91ed1c24ffde17ffffff" },
|
||||||
|
{ "Sailor Spinach Green", "2434123d581e628c307bb03c" },
|
||||||
|
{ "SCD Mystery Ver.", "2a1a318f59a5f79321c6de31" },
|
||||||
|
{ "Smurfy Blue", "0d3747165c772394bf2cb9ef" },
|
||||||
|
{ "SPB Yellow", "4a45157b7424c5ba39f7e948" },
|
||||||
|
{ "SQW Sea Foam Green", "37403d5c6b6694aca4b9d7cd" },
|
||||||
|
{ "Swampy Ogre Green", "39400d606b179aab24c1d62e" },
|
||||||
|
{ "TMT Pink", "3815205e2436963957bc486d" },
|
||||||
|
{ "UNS Blue", "0020430136710256b4036ce2" },
|
||||||
|
} },
|
||||||
|
{ name = "TheWolfBunny64/Anime and Manga Pack", palettes = {
|
||||||
|
{ "ANX BLUE", "0f2d451a4c74297ab93499e8" },
|
||||||
|
{ "ALM Hero Palette", "212f79d12021f3db43eff0f0" },
|
||||||
|
{ "Anime DGV Ver.", "1b241d2d3d3148624f5b7b63" },
|
||||||
|
{ "Anime Expo Red", "47110f771d19be2f28ee3b33" },
|
||||||
|
{ "Anime Expo Ver.", "242a2d656e729ba3a6e5eaeb" },
|
||||||
|
{ "BRS Blood", "3806065d0a0a951010bb1414" },
|
||||||
|
{ "Blue Stripes Ver.", "2d2e2e5b5d5d999b9c8bd3e1" },
|
||||||
|
{ "CDC Pink", "430427e10e82eac3d6f2f4f7" },
|
||||||
|
{ "Colorful Horizons", "373939d1525260bdc7f6cf72" },
|
||||||
|
{ "CRR Orange", "49230a7a3a11c35d1bf47522" },
|
||||||
|
{ "DKU Alpha Emerald", "11332f1c564f2d8a7e39ad9e" },
|
||||||
|
{ "DKU Gamma Palette", "24262b166668b6bec8f0e8d6" },
|
||||||
|
{ "DKU Vigilante Palette", "13131538534e878b92ada89a" },
|
||||||
|
{ "DMS Gold", "3734195d572b948c44baaf56" },
|
||||||
|
{ "DRM Tricolor", "450000e6000000a8f4ffe800" },
|
||||||
|
{ "DGB Orange", "48270878410ec06817f0831d" },
|
||||||
|
{ "DYN Hero Palette", "16161629463dc84a31d1d0cc" },
|
||||||
|
{ "EV-01", "303345765898f99b2254cf54" },
|
||||||
|
{ "EDN Academy Uniform", "483e39cc4c47dcbc76f7f8f5" },
|
||||||
|
{ "FRG Pretense Palette", "0000008d0000697d738ea99b" },
|
||||||
|
{ "GDS VISION", "4723008e4700ee77009fa0a0" },
|
||||||
|
{ "GR5 Fever", "311d3da662cdc5cedff7f7f7" },
|
||||||
|
{ "GKU GT Gi", "1221313d6ea5f0ac18dbe3e6" },
|
||||||
|
{ "GKU Gi", "061222173f72e7612cf9f0e1" },
|
||||||
|
{ "Grand Ivory", "4140396c6b5fadab98d9d6be" },
|
||||||
|
{ "GRZ Coat", "3d0b32ce26a9fce72dfbfbfa" },
|
||||||
|
{ "HKG Orange", "462718754129bb6841ea8352" },
|
||||||
|
{ "KNS Sherbet", "440021890042e5006ef08200" },
|
||||||
|
{ "KWB Blue", "00283b004462006c9d0088c5" },
|
||||||
|
{ "Legendary SSJ", "31411b536d2d84ae48a6da5b" },
|
||||||
|
{ "LSA Orange!", "461c00752f00bc4b00eb5e01" },
|
||||||
|
{ "Perfect MJN Emperor", "2718328550a9a3b453fdc1bf" },
|
||||||
|
{ "Perfected ULI", "393c4060646c99a0acc0c8d8" },
|
||||||
|
{ "PRC Marble Raspberry", "400a1b6b112eab1b49d6225c" },
|
||||||
|
{ "PTG Gold", "3633275a5541908868b4aa82" },
|
||||||
|
{ "SYN Beast Silver", "2d2e344b4d57787c8c969baf" },
|
||||||
|
{ "SHN Green", "1b3a162d6125489c3b5ac34a" },
|
||||||
|
{ "STH Red", "4a18127c291ec64130f8523c" },
|
||||||
|
{ "SSJ 3", "4a3c107c641cc6a02cf8c838" },
|
||||||
|
{ "SSJ Blue Evolved", "082d3e0d4b681578a71b97d1" },
|
||||||
|
{ "SSJ Blue", "01383d025d660496a305bccc" },
|
||||||
|
{ "SSJ God", "40001d6b0131ac024ed70362" },
|
||||||
|
{ "SSJ Rose", "4a34357b5759c58c8ff7afb3" },
|
||||||
|
{ "SSJ", "4c4b397f7e60cbc99afefcc1" },
|
||||||
|
{ "SVC Uniform", "321d1a593d34ac7c59acaba9" },
|
||||||
|
{ "SWA Cyan", "1a3a432c6171479cb459c3e2" },
|
||||||
|
{ "TMR Uniform", "474f4d755e88e94e60eeefeb" },
|
||||||
|
{ "Tri DGV Ver.", "272a2442473c697260848f79" },
|
||||||
|
{ "UAH Uniform", "0c4856a02929a4aaafededed" },
|
||||||
|
{ "UTV VISION", "262c48e24465f1a7b5f4f4f4" },
|
||||||
|
{ "ULI Sign", "1b1f212d34374853585a686f" },
|
||||||
|
{ "URM Green", "1734282657443d8b6c4dae88" },
|
||||||
|
{ "VGT Armor", "0619321554a9e3c56cefefef" },
|
||||||
|
{ "ANM vision", "231815385eaaf9be00ffffff" },
|
||||||
|
{ "HLO blue", "063f5c3368d349c4f2b0edfa" },
|
||||||
|
} },
|
||||||
|
{ name = "TheWolfBunny64/Bandai Namco Pack", palettes = {
|
||||||
|
{ "1st Vision Pastel", "101c33385ead7faf5aea7cb1" },
|
||||||
|
{ "765P Ver.", "383a445d6272959cb6bbc4e4" },
|
||||||
|
{ "765P Pink", "481720792736c23f57f34f6d" },
|
||||||
|
{ "765P TRICOLOR", "0b143f2743d2e22b30b4e04b" },
|
||||||
|
{ "ARS BLUE", "0e192e182a4e26447c30559c" },
|
||||||
|
{ "AQR Blue", "0030450050740080ba00a0e9" },
|
||||||
|
{ "BDN Tricolor", "001f350069b1df4f61f6b700" },
|
||||||
|
{ "CND Blue", "0b263c1340641e67a02681c8" },
|
||||||
|
{ "IDOL WORLD TRICOLOR!!!", "0b263c2681c8f34f6dffc30b" },
|
||||||
|
{ "LLA Purple!", "31152e52234d84387ca5469b" },
|
||||||
|
{ "MLV GOLD!", "3d351d665930a48e4dcdb261" },
|
||||||
|
{ "MLN Yellow!", "4c3a037f6105cc9c08ffc30b" },
|
||||||
|
{ "Moonlight Vision", "1010103050783890e8f8d868" },
|
||||||
|
{ "MUS Pink", "44002672003fb60065e4007f" },
|
||||||
|
{ "NMC Idol Pink", "4c22377f3a5ccc5c93ff74b8" },
|
||||||
|
{ "NJG Orange", "482d00794c00c27900f39800" },
|
||||||
|
{ "PCM-PALETTE", "420000dc0000ff8c00ffd800" },
|
||||||
|
{ "PCM Vision", "0000003732ffffb897ffff00" },
|
||||||
|
{ "PCM Yellow", "4c44007f7100ccb500ffe300" },
|
||||||
|
{ "RADIANT SMILE RAMP", "1b1c81e6016b01b3c4fff89b" },
|
||||||
|
{ "RX-78-2 GDM Mode", "333f3fa73736dbad40dbdcd5" },
|
||||||
|
{ "RYG Sunset", "2b10399238befd99e1ffe43f" },
|
||||||
|
{ "SHINY Sky Blue", "2a384c465d7f7095cc8dbbff" },
|
||||||
|
{ "SNS Red", "3911105f1c1b982d2bbf3936" },
|
||||||
|
{ "School Idol Blue", "2830663960d787adf5f9f9f8" },
|
||||||
|
{ "School Idol Mix", "31152ea5469b00a0e9f39800" },
|
||||||
|
{ "SDM Green", "04392c075f4a0c98760fbe94" },
|
||||||
|
{ "Sunny Passion Paradise", "0920341e6caee06846f0ba40" },
|
||||||
|
{ "SYL Rose Pink", "452728744243b9696be88486" },
|
||||||
|
{ "Tarnished Gold", "392f18604e299a7d42c19d53" },
|
||||||
|
{ "Vulnerable Blue", "ffb897cc93787f5c4b3732ff" },
|
||||||
|
{ "Wind Ring Emerald", "0c3627145a4220906a28b585" },
|
||||||
|
} },
|
||||||
|
{ name = "TheWolfBunny64/Company Pack", palettes = {
|
||||||
|
{ "AMZ Vision", "252f3e008296ff9900ffffff" },
|
||||||
|
{ "AND Green", "1242271e6e4230b0693ddc84" },
|
||||||
|
{ "APL Silver", "30333351555681888aa2aaad" },
|
||||||
|
{ "DUO Green", "1a3d002c660146a30158cc02" },
|
||||||
|
{ "DRC Copper", "3c291b64442ea06d4ac8895d" },
|
||||||
|
{ "GGL Blue", "13274921427a346ac34285f4" },
|
||||||
|
{ "GGL Green", "0f32181a542929864234a853" },
|
||||||
|
{ "GGL Red", "46140f75211abb352aea4335" },
|
||||||
|
{ "GGL Yellow", "4b38017d5e02c89604fbbc05" },
|
||||||
|
{ "SPT Green", "09401c0f6b3018ac4c1ed760" },
|
||||||
|
} },
|
||||||
|
{ name = "TheWolfBunny64/Food and Drink Pack", palettes = {
|
||||||
|
{ "7EL Color Combo", "062218147350eb0f2aff6c00" },
|
||||||
|
{ "BJB Beach", "273d3b4e7a7683ccc5dbe441" },
|
||||||
|
{ "BJB Storm", "1f3a31346152539b8368c2a4" },
|
||||||
|
{ "BKR Sundae", "40202178284bc04078f05097" },
|
||||||
|
{ "BGK Color Combo", "502314d62300ff8732f5ebdc" },
|
||||||
|
{ "CHT Orange", "442300723b00b75e00e57600" },
|
||||||
|
{ "CRK Color Combo", "460d0a8d1b15ec2e24f99b2a" },
|
||||||
|
{ "CCL Vision", "000000f40009d7d7d7ffffff" },
|
||||||
|
{ "Dew Green", "2d40004b6b0078ac0097d700" },
|
||||||
|
{ "DMP Vision", "000000006491e31837ffffff" },
|
||||||
|
{ "DRP Red", "290a0e4511186e1b278a2231" },
|
||||||
|
{ "DNK Vision", "48032b910756f20c90ff6e0c" },
|
||||||
|
{ "KFR Red", "33070e550c17881325ab182f" },
|
||||||
|
{ "KKR Vision", "000000166938cf152dffffff" },
|
||||||
|
{ "Lemon-Lime Green", "18310e30631d51a631f1c545" },
|
||||||
|
{ "MTD Ver.", "29673cd82a34a1d23fffffff" },
|
||||||
|
{ "PCS BLUE", "001b35002e59004a8e015db2" },
|
||||||
|
{ "PPS Vision", "0000001414c8ff1400ffffff" },
|
||||||
|
{ "PZH Red", "441012711c1fb52c31e3383e" },
|
||||||
|
{ "SPR Green", "002e17004d27007c3e009b4e" },
|
||||||
|
} },
|
||||||
|
{ name = "TheWolfBunny64/Gaming Pack", palettes = {
|
||||||
|
{ "AVG Raging Volcano", "1c0000a81000f83800f8b800" },
|
||||||
|
{ "Blue Bomber Vision", "0000000d4dc4639afce2cda7" },
|
||||||
|
{ "BBN Blue", "093e4b0f687e18a7ca1fd1fd" },
|
||||||
|
{ "BBG Green", "1f420e356e1854b0276adc31" },
|
||||||
|
{ "CHE Green", "20800040980080c800a0e000" },
|
||||||
|
{ "Classic Blurple", "22294138446d5b6dae7289da" },
|
||||||
|
{ "DMG Pea Green", "204631527f39aec440d7e894" },
|
||||||
|
{ "EBT Prolouge", "2c17084a290b854a1dc08226" },
|
||||||
|
{ "GGR Orange", "45230e743b17ba5e25e9762f" },
|
||||||
|
{ "Giga Kiwi DMG", "384828607028a0a830d0e040" },
|
||||||
|
{ "Investigation Yellow", "4c4a077f7c0cccc714fff919" },
|
||||||
|
{ "Phantom Red", "4b0b117e131cca1e2dfd2639" },
|
||||||
|
{ "PYO Green", "15431024771b39b42b48e236" },
|
||||||
|
{ "SES Blue", "073e4c0c687f14a7cc19d1ff" },
|
||||||
|
{ "SGA Tokyo Blue", "00273f00416a0068a90082d4" },
|
||||||
|
{ "Scarlett Green", "2e48034d78057cc0089bf00b" },
|
||||||
|
{ "Slime Blue", "0e2a3d1746662570a32f8ccc" },
|
||||||
|
{ "SNC Mega Blue", "13274120426c3369ad4084d9" },
|
||||||
|
{ "STM Gray", "3b3a396261609d9c99c5c3c0" },
|
||||||
|
{ "TYV Brown", "372c1f5c4a34937654b89469" },
|
||||||
|
{ "Tropical Cyber Space", "1414394344c1ef58f763fdfb" },
|
||||||
|
{ "XBX Green", "2b3c1249641f74a03192c83e" },
|
||||||
|
} },
|
||||||
|
{ name = "TheWolfBunny64/Holiday Pack", palettes = {
|
||||||
|
{ "Christmas Gold", "39321660542599873cc0a94b" },
|
||||||
|
{ "Christmas Silver", "2c32354a545977868e95a8b2" },
|
||||||
|
{ "Christmas Ver.", "300f0fa0323220a465cbb96a" },
|
||||||
|
{ "Classy Christmas", "534d579e5c5e8bab95e8e7df" },
|
||||||
|
{ "Clover Green", "1136161c5a252d903b39b54a" },
|
||||||
|
{ "Cupid's Love Palette", "5e081eb51a3ae48397e4cdd3" },
|
||||||
|
{ "Festive Christmas", "0a1609224b21cc1902bb9b60" },
|
||||||
|
{ "Glacial Winter Blue", "2839434360716c9ab487c1e2" },
|
||||||
|
{ "Grinchy Green", "3639085b5f0e929816b7be1c" },
|
||||||
|
{ "Halloween Ver.", "2c13319540a5f68c00ffcc00" },
|
||||||
|
{ "Haunted Halloween", "2f094c9e1fffffa52ecaff37" },
|
||||||
|
{ "Independence Boy", "0000000a3161b31942ffffff" },
|
||||||
|
{ "Irish Green", "143923225f3b37985e45be76" },
|
||||||
|
{ "Spooky Purple", "2f253f4f3e697e63a89e7cd2" },
|
||||||
|
{ "Winter Christmas", "341113ae3b4065b08fdddddd" },
|
||||||
|
} },
|
||||||
|
{ name = "TheWolfBunny64/J-Pop Pack", palettes = {
|
||||||
|
{ "A48 Pink", "4923317b3b53c45e84f676a6" },
|
||||||
|
{ "ATG PARETTO", "030a5b7a5375c375bcf4a7eb" },
|
||||||
|
{ "BBM KITSUNE RED", "2c00034a000677000995000c" },
|
||||||
|
{ "H46 Blue", "1b39442d5f724898b75bbee5" },
|
||||||
|
{ "J-Pop Idol Sherbet", "260c2b8129905bbee5f19db5" },
|
||||||
|
{ "MKU Blue", "05333f08566a0d8aaa11add5" },
|
||||||
|
{ "NZU PrePlay Palette", "47120fef3f3500b8b0fff200" },
|
||||||
|
{ "N46 Purple", "260c2b401448672073812990" },
|
||||||
|
{ "OOR RED", "370f0c5c1a14942920b93429" },
|
||||||
|
{ "YSB AMARANTH", "480c1b79142dc12048f2285a" },
|
||||||
|
} },
|
||||||
|
{ name = "TheWolfBunny64/Japanese Company Pack", palettes = {
|
||||||
|
{ "ANA Flight Blue", "00146e223f9a3b8bc000b3f0" },
|
||||||
|
{ "FMM Vision", "00301300602600a040008cd6" },
|
||||||
|
{ "LWS BLUE", "001f3600345b0053920068b7" },
|
||||||
|
{ "NHK Silver Gray", "262626404040666666808080" },
|
||||||
|
{ "SIT SUPER BLUE", "0b2438133d5e1f6196277abc" },
|
||||||
|
{ "SIT SUPER GREEN", "0634270b5742118b6a16ae85" },
|
||||||
|
{ "SKO Timely Vision", "2021210050a54393e6ffbf00" },
|
||||||
|
{ "SB109 PASTEL", "5503a63dacb8fd87b2f2d53f" },
|
||||||
|
{ "SNR Pink", "83534de74b5af485a1f9c2d0" },
|
||||||
|
{ "TKS CLOUDY BLUE", "27363b415a6368909f82b5c7" },
|
||||||
|
{ "NCO sea green", "073a310c6152149c8319c3a4" },
|
||||||
|
} },
|
||||||
|
{ name = "TheWolfBunny64/K-Pop Pack", palettes = {
|
||||||
|
{ "BGT ARMY PURPLE", "26154040246c6639ac8048d8" },
|
||||||
|
{ "BKP BLINK PINK", "4932377a535dc38594f4a7ba" },
|
||||||
|
{ "EER COSMIC LATTE", "4c4a457f7c73ccc6b8fff8e7" },
|
||||||
|
{ "LSS FEARLESS BLUE", "26314a40527c6784c781a5f9" },
|
||||||
|
{ "NCZ YELLOW", "43451e717333b4b851e2e766" },
|
||||||
|
{ "SVT COOL CARAT", "2b323e57647d92a8d1f7cac9" },
|
||||||
|
{ "SHW GREEN", "22413e396d685cafa673dbd0" },
|
||||||
|
{ "SWN PINK", "48172579273ec13f63f24f7c" },
|
||||||
|
{ "SKD STAY PINK", "4c142d7f224ccc367aff4499" },
|
||||||
|
{ "TROPICAL TWC APRICOT", "4c1c30993961ff5fa2fcc89b" },
|
||||||
|
} },
|
||||||
|
{ name = "TheWolfBunny64/Legacy Palette Pack", palettes = {
|
||||||
|
{ "TDS VC Green", "4a4a4a5a8c429cef29bdff21" },
|
||||||
|
{ "TDS VC Ver.", "2929186b6b52a5a58ccecead" },
|
||||||
|
{ "BLL PC Screen", "000000783800b86000f87800" },
|
||||||
|
{ "DMG Ver.", "2a453b365d48577c447f860f" },
|
||||||
|
{ "DGV Ver.", "2a2a2246463970705c8c8c73" },
|
||||||
|
{ "GMT Ver.", "12424c255955437a636ba64a" },
|
||||||
|
{ "GNW LCD", "3b3c336365569ea28ac6cbad" },
|
||||||
|
{ "GMK Ver.", "184421405d3b6b9c638cce94" },
|
||||||
|
{ "GBU Classic Ver.", "26302540503f67806481a17e" },
|
||||||
|
{ "HGM Ver.", "2d2d2b414f53687f84829fa6" },
|
||||||
|
{ "Light Ver.", "004f3b00694a009a7100b581" },
|
||||||
|
{ "LKA DX Ver.", "58382068884078c078f8f8b0" },
|
||||||
|
{ "MBM Ver.", "303030505050808080a0a0a0" },
|
||||||
|
{ "MGM V Ver.", "08203040689070a0e0d0d0d0" },
|
||||||
|
{ "NGP Ver.", "101010707070b0b0b0f0f0f0" },
|
||||||
|
{ "NKA 3310 Ver.", "2231273953425c846973a684" },
|
||||||
|
{ "Pocket Tales Ver.", "00000038500088a000d0d860" },
|
||||||
|
{ "Pocket Ver.", "1f1f1f4d533c8b956dc4cfa1" },
|
||||||
|
{ "PKS Ver.", "2d2d284b4b4378786c969687" },
|
||||||
|
{ "PKM Pinball Ver.", "181820786030a0b050e8f8b8" },
|
||||||
|
{ "PKM Ver.", "181010807098f0b088f8e8f8" },
|
||||||
|
{ "PKM mini Ver.", "1b2d1b525c52849484a5b9a5" },
|
||||||
|
{ "PKT Ver.", "10281838503050805070b070" },
|
||||||
|
{ "Rocky-Valley Holiday", "204008805850d89078c0f0f8" },
|
||||||
|
{ "T83 Ver.", "2e332a4e55467c88709caa8c" },
|
||||||
|
{ "TMG Ver.", "3c383878787cc0c0c7f1f0f9" },
|
||||||
|
{ "TGC Ver.", "0000000f4f2f6f8f4fdfff8f" },
|
||||||
|
{ "Timing Hero Ver.", "202e004c67188c994ccccc99" },
|
||||||
|
{ "Travel Wood", "482810705030a08058f8d8b0" },
|
||||||
|
{ "VMU Ver.", "0814804466546ca38688cca8" },
|
||||||
|
{ "VTB Ver.", "ff0000aa0000550000000000" },
|
||||||
|
{ "WTS Ver.", "0c322c2c626454a68c7cc67c" },
|
||||||
|
{ "WDS Ver.", "2c2c2c686868c0c0c0fefefe" },
|
||||||
|
} },
|
||||||
|
{ name = "TheWolfBunny64/Misc. Palette Pack", palettes = {
|
||||||
|
{ "Camouflage Ver.", "37353879533dac7e54bcab90" },
|
||||||
|
{ "Cherry Blossom Pink", "482534783f58c0648cf07eb0" },
|
||||||
|
{ "Emerald Green", "183c2428643c40a06050c878" },
|
||||||
|
{ "Fool's Gold and Silver", "2d30345a606997a1b0c5c66d" },
|
||||||
|
{ "Glitchy Blue", "0f254b193f7d2864c8337efb" },
|
||||||
|
{ "Gold, Silver, and Bronze", "2d1f1499684386949abeb049" },
|
||||||
|
{ "Golden Trophy", "453e0774680cb9a613e8d018" },
|
||||||
|
{ "Greenscale Ver.", "0c360c2c62346e870a9cbe0c" },
|
||||||
|
{ "Leprechaun Green", "10281d1b44302c6c4d378861" },
|
||||||
|
{ "Nightvision Green", "66bf2f519825335f171e390e" },
|
||||||
|
{ "Rising Sun Red", "38000d5d0016960024bc002d" },
|
||||||
|
{ "Treasure Gold", "3c360a655a12a2901ccbb524" },
|
||||||
|
{ "Wild West Vision", "3a160e924a36c3976ad9d7c7" },
|
||||||
|
} },
|
||||||
|
{ name = "TheWolfBunny64/Console Pack", palettes = {
|
||||||
|
{ "Advanced Indigo", "241f373c355d605494796aba" },
|
||||||
|
{ "Aegis Cherry", "42111e6e1d32b02f50dd3b64" },
|
||||||
|
{ "Ancient HSU Brown", "352f2b594f488f7f73b39f90" },
|
||||||
|
{ "ANC Green", "002e25004d3f007c64009b7e" },
|
||||||
|
{ "Brilliant Diamond Blue", "2638433f5d706595b47fbbe1" },
|
||||||
|
{ "COLLECTION of SAG Ver.", "041820345d51769a67b2c0a8" },
|
||||||
|
{ "Champion's Tunic", "281b13875b40009edde2dcb1" },
|
||||||
|
{ "CHZ Blue", "1735432759703e8fb44eb3e1" },
|
||||||
|
{ "DKG Arcade Blue", "15304223516f3881b147a2de" },
|
||||||
|
{ "DKG Barrel Brown", "3a220e613a179c5c25c3742f" },
|
||||||
|
{ "DMG-099", "3132313f642f6bad1984b510" },
|
||||||
|
{ "DMG-GOLD", "30361c505a3080904ca1b560" },
|
||||||
|
{ "DMG-SWITCH", "214231426b296c94218cad28" },
|
||||||
|
{ "DRL GB Ver.", "48530e788b1db9d03af6ff70" },
|
||||||
|
{ "EVE Brown", "3c2a0f644619a07028c88d32" },
|
||||||
|
{ "FMC Disk Yellow", "483a00796100c29b00f3c200" },
|
||||||
|
{ "FMC Frenzy", "231916a32135d9be72efecda" },
|
||||||
|
{ "Frog Coin Green", "00390039840000ef00fff7de" },
|
||||||
|
{ "Fury Blue", "0c1c2d152f4c224c792b5f98" },
|
||||||
|
{ "GMC Glimmer", "3e1318cf415111a396b6bed3" },
|
||||||
|
{ "Golden Wild", "372f1e5c4f32947f50b99f65" },
|
||||||
|
{ "GMB Brown", "331a11552c1d88472faa593b" },
|
||||||
|
{ "Green Awakening", "000b1636705898db75f1ffdd" },
|
||||||
|
{ "Green Banana", "2149003969394a9e0063df08" },
|
||||||
|
{ "INK Tricolor", "1c114c603bffff505eeaff3d" },
|
||||||
|
{ "LCD Clock Green", "183626285a4040906650b580" },
|
||||||
|
{ "LBO Fawn", "4033226b5539ac885cd7aa73" },
|
||||||
|
{ "MRO Red", "430400700700b40c00e10f00" },
|
||||||
|
{ "Metallic PLD Brass", "302d0f514c1a817929a29834" },
|
||||||
|
{ "Neon Blue", "033745055c730894b80ab9e6" },
|
||||||
|
{ "Neon Green", "0942000f6e0018b0001edc00" },
|
||||||
|
{ "Neon Orange", "4b30017d5002c88004faa005" },
|
||||||
|
{ "Neon Pink", "4c0f247f193ccc2860ff3278" },
|
||||||
|
{ "Neon Purple", "3600455a00739000b8b400e6" },
|
||||||
|
{ "Neon Red", "4c120c7f1e14cc3020ff3c28" },
|
||||||
|
{ "Neon Yellow", "454c00737f00b8cc00e6ff00" },
|
||||||
|
{ "Odyssey Boy", "2224215054457e8e67acbe8c" },
|
||||||
|
{ "Odyssey Gold", "3a30006150009b8000c2a000" },
|
||||||
|
{ "POCKET SWT", "3038206372518d9c7bb5c69c" },
|
||||||
|
{ "PKC Yellow", "4c42007f6e00ccb000ffdc00" },
|
||||||
|
{ "PKD Red", "461918752a28bb4340ea5450" },
|
||||||
|
{ "Royal Blue", "151949232a7a3844c44655f5" },
|
||||||
|
{ "Shining Pearl Pink", "3f2a30694750a87180d28ea0" },
|
||||||
|
{ "Sky Pop Ivory", "52502586825cbebb95e5e0b8" },
|
||||||
|
{ "Super FMC Supreme", "0846bad9404044ac71feda5a" },
|
||||||
|
{ "Super Mushroom Vision", "000000923404cc9e22f7cec3" },
|
||||||
|
{ "Super Star Pink", "440d2372163bb7235fe52c77" },
|
||||||
|
{ "Superball Ivory", "646432828250bcbc8aeef0bc" },
|
||||||
|
{ "Timeless Gold and Red", "37090a6f1215b91e23c8aa50" },
|
||||||
|
{ "Ultra Black", "17181d2629313d414f4d5263" },
|
||||||
|
{ "WRW MicroBlue", "05293c0844650d6da11189ca" },
|
||||||
|
{ "Wonder Purple", "451a42732c6fb846b2e658df" },
|
||||||
|
{ "Yellow Banana", "734900ad6939de9e00ffdf08" },
|
||||||
|
{ "YSH Egg Green", "1e3a0e336218519c2666c430" },
|
||||||
|
} },
|
||||||
|
{ name = "TheWolfBunny64/Sports Pack", palettes = {
|
||||||
|
{ "MLB Vision", "041e42bf0d3e057affffffff" },
|
||||||
|
{ "NASCAR Ver.", "000000007ac2e4002bffd659" },
|
||||||
|
{ "NBA Vision", "000000253b73c8102effffff" },
|
||||||
|
{ "NCAA Blue", "002e42004e6f007cb1009cde" },
|
||||||
|
{ "NFL Vision", "000000013369d50a0affffff" },
|
||||||
|
{ "OLY Bronze", "3d2618664029a46741cd8152" },
|
||||||
|
{ "OLY Gold", "3f360a6a5b12aa911cd5b624" },
|
||||||
|
{ "OLY Silver", "2f312e4f524e7e847c9ea59c" },
|
||||||
|
{ "PGA Tour Vision", "000000003c80f1373dffffff" },
|
||||||
|
{ "WWE White and Red", "40070c810e19d7182affffff" },
|
||||||
|
} },
|
||||||
|
{ name = "TheWolfBunny64/Traditional JPN Colors Pack", palettes = {
|
||||||
|
{ "Aquatic Iro", "192f603e62ad2ca9e1a0d8ef" },
|
||||||
|
{ "Fruity Orange", "241a089f563af08300f3bf88" },
|
||||||
|
{ "Ghostly Aoi", "0f235019448e5a79ba84a2d4" },
|
||||||
|
{ "Golden Kiiro", "6a5d21a69425dccb18f8e58c" },
|
||||||
|
{ "Lime Midori", "4759507b8d42aacf53e0ebaf" },
|
||||||
|
{ "Oni Aka", "640125a22041d9333fec6d71" },
|
||||||
|
{ "Sakura Pink", "a25768e7609eeebbcbfdeff2" },
|
||||||
|
{ "Silver Shiro", "383c3c727171afafb0dcdddd" },
|
||||||
|
{ "Tea Midori", "33363102876088cb7fd6e9ca" },
|
||||||
|
{ "Wisteria Murasaki", "2e29307058a3a59acadbd0e6" },
|
||||||
|
} },
|
||||||
|
{ name = "Trashuncle/Artistic", palettes = {
|
||||||
|
{ "TU PSL Park", "9200aeb722d4d442f0ec89ff" },
|
||||||
|
{ "TU Quicksilver", "555555717286a4a5c3bbbce1" },
|
||||||
|
} },
|
||||||
|
{ name = "Trashuncle/Crush", palettes = {
|
||||||
|
{ "TU Blueberry Crush", "0000002811ff4dffffffffff" },
|
||||||
|
{ "TU Grape Crush", "0000003d43cb0083ebffffff" },
|
||||||
|
{ "TU Kiwi Crush", "000000059e322eef7affffff" },
|
||||||
|
{ "TU Lavender Crush", "000000812482ab6a9bffffff" },
|
||||||
|
{ "TU Orange Crush", "000000c23615ff632cffffff" },
|
||||||
|
{ "TU Pineapple Crush", "000000ffff00ffff80ffffff" },
|
||||||
|
{ "TU Strawberry Crush", "000000ea1211ff5250ffffff" },
|
||||||
|
} },
|
||||||
|
{ name = "Trashuncle/DMG", palettes = {
|
||||||
|
{ "TU DMG Bright", "0c3a1d2a61414c7b2b919707" },
|
||||||
|
{ "TU DMG Clean", "1749022b5402496503788603" },
|
||||||
|
{ "TU DMG Weak Alt", "2c4d2054733c597730677f31" },
|
||||||
|
{ "TU DMG Weak", "364e1a5772405e7a406d833d" },
|
||||||
|
} },
|
||||||
|
{ name = "Trashuncle/GBP", palettes = {
|
||||||
|
{ "TU GBP Bright", "5b5d5a879481abb8a7ffffff" },
|
||||||
|
{ "TU GBP Clean", "3d493b98a291adbfacedfff4" },
|
||||||
|
{ "TU GBP Weak Alt", "4c4b3170734e757952848459" },
|
||||||
|
{ "TU GBP Weak", "434738535a42535f49667159" },
|
||||||
|
} },
|
||||||
|
{ name = "Trashuncle/Light", palettes = {
|
||||||
|
{ "TU Light Bright", "00637400a1b100daea00e3f3" },
|
||||||
|
{ "TU Light Clean", "007577019fa000aeaf01b4b9" },
|
||||||
|
{ "TU Light Dark", "0076a80092d10098c500a5f2" },
|
||||||
|
{ "TU Light Idealized", "006e9d0093c500a6cc00ccd5" },
|
||||||
|
} },
|
||||||
|
}
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
-- Yellow-only Advanced (redpp) deltas. Merged on top of data/palettes_gbc.lua
|
||||||
|
-- world tables when GameVersion.isYellow(); Red/Blue never load this file's
|
||||||
|
-- world path. Named SuperPalettes stay washed on Yellow -- PaletteFX.pal
|
||||||
|
-- prefers CGBBase under Advanced instead (title MEWMON/LOGO, YELLOWMON).
|
||||||
|
-- See #1639.
|
||||||
|
|
||||||
|
return {
|
||||||
|
world = {
|
||||||
|
-- Summer Beach House: Yellow-only tileset. Tile→group map mirrors HOUSE
|
||||||
|
-- (closest Advanced indoor profile); colors lean sand / wood / water.
|
||||||
|
tileGroups = {
|
||||||
|
BEACH_HOUSE = {
|
||||||
|
[0] = 5,
|
||||||
|
[1] = 0,
|
||||||
|
[2] = 0,
|
||||||
|
[3] = 0,
|
||||||
|
[4] = 1,
|
||||||
|
[5] = 0,
|
||||||
|
[6] = 3,
|
||||||
|
[7] = 3,
|
||||||
|
[8] = 2,
|
||||||
|
[9] = 2,
|
||||||
|
[10] = 2,
|
||||||
|
[11] = 2,
|
||||||
|
[12] = 2,
|
||||||
|
[13] = 2,
|
||||||
|
[14] = 5,
|
||||||
|
[15] = 5,
|
||||||
|
[16] = 0,
|
||||||
|
[17] = 0,
|
||||||
|
[18] = 0,
|
||||||
|
[19] = 0,
|
||||||
|
[20] = 1,
|
||||||
|
[21] = 0,
|
||||||
|
[22] = 3,
|
||||||
|
[23] = 3,
|
||||||
|
[24] = 5,
|
||||||
|
[25] = 5,
|
||||||
|
[26] = 5,
|
||||||
|
[27] = 5,
|
||||||
|
[28] = 0,
|
||||||
|
[29] = 0,
|
||||||
|
[30] = 5,
|
||||||
|
[31] = 5,
|
||||||
|
[32] = 0,
|
||||||
|
[33] = 0,
|
||||||
|
[34] = 3,
|
||||||
|
[35] = 5,
|
||||||
|
[36] = 5,
|
||||||
|
[37] = 5,
|
||||||
|
[38] = 5,
|
||||||
|
[39] = 5,
|
||||||
|
[40] = 5,
|
||||||
|
[41] = 5,
|
||||||
|
[42] = 2,
|
||||||
|
[43] = 2,
|
||||||
|
[44] = 5,
|
||||||
|
[45] = 5,
|
||||||
|
[46] = 5,
|
||||||
|
[47] = 5,
|
||||||
|
[48] = 5,
|
||||||
|
[49] = 5,
|
||||||
|
[50] = 5,
|
||||||
|
[51] = 5,
|
||||||
|
[52] = 5,
|
||||||
|
[53] = 5,
|
||||||
|
[54] = 5,
|
||||||
|
[55] = 5,
|
||||||
|
[56] = 5,
|
||||||
|
[57] = 5,
|
||||||
|
[58] = 5,
|
||||||
|
[59] = 5,
|
||||||
|
[60] = 5,
|
||||||
|
[61] = 5,
|
||||||
|
[62] = 5,
|
||||||
|
[63] = 5,
|
||||||
|
[64] = 0,
|
||||||
|
[65] = 0,
|
||||||
|
[66] = 0,
|
||||||
|
[67] = 0,
|
||||||
|
[68] = 0,
|
||||||
|
[69] = 0,
|
||||||
|
[70] = 5,
|
||||||
|
[71] = 5,
|
||||||
|
[72] = 5,
|
||||||
|
[73] = 5,
|
||||||
|
[74] = 5,
|
||||||
|
[75] = 5,
|
||||||
|
[76] = 5,
|
||||||
|
[77] = 5,
|
||||||
|
[78] = 5,
|
||||||
|
[79] = 5,
|
||||||
|
[80] = 5,
|
||||||
|
[81] = 5,
|
||||||
|
[82] = 5,
|
||||||
|
[83] = 5,
|
||||||
|
[84] = 5,
|
||||||
|
[85] = 5,
|
||||||
|
[86] = 5,
|
||||||
|
[87] = 5,
|
||||||
|
[88] = 5,
|
||||||
|
[89] = 5,
|
||||||
|
[90] = 5,
|
||||||
|
[91] = 5,
|
||||||
|
[92] = 5,
|
||||||
|
[93] = 5,
|
||||||
|
[94] = 5,
|
||||||
|
[95] = 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
groupColors = {
|
||||||
|
BEACH_HOUSE = {
|
||||||
|
-- sand / wood floor
|
||||||
|
{
|
||||||
|
{ 255, 239, 198 },
|
||||||
|
{ 206, 173, 115 },
|
||||||
|
{ 156, 123, 74 },
|
||||||
|
{ 58, 58, 58 },
|
||||||
|
},
|
||||||
|
-- signs / accents (warm coral)
|
||||||
|
{
|
||||||
|
{ 255, 239, 198 },
|
||||||
|
{ 255, 156, 148 },
|
||||||
|
{ 230, 82, 66 },
|
||||||
|
{ 58, 58, 58 },
|
||||||
|
},
|
||||||
|
-- foliage
|
||||||
|
{
|
||||||
|
{ 255, 239, 198 },
|
||||||
|
{ 123, 189, 82 },
|
||||||
|
{ 66, 123, 41 },
|
||||||
|
{ 58, 58, 58 },
|
||||||
|
},
|
||||||
|
-- water / windows
|
||||||
|
{
|
||||||
|
{ 255, 239, 198 },
|
||||||
|
{ 99, 189, 230 },
|
||||||
|
{ 41, 115, 189 },
|
||||||
|
{ 58, 58, 58 },
|
||||||
|
},
|
||||||
|
-- yellow props
|
||||||
|
{
|
||||||
|
{ 255, 239, 198 },
|
||||||
|
{ 255, 255, 82 },
|
||||||
|
{ 230, 148, 25 },
|
||||||
|
{ 58, 58, 58 },
|
||||||
|
},
|
||||||
|
-- wood walls / furniture
|
||||||
|
{
|
||||||
|
{ 255, 239, 198 },
|
||||||
|
{ 189, 140, 74 },
|
||||||
|
{ 140, 99, 41 },
|
||||||
|
{ 58, 58, 58 },
|
||||||
|
},
|
||||||
|
-- sky / cool trim
|
||||||
|
{
|
||||||
|
{ 255, 239, 198 },
|
||||||
|
{ 148, 189, 255 },
|
||||||
|
{ 99, 148, 230 },
|
||||||
|
{ 58, 58, 58 },
|
||||||
|
},
|
||||||
|
-- text
|
||||||
|
{
|
||||||
|
{ 255, 255, 255 },
|
||||||
|
{ 255, 255, 255 },
|
||||||
|
{ 255, 255, 255 },
|
||||||
|
{ 0, 0, 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
-- Yellow spriteOrder is 82 entries; Red Advanced assignment is 72.
|
||||||
|
-- Indices 60-69 are Yellow inserts (Pikachu, Jenny, mons, Jessie/James);
|
||||||
|
-- Red's ball/fossil/snorlax/... shift to 70-81.
|
||||||
|
spriteAssignment = {
|
||||||
|
[0] = 0,
|
||||||
|
[1] = 1,
|
||||||
|
[2] = 3,
|
||||||
|
[3] = "random",
|
||||||
|
[4] = 0,
|
||||||
|
[5] = "random",
|
||||||
|
[6] = "random",
|
||||||
|
[7] = "random",
|
||||||
|
[8] = 0,
|
||||||
|
[9] = "random",
|
||||||
|
[10] = "random",
|
||||||
|
[11] = "random",
|
||||||
|
[12] = "random",
|
||||||
|
[13] = "random",
|
||||||
|
[14] = "random",
|
||||||
|
[15] = 1,
|
||||||
|
[16] = 1,
|
||||||
|
[17] = "random",
|
||||||
|
[18] = "random",
|
||||||
|
[19] = "random",
|
||||||
|
[20] = "random",
|
||||||
|
[21] = 2,
|
||||||
|
[22] = 1,
|
||||||
|
[23] = 3,
|
||||||
|
[24] = "random",
|
||||||
|
[25] = "random",
|
||||||
|
[26] = "random",
|
||||||
|
[27] = "random",
|
||||||
|
[28] = "random",
|
||||||
|
[29] = 0,
|
||||||
|
[30] = 3,
|
||||||
|
[31] = 3,
|
||||||
|
[32] = "random",
|
||||||
|
[33] = "random",
|
||||||
|
[34] = "random",
|
||||||
|
[35] = "random",
|
||||||
|
[36] = "random",
|
||||||
|
[37] = "random",
|
||||||
|
[38] = "random",
|
||||||
|
[39] = "random",
|
||||||
|
[40] = 0,
|
||||||
|
[41] = 2,
|
||||||
|
[42] = "random",
|
||||||
|
[43] = "random",
|
||||||
|
[44] = "random",
|
||||||
|
[45] = "random",
|
||||||
|
[46] = "random",
|
||||||
|
[47] = "random",
|
||||||
|
[48] = "random",
|
||||||
|
[49] = "random",
|
||||||
|
[50] = "random",
|
||||||
|
[51] = "random",
|
||||||
|
[52] = "random",
|
||||||
|
[53] = "random",
|
||||||
|
[54] = "random",
|
||||||
|
[55] = 0,
|
||||||
|
[56] = 1,
|
||||||
|
[57] = 3,
|
||||||
|
[58] = 0,
|
||||||
|
[59] = 0,
|
||||||
|
[60] = 4, -- SPRITE_PIKACHU
|
||||||
|
[61] = 1, -- SPRITE_OFFICER_JENNY
|
||||||
|
[62] = 3, -- SPRITE_SANDSHREW
|
||||||
|
[63] = 2, -- SPRITE_ODDISH
|
||||||
|
[64] = 2, -- SPRITE_BULBASAUR
|
||||||
|
[65] = "random", -- SPRITE_JIGGLYPUFF
|
||||||
|
[66] = "random", -- SPRITE_CLEFAIRY
|
||||||
|
[67] = "random", -- SPRITE_CHANSEY
|
||||||
|
[68] = "random", -- SPRITE_JESSIE
|
||||||
|
[69] = "random", -- SPRITE_JAMES
|
||||||
|
[70] = 0, -- SPRITE_POKE_BALL
|
||||||
|
[71] = 0, -- SPRITE_FOSSIL
|
||||||
|
[72] = 3, -- SPRITE_BOULDER
|
||||||
|
[73] = 3, -- SPRITE_PAPER
|
||||||
|
[74] = 0, -- SPRITE_POKEDEX
|
||||||
|
[75] = 3, -- SPRITE_CLIPBOARD
|
||||||
|
[76] = 0, -- SPRITE_SNORLAX
|
||||||
|
[77] = 3, -- SPRITE_UNUSED_OLD_AMBER
|
||||||
|
[78] = 3, -- SPRITE_OLD_AMBER
|
||||||
|
[79] = "random",
|
||||||
|
[80] = "random",
|
||||||
|
[81] = 3, -- SPRITE_GAMBLER_ASLEEP
|
||||||
|
},
|
||||||
|
|
||||||
|
-- Group 4 is Pikachu yellow on Yellow Advanced (Red keeps its own [4]).
|
||||||
|
spritePalettes = {
|
||||||
|
[4] = {
|
||||||
|
{ 222, 255, 222 },
|
||||||
|
{ 255, 255, 0 },
|
||||||
|
{ 230, 115, 0 },
|
||||||
|
{ 0, 0, 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -42,27 +42,29 @@ local LINK_HEADINGS = { "HOW TO LINK", "COLOSSEUM", "TRADE CENTER" }
|
|||||||
|
|
||||||
local function linkCableHelp(game)
|
local function linkCableHelp(game)
|
||||||
local text = game.data.text or {}
|
local text = game.data.text or {}
|
||||||
local items, showMenu, askHeading
|
local items, menu, openMenu
|
||||||
function showMenu()
|
local function closeAll()
|
||||||
game.stack:push(Menu.new(game, items,
|
game.stack:pop()
|
||||||
{ 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
|
end
|
||||||
items = {}
|
items = {}
|
||||||
for i, label in ipairs(LINK_HEADINGS) do
|
for i, label in ipairs(LINK_HEADINGS) do
|
||||||
items[i] = { label = label, onSelect = function()
|
items[i] = { label = label, keepOpen = true, onSelect = function()
|
||||||
game.stack:push(TextBox.new(game,
|
game.stack:push(TextBox.new(game,
|
||||||
text["_LinkCableInfoText" .. i] or label, askHeading))
|
text["_LinkCableInfoText" .. i] or label))
|
||||||
end }
|
end }
|
||||||
end
|
end
|
||||||
items[#items + 1] = { label = "STOP READING" }
|
items[#items + 1] = { label = "STOP READING", onSelect = closeAll }
|
||||||
|
menu = Menu.new(game, items,
|
||||||
|
{ tx = 0, ty = 0, tw = 15, th = 10, rowStep = 2, itemY = 2,
|
||||||
|
onCancel = closeAll })
|
||||||
|
function openMenu() game.stack:push(menu) end
|
||||||
game.stack:push(TextBox.new(game,
|
game.stack:push(TextBox.new(game,
|
||||||
text._LinkCableHelpText1 or "TRAINER TIPS\fUsing a Game Link\nCable",
|
text._LinkCableHelpText1 or "TRAINER TIPS\fUsing a Game Link\nCable",
|
||||||
askHeading))
|
function()
|
||||||
|
game.stack:push(TextBox.new(game,
|
||||||
|
text._LinkCableHelpText2 or "Which heading do\nyou want to read?",
|
||||||
|
nil, { stay = { onShown = openMenu } }))
|
||||||
|
end))
|
||||||
end
|
end
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -84,21 +86,15 @@ return {
|
|||||||
{ "jump_if_false", 5 }, -- 2
|
{ "jump_if_false", 5 }, -- 2
|
||||||
{ "hide_object", "CELADON_MANSION_ROOF_HOUSE",
|
{ "hide_object", "CELADON_MANSION_ROOF_HOUSE",
|
||||||
"CELADONMANSION_ROOF_HOUSE_EEVEE_POKEBALL" }, -- 3 (old saves)
|
"CELADONMANSION_ROOF_HOUSE_EEVEE_POKEBALL" }, -- 3 (old saves)
|
||||||
{ "jump", 13 }, -- 4
|
{ "jump", 11 }, -- 4
|
||||||
{ "give_pokemon", "EEVEE", 25 }, -- 5
|
{ "give_pokemon", "EEVEE", 25, false, true }, -- 5
|
||||||
{ "jump_if_false", 12 }, -- 6 (party+box full)
|
{ "jump_if_false", 10 }, -- 6 (party+box full)
|
||||||
-- flag + HideObject before the jingle and GotMonText: the nickname
|
-- scripts/CeladonMansionRoofHouse.asm:17-20 (#426)
|
||||||
-- 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
|
{ "set_flag", "EVENT_GOT_EEVEE" }, -- 7
|
||||||
{ "hide_object", "CELADON_MANSION_ROOF_HOUSE",
|
{ "hide_object", "CELADON_MANSION_ROOF_HOUSE",
|
||||||
"CELADONMANSION_ROOF_HOUSE_EEVEE_POKEBALL" }, -- 8
|
"CELADONMANSION_ROOF_HOUSE_EEVEE_POKEBALL" }, -- 8
|
||||||
{ "text_sound", "Get_Item1" }, -- 9 (GotMonText jingle)
|
{ "jump", 11 }, -- 9
|
||||||
{ "show_text", "_GotMonText", { RAM = "EEVEE" } }, -- 10
|
{ "show_text", "_BoxIsFullText" }, -- 10
|
||||||
{ "jump", 13 }, -- 11
|
|
||||||
{ "show_text", "_BoxIsFullText" }, -- 12
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,10 @@
|
|||||||
return {
|
return {
|
||||||
CELADON_CITY = {
|
CELADON_CITY = {
|
||||||
talk = {
|
talk = {
|
||||||
-- CeladonCityPoliwrathText (scripts/CeladonCity.asm): text_far
|
|
||||||
-- _CeladonCityPoliwrathText, then plays the POLIWRATH cry and ends.
|
|
||||||
-- The cry playback has no port-side equivalent command, so we just
|
|
||||||
-- show the flavor line.
|
|
||||||
TEXT_CELADONCITY_POLIWRATH = {
|
TEXT_CELADONCITY_POLIWRATH = {
|
||||||
{"face_player"},
|
{"face_player"},
|
||||||
|
-- pokered/scripts/CeladonCity.asm:90
|
||||||
|
{"play_cry", "POLIWRATH", true},
|
||||||
{"show_text", "_CeladonCityPoliwrathText"},
|
{"show_text", "_CeladonCityPoliwrathText"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,30 +2,24 @@
|
|||||||
return {
|
return {
|
||||||
CELADON_MANSION_1F = {
|
CELADON_MANSION_1F = {
|
||||||
talk = {
|
talk = {
|
||||||
-- CeladonMansion1FClefairyText (scripts/CeladonMansion1F.asm): text_far
|
|
||||||
-- _CeladonMansion1FClefairyText, then plays the CLEFAIRY cry and ends.
|
|
||||||
-- The cry playback has no port-side equivalent command, so we just
|
|
||||||
-- show the flavor line.
|
|
||||||
TEXT_CELADONMANSION1F_CLEFAIRY = {
|
TEXT_CELADONMANSION1F_CLEFAIRY = {
|
||||||
{"face_player"},
|
{"face_player"},
|
||||||
|
-- pokered/scripts/CeladonMansion1F.asm:27
|
||||||
|
{"play_cry", "CLEFAIRY", true},
|
||||||
{"show_text", "_CeladonMansion1FClefairyText"},
|
{"show_text", "_CeladonMansion1FClefairyText"},
|
||||||
},
|
},
|
||||||
|
|
||||||
-- CeladonMansion1FMeowthText (scripts/CeladonMansion1F.asm): text_far
|
|
||||||
-- _CeladonMansion1FMeowthText, then plays the MEOWTH cry and ends.
|
|
||||||
-- The cry playback has no port-side equivalent command, so we just
|
|
||||||
-- show the flavor line.
|
|
||||||
TEXT_CELADONMANSION1F_MEOWTH = {
|
TEXT_CELADONMANSION1F_MEOWTH = {
|
||||||
{"face_player"},
|
{"face_player"},
|
||||||
|
-- pokered/scripts/CeladonMansion1F.asm:18
|
||||||
|
{"play_cry", "MEOWTH", true},
|
||||||
{"show_text", "_CeladonMansion1FMeowthText"},
|
{"show_text", "_CeladonMansion1FMeowthText"},
|
||||||
},
|
},
|
||||||
|
|
||||||
-- CeladonMansion1FNidoranFText (scripts/CeladonMansion1F.asm): text_far
|
|
||||||
-- _CeladonMansion1FNidoranFText, then plays the NIDORAN_F cry and ends.
|
|
||||||
-- The cry playback has no port-side equivalent command, so we just
|
|
||||||
-- show the flavor line.
|
|
||||||
TEXT_CELADONMANSION1F_NIDORANF = {
|
TEXT_CELADONMANSION1F_NIDORANF = {
|
||||||
{"face_player"},
|
{"face_player"},
|
||||||
|
-- pokered/scripts/CeladonMansion1F.asm:33
|
||||||
|
{"play_cry", "NIDORAN_F", true},
|
||||||
{"show_text", "_CeladonMansion1FNidoranFText"},
|
{"show_text", "_CeladonMansion1FNidoranFText"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,11 +4,10 @@
|
|||||||
return {
|
return {
|
||||||
LAVENDER_CUBONE_HOUSE = {
|
LAVENDER_CUBONE_HOUSE = {
|
||||||
talk = {
|
talk = {
|
||||||
-- LavenderCuboneHouseCuboneText: text_far _LavenderCuboneHouseCuboneText,
|
-- LavenderCuboneHouse.asm:10
|
||||||
-- then text_asm plays the CUBONE cry. Cry playback has no Commands
|
|
||||||
-- equivalent in this port, so just show the line.
|
|
||||||
TEXT_LAVENDERCUBONEHOUSE_CUBONE = {
|
TEXT_LAVENDERCUBONEHOUSE_CUBONE = {
|
||||||
{ "face_player" },
|
{ "face_player" },
|
||||||
|
{ "play_cry", "CUBONE", true },
|
||||||
{ "show_text", "_LavenderCuboneHouseCuboneText" },
|
{ "show_text", "_LavenderCuboneHouseCuboneText" },
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -31,16 +31,15 @@ M.MR_FUJIS_HOUSE = {
|
|||||||
{ "show_text", "_MrFujisHouseLittleGirlPokemonAreNiceToHugText" }, -- 6
|
{ "show_text", "_MrFujisHouseLittleGirlPokemonAreNiceToHugText" }, -- 6
|
||||||
},
|
},
|
||||||
|
|
||||||
-- scripts/MrFujisHouse.asm MrFujisHousePsyduckText: text_far then
|
-- scripts/MrFujisHouse.asm:56
|
||||||
-- PlayCry(PSYDUCK). Cry playback isn't modeled by Commands, so just
|
|
||||||
-- show the flavor text.
|
|
||||||
TEXT_MRFUJISHOUSE_PSYDUCK = {
|
TEXT_MRFUJISHOUSE_PSYDUCK = {
|
||||||
|
{ "play_cry", "PSYDUCK", true },
|
||||||
{ "show_text", "_MrFujisHousePsyduckText" },
|
{ "show_text", "_MrFujisHousePsyduckText" },
|
||||||
},
|
},
|
||||||
|
|
||||||
-- scripts/MrFujisHouse.asm MrFujisHouseNidorinoText: text_far then
|
-- scripts/MrFujisHouse.asm:63
|
||||||
-- PlayCry(NIDORINO).
|
|
||||||
TEXT_MRFUJISHOUSE_NIDORINO = {
|
TEXT_MRFUJISHOUSE_NIDORINO = {
|
||||||
|
{ "play_cry", "NIDORINO", true },
|
||||||
{ "show_text", "_MrFujisHouseNidorinoText" },
|
{ "show_text", "_MrFujisHouseNidorinoText" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,10 +4,9 @@
|
|||||||
return {
|
return {
|
||||||
ROUTE_16_FLY_HOUSE = {
|
ROUTE_16_FLY_HOUSE = {
|
||||||
talk = {
|
talk = {
|
||||||
-- Route16FlyHouseFearowText: text_asm just prints the one line and
|
-- scripts/Route16FlyHouse.asm:45-51
|
||||||
-- plays the FEAROW cry (no cry-playback command exists in this
|
|
||||||
-- port's Commands vocabulary, so only the text is ported).
|
|
||||||
TEXT_ROUTE16FLYHOUSE_FEAROW = {
|
TEXT_ROUTE16FLYHOUSE_FEAROW = {
|
||||||
|
{ "play_cry", "FEAROW", true },
|
||||||
{ "show_text", "_Route16FlyHouseFearowText" },
|
{ "show_text", "_Route16FlyHouseFearowText" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1047,6 +1047,7 @@ local championsRoomRivalScript = {
|
|||||||
-- numeric 26 into a jump ONTO the closing HALL_OF_FAME warp instead of past
|
-- numeric 26 into a jump ONTO the closing HALL_OF_FAME warp instead of past
|
||||||
-- it, so a returning champion warped straight into the induction.
|
-- it, so a returning champion warped straight into the induction.
|
||||||
{ "jump_if_true", "end" }, -- 3
|
{ "jump_if_true", "end" }, -- 3
|
||||||
|
{ "set_option", "animations", true }, -- scripts/ChampionsRoom.asm:57
|
||||||
{ "show_text", "_ChampionsRoomRivalIntroText" }, -- 4
|
{ "show_text", "_ChampionsRoomRivalIntroText" }, -- 4
|
||||||
-- ChampionsRoomRivalReadyToBattleScript plays MUSIC_FINAL_BATTLE after
|
-- ChampionsRoomRivalReadyToBattleScript plays MUSIC_FINAL_BATTLE after
|
||||||
-- the intro text, before the battle itself (#706); pushBattle's wipe-time
|
-- the intro text, before the battle itself (#706); pushBattle's wipe-time
|
||||||
|
|||||||
@@ -359,18 +359,22 @@ M.ROUTE_16_FLY_HOUSE = {
|
|||||||
TEXT_ROUTE16FLYHOUSE_BRUNETTE_GIRL = function(game, ow, npc, done)
|
TEXT_ROUTE16FLYHOUSE_BRUNETTE_GIRL = function(game, ow, npc, done)
|
||||||
local t = text(game)
|
local t = text(game)
|
||||||
if game.save.flags.EVENT_GOT_HM02 then
|
if game.save.flags.EVENT_GOT_HM02 then
|
||||||
push(game, t._Route16FlyHouseBrunetteGirlHm02ExplanationText
|
push(game, t._Route16FlyHouseBrunetteGirlHM02ExplanationText
|
||||||
or "HM02 is FLY!\fIt will whisk you\nback to any town!", done)
|
or "HM02 is FLY.\nIt will take you\vback to any town.\fPut it to good\nuse!", done)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
push(game, t._Route16FlyHouseBrunetteGirlText
|
push(game, t._Route16FlyHouseBrunetteGirlText
|
||||||
or "Shh! It's a\nsecret!\fMy POKéMON's\nHM02, take it!", function()
|
or "Oh, you found my\nsecret retreat!", function()
|
||||||
if not require("src.inventory.Bag").add(game.save, "HM_FLY", 1) then
|
if not require("src.inventory.Bag").add(game.save, "HM_FLY", 1) then
|
||||||
push(game, "You don't have\nroom for HM02!", done)
|
push(game, t._Route16FlyHouseBrunetteGirlHM02NoRoomText
|
||||||
|
or "You don't have any\nroom for this.", done)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
game.save.flags.EVENT_GOT_HM02 = true
|
game.save.flags.EVENT_GOT_HM02 = true
|
||||||
push(game, ("%s got\nHM02!"):format(game.save.player.name), done)
|
-- scripts/Route16FlyHouse.asm:32-35
|
||||||
|
push(game, fill(t._Route16FlyHouseBrunetteGirlReceivedHM02Text
|
||||||
|
or "{PLAYER} received\nHM02!", { player = game.save.player.name }),
|
||||||
|
done, require("src.render.TextBox").soundOpts(game, "Get_Key_Item"))
|
||||||
end)
|
end)
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# Linux AppImage packaging
|
||||||
|
|
||||||
|
Releases ship raw AppImages (no zip wrapper):
|
||||||
|
|
||||||
|
- `gen1recomp-<version>-linux-x86_64.AppImage`
|
||||||
|
- `gen1recomp-<version>-linux-arm64.AppImage`
|
||||||
|
|
||||||
|
```sh
|
||||||
|
chmod +x gen1recomp-*-linux-x86_64.AppImage
|
||||||
|
./gen1recomp-*-linux-x86_64.AppImage
|
||||||
|
```
|
||||||
|
|
||||||
|
Flatpak users should prefer the `.flatpak` bundle (see
|
||||||
|
[linux-flatpak.md](linux-flatpak.md)); it avoids host glibc / FUSE / curl
|
||||||
|
mismatches on immutable desktops.
|
||||||
|
|
||||||
|
## Common failure modes
|
||||||
|
|
||||||
|
| Symptom | Cause | Fix |
|
||||||
|
|---|---|---|
|
||||||
|
| Won't start / `libfuse.so.2` | No FUSE | `sudo apt install libfuse2` (or `libfuse2t64`) **or** `./app.AppImage --appimage-extract-and-run` |
|
||||||
|
| Update check / mods / save sync fail | Host `curl` missing or broken by AppImage `LD_LIBRARY_PATH` | Install curl; current builds scrub `LD_LIBRARY_PATH` for **host** curl and keep it for a bundled AppDir curl |
|
||||||
|
| Settings / sync reset after quit | Portable mode next to a read-only AppImage parent (`/opt`, system dir) | Remove `portable.txt` or move the AppImage to a writable folder; the game falls back to the XDG save dir when the probe write fails |
|
||||||
|
| "Download AppImage update" | Shell/`minShell` gate needs a full native package | Download the new `.AppImage`, `chmod +x`, replace the old file |
|
||||||
|
| Steam / Game Mode weirdness after update | Overlay `LD_PRELOAD` / PID change | HostShell unsets `LD_PRELOAD` for children and `execv`s `$APPIMAGE` on restart |
|
||||||
|
|
||||||
|
## Network transport (HostShell)
|
||||||
|
|
||||||
|
Desktop Linux fetches go through host or bundled `curl`:
|
||||||
|
|
||||||
|
1. Flatpak `/app/bin/curl` (bundled, keep sandbox libs)
|
||||||
|
2. `$APPDIR/usr/bin/curl` or `$APPDIR/bin/curl` (keep `$APPDIR` on `LD_LIBRARY_PATH`)
|
||||||
|
3. Host `curl` (`env -u LD_LIBRARY_PATH`, and always `-u LD_PRELOAD`)
|
||||||
|
|
||||||
|
## Portable mode
|
||||||
|
|
||||||
|
Drop `portable.txt` beside the `.AppImage` to keep saves next to the binary.
|
||||||
|
The launcher probes writability with a unique `.write_probe_<time>_<rand>.tmp`
|
||||||
|
file. Read-only parents fail soft and use `love.filesystem` XDG saves instead.
|
||||||
|
Flatpak ignores `portable.txt`.
|
||||||
|
|
||||||
|
## Auto-update
|
||||||
|
|
||||||
|
In-place updates download `gen1recomp-X.Y.Z.love` into the save directory.
|
||||||
|
Full shell bumps open the matching AppImage (or Flatpak) download URL — there
|
||||||
|
is no silent in-place AppImage replace yet.
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# x86_64 (macOS or Linux host with squashfs-tools)
|
||||||
|
scripts/build.sh linux --version X.Y.Z
|
||||||
|
|
||||||
|
# arm64 (aarch64 host + docker/podman)
|
||||||
|
scripts/build_linux_arm64.sh --version X.Y.Z
|
||||||
|
```
|
||||||
|
|
||||||
|
See [linux-arm64-build.md](linux-arm64-build.md) for the arm64 builder.
|
||||||
@@ -34,6 +34,25 @@ The package bundles PortMaster's LÖVE 11.5 aarch64 runtime. The launcher source
|
|||||||
|
|
||||||
Suspend/resume uses the existing LÖVE focus/visibility lifecycle: input is reset on focus loss and the game resumes when the window becomes visible again. Exact power-button behavior remains firmware-dependent; hardware validation has been performed on the TrimUI Brick, not every SBC or H700 device.
|
Suspend/resume uses the existing LÖVE focus/visibility lifecycle: input is reset on focus loss and the game resumes when the window becomes visible again. Exact power-button behavior remains firmware-dependent; hardware validation has been performed on the TrimUI Brick, not every SBC or H700 device.
|
||||||
|
|
||||||
|
## Power and Performance Tuning
|
||||||
|
|
||||||
|
The handheld build applies several optimizations to reduce power draw and ensure smooth 60 FPS frame pacing on low-power ARM SoCs:
|
||||||
|
|
||||||
|
- **KMSDRM / EGL Vsync Fix**: Handheld builds disable driver vsync (`vsync = 0`) to prevent GPU driver busy-wait spinloops in `eglSwapBuffers`, allowing `nanosleep()` and dropping idle CPU usage from ~25% to ~4%.
|
||||||
|
- **Idle Render Governor**: Drops presentation rate when a static screen (menus, text dialogs, stationary scenes) receives no input, cutting compositing work ~6x while preserving full 60 Hz game and audio clocks. Any button press restores full framerate immediately.
|
||||||
|
- **Sample Rate Scaling**: Audio synthesis is tuned to 22.05 kHz by default (`POKEPORT_AUDIO_RATE=22050`), halving synthesis CPU overhead on Cortex-A53 cores with no audible quality loss on handheld speakers.
|
||||||
|
- **Dynamic CPU Governor**: Defaults to `schedutil` instead of pinning `performance` on all cores, reducing thermals and extending battery life.
|
||||||
|
|
||||||
|
Environment variables for fine-tuning (configured in `gen1recomp-sbc.sh`):
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `POKEPORT_IDLE_AFTER` | `10` | Seconds without input before an idle screen drops presentation rate |
|
||||||
|
| `POKEPORT_IDLE_FPS` | `6` | Framerate while idle |
|
||||||
|
| `POKEPORT_AUDIO_RATE` | `22050` | Chip-synth sample rate (set to `44100` for full rate) |
|
||||||
|
| `POKEPORT_CPU_GOVERNOR` | `schedutil` | CPU scaling governor (`schedutil`, `performance`, `ondemand`) |
|
||||||
|
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
Release workflows build this automatically. Standalone builds resolve the latest published Gen1Recomp release by default:
|
Release workflows build this automatically. Standalone builds resolve the latest published Gen1Recomp release by default:
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
# Linux arm64 (aarch64) AppImage
|
# Linux arm64 (aarch64) AppImage
|
||||||
|
|
||||||
Releases ship `gen1recomp-<version>-linux-arm64.AppImage` alongside the
|
Releases ship `gen1recomp-<version>-linux-arm64.AppImage` alongside the
|
||||||
existing x86_64 `gen1recomp-<version>-linux.zip`. It targets 64-bit ARM
|
existing x86_64 `gen1recomp-<version>-linux-x86_64.AppImage`. It targets 64-bit ARM
|
||||||
desktop Linux: Raspberry Pi 4/5 running Raspberry Pi OS, Armbian and other
|
desktop Linux: Raspberry Pi 4/5 running Raspberry Pi OS, Armbian and other
|
||||||
SBC distros, arm64 VMs on Apple Silicon, Ampere/Graviton desktops, and the
|
SBC distros, arm64 VMs on Apple Silicon, Ampere/Graviton desktops, and the
|
||||||
aarch64 handhelds that run a full distro.
|
aarch64 handhelds that run a full distro. See also
|
||||||
|
[linux-appimage.md](linux-appimage.md) for shared AppImage failure modes.
|
||||||
|
|
||||||
> The Anbernic RG34XXSP has its own PortMaster-style pack
|
> The Anbernic RG34XXSP has its own PortMaster-style pack
|
||||||
> (`gen1recomp-*-rg34xxsp-stockos64-mod.zip`, see
|
> (`gen1recomp-*-rg34xxsp-stockos64-mod.zip`, see
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# Linux Flatpak
|
||||||
|
|
||||||
|
Releases may ship `gen1recomp-<version>-linux.flatpak` (x86_64 bundle).
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```sh
|
||||||
|
flatpak install --user ./gen1recomp-<version>-linux.flatpak
|
||||||
|
flatpak run com.theboisclub.gen1recomp
|
||||||
|
```
|
||||||
|
|
||||||
|
Or open the `.flatpak` in Discover / GNOME Software. AppStream metainfo
|
||||||
|
includes a `<releases>` entry so those UIs can show the version.
|
||||||
|
|
||||||
|
## Permissions
|
||||||
|
|
||||||
|
The manifest requests:
|
||||||
|
|
||||||
|
- network (updater, mods, save sync)
|
||||||
|
- X11 / Wayland / DRI / PulseAudio
|
||||||
|
- `--device=all` (SDL2 gamepads, rumble, gyro via evdev/hidraw)
|
||||||
|
- `--filesystem=home` (ROM import)
|
||||||
|
|
||||||
|
Curl is bundled at `/app/bin/curl` so the game does not need host curl.
|
||||||
|
|
||||||
|
## Updates
|
||||||
|
|
||||||
|
Lua/engine payloads still use the in-app `.love` updater. A native shell bump
|
||||||
|
points at the new `.flatpak` asset (`Download Flatpak update`).
|
||||||
|
|
||||||
|
After a `.love` payload download, restart uses `love.event.quit("restart")`
|
||||||
|
(`execv` of `/proc/self/exe` inside bwrap). If a restart hangs after an
|
||||||
|
update, fully quit and relaunch — PhysFS can retain a lock on the previous
|
||||||
|
payload across a bad handoff.
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```sh
|
||||||
|
scripts/build_flatpak.sh --version X.Y.Z
|
||||||
|
# → dist/flatpak/gen1recomp-X.Y.Z-linux.flatpak
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires `flatpak` + `flatpak-builder` and the Freedesktop 24.08 runtime from
|
||||||
|
Flathub (installed automatically on first build).
|
||||||
|
|
||||||
|
Release CI builds the bundle on `ubuntu-24.04` (`linux-flatpak` job) from the
|
||||||
|
shared `game.love` payload and publishes `gen1recomp-<ver>-linux.flatpak`
|
||||||
|
alongside the AppImages. PR CI path-gates the same script when `flatpak/` or
|
||||||
|
`scripts/build_flatpak.sh` change.
|
||||||
|
|
||||||
|
Flathub store submission is out of scope for the GitHub bundle channel.
|
||||||
+113
@@ -200,6 +200,60 @@ the adapter's own coverage table:
|
|||||||
python3 tools/modkit.py gen2check mods/my_mod
|
python3 tools/modkit.py gen2check mods/my_mod
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Imported version datasets
|
||||||
|
|
||||||
|
A mod can inspect semantic content from another game the player has already
|
||||||
|
imported without switching the active game or reaching into engine cache
|
||||||
|
internals:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local gold, reason = mod.datasets:open("gold")
|
||||||
|
if not gold then
|
||||||
|
-- reason is "unknown_version", "not_imported", or "invalid_cache"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local chikorita = gold.content.pokemon:get("CHIKORITA")
|
||||||
|
local normalVsGhost = gold.content.type_chart:get("NORMAL>GHOST")
|
||||||
|
local spritePath = gold.assets:path(chikorita.spriteFront)
|
||||||
|
|
||||||
|
for id, record in gold.content.pokemon:each() do
|
||||||
|
-- ids are returned in deterministic lexical order
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
`view.version` and `view.generation` identify the selected dataset.
|
||||||
|
`view.content` exposes the same registry names, aliases, generation routing,
|
||||||
|
and data-only record shapes as `mod.content`, but only `get`, `has`, and
|
||||||
|
`each`. Returned records are detached copies and cannot mutate either dataset.
|
||||||
|
Every generated base record passes the selected generation's existing public
|
||||||
|
schema before it is returned; extractor metadata beside record maps stays out
|
||||||
|
of the registry id space and is reserved against `register`, `override`,
|
||||||
|
`patch`, and `remove` writes through the active registry. A malformed record
|
||||||
|
makes `get` return nil, `has`
|
||||||
|
return false, and `each` return no rows, and invalidates that dataset view.
|
||||||
|
Records containing functions, userdata, threads, metatables, or cycles are not
|
||||||
|
exposed. Each open call receives an independent facade, so one mod cannot
|
||||||
|
replace another mod view method. Canonical boot shaping is included, such as
|
||||||
|
Gen 1 defaults and Yellow corrections, and Gold's Foresight matchup rows and
|
||||||
|
derived `held_items`.
|
||||||
|
|
||||||
|
`open` checks the completion marker, exact version-specific file inventory,
|
||||||
|
source/cache boundary, and file-size bounds without reading or decoding the
|
||||||
|
semantic modules. A root is read, bounded-decoded, normalized, and cached only
|
||||||
|
when a content operation first needs it. Each later view operation rechecks
|
||||||
|
readiness and the source bytes behind already cached roots; unchanged roots are
|
||||||
|
not decoded again. Missing, partial, and stale imports return
|
||||||
|
`nil, "not_imported"`. Malformed syntax, a resource-limit violation, or a
|
||||||
|
record that fails the public schema is discovered on first root access and
|
||||||
|
fails that operation closed; a later `open` against the same source returns
|
||||||
|
`nil, "invalid_cache"`. Generated modules use a bounded literal-only grammar
|
||||||
|
and are never executed. No raw ROM bytes or generated source are exposed.
|
||||||
|
`view.assets:path(relative)` and
|
||||||
|
`view.assets:info(relative)` accept only `assets/generated/...` paths and
|
||||||
|
keep them under the selected version cache prefix. The API never changes
|
||||||
|
`mod.game`, the active `Data` table, `GameVersion`, or cache mount state.
|
||||||
|
|
||||||
## Editing maps in Tiled
|
## Editing maps in Tiled
|
||||||
|
|
||||||
Maps are data, not assets, so they can be authored in a real map editor and
|
Maps are data, not assets, so they can be authored in a real map editor and
|
||||||
@@ -451,6 +505,65 @@ Menu choices and moves use the same engine methods as the native controls;
|
|||||||
their mutable logic. Tutorial, link, forced, stale, and covered battle states
|
their mutable logic. Tutorial, link, forced, stale, and covered battle states
|
||||||
refuse core intents. Use `mod.input` for ordinary text advance.
|
refuse core intents. Use `mod.input` for ordinary text advance.
|
||||||
|
|
||||||
|
## Battle rule hooks
|
||||||
|
|
||||||
|
Two decisions the OPTION screen and the cart make for the player, which a game
|
||||||
|
mode can make instead (RFC 0015). Neither writes the player's saved
|
||||||
|
preference, so a mode can hold a rule for as long as it is active and hand the
|
||||||
|
player's own setting back untouched.
|
||||||
|
|
||||||
|
`battle.style` wraps the SHIFT/SET read at the moment the foe's Pokémon faints
|
||||||
|
and the engine would offer a free switch:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
mod.hooks:wrap("battle.style", function(next, battle)
|
||||||
|
if myMode.active then return "set" end -- no "will you change POKéMON?"
|
||||||
|
return next(battle) -- the OPTION row, as today
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
Return `"set"` or `"shift"`; anything else reads as the vanilla answer.
|
||||||
|
|
||||||
|
`catch.nickname` wraps the `AskName` prompt after a capture (party or box),
|
||||||
|
the same question `pokemon.before_give`'s `gift.nickname` already answers for
|
||||||
|
script gifts:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
mod.hooks:wrap("catch.nickname", function(next, mon, ctx)
|
||||||
|
-- ctx = { battle = <BattleState>, name = <display name>, game = <Game> }
|
||||||
|
if myMode.active then return false end -- keep the species name
|
||||||
|
if myNames then return myNames[mon.species] end -- a string names it, no prompt
|
||||||
|
return next(mon, ctx) -- true: ask, as today
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
`false` keeps the species name with no prompt. A string is the nickname with
|
||||||
|
no prompt, clipped to the naming grid's ten characters (an empty string names
|
||||||
|
nothing). Anything else queues the prompt.
|
||||||
|
|
||||||
|
## Party-full custody at a catch
|
||||||
|
|
||||||
|
When a capture lands on a full party, the cart deposits the mon in storage
|
||||||
|
without a question. `catch.party_full` (RFC 0018) lets a mode stand in front
|
||||||
|
of `SendNewMonToBox` and take custody instead:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
mod.hooks:wrap("catch.party_full", function(next, ctx)
|
||||||
|
-- ctx = { battle = <BattleState>, mon = <Pokemon>, name = <display name>,
|
||||||
|
-- game = <Game> }
|
||||||
|
if myMode.active then
|
||||||
|
takeCustody(ctx.mon) -- the mon is the mod's problem now
|
||||||
|
return true -- nothing is deposited
|
||||||
|
end
|
||||||
|
return next(ctx) -- false: deposit, as today
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
A truthy return skips the box entirely -- the mon is neither in the party nor
|
||||||
|
in any box, and `pokemon.caught` reports `destination = "mod"` so the mode can
|
||||||
|
find its own custody again. Anything falsy deposits as always, "But every BOX
|
||||||
|
is full!" included.
|
||||||
|
|
||||||
## Rendering pipelines
|
## Rendering pipelines
|
||||||
|
|
||||||
Most registries hand the engine *content*. `render_pipelines` hands it
|
Most registries hand the engine *content*. `render_pipelines` hands it
|
||||||
|
|||||||
@@ -342,6 +342,7 @@ accepted and merged as-is.
|
|||||||
| `source` | string |
|
| `source` | string |
|
||||||
| `swarmGrass` | map of string -> {map?, rates, slots} |
|
| `swarmGrass` | map of string -> {map?, rates, slots} |
|
||||||
| `swarmWater` | map of string -> {map?, rate, slots} |
|
| `swarmWater` | map of string -> {map?, rate, slots} |
|
||||||
|
| `timeFishGroups` | map of string | integer 0..255 -> {day, nite} |
|
||||||
| `treeSets` | map of string -> {common, rare} |
|
| `treeSets` | map of string -> {common, rare} |
|
||||||
| `trees` | map of string -> string |
|
| `trees` | map of string -> string |
|
||||||
| `water` | map of string -> {map?, rate, slots} |
|
| `water` | map of string -> {map?, rate, slots} |
|
||||||
@@ -509,6 +510,26 @@ mod.content.item_effects:register("MOON_FLUTE", { use = fn, field = true })
|
|||||||
mod.content.items:patch("POTION", { price = 100 })
|
mod.content.items:patch("POTION", { price = 100 })
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### On Gold (Gen 2)
|
||||||
|
|
||||||
|
- semantics: `record`
|
||||||
|
- target: `Data.items`
|
||||||
|
|
||||||
|
The record differs; the registry name, the verbs and the id space
|
||||||
|
do not.
|
||||||
|
|
||||||
|
| field | type | required |
|
||||||
|
|---|---|---|
|
||||||
|
| `ball` | balls id | no |
|
||||||
|
| `effect` | item_effects id | no |
|
||||||
|
| `id` | string | yes |
|
||||||
|
| `index` | integer 0..255 | no |
|
||||||
|
| `machine` | {kind, move, number} | no |
|
||||||
|
| `name` | string | yes |
|
||||||
|
| `needsTarget` | boolean | no |
|
||||||
|
| `price` | integer >= 0 | yes |
|
||||||
|
| `tossable` | boolean | no |
|
||||||
|
|
||||||
## landmarks
|
## landmarks
|
||||||
|
|
||||||
- semantics: `record`
|
- semantics: `record`
|
||||||
@@ -598,7 +619,7 @@ mod.content.map_songs:override("PALLET_TOWN", "Music_Routes1")
|
|||||||
| `id` | string | yes |
|
| `id` | string | yes |
|
||||||
| `index` | integer >= 0 | no |
|
| `index` | integer >= 0 | no |
|
||||||
| `label` | string | no |
|
| `label` | string | no |
|
||||||
| `objects` | list of any value | no |
|
| `objects` | list of {pokemon?, ...} | no |
|
||||||
| `palette` | string | no |
|
| `palette` | string | no |
|
||||||
| `signs` | list of any value | no |
|
| `signs` | list of any value | no |
|
||||||
| `tileset` | tilesets id | yes |
|
| `tileset` | tilesets id | yes |
|
||||||
@@ -673,6 +694,34 @@ mod.content.move_effects:register("DRAIN_PP_EFFECT", { kind = "primary", run = f
|
|||||||
mod.content.moves:patch("BLIZZARD", { accuracy = 70 })
|
mod.content.moves:patch("BLIZZARD", { accuracy = 70 })
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### On Gold (Gen 2)
|
||||||
|
|
||||||
|
- semantics: `record`
|
||||||
|
- target: `Data.moves`
|
||||||
|
|
||||||
|
The record differs; the registry name, the verbs and the id space
|
||||||
|
do not.
|
||||||
|
|
||||||
|
| field | type | required |
|
||||||
|
|---|---|---|
|
||||||
|
| `accuracy` | integer 0..100 | yes |
|
||||||
|
| `anim` | any value | no |
|
||||||
|
| `category` | one of "physical" | "special" | "status" | no |
|
||||||
|
| `chargeText` | string | no |
|
||||||
|
| `counterable` | boolean | no |
|
||||||
|
| `effect` | move_effects id | yes |
|
||||||
|
| `fixedDamage` | integer >= 1 | function | no |
|
||||||
|
| `highCrit` | boolean | no |
|
||||||
|
| `id` | string | yes |
|
||||||
|
| `index` | integer 0..255 | no |
|
||||||
|
| `multiHit` | integer >= 1 | list of integer >= 1 | no |
|
||||||
|
| `name` | string | yes |
|
||||||
|
| `power` | integer 0..255 | yes |
|
||||||
|
| `pp` | integer 0..64 | yes |
|
||||||
|
| `priority` | integer -7..7 | no |
|
||||||
|
| `semiInvulnerable` | boolean | no |
|
||||||
|
| `type` | type_chart id | yes |
|
||||||
|
|
||||||
## music
|
## music
|
||||||
|
|
||||||
- semantics: `record`
|
- semantics: `record`
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
# RFC 0015: Battle rule hooks — `battle.style` and `catch.nickname`
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Proposed.
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
|
||||||
|
Two decisions in a Red/Blue/Yellow battle are made for the player by the
|
||||||
|
OPTION screen and by the cart, and a game mode has no way to make them
|
||||||
|
instead:
|
||||||
|
|
||||||
|
**Whether a faint offers a free switch.** `EnemySendOutFirstMon` reads the
|
||||||
|
battle-style bit: SHIFT asks "will you change POKéMON?" when the foe's Pokémon
|
||||||
|
faints, SET does not. The engine reads `save.options.battleStyle` inline at
|
||||||
|
that moment. A mode that wants SET — a tournament, a Nuzlocke, a battle royale
|
||||||
|
where party-as-health is the whole design — can only get it by writing the
|
||||||
|
player's saved preference, which leaks into their real playthrough the first
|
||||||
|
time anything calls `Game:writeOptions` (the speed hotkey does, on every
|
||||||
|
press).
|
||||||
|
|
||||||
|
**Whether a catch asks for a nickname.** `AddPartyMon` and `SendNewMonToBox`
|
||||||
|
both run `AskName` after a capture. A mode with a disposable team, a
|
||||||
|
randomizer that names what it hands out, a speedrun practice mod — all want to
|
||||||
|
answer that prompt themselves, and the only way today is to drive the yes/no
|
||||||
|
box from outside. The script-gift path already has this seam: a mod that sets
|
||||||
|
`gift.nickname` on `pokemon.before_give` skips `AskName`. The catch path does
|
||||||
|
not.
|
||||||
|
|
||||||
|
The immediate consumer is a battle-royale mode; neither hook is specific to it.
|
||||||
|
|
||||||
|
## The decision it extends
|
||||||
|
|
||||||
|
This extends the **additive, guarded seam convention** Route B in
|
||||||
|
`CONTRIBUTING-mods.md` documents, and is gated by the parity guarantee
|
||||||
|
`tests/engine/gate_meta_coverage.lua` enforces. `catch.nickname` mirrors a
|
||||||
|
contract that already exists for gifts (`pokemon.before_give`'s
|
||||||
|
`gift.nickname`), so the two ways a Pokémon joins the party answer the same
|
||||||
|
question the same way.
|
||||||
|
|
||||||
|
There is no in-repo D-number registry to amend.
|
||||||
|
|
||||||
|
## Exact API delta
|
||||||
|
|
||||||
|
### New hook: `battle.style`
|
||||||
|
|
||||||
|
```lua
|
||||||
|
mod.hooks:wrap("battle.style", function(next, battle)
|
||||||
|
if myMode.active then return "set" end
|
||||||
|
return next(battle) -- the OPTION row, as today
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
Call site: `BattleState:battleStyle()`, called from the enemy send-out path at
|
||||||
|
the moment the SHIFT prompt would be offered. The vanilla link reads
|
||||||
|
`save.options.battleStyle` (lower-cased, default `"shift"`) exactly as the
|
||||||
|
inline read did. A return of `"set"` or `"shift"` is used; anything else reads
|
||||||
|
as the vanilla answer, so a hook that returns nothing by mistake cannot change
|
||||||
|
the rule. The player's saved preference is never written.
|
||||||
|
|
||||||
|
### New hook: `catch.nickname`
|
||||||
|
|
||||||
|
```lua
|
||||||
|
mod.hooks:wrap("catch.nickname", function(next, mon, ctx)
|
||||||
|
-- ctx = { battle = <BattleState>, name = <display name>, game = <Game> }
|
||||||
|
if myMode.active then return false end -- keep the species name
|
||||||
|
if randomizer then return pickName(mon) end -- a string names it
|
||||||
|
return next(mon, ctx) -- true: ask, as today
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
Call site: `BattleState:offerNickname(mon, displayName)`, called from the
|
||||||
|
capture path where `AskName` ran, before the prompt is queued. The vanilla
|
||||||
|
link returns `true`. `false` skips the prompt and keeps the species name; a
|
||||||
|
string skips the prompt and is the nickname, clipped to the naming grid's ten
|
||||||
|
characters (an empty string names nothing, like the grid's own empty entry);
|
||||||
|
anything else queues the prompt as today. The method returns whether a prompt
|
||||||
|
was queued.
|
||||||
|
|
||||||
|
### No other surface changes
|
||||||
|
|
||||||
|
Both call sites are guarded by `Runtime.wantsHook`, and both vanilla links are
|
||||||
|
file-local functions, so a build with nothing wrapped runs the branch exactly
|
||||||
|
as before and allocates nothing it did not allocate before. `askNicknameUI` is
|
||||||
|
unchanged and still public.
|
||||||
|
|
||||||
|
## Migration
|
||||||
|
|
||||||
|
Nothing changes for existing mods. A mod that was writing
|
||||||
|
`save.options.battleStyle` to force a style should wrap `battle.style` instead
|
||||||
|
and stop writing the option.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- `tests/modkit/cases/battle_rule_hooks.lua` — through the public mod API: the
|
||||||
|
no-mod answers match the OPTION row and the cart's AskName; a wrapped mod
|
||||||
|
forces either style without the row being written; `false`, a string, a
|
||||||
|
too-long string, an empty string, and a fall-through each do what this RFC
|
||||||
|
says at the catch prompt.
|
||||||
|
- `tests/engine/gate_hooks.lua` — both names are in the live catalog and pass
|
||||||
|
the no-mod parity gate (vanilla called exactly once, result unchanged,
|
||||||
|
nothing allocated).
|
||||||
|
- `tests/engine/gate_meta_coverage.lua` — both names are covered by the unit
|
||||||
|
corpus.
|
||||||
|
|
||||||
|
## Backward compatibility
|
||||||
|
|
||||||
|
Additive. No existing hook, event, registry or manifest field changes shape.
|
||||||
|
The two new methods on `BattleState` are called only from the paths that
|
||||||
|
previously inlined their logic; the results with no subscriber are identical.
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
# RFC 0015: Read-only imported dataset views
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Proposed.
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
|
||||||
|
A cross-version content mod can read only the active merged dataset through
|
||||||
|
`mod.content`. Even when the player has already imported another supported
|
||||||
|
game, a mod cannot inspect that version's semantic species, moves, items, type
|
||||||
|
chart, or generated asset namespace. The available alternatives are private:
|
||||||
|
mutating `CacheFs.prefix`, mounting another cache over the active one, loading
|
||||||
|
generated Lua directly, or asking for a second raw-ROM import. They compose
|
||||||
|
poorly with the active game, expose unstable import layout, and make safe
|
||||||
|
read-only use impossible from the sandbox.
|
||||||
|
|
||||||
|
The concrete consumer is **Adaptive Trainers**, whose approved Phase G Kanto+
|
||||||
|
sidecar must derive nine Kanto-line continuations, Steel/type and move
|
||||||
|
definitions, and generated sprites from the player's existing verified Gold
|
||||||
|
cache while Red, Blue, or Yellow remains active. `mod.content` exposes only the
|
||||||
|
active R/B/Y dataset; `mod.imports` can read only separately declared raw mod
|
||||||
|
imports; and `mod.cache` is mod-private generated output. None can inspect the
|
||||||
|
launcher-owned Gold semantic dataset without a second ROM import and mod-side
|
||||||
|
ROM interpretation. The engine API remains generic and contains no Adaptive
|
||||||
|
Trainers policy.
|
||||||
|
|
||||||
|
## Decision and plan extended
|
||||||
|
|
||||||
|
This implements **D-AT-004: optional Kanto+ content consumes an
|
||||||
|
active-independent semantic dataset view**. The consuming design is tracked in
|
||||||
|
the Adaptive Trainers implementation plan,
|
||||||
|
[`docs/superpowers/plans/2026-08-14-adaptive-trainers.md`](https://github.com/MaxTomahawk/gen1recomp-adaptive-trainers/blob/main/docs/superpowers/plans/2026-08-14-adaptive-trainers.md),
|
||||||
|
Task 8. The delta is a generic, additive public API and contains no trainer
|
||||||
|
pools, scaling, boss identities, version-mixing rules, or Adaptive Trainers
|
||||||
|
policy.
|
||||||
|
|
||||||
|
## Exact API delta
|
||||||
|
|
||||||
|
Every sandboxed mod receives:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local view, reason = mod.datasets:open("gold")
|
||||||
|
```
|
||||||
|
|
||||||
|
A known version with the current completed import marker returns a read-only view:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
view = {
|
||||||
|
version = "gold",
|
||||||
|
generation = 2,
|
||||||
|
content = {
|
||||||
|
pokemon = {
|
||||||
|
get = function(self, id) end,
|
||||||
|
has = function(self, id) end,
|
||||||
|
each = function(self) end,
|
||||||
|
},
|
||||||
|
-- every public registry name and alias
|
||||||
|
},
|
||||||
|
assets = {
|
||||||
|
path = function(self, generatedPath) end,
|
||||||
|
info = function(self, generatedPath) end,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`get` returns a bounded detached data-only copy or nil. `has` reports data-only
|
||||||
|
semantic presence.
|
||||||
|
`each` returns ids in lexical order and detached values. The registries use
|
||||||
|
the selected version's generation routing and the engine's existing
|
||||||
|
`Schemas`, `Registry`, and `Builtins` normalization, so structured sources
|
||||||
|
such as type matchups retain the same public ids used by the active
|
||||||
|
`mod.content` facade and extractor metadata beside record maps is not exposed
|
||||||
|
as a record id or writable through the active registry's mutation verbs. Every
|
||||||
|
generated base record is checked with that selected generation's existing
|
||||||
|
public schema before it can cross `get`, `has`, or
|
||||||
|
`each`; there is no dataset-specific duplicate schema. No register, patch,
|
||||||
|
override, or remove verb is exposed. Each call returns an independent facade
|
||||||
|
over the cached internal dataset, so facade mutation cannot cross mod
|
||||||
|
boundaries.
|
||||||
|
|
||||||
|
`assets:path` returns the selected cache-prefixed virtual path.
|
||||||
|
`assets:info` returns sanitized `type` and optional `size` metadata. Both
|
||||||
|
accept only relative paths below `assets/generated/`, reject control
|
||||||
|
characters, absolute paths, backslashes, and traversal, and expose no byte
|
||||||
|
reader.
|
||||||
|
|
||||||
|
An unknown version returns `nil, "unknown_version"`. `open` checks the current
|
||||||
|
completion marker, exact version-specific file inventory, source/cache
|
||||||
|
boundary, and available file sizes; it does not read or decode semantic
|
||||||
|
modules. A missing, partial, or stale cache returns `nil, "not_imported"`.
|
||||||
|
|
||||||
|
The first content operation that needs a root reads it once, applies the
|
||||||
|
limits (8 MiB per module, 48 MiB aggregate, depth 64, 500,000 values, 2 MiB
|
||||||
|
per string, and 250,000 entries per table), decodes the restricted literal
|
||||||
|
grammar, applies canonical selected-version normalization, and caches the
|
||||||
|
detached root. Each later content or asset operation rechecks marker/file and
|
||||||
|
source-bound readiness. It also rereads the source bytes for already cached
|
||||||
|
roots; unchanged roots are not decoded again, while a changed root clears the
|
||||||
|
derived registry cache and is decoded on its next use.
|
||||||
|
|
||||||
|
Malformed syntax, non-table roots, binary/trailing content, resource-limit
|
||||||
|
violations, and records that fail the public schema are therefore discovered
|
||||||
|
on first access rather than during `open`. The triggering `get` returns nil,
|
||||||
|
`has` returns false, or `each` returns no rows; the whole internal view is
|
||||||
|
invalidated, and a subsequent `open` against the unchanged source returns
|
||||||
|
`nil, "invalid_cache"`. Actionable detail is engine-logged but not exposed to
|
||||||
|
the mod. Generated Lua is never executed. Functions, userdata, threads,
|
||||||
|
metatables, and cycles cannot cross the facade. The API never exposes raw ROM
|
||||||
|
bytes, generated source, host paths, or a mount.
|
||||||
|
|
||||||
|
## Migration and compatibility
|
||||||
|
|
||||||
|
Existing mods change nothing. `mod.datasets` is additive and requires no
|
||||||
|
permission. The service is allocated lazily on the first explicit
|
||||||
|
`mod.datasets:open` call. A boot with no mods, or with mods that do not call
|
||||||
|
it, performs no cross-version cache reads.
|
||||||
|
|
||||||
|
Opening a view does not change `GameVersion`, `CacheFs.prefix`, the active
|
||||||
|
`Data` table, PhysFS mounts, save state, or the selected game's behavior.
|
||||||
|
Red, Blue, Yellow, Gold, Silver, and Crystal keep their existing active data paths.
|
||||||
|
|
||||||
|
The completion marker and per-version required-file rules live in the pure,
|
||||||
|
injected `CacheContract` shared by the importer and dataset service. It also
|
||||||
|
defines source-tree behavior. Neither consumer mutates `CacheFs.prefix` while
|
||||||
|
checking readiness. Every `open` revalidates the contract and required module
|
||||||
|
inventory without semantic decoding. Every view operation rechecks that
|
||||||
|
readiness before serving cached state; a stale/remove/reimport transition
|
||||||
|
evicts the previous semantic view.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- `tests/modkit/cases/dataset_views.lua` loads sandboxed fixture mods through
|
||||||
|
the public API and covers Red, Blue, Yellow, Gold, Silver, and Crystal independently.
|
||||||
|
- The test proves semantic registry normalization, deterministic iteration,
|
||||||
|
detached records, read-only facades, cross-mod facade isolation,
|
||||||
|
version-prefixed generated assets,
|
||||||
|
traversal rejection, stable failure reasons, and stale-marker rejection.
|
||||||
|
- It also proves missing/empty/partial/stale/remove/reimport behavior, hostile
|
||||||
|
generated-source and malformed-record rejection, canonical Gen
|
||||||
|
1/Yellow/Gold hydration, active Red/Blue/Yellow isolation while reading Gold,
|
||||||
|
and the approved Kanto+ Gold records and assets.
|
||||||
|
- `tests/engine/dataset_views_lazy_validation.lua` counts semantic reads and
|
||||||
|
decoder calls to prove `open` decodes nothing, unused roots stay unread, and
|
||||||
|
repeated access does not re-decode an unchanged cached root.
|
||||||
|
- `tests/modkit/cases/dataset_views_nontermination.lua` proves generated code
|
||||||
|
is rejected rather than executed; `tests/engine/generated_data_decoder_test.lua`
|
||||||
|
proves every decoder resource bound.
|
||||||
|
- `tests/engine/dataset_views_no_mod_parity.lua` is the separate guarded no-mod
|
||||||
|
parity suite and proves the service stays unallocated with zero cache reads.
|
||||||
|
|
||||||
|
## Deprecation etiquette
|
||||||
|
|
||||||
|
Nothing is removed, renamed, superseded, or deprecated.
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
# RFC 0018: `catch.party_full` — custody of a catch the party cannot hold
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Proposed.
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
|
||||||
|
When a capture lands and the party already holds six Pokémon, the cart does
|
||||||
|
not ask the player anything: `AddPartyMon` fails and the mon goes to
|
||||||
|
`SendNewMonToBox` without a stop. The engine mirrors that —
|
||||||
|
`BattleState:storeCaughtMon` falls through to `Boxes.deposit` — and a game
|
||||||
|
mode has no way to stand in front of it.
|
||||||
|
|
||||||
|
That silence is wrong for any mode where the box is not a thing. A battle
|
||||||
|
royale locks the Pokémon Center PC for the whole match (the box is a second
|
||||||
|
health bar: deposit the healthy ones, fight with one, withdraw fresh ones),
|
||||||
|
so a seventh catch is deposited into storage the player cannot reach — the
|
||||||
|
mon is gone and everyone saw the fanfare. A Nuzlocke that counts a boxed mon
|
||||||
|
as lost, a randomizer that wants to hand out a replacement instead, a
|
||||||
|
challenge run that makes the player release someone for the catch — all want
|
||||||
|
the decision, and today there is no seam to catch it at.
|
||||||
|
|
||||||
|
The immediate consumer is a battle-royale mode, where the fix is the game's
|
||||||
|
own rule: at 6/6 you choose who makes room, and whoever leaves hits the
|
||||||
|
ground as a ball. Neither the decision nor the spill is specific to it.
|
||||||
|
|
||||||
|
## The decision it extends
|
||||||
|
|
||||||
|
This extends the **additive, guarded seam convention** Route B in
|
||||||
|
`CONTRIBUTING-mods.md` documents, and is gated by the parity guarantee
|
||||||
|
`tests/engine/gate_meta_coverage.lua` enforces. It sits next to
|
||||||
|
`catch.nickname` (RFC 0015) on the same capture path: that hook answers
|
||||||
|
*what the mon is called* once its home is decided; this one decides the home
|
||||||
|
when the party cannot hold it.
|
||||||
|
|
||||||
|
There is no in-repo D-number registry to amend.
|
||||||
|
|
||||||
|
## Exact API delta
|
||||||
|
|
||||||
|
### New hook: `catch.party_full`
|
||||||
|
|
||||||
|
```lua
|
||||||
|
mod.hooks:wrap("catch.party_full", function(next, ctx)
|
||||||
|
-- ctx = { battle = <BattleState>, mon = <Pokemon>, name = <display name>,
|
||||||
|
-- game = <Game> }
|
||||||
|
if myMode.active then
|
||||||
|
takeCustody(ctx.mon) -- the mon is the mod's problem now
|
||||||
|
return true -- nothing is deposited
|
||||||
|
end
|
||||||
|
return next(ctx) -- false: deposit, as today
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
Call site: `BattleState:partyFullDestination(mon)`, called from the capture
|
||||||
|
path at the moment `AddPartyMon` has failed and `SendNewMonToBox` would run.
|
||||||
|
The vanilla link returns `false`. A truthy return skips the box entirely —
|
||||||
|
the mon is neither in the party nor in any box, and `pokemon.caught` reports
|
||||||
|
`destination = "mod"` so the mode can find its own custody again. Anything
|
||||||
|
falsy deposits as always, "But every BOX is full!" included.
|
||||||
|
|
||||||
|
The method returns `"box"` or `"mod"`, and is a *method* on purpose: the
|
||||||
|
compatibility seam for engines that predate this RFC needs a name to ask for
|
||||||
|
(see below), and `battleStyle`/`offerNickname` set the precedent.
|
||||||
|
|
||||||
|
### One event value, already emitted
|
||||||
|
|
||||||
|
`pokemon.caught`'s `destination` gains a third value, `"mod"`, next to
|
||||||
|
`"party"` and `"box"`. Nothing that reads the event today matches on it.
|
||||||
|
|
||||||
|
### No other surface changes
|
||||||
|
|
||||||
|
The call site is guarded by `Runtime.wantsHook`, and the vanilla link is a
|
||||||
|
file-local, so a build with nothing wrapped runs the branch exactly as
|
||||||
|
before and allocates nothing it did not allocate before.
|
||||||
|
|
||||||
|
## Migration
|
||||||
|
|
||||||
|
Nothing changes for existing mods. A mode that was fighting the box after
|
||||||
|
the fact — withdrawing the deposit it could not prevent, or eating the loss —
|
||||||
|
should wrap `catch.party_full` and take the mon at the moment of the catch.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- `tests/modkit/cases/catch_party_full.lua` — through the public mod API:
|
||||||
|
with no mod a 6/6 catch lands in the box with the transfer text; a wrapped
|
||||||
|
mod that claims custody leaves the mon out of both party and boxes; a
|
||||||
|
fall-through deposits; the hook's ctx carries the battle, the mon, the
|
||||||
|
display name and the game.
|
||||||
|
- `tests/engine/gate_hooks.lua` — the name is in the live catalog and passes
|
||||||
|
the no-mod parity gate (vanilla called exactly once, result unchanged,
|
||||||
|
nothing allocated).
|
||||||
|
- `tests/engine/gate_meta_coverage.lua` — the name is covered by the unit
|
||||||
|
corpus.
|
||||||
|
|
||||||
|
## Backward compatibility
|
||||||
|
|
||||||
|
Additive. No existing hook, event, registry or manifest field changes shape.
|
||||||
|
`storeCaughtMon`'s box branch keeps its text, its nickname offer and its
|
||||||
|
`pokemon.caught` emit; the only reader-visible difference with no subscriber
|
||||||
|
is one extra method on `BattleState`.
|
||||||
|
|
||||||
|
## Compatibility seam for older engines
|
||||||
|
|
||||||
|
The call site is mid-function, so a mod on a stock engine cannot see it —
|
||||||
|
the same problem `world.talk` has. The battle-royale mod's shim covers the
|
||||||
|
gap by wrapping `Boxes.deposit` (the one call the old branch makes that a
|
||||||
|
patch can reach): during a match it raises `catch.party_full` first, and a
|
||||||
|
claim refuses the deposit, so nothing reaches a box even there. The mod then
|
||||||
|
takes custody from the `pokemon.caught` emit, which carries the mon. What
|
||||||
|
the shim cannot repair is the text: the old branch answers a refused deposit
|
||||||
|
with "But every BOX is full!" before the mode's own prompt opens — the wrong
|
||||||
|
reason for the right decision, and the argument for the seam.
|
||||||
+92
-4
@@ -214,12 +214,99 @@ finally the bare name handed to the system loader. Per-OS names are
|
|||||||
`librashader_bridge.dylib` (macOS), and `liblibrashader_bridge.so` or
|
`librashader_bridge.dylib` (macOS), and `liblibrashader_bridge.so` or
|
||||||
`librashader_bridge.so` (Linux and Android). Android resolves the bare name
|
`librashader_bridge.so` (Linux and Android). Android resolves the bare name
|
||||||
because the `.so` ships as an ordinary `jniLibs` entry, so `dlopen` finds it
|
because the `.so` ships as an ordinary `jniLibs` entry, so `dlopen` finds it
|
||||||
without a path. The desktop path is still a developer build sitting in cargo's
|
without a path. `ShaderFX.canConvert()` reports whether the library resolved on this machine,
|
||||||
output directory; nothing packages it next to a shipped game yet.
|
|
||||||
`ShaderFX.canConvert()` reports whether the library resolved on this machine,
|
|
||||||
and `ShaderFX.bridgeError()` says why not. Activating an already-converted
|
and `ShaderFX.bridgeError()` says why not. Activating an already-converted
|
||||||
preset never needs any of this.
|
preset never needs any of this.
|
||||||
|
|
||||||
|
**Shipped builds carry it.** `cargo` only ever emits the host's library, and
|
||||||
|
the macOS release runner packs macOS, Windows and Linux in a single
|
||||||
|
`scripts/build.sh all`, so release CI cross-builds one cdylib per platform on
|
||||||
|
its own native runner (the `shaderfx-bridge` matrix in
|
||||||
|
`.github/workflows/release.yml`) and stages it at `dist/native/<plat>/`.
|
||||||
|
`bundle_shader_bridge` in `scripts/build.sh` reads that directory, and
|
||||||
|
`SHADERFX_BRIDGE_<PLAT>` overrides it. Where each artifact puts the library:
|
||||||
|
|
||||||
|
| Artifact | Location | Found by |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| macOS .app | `Contents/MacOS/` | source base directory |
|
||||||
|
| Windows zip | next to `love.exe` | source base directory |
|
||||||
|
| Linux AppImage (both arches) | AppDir root, next to `game.love` | source base directory |
|
||||||
|
| Flatpak | `/app/share/gen1recomp/` | source base directory |
|
||||||
|
| RG34XXSP / ARM SBC ports | `libs.aarch64/` | bare name via `LD_LIBRARY_PATH` |
|
||||||
|
| Android APK | `jniLibs/<abi>/` | bare name |
|
||||||
|
| iOS | linked into the binary | `ffi.C` |
|
||||||
|
|
||||||
|
The macOS library is a universal binary, because LÖVE.app is: an arm64-only
|
||||||
|
bridge would fail `ffi.load` on Intel Macs the moment they hit CONVERT. The
|
||||||
|
two Linux libraries are built with `cargo-zigbuild` against
|
||||||
|
`{x86_64,aarch64}-unknown-linux-gnu.2.17`, below every consumer: the upstream
|
||||||
|
x86_64 LÖVE AppImage floors at GLIBC_2.29, the arm64 AppImage's own gate is
|
||||||
|
GLIBC_2.31, PortMaster's aarch64 LÖVE runtime floors at GLIBC_2.17, and ArkOS
|
||||||
|
is glibc 2.30. 2.17 is the oldest glibc with aarch64 support, so one library
|
||||||
|
per arch clears the whole fleet. `scripts/linux-arm64/verify_appimage.sh`
|
||||||
|
discovers every ELF object in the AppDir rather than globbing `bin/love` and
|
||||||
|
`lib/*.so*`, so the bridge is held to the same floor, resolution and
|
||||||
|
dlopen-not-linked rules as everything else, then dlopened once to prove
|
||||||
|
relocations and constructors run.
|
||||||
|
|
||||||
|
Setting `SHADERFX_BRIDGE_REQUIRED=1` turns the packagers' "not found" warning
|
||||||
|
into a hard error. Release CI sets it on every job that ships a bridge, so a
|
||||||
|
release cannot silently go out without one again.
|
||||||
|
|
||||||
|
**Where CONVERT is unavailable.** Switch and Xbox UWP ship without a bridge.
|
||||||
|
The Switch build uses devkitPro's toolchain, which is not a rustc target; the
|
||||||
|
UWP appcontainer is unproven for loading a desktop MSVC DLL. On both, presets
|
||||||
|
converted elsewhere still activate and run normally, and `ShaderFX.canConvert()`
|
||||||
|
returns false with `ShaderFX.bridgeError()` explaining why.
|
||||||
|
|
||||||
|
**Statically linked bridges.** On iOS there is no loadable library at all:
|
||||||
|
the bridge is linked straight into the app binary, so its symbols live in the
|
||||||
|
main image and are reachable only through `ffi.C`. The candidate list is empty
|
||||||
|
on iOS and, on every platform, a failed candidate walk falls back to probing
|
||||||
|
`ffi.C` for `librashader_translate_preset` after the `ffi.cdef`. If the symbol
|
||||||
|
is there, `ffi.C` becomes the library handle and `ShaderFX.translate` uses it
|
||||||
|
unchanged. If it is not, `ShaderFX.bridgeError()` reports that `ffi.C` was
|
||||||
|
probed as well, followed by the file paths that were tried (omitted on iOS,
|
||||||
|
where none are).
|
||||||
|
|
||||||
|
**Android ships the bridge inside the APK.** `scripts/build_android.sh`
|
||||||
|
stages `liblibrashader_bridge.so` into
|
||||||
|
`mobile/android/app/src/main/jniLibs/<abi>/` for the two ABIs the APK ships,
|
||||||
|
arm64-v8a and armeabi-v7a, so `ffi.load("liblibrashader_bridge.so")` finds it
|
||||||
|
by bare name in the app's native library directory. The build cross-compiles
|
||||||
|
the crate with `cargo ndk` against the same NDK the gradle project uses
|
||||||
|
(25.2.9519653), which needs `cargo install cargo-ndk` and
|
||||||
|
`rustup target add aarch64-linux-android armv7-linux-androideabi`; the script
|
||||||
|
names the exact command for any target that is missing. Set
|
||||||
|
`SHADERFX_BRIDGE_ANDROID_DIR` to a directory holding
|
||||||
|
`<abi>/liblibrashader_bridge.so` to bundle prebuilt libraries instead. As on
|
||||||
|
desktop, the bridge is only needed to CONVERT a preset: a build without it
|
||||||
|
still runs presets converted elsewhere, and the packager warns and continues
|
||||||
|
rather than failing.
|
||||||
|
|
||||||
|
**iOS links the bridge instead of loading it.** An iOS app cannot `dlopen` a
|
||||||
|
dylib shipped beside its binary, so `tools/shaderfx-bridge` also builds as a
|
||||||
|
`staticlib` and `scripts/build_ios.sh` links it into the app executable
|
||||||
|
(`bundle_shader_bridge_ios`, mirroring `bundle_shader_bridge` in
|
||||||
|
`scripts/build.sh`). `SHADERFX_BRIDGE_IOS` points at a prebuilt archive;
|
||||||
|
otherwise cargo builds `aarch64-apple-ios` for device builds and every
|
||||||
|
installed simulator target (`aarch64-apple-ios-sim`, `x86_64-apple-ios`) for
|
||||||
|
the Simulator, with `IPHONEOS_DEPLOYMENT_TARGET=15.0`, and `ARCHS` is pinned to
|
||||||
|
what got built. The archive is never linked directly: LÖVE 12 carries its own
|
||||||
|
glslang for shader validation and the crate drags in a second, incompatible
|
||||||
|
one, and linking both crashed inside `Shader::validateInternal` at startup.
|
||||||
|
Each slice is therefore prelinked with `ld -r -all_load
|
||||||
|
-exported_symbols_list` so only `_librashader_translate_preset` and
|
||||||
|
`_librashader_free_string` stay external and every other symbol (glslang,
|
||||||
|
spirv-cross, Rust std) becomes private to the object, then `rust-objcopy`
|
||||||
|
drops the `__LLVM` bitcode sections rustup's prebuilt std carries, since
|
||||||
|
Apple's `nm` cannot read them. `OTHER_LDFLAGS` adds `-Wl,-u` on both entry
|
||||||
|
points to keep them past dead-stripping and `-lc++` for the C++ the crate
|
||||||
|
needs; no Objective-C framework is involved. A build that linked the object
|
||||||
|
fails if `nm` cannot find `_librashader_translate_preset` in the finished
|
||||||
|
binary; a build that could not produce one only warns and lands where the
|
||||||
|
desktop packages without cargo land: converted presets run, CONVERT does not.
|
||||||
|
|
||||||
### Fixup
|
### Fixup
|
||||||
|
|
||||||
`ShaderFixup.lua` mechanically rewrites the emitted GLSL into something LOVE
|
`ShaderFixup.lua` mechanically rewrites the emitted GLSL into something LOVE
|
||||||
@@ -656,7 +743,8 @@ None of these are theoretical.
|
|||||||
`bundle_shader_bridge`, building it with cargo when a prebuilt one is not
|
`bundle_shader_bridge`, building it with cargo when a prebuilt one is not
|
||||||
supplied through `SHADERFX_BRIDGE`. A build host without cargo produces a
|
supplied through `SHADERFX_BRIDGE`. A build host without cargo produces a
|
||||||
package that can run converted presets but cannot CONVERT new ones, and says
|
package that can run converted presets but cannot CONVERT new ones, and says
|
||||||
so rather than failing. Android ships the `.so` via `jniLibs`.
|
so rather than failing. `scripts/build_android.sh` and `scripts/build_ios.sh`
|
||||||
|
follow the same rule with `cargo ndk` and the iOS static archive.
|
||||||
- **The buildbot shortlist is a temporary trim.** `KEPT_PRESETS` reflects one
|
- **The buildbot shortlist is a temporary trim.** `KEPT_PRESETS` reflects one
|
||||||
manual pass over `handheld/` and is expected to change, most likely to shrink.
|
manual pass over `handheld/` and is expected to change, most likely to shrink.
|
||||||
- **Tilt direction is unverified.** Which way forward and back rocking moves the
|
- **Tilt direction is unverified.** Which way forward and back rocking moves the
|
||||||
|
|||||||
+3
-2
@@ -63,8 +63,9 @@ mounted or deleted as stale; the launcher directs the player to a full package.
|
|||||||
## Release assets
|
## Release assets
|
||||||
|
|
||||||
Each tagged release `vX.Y.Z` carries the existing per-platform archives
|
Each tagged release `vX.Y.Z` carries the existing per-platform archives
|
||||||
(`gen1recomp-X.Y.Z-macos.zip`, `-windows.zip`, `-linux.zip`,
|
(`gen1recomp-X.Y.Z-macos.zip`, `-windows.zip`,
|
||||||
`-linux-arm64.AppImage`, `-android.apk`, `-ios.ipa`, `-switch.zip`, Xbox and
|
`-linux-x86_64.AppImage`, `-linux-arm64.AppImage`, `-linux.flatpak`,
|
||||||
|
`-android.apk`, `-ios.ipa`, `-switch.zip`, Xbox and
|
||||||
PortMaster archives) plus two assets the updater itself consumes:
|
PortMaster archives) plus two assets the updater itself consumes:
|
||||||
|
|
||||||
- `gen1recomp-X.Y.Z.love` - the payload, matched by the exact pattern
|
- `gen1recomp-X.Y.Z.love` - the payload, matched by the exact pattern
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=gen1recomp
|
||||||
|
Comment=Pokémon Gen 1/2 recompilation
|
||||||
|
Exec=gen1recomp
|
||||||
|
Icon=com.theboisclub.gen1recomp
|
||||||
|
Terminal=false
|
||||||
|
Categories=Game;
|
||||||
|
StartupWMClass=love
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<component type="desktop-application">
|
||||||
|
<id>com.theboisclub.gen1recomp</id>
|
||||||
|
<name>gen1recomp</name>
|
||||||
|
<summary>Pokémon Gen 1/2 recompilation</summary>
|
||||||
|
<metadata_license>CC0-1.0</metadata_license>
|
||||||
|
<project_license>LicenseRef-proprietary</project_license>
|
||||||
|
<developer_name>The Bois Club</developer_name>
|
||||||
|
<description>
|
||||||
|
<p>
|
||||||
|
A LÖVE-based recompilation of Pokémon Gen 1 and Gen 2 with a launcher,
|
||||||
|
ROM importer, mods, and optional cloud save sync. Requires your own
|
||||||
|
legal cartridge dumps.
|
||||||
|
</p>
|
||||||
|
</description>
|
||||||
|
<launchable type="desktop-id">com.theboisclub.gen1recomp.desktop</launchable>
|
||||||
|
<url type="homepage">https://github.com/bryanthaboi/gen1recomp</url>
|
||||||
|
<screenshots>
|
||||||
|
<screenshot type="default">
|
||||||
|
<caption>Launcher</caption>
|
||||||
|
<image>https://raw.githubusercontent.com/bryanthaboi/gen1recomp/main/assets/logo/gen1recomp_cover.png</image>
|
||||||
|
</screenshot>
|
||||||
|
</screenshots>
|
||||||
|
<content_rating type="oars-1.1"/>
|
||||||
|
<releases>
|
||||||
|
<release version="0.0.0" date="1970-01-01"/>
|
||||||
|
</releases>
|
||||||
|
</component>
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# Template consumed by scripts/build_flatpak.sh (placeholders substituted).
|
||||||
|
# Do not pass this file directly to flatpak-builder.
|
||||||
|
app-id: com.theboisclub.gen1recomp
|
||||||
|
runtime: org.freedesktop.Platform
|
||||||
|
runtime-version: "24.08"
|
||||||
|
sdk: org.freedesktop.Sdk
|
||||||
|
command: gen1recomp
|
||||||
|
finish-args:
|
||||||
|
- --share=network
|
||||||
|
- --share=ipc
|
||||||
|
- --socket=x11
|
||||||
|
- --socket=wayland
|
||||||
|
- --device=dri
|
||||||
|
- --socket=pulseaudio
|
||||||
|
# Full input device access so SDL2 sees udev/evdev gamepads + rumble/gyro
|
||||||
|
- --device=all
|
||||||
|
- --filesystem=home
|
||||||
|
modules:
|
||||||
|
- name: curl
|
||||||
|
buildsystem: simple
|
||||||
|
build-commands:
|
||||||
|
- tar -xJf curl-static.tar.xz
|
||||||
|
- install -Dm755 curl /app/bin/curl
|
||||||
|
sources:
|
||||||
|
- type: file
|
||||||
|
url: https://github.com/stunnel/static-curl/releases/download/8.21.0/curl-linux-x86_64-musl-8.21.0.tar.xz
|
||||||
|
sha256: e955f211202ded2536164588331acfc987dc4b7857efa3577717b1ffeab22029
|
||||||
|
dest-filename: curl-static.tar.xz
|
||||||
|
|
||||||
|
- name: love
|
||||||
|
buildsystem: simple
|
||||||
|
build-commands:
|
||||||
|
- chmod +x love-x86_64.AppImage
|
||||||
|
- ./love-x86_64.AppImage --appimage-extract
|
||||||
|
- mkdir -p /app/lib/love /app/bin /app/share/gen1recomp
|
||||||
|
- cp -a squashfs-root/bin/love /app/bin/love
|
||||||
|
- cp -a squashfs-root/lib/. /app/lib/love/
|
||||||
|
- |
|
||||||
|
cat > /app/bin/gen1recomp <<'EOF'
|
||||||
|
#!/bin/sh
|
||||||
|
export LD_LIBRARY_PATH="/app/lib/love:${LD_LIBRARY_PATH:-}"
|
||||||
|
exec /app/bin/love /app/share/gen1recomp/game.love "$@"
|
||||||
|
EOF
|
||||||
|
- chmod +x /app/bin/gen1recomp
|
||||||
|
- install -Dm644 game.love /app/share/gen1recomp/game.love
|
||||||
|
# BRIDGE-BEGIN
|
||||||
|
- install -Dm755 liblibrashader_bridge.so /app/share/gen1recomp/liblibrashader_bridge.so
|
||||||
|
# BRIDGE-END
|
||||||
|
- install -Dm644 com.theboisclub.gen1recomp.desktop /app/share/applications/com.theboisclub.gen1recomp.desktop
|
||||||
|
- install -Dm644 com.theboisclub.gen1recomp.metainfo.xml /app/share/metainfo/com.theboisclub.gen1recomp.metainfo.xml
|
||||||
|
- install -Dm644 icon.png /app/share/icons/hicolor/512x512/apps/com.theboisclub.gen1recomp.png
|
||||||
|
sources:
|
||||||
|
- type: file
|
||||||
|
url: https://github.com/love2d/love/releases/download/11.5/love-11.5-x86_64.AppImage
|
||||||
|
sha256: 65a673406431eff7167a15a032bf7a2e4ba50108e091eb7b176465831f9b5e00
|
||||||
|
dest-filename: love-x86_64.AppImage
|
||||||
|
- type: file
|
||||||
|
path: game.love
|
||||||
|
# BRIDGE-BEGIN
|
||||||
|
- type: file
|
||||||
|
path: liblibrashader_bridge.so
|
||||||
|
# BRIDGE-END
|
||||||
|
- type: file
|
||||||
|
path: com.theboisclub.gen1recomp.metainfo.xml
|
||||||
|
- type: file
|
||||||
|
path: com.theboisclub.gen1recomp.desktop
|
||||||
|
- type: file
|
||||||
|
path: icon.png
|
||||||
@@ -22,6 +22,56 @@ local PlatformHooks = require("src.core.PlatformHooks")
|
|||||||
local HostDisplay = require("src.core.HostDisplay")
|
local HostDisplay = require("src.core.HostDisplay")
|
||||||
local GameViewport = require("src.render.GameViewport")
|
local GameViewport = require("src.render.GameViewport")
|
||||||
|
|
||||||
|
-- Global emergency quit: holding Start + Select for 5 seconds forcefully terminates LOVE.
|
||||||
|
local emergencyQuitTimer = 0
|
||||||
|
|
||||||
|
local function checkEmergencyQuit(dt)
|
||||||
|
local held = false
|
||||||
|
if love.joystick and love.joystick.getJoysticks then
|
||||||
|
local joysticks = love.joystick.getJoysticks()
|
||||||
|
for _, j in ipairs(joysticks) do
|
||||||
|
if j:isGamepad() then
|
||||||
|
local start = j:isGamepadDown("start")
|
||||||
|
local selectBtn = j:isGamepadDown("back") or j:isGamepadDown("guide")
|
||||||
|
if start and selectBtn then
|
||||||
|
held = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local bCount = j:getButtonCount()
|
||||||
|
local s1 = (bCount >= 7 and j:isDown(7)) or (bCount >= 9 and j:isDown(9))
|
||||||
|
local s2 = (bCount >= 8 and j:isDown(8)) or (bCount >= 10 and j:isDown(10))
|
||||||
|
if s1 and s2 then
|
||||||
|
held = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if love.keyboard and love.keyboard.isDown then
|
||||||
|
if (love.keyboard.isDown("escape") and love.keyboard.isDown("return"))
|
||||||
|
or (love.keyboard.isDown("lalt") and love.keyboard.isDown("f4")) then
|
||||||
|
held = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if held then
|
||||||
|
emergencyQuitTimer = emergencyQuitTimer + (dt or 0.016)
|
||||||
|
if emergencyQuitTimer >= 5.0 then
|
||||||
|
print("[FORCE QUIT] Start + Select held for 5 seconds. Exiting forcefully.")
|
||||||
|
pcall(function()
|
||||||
|
if love.audio and love.audio.stop then love.audio.stop() end
|
||||||
|
if love.window and love.window.close then love.window.close() end
|
||||||
|
end)
|
||||||
|
local exitFn = os["exit"]
|
||||||
|
exitFn(0)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
emergencyQuitTimer = 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- Lua errors: persist a redacted trace in the save dir and surface a hint.
|
-- Lua errors: persist a redacted trace in the save dir and surface a hint.
|
||||||
do
|
do
|
||||||
local defaultErrorHandler = love.errorhandler or love.errhand
|
local defaultErrorHandler = love.errorhandler or love.errhand
|
||||||
@@ -30,10 +80,33 @@ do
|
|||||||
if ok and hint and type(msg) == "string" then
|
if ok and hint and type(msg) == "string" then
|
||||||
msg = msg .. "\n\n" .. hint
|
msg = msg .. "\n\n" .. hint
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if love.window and love.window.isOpen and love.window.isOpen() and love.graphics and love.graphics.isActive() then
|
||||||
|
local fullMsg = tostring(msg) .. "\n\n" .. tostring(debug.traceback()) .. "\n\n[Hold START + SELECT for 5s to Force Quit]"
|
||||||
|
return function()
|
||||||
|
love.event.pump()
|
||||||
|
for e, a in love.event.poll() do
|
||||||
|
if e == "quit" or (e == "keypressed" and a == "escape") then
|
||||||
|
return 1
|
||||||
|
elseif e == "gamepadpressed" and (a == "start" or a == "back") then
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
checkEmergencyQuit(0.016)
|
||||||
|
love.graphics.origin()
|
||||||
|
love.graphics.clear(0.10, 0.10, 0.12)
|
||||||
|
love.graphics.setColor(1, 0.4, 0.4, 1)
|
||||||
|
love.graphics.printf(fullMsg, 20, 20, love.graphics.getWidth() - 40)
|
||||||
|
love.graphics.present()
|
||||||
|
love.timer.sleep(0.016)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
if defaultErrorHandler then
|
if defaultErrorHandler then
|
||||||
return defaultErrorHandler(msg)
|
return defaultErrorHandler(msg)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
love.errhand = love.errorhandler
|
||||||
end
|
end
|
||||||
|
|
||||||
local Game, EditorApp, Importer, TouchEditor, Studio
|
local Game, EditorApp, Importer, TouchEditor, Studio
|
||||||
@@ -384,7 +457,16 @@ function bootGame(version, cartId)
|
|||||||
Game = require("src.core.Game2").new()
|
Game = require("src.core.Game2").new()
|
||||||
Game:load()
|
Game:load()
|
||||||
else
|
else
|
||||||
Game = require("src.core.Game")
|
-- Gen1 Game is a module singleton. Always re-require after in-process
|
||||||
|
-- EXIT GAME so a prior session cannot leave a table whose rawget(load)
|
||||||
|
-- is nil (release Android: bootGame then dies with load-a-nil-value).
|
||||||
|
-- rawget: type(mod.load) can lie via __index and skip a rebuild.
|
||||||
|
package.loaded["src.core.Game"] = nil
|
||||||
|
local gameMod = require("src.core.Game")
|
||||||
|
if type(rawget(gameMod, "load")) ~= "function" then
|
||||||
|
error("src.core.Game missing load after reload")
|
||||||
|
end
|
||||||
|
Game = gameMod
|
||||||
Game:load()
|
Game:load()
|
||||||
if os.getenv("POKEPORT_AUTOPILOT") then
|
if os.getenv("POKEPORT_AUTOPILOT") then
|
||||||
autopilot = require("tests.autopilot")
|
autopilot = require("tests.autopilot")
|
||||||
@@ -550,6 +632,7 @@ function love.load(args)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function love.update(dt)
|
function love.update(dt)
|
||||||
|
checkEmergencyQuit(dt)
|
||||||
HostDisplay.update(dt)
|
HostDisplay.update(dt)
|
||||||
SwitchDiagnostics.maybeFlush(false)
|
SwitchDiagnostics.maybeFlush(false)
|
||||||
-- NX only (no-op elsewhere): follow dock/undock without waiting for SDL.
|
-- NX only (no-op elsewhere): follow dock/undock without waiting for SDL.
|
||||||
@@ -1118,15 +1201,23 @@ function love.quit()
|
|||||||
-- docs/modding.md's core.quit_to_launcher entry) may veto returning to
|
-- docs/modding.md's core.quit_to_launcher entry) may veto returning to
|
||||||
-- this Lua launcher via that hook. Vanilla behavior (used when no mod
|
-- this Lua launcher via that hook. Vanilla behavior (used when no mod
|
||||||
-- claims the hook) is exactly the condition below.
|
-- claims the hook) is exactly the condition below.
|
||||||
local isAndroid = (love.system and love.system.getOS and love.system.getOS() == "Android")
|
--
|
||||||
|
-- Android and iOS both tear down LOVE in-process rather than
|
||||||
|
-- love.event.quit("restart"): Android's vendored love.cpp PHYSFS-crashes
|
||||||
|
-- on a second init (#575), and iOS's love.cpp forces DONE_RESTART for
|
||||||
|
-- every quit while warning that leftover threads make that unreliable.
|
||||||
|
-- SessionLifecycle workers (ChipAudio / Fetch / Check) make that warning
|
||||||
|
-- real -- endProcess joins them, then the native restart still blows up.
|
||||||
|
local osName = love.system and love.system.getOS and love.system.getOS()
|
||||||
|
local inProcessReturn = (osName == "Android" or osName == "iOS")
|
||||||
local wouldReturnToLauncher = PlatformHooks.quitToLauncher(function()
|
local wouldReturnToLauncher = PlatformHooks.quitToLauncher(function()
|
||||||
return Game and not Importer and not quitToLauncher and not scripted
|
return Game and not Importer and not quitToLauncher and not scripted
|
||||||
and (isAndroid or not launchedIntoGame)
|
and (inProcessReturn or not launchedIntoGame)
|
||||||
end)
|
end)
|
||||||
if wouldReturnToLauncher then
|
if wouldReturnToLauncher then
|
||||||
if isAndroid then
|
if inProcessReturn then
|
||||||
returnToLauncher()
|
returnToLauncher()
|
||||||
return true -- abort this quit; the restart lands back in the launcher
|
return true -- abort this quit; stay in the same LOVE run
|
||||||
end
|
end
|
||||||
quitToLauncher = true
|
quitToLauncher = true
|
||||||
-- Tell the fresh boot to ignore any boot-straight-into-a-game option this
|
-- Tell the fresh boot to ignore any boot-straight-into-a-game option this
|
||||||
@@ -1170,6 +1261,17 @@ function love.run()
|
|||||||
-- per-frame sleep-granularity jitter.
|
-- per-frame sleep-granularity jitter.
|
||||||
local nextFrame = love.timer and love.timer.getTime() or 0
|
local nextFrame = love.timer and love.timer.getTime() or 0
|
||||||
local dt = 0
|
local dt = 0
|
||||||
|
local idleFor = 0
|
||||||
|
local SLEEP_FLOOR = 0.001
|
||||||
|
local WAKE = {
|
||||||
|
keypressed = true, keyreleased = true, textinput = true,
|
||||||
|
mousepressed = true, mousereleased = true, mousemoved = true,
|
||||||
|
wheelmoved = true, touchpressed = true, touchreleased = true,
|
||||||
|
touchmoved = true, joystickpressed = true, joystickreleased = true,
|
||||||
|
joystickhat = true, gamepadpressed = true, gamepadreleased = true,
|
||||||
|
joystickadded = true, joystickremoved = true, filedropped = true,
|
||||||
|
directorydropped = true, focus = true, visible = true, resize = true,
|
||||||
|
}
|
||||||
|
|
||||||
return function()
|
return function()
|
||||||
-- process events
|
-- process events
|
||||||
@@ -1188,17 +1290,36 @@ function love.run()
|
|||||||
return a or 0
|
return a or 0
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
if WAKE[name] then
|
||||||
|
idleFor = 0
|
||||||
|
elseif name == "joystickaxis" and type(c) == "number" and math.abs(c) > 0.5 then
|
||||||
|
idleFor = 0
|
||||||
|
end
|
||||||
love.handlers[name](a, b, c, d, e, f)
|
love.handlers[name](a, b, c, d, e, f)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- update dt
|
-- update dt
|
||||||
if love.timer then dt = love.timer.step() end
|
if love.timer then dt = love.timer.step() end
|
||||||
|
idleFor = idleFor + dt
|
||||||
|
|
||||||
|
checkEmergencyQuit(dt)
|
||||||
|
|
||||||
-- call update and draw
|
-- call update and draw
|
||||||
if love.update then love.update(dt) end
|
if love.update then love.update(dt) end
|
||||||
|
|
||||||
if love.graphics and love.graphics.isActive() then
|
local visible = not (love.window and love.window.isVisible)
|
||||||
|
or love.window.isVisible()
|
||||||
|
local focused = not (love.window and love.window.hasFocus)
|
||||||
|
or love.window.hasFocus()
|
||||||
|
local cap = FrameCap.current
|
||||||
|
if not visible then
|
||||||
|
cap = 10
|
||||||
|
elseif Importer and (not focused or idleFor > 30) then
|
||||||
|
cap = 15
|
||||||
|
end
|
||||||
|
|
||||||
|
if visible and love.graphics and love.graphics.isActive() then
|
||||||
love.graphics.origin()
|
love.graphics.origin()
|
||||||
love.graphics.clear(love.graphics.getBackgroundColor())
|
love.graphics.clear(love.graphics.getBackgroundColor())
|
||||||
if love.draw then love.draw() end
|
if love.draw then love.draw() end
|
||||||
@@ -1209,9 +1330,9 @@ function love.run()
|
|||||||
if paced then
|
if paced then
|
||||||
-- Sleep out the remainder of the frame budget, measured from the
|
-- Sleep out the remainder of the frame budget, measured from the
|
||||||
-- carried deadline, in small chunks so the OS timer stays
|
-- carried deadline, in small chunks so the OS timer stays
|
||||||
-- responsive. vsync is untouched: when it already paces slower
|
-- responsive. The pacer yields to vsync inside a 1ms dead band, so
|
||||||
-- than the cap the remainder is <= 0 and this rounds to a no-op.
|
-- when the panel already paces at or below the cap it is a no-op.
|
||||||
local budget = 1 / FrameCap.current
|
local budget = 1 / cap
|
||||||
nextFrame = nextFrame + budget
|
nextFrame = nextFrame + budget
|
||||||
local now = love.timer.getTime()
|
local now = love.timer.getTime()
|
||||||
-- A stall (alt-tab, a GC pause, a blocked import) can leave the
|
-- A stall (alt-tab, a GC pause, a blocked import) can leave the
|
||||||
@@ -1222,8 +1343,8 @@ function love.run()
|
|||||||
end
|
end
|
||||||
while true do
|
while true do
|
||||||
local remaining = nextFrame - love.timer.getTime()
|
local remaining = nextFrame - love.timer.getTime()
|
||||||
if remaining <= 0 then break end
|
if remaining <= SLEEP_FLOOR then break end
|
||||||
love.timer.sleep(remaining < 0.001 and remaining or 0.001)
|
love.timer.sleep(0.001)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
love.timer.sleep(0.001)
|
love.timer.sleep(0.001)
|
||||||
|
|||||||
@@ -100,6 +100,11 @@ love-android 11.5a expects:
|
|||||||
Set `ANDROID_SDK_ROOT` (or `ANDROID_HOME`), or let the script write
|
Set `ANDROID_SDK_ROOT` (or `ANDROID_HOME`), or let the script write
|
||||||
`local.properties` when it finds `~/Library/Android/sdk`.
|
`local.properties` when it finds `~/Library/Android/sdk`.
|
||||||
|
|
||||||
|
**ShaderFX bridge**: `scripts/build_android.sh` bundles
|
||||||
|
`liblibrashader_bridge.so` for arm64-v8a and armeabi-v7a via `cargo ndk` (or
|
||||||
|
from `SHADERFX_BRIDGE_ANDROID_DIR`), and warns and continues when neither is
|
||||||
|
available; see `docs/shaderfx.md`.
|
||||||
|
|
||||||
Gradle flavor used: **`embedNoRecord`** (game fused into the APK, no microphone).
|
Gradle flavor used: **`embedNoRecord`** (game fused into the APK, no microphone).
|
||||||
Build task: `assembleEmbedNoRecordDebug`.
|
Build task: `assembleEmbedNoRecordDebug`.
|
||||||
|
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ void System::vibrate(double seconds) const
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
bool System::pickFile(const char *kind) const
|
bool System::pickFile(const char *kind, const char *destination) const
|
||||||
{
|
{
|
||||||
#ifdef LOVE_ANDROID
|
#ifdef LOVE_ANDROID
|
||||||
const char *dest = "picked_rom.gb";
|
const char *dest = "picked_rom.gb";
|
||||||
@@ -193,7 +193,8 @@ bool System::pickFile(const char *kind) const
|
|||||||
else if (strcmp(kind, "sav") == 0 || strcmp(kind, "save") == 0)
|
else if (strcmp(kind, "sav") == 0 || strcmp(kind, "save") == 0)
|
||||||
dest = "picked_save.sav";
|
dest = "picked_save.sav";
|
||||||
else if (strcmp(kind, "required_import") == 0)
|
else if (strcmp(kind, "required_import") == 0)
|
||||||
dest = "picked_required_import.bin";
|
dest = (destination != nullptr && destination[0] != '\0')
|
||||||
|
? destination : "picked_required_import.bin";
|
||||||
else if (strcmp(kind, "rom") == 0)
|
else if (strcmp(kind, "rom") == 0)
|
||||||
dest = "picked_rom.gb";
|
dest = "picked_rom.gb";
|
||||||
// Unknown kinds used to fall through to the ROM destination. Refuse them
|
// Unknown kinds used to fall through to the ROM destination. Refuse them
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ public:
|
|||||||
* "required_import" -> picked_required_import.bin.
|
* "required_import" -> picked_required_import.bin.
|
||||||
* @return Whether the picker was shown.
|
* @return Whether the picker was shown.
|
||||||
**/
|
**/
|
||||||
virtual bool pickFile(const char *kind = nullptr) const;
|
virtual bool pickFile(const char *kind = nullptr, const char *destination = nullptr) const;
|
||||||
virtual const char *pickFileKinds() const;
|
virtual const char *pickFileKinds() const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -101,7 +101,8 @@ int w_vibrate(lua_State *L)
|
|||||||
int w_pickFile(lua_State *L)
|
int w_pickFile(lua_State *L)
|
||||||
{
|
{
|
||||||
const char *kind = luaL_optstring(L, 1, nullptr);
|
const char *kind = luaL_optstring(L, 1, nullptr);
|
||||||
luax_pushboolean(L, instance()->pickFile(kind));
|
const char *destination = luaL_optstring(L, 2, nullptr);
|
||||||
|
luax_pushboolean(L, instance()->pickFile(kind, destination));
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import java.util.HashMap;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
import android.app.AlarmManager;
|
import android.app.AlarmManager;
|
||||||
@@ -111,6 +112,8 @@ public class GameActivity extends SDLActivity {
|
|||||||
// basename as its body, so RomImporter:focus can say so in the launcher
|
// basename as its body, so RomImporter:focus can say so in the launcher
|
||||||
// instead of leaving the player on "No ROM imported" (issue #442).
|
// instead of leaving the player on "No ROM imported" (issue #442).
|
||||||
private static final String PICK_ERROR_FILENAME = "pick_error.flag";
|
private static final String PICK_ERROR_FILENAME = "pick_error.flag";
|
||||||
|
// Written after a direct required-import copy has been fully published.
|
||||||
|
private static final String PICK_COMPLETE_FILENAME = "pick_complete.flag";
|
||||||
// Step bridge (love.system.syncHealthSteps): pending-steps delivery
|
// Step bridge (love.system.syncHealthSteps): pending-steps delivery
|
||||||
// consumed by the Pokéwalker mod, same contract as the iOS
|
// consumed by the Pokéwalker mod, same contract as the iOS
|
||||||
// GRHealthBridge. Steps come from the hardware TYPE_STEP_COUNTER
|
// GRHealthBridge. Steps come from the hardware TYPE_STEP_COUNTER
|
||||||
@@ -576,6 +579,21 @@ public class GameActivity extends SDLActivity {
|
|||||||
* picked_rom.gb, picked_mod.zip, picked_save.sav, or
|
* picked_rom.gb, picked_mod.zip, picked_save.sav, or
|
||||||
* picked_required_import.bin)
|
* picked_required_import.bin)
|
||||||
*/
|
*/
|
||||||
|
private static boolean isDirectRequiredDestination(String relative) {
|
||||||
|
if (relative == null || relative.length() == 0 || relative.startsWith("/")) return false;
|
||||||
|
String normalized = relative.replace('\\', '/');
|
||||||
|
if (!normalized.startsWith("mods/")) return false;
|
||||||
|
int marker = normalized.indexOf("/baseroms/");
|
||||||
|
if (marker <= "mods/".length() || marker + "/baseroms/".length() >= normalized.length()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !normalized.contains("//")
|
||||||
|
&& !normalized.equals("..")
|
||||||
|
&& !normalized.startsWith("../")
|
||||||
|
&& !normalized.contains("/../")
|
||||||
|
&& !normalized.endsWith("/..");
|
||||||
|
}
|
||||||
|
|
||||||
/** Legacy single-argument entry; resolves the save dir itself. */
|
/** Legacy single-argument entry; resolves the save dir itself. */
|
||||||
@Keep
|
@Keep
|
||||||
public static boolean showFilePicker(String destFilename) {
|
public static boolean showFilePicker(String destFilename) {
|
||||||
@@ -593,14 +611,30 @@ public class GameActivity extends SDLActivity {
|
|||||||
// onActivityResult copies the pick there, not into a recomputed
|
// onActivityResult copies the pick there, not into a recomputed
|
||||||
// (possibly different-volume) root (#604, #839).
|
// (possibly different-volume) root (#604, #839).
|
||||||
self.pendingPickSaveDir = (saveDir != null) ? saveDir : "";
|
self.pendingPickSaveDir = (saveDir != null) ? saveDir : "";
|
||||||
// Reject path separators so a hostile JNI caller cannot escape the
|
// Basename destinations keep the historical ROM/mod/save staging path.
|
||||||
// save identity directory.
|
// A nested destination is accepted only for an engine-generated mod
|
||||||
if (destFilename.indexOf('/') >= 0 || destFilename.indexOf('\\') >= 0) {
|
// baseroms path, then canonicalized beneath LOVE's mounted save root.
|
||||||
|
String normalizedDest = destFilename.replace('\\', '/');
|
||||||
|
boolean nested = normalizedDest.indexOf('/') >= 0;
|
||||||
|
if (nested && !isDirectRequiredDestination(normalizedDest)) {
|
||||||
|
Log.d("GameActivity", "refusing non-baseroms picker dest: " + destFilename);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
File rootCanonical = self.saveIdentityDir().getCanonicalFile();
|
||||||
|
File destCanonical = new File(rootCanonical, normalizedDest).getCanonicalFile();
|
||||||
|
String rootPrefix = rootCanonical.getPath() + File.separator;
|
||||||
|
if (destCanonical.equals(rootCanonical)
|
||||||
|
|| !destCanonical.getPath().startsWith(rootPrefix)) {
|
||||||
Log.d("GameActivity", "refusing unsafe picker dest: " + destFilename);
|
Log.d("GameActivity", "refusing unsafe picker dest: " + destFilename);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.d("GameActivity", "could not validate picker dest: " + e.getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
self.pendingPickFilename = destFilename;
|
self.pendingPickFilename = normalizedDest;
|
||||||
if (android.os.Build.VERSION.SDK_INT >= 21) {
|
if (android.os.Build.VERSION.SDK_INT >= 21) {
|
||||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||||
@@ -1277,13 +1311,7 @@ public class GameActivity extends SDLActivity {
|
|||||||
|
|
||||||
/** Drops a small flag file in the save identity for Lua to consume on focus. */
|
/** Drops a small flag file in the save identity for Lua to consume on focus. */
|
||||||
private void writeSaveDirFlag(String name, String body) {
|
private void writeSaveDirFlag(String name, String body) {
|
||||||
try {
|
writeFlagFile(saveIdentityDir(), name, body);
|
||||||
FileOutputStream fos = new FileOutputStream(new File(saveIdentityDir(), name), false);
|
|
||||||
fos.write(body.getBytes());
|
|
||||||
fos.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.d("GameActivity", "could not write " + name + ": " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1506,9 +1534,26 @@ public class GameActivity extends SDLActivity {
|
|||||||
Log.d("GameActivity", "could not create " + destDir);
|
Log.d("GameActivity", "could not create " + destDir);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String destName = pendingPickFilename != null
|
final String destName = pendingPickFilename != null
|
||||||
? pendingPickFilename : PICKED_ROM_FILENAME;
|
? pendingPickFilename : PICKED_ROM_FILENAME;
|
||||||
File destFile = new File(destDir, destName);
|
final boolean directRequired = isDirectRequiredDestination(destName);
|
||||||
|
final File destFile;
|
||||||
|
try {
|
||||||
|
File rootCanonical = destDir.getCanonicalFile();
|
||||||
|
destFile = new File(rootCanonical, destName).getCanonicalFile();
|
||||||
|
String rootPrefix = rootCanonical.getPath() + File.separator;
|
||||||
|
if (destFile.equals(rootCanonical)
|
||||||
|
|| !destFile.getPath().startsWith(rootPrefix)
|
||||||
|
|| (destName.indexOf('/') >= 0 && !directRequired)) {
|
||||||
|
Log.d("GameActivity", "refusing unsafe result dest: " + destName);
|
||||||
|
writeSaveDirFlag(PICK_ERROR_FILENAME, destName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.d("GameActivity", "could not validate result dest: " + e.getMessage());
|
||||||
|
writeSaveDirFlag(PICK_ERROR_FILENAME, destName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// ACTION_OPEN_DOCUMENT is meant to land in the system documents UI, but
|
// ACTION_OPEN_DOCUMENT is meant to land in the system documents UI, but
|
||||||
// some OEM shells (ColorOS) offer third-party file managers in a
|
// some OEM shells (ColorOS) offer third-party file managers in a
|
||||||
@@ -1534,15 +1579,110 @@ public class GameActivity extends SDLActivity {
|
|||||||
writeSaveDirFlag(PICK_ERROR_FILENAME, destName);
|
writeSaveDirFlag(PICK_ERROR_FILENAME, destName);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!copyAssetFile(source, destFile.getPath())) {
|
final InputStream pickedSource = source;
|
||||||
|
final File pickedRoot = destDir;
|
||||||
|
if (directRequired) {
|
||||||
|
// Optical-disc-sized imports must not block Android's UI thread and
|
||||||
|
// must not create a second picked_required_import.bin copy.
|
||||||
|
new Thread(new Runnable() {
|
||||||
|
@Override public void run() {
|
||||||
|
PickCopyResult result = copyRequiredImport(pickedSource, destFile);
|
||||||
|
if (!result.ok) {
|
||||||
|
writeFlagFile(pickedRoot, PICK_ERROR_FILENAME, destName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String marker = "v1\n" + destName + "\n" + result.md5 + "\n"
|
||||||
|
+ Long.toString(result.bytes) + "\n";
|
||||||
|
writeFlagFile(pickedRoot, PICK_COMPLETE_FILENAME, marker);
|
||||||
|
}
|
||||||
|
}, "gen1recomp-required-import").start();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!copyAssetFile(pickedSource, destFile.getPath())) {
|
||||||
Log.d("GameActivity", "could not copy picked file to " + destFile);
|
Log.d("GameActivity", "could not copy picked file to " + destFile);
|
||||||
// A truncated pick would only fail verification later, so drop it
|
|
||||||
// and report instead.
|
|
||||||
destFile.delete();
|
destFile.delete();
|
||||||
writeSaveDirFlag(PICK_ERROR_FILENAME, destName);
|
writeSaveDirFlag(PICK_ERROR_FILENAME, destName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final class PickCopyResult {
|
||||||
|
boolean ok = false;
|
||||||
|
long bytes = 0;
|
||||||
|
String md5 = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String hex(byte[] bytes) {
|
||||||
|
StringBuilder out = new StringBuilder(bytes.length * 2);
|
||||||
|
for (byte b : bytes) out.append(String.format(Locale.US, "%02x", b & 0xff));
|
||||||
|
return out.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeFlagFile(File dir, String name, String body) {
|
||||||
|
try {
|
||||||
|
if (!dir.isDirectory() && !dir.mkdirs()) return;
|
||||||
|
File tmp = new File(dir, name + ".tmp");
|
||||||
|
File dest = new File(dir, name);
|
||||||
|
FileOutputStream out = new FileOutputStream(tmp, false);
|
||||||
|
out.write(body.getBytes("UTF-8"));
|
||||||
|
out.getFD().sync();
|
||||||
|
out.close();
|
||||||
|
if (dest.exists() && !dest.delete()) { tmp.delete(); return; }
|
||||||
|
if (!tmp.renameTo(dest)) tmp.delete();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.d("GameActivity", "could not write " + name + ": " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PickCopyResult copyRequiredImport(InputStream source, File destination) {
|
||||||
|
PickCopyResult result = new PickCopyResult();
|
||||||
|
File parent = destination.getParentFile();
|
||||||
|
File partial = new File(destination.getPath() + ".part");
|
||||||
|
BufferedInputStream in = null;
|
||||||
|
BufferedOutputStream out = null;
|
||||||
|
FileOutputStream rawOut = null;
|
||||||
|
try {
|
||||||
|
if (parent != null && !parent.isDirectory() && !parent.mkdirs()) return result;
|
||||||
|
if (partial.exists() && !partial.delete()) return result;
|
||||||
|
MessageDigest md5 = MessageDigest.getInstance("MD5");
|
||||||
|
in = new BufferedInputStream(source, 1024 * 1024);
|
||||||
|
rawOut = new FileOutputStream(partial, false);
|
||||||
|
out = new BufferedOutputStream(rawOut, 1024 * 1024);
|
||||||
|
byte[] buf = new byte[1024 * 1024];
|
||||||
|
int n;
|
||||||
|
long total = 0;
|
||||||
|
while ((n = in.read(buf)) != -1) {
|
||||||
|
if (n == 0) continue;
|
||||||
|
out.write(buf, 0, n);
|
||||||
|
md5.update(buf, 0, n);
|
||||||
|
total += n;
|
||||||
|
}
|
||||||
|
out.flush();
|
||||||
|
rawOut.getFD().sync();
|
||||||
|
out.close(); out = null; rawOut = null;
|
||||||
|
in.close(); in = null;
|
||||||
|
|
||||||
|
// Publish only a complete same-directory file. Validation still
|
||||||
|
// happens in Lua against the manifest before a receipt is written.
|
||||||
|
if (destination.exists() && !destination.delete()) return result;
|
||||||
|
if (!partial.renameTo(destination)) return result;
|
||||||
|
result.ok = true;
|
||||||
|
result.bytes = total;
|
||||||
|
result.md5 = hex(md5.digest());
|
||||||
|
Log.d("GameActivity", "direct required import copied " + total
|
||||||
|
+ " bytes to " + destination);
|
||||||
|
return result;
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.d("GameActivity", "direct required import failed: " + e.getMessage());
|
||||||
|
return result;
|
||||||
|
} finally {
|
||||||
|
try { if (in != null) in.close(); } catch (IOException ignored) {}
|
||||||
|
try { if (out != null) out.close(); } catch (IOException ignored) {}
|
||||||
|
try { if (rawOut != null) rawOut.close(); } catch (IOException ignored) {}
|
||||||
|
if (!result.ok && partial.exists()) partial.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copies a given file from the assets folder to the destination.
|
* Copies a given file from the assets folder to the destination.
|
||||||
*
|
*
|
||||||
@@ -1565,13 +1705,12 @@ public class GameActivity extends SDLActivity {
|
|||||||
assert (source != null && destination != null);
|
assert (source != null && destination != null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
byte[] buf = new byte[1024];
|
byte[] buf = new byte[1024 * 1024];
|
||||||
chunk_read = source.read(buf);
|
while ((chunk_read = source.read(buf)) != -1) {
|
||||||
do {
|
if (chunk_read == 0) continue;
|
||||||
destination.write(buf, 0, chunk_read);
|
destination.write(buf, 0, chunk_read);
|
||||||
bytes_written += chunk_read;
|
bytes_written += chunk_read;
|
||||||
chunk_read = source.read(buf);
|
}
|
||||||
} while (chunk_read != -1);
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.d("GameActivity", "Copying failed:" + e.getMessage());
|
Log.d("GameActivity", "Copying failed:" + e.getMessage());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ The script verifies the final app before packaging it:
|
|||||||
|
|
||||||
- the public Documents plist settings are present
|
- the public Documents plist settings are present
|
||||||
- the native picker bridge is present
|
- the native picker bridge is present
|
||||||
|
- the SHADER FX librashader bridge is linked into the app binary, when this build produced one
|
||||||
- `game.love` exists and is non-empty
|
- `game.love` exists and is non-empty
|
||||||
|
|
||||||
If the payload is missing, the build fails instead of producing a blank app.
|
If the payload is missing, the build fails instead of producing a blank app.
|
||||||
|
|||||||
@@ -12,6 +12,34 @@
|
|||||||
"tintColor": "3b5ca8",
|
"tintColor": "3b5ca8",
|
||||||
"category": "games",
|
"category": "games",
|
||||||
"versions": [
|
"versions": [
|
||||||
|
{
|
||||||
|
"version": "0.2.27",
|
||||||
|
"date": "2026-08-26",
|
||||||
|
"size": 21365966,
|
||||||
|
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.2.27/gen1recomp++-0.2.27-ios.ipa",
|
||||||
|
"localizedDescription": "Download the correct version for your computer below.\n\n## Contributors\n\n- @1Jamie\n- @bryanthaboi\n- @ShaneMcGovernIE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.2.26",
|
||||||
|
"date": "2026-08-25",
|
||||||
|
"size": 21326715,
|
||||||
|
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.2.26/gen1recomp++-0.2.26-ios.ipa",
|
||||||
|
"localizedDescription": "Download the correct version for your computer below.\n\n## Issues closed\n\n- #917 Save editor still unable to save\n- #1074 Yellow ROM Importer may be broken\n- #1635 Missing FX jingle sounds for items\n- #1639 [Yellow] Summer Beach House colors in Advanced mode\n- #1644 Minor bug: Fly's screen fade is black instead of white\n- #1743 Super Nerd in Mt. Moon Wrong Dialogue\n\n## Contributors\n\n- @1Jamie\n- @bryanthaboi\n- @campavao\n- @HighDrexler"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.2.25",
|
||||||
|
"date": "2026-08-25",
|
||||||
|
"size": 21307139,
|
||||||
|
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.2.25/gen1recomp++-0.2.25-ios.ipa",
|
||||||
|
"localizedDescription": "Download the correct version for your computer below.\n\n## Issues closed\n\n- #941 Screen Size in Portrait\n- #1081 OS Slideshow\n- #1349 Modded Transparent Sprites getting cropped\n- #1602 Default on screen controls are different between RBY and Gold\n- #1618 [Silver] Potions [the item] are able to use rock smash as if they are Pokemon\n- #1624 gen 2 fullscreen\n- #1630 Gold missing attack feedback\n- #1633 #1488 Issue fixed, but a minor blemish remains.\n- #1661 bug: built in controls are missing.\n- #1669 gold item pc cannot hold two stacks of the same item id\n- #1676 Battle size: world\n- #1731 (Gold) 2 turn moves are broken\n- #1739 Town Map Sprite Icon dont have Color in Advanced Color Setting\n- #1740 Misaligned ! Box\n- #1742 Healing Screen\n- #1744 foresight does not allow normal type moves to hit ghosts\n- #1753 Pokémon show up in wide battle layout and use the incorrect colour palette\n- #1757 Potential deviations in game mechanics\n- #1758 Smog animation colors\n- #1760 \"Sent some money to mom\" cut off\n- #1761 Return / Frustration have incorrect base power\n- #1762 No incoming phone calls\n- #1763 [Gen2] Title Screen should restart after song ends\n- #1765 Battle animations all black when Flash isn't used in Rock Tunnel\n- #1770 \"Go!\" text visible when opening item menu\n- #1777 bug: Frame pacing micro stutter above 60Hz on steam deck oled\n- #1781 Missing Pokemon cries in Lavender Town\n- #1783 Order of items displayed is different from inventory when depositing\n- #1784 Pokemon cries not played in Celadon City\n- #1785 Wrong text order when picking up Eevee\n- #1787 Blackboard in Celadon Mansion is a little buggy\n- #1788 Cry of Pokemon that's about to evolve isn't played\n- #1789 Champion Blue doesn't force battle animations\n- #1790 Kris is red in battle screen\n- #1792 Violet City Bellsprout for Onix Trade Offers Bellsprout for Bellsprout\n- #1797 Pokedex diploma has minor visual errors\n- #1802 Weird persistant CPU usage spike after viewing mod list\n- #1803 A couple of issues in the secret house\n\n## Contributors\n\n- @1Jamie\n- @AverageConsumer\n- @bryanthaboi\n- @campavao\n- MaxTomahawk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.2.24",
|
||||||
|
"date": "2026-08-24",
|
||||||
|
"size": 14060528,
|
||||||
|
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.2.24/gen1recomp++-0.2.24-ios.ipa",
|
||||||
|
"localizedDescription": "Download the correct version for your computer below.\n\n## Contributors\n\n- @bryanthaboi"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "0.2.23",
|
"version": "0.2.23",
|
||||||
"date": "2026-08-24",
|
"date": "2026-08-24",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import UniformTypeIdentifiers
|
import UniformTypeIdentifiers
|
||||||
|
import CryptoKit
|
||||||
|
|
||||||
@objc(GRPickerBridge)
|
@objc(GRPickerBridge)
|
||||||
public final class GRPickerBridge: NSObject {
|
public final class GRPickerBridge: NSObject {
|
||||||
@@ -190,9 +191,17 @@ public final class GRPickerBridge: NSObject {
|
|||||||
@objc(presentPickerWithKind:saveDir:)
|
@objc(presentPickerWithKind:saveDir:)
|
||||||
public static func presentPicker(kind: UnsafePointer<CChar>?,
|
public static func presentPicker(kind: UnsafePointer<CChar>?,
|
||||||
saveDir: UnsafePointer<CChar>?) -> Bool {
|
saveDir: UnsafePointer<CChar>?) -> Bool {
|
||||||
|
return presentPicker(kind: kind, saveDir: saveDir, destination: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(presentPickerWithKind:saveDir:destination:)
|
||||||
|
public static func presentPicker(kind: UnsafePointer<CChar>?,
|
||||||
|
saveDir: UnsafePointer<CChar>?,
|
||||||
|
destination: UnsafePointer<CChar>?) -> Bool {
|
||||||
let kindStr = kind.map { String(cString: $0) } ?? "rom"
|
let kindStr = kind.map { String(cString: $0) } ?? "rom"
|
||||||
guard let dir = resolvedSaveDir(saveDir) else { return false }
|
guard let dir = resolvedSaveDir(saveDir) else { return false }
|
||||||
|
|
||||||
|
let requestedDestination = destination.map { String(cString: $0) }
|
||||||
let destName: String
|
let destName: String
|
||||||
var types: [UTType] = []
|
var types: [UTType] = []
|
||||||
switch kindStr {
|
switch kindStr {
|
||||||
@@ -202,7 +211,13 @@ public final class GRPickerBridge: NSObject {
|
|||||||
case "sav":
|
case "sav":
|
||||||
destName = "picked_save.sav"
|
destName = "picked_save.sav"
|
||||||
case "required_import":
|
case "required_import":
|
||||||
|
if let requestedDestination,
|
||||||
|
isDirectRequiredDestination(requestedDestination),
|
||||||
|
safeDestination(in: dir, relative: requestedDestination) != nil {
|
||||||
|
destName = requestedDestination
|
||||||
|
} else {
|
||||||
destName = "picked_required_import.bin"
|
destName = "picked_required_import.bin"
|
||||||
|
}
|
||||||
// A Nintendo 64 cartridge, for mods that build assets out of one --
|
// A Nintendo 64 cartridge, for mods that build assets out of one --
|
||||||
// the voxel mod's Pokemon Stadium battle models are the caller this
|
// the voxel mod's Pokemon Stadium battle models are the caller this
|
||||||
// was added for. Its own filename on purpose: an N64 ROM landing on
|
// was added for. Its own filename on purpose: an N64 ROM landing on
|
||||||
@@ -234,13 +249,19 @@ public final class GRPickerBridge: NSObject {
|
|||||||
types.append(.data)
|
types.append(.data)
|
||||||
if !types.contains(.item) { types.append(.item) }
|
if !types.contains(.item) { types.append(.item) }
|
||||||
|
|
||||||
|
let directRequired = kindStr == "required_import"
|
||||||
|
&& destName != "picked_required_import.bin"
|
||||||
let picker = UIDocumentPickerViewController(forOpeningContentTypes: types,
|
let picker = UIDocumentPickerViewController(forOpeningContentTypes: types,
|
||||||
asCopy: true)
|
asCopy: !directRequired)
|
||||||
picker.allowsMultipleSelection = false
|
picker.allowsMultipleSelection = false
|
||||||
let delegate = PickerDelegate { urls in
|
let delegate = PickerDelegate { urls in
|
||||||
guard let src = urls.first else { return }
|
guard let src = urls.first else { return }
|
||||||
|
if directRequired {
|
||||||
|
copyRequiredItemAsync(at: src, into: dir, relative: destName)
|
||||||
|
} else {
|
||||||
copyItem(at: src, into: dir, named: destName)
|
copyItem(at: src, into: dir, named: destName)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return present(picker, with: delegate)
|
return present(picker, with: delegate)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -410,6 +431,82 @@ public final class GRPickerBridge: NSObject {
|
|||||||
try? fm.moveItem(at: item, to: target)
|
try? fm.moveItem(at: item, to: target)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func isDirectRequiredDestination(_ relative: String) -> Bool {
|
||||||
|
let normalized = relative.replacingOccurrences(of: "\\", with: "/")
|
||||||
|
guard normalized.hasPrefix("mods/"),
|
||||||
|
let range = normalized.range(of: "/baseroms/"),
|
||||||
|
range.lowerBound > normalized.index(normalized.startIndex, offsetBy: 5),
|
||||||
|
range.upperBound < normalized.endIndex else { return false }
|
||||||
|
return !normalized.hasPrefix("/")
|
||||||
|
&& !normalized.contains("//")
|
||||||
|
&& !normalized.contains("/../")
|
||||||
|
&& !normalized.hasSuffix("/..")
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func safeDestination(in root: URL, relative: String) -> URL? {
|
||||||
|
guard isDirectRequiredDestination(relative) else { return nil }
|
||||||
|
let rootURL = root.standardizedFileURL
|
||||||
|
let candidate = rootURL.appendingPathComponent(relative).standardizedFileURL
|
||||||
|
let rootPath = rootURL.path.hasSuffix("/") ? rootURL.path : rootURL.path + "/"
|
||||||
|
guard candidate.path.hasPrefix(rootPath) else { return nil }
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func writeFlag(in dir: URL, name: String, body: String) {
|
||||||
|
try? body.data(using: .utf8)?.write(to: dir.appendingPathComponent(name),
|
||||||
|
options: .atomic)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func copyRequiredItemAsync(at src: URL, into dir: URL,
|
||||||
|
relative: String) {
|
||||||
|
DispatchQueue.global(qos: .userInitiated).async {
|
||||||
|
let scoped = src.startAccessingSecurityScopedResource()
|
||||||
|
defer { if scoped { src.stopAccessingSecurityScopedResource() } }
|
||||||
|
guard let dest = safeDestination(in: dir, relative: relative) else {
|
||||||
|
writeFlag(in: dir, name: "pick_error.flag", body: relative)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let fm = FileManager.default
|
||||||
|
ensureDirectory(dest.deletingLastPathComponent())
|
||||||
|
let partial = URL(fileURLWithPath: dest.path + ".part")
|
||||||
|
try? fm.removeItem(at: partial)
|
||||||
|
guard fm.createFile(atPath: partial.path, contents: nil) else {
|
||||||
|
writeFlag(in: dir, name: "pick_error.flag", body: relative)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasher = Insecure.MD5()
|
||||||
|
var total: UInt64 = 0
|
||||||
|
do {
|
||||||
|
let input = try FileHandle(forReadingFrom: src)
|
||||||
|
let output = try FileHandle(forWritingTo: partial)
|
||||||
|
defer {
|
||||||
|
try? input.close()
|
||||||
|
try? output.close()
|
||||||
|
}
|
||||||
|
while true {
|
||||||
|
let data = input.readData(ofLength: 1024 * 1024)
|
||||||
|
if data.isEmpty { break }
|
||||||
|
hasher.update(data: data)
|
||||||
|
output.write(data)
|
||||||
|
total += UInt64(data.count)
|
||||||
|
}
|
||||||
|
output.synchronizeFile()
|
||||||
|
try? fm.removeItem(at: dest)
|
||||||
|
try fm.moveItem(at: partial, to: dest)
|
||||||
|
let digest = hasher.finalize().map { String(format: "%02x", $0) }.joined()
|
||||||
|
let marker = "v1\n" + relative + "\n" + digest + "\n"
|
||||||
|
+ String(total) + "\n"
|
||||||
|
writeFlag(in: dir, name: "pick_complete.flag", body: marker)
|
||||||
|
NSLog("GRPickerBridge: direct required import delivered %llu bytes", total)
|
||||||
|
} catch {
|
||||||
|
try? fm.removeItem(at: partial)
|
||||||
|
NSLog("GRPickerBridge: direct required import failed: \(error)")
|
||||||
|
writeFlag(in: dir, name: "pick_error.flag", body: relative)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static func copyItem(at src: URL, into dir: URL, named name: String) {
|
private static func copyItem(at src: URL, into dir: URL, named name: String) {
|
||||||
let scoped = src.startAccessingSecurityScopedResource()
|
let scoped = src.startAccessingSecurityScopedResource()
|
||||||
defer { if scoped { src.stopAccessingSecurityScopedResource() } }
|
defer { if scoped { src.stopAccessingSecurityScopedResource() } }
|
||||||
@@ -426,7 +523,7 @@ public final class GRPickerBridge: NSObject {
|
|||||||
let report = "Could not copy \(src.lastPathComponent): " +
|
let report = "Could not copy \(src.lastPathComponent): " +
|
||||||
error.localizedDescription
|
error.localizedDescription
|
||||||
try? report.data(using: .utf8)?
|
try? report.data(using: .utf8)?
|
||||||
.write(to: dir.appendingPathComponent("pick_error.txt"))
|
.write(to: dir.appendingPathComponent("pick_error.flag"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -93,7 +93,20 @@ static int gr_callBridge(lua_State *L, const char *className,
|
|||||||
int w_pickFile(lua_State *L)
|
int w_pickFile(lua_State *L)
|
||||||
{
|
{
|
||||||
const char *kind = luaL_optstring(L, 1, "rom");
|
const char *kind = luaL_optstring(L, 1, "rom");
|
||||||
return gr_callBridge(L, "GRPickerBridge", "presentPickerWithKind:saveDir:", kind);
|
const char *destination = luaL_optstring(L, 2, nullptr);
|
||||||
|
Class cls = objc_getClass("GRPickerBridge");
|
||||||
|
if (cls == nullptr)
|
||||||
|
{
|
||||||
|
lua_pushboolean(L, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
typedef signed char (*GRPick)(Class, SEL, const char *, const char *,
|
||||||
|
const char *);
|
||||||
|
signed char ok = ((GRPick)objc_msgSend)(
|
||||||
|
cls, sel_registerName("presentPickerWithKind:saveDir:destination:"),
|
||||||
|
kind, gr_saveDirectory(), destination);
|
||||||
|
lua_pushboolean(L, ok != 0);
|
||||||
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// love.system.pickFileKinds() -> the comma-separated kinds supported by the
|
// love.system.pickFileKinds() -> the comma-separated kinds supported by the
|
||||||
|
|||||||
+68
-22
@@ -11,7 +11,7 @@
|
|||||||
#
|
#
|
||||||
# Output: dist/mac/gen1recomp-macos.zip
|
# Output: dist/mac/gen1recomp-macos.zip
|
||||||
# dist/win/gen1recomp-win64.zip
|
# dist/win/gen1recomp-win64.zip
|
||||||
# dist/linux/gen1recomp-linux.zip (fused x86_64 AppImage)
|
# dist/linux/gen1recomp-linux-x86_64.AppImage (fused x86_64 AppImage)
|
||||||
# dist/android/debug/*.apk (full gradle output stays under
|
# dist/android/debug/*.apk (full gradle output stays under
|
||||||
# mobile/android/app/build/outputs/apk/embedNoRecord/)
|
# mobile/android/app/build/outputs/apk/embedNoRecord/)
|
||||||
# dist/ios/<Config>-<sdk>/gen1recomp++.app (full xcodebuild output stays
|
# dist/ios/<Config>-<sdk>/gen1recomp++.app (full xcodebuild output stays
|
||||||
@@ -170,27 +170,53 @@ make_ico() { # $1 = output .ico path
|
|||||||
# --------------------------------------------------------------- macOS
|
# --------------------------------------------------------------- macOS
|
||||||
# ShaderFX's librashader bridge. Only the CONVERT action needs it, so a build
|
# ShaderFX's librashader bridge. Only the CONVERT action needs it, so a build
|
||||||
# without it still runs presets that were converted elsewhere.
|
# without it still runs presets that were converted elsewhere.
|
||||||
# SHADERFX_BRIDGE points at a prebuilt library; otherwise cargo builds it.
|
# SHADERFX_BRIDGE_<PLAT> or dist/native/<plat>/ points at a prebuilt library;
|
||||||
|
# otherwise cargo builds it, host platform only.
|
||||||
|
shader_bridge_host_plat() {
|
||||||
|
case "$(uname -s)-$(uname -m)" in
|
||||||
|
Darwin-*) printf 'mac' ;;
|
||||||
|
Linux-x86_64) printf 'linux-x64' ;;
|
||||||
|
Linux-aarch64|Linux-arm64) printf 'linux-arm64' ;;
|
||||||
|
*) printf '' ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
bundle_shader_bridge() {
|
bundle_shader_bridge() {
|
||||||
local dest="$1" name="$2"
|
local dest="$1" name="$2" plat="$3"
|
||||||
local src="${SHADERFX_BRIDGE:-}"
|
|
||||||
local crate="$ROOT/tools/shaderfx-bridge"
|
local crate="$ROOT/tools/shaderfx-bridge"
|
||||||
if [ -z "$src" ] && [ -f "$crate/target/release/$name" ]; then
|
local src="" var
|
||||||
src="$crate/target/release/$name"
|
[ -n "$plat" ] || fail "bundle_shader_bridge: no platform key for $name"
|
||||||
|
|
||||||
|
var="SHADERFX_BRIDGE_$(printf '%s' "$plat" | tr 'a-z-' 'A-Z_')"
|
||||||
|
eval "src=\${$var:-}"
|
||||||
|
|
||||||
|
if [ -z "$src" ] && [ -f "$DIST/native/$plat/$name" ]; then
|
||||||
|
src="$DIST/native/$plat/$name"
|
||||||
fi
|
fi
|
||||||
if [ -z "$src" ] && command -v cargo >/dev/null 2>&1; then
|
if [ -z "$src" ] && [ -n "${SHADERFX_BRIDGE:-}" ]; then
|
||||||
|
src="$SHADERFX_BRIDGE"
|
||||||
|
fi
|
||||||
|
if [ -z "$src" ] && [ "$plat" = "$(shader_bridge_host_plat)" ]; then
|
||||||
|
if [ -f "$crate/target/release/$name" ]; then
|
||||||
|
src="$crate/target/release/$name"
|
||||||
|
elif command -v cargo >/dev/null 2>&1; then
|
||||||
say "building the ShaderFX bridge with cargo"
|
say "building the ShaderFX bridge with cargo"
|
||||||
if (cd "$crate" && cargo build --release >/dev/null 2>&1); then
|
if (cd "$crate" && cargo build --release >/dev/null 2>&1); then
|
||||||
src="$crate/target/release/$name"
|
src="$crate/target/release/$name"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
if [ -n "$src" ] && [ -f "$src" ]; then
|
if [ -n "$src" ] && [ -f "$src" ]; then
|
||||||
mkdir -p "$dest"
|
mkdir -p "$dest"
|
||||||
cp "$src" "$dest/$name"
|
cp "$src" "$dest/$name"
|
||||||
say "bundled $name for SHADER FX preset conversion"
|
say "bundled $name for SHADER FX preset conversion ($plat)"
|
||||||
else
|
return 0
|
||||||
warn "$name not found: this build can run converted presets but not CONVERT new ones (set SHADERFX_BRIDGE or install cargo)"
|
|
||||||
fi
|
fi
|
||||||
|
if [ "${SHADERFX_BRIDGE_REQUIRED:-}" = "1" ]; then
|
||||||
|
fail "$name ($plat) not found: set $var or stage it at dist/native/$plat/$name"
|
||||||
|
fi
|
||||||
|
warn "$name not found: this build can run converted presets but not CONVERT new ones (set $var or install cargo)"
|
||||||
}
|
}
|
||||||
|
|
||||||
build_mac() {
|
build_mac() {
|
||||||
@@ -205,7 +231,7 @@ build_mac() {
|
|||||||
# drop any bundled placeholder .love and fuse ours in
|
# drop any bundled placeholder .love and fuse ours in
|
||||||
find "$out_app/Contents/Resources" -maxdepth 1 -name '*.love' -delete
|
find "$out_app/Contents/Resources" -maxdepth 1 -name '*.love' -delete
|
||||||
cp "$LOVE_FILE" "$out_app/Contents/Resources/game.love"
|
cp "$LOVE_FILE" "$out_app/Contents/Resources/game.love"
|
||||||
bundle_shader_bridge "$out_app/Contents/MacOS" "liblibrashader_bridge.dylib"
|
bundle_shader_bridge "$out_app/Contents/MacOS" "liblibrashader_bridge.dylib" mac
|
||||||
|
|
||||||
local plist="$out_app/Contents/Info.plist"
|
local plist="$out_app/Contents/Info.plist"
|
||||||
/usr/libexec/PlistBuddy -c "Set :CFBundleName $APP_NAME" "$plist" 2>/dev/null \
|
/usr/libexec/PlistBuddy -c "Set :CFBundleName $APP_NAME" "$plist" 2>/dev/null \
|
||||||
@@ -322,7 +348,7 @@ build_win() {
|
|||||||
warn "gen1tls.dll not found: Windows zip will not support wss:// (set GEN1TLS_DLL or build native/tls_dial)"
|
warn "gen1tls.dll not found: Windows zip will not support wss:// (set GEN1TLS_DLL or build native/tls_dial)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
bundle_shader_bridge "$out_dir" "librashader_bridge.dll"
|
bundle_shader_bridge "$out_dir" "librashader_bridge.dll" win-x64
|
||||||
|
|
||||||
# The exe's icon lives in love.exe's PE resources, so it must be patched
|
# The exe's icon lives in love.exe's PE resources, so it must be patched
|
||||||
# BEFORE the .love is appended: peresed rewrites the whole file and would
|
# BEFORE the .love is appended: peresed rewrites the whole file and would
|
||||||
@@ -419,7 +445,7 @@ build_linux() {
|
|||||||
unsquashfs -q -no-xattrs -o "$sfs_offset" -d "$appdir" "$love_appimage" >/dev/null
|
unsquashfs -q -no-xattrs -o "$sfs_offset" -d "$appdir" "$love_appimage" >/dev/null
|
||||||
|
|
||||||
cp "$LOVE_FILE" "$appdir/game.love"
|
cp "$LOVE_FILE" "$appdir/game.love"
|
||||||
bundle_shader_bridge "$appdir" "liblibrashader_bridge.so"
|
bundle_shader_bridge "$appdir" "liblibrashader_bridge.so" linux-x64
|
||||||
|
|
||||||
# Replace LÖVE's own desktop entry rather than keeping it: it says
|
# Replace LÖVE's own desktop entry rather than keeping it: it says
|
||||||
# Name=LÖVE / Icon=love, which is what appimaged, app menus and file
|
# Name=LÖVE / Icon=love, which is what appimaged, app menus and file
|
||||||
@@ -457,15 +483,31 @@ EOF
|
|||||||
# file-manager thumbnailers show for the file itself.
|
# file-manager thumbnailers show for the file itself.
|
||||||
[ -f "$ICON_SRC" ] || fail "missing icon source: $ICON_SRC"
|
[ -f "$ICON_SRC" ] || fail "missing icon source: $ICON_SRC"
|
||||||
rm -f "$appdir/love.svg" "$appdir/love.png" "$appdir/.DirIcon"
|
rm -f "$appdir/love.svg" "$appdir/love.png" "$appdir/.DirIcon"
|
||||||
|
if command -v sips >/dev/null 2>&1; then
|
||||||
sips -z 512 512 "$ICON_SRC" --out "$appdir/$APP_NAME.png" >/dev/null
|
sips -z 512 512 "$ICON_SRC" --out "$appdir/$APP_NAME.png" >/dev/null
|
||||||
|
elif command -v convert >/dev/null 2>&1; then
|
||||||
|
convert "$ICON_SRC" -resize 512x512 "$appdir/$APP_NAME.png"
|
||||||
|
else
|
||||||
|
fail "need sips (macOS) or ImageMagick convert to resize $ICON_SRC"
|
||||||
|
fi
|
||||||
cp "$appdir/$APP_NAME.png" "$appdir/.DirIcon"
|
cp "$appdir/$APP_NAME.png" "$appdir/.DirIcon"
|
||||||
|
|
||||||
|
# sed -i '' is BSD; GNU sed wants sed -i (no empty backup suffix).
|
||||||
|
if sed --version >/dev/null 2>&1; then
|
||||||
|
sed -i 's|^#FUSE_PATH="$APPDIR/my_game.love"$|FUSE_PATH="$APPDIR/game.love"|' "$appdir/AppRun"
|
||||||
|
else
|
||||||
sed -i '' 's|^#FUSE_PATH="$APPDIR/my_game.love"$|FUSE_PATH="$APPDIR/game.love"|' "$appdir/AppRun"
|
sed -i '' 's|^#FUSE_PATH="$APPDIR/my_game.love"$|FUSE_PATH="$APPDIR/game.love"|' "$appdir/AppRun"
|
||||||
|
fi
|
||||||
grep -q '^FUSE_PATH="\$APPDIR/game.love"$' "$appdir/AppRun" \
|
grep -q '^FUSE_PATH="\$APPDIR/game.love"$' "$appdir/AppRun" \
|
||||||
|| fail "failed to enable FUSE_PATH in AppRun (upstream AppRun changed?)"
|
|| fail "failed to enable FUSE_PATH in AppRun (upstream AppRun changed?)"
|
||||||
|
|
||||||
sed -i '' 's|^exec "\$APPDIR/bin/love"|if [ -n "$WAYLAND_DISPLAY" ] \&\& [ -z "$SDL_VIDEODRIVER" ]; then export SDL_VIDEODRIVER=x11; fi\
|
local wayland_hook='if [ -n "$WAYLAND_DISPLAY" ] && [ -z "$SDL_VIDEODRIVER" ]; then export SDL_VIDEODRIVER=x11; fi\
|
||||||
exec "$APPDIR/bin/love"|' "$appdir/AppRun"
|
exec "$APPDIR/bin/love"'
|
||||||
|
if sed --version >/dev/null 2>&1; then
|
||||||
|
sed -i "s|^exec \"\$APPDIR/bin/love\"|$wayland_hook|" "$appdir/AppRun"
|
||||||
|
else
|
||||||
|
sed -i '' "s|^exec \"\$APPDIR/bin/love\"|$wayland_hook|" "$appdir/AppRun"
|
||||||
|
fi
|
||||||
|
|
||||||
# Match the upstream image's compression (gzip, 128K blocks) so the
|
# Match the upstream image's compression (gzip, 128K blocks) so the
|
||||||
# bundled runtime can read it.
|
# bundled runtime can read it.
|
||||||
@@ -474,16 +516,20 @@ exec "$APPDIR/bin/love"|' "$appdir/AppRun"
|
|||||||
mksquashfs "$appdir" "$sfs_out" \
|
mksquashfs "$appdir" "$sfs_out" \
|
||||||
-comp gzip -b 131072 -noappend -all-root -no-xattrs -quiet >/dev/null
|
-comp gzip -b 131072 -noappend -all-root -no-xattrs -quiet >/dev/null
|
||||||
|
|
||||||
local out_bin="$WORK/$APP_NAME-x86_64.AppImage"
|
mkdir -p "$DIST/linux"
|
||||||
rm -f "$out_bin"
|
local out_bin="$DIST/linux/$APP_NAME-linux-x86_64.AppImage"
|
||||||
|
rm -f "$out_bin" "$out_bin.sha256"
|
||||||
head -c "$sfs_offset" "$love_appimage" > "$out_bin"
|
head -c "$sfs_offset" "$love_appimage" > "$out_bin"
|
||||||
cat "$sfs_out" >> "$out_bin"
|
cat "$sfs_out" >> "$out_bin"
|
||||||
chmod +x "$out_bin"
|
chmod +x "$out_bin"
|
||||||
|
if command -v sha256sum >/dev/null 2>&1; then
|
||||||
local zip_out="$DIST/linux/$APP_NAME-linux.zip"
|
printf '%s %s\n' "$(sha256sum "$out_bin" | awk '{print $1}')" "$(basename "$out_bin")" \
|
||||||
rm -f "$zip_out"
|
> "$out_bin.sha256"
|
||||||
(cd "$WORK" && zip -q -9 -j "$zip_out" "$(basename "$out_bin")")
|
else
|
||||||
say "Linux build: $zip_out"
|
printf '%s %s\n' "$(shasum -a 256 "$out_bin" | awk '{print $1}')" "$(basename "$out_bin")" \
|
||||||
|
> "$out_bin.sha256"
|
||||||
|
fi
|
||||||
|
say "Linux build: $out_bin"
|
||||||
}
|
}
|
||||||
|
|
||||||
# --------------------------------------------------------------- Android
|
# --------------------------------------------------------------- Android
|
||||||
|
|||||||
@@ -106,6 +106,13 @@ if [ -n "$TEST_APPLICATION_ID" ]; then
|
|||||||
APPLICATION_ID="$TEST_APPLICATION_ID"
|
APPLICATION_ID="$TEST_APPLICATION_ID"
|
||||||
APP_NAME="$APP_NAME (test)"
|
APP_NAME="$APP_NAME (test)"
|
||||||
fi
|
fi
|
||||||
|
# Optional overrides for side-by-side test APKs (never used by CI shipping builds).
|
||||||
|
if [ -n "${GEN1RECOMP_ANDROID_APPLICATION_ID:-}" ]; then
|
||||||
|
APPLICATION_ID="$GEN1RECOMP_ANDROID_APPLICATION_ID"
|
||||||
|
fi
|
||||||
|
if [ -n "${GEN1RECOMP_ANDROID_APP_NAME:-}" ]; then
|
||||||
|
APP_NAME="$GEN1RECOMP_ANDROID_APP_NAME"
|
||||||
|
fi
|
||||||
|
|
||||||
# --------------------------------------------------------------- preconditions
|
# --------------------------------------------------------------- preconditions
|
||||||
if [ ! -f "$ANDROID_DIR/settings.gradle" ] || [ ! -f "$ANDROID_DIR/gradlew" ]; then
|
if [ ! -f "$ANDROID_DIR/settings.gradle" ] || [ ! -f "$ANDROID_DIR/gradlew" ]; then
|
||||||
@@ -461,6 +468,115 @@ pack_game_love() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --------------------------------------------------------------- ShaderFX bridge
|
||||||
|
SHADER_BRIDGE_LIB="liblibrashader_bridge.so"
|
||||||
|
SHADER_BRIDGE_ABIS="arm64-v8a armeabi-v7a"
|
||||||
|
|
||||||
|
shader_bridge_rust_target() {
|
||||||
|
case "$1" in
|
||||||
|
arm64-v8a) printf 'aarch64-linux-android' ;;
|
||||||
|
armeabi-v7a) printf 'armv7-linux-androideabi' ;;
|
||||||
|
x86_64) printf 'x86_64-linux-android' ;;
|
||||||
|
x86) printf 'i686-linux-android' ;;
|
||||||
|
*) printf '' ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
shader_bridge_staged_count() {
|
||||||
|
local jni="$1" abi count=0
|
||||||
|
for abi in $SHADER_BRIDGE_ABIS; do
|
||||||
|
[ -f "$jni/$abi/$SHADER_BRIDGE_LIB" ] && count=$((count + 1))
|
||||||
|
done
|
||||||
|
printf '%s' "$count"
|
||||||
|
}
|
||||||
|
|
||||||
|
bundle_shader_bridge_android() {
|
||||||
|
local jni="$ANDROID_DIR/app/src/main/jniLibs"
|
||||||
|
local crate="$ROOT/tools/shaderfx-bridge"
|
||||||
|
local abi target
|
||||||
|
|
||||||
|
for abi in $SHADER_BRIDGE_ABIS; do
|
||||||
|
rm -f "$jni/$abi/$SHADER_BRIDGE_LIB"
|
||||||
|
done
|
||||||
|
|
||||||
|
local prebuilt="${SHADERFX_BRIDGE_ANDROID_DIR:-}"
|
||||||
|
if [ -n "$prebuilt" ]; then
|
||||||
|
for abi in $SHADER_BRIDGE_ABIS; do
|
||||||
|
if [ -f "$prebuilt/$abi/$SHADER_BRIDGE_LIB" ]; then
|
||||||
|
mkdir -p "$jni/$abi"
|
||||||
|
cp "$prebuilt/$abi/$SHADER_BRIDGE_LIB" "$jni/$abi/$SHADER_BRIDGE_LIB"
|
||||||
|
else
|
||||||
|
warn "SHADERFX_BRIDGE_ANDROID_DIR has no $abi/$SHADER_BRIDGE_LIB"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ "$(shader_bridge_staged_count "$jni")" -gt 0 ]; then
|
||||||
|
say "bundled $SHADER_BRIDGE_LIB for SHADER FX preset conversion (prebuilt)"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$crate/Cargo.toml" ]; then
|
||||||
|
warn "$SHADER_BRIDGE_LIB not found: this build can run converted presets but not CONVERT new ones (tools/shaderfx-bridge is missing)"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v cargo >/dev/null 2>&1 || ! cargo ndk --version >/dev/null 2>&1; then
|
||||||
|
warn "$SHADER_BRIDGE_LIB not found: this build can run converted presets but not CONVERT new ones (set SHADERFX_BRIDGE_ANDROID_DIR or run 'cargo install cargo-ndk')"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local ndk="${ANDROID_NDK_HOME:-}"
|
||||||
|
if [ -z "$ndk" ] || [ ! -d "$ndk" ]; then
|
||||||
|
ndk="${ANDROID_SDK_ROOT:-${ANDROID_HOME:-$HOME/Library/Android/sdk}}/ndk/$NDK_VERSION"
|
||||||
|
fi
|
||||||
|
if [ ! -d "$ndk" ]; then
|
||||||
|
warn "$SHADER_BRIDGE_LIB not built: NDK $NDK_VERSION not found (set ANDROID_NDK_HOME)"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local installed missing="" buildable=""
|
||||||
|
installed="$(rustup target list --installed 2>/dev/null || true)"
|
||||||
|
for abi in $SHADER_BRIDGE_ABIS; do
|
||||||
|
target="$(shader_bridge_rust_target "$abi")"
|
||||||
|
if grep -qx "$target" <<< "$installed"; then
|
||||||
|
buildable="$buildable $abi"
|
||||||
|
else
|
||||||
|
missing="$missing $target"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ -n "$missing" ]; then
|
||||||
|
warn "$SHADER_BRIDGE_LIB: skipping$missing. Run: rustup target add$missing"
|
||||||
|
fi
|
||||||
|
if [ -z "$buildable" ]; then
|
||||||
|
warn "$SHADER_BRIDGE_LIB not built: this build can run converted presets but not CONVERT new ones (no Android Rust targets installed)"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local args=()
|
||||||
|
for abi in $buildable; do
|
||||||
|
args+=(-t "$abi")
|
||||||
|
done
|
||||||
|
|
||||||
|
say "building the ShaderFX bridge with cargo-ndk (${buildable# })"
|
||||||
|
mkdir -p "$jni"
|
||||||
|
if ! (
|
||||||
|
cd "$crate"
|
||||||
|
export ANDROID_NDK_HOME="$ndk"
|
||||||
|
export ANDROID_NDK_ROOT="$ndk"
|
||||||
|
export CARGO_PROFILE_RELEASE_STRIP="symbols"
|
||||||
|
cargo ndk "${args[@]}" -o "$jni" build --release
|
||||||
|
); then
|
||||||
|
warn "$SHADER_BRIDGE_LIB failed to cross-compile: this build can run converted presets but not CONVERT new ones"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$(shader_bridge_staged_count "$jni")" -gt 0 ]; then
|
||||||
|
say "bundled $SHADER_BRIDGE_LIB for SHADER FX preset conversion"
|
||||||
|
else
|
||||||
|
warn "$SHADER_BRIDGE_LIB not found after cargo-ndk: this build can run converted presets but not CONVERT new ones"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# --------------------------------------------------------------- SDK check
|
# --------------------------------------------------------------- SDK check
|
||||||
require_android_sdk() {
|
require_android_sdk() {
|
||||||
local sdk="${ANDROID_SDK_ROOT:-${ANDROID_HOME:-}}"
|
local sdk="${ANDROID_SDK_ROOT:-${ANDROID_HOME:-}}"
|
||||||
@@ -575,5 +691,6 @@ if $PACKAGE_ONLY; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
require_android_sdk
|
require_android_sdk
|
||||||
|
bundle_shader_bridge_android
|
||||||
run_gradle
|
run_gradle
|
||||||
say "done"
|
say "done"
|
||||||
|
|||||||
Executable
+143
@@ -0,0 +1,143 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Build a single-file Flatpak bundle for x86_64 desktop Linux.
|
||||||
|
#
|
||||||
|
# Usage: scripts/build_flatpak.sh [--version X.Y.Z] [--game-love PATH]
|
||||||
|
#
|
||||||
|
# Output:
|
||||||
|
# dist/flatpak/gen1recomp-<version>-linux.flatpak
|
||||||
|
#
|
||||||
|
# Requires: flatpak, flatpak-builder, and network on first run (Freedesktop
|
||||||
|
# runtime + LÖVE AppImage download).
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
APP_ID="com.theboisclub.gen1recomp"
|
||||||
|
VERSION="0.0.0"
|
||||||
|
GAME_LOVE_IN=""
|
||||||
|
DIST="$ROOT/dist/flatpak"
|
||||||
|
BUILD_DIR="$ROOT/.bazinga/flatpak-build"
|
||||||
|
REPO_DIR="$ROOT/.bazinga/flatpak-repo"
|
||||||
|
STATE_DIR="$ROOT/.bazinga/flatpak-state"
|
||||||
|
CACHE="$ROOT/.bazinga/flatpak-cache"
|
||||||
|
|
||||||
|
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 ;;
|
||||||
|
--game-love) GAME_LOVE_IN="$2"; shift ;;
|
||||||
|
-h|--help)
|
||||||
|
sed -n '2,12p' "$0"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*) fail "unknown argument: $1" ;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
if ! printf '%s' "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||||||
|
fail "invalid --version '$VERSION' (expected X.Y.Z)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
command -v flatpak >/dev/null || fail "flatpak not found"
|
||||||
|
command -v flatpak-builder >/dev/null || fail "flatpak-builder not found"
|
||||||
|
|
||||||
|
mkdir -p "$DIST" "$BUILD_DIR" "$REPO_DIR" "$STATE_DIR" "$CACHE"
|
||||||
|
|
||||||
|
LOVE_FILE="$CACHE/game.love"
|
||||||
|
if [ -n "$GAME_LOVE_IN" ]; then
|
||||||
|
[ -f "$GAME_LOVE_IN" ] || fail "game.love not found: $GAME_LOVE_IN"
|
||||||
|
if [ "$(readlink -f "$GAME_LOVE_IN")" != "$(readlink -f "$LOVE_FILE")" ]; then
|
||||||
|
cp "$GAME_LOVE_IN" "$LOVE_FILE"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
say "packing game.love"
|
||||||
|
"$ROOT/scripts/pack_love.sh" --output "$LOVE_FILE" --version "$VERSION"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stage local sources next to the generated manifest (flatpak path: is relative).
|
||||||
|
cp "$ROOT/flatpak/$APP_ID.desktop" "$CACHE/$APP_ID.desktop"
|
||||||
|
# AppStream rejects icons larger than the declared hicolor size.
|
||||||
|
if command -v magick >/dev/null 2>&1; then
|
||||||
|
magick "$ROOT/assets/logo/gen1recomp_cover.png" -resize 512x512 "$CACHE/icon.png"
|
||||||
|
elif command -v convert >/dev/null 2>&1; then
|
||||||
|
convert "$ROOT/assets/logo/gen1recomp_cover.png" -resize 512x512 "$CACHE/icon.png"
|
||||||
|
else
|
||||||
|
fail "need ImageMagick (magick/convert) to resize the Flatpak icon to 512x512"
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
DATE="$(date -u +%Y-%m-%d)"
|
||||||
|
python3 - "$ROOT/flatpak/$APP_ID.metainfo.xml" "$CACHE/$APP_ID.metainfo.xml" "$VERSION" "$DATE" <<'PY'
|
||||||
|
import sys, pathlib, re
|
||||||
|
src, dst, ver, date = sys.argv[1:5]
|
||||||
|
text = pathlib.Path(src).read_text(encoding="utf-8")
|
||||||
|
release = f' <release version="{ver}" date="{date}"/>'
|
||||||
|
text = re.sub(
|
||||||
|
r"<releases>.*?</releases>",
|
||||||
|
"<releases>\n" + release + "\n </releases>",
|
||||||
|
text,
|
||||||
|
count=1,
|
||||||
|
flags=re.S,
|
||||||
|
)
|
||||||
|
pathlib.Path(dst).write_text(text, encoding="utf-8")
|
||||||
|
PY
|
||||||
|
|
||||||
|
BRIDGE_LIB="liblibrashader_bridge.so"
|
||||||
|
BRIDGE_SRC="${SHADERFX_BRIDGE_LINUX_X64:-}"
|
||||||
|
if [ -z "$BRIDGE_SRC" ] && [ -f "$ROOT/dist/native/linux-x64/$BRIDGE_LIB" ]; then
|
||||||
|
BRIDGE_SRC="$ROOT/dist/native/linux-x64/$BRIDGE_LIB"
|
||||||
|
fi
|
||||||
|
if [ -z "$BRIDGE_SRC" ] && [ -f "$ROOT/tools/shaderfx-bridge/target/release/$BRIDGE_LIB" ]; then
|
||||||
|
BRIDGE_SRC="$ROOT/tools/shaderfx-bridge/target/release/$BRIDGE_LIB"
|
||||||
|
fi
|
||||||
|
rm -f "$CACHE/$BRIDGE_LIB"
|
||||||
|
if [ -n "$BRIDGE_SRC" ] && [ -f "$BRIDGE_SRC" ]; then
|
||||||
|
cp "$BRIDGE_SRC" "$CACHE/$BRIDGE_LIB"
|
||||||
|
cp "$ROOT/flatpak/$APP_ID.yml" "$CACHE/$APP_ID.yml"
|
||||||
|
say "staged $BRIDGE_LIB for SHADER FX preset conversion"
|
||||||
|
elif [ "${SHADERFX_BRIDGE_REQUIRED:-}" = "1" ]; then
|
||||||
|
fail "$BRIDGE_LIB not found: set SHADERFX_BRIDGE_LINUX_X64 or stage it at dist/native/linux-x64/$BRIDGE_LIB"
|
||||||
|
else
|
||||||
|
warn "$BRIDGE_LIB not found: this bundle can run converted presets but not CONVERT new ones"
|
||||||
|
sed '/# BRIDGE-BEGIN/,/# BRIDGE-END/d' "$ROOT/flatpak/$APP_ID.yml" > "$CACHE/$APP_ID.yml"
|
||||||
|
fi
|
||||||
|
|
||||||
|
say "installing Freedesktop runtime (no-op if present)"
|
||||||
|
flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo || true
|
||||||
|
flatpak --user install -y flathub \
|
||||||
|
org.freedesktop.Platform//24.08 \
|
||||||
|
org.freedesktop.Sdk//24.08 \
|
||||||
|
|| warn "runtime install reported an error; flatpak-builder may still reuse cache"
|
||||||
|
|
||||||
|
say "building Flatpak ($APP_ID $VERSION)"
|
||||||
|
# Run from CACHE so path: sources resolve.
|
||||||
|
(
|
||||||
|
cd "$CACHE"
|
||||||
|
flatpak-builder --user --force-clean \
|
||||||
|
--state-dir="$STATE_DIR" \
|
||||||
|
--repo="$REPO_DIR" \
|
||||||
|
"$BUILD_DIR" "$APP_ID.yml"
|
||||||
|
)
|
||||||
|
|
||||||
|
OUT="$DIST/gen1recomp-${VERSION}-linux.flatpak"
|
||||||
|
rm -f "$OUT"
|
||||||
|
say "exporting bundle $OUT"
|
||||||
|
flatpak build-bundle "$REPO_DIR" "$OUT" "$APP_ID" \
|
||||||
|
--runtime-repo=https://flathub.org/repo/flathub.flatpakrepo
|
||||||
|
chmod 644 "$OUT"
|
||||||
|
if command -v sha256sum >/dev/null 2>&1; then
|
||||||
|
printf '%s %s\n' "$(sha256sum "$OUT" | awk '{print $1}')" "$(basename "$OUT")" > "$OUT.sha256"
|
||||||
|
else
|
||||||
|
printf '%s %s\n' "$(shasum -a 256 "$OUT" | awk '{print $1}')" "$(basename "$OUT")" > "$OUT.sha256"
|
||||||
|
fi
|
||||||
|
if [ -f "$CACHE/$BRIDGE_LIB" ]; then
|
||||||
|
[ -f "$BUILD_DIR/files/share/gen1recomp/$BRIDGE_LIB" ] \
|
||||||
|
|| fail "$BRIDGE_LIB was staged but is missing from the built /app/share/gen1recomp"
|
||||||
|
say "verified $BRIDGE_LIB in the Flatpak"
|
||||||
|
fi
|
||||||
|
say "Flatpak build: $OUT ($(du -h "$OUT" | cut -f1))"
|
||||||
|
say "sha256: $(cut -d' ' -f1 "$OUT.sha256")"
|
||||||
+148
-2
@@ -615,6 +615,138 @@ verify_game_payload() {
|
|||||||
say "game.love present ($(du -h "$app/game.love" | cut -f1))"
|
say "game.love present ($(du -h "$app/game.love" | cut -f1))"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SHADER_BRIDGE_LIB=""
|
||||||
|
SHADER_BRIDGE_NAME="liblibrashader_bridge.a"
|
||||||
|
SHADER_BRIDGE_OBJ="librashader_bridge.o"
|
||||||
|
|
||||||
|
SHADER_BRIDGE_ARCHS=""
|
||||||
|
|
||||||
|
rust_targets_for_sdk() {
|
||||||
|
if [ "$1" = "iphoneos" ]; then
|
||||||
|
printf 'aarch64-apple-ios'
|
||||||
|
else
|
||||||
|
printf 'aarch64-apple-ios-sim x86_64-apple-ios'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
build_shader_bridge_slice() {
|
||||||
|
local rust_target="$1"
|
||||||
|
local crate="$ROOT/tools/shaderfx-bridge"
|
||||||
|
local built="$crate/target/$rust_target/release/$SHADER_BRIDGE_NAME"
|
||||||
|
if [ -f "$built" ]; then
|
||||||
|
printf '%s' "$built"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if command -v cargo >/dev/null 2>&1 \
|
||||||
|
&& rustup target list --installed 2>/dev/null \
|
||||||
|
| grep -x "$rust_target" >/dev/null; then
|
||||||
|
say "building the ShaderFX bridge with cargo ($rust_target)" >&2
|
||||||
|
if (cd "$crate" && IPHONEOS_DEPLOYMENT_TARGET=15.0 \
|
||||||
|
cargo build --release --target "$rust_target" >/dev/null 2>&1); then
|
||||||
|
printf '%s' "$built"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
prelink_shader_bridge_slice() {
|
||||||
|
local archive="$1" sdk="$2" out="$3"
|
||||||
|
local arch platform sdk_version syms objcopy tmp
|
||||||
|
arch="$(lipo -archs "$archive" 2>/dev/null | awk '{print $1}')"
|
||||||
|
[ -n "$arch" ] || return 1
|
||||||
|
if [ "$sdk" = "iphoneos" ]; then platform="ios"; else platform="ios-simulator"; fi
|
||||||
|
sdk_version="$(xcrun --sdk "$sdk" --show-sdk-version 2>/dev/null)"
|
||||||
|
[ -n "$sdk_version" ] || return 1
|
||||||
|
syms="$LIBS_DIR/librashader_bridge.exports"
|
||||||
|
printf '_librashader_translate_preset\n_librashader_free_string\n' > "$syms"
|
||||||
|
tmp="$out.tmp"
|
||||||
|
xcrun ld -r -arch "$arch" -platform_version "$platform" 15.0 "$sdk_version" \
|
||||||
|
-all_load -exported_symbols_list "$syms" -o "$tmp" "$archive" || return 1
|
||||||
|
objcopy="$(ls "$(rustc --print sysroot 2>/dev/null)"/lib/rustlib/*/bin/rust-objcopy 2>/dev/null | head -1)"
|
||||||
|
if [ -n "$objcopy" ] && "$objcopy" --remove-section __LLVM,__bitcode \
|
||||||
|
--remove-section __LLVM,__cmdline "$tmp" "$out" 2>/dev/null; then
|
||||||
|
rm -f "$tmp"
|
||||||
|
else
|
||||||
|
mv "$tmp" "$out"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
bundle_shader_bridge_ios() {
|
||||||
|
local rust_targets="$1" sdk="$2"
|
||||||
|
local src="${SHADERFX_BRIDGE_IOS:-}"
|
||||||
|
local slices=() objs=() target slice obj i
|
||||||
|
SHADER_BRIDGE_LIB=""
|
||||||
|
SHADER_BRIDGE_ARCHS=""
|
||||||
|
rm -f "$LIBS_DIR/$SHADER_BRIDGE_NAME" "$LIBS_DIR/$SHADER_BRIDGE_OBJ" "$LIBS_DIR"/librashader_bridge.*.o
|
||||||
|
if [ -n "$src" ]; then
|
||||||
|
if [ -f "$src" ]; then
|
||||||
|
slices+=("$src")
|
||||||
|
else
|
||||||
|
warn "SHADERFX_BRIDGE_IOS=$src does not exist"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
for target in $rust_targets; do
|
||||||
|
if slice="$(build_shader_bridge_slice "$target")"; then
|
||||||
|
slices+=("$slice")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
if [ "${#slices[@]}" -gt 0 ]; then
|
||||||
|
mkdir -p "$LIBS_DIR"
|
||||||
|
i=0
|
||||||
|
for slice in "${slices[@]}"; do
|
||||||
|
i=$((i + 1))
|
||||||
|
obj="$LIBS_DIR/librashader_bridge.$i.o"
|
||||||
|
if prelink_shader_bridge_slice "$slice" "$sdk" "$obj"; then
|
||||||
|
objs+=("$obj")
|
||||||
|
else
|
||||||
|
warn "could not prelink $(basename "$slice") for $sdk"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
if [ "${#objs[@]}" -eq 1 ]; then
|
||||||
|
mv "${objs[0]}" "$LIBS_DIR/$SHADER_BRIDGE_OBJ"
|
||||||
|
elif [ "${#objs[@]}" -gt 1 ]; then
|
||||||
|
lipo -create "${objs[@]}" -output "$LIBS_DIR/$SHADER_BRIDGE_OBJ"
|
||||||
|
rm -f "${objs[@]}"
|
||||||
|
fi
|
||||||
|
if [ -f "$LIBS_DIR/$SHADER_BRIDGE_OBJ" ]; then
|
||||||
|
SHADER_BRIDGE_LIB="$LIBS_DIR/$SHADER_BRIDGE_OBJ"
|
||||||
|
SHADER_BRIDGE_ARCHS="$(lipo -archs "$SHADER_BRIDGE_LIB" 2>/dev/null || true)"
|
||||||
|
say "linking $SHADER_BRIDGE_OBJ for SHADER FX preset conversion (${SHADER_BRIDGE_ARCHS:-unknown arch})"
|
||||||
|
else
|
||||||
|
warn "$SHADER_BRIDGE_NAME not found: this build can run converted presets but not CONVERT new ones (set SHADERFX_BRIDGE_IOS, or install cargo plus one of: rustup target add $rust_targets)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
verify_shader_bridge() {
|
||||||
|
local app="$1"
|
||||||
|
local exe bin
|
||||||
|
if [ -z "$SHADER_BRIDGE_LIB" ]; then
|
||||||
|
warn "no SHADER FX bridge in this build: CONVERT stays unavailable, converted presets still run"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
exe="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' \
|
||||||
|
"$app/Info.plist" 2>/dev/null || true)"
|
||||||
|
bin="$app/${exe:-love}"
|
||||||
|
[ -f "$bin" ] || bin="$app/love"
|
||||||
|
if [ ! -f "$bin" ]; then
|
||||||
|
warn "no executable inside $(basename "$app"); skipping SHADER FX bridge check"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if nm "$bin" 2>/dev/null | grep -E ' _librashader_translate_preset$' >/dev/null \
|
||||||
|
|| xcrun dyld_info -exports "$bin" 2>/dev/null \
|
||||||
|
| grep -E ' _librashader_translate_preset$' >/dev/null; then
|
||||||
|
say "SHADER FX bridge present (librashader_translate_preset)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fail "built app does not carry the SHADER FX bridge symbols.
|
||||||
|
$SHADER_BRIDGE_OBJ was linked but librashader_translate_preset is absent,
|
||||||
|
so SHADER FX CONVERT would fail at runtime.
|
||||||
|
Rebuild after: rm -rf tools/shaderfx-bridge/target"
|
||||||
|
}
|
||||||
|
|
||||||
run_xcodebuild() {
|
run_xcodebuild() {
|
||||||
local config sdk destination
|
local config sdk destination
|
||||||
if $RELEASE; then
|
if $RELEASE; then
|
||||||
@@ -633,6 +765,8 @@ run_xcodebuild() {
|
|||||||
|
|
||||||
mkdir -p "$BUILD_DIR"
|
mkdir -p "$BUILD_DIR"
|
||||||
|
|
||||||
|
bundle_shader_bridge_ios "$(rust_targets_for_sdk "$sdk")" "$sdk"
|
||||||
|
|
||||||
# Prefer -target + SYMROOT over -derivedDataPath: modern Xcode requires
|
# Prefer -target + SYMROOT over -derivedDataPath: modern Xcode requires
|
||||||
# -scheme whenever -derivedDataPath is set, and love-ios ships no shared schemes.
|
# -scheme whenever -derivedDataPath is set, and love-ios ships no shared schemes.
|
||||||
# Always stamp both: the overlay plist expands $(MARKETING_VERSION) /
|
# Always stamp both: the overlay plist expands $(MARKETING_VERSION) /
|
||||||
@@ -661,6 +795,13 @@ run_xcodebuild() {
|
|||||||
ONLY_ACTIVE_ARCH=NO
|
ONLY_ACTIVE_ARCH=NO
|
||||||
DISABLE_MANUAL_TARGET_ORDER_BUILD_WARNING=YES
|
DISABLE_MANUAL_TARGET_ORDER_BUILD_WARNING=YES
|
||||||
)
|
)
|
||||||
|
if [ -n "$SHADER_BRIDGE_LIB" ]; then
|
||||||
|
args+=(OTHER_LDFLAGS="-Wl,-u,_librashader_translate_preset -Wl,-u,_librashader_free_string \"$SHADER_BRIDGE_LIB\" -lc++")
|
||||||
|
if [ -n "$SHADER_BRIDGE_ARCHS" ]; then
|
||||||
|
args+=(ARCHS="$SHADER_BRIDGE_ARCHS")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
if ! $DEVICE; then
|
if ! $DEVICE; then
|
||||||
# Simulator: ad-hoc signing (no certificate needed). A plain unsigned
|
# Simulator: ad-hoc signing (no certificate needed). A plain unsigned
|
||||||
# build would drop the entitlements file, and HealthKit refuses to run
|
# build would drop the entitlements file, and HealthKit refuses to run
|
||||||
@@ -723,11 +864,14 @@ run_xcodebuild() {
|
|||||||
|
|
||||||
local products="$BUILD_DIR/Build/Products/${config}-${sdk}"
|
local products="$BUILD_DIR/Build/Products/${config}-${sdk}"
|
||||||
local app=""
|
local app=""
|
||||||
local candidate
|
local candidate newest=0 mtime
|
||||||
for candidate in "$products/$PRODUCT_NAME.app" "$products/$APP_NAME.app" "$products/love.app"; do
|
for candidate in "$products/$PRODUCT_NAME.app" "$products/$APP_NAME.app" "$products/love.app"; do
|
||||||
if [ -d "$candidate" ]; then
|
if [ -d "$candidate" ]; then
|
||||||
|
mtime="$(stat -f %m "$candidate" 2>/dev/null || echo 0)"
|
||||||
|
if [ "$mtime" -gt "$newest" ]; then
|
||||||
|
newest="$mtime"
|
||||||
app="$candidate"
|
app="$candidate"
|
||||||
break
|
fi
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
if [ -z "$app" ]; then
|
if [ -z "$app" ]; then
|
||||||
@@ -736,6 +880,7 @@ run_xcodebuild() {
|
|||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
if [ "$app" != "$products/$APP_NAME.app" ]; then
|
if [ "$app" != "$products/$APP_NAME.app" ]; then
|
||||||
|
rm -rf "$products/$APP_NAME.app"
|
||||||
mv "$app" "$products/$APP_NAME.app"
|
mv "$app" "$products/$APP_NAME.app"
|
||||||
app="$products/$APP_NAME.app"
|
app="$products/$APP_NAME.app"
|
||||||
fi
|
fi
|
||||||
@@ -754,6 +899,7 @@ run_xcodebuild() {
|
|||||||
|
|
||||||
verify_game_payload "$app"
|
verify_game_payload "$app"
|
||||||
verify_native_bridge "$app"
|
verify_native_bridge "$app"
|
||||||
|
verify_shader_bridge "$app"
|
||||||
|
|
||||||
local dist_dir="$DIST/${config}-${sdk}"
|
local dist_dir="$DIST/${config}-${sdk}"
|
||||||
rm -rf "$dist_dir"
|
rm -rf "$dist_dir"
|
||||||
|
|||||||
@@ -100,6 +100,21 @@ then
|
|||||||
fi
|
fi
|
||||||
cp "$GAME_LOVE" "$IN_DIR/game.love"
|
cp "$GAME_LOVE" "$IN_DIR/game.love"
|
||||||
|
|
||||||
|
BRIDGE_LIB="liblibrashader_bridge.so"
|
||||||
|
BRIDGE_SRC="${SHADERFX_BRIDGE_LINUX_ARM64:-}"
|
||||||
|
if [ -z "$BRIDGE_SRC" ] && [ -f "$ROOT/dist/native/linux-arm64/$BRIDGE_LIB" ]; then
|
||||||
|
BRIDGE_SRC="$ROOT/dist/native/linux-arm64/$BRIDGE_LIB"
|
||||||
|
fi
|
||||||
|
rm -f "$IN_DIR/$BRIDGE_LIB"
|
||||||
|
if [ -n "$BRIDGE_SRC" ] && [ -f "$BRIDGE_SRC" ]; then
|
||||||
|
cp "$BRIDGE_SRC" "$IN_DIR/$BRIDGE_LIB"
|
||||||
|
say "staged $BRIDGE_LIB for SHADER FX preset conversion"
|
||||||
|
elif [ "${SHADERFX_BRIDGE_REQUIRED:-}" = "1" ]; then
|
||||||
|
fail "$BRIDGE_LIB not found: set SHADERFX_BRIDGE_LINUX_ARM64 or stage it at dist/native/linux-arm64/$BRIDGE_LIB"
|
||||||
|
else
|
||||||
|
warn "$BRIDGE_LIB not found: this build can run converted presets but not CONVERT new ones"
|
||||||
|
fi
|
||||||
|
|
||||||
# --------------------------------------------------------------- downloads
|
# --------------------------------------------------------------- downloads
|
||||||
# Fetched on the host and checksum-pinned here so the container never needs
|
# Fetched on the host and checksum-pinned here so the container never needs
|
||||||
# network access and every input is verified in exactly one place.
|
# network access and every input is verified in exactly one place.
|
||||||
|
|||||||
@@ -327,6 +327,14 @@ bundle_needed "$APPDIR/lib/liblove-$LOVE_VERSION.so"
|
|||||||
say "bundled $(ls "$APPDIR/lib" | wc -l) libraries: $(ls "$APPDIR/lib" | tr '\n' ' ')"
|
say "bundled $(ls "$APPDIR/lib" | wc -l) libraries: $(ls "$APPDIR/lib" | tr '\n' ' ')"
|
||||||
|
|
||||||
# ------------------------------------------------- host dependency contract
|
# ------------------------------------------------- host dependency contract
|
||||||
|
SHADER_BRIDGE=()
|
||||||
|
if [ -f "$IN/liblibrashader_bridge.so" ]; then
|
||||||
|
cp "$IN/liblibrashader_bridge.so" "$APPDIR/liblibrashader_bridge.so"
|
||||||
|
chmod 0755 "$APPDIR/liblibrashader_bridge.so"
|
||||||
|
SHADER_BRIDGE=("$APPDIR/liblibrashader_bridge.so")
|
||||||
|
say "bundled liblibrashader_bridge.so for SHADER FX preset conversion"
|
||||||
|
fi
|
||||||
|
|
||||||
# The portability promise, stated as an assertion instead of a paragraph in a
|
# The portability promise, stated as an assertion instead of a paragraph in a
|
||||||
# README: these are the ONLY sonames the shipped objects may require from the
|
# README: these are the ONLY sonames the shipped objects may require from the
|
||||||
# host. Everything driver-, session- or audio-related has to be reached
|
# host. Everything driver-, session- or audio-related has to be reached
|
||||||
@@ -339,7 +347,7 @@ say "bundled $(ls "$APPDIR/lib" | wc -l) libraries: $(ls "$APPDIR/lib" | tr '\n'
|
|||||||
HOST_ALLOWED_RE='^(ld-linux-aarch64\.so\.1|libc\.so\.6|libm\.so\.6|libdl\.so\.2|libpthread\.so\.0|librt\.so\.1|libstdc\+\+\.so\.6|libgcc_s\.so\.1|libatomic\.so\.1|libfreetype\.so\.6|libpng[0-9]*\.so\.[0-9]+|libz\.so\.1|libbrotli(dec|common)\.so\.1)$'
|
HOST_ALLOWED_RE='^(ld-linux-aarch64\.so\.1|libc\.so\.6|libm\.so\.6|libdl\.so\.2|libpthread\.so\.0|librt\.so\.1|libstdc\+\+\.so\.6|libgcc_s\.so\.1|libatomic\.so\.1|libfreetype\.so\.6|libpng[0-9]*\.so\.[0-9]+|libz\.so\.1|libbrotli(dec|common)\.so\.1)$'
|
||||||
|
|
||||||
unexpected=""
|
unexpected=""
|
||||||
for object in "$APPDIR/bin/love" "$APPDIR"/lib/*.so*; do
|
for object in "$APPDIR/bin/love" "$APPDIR"/lib/*.so* ${SHADER_BRIDGE[@]+"${SHADER_BRIDGE[@]}"}; do
|
||||||
while read -r soname; do
|
while read -r soname; do
|
||||||
[ -n "$soname" ] || continue
|
[ -n "$soname" ] || continue
|
||||||
# Satisfied from inside the AppDir, so not a host requirement at all.
|
# Satisfied from inside the AppDir, so not a host requirement at all.
|
||||||
|
|||||||
@@ -20,6 +20,23 @@ for required in AppRun bin/love game.love lib/liblove-11.5.so; do
|
|||||||
|| { echo "::error::AppImage is missing $required"; exit 1; }
|
|| { echo "::error::AppImage is missing $required"; exit 1; }
|
||||||
done
|
done
|
||||||
|
|
||||||
|
bridge="squashfs-root/liblibrashader_bridge.so"
|
||||||
|
if [ "${SHADERFX_BRIDGE_REQUIRED:-}" = "1" ] && [ ! -e "$bridge" ]; then
|
||||||
|
echo "::error::AppImage is missing liblibrashader_bridge.so"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Discovered, not enumerated: bin/love and lib/*.so* miss anything native at
|
||||||
|
# the AppDir root, which is where ShaderFX's bridge sits and where the next
|
||||||
|
# root-level library would land too.
|
||||||
|
scan=()
|
||||||
|
while IFS= read -r f; do
|
||||||
|
scan+=("$f")
|
||||||
|
done < <(find squashfs-root -type f -exec sh -c \
|
||||||
|
'head -c4 "$1" | od -An -tx1 | tr -d " \n" | grep -q "^7f454c46$"' _ {} \; -print | sort)
|
||||||
|
[ "${#scan[@]}" -gt 0 ] || { echo "::error::found no ELF objects in the AppDir"; exit 1; }
|
||||||
|
echo "scanning ${#scan[@]} ELF objects: ${scan[*]#squashfs-root/}"
|
||||||
|
|
||||||
# Every bundled object must resolve once AppRun's LD_LIBRARY_PATH is
|
# Every bundled object must resolve once AppRun's LD_LIBRARY_PATH is
|
||||||
# applied; an unresolved soname here is a user-visible launch crash.
|
# applied; an unresolved soname here is a user-visible launch crash.
|
||||||
#
|
#
|
||||||
@@ -28,14 +45,14 @@ done
|
|||||||
# hard-links libpulse/libasound/libX11/libwayland, so it only ever
|
# hard-links libpulse/libasound/libX11/libwayland, so it only ever
|
||||||
# started on a full desktop -- a bare runner is what exposed it.
|
# started on a full desktop -- a bare runner is what exposed it.
|
||||||
missing="$(LD_LIBRARY_PATH="$PWD/squashfs-root/lib" \
|
missing="$(LD_LIBRARY_PATH="$PWD/squashfs-root/lib" \
|
||||||
ldd squashfs-root/bin/love squashfs-root/lib/*.so* 2>/dev/null \
|
ldd "${scan[@]}" 2>/dev/null \
|
||||||
| grep 'not found' || true)"
|
| grep 'not found' || true)"
|
||||||
[ -z "$missing" ] || { echo "::error::unresolved deps:"; echo "$missing"; exit 1; }
|
[ -z "$missing" ] || { echo "::error::unresolved deps:"; echo "$missing"; exit 1; }
|
||||||
|
|
||||||
# Nothing may hard-link a driver, session or audio-stack library:
|
# Nothing may hard-link a driver, session or audio-stack library:
|
||||||
# those must be reached through dlopen so the AppImage runs on a box
|
# those must be reached through dlopen so the AppImage runs on a box
|
||||||
# with only ALSA, only Wayland, or only KMSDRM.
|
# with only ALSA, only Wayland, or only KMSDRM.
|
||||||
linked="$(for f in squashfs-root/bin/love squashfs-root/lib/*.so*; do
|
linked="$(for f in "${scan[@]}"; do
|
||||||
objdump -p "$f" 2>/dev/null | awk '/NEEDED/{print $2}'
|
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)"
|
done | sort -u | grep -E '^lib(pulse|asound|X11|wayland|GL|EGL|drm|gbm|xcb|cairo|sndio|dbus)' || true)"
|
||||||
[ -z "$linked" ] \
|
[ -z "$linked" ] \
|
||||||
@@ -45,7 +62,7 @@ done | sort -u | grep -E '^lib(pulse|asound|X11|wayland|GL|EGL|drm|gbm|xcb|cairo
|
|||||||
# the builder to a newer base, the glibc floor silently rises and
|
# 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
|
# every user on an older distro gets "GLIBC_2.xx not found" -- catch
|
||||||
# it here instead of in a release.
|
# it here instead of in a release.
|
||||||
floor="$(objdump -T squashfs-root/bin/love squashfs-root/lib/*.so* 2>/dev/null \
|
floor="$(objdump -T "${scan[@]}" 2>/dev/null \
|
||||||
| grep -o 'GLIBC_[0-9.]*' | sort -V | tail -1)"
|
| grep -o 'GLIBC_[0-9.]*' | sort -V | tail -1)"
|
||||||
echo "highest required glibc symbol version: $floor"
|
echo "highest required glibc symbol version: $floor"
|
||||||
[ -n "$floor" ] \
|
[ -n "$floor" ] \
|
||||||
@@ -54,4 +71,12 @@ highest="$(printf '%s\n' "$floor" "GLIBC_2.31" | sort -V | tail -1)"
|
|||||||
[ "$highest" = "GLIBC_2.31" ] \
|
[ "$highest" = "GLIBC_2.31" ] \
|
||||||
|| { echo "::error::AppImage requires $floor, above the bullseye 2.31 floor"; exit 1; }
|
|| { echo "::error::AppImage requires $floor, above the bullseye 2.31 floor"; exit 1; }
|
||||||
|
|
||||||
|
# The floor grep proves symbol versions; an actual dlopen additionally proves
|
||||||
|
# relocations and constructors. Native arch only, so it is skipped elsewhere.
|
||||||
|
if [ -e "$bridge" ] && [ "$(uname -m)" = "aarch64" ] && command -v python3 >/dev/null 2>&1; then
|
||||||
|
python3 -c "import ctypes,sys; ctypes.CDLL(sys.argv[1])" "$PWD/$bridge" \
|
||||||
|
|| { echo "::error::liblibrashader_bridge.so failed to dlopen"; exit 1; }
|
||||||
|
echo "librashader bridge dlopens cleanly"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "AppImage verified: $image"
|
echo "AppImage verified: $image"
|
||||||
|
|||||||
@@ -114,9 +114,11 @@ fi
|
|||||||
|
|
||||||
run_tier "T0 ROM builder version routing" python3 tests/build_rom_data_cli_test.py
|
run_tier "T0 ROM builder version routing" python3 tests/build_rom_data_cli_test.py
|
||||||
run_tier "T0 ROM manifest generator pin/overrides" python3 tests/rom_manifest_generator_test.py
|
run_tier "T0 ROM manifest generator pin/overrides" python3 tests/rom_manifest_generator_test.py
|
||||||
|
run_tier "T0 Yellow title OBP eye remap" python3 tests/title_pikachu_obp_test.py
|
||||||
run_tier "T0 Crystal manifest + specials coverage" "$LUA" tests/crystal_import_test.lua
|
run_tier "T0 Crystal manifest + specials coverage" "$LUA" tests/crystal_import_test.lua
|
||||||
run_tier "T0 switch CI workflow content gate" "$LUA" tests/switch_ci_workflows_test.lua
|
run_tier "T0 switch CI workflow content gate" "$LUA" tests/switch_ci_workflows_test.lua
|
||||||
run_tier "T0 switch transfer docs gate" "$LUA" tests/switch_transfer_docs_test.lua
|
run_tier "T0 switch transfer docs gate" "$LUA" tests/switch_transfer_docs_test.lua
|
||||||
|
run_tier "T0 ShaderFX bridge packaging gate" "$LUA" tests/shaderfx_bridge_packaging_test.lua
|
||||||
# NX Blue/Yellow asset overlay: ROM-free, must run on every checkout so a
|
# NX Blue/Yellow asset overlay: ROM-free, must run on every checkout so a
|
||||||
# Sound.lua / overlay regression is not gated only on switch-changes paths.
|
# Sound.lua / overlay regression is not gated only on switch-changes paths.
|
||||||
run_tier "T0 NX asset overlay fallback" "$LUA" tests/engine/assets_version_fallback_test.lua
|
run_tier "T0 NX asset overlay fallback" "$LUA" tests/engine/assets_version_fallback_test.lua
|
||||||
|
|||||||
+185
-39
@@ -34,6 +34,12 @@ local RomText = require("src.core.RomText")
|
|||||||
local Strings = require("src.core.Strings")
|
local Strings = require("src.core.Strings")
|
||||||
local WideBattle = require("src.battle.WideBattle")
|
local WideBattle = require("src.battle.WideBattle")
|
||||||
|
|
||||||
|
local Chrome2
|
||||||
|
do
|
||||||
|
local ok, v = pcall(require, "src.ui.gen2.Chrome")
|
||||||
|
Chrome2 = ok and v or nil
|
||||||
|
end
|
||||||
|
|
||||||
local romText = RomText
|
local romText = RomText
|
||||||
|
|
||||||
local BattleState = {}
|
local BattleState = {}
|
||||||
@@ -97,7 +103,7 @@ end
|
|||||||
-- space.
|
-- space.
|
||||||
function BattleState:extendedHUD()
|
function BattleState:extendedHUD()
|
||||||
local options = self.game and self.game.save and self.game.save.options
|
local options = self.game and self.game.save and self.game.save.options
|
||||||
local bg = options and options.battleBg
|
local bg = options and self:bgMode()
|
||||||
return self:wideLayout()
|
return self:wideLayout()
|
||||||
and options and options.battleHud == "extended"
|
and options and options.battleHud == "extended"
|
||||||
and ((options.battleFit == "fixed"
|
and ((options.battleFit == "fixed"
|
||||||
@@ -108,12 +114,12 @@ end
|
|||||||
function BattleState:extendedWorldHUD()
|
function BattleState:extendedWorldHUD()
|
||||||
local options = self.game and self.game.save and self.game.save.options
|
local options = self.game and self.game.save and self.game.save.options
|
||||||
return self:extendedHUD() and options
|
return self:extendedHUD() and options
|
||||||
and options.battleFit == "fixed" and options.battleBg == "world"
|
and options.battleFit == "fixed" and self:bgMode() == "world"
|
||||||
end
|
end
|
||||||
|
|
||||||
function BattleState:extendedBlackHUD()
|
function BattleState:extendedBlackHUD()
|
||||||
local options = self.game and self.game.save and self.game.save.options
|
local options = self.game and self.game.save and self.game.save.options
|
||||||
return self:extendedHUD() and options and options.battleBg == "black"
|
return self:extendedHUD() and options and self:bgMode() == "black"
|
||||||
end
|
end
|
||||||
|
|
||||||
-- BATTLE BG: what fills the screen AROUND the battle -- the letterbox voids
|
-- BATTLE BG: what fills the screen AROUND the battle -- the letterbox voids
|
||||||
@@ -130,6 +136,10 @@ end
|
|||||||
-- opaque 160x144 field over the top, so only the surround changes.
|
-- opaque 160x144 field over the top, so only the surround changes.
|
||||||
function BattleState:bgMode()
|
function BattleState:bgMode()
|
||||||
local options = self.game and self.game.save and self.game.save.options
|
local options = self.game and self.game.save and self.game.save.options
|
||||||
|
if options and self:wideLayout() and options.battleFit == "fill"
|
||||||
|
and options.battleHud == "extended" then
|
||||||
|
return "white"
|
||||||
|
end
|
||||||
local mode = options and options.battleBg
|
local mode = options and options.battleBg
|
||||||
if mode == "black" or mode == "world" then return mode end
|
if mode == "black" or mode == "world" then return mode end
|
||||||
return "white"
|
return "white"
|
||||||
@@ -212,6 +222,13 @@ local BALL_ANIMS = {
|
|||||||
SHAKE_ANIM = true, SHOWPIC_ANIM = true,
|
SHAKE_ANIM = true, SHOWPIC_ANIM = true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-- engine/battle/effects.asm:552
|
||||||
|
local ENEMY_STAT_DOWN_MISS = {
|
||||||
|
ATTACK_DOWN1_EFFECT = true, DEFENSE_DOWN1_EFFECT = true,
|
||||||
|
DEFENSE_DOWN2_EFFECT = true, SPEED_DOWN1_EFFECT = true,
|
||||||
|
ACCURACY_DOWN1_EFFECT = true,
|
||||||
|
}
|
||||||
|
|
||||||
local imageCache = {}
|
local imageCache = {}
|
||||||
-- The three tables below are keyed by the Image OBJECT, not by a path, and a
|
-- The three tables below are keyed by the Image OBJECT, not by a path, and a
|
||||||
-- running battle holds the pics it built at enter() (battler.sprite,
|
-- running battle holds the pics it built at enter() (battler.sprite,
|
||||||
@@ -229,6 +246,11 @@ local imagePadLeft = setmetatable({}, WEAK_KEYS)
|
|||||||
-- image -> { path, pal } so palette-fade variants (see fadeImage) can be
|
-- image -> { path, pal } so palette-fade variants (see fadeImage) can be
|
||||||
-- rebuilt for any battle pic, whatever code loaded it
|
-- rebuilt for any battle pic, whatever code loaded it
|
||||||
local imageMeta = setmetatable({}, WEAK_KEYS)
|
local imageMeta = setmetatable({}, WEAK_KEYS)
|
||||||
|
local function mattedPic(path)
|
||||||
|
return path:sub(1, 17) == "assets/generated/"
|
||||||
|
or path:sub(1, 17) == "save/mod-derived/"
|
||||||
|
end
|
||||||
|
|
||||||
-- pal = { name, colors } recolors the 4 GB shades like the Super Game Boy.
|
-- pal = { name, colors } recolors the 4 GB shades like the Super Game Boy.
|
||||||
-- trueColor art (14 §the 4-shade contract) opts out of the quantize
|
-- trueColor art (14 §the 4-shade contract) opts out of the quantize
|
||||||
-- entirely, so its palette variant collapses back onto the plain path.
|
-- entirely, so its palette variant collapses back onto the plain path.
|
||||||
@@ -254,6 +276,7 @@ local function getImage(path, pal, trueColor)
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
local w, h = id:getDimensions()
|
local w, h = id:getDimensions()
|
||||||
|
if mattedPic(Assets.resolve(path)) then
|
||||||
local bottom = h - 1
|
local bottom = h - 1
|
||||||
while bottom >= 0 do
|
while bottom >= 0 do
|
||||||
local opaque = false
|
local opaque = false
|
||||||
@@ -274,9 +297,10 @@ local function getImage(path, pal, trueColor)
|
|||||||
if opaque then break end
|
if opaque then break end
|
||||||
left = left + 1
|
left = left + 1
|
||||||
end
|
end
|
||||||
img = love.graphics.newImage(id)
|
|
||||||
pad = h - 1 - bottom
|
pad = h - 1 - bottom
|
||||||
padL = left
|
padL = left
|
||||||
|
end
|
||||||
|
img = love.graphics.newImage(id)
|
||||||
else
|
else
|
||||||
img = Assets.image(path) -- headless stub: no pixel access
|
img = Assets.image(path) -- headless stub: no pixel access
|
||||||
end
|
end
|
||||||
@@ -443,7 +467,7 @@ function BattleState:picImage(img)
|
|||||||
-- palettes (CYANMON reds 0.678/0.451) already rendered correctly. This mode
|
-- palettes (CYANMON reds 0.678/0.451) already rendered correctly. This mode
|
||||||
-- set mirrors PaletteFX.ensureZones / effectiveColors -- keep them in sync.
|
-- set mirrors PaletteFX.ensureZones / effectiveColors -- keep them in sync.
|
||||||
local mono = PaletteFX.mode == "og" or PaletteFX.mode == "og_inv"
|
local mono = PaletteFX.mode == "og" or PaletteFX.mode == "og_inv"
|
||||||
or PaletteFX.mode == "classic"
|
or PaletteFX.mode == "classic" or PaletteFX.forcesRawGrays()
|
||||||
if self.grayPics or mono then return grayImage(img) end
|
if self.grayPics or mono then return grayImage(img) end
|
||||||
-- SET_PAL_BATTLE_BLACK covers every battle palette slot, so the pics go
|
-- SET_PAL_BATTLE_BLACK covers every battle palette slot, so the pics go
|
||||||
-- dark with the HP bars while the blackout text is up (#292). The intro
|
-- dark with the HP bars while the blackout text is up (#292). The intro
|
||||||
@@ -2214,6 +2238,9 @@ function BattleState:update(dt)
|
|||||||
self.waitFrames = nil
|
self.waitFrames = nil
|
||||||
if destination == "menu" then
|
if destination == "menu" then
|
||||||
self.introSlide = nil
|
self.introSlide = nil
|
||||||
|
-- engine/battle/core.asm:2007
|
||||||
|
self.msgHold = nil
|
||||||
|
self.shown = nil
|
||||||
self.phase = "menu"
|
self.phase = "menu"
|
||||||
elseif destination == "finish" then
|
elseif destination == "finish" then
|
||||||
self:finish()
|
self:finish()
|
||||||
@@ -3041,18 +3068,33 @@ local BGP_INVERT = { [0] = 3, 2, 1, 0 } -- $1b (flash phase 1)
|
|||||||
local BGP_WHITE = { [0] = 0, 0, 0, 0 } -- $00 (flash phase 2)
|
local BGP_WHITE = { [0] = 0, 0, 0, 0 } -- $00 (flash phase 2)
|
||||||
local BGP_DARK = { [0] = 3, 3, 2, 1 } -- $6f DarkScreenPalette
|
local BGP_DARK = { [0] = 3, 3, 2, 1 } -- $6f DarkScreenPalette
|
||||||
local BGP_LIGHT = { [0] = 0, 0, 1, 2 } -- $90 LightScreenPalette
|
local BGP_LIGHT = { [0] = 0, 0, 1, 2 } -- $90 LightScreenPalette
|
||||||
local BGP_DARKEN = { [0] = 0, 1, 3, 3 } -- $f4 DarkenMonPalette (SGB)
|
local BGP_DARKEN_SGB = { [0] = 0, 1, 3, 3 } -- $f4
|
||||||
|
local BGP_DARKEN_MONO = { [0] = 1, 2, 3, 3 } -- $f9
|
||||||
|
|
||||||
|
-- engine/battle/animations.asm:1090
|
||||||
|
local function onSgb()
|
||||||
|
local m = require("src.render.PaletteFX").mode
|
||||||
|
return m == "gbc" or m == "gbc_inv"
|
||||||
|
end
|
||||||
|
|
||||||
-- FlashScreenLongSGB (animations.asm:1010): 12 BGP values per cycle,
|
-- FlashScreenLongSGB (animations.asm:1010): 12 BGP values per cycle,
|
||||||
-- 3 cycles; the first cycle holds each for 2 frames, the rest for 1
|
-- 3 cycles; the first cycle holds each for 2 frames, the rest for 1
|
||||||
-- (FlashScreenLongDelay)
|
-- (FlashScreenLongDelay)
|
||||||
local FLASH_LONG_MAPS = {
|
local FLASH_LONG_SGB = {
|
||||||
{ [0] = 0, 2, 3, 3 }, { [0] = 0, 3, 3, 3 }, { [0] = 3, 3, 3, 3 },
|
{ [0] = 0, 2, 3, 3 }, { [0] = 0, 3, 3, 3 }, { [0] = 3, 3, 3, 3 },
|
||||||
{ [0] = 0, 3, 3, 3 }, { [0] = 0, 2, 3, 3 }, { [0] = 0, 1, 2, 3 },
|
{ [0] = 0, 3, 3, 3 }, { [0] = 0, 2, 3, 3 }, { [0] = 0, 1, 2, 3 },
|
||||||
{ [0] = 0, 0, 1, 2 }, { [0] = 0, 0, 0, 1 }, { [0] = 0, 0, 0, 0 },
|
{ [0] = 0, 0, 1, 2 }, { [0] = 0, 0, 0, 1 }, { [0] = 0, 0, 0, 0 },
|
||||||
{ [0] = 0, 0, 0, 1 }, { [0] = 0, 0, 1, 2 }, { [0] = 0, 1, 2, 3 },
|
{ [0] = 0, 0, 0, 1 }, { [0] = 0, 0, 1, 2 }, { [0] = 0, 1, 2, 3 },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-- engine/battle/animations.asm:992
|
||||||
|
local FLASH_LONG_MONO = {
|
||||||
|
{ [0] = 1, 2, 3, 3 }, { [0] = 2, 3, 3, 3 }, { [0] = 3, 3, 3, 3 },
|
||||||
|
{ [0] = 2, 3, 3, 3 }, { [0] = 1, 2, 3, 3 }, { [0] = 0, 1, 2, 3 },
|
||||||
|
{ [0] = 0, 0, 1, 2 }, { [0] = 0, 0, 0, 1 }, { [0] = 0, 0, 0, 0 },
|
||||||
|
{ [0] = 0, 0, 0, 1 }, { [0] = 0, 0, 1, 2 }, { [0] = 0, 1, 2, 3 },
|
||||||
|
}
|
||||||
|
|
||||||
-- the shade map in force this frame (a running flash wins over the
|
-- the shade map in force this frame (a running flash wins over the
|
||||||
-- persistent palette)
|
-- persistent palette)
|
||||||
function BattleState:activeBgp()
|
function BattleState:activeBgp()
|
||||||
@@ -3186,7 +3228,7 @@ function BattleState:applyAnimEffect(ev)
|
|||||||
elseif e == "SE_LIGHT_SCREEN_PALETTE" then
|
elseif e == "SE_LIGHT_SCREEN_PALETTE" then
|
||||||
fx.bgp = BGP_LIGHT
|
fx.bgp = BGP_LIGHT
|
||||||
elseif e == "SE_DARKEN_MON_PALETTE" then
|
elseif e == "SE_DARKEN_MON_PALETTE" then
|
||||||
fx.bgp = BGP_DARKEN
|
fx.bgp = onSgb() and BGP_DARKEN_SGB or BGP_DARKEN_MONO
|
||||||
elseif e == "SE_RESET_SCREEN_PALETTE" then
|
elseif e == "SE_RESET_SCREEN_PALETTE" then
|
||||||
fx.bgp = nil
|
fx.bgp = nil
|
||||||
elseif e == "SE_DARK_SCREEN_FLASH" then
|
elseif e == "SE_DARK_SCREEN_FLASH" then
|
||||||
@@ -3196,8 +3238,9 @@ function BattleState:applyAnimEffect(ev)
|
|||||||
idx = 1, left = 2 }
|
idx = 1, left = 2 }
|
||||||
elseif e == "SE_FLASH_SCREEN_LONG" then
|
elseif e == "SE_FLASH_SCREEN_LONG" then
|
||||||
local steps = {}
|
local steps = {}
|
||||||
|
local maps = onSgb() and FLASH_LONG_SGB or FLASH_LONG_MONO
|
||||||
for cycle = 1, 3 do
|
for cycle = 1, 3 do
|
||||||
for _, m in ipairs(FLASH_LONG_MAPS) do
|
for _, m in ipairs(maps) do
|
||||||
steps[#steps + 1] = { map = m, frames = (cycle == 1) and 2 or 1 }
|
steps[#steps + 1] = { map = m, frames = (cycle == 1) and 2 or 1 }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -4139,6 +4182,12 @@ function BattleState:performMove(user, target, moveInst, isCalled)
|
|||||||
|
|
||||||
-- pure status moves
|
-- pure status moves
|
||||||
if move.power == 0 and record and record.kind == "primary" and record.run then
|
if move.power == 0 and record and record.kind == "primary" and record.run then
|
||||||
|
if ENEMY_STAT_DOWN_MISS[move.effect] and not user.isPlayer
|
||||||
|
and self.kind ~= "link" and self.rng(0, 255) < 64 then
|
||||||
|
self:cancelMoveAnim()
|
||||||
|
self:sayNext(self:romText("_AttackMissedText", "%s's\nattack missed!", displayName(user)))
|
||||||
|
return
|
||||||
|
end
|
||||||
-- accuracy-checked status effects run MoveHitTest, which has no
|
-- accuracy-checked status effects run MoveHitTest, which has no
|
||||||
-- 100%-accuracy early-out (even Thunder Wave misses on the 255
|
-- 100%-accuracy early-out (even Thunder Wave misses on the 255
|
||||||
-- roll) and misses outright against a mid-Fly/Dig target; the
|
-- roll) and misses outright against a mid-Fly/Dig target; the
|
||||||
@@ -4417,7 +4466,11 @@ function BattleState:awardExp()
|
|||||||
-- new current HP (house convention: potions drain the bar too, see
|
-- new current HP (house convention: potions drain the bar too, see
|
||||||
-- itemUsed) so the bar grows instead. Only the active player battler
|
-- itemUsed) so the bar grows instead. Only the active player battler
|
||||||
-- shares its table with the HUD; other party mons (EXP.ALL) have no bar.
|
-- shares its table with the HUD; other party mons (EXP.ALL) have no bar.
|
||||||
if mon == self.player.mon then self:drainNext() end
|
if mon == self.player.mon then
|
||||||
|
-- engine/battle/experience.asm:236
|
||||||
|
self.player.badgeExtraBoosts = nil
|
||||||
|
self:drainNext()
|
||||||
|
end
|
||||||
for _, moveId in ipairs(Experience.movesLearnedAt(
|
for _, moveId in ipairs(Experience.movesLearnedAt(
|
||||||
self.data.pokemon[mon.species], lv)) do
|
self.data.pokemon[mon.species], lv)) do
|
||||||
self:learnMove(mon, moveId)
|
self:learnMove(mon, moveId)
|
||||||
@@ -4484,8 +4537,7 @@ function BattleState:enemyMonFainted()
|
|||||||
-- remaining HP); SET / single-mon / fainted active skip the prompt.
|
-- remaining HP); SET / single-mon / fainted active skip the prompt.
|
||||||
local nextMon = self.enemyParty[self.enemyIndex]
|
local nextMon = self.enemyParty[self.enemyIndex]
|
||||||
local nextName = nextMon.nickname or self.data.pokemon[nextMon.species].name
|
local nextName = nextMon.nickname or self.data.pokemon[nextMon.species].name
|
||||||
local style = tostring((self.game.save.options or {}).battleStyle or "shift")
|
local style = self:battleStyle()
|
||||||
:lower()
|
|
||||||
local partyCount = #self:playerPartyView()
|
local partyCount = #self:playerPartyView()
|
||||||
-- ReplaceFaintedEnemyMon (core.asm:892-896): DrawEnemyPokeballs puts the
|
-- ReplaceFaintedEnemyMon (core.asm:892-896): DrawEnemyPokeballs puts the
|
||||||
-- foe's party ball row -- and the HUD chrome PlaceEnemyHUDTiles lays
|
-- foe's party ball row -- and the HUD chrome PlaceEnemyHUDTiles lays
|
||||||
@@ -5053,6 +5105,57 @@ function BattleState:ballMissMessage(shakes)
|
|||||||
return t._ItemUseBallText04 or self:romText("_ItemUseBallText04", "Shoot! It was so\nclose too!")
|
return t._ItemUseBallText04 or self:romText("_ItemUseBallText04", "Shoot! It was so\nclose too!")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- ------- battle rules a mode may own
|
||||||
|
--
|
||||||
|
-- Two decisions the OPTION screen and the cart make for the player that a
|
||||||
|
-- game mode may want to make instead: whether a faint offers a free switch,
|
||||||
|
-- and whether a catch asks for a nickname. Each is a hook around the vanilla
|
||||||
|
-- answer, so a mode can force it without touching the player's saved
|
||||||
|
-- preference and without the player being able to change it mid-match.
|
||||||
|
--
|
||||||
|
-- The vanilla links are file-locals so an empty chain allocates no closure.
|
||||||
|
|
||||||
|
local function styleFromOptions(battle)
|
||||||
|
return tostring(((battle.game.save or {}).options or {}).battleStyle or "shift")
|
||||||
|
:lower()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function alwaysAsk() return true end
|
||||||
|
|
||||||
|
-- "shift" or "set" for this battle. battle.style wraps the OPTION row: a
|
||||||
|
-- mod returns "set" or "shift"; anything else reads as the vanilla answer.
|
||||||
|
function BattleState:battleStyle()
|
||||||
|
if not Runtime.wantsHook("battle.style") then return styleFromOptions(self) end
|
||||||
|
local style = Runtime.call("battle.style", styleFromOptions, self)
|
||||||
|
if style == "set" then return "set" end
|
||||||
|
if style == "shift" then return "shift" end
|
||||||
|
return styleFromOptions(self)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- AskName for a catch (AddPartyMon / SendNewMonToBox). Vanilla queues the
|
||||||
|
-- yes/no prompt. catch.nickname may answer for the player: false keeps the
|
||||||
|
-- species name and shows nothing; a string is the nickname, shown nothing;
|
||||||
|
-- anything else asks as usual. Returns whether a prompt was queued.
|
||||||
|
--
|
||||||
|
-- The same verdict a script gift already takes from pokemon.before_give's
|
||||||
|
-- gift.nickname, for the other way a Pokemon joins the party.
|
||||||
|
function BattleState:offerNickname(mon, displayName)
|
||||||
|
if Runtime.wantsHook("catch.nickname") then
|
||||||
|
local verdict = Runtime.call("catch.nickname", alwaysAsk, mon,
|
||||||
|
{ battle = self, name = displayName, game = self.game })
|
||||||
|
if verdict == false then return false end
|
||||||
|
if type(verdict) == "string" then
|
||||||
|
-- the naming grid's own limit, so a mod cannot hand the party a name
|
||||||
|
-- the summary screen has no room to draw
|
||||||
|
verdict = verdict:sub(1, 10)
|
||||||
|
if #verdict > 0 then mon.nickname = verdict end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self:uiNext(function() return self:askNicknameUI(mon, displayName) end)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
-- AskName (engine/menus/naming_screen.asm): ClearSprites, wild field blank,
|
-- AskName (engine/menus/naming_screen.asm): ClearSprites, wild field blank,
|
||||||
-- PrintText, YES/NO while text stays (TextBox opts.choice). Shared by party
|
-- PrintText, YES/NO while text stays (TextBox opts.choice). Shared by party
|
||||||
-- AddPartyMon and SendNewMonToBox (#172).
|
-- AddPartyMon and SendNewMonToBox (#172).
|
||||||
@@ -5086,6 +5189,22 @@ end
|
|||||||
-- "New POKéDEX data will be added" + the dex entry page, then
|
-- "New POKéDEX data will be added" + the dex entry page, then
|
||||||
-- AddPartyMon or SendNewMonToBox (both call AskName), then the PC
|
-- AddPartyMon or SendNewMonToBox (both call AskName), then the PC
|
||||||
-- transfer text when the party was full.
|
-- transfer text when the party was full.
|
||||||
|
-- Where a caught mon goes when the party has no room for it (RFC 0018):
|
||||||
|
-- "box", as AddPartyMon falling through to SendNewMonToBox always did, or
|
||||||
|
-- "mod" when the catch.party_full hook claims it -- a game mode that has
|
||||||
|
-- done away with storage hands the decision to the player instead of
|
||||||
|
-- laundering the catch through a PC it has locked. A method rather than
|
||||||
|
-- an inline read, so a mod or a compatibility shim can tell a seam engine
|
||||||
|
-- from a stock one by name.
|
||||||
|
function BattleState:partyFullDestination(mon)
|
||||||
|
if not Runtime.wantsHook("catch.party_full") then return "box" end
|
||||||
|
local claimed = Runtime.call("catch.party_full", function() return false end,
|
||||||
|
{ battle = self, mon = mon, name = self.enemy and self.enemy.name,
|
||||||
|
game = self.game })
|
||||||
|
if claimed then return "mod" end
|
||||||
|
return "box"
|
||||||
|
end
|
||||||
|
|
||||||
function BattleState:storeCaughtMon()
|
function BattleState:storeCaughtMon()
|
||||||
-- ItemUseBall reloads the caught mon via LoadEnemyMonData
|
-- ItemUseBall reloads the caught mon via LoadEnemyMonData
|
||||||
-- (item_effects.asm:472-501), regenerating its move list from the
|
-- (item_effects.asm:472-501), regenerating its move list from the
|
||||||
@@ -5117,16 +5236,13 @@ function BattleState:storeCaughtMon()
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
local function askCaughtNickname()
|
local function askCaughtNickname()
|
||||||
local caught = self.enemy.mon
|
self:offerNickname(self.enemy.mon, self.enemy.name)
|
||||||
local enemyName = self.enemy.name
|
|
||||||
self:uiNext(function()
|
|
||||||
return self:askNicknameUI(caught, enemyName)
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
if Party.add(game.save.party, self.enemy.mon) then
|
if Party.add(game.save.party, self.enemy.mon) then
|
||||||
askCaughtNickname()
|
askCaughtNickname()
|
||||||
else
|
else
|
||||||
destination = "box"
|
destination = self:partyFullDestination(self.enemy.mon)
|
||||||
|
if destination == "box" then
|
||||||
local boxNum = require("src.pokemon.Boxes").deposit(game.save, self.enemy.mon)
|
local boxNum = require("src.pokemon.Boxes").deposit(game.save, self.enemy.mon)
|
||||||
if boxNum then
|
if boxNum then
|
||||||
askCaughtNickname()
|
askCaughtNickname()
|
||||||
@@ -5141,6 +5257,7 @@ function BattleState:storeCaughtMon()
|
|||||||
self:sayNext(Strings("But every BOX\nis full!"))
|
self:sayNext(Strings("But every BOX\nis full!"))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
Runtime.emit("pokemon.caught", {
|
Runtime.emit("pokemon.caught", {
|
||||||
battle = self, mon = self.enemy.mon, species = species, isNew = isNew,
|
battle = self, mon = self.enemy.mon, species = species, isNew = isNew,
|
||||||
ball = self.lastBall, destination = destination, game = game,
|
ball = self.lastBall, destination = destination, game = game,
|
||||||
@@ -5878,7 +5995,7 @@ function BattleState:drawZonePass(src, sx, sy)
|
|||||||
-- only the other two showed it. Keep this mode set in sync with picImage /
|
-- only the other two showed it. Keep this mode set in sync with picImage /
|
||||||
-- PaletteFX.ensureZones / WideBattle.monoMode.
|
-- PaletteFX.ensureZones / WideBattle.monoMode.
|
||||||
local mono = PaletteFX.mode == "og" or PaletteFX.mode == "og_inv"
|
local mono = PaletteFX.mode == "og" or PaletteFX.mode == "og_inv"
|
||||||
or PaletteFX.mode == "classic"
|
or PaletteFX.mode == "classic" or PaletteFX.forcesRawGrays()
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
love.graphics.setShader(shader)
|
love.graphics.setShader(shader)
|
||||||
local shaking = sx ~= 0 or sy ~= 0
|
local shaking = sx ~= 0 or sy ~= 0
|
||||||
@@ -5920,7 +6037,9 @@ function BattleState:animSpriteColors(s, px, py)
|
|||||||
local P
|
local P
|
||||||
-- engine/battle/animations.asm:551 (.notSGB)
|
-- engine/battle/animations.asm:551 (.notSGB)
|
||||||
if PaletteFX.usesSpriteObp() then
|
if PaletteFX.usesSpriteObp() then
|
||||||
P = PaletteFX.ogObj()
|
-- engine/battle/init_battle_variables.asm:18
|
||||||
|
P = require("src.core.GameVersion").isBlue() and PaletteFX.GBC_OBJ_BLUE
|
||||||
|
or PaletteFX.GBC_OBJ
|
||||||
if key == "f0" then key = "e4" elseif key == "f0x" then key = "e4x" end
|
if key == "f0" then key = "e4" elseif key == "f0x" then key = "e4x" end
|
||||||
else
|
else
|
||||||
P = self:zoneColorsAt(px or (s.x - 8 + 4), py or (s.y - 16 + 4))
|
P = self:zoneColorsAt(px or (s.x - 8 + 4), py or (s.y - 16 + 4))
|
||||||
@@ -6286,8 +6405,35 @@ end
|
|||||||
|
|
||||||
function BattleState:drawTextArea()
|
function BattleState:drawTextArea()
|
||||||
if not self:bottomUIVisible() then return end
|
if not self:bottomUIVisible() then return end
|
||||||
Font.drawBox(0, 12, 20, 6)
|
-- Gold only: routes the command box and its labels through GbcPalette,
|
||||||
|
-- the same fix TextBox.lua got for dialogue. moveSelect/mimicSelect below
|
||||||
|
-- aren't migrated yet since they need their own interior-patch handling
|
||||||
|
-- (see #240).
|
||||||
|
local gold = self.game and self.game.save
|
||||||
|
and (self.game.save.generation == 2 or self.game.save.version == "gold")
|
||||||
|
local Chrome = gold and Chrome2 or nil
|
||||||
|
local function box(tx, ty, tw, th)
|
||||||
|
if Chrome then
|
||||||
|
Chrome.paletteBox(tx, ty, tw, th)
|
||||||
|
else
|
||||||
|
Font.drawBox(tx, ty, tw, th)
|
||||||
love.graphics.setColor(0, 0, 0, 1)
|
love.graphics.setColor(0, 0, 0, 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local drawGlyph, finishGlyph = Font.drawCode, nil
|
||||||
|
if Chrome then
|
||||||
|
local _, dg, fg = Chrome.paletteGlyphs(Chrome.DEFAULT_BOX_PALETTE)
|
||||||
|
drawGlyph, finishGlyph = dg, fg
|
||||||
|
end
|
||||||
|
local function text(str, x, y)
|
||||||
|
local pen = x
|
||||||
|
for _, code in ipairs(Font.encode(str)) do
|
||||||
|
drawGlyph(code, pen, y)
|
||||||
|
pen = pen + Font.advanceOf(code)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
box(0, 12, 20, 6)
|
||||||
if self.phase == "messages"
|
if self.phase == "messages"
|
||||||
and (self.current or self.animPlaying or self.msgHold) then
|
and (self.current or self.animPlaying or self.msgHold) then
|
||||||
-- during the move animation self.current is nil but shown still holds
|
-- during the move animation self.current is nil but shown still holds
|
||||||
@@ -6306,48 +6452,47 @@ function BattleState:drawTextArea()
|
|||||||
for li, line in ipairs(self.shown or {}) do
|
for li, line in ipairs(self.shown or {}) do
|
||||||
local y = (ys[li] or 128) + off
|
local y = (ys[li] or 128) + off
|
||||||
for i = 1, #line do
|
for i = 1, #line do
|
||||||
Font.drawCode(line[i], 8 + (i - 1) * 8, y)
|
drawGlyph(line[i], 8 + (i - 1) * 8, y)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
-- the blinking down arrow ('▼', glyph $EE) while a \v CONT wait
|
-- the blinking down arrow ('▼', glyph $EE) while a \v CONT wait
|
||||||
-- (_ContText) or a typed-out page (PromptText) holds the box; both write
|
-- (_ContText) or a typed-out page (PromptText) holds the box; both write
|
||||||
-- it at (18,16), bottom-right, like TextBox / home/text.asm (#317)
|
-- it at (18,16), bottom-right, like TextBox / home/text.asm (#317)
|
||||||
if (self.msgWaiting or self.msgPrompt) and self.frame % 60 < 30 then
|
if (self.msgWaiting or self.msgPrompt) and self.frame % 60 < 30 then
|
||||||
Font.drawCode(0xEE, (0 + 20 - 2) * 8, (12 + 6 - 1) * 8 - 4)
|
drawGlyph(0xEE, (0 + 20 - 2) * 8, (12 + 6 - 1) * 8 - 4)
|
||||||
end
|
end
|
||||||
elseif self.phase == "menu" and self.demo then
|
elseif self.phase == "menu" and self.demo then
|
||||||
-- the old-man script (DisplayBattleMenu, core.asm:2038-2049): the
|
-- the old-man script (DisplayBattleMenu, core.asm:2038-2049): the
|
||||||
-- standard menu, with the '▶' hand drawn by the scripted keystrokes
|
-- standard menu, with the '▶' hand drawn by the scripted keystrokes
|
||||||
-- -- next to FIGHT (9,14) for the first 80 frames, then ITEM (9,16)
|
-- -- next to FIGHT (9,14) for the first 80 frames, then ITEM (9,16)
|
||||||
Font.drawBox(8, 12, 12, 6)
|
box(8, 12, 12, 6)
|
||||||
love.graphics.setColor(0, 0, 0, 1)
|
text(Strings("FIGHT", "battle"), 80, 112)
|
||||||
Font.draw(Strings("FIGHT", "battle"), 80, 112)
|
drawGlyph(0xE1, 128, 112); drawGlyph(0xE2, 136, 112)
|
||||||
Font.drawCode(0xE1, 128, 112); Font.drawCode(0xE2, 136, 112)
|
text(Strings("ITEM", "battle"), 80, 128); text(Strings("RUN", "battle"), 128, 128)
|
||||||
Font.draw(Strings("ITEM", "battle"), 80, 128); Font.draw(Strings("RUN", "battle"), 128, 128)
|
drawGlyph(0xED, 72, (self.demoTimer or 0) <= 80 and 112 or 128)
|
||||||
Font.drawCode(0xED, 72, (self.demoTimer or 0) <= 80 and 112 or 128)
|
|
||||||
elseif self.phase == "menu" then
|
elseif self.phase == "menu" then
|
||||||
local col = (self.menuIndex - 1) % 2
|
local col = (self.menuIndex - 1) % 2
|
||||||
local row = math.floor((self.menuIndex - 1) / 2)
|
local row = math.floor((self.menuIndex - 1) / 2)
|
||||||
if self.safari then
|
if self.safari then
|
||||||
-- SAFARI_BATTLE_MENU_TEMPLATE: full-width box, "BALLx BAIT /
|
-- SAFARI_BATTLE_MENU_TEMPLATE: full-width box, "BALLx BAIT /
|
||||||
-- THROW ROCK RUN" from (2,14)
|
-- THROW ROCK RUN" from (2,14)
|
||||||
Font.drawBox(0, 12, 20, 6)
|
box(0, 12, 20, 6)
|
||||||
Font.draw(Strings("BALLx"), 16, 112); Font.draw(Strings("BAIT"), 112, 112)
|
text(Strings("BALLx"), 16, 112); text(Strings("BAIT"), 112, 112)
|
||||||
Font.draw(Strings("THROW ROCK"), 16, 128); Font.draw(Strings("RUN", "battle"), 112, 128)
|
text(Strings("THROW ROCK"), 16, 128); text(Strings("RUN", "battle"), 112, 128)
|
||||||
-- DisplayBattleMenu .safariLeftColumn / .safariRightColumn print
|
-- DisplayBattleMenu .safariLeftColumn / .safariRightColumn print
|
||||||
-- wNumSafariBalls at hlcoord 7,14 with `lb bc, 1, 2` -- one byte, two
|
-- wNumSafariBalls at hlcoord 7,14 with `lb bc, 1, 2` -- one byte, two
|
||||||
-- digits, space padded -- right after the "BALLx" label at columns
|
-- digits, space padded -- right after the "BALLx" label at columns
|
||||||
-- 2..6 (engine/battle/core.asm:2074-2079, 2107-2112) (#540)
|
-- 2..6 (engine/battle/core.asm:2074-2079, 2107-2112) (#540)
|
||||||
Font.draw(("%2d"):format(self.safari.balls), 56, 112)
|
text(("%2d"):format(self.safari.balls), 56, 112)
|
||||||
Font.drawCode(0xED, (col == 0 and 8 or 104), 112 + row * 16)
|
drawGlyph(0xED, (col == 0 and 8 or 104), 112 + row * 16)
|
||||||
else
|
else
|
||||||
-- BATTLE_MENU_TEMPLATE: box (8,12)-(19,17), "FIGHT <PK><MN> /
|
-- BATTLE_MENU_TEMPLATE: box (8,12)-(19,17), "FIGHT <PK><MN> /
|
||||||
-- ITEM RUN" from (10,14); cursor columns 9 / 15
|
-- ITEM RUN" from (10,14); cursor columns 9 / 15
|
||||||
Font.drawBox(8, 12, 12, 6)
|
box(8, 12, 12, 6)
|
||||||
Font.draw(Strings("FIGHT", "battle"), 80, 112)
|
text(Strings("FIGHT", "battle"), 80, 112)
|
||||||
Font.drawCode(0xE1, 128, 112); Font.drawCode(0xE2, 136, 112)
|
drawGlyph(0xE1, 128, 112); drawGlyph(0xE2, 136, 112)
|
||||||
Font.draw(Strings("ITEM", "battle"), 80, 128); Font.draw(Strings("RUN", "battle"), 128, 128)
|
text(Strings("ITEM", "battle"), 80, 128); text(Strings("RUN", "battle"), 128, 128)
|
||||||
Font.drawCode(0xED, (col == 0 and 72 or 120), 112 + row * 16)
|
drawGlyph(0xED, (col == 0 and 72 or 120), 112 + row * 16)
|
||||||
end
|
end
|
||||||
elseif self.phase == "moveSelect" then
|
elseif self.phase == "moveSelect" then
|
||||||
-- pokered MoveSelectionMenu: move list in a box at (4,12) 16x6,
|
-- pokered MoveSelectionMenu: move list in a box at (4,12) 16x6,
|
||||||
@@ -6422,6 +6567,7 @@ function BattleState:drawTextArea()
|
|||||||
Font.drawCode(0xED, 8, (7 + self.mimicIndex) * 8)
|
Font.drawCode(0xED, 8, (7 + self.mimicIndex) * 8)
|
||||||
Font.draw(Strings("WHICH TECHNIQUE?"), 8, 112)
|
Font.draw(Strings("WHICH TECHNIQUE?"), 8, 112)
|
||||||
end
|
end
|
||||||
|
if finishGlyph then finishGlyph() end
|
||||||
end
|
end
|
||||||
|
|
||||||
function BattleState:draw()
|
function BattleState:draw()
|
||||||
|
|||||||
+30
-8
@@ -41,6 +41,34 @@ local function badgeBoost(battler, stat)
|
|||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- engine/battle/core.asm:6454
|
||||||
|
function Damage.applyBadgeBoost(battler, stat, value)
|
||||||
|
if battler.hazeStatReset then return value end
|
||||||
|
local row = badgeBoost(battler, stat)
|
||||||
|
if not row then return value end
|
||||||
|
local extra = battler.badgeExtraBoosts and battler.badgeExtraBoosts[stat] or 0
|
||||||
|
for _ = 1, 1 + extra do
|
||||||
|
value = math.min(999, math.floor(value * (row.num or 9) / (row.den or 8)))
|
||||||
|
end
|
||||||
|
return value
|
||||||
|
end
|
||||||
|
|
||||||
|
-- engine/battle/effects.asm:498,689
|
||||||
|
function Damage.reapplyBadgeBoosts(battler, changedStat)
|
||||||
|
if not battler or not battler.badges then return end
|
||||||
|
local extra = battler.badgeExtraBoosts
|
||||||
|
if not extra then extra = {} battler.badgeExtraBoosts = extra end
|
||||||
|
for _, row in ipairs(battler.badgeBoosts or Damage.BADGE_BOOSTS) do
|
||||||
|
if battler.badges[row.badge] then
|
||||||
|
if row.stat == changedStat then
|
||||||
|
extra[row.stat] = 0
|
||||||
|
else
|
||||||
|
extra[row.stat] = (extra[row.stat] or 0) + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- the merged status record for a battler's persistent condition, or nil
|
-- the merged status record for a battler's persistent condition, or nil
|
||||||
local function statusRecord(battler)
|
local function statusRecord(battler)
|
||||||
return Status.recordFor(battler.statuses, battler.mon.status)
|
return Status.recordFor(battler.statuses, battler.mon.status)
|
||||||
@@ -178,14 +206,8 @@ function Damage.compute(ruleset, attacker, defender, move, opts)
|
|||||||
-- badge boosts (x9/8), engine/battle/core.asm ApplyBadgeStatBoosts:
|
-- badge boosts (x9/8), engine/battle/core.asm ApplyBadgeStatBoosts:
|
||||||
-- Boulder -> attack, Thunder -> defense, Soul -> speed (TurnOrder),
|
-- Boulder -> attack, Thunder -> defense, Soul -> speed (TurnOrder),
|
||||||
-- Volcano -> special
|
-- Volcano -> special
|
||||||
local atkBoost = badgeBoost(attacker, atkStat)
|
atk = Damage.applyBadgeBoost(attacker, atkStat, atk)
|
||||||
if atkBoost then
|
dfn = Damage.applyBadgeBoost(defender, defStat, dfn)
|
||||||
atk = math.floor(atk * (atkBoost.num or 9) / (atkBoost.den or 8))
|
|
||||||
end
|
|
||||||
local defBoost = badgeBoost(defender, defStat)
|
|
||||||
if defBoost then
|
|
||||||
dfn = math.floor(dfn * (defBoost.num or 9) / (defBoost.den or 8))
|
|
||||||
end
|
|
||||||
-- burn halves physical attack (applied as part of the stat in Gen 1;
|
-- burn halves physical attack (applied as part of the stat in Gen 1;
|
||||||
-- the status record's statPenalty names the stat it cuts).
|
-- the status record's statPenalty names the stat it cuts).
|
||||||
-- hazeStatReset suppresses it: Haze (haze.asm ResetStats) copied the
|
-- hazeStatReset suppresses it: Haze (haze.asm ResetStats) copied the
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
-- is the registry view of all three -- the merged Data.move_effects a
|
-- is the registry view of all three -- the merged Data.move_effects a
|
||||||
-- battle dispatches on serves these same objects.
|
-- battle dispatches on serves these same objects.
|
||||||
|
|
||||||
|
local Damage = require("src.battle.Damage")
|
||||||
local Logger = require("src.core.Logger")
|
local Logger = require("src.core.Logger")
|
||||||
local StatusRegistry = require("src.battle.StatusRegistry")
|
local StatusRegistry = require("src.battle.StatusRegistry")
|
||||||
local TurnOrder = require("src.battle.TurnOrder")
|
local TurnOrder = require("src.battle.TurnOrder")
|
||||||
@@ -57,6 +58,10 @@ local function changeStage(battle, who, stat, delta, fromEnemy)
|
|||||||
-- recomputed and QuarterSpeedDueToParalysis/HalveAttackDueToBurn re-run,
|
-- recomputed and QuarterSpeedDueToParalysis/HalveAttackDueToBurn re-run,
|
||||||
-- re-baking the burn/para penalty and ending Haze's temporary lift.
|
-- re-baking the burn/para penalty and ending Haze's temporary lift.
|
||||||
who.hazeStatReset = nil
|
who.hazeStatReset = nil
|
||||||
|
if battle.ruleset and battle.ruleset.badgeBoostReapplyBug
|
||||||
|
and battle.kind ~= "link" and who == battle.player then
|
||||||
|
Damage.reapplyBadgeBoosts(who, stat)
|
||||||
|
end
|
||||||
-- _MonsStatsRoseText/_MonsStatsFellText: "X's / STAT rose!"; the
|
-- _MonsStatsRoseText/_MonsStatsFellText: "X's / STAT rose!"; the
|
||||||
-- two-stage variants scroll "greatly" onto a third line
|
-- two-stage variants scroll "greatly" onto a third line
|
||||||
local label = Strings(STAT_LABEL[stat]) -- looked up here, not at require (#811)
|
local label = Strings(STAT_LABEL[stat]) -- looked up here, not at require (#811)
|
||||||
@@ -132,6 +137,11 @@ end
|
|||||||
|
|
||||||
local function statDownSide(stat)
|
local function statDownSide(stat)
|
||||||
return function(battle, user, target)
|
return function(battle, user, target)
|
||||||
|
-- engine/battle/effects.asm:552
|
||||||
|
if not user.isPlayer and battle.kind ~= "link"
|
||||||
|
and battle.rng(0, 255) < 64 then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
if target.substituteHP then return {} end
|
if target.substituteHP then return {} end
|
||||||
if battle.rng(0, 255) >= 85 then return {} end -- 33 percent + 1 (85/256)
|
if battle.rng(0, 255) >= 85 then return {} end -- 33 percent + 1 (85/256)
|
||||||
-- StatModifierDownEffect's side-effect branch never runs MoveHitTest,
|
-- StatModifierDownEffect's side-effect branch never runs MoveHitTest,
|
||||||
@@ -254,6 +264,7 @@ MoveEffects.primary = {
|
|||||||
-- Attack-halving and paralysis Speed-quartering on BOTH battlers
|
-- Attack-halving and paralysis Speed-quartering on BOTH battlers
|
||||||
-- until the next stat recompute (a stage change or switch-in).
|
-- until the next stat recompute (a stage change or switch-in).
|
||||||
b.hazeStatReset = true
|
b.hazeStatReset = true
|
||||||
|
b.badgeExtraBoosts = nil
|
||||||
end
|
end
|
||||||
-- Gen 1 also removes the enemy's major status; if that cured sleep
|
-- Gen 1 also removes the enemy's major status; if that cured sleep
|
||||||
-- or freeze, the target forfeits its move this turn (haze.asm
|
-- or freeze, the target forfeits its move this turn (haze.asm
|
||||||
|
|||||||
@@ -14,15 +14,7 @@ local function effectiveSpeed(battler)
|
|||||||
battler.stages and battler.stages.speed or 0)
|
battler.stages and battler.stages.speed or 0)
|
||||||
-- ApplyBadgeStatBoosts: the SOULBADGE boosts speed; the rows come from
|
-- ApplyBadgeStatBoosts: the SOULBADGE boosts speed; the rows come from
|
||||||
-- the battler's merged badgeBoosts with the vanilla list as fallback
|
-- the battler's merged badgeBoosts with the vanilla list as fallback
|
||||||
local badges = battler.badges
|
spd = Damage.applyBadgeBoost(battler, "speed", spd)
|
||||||
if badges then
|
|
||||||
for _, row in ipairs(battler.badgeBoosts or Damage.BADGE_BOOSTS) do
|
|
||||||
if row.stat == "speed" and badges[row.badge] then
|
|
||||||
spd = math.floor(spd * (row.num or 9) / (row.den or 8))
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
-- paralysis quarters speed (the status record's statPenalty);
|
-- paralysis quarters speed (the status record's statPenalty);
|
||||||
-- hazeStatReset suppresses it because Haze (haze.asm ResetStats)
|
-- hazeStatReset suppresses it because Haze (haze.asm ResetStats)
|
||||||
-- copied the unmodified speed over the quartered battle stat, lifting
|
-- copied the unmodified speed over the quartered battle stat, lifting
|
||||||
|
|||||||
@@ -26,14 +26,10 @@ local WideBattle = {
|
|||||||
FIELD_BOTTOM = 104,
|
FIELD_BOTTOM = 104,
|
||||||
}
|
}
|
||||||
|
|
||||||
-- The forced-mono display modes re-threshold the whole finished frame
|
|
||||||
-- through the shade shader, and picImage hands them raw DMG grays for that
|
|
||||||
-- (#207). The wide layout has to know: it exposes a matching whole-surface
|
|
||||||
-- zone and leaves the HP bar fill gray, exactly like the zone pass does in
|
|
||||||
-- the classic layout (#229). Keep in sync with picImage / ensureZones.
|
|
||||||
local function monoMode()
|
local function monoMode()
|
||||||
local m = PaletteFX.mode
|
local m = PaletteFX.mode
|
||||||
return m == "og" or m == "og_inv" or m == "classic"
|
return m == "og" or m == "og_inv" or m == "classic"
|
||||||
|
or PaletteFX.forcesRawGrays()
|
||||||
end
|
end
|
||||||
|
|
||||||
local function shownHP(battler)
|
local function shownHP(battler)
|
||||||
@@ -97,6 +93,19 @@ local function battleIsTopState(battle)
|
|||||||
return not (stack and stack.top) or stack:top() == battle
|
return not (stack and stack.top) or stack:top() == battle
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- engine/menus/party_menu.asm:4
|
||||||
|
local function coveredByOpaqueState(battle)
|
||||||
|
local stack = battle.game and battle.game.stack
|
||||||
|
local states = stack and stack.states
|
||||||
|
if not states then return false end
|
||||||
|
local above = false
|
||||||
|
for i = 1, #states do
|
||||||
|
if above and states[i] and states[i].isOpaque then return true end
|
||||||
|
if states[i] == battle then above = true end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
local function anchorHUD(battle, x, y, w, h, anchor)
|
local function anchorHUD(battle, x, y, w, h, anchor)
|
||||||
if not battle:extendedHUD() or not battleIsTopState(battle) then return end
|
if not battle:extendedHUD() or not battleIsTopState(battle) then return end
|
||||||
local renderer = battle.game and battle.game.renderer
|
local renderer = battle.game and battle.game.renderer
|
||||||
@@ -351,7 +360,7 @@ function WideBattle.draw(battle)
|
|||||||
g.rectangle("fill", 0, 0, WideBattle.WIDTH, WideBattle.HEIGHT)
|
g.rectangle("fill", 0, 0, WideBattle.WIDTH, WideBattle.HEIGHT)
|
||||||
end
|
end
|
||||||
-- AskName clears the field the same way the classic layout does
|
-- AskName clears the field the same way the classic layout does
|
||||||
if battle.blankForAskName then return end
|
if battle.blankForAskName or coveredByOpaqueState(battle) then return end
|
||||||
|
|
||||||
local fx = battle.fx
|
local fx = battle.fx
|
||||||
local sx = (fx and fx.shakeX) or 0
|
local sx = (fx and fx.shakeX) or 0
|
||||||
|
|||||||
@@ -645,6 +645,7 @@ function Battle:smartAiState()
|
|||||||
-- wPlayerSubStatus5 & SUBSTATUS_LOCK_ON: the enemy's OWN Lock-On, since
|
-- wPlayerSubStatus5 & SUBSTATUS_LOCK_ON: the enemy's OWN Lock-On, since
|
||||||
-- BattleCommand_LockOn sets the bit on the target it was aimed at.
|
-- BattleCommand_LockOn sets the bit on the target it was aimed at.
|
||||||
playerLockOn = playerState.lockOn or nil,
|
playerLockOn = playerState.lockOn or nil,
|
||||||
|
playerIdentified = playerState.identified or nil,
|
||||||
playerPhysicalMoves = physical,
|
playerPhysicalMoves = physical,
|
||||||
enemyRage = enemyState.rage,
|
enemyRage = enemyState.rage,
|
||||||
enemyRageCount = enemyState.rageCount,
|
enemyRageCount = enemyState.rageCount,
|
||||||
@@ -1082,7 +1083,7 @@ function Battle:hitOnce(attacker, defender, def, opts)
|
|||||||
local attackerStages = self.stages[self:sideOf(attacker)]
|
local attackerStages = self.stages[self:sideOf(attacker)]
|
||||||
local defenderStages = self.stages[self:sideOf(defender)]
|
local defenderStages = self.stages[self:sideOf(defender)]
|
||||||
local types = self.data.type_chart and self.data.type_chart.types
|
local types = self.data.type_chart and self.data.type_chart.types
|
||||||
local matchups = self.data.type_chart and self.data.type_chart.matchups
|
local matchups = self:matchupsAgainst(defender)
|
||||||
|
|
||||||
local heldEffect, heldParam = self:heldEffect(attacker, "damage")
|
local heldEffect, heldParam = self:heldEffect(attacker, "damage")
|
||||||
-- BattleCommand_Critical: SUBSTATUS_FOCUS_ENERGY (Focus Energy or a
|
-- BattleCommand_Critical: SUBSTATUS_FOCUS_ENERGY (Focus Energy or a
|
||||||
@@ -1202,6 +1203,8 @@ function Battle:hitOnce(attacker, defender, def, opts)
|
|||||||
if def.effect == "EFFECT_FALSE_SWIPE" and damage >= (defender.hp or 0) then
|
if def.effect == "EFFECT_FALSE_SWIPE" and damage >= (defender.hp or 0) then
|
||||||
damage = math.max(0, (defender.hp or 0) - 1)
|
damage = math.max(0, (defender.hp or 0) - 1)
|
||||||
end
|
end
|
||||||
|
-- engine/battle_anims/anim_commands.asm:1200
|
||||||
|
if self.moveEvent then self.moveEvent.effectiveness = info.effectiveness end
|
||||||
return self:dealDamage(attacker, defender, damage, {
|
return self:dealDamage(attacker, defender, damage, {
|
||||||
critical = critical, effectiveness = info.effectiveness,
|
critical = critical, effectiveness = info.effectiveness,
|
||||||
-- Counter answers physical damage and Mirror Coat special, so what kind
|
-- Counter answers physical damage and Mirror Coat special, so what kind
|
||||||
@@ -1690,6 +1693,13 @@ function Battle:useMove(attacker, defender, moveId)
|
|||||||
text = ("Magnitude %d!"):format(number) })
|
text = ("Magnitude %d!"):format(number) })
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- data/moves/effects.asm:1607, :1649
|
||||||
|
if def.effect == "EFFECT_RETURN" then
|
||||||
|
powerOverride = Effects.happinessPower(attacker.happiness)
|
||||||
|
elseif def.effect == "EFFECT_FRUSTRATION" then
|
||||||
|
powerOverride = Effects.happinessPower(attacker.happiness, true)
|
||||||
|
end
|
||||||
|
|
||||||
if not sureHit
|
if not sureHit
|
||||||
and not self:accuracyRoll(def, attacker, defender) then
|
and not self:accuracyRoll(def, attacker, defender) then
|
||||||
-- data/moves/effects.asm:148-151: `selfdestruct` sits between checkhit and
|
-- data/moves/effects.asm:148-151: `selfdestruct` sits between checkhit and
|
||||||
@@ -1744,7 +1754,7 @@ function Battle:useMove(attacker, defender, moveId)
|
|||||||
-- thing that stops SONIC BOOM, NIGHT SHADE or SUPER FANG.
|
-- thing that stops SONIC BOOM, NIGHT SHADE or SUPER FANG.
|
||||||
local defenderTypes = (self:speciesDef(defender) or {}).types
|
local defenderTypes = (self:speciesDef(defender) or {}).types
|
||||||
or defender.types
|
or defender.types
|
||||||
local matchups = self.data.type_chart and self.data.type_chart.matchups
|
local matchups = self:matchupsAgainst(defender)
|
||||||
if Damage.typeMultiplier(def.type, defenderTypes, matchups) == 0 then
|
if Damage.typeMultiplier(def.type, defenderTypes, matchups) == 0 then
|
||||||
self:markMissed()
|
self:markMissed()
|
||||||
self:emit({ kind = "message",
|
self:emit({ kind = "message",
|
||||||
@@ -2119,6 +2129,21 @@ Battle.MOVE_EFFECTS.EFFECT_LOCK_ON = function(self, attacker, defender)
|
|||||||
text = self:monName(attacker) .. " took aim!" })
|
text = self:monName(attacker) .. " took aim!" })
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- engine/battle/move_effects/foresight.asm
|
||||||
|
Battle.MOVE_EFFECTS.EFFECT_FORESIGHT = function(self, attacker, defender,
|
||||||
|
def, _, sureHit)
|
||||||
|
if not sureHit
|
||||||
|
and not self:accuracyRoll(def, attacker, defender) then
|
||||||
|
return fail(self)
|
||||||
|
end
|
||||||
|
local target = self:volatile(defender)
|
||||||
|
if target.vanished or target.identified then return fail(self) end
|
||||||
|
target.identified = true
|
||||||
|
self:emit({ kind = "message",
|
||||||
|
text = self:monName(attacker) .. " identified "
|
||||||
|
.. self:monName(defender) .. "!" })
|
||||||
|
end
|
||||||
|
|
||||||
-- BattleCommand_CheckHit's .LockOn: the flag is read AND cleared by the next
|
-- BattleCommand_CheckHit's .LockOn: the flag is read AND cleared by the next
|
||||||
-- move aimed at the mon carrying it, whether or not that move was the one the
|
-- move aimed at the mon carrying it, whether or not that move was the one the
|
||||||
-- lock-on was meant for, and whether or not the exception at :1683-1688 then
|
-- lock-on was meant for, and whether or not the exception at :1683-1688 then
|
||||||
@@ -2166,9 +2191,15 @@ function Battle:accuracyRoll(def, attacker, defender, accuracy)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function Battle:vanillaAccuracyRoll(accuracy, attacker, defender)
|
function Battle:vanillaAccuracyRoll(accuracy, attacker, defender)
|
||||||
return Damage.rollHit(self:moveAccuracy(accuracy, defender),
|
local acc = self.stages[self:sideOf(attacker)].accuracy
|
||||||
self.stages[self:sideOf(attacker)].accuracy,
|
local eva = self.stages[self:sideOf(defender)].evasion
|
||||||
self.stages[self:sideOf(defender)].evasion, self.random)
|
-- engine/battle/effect_commands.asm:1786
|
||||||
|
if defender and self:volatile(defender).identified
|
||||||
|
and (eva or 0) >= (acc or 0) then
|
||||||
|
acc, eva = 0, 0
|
||||||
|
end
|
||||||
|
return Damage.rollHit(self:moveAccuracy(accuracy, defender), acc, eva,
|
||||||
|
self.random)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- BattleCommand_StatDown's SUBSTATUS_MIST arm (a GUARD SPEC): a drop the FOE
|
-- BattleCommand_StatDown's SUBSTATUS_MIST arm (a GUARD SPEC): a drop the FOE
|
||||||
@@ -2354,7 +2385,7 @@ Battle.MOVE_EFFECTS.EFFECT_FUTURE_SIGHT = function(self, attacker, defender, def
|
|||||||
stages = self.stages[self:sideOf(defender)],
|
stages = self.stages[self:sideOf(defender)],
|
||||||
},
|
},
|
||||||
types = self.data.type_chart and self.data.type_chart.types,
|
types = self.data.type_chart and self.data.type_chart.types,
|
||||||
matchups = self.data.type_chart and self.data.type_chart.matchups,
|
matchups = self:matchupsAgainst(defender),
|
||||||
random = self.random,
|
random = self.random,
|
||||||
})
|
})
|
||||||
state.futureSight = Effects.FUTURE_SIGHT_TURNS
|
state.futureSight = Effects.FUTURE_SIGHT_TURNS
|
||||||
@@ -3072,6 +3103,30 @@ function Battle:safeguarded(mon)
|
|||||||
return (self.screens[self:sideOf(mon)].safeguard or 0) > 0
|
return (self.screens[self:sideOf(mon)].safeguard or 0) > 0
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- engine/battle/effect_commands.asm:1305
|
||||||
|
function Battle:matchupsAgainst(defender)
|
||||||
|
local chart = self.data.type_chart
|
||||||
|
local rows = chart and chart.matchups
|
||||||
|
if not rows or not defender then return rows end
|
||||||
|
if not self:volatile(defender).identified then return rows end
|
||||||
|
local skipped = chart.foresightMatchups
|
||||||
|
if not skipped or #skipped == 0 then return rows end
|
||||||
|
if not self.identifiedMatchups then
|
||||||
|
local drop = {}
|
||||||
|
for _, row in ipairs(skipped) do
|
||||||
|
drop[tostring(row.attacker) .. "/" .. tostring(row.defender)] = true
|
||||||
|
end
|
||||||
|
local out = {}
|
||||||
|
for _, row in ipairs(rows) do
|
||||||
|
if not drop[tostring(row.attacker) .. "/" .. tostring(row.defender)] then
|
||||||
|
out[#out + 1] = row
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self.identifiedMatchups = out
|
||||||
|
end
|
||||||
|
return self.identifiedMatchups
|
||||||
|
end
|
||||||
|
|
||||||
-- `source` is the battler that inflicted it, carried only so
|
-- `source` is the battler that inflicted it, carried only so
|
||||||
-- battle.status_inflicted can name it the way Gen 1's does.
|
-- battle.status_inflicted can name it the way Gen 1's does.
|
||||||
-- BattleCommand_Paralyze and BattleCommand_Poison refuse on a zero matchup,
|
-- BattleCommand_Paralyze and BattleCommand_Poison refuse on a zero matchup,
|
||||||
@@ -3084,7 +3139,7 @@ function Battle:statusRefusedByType(defender, moveType, status)
|
|||||||
end
|
end
|
||||||
local types = (self:speciesDef(defender) or {}).types or defender.types or {}
|
local types = (self:speciesDef(defender) or {}).types or defender.types or {}
|
||||||
if moveType then
|
if moveType then
|
||||||
local matchups = self.data.type_chart and self.data.type_chart.matchups
|
local matchups = self:matchupsAgainst(defender)
|
||||||
if Damage.typeMultiplier(moveType, types, matchups) == 0 then return true end
|
if Damage.typeMultiplier(moveType, types, matchups) == 0 then return true end
|
||||||
end
|
end
|
||||||
if status == "poison" or status == "toxic" then
|
if status == "poison" or status == "toxic" then
|
||||||
@@ -3917,6 +3972,8 @@ end
|
|||||||
-- check. Split out because playerAttack needs the same answer.
|
-- check. Split out because playerAttack needs the same answer.
|
||||||
function Battle:lockedInMove(mon)
|
function Battle:lockedInMove(mon)
|
||||||
local state = self:volatile(mon)
|
local state = self:volatile(mon)
|
||||||
|
-- engine/battle/core.asm:543
|
||||||
|
if state.chargeMove then return state.chargeMove end
|
||||||
if state.rolloutLock then return state.rolloutLock end
|
if state.rolloutLock then return state.rolloutLock end
|
||||||
if state.rampageMove and (state.rampageTurns or 0) > 0 then
|
if state.rampageMove and (state.rampageTurns or 0) > 0 then
|
||||||
return state.rampageMove
|
return state.rampageMove
|
||||||
|
|||||||
@@ -284,6 +284,14 @@ function Effects.magnitudePower(random)
|
|||||||
return last[2], last[3]
|
return last[2], last[3]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- engine/battle/move_effects/return.asm:1-24, frustration.asm:1-25
|
||||||
|
function Effects.happinessPower(happiness, frustration)
|
||||||
|
local h = happiness or 0
|
||||||
|
if h < 0 then h = 0 elseif h > 255 then h = 255 end
|
||||||
|
if frustration then h = 255 - h end
|
||||||
|
return math.floor(h * 10 / 25)
|
||||||
|
end
|
||||||
|
|
||||||
-- ------------------------------------------------------------------- weather
|
-- ------------------------------------------------------------------- weather
|
||||||
--
|
--
|
||||||
-- BattleCommand_StartRain / StartSun / StartSandstorm all set wWeatherCount to
|
-- BattleCommand_StartRain / StartSun / StartSandstorm all set wWeatherCount to
|
||||||
|
|||||||
@@ -58,9 +58,12 @@ Prize.AMULET_COIN = "AMULET_COIN"
|
|||||||
-- data/text/battle.asm. Declared here and formatted at the call site so
|
-- data/text/battle.asm. Declared here and formatted at the call site so
|
||||||
-- Strings.source is what registers them, the same way Decorations declares
|
-- Strings.source is what registers them, the same way Decorations declares
|
||||||
-- its own five. No line markers: every battle message in this port is one
|
-- its own five. No line markers: every battle message in this port is one
|
||||||
-- flowing string that Chrome.wrap breaks to the box.
|
-- flowing string that Chrome.wrap breaks to the box, except SentSomeToMomText,
|
||||||
|
-- which keeps the cart's own `line`/`cont` breaks because it does not fit two
|
||||||
|
-- rows.
|
||||||
local GOT_MONEY = Strings.source("%s got %s%d for winning!")
|
local GOT_MONEY = Strings.source("%s got %s%d for winning!")
|
||||||
local SENT_SOME = Strings.source("%s got %s%d for winning! Sent some to MOM!")
|
-- data/text/battle.asm:179-185
|
||||||
|
local SENT_SOME = Strings.source("%s got %s%d\nfor winning!\vSent some to MOM!")
|
||||||
-- The half and all texts really are this short on the cart: they replace the
|
-- The half and all texts really are this short on the cart: they replace the
|
||||||
-- money line rather than following it, which is a quirk no Gold player can
|
-- money line rather than following it, which is a quirk no Gold player can
|
||||||
-- see because BankOfMom only ever writes MOM_SAVING_SOME_MONEY_F.
|
-- see because BankOfMom only ever writes MOM_SAVING_SOME_MONEY_F.
|
||||||
|
|||||||
@@ -23,4 +23,6 @@ return {
|
|||||||
-- (core.asm:426-464): poison/burn/leech seed tick before the slower
|
-- (core.asm:426-464): poison/burn/leech seed tick before the slower
|
||||||
-- mon acts, not in an end-of-round sweep like Gen 3+.
|
-- mon acts, not in an end-of-round sweep like Gen 3+.
|
||||||
residualAfterMove = true,
|
residualAfterMove = true,
|
||||||
|
-- engine/battle/effects.asm:498,689
|
||||||
|
badgeBoostReapplyBug = true,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ local BATTLER_FIELDS = {
|
|||||||
"chargeReady", "invulnerable", "mustRecharge",
|
"chargeReady", "invulnerable", "mustRecharge",
|
||||||
"thrashTurns", "thrashAnnounced", "focusEnergy", "leechSeeded",
|
"thrashTurns", "thrashAnnounced", "focusEnergy", "leechSeeded",
|
||||||
"lightScreen", "reflect", "mist", "xAccuracy", "lastMove", "flinched",
|
"lightScreen", "reflect", "mist", "xAccuracy", "lastMove", "flinched",
|
||||||
"skipMove", "hazeStatReset", "drainFloor", "drainHold", "trappingTurns",
|
"skipMove", "hazeStatReset", "badgeExtraBoosts", "drainFloor", "drainHold", "trappingTurns",
|
||||||
"trapMove", "trapDamage", "fainted",
|
"trapMove", "trapDamage", "fainted",
|
||||||
"aiLayer2",
|
"aiLayer2",
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-7
@@ -82,8 +82,9 @@ local function copy(value)
|
|||||||
return out
|
return out
|
||||||
end
|
end
|
||||||
|
|
||||||
function Data:applyVersionedFieldData()
|
function Data:applyVersionedFieldData(version)
|
||||||
if require("src.core.GameVersion").isYellow() then
|
version = version or require("src.core.GameVersion").get()
|
||||||
|
if version == "yellow" then
|
||||||
self.field.trades = copy(YELLOW_TRADES)
|
self.field.trades = copy(YELLOW_TRADES)
|
||||||
-- The old man's catch demo is a RATTATA in Yellow
|
-- The old man's catch demo is a RATTATA in Yellow
|
||||||
-- (scripts/ViridianCity.asm ViridianCityOldManStartCatchTrainingScript
|
-- (scripts/ViridianCity.asm ViridianCityOldManStartCatchTrainingScript
|
||||||
@@ -106,7 +107,8 @@ end
|
|||||||
|
|
||||||
-- Fills only what the cache is missing, so an importer that learns to
|
-- Fills only what the cache is missing, so an importer that learns to
|
||||||
-- stamp one of these keys silently takes over from the engine.
|
-- stamp one of these keys silently takes over from the engine.
|
||||||
function Data:seedDefaults()
|
function Data:seedDefaults(version)
|
||||||
|
version = version or require("src.core.GameVersion").get()
|
||||||
local constants = self.constants or {}
|
local constants = self.constants or {}
|
||||||
self.constants = constants
|
self.constants = constants
|
||||||
self.field = self.field or {}
|
self.field = self.field or {}
|
||||||
@@ -131,7 +133,7 @@ function Data:seedDefaults()
|
|||||||
if constants.dexDigits == nil then
|
if constants.dexDigits == nil then
|
||||||
constants.dexDigits = math.max(3, #tostring(constants.dexSize))
|
constants.dexDigits = math.max(3, #tostring(constants.dexSize))
|
||||||
end
|
end
|
||||||
self:applyVersionedFieldData()
|
Data.applyVersionedFieldData(self, version)
|
||||||
local boot = self.field.boot
|
local boot = self.field.boot
|
||||||
if boot == nil then
|
if boot == nil then
|
||||||
boot = {}
|
boot = {}
|
||||||
@@ -144,7 +146,7 @@ function Data:seedDefaults()
|
|||||||
-- only the un-overridden default flips, so a total conversion that set
|
-- only the un-overridden default flips, so a total conversion that set
|
||||||
-- field.boot.screens.splash keeps its choice on any version.
|
-- field.boot.screens.splash keeps its choice on any version.
|
||||||
if boot.screens.splash == BOOT_DEFAULTS.screens.splash
|
if boot.screens.splash == BOOT_DEFAULTS.screens.splash
|
||||||
and require("src.core.GameVersion").isYellow() then
|
and version == "yellow" then
|
||||||
boot.screens.splash = "YellowIntro"
|
boot.screens.splash = "YellowIntro"
|
||||||
end
|
end
|
||||||
-- the naming screen presets the importer already extracts but nothing
|
-- the naming screen presets the importer already extracts but nothing
|
||||||
@@ -163,11 +165,15 @@ function Data:seedDefaults()
|
|||||||
-- extractor never writes headers for them. Seed the EVENT_BEAT_* /
|
-- extractor never writes headers for them. Seed the EVENT_BEAT_* /
|
||||||
-- after-battle rows so Blaine's SetEventRange deactivation and talk
|
-- after-battle rows so Blaine's SetEventRange deactivation and talk
|
||||||
-- after-text work like the other gyms (scripts/CinnabarGym.asm).
|
-- after-text work like the other gyms (scripts/CinnabarGym.asm).
|
||||||
self:seedCinnabarGymTrainerHeaders()
|
Data.seedCinnabarGymTrainerHeaders(self)
|
||||||
-- #197: the Fighting Dojo Karate Master is text_asm, so the extractor
|
-- #197: the Fighting Dojo Karate Master is text_asm, so the extractor
|
||||||
-- writes no header for him -- seed one so he engages on sight and has
|
-- writes no header for him -- seed one so he engages on sight and has
|
||||||
-- his defeat / re-talk lines (same idea as the Cinnabar seed above).
|
-- his defeat / re-talk lines (same idea as the Cinnabar seed above).
|
||||||
self:seedFightingDojoKarateMaster()
|
Data.seedFightingDojoKarateMaster(self)
|
||||||
|
-- #1743: Mt. Moon B2F Super Nerd is text_asm (no def_trainers), so Yellow's
|
||||||
|
-- extractor never writes his header -- seed one so engageTrainer finds
|
||||||
|
-- battle/won/after text instead of the "I like shorts!" fallback.
|
||||||
|
Data.seedMtMoonB2FSuperNerd(self)
|
||||||
-- #189: 1F cabin door order vs rooms map (survey zoom)
|
-- #189: 1F cabin door order vs rooms map (survey zoom)
|
||||||
require("src.world.SsAnneLayout").apply(self.maps)
|
require("src.world.SsAnneLayout").apply(self.maps)
|
||||||
end
|
end
|
||||||
@@ -199,6 +205,25 @@ function Data:seedFightingDojoKarateMaster()
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Mt. Moon B2F Super Nerd (object index 1) is text_asm with no def_trainers
|
||||||
|
-- row, so Yellow never gets a trainerHeaders.MtMoonB2F[1] entry (Red/Blue
|
||||||
|
-- pin one in make_rom_manifest.py). Without it, engageTrainer falls through
|
||||||
|
-- to the hard-coded "I like shorts!" fallback (#1743). trainerDefeated still
|
||||||
|
-- tracks him via defeatedTrainers[npc.id]; the fabricated event name matches
|
||||||
|
-- the shipped Red/Blue manifest pin for consistency with fossil drivers.
|
||||||
|
function Data:seedMtMoonB2FSuperNerd()
|
||||||
|
local headers = self.trainer_headers
|
||||||
|
if not headers then return end
|
||||||
|
headers.MtMoonB2F = headers.MtMoonB2F or {}
|
||||||
|
if headers.MtMoonB2F[1] then return end
|
||||||
|
headers.MtMoonB2F[1] = {
|
||||||
|
battle = "_MtMoonB2FSuperNerdTheyreBothMineText",
|
||||||
|
won = "_MtMoonB2FSuperNerdOkIllShareText",
|
||||||
|
after = "_MtMoonB2FSuperNerdTheresAPokemonLabText",
|
||||||
|
event = "EVENT_BEAT_MT_MOON_3_SUPER_NERD",
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
function Data:seedCinnabarGymTrainerHeaders()
|
function Data:seedCinnabarGymTrainerHeaders()
|
||||||
local headers = self.trainer_headers
|
local headers = self.trainer_headers
|
||||||
if not headers or headers.CinnabarGym then return end
|
if not headers or headers.CinnabarGym then return end
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
-- Canonical pure shaping shared by the active boot and inactive dataset views.
|
||||||
|
-- The caller supplies both the data table and selected version; no global
|
||||||
|
-- GameVersion, cache prefix, mount, or active Data table is changed.
|
||||||
|
|
||||||
|
local GameVersion = require("src.core.GameVersion")
|
||||||
|
|
||||||
|
local DatasetHydration = {}
|
||||||
|
|
||||||
|
function DatasetHydration.applyGen2(data, moduleLoader)
|
||||||
|
local chart = data.type_chart or {}
|
||||||
|
chart.matchups = chart.matchups or {}
|
||||||
|
for _, row in ipairs(chart.foresightMatchups or {}) do
|
||||||
|
chart.matchups[#chart.matchups + 1] = row
|
||||||
|
end
|
||||||
|
data.type_chart = chart
|
||||||
|
data.gen2HeldItems =
|
||||||
|
(moduleLoader or require)("src.core.gen2.ItemEffects").heldItemsFrom(data.items)
|
||||||
|
return data
|
||||||
|
end
|
||||||
|
|
||||||
|
function DatasetHydration.apply(data, version, moduleLoader)
|
||||||
|
if GameVersion.generation(version) == 2 then
|
||||||
|
return DatasetHydration.applyGen2(data, moduleLoader)
|
||||||
|
end
|
||||||
|
require("src.core.Data").seedDefaults(data, version)
|
||||||
|
return data
|
||||||
|
end
|
||||||
|
|
||||||
|
return DatasetHydration
|
||||||
+19
-1
@@ -6,6 +6,9 @@ local FixedStep = {}
|
|||||||
|
|
||||||
FixedStep.STEP = 1 / 60
|
FixedStep.STEP = 1 / 60
|
||||||
local MAX_ACCUM = 0.25 -- avoid spiral of death after a stall
|
local MAX_ACCUM = 0.25 -- avoid spiral of death after a stall
|
||||||
|
local SMOOTH_FRAMES = 4
|
||||||
|
local SMOOTH_MAX = 1 / 60 * 2.5
|
||||||
|
local STEP_EPS = 1 / 60 * 0.02
|
||||||
|
|
||||||
-- Phase the accumulator is re-seeded with once an absorbed hitch frame has
|
-- Phase the accumulator is re-seeded with once an absorbed hitch frame has
|
||||||
-- been paid for. Half a step is the balanced point: a frame has to come in
|
-- been paid for. Half a step is the balanced point: a frame has to come in
|
||||||
@@ -23,6 +26,7 @@ function FixedStep:init(callback)
|
|||||||
self.accum = 0
|
self.accum = 0
|
||||||
self.callback = callback
|
self.callback = callback
|
||||||
self.suppressCatchup = false
|
self.suppressCatchup = false
|
||||||
|
self.dtHistory, self.dtSum = nil, 0
|
||||||
end
|
end
|
||||||
|
|
||||||
-- The anti-spiral clamp doubles as a steps-per-frame ceiling (0.25s = 15
|
-- The anti-spiral clamp doubles as a steps-per-frame ceiling (0.25s = 15
|
||||||
@@ -39,12 +43,26 @@ function FixedStep:update(dt)
|
|||||||
-- the burst it would otherwise release doesn't play out as a slide.
|
-- the burst it would otherwise release doesn't play out as a slide.
|
||||||
if self.suppressCatchup then
|
if self.suppressCatchup then
|
||||||
self.suppressCatchup = false
|
self.suppressCatchup = false
|
||||||
|
self.dtHistory, self.dtSum = nil, 0
|
||||||
self.accum = self.STEP * RESEED_PHASE
|
self.accum = self.STEP * RESEED_PHASE
|
||||||
self.callback(self.STEP)
|
self.callback(self.STEP)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
if dt > 0 and dt <= SMOOTH_MAX then
|
||||||
|
local hist = self.dtHistory
|
||||||
|
if not hist then hist = {}; self.dtHistory = hist; self.dtSum = 0 end
|
||||||
|
hist[#hist + 1] = dt
|
||||||
|
self.dtSum = self.dtSum + dt
|
||||||
|
if #hist > SMOOTH_FRAMES then
|
||||||
|
self.dtSum = self.dtSum - hist[1]
|
||||||
|
table.remove(hist, 1)
|
||||||
|
end
|
||||||
|
dt = self.dtSum / #hist
|
||||||
|
else
|
||||||
|
self.dtHistory, self.dtSum = nil, 0
|
||||||
|
end
|
||||||
self.accum = math.min(self.accum + dt, self.maxAccum or MAX_ACCUM)
|
self.accum = math.min(self.accum + dt, self.maxAccum or MAX_ACCUM)
|
||||||
while self.accum >= self.STEP do
|
while self.accum >= self.STEP - STEP_EPS do
|
||||||
self.accum = self.accum - self.STEP
|
self.accum = self.accum - self.STEP
|
||||||
self.callback(self.STEP)
|
self.callback(self.STEP)
|
||||||
end
|
end
|
||||||
|
|||||||
+16
-12
@@ -1258,6 +1258,7 @@ function Game:applyOptions(opts)
|
|||||||
if Sound.applyOptions then Sound.applyOptions(opts) end
|
if Sound.applyOptions then Sound.applyOptions(opts) end
|
||||||
require("src.render.PaletteFX").applyOptions(opts)
|
require("src.render.PaletteFX").applyOptions(opts)
|
||||||
require("src.render.Tilt").applyOptions(opts)
|
require("src.render.Tilt").applyOptions(opts)
|
||||||
|
require("src.render.Letterbox").applyOptions(opts)
|
||||||
-- after Tilt, so a persisted world pipeline can switch the tilt level it
|
-- after Tilt, so a persisted world pipeline can switch the tilt level it
|
||||||
-- just restored back off (the two are mutually exclusive)
|
-- just restored back off (the two are mutually exclusive)
|
||||||
require("src.render.Pipelines").applyOptions(opts)
|
require("src.render.Pipelines").applyOptions(opts)
|
||||||
@@ -1324,9 +1325,7 @@ function Game:restoreSave(loaded, recovered, opts)
|
|||||||
self:adoptSave(loaded)
|
self:adoptSave(loaded)
|
||||||
-- SaveData.load already attached the standalone options.lua table
|
-- SaveData.load already attached the standalone options.lua table
|
||||||
self:applyOptions(loaded.options)
|
self:applyOptions(loaded.options)
|
||||||
-- saves from before OT/ID stamping: backfill with the player's (after
|
-- saves from before OT/ID stamping: backfill with the player's
|
||||||
-- the scrub, so every mon the stamp loop sees is known)
|
|
||||||
SaveData.repairTradedOtIds(loaded)
|
|
||||||
local stamp = require("src.battle.BattleState").stampOT
|
local stamp = require("src.battle.BattleState").stampOT
|
||||||
for _, mon in ipairs(loaded.party or {}) do stamp(loaded, mon) end
|
for _, mon in ipairs(loaded.party or {}) do stamp(loaded, mon) end
|
||||||
for _, box in ipairs(loaded.boxes or {}) do
|
for _, box in ipairs(loaded.boxes or {}) do
|
||||||
@@ -1389,8 +1388,13 @@ end
|
|||||||
|
|
||||||
-- Drop every session-owned field so the next Game:load() starts clean when
|
-- Drop every session-owned field so the next Game:load() starts clean when
|
||||||
-- the process returns to the launcher in-place (Android / intent_game).
|
-- the process returns to the launcher in-place (Android / intent_game).
|
||||||
-- main.lua must not guess field names: new systems (Game.network, …) are
|
--
|
||||||
-- cleared automatically because only functions (methods) are kept.
|
-- Gen1 Game is a MODULE SINGLETON (methods live on this table). Never fan
|
||||||
|
-- out arbitrary field:release() here: session fields can hold shared modules
|
||||||
|
-- (Fetch, SyncClient, …) whose :release is a job-handle API, not instance
|
||||||
|
-- teardown -- calling them as value:release() corrupts process state and has
|
||||||
|
-- been observed to leave Game.load nil after EXIT GAME on Android.
|
||||||
|
-- Explicit GPU owners are released below; everything else is just dropped.
|
||||||
function Game:reset()
|
function Game:reset()
|
||||||
if self.stack and self.stack.clear then
|
if self.stack and self.stack.clear then
|
||||||
pcall(function() self.stack:clear() end)
|
pcall(function() self.stack:clear() end)
|
||||||
@@ -1403,23 +1407,23 @@ function Game:reset()
|
|||||||
if canvas and canvas.release then pcall(canvas.release, canvas) end
|
if canvas and canvas.release then pcall(canvas.release, canvas) end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if self.renderer and self.renderer.releaseCanvases then
|
if self.renderer then
|
||||||
pcall(function() self.renderer:releaseCanvases() end)
|
local release = self.renderer.releaseCanvases or self.renderer.release
|
||||||
|
if release then pcall(release, self.renderer) end
|
||||||
end
|
end
|
||||||
|
-- Keep methods; clear every other field (including session scalars like
|
||||||
|
-- speedOverride). Re-seed module constants afterward.
|
||||||
|
local skinFast = self.SKIN_FAST_FORWARD
|
||||||
local keys = {}
|
local keys = {}
|
||||||
for key, value in pairs(self) do
|
for key, value in pairs(self) do
|
||||||
if type(value) ~= "function" then
|
if type(value) ~= "function" then
|
||||||
if key ~= "world" and key ~= "renderer" and key ~= "_canvases" then
|
|
||||||
if type(value) == "table" and value.release then
|
|
||||||
pcall(value.release, value)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
keys[#keys + 1] = key
|
keys[#keys + 1] = key
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
for _, key in ipairs(keys) do
|
for _, key in ipairs(keys) do
|
||||||
self[key] = nil
|
self[key] = nil
|
||||||
end
|
end
|
||||||
|
self.SKIN_FAST_FORWARD = skinFast or 4
|
||||||
end
|
end
|
||||||
|
|
||||||
return Game
|
return Game
|
||||||
|
|||||||
+11
-7
@@ -350,6 +350,10 @@ function Game2:showTitle()
|
|||||||
onContinue = function()
|
onContinue = function()
|
||||||
self:showMainMenu()
|
self:showMainMenu()
|
||||||
end,
|
end,
|
||||||
|
-- engine/menus/intro_menu.asm:848-889
|
||||||
|
onTimeout = function()
|
||||||
|
self:showCopyright()
|
||||||
|
end,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1783,6 +1787,7 @@ function Game2:hotkey(key)
|
|||||||
local GbcPalette = require("src.render.GbcPalette")
|
local GbcPalette = require("src.render.GbcPalette")
|
||||||
GbcPalette.setMode(options.color or "gbc")
|
GbcPalette.setMode(options.color or "gbc")
|
||||||
options.color = GbcPalette.cycle(1)
|
options.color = GbcPalette.cycle(1)
|
||||||
|
options.palette = ""
|
||||||
persist()
|
persist()
|
||||||
return true
|
return true
|
||||||
elseif key == "3" then
|
elseif key == "3" then
|
||||||
@@ -2036,6 +2041,7 @@ function Game2:applyOptions()
|
|||||||
Zoom.allowSurvey = caps.survey
|
Zoom.allowSurvey = caps.survey
|
||||||
if not caps.survey and Zoom.offset < 0 then Zoom.offset = 0 end
|
if not caps.survey and Zoom.offset < 0 then Zoom.offset = 0 end
|
||||||
require("src.render.Tilt").applyOptions(options)
|
require("src.render.Tilt").applyOptions(options)
|
||||||
|
require("src.render.Letterbox").applyOptions(options)
|
||||||
require("src.render.GbcPalette").applyOptions(options)
|
require("src.render.GbcPalette").applyOptions(options)
|
||||||
-- engine/gfx/load_font.asm:29 LoadFrame, off options.lua's wTextboxFrame.
|
-- engine/gfx/load_font.asm:29 LoadFrame, off options.lua's wTextboxFrame.
|
||||||
Font.setFrame(options.frame or 1)
|
Font.setFrame(options.frame or 1)
|
||||||
@@ -2204,6 +2210,8 @@ end
|
|||||||
-- In-process return-to-launcher (Android / intent_game): drop session fields
|
-- In-process return-to-launcher (Android / intent_game): drop session fields
|
||||||
-- so a later Game2.new() + load is not sharing a live stack or mod loader.
|
-- so a later Game2.new() + load is not sharing a live stack or mod loader.
|
||||||
-- Methods live on the class table; pairs(self) only sees instance state.
|
-- Methods live on the class table; pairs(self) only sees instance state.
|
||||||
|
-- Same rule as Gen1: only release known GPU owners -- never fan out
|
||||||
|
-- arbitrary field:release() (shared modules use :release as a handle API).
|
||||||
function Game2:reset()
|
function Game2:reset()
|
||||||
if self.stack and self.stack.clear then
|
if self.stack and self.stack.clear then
|
||||||
pcall(function() self.stack:clear() end)
|
pcall(function() self.stack:clear() end)
|
||||||
@@ -2216,17 +2224,13 @@ function Game2:reset()
|
|||||||
if canvas and canvas.release then pcall(canvas.release, canvas) end
|
if canvas and canvas.release then pcall(canvas.release, canvas) end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if self.renderer and self.renderer.releaseCanvases then
|
if self.renderer then
|
||||||
pcall(function() self.renderer:releaseCanvases() end)
|
local release = self.renderer.releaseCanvases or self.renderer.release
|
||||||
|
if release then pcall(release, self.renderer) end
|
||||||
end
|
end
|
||||||
local keys = {}
|
local keys = {}
|
||||||
for key, value in pairs(self) do
|
for key, value in pairs(self) do
|
||||||
if type(value) ~= "function" then
|
if type(value) ~= "function" then
|
||||||
if key ~= "world" and key ~= "renderer" and key ~= "_canvases" then
|
|
||||||
if type(value) == "table" and value.release then
|
|
||||||
pcall(value.release, value)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
keys[#keys + 1] = key
|
keys[#keys + 1] = key
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+18
-1
@@ -57,7 +57,10 @@ function GamepadMap._setForceNXForTests(v)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function nxActive()
|
local function nxActive()
|
||||||
if GamepadMap._forceNXForTests then return true end
|
if GamepadMap._forceNXForTests == true then return true end
|
||||||
|
if GamepadMap._forceNXForTests == false and love and (love._os ~= "NX" and (not love.system or love.system.getOS() ~= "NX")) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
if love and love._os == "NX" then return true end
|
if love and love._os == "NX" then return true end
|
||||||
if love and love.system and love.system.getOS() == "NX" then return true end
|
if love and love.system and love.system.getOS() == "NX" then return true end
|
||||||
return false
|
return false
|
||||||
@@ -78,6 +81,20 @@ function GamepadMap.mapGamepadButton(button)
|
|||||||
return GamepadMap.gamepadBindings()[button]
|
return GamepadMap.gamepadBindings()[button]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function GamepadMap.mapLauncherButton(button)
|
||||||
|
if nxActive() then
|
||||||
|
if button == "a" then return "b"
|
||||||
|
elseif button == "b" then return "a"
|
||||||
|
elseif button == "x" then return "y"
|
||||||
|
elseif button == "y" then return "x"
|
||||||
|
elseif button == "back" then return "select"
|
||||||
|
end
|
||||||
|
elseif button == "back" then
|
||||||
|
return "select"
|
||||||
|
end
|
||||||
|
return button
|
||||||
|
end
|
||||||
|
|
||||||
-- Select+face display chords (docs / Nintendo UX):
|
-- Select+face display chords (docs / Nintendo UX):
|
||||||
-- Select+A → "2" (COLORS), Select+B → "3" (TILT),
|
-- Select+A → "2" (COLORS), Select+B → "3" (TILT),
|
||||||
-- Select+Y → "5", Select+X → "6", Select+L (leftshoulder) → "7".
|
-- Select+Y → "5", Select+X → "6", Select+L (leftshoulder) → "7".
|
||||||
|
|||||||
+141
-22
@@ -4,18 +4,79 @@ local HostShell = {}
|
|||||||
|
|
||||||
-- Our AppRun exports LD_LIBRARY_PATH="$APPDIR/lib:..." so every subprocess we
|
-- Our AppRun exports LD_LIBRARY_PATH="$APPDIR/lib:..." so every subprocess we
|
||||||
-- spawn tries to link against the libraries we're shipping instead of the
|
-- spawn tries to link against the libraries we're shipping instead of the
|
||||||
-- system ones. We want to unset the var so that any system tools can find
|
-- system ones. Host tools (curl, zenity) need that unset. A *bundled* AppDir
|
||||||
-- their proper libraries. Only needed when running in an AppImage.
|
-- curl must keep APPDIR on LD_LIBRARY_PATH so it resolves the bundled
|
||||||
-- LD_PRELOAD is Steam's overlay (#1470): its 32-bit half cannot load into a
|
-- libssl/libcrypto -- scrubbing it forces host OpenSSL and segfaults across
|
||||||
-- 64-bit child, so ld.so prints an error into that child's output.
|
-- distros. LD_PRELOAD is always scrubbed: Steam's overlay (#1470) cannot load
|
||||||
|
-- into a 64-bit child.
|
||||||
function HostShell.envPrefix()
|
function HostShell.envPrefix()
|
||||||
|
return HostShell.curlEnvPrefix("host")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- kind "bundled" = AppDir / Flatpak curl (keep LD_LIBRARY_PATH, scrub preload).
|
||||||
|
-- kind "host" = system curl (scrub both).
|
||||||
|
function HostShell.curlEnvPrefix(kind)
|
||||||
local unset = ""
|
local unset = ""
|
||||||
if os.getenv("APPIMAGE") then unset = unset .. "-u LD_LIBRARY_PATH " end
|
if kind ~= "bundled" and os.getenv("APPIMAGE") then
|
||||||
|
unset = unset .. "-u LD_LIBRARY_PATH "
|
||||||
|
end
|
||||||
if os.getenv("LD_PRELOAD") then unset = unset .. "-u LD_PRELOAD " end
|
if os.getenv("LD_PRELOAD") then unset = unset .. "-u LD_PRELOAD " end
|
||||||
if unset == "" then return "" end
|
if unset == "" then return "" end
|
||||||
return "env " .. unset
|
return "env " .. unset
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local curlResolved = nil -- { path=, kind= } once per Lua state
|
||||||
|
|
||||||
|
local function pathIsExecutable(path)
|
||||||
|
if type(path) ~= "string" or path == "" then return false end
|
||||||
|
local f = io.open(path, "rb")
|
||||||
|
if not f then return false end
|
||||||
|
f:close()
|
||||||
|
-- Best-effort: existence is enough; exec bit is checked by the spawn.
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Prefer Flatpak /app/bin/curl, then $APPDIR/{usr/,}bin/curl, else host "curl".
|
||||||
|
function HostShell.resolveCurl()
|
||||||
|
if curlResolved ~= nil then return curlResolved.path, curlResolved.kind end
|
||||||
|
local candidates = {}
|
||||||
|
if os.getenv("FLATPAK_ID") then
|
||||||
|
candidates[#candidates + 1] = { "/app/bin/curl", "bundled" }
|
||||||
|
end
|
||||||
|
local appdir = os.getenv("APPDIR")
|
||||||
|
if type(appdir) == "string" and appdir ~= "" then
|
||||||
|
candidates[#candidates + 1] = { appdir .. "/usr/bin/curl", "bundled" }
|
||||||
|
candidates[#candidates + 1] = { appdir .. "/bin/curl", "bundled" }
|
||||||
|
end
|
||||||
|
for _, c in ipairs(candidates) do
|
||||||
|
if pathIsExecutable(c[1]) then
|
||||||
|
curlResolved = { path = c[1], kind = c[2] }
|
||||||
|
return curlResolved.path, curlResolved.kind
|
||||||
|
end
|
||||||
|
end
|
||||||
|
curlResolved = { path = "curl", kind = "host" }
|
||||||
|
return curlResolved.path, curlResolved.kind
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Quoted curl argv0 for a shell command, plus the matching env prefix.
|
||||||
|
function HostShell.curlInvocation()
|
||||||
|
local path, kind = HostShell.resolveCurl()
|
||||||
|
return HostShell.curlEnvPrefix(kind) .. HostShell.quote(path), kind
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Diagnostics for support / About screens.
|
||||||
|
function HostShell.curlDiagnostics()
|
||||||
|
local path, kind = HostShell.resolveCurl()
|
||||||
|
return {
|
||||||
|
path = path,
|
||||||
|
kind = kind,
|
||||||
|
appimage = os.getenv("APPIMAGE") ~= nil,
|
||||||
|
appdir = os.getenv("APPDIR"),
|
||||||
|
flatpakId = os.getenv("FLATPAK_ID"),
|
||||||
|
haveCurl = HostShell.haveCurl(),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
-- Windows: every host tool we shell out to (curl for the update and mod-index
|
-- Windows: every host tool we shell out to (curl for the update and mod-index
|
||||||
-- fetches, the PowerShell ROM picker, the update downloader's `start /b`) is
|
-- fetches, the PowerShell ROM picker, the update downloader's `start /b`) is
|
||||||
-- spawned through io.popen / os.execute, which run it under cmd.exe. A
|
-- spawned through io.popen / os.execute, which run it under cmd.exe. A
|
||||||
@@ -126,12 +187,19 @@ local function withPopenLock(fn)
|
|||||||
if not okAtomic then fn() end
|
if not okAtomic then fn() end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Wraps io.popen with the AppImage env fix applied and lua errors swallowed
|
-- Wraps io.popen with the AppImage env fix applied and lua errors swallowed.
|
||||||
function HostShell.popen(command, mode)
|
-- opts.envPrefix overrides the default HostShell.envPrefix() (pass "" to skip,
|
||||||
|
-- or curlEnvPrefix(kind) when spawning a resolved curl binary).
|
||||||
|
function HostShell.popen(command, mode, opts)
|
||||||
HostShell.releasePointerGrab()
|
HostShell.releasePointerGrab()
|
||||||
|
local prefix = HostShell.envPrefix()
|
||||||
|
if type(opts) == "table" and opts.envPrefix ~= nil then
|
||||||
|
prefix = opts.envPrefix
|
||||||
|
end
|
||||||
local pipe
|
local pipe
|
||||||
|
local line = HostShell.shellCommand(prefix .. command)
|
||||||
withPopenLock(function()
|
withPopenLock(function()
|
||||||
local ok, p = pcall(io.popen, HostShell.envPrefix() .. command, mode or "r")
|
local ok, p = pcall(io.popen, line, mode or "r")
|
||||||
pipe = (ok and p) or nil
|
pipe = (ok and p) or nil
|
||||||
end)
|
end)
|
||||||
return pipe
|
return pipe
|
||||||
@@ -158,7 +226,15 @@ end
|
|||||||
-- and the app dies. There we relaunch through the GameActivity.restartApp
|
-- and the app dies. There we relaunch through the GameActivity.restartApp
|
||||||
-- JNI bridge (love.system.restartApp), which schedules our launch intent
|
-- JNI bridge (love.system.restartApp), which schedules our launch intent
|
||||||
-- and kills the process so no native state can leak into the fresh run.
|
-- and kills the process so no native state can leak into the fresh run.
|
||||||
-- On every other platform the in-process restart works, so keep it.
|
-- iOS is the same class of problem with a sharper edge: love.cpp under
|
||||||
|
-- LOVE_IOS forces DONE_RESTART for *every* quit (Apple forbids programmatic
|
||||||
|
-- exit) and comments that leftover threads make that restart unreliable --
|
||||||
|
-- which our ChipAudio / Fetch / Check workers are. There is no
|
||||||
|
-- restartApp bridge on iOS, so callers that want "back to launcher" must
|
||||||
|
-- use main.lua's in-process returnToLauncher (love.quit aborts the quit);
|
||||||
|
-- HostShell.restart itself refuses quit("restart") and falls back to a
|
||||||
|
-- bare quit() so a mod that still calls restart does not pick the worst
|
||||||
|
-- path on purpose.
|
||||||
function HostShell.restart()
|
function HostShell.restart()
|
||||||
if not (love and love.event and love.event.quit) then return end
|
if not (love and love.event and love.event.quit) then return end
|
||||||
|
|
||||||
@@ -174,9 +250,23 @@ function HostShell.restart()
|
|||||||
love.event.quit()
|
love.event.quit()
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
if osName == "iOS" then
|
||||||
|
-- No process-kill bridge. A bare quit still becomes DONE_RESTART in
|
||||||
|
-- love.cpp, but quit("restart") is the path that also runs our
|
||||||
|
-- endProcess worker joins first and then re-enters runlove -- the
|
||||||
|
-- combination that crashes EXIT GAME. Prefer the softer quit.
|
||||||
|
love.event.quit()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
local appimage = os.getenv("APPIMAGE")
|
local appimage = os.getenv("APPIMAGE")
|
||||||
if not appimage then
|
if not appimage then
|
||||||
|
-- Flatpak and plain desktop: love.event.quit("restart") uses
|
||||||
|
-- execv(/proc/self/exe). Inside bwrap that path is valid, but PhysFS
|
||||||
|
-- locks on a mounted .love payload can survive the exec boundary and
|
||||||
|
-- hang the next Boot.mount. Callers that just wrote updates/*.love
|
||||||
|
-- should already have closed archive handles; Boot.run still owns
|
||||||
|
-- crash-guard rollback if a mid-handoff restart dies.
|
||||||
love.event.quit("restart")
|
love.event.quit("restart")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -274,6 +364,18 @@ local function fetchError(url, status, noise)
|
|||||||
return ("fetch failed for %s: %s"):format(url, why)
|
return ("fetch failed for %s: %s"):format(url, why)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function HostShell.isWindows()
|
||||||
|
return (love and love.system and love.system.getOS
|
||||||
|
and love.system.getOS() == "Windows") or false
|
||||||
|
end
|
||||||
|
|
||||||
|
function HostShell.shellCommand(command)
|
||||||
|
command = tostring(command)
|
||||||
|
if not HostShell.isWindows() then return command end
|
||||||
|
if command:sub(1, 1) ~= '"' then return command end
|
||||||
|
return '"' .. command .. '"'
|
||||||
|
end
|
||||||
|
|
||||||
-- Shell quoting for one curl argument; cmd.exe has no single-quote form.
|
-- Shell quoting for one curl argument; cmd.exe has no single-quote form.
|
||||||
function HostShell.quote(s)
|
function HostShell.quote(s)
|
||||||
s = tostring(s)
|
s = tostring(s)
|
||||||
@@ -325,7 +427,10 @@ local curlAvailable = nil
|
|||||||
|
|
||||||
function HostShell.haveCurl()
|
function HostShell.haveCurl()
|
||||||
if curlAvailable ~= nil then return curlAvailable end
|
if curlAvailable ~= nil then return curlAvailable end
|
||||||
local pipe = HostShell.popen("curl --version")
|
local path, kind = HostShell.resolveCurl()
|
||||||
|
local pipe = HostShell.popen(
|
||||||
|
HostShell.quote(path) .. " --version", "r",
|
||||||
|
{ envPrefix = HostShell.curlEnvPrefix(kind) })
|
||||||
if not pipe then curlAvailable = false return false end
|
if not pipe then curlAvailable = false return false end
|
||||||
local readOk, out = pcall(function() return pipe:read("*a") end)
|
local readOk, out = pcall(function() return pipe:read("*a") end)
|
||||||
HostShell.pclose(pipe)
|
HostShell.pclose(pipe)
|
||||||
@@ -333,6 +438,12 @@ function HostShell.haveCurl()
|
|||||||
return curlAvailable
|
return curlAvailable
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function curlCmd(flags)
|
||||||
|
local path, kind = HostShell.resolveCurl()
|
||||||
|
return HostShell.quote(path) .. " " .. flags,
|
||||||
|
{ envPrefix = HostShell.curlEnvPrefix(kind) }
|
||||||
|
end
|
||||||
|
|
||||||
-- An older mobile build reports nil here and falls back to the "no transport"
|
-- An older mobile build reports nil here and falls back to the "no transport"
|
||||||
-- error the callers already show.
|
-- error the callers already show.
|
||||||
local function haveBridge()
|
local function haveBridge()
|
||||||
@@ -393,9 +504,11 @@ function HostShell.httpDownload(url, absPath, userAgent, accept, maxTime, etagPa
|
|||||||
if type(absPath) ~= "string" or absPath == "" then return nil, "missing path" end
|
if type(absPath) ~= "string" or absPath == "" then return nil, "missing path" end
|
||||||
userAgent = userAgent or "gen1recomp"
|
userAgent = userAgent or "gen1recomp"
|
||||||
if HostShell.haveCurl() then
|
if HostShell.haveCurl() then
|
||||||
local cmd = ("curl -fsSL --proto =http,https --proto-redir =http,https "
|
local head, popts = curlCmd(
|
||||||
|
("-fsSL --proto =http,https --proto-redir =http,https "
|
||||||
.. "--connect-timeout 15 --max-time %d ")
|
.. "--connect-timeout 15 --max-time %d ")
|
||||||
:format(tonumber(maxTime) or 300)
|
:format(tonumber(maxTime) or 300))
|
||||||
|
local cmd = head
|
||||||
.. "-H " .. HostShell.quote("User-Agent: " .. userAgent) .. " "
|
.. "-H " .. HostShell.quote("User-Agent: " .. userAgent) .. " "
|
||||||
if accept then
|
if accept then
|
||||||
cmd = cmd .. "-H " .. HostShell.quote("Accept: " .. accept) .. " "
|
cmd = cmd .. "-H " .. HostShell.quote("Accept: " .. accept) .. " "
|
||||||
@@ -407,7 +520,7 @@ function HostShell.httpDownload(url, absPath, userAgent, accept, maxTime, etagPa
|
|||||||
cmd = cmd .. "-o " .. HostShell.quote(absPath) .. " "
|
cmd = cmd .. "-o " .. HostShell.quote(absPath) .. " "
|
||||||
.. "-w " .. HostShell.quote(HTTP_MARK_FMT) .. " "
|
.. "-w " .. HostShell.quote(HTTP_MARK_FMT) .. " "
|
||||||
.. HostShell.quote(url) .. " 2>&1"
|
.. HostShell.quote(url) .. " 2>&1"
|
||||||
local pipe = HostShell.popen(cmd)
|
local pipe = HostShell.popen(cmd, "r", popts)
|
||||||
if not pipe then return nil, "could not start download" end
|
if not pipe then return nil, "could not start download" end
|
||||||
local readOk, out = pcall(function() return pipe:read("*a") end)
|
local readOk, out = pcall(function() return pipe:read("*a") end)
|
||||||
HostShell.pclose(pipe)
|
HostShell.pclose(pipe)
|
||||||
@@ -450,16 +563,18 @@ function HostShell.httpGet(url, userAgent, accept, maxTime)
|
|||||||
-- BODY, and on the two services this talks to that body is the whole
|
-- BODY, and on the two services this talks to that body is the whole
|
||||||
-- diagnosis: GitHub's 403 says "API rate limit exceeded for <ip>", which
|
-- diagnosis: GitHub's 403 says "API rate limit exceeded for <ip>", which
|
||||||
-- tells a user to wait rather than to go hunting for a broken index.
|
-- tells a user to wait rather than to go hunting for a broken index.
|
||||||
local cmd = ("curl -sSL --proto =http,https --proto-redir =http,https "
|
local head, popts = curlCmd(
|
||||||
|
("-sSL --proto =http,https --proto-redir =http,https "
|
||||||
.. "--connect-timeout 10 --max-time %d ")
|
.. "--connect-timeout 10 --max-time %d ")
|
||||||
:format(tonumber(maxTime) or 40)
|
:format(tonumber(maxTime) or 40))
|
||||||
|
local cmd = head
|
||||||
.. "-H " .. HostShell.quote("User-Agent: " .. userAgent) .. " "
|
.. "-H " .. HostShell.quote("User-Agent: " .. userAgent) .. " "
|
||||||
if accept then
|
if accept then
|
||||||
cmd = cmd .. "-H " .. HostShell.quote("Accept: " .. accept) .. " "
|
cmd = cmd .. "-H " .. HostShell.quote("Accept: " .. accept) .. " "
|
||||||
end
|
end
|
||||||
cmd = cmd .. "-w " .. HostShell.quote(HTTP_MARK_FMT) .. " "
|
cmd = cmd .. "-w " .. HostShell.quote(HTTP_MARK_FMT) .. " "
|
||||||
.. HostShell.quote(url) .. " 2>&1"
|
.. HostShell.quote(url) .. " 2>&1"
|
||||||
local pipe = HostShell.popen(cmd)
|
local pipe = HostShell.popen(cmd, "r", popts)
|
||||||
if not pipe then return nil, "could not run curl" end
|
if not pipe then return nil, "could not run curl" end
|
||||||
local readOk, out = pcall(function() return pipe:read("*a") end)
|
local readOk, out = pcall(function() return pipe:read("*a") end)
|
||||||
HostShell.pclose(pipe)
|
HostShell.pclose(pipe)
|
||||||
@@ -546,9 +661,11 @@ function HostShell.httpPost(url, body, contentType, userAgent, maxTime)
|
|||||||
-- newlines. The body is staged above because io.popen cannot be opened
|
-- newlines. The body is staged above because io.popen cannot be opened
|
||||||
-- for both writing and reading. No -f, matching httpGet: the response
|
-- for both writing and reading. No -f, matching httpGet: the response
|
||||||
-- body is discarded anyway, and curl's stderr carries the diagnosis.
|
-- body is discarded anyway, and curl's stderr carries the diagnosis.
|
||||||
local cmd = ("curl -sSL --proto =http,https --proto-redir =http,https "
|
local head, popts = curlCmd(
|
||||||
|
("-sSL --proto =http,https --proto-redir =http,https "
|
||||||
.. "--connect-timeout 10 --max-time %d ")
|
.. "--connect-timeout 10 --max-time %d ")
|
||||||
:format(tonumber(maxTime) or 40)
|
:format(tonumber(maxTime) or 40))
|
||||||
|
local cmd = head
|
||||||
.. "-X POST "
|
.. "-X POST "
|
||||||
.. "-H " .. HostShell.quote("User-Agent: " .. userAgent) .. " "
|
.. "-H " .. HostShell.quote("User-Agent: " .. userAgent) .. " "
|
||||||
if contentType then
|
if contentType then
|
||||||
@@ -558,7 +675,7 @@ function HostShell.httpPost(url, body, contentType, userAgent, maxTime)
|
|||||||
.. "--data-binary " .. HostShell.quote("@" .. bodyPath) .. " "
|
.. "--data-binary " .. HostShell.quote("@" .. bodyPath) .. " "
|
||||||
.. "-w " .. HostShell.quote(HTTP_MARK_FMT) .. " "
|
.. "-w " .. HostShell.quote(HTTP_MARK_FMT) .. " "
|
||||||
.. HostShell.quote(url) .. " 2>&1"
|
.. HostShell.quote(url) .. " 2>&1"
|
||||||
local pipe = HostShell.popen(cmd)
|
local pipe = HostShell.popen(cmd, "r", popts)
|
||||||
if not pipe then
|
if not pipe then
|
||||||
pcall(os.remove, bodyPath)
|
pcall(os.remove, bodyPath)
|
||||||
return nil, "could not run curl"
|
return nil, "could not run curl"
|
||||||
@@ -735,8 +852,10 @@ function HostShell.httpRequest(url, opts)
|
|||||||
pcall(os.remove, headerPath)
|
pcall(os.remove, headerPath)
|
||||||
end
|
end
|
||||||
|
|
||||||
local cmd = ("curl -sSL --proto =http,https --proto-redir =http,https "
|
local head, popts = curlCmd(
|
||||||
.. "--connect-timeout 10 --max-time %d "):format(maxTime)
|
("-sSL --proto =http,https --proto-redir =http,https "
|
||||||
|
.. "--connect-timeout 10 --max-time %d "):format(maxTime))
|
||||||
|
local cmd = head
|
||||||
.. "-X " .. HostShell.quote(method) .. " "
|
.. "-X " .. HostShell.quote(method) .. " "
|
||||||
.. "-H " .. HostShell.quote("@" .. headerPath) .. " "
|
.. "-H " .. HostShell.quote("@" .. headerPath) .. " "
|
||||||
if body then
|
if body then
|
||||||
@@ -745,7 +864,7 @@ function HostShell.httpRequest(url, opts)
|
|||||||
cmd = cmd .. "-w " .. HostShell.quote(HTTP_MARK_FMT) .. " "
|
cmd = cmd .. "-w " .. HostShell.quote(HTTP_MARK_FMT) .. " "
|
||||||
.. HostShell.quote(url) .. " 2>&1"
|
.. HostShell.quote(url) .. " 2>&1"
|
||||||
|
|
||||||
local pipe = HostShell.popen(cmd)
|
local pipe = HostShell.popen(cmd, "r", popts)
|
||||||
if not pipe then
|
if not pipe then
|
||||||
cleanup()
|
cleanup()
|
||||||
return nil, "could not run curl"
|
return nil, "could not run curl"
|
||||||
|
|||||||
@@ -227,7 +227,11 @@ local function metadata(options, context)
|
|||||||
end
|
end
|
||||||
if flags and flags.fullscreen == true then add("Fullscreen", "yes") end
|
if flags and flags.fullscreen == true then add("Fullscreen", "yes") end
|
||||||
local version = appVersion()
|
local version = appVersion()
|
||||||
add("App", version ~= "" and Version.title() or "gen1recomp")
|
-- Bug reports always want the stamped engine when we have one; window
|
||||||
|
-- chrome hides it on release builds (Version.title). Unstamped
|
||||||
|
-- working-tree builds stay as plain "gen1recomp" so the placeholder
|
||||||
|
-- never lands in a filed issue.
|
||||||
|
add("App", version ~= "" and ("gen1recomp v" .. version) or "gen1recomp")
|
||||||
add("LÖVE", loveVersion())
|
add("LÖVE", loveVersion())
|
||||||
if safeMode then add("Safe mode", "on") end
|
if safeMode then add("Safe mode", "on") end
|
||||||
return {
|
return {
|
||||||
|
|||||||
+44
-4
@@ -173,13 +173,40 @@ function SaveData.gameFolders()
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- The game folder carrying portable.txt, or false. First candidate holding
|
-- The game folder carrying portable.txt, or false. First candidate holding
|
||||||
-- the marker wins.
|
-- the marker *and* a writable probe wins. Flatpak installs always use the
|
||||||
|
-- sandbox XDG save dir (portable USB mode does not apply). A read-only
|
||||||
|
-- parent (system /opt, immutable OS, etc.) falls back to LOVE's save
|
||||||
|
-- directory without throwing -- probe I/O is fully guarded.
|
||||||
|
local function dirIsWritable(dir)
|
||||||
|
if type(dir) ~= "string" or dir == "" then return false end
|
||||||
|
local ok, writable = pcall(function()
|
||||||
|
local name = string.format(".write_probe_%d_%d.tmp",
|
||||||
|
os.time() % 100000000, math.random(0, 999999))
|
||||||
|
local path = dir .. SEP .. name
|
||||||
|
local f, err = io.open(path, "wb")
|
||||||
|
if not f then return false end
|
||||||
|
local wrote = f:write("ok")
|
||||||
|
f:close()
|
||||||
|
pcall(os.remove, path)
|
||||||
|
return wrote ~= nil
|
||||||
|
end)
|
||||||
|
return ok and writable == true
|
||||||
|
end
|
||||||
|
|
||||||
local function detectPortable()
|
local function detectPortable()
|
||||||
if portableChecked then return portableBase end
|
if portableChecked then return portableBase end
|
||||||
portableChecked = true
|
portableChecked = true
|
||||||
portableBase = false
|
portableBase = false
|
||||||
|
-- Flatpak: never honor portable.txt (sandbox home is the persistence root).
|
||||||
|
if type(os.getenv) == "function" and os.getenv("FLATPAK_ID") then
|
||||||
|
return portableBase
|
||||||
|
end
|
||||||
for _, base in ipairs(SaveData.gameFolders()) do
|
for _, base in ipairs(SaveData.gameFolders()) do
|
||||||
if pathExists(base .. SEP .. PORTABLE_MARKER) then
|
local markerOk = false
|
||||||
|
pcall(function()
|
||||||
|
markerOk = pathExists(base .. SEP .. PORTABLE_MARKER)
|
||||||
|
end)
|
||||||
|
if markerOk and dirIsWritable(base) then
|
||||||
portableBase = base
|
portableBase = base
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
@@ -187,6 +214,13 @@ local function detectPortable()
|
|||||||
return portableBase
|
return portableBase
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Soft-reset cache so tests can re-run detect after env changes.
|
||||||
|
function SaveData._resetPortableCacheForTests()
|
||||||
|
portableChecked = false
|
||||||
|
portableBase = false
|
||||||
|
portableFsCache = nil
|
||||||
|
end
|
||||||
|
|
||||||
function SaveData.isPortable()
|
function SaveData.isPortable()
|
||||||
return detectPortable() ~= false
|
return detectPortable() ~= false
|
||||||
end
|
end
|
||||||
@@ -279,6 +313,7 @@ function SaveData.defaultOptions()
|
|||||||
zoom = 0,
|
zoom = 0,
|
||||||
-- OVERWORLD beyond-edge fill: trees | water | black
|
-- OVERWORLD beyond-edge fill: trees | water | black
|
||||||
voidFill = "trees",
|
voidFill = "trees",
|
||||||
|
uiLetterbox = "auto",
|
||||||
-- windowed | borderless (desktop fullscreen); ignored on mobile
|
-- windowed | borderless (desktop fullscreen); ignored on mobile
|
||||||
videoMode = "windowed",
|
videoMode = "windowed",
|
||||||
-- lock the window to an exact 160x144 multiple, 1..4 (0 = OFF); see
|
-- lock the window to an exact 160x144 multiple, 1..4 (0 = OFF); see
|
||||||
@@ -1983,6 +2018,11 @@ SaveData.addCoreMigration(3, function(save)
|
|||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
-- #1461 repair, one-shot on pre-format-5 saves
|
||||||
|
SaveData.addCoreMigration(4, function(save)
|
||||||
|
SaveData.repairTradedOtIds(save)
|
||||||
|
end)
|
||||||
|
|
||||||
-- ------- write
|
-- ------- write
|
||||||
|
|
||||||
-- Game progress only; options are written separately via saveOptions.
|
-- Game progress only; options are written separately via saveOptions.
|
||||||
@@ -2394,8 +2434,8 @@ function SaveData.applyPostGameHome(save, boot)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- 0.1.82-0.1.9x loads stamped the player's own id onto traded mons and saved
|
-- 0.1.82-0.1.9x loads stamped the player's own id onto traded mons and saved
|
||||||
-- it, which reads back as home-caught. A caught mon can never carry
|
-- it, which reads back as home-caught. Run only as the pre-format-5
|
||||||
-- traded=true, so clearing that pair is safe on every load. #1461
|
-- migration: a same-id trade partner makes that pair legitimate. #1461
|
||||||
function SaveData.repairTradedOtIds(save)
|
function SaveData.repairTradedOtIds(save)
|
||||||
local playerId = save and save.player and save.player.id
|
local playerId = save and save.player and save.player.id
|
||||||
if playerId == nil then return 0 end
|
if playerId == nil then return 0 end
|
||||||
|
|||||||
+69
-14
@@ -70,9 +70,27 @@ local function fail(state, why)
|
|||||||
error(("parse error at byte %d: %s"):format(state.pos, why), 0)
|
error(("parse error at byte %d: %s"):format(state.pos, why), 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function limit(state, name)
|
||||||
|
return state.limits and state.limits[name]
|
||||||
|
end
|
||||||
|
|
||||||
|
local function bumpNode(state)
|
||||||
|
state.nodes = state.nodes + 1
|
||||||
|
local maximum = limit(state, "maxNodes")
|
||||||
|
if maximum and state.nodes > maximum then fail(state, "too many values") end
|
||||||
|
end
|
||||||
|
|
||||||
local function skip(state)
|
local function skip(state)
|
||||||
|
while true do
|
||||||
local _, last = state.src:find("^[ \t\r\n]*", state.pos)
|
local _, last = state.src:find("^[ \t\r\n]*", state.pos)
|
||||||
state.pos = last + 1
|
state.pos = last + 1
|
||||||
|
if not (state.limits and state.limits.allowComments
|
||||||
|
and state.src:sub(state.pos, state.pos + 1) == "--") then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local newline = state.src:find("\n", state.pos + 2, true)
|
||||||
|
state.pos = newline and newline + 1 or (#state.src + 1)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function peek(state)
|
local function peek(state)
|
||||||
@@ -90,7 +108,10 @@ local function readString(state)
|
|||||||
fail(state, "unterminated string")
|
fail(state, "unterminated string")
|
||||||
elseif c == '"' then
|
elseif c == '"' then
|
||||||
state.pos = i + 1
|
state.pos = i + 1
|
||||||
return table.concat(out)
|
local value = table.concat(out)
|
||||||
|
local maximum = limit(state, "maxStringBytes")
|
||||||
|
if maximum and #value > maximum then fail(state, "string too long") end
|
||||||
|
return value
|
||||||
elseif c == "\\" then
|
elseif c == "\\" then
|
||||||
local nxt = src:sub(i + 1, i + 1)
|
local nxt = src:sub(i + 1, i + 1)
|
||||||
if nxt:match("%d") then
|
if nxt:match("%d") then
|
||||||
@@ -136,10 +157,14 @@ end
|
|||||||
local readValue
|
local readValue
|
||||||
|
|
||||||
local function readTable(state)
|
local function readTable(state)
|
||||||
|
bumpNode(state)
|
||||||
state.depth = state.depth + 1
|
state.depth = state.depth + 1
|
||||||
if state.depth > MAX_DEPTH then fail(state, "table nesting too deep") end
|
if state.depth > (limit(state, "maxDepth") or MAX_DEPTH) then
|
||||||
|
fail(state, "table nesting too deep")
|
||||||
|
end
|
||||||
state.pos = state.pos + 1
|
state.pos = state.pos + 1
|
||||||
local out = {}
|
local out = {}
|
||||||
|
local entries, nextArray = 0, 1
|
||||||
skip(state)
|
skip(state)
|
||||||
if peek(state) == "}" then
|
if peek(state) == "}" then
|
||||||
state.pos = state.pos + 1
|
state.pos = state.pos + 1
|
||||||
@@ -148,7 +173,7 @@ local function readTable(state)
|
|||||||
end
|
end
|
||||||
while true do
|
while true do
|
||||||
skip(state)
|
skip(state)
|
||||||
local key
|
local key, value
|
||||||
local c = peek(state)
|
local c = peek(state)
|
||||||
if c == "[" then
|
if c == "[" then
|
||||||
state.pos = state.pos + 1
|
state.pos = state.pos + 1
|
||||||
@@ -156,15 +181,38 @@ local function readTable(state)
|
|||||||
skip(state)
|
skip(state)
|
||||||
if peek(state) ~= "]" then fail(state, "expected ]") end
|
if peek(state) ~= "]" then fail(state, "expected ]") end
|
||||||
state.pos = state.pos + 1
|
state.pos = state.pos + 1
|
||||||
elseif c:match("[%a_]") then
|
|
||||||
key = readIdent(state)
|
|
||||||
else
|
|
||||||
fail(state, "expected key")
|
|
||||||
end
|
|
||||||
skip(state)
|
skip(state)
|
||||||
if peek(state) ~= "=" then fail(state, "expected =") end
|
if peek(state) ~= "=" then fail(state, "expected =") end
|
||||||
state.pos = state.pos + 1
|
state.pos = state.pos + 1
|
||||||
out[key] = readValue(state)
|
value = readValue(state)
|
||||||
|
elseif c:match("[%a_]") then
|
||||||
|
local start = state.pos
|
||||||
|
local ident = readIdent(state)
|
||||||
|
skip(state)
|
||||||
|
if peek(state) == "=" then
|
||||||
|
key = ident
|
||||||
|
state.pos = state.pos + 1
|
||||||
|
value = readValue(state)
|
||||||
|
elseif state.limits and state.limits.allowArray then
|
||||||
|
state.pos = start
|
||||||
|
key = nextArray
|
||||||
|
value = readValue(state)
|
||||||
|
else
|
||||||
|
fail(state, "expected =")
|
||||||
|
end
|
||||||
|
elseif state.limits and state.limits.allowArray then
|
||||||
|
key = nextArray
|
||||||
|
value = readValue(state)
|
||||||
|
else
|
||||||
|
fail(state, "expected key")
|
||||||
|
end
|
||||||
|
if key == nil then fail(state, "nil table key") end
|
||||||
|
if out[key] ~= nil then fail(state, "duplicate table key") end
|
||||||
|
entries = entries + 1
|
||||||
|
local maximum = limit(state, "maxTableEntries")
|
||||||
|
if maximum and entries > maximum then fail(state, "too many table entries") end
|
||||||
|
out[key] = value
|
||||||
|
if type(key) == "number" and key == nextArray then nextArray = nextArray + 1 end
|
||||||
skip(state)
|
skip(state)
|
||||||
local sep = peek(state)
|
local sep = peek(state)
|
||||||
if sep == "," then
|
if sep == "," then
|
||||||
@@ -189,25 +237,30 @@ readValue = function(state)
|
|||||||
skip(state)
|
skip(state)
|
||||||
local c = peek(state)
|
local c = peek(state)
|
||||||
if c == '"' then
|
if c == '"' then
|
||||||
|
bumpNode(state)
|
||||||
return readString(state)
|
return readString(state)
|
||||||
elseif c == "{" then
|
elseif c == "{" then
|
||||||
return readTable(state)
|
return readTable(state)
|
||||||
elseif c:match("[%a_]") then
|
elseif c:match("[%a_]") then
|
||||||
-- the only bare words in the grammar are the boolean literals
|
-- the only bare words in the grammar are the boolean literals
|
||||||
local word = readIdent(state)
|
local word = readIdent(state)
|
||||||
if word == "true" then return true end
|
if word == "true" then bumpNode(state); return true end
|
||||||
if word == "false" then return false end
|
if word == "false" then bumpNode(state); return false end
|
||||||
state.pos = state.pos - #word
|
state.pos = state.pos - #word
|
||||||
fail(state, "unexpected name '" .. word .. "'")
|
fail(state, "unexpected name '" .. word .. "'")
|
||||||
elseif c:match("[%-%d%.]") then
|
elseif c:match("[%-%d%.]") then
|
||||||
|
bumpNode(state)
|
||||||
return readNumber(state)
|
return readNumber(state)
|
||||||
end
|
end
|
||||||
fail(state, c == "" and "unexpected end of input" or "unexpected character")
|
fail(state, c == "" and "unexpected end of input" or "unexpected character")
|
||||||
end
|
end
|
||||||
|
|
||||||
function SaveSerializer.decode(str)
|
function SaveSerializer.decode(str, limits)
|
||||||
if type(str) ~= "string" then return nil, "save must be a string" end
|
if type(str) ~= "string" then return nil, "save must be a string" end
|
||||||
local state = { src = str, pos = 1, depth = 0 }
|
if limits and limits.maxBytes and #str > limits.maxBytes then
|
||||||
|
return nil, ("input is %d bytes (max %d)"):format(#str, limits.maxBytes)
|
||||||
|
end
|
||||||
|
local state = { src = str, pos = 1, depth = 0, nodes = 0, limits = limits }
|
||||||
local ok, result = pcall(function()
|
local ok, result = pcall(function()
|
||||||
skip(state)
|
skip(state)
|
||||||
local word = state.src:match("^[%a_][%w_]*", state.pos)
|
local word = state.src:match("^[%a_][%w_]*", state.pos)
|
||||||
@@ -219,7 +272,9 @@ function SaveSerializer.decode(str)
|
|||||||
return value
|
return value
|
||||||
end)
|
end)
|
||||||
if not ok then return nil, result end
|
if not ok then return nil, result end
|
||||||
if type(result) ~= "table" then return nil, "save root must be a table" end
|
if type(result) ~= "table" then
|
||||||
|
return nil, (limits and limits.rootName or "save") .. " root must be a table"
|
||||||
|
end
|
||||||
return result
|
return result
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -76,7 +76,14 @@ local function sdlFfi()
|
|||||||
]])
|
]])
|
||||||
if sdlCdefOk then
|
if sdlCdefOk then
|
||||||
local okLoad, lib = pcall(ffi.load, "SDL2")
|
local okLoad, lib = pcall(ffi.load, "SDL2")
|
||||||
sdlLib = okLoad and lib or ffi.C
|
lib = okLoad and lib or ffi.C
|
||||||
|
local okSyms = pcall(function()
|
||||||
|
return lib.SDL_InitSubSystem, lib.SDL_NumSensors, lib.SDL_SensorGetDeviceType,
|
||||||
|
lib.SDL_SensorOpen, lib.SDL_SensorUpdate, lib.SDL_SensorGetData,
|
||||||
|
lib.SDL_GL_GetCurrentWindow, lib.SDL_GetWindowDisplayIndex,
|
||||||
|
lib.SDL_GetDisplayOrientation
|
||||||
|
end)
|
||||||
|
sdlLib = okSyms and lib or false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if not sdlCdefOk or not sdlLib then return nil end
|
if not sdlCdefOk or not sdlLib then return nil end
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ function SessionLifecycle.endMountedSession(version)
|
|||||||
local Runtime = require("src.mods.Runtime")
|
local Runtime = require("src.mods.Runtime")
|
||||||
if Runtime.reset then Runtime.reset() end
|
if Runtime.reset then Runtime.reset() end
|
||||||
if Assets.installLoader then Assets.installLoader(nil) end
|
if Assets.installLoader then Assets.installLoader(nil) end
|
||||||
|
local Loader = package.loaded["src.mods.Loader"]
|
||||||
|
if Loader and Loader.endSession then Loader.endSession() end
|
||||||
local okCompat, LegacyCompat = pcall(require, "src.mods.LegacyCompat")
|
local okCompat, LegacyCompat = pcall(require, "src.mods.LegacyCompat")
|
||||||
if okCompat and LegacyCompat.reset then LegacyCompat.reset() end
|
if okCompat and LegacyCompat.reset then LegacyCompat.reset() end
|
||||||
end
|
end
|
||||||
@@ -64,11 +66,18 @@ end
|
|||||||
|
|
||||||
-- EXIT GAME / intent_game before dropping Game. Stops audio and resets the
|
-- EXIT GAME / intent_game before dropping Game. Stops audio and resets the
|
||||||
-- live game instance so map/GPU holders are gone before endMountedSession.
|
-- live game instance so map/GPU holders are gone before endMountedSession.
|
||||||
|
--
|
||||||
|
-- ChipAudio's worker stays alive across game sessions (see tier comment
|
||||||
|
-- above). shutdown() joins the thread and is process-exit only -- calling
|
||||||
|
-- it here on every Android EXIT GAME has been observed to leave the Gen1
|
||||||
|
-- Game singleton unbootable (Game.load nil) on the next Play of a version
|
||||||
|
-- already opened this process, while a debug APK with a different liblove
|
||||||
|
-- did not reproduce.
|
||||||
function SessionLifecycle.endGameSession(game)
|
function SessionLifecycle.endGameSession(game)
|
||||||
pcall(function() require("src.core.Music").stop() end)
|
pcall(function() require("src.core.Music").stop() end)
|
||||||
pcall(function() require("src.core.Sound").stop() end)
|
pcall(function() require("src.core.Sound").stop() end)
|
||||||
if package.loaded["src.core.ChipAudio"] then
|
if package.loaded["src.core.ChipAudio"] then
|
||||||
pcall(package.loaded["src.core.ChipAudio"].shutdown)
|
pcall(package.loaded["src.core.ChipAudio"].stopMusic)
|
||||||
end
|
end
|
||||||
if package.loaded["src.core.DiscordPresence"] then
|
if package.loaded["src.core.DiscordPresence"] then
|
||||||
pcall(package.loaded["src.core.DiscordPresence"].shutdown)
|
pcall(package.loaded["src.core.DiscordPresence"].shutdown)
|
||||||
@@ -87,6 +96,13 @@ function SessionLifecycle.endGameSession(game)
|
|||||||
if game and game.reset then
|
if game and game.reset then
|
||||||
pcall(function() game:reset() end)
|
pcall(function() game:reset() end)
|
||||||
end
|
end
|
||||||
|
-- Gen1 Game is the module singleton. Always drop the cached module after
|
||||||
|
-- a session that owned it so the next bootGame require rebuilds a clean
|
||||||
|
-- table. A type(game.load) check is not enough: a wrong table parked in
|
||||||
|
-- package.loaded (e.g. with __index) can still look like it has load.
|
||||||
|
if game and package.loaded["src.core.Game"] == game then
|
||||||
|
package.loaded["src.core.Game"] = nil
|
||||||
|
end
|
||||||
|
|
||||||
local Input = require("src.core.Input")
|
local Input = require("src.core.Input")
|
||||||
local TouchControls = require("src.core.TouchControls")
|
local TouchControls = require("src.core.TouchControls")
|
||||||
|
|||||||
@@ -371,6 +371,13 @@ function Sound.waitSfxDone()
|
|||||||
curSfx = nil
|
curSfx = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- SFXChannelsOff (home/audio.asm:545)
|
||||||
|
function Sound.sfxChannelsOff()
|
||||||
|
if not curSfx then return end
|
||||||
|
pcall(curSfx.src.stop, curSfx.src)
|
||||||
|
curSfx = nil
|
||||||
|
end
|
||||||
|
|
||||||
local function startSfx(data, name, def)
|
local function startSfx(data, name, def)
|
||||||
local src = playPath(data, name, def)
|
local src = playPath(data, name, def)
|
||||||
if not src then return end
|
if not src then return end
|
||||||
|
|||||||
@@ -217,10 +217,6 @@ function TouchControls.defaultLayout(ww, wh, ox, oy, scale)
|
|||||||
local abW = dpadW * 0.46
|
local abW = dpadW * 0.46
|
||||||
local ssW = dpadW * 0.30
|
local ssW = dpadW * 0.30
|
||||||
local margin = dpadW * 0.12
|
local margin = dpadW * 0.12
|
||||||
local ok, GameVersion = pcall(require, "src.core.GameVersion")
|
|
||||||
if ok and GameVersion.generation and GameVersion.generation() == 2 then
|
|
||||||
margin = math.max(margin, math.min(ww * 0.10, 72))
|
|
||||||
end
|
|
||||||
return {
|
return {
|
||||||
dpad = { cx = ox + margin + dpadW / 2, cy = oy + wh - margin - dpadW / 2, w = dpadW },
|
dpad = { cx = ox + margin + dpadW / 2, cy = oy + wh - margin - dpadW / 2, w = dpadW },
|
||||||
a = { cx = ox + ww - margin - abW * 0.55, cy = oy + wh - margin - abW * 1.75, w = abW },
|
a = { cx = ox + ww - margin - abW * 0.55, cy = oy + wh - margin - abW * 1.75, w = abW },
|
||||||
|
|||||||
+16
-4
@@ -18,14 +18,26 @@ local Version = {
|
|||||||
-- exceeds the shell it provides.
|
-- exceeds the shell it provides.
|
||||||
modApi = 2, -- mod API major (manifest `api`)
|
modApi = 2, -- mod API major (manifest `api`)
|
||||||
linkProtocol = 2, -- link handshake wire version (Handshake.PROTOCOL)
|
linkProtocol = 2, -- link handshake wire version (Handshake.PROTOCOL)
|
||||||
saveFormat = 4, -- save.meta.format
|
saveFormat = 5, -- save.meta.format
|
||||||
cache = "rom-cache-v5", -- ROM import cache generation (RomImporter marker)
|
cache = "rom-cache-v5", -- ROM import cache generation (RomImporter marker)
|
||||||
}
|
}
|
||||||
|
|
||||||
-- "gen1recomp v0.0.0-dev" (or the stamped release version in shipped builds)
|
-- True for the working-tree placeholder and any stamped "-dev" pre-release.
|
||||||
|
-- Shipped builds get a bare X.Y.Z from CI and are not "dev" here.
|
||||||
|
function Version.isDev()
|
||||||
|
local engine = tostring(Version.engine or "")
|
||||||
|
return engine == "0.0.0-dev" or engine:find("%-dev", 1) ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Window / chrome title. Dev builds keep the version visible
|
||||||
|
-- ("gen1recomp v0.0.0-dev"); release builds are just the base name so Linux
|
||||||
|
-- window chrome and taskbars do not read "gen1recomp 0.1.73".
|
||||||
function Version.title(base)
|
function Version.title(base)
|
||||||
return (base or "gen1recomp")
|
base = base or "gen1recomp"
|
||||||
.. " v" .. Version.engine
|
if Version.isDev() then
|
||||||
|
return base .. " v" .. Version.engine
|
||||||
|
end
|
||||||
|
return base
|
||||||
end
|
end
|
||||||
|
|
||||||
return Version
|
return Version
|
||||||
|
|||||||
@@ -89,10 +89,12 @@ local function handle(cmd)
|
|||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local idleWait = false
|
||||||
|
|
||||||
while true do
|
while true do
|
||||||
-- drain every pending command first, so a stop/new-play is seen promptly
|
-- drain every pending command first, so a stop/new-play is seen promptly
|
||||||
local quit = false
|
local quit = false
|
||||||
local cmd = cmdCh:pop()
|
local cmd = idleWait and cmdCh:demand(0.05) or cmdCh:pop()
|
||||||
while cmd do
|
while cmd do
|
||||||
if handle(cmd) then quit = true end
|
if handle(cmd) then quit = true end
|
||||||
cmd = cmdCh:pop()
|
cmd = cmdCh:pop()
|
||||||
@@ -100,6 +102,7 @@ while true do
|
|||||||
if quit then break end
|
if quit then break end
|
||||||
|
|
||||||
if engine and not finished and gen and outCh:getCount() < LOOKAHEAD then
|
if engine and not finished and gen and outCh:getCount() < LOOKAHEAD then
|
||||||
|
idleWait = false
|
||||||
local activeGen = gen
|
local activeGen = gen
|
||||||
local ok, sd = pcall(ChipSynth.soundData, engine, BUF, 2)
|
local ok, sd = pcall(ChipSynth.soundData, engine, BUF, 2)
|
||||||
if not ok then
|
if not ok then
|
||||||
@@ -113,8 +116,10 @@ while true do
|
|||||||
finished = true
|
finished = true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
elseif engine and not finished and gen then
|
||||||
|
idleWait = false
|
||||||
|
love.timer.sleep(0.005)
|
||||||
else
|
else
|
||||||
-- nothing to do (idle, or the look-ahead is full): yield the core
|
idleWait = true
|
||||||
love.timer.sleep(0.001)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -164,14 +164,12 @@ local function receiveItemToPc(save, id, data)
|
|||||||
save.pcItems = save.pcItems or {}
|
save.pcItems = save.pcItems or {}
|
||||||
local pc = save.pcItems
|
local pc = save.pcItems
|
||||||
local held = pc[id] or 0
|
local held = pc[id] or 0
|
||||||
if held == 0 then
|
|
||||||
local cap = (data and data.field and data.field.pcItemCap) or PC_ITEM_CAPACITY
|
local cap = (data and data.field and data.field.pcItemCap) or PC_ITEM_CAPACITY
|
||||||
local stacks = 0
|
local function stacksFor(n) return math.ceil((n or 0) / MAX_STACK) end
|
||||||
for _ in pairs(pc) do stacks = stacks + 1 end
|
local used = 0
|
||||||
if stacks >= cap then return false end
|
for _, count in pairs(pc) do used = used + stacksFor(count) end
|
||||||
elseif held + 1 > MAX_STACK then
|
-- engine/items/items.asm:156 PutItemInPocket
|
||||||
return false
|
if used + stacksFor(held + 1) - stacksFor(held) > cap then return false end
|
||||||
end
|
|
||||||
pc[id] = held + 1
|
pc[id] = held + 1
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -309,6 +309,7 @@ Save.DEFAULT_OPTIONS = {
|
|||||||
-- Game Boy. The Gen 1 save's equivalent key is `colors` (SGB packs), which
|
-- Game Boy. The Gen 1 save's equivalent key is `colors` (SGB packs), which
|
||||||
-- means something different, hence the different name.
|
-- means something different, hence the different name.
|
||||||
color = "gbc",
|
color = "gbc",
|
||||||
|
palette = "",
|
||||||
videoMode = "windowed",
|
videoMode = "windowed",
|
||||||
fpsCap = 60,
|
fpsCap = 60,
|
||||||
-- BATTLE BG (#1709): white | black, the surround around the battle screen.
|
-- BATTLE BG (#1709): white | black, the surround around the battle screen.
|
||||||
@@ -316,6 +317,7 @@ Save.DEFAULT_OPTIONS = {
|
|||||||
-- VOID FILL: fade | water | trees | black. fade is each map header's own
|
-- VOID FILL: fade | water | trees | black. fade is each map header's own
|
||||||
-- border block with the dissolve across a boundary (#1418).
|
-- border block with the dissolve across a boundary (#1418).
|
||||||
voidFill = "fade",
|
voidFill = "fade",
|
||||||
|
uiLetterbox = "auto",
|
||||||
musicVol = 7, -- 0-7, like the GB's NR50 master volume
|
musicVol = 7, -- 0-7, like the GB's NR50 master volume
|
||||||
sfxVol = 7, -- 0-7
|
sfxVol = 7, -- 0-7
|
||||||
musicFilter = 0, -- low-pass steps, 0 = off
|
musicFilter = 0, -- low-pass steps, 0 = off
|
||||||
@@ -343,6 +345,7 @@ Save.OPTIONS_KEY = "gold"
|
|||||||
|
|
||||||
local SHARED_KEYS = {
|
local SHARED_KEYS = {
|
||||||
touchControls = true, haptics = true, screenPos = true,
|
touchControls = true, haptics = true, screenPos = true,
|
||||||
|
videoMode = true,
|
||||||
mods = true, modsByVersion = true, modsGen2 = true,
|
mods = true, modsByVersion = true, modsGen2 = true,
|
||||||
modOptions = true, modProfiles = true, modProfilesSeeded = true,
|
modOptions = true, modProfiles = true, modProfilesSeeded = true,
|
||||||
activeProfile = true,
|
activeProfile = true,
|
||||||
@@ -361,12 +364,9 @@ function Save.loadOptions(fs)
|
|||||||
end
|
end
|
||||||
if type(loaded) == "table" then
|
if type(loaded) == "table" then
|
||||||
for key in pairs(SHARED_KEYS) do
|
for key in pairs(SHARED_KEYS) do
|
||||||
if loaded[key] ~= nil then options[key] = loaded[key] end
|
if loaded[key] ~= nil then
|
||||||
end
|
options[key] = loaded[key]
|
||||||
end
|
elseif type(stored) == "table" and stored[key] ~= nil then
|
||||||
if type(stored) == "table" then
|
|
||||||
for key in pairs(SHARED_KEYS) do
|
|
||||||
if options[key] == nil and stored[key] ~= nil then
|
|
||||||
options[key] = stored[key]
|
options[key] = stored[key]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ local CacheContract = {}
|
|||||||
|
|
||||||
CacheContract.FORMAT = "rom-cache-v10:"
|
CacheContract.FORMAT = "rom-cache-v10:"
|
||||||
CacheContract.VERSION_FORMAT = {
|
CacheContract.VERSION_FORMAT = {
|
||||||
crystal = "rom-cache-v10-crystal2:",
|
crystal = "rom-cache-v10-crystal3:",
|
||||||
}
|
}
|
||||||
CacheContract.MARKER_PATH = "rom-cache.complete"
|
CacheContract.MARKER_PATH = "rom-cache.complete"
|
||||||
|
|
||||||
@@ -34,6 +34,11 @@ CacheContract.VERSION_REQUIRED_FILES = {
|
|||||||
"assets/generated/battle/trainers/jessie_james.png",
|
"assets/generated/battle/trainers/jessie_james.png",
|
||||||
"assets/generated/battle/profoakb.png",
|
"assets/generated/battle/profoakb.png",
|
||||||
"assets/generated/pikachu/pikapic_1.png",
|
"assets/generated/pikachu/pikapic_1.png",
|
||||||
|
"assets/generated/minigame/surf_1a.png",
|
||||||
|
"assets/generated/minigame/surf_1b.png",
|
||||||
|
"assets/generated/minigame/surf_1c.png",
|
||||||
|
"assets/generated/minigame/title_bg.png",
|
||||||
|
"assets/generated/minigame/intro_pika_0.png",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,12 +138,70 @@ CacheContract.VERSION_REQUIRED_FILES_OVERRIDE = {
|
|||||||
CacheContract.VERSION_REQUIRED_FILES_OVERRIDE.silver =
|
CacheContract.VERSION_REQUIRED_FILES_OVERRIDE.silver =
|
||||||
CacheContract.VERSION_REQUIRED_FILES_OVERRIDE.gold
|
CacheContract.VERSION_REQUIRED_FILES_OVERRIDE.gold
|
||||||
|
|
||||||
|
local SEMANTIC_MODULES = {
|
||||||
|
[1] = {
|
||||||
|
"constants", "maps", "tilesets", "text", "text_pointers",
|
||||||
|
"trainer_headers", "font", "sprites", "pokemon", "moves", "items",
|
||||||
|
"type_chart", "trainers", "encounters", "field", "battle_anims",
|
||||||
|
},
|
||||||
|
[2] = {
|
||||||
|
"pokemon", "moves", "items", "type_chart", "audio", "font", "maps",
|
||||||
|
"tilesets", "text", "trainers", "encounters", "sprites", "palettes",
|
||||||
|
"icons", "battle_anims", "constants", "landmarks",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
local OPTIONAL_SEMANTIC_MODULES = {
|
||||||
|
[1] = { "audio", "palettes", "icons" },
|
||||||
|
[2] = {},
|
||||||
|
}
|
||||||
|
|
||||||
|
local function copy(values)
|
||||||
|
local out = {}
|
||||||
|
for index, value in ipairs(values or {}) do out[index] = value end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
function CacheContract.requiredFilesFor(version)
|
function CacheContract.requiredFilesFor(version)
|
||||||
local override = CacheContract.VERSION_REQUIRED_FILES_OVERRIDE[version]
|
local override = CacheContract.VERSION_REQUIRED_FILES_OVERRIDE[version]
|
||||||
if override then return override, true end
|
if override then return override, true end
|
||||||
return CacheContract.REQUIRED_FILES, false
|
return CacheContract.REQUIRED_FILES, false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- A detached, version-specific inventory for read-only consumers. Semantic
|
||||||
|
-- consumers additionally require every generated module that backs the public
|
||||||
|
-- registry surface; optional semantic roots are discovered lazily.
|
||||||
|
function CacheContract.requiredFiles(version, semantic)
|
||||||
|
local base, isOverride = CacheContract.requiredFilesFor(version)
|
||||||
|
local files = copy(base)
|
||||||
|
if not isOverride then
|
||||||
|
for _, path in ipairs(CacheContract.VERSION_REQUIRED_FILES[version] or {}) do
|
||||||
|
files[#files + 1] = path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if semantic then
|
||||||
|
for _, name in ipairs(SEMANTIC_MODULES[GameVersion.generation(version)] or {}) do
|
||||||
|
files[#files + 1] = "data/generated/" .. name .. ".lua"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local seen, out = {}, {}
|
||||||
|
for _, path in ipairs(files) do
|
||||||
|
if not seen[path] then
|
||||||
|
seen[path] = true
|
||||||
|
out[#out + 1] = path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
|
function CacheContract.semanticModules(version)
|
||||||
|
return copy(SEMANTIC_MODULES[GameVersion.generation(version)])
|
||||||
|
end
|
||||||
|
|
||||||
|
function CacheContract.optionalSemanticModules(version)
|
||||||
|
return copy(OPTIONAL_SEMANTIC_MODULES[GameVersion.generation(version)])
|
||||||
|
end
|
||||||
|
|
||||||
function CacheContract.formatFor(version)
|
function CacheContract.formatFor(version)
|
||||||
return CacheContract.VERSION_FORMAT[version] or CacheContract.FORMAT
|
return CacheContract.VERSION_FORMAT[version] or CacheContract.FORMAT
|
||||||
end
|
end
|
||||||
@@ -260,4 +323,59 @@ function CacheContract.sourceTreeHasData(version)
|
|||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Inspect another version without mutating CacheFs.prefix, GameVersion, mounts,
|
||||||
|
-- or the active Data table. The injected filesystem addresses exact paths.
|
||||||
|
local function readAt(fs, path)
|
||||||
|
if fs.readAt then return fs.readAt(path) end
|
||||||
|
if fs.read then return fs.read(path) end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function isFileAt(fs, path)
|
||||||
|
if fs.getInfo then
|
||||||
|
local info = fs.getInfo(path, "file")
|
||||||
|
return info ~= nil and (info.type == nil or info.type == "file")
|
||||||
|
end
|
||||||
|
if fs.existsAt then return fs.existsAt(path) == true end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function hasExactFiles(version, fs, prefix, semantic)
|
||||||
|
for _, path in ipairs(CacheContract.requiredFiles(version, semantic)) do
|
||||||
|
if not isFileAt(fs, prefix .. path) then return false, path end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function sourceReady(version, fs, semantic)
|
||||||
|
if not (fs.getInfo and fs.getRealDirectory and fs.getSource) then return nil end
|
||||||
|
local prefix = version == "red" and "" or GameVersion.cachePrefix(version)
|
||||||
|
local complete = hasExactFiles(version, fs, prefix, semantic)
|
||||||
|
if not complete then return nil end
|
||||||
|
local first = prefix .. CacheContract.requiredFiles(version, semantic)[1]
|
||||||
|
if fs.getRealDirectory(first) ~= fs.getSource() then return nil end
|
||||||
|
return { kind = "source", prefix = prefix }
|
||||||
|
end
|
||||||
|
|
||||||
|
function CacheContract.inspect(version, fs, opts)
|
||||||
|
opts = opts or {}
|
||||||
|
if not (GameVersion.VERSIONS[version] and fs) then
|
||||||
|
return nil, "not_imported", "unsupported cache inspection"
|
||||||
|
end
|
||||||
|
if opts.allowSource then
|
||||||
|
local source = sourceReady(version, fs, opts.semantic)
|
||||||
|
if source then return source end
|
||||||
|
end
|
||||||
|
local prefix = GameVersion.cachePrefix(version)
|
||||||
|
local marker = readAt(fs, prefix .. CacheContract.MARKER_PATH)
|
||||||
|
if not CacheContract.markerMatches(version, marker) then
|
||||||
|
return nil, "not_imported", "completion marker is missing or stale"
|
||||||
|
end
|
||||||
|
local complete, missing = hasExactFiles(version, fs, prefix, opts.semantic)
|
||||||
|
if not complete then
|
||||||
|
return nil, "not_imported", "required cache file is missing: " .. tostring(missing)
|
||||||
|
end
|
||||||
|
return { kind = "cache", prefix = prefix, marker = marker }
|
||||||
|
end
|
||||||
|
|
||||||
return CacheContract
|
return CacheContract
|
||||||
|
|||||||
+14
-5
@@ -324,9 +324,9 @@ function CacheFs.openWrite(rel)
|
|||||||
return file
|
return file
|
||||||
end
|
end
|
||||||
|
|
||||||
-- read cache-relative `rel`; returns the bytes or nil
|
-- Read an exact version-qualified path without consulting CacheFs.prefix.
|
||||||
function CacheFs.read(rel)
|
-- Readiness checks use this to inspect another version without global state.
|
||||||
rel = withPrefix(rel)
|
function CacheFs.readAt(rel)
|
||||||
local root = CacheFs.root()
|
local root = CacheFs.root()
|
||||||
if root then
|
if root then
|
||||||
local f = io.open(realPath(root, rel), "rb")
|
local f = io.open(realPath(root, rel), "rb")
|
||||||
@@ -341,6 +341,11 @@ function CacheFs.read(rel)
|
|||||||
return love.filesystem.read(rel)
|
return love.filesystem.read(rel)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- read cache-relative `rel`; returns the bytes or nil
|
||||||
|
function CacheFs.read(rel)
|
||||||
|
return CacheFs.readAt(withPrefix(rel))
|
||||||
|
end
|
||||||
|
|
||||||
-- Read cache-relative `rel` for the active GameVersion when PhysFS may hide
|
-- Read cache-relative `rel` for the active GameVersion when PhysFS may hide
|
||||||
-- prefixed Blue/Yellow/Gold trees (fused NX mount hole). Same order Data:load
|
-- prefixed Blue/Yellow/Gold trees (fused NX mount hole). Same order Data:load
|
||||||
-- already used: active version prefix with CacheFs.prefix cleared, then
|
-- already used: active version prefix with CacheFs.prefix cleared, then
|
||||||
@@ -386,8 +391,7 @@ function CacheFs.loadActive(rel)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- does cache-relative `rel` exist as a file?
|
-- does cache-relative `rel` exist as a file?
|
||||||
function CacheFs.exists(rel)
|
function CacheFs.existsAt(rel)
|
||||||
rel = withPrefix(rel)
|
|
||||||
local root = CacheFs.root()
|
local root = CacheFs.root()
|
||||||
if root then
|
if root then
|
||||||
local f = io.open(realPath(root, rel), "rb")
|
local f = io.open(realPath(root, rel), "rb")
|
||||||
@@ -398,6 +402,11 @@ function CacheFs.exists(rel)
|
|||||||
return love.filesystem.getInfo(rel, "file") ~= nil
|
return love.filesystem.getInfo(rel, "file") ~= nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function CacheFs.exists(rel)
|
||||||
|
return CacheFs.existsAt(withPrefix(rel))
|
||||||
|
end
|
||||||
|
|
||||||
-- remove a single cache-relative file
|
-- remove a single cache-relative file
|
||||||
function CacheFs.remove(rel)
|
function CacheFs.remove(rel)
|
||||||
rel = withPrefix(rel)
|
rel = withPrefix(rel)
|
||||||
|
|||||||
@@ -7,6 +7,28 @@ local SHADES = {
|
|||||||
{ 0, 0, 0, 1 },
|
{ 0, 0, 0, 1 },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ImageWriter.SHADES = SHADES
|
||||||
|
|
||||||
|
-- Title screen rOBP0 = %11100000 ($E0): OBJ shades 1 and 2 draw as white,
|
||||||
|
-- shade 3 as black (pokeyellow engine/movie/title.asm after PlacePikachu).
|
||||||
|
-- Eye OAM is baked into the MEWMON-colored BG PNG; without this remap the
|
||||||
|
-- shade-1 glints become body yellow under the title palette.
|
||||||
|
function ImageWriter.applyTitleObp0(image)
|
||||||
|
local mid, dark = SHADES[2][1], SHADES[3][1]
|
||||||
|
local w, h = image:getWidth(), image:getHeight()
|
||||||
|
for y = 0, h - 1 do
|
||||||
|
for x = 0, w - 1 do
|
||||||
|
local r, g, b, a = image:getPixel(x, y)
|
||||||
|
if a ~= 0 then
|
||||||
|
if math.abs(r - mid) < 0.02 or math.abs(r - dark) < 0.02 then
|
||||||
|
image:setPixel(x, y, 1, 1, 1, 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return image
|
||||||
|
end
|
||||||
|
|
||||||
local function assertDimensions(raw, width, height, bits)
|
local function assertDimensions(raw, width, height, bits)
|
||||||
assert(width % 8 == 0 and height % 8 == 0,
|
assert(width % 8 == 0 and height % 8 == 0,
|
||||||
("%dbpp dimensions must be tile-aligned: %dx%d")
|
("%dbpp dimensions must be tile-aligned: %dx%d")
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ local SaveData = require("src.core.SaveData")
|
|||||||
|
|
||||||
local LauncherSettings = {}
|
local LauncherSettings = {}
|
||||||
|
|
||||||
|
local function bgLocked(opts)
|
||||||
|
return opts.battleLayout == "wide" and opts.battleFit == "fill"
|
||||||
|
and opts.battleHud == "extended"
|
||||||
|
end
|
||||||
|
|
||||||
local function wrapIndex(i, n)
|
local function wrapIndex(i, n)
|
||||||
i = i % n
|
i = i % n
|
||||||
if i < 0 then i = i + n end
|
if i < 0 then i = i + n end
|
||||||
@@ -156,8 +161,6 @@ local function coreRows(opts, hooks)
|
|||||||
opts.battleLayout = opts.battleLayout == "wide" and "og" or "wide"
|
opts.battleLayout = opts.battleLayout == "wide" and "og" or "wide"
|
||||||
if opts.battleLayout ~= "wide" then
|
if opts.battleLayout ~= "wide" then
|
||||||
opts.battleHud = "standard"
|
opts.battleHud = "standard"
|
||||||
elseif opts.battleFit == "fill" and opts.battleHud == "extended" then
|
|
||||||
opts.battleBg = "white"
|
|
||||||
end
|
end
|
||||||
return true
|
return true
|
||||||
end)
|
end)
|
||||||
@@ -167,10 +170,6 @@ local function coreRows(opts, hooks)
|
|||||||
end,
|
end,
|
||||||
function()
|
function()
|
||||||
opts.battleFit = opts.battleFit == "fill" and "fixed" or "fill"
|
opts.battleFit = opts.battleFit == "fill" and "fixed" or "fill"
|
||||||
if opts.battleFit == "fill" and opts.battleLayout == "wide"
|
|
||||||
and opts.battleHud == "extended" then
|
|
||||||
opts.battleBg = "white"
|
|
||||||
end
|
|
||||||
return true
|
return true
|
||||||
end)
|
end)
|
||||||
add(Strings("BATTLE HUD"),
|
add(Strings("BATTLE HUD"),
|
||||||
@@ -185,28 +184,17 @@ local function coreRows(opts, hooks)
|
|||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
opts.battleHud = opts.battleHud == "extended" and "standard" or "extended"
|
opts.battleHud = opts.battleHud == "extended" and "standard" or "extended"
|
||||||
if opts.battleHud == "extended" and opts.battleFit == "fill" then
|
|
||||||
opts.battleBg = "white"
|
|
||||||
end
|
|
||||||
return true
|
return true
|
||||||
end)
|
end)
|
||||||
add(Strings("BATTLE BG"),
|
add(Strings("BATTLE BG"),
|
||||||
function()
|
function()
|
||||||
if opts.battleLayout == "wide" and opts.battleFit == "fill"
|
if bgLocked(opts) then return Strings("AUTO (FILL HUD)") end
|
||||||
and opts.battleHud == "extended" then
|
|
||||||
opts.battleBg = "white"
|
|
||||||
return Strings("AUTO")
|
|
||||||
end
|
|
||||||
if opts.battleBg == "black" then return Strings("BLACK") end
|
if opts.battleBg == "black" then return Strings("BLACK") end
|
||||||
if opts.battleBg == "world" then return Strings("WORLD") end
|
if opts.battleBg == "world" then return Strings("WORLD") end
|
||||||
return Strings("WHITE")
|
return Strings("WHITE")
|
||||||
end,
|
end,
|
||||||
function(dir)
|
function(dir)
|
||||||
if opts.battleLayout == "wide" and opts.battleFit == "fill"
|
if bgLocked(opts) then return false end
|
||||||
and opts.battleHud == "extended" then
|
|
||||||
opts.battleBg = "white"
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
local order = { "white", "black", "world" }
|
local order = { "white", "black", "world" }
|
||||||
local cur = 1
|
local cur = 1
|
||||||
for i, mode in ipairs(order) do
|
for i, mode in ipairs(order) do
|
||||||
@@ -563,7 +551,7 @@ end
|
|||||||
-- src/ui/gen2/OptionsMenu.lua's ROWS; when editing one, keep the two in sync.
|
-- src/ui/gen2/OptionsMenu.lua's ROWS; when editing one, keep the two in sync.
|
||||||
local GEN2_KEY = "gold"
|
local GEN2_KEY = "gold"
|
||||||
|
|
||||||
local function gen2Rows(opts, hooks)
|
local function gen2Rows(opts, hooks, shared)
|
||||||
local rows = {}
|
local rows = {}
|
||||||
local function add(label, value, step)
|
local function add(label, value, step)
|
||||||
rows[#rows + 1] = { label = label, value = value, step = step }
|
rows[#rows + 1] = { label = label, value = value, step = step }
|
||||||
@@ -617,6 +605,7 @@ local function gen2Rows(opts, hooks)
|
|||||||
end
|
end
|
||||||
opts.color =
|
opts.color =
|
||||||
GbcPalette.MODES[wrapIndex(idx - 1 + dir, #GbcPalette.MODES) + 1]
|
GbcPalette.MODES[wrapIndex(idx - 1 + dir, #GbcPalette.MODES) + 1]
|
||||||
|
opts.palette = ""
|
||||||
return true
|
return true
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
@@ -669,9 +658,9 @@ local function gen2Rows(opts, hooks)
|
|||||||
local okVm, VideoMode = pcall(require, "src.core.VideoMode")
|
local okVm, VideoMode = pcall(require, "src.core.VideoMode")
|
||||||
if okVm then
|
if okVm then
|
||||||
add(Strings("VIDEO MODE"),
|
add(Strings("VIDEO MODE"),
|
||||||
function() return VideoMode.modeLabel(opts.videoMode) end,
|
function() return VideoMode.modeLabel(shared.videoMode) end,
|
||||||
function(dir)
|
function(dir)
|
||||||
opts.videoMode = VideoMode.cycle(opts.videoMode, dir)
|
shared.videoMode = VideoMode.cycle(shared.videoMode, dir)
|
||||||
return true
|
return true
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
@@ -690,7 +679,7 @@ local function gen2Rows(opts, hooks)
|
|||||||
add(Strings("BATTLE BG"), ladder(opts, "battleBg",
|
add(Strings("BATTLE BG"), ladder(opts, "battleBg",
|
||||||
{ { "white", "WHITE" }, { "black", "BLACK" } }, "white"))
|
{ { "white", "WHITE" }, { "black", "BLACK" } }, "white"))
|
||||||
|
|
||||||
addTouchRows(rows, add, opts, hooks)
|
addTouchRows(rows, add, shared, hooks)
|
||||||
|
|
||||||
return rows
|
return rows
|
||||||
end
|
end
|
||||||
@@ -718,7 +707,7 @@ function LauncherSettings.open(hooks, version)
|
|||||||
opts[GEN2_KEY] = block
|
opts[GEN2_KEY] = block
|
||||||
end
|
end
|
||||||
sections = {
|
sections = {
|
||||||
{ title = Strings("OPTIONS"), rows = gen2Rows(block, hooks) },
|
{ title = Strings("OPTIONS"), rows = gen2Rows(block, hooks, opts) },
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
sections = {
|
sections = {
|
||||||
|
|||||||
+95
-11
@@ -377,12 +377,19 @@ end
|
|||||||
-- visible while typing, and blinks a caret on the importer's pulse clock.
|
-- visible while typing, and blinks a caret on the importer's pulse clock.
|
||||||
local function textField(imp, x, y, w, h, key, rawText, placeholder, focused, action)
|
local function textField(imp, x, y, w, h, key, rawText, placeholder, focused, action)
|
||||||
Kit._audit("control", x, y, w, h, key)
|
Kit._audit("control", x, y, w, h, key)
|
||||||
Kit.focusable(key, x, y, w, h)
|
local isFocused = Kit.focusable(key, x, y, w, h)
|
||||||
Theme.fill(x, y, w, h, PAL.bg, 1)
|
Theme.fill(x, y, w, h, PAL.bg, 1)
|
||||||
|
if isFocused then
|
||||||
|
local glowPulse = 0.5 + 0.5 * math.sin(Kit.time * 5)
|
||||||
|
Theme.strokeRounded(x - 3, y - 3, w + 6, h + 6, PAL.ink, 0.35 + 0.25 * glowPulse, 2.5, 4)
|
||||||
|
Theme.strokeRounded(x, y, w, h, PAL.ink, 0.95 + 0.05 * glowPulse, 2, 2)
|
||||||
|
Theme.fillRounded(x, y, w, h, PAL.ink, 0.12 + 0.06 * glowPulse, 2)
|
||||||
|
else
|
||||||
Theme.stroke(x, y, w, h, PAL.line,
|
Theme.stroke(x, y, w, h, PAL.line,
|
||||||
focused and Theme.A.focus or
|
focused and Theme.A.focus or
|
||||||
(Kit.hover(x, y, w, h) and Theme.A.hover or Theme.A.hairline),
|
(Kit.hover(x, y, w, h) and Theme.A.hover or Theme.A.hairline),
|
||||||
focused and 2 or 1)
|
focused and 2 or 1)
|
||||||
|
end
|
||||||
local pad = math.floor(10 * Kit.scale)
|
local pad = math.floor(10 * Kit.scale)
|
||||||
local ty = y + (h - Kit.textHeight("button")) / 2
|
local ty = y + (h - Kit.textHeight("button")) / 2
|
||||||
local text = rawText or ""
|
local text = rawText or ""
|
||||||
@@ -397,10 +404,37 @@ local function textField(imp, x, y, w, h, key, rawText, placeholder, focused, ac
|
|||||||
Kit.textHeight("button"), PAL.ink, 1)
|
Kit.textHeight("button"), PAL.ink, 1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if action and (Kit.press(x, y, w, h) or Kit._activateId == key) then
|
if (Kit.press(x, y, w, h) or Kit._activateId == key) then
|
||||||
|
if Kit.VirtualKeyboard then
|
||||||
|
Kit.VirtualKeyboard.open({
|
||||||
|
text = text,
|
||||||
|
targetId = key,
|
||||||
|
title = placeholder or "Enter Text",
|
||||||
|
onDone = function(newText, confirmed)
|
||||||
|
if confirmed then
|
||||||
|
if imp._indexPrompt and key:find("index") then
|
||||||
|
imp._indexPrompt.text = newText
|
||||||
|
elseif imp._rename and key:find("rename") then
|
||||||
|
imp._rename.text = newText
|
||||||
|
elseif imp._profileRenamePrompt and key:find("profren") then
|
||||||
|
imp._profileRenamePrompt.text = newText
|
||||||
|
elseif imp._profileSavePrompt and key:find("profsave") then
|
||||||
|
imp._profileSavePrompt.text = newText
|
||||||
|
elseif imp._settingsText and key:find("settext") then
|
||||||
|
imp._settingsText.text = newText
|
||||||
|
elseif imp.tab == "find" then
|
||||||
|
imp._findQuery = newText
|
||||||
|
if imp._refreshFind then imp:_refreshFind() end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
})
|
||||||
|
end
|
||||||
|
if action then
|
||||||
queueAction(imp, key, action)
|
queueAction(imp, key, action)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local CART_COLOR = {
|
local CART_COLOR = {
|
||||||
red = PAL.railRed, blue = PAL.railBlue, yellow = PAL.railGold,
|
red = PAL.railRed, blue = PAL.railBlue, yellow = PAL.railGold,
|
||||||
@@ -984,6 +1018,17 @@ local function modScopeChipsWidth(options, gap, m)
|
|||||||
return need
|
return need
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function profileState(imp)
|
||||||
|
if imp._profileCache == nil then
|
||||||
|
local LauncherMods = require("src.mods.LauncherMods")
|
||||||
|
local SaveData = require("src.core.SaveData")
|
||||||
|
local options = SaveData.loadOptions()
|
||||||
|
local list, cur = LauncherMods.getProfiles(options)
|
||||||
|
imp._profileCache = { options = options, list = list, active = cur }
|
||||||
|
end
|
||||||
|
return imp._profileCache
|
||||||
|
end
|
||||||
|
|
||||||
local function buildModScopeRow(imp, x, y, w, m)
|
local function buildModScopeRow(imp, x, y, w, m)
|
||||||
local LauncherMods = require("src.mods.LauncherMods")
|
local LauncherMods = require("src.mods.LauncherMods")
|
||||||
local h = math.max(Kit.tapMin(), math.floor(26 * m.s))
|
local h = math.max(Kit.tapMin(), math.floor(26 * m.s))
|
||||||
@@ -994,7 +1039,7 @@ local function buildModScopeRow(imp, x, y, w, m)
|
|||||||
local options = modScopeOptions(imp)
|
local options = modScopeOptions(imp)
|
||||||
|
|
||||||
-- Dedicated Profile control section (cycle button + gear icon button) on right side of Scope Bar
|
-- Dedicated Profile control section (cycle button + gear icon button) on right side of Scope Bar
|
||||||
local _, activeProf = LauncherMods.getProfiles()
|
local activeProf = profileState(imp).active
|
||||||
local isCompact = (w < math.floor(500 * m.s))
|
local isCompact = (w < math.floor(500 * m.s))
|
||||||
local nameText = tostring(activeProf or "Default")
|
local nameText = tostring(activeProf or "Default")
|
||||||
local profLabel = isCompact and nameText or Strings("Profile: %s", nameText)
|
local profLabel = isCompact and nameText or Strings("Profile: %s", nameText)
|
||||||
@@ -3395,13 +3440,19 @@ local function buildFooter(imp, m, y)
|
|||||||
local bx = m.x + math.floor((m.w - topW) / 2)
|
local bx = m.x + math.floor((m.w - topW) / 2)
|
||||||
local my = cy + math.floor((rowH - dh) / 2)
|
local my = cy + math.floor((rowH - dh) / 2)
|
||||||
local chipY = cy + math.floor((rowH - chipH) / 2)
|
local chipY = cy + math.floor((rowH - chipH) / 2)
|
||||||
|
local isFocused = Kit.focusable("bcg", bx, my, dw, dh)
|
||||||
local hot = Kit.hover(bx, my, dw, dh)
|
local hot = Kit.hover(bx, my, dw, dh)
|
||||||
|
if isFocused then
|
||||||
|
local glowPulse = 0.5 + 0.5 * math.sin(Kit.time * 5)
|
||||||
|
Theme.strokeRounded(bx - 3, my - 3, dw + 6, dh + 6, PAL.ink, 0.35 + 0.25 * glowPulse, 2.5, 4)
|
||||||
|
Theme.strokeRounded(bx, my, dw, dh, PAL.ink, 0.95 + 0.05 * glowPulse, 2, 2)
|
||||||
|
end
|
||||||
love.graphics.setShader(imp.invertShader)
|
love.graphics.setShader(imp.invertShader)
|
||||||
love.graphics.setColor(1, 1, 1, hot and 1 or 0.85)
|
love.graphics.setColor(1, 1, 1, (hot or isFocused) and 1 or 0.85)
|
||||||
love.graphics.draw(imp.bcg, Theme.snap(bx), Theme.snap(my), 0, scale, scale)
|
love.graphics.draw(imp.bcg, Theme.snap(bx), Theme.snap(my), 0, scale, scale)
|
||||||
love.graphics.setShader()
|
love.graphics.setShader()
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
if Kit.press(bx, my, dw, dh) then
|
if Kit.press(bx, my, dw, dh) or Kit._activateId == "bcg" then
|
||||||
queueAction(imp, "bcg", function() love.system.openURL(COMMUNITY_URL) end)
|
queueAction(imp, "bcg", function() love.system.openURL(COMMUNITY_URL) end)
|
||||||
end
|
end
|
||||||
local cx = bx + dw
|
local cx = bx + dw
|
||||||
@@ -3698,9 +3749,8 @@ local function buildSingleProfileActionsModal(imp, m)
|
|||||||
if not pName then imp._singleProfileActions = nil return end
|
if not pName then imp._singleProfileActions = nil return end
|
||||||
|
|
||||||
local LauncherMods = require("src.mods.LauncherMods")
|
local LauncherMods = require("src.mods.LauncherMods")
|
||||||
local SaveData = require("src.core.SaveData")
|
local prof = profileState(imp)
|
||||||
local options = SaveData.loadOptions()
|
local options, profiles = prof.options, prof.list
|
||||||
local profiles, active = LauncherMods.getProfiles(options)
|
|
||||||
|
|
||||||
local pad = math.floor(18 * m.s)
|
local pad = math.floor(18 * m.s)
|
||||||
local w = math.min(math.floor(380 * m.s), m.w - 2 * m.pad)
|
local w = math.min(math.floor(380 * m.s), m.w - 2 * m.pad)
|
||||||
@@ -3715,6 +3765,7 @@ local function buildSingleProfileActionsModal(imp, m)
|
|||||||
action = function()
|
action = function()
|
||||||
LauncherMods.duplicateProfile(pName, options)
|
LauncherMods.duplicateProfile(pName, options)
|
||||||
imp._singleProfileActions = nil
|
imp._singleProfileActions = nil
|
||||||
|
if imp._refreshMods then imp:_refreshMods() end
|
||||||
end
|
end
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -3736,6 +3787,7 @@ local function buildSingleProfileActionsModal(imp, m)
|
|||||||
imp:pressDelete("profile", pName, nil, function()
|
imp:pressDelete("profile", pName, nil, function()
|
||||||
LauncherMods.deleteProfile(pName, options)
|
LauncherMods.deleteProfile(pName, options)
|
||||||
imp._singleProfileActions = nil
|
imp._singleProfileActions = nil
|
||||||
|
if imp._refreshMods then imp:_refreshMods() end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
@@ -3771,9 +3823,8 @@ end
|
|||||||
-- Modal for Mod Profiles (#593) - interactive profile manager (switch, edit, duplicate, delete)
|
-- Modal for Mod Profiles (#593) - interactive profile manager (switch, edit, duplicate, delete)
|
||||||
local function buildProfilesModal(imp, m)
|
local function buildProfilesModal(imp, m)
|
||||||
local LauncherMods = require("src.mods.LauncherMods")
|
local LauncherMods = require("src.mods.LauncherMods")
|
||||||
local SaveData = require("src.core.SaveData")
|
local prof = profileState(imp)
|
||||||
local options = SaveData.loadOptions()
|
local options, profiles, active = prof.options, prof.list, prof.active
|
||||||
local profiles, active = LauncherMods.getProfiles(options)
|
|
||||||
|
|
||||||
local pad = math.floor(18 * m.s)
|
local pad = math.floor(18 * m.s)
|
||||||
local w = math.min(math.floor(460 * m.s), m.w - 2 * m.pad)
|
local w = math.min(math.floor(460 * m.s), m.w - 2 * m.pad)
|
||||||
@@ -5593,6 +5644,8 @@ end
|
|||||||
-- a click on the scrim lands on whatever button happens to be behind it.
|
-- a click on the scrim lands on whatever button happens to be behind it.
|
||||||
-- Keep this list in sync with buildModals below.
|
-- Keep this list in sync with buildModals below.
|
||||||
local function modalUp(imp)
|
local function modalUp(imp)
|
||||||
|
if Kit.FileBrowser and Kit.FileBrowser.active then return true end
|
||||||
|
if Kit.VirtualKeyboard and Kit.VirtualKeyboard.active then return true end
|
||||||
return (imp._settingsText or imp._settings or imp._rename
|
return (imp._settingsText or imp._settings or imp._rename
|
||||||
or imp._indexPrompt or imp._modConfirm or imp._modReleaseNotes
|
or imp._indexPrompt or imp._modConfirm or imp._modReleaseNotes
|
||||||
or imp._appPatchNotes
|
or imp._appPatchNotes
|
||||||
@@ -5605,6 +5658,14 @@ local function modalUp(imp)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function buildModals(imp, m)
|
local function buildModals(imp, m)
|
||||||
|
if Kit.VirtualKeyboard and Kit.VirtualKeyboard.active then
|
||||||
|
Kit.VirtualKeyboard.draw(m)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
if Kit.FileBrowser and Kit.FileBrowser.active then
|
||||||
|
Kit.FileBrowser.draw(m)
|
||||||
|
return true
|
||||||
|
end
|
||||||
if imp._profileRenamePrompt then
|
if imp._profileRenamePrompt then
|
||||||
buildPrompt(imp, m, {
|
buildPrompt(imp, m, {
|
||||||
key = "profren", title = Strings("Rename profile"),
|
key = "profren", title = Strings("Rename profile"),
|
||||||
@@ -5776,6 +5837,10 @@ end
|
|||||||
|
|
||||||
local function drawPadCursor(imp)
|
local function drawPadCursor(imp)
|
||||||
if not imp._padCursorActive then return end
|
if not imp._padCursorActive then return end
|
||||||
|
if (Kit.FileBrowser and Kit.FileBrowser.active)
|
||||||
|
or (Kit.VirtualKeyboard and Kit.VirtualKeyboard.active) then
|
||||||
|
return
|
||||||
|
end
|
||||||
-- Pixel-snap on NX: subpixel polygon edges shimmer on the 720p Switch
|
-- Pixel-snap on NX: subpixel polygon edges shimmer on the 720p Switch
|
||||||
-- framebuffer when the stick advances by fractional pixels each frame.
|
-- framebuffer when the stick advances by fractional pixels each frame.
|
||||||
local x, y = imp._padCursor.x, imp._padCursor.y
|
local x, y = imp._padCursor.x, imp._padCursor.y
|
||||||
@@ -5959,6 +6024,25 @@ function LauncherView.draw(imp)
|
|||||||
|
|
||||||
Kit.endFrame()
|
Kit.endFrame()
|
||||||
drawPadCursor(imp)
|
drawPadCursor(imp)
|
||||||
|
|
||||||
|
if imp._cursorModeToast and imp._cursorModeToastTime then
|
||||||
|
local elapsed = love.timer.getTime() - imp._cursorModeToastTime
|
||||||
|
if elapsed < 2.2 then
|
||||||
|
local alpha = 1.0
|
||||||
|
if elapsed > 1.7 then alpha = math.max(0, (2.2 - elapsed) / 0.5) end
|
||||||
|
local msg = imp._cursorModeToast
|
||||||
|
local tw = Kit.textWidth("small", msg) + 36 * m.s
|
||||||
|
local th = 34 * m.s
|
||||||
|
local tx = (m.W - tw) / 2
|
||||||
|
local ty = 16 * m.s
|
||||||
|
Theme.fillRounded(tx, ty, tw, th, PAL.surface, 0.95 * alpha, 6)
|
||||||
|
Theme.strokeRounded(tx - 2, ty - 2, tw + 4, th + 4, PAL.railBlue, 0.35 * alpha, 2, 8)
|
||||||
|
Theme.strokeRounded(tx, ty, tw, th, PAL.lineStrong, 0.85 * alpha, 1.5, 6)
|
||||||
|
Kit.textCenterBold("small", msg, tx, ty + 8 * m.s, tw, PAL.heading)
|
||||||
|
else
|
||||||
|
imp._cursorModeToast = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return LauncherView
|
return LauncherView
|
||||||
|
|||||||
+118
-2
@@ -1535,6 +1535,14 @@ function RomExtractor:extractYellowTitleArt()
|
|||||||
local bg = sheetTiles("TitlePikachuBGGraphics", 64)
|
local bg = sheetTiles("TitlePikachuBGGraphics", 64)
|
||||||
local ob = sheetTiles("TitlePikachuOBGraphics", 12)
|
local ob = sheetTiles("TitlePikachuOBGraphics", 12)
|
||||||
local obClear = sheetTiles("TitlePikachuOBGraphics", 12, true)
|
local obClear = sheetTiles("TitlePikachuOBGraphics", 12, true)
|
||||||
|
-- Title rOBP0=$E0: remap eye OAM shades 1/2 → white before baking into the
|
||||||
|
-- MEWMON BG composition (otherwise glints read as body yellow).
|
||||||
|
local eyeOb = {}
|
||||||
|
for i, tile in ipairs(obClear) do
|
||||||
|
local copy = ImageWriter.blank(8, 8, 0, 0, 0, 0)
|
||||||
|
ImageWriter.blit(copy, tile, 0, 0)
|
||||||
|
eyeOb[i] = ImageWriter.applyTitleObp0(copy)
|
||||||
|
end
|
||||||
local function tileFor(id)
|
local function tileFor(id)
|
||||||
if id < 0x80 then return logo[id + 1] end
|
if id < 0x80 then return logo[id + 1] end
|
||||||
if id < 0xF0 then return bg[id - 0x80 + 1] end
|
if id < 0xF0 then return bg[id - 0x80 + 1] end
|
||||||
@@ -1623,13 +1631,13 @@ function RomExtractor:extractYellowTitleArt()
|
|||||||
local overlay = ImageWriter.blank(48, 16, 1, 1, 1, 0)
|
local overlay = ImageWriter.blank(48, 16, 1, 1, 1, 0)
|
||||||
ImageWriter.blit(overlay, pikachu, 0, 0, 24, 16, 48, 16)
|
ImageWriter.blit(overlay, pikachu, 0, 0, 24, 16, 48, 16)
|
||||||
for _, e in ipairs(EYE_LAYOUT) do
|
for _, e in ipairs(EYE_LAYOUT) do
|
||||||
blitSprite(overlay, obClear[base + e[1]], e[2] - 24, e[3] - 16, e[4])
|
blitSprite(overlay, eyeOb[base + e[1]], e[2] - 24, e[3] - 16, e[4])
|
||||||
end
|
end
|
||||||
overlays[suffix] = overlay
|
overlays[suffix] = overlay
|
||||||
end
|
end
|
||||||
-- open eyes bake into pikachu.png AFTER the blank-face crops
|
-- open eyes bake into pikachu.png AFTER the blank-face crops
|
||||||
for _, e in ipairs(EYE_LAYOUT) do
|
for _, e in ipairs(EYE_LAYOUT) do
|
||||||
blitSprite(pikachu, obClear[e[1]], e[2], e[3], e[4])
|
blitSprite(pikachu, eyeOb[e[1]], e[2], e[3], e[4])
|
||||||
end
|
end
|
||||||
self:save(pikachu, "title/pikachu.png")
|
self:save(pikachu, "title/pikachu.png")
|
||||||
for suffix, overlay in pairs(overlays) do
|
for suffix, overlay in pairs(overlays) do
|
||||||
@@ -1637,6 +1645,113 @@ function RomExtractor:extractYellowTitleArt()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function RomExtractor:extractSurfingPikachuTitleArt()
|
||||||
|
-- engine/minigame/surfing_pikachu.asm
|
||||||
|
-- DrawSurfingPikachuMinigameIntroBackground: compose the 160x144
|
||||||
|
-- "Pikachu's Beach" title from SurfingMinigame_* tilemaps over
|
||||||
|
-- SurfingPikachu1Graphics3 tiles (mirrors tools/build_rom_data.py).
|
||||||
|
if not self.symbols["SurfingPikachu1Graphics3"] then return end
|
||||||
|
if not self.symbols["SurfingMinigame_BeachIntroTilemap"] then return end
|
||||||
|
|
||||||
|
local beachIntro = self:symbol("SurfingMinigame_BeachIntroTilemap")
|
||||||
|
local useCtrlPad = self:symbol("SurfingMinigame_UseControlPadTilemap")
|
||||||
|
local toSurfRad = self:symbol("SurfingMinigame_ToSurfRadTilemap")
|
||||||
|
local titleMap = self:symbol("SurfingMinigame_TitleTilemap")
|
||||||
|
|
||||||
|
local beachBytes = self.rom:bytes(
|
||||||
|
beachIntro.bank, beachIntro.address, 12 * 20)
|
||||||
|
local useCtrlBytes = self.rom:bytes(
|
||||||
|
useCtrlPad.bank, useCtrlPad.address, 15)
|
||||||
|
local toSurfRadBytes = self.rom:bytes(
|
||||||
|
toSurfRad.bank, toSurfRad.address, 13)
|
||||||
|
local titleMapBytes = self.rom:bytes(
|
||||||
|
titleMap.bank, titleMap.address, 6 * 12)
|
||||||
|
|
||||||
|
local screen = {}
|
||||||
|
for _ = 1, 20 * 18 do screen[#screen + 1] = 0xff end
|
||||||
|
|
||||||
|
for i = 1, #beachBytes do
|
||||||
|
screen[6 * 20 + i] = beachBytes[i]
|
||||||
|
end
|
||||||
|
for r = 0, 5 do
|
||||||
|
for c = 0, 11 do
|
||||||
|
screen[r * 20 + (4 + c) + 1] = titleMapBytes[r * 12 + c + 1]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for r = 0, 2 do
|
||||||
|
for c = 0, 14 do
|
||||||
|
screen[(7 + r) * 20 + (3 + c) + 1] = 0xff
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for i = 1, #useCtrlBytes do
|
||||||
|
screen[7 * 20 + 3 + i] = useCtrlBytes[i]
|
||||||
|
end
|
||||||
|
for i = 1, #toSurfRadBytes do
|
||||||
|
screen[9 * 20 + 4 + i] = toSurfRadBytes[i]
|
||||||
|
end
|
||||||
|
|
||||||
|
local gfx3 = self:symbol("SurfingPikachu1Graphics3")
|
||||||
|
local rawGfx3 = self.rom:bytes(gfx3.bank, gfx3.address, 144 * 16)
|
||||||
|
local tiles = {}
|
||||||
|
for tile = 0, 143 do
|
||||||
|
local one = {}
|
||||||
|
for j = 1, 16 do one[j] = rawGfx3[tile * 16 + j] end
|
||||||
|
tiles[tile + 1] = ImageWriter.decode2bpp(one, 8, 8, false)
|
||||||
|
end
|
||||||
|
local blank = ImageWriter.blank(8, 8, 1, 1, 1, 1)
|
||||||
|
|
||||||
|
local titleBg = ImageWriter.blank(160, 144, 1, 1, 1, 1)
|
||||||
|
for r = 0, 17 do
|
||||||
|
for c = 0, 19 do
|
||||||
|
local tileId = screen[r * 20 + c + 1]
|
||||||
|
local tileImg = blank
|
||||||
|
if tileId ~= 0xff then
|
||||||
|
local idx = tileId >= 0x80 and (tileId - 0x80 + 1) or (128 + tileId + 1)
|
||||||
|
if idx >= 1 and idx <= 144 then tileImg = tiles[idx] end
|
||||||
|
end
|
||||||
|
ImageWriter.blit(titleBg, tileImg, c * 8, r * 8)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self:save(titleBg, "minigame/title_bg.png")
|
||||||
|
|
||||||
|
-- Intro paddling Pikachu frames (surfing_pikachu_oam.asm .IntroPikachu).
|
||||||
|
local INTRO_PIKA_FRAME_BASE = { 0x80, 0x84, 0x88, 0x8c }
|
||||||
|
local INTRO_PIKA_OAM = {
|
||||||
|
{ -12, -16, 0x03, true }, { -12, -8, 0x02, true },
|
||||||
|
{ -12, 0, 0x01, true }, { -12, 8, 0x00, true },
|
||||||
|
{ -4, -16, 0x13, true }, { -4, -8, 0x12, true },
|
||||||
|
{ -4, 0, 0x11, true }, { -4, 8, 0x10, true },
|
||||||
|
{ 4, -16, 0x23, true }, { 4, -8, 0x22, true },
|
||||||
|
{ 4, 0, 0x21, true }, { 4, 8, 0x20, true },
|
||||||
|
}
|
||||||
|
local function blitIntroTile(target, tile, tx, ty, flipX)
|
||||||
|
for y = 0, 7 do
|
||||||
|
for x = 0, 7 do
|
||||||
|
local sx = flipX and (7 - x) or x
|
||||||
|
local r, g, b, a = tile:getPixel(sx, y)
|
||||||
|
local px, py = tx + x, ty + y
|
||||||
|
if a ~= 0 and px >= 0 and py >= 0
|
||||||
|
and px < target:getWidth() and py < target:getHeight() then
|
||||||
|
target:setPixel(px, py, r, g, b, a)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for frame, vramBase in ipairs(INTRO_PIKA_FRAME_BASE) do
|
||||||
|
local pose = ImageWriter.blank(32, 24, 1, 1, 1, 0)
|
||||||
|
local sheetBase = vramBase - 0x80
|
||||||
|
for _, sp in ipairs(INTRO_PIKA_OAM) do
|
||||||
|
local dy, dx, rel, flipX = sp[1], sp[2], sp[3], sp[4]
|
||||||
|
local tileImg = tiles[sheetBase + rel + 1]
|
||||||
|
if tileImg then
|
||||||
|
blitIntroTile(pose, tileImg,
|
||||||
|
16 + dx + (flipX and 8 or 0), 12 + dy, flipX)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self:save(pose, ("minigame/intro_pika_%d.png"):format(frame - 1))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
function RomExtractor:raw2bpp(label, width, height, relative, options)
|
function RomExtractor:raw2bpp(label, width, height, relative, options)
|
||||||
options = options or {}
|
options = options or {}
|
||||||
local expected = width * height / 4
|
local expected = width * height / 4
|
||||||
@@ -1978,6 +2093,7 @@ function RomExtractor:extractField()
|
|||||||
self:save(image, spec[5])
|
self:save(image, spec[5])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
self:extractSurfingPikachuTitleArt()
|
||||||
|
|
||||||
-- Yellow-only: TalkToPikachu's framed portrait, one 5x5 base frame per
|
-- Yellow-only: TalkToPikachu's framed portrait, one 5x5 base frame per
|
||||||
-- PikaPicAnimScript -- each script's FIRST pikapic_loadgfx in
|
-- PikaPicAnimScript -- each script's FIRST pikapic_loadgfx in
|
||||||
|
|||||||
@@ -151,6 +151,19 @@ local TEXT_BUFFERS = {
|
|||||||
[0xc602] = "wOTTrademonSpeciesName",
|
[0xc602] = "wOTTrademonSpeciesName",
|
||||||
[0xc618] = "wOTTrademonSenderName",
|
[0xc618] = "wOTTrademonSenderName",
|
||||||
}
|
}
|
||||||
|
-- ../pokecrystal/ram/wram.asm:1925,2333
|
||||||
|
local TEXT_BUFFERS_CRYSTAL = {
|
||||||
|
[0xd050] = "wMonOrItemNameBuffer",
|
||||||
|
[0xd073] = "wStringBuffer1",
|
||||||
|
[0xd086] = "wStringBuffer2",
|
||||||
|
[0xd099] = "wStringBuffer3",
|
||||||
|
[0xd0ac] = "wStringBuffer4",
|
||||||
|
[0xd0bf] = "wStringBuffer5",
|
||||||
|
[0xc6d1] = "wPlayerTrademonSpeciesName",
|
||||||
|
[0xc6e7] = "wPlayerTrademonSenderName",
|
||||||
|
[0xc703] = "wOTTrademonSpeciesName",
|
||||||
|
[0xc719] = "wOTTrademonSenderName",
|
||||||
|
}
|
||||||
-- The text commands that print nothing and carry no argument
|
-- The text commands that print nothing and carry no argument
|
||||||
-- (macros/scripts/text.asm, in TextCommands order): TX_LOW, TX_SCROLL,
|
-- (macros/scripts/text.asm, in TextCommands order): TX_LOW, TX_SCROLL,
|
||||||
-- TX_PAUSE, TX_WAIT_BUTTON, TX_DAY, and the six TX_SOUND_* jingles.
|
-- TX_PAUSE, TX_WAIT_BUTTON, TX_DAY, and the six TX_SOUND_* jingles.
|
||||||
@@ -2295,6 +2308,8 @@ function RomExtractorGen2:extractTitle()
|
|||||||
trailBobAmplitude = silver and 3 or 2,
|
trailBobAmplitude = silver and 3 or 2,
|
||||||
trailPhaseStep = silver and 7 or 3,
|
trailPhaseStep = silver and 7 or 3,
|
||||||
trailPhase = silver and 0 or nil,
|
trailPhase = silver and 0 or nil,
|
||||||
|
-- TitleScreenTimer (engine/menus/intro_menu.asm:951-966).
|
||||||
|
timeoutFrames = silver and (73 * 60 + 36) or (84 * 60 + 16),
|
||||||
}
|
}
|
||||||
self:write("title", data)
|
self:write("title", data)
|
||||||
return data
|
return data
|
||||||
@@ -2805,6 +2820,8 @@ end
|
|||||||
-- is which alongside the text lets a caller fill them in order without
|
-- is which alongside the text lets a caller fill them in order without
|
||||||
-- changing a marker every screen already reads.
|
-- changing a marker every screen already reads.
|
||||||
function RomExtractorGen2:decodeGen2Text(bank, address, charmap, buffers)
|
function RomExtractorGen2:decodeGen2Text(bank, address, charmap, buffers)
|
||||||
|
local textBuffers = (self.edition == "crystal") and TEXT_BUFFERS_CRYSTAL
|
||||||
|
or TEXT_BUFFERS
|
||||||
local out = {}
|
local out = {}
|
||||||
local i = 0
|
local i = 0
|
||||||
local hops = 0
|
local hops = 0
|
||||||
@@ -2840,7 +2857,7 @@ function RomExtractorGen2:decodeGen2Text(bank, address, charmap, buffers)
|
|||||||
out[#out + 1] = "{STRBUF}"
|
out[#out + 1] = "{STRBUF}"
|
||||||
if buffers then
|
if buffers then
|
||||||
local target = self.rom:word(bank, address + i + 1)
|
local target = self.rom:word(bank, address + i + 1)
|
||||||
buffers[#buffers + 1] = TEXT_BUFFERS[target] or target
|
buffers[#buffers + 1] = textBuffers[target] or target
|
||||||
end
|
end
|
||||||
i = i + 2
|
i = i + 2
|
||||||
elseif b == 0x4e or b == 0x4f then
|
elseif b == 0x4e or b == 0x4f then
|
||||||
|
|||||||
+430
-35
@@ -9,6 +9,9 @@ local SafeArea = require("src.core.SafeArea")
|
|||||||
local RomImporter = {}
|
local RomImporter = {}
|
||||||
RomImporter.__index = RomImporter
|
RomImporter.__index = RomImporter
|
||||||
|
|
||||||
|
local PICK_COMPLETE_FILENAME = "pick_complete.flag"
|
||||||
|
local finishDirectRequiredImport
|
||||||
|
|
||||||
-- love.system.pickFile is a NATIVE BRIDGE, not part of LÖVE: it exists only on
|
-- love.system.pickFile is a NATIVE BRIDGE, not part of LÖVE: it exists only on
|
||||||
-- builds that compiled one (Android, and iOS builds patched by
|
-- builds that compiled one (Android, and iOS builds patched by
|
||||||
-- mobile/ios/patch_love_src.py). A build without it must fall back to the
|
-- mobile/ios/patch_love_src.py). A build without it must fall back to the
|
||||||
@@ -1260,7 +1263,12 @@ end
|
|||||||
-- up the background worker or reach out to the network.
|
-- up the background worker or reach out to the network.
|
||||||
local function updaterAllowed()
|
local function updaterAllowed()
|
||||||
if not Platform.networkValidated() then return false end
|
if not Platform.networkValidated() then return false end
|
||||||
if not (love.filesystem.isFused and love.filesystem.isFused()) then return false end
|
local isHandheld = os.getenv("HANDHELD") == "1" or os.getenv("PORTMASTER") == "1"
|
||||||
|
or os.getenv("POKEPORT_HANDHELD") == "1" or os.getenv("TRIMUI") == "1"
|
||||||
|
or os.getenv("MUOS") == "1" or os.getenv("KNULLI") == "1"
|
||||||
|
if not (love.filesystem.isFused and love.filesystem.isFused()) and not isHandheld then
|
||||||
|
return false
|
||||||
|
end
|
||||||
if os.getenv("POKEPORT_AUTOPILOT") or os.getenv("POKEPORT_DRIVER") then return false end
|
if os.getenv("POKEPORT_AUTOPILOT") or os.getenv("POKEPORT_DRIVER") then return false end
|
||||||
if os.getenv("POKEPORT_IMPORT_ONLY") == "1" then return false end
|
if os.getenv("POKEPORT_IMPORT_ONLY") == "1" then return false end
|
||||||
return true
|
return true
|
||||||
@@ -1582,8 +1590,11 @@ function RomImporter:focus(f)
|
|||||||
.. love.filesystem.getSaveDirectory()
|
.. love.filesystem.getSaveDirectory()
|
||||||
local legacyRequiredPick = self.requiredImportLegacyRomPick
|
local legacyRequiredPick = self.requiredImportLegacyRomPick
|
||||||
and self.pickerPendingKind == "required_import"
|
and self.pickerPendingKind == "required_import"
|
||||||
if pickError:find("picked_required_import", 1, true)
|
if self.pickerPendingKind == "required_import"
|
||||||
|
or pickError:find("picked_required_import", 1, true)
|
||||||
or pickError:find("picked_stadium", 1, true)
|
or pickError:find("picked_stadium", 1, true)
|
||||||
|
or pickError:find("/baseroms/", 1, true)
|
||||||
|
or pickError:find("\\baseroms\\", 1, true)
|
||||||
or (legacyRequiredPick and pickError:find("picked_rom", 1, true)) then
|
or (legacyRequiredPick and pickError:find("picked_rom", 1, true)) then
|
||||||
self.modNotice = { ok = false, text = text }
|
self.modNotice = { ok = false, text = text }
|
||||||
self.pickerPendingKind = nil
|
self.pickerPendingKind = nil
|
||||||
@@ -1606,6 +1617,22 @@ function RomImporter:focus(f)
|
|||||||
end
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Current mobile bridge: the native picker has already streamed a raw
|
||||||
|
-- dependency into mods/<id>/baseroms and published its digest/size marker.
|
||||||
|
local completedRequired = love.filesystem.getInfo(PICK_COMPLETE_FILENAME, "file")
|
||||||
|
and love.filesystem.read(PICK_COMPLETE_FILENAME)
|
||||||
|
if completedRequired then
|
||||||
|
love.filesystem.remove(PICK_COMPLETE_FILENAME)
|
||||||
|
self.pickPending = nil
|
||||||
|
finishDirectRequiredImport(self, completedRequired)
|
||||||
|
self.pickerPendingKind = nil
|
||||||
|
self.pickerPendingModId = nil
|
||||||
|
self.pickerPendingImportId = nil
|
||||||
|
self.requiredImportLegacyRomPick = nil
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
local requiredName = findPendingRequiredImport(self)
|
local requiredName = findPendingRequiredImport(self)
|
||||||
if requiredName then
|
if requiredName then
|
||||||
local modId, importId = self.pickerPendingModId, self.pickerPendingImportId
|
local modId, importId = self.pickerPendingModId, self.pickerPendingImportId
|
||||||
@@ -2033,8 +2060,42 @@ function RomImporter:chooseMod()
|
|||||||
end
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
local isHandheld = os.getenv("HANDHELD") == "1" or os.getenv("PORTMASTER") == "1"
|
||||||
|
or os.getenv("POKEPORT_HANDHELD") == "1" or os.getenv("TRIMUI") == "1"
|
||||||
|
or os.getenv("MUOS") == "1" or os.getenv("KNULLI") == "1"
|
||||||
|
|
||||||
|
if isHandheld then
|
||||||
|
local okKit, Kit = pcall(require, "src.ui.kit.Kit")
|
||||||
|
if okKit and Kit.FileBrowser then
|
||||||
|
self._padCursorActive = false
|
||||||
|
Kit.FileBrowser.open({
|
||||||
|
title = "Select Mod (.zip)",
|
||||||
|
mode = "mod",
|
||||||
|
onSelect = function(pickedPath)
|
||||||
|
self:_installMod(pickedPath)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local path = chooseZip()
|
local path = chooseZip()
|
||||||
if path then self:_installMod(path) end
|
if path then
|
||||||
|
self:_installMod(path)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local okKit, Kit = pcall(require, "src.ui.kit.Kit")
|
||||||
|
if okKit and Kit.FileBrowser then
|
||||||
|
self._padCursorActive = false
|
||||||
|
Kit.FileBrowser.open({
|
||||||
|
title = "Select Mod (.zip)",
|
||||||
|
mode = "mod",
|
||||||
|
onSelect = function(pickedPath)
|
||||||
|
self:_installMod(pickedPath)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function requiredManifest(self, modId)
|
local function requiredManifest(self, modId)
|
||||||
@@ -2059,6 +2120,122 @@ local function requiredImportNotice(self, modId, importId, text)
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Current Android/iOS bridges can stream a raw required import directly from
|
||||||
|
-- the system document provider into its engine-owned baseroms destination.
|
||||||
|
-- The bridge hashes/counts the same bytes and then publishes this tiny marker:
|
||||||
|
-- v1\n<relative destination>\n<md5>\n<byte count>\n
|
||||||
|
-- Older mobile builds still stage picked_required_import.bin, so this is an
|
||||||
|
-- additive completion path rather than a replacement for the legacy one.
|
||||||
|
local function directRequiredImportTarget(self, marker)
|
||||||
|
local fields = {}
|
||||||
|
for line in tostring(marker or ""):gmatch("[^\r\n]+") do
|
||||||
|
fields[#fields + 1] = line
|
||||||
|
if #fields > 4 then break end
|
||||||
|
end
|
||||||
|
local version, path, digest, count = fields[1], fields[2], fields[3], fields[4]
|
||||||
|
if version ~= "v1" or not path or not digest or not count or #digest ~= 32
|
||||||
|
or not digest:match("^%x+$") or not count:match("^%d+$") then
|
||||||
|
return nil, "The completed dependency marker was malformed."
|
||||||
|
end
|
||||||
|
digest = digest:lower()
|
||||||
|
count = tonumber(count)
|
||||||
|
if not count then return nil, "The completed dependency size was invalid." end
|
||||||
|
|
||||||
|
if not self.mods and self._refreshMods then pcall(self._refreshMods, self) end
|
||||||
|
local RequiredImports = require("src.mods.RequiredImports")
|
||||||
|
|
||||||
|
local function matchRow(row, wantedImportId)
|
||||||
|
local manifest = row and row.manifest
|
||||||
|
if not manifest then return nil end
|
||||||
|
for _, spec in ipairs(RequiredImports.specs(manifest)) do
|
||||||
|
if (not wantedImportId or spec.id == wantedImportId)
|
||||||
|
and RequiredImports.path(manifest, spec) == path then
|
||||||
|
return manifest, spec, row.id or manifest.id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Normal resume: bind the marker to exactly the request that opened picker.
|
||||||
|
if self.pickerPendingModId and self.pickerPendingImportId then
|
||||||
|
for _, row in ipairs(self.mods or {}) do
|
||||||
|
if row.id == self.pickerPendingModId then
|
||||||
|
local manifest, spec, modId = matchRow(row, self.pickerPendingImportId)
|
||||||
|
if manifest then return path, digest, count, manifest, spec, modId end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil, "The completed dependency did not match the pending import request."
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Android may recreate GameActivity while DocumentsUI is open. If Lua was
|
||||||
|
-- restarted too, recover by the exact engine-owned destination. The path
|
||||||
|
-- includes the mod id and only a manifest-declared import can match it.
|
||||||
|
for _, row in ipairs(self.mods or {}) do
|
||||||
|
local manifest, spec, modId = matchRow(row)
|
||||||
|
if manifest then return path, digest, count, manifest, spec, modId end
|
||||||
|
end
|
||||||
|
return nil, "The completed dependency did not match an installed mod request."
|
||||||
|
end
|
||||||
|
|
||||||
|
finishDirectRequiredImport = function(self, marker)
|
||||||
|
local path, digestOrErr, nativeSize, manifest, spec, modId =
|
||||||
|
directRequiredImportTarget(self, marker)
|
||||||
|
if not path then
|
||||||
|
self.modNotice = { ok = false, text = tostring(digestOrErr) }
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local digest = digestOrErr
|
||||||
|
local RequiredImports = require("src.mods.RequiredImports")
|
||||||
|
local info = love.filesystem.getInfo(path, "file")
|
||||||
|
if not info or type(info.size) ~= "number" then
|
||||||
|
requiredImportNotice(self, modId, spec.id,
|
||||||
|
"The completed dependency file was not found.")
|
||||||
|
self.modNotice = nil
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
if info.size ~= nativeSize then
|
||||||
|
love.filesystem.remove(path)
|
||||||
|
if RequiredImports.receiptPath then
|
||||||
|
love.filesystem.remove(RequiredImports.receiptPath(manifest, spec))
|
||||||
|
end
|
||||||
|
requiredImportNotice(self, modId, spec.id,
|
||||||
|
("Dependency copy size changed after completion (expected %d bytes, found %d).")
|
||||||
|
:format(nativeSize, info.size))
|
||||||
|
self.modNotice = nil
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local sizeErr = RequiredImports.sizeError(spec, info.size, true)
|
||||||
|
if sizeErr then
|
||||||
|
love.filesystem.remove(path)
|
||||||
|
if RequiredImports.receiptPath then
|
||||||
|
love.filesystem.remove(RequiredImports.receiptPath(manifest, spec))
|
||||||
|
end
|
||||||
|
requiredImportNotice(self, modId, spec.id, sizeErr)
|
||||||
|
self.modNotice = nil
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- No second 665 MiB / 1.46 GiB read: native calculated this digest while
|
||||||
|
-- streaming the selected document into the one final copy.
|
||||||
|
local ok, detail = RequiredImports.acceptStoredDigest(
|
||||||
|
manifest, spec.id, digest, love.filesystem)
|
||||||
|
if not ok then
|
||||||
|
love.filesystem.remove(path)
|
||||||
|
if RequiredImports.receiptPath then
|
||||||
|
love.filesystem.remove(RequiredImports.receiptPath(manifest, spec))
|
||||||
|
end
|
||||||
|
requiredImportNotice(self, modId, spec.id, detail)
|
||||||
|
self.modNotice = nil
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
self.requiredImportNotice = nil
|
||||||
|
self.modNotice = { ok = true, text = "Imported " .. tostring(spec.id)
|
||||||
|
.. " for " .. tostring(manifest.name or manifest.id) .. "." }
|
||||||
|
self:_refreshMods()
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
function RomImporter:_importRequiredData(modId, importId, data)
|
function RomImporter:_importRequiredData(modId, importId, data)
|
||||||
local manifest = requiredManifest(self, modId)
|
local manifest = requiredManifest(self, modId)
|
||||||
if not manifest then
|
if not manifest then
|
||||||
@@ -2218,7 +2395,15 @@ function RomImporter:chooseRequiredImport(modId, importId)
|
|||||||
self.pickerPendingModId = modId
|
self.pickerPendingModId = modId
|
||||||
self.pickerPendingImportId = importId
|
self.pickerPendingImportId = importId
|
||||||
self.requiredImportLegacyRomPick = legacyAndroidPicker or nil
|
self.requiredImportLegacyRomPick = legacyAndroidPicker or nil
|
||||||
if not pickFile(legacyAndroidPicker and "rom" or "required_import") then
|
-- Raw imports can go straight to their final private destination on current
|
||||||
|
-- Android/iOS bridges. N64 stays on staging because the launcher still has
|
||||||
|
-- to canonicalize byte order/copier headers before storing it.
|
||||||
|
local directDestination = (self.mobileFileBridge and not legacyAndroidPicker
|
||||||
|
and spec.format ~= "n64"
|
||||||
|
and type(manifest.path) == "string" and manifest.path ~= "")
|
||||||
|
and require("src.mods.RequiredImports").path(manifest, spec) or nil
|
||||||
|
if not pickFile(legacyAndroidPicker and "rom" or "required_import",
|
||||||
|
directDestination) then
|
||||||
self.pickerPendingKind = nil
|
self.pickerPendingKind = nil
|
||||||
self.pickerPendingModId = nil
|
self.pickerPendingModId = nil
|
||||||
self.pickerPendingImportId = nil
|
self.pickerPendingImportId = nil
|
||||||
@@ -2333,8 +2518,42 @@ function RomImporter:chooseSaveImport(version)
|
|||||||
end
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
local isHandheld = os.getenv("HANDHELD") == "1" or os.getenv("PORTMASTER") == "1"
|
||||||
|
or os.getenv("POKEPORT_HANDHELD") == "1" or os.getenv("TRIMUI") == "1"
|
||||||
|
or os.getenv("MUOS") == "1" or os.getenv("KNULLI") == "1"
|
||||||
|
|
||||||
|
if isHandheld then
|
||||||
|
local okKit, Kit = pcall(require, "src.ui.kit.Kit")
|
||||||
|
if okKit and Kit.FileBrowser then
|
||||||
|
self._padCursorActive = false
|
||||||
|
Kit.FileBrowser.open({
|
||||||
|
title = "Select Save (.sav)",
|
||||||
|
mode = "save",
|
||||||
|
onSelect = function(pickedPath)
|
||||||
|
self:_importSave(version, pickedPath)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local path = chooseSav()
|
local path = chooseSav()
|
||||||
if path then self:_importSave(version, path) end
|
if path then
|
||||||
|
self:_importSave(version, path)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local okKit, Kit = pcall(require, "src.ui.kit.Kit")
|
||||||
|
if okKit and Kit.FileBrowser then
|
||||||
|
self._padCursorActive = false
|
||||||
|
Kit.FileBrowser.open({
|
||||||
|
title = "Select Save (.sav)",
|
||||||
|
mode = "save",
|
||||||
|
onSelect = function(pickedPath)
|
||||||
|
self:_importSave(version, pickedPath)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- "Export save" button: write the active slot back out to a raw .sav in the save
|
-- "Export save" button: write the active slot back out to a raw .sav in the save
|
||||||
@@ -2475,11 +2694,42 @@ function RomImporter:choose(version)
|
|||||||
end
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
local isHandheld = os.getenv("HANDHELD") == "1" or os.getenv("PORTMASTER") == "1"
|
||||||
|
or os.getenv("POKEPORT_HANDHELD") == "1" or os.getenv("TRIMUI") == "1"
|
||||||
|
or os.getenv("MUOS") == "1" or os.getenv("KNULLI") == "1"
|
||||||
|
|
||||||
|
if isHandheld then
|
||||||
|
local okKit, Kit = pcall(require, "src.ui.kit.Kit")
|
||||||
|
if okKit and Kit.FileBrowser then
|
||||||
|
self._padCursorActive = false
|
||||||
|
Kit.FileBrowser.open({
|
||||||
|
title = "Select " .. (GameVersion.info(self.chooseVersion).displayName or "ROM"),
|
||||||
|
mode = "rom",
|
||||||
|
onSelect = function(pickedPath)
|
||||||
|
self:startPath(pickedPath)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local path = chooseRom(GameVersion.info(self.chooseVersion).displayName)
|
local path = chooseRom(GameVersion.info(self.chooseVersion).displayName)
|
||||||
if path then
|
if path then
|
||||||
self:startPath(path)
|
self:startPath(path)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
local okKit, Kit = pcall(require, "src.ui.kit.Kit")
|
||||||
|
if okKit and Kit.FileBrowser then
|
||||||
|
self._padCursorActive = false
|
||||||
|
Kit.FileBrowser.open({
|
||||||
|
title = "Select " .. (GameVersion.info(self.chooseVersion).displayName or "ROM"),
|
||||||
|
mode = "rom",
|
||||||
|
onSelect = function(pickedPath)
|
||||||
|
self:startPath(pickedPath)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
end
|
||||||
-- Handheld Linux (Anbernic stock OS / PortMaster) rarely has zenity or
|
-- Handheld Linux (Anbernic stock OS / PortMaster) rarely has zenity or
|
||||||
-- kdialog. Fall back to the same "drop a .gb/.gbc next to the game" scan
|
-- kdialog. Fall back to the same "drop a .gb/.gbc next to the game" scan
|
||||||
-- used on Android, which works when the game is launched as an unpacked
|
-- used on Android, which works when the game is launched as an unpacked
|
||||||
@@ -2549,6 +2799,8 @@ function RomImporter:_pollPickedFiles(dt)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
local found = love.filesystem.getInfo("export_done.flag", "file") ~= nil
|
local found = love.filesystem.getInfo("export_done.flag", "file") ~= nil
|
||||||
|
or love.filesystem.getInfo("pick_error.flag", "file") ~= nil
|
||||||
|
or love.filesystem.getInfo(PICK_COMPLETE_FILENAME, "file") ~= nil
|
||||||
if not found then
|
if not found then
|
||||||
for _, name in ipairs(love.filesystem.getDirectoryItems("")) do
|
for _, name in ipairs(love.filesystem.getDirectoryItems("")) do
|
||||||
local n = name:lower()
|
local n = name:lower()
|
||||||
@@ -2850,6 +3102,14 @@ function RomImporter:_cycleTab(delta)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function RomImporter:_updatePadCursor(dt)
|
function RomImporter:_updatePadCursor(dt)
|
||||||
|
local okKit, Kit = pcall(require, "src.ui.kit.Kit")
|
||||||
|
if okKit then
|
||||||
|
if (Kit.FileBrowser and Kit.FileBrowser.active)
|
||||||
|
or (Kit.VirtualKeyboard and Kit.VirtualKeyboard.active) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
if self.isNX then
|
if self.isNX then
|
||||||
self:_ensureNxPointerBridge()
|
self:_ensureNxPointerBridge()
|
||||||
-- Cap dt so a hitch in the FlexLove immediate-mode frame does not fling
|
-- Cap dt so a hitch in the FlexLove immediate-mode frame does not fling
|
||||||
@@ -2892,14 +3152,6 @@ function RomImporter:_updatePadCursor(dt)
|
|||||||
local ny = self._padCursor.y + dy * speed * dt
|
local ny = self._padCursor.y + dy * speed * dt
|
||||||
self._padCursor.x = math.max(ox, math.min(ox + w, nx))
|
self._padCursor.x = math.max(ox, math.min(ox + w, nx))
|
||||||
self._padCursor.y = math.max(oy, math.min(oy + h, ny))
|
self._padCursor.y = math.max(oy, math.min(oy + h, ny))
|
||||||
-- Pushing INTO the top/bottom edge scrolls the page instead of stalling.
|
|
||||||
-- The cursor is clamped to the safe area above, so on a short window the
|
|
||||||
-- rows below the fold are unreachable on a stickless handheld: no mouse
|
|
||||||
-- wheel, no touchscreen, and no right stick to feed the existing wheel
|
|
||||||
-- path. Only the OVERSHOOT scrolls -- parking the cursor at the edge does
|
|
||||||
-- nothing, it has to be actively pushed -- and this block only runs on pad
|
|
||||||
-- input, so a real mouse is unaffected. /48 matches the pixels-per-notch
|
|
||||||
-- LauncherView.draw multiplies back out.
|
|
||||||
local overY = 0
|
local overY = 0
|
||||||
if ny > oy + h then overY = ny - (oy + h)
|
if ny > oy + h then overY = ny - (oy + h)
|
||||||
elseif ny < oy then overY = ny - oy end
|
elseif ny < oy then overY = ny - oy end
|
||||||
@@ -2907,17 +3159,12 @@ function RomImporter:_updatePadCursor(dt)
|
|||||||
if overY ~= 0 and self._flex then
|
if overY ~= 0 and self._flex then
|
||||||
require("src.import.LauncherView").wheelmoved(self, 0, -overY / 48)
|
require("src.import.LauncherView").wheelmoved(self, 0, -overY / 48)
|
||||||
end
|
end
|
||||||
-- Desktop: FlexLove polls the real mouse, so warp it with the pad pointer.
|
|
||||||
-- NX: the getPosition bridge already returns pad coords -- skip setPosition.
|
|
||||||
if not self.isNX and love.mouse.setPosition then
|
if not self.isNX and love.mouse.setPosition then
|
||||||
pcall(love.mouse.setPosition, self._padCursor.x, self._padCursor.y)
|
pcall(love.mouse.setPosition, self._padCursor.x, self._padCursor.y)
|
||||||
self._lastMouseX, self._lastMouseY = self._padCursor.x, self._padCursor.y
|
self._lastMouseX, self._lastMouseY = self._padCursor.x, self._padCursor.y
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Right stick scrolls whatever the pad pointer sits over, through the
|
|
||||||
-- view's wheel path, so the page and the modal scrollers all behave like a
|
|
||||||
-- mouse wheel would.
|
|
||||||
local ry = self._padAxis.righty or 0
|
local ry = self._padAxis.righty or 0
|
||||||
if math.abs(ry) > PAD_DEAD and self._flex then
|
if math.abs(ry) > PAD_DEAD and self._flex then
|
||||||
self:_activatePadCursor()
|
self:_activatePadCursor()
|
||||||
@@ -2926,30 +3173,168 @@ function RomImporter:_updatePadCursor(dt)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function RomImporter:gamepadpressed(_, button)
|
function RomImporter:gamepadpressed(_, button)
|
||||||
self:_activatePadCursor()
|
local action = (GamepadMap.mapLauncherButton and GamepadMap.mapLauncherButton(button)) or button
|
||||||
-- Map through GamepadMap so NX swaps SDL face labels to Nintendo A/B.
|
local okKit, Kit = pcall(require, "src.ui.kit.Kit")
|
||||||
local action = GamepadMap.mapGamepadButton(button)
|
if okKit then
|
||||||
if action == "a" then
|
if Kit.VirtualKeyboard and Kit.VirtualKeyboard.active then
|
||||||
-- Instant click at the virtual pointer: dispatched straight into the
|
if Kit.VirtualKeyboard.gamepadpressed(action) then return end
|
||||||
-- view, since the launcher no longer hit-tests presses itself.
|
|
||||||
if self._flex then
|
|
||||||
require("src.import.LauncherView").clickAt(self,
|
|
||||||
self._padCursor.x, self._padCursor.y)
|
|
||||||
end
|
end
|
||||||
elseif button == "leftshoulder" then
|
if Kit.FileBrowser and Kit.FileBrowser.active then
|
||||||
|
if Kit.FileBrowser.gamepadpressed(action) then return end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Y button toggle between Native Controller Navigation and Virtual Pointer Cursor:
|
||||||
|
if action == "y" or button == "y" then
|
||||||
|
self._padCursorActive = not self._padCursorActive
|
||||||
|
if okKit then Kit._ringShown = not self._padCursorActive end
|
||||||
|
self._cursorModeToast = self._padCursorActive and "Cursor Navigation [Y]" or "Controller Menu Navigation [Y]"
|
||||||
|
self._cursorModeToastTime = love.timer.getTime()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Select button: toggle Virtual Keyboard
|
||||||
|
if button == "back" or button == "select" or action == "select" then
|
||||||
|
if okKit and Kit.VirtualKeyboard then
|
||||||
|
if Kit.VirtualKeyboard.active then
|
||||||
|
Kit.VirtualKeyboard.close(false)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if self._indexPrompt then
|
||||||
|
Kit.VirtualKeyboard.open({
|
||||||
|
text = self._indexPrompt.text or "",
|
||||||
|
title = "Add a mod index",
|
||||||
|
onDone = function(newText, confirmed)
|
||||||
|
if confirmed and self._indexPrompt then self._indexPrompt.text = newText end
|
||||||
|
end
|
||||||
|
})
|
||||||
|
return
|
||||||
|
elseif self._rename then
|
||||||
|
Kit.VirtualKeyboard.open({
|
||||||
|
text = self._rename.text or "",
|
||||||
|
title = "Name save slot",
|
||||||
|
onDone = function(newText, confirmed)
|
||||||
|
if confirmed and self._rename then self._rename.text = newText end
|
||||||
|
end
|
||||||
|
})
|
||||||
|
return
|
||||||
|
elseif self._profileRenamePrompt then
|
||||||
|
Kit.VirtualKeyboard.open({
|
||||||
|
text = self._profileRenamePrompt.text or "",
|
||||||
|
title = "Rename profile",
|
||||||
|
onDone = function(newText, confirmed)
|
||||||
|
if confirmed and self._profileRenamePrompt then self._profileRenamePrompt.text = newText end
|
||||||
|
end
|
||||||
|
})
|
||||||
|
return
|
||||||
|
elseif self._profileSavePrompt then
|
||||||
|
Kit.VirtualKeyboard.open({
|
||||||
|
text = self._profileSavePrompt.text or "",
|
||||||
|
title = "Save mod profile",
|
||||||
|
onDone = function(newText, confirmed)
|
||||||
|
if confirmed and self._profileSavePrompt then self._profileSavePrompt.text = newText end
|
||||||
|
end
|
||||||
|
})
|
||||||
|
return
|
||||||
|
elseif self._settingsText then
|
||||||
|
Kit.VirtualKeyboard.open({
|
||||||
|
text = self._settingsText.text or "",
|
||||||
|
title = (self._settingsText.row and self._settingsText.row.label) or "Edit Text",
|
||||||
|
onDone = function(newText, confirmed)
|
||||||
|
if confirmed and self._settingsText then self._settingsText.text = newText end
|
||||||
|
end
|
||||||
|
})
|
||||||
|
return
|
||||||
|
elseif self.tab == "find" then
|
||||||
|
Kit.VirtualKeyboard.open({
|
||||||
|
text = self._findQuery or "",
|
||||||
|
title = "Search Mods",
|
||||||
|
onDone = function(newText, confirmed)
|
||||||
|
if confirmed then
|
||||||
|
self._findQuery = newText
|
||||||
|
if self._refreshFind then self:_refreshFind() end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
})
|
||||||
|
return
|
||||||
|
else
|
||||||
|
Kit.VirtualKeyboard.open({
|
||||||
|
text = "",
|
||||||
|
title = "Virtual Keyboard",
|
||||||
|
onDone = function() end
|
||||||
|
})
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Shoulder buttons: cycle tabs
|
||||||
|
if button == "leftshoulder" then
|
||||||
self:_cycleTab(-1)
|
self:_cycleTab(-1)
|
||||||
|
return
|
||||||
elseif button == "rightshoulder" then
|
elseif button == "rightshoulder" then
|
||||||
self:_cycleTab(1)
|
self:_cycleTab(1)
|
||||||
elseif button == "dpup" or button == "dpdown"
|
return
|
||||||
or button == "dpleft" or button == "dpright" then
|
end
|
||||||
self._padDir[button] = true
|
|
||||||
elseif button == "start" or button == "back" then
|
-- Start button: Play / Choose ROM
|
||||||
-- Start / Select: Play if ready, else Choose ROM on the active game tab.
|
if button == "start" then
|
||||||
if self.workState == "working" then return end
|
if self.workState == "working" then return end
|
||||||
local version = self.tab
|
local version = self.tab
|
||||||
if GameVersion.VERSIONS[version] then
|
if GameVersion.VERSIONS[version] then
|
||||||
if self.ready[version] then self:play(version) else self:_romAction(version) end
|
if self.ready[version] then self:play(version) else self:_romAction(version) end
|
||||||
end
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if not self._padCursorActive and not self.isNX then
|
||||||
|
if okKit then Kit._ringShown = true end
|
||||||
|
if action == "a" then
|
||||||
|
if okKit and Kit.focusId then
|
||||||
|
Kit.activateFocused()
|
||||||
|
elseif self._flex then
|
||||||
|
require("src.import.LauncherView").clickAt(self,
|
||||||
|
self._padCursor.x, self._padCursor.y)
|
||||||
|
end
|
||||||
|
return
|
||||||
|
elseif action == "b" then
|
||||||
|
if self._indexPrompt then self._indexPrompt = nil; self:_disarmTextInput(); return
|
||||||
|
elseif self._rename then self._rename = nil; self:_disarmTextInput(); return
|
||||||
|
elseif self._profileRenamePrompt then self._profileRenamePrompt = nil; self:_disarmTextInput(); return
|
||||||
|
elseif self._profileSavePrompt then self._profileSavePrompt = nil; self:_disarmTextInput(); return
|
||||||
|
elseif self._settingsText then self._settingsText = nil; self:_disarmTextInput(); return
|
||||||
|
end
|
||||||
|
elseif button == "dpup" or action == "dpup" then
|
||||||
|
if okKit then Kit.navigate("up") end
|
||||||
|
return
|
||||||
|
elseif button == "dpdown" or action == "dpdown" then
|
||||||
|
if okKit then Kit.navigate("down") end
|
||||||
|
return
|
||||||
|
elseif button == "dpleft" or action == "dpleft" then
|
||||||
|
if okKit then Kit.navigate("left") end
|
||||||
|
return
|
||||||
|
elseif button == "dpright" or action == "dpright" then
|
||||||
|
if okKit then Kit.navigate("right") end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
else
|
||||||
|
self:_activatePadCursor()
|
||||||
|
if action == "a" then
|
||||||
|
if self._flex then
|
||||||
|
require("src.import.LauncherView").clickAt(self,
|
||||||
|
self._padCursor.x, self._padCursor.y)
|
||||||
|
end
|
||||||
|
elseif action == "b" then
|
||||||
|
if self._indexPrompt then self._indexPrompt = nil; self:_disarmTextInput(); return
|
||||||
|
elseif self._rename then self._rename = nil; self:_disarmTextInput(); return
|
||||||
|
elseif self._profileRenamePrompt then self._profileRenamePrompt = nil; self:_disarmTextInput(); return
|
||||||
|
elseif self._profileSavePrompt then self._profileSavePrompt = nil; self:_disarmTextInput(); return
|
||||||
|
elseif self._settingsText then self._settingsText = nil; self:_disarmTextInput(); return
|
||||||
|
end
|
||||||
|
elseif button == "dpup" or button == "dpdown"
|
||||||
|
or button == "dpleft" or button == "dpright" then
|
||||||
|
self._padDir[button] = true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -2963,9 +3348,9 @@ end
|
|||||||
function RomImporter:gamepadaxis(_, axis, value)
|
function RomImporter:gamepadaxis(_, axis, value)
|
||||||
if axis == "leftx" or axis == "lefty" or axis == "righty" then
|
if axis == "leftx" or axis == "lefty" or axis == "righty" then
|
||||||
self._padAxis[axis] = value
|
self._padAxis[axis] = value
|
||||||
if math.abs(value) > PAD_DEAD then
|
if self._padCursorActive and math.abs(value) > PAD_DEAD then
|
||||||
self:_activatePadCursor()
|
self:_activatePadCursor()
|
||||||
elseif axis == "lefty" then
|
elseif axis == "lefty" and math.abs(value) <= PAD_DEAD then
|
||||||
self._padStickCentered = true
|
self._padStickCentered = true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -3914,6 +4299,15 @@ function RomImporter:fileUrl(path)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function RomImporter:keypressed(key)
|
function RomImporter:keypressed(key)
|
||||||
|
local okKit, Kit = pcall(require, "src.ui.kit.Kit")
|
||||||
|
if okKit then
|
||||||
|
if Kit.FileBrowser and Kit.FileBrowser.active then
|
||||||
|
if Kit.FileBrowser.keypressed(key) then return end
|
||||||
|
end
|
||||||
|
if Kit.VirtualKeyboard and Kit.VirtualKeyboard.active then
|
||||||
|
if Kit.VirtualKeyboard.keypressed(key) then return end
|
||||||
|
end
|
||||||
|
end
|
||||||
if self._profileSavePrompt then
|
if self._profileSavePrompt then
|
||||||
if key == "backspace" then
|
if key == "backspace" then
|
||||||
self._profileSavePrompt.text = utf8Back(self._profileSavePrompt.text or "")
|
self._profileSavePrompt.text = utf8Back(self._profileSavePrompt.text or "")
|
||||||
@@ -4803,6 +5197,7 @@ function RomImporter:_refreshMods()
|
|||||||
local LauncherMods = require("src.mods.LauncherMods")
|
local LauncherMods = require("src.mods.LauncherMods")
|
||||||
local SaveData = require("src.core.SaveData")
|
local SaveData = require("src.core.SaveData")
|
||||||
self._cartPlan = nil
|
self._cartPlan = nil
|
||||||
|
self._profileCache = nil
|
||||||
self.findInstalled = nil
|
self.findInstalled = nil
|
||||||
self.safeMode = SaveData.isSafeMode(SaveData.loadOptions())
|
self.safeMode = SaveData.isSafeMode(SaveData.loadOptions())
|
||||||
-- Once per session, ahead of the first listing: pull in any mod the player
|
-- Once per session, ahead of the first listing: pull in any mod the player
|
||||||
|
|||||||
@@ -184,6 +184,14 @@ local function adjacentSleepingSnorlax(save, ow)
|
|||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function itemUseLine(data, save, name)
|
||||||
|
return romText(data, "_ItemUseText001", "%s used\n%s!", save.player.name, name)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- PrintItemUseTextAndRemoveItem (item_effects.asm): used-line + SFX_HEAL_AILMENT.
|
||||||
|
-- BagMenu plays Heal_Ailment via TextBox.soundOpts when extra.useJingle is set.
|
||||||
|
local USE_JINGLE = { useJingle = true }
|
||||||
|
|
||||||
-- Use an item on a target party mon (target may be nil for targetless
|
-- Use an item on a target party mon (target may be nil for targetless
|
||||||
-- items). data = generated data tables; battle = BattleState when used
|
-- items). data = generated data tables; battle = BattleState when used
|
||||||
-- mid-battle; ow = the overworld (OverworldState), needed only to check
|
-- mid-battle; ow = the overworld (OverworldState), needed only to check
|
||||||
@@ -280,7 +288,7 @@ function ItemEffects.use(data, save, itemId, target, battle, moveIndex, ow)
|
|||||||
"All sleeping\nPOKéMON woke up!") }
|
"All sleeping\nPOKéMON woke up!") }
|
||||||
end
|
end
|
||||||
|
|
||||||
-- battle-only items
|
-- battle-only items (PrintItemUseTextAndRemoveItem + Heal_Ailment, #1635)
|
||||||
if X_ITEMS[itemId] or itemId == "DIRE_HIT" or itemId == "GUARD_SPEC"
|
if X_ITEMS[itemId] or itemId == "DIRE_HIT" or itemId == "GUARD_SPEC"
|
||||||
or itemId == "POKE_DOLL" then
|
or itemId == "POKE_DOLL" then
|
||||||
if not battle then
|
if not battle then
|
||||||
@@ -293,43 +301,53 @@ function ItemEffects.use(data, save, itemId, target, battle, moveIndex, ow)
|
|||||||
require("src.world.PikachuFollower")
|
require("src.world.PikachuFollower")
|
||||||
.modifyHappiness(save, "USEDXITEM", b and b.mon)
|
.modifyHappiness(save, "USEDXITEM", b and b.mon)
|
||||||
end
|
end
|
||||||
|
local used = itemUseLine(data, save, name)
|
||||||
if itemId == "X_ACCURACY" then
|
if itemId == "X_ACCURACY" then
|
||||||
-- ItemUseXAccuracy sets USING_X_ACCURACY: moves never miss
|
-- ItemUseXAccuracy sets USING_X_ACCURACY: moves never miss
|
||||||
-- (not an accuracy stage)
|
-- (not an accuracy stage); vanilla prints only the used line
|
||||||
b.xAccuracy = true
|
b.xAccuracy = true
|
||||||
return "consumed", { Strings("%s's\nhits will never\nmiss!", b.name) }
|
return "consumed", { used }, USE_JINGLE
|
||||||
end
|
end
|
||||||
if X_ITEMS[itemId] then
|
if X_ITEMS[itemId] then
|
||||||
local stat = X_ITEMS[itemId]
|
local stat = X_ITEMS[itemId]
|
||||||
local cur = b.stages[stat] or 0
|
local cur = b.stages[stat] or 0
|
||||||
-- ItemUseXStat removes the item BEFORE running the stat-up
|
-- ItemUseXStat: PrintItemUseTextAndRemoveItem, then StatModifierUpEffect
|
||||||
-- effect, so at +6 it is still consumed and StatModifierUpEffect
|
|
||||||
-- just prints "Nothing happened!"
|
|
||||||
if cur >= 6 then
|
if cur >= 6 then
|
||||||
return "consumed", { romText(data, "_NothingHappenedText",
|
return "consumed", { used }, {
|
||||||
"Nothing happened!") }
|
useJingle = true,
|
||||||
|
afterMessages = { romText(data, "_NothingHappenedText",
|
||||||
|
"Nothing happened!") },
|
||||||
|
}
|
||||||
end
|
end
|
||||||
b.stages[stat] = cur + 1
|
b.stages[stat] = cur + 1
|
||||||
return "consumed", { Strings("%s's\n%s rose!", b.name, Strings(STAT_LABEL[stat])) }
|
b.hazeStatReset = nil
|
||||||
|
if battle.ruleset and battle.ruleset.badgeBoostReapplyBug
|
||||||
|
and battle.kind ~= "link" then
|
||||||
|
require("src.battle.Damage").reapplyBadgeBoosts(b, stat)
|
||||||
|
end
|
||||||
|
return "consumed", { used }, {
|
||||||
|
useJingle = true,
|
||||||
|
afterMessages = { Strings("%s's\n%s rose!", b.name,
|
||||||
|
Strings(STAT_LABEL[stat])) },
|
||||||
|
}
|
||||||
end
|
end
|
||||||
-- ItemUseDireHit/ItemUseGuardSpec always set the bit and consume
|
-- ItemUseDireHit/ItemUseGuardSpec always set the bit and consume
|
||||||
-- the item, even when it is already active
|
-- the item, even when it is already active; vanilla prints only used
|
||||||
if itemId == "DIRE_HIT" then
|
if itemId == "DIRE_HIT" then
|
||||||
b.focusEnergy = true
|
b.focusEnergy = true
|
||||||
return "consumed", { romText(data, "_GettingPumpedText",
|
return "consumed", { used }, USE_JINGLE
|
||||||
"%s's\ngetting pumped!", b.name) }
|
|
||||||
end
|
end
|
||||||
if itemId == "GUARD_SPEC" then
|
if itemId == "GUARD_SPEC" then
|
||||||
b.mist = true
|
b.mist = true
|
||||||
return "consumed", { Strings("%s's\nprotected against\nstat changes!", b.name) }
|
return "consumed", { used }, USE_JINGLE
|
||||||
end
|
end
|
||||||
if itemId == "POKE_DOLL" then
|
if itemId == "POKE_DOLL" then
|
||||||
if battle.kind ~= "wild" then
|
if battle.kind ~= "wild" then
|
||||||
-- ItemUsePokeDoll jumps to ItemUseNotTime in trainer battles
|
-- ItemUsePokeDoll jumps to ItemUseNotTime in trainer battles
|
||||||
return "failed", { notTime(data, save) }
|
return "failed", { notTime(data, save) }
|
||||||
end
|
end
|
||||||
return "consumed_escape", { romText(data, "_WildRanText",
|
-- PrintItemUseTextAndRemoveItem then escape
|
||||||
"The wild POKéMON\nran away!", battle.enemy and battle.enemy.name) }
|
return "consumed_escape", { used }, USE_JINGLE
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -608,7 +626,7 @@ function ItemEffects.use(data, save, itemId, target, battle, moveIndex, ow)
|
|||||||
if REPELS[itemId] then
|
if REPELS[itemId] then
|
||||||
local steps = itemId == "REPEL" and 100 or itemId == "SUPER_REPEL" and 200 or 250
|
local steps = itemId == "REPEL" and 100 or itemId == "SUPER_REPEL" and 200 or 250
|
||||||
save.repelSteps = steps
|
save.repelSteps = steps
|
||||||
return "consumed", { Strings("%s used\n%s!", save.player.name, name) }
|
return "consumed", { itemUseLine(data, save, name) }, USE_JINGLE
|
||||||
end
|
end
|
||||||
|
|
||||||
return "failed", { notTime(data, save) }
|
return "failed", { notTime(data, save) }
|
||||||
|
|||||||
@@ -163,8 +163,8 @@ end
|
|||||||
|
|
||||||
-- a module the build dropped disables its registry rather than the game:
|
-- a module the build dropped disables its registry rather than the game:
|
||||||
-- the consumer still reads its own table, so vanilla keeps working
|
-- the consumer still reads its own table, so vanilla keeps working
|
||||||
local function load(path)
|
local function load(path, moduleLoader)
|
||||||
local ok, module = pcall(require, path)
|
local ok, module = pcall(moduleLoader or require, path)
|
||||||
if ok then return module end
|
if ok then return module end
|
||||||
Logger.warn("builtin registrations skipped for %s (%s)", path, tostring(module))
|
Logger.warn("builtin registrations skipped for %s (%s)", path, tostring(module))
|
||||||
return nil
|
return nil
|
||||||
@@ -187,14 +187,14 @@ local function isolate(registry)
|
|||||||
}, { __index = registry })
|
}, { __index = registry })
|
||||||
end
|
end
|
||||||
|
|
||||||
function Builtins.install(content, data, generation)
|
function Builtins.install(content, data, generation, moduleLoader)
|
||||||
for _, entry in ipairs(registrantsFor(generation)) do
|
for _, entry in ipairs(registrantsFor(generation)) do
|
||||||
local registry = content[entry.name] and isolate(content[entry.name])
|
local registry = content[entry.name] and isolate(content[entry.name])
|
||||||
if registry then
|
if registry then
|
||||||
if entry.install then
|
if entry.install then
|
||||||
local modules, complete = {}, true
|
local modules, complete = {}, true
|
||||||
for i, path in ipairs(entry.modules) do
|
for i, path in ipairs(entry.modules) do
|
||||||
modules[i] = load(path)
|
modules[i] = load(path, moduleLoader)
|
||||||
if modules[i] == nil then complete = false end
|
if modules[i] == nil then complete = false end
|
||||||
end
|
end
|
||||||
-- data is the fourth argument, not the second, so the existing
|
-- data is the fourth argument, not the second, so the existing
|
||||||
@@ -204,7 +204,7 @@ function Builtins.install(content, data, generation)
|
|||||||
entry.install(registry, modules, Builtins.OWNER, data)
|
entry.install(registry, modules, Builtins.OWNER, data)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
local module = load(entry.from)
|
local module = load(entry.from, moduleLoader)
|
||||||
-- entry.fn names the entry point for a module that owns more than one
|
-- entry.fn names the entry point for a module that owns more than one
|
||||||
-- registry (Gold's Battle owns statuses and move_effects); the default
|
-- registry (Gold's Battle owns statuses and move_effects); the default
|
||||||
-- is the registerInto every single-registry module exposes
|
-- is the registerInto every single-registry module exposes
|
||||||
|
|||||||
@@ -0,0 +1,412 @@
|
|||||||
|
-- Read-only, bounded views over version-scoped generated datasets.
|
||||||
|
|
||||||
|
local CacheContract = require("src.import.CacheContract")
|
||||||
|
local DatasetHydration = require("src.core.DatasetHydration")
|
||||||
|
local GameVersion = require("src.core.GameVersion")
|
||||||
|
local Logger = require("src.core.Logger")
|
||||||
|
local SaveSerializer = require("src.core.SaveSerializer")
|
||||||
|
local Builtins = require("src.mods.Builtins")
|
||||||
|
local Registry = require("src.mods.Registry")
|
||||||
|
local Schemas = require("src.mods.Schemas")
|
||||||
|
|
||||||
|
local DatasetViews = {}
|
||||||
|
DatasetViews.__index = DatasetViews
|
||||||
|
|
||||||
|
-- A generated module is ROM-sized data, never an arbitrary program. These
|
||||||
|
-- caps bound both one malicious file and the aggregate work one open performs.
|
||||||
|
local DECODE_LIMITS = {
|
||||||
|
allowArray = true,
|
||||||
|
allowComments = true,
|
||||||
|
maxBytes = 8 * 1024 * 1024,
|
||||||
|
maxDepth = 64,
|
||||||
|
maxNodes = 500000,
|
||||||
|
maxStringBytes = 2 * 1024 * 1024,
|
||||||
|
maxTableEntries = 250000,
|
||||||
|
rootName = "generated data",
|
||||||
|
}
|
||||||
|
local MAX_AGGREGATE_BYTES = 48 * 1024 * 1024
|
||||||
|
|
||||||
|
local GEN1_ROOTS = {
|
||||||
|
constants = "constants", maps = "maps", tilesets = "tilesets",
|
||||||
|
text = "text", text_pointers = "text_pointers",
|
||||||
|
trainer_headers = "trainer_headers", font = "font", sprites = "sprites",
|
||||||
|
pokemon = "pokemon", moves = "moves", items = "items",
|
||||||
|
type_chart = "type_chart", trainers = "trainers",
|
||||||
|
encounters = "encounters", field = "field",
|
||||||
|
battle_anims = "battle_anims", audio = "audio",
|
||||||
|
palettes = "palettes", icons = "icons",
|
||||||
|
}
|
||||||
|
|
||||||
|
local GEN2_ROOTS = {
|
||||||
|
pokemon = "pokemon", moves = "moves", items = "items",
|
||||||
|
type_chart = "type_chart", audio = "audio", font = "font",
|
||||||
|
gen2Maps = "maps", gen2Tilesets = "tilesets", gen2Text = "text",
|
||||||
|
gen2Trainers = "trainers", gen2Encounters = "encounters",
|
||||||
|
gen2Sprites = "sprites", gen2Palettes = "palettes",
|
||||||
|
gen2Icons = "icons", gen2BattleAnims = "battle_anims",
|
||||||
|
gen2Constants = "constants", gen2Landmarks = "landmarks",
|
||||||
|
}
|
||||||
|
|
||||||
|
local function resolvePath(root, suffix)
|
||||||
|
local node = root
|
||||||
|
for key in suffix:gmatch("[^.]+") do
|
||||||
|
if type(node) ~= "table" then return nil end
|
||||||
|
node = node[key]
|
||||||
|
end
|
||||||
|
return node
|
||||||
|
end
|
||||||
|
|
||||||
|
local function assetRelative(path)
|
||||||
|
if type(path) ~= "string" or path == "" then
|
||||||
|
error("dataset asset path is required", 3)
|
||||||
|
end
|
||||||
|
if path:find("\\", 1, true) or path:sub(1, 1) == "/"
|
||||||
|
or path:find("[%z\1-\31]") then
|
||||||
|
error("dataset asset path must be a generated relative path", 3)
|
||||||
|
end
|
||||||
|
if path:sub(1, 17) ~= "assets/generated/" then
|
||||||
|
error("dataset assets are limited to assets/generated/", 3)
|
||||||
|
end
|
||||||
|
for segment in path:gmatch("[^/]+") do
|
||||||
|
if segment == "." or segment == ".." then
|
||||||
|
error("dataset asset path may not traverse directories", 3)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return path
|
||||||
|
end
|
||||||
|
|
||||||
|
local function copyData(value, state, depth)
|
||||||
|
state = state or { seen = {}, nodes = 0 }
|
||||||
|
state.nodes = state.nodes + 1
|
||||||
|
if state.nodes > DECODE_LIMITS.maxNodes then return nil, false end
|
||||||
|
local kind = type(value)
|
||||||
|
if kind == "nil" or kind == "boolean" or kind == "number" then return value end
|
||||||
|
if kind == "string" then
|
||||||
|
if #value > DECODE_LIMITS.maxStringBytes then return nil, false end
|
||||||
|
return value
|
||||||
|
end
|
||||||
|
if kind ~= "table" or getmetatable(value) ~= nil then return nil, false end
|
||||||
|
depth = (depth or 0) + 1
|
||||||
|
if depth > DECODE_LIMITS.maxDepth or state.seen[value] then return nil, false end
|
||||||
|
state.seen[value] = true
|
||||||
|
local out, entries = {}, 0
|
||||||
|
for key, child in pairs(value) do
|
||||||
|
entries = entries + 1
|
||||||
|
if entries > DECODE_LIMITS.maxTableEntries then return nil, false end
|
||||||
|
local keyCopy, keyOk = copyData(key, state, depth)
|
||||||
|
local childCopy, childOk = copyData(child, state, depth)
|
||||||
|
if keyOk == false or childOk == false or keyCopy == nil then return nil, false end
|
||||||
|
out[keyCopy] = childCopy
|
||||||
|
end
|
||||||
|
state.seen[value] = nil
|
||||||
|
return out, true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function isDataOnly(value, state, depth)
|
||||||
|
state = state or { seen = {}, nodes = 0 }
|
||||||
|
state.nodes = state.nodes + 1
|
||||||
|
if state.nodes > DECODE_LIMITS.maxNodes then return false end
|
||||||
|
local kind = type(value)
|
||||||
|
if kind == "nil" or kind == "boolean" or kind == "number" then return true end
|
||||||
|
if kind == "string" then return #value <= DECODE_LIMITS.maxStringBytes end
|
||||||
|
if kind ~= "table" or getmetatable(value) ~= nil then return false end
|
||||||
|
depth = (depth or 0) + 1
|
||||||
|
if depth > DECODE_LIMITS.maxDepth or state.seen[value] then return false end
|
||||||
|
state.seen[value] = true
|
||||||
|
local entries = 0
|
||||||
|
for key, child in pairs(value) do
|
||||||
|
entries = entries + 1
|
||||||
|
if entries > DECODE_LIMITS.maxTableEntries
|
||||||
|
or not isDataOnly(key, state, depth)
|
||||||
|
or not isDataOnly(child, state, depth) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
state.seen[value] = nil
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function DatasetViews.new(fs, engineRequire, decoder)
|
||||||
|
assert(fs and fs.read and fs.getInfo,
|
||||||
|
"DatasetViews.new requires a readable filesystem")
|
||||||
|
return setmetatable({ fs = fs, engineRequire = engineRequire or require,
|
||||||
|
decoder = decoder or SaveSerializer.decode, datasets = {} }, DatasetViews)
|
||||||
|
end
|
||||||
|
|
||||||
|
function DatasetViews:_preflight(version, inspected)
|
||||||
|
local paths, aggregate = {}, 0
|
||||||
|
local modules = CacheContract.semanticModules(version)
|
||||||
|
for _, name in ipairs(CacheContract.optionalSemanticModules(version)) do
|
||||||
|
local path = inspected.prefix .. "data/generated/" .. name .. ".lua"
|
||||||
|
if self.fs.getInfo(path, "file") then modules[#modules + 1] = name end
|
||||||
|
end
|
||||||
|
for _, name in ipairs(modules) do
|
||||||
|
local path = inspected.prefix .. "data/generated/" .. name .. ".lua"
|
||||||
|
local info = self.fs.getInfo(path, "file")
|
||||||
|
local size = info and info.size
|
||||||
|
if size and size > DECODE_LIMITS.maxBytes then return nil, name .. ": size limit" end
|
||||||
|
aggregate = aggregate + (size or 0)
|
||||||
|
if aggregate > MAX_AGGREGATE_BYTES then return nil, "aggregate size limit" end
|
||||||
|
paths[name] = path
|
||||||
|
end
|
||||||
|
return { paths = paths, key = table.concat(modules, "\n") }
|
||||||
|
end
|
||||||
|
|
||||||
|
local function resetInternal(view)
|
||||||
|
view.moduleCache, view.data, view.registries = {}, nil, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function DatasetViews:_reject(view, moduleName, source, detail)
|
||||||
|
view.invalid = { module = moduleName, source = source, detail = detail }
|
||||||
|
view.data, view.registries = nil, nil
|
||||||
|
Logger.warn("dataset %s cache rejected: %s", view.version, tostring(detail))
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function DatasetViews:_ready(view)
|
||||||
|
if view.invalid or view.unavailable then return false end
|
||||||
|
local inspected, _, detail = CacheContract.inspect(view.version, self.fs, {
|
||||||
|
allowSource = true, semantic = true,
|
||||||
|
})
|
||||||
|
if not inspected then
|
||||||
|
view.unavailable = true
|
||||||
|
if self.datasets[view.version] == view then self.datasets[view.version] = nil end
|
||||||
|
Logger.warn("dataset %s unavailable: %s", view.version, detail)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
local plan, invalid = self:_preflight(view.version, inspected)
|
||||||
|
if not plan then
|
||||||
|
self:_reject(view, nil, nil, invalid)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
if view.prefix ~= inspected.prefix or view.plan.key ~= plan.key then
|
||||||
|
view.prefix, view.plan = inspected.prefix, plan
|
||||||
|
resetInternal(view)
|
||||||
|
else
|
||||||
|
view.plan = plan
|
||||||
|
end
|
||||||
|
for name, cached in pairs(view.moduleCache) do
|
||||||
|
local source = self.fs.read(plan.paths[name])
|
||||||
|
if type(source) ~= "string" then
|
||||||
|
self:_reject(view, name, source, name .. ": unreadable generated module")
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
if source ~= cached.source then
|
||||||
|
resetInternal(view)
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return not view.invalid
|
||||||
|
end
|
||||||
|
|
||||||
|
function DatasetViews:_module(view, root)
|
||||||
|
local moduleName = view.modules[root]
|
||||||
|
if not moduleName then return nil end
|
||||||
|
local cached = view.moduleCache[moduleName]
|
||||||
|
if cached then return cached.value end
|
||||||
|
local path = view.plan.paths[moduleName]
|
||||||
|
if not path then return nil end
|
||||||
|
local source = self.fs.read(path)
|
||||||
|
if type(source) ~= "string" then
|
||||||
|
return self:_reject(view, moduleName, source,
|
||||||
|
moduleName .. ": unreadable generated module")
|
||||||
|
end
|
||||||
|
local aggregate = #source
|
||||||
|
for _, loaded in pairs(view.moduleCache) do aggregate = aggregate + #loaded.source end
|
||||||
|
if aggregate > MAX_AGGREGATE_BYTES then
|
||||||
|
return self:_reject(view, moduleName, source, "aggregate size limit")
|
||||||
|
end
|
||||||
|
local value, err = self.decoder(source, DECODE_LIMITS)
|
||||||
|
if type(value) ~= "table" then
|
||||||
|
return self:_reject(view, moduleName, source,
|
||||||
|
moduleName .. ": " .. tostring(err or "non-table root"))
|
||||||
|
end
|
||||||
|
view.moduleCache[moduleName] = { source = source, value = value }
|
||||||
|
return value
|
||||||
|
end
|
||||||
|
|
||||||
|
function DatasetViews:_data(view)
|
||||||
|
if view.data then return view.data end
|
||||||
|
local data = {}
|
||||||
|
setmetatable(data, {
|
||||||
|
__index = function(target, root)
|
||||||
|
local value = self:_module(view, root)
|
||||||
|
if value ~= nil then rawset(target, root, value) end
|
||||||
|
return value
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
DatasetHydration.apply(data, view.version, self.engineRequire)
|
||||||
|
if view.invalid then error(view.invalid.detail, 0) end
|
||||||
|
view.data = data
|
||||||
|
return data
|
||||||
|
end
|
||||||
|
|
||||||
|
local function registryBase(data, target)
|
||||||
|
return function() return resolvePath(data, target) end
|
||||||
|
end
|
||||||
|
|
||||||
|
function DatasetViews:_registries(view)
|
||||||
|
if view.registries then return view.registries end
|
||||||
|
local data = self:_data(view)
|
||||||
|
local registries = {}
|
||||||
|
for name, catalogSpec in pairs(Schemas.REGISTRIES) do
|
||||||
|
local spec = Schemas.shapeFor(name, catalogSpec, view.generation)
|
||||||
|
local registry = Registry.new(name, spec)
|
||||||
|
local target = Schemas.targetFor(name, catalogSpec, view.generation)
|
||||||
|
if target then registry.base = registryBase(data, target) end
|
||||||
|
registries[name] = registry
|
||||||
|
end
|
||||||
|
Builtins.install(registries, data, view.generation, self.engineRequire)
|
||||||
|
for _, registry in pairs(registries) do registry:freeze() end
|
||||||
|
view.registries = registries
|
||||||
|
return registries
|
||||||
|
end
|
||||||
|
|
||||||
|
function DatasetViews:_registry(view, name)
|
||||||
|
local service = self
|
||||||
|
local function registryForRead()
|
||||||
|
if not service:_ready(view) then return nil end
|
||||||
|
local ok, registries = pcall(service._registries, service, view)
|
||||||
|
if not ok then
|
||||||
|
if not view.invalid then service:_reject(view, nil, nil, registries) end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
if view.invalid then return nil end
|
||||||
|
return registries[name]
|
||||||
|
end
|
||||||
|
local function validate(registry, id, value)
|
||||||
|
if value == nil then return true end
|
||||||
|
local ok, detail = Schemas.check(registry.spec, registry.name, id,
|
||||||
|
value, "override")
|
||||||
|
if ok then return true end
|
||||||
|
local target = registry.spec.target
|
||||||
|
or Schemas.targetFor(registry.name, registry.spec, view.generation)
|
||||||
|
local root = target and target:match("^[^%.]+")
|
||||||
|
local moduleName = root and view.modules[root]
|
||||||
|
if root == "gen2HeldItems" then moduleName = "items" end
|
||||||
|
local cached = moduleName and view.moduleCache[moduleName]
|
||||||
|
service:_reject(view, moduleName, cached and cached.source,
|
||||||
|
"invalid " .. registry.name .. " record: " .. detail)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
local function rawAt(id)
|
||||||
|
local registry = registryForRead()
|
||||||
|
local value = registry and registry:get(id)
|
||||||
|
if value == nil or not validate(registry, id, value)
|
||||||
|
or not isDataOnly(value) then return nil end
|
||||||
|
return value
|
||||||
|
end
|
||||||
|
local function valueAt(id)
|
||||||
|
local value = rawAt(id)
|
||||||
|
if value == nil then return nil end
|
||||||
|
return (copyData(value))
|
||||||
|
end
|
||||||
|
return {
|
||||||
|
get = function(_, id)
|
||||||
|
if type(id) ~= "string" or id == "" then return nil end
|
||||||
|
return valueAt(id)
|
||||||
|
end,
|
||||||
|
has = function(_, id)
|
||||||
|
if type(id) ~= "string" or id == "" then return false end
|
||||||
|
return rawAt(id) ~= nil
|
||||||
|
end,
|
||||||
|
each = function()
|
||||||
|
local ids = {}
|
||||||
|
local registry = registryForRead()
|
||||||
|
if registry then
|
||||||
|
for id, value in registry:each() do
|
||||||
|
if not validate(registry, id, value) then
|
||||||
|
ids = {}
|
||||||
|
break
|
||||||
|
end
|
||||||
|
if type(id) == "string" and isDataOnly(value) then
|
||||||
|
ids[#ids + 1] = id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.sort(ids)
|
||||||
|
local index = 0
|
||||||
|
return function()
|
||||||
|
index = index + 1
|
||||||
|
local id = ids[index]
|
||||||
|
if id == nil then return nil end
|
||||||
|
return id, valueAt(id)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
function DatasetViews:_assets(view)
|
||||||
|
local service = self
|
||||||
|
local assets = {}
|
||||||
|
function assets:path(path)
|
||||||
|
if not service:_ready(view) then return nil end
|
||||||
|
return view.prefix .. assetRelative(path)
|
||||||
|
end
|
||||||
|
function assets:info(path)
|
||||||
|
local full = self:path(path)
|
||||||
|
if not full then return nil end
|
||||||
|
local info = service.fs.getInfo and service.fs.getInfo(full, "file")
|
||||||
|
if not info or (info.type and info.type ~= "file") then return nil end
|
||||||
|
local out = { type = "file" }
|
||||||
|
if info.size ~= nil then out.size = info.size end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
return assets
|
||||||
|
end
|
||||||
|
|
||||||
|
function DatasetViews:open(version)
|
||||||
|
if type(version) ~= "string" or not GameVersion.VERSIONS[version] then
|
||||||
|
return nil, "unknown_version"
|
||||||
|
end
|
||||||
|
local inspected, reason = CacheContract.inspect(version, self.fs, {
|
||||||
|
allowSource = true, semantic = true,
|
||||||
|
})
|
||||||
|
if not inspected then
|
||||||
|
self.datasets[version] = nil
|
||||||
|
return nil, reason
|
||||||
|
end
|
||||||
|
local plan, invalid = self:_preflight(version, inspected)
|
||||||
|
if not plan then
|
||||||
|
Logger.warn("dataset %s cache rejected: %s", version, invalid)
|
||||||
|
return nil, "invalid_cache"
|
||||||
|
end
|
||||||
|
|
||||||
|
local internal = self.datasets[version]
|
||||||
|
if internal and internal.invalid then
|
||||||
|
local bad = internal.invalid
|
||||||
|
local path = bad.module and plan.paths[bad.module]
|
||||||
|
local source = path and self.fs.read(path)
|
||||||
|
if source == bad.source then return nil, "invalid_cache" end
|
||||||
|
internal = nil
|
||||||
|
self.datasets[version] = nil
|
||||||
|
end
|
||||||
|
local changed = not internal or internal.prefix ~= inspected.prefix
|
||||||
|
or internal.plan.key ~= plan.key
|
||||||
|
if changed then
|
||||||
|
if internal then internal.unavailable = true end
|
||||||
|
internal = {
|
||||||
|
version = version,
|
||||||
|
generation = GameVersion.generation(version),
|
||||||
|
prefix = inspected.prefix,
|
||||||
|
plan = plan,
|
||||||
|
modules = GameVersion.generation(version) == 2 and GEN2_ROOTS or GEN1_ROOTS,
|
||||||
|
moduleCache = {},
|
||||||
|
}
|
||||||
|
self.datasets[version] = internal
|
||||||
|
else
|
||||||
|
internal.plan = plan
|
||||||
|
end
|
||||||
|
|
||||||
|
local view = { version = version, generation = internal.generation, content = {} }
|
||||||
|
for name in pairs(Schemas.REGISTRIES) do
|
||||||
|
view.content[name] = self:_registry(internal, name)
|
||||||
|
end
|
||||||
|
for alias, canonical in pairs(Schemas.ALIASES) do
|
||||||
|
view.content[alias] = view.content[canonical]
|
||||||
|
end
|
||||||
|
view.assets = self:_assets(internal)
|
||||||
|
return view
|
||||||
|
end
|
||||||
|
|
||||||
|
return DatasetViews
|
||||||
+23
-2
@@ -147,12 +147,19 @@ local SUPPORTED_REQUIRES = {
|
|||||||
local ENGINE_PREFIX = (debug.getinfo(1, "S").source or "")
|
local ENGINE_PREFIX = (debug.getinfo(1, "S").source or "")
|
||||||
:gsub("^@", ""):gsub("mods[/\\]Loader%.lua$", "")
|
:gsub("^@", ""):gsub("mods[/\\]Loader%.lua$", "")
|
||||||
|
|
||||||
|
local ENGINE_CHUNKS = {
|
||||||
|
["main.lua"] = true,
|
||||||
|
["conf.lua"] = true,
|
||||||
|
}
|
||||||
|
|
||||||
local function callerIsMod(level)
|
local function callerIsMod(level)
|
||||||
if ENGINE_PREFIX == "" then return false end
|
if ENGINE_PREFIX == "" then return false end
|
||||||
local info = debug.getinfo(level, "S")
|
local info = debug.getinfo(level, "S")
|
||||||
local source = info and info.source
|
local source = info and info.source
|
||||||
if not source or source:sub(1, 1) ~= "@" then return false end
|
if not source or source:sub(1, 1) ~= "@" then return false end
|
||||||
return source:sub(2, 1 + #ENGINE_PREFIX) ~= ENGINE_PREFIX
|
local path = source:sub(2)
|
||||||
|
if ENGINE_CHUNKS[path] then return false end
|
||||||
|
return path:sub(1, #ENGINE_PREFIX) ~= ENGINE_PREFIX
|
||||||
end
|
end
|
||||||
|
|
||||||
local function scanRequire(name)
|
local function scanRequire(name)
|
||||||
@@ -207,6 +214,11 @@ local function engineRequire(name)
|
|||||||
return module
|
return module
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function Loader.endSession()
|
||||||
|
devShim.generation = nil
|
||||||
|
devShim.errors = nil
|
||||||
|
end
|
||||||
|
|
||||||
function Loader:_installDevShim()
|
function Loader:_installDevShim()
|
||||||
for id, mod in pairs(self.mods) do
|
for id, mod in pairs(self.mods) do
|
||||||
devShim.permissions[id] = mod.manifest.permissionSet
|
devShim.permissions[id] = mod.manifest.permissionSet
|
||||||
@@ -234,7 +246,7 @@ function Loader:_installDevShim()
|
|||||||
-- The Gen 1 name a mod asked for, answered by the Gen 2 arm behind it.
|
-- The Gen 1 name a mod asked for, answered by the Gen 2 arm behind it.
|
||||||
-- Engine code keeps the real module: src/render/PaletteFX.lua:776
|
-- Engine code keeps the real module: src/render/PaletteFX.lua:776
|
||||||
-- requires src.core.Game on both generations and means it.
|
-- requires src.core.Game on both generations and means it.
|
||||||
if devShim.generation ~= 1 and Gen2Compat.serves(name)
|
if devShim.generation == 2 and Gen2Compat.serves(name)
|
||||||
and (owner or callerIsMod(3)) then
|
and (owner or callerIsMod(3)) then
|
||||||
local adapter = Gen2Compat.resolve(name, Runtime.currentMod)
|
local adapter = Gen2Compat.resolve(name, Runtime.currentMod)
|
||||||
if adapter then
|
if adapter then
|
||||||
@@ -1154,6 +1166,15 @@ function Loader:_api(mod)
|
|||||||
developer = loader.dev == true,
|
developer = loader.dev == true,
|
||||||
-- a deep copy: what a mod does to its own view never reaches the loader
|
-- a deep copy: what a mod does to its own view never reaches the loader
|
||||||
manifest = Merge.deepCopy(mod.manifest),
|
manifest = Merge.deepCopy(mod.manifest),
|
||||||
|
datasets = {
|
||||||
|
open = function(_, version)
|
||||||
|
if not loader.datasetViews then
|
||||||
|
local DatasetViews = engineRequire("src.mods.DatasetViews")
|
||||||
|
loader.datasetViews = DatasetViews.new(loader.fs, engineRequire)
|
||||||
|
end
|
||||||
|
return loader.datasetViews:open(version)
|
||||||
|
end,
|
||||||
|
},
|
||||||
content = {},
|
content = {},
|
||||||
exports = {},
|
exports = {},
|
||||||
DELETE = Registry.DELETE,
|
DELETE = Registry.DELETE,
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ Registry.__index = Registry
|
|||||||
-- exposed to mods as mod.DELETE: a patch value that unsets a field
|
-- exposed to mods as mod.DELETE: a patch value that unsets a field
|
||||||
Registry.DELETE = Merge.DELETE
|
Registry.DELETE = Merge.DELETE
|
||||||
|
|
||||||
|
local function assertWritable(self, id)
|
||||||
|
if self.spec.reservedIds and self.spec.reservedIds[id] then
|
||||||
|
error(("%s id is reserved engine metadata: %s"):format(self.name, id))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- spec comes from Schemas.REGISTRIES[name]; bare Registry.new(name) keeps
|
-- spec comes from Schemas.REGISTRIES[name]; bare Registry.new(name) keeps
|
||||||
-- the v1 record behavior for standalone use in tests and tools
|
-- the v1 record behavior for standalone use in tests and tools
|
||||||
function Registry.new(name, spec)
|
function Registry.new(name, spec)
|
||||||
@@ -33,6 +39,7 @@ local function append(self, id, op, value, owner)
|
|||||||
error(self.name .. ": content is frozen after load")
|
error(self.name .. ": content is frozen after load")
|
||||||
end
|
end
|
||||||
assert(type(id) == "string" and id ~= "", self.name .. " id is required")
|
assert(type(id) == "string" and id ~= "", self.name .. " id is required")
|
||||||
|
assertWritable(self, id)
|
||||||
local list = self.ops[id]
|
local list = self.ops[id]
|
||||||
if not list then
|
if not list then
|
||||||
list = {}
|
list = {}
|
||||||
|
|||||||
+37
-3
@@ -676,9 +676,10 @@ end
|
|||||||
--
|
--
|
||||||
-- So beside `value` / `fields` / `keys` / `keyValue` a spec may carry
|
-- So beside `value` / `fields` / `keys` / `keyValue` a spec may carry
|
||||||
-- `gen2Value` / `gen2Fields` / `gen2Keys` / `gen2KeyValue`, and beside
|
-- `gen2Value` / `gen2Fields` / `gen2Keys` / `gen2KeyValue`, and beside
|
||||||
-- `semantics` / `extra` / `write` / `baseAt` / `baseIds` / `example` /
|
-- `semantics` / `extra` / `write` / `baseAt` / `baseIds` / `reservedIds` /
|
||||||
-- `notes` the matching `gen2*`. Absent means "the Gen 1 shape is right here
|
-- `example` / `notes` the matching `gen2*`. Absent means "the Gen 1 shape is
|
||||||
-- too", which is the common case and why most registries carry none of this.
|
-- right here too", which is the common case and why most registries carry
|
||||||
|
-- none of this.
|
||||||
-- The registry NAME, the verbs and (wherever the id space allows it) the ids
|
-- The registry NAME, the verbs and (wherever the id space allows it) the ids
|
||||||
-- stay shared, exactly as the routing table keeps them shared.
|
-- stay shared, exactly as the routing table keeps them shared.
|
||||||
--
|
--
|
||||||
@@ -700,6 +701,7 @@ local GEN2_SHAPE = {
|
|||||||
gen2KeyValue = "keyValue", gen2Extra = "extra",
|
gen2KeyValue = "keyValue", gen2Extra = "extra",
|
||||||
gen2Semantics = "semantics", gen2Write = "write",
|
gen2Semantics = "semantics", gen2Write = "write",
|
||||||
gen2BaseAt = "baseAt", gen2BaseIds = "baseIds",
|
gen2BaseAt = "baseAt", gen2BaseIds = "baseIds",
|
||||||
|
gen2ReservedIds = "reservedIds",
|
||||||
gen2Example = "example", gen2Notes = "notes",
|
gen2Example = "example", gen2Notes = "notes",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -757,6 +759,32 @@ end
|
|||||||
local R = {}
|
local R = {}
|
||||||
Schemas.REGISTRIES = R
|
Schemas.REGISTRIES = R
|
||||||
|
|
||||||
|
-- Some generated Gen 2 modules keep extractor metadata beside their public
|
||||||
|
-- record maps. These callbacks are the registry normalization boundary: the
|
||||||
|
-- metadata remains available to engine consumers through Data, but is not an
|
||||||
|
-- id a mod can read or overwrite through the record registry.
|
||||||
|
local function recordMapExcept(...)
|
||||||
|
local excluded = {}
|
||||||
|
for index = 1, select("#", ...) do excluded[select(index, ...)] = true end
|
||||||
|
return function(base, id)
|
||||||
|
if excluded[id] then return nil end
|
||||||
|
return base[id]
|
||||||
|
end, function(base)
|
||||||
|
local ids = {}
|
||||||
|
for id in pairs(base) do
|
||||||
|
if not excluded[id] then ids[#ids + 1] = id end
|
||||||
|
end
|
||||||
|
return ids
|
||||||
|
end, excluded
|
||||||
|
end
|
||||||
|
|
||||||
|
local pokemonGen2BaseAt, pokemonGen2BaseIds, pokemonGen2ReservedIds =
|
||||||
|
recordMapExcept("growthRates", "tmhmMoves")
|
||||||
|
local movesGen2BaseAt, movesGen2BaseIds, movesGen2ReservedIds =
|
||||||
|
recordMapExcept("generation", "source")
|
||||||
|
local itemsGen2BaseAt, itemsGen2BaseIds, itemsGen2ReservedIds =
|
||||||
|
recordMapExcept("generation", "source", "pockets")
|
||||||
|
|
||||||
-- ------- shared Gen 2 leaves
|
-- ------- shared Gen 2 leaves
|
||||||
--
|
--
|
||||||
-- The ROM name spaces Gold's tables key by. They are enums rather than
|
-- The ROM name spaces Gold's tables key by. They are enums rather than
|
||||||
@@ -779,6 +807,8 @@ local gen2PaletteRow = f.list(gen2Color)
|
|||||||
|
|
||||||
R.pokemon = {
|
R.pokemon = {
|
||||||
semantics = "record", target = "pokemon",
|
semantics = "record", target = "pokemon",
|
||||||
|
gen2BaseAt = pokemonGen2BaseAt, gen2BaseIds = pokemonGen2BaseIds,
|
||||||
|
gen2ReservedIds = pokemonGen2ReservedIds,
|
||||||
fields = {
|
fields = {
|
||||||
id = f.str, name = f.str, dex = f.int(1),
|
id = f.str, name = f.str, dex = f.int(1),
|
||||||
index = f.opt(f.int(0, 255)),
|
index = f.opt(f.int(0, 255)),
|
||||||
@@ -870,6 +900,8 @@ R.pokemon = {
|
|||||||
|
|
||||||
R.moves = {
|
R.moves = {
|
||||||
semantics = "record", target = "moves",
|
semantics = "record", target = "moves",
|
||||||
|
gen2BaseAt = movesGen2BaseAt, gen2BaseIds = movesGen2BaseIds,
|
||||||
|
gen2ReservedIds = movesGen2ReservedIds,
|
||||||
fields = {
|
fields = {
|
||||||
id = f.str, name = f.str,
|
id = f.str, name = f.str,
|
||||||
index = f.opt(f.int(0, 255)),
|
index = f.opt(f.int(0, 255)),
|
||||||
@@ -894,6 +926,8 @@ R.moves = {
|
|||||||
|
|
||||||
R.items = {
|
R.items = {
|
||||||
semantics = "record", target = "items",
|
semantics = "record", target = "items",
|
||||||
|
gen2BaseAt = itemsGen2BaseAt, gen2BaseIds = itemsGen2BaseIds,
|
||||||
|
gen2ReservedIds = itemsGen2ReservedIds,
|
||||||
fields = {
|
fields = {
|
||||||
id = f.str, name = f.str,
|
id = f.str, name = f.str,
|
||||||
index = f.opt(f.int(0, 255)),
|
index = f.opt(f.int(0, 255)),
|
||||||
|
|||||||
@@ -18,8 +18,6 @@
|
|||||||
-- colour IS its palettes -- so one substitution at this seam recolours the
|
-- colour IS its palettes -- so one substitution at this seam recolours the
|
||||||
-- whole game without a single screen knowing about it:
|
-- whole game without a single screen knowing about it:
|
||||||
--
|
--
|
||||||
-- GBC the cart's own palettes. The default: this is a Game Boy
|
|
||||||
-- Color game and its colour is the point.
|
|
||||||
-- DMG the original grey Game Boy. Every palette collapses to the
|
-- DMG the original grey Game Boy. Every palette collapses to the
|
||||||
-- four hardware shades, and the sheets that are drawn straight
|
-- four hardware shades, and the sheets that are drawn straight
|
||||||
-- (chrome, text) are already those shades, so the screen is
|
-- (chrome, text) are already those shades, so the screen is
|
||||||
@@ -32,9 +30,12 @@
|
|||||||
local GbcPalette = {}
|
local GbcPalette = {}
|
||||||
|
|
||||||
GbcPalette.MODES = { "gbc", "dmg", "classic" }
|
GbcPalette.MODES = { "gbc", "dmg", "classic" }
|
||||||
GbcPalette.MODE_LABELS = { gbc = "GBC", dmg = "DMG", classic = "CLASSIC" }
|
GbcPalette.MODE_LABELS = { gbc = "GEN 2", dmg = "DMG", classic = "CLASSIC",
|
||||||
|
custom = "GBC" }
|
||||||
GbcPalette.mode = "gbc"
|
GbcPalette.mode = "gbc"
|
||||||
|
|
||||||
|
GbcPalette.CUSTOM_MODE = "custom"
|
||||||
|
|
||||||
-- rBGP's four shades as the hardware shows them.
|
-- rBGP's four shades as the hardware shows them.
|
||||||
local DMG_SHADES = {
|
local DMG_SHADES = {
|
||||||
{ 255, 255, 255 }, { 170, 170, 170 }, { 85, 85, 85 }, { 0, 0, 0 },
|
{ 255, 255, 255 }, { 170, 170, 170 }, { 85, 85, 85 }, { 0, 0, 0 },
|
||||||
@@ -162,11 +163,22 @@ local function channel(colors, index)
|
|||||||
return (c[1] or 0) / 255, (c[2] or 0) / 255, (c[3] or 0) / 255
|
return (c[1] or 0) / 255, (c[2] or 0) / 255, (c[3] or 0) / 255
|
||||||
end
|
end
|
||||||
|
|
||||||
|
GbcPalette.customRamp = nil
|
||||||
|
|
||||||
|
function GbcPalette.setCustomRamp(ramp)
|
||||||
|
local prev = GbcPalette.customRamp
|
||||||
|
GbcPalette.customRamp = ramp
|
||||||
|
if (prev ~= nil) ~= (ramp ~= nil) or prev ~= ramp then
|
||||||
|
pcall(function() require("src.render.SpriteRenderer").invalidate() end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- What a palette actually draws as under the current COLOR mode. Anything
|
-- What a palette actually draws as under the current COLOR mode. Anything
|
||||||
-- that reads a colour out of a palette directly -- a canvas cleared to BG
|
-- that reads a colour out of a palette directly -- a canvas cleared to BG
|
||||||
-- colour 0, say -- has to go through this too, or the backdrop would keep its
|
-- colour 0, say -- has to go through this too, or the backdrop would keep its
|
||||||
-- cart colour while everything on top of it went grey.
|
-- cart colour while everything on top of it went grey.
|
||||||
function GbcPalette.resolve(colors)
|
function GbcPalette.resolve(colors)
|
||||||
|
if GbcPalette.customRamp then return GbcPalette.customRamp end
|
||||||
if GbcPalette.mode == "gbc" then return colors end
|
if GbcPalette.mode == "gbc" then return colors end
|
||||||
return DMG_SHADES
|
return DMG_SHADES
|
||||||
end
|
end
|
||||||
@@ -249,6 +261,10 @@ function GbcPalette.presentColors()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function GbcPalette.setMode(mode)
|
function GbcPalette.setMode(mode)
|
||||||
|
if mode == GbcPalette.CUSTOM_MODE then
|
||||||
|
GbcPalette.mode = mode
|
||||||
|
return mode
|
||||||
|
end
|
||||||
for _, name in ipairs(GbcPalette.MODES) do
|
for _, name in ipairs(GbcPalette.MODES) do
|
||||||
if name == mode then
|
if name == mode then
|
||||||
GbcPalette.mode = mode
|
GbcPalette.mode = mode
|
||||||
@@ -260,7 +276,7 @@ function GbcPalette.setMode(mode)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function GbcPalette.modeLabel(mode)
|
function GbcPalette.modeLabel(mode)
|
||||||
return GbcPalette.MODE_LABELS[mode or GbcPalette.mode] or "GBC"
|
return GbcPalette.MODE_LABELS[mode or GbcPalette.mode] or "GEN 2"
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Advance GBC -> DMG -> CLASSIC -> GBC. `delta` may be -1 to step back, so
|
-- Advance GBC -> DMG -> CLASSIC -> GBC. `delta` may be -1 to step back, so
|
||||||
@@ -273,11 +289,20 @@ function GbcPalette.cycle(delta)
|
|||||||
local count = #GbcPalette.MODES
|
local count = #GbcPalette.MODES
|
||||||
at = (at - 1 + (delta or 1)) % count + 1
|
at = (at - 1 + (delta or 1)) % count + 1
|
||||||
GbcPalette.mode = GbcPalette.MODES[at]
|
GbcPalette.mode = GbcPalette.MODES[at]
|
||||||
|
GbcPalette.setCustomRamp(nil)
|
||||||
return GbcPalette.mode
|
return GbcPalette.mode
|
||||||
end
|
end
|
||||||
|
|
||||||
function GbcPalette.applyOptions(opts)
|
function GbcPalette.applyOptions(opts)
|
||||||
return GbcPalette.setMode(opts and opts.color or "gbc")
|
local mode = GbcPalette.setMode(opts and opts.color or "gbc")
|
||||||
|
local id = opts and opts.palette
|
||||||
|
if id and id ~= "" then
|
||||||
|
local ok, Palette = pcall(require, "src.render.Palette")
|
||||||
|
GbcPalette.setCustomRamp(ok and Palette.ramp(id) or nil)
|
||||||
|
else
|
||||||
|
GbcPalette.setCustomRamp(nil)
|
||||||
|
end
|
||||||
|
return mode
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Point the shader at one 4-color palette. Colors are 0-255 triples, matching
|
-- Point the shader at one 4-color palette. Colors are 0-255 triples, matching
|
||||||
@@ -431,4 +456,14 @@ function GbcPalette.with(colors, body)
|
|||||||
return applied
|
return applied
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function GbcPalette.withRaw(colors, body)
|
||||||
|
local previous = love and love.graphics and love.graphics.getShader
|
||||||
|
and love.graphics.getShader() or nil
|
||||||
|
local applied = GbcPalette.useRaw(colors)
|
||||||
|
local ok, err = pcall(body)
|
||||||
|
if love and love.graphics then love.graphics.setShader(previous) end
|
||||||
|
if not ok then error(err, 0) end
|
||||||
|
return applied
|
||||||
|
end
|
||||||
|
|
||||||
return GbcPalette
|
return GbcPalette
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
-- UI LETTERBOX: what fills the window around a 160x144 screen.
|
||||||
|
--
|
||||||
|
-- AUTO keeps what each screen was authored with (Gen 1 black, Gen 2 mostly
|
||||||
|
-- white, the Pokedex and credits black); the other three override every
|
||||||
|
-- screen uniformly. PALETTE takes the active COLORS ramp's paper shade, so
|
||||||
|
-- the bars match the panel they frame instead of a fixed grey.
|
||||||
|
|
||||||
|
local Letterbox = {}
|
||||||
|
|
||||||
|
-- Set from applyOptions on both generations, the same shape GbcPalette.setMode
|
||||||
|
-- and Tilt.setLevel use, so a draw call needs no options table in hand.
|
||||||
|
Letterbox.mode = "auto"
|
||||||
|
|
||||||
|
Letterbox.MODES = { "auto", "black", "white", "palette" }
|
||||||
|
|
||||||
|
local LABELS = {
|
||||||
|
auto = "AUTO", black = "BLACK", white = "WHITE", palette = "PALETTE",
|
||||||
|
}
|
||||||
|
|
||||||
|
function Letterbox.normalize(mode)
|
||||||
|
for _, id in ipairs(Letterbox.MODES) do
|
||||||
|
if mode == id then return id end
|
||||||
|
end
|
||||||
|
return "auto"
|
||||||
|
end
|
||||||
|
|
||||||
|
function Letterbox.label(mode)
|
||||||
|
return LABELS[Letterbox.normalize(mode)]
|
||||||
|
end
|
||||||
|
|
||||||
|
function Letterbox.cycle(mode, dir)
|
||||||
|
local at = 1
|
||||||
|
for i, id in ipairs(Letterbox.MODES) do
|
||||||
|
if id == Letterbox.normalize(mode) then at = i break end
|
||||||
|
end
|
||||||
|
local n = #Letterbox.MODES
|
||||||
|
return Letterbox.MODES[(at - 1 + (dir or 1)) % n + 1]
|
||||||
|
end
|
||||||
|
|
||||||
|
-- r, g, b for `mode`, falling back to the screen's authored colour on AUTO.
|
||||||
|
--
|
||||||
|
-- `paper` is supplied by the caller, not looked up here: the two generations
|
||||||
|
-- keep their picked palette in different modules (Gen 1 in PaletteFX, Gen 2 in
|
||||||
|
-- GbcPalette) and GbcPalette.available() is true on both, so anything that
|
||||||
|
-- sniffed for it answered with Gen 2's white default on a Gen 1 game.
|
||||||
|
function Letterbox.color(mode, r, g, b, paper)
|
||||||
|
mode = Letterbox.normalize(mode)
|
||||||
|
if mode == "black" then return 0, 0, 0 end
|
||||||
|
if mode == "white" then return 1, 1, 1 end
|
||||||
|
if mode == "palette" and paper then
|
||||||
|
local pr, pg, pb = paper()
|
||||||
|
if pr then return pr, pg, pb end
|
||||||
|
end
|
||||||
|
return r or 0, g or 0, b or 0
|
||||||
|
end
|
||||||
|
|
||||||
|
function Letterbox.setMode(mode)
|
||||||
|
Letterbox.mode = Letterbox.normalize(mode)
|
||||||
|
return Letterbox.mode
|
||||||
|
end
|
||||||
|
|
||||||
|
function Letterbox.applyOptions(options)
|
||||||
|
return Letterbox.setMode(options and options.uiLetterbox)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- What a drawWidescreen should paint the window with, given the colour that
|
||||||
|
-- screen was authored to use and its generation's paper reader.
|
||||||
|
function Letterbox.fill(r, g, b, paper)
|
||||||
|
return Letterbox.color(Letterbox.mode, r, g, b, paper)
|
||||||
|
end
|
||||||
|
|
||||||
|
return Letterbox
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
|
||||||
|
local Palette = {}
|
||||||
|
|
||||||
|
local pack = nil
|
||||||
|
|
||||||
|
function Palette.packs()
|
||||||
|
if pack == nil then
|
||||||
|
local ok, data = pcall(require, "data.gb_palettes")
|
||||||
|
pack = ok and data or false
|
||||||
|
end
|
||||||
|
return pack or {}
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local MONO_STEPS = { 1.00, 0.72, 0.38, 0.12 }
|
||||||
|
|
||||||
|
Palette.MONO_COLOURS = {
|
||||||
|
{ "GREEN", 0x9b, 0xbc, 0x0f }, -- DMG
|
||||||
|
{ "LIME", 0xa8, 0xd8, 0x38 },
|
||||||
|
{ "OLIVE", 0xa6, 0xac, 0x84 }, -- Pocket
|
||||||
|
{ "AMBER", 0xff, 0xb0, 0x00 }, -- plasma terminal
|
||||||
|
{ "ORANGE", 0xff, 0x80, 0x30 },
|
||||||
|
{ "RED", 0xff, 0x50, 0x50 },
|
||||||
|
{ "ROSE", 0xff, 0x90, 0xb0 },
|
||||||
|
{ "MAGENTA", 0xf0, 0x60, 0xd0 },
|
||||||
|
{ "PURPLE", 0xb0, 0x70, 0xf0 },
|
||||||
|
{ "INDIGO", 0x80, 0x80, 0xf0 },
|
||||||
|
{ "BLUE", 0x50, 0xa0, 0xff },
|
||||||
|
{ "CYAN", 0x40, 0xe0, 0xe0 }, -- VFD
|
||||||
|
{ "TEAL", 0x40, 0xc0, 0xa0 },
|
||||||
|
{ "MINT", 0x90, 0xe0, 0xb0 },
|
||||||
|
{ "SAND", 0xe0, 0xc8, 0x90 },
|
||||||
|
{ "BROWN", 0xc0, 0x90, 0x50 },
|
||||||
|
{ "WHITE", 0xff, 0xff, 0xff }, -- plain DMG greys
|
||||||
|
{ "SILVER", 0xd0, 0xd8, 0xe0 },
|
||||||
|
}
|
||||||
|
|
||||||
|
local function monoRamp(r, g, b)
|
||||||
|
local out = {}
|
||||||
|
for i = 1, 4 do
|
||||||
|
local f = MONO_STEPS[i]
|
||||||
|
out[i] = { math.floor(r * f + 0.5),
|
||||||
|
math.floor(g * f + 0.5),
|
||||||
|
math.floor(b * f + 0.5) }
|
||||||
|
end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
|
Palette.monoRamp = monoRamp -- named for the suite
|
||||||
|
|
||||||
|
|
||||||
|
function Palette.packId(group, name)
|
||||||
|
return "p:" .. group .. "/" .. name
|
||||||
|
end
|
||||||
|
|
||||||
|
function Palette.monoId(r, g, b)
|
||||||
|
return ("m:%02x%02x%02x"):format(r, g, b)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function unhex(text)
|
||||||
|
if type(text) ~= "string" or #text ~= 24 then return nil end
|
||||||
|
local out = {}
|
||||||
|
for i = 1, 4 do
|
||||||
|
local at = (i - 1) * 6
|
||||||
|
local r = tonumber(text:sub(at + 1, at + 2), 16)
|
||||||
|
local g = tonumber(text:sub(at + 3, at + 4), 16)
|
||||||
|
local b = tonumber(text:sub(at + 5, at + 6), 16)
|
||||||
|
if not (r and g and b) then return nil end
|
||||||
|
out[i] = { r, g, b }
|
||||||
|
end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
|
Palette.unhex = unhex -- named for the suite
|
||||||
|
|
||||||
|
local RAMP_INDEX = 2
|
||||||
|
|
||||||
|
local GREY_CHROMA = 12
|
||||||
|
local HUE_ARC = 50
|
||||||
|
|
||||||
|
Palette.GREY_CHROMA = GREY_CHROMA -- named for the suite
|
||||||
|
Palette.HUE_ARC = HUE_ARC
|
||||||
|
|
||||||
|
local function hueChroma(c)
|
||||||
|
local r, g, b = c[1] or 0, c[2] or 0, c[3] or 0
|
||||||
|
local mx = math.max(r, g, b)
|
||||||
|
local mn = math.min(r, g, b)
|
||||||
|
local chroma = mx - mn
|
||||||
|
if chroma <= 0 then return 0, 0 end
|
||||||
|
local h
|
||||||
|
if mx == r then h = ((g - b) / chroma) % 6
|
||||||
|
elseif mx == g then h = (b - r) / chroma + 2
|
||||||
|
else h = (r - g) / chroma + 4 end
|
||||||
|
return h * 60, chroma
|
||||||
|
end
|
||||||
|
|
||||||
|
Palette.hueChroma = hueChroma -- named for the suite
|
||||||
|
|
||||||
|
local function hueArc(hues)
|
||||||
|
local best = 360
|
||||||
|
for i = 1, #hues do
|
||||||
|
local widest = 0
|
||||||
|
for j = 1, #hues do
|
||||||
|
local d = (hues[j] - hues[i]) % 360
|
||||||
|
if d > widest then widest = d end
|
||||||
|
end
|
||||||
|
if widest < best then best = widest end
|
||||||
|
end
|
||||||
|
return best
|
||||||
|
end
|
||||||
|
|
||||||
|
Palette.hueArc = hueArc -- named for the suite
|
||||||
|
|
||||||
|
function Palette.categoryOf(ramp)
|
||||||
|
if type(ramp) ~= "table" or not ramp[4] then return nil end
|
||||||
|
local hues = {}
|
||||||
|
for i = 1, 4 do
|
||||||
|
local h, chroma = hueChroma(ramp[i])
|
||||||
|
if chroma > GREY_CHROMA then hues[#hues + 1] = h end
|
||||||
|
end
|
||||||
|
if #hues == 0 then return "grey" end
|
||||||
|
return hueArc(hues) <= HUE_ARC and "single" or "full"
|
||||||
|
end
|
||||||
|
|
||||||
|
Palette.CATEGORIES = {
|
||||||
|
{ key = "full", label = "FULL COLOUR" },
|
||||||
|
{ key = "single", label = "SINGLE COLOUR" },
|
||||||
|
{ key = "grey", label = "GREYSCALE" },
|
||||||
|
}
|
||||||
|
|
||||||
|
local byId, buckets = nil, nil
|
||||||
|
|
||||||
|
local function buildIndex()
|
||||||
|
byId, buckets = {}, {}
|
||||||
|
for _, category in ipairs(Palette.CATEGORIES) do buckets[category.key] = {} end
|
||||||
|
for _, p in ipairs(Palette.packs()) do
|
||||||
|
for _, entry in ipairs(p.palettes or {}) do
|
||||||
|
local name = entry[1]
|
||||||
|
if type(name) == "string" then
|
||||||
|
local id = Palette.packId(p.name, name)
|
||||||
|
byId[id] = entry
|
||||||
|
local raw = unhex(entry[RAMP_INDEX] or entry[2])
|
||||||
|
local bucket = raw and buckets[Palette.categoryOf(raw) or ""] or nil
|
||||||
|
if bucket then
|
||||||
|
bucket[#bucket + 1] = { name = name, pack = p.name, id = id }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function ensureIndex()
|
||||||
|
if not byId then buildIndex() end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Palette.categories()
|
||||||
|
ensureIndex()
|
||||||
|
local out = {}
|
||||||
|
for i, category in ipairs(Palette.CATEGORIES) do
|
||||||
|
out[i] = { key = category.key, name = category.label,
|
||||||
|
palettes = buckets[category.key] or {} }
|
||||||
|
end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
|
function Palette.category(key)
|
||||||
|
ensureIndex()
|
||||||
|
if not buckets[key] then return nil end
|
||||||
|
for _, category in ipairs(Palette.CATEGORIES) do
|
||||||
|
if category.key == key then
|
||||||
|
return { key = key, name = category.label, palettes = buckets[key] }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function Palette.locate(id)
|
||||||
|
if type(id) ~= "string" or id == "" then return nil end
|
||||||
|
ensureIndex()
|
||||||
|
for _, category in ipairs(Palette.CATEGORIES) do
|
||||||
|
for i, item in ipairs(buckets[category.key] or {}) do
|
||||||
|
if item.id == id then return category.key, i end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function findPack(group, name)
|
||||||
|
ensureIndex()
|
||||||
|
return byId[Palette.packId(group, name)]
|
||||||
|
end
|
||||||
|
|
||||||
|
Palette.findPack = findPack -- named for the suite
|
||||||
|
|
||||||
|
function Palette.label(id)
|
||||||
|
if type(id) ~= "string" or id == "" then return nil end
|
||||||
|
local hex = id:match("^m:(%x+)$")
|
||||||
|
if hex then
|
||||||
|
local r = tonumber(hex:sub(1, 2), 16)
|
||||||
|
local g = tonumber(hex:sub(3, 4), 16)
|
||||||
|
local b = tonumber(hex:sub(5, 6), 16)
|
||||||
|
for _, c in ipairs(Palette.MONO_COLOURS) do
|
||||||
|
if c[2] == r and c[3] == g and c[4] == b then return c[1] end
|
||||||
|
end
|
||||||
|
return "MONO"
|
||||||
|
end
|
||||||
|
local name = id:match("^p:.*/([^/]+)$")
|
||||||
|
return name or id
|
||||||
|
end
|
||||||
|
|
||||||
|
local cacheId, cacheRamp = nil, nil
|
||||||
|
|
||||||
|
function Palette.ramp(id)
|
||||||
|
if type(id) ~= "string" or id == "" then return nil end
|
||||||
|
if id == cacheId then return cacheRamp end
|
||||||
|
|
||||||
|
local out = nil
|
||||||
|
local hex = id:match("^m:(%x+)$")
|
||||||
|
if hex and #hex == 6 then
|
||||||
|
out = monoRamp(tonumber(hex:sub(1, 2), 16) or 0,
|
||||||
|
tonumber(hex:sub(3, 4), 16) or 0,
|
||||||
|
tonumber(hex:sub(5, 6), 16) or 0)
|
||||||
|
else
|
||||||
|
local group, name = id:match("^p:(.*)/([^/]+)$")
|
||||||
|
if group then
|
||||||
|
local entry = findPack(group, name)
|
||||||
|
local ramp = entry and unhex(entry[RAMP_INDEX] or entry[2])
|
||||||
|
if ramp then
|
||||||
|
out = { ramp[4], ramp[3], ramp[2], ramp[1] }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
cacheId, cacheRamp = id, out
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
|
function Palette.invalidate()
|
||||||
|
cacheId, cacheRamp = nil, nil
|
||||||
|
byId, buckets = nil, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function Palette.swatch(id)
|
||||||
|
local ramp = Palette.ramp(id)
|
||||||
|
if not ramp then return nil end
|
||||||
|
return { ramp[4], ramp[3], ramp[2], ramp[1] }
|
||||||
|
end
|
||||||
|
|
||||||
|
return Palette
|
||||||
+98
-34
@@ -20,6 +20,8 @@ local PaletteFX = {}
|
|||||||
local shader -- false = unavailable (headless / no shader support)
|
local shader -- false = unavailable (headless / no shader support)
|
||||||
local gbcPack -- false = missing; nil = not loaded yet
|
local gbcPack -- false = missing; nil = not loaded yet
|
||||||
local yellowPack -- false = missing; nil = not loaded yet
|
local yellowPack -- false = missing; nil = not loaded yet
|
||||||
|
local gbcYellowPack -- Yellow Advanced deltas; false = missing; nil = not loaded yet
|
||||||
|
|
||||||
|
|
||||||
-- Cycle order matches OptionsMenu / hotkey 2. The three real colorizations
|
-- Cycle order matches OptionsMenu / hotkey 2. The three real colorizations
|
||||||
-- come first (OG RED/BLUE/YELLOW = GBC hardware, SGB = per-map Super Game Boy,
|
-- come first (OG RED/BLUE/YELLOW = GBC hardware, SGB = per-map Super Game Boy,
|
||||||
@@ -300,6 +302,38 @@ function PaletteFX.yellowPack()
|
|||||||
return yellowPack or nil
|
return yellowPack or nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Yellow-only Advanced deltas (BEACH_HOUSE, sprite remap, YELLOWMON). Never
|
||||||
|
-- consulted on Red/Blue (#1639).
|
||||||
|
function PaletteFX.gbcYellowPack()
|
||||||
|
if gbcYellowPack == nil then
|
||||||
|
local ok, pack = pcall(require, "data.palettes_gbc_yellow")
|
||||||
|
gbcYellowPack = ok and pack or false
|
||||||
|
end
|
||||||
|
return gbcYellowPack or nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Active world bake tables for Advanced. Yellow merges gbc_yellow deltas
|
||||||
|
-- via __index so shared Red tilesets stay identical byte-for-byte.
|
||||||
|
function PaletteFX.worldPack()
|
||||||
|
local pack = PaletteFX.gbcPack()
|
||||||
|
local w = pack and pack.world
|
||||||
|
if not w then return nil end
|
||||||
|
if not GameVersion.isYellow() then return w end
|
||||||
|
local y = PaletteFX.gbcYellowPack()
|
||||||
|
local yw = y and y.world
|
||||||
|
if not yw then return w end
|
||||||
|
return {
|
||||||
|
tileGroups = setmetatable(yw.tileGroups or {}, { __index = w.tileGroups }),
|
||||||
|
groupColors = setmetatable(yw.groupColors or {}, { __index = w.groupColors }),
|
||||||
|
roofGroup = w.roofGroup,
|
||||||
|
roofByMapIndex = w.roofByMapIndex,
|
||||||
|
spriteAssignment = yw.spriteAssignment or w.spriteAssignment,
|
||||||
|
spritePalettes = yw.spritePalettes
|
||||||
|
and setmetatable(yw.spritePalettes, { __index = w.spritePalettes })
|
||||||
|
or w.spritePalettes,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
function PaletteFX.usesGbcPack(mode)
|
function PaletteFX.usesGbcPack(mode)
|
||||||
mode = mode or PaletteFX.mode
|
mode = mode or PaletteFX.mode
|
||||||
return mode == "redpp"
|
return mode == "redpp"
|
||||||
@@ -453,7 +487,10 @@ function PaletteFX.pal(data, name)
|
|||||||
if fromRom then return fromRom end
|
if fromRom then return fromRom end
|
||||||
end
|
end
|
||||||
if GameVersion.isYellow() then
|
if GameVersion.isYellow() then
|
||||||
if PaletteFX.usesYellowCgb() then
|
-- OG YELLOW and Advanced both use CGBBase for named pals: SuperPalettes
|
||||||
|
-- wash out yellows (title MEWMON/LOGO, YELLOWMON) to pale cream. World
|
||||||
|
-- tile bake still comes from the Advanced GBC pack via worldPack (#1639).
|
||||||
|
if PaletteFX.usesYellowCgb() or PaletteFX.usesGbcPack() then
|
||||||
local fromCgb = yellowCgbNamedPal(data, name)
|
local fromCgb = yellowCgbNamedPal(data, name)
|
||||||
if fromCgb then return fromCgb end
|
if fromCgb then return fromCgb end
|
||||||
end
|
end
|
||||||
@@ -511,7 +548,8 @@ function PaletteFX.monPal(data, species, transformed)
|
|||||||
if pal then return pal end
|
if pal then return pal end
|
||||||
end
|
end
|
||||||
local name = p.pokemon[species] or "MEWMON"
|
local name = p.pokemon[species] or "MEWMON"
|
||||||
if PaletteFX.usesYellowCgb() then
|
if PaletteFX.usesYellowCgb()
|
||||||
|
or (GameVersion.isYellow() and PaletteFX.usesGbcPack()) then
|
||||||
local yc = PaletteFX.pal(data, name)
|
local yc = PaletteFX.pal(data, name)
|
||||||
if yc then return yc end
|
if yc then return yc end
|
||||||
end
|
end
|
||||||
@@ -618,16 +656,14 @@ local ROUTE_6_SAFFRON = { mapId = "ROUTE_6", useMapId = "SAFFRON_CITY", cellYBel
|
|||||||
-- (false for a mod tileset with no pokered-gbc counterpart, or when the
|
-- (false for a mod tileset with no pokered-gbc counterpart, or when the
|
||||||
-- pack failed to load at all)
|
-- pack failed to load at all)
|
||||||
function PaletteFX.hasWorldTileset(tileset)
|
function PaletteFX.hasWorldTileset(tileset)
|
||||||
local pack = PaletteFX.gbcPack()
|
local w = PaletteFX.worldPack()
|
||||||
local w = pack and pack.world
|
|
||||||
return (w and w.tileGroups[tileset]) ~= nil
|
return (w and w.tileGroups[tileset]) ~= nil
|
||||||
end
|
end
|
||||||
|
|
||||||
-- the palette-group (0-7) a tile GRAPHIC id resolves to in this tileset,
|
-- the palette-group (0-7) a tile GRAPHIC id resolves to in this tileset,
|
||||||
-- with the current map's tile-id exceptions (if any) applied first
|
-- with the current map's tile-id exceptions (if any) applied first
|
||||||
function PaletteFX.worldGroupAt(tileset, mapId, tileId)
|
function PaletteFX.worldGroupAt(tileset, mapId, tileId)
|
||||||
local pack = PaletteFX.gbcPack()
|
local w = PaletteFX.worldPack()
|
||||||
local w = pack and pack.world
|
|
||||||
local groups = w and w.tileGroups[tileset]
|
local groups = w and w.tileGroups[tileset]
|
||||||
if not groups then return nil end
|
if not groups then return nil end
|
||||||
local exc = TILE_GROUP_EXCEPTIONS[mapId]
|
local exc = TILE_GROUP_EXCEPTIONS[mapId]
|
||||||
@@ -642,8 +678,7 @@ end
|
|||||||
-- Saffron's roof colors while the player stands in its top 2 cell rows,
|
-- Saffron's roof colors while the player stands in its top 2 cell rows,
|
||||||
-- like pokered's wYCoord check -- data is Game.data, for the map lookup)
|
-- like pokered's wYCoord check -- data is Game.data, for the map lookup)
|
||||||
function PaletteFX.worldGroupColors(data, tileset, mapId, playerCellY)
|
function PaletteFX.worldGroupColors(data, tileset, mapId, playerCellY)
|
||||||
local pack = PaletteFX.gbcPack()
|
local w = PaletteFX.worldPack()
|
||||||
local w = pack and pack.world
|
|
||||||
local base = w and w.groupColors[tileset]
|
local base = w and w.groupColors[tileset]
|
||||||
if not base then return nil end
|
if not base then return nil end
|
||||||
if not w.roofGroup[tileset] then return darkGroups(base) end
|
if not w.roofGroup[tileset] then return darkGroups(base) end
|
||||||
@@ -678,17 +713,18 @@ end
|
|||||||
-- here): a stable hash instead, so the same NPC instance always shows the
|
-- here): a stable hash instead, so the same NPC instance always shows the
|
||||||
-- same one of the 4 SPR_PAL_* colors.
|
-- same one of the 4 SPR_PAL_* colors.
|
||||||
function PaletteFX.spriteObp(spriteDef, seed)
|
function PaletteFX.spriteObp(spriteDef, seed)
|
||||||
local pack = PaletteFX.gbcPack()
|
local w = PaletteFX.worldPack()
|
||||||
local w = pack and pack.world
|
|
||||||
local src = spriteDef and (spriteDef.paletteSource or spriteDef.source)
|
local src = spriteDef and (spriteDef.paletteSource or spriteDef.source)
|
||||||
if not (w and src) then return nil end
|
if not (w and src) then return nil end
|
||||||
local idx = tonumber(src:match("%[(%d+)%]"))
|
local idx = tonumber(src:match("%[(%d+)%]"))
|
||||||
-- RedBikeSprite and SurfingPikachuSprite load outside
|
-- RedBikeSprite and SurfingPikachuSprite load outside
|
||||||
-- SpriteSheetPointerTable, so their source has no bracketed index;
|
-- SpriteSheetPointerTable, so their source has no bracketed index;
|
||||||
-- they wear the player's OBP palette (spriteAssignment[0]).
|
-- they wear the player's OBP palette (spriteAssignment[0]) on Red/Blue.
|
||||||
if not idx and (src:find("RedBikeSprite", 1, true)
|
-- On Yellow, Surfing Pikachu uses the Pikachu yellow OBJ group (#1639).
|
||||||
or src:find("SurfingPikachuSprite", 1, true)) then
|
if not idx and src:find("RedBikeSprite", 1, true) then
|
||||||
idx = 0
|
idx = 0
|
||||||
|
elseif not idx and src:find("SurfingPikachuSprite", 1, true) then
|
||||||
|
idx = GameVersion.isYellow() and 60 or 0
|
||||||
end
|
end
|
||||||
local group = idx and w.spriteAssignment[idx]
|
local group = idx and w.spriteAssignment[idx]
|
||||||
if group == nil then return nil end
|
if group == nil then return nil end
|
||||||
@@ -761,6 +797,18 @@ function PaletteFX.shadeMap()
|
|||||||
return shadeMap
|
return shadeMap
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function invalidateColorCaches()
|
||||||
|
pcall(function() require("src.battle.BattleState").invalidate() end)
|
||||||
|
pcall(function() require("src.render.SpriteRenderer").invalidate() end)
|
||||||
|
pcall(function()
|
||||||
|
require("src.world.MapLoader").invalidateAll()
|
||||||
|
local Game = require("src.core.Game")
|
||||||
|
if Game.overworld and Game.overworld.map and Game.overworld.reloadMap then
|
||||||
|
Game.overworld:reloadMap(Game.overworld.map.id, "colors")
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
function PaletteFX.setMode(mode)
|
function PaletteFX.setMode(mode)
|
||||||
local prev = PaletteFX.mode
|
local prev = PaletteFX.mode
|
||||||
local ok = false
|
local ok = false
|
||||||
@@ -772,27 +820,35 @@ function PaletteFX.setMode(mode)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
if not ok then PaletteFX.mode = "gbc" end
|
if not ok then PaletteFX.mode = "gbc" end
|
||||||
-- battle pics and overworld sprites bake the active pack into ImageData;
|
if prev ~= PaletteFX.mode then invalidateColorCaches() end
|
||||||
-- drop those caches when the pack (or any COLORS mode) changes so the
|
|
||||||
-- next draw re-tints
|
|
||||||
if prev ~= PaletteFX.mode then
|
|
||||||
pcall(function() require("src.battle.BattleState").invalidate() end)
|
|
||||||
pcall(function() require("src.render.SpriteRenderer").invalidate() end)
|
|
||||||
-- RED++'s baked tileset atlas (TileRenderer.getGbcAtlas) is built once
|
|
||||||
-- per loaded map, so a mode toggle needs every cached Map/TileRenderer
|
|
||||||
-- dropped and the currently-visible one rebuilt in place -- otherwise
|
|
||||||
-- the on-screen map keeps its stale (wrong-mode) atlas until the next
|
|
||||||
-- map transition happens to reload it.
|
|
||||||
pcall(function()
|
|
||||||
require("src.world.MapLoader").invalidateAll()
|
|
||||||
local Game = require("src.core.Game")
|
|
||||||
if Game.overworld and Game.overworld.map and Game.overworld.reloadMap then
|
|
||||||
Game.overworld:reloadMap(Game.overworld.map.id, "colors")
|
|
||||||
end
|
end
|
||||||
end)
|
|
||||||
|
PaletteFX.customRamp = nil
|
||||||
|
|
||||||
|
function PaletteFX.setCustomRamp(ramp)
|
||||||
|
local prev = PaletteFX.customRamp
|
||||||
|
PaletteFX.customRamp = ramp
|
||||||
|
if (prev ~= nil) ~= (ramp ~= nil) or prev ~= ramp then
|
||||||
|
invalidateColorCaches()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function PaletteFX.pickerActive()
|
||||||
|
local ok, Game = pcall(require, "src.core.Game")
|
||||||
|
local states = ok and Game.stack and Game.stack.states
|
||||||
|
local top = states and states[#states]
|
||||||
|
return top ~= nil and top.screenId == "PaletteScreen"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Whether the active COLORS state needs a battle pic rendered as raw DMG
|
||||||
|
-- grays instead of real colour, the same "forced-mono" contract OG/OG
|
||||||
|
-- INV/CLASSIC already use (issue #207). A custom palette needs this too,
|
||||||
|
-- since ensureZones re-thresholds the already-drawn frame. Keep this in
|
||||||
|
-- sync with ensureZones and the mode checks in BattleState and WideBattle.
|
||||||
|
function PaletteFX.forcesRawGrays()
|
||||||
|
return PaletteFX.customRamp ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
function PaletteFX.cycleMode()
|
function PaletteFX.cycleMode()
|
||||||
local cur = PaletteFX.mode or "gbc"
|
local cur = PaletteFX.mode or "gbc"
|
||||||
local idx = 1
|
local idx = 1
|
||||||
@@ -805,6 +861,13 @@ end
|
|||||||
|
|
||||||
function PaletteFX.applyOptions(opts)
|
function PaletteFX.applyOptions(opts)
|
||||||
PaletteFX.setMode(opts and opts.colors or "gbc")
|
PaletteFX.setMode(opts and opts.colors or "gbc")
|
||||||
|
local id = opts and opts.palette
|
||||||
|
if id and id ~= "" then
|
||||||
|
local ok, Palette = pcall(require, "src.render.Palette")
|
||||||
|
PaletteFX.setCustomRamp(ok and Palette.ramp(id) or nil)
|
||||||
|
else
|
||||||
|
PaletteFX.setCustomRamp(nil)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function PaletteFX.modeLabel(mode)
|
function PaletteFX.modeLabel(mode)
|
||||||
@@ -817,12 +880,11 @@ function PaletteFX.modeLabel(mode)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- When a state exposes no SGB zones but COLORS needs a forced palette
|
-- When a state exposes no SGB zones but COLORS needs a forced palette
|
||||||
-- (OG / OG INV / CLASSIC), invent a whole-screen zone so the shade-remap
|
|
||||||
-- shader still runs. GBC / RED++ / GBC INV leave nil alone (raw DMG canvas).
|
|
||||||
function PaletteFX.ensureZones(zones)
|
function PaletteFX.ensureZones(zones)
|
||||||
if zones and zones[1] then return zones end
|
if zones and zones[1] then return zones end
|
||||||
local mode = PaletteFX.mode or "gbc"
|
local mode = PaletteFX.mode or "gbc"
|
||||||
if mode == "og" or mode == "og_inv" or mode == "classic" then
|
if mode == "og" or mode == "og_inv" or mode == "classic"
|
||||||
|
or (PaletteFX.forcesRawGrays() and not PaletteFX.pickerActive()) then
|
||||||
return { PaletteFX.whole(PaletteFX.GRAYS) }
|
return { PaletteFX.whole(PaletteFX.GRAYS) }
|
||||||
end
|
end
|
||||||
return zones
|
return zones
|
||||||
@@ -852,7 +914,9 @@ function PaletteFX.effectiveColors(c)
|
|||||||
if not c then return nil end
|
if not c then return nil end
|
||||||
local mode = PaletteFX.mode or "gbc"
|
local mode = PaletteFX.mode or "gbc"
|
||||||
local out = c
|
local out = c
|
||||||
if mode == "og" then
|
if PaletteFX.customRamp and not PaletteFX.pickerActive() then
|
||||||
|
out = PaletteFX.customRamp
|
||||||
|
elseif mode == "og" then
|
||||||
out = PaletteFX.GRAYS
|
out = PaletteFX.GRAYS
|
||||||
elseif mode == "og_inv" then
|
elseif mode == "og_inv" then
|
||||||
out = PaletteFX.permute(PaletteFX.GRAYS, INV_MAP)
|
out = PaletteFX.permute(PaletteFX.GRAYS, INV_MAP)
|
||||||
|
|||||||
+13
-3
@@ -338,6 +338,7 @@ function Renderer:beginFrame(transparent)
|
|||||||
-- warp-fade overlay from Transition (issue #121); cleared each frame so
|
-- warp-fade overlay from Transition (issue #121); cleared each frame so
|
||||||
-- a popped transition cannot leave a sticky black veil
|
-- a popped transition cannot leave a sticky black veil
|
||||||
self.worldFadeAlpha = nil
|
self.worldFadeAlpha = nil
|
||||||
|
self.worldFadeColor = nil
|
||||||
-- battle-transition wipe, drawn over the whole surface (BattleTransition)
|
-- battle-transition wipe, drawn over the whole surface (BattleTransition)
|
||||||
self.battleWipe = nil
|
self.battleWipe = nil
|
||||||
-- whole-surface veil in screen space (battle-transition flash, the
|
-- whole-surface veil in screen space (battle-transition flash, the
|
||||||
@@ -817,7 +818,7 @@ function Renderer:frameRects()
|
|||||||
-- GB pixel stops being a whole number of screen pixels, which is the trade
|
-- GB pixel stops being a whole number of screen pixels, which is the trade
|
||||||
-- the setting exists to offer. Clamped on the horizontal too, so a narrow
|
-- the setting exists to offer. Clamped on the horizontal too, so a narrow
|
||||||
-- window scales to fit instead of overflowing off both sides.
|
-- window scales to fit instead of overflowing off both sides.
|
||||||
if self.uiFill then
|
if self.uiFill and not FaithfulRes.scaleCap() then
|
||||||
Up = math.min(ph / uih, pw / uiw)
|
Up = math.min(ph / uih, pw / uiw)
|
||||||
end
|
end
|
||||||
if uiw * Up > pw or uih * Up > ph then
|
if uiw * Up > pw or uih * Up > ph then
|
||||||
@@ -971,6 +972,13 @@ function Renderer:endFrame(zones, worldZones)
|
|||||||
and not FaithfulRes.scaleCap() then
|
and not FaithfulRes.scaleCap() then
|
||||||
clearR, clearG, clearB = PaletteFX.paperShade(Game and Game.data)
|
clearR, clearG, clearB = PaletteFX.paperShade(Game and Game.data)
|
||||||
end
|
end
|
||||||
|
-- UI LETTERBOX overrides whatever the rules above settled on, except
|
||||||
|
-- under FAITHFUL RATIO's mobile lock, which promises black bars.
|
||||||
|
if not FaithfulRes.scaleCap() then
|
||||||
|
local Letterbox = require("src.render.Letterbox")
|
||||||
|
clearR, clearG, clearB = Letterbox.fill(clearR, clearG, clearB,
|
||||||
|
function() return PaletteFX.paperShade(Game and Game.data) end)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
if cut then
|
if cut then
|
||||||
love.graphics.setColor(0, 0, 0, 1)
|
love.graphics.setColor(0, 0, 0, 1)
|
||||||
@@ -1045,7 +1053,8 @@ function Renderer:endFrame(zones, worldZones)
|
|||||||
-- the screen-space overlays the flat path draws over its composite
|
-- the screen-space overlays the flat path draws over its composite
|
||||||
local fade = self.worldFadeAlpha
|
local fade = self.worldFadeAlpha
|
||||||
if fade and fade > 0 then
|
if fade and fade > 0 then
|
||||||
love.graphics.setColor(0, 0, 0, fade)
|
local c = self.worldFadeColor or { 0, 0, 0 }
|
||||||
|
love.graphics.setColor(c[1], c[2], c[3], fade)
|
||||||
love.graphics.rectangle("fill", vux, vuy, vuw, vuh)
|
love.graphics.rectangle("fill", vux, vuy, vuw, vuh)
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
end
|
end
|
||||||
@@ -1132,7 +1141,8 @@ function Renderer:endFrame(zones, worldZones)
|
|||||||
-- composite normally if one is ever stacked that way.
|
-- composite normally if one is ever stacked that way.
|
||||||
local fade = self.worldFadeAlpha
|
local fade = self.worldFadeAlpha
|
||||||
if fade and fade > 0 then
|
if fade and fade > 0 then
|
||||||
love.graphics.setColor(0, 0, 0, fade)
|
local c = self.worldFadeColor or { 0, 0, 0 }
|
||||||
|
love.graphics.setColor(c[1], c[2], c[3], fade)
|
||||||
love.graphics.rectangle("fill", vux, vuy, vuw, vuh)
|
love.graphics.rectangle("fill", vux, vuy, vuw, vuh)
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
end
|
end
|
||||||
|
|||||||
+17
-3
@@ -264,7 +264,9 @@ ShaderFX.BRIDGE_DIR = "tools/shaderfx-bridge"
|
|||||||
local function libNames()
|
local function libNames()
|
||||||
local osName = (love and love.system and love.system.getOS
|
local osName = (love and love.system and love.system.getOS
|
||||||
and love.system.getOS()) or ""
|
and love.system.getOS()) or ""
|
||||||
if osName == "Windows" then
|
if osName == "iOS" then
|
||||||
|
return {}
|
||||||
|
elseif osName == "Windows" then
|
||||||
return { "librashader_bridge.dll" }
|
return { "librashader_bridge.dll" }
|
||||||
elseif osName == "OS X" then
|
elseif osName == "OS X" then
|
||||||
return { "liblibrashader_bridge.dylib", "librashader_bridge.dylib" }
|
return { "liblibrashader_bridge.dylib", "librashader_bridge.dylib" }
|
||||||
@@ -313,11 +315,13 @@ end
|
|||||||
|
|
||||||
-- Every place the bridge may sit, most specific first.
|
-- Every place the bridge may sit, most specific first.
|
||||||
local function libCandidates()
|
local function libCandidates()
|
||||||
|
local names = libNames()
|
||||||
|
if #names == 0 then return {} end
|
||||||
local out = {}
|
local out = {}
|
||||||
local override = os.getenv("LIBRASHADER_BRIDGE_DLL")
|
local override = os.getenv("LIBRASHADER_BRIDGE_DLL")
|
||||||
if override and override ~= "" then out[#out + 1] = override end
|
if override and override ~= "" then out[#out + 1] = override end
|
||||||
local dirs, save = sourceDirs(), saveDir()
|
local dirs, save = sourceDirs(), saveDir()
|
||||||
for _, name in ipairs(libNames()) do
|
for _, name in ipairs(names) do
|
||||||
for _, dir in ipairs(dirs) do
|
for _, dir in ipairs(dirs) do
|
||||||
out[#out + 1] = dir .. "/" .. name
|
out[#out + 1] = dir .. "/" .. name
|
||||||
out[#out + 1] = dir .. "/" .. ShaderFX.BRIDGE_DIR .. "/target/release/" .. name
|
out[#out + 1] = dir .. "/" .. ShaderFX.BRIDGE_DIR .. "/target/release/" .. name
|
||||||
@@ -353,7 +357,17 @@ local function ensureLib()
|
|||||||
end
|
end
|
||||||
tried[#tried + 1] = path
|
tried[#tried + 1] = path
|
||||||
end
|
end
|
||||||
libError = "librashader bridge not found; looked in " .. table.concat(tried, ", ")
|
local okSym, sym = pcall(function()
|
||||||
|
return ffi.C and ffi.C.librashader_translate_preset
|
||||||
|
end)
|
||||||
|
if okSym and sym ~= nil then
|
||||||
|
lib = ffi.C
|
||||||
|
return lib
|
||||||
|
end
|
||||||
|
libError = "librashader bridge not found; ffi.C has no librashader_translate_preset"
|
||||||
|
if #tried > 0 then
|
||||||
|
libError = libError .. "; looked in " .. table.concat(tried, ", ")
|
||||||
|
end
|
||||||
return nil, libError
|
return nil, libError
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
+31
-3
@@ -11,6 +11,12 @@ local UIVisibility = require("src.battle.UIVisibility")
|
|||||||
local Theme = require("src.ui.Theme")
|
local Theme = require("src.ui.Theme")
|
||||||
local Timing = require("src.core.Timing")
|
local Timing = require("src.core.Timing")
|
||||||
|
|
||||||
|
local Chrome2
|
||||||
|
do
|
||||||
|
local ok, v = pcall(require, "src.ui.gen2.Chrome")
|
||||||
|
Chrome2 = ok and v or nil
|
||||||
|
end
|
||||||
|
|
||||||
local TextBox = {}
|
local TextBox = {}
|
||||||
TextBox.__index = TextBox
|
TextBox.__index = TextBox
|
||||||
TextBox.isTextBox = true
|
TextBox.isTextBox = true
|
||||||
@@ -470,8 +476,21 @@ function TextBox:draw()
|
|||||||
-- (pokegold engine/pokegear/pokegear.asm TownMapPals sends every tile
|
-- (pokegold engine/pokegear/pokegear.asm TownMapPals sends every tile
|
||||||
-- >= $60 to palette 0). Gen 1's Game has no textboxPaper, so it stays nil.
|
-- >= $60 to palette 0). Gen 1's Game has no textboxPaper, so it stays nil.
|
||||||
local paper = self.game and self.game.textboxPaper and self.game:textboxPaper()
|
local paper = self.game and self.game.textboxPaper and self.game:textboxPaper()
|
||||||
|
|
||||||
|
local gold = self.game and self.game.save
|
||||||
|
and (self.game.save.generation == 2 or self.game.save.version == "gold")
|
||||||
|
local Chrome = gold and Chrome2 or nil
|
||||||
|
local drawGlyph, finishGlyph = Font.drawCode, nil
|
||||||
|
if Chrome then
|
||||||
|
local base = paper and { paper, paper, paper, { 0, 0, 0 } }
|
||||||
|
or Chrome.DEFAULT_BOX_PALETTE
|
||||||
|
Chrome.paletteBox(self.boxTx, self.boxTy, self.boxTw, self.boxTh, base)
|
||||||
|
local _, dg, fg = Chrome.paletteGlyphs(base)
|
||||||
|
drawGlyph, finishGlyph = dg, fg
|
||||||
|
else
|
||||||
Font.drawBox(self.boxTx, self.boxTy, self.boxTw, self.boxTh, paper)
|
Font.drawBox(self.boxTx, self.boxTy, self.boxTw, self.boxTh, paper)
|
||||||
love.graphics.setColor(0, 0, 0, 1)
|
love.graphics.setColor(0, 0, 0, 1)
|
||||||
|
end
|
||||||
if self.scrollPx and self.scrollPx > 0 then
|
if self.scrollPx and self.scrollPx > 0 then
|
||||||
self.scrollPx = self.scrollPx - 2
|
self.scrollPx = self.scrollPx - 2
|
||||||
if self.scrollPx <= 0 then self.scrollPx = nil end
|
if self.scrollPx <= 0 then self.scrollPx = nil end
|
||||||
@@ -491,17 +510,25 @@ function TextBox:draw()
|
|||||||
-- measured with; every fixed-width page still lands on the 8px grid
|
-- measured with; every fixed-width page still lands on the 8px grid
|
||||||
local pen = self.textX
|
local pen = self.textX
|
||||||
for _, code in ipairs(line) do
|
for _, code in ipairs(line) do
|
||||||
Font.drawCode(code, pen, y)
|
drawGlyph(code, pen, y)
|
||||||
pen = pen + Font.advanceOf(code)
|
pen = pen + Font.advanceOf(code)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if self.money then
|
if self.money then
|
||||||
-- money box (engine/menus/text_box.asm:130): DisplayMoneyBox at
|
-- money box (engine/menus/text_box.asm:130): DisplayMoneyBox at
|
||||||
-- hlcoord 11,0, the amount right-aligned on its middle row
|
-- hlcoord 11,0, the amount right-aligned on its middle row
|
||||||
|
if Chrome then
|
||||||
|
Chrome.paletteBox(11, 0, 9, 3, Chrome.DEFAULT_BOX_PALETTE)
|
||||||
|
else
|
||||||
Font.drawBox(11, 0, 9, 3)
|
Font.drawBox(11, 0, 9, 3)
|
||||||
love.graphics.setColor(0, 0, 0, 1)
|
love.graphics.setColor(0, 0, 0, 1)
|
||||||
|
end
|
||||||
local money = ("¥%d"):format(self.money() or 0)
|
local money = ("¥%d"):format(self.money() or 0)
|
||||||
Font.draw(money, 152 - Font.width(money), 8)
|
local pen = 152 - Font.width(money)
|
||||||
|
for _, code in ipairs(Font.encode(money)) do
|
||||||
|
drawGlyph(code, pen, 8)
|
||||||
|
pen = pen + Font.advanceOf(code)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
if (self.waiting or (self.done and not self.choice and not self.auto
|
if (self.waiting or (self.done and not self.choice and not self.auto
|
||||||
and (not self.stay
|
and (not self.stay
|
||||||
@@ -509,10 +536,11 @@ function TextBox:draw()
|
|||||||
and self.blink < 30 then
|
and self.blink < 30 then
|
||||||
-- page-advance cursor: glyph $EE by default, the blinking down arrow
|
-- page-advance cursor: glyph $EE by default, the blinking down arrow
|
||||||
-- the original prints via `ld a, "▼"` (home/text.asm)
|
-- the original prints via `ld a, "▼"` (home/text.asm)
|
||||||
Font.drawCode(Theme.moreArrow or 0xEE,
|
drawGlyph(Theme.moreArrow or 0xEE,
|
||||||
(self.boxTx + self.boxTw - 2) * 8,
|
(self.boxTx + self.boxTw - 2) * 8,
|
||||||
(self.boxTy + self.boxTh - 1) * 8 - 4)
|
(self.boxTy + self.boxTh - 1) * 8 - 4)
|
||||||
end
|
end
|
||||||
|
if finishGlyph then finishGlyph() end
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -65,16 +65,24 @@ end
|
|||||||
-- (ViridianGym.asm .afterBeat, RocketHideoutB4F BeatGiovanniScript) call
|
-- (ViridianGym.asm .afterBeat, RocketHideoutB4F BeatGiovanniScript) call
|
||||||
-- GBFadeOutToBlack -> GBFadeInFromBlack instead, so the default keeps the
|
-- GBFadeOutToBlack -> GBFadeInFromBlack instead, so the default keeps the
|
||||||
-- symmetric 32-frame fade back in (home/fade.asm:21, b = 4).
|
-- symmetric 32-frame fade back in (home/fade.asm:21, b = 4).
|
||||||
function Transition.new(game, onMidpoint, onDone, warp)
|
--
|
||||||
|
-- opts.color = {r,g,b} in 0..1 (default black). opts.frames / opts.framesIn
|
||||||
|
-- override duration. Fly/Teleport/Dig/Escape Rope use white
|
||||||
|
-- GBFadeOutToWhite / GBFadeInFromWhite (#1644).
|
||||||
|
function Transition.new(game, onMidpoint, onDone, warp, opts)
|
||||||
|
opts = opts or {}
|
||||||
local self = setmetatable({}, Transition)
|
local self = setmetatable({}, Transition)
|
||||||
self.game = game
|
self.game = game
|
||||||
self.onMidpoint = onMidpoint
|
self.onMidpoint = onMidpoint
|
||||||
self.onDone = onDone
|
self.onDone = onDone
|
||||||
self.t = 0
|
self.t = 0
|
||||||
self.phase = "out"
|
self.phase = "out"
|
||||||
|
self.color = opts.color or { 0, 0, 0 }
|
||||||
local style = styleOf(game, "warp_fade")
|
local style = styleOf(game, "warp_fade")
|
||||||
self.frames = style.frames or FRAMES
|
self.frames = opts.frames or style.frames or FRAMES
|
||||||
if warp then
|
if opts.framesIn ~= nil then
|
||||||
|
self.framesIn = opts.framesIn
|
||||||
|
elseif warp then
|
||||||
-- a style may still ask for a fade in (mods, and the record is
|
-- a style may still ask for a fade in (mods, and the record is
|
||||||
-- data-driven); the built-in warp is 0, matching hardware
|
-- data-driven); the built-in warp is 0, matching hardware
|
||||||
self.framesIn = style.framesIn or FRAMES_IN
|
self.framesIn = style.framesIn or FRAMES_IN
|
||||||
@@ -125,6 +133,7 @@ end
|
|||||||
|
|
||||||
function Transition:draw()
|
function Transition:draw()
|
||||||
local alpha = self:alpha()
|
local alpha = self:alpha()
|
||||||
|
local c = self.color or { 0, 0, 0 }
|
||||||
-- Survey zoom draws the overworld into a window-filling world canvas
|
-- Survey zoom draws the overworld into a window-filling world canvas
|
||||||
-- while the UI pass stays the classic 160x144 letterbox. A rect on the
|
-- while the UI pass stays the classic 160x144 letterbox. A rect on the
|
||||||
-- UI canvas only darkens that center box (issue #121); when the world
|
-- UI canvas only darkens that center box (issue #121); when the world
|
||||||
@@ -133,9 +142,10 @@ function Transition:draw()
|
|||||||
local r = self.game and self.game.renderer
|
local r = self.game and self.game.renderer
|
||||||
if r and r.worldActive then
|
if r and r.worldActive then
|
||||||
r.worldFadeAlpha = alpha
|
r.worldFadeAlpha = alpha
|
||||||
|
r.worldFadeColor = c
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
love.graphics.setColor(0, 0, 0, alpha)
|
love.graphics.setColor(c[1], c[2], c[3], alpha)
|
||||||
love.graphics.rectangle("fill", 0, 0, 160, 144)
|
love.graphics.rectangle("fill", 0, 0, 160, 144)
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
end
|
end
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user