mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-28 18:06:09 +02:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9af0d5a723 |
@@ -333,56 +333,6 @@ 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, shaderfx-bridge]
|
needs: [version, love-payload]
|
||||||
# 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,22 +155,13 @@ 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
|
||||||
@@ -182,45 +173,6 @@ 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]
|
||||||
@@ -334,85 +286,8 @@ 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, linux-flatpak, native-tls-win, shaderfx-bridge]
|
needs: [version, love-payload, xbox-uwp, linux-arm64, native-tls-win]
|
||||||
runs-on: ${{ fromJSON(github.repository == 'bryanthaboi/gen1recomp' && '["self-hosted", "macOS"]' || '"macos-latest"') }}
|
runs-on: ${{ fromJSON(github.repository == 'bryanthaboi/gen1recomp' && '["self-hosted", "macOS"]' || '"macos-latest"') }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -434,28 +309,6 @@ 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).
|
||||||
@@ -505,7 +358,6 @@ 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);
|
||||||
@@ -520,8 +372,6 @@ 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:
|
||||||
@@ -579,8 +429,6 @@ 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
|
||||||
@@ -594,7 +442,6 @@ 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
|
||||||
@@ -651,13 +498,6 @@ 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
|
||||||
@@ -669,24 +509,16 @@ 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"
|
||||||
# x86_64 desktop Linux: raw AppImage (no zip wrapper).
|
cp "dist/linux/gen1recomp-linux.zip" "$outdir/gen1recomp-${v}-linux.zip"
|
||||||
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 so `chmod +x && ./it` just works.
|
# AppImage rather than a zip 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"
|
||||||
@@ -824,9 +656,8 @@ 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-x86_64.AppImage"
|
"dist/release/gen1recomp-${v}-linux.zip"
|
||||||
"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"
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
# Gen1Recomp
|
# Gen1Recomp
|
||||||
|
|
||||||
A native LÖVE2D recreation of Poke Red, Blue, Gold, Silver, and Crystal. The
|
A native LÖVE2D recreation of Poke Red, Blue and Yellow. The engine and map
|
||||||
engine and map behavior are hand-written Lua; game data and graphics are
|
behavior are hand-written Lua; game data and graphics are decoded from a ROM
|
||||||
decoded from a ROM supplied by the player.
|
supplied by the player.
|
||||||
|
|
||||||
And before you say, "that's not a recomp", you're wrong. Recomp is an acronym. ***Reverse Engineering Causes Obsessive Mental Problems***
|
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, 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.
|
> **We are NOT affiliated with the website `gen1recomp[.]com`** That website is not run by this project, was not authorized by us, and we have no idea who operates it. It is impersonating this project; do not download anything from it, and treat anything it hosts or claims as untrustworthy. Even if the site currently links back to this repository, the people behind it can change its content at any time, so nothing on it should ever be trusted. This GitHub repository and the Discord linked below are the only official sources for this project. Also, as I assumed would eventually happen, the idiot that made that website now pumped it full of adware. Please stay away from that website.
|
||||||
|
|
||||||
<p align="center"><img src="https://raw.githubusercontent.com/bryanthaboi/gen1recomp/refs/heads/dev/assets/logo/logo.png"></p>
|
<p align="center"><img src="https://raw.githubusercontent.com/bryanthaboi/gen1recomp/refs/heads/dev/assets/logo/logo.png"></p>
|
||||||
|
|
||||||
# [SUPPORT / ANNOUNCEMENTS / MODS ALL FOUND ON THE DISCORD](https://bois.icu)
|
**SUPPORT / ANNOUNCEMENTS / MODS:** [Discord](https://bois.icu)
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
|
||||||
@@ -228,27 +228,18 @@ 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 desktop (AppImage / Flatpak)
|
## Linux on arm64 (Raspberry Pi)
|
||||||
|
|
||||||
Releases ship raw AppImages (no zip wrapper) plus an optional Flatpak bundle:
|
Alongside the x86_64 `gen1recomp-*-linux.zip`, every release ships
|
||||||
|
`gen1recomp-*-linux-arm64.AppImage` for 64-bit ARM desktop Linux — Raspberry
|
||||||
- `gen1recomp-*-linux-x86_64.AppImage`
|
Pi 4/5, Armbian and other SBC distros, and arm64 VMs on Apple Silicon:
|
||||||
- `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
|
||||||
```
|
```
|
||||||
|
|
||||||
Shared troubleshooting (FUSE, curl, portable mode): [docs/linux-appimage.md](docs/linux-appimage.md).
|
LÖVE publishes no aarch64 binary of any kind, so this artifact compiles the
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
+38
-174
@@ -192,20 +192,6 @@ 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
|
||||||
@@ -217,203 +203,81 @@ 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'
|
||||||
#!/usr/bin/env bash
|
#!/bin/bash
|
||||||
# gen1recomp-sbc — High-Performance Native ARM64 Handheld Port Launcher
|
# gen1recomp-sbc — Linux ARM SBC / PortMaster launcher
|
||||||
# Compatible with TrimUI Brick, TrimUI Smart Pro, Anbernic (H700/RK3566), muOS, Knulli, ArkOS, Stock OS
|
# Uses SHDIR-relative paths so firmware-specific mount points do not matter.
|
||||||
|
|
||||||
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)"
|
||||||
GAMEDIR="$SHDIR/gen1recomp-sbc"
|
|
||||||
CONFDIR="$GAMEDIR/conf"
|
|
||||||
|
|
||||||
mkdir -p "$CONFDIR"
|
if [ -d "/mnt/SDCARD/Apps/PortMaster/PortMaster/" ]; then
|
||||||
cd "$GAMEDIR" || exit 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"
|
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
|
elif [ -d "/mnt/SDCARD/Roms/ports/PortMaster" ]; then
|
||||||
controlfolder="/mnt/SDCARD/Roms/ports/PortMaster"
|
controlfolder="/mnt/SDCARD/Roms/ports/PortMaster"
|
||||||
elif [ -d "/mnt/SDCARD/Roms/PORTS/PortMaster" ]; then
|
elif [ -d "/mnt/SDCARD/Data/PortMaster/" ]; then
|
||||||
controlfolder="/mnt/SDCARD/Roms/PORTS/PortMaster"
|
controlfolder="/mnt/SDCARD/Data/PortMaster"
|
||||||
elif [ -d "/mnt/SDCARD/Ports/PortMaster" ]; then
|
|
||||||
controlfolder="/mnt/SDCARD/Ports/PortMaster"
|
|
||||||
elif [ -d "$SHDIR/PortMaster" ]; then
|
elif [ -d "$SHDIR/PortMaster" ]; then
|
||||||
controlfolder="$SHDIR/PortMaster"
|
controlfolder="$SHDIR/PortMaster"
|
||||||
elif [ -d "/opt/system/Tools/PortMaster" ]; then
|
elif [ -d "/opt/system/Tools/PortMaster/" ]; then
|
||||||
controlfolder="/opt/system/Tools/PortMaster"
|
controlfolder="/opt/system/Tools/PortMaster"
|
||||||
elif [ -d "/opt/tools/PortMaster" ]; then
|
elif [ -d "/opt/tools/PortMaster/" ]; then
|
||||||
controlfolder="/opt/tools/PortMaster"
|
controlfolder="/opt/tools/PortMaster"
|
||||||
elif [ -d "$XDG_DATA_HOME/PortMaster" ]; then
|
elif [ -d "$XDG_DATA_HOME/PortMaster/" ]; then
|
||||||
controlfolder="$XDG_DATA_HOME/PortMaster"
|
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
|
elif [ -d "/roms/ports/PortMaster" ]; then
|
||||||
controlfolder="/roms/ports/PortMaster"
|
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
|
else
|
||||||
echo "PortMaster control.txt not found, using standalone handheld configuration."
|
controlfolder="/mnt/SDCARD/Roms/PORTS/PortMaster"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 2. CPU governor
|
if [ ! -f "$controlfolder/control.txt" ]; then
|
||||||
# Pinning every core to performance keeps handhelds at peak clock even when gameplay
|
echo "PortMaster control.txt not found under $controlfolder" >&2
|
||||||
# uses only a fraction of CPU. Schedutil is the default; set POKEPORT_CPU_GOVERNOR=performance
|
exit 1
|
||||||
# only for profiling or devices needing an extra boost.
|
fi
|
||||||
CPU_GOVERNOR_OVERRIDE="${POKEPORT_CPU_GOVERNOR:-schedutil}"
|
# shellcheck disable=SC1090
|
||||||
CPU_GOVERNOR_STATE=""
|
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
|
||||||
|
|
||||||
write_cpu_governor() {
|
GAMEDIR="$SHDIR/gen1recomp-sbc"
|
||||||
local governor_path="$1"
|
CONFDIR="$GAMEDIR/conf"
|
||||||
local governor="$2"
|
mkdir -p "$CONFDIR"
|
||||||
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() {
|
cd "$GAMEDIR" || exit 1
|
||||||
[ -n "$CPU_GOVERNOR_STATE" ] || return 0
|
> "$GAMEDIR/log.txt" && exec > >(tee "$GAMEDIR/log.txt") 2>&1
|
||||||
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:$GAMEDIR/libs:/usr/trimui/lib:/mnt/SDCARD/System/lib:/usr/lib64:/usr/lib:${LD_LIBRARY_PATH:-}"
|
export LD_LIBRARY_PATH="$GAMEDIR/libs.aarch64:${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}"
|
||||||
|
|
||||||
# Audio buffer size
|
$ESUDO chmod a+x ./bin/love.aarch64 2>/dev/null || chmod a+x ./bin/love.aarch64
|
||||||
export SDL_AUDIO_SAMPLES=1024
|
$ESUDO chmod 666 /dev/uinput 2>/dev/null || true
|
||||||
|
|
||||||
# Chip-synth sample rate. The audio worker is the dominant CPU cost while
|
if [ -n "${GPTOKEYB:-}" ]; then
|
||||||
# idle (music keeps playing when the screen is static), and synthesis cost
|
$GPTOKEYB "love.aarch64" &
|
||||||
# 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,20 +156,6 @@ 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,16 +62,7 @@ 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
|
||||||
|
|||||||
@@ -1,765 +0,0 @@
|
|||||||
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" },
|
|
||||||
} },
|
|
||||||
}
|
|
||||||
+12
-19
@@ -1251,26 +1251,19 @@ local function pokemonTower2FRivalScript(playerX)
|
|||||||
and TOWER_RIVAL_EXIT_DOWN_THEN_RIGHT
|
and TOWER_RIVAL_EXIT_DOWN_THEN_RIGHT
|
||||||
or TOWER_RIVAL_EXIT_RIGHT_THEN_DOWN
|
or TOWER_RIVAL_EXIT_RIGHT_THEN_DOWN
|
||||||
return {
|
return {
|
||||||
{ "face_player" },
|
{ "face_player" }, -- 1
|
||||||
{ "check_flag", "EVENT_BEAT_POKEMON_TOWER_RIVAL" },
|
{ "check_flag", "EVENT_BEAT_POKEMON_TOWER_RIVAL" }, -- 2
|
||||||
{ "jump_if_true", "beaten" },
|
{ "jump_if_true", 12 }, -- 3
|
||||||
{ "show_text", "_PokemonTower2FRivalWhatBringsYouHereText" },
|
{ "show_text", "_PokemonTower2FRivalWhatBringsYouHereText" }, -- 4
|
||||||
-- .DefeatedText rides BIT_PRINT_END_BATTLE_TEXT, so it prints on the
|
{ "rival_battle", "OPP_RIVAL2", 4 }, -- 5
|
||||||
-- battle screen -- scripts/PokemonTower2F.asm:145-150
|
{ "jump_if_false", "end" }, -- 6 loss: stay
|
||||||
{ "save_end_battle_text", "_PokemonTower2FRivalDefeatedText" },
|
{ "set_flag", "EVENT_BEAT_POKEMON_TOWER_RIVAL" }, -- 7
|
||||||
{ "rival_battle", "OPP_RIVAL2", 4 },
|
{ "show_text", "_PokemonTower2FRivalDefeatedText" }, -- 8
|
||||||
{ "jump_if_false", "end" }, -- loss: stay
|
|
||||||
{ "set_flag", "EVENT_BEAT_POKEMON_TOWER_RIVAL" },
|
|
||||||
-- the win re-runs DisplayTextID on the beaten branch
|
|
||||||
-- scripts/PokemonTower2F.asm:72-75, :137-140
|
|
||||||
{ "show_text", "_PokemonTower2FRivalHowsYourDexText" },
|
|
||||||
{ "play_music", "Music_MeetRival", { start = "rival" } },
|
{ "play_music", "Music_MeetRival", { start = "rival" } },
|
||||||
{ "walk_npc", 1, exitDirs },
|
{ "walk_npc", 1, exitDirs }, -- 9
|
||||||
{ "hide_object", "POKEMON_TOWER_2F", "POKEMONTOWER2F_RIVAL" },
|
{ "hide_object", "POKEMON_TOWER_2F", "POKEMONTOWER2F_RIVAL" }, -- 10
|
||||||
{ "play_default_music" }, -- scripts/PokemonTower2F.asm:124
|
{ "jump", "end" }, -- 11
|
||||||
{ "jump", "end" },
|
{ "show_text", "_PokemonTower2FRivalHowsYourDexText" }, -- 12
|
||||||
{ "label", "beaten" },
|
|
||||||
{ "show_text", "_PokemonTower2FRivalHowsYourDexText" },
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -167,11 +167,13 @@ M.POKEMON_TOWER_6F = {
|
|||||||
-- PlayCry RESTLESS_SOUL (EQU MAROWAK, constants/pokemon_constants
|
-- PlayCry RESTLESS_SOUL (EQU MAROWAK, constants/pokemon_constants
|
||||||
-- .asm:209) + WaitForSoundToFinish + DelayFrames 30 before the
|
-- .asm:209) + WaitForSoundToFinish + DelayFrames 30 before the
|
||||||
-- calmed line; the port dropped the first text and the cry
|
-- calmed line; the port dropped the first text and the cry
|
||||||
-- (#867). text/PokemonTower6F.asm:1-5 (#1849)
|
-- (#867). play_cry arms the next show_text, so the cry rides
|
||||||
|
-- the calmed box's open with the button prompt kept, and the
|
||||||
|
-- wait row stands in for the asm's 30-frame gap.
|
||||||
local rows = {
|
local rows = {
|
||||||
{ "play_cry", "MAROWAK" },
|
|
||||||
{ "show_text", t._PokemonTower6FGhostWasCubonesMotherText
|
{ "show_text", t._PokemonTower6FGhostWasCubonesMotherText
|
||||||
or "The GHOST was the\nrestless soul of\vCUBONE's mother!" },
|
or "The GHOST was the\nrestless soul of\vCUBONE's mother!" },
|
||||||
|
{ "play_cry", "MAROWAK", true },
|
||||||
{ "wait", 30 },
|
{ "wait", 30 },
|
||||||
{ "show_text", t._PokemonTower6FSoulWasCalmedText
|
{ "show_text", t._PokemonTower6FSoulWasCalmedText
|
||||||
or "The mother's soul\nwas calmed.\012It departed to\nthe afterlife!" },
|
or "The mother's soul\nwas calmed.\012It departed to\nthe afterlife!" },
|
||||||
@@ -189,8 +191,7 @@ M.POKEMON_TOWER_6F = {
|
|||||||
end
|
end
|
||||||
ow:afterBattle(result, battle)
|
ow:afterBattle(result, battle)
|
||||||
end
|
end
|
||||||
-- InitWildBattle runs the wipe before the disguise (core.asm:6695-6702)
|
game.stack:push(battle)
|
||||||
ow:pushBattle(battle)
|
|
||||||
end))
|
end))
|
||||||
return true
|
return true
|
||||||
end,
|
end,
|
||||||
@@ -442,9 +443,6 @@ M.ROCKET_HIDEOUT_B4F = {
|
|||||||
or "I hope we meet\nagain..."
|
or "I hope we meet\nagain..."
|
||||||
game.stack:push(TextBox.new(game, impressed, function()
|
game.stack:push(TextBox.new(game, impressed, function()
|
||||||
local battle = BattleState.newTrainer(game, "OPP_GIOVANNI", 1)
|
local battle = BattleState.newTrainer(game, "OPP_GIOVANNI", 1)
|
||||||
-- SaveEndBattleTextPointers (scripts/RocketHideoutB4F.asm:99-120):
|
|
||||||
-- PrintEndBattleText prints it on the battle screen (#1817)
|
|
||||||
battle.endBattleText = TextBox.substitute(game, cannotBe)
|
|
||||||
battle.onFinish = function(result)
|
battle.onFinish = function(result)
|
||||||
if result ~= "win" then
|
if result ~= "win" then
|
||||||
ow:afterBattle(result, battle)
|
ow:afterBattle(result, battle)
|
||||||
@@ -453,7 +451,9 @@ M.ROCKET_HIDEOUT_B4F = {
|
|||||||
end
|
end
|
||||||
game.save.defeatedTrainers[npc.id] = true
|
game.save.defeatedTrainers[npc.id] = true
|
||||||
game.save.flags.EVENT_BEAT_ROCKET_HIDEOUT_GIOVANNI = true
|
game.save.flags.EVENT_BEAT_ROCKET_HIDEOUT_GIOVANNI = true
|
||||||
-- BeatGiovanniScript (scripts/RocketHideoutB4F.asm)
|
-- End-battle "WHAT!" then BeatGiovanniScript's hope text,
|
||||||
|
-- fade, HideObject Giovanni, ShowObject Silph Scope.
|
||||||
|
game.stack:push(TextBox.new(game, cannotBe, function()
|
||||||
game.stack:push(TextBox.new(game, hope, function()
|
game.stack:push(TextBox.new(game, hope, function()
|
||||||
local Transition = require("src.render.Transition")
|
local Transition = require("src.render.Transition")
|
||||||
game.stack:push(Transition.new(game, function()
|
game.stack:push(Transition.new(game, function()
|
||||||
@@ -468,6 +468,7 @@ M.ROCKET_HIDEOUT_B4F = {
|
|||||||
done()
|
done()
|
||||||
end))
|
end))
|
||||||
end))
|
end))
|
||||||
|
end))
|
||||||
end
|
end
|
||||||
ow:pushBattle(battle)
|
ow:pushBattle(battle)
|
||||||
end))
|
end))
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
# 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,25 +34,6 @@ 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,11 +1,10 @@
|
|||||||
# 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-x86_64.AppImage`. It targets 64-bit ARM
|
existing x86_64 `gen1recomp-<version>-linux.zip`. 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. See also
|
aarch64 handhelds that run a full distro.
|
||||||
[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
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -1111,37 +1111,6 @@ own state, preserving selective control outside a battle; a wrapper that only
|
|||||||
owns battle presentation should return `false` only for its active battle or
|
owns battle presentation should return `false` only for its active battle or
|
||||||
text-box state.
|
text-box state.
|
||||||
|
|
||||||
`pokemon.level_visible` (RFC 0019) takes a Pokémon's level off the screens
|
|
||||||
that print it, without moving anything else on them:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
mod.hooks:wrap("pokemon.level_visible", function(next, mon, ctx)
|
|
||||||
-- ctx = { where = "battle.enemy" | "battle.player" | "party" | "summary",
|
|
||||||
-- game = <Game> }
|
|
||||||
if myMode.active and ctx.where ~= "party" then return false end
|
|
||||||
return next(mon, ctx)
|
|
||||||
end)
|
|
||||||
```
|
|
||||||
|
|
||||||
It receives `(next, mon, ctx)` and defaults to `true`, so vanilla rendering
|
|
||||||
is unchanged. Only an explicit `false` suppresses; `nil` and anything else
|
|
||||||
print, so a wrapper that forgets a branch cannot blank a screen by accident.
|
|
||||||
The `<LV>` glyph goes with the digits, and on status page 2 so does the
|
|
||||||
`<to>` arrow that points at the next level, because an arrow with nothing
|
|
||||||
after it is half a sentence. A status condition still replaces the level on
|
|
||||||
a battle healthbox exactly as it does in the cart, so hiding a level never
|
|
||||||
hides `PSN` or `BRN`.
|
|
||||||
|
|
||||||
`ctx.where` names the surface rather than the widget, because the number
|
|
||||||
means different things on different screens: on a battle HUD an opponent's
|
|
||||||
level is information about them, on the party and status screens your own
|
|
||||||
level is information about you. A mode can hide one and keep the other.
|
|
||||||
|
|
||||||
**Gen 1 only for now.** The Gen 2 screens keep their own level readouts and
|
|
||||||
do not consult this hook, and neither does the Gen 1 PC box list, where the
|
|
||||||
level is part of a row label rather than a drawn field. Both are noted in
|
|
||||||
RFC 0019 as follow-ups.
|
|
||||||
|
|
||||||
`core.logic_speed` receives `(next, game)` once per `Game:logicSpeed()` call
|
`core.logic_speed` receives `(next, game)` once per `Game:logicSpeed()` call
|
||||||
(once per frame). Vanilla behavior resolves the per-category GAME SPEED
|
(once per frame). Vanilla behavior resolves the per-category GAME SPEED
|
||||||
option (`GameSpeed.CATEGORIES`: overworld/battle/menu) for whichever
|
option (`GameSpeed.CATEGORIES`: overworld/battle/menu) for whichever
|
||||||
|
|||||||
@@ -821,7 +821,7 @@ mod.content.phone_contacts:patch("PHONE_YOUNGSTER_JOEY", { map = "ROUTE_31" })
|
|||||||
| `catchRate` | integer 0..255 | yes |
|
| `catchRate` | integer 0..255 | yes |
|
||||||
| `cry` | cries id | no |
|
| `cry` | cries id | no |
|
||||||
| `dex` | integer >= 1 | yes |
|
| `dex` | integer >= 1 | yes |
|
||||||
| `dexEntry` | {heightFt, heightIn, heightM?, kind, text, text2?, weight, weightKg?} | no |
|
| `dexEntry` | {heightFt, heightIn, heightM?, kind, text, weight, weightKg?} | no |
|
||||||
| `evolutions` | list of {item?, level?, method, species} | yes |
|
| `evolutions` | list of {item?, level?, method, species} | yes |
|
||||||
| `frontSize` | integer 1..7 | yes |
|
| `frontSize` | integer 1..7 | yes |
|
||||||
| `growthRate` | growth_rates id | yes |
|
| `growthRate` | growth_rates id | yes |
|
||||||
|
|||||||
@@ -1,157 +0,0 @@
|
|||||||
# RFC 0019: `pokemon.level_visible` — a level a mode can take off the screen
|
|
||||||
|
|
||||||
## Status
|
|
||||||
|
|
||||||
Proposed.
|
|
||||||
|
|
||||||
## Motivation
|
|
||||||
|
|
||||||
A Pokémon's level is printed on four Gen 1 surfaces — both battle
|
|
||||||
healthboxes, the party rows, and page 1 and page 2 of the status screen —
|
|
||||||
and every one of them prints it unconditionally. There is no seam. A mode
|
|
||||||
that wants the number gone has exactly two options today, and both are bad.
|
|
||||||
|
|
||||||
It can paint over the text from `render.hud`, which means knowing four pixel
|
|
||||||
rectangles, matching the background shade, and surviving the palette flashes
|
|
||||||
and the healthbox slide-in. Or it can monkey-patch the drawing modules from
|
|
||||||
inside its sandbox, which works — `require` hands a mod the engine's own
|
|
||||||
table — and is precisely what `CONTRIBUTING-mods.md` tells mods not to do.
|
|
||||||
A mod that took the second road already deleted its patching layer once, for
|
|
||||||
the reasons that document gives.
|
|
||||||
|
|
||||||
The motivating case is a battle royale where every party is scaled to a
|
|
||||||
shared rung that rises with the fog. The number on the healthbox is
|
|
||||||
therefore never news — it is the same for everyone, it changes on a clock,
|
|
||||||
and a player reading `:L37` on an opponent learns nothing except that they
|
|
||||||
are playing the same match. Worse, it reads as a threat it is not: a Lv37
|
|
||||||
opponent looks dangerous to a player who has not worked out that their own
|
|
||||||
team is Lv37 too. The mode wants the level off the HUD and out of the party
|
|
||||||
list, and the announcement it replaces it with is one line about everyone
|
|
||||||
getting stronger at once.
|
|
||||||
|
|
||||||
Nothing about that is specific to a battle royale. A randomizer that hides
|
|
||||||
levels to keep an encounter unreadable, a challenge run that forbids
|
|
||||||
level-checking, a hard mode that withholds an opponent's level, and a
|
|
||||||
"blind" Nuzlocke all want the same switch, and none of them should have to
|
|
||||||
learn where the `<LV>` glyph lives.
|
|
||||||
|
|
||||||
## 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 with the presentation predicates already on the battle screen —
|
|
||||||
`battle.status_hud_visible`, `battle.bottom_ui_visible` and
|
|
||||||
`battle.caught_marker_visible` — and takes the same shape: a hook consulted
|
|
||||||
behind `Runtime.wantsHook`, defaulting to visible, where only an explicit
|
|
||||||
`false` suppresses. The difference is that a level is not a battle-only
|
|
||||||
readout, so this one is not named `battle.*` and carries the surface that
|
|
||||||
asked.
|
|
||||||
|
|
||||||
There is no in-repo D-number registry to amend.
|
|
||||||
|
|
||||||
## Exact API delta
|
|
||||||
|
|
||||||
### New hook: `pokemon.level_visible`
|
|
||||||
|
|
||||||
```lua
|
|
||||||
mod.hooks:wrap("pokemon.level_visible", function(next, mon, ctx)
|
|
||||||
-- ctx = { where = "battle.enemy" | "battle.player" | "party" | "summary",
|
|
||||||
-- game = <Game> }
|
|
||||||
if myMode.active and ctx.where ~= "party" then
|
|
||||||
return false -- the level is not printed on that surface
|
|
||||||
end
|
|
||||||
return next(mon, ctx) -- true: printed, as today
|
|
||||||
end)
|
|
||||||
```
|
|
||||||
|
|
||||||
Default `true`. Only an explicit `false` suppresses; `nil` and every other
|
|
||||||
value print, so a wrapper that forgets a branch cannot blank a screen by
|
|
||||||
accident.
|
|
||||||
|
|
||||||
`ctx.where` names the surface rather than the widget, because the number
|
|
||||||
means different things on different screens: on a battle HUD an opponent's
|
|
||||||
level is information about *them*, on the party and status screens your own
|
|
||||||
level is information about *you*. A mode that wants to hide the first and
|
|
||||||
keep the second can, and the motivating mode does exactly that.
|
|
||||||
|
|
||||||
### New module: `src/ui/LevelDisplay.lua`
|
|
||||||
|
|
||||||
One function, `LevelDisplay.visible(mon, where, game)`, wrapping the
|
|
||||||
`wantsHook`/`call` pair. It exists so the four call sites are a one-line
|
|
||||||
guard each instead of four copies of the same five lines, and so the hook
|
|
||||||
has one definition of "visible" rather than four that can drift.
|
|
||||||
|
|
||||||
### Call sites
|
|
||||||
|
|
||||||
| Surface | File | `where` |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| Enemy healthbox | `src/battle/BattleState.lua` | `battle.enemy` |
|
|
||||||
| Player healthbox | `src/battle/BattleState.lua` | `battle.player` |
|
|
||||||
| Party rows | `src/ui/PartyMenu.lua` | `party` |
|
|
||||||
| Status page 1 and 2 | `src/ui/SummaryMenu.lua` | `summary` |
|
|
||||||
|
|
||||||
No layout moves. Each site keeps its own hand-rolled PrintLevel rule
|
|
||||||
(`home/pokemon.asm:335-345` — the `<LV>` tile, then the digits, with a level
|
|
||||||
of 100 writing its third digit back over the tile); it just asks first.
|
|
||||||
|
|
||||||
Two details are deliberate:
|
|
||||||
|
|
||||||
- **A status condition still replaces the level on a healthbox**, exactly as
|
|
||||||
it does in the cart, so hiding the level never hides `PSN` or `BRN`. The
|
|
||||||
guard is an `elseif` on the existing status branch, not a wrapper around
|
|
||||||
it.
|
|
||||||
- **On status page 2 the `<to>` arrow is hidden with the level it points
|
|
||||||
at.** The arrow introduces the next level; on its own it is half a
|
|
||||||
sentence. The EXP-to-next-level figure beside it is a different field and
|
|
||||||
still prints.
|
|
||||||
|
|
||||||
### No other surface changes
|
|
||||||
|
|
||||||
No event, no registry, no save field, no manifest key.
|
|
||||||
|
|
||||||
## Migration
|
|
||||||
|
|
||||||
None. The hook is additive and defaults to current behaviour.
|
|
||||||
|
|
||||||
## Verification
|
|
||||||
|
|
||||||
- `tests/modkit/cases/pokemon_level_visible.lua` — the contract through the
|
|
||||||
public mod API: default true with no mod, `false` suppresses, the surface
|
|
||||||
and the mon reach the hook, falling through prints, and a `nil` mon
|
|
||||||
answers rather than throwing.
|
|
||||||
- `tests/engine/gate_hooks.lua` — the structural parity gate picks the hook
|
|
||||||
up automatically, because it walks the live catalog rather than a list.
|
|
||||||
- `tests/engine/gate_meta_coverage.lua` — the coverage ratchet; the seam is
|
|
||||||
covered by name from the change that introduces it, so it never enters the
|
|
||||||
DEBT ledger.
|
|
||||||
|
|
||||||
## Backward compatibility
|
|
||||||
|
|
||||||
A build with no mod wrapping the hook never reaches `Runtime.call`:
|
|
||||||
`wantsHook` is checked first, which matters because two of the four sites
|
|
||||||
are on a per-frame draw path. Pixels are unchanged, and the parity gates
|
|
||||||
assert it.
|
|
||||||
|
|
||||||
## Scope, and what is deliberately not in this change
|
|
||||||
|
|
||||||
**Gen 1 only.** The Gen 2 screens keep their own level readouts
|
|
||||||
(`src/ui/gen2/PartyMenu.lua`, `SummaryMenu.lua`, `BoxMenu.lua`,
|
|
||||||
`HallOfFame.lua`) and do not consult the hook. So does the Gen 1 PC box list
|
|
||||||
(`src/ui/BoxMenu.lua`), where the level is baked into a row label string
|
|
||||||
rather than drawn as a field, and the box-to-PNG print path beside it.
|
|
||||||
|
|
||||||
Those are mechanical follow-ups, held back so this change stays reviewable
|
|
||||||
against screens that can actually be exercised here. The limitation is
|
|
||||||
stated in `docs/modding.md` beside the hook, so a mod author reads it before
|
|
||||||
depending on it rather than after.
|
|
||||||
|
|
||||||
## Compatibility seam for older engines
|
|
||||||
|
|
||||||
There is none, and none is possible without patching: the call sites are
|
|
||||||
mid-draw, so a mod on a stock engine cannot reach them. That is the argument
|
|
||||||
for the seam rather than a gap around it — the alternatives available to a
|
|
||||||
mod today are painting over the engine's own pixels or reaching into its
|
|
||||||
render modules, and the second is the thing the mod contract exists to
|
|
||||||
prevent.
|
|
||||||
+3
-42
@@ -214,51 +214,12 @@ 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. `ShaderFX.canConvert()` reports whether the library resolved on this machine,
|
without a path. The desktop path is still a developer build sitting in cargo's
|
||||||
|
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:
|
**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
|
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
|
main image and are reachable only through `ffi.C`. The candidate list is empty
|
||||||
|
|||||||
+2
-3
@@ -63,9 +63,8 @@ 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`,
|
(`gen1recomp-X.Y.Z-macos.zip`, `-windows.zip`, `-linux.zip`,
|
||||||
`-linux-x86_64.AppImage`, `-linux-arm64.AppImage`, `-linux.flatpak`,
|
`-linux-arm64.AppImage`, `-android.apk`, `-ios.ipa`, `-switch.zip`, Xbox and
|
||||||
`-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
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
[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
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<?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>MIT</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://gen1re.com/</url>
|
|
||||||
<url type="vcs-browser">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>
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
# 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,56 +22,6 @@ 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
|
||||||
@@ -80,33 +30,10 @@ 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
|
||||||
@@ -632,7 +559,6 @@ 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.
|
||||||
@@ -1303,8 +1229,6 @@ function love.run()
|
|||||||
if love.timer then dt = love.timer.step() end
|
if love.timer then dt = love.timer.step() end
|
||||||
idleFor = idleFor + dt
|
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
|
||||||
|
|
||||||
|
|||||||
@@ -12,139 +12,6 @@
|
|||||||
"tintColor": "3b5ca8",
|
"tintColor": "3b5ca8",
|
||||||
"category": "games",
|
"category": "games",
|
||||||
"versions": [
|
"versions": [
|
||||||
{
|
|
||||||
"version": "0.2.30",
|
|
||||||
"date": "2026-08-27",
|
|
||||||
"size": 21375253,
|
|
||||||
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.2.30/gen1recomp++-0.2.30-ios.ipa",
|
|
||||||
"localizedDescription": "Download the correct version for your computer below.\n\n## Issues closed\n\n- #1562 Link battles show no withdraw message on switch\n- #1804 closed poke ball is not shown sometimes after catching\n- #1805 Missing jingle + closing menu when using ability enhancers\n- #1810 Wrong amount of hidden coins in the game corner\n- #1811 Slot machine issues\n- #1814 Rocket has no vision\n- #1817 Giovanni dialogue in the hideout\n- #1818 Sound effect missing when teaching a move\n- #1819 Option box in Pokemon menu not connected to bottom of the screen\n- #1821 Cursor position still on SAVE after loading\n- #1826 EXP calculation after opposing trainer switches out its PKMN\n- #1827 Behavior of sprite replacement mods on Crystal\n- #1829 Pokedex could use further clean up\n- #1831 Spinning too fast\n- #1833 Cancel option missing when selecting Pokemon in the overworld\n- #1834 Possible bug with Unown in Gold, Silver, Crystal\n- #1836 Crystal Kanto Flight Bugged\n- #1840 Two small issues with flying\n- #1842 Pokemon Tower rival bugs\n- #1845 Pokemon Trainer Item PC wrong Layout\n- #1846 No jingle when leveling up with rare candy\n- #1847 Someones / Bill PC Wrong Layout\n- #1848 Nightshade animation too slow\n- #1849 Pokemon Tower Marowak bugs\n- #1853 Try Catching Pokemon have wierd White nohing\n- #1858 (GSC) Broken colors on stats screen since 0.2.27\n- #1860 (GSC) Jingles are cut off if you advance the dialogue while they play\n- #1862 (GSC) Whirlpool disappearing before the SFX ends\n\n## Contributors\n\n- @bryanthaboi\n- @colsonrice\n- @thibautbus"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "0.2.29",
|
|
||||||
"date": "2026-08-26",
|
|
||||||
"size": 21366635,
|
|
||||||
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.2.29/gen1recomp++-0.2.29-ios.ipa",
|
|
||||||
"localizedDescription": "Download the correct version for your computer below.\n\n## Contributors\n\n- @bryanthaboi\n- @ShaneMcGovernIE"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "0.2.28",
|
|
||||||
"date": "2026-08-26",
|
|
||||||
"size": 21366087,
|
|
||||||
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.2.28/gen1recomp++-0.2.28-ios.ipa",
|
|
||||||
"localizedDescription": "Download the correct version for your computer below.\n\n## Contributors\n\n- @bryanthaboi"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"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",
|
|
||||||
"date": "2026-08-24",
|
|
||||||
"size": 14060437,
|
|
||||||
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.2.23/gen1recomp++-0.2.23-ios.ipa",
|
|
||||||
"localizedDescription": "Download the correct version for your computer below.\n\n## Contributors\n\n- @bryanthaboi"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "0.2.22",
|
|
||||||
"date": "2026-08-24",
|
|
||||||
"size": 14060365,
|
|
||||||
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.2.22/gen1recomp++-0.2.22-ios.ipa",
|
|
||||||
"localizedDescription": "Download the correct version for your computer below.\n\n## Contributors\n\n- @bryanthaboi\n- @ShaneMcGovernIE\n- @thibautbus"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "0.2.21",
|
|
||||||
"date": "2026-08-24",
|
|
||||||
"size": 14058979,
|
|
||||||
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.2.21/gen1recomp++-0.2.21-ios.ipa",
|
|
||||||
"localizedDescription": "Download the correct version for your computer below.\n\n## Issues closed\n\n- #1483 (Pokémon Gold) Missing HM/TM fanfare when receiving them\n- #1610 Wrong text when choosing Pokemon to trade\n- #1615 Fishing Guru dialogue issue\n- #1640 Sync doesnt work on XBOX build\n- #1646 Cashier dialogue + sound effects\n- #1649 Pokemon cries missing in Vermilion City\n- #1651 Ticket taker asks for ticket too much\n- #1653 Wrap animation is too fast and has no screen shake\n- #1656 Map visible in between end of fight & evolution\n- #1683 Music not changing when talking to trainer\n- #1685 Cancel option missing in item menu\n- #1686 Issues with teaching TMs & HMs\n- #1687 Pokemon cries missing on S.S. Anne\n- #1688 S.S. Anne rival dialogue\n- #1689 S.S. Anne departing cutscene broken\n- #1690 Wrong dialogue when taking back entrance to museum\n- #1693 (Gold) Issues not properly fixed\n- #1694 (Gold) Arrow when seeing items in the bag should be red, not black\n- #1695 (Gold) Wrong layout in TMs/HMs pocket\n- #1696 (Gold) Caught pokémon don't get healed when automatically sent to the PC\n- #1697 Standardize identification of mod-created options/settings screens\n- #1702 Lt. Surge's door opening isn't synced with the sound effect\n- #1704 Ledge jumping on the bike doesn't slow you down\n- #1705 The bike shouldn't have Use/Toss options\n- #1706 (Gold) Cursor wraps around the edge in the battle interface\n- #1707 (Gold) Pokémon sometimes wake up in the same turn they were put to sleep\n- #1708 (Gold) Missing rod when fishing\n- #1710 (Gold) You shouldn't be able to change boxes by pressing LEFT or RIGHT\n- #1711 (Gold) Missing animation for FLY in the overworld\n- #1712 (Gold) Wrong cursor in the map when using FLY\n- #1713 (Gold) Low framerate when jumping off ledges\n- #1716 (Gold) Player can go through a whirlpool even without having the HM\n- #1717 (Gold) Missing animation and SFX when using whirlpool in the overworld\n- #1718 (Gold) Can't pause in the Cycling Road while moving down\n- #1719 (Gold) Pokémon given by NPCs not being registered in the pokédex\n- #1720 Critical hit on multi-hit moves repeats the message\n- #1721 Rival not visible while talking if you lose first fight\n- #1724 Centralize ROM cache readiness and publication in an engine-owned contract\n- #1725 (Gold) Text not spaced nor centered correctly for secondary actions in the bag\n- #1732 Cursor doesn't stay on last used item\n- #1745 Viewing items makes ingame menu disappear\n- #1748 (Gold) Overworld pokémon sprite are stactic\n- #1749 (Gold) You shouldn't be able to surf on Cycling Road\n- #1751 (Gold) Synthesis and Morning Sun always recover 50% of max HP\n- #1754 Leaving building with bike speed\n- #1756 No cursor or gamepad controls in skin menu, locked out on Xbox\n\n## Contributors\n\n- @1Jamie\n- @BoLayerDev\n- @bryanthaboi\n- @campavao\n- @caorthann-celt\n- @dlloa\n- @syybott\n- @thibautbus\n- MaxTomahawk"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "0.2.20",
|
|
||||||
"date": "2026-08-22",
|
|
||||||
"size": 13812226,
|
|
||||||
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.2.20/gen1recomp++-0.2.20-ios.ipa",
|
|
||||||
"localizedDescription": "Download the correct version for your computer below.\n\n## Issues closed\n\n- #1536 Magnet Train Cutscene visual bugs\n- #1597 Link cable looks broken\n- #1643 Pikachu + Title uses an incorrect Colour Palette\n- #1662 The game glitches after exiting, returning to the menu, and choosing another one.\n\n## Contributors\n\n- @1Jamie\n- @bryanthaboi\n- @colsonrice"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "0.2.19",
|
|
||||||
"date": "2026-08-22",
|
|
||||||
"size": 13778435,
|
|
||||||
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.2.19/gen1recomp++-0.2.19-ios.ipa",
|
|
||||||
"localizedDescription": "Download the correct version for your computer below.\n\n## Issues closed\n\n- #1274 Choose ROM imports a different version than the one selected when several dumps are pending (Linux, no file picker)\n- #1484 (Pokémon Gold) Bill's PC doesn't show equipped items + missing orange filter on pokémon sprite\n- #1486 (Pokémon Gold) Can't deposit key items\n- #1513 Experience share issue different case from #1464\n- #1517 Game Crash Opening Stats\n- #1548 Colisión\n- #1556 (Gold) Missing SFX for a lot of actions\n- #1564 Bide's storing turn is missing the screen shake\n- #1567 TM/HM case not in order\n- #1663 transition pops the top state, destroying anything pushed from a midpoint callback\n- #1664 gold bide spends pp every turn instead of only the turn it is selected\n- #1665 gold has no bide lock-in, so a mid-bide switch of move strands the counter\n- #1666 sleep talk is inert in gold: EFFECT_SLEEP_TALK has no move effect entry\n- #1667 gold: a fainted party mon keeps its exp participant credit and halves the survivor's exp\n- #1668 summary page 2 formats a move's pp with no numeric guard\n- #1670 gold tm/hm pocket capacity is an invented 64 where the cart has 57 slots\n- #1672 bill's pc move screen is missing the box name arrows\n\n## Contributors\n\n- @AverageConsumer\n- @bryanthaboi\n- @colsonrice\n- @jramiresbrito"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "0.2.18",
|
|
||||||
"date": "2026-08-21",
|
|
||||||
"size": 13772120,
|
|
||||||
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.2.18/gen1recomp++-0.2.18-ios.ipa",
|
|
||||||
"localizedDescription": "Download the correct version for your computer below.\n\n## Contributors\n\n- @bryanthaboi"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "0.2.17",
|
|
||||||
"date": "2026-08-21",
|
|
||||||
"size": 13771903,
|
|
||||||
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.2.17/gen1recomp++-0.2.17-ios.ipa",
|
|
||||||
"localizedDescription": "Download the correct version for your computer below.\n\n## Contributors\n\n- @bryanthaboi"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "0.2.16",
|
|
||||||
"date": "2026-08-21",
|
|
||||||
"size": 13769300,
|
|
||||||
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.2.16/gen1recomp++-0.2.16-ios.ipa",
|
|
||||||
"localizedDescription": "Download the correct version for your computer below.\n\n## Issues closed\n\n- #1367 Android auto scrolling down bug\n- #1570 Pokemon Gold isn't noticing my controller inputs on...ANY controller I own, despite all of them working in the other games, and in the main launcher\n- #1585 Power-saving mode or dark mode closes the game on Android.\n- #1611 bug: keyboard gets stuck IOS\n- #1612 Fold 7 infinite scrolling mouse.\n- #1636 Game freezes when trainer battle starts\n- #1638 Game screen orientation on smartphone\n- #1641 [Gold] [Android] PKMN Evolutions sequence freezes the Game \n\n## Contributors\n\n- @1Jamie\n- @bryanthaboi\n- @HighDrexler\n- MaxTomahawk"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "0.2.15",
|
|
||||||
"date": "2026-08-21",
|
|
||||||
"size": 13753007,
|
|
||||||
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.2.15/gen1recomp++-0.2.15-ios.ipa",
|
|
||||||
"localizedDescription": "Download the correct version for your computer below.\n\n## Issues closed\n\n- #1589 Player sprite still blinking on the Town Map\n- #1595 Pokedex completion certificate is not displaying correctly\n- #1597 Link cable looks broken\n- #1613 Visual differences in Pokemon trades\n- #1619 Save Menu cutting off and Put into another place\n\n## Contributors\n\n- @1Jamie\n- @bryanthaboi"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "0.2.14",
|
|
||||||
"date": "2026-08-20",
|
|
||||||
"size": 13749372,
|
|
||||||
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.2.14/gen1recomp++-0.2.14-ios.ipa",
|
|
||||||
"localizedDescription": "Download the correct version for your computer below.\n\n## Contributors\n\n- @bryanthaboi"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "0.2.13",
|
|
||||||
"date": "2026-08-20",
|
|
||||||
"size": 13749377,
|
|
||||||
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.2.13/gen1recomp++-0.2.13-ios.ipa",
|
|
||||||
"localizedDescription": "Download the correct version for your computer below.\n\n## Issues closed\n\n- #1474 (Pokémon Gold) Pokédex mode not being saved\n- #1475 (Pokémon Gold) Vital Throw attacking before the opponent\n- #1477 (Pokémon Gold) Map when using fly keeps the arrow from the pokégear\n- #1478 (Pokémon Gold) Wrong move layout in battles\n- #1479 [Gold] tiles broken at Route 28 and Mt Silver\n- #1482 (Pokémon Gold) Nothing happens when trying to use Coin Case\n- #1488 Pokemon EXP calculation after trading went back to issue #984\n- #1510 Title Screen Transition is Missing\n- #1511 Broken Trainer Rival Name Layout\n- #1512 Gen 2 Post battle interactions don't exist\n- #1514 Health damage timing\n- #1516 Move learning timing\n- #1521 Different Item Menu\n- #1522 Wrong Save Game Layout\n- #1545 Pikachu not sliding out when switching Pokemon.\n- #1557 (Gold) Impossible to get TM 03 in Celadon Mansion at night\n- #1558 (Gold) Missing colors and symbol in stats screen\n- #1563 Player mon pic cuts away instead of shrinking when recalled\n- #1565 Thrash doesn't lock in when the first use misses\n- #1566 Center PC missing the PKMN LEAGUE entry after the Hall of Fame\n- #1569 Goldenrod Gift Spearow Bugged\n- #1577 Thrash has no animation past the first turn\n- #1578 Hitting yourself in confusion animation missing\n- #1579 Missing dialogue for Cerulean City Rocket\n- #1594 Pokemon menu closing too soon when using rare candy\n- #1596 Evolution dialogue & missing jingle\n- #1606 Misty dialogue issue\n- #1608 \"There's no will to fight!\" message issues\n\n## Contributors\n\n- @bryanthaboi"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "0.2.12",
|
|
||||||
"date": "2026-08-20",
|
|
||||||
"size": 13737259,
|
|
||||||
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.2.12/gen1recomp++-0.2.12-ios.ipa",
|
|
||||||
"localizedDescription": "Download the correct version for your computer below.\n\n## Issues closed\n\n- #1582 Sync not working between steamdeck and windows\n- #1583 Can’t sync between iOS and windows\n\n## Contributors\n\n- @AverageConsumer\n- @bryanthaboi\n- @thibautbus"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"version": "0.2.11",
|
"version": "0.2.11",
|
||||||
"date": "2026-08-20",
|
"date": "2026-08-20",
|
||||||
|
|||||||
+22
-68
@@ -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-x86_64.AppImage (fused x86_64 AppImage)
|
# dist/linux/gen1recomp-linux.zip (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,53 +170,27 @@ 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_<PLAT> or dist/native/<plat>/ points at a prebuilt library;
|
# SHADERFX_BRIDGE points at a prebuilt library; otherwise cargo builds it.
|
||||||
# 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" plat="$3"
|
local dest="$1" name="$2"
|
||||||
|
local src="${SHADERFX_BRIDGE:-}"
|
||||||
local crate="$ROOT/tools/shaderfx-bridge"
|
local crate="$ROOT/tools/shaderfx-bridge"
|
||||||
local src="" var
|
if [ -z "$src" ] && [ -f "$crate/target/release/$name" ]; then
|
||||||
[ -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
|
|
||||||
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"
|
src="$crate/target/release/$name"
|
||||||
elif command -v cargo >/dev/null 2>&1; then
|
fi
|
||||||
|
if [ -z "$src" ] && 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 ($plat)"
|
say "bundled $name for SHADER FX preset conversion"
|
||||||
return 0
|
else
|
||||||
|
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() {
|
||||||
@@ -231,7 +205,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" mac
|
bundle_shader_bridge "$out_app/Contents/MacOS" "liblibrashader_bridge.dylib"
|
||||||
|
|
||||||
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 \
|
||||||
@@ -348,7 +322,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" win-x64
|
bundle_shader_bridge "$out_dir" "librashader_bridge.dll"
|
||||||
|
|
||||||
# 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
|
||||||
@@ -445,7 +419,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" linux-x64
|
bundle_shader_bridge "$appdir" "liblibrashader_bridge.so"
|
||||||
|
|
||||||
# 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
|
||||||
@@ -483,31 +457,15 @@ 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?)"
|
||||||
|
|
||||||
local wayland_hook='if [ -n "$WAYLAND_DISPLAY" ] && [ -z "$SDL_VIDEODRIVER" ]; then export SDL_VIDEODRIVER=x11; fi\
|
sed -i '' 's|^exec "\$APPDIR/bin/love"|if [ -n "$WAYLAND_DISPLAY" ] \&\& [ -z "$SDL_VIDEODRIVER" ]; then export SDL_VIDEODRIVER=x11; fi\
|
||||||
exec "$APPDIR/bin/love"'
|
exec "$APPDIR/bin/love"|' "$appdir/AppRun"
|
||||||
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.
|
||||||
@@ -516,20 +474,16 @@ exec "$APPDIR/bin/love"'
|
|||||||
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
|
||||||
|
|
||||||
mkdir -p "$DIST/linux"
|
local out_bin="$WORK/$APP_NAME-x86_64.AppImage"
|
||||||
local out_bin="$DIST/linux/$APP_NAME-linux-x86_64.AppImage"
|
rm -f "$out_bin"
|
||||||
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
|
|
||||||
printf '%s %s\n' "$(sha256sum "$out_bin" | awk '{print $1}')" "$(basename "$out_bin")" \
|
local zip_out="$DIST/linux/$APP_NAME-linux.zip"
|
||||||
> "$out_bin.sha256"
|
rm -f "$zip_out"
|
||||||
else
|
(cd "$WORK" && zip -q -9 -j "$zip_out" "$(basename "$out_bin")")
|
||||||
printf '%s %s\n' "$(shasum -a 256 "$out_bin" | awk '{print $1}')" "$(basename "$out_bin")" \
|
say "Linux build: $zip_out"
|
||||||
> "$out_bin.sha256"
|
|
||||||
fi
|
|
||||||
say "Linux build: $out_bin"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# --------------------------------------------------------------- Android
|
# --------------------------------------------------------------- Android
|
||||||
|
|||||||
@@ -1,143 +0,0 @@
|
|||||||
#!/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")"
|
|
||||||
@@ -100,21 +100,6 @@ 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,14 +327,6 @@ 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
|
||||||
@@ -347,7 +339,7 @@ fi
|
|||||||
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* ${SHADER_BRIDGE[@]+"${SHADER_BRIDGE[@]}"}; do
|
for object in "$APPDIR/bin/love" "$APPDIR"/lib/*.so*; 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,23 +20,6 @@ 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.
|
||||||
#
|
#
|
||||||
@@ -45,14 +28,14 @@ echo "scanning ${#scan[@]} ELF objects: ${scan[*]#squashfs-root/}"
|
|||||||
# 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 "${scan[@]}" 2>/dev/null \
|
ldd squashfs-root/bin/love squashfs-root/lib/*.so* 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 "${scan[@]}"; do
|
linked="$(for f in squashfs-root/bin/love squashfs-root/lib/*.so*; 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" ] \
|
||||||
@@ -62,7 +45,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 "${scan[@]}" 2>/dev/null \
|
floor="$(objdump -T squashfs-root/bin/love squashfs-root/lib/*.so* 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" ] \
|
||||||
@@ -71,12 +54,4 @@ 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"
|
||||||
|
|||||||
@@ -118,7 +118,6 @@ 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
|
||||||
|
|||||||
@@ -104,9 +104,7 @@ local SE_FRAMES = {
|
|||||||
SE_FLASH_MON_PIC = 4, SE_FLASH_ENEMY_MON_PIC = 4,
|
SE_FLASH_MON_PIC = 4, SE_FLASH_ENEMY_MON_PIC = 4,
|
||||||
SE_TRANSFORM_MON = 4,
|
SE_TRANSFORM_MON = 4,
|
||||||
SE_SUBSTITUTE_MON = 3,
|
SE_SUBSTITUTE_MON = 3,
|
||||||
-- AnimationWavyScreen: `ld c, $ff` counts outer passes, and the inner
|
SE_WAVY_SCREEN = 255, -- AnimationWavyScreen: ld c, $ff frames
|
||||||
-- loop exits twice per displayed frame (animations.asm:1884-1903)
|
|
||||||
SE_WAVY_SCREEN = 128,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
-- data/battle_anims/special_effects.asm AnimationIdSpecialEffects:
|
-- data/battle_anims/special_effects.asm AnimationIdSpecialEffects:
|
||||||
@@ -504,10 +502,15 @@ function AnimPlayer:start(moveId, attackerIsPlayer, opts)
|
|||||||
local transform = resolveTransform(sub.type, attackerIsPlayer)
|
local transform = resolveTransform(sub.type, attackerIsPlayer)
|
||||||
local first, last, dir = 1, #sub.blocks, 1
|
local first, last, dir = 1, #sub.blocks, 1
|
||||||
if transform == "REVERSE" then first, last, dir = last, first, -1 end
|
if transform == "REVERSE" then first, last, dir = last, first, -1 end
|
||||||
-- DoBallShakeSpecialEffects: animations.asm:739-747, :623-627
|
-- DoBallShakeSpecialEffects: each ball shake opens with SFX_TINK
|
||||||
local pendingTink = false
|
-- and a 40-frame pause, then rewinds the same subanimation; the
|
||||||
|
-- mode-4 frame blocks persist, so the resting ball stays visible
|
||||||
|
-- through the pauses between wobbles
|
||||||
for _ = 1, (opts and opts.shakes) or 1 do
|
for _ = 1, (opts and opts.shakes) or 1 do
|
||||||
pendingTink = opts and opts.shakes ~= nil
|
if opts and opts.shakes then
|
||||||
|
events[#events + 1] = { effect = "SFX_TINK", frame = frame }
|
||||||
|
emit(40)
|
||||||
|
end
|
||||||
local dest = 1 -- PlaySubanimation resets the OAM cursor per row
|
local dest = 1 -- PlaySubanimation resets the OAM cursor per row
|
||||||
local nblocks = math.abs(last - first) + 1
|
local nblocks = math.abs(last - first) + 1
|
||||||
local played = 0
|
local played = 0
|
||||||
@@ -565,11 +568,6 @@ function AnimPlayer:start(moveId, attackerIsPlayer, opts)
|
|||||||
-- DoSpecialEffectByAnimationId runs after every frame
|
-- DoSpecialEffectByAnimationId runs after every frame
|
||||||
-- block with wSubAnimCounter = blocks remaining
|
-- block with wSubAnimCounter = blocks remaining
|
||||||
played = played + 1
|
played = played + 1
|
||||||
if pendingTink then
|
|
||||||
pendingTink = false
|
|
||||||
events[#events + 1] = { effect = "SFX_TINK", frame = frame }
|
|
||||||
emit(40)
|
|
||||||
end
|
|
||||||
if ballFlicker then obp0Flip = not obp0Flip end
|
if ballFlicker then obp0Flip = not obp0Flip end
|
||||||
if idFx then
|
if idFx then
|
||||||
local counter = nblocks - played + 1
|
local counter = nblocks - played + 1
|
||||||
|
|||||||
+33
-100
@@ -16,7 +16,6 @@ local Damage = require("src.battle.Damage")
|
|||||||
local EffectRegistry = require("src.battle.EffectRegistry")
|
local EffectRegistry = require("src.battle.EffectRegistry")
|
||||||
local Experience = require("src.battle.Experience")
|
local Experience = require("src.battle.Experience")
|
||||||
local Font = require("src.render.Font")
|
local Font = require("src.render.Font")
|
||||||
local LevelDisplay = require("src.ui.LevelDisplay")
|
|
||||||
local Logger = require("src.core.Logger")
|
local Logger = require("src.core.Logger")
|
||||||
local MoveEffects = require("src.battle.MoveEffects")
|
local MoveEffects = require("src.battle.MoveEffects")
|
||||||
local Party = require("src.pokemon.Party")
|
local Party = require("src.pokemon.Party")
|
||||||
@@ -35,12 +34,6 @@ 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 = {}
|
||||||
@@ -341,22 +334,6 @@ local function monPalette(data, species)
|
|||||||
return { name = name, colors = colors }
|
return { name = name, colors = colors }
|
||||||
end
|
end
|
||||||
|
|
||||||
-- MarowakAnim OBJ pics under OAM_PAL1:
|
|
||||||
-- engine/battle/ghost_marowak_anim.asm:3-5,77
|
|
||||||
local function objPicPalette()
|
|
||||||
local PaletteFX = require("src.render.PaletteFX")
|
|
||||||
if not PaletteFX.usesSpriteObp() then return nil end
|
|
||||||
local colors, group = PaletteFX.ogObj()
|
|
||||||
if not colors then return nil end
|
|
||||||
return { name = "obp1:" .. tostring(group), colors = colors }
|
|
||||||
end
|
|
||||||
|
|
||||||
local function objPic(path, trueColor)
|
|
||||||
local pal = objPicPalette()
|
|
||||||
if not pal then return nil end
|
|
||||||
return getImage(path, pal, trueColor)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- a named palette from the active COLORS pack as a getImage pal
|
-- a named palette from the active COLORS pack as a getImage pal
|
||||||
local function namedPalette(data, name)
|
local function namedPalette(data, name)
|
||||||
local PaletteFX = require("src.render.PaletteFX")
|
local PaletteFX = require("src.render.PaletteFX")
|
||||||
@@ -484,7 +461,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.forcesRawGrays()
|
or PaletteFX.mode == "classic"
|
||||||
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
|
||||||
@@ -914,8 +891,7 @@ local function disguiseAsGhost(self)
|
|||||||
self.enemy.name = "GHOST"
|
self.enemy.name = "GHOST"
|
||||||
self.enemy.sprite = getImage("assets/generated/battle/front/ghost.png",
|
self.enemy.sprite = getImage("assets/generated/battle/front/ghost.png",
|
||||||
monPalette(self.data, self.enemy.mon.species))
|
monPalette(self.data, self.enemy.mon.species))
|
||||||
-- _EnemyAppearedText (data/text/text_2.asm:1251-1255) has no article
|
self.introText = Strings("The GHOST\nappeared!")
|
||||||
self.introText = self:romText("_EnemyAppearedText", "%s\nappeared!", self.enemy.name)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Pokémon Tower ghosts (engine/battle/core.asm): without the Silph Scope
|
-- Pokémon Tower ghosts (engine/battle/core.asm): without the Silph Scope
|
||||||
@@ -959,11 +935,7 @@ function BattleState:queueScopeReveal()
|
|||||||
local unveiled = self.data.text and self.data.text._UnveiledGhostText
|
local unveiled = self.data.text and self.data.text._UnveiledGhostText
|
||||||
self:say(unveiled
|
self:say(unveiled
|
||||||
or Strings("SILPH SCOPE\nunveiled the\vGHOST's identity!"))
|
or Strings("SILPH SCOPE\nunveiled the\vGHOST's identity!"))
|
||||||
self:act(function()
|
self:act(function() self.ghostReveal = { t = 0 } end)
|
||||||
self.ghostReveal = { t = 0 }
|
|
||||||
local ghostObj = objPic("assets/generated/battle/front/ghost.png")
|
|
||||||
if ghostObj then self.enemy.sprite = ghostObj end
|
|
||||||
end)
|
|
||||||
table.insert(self.queue, { wait = BattleState.GHOST_REVEAL_FRAMES })
|
table.insert(self.queue, { wait = BattleState.GHOST_REVEAL_FRAMES })
|
||||||
self:say(self:romText("_WildMonAppearedText", "Wild %s\nappeared!",
|
self:say(self:romText("_WildMonAppearedText", "Wild %s\nappeared!",
|
||||||
self.ghostReal and self.ghostReal.name or self.enemy.name))
|
self.ghostReal and self.ghostReal.name or self.enemy.name))
|
||||||
@@ -3286,9 +3258,9 @@ function BattleState:applyAnimEffect(ev)
|
|||||||
end
|
end
|
||||||
fx.hudShakeProg = prog
|
fx.hudShakeProg = prog
|
||||||
elseif e == "SE_WAVY_SCREEN" then
|
elseif e == "SE_WAVY_SCREEN" then
|
||||||
-- AnimationWavyScreen: 255 outer passes, two per displayed frame
|
-- AnimationWavyScreen: 255 frames of per-scanline SCX offsets
|
||||||
-- (animations.asm:1884-1903), walking WavyScreenLineOffsets
|
-- walking WavyScreenLineOffsets
|
||||||
fx.wavy = { left = 128, phase = 0 }
|
fx.wavy = { left = 255, phase = 0 }
|
||||||
|
|
||||||
-- ---------------------------------------------- mon pic effects
|
-- ---------------------------------------------- mon pic effects
|
||||||
elseif e == "SE_SLIDE_MON_OFF" then
|
elseif e == "SE_SLIDE_MON_OFF" then
|
||||||
@@ -3672,7 +3644,7 @@ function BattleState:updateFx()
|
|||||||
end
|
end
|
||||||
if fx.wavy then
|
if fx.wavy then
|
||||||
fx.wavy.left = fx.wavy.left - 1
|
fx.wavy.left = fx.wavy.left - 1
|
||||||
fx.wavy.phase = fx.wavy.phase + 2
|
fx.wavy.phase = fx.wavy.phase + 1
|
||||||
if fx.wavy.left <= 0 then fx.wavy = nil end
|
if fx.wavy.left <= 0 then fx.wavy = nil end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -3726,22 +3698,12 @@ function BattleState:updateFx()
|
|||||||
local real = self.ghostReal
|
local real = self.ghostReal
|
||||||
if real then
|
if real then
|
||||||
self.enemy.name = real.name or self.enemy.name
|
self.enemy.name = real.name or self.enemy.name
|
||||||
gr.bgSprite = real.sprite or self.enemy.sprite
|
self.enemy.sprite = real.sprite or self.enemy.sprite
|
||||||
local objReal
|
|
||||||
if objPicPalette() then
|
|
||||||
local Sprites = require("src.pokemon.Sprites")
|
|
||||||
local path, tc = Sprites.path(self.data, self.enemy.mon.species,
|
|
||||||
"front", { mon = self.enemy.mon, kind = "battle" })
|
|
||||||
objReal = path and objPic(path, tc)
|
|
||||||
end
|
|
||||||
self.enemy.sprite = objReal or gr.bgSprite
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
pf.fade = math.min(1, math.ceil((gr.t - outEnd) / 10) / 4)
|
pf.fade = math.min(1, math.ceil((gr.t - outEnd) / 10) / 4)
|
||||||
end
|
end
|
||||||
if gr.t >= BattleState.GHOST_REVEAL_FRAMES then
|
if gr.t >= BattleState.GHOST_REVEAL_FRAMES then
|
||||||
-- home/clear_sprites.asm:1
|
|
||||||
if gr.bgSprite then self.enemy.sprite = gr.bgSprite end
|
|
||||||
self.ghostReveal, self.scopeReveal, pf.fade = nil, nil, nil
|
self.ghostReveal, self.scopeReveal, pf.fade = nil, nil, nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -3857,9 +3819,6 @@ function BattleState:executeAction(user, target, action)
|
|||||||
-- EnemySendOutFirstMon (core.asm:1314-1315): clears player's trap
|
-- EnemySendOutFirstMon (core.asm:1314-1315): clears player's trap
|
||||||
clearTrapping(self.player)
|
clearTrapping(self.player)
|
||||||
self:syncSides()
|
self:syncSides()
|
||||||
-- EnemySendOut (core.asm:1276-1289): only the mon on the field stays flagged
|
|
||||||
self.participants = {}
|
|
||||||
self:markParticipant()
|
|
||||||
Runtime.emit("battle.battler_switched", {
|
Runtime.emit("battle.battler_switched", {
|
||||||
battle = self, side = self.sides[2], battler = self.enemy,
|
battle = self, side = self.sides[2], battler = self.enemy,
|
||||||
previous = previous,
|
previous = previous,
|
||||||
@@ -5985,7 +5944,8 @@ local WAVY_OFFSETS = { 0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 2, 2, 1, 1, 1,
|
|||||||
0, 0, 0, 0, 0, -1, -1, -1, -2, -2, -2, -2, -2,
|
0, 0, 0, 0, 0, -1, -1, -1, -2, -2, -2, -2, -2,
|
||||||
-1, -1, -1 }
|
-1, -1, -1 }
|
||||||
|
|
||||||
-- WavyScreen_SetSCX: animations.asm:1916-1927
|
-- wave the BG canvas one scanline at a time; the offset table walks
|
||||||
|
-- one entry per frame like the asm's advancing pointer
|
||||||
function BattleState:applyWavy(src)
|
function BattleState:applyWavy(src)
|
||||||
local wavy = self.fx and self.fx.wavy
|
local wavy = self.fx and self.fx.wavy
|
||||||
if not wavy then return src end
|
if not wavy then return src end
|
||||||
@@ -5998,7 +5958,7 @@ function BattleState:applyWavy(src)
|
|||||||
for line = 0, 143 do
|
for line = 0, 143 do
|
||||||
self.waveQuad:setViewport(0, line, 160, 1)
|
self.waveQuad:setViewport(0, line, 160, 1)
|
||||||
g.draw(src, self.waveQuad,
|
g.draw(src, self.waveQuad,
|
||||||
WAVY_OFFSETS[(line * 2 + wavy.phase) % 32 + 1], line)
|
WAVY_OFFSETS[(line + wavy.phase) % 32 + 1], line)
|
||||||
end
|
end
|
||||||
g.setCanvas(prev)
|
g.setCanvas(prev)
|
||||||
return self.waveCanvas
|
return self.waveCanvas
|
||||||
@@ -6029,7 +5989,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.forcesRawGrays()
|
or PaletteFX.mode == "classic"
|
||||||
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
|
||||||
@@ -6338,7 +6298,7 @@ function BattleState:drawHUDs(slide)
|
|||||||
end
|
end
|
||||||
if self.enemy.shownStatus then
|
if self.enemy.shownStatus then
|
||||||
Font.draw(self:statusLabel({ status = self.enemy.shownStatus }), 40, 8)
|
Font.draw(self:statusLabel({ status = self.enemy.shownStatus }), 40, 8)
|
||||||
elseif LevelDisplay.visible(self.enemy.mon, "battle.enemy", self.game) then
|
else
|
||||||
hudTile(0x6E, 32, 8) -- <LV>
|
hudTile(0x6E, 32, 8) -- <LV>
|
||||||
Font.draw(tostring(self.enemy.mon.level), 40, 8)
|
Font.draw(tostring(self.enemy.mon.level), 40, 8)
|
||||||
end
|
end
|
||||||
@@ -6422,7 +6382,7 @@ function BattleState:drawHUDs(slide)
|
|||||||
Font.draw(self.player.name, nameX(10, self.player.name), 56)
|
Font.draw(self.player.name, nameX(10, self.player.name), 56)
|
||||||
if self.player.shownStatus then
|
if self.player.shownStatus then
|
||||||
Font.draw(self:statusLabel({ status = self.player.shownStatus }), 120, 64)
|
Font.draw(self:statusLabel({ status = self.player.shownStatus }), 120, 64)
|
||||||
elseif LevelDisplay.visible(self.player.mon, "battle.player", self.game) then
|
else
|
||||||
hudTile(0x6E, 112, 64) -- <LV>
|
hudTile(0x6E, 112, 64) -- <LV>
|
||||||
Font.draw(tostring(self.player.mon.level), 120, 64)
|
Font.draw(tostring(self.player.mon.level), 120, 64)
|
||||||
end
|
end
|
||||||
@@ -6439,35 +6399,8 @@ end
|
|||||||
|
|
||||||
function BattleState:drawTextArea()
|
function BattleState:drawTextArea()
|
||||||
if not self:bottomUIVisible() then return end
|
if not self:bottomUIVisible() then return end
|
||||||
-- Gold only: routes the command box and its labels through GbcPalette,
|
Font.drawBox(0, 12, 20, 6)
|
||||||
-- 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
|
||||||
@@ -6486,47 +6419,48 @@ 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
|
||||||
drawGlyph(line[i], 8 + (i - 1) * 8, y)
|
Font.drawCode(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
|
||||||
drawGlyph(0xEE, (0 + 20 - 2) * 8, (12 + 6 - 1) * 8 - 4)
|
Font.drawCode(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)
|
||||||
box(8, 12, 12, 6)
|
Font.drawBox(8, 12, 12, 6)
|
||||||
text(Strings("FIGHT", "battle"), 80, 112)
|
love.graphics.setColor(0, 0, 0, 1)
|
||||||
drawGlyph(0xE1, 128, 112); drawGlyph(0xE2, 136, 112)
|
Font.draw(Strings("FIGHT", "battle"), 80, 112)
|
||||||
text(Strings("ITEM", "battle"), 80, 128); text(Strings("RUN", "battle"), 128, 128)
|
Font.drawCode(0xE1, 128, 112); Font.drawCode(0xE2, 136, 112)
|
||||||
drawGlyph(0xED, 72, (self.demoTimer or 0) <= 80 and 112 or 128)
|
Font.draw(Strings("ITEM", "battle"), 80, 128); Font.draw(Strings("RUN", "battle"), 128, 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)
|
||||||
box(0, 12, 20, 6)
|
Font.drawBox(0, 12, 20, 6)
|
||||||
text(Strings("BALLx"), 16, 112); text(Strings("BAIT"), 112, 112)
|
Font.draw(Strings("BALLx"), 16, 112); Font.draw(Strings("BAIT"), 112, 112)
|
||||||
text(Strings("THROW ROCK"), 16, 128); text(Strings("RUN", "battle"), 112, 128)
|
Font.draw(Strings("THROW ROCK"), 16, 128); Font.draw(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)
|
||||||
text(("%2d"):format(self.safari.balls), 56, 112)
|
Font.draw(("%2d"):format(self.safari.balls), 56, 112)
|
||||||
drawGlyph(0xED, (col == 0 and 8 or 104), 112 + row * 16)
|
Font.drawCode(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
|
||||||
box(8, 12, 12, 6)
|
Font.drawBox(8, 12, 12, 6)
|
||||||
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, (col == 0 and 72 or 120), 112 + row * 16)
|
Font.drawCode(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,
|
||||||
@@ -6601,7 +6535,6 @@ 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()
|
||||||
|
|||||||
@@ -26,10 +26,14 @@ 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)
|
||||||
|
|||||||
@@ -88,7 +88,6 @@ function AnimRunner.new(opts)
|
|||||||
}
|
}
|
||||||
self.objects = AnimObjects.new(self.data, self.constants, self.env)
|
self.objects = AnimObjects.new(self.data, self.constants, self.env)
|
||||||
self.bg = BgEffects.new(self.constants, self.env)
|
self.bg = BgEffects.new(self.constants, self.env)
|
||||||
self.animId = opts.animId -- wFXAnimID
|
|
||||||
self.gfxOrder = self.constants.battleAnimGfxOrder or {}
|
self.gfxOrder = self.constants.battleAnimGfxOrder or {}
|
||||||
self.sfxOrder = opts.sfxOrder or {}
|
self.sfxOrder = opts.sfxOrder or {}
|
||||||
|
|
||||||
|
|||||||
+2
-9
@@ -186,8 +186,7 @@ function Game:makeTitleState()
|
|||||||
onContinue = function()
|
onContinue = function()
|
||||||
local loaded, recovered = SaveData.load()
|
local loaded, recovered = SaveData.load()
|
||||||
if loaded then
|
if loaded then
|
||||||
self:restoreSave(loaded, recovered,
|
self:restoreSave(loaded, recovered, { freshBoot = true })
|
||||||
{ freshBoot = true, continued = true })
|
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
onExit = self.onExit,
|
onExit = self.onExit,
|
||||||
@@ -1259,7 +1258,6 @@ 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)
|
||||||
@@ -1323,9 +1321,6 @@ function Game:restoreSave(loaded, recovered, opts)
|
|||||||
report.recovered = recovered
|
report.recovered = recovered
|
||||||
report.modsDiff = modsDiff
|
report.modsDiff = modsDiff
|
||||||
self.save = loaded
|
self.save = loaded
|
||||||
-- the START cursor lives outside sGameData (ram/sram.asm:17-21)
|
|
||||||
loaded.startMenuIndex = nil
|
|
||||||
self.startMenuIndex = nil
|
|
||||||
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)
|
||||||
@@ -1340,10 +1335,8 @@ function Game:restoreSave(loaded, recovered, opts)
|
|||||||
-- freshBoot threads through from the caller (onContinue and F2 both set
|
-- freshBoot threads through from the caller (onContinue and F2 both set
|
||||||
-- it); a future caller that doesn't ask for it keeps the ordinary
|
-- it); a future caller that doesn't ask for it keeps the ordinary
|
||||||
-- crossfade by default.
|
-- crossfade by default.
|
||||||
-- engine/menus/main_menu.asm:110 (CONTINUE forces PLAYER_DIR_DOWN)
|
|
||||||
local facing = (opts and opts.continued) and "down" or loaded.player.facing
|
|
||||||
self.stack:push(self.overworld, loaded.player.map,
|
self.stack:push(self.overworld, loaded.player.map,
|
||||||
loaded.player.x, loaded.player.y, facing,
|
loaded.player.x, loaded.player.y, loaded.player.facing,
|
||||||
{ via = "boot", freshBoot = opts and opts.freshBoot })
|
{ via = "boot", freshBoot = opts and opts.freshBoot })
|
||||||
self.saveReport = report
|
self.saveReport = report
|
||||||
if not SaveData.emptyReport(report) then
|
if not SaveData.emptyReport(report) then
|
||||||
|
|||||||
@@ -1024,11 +1024,6 @@ function Game2:load()
|
|||||||
require("src.core.gen2.Phone").useRegistry(self.data)
|
require("src.core.gen2.Phone").useRegistry(self.data)
|
||||||
require("src.core.gen2.Decorations").useRegistry(self.data)
|
require("src.core.gen2.Decorations").useRegistry(self.data)
|
||||||
require("src.core.gen2.Apricorns").useRegistry(self.data)
|
require("src.core.gen2.Apricorns").useRegistry(self.data)
|
||||||
-- data.gen2Pokedex is a separate table from the `pokemon` registry's own
|
|
||||||
-- merge target (data.pokemon): a translation mod's
|
|
||||||
-- mod.content.pokemon:patch(id, { dexEntry = ... }) would otherwise never
|
|
||||||
-- reach the #DEX screen. See src/core/gen2/PokedexText.lua.
|
|
||||||
require("src.core.gen2.PokedexText").apply(self.data)
|
|
||||||
|
|
||||||
-- Rendering pipelines: the engine half of the render_pipelines registry
|
-- Rendering pipelines: the engine half of the render_pipelines registry
|
||||||
-- (src/render/Pipelines.lua). install() points it at GOLD's merged dataset
|
-- (src/render/Pipelines.lua). install() points it at GOLD's merged dataset
|
||||||
@@ -1792,7 +1787,6 @@ 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
|
||||||
@@ -2046,7 +2040,6 @@ 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)
|
||||||
|
|||||||
+2
-21
@@ -40,10 +40,8 @@ GamepadMap.NX_RAW_BUTTON_BINDINGS = {
|
|||||||
|
|
||||||
-- Raw index -> gamepad button *name* for RomImporter (then NX face swap applies).
|
-- Raw index -> gamepad button *name* for RomImporter (then NX face swap applies).
|
||||||
GamepadMap.RAW_TO_GAMEPAD_BUTTON = {
|
GamepadMap.RAW_TO_GAMEPAD_BUTTON = {
|
||||||
[1] = "a", [2] = "b", [3] = "x", [4] = "y",
|
[1] = "a", [2] = "b",
|
||||||
[5] = "leftshoulder", [6] = "rightshoulder",
|
|
||||||
[7] = "back", [8] = "start", [9] = "back", [10] = "start",
|
[7] = "back", [8] = "start", [9] = "back", [10] = "start",
|
||||||
[11] = "triggerleft", [12] = "triggerright",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
GamepadMap.NX_RAW_TO_GAMEPAD_BUTTON = {
|
GamepadMap.NX_RAW_TO_GAMEPAD_BUTTON = {
|
||||||
@@ -59,10 +57,7 @@ function GamepadMap._setForceNXForTests(v)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function nxActive()
|
local function nxActive()
|
||||||
if GamepadMap._forceNXForTests == true then return true end
|
if GamepadMap._forceNXForTests 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
|
||||||
@@ -83,20 +78,6 @@ 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".
|
||||||
|
|||||||
+21
-124
@@ -4,79 +4,18 @@ 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. Host tools (curl, zenity) need that unset. A *bundled* AppDir
|
-- system ones. We want to unset the var so that any system tools can find
|
||||||
-- curl must keep APPDIR on LD_LIBRARY_PATH so it resolves the bundled
|
-- their proper libraries. Only needed when running in an AppImage.
|
||||||
-- libssl/libcrypto -- scrubbing it forces host OpenSSL and segfaults across
|
-- LD_PRELOAD is Steam's overlay (#1470): its 32-bit half cannot load into a
|
||||||
-- distros. LD_PRELOAD is always scrubbed: Steam's overlay (#1470) cannot load
|
-- 64-bit child, so ld.so prints an error into that child's output.
|
||||||
-- 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 kind ~= "bundled" and os.getenv("APPIMAGE") then
|
if os.getenv("APPIMAGE") then unset = unset .. "-u LD_LIBRARY_PATH " end
|
||||||
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
|
||||||
@@ -187,19 +126,12 @@ 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
|
||||||
-- opts.envPrefix overrides the default HostShell.envPrefix() (pass "" to skip,
|
function HostShell.popen(command, mode)
|
||||||
-- 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, line, mode or "r")
|
local ok, p = pcall(io.popen, HostShell.envPrefix() .. command, mode or "r")
|
||||||
pipe = (ok and p) or nil
|
pipe = (ok and p) or nil
|
||||||
end)
|
end)
|
||||||
return pipe
|
return pipe
|
||||||
@@ -261,12 +193,6 @@ function HostShell.restart()
|
|||||||
|
|
||||||
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
|
||||||
@@ -364,18 +290,6 @@ 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)
|
||||||
@@ -427,10 +341,7 @@ local curlAvailable = nil
|
|||||||
|
|
||||||
function HostShell.haveCurl()
|
function HostShell.haveCurl()
|
||||||
if curlAvailable ~= nil then return curlAvailable end
|
if curlAvailable ~= nil then return curlAvailable end
|
||||||
local path, kind = HostShell.resolveCurl()
|
local pipe = HostShell.popen("curl --version")
|
||||||
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)
|
||||||
@@ -438,12 +349,6 @@ 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()
|
||||||
@@ -504,11 +409,9 @@ 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 head, popts = curlCmd(
|
local cmd = ("curl -fsSL --proto =http,https --proto-redir =http,https "
|
||||||
("-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) .. " "
|
||||||
@@ -520,7 +423,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, "r", popts)
|
local pipe = HostShell.popen(cmd)
|
||||||
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)
|
||||||
@@ -563,18 +466,16 @@ 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 head, popts = curlCmd(
|
local cmd = ("curl -sSL --proto =http,https --proto-redir =http,https "
|
||||||
("-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, "r", popts)
|
local pipe = HostShell.popen(cmd)
|
||||||
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)
|
||||||
@@ -661,11 +562,9 @@ 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 head, popts = curlCmd(
|
local cmd = ("curl -sSL --proto =http,https --proto-redir =http,https "
|
||||||
("-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
|
||||||
@@ -675,7 +574,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, "r", popts)
|
local pipe = HostShell.popen(cmd)
|
||||||
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"
|
||||||
@@ -852,10 +751,8 @@ function HostShell.httpRequest(url, opts)
|
|||||||
pcall(os.remove, headerPath)
|
pcall(os.remove, headerPath)
|
||||||
end
|
end
|
||||||
|
|
||||||
local head, popts = curlCmd(
|
local cmd = ("curl -sSL --proto =http,https --proto-redir =http,https "
|
||||||
("-sSL --proto =http,https --proto-redir =http,https "
|
.. "--connect-timeout 10 --max-time %d "):format(maxTime)
|
||||||
.. "--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
|
||||||
@@ -864,7 +761,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, "r", popts)
|
local pipe = HostShell.popen(cmd)
|
||||||
if not pipe then
|
if not pipe then
|
||||||
cleanup()
|
cleanup()
|
||||||
return nil, "could not run curl"
|
return nil, "could not run curl"
|
||||||
|
|||||||
+2
-37
@@ -173,40 +173,13 @@ 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 *and* a writable probe wins. Flatpak installs always use the
|
-- the marker wins.
|
||||||
-- 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
|
||||||
local markerOk = false
|
if pathExists(base .. SEP .. PORTABLE_MARKER) then
|
||||||
pcall(function()
|
|
||||||
markerOk = pathExists(base .. SEP .. PORTABLE_MARKER)
|
|
||||||
end)
|
|
||||||
if markerOk and dirIsWritable(base) then
|
|
||||||
portableBase = base
|
portableBase = base
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
@@ -214,13 +187,6 @@ 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
|
||||||
@@ -313,7 +279,6 @@ 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
|
||||||
|
|||||||
@@ -1101,10 +1101,8 @@ function Breeding.hatch(data, save, index, nickname, where)
|
|||||||
hatched.experience = egg.experience
|
hatched.experience = egg.experience
|
||||||
-- `ld a, [de] / ld [hli], a` twice: HP := MaxHP.
|
-- `ld a, [de] / ld [hli], a` twice: HP := MaxHP.
|
||||||
hatched.hp = hatched.maxHp
|
hatched.hp = hatched.maxHp
|
||||||
-- HatchEggs overwrites OT unconditionally, so the ODD_EGG's "ODD" and its
|
hatched.ot = egg.ot or (save.player and save.player.name)
|
||||||
-- table id do not survive -- engine/pokemon/breeding.asm:299-309
|
hatched.otId = egg.otId or (save.player and save.player.id)
|
||||||
hatched.ot = (save.player and save.player.name) or egg.ot
|
|
||||||
hatched.otId = (save.player and save.player.id) or egg.otId
|
|
||||||
hatched.caughtLevel = egg.level or Breeding.EGG_LEVEL
|
hatched.caughtLevel = egg.level or Breeding.EGG_LEVEL
|
||||||
-- SetEggMonCaughtData swaps wCurPartyLevel for CAUGHT_EGG_LEVEL around the
|
-- SetEggMonCaughtData swaps wCurPartyLevel for CAUGHT_EGG_LEVEL around the
|
||||||
-- shared setter -- engine/pokemon/caught_data.asm:235-246.
|
-- shared setter -- engine/pokemon/caught_data.asm:235-246.
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
-- Projects a mod's `pokemon` registry dexEntry onto the #DEX screen's own
|
|
||||||
-- data table.
|
|
||||||
--
|
|
||||||
-- data.gen2Pokedex.entries[species] (data/generated/pokedex.lua,
|
|
||||||
-- RomExtractorGen2:extractPokedex) is what src/ui/gen2/PokedexMenu.lua
|
|
||||||
-- actually reads for the KIND label and the two description pages
|
|
||||||
-- (entry.kind, entry.text, entry.text2) -- a table loaded straight from
|
|
||||||
-- disk in Game2:load, BEFORE mods:load runs, and never routed through a
|
|
||||||
-- registry. The `pokemon` registry's own merge target is the separate
|
|
||||||
-- data.pokemon table (src/mods/Schemas.lua, R.pokemon: `target = "pokemon"`),
|
|
||||||
-- which mod.content.pokemon:patch(id, { dexEntry = { kind = ..., text = ...,
|
|
||||||
-- text2 = ... } }) already reaches -- so a translation mod's #DEX text was
|
|
||||||
-- silently invisible in-game despite validating against the registry.
|
|
||||||
--
|
|
||||||
-- This is the missing link: called once after the merge (src/core/Game2.lua),
|
|
||||||
-- the same way ItemEffects.applyHeldItems projects held_items onto
|
|
||||||
-- data.gen2HeldItems. height/weight/dex stay untouched -- game state, not
|
|
||||||
-- text a translation carries.
|
|
||||||
local PokedexText = {}
|
|
||||||
|
|
||||||
function PokedexText.apply(data)
|
|
||||||
local dex = data and data.gen2Pokedex
|
|
||||||
local pokemon = data and data.pokemon
|
|
||||||
if not (dex and dex.entries and pokemon) then return 0 end
|
|
||||||
local count = 0
|
|
||||||
for species, entry in pairs(dex.entries) do
|
|
||||||
local def = pokemon[species]
|
|
||||||
local override = def and def.dexEntry
|
|
||||||
if override and (override.kind or override.text or override.text2) then
|
|
||||||
if override.kind then entry.kind = override.kind end
|
|
||||||
if override.text then entry.text = override.text end
|
|
||||||
if override.text2 then entry.text2 = override.text2 end
|
|
||||||
count = count + 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return count
|
|
||||||
end
|
|
||||||
|
|
||||||
return PokedexText
|
|
||||||
@@ -309,7 +309,6 @@ 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.
|
||||||
@@ -317,7 +316,6 @@ 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
|
||||||
|
|||||||
@@ -605,7 +605,6 @@ local function gen2Rows(opts, hooks, shared)
|
|||||||
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
|
||||||
|
|||||||
@@ -377,19 +377,12 @@ 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)
|
||||||
local isFocused = Kit.focusable(key, x, y, w, h)
|
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 ""
|
||||||
@@ -404,36 +397,9 @@ 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 (Kit.press(x, y, w, h) or Kit._activateId == key) then
|
if action and (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 = {
|
||||||
@@ -1394,9 +1360,7 @@ local function buildHeader(imp, m)
|
|||||||
-- under the gear and the quit X -- "the settings is covering the logo".
|
-- under the gear and the quit X -- "the settings is covering the logo".
|
||||||
-- Reserving the space on both sides costs a little width and cannot
|
-- Reserving the space on both sides costs a little width and cannot
|
||||||
-- overlap at any window size.
|
-- overlap at any window size.
|
||||||
-- iOS has no quit button (the OS owns app exit), so the cluster is the
|
local clusterW = 2 * gear + math.floor(6 * m.s) + m.pad
|
||||||
-- gear alone and the wordmark gets that width back
|
|
||||||
local clusterW = (imp.ios and gear or 2 * gear + math.floor(6 * m.s)) + m.pad
|
|
||||||
local boxX = m.x + clusterW
|
local boxX = m.x + clusterW
|
||||||
local boxW = math.max(0, m.w - 2 * clusterW)
|
local boxW = math.max(0, m.w - 2 * clusterW)
|
||||||
if imp.logo and boxW > 0 then
|
if imp.logo and boxW > 0 then
|
||||||
@@ -1430,8 +1394,8 @@ local function buildHeader(imp, m)
|
|||||||
-- inboard of it -- but the two are REGISTERED gear first, because the first
|
-- inboard of it -- but the two are REGISTERED gear first, because the first
|
||||||
-- focusable of the first frame adopts the keyboard ring and that must not be
|
-- focusable of the first frame adopts the keyboard ring and that must not be
|
||||||
-- the button that exits the app.
|
-- the button that exits the app.
|
||||||
local quitX = not imp.ios and rx - gear or nil
|
local quitX = rx - gear
|
||||||
if quitX then rx = quitX - math.floor(6 * m.s) end
|
rx = quitX - math.floor(6 * m.s)
|
||||||
|
|
||||||
-- Settings gear. It now also owns the CONTROL settings (touch overlay
|
-- Settings gear. It now also owns the CONTROL settings (touch overlay
|
||||||
-- editor, reset rebinds), which used to be buttons stacked in the game
|
-- editor, reset rebinds), which used to be buttons stacked in the game
|
||||||
@@ -1443,7 +1407,7 @@ local function buildHeader(imp, m)
|
|||||||
chrome.gear.image = imp._gearIcon
|
chrome.gear.image = imp._gearIcon
|
||||||
btn(imp, rx, by, gear, gear, "gear", "", chrome.gear)
|
btn(imp, rx, by, gear, gear, "gear", "", chrome.gear)
|
||||||
|
|
||||||
if quitX then btn(imp, quitX, by, gear, gear, "quit", "", chrome.quit) end
|
btn(imp, quitX, by, gear, gear, "quit", "", chrome.quit)
|
||||||
|
|
||||||
-- The self-update control lives in the FOOTER next to the BCG mark (small,
|
-- The self-update control lives in the FOOTER next to the BCG mark (small,
|
||||||
-- out of the wordmark's way -- it used to overlap the logo on a phone). It
|
-- out of the wordmark's way -- it used to overlap the logo on a phone). It
|
||||||
@@ -3442,19 +3406,13 @@ 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 or isFocused) and 1 or 0.85)
|
love.graphics.setColor(1, 1, 1, hot 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) or Kit._activateId == "bcg" then
|
if Kit.press(bx, my, dw, dh) 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
|
||||||
@@ -5646,8 +5604,6 @@ 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
|
||||||
@@ -5660,14 +5616,6 @@ 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"),
|
||||||
@@ -5839,10 +5787,6 @@ 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
|
||||||
@@ -6026,25 +5970,6 @@ 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
|
||||||
|
|||||||
@@ -2753,8 +2753,7 @@ function RomExtractorGen2:extractAudio(maps)
|
|||||||
local speciesOrder = self.manifest.constants.speciesOrder or {}
|
local speciesOrder = self.manifest.constants.speciesOrder or {}
|
||||||
local cries = {}
|
local cries = {}
|
||||||
for index, species in ipairs(speciesOrder) do
|
for index, species in ipairs(speciesOrder) do
|
||||||
-- data/pokemon/cries.asm:209 gives UNOWN a real cry row
|
if species ~= "UNOWN" and not tostring(species):match("^UNUSED") then
|
||||||
if not tostring(species):match("^UNUSED") then
|
|
||||||
local row = self.rom:bytes(
|
local row = self.rom:bytes(
|
||||||
pokeCryPtr.bank, pokeCryPtr.address + (index - 1) * 6, 6)
|
pokeCryPtr.bank, pokeCryPtr.address + (index - 1) * 6, 6)
|
||||||
local cryIndex = row[1] + row[2] * 256
|
local cryIndex = row[1] + row[2] * 256
|
||||||
|
|||||||
+36
-341
@@ -1263,12 +1263,7 @@ 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
|
||||||
local isHandheld = os.getenv("HANDHELD") == "1" or os.getenv("PORTMASTER") == "1"
|
if not (love.filesystem.isFused and love.filesystem.isFused()) then return false end
|
||||||
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
|
||||||
@@ -2060,42 +2055,8 @@ 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
|
if path then self:_installMod(path) end
|
||||||
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)
|
||||||
@@ -2296,8 +2257,8 @@ function RomImporter:_importRequiredSource(modId, importId, source, confirmed)
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
if spec.format ~= "n64" and (size == nil
|
if type(size) == "number" and size > RequiredImports.LARGE_WARN_BYTES
|
||||||
or size > RequiredImports.LARGE_WARN_BYTES) then
|
and spec.format ~= "n64" then
|
||||||
local ok, result = streamRequiredImport(manifest, importId, source)
|
local ok, result = streamRequiredImport(manifest, importId, source)
|
||||||
if ok then
|
if ok then
|
||||||
self.requiredImportNotice = nil
|
self.requiredImportNotice = nil
|
||||||
@@ -2518,42 +2479,8 @@ 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
|
if path then self:_importSave(version, path) end
|
||||||
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
|
||||||
@@ -2694,42 +2621,11 @@ 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
|
||||||
@@ -3102,14 +2998,6 @@ 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
|
||||||
@@ -3152,6 +3040,14 @@ 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
|
||||||
@@ -3159,12 +3055,17 @@ 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()
|
||||||
@@ -3173,183 +3074,29 @@ function RomImporter:_updatePadCursor(dt)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function RomImporter:gamepadpressed(_, button)
|
function RomImporter:gamepadpressed(_, button)
|
||||||
local action = (GamepadMap.mapLauncherButton and GamepadMap.mapLauncherButton(button)) or button
|
|
||||||
local okKit, Kit = pcall(require, "src.ui.kit.Kit")
|
|
||||||
if okKit then
|
|
||||||
if Kit.VirtualKeyboard and Kit.VirtualKeyboard.active then
|
|
||||||
if Kit.VirtualKeyboard.gamepadpressed(action) then return end
|
|
||||||
end
|
|
||||||
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 on handhelds
|
|
||||||
if button == "back" or button == "select" or action == "select" then
|
|
||||||
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"
|
|
||||||
or os.getenv("POKEPORT_SBC") == "1" or os.getenv("ANBERNIC") == "1"
|
|
||||||
if not isHandheld then return end
|
|
||||||
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)
|
|
||||||
return
|
|
||||||
elseif button == "rightshoulder" then
|
|
||||||
self:_cycleTab(1)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Triggers (L2 / R2): fast scroll content / lists up and down
|
|
||||||
if button == "triggerleft" or button == "lefttrigger" or button == "l2"
|
|
||||||
or action == "triggerleft" or action == "l2" then
|
|
||||||
require("src.import.LauncherView").wheelmoved(self, 0, 4)
|
|
||||||
return
|
|
||||||
elseif button == "triggerright" or button == "righttrigger" or button == "r2"
|
|
||||||
or action == "triggerright" or action == "r2" then
|
|
||||||
require("src.import.LauncherView").wheelmoved(self, 0, -4)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Start button: Play / Choose ROM
|
|
||||||
if button == "start" then
|
|
||||||
if self.workState == "working" then return end
|
|
||||||
local version = self.tab
|
|
||||||
if GameVersion.VERSIONS[version] then
|
|
||||||
if self.ready[version] then self:play(version) else self:_romAction(version) 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()
|
self:_activatePadCursor()
|
||||||
|
-- Map through GamepadMap so NX swaps SDL face labels to Nintendo A/B.
|
||||||
|
local action = GamepadMap.mapGamepadButton(button)
|
||||||
if action == "a" then
|
if action == "a" then
|
||||||
|
-- Instant click at the virtual pointer: dispatched straight into the
|
||||||
|
-- view, since the launcher no longer hit-tests presses itself.
|
||||||
if self._flex then
|
if self._flex then
|
||||||
require("src.import.LauncherView").clickAt(self,
|
require("src.import.LauncherView").clickAt(self,
|
||||||
self._padCursor.x, self._padCursor.y)
|
self._padCursor.x, self._padCursor.y)
|
||||||
end
|
end
|
||||||
elseif action == "b" then
|
elseif button == "leftshoulder" then
|
||||||
if self._indexPrompt then self._indexPrompt = nil; self:_disarmTextInput(); return
|
self:_cycleTab(-1)
|
||||||
elseif self._rename then self._rename = nil; self:_disarmTextInput(); return
|
elseif button == "rightshoulder" then
|
||||||
elseif self._profileRenamePrompt then self._profileRenamePrompt = nil; self:_disarmTextInput(); return
|
self:_cycleTab(1)
|
||||||
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"
|
elseif button == "dpup" or button == "dpdown"
|
||||||
or button == "dpleft" or button == "dpright" then
|
or button == "dpleft" or button == "dpright" then
|
||||||
self._padDir[button] = true
|
self._padDir[button] = true
|
||||||
|
elseif button == "start" or button == "back" then
|
||||||
|
-- Start / Select: Play if ready, else Choose ROM on the active game tab.
|
||||||
|
if self.workState == "working" then return end
|
||||||
|
local version = self.tab
|
||||||
|
if GameVersion.VERSIONS[version] then
|
||||||
|
if self.ready[version] then self:play(version) else self:_romAction(version) end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -3364,29 +3111,11 @@ 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 self._padCursorActive and math.abs(value) > PAD_DEAD then
|
if math.abs(value) > PAD_DEAD then
|
||||||
self:_activatePadCursor()
|
self:_activatePadCursor()
|
||||||
elseif axis == "lefty" and math.abs(value) <= PAD_DEAD then
|
elseif axis == "lefty" then
|
||||||
self._padStickCentered = true
|
self._padStickCentered = true
|
||||||
end
|
end
|
||||||
elseif axis == "triggerleft" or axis == "lefttrigger" then
|
|
||||||
if value > 0.4 then
|
|
||||||
if not self._l2TriggerActive then
|
|
||||||
require("src.import.LauncherView").wheelmoved(self, 0, 4)
|
|
||||||
self._l2TriggerActive = true
|
|
||||||
end
|
|
||||||
elseif value < 0.2 then
|
|
||||||
self._l2TriggerActive = false
|
|
||||||
end
|
|
||||||
elseif axis == "triggerright" or axis == "righttrigger" then
|
|
||||||
if value > 0.4 then
|
|
||||||
if not self._r2TriggerActive then
|
|
||||||
require("src.import.LauncherView").wheelmoved(self, 0, -4)
|
|
||||||
self._r2TriggerActive = true
|
|
||||||
end
|
|
||||||
elseif value < 0.2 then
|
|
||||||
self._r2TriggerActive = false
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -3414,24 +3143,6 @@ function RomImporter:joystickaxis(joystick, axis, value)
|
|||||||
self:gamepadaxis(joystick, "leftx", value)
|
self:gamepadaxis(joystick, "leftx", value)
|
||||||
elseif axis == 2 then
|
elseif axis == 2 then
|
||||||
self:gamepadaxis(joystick, "lefty", value)
|
self:gamepadaxis(joystick, "lefty", value)
|
||||||
elseif axis == 3 or axis == 5 then
|
|
||||||
if value > 0.4 then
|
|
||||||
if not self._l2RawActive then
|
|
||||||
require("src.import.LauncherView").wheelmoved(self, 0, 4)
|
|
||||||
self._l2RawActive = true
|
|
||||||
end
|
|
||||||
elseif value < 0.2 then
|
|
||||||
self._l2RawActive = false
|
|
||||||
end
|
|
||||||
elseif axis == 4 or axis == 6 then
|
|
||||||
if value > 0.4 then
|
|
||||||
if not self._r2RawActive then
|
|
||||||
require("src.import.LauncherView").wheelmoved(self, 0, -4)
|
|
||||||
self._r2RawActive = true
|
|
||||||
end
|
|
||||||
elseif value < 0.2 then
|
|
||||||
self._r2RawActive = false
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -4266,7 +3977,6 @@ function RomImporter:_toggleSafeMode()
|
|||||||
self.mods = nil
|
self.mods = nil
|
||||||
self.findInstalled = nil
|
self.findInstalled = nil
|
||||||
self._modSortCache = nil
|
self._modSortCache = nil
|
||||||
self._cartCaptureCache = nil
|
|
||||||
self._modInfoFetch = nil
|
self._modInfoFetch = nil
|
||||||
self.modNotice = nil
|
self.modNotice = nil
|
||||||
end
|
end
|
||||||
@@ -4352,15 +4062,6 @@ 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 "")
|
||||||
@@ -4593,7 +4294,7 @@ function RomImporter:_selectCart(version, id)
|
|||||||
self.activeCart[version] = id
|
self.activeCart[version] = id
|
||||||
self._cartPlan = nil
|
self._cartPlan = nil
|
||||||
-- the MODS panel answers for the cart now, so its cached rows are stale
|
-- the MODS panel answers for the cart now, so its cached rows are stale
|
||||||
self.mods, self._modSortCache, self._cartCaptureCache = nil, nil, nil
|
self.mods, self._modSortCache = nil, nil
|
||||||
local scope = self:slotScope(version)
|
local scope = self:slotScope(version)
|
||||||
self.slots[scope] = nil
|
self.slots[scope] = nil
|
||||||
self.slotScroll[scope] = nil
|
self.slotScroll[scope] = nil
|
||||||
@@ -4892,11 +4593,7 @@ end
|
|||||||
-- pins the off ones too, so they are part of the cart it would write.
|
-- pins the off ones too, so they are part of the cart it would write.
|
||||||
function RomImporter:_cartCaptureCount(version)
|
function RomImporter:_cartCaptureCount(version)
|
||||||
if not GameVersion.VERSIONS[version] then return 0 end
|
if not GameVersion.VERSIONS[version] then return 0 end
|
||||||
local cache = self._cartCaptureCache
|
return #cartModRows(self, version)
|
||||||
if cache and cache.version == version then return cache.n end
|
|
||||||
local n = #cartModRows(self, version)
|
|
||||||
self._cartCaptureCache = { version = version, n = n }
|
|
||||||
return n
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function RomImporter:_cartAuthor()
|
function RomImporter:_cartAuthor()
|
||||||
@@ -5255,7 +4952,6 @@ function RomImporter:_refreshMods()
|
|||||||
local SaveData = require("src.core.SaveData")
|
local SaveData = require("src.core.SaveData")
|
||||||
self._cartPlan = nil
|
self._cartPlan = nil
|
||||||
self._profileCache = nil
|
self._profileCache = nil
|
||||||
self._cartCaptureCache = 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
|
||||||
@@ -5290,7 +4986,6 @@ function RomImporter:_refreshMods()
|
|||||||
if m.targetsHere ~= false then kept[#kept + 1] = m end
|
if m.targetsHere ~= false then kept[#kept + 1] = m end
|
||||||
end
|
end
|
||||||
self.mods = kept
|
self.mods = kept
|
||||||
self._cartCaptureCache = { version = self.modScope, n = #kept }
|
|
||||||
end
|
end
|
||||||
-- a pin is judged against the whole listing: the cart named it, so it is
|
-- a pin is judged against the whole listing: the cart named it, so it is
|
||||||
-- listed even where the game filter above would have dropped it
|
-- listed even where the game filter above would have dropped it
|
||||||
|
|||||||
@@ -20,37 +20,6 @@ local GameVersion = require("src.core.GameVersion")
|
|||||||
|
|
||||||
local SaveFileIO = {}
|
local SaveFileIO = {}
|
||||||
|
|
||||||
-- The cartridge image an imported Gen 2 slot came from, kept BESIDE the slot
|
|
||||||
-- rather than inside it: export needs the regions the codec does not model,
|
|
||||||
-- and 32 KB of binary in the serialized table is 40 KB of Lua source reparsed
|
|
||||||
-- on every save and load.
|
|
||||||
local function cartPath(version, slotId)
|
|
||||||
return ("saves/%s/%s.cart"):format(version, tostring(slotId))
|
|
||||||
end
|
|
||||||
|
|
||||||
local function cartFs()
|
|
||||||
local portable = SaveData.portableFs and SaveData.portableFs()
|
|
||||||
return portable or (love and love.filesystem)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function writeCart(version, slotId, bytes)
|
|
||||||
local fs = cartFs()
|
|
||||||
if not (fs and fs.write and bytes) then return end
|
|
||||||
if fs.createDirectory then
|
|
||||||
fs.createDirectory("saves")
|
|
||||||
fs.createDirectory("saves/" .. version)
|
|
||||||
end
|
|
||||||
fs.write(cartPath(version, slotId), bytes)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function readCart(version, slotId)
|
|
||||||
local fs = cartFs()
|
|
||||||
if not (fs and fs.read) then return nil end
|
|
||||||
local ok, bytes = pcall(fs.read, cartPath(version, slotId))
|
|
||||||
if ok and type(bytes) == "string" then return bytes end
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local SAVE_SIZE = SaveConvert.SAVE_SIZE
|
local SAVE_SIZE = SaveConvert.SAVE_SIZE
|
||||||
|
|
||||||
-- Resolve raw save bytes from whatever the launcher hands us:
|
-- Resolve raw save bytes from whatever the launcher hands us:
|
||||||
@@ -109,17 +78,8 @@ function SaveFileIO.importToSlot(source, version, force)
|
|||||||
version = version or GameVersion.get()
|
version = version or GameVersion.get()
|
||||||
local bytes, readErr = readSource(source)
|
local bytes, readErr = readSource(source)
|
||||||
if not bytes then return false, readErr end
|
if not bytes then return false, readErr end
|
||||||
-- The GAME decides before the BYTES do. Everything below this line used to
|
|
||||||
-- judge a save by Gen 1's rules whatever game it was for, and a Gen 2 cart
|
|
||||||
-- is MBC3+TIMER: a real Gold/Silver/Crystal .sav carries an RTC footer, so
|
|
||||||
-- it is 32786 bytes, misses the size test, and was then measured against
|
|
||||||
-- pokered's checksum -- which is why a perfectly good Crystal save reported
|
|
||||||
-- as corrupt (#1832). mainChecksumValid now takes the game and asks that
|
|
||||||
-- generation's rule.
|
|
||||||
local supported, unsupportedWhy = SaveConvert.importSupported(version)
|
|
||||||
if not supported then return false, unsupportedWhy end
|
|
||||||
if #bytes ~= SAVE_SIZE then
|
if #bytes ~= SAVE_SIZE then
|
||||||
local check = SaveConvert.mainChecksumValid(bytes, version)
|
local check = SaveConvert.mainChecksumValid(bytes)
|
||||||
if check == nil then
|
if check == nil then
|
||||||
return false, ("A save file must be %d bytes (32 KB); this one is %d.")
|
return false, ("A save file must be %d bytes (32 KB); this one is %d.")
|
||||||
:format(SAVE_SIZE, #bytes)
|
:format(SAVE_SIZE, #bytes)
|
||||||
@@ -127,12 +87,7 @@ function SaveFileIO.importToSlot(source, version, force)
|
|||||||
if check == false then
|
if check == false then
|
||||||
return false, "save data checksum invalid (main data checksum mismatch)"
|
return false, "save data checksum invalid (main data checksum mismatch)"
|
||||||
end
|
end
|
||||||
-- The confirm exists because a Gen 1 save bigger than 32768 is a surprise
|
if #bytes > SAVE_SIZE and not force then
|
||||||
-- worth asking about. On a Gen 2 cart it is the normal shape -- every
|
|
||||||
-- real one has the footer -- so asking would be a prompt with one sensible
|
|
||||||
-- answer, on every import, forever.
|
|
||||||
if #bytes > SAVE_SIZE and not force
|
|
||||||
and not SaveConvert.isGen2Cart(version) then
|
|
||||||
return false, nil, { needsConfirm = true, size = #bytes }
|
return false, nil, { needsConfirm = true, size = #bytes }
|
||||||
end
|
end
|
||||||
bytes = #bytes > SAVE_SIZE and bytes:sub(1, SAVE_SIZE)
|
bytes = #bytes > SAVE_SIZE and bytes:sub(1, SAVE_SIZE)
|
||||||
@@ -156,7 +111,6 @@ function SaveFileIO.importToSlot(source, version, force)
|
|||||||
return false, "could not write the imported save: " .. tostring(writeErr)
|
return false, "could not write the imported save: " .. tostring(writeErr)
|
||||||
end
|
end
|
||||||
SaveData.setActiveSlot(version, slotId)
|
SaveData.setActiveSlot(version, slotId)
|
||||||
if SaveConvert.isGen2Cart(version) then writeCart(version, slotId, bytes) end
|
|
||||||
return true, slotId
|
return true, slotId
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -171,10 +125,9 @@ function SaveFileIO.exportActiveSlot(version)
|
|||||||
version = version or GameVersion.get()
|
version = version or GameVersion.get()
|
||||||
local save = SaveData.load(version)
|
local save = SaveData.load(version)
|
||||||
if not save then return false, "this game has no save to export yet" end
|
if not save then return false, "this game has no save to export yet" end
|
||||||
local slotId = SaveData.activeSlot(version) or "save"
|
local bytes, exportErr = SaveConvert.exportSav(save, version)
|
||||||
local bytes, exportErr = SaveConvert.exportSav(save, version,
|
|
||||||
readCart(version, slotId))
|
|
||||||
if not bytes then return false, exportErr end
|
if not bytes then return false, exportErr end
|
||||||
|
local slotId = SaveData.activeSlot(version) or "save"
|
||||||
-- Portable mode is the same seam SaveData's own persistFs uses: when
|
-- Portable mode is the same seam SaveData's own persistFs uses: when
|
||||||
-- portable.txt marks the install every persistent write leaves the OS save
|
-- portable.txt marks the install every persistent write leaves the OS save
|
||||||
-- directory for the game folder, and an export is no exception. Writing
|
-- directory for the game folder, and an export is no exception. Writing
|
||||||
|
|||||||
@@ -94,11 +94,9 @@ function ItemEffects.healsHP(id)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- .useRareCandy prints over the still-drawn party menu
|
-- .useRareCandy prints over the still-drawn party menu
|
||||||
-- (engine/items/item_effects.asm:1392-1418); .useVitamin ends at
|
-- (engine/items/item_effects.asm:1392-1418)
|
||||||
-- RemoveUsedItem the same way (engine/items/item_effects.asm:1315-1322)
|
|
||||||
function ItemEffects.keepsPartyMenuOpen(id)
|
function ItemEffects.keepsPartyMenuOpen(id)
|
||||||
return ItemEffects.healsHP(id) or id == "RARE_CANDY"
|
return ItemEffects.healsHP(id) or id == "RARE_CANDY"
|
||||||
or VITAMINS[id] ~= nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function ItemEffects.isBattleMedicine(id)
|
function ItemEffects.isBattleMedicine(id)
|
||||||
@@ -528,9 +526,8 @@ function ItemEffects.use(data, save, itemId, target, battle, moveIndex, ow)
|
|||||||
-- _VitaminStatRoseText's slot order is localization-dependent (the
|
-- _VitaminStatRoseText's slot order is localization-dependent (the
|
||||||
-- Spanish ROM puts the stat before the name), so the extracted line
|
-- Spanish ROM puts the stat before the name), so the extracted line
|
||||||
-- cannot be filled positionally; the engine wording stands
|
-- cannot be filled positionally; the engine wording stands
|
||||||
-- engine/items/item_effects.asm:1313
|
|
||||||
return "consumed", { Strings("%s's %s\nrose!", monName(data, target),
|
return "consumed", { Strings("%s's %s\nrose!", monName(data, target),
|
||||||
Strings(STAT_LABEL[vitaminStat])) }, { useJingle = true }
|
Strings(STAT_LABEL[vitaminStat])) }
|
||||||
end
|
end
|
||||||
|
|
||||||
-- PP UP boosts the move the player picked (ItemUsePPUp's move menu)
|
-- PP UP boosts the move the player picked (ItemUsePPUp's move menu)
|
||||||
|
|||||||
+4
-26
@@ -23,7 +23,6 @@ local Protocol = require("src.link.Protocol")
|
|||||||
local Runtime = require("src.mods.Runtime")
|
local Runtime = require("src.mods.Runtime")
|
||||||
local TurnOrder = require("src.battle.TurnOrder")
|
local TurnOrder = require("src.battle.TurnOrder")
|
||||||
local Strings = require("src.core.Strings")
|
local Strings = require("src.core.Strings")
|
||||||
local Timing = require("src.core.Timing")
|
|
||||||
|
|
||||||
local LinkBattle = {}
|
local LinkBattle = {}
|
||||||
|
|
||||||
@@ -474,22 +473,11 @@ function LinkBattle.new(game, net, opts)
|
|||||||
-- switches happen before attacks (both may switch)
|
-- switches happen before attacks (both may switch)
|
||||||
if myMsg.kind == "switch" then
|
if myMsg.kind == "switch" then
|
||||||
local idx = myMsg.index
|
local idx = myMsg.index
|
||||||
-- SwitchPlayerMon (engine/battle/core.asm:2419-2423); a post-faint
|
s:act(function() sendOutPlayer(s, myParty[idx]) end)
|
||||||
-- replacement shares sendOutPlayer and prints neither line
|
|
||||||
s:act(function()
|
|
||||||
s:sayNextAuto(s:withdrawText(s.player.name), Timing.SWITCH_PLAYER_MON)
|
|
||||||
s:queueRetreatAnim()
|
|
||||||
s:actNext(function() sendOutPlayer(s, myParty[idx]) end)
|
|
||||||
end)
|
|
||||||
myAction = nil
|
myAction = nil
|
||||||
end
|
end
|
||||||
if theirSwitch then
|
if theirSwitch then
|
||||||
-- SwitchEnemyMon (engine/battle/trainer_ai.asm:596-599)
|
s:act(function() sendOutEnemy(s, theirParty[theirSwitch]) end)
|
||||||
s:act(function()
|
|
||||||
s:sayNext(s:romText("_AIBattleWithdrawText", "%s with-\ndrew %s!",
|
|
||||||
theirName, s.enemy.name))
|
|
||||||
s:actNext(function() sendOutEnemy(s, theirParty[theirSwitch]) end)
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
s:act(function()
|
s:act(function()
|
||||||
@@ -897,21 +885,11 @@ function LinkBattle.newSpectator(game, net, opts)
|
|||||||
|
|
||||||
if hostMsg.kind == "switch" then
|
if hostMsg.kind == "switch" then
|
||||||
local idx = hostMsg.index
|
local idx = hostMsg.index
|
||||||
-- SwitchPlayerMon (engine/battle/core.asm:2419-2423)
|
s:act(function() sendOutHost(s, hostParty[idx]) end)
|
||||||
s:act(function()
|
|
||||||
s:sayNextAuto(s:withdrawText(s.player.name), Timing.SWITCH_PLAYER_MON)
|
|
||||||
s:queueRetreatAnim()
|
|
||||||
s:actNext(function() sendOutHost(s, hostParty[idx]) end)
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
if guestMsg.kind == "switch" then
|
if guestMsg.kind == "switch" then
|
||||||
local idx = guestMsg.index
|
local idx = guestMsg.index
|
||||||
-- SwitchEnemyMon (engine/battle/trainer_ai.asm:596-599)
|
s:act(function() sendOutGuest(s, guestParty[idx]) end)
|
||||||
s:act(function()
|
|
||||||
s:sayNext(s:romText("_AIBattleWithdrawText", "%s with-\ndrew %s!",
|
|
||||||
guestName, s.enemy.name))
|
|
||||||
s:actNext(function() sendOutGuest(s, guestParty[idx]) end)
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
s:act(function()
|
s:act(function()
|
||||||
|
|||||||
@@ -1711,8 +1711,8 @@ local function buildStartMenu()
|
|||||||
function overrides.new(game)
|
function overrides.new(game)
|
||||||
local g = live() or game
|
local g = live() or game
|
||||||
local save = g and g.save
|
local save = g and g.save
|
||||||
if g and g.startMenuIndex then
|
if save and save.startMenuIndex then
|
||||||
Start2.lastIndex = g.startMenuIndex
|
Start2.lastIndex = save.startMenuIndex
|
||||||
end
|
end
|
||||||
return newOrig(g, {
|
return newOrig(g, {
|
||||||
save = save,
|
save = save,
|
||||||
@@ -1741,7 +1741,7 @@ COVERAGE["src.ui.StartMenu"] = {
|
|||||||
new = "synthesises onClose (stack:pop) and onChoose "
|
new = "synthesises onClose (stack:pop) and onChoose "
|
||||||
.. "(Game2:openStartMenuItem); without them the menu cannot be left",
|
.. "(Game2:openStartMenuItem); without them the menu cannot be left",
|
||||||
index = "the cursor is menu.list.index on Gold (a Chrome.List)",
|
index = "the cursor is menu.list.index on Gold (a Chrome.List)",
|
||||||
["game.startMenuIndex"] = "copied INTO StartMenu.lastIndex on construct "
|
["save.startMenuIndex"] = "copied INTO StartMenu.lastIndex on construct "
|
||||||
.. "and never copied back: Gold's cursor is a class field, not save data",
|
.. "and never copied back: Gold's cursor is a class field, not save data",
|
||||||
tx = "the box is fixed at Chrome.box(10, 0, 10, h); tx/ty/tw/th/anchor/"
|
tx = "the box is fixed at Chrome.box(10, 0, 10, h); tx/ty/tw/th/anchor/"
|
||||||
.. "maxVisible/startCloses/noSound do not exist and writes are inert",
|
.. "maxVisible/startCloses/noSound do not exist and writes are inert",
|
||||||
|
|||||||
@@ -826,14 +826,10 @@ R.pokemon = {
|
|||||||
item = f.opt(f.id("items")),
|
item = f.opt(f.id("items")),
|
||||||
species = f.id("pokemon") }),
|
species = f.id("pokemon") }),
|
||||||
spriteFront = f.path, spriteBack = f.path, frontSize = f.int(1, 7),
|
spriteFront = f.path, spriteBack = f.path, frontSize = f.int(1, 7),
|
||||||
-- text2 is the #DEX entry's second description page (Pokedex_asm's bare
|
|
||||||
-- `page` macro, engine/pokedex/pokedex.asm): PokedexMenu:drawEntryBody
|
|
||||||
-- shows `entry.text` on page 1 and `entry.text2` on page 2, so a
|
|
||||||
-- translation needs both to cover the whole entry.
|
|
||||||
dexEntry = f.opt(f.rec{ kind = f.str, heightFt = f.int(0),
|
dexEntry = f.opt(f.rec{ kind = f.str, heightFt = f.int(0),
|
||||||
heightIn = f.int(0, 11), weight = f.num,
|
heightIn = f.int(0, 11), weight = f.num,
|
||||||
heightM = f.opt(f.num), weightKg = f.opt(f.num),
|
heightM = f.opt(f.num), weightKg = f.opt(f.num),
|
||||||
text = f.str, text2 = f.opt(f.str) }),
|
text = f.str }),
|
||||||
icon = f.opt(f.union{ f.str, f.rec{ image = f.path,
|
icon = f.opt(f.union{ f.str, f.rec{ image = f.path,
|
||||||
frames = f.opt(f.int(1)) } }),
|
frames = f.opt(f.int(1)) } }),
|
||||||
cry = f.opt(f.id("cries")), palette = f.opt(f.id("palettes")),
|
cry = f.opt(f.id("cries")), palette = f.opt(f.id("palettes")),
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
-- 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
|
||||||
@@ -30,12 +32,9 @@
|
|||||||
local GbcPalette = {}
|
local GbcPalette = {}
|
||||||
|
|
||||||
GbcPalette.MODES = { "gbc", "dmg", "classic" }
|
GbcPalette.MODES = { "gbc", "dmg", "classic" }
|
||||||
GbcPalette.MODE_LABELS = { gbc = "GEN 2", dmg = "DMG", classic = "CLASSIC",
|
GbcPalette.MODE_LABELS = { gbc = "GBC", 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 },
|
||||||
@@ -163,22 +162,11 @@ 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
|
||||||
@@ -261,10 +249,6 @@ 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
|
||||||
@@ -276,7 +260,7 @@ function GbcPalette.setMode(mode)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function GbcPalette.modeLabel(mode)
|
function GbcPalette.modeLabel(mode)
|
||||||
return GbcPalette.MODE_LABELS[mode or GbcPalette.mode] or "GEN 2"
|
return GbcPalette.MODE_LABELS[mode or GbcPalette.mode] or "GBC"
|
||||||
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
|
||||||
@@ -289,20 +273,11 @@ 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)
|
||||||
local mode = GbcPalette.setMode(opts and opts.color or "gbc")
|
return 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
|
||||||
@@ -456,14 +431,4 @@ 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
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
-- 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
|
|
||||||
@@ -1,249 +0,0 @@
|
|||||||
|
|
||||||
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
|
|
||||||
+22
-50
@@ -797,18 +797,6 @@ 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
|
||||||
@@ -820,33 +808,25 @@ function PaletteFX.setMode(mode)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
if not ok then PaletteFX.mode = "gbc" end
|
if not ok then PaletteFX.mode = "gbc" end
|
||||||
if prev ~= PaletteFX.mode then invalidateColorCaches() end
|
-- battle pics and overworld sprites bake the active pack into ImageData;
|
||||||
end
|
-- drop those caches when the pack (or any COLORS mode) changes so the
|
||||||
|
-- next draw re-tints
|
||||||
PaletteFX.customRamp = nil
|
if prev ~= PaletteFX.mode then
|
||||||
|
pcall(function() require("src.battle.BattleState").invalidate() end)
|
||||||
function PaletteFX.setCustomRamp(ramp)
|
pcall(function() require("src.render.SpriteRenderer").invalidate() end)
|
||||||
local prev = PaletteFX.customRamp
|
-- RED++'s baked tileset atlas (TileRenderer.getGbcAtlas) is built once
|
||||||
PaletteFX.customRamp = ramp
|
-- per loaded map, so a mode toggle needs every cached Map/TileRenderer
|
||||||
if (prev ~= nil) ~= (ramp ~= nil) or prev ~= ramp then
|
-- dropped and the currently-visible one rebuilt in place -- otherwise
|
||||||
invalidateColorCaches()
|
-- 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
|
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
|
end
|
||||||
|
|
||||||
function PaletteFX.cycleMode()
|
function PaletteFX.cycleMode()
|
||||||
@@ -861,13 +841,6 @@ 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)
|
||||||
@@ -880,11 +853,12 @@ 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"
|
if mode == "og" or mode == "og_inv" or mode == "classic" then
|
||||||
or (PaletteFX.forcesRawGrays() and not PaletteFX.pickerActive()) then
|
|
||||||
return { PaletteFX.whole(PaletteFX.GRAYS) }
|
return { PaletteFX.whole(PaletteFX.GRAYS) }
|
||||||
end
|
end
|
||||||
return zones
|
return zones
|
||||||
@@ -914,9 +888,7 @@ 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 PaletteFX.customRamp and not PaletteFX.pickerActive() then
|
if mode == "og" 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)
|
||||||
|
|||||||
@@ -332,7 +332,6 @@ end
|
|||||||
|
|
||||||
-- transparent: the world pass shows through (UI pass draws overlays only)
|
-- transparent: the world pass shows through (UI pass draws overlays only)
|
||||||
function Renderer:beginFrame(transparent)
|
function Renderer:beginFrame(transparent)
|
||||||
self.uiOpaque = not transparent
|
|
||||||
self.worldActive = false
|
self.worldActive = false
|
||||||
self.uprightActive = false
|
self.uprightActive = false
|
||||||
self.worldOverride = nil
|
self.worldOverride = nil
|
||||||
@@ -700,18 +699,6 @@ function Renderer:blitCanvas(canvas, sx, sy, zoneList, zoneSx, zoneSy,
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
love.graphics.setShader(shader)
|
love.graphics.setShader(shader)
|
||||||
-- data/sgb/sgb_packets.asm:123-127: zone 1 is the whole-screen ATTR_BLK,
|
|
||||||
-- so the underpaint is only drawn when it leaves part of the box bare (#1866)
|
|
||||||
local first = zoneList[1]
|
|
||||||
if canvas == self.canvas and self.uiOpaque and first.colors
|
|
||||||
and not (bx + first.x * zoneSx <= boxX
|
|
||||||
and by + first.y * zoneSy <= boxY
|
|
||||||
and bx + (first.x + first.w) * zoneSx >= boxX + boxW
|
|
||||||
and by + (first.y + first.h) * zoneSy >= boxY + boxH) then
|
|
||||||
PaletteFX.sendColors(shader, first.colors)
|
|
||||||
love.graphics.setScissor(boxX, boxY, boxW, boxH)
|
|
||||||
love.graphics.draw(canvas, bx, by, 0, sx, sy)
|
|
||||||
end
|
|
||||||
-- a colors == false zone is the trueColor opt-out: its rect draws with
|
-- a colors == false zone is the trueColor opt-out: its rect draws with
|
||||||
-- no shader at all. Nothing sets one without a mod, so a vanilla zone
|
-- no shader at all. Nothing sets one without a mod, so a vanilla zone
|
||||||
-- list never toggles and issues exactly the calls it always did.
|
-- list never toggles and issues exactly the calls it always did.
|
||||||
@@ -985,13 +972,6 @@ 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)
|
||||||
|
|||||||
+3
-118
@@ -11,12 +11,6 @@ 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
|
||||||
@@ -28,52 +22,6 @@ local MAX_COLS = 18
|
|||||||
-- pokegold constants/ram_constants.asm: TEXT_DELAY_FAST/MED/SLOW = 1/3/5
|
-- pokegold constants/ram_constants.asm: TEXT_DELAY_FAST/MED/SLOW = 1/3/5
|
||||||
local NAME_DELAYS = { FAST = 1, MID = 3, SLOW = 5 }
|
local NAME_DELAYS = { FAST = 1, MID = 3, SLOW = 5 }
|
||||||
|
|
||||||
-- TextCommand_PAUSE (home/text.asm:492-504): a mid-string wait callers
|
|
||||||
-- embed as TextBox.PAUSE, stripped here and re-anchored after pagination.
|
|
||||||
TextBox.PAUSE = "\1"
|
|
||||||
local PAUSE_FRAMES = 30
|
|
||||||
|
|
||||||
local function glyphs(str)
|
|
||||||
return #Font.split((str:gsub("[\n\v\f]", "")))
|
|
||||||
end
|
|
||||||
|
|
||||||
local function stripPauses(text)
|
|
||||||
if not text:find(TextBox.PAUSE, 1, true) then return text, nil end
|
|
||||||
local out, marks, count, pos = {}, {}, 0, 1
|
|
||||||
while true do
|
|
||||||
local i = text:find(TextBox.PAUSE, pos, true)
|
|
||||||
if not i then
|
|
||||||
out[#out + 1] = text:sub(pos)
|
|
||||||
break
|
|
||||||
end
|
|
||||||
local chunk = text:sub(pos, i - 1)
|
|
||||||
out[#out + 1] = chunk
|
|
||||||
count = count + glyphs(chunk)
|
|
||||||
marks[#marks + 1] = count
|
|
||||||
pos = i + 1
|
|
||||||
end
|
|
||||||
return table.concat(out), marks
|
|
||||||
end
|
|
||||||
|
|
||||||
-- glyph offsets into the whole text -> [page][line][char]
|
|
||||||
local function mapPauses(pages, marks)
|
|
||||||
local at, acc, mi = {}, 0, 1
|
|
||||||
for pi, page in ipairs(pages) do
|
|
||||||
for li, line in ipairs(page) do
|
|
||||||
local n = #Font.split(line)
|
|
||||||
while mi <= #marks and marks[mi] <= acc + n do
|
|
||||||
local ci = marks[mi] - acc
|
|
||||||
at[pi] = at[pi] or {}
|
|
||||||
at[pi][li] = at[pi][li] or {}
|
|
||||||
at[pi][li][ci] = mi
|
|
||||||
mi = mi + 1
|
|
||||||
end
|
|
||||||
acc = acc + n
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return at
|
|
||||||
end
|
|
||||||
|
|
||||||
-- opts.choice: when the last page has typed out, a YES/NO ChoiceBox pops
|
-- opts.choice: when the last page has typed out, a YES/NO ChoiceBox pops
|
||||||
-- up over the still-visible text (YesNoChoicePokeCenter and friends);
|
-- up over the still-visible text (YesNoChoicePokeCenter and friends);
|
||||||
-- the box then closes and choice(yes) runs instead of onDone.
|
-- the box then closes and choice(yes) runs instead of onDone.
|
||||||
@@ -112,8 +60,6 @@ function TextBox.new(game, text, onDone, opts)
|
|||||||
self.stay = opts and opts.stay
|
self.stay = opts and opts.stay
|
||||||
-- engine/events/hidden_events/cinnabar_gym_quiz.asm:119
|
-- engine/events/hidden_events/cinnabar_gym_quiz.asm:119
|
||||||
self.preSound = opts and opts.preSound
|
self.preSound = opts and opts.preSound
|
||||||
-- pokegold engine/overworld/scripting.asm:485 WaitSFX
|
|
||||||
self.sfxWait = opts and opts.sfxWait
|
|
||||||
-- opts.instant: put the LAST page up already typed, with no typewriter and
|
-- opts.instant: put the LAST page up already typed, with no typewriter and
|
||||||
-- no page waits. A `yesorno` follows a `writetext` that has already been
|
-- no page waits. A `yesorno` follows a `writetext` that has already been
|
||||||
-- read, so re-typing the line under the YES/NO box would be wrong -- the
|
-- read, so re-typing the line under the YES/NO box would be wrong -- the
|
||||||
@@ -129,13 +75,7 @@ function TextBox.new(game, text, onDone, opts)
|
|||||||
self.line1Y = (self.boxTy + 2) * 8
|
self.line1Y = (self.boxTy + 2) * 8
|
||||||
self.line2Y = (self.boxTy + 4) * 8
|
self.line2Y = (self.boxTy + 4) * 8
|
||||||
text = TextBox.substitute(game, text)
|
text = TextBox.substitute(game, text)
|
||||||
local marks
|
|
||||||
text, marks = stripPauses(text)
|
|
||||||
self.pages = TextBox.paginate(text, self.maxCols)
|
self.pages = TextBox.paginate(text, self.maxCols)
|
||||||
-- opts.pauseSounds[i] is the sfx the i-th marker fires once its wait is
|
|
||||||
-- over (text_asm SFX_SWAP, engine/pokemon/learn_move.asm:210-213)
|
|
||||||
self.pauseSounds = opts and opts.pauseSounds
|
|
||||||
self.pauseAt = marks and mapPauses(self.pages, marks) or nil
|
|
||||||
self.pageIndex = 1
|
self.pageIndex = 1
|
||||||
self.lineIndex = 1
|
self.lineIndex = 1
|
||||||
self.charIndex = 0
|
self.charIndex = 0
|
||||||
@@ -330,14 +270,6 @@ function TextBox:visibleText()
|
|||||||
return #out > 0 and out or nil
|
return #out > 0 and out or nil
|
||||||
end
|
end
|
||||||
|
|
||||||
-- pokegold engine/overworld/scripting.asm:484-485 PlaySFX / WaitSFX
|
|
||||||
function TextBox:sfxHeld()
|
|
||||||
if not self.sfxWait then return false end
|
|
||||||
if require("src.core.Sound").sfxBusy() then return true end
|
|
||||||
self.sfxWait = nil
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
function TextBox:update(dt)
|
function TextBox:update(dt)
|
||||||
local input = self.game.input
|
local input = self.game.input
|
||||||
self.blink = (self.blink + 1) % 60
|
self.blink = (self.blink + 1) % 60
|
||||||
@@ -359,20 +291,6 @@ function TextBox:update(dt)
|
|||||||
self.holdFrames = self.holdFrames - 1
|
self.holdFrames = self.holdFrames - 1
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
-- home/text.asm:492
|
|
||||||
if self.pauseFrames then
|
|
||||||
if self.pauseFrames > 0 then
|
|
||||||
self.pauseFrames = self.pauseFrames - 1
|
|
||||||
return
|
|
||||||
end
|
|
||||||
self.pauseFrames = nil
|
|
||||||
local snd = self.pauseSounds and self.pauseSounds[self.pauseMark]
|
|
||||||
if type(snd) == "function" then
|
|
||||||
snd()
|
|
||||||
elseif snd then
|
|
||||||
require("src.core.Sound").play(self.game.data, snd)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if self.done then
|
if self.done then
|
||||||
-- opts.stay: the box is finished but stays up under whatever the caller
|
-- opts.stay: the box is finished but stays up under whatever the caller
|
||||||
-- pushed over it; StateStack updates the top state only, so this runs
|
-- pushed over it; StateStack updates the top state only, so this runs
|
||||||
@@ -454,7 +372,6 @@ function TextBox:update(dt)
|
|||||||
end
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
if self:sfxHeld() then return end
|
|
||||||
if input:wasPressed("a") or input:wasPressed("b") then
|
if input:wasPressed("a") or input:wasPressed("b") then
|
||||||
require("src.core.Sound").play(self.game.data, "Press_AB")
|
require("src.core.Sound").play(self.game.data, "Press_AB")
|
||||||
self.game.stack:pop()
|
self.game.stack:pop()
|
||||||
@@ -470,7 +387,6 @@ function TextBox:update(dt)
|
|||||||
self.preWait = self.preWait - 1
|
self.preWait = self.preWait - 1
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
if self:sfxHeld() then return end
|
|
||||||
if input:wasPressed("a") or input:wasPressed("b") then
|
if input:wasPressed("a") or input:wasPressed("b") then
|
||||||
require("src.core.Sound").play(self.game.data, "Press_AB")
|
require("src.core.Sound").play(self.game.data, "Press_AB")
|
||||||
self.waiting = false
|
self.waiting = false
|
||||||
@@ -508,15 +424,6 @@ function TextBox:update(dt)
|
|||||||
self.charIndex = self.charIndex + 1
|
self.charIndex = self.charIndex + 1
|
||||||
local line = self.shown[#self.shown]
|
local line = self.shown[#self.shown]
|
||||||
line[#line + 1] = self.codes[self.charIndex]
|
line[#line + 1] = self.codes[self.charIndex]
|
||||||
local marks = self.pauseAt and self.pauseAt[self.pageIndex]
|
|
||||||
marks = marks and marks[self.lineIndex]
|
|
||||||
if marks and marks[self.charIndex] then
|
|
||||||
self.pauseMark = marks[self.charIndex]
|
|
||||||
-- TextCommand_PAUSE reads hJoyHeld, so a held A/B skips the wait
|
|
||||||
self.pauseFrames = (input:isDown("a") or input:isDown("b"))
|
|
||||||
and 0 or PAUSE_FRAMES
|
|
||||||
break
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
-- line finished
|
-- line finished
|
||||||
local page = self.pages[self.pageIndex]
|
local page = self.pages[self.pageIndex]
|
||||||
@@ -563,21 +470,8 @@ 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
|
||||||
@@ -597,25 +491,17 @@ 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
|
||||||
drawGlyph(code, pen, y)
|
Font.drawCode(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)
|
||||||
local pen = 152 - Font.width(money)
|
Font.draw(money, 152 - Font.width(money), 8)
|
||||||
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
|
||||||
@@ -623,11 +509,10 @@ 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)
|
||||||
drawGlyph(Theme.moreArrow or 0xEE,
|
Font.drawCode(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
|
||||||
|
|
||||||
|
|||||||
@@ -150,10 +150,13 @@ end
|
|||||||
|
|
||||||
-- true while the spinner arrow tiles should show the 'blur' graphic; false
|
-- true while the spinner arrow tiles should show the 'blur' graphic; false
|
||||||
-- means draw nothing extra (the static window tile shows through,
|
-- means draw nothing extra (the static window tile shows through,
|
||||||
-- matching the asm's restore-to-original behavior).
|
-- matching the asm's restore-to-original behavior). The 8-tick
|
||||||
-- spinners.asm:17-22, home/overworld.asm:1844-1846, :49-52
|
-- half-period approximates one GB movement step (2px/frame); this is a
|
||||||
|
-- deliberate approximation of wSimulatedJoypadStatesIndex bit-0 parity, not
|
||||||
|
-- a cycle-accurate replication -- the port's tweened scriptMove has no
|
||||||
|
-- direct equivalent discrete step counter.
|
||||||
function TileRenderer.spinBlurActive()
|
function TileRenderer.spinBlurActive()
|
||||||
return spinning and (math.floor(animFrame / 16) % 2 == 0)
|
return spinning and (math.floor(animFrame / 8) % 2 == 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ------------------------------------------------------------------
|
-- ------------------------------------------------------------------
|
||||||
|
|||||||
@@ -1,335 +0,0 @@
|
|||||||
-- GENERATED by tools/gen2_sram_offsets.py. Do not edit by hand.
|
|
||||||
-- Regenerate from a pret/pokegold + pret/pokecrystal build; see that
|
|
||||||
-- script's header for the derivation and the assertion behind it.
|
|
||||||
local Gen2Layout = {}
|
|
||||||
|
|
||||||
Gen2Layout.goldSilver = {
|
|
||||||
sCheckValue1 = 0x2008,
|
|
||||||
sCheckValue2 = 0x2D6B,
|
|
||||||
sChecksum = 0x2D69,
|
|
||||||
sGameData = 0x2009,
|
|
||||||
sGameDataEnd = 0x2D69,
|
|
||||||
wPlayerName = 0x200B,
|
|
||||||
wPlayerID = 0x2009,
|
|
||||||
wMoney = 0x23DB,
|
|
||||||
wCoins = 0x23E2,
|
|
||||||
wBadges = 0x23E4,
|
|
||||||
wKantoBadges = 0x23E5,
|
|
||||||
wRivalName = 0x2021,
|
|
||||||
wMomsName = 0x2016,
|
|
||||||
wPartyCount = 0x288A,
|
|
||||||
wPartySpecies = 0x288B,
|
|
||||||
wPartyMons = 0x2892,
|
|
||||||
wPartyMonNicknames = 0x29F4,
|
|
||||||
wPartyMonOTs = 0x29B2,
|
|
||||||
wNumItems = 0x241F,
|
|
||||||
wItems = 0x2420,
|
|
||||||
wNumKeyItems = 0x2449,
|
|
||||||
wKeyItems = 0x244A,
|
|
||||||
wNumBalls = 0x2464,
|
|
||||||
wBalls = 0x2465,
|
|
||||||
wTMsHMs = 0x23E6,
|
|
||||||
wPokedexCaught = 0x2A4C,
|
|
||||||
wPokedexSeen = 0x2A6C,
|
|
||||||
wCurBox = 0x2724,
|
|
||||||
wBoxNames = 0x2727,
|
|
||||||
wMapGroup = 0x2868,
|
|
||||||
wMapNumber = 0x2869,
|
|
||||||
wXCoord = 0x286B,
|
|
||||||
wYCoord = 0x286A,
|
|
||||||
wEventFlags = 0x261F,
|
|
||||||
wPlayerState = 0x24EA,
|
|
||||||
wGameTimeHours = 0x2053,
|
|
||||||
wGameTimeMinutes = 0x2055,
|
|
||||||
-- The 14 archived boxes, listed rather than strided (see BOX_COUNT).
|
|
||||||
boxes = { 0x4000, 0x4450, 0x48A0, 0x4CF0, 0x5140, 0x5590, 0x59E0, 0x6000, 0x6450, 0x68A0, 0x6CF0, 0x7140, 0x7590, 0x79E0 },
|
|
||||||
}
|
|
||||||
|
|
||||||
Gen2Layout.crystal = {
|
|
||||||
sCheckValue1 = 0x2008,
|
|
||||||
sCheckValue2 = 0x2D0F,
|
|
||||||
sChecksum = 0x2D0D,
|
|
||||||
sGameData = 0x2009,
|
|
||||||
sGameDataEnd = 0x2B83,
|
|
||||||
wPlayerName = 0x200B,
|
|
||||||
wPlayerID = 0x2009,
|
|
||||||
wMoney = 0x23DC,
|
|
||||||
wCoins = 0x23E3,
|
|
||||||
wBadges = 0x23E5,
|
|
||||||
wKantoBadges = 0x23E6,
|
|
||||||
wRivalName = 0x2021,
|
|
||||||
wMomsName = 0x2016,
|
|
||||||
wPartyCount = 0x2865,
|
|
||||||
wPartySpecies = 0x2866,
|
|
||||||
wPartyMons = 0x286D,
|
|
||||||
wPartyMonNicknames = 0x29CF,
|
|
||||||
wPartyMonOTs = 0x298D,
|
|
||||||
wNumItems = 0x2420,
|
|
||||||
wItems = 0x2421,
|
|
||||||
wNumKeyItems = 0x244A,
|
|
||||||
wKeyItems = 0x244B,
|
|
||||||
wNumBalls = 0x2465,
|
|
||||||
wBalls = 0x2466,
|
|
||||||
wTMsHMs = 0x23E7,
|
|
||||||
wPokedexCaught = 0x2A27,
|
|
||||||
wPokedexSeen = 0x2A47,
|
|
||||||
wCurBox = 0x2700,
|
|
||||||
wBoxNames = 0x2703,
|
|
||||||
wMapGroup = 0x2843,
|
|
||||||
wMapNumber = 0x2844,
|
|
||||||
wXCoord = 0x2846,
|
|
||||||
wYCoord = 0x2845,
|
|
||||||
wEventFlags = 0x2600,
|
|
||||||
wPlayerState = 0x24EB,
|
|
||||||
wGameTimeHours = 0x2052,
|
|
||||||
wGameTimeMinutes = 0x2054,
|
|
||||||
-- The 14 archived boxes, listed rather than strided (see BOX_COUNT).
|
|
||||||
boxes = { 0x4000, 0x4450, 0x48A0, 0x4CF0, 0x5140, 0x5590, 0x59E0, 0x6000, 0x6450, 0x68A0, 0x6CF0, 0x7140, 0x7590, 0x79E0 },
|
|
||||||
-- The backup copy the game falls back to when the primary
|
|
||||||
-- checksum fails. Same shape, shifted.
|
|
||||||
backup = {
|
|
||||||
sCheckValue1 = 0x1208,
|
|
||||||
sCheckValue2 = 0x1F0F,
|
|
||||||
sChecksum = 0x1F0D,
|
|
||||||
sGameData = 0x1209,
|
|
||||||
sGameDataEnd = 0x1D83,
|
|
||||||
wPlayerName = 0x120B,
|
|
||||||
wPlayerID = 0x1209,
|
|
||||||
wMoney = 0x15DC,
|
|
||||||
wCoins = 0x15E3,
|
|
||||||
wBadges = 0x15E5,
|
|
||||||
wKantoBadges = 0x15E6,
|
|
||||||
wRivalName = 0x1221,
|
|
||||||
wMomsName = 0x1216,
|
|
||||||
wPartyCount = 0x1A65,
|
|
||||||
wPartySpecies = 0x1A66,
|
|
||||||
wPartyMons = 0x1A6D,
|
|
||||||
wPartyMonNicknames = 0x1BCF,
|
|
||||||
wPartyMonOTs = 0x1B8D,
|
|
||||||
wNumItems = 0x1620,
|
|
||||||
wItems = 0x1621,
|
|
||||||
wNumKeyItems = 0x164A,
|
|
||||||
wKeyItems = 0x164B,
|
|
||||||
wNumBalls = 0x1665,
|
|
||||||
wBalls = 0x1666,
|
|
||||||
wTMsHMs = 0x15E7,
|
|
||||||
wPokedexCaught = 0x1C27,
|
|
||||||
wPokedexSeen = 0x1C47,
|
|
||||||
wCurBox = 0x1900,
|
|
||||||
wBoxNames = 0x1903,
|
|
||||||
wMapGroup = 0x1A43,
|
|
||||||
wMapNumber = 0x1A44,
|
|
||||||
wXCoord = 0x1A46,
|
|
||||||
wYCoord = 0x1A45,
|
|
||||||
wEventFlags = 0x1800,
|
|
||||||
wPlayerState = 0x16EB,
|
|
||||||
wGameTimeHours = 0x1252,
|
|
||||||
wGameTimeMinutes = 0x1254,
|
|
||||||
boxes = { 0x4000, 0x4450, 0x48A0, 0x4CF0, 0x5140, 0x5590, 0x59E0, 0x6000, 0x6450, 0x68A0, 0x6CF0, 0x7140, 0x7590, 0x79E0 },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
Gen2Layout.charmap = {
|
|
||||||
[0x05] = "ガ",
|
|
||||||
[0x06] = "ギ",
|
|
||||||
[0x07] = "グ",
|
|
||||||
[0x08] = "ゲ",
|
|
||||||
[0x09] = "ゴ",
|
|
||||||
[0x0A] = "ザ",
|
|
||||||
[0x0B] = "ジ",
|
|
||||||
[0x0C] = "ズ",
|
|
||||||
[0x0D] = "ゼ",
|
|
||||||
[0x0E] = "ゾ",
|
|
||||||
[0x0F] = "ダ",
|
|
||||||
[0x10] = "ヂ",
|
|
||||||
[0x11] = "ヅ",
|
|
||||||
[0x12] = "デ",
|
|
||||||
[0x13] = "ド",
|
|
||||||
[0x19] = "バ",
|
|
||||||
[0x1A] = "ビ",
|
|
||||||
[0x1B] = "ブ",
|
|
||||||
[0x1C] = "ボ",
|
|
||||||
[0x26] = "が",
|
|
||||||
[0x27] = "ぎ",
|
|
||||||
[0x28] = "ぐ",
|
|
||||||
[0x29] = "げ",
|
|
||||||
[0x2A] = "ご",
|
|
||||||
[0x2B] = "ざ",
|
|
||||||
[0x2C] = "じ",
|
|
||||||
[0x2D] = "ず",
|
|
||||||
[0x2E] = "ぜ",
|
|
||||||
[0x2F] = "ぞ",
|
|
||||||
[0x30] = "だ",
|
|
||||||
[0x31] = "ぢ",
|
|
||||||
[0x32] = "づ",
|
|
||||||
[0x33] = "で",
|
|
||||||
[0x34] = "ど",
|
|
||||||
[0x3A] = "ば",
|
|
||||||
[0x3B] = "び",
|
|
||||||
[0x3C] = "ぶ",
|
|
||||||
[0x3D] = "べ",
|
|
||||||
[0x3E] = "ぼ",
|
|
||||||
[0x3F] = "⁂",
|
|
||||||
[0x40] = "パ",
|
|
||||||
[0x41] = "ピ",
|
|
||||||
[0x42] = "プ",
|
|
||||||
[0x43] = "ポ",
|
|
||||||
[0x44] = "ぱ",
|
|
||||||
[0x45] = "ぴ",
|
|
||||||
[0x46] = "ぷ",
|
|
||||||
[0x47] = "ぺ",
|
|
||||||
[0x48] = "ぽ",
|
|
||||||
[0x50] = "@",
|
|
||||||
[0x54] = "#",
|
|
||||||
[0x60] = "■",
|
|
||||||
[0x61] = "▲",
|
|
||||||
[0x62] = "☎",
|
|
||||||
[0x6E] = "ぃ",
|
|
||||||
[0x6F] = "ぅ",
|
|
||||||
[0x70] = "PO",
|
|
||||||
[0x71] = "KE",
|
|
||||||
[0x72] = "“",
|
|
||||||
[0x73] = "”",
|
|
||||||
[0x74] = "·",
|
|
||||||
[0x75] = "…",
|
|
||||||
[0x76] = "ぁ",
|
|
||||||
[0x77] = "ぇ",
|
|
||||||
[0x78] = "ぉ",
|
|
||||||
[0x79] = "┌",
|
|
||||||
[0x7A] = "─",
|
|
||||||
[0x7B] = "┐",
|
|
||||||
[0x7C] = "│",
|
|
||||||
[0x7D] = "└",
|
|
||||||
[0x7E] = "┘",
|
|
||||||
[0x7F] = " ",
|
|
||||||
[0x80] = "A",
|
|
||||||
[0x81] = "B",
|
|
||||||
[0x82] = "C",
|
|
||||||
[0x83] = "D",
|
|
||||||
[0x84] = "E",
|
|
||||||
[0x85] = "F",
|
|
||||||
[0x86] = "G",
|
|
||||||
[0x87] = "H",
|
|
||||||
[0x88] = "I",
|
|
||||||
[0x89] = "J",
|
|
||||||
[0x8A] = "K",
|
|
||||||
[0x8B] = "L",
|
|
||||||
[0x8C] = "M",
|
|
||||||
[0x8D] = "N",
|
|
||||||
[0x8E] = "O",
|
|
||||||
[0x8F] = "P",
|
|
||||||
[0x90] = "Q",
|
|
||||||
[0x91] = "R",
|
|
||||||
[0x92] = "S",
|
|
||||||
[0x93] = "T",
|
|
||||||
[0x94] = "U",
|
|
||||||
[0x95] = "V",
|
|
||||||
[0x96] = "W",
|
|
||||||
[0x97] = "X",
|
|
||||||
[0x98] = "Y",
|
|
||||||
[0x99] = "Z",
|
|
||||||
[0x9A] = "(",
|
|
||||||
[0x9B] = ")",
|
|
||||||
[0x9C] = ":",
|
|
||||||
[0x9D] = ";",
|
|
||||||
[0x9E] = "[",
|
|
||||||
[0x9F] = "]",
|
|
||||||
[0xA0] = "a",
|
|
||||||
[0xA1] = "b",
|
|
||||||
[0xA2] = "c",
|
|
||||||
[0xA3] = "d",
|
|
||||||
[0xA4] = "e",
|
|
||||||
[0xA5] = "f",
|
|
||||||
[0xA6] = "g",
|
|
||||||
[0xA7] = "h",
|
|
||||||
[0xA8] = "i",
|
|
||||||
[0xA9] = "j",
|
|
||||||
[0xAA] = "k",
|
|
||||||
[0xAB] = "l",
|
|
||||||
[0xAC] = "m",
|
|
||||||
[0xAD] = "n",
|
|
||||||
[0xAE] = "o",
|
|
||||||
[0xAF] = "p",
|
|
||||||
[0xB0] = "q",
|
|
||||||
[0xB1] = "r",
|
|
||||||
[0xB2] = "s",
|
|
||||||
[0xB3] = "t",
|
|
||||||
[0xB4] = "u",
|
|
||||||
[0xB5] = "v",
|
|
||||||
[0xB6] = "w",
|
|
||||||
[0xB7] = "x",
|
|
||||||
[0xB8] = "y",
|
|
||||||
[0xB9] = "z",
|
|
||||||
[0xBA] = "こ",
|
|
||||||
[0xBB] = "さ",
|
|
||||||
[0xBC] = "し",
|
|
||||||
[0xBD] = "す",
|
|
||||||
[0xBE] = "せ",
|
|
||||||
[0xBF] = "そ",
|
|
||||||
[0xC0] = "Ä",
|
|
||||||
[0xC1] = "Ö",
|
|
||||||
[0xC2] = "Ü",
|
|
||||||
[0xC3] = "ä",
|
|
||||||
[0xC4] = "ö",
|
|
||||||
[0xC5] = "ü",
|
|
||||||
[0xC6] = "に",
|
|
||||||
[0xC7] = "ぬ",
|
|
||||||
[0xC8] = "ね",
|
|
||||||
[0xC9] = "の",
|
|
||||||
[0xCA] = "は",
|
|
||||||
[0xCB] = "ひ",
|
|
||||||
[0xCC] = "ふ",
|
|
||||||
[0xCD] = "へ",
|
|
||||||
[0xCE] = "ほ",
|
|
||||||
[0xCF] = "ま",
|
|
||||||
[0xD0] = "'d",
|
|
||||||
[0xD1] = "'l",
|
|
||||||
[0xD2] = "'m",
|
|
||||||
[0xD3] = "'r",
|
|
||||||
[0xD4] = "'s",
|
|
||||||
[0xD5] = "'t",
|
|
||||||
[0xD6] = "'v",
|
|
||||||
[0xD7] = "ら",
|
|
||||||
[0xD8] = "り",
|
|
||||||
[0xD9] = "る",
|
|
||||||
[0xDA] = "れ",
|
|
||||||
[0xDB] = "ろ",
|
|
||||||
[0xDC] = "わ",
|
|
||||||
[0xDD] = "を",
|
|
||||||
[0xDE] = "ん",
|
|
||||||
[0xDF] = "←",
|
|
||||||
[0xE0] = "'",
|
|
||||||
[0xE1] = "PK",
|
|
||||||
[0xE2] = "MN",
|
|
||||||
[0xE3] = "-",
|
|
||||||
[0xE4] = "゚",
|
|
||||||
[0xE5] = "゙",
|
|
||||||
[0xE6] = "?",
|
|
||||||
[0xE7] = "!",
|
|
||||||
[0xE8] = ".",
|
|
||||||
[0xE9] = "&",
|
|
||||||
[0xEA] = "é",
|
|
||||||
[0xEB] = "→",
|
|
||||||
[0xEC] = "▷",
|
|
||||||
[0xED] = "▶",
|
|
||||||
[0xEE] = "▼",
|
|
||||||
[0xEF] = "♂",
|
|
||||||
[0xF0] = "¥",
|
|
||||||
[0xF1] = "×",
|
|
||||||
[0xF2] = ".",
|
|
||||||
[0xF3] = "/",
|
|
||||||
[0xF4] = ",",
|
|
||||||
[0xF5] = "♀",
|
|
||||||
[0xF6] = "0",
|
|
||||||
[0xF7] = "1",
|
|
||||||
[0xF8] = "2",
|
|
||||||
[0xF9] = "3",
|
|
||||||
[0xFA] = "4",
|
|
||||||
[0xFB] = "5",
|
|
||||||
[0xFC] = "6",
|
|
||||||
[0xFD] = "7",
|
|
||||||
[0xFE] = "8",
|
|
||||||
[0xFF] = "9",
|
|
||||||
}
|
|
||||||
|
|
||||||
return Gen2Layout
|
|
||||||
@@ -1,690 +0,0 @@
|
|||||||
-- Vanilla Gen 2 (Gold/Silver/Crystal) raw SRAM <-> src/core/gen2/Save.lua.
|
|
||||||
-- Companion to GenSave.lua, which is Gen 1 only.
|
|
||||||
--
|
|
||||||
-- Offsets come from Gen2Layout.lua, generated by tools/gen2_sram_offsets.py.
|
|
||||||
-- Gold and Silver share a layout; Crystal does not.
|
|
||||||
--
|
|
||||||
-- Pure Lua, no love.* at require time, same as GenSave.
|
|
||||||
|
|
||||||
local Gen2Layout = require("src.save_convert.Gen2Layout")
|
|
||||||
|
|
||||||
local Gen2Save = {}
|
|
||||||
|
|
||||||
Gen2Save.SAVE_SIZE = 32768
|
|
||||||
Gen2Save.PARTY_STRUCT = 48
|
|
||||||
Gen2Save.NAME_LENGTH = 11
|
|
||||||
Gen2Save.PARTY_LENGTH = 6
|
|
||||||
|
|
||||||
function Gen2Save.layoutFor(gameVersion)
|
|
||||||
if gameVersion == "crystal" then return Gen2Layout.crystal end
|
|
||||||
if gameVersion == "gold" or gameVersion == "silver" then return Gen2Layout.goldSilver end
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local function u8(b, o) return b:byte(o + 1) end
|
|
||||||
local function be(b, o, n)
|
|
||||||
local v = 0
|
|
||||||
for i = 0, n - 1 do v = v * 256 + b:byte(o + i + 1) end
|
|
||||||
return v
|
|
||||||
end
|
|
||||||
|
|
||||||
-- The cart's text encoding, from Gen2Layout.charmap. 0x50 terminates.
|
|
||||||
local function text(b, o, n)
|
|
||||||
local out = {}
|
|
||||||
for i = 0, n - 1 do
|
|
||||||
local c = b:byte(o + i + 1)
|
|
||||||
if not c or c == 0x50 then break end
|
|
||||||
out[#out + 1] = Gen2Layout.charmap[c] or "?"
|
|
||||||
end
|
|
||||||
return table.concat(out)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Both check values AND the 16-bit sum between them. A blank SRAM sums to a
|
|
||||||
-- valid 0 == 0, so the check values are what reject it.
|
|
||||||
function Gen2Save.checksumValid(bytes, L)
|
|
||||||
if #bytes < Gen2Save.SAVE_SIZE then return nil end
|
|
||||||
if u8(bytes, L.sCheckValue1) ~= 0x63 then return false end
|
|
||||||
if u8(bytes, L.sCheckValue2) ~= 0x7F then return false end
|
|
||||||
local sum = 0
|
|
||||||
for i = L.sGameData, L.sGameDataEnd - 1 do sum = (sum + u8(bytes, i)) % 65536 end
|
|
||||||
local stored = u8(bytes, L.sChecksum) + u8(bytes, L.sChecksum + 1) * 256
|
|
||||||
return sum == stored
|
|
||||||
end
|
|
||||||
|
|
||||||
-- The cart stores numbers, the engine is keyed by name. Same shape
|
|
||||||
-- GenSave.crosswalks builds for Gen 1. Without `data` the raw numbers survive.
|
|
||||||
local function byIndex(defs)
|
|
||||||
local out = {}
|
|
||||||
for id, def in pairs(defs or {}) do
|
|
||||||
if type(def) == "table" and def.index ~= nil then out[def.index] = id end
|
|
||||||
end
|
|
||||||
return out
|
|
||||||
end
|
|
||||||
|
|
||||||
local function toIndex(defs)
|
|
||||||
local out = {}
|
|
||||||
for id, def in pairs(defs or {}) do
|
|
||||||
if type(def) == "table" and def.index ~= nil then out[id] = def.index end
|
|
||||||
end
|
|
||||||
return out
|
|
||||||
end
|
|
||||||
|
|
||||||
function Gen2Save.crosswalks(data)
|
|
||||||
data = data or {}
|
|
||||||
local items = byIndex(data.items)
|
|
||||||
-- Maps are found by (group, number) rather than by a flat index.
|
|
||||||
local maps = {}
|
|
||||||
for id, def in pairs(data.maps or {}) do
|
|
||||||
if type(def) == "table" and def.group and def.map then
|
|
||||||
maps[def.group * 256 + def.map] = id
|
|
||||||
end
|
|
||||||
end
|
|
||||||
local mapIds = {}
|
|
||||||
for id, def in pairs(data.maps or {}) do
|
|
||||||
if type(def) == "table" and def.group and def.map then
|
|
||||||
mapIds[id] = { def.group, def.map }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return {
|
|
||||||
pokemon = byIndex(data.pokemon),
|
|
||||||
moves = byIndex(data.moves),
|
|
||||||
items = items,
|
|
||||||
maps = maps,
|
|
||||||
pokemonIndex = toIndex(data.pokemon),
|
|
||||||
moveIndex = toIndex(data.moves),
|
|
||||||
itemIndex = toIndex(data.items),
|
|
||||||
mapIds = mapIds,
|
|
||||||
itemDefs = data.items or {},
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Name if the crosswalk knows it, the raw number if not. Never silently drops
|
|
||||||
-- a value: an id this build cannot name is still the player's.
|
|
||||||
local function named(map, index)
|
|
||||||
if index == nil or index == 0 then return nil end
|
|
||||||
return map[index] or index
|
|
||||||
end
|
|
||||||
|
|
||||||
-- constants/battle_constants.asm: SLP_MASK is bits 0-2, PSN=3 BRN=4 FRZ=5
|
|
||||||
-- PAR=6. Engine wants an ItemEffects.STATUS_CLASS key, nil when healthy.
|
|
||||||
local STATUS_BITS = { { 3, "psn" }, { 4, "brn" }, { 5, "frz" }, { 6, "par" } }
|
|
||||||
|
|
||||||
local function encodeStatus(name, turns)
|
|
||||||
if name == "slp" then return math.min(math.max(turns or 1, 1), 7) end
|
|
||||||
for _, row in ipairs(STATUS_BITS) do
|
|
||||||
if row[2] == name then return 2 ^ row[1] end
|
|
||||||
end
|
|
||||||
return 0
|
|
||||||
end
|
|
||||||
|
|
||||||
local function decodeStatus(byte)
|
|
||||||
local turns = byte % 8
|
|
||||||
if turns > 0 then return "slp", turns end
|
|
||||||
for _, row in ipairs(STATUS_BITS) do
|
|
||||||
if math.floor(byte / 2 ^ row[1]) % 2 == 1 then return row[2], nil end
|
|
||||||
end
|
|
||||||
return nil, nil
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Four DVs as nibbles in two bytes; the HP DV is rebuilt from the low bit of
|
|
||||||
-- each (pokecrystal engine/pokemon/health.asm).
|
|
||||||
local function decodeDVs(bytes, o)
|
|
||||||
local hi, lo = u8(bytes, o), u8(bytes, o + 1)
|
|
||||||
local atk = math.floor(hi / 16)
|
|
||||||
local def = hi % 16
|
|
||||||
local spd = math.floor(lo / 16)
|
|
||||||
local spc = lo % 16
|
|
||||||
local hp = (atk % 2) * 8 + (def % 2) * 4 + (spd % 2) * 2 + (spc % 2)
|
|
||||||
return { hp = hp, attack = atk, defense = def, speed = spd, special = spc }
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Five 16-bit stat experience words, in the cart's order.
|
|
||||||
local function decodeStatExp(bytes, o)
|
|
||||||
return {
|
|
||||||
hp = be(bytes, o, 2), attack = be(bytes, o + 2, 2),
|
|
||||||
defense = be(bytes, o + 4, 2), speed = be(bytes, o + 6, 2),
|
|
||||||
special = be(bytes, o + 8, 2),
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
-- The first 32 bytes, which a box mon and a party mon share.
|
|
||||||
local function decodeSharedMon(bytes, o, x)
|
|
||||||
local moves, pp = {}, {}
|
|
||||||
for i = 0, 3 do
|
|
||||||
local id = named(x.moves, u8(bytes, o + 2 + i))
|
|
||||||
if id then moves[#moves + 1] = id end
|
|
||||||
end
|
|
||||||
for i = 0, 3 do pp[i + 1] = u8(bytes, o + 0x17 + i) % 64 end
|
|
||||||
return {
|
|
||||||
species = named(x.pokemon, u8(bytes, o)),
|
|
||||||
item = named(x.items, u8(bytes, o + 1)),
|
|
||||||
moves = moves, pp = pp,
|
|
||||||
otId = be(bytes, o + 6, 2),
|
|
||||||
experience = be(bytes, o + 8, 3),
|
|
||||||
statExp = decodeStatExp(bytes, o + 0x0B),
|
|
||||||
dvs = decodeDVs(bytes, o + 0x15),
|
|
||||||
happiness = u8(bytes, o + 0x1B),
|
|
||||||
pokerus = u8(bytes, o + 0x1C),
|
|
||||||
caughtData = be(bytes, o + 0x1D, 2),
|
|
||||||
level = u8(bytes, o + 0x1F),
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
local function decodeMon(bytes, o, x)
|
|
||||||
local mon = decodeSharedMon(bytes, o, x)
|
|
||||||
mon.status, mon.statusTurns = decodeStatus(u8(bytes, o + 0x20))
|
|
||||||
mon.hp = be(bytes, o + 0x22, 2)
|
|
||||||
mon.maxHp = be(bytes, o + 0x24, 2)
|
|
||||||
mon.stats = {
|
|
||||||
hp = mon.maxHp,
|
|
||||||
attack = be(bytes, o + 0x26, 2), defense = be(bytes, o + 0x28, 2),
|
|
||||||
speed = be(bytes, o + 0x2A, 2), specialAttack = be(bytes, o + 0x2C, 2),
|
|
||||||
specialDefense = be(bytes, o + 0x2E, 2),
|
|
||||||
}
|
|
||||||
return mon
|
|
||||||
end
|
|
||||||
|
|
||||||
-- box_struct: OTs come BEFORE nicknames, and a box mon is 32 bytes with none
|
|
||||||
-- of the party's computed stats.
|
|
||||||
Gen2Save.BOX_CAPACITY = 20
|
|
||||||
Gen2Save.BOX_MON_STRUCT = 32
|
|
||||||
local BOX_SPECIES, BOX_MONS, BOX_OTS, BOX_NICKS = 0x01, 0x16, 0x296, 0x372
|
|
||||||
|
|
||||||
local decodeBoxMon = decodeSharedMon
|
|
||||||
|
|
||||||
-- A count past BOX_CAPACITY means the wrong layout, not an odd save.
|
|
||||||
function Gen2Save.decodeBoxes(bytes, L, x)
|
|
||||||
local boxes = {}
|
|
||||||
for index, base in ipairs(L.boxes) do
|
|
||||||
local count = u8(bytes, base)
|
|
||||||
if count > Gen2Save.BOX_CAPACITY then
|
|
||||||
return nil, ("box %d reports %d Pokemon, which is impossible; this is the "
|
|
||||||
.. "wrong layout for this save"):format(index, count)
|
|
||||||
end
|
|
||||||
local mons = {}
|
|
||||||
for i = 0, count - 1 do
|
|
||||||
local mon = decodeBoxMon(bytes, base + BOX_MONS + i * Gen2Save.BOX_MON_STRUCT, x)
|
|
||||||
-- The species list beside the mons must agree, which is a free check
|
|
||||||
-- on the layout.
|
|
||||||
local listed = named(x.pokemon, u8(bytes, base + BOX_SPECIES + i))
|
|
||||||
if listed ~= mon.species then
|
|
||||||
return nil, ("box %d slot %d: the species list says %s and the stored "
|
|
||||||
.. "Pokemon says %s; wrong layout for this save")
|
|
||||||
:format(index, i + 1, tostring(listed), tostring(mon.species))
|
|
||||||
end
|
|
||||||
mon.ot = text(bytes, base + BOX_OTS + i * Gen2Save.NAME_LENGTH, Gen2Save.NAME_LENGTH)
|
|
||||||
mon.nickname = text(bytes, base + BOX_NICKS + i * Gen2Save.NAME_LENGTH, Gen2Save.NAME_LENGTH)
|
|
||||||
mons[#mons + 1] = mon
|
|
||||||
end
|
|
||||||
boxes[index] = mons
|
|
||||||
end
|
|
||||||
return boxes
|
|
||||||
end
|
|
||||||
|
|
||||||
-- The bag. Gen 2 splits it into four pockets, and they are not all shaped the
|
|
||||||
-- same: ITEM and BALL are (id, quantity) pairs, KEY_ITEM is bare ids because a
|
|
||||||
-- key item is unique, and TM_HM is a flat run of counts indexed by TM number.
|
|
||||||
-- All of the list pockets are terminated by 0xFF as well as counted, and the
|
|
||||||
-- count is trusted only as far as the terminator.
|
|
||||||
-- One bag keyed by item id; PackMenu buckets it by each item's `pocket`.
|
|
||||||
-- ITEM and BALL are (id, quantity) pairs, KEY_ITEM is bare ids.
|
|
||||||
local function addPairs(out, bytes, countAt, listAt, cap, x)
|
|
||||||
local n = u8(bytes, countAt)
|
|
||||||
if n > cap then n = cap end
|
|
||||||
for i = 0, n - 1 do
|
|
||||||
local raw = u8(bytes, listAt + i * 2)
|
|
||||||
if raw == 0xFF or raw == 0 then break end
|
|
||||||
local id = named(x.items, raw)
|
|
||||||
out[id] = (out[id] or 0) + u8(bytes, listAt + i * 2 + 1)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function addIds(out, bytes, countAt, listAt, cap, x)
|
|
||||||
local n = u8(bytes, countAt)
|
|
||||||
if n > cap then n = cap end
|
|
||||||
for i = 0, n - 1 do
|
|
||||||
local raw = u8(bytes, listAt + i)
|
|
||||||
if raw == 0xFF or raw == 0 then break end
|
|
||||||
local id = named(x.items, raw)
|
|
||||||
out[id] = (out[id] or 0) + 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- TM_HM is a flat run of counts indexed by TM number.
|
|
||||||
local function addMachines(out, bytes, at, x, items)
|
|
||||||
local byNumber = {}
|
|
||||||
for id, def in pairs(items or {}) do
|
|
||||||
if type(def) == "table" and def.tmNumber then byNumber[def.tmNumber] = id end
|
|
||||||
end
|
|
||||||
for number, id in pairs(byNumber) do
|
|
||||||
local count = u8(bytes, at + number - 1)
|
|
||||||
if count > 0 then out[id] = (out[id] or 0) + count end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Byte index -> byte, which is what Save.scrubEvents validates.
|
|
||||||
local function decodeFlagBytes(bytes, at, count)
|
|
||||||
local out = {}
|
|
||||||
for i = 0, count - 1 do out[i] = u8(bytes, at + i) end
|
|
||||||
return out
|
|
||||||
end
|
|
||||||
|
|
||||||
-- A set keyed by species id: save.pokedex.caught[species] = true.
|
|
||||||
local function decodeDex(bytes, at, x)
|
|
||||||
local out = {}
|
|
||||||
for i = 0, Gen2Save.NUM_SPECIES - 1 do
|
|
||||||
local byte = u8(bytes, at + math.floor(i / 8))
|
|
||||||
if math.floor(byte / (2 ^ (i % 8))) % 2 == 1 then
|
|
||||||
out[named(x.pokemon, i + 1) or (i + 1)] = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return out
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Badges by NAME, which is how FieldMoves.hasBadge and Battle:hasBadge read
|
|
||||||
-- them. Bit position is accepted as a fallback key there, but the name is the
|
|
||||||
-- primary and is what a save written by this project carries.
|
|
||||||
Gen2Save.JOHTO_BADGES = {
|
|
||||||
"ZEPHYR", "HIVE", "PLAIN", "FOG", "MINERAL", "STORM", "GLACIER", "RISING",
|
|
||||||
}
|
|
||||||
Gen2Save.KANTO_BADGES = {
|
|
||||||
"BOULDER", "CASCADE", "THUNDER", "RAINBOW",
|
|
||||||
"SOUL", "MARSH", "VOLCANO", "EARTH",
|
|
||||||
}
|
|
||||||
local function decodeBadges(byte, order)
|
|
||||||
local out = {}
|
|
||||||
for bit, name in ipairs(order) do
|
|
||||||
if math.floor(byte / (2 ^ (bit - 1))) % 2 == 1 then out[name] = true end
|
|
||||||
end
|
|
||||||
return out
|
|
||||||
end
|
|
||||||
|
|
||||||
Gen2Save.NUM_SPECIES = 251
|
|
||||||
Gen2Save.EVENT_BYTES = 256
|
|
||||||
|
|
||||||
-- decode(bytes, gameVersion, data) -> partial save table, err
|
|
||||||
function Gen2Save.decode(bytes, gameVersion, data)
|
|
||||||
local L = Gen2Save.layoutFor(gameVersion)
|
|
||||||
if not L then return nil, "no Gen 2 layout for " .. tostring(gameVersion) end
|
|
||||||
if type(bytes) ~= "string" or #bytes < Gen2Save.SAVE_SIZE then
|
|
||||||
return nil, ("save must be at least %d bytes"):format(Gen2Save.SAVE_SIZE)
|
|
||||||
end
|
|
||||||
-- TryLoadSaveFile falls back to VerifyBackupChecksum and loads the backup
|
|
||||||
-- copy, so a save the real cartridge would open must not be refused here.
|
|
||||||
-- Crystal's backup is contiguous and laid out like the primary; Gold and
|
|
||||||
-- Silver split theirs across three sections and have none to offer.
|
|
||||||
if Gen2Save.checksumValid(bytes, L) ~= true then
|
|
||||||
if L.backup and Gen2Save.checksumValid(bytes, L.backup) == true then
|
|
||||||
L = L.backup
|
|
||||||
else
|
|
||||||
return nil, "save data checksum invalid (Gen 2 check values or sum mismatch)"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local x = Gen2Save.crosswalks(data)
|
|
||||||
local count = u8(bytes, L.wPartyCount)
|
|
||||||
if count > Gen2Save.PARTY_LENGTH then
|
|
||||||
return nil, ("party count %d is impossible; this is probably the wrong layout")
|
|
||||||
:format(count)
|
|
||||||
end
|
|
||||||
local party = {}
|
|
||||||
for i = 0, count - 1 do
|
|
||||||
local mon = decodeMon(bytes, L.wPartyMons + i * Gen2Save.PARTY_STRUCT, x)
|
|
||||||
mon.nickname = text(bytes, L.wPartyMonNicknames + i * Gen2Save.NAME_LENGTH,
|
|
||||||
Gen2Save.NAME_LENGTH)
|
|
||||||
mon.ot = text(bytes, L.wPartyMonOTs + i * Gen2Save.NAME_LENGTH,
|
|
||||||
Gen2Save.NAME_LENGTH)
|
|
||||||
party[#party + 1] = mon
|
|
||||||
end
|
|
||||||
|
|
||||||
local boxes, boxErr = Gen2Save.decodeBoxes(bytes, L, x)
|
|
||||||
if not boxes then return nil, boxErr end
|
|
||||||
|
|
||||||
local inventory = {}
|
|
||||||
addPairs(inventory, bytes, L.wNumItems, L.wItems, 20, x)
|
|
||||||
addIds(inventory, bytes, L.wNumKeyItems, L.wKeyItems, 25, x)
|
|
||||||
addPairs(inventory, bytes, L.wNumBalls, L.wBalls, 12, x)
|
|
||||||
if L.wTMsHMs then addMachines(inventory, bytes, L.wTMsHMs, x, (data or {}).items) end
|
|
||||||
|
|
||||||
return {
|
|
||||||
generation = 2,
|
|
||||||
version = gameVersion,
|
|
||||||
player = {
|
|
||||||
name = text(bytes, L.wPlayerName, Gen2Save.NAME_LENGTH),
|
|
||||||
id = be(bytes, L.wPlayerID, 2),
|
|
||||||
money = be(bytes, L.wMoney, 3),
|
|
||||||
coins = be(bytes, L.wCoins, 2),
|
|
||||||
badges = decodeBadges(u8(bytes, L.wBadges), Gen2Save.JOHTO_BADGES),
|
|
||||||
kantoBadges = decodeBadges(u8(bytes, L.wKantoBadges), Gen2Save.KANTO_BADGES),
|
|
||||||
},
|
|
||||||
rival = { name = text(bytes, L.wRivalName, Gen2Save.NAME_LENGTH) },
|
|
||||||
mom = { name = text(bytes, L.wMomsName, Gen2Save.NAME_LENGTH) },
|
|
||||||
party = party,
|
|
||||||
boxes = boxes,
|
|
||||||
currentBox = u8(bytes, L.wCurBox) % 16 + 1,
|
|
||||||
inventory = inventory,
|
|
||||||
-- Species ids, 1-based, so the set keys match save.pokedex.caught[species].
|
|
||||||
pokedex = {
|
|
||||||
caught = decodeDex(bytes, L.wPokedexCaught, x),
|
|
||||||
seen = decodeDex(bytes, L.wPokedexSeen, x),
|
|
||||||
},
|
|
||||||
events = decodeFlagBytes(bytes, L.wEventFlags, Gen2Save.EVENT_BYTES),
|
|
||||||
-- Save.summary does `save.position.map or save.spawn`.
|
|
||||||
position = {
|
|
||||||
map = x.maps[u8(bytes, L.wMapGroup) * 256 + u8(bytes, L.wMapNumber)],
|
|
||||||
mapGroup = u8(bytes, L.wMapGroup), mapNumber = u8(bytes, L.wMapNumber),
|
|
||||||
x = u8(bytes, L.wXCoord), y = u8(bytes, L.wYCoord),
|
|
||||||
},
|
|
||||||
playTime = {
|
|
||||||
hours = be(bytes, L.wGameTimeHours, 2),
|
|
||||||
minutes = u8(bytes, L.wGameTimeMinutes),
|
|
||||||
seconds = 0, frames = 0,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Everything the cart does not carry comes from a fresh game, exactly as
|
|
||||||
-- Gen 1's mergeDefaults does: the decode above models what the SRAM holds,
|
|
||||||
-- and mail, phone contacts, the unown dex, the hall of fame and the RTC are
|
|
||||||
-- left to the engine's own defaults rather than invented here.
|
|
||||||
--
|
|
||||||
-- Loaded lazily. src/core/gen2/Save.lua pulls in love.filesystem at require
|
|
||||||
-- time, and this module has to stay require-clean for the headless CLI and
|
|
||||||
-- the tests, same rule GenSave follows.
|
|
||||||
function Gen2Save.mergeDefaults(decoded, gameVersion)
|
|
||||||
local ok, Save = pcall(require, "src.core.gen2.Save")
|
|
||||||
if not ok then return decoded end
|
|
||||||
local base = Save.newGame({ playerName = decoded.player and decoded.player.name })
|
|
||||||
for k, v in pairs(decoded) do base[k] = v end
|
|
||||||
base.version = gameVersion
|
|
||||||
return base
|
|
||||||
end
|
|
||||||
|
|
||||||
-- ------------------------------------------------------------------
|
|
||||||
-- Export
|
|
||||||
-- ------------------------------------------------------------------
|
|
||||||
|
|
||||||
local function putU8(t, at, v) t[at] = v % 256 end
|
|
||||||
local function putBE(t, at, v, n)
|
|
||||||
for i = n - 1, 0, -1 do t[at + i] = v % 256; v = math.floor(v / 256) end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Engine id back to the cart's number. A raw number passes through, which is
|
|
||||||
-- how a save imported without a crosswalk round trips.
|
|
||||||
local function indexOf(map, id)
|
|
||||||
if id == nil then return 0 end
|
|
||||||
if type(id) == "number" then return id end
|
|
||||||
return map[id] or 0
|
|
||||||
end
|
|
||||||
|
|
||||||
local function charBytes(str, i)
|
|
||||||
local b = str:byte(i)
|
|
||||||
if not b then return 0 end
|
|
||||||
if b < 0x80 then return 1 end
|
|
||||||
if b >= 0xF0 then return 4 end
|
|
||||||
if b >= 0xE0 then return 3 end
|
|
||||||
if b >= 0xC0 then return 2 end
|
|
||||||
return 1
|
|
||||||
end
|
|
||||||
|
|
||||||
local function glyphChars(glyph)
|
|
||||||
local n, i = 0, 1
|
|
||||||
while i <= #glyph do i = i + charBytes(glyph, i); n = n + 1 end
|
|
||||||
return n
|
|
||||||
end
|
|
||||||
|
|
||||||
-- One-CHARACTER glyphs plus PK and MN. The cart's table also carries the
|
|
||||||
-- ligature halves PO and KE, and accepting those turns a name containing "PO"
|
|
||||||
-- into 0x70 where the cart had a plain P. #glyph counts BYTES, so a byte test
|
|
||||||
-- would also drop every multi-byte glyph and turn NIDORAN into NIDORAN?.
|
|
||||||
local REVERSE = nil
|
|
||||||
local function reverseCharmap()
|
|
||||||
if REVERSE then return REVERSE end
|
|
||||||
REVERSE = {}
|
|
||||||
for code, glyph in pairs(Gen2Layout.charmap) do
|
|
||||||
if glyphChars(glyph) == 1 then REVERSE[glyph] = code end
|
|
||||||
end
|
|
||||||
for code, glyph in pairs(Gen2Layout.charmap) do
|
|
||||||
if glyph == "PK" or glyph == "MN" then REVERSE[glyph] = code end
|
|
||||||
end
|
|
||||||
return REVERSE
|
|
||||||
end
|
|
||||||
|
|
||||||
local function putText(t, at, str, n)
|
|
||||||
local codes = reverseCharmap()
|
|
||||||
local i, written = 1, 0
|
|
||||||
while i <= #str and written < n - 1 do
|
|
||||||
local two = str:sub(i, i + 1)
|
|
||||||
if (two == "PK" or two == "MN") and codes[two] then
|
|
||||||
t[at + written] = codes[two]; i = i + 2
|
|
||||||
else
|
|
||||||
local w = charBytes(str, i)
|
|
||||||
t[at + written] = codes[str:sub(i, i + w - 1)] or 0xE6
|
|
||||||
i = i + w
|
|
||||||
end
|
|
||||||
written = written + 1
|
|
||||||
end
|
|
||||||
t[at + written] = 0x50
|
|
||||||
end
|
|
||||||
|
|
||||||
local function putBadges(t, at, owned, order)
|
|
||||||
local byte = 0
|
|
||||||
for bit, name in ipairs(order) do
|
|
||||||
if owned and owned[name] then byte = byte + 2 ^ (bit - 1) end
|
|
||||||
end
|
|
||||||
t[at] = byte
|
|
||||||
end
|
|
||||||
|
|
||||||
local function putSharedMon(t, o, mon, x)
|
|
||||||
putU8(t, o, indexOf(x.pokemonIndex, mon.species))
|
|
||||||
putU8(t, o + 1, indexOf(x.itemIndex, mon.item))
|
|
||||||
for i = 0, 3 do
|
|
||||||
putU8(t, o + 2 + i, indexOf(x.moveIndex, (mon.moves or {})[i + 1]))
|
|
||||||
end
|
|
||||||
putBE(t, o + 6, mon.otId or 0, 2)
|
|
||||||
putBE(t, o + 8, mon.experience or 0, 3)
|
|
||||||
local se = mon.statExp or {}
|
|
||||||
putBE(t, o + 0x0B, se.hp or 0, 2); putBE(t, o + 0x0D, se.attack or 0, 2)
|
|
||||||
putBE(t, o + 0x0F, se.defense or 0, 2); putBE(t, o + 0x11, se.speed or 0, 2)
|
|
||||||
putBE(t, o + 0x13, se.special or 0, 2)
|
|
||||||
local d = mon.dvs or {}
|
|
||||||
putU8(t, o + 0x15, (d.attack or 0) * 16 + (d.defense or 0))
|
|
||||||
putU8(t, o + 0x16, (d.speed or 0) * 16 + (d.special or 0))
|
|
||||||
for i = 0, 3 do
|
|
||||||
putU8(t, o + 0x17 + i, (mon.ppRaw or {})[i + 1] or (mon.pp or {})[i + 1] or 0)
|
|
||||||
end
|
|
||||||
putU8(t, o + 0x1B, mon.happiness or 0)
|
|
||||||
-- 0x1C-0x1E belong to the mon, not to the slot: leaving them to the template
|
|
||||||
-- means reordering the party gives slot 1 the previous occupant's pokerus
|
|
||||||
-- and caught data.
|
|
||||||
putU8(t, o + 0x1C, mon.pokerus or 0)
|
|
||||||
putBE(t, o + 0x1D, mon.caughtData or 0, 2)
|
|
||||||
putU8(t, o + 0x1F, mon.level or 0)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- The bag back into its four pockets, by each item's own `pocket`.
|
|
||||||
local function putBag(t, L, inventory, x)
|
|
||||||
local buckets = { ITEM = {}, KEY_ITEM = {}, BALL = {}, TM_HM = {} }
|
|
||||||
for id, count in pairs(inventory or {}) do
|
|
||||||
local def = x.itemDefs[id]
|
|
||||||
local pocket = (type(def) == "table" and def.pocket) or "ITEM"
|
|
||||||
if buckets[pocket] == nil then pocket = "ITEM" end
|
|
||||||
buckets[pocket][#buckets[pocket] + 1] = { id = id, count = count, def = def }
|
|
||||||
end
|
|
||||||
-- By cart index, so the order is deterministic. A flat inventory has no
|
|
||||||
-- order of its own, so the pocket order a round trip produces is stable
|
|
||||||
-- rather than original.
|
|
||||||
for _, list in pairs(buckets) do
|
|
||||||
table.sort(list, function(a, b)
|
|
||||||
return indexOf(x.itemIndex, a.id) < indexOf(x.itemIndex, b.id)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
local overflow = nil
|
|
||||||
local function writePairs(countAt, listAt, cap, list, pocket)
|
|
||||||
if #list > cap then overflow = overflow or { pocket, #list, cap } end
|
|
||||||
local n = math.min(#list, cap)
|
|
||||||
putU8(t, countAt, n)
|
|
||||||
for i = 1, n do
|
|
||||||
putU8(t, listAt + (i - 1) * 2, indexOf(x.itemIndex, list[i].id))
|
|
||||||
putU8(t, listAt + (i - 1) * 2 + 1, math.min(list[i].count, 99))
|
|
||||||
end
|
|
||||||
putU8(t, listAt + n * 2, 0xFF)
|
|
||||||
end
|
|
||||||
local function writeIds(countAt, listAt, cap, list, pocket)
|
|
||||||
if #list > cap then overflow = overflow or { pocket, #list, cap } end
|
|
||||||
local n = math.min(#list, cap)
|
|
||||||
putU8(t, countAt, n)
|
|
||||||
for i = 1, n do putU8(t, listAt + i - 1, indexOf(x.itemIndex, list[i].id)) end
|
|
||||||
putU8(t, listAt + n, 0xFF)
|
|
||||||
end
|
|
||||||
|
|
||||||
writePairs(L.wNumItems, L.wItems, 20, buckets.ITEM, "ITEM")
|
|
||||||
writeIds(L.wNumKeyItems, L.wKeyItems, 25, buckets.KEY_ITEM, "KEY_ITEM")
|
|
||||||
writePairs(L.wNumBalls, L.wBalls, 12, buckets.BALL, "BALL")
|
|
||||||
if L.wTMsHMs then
|
|
||||||
for _, row in ipairs(buckets.TM_HM) do
|
|
||||||
local number = type(row.def) == "table" and row.def.tmNumber
|
|
||||||
if number then putU8(t, L.wTMsHMs + number - 1, math.min(row.count, 99)) end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return overflow
|
|
||||||
end
|
|
||||||
|
|
||||||
local function putFlagSet(t, at, set, count, indexFor)
|
|
||||||
for i = 0, count - 1 do
|
|
||||||
local byteAt = at + math.floor(i / 8)
|
|
||||||
local bit = 2 ^ (i % 8)
|
|
||||||
local cur = t[byteAt] or 0
|
|
||||||
local on = math.floor(cur / bit) % 2 == 1
|
|
||||||
local want = set and set[indexFor(i)] == true
|
|
||||||
if on ~= want then t[byteAt] = want and (cur + bit) or (cur - bit) end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- encode(save, gameVersion, template, data) -> bytes, err
|
|
||||||
--
|
|
||||||
-- Writes into the cartridge image the save came from: Gen 2 SRAM holds a great
|
|
||||||
-- deal this codec does not model and the real game trusts it on CONTINUE, so a
|
|
||||||
-- save with no image behind it is refused rather than built from nothing.
|
|
||||||
--
|
|
||||||
-- Only the primary copy is written. TryLoadSaveFile rewrites the backup from
|
|
||||||
-- the primary on every successful load.
|
|
||||||
function Gen2Save.encode(save, gameVersion, template, data)
|
|
||||||
local L = Gen2Save.layoutFor(gameVersion)
|
|
||||||
if not L then return nil, "no Gen 2 layout for " .. tostring(gameVersion) end
|
|
||||||
if type(save) ~= "table" then return nil, "expected a save table" end
|
|
||||||
if type(template) ~= "string" or #template < Gen2Save.SAVE_SIZE then
|
|
||||||
return nil, "this save has no cartridge image to write back into, and a "
|
|
||||||
.. "Gen 2 save built from nothing does not boot on real hardware"
|
|
||||||
end
|
|
||||||
|
|
||||||
local x = Gen2Save.crosswalks(data)
|
|
||||||
local t = {}
|
|
||||||
for i = 0, Gen2Save.SAVE_SIZE - 1 do t[i] = template:byte(i + 1) end
|
|
||||||
|
|
||||||
local p = save.player or {}
|
|
||||||
putText(t, L.wPlayerName, p.name or "", Gen2Save.NAME_LENGTH)
|
|
||||||
putBE(t, L.wPlayerID, p.id or 0, 2)
|
|
||||||
putBE(t, L.wMoney, p.money or 0, 3)
|
|
||||||
putBE(t, L.wCoins, p.coins or 0, 2)
|
|
||||||
putBadges(t, L.wBadges, p.badges, Gen2Save.JOHTO_BADGES)
|
|
||||||
putBadges(t, L.wKantoBadges, p.kantoBadges, Gen2Save.KANTO_BADGES)
|
|
||||||
putText(t, L.wRivalName, (save.rival or {}).name or "", Gen2Save.NAME_LENGTH)
|
|
||||||
putText(t, L.wMomsName, (save.mom or {}).name or "", Gen2Save.NAME_LENGTH)
|
|
||||||
|
|
||||||
local party = save.party or {}
|
|
||||||
if #party > Gen2Save.PARTY_LENGTH then
|
|
||||||
return nil, ("a party of %d cannot be written to a cartridge"):format(#party)
|
|
||||||
end
|
|
||||||
putU8(t, L.wPartyCount, #party)
|
|
||||||
for i, mon in ipairs(party) do
|
|
||||||
putU8(t, L.wPartySpecies + i - 1, indexOf(x.pokemonIndex, mon.species))
|
|
||||||
local o = L.wPartyMons + (i - 1) * Gen2Save.PARTY_STRUCT
|
|
||||||
putSharedMon(t, o, mon, x)
|
|
||||||
putU8(t, o + 0x20, encodeStatus(mon.status, mon.statusTurns))
|
|
||||||
putBE(t, o + 0x22, mon.hp or 0, 2)
|
|
||||||
local st = mon.stats or {}
|
|
||||||
putBE(t, o + 0x24, mon.maxHp or st.hp or 0, 2)
|
|
||||||
putBE(t, o + 0x26, st.attack or 0, 2); putBE(t, o + 0x28, st.defense or 0, 2)
|
|
||||||
putBE(t, o + 0x2A, st.speed or 0, 2); putBE(t, o + 0x2C, st.specialAttack or 0, 2)
|
|
||||||
putBE(t, o + 0x2E, st.specialDefense or 0, 2)
|
|
||||||
putText(t, L.wPartyMonNicknames + (i - 1) * Gen2Save.NAME_LENGTH,
|
|
||||||
mon.nickname or "", Gen2Save.NAME_LENGTH)
|
|
||||||
putText(t, L.wPartyMonOTs + (i - 1) * Gen2Save.NAME_LENGTH,
|
|
||||||
mon.ot or "", Gen2Save.NAME_LENGTH)
|
|
||||||
end
|
|
||||||
putU8(t, L.wPartySpecies + #party, 0xFF)
|
|
||||||
|
|
||||||
for index, base in ipairs(L.boxes) do
|
|
||||||
local box = (save.boxes or {})[index] or {}
|
|
||||||
if #box > Gen2Save.BOX_CAPACITY then
|
|
||||||
return nil, ("box %d holds %d, which a cartridge cannot"):format(index, #box)
|
|
||||||
end
|
|
||||||
putU8(t, base, #box)
|
|
||||||
for i, mon in ipairs(box) do
|
|
||||||
putU8(t, base + BOX_SPECIES + i - 1, indexOf(x.pokemonIndex, mon.species))
|
|
||||||
putSharedMon(t, base + BOX_MONS + (i - 1) * Gen2Save.BOX_MON_STRUCT, mon, x)
|
|
||||||
putText(t, base + BOX_OTS + (i - 1) * Gen2Save.NAME_LENGTH,
|
|
||||||
mon.ot or "", Gen2Save.NAME_LENGTH)
|
|
||||||
putText(t, base + BOX_NICKS + (i - 1) * Gen2Save.NAME_LENGTH,
|
|
||||||
mon.nickname or "", Gen2Save.NAME_LENGTH)
|
|
||||||
end
|
|
||||||
putU8(t, base + BOX_SPECIES + #box, 0xFF)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Refuse rather than drop. Without item defs every item buckets into ITEM,
|
|
||||||
-- which holds 20, and a real bag is bigger than that.
|
|
||||||
local overflow = putBag(t, L, save.inventory, x)
|
|
||||||
if overflow then
|
|
||||||
return nil, ("the %s pocket would need %d slots and a cartridge has %d; "
|
|
||||||
.. "the item table for this game is needed to sort the bag")
|
|
||||||
:format(overflow[1], overflow[2], overflow[3])
|
|
||||||
end
|
|
||||||
if save.currentBox then putU8(t, L.wCurBox, (save.currentBox - 1) % 16) end
|
|
||||||
if save.boxNames and L.wBoxNames then
|
|
||||||
for i = 1, 14 do
|
|
||||||
putText(t, L.wBoxNames + (i - 1) * 9, save.boxNames[i] or "", 9)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if save.pokedex then
|
|
||||||
local function species(i) return x.pokemon[i + 1] or (i + 1) end
|
|
||||||
putFlagSet(t, L.wPokedexCaught, save.pokedex.caught, Gen2Save.NUM_SPECIES, species)
|
|
||||||
putFlagSet(t, L.wPokedexSeen, save.pokedex.seen, Gen2Save.NUM_SPECIES, species)
|
|
||||||
end
|
|
||||||
for i = 0, Gen2Save.EVENT_BYTES - 1 do
|
|
||||||
local byte = (save.events or {})[i]
|
|
||||||
if type(byte) == "number" then putU8(t, L.wEventFlags + i, byte) end
|
|
||||||
end
|
|
||||||
|
|
||||||
local pos = save.position
|
|
||||||
if pos then
|
|
||||||
local ids = pos.map and x.mapIds[pos.map]
|
|
||||||
putU8(t, L.wMapGroup, (ids and ids[1]) or pos.mapGroup or 0)
|
|
||||||
putU8(t, L.wMapNumber, (ids and ids[2]) or pos.mapNumber or 0)
|
|
||||||
putU8(t, L.wXCoord, pos.x or 0)
|
|
||||||
putU8(t, L.wYCoord, pos.y or 0)
|
|
||||||
end
|
|
||||||
local pt = save.playTime
|
|
||||||
if pt then
|
|
||||||
putBE(t, L.wGameTimeHours, pt.hours or 0, 2)
|
|
||||||
putU8(t, L.wGameTimeMinutes, pt.minutes or 0)
|
|
||||||
end
|
|
||||||
|
|
||||||
putU8(t, L.sCheckValue1, 0x63)
|
|
||||||
putU8(t, L.sCheckValue2, 0x7F)
|
|
||||||
local sum = 0
|
|
||||||
for i = L.sGameData, L.sGameDataEnd - 1 do sum = (sum + t[i]) % 65536 end
|
|
||||||
putU8(t, L.sChecksum, sum % 256)
|
|
||||||
putU8(t, L.sChecksum + 1, math.floor(sum / 256) % 256)
|
|
||||||
|
|
||||||
local out = {}
|
|
||||||
for i = 0, Gen2Save.SAVE_SIZE - 1 do out[i + 1] = string.char(t[i]) end
|
|
||||||
-- Whatever followed the 32 KiB of SRAM is the cart's RTC footer. Dropping it
|
|
||||||
-- resets the clock, which costs the player daily events and the bug contest
|
|
||||||
-- and earns them the clock-adjustment penalty.
|
|
||||||
return table.concat(out) .. template:sub(Gen2Save.SAVE_SIZE + 1)
|
|
||||||
end
|
|
||||||
|
|
||||||
return Gen2Save
|
|
||||||
@@ -21,23 +21,12 @@
|
|||||||
-- require alone cannot see them there (#420).
|
-- require alone cannot see them there (#420).
|
||||||
|
|
||||||
local GenSave = require("src.save_convert.GenSave")
|
local GenSave = require("src.save_convert.GenSave")
|
||||||
local Gen2Save = require("src.save_convert.Gen2Save")
|
local GameVersion = require("src.core.GameVersion")
|
||||||
|
|
||||||
local SaveConvert = {}
|
local SaveConvert = {}
|
||||||
|
|
||||||
SaveConvert.SAVE_SIZE = GenSave.SAVE_SIZE
|
SaveConvert.SAVE_SIZE = GenSave.SAVE_SIZE
|
||||||
-- Is this a real save for THIS GAME? Dispatches on the generation, because
|
SaveConvert.mainChecksumValid = GenSave.mainChecksumValid
|
||||||
-- the two do not share a rule: Gen 1 stores a complement checksum of its main
|
|
||||||
-- data block, Gen 2 stores two check values plus a 16-bit sum. Run one over
|
|
||||||
-- the other's bytes and the answer is always no.
|
|
||||||
--
|
|
||||||
-- gameVersion is optional and defaults to Gen 1's rule, which is what every
|
|
||||||
-- caller meant before Gen 2 had a codec.
|
|
||||||
function SaveConvert.mainChecksumValid(bytes, gameVersion)
|
|
||||||
local L = gameVersion and Gen2Save.layoutFor(gameVersion)
|
|
||||||
if L then return Gen2Save.checksumValid(bytes, L) end
|
|
||||||
return GenSave.mainChecksumValid(bytes)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- ------------------------------------------------------------------
|
-- ------------------------------------------------------------------
|
||||||
-- Crosswalk data loading (cached). Mirrors src/core/Data.lua: prefer
|
-- Crosswalk data loading (cached). Mirrors src/core/Data.lua: prefer
|
||||||
@@ -136,24 +125,6 @@ local function loadCacheTable(gameVersion, filePath)
|
|||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
-- The generated tables Gen2Save needs to turn cart numbers into the ids the
|
|
||||||
-- engine is keyed by. Deliberately not ensureData: that one also demands Gen
|
|
||||||
-- 1's charmap, event flags and hidden items, none of which a Gen 2 cache has
|
|
||||||
-- or a Gen 2 save uses.
|
|
||||||
local gen2Data = {}
|
|
||||||
local function ensureGen2Data(gameVersion)
|
|
||||||
local key = gameVersion or "*"
|
|
||||||
if gen2Data[key] == nil then
|
|
||||||
local out = {}
|
|
||||||
for _, name in ipairs({ "pokemon", "moves", "items", "maps" }) do
|
|
||||||
out[name] = loadCacheTable(gameVersion, "data/generated/" .. name .. ".lua")
|
|
||||||
or (loadTable("data.generated." .. name, "data/generated/" .. name .. ".lua"))
|
|
||||||
end
|
|
||||||
gen2Data[key] = out
|
|
||||||
end
|
|
||||||
return gen2Data[key]
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Crosswalk sets keyed by the game whose cache they came from ("*" for the
|
-- Crosswalk sets keyed by the game whose cache they came from ("*" for the
|
||||||
-- require-resolved set): Yellow's tables are not Red's, so one import must
|
-- require-resolved set): Yellow's tables are not Red's, so one import must
|
||||||
-- never be handed the previous import's data (#420).
|
-- never be handed the previous import's data (#420).
|
||||||
@@ -257,36 +228,11 @@ SaveConvert.mergeDefaults = mergeDefaults
|
|||||||
-- pokered's single sPlayerName..sMainDataCheckSum window), and no Gen 2
|
-- pokered's single sPlayerName..sMainDataCheckSum window), and no Gen 2
|
||||||
-- codec exists yet. Both directions answer with a plain message the
|
-- codec exists yet. Both directions answer with a plain message the
|
||||||
-- launcher's save card renders as-is, instead of pushing a Gen 2 save
|
-- launcher's save card renders as-is, instead of pushing a Gen 2 save
|
||||||
-- Can a cart save for this game cross in or out at all? Public because the
|
-- table through Gen 1 offsets and surfacing a codec traceback.
|
||||||
-- launcher has to ask about the GAME before it measures the BYTES.
|
local function gen2CartName(gameVersion)
|
||||||
--
|
if not GameVersion.VERSIONS[gameVersion] then return nil end
|
||||||
-- SaveFileIO.importToSlot judges anything that is not exactly SAVE_SIZE with
|
if GameVersion.generation(gameVersion) ~= 2 then return nil end
|
||||||
-- mainChecksumValid, which is pokered's main-data checksum. A Gen 2 cart is
|
return GameVersion.info(gameVersion).displayName
|
||||||
-- MBC3+TIMER, so a real Gold/Silver/Crystal .sav carries an RTC footer and is
|
|
||||||
-- 32786 bytes: it misses the size test and is then measured against a rule
|
|
||||||
-- that was never going to match it. The player is told "save data checksum
|
|
||||||
-- invalid" about a save that is perfectly good (#1832).
|
|
||||||
--
|
|
||||||
-- Returns true, or false plus the same sentence importSav/exportSav would have
|
|
||||||
-- answered with, so a caller that asks early and a caller that does not cannot
|
|
||||||
-- describe the same game two different ways.
|
|
||||||
-- Does this game use a Gen 2 cart save? The RTC footer that follows one is
|
|
||||||
-- expected rather than surprising, which the import path needs to know.
|
|
||||||
function SaveConvert.isGen2Cart(gameVersion)
|
|
||||||
return Gen2Save.layoutFor(gameVersion) ~= nil
|
|
||||||
end
|
|
||||||
|
|
||||||
function SaveConvert.importSupported(gameVersion)
|
|
||||||
-- Gen 2 imports through Gen2Save now. Kept as a predicate rather than
|
|
||||||
-- deleted: SaveFileIO asks it before it measures the bytes, and export
|
|
||||||
-- still answers no.
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Gen 2 exports through Gen2Save now, but only for a save with a cartridge
|
|
||||||
-- image behind it; encode says so itself.
|
|
||||||
function SaveConvert.exportSupported(gameVersion)
|
|
||||||
return true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- importSav(bytes, version, gameVersion) -> saveTable, err
|
-- importSav(bytes, version, gameVersion) -> saveTable, err
|
||||||
@@ -301,16 +247,9 @@ function SaveConvert.importSav(bytes, version, gameVersion)
|
|||||||
if type(bytes) ~= "string" then
|
if type(bytes) ~= "string" then
|
||||||
return nil, "expected raw save bytes as a string"
|
return nil, "expected raw save bytes as a string"
|
||||||
end
|
end
|
||||||
local supported, unsupportedWhy = SaveConvert.importSupported(gameVersion)
|
local gen2Name = gen2CartName(gameVersion)
|
||||||
if not supported then return nil, unsupportedWhy end
|
if gen2Name then
|
||||||
-- Gen 2 is a different SRAM entirely: different bank map, different party
|
return nil, gen2Name .. " uses a Gen 2 cart save; importing one is not supported yet."
|
||||||
-- struct, its own check values. Gen2Save owns it, and it needs no crosswalk
|
|
||||||
-- tables because it decodes ids the engine already speaks.
|
|
||||||
if Gen2Save.layoutFor(gameVersion) then
|
|
||||||
local decoded, gen2Err = Gen2Save.decode(bytes, gameVersion,
|
|
||||||
ensureGen2Data(gameVersion))
|
|
||||||
if not decoded then return nil, gen2Err end
|
|
||||||
return Gen2Save.mergeDefaults(decoded, gameVersion)
|
|
||||||
end
|
end
|
||||||
if #bytes ~= GenSave.SAVE_SIZE then
|
if #bytes ~= GenSave.SAVE_SIZE then
|
||||||
return nil, ("save must be %d bytes, got %d"):format(GenSave.SAVE_SIZE, #bytes)
|
return nil, ("save must be %d bytes, got %d"):format(GenSave.SAVE_SIZE, #bytes)
|
||||||
@@ -334,22 +273,19 @@ function SaveConvert.importSav(bytes, version, gameVersion)
|
|||||||
return mergeDefaults(decoded, version)
|
return mergeDefaults(decoded, version)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- exportSav(saveTable, gameVersion, cartImage) -> bytes, err
|
-- exportSav(saveTable, gameVersion) -> bytes, err
|
||||||
-- Encodes a save table back to a raw 32768-byte SRAM image. Template-aware:
|
-- Encodes a save table back to a raw 32768-byte SRAM image. Template-aware:
|
||||||
-- if the table still carries the stashed import template (saveTable.rawImport)
|
-- if the table still carries the stashed import template (saveTable.rawImport)
|
||||||
-- GenSave reproduces every unmodeled region from it; otherwise those regions
|
-- GenSave reproduces every unmodeled region from it; otherwise those regions
|
||||||
-- are zero-filled. gameVersion selects the crosswalk tables exactly as in
|
-- are zero-filled. gameVersion selects the crosswalk tables exactly as in
|
||||||
-- importSav. On failure returns nil + a message (never raises).
|
-- importSav. On failure returns nil + a message (never raises).
|
||||||
function SaveConvert.exportSav(saveTable, gameVersion, cartImage)
|
function SaveConvert.exportSav(saveTable, gameVersion)
|
||||||
if type(saveTable) ~= "table" then
|
if type(saveTable) ~= "table" then
|
||||||
return nil, "expected a save table"
|
return nil, "expected a save table"
|
||||||
end
|
end
|
||||||
local supported, unsupportedWhy = SaveConvert.exportSupported(gameVersion)
|
local gen2Name = gen2CartName(gameVersion)
|
||||||
if not supported then return nil, unsupportedWhy end
|
if gen2Name then
|
||||||
-- Gen 2 has its own SRAM and its own codec, and needs no Gen 1 crosswalks.
|
return nil, gen2Name .. " uses a Gen 2 cart save; exporting one is not supported yet."
|
||||||
if Gen2Save.layoutFor(gameVersion) then
|
|
||||||
return Gen2Save.encode(saveTable, gameVersion, cartImage,
|
|
||||||
ensureGen2Data(gameVersion))
|
|
||||||
end
|
end
|
||||||
local data, derr = ensureData(gameVersion)
|
local data, derr = ensureData(gameVersion)
|
||||||
if not data then return nil, derr end
|
if not data then return nil, derr end
|
||||||
|
|||||||
@@ -451,13 +451,12 @@ function H.CutDownTreeOrGrass(ctx)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- DisappearWhirlpool is CutDownTreeOrGrass with PlayWhirlpoolSound in place of
|
-- DisappearWhirlpool is CutDownTreeOrGrass with PlayWhirlpoolSound in place of
|
||||||
-- OWCutAnimation, and the block only reaches the screen after the sound
|
-- OWCutAnimation: the same block write, the same redraw, then the surf wash the
|
||||||
-- -- engine/events/overworld.asm:1157-1164 (#1717, #1862)
|
-- port owns as World:playWhirlpoolSound. #1717
|
||||||
function H.DisappearWhirlpool(ctx)
|
function H.DisappearWhirlpool(ctx)
|
||||||
local index = ctx and ctx.cutWhirlpoolBlockIndex
|
local ret = H.CutDownTreeOrGrass(ctx)
|
||||||
local blockId = ctx and ctx.cutWhirlpoolReplacement
|
call(ctx, "playWhirlpoolSound")
|
||||||
call(ctx, "playWhirlpoolSound", index, blockId)
|
return ret
|
||||||
return nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- BlindingFlash sets STATUSFLAGS_FLASH_F and reloads the palettes. Setting
|
-- BlindingFlash sets STATUSFLAGS_FLASH_F and reloads the palettes. Setting
|
||||||
|
|||||||
@@ -152,12 +152,6 @@ local function answer(vm, value)
|
|||||||
vm.scriptVar = value or 0
|
vm.scriptVar = value or 0
|
||||||
end
|
end
|
||||||
|
|
||||||
-- WaitSFX (pokegold home/audio.asm); a test stub that calls a handler off
|
|
||||||
-- the coroutine has no sfx to drain.
|
|
||||||
local function drainSfx()
|
|
||||||
if coroutine.running() then coroutine.yield({ kind = "waitsfx" }) end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Every routine that ends `call GetPokemonName / jp
|
-- Every routine that ends `call GetPokemonName / jp
|
||||||
-- CopyPokemonName_Buffer1_Buffer3` puts a name where the next writetext's
|
-- CopyPokemonName_Buffer1_Buffer3` puts a name where the next writetext's
|
||||||
-- {STRBUF} will find it.
|
-- {STRBUF} will find it.
|
||||||
@@ -458,10 +452,8 @@ H.BugContestJudging = function(vm)
|
|||||||
local what = (h.monName and h.monName(entry.species)) or entry.species
|
local what = (h.monName and h.monName(entry.species)) or entry.species
|
||||||
or ""
|
or ""
|
||||||
vm:showRaw(Strings(row.page, who, what))
|
vm:showRaw(Strings(row.page, who, what))
|
||||||
-- pokegold engine/events/bug_contest/judging.asm:29-32
|
|
||||||
drainSfx()
|
|
||||||
if h.playSfxNamed then h.playSfxNamed(row.sfx) end
|
if h.playSfxNamed then h.playSfxNamed(row.sfx) end
|
||||||
vm:showRaw(Strings(row.score, entry.score or 0), nil, nil, true)
|
vm:showRaw(Strings(row.score, entry.score or 0))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
answer(vm, place)
|
answer(vm, place)
|
||||||
@@ -2041,10 +2033,12 @@ H.ProfOaksPCBoot = function(vm)
|
|||||||
vm:showRaw(Strings(OAK_PC_TEXT.counts, seen, caught))
|
vm:showRaw(Strings(OAK_PC_TEXT.counts, seen, caught))
|
||||||
local rating = findOakRating(caught)
|
local rating = findOakRating(caught)
|
||||||
local h = hooks(vm)
|
local h = hooks(vm)
|
||||||
-- pokegold engine/events/prof_oaks_pc.asm:18-20 PlaySFX / JoyWaitAorB / WaitSFX
|
|
||||||
drainSfx()
|
|
||||||
if h.playSfxNamed then h.playSfxNamed(rating.sfx) end
|
if h.playSfxNamed then h.playSfxNamed(rating.sfx) end
|
||||||
vm:showRaw(Strings(rating.text), nil, nil, true)
|
vm:showRaw(Strings(rating.text))
|
||||||
|
-- `call PlaySFX / call JoyWaitAorB / call WaitSFX`: the fanfare is left
|
||||||
|
-- playing under the rating text, and the caller (OaksLab's own script)
|
||||||
|
-- waits it out before closing the box.
|
||||||
|
coroutine.yield({ kind = "waitsfx" })
|
||||||
end
|
end
|
||||||
|
|
||||||
-- The whose-PC menu's PROF.OAK's PC row (src/ui/gen2/CenterPcMenu.lua) runs
|
-- The whose-PC menu's PROF.OAK's PC row (src/ui/gen2/CenterPcMenu.lua) runs
|
||||||
|
|||||||
+6
-10
@@ -700,10 +700,8 @@ local function runCmd(self, cmd, op)
|
|||||||
-- GetPocketName fills from ItemPocketNames: KEY ITEMs, BALLs and TMs
|
-- GetPocketName fills from ItemPocketNames: KEY ITEMs, BALLs and TMs
|
||||||
-- name their own pocket, not the ITEM one (data/text/common_2.asm
|
-- name their own pocket, not the ITEM one (data/text/common_2.asm
|
||||||
-- :1351, data/items/pocket_names.asm:10-13).
|
-- :1351, data/items/pocket_names.asm:10-13).
|
||||||
-- Script_specialsound's WaitSFX (scripting.asm:485): the box holds
|
|
||||||
-- its press until the jingle ends.
|
|
||||||
self:showRaw(Strings("{PLAYER} put the\n%s in\nthe %s.",
|
self:showRaw(Strings("{PLAYER} put the\n%s in\nthe %s.",
|
||||||
name, self:pocketName(item)), nil, nil, true)
|
name, self:pocketName(item)))
|
||||||
else
|
else
|
||||||
self:showRaw(Strings("The %s\nis full…", self:pocketName(item)))
|
self:showRaw(Strings("The %s\nis full…", self:pocketName(item)))
|
||||||
end
|
end
|
||||||
@@ -1512,8 +1510,8 @@ local function runCmd(self, cmd, op)
|
|||||||
self.playSoundFn(SFX_ITEM)
|
self.playSoundFn(SFX_ITEM)
|
||||||
end
|
end
|
||||||
-- FruitTreeScript's tail is `specialsound / itemnotify` with NOTHING
|
-- FruitTreeScript's tail is `specialsound / itemnotify` with NOTHING
|
||||||
-- between them (engine/events/fruit_trees.asm:23-24);
|
-- between them (engine/events/fruit_trees.asm:23-24), and Script_specialsound
|
||||||
-- Script_specialsound ends PlaySFX / WaitSFX (scripting.asm:484-485)
|
-- is a bare PlaySFX -- it does not wait either (scripting.asm:476-483).
|
||||||
-- The port used to park here on a `waitsfx`, which is the same seam
|
-- The port used to park here on a `waitsfx`, which is the same seam
|
||||||
-- GiveItemScript's did: this port's box takes its own button and pops on
|
-- GiveItemScript's did: this port's box takes its own button and pops on
|
||||||
-- it, so the park ran with an EMPTY state stack and the bare overworld
|
-- it, so the park ran with an EMPTY state stack and the bare overworld
|
||||||
@@ -1525,7 +1523,7 @@ local function runCmd(self, cmd, op)
|
|||||||
-- noun still comes from ItemPocketNames rather than from a third copy of
|
-- noun still comes from ItemPocketNames rather than from a third copy of
|
||||||
-- the literal (data/items/pocket_names.asm:10-13).
|
-- the literal (data/items/pocket_names.asm:10-13).
|
||||||
self:showRaw(Strings("{PLAYER} put the\n%s in\nthe %s.",
|
self:showRaw(Strings("{PLAYER} put the\n%s in\nthe %s.",
|
||||||
name, self:pocketName(item)), nil, nil, true)
|
name, self:pocketName(item)))
|
||||||
return "end"
|
return "end"
|
||||||
elseif op == "describedecoration" then
|
elseif op == "describedecoration" then
|
||||||
-- `describedecoration byte` picks one of five DECODESC_* arms
|
-- `describedecoration byte` picks one of five DECODESC_* arms
|
||||||
@@ -2429,7 +2427,7 @@ end
|
|||||||
-- the port's world does not tick while a box is on the stack (Game2:update
|
-- the port's world does not tick while a box is on the stack (Game2:update
|
||||||
-- stops at the top state), so the only clock that can count it is the box's;
|
-- stops at the top state), so the only clock that can count it is the box's;
|
||||||
-- World:showText is where it lands.
|
-- World:showText is where it lands.
|
||||||
function Vm:showRaw(body, stay, hold, sfxWait)
|
function Vm:showRaw(body, stay, hold)
|
||||||
if not body or body == "" then body = "..." end
|
if not body or body == "" then body = "..." end
|
||||||
if self.stringBuffer and self.stringBuffer ~= "" then
|
if self.stringBuffer and self.stringBuffer ~= "" then
|
||||||
body = body:gsub("{STRBUF}", self.stringBuffer)
|
body = body:gsub("{STRBUF}", self.stringBuffer)
|
||||||
@@ -2440,8 +2438,6 @@ function Vm:showRaw(body, stay, hold, sfxWait)
|
|||||||
text = body,
|
text = body,
|
||||||
stay = (stay or self:textStays()) and true or false,
|
stay = (stay or self:textStays()) and true or false,
|
||||||
hold = hold,
|
hold = hold,
|
||||||
-- pokegold engine/overworld/scripting.asm:485 WaitSFX
|
|
||||||
sfxWait = sfxWait and true or nil,
|
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -2650,7 +2646,7 @@ function Vm:resume(resumeValue)
|
|||||||
if req and req.kind == "text" and self.showTextFn then
|
if req and req.kind == "text" and self.showTextFn then
|
||||||
self.showTextFn(req.text, function()
|
self.showTextFn(req.text, function()
|
||||||
self:resume()
|
self:resume()
|
||||||
end, req.stay, req.hold, req.sfxWait)
|
end, req.stay, req.hold)
|
||||||
elseif req and req.kind == "wait" then
|
elseif req and req.kind == "wait" then
|
||||||
self.waitLeft = req.frames or 0
|
self.waitLeft = req.frames or 0
|
||||||
elseif req and req.kind == "waitbutton" then
|
elseif req and req.kind == "waitbutton" then
|
||||||
|
|||||||
+6
-42
@@ -116,36 +116,6 @@ function SyncEngine.overlaps(a, b)
|
|||||||
return aStart <= bEnd and bStart <= aEnd
|
return aStart <= bEnd and bStart <= aEnd
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Inline, under .meta, or under .remoteMeta, depending on the endpoint.
|
|
||||||
function SyncEngine.metaOf(row)
|
|
||||||
if type(row) ~= "table" then return nil end
|
|
||||||
if type(row.meta) == "table" then return row.meta end
|
|
||||||
if type(row.remoteMeta) == "table" then return row.remoteMeta end
|
|
||||||
return row
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Gen 2 stores playTime as a table, so meta.playTime is nil on a Gold save
|
|
||||||
-- and summary.timeText is the only field that survives.
|
|
||||||
local function playedMinutes(meta)
|
|
||||||
if type(meta) ~= "table" then return nil end
|
|
||||||
local summary = type(meta.summary) == "table" and meta.summary or nil
|
|
||||||
local text = summary and summary.timeText
|
|
||||||
if type(text) == "string" then
|
|
||||||
local hours, minutes = text:match("^(%d+):(%d%d)$")
|
|
||||||
if hours then return tonumber(hours) * 60 + tonumber(minutes) end
|
|
||||||
end
|
|
||||||
local seconds = tonumber(meta.playTime)
|
|
||||||
if seconds then return math.floor(seconds / 60) end
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Same minute of playtime means the same point in the playthrough, so there
|
|
||||||
-- is no fork. Unknown playtime on either side is NOT a match.
|
|
||||||
function SyncEngine.samePlaytime(a, b)
|
|
||||||
local left, right = playedMinutes(a), playedMinutes(b)
|
|
||||||
return left ~= nil and left == right
|
|
||||||
end
|
|
||||||
|
|
||||||
function SyncEngine.new(opts)
|
function SyncEngine.new(opts)
|
||||||
opts = opts or {}
|
opts = opts or {}
|
||||||
local eng = setmetatable({}, SyncEngine)
|
local eng = setmetatable({}, SyncEngine)
|
||||||
@@ -487,11 +457,7 @@ function SyncEngine:_planFrom(remoteState)
|
|||||||
if not row then
|
if not row then
|
||||||
self:_queueUpload(entry, key, false)
|
self:_queueUpload(entry, key, false)
|
||||||
elseif localChanged and remoteChanged then
|
elseif localChanged and remoteChanged then
|
||||||
if SyncEngine.samePlaytime(entry.meta, SyncEngine.metaOf(row)) then
|
|
||||||
self:_queueUpload(entry, key, true)
|
|
||||||
else
|
|
||||||
self:_addConflict(entry, key, row)
|
self:_addConflict(entry, key, row)
|
||||||
end
|
|
||||||
elseif localChanged then
|
elseif localChanged then
|
||||||
self:_queueUpload(entry, key, false)
|
self:_queueUpload(entry, key, false)
|
||||||
elseif remoteChanged and key ~= self.protectedKey then
|
elseif remoteChanged and key ~= self.protectedKey then
|
||||||
@@ -511,7 +477,12 @@ function SyncEngine:_planFrom(remoteState)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function SyncEngine:_addConflict(entry, key, row)
|
function SyncEngine:_addConflict(entry, key, row)
|
||||||
local remoteMeta = SyncEngine.metaOf(row)
|
local remoteMeta = row
|
||||||
|
if type(row.meta) == "table" then
|
||||||
|
remoteMeta = row.meta
|
||||||
|
elseif type(row.remoteMeta) == "table" then
|
||||||
|
remoteMeta = row.remoteMeta
|
||||||
|
end
|
||||||
self.conflicts[#self.conflicts + 1] = {
|
self.conflicts[#self.conflicts + 1] = {
|
||||||
key = key,
|
key = key,
|
||||||
version = entry.version,
|
version = entry.version,
|
||||||
@@ -557,14 +528,7 @@ function SyncEngine:_queueUpload(entry, key, force)
|
|||||||
end, function(e, res)
|
end, function(e, res)
|
||||||
if res.code == 409 then
|
if res.code == 409 then
|
||||||
local row = res.data or {}
|
local row = res.data or {}
|
||||||
-- Retried with force only once: a forced write that still 409s is a
|
|
||||||
-- real refusal, and retrying it would spin.
|
|
||||||
if not force
|
|
||||||
and SyncEngine.samePlaytime(entry.meta, SyncEngine.metaOf(row)) then
|
|
||||||
e:_queueUpload(entry, key, true)
|
|
||||||
else
|
|
||||||
e:_addConflict(entry, key, row)
|
e:_addConflict(entry, key, row)
|
||||||
end
|
|
||||||
if not e:busy() then e:_finish() end
|
if not e:busy() then e:_finish() end
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|||||||
+1
-3
@@ -360,8 +360,6 @@ local function vanillaUseOn(game, battle, id, target, list, moveIndex, picker)
|
|||||||
-- .useItem_partyMenu jumps back to StartMenu_Item once UseItem
|
-- .useItem_partyMenu jumps back to StartMenu_Item once UseItem
|
||||||
-- returns, cursor still on the candy (start_sub_menus.asm) -- so
|
-- returns, cursor still on the candy (start_sub_menus.asm) -- so
|
||||||
-- mashing A burns through a stack of them (#796)
|
-- mashing A burns through a stack of them (#796)
|
||||||
-- RareCandyText carries sound_get_item_1
|
|
||||||
-- (engine/menus/party_menu.asm:289-293)
|
|
||||||
showMessages(game, payload, function()
|
showMessages(game, payload, function()
|
||||||
local StatBox = require("src.battle.BattleState").StatBox
|
local StatBox = require("src.battle.BattleState").StatBox
|
||||||
game.stack:push(StatBox.new(game, target, function()
|
game.stack:push(StatBox.new(game, target, function()
|
||||||
@@ -402,7 +400,7 @@ local function vanillaUseOn(game, battle, id, target, list, moveIndex, picker)
|
|||||||
end
|
end
|
||||||
nextStep()
|
nextStep()
|
||||||
end))
|
end))
|
||||||
end, TextBox.soundOpts(game, "Get_Item1"))
|
end)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
-- HP medicine: fill the bar in the still-open picker first, then print
|
-- HP medicine: fill the bar in the still-open picker first, then print
|
||||||
|
|||||||
+38
-82
@@ -13,21 +13,9 @@ local Strings = require("src.core.Strings")
|
|||||||
|
|
||||||
local BoxMenu = {}
|
local BoxMenu = {}
|
||||||
|
|
||||||
-- PrintListMenuEntries prints the nickname at hlcoord 6,4 and PrintLevel
|
local function monLabel(game, mon)
|
||||||
-- one row down, 8 columns right (home/list_menu.asm:364-365, 459-461)
|
|
||||||
local function monRow(game, mon, value)
|
|
||||||
local def = game.data.pokemon[mon.species]
|
local def = game.data.pokemon[mon.species]
|
||||||
return {
|
return Strings("%s :L%d", mon.nickname or def.name, mon.level)
|
||||||
label = mon.nickname or def.name,
|
|
||||||
sub = Strings(":L%d", mon.level),
|
|
||||||
value = value,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
-- the $ff terminator's row (home/list_menu.asm:371-372, 523-528)
|
|
||||||
local function withCancel(items)
|
|
||||||
items[#items + 1] = { cancel = true, label = Strings("CANCEL") }
|
|
||||||
return items
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function monName(game, mon)
|
local function monName(game, mon)
|
||||||
@@ -73,15 +61,14 @@ local function withdraw(game)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
local items = {}
|
local items = {}
|
||||||
for i, mon in ipairs(box) do table.insert(items, monRow(game, mon, i)) end
|
for i, mon in ipairs(box) do
|
||||||
game.stack:push(ListMenu.new(game, nil, withCancel(items), {
|
table.insert(items, { label = monLabel(game, mon), value = i })
|
||||||
|
end
|
||||||
|
game.stack:push(ListMenu.new(game,
|
||||||
|
Strings("BOX %d (WITHDRAW)", game.save.currentBox), items, {
|
||||||
noSound = true, -- PCMainMenu holds BIT_NO_MENU_BUTTON_SOUND (#570)
|
noSound = true, -- PCMainMenu holds BIT_NO_MENU_BUTTON_SOUND (#570)
|
||||||
kind = "pc_box_withdraw",
|
kind = "pc_box_withdraw",
|
||||||
-- DisplayMonListMenu draws LIST_MENU_BOX over the PC menu, which stays
|
|
||||||
-- visible around it (home/list_menu.asm:29-31)
|
|
||||||
itemBox = true,
|
|
||||||
onChoose = function(item, list)
|
onChoose = function(item, list)
|
||||||
if item.cancel then list:close() return end
|
|
||||||
local mon = box[item.value]
|
local mon = box[item.value]
|
||||||
if not mon then return end
|
if not mon then return end
|
||||||
monSubmenu(game, "WITHDRAW", mon, function()
|
monSubmenu(game, "WITHDRAW", mon, function()
|
||||||
@@ -124,14 +111,12 @@ local function deposit(game)
|
|||||||
end
|
end
|
||||||
local items = {}
|
local items = {}
|
||||||
for i, mon in ipairs(game.save.party) do
|
for i, mon in ipairs(game.save.party) do
|
||||||
table.insert(items, monRow(game, mon, i))
|
table.insert(items, { label = monLabel(game, mon), value = i })
|
||||||
end
|
end
|
||||||
game.stack:push(ListMenu.new(game, nil, withCancel(items), {
|
game.stack:push(ListMenu.new(game, "PARTY (DEPOSIT)", items, {
|
||||||
noSound = true, -- PCMainMenu holds BIT_NO_MENU_BUTTON_SOUND (#570)
|
noSound = true, -- PCMainMenu holds BIT_NO_MENU_BUTTON_SOUND (#570)
|
||||||
kind = "pc_box_deposit",
|
kind = "pc_box_deposit",
|
||||||
itemBox = true,
|
|
||||||
onChoose = function(item, list)
|
onChoose = function(item, list)
|
||||||
if item.cancel then list:close() return end
|
|
||||||
local mon = game.save.party[item.value]
|
local mon = game.save.party[item.value]
|
||||||
if not mon then return end
|
if not mon then return end
|
||||||
local Follower = require("src.world.PikachuFollower")
|
local Follower = require("src.world.PikachuFollower")
|
||||||
@@ -178,13 +163,14 @@ local function release(game)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
local items = {}
|
local items = {}
|
||||||
for i, mon in ipairs(box) do table.insert(items, monRow(game, mon, i)) end
|
for i, mon in ipairs(box) do
|
||||||
game.stack:push(ListMenu.new(game, nil, withCancel(items), {
|
table.insert(items, { label = monLabel(game, mon), value = i })
|
||||||
|
end
|
||||||
|
game.stack:push(ListMenu.new(game,
|
||||||
|
Strings("BOX %d (RELEASE)", game.save.currentBox), items, {
|
||||||
noSound = true, -- PCMainMenu holds BIT_NO_MENU_BUTTON_SOUND (#570)
|
noSound = true, -- PCMainMenu holds BIT_NO_MENU_BUTTON_SOUND (#570)
|
||||||
kind = "pc_box_release",
|
kind = "pc_box_release",
|
||||||
itemBox = true,
|
onChoose = function(_, list)
|
||||||
onChoose = function(item, list)
|
|
||||||
if item.cancel then list:close() return end
|
|
||||||
local mon = box[list.index]
|
local mon = box[list.index]
|
||||||
if not mon then return end
|
if not mon then return end
|
||||||
local name = monName(game, mon)
|
local name = monName(game, mon)
|
||||||
@@ -216,67 +202,37 @@ local function release(game)
|
|||||||
}))
|
}))
|
||||||
end
|
end
|
||||||
|
|
||||||
-- DisplayChangeBoxMenu: engine/menus/save.asm:437-506
|
local function changeBox(game)
|
||||||
local function changeBoxMenu(game)
|
|
||||||
local boxes = Boxes.ensure(game.save)
|
local boxes = Boxes.ensure(game.save)
|
||||||
local items = {}
|
local items = {}
|
||||||
for i = 1, Boxes.COUNT do
|
for i = 1, Boxes.COUNT do
|
||||||
items[i] = {
|
local mark = i == game.save.currentBox and "*" or " "
|
||||||
label = Strings("BOX%2d", i),
|
table.insert(items, {
|
||||||
onSelect = function()
|
label = Strings("%sBOX %2d", mark, i),
|
||||||
game.save.currentBox = i
|
right = ("%d/%d"):format(#boxes[i], Boxes.CAPACITY),
|
||||||
|
value = i,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
game.stack:push(ListMenu.new(game, "CHANGE BOX", items, {
|
||||||
|
noSound = true, -- PCMainMenu holds BIT_NO_MENU_BUTTON_SOUND (#570)
|
||||||
|
kind = "pc_box_change",
|
||||||
|
onChoose = function(item, list)
|
||||||
|
-- the original asks BEFORE switching ("When you change a #MON
|
||||||
|
-- BOX, data will be saved. OK?"); declining aborts the change
|
||||||
|
game.stack:push(TextBox.new(game,
|
||||||
|
Strings("When you change a\nPOKéMON BOX, data\nwill be saved. OK?"), nil, {
|
||||||
|
noSound = true,
|
||||||
|
choice = function(yes)
|
||||||
|
if not yes then return end
|
||||||
|
game.save.currentBox = item.value
|
||||||
if game.writeSave then game:writeSave() end
|
if game.writeSave then game:writeSave() end
|
||||||
-- ChangeBox (engine/menus/save.asm) rings SFX_SAVE after SaveGameData (#1044)
|
-- ChangeBox (engine/menus/save.asm) rings SFX_SAVE after SaveGameData (#1044)
|
||||||
require("src.core.Sound").play(game.data, "Save")
|
require("src.core.Sound").play(game.data, "Save")
|
||||||
|
list:close()
|
||||||
end,
|
end,
|
||||||
}
|
}))
|
||||||
end
|
|
||||||
local menu = Menu.new(game, items, {
|
|
||||||
tx = 11, ty = 0, tw = 9, th = 14, rowStep = 1, itemY = 1, noSound = true,
|
|
||||||
})
|
|
||||||
menu.kind = "pc_box_change" -- screen.render_visible identity
|
|
||||||
menu.index = math.max(1, math.min(Boxes.COUNT, game.save.currentBox or 1))
|
|
||||||
local t = game.data.text
|
|
||||||
local baseDraw = menu.draw
|
|
||||||
function menu:draw()
|
|
||||||
-- ChooseABoxText goes to the standard box, over BillsPCMenu's "What?"
|
|
||||||
Font.drawBox(0, 12, 20, 6)
|
|
||||||
love.graphics.setColor(0, 0, 0, 1)
|
|
||||||
local y = 112
|
|
||||||
for line in ((t._ChooseABoxText or Strings("Choose a\nPOKéMON BOX."))
|
|
||||||
.. "\n"):gmatch("([^\n]*)\n") do
|
|
||||||
Font.draw(line, 8, y)
|
|
||||||
y = y + 16
|
|
||||||
end
|
|
||||||
Font.drawBox(0, 0, 11, 4)
|
|
||||||
love.graphics.setColor(0, 0, 0, 1)
|
|
||||||
Font.draw(Strings("BOX No."), 8, 16)
|
|
||||||
local n = game.save.currentBox or 1
|
|
||||||
Font.draw(tostring(n), n >= 10 and 64 or 72, 16)
|
|
||||||
baseDraw(self)
|
|
||||||
love.graphics.setColor(0, 0, 0, 1)
|
|
||||||
for i = 1, Boxes.COUNT do
|
|
||||||
if #boxes[i] > 0 then ListMenu.drawBall(148, i * 8 + 4) end
|
|
||||||
end
|
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
|
||||||
end
|
|
||||||
game.stack:push(menu)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- ChangeBox (engine/menus/save.asm:358-368) asks before showing the menu
|
|
||||||
-- and returns to the PC menu on No.
|
|
||||||
local function changeBox(game)
|
|
||||||
local t = game.data.text
|
|
||||||
local ask = TextBox.new(game, t._WhenYouChangeBoxText
|
|
||||||
or Strings("When you change a\nPOKéMON BOX, data\vwill be saved.\fIs that okay?"),
|
|
||||||
nil, {
|
|
||||||
noSound = true,
|
|
||||||
choice = function(yes)
|
|
||||||
if yes then changeBoxMenu(game) end
|
|
||||||
end,
|
end,
|
||||||
})
|
}))
|
||||||
ask.kind = "pc_box_change"
|
|
||||||
game.stack:push(ask)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- bills_pc.asm BillsPCMenu chrome: What? text box + BOX No. overlay
|
-- bills_pc.asm BillsPCMenu chrome: What? text box + BOX No. overlay
|
||||||
|
|||||||
+5
-37
@@ -87,15 +87,6 @@ local function frameSheet(game)
|
|||||||
return frameCache[path]
|
return frameCache[path]
|
||||||
end
|
end
|
||||||
|
|
||||||
-- one tile of the sheet, for the CONTENTS screen's divider column
|
|
||||||
-- (engine/menus/pokedex.asm:350, DrawPokedexVerticalLine)
|
|
||||||
function DexEntryMenu.tile(game, code, tx, ty)
|
|
||||||
local frame = frameSheet(game)
|
|
||||||
if not (frame and code) then return end
|
|
||||||
local quad = frame.quads[code - 0x60]
|
|
||||||
if quad then love.graphics.draw(frame.img, quad, tx * 8, ty * 8) end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- engine/menus/pokedex.asm:601
|
-- engine/menus/pokedex.asm:601
|
||||||
local DIVIDER = {
|
local DIVIDER = {
|
||||||
0x68, 0x69, 0x6B, 0x69, 0x6B, 0x69, 0x6B, 0x69, 0x6B, 0x6B,
|
0x68, 0x69, 0x6B, 0x69, 0x6B, 0x69, 0x6B, 0x69, 0x6B, 0x6B,
|
||||||
@@ -115,28 +106,14 @@ function DexEntryMenu.new(game, speciesOrOpts, onDone)
|
|||||||
self.sprite = ok and img or nil
|
self.sprite = ok and img or nil
|
||||||
self.spriteTrueColor = self.sprite and trueColor or false
|
self.spriteTrueColor = self.sprite and trueColor or false
|
||||||
self.page = 1
|
self.page = 1
|
||||||
self.blink = 0
|
|
||||||
local pages = descPages(game, self.def, forceOwned)
|
local pages = descPages(game, self.def, forceOwned)
|
||||||
self.pageCount = pages and #pages or 1
|
self.pageCount = pages and #pages or 1
|
||||||
-- engine/menus/pokedex.asm:500-506, home/pokemon.asm:145-148
|
require("src.core.Sound").playCry(game.data, species)
|
||||||
self.crySrc = require("src.core.Sound").playCry(game.data, species)
|
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
-- home/pokemon.asm:148 (jp WaitForSoundToFinish)
|
|
||||||
function DexEntryMenu:crying()
|
|
||||||
local src = self.crySrc
|
|
||||||
if not src then return false end
|
|
||||||
local ok, playing = pcall(src.isPlaying, src)
|
|
||||||
if ok and playing then return true end
|
|
||||||
self.crySrc = nil
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
function DexEntryMenu:update(dt)
|
function DexEntryMenu:update(dt)
|
||||||
local input = self.game.input
|
local input = self.game.input
|
||||||
self.blink = ((self.blink or 0) + 1) % 60
|
|
||||||
if self:crying() then return end
|
|
||||||
if input:wasPressed("a") or input:wasPressed("b") then
|
if input:wasPressed("a") or input:wasPressed("b") then
|
||||||
-- home/text.asm:245
|
-- home/text.asm:245
|
||||||
if self.page < (self.pageCount or 1) then
|
if self.page < (self.pageCount or 1) then
|
||||||
@@ -150,18 +127,15 @@ end
|
|||||||
|
|
||||||
function DexEntryMenu:draw()
|
function DexEntryMenu:draw()
|
||||||
DexEntryMenu.render(self.game, self.def, self.sprite, self.forceOwned,
|
DexEntryMenu.render(self.game, self.def, self.sprite, self.forceOwned,
|
||||||
self.spriteTrueColor, self.page,
|
self.spriteTrueColor, self.page)
|
||||||
{ crying = self:crying(),
|
|
||||||
arrow = (self.blink or 0) < 30 })
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Static entry-page renderer, shared with the printer stand-in
|
-- Static entry-page renderer, shared with the printer stand-in
|
||||||
-- (src/core/Printer.lua renders the same page into a PNG the way
|
-- (src/core/Printer.lua renders the same page into a PNG the way
|
||||||
-- PrintPokedexEntry rendered it to the Game Boy Printer).
|
-- PrintPokedexEntry rendered it to the Game Boy Printer).
|
||||||
-- engine/menus/pokedex.asm:399
|
-- engine/menus/pokedex.asm:399
|
||||||
function DexEntryMenu.render(game, def, sprite, forceOwned, trueColor, page, state)
|
function DexEntryMenu.render(game, def, sprite, forceOwned, trueColor, page)
|
||||||
page = page or 1
|
page = page or 1
|
||||||
state = state or {}
|
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
love.graphics.rectangle("fill", 0, 0, 160, 144)
|
love.graphics.rectangle("fill", 0, 0, 160, 144)
|
||||||
local frame = frameSheet(game)
|
local frame = frameSheet(game)
|
||||||
@@ -211,12 +185,6 @@ function DexEntryMenu.render(game, def, sprite, forceOwned, trueColor, page, sta
|
|||||||
Font.draw(Strings("No.") .. ("%0" .. digits .. "d"):format(def.dex or 0),
|
Font.draw(Strings("No.") .. ("%0" .. digits .. "d"):format(def.dex or 0),
|
||||||
16, 64)
|
16, 64)
|
||||||
local owned = ownedFor(game, def, forceOwned)
|
local owned = ownedFor(game, def, forceOwned)
|
||||||
-- engine/menus/pokedex.asm:516: everything below the divider waits on the
|
|
||||||
-- cry the line above it started
|
|
||||||
if state.crying then
|
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
-- engine/menus/pokedex.asm:449, numbers only once owned
|
-- engine/menus/pokedex.asm:449, numbers only once owned
|
||||||
if owned and e.heightFt then
|
if owned and e.heightFt then
|
||||||
if e.heightM then
|
if e.heightM then
|
||||||
@@ -234,8 +202,8 @@ function DexEntryMenu.render(game, def, sprite, forceOwned, trueColor, page, sta
|
|||||||
for i, line in ipairs(lines) do
|
for i, line in ipairs(lines) do
|
||||||
Font.draw(line, 8, 72 + i * 16)
|
Font.draw(line, 8, 72 + i * 16)
|
||||||
end
|
end
|
||||||
-- home/text.asm:245 places the '▼' and home/joypad2.asm:55-83 blinks it
|
-- home/text.asm:245
|
||||||
if page < #pages and state.arrow ~= false then
|
if page < #pages then
|
||||||
Font.drawCode(Theme.moreArrow, 144, 128)
|
Font.drawCode(Theme.moreArrow, 144, 128)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
-- Whether a Pokémon's level is printed on a screen that would normally
|
|
||||||
-- print it (RFC 0019).
|
|
||||||
--
|
|
||||||
-- Every Gen 1 screen that shows a level does it with the same pokered rule
|
|
||||||
-- -- home/pokemon.asm:335-345 PrintLevel: the <LV> tile, then the digits
|
|
||||||
-- left-aligned after it, with a level of 100 writing its third digit back
|
|
||||||
-- over the tile -- and each screen hand-rolls that rule against its own
|
|
||||||
-- coordinates. This module does not touch any of that. It answers one
|
|
||||||
-- question, in one place, so that a mode which wants the number off does
|
|
||||||
-- not have to know four call sites and a glyph code.
|
|
||||||
--
|
|
||||||
-- Default is true, and `Runtime.wantsHook` is checked first, so a build
|
|
||||||
-- with no mod wrapping the hook prints exactly what it always did and pays
|
|
||||||
-- nothing for the seam on a per-frame draw path.
|
|
||||||
--
|
|
||||||
-- `where` names the surface rather than the widget, because the number
|
|
||||||
-- means different things on different screens: on the battle HUD an
|
|
||||||
-- opponent's level is information about them, on the party and status
|
|
||||||
-- screens your own level is information about you. A mode can hide one and
|
|
||||||
-- keep the other.
|
|
||||||
|
|
||||||
local Runtime = require("src.mods.Runtime")
|
|
||||||
|
|
||||||
local LevelDisplay = {}
|
|
||||||
|
|
||||||
-- where: "battle.enemy" | "battle.player" | "party" | "summary"
|
|
||||||
function LevelDisplay.visible(mon, where, game)
|
|
||||||
if not Runtime.wantsHook("pokemon.level_visible") then return true end
|
|
||||||
return Runtime.call("pokemon.level_visible", function() return true end,
|
|
||||||
mon, { where = where, game = game }) ~= false
|
|
||||||
end
|
|
||||||
|
|
||||||
return LevelDisplay
|
|
||||||
+26
-41
@@ -120,9 +120,8 @@ function ListMenu.new(game, title, items, opts)
|
|||||||
-- so their lists opt out of the A/B beep the same way Menu's noSound does
|
-- so their lists opt out of the A/B beep the same way Menu's noSound does
|
||||||
self.noSound = opts.noSound or false
|
self.noSound = opts.noSound or false
|
||||||
-- the bag's item list: a partial box the map stays visible around, not a
|
-- the bag's item list: a partial box the map stays visible around, not a
|
||||||
-- screen of its own (home/list_menu.asm:29-31). Every DisplayListMenuID
|
-- screen of its own (home/list_menu.asm:29-31)
|
||||||
-- caller gets the same box, the PC item lists included (#1845).
|
self.itemBox = opts.itemBox or false
|
||||||
self.itemBox = opts.itemBox or opts.messageBox or false
|
|
||||||
if self.itemBox then
|
if self.itemBox then
|
||||||
self.isOpaque = false
|
self.isOpaque = false
|
||||||
-- keep RunDefaultPaletteCommand's last palette: ItemMenuLoop never sets
|
-- keep RunDefaultPaletteCommand's last palette: ItemMenuLoop never sets
|
||||||
@@ -257,34 +256,6 @@ function ListMenu:close()
|
|||||||
if top == self then self.game.stack:pop() end
|
if top == self then self.game.stack:pop() end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- standard bottom text box (PrintText); long prompts wrap and keep
|
|
||||||
-- their last two lines, like the GB's scrolled box (#115/#174)
|
|
||||||
local function drawMessageBox(self)
|
|
||||||
Font.drawBox(0, 12, 20, 6)
|
|
||||||
love.graphics.setColor(0, 0, 0, 1)
|
|
||||||
if not self.footer then return end
|
|
||||||
local flat = {}
|
|
||||||
for _, page in ipairs(require("src.render.TextBox").paginate(self.footer)) do
|
|
||||||
for _, line in ipairs(page) do flat[#flat + 1] = line end
|
|
||||||
end
|
|
||||||
local y = 112
|
|
||||||
for i = math.max(1, #flat - 1), #flat do
|
|
||||||
Font.draw(flat[i], 8, y)
|
|
||||||
y = y + 16
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- the Pokédex owned-ball marker tile, also the CHANGE BOX screen's
|
|
||||||
-- PokeballTileGraphics marker (engine/menus/save.asm:495-499)
|
|
||||||
function ListMenu.drawBall(x, y)
|
|
||||||
local r, g, b, a = love.graphics.getColor()
|
|
||||||
love.graphics.circle("fill", x, y, 3.5)
|
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
|
||||||
love.graphics.rectangle("fill", x - 3.5, y - 0.5, 7, 1)
|
|
||||||
love.graphics.circle("fill", x, y, 1.2)
|
|
||||||
love.graphics.setColor(r, g, b, a)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- PrintListMenuEntries, minus the price column StartMenu_Item never asks for
|
-- PrintListMenuEntries, minus the price column StartMenu_Item never asks for
|
||||||
-- (wPrintItemPrices = 0, engine/menus/start_sub_menus.asm)
|
-- (wPrintItemPrices = 0, engine/menus/start_sub_menus.asm)
|
||||||
function ListMenu:drawItemBox()
|
function ListMenu:drawItemBox()
|
||||||
@@ -303,10 +274,7 @@ function ListMenu:drawItemBox()
|
|||||||
if item.cancel then sawCancel = true end
|
if item.cancel then sawCancel = true end
|
||||||
local y = ITEM_TOP_Y + (row - 1) * 16
|
local y = ITEM_TOP_Y + (row - 1) * 16
|
||||||
Font.draw(item.label, ITEM_NAME_X, y)
|
Font.draw(item.label, ITEM_NAME_X, y)
|
||||||
if item.sub then
|
if item.right then
|
||||||
-- PrintLevel, one row down and 8 columns right (home/list_menu.asm:459-461)
|
|
||||||
Font.draw(item.sub, ITEM_QTY_X, y + 8)
|
|
||||||
elseif item.right then
|
|
||||||
-- '×' at column 14, PrintNumber's two right-aligned digits after it
|
-- '×' at column 14, PrintNumber's two right-aligned digits after it
|
||||||
-- (home/list_menu.asm:479-490)
|
-- (home/list_menu.asm:479-490)
|
||||||
local count = item.right:sub(2)
|
local count = item.right:sub(2)
|
||||||
@@ -326,9 +294,6 @@ function ListMenu:drawItemBox()
|
|||||||
if shown == self.rows and not sawCancel then
|
if shown == self.rows and not sawCancel then
|
||||||
Font.drawCode(Theme.moreArrow, ITEM_MORE_X, ITEM_MORE_Y)
|
Font.drawCode(Theme.moreArrow, ITEM_MORE_X, ITEM_MORE_Y)
|
||||||
end
|
end
|
||||||
-- players_pc.asm:97/151/205 PrintText the prompt before DisplayListMenuID,
|
|
||||||
-- so the bottom box sits under the list from the first frame
|
|
||||||
if self.messageBox or self.footer then drawMessageBox(self) end
|
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -361,7 +326,13 @@ function ListMenu:draw()
|
|||||||
-- one blank glyph after the name, measured in glyph advances rather
|
-- one blank glyph after the name, measured in glyph advances rather
|
||||||
-- than bytes: NIDORAN♂/♀ carry a multi-byte charmap entry, so
|
-- than bytes: NIDORAN♂/♀ carry a multi-byte charmap entry, so
|
||||||
-- `#item.label` overcounted by 2 and pushed their ball 16px right (#285)
|
-- `#item.label` overcounted by 2 and pushed their ball 16px right (#285)
|
||||||
ListMenu.drawBall(16 + Font.width(label) + 8 + 3, y + 3)
|
local bx = 16 + Font.width(label) + 8 + 3
|
||||||
|
local by = y + 3
|
||||||
|
love.graphics.circle("fill", bx, by, 3.5)
|
||||||
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
|
love.graphics.rectangle("fill", bx - 3.5, by - 0.5, 7, 1)
|
||||||
|
love.graphics.circle("fill", bx, by, 1.2)
|
||||||
|
love.graphics.setColor(unpack(textColor))
|
||||||
end
|
end
|
||||||
if item.right then
|
if item.right then
|
||||||
Font.draw(item.right, 160 - 8 - Font.width(item.right), y)
|
Font.draw(item.right, 160 - 8 - Font.width(item.right), y)
|
||||||
@@ -389,8 +360,22 @@ function ListMenu:draw()
|
|||||||
local money = ("¥%d"):format(self.money and self.money() or 0)
|
local money = ("¥%d"):format(self.money and self.money() or 0)
|
||||||
Font.draw(money, 152 - Font.width(money), 8)
|
Font.draw(money, 152 - Font.width(money), 8)
|
||||||
end
|
end
|
||||||
if self.dialogue then
|
if self.dialogue or (self.messageBox and self.footer) then
|
||||||
drawMessageBox(self)
|
-- standard bottom text box (PrintText); long prompts wrap and keep
|
||||||
|
-- their last two lines, like the GB's scrolled box (#115/#174)
|
||||||
|
Font.drawBox(0, 12, 20, 6)
|
||||||
|
love.graphics.setColor(0, 0, 0, 1)
|
||||||
|
if self.footer then
|
||||||
|
local flat = {}
|
||||||
|
for _, page in ipairs(require("src.render.TextBox").paginate(self.footer)) do
|
||||||
|
for _, line in ipairs(page) do flat[#flat + 1] = line end
|
||||||
|
end
|
||||||
|
local y = 112
|
||||||
|
for i = math.max(1, #flat - 1), #flat do
|
||||||
|
Font.draw(flat[i], 8, y)
|
||||||
|
y = y + 16
|
||||||
|
end
|
||||||
|
end
|
||||||
elseif self.footer then
|
elseif self.footer then
|
||||||
-- bare footer (bag money line, etc.)
|
-- bare footer (bag money line, etc.)
|
||||||
local flat = {}
|
local flat = {}
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
-- Scrolling for a menu line too long for its box: hold, step one character
|
|
||||||
-- at a time, hold on the tail, snap back. Shared by the Gen 1 four-box
|
|
||||||
-- viewport (src/ui/OptionRows.lua) and the Gen 2 textbox
|
|
||||||
-- (src/ui/gen2/OptionsMenu.lua) -- different widths, same problem.
|
|
||||||
|
|
||||||
local Marquee = {}
|
|
||||||
|
|
||||||
Marquee.HOLD = 1.0
|
|
||||||
Marquee.STEP = 0.25
|
|
||||||
|
|
||||||
-- Only ever one row scrolls (the cursor's), so one shared phase keeps that
|
|
||||||
-- row's label and value stepping together.
|
|
||||||
local phase = { key = nil, start = 0 }
|
|
||||||
|
|
||||||
local function clock()
|
|
||||||
local timer = love and love.timer and love.timer.getTime
|
|
||||||
return timer and timer() or 0
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Pure, so a test can step it without a clock.
|
|
||||||
function Marquee.at(text, maxChars, elapsed)
|
|
||||||
text = text or ""
|
|
||||||
local over = #text - maxChars
|
|
||||||
if over <= 0 then return text end
|
|
||||||
local span = Marquee.HOLD * 2 + over * Marquee.STEP
|
|
||||||
local now = elapsed % span
|
|
||||||
local offset
|
|
||||||
if now < Marquee.HOLD then
|
|
||||||
offset = 0
|
|
||||||
elseif now < Marquee.HOLD + over * Marquee.STEP then
|
|
||||||
offset = math.floor((now - Marquee.HOLD) / Marquee.STEP)
|
|
||||||
else
|
|
||||||
offset = over
|
|
||||||
end
|
|
||||||
return text:sub(offset + 1, offset + maxChars)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- `key` identifies the highlighted row; changing it restarts the cycle.
|
|
||||||
function Marquee.scroll(text, maxChars, key)
|
|
||||||
if not text or #text <= maxChars then return text or "" end
|
|
||||||
if phase.key ~= key then phase.key, phase.start = key, clock() end
|
|
||||||
return Marquee.at(text, maxChars, clock() - phase.start)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Marquee.clip(text, maxChars)
|
|
||||||
return (text or ""):sub(1, maxChars)
|
|
||||||
end
|
|
||||||
|
|
||||||
return Marquee
|
|
||||||
@@ -120,17 +120,14 @@ function MoveLearnMenu:finish(learned)
|
|||||||
local opts
|
local opts
|
||||||
if learned then
|
if learned then
|
||||||
-- pokered pages this as four texts in a row; _ForgotAndText carries
|
-- pokered pages this as four texts in a row; _ForgotAndText carries
|
||||||
-- the "And..." tail. Each text_pause holds the box, and the first one
|
-- the "And..." tail
|
||||||
-- is followed by SFX_SWAP (engine/pokemon/learn_move.asm:208-222).
|
|
||||||
msg = romText(game.data, "_OneTwoAndText", "1, 2 and...")
|
msg = romText(game.data, "_OneTwoAndText", "1, 2 and...")
|
||||||
.. TextBox.PAUSE
|
|
||||||
.. romText(game.data, "_PoofText", " Poof!")
|
.. romText(game.data, "_PoofText", " Poof!")
|
||||||
.. TextBox.PAUSE
|
|
||||||
.. romText(game.data, "_ForgotAndText",
|
.. romText(game.data, "_ForgotAndText",
|
||||||
"\f%s forgot\n%s!\fAnd...", name, self.forgot)
|
"\f%s forgot\n%s!\fAnd...", name, self.forgot)
|
||||||
.. "\f" .. romText(game.data, "_LearnedMove1Text",
|
.. "\f" .. romText(game.data, "_LearnedMove1Text",
|
||||||
"%s learned\n%s!", name, mdef.name)
|
"%s learned\n%s!", name, mdef.name)
|
||||||
opts = { pauseSounds = { "Swap" }, auto = { sound = function()
|
opts = { auto = { sound = function()
|
||||||
return require("src.core.Sound").play(game.data, self.learnedSound)
|
return require("src.core.Sound").play(game.data, self.learnedSound)
|
||||||
end, wait = true } }
|
end, wait = true } }
|
||||||
else
|
else
|
||||||
|
|||||||
+3
-19
@@ -7,18 +7,12 @@
|
|||||||
-- rows that open something instead (MODS, CANCEL stays the caller's).
|
-- rows that open something instead (MODS, CANCEL stays the caller's).
|
||||||
|
|
||||||
local Font = require("src.render.Font")
|
local Font = require("src.render.Font")
|
||||||
local Marquee = require("src.ui.Marquee")
|
|
||||||
local Theme = require("src.ui.Theme")
|
local Theme = require("src.ui.Theme")
|
||||||
|
|
||||||
local OptionRows = {}
|
local OptionRows = {}
|
||||||
|
|
||||||
OptionRows.VISIBLE = 4 -- option boxes on screen at once (4 tiles each)
|
OptionRows.VISIBLE = 4 -- option boxes on screen at once (4 tiles each)
|
||||||
|
|
||||||
-- Font.drawBox(0, y, 20, 4) spends column 19 on the frame, so a line drawn
|
|
||||||
-- to the 160px screen edge prints over its own border.
|
|
||||||
local CONTENT_RIGHT = 152
|
|
||||||
local function fits(x) return math.floor((CONTENT_RIGHT - x) / 8) end
|
|
||||||
|
|
||||||
-- keep the cursor's box inside the viewport; the fixed bottom row shows
|
-- keep the cursor's box inside the viewport; the fixed bottom row shows
|
||||||
-- the tail of the list
|
-- the tail of the list
|
||||||
function OptionRows.clampScroll(index, scroll, total, bottomRow)
|
function OptionRows.clampScroll(index, scroll, total, bottomRow)
|
||||||
@@ -33,7 +27,7 @@ function OptionRows.clampScroll(index, scroll, total, bottomRow)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- one bordered box per row, label line + value line, with the fixed
|
-- one bordered box per row, label line + value line, with the fixed
|
||||||
-- bottom line below (BACK in the options menu, the manager's footer)
|
-- bottom line below (CANCEL in the options menu, the manager's footer)
|
||||||
function OptionRows.draw(game, rows, index, scroll, bottomLabel, bottomRow)
|
function OptionRows.draw(game, rows, index, scroll, bottomLabel, bottomRow)
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
love.graphics.rectangle("fill", 0, 0, 160, 144)
|
love.graphics.rectangle("fill", 0, 0, 160, 144)
|
||||||
@@ -43,18 +37,8 @@ function OptionRows.draw(game, rows, index, scroll, bottomLabel, bottomRow)
|
|||||||
if not row then break end
|
if not row then break end
|
||||||
Font.drawBox(0, (slot - 1) * 4, 20, 4)
|
Font.drawBox(0, (slot - 1) * 4, 20, 4)
|
||||||
love.graphics.setColor(0, 0, 0, 1)
|
love.graphics.setColor(0, 0, 0, 1)
|
||||||
local label = row.label or ""
|
Font.draw(row.label, 16, ((slot - 1) * 4 + 1) * 8)
|
||||||
local value = row.value and row.value(game) or ""
|
Font.draw(row.value and row.value(game) or "", 24, ((slot - 1) * 4 + 2) * 8)
|
||||||
if i == index then
|
|
||||||
local key = tostring(row.id or row.label) .. "\0" .. tostring(value)
|
|
||||||
label = Marquee.scroll(label, fits(16), key)
|
|
||||||
value = Marquee.scroll(value, fits(24), key)
|
|
||||||
else
|
|
||||||
label = Marquee.clip(label, fits(16))
|
|
||||||
value = Marquee.clip(value, fits(24))
|
|
||||||
end
|
|
||||||
Font.draw(label, 16, ((slot - 1) * 4 + 1) * 8)
|
|
||||||
Font.draw(value, 24, ((slot - 1) * 4 + 2) * 8)
|
|
||||||
if i == index then
|
if i == index then
|
||||||
Font.drawCode(Theme.cursor, 8, ((slot - 1) * 4 + 1) * 8)
|
Font.drawCode(Theme.cursor, 8, ((slot - 1) * 4 + 1) * 8)
|
||||||
end
|
end
|
||||||
|
|||||||
+21
-137
@@ -11,12 +11,10 @@
|
|||||||
-- bottom line like pokered's.
|
-- bottom line like pokered's.
|
||||||
|
|
||||||
local PaletteFX = require("src.render.PaletteFX")
|
local PaletteFX = require("src.render.PaletteFX")
|
||||||
local Palette = require("src.render.Palette")
|
|
||||||
local Pipelines = require("src.render.Pipelines")
|
local Pipelines = require("src.render.Pipelines")
|
||||||
local Tilt = require("src.render.Tilt")
|
local Tilt = require("src.render.Tilt")
|
||||||
local ShaderFX = require("src.render.ShaderFX")
|
local ShaderFX = require("src.render.ShaderFX")
|
||||||
local Zoom = require("src.render.Zoom")
|
local Zoom = require("src.render.Zoom")
|
||||||
local Letterbox = require("src.render.Letterbox")
|
|
||||||
local TileRenderer = require("src.render.TileRenderer")
|
local TileRenderer = require("src.render.TileRenderer")
|
||||||
local GameSpeed = require("src.core.GameSpeed")
|
local GameSpeed = require("src.core.GameSpeed")
|
||||||
local GameVersion = require("src.core.GameVersion")
|
local GameVersion = require("src.core.GameVersion")
|
||||||
@@ -129,6 +127,14 @@ local function stepVolume(v, dir)
|
|||||||
return math.max(0, math.min(7, (v or 7) + dir))
|
return math.max(0, math.min(7, (v or 7) + dir))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function colorIndex(opts)
|
||||||
|
local cur = opts.colors or "gbc"
|
||||||
|
for i, m in ipairs(PaletteFX.MODES) do
|
||||||
|
if m == cur then return i end
|
||||||
|
end
|
||||||
|
return 1
|
||||||
|
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
|
||||||
@@ -335,14 +341,15 @@ local function buildRows(game)
|
|||||||
end },
|
end },
|
||||||
{ id = "colors", label = Strings("COLORS"),
|
{ id = "colors", label = Strings("COLORS"),
|
||||||
value = function(g)
|
value = function(g)
|
||||||
local id = g.save.options.palette
|
|
||||||
if id and id ~= "" then
|
|
||||||
return Palette.label(id) or PaletteFX.modeLabel(g.save.options.colors or "gbc")
|
|
||||||
end
|
|
||||||
return PaletteFX.modeLabel(g.save.options.colors or "gbc")
|
return PaletteFX.modeLabel(g.save.options.colors or "gbc")
|
||||||
end,
|
end,
|
||||||
activate = function(g)
|
step = function(g, dir)
|
||||||
require("src.ui.Screens").push(g, "PaletteScreen")
|
local o = g.save.options
|
||||||
|
local i = colorIndex(o)
|
||||||
|
i = wrapIndex(i - 1 + dir, #PaletteFX.MODES) + 1
|
||||||
|
o.colors = PaletteFX.MODES[i]
|
||||||
|
PaletteFX.setMode(o.colors)
|
||||||
|
return true
|
||||||
end },
|
end },
|
||||||
{ id = "tilt", label = Strings("TILT"),
|
{ id = "tilt", label = Strings("TILT"),
|
||||||
value = function(g) return Tilt.levelLabel(g.save.options.tilt or 0) end,
|
value = function(g) return Tilt.levelLabel(g.save.options.tilt or 0) end,
|
||||||
@@ -369,16 +376,6 @@ local function buildRows(game)
|
|||||||
-- cycling in place on this row.
|
-- cycling in place on this row.
|
||||||
-- ShaderFXScreen.lua does the actual list/activate; this row only opens
|
-- ShaderFXScreen.lua does the actual list/activate; this row only opens
|
||||||
-- it and shows what is currently active.
|
-- it and shows what is currently active.
|
||||||
{ id = "uiLetterbox", label = Strings("UI LETTERBOX"),
|
|
||||||
value = function(g)
|
|
||||||
return Strings(Letterbox.label(g.save.options.uiLetterbox))
|
|
||||||
end,
|
|
||||||
step = function(g, dir)
|
|
||||||
local o = g.save.options
|
|
||||||
o.uiLetterbox = Letterbox.cycle(o.uiLetterbox, dir)
|
|
||||||
Letterbox.setMode(o.uiLetterbox)
|
|
||||||
return true
|
|
||||||
end },
|
|
||||||
{ id = "shaderfx", label = Strings("SHADER FX"),
|
{ id = "shaderfx", label = Strings("SHADER FX"),
|
||||||
value = function(g)
|
value = function(g)
|
||||||
return shaderfxLabel(ShaderFX.activeEntry("main"))
|
return shaderfxLabel(ShaderFX.activeEntry("main"))
|
||||||
@@ -646,90 +643,8 @@ local function buildRows(game)
|
|||||||
return rows
|
return rows
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Grouping runs AFTER the ui.options.rows hook and never touches self.rows,
|
|
||||||
-- so a mod still sees and edits the flat list it always did; a row in no
|
|
||||||
-- group stays where it is, which is where a mod's own additions land.
|
|
||||||
local GROUPS = {
|
|
||||||
{ id = "group.battle", label = "BATTLE OPTIONS",
|
|
||||||
members = { "animations", "battleStyle", "battleLayout", "battleFit",
|
|
||||||
"battleHud", "battleBg" } },
|
|
||||||
{ id = "group.audio", label = "AUDIO",
|
|
||||||
members = { "musicVol", "sfxVol", "pikaVol", "musicFilter" } },
|
|
||||||
{ id = "group.video", label = "VIDEO",
|
|
||||||
members = { "uiLayout", "videoMode", "orientation", "faithfulRes",
|
|
||||||
"screenPos", "fpsCap" } },
|
|
||||||
{ id = "group.speed", label = "SPEED",
|
|
||||||
members = { "textSpeed", "speedOverworld", "speedBattle", "speedMenu" } },
|
|
||||||
{ id = "group.graphics", label = "GRAPHICS",
|
|
||||||
members = { "colors", "uiLetterbox", "shaderfx", "shaderfx2" } },
|
|
||||||
-- A mod's Pipelines row splices in after TILT and is in no group, so it
|
|
||||||
-- stays on the top level rather than being swallowed into this page.
|
|
||||||
{ id = "group.extras", label = "EXTRAS",
|
|
||||||
members = { "tilt", "zoom", "voidFill" } },
|
|
||||||
}
|
|
||||||
|
|
||||||
-- The top level's order, groups and singles alike. Anything not named here
|
|
||||||
-- (a mod's row, the touch rows) keeps its flat position after these.
|
|
||||||
local ORDER = {
|
|
||||||
"group.speed", "group.video", "group.graphics", "group.audio",
|
|
||||||
"performance", "ruleset", "group.battle", "group.extras", "mods",
|
|
||||||
}
|
|
||||||
|
|
||||||
-- Each group replaced by one opener.
|
|
||||||
local function groupRows(game, rows)
|
|
||||||
local owner, picked = {}, {}
|
|
||||||
for _, group in ipairs(GROUPS) do
|
|
||||||
for _, id in ipairs(group.members) do owner[id] = group end
|
|
||||||
picked[group.id] = {}
|
|
||||||
end
|
|
||||||
for _, row in ipairs(rows) do
|
|
||||||
local group = row.id and owner[row.id]
|
|
||||||
if group then picked[group.id][#picked[group.id] + 1] = row end
|
|
||||||
end
|
|
||||||
local made = {}
|
|
||||||
for _, group in ipairs(GROUPS) do
|
|
||||||
local members = picked[group.id]
|
|
||||||
if #members > 0 then
|
|
||||||
made[group.id] = {
|
|
||||||
id = group.id, label = Strings(group.label), group = true,
|
|
||||||
value = function() return Strings("%d OPTIONS", #members) end,
|
|
||||||
-- No onCancel: BACK out of a page returns here, it does not close
|
|
||||||
-- OPTIONS, so the caller's close callback must not fire.
|
|
||||||
activate = function(g)
|
|
||||||
g.stack:push(OptionsMenu.new(g, { rows = members }))
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
local byId, view, taken = {}, {}, {}
|
|
||||||
for _, row in ipairs(rows) do
|
|
||||||
if row.id and not owner[row.id] then byId[row.id] = row end
|
|
||||||
end
|
|
||||||
for _, id in ipairs(ORDER) do
|
|
||||||
local row = made[id] or byId[id]
|
|
||||||
if row then
|
|
||||||
view[#view + 1] = row
|
|
||||||
taken[id] = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
-- Whatever ORDER does not name, in the order the flat list had it: a mod's
|
|
||||||
-- own rows, and the platform rows that come and go.
|
|
||||||
for _, row in ipairs(rows) do
|
|
||||||
local id = row.id
|
|
||||||
if not (id and (owner[id] or taken[id])) then view[#view + 1] = row end
|
|
||||||
end
|
|
||||||
return view
|
|
||||||
end
|
|
||||||
|
|
||||||
-- opts.rows makes a submenu: a fixed row list, no hook and no regrouping, so
|
|
||||||
-- a group's page is this same screen driving the rows it was handed.
|
|
||||||
function OptionsMenu.new(game, opts)
|
function OptionsMenu.new(game, opts)
|
||||||
opts = opts or {}
|
opts = opts or {}
|
||||||
if opts.rows then
|
|
||||||
return setmetatable({ game = game, rows = opts.rows, view = opts.rows,
|
|
||||||
index = 1, scroll = 0, sub = true,
|
|
||||||
onCancel = opts.onCancel }, OptionsMenu)
|
|
||||||
end
|
|
||||||
local rows = buildRows(game)
|
local rows = buildRows(game)
|
||||||
local hooked = Runtime.call("ui.options.rows", sameRows, game, rows)
|
local hooked = Runtime.call("ui.options.rows", sameRows, game, rows)
|
||||||
if type(hooked) == "table" then
|
if type(hooked) == "table" then
|
||||||
@@ -738,44 +653,14 @@ function OptionsMenu.new(game, opts)
|
|||||||
Logger.error("ui.options.rows returned %s; keeping the vanilla rows",
|
Logger.error("ui.options.rows returned %s; keeping the vanilla rows",
|
||||||
type(hooked))
|
type(hooked))
|
||||||
end
|
end
|
||||||
local self = setmetatable({ game = game, rows = rows, index = 1, scroll = 0,
|
return setmetatable({ game = game, rows = rows, index = 1, scroll = 0,
|
||||||
onCancel = opts.onCancel }, OptionsMenu)
|
onCancel = opts.onCancel }, OptionsMenu)
|
||||||
self.view = groupRows(game, rows)
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Cursor onto a row by id, opening its group page first if it lives in one.
|
|
||||||
-- Returns the screen the row ended up on, so a caller can keep driving it.
|
|
||||||
function OptionsMenu:focusRow(id)
|
|
||||||
local rows = self.view or self.rows
|
|
||||||
for i, row in ipairs(rows) do
|
|
||||||
if row.id == id then
|
|
||||||
self.index = i
|
|
||||||
self.scroll = OptionRows.clampScroll(i, self.scroll or 0, #rows, #rows + 1)
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
end
|
|
||||||
for i, row in ipairs(rows) do
|
|
||||||
if row.group and row.activate then
|
|
||||||
local group
|
|
||||||
for _, g in ipairs(GROUPS) do if g.id == row.id then group = g end end
|
|
||||||
for _, member in ipairs(group and group.members or {}) do
|
|
||||||
if member == id then
|
|
||||||
self.index = i
|
|
||||||
row.activate(self.game)
|
|
||||||
local sub = self.game.stack:top()
|
|
||||||
return sub.focusRow and sub:focusRow(id) or sub
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function OptionsMenu:update(dt)
|
function OptionsMenu:update(dt)
|
||||||
local input = self.game.input
|
local input = self.game.input
|
||||||
local rows = self.view or self.rows
|
local rows = self.rows
|
||||||
-- BACK sits below the hook-built rows so a mod cannot orphan the exit
|
-- CANCEL sits below the hook-built rows so a mod cannot orphan the exit
|
||||||
local cancelRow = #rows + 1
|
local cancelRow = #rows + 1
|
||||||
local changed = false
|
local changed = false
|
||||||
if input:wasPressed("up") then
|
if input:wasPressed("up") then
|
||||||
@@ -817,15 +702,14 @@ function OptionsMenu:update(dt)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function OptionsMenu:draw()
|
function OptionsMenu:draw()
|
||||||
-- Through Strings, like every other label on this menu. BACK is
|
-- Through Strings, like every other label on this menu. CANCEL is
|
||||||
-- appended AFTER the rows hook (see the header), which is what keeps a mod
|
-- appended AFTER the rows hook (see the header), which is what keeps a mod
|
||||||
-- from orphaning the exit -- but it also means a translation mod never sees
|
-- from orphaning the exit -- but it also means a translation mod never sees
|
||||||
-- this string, and cannot: there is no row for it to rewrite. So the one
|
-- this string, and cannot: there is no row for it to rewrite. So the one
|
||||||
-- word a Spanish player could not read on a fully translated OPTIONS menu
|
-- word a Spanish player could not read on a fully translated OPTIONS menu
|
||||||
-- was the way out of it.
|
-- was the way out of it.
|
||||||
local rows = self.view or self.rows
|
OptionRows.draw(self.game, self.rows, self.index, self.scroll or 0,
|
||||||
OptionRows.draw(self.game, rows, self.index, self.scroll or 0,
|
Strings("CANCEL"), #self.rows + 1)
|
||||||
Strings("BACK"), #rows + 1)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return OptionsMenu
|
return OptionsMenu
|
||||||
|
|||||||
@@ -1,578 +0,0 @@
|
|||||||
|
|
||||||
local Font = require("src.render.Font")
|
|
||||||
local Theme = require("src.ui.Theme")
|
|
||||||
local Sound = require("src.core.Sound")
|
|
||||||
local Strings = require("src.core.Strings")
|
|
||||||
local Palette = require("src.render.Palette")
|
|
||||||
local PaletteFX = require("src.render.PaletteFX")
|
|
||||||
|
|
||||||
local PaletteScreen = {}
|
|
||||||
PaletteScreen.__index = PaletteScreen
|
|
||||||
|
|
||||||
PaletteScreen.isOpaque = true
|
|
||||||
|
|
||||||
local COLS, ROWS = 5, 4
|
|
||||||
local TILE_W, TILE_H = 28, 20
|
|
||||||
local GAP = 4
|
|
||||||
local GRID_X, GRID_Y = 2, 14
|
|
||||||
local PER_PAGE = COLS * ROWS
|
|
||||||
|
|
||||||
local SCREEN_W, SCREEN_H = 160, 144
|
|
||||||
|
|
||||||
local BACK = 0
|
|
||||||
|
|
||||||
local function sanitize(text)
|
|
||||||
text = tostring(text or ""):upper():gsub("_", "'")
|
|
||||||
text = text:gsub("[^A-Z0-9 %.'%-]", " ")
|
|
||||||
text = text:gsub("%s+", " "):gsub("^ ", ""):gsub(" $", "")
|
|
||||||
return text
|
|
||||||
end
|
|
||||||
|
|
||||||
PaletteScreen.sanitize = sanitize -- named for the suite
|
|
||||||
|
|
||||||
local function wrap(text, width, lines)
|
|
||||||
local out = {}
|
|
||||||
local rest = text
|
|
||||||
for i = 1, lines do
|
|
||||||
if rest == "" then break end
|
|
||||||
if #rest <= width then
|
|
||||||
out[i] = rest
|
|
||||||
rest = ""
|
|
||||||
else
|
|
||||||
local cut = rest:sub(1, width + 1):match("^.*() ")
|
|
||||||
if not cut or cut < 2 then cut = width + 1 end
|
|
||||||
out[i] = rest:sub(1, cut - 1)
|
|
||||||
rest = rest:sub(cut):gsub("^ +", "")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if rest ~= "" and #out > 0 then
|
|
||||||
local last = out[#out]
|
|
||||||
out[#out] = last:sub(1, math.max(0, width - 1)) .. "."
|
|
||||||
end
|
|
||||||
return out
|
|
||||||
end
|
|
||||||
|
|
||||||
PaletteScreen.wrap = wrap -- named for the suite
|
|
||||||
|
|
||||||
local function packLabel(name, width)
|
|
||||||
local text = sanitize(name)
|
|
||||||
if #text > width then
|
|
||||||
local leaf = tostring(name):match("([^/]+)$")
|
|
||||||
if leaf then text = sanitize(leaf) end
|
|
||||||
end
|
|
||||||
return wrap(text, width, 1)[1] or ""
|
|
||||||
end
|
|
||||||
|
|
||||||
PaletteScreen.packLabel = packLabel -- named for the suite
|
|
||||||
|
|
||||||
|
|
||||||
local function fill(x, y, w, h, c)
|
|
||||||
love.graphics.setColor(c[1] / 255, c[2] / 255, c[3] / 255, 1)
|
|
||||||
love.graphics.rectangle("fill", x, y, w, h)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function frame(x, y, w, h, shade)
|
|
||||||
love.graphics.setColor(shade, shade, shade, 1)
|
|
||||||
love.graphics.rectangle("line", x + 0.5, y + 0.5, w - 1, h - 1)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function drawSwatch(x, y, w, h, swatch)
|
|
||||||
if not swatch then
|
|
||||||
frame(x, y, w, h, 0.5)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
local band = w / 4
|
|
||||||
for i = 1, 4 do
|
|
||||||
fill(x + math.floor((i - 1) * band), y,
|
|
||||||
math.ceil(band), h, swatch[i])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
PaletteScreen.drawSwatch = drawSwatch -- named for the suite
|
|
||||||
|
|
||||||
local function drawBack(selected)
|
|
||||||
if selected then
|
|
||||||
love.graphics.setColor(0, 0, 0, 1)
|
|
||||||
love.graphics.rectangle("fill", 0, 0, 10, 9)
|
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
|
||||||
else
|
|
||||||
love.graphics.setColor(0, 0, 0, 1)
|
|
||||||
end
|
|
||||||
for i = 0, 3 do
|
|
||||||
love.graphics.rectangle("fill", 2 + i, 4 - i, 1, 1 + i * 2)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
PaletteScreen.drawBack = drawBack -- named for the suite
|
|
||||||
|
|
||||||
|
|
||||||
local function engineModes()
|
|
||||||
local labels = PaletteFX.MODE_LABELS or {}
|
|
||||||
local out = {}
|
|
||||||
for i, id in ipairs(PaletteFX.MODES) do
|
|
||||||
out[i] = { PaletteFX.modeLabel(id) or labels[id] or id:upper(), id }
|
|
||||||
end
|
|
||||||
return out
|
|
||||||
end
|
|
||||||
|
|
||||||
local function dropConflictingMode(game)
|
|
||||||
local mode = PaletteFX.mode
|
|
||||||
local bakes = PaletteFX.usesGbcPack(mode) or PaletteFX.usesSpriteObp(mode)
|
|
||||||
if not bakes then return end
|
|
||||||
local o = game.save.options
|
|
||||||
o.colors = "gbc"
|
|
||||||
PaletteFX.setMode("gbc")
|
|
||||||
end
|
|
||||||
|
|
||||||
local function liveOpts(game)
|
|
||||||
dropConflictingMode(game)
|
|
||||||
return {
|
|
||||||
palette = Palette,
|
|
||||||
get = function() return game.save.options.palette or "" end,
|
|
||||||
set = function(v)
|
|
||||||
local o = game.save.options
|
|
||||||
o.palette = v
|
|
||||||
if v ~= "" then dropConflictingMode(game) end
|
|
||||||
PaletteFX.setCustomRamp(v ~= "" and Palette.ramp(v) or nil)
|
|
||||||
if game.writeOptions then pcall(game.writeOptions, game) end
|
|
||||||
end,
|
|
||||||
modes = engineModes(),
|
|
||||||
getMode = function() return game.save.options.colors or "gbc" end,
|
|
||||||
setMode = function(v)
|
|
||||||
local o = game.save.options
|
|
||||||
o.colors = v
|
|
||||||
PaletteFX.setMode(v)
|
|
||||||
if game.writeOptions then pcall(game.writeOptions, game) end
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
function PaletteScreen.new(game, opts)
|
|
||||||
opts = opts or liveOpts(game)
|
|
||||||
local self = setmetatable({
|
|
||||||
game = game,
|
|
||||||
Palette = opts.palette,
|
|
||||||
get = opts.get, set = opts.set,
|
|
||||||
modes = opts.modes or {},
|
|
||||||
getMode = opts.getMode, setMode = opts.setMode,
|
|
||||||
onChange = opts.onChange,
|
|
||||||
view = "root",
|
|
||||||
index = 1,
|
|
||||||
scroll = 0,
|
|
||||||
group = nil,
|
|
||||||
stackTrail = {},
|
|
||||||
}, PaletteScreen)
|
|
||||||
self.rows = self:buildRoot()
|
|
||||||
self:enterFromSelection()
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
function PaletteScreen:buildRoot()
|
|
||||||
local rows = {}
|
|
||||||
for _, category in ipairs(self.Palette and self.Palette.categories() or {}) do
|
|
||||||
if #(category.palettes or {}) > 0 then
|
|
||||||
rows[#rows + 1] = { label = category.name, folder = true,
|
|
||||||
view = "tiles", group = category }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if self.Palette and self.Palette.MONO_COLOURS then
|
|
||||||
rows[#rows + 1] = { label = "MONOCHROME", folder = true, view = "mono" }
|
|
||||||
end
|
|
||||||
for _, mode in ipairs(self.modes) do
|
|
||||||
rows[#rows + 1] = { label = mode[1], mode = mode[2] }
|
|
||||||
end
|
|
||||||
return rows
|
|
||||||
end
|
|
||||||
|
|
||||||
function PaletteScreen:rootIndexFor(view, group)
|
|
||||||
for i, row in ipairs(self.rows or {}) do
|
|
||||||
if row.view == view then
|
|
||||||
if view ~= "tiles" then return i end
|
|
||||||
if row.group and group and row.group.key == group.key then return i end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return 1
|
|
||||||
end
|
|
||||||
|
|
||||||
function PaletteScreen:enterFromSelection()
|
|
||||||
local Palette = self.Palette
|
|
||||||
if not Palette then return end
|
|
||||||
local id = self.get and self.get() or ""
|
|
||||||
if type(id) ~= "string" or id == "" then return end
|
|
||||||
|
|
||||||
if id:match("^m:") then
|
|
||||||
for i, c in ipairs(Palette.MONO_COLOURS) do
|
|
||||||
if Palette.monoId(c[2], c[3], c[4]) == id then
|
|
||||||
self:openAt("mono", nil, i)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local key, at = Palette.locate(id)
|
|
||||||
if key then self:openAt("tiles", Palette.category(key), at) end
|
|
||||||
end
|
|
||||||
|
|
||||||
function PaletteScreen:openAt(view, group, index)
|
|
||||||
self.stackTrail = { { view = "root", index = self:rootIndexFor(view, group),
|
|
||||||
scroll = 0, group = nil } }
|
|
||||||
self.view, self.group, self.index, self.scroll = view, group, index or 1, 0
|
|
||||||
local items = self:items()
|
|
||||||
self:clampScroll(#items, #items + 1, PER_PAGE)
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
function PaletteScreen:items()
|
|
||||||
local Palette = self.Palette
|
|
||||||
if self.view == "root" then return self.rows or {} end
|
|
||||||
if self.view == "tiles" then
|
|
||||||
return self.group and self.group.palettes or {}
|
|
||||||
end
|
|
||||||
if self.view == "mono" then
|
|
||||||
return Palette and Palette.MONO_COLOURS or {}
|
|
||||||
end
|
|
||||||
return {}
|
|
||||||
end
|
|
||||||
|
|
||||||
function PaletteScreen:isGrid()
|
|
||||||
return self.view == "tiles" or self.view == "mono"
|
|
||||||
end
|
|
||||||
|
|
||||||
function PaletteScreen:idAt(i)
|
|
||||||
local Palette = self.Palette
|
|
||||||
if not Palette then return nil end
|
|
||||||
if self.view == "tiles" then
|
|
||||||
local entry = self.group and (self.group.palettes or {})[i]
|
|
||||||
return entry and entry.id or nil
|
|
||||||
elseif self.view == "mono" then
|
|
||||||
local c = Palette.MONO_COLOURS[i]
|
|
||||||
return c and Palette.monoId(c[2], c[3], c[4]) or nil
|
|
||||||
end
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
function PaletteScreen:title()
|
|
||||||
if self.view == "root" then return Strings("COLOUR") end
|
|
||||||
if self.view == "mono" then return Strings("MONOCHROME") end
|
|
||||||
return sanitize(self.group and self.group.name or "")
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
function PaletteScreen:update()
|
|
||||||
local input = self.game.input
|
|
||||||
local items = self:items()
|
|
||||||
local total = #items
|
|
||||||
local cancel = total + 1
|
|
||||||
|
|
||||||
if input:wasPressed("b") then
|
|
||||||
self:back()
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if self:isGrid() then
|
|
||||||
self:moveGrid(input, total, cancel)
|
|
||||||
else
|
|
||||||
self:moveList(input, total, cancel)
|
|
||||||
end
|
|
||||||
|
|
||||||
if input:wasPressed("a") then
|
|
||||||
self:activate(cancel)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function PaletteScreen:moveList(input, total, cancel)
|
|
||||||
if self.index == BACK then
|
|
||||||
if input:wasPressed("down") or input:wasPressed("right") then
|
|
||||||
self.index = 1
|
|
||||||
end
|
|
||||||
return
|
|
||||||
end
|
|
||||||
if input:wasPressed("left") then
|
|
||||||
self.index = BACK
|
|
||||||
return
|
|
||||||
end
|
|
||||||
if input:wasPressed("up") then
|
|
||||||
self.index = self.index > 1 and self.index - 1 or BACK
|
|
||||||
elseif input:wasPressed("down") then
|
|
||||||
self.index = self.index < cancel and self.index + 1 or 1
|
|
||||||
end
|
|
||||||
self:clampScroll(total, cancel, 12)
|
|
||||||
end
|
|
||||||
|
|
||||||
function PaletteScreen:moveGrid(input, total, cancel)
|
|
||||||
local i = self.index
|
|
||||||
|
|
||||||
if i == BACK then
|
|
||||||
if input:wasPressed("down") or input:wasPressed("right") then
|
|
||||||
self.index = self.scroll + 1
|
|
||||||
if self.index > total then self.index = 1 end
|
|
||||||
end
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if i > total then
|
|
||||||
if input:wasPressed("up") then
|
|
||||||
self.index = total
|
|
||||||
elseif input:wasPressed("down") then
|
|
||||||
self.index = 1
|
|
||||||
end
|
|
||||||
self:clampScroll(total, cancel, PER_PAGE)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if input:wasPressed("left") then
|
|
||||||
self.index = ((i - 1) % COLS == 0) and BACK or i - 1
|
|
||||||
elseif input:wasPressed("right") then
|
|
||||||
self.index = i < total and i + 1 or 1
|
|
||||||
elseif input:wasPressed("up") then
|
|
||||||
self.index = (i - COLS >= 1) and i - COLS or BACK
|
|
||||||
elseif input:wasPressed("down") then
|
|
||||||
if i + COLS <= total then
|
|
||||||
self.index = i + COLS
|
|
||||||
else
|
|
||||||
self.index = cancel
|
|
||||||
end
|
|
||||||
end
|
|
||||||
self:clampScroll(total, cancel, PER_PAGE)
|
|
||||||
end
|
|
||||||
|
|
||||||
function PaletteScreen:clampScroll(total, cancel, visible)
|
|
||||||
if self.index == BACK then return end
|
|
||||||
if self.index >= cancel then
|
|
||||||
self.scroll = math.max(0, total - visible)
|
|
||||||
if self:isGrid() then
|
|
||||||
self.scroll = math.floor(self.scroll / COLS) * COLS
|
|
||||||
end
|
|
||||||
return
|
|
||||||
end
|
|
||||||
local step = self:isGrid() and COLS or 1
|
|
||||||
if self.index <= self.scroll then
|
|
||||||
self.scroll = math.floor((self.index - 1) / step) * step
|
|
||||||
elseif self.index > self.scroll + visible then
|
|
||||||
self.scroll = math.floor((self.index - 1) / step) * step - visible + step
|
|
||||||
end
|
|
||||||
if self.scroll < 0 then self.scroll = 0 end
|
|
||||||
end
|
|
||||||
|
|
||||||
function PaletteScreen:activate(cancel)
|
|
||||||
if self.index == BACK or self.index >= cancel then
|
|
||||||
self:back()
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if self.view == "root" then
|
|
||||||
local row = (self.rows or {})[self.index]
|
|
||||||
if not row then return end
|
|
||||||
if row.folder then
|
|
||||||
self:push(row.view, row.group)
|
|
||||||
elseif row.mode then
|
|
||||||
if self.set then self.set("") end
|
|
||||||
if self.setMode then self.setMode(row.mode) end
|
|
||||||
self:changed()
|
|
||||||
end
|
|
||||||
|
|
||||||
elseif self:isGrid() then
|
|
||||||
local id = self:idAt(self.index)
|
|
||||||
if id then
|
|
||||||
if self.set then self.set(id) end
|
|
||||||
self:changed()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function PaletteScreen:changed()
|
|
||||||
if self.game.data and Sound and Sound.play then
|
|
||||||
pcall(Sound.play, self.game.data, "Press_AB")
|
|
||||||
end
|
|
||||||
if self.onChange then self.onChange() end
|
|
||||||
end
|
|
||||||
|
|
||||||
function PaletteScreen:push(view, group)
|
|
||||||
self.stackTrail[#self.stackTrail + 1] =
|
|
||||||
{ view = self.view, index = self.index, scroll = self.scroll,
|
|
||||||
group = self.group }
|
|
||||||
self.view, self.index, self.scroll = view, 1, 0
|
|
||||||
self.group = group
|
|
||||||
self:cursorToSelection()
|
|
||||||
end
|
|
||||||
|
|
||||||
function PaletteScreen:cursorToSelection()
|
|
||||||
local id = self.get and self.get() or ""
|
|
||||||
if type(id) ~= "string" or id == "" then return end
|
|
||||||
local items = self:items()
|
|
||||||
for i = 1, #items do
|
|
||||||
if self:idAt(i) == id then
|
|
||||||
self.index = i
|
|
||||||
self:clampScroll(#items, #items + 1,
|
|
||||||
self:isGrid() and PER_PAGE or 12)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function PaletteScreen:back()
|
|
||||||
local prev = table.remove(self.stackTrail)
|
|
||||||
if prev then
|
|
||||||
self.view, self.index = prev.view, prev.index
|
|
||||||
self.scroll, self.group = prev.scroll, prev.group
|
|
||||||
return
|
|
||||||
end
|
|
||||||
self:close()
|
|
||||||
end
|
|
||||||
|
|
||||||
function PaletteScreen:close()
|
|
||||||
if self.game.data and Sound and Sound.play then
|
|
||||||
pcall(Sound.play, self.game.data, "Press_AB")
|
|
||||||
end
|
|
||||||
self.game.stack:pop()
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
function PaletteScreen:sgbPalettes()
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
function PaletteScreen:draw()
|
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
|
||||||
love.graphics.rectangle("fill", 0, 0, SCREEN_W, SCREEN_H)
|
|
||||||
|
|
||||||
drawBack(self.index == BACK)
|
|
||||||
|
|
||||||
love.graphics.setColor(0, 0, 0, 1)
|
|
||||||
Font.draw(self:title(), 12, 0)
|
|
||||||
|
|
||||||
if self:isGrid() then
|
|
||||||
self:drawGrid()
|
|
||||||
else
|
|
||||||
self:drawList()
|
|
||||||
end
|
|
||||||
|
|
||||||
love.graphics.setColor(0, 0, 0, 1)
|
|
||||||
local items = self:items()
|
|
||||||
if self.index >= #items + 1 then
|
|
||||||
Font.draw(Strings("CANCEL"), 16, SCREEN_H - 8)
|
|
||||||
Font.drawCode(Theme.cursor, 8, SCREEN_H - 8)
|
|
||||||
end
|
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
|
||||||
end
|
|
||||||
|
|
||||||
function PaletteScreen:drawList()
|
|
||||||
local items = self:items()
|
|
||||||
local mode = self.getMode and self.getMode() or nil
|
|
||||||
local visible = 12
|
|
||||||
for slot = 1, visible do
|
|
||||||
local i = self.scroll + slot
|
|
||||||
local row = items[i]
|
|
||||||
if not row then break end
|
|
||||||
local y = 16 + (slot - 1) * 8
|
|
||||||
|
|
||||||
love.graphics.setColor(0, 0, 0, 1)
|
|
||||||
Font.draw(Strings(row.label or ""), 16, y)
|
|
||||||
if i == self.index then
|
|
||||||
Font.drawCode(Theme.cursor, 8, y)
|
|
||||||
end
|
|
||||||
if row.folder then
|
|
||||||
Font.drawCode(Theme.cursor, 8 + 8 * 17, y)
|
|
||||||
elseif row.mode and mode and row.mode == mode then
|
|
||||||
love.graphics.rectangle("fill", 8 + 8 * 17 + 2, y + 2, 4, 4)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function PaletteScreen:drawGrid()
|
|
||||||
local Palette = self.Palette
|
|
||||||
local items = self:items()
|
|
||||||
local total = #items
|
|
||||||
|
|
||||||
for slot = 1, PER_PAGE do
|
|
||||||
local i = self.scroll + slot
|
|
||||||
if i > total then break end
|
|
||||||
local col = (slot - 1) % COLS
|
|
||||||
local row = math.floor((slot - 1) / COLS)
|
|
||||||
local x = GRID_X + col * (TILE_W + GAP)
|
|
||||||
local y = GRID_Y + row * (TILE_H + GAP)
|
|
||||||
|
|
||||||
local swatch
|
|
||||||
if self.view == "mono" then
|
|
||||||
local c = Palette and Palette.MONO_COLOURS[i]
|
|
||||||
if c then
|
|
||||||
local ramp = Palette.monoRamp(c[2], c[3], c[4])
|
|
||||||
swatch = { ramp[4], ramp[3], ramp[2], ramp[1] }
|
|
||||||
end
|
|
||||||
else
|
|
||||||
swatch = Palette and Palette.swatch(self:idAt(i)) or nil
|
|
||||||
end
|
|
||||||
drawSwatch(x, y, TILE_W, TILE_H, swatch)
|
|
||||||
|
|
||||||
if i == self.index then
|
|
||||||
frame(x - 2, y - 2, TILE_W + 4, TILE_H + 4, 0)
|
|
||||||
frame(x - 1, y - 1, TILE_W + 2, TILE_H + 2, 1)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
self:drawFooter(total)
|
|
||||||
end
|
|
||||||
|
|
||||||
function PaletteScreen:drawFooter(total)
|
|
||||||
local Palette = self.Palette
|
|
||||||
love.graphics.setColor(0, 0, 0, 1)
|
|
||||||
|
|
||||||
local name, pack = "", nil
|
|
||||||
if self.index >= 1 and self.index <= total then
|
|
||||||
if self.view == "mono" then
|
|
||||||
local c = Palette and Palette.MONO_COLOURS[self.index]
|
|
||||||
name = c and c[1] or ""
|
|
||||||
else
|
|
||||||
local entry = self.group and (self.group.palettes or {})[self.index]
|
|
||||||
if entry then
|
|
||||||
name = sanitize(entry.name)
|
|
||||||
pack = entry.pack
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local lines = wrap(name, 20, 2)
|
|
||||||
for i = 1, #lines do
|
|
||||||
Font.draw(lines[i], 0, 110 + (i - 1) * 8)
|
|
||||||
end
|
|
||||||
|
|
||||||
local at = 0
|
|
||||||
if total > PER_PAGE then
|
|
||||||
local page = math.floor(self.scroll / PER_PAGE) + 1
|
|
||||||
local pages = math.ceil(total / PER_PAGE)
|
|
||||||
Font.draw(("%d/%d"):format(page, pages), 0, 128)
|
|
||||||
at = 40
|
|
||||||
end
|
|
||||||
if pack then
|
|
||||||
Font.draw(packLabel(pack, (SCREEN_W - at) / 8), at, 128)
|
|
||||||
end
|
|
||||||
|
|
||||||
local stored = self.get and self.get() or ""
|
|
||||||
if type(stored) == "string" and stored ~= "" then
|
|
||||||
for slot = 1, PER_PAGE do
|
|
||||||
local i = self.scroll + slot
|
|
||||||
if i > total then break end
|
|
||||||
if self:idAt(i) == stored then
|
|
||||||
local col = (slot - 1) % COLS
|
|
||||||
local row = math.floor((slot - 1) / COLS)
|
|
||||||
love.graphics.setColor(0, 0, 0, 1)
|
|
||||||
love.graphics.rectangle(
|
|
||||||
"fill",
|
|
||||||
GRID_X + col * (TILE_W + GAP) + TILE_W - 5,
|
|
||||||
GRID_Y + row * (TILE_H + GAP) + TILE_H - 5, 4, 4)
|
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
|
||||||
love.graphics.rectangle(
|
|
||||||
"fill",
|
|
||||||
GRID_X + col * (TILE_W + GAP) + TILE_W - 4,
|
|
||||||
GRID_Y + row * (TILE_H + GAP) + TILE_H - 4, 2, 2)
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return PaletteScreen
|
|
||||||
+5
-26
@@ -11,7 +11,6 @@
|
|||||||
|
|
||||||
local Assets = require("src.render.Assets")
|
local Assets = require("src.render.Assets")
|
||||||
local Font = require("src.render.Font")
|
local Font = require("src.render.Font")
|
||||||
local LevelDisplay = require("src.ui.LevelDisplay")
|
|
||||||
local Logger = require("src.core.Logger")
|
local Logger = require("src.core.Logger")
|
||||||
local Runtime = require("src.mods.Runtime")
|
local Runtime = require("src.mods.Runtime")
|
||||||
local Screens = require("src.ui.Screens")
|
local Screens = require("src.ui.Screens")
|
||||||
@@ -72,12 +71,6 @@ function PartyMenu:sgbPalettes(game)
|
|||||||
return zones
|
return zones
|
||||||
end
|
end
|
||||||
|
|
||||||
-- data/moves/field_moves.asm: leftmost tile per field move name
|
|
||||||
local FIELD_MOVE_X = {
|
|
||||||
cut = 12, fly = 12, surf = 12, flash = 12, DIG = 12,
|
|
||||||
strength = 10, TELEPORT = 10, softboiled = 8,
|
|
||||||
}
|
|
||||||
|
|
||||||
local function sameItems(_, items) return items end
|
local function sameItems(_, items) return items end
|
||||||
|
|
||||||
local function followerUnavailable(game, mon)
|
local function followerUnavailable(game, mon)
|
||||||
@@ -713,9 +706,6 @@ function PartyMenu:update(dt)
|
|||||||
-- (text_box.asm .donePrintingNames). #768
|
-- (text_box.asm .donePrintingNames). #768
|
||||||
items[#items + 1] = { label = Strings("STATS"), action = "stats" }
|
items[#items + 1] = { label = Strings("STATS"), action = "stats" }
|
||||||
items[#items + 1] = { label = Strings("SWITCH"), action = "switch" }
|
items[#items + 1] = { label = Strings("SWITCH"), action = "switch" }
|
||||||
-- CANCEL closes the whole party menu (start_sub_menus.asm:71-75
|
|
||||||
-- .exitMenu), where B returns to the party list. #1833
|
|
||||||
items[#items + 1] = { label = Strings("CANCEL"), action = "cancel" }
|
|
||||||
end
|
end
|
||||||
local ctx = { battle = self.battle, overworld = ow }
|
local ctx = { battle = self.battle, overworld = ow }
|
||||||
local hooked = Runtime.call("ui.party.submenu", sameItems,
|
local hooked = Runtime.call("ui.party.submenu", sameItems,
|
||||||
@@ -795,11 +785,7 @@ function PartyMenu:draw()
|
|||||||
-- level at column 13 (<LV> tile + digits, PrintLevel) AND the
|
-- level at column 13 (<LV> tile + digits, PrintLevel) AND the
|
||||||
-- status/FNT text at column 17 (PrintStatusCondition), like the
|
-- status/FNT text at column 17 (PrintStatusCondition), like the
|
||||||
-- original rows -- statused mons keep their level display
|
-- original rows -- statused mons keep their level display
|
||||||
if not LevelDisplay.visible(mon, "party", self.game) then -- RFC 0019
|
if mon.level < 100 then
|
||||||
-- the level column is simply empty; the status/FNT column at 17 is a
|
|
||||||
-- separate field and still prints, exactly as it does for a mon whose
|
|
||||||
-- level is on screen
|
|
||||||
elseif mon.level < 100 then
|
|
||||||
HudTiles.tile(0x6E, 104, y) -- <LV>
|
HudTiles.tile(0x6E, 104, y) -- <LV>
|
||||||
Font.draw(tostring(mon.level), 112, y)
|
Font.draw(tostring(mon.level), 112, y)
|
||||||
else
|
else
|
||||||
@@ -887,19 +873,12 @@ function PartyMenu:draw()
|
|||||||
end
|
end
|
||||||
if self.submenu then
|
if self.submenu then
|
||||||
local n = #self.subItems
|
local n = #self.subItems
|
||||||
-- engine/menus/text_box.asm:397-440, data/text_boxes.asm:33 #1819
|
Font.drawBox(9, 17 - n * 2 - 1, 11, n * 2 + 1)
|
||||||
local lx = 12
|
local y0 = (17 - n * 2) * 8
|
||||||
for _, entry in ipairs(self.subItems) do
|
|
||||||
local mx = FIELD_MOVE_X[entry.move or entry.action]
|
|
||||||
if mx and mx < lx then lx = mx end
|
|
||||||
end
|
|
||||||
local top = n > 3 and math.max(0, 16 - n * 2) or 11
|
|
||||||
Font.drawBox(lx - 1, top, 21 - lx, 18 - top)
|
|
||||||
local y0 = (18 - n * 2) * 8
|
|
||||||
for si, entry in ipairs(self.subItems) do
|
for si, entry in ipairs(self.subItems) do
|
||||||
Font.draw(entry.label, (lx + 1) * 8, y0 + (si - 1) * 16)
|
Font.draw(entry.label, 88, y0 + (si - 1) * 16)
|
||||||
end
|
end
|
||||||
Font.drawCode(Theme.cursor, lx * 8, y0 + (self.subIndex - 1) * 16)
|
Font.drawCode(Theme.cursor, 80, y0 + (self.subIndex - 1) * 16)
|
||||||
end
|
end
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
end
|
end
|
||||||
|
|||||||
+3
-46
@@ -11,8 +11,6 @@ local ListMenu = require("src.ui.ListMenu")
|
|||||||
local Menu = require("src.ui.Menu")
|
local Menu = require("src.ui.Menu")
|
||||||
local Sound = require("src.core.Sound")
|
local Sound = require("src.core.Sound")
|
||||||
local Strings = require("src.core.Strings")
|
local Strings = require("src.core.Strings")
|
||||||
local TextBox = require("src.render.TextBox")
|
|
||||||
local romText = require("src.core.RomText")
|
|
||||||
|
|
||||||
local PlayerPC = {}
|
local PlayerPC = {}
|
||||||
|
|
||||||
@@ -38,20 +36,9 @@ local function buildItems(game, store, order)
|
|||||||
})
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
-- the $ff terminator's row (home/list_menu.asm:371-372, 523-528)
|
|
||||||
items[#items + 1] = { cancel = true, label = Strings("CANCEL") }
|
|
||||||
return items
|
return items
|
||||||
end
|
end
|
||||||
|
|
||||||
-- A on CANCEL leaves the list exactly like B (home/list_menu.asm:105-110)
|
|
||||||
local function leftOnCancel(item, list)
|
|
||||||
if item and item.cancel then
|
|
||||||
list:close()
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Ask "How many?" (DepositHowManyText/WithdrawHowManyText →
|
-- Ask "How many?" (DepositHowManyText/WithdrawHowManyText →
|
||||||
-- DisplayChooseQuantityMenu) capped at the stack count. Key items and
|
-- DisplayChooseQuantityMenu) capped at the stack count. Key items and
|
||||||
-- HMs always move one, with no prompt (IsKeyItem in players_pc.asm).
|
-- HMs always move one, with no prompt (IsKeyItem in players_pc.asm).
|
||||||
@@ -89,21 +76,11 @@ end
|
|||||||
|
|
||||||
local function withdraw(game)
|
local function withdraw(game)
|
||||||
local pc = game.save.pcItems
|
local pc = game.save.pcItems
|
||||||
-- players_pc.asm:144-149: an empty list is never opened
|
game.stack:push(ListMenu.new(game, "WITHDRAW ITEM", buildItems(game, pc), {
|
||||||
if next(pc) == nil then
|
|
||||||
game.stack:push(TextBox.new(game, romText(game.data, "_NothingStoredText",
|
|
||||||
"There is nothing\nstored."), nil, { noSound = true }))
|
|
||||||
return
|
|
||||||
end
|
|
||||||
game.stack:push(ListMenu.new(game, nil, buildItems(game, pc), {
|
|
||||||
kind = "pc_item_withdraw",
|
kind = "pc_item_withdraw",
|
||||||
messageBox = true,
|
messageBox = true,
|
||||||
-- players_pc.asm:151-152 WhatToWithdrawText, printed before the list
|
|
||||||
footer = romText(game.data, "_WhatToWithdrawText",
|
|
||||||
"What do you want\nto withdraw?"),
|
|
||||||
noSound = true, -- PlayerPCMenu holds BIT_NO_MENU_BUTTON_SOUND (#570)
|
noSound = true, -- PlayerPCMenu holds BIT_NO_MENU_BUTTON_SOUND (#570)
|
||||||
onChoose = function(item, list)
|
onChoose = function(item, list)
|
||||||
if leftOnCancel(item, list) then return end
|
|
||||||
askQuantity(game, list, pc[item.value] or 1, item.value, function(qty)
|
askQuantity(game, list, pc[item.value] or 1, item.value, function(qty)
|
||||||
local Bag = require("src.inventory.Bag")
|
local Bag = require("src.inventory.Bag")
|
||||||
if not Bag.add(game.save, item.value, qty, game.data) then
|
if not Bag.add(game.save, item.value, qty, game.data) then
|
||||||
@@ -135,21 +112,11 @@ local function deposit(game)
|
|||||||
local Bag = require("src.inventory.Bag")
|
local Bag = require("src.inventory.Bag")
|
||||||
-- engine/menus/players_pc.asm:99 wListPointer = wNumBagItems, so deposit order == bag order
|
-- engine/menus/players_pc.asm:99 wListPointer = wNumBagItems, so deposit order == bag order
|
||||||
local order = Bag.order(game.save, game.data)
|
local order = Bag.order(game.save, game.data)
|
||||||
-- players_pc.asm:90-95: an empty bag never reaches the list
|
game.stack:push(ListMenu.new(game, "DEPOSIT ITEM", buildItems(game, inv, order), {
|
||||||
if #order == 0 then
|
|
||||||
game.stack:push(TextBox.new(game, romText(game.data, "_NothingToDepositText",
|
|
||||||
"You have nothing\nto deposit."), nil, { noSound = true }))
|
|
||||||
return
|
|
||||||
end
|
|
||||||
game.stack:push(ListMenu.new(game, nil, buildItems(game, inv, order), {
|
|
||||||
kind = "pc_item_deposit",
|
kind = "pc_item_deposit",
|
||||||
messageBox = true,
|
messageBox = true,
|
||||||
-- players_pc.asm:97-98 WhatToDepositText, printed before the list
|
|
||||||
footer = romText(game.data, "_WhatToDepositText",
|
|
||||||
"What do you want\nto deposit?"),
|
|
||||||
noSound = true, -- PlayerPCMenu holds BIT_NO_MENU_BUTTON_SOUND (#570)
|
noSound = true, -- PlayerPCMenu holds BIT_NO_MENU_BUTTON_SOUND (#570)
|
||||||
onChoose = function(item, list)
|
onChoose = function(item, list)
|
||||||
if leftOnCancel(item, list) then return end
|
|
||||||
askQuantity(game, list, inv[item.value] or 1, item.value, function(qty)
|
askQuantity(game, list, inv[item.value] or 1, item.value, function(qty)
|
||||||
if pcFull(game, pc, item.value) then
|
if pcFull(game, pc, item.value) then
|
||||||
list.footer = Strings("No room left to\nstore items.")
|
list.footer = Strings("No room left to\nstore items.")
|
||||||
@@ -167,21 +134,11 @@ end
|
|||||||
|
|
||||||
local function toss(game)
|
local function toss(game)
|
||||||
local pc = game.save.pcItems
|
local pc = game.save.pcItems
|
||||||
-- players_pc.asm:196-201: an empty list is never opened
|
game.stack:push(ListMenu.new(game, "TOSS ITEM", buildItems(game, pc), {
|
||||||
if next(pc) == nil then
|
|
||||||
game.stack:push(TextBox.new(game, romText(game.data, "_NothingStoredText",
|
|
||||||
"There is nothing\nstored."), nil, { noSound = true }))
|
|
||||||
return
|
|
||||||
end
|
|
||||||
game.stack:push(ListMenu.new(game, nil, buildItems(game, pc), {
|
|
||||||
kind = "pc_item_toss",
|
kind = "pc_item_toss",
|
||||||
messageBox = true,
|
messageBox = true,
|
||||||
-- players_pc.asm:205-206 WhatToTossText, printed before the list
|
|
||||||
footer = romText(game.data, "_WhatToTossText",
|
|
||||||
"What do you want\nto toss away?"),
|
|
||||||
noSound = true, -- PlayerPCMenu holds BIT_NO_MENU_BUTTON_SOUND (#570)
|
noSound = true, -- PlayerPCMenu holds BIT_NO_MENU_BUTTON_SOUND (#570)
|
||||||
onChoose = function(item, list)
|
onChoose = function(item, list)
|
||||||
if leftOnCancel(item, list) then return end
|
|
||||||
local def = game.data.items[item.value]
|
local def = game.data.items[item.value]
|
||||||
if (def and def.keyItem) or item.value:find("^HM_") then
|
if (def and def.keyItem) or item.value:find("^HM_") then
|
||||||
list.footer = Strings("That's too impor-\ntant to toss!")
|
list.footer = Strings("That's too impor-\ntant to toss!")
|
||||||
|
|||||||
+45
-236
@@ -1,38 +1,9 @@
|
|||||||
-- Pokédex CONTENTS screen: engine/menus/pokedex.asm:157-348
|
-- Minimal Pokédex: dex-ordered list with seen/owned markers.
|
||||||
-- (Yellow: pokeyellow engine/menus/pokedex.asm:263-336).
|
|
||||||
|
|
||||||
local Font = require("src.render.Font")
|
local ListMenu = require("src.ui.ListMenu")
|
||||||
local Strings = require("src.core.Strings")
|
local Strings = require("src.core.Strings")
|
||||||
local Theme = require("src.ui.Theme")
|
|
||||||
|
|
||||||
local PokedexMenu = {}
|
local PokedexMenu = {}
|
||||||
PokedexMenu.__index = PokedexMenu
|
|
||||||
PokedexMenu.isOpaque = true
|
|
||||||
|
|
||||||
-- engine/menus/pokedex.asm:226 (`ld d, 7`), :21 (top menu item Y 3, X 0)
|
|
||||||
local ROWS = 7
|
|
||||||
local FIRST_ROW_Y = 24
|
|
||||||
local NUM_X, BALL_X, NAME_X, CURSOR_X = 8, 24, 32, 0
|
|
||||||
|
|
||||||
-- engine/menus/pokedex.asm:163-199
|
|
||||||
-- (Yellow: pokeyellow engine/menus/pokedex.asm:266-306)
|
|
||||||
local RED_ROWS = { seen = 2, own = 5, rule = 8, items = 10 }
|
|
||||||
local YELLOW_ROWS = { seen = 1, own = 4, rule = 6, items = 8 }
|
|
||||||
|
|
||||||
-- engine/menus/pokedex.asm:350 (DrawPokedexVerticalLine): column 14, $71
|
|
||||||
-- toggling to $70 down each nine-tile run, both runs restarting on $71
|
|
||||||
local function dividerCodes()
|
|
||||||
local codes = { [0] = 0x71 }
|
|
||||||
for _, top in ipairs({ 1, 9 }) do
|
|
||||||
local code = 0x71
|
|
||||||
for i = 0, 8 do
|
|
||||||
codes[top + i] = code
|
|
||||||
code = code == 0x71 and 0x70 or 0x71
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return codes
|
|
||||||
end
|
|
||||||
local DIVIDER = dividerCodes()
|
|
||||||
|
|
||||||
-- SGB: PalPacket_Pokedex, whole screen
|
-- SGB: PalPacket_Pokedex, whole screen
|
||||||
function PokedexMenu:sgbPalettes(game)
|
function PokedexMenu:sgbPalettes(game)
|
||||||
@@ -41,168 +12,58 @@ end
|
|||||||
|
|
||||||
function PokedexMenu.new(game, opts)
|
function PokedexMenu.new(game, opts)
|
||||||
opts = opts or {}
|
opts = opts or {}
|
||||||
local self = setmetatable({}, PokedexMenu)
|
|
||||||
self.game = game
|
|
||||||
self.onCancel = opts.onCancel -- B returns to the start menu when opened from it
|
|
||||||
self.index = 1
|
|
||||||
self.scroll = 0
|
|
||||||
self.rowsAt = require("src.core.GameVersion").isYellow()
|
|
||||||
and YELLOW_ROWS or RED_ROWS
|
|
||||||
local dex = game.save.pokedex or { seen = {}, owned = {} }
|
local dex = game.save.pokedex or { seen = {}, owned = {} }
|
||||||
local byDex = {}
|
local byDex = {}
|
||||||
for species, def in pairs(game.data.pokemon) do
|
for species, def in pairs(game.data.pokemon) do
|
||||||
if def.dex then byDex[def.dex] = def end
|
if def.dex then byDex[def.dex] = def end
|
||||||
end
|
end
|
||||||
|
local items = {}
|
||||||
|
local seen, owned = 0, 0
|
||||||
-- dex bound and number width come from constants; the fallbacks keep a
|
-- dex bound and number width come from constants; the fallbacks keep a
|
||||||
-- cache imported before those keys existed on the Kanto numbering
|
-- cache imported before those keys existed on the Kanto numbering
|
||||||
local constants = game.data.constants or {}
|
local constants = game.data.constants or {}
|
||||||
local numFmt = ("%%0%dd"):format(constants.dexDigits or 3)
|
local numFmt = ("%%0%dd"):format(constants.dexDigits or 3)
|
||||||
local dexSize = constants.dexSize or 151
|
for n = 1, constants.dexSize or 151 do
|
||||||
-- engine/menus/pokedex.asm:200-216: the list stops at wDexMaxSeenMon, the
|
|
||||||
-- highest number the player has seen, not at the end of the roster
|
|
||||||
local maxSeen = 0
|
|
||||||
local seen, owned = 0, 0
|
|
||||||
for n = 1, dexSize do
|
|
||||||
local def = byDex[n]
|
local def = byDex[n]
|
||||||
if def then
|
if def then
|
||||||
|
local label
|
||||||
if dex.owned[def.id] then
|
if dex.owned[def.id] then
|
||||||
|
label = (numFmt .. " %s"):format(n, def.name)
|
||||||
owned = owned + 1
|
owned = owned + 1
|
||||||
seen = seen + 1
|
seen = seen + 1
|
||||||
maxSeen = n
|
|
||||||
elseif dex.seen[def.id] then
|
elseif dex.seen[def.id] then
|
||||||
|
label = (numFmt .. " %s"):format(n, def.name)
|
||||||
seen = seen + 1
|
seen = seen + 1
|
||||||
maxSeen = n
|
else
|
||||||
|
label = (numFmt .. " -----"):format(n)
|
||||||
end
|
end
|
||||||
end
|
table.insert(items, {
|
||||||
end
|
label = label,
|
||||||
local items = {}
|
|
||||||
for n = 1, math.min(dexSize, maxSeen) do
|
|
||||||
local def = byDex[n]
|
|
||||||
if def then
|
|
||||||
local known = dex.owned[def.id] or dex.seen[def.id]
|
|
||||||
-- engine/menus/pokedex.asm:265 (.dashedLine) for anything unseen
|
|
||||||
local name = known and def.name or "----------"
|
|
||||||
items[#items + 1] = {
|
|
||||||
num = numFmt:format(n),
|
|
||||||
name = name,
|
|
||||||
label = (numFmt .. " %s"):format(n, name),
|
|
||||||
-- owned entries carry the pokéball marker like the original
|
-- owned entries carry the pokéball marker like the original
|
||||||
-- list; seen-only entries are just the name
|
-- list; seen-only entries are just the name
|
||||||
ball = dex.owned[def.id] or nil,
|
ball = dex.owned[def.id] or nil,
|
||||||
value = known and def.id or nil,
|
value = (dex.owned[def.id] or dex.seen[def.id]) and def.id or nil,
|
||||||
}
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
self.items = items
|
local list = ListMenu.new(game, "POKéDEX", items, {
|
||||||
-- SEEN / OWN, `lb bc, 1, 3` at hlcoord 16,3 and 16,6: engine/menus/
|
-- SEEN / OWN in the original's fixed three-digit field: engine/menus/
|
||||||
-- pokedex.asm HandlePokedexListMenu (#639)
|
-- pokedex.asm HandlePokedexListMenu prints both counts with
|
||||||
self.seenCount, self.ownedCount = seen, owned
|
-- `lb bc, 1, 3` (hlcoord 16,3 and 16,6), labelled by PokedexSeenText
|
||||||
self.footer = Strings("SEEN %3d OWN %3d", seen, owned)
|
-- and PokedexOwnText ("OWN", not "OWNED"). The width is load bearing
|
||||||
return self
|
-- here: a bare ListMenu footer goes through the 18-column text wrap,
|
||||||
end
|
-- so the old 19-glyph "SEEN 100 OWNED 100" split in two and its first
|
||||||
|
-- half landed on the list's last row at y=120 (#639).
|
||||||
-- HandleMenuInput_ replays SFX_PRESS_AB for A and B (home/window.asm)
|
footer = Strings("SEEN %3d OWN %3d", seen, owned),
|
||||||
local function beep(self)
|
pageJump = true, -- Left/Right page jumps like the original
|
||||||
if not (self.game and self.game.data) then return end
|
onCancel = opts.onCancel, -- B returns to the start menu when opened from it
|
||||||
require("src.core.Sound").play(self.game.data, "Press_AB")
|
onChoose = function(item, dexList)
|
||||||
end
|
|
||||||
|
|
||||||
function PokedexMenu:rows()
|
|
||||||
return math.min(ROWS, #self.items)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- engine/menus/pokedex.asm:290-313
|
|
||||||
function PokedexMenu:syncScroll()
|
|
||||||
local rows = self:rows()
|
|
||||||
if rows == 0 then
|
|
||||||
self.index, self.scroll = 1, 0
|
|
||||||
return
|
|
||||||
end
|
|
||||||
self.index = math.max(1, math.min(#self.items, self.index))
|
|
||||||
if self.index - self.scroll > rows then self.scroll = self.index - rows end
|
|
||||||
if self.index - self.scroll < 1 then self.scroll = self.index - 1 end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- engine/menus/pokedex.asm:314-342: Left and Right move wListScrollOffset
|
|
||||||
-- by seven and never touch wCurrentMenuItem, so the cursor keeps its row
|
|
||||||
function PokedexMenu:pageScroll(dir)
|
|
||||||
local n = #self.items
|
|
||||||
if n < ROWS then return end
|
|
||||||
local row = self.index - self.scroll
|
|
||||||
local scroll = self.scroll + dir * ROWS
|
|
||||||
scroll = math.max(0, math.min(n - ROWS, scroll))
|
|
||||||
self.scroll = scroll
|
|
||||||
self.index = math.min(n, scroll + row)
|
|
||||||
end
|
|
||||||
|
|
||||||
function PokedexMenu:close()
|
|
||||||
local top = self.game.stack:top()
|
|
||||||
if top == self then self.game.stack:pop() end
|
|
||||||
end
|
|
||||||
|
|
||||||
function PokedexMenu:update(dt)
|
|
||||||
-- back on top of the stack, so the side menu's hollow '▷' is gone
|
|
||||||
-- (engine/menus/pokedex.asm:61, PlaceUnfilledArrowMenuCursor)
|
|
||||||
self.hollowIndex = nil
|
|
||||||
local input = self.game.input
|
|
||||||
if #self.items == 0 then
|
|
||||||
if input:wasPressed("a") or input:wasPressed("b") then
|
|
||||||
beep(self)
|
|
||||||
self.game.stack:pop()
|
|
||||||
if self.onCancel then self.onCancel() end
|
|
||||||
end
|
|
||||||
return
|
|
||||||
end
|
|
||||||
if input:wasPressed("up") then
|
|
||||||
self.index = self.index - 1
|
|
||||||
elseif input:wasPressed("down") then
|
|
||||||
self.index = self.index + 1
|
|
||||||
elseif input:wasPressed("left") then
|
|
||||||
self:pageScroll(-1)
|
|
||||||
elseif input:wasPressed("right") then
|
|
||||||
self:pageScroll(1)
|
|
||||||
elseif input:wasPressed("b") then
|
|
||||||
beep(self)
|
|
||||||
self.game.stack:pop()
|
|
||||||
if self.onCancel then self.onCancel() end
|
|
||||||
return
|
|
||||||
elseif input:wasPressed("a") then
|
|
||||||
beep(self)
|
|
||||||
self.onChoose(self.items[self.index], self)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
self:syncScroll()
|
|
||||||
end
|
|
||||||
|
|
||||||
-- engine/menus/pokedex.asm:371-375, :81-85
|
|
||||||
function PokedexMenu:sideItems()
|
|
||||||
local labels = { Strings("DATA"), Strings("CRY"), Strings("AREA") }
|
|
||||||
if require("src.core.GameVersion").isYellow() then
|
|
||||||
labels[#labels + 1] = Strings("PRNT")
|
|
||||||
end
|
|
||||||
labels[#labels + 1] = Strings("QUIT")
|
|
||||||
return labels
|
|
||||||
end
|
|
||||||
|
|
||||||
local function sideRowY(menu, row)
|
|
||||||
return (menu.ty + menu.th - 2 - (#menu.items - row) * 2) * 8
|
|
||||||
end
|
|
||||||
|
|
||||||
local function drawSideMenu(self)
|
|
||||||
love.graphics.setColor(0, 0, 0, 1)
|
|
||||||
for row = 1, #self.items do
|
|
||||||
Font.draw(self.items[row].label, (self.tx + 2) * 8, sideRowY(self, row))
|
|
||||||
end
|
|
||||||
Font.drawCode(Theme.cursor, (self.tx + 1) * 8, sideRowY(self, self.index))
|
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- engine/menus/pokedex.asm HandlePokedexSideMenu, ShowPokedexMenu
|
|
||||||
-- .exitPokedex (#571)
|
|
||||||
local function chooseEntry(item, dexList)
|
|
||||||
local game = dexList.game
|
|
||||||
-- engine/menus/pokedex.asm:77-80: an unseen row hands back b=2 at once
|
|
||||||
if not item.value then return end
|
if not item.value then return end
|
||||||
|
-- the DATA / CRY / AREA / QUIT choice (engine/menus/pokedex.asm
|
||||||
|
-- PokedexMenuItemsText); CRY keeps the side menu open like the
|
||||||
|
-- original. B is what returns to the list: HandlePokedexSideMenu
|
||||||
|
-- hands back b=2 for B and b=1 for QUIT, and ShowPokedexMenu sends
|
||||||
|
-- b=1 to .exitPokedex, so QUIT closes the whole Pokédex (#571)
|
||||||
local Menu = require("src.ui.Menu")
|
local Menu = require("src.ui.Menu")
|
||||||
local Screens = require("src.ui.Screens")
|
local Screens = require("src.ui.Screens")
|
||||||
local entries = {
|
local entries = {
|
||||||
@@ -216,8 +77,9 @@ local function chooseEntry(item, dexList)
|
|||||||
Screens.push(game, "TownMap", { nestSpecies = item.value })
|
Screens.push(game, "TownMap", { nestSpecies = item.value })
|
||||||
end },
|
end },
|
||||||
}
|
}
|
||||||
-- pokeyellow engine/menus/pokedex.asm PokedexMenuItemsText,
|
-- Yellow's PRNT item (engine/menus/pokedex.asm PokedexMenuItemsText
|
||||||
-- PrintPokedexEntry
|
-- _YELLOW branch -> PrintPokedexEntry): the Game Boy Printer job is
|
||||||
|
-- stood in for by a PNG of the entry page saved under prints/.
|
||||||
if require("src.core.GameVersion").isYellow() then
|
if require("src.core.GameVersion").isYellow() then
|
||||||
entries[#entries + 1] = { label = Strings("PRNT"), onSelect = function()
|
entries[#entries + 1] = { label = Strings("PRNT"), onSelect = function()
|
||||||
local DexEntryMenu = require("src.ui.DexEntryMenu")
|
local DexEntryMenu = require("src.ui.DexEntryMenu")
|
||||||
@@ -239,72 +101,19 @@ local function chooseEntry(item, dexList)
|
|||||||
end }
|
end }
|
||||||
end
|
end
|
||||||
entries[#entries + 1] = { label = Strings("QUIT"), onSelect = function()
|
entries[#entries + 1] = { label = Strings("QUIT"), onSelect = function()
|
||||||
-- engine/menus/pokedex.asm .exitPokedex
|
-- .exitPokedex: drop the dex list too and hand back to whoever
|
||||||
|
-- opened it (the start menu, whose saved cursor is still on
|
||||||
|
-- POKéDEX -- wBattleAndStartSavedMenuItem)
|
||||||
dexList:close()
|
dexList:close()
|
||||||
if dexList.onCancel then dexList.onCancel() end
|
if opts.onCancel then opts.onCancel() end
|
||||||
end }
|
end }
|
||||||
local ty = dexList.rowsAt.items - 2
|
game.stack:push(Menu.new(game, entries,
|
||||||
local side = Menu.new(game, entries, { tx = 14, ty = ty, tw = 6 })
|
{ tx = 12, ty = 8, tw = 8,
|
||||||
-- the block is already on screen, so the side menu is the cursor alone:
|
th = #entries * 2 + 2 }))
|
||||||
-- text at column 16, cursor at 15 (engine/menus/pokedex.asm:82-86)
|
end,
|
||||||
side.tx, side.tw, side.ty, side.th = 14, 6, ty, #entries * 2 + 2
|
})
|
||||||
side.draw = drawSideMenu
|
list.sgbPalettes = PokedexMenu.sgbPalettes
|
||||||
dexList.hollowIndex = dexList.index
|
return list
|
||||||
game.stack:push(side)
|
|
||||||
end
|
|
||||||
PokedexMenu.onChoose = chooseEntry
|
|
||||||
|
|
||||||
-- engine/menus/pokedex.asm:256 writes tile $72 in the column left of the
|
|
||||||
-- name; the port paints the same marker rather than carry a one-tile sheet
|
|
||||||
local function drawBall(x, y)
|
|
||||||
love.graphics.circle("fill", x + 4, y + 4, 3.5)
|
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
|
||||||
love.graphics.rectangle("fill", x + 0.5, y + 3.5, 7, 1)
|
|
||||||
love.graphics.circle("fill", x + 4, y + 4, 1.2)
|
|
||||||
love.graphics.setColor(0, 0, 0, 1)
|
|
||||||
end
|
|
||||||
|
|
||||||
function PokedexMenu:draw()
|
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
|
||||||
love.graphics.rectangle("fill", 0, 0, 160, 144)
|
|
||||||
local at = self.rowsAt
|
|
||||||
local DexEntryMenu = require("src.ui.DexEntryMenu")
|
|
||||||
-- engine/menus/pokedex.asm:159-176: the divider column and the rule that
|
|
||||||
-- fences SEEN / OWN off from the side-menu block
|
|
||||||
for ty = 0, 17 do
|
|
||||||
DexEntryMenu.tile(self.game, DIVIDER[ty], 14, ty)
|
|
||||||
end
|
|
||||||
love.graphics.setColor(0, 0, 0, 1)
|
|
||||||
Font.draw(("─"):rep(5), 120, at.rule * 8)
|
|
||||||
Font.draw(Strings("CONTENTS"), 8, 8)
|
|
||||||
Font.draw(Strings("SEEN"), 128, at.seen * 8)
|
|
||||||
Font.draw(Strings("OWN"), 128, at.own * 8)
|
|
||||||
-- PrintNumber's fixed three-digit field ends on column 19
|
|
||||||
local function count(n, ty)
|
|
||||||
local text = tostring(n)
|
|
||||||
Font.draw(text, 152 - Font.width(text), ty * 8)
|
|
||||||
end
|
|
||||||
count(self.seenCount, at.seen + 1)
|
|
||||||
count(self.ownedCount, at.own + 1)
|
|
||||||
local labels = self:sideItems()
|
|
||||||
for i, label in ipairs(labels) do
|
|
||||||
Font.draw(label, 128, (at.items + (i - 1) * 2) * 8)
|
|
||||||
end
|
|
||||||
for row = 1, self:rows() do
|
|
||||||
local i = self.scroll + row
|
|
||||||
local item = self.items[i]
|
|
||||||
if not item then break end
|
|
||||||
local y = FIRST_ROW_Y + (row - 1) * 16
|
|
||||||
-- the number sits one row above its name (engine/menus/pokedex.asm:242-246)
|
|
||||||
Font.draw(item.num, NUM_X, y - 8)
|
|
||||||
if item.ball then drawBall(BALL_X, y) end
|
|
||||||
Font.draw(item.name, NAME_X, y)
|
|
||||||
if i == self.index then
|
|
||||||
Font.drawCode(self.hollowIndex == i
|
|
||||||
and Theme.cursorHollow or Theme.cursor, CURSOR_X, y)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return PokedexMenu
|
return PokedexMenu
|
||||||
|
|||||||
+23
-14
@@ -192,9 +192,9 @@ function SlotMachine.new(game, lucky)
|
|||||||
local self = setmetatable({}, SlotMachine)
|
local self = setmetatable({}, SlotMachine)
|
||||||
self.game = game
|
self.game = game
|
||||||
self.wheels = game.data.field.slotWheels
|
self.wheels = game.data.field.slotWheels
|
||||||
-- bet | spinup | spin | reroll | flash | message | payout | onemore
|
-- intro | bet | spinup | spin | reroll | flash | message | payout | onemore
|
||||||
-- PromptUserToPlaySlots: engine/slots/slot_machine.asm:9-23
|
-- PromptUserToPlaySlots asks "Want to play?" before the session starts.
|
||||||
self.stage = "bet"
|
self.stage = "intro"
|
||||||
self.yesno = 1 -- YES/NO cursor (1 = YES); wCurrentMenuItem
|
self.yesno = 1 -- YES/NO cursor (1 = YES); wCurrentMenuItem
|
||||||
-- CoinMultiplierSlotMachineText lists ×3/×2/×1 with the cursor defaulting to
|
-- CoinMultiplierSlotMachineText lists ×3/×2/×1 with the cursor defaulting to
|
||||||
-- the top (wCurrentMenuItem 0), i.e. bet = 3 - menuItem.
|
-- the top (wCurrentMenuItem 0), i.e. bet = 3 - menuItem.
|
||||||
@@ -417,7 +417,7 @@ function SlotMachine:startPayout()
|
|||||||
self.flash = false
|
self.flash = false
|
||||||
end
|
end
|
||||||
|
|
||||||
-- the "One more go?" YES/NO prompt.
|
-- YES/NO prompt shared by the intro ("Want to play?") and "One more go?".
|
||||||
function SlotMachine:updateYesNo(onYes)
|
function SlotMachine:updateYesNo(onYes)
|
||||||
local input = self.game.input
|
local input = self.game.input
|
||||||
if input:wasPressed("up") or input:wasPressed("down") then
|
if input:wasPressed("up") or input:wasPressed("down") then
|
||||||
@@ -435,6 +435,12 @@ function SlotMachine:update(dt)
|
|||||||
local input = self.game.input
|
local input = self.game.input
|
||||||
local save = self.game.save
|
local save = self.game.save
|
||||||
|
|
||||||
|
if self.stage == "intro" then
|
||||||
|
-- PromptUserToPlaySlots: "A slot machine! Want to play?"
|
||||||
|
self:updateYesNo(function() self:enterBet() end)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
if self.stage == "message" then
|
if self.stage == "message" then
|
||||||
if self.exitTimer then
|
if self.exitTimer then
|
||||||
-- OutOfCoinsSlotMachineText: DelayFrames 60, then leave
|
-- OutOfCoinsSlotMachineText: DelayFrames 60, then leave
|
||||||
@@ -632,7 +638,9 @@ end
|
|||||||
-- menu on the right (like MainSlotMachineLoop's TextBoxBorder + menus).
|
-- menu on the right (like MainSlotMachineLoop's TextBoxBorder + menus).
|
||||||
function SlotMachine:drawBottom()
|
function SlotMachine:drawBottom()
|
||||||
local lines
|
local lines
|
||||||
if self.stage == "bet" then
|
if self.stage == "intro" then
|
||||||
|
lines = { "A slot machine!", "Want to play?" }
|
||||||
|
elseif self.stage == "bet" then
|
||||||
lines = { "Bet how many", "coins?" }
|
lines = { "Bet how many", "coins?" }
|
||||||
elseif self.stage == "onemore" then
|
elseif self.stage == "onemore" then
|
||||||
lines = { "One more", "go?" }
|
lines = { "One more", "go?" }
|
||||||
@@ -653,21 +661,22 @@ function SlotMachine:drawBottom()
|
|||||||
Font.draw(lines[1] or "", 8, 14 * 8)
|
Font.draw(lines[1] or "", 8, 14 * 8)
|
||||||
Font.draw(lines[2] or "", 8, 16 * 8)
|
Font.draw(lines[2] or "", 8, 16 * 8)
|
||||||
if self.stage == "bet" then
|
if self.stage == "bet" then
|
||||||
-- hlcoord 14,11 with b=5 c=4 -- engine/slots/slot_machine.asm:83-86
|
Font.drawBox(14, 11, 6, 5)
|
||||||
Font.drawBox(14, 11, 6, 7)
|
|
||||||
love.graphics.setColor(0, 0, 0, 1)
|
love.graphics.setColor(0, 0, 0, 1)
|
||||||
Font.draw("×3", 16 * 8, 12 * 8)
|
Font.draw("×3", 16 * 8, 12 * 8)
|
||||||
Font.draw("×2", 16 * 8, 13 * 8)
|
Font.draw("×2", 16 * 8, 13 * 8)
|
||||||
Font.draw("×1", 16 * 8, 14 * 8)
|
Font.draw("×1", 16 * 8, 14 * 8)
|
||||||
Font.drawCode(0xED, 15 * 8, (12 + self.betIndex) * 8)
|
Font.drawCode(0xED, 15 * 8, (12 + self.betIndex) * 8)
|
||||||
elseif self.stage == "onemore" then
|
elseif self.stage == "intro" or self.stage == "onemore" then
|
||||||
-- hlcoord 14,12 with the YES_NO_MENU 4x3 body
|
-- "One more go?" sits at the right of the box (hlcoord 14,12); the longer
|
||||||
-- engine/slots/slot_machine.asm:136-138, data/yes_no_menu_strings.asm:10
|
-- "A slot machine!" prompt would clip against it, so the intro's YES/NO
|
||||||
Font.drawBox(14, 12, 6, 5)
|
-- floats above the reels instead.
|
||||||
|
local by = self.stage == "intro" and 6 or 11
|
||||||
|
Font.drawBox(13, by, 6, 5)
|
||||||
love.graphics.setColor(0, 0, 0, 1)
|
love.graphics.setColor(0, 0, 0, 1)
|
||||||
Font.draw(Strings("YES"), 16 * 8, 13 * 8)
|
Font.draw(Strings("YES"), 15 * 8, (by + 1) * 8)
|
||||||
Font.draw(Strings("NO"), 16 * 8, 14 * 8)
|
Font.draw(Strings("NO"), 15 * 8, (by + 2) * 8)
|
||||||
Font.drawCode(0xED, 15 * 8, (13 + (self.yesno == 1 and 0 or 1)) * 8)
|
Font.drawCode(0xED, 14 * 8, (by + 1 + (self.yesno == 1 and 0 or 1)) * 8)
|
||||||
end
|
end
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -200,12 +200,12 @@ function StartMenu.new(game)
|
|||||||
anchor = "topright" })
|
anchor = "topright" })
|
||||||
-- the cursor position survives closing the menu
|
-- the cursor position survives closing the menu
|
||||||
-- (wBattleAndStartSavedMenuItem, home/start_menu.asm)
|
-- (wBattleAndStartSavedMenuItem, home/start_menu.asm)
|
||||||
menu.index = math.min(game.startMenuIndex or 1, #items)
|
menu.index = math.min(game.save.startMenuIndex or 1, #items)
|
||||||
menu:clampScroll()
|
menu:clampScroll()
|
||||||
local baseUpdate = menu.update
|
local baseUpdate = menu.update
|
||||||
menu.update = function(self, dt)
|
menu.update = function(self, dt)
|
||||||
baseUpdate(self, dt)
|
baseUpdate(self, dt)
|
||||||
game.startMenuIndex = self.index
|
game.save.startMenuIndex = self.index
|
||||||
end
|
end
|
||||||
|
|
||||||
-- inside the Safari Zone the start menu also shows remaining steps and
|
-- inside the Safari Zone the start menu also shows remaining steps and
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ local Font = require("src.render.Font")
|
|||||||
-- TypeChart.displayName maps it back to "PSYCHIC", like HallOfFame and the
|
-- TypeChart.displayName maps it back to "PSYCHIC", like HallOfFame and the
|
||||||
-- battle move-type box already do (#214).
|
-- battle move-type box already do (#214).
|
||||||
local TypeChart = require("src.battle.TypeChart")
|
local TypeChart = require("src.battle.TypeChart")
|
||||||
local LevelDisplay = require("src.ui.LevelDisplay")
|
|
||||||
local Strings = require("src.core.Strings")
|
local Strings = require("src.core.Strings")
|
||||||
local Stats = require("src.pokemon.Stats")
|
local Stats = require("src.pokemon.Stats")
|
||||||
local Status = require("src.battle.Status")
|
local Status = require("src.battle.Status")
|
||||||
@@ -138,9 +137,7 @@ function SummaryMenu:draw()
|
|||||||
if self.page == 1 then
|
if self.page == 1 then
|
||||||
-- level is page 1 only: StatusScreen2 opens with ClearScreenArea over
|
-- level is page 1 only: StatusScreen2 opens with ClearScreenArea over
|
||||||
-- (9,2) 5x10 (status_screen.asm:303-305). #280
|
-- (9,2) 5x10 (status_screen.asm:303-305). #280
|
||||||
if LevelDisplay.visible(mon, "summary", self.game) then -- RFC 0019
|
|
||||||
printLevel(14, 2, mon.level)
|
printLevel(14, 2, mon.level)
|
||||||
end
|
|
||||||
drawLineBox(19, 1, 6, 10)
|
drawLineBox(19, 1, 6, 10)
|
||||||
-- engine/pokemon/status_screen.asm:120-125
|
-- engine/pokemon/status_screen.asm:120-125
|
||||||
local PaletteFX = require("src.render.PaletteFX")
|
local PaletteFX = require("src.render.PaletteFX")
|
||||||
@@ -199,12 +196,8 @@ function SummaryMenu:draw()
|
|||||||
local nextExp = mon.level < 100
|
local nextExp = mon.level < 100
|
||||||
and (Growth.expForLevel(def.growthRate, mon.level + 1) - mon.exp) or 0
|
and (Growth.expForLevel(def.growthRate, mon.level + 1) - mon.exp) or 0
|
||||||
Font.draw(("%7d"):format(math.max(0, nextExp)), 56, 48)
|
Font.draw(("%7d"):format(math.max(0, nextExp)), 56, 48)
|
||||||
if LevelDisplay.visible(mon, "summary", self.game) then -- RFC 0019
|
|
||||||
-- the '<to>' arrow is half a sentence without the level it points at,
|
|
||||||
-- so the pair is hidden together
|
|
||||||
HudTiles.statusTile(0x70, 112, 48) -- '<to>' at (14,6), was missing (#280)
|
HudTiles.statusTile(0x70, 112, 48) -- '<to>' at (14,6), was missing (#280)
|
||||||
printLevel(16, 6, math.min(100, mon.level + 1))
|
printLevel(16, 6, math.min(100, mon.level + 1))
|
||||||
end
|
|
||||||
Font.drawBox(0, 8, 20, 10)
|
Font.drawBox(0, 8, 20, 10)
|
||||||
for i = 1, 4 do
|
for i = 1, 4 do
|
||||||
local mv = mon.moves[i]
|
local mv = mon.moves[i]
|
||||||
|
|||||||
+24
-40
@@ -130,25 +130,6 @@ local function markerXY(loc)
|
|||||||
return loc.x * 8 + 16, loc.y * 8 + 8
|
return loc.x * 8 + 16, loc.y * 8 + 8
|
||||||
end
|
end
|
||||||
|
|
||||||
-- the marker wears its own sheet's OBJ palette and shade-0 keying
|
|
||||||
-- engine/items/town_map.asm:342
|
|
||||||
local function markerSheet(def, seed)
|
|
||||||
local colors, group
|
|
||||||
if PaletteFX.usesGbcPack() then
|
|
||||||
colors, group = PaletteFX.spriteObp(def, seed)
|
|
||||||
end
|
|
||||||
if not colors then
|
|
||||||
if PaletteFX.usesSpriteObp() then
|
|
||||||
colors, group = PaletteFX.ogObj()
|
|
||||||
else
|
|
||||||
colors, group = PaletteFX.dmgObj()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
local ok, img = pcall(SpriteRenderer.obpImage, def and def.image, colors, group)
|
|
||||||
if not (ok and img) then return nil, nil end
|
|
||||||
return img, love.graphics.newQuad(0, 0, 16, 16, img:getDimensions())
|
|
||||||
end
|
|
||||||
|
|
||||||
-- the row-0 name banner; fly mode prefixes "To " like LoadTownMap_Fly
|
-- the row-0 name banner; fly mode prefixes "To " like LoadTownMap_Fly
|
||||||
-- (engine/menus/town_map.asm prints the destination as "To <NAME>")
|
-- (engine/menus/town_map.asm prints the destination as "To <NAME>")
|
||||||
function TownMap:bannerText(loc)
|
function TownMap:bannerText(loc)
|
||||||
@@ -243,18 +224,32 @@ function TownMap.new(game, opts)
|
|||||||
local mapId = game.overworld and game.overworld.map and game.overworld.map.id
|
local mapId = game.overworld and game.overworld.map and game.overworld.map.id
|
||||||
self.playerLoc = mapId and self.byMap[mapId] or nil
|
self.playerLoc = mapId and self.byMap[mapId] or nil
|
||||||
-- engine/items/town_map.asm:347
|
-- engine/items/town_map.asm:347
|
||||||
|
do
|
||||||
local playerSprites = (game.data.field and game.data.field.playerSprites)
|
local playerSprites = (game.data.field and game.data.field.playerSprites)
|
||||||
or {}
|
or {}
|
||||||
local sprites = game.data.sprites or {}
|
local sprites = game.data.sprites or {}
|
||||||
self.playerSheet, self.playerQuad =
|
local red = sprites[playerSprites.walk or "SPRITE_RED"]
|
||||||
markerSheet(sprites[playerSprites.walk or "SPRITE_RED"] or sprites.SPRITE_RED,
|
or sprites.SPRITE_RED
|
||||||
"player")
|
-- the marker is the overworld walking sheet, so it wears that sheet's OBJ
|
||||||
-- LoadTownMap_Fly overwrites the cursor tiles with BirdSprite and marks
|
-- palette and shade-0 keying -- engine/items/town_map.asm:342
|
||||||
-- the destination with it -- engine/items/town_map.asm:146-149, 177-179
|
local colors, group
|
||||||
if self.fly then
|
if PaletteFX.usesGbcPack() then
|
||||||
self.birdSheet, self.birdQuad =
|
colors, group = PaletteFX.spriteObp(red, "player")
|
||||||
markerSheet(sprites[playerSprites.fly or "SPRITE_BIRD"]
|
end
|
||||||
or sprites.SPRITE_BIRD, "bird")
|
if not colors then
|
||||||
|
if PaletteFX.usesSpriteObp() then
|
||||||
|
colors, group = PaletteFX.ogObj()
|
||||||
|
else
|
||||||
|
colors, group = PaletteFX.dmgObj()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local ok, img = pcall(SpriteRenderer.obpImage,
|
||||||
|
red and red.image, colors, group)
|
||||||
|
if ok and img then
|
||||||
|
self.playerSheet = img
|
||||||
|
self.playerQuad = love.graphics.newQuad(0, 0, 16, 16,
|
||||||
|
img:getDimensions())
|
||||||
|
end
|
||||||
end
|
end
|
||||||
self.sel = 1
|
self.sel = 1
|
||||||
-- LoadTownMap_Fly always opens with hl on wFlyLocationsList[0], the FIRST
|
-- LoadTownMap_Fly always opens with hl on wFlyLocationsList[0], the FIRST
|
||||||
@@ -407,19 +402,8 @@ function TownMap:draw()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
-- WriteTownMapSpriteOAM carry quirk: -4 X, -3 Y for cursor and player alike -- engine/items/town_map.asm:454
|
-- WriteTownMapSpriteOAM carry quirk: -4 X, -3 Y for cursor and player alike -- engine/items/town_map.asm:454
|
||||||
-- LoadTownMap_Fly's .inputLoop has no blinking animation
|
|
||||||
-- engine/items/town_map.asm:190-198
|
|
||||||
local showCursor = true
|
local showCursor = true
|
||||||
if self.fly and self.birdSheet then
|
if GameVersion.generation() == 1 then
|
||||||
if selected then
|
|
||||||
local x, y = markerXY(selected)
|
|
||||||
love.graphics.draw(self.birdSheet, self.birdQuad, x - 4, y - 3)
|
|
||||||
if PaletteFX.usesSpriteObp() or PaletteFX.usesGbcPack() then
|
|
||||||
PaletteFX.markUiSpriteRedraw(self.birdSheet, self.birdQuad, x - 4, y - 3)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
showCursor = false
|
|
||||||
elseif GameVersion.generation() == 1 then
|
|
||||||
showCursor = self.blink < 25
|
showCursor = self.blink < 25
|
||||||
else
|
else
|
||||||
showCursor = self.blink % 16 < 10
|
showCursor = self.blink % 16 < 10
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
|
|
||||||
local bit = require("bit")
|
local bit = require("bit")
|
||||||
local Assets = require("src.render.Assets")
|
local Assets = require("src.render.Assets")
|
||||||
local Chrome = require("src.ui.gen2.Chrome")
|
|
||||||
local GbcPalette = require("src.render.GbcPalette")
|
local GbcPalette = require("src.render.GbcPalette")
|
||||||
local Palettes = require("src.world.gen2.Palettes")
|
local Palettes = require("src.world.gen2.Palettes")
|
||||||
|
|
||||||
@@ -165,8 +164,12 @@ local function needsCanvas(runner)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- The battle background is BG colour 0 everywhere the two pic boxes and the
|
-- The battle background is BG colour 0 everywhere the two pic boxes and the
|
||||||
|
-- text box are not, which Chrome.clear draws as plain white; anything the
|
||||||
|
-- scanline blit exposes has to be that colour and not a hole.
|
||||||
function BattleAnimView:fillBackground()
|
function BattleAnimView:fillBackground()
|
||||||
Chrome.paletteFill(0, 0, SCREEN_W, SCREEN_H)
|
local G = love.graphics
|
||||||
|
G.setColor(1, 1, 1, 1)
|
||||||
|
G.rectangle("fill", 0, 0, SCREEN_W, SCREEN_H)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- One reusable quad, re-aimed per scanline. A row shifted by `dx` is drawn
|
-- One reusable quad, re-aimed per scanline. A row shifted by `dx` is drawn
|
||||||
|
|||||||
+35
-91
@@ -605,66 +605,23 @@ function BattleState:animData(mon)
|
|||||||
local def = self.pokemon and mon and self.pokemon[mon.species]
|
local def = self.pokemon and mon and self.pokemon[mon.species]
|
||||||
if not def then return nil end
|
if not def then return nil end
|
||||||
if mon.species == Unown.SPECIES and def.letters then
|
if mon.species == Unown.SPECIES and def.letters then
|
||||||
local entry = def.letters[Unown.name(Unown.monLetter(mon))]
|
local entry = def.letters[Unown.monLetter(mon)]
|
||||||
if entry and entry.anim then return entry.anim end
|
if entry and entry.anim then return entry.anim end
|
||||||
end
|
end
|
||||||
return def.anim
|
return def.anim
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ../pokecrystal/engine/gfx/load_pics.asm:105-131
|
|
||||||
function BattleState:frontPicReplaced(mon)
|
|
||||||
local def = self.pokemon and mon and self.pokemon[mon.species]
|
|
||||||
local vanilla = def and def.spriteFront
|
|
||||||
if mon and mon.species == Unown.SPECIES then
|
|
||||||
vanilla = Unown.formSprite(self.pokemon, Unown.monLetter(mon), false)
|
|
||||||
or vanilla
|
|
||||||
end
|
|
||||||
if type(vanilla) ~= "string" then return false end
|
|
||||||
local _, _, path = self:pic(mon, false)
|
|
||||||
if type(path) == "string" and path ~= vanilla then return true end
|
|
||||||
return Assets.resolve(vanilla) ~= vanilla
|
|
||||||
end
|
|
||||||
|
|
||||||
-- one cart asset, two files on the pokemon.sprite seam (#1827)
|
|
||||||
-- ../pokecrystal/engine/gfx/load_pics.asm:132-158
|
|
||||||
function BattleState:animSheetPath(mon, data)
|
|
||||||
local path = data and data.sheet
|
|
||||||
if type(path) ~= "string" then return nil, false end
|
|
||||||
if not Runtime.wantsHook("pokemon.sprite") then
|
|
||||||
return path, Assets.resolve(path) ~= path
|
|
||||||
end
|
|
||||||
local letter
|
|
||||||
if mon and mon.species == Unown.SPECIES then letter = Unown.monLetter(mon) end
|
|
||||||
local hooked = Runtime.call("pokemon.sprite",
|
|
||||||
function(value) return value end, path, {
|
|
||||||
species = mon and mon.species,
|
|
||||||
side = "front",
|
|
||||||
kind = "battle_anim",
|
|
||||||
mon = mon,
|
|
||||||
data = (self.game and self.game.data) or nil,
|
|
||||||
letter = letter,
|
|
||||||
shiny = mon and mon.shiny and true or false,
|
|
||||||
})
|
|
||||||
if type(hooked) == "string" and hooked ~= "" and hooked ~= path then
|
|
||||||
return hooked, true
|
|
||||||
end
|
|
||||||
return path, Assets.resolve(path) ~= path
|
|
||||||
end
|
|
||||||
|
|
||||||
-- ANIM_MON_NORMAL, the scene BattleStartMessage runs on the enemy's frontpic.
|
-- ANIM_MON_NORMAL, the scene BattleStartMessage runs on the enemy's frontpic.
|
||||||
-- ../pokecrystal/engine/battle/core.asm:9112-9113
|
-- ../pokecrystal/engine/battle/core.asm:9112-9113
|
||||||
function BattleState:startFrontAnim(mon)
|
function BattleState:startFrontAnim(mon)
|
||||||
self.frontAnim = nil
|
self.frontAnim = nil
|
||||||
local data = self:animData(mon)
|
local data = self:animData(mon)
|
||||||
if not data then return end
|
if not data then return end
|
||||||
local sheet, sheetReplaced = self:animSheetPath(mon, data)
|
local cached = self.picCache[data.sheet]
|
||||||
if not sheet then return end
|
|
||||||
if not sheetReplaced and self:frontPicReplaced(mon) then return end
|
|
||||||
local cached = self.picCache[sheet]
|
|
||||||
if cached == nil then
|
if cached == nil then
|
||||||
local ok, image = pcall(Assets.image, sheet)
|
local ok, image = pcall(Assets.image, data.sheet)
|
||||||
cached = ok and image or false
|
cached = ok and image or false
|
||||||
self.picCache[sheet] = cached
|
self.picCache[data.sheet] = cached
|
||||||
end
|
end
|
||||||
if not cached then return end
|
if not cached then return end
|
||||||
local runner = MonAnim.new(data, "battle")
|
local runner = MonAnim.new(data, "battle")
|
||||||
@@ -3563,7 +3520,7 @@ function BattleState:drawHud()
|
|||||||
-- And nothing at all before UpdateEnemyHUD has ever run: the intro bands
|
-- And nothing at all before UpdateEnemyHUD has ever run: the intro bands
|
||||||
-- slide in over a blanked tilemap (core.asm:8554/8564).
|
-- slide in over a blanked tilemap (core.asm:8554/8564).
|
||||||
if showStatus and self.showEnemyHud and not self:hudCleared("enemy") then
|
if showStatus and self.showEnemyHud and not self:hudCleared("enemy") then
|
||||||
Chrome.printThrough(self:name(enemy), 1, 0, Chrome.DEFAULT_BOX_PALETTE)
|
Chrome.print(self:name(enemy), 1, 0)
|
||||||
-- PrintLevel writes <LV> at the coordinate it is given and then LEFT-aligns
|
-- PrintLevel writes <LV> at the coordinate it is given and then LEFT-aligns
|
||||||
-- the digits after it, so the glyph is pinned to column 6 whether the level
|
-- the digits after it, so the glyph is pinned to column 6 whether the level
|
||||||
-- is 5 or 100; only a three-digit level moves, and it does so by eating the
|
-- is 5 or 100; only a three-digit level moves, and it does so by eating the
|
||||||
@@ -3571,12 +3528,10 @@ function BattleState:drawHud()
|
|||||||
-- PlaceNonFaintStatus (engine/pokemon/mon_stats.asm): a statused mon's tag
|
-- PlaceNonFaintStatus (engine/pokemon/mon_stats.asm): a statused mon's tag
|
||||||
-- prints where the level goes, and DrawEnemyHUD's `.skip_level` arm drops
|
-- prints where the level goes, and DrawEnemyHUD's `.skip_level` arm drops
|
||||||
-- the level entirely while one is up.
|
-- the level entirely while one is up.
|
||||||
Chrome.printThrough(self:statusTag(enemy, "enemy")
|
Chrome.print(self:statusTag(enemy, "enemy")
|
||||||
or ("<LV>" .. tostring(enemy.level or 1)), 6, 1, Chrome.DEFAULT_BOX_PALETTE)
|
or ("<LV>" .. tostring(enemy.level or 1)), 6, 1)
|
||||||
local enemyGender = self:genderSymbol(enemy)
|
local enemyGender = self:genderSymbol(enemy)
|
||||||
if enemyGender then
|
if enemyGender then Chrome.print(enemyGender, 9, 1) end
|
||||||
Chrome.printThrough(enemyGender, 9, 1, Chrome.DEFAULT_BOX_PALETTE)
|
|
||||||
end
|
|
||||||
-- `ld a, [wBattleMode] / dec a / ret nz`, then CheckCaughtMon puts $5d at
|
-- `ld a, [wBattleMode] / dec a / ret nz`, then CheckCaughtMon puts $5d at
|
||||||
-- (1,1) (engine/battle/trainer_huds.asm:140-152).
|
-- (1,1) (engine/battle/trainer_huds.asm:140-152).
|
||||||
if self.battle and self.battle.wild and self.caughtMark then
|
if self.battle and self.battle.wild and self.caughtMark then
|
||||||
@@ -3625,20 +3580,17 @@ function BattleState:drawHud()
|
|||||||
Font.useBattleExtra(wasBattle)
|
Font.useBattleExtra(wasBattle)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
Chrome.printThrough(self:name(player), 10, 7, Chrome.DEFAULT_BOX_PALETTE)
|
Chrome.print(self:name(player), 10, 7)
|
||||||
-- PrintPlayerHUD places the same status tag at (14,8) and skips the level
|
-- PrintPlayerHUD places the same status tag at (14,8) and skips the level
|
||||||
-- while it is up.
|
-- while it is up.
|
||||||
Chrome.printThrough(self:statusTag(player, "player")
|
Chrome.print(self:statusTag(player, "player")
|
||||||
or ("<LV>" .. tostring(self.shownLevel or player.level or 1)), 14, 8,
|
or ("<LV>" .. tostring(self.shownLevel or player.level or 1)), 14, 8)
|
||||||
Chrome.DEFAULT_BOX_PALETTE)
|
|
||||||
local playerGender = self:genderSymbol(player)
|
local playerGender = self:genderSymbol(player)
|
||||||
if playerGender then
|
if playerGender then Chrome.print(playerGender, 17, 8) end
|
||||||
Chrome.printThrough(playerGender, 17, 8, Chrome.DEFAULT_BOX_PALETTE)
|
|
||||||
end
|
|
||||||
self:drawHpBar(player, "player", 10, 9)
|
self:drawHpBar(player, "player", 10, 9)
|
||||||
local maxHp = player.maxHp or (player.stats and player.stats.hp) or 0
|
local maxHp = player.maxHp or (player.stats and player.stats.hp) or 0
|
||||||
Chrome.printRightThrough(("%d/%d"):format(self:hudHp(player, "player"), maxHp),
|
Chrome.printRight(("%d/%d"):format(self:hudHp(player, "player"), maxHp),
|
||||||
18, 10, Chrome.DEFAULT_BOX_PALETTE)
|
18, 10)
|
||||||
-- Stub on the RIGHT (tile $73), border from (18,10) laid leftward, exp bar
|
-- Stub on the RIGHT (tile $73), border from (18,10) laid leftward, exp bar
|
||||||
-- at (10,11).
|
-- at (10,11).
|
||||||
-- The chased fill, not the mon's: AnimateExpBar crawls it, and reading the
|
-- The chased fill, not the mon's: AnimateExpBar crawls it, and reading the
|
||||||
@@ -3707,8 +3659,8 @@ end
|
|||||||
function BattleState:printMessage()
|
function BattleState:printMessage()
|
||||||
local lines = Chrome.wrap(self.message or "", TEXT_WIDTH)
|
local lines = Chrome.wrap(self.message or "", TEXT_WIDTH)
|
||||||
for i = 1, math.min(#lines, TEXT_ROWS) do
|
for i = 1, math.min(#lines, TEXT_ROWS) do
|
||||||
Chrome.printThrough(lines[i], TEXT_INNER_X,
|
Chrome.print(lines[i], TEXT_INNER_X,
|
||||||
TEXT_INNER_Y + (i - 1) * TEXT_ROW_STEP, Chrome.DEFAULT_BOX_PALETTE)
|
TEXT_INNER_Y + (i - 1) * TEXT_ROW_STEP)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -3718,17 +3670,15 @@ function BattleState:drawMoveInfoBox(move)
|
|||||||
if not move then return end
|
if not move then return end
|
||||||
local fighter = self.battle and self.battle.player
|
local fighter = self.battle and self.battle.player
|
||||||
if fighter and self.battle:moveDisabled(fighter, move.id) then
|
if fighter and self.battle:moveDisabled(fighter, move.id) then
|
||||||
Chrome.printThrough("Disabled!", 1, 10, Chrome.DEFAULT_BOX_PALETTE)
|
Chrome.print("Disabled!", 1, 10)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local def = self.game and self.game.data and self.game.data.moves
|
local def = self.game and self.game.data and self.game.data.moves
|
||||||
and self.game.data.moves[move.id]
|
and self.game.data.moves[move.id]
|
||||||
Chrome.printThrough("TYPE/", 1, 9, Chrome.DEFAULT_BOX_PALETTE)
|
Chrome.print("TYPE/", 1, 9)
|
||||||
local moveType = def and def.type
|
local moveType = def and def.type
|
||||||
Chrome.printThrough(moveType and (TYPE_NAMES[moveType] or moveType) or "",
|
Chrome.print(moveType and (TYPE_NAMES[moveType] or moveType) or "", 2, 10)
|
||||||
2, 10, Chrome.DEFAULT_BOX_PALETTE)
|
Chrome.print(("%2d/%2d"):format(move.pp or 0, move.maxPp or 0), 5, 11)
|
||||||
Chrome.printThrough(("%2d/%2d"):format(move.pp or 0, move.maxPp or 0),
|
|
||||||
5, 11, Chrome.DEFAULT_BOX_PALETTE)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function BattleState:drawPanel()
|
function BattleState:drawPanel()
|
||||||
@@ -3737,7 +3687,7 @@ function BattleState:drawPanel()
|
|||||||
-- required there; everywhere else a missing side is a caller bug.
|
-- required there; everywhere else a missing side is a caller bug.
|
||||||
local hasPlayer = self.battle and (self.battle.player or self.tutorial)
|
local hasPlayer = self.battle and (self.battle.player or self.tutorial)
|
||||||
if not (self.battle and hasPlayer and self.battle.enemy) then
|
if not (self.battle and hasPlayer and self.battle.enemy) then
|
||||||
Chrome.printThrough("NO BATTLE", 1, 1, Chrome.DEFAULT_BOX_PALETTE)
|
Chrome.print("NO BATTLE", 1, 1)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
self:drawHud()
|
self:drawHud()
|
||||||
@@ -3769,10 +3719,8 @@ function BattleState:drawPanel()
|
|||||||
local col = ((i - 1) % 2) * spacing
|
local col = ((i - 1) % 2) * spacing
|
||||||
local row = math.floor((i - 1) / 2) * 2
|
local row = math.floor((i - 1) / 2) * 2
|
||||||
local tx, ty = boxX + 2 + col, 14 + row
|
local tx, ty = boxX + 2 + col, 14 + row
|
||||||
if i == self.menuIndex then
|
if i == self.menuIndex then Chrome.cursor(tx - 1, ty) end
|
||||||
Chrome.cursorThrough(tx - 1, ty, Chrome.DEFAULT_BOX_PALETTE)
|
Chrome.print(label, tx, ty)
|
||||||
end
|
|
||||||
Chrome.printThrough(label, tx, ty, Chrome.DEFAULT_BOX_PALETTE)
|
|
||||||
end
|
end
|
||||||
elseif self.phase == "moves"
|
elseif self.phase == "moves"
|
||||||
or (self.phase == "choose-forget" and (self.messageTimer or 0) <= 0) then
|
or (self.phase == "choose-forget" and (self.messageTimer or 0) <= 0) then
|
||||||
@@ -3792,24 +3740,21 @@ function BattleState:drawPanel()
|
|||||||
for i, move in ipairs(moves) do
|
for i, move in ipairs(moves) do
|
||||||
local ty = 13 + (i - 1)
|
local ty = 13 + (i - 1)
|
||||||
-- Cursor in the box's own gutter, not clipped against the border.
|
-- Cursor in the box's own gutter, not clipped against the border.
|
||||||
if i == cursorRow then
|
if i == cursorRow then Chrome.cursor(cursorCol, ty) end
|
||||||
Chrome.cursorThrough(cursorCol, ty, Chrome.DEFAULT_BOX_PALETTE)
|
|
||||||
end
|
|
||||||
-- The held slot's marker. `.battle_player_moves` writes '▷' into the
|
-- The held slot's marker. `.battle_player_moves` writes '▷' into the
|
||||||
-- row wSwappingMove names (engine/battle/core.asm:5157-5165) so a move
|
-- row wSwappingMove names (engine/battle/core.asm:5157-5165) so a move
|
||||||
-- picked up for a swap is visible while the cursor moves off it. It
|
-- picked up for a swap is visible while the cursor moves off it. It
|
||||||
-- hlcoord 5, 13 is the cursor's own gutter, so PlaceMenuCursor covers
|
-- hlcoord 5, 13 is the cursor's own gutter, so PlaceMenuCursor covers
|
||||||
-- the marker on the cursor's row.
|
-- the marker on the cursor's row.
|
||||||
if not forgetting and self.moveSwapIndex == i and i ~= cursorRow then
|
if not forgetting and self.moveSwapIndex == i and i ~= cursorRow then
|
||||||
Chrome.printThrough("\u{25B7}", cursorCol, ty, Chrome.DEFAULT_BOX_PALETTE)
|
Chrome.print("\u{25B7}", cursorCol, ty)
|
||||||
end
|
end
|
||||||
local def = self.game and self.game.data and self.game.data.moves
|
local def = self.game and self.game.data and self.game.data.moves
|
||||||
and self.game.data.moves[move.id]
|
and self.game.data.moves[move.id]
|
||||||
Chrome.printThrough((def and def.name) or move.id, nameCol, ty,
|
Chrome.print((def and def.name) or move.id, nameCol, ty)
|
||||||
Chrome.DEFAULT_BOX_PALETTE)
|
|
||||||
if not moveMenu then
|
if not moveMenu then
|
||||||
Chrome.printRightThrough(("%d/%d"):format(move.pp or 0, move.maxPp or 0),
|
Chrome.printRight(("%d/%d"):format(move.pp or 0, move.maxPp or 0),
|
||||||
19, ty, Chrome.DEFAULT_BOX_PALETTE)
|
19, ty)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if moveMenu then self:drawMoveInfoBox(moves[cursorRow]) end
|
if moveMenu then self:drawMoveInfoBox(moves[cursorRow]) end
|
||||||
@@ -3828,14 +3773,13 @@ function BattleState:drawPanel()
|
|||||||
local left = (self.phase == "ask-shift" or self.phase == "ask-next-mon")
|
local left = (self.phase == "ask-shift" or self.phase == "ask-next-mon")
|
||||||
and 1 or 14
|
and 1 or 14
|
||||||
Chrome.box(left, 7, 6, 5)
|
Chrome.box(left, 7, 6, 5)
|
||||||
Chrome.printThrough("YES", left + 2, 8, Chrome.DEFAULT_BOX_PALETTE)
|
Chrome.print("YES", left + 2, 8)
|
||||||
Chrome.printThrough("NO", left + 2, 10, Chrome.DEFAULT_BOX_PALETTE)
|
Chrome.print("NO", left + 2, 10)
|
||||||
local index = self.phase == "ask-nickname" and self.nicknameIndex
|
local index = self.phase == "ask-nickname" and self.nicknameIndex
|
||||||
or self.phase == "ask-shift" and self.shiftIndex
|
or self.phase == "ask-shift" and self.shiftIndex
|
||||||
or self.phase == "ask-next-mon" and self.nextMonIndex
|
or self.phase == "ask-next-mon" and self.nextMonIndex
|
||||||
or self.forgetChoice
|
or self.forgetChoice
|
||||||
Chrome.cursorThrough(left + 1, index == 1 and 8 or 10,
|
Chrome.cursor(left + 1, index == 1 and 8 or 10)
|
||||||
Chrome.DEFAULT_BOX_PALETTE)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if self.phase == "stats-box" and self.statsBoxMon then
|
if self.phase == "stats-box" and self.statsBoxMon then
|
||||||
@@ -3864,9 +3808,8 @@ function BattleState:drawStatsBox(mon)
|
|||||||
Chrome.textbox(9, 0, 9, 10)
|
Chrome.textbox(9, 0, 9, 10)
|
||||||
for i, row in ipairs(STATS_BOX_ROWS) do
|
for i, row in ipairs(STATS_BOX_ROWS) do
|
||||||
local ty = 1 + (i - 1) * 2
|
local ty = 1 + (i - 1) * 2
|
||||||
Chrome.printThrough(Strings(row[1]), 11, ty, Chrome.DEFAULT_BOX_PALETTE)
|
Chrome.print(Strings(row[1]), 11, ty)
|
||||||
Chrome.printRightThrough(("%d"):format(stats[row[2]] or 0), 19, ty + 1,
|
Chrome.printRight(("%d"):format(stats[row[2]] or 0), 19, ty + 1)
|
||||||
Chrome.DEFAULT_BOX_PALETTE)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -3950,7 +3893,8 @@ end
|
|||||||
|
|
||||||
function BattleState:drawWidescreen(winW, winH)
|
function BattleState:drawWidescreen(winW, winH)
|
||||||
local G = love.graphics
|
local G = love.graphics
|
||||||
Chrome.letterbox(winW, winH, 1, 1, 1)
|
G.setColor(1, 1, 1, 1)
|
||||||
|
G.rectangle("fill", 0, 0, winW, winH)
|
||||||
local scale = Chrome.fitScale(winW, winH)
|
local scale = Chrome.fitScale(winW, winH)
|
||||||
local ox, oy = Chrome.fitOrigin(winW, winH, scale)
|
local ox, oy = Chrome.fitOrigin(winW, winH, scale)
|
||||||
G.push()
|
G.push()
|
||||||
|
|||||||
@@ -945,7 +945,8 @@ end
|
|||||||
|
|
||||||
function BoxMenu:drawWidescreen(winW, winH)
|
function BoxMenu:drawWidescreen(winW, winH)
|
||||||
local G = love.graphics
|
local G = love.graphics
|
||||||
Chrome.letterbox(winW, winH, 1, 1, 1)
|
G.setColor(1, 1, 1, 1)
|
||||||
|
G.rectangle("fill", 0, 0, winW, winH)
|
||||||
local scale = Chrome.fitScale(winW, winH)
|
local scale = Chrome.fitScale(winW, winH)
|
||||||
G.push()
|
G.push()
|
||||||
G.translate(Chrome.fitOrigin(winW, winH, scale))
|
G.translate(Chrome.fitOrigin(winW, winH, scale))
|
||||||
|
|||||||
@@ -968,7 +968,8 @@ end
|
|||||||
|
|
||||||
function CardFlip:drawWidescreen(winW, winH)
|
function CardFlip:drawWidescreen(winW, winH)
|
||||||
local G = love.graphics
|
local G = love.graphics
|
||||||
Chrome.letterbox(winW, winH, 1, 1, 1)
|
G.setColor(1, 1, 1, 1)
|
||||||
|
G.rectangle("fill", 0, 0, winW, winH)
|
||||||
local scale = Chrome.fitScale(winW, winH)
|
local scale = Chrome.fitScale(winW, winH)
|
||||||
G.push()
|
G.push()
|
||||||
G.translate(Chrome.fitOrigin(winW, winH, scale))
|
G.translate(Chrome.fitOrigin(winW, winH, scale))
|
||||||
|
|||||||
@@ -309,7 +309,8 @@ end
|
|||||||
|
|
||||||
function CenterPcMenu:drawWidescreen(winW, winH)
|
function CenterPcMenu:drawWidescreen(winW, winH)
|
||||||
local G = love.graphics
|
local G = love.graphics
|
||||||
Chrome.letterbox(winW, winH, 1, 1, 1)
|
G.setColor(1, 1, 1, 1)
|
||||||
|
G.rectangle("fill", 0, 0, winW, winH)
|
||||||
local scale = Chrome.fitScale(winW, winH)
|
local scale = Chrome.fitScale(winW, winH)
|
||||||
G.push()
|
G.push()
|
||||||
G.translate(Chrome.fitOrigin(winW, winH, scale))
|
G.translate(Chrome.fitOrigin(winW, winH, scale))
|
||||||
|
|||||||
+38
-153
@@ -27,39 +27,13 @@ Chrome.SCREEN_H = 18
|
|||||||
-- charmap.asm "¥", the money field's own prefix tile.
|
-- charmap.asm "¥", the money field's own prefix tile.
|
||||||
local YEN = "\xc2\xa5"
|
local YEN = "\xc2\xa5"
|
||||||
|
|
||||||
function Chrome.paletteFill(px, py, pw, ph, palette)
|
function Chrome.clear()
|
||||||
palette = palette or Chrome.DEFAULT_BOX_PALETTE
|
|
||||||
local G = love.graphics
|
local G = love.graphics
|
||||||
G.setColor(1, 1, 1, 1)
|
G.setColor(1, 1, 1, 1)
|
||||||
if GbcPalette.available() then
|
G.rectangle("fill", 0, 0, Chrome.SCREEN_W * 8, Chrome.SCREEN_H * 8)
|
||||||
GbcPalette.with(palette, function() G.rectangle("fill", px, py, pw, ph) end)
|
|
||||||
else
|
|
||||||
G.rectangle("fill", px, py, pw, ph)
|
|
||||||
end
|
|
||||||
G.setColor(0, 0, 0, 1)
|
G.setColor(0, 0, 0, 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Every drawWidescreen paints the window around its 160x144 panel through
|
|
||||||
-- here, so UI LETTERBOX has exactly one place to override. r/g/b is what the
|
|
||||||
-- screen was authored with and is what AUTO keeps.
|
|
||||||
local function letterboxPaper()
|
|
||||||
local c = GbcPalette.resolve(Chrome.DEFAULT_BOX_PALETTE)
|
|
||||||
c = c and c[1]
|
|
||||||
if not c then return nil end
|
|
||||||
return c[1] / 255, c[2] / 255, c[3] / 255
|
|
||||||
end
|
|
||||||
|
|
||||||
function Chrome.letterbox(winW, winH, r, g, b)
|
|
||||||
local Letterbox = require("src.render.Letterbox")
|
|
||||||
local G = love.graphics
|
|
||||||
G.setColor(Letterbox.fill(r or 1, g or 1, b or 1, letterboxPaper))
|
|
||||||
G.rectangle("fill", 0, 0, winW, winH)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Chrome.clear()
|
|
||||||
Chrome.paletteFill(0, 0, Chrome.SCREEN_W * 8, Chrome.SCREEN_H * 8)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- The blit scale every `drawWidescreen` paints its 160x144 panel at.
|
-- The blit scale every `drawWidescreen` paints its 160x144 panel at.
|
||||||
--
|
--
|
||||||
-- One GB pixel has to cover a WHOLE number of window pixels or the 8x8 grid
|
-- One GB pixel has to cover a WHOLE number of window pixels or the 8x8 grid
|
||||||
@@ -103,24 +77,10 @@ function Chrome.positionLift(winW, winH, scale)
|
|||||||
or Chrome.fitScale(winW, winH)), ScreenPosition.safeTop())
|
or Chrome.fitScale(winW, winH)), ScreenPosition.safeTop())
|
||||||
end
|
end
|
||||||
|
|
||||||
Chrome.DEFAULT_BOX_PALETTE = {
|
|
||||||
{ 255, 255, 255 }, { 255, 255, 255 }, { 255, 255, 255 }, { 0, 0, 0 },
|
|
||||||
}
|
|
||||||
|
|
||||||
function Chrome.paletteBox(tx, ty, tw, th, palette)
|
|
||||||
palette = palette or Chrome.DEFAULT_BOX_PALETTE
|
|
||||||
if GbcPalette.available() then
|
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
|
||||||
GbcPalette.with(palette, function() Font.drawBox(tx, ty, tw, th) end)
|
|
||||||
else
|
|
||||||
Font.drawBox(tx, ty, tw, th, palette[1])
|
|
||||||
end
|
|
||||||
love.graphics.setColor(0, 0, 0, 1)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- A bordered box, tile coords. Leaves the draw color black for text.
|
-- A bordered box, tile coords. Leaves the draw color black for text.
|
||||||
function Chrome.box(tx, ty, tw, th)
|
function Chrome.box(tx, ty, tw, th)
|
||||||
Chrome.paletteBox(tx, ty, tw, th)
|
Font.drawBox(tx, ty, tw, th)
|
||||||
|
love.graphics.setColor(0, 0, 0, 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Gold's Textbox helper takes an interior width/height and draws the border
|
-- Gold's Textbox helper takes an interior width/height and draws the border
|
||||||
@@ -130,16 +90,11 @@ function Chrome.textbox(tx, ty, interiorW, interiorH)
|
|||||||
Chrome.box(tx, ty, interiorW + 2, interiorH + 2)
|
Chrome.box(tx, ty, interiorW + 2, interiorH + 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function flatPrint(text, tx, ty)
|
function Chrome.print(text, tx, ty)
|
||||||
love.graphics.setColor(0, 0, 0, 1)
|
love.graphics.setColor(0, 0, 0, 1)
|
||||||
return Font.draw(text, tx * 8, ty * 8)
|
return Font.draw(text, tx * 8, ty * 8)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Chrome.print(text, tx, ty)
|
|
||||||
if not GbcPalette.available() then return flatPrint(text, tx, ty) end
|
|
||||||
return Chrome.printThrough(text, tx, ty, Chrome.DEFAULT_BOX_PALETTE)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Prints a string the way a tilemap screen does: the glyph tiles replace the
|
-- Prints a string the way a tilemap screen does: the glyph tiles replace the
|
||||||
-- cells they land on, and both their ink and their blank pixels go through
|
-- cells they land on, and both their ink and their blank pixels go through
|
||||||
-- that screen's own BG palette.
|
-- that screen's own BG palette.
|
||||||
@@ -169,39 +124,32 @@ function Chrome.throughPalette(palette, invert)
|
|||||||
return GbcPalette.remap(pal, GbcPalette.bgp)
|
return GbcPalette.remap(pal, GbcPalette.bgp)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Chrome.rawPalette(palette, invert)
|
-- `palette` is a 4-colour table of 0-255 triples. `invert` is what
|
||||||
if invert then return { palette[4], palette[3], palette[2], palette[1] } end
|
-- Chrome.printInverted passes; see there.
|
||||||
return palette
|
function Chrome.printThrough(text, tx, ty, palette, invert)
|
||||||
end
|
|
||||||
|
|
||||||
-- The per-glyph shader/tint switch printThrough needs, factored out so a
|
|
||||||
-- caller that positions its own glyphs (TextBox's typewriter/scroll
|
|
||||||
-- animation) gets the same shaded ink without re-deriving the TTF-vs-tile
|
|
||||||
-- split.
|
|
||||||
--
|
|
||||||
-- Returns (pal, drawGlyph, finish):
|
|
||||||
-- pal the resolved ramp, or nil if there's no shader to draw
|
|
||||||
-- through; a caller that also draws a paper rect should skip
|
|
||||||
-- it and take the plain Chrome.print degrade instead.
|
|
||||||
-- drawGlyph (code, x, y): draws one glyph, shaded through `pal` for a
|
|
||||||
-- tile glyph or tinted with pal[4] for a TTF one (see the
|
|
||||||
-- inline comment below, gen1recomp#1642).
|
|
||||||
-- finish call once after the last glyph to restore the previous
|
|
||||||
-- shader and draw colour.
|
|
||||||
function Chrome.paletteGlyphs(palette, invert, raw)
|
|
||||||
if not (palette and GbcPalette.available()) then
|
if not (palette and GbcPalette.available()) then
|
||||||
return nil, Font.drawCode, function() end
|
return Chrome.print(text, tx, ty)
|
||||||
end
|
end
|
||||||
-- `pal` is used below and never touches the caller's palette again --
|
-- `pal` is used below and never touches the caller's palette again --
|
||||||
-- including useRaw for the draw, since useRaw skips the fold GbcPalette.use
|
-- including useRaw for the draw, since useRaw skips the fold GbcPalette.use
|
||||||
-- would otherwise apply and folding it a second time would undo this.
|
-- would otherwise apply and folding it a second time would undo this.
|
||||||
local pal = raw and Chrome.rawPalette(palette, invert)
|
local pal = Chrome.throughPalette(palette, invert)
|
||||||
or Chrome.throughPalette(palette, invert)
|
local width = Font.width(text)
|
||||||
|
local paper = pal[1] or { 255, 255, 255 }
|
||||||
|
love.graphics.setColor(paper[1] / 255, paper[2] / 255, paper[3] / 255, 1)
|
||||||
|
love.graphics.rectangle("fill", tx * 8, ty * 8, width, 8)
|
||||||
|
-- Per glyph, not per string: a TTF-mod build still keeps the multi-byte
|
||||||
|
-- charmap sequences (the naming screen's <PK>/<MN>, the 'd/'l/'s ligatures)
|
||||||
|
-- and anything the mod names in ttf.tiles (Font.lua's own note on keeping
|
||||||
|
-- digits tile-based for column alignment) on their ROM tiles, so a string
|
||||||
|
-- can freely mix the two kinds of glyph.
|
||||||
local ink = pal[4] or { 0, 0, 0 }
|
local ink = pal[4] or { 0, 0, 0 }
|
||||||
local previous = love.graphics.getShader()
|
local previous = love.graphics.getShader()
|
||||||
local shaded = false
|
local shaded = false
|
||||||
local function drawGlyph(code, x, y)
|
local pen = tx * 8
|
||||||
|
for _, code in ipairs(Font.encode(text)) do
|
||||||
if code >= Font.TTF_BASE then
|
if code >= Font.TTF_BASE then
|
||||||
|
-- The shader below recovers a shade from the RED CHANNEL of an already
|
||||||
-- flat-shaded 2bpp tile sheet -- exactly what a TTF glyph is not.
|
-- flat-shaded 2bpp tile sheet -- exactly what a TTF glyph is not.
|
||||||
-- LÖVE's font rasterizer stores glyph coverage as alpha over a plain
|
-- LÖVE's font rasterizer stores glyph coverage as alpha over a plain
|
||||||
-- white texture, which this shader reads back as shade 0 no matter how
|
-- white texture, which this shader reads back as shade 0 no matter how
|
||||||
@@ -219,28 +167,11 @@ function Chrome.paletteGlyphs(palette, invert, raw)
|
|||||||
GbcPalette.useRaw(pal)
|
GbcPalette.useRaw(pal)
|
||||||
shaded = true
|
shaded = true
|
||||||
end
|
end
|
||||||
Font.drawCode(code, x, y)
|
Font.drawCode(code, pen, ty * 8)
|
||||||
end
|
|
||||||
local function finish()
|
|
||||||
if shaded then love.graphics.setShader(previous) end
|
|
||||||
love.graphics.setColor(0, 0, 0, 1)
|
|
||||||
end
|
|
||||||
return pal, drawGlyph, finish
|
|
||||||
end
|
|
||||||
|
|
||||||
function Chrome.printThrough(text, tx, ty, palette, invert, raw)
|
|
||||||
local pal, drawGlyph, finish = Chrome.paletteGlyphs(palette, invert, raw)
|
|
||||||
if not pal then return Chrome.print(text, tx, ty) end
|
|
||||||
local width = Font.width(text)
|
|
||||||
local paper = pal[1] or { 255, 255, 255 }
|
|
||||||
love.graphics.setColor(paper[1] / 255, paper[2] / 255, paper[3] / 255, 1)
|
|
||||||
love.graphics.rectangle("fill", tx * 8, ty * 8, width, 8)
|
|
||||||
local pen = tx * 8
|
|
||||||
for _, code in ipairs(Font.encode(text)) do
|
|
||||||
drawGlyph(code, pen, ty * 8)
|
|
||||||
pen = pen + Font.advanceOf(code)
|
pen = pen + Font.advanceOf(code)
|
||||||
end
|
end
|
||||||
finish()
|
if shaded then love.graphics.setShader(previous) end
|
||||||
|
love.graphics.setColor(0, 0, 0, 1)
|
||||||
return width
|
return width
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -255,34 +186,12 @@ end
|
|||||||
|
|
||||||
-- Right-aligned within a field that ends at tile `txEnd` (exclusive), which is
|
-- Right-aligned within a field that ends at tile `txEnd` (exclusive), which is
|
||||||
-- how Gold prints numbers (PrintNum fills from the right).
|
-- how Gold prints numbers (PrintNum fills from the right).
|
||||||
local function flatPrintRight(text, txEnd, ty)
|
function Chrome.printRight(text, txEnd, ty)
|
||||||
local width = Font.width(text)
|
local width = Font.width(text)
|
||||||
love.graphics.setColor(0, 0, 0, 1)
|
love.graphics.setColor(0, 0, 0, 1)
|
||||||
return Font.draw(text, txEnd * 8 - width, ty * 8)
|
return Font.draw(text, txEnd * 8 - width, ty * 8)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Chrome.printRight(text, txEnd, ty)
|
|
||||||
if not GbcPalette.available() then return flatPrintRight(text, txEnd, ty) end
|
|
||||||
return Chrome.printRightThrough(text, txEnd, ty, Chrome.DEFAULT_BOX_PALETTE)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Chrome.printRightThrough(text, txEnd, ty, palette, invert, raw)
|
|
||||||
local pal, drawGlyph, finish = Chrome.paletteGlyphs(palette, invert, raw)
|
|
||||||
if not pal then return Chrome.printRight(text, txEnd, ty) end
|
|
||||||
local width = Font.width(text)
|
|
||||||
local tx = txEnd * 8 - width
|
|
||||||
local paper = pal[1] or { 255, 255, 255 }
|
|
||||||
love.graphics.setColor(paper[1] / 255, paper[2] / 255, paper[3] / 255, 1)
|
|
||||||
love.graphics.rectangle("fill", tx, ty * 8, width, 8)
|
|
||||||
local pen = tx
|
|
||||||
for _, code in ipairs(Font.encode(text)) do
|
|
||||||
drawGlyph(code, pen, ty * 8)
|
|
||||||
pen = pen + Font.advanceOf(code)
|
|
||||||
end
|
|
||||||
finish()
|
|
||||||
return width
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Wrap text to `width` tiles, measuring with the real font so a proportional
|
-- Wrap text to `width` tiles, measuring with the real font so a proportional
|
||||||
-- page wraps where it actually overflows. Nothing on a 160px screen may print
|
-- page wraps where it actually overflows. Nothing on a 160px screen may print
|
||||||
-- past tile 20: text that does is drawn outside the GB frame entirely.
|
-- past tile 20: text that does is drawn outside the GB frame entirely.
|
||||||
@@ -318,27 +227,21 @@ function Chrome.printWrapped(text, tx, ty, width, rows)
|
|||||||
return #lines
|
return #lines
|
||||||
end
|
end
|
||||||
|
|
||||||
local function flatCursor(tx, ty, hollow)
|
function Chrome.cursor(tx, ty, hollow)
|
||||||
love.graphics.setColor(0, 0, 0, 1)
|
love.graphics.setColor(0, 0, 0, 1)
|
||||||
Font.drawCode(hollow and Chrome.CURSOR_HOLLOW or Chrome.CURSOR, tx * 8, ty * 8)
|
Font.drawCode(hollow and Chrome.CURSOR_HOLLOW or Chrome.CURSOR, tx * 8, ty * 8)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Chrome.cursor(tx, ty, hollow)
|
|
||||||
if not GbcPalette.available() then return flatCursor(tx, ty, hollow) end
|
|
||||||
return Chrome.cursorThrough(tx, ty, Chrome.DEFAULT_BOX_PALETTE, nil, hollow)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- The cursor glyph through a palette, the way Chrome.printThrough draws text.
|
-- The cursor glyph through a palette, the way Chrome.printThrough draws text.
|
||||||
-- A screen whose font page is inverted (the #DEX: Pokedex_LoadInvertedFont
|
-- A screen whose font page is inverted (the #DEX: Pokedex_LoadInvertedFont
|
||||||
-- xors both bitplanes, so it runs white on black) needs its cursor inverted
|
-- xors both bitplanes, so it runs white on black) needs its cursor inverted
|
||||||
-- with everything else -- Chrome.cursor's flat black is invisible against that
|
-- with everything else -- Chrome.cursor's flat black is invisible against that
|
||||||
-- ground, which is what made the dex's action arrow look absent.
|
-- ground, which is what made the dex's action arrow look absent.
|
||||||
function Chrome.cursorThrough(tx, ty, palette, invert, hollow, raw)
|
function Chrome.cursorThrough(tx, ty, palette, invert, hollow)
|
||||||
if not (palette and GbcPalette.available()) then
|
if not (palette and GbcPalette.available()) then
|
||||||
return Chrome.cursor(tx, ty, hollow)
|
return Chrome.cursor(tx, ty, hollow)
|
||||||
end
|
end
|
||||||
local pal = raw and Chrome.rawPalette(palette, invert)
|
local pal = Chrome.throughPalette(palette, invert)
|
||||||
or Chrome.throughPalette(palette, invert)
|
|
||||||
local paper = pal[1] or { 255, 255, 255 }
|
local paper = pal[1] or { 255, 255, 255 }
|
||||||
love.graphics.setColor(paper[1] / 255, paper[2] / 255, paper[3] / 255, 1)
|
love.graphics.setColor(paper[1] / 255, paper[2] / 255, paper[3] / 255, 1)
|
||||||
love.graphics.rectangle("fill", tx * 8, ty * 8, 8, 8)
|
love.graphics.rectangle("fill", tx * 8, ty * 8, 8, 8)
|
||||||
@@ -383,8 +286,8 @@ end
|
|||||||
-- PRINTNUM_LEADINGZEROS.
|
-- PRINTNUM_LEADINGZEROS.
|
||||||
function Chrome.coinBalanceBox(coins)
|
function Chrome.coinBalanceBox(coins)
|
||||||
Chrome.textbox(11, 0, 7, 1)
|
Chrome.textbox(11, 0, 7, 1)
|
||||||
Chrome.printThrough("COIN", 12, 0, Chrome.DEFAULT_BOX_PALETTE)
|
Chrome.print("COIN", 12, 0)
|
||||||
Chrome.printThrough(Chrome.number(coins, 4, true), 13, 1, Chrome.DEFAULT_BOX_PALETTE)
|
Chrome.print(Chrome.number(coins, 4, true), 13, 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- DisplayMoneyAndCoinBalance: one Textbox at (5,0) with a 13x3 interior
|
-- DisplayMoneyAndCoinBalance: one Textbox at (5,0) with a 13x3 interior
|
||||||
@@ -392,10 +295,10 @@ end
|
|||||||
-- "COIN" at (6,3) with the four-digit count at (15,3).
|
-- "COIN" at (6,3) with the four-digit count at (15,3).
|
||||||
function Chrome.moneyAndCoinBalanceBox(money, coins)
|
function Chrome.moneyAndCoinBalanceBox(money, coins)
|
||||||
Chrome.textbox(5, 0, 13, 3)
|
Chrome.textbox(5, 0, 13, 3)
|
||||||
Chrome.printThrough("MONEY", 6, 1, Chrome.DEFAULT_BOX_PALETTE)
|
Chrome.print("MONEY", 6, 1)
|
||||||
Chrome.printThrough(Chrome.money(money), 12, 1, Chrome.DEFAULT_BOX_PALETTE)
|
Chrome.print(Chrome.money(money), 12, 1)
|
||||||
Chrome.printThrough("COIN", 6, 3, Chrome.DEFAULT_BOX_PALETTE)
|
Chrome.print("COIN", 6, 3)
|
||||||
Chrome.printThrough(Chrome.number(coins, 4, true), 15, 3, Chrome.DEFAULT_BOX_PALETTE)
|
Chrome.print(Chrome.number(coins, 4, true), 15, 3)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- PlaceMoneyTopRight: MoneyTopRightMenuHeader is `menu_coords 11, 0,
|
-- PlaceMoneyTopRight: MoneyTopRightMenuHeader is `menu_coords 11, 0,
|
||||||
@@ -403,7 +306,7 @@ end
|
|||||||
-- prints the number at MenuBoxCoord2Tile + SCREEN_WIDTH + 1, i.e. (12,1).
|
-- prints the number at MenuBoxCoord2Tile + SCREEN_WIDTH + 1, i.e. (12,1).
|
||||||
function Chrome.moneyBalanceBox(money)
|
function Chrome.moneyBalanceBox(money)
|
||||||
Chrome.box(11, 0, 9, 3)
|
Chrome.box(11, 0, 9, 3)
|
||||||
Chrome.printThrough(Chrome.money(money), 12, 1, Chrome.DEFAULT_BOX_PALETTE)
|
Chrome.print(Chrome.money(money), 12, 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- A vertical cursor list.
|
-- A vertical cursor list.
|
||||||
@@ -441,7 +344,6 @@ function List.new(opts)
|
|||||||
self.onChoose = opts.onChoose
|
self.onChoose = opts.onChoose
|
||||||
self.onCancel = opts.onCancel
|
self.onCancel = opts.onCancel
|
||||||
self.onMove = opts.onMove
|
self.onMove = opts.onMove
|
||||||
self.palette = opts.palette == nil and Chrome.DEFAULT_BOX_PALETTE or opts.palette
|
|
||||||
self.index = math.max(1, math.min(opts.index or 1, math.max(1, #self.items)))
|
self.index = math.max(1, math.min(opts.index or 1, math.max(1, #self.items)))
|
||||||
self.scroll = 0
|
self.scroll = 0
|
||||||
self:ensureVisible()
|
self:ensureVisible()
|
||||||
@@ -507,33 +409,16 @@ function List:draw()
|
|||||||
local item = self.items[i]
|
local item = self.items[i]
|
||||||
if item then
|
if item then
|
||||||
local ty = self.y + (row - 1) * self.spacing
|
local ty = self.y + (row - 1) * self.spacing
|
||||||
if i == self.index then
|
if i == self.index then Chrome.cursor(self.x - 1, ty) end
|
||||||
if self.palette then
|
Chrome.print(item.label, self.x, ty)
|
||||||
Chrome.cursorThrough(self.x - 1, ty, self.palette)
|
|
||||||
else
|
|
||||||
flatCursor(self.x - 1, ty)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if self.palette then
|
|
||||||
Chrome.printThrough(item.label, self.x, ty, self.palette)
|
|
||||||
else
|
|
||||||
flatPrint(item.label, self.x, ty)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
-- Scrolling lists get the ▼ hint Gold shows when there is more below.
|
-- Scrolling lists get the ▼ hint Gold shows when there is more below.
|
||||||
if self.rows < #self.items and self.scroll + self.rows < #self.items then
|
if self.rows < #self.items and self.scroll + self.rows < #self.items then
|
||||||
if self.palette then
|
|
||||||
local _, drawGlyph, finish = Chrome.paletteGlyphs(self.palette)
|
|
||||||
drawGlyph(Chrome.DOWN_ARROW, (self.x - 1) * 8,
|
|
||||||
(self.y + self.rows * self.spacing - 1) * 8)
|
|
||||||
finish()
|
|
||||||
else
|
|
||||||
love.graphics.setColor(0, 0, 0, 1)
|
love.graphics.setColor(0, 0, 0, 1)
|
||||||
Font.drawCode(Chrome.DOWN_ARROW, (self.x - 1) * 8,
|
Font.drawCode(Chrome.DOWN_ARROW, (self.x - 1) * 8,
|
||||||
(self.y + self.rows * self.spacing - 1) * 8)
|
(self.y + self.rows * self.spacing - 1) * 8)
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return Chrome
|
return Chrome
|
||||||
|
|||||||
@@ -176,7 +176,8 @@ end
|
|||||||
|
|
||||||
function ContestMenu:drawWidescreen(winW, winH)
|
function ContestMenu:drawWidescreen(winW, winH)
|
||||||
local G = love.graphics
|
local G = love.graphics
|
||||||
Chrome.letterbox(winW, winH, 1, 1, 1)
|
G.setColor(1, 1, 1, 1)
|
||||||
|
G.rectangle("fill", 0, 0, winW, winH)
|
||||||
local scale = Chrome.fitScale(winW, winH)
|
local scale = Chrome.fitScale(winW, winH)
|
||||||
G.push()
|
G.push()
|
||||||
G.translate(Chrome.fitOrigin(winW, winH, scale))
|
G.translate(Chrome.fitOrigin(winW, winH, scale))
|
||||||
|
|||||||
@@ -841,7 +841,8 @@ end
|
|||||||
|
|
||||||
function Credits:drawWidescreen(winW, winH)
|
function Credits:drawWidescreen(winW, winH)
|
||||||
local G = love.graphics
|
local G = love.graphics
|
||||||
Chrome.letterbox(winW, winH, 0, 0, 0)
|
G.setColor(0, 0, 0, 1)
|
||||||
|
G.rectangle("fill", 0, 0, winW, winH)
|
||||||
local scale = Chrome.fitScale(winW, winH)
|
local scale = Chrome.fitScale(winW, winH)
|
||||||
G.push()
|
G.push()
|
||||||
G.translate(Chrome.fitOrigin(winW, winH, scale))
|
G.translate(Chrome.fitOrigin(winW, winH, scale))
|
||||||
|
|||||||
@@ -1275,7 +1275,8 @@ end
|
|||||||
function CrystalIntro:drawWidescreen(winW, winH)
|
function CrystalIntro:drawWidescreen(winW, winH)
|
||||||
local G = love.graphics
|
local G = love.graphics
|
||||||
local fill = self:surroundColor()
|
local fill = self:surroundColor()
|
||||||
Chrome.letterbox(winW, winH, fill[1] / 255, fill[2] / 255, fill[3] / 255)
|
G.setColor(fill[1] / 255, fill[2] / 255, fill[3] / 255, 1)
|
||||||
|
G.rectangle("fill", 0, 0, winW, winH)
|
||||||
local scale = Chrome.fitScale(winW, winH)
|
local scale = Chrome.fitScale(winW, winH)
|
||||||
G.push()
|
G.push()
|
||||||
G.translate(Chrome.fitOrigin(winW, winH, scale))
|
G.translate(Chrome.fitOrigin(winW, winH, scale))
|
||||||
|
|||||||
@@ -410,7 +410,8 @@ end
|
|||||||
function CrystalSplash:drawWidescreen(winW, winH)
|
function CrystalSplash:drawWidescreen(winW, winH)
|
||||||
local G = love.graphics
|
local G = love.graphics
|
||||||
local backdrop = GbcPalette.color(self.bgColors, 1) or BLACK
|
local backdrop = GbcPalette.color(self.bgColors, 1) or BLACK
|
||||||
Chrome.letterbox(winW, winH, backdrop[1] / 255, backdrop[2] / 255, backdrop[3] / 255)
|
G.setColor(backdrop[1] / 255, backdrop[2] / 255, backdrop[3] / 255, 1)
|
||||||
|
G.rectangle("fill", 0, 0, winW, winH)
|
||||||
local scale = Chrome.fitScale(winW, winH)
|
local scale = Chrome.fitScale(winW, winH)
|
||||||
G.push()
|
G.push()
|
||||||
G.translate(Chrome.fitOrigin(winW, winH, scale))
|
G.translate(Chrome.fitOrigin(winW, winH, scale))
|
||||||
|
|||||||
@@ -263,7 +263,8 @@ end
|
|||||||
|
|
||||||
function DecorationMenu:drawWidescreen(winW, winH)
|
function DecorationMenu:drawWidescreen(winW, winH)
|
||||||
local G = love.graphics
|
local G = love.graphics
|
||||||
Chrome.letterbox(winW, winH, 1, 1, 1)
|
G.setColor(1, 1, 1, 1)
|
||||||
|
G.rectangle("fill", 0, 0, winW, winH)
|
||||||
local scale = Chrome.fitScale(winW, winH)
|
local scale = Chrome.fitScale(winW, winH)
|
||||||
G.push()
|
G.push()
|
||||||
G.translate(Chrome.fitOrigin(winW, winH, scale))
|
G.translate(Chrome.fitOrigin(winW, winH, scale))
|
||||||
|
|||||||
@@ -180,7 +180,8 @@ function Diploma:drawWidescreen(winW, winH)
|
|||||||
-- Nil palette degrades to the DMG ramp's colour 1, i.e. white, matching the
|
-- Nil palette degrades to the DMG ramp's colour 1, i.e. white, matching the
|
||||||
-- Chrome.clear() the panel falls back to when the cache has no gfx/diploma.
|
-- Chrome.clear() the panel falls back to when the cache has no gfx/diploma.
|
||||||
local paper = GbcPalette.color(self:palette(), 1)
|
local paper = GbcPalette.color(self:palette(), 1)
|
||||||
Chrome.letterbox(winW, winH, paper[1] / 255, paper[2] / 255, paper[3] / 255)
|
G.setColor(paper[1] / 255, paper[2] / 255, paper[3] / 255, 1)
|
||||||
|
G.rectangle("fill", 0, 0, winW, winH)
|
||||||
G.setColor(1, 1, 1, 1)
|
G.setColor(1, 1, 1, 1)
|
||||||
local scale = Chrome.fitScale(winW, winH)
|
local scale = Chrome.fitScale(winW, winH)
|
||||||
local ox, oy = Chrome.fitOrigin(winW, winH, scale)
|
local ox, oy = Chrome.fitOrigin(winW, winH, scale)
|
||||||
|
|||||||
@@ -440,7 +440,8 @@ end
|
|||||||
|
|
||||||
function EggHatchAnim:drawWidescreen(winW, winH)
|
function EggHatchAnim:drawWidescreen(winW, winH)
|
||||||
local G = love.graphics
|
local G = love.graphics
|
||||||
Chrome.letterbox(winW, winH, 1, 1, 1)
|
G.setColor(1, 1, 1, 1)
|
||||||
|
G.rectangle("fill", 0, 0, winW, winH)
|
||||||
local scale = Chrome.fitScale(winW, winH)
|
local scale = Chrome.fitScale(winW, winH)
|
||||||
G.push()
|
G.push()
|
||||||
G.translate(Chrome.fitOrigin(winW, winH, scale))
|
G.translate(Chrome.fitOrigin(winW, winH, scale))
|
||||||
|
|||||||
@@ -567,7 +567,8 @@ end
|
|||||||
|
|
||||||
function EvolutionAnim:drawWidescreen(winW, winH)
|
function EvolutionAnim:drawWidescreen(winW, winH)
|
||||||
local G = love.graphics
|
local G = love.graphics
|
||||||
Chrome.letterbox(winW, winH, 1, 1, 1)
|
G.setColor(1, 1, 1, 1)
|
||||||
|
G.rectangle("fill", 0, 0, winW, winH)
|
||||||
local scale = Chrome.fitScale(winW, winH)
|
local scale = Chrome.fitScale(winW, winH)
|
||||||
G.push()
|
G.push()
|
||||||
G.translate(Chrome.fitOrigin(winW, winH, scale))
|
G.translate(Chrome.fitOrigin(winW, winH, scale))
|
||||||
|
|||||||
@@ -369,7 +369,8 @@ end
|
|||||||
function GameFreakPresents:drawWidescreen(winW, winH)
|
function GameFreakPresents:drawWidescreen(winW, winH)
|
||||||
local G = love.graphics
|
local G = love.graphics
|
||||||
local backdrop = GbcPalette.color(self.bgColors, 1) or BLACK
|
local backdrop = GbcPalette.color(self.bgColors, 1) or BLACK
|
||||||
Chrome.letterbox(winW, winH, backdrop[1] / 255, backdrop[2] / 255, backdrop[3] / 255)
|
G.setColor(backdrop[1] / 255, backdrop[2] / 255, backdrop[3] / 255, 1)
|
||||||
|
G.rectangle("fill", 0, 0, winW, winH)
|
||||||
local scale = Chrome.fitScale(winW, winH)
|
local scale = Chrome.fitScale(winW, winH)
|
||||||
G.push()
|
G.push()
|
||||||
G.translate(Chrome.fitOrigin(winW, winH, scale))
|
G.translate(Chrome.fitOrigin(winW, winH, scale))
|
||||||
|
|||||||
@@ -125,7 +125,8 @@ end
|
|||||||
|
|
||||||
function GenderSelect:drawWidescreen(winW, winH)
|
function GenderSelect:drawWidescreen(winW, winH)
|
||||||
local G = love.graphics
|
local G = love.graphics
|
||||||
Chrome.letterbox(winW, winH, 1, 1, 1)
|
G.setColor(1, 1, 1, 1)
|
||||||
|
G.rectangle("fill", 0, 0, winW, winH)
|
||||||
local scale = Chrome.fitScale(winW, winH)
|
local scale = Chrome.fitScale(winW, winH)
|
||||||
local ox, oy = Chrome.fitOrigin(winW, winH, scale)
|
local ox, oy = Chrome.fitOrigin(winW, winH, scale)
|
||||||
G.push()
|
G.push()
|
||||||
|
|||||||
@@ -1029,7 +1029,8 @@ end
|
|||||||
|
|
||||||
function GoldSilverIntro:drawWidescreen(winW, winH)
|
function GoldSilverIntro:drawWidescreen(winW, winH)
|
||||||
local G = love.graphics
|
local G = love.graphics
|
||||||
Chrome.letterbox(winW, winH, 0, 0, 0)
|
G.setColor(0, 0, 0, 1)
|
||||||
|
G.rectangle("fill", 0, 0, winW, winH)
|
||||||
local scale = Chrome.fitScale(winW, winH)
|
local scale = Chrome.fitScale(winW, winH)
|
||||||
G.push()
|
G.push()
|
||||||
G.translate(Chrome.fitOrigin(winW, winH, scale))
|
G.translate(Chrome.fitOrigin(winW, winH, scale))
|
||||||
|
|||||||
@@ -668,7 +668,8 @@ end
|
|||||||
|
|
||||||
function HallOfFame:drawWidescreen(winW, winH)
|
function HallOfFame:drawWidescreen(winW, winH)
|
||||||
local G = love.graphics
|
local G = love.graphics
|
||||||
Chrome.letterbox(winW, winH, 0, 0, 0)
|
G.setColor(0, 0, 0, 1)
|
||||||
|
G.rectangle("fill", 0, 0, winW, winH)
|
||||||
local scale = Chrome.fitScale(winW, winH)
|
local scale = Chrome.fitScale(winW, winH)
|
||||||
G.push()
|
G.push()
|
||||||
G.translate(Chrome.fitOrigin(winW, winH, scale))
|
G.translate(Chrome.fitOrigin(winW, winH, scale))
|
||||||
|
|||||||
@@ -358,7 +358,8 @@ end
|
|||||||
|
|
||||||
function InitClock:drawWidescreen(winW, winH)
|
function InitClock:drawWidescreen(winW, winH)
|
||||||
local G = love.graphics
|
local G = love.graphics
|
||||||
Chrome.letterbox(winW, winH, 1, 1, 1)
|
G.setColor(1, 1, 1, 1)
|
||||||
|
G.rectangle("fill", 0, 0, winW, winH)
|
||||||
local scale = Chrome.fitScale(winW, winH)
|
local scale = Chrome.fitScale(winW, winH)
|
||||||
local ox, oy = Chrome.fitOrigin(winW, winH, scale)
|
local ox, oy = Chrome.fitOrigin(winW, winH, scale)
|
||||||
G.push()
|
G.push()
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user