Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f7695308b7 | |||
| a8936e79d6 | |||
| f6b29e6caa | |||
| 3069b2e2a9 | |||
| 6625391f76 | |||
| c40dcd6159 | |||
| 35f5c9513c | |||
| 5a0f9ab06d | |||
| 13b58dc51e | |||
| d872010209 | |||
| 6306975352 | |||
| 819150f50f | |||
| bfba1f7bb7 | |||
| 8278b209b4 | |||
| 1c4515eaaa | |||
| c04b93ce1c | |||
| 2c9970a643 | |||
| 352a9a0eb8 | |||
| 5533785364 | |||
| 11f4f5570e | |||
| 6239387643 | |||
| 4a50cb0728 | |||
| 78aa1f3c0a | |||
| 5128b840b1 | |||
| da14b7ee3f | |||
| 2dbe21babf | |||
| 7a85862602 | |||
| 09b12a7cf0 | |||
| e2f2b432ff | |||
| 8c274d969e | |||
| 4c7633b56e | |||
| 943ae23e73 | |||
| 839cf19088 | |||
| 5019ba2caf | |||
| 0b267fc536 | |||
| 290bde1c39 | |||
| 7e92e54669 | |||
| b89b895962 | |||
| 4311955c43 | |||
| 747d9acad1 | |||
| 5041125751 | |||
| 15029d811f | |||
| 2c2a5a4220 | |||
| f64666c6ce | |||
| 3f4aaccbf5 | |||
| 20ad4e6c41 | |||
| 60b3cebdce | |||
| 9468ffccdf | |||
| 948ae8abbd | |||
| cb22684c2a | |||
| 94be169e35 | |||
| 36188ef18a | |||
| 7a1b4e5c45 | |||
| eba6e8d752 | |||
| ec5a72b365 | |||
| 9325d56046 | |||
| d650e605b0 | |||
| 47923d95b3 | |||
| b5a673b252 | |||
| 938bb093e3 |
@@ -0,0 +1,101 @@
|
||||
name: Bug report
|
||||
description: Something in the game is broken, wrong, or not behaving like the original.
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
A screenshot is worth more than any description. If you can grab one, grab one.
|
||||
If you genuinely can't, that's fine, but then the details below need to be thorough
|
||||
enough that someone can find the bug without ever seeing your screen.
|
||||
|
||||
- type: input
|
||||
id: summary
|
||||
attributes:
|
||||
label: One line summary
|
||||
description: What's broken, in a sentence.
|
||||
placeholder: Warp at the south end of the Route 5-8 underground path drops you in the wrong spot
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: os
|
||||
attributes:
|
||||
label: Which build are you running
|
||||
options:
|
||||
- macOS
|
||||
- Windows
|
||||
- Both
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: The release you downloaded. It's in the release title, like 0.1.4.
|
||||
placeholder: 0.1.4
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: location
|
||||
attributes:
|
||||
label: Where in the game
|
||||
description: >
|
||||
Be specific. Route, town, building, floor, menu, battle. If it's a map
|
||||
transition, say which side you came in from.
|
||||
placeholder: Underground path between Route 5 and Route 8, south entrance
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: screenshot
|
||||
attributes:
|
||||
label: Screenshot, or a detailed description if you couldn't take one
|
||||
description: >
|
||||
Drag the image straight into this box. If you couldn't get a screenshot,
|
||||
write "no screenshot" and then describe exactly what was on screen: where
|
||||
the player sprite was, what the tiles around it looked like, any text on
|
||||
screen, whether anything was frozen or flickering. The more concrete, the better.
|
||||
placeholder: |
|
||||
Drop the image here.
|
||||
|
||||
Or, with no screenshot:
|
||||
Screen faded out like a normal warp, then faded back in with the player
|
||||
standing on the grass tile north of the Route 6 gate house instead of
|
||||
inside the tunnel. Player was facing down. No text box. Music kept playing
|
||||
the underground path track instead of switching to the Route 6 track.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: How to make it happen
|
||||
description: Numbered steps, starting from a point someone else can get to.
|
||||
placeholder: |
|
||||
1. Start a new game, get to Cerulean
|
||||
2. Go south to Route 5, enter the underground path building
|
||||
3. Walk to the south end of the tunnel
|
||||
4. Step onto the stairs
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: What should have happened
|
||||
placeholder: Should have come out inside the Route 8 side entrance building, facing up.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: extra
|
||||
attributes:
|
||||
label: Anything else
|
||||
description: >
|
||||
Does it happen every time or only sometimes. Did it start after a specific
|
||||
release. Anything you were doing right before it. Leave blank if nothing comes to mind.
|
||||
validations:
|
||||
required: false
|
||||
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
@@ -0,0 +1,88 @@
|
||||
name: Feature request
|
||||
description: Ask for something new, or a change to how something already works.
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
"Can we add X" on its own is hard to act on. Say what you want, why you want it,
|
||||
and how you picture it working. A request with real detail is one someone can
|
||||
actually build without guessing at what you meant.
|
||||
|
||||
- type: input
|
||||
id: summary
|
||||
attributes:
|
||||
label: One line summary
|
||||
description: What you want, in a sentence.
|
||||
placeholder: Add a run toggle so you can move at bike speed without the bike
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: what
|
||||
attributes:
|
||||
label: What do you want
|
||||
description: >
|
||||
Describe it properly. What is it, where does it live in the game, what does the
|
||||
player see or do. If it changes something that already exists, say what it does today
|
||||
and what it should do instead.
|
||||
placeholder: |
|
||||
A hold-to-run button, the way later Pokemon games do it. Hold B while moving on
|
||||
foot and the player moves at bike speed. Release and you go back to walking.
|
||||
Doesn't work indoors where the bike doesn't work, doesn't work in battles or menus.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: why
|
||||
attributes:
|
||||
label: Why is this worth doing
|
||||
description: >
|
||||
What's annoying or missing right now. What does this fix. If it's just because you
|
||||
think it would be fun, say that, it's a real answer.
|
||||
placeholder: |
|
||||
Backtracking across routes you've already cleared is slow, and the bike is a menu
|
||||
dive away and doesn't work in a lot of places. Running would cut a lot of dead time
|
||||
without making the game easier.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: how
|
||||
attributes:
|
||||
label: How should it work
|
||||
description: >
|
||||
The specifics. Which button, which menu, what happens in the edge cases. If you don't
|
||||
know, say what you'd expect as a player and leave the rest open.
|
||||
placeholder: |
|
||||
- Hold B on the overworld to run
|
||||
- Same speed as the bike
|
||||
- Disabled anywhere the bike is disabled
|
||||
- Sprite should probably need a running animation, not sure what that takes
|
||||
- No effect on encounter rates
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: scope
|
||||
attributes:
|
||||
label: Does this change how the original game plays
|
||||
description: >
|
||||
Some requests are quality of life, some change the actual game. Both are fine,
|
||||
it just helps to know which one you're asking for.
|
||||
options:
|
||||
- Quality of life, original game is untouched
|
||||
- Changes how the game plays
|
||||
- Not sure
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: extra
|
||||
attributes:
|
||||
label: Anything else
|
||||
description: >
|
||||
Reference screenshots, how another game does it, related issues. Leave blank
|
||||
if nothing comes to mind.
|
||||
validations:
|
||||
required: false
|
||||
@@ -0,0 +1,163 @@
|
||||
name: ci
|
||||
|
||||
# The ROM-free test run (21-testing-and-ci §CI).
|
||||
#
|
||||
# CI has no ROM and never will: data/generated/ is produced by a SHA-1
|
||||
# verified import of a cartridge dump, and no ROM bytes are ever committed.
|
||||
# That is why the suite is tiered -- T1 (primitives), T2 (engine invariants)
|
||||
# and T4 (mod SDK) run against the committed tests/fixture_data dataset, so
|
||||
# they need no ROM, no display and no assets beyond what is in the repo.
|
||||
# The T3 content tier asserts Pokemon Red facts; scripts/test.sh detects
|
||||
# data/generated/ is absent and skips it rather than failing.
|
||||
#
|
||||
# Runs alongside release.yml, which is untouched by this file.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
# a force-push while CI is mid-run should cancel the stale run, not queue
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
headless:
|
||||
name: headless suites (no ROM)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# LuaJIT, not lua5.4: LOVE 11.x embeds LuaJIT 2.1 and the engine is
|
||||
# written to Lua 5.1 semantics, so CI must run the interpreter the
|
||||
# game actually ships with or it would green-light 5.4-only syntax.
|
||||
- name: install luajit
|
||||
run: sudo apt-get update && sudo apt-get install -y luajit
|
||||
|
||||
- name: interpreter version
|
||||
run: luajit -v
|
||||
|
||||
- name: run every ROM-free tier
|
||||
run: ./scripts/test.sh
|
||||
|
||||
fixture-dataset:
|
||||
name: fixture dataset integrity
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: sudo apt-get update && sudo apt-get install -y luajit
|
||||
- run: python3 -m pip install --upgrade pillow
|
||||
|
||||
# the fixture PNGs are committed (they are 8x8 placeholders, not
|
||||
# ROM-derived); assert they are still readable 4-shade images rather
|
||||
# than regenerating them, so a corrupted commit is caught
|
||||
- name: fixture assets are valid PNGs
|
||||
run: |
|
||||
python3 - <<'PY'
|
||||
import glob, sys
|
||||
from PIL import Image
|
||||
paths = sorted(glob.glob("tests/fixture_data/assets/*.png"))
|
||||
if not paths:
|
||||
sys.exit("no fixture assets found")
|
||||
for path in paths:
|
||||
with Image.open(path) as image:
|
||||
image.load()
|
||||
print(f"ok {path} {image.size} {image.mode}")
|
||||
print(f"\n{len(paths)} fixture assets valid")
|
||||
PY
|
||||
|
||||
# the fingerprint golden is the parity tripwire; prove it still
|
||||
# matches the dataset on a clean checkout
|
||||
- name: fingerprint gate
|
||||
run: luajit tests/engine/gate_fingerprint.lua
|
||||
|
||||
- name: parity-guarantee meta-test
|
||||
run: luajit tests/engine/gate_meta_coverage.lua
|
||||
|
||||
# Only the differ is under test here, and the job is named for that. The
|
||||
# capture half of the golden pipeline does not exist: a POKEPORT_DRIVER
|
||||
# chunk runs after main.lua has already booted the game, and
|
||||
# src/core/Data.lua has no POKEPORT_DATA_DIR branch, so no LOVE process
|
||||
# can be pointed at tests/fixture_data. There is deliberately no step
|
||||
# here that runs scripts/test.sh with WITH_SHOTS: it would have nothing
|
||||
# to capture and nothing to diff, and a job that green-lights on skipped
|
||||
# work is worse than an absent one.
|
||||
shot-differ:
|
||||
name: screenshot differ (capture not yet wired)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: python3 -m pip install --upgrade pillow
|
||||
|
||||
# 21-testing-and-ci §"Testing & acceptance criteria": compare_shots
|
||||
# flags a deliberately corrupted golden and passes the clean one.
|
||||
# That is the half of the pipeline this repo can actually prove.
|
||||
- name: compare_shots self-test
|
||||
run: |
|
||||
set -e
|
||||
python3 - <<'PY'
|
||||
import os
|
||||
from PIL import Image
|
||||
os.makedirs("/tmp/g", exist_ok=True)
|
||||
os.makedirs("/tmp/s", exist_ok=True)
|
||||
base = Image.new("RGB", (160, 144), (255, 255, 255))
|
||||
for x in range(0, 160, 8):
|
||||
for y in range(0, 144, 8):
|
||||
base.putpixel((x, y), (0, 0, 0))
|
||||
base.save("/tmp/g/clean.png")
|
||||
base.save("/tmp/s/clean.png")
|
||||
base.save("/tmp/g/broken.png")
|
||||
bad = base.copy()
|
||||
for x in range(40, 60):
|
||||
for y in range(40, 60):
|
||||
bad.putpixel((x, y), (255, 0, 0))
|
||||
bad.save("/tmp/s/broken.png")
|
||||
PY
|
||||
if python3 tools/compare_shots.py /tmp/g /tmp/s; then
|
||||
echo "compare_shots passed a corrupted golden -- differ is broken"
|
||||
exit 1
|
||||
fi
|
||||
rm /tmp/g/broken.png /tmp/s/broken.png
|
||||
python3 tools/compare_shots.py /tmp/g /tmp/s
|
||||
|
||||
# an empty golden directory must not read as success
|
||||
- name: differ refuses to pass vacuously
|
||||
run: |
|
||||
set -e
|
||||
mkdir -p /tmp/empty-goldens /tmp/empty-shots
|
||||
if python3 tools/compare_shots.py /tmp/empty-goldens /tmp/empty-shots; then
|
||||
echo "compare_shots passed with no goldens"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
lint:
|
||||
name: mod lint (no ROM-derived content)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# the MK305 dump check key-diffs shipped tables through luajit, and
|
||||
# modkit treats a missing interpreter as a fatal MK100 -- without
|
||||
# this install the gate would fail instead of failing open
|
||||
- run: sudo apt-get update && sudo apt-get install -y luajit
|
||||
- run: python3 -m pip install --upgrade pillow
|
||||
|
||||
# constraint 1, enforced automatically: a committed mod that ships
|
||||
# ROM-derived bytes fails the build. `lint` is the no-ROM-content
|
||||
# check; `validate` is deliberately not run here because it resolves
|
||||
# a mod against the fixture dataset, and a Red-content mod such as
|
||||
# example_mew_starter legitimately does not resolve against it.
|
||||
- name: lint every committed mod
|
||||
run: |
|
||||
set -e
|
||||
found=0
|
||||
for mod in mods/*/; do
|
||||
[ -f "$mod/manifest.json" ] || continue
|
||||
found=1
|
||||
echo "== $mod"
|
||||
python3 tools/modkit.py lint "${mod%/}"
|
||||
done
|
||||
if [ "$found" = "0" ]; then
|
||||
echo "no committed mods to lint"
|
||||
fi
|
||||
@@ -1,7 +1,7 @@
|
||||
name: Release
|
||||
|
||||
# Builds the macOS and Windows desktop apps on the self-hosted Mac runner
|
||||
# and publishes them as a GitHub Release.
|
||||
# Builds the macOS, Windows, and Linux desktop apps plus an Android APK
|
||||
# on the self-hosted Mac runner, and publishes them as a GitHub Release.
|
||||
#
|
||||
# Versioning:
|
||||
# - First ever release is 0.1.0.
|
||||
@@ -14,6 +14,12 @@ name: Release
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
# CI/workflow and docs-only changes don't ship anything to users, so they
|
||||
# don't earn a release. A push touching these *and* real source still
|
||||
# releases; only pushes confined entirely to these paths are skipped.
|
||||
paths-ignore:
|
||||
- '.github/**'
|
||||
- '**.md'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
@@ -23,6 +29,8 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
issues: read
|
||||
pull-requests: read
|
||||
|
||||
concurrency:
|
||||
group: release
|
||||
@@ -144,14 +152,20 @@ jobs:
|
||||
echo "Identities available to codesign:"
|
||||
security find-identity -v -p codesigning "$KEYCHAIN_PATH"
|
||||
|
||||
- name: Build macOS + Windows
|
||||
- name: Build macOS + Windows + Linux
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Sign in-build (identity auto-detected from the temp keychain);
|
||||
# notarize separately below so it uses secret credentials, not a
|
||||
# login-keychain profile.
|
||||
# login-keychain profile. "all" also builds the Linux AppImage,
|
||||
# which needs no signing/notarization.
|
||||
scripts/build.sh all --version "${{ steps.ver.outputs.version }}" --no-notarize
|
||||
|
||||
- name: Build Android
|
||||
run: |
|
||||
set -euo pipefail
|
||||
scripts/build_android.sh --version "${{ steps.ver.outputs.version }}"
|
||||
|
||||
- name: Notarize & staple macOS app
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -163,8 +177,8 @@ jobs:
|
||||
set -a; . "$ci_dir/notary.env"; set +a
|
||||
echo "::add-mask::$APPLE_APP_PASSWORD"
|
||||
|
||||
app=".bazinga/work/PokemonRed.app"
|
||||
zip="dist/mac/PokemonRed-macos.zip"
|
||||
app=".bazinga/work/gen1recomp.app"
|
||||
zip="dist/mac/gen1recomp-macos.zip"
|
||||
[ -d "$app" ] || { echo "::error::signed app not found at $app"; exit 1; }
|
||||
if [ -z "${APPLE_ID:-}" ] || [ -z "${APPLE_APP_PASSWORD:-}" ] || [ -z "${APPLE_TEAM_ID:-}" ]; then
|
||||
echo "::error::notary.env is missing APPLE_ID / APPLE_APP_PASSWORD / APPLE_TEAM_ID."
|
||||
@@ -194,10 +208,27 @@ jobs:
|
||||
outdir="dist/release"
|
||||
rm -rf "$outdir"
|
||||
mkdir -p "$outdir"
|
||||
cp "dist/mac/PokemonRed-macos.zip" "$outdir/PokemonRed-${v}-macos.zip"
|
||||
cp "dist/win/PokemonRed-win64.zip" "$outdir/PokemonRed-${v}-windows.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/linux/gen1recomp-linux.zip" "$outdir/gen1recomp-${v}-linux.zip"
|
||||
apk="$(find dist/android/debug -name '*.apk' | head -1)"
|
||||
[ -n "$apk" ] || { echo "::error::no Android APK found under dist/android/debug"; exit 1; }
|
||||
cp "$apk" "$outdir/gen1recomp-${v}-android.apk"
|
||||
|
||||
# Platform-independent update payload, built alongside the desktop
|
||||
# apps above (same game.love that gets fused into each of them).
|
||||
love_file=".bazinga/work/game.love"
|
||||
[ -f "$love_file" ] || { echo "::error::$love_file not found (expected from scripts/build.sh)"; exit 1; }
|
||||
cp "$love_file" "$outdir/gen1recomp-${v}.love"
|
||||
|
||||
ls -lh "$outdir"
|
||||
|
||||
# Checksums for every staged release asset (sums file itself is
|
||||
# written after this and named outside the gen1recomp-* glob, so it
|
||||
# never lists itself).
|
||||
(cd "$outdir" && shasum -a 256 gen1recomp-* > sha256sums.txt)
|
||||
cat "$outdir/sha256sums.txt"
|
||||
|
||||
- name: Publish GitHub Release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
@@ -206,14 +237,51 @@ jobs:
|
||||
v="${{ steps.ver.outputs.version }}"
|
||||
tag="${{ steps.ver.outputs.tag }}"
|
||||
|
||||
# Issues this release closes, per GitHub's own "closing issues" links
|
||||
# (works for squash, rebase, and merge commits alike). Scans every
|
||||
# commit since the previous tag so a skipped release run doesn't drop
|
||||
# issues on the floor.
|
||||
prev_tag="$(git tag -l 'v*' --sort=-v:refname | grep -v "^${tag}$" | head -1 || true)"
|
||||
range="${prev_tag:+${prev_tag}..}$GITHUB_SHA"
|
||||
|
||||
closed=""
|
||||
for pr in $(git log --pretty=%H "$range" \
|
||||
| xargs -I{} gh api "repos/$GITHUB_REPOSITORY/commits/{}/pulls" \
|
||||
--jq '.[].number' 2>/dev/null \
|
||||
| sort -un || true); do
|
||||
closed+="$(gh api graphql \
|
||||
-f owner="${GITHUB_REPOSITORY%/*}" \
|
||||
-f name="${GITHUB_REPOSITORY#*/}" \
|
||||
-F pr="$pr" \
|
||||
-f query='
|
||||
query($owner:String!, $name:String!, $pr:Int!) {
|
||||
repository(owner:$owner, name:$name) {
|
||||
pullRequest(number:$pr) {
|
||||
closingIssuesReferences(first:50) { nodes { number title } }
|
||||
}
|
||||
}
|
||||
}' \
|
||||
--jq '.data.repository.pullRequest.closingIssuesReferences.nodes[]
|
||||
| "- #\(.number) \(.title)"' 2>/dev/null || true)"$'\n'
|
||||
done
|
||||
closed="$(printf '%s' "$closed" | grep . | sort -t'#' -k2 -n || true)"
|
||||
|
||||
notes="Download the correct version for your computer below."
|
||||
if [ -n "$closed" ]; then
|
||||
notes+=$'\n\n## Issues closed\n\n'"$closed"
|
||||
fi
|
||||
printf 'Release notes:\n%s\n' "$notes"
|
||||
|
||||
gh release create "$tag" \
|
||||
--target "$GITHUB_SHA" \
|
||||
--title "$v" \
|
||||
--notes "$notes" \
|
||||
"dist/release/PokemonRed-${v}-macos.zip" \
|
||||
"dist/release/PokemonRed-${v}-windows.zip"
|
||||
"dist/release/gen1recomp-${v}-macos.zip" \
|
||||
"dist/release/gen1recomp-${v}-windows.zip" \
|
||||
"dist/release/gen1recomp-${v}-linux.zip" \
|
||||
"dist/release/gen1recomp-${v}-android.apk" \
|
||||
"dist/release/gen1recomp-${v}.love" \
|
||||
"dist/release/sha256sums.txt"
|
||||
|
||||
echo "Published release $tag"
|
||||
|
||||
|
||||
@@ -0,0 +1,326 @@
|
||||
# Contributing to the mod platform
|
||||
|
||||
Two routes
|
||||
|
||||
| You are... | Lane | Review bar |
|
||||
|---|---|---|
|
||||
| adding a mod to the gallery, or listing one in the showcase | [Lane A](#route-a--contributing-a-mod) | template + polish checklist + green `modkit validate` |
|
||||
| changing the loader, a registry schema, an event/hook name, or a manifest field | [Lane B](#route-b--contributing-an-engine--mod-api-change) | RFC + backward-compat statement + parity test + generated docs |
|
||||
|
||||
If you are not sure which lane you are in, ask this: **could my change make
|
||||
somebody else's existing mod behave differently?** If yes, it is Lane B.
|
||||
|
||||
---
|
||||
|
||||
## Route A — contributing a mod
|
||||
|
||||
### 1. Scaffold
|
||||
|
||||
```sh
|
||||
python3 tools/modkit.py scaffold my_mod --profile content
|
||||
```
|
||||
|
||||
`--profile` is one of `content`, `overhaul`, `total_conversion`. The
|
||||
scaffold refuses to overwrite an existing directory, and prints the next
|
||||
commands.
|
||||
|
||||
Or copy the gallery entry closest to your intent — that is what the gallery
|
||||
is for:
|
||||
|
||||
| You want to... | Copy |
|
||||
|---|---|
|
||||
| change numbers | `mods/examples/example_balance_tweaks` |
|
||||
| change art | `mods/examples/example_shiny_palette` |
|
||||
| add music or cries | `mods/examples/example_jukebox` |
|
||||
| add a quest, NPC or dialogue | `mods/examples/example_lost_parcel` |
|
||||
| change how battles work | `mods/examples/example_weather` |
|
||||
| add a screen or a tool | `mods/examples/example_dexnav` |
|
||||
| build a whole new game | `mods/examples/example_mini_conversion` |
|
||||
|
||||
### 2. What the PR must contain
|
||||
|
||||
1. **A green `modkit validate`.** CI runs it; so should you.
|
||||
|
||||
```sh
|
||||
python3 tools/modkit.py validate mods/examples/<id> --base imported
|
||||
python3 tools/modkit.py lint mods/examples/<id>
|
||||
```
|
||||
|
||||
`validate` drives the *real* loader headlessly, so a mod that passes
|
||||
here does not surface load errors in game. `--base imported` folds
|
||||
against the full vanilla id space; without it, rules that can only be
|
||||
decided against real Red content (`MK103`, the patch-target check) are
|
||||
reported as skipped rather than guessed at.
|
||||
|
||||
2. **A `tests/` directory** with at least one suite that loads the mod
|
||||
through the headless loader and asserts its *stated effect* — not just
|
||||
that it loaded.
|
||||
|
||||
```lua
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
local T = require("tests.modkit")
|
||||
local Data = require("src.core.Data"); Data:load()
|
||||
local run = T.sdk.loadMod("mods/examples/my_mod", { data = Data })
|
||||
T.eq(#run.errors, 0, "loads clean")
|
||||
T.eq(Data.pokemon.PIKACHU.baseStats.speed, 120, "the patch landed")
|
||||
run.release()
|
||||
T.finish("my_mod")
|
||||
```
|
||||
|
||||
Add a `.modkitignore` listing the suite so it stays out of the
|
||||
distributed package — a test requiring engine modules is a
|
||||
private-require finding against the shipped archive, and `pack` treats
|
||||
warnings as fatal.
|
||||
|
||||
3. **A `README.md`** that opens with one sentence saying what the mod does,
|
||||
names its persona, and gives the three commands to try it. No
|
||||
prerequisites the scaffold did not already create.
|
||||
|
||||
4. **A `mod.card`** meeting the [§3.2 schema](#modcard):
|
||||
|
||||
```lua
|
||||
return {
|
||||
summary = "One sentence, <=100 chars.",
|
||||
author = "Your handle", -- never blank; no author is anonymous by omission
|
||||
tags = { "balance", "beginner" },
|
||||
differences = { changed = {…}, added = {…}, known = {…} },
|
||||
credits = { { who = "…", for_ = "original chiptune arrangement" } },
|
||||
compat = { engine = ">=1.0.0 <2.0.0", modApi = 2 },
|
||||
}
|
||||
```
|
||||
|
||||
5. **A `CHANGELOG.md`** in keep-a-changelog format, with a heading matching
|
||||
`manifest.version`. `validate` warns when the version advanced without
|
||||
one.
|
||||
|
||||
6. **Disabled by default.** Gallery entries live in `mods/examples/`, which
|
||||
the loader's one-level discovery does not walk, so a fresh install
|
||||
discovers none of them and the vanilla game is unchanged.
|
||||
|
||||
7. **No ROM-derived bytes.** Art and audio ship as originals or as a
|
||||
`transforms.lua` operating on the player's own cache. `modkit lint`
|
||||
is the hard floor; see
|
||||
[the legal posture](#legal-posture-non-negotiable).
|
||||
|
||||
CI checks 1, 2, 6 and 7 mechanically. A reviewer checks 3, 4, 5 and the
|
||||
polish checklist.
|
||||
|
||||
### 3. Category
|
||||
|
||||
`manifest.category` is a closed vocabulary. An unknown value is a warning,
|
||||
not a hard error, so the list can grow without breaking old mods.
|
||||
|
||||
| category | Meaning | Typical profile |
|
||||
|---|---|---|
|
||||
| `TWEAK` | Small data edits: stats, prices, learnsets, encounter tables | content |
|
||||
| `BALANCE` | Systematic rebalance across many records or a ruleset | content / overhaul |
|
||||
| `CONTENT` | New species / moves / items / maps / trainers | content |
|
||||
| `QUEST` | New story, NPCs, dialogue, cutscenes | content |
|
||||
| `MECHANIC` | New or changed battle/field mechanics via hooks/effects | overhaul |
|
||||
| `GRAPHICS` | Sprite / tileset / palette / font changes | content |
|
||||
| `AUDIO` | Music, sfx, cries | content |
|
||||
| `UI` | New or modified screens, menus, overlays | content / overhaul |
|
||||
| `TOOL` | Dev/QoL utilities, overlays, inter-mod libraries | content |
|
||||
| `TOTAL_CONVERSION` | Full re-theme; owns its own tri-ledger | total_conversion |
|
||||
| `OTHER` | Fallback | any |
|
||||
|
||||
`GAMEPLAY` is accepted as an alias for `TWEAK`, so `example_mew_starter`
|
||||
keeps validating with the value it has shipped since before the taxonomy
|
||||
existed.
|
||||
|
||||
### 4. `mod.card`
|
||||
|
||||
The manifest is the *engine's* contract: identity, load order, dependencies,
|
||||
permissions, profile. The card is the *human-facing* one: who made this,
|
||||
what it changes, what it does not do yet. It is never read by the loader's
|
||||
merge — only by tooling and the manager's detail pane — so an absent or
|
||||
malformed card can never break a load.
|
||||
|
||||
Two fields deserve their own note:
|
||||
|
||||
- **`differences`** is a self-declared tri-ledger, mirroring the discipline
|
||||
the engine holds itself to. `changed` and `added` let a player see the
|
||||
blast radius before installing; `known` is where you are honest about
|
||||
what is rough. A card with an empty `known` on a complex mod reads as
|
||||
carelessness, not polish.
|
||||
- **`screenshots[].transform`** describes a screenshot by the *driver
|
||||
script* that regenerates it from the player's build, rather than shipping
|
||||
the pixels. That is the legal posture extended to your marketing: a
|
||||
distributed mod never carries ROM-derived bytes, not even in its preview
|
||||
images.
|
||||
|
||||
### 5. Tags
|
||||
|
||||
Lowercase kebab strings, open vocabulary. The showcase generator
|
||||
lowercases and de-dupes. A recommended starting set: `beginner`,
|
||||
`data-only`, `quality-of-life`, `hardcore`, `cosmetic`, `story`, `ruleset`,
|
||||
`audio`, `ui`, `total-conversion`.
|
||||
|
||||
---
|
||||
|
||||
## Route B — contributing an engine / mod-API change
|
||||
|
||||
Changing the loader, a registry schema, an event or hook name, or a manifest
|
||||
field touches the **compatibility surface** the project promises to hold
|
||||
stable. Those PRs carry five obligations.
|
||||
|
||||
### 1. An RFC
|
||||
|
||||
`docs/rfcs/NNNN-<slug>.md`, covering:
|
||||
|
||||
- **Motivation** — the mod that cannot be written today.
|
||||
- **The decision it extends or amends** — name the D-number and the plan
|
||||
file, so the change is traceable to the design it modifies.
|
||||
- **The exact API delta** — new registry names, new schema fields, new
|
||||
event/hook names and their payload shapes and call sites.
|
||||
- **A migration note for existing mods** — what an author has to do, if
|
||||
anything. "Nothing" is a valid and preferred answer.
|
||||
|
||||
### 2. A backward-compatibility statement
|
||||
|
||||
Show that the v1 surface still works: `content.X:register/override/get`,
|
||||
`events:on`, `hooks:wrap`, `mod.log`, `mod:read`, the manifest v1 fields,
|
||||
and `pokemon.before_give`.
|
||||
|
||||
**A change that would break a v1 mod is rejected unless it is
|
||||
additive-with-alias.** `mods/example_mew_starter` is the live proof: it is
|
||||
api 1, uses `category = "GAMEPLAY"`, copies a whole species record because
|
||||
`patch` did not exist yet, and it must keep loading unchanged.
|
||||
|
||||
### 3. A parity-guarantee test
|
||||
|
||||
Two tests, not one:
|
||||
|
||||
- **The no-mod test** — vanilla behavior is unchanged with nothing
|
||||
installed. A new hook with no subscriber must return the vanilla value;
|
||||
a new registry must be a provable no-op when empty; a new event must not
|
||||
allocate its payload when nothing wants it (`Runtime.wants(name)` /
|
||||
`Runtime.wantsHook(name)` guard the hot paths).
|
||||
- **The mod-API test** — the new seam, exercised through the *public* mod
|
||||
API rather than by reaching into internals. If the test has to require a
|
||||
private module to drive your seam, the seam is not finished.
|
||||
|
||||
### 4. Docs with the change
|
||||
|
||||
The reference pages are generated from `src/mods/Schemas.lua`, so a new
|
||||
registry or a new schema field lands with its catalog entry in the same PR
|
||||
and the generator runs clean:
|
||||
|
||||
```sh
|
||||
luajit tools/gen_registry_docs.lua # in-repo default
|
||||
luajit tools/gen_registry_docs.lua ../project.wiki # the wiki checkout
|
||||
```
|
||||
|
||||
The prose reference lives in the GitHub wiki; the generated pages are
|
||||
written into a checkout of it, so they cannot drift from the engine.
|
||||
|
||||
### 5. Deprecation etiquette
|
||||
|
||||
**Nothing is removed.** A superseded seam is marked deprecated in the
|
||||
generated reference with its replacement named, keeps firing and working,
|
||||
and is listed in the deprecations page.
|
||||
|
||||
`pokemon.before_give` is the worked precedent: the `pokemon.give` hook
|
||||
supersedes it, and it is grandfathered forever anyway.
|
||||
|
||||
### Review
|
||||
|
||||
PRs touching `src/mods/`, `src/mods/Schemas.lua`, or the event/hook catalog
|
||||
need the RFC label and a green parity gate before merge.
|
||||
|
||||
---
|
||||
|
||||
## The polish checklist
|
||||
|
||||
Every gallery example and every community mod the guide recommends meets
|
||||
this bar. `[auto]` items are checked by `modkit validate`; `[review]` items
|
||||
by a human.
|
||||
|
||||
### Error messages
|
||||
|
||||
- `[auto]` No bare `error()` or `assert()` in mod callbacks. Every failure
|
||||
path uses `mod.log:warn` / `mod.log:error` — the loader already prefixes
|
||||
`[modid]` — **and names a remediation**:
|
||||
|
||||
```lua
|
||||
-- no
|
||||
local mew = assert(mod.content.pokemon:get("MEW"), "Mew is missing")
|
||||
|
||||
-- yes
|
||||
if not mod.content.pokemon:get("MEW") then
|
||||
mod.log:warn("MEW missing from the merged view -- is a species mod "
|
||||
.. "loaded before this one? speed patch skipped")
|
||||
return
|
||||
end
|
||||
```
|
||||
|
||||
- `[review]` Every registration is validated against its schema, so a typo
|
||||
is a load-time message naming the field, not a nil-index crash three
|
||||
screens later.
|
||||
|
||||
### Empty states
|
||||
|
||||
- `[review]` Every screen a mod adds renders a sentence when its data set
|
||||
is empty — "No songs registered", "Nothing seen yet" — never a blank box.
|
||||
`ListMenu` gives you this for free.
|
||||
|
||||
### First-run experience
|
||||
|
||||
- `[review]` The README opens with one sentence of what the mod does, then
|
||||
the commands to try it.
|
||||
- `[auto]` The mod loads clean on a fresh install — zero `Loader.errors` —
|
||||
with only its declared dependencies.
|
||||
- `[review]` Options have sane defaults, so the mod does something useful
|
||||
before the player opens its options pane.
|
||||
|
||||
### Credits and honoring authors
|
||||
|
||||
- `[review]` `mod.card.credits` names every upstream contribution — art,
|
||||
music arrangement, borrowed code — and what it was for. A mod that ports
|
||||
another community work credits it and links it.
|
||||
- `[auto]` `mod.card.author` (or `authors`) is present and non-empty. The
|
||||
showcase and the manager both surface it, so no author is anonymous by
|
||||
omission.
|
||||
- `[review]` Asset provenance is honest: originals declared original,
|
||||
cache-derived output produced by a declared transform, third-party assets
|
||||
credited and license-compatible.
|
||||
|
||||
### Legal posture (non-negotiable)
|
||||
|
||||
- `[auto]` **No ROM-derived bytes in the packaged mod.** `modkit pack`
|
||||
refuses otherwise, and `pack` runs `validate --strict`, so even warnings
|
||||
block the archive.
|
||||
- `[review]` A total conversion carries the TC legal callout: the Red
|
||||
import still runs and supplies fallback infrastructure, the conversion
|
||||
overrides on top, and it distributes recipes rather than extracted
|
||||
content.
|
||||
|
||||
---
|
||||
|
||||
## Versioning etiquette
|
||||
|
||||
Three version numbers coexist.
|
||||
|
||||
**Engine version** — `src/core/Version.lua`. Major = a breaking change to
|
||||
the mod-facing schemas or API; minor = new backward-compatible seams;
|
||||
patch = bugfix.
|
||||
|
||||
**Mod API version** — the integer `modApi`, currently `2`. Bumped only on a
|
||||
breaking change to the `mod` object surface. A manifest's `api` field pins
|
||||
the surface the mod was written against, so an api-2 mod keeps working when
|
||||
the engine ships api 3.
|
||||
|
||||
**Your mod's version** — the manifest `version`, semver:
|
||||
|
||||
| bump | when |
|
||||
|---|---|
|
||||
| patch | data fixes; no save-shape change, no new content ids |
|
||||
| minor | new content ids, new options with defaults, new optional deps |
|
||||
| major | removed or renamed content ids, a changed `mod.save` shape (needs a `mod.migrations:add(sinceVersion, fn)` entry), or a raised `game_version` floor |
|
||||
|
||||
Declare the engine range you target in `game_version` (a semver range, e.g.
|
||||
`">=1.0.0 <2.0.0"`). The loader checks it on load; a mismatch is a clear,
|
||||
mod-attributed manager error, never a silent partial load.
|
||||
|
||||
Every version change gets a `CHANGELOG.md` heading. `modkit validate` warns
|
||||
when `manifest.version` advanced without one.
|
||||
@@ -0,0 +1,84 @@
|
||||
#!/bin/bash
|
||||
# Pokémon Red (LÖVE2D) - double-click launcher for macOS.
|
||||
#
|
||||
# First run: decodes a user-provided .gb file, installs anything missing,
|
||||
# builds the game data, then launches.
|
||||
# Every later run: launches the game straight away.
|
||||
|
||||
cd "$(dirname "$0")" || exit 1
|
||||
|
||||
say() { printf '\033[1;32m==>\033[0m %s\n' "$*"; }
|
||||
warn() { printf '\033[1;33m !!\033[0m %s\n' "$*"; }
|
||||
err() { printf '\033[1;31merror:\033[0m %s\n' "$*"; }
|
||||
|
||||
pause_exit() { # keep the Terminal window readable after a failure
|
||||
echo
|
||||
read -n 1 -s -r -p "Press any key to close this window..."
|
||||
echo
|
||||
exit "${1:-0}"
|
||||
}
|
||||
|
||||
ask() { # ask "question" -> yes by default
|
||||
local a
|
||||
read -r -p "$1 [Y/n] " a
|
||||
case "$a" in n|N|no|NO) return 1 ;; *) return 0 ;; esac
|
||||
}
|
||||
|
||||
printf '\n \033[1mPokémon Red - LÖVE2D port\033[0m\n\n'
|
||||
|
||||
have_love() {
|
||||
command -v love >/dev/null 2>&1 && return 0
|
||||
[ -x "/Applications/love.app/Contents/MacOS/love" ] && return 0
|
||||
[ -x "$HOME/Applications/love.app/Contents/MacOS/love" ] && return 0
|
||||
return 1
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- fast path
|
||||
if [ -f data/generated/maps.lua ] && have_love; then
|
||||
say "already set up, launching the game"
|
||||
scripts/run.sh || { err "the game failed to start"; pause_exit 1; }
|
||||
exit 0
|
||||
fi
|
||||
|
||||
say "first-time setup"
|
||||
|
||||
# ------------------------------------------------------- Xcode CLT (python3)
|
||||
if ! command -v python3 >/dev/null 2>&1; then
|
||||
warn "python3 is missing; it comes with Apple's command-line tools"
|
||||
if ask "Install Apple's command-line tools now?"; then
|
||||
xcode-select --install 2>/dev/null || true
|
||||
echo
|
||||
warn "Finish the Apple installer window that just opened, then"
|
||||
warn "double-click Play-Mac.command again to continue."
|
||||
pause_exit 0
|
||||
else
|
||||
err "cannot continue without python3"
|
||||
pause_exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# ----------------------------------------------------------------- Homebrew
|
||||
if ! have_love && ! command -v brew >/dev/null 2>&1 \
|
||||
&& [ ! -x /opt/homebrew/bin/brew ] && [ ! -x /usr/local/bin/brew ]; then
|
||||
warn "LÖVE (the game engine) is not installed; the easiest installer is Homebrew"
|
||||
if ask "Install Homebrew now? (asks for your macOS password)"; then
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" \
|
||||
|| { err "Homebrew install failed"; pause_exit 1; }
|
||||
else
|
||||
warn "OK, download LÖVE 11.x yourself from https://love2d.org,"
|
||||
warn "drop love.app into /Applications, then run this again."
|
||||
pause_exit 1
|
||||
fi
|
||||
fi
|
||||
# make brew visible in THIS shell (fresh installs aren't on PATH yet)
|
||||
[ -x /opt/homebrew/bin/brew ] && eval "$(/opt/homebrew/bin/brew shellenv)"
|
||||
[ -x /usr/local/bin/brew ] && eval "$(/usr/local/bin/brew shellenv)"
|
||||
|
||||
# ------------------------------------------------------------------- build
|
||||
echo
|
||||
scripts/setup.sh \
|
||||
|| { err "setup failed - see the messages above"; pause_exit 1; }
|
||||
|
||||
say "setup done, launching the game"
|
||||
scripts/run.sh || { err "the game failed to start"; pause_exit 1; }
|
||||
exit 0
|
||||
@@ -0,0 +1,12 @@
|
||||
@echo off
|
||||
rem Pokemon Red (LOVE2D) - double-click launcher for Windows.
|
||||
rem First run: decodes a user-provided .gb and installs Python/LOVE if needed,
|
||||
rem builds the game data, then launches. Later runs: launches straight away.
|
||||
title Pokemon Red - LOVE2D port
|
||||
cd /d "%~dp0"
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "scripts\bootstrap.ps1"
|
||||
if errorlevel 1 (
|
||||
echo.
|
||||
echo Something went wrong - see the messages above.
|
||||
pause
|
||||
)
|
||||
@@ -1,141 +1,138 @@
|
||||
# Pokemon Gen 1 Recompilation Project
|
||||
|
||||
A native LÖVE2D recreation of Pokemon Red. The engine and map behavior are
|
||||
hand-written Lua; game data and graphics are decoded from a ROM supplied by
|
||||
the player.
|
||||
A native LÖVE2D recreation of Pokemon Red and Blue. The engine and map
|
||||
behavior are hand-written Lua; game data and graphics are decoded from a ROM
|
||||
supplied by the player.
|
||||
|
||||
SUPPORT AND ANNOUNCEMENTS: [Discord](https://bois.icu)
|
||||
|
||||
This project does not include a ROM, emulate the Game Boy, transpile assembly,
|
||||
or download a disassembly. A canonical US Pokemon Red ROM is the only game
|
||||
content input.
|
||||
|
||||
```text
|
||||
first boot
|
||||
Pokemon Red ROM -> in-app Lua importer -> private LÖVE save directory
|
||||
-> generated Lua data and PNGs
|
||||
-> compact audio channel programs
|
||||
-> LÖVE2D engine
|
||||
```
|
||||
or download a disassembly. A canonical US Pokemon Red or Blue ROM is the only
|
||||
game content input.
|
||||
|
||||
The ROM is verified, used during import, and then released from memory. It is
|
||||
not copied into the cache. Later launches load the private generated cache and
|
||||
do not ask for the ROM again.
|
||||
do not ask for the ROM again. Red and Blue can both be imported and played
|
||||
side by side.
|
||||
|
||||
## Packaged App
|
||||
## Quick Start
|
||||
|
||||
Open the desktop app. On first boot, choose your legally obtained `.gb` file
|
||||
or drop it onto the window. Import takes a few seconds and the game starts
|
||||
automatically.
|
||||
|
||||
Only the canonical 1 MiB US Red ROM is accepted. The importer verifies SHA-1
|
||||
`ea9bcae617fdf159b045185467ae58b2e4a48b9a` before creating any game data.
|
||||
The packaged app contains neither a ROM nor pre-extracted game data.
|
||||
Only the canonical 1 MiB US Red and Blue ROMs are accepted. The importer
|
||||
verifies SHA-1 before creating any game data:
|
||||
|
||||
Music, sound effects, and cries are synthesized while the game runs from
|
||||
compact Game Boy audio channel programs copied out of the verified ROM. No
|
||||
WAV or OGG library is bundled or generated.
|
||||
- Red: `ea9bcae617fdf159b045185467ae58b2e4a48b9a`
|
||||
- Blue: `d7037c83e1ae5b39bde3c30787637ba1d4c48ce2`
|
||||
|
||||
## Source Checkout
|
||||
The packaged app contains neither a ROM nor pre-extracted game data. Music,
|
||||
sound effects, and cries are synthesized while the game runs from compact
|
||||
audio channel programs copied out of the verified ROM.
|
||||
|
||||
The source launchers retain an optional Python workflow for developers. Place
|
||||
the ROM in the project folder and double-click `Play-Mac.command` or
|
||||
`Play-Windows.bat`, or run:
|
||||
## Controls
|
||||
|
||||
| Action | Keyboard | Controller |
|
||||
|--------|----------|------------|
|
||||
| Move | Arrow keys / WASD | D-pad / left stick |
|
||||
| A | Z / Enter / Space | A |
|
||||
| B | X / Backspace | B |
|
||||
| Start | Escape | Start |
|
||||
| Select | Tab / Shift | Back / Select |
|
||||
|
||||
Rebind any of these in-game under **OPTIONS → CONTROLS**. Controllers are
|
||||
supported out of the box.
|
||||
|
||||
### Hotkeys
|
||||
|
||||
| Key | What it does |
|
||||
|-----|----------------|
|
||||
| `-` / `=` | Zoom out / in (overworld; also mouse wheel) |
|
||||
| `2` | Cycle COLORS |
|
||||
| `3` | Cycle TILT (free-roam overworld) |
|
||||
| `4` | Cycle ZOOM through every level (free-roam overworld) |
|
||||
| `5` | Cycle GBC FX |
|
||||
| `F1` | Save |
|
||||
| `F2` | Load |
|
||||
| `F10` | Open / close the mod manager |
|
||||
|
||||
COLORS, TILT, ZOOM, GBC FX, and VOID FILL are also in the Options menu
|
||||
and persist in `options.lua`.
|
||||
|
||||
## Running From Source
|
||||
|
||||
Requires LÖVE 11.x. Place a Red or Blue ROM in the project folder and
|
||||
double-click `Play-Mac.command` or `Play-Windows.bat`, or run:
|
||||
|
||||
```sh
|
||||
scripts/setup.sh --rom "/path/to/Pokemon Red.gb"
|
||||
scripts/setup.sh --rom "/path/to/Pokemon Red.gb" # or Pokemon Blue.gb
|
||||
scripts/run.sh
|
||||
```
|
||||
|
||||
Windows PowerShell:
|
||||
then `love .` for later launches. Windows PowerShell scripts, the optional
|
||||
developer data build, test suites, and cache management are covered in
|
||||
[Developer Setup](https://github.com/bryanthaboi/pokemon-gen1-recomp-project/wiki/Guide-Developer-Setup).
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File scripts\setup.ps1 -Rom C:\path\red.gb
|
||||
powershell -ExecutionPolicy Bypass -File scripts\run.ps1
|
||||
```
|
||||
## Portable Mode
|
||||
|
||||
The setup scripts also accept `ROM_PATH`. With no argument, they use the first
|
||||
`.gb` file in the project root.
|
||||
By default the game keeps your save, options, and the private ROM-derived
|
||||
data cache in your OS's normal per-user app data folder. To keep everything
|
||||
next to the game instead (handy for a USB stick or portable drive you carry
|
||||
between computers), drop an empty file named `portable.txt` next to the app
|
||||
(next to `PokemonRed.app`/`.exe`, or next to `main.lua`/`conf.lua` when
|
||||
running from source), then launch the game. Portable mode is desktop-only
|
||||
(Windows, Linux, macOS); it has no effect on Android or iOS, where the app
|
||||
runs from a read-only package.
|
||||
|
||||
## Developer Data Build
|
||||
With `portable.txt` present:
|
||||
|
||||
Requirements: Python 3.10+ and Pillow.
|
||||
- `save.lua`, `save.lua.bak`, and `options.lua` are read from and written to
|
||||
that same folder instead of the OS save directory.
|
||||
- A ROM import writes the generated `data/generated` and `assets/generated`
|
||||
cache straight into that folder too (nothing is left in the OS save
|
||||
directory), so a later launch reuses it without asking for the ROM again
|
||||
even on a different computer, as long as the same folder comes along.
|
||||
- Deleting `portable.txt` switches back to the normal OS save directory; nothing
|
||||
already written to either location is touched automatically, so copy files
|
||||
over yourself if you want to carry existing progress across the switch.
|
||||
|
||||
```sh
|
||||
python3 -m pip install pillow
|
||||
python3 tools/build_data.py --rom "/path/to/Pokemon Red.gb" --clean
|
||||
```
|
||||
## Modding
|
||||
|
||||
This command produces the data modules and 495 PNGs in the source tree for
|
||||
development and parity checks. It is not used by the packaged app.
|
||||
The game ships a native mod platform: content registries, events and hooks,
|
||||
per-mod saves and options, and an in-game manager. The full modding book —
|
||||
getting started, a twelve-rung tutorial ladder, a cookbook, and the generated
|
||||
reference — lives on the
|
||||
[project wiki](https://github.com/bryanthaboi/pokemon-gen1-recomp-project/wiki).
|
||||
|
||||
## Running
|
||||
Shipped example mods, one per kind of author, live in [`mods/`](mods/).
|
||||
|
||||
Requires LÖVE 11.x:
|
||||
## Bugs and Ideas
|
||||
|
||||
```sh
|
||||
love .
|
||||
```
|
||||
Found a bug? A warp dropping you somewhere it shouldn't, a battle doing math
|
||||
that looks wrong, text in the wrong box, anything that does not match the
|
||||
original game.
|
||||
[Open a bug report](https://github.com/bryanthaboi/pokemon-gen1-recomp-project/issues/new?template=bug_report.yml).
|
||||
Attach a screenshot if you can. It saves a lot of back and forth, and if you
|
||||
can't get one, the form asks you to describe what you saw instead.
|
||||
|
||||
Controls: arrow keys or WASD move; Z, Enter, or Space is A; X or Backspace is
|
||||
B; Escape opens START. F1 saves and F2 loads. Controllers are supported.
|
||||
Thought of a feature that could be good, or a way to improve one that already
|
||||
exists?
|
||||
[Open a feature request](https://github.com/bryanthaboi/pokemon-gen1-recomp-project/issues/new?template=feature_request.yml).
|
||||
Say what you want, why it is worth doing, and how you picture it working. A
|
||||
request with real detail is one that can actually get built.
|
||||
|
||||
## Link Play
|
||||
## More
|
||||
|
||||
START > LINK connects two copies directly over UDP. The host chooses HOST A
|
||||
GAME and shares the shown address; the other player chooses JOIN A GAME.
|
||||
The default port is 7777 and can be overridden with `POKEPORT_LINK_PORT`.
|
||||
|
||||
## Save Editor
|
||||
|
||||
Edit party, boxes, items, events, map location, and Pokédex flags without
|
||||
playing through the game. Close the game first, then from the repo root:
|
||||
|
||||
```sh
|
||||
love . --editor
|
||||
# or
|
||||
POKEPORT_EDITOR=1 love .
|
||||
# open a specific save
|
||||
love . --editor --save "/path/to/save.lua"
|
||||
```
|
||||
|
||||
By default it loads the game's LÖVE save (`save.lua`):
|
||||
|
||||
- macOS: `~/Library/Application Support/LOVE/pokemon-love2d/save.lua (or without the LOVE in a built version)`
|
||||
- Linux: `~/.local/share/love/pokemon-love2d/save.lua`
|
||||
- Windows: `%APPDATA%\love\pokemon-love2d\save.lua`
|
||||
|
||||
If that file is missing or you want another copy, use **Open...**, drop a
|
||||
`save.lua` onto the window, or pass `--save`. Each write makes a
|
||||
`save.lua.bak-YYYYMMDD-HHMMSS` backup first.
|
||||
|
||||
See `tools/save-editor/README.md` for headless tests.
|
||||
|
||||
## Layout
|
||||
|
||||
```text
|
||||
tools/ ROM decoder, save editor, and developer verification tools
|
||||
data/generated/ generated Lua game data (gitignored)
|
||||
data/scripts/ hand-ported map behavior
|
||||
assets/generated generated graphics and compact audio cache (gitignored)
|
||||
src/ hand-written LÖVE engine
|
||||
scripts/ setup, run, and packaging helpers
|
||||
mobile/ Android and iOS build trees
|
||||
tests/ headless behavior and parity suites
|
||||
docs/ architecture, behavior notes, and platform docs
|
||||
```
|
||||
|
||||
See `docs/architecture.md` for runtime details and
|
||||
`docs/behavior-porting-notes.md` for formula provenance.
|
||||
|
||||
## Delete generated files (mac)
|
||||
|
||||
```sh
|
||||
rm -rf data/generated assets/generated \
|
||||
"$HOME/Library/Application Support/LOVE/pokemon-love2d/data/generated" \
|
||||
"$HOME/Library/Application Support/LOVE/pokemon-love2d/assets/generated"
|
||||
|
||||
rm -f "$HOME/Library/Application Support/LOVE/pokemon-love2d/rom-cache.complete"
|
||||
|
||||
```
|
||||
- [Link play](https://github.com/bryanthaboi/pokemon-gen1-recomp-project/wiki/Guide-Link-Play)
|
||||
— START > LINK connects two copies directly over UDP.
|
||||
- [Save editor](https://github.com/bryanthaboi/pokemon-gen1-recomp-project/wiki/Guide-Save-Editor)
|
||||
— edit party, boxes, items, events, and Pokédex flags outside the game.
|
||||
- `docs/architecture.md` — runtime details;
|
||||
`docs/behavior-porting-notes.md` — formula provenance.
|
||||
|
||||
## Special Thanks
|
||||
This project would not be possible without [pret](https://github.com/pret) > the pret band of decompiling maniacs > and their [pokered](https://github.com/pret/pokered) disassembly.
|
||||
|
||||
This project would not be possible without [pret](https://github.com/pret) >
|
||||
the pret band of decompiling maniacs > and their
|
||||
[pokered](https://github.com/pret/pokered) disassembly.
|
||||
|
||||
|
After Width: | Height: | Size: 753 KiB |
@@ -0,0 +1,9 @@
|
||||
# On-screen touch control art
|
||||
|
||||
From Xelu's "Free Controller & Keyboard Prompts" pack (Nicolae Berbece /
|
||||
Those Awesome Guys), public domain under CC0:
|
||||
https://thoseawesomeguys.com/prompts/
|
||||
|
||||
256px Nintendo Switch set, renamed for src/core/TouchControls.lua:
|
||||
d-pad (neutral + per-direction highlight), A, B, plus (START) and
|
||||
minus (SELECT).
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 751 B |
|
After Width: | Height: | Size: 1.5 KiB |
@@ -1,13 +1,16 @@
|
||||
function love.conf(t)
|
||||
local editor = os.getenv("POKEPORT_EDITOR") == "1"
|
||||
local developer = os.getenv("POKEPORT_DEV") == "1"
|
||||
if arg then
|
||||
for _, a in ipairs(arg) do
|
||||
if a == "--editor" then editor = true end
|
||||
if a == "--developer" then developer = true end
|
||||
end
|
||||
end
|
||||
-- main.lua runs in the same Lua state right after conf.lua; stash the
|
||||
-- decision in a global so it doesn't need to reparse `arg`.
|
||||
_G.POKEPORT_EDITOR_MODE = editor
|
||||
_G.POKEPORT_DEV_MODE = developer
|
||||
|
||||
if editor then
|
||||
t.identity = os.getenv("POKEPORT_IDENTITY") or "pokemon-love2d-editor"
|
||||
@@ -16,9 +19,17 @@ function love.conf(t)
|
||||
t.window.height = 800
|
||||
else
|
||||
t.identity = os.getenv("POKEPORT_IDENTITY") or "pokemon-love2d"
|
||||
t.window.title = "Pokemon Red (Gen 1 Recompilation Project)"
|
||||
t.window.width = 160 * 4
|
||||
t.window.height = 144 * 4
|
||||
-- Version.lua has zero requires, so it is loadable this early; fall
|
||||
-- back to the plain title if the source is not mounted yet
|
||||
local ok, Version = pcall(require, "src.core.Version")
|
||||
t.window.title = ok and Version.title()
|
||||
or "gen1recomp"
|
||||
-- Open at the launcher's design size (the split-screen ROM selector is
|
||||
-- laid out for 1024x768). The window is resizable and the 160x144 game
|
||||
-- canvas letterboxes into whatever size it ends up, so this only sets the
|
||||
-- starting size, not the game's resolution.
|
||||
t.window.width = 1024
|
||||
t.window.height = 768
|
||||
end
|
||||
t.version = "11.5"
|
||||
t.window.vsync = 1
|
||||
@@ -30,14 +41,30 @@ function love.conf(t)
|
||||
local osName = love._os
|
||||
local mobile = osName == "Android" or osName == "iOS"
|
||||
if mobile then
|
||||
-- On Android/iOS, width/height aspect picks portrait vs landscape
|
||||
-- (fullscreen alone is not enough). Use a tall portrait size; the
|
||||
-- OS then resizes to the real display. highdpi is required for
|
||||
-- Retina iOS (Android always behaves as highdpi).
|
||||
-- resizable is what unlocks orientation. SDL's Android backend, given no
|
||||
-- SDL_HINT_ORIENTATIONS (LÖVE sets none), calls setRequestedOrientation
|
||||
-- at window creation -- FULL_SENSOR when the window is resizable (rotates
|
||||
-- freely to portrait or landscape), otherwise locked to the window's w/h
|
||||
-- aspect. So a non-resizable tall window forced portrait; resizable lets
|
||||
-- the game follow the device. The renderer letterboxes the 160x144
|
||||
-- viewport into whatever size results, and the on-screen touch controls
|
||||
-- re-lay themselves out from the new window size, so both orientations
|
||||
-- just work. iOS follows the Info.plist orientations
|
||||
-- (see mobile/ios/overlays/love-ios.plist, now portrait + landscape).
|
||||
t.window.resizable = true
|
||||
-- Starting size is a tall portrait hint; the OS resizes to the real
|
||||
-- display and rotations resize again. highdpi is required for Retina iOS
|
||||
-- (Android always behaves as highdpi).
|
||||
t.window.width = 1080
|
||||
t.window.height = 1920
|
||||
t.window.fullscreen = true
|
||||
t.window.highdpi = true
|
||||
-- Android only (irrelevant on iOS): puts the save directory under the
|
||||
-- app's external-files folder, which is readable/writable via USB or a
|
||||
-- file manager with no runtime permission, so RomImporter can ask the
|
||||
-- player to copy their ROM there instead of needing a native file
|
||||
-- picker (LOVE 11.5 on Android has none -- see src/import/RomImporter.lua).
|
||||
t.externalstorage = osName == "Android"
|
||||
else
|
||||
t.window.resizable = true
|
||||
end
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
-- Celadon Chief House (scripts/CeladonChiefHouse.asm). The CHIEF is a
|
||||
-- talk-only NPC in the original ROM, and ChiefData (data/trainers/
|
||||
-- parties.asm) is an empty, unreferenced trainer entry. After the HALL
|
||||
-- OF FAME (EVENT_BEAT_CHAMPION_RIVAL) the CHIEF puts up a fight, using a
|
||||
-- reconstructed party for the otherwise-unused OPP_CHIEF class.
|
||||
|
||||
return {
|
||||
talk = {
|
||||
TEXT_CELADONCHIEFHOUSE_CHIEF = {
|
||||
{ "face_player" }, -- 1
|
||||
{ "check_flag", "EVENT_BEAT_CHAMPION_RIVAL" }, -- 2
|
||||
{ "jump_if_false", 12 }, -- 3
|
||||
{ "check_flag", "EVENT_BEAT_CELADON_CHIEF" }, -- 4
|
||||
{ "jump_if_true", 12 }, -- 5
|
||||
{ "show_text", "So you've come to\nshut down my\noperation?\f"
|
||||
.. "TEAM ROCKET's\nCHIEF won't go\ndown so easy!" }, -- 6
|
||||
{ "start_battle", "trainer", "OPP_CHIEF", 1 }, -- 7
|
||||
{ "jump_if_false", "end" }, -- 8
|
||||
{ "set_flag", "EVENT_BEAT_CELADON_CHIEF" }, -- 9
|
||||
{ "show_text", "Gah! Even the\nCHIEF is no match\nfor you!\f"
|
||||
.. "TEAM ROCKET is\nfinished for\ngood!" }, -- 10
|
||||
{ "jump", "end" }, -- 11
|
||||
{ "show_text", "_CeladonChiefHouseChiefText" }, -- 12
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
-- Flavor talk scripts for CopycatsHouse2F (registry id COPYCATS_HOUSE_2F).
|
||||
-- Source: pokered/scripts/CopycatsHouse2F.asm, pokered/text/CopycatsHouse2F.asm
|
||||
--
|
||||
-- TEXT_COPYCATSHOUSE2F_COPYCAT is already ported (with the POKE DOLL /
|
||||
-- TM31 MIMIC trade) in data/scripts/story4.lua, so it is intentionally
|
||||
-- omitted here.
|
||||
-- TEXT_COPYCATSHOUSE2F_COPYCAT (POKE DOLL -> TM31 MIMIC) lives in
|
||||
-- data/scripts/story4.lua.
|
||||
|
||||
return {
|
||||
COPYCATS_HOUSE_2F = {
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
-- Pewter City flavor dialogue (pokered/scripts/PewterCity.asm).
|
||||
-- PewterCity_TextPointers text_asm bodies for the SUPER_NERD1 museum
|
||||
-- guide, SUPER_NERD2 garden nerd, and the leaving-east YOUNGSTER.
|
||||
-- guide and SUPER_NERD2 garden nerd.
|
||||
--
|
||||
-- The escort choreography (SUPER_NERD1 walking the player to the
|
||||
-- museum, YOUNGSTER walking the player to the gym) is scripted NPC
|
||||
-- movement + a wPewterCityCurScript state machine that steers the
|
||||
-- player off-map; that part is already covered on this map by
|
||||
-- story5.lua's onStep gate (walks the player back a step at the
|
||||
-- east exit before EVENT_BEAT_BROCK). Here we only port the real
|
||||
-- YES/NO-branched flavor text these NPCs speak when talked to.
|
||||
-- The YOUNGSTER's gym escort (talk + east-exit onStep) lives in
|
||||
-- story5.lua so the lockstep RLE walk is not overwritten by this
|
||||
-- flavor merge. SUPER_NERD1's museum escort is not ported; only the
|
||||
-- YES/NO-branched flavor text is here.
|
||||
|
||||
local M = {}
|
||||
|
||||
@@ -60,15 +57,6 @@ M.PEWTER_CITY = {
|
||||
end)
|
||||
end,
|
||||
|
||||
-- PewterCityYoungsterText (scripts/PewterCity.asm): the "follow
|
||||
-- me" line the youngster says when the player is stopped from
|
||||
-- leaving Pewter east before beating Brock; the actual gate/step
|
||||
-- block is handled by story5.lua's onStep for this map.
|
||||
TEXT_PEWTERCITY_YOUNGSTER = function(game, ow, npc, done)
|
||||
local t = text(game)
|
||||
push(game, t._PewterCityYoungsterYoureATrainerFollowMeText
|
||||
or "You're a trainer\nright? BROCK's\nlooking for new\nchallengers!\nFollow me!", done)
|
||||
end,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
-- Red's House 2F (pokered data/events/hidden_events.asm +
|
||||
-- engine/events/hidden_objects PrintRedSNESText).
|
||||
--
|
||||
-- The bedroom SNES is a hidden_event at (3, 5) with ANY_FACING that
|
||||
-- prints _RedBedroomSNESText. field.py extracts OpenRedsPC on this map
|
||||
-- but skips PrintRedSNESText, so interaction was a no-op (#135).
|
||||
|
||||
local TextBox = require("src.render.TextBox")
|
||||
|
||||
return {
|
||||
REDS_HOUSE_2F = {
|
||||
-- hidden_events.asm:
|
||||
-- hidden_event 3, 5, PrintRedSNESText, ANY_FACING
|
||||
onInteract = function(game, ow, fx, fy)
|
||||
if fx ~= 3 or fy ~= 5 then return false end
|
||||
local text = game.data.text._RedBedroomSNESText
|
||||
or "{PLAYER} is\nplaying the SNES!"
|
||||
game.stack:push(TextBox.new(game, text))
|
||||
return true
|
||||
end,
|
||||
},
|
||||
}
|
||||
@@ -5,19 +5,23 @@ return {
|
||||
SILPH_CO_9F = {
|
||||
talk = {
|
||||
-- SilphCo9FNurseText (pokered/scripts/SilphCo9F.asm):
|
||||
-- before EVENT_BEAT_SILPH_CO_GIOVANNI: heals the party and shows
|
||||
-- "You look tired..." then "Don't give up!"; after the event, just
|
||||
-- says thanks. Nurse texts are not in data/generated/text.lua, so
|
||||
-- the exact pokered/text/SilphCo9F.asm strings are used as literals.
|
||||
-- before EVENT_BEAT_SILPH_CO_GIOVANNI: heals the party, white fade
|
||||
-- (Delay3 between out/in; no Music_PkmnHealed), then "Don't give
|
||||
-- up!"; after the event, just says thanks. Nurse texts are not in
|
||||
-- data/generated/text.lua, so the exact pokered/text/SilphCo9F.asm
|
||||
-- strings are used as literals.
|
||||
TEXT_SILPHCO9F_NURSE = {
|
||||
{ "face_player" },
|
||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||
{ "jump_if_true", 8 },
|
||||
{ "show_text", "You look tired!\nYou should take a\nquick nap!" },
|
||||
{ "heal_party" },
|
||||
{ "show_text", "Don't give up!" },
|
||||
{ "jump", 9 },
|
||||
{ "show_text", "Thank you so\nmuch!" },
|
||||
{ "face_player" }, -- 1
|
||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" }, -- 2
|
||||
{ "jump_if_true", 11 }, -- 3
|
||||
{ "show_text", "You look tired!\nYou should take a\nquick nap!" }, -- 4
|
||||
{ "heal_party" }, -- 5
|
||||
{ "fade", "out", "white" }, -- 6
|
||||
{ "wait", 3 }, -- 7 Delay3
|
||||
{ "fade", "in", "white" }, -- 8
|
||||
{ "show_text", "Don't give up!" }, -- 9
|
||||
{ "jump", "end" }, -- 10
|
||||
{ "show_text", "Thank you so\nmuch!" }, -- 11
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
-- Viridian City flavor dialogue (pokered/scripts/ViridianCity.asm).
|
||||
-- Ports the text_asm bodies for GAMBLER1, YOUNGSTER2, GIRL and OLD_MAN.
|
||||
-- Ports the text_asm bodies for GAMBLER1, YOUNGSTER2 and GIRL.
|
||||
--
|
||||
-- Not ported here (already handled elsewhere / not talk-reachable):
|
||||
-- * TEXT_VIRIDIANCITY_FISHER (TM42 gift) -- already ported as a
|
||||
-- `gift()` entry in data/scripts/story5.lua's M.VIRIDIAN_CITY.talk.
|
||||
-- * TEXT_VIRIDIANCITY_OLD_MAN_SLEEPY / TEXT_VIRIDIANCITY_GYM_LOCKED --
|
||||
-- these are step-triggered blocking texts (ViridianCityCheckGotPokedexScript /
|
||||
-- ViridianCityCheckGymOpenScript), not npc talk text_asm bodies; the
|
||||
-- gates themselves are already implemented via story5.lua's onStep
|
||||
-- chain (viridianOldManStep / viridianGymLock) for this map.
|
||||
-- * The old man's catch-training minigame trigger (SCRIPT_VIRIDIANCITY_
|
||||
-- OLD_MAN_START_CATCH_TRAINING / battle vs. WEEDLE) is a full
|
||||
-- scripted-battle cutscene outside this task's Commands vocabulary
|
||||
-- (no static_battle-style "battle a scripted old-man WEEDLE" command
|
||||
-- exists); we port the real YES/NO branch text he speaks but the
|
||||
-- "yes" branch here just shows the "I'll show you how" line rather
|
||||
-- than actually starting the minigame, since that machinery isn't
|
||||
-- ported to this map yet.
|
||||
-- * TEXT_VIRIDIANCITY_OLD_MAN (the walking man at (17,5)) and
|
||||
-- TEXT_VIRIDIANCITY_OLD_MAN_SLEEPY (the sleeper at (18,9)) -- both
|
||||
-- live in data/scripts/story.lua, which owns this map's onStep gate
|
||||
-- and can reach the `old_man_demo` command for the real catch
|
||||
-- tutorial. Keep them there: story.lua loads BEFORE this file, so a
|
||||
-- duplicate here would silently win the merge.
|
||||
-- * TEXT_VIRIDIANCITY_GYM_LOCKED -- a step-triggered blocking text
|
||||
-- (ViridianCityCheckGymOpenScript), implemented by story5.lua's
|
||||
-- onStep chain (viridianGymLock -> viridianOldManStep) for this map.
|
||||
|
||||
local M = {}
|
||||
|
||||
@@ -91,24 +87,6 @@ M.VIRIDIAN_CITY = {
|
||||
end
|
||||
end,
|
||||
|
||||
-- ViridianCityOldManText (scripts/ViridianCity.asm): once he's had
|
||||
-- his coffee, he asks (YES/NO) whether you want to learn how to
|
||||
-- catch Pokemon. YES leads into the catch-training minigame
|
||||
-- (SCRIPT_VIRIDIANCITY_OLD_MAN_START_CATCH_TRAINING, not ported --
|
||||
-- see file header); NO just brushes you off ("Time is money...").
|
||||
TEXT_VIRIDIANCITY_OLD_MAN = function(game, ow, npc, done)
|
||||
local t = text(game)
|
||||
ask(game, t._ViridianCityOldManHadMyCoffeeNowText
|
||||
or "Ahh, I've had my\ncoffee now and I\nfeel great!\nSure you can go\nthrough!\nAre you in a\nhurry?", function(yes)
|
||||
if yes then
|
||||
push(game, t._ViridianCityOldManKnowHowToCatchPokemonText
|
||||
or "I see you're using\na POKéDEX.\nWhen you catch a\nPOKéMON, POKéDEX\nis automatically\nupdated.\nWhat? Don't you\nknow how to catch\nPOKéMON?\nI'll show you\nhow to then.", done)
|
||||
else
|
||||
push(game, t._ViridianCityOldManTimeIsMoneyText
|
||||
or "Time is money...\nGo along then.", done)
|
||||
end
|
||||
end)
|
||||
end,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ local files = {
|
||||
"data.scripts.flavor.pokemon_fan_club",
|
||||
"data.scripts.flavor.power_plant",
|
||||
"data.scripts.flavor.reds_house_1f",
|
||||
"data.scripts.flavor.reds_house_2f",
|
||||
"data.scripts.flavor.route11_gate_2f",
|
||||
"data.scripts.flavor.route18_gate_2f",
|
||||
"data.scripts.flavor.route_12_gate_2f",
|
||||
|
||||
@@ -5,18 +5,26 @@
|
||||
-- onBoulderMoved = fn } where a script is a list of { "command", args... }
|
||||
-- rows executed by src/script/ScriptRunner.lua. Every hand-ported script
|
||||
-- cites the pokered source it was ported from.
|
||||
--
|
||||
-- The modules land in src/script/MapScripts.lua as the engine's base
|
||||
-- contribution: attachBase keeps the historical merge (talk tables merge
|
||||
-- per TEXT constant, other hooks are replaced by later files, so
|
||||
-- different files can each add NPCs to the same map), and mod
|
||||
-- contributions from the map_scripts registry compose on top of it.
|
||||
|
||||
local registry = {
|
||||
PALLET_TOWN = require("data.scripts.pallet_town"),
|
||||
OAKS_LAB = require("data.scripts.oaks_lab"),
|
||||
REDS_HOUSE_1F = require("data.scripts.reds_house"),
|
||||
CELADON_MANSION_ROOF_HOUSE = require("data.scripts.celadon_eevee"),
|
||||
}
|
||||
local MapScripts = require("src.script.MapScripts")
|
||||
|
||||
-- story-critical scripts, one table per map. Later files MERGE into
|
||||
-- earlier ones: talk tables merge per TEXT constant, other hooks
|
||||
-- (onEnter, onVictory, ...) are replaced, so different files can each
|
||||
-- add NPCs to the same map.
|
||||
for _, mapEntry in ipairs({
|
||||
{ "PALLET_TOWN", "data.scripts.pallet_town" },
|
||||
{ "OAKS_LAB", "data.scripts.oaks_lab" },
|
||||
{ "REDS_HOUSE_1F", "data.scripts.reds_house" },
|
||||
{ "CELADON_MANSION_ROOF_HOUSE", "data.scripts.celadon_eevee" },
|
||||
}) do
|
||||
MapScripts.attachBase(mapEntry[1], require(mapEntry[2]))
|
||||
end
|
||||
|
||||
-- story-critical scripts, one table per map, in the order the old merge
|
||||
-- loop required them
|
||||
for _, file in ipairs({ "data.scripts.story", "data.scripts.story2",
|
||||
"data.scripts.story3", "data.scripts.story4",
|
||||
"data.scripts.story5", "data.scripts.story6",
|
||||
@@ -24,33 +32,24 @@ for _, file in ipairs({ "data.scripts.story", "data.scripts.story2",
|
||||
"data.scripts.safari", "data.scripts.seafoam",
|
||||
"data.scripts.gyms" }) do
|
||||
for mapId, mod in pairs(require(file)) do
|
||||
local existing = registry[mapId]
|
||||
if not existing then
|
||||
registry[mapId] = mod
|
||||
else
|
||||
for k, v in pairs(mod) do
|
||||
if k == "talk" and existing.talk then
|
||||
for textConst, script in pairs(v) do
|
||||
existing.talk[textConst] = script
|
||||
end
|
||||
else
|
||||
existing[k] = v
|
||||
end
|
||||
end
|
||||
end
|
||||
MapScripts.attachBase(mapId, mod)
|
||||
end
|
||||
end
|
||||
|
||||
local M = {}
|
||||
|
||||
function M.get(mapId)
|
||||
return registry[mapId]
|
||||
return MapScripts.get(mapId)
|
||||
end
|
||||
|
||||
-- script to run when the player talks to an object with this TEXT_ constant
|
||||
function M.talkScript(mapId, textConst)
|
||||
local mod = registry[mapId]
|
||||
return mod and mod.talk and mod.talk[textConst] or nil
|
||||
return MapScripts.talkScript(mapId, textConst)
|
||||
end
|
||||
|
||||
-- attribution for that script's run: the owning mod's source, nil for base
|
||||
function M.talkSource(mapId, textConst)
|
||||
return MapScripts.talkSource(mapId, textConst)
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@@ -6,12 +6,13 @@
|
||||
-- takes it ("I'll take this one, then!") and both balls disappear.
|
||||
-- Source: scripts/OaksLab.asm OaksLabCharmanderPokeBallText /
|
||||
-- OaksLabRivalTakePokeBallScript.
|
||||
-- * Rival (object 1): before starter -> "gramps isn't around"; with
|
||||
-- * Rival (object 1): before starter -> "go ahead and choose" once Oak
|
||||
-- has walked you in, else "gramps isn't around" (#218); with
|
||||
-- starter -> taunt + battle OPP_RIVAL1 with the counter-pick party
|
||||
-- (player Bulbasaur -> rival Charmander etc., parties 1/2/3 =
|
||||
-- Squirtle/Bulbasaur/Charmander in data/trainers/parties.asm);
|
||||
-- afterwards he gloats or sulks and marches out of the lab
|
||||
-- (OaksLabRivalBattleEndScript).
|
||||
-- afterwards HealParty + flag always, then he gloats or sulks and
|
||||
-- marches out (OaksLabRivalEndBattleScript). A loss does not black out.
|
||||
|
||||
-- ball objects: CHARMANDER (6,3), SQUIRTLE (7,3), BULBASAUR (8,3);
|
||||
-- rival = object 1 at (4,3). rivalBallX is the counter-pick's column.
|
||||
@@ -19,72 +20,137 @@ local function starterBall(askText, species, choseFlag, ownBall,
|
||||
rivalBallX, rivalBall)
|
||||
return {
|
||||
{ "check_flag", "EVENT_GOT_STARTER" }, -- 1
|
||||
{ "jump_if_true", 19 }, -- 2
|
||||
{ "jump_if_true", 20 }, -- 2
|
||||
-- no picking until Oak has walked you in (OaksLabScript gating)
|
||||
{ "check_flag", "EVENT_FOLLOWED_OAK_INTO_LAB" }, -- 3
|
||||
{ "jump_if_false", 19 }, -- 4
|
||||
{ "ask", askText }, -- 5
|
||||
{ "jump_if_false", 20 }, -- 6
|
||||
{ "give_pokemon", species, 5 }, -- 7
|
||||
{ "set_flag", "EVENT_GOT_STARTER" }, -- 8
|
||||
{ "set_flag", choseFlag }, -- 9
|
||||
{ "jump_if_false", 20 }, -- 4
|
||||
-- the Pokédex "new species" entry shows before the ask (predef
|
||||
-- StarterDex ahead of OaksLabYouWant...Text). StarterDex temporarily
|
||||
-- sets the owned bits so ShowPokedexData prints height/weight/text;
|
||||
-- forceOwned is that bypass without mutating save.pokedex.owned.
|
||||
{ "push_screen", "DexEntryMenu",
|
||||
{ species = species, forceOwned = true } }, -- 5
|
||||
{ "ask", askText }, -- 6
|
||||
{ "jump_if_false", 21 }, -- 7
|
||||
-- OaksLab.asm prints ReceivedMon then AddPartyMon (AskName lives
|
||||
-- inside give_pokemon). Show the received text first so the
|
||||
-- nickname prompt follows "you got X", matching Gen1.
|
||||
{ "show_text", "_OaksLabReceivedMonText", { RAM = species } }, -- 8
|
||||
{ "give_pokemon", species, 5 }, -- 9
|
||||
{ "set_flag", "EVENT_GOT_STARTER" }, -- 10
|
||||
{ "set_flag", choseFlag }, -- 11
|
||||
-- POKé BALLs are not handed out here in the original -- Oak gives
|
||||
-- them later, at OaksLabOak1Text's .give_poke_balls beat once the
|
||||
-- player has beaten the Route 22 rival (see TEXT_OAKSLAB_OAK1 below)
|
||||
{ "show_text", "_OaksLabReceivedMonText", { RAM = species } }, -- 10
|
||||
{ "hide_object", "OAKS_LAB", ownBall }, -- 11
|
||||
{ "hide_object", "OAKS_LAB", ownBall }, -- 12
|
||||
-- the rival walks to the countering ball (around the furniture)
|
||||
{ "move_npc_to", 1, rivalBallX, 4 }, -- 12
|
||||
{ "face_object", 1, "up" }, -- 13
|
||||
{ "show_text", "_OaksLabRivalIllTakeThisOneText" }, -- 14
|
||||
{ "hide_object", "OAKS_LAB", rivalBall }, -- 15
|
||||
{ "move_npc_to", 1, rivalBallX, 4 }, -- 13
|
||||
{ "face_object", 1, "up" }, -- 14
|
||||
{ "show_text", "_OaksLabRivalIllTakeThisOneText" }, -- 15
|
||||
{ "hide_object", "OAKS_LAB", rivalBall }, -- 16
|
||||
{ "show_text", "_OaksLabRivalReceivedMonText",
|
||||
{ RAM = rivalBall == "OAKSLAB_CHARMANDER_POKE_BALL" and "CHARMANDER"
|
||||
or rivalBall == "OAKSLAB_SQUIRTLE_POKE_BALL" and "SQUIRTLE"
|
||||
or "BULBASAUR" } }, -- 16
|
||||
{ "jump", 20 }, -- 17
|
||||
{ "jump", 20 }, -- 18 (spacer)
|
||||
{ "show_text", "_OaksLabThoseArePokeBallsText" }, -- 19
|
||||
or "BULBASAUR" } }, -- 17
|
||||
{ "jump", 21 }, -- 18
|
||||
{ "jump", 21 }, -- 19 (spacer)
|
||||
{ "show_text", "_OaksLabThoseArePokeBallsText" }, -- 20
|
||||
}
|
||||
end
|
||||
|
||||
return {
|
||||
talk = {
|
||||
-- Oak: accepts the parcel and hands over the Pokédex, then (once the
|
||||
-- player has beaten the Route 22 rival) hands over the real POKé
|
||||
-- BALLs (scripts/OaksLab.asm OaksLabOak1Text, simplified: no rival
|
||||
-- recall / dex-rating / "pokemon can fight" / "around the world"
|
||||
-- branches -- see docs/known-differences.md)
|
||||
-- Oak: OaksLabOak1Text. Parcel delivery kicks SCRIPT_OAKSLAB_RIVAL_
|
||||
-- ARRIVES_AT_OAKS_REQUEST + OaksLabOakGivesPokedexScript (rival walk-
|
||||
-- in, full Pokédex speech, rival exit, Route 22 arm). Dex-rating
|
||||
-- (DisplayDexRating) is still skipped.
|
||||
TEXT_OAKSLAB_OAK1 = {
|
||||
{ "face_player" }, -- 1
|
||||
{ "check_flag", "EVENT_GOT_OAKS_PARCEL" }, -- 2
|
||||
{ "jump_if_false", 12 }, -- 3
|
||||
{ "check_flag", "EVENT_OAK_GOT_PARCEL" }, -- 4
|
||||
{ "jump_if_true", 12 }, -- 5
|
||||
{ "show_text", "_OaksLabOak1DeliverParcelText" }, -- 6
|
||||
{ "take_item", "OAKS_PARCEL", 1 }, -- 7
|
||||
{ "set_flag", "EVENT_OAK_GOT_PARCEL" }, -- 8
|
||||
{ "show_text", "_OaksLabOak1PokemonAroundTheWorldText" }, -- 9
|
||||
{ "set_flag", "EVENT_GOT_POKEDEX" }, -- 10
|
||||
{ "jump", 30 }, -- 11
|
||||
{ "check_flag", "EVENT_GOT_STARTER" }, -- 12
|
||||
{ "jump_if_false", 27 }, -- 13
|
||||
{ "check_item", "POKE_BALL" }, -- 14
|
||||
{ "jump_if_true", 25 }, -- 15
|
||||
{ "check_flag", "EVENT_BEAT_ROUTE22_RIVAL_1ST_BATTLE" }, -- 16
|
||||
{ "jump_if_false", 29 }, -- 17
|
||||
{ "check_flag", "EVENT_GOT_POKEBALLS_FROM_OAK" }, -- 18
|
||||
{ "jump_if_true", 25 }, -- 19
|
||||
{ "set_flag", "EVENT_GOT_POKEBALLS_FROM_OAK" }, -- 20
|
||||
{ "give_item", "POKE_BALL", 5, false }, -- 21
|
||||
{ "show_text", "_OaksLabOak1ReceivedPokeballsText" }, -- 22
|
||||
{ "show_text", "_OaksLabGivePokeballsExplanationText" }, -- 23
|
||||
{ "jump", 30 }, -- 24
|
||||
{ "show_text", "_OaksLabOak1ComeSeeMeSometimesText" }, -- 25
|
||||
{ "jump", 30 }, -- 26
|
||||
{ "show_text", "_OaksLabOak1WhichPokemonDoYouWantText" }, -- 27
|
||||
{ "jump", 30 }, -- 28
|
||||
{ "show_text", "_OaksLabOak1RaiseYourYoungPokemonText" }, -- 29 (30 = end)
|
||||
{ "face_player" },
|
||||
{ "check_item", "POKE_BALL" },
|
||||
{ "jump_if_true", "come_see" },
|
||||
{ "check_flag", "EVENT_BEAT_ROUTE22_RIVAL_1ST_BATTLE" },
|
||||
{ "jump_if_true", "give_balls" },
|
||||
{ "check_flag", "EVENT_GOT_POKEDEX" },
|
||||
{ "jump_if_true", "around_world" },
|
||||
{ "check_flag", "EVENT_BATTLED_RIVAL_IN_OAKS_LAB" },
|
||||
{ "jump_if_false", "pre_lab_battle" },
|
||||
{ "check_item", "OAKS_PARCEL" },
|
||||
{ "jump_if_false", "raise_young" },
|
||||
-- OaksLabOak1Text.got_parcel → RivalArrives + OakGivesPokedex
|
||||
{ "show_text", "_OaksLabOak1DeliverParcelText" },
|
||||
{ "play_sound", "Get_Key_Item" },
|
||||
{ "show_text", "_OaksLabOak1ParcelThanksText" },
|
||||
{ "take_item", "OAKS_PARCEL", 1 },
|
||||
{ "stop_music" },
|
||||
{ "play_music", "Music_MeetRival" },
|
||||
{ "show_text", "_OaksLabRivalGrampsText" },
|
||||
{ "show_object", "OAKS_LAB", "OAKSLAB_RIVAL" },
|
||||
-- OaksLabCalcRivalMovementScript: sprite map (8,11) → cell (4,7)
|
||||
-- when the player stands below Oak (the usual desk talk).
|
||||
{ "place_npc", 1, 4, 7, "up" },
|
||||
{ "move_npc_to", 1, 4, 3 },
|
||||
{ "play_music", "Music_OaksLab" },
|
||||
{ "face_object", 1, "up" },
|
||||
{ "face_object", 5, "down" },
|
||||
{ "show_text", "_OaksLabRivalWhatDidYouCallMeForText" },
|
||||
{ "face_object", 1, "up" },
|
||||
{ "face_object", 5, "down" },
|
||||
{ "show_text", "_OaksLabOakIHaveARequestText" },
|
||||
{ "face_object", 1, "up" },
|
||||
{ "face_object", 5, "down" },
|
||||
{ "show_text", "_OaksLabOakMyInventionPokedexText" },
|
||||
{ "show_text", "_OaksLabOakGotPokedexText" },
|
||||
{ "play_sound", "Get_Key_Item" },
|
||||
{ "hide_object", "OAKS_LAB", "OAKSLAB_POKEDEX1" },
|
||||
{ "hide_object", "OAKS_LAB", "OAKSLAB_POKEDEX2" },
|
||||
{ "face_object", 1, "up" },
|
||||
{ "face_object", 5, "down" },
|
||||
{ "show_text", "_OaksLabOakThatWasMyDreamText" },
|
||||
{ "face_object", 1, "right" },
|
||||
{ "show_text", "_OaksLabRivalLeaveItAllToMeText" },
|
||||
{ "set_flag", "EVENT_GOT_POKEDEX" },
|
||||
{ "set_flag", "EVENT_OAK_GOT_PARCEL" },
|
||||
{ "hide_object", "VIRIDIAN_CITY", "VIRIDIANCITY_OLD_MAN_SLEEPY" },
|
||||
{ "show_object", "VIRIDIAN_CITY", "VIRIDIANCITY_OLD_MAN" },
|
||||
{ "stop_music" },
|
||||
{ "play_music", "Music_MeetRival" },
|
||||
{ "move_npc_to", 1, 4, 7 },
|
||||
{ "hide_object", "OAKS_LAB", "OAKSLAB_RIVAL" },
|
||||
{ "play_music", "Music_OaksLab" },
|
||||
{ "set_flag", "EVENT_1ST_ROUTE22_RIVAL_BATTLE" },
|
||||
{ "clear_flag", "EVENT_2ND_ROUTE22_RIVAL_BATTLE" },
|
||||
{ "set_flag", "EVENT_ROUTE22_RIVAL_WANTS_BATTLE" },
|
||||
{ "show_object", "ROUTE_22", "ROUTE22_RIVAL1" },
|
||||
{ "jump", "end" },
|
||||
|
||||
{ "label", "raise_young" },
|
||||
{ "show_text", "_OaksLabOak1RaiseYourYoungPokemonText" },
|
||||
{ "jump", "end" },
|
||||
|
||||
{ "label", "pre_lab_battle" },
|
||||
{ "check_flag", "EVENT_GOT_STARTER" },
|
||||
{ "jump_if_true", "can_fight" },
|
||||
{ "show_text", "_OaksLabOak1WhichPokemonDoYouWantText" },
|
||||
{ "jump", "end" },
|
||||
{ "label", "can_fight" },
|
||||
{ "show_text", "_OaksLabOak1YourPokemonCanFightText" },
|
||||
{ "jump", "end" },
|
||||
|
||||
{ "label", "around_world" },
|
||||
{ "show_text", "_OaksLabOak1PokemonAroundTheWorldText" },
|
||||
{ "jump", "end" },
|
||||
|
||||
{ "label", "give_balls" },
|
||||
{ "check_flag", "EVENT_GOT_POKEBALLS_FROM_OAK" },
|
||||
{ "jump_if_true", "come_see" },
|
||||
{ "set_flag", "EVENT_GOT_POKEBALLS_FROM_OAK" },
|
||||
{ "give_item", "POKE_BALL", 5, false },
|
||||
{ "show_text", "_OaksLabOak1ReceivedPokeballsText" },
|
||||
{ "show_text", "_OaksLabGivePokeballsExplanationText" },
|
||||
{ "jump", "end" },
|
||||
|
||||
{ "label", "come_see" },
|
||||
{ "show_text", "_OaksLabOak1ComeSeeMeSometimesText" },
|
||||
},
|
||||
|
||||
TEXT_OAKSLAB_CHARMANDER_POKE_BALL =
|
||||
@@ -97,45 +163,76 @@ return {
|
||||
starterBall("_OaksLabYouWantBulbasaurText", "BULBASAUR", "EVENT_CHOSE_BULBASAUR",
|
||||
"OAKSLAB_BULBASAUR_POKE_BALL", 6, "OAKSLAB_CHARMANDER_POKE_BALL"),
|
||||
|
||||
-- Talking to the rival only ever prints a line: the lab battle is a
|
||||
-- coordinate trigger (OaksLabRivalChallengesPlayerScript, wYCoord == 6;
|
||||
-- see onStep below), never a talk action. The handler used to fall
|
||||
-- through from the "looks stronger" taunt straight into start_battle,
|
||||
-- so talking to Blue at the table launched the rival fight before the
|
||||
-- player ever stepped onto the trigger (#219). scripts/OaksLab.asm
|
||||
-- OaksLabText8 branches text only:
|
||||
-- * got starter, not yet battled -> _OaksLabRivalMyPokemonLooksStronger
|
||||
-- * already battled -> _OaksLabRivalFedUpWithWaitingText (a Route 22
|
||||
-- line; normally unreachable since the rival is hidden after the
|
||||
-- lab battle in OaksLabRivalEndBattleScript)
|
||||
-- * no starter yet -> the FOLLOWED_OAK pre-starter fork (#218)
|
||||
TEXT_OAKSLAB_RIVAL = {
|
||||
{ "face_player" }, -- 1
|
||||
{ "check_flag", "EVENT_GOT_STARTER" }, -- 2
|
||||
{ "jump_if_false", 20 }, -- 3
|
||||
{ "check_flag", "EVENT_BATTLED_RIVAL_IN_OAKS_LAB" }, -- 4
|
||||
{ "jump_if_true", 18 }, -- 5
|
||||
{ "show_text", "_OaksLabRivalMyPokemonLooksStrongerText" }, -- 6
|
||||
{ "check_flag", "EVENT_CHOSE_BULBASAUR" }, -- 7
|
||||
{ "jump_if_false", 11 }, -- 8
|
||||
{ "start_battle", "trainer", "OPP_RIVAL1", 3 }, -- 9 Charmander
|
||||
{ "jump", 16 }, -- 10
|
||||
{ "check_flag", "EVENT_CHOSE_SQUIRTLE" }, -- 11
|
||||
{ "jump_if_false", 15 }, -- 12
|
||||
{ "start_battle", "trainer", "OPP_RIVAL1", 2 }, -- 13 Bulbasaur
|
||||
{ "jump", 16 }, -- 14
|
||||
{ "start_battle", "trainer", "OPP_RIVAL1", 1 }, -- 15 Squirtle
|
||||
{ "set_flag", "EVENT_BATTLED_RIVAL_IN_OAKS_LAB" }, -- 16
|
||||
{ "jump", 21 }, -- 17
|
||||
{ "show_text", "_OaksLabRivalFedUpWithWaitingText" }, -- 18
|
||||
{ "jump", 25 }, -- 19
|
||||
{ "show_text", "_OaksLabRivalGrampsIsntAroundText" }, -- 20
|
||||
-- battle aftermath: on a win the rival sulks and marches out
|
||||
-- (OaksLabRivalBattleEnd); on a loss the blackout already warped
|
||||
-- us away, so the script just ends
|
||||
{ "jump_if_false", 25 }, -- 21
|
||||
{ "show_text", "_OaksLabRivalIPickedTheWrongPokemonText" }, -- 22
|
||||
{ "move_npc_to", 1, 4, 11 }, -- 23
|
||||
{ "hide_object", "OAKS_LAB", "OAKSLAB_RIVAL" }, -- 24 (25 = end)
|
||||
{ "face_player" },
|
||||
{ "check_flag", "EVENT_GOT_STARTER" },
|
||||
{ "jump_if_false", "pre_starter" },
|
||||
{ "check_flag", "EVENT_BATTLED_RIVAL_IN_OAKS_LAB" },
|
||||
{ "jump_if_true", "after_battle" },
|
||||
-- has a starter, has not fought yet: just the taunt. The battle is
|
||||
-- the coordinate trigger in onStep, not this talk (#219)
|
||||
{ "show_text", "_OaksLabRivalMyPokemonLooksStrongerText" },
|
||||
{ "jump", "end" },
|
||||
|
||||
{ "label", "after_battle" },
|
||||
{ "show_text", "_OaksLabRivalFedUpWithWaitingText" },
|
||||
{ "jump", "end" },
|
||||
|
||||
-- pre-starter fork (scripts/OaksLab.asm OaksLabText8 rival handler):
|
||||
-- Oak has already escorted you in (three balls on the table) -> he
|
||||
-- waves you on to choose; not yet escorted (very early game) -> the
|
||||
-- "Gramps isn't around" line. #218
|
||||
{ "label", "pre_starter" },
|
||||
{ "check_flag", "EVENT_FOLLOWED_OAK_INTO_LAB" },
|
||||
{ "jump_if_false", "gramps_gone" },
|
||||
{ "show_text", "_OaksLabRivalGoAheadAndChooseText" },
|
||||
{ "jump", "end" },
|
||||
|
||||
{ "label", "gramps_gone" },
|
||||
{ "show_text", "_OaksLabRivalGrampsIsntAroundText" },
|
||||
},
|
||||
},
|
||||
|
||||
-- Saves that got the Pokédex before #106 never wrote objectToggles for
|
||||
-- the table sprites; re-entering the lab applies the same HideObject
|
||||
-- the gift script now does (OaksLab.asm OakGivesPokedex).
|
||||
onEnter = function(game, ow)
|
||||
if not (game.save.flags and game.save.flags.EVENT_GOT_POKEDEX) then
|
||||
return
|
||||
end
|
||||
local Commands = require("src.script.Commands")
|
||||
local ctx = { save = game.save, game = game, overworld = ow }
|
||||
Commands.hide_object(ctx, "OAKS_LAB", "OAKSLAB_POKEDEX1")
|
||||
Commands.hide_object(ctx, "OAKS_LAB", "OAKSLAB_POKEDEX2")
|
||||
end,
|
||||
|
||||
-- Oak stops you leaving without a starter; the rival stops you on
|
||||
-- the way out for the first battle (scripts/OaksLab.asm
|
||||
-- OaksLabScript8 / OaksLabRivalChallenge)
|
||||
onStep = function(game, ow, x, y)
|
||||
local flags = game.save.flags
|
||||
-- Oak blocks the exit mats (4,11)/(5,11) until you take a starter
|
||||
-- Oak stops you leaving without a starter at the bookshelf row (cell
|
||||
-- y == 6): this is the same wYCoord == 6 coordinate script the rival
|
||||
-- challenge just below uses (scripts/OaksLab.asm, both gated on
|
||||
-- EVENT_GOT_STARTER), so Oak halts you level with the shelves, not one
|
||||
-- corridor length later on the exit mat. At y>=6 only the x=4,5
|
||||
-- corridor is walkable and the up-push keeps the player above y=7, so
|
||||
-- this only ever fires at y=6 in the corridor -- matching the rival
|
||||
-- trigger's shape. (#232)
|
||||
if flags.EVENT_FOLLOWED_OAK_INTO_LAB and not flags.EVENT_GOT_STARTER
|
||||
and y == 11 and (x == 4 or x == 5) then
|
||||
and y >= 6 then
|
||||
ow.runner:run({
|
||||
{ "show_text", "_OaksLabOakDontGoAwayYetText" },
|
||||
{ "move_player", "up", 1 },
|
||||
@@ -171,10 +268,21 @@ return {
|
||||
local party = flags.EVENT_CHOSE_BULBASAUR and 3
|
||||
or flags.EVENT_CHOSE_SQUIRTLE and 2 or 1
|
||||
table.insert(rows, { "start_battle", "trainer", "OPP_RIVAL1", party })
|
||||
-- OaksLabRivalEndBattleScript: heal + flag on win or loss; no blackout
|
||||
table.insert(rows, { "heal_party" })
|
||||
table.insert(rows, { "set_flag", "EVENT_BATTLED_RIVAL_IN_OAKS_LAB" })
|
||||
-- a loss blacks out to another map: end the script there
|
||||
table.insert(rows, { "jump_if_false", base + 7 })
|
||||
-- OaksLabRivalEndBattleScript: on WIN, print the "picked the wrong
|
||||
-- POKéMON!" gloat, then BOTH win and loss print the shared exit line
|
||||
-- _OaksLabRivalSmellYouLaterText ("OK! I'll make my POKéMON fight to
|
||||
-- toughen it up!\012<PLAYER>! Gramps! Smell you later!") before Blue
|
||||
-- marches out. A loss skips only the gloat (that taunt was already
|
||||
-- shown in-battle via Rival1WinText), never the exit line (#231). The
|
||||
-- jump_if_false convergence point is the exit line: base+6 indexes the
|
||||
-- SmellYouLater row below, so WIN falls IPicked -> SmellYouLater and
|
||||
-- LOSS jumps straight to SmellYouLater (both then walk-out + hide).
|
||||
table.insert(rows, { "jump_if_false", base + 6 })
|
||||
table.insert(rows, { "show_text", "_OaksLabRivalIPickedTheWrongPokemonText" })
|
||||
table.insert(rows, { "show_text", "_OaksLabRivalSmellYouLaterText" })
|
||||
table.insert(rows, { "move_npc_to", 1, 4, 11 })
|
||||
table.insert(rows, { "hide_object", "OAKS_LAB", "OAKSLAB_RIVAL" })
|
||||
ow.runner:run(rows, { npc = rival })
|
||||
|
||||
@@ -10,15 +10,39 @@
|
||||
-- The girl, fisher and the four signs resolve automatically through the
|
||||
-- extracted text pointers (no script needed).
|
||||
|
||||
-- Once the HALL OF FAME has been reached (EVENT_BEAT_CHAMPION_RIVAL, set
|
||||
-- alongside Commands.record_hall_of_fame) Oak offers the ProfOakData
|
||||
-- battle (data/trainers/parties.asm) -- three teams picked by the
|
||||
-- player's starter, mirroring the rival's type-advantage lineup, that go
|
||||
-- unused in the original ROM.
|
||||
return {
|
||||
talk = {
|
||||
TEXT_PALLETTOWN_OAK = {
|
||||
{ "face_player" }, -- 1
|
||||
{ "check_flag", "EVENT_GOT_STARTER" }, -- 2
|
||||
{ "jump_if_true", 6 }, -- 3
|
||||
{ "show_text", "_PalletTownOakHeyWaitDontGoOutText" },-- 4
|
||||
{ "jump", 7 }, -- 5
|
||||
{ "show_text", "_PalletTownOakItsUnsafeText" }, -- 6
|
||||
{ "face_player" }, -- 1
|
||||
{ "check_flag", "EVENT_BEAT_CHAMPION_RIVAL" }, -- 2
|
||||
{ "jump_if_false", 20 }, -- 3
|
||||
{ "check_flag", "EVENT_BEAT_PROF_OAK" }, -- 4
|
||||
{ "jump_if_true", 20 }, -- 5
|
||||
{ "show_text", "OAK: So you want\nto test your\nskills on me?\f"
|
||||
.. "Very well! Let\nme show you what\na real trainer\ncan do!" }, -- 6
|
||||
{ "check_flag", "EVENT_CHOSE_BULBASAUR" }, -- 7
|
||||
{ "jump_if_false", 11 }, -- 8
|
||||
{ "start_battle", "trainer", "OPP_PROF_OAK", 3 }, -- 9 CHARIZARD
|
||||
{ "jump", 16 }, -- 10
|
||||
{ "check_flag", "EVENT_CHOSE_SQUIRTLE" }, -- 11
|
||||
{ "jump_if_false", 15 }, -- 12
|
||||
{ "start_battle", "trainer", "OPP_PROF_OAK", 2 }, -- 13 VENUSAUR
|
||||
{ "jump", 16 }, -- 14
|
||||
{ "start_battle", "trainer", "OPP_PROF_OAK", 1 }, -- 15 BLASTOISE
|
||||
{ "jump_if_false", "end" }, -- 16
|
||||
{ "set_flag", "EVENT_BEAT_PROF_OAK" }, -- 17
|
||||
{ "show_text", "OAK: Impressive!\nYou truly are a\nPOKéMON MASTER!" }, -- 18
|
||||
{ "jump", "end" }, -- 19
|
||||
{ "check_flag", "EVENT_GOT_STARTER" }, -- 20
|
||||
{ "jump_if_true", 24 }, -- 21
|
||||
{ "show_text", "_PalletTownOakHeyWaitDontGoOutText" }, -- 22
|
||||
{ "jump", "end" }, -- 23
|
||||
{ "show_text", "_PalletTownOakItsUnsafeText" }, -- 24
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
-- Hand-ported from pret/pokered scripts/RedsHouse1F.asm.
|
||||
-- Mom (RedsHouse1FMomText, text_asm) heals the party and shows the
|
||||
-- "you should rest" / "looking great" dialogue. The intro "wake up"
|
||||
-- branch is tied to the unported intro cutscene, so the heal path is used.
|
||||
-- Mom (RedsHouse1FMomText): pre-starter shows the wake-up / Oak tip;
|
||||
-- after EVENT_GOT_STARTER, RedsHouse1FMomHealScript fades to white,
|
||||
-- heals, plays MUSIC_PKMN_HEALED, fades back, then "looking great".
|
||||
|
||||
return {
|
||||
talk = {
|
||||
TEXT_REDSHOUSE1F_MOM = {
|
||||
{ "face_player" },
|
||||
{ "show_text", "_RedsHouse1FMomYouShouldRestText" },
|
||||
{ "heal_party" },
|
||||
{ "show_text", "_RedsHouse1FMomLookingGreatText" },
|
||||
{ "face_player" }, -- 1
|
||||
{ "check_flag", "EVENT_GOT_STARTER" }, -- 2
|
||||
{ "jump_if_true", 6 }, -- 3
|
||||
{ "show_text", "_RedsHouse1FMomWakeUpText" }, -- 4
|
||||
{ "jump", "end" }, -- 5
|
||||
-- RedsHouse1FMomHealScript
|
||||
{ "show_text", "_RedsHouse1FMomYouShouldRestText" }, -- 6
|
||||
{ "fade", "out", "white" }, -- 7 GBFadeOutToWhite
|
||||
{ "heal_party" }, -- 8
|
||||
{ "play_once", "Music_PkmnHealed" }, -- 9 wait + restore map
|
||||
{ "fade", "in", "white" }, -- 10 GBFadeInFromWhite
|
||||
{ "show_text", "_RedsHouse1FMomLookingGreatText" }, -- 11
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -10,6 +10,31 @@ local M = {}
|
||||
-- -------------------------------------------------------------------
|
||||
|
||||
M.VIRIDIAN_MART = {
|
||||
-- scripts/ViridianMart.asm: the parcel hand-off is the map's DEFAULT
|
||||
-- script, not a talk. Entering with a starter and no parcel runs
|
||||
-- ViridianMartDefaultScript -- the clerk calls out, then
|
||||
-- StartSimulatingJoypadStates walks the player to the counter
|
||||
-- (.PlayerMovement: PAD_LEFT 1, PAD_UP 2, door (3,7) -> counter (2,5))
|
||||
-- and ViridianMartOaksParcelScript hands the parcel over. The player
|
||||
-- never presses A. This matters beyond convenience: the parcel gates
|
||||
-- Oak's Pokedex and the old man clearing Route 2, so vanilla guarantees
|
||||
-- it on entry rather than letting you walk out without it.
|
||||
--
|
||||
-- The talk branch below is kept as the fallback for a save that reaches
|
||||
-- the counter without this having fired.
|
||||
onEnter = function(game, ow)
|
||||
local f = game.save.flags
|
||||
if f.EVENT_OAK_GOT_PARCEL or f.EVENT_GOT_OAKS_PARCEL then return end
|
||||
if not f.EVENT_GOT_STARTER then return end
|
||||
ow:queueScript({
|
||||
{ "show_text", "_ViridianMartClerkYouCameFromPalletTownText" },
|
||||
{ "move_player", "left", 1 },
|
||||
{ "move_player", "up", 2 },
|
||||
-- the quest text's last page is "{PLAYER} got\nOAK's PARCEL!"
|
||||
{ "give_item", "OAKS_PARCEL", 1, "_ViridianMartClerkParcelQuestText" },
|
||||
{ "set_flag", "EVENT_GOT_OAKS_PARCEL" },
|
||||
})
|
||||
end,
|
||||
talk = {
|
||||
TEXT_VIRIDIANMART_CLERK = {
|
||||
{ "check_flag", "EVENT_OAK_GOT_PARCEL" }, -- 1
|
||||
@@ -33,42 +58,48 @@ M.VIRIDIAN_MART = {
|
||||
|
||||
M.VIRIDIAN_CITY = {
|
||||
talk = {
|
||||
-- the old man napping on the north path (scripts/ViridianCity.asm)
|
||||
-- after his coffee, the old man offers the catch tutorial: not in a
|
||||
-- hurry -> he demos catching a wild mon (BATTLE_TYPE_OLD_MAN)
|
||||
-- The GAMBLER_ASLEEP at (18,9) (ViridianCityOldManSleepyText): he
|
||||
-- only ever grumbles and shoves you back down -- he never wakes,
|
||||
-- moves or hides. The coffee ask and the catch tutorial belong to
|
||||
-- the *other* old man, the walking SPRITE_GAMBLER at (17,5)
|
||||
-- (TEXT_VIRIDIANCITY_OLD_MAN, below). The two are swapped by the
|
||||
-- Pokédex in data/scripts/oaks_lab.lua, not by talking to either.
|
||||
TEXT_VIRIDIANCITY_OLD_MAN_SLEEPY = {
|
||||
{ "check_flag", "EVENT_OAK_GOT_PARCEL" }, -- 1
|
||||
{ "jump_if_true", 5 }, -- 2
|
||||
{ "show_text", "_ViridianCityOldManSleepyPrivatePropertyText" }, -- 3
|
||||
{ "jump", 14 }, -- 4
|
||||
{ "face_player" }, -- 5
|
||||
{ "ask", "_ViridianCityOldManHadMyCoffeeNowText" }, -- 6
|
||||
{ "jump_if_true", 12 }, -- 7 (in a hurry)
|
||||
{ "show_text", "_ViridianCityOldManKnowHowToCatchPokemonText" }, -- 8
|
||||
{ "show_text", "_ViridianCityOldManYouNeedToWeakenTheTargetText" }, -- 9
|
||||
{ "old_man_demo" }, -- 10
|
||||
{ "jump", 13 }, -- 11
|
||||
{ "show_text", "_ViridianCityOldManTimeIsMoneyText" }, -- 12
|
||||
{ "hide_object", "VIRIDIAN_CITY", "VIRIDIANCITY_OLD_MAN_SLEEPY" }, -- 13
|
||||
{ "show_text", "_ViridianCityOldManSleepyPrivatePropertyText" }, -- 1
|
||||
{ "move_player", "down", 1 }, -- 2
|
||||
},
|
||||
|
||||
-- The walking old man at (17,5), shown once the Pokédex swaps him in
|
||||
-- (ViridianCityOldManText). "Are you in a hurry?" -- YES brushes you
|
||||
-- off, NO leads into the catch tutorial: he explains, demos a catch
|
||||
-- on a wild WEEDLE (BATTLE_TYPE_OLD_MAN), then comments afterwards.
|
||||
-- pokered prints YouNeedToWeakenTheTarget *after* the demo battle
|
||||
-- (ViridianCityOldManEndCatchTrainingScript), not before it.
|
||||
TEXT_VIRIDIANCITY_OLD_MAN = {
|
||||
{ "face_player" }, -- 1
|
||||
{ "ask", "_ViridianCityOldManHadMyCoffeeNowText" }, -- 2
|
||||
{ "jump_if_true", 8 }, -- 3 (yes = in a hurry)
|
||||
{ "show_text", "_ViridianCityOldManKnowHowToCatchPokemonText" }, -- 4
|
||||
{ "old_man_demo" }, -- 5
|
||||
{ "show_text", "_ViridianCityOldManYouNeedToWeakenTheTargetText" },-- 6
|
||||
{ "jump", 9 }, -- 7
|
||||
{ "show_text", "_ViridianCityOldManTimeIsMoneyText" }, -- 8 (9 = end)
|
||||
},
|
||||
},
|
||||
-- the sleeping old man lies across the Route 22 path (18,9): until
|
||||
-- he moves you can't slip past on either side (scripts/ViridianCity
|
||||
-- blocks the whole corridor, not just his tile)
|
||||
-- ViridianCityCheckGotPokedexScript: the north corridor is gated on
|
||||
-- EVENT_GOT_POKEDEX, NOT on the sleeper being hidden, and it triggers
|
||||
-- on exactly one cell -- (19,9), the gap east of the sleeper (18,9)
|
||||
-- and the girl (17,9). With the Pokédex the check returns immediately
|
||||
-- and you simply walk past at x=19.
|
||||
onStep = function(game, ow, x, y)
|
||||
local gone = game.save.objectToggles and game.save.objectToggles.VIRIDIAN_CITY
|
||||
and game.save.objectToggles.VIRIDIAN_CITY.VIRIDIANCITY_OLD_MAN_SLEEPY == false
|
||||
if gone then return false end
|
||||
-- crossing north of his row through the 3-wide gap (x 17-19, y<=8)
|
||||
if y <= 8 and x >= 17 and x <= 19 then
|
||||
local TextBox = require("src.render.TextBox")
|
||||
game.stack:push(TextBox.new(game,
|
||||
game.data.text._ViridianCityOldManSleepyPrivatePropertyText
|
||||
or "You can't go\nthrough here!\fThis is private\nproperty!",
|
||||
function() ow:scriptMove(ow.player, "down", 1) end))
|
||||
return true
|
||||
end
|
||||
return false
|
||||
if game.save.flags and game.save.flags.EVENT_GOT_POKEDEX then return false end
|
||||
if x ~= 19 or y ~= 9 then return false end
|
||||
local TextBox = require("src.render.TextBox")
|
||||
game.stack:push(TextBox.new(game,
|
||||
game.data.text._ViridianCityOldManSleepyPrivatePropertyText
|
||||
or "You can't go\nthrough here!\fThis is private\nproperty!",
|
||||
function() ow:scriptMove(ow.player, "down", 1) end))
|
||||
return true
|
||||
end,
|
||||
}
|
||||
|
||||
@@ -102,26 +133,145 @@ M.BLUES_HOUSE = {
|
||||
|
||||
M.BILLS_HOUSE = {
|
||||
talk = {
|
||||
TEXT_BILLSHOUSE_BILL_POKEMON = {
|
||||
{ "check_flag", "EVENT_GOT_SS_TICKET" }, -- 1
|
||||
{ "jump_if_true", 11 }, -- 2
|
||||
{ "show_text", "_BillsHouseBillImNotAPokemonText" }, -- 3
|
||||
{ "show_text", "_BillsHouseBillNoYouGottaHelpText" }, -- 4
|
||||
-- the cell-separator PC throws its switch
|
||||
-- (bills_house_pc.asm BillsHouseInitiatedText: SFX_SWITCH)
|
||||
{ "play_sound", "Switch" }, -- 5
|
||||
{ "show_text", "_BillsHouseBillThankYouText" }, -- 6
|
||||
-- BillsHouseBillPokemonText: "I'm not a POKéMON!", a YES/NO choice
|
||||
-- (NO only adds "No, you gotta help!" before rejoining the YES
|
||||
-- path), then the "get in the TELEPORTER" line and the monster
|
||||
-- walking into the cell-separator machine
|
||||
-- (BillsHousePokemonWalkToMachineScript: up 3, or around the player
|
||||
-- when they stand in the way facing down), where it is hidden and
|
||||
-- EVENT_BILL_SAID_USE_CELL_SEPARATOR arms the PC at (1,4) -- see
|
||||
-- OverworldState.billsHousePC for the separator itself.
|
||||
TEXT_BILLSHOUSE_BILL_POKEMON = function(game, ow, npc, done)
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local t = game.data.text
|
||||
local function toMachine()
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._BillsHouseBillUseSeparationSystemText
|
||||
or "When I'm in the\nTELEPORTER, run\nthe Cell\nSeparation System!", function()
|
||||
local function entered()
|
||||
local Commands = require("src.script.Commands")
|
||||
Commands.hide_object({ game = game, save = game.save,
|
||||
overworld = ow },
|
||||
"BILLS_HOUSE", "BILLSHOUSE_BILL_POKEMON")
|
||||
game.save.flags.EVENT_BILL_SAID_USE_CELL_SEPARATOR = true
|
||||
done()
|
||||
end
|
||||
if ow.player.facing == "down" then
|
||||
-- the player is standing on his straight path: walk around
|
||||
-- (.PokemonWalkAroundPlayerMovement)
|
||||
ow:scriptMove(npc, "right", 1, function()
|
||||
ow:scriptMove(npc, "up", 2, function()
|
||||
ow:scriptMove(npc, "left", 1, function()
|
||||
ow:scriptMove(npc, "up", 1, entered)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
else
|
||||
ow:scriptMove(npc, "up", 3, entered)
|
||||
end
|
||||
end))
|
||||
end
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._BillsHouseBillImNotAPokemonText or "Hey! I'm not a\nPOKéMON!",
|
||||
nil, { choice = function(yes)
|
||||
if yes then
|
||||
toMachine()
|
||||
else
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._BillsHouseBillNoYouGottaHelpText
|
||||
or "No! You gotta\nhelp me!", toMachine))
|
||||
end
|
||||
end }))
|
||||
end,
|
||||
|
||||
-- BillsHouseBillSSTicketText (human Bill after the separation)
|
||||
TEXT_BILLSHOUSE_BILL_SS_TICKET = {
|
||||
{ "face_player" }, -- 1
|
||||
{ "check_flag", "EVENT_GOT_SS_TICKET" }, -- 2
|
||||
{ "jump_if_true", 12 }, -- 3
|
||||
{ "show_text", "_BillsHouseBillThankYouText" }, -- 4
|
||||
-- pokered gives first (GiveItem fills wStringBuffer), then prints
|
||||
-- the received text that reads it (scripts/BillsHouse.asm; the
|
||||
-- item id is S_S_TICKET in generated items.lua -- keyItem, so the
|
||||
-- sound_get_key_item jingle plays like BillsHouse.asm:196)
|
||||
{ "give_item", "S_S_TICKET", 1, false }, -- 7
|
||||
{ "show_text", "_SSTicketReceivedText" }, -- 8
|
||||
{ "set_flag", "EVENT_GOT_SS_TICKET" }, -- 9
|
||||
{ "jump", 12 }, -- 10
|
||||
{ "show_text", "_BillsHouseBillCheckOutMyRarePokemonText" }, -- 11
|
||||
{ "give_item", "S_S_TICKET", 1, false }, -- 5
|
||||
{ "show_text", "_SSTicketReceivedText" }, -- 6
|
||||
{ "set_flag", "EVENT_GOT_SS_TICKET" }, -- 7
|
||||
-- The two Cerulean guards are a SWAP PAIR, not scenery
|
||||
-- (BillsHouse.asm:174-178): handing over the ticket shows GUARD1 at
|
||||
-- (28,12) and hides GUARD2 at (27,12). This matters far more than it
|
||||
-- looks: (27,12) is the ONLY walkable neighbour of the trashed
|
||||
-- house's south door at (27,11), and that house is one of the two
|
||||
-- ways through the fence that splits Cerulean in half (the badge
|
||||
-- house is the other). Leaving GUARD2 up forever severs the city --
|
||||
-- the gym/mart half can never reach the Route 5 exit.
|
||||
-- Same swap fires after the TM28 Rocket (CeruleanCity_2.asm
|
||||
-- CeruleanHideRocket), so either route opens the path.
|
||||
{ "show_object", "CERULEAN_CITY", "CERULEANCITY_GUARD1" }, -- 8
|
||||
{ "hide_object", "CERULEAN_CITY", "CERULEANCITY_GUARD2" }, -- 9
|
||||
{ "show_text", "_BillsHouseBillWhyDontYouGoInsteadOfMeText" }, -- 10
|
||||
{ "jump", 13 }, -- 11
|
||||
{ "show_text", "_BillsHouseBillWhyDontYouGoInsteadOfMeText" }, -- 12
|
||||
},
|
||||
|
||||
TEXT_BILLSHOUSE_BILL_CHECK_OUT_MY_RARE_POKEMON = {
|
||||
{ "face_player" }, -- 1
|
||||
{ "show_text", "_BillsHouseBillCheckOutMyRarePokemonText" }, -- 2
|
||||
},
|
||||
},
|
||||
-- repair saves that already got the ticket under the old collapsed
|
||||
-- script (the monster never hidden, human Bill never shown)
|
||||
onEnter = function(game, ow)
|
||||
local flags = game.save.flags
|
||||
if flags.EVENT_GOT_SS_TICKET
|
||||
and not flags.EVENT_USED_CELL_SEPARATOR_ON_BILL then
|
||||
local Commands = require("src.script.Commands")
|
||||
local ctx = { game = game, save = game.save, overworld = ow }
|
||||
Commands.hide_object(ctx, "BILLS_HOUSE", "BILLSHOUSE_BILL_POKEMON")
|
||||
Commands.show_object(ctx, "BILLS_HOUSE", "BILLSHOUSE_BILL1")
|
||||
flags.EVENT_BILL_SAID_USE_CELL_SEPARATOR = true
|
||||
flags.EVENT_USED_CELL_SEPARATOR_ON_BILL = true
|
||||
flags.EVENT_MET_BILL = true
|
||||
flags.EVENT_MET_BILL_2 = true
|
||||
end
|
||||
-- After Route25ToggleBillsScript, BILL2 is the visible human Bill
|
||||
-- ("check out my rare POKéMON"). Re-apply on enter so a mid-house
|
||||
-- load still matches the toggles if the pool was rebuilt.
|
||||
if flags.EVENT_LEFT_BILLS_HOUSE_AFTER_HELPING then
|
||||
local Commands = require("src.script.Commands")
|
||||
local ctx = { game = game, save = game.save, overworld = ow }
|
||||
Commands.hide_object(ctx, "BILLS_HOUSE", "BILLSHOUSE_BILL_POKEMON")
|
||||
Commands.hide_object(ctx, "BILLS_HOUSE", "BILLSHOUSE_BILL1")
|
||||
Commands.show_object(ctx, "BILLS_HOUSE", "BILLSHOUSE_BILL2")
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
-- -------------------------------------------------------------------
|
||||
-- Route 25 outside Bill's house (scripts/Route25.asm
|
||||
-- Route25ToggleBillsScript): leaving after the SS Ticket arms the
|
||||
-- Eevee PC list and swaps human Bill to his post-help dialogue NPC.
|
||||
-- Leaving mid-quest (Bill still in the machine) puts the monster back.
|
||||
-- -------------------------------------------------------------------
|
||||
|
||||
M.ROUTE_25 = {
|
||||
onEnter = function(game, ow)
|
||||
local flags = game.save.flags
|
||||
if flags.EVENT_LEFT_BILLS_HOUSE_AFTER_HELPING then return end
|
||||
local Commands = require("src.script.Commands")
|
||||
local ctx = { game = game, save = game.save, overworld = ow }
|
||||
if not flags.EVENT_MET_BILL_2 then
|
||||
flags.EVENT_BILL_SAID_USE_CELL_SEPARATOR = nil
|
||||
Commands.show_object(ctx, "BILLS_HOUSE", "BILLSHOUSE_BILL_POKEMON")
|
||||
return
|
||||
end
|
||||
if not flags.EVENT_GOT_SS_TICKET then return end
|
||||
flags.EVENT_LEFT_BILLS_HOUSE_AFTER_HELPING = true
|
||||
-- TOGGLE_NUGGET_BRIDGE_GUY (ROUTE24_COOLTRAINER_M1)
|
||||
Commands.hide_object(ctx, "ROUTE_24", "ROUTE24_COOLTRAINER_M1")
|
||||
Commands.hide_object(ctx, "BILLS_HOUSE", "BILLSHOUSE_BILL1")
|
||||
Commands.show_object(ctx, "BILLS_HOUSE", "BILLSHOUSE_BILL2")
|
||||
end,
|
||||
}
|
||||
|
||||
-- -------------------------------------------------------------------
|
||||
@@ -142,17 +292,63 @@ M.VERMILION_CITY = {
|
||||
game.save.trashPuzzle = puz
|
||||
puz.first = love.math.random(0, 7) * 2
|
||||
end,
|
||||
-- VermilionCityDefaultScript's per-frame SSAnneTicketCheckCoords check:
|
||||
-- the unguarded cell (18,30) just west of the sailor leads straight
|
||||
-- onto the dock warp. Stepping onto it facing down always runs the
|
||||
-- sailor dialog (DisplayTextID TEXT_VERMILIONCITY_SAILOR1); only a
|
||||
-- ticket while the ship is still docked lets the player continue --
|
||||
-- otherwise they are walked back up. The sailor himself never hides.
|
||||
onStep = function(game, ow, x, y)
|
||||
if x ~= 18 or y ~= 30 then return false end
|
||||
if ow.player.facing ~= "down" then return false end
|
||||
local Flags = require("src.script.Flags")
|
||||
local t = game.data.text
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local shipLeft = Flags.get(game.save, "EVENT_SS_ANNE_LEFT")
|
||||
local hasTicket = (game.save.inventory.S_S_TICKET or 0) > 0
|
||||
if shipLeft then
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._VermilionCitySailor1ShipSetSailText or "The ship set sail.",
|
||||
function() ow:scriptMove(ow.player, "up", 1) end))
|
||||
return true
|
||||
end
|
||||
-- Walk-past is never facing-right / inFrontOfOrBehindGuardCoords, so
|
||||
-- VermilionCitySailor1Text always takes .greet_player_and_check_ticket:
|
||||
-- DoYouHaveATicket, then FlashedTicket (allow through) or YouNeedATicket
|
||||
-- (walk back). Returning true while the box is up also blocks the
|
||||
-- dock warp on this step.
|
||||
local ask = t._VermilionCitySailor1DoYouHaveATicketText
|
||||
or "Welcome to S.S.\nANNE!\fExcuse me, do you\nhave a ticket?"
|
||||
if hasTicket then
|
||||
game.stack:push(TextBox.new(game,
|
||||
ask .. "\f"
|
||||
.. (t._VermilionCitySailor1FlashedTicketText
|
||||
or "{PLAYER} flashed\nthe S.S.TICKET!")))
|
||||
return true
|
||||
end
|
||||
game.stack:push(TextBox.new(game,
|
||||
ask .. "\f"
|
||||
.. (t._VermilionCitySailor1YouNeedATicketText
|
||||
or "You need a ticket\nto get aboard."),
|
||||
function() ow:scriptMove(ow.player, "up", 1) end))
|
||||
return true
|
||||
end,
|
||||
talk = {
|
||||
-- the sailor guarding the dock gangway
|
||||
-- the sailor guarding the dock gangway (VermilionCitySailor1Text):
|
||||
-- flashing the ticket just lets you through -- he never hides, and
|
||||
-- once the ship has sailed he only reports it gone
|
||||
TEXT_VERMILIONCITY_SAILOR1 = {
|
||||
{ "face_player" }, -- 1
|
||||
{ "show_text", "_VermilionCitySailor1WelcomeToSSAnneText" }, -- 2
|
||||
{ "check_item", "S_S_TICKET" }, -- 3
|
||||
{ "jump_if_false", 8 }, -- 4
|
||||
{ "show_text", "_VermilionCitySailor1FlashedTicketText" }, -- 5
|
||||
{ "hide_object", "VERMILION_CITY", "VERMILIONCITY_SAILOR1" }, -- 6
|
||||
{ "jump", 9 }, -- 7
|
||||
{ "show_text", "_VermilionCitySailor1YouNeedATicketText" }, -- 8
|
||||
{ "check_flag", "EVENT_SS_ANNE_LEFT" }, -- 2
|
||||
{ "jump_if_true", 11 }, -- 3
|
||||
{ "show_text", "_VermilionCitySailor1DoYouHaveATicketText" }, -- 4
|
||||
{ "check_item", "S_S_TICKET" }, -- 5
|
||||
{ "jump_if_false", 9 }, -- 6
|
||||
{ "show_text", "_VermilionCitySailor1FlashedTicketText" }, -- 7
|
||||
{ "jump", 12 }, -- 8
|
||||
{ "show_text", "_VermilionCitySailor1YouNeedATicketText" }, -- 9
|
||||
{ "jump", 12 }, -- 10
|
||||
{ "show_text", "_VermilionCitySailor1ShipSetSailText" }, -- 11
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -175,18 +371,21 @@ M.SS_ANNE_2F = {
|
||||
|
||||
M.SS_ANNE_CAPTAINS_ROOM = {
|
||||
talk = {
|
||||
-- SSAnneCaptainsRoomCaptainText: after the rub line's text_asm tail,
|
||||
-- pokered plays MUSIC_PKMN_HEALED (scripts/SSAnneCaptainsRoom.asm).
|
||||
TEXT_SSANNECAPTAINSROOM_CAPTAIN = {
|
||||
{ "check_flag", "EVENT_GOT_HM01" }, -- 1
|
||||
{ "jump_if_true", 9 }, -- 2
|
||||
{ "jump_if_true", 10 }, -- 2
|
||||
{ "show_text", "_SSAnneCaptainsRoomRubCaptainsBackText" }, -- 3
|
||||
{ "show_text", "_SSAnneCaptainsRoomCaptainIFeelMuchBetterText" }, -- 4
|
||||
{ "play_once", "Music_PkmnHealed" }, -- 4
|
||||
{ "show_text", "_SSAnneCaptainsRoomCaptainIFeelMuchBetterText" }, -- 5
|
||||
-- give-then-print like scripts/SSAnneCaptainsRoom.asm (GiveItem
|
||||
-- fills wStringBuffer; the received text reads it)
|
||||
{ "give_item", "HM_CUT", 1, false }, -- 5
|
||||
{ "show_text", "_SSAnneCaptainsRoomCaptainReceivedHM01Text" }, -- 6
|
||||
{ "set_flag", "EVENT_GOT_HM01" }, -- 7
|
||||
{ "jump", 10 }, -- 8
|
||||
{ "show_text", "_SSAnneCaptainsRoomCaptainNotSickAnymoreText" }, -- 9
|
||||
{ "give_item", "HM_CUT", 1, false }, -- 6
|
||||
{ "show_text", "_SSAnneCaptainsRoomCaptainReceivedHM01Text" }, -- 7
|
||||
{ "set_flag", "EVENT_GOT_HM01" }, -- 8
|
||||
{ "jump", 11 }, -- 9
|
||||
{ "show_text", "_SSAnneCaptainsRoomCaptainNotSickAnymoreText" }, -- 10
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -196,7 +395,93 @@ M.SS_ANNE_CAPTAINS_ROOM = {
|
||||
-- MrFujisHouse.asm; the teleport back to his house is a warp)
|
||||
-- -------------------------------------------------------------------
|
||||
|
||||
-- scripts/PokemonTower7F.asm: after each Rocket loses,
|
||||
-- PokemonTower7FEndBattleScript shows its EndBattle text (EndTrainerBattle),
|
||||
-- prints its AfterBattle text (DisplayTextID), then
|
||||
-- PokemonTower7FRocketLeaveMovementScript walks the grunt off toward the
|
||||
-- (9,16) stairs (MoveSprite) and PokemonTower7FHideNPCScript despawns it
|
||||
-- (HideObject). Without this the beaten grunts stood in the corridor
|
||||
-- forever (#200).
|
||||
local POKEMON_TOWER_7F_ROCKETS = {
|
||||
{ index = 1, name = "POKEMONTOWER7F_ROCKET1",
|
||||
beat = "EVENT_BEAT_POKEMONTOWER_7_TRAINER_0",
|
||||
after = "_PokemonTower7FRocket1AfterBattleText" },
|
||||
{ index = 2, name = "POKEMONTOWER7F_ROCKET2",
|
||||
beat = "EVENT_BEAT_POKEMONTOWER_7_TRAINER_1",
|
||||
after = "_PokemonTower7FRocket2AfterBattleText" },
|
||||
{ index = 3, name = "POKEMONTOWER7F_ROCKET3",
|
||||
beat = "EVENT_BEAT_POKEMONTOWER_7_TRAINER_2",
|
||||
after = "_PokemonTower7FRocket3AfterBattleText" },
|
||||
}
|
||||
|
||||
-- PokemonTower7FNPCCoordMovementTable, keyed by the PLAYER's tile ("x,y")
|
||||
-- at the engagement (both the talk and the sight walk-up leave the player on
|
||||
-- the tile the vanilla table indexes). Each list is that entry's
|
||||
-- NPC_MOVEMENT_* exit, applied from the grunt's current tile. The exact
|
||||
-- tables are one per grunt (dbmapcoord bytes decoded back to x,y).
|
||||
local POKEMON_TOWER_7F_EXITS = {
|
||||
[1] = { -- ROCKET1 @ (9,11), faces RIGHT
|
||||
["9,12"] = { "right", "down", "down", "down", "down", "down", "left" },
|
||||
["10,11"] = { "down", "right", "down", "down", "down", "down" },
|
||||
["11,11"] = { "down", "down", "down", "down", "down" },
|
||||
["12,11"] = { "down", "down", "down", "down", "down" },
|
||||
},
|
||||
[2] = { -- ROCKET2 @ (12,9), faces LEFT
|
||||
["12,10"] = { "left", "down", "down", "down", "down", "down", "down" },
|
||||
["11,9"] = { "down", "down", "down", "left", "down", "down" },
|
||||
["10,9"] = { "down", "down", "down", "down", "down" },
|
||||
["9,9"] = { "down", "down", "down", "down", "down" },
|
||||
},
|
||||
[3] = { -- ROCKET3 @ (9,7), faces RIGHT
|
||||
["9,8"] = { "right", "down", "down", "down", "down", "down", "down" },
|
||||
["10,7"] = { "down", "down", "down", "down", "down" },
|
||||
["11,7"] = { "down", "down", "down", "down", "down" },
|
||||
["12,7"] = { "down", "down", "down", "down", "down" },
|
||||
},
|
||||
}
|
||||
-- The vanilla table only lists the sight/talk tiles the map geometry allows;
|
||||
-- a plain down-walk keeps any other engagement tile (e.g. talking from the
|
||||
-- side) safe -- it still clears the corridor and despawns the grunt.
|
||||
local POKEMON_TOWER_7F_EXIT_FALLBACK =
|
||||
{ "down", "down", "down", "down", "down" }
|
||||
|
||||
M.POKEMON_TOWER_7F = {
|
||||
-- Repair saves that beat a grunt before the exit walk was ported: in
|
||||
-- vanilla HideObject persists (wMissableObjectFlags), so a beaten grunt is
|
||||
-- still gone on re-entry. hide_object below writes objectToggles for new
|
||||
-- wins; this hides any grunt whose BEAT flag is already set.
|
||||
onEnter = function(game, ow)
|
||||
local Commands = require("src.script.Commands")
|
||||
local ctx = { game = game, save = game.save, overworld = ow }
|
||||
for _, r in ipairs(POKEMON_TOWER_7F_ROCKETS) do
|
||||
if game.save.flags[r.beat] then
|
||||
Commands.hide_object(ctx, "POKEMON_TOWER_7F", r.name)
|
||||
end
|
||||
end
|
||||
end,
|
||||
-- runVictoryHook fires after every trainer win on the map (both the sight
|
||||
-- and talk paths route through engageTrainer -> checkVictoryRewards), so
|
||||
-- this walks off whichever grunt was just beaten but is still standing.
|
||||
-- The AfterBattle text + walk queue UNDER the EndBattle box engageTrainer
|
||||
-- pushes right after, giving the vanilla order: EndBattle, AfterBattle,
|
||||
-- walk, despawn.
|
||||
onVictory = function(game, ow)
|
||||
if ow.runner:isRunning() then return end
|
||||
for _, r in ipairs(POKEMON_TOWER_7F_ROCKETS) do
|
||||
local npc = ow:npcByIndex(r.index)
|
||||
if npc and game.save.flags[r.beat] then
|
||||
local key = ow.player.cellX .. "," .. ow.player.cellY
|
||||
local dirs = (POKEMON_TOWER_7F_EXITS[r.index] or {})[key]
|
||||
or POKEMON_TOWER_7F_EXIT_FALLBACK
|
||||
ow.runner:run({
|
||||
{ "show_text", r.after }, -- DisplayTextID
|
||||
{ "walk_npc", r.index, dirs }, -- MoveSprite
|
||||
{ "hide_object", "POKEMON_TOWER_7F", r.name }, -- HideObject
|
||||
}, { npc = npc })
|
||||
return
|
||||
end
|
||||
end
|
||||
end,
|
||||
talk = {
|
||||
TEXT_POKEMONTOWER7F_MR_FUJI = {
|
||||
{ "face_player" }, -- 1
|
||||
@@ -208,7 +493,17 @@ M.POKEMON_TOWER_7F = {
|
||||
{ "show_object", "MR_FUJIS_HOUSE", "MRFUJISHOUSE_MR_FUJI" }, -- 5
|
||||
{ "hide_object", "SAFFRON_CITY", "SAFFRONCITY_ROCKET8" }, -- 6
|
||||
{ "show_object", "SAFFRON_CITY", "SAFFRONCITY_ROCKET9" }, -- 7
|
||||
{ "warp", "MR_FUJIS_HOUSE", 3, 3, "down" }, -- 8
|
||||
-- pokered warps to wDestinationWarpID $1 (0-based) -- the house's
|
||||
-- SECOND warp, the door mat at (3,7) -- facing UP
|
||||
-- (PokemonTower7FWarpToMrFujiHouseScript: SPRITE_FACING_UP +
|
||||
-- hWarpDestinationMap MR_FUJIS_HOUSE). Landing (3,3) instead put
|
||||
-- the player at the Pokédex table, and the route's first waypoint
|
||||
-- (3,7) then stepped onto a LIVE door mat and exited the house
|
||||
-- before ever talking to Fuji -- so the POKE_FLUTE was never
|
||||
-- collected and the Route 16 SNORLAX sealed the map. The arrival
|
||||
-- mat itself is inert until stepped off (warpEntryCell), which is
|
||||
-- what makes the vanilla coordinates safe.
|
||||
{ "warp", "MR_FUJIS_HOUSE", 3, 7, "up" }, -- 8
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -343,6 +638,50 @@ M.WARDENS_HOUSE = {
|
||||
-- -------------------------------------------------------------------
|
||||
|
||||
M.SILPH_CO_11F = {
|
||||
-- Giovanni's battle is a COORDINATE TRIGGER, not a talk.
|
||||
-- SilphCo11FDefaultScript (scripts/SilphCo11F.asm) checks
|
||||
-- .PlayerCoordsArray -- (6,13) and (7,12) -- every frame while
|
||||
-- EVENT_BEAT_SILPH_CO_GIOVANNI is unset: standing there shows his text,
|
||||
-- walks him three tiles down (.GiovanniMovement), and starts the fight.
|
||||
-- He also has no trainer-header entry, so sight engagement never fires
|
||||
-- either. Without this hook he was a talk-only statue four tiles away
|
||||
-- from anything the route (or a vanilla-faithful player walking the same
|
||||
-- line) would touch, and the whole Silph ending -- the flag, the Master
|
||||
-- Ball, the Saffron streets clearing -- silently never happened.
|
||||
--
|
||||
-- engageTrainer shows TEXT_SILPHCO11F_GIOVANNI as the battle text and,
|
||||
-- via victories.lua OPP_GIOVANNI#2, sets the event on a win; a loss
|
||||
-- sets nothing, so the trigger re-arms exactly as vanilla does.
|
||||
onStep = function(game, ow, x, y)
|
||||
if game.save.flags.EVENT_BEAT_SILPH_CO_GIOVANNI then return false end
|
||||
if not ((x == 6 and y == 13) or (x == 7 and y == 12)) then return false end
|
||||
local gio
|
||||
for _, npc in ipairs(ow.npcs) do
|
||||
if npc.def and npc.def.name == "SILPHCO11F_GIOVANNI" then gio = npc break end
|
||||
end
|
||||
if not gio or ow:trainerDefeated(gio) then return false end
|
||||
ow:scriptMove(gio, "down", 3, function()
|
||||
gio:facePlayer(ow.player)
|
||||
ow:engageTrainer(gio, function()
|
||||
-- SilphCo11FTeamRocketLeavesScript: Giovanni leaves the floor
|
||||
-- after the loss (the street rockets are handled by
|
||||
-- M.SAFFRON_CITY.onEnter in story4.lua).
|
||||
if game.save.flags.EVENT_BEAT_SILPH_CO_GIOVANNI then
|
||||
local Commands = require("src.script.Commands")
|
||||
local ctx = { game = game, save = game.save, overworld = ow }
|
||||
Commands.hide_object(ctx, "SILPH_CO_11F", "SILPHCO11F_GIOVANNI")
|
||||
end
|
||||
end)
|
||||
end)
|
||||
return true
|
||||
end,
|
||||
onEnter = function(game, ow)
|
||||
if game.save.flags.EVENT_BEAT_SILPH_CO_GIOVANNI then
|
||||
local Commands = require("src.script.Commands")
|
||||
local ctx = { game = game, save = game.save, overworld = ow }
|
||||
Commands.hide_object(ctx, "SILPH_CO_11F", "SILPHCO11F_GIOVANNI")
|
||||
end
|
||||
end,
|
||||
talk = {
|
||||
TEXT_SILPHCO11F_SILPH_PRESIDENT = {
|
||||
{ "face_player" }, -- 1
|
||||
@@ -363,7 +702,8 @@ M.SILPH_CO_11F = {
|
||||
-- -------------------------------------------------------------------
|
||||
-- Victory Road boulder switches (scripts/VictoryRoad1F/2F/3F.asm):
|
||||
-- a boulder resting on a switch removes a barrier block; the 3F hole
|
||||
-- drops a boulder down to the 2F switch.
|
||||
-- drops a boulder down to the 2F switch, and also dungeon-warps the
|
||||
-- player (IsPlayerOnDungeonWarp + DungeonWarpList/Data -> 2F at 22,16).
|
||||
-- -------------------------------------------------------------------
|
||||
|
||||
local function boulderAt(ow, x, y)
|
||||
@@ -429,6 +769,18 @@ M.VICTORY_ROAD_3F = {
|
||||
Commands.show_object(ctx, "VICTORY_ROAD_2F", "VICTORYROAD2F_BOULDER")
|
||||
end
|
||||
end,
|
||||
-- scripts/VictoryRoad3F.asm VictoryRoad3FDefaultScript: the same hole
|
||||
-- is a dungeon warp for the player (wDungeonWarpDestinationMap =
|
||||
-- VICTORY_ROAD_2F, wWhichDungeonWarp = 2 -> DungeonWarpData 22,16).
|
||||
-- Mirrors POKEMON_MANSION_3F.onStep; CAVERN $22 is walkable so the
|
||||
-- fall is onStep, not a collision block.
|
||||
onStep = function(game, ow, x, y)
|
||||
if x == 23 and y == 15 then
|
||||
ow:startWarpTo("VICTORY_ROAD_2F", 22, 16, ow.player.facing)
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end,
|
||||
}
|
||||
|
||||
-- -------------------------------------------------------------------
|
||||
@@ -441,6 +793,16 @@ M.VICTORY_ROAD_3F = {
|
||||
-- IndigoPlateauLobby.asm) so the champion is re-fightable on rematches,
|
||||
-- like pokered's re-entry cutscene. EVENT_BEAT_CHAMPION_RIVAL stays set
|
||||
-- forever (postgame gates like the Cerulean cave guard read it).
|
||||
--
|
||||
-- pokered forces the fight on map entry: Agatha's victory arms
|
||||
-- SCRIPT_CHAMPIONSROOM_PLAYER_ENTERS (scripts/AgathasRoom.asm), and
|
||||
-- ChampionsRoomPlayerEntersScript then runs RivalEntrance_RLEMovement
|
||||
-- (up 1, right 1, up 3) before ChampionsRoomRivalReadyToBattleScript.
|
||||
-- The rival object has no trainer header / sight range, so without that
|
||||
-- entrance script the player can walk past (issue #99). We arm on the
|
||||
-- run flag instead of Agatha's victory bit: same observable effect for
|
||||
-- first clear and Indigo rematches, without a cross-map script pointer.
|
||||
--
|
||||
-- scripts/ChampionsRoom.asm ChampionsRoomRivalDefeatedScript ->
|
||||
-- OakArrivesScript -> OakCongratulatesPlayerScript ->
|
||||
-- OakDisappointedWithRivalScript -> OakComeWithMeScript -> OakExitsScript,
|
||||
@@ -449,43 +811,69 @@ M.VICTORY_ROAD_3F = {
|
||||
-- itself is NOT run here: it belongs to the HALL_OF_FAME room script
|
||||
-- (scripts/HallOfFame.asm), so we set a one-shot marker and warp; the room
|
||||
-- onEnter (M.HALL_OF_FAME below) drives the HoF Oak speech + record.
|
||||
local championsRoomRivalScript = {
|
||||
{ "face_player" }, -- 1
|
||||
{ "check_flag", "EVENT_BEAT_CHAMPION_RIVAL_THIS_RUN" }, -- 2
|
||||
{ "jump_if_true", 25 }, -- 3 past end
|
||||
{ "show_text", "_ChampionsRoomRivalIntroText" }, -- 4
|
||||
{ "rival_battle", "OPP_RIVAL3", 1 }, -- 5
|
||||
{ "jump_if_false", 25 }, -- 6 past end
|
||||
{ "set_flag", "EVENT_BEAT_CHAMPION_RIVAL_THIS_RUN" }, -- 7
|
||||
{ "set_flag", "EVENT_BEAT_CHAMPION_RIVAL" }, -- 8
|
||||
-- ChampionsRoomRivalDefeatedScript re-displays TEXT_CHAMPIONSROOM_RIVAL,
|
||||
-- whose text_asm takes the EVENT_BEAT_CHAMPION_RIVAL branch =
|
||||
-- _ChampionsRoomRivalAfterBattleText (the in-battle _RivalDefeatedText
|
||||
-- is the port's generic "<PLAYER> defeated BLUE!" engine line instead).
|
||||
{ "show_text", "_ChampionsRoomRivalAfterBattleText" }, -- 9
|
||||
-- ChampionsRoomOakArrivesScript: Music_Cities1AlternateTempo
|
||||
-- (Cities1, kept into HALL_OF_FAME like BIT_NO_MAP_MUSIC after
|
||||
-- defeating RIVAL3), then Oak's "{PLAYER}!" + reveal + walk in
|
||||
{ "play_music", "Music_Cities1", { keep = true } }, -- 10
|
||||
{ "show_text", "_ChampionsRoomOakText" }, -- 11
|
||||
{ "show_object", "CHAMPIONS_ROOM", "CHAMPIONSROOM_OAK" }, -- 12
|
||||
{ "move_npc", 2, "up", 5 }, -- 13 OakEntranceAfterVictoryMovement
|
||||
-- OakCongratulatesPlayerScript: rival faces left, Oak faces down
|
||||
{ "face_object", 1, "left" }, -- 14
|
||||
{ "face_object", 2, "down" }, -- 15
|
||||
{ "show_text", "_ChampionsRoomOakCongratulatesPlayerText" }, -- 16
|
||||
-- OakDisappointedWithRivalScript: Oak turns to the rival (right)
|
||||
{ "face_object", 2, "right" }, -- 17
|
||||
{ "show_text", "_ChampionsRoomOakDisappointedWithRivalText" }, -- 18
|
||||
-- OakComeWithMeScript: Oak faces down again, then exits up
|
||||
{ "face_object", 2, "down" }, -- 19
|
||||
{ "show_text", "_ChampionsRoomOakComeWithMeText" }, -- 20
|
||||
{ "move_npc", 2, "up", 2 }, -- 21 OakExitChampionsRoomMovement
|
||||
{ "hide_object", "CHAMPIONS_ROOM", "CHAMPIONSROOM_OAK" }, -- 22
|
||||
-- hand the induction off to the HALL_OF_FAME room (consumed by its
|
||||
-- onEnter), then warp up into it (destWarp 1 lands at (4,7) facing up)
|
||||
{ "set_field", "pendingHallOfFame", true }, -- 23
|
||||
{ "warp", "HALL_OF_FAME", 4, 7, "up" }, -- 24
|
||||
}
|
||||
|
||||
M.CHAMPIONS_ROOM = {
|
||||
onEnter = function(game, ow)
|
||||
if game.save.flags.EVENT_BEAT_CHAMPION_RIVAL_THIS_RUN then return end
|
||||
-- Lance entrance warps land at y=7; HoF return warps land at y=0.
|
||||
-- Only the south entry should run ChampionsRoomPlayerEntersScript.
|
||||
if ow.player.cellY < 7 then return end
|
||||
local rival
|
||||
for _, npc in ipairs(ow.npcs) do
|
||||
if npc.def and npc.def.name == "CHAMPIONSROOM_RIVAL" then
|
||||
rival = npc
|
||||
break
|
||||
end
|
||||
end
|
||||
-- RivalEntrance_RLEMovement, then the battle/Oak script (queued
|
||||
-- separately so talk-script jump indices stay 1-based as written).
|
||||
ow:queueScript({
|
||||
{ "move_player", "up", 1 },
|
||||
{ "move_player", "right", 1 },
|
||||
{ "move_player", "up", 3 },
|
||||
})
|
||||
ow:queueScript(championsRoomRivalScript, { npc = rival })
|
||||
end,
|
||||
talk = {
|
||||
TEXT_CHAMPIONSROOM_RIVAL = {
|
||||
{ "face_player" }, -- 1
|
||||
{ "check_flag", "EVENT_BEAT_CHAMPION_RIVAL_THIS_RUN" }, -- 2
|
||||
{ "jump_if_true", 24 }, -- 3
|
||||
{ "show_text", "_ChampionsRoomRivalIntroText" }, -- 4
|
||||
{ "rival_battle", "OPP_RIVAL3", 1 }, -- 5
|
||||
{ "jump_if_false", 24 }, -- 6
|
||||
{ "set_flag", "EVENT_BEAT_CHAMPION_RIVAL_THIS_RUN" }, -- 7
|
||||
{ "set_flag", "EVENT_BEAT_CHAMPION_RIVAL" }, -- 8
|
||||
-- ChampionsRoomRivalDefeatedScript re-displays TEXT_CHAMPIONSROOM_RIVAL,
|
||||
-- whose text_asm takes the EVENT_BEAT_CHAMPION_RIVAL branch =
|
||||
-- _ChampionsRoomRivalAfterBattleText (the in-battle _RivalDefeatedText
|
||||
-- is the port's generic "<PLAYER> defeated BLUE!" engine line instead).
|
||||
{ "show_text", "_ChampionsRoomRivalAfterBattleText" }, -- 9
|
||||
-- ChampionsRoomOakArrivesScript: Oak's "{PLAYER}!" then reveal + walk in
|
||||
{ "show_text", "_ChampionsRoomOakText" }, -- 10
|
||||
{ "show_object", "CHAMPIONS_ROOM", "CHAMPIONSROOM_OAK" }, -- 11
|
||||
{ "move_npc", 2, "up", 5 }, -- 12 OakEntranceAfterVictoryMovement (3,7)->(3,2)
|
||||
-- OakCongratulatesPlayerScript: rival faces left, Oak faces down
|
||||
{ "face_object", 1, "left" }, -- 13
|
||||
{ "face_object", 2, "down" }, -- 14
|
||||
{ "show_text", "_ChampionsRoomOakCongratulatesPlayerText" }, -- 15
|
||||
-- OakDisappointedWithRivalScript: Oak turns to the rival (right)
|
||||
{ "face_object", 2, "right" }, -- 16
|
||||
{ "show_text", "_ChampionsRoomOakDisappointedWithRivalText" }, -- 17
|
||||
-- OakComeWithMeScript: Oak faces down again, then exits up
|
||||
{ "face_object", 2, "down" }, -- 18
|
||||
{ "show_text", "_ChampionsRoomOakComeWithMeText" }, -- 19
|
||||
{ "move_npc", 2, "up", 2 }, -- 20 OakExitChampionsRoomMovement (3,2)->(3,0)
|
||||
{ "hide_object", "CHAMPIONS_ROOM", "CHAMPIONSROOM_OAK" }, -- 21
|
||||
-- hand the induction off to the HALL_OF_FAME room (consumed by its
|
||||
-- onEnter), then warp up into it (destWarp 1 lands at (4,7) facing up)
|
||||
{ "set_field", "pendingHallOfFame", true }, -- 22
|
||||
{ "warp", "HALL_OF_FAME", 4, 7, "up" }, -- 23
|
||||
},
|
||||
TEXT_CHAMPIONSROOM_RIVAL = championsRoomRivalScript,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -515,6 +903,29 @@ M.HALL_OF_FAME = {
|
||||
local toggles = game.save.objectToggles and game.save.objectToggles.HALL_OF_FAME
|
||||
if toggles then toggles.HALLOFFAME_OAK = nil end
|
||||
|
||||
-- Make the Hall of Fame recording machine interactable. The console
|
||||
-- juts from the north wall as the two solid cells (4,1) and (5,1); a
|
||||
-- sign on each lets the player face the machine (from below, or from
|
||||
-- either side) and press A to run the TEXT_HALLOFFAME_PC talk script.
|
||||
-- maps.lua is generated from the ROM and gitignored, so the sign is
|
||||
-- injected here (a tracked script) rather than baked into map data.
|
||||
local def = ow.map.def
|
||||
def.signs = def.signs or {}
|
||||
local present = false
|
||||
for _, s in ipairs(def.signs) do
|
||||
if s.text == "TEXT_HALLOFFAME_PC" then present = true break end
|
||||
end
|
||||
if not present then
|
||||
table.insert(def.signs, { text = "TEXT_HALLOFFAME_PC", x = 4, y = 1 })
|
||||
table.insert(def.signs, { text = "TEXT_HALLOFFAME_PC", x = 5, y = 1 })
|
||||
end
|
||||
-- the live Map instance built its signAt lookup from def.signs before
|
||||
-- this hook ran, so rebuild it (idempotent) to pick up the injection
|
||||
ow.map.signAt = {}
|
||||
for _, s in ipairs(def.signs) do
|
||||
ow.map.signAt[s.y * ow.map.widthCells + s.x] = s
|
||||
end
|
||||
|
||||
if not game.save.pendingHallOfFame then return end
|
||||
game.save.pendingHallOfFame = false
|
||||
ow:queueScript({
|
||||
@@ -528,6 +939,21 @@ M.HALL_OF_FAME = {
|
||||
{ "record_hall_of_fame" }, -- predef HallOfFamePC: induction + credits
|
||||
})
|
||||
end,
|
||||
talk = {
|
||||
-- The recording machine doubles as a "warp home" PC: a YES/NO prompt
|
||||
-- that teleports back to the new-game bedroom spawn (special_warps.asm
|
||||
-- NewGameWarp: REDS_HOUSE_2F, 3, 6, facing down). Fabricated
|
||||
-- convenience -- there is no such prompt in the original ROM.
|
||||
-- Heal + remember_outdoor so house LAST_MAP mats land in Pallet, not
|
||||
-- Indigo Plateau (issue #103 escape path for already-stuck saves).
|
||||
TEXT_HALLOFFAME_PC = {
|
||||
{ "ask", "Return to\nPALLET TOWN?" }, -- 1 YES/NO -> lastCheck
|
||||
{ "jump_if_false", "end" }, -- 2 NO: back away
|
||||
{ "heal_party" }, -- 3
|
||||
{ "remember_outdoor", "PALLET_TOWN", 5, 6 }, -- 4 LAST_MAP -> Pallet door
|
||||
{ "warp", "REDS_HOUSE_2F", 3, 6, "down" }, -- 5 YES: home to your room
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
-- -------------------------------------------------------------------
|
||||
@@ -538,35 +964,86 @@ M.HALL_OF_FAME = {
|
||||
|
||||
M.CERULEAN_CITY = {
|
||||
talk = {
|
||||
-- CeruleanCityRivalText: Bill line once beaten; pre-battle otherwise.
|
||||
-- Post-fight walk matches CeruleanCityMovement4 (right then into town).
|
||||
TEXT_CERULEANCITY_RIVAL = {
|
||||
{ "face_player" }, -- 1
|
||||
{ "check_flag", "EVENT_BEAT_CERULEAN_RIVAL" }, -- 2
|
||||
{ "jump_if_true", 9 }, -- 3
|
||||
{ "show_text", "_CeruleanCityRivalPreBattleText" }, -- 4
|
||||
{ "rival_battle", "OPP_RIVAL1", 7 }, -- 5
|
||||
{ "jump_if_false", 10 }, -- 6
|
||||
{ "set_flag", "EVENT_BEAT_CERULEAN_RIVAL" }, -- 7
|
||||
{ "show_text", "_CeruleanCityRivalDefeatedText" }, -- 8
|
||||
{ "jump", 10 }, -- 9
|
||||
{ "face_player" }, -- 1
|
||||
{ "check_flag", "EVENT_BEAT_CERULEAN_RIVAL" }, -- 2
|
||||
{ "jump_if_true", 13 }, -- 3
|
||||
{ "show_text", "_CeruleanCityRivalPreBattleText" }, -- 4
|
||||
{ "rival_battle", "OPP_RIVAL1", 7 }, -- 5
|
||||
{ "jump_if_false", "end" }, -- 6
|
||||
{ "set_flag", "EVENT_BEAT_CERULEAN_RIVAL" }, -- 7
|
||||
{ "show_text", "_CeruleanCityRivalDefeatedText" }, -- 8
|
||||
{ "show_text", "_CeruleanCityRivalIWentToBillsText" }, -- 9
|
||||
{ "walk_npc", 1, { "right", "down", "down", "down",
|
||||
"down", "down", "down" } }, -- 10
|
||||
{ "hide_object", "CERULEAN_CITY", "CERULEANCITY_RIVAL" }, -- 11
|
||||
{ "jump", "end" }, -- 12
|
||||
{ "show_text", "_CeruleanCityRivalIWentToBillsText" }, -- 13
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
-- PokemonTower2F.asm: after the battle, PokemonTower2FDefeatedRivalScript
|
||||
-- walks him out (RightThenDown vs DownThenRight from EVENT_POKEMON_TOWER_
|
||||
-- RIVAL_ON_LEFT) then HideObject TOGGLE_POKEMON_TOWER_2F_RIVAL. Player
|
||||
-- at (15,5) is the ON_LEFT case.
|
||||
local TOWER_RIVAL_EXIT_RIGHT_THEN_DOWN =
|
||||
{ "right", "down", "down", "right", "down", "down", "right", "right" }
|
||||
local TOWER_RIVAL_EXIT_DOWN_THEN_RIGHT =
|
||||
{ "down", "down", "right", "right", "right", "right", "down", "down" }
|
||||
|
||||
local function pokemonTower2FRivalScript(playerX)
|
||||
local exitDirs = (playerX == 15)
|
||||
and TOWER_RIVAL_EXIT_DOWN_THEN_RIGHT
|
||||
or TOWER_RIVAL_EXIT_RIGHT_THEN_DOWN
|
||||
return {
|
||||
{ "face_player" }, -- 1
|
||||
{ "check_flag", "EVENT_BEAT_POKEMON_TOWER_RIVAL" }, -- 2
|
||||
{ "jump_if_true", 12 }, -- 3
|
||||
{ "show_text", "_PokemonTower2FRivalWhatBringsYouHereText" }, -- 4
|
||||
{ "rival_battle", "OPP_RIVAL2", 4 }, -- 5
|
||||
{ "jump_if_false", "end" }, -- 6 loss: stay
|
||||
{ "set_flag", "EVENT_BEAT_POKEMON_TOWER_RIVAL" }, -- 7
|
||||
{ "show_text", "_PokemonTower2FRivalDefeatedText" }, -- 8
|
||||
{ "walk_npc", 1, exitDirs }, -- 9
|
||||
{ "hide_object", "POKEMON_TOWER_2F", "POKEMONTOWER2F_RIVAL" }, -- 10
|
||||
{ "jump", "end" }, -- 11
|
||||
{ "show_text", "_PokemonTower2FRivalHowsYourDexText" }, -- 12
|
||||
}
|
||||
end
|
||||
|
||||
M.POKEMON_TOWER_2F = {
|
||||
rivalScript = pokemonTower2FRivalScript,
|
||||
talk = {
|
||||
TEXT_POKEMONTOWER2F_RIVAL = {
|
||||
{ "face_player" }, -- 1
|
||||
{ "check_flag", "EVENT_BEAT_POKEMON_TOWER_RIVAL" }, -- 2
|
||||
{ "jump_if_true", 10 }, -- 3
|
||||
{ "show_text", "_PokemonTower2FRivalWhatBringsYouHereText" }, -- 4
|
||||
{ "rival_battle", "OPP_RIVAL2", 4 }, -- 5
|
||||
{ "jump_if_false", 11 }, -- 6
|
||||
{ "set_flag", "EVENT_BEAT_POKEMON_TOWER_RIVAL" }, -- 7
|
||||
{ "show_text", "_PokemonTower2FRivalDefeatedText" }, -- 8
|
||||
{ "jump", 11 }, -- 9
|
||||
{ "show_text", "_PokemonTower2FRivalHowsYourDexText" }, -- 10
|
||||
},
|
||||
TEXT_POKEMONTOWER2F_RIVAL = function(game, ow, npc, done)
|
||||
ow.runner:run(pokemonTower2FRivalScript(ow.player.cellX),
|
||||
{ npc = npc, onDone = done })
|
||||
end,
|
||||
},
|
||||
-- Saves that beat him before the exit walk was ported still have the
|
||||
-- flag but a visible rival; hide on enter like BillsHouse repairs.
|
||||
onEnter = function(game, ow)
|
||||
if not game.save.flags.EVENT_BEAT_POKEMON_TOWER_RIVAL then return end
|
||||
local Commands = require("src.script.Commands")
|
||||
Commands.hide_object({ game = game, save = game.save, overworld = ow },
|
||||
"POKEMON_TOWER_2F", "POKEMONTOWER2F_RIVAL")
|
||||
end,
|
||||
-- PokemonTower2FDefaultScript: walking past the rival's tile forces
|
||||
-- the encounter (ArePlayerCoordsInArray on (15,5)/(14,6)) -- he never
|
||||
-- waits to be talked to
|
||||
onStep = function(game, ow, x, y)
|
||||
if game.save.flags.EVENT_BEAT_POKEMON_TOWER_RIVAL then return false end
|
||||
if not ((x == 15 and y == 5) or (x == 14 and y == 6)) then return false end
|
||||
if ow.runner:isRunning() then return false end
|
||||
local rival = ow:npcByIndex(1)
|
||||
if not rival then return false end
|
||||
ow.player.facing = (x == 15) and "left" or "up"
|
||||
require("src.core.Music").play(game.data, "Music_MeetRival")
|
||||
ow.runner:run(pokemonTower2FRivalScript(x), { npc = rival })
|
||||
return true
|
||||
end,
|
||||
}
|
||||
|
||||
-- -------------------------------------------------------------------
|
||||
|
||||
@@ -278,6 +278,23 @@ M.PALLET_TOWN = {
|
||||
|
||||
local DRINKS = { "FRESH_WATER", "SODA_POP", "LEMONADE" }
|
||||
|
||||
-- Hand over the first drink in the bag, if any. Mirrors RemoveGuardDrink
|
||||
-- (engine/items/inventory.asm), which walks the same three item ids and
|
||||
-- removes ONE, and the caller's BIT_GAVE_SAFFRON_GUARDS_DRINK.
|
||||
local function takeGuardDrink(game)
|
||||
for _, drink in ipairs(DRINKS) do
|
||||
if (game.save.inventory[drink] or 0) > 0 then
|
||||
game.save.inventory[drink] = game.save.inventory[drink] - 1
|
||||
if game.save.inventory[drink] == 0 then
|
||||
game.save.inventory[drink] = nil
|
||||
end
|
||||
game.save.flags.EVENT_GAVE_GUARDS_DRINK = true
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function saffronGate(guardText, triggers, horizontal)
|
||||
return {
|
||||
talk = {
|
||||
@@ -289,18 +306,15 @@ local function saffronGate(guardText, triggers, horizontal)
|
||||
t._SaffronGateGuardThanksForTheDrinkText or "Gee, that was\ntasty!", done))
|
||||
return
|
||||
end
|
||||
for _, drink in ipairs(DRINKS) do
|
||||
if (game.save.inventory[drink] or 0) > 0 then
|
||||
game.save.inventory[drink] = game.save.inventory[drink] - 1
|
||||
if game.save.inventory[drink] == 0 then
|
||||
game.save.inventory[drink] = nil
|
||||
end
|
||||
game.save.flags.EVENT_GAVE_GUARDS_DRINK = true
|
||||
game.stack:push(TextBox.new(game,
|
||||
(t._SaffronGateGuardYouCanGoOnThroughText or
|
||||
"Thanks! You can\ngo on through!"), done))
|
||||
return
|
||||
end
|
||||
if takeGuardDrink(game) then
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._SaffronGateGuardImParchedText or "Whoa, boy!\nI'm parched!",
|
||||
function()
|
||||
game.stack:push(TextBox.new(game,
|
||||
(t._SaffronGateGuardYouCanGoOnThroughText or
|
||||
"You can go on\nthrough!"), done))
|
||||
end))
|
||||
return
|
||||
end
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._SaffronGateGuardGeeImThirstyText or "Gee, I'm thirsty\nthough!", done))
|
||||
@@ -317,6 +331,31 @@ local function saffronGate(guardText, triggers, horizontal)
|
||||
if game.save.flags.EVENT_GAVE_GUARDS_DRINK then return false end
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local t = game.data.text
|
||||
-- Stepping on the trigger WITH a drink hands it over right here.
|
||||
--
|
||||
-- Route5GateDefaultScript (scripts/Route5Gate.asm) runs
|
||||
-- `farcall RemoveGuardDrink` before it decides anything: the coord
|
||||
-- trigger itself takes the drink and sets
|
||||
-- BIT_GAVE_SAFFRON_GUARDS_DRINK, and only a player carrying nothing
|
||||
-- gets the thirsty line and the walk-back. We had the removal on the
|
||||
-- guard's TALK handler only, so walking up with a FRESH_WATER in the
|
||||
-- bag was turned away and the four gates stayed shut unless you
|
||||
-- happened to talk to him -- which vanilla never requires.
|
||||
--
|
||||
-- Saffron is the middle of the map, so this sealed it: every route
|
||||
-- through the city (Celadon <-> Lavender, Vermilion <-> Cerulean the
|
||||
-- short way) was unreachable, and the bot could not get to Lavender
|
||||
-- for the POKE_FLUTE at all.
|
||||
if takeGuardDrink(game) then
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._SaffronGateGuardImParchedText or "Whoa, boy!\nI'm parched!",
|
||||
function()
|
||||
game.stack:push(TextBox.new(game,
|
||||
(t._SaffronGateGuardYouCanGoOnThroughText or
|
||||
"You can go on\nthrough!")))
|
||||
end))
|
||||
return true
|
||||
end
|
||||
local back
|
||||
if horizontal then
|
||||
back = ow.player.facing == "left" and "right" or "left"
|
||||
@@ -324,7 +363,7 @@ local function saffronGate(guardText, triggers, horizontal)
|
||||
back = ow.player.facing == "up" and "down" or "up"
|
||||
end
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._SaffronGateGuardImParchedText or "I'm parched...\nNo entry until\nI get a drink!",
|
||||
t._SaffronGateGuardGeeImThirstyText or "Gee, I'm thirsty\nthough!\nThe road's closed.",
|
||||
function()
|
||||
ow:scriptMove(ow.player, back, 1)
|
||||
end))
|
||||
@@ -386,40 +425,112 @@ M.BIKE_SHOP = {
|
||||
-- the Cinnabar lab (the wait is skipped).
|
||||
-- -------------------------------------------------------------------
|
||||
|
||||
local function mtMoonFossil(itemId, otherName)
|
||||
-- The Super Nerd (object index 1) claims both fossils and blocks the way
|
||||
-- to them. In scripts/MtMoonB2F.asm he isn't a sight-line trainer: his
|
||||
-- header carries no range, so MtMoonB2FDefaultScript force-triggers the
|
||||
-- battle the instant the player steps onto (13,8) -- the chokepoint tile
|
||||
-- to his right -- and reaching for a fossil intercepts you too.
|
||||
local function superNerdBeaten(ow)
|
||||
local nerd = ow:npcByIndex(1)
|
||||
return not nerd or ow:trainerDefeated(nerd)
|
||||
end
|
||||
|
||||
local function engageSuperNerd(game, ow, onDone)
|
||||
local nerd = ow:npcByIndex(1)
|
||||
if not nerd or ow:trainerDefeated(nerd) then
|
||||
if onDone then onDone() end
|
||||
return
|
||||
end
|
||||
nerd:facePlayer(ow.player)
|
||||
ow:engageTrainer(nerd, onDone)
|
||||
end
|
||||
|
||||
-- MtMoonB2FMoveSuperNerdScript: player-near-dome coords walk RIGHT then
|
||||
-- UP (MoveRight falls through into MoveUp); near-helix walks UP only.
|
||||
local function mtMoonNerdWalk(px, py, itemId)
|
||||
if (px == 12 and py == 7) or (px == 11 and py == 6) or (px == 12 and py == 5) then
|
||||
return { "right", "up" }
|
||||
end
|
||||
if (px == 13 and py == 7) or (px == 14 and py == 6) or (px == 14 and py == 5) then
|
||||
return { "up" }
|
||||
end
|
||||
return (itemId == "DOME_FOSSIL") and { "right", "up" } or { "up" }
|
||||
end
|
||||
|
||||
-- scripts/MtMoonB2F.asm Dome/Helix fossil text_asm + MoveSuperNerd +
|
||||
-- SuperNerdTakesOtherFossil: pick one, hide it, walk the nerd to the
|
||||
-- other, "All right. Then this is mine!", hide the other.
|
||||
local function mtMoonFossil(itemId, otherName, gotFlag)
|
||||
return function(game, ow, npc, done)
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local ChoiceBox = require("src.ui.ChoiceBox")
|
||||
if game.save.flags.EVENT_GOT_A_FOSSIL then
|
||||
game.stack:push(TextBox.new(game, "You already took\na fossil.", done))
|
||||
local t = game.data.text
|
||||
local flags = game.save.flags
|
||||
if flags.EVENT_GOT_DOME_FOSSIL or flags.EVENT_GOT_HELIX_FOSSIL then
|
||||
done()
|
||||
return
|
||||
end
|
||||
game.stack:push(TextBox.new(game, "You found a\nfossil! Take it?", function()
|
||||
game.stack:push(ChoiceBox.new(game, function(yes)
|
||||
if not yes then done() return end
|
||||
game.save.inventory[itemId] = 1
|
||||
game.save.flags.EVENT_GOT_A_FOSSIL = true
|
||||
local Commands = require("src.script.Commands")
|
||||
local ctx = { save = game.save, overworld = ow, game = game }
|
||||
Commands.hide_object(ctx, "MT_MOON_B2F", npc.def.name)
|
||||
Commands.hide_object(ctx, "MT_MOON_B2F", otherName)
|
||||
local name = game.data.items[itemId].name
|
||||
if not superNerdBeaten(ow) then
|
||||
engageSuperNerd(game, ow, done)
|
||||
return
|
||||
end
|
||||
local want = (itemId == "DOME_FOSSIL")
|
||||
and (t._MtMoonB2FDomeFossilYouWantText or "You want the\nDOME FOSSIL?")
|
||||
or (t._MtMoonB2FHelixFossilYouWantText or "You want the\nHELIX FOSSIL?")
|
||||
game.stack:push(TextBox.new(game, want, nil, { choice = function(yes)
|
||||
if not yes then done() return end
|
||||
if not require("src.inventory.Bag").add(game.save, itemId, 1) then
|
||||
game.stack:push(TextBox.new(game,
|
||||
("%s got the\n%s!"):format(game.save.player.name, name), done))
|
||||
end))
|
||||
end))
|
||||
t._MtMoonB2FYouHaveNoRoomText or "Look, you've got\nno room for this.",
|
||||
done))
|
||||
return
|
||||
end
|
||||
local idef = game.data.items[itemId]
|
||||
game.stringBuffer = idef and idef.name or itemId
|
||||
require("src.core.Sound").play(game.data, "Get_Key_Item")
|
||||
local dirs = mtMoonNerdWalk(ow.player.cellX, ow.player.cellY, itemId)
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._MtMoonB2FReceivedFossilText
|
||||
or ("{PLAYER} got the\n" .. game.stringBuffer .. "!"),
|
||||
function()
|
||||
local Commands = require("src.script.Commands")
|
||||
Commands.hide_object(
|
||||
{ save = game.save, overworld = ow, game = game },
|
||||
"MT_MOON_B2F", npc.def.name)
|
||||
flags[gotFlag] = true
|
||||
ow.runner:run({
|
||||
{ "walk_npc", 1, dirs },
|
||||
{ "text_opts", { auto = true } },
|
||||
{ "show_text", "_MtMoonB2FSuperNerdThenThisIsMineText" },
|
||||
{ "play_sound", "Get_Key_Item" },
|
||||
{ "hide_object", "MT_MOON_B2F", otherName },
|
||||
}, { onDone = done })
|
||||
end))
|
||||
end }))
|
||||
end
|
||||
end
|
||||
|
||||
M.MT_MOON_B2F = {
|
||||
-- MtMoonB2FDefaultScript forces the Super Nerd battle when the player
|
||||
-- steps onto (13,8), the tile beside him guarding the fossils.
|
||||
onStep = function(game, ow, x, y)
|
||||
if x == 13 and y == 8 and not superNerdBeaten(ow) then
|
||||
engageSuperNerd(game, ow, nil)
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end,
|
||||
talk = {
|
||||
TEXT_MTMOONB2F_DOME_FOSSIL = mtMoonFossil("DOME_FOSSIL", "MTMOONB2F_HELIX_FOSSIL"),
|
||||
TEXT_MTMOONB2F_HELIX_FOSSIL = mtMoonFossil("HELIX_FOSSIL", "MTMOONB2F_DOME_FOSSIL"),
|
||||
TEXT_MTMOONB2F_DOME_FOSSIL = mtMoonFossil(
|
||||
"DOME_FOSSIL", "MTMOONB2F_HELIX_FOSSIL", "EVENT_GOT_DOME_FOSSIL"),
|
||||
TEXT_MTMOONB2F_HELIX_FOSSIL = mtMoonFossil(
|
||||
"HELIX_FOSSIL", "MTMOONB2F_DOME_FOSSIL", "EVENT_GOT_HELIX_FOSSIL"),
|
||||
},
|
||||
}
|
||||
|
||||
-- The ticket clerk (scripts/Museum1F.asm Museum1FScientist1Text):
|
||||
-- Y50, once; declining at the rope walks you back out.
|
||||
-- Y50, once. Declining at the rope shoves the player one tile SOUTH back off
|
||||
-- the exhibit rope they crossed heading north (#151); the museum floor has no
|
||||
-- ledges, so a plain scriptMove("down",1) is the correct primitive.
|
||||
local function museumClerk(game, ow, done, onDecline)
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local ChoiceBox = require("src.ui.ChoiceBox")
|
||||
@@ -454,7 +565,7 @@ M.MUSEUM_1F = {
|
||||
if y == 4 and (x == 9 or x == 10)
|
||||
and not game.save.flags.EVENT_BOUGHT_MUSEUM_TICKET then
|
||||
museumClerk(game, ow, nil, function()
|
||||
ow:scriptMove(ow.player, "right", 1)
|
||||
ow:scriptMove(ow.player, "down", 1)
|
||||
end)
|
||||
return true
|
||||
end
|
||||
@@ -646,73 +757,174 @@ M.CINNABAR_LAB_FOSSIL_ROOM = {
|
||||
-- -------------------------------------------------------------------
|
||||
-- Day-care (scripts/Daycare.asm): the boarded Pokémon earns 1 exp per
|
||||
-- step; the fee is ¥100 plus ¥100 per level gained.
|
||||
--
|
||||
-- #118: do not raise mon.level until a paid retrieve (pokered reverts
|
||||
-- wDayCareMonBoxLevel on .leaveMonInDayCare). Fold pending steps into
|
||||
-- mon.exp once and clear them so a second talk cannot re-apply the same
|
||||
-- walk. Fill {RAM:wNameBuffer}/{RAM:wDayCareMonName}/{NUM:...} here —
|
||||
-- TextBox.TOKENS.RAM only knows wStringBuffer.
|
||||
-- -------------------------------------------------------------------
|
||||
|
||||
local function fillDaycareText(s, subs)
|
||||
s = s:gsub("{PLAYER}", subs.player or "")
|
||||
s = s:gsub("{RAM:([^}]*)}", function(name) return subs[name] or "" end)
|
||||
-- extractor NUM spans may include flags after a comma; keep the name
|
||||
s = s:gsub("{NUM:([%w_]+)[^}]*}", function(name)
|
||||
return tostring(subs[name] or "0")
|
||||
end)
|
||||
return s
|
||||
end
|
||||
|
||||
M.DAYCARE = {
|
||||
talk = {
|
||||
TEXT_DAYCARE_GENTLEMAN = function(game, ow, npc, done)
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local ChoiceBox = require("src.ui.ChoiceBox")
|
||||
local t = game.data.text
|
||||
local dc = game.save.daycare
|
||||
local playerName = game.save.player and game.save.player.name or "RED"
|
||||
|
||||
local function monName(mon)
|
||||
local def = game.data.pokemon[mon.species]
|
||||
return mon.nickname or (def and def.name) or mon.species
|
||||
end
|
||||
|
||||
if dc and dc.mon then
|
||||
local Growth = require("src.pokemon.Growth")
|
||||
local Stats = require("src.pokemon.Stats")
|
||||
local Party = require("src.pokemon.Party")
|
||||
local mon = dc.mon
|
||||
local def = game.data.pokemon[mon.species]
|
||||
mon.exp = mon.exp + (dc.steps or 0)
|
||||
local newLevel = math.min(100, Growth.levelForExp(def.growthRate, mon.exp))
|
||||
local fee = 100 + (newLevel - mon.level) * 100
|
||||
local grew = newLevel > mon.level
|
||||
mon.level = newLevel
|
||||
mon.stats = Stats.calc(def, mon.level, mon.dvs, mon.statExp)
|
||||
mon.hp = mon.stats.hp
|
||||
local msg = grew and (t._DaycareGentlemanMonHasGrownText or "It's grown a lot!")
|
||||
or "Back already?"
|
||||
game.stack:push(TextBox.new(game,
|
||||
msg .. ("\fThe fee is ¥%d.\nGet it back?"):format(fee), function()
|
||||
game.stack:push(ChoiceBox.new(game, function(yes)
|
||||
if yes and game.save.money >= fee then
|
||||
-- Apply deferred step-exp once (OverworldController only bumps
|
||||
-- daycare.steps). Clearing prevents double-count on re-talk.
|
||||
mon.exp = (mon.exp or 0) + (dc.steps or 0)
|
||||
dc.steps = 0
|
||||
-- depositLevel mirrors wDayCareMonBoxLevel: fee baseline that
|
||||
-- must survive a declined retrieve. Fall back to mon.level for
|
||||
-- older saves that predate the field.
|
||||
if dc.depositLevel == nil then dc.depositLevel = mon.level end
|
||||
local startLevel = dc.depositLevel
|
||||
local newLevel = Growth.levelForExp(def and def.growthRate, mon.exp)
|
||||
if newLevel >= 100 then
|
||||
newLevel = 100
|
||||
if def then
|
||||
mon.exp = Growth.expForLevel(def.growthRate, 100)
|
||||
end
|
||||
end
|
||||
local levelsGrown = math.max(0, newLevel - startLevel)
|
||||
local fee = 100 + levelsGrown * 100
|
||||
local name = monName(mon)
|
||||
local subs = {
|
||||
player = playerName,
|
||||
wNameBuffer = name,
|
||||
wDayCareMonName = name,
|
||||
wDayCareNumLevelsGrown = levelsGrown,
|
||||
wDayCareTotalCost = fee,
|
||||
}
|
||||
local statusText = levelsGrown > 0
|
||||
and (t._DaycareGentlemanMonHasGrownText
|
||||
or "Your {RAM:wNameBuffer}\nhas grown a lot!\fBy level, it's\ngrown by {NUM:wDayCareNumLevelsGrown, 1, 3}!\fAren't I great?")
|
||||
or (t._DaycareGentlemanMonNeedsMoreTimeText
|
||||
or "Back already?\nYour {RAM:wNameBuffer}\nneeds some more\ntime with me.")
|
||||
|
||||
game.stack:push(TextBox.new(game, fillDaycareText(statusText, subs), function()
|
||||
if #game.save.party >= Party.MAX then
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._DaycareGentlemanNoRoomForMonText
|
||||
or "You have no room\nfor this POKéMON!", done))
|
||||
return
|
||||
end
|
||||
game.stack:push(TextBox.new(game,
|
||||
fillDaycareText(
|
||||
t._DaycareGentlemanOweMoneyText
|
||||
or "You owe me ¥{NUM:wDayCareTotalCost, 2 | LEADING_ZEROES | LEFT_ALIGN}\nfor the return\nof this POKéMON.",
|
||||
subs),
|
||||
nil, { choice = function(yes)
|
||||
if not yes then
|
||||
-- .leaveMonInDayCare: revert any transient level bump
|
||||
mon.level = startLevel
|
||||
game.stack:push(TextBox.new(game,
|
||||
(t._DaycareGentlemanAllRightThenText or "All right then,\n")
|
||||
.. (t._DaycareGentlemanComeAgainText or "come again."),
|
||||
done))
|
||||
return
|
||||
end
|
||||
if (game.save.money or 0) < fee then
|
||||
mon.level = startLevel
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._DaycareGentlemanNotEnoughMoneyText
|
||||
or "Hey, you don't\nhave enough ¥!", done))
|
||||
return
|
||||
end
|
||||
game.save.money = game.save.money - fee
|
||||
mon.level = newLevel
|
||||
if def then
|
||||
mon.stats = Stats.calc(def, mon.level, mon.dvs, mon.statExp)
|
||||
mon.hp = mon.stats.hp
|
||||
-- Daycare.asm: WriteMonMoves with wLearningMovesFromDayCare
|
||||
local Pokemon = require("src.pokemon.Pokemon")
|
||||
Pokemon.learnMovesFromDayCare(
|
||||
game.data, mon, def, startLevel, newLevel)
|
||||
end
|
||||
table.insert(game.save.party, mon)
|
||||
game.save.daycare = nil
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._DaycareGentlemanGotMonBackText or "Here you go!", done))
|
||||
else
|
||||
game.stack:push(TextBox.new(game,
|
||||
yes and (t._DaycareGentlemanOweMoneyText or "You owe me money!")
|
||||
or "Come again!", done))
|
||||
end
|
||||
end))
|
||||
t._DaycareGentlemanHeresYourMonText
|
||||
or "Thank you! Here's\nyour POKéMON!", function()
|
||||
game.stack:push(TextBox.new(game,
|
||||
fillDaycareText(
|
||||
t._DaycareGentlemanGotMonBackText
|
||||
or "{PLAYER} got\n{RAM:wDayCareMonName} back!",
|
||||
subs), done))
|
||||
end))
|
||||
end }))
|
||||
end))
|
||||
return
|
||||
end
|
||||
|
||||
if #game.save.party < 2 then
|
||||
game.stack:push(TextBox.new(game,
|
||||
"You only have one\nPOKéMON with you!", done))
|
||||
return
|
||||
end
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._DaycareGentlemanIntroText or "I can raise a\nPOKéMON for you.", function()
|
||||
game.stack:push(ChoiceBox.new(game, function(yes)
|
||||
if not yes then done() return end
|
||||
local PartyMenu = require("src.ui.PartyMenu")
|
||||
game.stack:push(PartyMenu.new(game, {
|
||||
pickOnly = true,
|
||||
onSwitch = function(mon)
|
||||
for i, m in ipairs(game.save.party) do
|
||||
if m == mon then table.remove(game.save.party, i) break end
|
||||
end
|
||||
game.save.daycare = { mon = mon, steps = 0 }
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._DaycareGentlemanWillLookAfterMonText or
|
||||
"Fine, I'll look\nafter it a while!", done))
|
||||
end,
|
||||
}))
|
||||
end))
|
||||
end))
|
||||
t._DaycareGentlemanIntroText
|
||||
or "I run a DAYCARE.\nWould you like me\nto raise one of\nyour POKéMON?",
|
||||
nil, { choice = function(yes)
|
||||
if not yes then
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._DaycareGentlemanComeAgainText or "come again.", done))
|
||||
return
|
||||
end
|
||||
if #game.save.party < 2 then
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._DaycareGentlemanOnlyHaveOneMonText
|
||||
or "You only have one\nPOKéMON with you.", done))
|
||||
return
|
||||
end
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._DaycareGentlemanWhichMonText or "Which POKéMON\nshould I raise?",
|
||||
function()
|
||||
local PartyMenu = require("src.ui.PartyMenu")
|
||||
game.stack:push(PartyMenu.new(game, {
|
||||
pickOnly = true,
|
||||
onSwitch = function(mon)
|
||||
for i, m in ipairs(game.save.party) do
|
||||
if m == mon then table.remove(game.save.party, i) break end
|
||||
end
|
||||
local name = monName(mon)
|
||||
-- depositLevel = wDayCareMonBoxLevel at deposit time
|
||||
game.save.daycare = {
|
||||
mon = mon, steps = 0, depositLevel = mon.level,
|
||||
}
|
||||
game.stack:push(TextBox.new(game,
|
||||
fillDaycareText(
|
||||
t._DaycareGentlemanWillLookAfterMonText
|
||||
or "Fine, I'll look\nafter {RAM:wNameBuffer}\nfor a while.",
|
||||
{ player = playerName, wNameBuffer = name }),
|
||||
function()
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._DaycareGentlemanComeSeeMeInAWhileText
|
||||
or "Come see me in\na while.", done))
|
||||
end))
|
||||
end,
|
||||
}))
|
||||
end))
|
||||
end }))
|
||||
end,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -61,9 +61,61 @@ M.ROUTE_12_SUPER_ROD_HOUSE = {
|
||||
M.ROUTE_12_SUPER_ROD_HOUSE.talk.TEXT_ROUTE12SUPERRODHOUSE_FISHING_GURU[9] =
|
||||
{ "show_text", "_Route12SuperRodHouseFishingGuruTryFishingText" }
|
||||
|
||||
-- -------------------------------------------------------------------
|
||||
-- Pokemon Tower 5F purified zone (scripts/PokemonTower5F.asm
|
||||
-- PokemonTower5FDefaultScript): the 2x2 center pad heals the party once
|
||||
-- per visit. EVENT_IN_PURIFIED_ZONE latches until the player steps off;
|
||||
-- while on the pad the map script also sets BIT_NO_BATTLES (we return
|
||||
-- true from onStep so wild encounters are skipped the same way).
|
||||
-- -------------------------------------------------------------------
|
||||
|
||||
local TOWER_5F_PURIFIED = {
|
||||
[10 * 256 + 8] = true, [11 * 256 + 8] = true,
|
||||
[10 * 256 + 9] = true, [11 * 256 + 9] = true,
|
||||
}
|
||||
|
||||
-- HealParty -> GBFadeOutToWhite -> Delay3 -> Delay3 -> GBFadeInFromWhite
|
||||
-- -> TEXT_POKEMONTOWER5F_PURIFIEDZONE (no Music_PkmnHealed).
|
||||
local TOWER_5F_HEAL = {
|
||||
{ "heal_party" },
|
||||
{ "fade", "out", "white" },
|
||||
{ "wait", 3 },
|
||||
{ "wait", 3 },
|
||||
{ "fade", "in", "white" },
|
||||
{ "show_text", "_PokemonTower5FPurifiedZoneText" },
|
||||
}
|
||||
|
||||
M.POKEMON_TOWER_5F = {
|
||||
onStep = function(game, ow, x, y)
|
||||
if not TOWER_5F_PURIFIED[x * 256 + y] then
|
||||
game.save.flags.EVENT_IN_PURIFIED_ZONE = nil
|
||||
return false
|
||||
end
|
||||
if game.save.flags.EVENT_IN_PURIFIED_ZONE then
|
||||
return true
|
||||
end
|
||||
if ow.runner and ow.runner:isRunning() then return false end
|
||||
game.save.flags.EVENT_IN_PURIFIED_ZONE = true
|
||||
if ow.runner then
|
||||
ow.runner:run(TOWER_5F_HEAL)
|
||||
elseif ow.queueScript then
|
||||
ow:queueScript(TOWER_5F_HEAL)
|
||||
end
|
||||
return true
|
||||
end,
|
||||
}
|
||||
|
||||
-- -------------------------------------------------------------------
|
||||
-- The ghost Marowak (scripts/PokemonTower6F.asm): blocks the stairs at
|
||||
-- (10,16) until identified with the Silph Scope and defeated.
|
||||
-- (10,16) until defeated.
|
||||
--
|
||||
-- PokemonTower6FDefaultScript starts the RESTLESS SOUL battle with NO
|
||||
-- Silph Scope check at the trigger -- the scope only decides whether the
|
||||
-- battle is disguised (IsGhostBattle -> makeGhost: "too scared to move",
|
||||
-- balls dodged). An earlier version of this port turned the player back
|
||||
-- without the scope and never opened the battle, which made 6F
|
||||
-- impassable on any route that skips Rocket Hideout; vanilla lets the
|
||||
-- battle open and a POKE_DOLL end it (see wBattleResult below).
|
||||
-- -------------------------------------------------------------------
|
||||
|
||||
M.POKEMON_TOWER_6F = {
|
||||
@@ -71,26 +123,32 @@ M.POKEMON_TOWER_6F = {
|
||||
if game.save.flags.EVENT_BEAT_GHOST_MAROWAK then return false end
|
||||
if x ~= 10 or y ~= 16 then return false end
|
||||
local TextBox = require("src.render.TextBox")
|
||||
if not game.save.inventory.SILPH_SCOPE then
|
||||
game.stack:push(TextBox.new(game,
|
||||
"A GHOST blocks\nthe way...\fDarn! You can't\nidentify it!",
|
||||
function()
|
||||
local back = ow.player.facing == "up" and "down" or "up"
|
||||
ow:scriptMove(ow.player, back, 1)
|
||||
end))
|
||||
return true
|
||||
end
|
||||
local t = game.data.text
|
||||
game.stack:push(TextBox.new(game,
|
||||
"The GHOST was\nMAROWAK!\fThe restless soul\nattacks!", function()
|
||||
t._PokemonTower6FBeGoneText or "Be gone...\nIntruders...", function()
|
||||
local BattleState = require("src.battle.BattleState")
|
||||
local battle = BattleState.newWild(game, "MAROWAK", 30)
|
||||
if not game.save.inventory.SILPH_SCOPE then
|
||||
battle:makeGhost()
|
||||
end
|
||||
battle.onFinish = function(result)
|
||||
if result == "win" then
|
||||
-- wBattleResult parity (PokemonTower6FMarowakBattleScript's
|
||||
-- "and a / jr nz"): losing writes $1 and running writes $2, but
|
||||
-- ItemUsePokeDoll ends the battle WITHOUT touching it, so the
|
||||
-- script reads 0 -- defeated. That is the famous Poke Doll
|
||||
-- trick, and the speedrun route this bot follows depends on it.
|
||||
if result == "win" or battle.pokeDollEscape then
|
||||
game.save.flags.EVENT_BEAT_GHOST_MAROWAK = true
|
||||
game.stack:push(TextBox.new(game,
|
||||
"The restless soul\ncalmed down and\ndeparted!"))
|
||||
t._PokemonTower6FSoulWasCalmedText
|
||||
or "The mother's soul\nwas calmed.\012It departed to\nthe afterlife!"))
|
||||
elseif result ~= "lose" then
|
||||
-- .did_not_defeat: one simulated step right, off the trigger,
|
||||
-- so fleeing does not leave you standing on a cell that
|
||||
-- immediately re-fires.
|
||||
ow:scriptMove(ow.player, "right", 1)
|
||||
end
|
||||
ow:afterBattle(result)
|
||||
ow:afterBattle(result, battle)
|
||||
end
|
||||
game.stack:push(battle)
|
||||
end))
|
||||
@@ -136,18 +194,20 @@ local WALK_DIRVEC = { up = { 0, -1 }, down = { 0, 1 }, left = { -1, 0 }, right =
|
||||
local WALK_OPP = { up = "down", down = "up", left = "right", right = "left" }
|
||||
local WALK_ORDER = { "up", "down", "left", "right" }
|
||||
|
||||
local function elevatorWalkOut(ow, floor)
|
||||
local m, p = ow.map, ow.player
|
||||
-- .UpdateWarp is run twice, so BOTH car warp entries get the same
|
||||
-- (warp id, map id): point every exit warp at the picked floor's
|
||||
-- elevator-door warp (the reciprocal warp found while building the
|
||||
-- menu). The car map's def is shared generated data, but its own
|
||||
-- warps are only ever read from inside the car, and this rewrite runs
|
||||
-- on every ride before the walk-out fires, so it is self-correcting.
|
||||
for _, w in ipairs(m.def.warps) do
|
||||
-- .UpdateWarp: point EVERY car exit warp at the same floor's elevator
|
||||
-- door (warp id, map id). Shared generated map data, but the car's
|
||||
-- warps are only read from inside the car; rides rewrite them again.
|
||||
local function elevatorSetExit(ow, floor)
|
||||
if not floor then return end
|
||||
for _, w in ipairs(ow.map.def.warps) do
|
||||
w.destMap = floor.map
|
||||
w.destWarp = floor.warpIdx
|
||||
end
|
||||
end
|
||||
|
||||
local function elevatorWalkOut(ow, floor)
|
||||
local m, p = ow.map, ow.player
|
||||
elevatorSetExit(ow, floor)
|
||||
-- leave by the exit tile under the player (they warped in onto one),
|
||||
-- else the nearest
|
||||
local door
|
||||
@@ -185,37 +245,68 @@ local function elevatorWalkOut(ow, floor)
|
||||
end)
|
||||
end
|
||||
|
||||
local function elevatorFloors(elevatorMapId, game)
|
||||
local floors = {}
|
||||
for mapId, def in pairs(game.data.maps) do
|
||||
for i, w in ipairs(def.warps) do
|
||||
if w.destMap == elevatorMapId then
|
||||
-- short floor token pokered actually prints, e.g.
|
||||
-- SILPH_CO_10F -> "10F", ROCKET_HIDEOUT_B2F -> "B2F"
|
||||
local token = mapId:match("_([^_]+)$") or mapId
|
||||
-- warpIdx: this floor's warp back into the elevator IS the
|
||||
-- warp the car's rewritten exit lands on (the reciprocal
|
||||
-- pair), matching wElevatorWarpMaps' (warp id, map id)
|
||||
table.insert(floors,
|
||||
{ map = mapId, x = w.x, y = w.y, token = token, warpIdx = i })
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
-- numeric floor order (SilphCoElevatorFloors' FLOOR_1F..FLOOR_11F),
|
||||
-- not lexicographic -- otherwise 10F/11F sort before 2F..9F
|
||||
table.sort(floors, function(a, b)
|
||||
return (tonumber(a.token:match("%d+")) or 0) <
|
||||
(tonumber(b.token:match("%d+")) or 0)
|
||||
end)
|
||||
return floors
|
||||
end
|
||||
|
||||
local function elevatorSeedExit(ow, floors, fromMapId)
|
||||
-- Seed a walk-out destination before the menu (or key-gate text):
|
||||
-- entry floor when known, else the first listed floor (1F). Choosing
|
||||
-- a floor still rewrites via elevatorWalkOut; B-cancel / no-key leave
|
||||
-- keeps this seed so walking out of the car cannot hit a missing ROM
|
||||
-- placeholder (#123) or the car's static default floor (#90: Rocket
|
||||
-- Hideout defaults to B1F even when entered from B2F/B4F).
|
||||
local exitFloor = floors[1]
|
||||
if fromMapId then
|
||||
for _, f in ipairs(floors) do
|
||||
if f.map == fromMapId then exitFloor = f break end
|
||||
end
|
||||
end
|
||||
elevatorSetExit(ow, exitFloor)
|
||||
return exitFloor
|
||||
end
|
||||
|
||||
local function elevator(elevatorMapId, keyGate, preFrames)
|
||||
return {
|
||||
onEnter = function(game, ow)
|
||||
-- fromMapId: the floor the player just left (setMap passes it), so a
|
||||
-- B-cancel can still walk out onto a real map. Silph's ROM car warps
|
||||
-- default to UNUSED_MAP_ED, which is not in Data.maps -- Warp.resolve
|
||||
-- asserted and hard-crashed (#123).
|
||||
onEnter = function(game, ow, fromMapId)
|
||||
local floors = elevatorFloors(elevatorMapId, game)
|
||||
elevatorSeedExit(ow, floors, fromMapId)
|
||||
-- Rocket Hideout: without LIFT_KEY the panel only prints the need-
|
||||
-- a-key line (scripts/RocketHideoutElevator.asm). Exit warps are
|
||||
-- still seeded above so walking out returns to the entry floor
|
||||
-- instead of the car's ROM default (B1F) — #90 / #105.
|
||||
if keyGate and not game.save.inventory[keyGate.item] then
|
||||
local TextBox = require("src.render.TextBox")
|
||||
game.stack:push(TextBox.new(game,
|
||||
game.data.text[keyGate.text] or "It appears to\nneed a key."))
|
||||
return
|
||||
end
|
||||
local floors = {}
|
||||
for mapId, def in pairs(game.data.maps) do
|
||||
for i, w in ipairs(def.warps) do
|
||||
if w.destMap == elevatorMapId then
|
||||
-- short floor token pokered actually prints, e.g.
|
||||
-- SILPH_CO_10F -> "10F", ROCKET_HIDEOUT_B2F -> "B2F"
|
||||
local token = mapId:match("_([^_]+)$") or mapId
|
||||
-- warpIdx: this floor's warp back into the elevator IS the
|
||||
-- warp the car's rewritten exit lands on (the reciprocal
|
||||
-- pair), matching wElevatorWarpMaps' (warp id, map id)
|
||||
table.insert(floors,
|
||||
{ map = mapId, x = w.x, y = w.y, token = token, warpIdx = i })
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
-- numeric floor order (SilphCoElevatorFloors' FLOOR_1F..FLOOR_11F),
|
||||
-- not lexicographic -- otherwise 10F/11F sort before 2F..9F
|
||||
table.sort(floors, function(a, b)
|
||||
return (tonumber(a.token:match("%d+")) or 0) <
|
||||
(tonumber(b.token:match("%d+")) or 0)
|
||||
end)
|
||||
local items = {}
|
||||
for _, f in ipairs(floors) do
|
||||
table.insert(items, { label = f.token, value = f })
|
||||
@@ -248,7 +339,8 @@ local function elevator(elevatorMapId, keyGate, preFrames)
|
||||
end,
|
||||
onCancel = function()
|
||||
-- DisplayElevatorFloorMenu: `ret c` on B -- no warp, nothing
|
||||
-- happens, the player just stays in the car
|
||||
-- happens, the player just stays in the car (exit warps were
|
||||
-- already seeded to the entry floor above)
|
||||
end,
|
||||
}))
|
||||
end,
|
||||
@@ -260,6 +352,98 @@ M.CELADON_MART_ELEVATOR = elevator("CELADON_MART_ELEVATOR", nil, 9)
|
||||
M.ROCKET_HIDEOUT_ELEVATOR = elevator("ROCKET_HIDEOUT_ELEVATOR",
|
||||
{ item = "LIFT_KEY", text = "_RocketHideoutElevatorAppearsToNeedKeyText" })
|
||||
|
||||
-- -------------------------------------------------------------------
|
||||
-- Rocket Hideout B4F (scripts/RocketHideoutB4F.asm):
|
||||
-- Rocket3's after-battle text_asm drops the LIFT KEY item ball
|
||||
-- (CheckAndSetEvent EVENT_ROCKET_DROPPED_LIFT_KEY / ShowObject
|
||||
-- TOGGLE_ROCKET_HIDEOUT_B4F_ITEM_5). Both start hidden in the map
|
||||
-- objects; without this talk side-effect the key never appears (#90,
|
||||
-- #105).
|
||||
-- Giovanni's post-battle script likewise ShowObject's the Silph Scope
|
||||
-- after the hope-we-meet-again line (TOGGLE_ROCKET_HIDEOUT_B4F_ITEM_4).
|
||||
-- -------------------------------------------------------------------
|
||||
|
||||
M.ROCKET_HIDEOUT_B4F = {
|
||||
talk = {
|
||||
TEXT_ROCKETHIDEOUTB4F_ROCKET3 = function(game, ow, npc, done)
|
||||
if not ow:trainerDefeated(npc) then
|
||||
ow:engageTrainer(npc, done)
|
||||
return
|
||||
end
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local t = game.data.text
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._RocketHideoutB4FRocket3AfterBattleText
|
||||
or "Oh no! I dropped\nthe LIFT KEY!",
|
||||
function()
|
||||
-- CheckAndSetEvent EVENT_ROCKET_DROPPED_LIFT_KEY: first talk
|
||||
-- after the win reveals the ball; later talks only reprint.
|
||||
if not game.save.flags.EVENT_ROCKET_DROPPED_LIFT_KEY then
|
||||
game.save.flags.EVENT_ROCKET_DROPPED_LIFT_KEY = true
|
||||
local Commands = require("src.script.Commands")
|
||||
Commands.show_object(
|
||||
{ game = game, save = game.save, overworld = ow },
|
||||
"ROCKET_HIDEOUT_B4F", "ROCKETHIDEOUTB4F_LIFT_KEY")
|
||||
end
|
||||
done()
|
||||
end))
|
||||
end,
|
||||
|
||||
TEXT_ROCKETHIDEOUTB4F_GIOVANNI = function(game, ow, npc, done)
|
||||
-- Giovanni has no trainer-header row (def_trainers 2); his text_asm
|
||||
-- owns both the engage and the BeatGiovanniScript aftermath.
|
||||
if ow:trainerDefeated(npc)
|
||||
or game.save.flags.EVENT_BEAT_ROCKET_HIDEOUT_GIOVANNI then
|
||||
local TextBox = require("src.render.TextBox")
|
||||
game.stack:push(TextBox.new(game,
|
||||
game.data.text._RocketHideoutB4FGiovanniHopeWeMeetAgainText
|
||||
or "I hope we meet\nagain...", done))
|
||||
return
|
||||
end
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local BattleState = require("src.battle.BattleState")
|
||||
local t = game.data.text
|
||||
local impressed = t._RocketHideoutB4FGiovanniImpressedYouGotHereText
|
||||
or "So! I must say, I\nam impressed you\ngot here!"
|
||||
local cannotBe = t._RocketHideoutB4FGiovanniWhatCannotBeText
|
||||
or "WHAT!\nThis cannot be!"
|
||||
local hope = t._RocketHideoutB4FGiovanniHopeWeMeetAgainText
|
||||
or "I hope we meet\nagain..."
|
||||
game.stack:push(TextBox.new(game, impressed, function()
|
||||
local battle = BattleState.newTrainer(game, "OPP_GIOVANNI", 1)
|
||||
battle.onFinish = function(result)
|
||||
if result ~= "win" then
|
||||
ow:afterBattle(result, battle)
|
||||
done()
|
||||
return
|
||||
end
|
||||
game.save.defeatedTrainers[npc.id] = true
|
||||
game.save.flags.EVENT_BEAT_ROCKET_HIDEOUT_GIOVANNI = true
|
||||
-- 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()
|
||||
local Transition = require("src.render.Transition")
|
||||
game.stack:push(Transition.new(game, function()
|
||||
local Commands = require("src.script.Commands")
|
||||
local ctx = { game = game, save = game.save, overworld = ow }
|
||||
Commands.hide_object(ctx, "ROCKET_HIDEOUT_B4F",
|
||||
"ROCKETHIDEOUTB4F_GIOVANNI")
|
||||
Commands.show_object(ctx, "ROCKET_HIDEOUT_B4F",
|
||||
"ROCKETHIDEOUTB4F_SILPH_SCOPE")
|
||||
end, function()
|
||||
ow:afterBattle(result, battle)
|
||||
done()
|
||||
end))
|
||||
end))
|
||||
end))
|
||||
end
|
||||
ow:pushBattle(battle)
|
||||
end))
|
||||
end,
|
||||
},
|
||||
}
|
||||
|
||||
-- -------------------------------------------------------------------
|
||||
-- Game Corner coins, prizes, and the rocket-poster switch that reveals
|
||||
-- the hideout stairs (scripts/GameCorner.asm, data/events/prizes.asm +
|
||||
@@ -272,14 +456,23 @@ M.GAME_CORNER = {
|
||||
-- EVENT_FOUND_ROCKET_HIDEOUT is unset, $43 after)
|
||||
onEnter = function(game, ow)
|
||||
local poster = game.data.field.gameCornerPoster
|
||||
if not poster then return end
|
||||
local block = game.save.flags[poster.event] and poster.openBlock
|
||||
or poster.closedBlock
|
||||
ow:replaceBlock(poster.x, poster.y, block)
|
||||
if poster then
|
||||
local block = game.save.flags[poster.event] and poster.openBlock
|
||||
or poster.closedBlock
|
||||
ow:replaceBlock(poster.x, poster.y, block)
|
||||
end
|
||||
-- pick this visit's lucky slot machine
|
||||
-- (wLuckySlotHiddenEventIndex, engine/slots/game_corner_slots2.asm)
|
||||
local seats = game.data.field.slotMachines.GAME_CORNER
|
||||
ow.luckySlot = love.math.random(1, #seats)
|
||||
-- #131: pre-#50 saves beat the poster grunt (defeatedTrainers) but
|
||||
-- never hid him; clear the tile if he is already marked defeated
|
||||
local rocketId = "GAME_CORNER_obj_11"
|
||||
if game.save.defeatedTrainers and game.save.defeatedTrainers[rocketId] then
|
||||
local Commands = require("src.script.Commands")
|
||||
Commands.hide_object({ game = game, save = game.save, overworld = ow },
|
||||
"GAME_CORNER", "GAMECORNER_ROCKET")
|
||||
end
|
||||
end,
|
||||
talk = {
|
||||
-- the poster bg event: pressing A reveals the hidden switch
|
||||
@@ -303,6 +496,48 @@ M.GAME_CORNER = {
|
||||
done()
|
||||
end))
|
||||
end,
|
||||
-- the grunt guarding the poster (GameCornerRocketText /
|
||||
-- GameCornerRocketBattleScript / GameCornerRocketExitScript): after
|
||||
-- losing he warns the BOSS and leaves the floor for good, freeing
|
||||
-- the tile in front of the hideout switch
|
||||
TEXT_GAMECORNER_ROCKET = function(game, ow, npc, done)
|
||||
local Commands = require("src.script.Commands")
|
||||
local function hideRocket()
|
||||
Commands.hide_object({ game = game, save = game.save,
|
||||
overworld = ow },
|
||||
"GAME_CORNER", "GAMECORNER_ROCKET")
|
||||
end
|
||||
-- already beaten: hide anyway so pre-#50 saves that only have
|
||||
-- defeatedTrainers (no objectToggles hide) clear the poster tile
|
||||
if ow:trainerDefeated(npc) then
|
||||
hideRocket()
|
||||
done()
|
||||
return
|
||||
end
|
||||
ow:engageTrainer(npc, function()
|
||||
if not ow:trainerDefeated(npc) then
|
||||
done()
|
||||
return
|
||||
end
|
||||
local TextBox = require("src.render.TextBox")
|
||||
game.stack:push(TextBox.new(game,
|
||||
game.data.text._GameCornerRocketAfterBattleText
|
||||
or "Our hideout might\nbe discovered! I\nbetter tell BOSS!",
|
||||
function()
|
||||
-- #198: GameCornerRocketExitScript (scripts/GameCorner.asm)
|
||||
-- ApplyMovementData walks the grunt one tile UP into the poster
|
||||
-- (the hideout's secret entrance at 9,4) before HideObject, so
|
||||
-- he leaves the floor rather than popping out of existence on
|
||||
-- (9,5). scriptMove locks player input (#scriptMoves>0) and
|
||||
-- ignores collision, so we despawn + unfreeze (done) only once
|
||||
-- the step lands.
|
||||
ow:scriptMove(npc, "up", 1, function()
|
||||
hideRocket()
|
||||
done()
|
||||
end)
|
||||
end))
|
||||
end)
|
||||
end,
|
||||
TEXT_GAMECORNER_CLERK1 = function(game, ow, npc, done)
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local ChoiceBox = require("src.ui.ChoiceBox")
|
||||
@@ -349,53 +584,93 @@ M.GAME_CORNER = {
|
||||
},
|
||||
}
|
||||
|
||||
-- Red-version prize lists (data/events/prizes.asm, prize_mon_levels.asm)
|
||||
local PRIZES = {
|
||||
-- Game Corner prize lists (data/events/prizes.asm, prize_mon_levels.asm).
|
||||
-- The six mon prizes differ between Red and Blue; the three TM prizes are
|
||||
-- identical, so they are shared and appended to each version's mon list.
|
||||
local PRIZE_TMS = {
|
||||
{ kind = "item", item = "TM_DRAGON_RAGE", cost = 3300 },
|
||||
{ kind = "item", item = "TM_HYPER_BEAM", cost = 5500 },
|
||||
{ kind = "item", item = "TM_SUBSTITUTE", cost = 7700 },
|
||||
}
|
||||
local RED_PRIZES = {
|
||||
{ kind = "mon", species = "ABRA", level = 9, cost = 180 },
|
||||
{ kind = "mon", species = "CLEFAIRY", level = 8, cost = 500 },
|
||||
{ kind = "mon", species = "NIDORINA", level = 17, cost = 1200 },
|
||||
{ kind = "mon", species = "DRATINI", level = 18, cost = 2800 },
|
||||
{ kind = "mon", species = "SCYTHER", level = 25, cost = 5500 },
|
||||
{ kind = "mon", species = "PORYGON", level = 26, cost = 9999 },
|
||||
{ kind = "item", item = "TM_DRAGON_RAGE", cost = 3300 },
|
||||
{ kind = "item", item = "TM_HYPER_BEAM", cost = 5500 },
|
||||
{ kind = "item", item = "TM_SUBSTITUTE", cost = 7700 },
|
||||
PRIZE_TMS[1], PRIZE_TMS[2], PRIZE_TMS[3],
|
||||
}
|
||||
local BLUE_PRIZES = {
|
||||
{ kind = "mon", species = "ABRA", level = 6, cost = 120 },
|
||||
{ kind = "mon", species = "CLEFAIRY", level = 12, cost = 750 },
|
||||
{ kind = "mon", species = "NIDORINO", level = 17, cost = 1200 },
|
||||
{ kind = "mon", species = "PINSIR", level = 20, cost = 2500 },
|
||||
{ kind = "mon", species = "DRATINI", level = 24, cost = 4600 },
|
||||
{ kind = "mon", species = "PORYGON", level = 18, cost = 6500 },
|
||||
PRIZE_TMS[1], PRIZE_TMS[2], PRIZE_TMS[3],
|
||||
}
|
||||
|
||||
local function activePrizes()
|
||||
return require("src.core.GameVersion").isBlue() and BLUE_PRIZES or RED_PRIZES
|
||||
end
|
||||
|
||||
-- Prize counters (engine/menus/prize_menu.asm CeladonPrizeMenu; the prize
|
||||
-- list itself is data/events/prizes.asm, prize_mon_levels.asm). Gen1 gates
|
||||
-- the prize window on the COIN CASE: it does IsItemInBag COIN_CASE first, and
|
||||
-- with no case prints RequireCoinCaseText and returns without ever opening a
|
||||
-- window; only with the case does it print ExchangeCoinsForPrizesText and then
|
||||
-- show the prizes. #194: the port used to open the window unconditionally and
|
||||
-- skip both text boxes.
|
||||
local function prizeCounter(game, ow, npc, done)
|
||||
local ListMenu = require("src.ui.ListMenu")
|
||||
local Commands = require("src.script.Commands")
|
||||
local items = {}
|
||||
for _, p in ipairs(PRIZES) do
|
||||
local label
|
||||
if p.kind == "mon" then
|
||||
label = ("%s L%d"):format(game.data.pokemon[p.species].name, p.level)
|
||||
else
|
||||
label = game.data.items[p.item].name
|
||||
end
|
||||
table.insert(items, { label = label, right = tostring(p.cost), value = p })
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local t = game.data.text
|
||||
-- IsItemInBag COIN_CASE: without the case, deny and open no window
|
||||
-- (COIN_CASE is a numeric count in save.inventory, nil when absent).
|
||||
if not game.save.inventory.COIN_CASE then
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._RequireCoinCaseText or "A COIN CASE is\nrequired!", done))
|
||||
return
|
||||
end
|
||||
local list
|
||||
list = ListMenu.new(game, "PRIZES (COINS)", items, {
|
||||
footer = ("COINS %d"):format(game.save.coins or 0),
|
||||
onChoose = function(item)
|
||||
local p = item.value
|
||||
if (game.save.coins or 0) < p.cost then
|
||||
list.footer = "Not enough coins!"
|
||||
return
|
||||
-- ExchangeCoinsForPrizesText plays before the prize window opens.
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._ExchangeCoinsForPrizesText or "We exchange your\ncoins for prizes.",
|
||||
function()
|
||||
local items = {}
|
||||
for _, p in ipairs(activePrizes()) do
|
||||
local label
|
||||
if p.kind == "mon" then
|
||||
label = ("%s L%d"):format(game.data.pokemon[p.species].name, p.level)
|
||||
else
|
||||
label = game.data.items[p.item].name
|
||||
end
|
||||
table.insert(items,
|
||||
{ label = label, right = tostring(p.cost), value = p })
|
||||
end
|
||||
game.save.coins = game.save.coins - p.cost
|
||||
if p.kind == "mon" then
|
||||
Commands.give_pokemon({ save = game.save, game = game },
|
||||
p.species, p.level)
|
||||
else
|
||||
game.save.inventory[p.item] = (game.save.inventory[p.item] or 0) + 1
|
||||
end
|
||||
list.footer = ("Got it! COINS %d"):format(game.save.coins)
|
||||
end,
|
||||
onCancel = done,
|
||||
})
|
||||
game.stack:push(list)
|
||||
local list
|
||||
list = ListMenu.new(game, "PRIZES (COINS)", items, {
|
||||
footer = ("COINS %d"):format(game.save.coins or 0),
|
||||
onChoose = function(item)
|
||||
local p = item.value
|
||||
if (game.save.coins or 0) < p.cost then
|
||||
list.footer = "Not enough coins!"
|
||||
return
|
||||
end
|
||||
game.save.coins = game.save.coins - p.cost
|
||||
if p.kind == "mon" then
|
||||
Commands.give_pokemon({ save = game.save, game = game },
|
||||
p.species, p.level)
|
||||
else
|
||||
game.save.inventory[p.item] = (game.save.inventory[p.item] or 0) + 1
|
||||
end
|
||||
list.footer = ("Got it! COINS %d"):format(game.save.coins)
|
||||
end,
|
||||
onCancel = done,
|
||||
})
|
||||
game.stack:push(list)
|
||||
end))
|
||||
end
|
||||
|
||||
M.GAME_CORNER_PRIZE_ROOM = {
|
||||
@@ -411,18 +686,63 @@ M.GAME_CORNER_PRIZE_ROOM = {
|
||||
-- and the player steps off the dock, the ship sets sail.
|
||||
-- -------------------------------------------------------------------
|
||||
|
||||
-- the ship's hull/deck blocks (block cols 5-8, rows 1-2) and the water
|
||||
-- that replaces them once she sails (the surrounding blocks of each row)
|
||||
local DOCK_SHIP_BLOCKS = {
|
||||
{ bx = 5, by = 1, water = 1 }, { bx = 6, by = 1, water = 1 },
|
||||
{ bx = 7, by = 1, water = 1 }, { bx = 8, by = 1, water = 1 },
|
||||
{ bx = 5, by = 2, water = 13 }, { bx = 6, by = 2, water = 13 },
|
||||
{ bx = 7, by = 2, water = 13 }, { bx = 8, by = 2, water = 13 },
|
||||
}
|
||||
|
||||
M.VERMILION_DOCK = {
|
||||
onEnter = function(game, ow)
|
||||
if game.save.flags.EVENT_SS_ANNE_LEFT then
|
||||
local Flags = require("src.script.Flags")
|
||||
local f = game.save.flags
|
||||
if Flags.get(game.save, "EVENT_SS_ANNE_LEFT") then
|
||||
-- the ship is long gone: erase her right away, and anyone who
|
||||
-- still lands here is sent back out past the guard
|
||||
for _, b in ipairs(DOCK_SHIP_BLOCKS) do
|
||||
ow.map:setBlock(b.bx, b.by, b.water)
|
||||
end
|
||||
ow.map.renderer:rebuild()
|
||||
local TextBox = require("src.render.TextBox")
|
||||
game.stack:push(TextBox.new(game,
|
||||
game.data.text._VermilionCitySailor1ShipSetSailText
|
||||
or "The ship set sail.", function()
|
||||
ow:startWarpTo("VERMILION_CITY", 19, 30, "up")
|
||||
ow:startWarpTo("VERMILION_CITY", 18, 29, "up")
|
||||
end))
|
||||
elseif game.save.flags.EVENT_GOT_HM01 then
|
||||
game.save.flags.EVENT_SS_ANNE_LEFT = true
|
||||
require("src.core.Sound").play(game.data, "SS_Anne_Horn")
|
||||
elseif f.EVENT_GOT_HM01 and ow.player.cellY == 2 then
|
||||
-- VermilionDockSSAnneLeavesScript: only stepping OFF the ship
|
||||
-- triggers the departure (wDestinationWarpID == 1 in pokered) --
|
||||
-- Music_Surfing plays for the sail-away cutscene, smoke puffs
|
||||
-- drift off the funnel, the horn blows, the ship is erased to
|
||||
-- open water, and the player is walked off the dock into the
|
||||
-- city past the guard (VermilionCity's
|
||||
-- SCRIPT_VERMILIONCITY_PLAYER_EXIT_SHIP walk)
|
||||
Flags.set(game.save, "EVENT_SS_ANNE_LEFT")
|
||||
local Music = require("src.core.Music")
|
||||
Music.stop()
|
||||
Music.play(game.data, "Music_Surfing")
|
||||
local function puff(n, cx)
|
||||
if n <= 0 then return end
|
||||
ow:startDustAnim(cx, 1, function() puff(n - 1, cx + 2) end)
|
||||
end
|
||||
puff(3, 15)
|
||||
local rows = {}
|
||||
rows[#rows + 1] = { "wait", 100 }
|
||||
rows[#rows + 1] = { "play_sound", "SS_Anne_Horn" }
|
||||
for _, b in ipairs(DOCK_SHIP_BLOCKS) do
|
||||
rows[#rows + 1] = { "replace_block", b.bx, b.by, b.water }
|
||||
end
|
||||
rows[#rows + 1] = { "wait", 30 }
|
||||
rows[#rows + 1] = { "move_player", "up", 2 }
|
||||
-- keep Music_Surfing across the city warp (pokered stays on it
|
||||
-- through the exit-ship walk)
|
||||
rows[#rows + 1] = { "play_music", "Music_Surfing", { keep = true } }
|
||||
rows[#rows + 1] = { "warp", "VERMILION_CITY", 18, 31, "up" }
|
||||
rows[#rows + 1] = { "move_player", "up", 2 }
|
||||
ow:queueScript(rows)
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
@@ -134,12 +134,19 @@ M.MT_MOON_POKECENTER = {
|
||||
-- Karate Master, take HITMONLEE or HITMONCHAN (the other disappears)
|
||||
-- -------------------------------------------------------------------
|
||||
|
||||
-- ownBall/otherBall keep their spots for signature symmetry; only ownBall
|
||||
-- is ever hidden (Gen1 removes just the chosen ball).
|
||||
local function dojoBall(species, ownBall, otherBall, askKey)
|
||||
return function(game, ow, npc, done)
|
||||
local t = text(game)
|
||||
local flags = game.save.flags
|
||||
-- Already took one prize: the OTHER ball is still on the mat, so
|
||||
-- talking to it now gives the "Better not get greedy..." refusal
|
||||
-- (FightingDojo.asm .gotThePokemon / _FightingDojoBetterNotGetGreedyText),
|
||||
-- not a silent no-op (#197).
|
||||
if flags.EVENT_GOT_HITMONLEE or flags.EVENT_GOT_HITMONCHAN then
|
||||
done()
|
||||
push(game, t._FightingDojoBetterNotGetGreedyText
|
||||
or "Better not get\ngreedy...", done)
|
||||
return
|
||||
end
|
||||
if not flags.EVENT_BEAT_KARATE_MASTER then
|
||||
@@ -153,8 +160,10 @@ local function dojoBall(species, ownBall, otherBall, askKey)
|
||||
local Commands = require("src.script.Commands")
|
||||
local ctx = { save = game.save, game = game, overworld = ow }
|
||||
Commands.give_pokemon(ctx, species, 30)
|
||||
-- Hide ONLY the chosen ball; the other stays (FightingDojo.asm hides
|
||||
-- just the picked object's index) and routes to the greedy line above
|
||||
-- when talked to (#197).
|
||||
Commands.hide_object(ctx, "FIGHTING_DOJO", ownBall)
|
||||
Commands.hide_object(ctx, "FIGHTING_DOJO", otherBall)
|
||||
push(game, ("%s got\n%s!"):format(game.save.player.name, species), done)
|
||||
end)
|
||||
end
|
||||
@@ -171,6 +180,21 @@ M.FIGHTING_DOJO = {
|
||||
"FIGHTINGDOJO_HITMONLEE_POKE_BALL",
|
||||
"_FightingDojoHitmonchanPokeBallText"),
|
||||
},
|
||||
-- The two dojo posters on the north wall (FightingDojo.asm bg_events at
|
||||
-- the top of the room, cells (4,0)/(5,0) directly above the prize balls)
|
||||
-- print _EnemiesOnEverySideText. The map extractor dropped the dojo
|
||||
-- bg_events (signs = {}), so wire the poster read here, the same
|
||||
-- facing-up + coord shape the mansion switches use. Reachable only once
|
||||
-- a claimed prize frees the ball cell below the poster (#197).
|
||||
onInteract = function(game, ow, fx, fy)
|
||||
if ow.player.facing ~= "up" then return false end
|
||||
if fy == 0 and (fx == 4 or fx == 5) then
|
||||
push(game, text(game)._EnemiesOnEverySideText
|
||||
or "Enemies on every\nside!")
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end,
|
||||
}
|
||||
|
||||
-- -------------------------------------------------------------------
|
||||
@@ -212,30 +236,31 @@ M.COPYCATS_HOUSE_2F = {
|
||||
talk = {
|
||||
TEXT_COPYCATSHOUSE2F_COPYCAT = function(game, ow, npc, done)
|
||||
local t = text(game)
|
||||
local Bag = require("src.inventory.Bag")
|
||||
if game.save.flags.EVENT_GOT_TM31 then
|
||||
push(game, t._CopycatsHouse2FCopycatWhirlingText
|
||||
or "Huh? Huh? Huh?", done)
|
||||
push(game, t._CopycatsHouse2FCopycatTM31Explanation2Text, done)
|
||||
return
|
||||
end
|
||||
if (game.save.inventory.POKE_DOLL or 0) > 0 then
|
||||
ask(game, t._CopycatsHouse2FCopycatPokeDollText
|
||||
or "Oh wow!\nA POKé DOLL!\fFor me?\nCan I have it?", function(yes)
|
||||
if not yes then done() return end
|
||||
game.save.inventory.POKE_DOLL = game.save.inventory.POKE_DOLL - 1
|
||||
if game.save.inventory.POKE_DOLL == 0 then
|
||||
game.save.inventory.POKE_DOLL = nil
|
||||
end
|
||||
if not require("src.inventory.Bag").add(game.save, "TM_MIMIC", 1) then
|
||||
push(game, "You don't have\nroom for TM31!", done)
|
||||
-- Mimic line first; auto-trade POKE DOLL for TM31 (no YES/NO).
|
||||
push(game, t._CopycatsHouse2FCopycatDoYouLikePokemonText, function()
|
||||
if (game.save.inventory.POKE_DOLL or 0) <= 0 then
|
||||
done()
|
||||
return
|
||||
end
|
||||
push(game, t._CopycatsHouse2FCopycatTM31PreReceiveText, function()
|
||||
if not Bag.add(game.save, "TM_MIMIC", 1) then
|
||||
push(game, t._CopycatsHouse2FCopycatTM31NoRoomText, done)
|
||||
return
|
||||
end
|
||||
game.stringBuffer = game.data.items.TM_MIMIC.name
|
||||
require("src.core.Sound").play(game.data, "Get_Item1")
|
||||
Bag.remove(game.save, "POKE_DOLL", 1)
|
||||
game.save.flags.EVENT_GOT_TM31 = true
|
||||
push(game, ("%s got\nTM31!"):format(game.save.player.name), done)
|
||||
push(game, t._CopycatsHouse2FCopycatReceivedTM31Text, function()
|
||||
push(game, t._CopycatsHouse2FCopycatTM31Explanation1Text, done)
|
||||
end)
|
||||
end)
|
||||
return
|
||||
end
|
||||
push(game, t._CopycatsHouse2FCopycatText
|
||||
or "I like to mimic\npeople!", done)
|
||||
end)
|
||||
end,
|
||||
},
|
||||
}
|
||||
@@ -457,6 +482,25 @@ M.ROUTE_24 = {
|
||||
battleOrDone()
|
||||
end,
|
||||
},
|
||||
-- Route24DefaultScript forces TEXT_ROUTE24_COOLTRAINER_M1 whenever the
|
||||
-- player stands on (10,15) in front of the recruiter while
|
||||
-- EVENT_GOT_NUGGET is unset (dbmapcoord 10,15), independent of facing
|
||||
-- or input -- he has no trainer header, so sight never engages him.
|
||||
onStep = function(game, ow, x, y)
|
||||
if game.save.flags.EVENT_GOT_NUGGET then return false end
|
||||
if not (x == 10 and y == 15) then return false end
|
||||
if ow.runner:isRunning() or ow.engaging then return false end
|
||||
local recruiter
|
||||
for _, npc in ipairs(ow.npcs) do
|
||||
if npc.def and npc.def.name == "ROUTE24_COOLTRAINER_M1" then
|
||||
recruiter = npc
|
||||
break
|
||||
end
|
||||
end
|
||||
if not recruiter then return false end
|
||||
ow:showMapText("TEXT_ROUTE24_COOLTRAINER_M1", recruiter)
|
||||
return true
|
||||
end,
|
||||
}
|
||||
|
||||
-- -------------------------------------------------------------------
|
||||
@@ -626,11 +670,153 @@ local function e4ExitSeal(flag, closedBlock, openBlock, dontRunText, autoFlag)
|
||||
}
|
||||
end
|
||||
|
||||
-- Lorelei/Bruno/Agatha EndBattleScript (pokered): EndTrainerBattle then
|
||||
-- DisplayTextID -> TalkToTrainer, which prints the AfterBattle text on a
|
||||
-- win. engageTrainer only shows the won line, so wrap talk like Lance /
|
||||
-- Game Corner Rocket and push header.after immediately after a win.
|
||||
local function e4LeaderTalk(afterLabel)
|
||||
return function(game, ow, npc, done)
|
||||
done = done or function() end
|
||||
if ow:trainerDefeated(npc) then
|
||||
local after = text(game)[afterLabel]
|
||||
if after then push(game, after, done) else done() end
|
||||
return
|
||||
end
|
||||
ow:engageTrainer(npc, function()
|
||||
if not ow:trainerDefeated(npc) then
|
||||
done()
|
||||
return
|
||||
end
|
||||
local after = text(game)[afterLabel]
|
||||
if after then push(game, after, done) else done() end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
M.LORELEIS_ROOM = e4ExitSeal("EVENT_BEAT_LORELEIS_ROOM_TRAINER_0", 0x24, 0x05,
|
||||
"_LoreleisRoomLoreleiDontRunAwayText", "EVENT_AUTOWALKED_INTO_LORELEIS_ROOM")
|
||||
M.LORELEIS_ROOM.talk = {
|
||||
TEXT_LORELEISROOM_LORELEI = e4LeaderTalk("_LoreleisRoomLoreleiAfterBattleText"),
|
||||
}
|
||||
M.BRUNOS_ROOM = e4ExitSeal("EVENT_BEAT_BRUNOS_ROOM_TRAINER_0", 0x24, 0x05,
|
||||
"_BrunosRoomBrunoDontRunAwayText", "EVENT_AUTOWALKED_INTO_BRUNOS_ROOM")
|
||||
M.BRUNOS_ROOM.talk = {
|
||||
TEXT_BRUNOSROOM_BRUNO = e4LeaderTalk("_BrunoAfterBattleText"),
|
||||
}
|
||||
M.AGATHAS_ROOM = e4ExitSeal("EVENT_BEAT_AGATHAS_ROOM_TRAINER_0", 0x3b, 0x0e,
|
||||
"_AgathasRoomAgathaDontRunAwayText", "EVENT_AUTOWALKED_INTO_AGATHAS_ROOM")
|
||||
M.AGATHAS_ROOM.talk = {
|
||||
TEXT_AGATHASROOM_AGATHA = e4LeaderTalk("_AgathaAfterBattleText"),
|
||||
}
|
||||
|
||||
-- -------------------------------------------------------------------
|
||||
-- Lance's room (scripts/LancesRoom.asm). Unlike the other three E4
|
||||
-- rooms this one gates its ENTRANCE, not its exit: the .blk ships with
|
||||
-- the arena doorway CLOSED (blocks $72/$73 at block (2,6)/(3,6), cells
|
||||
-- (4-7,12-13)), and LanceShowOrHideEntranceBlocks OPENS it ($31/$32)
|
||||
-- on every map load while EVENT_LANCES_ROOM_LOCK_DOOR is unset.
|
||||
-- Without this script the doorway never opened, so the whole arena --
|
||||
-- Lance AND both CHAMPIONS_ROOM warps at (5,0)/(6,0) -- was sealed off
|
||||
-- from the entrance hall and the league dead-ended here ("goto (6,11)
|
||||
-- unreachable on LANCES_ROOM").
|
||||
--
|
||||
-- LancesRoomDefaultScript's coordinate triggers, all inert once
|
||||
-- EVENT_BEAT_LANCE is set:
|
||||
-- (5,1)/(6,2) beside Lance -> his battle starts (a coordinate
|
||||
-- trigger, not a talk; victories.lua OPP_LANCE#1 sets
|
||||
-- EVENT_BEAT_LANCE on the win, so a loss re-arms)
|
||||
-- (5,11)/(6,11) the doorway -> CheckAndSetEvent
|
||||
-- EVENT_LANCES_ROOM_LOCK_DOOR: first crossing seals
|
||||
-- the door behind the player with SFX_GO_INSIDE
|
||||
-- (24,16) the entrance staircase -> WalkToLance: an auto-walk
|
||||
-- landing on (6,11). Vanilla WalkToLance_RLEList is
|
||||
-- up 12 / left 12 / down 7 / left 6 and skips collision
|
||||
-- through void tiles; our camera follows that literally
|
||||
-- and briefly shows the empty upper chamber (reads as
|
||||
-- "Gary's room"). We keep the same landing cell but
|
||||
-- route along the open-door floor corridor instead.
|
||||
-- -------------------------------------------------------------------
|
||||
|
||||
-- Floor corridor (24,16) -> (6,11) with EVENT_LANCES_ROOM_LOCK_DOOR
|
||||
-- unset (entrance blocks $31/$32). Exposed for parity tests.
|
||||
local LANCE_WALK_IN = {
|
||||
{ "down", 2 }, { "left", 6 }, { "down", 5 }, { "left", 10 },
|
||||
{ "up", 9 }, { "left", 2 }, { "up", 3 },
|
||||
}
|
||||
|
||||
local function lanceEntranceBlocks(game, ow)
|
||||
local locked = game.save.flags.EVENT_LANCES_ROOM_LOCK_DOOR
|
||||
ow:replaceBlock(2, 6, locked and 0x72 or 0x31)
|
||||
ow:replaceBlock(3, 6, locked and 0x73 or 0x32)
|
||||
end
|
||||
|
||||
local function lanceLockDoor(game, ow)
|
||||
if game.save.flags.EVENT_LANCES_ROOM_LOCK_DOOR then return end
|
||||
game.save.flags.EVENT_LANCES_ROOM_LOCK_DOOR = true
|
||||
require("src.core.Sound").play(game.data, "Go_Inside")
|
||||
lanceEntranceBlocks(game, ow)
|
||||
end
|
||||
|
||||
local function lanceWalkIn(game, ow)
|
||||
local i = 0
|
||||
local function step()
|
||||
i = i + 1
|
||||
local seg = LANCE_WALK_IN[i]
|
||||
if not seg then
|
||||
-- lands on (6,11); vanilla's per-frame coord poll then locks the
|
||||
-- door at once. scriptMove landings do not fire onStep, so lock
|
||||
-- here.
|
||||
lanceLockDoor(game, ow)
|
||||
return
|
||||
end
|
||||
ow:scriptMove(ow.player, seg[1], seg[2], step)
|
||||
end
|
||||
step()
|
||||
end
|
||||
|
||||
M.LANCES_ROOM = {
|
||||
walkInRoute = LANCE_WALK_IN,
|
||||
onEnter = function(game, ow)
|
||||
lanceEntranceBlocks(game, ow)
|
||||
-- the warp arrival lands ON the staircase trigger, and onStep only
|
||||
-- fires for completed steps -- start the walk-in here, the same
|
||||
-- way Lorelei's auto walk-in runs from its onEnter
|
||||
if not game.save.flags.EVENT_BEAT_LANCE
|
||||
and ow.player.cellX == 24 and ow.player.cellY == 16 then
|
||||
lanceWalkIn(game, ow)
|
||||
end
|
||||
end,
|
||||
onStep = function(game, ow, x, y)
|
||||
if game.save.flags.EVENT_BEAT_LANCE then return false end
|
||||
if (x == 5 and y == 1) or (x == 6 and y == 2) then
|
||||
local lance
|
||||
for _, npc in ipairs(ow.npcs) do
|
||||
if npc.def and npc.def.name == "LANCESROOM_LANCE" then lance = npc break end
|
||||
end
|
||||
if not lance or ow:trainerDefeated(lance) then return false end
|
||||
lance:facePlayer(ow.player)
|
||||
-- LancesRoomLanceEndBattleScript DisplayTextID -> TalkToTrainer
|
||||
-- after-battle text (rival became champion first). engageTrainer
|
||||
-- only shows the won line, so push the after text on a win.
|
||||
ow:engageTrainer(lance, function()
|
||||
if not ow:trainerDefeated(lance) then return end
|
||||
local after = text(game)._LancesRoomLanceAfterBattleText
|
||||
if after then push(game, after) end
|
||||
end)
|
||||
return true
|
||||
end
|
||||
if (x == 5 or x == 6) and y == 11 then
|
||||
lanceLockDoor(game, ow)
|
||||
return false
|
||||
end
|
||||
if x == 24 and y == 16 then
|
||||
lanceWalkIn(game, ow)
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end,
|
||||
}
|
||||
|
||||
M.CELADON_CHIEF_HOUSE = require("data.scripts.celadon_chief_house")
|
||||
|
||||
return M
|
||||
|
||||
@@ -196,14 +196,24 @@ local function inCoords(coords, x, y)
|
||||
return false
|
||||
end
|
||||
|
||||
-- coordinate block: show a line and push the player back one step
|
||||
-- coordinate block: show a line and shove the player back one step. pokered
|
||||
-- shoves with a SIMULATED JOYPAD press (scripts/ViridianCity.asm ViridianCity-
|
||||
-- CheckGymOpenScript), which runs the normal overworld step pipeline including
|
||||
-- HandleLedges (engine/overworld/ledges.asm), so the shove must HOP a ledge
|
||||
-- when one sits in front: the tile below the Viridian Gym door is a down-ledge
|
||||
-- (#151). Gates with no ledge in front (Cinnabar gym lock at (18,4)) get
|
||||
-- checkLedgeHop == false and fall back to the plain forced step, unchanged.
|
||||
local function stepGate(opts)
|
||||
return function(game, ow, x, y)
|
||||
if not inCoords(opts.coords, x, y) then return false end
|
||||
if not opts.blocked(game) then return false end
|
||||
require("src.core.Sound").play(game.data, "Denied")
|
||||
push(game, text(game)[opts.text] or opts.fallback,
|
||||
function() ow:scriptMove(ow.player, opts.push, 1) end)
|
||||
push(game, text(game)[opts.text] or opts.fallback, function()
|
||||
ow.player.facing = opts.push
|
||||
if not ow:checkLedgeHop(opts.push) then
|
||||
ow:scriptMove(ow.player, opts.push, 1)
|
||||
end
|
||||
end)
|
||||
return true
|
||||
end
|
||||
end
|
||||
@@ -259,23 +269,183 @@ M.CINNABAR_ISLAND = {
|
||||
end,
|
||||
}
|
||||
|
||||
-- Pewter's youngster stops you leaving east before Brock is beaten
|
||||
-- (scripts/PewterCity.asm PewterCityCheckPlayerLeavingEastScript; the
|
||||
-- original escorts you to the gym, we walk you back a step)
|
||||
-- Pewter's youngster stops you leaving east before Brock is beaten and
|
||||
-- escorts you to the gym (scripts/PewterCity.asm
|
||||
-- PewterCityCheckPlayerLeavingEastScript /
|
||||
-- PewterCityYoungsterShowsPlayerGymScript, engine/overworld/auto_movement.asm
|
||||
-- PewterGymGuyMovementScriptPointerTable, engine/events/pewter_guys.asm
|
||||
-- PewterGymGuyCoords). Same lockstep style as Oak's lab escort: the
|
||||
-- youngster walks RLEList_PewterGymGuy while the player plays the
|
||||
-- reverse of RLEList_PewterGymPlayer with a PewterGuys positioning
|
||||
-- preamble. Ends at (11,18) / (12,18) by the gym, not phasing through
|
||||
-- the building.
|
||||
local pewterEscort = {}
|
||||
|
||||
-- RLEList_PewterGymGuy (NPC directions play forward)
|
||||
pewterEscort.guySteps = {
|
||||
"down", "down",
|
||||
"left", "left", "left", "left", "left", "left", "left", "left",
|
||||
"left", "left", "left", "left", "left", "left", "left",
|
||||
"up", "up", "up", "up", "up",
|
||||
"left", "left", "left", "left", "left", "left", "left", "left",
|
||||
"left", "left", "left",
|
||||
"down", "down", "down", "down", "down",
|
||||
"right", "right", "right",
|
||||
}
|
||||
|
||||
-- Walk home: reverse of guySteps with opposite facings (gym → spawn).
|
||||
do
|
||||
local opp = { up = "down", down = "up", left = "right", right = "left" }
|
||||
local ret = {}
|
||||
for i = #pewterEscort.guySteps, 1, -1 do
|
||||
ret[#ret + 1] = opp[pewterEscort.guySteps[i]]
|
||||
end
|
||||
pewterEscort.guyReturnSteps = ret
|
||||
end
|
||||
|
||||
-- RLEList_PewterGymPlayer before reverse / PewterGuys (NO_INPUT, RIGHT×2,
|
||||
-- DOWN×5, LEFT×11, UP×5, LEFT×15)
|
||||
pewterEscort.playerRle = {
|
||||
"NO",
|
||||
"right", "right",
|
||||
"down", "down", "down", "down", "down",
|
||||
"left", "left", "left", "left", "left", "left", "left", "left",
|
||||
"left", "left", "left",
|
||||
"up", "up", "up", "up", "up",
|
||||
"left", "left", "left", "left", "left", "left", "left", "left",
|
||||
"left", "left", "left", "left", "left", "left", "left",
|
||||
}
|
||||
|
||||
-- PewterGymGuyCoords: (x, y) -> positioning moves written after the RLE
|
||||
-- (played in reverse; $00 pauses are one overworld frame ≈ 1/8 tile for
|
||||
-- the NPC, so eight of them ≈ one guy head-start step)
|
||||
pewterEscort.preambles = {
|
||||
["34,16"] = { "left", "down", "down", "right" },
|
||||
["35,17"] = { "left", "down", "right", "left" },
|
||||
["37,18"] = { "left", "left", "left",
|
||||
"NO", "NO", "NO", "NO", "NO", "NO", "NO", "NO" },
|
||||
["37,19"] = { "left", "left", "up", "left" },
|
||||
["36,17"] = { "left", "down", "left",
|
||||
"NO", "NO", "NO", "NO", "NO", "NO", "NO", "NO" },
|
||||
}
|
||||
|
||||
-- Realized player path for a trigger tile: PewterGuys overwrites the
|
||||
-- last RLE byte and appends the preamble, then simulated joypad plays
|
||||
-- high→low (reverse). Leading NO×8 collapses to guyHeadStart=1; the
|
||||
-- trailing end-of-list NO is dropped (one-frame pause).
|
||||
function pewterEscort.playerPlan(x, y)
|
||||
local pre = pewterEscort.preambles[x .. "," .. y]
|
||||
if not pre then return nil end
|
||||
local buf = {}
|
||||
for i, d in ipairs(pewterEscort.playerRle) do buf[i] = d end
|
||||
buf[#buf] = pre[1]
|
||||
for i = 2, #pre do buf[#buf + 1] = pre[i] end
|
||||
local path = {}
|
||||
for i = #buf, 1, -1 do path[#path + 1] = buf[i] end
|
||||
local head = 0
|
||||
while path[head + 1] == "NO" do head = head + 1 end
|
||||
local tail = #path
|
||||
while tail > head and path[tail] == "NO" do tail = tail - 1 end
|
||||
local steps = {}
|
||||
for i = head + 1, tail do steps[#steps + 1] = path[i] end
|
||||
return { steps = steps, guyHeadStart = math.floor(head / 8) }
|
||||
end
|
||||
|
||||
local function pewterGymEscort(game, ow)
|
||||
if ow.runner:isRunning() or #ow.scriptMoves > 0 then return end
|
||||
local x, y = ow.player.cellX, ow.player.cellY
|
||||
local plan = pewterEscort.playerPlan(x, y)
|
||||
local Music = require("src.core.Music")
|
||||
local t = text(game)
|
||||
local follow = t._PewterCityYoungsterYoureATrainerFollowMeText
|
||||
or "You're a trainer\nright? BROCK's\nlooking for new\nchallengers!\nFollow me!"
|
||||
-- PewterGuys only has entries for five tiles; an unmatched talk tile
|
||||
-- (e.g. (36,16) east of him) just gets the follow-me line, same as a
|
||||
-- failed coords lookup would refuse to arm the walk.
|
||||
if not plan then
|
||||
push(game, follow)
|
||||
return
|
||||
end
|
||||
local guy = ow:npcByIndex(5) -- PEWTERCITY_YOUNGSTER
|
||||
local guySteps = pewterEscort.guySteps
|
||||
local head = plan.guyHeadStart
|
||||
|
||||
-- After the walk: face the player, restore map music, "Go take on
|
||||
-- BROCK", then retrace RLEList_PewterGymGuy back to his spawn (35,16).
|
||||
-- (pokered teleports him via MovementData_PewterGymGuyExit; we walk
|
||||
-- the same route home instead. Brock victory still HideObject's him.)
|
||||
local function walkHome()
|
||||
if not guy then return end
|
||||
local ret = pewterEscort.guyReturnSteps
|
||||
local i = 0
|
||||
local function tick()
|
||||
i = i + 1
|
||||
if not ret[i] then
|
||||
guy.facing = "down"
|
||||
return
|
||||
end
|
||||
ow:scriptMove(guy, ret[i], 1, tick)
|
||||
end
|
||||
tick()
|
||||
end
|
||||
|
||||
local function afterWalk()
|
||||
if guy then guy.facing = "left" end
|
||||
Music.playMap(game.data, "PEWTER_CITY")
|
||||
push(game, t._PewterCityYoungsterGoTakeOnBrockText
|
||||
or "Go take on BROCK\nat the GYM first!", walkHome)
|
||||
end
|
||||
|
||||
local function lockstep()
|
||||
local i = 0
|
||||
local function tick()
|
||||
i = i + 1
|
||||
local ps = plan.steps[i]
|
||||
if not ps then
|
||||
afterWalk()
|
||||
return
|
||||
end
|
||||
local gs = guySteps[head + i]
|
||||
if guy and gs then ow:scriptMove(guy, gs, 1) end
|
||||
ow:scriptMove(ow.player, ps, 1, tick)
|
||||
end
|
||||
tick()
|
||||
end
|
||||
|
||||
local function beginWalk()
|
||||
Music.play(game.data, "Music_MuseumGuy")
|
||||
if guy and head > 0 then
|
||||
local h = 0
|
||||
local function headTick()
|
||||
h = h + 1
|
||||
if h > head then lockstep(); return end
|
||||
ow:scriptMove(guy, guySteps[h], 1, headTick)
|
||||
end
|
||||
headTick()
|
||||
else
|
||||
lockstep()
|
||||
end
|
||||
end
|
||||
|
||||
push(game, follow, beginWalk)
|
||||
end
|
||||
|
||||
M.PEWTER_CITY = {
|
||||
escort = pewterEscort,
|
||||
-- PewterCityYoungsterText: talking also arms the gym escort script
|
||||
talk = {
|
||||
TEXT_PEWTERCITY_YOUNGSTER = function(game, ow, npc, done)
|
||||
pewterGymEscort(game, ow)
|
||||
if done then done() end
|
||||
end,
|
||||
},
|
||||
onStep = function(game, ow, x, y)
|
||||
if game.save.flags.EVENT_BEAT_BROCK then return false end
|
||||
if ow.runner:isRunning() or #ow.scriptMoves > 0 then return false end
|
||||
if not inCoords({ { 35, 17 }, { 36, 17 }, { 37, 18 }, { 37, 19 } }, x, y) then
|
||||
return false
|
||||
end
|
||||
local t = text(game)
|
||||
push(game, t._PewterCityYoungsterYoureATrainerFollowMeText
|
||||
or "Hey! You're a\ntrainer, right?", function()
|
||||
push(game, t._PewterCityYoungsterGoTakeOnBrockText
|
||||
or "Go take on BROCK\nat the GYM first!", function()
|
||||
ow:scriptMove(ow.player, "left", 1)
|
||||
end)
|
||||
end)
|
||||
pewterGymEscort(game, ow)
|
||||
return true
|
||||
end,
|
||||
}
|
||||
@@ -297,6 +467,19 @@ end
|
||||
-- EVENT_ROUTE22_RIVAL_WANTS_BATTLE in Oak's lab (expired by Pewter
|
||||
-- Gym) and again in Viridian Gym; we derive the same windows from the
|
||||
-- surrounding story flags so old saves work too.
|
||||
-- Rival1Exit → Viridian (right/down); Rival2Exit → League (left).
|
||||
local function route22ExitDirs(n, py)
|
||||
if n == 2 then
|
||||
if py == 5 then return { "left", "left", "left", "left" } end
|
||||
return { "left", "left", "left" }
|
||||
end
|
||||
if py == 5 then
|
||||
return { "right", "right", "down", "down", "down", "down", "down" }
|
||||
end
|
||||
return { "up", "right", "right", "right",
|
||||
"down", "down", "down", "down", "down", "down" }
|
||||
end
|
||||
|
||||
local function route22Scene(n, objIndex, objName, oppClass, baseParty, beatFlag, py)
|
||||
return {
|
||||
{ "show_object", "ROUTE_22", objName }, -- 1
|
||||
@@ -308,7 +491,7 @@ local function route22Scene(n, objIndex, objName, oppClass, baseParty, beatFlag,
|
||||
{ "set_flag", beatFlag }, -- 7
|
||||
{ "show_text", "_Route22Rival" .. n .. "DefeatedText" }, -- 8
|
||||
{ "show_text", "_Route22RivalAfterBattleText" .. n }, -- 9
|
||||
{ "move_npc_to", objIndex, 25, 5 }, -- 10
|
||||
{ "walk_npc", objIndex, route22ExitDirs(n, py) }, -- 10
|
||||
{ "hide_object", "ROUTE_22", objName }, -- 11
|
||||
}
|
||||
end
|
||||
@@ -335,6 +518,14 @@ M.ROUTE_22 = {
|
||||
-- Cerulean City: the Nugget Bridge rival ambush (CeruleanCityCoords2),
|
||||
-- the TM28 rocket thief with his forced-fight cells (Coords1), and the
|
||||
-- cave guard who steps aside once you are Champion.
|
||||
-- Post-battle: Bill text then CeruleanCityMovement3/4 into town.
|
||||
local function ceruleanRivalExitDirs(px)
|
||||
if px == 20 then
|
||||
return { "right", "down", "down", "down", "down", "down", "down" }
|
||||
end
|
||||
return { "left", "down", "down", "down", "down", "down", "down" }
|
||||
end
|
||||
|
||||
local function ceruleanRivalScene(px, py)
|
||||
return {
|
||||
{ "show_object", "CERULEAN_CITY", "CERULEANCITY_RIVAL" }, -- 1
|
||||
@@ -342,32 +533,43 @@ local function ceruleanRivalScene(px, py)
|
||||
{ "face_object", 1, "down" }, -- 3
|
||||
{ "show_text", "_CeruleanCityRivalPreBattleText" }, -- 4
|
||||
{ "rival_battle", "OPP_RIVAL1", 7 }, -- 5
|
||||
{ "jump_if_false", 10 }, -- 6
|
||||
{ "jump_if_false", 11 }, -- 6
|
||||
{ "set_flag", "EVENT_BEAT_CERULEAN_RIVAL" }, -- 7
|
||||
{ "show_text", "_CeruleanCityRivalDefeatedText" }, -- 8
|
||||
{ "move_npc_to", 1, 20, 2 }, -- 9
|
||||
{ "hide_object", "CERULEAN_CITY", "CERULEANCITY_RIVAL" }, -- 10
|
||||
{ "show_text", "_CeruleanCityRivalIWentToBillsText" }, -- 9
|
||||
{ "walk_npc", 1, ceruleanRivalExitDirs(px) }, -- 10
|
||||
{ "hide_object", "CERULEAN_CITY", "CERULEANCITY_RIVAL" }, -- 11
|
||||
}
|
||||
end
|
||||
|
||||
-- scripts/CeruleanCity.asm CeruleanCityRocketText: fight the thief,
|
||||
-- then he returns TM28 (DIG) and hurries off
|
||||
-- then he returns TM28 (DIG) and hurries off. CeruleanHideRocket
|
||||
-- (CeruleanCity_2.asm) is GBFadeOutToBlack → Show GUARD1 / Hide GUARD2 /
|
||||
-- Hide ROCKET → GBFadeInFromBlack — not a bare hide_object.
|
||||
local rocketRows = {
|
||||
{ "face_player" }, -- 1
|
||||
{ "check_flag", "EVENT_GOT_TM28" }, -- 2
|
||||
{ "jump_if_true", 15 }, -- 3
|
||||
{ "jump_if_true", 15 }, -- 3 → CeruleanHideRocket
|
||||
{ "check_flag", "EVENT_BEAT_CERULEAN_ROCKET_THIEF" }, -- 4
|
||||
{ "jump_if_true", 9 }, -- 5
|
||||
{ "show_text", "_CeruleanCityRocketText" }, -- 6
|
||||
{ "start_battle", "trainer", "OPP_ROCKET", 5 }, -- 7
|
||||
{ "jump_if_false", 16 }, -- 8
|
||||
{ "jump_if_false", "end" }, -- 8
|
||||
{ "show_text", "_CeruleanCityRocketIllReturnTheTMText" }, -- 9
|
||||
{ "set_flag", "EVENT_BEAT_CERULEAN_ROCKET_THIEF" }, -- 10
|
||||
{ "give_item", "TM_DIG", 1, false }, -- 11 (row 13 prints)
|
||||
{ "set_flag", "EVENT_GOT_TM28" }, -- 12
|
||||
{ "show_text", "_CeruleanCityRocketReceivedTM28Text" }, -- 13
|
||||
{ "show_text", "_CeruleanCityRocketIBetterGetMovingText" }, -- 14
|
||||
{ "hide_object", "CERULEAN_CITY", "CERULEANCITY_ROCKET" }, -- 15
|
||||
{ "fade", "out" }, -- 15 GBFadeOutToBlack
|
||||
-- CeruleanHideRocket while black: GUARD1 (28,12) appears, GUARD2
|
||||
-- (27,12) and the ROCKET go. GUARD2 blocks the trashed-house south
|
||||
-- door neighbour — the swap reconnects the city (Bill's ticket does
|
||||
-- the same in story.lua; either route is enough).
|
||||
{ "show_object", "CERULEAN_CITY", "CERULEANCITY_GUARD1" }, -- 16
|
||||
{ "hide_object", "CERULEAN_CITY", "CERULEANCITY_GUARD2" }, -- 17
|
||||
{ "hide_object", "CERULEAN_CITY", "CERULEANCITY_ROCKET" }, -- 18
|
||||
{ "fade", "in" }, -- 19 GBFadeInFromBlack
|
||||
}
|
||||
|
||||
M.CERULEAN_CITY = {
|
||||
@@ -444,10 +646,22 @@ local function bikeGateGuard(coords, stopText, explainText)
|
||||
return function(game, ow, x, y)
|
||||
if game.save.inventory.BICYCLE then return false end
|
||||
if not inCoords(coords, x, y) then return false end
|
||||
-- walk the player up to the tile beside the counter, no further:
|
||||
-- (matchedY - closestY) tiles, 0 when already next to it
|
||||
-- (Route16Gate1FDefaultScript's wCoordIndex-1). Forcing a fixed one
|
||||
-- tile up from the counter-adjacent row shoved the player onto the
|
||||
-- impassable desk and boxed them in.
|
||||
local closestY = coords[1][2]
|
||||
for _, c in ipairs(coords) do
|
||||
if c[2] < closestY then closestY = c[2] end
|
||||
end
|
||||
local dist = y - closestY
|
||||
local t = text(game)
|
||||
push(game, t[stopText] or "Hey! Wait up!", function()
|
||||
push(game, t[explainText] or "You need a\nBICYCLE for\nCYCLING ROAD!", function()
|
||||
ow:scriptMove(ow.player, "up", 1)
|
||||
if dist > 0 then
|
||||
ow:scriptMove(ow.player, "up", dist)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
return true
|
||||
|
||||
@@ -2,27 +2,111 @@
|
||||
-- "OPP_CLASS#partyIndex" (the object_event trainer args). Hand-ported
|
||||
-- from the leaders'/bosses' text_asm victory scripts:
|
||||
-- gym badges: scripts/PewterGym.asm ... ViridianGym.asm
|
||||
-- Rocket Hideout Giovanni: his Silph Scope is an item ball next to him
|
||||
-- (data/maps/objects/RocketHideoutB4F.asm), so no reward entry needed.
|
||||
-- Rocket Hideout Giovanni: Silph Scope is a hidden item ball revealed by
|
||||
-- ShowObject in RocketHideoutB4FBeatGiovanniScript (ported as the
|
||||
-- TEXT_ROCKETHIDEOUTB4F_GIOVANNI talk handler in story3.lua).
|
||||
-- The TM each gym leader hands out afterwards is also ported.
|
||||
--
|
||||
-- `deactivate` lists the EVENT_BEAT_* flags each gym's victory script
|
||||
-- sets to retire unfought non-leader trainers (PewterGym.asm
|
||||
-- "; deactivate gym trainers" / SetEventRange in the other gyms, and
|
||||
-- FightingDojo.asm SetEventRange EVENT_BEAT_KARATE_MASTER ..
|
||||
-- EVENT_BEAT_FIGHTING_DOJO_TRAINER_3).
|
||||
--
|
||||
-- `dialogue` is the end-battle + post-battle text chain each gym leader
|
||||
-- runs (SaveEndBattleTextPointers then the map's *PostBattle / ReceiveTM
|
||||
-- script). Leaders are not def_trainers entries, so engageTrainer has
|
||||
-- no header.won — checkVictoryRewards shows this chain instead of a
|
||||
-- synthetic "received badge/TM" stub.
|
||||
|
||||
local function range(prefix, first, last)
|
||||
local t = {}
|
||||
for i = first, last do
|
||||
t[#t + 1] = prefix .. i
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
return {
|
||||
-- PewterGym.asm .gymVictory also HideObject TOGGLE_GYM_GUY
|
||||
-- (PEWTERCITY_YOUNGSTER) and TOGGLE_ROUTE_22_RIVAL_1 so the east-exit
|
||||
-- escort NPC and the first Route 22 rival stay gone after the badge.
|
||||
["OPP_BROCK#1"] = { badge = "BOULDERBADGE", flag = "EVENT_BEAT_BROCK",
|
||||
item = "TM_BIDE" },
|
||||
item = "TM_BIDE",
|
||||
deactivate = { "EVENT_BEAT_PEWTER_GYM_TRAINER_0" },
|
||||
hide = {
|
||||
{ "PEWTER_CITY", "PEWTERCITY_YOUNGSTER" },
|
||||
{ "ROUTE_22", "ROUTE22_RIVAL1" },
|
||||
},
|
||||
dialogue = {
|
||||
"_PewterGymBrockReceivedBoulderBadgeText",
|
||||
"_PewterGymBrockBoulderBadgeInfoText",
|
||||
"_PewterGymBrockWaitTakeThisText",
|
||||
"_PewterGymReceivedTM34Text",
|
||||
"_TM34ExplanationText",
|
||||
} },
|
||||
["OPP_MISTY#1"] = { badge = "CASCADEBADGE", flag = "EVENT_BEAT_MISTY",
|
||||
item = "TM_BUBBLEBEAM" },
|
||||
item = "TM_BUBBLEBEAM",
|
||||
deactivate = range("EVENT_BEAT_CERULEAN_GYM_TRAINER_", 0, 1),
|
||||
dialogue = {
|
||||
"_CeruleanGymMistyReceivedCascadeBadgeText",
|
||||
"_CeruleanGymMistyCascadeBadgeInfoText",
|
||||
"_CeruleanGymMistyReceivedTM11Text",
|
||||
} },
|
||||
["OPP_LT_SURGE#1"] = { badge = "THUNDERBADGE", flag = "EVENT_BEAT_LT_SURGE",
|
||||
item = "TM_THUNDERBOLT" },
|
||||
item = "TM_THUNDERBOLT",
|
||||
deactivate = range("EVENT_BEAT_VERMILION_GYM_TRAINER_", 0, 2),
|
||||
dialogue = {
|
||||
"_VermilionGymLTSurgeReceivedThunderBadgeText",
|
||||
"_VermilionGymLTSurgeThunderBadgeInfoText",
|
||||
"_VermilionGymLTSurgeReceivedTM24Text",
|
||||
"_TM24ExplanationText",
|
||||
} },
|
||||
["OPP_ERIKA#1"] = { badge = "RAINBOWBADGE", flag = "EVENT_BEAT_ERIKA",
|
||||
item = "TM_MEGA_DRAIN" },
|
||||
item = "TM_MEGA_DRAIN",
|
||||
deactivate = range("EVENT_BEAT_CELADON_GYM_TRAINER_", 0, 6),
|
||||
dialogue = {
|
||||
"_CeladonGymErikaReceivedRainbowBadgeText",
|
||||
"_CeladonGymRainbowBadgeInfoText",
|
||||
"_CeladonGymReceivedTM21Text",
|
||||
"_TM21ExplanationText",
|
||||
} },
|
||||
["OPP_KOGA#1"] = { badge = "SOULBADGE", flag = "EVENT_BEAT_KOGA",
|
||||
item = "TM_TOXIC" },
|
||||
item = "TM_TOXIC",
|
||||
deactivate = range("EVENT_BEAT_FUCHSIA_GYM_TRAINER_", 0, 5),
|
||||
dialogue = {
|
||||
"_FuchsiaGymKogaReceivedSoulBadgeText",
|
||||
"_FuchsiaGymKogaSoulBadgeInfoText",
|
||||
"_FuchsiaGymKogaReceivedTM06Text",
|
||||
"_FuchsiaGymKogaTM06ExplanationText",
|
||||
} },
|
||||
["OPP_SABRINA#1"] = { badge = "MARSHBADGE", flag = "EVENT_BEAT_SABRINA",
|
||||
item = "TM_PSYWAVE" },
|
||||
item = "TM_PSYWAVE",
|
||||
deactivate = range("EVENT_BEAT_SAFFRON_GYM_TRAINER_", 0, 6),
|
||||
dialogue = {
|
||||
"_SaffronGymSabrinaReceivedMarshBadgeText",
|
||||
"_SaffronGymSabrinaMarshBadgeInfoText",
|
||||
"_SaffronGymSabrinaReceivedTM46Text",
|
||||
"_TM46ExplanationText",
|
||||
} },
|
||||
["OPP_BLAINE#1"] = { badge = "VOLCANOBADGE", flag = "EVENT_BEAT_BLAINE",
|
||||
item = "TM_FIRE_BLAST" },
|
||||
item = "TM_FIRE_BLAST",
|
||||
deactivate = range("EVENT_BEAT_CINNABAR_GYM_TRAINER_", 0, 6),
|
||||
dialogue = {
|
||||
"_CinnabarGymBlaineReceivedVolcanoBadgeText",
|
||||
"_CinnabarGymBlaineVolcanoBadgeInfoText",
|
||||
"_CinnabarGymBlaineReceivedTM38Text",
|
||||
"_CinnabarGymBlaineTM38ExplanationText",
|
||||
} },
|
||||
["OPP_GIOVANNI#3"] = { badge = "EARTHBADGE", flag = "EVENT_BEAT_GIOVANNI",
|
||||
item = "TM_FISSURE" },
|
||||
item = "TM_FISSURE",
|
||||
deactivate = range("EVENT_BEAT_VIRIDIAN_GYM_TRAINER_", 0, 7),
|
||||
dialogue = {
|
||||
"_ViridianGymGiovanniReceivedEarthBadgeText",
|
||||
"_ViridianGymGiovanniEarthBadgeInfoText",
|
||||
"_ViridianGymGiovanniReceivedTM27Text",
|
||||
"_ViridianGymGiovanniTM27ExplanationText",
|
||||
} },
|
||||
|
||||
-- Silph Co. Giovanni: unlocks the president's Master Ball gift
|
||||
["OPP_GIOVANNI#2"] = { flag = "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||
@@ -30,8 +114,12 @@ return {
|
||||
-- Fighting Dojo Karate Master (scripts/FightingDojo.asm
|
||||
-- FightingDojoKarateMasterPostBattleScript sets EVENT_BEAT_KARATE_MASTER,
|
||||
-- which gates the HITMONLEE/HITMONCHAN gift). OPP_BLACKBELT party 1 is
|
||||
-- only him (data/maps/objects/FightingDojo.asm).
|
||||
["OPP_BLACKBELT#1"] = { flag = "EVENT_BEAT_KARATE_MASTER" },
|
||||
-- only him (data/maps/objects/FightingDojo.asm). `dialogue` is the
|
||||
-- concede + prize offer he speaks after the win (his header supplies the
|
||||
-- "Hwa! Arrgh! Beaten!" won line first; #197).
|
||||
["OPP_BLACKBELT#1"] = { flag = "EVENT_BEAT_KARATE_MASTER",
|
||||
deactivate = range("EVENT_BEAT_FIGHTING_DOJO_TRAINER_", 0, 3),
|
||||
dialogue = { "_FightingDojoKarateMasterIWillGiveYouAPokemonText" } },
|
||||
|
||||
-- Elite Four progress flags (their rooms' door logic isn't ported, but
|
||||
-- the flags make the Hall of Fame checkable)
|
||||
|
||||
@@ -6,4 +6,37 @@ work for true parity. Faithfully-ported behavior is documented in
|
||||
docs/behavior-porting-notes.md; deliberate additions beyond the original
|
||||
are in docs/new-features.md.
|
||||
|
||||
None currently.
|
||||
## Reimplemented unused Prof. Oak and Rocket Chief battles
|
||||
|
||||
The original ROM defines trainer data for `PROF_OAK` and `CHIEF`
|
||||
(`data/trainers/parties.asm`) but never attaches either to an NPC, so
|
||||
both battles are unreachable in the real game. This project makes them
|
||||
fightable after the Hall of Fame:
|
||||
|
||||
- Prof. Oak battles you in Pallet Town once `EVENT_BEAT_CHAMPION_RIVAL`
|
||||
is set, using `ProfOakData`'s three starter-matched teams (the team is
|
||||
picked by the type that counters your starter, mirroring the rival).
|
||||
- The Celadon Game Corner Chief battles you in his house post-game.
|
||||
`ChiefData` is empty in the ROM, so `OPP_CHIEF` is given a
|
||||
reconstructed party.
|
||||
|
||||
This is an intentional divergence: neither battle can be triggered in the
|
||||
original game.
|
||||
|
||||
## Reimplemented unused Silph Co. card-key doors
|
||||
|
||||
`engine/events/card_key.asm` and the unused `CardKeyTable1/2/3` coordinate
|
||||
lists (`data/events/card_key_coords.asm`) describe locked doors for Silph
|
||||
Co. floors 2F-11F, but no retail `.blk` map layout ever places the closed
|
||||
door block at those coordinates, so the card key check is dead code in
|
||||
the original game. This project stamps the closed door block (`$54`/`$5f`
|
||||
on floors 2F-10F, `$20` on 11F) over each of the 20 door coordinates on
|
||||
map load, and swaps it for the open block once that door's
|
||||
`EVENT_SILPH_CO_n_UNLOCKED_DOORn` flag is set (using the key from a Team
|
||||
Rocket grunt, as in the original's unused design).
|
||||
|
||||
This is an intentional divergence: the doors are not visible or
|
||||
functional in the original game. The door layout lives in
|
||||
`tools/rom_manifest.json` (`field.cardKeyDoors.closedDoors`), hand-ported
|
||||
since no retail ROM data encodes it; `src/import/RomExtractor.lua` copies
|
||||
it straight through on ROM import.
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
# Launcher
|
||||
|
||||
The launcher is `src/import/RomImporter.lua`, the first-run / title screen
|
||||
that runs before `Game:load`. Besides ROM import (see the file's own header)
|
||||
it hosts a tabbed shell covering per-game save slots and a mod manager. This
|
||||
file documents the runtime model; the visual spec lives separately.
|
||||
|
||||
## Android multi-ROM import
|
||||
|
||||
On Android, `love.system.pickFile()` opens the Storage Access Framework
|
||||
picker (`GameActivity.showRomFilePicker`); the chosen file is copied to
|
||||
`picked_rom.gb` in the app save directory. `RomImporter` then imports on
|
||||
refocus / Choose via `findPendingRom`: only a 1 MiB `.gb` whose SHA-1 maps
|
||||
to a version that is **not** yet ready counts as pending. A leftover
|
||||
`picked_rom.gb` from Red therefore cannot block Blue's Choose (issue #167).
|
||||
After a successful import the consumed save-dir `.gb` is removed.
|
||||
|
||||
**Manual check (device/emulator):** import Red → switch to Blue → Choose →
|
||||
system file picker must appear (not a silent Red re-extract) → pick Blue →
|
||||
Blue becomes ready beside Red.
|
||||
|
||||
## Tab structure
|
||||
|
||||
`self.tab` is one of `"red"`, `"blue"`, `"yellow"`, `"mods"`. The tab bar
|
||||
draws one chip per game plus a MODS chip and rebuilds `self.tabRects` every
|
||||
frame so `mousepressed` can dispatch clicks; switching tabs mid-import is
|
||||
allowed (a dropped ROM still routes by SHA-1 regardless of which tab shows).
|
||||
|
||||
- A game tab (`_drawGamePanel`) shows the ROM card, the SAVE FILES card, the
|
||||
Play button, and the SAVE SLOT card in a responsive two-column grid (see
|
||||
Responsiveness). The MODS tab (`_drawModsPanel`) shows the mod list instead.
|
||||
- The self-updater banner (`self.Check`, see `docs/updater.md`) draws as a
|
||||
centered pill in a reserved band just above the footer, on every tab. That
|
||||
position is unchanged by this redesign, so `docs/updater.md` needed no edits.
|
||||
|
||||
## Save slot model
|
||||
|
||||
All slot I/O lives in `src/core/SaveData.lua` and goes through the same fs
|
||||
abstraction (`persistFs`) every other save/options call uses, so portable
|
||||
mode (an `io.*` filesystem used when `portable.txt` marks the install)
|
||||
keeps working unchanged.
|
||||
|
||||
- **Files.** A version's playthroughs live under `saves/<version>/`, one file
|
||||
per slot: `saves/<version>/slot1.lua` plus a rolling `.bak` and staged
|
||||
`.tmp` witness (`slotNames`), mirroring the write/recovery discipline
|
||||
`SaveData.save`/`load` already use for the flat legacy file. Slot ids match
|
||||
`slot%d+`; `createSlot` allocates one past the highest existing number so a
|
||||
reused id can never collide with a lingering file.
|
||||
- **Registry.** The ordered slot list and which one is active persist in
|
||||
`options.lua` (via the existing `SaveData.loadOptions`/`saveOptions`):
|
||||
`options.saveSlots = { [version] = { list = {"slot1", ...}, active = "slot1" } }`.
|
||||
- **Active slot resolution.** `saveNames(version)`, the function every
|
||||
existing caller (`TitleState` hasSave/load/save, recovery order) already
|
||||
goes through, now resolves the *active* slot instead of a fixed flat name.
|
||||
Resolved once per version per process (`ensureVersionSlots`, cached in
|
||||
`activeSlotCache`/`slotsChecked`): a registry entry wins; otherwise a lazy
|
||||
legacy migration may create one; otherwise the flat legacy path is used
|
||||
(`save.lua` / `save_blue.lua`), so a pre-slots install keeps working as before.
|
||||
- **Legacy migration.** One-time per version, lazy on first
|
||||
`listSlots`/`load`/`saveNames` call (`tryMigrateLegacy`): if a flat legacy
|
||||
file exists and no `saves/<version>/` registry does, its main + `.bak` are
|
||||
copied into `saves/<version>/slot1.lua(.bak)`, verified readable
|
||||
(`decodeSlot`: main, then `.tmp`, then `.bak`), and only then are the
|
||||
originals removed and `slot1` registered as active. A copy that fails to
|
||||
verify leaves the originals in place; migration never loses data.
|
||||
|
||||
The launcher-facing API:
|
||||
- `SaveData.listSlots(version)` -> array of `{id, exists, name, meta}` for
|
||||
every registered slot. `name` is the save's player name, or `nil` for an
|
||||
empty slot; `meta` is `{badges, timeText, dexCount}` (the same fields the
|
||||
title screen's `ContinueInfo` shows) or `nil`. The pure part,
|
||||
`SaveData.slotSummary(save)`, is unit-testable with no filesystem.
|
||||
- `SaveData.setActiveSlot(version, slotId)` registers the id if new, persists
|
||||
it as active, and updates the process cache so the very next save/load
|
||||
lands there. The launcher calls this the moment a slot row is clicked
|
||||
(`RomImporter:_selectSlot`); pressing Play needs no signature change, since
|
||||
`Game.lua`/`main.lua` still just call `SaveData.load()`/`save()`.
|
||||
- `SaveData.createSlot(version)` -> new slot id, registered but with **no
|
||||
save file written**. An empty slot means the title screen offers NEW GAME
|
||||
only, which needs no further changes.
|
||||
|
||||
## Launcher mod manager
|
||||
|
||||
`src/mods/LauncherMods.lua` is a launcher-only read of the mod set. It runs
|
||||
before `Game:load`, so **it never loads a mod's entry chunk**; only
|
||||
`manifest.json` is read and validated (`src/mods/Manifest.validate`), the way
|
||||
`Loader:_discover` finds mods without running them. The real loader
|
||||
(`src/mods/Loader.lua`) still owns the actual load at boot.
|
||||
|
||||
- `LauncherMods.list()` scans `mods/` one level deep (first id wins on a
|
||||
duplicate) and returns one row per mod:
|
||||
`{id, name, version, badge, description, enabled, status, statusDetail}`.
|
||||
`badge` is the manifest's `category`, falling back to `profile`, then
|
||||
`"MOD"`, uppercased. `enabled` reads `options.mods[id]` (missing means
|
||||
enabled, matching the loader's own default).
|
||||
- `status` is `"ok"`, `"warn"`, or `"conflict"`, computed by the pure
|
||||
`LauncherMods.deriveList`/`statusFor` against `ManagerState.resolveToggle`
|
||||
and the validated manifests: `conflict` when enabling this mod collides
|
||||
with another enabled one; `warn` for an out-of-range `game_version` or an
|
||||
absent/disabled/wrong-version hard dependency; `ok` otherwise. Having no
|
||||
`love.*` calls, this half is table-driven by the test suite on its own.
|
||||
- `LauncherMods.setEnabled(id, bool)` persists `options.mods[id]` as a plain
|
||||
boolean, the exact shape `Loader:_saveState` writes, so the running game
|
||||
and the in-game `ManagerState` see the change on next boot. The mods panel
|
||||
calls this on every toggle and re-derives the list right away
|
||||
(`RomImporter:_refreshMods`) so a status change (e.g. a new conflict)
|
||||
shows without waiting for a reload.
|
||||
- `LauncherMods.installZip(path)` mounts the archive with
|
||||
`love.filesystem.mount`, locates the mod root via `locateRoot` (manifest at
|
||||
the zip root, or inside one top-level folder), validates its manifest, and
|
||||
copies the tree into the save-dir `mods/<id>/` before unmounting. Rejects a
|
||||
duplicate of an already-installed mod id, and accepts either an external
|
||||
path string or a LOVE `DroppedFile`, staging a dropped file into a save-dir
|
||||
temp first (mount only reaches save-dir-relative paths), the same way
|
||||
`RomImporter` handles a dropped ROM. A failed copy rolls its partial tree
|
||||
back, and every path unmounts and clears the staged temp file.
|
||||
|
||||
## Import / Export save
|
||||
|
||||
The SAVE FILES card wires a raw Gen1 `.sav` battery image to the save slots
|
||||
through `src/import/SaveFileIO.lua`, which sits on top of
|
||||
`src/save_convert/SaveConvert.lua` and the slot API in `SaveData`.
|
||||
|
||||
- **Import save** is live once the game's ROM is imported (playable). It opens
|
||||
a native `.sav` picker (`chooseSav`, the per-OS dialogs mirror `chooseZip`;
|
||||
Android has no picker and shows a drop hint). `SaveFileIO.importToSlot`
|
||||
reads the bytes (an absolute path, a dropped LOVE file, or raw bytes),
|
||||
guards the 32768-byte size, runs `SaveConvert.importSav` (which also rejects
|
||||
a bad main-data checksum), then registers a fresh slot (`SaveData.createSlot`),
|
||||
writes it (`SaveData.writeSlot`), and makes it active (`SaveData.setActiveSlot`).
|
||||
The meta stamp is re-stamped off `gen1_import` to the current numeric format
|
||||
so `SaveData.load`'s migration pass accepts the slot. On success the SAVE SLOT
|
||||
panel is refreshed with the new slot selected.
|
||||
- **Export save** is live only when the active slot actually holds a save
|
||||
(checked against `listSlots`). `SaveFileIO.exportActiveSlot` loads the active
|
||||
slot, encodes it back with `SaveConvert.exportSav` (a slot never keeps
|
||||
`rawImport`, so this is a zero-filled template export, which is valid), and
|
||||
writes `exports/gen1recomp-<version>-<slotId>.sav` in the save directory
|
||||
(`love.filesystem.createDirectory("exports")`). It returns the absolute path
|
||||
(`love.filesystem.getSaveDirectory()`), which the notice line shows with a
|
||||
desktop "Open folder" affordance (`love.system.openURL("file://" .. dir)`).
|
||||
- **Drag-drop.** `filedropped` routes a `.sav` to the import path for the
|
||||
currently active game tab; when a non-game tab (mods, or the locked yellow
|
||||
placeholder) is showing it defaults to red, the always-present first game
|
||||
(`_savedropTarget`). `.gb` (ROM) and `.zip` (mod) routing is unchanged.
|
||||
- **Failure UX.** Every error path (wrong size, bad checksum, write failure,
|
||||
nothing to export, ROM not imported yet) surfaces as a red notice line on the
|
||||
card. Nothing raises and nothing silently no-ops.
|
||||
|
||||
`SaveFileIO` is love-free enough to unit-test through the same in-memory
|
||||
filesystem stub the slot backend uses (`tests/engine/save_file_io_tests.lua`).
|
||||
|
||||
## Responsiveness
|
||||
|
||||
Every measurement derives from `love.graphics.getDimensions()` each frame
|
||||
plus the existing global scale `s = clamp(height / 768, 0.7, 1.6)`; nothing
|
||||
assumes a fixed window size. The game panel's two-column grid (ROM/SAVE
|
||||
FILES/Play on the left, SAVE SLOT on the right) collapses to one stacked
|
||||
column, slot card below Play, when the window is too narrow for both
|
||||
`~300 * s`-wide columns. The save-slot list and the mod list both scroll
|
||||
(wheel, or drag on touch/desktop) clamped to their own content extent,
|
||||
recomputed every draw. The tab bar labels only the active chip so it stays
|
||||
narrow-safe, and content caps out at `~1440 * s` wide, centered.
|
||||
@@ -1,58 +1,154 @@
|
||||
# Native modding
|
||||
|
||||
The game has a built-in Lua mod runtime. Mods are installed under the LÖVE
|
||||
save directory in `mods/<id>/` and are loaded after the verified ROM data has
|
||||
been imported but before the title screen is created.
|
||||
The modding book lives on the
|
||||
[project wiki](https://github.com/bryanthaboi/pokemon-gen1-recomp-project/wiki).
|
||||
|
||||
## Minimal mod
|
||||
- [Getting started](https://github.com/bryanthaboi/pokemon-gen1-recomp-project/wiki/Getting-Started)
|
||||
— install a mod, write a first one, enable and disable it.
|
||||
- [Tutorials](https://github.com/bryanthaboi/pokemon-gen1-recomp-project/wiki/Tutorials)
|
||||
— twelve dependency-ordered rungs, each a runnable mod.
|
||||
- [Cookbook](https://github.com/bryanthaboi/pokemon-gen1-recomp-project/wiki/Cookbook)
|
||||
— task-sized recipes.
|
||||
- [Registry reference](https://github.com/bryanthaboi/pokemon-gen1-recomp-project/wiki/Reference-Registries)
|
||||
— every registry, generated from `src/mods/Schemas.lua`.
|
||||
|
||||
```text
|
||||
mods/example_mod/
|
||||
├── manifest.json
|
||||
└── main.lua
|
||||
Regenerate the reference straight into a wiki checkout:
|
||||
|
||||
```sh
|
||||
luajit tools/gen_registry_docs.lua ../pokemon-gen1-recomp-project.wiki
|
||||
```
|
||||
|
||||
`manifest.json`:
|
||||
## Rendering pipelines
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "example_mod",
|
||||
"name": "Example Mod",
|
||||
"version": "1.0.0",
|
||||
"entry": "main.lua",
|
||||
"priority": 0,
|
||||
"dependencies": [],
|
||||
"optional_dependencies": [],
|
||||
"conflicts": []
|
||||
}
|
||||
```
|
||||
Most registries hand the engine *content*. `render_pipelines` hands it
|
||||
*drawing*: a pipeline is a display mode a mod owns, which may replace the
|
||||
overworld's world pass with geometry of its own and/or post-process the
|
||||
finished image. `mods/voxel_world` is the worked example — a 3D diorama
|
||||
overworld plus a tilt-shift miniature pass, in about 120 lines of glue over
|
||||
its renderer.
|
||||
|
||||
`main.lua`:
|
||||
A record declares what the mode *is*; the engine
|
||||
(`src/render/Pipelines.lua`) supplies everything about *being a display
|
||||
mode*: the OFF/1/2/3 ladder, an options row next to TILT, a hotkey,
|
||||
persistence in `save.options.pipelines`, and the rule that a world pipeline
|
||||
and the engine's own TILT are mutually exclusive.
|
||||
|
||||
```lua
|
||||
return function(mod)
|
||||
mod.log:info("hello from a native mod")
|
||||
|
||||
mod.content.pokemon:override("PIKACHU", {
|
||||
name = "PIKACHU",
|
||||
types = { "ELECTRIC" },
|
||||
base_stats = { hp = 35, attack = 55, defense = 40, speed = 90, special = 50 },
|
||||
})
|
||||
|
||||
mod.events:on("battle.start", function(context)
|
||||
context.mod_message = "A native mod changed this battle."
|
||||
end)
|
||||
end
|
||||
mod.content.render_pipelines:register("diorama", {
|
||||
label = "DIORAMA", -- options row label
|
||||
levels = { "OFF", "15", "35", "50" }, -- ladder; defaults to OFF/ON
|
||||
hotkey = "6", -- checked after the engine's keys
|
||||
priority = 20, -- highest eligible wins the world
|
||||
available = function() return Renderer3D.ok() end,
|
||||
update = function(dt, level) Camera.ease(dt, level) end,
|
||||
drawWorld = function(ctx) return renderScene(ctx) end,
|
||||
})
|
||||
```
|
||||
|
||||
Mods should use registries and events instead of requiring private engine
|
||||
modules. Registries currently cover Pokémon, moves, items, maps, tilesets,
|
||||
encounters, trainers, sprites, music, audio, text, scripts, and UI.
|
||||
Three draw stages, each optional; a record needs at least one:
|
||||
|
||||
Enablement is stored in the normal persistent `options.lua` file alongside
|
||||
audio, display, and battle settings, so starting a new game does not disable
|
||||
the selected mods. Changes take effect after restarting the game.
|
||||
| stage | signature | runs |
|
||||
| --- | --- | --- |
|
||||
| `drawWorld` | `(ctx) -> canvas \| nil` | instead of the flat/tilt world pass |
|
||||
| `worldPresent` | `(canvas, ctx) -> canvas` | over the world, **before** the UI composites |
|
||||
| `present` | `(canvas, ctx) -> canvas` | over the whole frame, world and UI alike |
|
||||
|
||||
The loader deliberately does not import or execute arbitrary ROM-hack patches.
|
||||
The supported content source remains the verified base Pokémon Red ROM plus
|
||||
native mods.
|
||||
`worldPresent` is the one to reach for when an effect must leave dialog
|
||||
boxes and menus crisp — a depth-of-field or colour grade on the world only.
|
||||
`present` is for effects that genuinely own the screen, like a CRT curve.
|
||||
|
||||
`ctx` carries the frame: `state`, `cam`, `vw`/`vh` (world-pixel view),
|
||||
`width`/`height` (window pixels), `scale`, `level`, `paletteFor(map)` and
|
||||
`spriteColors(map)`. It also carries `ctx.drawFx(project, scale)` — call it
|
||||
with your own projection and the engine draws every active field effect
|
||||
(the "!" bubble, the Poké Center heal machine, the Fly bird, the fishing
|
||||
rod, Rock Tunnel darkness) at its correct anchor under your camera. There
|
||||
is exactly one copy of each effect, so a new engine effect works in your
|
||||
pipeline without you touching anything.
|
||||
|
||||
Three rules worth knowing:
|
||||
|
||||
- **`gate` governs input, never the draw.** It decides whether the player
|
||||
may *change* the mode (default: free-roam overworld only). A mode that
|
||||
stopped rendering during a warp would flash the flat 2D world every time
|
||||
the player walked through a door.
|
||||
- **`available` is re-read every frame** and is the only thing that decides
|
||||
whether the mode can render at all. Answer `false` on a headless run or a
|
||||
driver with no depth canvas and the engine silently keeps the vanilla 2D
|
||||
path — which is why shipping a pipeline enabled is safe.
|
||||
- **A callback that throws retires its pipeline**, attributed to your mod in
|
||||
the manager's error feed, and the frame falls back to 2D. A broken
|
||||
renderer costs the player a display mode, never the game.
|
||||
|
||||
Returning `nil` from `drawWorld` is a normal answer meaning "not this
|
||||
frame"; the engine draws the vanilla world instead.
|
||||
|
||||
## Battle sprite scaling
|
||||
|
||||
The enemy's front pic draws at 1x and the player's back pic at 2x, the way
|
||||
the Game Boy did. A mod can override either, per species or per image.
|
||||
|
||||
Per species, on the `pokemon` record:
|
||||
|
||||
```lua
|
||||
-- MEW's back pic renders 1.5x; its front pic is untouched
|
||||
mod.content.pokemon:patch("MEW", { battleScaleBack = 1.5 })
|
||||
```
|
||||
|
||||
`battleScaleFront` scales the enemy pic, `battleScaleBack` the player pic;
|
||||
both take a number in `0.25 .. 4.0`.
|
||||
|
||||
Per image, on the `battle_sprite_scales` registry, keyed by the asset path
|
||||
exactly as the data references it:
|
||||
|
||||
```lua
|
||||
mod.content.battle_sprite_scales:register("abra_back", {
|
||||
path = "assets/generated/battle/back/abrab.png",
|
||||
scale = 1.5,
|
||||
})
|
||||
```
|
||||
|
||||
An image-level entry beats the species scale for that one pic, and it is
|
||||
the only way to scale a pic that is not species-keyed — the player's
|
||||
trainer back sprite, held on screen until "Go!", is a bare image path.
|
||||
|
||||
The resolution order at draw time is **image-level → species-level →
|
||||
default** (1x front, 2x back).
|
||||
|
||||
- **The pic stays grounded at every scale.** The player pic keeps its feet
|
||||
flush on the text-box top (`y = 96`); the enemy pic keeps its bottom edge
|
||||
and horizontal centre pinned in its 7×7 slot. A larger pic grows upward
|
||||
and outward from that anchor, never off the shelf.
|
||||
- **Scaling composes with the send-out grow.** The `AnimateSendingOutMon`
|
||||
ball-to-pic grow multiplies your scale through each stage, so a rescaled
|
||||
mon still grows into place from the ball, grounded the whole way.
|
||||
|
||||
## Developer console
|
||||
|
||||
Boot with developer mode on to unlock the in-game console and hot-reload
|
||||
hotkeys. Either set `POKEPORT_DEV=1` in the environment or pass
|
||||
`--developer` on the command line:
|
||||
|
||||
```sh
|
||||
love . --developer
|
||||
```
|
||||
|
||||
While developer mode is active:
|
||||
|
||||
- `` ` `` (backtick) opens the console overlay — a Lua REPL with `game`,
|
||||
`data` and `mods` in scope. Press `` ` `` again to close it.
|
||||
- `F5` hot-reloads mods and asset caches without restarting.
|
||||
|
||||
The console understands these verbs (anything else is evaluated as Lua):
|
||||
|
||||
- `warp MAP [x y]` — teleport to a map (default cell 5,5).
|
||||
- `give ID [n|level]` — add an item (count) or a Pokémon (level).
|
||||
- `flag NAME [on|off]` — read or set an event flag.
|
||||
- `party` — dump the current party.
|
||||
- `mods` — list loaded mods and their state.
|
||||
- `reload` — hot-reload mods (same as `F5`).
|
||||
- `trace PAT | trace off` — trace events/hooks matching a glob pattern.
|
||||
- `help` — list the verbs.
|
||||
|
||||
Developer mode also arms the mod loader's dev tripwire, which flags mods
|
||||
that reach outside their permission set.
|
||||
|
||||
@@ -8,27 +8,42 @@ behavior is in docs/behavior-porting-notes.md.
|
||||
|
||||
## Survey zoom
|
||||
|
||||
The mouse wheel (or `-`/`=`) zooms the overworld between 1 pixel per world
|
||||
pixel (full survey) and 2× the window fit scale (close-up), in crisp
|
||||
integer steps. This has no Game Boy equivalent:
|
||||
The mouse wheel (or `-`/`=`), the Options **ZOOM** row, or hotkey `4`
|
||||
zooms the overworld between 1 pixel per world pixel (full survey) and 2×
|
||||
the window fit scale (close-up), in crisp integer steps. This has no Game
|
||||
Boy equivalent:
|
||||
|
||||
- Connected maps render their full bodies, and their NPCs appear as
|
||||
visual-only "ghosts", they wander but have no sight lines, triggers,
|
||||
dialogue, or collision until the map is actually entered.
|
||||
- Menus, text boxes, and battles draw at normal scale on top of the
|
||||
zoomed world. Zoom input is ignored while a script, menu, or battle is
|
||||
active; the zoom level persists across warps and is never saved.
|
||||
- Beyond the border ring the border block repeats indefinitely (interiors
|
||||
stay black, seaside towns stay water, except OVERWORLD-tileset maps,
|
||||
whose beyond-edge space fills with the solid tree wall instead of the
|
||||
per-map border block), and each visible map area is colorized with its
|
||||
own SGB palette (the original recolored the whole screen per map).
|
||||
active; the zoom offset is persisted as `save.options.zoom` (default
|
||||
`0` = FIT) and survives New Game via `options.lua`.
|
||||
- Hotkey `4` ticks through every integer zoom level (survey → FIT →
|
||||
close-up → wrap). The Options row shows `FIT` / `OUTn` / `INn`.
|
||||
- Beyond the border ring the void fill repeats indefinitely (see VOID
|
||||
FILL below); interiors keep their own border block. Each visible map
|
||||
area is colorized with its own SGB palette (the original recolored the
|
||||
whole screen per map).
|
||||
- Neighbor maps load two connection hops out so corner-adjacent maps
|
||||
don't pop in and out, and ghost NPCs share instances with the real ones
|
||||
so their wander positions persist across seamless connection crossings
|
||||
(a warp or fresh map entry still respawns everything at its script
|
||||
position, like the original's per-entry sprite init).
|
||||
|
||||
## VOID FILL
|
||||
|
||||
The Options **VOID FILL** row picks what paints the infinite beyond-edge
|
||||
space on OVERWORLD-tileset maps during survey zoom:
|
||||
|
||||
- **TREES** (default): solid tree wall block `$0F`.
|
||||
- **WATER**: animated water tile `$14` (same hshift cycle as on-map water).
|
||||
- **BLACK**: solid black.
|
||||
|
||||
Other tilesets are unchanged (house/cave borders stay as authored).
|
||||
Persisted as `save.options.voidFill`.
|
||||
|
||||
## Tilt mode
|
||||
|
||||
The `3` key (and the Options menu TILT row) cycles a visual-only perspective
|
||||
@@ -42,8 +57,10 @@ Game Boy equivalent:
|
||||
rows above the player recede and rows below come toward the viewer. Only
|
||||
things that actually *stand* on the ground draw as upright billboards,
|
||||
unscaled and pixel-identical to flat mode: the player, NPCs, item balls,
|
||||
and the screen-anchored FX attached to them (heal machine glow, emote
|
||||
bubbles, the fishing rod, the FLY bird). An earlier revision tried
|
||||
and the standing FX attached to them (emote bubbles, the fishing rod,
|
||||
the FLY bird). The Poké Center heal-machine overlay stays on the ground
|
||||
plane with the machine tiles (it is OAM glued to a BG graphic, not a
|
||||
standing sprite). An earlier revision tried
|
||||
billboarding buildings/trees/signs too (cutting them out of the ground
|
||||
per hand-curated per-tileset tables); that chased an endless tail of
|
||||
special cases, dense tree canopy, fences fused into grass, building
|
||||
@@ -68,19 +85,31 @@ Game Boy equivalent:
|
||||
|
||||
## Colors mode
|
||||
|
||||
The `2` key (and the Options menu COLORS row) cycles the global shade-remap
|
||||
display mode through **GBC → OG → OG INV → GBC INV → CLASSIC → GBC**:
|
||||
The `2` key (and the Options menu COLORS row) cycles the display mode
|
||||
through **OG RED → SGB → RED++ → OG → OG INV → SGB INV → CLASSIC → OG RED**.
|
||||
The first three are the real colorizations; the rest are DMG-shade novelties:
|
||||
|
||||
- **GBC** (default): current SGB / GBC zone palettes.
|
||||
- **OG RED**: the Game Boy Color boot-ROM look for Pokemon Red -- one global
|
||||
red BG palette + one green OBJ palette, every map, no per-map variation
|
||||
(Pokemon Red has no CGB code, so on a GBC the boot ROM colors it globally).
|
||||
The player/NPCs stay green over the red terrain via the OBP bake +
|
||||
post-zone redraw (`PaletteFX.GBC_BG` / `GBC_OBJ`).
|
||||
- **SGB** (default): the per-map Super Game Boy region palettes
|
||||
(`data/sgb/sgb_palettes.asm`). Sprites tint with the region palette, as on
|
||||
real SGB. (This is the mode formerly mislabeled "GBC".)
|
||||
- **RED++**: pokered-gbc SuperPalettes -- real per-tile GBC coloring plus
|
||||
per-species mon colors (`data/palettes_gbc.lua`).
|
||||
- **OG**: force the four DMG grays (colorization off).
|
||||
- **OG INV**: inverted DMG grays.
|
||||
- **GBC INV**: each SGB zone palette with shade order reversed.
|
||||
- **SGB INV**: each SGB zone palette with shade order reversed.
|
||||
- **CLASSIC**: original Game Boy pea-soup greens
|
||||
(`#9BBC0F` / `#8BAC0F` / `#306230` / `#0F380F`).
|
||||
|
||||
The transform is applied centrally in `PaletteFX.sendColors`, so it covers
|
||||
overworld, menus, battles, and tilt upright billboards. Persisted as
|
||||
`save.options.colors`.
|
||||
The shade-remap transform is applied centrally in `PaletteFX.sendColors`, so
|
||||
it covers overworld, menus, battles, and tilt upright billboards. OG RED's
|
||||
global BG palette is supplied by `OverworldState:overworldBgColors` (per-map
|
||||
override in the overworld pass). Persisted as `save.options.colors`; the
|
||||
`gbc` / `gbc_inv` save ids are kept for back-compat under the new labels.
|
||||
|
||||
## GBC FX
|
||||
|
||||
@@ -125,13 +154,43 @@ Nidorino-vs-Gengar attract scene) mirror the original.
|
||||
Options persist in a standalone `options.lua` (separate from the game
|
||||
progress `save.lua`), so audio/display/battle preferences survive New Game
|
||||
and aren't wiped when a save slot is cleared. Changing a row in the Options
|
||||
menu or cycling hotkeys `2`/`3`/`5` writes immediately; an in-game save also
|
||||
menu or cycling hotkeys `2`/`3`/`4`/`5` writes immediately; an in-game save also
|
||||
flushes the live options. Old saves that still embed an `options` table are
|
||||
migrated once into `options.lua` on load.
|
||||
|
||||
- Music / SFX volume
|
||||
- Music Filter
|
||||
- OG GLITCHES on / off (Gen 1 quirks vs. modern-clean battle rules)
|
||||
- COLORS (GBC / OG / OG INV / GBC INV / CLASSIC), also hotkey `2`
|
||||
- COLORS (OG RED / SGB / RED++ / OG / OG INV / SGB INV / CLASSIC), also
|
||||
hotkey `2` (OG RED = GBC boot-ROM look; RED++ uses pokered-gbc
|
||||
SuperPalettes + per-species mon colors)
|
||||
- TILT (OFF / 15 / 35 / 50), also hotkey `3` while free-roaming
|
||||
- GBC FX (OFF / 1 / 2 / 3 / 4), also hotkey `5`
|
||||
- ZOOM (FIT / OUTn / INn), also hotkey `4` while free-roaming; wheel and
|
||||
`-`/`=` step one level and save
|
||||
- VOID FILL (TREES / WATER / BLACK) for OVERWORLD beyond-edge space
|
||||
- GBC FX (OFF / 1 / 2 / 3 / 4), also hotkey `5`
|
||||
- MAX FPS (30 / 40 / 50 / 60 / 75 / 90 / 100 / 120 / 144 / 160, default 60),
|
||||
a hard render frame-rate cap (`save.options.fpsCap`).
|
||||
|
||||
## Battle transition cascade + white battle letterbox
|
||||
|
||||
Into-battle wipes still run the original eight styles inside the classic
|
||||
160×144 letterbox. On wide/tall windows (survey zoom), matching black 8×8
|
||||
blocks cascade outward from that square into the surrounding world so the
|
||||
void outside the OG wipe fills in lockstep. Once the battle state is up,
|
||||
letterbox voids around the battle canvas fill **white** instead of black
|
||||
so the whole window reads as one continuous battle screen.
|
||||
|
||||
## On-screen touch controls (mobile)
|
||||
|
||||
On Android/iOS the game draws a translucent d-pad (bottom-left), A/B
|
||||
buttons (bottom-right, Game Boy diagonal), and +/- START/SELECT (bottom
|
||||
center) over the frame, using Xelu's CC0 controller prompts
|
||||
(`assets/touch/`). Real buttons, not gestures: press lands the frame the
|
||||
finger does, sliding on the d-pad changes direction without lifting, and
|
||||
multi-touch chords (e.g. hold a direction + tap B) work. The overlay only
|
||||
appears while no controller is being used: the first gamepad button or
|
||||
stick push hides it, the next screen touch brings it back, and unplugging
|
||||
the last controller restores it immediately. Layout re-derives from the
|
||||
window size on rotation. Desktop testing: `POKEPORT_TOUCH=1 love .` forces
|
||||
the overlay on and lets the mouse act as a finger (`=0` forces it off).
|
||||
@@ -0,0 +1,126 @@
|
||||
# Updater
|
||||
|
||||
A fused build (`love.filesystem.isFused()` true) ships a bundled `game.love`
|
||||
baked into the executable, but that bundled copy is only ever the *fallback*.
|
||||
On every launch, before anything else runs, `Boot.run` (`src/update/Boot.lua`)
|
||||
looks in the save directory's `updates/` folder for a downloaded
|
||||
`gen1recomp-X.Y.Z.love` payload that is both strictly newer than the bundled
|
||||
engine version and runnable on this shell. If one qualifies, it is mounted
|
||||
over `/` (so its files win over the fused source for every subsequent
|
||||
`require`) and chainloaded in place: the payload's `main.lua` and `love.load`
|
||||
run as if they had shipped in the executable. A dev/source checkout is never
|
||||
fused, so `Boot.run` no-ops there and the working tree always runs itself.
|
||||
|
||||
The pieces are deliberately layered so the risky part is small. `Boot.select`
|
||||
is a pure function (no `love.*` calls) that, given probed candidates and the
|
||||
bundled `engine`/`shell`, decides what to run and what stale payloads to
|
||||
delete. `Boot.probePayload` mounts one archive at an isolated mountpoint and
|
||||
reads its `src/core/Version.lua` with `loadstring` (never `require`, so it is
|
||||
never cached as a module) to learn its `engine` and `minShell`. `Boot.run`
|
||||
orchestrates the crash guard, enumeration, selection, and the mount +
|
||||
chainload, with full rollback on any failure. Checking for and fetching a
|
||||
new payload is a separate, slower path: `src/update/Check.lua` is a thin
|
||||
main-thread state machine the launcher screen polls, while the curl calls,
|
||||
JSON parsing, and sha256 verification run on a background `love.thread`
|
||||
(`src/update/check_worker.lua`) so a hung network call never blocks a frame.
|
||||
|
||||
## Version.lua fields
|
||||
|
||||
`src/core/Version.lua` carries three fields the updater reads directly (the
|
||||
existing `modApi`, `linkProtocol`, `saveFormat`, and `cache` fields are
|
||||
untouched):
|
||||
|
||||
- `engine` - the semver release, e.g. `"1.4.0"`. The repo default is the
|
||||
`"0.0.0-dev"` placeholder; CI stamps the real `X.Y.Z` into the packed
|
||||
`game.love` only, never the working tree. A `"0.0.0-dev"` engine always
|
||||
reports itself up to date (it never chases a release, and it never counts
|
||||
as a valid payload to chainload).
|
||||
- `shell` - the native-shell contract this build's fused executable
|
||||
implements.
|
||||
- `minShell` - the lowest shell contract required to *run* this payload.
|
||||
|
||||
Bump `minShell` only when a payload needs something the currently-shipped
|
||||
native shell cannot provide, for example a LOVE version bump, a new required
|
||||
system binary, or a change to `love.run` itself (see Known limitations
|
||||
below). An older shell refuses to chainload a payload whose `minShell`
|
||||
exceeds the shell it provides; `Boot.select` keeps that payload in `updates/`
|
||||
rather than deleting it, in case a future shell upgrade can run it, and
|
||||
`Check`'s worker reports `needs_full` so the player is pointed at a full
|
||||
installer instead. Do not bump `minShell` for an ordinary Lua/data release;
|
||||
that is exactly the case the updater exists to avoid a reinstall for.
|
||||
|
||||
## Release assets
|
||||
|
||||
Each tagged release `vX.Y.Z` carries the existing per-platform archives
|
||||
(`gen1recomp-X.Y.Z-macos.zip`, `-windows.zip`, `-linux.zip`,
|
||||
`-android.apk`) plus two assets the updater itself consumes:
|
||||
|
||||
- `gen1recomp-X.Y.Z.love` - the payload, matched by the exact pattern
|
||||
`gen1recomp-<version>.love` (see `isPayloadName` in `Boot.lua` and
|
||||
`Check.parseRelease`).
|
||||
- `sha256sums.txt` - `shasum -a 256` output (`<hex> <filename>`, bare
|
||||
filenames) covering at least the `.love` payload. `Check.parseSums`
|
||||
tolerates a leading `*` binary marker and a `./` prefix but expects the
|
||||
filename otherwise to match the asset name exactly.
|
||||
|
||||
A release missing either asset is treated as "no in-place update available":
|
||||
`Check` reports `needs_full` and sends the player to `Check.releaseUrl()`
|
||||
(`https://github.com/bryanthaboi/pokemon-gen1-recomp-project/releases/latest`).
|
||||
|
||||
## Save-directory layout
|
||||
|
||||
Under the save directory (identity `pokemon-love2d`):
|
||||
|
||||
```
|
||||
updates/gen1recomp-<X.Y.Z>.love downloaded payload(s)
|
||||
updates/pending.txt crash-guard marker
|
||||
```
|
||||
|
||||
`pending.txt` holds the filename of the payload currently being chainloaded.
|
||||
`Boot.run`'s `chainload` writes it immediately before mounting, and removes it
|
||||
on both a successful handoff and a clean rollback. If it is still present the
|
||||
*next* time `Boot.run` starts, the previous boot died mid-handoff, so that
|
||||
named payload is distrusted: it and the marker are deleted before candidates
|
||||
are enumerated. Boot may still fall back to an older valid payload, or to the
|
||||
bundled game, in that case.
|
||||
|
||||
## Update flow
|
||||
|
||||
1. **Boot** (every launch, fused builds only): crash-guard check, enumerate
|
||||
and probe every `updates/*.love`, pick the highest engine that is
|
||||
strictly newer than the bundled one and whose `minShell` this shell
|
||||
satisfies, delete stale payloads, chainload the winner (or run the
|
||||
bundled game if none qualifies).
|
||||
2. **Check** (launcher screen): `Check.start()` kicks off an async check
|
||||
against the GitHub releases API; safe to call every frame, it is a no-op
|
||||
once a check is in flight or has reached a terminal state. `Check.state()`
|
||||
reports `idle | checking | uptodate | available | downloading | ready |
|
||||
needs_full | error` plus the latest version and download progress.
|
||||
3. **Download + verify**: on `available`, `Check.download()` tells the
|
||||
worker to fetch the payload, polling the growing `.part` file for
|
||||
progress. On completion the worker re-fetches `sha256sums.txt`, verifies
|
||||
the payload's sha256, and probes it with `Boot.probePayload` to gate its
|
||||
`minShell` against this shell's `shell`. A verified, runnable payload is
|
||||
renamed into place and reported as `ready`; anything else reports
|
||||
`error` or `needs_full` and leaves `updates/` clean.
|
||||
4. **Restart to apply**: a `ready` payload just sits in `updates/` until the
|
||||
player relaunches; the next launch's Boot step (1) is what actually
|
||||
mounts and runs it. There is no in-session hot-swap.
|
||||
|
||||
## Known limitations
|
||||
|
||||
- **`love.run` persists across handoff.** By the time `chainload` runs, the
|
||||
bundled `love.run` has already returned its stepper to LOVE; redefining the
|
||||
global `love.run` from the payload's `main.lua` does not affect the loop
|
||||
already driving the frame. A payload that must change `love.run` itself
|
||||
needs a `minShell` bump so an older shell refuses to chainload it rather
|
||||
than running with half its intended behavior.
|
||||
- **Android has no in-app download transport yet.** `check_worker.lua`
|
||||
shells out to curl for both the release check and the download; curl is
|
||||
absent on Android, so `Check` degrades to `status = "error"` there (the
|
||||
launcher UI hides on that status) and the player is directed to the
|
||||
releases page via `Check.releaseUrl()` instead.
|
||||
- **Dev/source runs never self-update.** `Boot.run` returns immediately when
|
||||
`love.filesystem.isFused()` is false, and a working tree's `engine` is the
|
||||
`"0.0.0-dev"` placeholder that always reports up to date, so a source
|
||||
checkout is always "the game" itself; updating it means pulling the repo.
|
||||
@@ -13,7 +13,37 @@ local driverCo -- optional frame-driver (POKEPORT_DRIVER=file.lua): a
|
||||
-- coroutine that receives `Game` and yields once per
|
||||
-- frame; used headless (xvfb) for scripted screenshots
|
||||
|
||||
local function bootGame()
|
||||
-- --speed N / POKEPORT_SPEED=N: run the logic clock N times faster without
|
||||
-- touching audio (src/core/GameSpeed.lua). Overrides the saved option so a
|
||||
-- bot or screenshot run is not at the mercy of the player's last choice.
|
||||
local speedOverride = tonumber(os.getenv("POKEPORT_SPEED"))
|
||||
|
||||
-- POKEPORT_TOUCH=1 forces the mobile on-screen controls on and lets the
|
||||
-- mouse stand in for a finger, so the overlay can be exercised on desktop
|
||||
-- (see src/core/TouchControls.lua).
|
||||
local mouseTouch = os.getenv("POKEPORT_TOUCH") == "1"
|
||||
|
||||
-- How many times to run a scripted act+step loop per rendered frame. Only
|
||||
-- scripted runs use this; interactive play fast-forwards through
|
||||
-- Game.speedOverride / the GAME SPEED option instead.
|
||||
local function scriptedIterations()
|
||||
if not (autopilot or driverCo) then return 1 end
|
||||
return math.max(1, math.floor(require("src.core.GameSpeed").clamp(speedOverride)))
|
||||
end
|
||||
|
||||
local function bootGame(version)
|
||||
-- The launcher hands us the chosen game (Red / Blue); scripted and headless
|
||||
-- runs fall back to POKEPORT_VERSION, then Red. Set the active version and
|
||||
-- overlay its extracted cache BEFORE anything requires generated data, so
|
||||
-- data/generated + assets/generated resolve to that version's files.
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
GameVersion.set(version or os.getenv("POKEPORT_VERSION") or "red")
|
||||
require("src.import.CacheFs").mountVersion(GameVersion.get())
|
||||
if love.window and love.window.setTitle then
|
||||
local Version = require("src.core.Version")
|
||||
love.window.setTitle(Version.title(
|
||||
GameVersion.info().displayName .. " (Gen 1 Recompilation Project)"))
|
||||
end
|
||||
Game = require("src.core.Game")
|
||||
Game:load()
|
||||
if os.getenv("POKEPORT_AUTOPILOT") then
|
||||
@@ -24,15 +54,29 @@ local function bootGame()
|
||||
local fn = assert(loadfile(driverPath))()
|
||||
driverCo = coroutine.create(fn)
|
||||
end
|
||||
-- After the two above are known: a scripted run drives the multiplier
|
||||
-- from love.update's loop, so the in-engine one must stay at 1 or the
|
||||
-- two would compound (10x10 = 100 steps per observation).
|
||||
Game.speedOverride = (autopilot or driverCo) and 1 or speedOverride
|
||||
end
|
||||
|
||||
function love.load(args)
|
||||
-- Self-updater boot shell: a fused build may mount and chainload a newer
|
||||
-- downloaded payload here. True means it took over, so we must stop. A
|
||||
-- dev / source checkout no-ops (see src/update/Boot.lua).
|
||||
local Boot = require("src.update.Boot")
|
||||
if Boot.run(args) then return end
|
||||
|
||||
local savePath
|
||||
for i, a in ipairs(args or {}) do
|
||||
if a == "--editor" then
|
||||
editorMode = true
|
||||
elseif a == "--developer" then
|
||||
_G.POKEPORT_DEV_MODE = true
|
||||
elseif a == "--save" and args[i + 1] and args[i + 1] ~= "" then
|
||||
savePath = args[i + 1]
|
||||
elseif a == "--speed" and tonumber(args[i + 1]) then
|
||||
speedOverride = tonumber(args[i + 1])
|
||||
end
|
||||
end
|
||||
love.graphics.setDefaultFilter("nearest", "nearest")
|
||||
@@ -47,43 +91,82 @@ function love.load(args)
|
||||
end
|
||||
|
||||
local RomImporter = require("src.import.RomImporter")
|
||||
if os.getenv("POKEPORT_FORCE_IMPORT") == "1" or not RomImporter.isReady() then
|
||||
Importer = RomImporter.new(function()
|
||||
if os.getenv("POKEPORT_IMPORT_ONLY") == "1" then
|
||||
love.event.quit()
|
||||
return
|
||||
end
|
||||
Importer = nil
|
||||
bootGame()
|
||||
end)
|
||||
local importPath = os.getenv("POKEPORT_IMPORT_ROM")
|
||||
if importPath then Importer:startPath(importPath) end
|
||||
local forceImport = os.getenv("POKEPORT_FORCE_IMPORT") == "1"
|
||||
local importPath = os.getenv("POKEPORT_IMPORT_ROM")
|
||||
-- Scripted / headless runs pick their game from POKEPORT_VERSION (default
|
||||
-- Red); the launcher's per-column choice does not apply to them.
|
||||
local scriptedVersion = os.getenv("POKEPORT_VERSION") or "red"
|
||||
local ready = RomImporter.isReady(scriptedVersion)
|
||||
-- Scripted / headless runs have to reach the game with no human pressing
|
||||
-- Play: an autopilot, a frame driver, an import-only build step, or an
|
||||
-- explicit ROM path all bypass the interactive launcher and keep today's
|
||||
-- import-then-boot (or boot-straight-in) behavior.
|
||||
local scripted = os.getenv("POKEPORT_AUTOPILOT") or os.getenv("POKEPORT_DRIVER")
|
||||
or os.getenv("POKEPORT_IMPORT_ONLY") == "1" or importPath ~= nil
|
||||
|
||||
if scripted then
|
||||
if forceImport or not ready then
|
||||
-- The importer detects the dropped/loaded ROM's version by SHA-1 and
|
||||
-- passes it to onComplete; boot that version.
|
||||
Importer = RomImporter.new(function(version)
|
||||
if os.getenv("POKEPORT_IMPORT_ONLY") == "1" then
|
||||
love.event.quit()
|
||||
return
|
||||
end
|
||||
Importer = nil
|
||||
bootGame(version or scriptedVersion)
|
||||
end)
|
||||
if importPath then Importer:startPath(importPath) end
|
||||
return
|
||||
end
|
||||
bootGame(scriptedVersion)
|
||||
return
|
||||
end
|
||||
bootGame()
|
||||
|
||||
-- Interactive: the launcher always runs. Red and Blue are each live: a
|
||||
-- column shows Play when that game's ROM is already imported, or Choose ROM
|
||||
-- / drag-drop when it is not (Yellow is still a placeholder). Any dropped
|
||||
-- .gb is routed to Red or Blue by its SHA-1; pressing Play boots that game.
|
||||
Importer = RomImporter.new(function(version)
|
||||
Importer = nil
|
||||
bootGame(version)
|
||||
end, { launcher = true, forceImport = forceImport })
|
||||
end
|
||||
|
||||
function love.update(dt)
|
||||
if editorMode then return EditorApp.update(dt) end
|
||||
if Importer then return Importer:update(dt) end
|
||||
|
||||
-- Scripted runs (autopilot / POKEPORT_DRIVER) observe and act exactly
|
||||
-- once per Game:update, so they must keep a 1:1 relationship with the
|
||||
-- logic step. Fast-forwarding them by scaling the step inside
|
||||
-- Game:update would run N steps per observation: a held direction walks
|
||||
-- through all N, the player slides past the waypoint, and the script
|
||||
-- re-plans from an overshot cell. So iterate the whole act+step loop
|
||||
-- instead -- same script, just more of it per rendered frame.
|
||||
local iterations = scriptedIterations()
|
||||
|
||||
if autopilot then
|
||||
autopilot.update()
|
||||
Game:update(1 / 60) -- deterministic stepping for the autopilot
|
||||
for _ = 1, iterations do
|
||||
autopilot.update()
|
||||
Game:update(1 / 60) -- deterministic stepping for the autopilot
|
||||
end
|
||||
return
|
||||
end
|
||||
if driverCo then
|
||||
local ok, err = coroutine.resume(driverCo, Game)
|
||||
if not ok then
|
||||
print("driver error: " .. tostring(err))
|
||||
love.event.quit(1)
|
||||
return
|
||||
for _ = 1, iterations do
|
||||
local ok, err = coroutine.resume(driverCo, Game)
|
||||
if not ok then
|
||||
print("driver error: " .. tostring(err))
|
||||
love.event.quit(1)
|
||||
return
|
||||
end
|
||||
if coroutine.status(driverCo) == "dead" then
|
||||
love.event.quit()
|
||||
return
|
||||
end
|
||||
Game:update(1 / 60)
|
||||
end
|
||||
if coroutine.status(driverCo) == "dead" then
|
||||
love.event.quit()
|
||||
return
|
||||
end
|
||||
Game:update(1 / 60)
|
||||
return
|
||||
end
|
||||
Game:update(dt)
|
||||
@@ -139,6 +222,31 @@ function love.gamepadaxis(joystick, axis, value)
|
||||
Game:gamepadaxis(joystick, axis, value)
|
||||
end
|
||||
|
||||
function love.joystickremoved(joystick)
|
||||
if editorMode then return end
|
||||
if Importer then return end
|
||||
Game:joystickremoved(joystick)
|
||||
end
|
||||
|
||||
-- f is true on focus gained, false on focus lost (e.g. alt-tab). A held
|
||||
-- direction's key-up can be delivered to the OS instead of the game while
|
||||
-- unfocused, so reset input on either transition rather than trust it.
|
||||
function love.focus(f)
|
||||
if editorMode then return end
|
||||
if Importer then
|
||||
if Importer.focus then Importer:focus(f) end
|
||||
return
|
||||
end
|
||||
Game:focus(f)
|
||||
end
|
||||
|
||||
-- v is true when the window becomes visible again, false on minimize.
|
||||
function love.visible(v)
|
||||
if editorMode then return end
|
||||
if Importer then return end
|
||||
Game:visible(v)
|
||||
end
|
||||
|
||||
function love.touchpressed(id, x, y, dx, dy, pressure)
|
||||
if editorMode then return end
|
||||
if Importer then return Importer:mousepressed(x, y, 1) end
|
||||
@@ -171,6 +279,9 @@ function love.mousepressed(x, y, button)
|
||||
if editorMode and EditorApp.mousepressed then
|
||||
return EditorApp.mousepressed(x, y, button)
|
||||
end
|
||||
if mouseTouch and Game and button == 1 then
|
||||
Game:touchpressed("mouse", x, y)
|
||||
end
|
||||
end
|
||||
|
||||
function love.mousereleased(x, y, button)
|
||||
@@ -178,6 +289,16 @@ function love.mousereleased(x, y, button)
|
||||
if editorMode and EditorApp.mousereleased then
|
||||
return EditorApp.mousereleased(x, y, button)
|
||||
end
|
||||
if mouseTouch and Game and button == 1 then
|
||||
Game:touchreleased("mouse", x, y)
|
||||
end
|
||||
end
|
||||
|
||||
function love.mousemoved(x, y)
|
||||
if editorMode or Importer then return end
|
||||
if mouseTouch and Game and love.mouse.isDown(1) then
|
||||
Game:touchmoved("mouse", x, y)
|
||||
end
|
||||
end
|
||||
|
||||
function love.textinput(text)
|
||||
@@ -191,6 +312,9 @@ function love.quit()
|
||||
if editorMode and EditorApp.quit then
|
||||
return EditorApp.quit() -- return true to abort quit
|
||||
end
|
||||
pcall(function()
|
||||
require("src.core.DiscordPresence").shutdown()
|
||||
end)
|
||||
end
|
||||
|
||||
function love.filedropped(file)
|
||||
@@ -199,3 +323,78 @@ function love.filedropped(file)
|
||||
end
|
||||
if Importer then Importer:filedropped(file) end
|
||||
end
|
||||
|
||||
local function pacingEnabled()
|
||||
if os.getenv("POKEPORT_AUTOPILOT") then return false end
|
||||
if os.getenv("POKEPORT_DRIVER") then return false end
|
||||
if os.getenv("POKEPORT_IMPORT_ONLY") == "1" then return false end
|
||||
return true
|
||||
end
|
||||
|
||||
function love.run()
|
||||
if love.load then love.load(love.arg.parseGameArguments(arg), arg) end
|
||||
|
||||
-- don't let love.load's cost land in the first frame's dt
|
||||
if love.timer then love.timer.step() end
|
||||
|
||||
local FrameCap = require("src.core.FrameCap")
|
||||
local paced = pacingEnabled()
|
||||
-- The deadline the next present() should not beat. Carried forward one
|
||||
-- budget per frame so pacing stays even instead of drifting with the
|
||||
-- per-frame sleep-granularity jitter.
|
||||
local nextFrame = love.timer and love.timer.getTime() or 0
|
||||
local dt = 0
|
||||
|
||||
return function()
|
||||
-- process events
|
||||
if love.event then
|
||||
love.event.pump()
|
||||
for name, a, b, c, d, e, f in love.event.poll() do
|
||||
if name == "quit" then
|
||||
if not love.quit or not love.quit() then
|
||||
return a or 0
|
||||
end
|
||||
end
|
||||
love.handlers[name](a, b, c, d, e, f)
|
||||
end
|
||||
end
|
||||
|
||||
-- update dt
|
||||
if love.timer then dt = love.timer.step() end
|
||||
|
||||
-- call update and draw
|
||||
if love.update then love.update(dt) end
|
||||
|
||||
if love.graphics and love.graphics.isActive() then
|
||||
love.graphics.origin()
|
||||
love.graphics.clear(love.graphics.getBackgroundColor())
|
||||
if love.draw then love.draw() end
|
||||
love.graphics.present()
|
||||
end
|
||||
|
||||
if love.timer then
|
||||
if paced then
|
||||
-- Sleep out the remainder of the frame budget, measured from the
|
||||
-- carried deadline, in small chunks so the OS timer stays
|
||||
-- responsive. vsync is untouched: when it already paces slower
|
||||
-- than the cap the remainder is <= 0 and this rounds to a no-op.
|
||||
local budget = 1 / FrameCap.current
|
||||
nextFrame = nextFrame + budget
|
||||
local now = love.timer.getTime()
|
||||
-- A stall (alt-tab, a GC pause, a blocked import) can leave the
|
||||
-- deadline more than a full budget in the past; re-anchor to now so
|
||||
-- we pace the next frame rather than burst uncapped to catch up.
|
||||
if now - nextFrame > budget then
|
||||
nextFrame = now
|
||||
end
|
||||
while true do
|
||||
local remaining = nextFrame - love.timer.getTime()
|
||||
if remaining <= 0 then break end
|
||||
love.timer.sleep(remaining < 0.001 and remaining or 0.001)
|
||||
end
|
||||
else
|
||||
love.timer.sleep(0.001)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
# Android (love-android 11.5a)
|
||||
|
||||
`mobile/android/` is a **vendored copy** of
|
||||
[love2d/love-android](https://github.com/love2d/love-android) at tag
|
||||
**11.5a** (matches `conf.lua` `t.version = "11.5"`), tracked directly in
|
||||
this repo, no git submodules involved. Nested `love` sources live at
|
||||
`mobile/android/love/src/jni/love` (also vendored). Build outputs
|
||||
(`app/build/`, `love/build/`, `.gradle/`, `local.properties`) stay
|
||||
gitignored.
|
||||
|
||||
## Refreshing the vendored tree
|
||||
|
||||
To pick up a newer love-android release, replace the tree and re-vendor:
|
||||
|
||||
```bash
|
||||
rm -rf mobile/android
|
||||
git clone --depth 1 --branch <new-tag> --recurse-submodules --shallow-submodules \
|
||||
https://github.com/love2d/love-android.git mobile/android
|
||||
rm -rf mobile/android/.git mobile/android/love/src/jni/love/.git \
|
||||
mobile/android/.gitmodules
|
||||
```
|
||||
|
||||
`scripts/build_android.sh` re-applies project branding on every run
|
||||
(`gradle.properties` app id / name / portrait, plus permission trims), so a
|
||||
refresh is safe, just rebuild.
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
# Build the APK
|
||||
scripts/build_android.sh
|
||||
|
||||
# Build the APK, setting app.version_name/app.version_code to match a release
|
||||
scripts/build_android.sh --version 0.2.5
|
||||
|
||||
# Zip game.love + branding only (no Android SDK required)
|
||||
scripts/build_android.sh --package-only
|
||||
```
|
||||
|
||||
Or via `scripts/build.sh android [--version X.Y.Z]`.
|
||||
|
||||
The embedded `game.love` deliberately excludes `data/generated/`,
|
||||
`assets/generated/`, and any ROM. It contains the first-boot Lua importer and
|
||||
`tools/rom_manifest.json`.
|
||||
|
||||
ROM import on Android uses `love.system.pickFile()` →
|
||||
`GameActivity.showRomFilePicker` (Storage Access Framework), which copies the
|
||||
chosen file to `picked_rom.gb` under the app save directory. `RomImporter`
|
||||
imports pending (not-yet-ready) `.gb` files from that folder on Choose /
|
||||
refocus; see `docs/launcher.md` (Android multi-ROM import). The APK payload
|
||||
itself remains data-free (no embedded ROM or generated cache).
|
||||
|
||||
### SDK / NDK
|
||||
|
||||
love-android 11.5a expects:
|
||||
|
||||
- **JDK 17**
|
||||
- Android SDK with **API 34**
|
||||
- NDK **25.2.9519653** (Apple Silicon host supported)
|
||||
|
||||
Set `ANDROID_SDK_ROOT` (or `ANDROID_HOME`), or let the script write
|
||||
`local.properties` when it finds `~/Library/Android/sdk`.
|
||||
|
||||
Gradle flavor used: **`embedNoRecord`** (game fused into the APK, no microphone).
|
||||
Build task: `assembleEmbedNoRecordDebug`.
|
||||
|
||||
The APK lands under `app/build/outputs/apk/embedNoRecord/debug/`.
|
||||
`scripts/build_android.sh` also copies it to `dist/android/debug/`.
|
||||
|
||||
### Payload path
|
||||
|
||||
`app/src/embed/assets/game.love` - zip of `main.lua`, `conf.lua`, `src/`,
|
||||
`data/`, `assets/`, and `tools/rom_manifest.json`. Generated game data,
|
||||
scripts, tests, and mobile build sources are excluded.
|
||||
|
||||
## Branding (applied by the build script)
|
||||
|
||||
| Setting | Value |
|
||||
| --- | --- |
|
||||
| `app.application_id` | `com.theboisclub.pokemonred` |
|
||||
| `app.name` | Pokemon Red |
|
||||
| `app.orientation` | `portrait` |
|
||||
| `app.version_name` / `app.version_code` | set from `--version X.Y.Z` (code = major*10000 + minor*100 + patch); left as-is if `--version` is omitted |
|
||||
| Permissions | INTERNET / RECORD_AUDIO / WRITE_EXTERNAL_STORAGE stripped; VIBRATE + BLUETOOTH kept |
|
||||
|
||||
## Releases
|
||||
|
||||
`.github/workflows/release.yml` builds the APK with `--version` set to the
|
||||
release version and publishes it alongside the macOS/Windows/Linux builds as
|
||||
`PokemonRed-<version>-android.apk`.
|
||||
|
||||
## Signing
|
||||
|
||||
Signed with the default Android keystore (no setup required).
|
||||
@@ -0,0 +1,33 @@
|
||||
assets/
|
||||
bin/
|
||||
gen/
|
||||
libs/
|
||||
obj/
|
||||
|
||||
local.properties
|
||||
|
||||
.*.swp
|
||||
.*~
|
||||
*~
|
||||
*.o
|
||||
*.so
|
||||
*.a
|
||||
*.orig
|
||||
*.rej
|
||||
|
||||
|
||||
# Downloaded gradle wrapper
|
||||
.gradle
|
||||
|
||||
# Android Studio project files
|
||||
.idea
|
||||
*.iml
|
||||
|
||||
# Gradle build files
|
||||
build
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
|
||||
# love-android specific
|
||||
!app/src/main/assets/
|
||||
app/src/main/assets/game.love
|
||||
@@ -0,0 +1,228 @@
|
||||
Android Port of LÖVE, an awesome 2D game engine for Lua (http://love2d.org)
|
||||
Copyright (c) 2006-2023 LOVE Development Team
|
||||
|
||||

|
||||
|
||||
Download:
|
||||
---------
|
||||
|
||||
You can download pre-built Android packages from https://github.com/love2d/love/releases/latest that allow
|
||||
you to run .love files by opening them using a file manager of your choice.
|
||||
|
||||
Important:
|
||||
----------
|
||||
|
||||
If you want to build from source, make sure to clone the submodules too. Often errors include missing
|
||||
liblove.so and "Missing LÖVE" error when building. A proper way to clone this repository is:
|
||||
|
||||
```
|
||||
git clone --recurse-submodules https://github.com/love2d/love-android
|
||||
```
|
||||
|
||||
Add `-b <branch/tag name>` and `--depth 1` if needed.
|
||||
|
||||
If you already cloned the repository but forgot to initialize the submodules, execute:
|
||||
|
||||
```
|
||||
git submodule sync --recursive
|
||||
git submodule update --init --force --recursive
|
||||
```
|
||||
|
||||
In the repository directory. For the last command, add `--depth 1` if needed.
|
||||
|
||||
Instructions:
|
||||
-------------
|
||||
|
||||
Detailed instructions can be viewed at https://github.com/love2d/love-android/wiki
|
||||
|
||||
Quick Start:
|
||||
------------
|
||||
|
||||
Before you start, install JDK 17 (not later not earlier). If you intend to build from Android Studio, skip this step as
|
||||
Android Studio bundles its own JDK 17.
|
||||
|
||||
Install Android SDK with SDK API 34 (34.x.y) and Android NDK 25.2.9519653, set the environment variable
|
||||
`ANDROID_SDK_ROOT` to your Android SDK location and run:
|
||||
|
||||
```
|
||||
$ ./gradlew assembleNormalRecord
|
||||
```
|
||||
|
||||
in the root folder of this project. This should give you an .apk file in the `app/build/outputs/apk/normalRecord`
|
||||
subdirectory that you can then sign and install on your phone. The `normalRecord` .apk variant is what you normally have
|
||||
when downloading APK from https://love2d.org.
|
||||
|
||||
If you want to put your game inside the APK, you can either:
|
||||
|
||||
1. Put all your games in `app/src/embed/assets` such that your `main.lua` path is `app/src/embed/assets/main.lua`; or
|
||||
2. Put your zipped \*.love in `app/src/embed/assets` with name `game.love`
|
||||
|
||||
And change the application id, application version string and codes, application display name, and the icons (see
|
||||
[Game Packaging Wiki](https://github.com/love2d/love-android/wiki/Game-Packaging). Afterwards,
|
||||
run either `gradlew assembleEmbedNoRecordRelease` (or `gradlew assembleEmbedRecordRelease` if your game uses microphone) to
|
||||
generate APK which you can install or `gradlew bundleEmbedNoRecordRelease` (or `gradlew bundleEmbedRecordRelease`) to generate
|
||||
AAB which you can upload to Play Store.
|
||||
|
||||
Alternatively, you can install Android Studio **2022.3.1** or later. After opening it for the first time, open its SDK Manager
|
||||
and on the tab "SDK Tools", tick "Show Package Details" then select NDK (Side By Side) version 25.2.9519653. After that, open
|
||||
the repository root.
|
||||
|
||||
Notice: Previously, the embed + APKTool method is preferred, but recent announcements by Google render that method obsolete.
|
||||
|
||||
Bugs:
|
||||
-----
|
||||
|
||||
Bugs and/or feature requests should be reported to the issue tracker at:
|
||||
* https://github.com/love2d/love-android/issues - LÖVE-Android-specific issues
|
||||
* https://github.com/love2d/love/issues - for LÖVE in general
|
||||
|
||||
Note: If in doubt, fill an issue report to https://github.com/love2d/love/issues. We'll transfer the issue to this repository as needed.
|
||||
|
||||
Changelog:
|
||||
----------
|
||||
|
||||
11.5:
|
||||
|
||||
* Contains all relevant changes for desktop LÖVE [11.5](https://love2d.org/wiki/11.5).
|
||||
* Fixed audio in Android still playing in the background in certain cases. ([love2d/love#1828](https://github.com/love2d/love/issues/1828))
|
||||
|
||||
|
||||
11.4:
|
||||
|
||||
* Contains all relevant changes for desktop LÖVE [11.4](https://love2d.org/wiki/11.4).
|
||||
* Changed how LOVE looks for game due to Android restrictions.
|
||||
* Fixed "pointer too large" and "bad lightuserdata" issue when compiling LOVE with recent Android SDK and running LOVE on Android 11 and later.
|
||||
* For Developers: Added experimental support for 3rd-party Lua modules. Please refer to `love/src/jni/lua-modules` for more information.
|
||||
* For Packaging: Added experimental fusing method. [See the FAQ for details](https://github.com/love2d/love-android/wiki/FAQ---Frequently-Asked-Questions#whats-experimental-fusing-method).
|
||||
|
||||
11.3:
|
||||
|
||||
* Contains all relevant changes for desktop LÖVE [11.3](https://love2d.org/wiki/11.3).
|
||||
* Added support for microphone recording on Android. **This is disabled in Play Store builds**.
|
||||
* Added t.audio.mic (`false` by default). On Android, setting it to true requests microphone recording permission from the user.
|
||||
* Fixed performance regression on Android devices with Adreno GPU.
|
||||
* Fixed video playback support on Android devices with Adreno GPU.
|
||||
|
||||
11.2:
|
||||
|
||||
* Contains all relevant changes for desktop LÖVE [11.2](https://love2d.org/wiki/11.2).
|
||||
* Added support for ARM64 devices to comply with Play Store requirements.
|
||||
* Fixed `love.system.openURL` crashing in some cases.
|
||||
* Changed target SDK to 28 so it comply with Play Store requirements.
|
||||
|
||||
0.10.2:
|
||||
|
||||
* Contains all relevant changes for desktop LÖVE [0.10.2](https://love2d.org/wiki/0.10.2).
|
||||
* Upgrade of SDL2 to 2.0.5 (fixes an issue with the accelerometer)
|
||||
|
||||
0.10.1:
|
||||
|
||||
* Contains all relevant changes for desktop LÖVE [0.10.1](https://love2d.org/wiki/0.10.1).
|
||||
* Added a new love.conf flag t.externalstorage, which determines whether files are saved in internal or external storage on Android devices.
|
||||
* Fixed audio on Android to pause when the app is inactive, and resume when the app becomes active again.
|
||||
* Fixed a driver bug on some Android devices which caused all objects to show up as black.
|
||||
* Fixed love.graphics.clear(colortable) causing crashes on OpenGL ES 2 systems when a Canvas is active.
|
||||
* New icons
|
||||
|
||||
0.10.0:
|
||||
|
||||
* first official release!
|
||||
* Disabled JIT by default as it can cause performance problems. To enable JIT call jit.on()
|
||||
|
||||
0.10.0-alpha2:
|
||||
|
||||
* Update to the next love API 0.10.0 (not yet officially released)
|
||||
* Added building of libtheora
|
||||
* Updated LuaJIT from 2.0.1 to 2.1
|
||||
* Fixed a compatibility issue with Android 2.3 devices
|
||||
* Updated libogg from 1.3.2. to 1.3.5
|
||||
* Updated OpenAL to 1.17.0
|
||||
* Updated SDL2 to a dev version of 2.0.4
|
||||
|
||||
0.9.2a:
|
||||
|
||||
* Added bugfix for ParticleSystem:clone
|
||||
|
||||
0.9.2:
|
||||
|
||||
* updated API to match that of LÖVE 0.9.2
|
||||
* love.window.setFullscreen can be used to switch between regular and immersive mode without status and navbar
|
||||
* added loading of games by opening a main.lua file
|
||||
* quitting LÖVE now conforms to the Android application lifecycle
|
||||
* stop vibrator when app is paused
|
||||
* fixed battery drain by properly pausing OpenAL device
|
||||
* fixed printing of non-number and non-string values
|
||||
* fixed compilation of Android NDK r10
|
||||
* fixed compilation warnings concerning ```APP_PLATFORM```
|
||||
* old instance is shut down when opening a new game (note: it may crash when opening games at a high frequency, e.g. more than 2 per second)
|
||||
* updated OpenAL-Soft to version 1.16.0
|
||||
* updated to newer SDL version (f9244b2a151)
|
||||
|
||||
0.9.1b:
|
||||
|
||||
* added love.system.vibrate(seconds)
|
||||
* print statements are now redirected to logcat. Output is prefixed with "[LOVE] "
|
||||
* removed DevIL, libpng, libjpeg, libmng, and libtiff
|
||||
* pngs are loaded using lodepng and jpegs using libturbo-jpeg
|
||||
* repeatedly fixed a bug which caused Release builds to crash
|
||||
* update to latest mobile-common branch
|
||||
|
||||
0.9.1a:
|
||||
|
||||
* using latest SDL\_androidgl.c (fixes some random performance issues)
|
||||
* using latest love-android @ changeset 8659be0e75a3 (adds support for
|
||||
compressed textures)
|
||||
|
||||
0.9.1:
|
||||
|
||||
* uses 0.9.1 API
|
||||
* fixed crash on Moto G (and possibly other devices). This was a nasty bug that would just show a blue screen without an error message. The bug was resolved using the help of headchant
|
||||
* fixed loading of jpegs (it probably hasn't worked up to now)
|
||||
* fixed issues with looping over active touches. This fix was sponsored by slime!
|
||||
|
||||
beta2:
|
||||
|
||||
* fixed bug with canvases
|
||||
* fixed writing of files when no identity in conf.lua was set
|
||||
* added file association (somewhat experimental)
|
||||
|
||||
beta1:
|
||||
|
||||
* fixed nasty crash on startup bug
|
||||
* fixed love.filesystem.getDirectoryItems()
|
||||
|
||||
alpha9:
|
||||
|
||||
* Packaged games do not get duplicated for loading, instead are loaded from memory (!!!)
|
||||
* Using inofficial physfs 2.1
|
||||
* Removed love.android.getDisplayMetrics(), instead use love.window.getPixelScale()
|
||||
* Properly link LGPL libraries dynamically. Everything else is linked statically
|
||||
* Added an icon (design by @josefnpat)
|
||||
* Fixed crash on startup on OUYA (and possibly other devices)
|
||||
|
||||
alpha8:
|
||||
|
||||
* Exposing DisplayMetrics in love.android.getDisplayMetrics())
|
||||
* Accelerometer is now available as a joystick
|
||||
* enabled armv6 compilation (larger files, better compatibility with Tegra2 devices)
|
||||
* updated to latest mobile-common branch (including (very) basic multi-touch gesture tracking)
|
||||
* updated OpenAL from 1.13 to 1.15.1
|
||||
* updated jpeg library from 8c to 9a
|
||||
* updated lcms from 2.2 to 2.5
|
||||
* updated libogg from 1.3.0 to 1.3.1
|
||||
* updated libvorbis from 1.3.2 to 1.3.4
|
||||
* updated mpg123 from 1.13.4 to 1.17.0
|
||||
|
||||
alpha7:
|
||||
|
||||
* love.system.getOS() now returns "Android"
|
||||
* hardware search key is reported as "search"
|
||||
* switched to mobile-common branch
|
||||
* using new love.touch module (love.touchpressed(id,x,y,p), love.touchmoved(id,x,y,p), love.touchmoved(id,x,y,p))
|
||||
* added LOVE_ANDROID define
|
||||
|
||||
License:
|
||||
--------
|
||||
|
||||
This project contains code from multiple projects using various licenses. Please look at LOVE
|
||||
[license.txt](https://github.com/love2d/love/blob/master/license.txt) for the respective licenses.
|
||||
@@ -0,0 +1,72 @@
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
namespace "org.love2d.android.executable"
|
||||
ndkVersion '25.2.9519653'
|
||||
|
||||
defaultConfig {
|
||||
applicationId project.properties["app.application_id"]
|
||||
versionCode project.properties["app.version_code"].toInteger()
|
||||
versionName project.properties["app.version_name"]
|
||||
minSdk 16
|
||||
compileSdk 34
|
||||
targetSdk 34
|
||||
|
||||
def getAppName = {
|
||||
def nameArray = project.properties["app.name_byte_array"]
|
||||
def name = project.properties["app.name"]
|
||||
if (name != null && nameArray != null) {
|
||||
throw new Exception("Only define either `app.name` or `app.name_byte_array` in gradle.properties, but not both!")
|
||||
}
|
||||
|
||||
if (name == null) {
|
||||
def nameArraySplit = nameArray.split(",")
|
||||
def nameBytes = new byte[nameArraySplit.length]
|
||||
def count = 0
|
||||
for (String s: nameArraySplit) {
|
||||
nameBytes[count++] = (byte) Integer.parseInt(s)
|
||||
}
|
||||
return new String(nameBytes, StandardCharsets.UTF_8)
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
manifestPlaceholders = [
|
||||
NAME:getAppName(),
|
||||
ORIENTATION:project.properties["app.orientation"],
|
||||
]
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
flavorDimensions = ['mode', 'recording']
|
||||
productFlavors {
|
||||
normal {
|
||||
dimension 'mode'
|
||||
}
|
||||
embed {
|
||||
dimension 'mode'
|
||||
}
|
||||
record {
|
||||
dimension 'recording'
|
||||
}
|
||||
noRecord {
|
||||
dimension 'recording'
|
||||
}
|
||||
}
|
||||
lint {
|
||||
abortOnError false
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api 'androidx.multidex:multidex:2.0.1'
|
||||
api fileTree(dir: 'libs', include: ['*.jar'])
|
||||
api 'androidx.appcompat:appcompat:1.6.1'
|
||||
api project(':love')
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in /Users/diglet/android-sdk-macosx/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
-keep class org.love2d.android.GameActivity { *; }
|
||||
-keep class org.libsdl.app.** { *; }
|
||||
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||
<!-- OpenGL ES 2.0 -->
|
||||
<uses-feature android:glEsVersion="0x00020000" />
|
||||
<!-- Touchscreen support -->
|
||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
||||
<!-- Game controller support -->
|
||||
<uses-feature android:name="android.hardware.bluetooth" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.gamepad" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.usb.host" android:required="false" />
|
||||
<!-- External mouse input events -->
|
||||
<uses-feature android:name="android.hardware.type.pc" android:required="false" />
|
||||
<!-- Low latency audio -->
|
||||
<uses-feature android:name="android.hardware.audio.low_latency" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.audio.pro" android:required="false" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@drawable/love"
|
||||
android:label="${NAME}" >
|
||||
<activity
|
||||
android:name="org.love2d.android.GameActivity"
|
||||
android:exported="true"
|
||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation"
|
||||
android:label="${NAME}"
|
||||
android:launchMode="singleInstance"
|
||||
android:screenOrientation="${ORIENTATION}"
|
||||
android:resizeableActivity="false"
|
||||
android:theme="@android:style/Theme.NoTitleBar.Fullscreen" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="tv.ouya.intent.category.GAME" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -0,0 +1,5 @@
|
||||
values/generated_strings.xml
|
||||
values/generated_integers.xml
|
||||
drawable-*/icon.png
|
||||
drawable-*/notificationicon.png
|
||||
mipmap-*/ic_launcher.png
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 10 KiB |
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="colorPrimary">#3F51B5</color>
|
||||
<color name="colorPrimaryDark">#303F9F</color>
|
||||
<color name="colorAccent">#FF4081</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<resources>
|
||||
|
||||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="colorAccent">@color/colorAccent</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
>
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" tools:node="remove" />
|
||||
</manifest>
|
||||
@@ -0,0 +1,68 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools" >
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" tools:replace="maxSdkVersion" />
|
||||
|
||||
<application tools:node="merge">
|
||||
<service android:name="org.love2d.android.DownloadService" />
|
||||
<activity
|
||||
android:name="org.love2d.android.GameActivity"
|
||||
tools:node="merge" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="file" />
|
||||
<data android:mimeType="*/*" />
|
||||
<data android:pathPattern=".*\\.love" />
|
||||
<data android:host="*" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="content" />
|
||||
<data android:mimeType="application/x-love-game" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="content" />
|
||||
<data android:mimeType="application/octet-stream" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="org.love2d.android.DownloadActivity"
|
||||
android:exported="true"
|
||||
android:noHistory="true"
|
||||
android:theme="@android:style/Theme.NoTitleBar" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="http"
|
||||
android:host="*"
|
||||
android:mimeType="*/*"
|
||||
android:pathPattern=".*\\.love" />
|
||||
<data android:scheme="https"
|
||||
android:host="*"
|
||||
android:mimeType="*/*"
|
||||
android:pathPattern=".*\\.love" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="org.love2d.android.SelectorActivity"
|
||||
android:label="LÖVE Loader"
|
||||
android:exported="true"
|
||||
android:enabled="@bool/selector_active"
|
||||
android:theme="@style/Theme.AppCompat.NoActionBar" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="tv.ouya.intent.category.GAME" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -0,0 +1,35 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
gradle.projectsEvaluated {
|
||||
tasks.withType(JavaCompile) {
|
||||
options.compilerArgs << "-Xlint:deprecation"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.1.1'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
}
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
# This file is automatically generated by Android Tools.
|
||||
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
|
||||
#
|
||||
# This file must be checked in Version Control Systems.
|
||||
#
|
||||
# To customize properties used by the Ant build system use,
|
||||
# "build.properties", and override values to adapt the script to your
|
||||
# project structure.
|
||||
|
||||
# Project target.
|
||||
target=android-19
|
||||
@@ -0,0 +1,21 @@
|
||||
# To set the application display name, only define either `app.name` or `app.name_byte_array`.
|
||||
# For a rule of thumb:
|
||||
# * Use `app.name` if your app name doesn't contain any non-ANSI characters.
|
||||
# * Otherwise, convert your app name to UTF-8 byte array, comma delimited, and put
|
||||
# it in `app.name_byte_array`
|
||||
#app.name=LÖVE for Android
|
||||
|
||||
app.application_id=com.theboisclub.pokemonred
|
||||
# fullUser: allow every orientation the player's device permits (portrait and
|
||||
# landscape), honouring their auto-rotate lock. Was "portrait" (locked).
|
||||
app.orientation=fullUser
|
||||
app.version_code=32
|
||||
app.version_name=11.5a
|
||||
|
||||
# No need to modify anything past this line!
|
||||
android.enableJetifier=false
|
||||
android.useAndroidX=true
|
||||
android.defaults.buildfeatures.buildconfig=true
|
||||
android.nonTransitiveRClass=true
|
||||
android.nonFinalResIds=true
|
||||
app.name=Pokemon Red
|
||||
@@ -0,0 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip
|
||||
networkTimeout=10000
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
@@ -0,0 +1,244 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
@@ -0,0 +1,92 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -0,0 +1,122 @@
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
// If you want to use the debugger for JNI code, you want to debug this lib-project in Debug!
|
||||
// And due to gradle limitations/bugs, the best way is to always debug it in debug.
|
||||
// See https://code.google.com/p/android/issues/detail?id=52962
|
||||
// and http://stackoverflow.com/questions/27277433/why-does-gradle-build-my-module-in-release-mode-when-the-app-is-in-debug
|
||||
// defaultPublishConfig "debug"
|
||||
namespace "org.love2d.android"
|
||||
ndkVersion '25.2.9519653'
|
||||
|
||||
defaultConfig {
|
||||
minSdk 16
|
||||
compileSdk 34
|
||||
targetSdk 34
|
||||
externalNativeBuild {
|
||||
ndkBuild {
|
||||
arguments "-j" + Runtime.runtime.availableProcessors()
|
||||
}
|
||||
}
|
||||
ndk {
|
||||
// Specifies the ABI configurations of your native
|
||||
// libraries Gradle should build and package with your APK.
|
||||
// noinspection ChromeOsAbiSupport
|
||||
abiFilters 'armeabi-v7a', 'arm64-v8a'
|
||||
debugSymbolLevel 'SYMBOL_TABLE'
|
||||
}
|
||||
}
|
||||
|
||||
def retrieveAll3pModules = { ->
|
||||
def modules = []
|
||||
|
||||
fileTree("src/jni/lua-modules").visit { FileVisitDetails details ->
|
||||
if (details.isDirectory()) {
|
||||
if (file(details.file.path + "/Android.mk").exists()) {
|
||||
def logger = project.getLogger()
|
||||
logger.lifecycle("3rd-party module: " + details.file.path)
|
||||
|
||||
def javainfo = file(details.file.path + "/java.txt")
|
||||
if (javainfo.exists()) {
|
||||
def fstream = new FileInputStream(javainfo)
|
||||
def infile = new BufferedReader(new InputStreamReader(fstream))
|
||||
def javapath = infile.readLine().replace("\\", "/")
|
||||
def mpath = ""
|
||||
|
||||
if (javapath[0] != '/') {
|
||||
mpath = details.file.path + "/" + javapath
|
||||
} else {
|
||||
mpath = details.file.path + javapath
|
||||
}
|
||||
|
||||
modules << mpath
|
||||
|
||||
logger.lifecycle("Registered path " + mpath)
|
||||
infile.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return modules
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
debug {
|
||||
ndk {
|
||||
// noinspection ChromeOsAbiSupport
|
||||
abiFilters += 'x86_64'
|
||||
}
|
||||
}
|
||||
}
|
||||
flavorDimensions = ['mode']
|
||||
productFlavors {
|
||||
normal {
|
||||
dimension 'mode'
|
||||
}
|
||||
embed {
|
||||
dimension 'mode'
|
||||
}
|
||||
}
|
||||
sourceSets {
|
||||
main {
|
||||
java {
|
||||
srcDir 'src/jni/SDL2/android-project/app/src/main/java'
|
||||
srcDir 'src/main/java'
|
||||
srcDirs += retrieveAll3pModules()
|
||||
}
|
||||
}
|
||||
normal {
|
||||
java {
|
||||
srcDirs += 'src/normal/java'
|
||||
}
|
||||
}
|
||||
}
|
||||
externalNativeBuild {
|
||||
ndkBuild {
|
||||
path 'src/jni/Android.mk'
|
||||
}
|
||||
}
|
||||
packagingOptions {
|
||||
jniLibs {
|
||||
excludes += [
|
||||
'lib/arm64-v8a/libSDL2.so',
|
||||
'lib/armeabi-v7a/libSDL2.so',
|
||||
'lib/x86_64/libSDL2.so',
|
||||
'lib/x86/libSDL2.so'
|
||||
]
|
||||
}
|
||||
}
|
||||
lint {
|
||||
abortOnError false
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api fileTree(dir: 'libs', include: ['*.jar'])
|
||||
api 'androidx.appcompat:appcompat:1.6.1'
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in /Users/diglet/android-sdk-macosx/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<bool name="embed">true</bool>
|
||||
</resources>
|
||||
@@ -0,0 +1,18 @@
|
||||
!LuaJIT-2.1/android/armeabi/libluajit.a
|
||||
!LuaJIT-2.1/android/armeabi-v7a/libluajit.a
|
||||
!LuaJIT-2.1/android/x86/libluajit.a
|
||||
|
||||
LuaJIT-2.1/src/host/buildvm
|
||||
LuaJIT-2.1/src/host/buildvm_arch.h
|
||||
LuaJIT-2.1/src/host/minilua
|
||||
LuaJIT-2.1/src/jit/vmdef.lua
|
||||
LuaJIT-2.1/src/libluajit.a
|
||||
LuaJIT-2.1/src/lj_bcdef.h
|
||||
LuaJIT-2.1/src/lj_ffdef.h
|
||||
LuaJIT-2.1/src/lj_folddef.h
|
||||
LuaJIT-2.1/src/lj_libdef.h
|
||||
LuaJIT-2.1/src/lj_recdef.h
|
||||
LuaJIT-2.1/src/lj_vm.s
|
||||
LuaJIT-2.1/src/luajit
|
||||
|
||||
love/src/Makefile.am
|
||||
@@ -0,0 +1,19 @@
|
||||
LOVE_JNI_DIR := $(call my-dir)
|
||||
|
||||
ifeq ($(OS),Windows_NT)
|
||||
SHELL_EXECUTOR := powershell -executionpolicy unrestricted
|
||||
SCRIPT_EXTENSION := ps1
|
||||
else
|
||||
SHELL_EXECUTOR := sh
|
||||
SCRIPT_EXTENSION := sh
|
||||
endif
|
||||
|
||||
IS_NDK_R17 := $(shell ${SHELL_EXECUTOR} ${LOVE_JNI_DIR}/detect_ndkrel.${SCRIPT_EXTENSION} $(NDK_ROOT)/source.properties 17)
|
||||
IS_ANDROID_21 := $(shell ${SHELL_EXECUTOR} ${LOVE_JNI_DIR}/detect_androidapi.${SCRIPT_EXTENSION} $(TARGET_PLATFORM) 21)
|
||||
HAS_LOVE := $(shell ${SHELL_EXECUTOR} ${LOVE_JNI_DIR}/detect_love.${SCRIPT_EXTENSION} ${LOVE_JNI_DIR})
|
||||
|
||||
ifneq (${HAS_LOVE},yes)
|
||||
$(error Missing LOVE. Make sure to initialize the submodule correctly!)
|
||||
endif
|
||||
|
||||
include $(call all-subdir-makefiles)
|
||||
@@ -0,0 +1,15 @@
|
||||
# Uncomment this if you're using STL in your project
|
||||
# See CPLUSPLUS-SUPPORT.html in the NDK documentation for more information
|
||||
# APP_STL := stlport_static
|
||||
APP_STL := c++_shared
|
||||
APP_ABI := armeabi-v7a
|
||||
APP_CPPFLAGS := -frtti
|
||||
APP_LDFLAGS := -llog -landroid -lz -fuse-ld=lld
|
||||
APP_PLATFORM := 10
|
||||
NDK_TOOLCHAIN_VERSION := clang
|
||||
|
||||
# Fix for building on Windows
|
||||
# http://stackoverflow.com/questions/12598933/ndk-build-createprocess-make-e-87-the-parameter-is-incorrect
|
||||
APP_SHORT_COMMANDS := true
|
||||
|
||||
# APP_OPTIM := debug
|
||||
@@ -0,0 +1,15 @@
|
||||
*.[oa]
|
||||
# Prebuilt vendored static libs Android.mk links against directly (not
|
||||
# built by the Gradle/NDK build itself) -- must stay tracked or a fresh
|
||||
# checkout fails ndk-build with "LOCAL_SRC_FILES points to a missing file".
|
||||
!android/*/libluajit.a
|
||||
*.so
|
||||
*.obj
|
||||
*.lib
|
||||
*.exp
|
||||
*.dll
|
||||
*.exe
|
||||
*.manifest
|
||||
*.dmp
|
||||
*.swp
|
||||
.tags
|
||||
@@ -0,0 +1,9 @@
|
||||
LOCAL_PATH:= $(call my-dir)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
|
||||
LOCAL_MODULE := libluajit
|
||||
LOCAL_SRC_FILES := android/$(TARGET_ARCH_ABI)/libluajit.a
|
||||
LOCAL_EXPORT_C_INCLUDES := ${LOCAL_PATH}/src ${LOCAL_PATH}/android/$(TARGET_ARCH_ABI)
|
||||
|
||||
include $(PREBUILT_STATIC_LIBRARY)
|
||||
@@ -0,0 +1,56 @@
|
||||
===============================================================================
|
||||
LuaJIT -- a Just-In-Time Compiler for Lua. https://luajit.org/
|
||||
|
||||
Copyright (C) 2005-2023 Mike Pall. All rights reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
[ MIT license: https://www.opensource.org/licenses/mit-license.php ]
|
||||
|
||||
===============================================================================
|
||||
[ LuaJIT includes code from Lua 5.1/5.2, which has this license statement: ]
|
||||
|
||||
Copyright (C) 1994-2012 Lua.org, PUC-Rio.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
===============================================================================
|
||||
[ LuaJIT includes code from dlmalloc, which has this license statement: ]
|
||||
|
||||
This is a version (aka dlmalloc) of malloc/free/realloc written by
|
||||
Doug Lea and released to the public domain, as explained at
|
||||
https://creativecommons.org/licenses/publicdomain
|
||||
|
||||
===============================================================================
|
||||
@@ -0,0 +1,173 @@
|
||||
##############################################################################
|
||||
# LuaJIT top level Makefile for installation. Requires GNU Make.
|
||||
#
|
||||
# Please read doc/install.html before changing any variables!
|
||||
#
|
||||
# Suitable for POSIX platforms (Linux, *BSD, OSX etc.).
|
||||
# Note: src/Makefile has many more configurable options.
|
||||
#
|
||||
# ##### This Makefile is NOT useful for Windows! #####
|
||||
# For MSVC, please follow the instructions given in src/msvcbuild.bat.
|
||||
# For MinGW and Cygwin, cd to src and run make with the Makefile there.
|
||||
#
|
||||
# Copyright (C) 2005-2023 Mike Pall. See Copyright Notice in luajit.h
|
||||
##############################################################################
|
||||
|
||||
MAJVER= 2
|
||||
MINVER= 1
|
||||
ABIVER= 5.1
|
||||
|
||||
# LuaJIT uses rolling releases. The release version is based on the time of
|
||||
# the latest git commit. The 'git' command must be available during the build.
|
||||
RELVER= $(shell cat src/luajit_relver.txt 2>/dev/null || : )
|
||||
# Note: setting it with := doesn't work, since it will change during the build.
|
||||
|
||||
MMVERSION= $(MAJVER).$(MINVER)
|
||||
VERSION= $(MMVERSION).$(RELVER)
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Change the installation path as needed. This automatically adjusts
|
||||
# the paths in src/luaconf.h, too. Note: PREFIX must be an absolute path!
|
||||
#
|
||||
export PREFIX= /usr/local
|
||||
export MULTILIB= lib
|
||||
##############################################################################
|
||||
|
||||
DPREFIX= $(DESTDIR)$(PREFIX)
|
||||
INSTALL_BIN= $(DPREFIX)/bin
|
||||
INSTALL_LIB= $(DPREFIX)/$(MULTILIB)
|
||||
INSTALL_SHARE= $(DPREFIX)/share
|
||||
INSTALL_DEFINC= $(DPREFIX)/include/luajit-$(MMVERSION)
|
||||
INSTALL_INC= $(INSTALL_DEFINC)
|
||||
|
||||
INSTALL_LJLIBD= $(INSTALL_SHARE)/luajit-$(MMVERSION)
|
||||
INSTALL_JITLIB= $(INSTALL_LJLIBD)/jit
|
||||
INSTALL_LMODD= $(INSTALL_SHARE)/lua
|
||||
INSTALL_LMOD= $(INSTALL_LMODD)/$(ABIVER)
|
||||
INSTALL_CMODD= $(INSTALL_LIB)/lua
|
||||
INSTALL_CMOD= $(INSTALL_CMODD)/$(ABIVER)
|
||||
INSTALL_MAN= $(INSTALL_SHARE)/man/man1
|
||||
INSTALL_PKGCONFIG= $(INSTALL_LIB)/pkgconfig
|
||||
|
||||
INSTALL_TNAME= luajit-$(VERSION)
|
||||
INSTALL_TSYMNAME= luajit
|
||||
INSTALL_ANAME= libluajit-$(ABIVER).a
|
||||
INSTALL_SOSHORT1= libluajit-$(ABIVER).so
|
||||
INSTALL_SOSHORT2= libluajit-$(ABIVER).so.$(MAJVER)
|
||||
INSTALL_SONAME= libluajit-$(ABIVER).so.$(VERSION)
|
||||
INSTALL_DYLIBSHORT1= libluajit-$(ABIVER).dylib
|
||||
INSTALL_DYLIBSHORT2= libluajit-$(ABIVER).$(MAJVER).dylib
|
||||
INSTALL_DYLIBNAME= libluajit-$(ABIVER).$(VERSION).dylib
|
||||
INSTALL_PCNAME= luajit.pc
|
||||
|
||||
INSTALL_STATIC= $(INSTALL_LIB)/$(INSTALL_ANAME)
|
||||
INSTALL_DYN= $(INSTALL_LIB)/$(INSTALL_SONAME)
|
||||
INSTALL_SHORT1= $(INSTALL_LIB)/$(INSTALL_SOSHORT1)
|
||||
INSTALL_SHORT2= $(INSTALL_LIB)/$(INSTALL_SOSHORT2)
|
||||
INSTALL_T= $(INSTALL_BIN)/$(INSTALL_TNAME)
|
||||
INSTALL_TSYM= $(INSTALL_BIN)/$(INSTALL_TSYMNAME)
|
||||
INSTALL_PC= $(INSTALL_PKGCONFIG)/$(INSTALL_PCNAME)
|
||||
|
||||
INSTALL_DIRS= $(INSTALL_BIN) $(INSTALL_LIB) $(INSTALL_INC) $(INSTALL_MAN) \
|
||||
$(INSTALL_PKGCONFIG) $(INSTALL_JITLIB) $(INSTALL_LMOD) $(INSTALL_CMOD)
|
||||
UNINSTALL_DIRS= $(INSTALL_JITLIB) $(INSTALL_LJLIBD) $(INSTALL_INC) \
|
||||
$(INSTALL_LMOD) $(INSTALL_LMODD) $(INSTALL_CMOD) $(INSTALL_CMODD)
|
||||
|
||||
RM= rm -f
|
||||
MKDIR= mkdir -p
|
||||
RMDIR= rmdir 2>/dev/null
|
||||
SYMLINK= ln -sf
|
||||
INSTALL_X= install -m 0755
|
||||
INSTALL_F= install -m 0644
|
||||
UNINSTALL= $(RM)
|
||||
LDCONFIG= ldconfig -n 2>/dev/null
|
||||
SED_PC= sed -e "s|^prefix=.*|prefix=$(PREFIX)|" \
|
||||
-e "s|^multilib=.*|multilib=$(MULTILIB)|" \
|
||||
-e "s|^relver=.*|relver=$(RELVER)|"
|
||||
ifneq ($(INSTALL_DEFINC),$(INSTALL_INC))
|
||||
SED_PC+= -e "s|^includedir=.*|includedir=$(INSTALL_INC)|"
|
||||
endif
|
||||
|
||||
FILE_T= luajit
|
||||
FILE_A= libluajit.a
|
||||
FILE_SO= libluajit.so
|
||||
FILE_MAN= luajit.1
|
||||
FILE_PC= luajit.pc
|
||||
FILES_INC= lua.h lualib.h lauxlib.h luaconf.h lua.hpp luajit.h
|
||||
FILES_JITLIB= bc.lua bcsave.lua dump.lua p.lua v.lua zone.lua \
|
||||
dis_x86.lua dis_x64.lua dis_arm.lua dis_arm64.lua \
|
||||
dis_arm64be.lua dis_ppc.lua dis_mips.lua dis_mipsel.lua \
|
||||
dis_mips64.lua dis_mips64el.lua \
|
||||
dis_mips64r6.lua dis_mips64r6el.lua \
|
||||
vmdef.lua
|
||||
|
||||
ifeq (,$(findstring Windows,$(OS)))
|
||||
HOST_SYS:= $(shell uname -s)
|
||||
else
|
||||
HOST_SYS= Windows
|
||||
endif
|
||||
TARGET_SYS?= $(HOST_SYS)
|
||||
|
||||
ifeq (Darwin,$(TARGET_SYS))
|
||||
INSTALL_SONAME= $(INSTALL_DYLIBNAME)
|
||||
INSTALL_SOSHORT1= $(INSTALL_DYLIBSHORT1)
|
||||
INSTALL_SOSHORT2= $(INSTALL_DYLIBSHORT2)
|
||||
LDCONFIG= :
|
||||
endif
|
||||
|
||||
##############################################################################
|
||||
|
||||
INSTALL_DEP= src/luajit
|
||||
|
||||
default all $(INSTALL_DEP):
|
||||
@echo "==== Building LuaJIT $(MMVERSION) ===="
|
||||
$(MAKE) -C src
|
||||
@echo "==== Successfully built LuaJIT $(MMVERSION) ===="
|
||||
|
||||
install: $(INSTALL_DEP)
|
||||
@echo "==== Installing LuaJIT $(VERSION) to $(PREFIX) ===="
|
||||
$(MKDIR) $(INSTALL_DIRS)
|
||||
cd src && $(INSTALL_X) $(FILE_T) $(INSTALL_T)
|
||||
cd src && test -f $(FILE_A) && $(INSTALL_F) $(FILE_A) $(INSTALL_STATIC) || :
|
||||
$(RM) $(INSTALL_DYN) $(INSTALL_SHORT1) $(INSTALL_SHORT2)
|
||||
cd src && test -f $(FILE_SO) && \
|
||||
$(INSTALL_X) $(FILE_SO) $(INSTALL_DYN) && \
|
||||
( $(LDCONFIG) $(INSTALL_LIB) || : ) && \
|
||||
$(SYMLINK) $(INSTALL_SONAME) $(INSTALL_SHORT1) && \
|
||||
$(SYMLINK) $(INSTALL_SONAME) $(INSTALL_SHORT2) || :
|
||||
cd etc && $(INSTALL_F) $(FILE_MAN) $(INSTALL_MAN)
|
||||
cd etc && $(SED_PC) $(FILE_PC) > $(FILE_PC).tmp && \
|
||||
$(INSTALL_F) $(FILE_PC).tmp $(INSTALL_PC) && \
|
||||
$(RM) $(FILE_PC).tmp
|
||||
cd src && $(INSTALL_F) $(FILES_INC) $(INSTALL_INC)
|
||||
cd src/jit && $(INSTALL_F) $(FILES_JITLIB) $(INSTALL_JITLIB)
|
||||
$(SYMLINK) $(INSTALL_TNAME) $(INSTALL_TSYM)
|
||||
@echo "==== Successfully installed LuaJIT $(VERSION) to $(PREFIX) ===="
|
||||
|
||||
uninstall:
|
||||
@echo "==== Uninstalling LuaJIT $(VERSION) from $(PREFIX) ===="
|
||||
$(UNINSTALL) $(INSTALL_TSYM) $(INSTALL_T) $(INSTALL_STATIC) $(INSTALL_DYN) $(INSTALL_SHORT1) $(INSTALL_SHORT2) $(INSTALL_MAN)/$(FILE_MAN) $(INSTALL_PC)
|
||||
for file in $(FILES_JITLIB); do \
|
||||
$(UNINSTALL) $(INSTALL_JITLIB)/$$file; \
|
||||
done
|
||||
for file in $(FILES_INC); do \
|
||||
$(UNINSTALL) $(INSTALL_INC)/$$file; \
|
||||
done
|
||||
$(LDCONFIG) $(INSTALL_LIB)
|
||||
$(RMDIR) $(UNINSTALL_DIRS) || :
|
||||
@echo "==== Successfully uninstalled LuaJIT $(VERSION) from $(PREFIX) ===="
|
||||
|
||||
##############################################################################
|
||||
|
||||
amalg:
|
||||
@echo "==== Building LuaJIT $(MMVERSION) (amalgamation) ===="
|
||||
$(MAKE) -C src amalg
|
||||
@echo "==== Successfully built LuaJIT $(MMVERSION) (amalgamation) ===="
|
||||
|
||||
clean:
|
||||
$(MAKE) -C src clean
|
||||
|
||||
.PHONY: all install amalg clean
|
||||
|
||||
##############################################################################
|
||||
@@ -0,0 +1,16 @@
|
||||
README for LuaJIT 2.1
|
||||
---------------------
|
||||
|
||||
LuaJIT is a Just-In-Time (JIT) compiler for the Lua programming language.
|
||||
|
||||
Project Homepage: https://luajit.org/
|
||||
|
||||
LuaJIT is Copyright (C) 2005-2023 Mike Pall.
|
||||
LuaJIT is free software, released under the MIT license.
|
||||
See full Copyright Notice in the COPYRIGHT file or in luajit.h.
|
||||
|
||||
Documentation for LuaJIT is available in HTML format.
|
||||
Please point your favorite browser to:
|
||||
|
||||
doc/luajit.html
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
----------------------------------------------------------------------------
|
||||
-- LuaJIT bytecode listing module.
|
||||
--
|
||||
-- Copyright (C) 2005-2023 Mike Pall. All rights reserved.
|
||||
-- Released under the MIT license. See Copyright Notice in luajit.h
|
||||
----------------------------------------------------------------------------
|
||||
--
|
||||
-- This module lists the bytecode of a Lua function. If it's loaded by -jbc
|
||||
-- it hooks into the parser and lists all functions of a chunk as they
|
||||
-- are parsed.
|
||||
--
|
||||
-- Example usage:
|
||||
--
|
||||
-- luajit -jbc -e 'local x=0; for i=1,1e6 do x=x+i end; print(x)'
|
||||
-- luajit -jbc=- foo.lua
|
||||
-- luajit -jbc=foo.list foo.lua
|
||||
--
|
||||
-- Default output is to stderr. To redirect the output to a file, pass a
|
||||
-- filename as an argument (use '-' for stdout) or set the environment
|
||||
-- variable LUAJIT_LISTFILE. The file is overwritten every time the module
|
||||
-- is started.
|
||||
--
|
||||
-- This module can also be used programmatically:
|
||||
--
|
||||
-- local bc = require("jit.bc")
|
||||
--
|
||||
-- local function foo() print("hello") end
|
||||
--
|
||||
-- bc.dump(foo) --> -- BYTECODE -- [...]
|
||||
-- print(bc.line(foo, 2)) --> 0002 KSTR 1 1 ; "hello"
|
||||
--
|
||||
-- local out = {
|
||||
-- -- Do something with each line:
|
||||
-- write = function(t, ...) io.write(...) end,
|
||||
-- close = function(t) end,
|
||||
-- flush = function(t) end,
|
||||
-- }
|
||||
-- bc.dump(foo, out)
|
||||
--
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
-- Cache some library functions and objects.
|
||||
local jit = require("jit")
|
||||
local jutil = require("jit.util")
|
||||
local vmdef = require("jit.vmdef")
|
||||
local bit = require("bit")
|
||||
local sub, gsub, format = string.sub, string.gsub, string.format
|
||||
local byte, band, shr = string.byte, bit.band, bit.rshift
|
||||
local funcinfo, funcbc, funck = jutil.funcinfo, jutil.funcbc, jutil.funck
|
||||
local funcuvname = jutil.funcuvname
|
||||
local bcnames = vmdef.bcnames
|
||||
local stdout, stderr = io.stdout, io.stderr
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
local function ctlsub(c)
|
||||
if c == "\n" then return "\\n"
|
||||
elseif c == "\r" then return "\\r"
|
||||
elseif c == "\t" then return "\\t"
|
||||
else return format("\\%03d", byte(c))
|
||||
end
|
||||
end
|
||||
|
||||
-- Return one bytecode line.
|
||||
local function bcline(func, pc, prefix)
|
||||
local ins, m = funcbc(func, pc)
|
||||
if not ins then return end
|
||||
local ma, mb, mc = band(m, 7), band(m, 15*8), band(m, 15*128)
|
||||
local a = band(shr(ins, 8), 0xff)
|
||||
local oidx = 6*band(ins, 0xff)
|
||||
local op = sub(bcnames, oidx+1, oidx+6)
|
||||
local s = format("%04d %s %-6s %3s ",
|
||||
pc, prefix or " ", op, ma == 0 and "" or a)
|
||||
local d = shr(ins, 16)
|
||||
if mc == 13*128 then -- BCMjump
|
||||
return format("%s=> %04d\n", s, pc+d-0x7fff)
|
||||
end
|
||||
if mb ~= 0 then
|
||||
d = band(d, 0xff)
|
||||
elseif mc == 0 then
|
||||
return s.."\n"
|
||||
end
|
||||
local kc
|
||||
if mc == 10*128 then -- BCMstr
|
||||
kc = funck(func, -d-1)
|
||||
kc = format(#kc > 40 and '"%.40s"~' or '"%s"', gsub(kc, "%c", ctlsub))
|
||||
elseif mc == 9*128 then -- BCMnum
|
||||
kc = funck(func, d)
|
||||
if op == "TSETM " then kc = kc - 2^52 end
|
||||
elseif mc == 12*128 then -- BCMfunc
|
||||
local fi = funcinfo(funck(func, -d-1))
|
||||
if fi.ffid then
|
||||
kc = vmdef.ffnames[fi.ffid]
|
||||
else
|
||||
kc = fi.loc
|
||||
end
|
||||
elseif mc == 5*128 then -- BCMuv
|
||||
kc = funcuvname(func, d)
|
||||
end
|
||||
if ma == 5 then -- BCMuv
|
||||
local ka = funcuvname(func, a)
|
||||
if kc then kc = ka.." ; "..kc else kc = ka end
|
||||
end
|
||||
if mb ~= 0 then
|
||||
local b = shr(ins, 24)
|
||||
if kc then return format("%s%3d %3d ; %s\n", s, b, d, kc) end
|
||||
return format("%s%3d %3d\n", s, b, d)
|
||||
end
|
||||
if kc then return format("%s%3d ; %s\n", s, d, kc) end
|
||||
if mc == 7*128 and d > 32767 then d = d - 65536 end -- BCMlits
|
||||
return format("%s%3d\n", s, d)
|
||||
end
|
||||
|
||||
-- Collect branch targets of a function.
|
||||
local function bctargets(func)
|
||||
local target = {}
|
||||
for pc=1,1000000000 do
|
||||
local ins, m = funcbc(func, pc)
|
||||
if not ins then break end
|
||||
if band(m, 15*128) == 13*128 then target[pc+shr(ins, 16)-0x7fff] = true end
|
||||
end
|
||||
return target
|
||||
end
|
||||
|
||||
-- Dump bytecode instructions of a function.
|
||||
local function bcdump(func, out, all)
|
||||
if not out then out = stdout end
|
||||
local fi = funcinfo(func)
|
||||
if all and fi.children then
|
||||
for n=-1,-1000000000,-1 do
|
||||
local k = funck(func, n)
|
||||
if not k then break end
|
||||
if type(k) == "proto" then bcdump(k, out, true) end
|
||||
end
|
||||
end
|
||||
out:write(format("-- BYTECODE -- %s-%d\n", fi.loc, fi.lastlinedefined))
|
||||
local target = bctargets(func)
|
||||
for pc=1,1000000000 do
|
||||
local s = bcline(func, pc, target[pc] and "=>")
|
||||
if not s then break end
|
||||
out:write(s)
|
||||
end
|
||||
out:write("\n")
|
||||
out:flush()
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
-- Active flag and output file handle.
|
||||
local active, out
|
||||
|
||||
-- List handler.
|
||||
local function h_list(func)
|
||||
return bcdump(func, out)
|
||||
end
|
||||
|
||||
-- Detach list handler.
|
||||
local function bclistoff()
|
||||
if active then
|
||||
active = false
|
||||
jit.attach(h_list)
|
||||
if out and out ~= stdout and out ~= stderr then out:close() end
|
||||
out = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- Open the output file and attach list handler.
|
||||
local function bcliston(outfile)
|
||||
if active then bclistoff() end
|
||||
if not outfile then outfile = os.getenv("LUAJIT_LISTFILE") end
|
||||
if outfile then
|
||||
out = outfile == "-" and stdout or assert(io.open(outfile, "w"))
|
||||
else
|
||||
out = stderr
|
||||
end
|
||||
jit.attach(h_list, "bc")
|
||||
active = true
|
||||
end
|
||||
|
||||
-- Public module functions.
|
||||
return {
|
||||
line = bcline,
|
||||
dump = bcdump,
|
||||
targets = bctargets,
|
||||
on = bcliston,
|
||||
off = bclistoff,
|
||||
start = bcliston -- For -j command line option.
|
||||
}
|
||||
|
||||
@@ -0,0 +1,705 @@
|
||||
----------------------------------------------------------------------------
|
||||
-- LuaJIT module to save/list bytecode.
|
||||
--
|
||||
-- Copyright (C) 2005-2023 Mike Pall. All rights reserved.
|
||||
-- Released under the MIT license. See Copyright Notice in luajit.h
|
||||
----------------------------------------------------------------------------
|
||||
--
|
||||
-- This module saves or lists the bytecode for an input file.
|
||||
-- It's run by the -b command line option.
|
||||
--
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
local jit = require("jit")
|
||||
assert(jit.version_num == 20199, "LuaJIT core/library version mismatch")
|
||||
local bit = require("bit")
|
||||
|
||||
-- Symbol name prefix for LuaJIT bytecode.
|
||||
local LJBC_PREFIX = "luaJIT_BC_"
|
||||
|
||||
local type, assert = type, assert
|
||||
local format = string.format
|
||||
local tremove, tconcat = table.remove, table.concat
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
local function usage()
|
||||
io.stderr:write[[
|
||||
Save LuaJIT bytecode: luajit -b[options] input output
|
||||
-l Only list bytecode.
|
||||
-s Strip debug info (default).
|
||||
-g Keep debug info.
|
||||
-n name Set module name (default: auto-detect from input name).
|
||||
-t type Set output file type (default: auto-detect from output name).
|
||||
-a arch Override architecture for object files (default: native).
|
||||
-o os Override OS for object files (default: native).
|
||||
-F name Override filename (default: input filename).
|
||||
-e chunk Use chunk string as input.
|
||||
-- Stop handling options.
|
||||
- Use stdin as input and/or stdout as output.
|
||||
|
||||
File types: c cc h obj o raw (default)
|
||||
]]
|
||||
os.exit(1)
|
||||
end
|
||||
|
||||
local function check(ok, ...)
|
||||
if ok then return ok, ... end
|
||||
io.stderr:write("luajit: ", ...)
|
||||
io.stderr:write("\n")
|
||||
os.exit(1)
|
||||
end
|
||||
|
||||
local function readfile(ctx, input)
|
||||
if type(input) == "function" then return input end
|
||||
if ctx.filename then
|
||||
local data
|
||||
if input == "-" then
|
||||
data = io.stdin:read("*a")
|
||||
else
|
||||
local fp = assert(io.open(input, "rb"))
|
||||
data = assert(fp:read("*a"))
|
||||
assert(fp:close())
|
||||
end
|
||||
return check(load(data, ctx.filename))
|
||||
else
|
||||
if input == "-" then input = nil end
|
||||
return check(loadfile(input))
|
||||
end
|
||||
end
|
||||
|
||||
local function savefile(name, mode)
|
||||
if name == "-" then return io.stdout end
|
||||
return check(io.open(name, mode))
|
||||
end
|
||||
|
||||
local function set_stdout_binary(ffi)
|
||||
ffi.cdef[[int _setmode(int fd, int mode);]]
|
||||
ffi.C._setmode(1, 0x8000)
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
local map_type = {
|
||||
raw = "raw", c = "c", cc = "c", h = "h", o = "obj", obj = "obj",
|
||||
}
|
||||
|
||||
local map_arch = {
|
||||
x86 = { e = "le", b = 32, m = 3, p = 0x14c, },
|
||||
x64 = { e = "le", b = 64, m = 62, p = 0x8664, },
|
||||
arm = { e = "le", b = 32, m = 40, p = 0x1c0, },
|
||||
arm64 = { e = "le", b = 64, m = 183, p = 0xaa64, },
|
||||
arm64be = { e = "be", b = 64, m = 183, },
|
||||
ppc = { e = "be", b = 32, m = 20, },
|
||||
mips = { e = "be", b = 32, m = 8, f = 0x50001006, },
|
||||
mipsel = { e = "le", b = 32, m = 8, f = 0x50001006, },
|
||||
mips64 = { e = "be", b = 64, m = 8, f = 0x80000007, },
|
||||
mips64el = { e = "le", b = 64, m = 8, f = 0x80000007, },
|
||||
mips64r6 = { e = "be", b = 64, m = 8, f = 0xa0000407, },
|
||||
mips64r6el = { e = "le", b = 64, m = 8, f = 0xa0000407, },
|
||||
}
|
||||
|
||||
local map_os = {
|
||||
linux = true, windows = true, osx = true, freebsd = true, netbsd = true,
|
||||
openbsd = true, dragonfly = true, solaris = true,
|
||||
}
|
||||
|
||||
local function checkarg(str, map, err)
|
||||
str = str:lower()
|
||||
local s = check(map[str], "unknown ", err)
|
||||
return type(s) == "string" and s or str
|
||||
end
|
||||
|
||||
local function detecttype(str)
|
||||
local ext = str:lower():match("%.(%a+)$")
|
||||
return map_type[ext] or "raw"
|
||||
end
|
||||
|
||||
local function checkmodname(str)
|
||||
check(str:match("^[%w_.%-]+$"), "bad module name")
|
||||
return str:gsub("[%.%-]", "_")
|
||||
end
|
||||
|
||||
local function detectmodname(str)
|
||||
if type(str) == "string" then
|
||||
local tail = str:match("[^/\\]+$")
|
||||
if tail then str = tail end
|
||||
local head = str:match("^(.*)%.[^.]*$")
|
||||
if head then str = head end
|
||||
str = str:match("^[%w_.%-]+")
|
||||
else
|
||||
str = nil
|
||||
end
|
||||
check(str, "cannot derive module name, use -n name")
|
||||
return str:gsub("[%.%-]", "_")
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
local function bcsave_tail(fp, output, s)
|
||||
local ok, err = fp:write(s)
|
||||
if ok and output ~= "-" then ok, err = fp:close() end
|
||||
check(ok, "cannot write ", output, ": ", err)
|
||||
end
|
||||
|
||||
local function bcsave_raw(output, s)
|
||||
if output == "-" and jit.os == "Windows" then
|
||||
local ok, ffi = pcall(require, "ffi")
|
||||
check(ok, "FFI library required to write binary file to stdout")
|
||||
set_stdout_binary(ffi)
|
||||
end
|
||||
local fp = savefile(output, "wb")
|
||||
bcsave_tail(fp, output, s)
|
||||
end
|
||||
|
||||
local function bcsave_c(ctx, output, s)
|
||||
local fp = savefile(output, "w")
|
||||
if ctx.type == "c" then
|
||||
fp:write(format([[
|
||||
#ifdef __cplusplus
|
||||
extern "C"
|
||||
#endif
|
||||
#ifdef _WIN32
|
||||
__declspec(dllexport)
|
||||
#endif
|
||||
const unsigned char %s%s[] = {
|
||||
]], LJBC_PREFIX, ctx.modname))
|
||||
else
|
||||
fp:write(format([[
|
||||
#define %s%s_SIZE %d
|
||||
static const unsigned char %s%s[] = {
|
||||
]], LJBC_PREFIX, ctx.modname, #s, LJBC_PREFIX, ctx.modname))
|
||||
end
|
||||
local t, n, m = {}, 0, 0
|
||||
for i=1,#s do
|
||||
local b = tostring(string.byte(s, i))
|
||||
m = m + #b + 1
|
||||
if m > 78 then
|
||||
fp:write(tconcat(t, ",", 1, n), ",\n")
|
||||
n, m = 0, #b + 1
|
||||
end
|
||||
n = n + 1
|
||||
t[n] = b
|
||||
end
|
||||
bcsave_tail(fp, output, tconcat(t, ",", 1, n).."\n};\n")
|
||||
end
|
||||
|
||||
local function bcsave_elfobj(ctx, output, s, ffi)
|
||||
ffi.cdef[[
|
||||
typedef struct {
|
||||
uint8_t emagic[4], eclass, eendian, eversion, eosabi, eabiversion, epad[7];
|
||||
uint16_t type, machine;
|
||||
uint32_t version;
|
||||
uint32_t entry, phofs, shofs;
|
||||
uint32_t flags;
|
||||
uint16_t ehsize, phentsize, phnum, shentsize, shnum, shstridx;
|
||||
} ELF32header;
|
||||
typedef struct {
|
||||
uint8_t emagic[4], eclass, eendian, eversion, eosabi, eabiversion, epad[7];
|
||||
uint16_t type, machine;
|
||||
uint32_t version;
|
||||
uint64_t entry, phofs, shofs;
|
||||
uint32_t flags;
|
||||
uint16_t ehsize, phentsize, phnum, shentsize, shnum, shstridx;
|
||||
} ELF64header;
|
||||
typedef struct {
|
||||
uint32_t name, type, flags, addr, ofs, size, link, info, align, entsize;
|
||||
} ELF32sectheader;
|
||||
typedef struct {
|
||||
uint32_t name, type;
|
||||
uint64_t flags, addr, ofs, size;
|
||||
uint32_t link, info;
|
||||
uint64_t align, entsize;
|
||||
} ELF64sectheader;
|
||||
typedef struct {
|
||||
uint32_t name, value, size;
|
||||
uint8_t info, other;
|
||||
uint16_t sectidx;
|
||||
} ELF32symbol;
|
||||
typedef struct {
|
||||
uint32_t name;
|
||||
uint8_t info, other;
|
||||
uint16_t sectidx;
|
||||
uint64_t value, size;
|
||||
} ELF64symbol;
|
||||
typedef struct {
|
||||
ELF32header hdr;
|
||||
ELF32sectheader sect[6];
|
||||
ELF32symbol sym[2];
|
||||
uint8_t space[4096];
|
||||
} ELF32obj;
|
||||
typedef struct {
|
||||
ELF64header hdr;
|
||||
ELF64sectheader sect[6];
|
||||
ELF64symbol sym[2];
|
||||
uint8_t space[4096];
|
||||
} ELF64obj;
|
||||
]]
|
||||
local symname = LJBC_PREFIX..ctx.modname
|
||||
local ai = assert(map_arch[ctx.arch])
|
||||
local is64, isbe = ai.b == 64, ai.e == "be"
|
||||
|
||||
-- Handle different host/target endianess.
|
||||
local function f32(x) return x end
|
||||
local f16, fofs = f32, f32
|
||||
if ffi.abi("be") ~= isbe then
|
||||
f32 = bit.bswap
|
||||
function f16(x) return bit.rshift(bit.bswap(x), 16) end
|
||||
if is64 then
|
||||
local two32 = ffi.cast("int64_t", 2^32)
|
||||
function fofs(x) return bit.bswap(x)*two32 end
|
||||
else
|
||||
fofs = f32
|
||||
end
|
||||
end
|
||||
|
||||
-- Create ELF object and fill in header.
|
||||
local o = ffi.new(is64 and "ELF64obj" or "ELF32obj")
|
||||
local hdr = o.hdr
|
||||
if ctx.os == "bsd" or ctx.os == "other" then -- Determine native hdr.eosabi.
|
||||
local bf = assert(io.open("/bin/ls", "rb"))
|
||||
local bs = bf:read(9)
|
||||
bf:close()
|
||||
ffi.copy(o, bs, 9)
|
||||
check(hdr.emagic[0] == 127, "no support for writing native object files")
|
||||
else
|
||||
hdr.emagic = "\127ELF"
|
||||
hdr.eosabi = ({ freebsd=9, netbsd=2, openbsd=12, solaris=6 })[ctx.os] or 0
|
||||
end
|
||||
hdr.eclass = is64 and 2 or 1
|
||||
hdr.eendian = isbe and 2 or 1
|
||||
hdr.eversion = 1
|
||||
hdr.type = f16(1)
|
||||
hdr.machine = f16(ai.m)
|
||||
hdr.flags = f32(ai.f or 0)
|
||||
hdr.version = f32(1)
|
||||
hdr.shofs = fofs(ffi.offsetof(o, "sect"))
|
||||
hdr.ehsize = f16(ffi.sizeof(hdr))
|
||||
hdr.shentsize = f16(ffi.sizeof(o.sect[0]))
|
||||
hdr.shnum = f16(6)
|
||||
hdr.shstridx = f16(2)
|
||||
|
||||
-- Fill in sections and symbols.
|
||||
local sofs, ofs = ffi.offsetof(o, "space"), 1
|
||||
for i,name in ipairs{
|
||||
".symtab", ".shstrtab", ".strtab", ".rodata", ".note.GNU-stack",
|
||||
} do
|
||||
local sect = o.sect[i]
|
||||
sect.align = fofs(1)
|
||||
sect.name = f32(ofs)
|
||||
ffi.copy(o.space+ofs, name)
|
||||
ofs = ofs + #name+1
|
||||
end
|
||||
o.sect[1].type = f32(2) -- .symtab
|
||||
o.sect[1].link = f32(3)
|
||||
o.sect[1].info = f32(1)
|
||||
o.sect[1].align = fofs(8)
|
||||
o.sect[1].ofs = fofs(ffi.offsetof(o, "sym"))
|
||||
o.sect[1].entsize = fofs(ffi.sizeof(o.sym[0]))
|
||||
o.sect[1].size = fofs(ffi.sizeof(o.sym))
|
||||
o.sym[1].name = f32(1)
|
||||
o.sym[1].sectidx = f16(4)
|
||||
o.sym[1].size = fofs(#s)
|
||||
o.sym[1].info = 17
|
||||
o.sect[2].type = f32(3) -- .shstrtab
|
||||
o.sect[2].ofs = fofs(sofs)
|
||||
o.sect[2].size = fofs(ofs)
|
||||
o.sect[3].type = f32(3) -- .strtab
|
||||
o.sect[3].ofs = fofs(sofs + ofs)
|
||||
o.sect[3].size = fofs(#symname+2)
|
||||
ffi.copy(o.space+ofs+1, symname)
|
||||
ofs = ofs + #symname + 2
|
||||
o.sect[4].type = f32(1) -- .rodata
|
||||
o.sect[4].flags = fofs(2)
|
||||
o.sect[4].ofs = fofs(sofs + ofs)
|
||||
o.sect[4].size = fofs(#s)
|
||||
o.sect[5].type = f32(1) -- .note.GNU-stack
|
||||
o.sect[5].ofs = fofs(sofs + ofs + #s)
|
||||
|
||||
-- Write ELF object file.
|
||||
local fp = savefile(output, "wb")
|
||||
fp:write(ffi.string(o, ffi.sizeof(o)-4096+ofs))
|
||||
bcsave_tail(fp, output, s)
|
||||
end
|
||||
|
||||
local function bcsave_peobj(ctx, output, s, ffi)
|
||||
ffi.cdef[[
|
||||
typedef struct {
|
||||
uint16_t arch, nsects;
|
||||
uint32_t time, symtabofs, nsyms;
|
||||
uint16_t opthdrsz, flags;
|
||||
} PEheader;
|
||||
typedef struct {
|
||||
char name[8];
|
||||
uint32_t vsize, vaddr, size, ofs, relocofs, lineofs;
|
||||
uint16_t nreloc, nline;
|
||||
uint32_t flags;
|
||||
} PEsection;
|
||||
typedef struct __attribute((packed)) {
|
||||
union {
|
||||
char name[8];
|
||||
uint32_t nameref[2];
|
||||
};
|
||||
uint32_t value;
|
||||
int16_t sect;
|
||||
uint16_t type;
|
||||
uint8_t scl, naux;
|
||||
} PEsym;
|
||||
typedef struct __attribute((packed)) {
|
||||
uint32_t size;
|
||||
uint16_t nreloc, nline;
|
||||
uint32_t cksum;
|
||||
uint16_t assoc;
|
||||
uint8_t comdatsel, unused[3];
|
||||
} PEsymaux;
|
||||
typedef struct {
|
||||
PEheader hdr;
|
||||
PEsection sect[2];
|
||||
// Must be an even number of symbol structs.
|
||||
PEsym sym0;
|
||||
PEsymaux sym0aux;
|
||||
PEsym sym1;
|
||||
PEsymaux sym1aux;
|
||||
PEsym sym2;
|
||||
PEsym sym3;
|
||||
uint32_t strtabsize;
|
||||
uint8_t space[4096];
|
||||
} PEobj;
|
||||
]]
|
||||
local symname = LJBC_PREFIX..ctx.modname
|
||||
local ai = assert(map_arch[ctx.arch])
|
||||
local is64 = ai.b == 64
|
||||
local symexport = " /EXPORT:"..symname..",DATA "
|
||||
|
||||
-- The file format is always little-endian. Swap if the host is big-endian.
|
||||
local function f32(x) return x end
|
||||
local f16 = f32
|
||||
if ffi.abi("be") then
|
||||
f32 = bit.bswap
|
||||
function f16(x) return bit.rshift(bit.bswap(x), 16) end
|
||||
end
|
||||
|
||||
-- Create PE object and fill in header.
|
||||
local o = ffi.new("PEobj")
|
||||
local hdr = o.hdr
|
||||
hdr.arch = f16(assert(ai.p))
|
||||
hdr.nsects = f16(2)
|
||||
hdr.symtabofs = f32(ffi.offsetof(o, "sym0"))
|
||||
hdr.nsyms = f32(6)
|
||||
|
||||
-- Fill in sections and symbols.
|
||||
o.sect[0].name = ".drectve"
|
||||
o.sect[0].size = f32(#symexport)
|
||||
o.sect[0].flags = f32(0x00100a00)
|
||||
o.sym0.sect = f16(1)
|
||||
o.sym0.scl = 3
|
||||
o.sym0.name = ".drectve"
|
||||
o.sym0.naux = 1
|
||||
o.sym0aux.size = f32(#symexport)
|
||||
o.sect[1].name = ".rdata"
|
||||
o.sect[1].size = f32(#s)
|
||||
o.sect[1].flags = f32(0x40300040)
|
||||
o.sym1.sect = f16(2)
|
||||
o.sym1.scl = 3
|
||||
o.sym1.name = ".rdata"
|
||||
o.sym1.naux = 1
|
||||
o.sym1aux.size = f32(#s)
|
||||
o.sym2.sect = f16(2)
|
||||
o.sym2.scl = 2
|
||||
o.sym2.nameref[1] = f32(4)
|
||||
o.sym3.sect = f16(-1)
|
||||
o.sym3.scl = 2
|
||||
o.sym3.value = f32(1)
|
||||
o.sym3.name = "@feat.00" -- Mark as SafeSEH compliant.
|
||||
ffi.copy(o.space, symname)
|
||||
local ofs = #symname + 1
|
||||
o.strtabsize = f32(ofs + 4)
|
||||
o.sect[0].ofs = f32(ffi.offsetof(o, "space") + ofs)
|
||||
ffi.copy(o.space + ofs, symexport)
|
||||
ofs = ofs + #symexport
|
||||
o.sect[1].ofs = f32(ffi.offsetof(o, "space") + ofs)
|
||||
|
||||
-- Write PE object file.
|
||||
local fp = savefile(output, "wb")
|
||||
fp:write(ffi.string(o, ffi.sizeof(o)-4096+ofs))
|
||||
bcsave_tail(fp, output, s)
|
||||
end
|
||||
|
||||
local function bcsave_machobj(ctx, output, s, ffi)
|
||||
ffi.cdef[[
|
||||
typedef struct
|
||||
{
|
||||
uint32_t magic, cputype, cpusubtype, filetype, ncmds, sizeofcmds, flags;
|
||||
} mach_header;
|
||||
typedef struct
|
||||
{
|
||||
mach_header; uint32_t reserved;
|
||||
} mach_header_64;
|
||||
typedef struct {
|
||||
uint32_t cmd, cmdsize;
|
||||
char segname[16];
|
||||
uint32_t vmaddr, vmsize, fileoff, filesize;
|
||||
uint32_t maxprot, initprot, nsects, flags;
|
||||
} mach_segment_command;
|
||||
typedef struct {
|
||||
uint32_t cmd, cmdsize;
|
||||
char segname[16];
|
||||
uint64_t vmaddr, vmsize, fileoff, filesize;
|
||||
uint32_t maxprot, initprot, nsects, flags;
|
||||
} mach_segment_command_64;
|
||||
typedef struct {
|
||||
char sectname[16], segname[16];
|
||||
uint32_t addr, size;
|
||||
uint32_t offset, align, reloff, nreloc, flags;
|
||||
uint32_t reserved1, reserved2;
|
||||
} mach_section;
|
||||
typedef struct {
|
||||
char sectname[16], segname[16];
|
||||
uint64_t addr, size;
|
||||
uint32_t offset, align, reloff, nreloc, flags;
|
||||
uint32_t reserved1, reserved2, reserved3;
|
||||
} mach_section_64;
|
||||
typedef struct {
|
||||
uint32_t cmd, cmdsize, symoff, nsyms, stroff, strsize;
|
||||
} mach_symtab_command;
|
||||
typedef struct {
|
||||
int32_t strx;
|
||||
uint8_t type, sect;
|
||||
int16_t desc;
|
||||
uint32_t value;
|
||||
} mach_nlist;
|
||||
typedef struct {
|
||||
int32_t strx;
|
||||
uint8_t type, sect;
|
||||
uint16_t desc;
|
||||
uint64_t value;
|
||||
} mach_nlist_64;
|
||||
typedef struct
|
||||
{
|
||||
int32_t magic, nfat_arch;
|
||||
} mach_fat_header;
|
||||
typedef struct
|
||||
{
|
||||
int32_t cputype, cpusubtype, offset, size, align;
|
||||
} mach_fat_arch;
|
||||
typedef struct {
|
||||
struct {
|
||||
mach_header hdr;
|
||||
mach_segment_command seg;
|
||||
mach_section sec;
|
||||
mach_symtab_command sym;
|
||||
} arch[1];
|
||||
mach_nlist sym_entry;
|
||||
uint8_t space[4096];
|
||||
} mach_obj;
|
||||
typedef struct {
|
||||
struct {
|
||||
mach_header_64 hdr;
|
||||
mach_segment_command_64 seg;
|
||||
mach_section_64 sec;
|
||||
mach_symtab_command sym;
|
||||
} arch[1];
|
||||
mach_nlist_64 sym_entry;
|
||||
uint8_t space[4096];
|
||||
} mach_obj_64;
|
||||
typedef struct {
|
||||
mach_fat_header fat;
|
||||
mach_fat_arch fat_arch[2];
|
||||
struct {
|
||||
mach_header hdr;
|
||||
mach_segment_command seg;
|
||||
mach_section sec;
|
||||
mach_symtab_command sym;
|
||||
} arch[2];
|
||||
mach_nlist sym_entry;
|
||||
uint8_t space[4096];
|
||||
} mach_fat_obj;
|
||||
typedef struct {
|
||||
mach_fat_header fat;
|
||||
mach_fat_arch fat_arch[2];
|
||||
struct {
|
||||
mach_header_64 hdr;
|
||||
mach_segment_command_64 seg;
|
||||
mach_section_64 sec;
|
||||
mach_symtab_command sym;
|
||||
} arch[2];
|
||||
mach_nlist_64 sym_entry;
|
||||
uint8_t space[4096];
|
||||
} mach_fat_obj_64;
|
||||
]]
|
||||
local symname = '_'..LJBC_PREFIX..ctx.modname
|
||||
local isfat, is64, align, mobj = false, false, 4, "mach_obj"
|
||||
if ctx.arch == "x64" then
|
||||
is64, align, mobj = true, 8, "mach_obj_64"
|
||||
elseif ctx.arch == "arm" then
|
||||
isfat, mobj = true, "mach_fat_obj"
|
||||
elseif ctx.arch == "arm64" then
|
||||
is64, align, isfat, mobj = true, 8, true, "mach_fat_obj_64"
|
||||
else
|
||||
check(ctx.arch == "x86", "unsupported architecture for OSX")
|
||||
end
|
||||
local function aligned(v, a) return bit.band(v+a-1, -a) end
|
||||
local be32 = bit.bswap -- Mach-O FAT is BE, supported archs are LE.
|
||||
|
||||
-- Create Mach-O object and fill in header.
|
||||
local o = ffi.new(mobj)
|
||||
local mach_size = aligned(ffi.offsetof(o, "space")+#symname+2, align)
|
||||
local cputype = ({ x86={7}, x64={0x01000007}, arm={7,12}, arm64={0x01000007,0x0100000c} })[ctx.arch]
|
||||
local cpusubtype = ({ x86={3}, x64={3}, arm={3,9}, arm64={3,0} })[ctx.arch]
|
||||
if isfat then
|
||||
o.fat.magic = be32(0xcafebabe)
|
||||
o.fat.nfat_arch = be32(#cpusubtype)
|
||||
end
|
||||
|
||||
-- Fill in sections and symbols.
|
||||
for i=0,#cpusubtype-1 do
|
||||
local ofs = 0
|
||||
if isfat then
|
||||
local a = o.fat_arch[i]
|
||||
a.cputype = be32(cputype[i+1])
|
||||
a.cpusubtype = be32(cpusubtype[i+1])
|
||||
-- Subsequent slices overlap each other to share data.
|
||||
ofs = ffi.offsetof(o, "arch") + i*ffi.sizeof(o.arch[0])
|
||||
a.offset = be32(ofs)
|
||||
a.size = be32(mach_size-ofs+#s)
|
||||
end
|
||||
local a = o.arch[i]
|
||||
a.hdr.magic = is64 and 0xfeedfacf or 0xfeedface
|
||||
a.hdr.cputype = cputype[i+1]
|
||||
a.hdr.cpusubtype = cpusubtype[i+1]
|
||||
a.hdr.filetype = 1
|
||||
a.hdr.ncmds = 2
|
||||
a.hdr.sizeofcmds = ffi.sizeof(a.seg)+ffi.sizeof(a.sec)+ffi.sizeof(a.sym)
|
||||
a.seg.cmd = is64 and 0x19 or 0x1
|
||||
a.seg.cmdsize = ffi.sizeof(a.seg)+ffi.sizeof(a.sec)
|
||||
a.seg.vmsize = #s
|
||||
a.seg.fileoff = mach_size-ofs
|
||||
a.seg.filesize = #s
|
||||
a.seg.maxprot = 1
|
||||
a.seg.initprot = 1
|
||||
a.seg.nsects = 1
|
||||
ffi.copy(a.sec.sectname, "__data")
|
||||
ffi.copy(a.sec.segname, "__DATA")
|
||||
a.sec.size = #s
|
||||
a.sec.offset = mach_size-ofs
|
||||
a.sym.cmd = 2
|
||||
a.sym.cmdsize = ffi.sizeof(a.sym)
|
||||
a.sym.symoff = ffi.offsetof(o, "sym_entry")-ofs
|
||||
a.sym.nsyms = 1
|
||||
a.sym.stroff = ffi.offsetof(o, "sym_entry")+ffi.sizeof(o.sym_entry)-ofs
|
||||
a.sym.strsize = aligned(#symname+2, align)
|
||||
end
|
||||
o.sym_entry.type = 0xf
|
||||
o.sym_entry.sect = 1
|
||||
o.sym_entry.strx = 1
|
||||
ffi.copy(o.space+1, symname)
|
||||
|
||||
-- Write Macho-O object file.
|
||||
local fp = savefile(output, "wb")
|
||||
fp:write(ffi.string(o, mach_size))
|
||||
bcsave_tail(fp, output, s)
|
||||
end
|
||||
|
||||
local function bcsave_obj(ctx, output, s)
|
||||
local ok, ffi = pcall(require, "ffi")
|
||||
check(ok, "FFI library required to write this file type")
|
||||
if output == "-" and jit.os == "Windows" then
|
||||
set_stdout_binary(ffi)
|
||||
end
|
||||
if ctx.os == "windows" then
|
||||
return bcsave_peobj(ctx, output, s, ffi)
|
||||
elseif ctx.os == "osx" then
|
||||
return bcsave_machobj(ctx, output, s, ffi)
|
||||
else
|
||||
return bcsave_elfobj(ctx, output, s, ffi)
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
local function bclist(ctx, input, output)
|
||||
local f = readfile(ctx, input)
|
||||
require("jit.bc").dump(f, savefile(output, "w"), true)
|
||||
end
|
||||
|
||||
local function bcsave(ctx, input, output)
|
||||
local f = readfile(ctx, input)
|
||||
local s = string.dump(f, ctx.strip)
|
||||
local t = ctx.type
|
||||
if not t then
|
||||
t = detecttype(output)
|
||||
ctx.type = t
|
||||
end
|
||||
if t == "raw" then
|
||||
bcsave_raw(output, s)
|
||||
else
|
||||
if not ctx.modname then ctx.modname = detectmodname(input) end
|
||||
if t == "obj" then
|
||||
bcsave_obj(ctx, output, s)
|
||||
else
|
||||
bcsave_c(ctx, output, s)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function docmd(...)
|
||||
local arg = {...}
|
||||
local n = 1
|
||||
local list = false
|
||||
local ctx = {
|
||||
strip = true, arch = jit.arch, os = jit.os:lower(),
|
||||
type = false, modname = false,
|
||||
}
|
||||
while n <= #arg do
|
||||
local a = arg[n]
|
||||
if type(a) == "string" and a:sub(1, 1) == "-" and a ~= "-" then
|
||||
tremove(arg, n)
|
||||
if a == "--" then break end
|
||||
for m=2,#a do
|
||||
local opt = a:sub(m, m)
|
||||
if opt == "l" then
|
||||
list = true
|
||||
elseif opt == "s" then
|
||||
ctx.strip = true
|
||||
elseif opt == "g" then
|
||||
ctx.strip = false
|
||||
else
|
||||
if arg[n] == nil or m ~= #a then usage() end
|
||||
if opt == "e" then
|
||||
if n ~= 1 then usage() end
|
||||
arg[1] = check(loadstring(arg[1]))
|
||||
elseif opt == "n" then
|
||||
ctx.modname = checkmodname(tremove(arg, n))
|
||||
elseif opt == "t" then
|
||||
ctx.type = checkarg(tremove(arg, n), map_type, "file type")
|
||||
elseif opt == "a" then
|
||||
ctx.arch = checkarg(tremove(arg, n), map_arch, "architecture")
|
||||
elseif opt == "o" then
|
||||
ctx.os = checkarg(tremove(arg, n), map_os, "OS name")
|
||||
elseif opt == "F" then
|
||||
ctx.filename = "@"..tremove(arg, n)
|
||||
else
|
||||
usage()
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
n = n + 1
|
||||
end
|
||||
end
|
||||
if list then
|
||||
if #arg == 0 or #arg > 2 then usage() end
|
||||
bclist(ctx, arg[1], arg[2] or "-")
|
||||
else
|
||||
if #arg ~= 2 then usage() end
|
||||
bcsave(ctx, arg[1], arg[2])
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
-- Public module functions.
|
||||
return {
|
||||
start = docmd -- Process -b command line option.
|
||||
}
|
||||
|
||||
@@ -0,0 +1,689 @@
|
||||
----------------------------------------------------------------------------
|
||||
-- LuaJIT ARM disassembler module.
|
||||
--
|
||||
-- Copyright (C) 2005-2023 Mike Pall. All rights reserved.
|
||||
-- Released under the MIT license. See Copyright Notice in luajit.h
|
||||
----------------------------------------------------------------------------
|
||||
-- This is a helper module used by the LuaJIT machine code dumper module.
|
||||
--
|
||||
-- It disassembles most user-mode ARMv7 instructions
|
||||
-- NYI: Advanced SIMD and VFP instructions.
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
local type = type
|
||||
local sub, byte, format = string.sub, string.byte, string.format
|
||||
local match, gmatch = string.match, string.gmatch
|
||||
local concat = table.concat
|
||||
local bit = require("bit")
|
||||
local band, bor, ror, tohex = bit.band, bit.bor, bit.ror, bit.tohex
|
||||
local lshift, rshift, arshift = bit.lshift, bit.rshift, bit.arshift
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Opcode maps
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
local map_loadc = {
|
||||
shift = 8, mask = 15,
|
||||
[10] = {
|
||||
shift = 20, mask = 1,
|
||||
[0] = {
|
||||
shift = 23, mask = 3,
|
||||
[0] = "vmovFmDN", "vstmFNdr",
|
||||
_ = {
|
||||
shift = 21, mask = 1,
|
||||
[0] = "vstrFdl",
|
||||
{ shift = 16, mask = 15, [13] = "vpushFdr", _ = "vstmdbFNdr", }
|
||||
},
|
||||
},
|
||||
{
|
||||
shift = 23, mask = 3,
|
||||
[0] = "vmovFDNm",
|
||||
{ shift = 16, mask = 15, [13] = "vpopFdr", _ = "vldmFNdr", },
|
||||
_ = {
|
||||
shift = 21, mask = 1,
|
||||
[0] = "vldrFdl", "vldmdbFNdr",
|
||||
},
|
||||
},
|
||||
},
|
||||
[11] = {
|
||||
shift = 20, mask = 1,
|
||||
[0] = {
|
||||
shift = 23, mask = 3,
|
||||
[0] = "vmovGmDN", "vstmGNdr",
|
||||
_ = {
|
||||
shift = 21, mask = 1,
|
||||
[0] = "vstrGdl",
|
||||
{ shift = 16, mask = 15, [13] = "vpushGdr", _ = "vstmdbGNdr", }
|
||||
},
|
||||
},
|
||||
{
|
||||
shift = 23, mask = 3,
|
||||
[0] = "vmovGDNm",
|
||||
{ shift = 16, mask = 15, [13] = "vpopGdr", _ = "vldmGNdr", },
|
||||
_ = {
|
||||
shift = 21, mask = 1,
|
||||
[0] = "vldrGdl", "vldmdbGNdr",
|
||||
},
|
||||
},
|
||||
},
|
||||
_ = {
|
||||
shift = 0, mask = 0 -- NYI ldc, mcrr, mrrc.
|
||||
},
|
||||
}
|
||||
|
||||
local map_vfps = {
|
||||
shift = 6, mask = 0x2c001,
|
||||
[0] = "vmlaF.dnm", "vmlsF.dnm",
|
||||
[0x04000] = "vnmlsF.dnm", [0x04001] = "vnmlaF.dnm",
|
||||
[0x08000] = "vmulF.dnm", [0x08001] = "vnmulF.dnm",
|
||||
[0x0c000] = "vaddF.dnm", [0x0c001] = "vsubF.dnm",
|
||||
[0x20000] = "vdivF.dnm",
|
||||
[0x24000] = "vfnmsF.dnm", [0x24001] = "vfnmaF.dnm",
|
||||
[0x28000] = "vfmaF.dnm", [0x28001] = "vfmsF.dnm",
|
||||
[0x2c000] = "vmovF.dY",
|
||||
[0x2c001] = {
|
||||
shift = 7, mask = 0x1e01,
|
||||
[0] = "vmovF.dm", "vabsF.dm",
|
||||
[0x0200] = "vnegF.dm", [0x0201] = "vsqrtF.dm",
|
||||
[0x0800] = "vcmpF.dm", [0x0801] = "vcmpeF.dm",
|
||||
[0x0a00] = "vcmpzF.d", [0x0a01] = "vcmpzeF.d",
|
||||
[0x0e01] = "vcvtG.dF.m",
|
||||
[0x1000] = "vcvt.f32.u32Fdm", [0x1001] = "vcvt.f32.s32Fdm",
|
||||
[0x1800] = "vcvtr.u32F.dm", [0x1801] = "vcvt.u32F.dm",
|
||||
[0x1a00] = "vcvtr.s32F.dm", [0x1a01] = "vcvt.s32F.dm",
|
||||
},
|
||||
}
|
||||
|
||||
local map_vfpd = {
|
||||
shift = 6, mask = 0x2c001,
|
||||
[0] = "vmlaG.dnm", "vmlsG.dnm",
|
||||
[0x04000] = "vnmlsG.dnm", [0x04001] = "vnmlaG.dnm",
|
||||
[0x08000] = "vmulG.dnm", [0x08001] = "vnmulG.dnm",
|
||||
[0x0c000] = "vaddG.dnm", [0x0c001] = "vsubG.dnm",
|
||||
[0x20000] = "vdivG.dnm",
|
||||
[0x24000] = "vfnmsG.dnm", [0x24001] = "vfnmaG.dnm",
|
||||
[0x28000] = "vfmaG.dnm", [0x28001] = "vfmsG.dnm",
|
||||
[0x2c000] = "vmovG.dY",
|
||||
[0x2c001] = {
|
||||
shift = 7, mask = 0x1e01,
|
||||
[0] = "vmovG.dm", "vabsG.dm",
|
||||
[0x0200] = "vnegG.dm", [0x0201] = "vsqrtG.dm",
|
||||
[0x0800] = "vcmpG.dm", [0x0801] = "vcmpeG.dm",
|
||||
[0x0a00] = "vcmpzG.d", [0x0a01] = "vcmpzeG.d",
|
||||
[0x0e01] = "vcvtF.dG.m",
|
||||
[0x1000] = "vcvt.f64.u32GdFm", [0x1001] = "vcvt.f64.s32GdFm",
|
||||
[0x1800] = "vcvtr.u32FdG.m", [0x1801] = "vcvt.u32FdG.m",
|
||||
[0x1a00] = "vcvtr.s32FdG.m", [0x1a01] = "vcvt.s32FdG.m",
|
||||
},
|
||||
}
|
||||
|
||||
local map_datac = {
|
||||
shift = 24, mask = 1,
|
||||
[0] = {
|
||||
shift = 4, mask = 1,
|
||||
[0] = {
|
||||
shift = 8, mask = 15,
|
||||
[10] = map_vfps,
|
||||
[11] = map_vfpd,
|
||||
-- NYI cdp, mcr, mrc.
|
||||
},
|
||||
{
|
||||
shift = 8, mask = 15,
|
||||
[10] = {
|
||||
shift = 20, mask = 15,
|
||||
[0] = "vmovFnD", "vmovFDn",
|
||||
[14] = "vmsrD",
|
||||
[15] = { shift = 12, mask = 15, [15] = "vmrs", _ = "vmrsD", },
|
||||
},
|
||||
},
|
||||
},
|
||||
"svcT",
|
||||
}
|
||||
|
||||
local map_loadcu = {
|
||||
shift = 0, mask = 0, -- NYI unconditional CP load/store.
|
||||
}
|
||||
|
||||
local map_datacu = {
|
||||
shift = 0, mask = 0, -- NYI unconditional CP data.
|
||||
}
|
||||
|
||||
local map_simddata = {
|
||||
shift = 0, mask = 0, -- NYI SIMD data.
|
||||
}
|
||||
|
||||
local map_simdload = {
|
||||
shift = 0, mask = 0, -- NYI SIMD load/store, preload.
|
||||
}
|
||||
|
||||
local map_preload = {
|
||||
shift = 0, mask = 0, -- NYI preload.
|
||||
}
|
||||
|
||||
local map_media = {
|
||||
shift = 20, mask = 31,
|
||||
[0] = false,
|
||||
{ --01
|
||||
shift = 5, mask = 7,
|
||||
[0] = "sadd16DNM", "sasxDNM", "ssaxDNM", "ssub16DNM",
|
||||
"sadd8DNM", false, false, "ssub8DNM",
|
||||
},
|
||||
{ --02
|
||||
shift = 5, mask = 7,
|
||||
[0] = "qadd16DNM", "qasxDNM", "qsaxDNM", "qsub16DNM",
|
||||
"qadd8DNM", false, false, "qsub8DNM",
|
||||
},
|
||||
{ --03
|
||||
shift = 5, mask = 7,
|
||||
[0] = "shadd16DNM", "shasxDNM", "shsaxDNM", "shsub16DNM",
|
||||
"shadd8DNM", false, false, "shsub8DNM",
|
||||
},
|
||||
false,
|
||||
{ --05
|
||||
shift = 5, mask = 7,
|
||||
[0] = "uadd16DNM", "uasxDNM", "usaxDNM", "usub16DNM",
|
||||
"uadd8DNM", false, false, "usub8DNM",
|
||||
},
|
||||
{ --06
|
||||
shift = 5, mask = 7,
|
||||
[0] = "uqadd16DNM", "uqasxDNM", "uqsaxDNM", "uqsub16DNM",
|
||||
"uqadd8DNM", false, false, "uqsub8DNM",
|
||||
},
|
||||
{ --07
|
||||
shift = 5, mask = 7,
|
||||
[0] = "uhadd16DNM", "uhasxDNM", "uhsaxDNM", "uhsub16DNM",
|
||||
"uhadd8DNM", false, false, "uhsub8DNM",
|
||||
},
|
||||
{ --08
|
||||
shift = 5, mask = 7,
|
||||
[0] = "pkhbtDNMU", false, "pkhtbDNMU",
|
||||
{ shift = 16, mask = 15, [15] = "sxtb16DMU", _ = "sxtab16DNMU", },
|
||||
"pkhbtDNMU", "selDNM", "pkhtbDNMU",
|
||||
},
|
||||
false,
|
||||
{ --0a
|
||||
shift = 5, mask = 7,
|
||||
[0] = "ssatDxMu", "ssat16DxM", "ssatDxMu",
|
||||
{ shift = 16, mask = 15, [15] = "sxtbDMU", _ = "sxtabDNMU", },
|
||||
"ssatDxMu", false, "ssatDxMu",
|
||||
},
|
||||
{ --0b
|
||||
shift = 5, mask = 7,
|
||||
[0] = "ssatDxMu", "revDM", "ssatDxMu",
|
||||
{ shift = 16, mask = 15, [15] = "sxthDMU", _ = "sxtahDNMU", },
|
||||
"ssatDxMu", "rev16DM", "ssatDxMu",
|
||||
},
|
||||
{ --0c
|
||||
shift = 5, mask = 7,
|
||||
[3] = { shift = 16, mask = 15, [15] = "uxtb16DMU", _ = "uxtab16DNMU", },
|
||||
},
|
||||
false,
|
||||
{ --0e
|
||||
shift = 5, mask = 7,
|
||||
[0] = "usatDwMu", "usat16DwM", "usatDwMu",
|
||||
{ shift = 16, mask = 15, [15] = "uxtbDMU", _ = "uxtabDNMU", },
|
||||
"usatDwMu", false, "usatDwMu",
|
||||
},
|
||||
{ --0f
|
||||
shift = 5, mask = 7,
|
||||
[0] = "usatDwMu", "rbitDM", "usatDwMu",
|
||||
{ shift = 16, mask = 15, [15] = "uxthDMU", _ = "uxtahDNMU", },
|
||||
"usatDwMu", "revshDM", "usatDwMu",
|
||||
},
|
||||
{ --10
|
||||
shift = 12, mask = 15,
|
||||
[15] = {
|
||||
shift = 5, mask = 7,
|
||||
"smuadNMS", "smuadxNMS", "smusdNMS", "smusdxNMS",
|
||||
},
|
||||
_ = {
|
||||
shift = 5, mask = 7,
|
||||
[0] = "smladNMSD", "smladxNMSD", "smlsdNMSD", "smlsdxNMSD",
|
||||
},
|
||||
},
|
||||
false, false, false,
|
||||
{ --14
|
||||
shift = 5, mask = 7,
|
||||
[0] = "smlaldDNMS", "smlaldxDNMS", "smlsldDNMS", "smlsldxDNMS",
|
||||
},
|
||||
{ --15
|
||||
shift = 5, mask = 7,
|
||||
[0] = { shift = 12, mask = 15, [15] = "smmulNMS", _ = "smmlaNMSD", },
|
||||
{ shift = 12, mask = 15, [15] = "smmulrNMS", _ = "smmlarNMSD", },
|
||||
false, false, false, false,
|
||||
"smmlsNMSD", "smmlsrNMSD",
|
||||
},
|
||||
false, false,
|
||||
{ --18
|
||||
shift = 5, mask = 7,
|
||||
[0] = { shift = 12, mask = 15, [15] = "usad8NMS", _ = "usada8NMSD", },
|
||||
},
|
||||
false,
|
||||
{ --1a
|
||||
shift = 5, mask = 3, [2] = "sbfxDMvw",
|
||||
},
|
||||
{ --1b
|
||||
shift = 5, mask = 3, [2] = "sbfxDMvw",
|
||||
},
|
||||
{ --1c
|
||||
shift = 5, mask = 3,
|
||||
[0] = { shift = 0, mask = 15, [15] = "bfcDvX", _ = "bfiDMvX", },
|
||||
},
|
||||
{ --1d
|
||||
shift = 5, mask = 3,
|
||||
[0] = { shift = 0, mask = 15, [15] = "bfcDvX", _ = "bfiDMvX", },
|
||||
},
|
||||
{ --1e
|
||||
shift = 5, mask = 3, [2] = "ubfxDMvw",
|
||||
},
|
||||
{ --1f
|
||||
shift = 5, mask = 3, [2] = "ubfxDMvw",
|
||||
},
|
||||
}
|
||||
|
||||
local map_load = {
|
||||
shift = 21, mask = 9,
|
||||
{
|
||||
shift = 20, mask = 5,
|
||||
[0] = "strtDL", "ldrtDL", [4] = "strbtDL", [5] = "ldrbtDL",
|
||||
},
|
||||
_ = {
|
||||
shift = 20, mask = 5,
|
||||
[0] = "strDL", "ldrDL", [4] = "strbDL", [5] = "ldrbDL",
|
||||
}
|
||||
}
|
||||
|
||||
local map_load1 = {
|
||||
shift = 4, mask = 1,
|
||||
[0] = map_load, map_media,
|
||||
}
|
||||
|
||||
local map_loadm = {
|
||||
shift = 20, mask = 1,
|
||||
[0] = {
|
||||
shift = 23, mask = 3,
|
||||
[0] = "stmdaNR", "stmNR",
|
||||
{ shift = 16, mask = 63, [45] = "pushR", _ = "stmdbNR", }, "stmibNR",
|
||||
},
|
||||
{
|
||||
shift = 23, mask = 3,
|
||||
[0] = "ldmdaNR", { shift = 16, mask = 63, [61] = "popR", _ = "ldmNR", },
|
||||
"ldmdbNR", "ldmibNR",
|
||||
},
|
||||
}
|
||||
|
||||
local map_data = {
|
||||
shift = 21, mask = 15,
|
||||
[0] = "andDNPs", "eorDNPs", "subDNPs", "rsbDNPs",
|
||||
"addDNPs", "adcDNPs", "sbcDNPs", "rscDNPs",
|
||||
"tstNP", "teqNP", "cmpNP", "cmnNP",
|
||||
"orrDNPs", "movDPs", "bicDNPs", "mvnDPs",
|
||||
}
|
||||
|
||||
local map_mul = {
|
||||
shift = 21, mask = 7,
|
||||
[0] = "mulNMSs", "mlaNMSDs", "umaalDNMS", "mlsDNMS",
|
||||
"umullDNMSs", "umlalDNMSs", "smullDNMSs", "smlalDNMSs",
|
||||
}
|
||||
|
||||
local map_sync = {
|
||||
shift = 20, mask = 15, -- NYI: brackets around N. R(D+1) for ldrexd/strexd.
|
||||
[0] = "swpDMN", false, false, false,
|
||||
"swpbDMN", false, false, false,
|
||||
"strexDMN", "ldrexDN", "strexdDN", "ldrexdDN",
|
||||
"strexbDMN", "ldrexbDN", "strexhDN", "ldrexhDN",
|
||||
}
|
||||
|
||||
local map_mulh = {
|
||||
shift = 21, mask = 3,
|
||||
[0] = { shift = 5, mask = 3,
|
||||
[0] = "smlabbNMSD", "smlatbNMSD", "smlabtNMSD", "smlattNMSD", },
|
||||
{ shift = 5, mask = 3,
|
||||
[0] = "smlawbNMSD", "smulwbNMS", "smlawtNMSD", "smulwtNMS", },
|
||||
{ shift = 5, mask = 3,
|
||||
[0] = "smlalbbDNMS", "smlaltbDNMS", "smlalbtDNMS", "smlalttDNMS", },
|
||||
{ shift = 5, mask = 3,
|
||||
[0] = "smulbbNMS", "smultbNMS", "smulbtNMS", "smulttNMS", },
|
||||
}
|
||||
|
||||
local map_misc = {
|
||||
shift = 4, mask = 7,
|
||||
-- NYI: decode PSR bits of msr.
|
||||
[0] = { shift = 21, mask = 1, [0] = "mrsD", "msrM", },
|
||||
{ shift = 21, mask = 3, "bxM", false, "clzDM", },
|
||||
{ shift = 21, mask = 3, "bxjM", },
|
||||
{ shift = 21, mask = 3, "blxM", },
|
||||
false,
|
||||
{ shift = 21, mask = 3, [0] = "qaddDMN", "qsubDMN", "qdaddDMN", "qdsubDMN", },
|
||||
false,
|
||||
{ shift = 21, mask = 3, "bkptK", },
|
||||
}
|
||||
|
||||
local map_datar = {
|
||||
shift = 4, mask = 9,
|
||||
[9] = {
|
||||
shift = 5, mask = 3,
|
||||
[0] = { shift = 24, mask = 1, [0] = map_mul, map_sync, },
|
||||
{ shift = 20, mask = 1, [0] = "strhDL", "ldrhDL", },
|
||||
{ shift = 20, mask = 1, [0] = "ldrdDL", "ldrsbDL", },
|
||||
{ shift = 20, mask = 1, [0] = "strdDL", "ldrshDL", },
|
||||
},
|
||||
_ = {
|
||||
shift = 20, mask = 25,
|
||||
[16] = { shift = 7, mask = 1, [0] = map_misc, map_mulh, },
|
||||
_ = {
|
||||
shift = 0, mask = 0xffffffff,
|
||||
[bor(0xe1a00000)] = "nop",
|
||||
_ = map_data,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
local map_datai = {
|
||||
shift = 20, mask = 31, -- NYI: decode PSR bits of msr. Decode imm12.
|
||||
[16] = "movwDW", [20] = "movtDW",
|
||||
[18] = { shift = 0, mask = 0xf00ff, [0] = "nopv6", _ = "msrNW", },
|
||||
[22] = "msrNW",
|
||||
_ = map_data,
|
||||
}
|
||||
|
||||
local map_branch = {
|
||||
shift = 24, mask = 1,
|
||||
[0] = "bB", "blB"
|
||||
}
|
||||
|
||||
local map_condins = {
|
||||
[0] = map_datar, map_datai, map_load, map_load1,
|
||||
map_loadm, map_branch, map_loadc, map_datac
|
||||
}
|
||||
|
||||
-- NYI: setend.
|
||||
local map_uncondins = {
|
||||
[0] = false, map_simddata, map_simdload, map_preload,
|
||||
false, "blxB", map_loadcu, map_datacu,
|
||||
}
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
local map_gpr = {
|
||||
[0] = "r0", "r1", "r2", "r3", "r4", "r5", "r6", "r7",
|
||||
"r8", "r9", "r10", "r11", "r12", "sp", "lr", "pc",
|
||||
}
|
||||
|
||||
local map_cond = {
|
||||
[0] = "eq", "ne", "hs", "lo", "mi", "pl", "vs", "vc",
|
||||
"hi", "ls", "ge", "lt", "gt", "le", "al",
|
||||
}
|
||||
|
||||
local map_shift = { [0] = "lsl", "lsr", "asr", "ror", }
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
-- Output a nicely formatted line with an opcode and operands.
|
||||
local function putop(ctx, text, operands)
|
||||
local pos = ctx.pos
|
||||
local extra = ""
|
||||
if ctx.rel then
|
||||
local sym = ctx.symtab[ctx.rel]
|
||||
if sym then
|
||||
extra = "\t->"..sym
|
||||
elseif band(ctx.op, 0x0e000000) ~= 0x0a000000 then
|
||||
extra = "\t; 0x"..tohex(ctx.rel)
|
||||
end
|
||||
end
|
||||
if ctx.hexdump > 0 then
|
||||
ctx.out(format("%08x %s %-5s %s%s\n",
|
||||
ctx.addr+pos, tohex(ctx.op), text, concat(operands, ", "), extra))
|
||||
else
|
||||
ctx.out(format("%08x %-5s %s%s\n",
|
||||
ctx.addr+pos, text, concat(operands, ", "), extra))
|
||||
end
|
||||
ctx.pos = pos + 4
|
||||
end
|
||||
|
||||
-- Fallback for unknown opcodes.
|
||||
local function unknown(ctx)
|
||||
return putop(ctx, ".long", { "0x"..tohex(ctx.op) })
|
||||
end
|
||||
|
||||
-- Format operand 2 of load/store opcodes.
|
||||
local function fmtload(ctx, op, pos)
|
||||
local base = map_gpr[band(rshift(op, 16), 15)]
|
||||
local x, ofs
|
||||
local ext = (band(op, 0x04000000) == 0)
|
||||
if not ext and band(op, 0x02000000) == 0 then
|
||||
ofs = band(op, 4095)
|
||||
if band(op, 0x00800000) == 0 then ofs = -ofs end
|
||||
if base == "pc" then ctx.rel = ctx.addr + pos + 8 + ofs end
|
||||
ofs = "#"..ofs
|
||||
elseif ext and band(op, 0x00400000) ~= 0 then
|
||||
ofs = band(op, 15) + band(rshift(op, 4), 0xf0)
|
||||
if band(op, 0x00800000) == 0 then ofs = -ofs end
|
||||
if base == "pc" then ctx.rel = ctx.addr + pos + 8 + ofs end
|
||||
ofs = "#"..ofs
|
||||
else
|
||||
ofs = map_gpr[band(op, 15)]
|
||||
if ext or band(op, 0xfe0) == 0 then
|
||||
elseif band(op, 0xfe0) == 0x60 then
|
||||
ofs = format("%s, rrx", ofs)
|
||||
else
|
||||
local sh = band(rshift(op, 7), 31)
|
||||
if sh == 0 then sh = 32 end
|
||||
ofs = format("%s, %s #%d", ofs, map_shift[band(rshift(op, 5), 3)], sh)
|
||||
end
|
||||
if band(op, 0x00800000) == 0 then ofs = "-"..ofs end
|
||||
end
|
||||
if ofs == "#0" then
|
||||
x = format("[%s]", base)
|
||||
elseif band(op, 0x01000000) == 0 then
|
||||
x = format("[%s], %s", base, ofs)
|
||||
else
|
||||
x = format("[%s, %s]", base, ofs)
|
||||
end
|
||||
if band(op, 0x01200000) == 0x01200000 then x = x.."!" end
|
||||
return x
|
||||
end
|
||||
|
||||
-- Format operand 2 of vector load/store opcodes.
|
||||
local function fmtvload(ctx, op, pos)
|
||||
local base = map_gpr[band(rshift(op, 16), 15)]
|
||||
local ofs = band(op, 255)*4
|
||||
if band(op, 0x00800000) == 0 then ofs = -ofs end
|
||||
if base == "pc" then ctx.rel = ctx.addr + pos + 8 + ofs end
|
||||
if ofs == 0 then
|
||||
return format("[%s]", base)
|
||||
else
|
||||
return format("[%s, #%d]", base, ofs)
|
||||
end
|
||||
end
|
||||
|
||||
local function fmtvr(op, vr, sh0, sh1)
|
||||
if vr == "s" then
|
||||
return format("s%d", 2*band(rshift(op, sh0), 15)+band(rshift(op, sh1), 1))
|
||||
else
|
||||
return format("d%d", band(rshift(op, sh0), 15)+band(rshift(op, sh1-4), 16))
|
||||
end
|
||||
end
|
||||
|
||||
-- Disassemble a single instruction.
|
||||
local function disass_ins(ctx)
|
||||
local pos = ctx.pos
|
||||
local b0, b1, b2, b3 = byte(ctx.code, pos+1, pos+4)
|
||||
local op = bor(lshift(b3, 24), lshift(b2, 16), lshift(b1, 8), b0)
|
||||
local operands = {}
|
||||
local suffix = ""
|
||||
local last, name, pat
|
||||
local vr
|
||||
ctx.op = op
|
||||
ctx.rel = nil
|
||||
|
||||
local cond = rshift(op, 28)
|
||||
local opat
|
||||
if cond == 15 then
|
||||
opat = map_uncondins[band(rshift(op, 25), 7)]
|
||||
else
|
||||
if cond ~= 14 then suffix = map_cond[cond] end
|
||||
opat = map_condins[band(rshift(op, 25), 7)]
|
||||
end
|
||||
while type(opat) ~= "string" do
|
||||
if not opat then return unknown(ctx) end
|
||||
opat = opat[band(rshift(op, opat.shift), opat.mask)] or opat._
|
||||
end
|
||||
name, pat = match(opat, "^([a-z0-9]*)(.*)")
|
||||
if sub(pat, 1, 1) == "." then
|
||||
local s2, p2 = match(pat, "^([a-z0-9.]*)(.*)")
|
||||
suffix = suffix..s2
|
||||
pat = p2
|
||||
end
|
||||
|
||||
for p in gmatch(pat, ".") do
|
||||
local x = nil
|
||||
if p == "D" then
|
||||
x = map_gpr[band(rshift(op, 12), 15)]
|
||||
elseif p == "N" then
|
||||
x = map_gpr[band(rshift(op, 16), 15)]
|
||||
elseif p == "S" then
|
||||
x = map_gpr[band(rshift(op, 8), 15)]
|
||||
elseif p == "M" then
|
||||
x = map_gpr[band(op, 15)]
|
||||
elseif p == "d" then
|
||||
x = fmtvr(op, vr, 12, 22)
|
||||
elseif p == "n" then
|
||||
x = fmtvr(op, vr, 16, 7)
|
||||
elseif p == "m" then
|
||||
x = fmtvr(op, vr, 0, 5)
|
||||
elseif p == "P" then
|
||||
if band(op, 0x02000000) ~= 0 then
|
||||
x = ror(band(op, 255), 2*band(rshift(op, 8), 15))
|
||||
else
|
||||
x = map_gpr[band(op, 15)]
|
||||
if band(op, 0xff0) ~= 0 then
|
||||
operands[#operands+1] = x
|
||||
local s = map_shift[band(rshift(op, 5), 3)]
|
||||
local r = nil
|
||||
if band(op, 0xf90) == 0 then
|
||||
if s == "ror" then s = "rrx" else r = "#32" end
|
||||
elseif band(op, 0x10) == 0 then
|
||||
r = "#"..band(rshift(op, 7), 31)
|
||||
else
|
||||
r = map_gpr[band(rshift(op, 8), 15)]
|
||||
end
|
||||
if name == "mov" then name = s; x = r
|
||||
elseif r then x = format("%s %s", s, r)
|
||||
else x = s end
|
||||
end
|
||||
end
|
||||
elseif p == "L" then
|
||||
x = fmtload(ctx, op, pos)
|
||||
elseif p == "l" then
|
||||
x = fmtvload(ctx, op, pos)
|
||||
elseif p == "B" then
|
||||
local addr = ctx.addr + pos + 8 + arshift(lshift(op, 8), 6)
|
||||
if cond == 15 then addr = addr + band(rshift(op, 23), 2) end
|
||||
ctx.rel = addr
|
||||
x = "0x"..tohex(addr)
|
||||
elseif p == "F" then
|
||||
vr = "s"
|
||||
elseif p == "G" then
|
||||
vr = "d"
|
||||
elseif p == "." then
|
||||
suffix = suffix..(vr == "s" and ".f32" or ".f64")
|
||||
elseif p == "R" then
|
||||
if band(op, 0x00200000) ~= 0 and #operands == 1 then
|
||||
operands[1] = operands[1].."!"
|
||||
end
|
||||
local t = {}
|
||||
for i=0,15 do
|
||||
if band(rshift(op, i), 1) == 1 then t[#t+1] = map_gpr[i] end
|
||||
end
|
||||
x = "{"..concat(t, ", ").."}"
|
||||
elseif p == "r" then
|
||||
if band(op, 0x00200000) ~= 0 and #operands == 2 then
|
||||
operands[1] = operands[1].."!"
|
||||
end
|
||||
local s = tonumber(sub(last, 2))
|
||||
local n = band(op, 255)
|
||||
if vr == "d" then n = rshift(n, 1) end
|
||||
operands[#operands] = format("{%s-%s%d}", last, vr, s+n-1)
|
||||
elseif p == "W" then
|
||||
x = band(op, 0x0fff) + band(rshift(op, 4), 0xf000)
|
||||
elseif p == "T" then
|
||||
x = "#0x"..tohex(band(op, 0x00ffffff), 6)
|
||||
elseif p == "U" then
|
||||
x = band(rshift(op, 7), 31)
|
||||
if x == 0 then x = nil end
|
||||
elseif p == "u" then
|
||||
x = band(rshift(op, 7), 31)
|
||||
if band(op, 0x40) == 0 then
|
||||
if x == 0 then x = nil else x = "lsl #"..x end
|
||||
else
|
||||
if x == 0 then x = "asr #32" else x = "asr #"..x end
|
||||
end
|
||||
elseif p == "v" then
|
||||
x = band(rshift(op, 7), 31)
|
||||
elseif p == "w" then
|
||||
x = band(rshift(op, 16), 31)
|
||||
elseif p == "x" then
|
||||
x = band(rshift(op, 16), 31) + 1
|
||||
elseif p == "X" then
|
||||
x = band(rshift(op, 16), 31) - last + 1
|
||||
elseif p == "Y" then
|
||||
x = band(rshift(op, 12), 0xf0) + band(op, 0x0f)
|
||||
elseif p == "K" then
|
||||
x = "#0x"..tohex(band(rshift(op, 4), 0x0000fff0) + band(op, 15), 4)
|
||||
elseif p == "s" then
|
||||
if band(op, 0x00100000) ~= 0 then suffix = "s"..suffix end
|
||||
else
|
||||
assert(false)
|
||||
end
|
||||
if x then
|
||||
last = x
|
||||
if type(x) == "number" then x = "#"..x end
|
||||
operands[#operands+1] = x
|
||||
end
|
||||
end
|
||||
|
||||
return putop(ctx, name..suffix, operands)
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
-- Disassemble a block of code.
|
||||
local function disass_block(ctx, ofs, len)
|
||||
if not ofs then ofs = 0 end
|
||||
local stop = len and ofs+len or #ctx.code
|
||||
ctx.pos = ofs
|
||||
ctx.rel = nil
|
||||
while ctx.pos < stop do disass_ins(ctx) end
|
||||
end
|
||||
|
||||
-- Extended API: create a disassembler context. Then call ctx:disass(ofs, len).
|
||||
local function create(code, addr, out)
|
||||
local ctx = {}
|
||||
ctx.code = code
|
||||
ctx.addr = addr or 0
|
||||
ctx.out = out or io.write
|
||||
ctx.symtab = {}
|
||||
ctx.disass = disass_block
|
||||
ctx.hexdump = 8
|
||||
return ctx
|
||||
end
|
||||
|
||||
-- Simple API: disassemble code (a string) at address and output via out.
|
||||
local function disass(code, addr, out)
|
||||
create(code, addr, out):disass()
|
||||
end
|
||||
|
||||
-- Return register name for RID.
|
||||
local function regname(r)
|
||||
if r < 16 then return map_gpr[r] end
|
||||
return "d"..(r-16)
|
||||
end
|
||||
|
||||
-- Public module functions.
|
||||
return {
|
||||
create = create,
|
||||
disass = disass,
|
||||
regname = regname
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
----------------------------------------------------------------------------
|
||||
-- LuaJIT ARM64BE disassembler wrapper module.
|
||||
--
|
||||
-- Copyright (C) 2005-2023 Mike Pall. All rights reserved.
|
||||
-- Released under the MIT license. See Copyright Notice in luajit.h
|
||||
----------------------------------------------------------------------------
|
||||
-- ARM64 instructions are always little-endian. So just forward to the
|
||||
-- common ARM64 disassembler module. All the interesting stuff is there.
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
return require((string.match(..., ".*%.") or "").."dis_arm64")
|
||||
|
||||
@@ -0,0 +1,694 @@
|
||||
----------------------------------------------------------------------------
|
||||
-- LuaJIT MIPS disassembler module.
|
||||
--
|
||||
-- Copyright (C) 2005-2023 Mike Pall. All rights reserved.
|
||||
-- Released under the MIT/X license. See Copyright Notice in luajit.h
|
||||
----------------------------------------------------------------------------
|
||||
-- This is a helper module used by the LuaJIT machine code dumper module.
|
||||
--
|
||||
-- It disassembles all standard MIPS32R1/R2 instructions.
|
||||
-- Default mode is big-endian, but see: dis_mipsel.lua
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
local type = type
|
||||
local byte, format = string.byte, string.format
|
||||
local match, gmatch = string.match, string.gmatch
|
||||
local concat = table.concat
|
||||
local bit = require("bit")
|
||||
local band, bor, tohex = bit.band, bit.bor, bit.tohex
|
||||
local lshift, rshift, arshift = bit.lshift, bit.rshift, bit.arshift
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Extended opcode maps common to all MIPS releases
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
local map_srl = { shift = 21, mask = 1, [0] = "srlDTA", "rotrDTA", }
|
||||
local map_srlv = { shift = 6, mask = 1, [0] = "srlvDTS", "rotrvDTS", }
|
||||
|
||||
local map_cop0 = {
|
||||
shift = 25, mask = 1,
|
||||
[0] = {
|
||||
shift = 21, mask = 15,
|
||||
[0] = "mfc0TDW", [4] = "mtc0TDW",
|
||||
[10] = "rdpgprDT",
|
||||
[11] = { shift = 5, mask = 1, [0] = "diT0", "eiT0", },
|
||||
[14] = "wrpgprDT",
|
||||
}, {
|
||||
shift = 0, mask = 63,
|
||||
[1] = "tlbr", [2] = "tlbwi", [6] = "tlbwr", [8] = "tlbp",
|
||||
[24] = "eret", [31] = "deret",
|
||||
[32] = "wait",
|
||||
},
|
||||
}
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Primary and extended opcode maps for MIPS R1-R5
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
local map_movci = { shift = 16, mask = 1, [0] = "movfDSC", "movtDSC", }
|
||||
|
||||
local map_special = {
|
||||
shift = 0, mask = 63,
|
||||
[0] = { shift = 0, mask = -1, [0] = "nop", _ = "sllDTA" },
|
||||
map_movci, map_srl, "sraDTA",
|
||||
"sllvDTS", false, map_srlv, "sravDTS",
|
||||
"jrS", "jalrD1S", "movzDST", "movnDST",
|
||||
"syscallY", "breakY", false, "sync",
|
||||
"mfhiD", "mthiS", "mfloD", "mtloS",
|
||||
"dsllvDST", false, "dsrlvDST", "dsravDST",
|
||||
"multST", "multuST", "divST", "divuST",
|
||||
"dmultST", "dmultuST", "ddivST", "ddivuST",
|
||||
"addDST", "addu|moveDST0", "subDST", "subu|neguDS0T",
|
||||
"andDST", "or|moveDST0", "xorDST", "nor|notDST0",
|
||||
false, false, "sltDST", "sltuDST",
|
||||
"daddDST", "dadduDST", "dsubDST", "dsubuDST",
|
||||
"tgeSTZ", "tgeuSTZ", "tltSTZ", "tltuSTZ",
|
||||
"teqSTZ", false, "tneSTZ", false,
|
||||
"dsllDTA", false, "dsrlDTA", "dsraDTA",
|
||||
"dsll32DTA", false, "dsrl32DTA", "dsra32DTA",
|
||||
}
|
||||
|
||||
local map_special2 = {
|
||||
shift = 0, mask = 63,
|
||||
[0] = "maddST", "madduST", "mulDST", false,
|
||||
"msubST", "msubuST",
|
||||
[32] = "clzDS", [33] = "cloDS",
|
||||
[63] = "sdbbpY",
|
||||
}
|
||||
|
||||
local map_bshfl = {
|
||||
shift = 6, mask = 31,
|
||||
[2] = "wsbhDT",
|
||||
[16] = "sebDT",
|
||||
[24] = "sehDT",
|
||||
}
|
||||
|
||||
local map_dbshfl = {
|
||||
shift = 6, mask = 31,
|
||||
[2] = "dsbhDT",
|
||||
[5] = "dshdDT",
|
||||
}
|
||||
|
||||
local map_special3 = {
|
||||
shift = 0, mask = 63,
|
||||
[0] = "extTSAK", [1] = "dextmTSAP", [3] = "dextTSAK",
|
||||
[4] = "insTSAL", [6] = "dinsuTSEQ", [7] = "dinsTSAL",
|
||||
[32] = map_bshfl, [36] = map_dbshfl, [59] = "rdhwrTD",
|
||||
}
|
||||
|
||||
local map_regimm = {
|
||||
shift = 16, mask = 31,
|
||||
[0] = "bltzSB", "bgezSB", "bltzlSB", "bgezlSB",
|
||||
false, false, false, false,
|
||||
"tgeiSI", "tgeiuSI", "tltiSI", "tltiuSI",
|
||||
"teqiSI", false, "tneiSI", false,
|
||||
"bltzalSB", "bgezalSB", "bltzallSB", "bgezallSB",
|
||||
false, false, false, false,
|
||||
false, false, false, false,
|
||||
false, false, false, "synciSO",
|
||||
}
|
||||
|
||||
local map_cop1s = {
|
||||
shift = 0, mask = 63,
|
||||
[0] = "add.sFGH", "sub.sFGH", "mul.sFGH", "div.sFGH",
|
||||
"sqrt.sFG", "abs.sFG", "mov.sFG", "neg.sFG",
|
||||
"round.l.sFG", "trunc.l.sFG", "ceil.l.sFG", "floor.l.sFG",
|
||||
"round.w.sFG", "trunc.w.sFG", "ceil.w.sFG", "floor.w.sFG",
|
||||
false,
|
||||
{ shift = 16, mask = 1, [0] = "movf.sFGC", "movt.sFGC" },
|
||||
"movz.sFGT", "movn.sFGT",
|
||||
false, "recip.sFG", "rsqrt.sFG", false,
|
||||
false, false, false, false,
|
||||
false, false, false, false,
|
||||
false, "cvt.d.sFG", false, false,
|
||||
"cvt.w.sFG", "cvt.l.sFG", "cvt.ps.sFGH", false,
|
||||
false, false, false, false,
|
||||
false, false, false, false,
|
||||
"c.f.sVGH", "c.un.sVGH", "c.eq.sVGH", "c.ueq.sVGH",
|
||||
"c.olt.sVGH", "c.ult.sVGH", "c.ole.sVGH", "c.ule.sVGH",
|
||||
"c.sf.sVGH", "c.ngle.sVGH", "c.seq.sVGH", "c.ngl.sVGH",
|
||||
"c.lt.sVGH", "c.nge.sVGH", "c.le.sVGH", "c.ngt.sVGH",
|
||||
}
|
||||
|
||||
local map_cop1d = {
|
||||
shift = 0, mask = 63,
|
||||
[0] = "add.dFGH", "sub.dFGH", "mul.dFGH", "div.dFGH",
|
||||
"sqrt.dFG", "abs.dFG", "mov.dFG", "neg.dFG",
|
||||
"round.l.dFG", "trunc.l.dFG", "ceil.l.dFG", "floor.l.dFG",
|
||||
"round.w.dFG", "trunc.w.dFG", "ceil.w.dFG", "floor.w.dFG",
|
||||
false,
|
||||
{ shift = 16, mask = 1, [0] = "movf.dFGC", "movt.dFGC" },
|
||||
"movz.dFGT", "movn.dFGT",
|
||||
false, "recip.dFG", "rsqrt.dFG", false,
|
||||
false, false, false, false,
|
||||
false, false, false, false,
|
||||
"cvt.s.dFG", false, false, false,
|
||||
"cvt.w.dFG", "cvt.l.dFG", false, false,
|
||||
false, false, false, false,
|
||||
false, false, false, false,
|
||||
"c.f.dVGH", "c.un.dVGH", "c.eq.dVGH", "c.ueq.dVGH",
|
||||
"c.olt.dVGH", "c.ult.dVGH", "c.ole.dVGH", "c.ule.dVGH",
|
||||
"c.df.dVGH", "c.ngle.dVGH", "c.deq.dVGH", "c.ngl.dVGH",
|
||||
"c.lt.dVGH", "c.nge.dVGH", "c.le.dVGH", "c.ngt.dVGH",
|
||||
}
|
||||
|
||||
local map_cop1ps = {
|
||||
shift = 0, mask = 63,
|
||||
[0] = "add.psFGH", "sub.psFGH", "mul.psFGH", false,
|
||||
false, "abs.psFG", "mov.psFG", "neg.psFG",
|
||||
false, false, false, false,
|
||||
false, false, false, false,
|
||||
false,
|
||||
{ shift = 16, mask = 1, [0] = "movf.psFGC", "movt.psFGC" },
|
||||
"movz.psFGT", "movn.psFGT",
|
||||
false, false, false, false,
|
||||
false, false, false, false,
|
||||
false, false, false, false,
|
||||
"cvt.s.puFG", false, false, false,
|
||||
false, false, false, false,
|
||||
"cvt.s.plFG", false, false, false,
|
||||
"pll.psFGH", "plu.psFGH", "pul.psFGH", "puu.psFGH",
|
||||
"c.f.psVGH", "c.un.psVGH", "c.eq.psVGH", "c.ueq.psVGH",
|
||||
"c.olt.psVGH", "c.ult.psVGH", "c.ole.psVGH", "c.ule.psVGH",
|
||||
"c.psf.psVGH", "c.ngle.psVGH", "c.pseq.psVGH", "c.ngl.psVGH",
|
||||
"c.lt.psVGH", "c.nge.psVGH", "c.le.psVGH", "c.ngt.psVGH",
|
||||
}
|
||||
|
||||
local map_cop1w = {
|
||||
shift = 0, mask = 63,
|
||||
[32] = "cvt.s.wFG", [33] = "cvt.d.wFG",
|
||||
}
|
||||
|
||||
local map_cop1l = {
|
||||
shift = 0, mask = 63,
|
||||
[32] = "cvt.s.lFG", [33] = "cvt.d.lFG",
|
||||
}
|
||||
|
||||
local map_cop1bc = {
|
||||
shift = 16, mask = 3,
|
||||
[0] = "bc1fCB", "bc1tCB", "bc1flCB", "bc1tlCB",
|
||||
}
|
||||
|
||||
local map_cop1 = {
|
||||
shift = 21, mask = 31,
|
||||
[0] = "mfc1TG", "dmfc1TG", "cfc1TG", "mfhc1TG",
|
||||
"mtc1TG", "dmtc1TG", "ctc1TG", "mthc1TG",
|
||||
map_cop1bc, false, false, false,
|
||||
false, false, false, false,
|
||||
map_cop1s, map_cop1d, false, false,
|
||||
map_cop1w, map_cop1l, map_cop1ps,
|
||||
}
|
||||
|
||||
local map_cop1x = {
|
||||
shift = 0, mask = 63,
|
||||
[0] = "lwxc1FSX", "ldxc1FSX", false, false,
|
||||
false, "luxc1FSX", false, false,
|
||||
"swxc1FSX", "sdxc1FSX", false, false,
|
||||
false, "suxc1FSX", false, "prefxMSX",
|
||||
false, false, false, false,
|
||||
false, false, false, false,
|
||||
false, false, false, false,
|
||||
false, false, "alnv.psFGHS", false,
|
||||
"madd.sFRGH", "madd.dFRGH", false, false,
|
||||
false, false, "madd.psFRGH", false,
|
||||
"msub.sFRGH", "msub.dFRGH", false, false,
|
||||
false, false, "msub.psFRGH", false,
|
||||
"nmadd.sFRGH", "nmadd.dFRGH", false, false,
|
||||
false, false, "nmadd.psFRGH", false,
|
||||
"nmsub.sFRGH", "nmsub.dFRGH", false, false,
|
||||
false, false, "nmsub.psFRGH", false,
|
||||
}
|
||||
|
||||
local map_pri = {
|
||||
[0] = map_special, map_regimm, "jJ", "jalJ",
|
||||
"beq|beqz|bST00B", "bne|bnezST0B", "blezSB", "bgtzSB",
|
||||
"addiTSI", "addiu|liTS0I", "sltiTSI", "sltiuTSI",
|
||||
"andiTSU", "ori|liTS0U", "xoriTSU", "luiTU",
|
||||
map_cop0, map_cop1, false, map_cop1x,
|
||||
"beql|beqzlST0B", "bnel|bnezlST0B", "blezlSB", "bgtzlSB",
|
||||
"daddiTSI", "daddiuTSI", false, false,
|
||||
map_special2, "jalxJ", false, map_special3,
|
||||
"lbTSO", "lhTSO", "lwlTSO", "lwTSO",
|
||||
"lbuTSO", "lhuTSO", "lwrTSO", false,
|
||||
"sbTSO", "shTSO", "swlTSO", "swTSO",
|
||||
false, false, "swrTSO", "cacheNSO",
|
||||
"llTSO", "lwc1HSO", "lwc2TSO", "prefNSO",
|
||||
false, "ldc1HSO", "ldc2TSO", "ldTSO",
|
||||
"scTSO", "swc1HSO", "swc2TSO", false,
|
||||
false, "sdc1HSO", "sdc2TSO", "sdTSO",
|
||||
}
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Primary and extended opcode maps for MIPS R6
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
local map_mul_r6 = { shift = 6, mask = 3, [2] = "mulDST", [3] = "muhDST" }
|
||||
local map_mulu_r6 = { shift = 6, mask = 3, [2] = "muluDST", [3] = "muhuDST" }
|
||||
local map_div_r6 = { shift = 6, mask = 3, [2] = "divDST", [3] = "modDST" }
|
||||
local map_divu_r6 = { shift = 6, mask = 3, [2] = "divuDST", [3] = "moduDST" }
|
||||
local map_dmul_r6 = { shift = 6, mask = 3, [2] = "dmulDST", [3] = "dmuhDST" }
|
||||
local map_dmulu_r6 = { shift = 6, mask = 3, [2] = "dmuluDST", [3] = "dmuhuDST" }
|
||||
local map_ddiv_r6 = { shift = 6, mask = 3, [2] = "ddivDST", [3] = "dmodDST" }
|
||||
local map_ddivu_r6 = { shift = 6, mask = 3, [2] = "ddivuDST", [3] = "dmoduDST" }
|
||||
|
||||
local map_special_r6 = {
|
||||
shift = 0, mask = 63,
|
||||
[0] = { shift = 0, mask = -1, [0] = "nop", _ = "sllDTA" },
|
||||
false, map_srl, "sraDTA",
|
||||
"sllvDTS", false, map_srlv, "sravDTS",
|
||||
"jrS", "jalrD1S", false, false,
|
||||
"syscallY", "breakY", false, "sync",
|
||||
"clzDS", "cloDS", "dclzDS", "dcloDS",
|
||||
"dsllvDST", "dlsaDSTA", "dsrlvDST", "dsravDST",
|
||||
map_mul_r6, map_mulu_r6, map_div_r6, map_divu_r6,
|
||||
map_dmul_r6, map_dmulu_r6, map_ddiv_r6, map_ddivu_r6,
|
||||
"addDST", "addu|moveDST0", "subDST", "subu|neguDS0T",
|
||||
"andDST", "or|moveDST0", "xorDST", "nor|notDST0",
|
||||
false, false, "sltDST", "sltuDST",
|
||||
"daddDST", "dadduDST", "dsubDST", "dsubuDST",
|
||||
"tgeSTZ", "tgeuSTZ", "tltSTZ", "tltuSTZ",
|
||||
"teqSTZ", "seleqzDST", "tneSTZ", "selnezDST",
|
||||
"dsllDTA", false, "dsrlDTA", "dsraDTA",
|
||||
"dsll32DTA", false, "dsrl32DTA", "dsra32DTA",
|
||||
}
|
||||
|
||||
local map_bshfl_r6 = {
|
||||
shift = 9, mask = 3,
|
||||
[1] = "alignDSTa",
|
||||
_ = {
|
||||
shift = 6, mask = 31,
|
||||
[0] = "bitswapDT",
|
||||
[2] = "wsbhDT",
|
||||
[16] = "sebDT",
|
||||
[24] = "sehDT",
|
||||
}
|
||||
}
|
||||
|
||||
local map_dbshfl_r6 = {
|
||||
shift = 9, mask = 3,
|
||||
[1] = "dalignDSTa",
|
||||
_ = {
|
||||
shift = 6, mask = 31,
|
||||
[0] = "dbitswapDT",
|
||||
[2] = "dsbhDT",
|
||||
[5] = "dshdDT",
|
||||
}
|
||||
}
|
||||
|
||||
local map_special3_r6 = {
|
||||
shift = 0, mask = 63,
|
||||
[0] = "extTSAK", [1] = "dextmTSAP", [3] = "dextTSAK",
|
||||
[4] = "insTSAL", [6] = "dinsuTSEQ", [7] = "dinsTSAL",
|
||||
[32] = map_bshfl_r6, [36] = map_dbshfl_r6, [59] = "rdhwrTD",
|
||||
}
|
||||
|
||||
local map_regimm_r6 = {
|
||||
shift = 16, mask = 31,
|
||||
[0] = "bltzSB", [1] = "bgezSB",
|
||||
[6] = "dahiSI", [30] = "datiSI",
|
||||
[23] = "sigrieI", [31] = "synciSO",
|
||||
}
|
||||
|
||||
local map_pcrel_r6 = {
|
||||
shift = 19, mask = 3,
|
||||
[0] = "addiupcS2", "lwpcS2", "lwupcS2", {
|
||||
shift = 18, mask = 1,
|
||||
[0] = "ldpcS3", { shift = 16, mask = 3, [2] = "auipcSI", [3] = "aluipcSI" }
|
||||
}
|
||||
}
|
||||
|
||||
local map_cop1s_r6 = {
|
||||
shift = 0, mask = 63,
|
||||
[0] = "add.sFGH", "sub.sFGH", "mul.sFGH", "div.sFGH",
|
||||
"sqrt.sFG", "abs.sFG", "mov.sFG", "neg.sFG",
|
||||
"round.l.sFG", "trunc.l.sFG", "ceil.l.sFG", "floor.l.sFG",
|
||||
"round.w.sFG", "trunc.w.sFG", "ceil.w.sFG", "floor.w.sFG",
|
||||
"sel.sFGH", false, false, false,
|
||||
"seleqz.sFGH", "recip.sFG", "rsqrt.sFG", "selnez.sFGH",
|
||||
"maddf.sFGH", "msubf.sFGH", "rint.sFG", "class.sFG",
|
||||
"min.sFGH", "mina.sFGH", "max.sFGH", "maxa.sFGH",
|
||||
false, "cvt.d.sFG", false, false,
|
||||
"cvt.w.sFG", "cvt.l.sFG",
|
||||
}
|
||||
|
||||
local map_cop1d_r6 = {
|
||||
shift = 0, mask = 63,
|
||||
[0] = "add.dFGH", "sub.dFGH", "mul.dFGH", "div.dFGH",
|
||||
"sqrt.dFG", "abs.dFG", "mov.dFG", "neg.dFG",
|
||||
"round.l.dFG", "trunc.l.dFG", "ceil.l.dFG", "floor.l.dFG",
|
||||
"round.w.dFG", "trunc.w.dFG", "ceil.w.dFG", "floor.w.dFG",
|
||||
"sel.dFGH", false, false, false,
|
||||
"seleqz.dFGH", "recip.dFG", "rsqrt.dFG", "selnez.dFGH",
|
||||
"maddf.dFGH", "msubf.dFGH", "rint.dFG", "class.dFG",
|
||||
"min.dFGH", "mina.dFGH", "max.dFGH", "maxa.dFGH",
|
||||
"cvt.s.dFG", false, false, false,
|
||||
"cvt.w.dFG", "cvt.l.dFG",
|
||||
}
|
||||
|
||||
local map_cop1w_r6 = {
|
||||
shift = 0, mask = 63,
|
||||
[0] = "cmp.af.sFGH", "cmp.un.sFGH", "cmp.eq.sFGH", "cmp.ueq.sFGH",
|
||||
"cmp.lt.sFGH", "cmp.ult.sFGH", "cmp.le.sFGH", "cmp.ule.sFGH",
|
||||
"cmp.saf.sFGH", "cmp.sun.sFGH", "cmp.seq.sFGH", "cmp.sueq.sFGH",
|
||||
"cmp.slt.sFGH", "cmp.sult.sFGH", "cmp.sle.sFGH", "cmp.sule.sFGH",
|
||||
false, "cmp.or.sFGH", "cmp.une.sFGH", "cmp.ne.sFGH",
|
||||
false, false, false, false,
|
||||
false, "cmp.sor.sFGH", "cmp.sune.sFGH", "cmp.sne.sFGH",
|
||||
false, false, false, false,
|
||||
"cvt.s.wFG", "cvt.d.wFG",
|
||||
}
|
||||
|
||||
local map_cop1l_r6 = {
|
||||
shift = 0, mask = 63,
|
||||
[0] = "cmp.af.dFGH", "cmp.un.dFGH", "cmp.eq.dFGH", "cmp.ueq.dFGH",
|
||||
"cmp.lt.dFGH", "cmp.ult.dFGH", "cmp.le.dFGH", "cmp.ule.dFGH",
|
||||
"cmp.saf.dFGH", "cmp.sun.dFGH", "cmp.seq.dFGH", "cmp.sueq.dFGH",
|
||||
"cmp.slt.dFGH", "cmp.sult.dFGH", "cmp.sle.dFGH", "cmp.sule.dFGH",
|
||||
false, "cmp.or.dFGH", "cmp.une.dFGH", "cmp.ne.dFGH",
|
||||
false, false, false, false,
|
||||
false, "cmp.sor.dFGH", "cmp.sune.dFGH", "cmp.sne.dFGH",
|
||||
false, false, false, false,
|
||||
"cvt.s.lFG", "cvt.d.lFG",
|
||||
}
|
||||
|
||||
local map_cop1_r6 = {
|
||||
shift = 21, mask = 31,
|
||||
[0] = "mfc1TG", "dmfc1TG", "cfc1TG", "mfhc1TG",
|
||||
"mtc1TG", "dmtc1TG", "ctc1TG", "mthc1TG",
|
||||
false, "bc1eqzHB", false, false,
|
||||
false, "bc1nezHB", false, false,
|
||||
map_cop1s_r6, map_cop1d_r6, false, false,
|
||||
map_cop1w_r6, map_cop1l_r6,
|
||||
}
|
||||
|
||||
local function maprs_popTS(rs, rt)
|
||||
if rt == 0 then return 0 elseif rs == 0 then return 1
|
||||
elseif rs == rt then return 2 else return 3 end
|
||||
end
|
||||
|
||||
local map_pop06_r6 = {
|
||||
maprs = maprs_popTS, [0] = "blezSB", "blezalcTB", "bgezalcTB", "bgeucSTB"
|
||||
}
|
||||
local map_pop07_r6 = {
|
||||
maprs = maprs_popTS, [0] = "bgtzSB", "bgtzalcTB", "bltzalcTB", "bltucSTB"
|
||||
}
|
||||
local map_pop26_r6 = {
|
||||
maprs = maprs_popTS, "blezcTB", "bgezcTB", "bgecSTB"
|
||||
}
|
||||
local map_pop27_r6 = {
|
||||
maprs = maprs_popTS, "bgtzcTB", "bltzcTB", "bltcSTB"
|
||||
}
|
||||
|
||||
local function maprs_popS(rs, rt)
|
||||
if rs == 0 then return 0 else return 1 end
|
||||
end
|
||||
|
||||
local map_pop66_r6 = {
|
||||
maprs = maprs_popS, [0] = "jicTI", "beqzcSb"
|
||||
}
|
||||
local map_pop76_r6 = {
|
||||
maprs = maprs_popS, [0] = "jialcTI", "bnezcSb"
|
||||
}
|
||||
|
||||
local function maprs_popST(rs, rt)
|
||||
if rs >= rt then return 0 elseif rs == 0 then return 1 else return 2 end
|
||||
end
|
||||
|
||||
local map_pop10_r6 = {
|
||||
maprs = maprs_popST, [0] = "bovcSTB", "beqzalcTB", "beqcSTB"
|
||||
}
|
||||
local map_pop30_r6 = {
|
||||
maprs = maprs_popST, [0] = "bnvcSTB", "bnezalcTB", "bnecSTB"
|
||||
}
|
||||
|
||||
local map_pri_r6 = {
|
||||
[0] = map_special_r6, map_regimm_r6, "jJ", "jalJ",
|
||||
"beq|beqz|bST00B", "bne|bnezST0B", map_pop06_r6, map_pop07_r6,
|
||||
map_pop10_r6, "addiu|liTS0I", "sltiTSI", "sltiuTSI",
|
||||
"andiTSU", "ori|liTS0U", "xoriTSU", "aui|luiTS0U",
|
||||
map_cop0, map_cop1_r6, false, false,
|
||||
false, false, map_pop26_r6, map_pop27_r6,
|
||||
map_pop30_r6, "daddiuTSI", false, false,
|
||||
false, "dauiTSI", false, map_special3_r6,
|
||||
"lbTSO", "lhTSO", false, "lwTSO",
|
||||
"lbuTSO", "lhuTSO", false, false,
|
||||
"sbTSO", "shTSO", false, "swTSO",
|
||||
false, false, false, false,
|
||||
false, "lwc1HSO", "bc#", false,
|
||||
false, "ldc1HSO", map_pop66_r6, "ldTSO",
|
||||
false, "swc1HSO", "balc#", map_pcrel_r6,
|
||||
false, "sdc1HSO", map_pop76_r6, "sdTSO",
|
||||
}
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
local map_gpr = {
|
||||
[0] = "r0", "r1", "r2", "r3", "r4", "r5", "r6", "r7",
|
||||
"r8", "r9", "r10", "r11", "r12", "r13", "r14", "r15",
|
||||
"r16", "r17", "r18", "r19", "r20", "r21", "r22", "r23",
|
||||
"r24", "r25", "r26", "r27", "r28", "sp", "r30", "ra",
|
||||
}
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
-- Output a nicely formatted line with an opcode and operands.
|
||||
local function putop(ctx, text, operands)
|
||||
local pos = ctx.pos
|
||||
local extra = ""
|
||||
if ctx.rel then
|
||||
local sym = ctx.symtab[ctx.rel]
|
||||
if sym then extra = "\t->"..sym end
|
||||
end
|
||||
if ctx.hexdump > 0 then
|
||||
ctx.out(format("%08x %s %-7s %s%s\n",
|
||||
ctx.addr+pos, tohex(ctx.op), text, concat(operands, ", "), extra))
|
||||
else
|
||||
ctx.out(format("%08x %-7s %s%s\n",
|
||||
ctx.addr+pos, text, concat(operands, ", "), extra))
|
||||
end
|
||||
ctx.pos = pos + 4
|
||||
end
|
||||
|
||||
-- Fallback for unknown opcodes.
|
||||
local function unknown(ctx)
|
||||
return putop(ctx, ".long", { "0x"..tohex(ctx.op) })
|
||||
end
|
||||
|
||||
local function get_be(ctx)
|
||||
local pos = ctx.pos
|
||||
local b0, b1, b2, b3 = byte(ctx.code, pos+1, pos+4)
|
||||
return bor(lshift(b0, 24), lshift(b1, 16), lshift(b2, 8), b3)
|
||||
end
|
||||
|
||||
local function get_le(ctx)
|
||||
local pos = ctx.pos
|
||||
local b0, b1, b2, b3 = byte(ctx.code, pos+1, pos+4)
|
||||
return bor(lshift(b3, 24), lshift(b2, 16), lshift(b1, 8), b0)
|
||||
end
|
||||
|
||||
-- Disassemble a single instruction.
|
||||
local function disass_ins(ctx)
|
||||
local op = ctx:get()
|
||||
local operands = {}
|
||||
local last = nil
|
||||
ctx.op = op
|
||||
ctx.rel = nil
|
||||
|
||||
local opat = ctx.map_pri[rshift(op, 26)]
|
||||
while type(opat) ~= "string" do
|
||||
if not opat then return unknown(ctx) end
|
||||
if opat.maprs then
|
||||
opat = opat[opat.maprs(band(rshift(op,21),31), band(rshift(op,16),31))]
|
||||
else
|
||||
opat = opat[band(rshift(op, opat.shift), opat.mask)] or opat._
|
||||
end
|
||||
end
|
||||
local name, pat = match(opat, "^([a-z0-9_.]*)(.*)")
|
||||
local altname, pat2 = match(pat, "|([a-z0-9_.|]*)(.*)")
|
||||
if altname then pat = pat2 end
|
||||
|
||||
for p in gmatch(pat, ".") do
|
||||
local x = nil
|
||||
if p == "S" then
|
||||
x = map_gpr[band(rshift(op, 21), 31)]
|
||||
elseif p == "T" then
|
||||
x = map_gpr[band(rshift(op, 16), 31)]
|
||||
elseif p == "D" then
|
||||
x = map_gpr[band(rshift(op, 11), 31)]
|
||||
elseif p == "F" then
|
||||
x = "f"..band(rshift(op, 6), 31)
|
||||
elseif p == "G" then
|
||||
x = "f"..band(rshift(op, 11), 31)
|
||||
elseif p == "H" then
|
||||
x = "f"..band(rshift(op, 16), 31)
|
||||
elseif p == "R" then
|
||||
x = "f"..band(rshift(op, 21), 31)
|
||||
elseif p == "A" then
|
||||
x = band(rshift(op, 6), 31)
|
||||
elseif p == "a" then
|
||||
x = band(rshift(op, 6), 7)
|
||||
elseif p == "E" then
|
||||
x = band(rshift(op, 6), 31) + 32
|
||||
elseif p == "M" then
|
||||
x = band(rshift(op, 11), 31)
|
||||
elseif p == "N" then
|
||||
x = band(rshift(op, 16), 31)
|
||||
elseif p == "C" then
|
||||
x = band(rshift(op, 18), 7)
|
||||
if x == 0 then x = nil end
|
||||
elseif p == "K" then
|
||||
x = band(rshift(op, 11), 31) + 1
|
||||
elseif p == "P" then
|
||||
x = band(rshift(op, 11), 31) + 33
|
||||
elseif p == "L" then
|
||||
x = band(rshift(op, 11), 31) - last + 1
|
||||
elseif p == "Q" then
|
||||
x = band(rshift(op, 11), 31) - last + 33
|
||||
elseif p == "I" then
|
||||
x = arshift(lshift(op, 16), 16)
|
||||
elseif p == "2" then
|
||||
x = arshift(lshift(op, 13), 11)
|
||||
elseif p == "3" then
|
||||
x = arshift(lshift(op, 14), 11)
|
||||
elseif p == "U" then
|
||||
x = band(op, 0xffff)
|
||||
elseif p == "O" then
|
||||
local disp = arshift(lshift(op, 16), 16)
|
||||
operands[#operands] = format("%d(%s)", disp, last)
|
||||
elseif p == "X" then
|
||||
local index = map_gpr[band(rshift(op, 16), 31)]
|
||||
operands[#operands] = format("%s(%s)", index, last)
|
||||
elseif p == "B" then
|
||||
x = ctx.addr + ctx.pos + arshift(lshift(op, 16), 14) + 4
|
||||
ctx.rel = x
|
||||
x = format("0x%08x", x)
|
||||
elseif p == "b" then
|
||||
x = ctx.addr + ctx.pos + arshift(lshift(op, 11), 9) + 4
|
||||
ctx.rel = x
|
||||
x = format("0x%08x", x)
|
||||
elseif p == "#" then
|
||||
x = ctx.addr + ctx.pos + arshift(lshift(op, 6), 4) + 4
|
||||
ctx.rel = x
|
||||
x = format("0x%08x", x)
|
||||
elseif p == "J" then
|
||||
local a = ctx.addr + ctx.pos
|
||||
x = a - band(a, 0x0fffffff) + band(op, 0x03ffffff)*4
|
||||
ctx.rel = x
|
||||
x = format("0x%08x", x)
|
||||
elseif p == "V" then
|
||||
x = band(rshift(op, 8), 7)
|
||||
if x == 0 then x = nil end
|
||||
elseif p == "W" then
|
||||
x = band(op, 7)
|
||||
if x == 0 then x = nil end
|
||||
elseif p == "Y" then
|
||||
x = band(rshift(op, 6), 0x000fffff)
|
||||
if x == 0 then x = nil end
|
||||
elseif p == "Z" then
|
||||
x = band(rshift(op, 6), 1023)
|
||||
if x == 0 then x = nil end
|
||||
elseif p == "0" then
|
||||
if last == "r0" or last == 0 then
|
||||
local n = #operands
|
||||
operands[n] = nil
|
||||
last = operands[n-1]
|
||||
if altname then
|
||||
local a1, a2 = match(altname, "([^|]*)|(.*)")
|
||||
if a1 then name, altname = a1, a2
|
||||
else name = altname end
|
||||
end
|
||||
end
|
||||
elseif p == "1" then
|
||||
if last == "ra" then
|
||||
operands[#operands] = nil
|
||||
end
|
||||
else
|
||||
assert(false)
|
||||
end
|
||||
if x then operands[#operands+1] = x; last = x end
|
||||
end
|
||||
|
||||
return putop(ctx, name, operands)
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
-- Disassemble a block of code.
|
||||
local function disass_block(ctx, ofs, len)
|
||||
if not ofs then ofs = 0 end
|
||||
local stop = len and ofs+len or #ctx.code
|
||||
stop = stop - stop % 4
|
||||
ctx.pos = ofs - ofs % 4
|
||||
ctx.rel = nil
|
||||
while ctx.pos < stop do disass_ins(ctx) end
|
||||
end
|
||||
|
||||
-- Extended API: create a disassembler context. Then call ctx:disass(ofs, len).
|
||||
local function create(code, addr, out)
|
||||
local ctx = {}
|
||||
ctx.code = code
|
||||
ctx.addr = addr or 0
|
||||
ctx.out = out or io.write
|
||||
ctx.symtab = {}
|
||||
ctx.disass = disass_block
|
||||
ctx.hexdump = 8
|
||||
ctx.get = get_be
|
||||
ctx.map_pri = map_pri
|
||||
return ctx
|
||||
end
|
||||
|
||||
local function create_el(code, addr, out)
|
||||
local ctx = create(code, addr, out)
|
||||
ctx.get = get_le
|
||||
return ctx
|
||||
end
|
||||
|
||||
local function create_r6(code, addr, out)
|
||||
local ctx = create(code, addr, out)
|
||||
ctx.map_pri = map_pri_r6
|
||||
return ctx
|
||||
end
|
||||
|
||||
local function create_r6_el(code, addr, out)
|
||||
local ctx = create(code, addr, out)
|
||||
ctx.get = get_le
|
||||
ctx.map_pri = map_pri_r6
|
||||
return ctx
|
||||
end
|
||||
|
||||
-- Simple API: disassemble code (a string) at address and output via out.
|
||||
local function disass(code, addr, out)
|
||||
create(code, addr, out):disass()
|
||||
end
|
||||
|
||||
local function disass_el(code, addr, out)
|
||||
create_el(code, addr, out):disass()
|
||||
end
|
||||
|
||||
local function disass_r6(code, addr, out)
|
||||
create_r6(code, addr, out):disass()
|
||||
end
|
||||
|
||||
local function disass_r6_el(code, addr, out)
|
||||
create_r6_el(code, addr, out):disass()
|
||||
end
|
||||
|
||||
-- Return register name for RID.
|
||||
local function regname(r)
|
||||
if r < 32 then return map_gpr[r] end
|
||||
return "f"..(r-32)
|
||||
end
|
||||
|
||||
-- Public module functions.
|
||||
return {
|
||||
create = create,
|
||||
create_el = create_el,
|
||||
create_r6 = create_r6,
|
||||
create_r6_el = create_r6_el,
|
||||
disass = disass,
|
||||
disass_el = disass_el,
|
||||
disass_r6 = disass_r6,
|
||||
disass_r6_el = disass_r6_el,
|
||||
regname = regname
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
----------------------------------------------------------------------------
|
||||
-- LuaJIT MIPS64 disassembler wrapper module.
|
||||
--
|
||||
-- Copyright (C) 2005-2023 Mike Pall. All rights reserved.
|
||||
-- Released under the MIT license. See Copyright Notice in luajit.h
|
||||
----------------------------------------------------------------------------
|
||||
-- This module just exports the big-endian functions from the
|
||||
-- MIPS disassembler module. All the interesting stuff is there.
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
local dis_mips = require((string.match(..., ".*%.") or "").."dis_mips")
|
||||
return {
|
||||
create = dis_mips.create,
|
||||
disass = dis_mips.disass,
|
||||
regname = dis_mips.regname
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
----------------------------------------------------------------------------
|
||||
-- LuaJIT MIPS64EL disassembler wrapper module.
|
||||
--
|
||||
-- Copyright (C) 2005-2023 Mike Pall. All rights reserved.
|
||||
-- Released under the MIT license. See Copyright Notice in luajit.h
|
||||
----------------------------------------------------------------------------
|
||||
-- This module just exports the little-endian functions from the
|
||||
-- MIPS disassembler module. All the interesting stuff is there.
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
local dis_mips = require((string.match(..., ".*%.") or "").."dis_mips")
|
||||
return {
|
||||
create = dis_mips.create_el,
|
||||
disass = dis_mips.disass_el,
|
||||
regname = dis_mips.regname
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
----------------------------------------------------------------------------
|
||||
-- LuaJIT MIPS64R6 disassembler wrapper module.
|
||||
--
|
||||
-- Copyright (C) 2005-2023 Mike Pall. All rights reserved.
|
||||
-- Released under the MIT license. See Copyright Notice in luajit.h
|
||||
----------------------------------------------------------------------------
|
||||
-- This module just exports the r6 big-endian functions from the
|
||||
-- MIPS disassembler module. All the interesting stuff is there.
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
local dis_mips = require((string.match(..., ".*%.") or "").."dis_mips")
|
||||
return {
|
||||
create = dis_mips.create_r6,
|
||||
disass = dis_mips.disass_r6,
|
||||
regname = dis_mips.regname
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
----------------------------------------------------------------------------
|
||||
-- LuaJIT MIPS64R6EL disassembler wrapper module.
|
||||
--
|
||||
-- Copyright (C) 2005-2023 Mike Pall. All rights reserved.
|
||||
-- Released under the MIT license. See Copyright Notice in luajit.h
|
||||
----------------------------------------------------------------------------
|
||||
-- This module just exports the r6 little-endian functions from the
|
||||
-- MIPS disassembler module. All the interesting stuff is there.
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
local dis_mips = require((string.match(..., ".*%.") or "").."dis_mips")
|
||||
return {
|
||||
create = dis_mips.create_r6_el,
|
||||
disass = dis_mips.disass_r6_el,
|
||||
regname = dis_mips.regname
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
----------------------------------------------------------------------------
|
||||
-- LuaJIT MIPSEL disassembler wrapper module.
|
||||
--
|
||||
-- Copyright (C) 2005-2023 Mike Pall. All rights reserved.
|
||||
-- Released under the MIT license. See Copyright Notice in luajit.h
|
||||
----------------------------------------------------------------------------
|
||||
-- This module just exports the little-endian functions from the
|
||||
-- MIPS disassembler module. All the interesting stuff is there.
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
local dis_mips = require((string.match(..., ".*%.") or "").."dis_mips")
|
||||
return {
|
||||
create = dis_mips.create_el,
|
||||
disass = dis_mips.disass_el,
|
||||
regname = dis_mips.regname
|
||||
}
|
||||
|
||||
@@ -0,0 +1,591 @@
|
||||
----------------------------------------------------------------------------
|
||||
-- LuaJIT PPC disassembler module.
|
||||
--
|
||||
-- Copyright (C) 2005-2023 Mike Pall. All rights reserved.
|
||||
-- Released under the MIT/X license. See Copyright Notice in luajit.h
|
||||
----------------------------------------------------------------------------
|
||||
-- This is a helper module used by the LuaJIT machine code dumper module.
|
||||
--
|
||||
-- It disassembles all common, non-privileged 32/64 bit PowerPC instructions
|
||||
-- plus the e500 SPE instructions and some Cell/Xenon extensions.
|
||||
--
|
||||
-- NYI: VMX, VMX128
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
local type = type
|
||||
local byte, format = string.byte, string.format
|
||||
local match, gmatch, gsub = string.match, string.gmatch, string.gsub
|
||||
local concat = table.concat
|
||||
local bit = require("bit")
|
||||
local band, bor, tohex = bit.band, bit.bor, bit.tohex
|
||||
local lshift, rshift, arshift = bit.lshift, bit.rshift, bit.arshift
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Primary and extended opcode maps
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
local map_crops = {
|
||||
shift = 1, mask = 1023,
|
||||
[0] = "mcrfXX",
|
||||
[33] = "crnor|crnotCCC=", [129] = "crandcCCC",
|
||||
[193] = "crxor|crclrCCC%", [225] = "crnandCCC",
|
||||
[257] = "crandCCC", [289] = "creqv|crsetCCC%",
|
||||
[417] = "crorcCCC", [449] = "cror|crmoveCCC=",
|
||||
[16] = "b_lrKB", [528] = "b_ctrKB",
|
||||
[150] = "isync",
|
||||
}
|
||||
|
||||
local map_rlwinm = setmetatable({
|
||||
shift = 0, mask = -1,
|
||||
},
|
||||
{ __index = function(t, x)
|
||||
local rot = band(rshift(x, 11), 31)
|
||||
local mb = band(rshift(x, 6), 31)
|
||||
local me = band(rshift(x, 1), 31)
|
||||
if mb == 0 and me == 31-rot then
|
||||
return "slwiRR~A."
|
||||
elseif me == 31 and mb == 32-rot then
|
||||
return "srwiRR~-A."
|
||||
else
|
||||
return "rlwinmRR~AAA."
|
||||
end
|
||||
end
|
||||
})
|
||||
|
||||
local map_rld = {
|
||||
shift = 2, mask = 7,
|
||||
[0] = "rldiclRR~HM.", "rldicrRR~HM.", "rldicRR~HM.", "rldimiRR~HM.",
|
||||
{
|
||||
shift = 1, mask = 1,
|
||||
[0] = "rldclRR~RM.", "rldcrRR~RM.",
|
||||
},
|
||||
}
|
||||
|
||||
local map_ext = setmetatable({
|
||||
shift = 1, mask = 1023,
|
||||
|
||||
[0] = "cmp_YLRR", [32] = "cmpl_YLRR",
|
||||
[4] = "twARR", [68] = "tdARR",
|
||||
|
||||
[8] = "subfcRRR.", [40] = "subfRRR.",
|
||||
[104] = "negRR.", [136] = "subfeRRR.",
|
||||
[200] = "subfzeRR.", [232] = "subfmeRR.",
|
||||
[520] = "subfcoRRR.", [552] = "subfoRRR.",
|
||||
[616] = "negoRR.", [648] = "subfeoRRR.",
|
||||
[712] = "subfzeoRR.", [744] = "subfmeoRR.",
|
||||
|
||||
[9] = "mulhduRRR.", [73] = "mulhdRRR.", [233] = "mulldRRR.",
|
||||
[457] = "divduRRR.", [489] = "divdRRR.",
|
||||
[745] = "mulldoRRR.",
|
||||
[969] = "divduoRRR.", [1001] = "divdoRRR.",
|
||||
|
||||
[10] = "addcRRR.", [138] = "addeRRR.",
|
||||
[202] = "addzeRR.", [234] = "addmeRR.", [266] = "addRRR.",
|
||||
[522] = "addcoRRR.", [650] = "addeoRRR.",
|
||||
[714] = "addzeoRR.", [746] = "addmeoRR.", [778] = "addoRRR.",
|
||||
|
||||
[11] = "mulhwuRRR.", [75] = "mulhwRRR.", [235] = "mullwRRR.",
|
||||
[459] = "divwuRRR.", [491] = "divwRRR.",
|
||||
[747] = "mullwoRRR.",
|
||||
[971] = "divwouRRR.", [1003] = "divwoRRR.",
|
||||
|
||||
[15] = "iselltRRR", [47] = "iselgtRRR", [79] = "iseleqRRR",
|
||||
|
||||
[144] = { shift = 20, mask = 1, [0] = "mtcrfRZ~", "mtocrfRZ~", },
|
||||
[19] = { shift = 20, mask = 1, [0] = "mfcrR", "mfocrfRZ", },
|
||||
[371] = { shift = 11, mask = 1023, [392] = "mftbR", [424] = "mftbuR", },
|
||||
[339] = {
|
||||
shift = 11, mask = 1023,
|
||||
[32] = "mferR", [256] = "mflrR", [288] = "mfctrR", [16] = "mfspefscrR",
|
||||
},
|
||||
[467] = {
|
||||
shift = 11, mask = 1023,
|
||||
[32] = "mtxerR", [256] = "mtlrR", [288] = "mtctrR", [16] = "mtspefscrR",
|
||||
},
|
||||
|
||||
[20] = "lwarxRR0R", [84] = "ldarxRR0R",
|
||||
|
||||
[21] = "ldxRR0R", [53] = "lduxRRR",
|
||||
[149] = "stdxRR0R", [181] = "stduxRRR",
|
||||
[341] = "lwaxRR0R", [373] = "lwauxRRR",
|
||||
|
||||
[23] = "lwzxRR0R", [55] = "lwzuxRRR",
|
||||
[87] = "lbzxRR0R", [119] = "lbzuxRRR",
|
||||
[151] = "stwxRR0R", [183] = "stwuxRRR",
|
||||
[215] = "stbxRR0R", [247] = "stbuxRRR",
|
||||
[279] = "lhzxRR0R", [311] = "lhzuxRRR",
|
||||
[343] = "lhaxRR0R", [375] = "lhauxRRR",
|
||||
[407] = "sthxRR0R", [439] = "sthuxRRR",
|
||||
|
||||
[54] = "dcbst-R0R", [86] = "dcbf-R0R",
|
||||
[150] = "stwcxRR0R.", [214] = "stdcxRR0R.",
|
||||
[246] = "dcbtst-R0R", [278] = "dcbt-R0R",
|
||||
[310] = "eciwxRR0R", [438] = "ecowxRR0R",
|
||||
[470] = "dcbi-RR",
|
||||
|
||||
[598] = {
|
||||
shift = 21, mask = 3,
|
||||
[0] = "sync", "lwsync", "ptesync",
|
||||
},
|
||||
[758] = "dcba-RR",
|
||||
[854] = "eieio", [982] = "icbi-R0R", [1014] = "dcbz-R0R",
|
||||
|
||||
[26] = "cntlzwRR~", [58] = "cntlzdRR~",
|
||||
[122] = "popcntbRR~",
|
||||
[154] = "prtywRR~", [186] = "prtydRR~",
|
||||
|
||||
[28] = "andRR~R.", [60] = "andcRR~R.", [124] = "nor|notRR~R=.",
|
||||
[284] = "eqvRR~R.", [316] = "xorRR~R.",
|
||||
[412] = "orcRR~R.", [444] = "or|mrRR~R=.", [476] = "nandRR~R.",
|
||||
[508] = "cmpbRR~R",
|
||||
|
||||
[512] = "mcrxrX",
|
||||
|
||||
[532] = "ldbrxRR0R", [660] = "stdbrxRR0R",
|
||||
|
||||
[533] = "lswxRR0R", [597] = "lswiRR0A",
|
||||
[661] = "stswxRR0R", [725] = "stswiRR0A",
|
||||
|
||||
[534] = "lwbrxRR0R", [662] = "stwbrxRR0R",
|
||||
[790] = "lhbrxRR0R", [918] = "sthbrxRR0R",
|
||||
|
||||
[535] = "lfsxFR0R", [567] = "lfsuxFRR",
|
||||
[599] = "lfdxFR0R", [631] = "lfduxFRR",
|
||||
[663] = "stfsxFR0R", [695] = "stfsuxFRR",
|
||||
[727] = "stfdxFR0R", [759] = "stfduxFR0R",
|
||||
[855] = "lfiwaxFR0R",
|
||||
[983] = "stfiwxFR0R",
|
||||
|
||||
[24] = "slwRR~R.",
|
||||
|
||||
[27] = "sldRR~R.", [536] = "srwRR~R.",
|
||||
[792] = "srawRR~R.", [824] = "srawiRR~A.",
|
||||
|
||||
[794] = "sradRR~R.", [826] = "sradiRR~H.", [827] = "sradiRR~H.",
|
||||
[922] = "extshRR~.", [954] = "extsbRR~.", [986] = "extswRR~.",
|
||||
|
||||
[539] = "srdRR~R.",
|
||||
},
|
||||
{ __index = function(t, x)
|
||||
if band(x, 31) == 15 then return "iselRRRC" end
|
||||
end
|
||||
})
|
||||
|
||||
local map_ld = {
|
||||
shift = 0, mask = 3,
|
||||
[0] = "ldRRE", "lduRRE", "lwaRRE",
|
||||
}
|
||||
|
||||
local map_std = {
|
||||
shift = 0, mask = 3,
|
||||
[0] = "stdRRE", "stduRRE",
|
||||
}
|
||||
|
||||
local map_fps = {
|
||||
shift = 5, mask = 1,
|
||||
{
|
||||
shift = 1, mask = 15,
|
||||
[0] = false, false, "fdivsFFF.", false,
|
||||
"fsubsFFF.", "faddsFFF.", "fsqrtsF-F.", false,
|
||||
"fresF-F.", "fmulsFF-F.", "frsqrtesF-F.", false,
|
||||
"fmsubsFFFF~.", "fmaddsFFFF~.", "fnmsubsFFFF~.", "fnmaddsFFFF~.",
|
||||
}
|
||||
}
|
||||
|
||||
local map_fpd = {
|
||||
shift = 5, mask = 1,
|
||||
[0] = {
|
||||
shift = 1, mask = 1023,
|
||||
[0] = "fcmpuXFF", [32] = "fcmpoXFF", [64] = "mcrfsXX",
|
||||
[38] = "mtfsb1A.", [70] = "mtfsb0A.", [134] = "mtfsfiA>>-A>",
|
||||
[8] = "fcpsgnFFF.", [40] = "fnegF-F.", [72] = "fmrF-F.",
|
||||
[136] = "fnabsF-F.", [264] = "fabsF-F.",
|
||||
[12] = "frspF-F.",
|
||||
[14] = "fctiwF-F.", [15] = "fctiwzF-F.",
|
||||
[583] = "mffsF.", [711] = "mtfsfZF.",
|
||||
[392] = "frinF-F.", [424] = "frizF-F.",
|
||||
[456] = "fripF-F.", [488] = "frimF-F.",
|
||||
[814] = "fctidF-F.", [815] = "fctidzF-F.", [846] = "fcfidF-F.",
|
||||
},
|
||||
{
|
||||
shift = 1, mask = 15,
|
||||
[0] = false, false, "fdivFFF.", false,
|
||||
"fsubFFF.", "faddFFF.", "fsqrtF-F.", "fselFFFF~.",
|
||||
"freF-F.", "fmulFF-F.", "frsqrteF-F.", false,
|
||||
"fmsubFFFF~.", "fmaddFFFF~.", "fnmsubFFFF~.", "fnmaddFFFF~.",
|
||||
}
|
||||
}
|
||||
|
||||
local map_spe = {
|
||||
shift = 0, mask = 2047,
|
||||
|
||||
[512] = "evaddwRRR", [514] = "evaddiwRAR~",
|
||||
[516] = "evsubwRRR~", [518] = "evsubiwRAR~",
|
||||
[520] = "evabsRR", [521] = "evnegRR",
|
||||
[522] = "evextsbRR", [523] = "evextshRR", [524] = "evrndwRR",
|
||||
[525] = "evcntlzwRR", [526] = "evcntlswRR",
|
||||
|
||||
[527] = "brincRRR",
|
||||
|
||||
[529] = "evandRRR", [530] = "evandcRRR", [534] = "evxorRRR",
|
||||
[535] = "evor|evmrRRR=", [536] = "evnor|evnotRRR=",
|
||||
[537] = "eveqvRRR", [539] = "evorcRRR", [542] = "evnandRRR",
|
||||
|
||||
[544] = "evsrwuRRR", [545] = "evsrwsRRR",
|
||||
[546] = "evsrwiuRRA", [547] = "evsrwisRRA",
|
||||
[548] = "evslwRRR", [550] = "evslwiRRA",
|
||||
[552] = "evrlwRRR", [553] = "evsplatiRS",
|
||||
[554] = "evrlwiRRA", [555] = "evsplatfiRS",
|
||||
[556] = "evmergehiRRR", [557] = "evmergeloRRR",
|
||||
[558] = "evmergehiloRRR", [559] = "evmergelohiRRR",
|
||||
|
||||
[560] = "evcmpgtuYRR", [561] = "evcmpgtsYRR",
|
||||
[562] = "evcmpltuYRR", [563] = "evcmpltsYRR",
|
||||
[564] = "evcmpeqYRR",
|
||||
|
||||
[632] = "evselRRR", [633] = "evselRRRW",
|
||||
[634] = "evselRRRW", [635] = "evselRRRW",
|
||||
[636] = "evselRRRW", [637] = "evselRRRW",
|
||||
[638] = "evselRRRW", [639] = "evselRRRW",
|
||||
|
||||
[640] = "evfsaddRRR", [641] = "evfssubRRR",
|
||||
[644] = "evfsabsRR", [645] = "evfsnabsRR", [646] = "evfsnegRR",
|
||||
[648] = "evfsmulRRR", [649] = "evfsdivRRR",
|
||||
[652] = "evfscmpgtYRR", [653] = "evfscmpltYRR", [654] = "evfscmpeqYRR",
|
||||
[656] = "evfscfuiR-R", [657] = "evfscfsiR-R",
|
||||
[658] = "evfscfufR-R", [659] = "evfscfsfR-R",
|
||||
[660] = "evfsctuiR-R", [661] = "evfsctsiR-R",
|
||||
[662] = "evfsctufR-R", [663] = "evfsctsfR-R",
|
||||
[664] = "evfsctuizR-R", [666] = "evfsctsizR-R",
|
||||
[668] = "evfststgtYRR", [669] = "evfststltYRR", [670] = "evfststeqYRR",
|
||||
|
||||
[704] = "efsaddRRR", [705] = "efssubRRR",
|
||||
[708] = "efsabsRR", [709] = "efsnabsRR", [710] = "efsnegRR",
|
||||
[712] = "efsmulRRR", [713] = "efsdivRRR",
|
||||
[716] = "efscmpgtYRR", [717] = "efscmpltYRR", [718] = "efscmpeqYRR",
|
||||
[719] = "efscfdR-R",
|
||||
[720] = "efscfuiR-R", [721] = "efscfsiR-R",
|
||||
[722] = "efscfufR-R", [723] = "efscfsfR-R",
|
||||
[724] = "efsctuiR-R", [725] = "efsctsiR-R",
|
||||
[726] = "efsctufR-R", [727] = "efsctsfR-R",
|
||||
[728] = "efsctuizR-R", [730] = "efsctsizR-R",
|
||||
[732] = "efststgtYRR", [733] = "efststltYRR", [734] = "efststeqYRR",
|
||||
|
||||
[736] = "efdaddRRR", [737] = "efdsubRRR",
|
||||
[738] = "efdcfuidR-R", [739] = "efdcfsidR-R",
|
||||
[740] = "efdabsRR", [741] = "efdnabsRR", [742] = "efdnegRR",
|
||||
[744] = "efdmulRRR", [745] = "efddivRRR",
|
||||
[746] = "efdctuidzR-R", [747] = "efdctsidzR-R",
|
||||
[748] = "efdcmpgtYRR", [749] = "efdcmpltYRR", [750] = "efdcmpeqYRR",
|
||||
[751] = "efdcfsR-R",
|
||||
[752] = "efdcfuiR-R", [753] = "efdcfsiR-R",
|
||||
[754] = "efdcfufR-R", [755] = "efdcfsfR-R",
|
||||
[756] = "efdctuiR-R", [757] = "efdctsiR-R",
|
||||
[758] = "efdctufR-R", [759] = "efdctsfR-R",
|
||||
[760] = "efdctuizR-R", [762] = "efdctsizR-R",
|
||||
[764] = "efdtstgtYRR", [765] = "efdtstltYRR", [766] = "efdtsteqYRR",
|
||||
|
||||
[768] = "evlddxRR0R", [769] = "evlddRR8",
|
||||
[770] = "evldwxRR0R", [771] = "evldwRR8",
|
||||
[772] = "evldhxRR0R", [773] = "evldhRR8",
|
||||
[776] = "evlhhesplatxRR0R", [777] = "evlhhesplatRR2",
|
||||
[780] = "evlhhousplatxRR0R", [781] = "evlhhousplatRR2",
|
||||
[782] = "evlhhossplatxRR0R", [783] = "evlhhossplatRR2",
|
||||
[784] = "evlwhexRR0R", [785] = "evlwheRR4",
|
||||
[788] = "evlwhouxRR0R", [789] = "evlwhouRR4",
|
||||
[790] = "evlwhosxRR0R", [791] = "evlwhosRR4",
|
||||
[792] = "evlwwsplatxRR0R", [793] = "evlwwsplatRR4",
|
||||
[796] = "evlwhsplatxRR0R", [797] = "evlwhsplatRR4",
|
||||
|
||||
[800] = "evstddxRR0R", [801] = "evstddRR8",
|
||||
[802] = "evstdwxRR0R", [803] = "evstdwRR8",
|
||||
[804] = "evstdhxRR0R", [805] = "evstdhRR8",
|
||||
[816] = "evstwhexRR0R", [817] = "evstwheRR4",
|
||||
[820] = "evstwhoxRR0R", [821] = "evstwhoRR4",
|
||||
[824] = "evstwwexRR0R", [825] = "evstwweRR4",
|
||||
[828] = "evstwwoxRR0R", [829] = "evstwwoRR4",
|
||||
|
||||
[1027] = "evmhessfRRR", [1031] = "evmhossfRRR", [1032] = "evmheumiRRR",
|
||||
[1033] = "evmhesmiRRR", [1035] = "evmhesmfRRR", [1036] = "evmhoumiRRR",
|
||||
[1037] = "evmhosmiRRR", [1039] = "evmhosmfRRR", [1059] = "evmhessfaRRR",
|
||||
[1063] = "evmhossfaRRR", [1064] = "evmheumiaRRR", [1065] = "evmhesmiaRRR",
|
||||
[1067] = "evmhesmfaRRR", [1068] = "evmhoumiaRRR", [1069] = "evmhosmiaRRR",
|
||||
[1071] = "evmhosmfaRRR", [1095] = "evmwhssfRRR", [1096] = "evmwlumiRRR",
|
||||
[1100] = "evmwhumiRRR", [1101] = "evmwhsmiRRR", [1103] = "evmwhsmfRRR",
|
||||
[1107] = "evmwssfRRR", [1112] = "evmwumiRRR", [1113] = "evmwsmiRRR",
|
||||
[1115] = "evmwsmfRRR", [1127] = "evmwhssfaRRR", [1128] = "evmwlumiaRRR",
|
||||
[1132] = "evmwhumiaRRR", [1133] = "evmwhsmiaRRR", [1135] = "evmwhsmfaRRR",
|
||||
[1139] = "evmwssfaRRR", [1144] = "evmwumiaRRR", [1145] = "evmwsmiaRRR",
|
||||
[1147] = "evmwsmfaRRR",
|
||||
|
||||
[1216] = "evaddusiaawRR", [1217] = "evaddssiaawRR",
|
||||
[1218] = "evsubfusiaawRR", [1219] = "evsubfssiaawRR",
|
||||
[1220] = "evmraRR",
|
||||
[1222] = "evdivwsRRR", [1223] = "evdivwuRRR",
|
||||
[1224] = "evaddumiaawRR", [1225] = "evaddsmiaawRR",
|
||||
[1226] = "evsubfumiaawRR", [1227] = "evsubfsmiaawRR",
|
||||
|
||||
[1280] = "evmheusiaawRRR", [1281] = "evmhessiaawRRR",
|
||||
[1283] = "evmhessfaawRRR", [1284] = "evmhousiaawRRR",
|
||||
[1285] = "evmhossiaawRRR", [1287] = "evmhossfaawRRR",
|
||||
[1288] = "evmheumiaawRRR", [1289] = "evmhesmiaawRRR",
|
||||
[1291] = "evmhesmfaawRRR", [1292] = "evmhoumiaawRRR",
|
||||
[1293] = "evmhosmiaawRRR", [1295] = "evmhosmfaawRRR",
|
||||
[1320] = "evmhegumiaaRRR", [1321] = "evmhegsmiaaRRR",
|
||||
[1323] = "evmhegsmfaaRRR", [1324] = "evmhogumiaaRRR",
|
||||
[1325] = "evmhogsmiaaRRR", [1327] = "evmhogsmfaaRRR",
|
||||
[1344] = "evmwlusiaawRRR", [1345] = "evmwlssiaawRRR",
|
||||
[1352] = "evmwlumiaawRRR", [1353] = "evmwlsmiaawRRR",
|
||||
[1363] = "evmwssfaaRRR", [1368] = "evmwumiaaRRR",
|
||||
[1369] = "evmwsmiaaRRR", [1371] = "evmwsmfaaRRR",
|
||||
[1408] = "evmheusianwRRR", [1409] = "evmhessianwRRR",
|
||||
[1411] = "evmhessfanwRRR", [1412] = "evmhousianwRRR",
|
||||
[1413] = "evmhossianwRRR", [1415] = "evmhossfanwRRR",
|
||||
[1416] = "evmheumianwRRR", [1417] = "evmhesmianwRRR",
|
||||
[1419] = "evmhesmfanwRRR", [1420] = "evmhoumianwRRR",
|
||||
[1421] = "evmhosmianwRRR", [1423] = "evmhosmfanwRRR",
|
||||
[1448] = "evmhegumianRRR", [1449] = "evmhegsmianRRR",
|
||||
[1451] = "evmhegsmfanRRR", [1452] = "evmhogumianRRR",
|
||||
[1453] = "evmhogsmianRRR", [1455] = "evmhogsmfanRRR",
|
||||
[1472] = "evmwlusianwRRR", [1473] = "evmwlssianwRRR",
|
||||
[1480] = "evmwlumianwRRR", [1481] = "evmwlsmianwRRR",
|
||||
[1491] = "evmwssfanRRR", [1496] = "evmwumianRRR",
|
||||
[1497] = "evmwsmianRRR", [1499] = "evmwsmfanRRR",
|
||||
}
|
||||
|
||||
local map_pri = {
|
||||
[0] = false, false, "tdiARI", "twiARI",
|
||||
map_spe, false, false, "mulliRRI",
|
||||
"subficRRI", false, "cmpl_iYLRU", "cmp_iYLRI",
|
||||
"addicRRI", "addic.RRI", "addi|liRR0I", "addis|lisRR0I",
|
||||
"b_KBJ", "sc", "bKJ", map_crops,
|
||||
"rlwimiRR~AAA.", map_rlwinm, false, "rlwnmRR~RAA.",
|
||||
"oriNRR~U", "orisRR~U", "xoriRR~U", "xorisRR~U",
|
||||
"andi.RR~U", "andis.RR~U", map_rld, map_ext,
|
||||
"lwzRRD", "lwzuRRD", "lbzRRD", "lbzuRRD",
|
||||
"stwRRD", "stwuRRD", "stbRRD", "stbuRRD",
|
||||
"lhzRRD", "lhzuRRD", "lhaRRD", "lhauRRD",
|
||||
"sthRRD", "sthuRRD", "lmwRRD", "stmwRRD",
|
||||
"lfsFRD", "lfsuFRD", "lfdFRD", "lfduFRD",
|
||||
"stfsFRD", "stfsuFRD", "stfdFRD", "stfduFRD",
|
||||
false, false, map_ld, map_fps,
|
||||
false, false, map_std, map_fpd,
|
||||
}
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
local map_gpr = {
|
||||
[0] = "r0", "sp", "r2", "r3", "r4", "r5", "r6", "r7",
|
||||
"r8", "r9", "r10", "r11", "r12", "r13", "r14", "r15",
|
||||
"r16", "r17", "r18", "r19", "r20", "r21", "r22", "r23",
|
||||
"r24", "r25", "r26", "r27", "r28", "r29", "r30", "r31",
|
||||
}
|
||||
|
||||
local map_cond = { [0] = "lt", "gt", "eq", "so", "ge", "le", "ne", "ns", }
|
||||
|
||||
-- Format a condition bit.
|
||||
local function condfmt(cond)
|
||||
if cond <= 3 then
|
||||
return map_cond[band(cond, 3)]
|
||||
else
|
||||
return format("4*cr%d+%s", rshift(cond, 2), map_cond[band(cond, 3)])
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
-- Output a nicely formatted line with an opcode and operands.
|
||||
local function putop(ctx, text, operands)
|
||||
local pos = ctx.pos
|
||||
local extra = ""
|
||||
if ctx.rel then
|
||||
local sym = ctx.symtab[ctx.rel]
|
||||
if sym then extra = "\t->"..sym end
|
||||
end
|
||||
if ctx.hexdump > 0 then
|
||||
ctx.out(format("%08x %s %-7s %s%s\n",
|
||||
ctx.addr+pos, tohex(ctx.op), text, concat(operands, ", "), extra))
|
||||
else
|
||||
ctx.out(format("%08x %-7s %s%s\n",
|
||||
ctx.addr+pos, text, concat(operands, ", "), extra))
|
||||
end
|
||||
ctx.pos = pos + 4
|
||||
end
|
||||
|
||||
-- Fallback for unknown opcodes.
|
||||
local function unknown(ctx)
|
||||
return putop(ctx, ".long", { "0x"..tohex(ctx.op) })
|
||||
end
|
||||
|
||||
-- Disassemble a single instruction.
|
||||
local function disass_ins(ctx)
|
||||
local pos = ctx.pos
|
||||
local b0, b1, b2, b3 = byte(ctx.code, pos+1, pos+4)
|
||||
local op = bor(lshift(b0, 24), lshift(b1, 16), lshift(b2, 8), b3)
|
||||
local operands = {}
|
||||
local last = nil
|
||||
local rs = 21
|
||||
ctx.op = op
|
||||
ctx.rel = nil
|
||||
|
||||
local opat = map_pri[rshift(b0, 2)]
|
||||
while type(opat) ~= "string" do
|
||||
if not opat then return unknown(ctx) end
|
||||
opat = opat[band(rshift(op, opat.shift), opat.mask)]
|
||||
end
|
||||
local name, pat = match(opat, "^([a-z0-9_.]*)(.*)")
|
||||
local altname, pat2 = match(pat, "|([a-z0-9_.]*)(.*)")
|
||||
if altname then pat = pat2 end
|
||||
|
||||
for p in gmatch(pat, ".") do
|
||||
local x = nil
|
||||
if p == "R" then
|
||||
x = map_gpr[band(rshift(op, rs), 31)]
|
||||
rs = rs - 5
|
||||
elseif p == "F" then
|
||||
x = "f"..band(rshift(op, rs), 31)
|
||||
rs = rs - 5
|
||||
elseif p == "A" then
|
||||
x = band(rshift(op, rs), 31)
|
||||
rs = rs - 5
|
||||
elseif p == "S" then
|
||||
x = arshift(lshift(op, 27-rs), 27)
|
||||
rs = rs - 5
|
||||
elseif p == "I" then
|
||||
x = arshift(lshift(op, 16), 16)
|
||||
elseif p == "U" then
|
||||
x = band(op, 0xffff)
|
||||
elseif p == "D" or p == "E" then
|
||||
local disp = arshift(lshift(op, 16), 16)
|
||||
if p == "E" then disp = band(disp, -4) end
|
||||
if last == "r0" then last = "0" end
|
||||
operands[#operands] = format("%d(%s)", disp, last)
|
||||
elseif p >= "2" and p <= "8" then
|
||||
local disp = band(rshift(op, rs), 31) * p
|
||||
if last == "r0" then last = "0" end
|
||||
operands[#operands] = format("%d(%s)", disp, last)
|
||||
elseif p == "H" then
|
||||
x = band(rshift(op, rs), 31) + lshift(band(op, 2), 4)
|
||||
rs = rs - 5
|
||||
elseif p == "M" then
|
||||
x = band(rshift(op, rs), 31) + band(op, 0x20)
|
||||
elseif p == "C" then
|
||||
x = condfmt(band(rshift(op, rs), 31))
|
||||
rs = rs - 5
|
||||
elseif p == "B" then
|
||||
local bo = rshift(op, 21)
|
||||
local cond = band(rshift(op, 16), 31)
|
||||
local cn = ""
|
||||
rs = rs - 10
|
||||
if band(bo, 4) == 0 then
|
||||
cn = band(bo, 2) == 0 and "dnz" or "dz"
|
||||
if band(bo, 0x10) == 0 then
|
||||
cn = cn..(band(bo, 8) == 0 and "f" or "t")
|
||||
end
|
||||
if band(bo, 0x10) == 0 then x = condfmt(cond) end
|
||||
name = name..(band(bo, 1) == band(rshift(op, 15), 1) and "-" or "+")
|
||||
elseif band(bo, 0x10) == 0 then
|
||||
cn = map_cond[band(cond, 3) + (band(bo, 8) == 0 and 4 or 0)]
|
||||
if cond > 3 then x = "cr"..rshift(cond, 2) end
|
||||
name = name..(band(bo, 1) == band(rshift(op, 15), 1) and "-" or "+")
|
||||
end
|
||||
name = gsub(name, "_", cn)
|
||||
elseif p == "J" then
|
||||
x = arshift(lshift(op, 27-rs), 29-rs)*4
|
||||
if band(op, 2) == 0 then x = ctx.addr + pos + x end
|
||||
ctx.rel = x
|
||||
x = "0x"..tohex(x)
|
||||
elseif p == "K" then
|
||||
if band(op, 1) ~= 0 then name = name.."l" end
|
||||
if band(op, 2) ~= 0 then name = name.."a" end
|
||||
elseif p == "X" or p == "Y" then
|
||||
x = band(rshift(op, rs+2), 7)
|
||||
if x == 0 and p == "Y" then x = nil else x = "cr"..x end
|
||||
rs = rs - 5
|
||||
elseif p == "W" then
|
||||
x = "cr"..band(op, 7)
|
||||
elseif p == "Z" then
|
||||
x = band(rshift(op, rs-4), 255)
|
||||
rs = rs - 10
|
||||
elseif p == ">" then
|
||||
operands[#operands] = rshift(operands[#operands], 1)
|
||||
elseif p == "0" then
|
||||
if last == "r0" then
|
||||
operands[#operands] = nil
|
||||
if altname then name = altname end
|
||||
end
|
||||
elseif p == "L" then
|
||||
name = gsub(name, "_", band(op, 0x00200000) ~= 0 and "d" or "w")
|
||||
elseif p == "." then
|
||||
if band(op, 1) == 1 then name = name.."." end
|
||||
elseif p == "N" then
|
||||
if op == 0x60000000 then name = "nop"; break end
|
||||
elseif p == "~" then
|
||||
local n = #operands
|
||||
operands[n-1], operands[n] = operands[n], operands[n-1]
|
||||
elseif p == "=" then
|
||||
local n = #operands
|
||||
if last == operands[n-1] then
|
||||
operands[n] = nil
|
||||
name = altname
|
||||
end
|
||||
elseif p == "%" then
|
||||
local n = #operands
|
||||
if last == operands[n-1] and last == operands[n-2] then
|
||||
operands[n] = nil
|
||||
operands[n-1] = nil
|
||||
name = altname
|
||||
end
|
||||
elseif p == "-" then
|
||||
rs = rs - 5
|
||||
else
|
||||
assert(false)
|
||||
end
|
||||
if x then operands[#operands+1] = x; last = x end
|
||||
end
|
||||
|
||||
return putop(ctx, name, operands)
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
-- Disassemble a block of code.
|
||||
local function disass_block(ctx, ofs, len)
|
||||
if not ofs then ofs = 0 end
|
||||
local stop = len and ofs+len or #ctx.code
|
||||
stop = stop - stop % 4
|
||||
ctx.pos = ofs - ofs % 4
|
||||
ctx.rel = nil
|
||||
while ctx.pos < stop do disass_ins(ctx) end
|
||||
end
|
||||
|
||||
-- Extended API: create a disassembler context. Then call ctx:disass(ofs, len).
|
||||
local function create(code, addr, out)
|
||||
local ctx = {}
|
||||
ctx.code = code
|
||||
ctx.addr = addr or 0
|
||||
ctx.out = out or io.write
|
||||
ctx.symtab = {}
|
||||
ctx.disass = disass_block
|
||||
ctx.hexdump = 8
|
||||
return ctx
|
||||
end
|
||||
|
||||
-- Simple API: disassemble code (a string) at address and output via out.
|
||||
local function disass(code, addr, out)
|
||||
create(code, addr, out):disass()
|
||||
end
|
||||
|
||||
-- Return register name for RID.
|
||||
local function regname(r)
|
||||
if r < 32 then return map_gpr[r] end
|
||||
return "f"..(r-32)
|
||||
end
|
||||
|
||||
-- Public module functions.
|
||||
return {
|
||||
create = create,
|
||||
disass = disass,
|
||||
regname = regname
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
----------------------------------------------------------------------------
|
||||
-- LuaJIT x64 disassembler wrapper module.
|
||||
--
|
||||
-- Copyright (C) 2005-2023 Mike Pall. All rights reserved.
|
||||
-- Released under the MIT license. See Copyright Notice in luajit.h
|
||||
----------------------------------------------------------------------------
|
||||
-- This module just exports the 64 bit functions from the combined
|
||||
-- x86/x64 disassembler module. All the interesting stuff is there.
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
local dis_x86 = require((string.match(..., ".*%.") or "").."dis_x86")
|
||||
return {
|
||||
create = dis_x86.create64,
|
||||
disass = dis_x86.disass64,
|
||||
regname = dis_x86.regname64
|
||||
}
|
||||
|
||||
@@ -0,0 +1,953 @@
|
||||
----------------------------------------------------------------------------
|
||||
-- LuaJIT x86/x64 disassembler module.
|
||||
--
|
||||
-- Copyright (C) 2005-2023 Mike Pall. All rights reserved.
|
||||
-- Released under the MIT license. See Copyright Notice in luajit.h
|
||||
----------------------------------------------------------------------------
|
||||
-- This is a helper module used by the LuaJIT machine code dumper module.
|
||||
--
|
||||
-- Sending small code snippets to an external disassembler and mixing the
|
||||
-- output with our own stuff was too fragile. So I had to bite the bullet
|
||||
-- and write yet another x86 disassembler. Oh well ...
|
||||
--
|
||||
-- The output format is very similar to what ndisasm generates. But it has
|
||||
-- been developed independently by looking at the opcode tables from the
|
||||
-- Intel and AMD manuals. The supported instruction set is quite extensive
|
||||
-- and reflects what a current generation Intel or AMD CPU implements in
|
||||
-- 32 bit and 64 bit mode. Yes, this includes MMX, SSE, SSE2, SSE3, SSSE3,
|
||||
-- SSE4.1, SSE4.2, SSE4a, AVX, AVX2 and even privileged and hypervisor
|
||||
-- (VMX/SVM) instructions.
|
||||
--
|
||||
-- Notes:
|
||||
-- * The (useless) a16 prefix, 3DNow and pre-586 opcodes are unsupported.
|
||||
-- * No attempt at optimization has been made -- it's fast enough for my needs.
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
local type = type
|
||||
local sub, byte, format = string.sub, string.byte, string.format
|
||||
local match, gmatch, gsub = string.match, string.gmatch, string.gsub
|
||||
local lower, rep = string.lower, string.rep
|
||||
local bit = require("bit")
|
||||
local tohex = bit.tohex
|
||||
|
||||
-- Map for 1st opcode byte in 32 bit mode. Ugly? Well ... read on.
|
||||
local map_opc1_32 = {
|
||||
--0x
|
||||
[0]="addBmr","addVmr","addBrm","addVrm","addBai","addVai","push es","pop es",
|
||||
"orBmr","orVmr","orBrm","orVrm","orBai","orVai","push cs","opc2*",
|
||||
--1x
|
||||
"adcBmr","adcVmr","adcBrm","adcVrm","adcBai","adcVai","push ss","pop ss",
|
||||
"sbbBmr","sbbVmr","sbbBrm","sbbVrm","sbbBai","sbbVai","push ds","pop ds",
|
||||
--2x
|
||||
"andBmr","andVmr","andBrm","andVrm","andBai","andVai","es:seg","daa",
|
||||
"subBmr","subVmr","subBrm","subVrm","subBai","subVai","cs:seg","das",
|
||||
--3x
|
||||
"xorBmr","xorVmr","xorBrm","xorVrm","xorBai","xorVai","ss:seg","aaa",
|
||||
"cmpBmr","cmpVmr","cmpBrm","cmpVrm","cmpBai","cmpVai","ds:seg","aas",
|
||||
--4x
|
||||
"incVR","incVR","incVR","incVR","incVR","incVR","incVR","incVR",
|
||||
"decVR","decVR","decVR","decVR","decVR","decVR","decVR","decVR",
|
||||
--5x
|
||||
"pushUR","pushUR","pushUR","pushUR","pushUR","pushUR","pushUR","pushUR",
|
||||
"popUR","popUR","popUR","popUR","popUR","popUR","popUR","popUR",
|
||||
--6x
|
||||
"sz*pushaw,pusha","sz*popaw,popa","boundVrm","arplWmr",
|
||||
"fs:seg","gs:seg","o16:","a16",
|
||||
"pushUi","imulVrmi","pushBs","imulVrms",
|
||||
"insb","insVS","outsb","outsVS",
|
||||
--7x
|
||||
"joBj","jnoBj","jbBj","jnbBj","jzBj","jnzBj","jbeBj","jaBj",
|
||||
"jsBj","jnsBj","jpeBj","jpoBj","jlBj","jgeBj","jleBj","jgBj",
|
||||
--8x
|
||||
"arith!Bmi","arith!Vmi","arith!Bmi","arith!Vms",
|
||||
"testBmr","testVmr","xchgBrm","xchgVrm",
|
||||
"movBmr","movVmr","movBrm","movVrm",
|
||||
"movVmg","leaVrm","movWgm","popUm",
|
||||
--9x
|
||||
"nop*xchgVaR|pause|xchgWaR|repne nop","xchgVaR","xchgVaR","xchgVaR",
|
||||
"xchgVaR","xchgVaR","xchgVaR","xchgVaR",
|
||||
"sz*cbw,cwde,cdqe","sz*cwd,cdq,cqo","call farViw","wait",
|
||||
"sz*pushfw,pushf","sz*popfw,popf","sahf","lahf",
|
||||
--Ax
|
||||
"movBao","movVao","movBoa","movVoa",
|
||||
"movsb","movsVS","cmpsb","cmpsVS",
|
||||
"testBai","testVai","stosb","stosVS",
|
||||
"lodsb","lodsVS","scasb","scasVS",
|
||||
--Bx
|
||||
"movBRi","movBRi","movBRi","movBRi","movBRi","movBRi","movBRi","movBRi",
|
||||
"movVRI","movVRI","movVRI","movVRI","movVRI","movVRI","movVRI","movVRI",
|
||||
--Cx
|
||||
"shift!Bmu","shift!Vmu","retBw","ret","vex*3$lesVrm","vex*2$ldsVrm","movBmi","movVmi",
|
||||
"enterBwu","leave","retfBw","retf","int3","intBu","into","iretVS",
|
||||
--Dx
|
||||
"shift!Bm1","shift!Vm1","shift!Bmc","shift!Vmc","aamBu","aadBu","salc","xlatb",
|
||||
"fp*0","fp*1","fp*2","fp*3","fp*4","fp*5","fp*6","fp*7",
|
||||
--Ex
|
||||
"loopneBj","loopeBj","loopBj","sz*jcxzBj,jecxzBj,jrcxzBj",
|
||||
"inBau","inVau","outBua","outVua",
|
||||
"callVj","jmpVj","jmp farViw","jmpBj","inBad","inVad","outBda","outVda",
|
||||
--Fx
|
||||
"lock:","int1","repne:rep","rep:","hlt","cmc","testb!Bm","testv!Vm",
|
||||
"clc","stc","cli","sti","cld","std","incb!Bm","incd!Vm",
|
||||
}
|
||||
assert(#map_opc1_32 == 255)
|
||||
|
||||
-- Map for 1st opcode byte in 64 bit mode (overrides only).
|
||||
local map_opc1_64 = setmetatable({
|
||||
[0x06]=false, [0x07]=false, [0x0e]=false,
|
||||
[0x16]=false, [0x17]=false, [0x1e]=false, [0x1f]=false,
|
||||
[0x27]=false, [0x2f]=false, [0x37]=false, [0x3f]=false,
|
||||
[0x60]=false, [0x61]=false, [0x62]=false, [0x63]="movsxdVrDmt", [0x67]="a32:",
|
||||
[0x40]="rex*", [0x41]="rex*b", [0x42]="rex*x", [0x43]="rex*xb",
|
||||
[0x44]="rex*r", [0x45]="rex*rb", [0x46]="rex*rx", [0x47]="rex*rxb",
|
||||
[0x48]="rex*w", [0x49]="rex*wb", [0x4a]="rex*wx", [0x4b]="rex*wxb",
|
||||
[0x4c]="rex*wr", [0x4d]="rex*wrb", [0x4e]="rex*wrx", [0x4f]="rex*wrxb",
|
||||
[0x82]=false, [0x9a]=false, [0xc4]="vex*3", [0xc5]="vex*2", [0xce]=false,
|
||||
[0xd4]=false, [0xd5]=false, [0xd6]=false, [0xea]=false,
|
||||
}, { __index = map_opc1_32 })
|
||||
|
||||
-- Map for 2nd opcode byte (0F xx). True CISC hell. Hey, I told you.
|
||||
-- Prefix dependent MMX/SSE opcodes: (none)|rep|o16|repne, -|F3|66|F2
|
||||
local map_opc2 = {
|
||||
--0x
|
||||
[0]="sldt!Dmp","sgdt!Ump","larVrm","lslVrm",nil,"syscall","clts","sysret",
|
||||
"invd","wbinvd",nil,"ud1",nil,"$prefetch!Bm","femms","3dnowMrmu",
|
||||
--1x
|
||||
"movupsXrm|movssXrvm|movupdXrm|movsdXrvm",
|
||||
"movupsXmr|movssXmvr|movupdXmr|movsdXmvr",
|
||||
"movhlpsXrm$movlpsXrm|movsldupXrm|movlpdXrm|movddupXrm",
|
||||
"movlpsXmr||movlpdXmr",
|
||||
"unpcklpsXrvm||unpcklpdXrvm",
|
||||
"unpckhpsXrvm||unpckhpdXrvm",
|
||||
"movlhpsXrm$movhpsXrm|movshdupXrm|movhpdXrm",
|
||||
"movhpsXmr||movhpdXmr",
|
||||
"$prefetcht!Bm","hintnopVm","hintnopVm","hintnopVm",
|
||||
"hintnopVm","hintnopVm","hintnopVm","hintnopVm",
|
||||
--2x
|
||||
"movUmx$","movUmy$","movUxm$","movUym$","movUmz$",nil,"movUzm$",nil,
|
||||
"movapsXrm||movapdXrm",
|
||||
"movapsXmr||movapdXmr",
|
||||
"cvtpi2psXrMm|cvtsi2ssXrvVmt|cvtpi2pdXrMm|cvtsi2sdXrvVmt",
|
||||
"movntpsXmr|movntssXmr|movntpdXmr|movntsdXmr",
|
||||
"cvttps2piMrXm|cvttss2siVrXm|cvttpd2piMrXm|cvttsd2siVrXm",
|
||||
"cvtps2piMrXm|cvtss2siVrXm|cvtpd2piMrXm|cvtsd2siVrXm",
|
||||
"ucomissXrm||ucomisdXrm",
|
||||
"comissXrm||comisdXrm",
|
||||
--3x
|
||||
"wrmsr","rdtsc","rdmsr","rdpmc","sysenter","sysexit",nil,"getsec",
|
||||
"opc3*38",nil,"opc3*3a",nil,nil,nil,nil,nil,
|
||||
--4x
|
||||
"cmovoVrm","cmovnoVrm","cmovbVrm","cmovnbVrm",
|
||||
"cmovzVrm","cmovnzVrm","cmovbeVrm","cmovaVrm",
|
||||
"cmovsVrm","cmovnsVrm","cmovpeVrm","cmovpoVrm",
|
||||
"cmovlVrm","cmovgeVrm","cmovleVrm","cmovgVrm",
|
||||
--5x
|
||||
"movmskpsVrXm$||movmskpdVrXm$","sqrtpsXrm|sqrtssXrm|sqrtpdXrm|sqrtsdXrm",
|
||||
"rsqrtpsXrm|rsqrtssXrvm","rcppsXrm|rcpssXrvm",
|
||||
"andpsXrvm||andpdXrvm","andnpsXrvm||andnpdXrvm",
|
||||
"orpsXrvm||orpdXrvm","xorpsXrvm||xorpdXrvm",
|
||||
"addpsXrvm|addssXrvm|addpdXrvm|addsdXrvm","mulpsXrvm|mulssXrvm|mulpdXrvm|mulsdXrvm",
|
||||
"cvtps2pdXrm|cvtss2sdXrvm|cvtpd2psXrm|cvtsd2ssXrvm",
|
||||
"cvtdq2psXrm|cvttps2dqXrm|cvtps2dqXrm",
|
||||
"subpsXrvm|subssXrvm|subpdXrvm|subsdXrvm","minpsXrvm|minssXrvm|minpdXrvm|minsdXrvm",
|
||||
"divpsXrvm|divssXrvm|divpdXrvm|divsdXrvm","maxpsXrvm|maxssXrvm|maxpdXrvm|maxsdXrvm",
|
||||
--6x
|
||||
"punpcklbwPrvm","punpcklwdPrvm","punpckldqPrvm","packsswbPrvm",
|
||||
"pcmpgtbPrvm","pcmpgtwPrvm","pcmpgtdPrvm","packuswbPrvm",
|
||||
"punpckhbwPrvm","punpckhwdPrvm","punpckhdqPrvm","packssdwPrvm",
|
||||
"||punpcklqdqXrvm","||punpckhqdqXrvm",
|
||||
"movPrVSm","movqMrm|movdquXrm|movdqaXrm",
|
||||
--7x
|
||||
"pshufwMrmu|pshufhwXrmu|pshufdXrmu|pshuflwXrmu","pshiftw!Pvmu",
|
||||
"pshiftd!Pvmu","pshiftq!Mvmu||pshiftdq!Xvmu",
|
||||
"pcmpeqbPrvm","pcmpeqwPrvm","pcmpeqdPrvm","emms*|",
|
||||
"vmreadUmr||extrqXmuu$|insertqXrmuu$","vmwriteUrm||extrqXrm$|insertqXrm$",
|
||||
nil,nil,
|
||||
"||haddpdXrvm|haddpsXrvm","||hsubpdXrvm|hsubpsXrvm",
|
||||
"movVSmMr|movqXrm|movVSmXr","movqMmr|movdquXmr|movdqaXmr",
|
||||
--8x
|
||||
"joVj","jnoVj","jbVj","jnbVj","jzVj","jnzVj","jbeVj","jaVj",
|
||||
"jsVj","jnsVj","jpeVj","jpoVj","jlVj","jgeVj","jleVj","jgVj",
|
||||
--9x
|
||||
"setoBm","setnoBm","setbBm","setnbBm","setzBm","setnzBm","setbeBm","setaBm",
|
||||
"setsBm","setnsBm","setpeBm","setpoBm","setlBm","setgeBm","setleBm","setgBm",
|
||||
--Ax
|
||||
"push fs","pop fs","cpuid","btVmr","shldVmru","shldVmrc",nil,nil,
|
||||
"push gs","pop gs","rsm","btsVmr","shrdVmru","shrdVmrc","fxsave!Dmp","imulVrm",
|
||||
--Bx
|
||||
"cmpxchgBmr","cmpxchgVmr","$lssVrm","btrVmr",
|
||||
"$lfsVrm","$lgsVrm","movzxVrBmt","movzxVrWmt",
|
||||
"|popcntVrm","ud2Dp","bt!Vmu","btcVmr",
|
||||
"bsfVrm","bsrVrm|lzcntVrm|bsrWrm","movsxVrBmt","movsxVrWmt",
|
||||
--Cx
|
||||
"xaddBmr","xaddVmr",
|
||||
"cmppsXrvmu|cmpssXrvmu|cmppdXrvmu|cmpsdXrvmu","$movntiVmr|",
|
||||
"pinsrwPrvWmu","pextrwDrPmu",
|
||||
"shufpsXrvmu||shufpdXrvmu","$cmpxchg!Qmp",
|
||||
"bswapVR","bswapVR","bswapVR","bswapVR","bswapVR","bswapVR","bswapVR","bswapVR",
|
||||
--Dx
|
||||
"||addsubpdXrvm|addsubpsXrvm","psrlwPrvm","psrldPrvm","psrlqPrvm",
|
||||
"paddqPrvm","pmullwPrvm",
|
||||
"|movq2dqXrMm|movqXmr|movdq2qMrXm$","pmovmskbVrMm||pmovmskbVrXm",
|
||||
"psubusbPrvm","psubuswPrvm","pminubPrvm","pandPrvm",
|
||||
"paddusbPrvm","padduswPrvm","pmaxubPrvm","pandnPrvm",
|
||||
--Ex
|
||||
"pavgbPrvm","psrawPrvm","psradPrvm","pavgwPrvm",
|
||||
"pmulhuwPrvm","pmulhwPrvm",
|
||||
"|cvtdq2pdXrm|cvttpd2dqXrm|cvtpd2dqXrm","$movntqMmr||$movntdqXmr",
|
||||
"psubsbPrvm","psubswPrvm","pminswPrvm","porPrvm",
|
||||
"paddsbPrvm","paddswPrvm","pmaxswPrvm","pxorPrvm",
|
||||
--Fx
|
||||
"|||lddquXrm","psllwPrvm","pslldPrvm","psllqPrvm",
|
||||
"pmuludqPrvm","pmaddwdPrvm","psadbwPrvm","maskmovqMrm||maskmovdquXrm$",
|
||||
"psubbPrvm","psubwPrvm","psubdPrvm","psubqPrvm",
|
||||
"paddbPrvm","paddwPrvm","padddPrvm","ud",
|
||||
}
|
||||
assert(map_opc2[255] == "ud")
|
||||
|
||||
-- Map for three-byte opcodes. Can't wait for their next invention.
|
||||
local map_opc3 = {
|
||||
["38"] = { -- [66] 0f 38 xx
|
||||
--0x
|
||||
[0]="pshufbPrvm","phaddwPrvm","phadddPrvm","phaddswPrvm",
|
||||
"pmaddubswPrvm","phsubwPrvm","phsubdPrvm","phsubswPrvm",
|
||||
"psignbPrvm","psignwPrvm","psigndPrvm","pmulhrswPrvm",
|
||||
"||permilpsXrvm","||permilpdXrvm",nil,nil,
|
||||
--1x
|
||||
"||pblendvbXrma",nil,nil,nil,
|
||||
"||blendvpsXrma","||blendvpdXrma","||permpsXrvm","||ptestXrm",
|
||||
"||broadcastssXrm","||broadcastsdXrm","||broadcastf128XrlXm",nil,
|
||||
"pabsbPrm","pabswPrm","pabsdPrm",nil,
|
||||
--2x
|
||||
"||pmovsxbwXrm","||pmovsxbdXrm","||pmovsxbqXrm","||pmovsxwdXrm",
|
||||
"||pmovsxwqXrm","||pmovsxdqXrm",nil,nil,
|
||||
"||pmuldqXrvm","||pcmpeqqXrvm","||$movntdqaXrm","||packusdwXrvm",
|
||||
"||maskmovpsXrvm","||maskmovpdXrvm","||maskmovpsXmvr","||maskmovpdXmvr",
|
||||
--3x
|
||||
"||pmovzxbwXrm","||pmovzxbdXrm","||pmovzxbqXrm","||pmovzxwdXrm",
|
||||
"||pmovzxwqXrm","||pmovzxdqXrm","||permdXrvm","||pcmpgtqXrvm",
|
||||
"||pminsbXrvm","||pminsdXrvm","||pminuwXrvm","||pminudXrvm",
|
||||
"||pmaxsbXrvm","||pmaxsdXrvm","||pmaxuwXrvm","||pmaxudXrvm",
|
||||
--4x
|
||||
"||pmulddXrvm","||phminposuwXrm",nil,nil,
|
||||
nil,"||psrlvVSXrvm","||psravdXrvm","||psllvVSXrvm",
|
||||
--5x
|
||||
[0x58] = "||pbroadcastdXrlXm",[0x59] = "||pbroadcastqXrlXm",
|
||||
[0x5a] = "||broadcasti128XrlXm",
|
||||
--7x
|
||||
[0x78] = "||pbroadcastbXrlXm",[0x79] = "||pbroadcastwXrlXm",
|
||||
--8x
|
||||
[0x8c] = "||pmaskmovXrvVSm",
|
||||
[0x8e] = "||pmaskmovVSmXvr",
|
||||
--9x
|
||||
[0x96] = "||fmaddsub132pHXrvm",[0x97] = "||fmsubadd132pHXrvm",
|
||||
[0x98] = "||fmadd132pHXrvm",[0x99] = "||fmadd132sHXrvm",
|
||||
[0x9a] = "||fmsub132pHXrvm",[0x9b] = "||fmsub132sHXrvm",
|
||||
[0x9c] = "||fnmadd132pHXrvm",[0x9d] = "||fnmadd132sHXrvm",
|
||||
[0x9e] = "||fnmsub132pHXrvm",[0x9f] = "||fnmsub132sHXrvm",
|
||||
--Ax
|
||||
[0xa6] = "||fmaddsub213pHXrvm",[0xa7] = "||fmsubadd213pHXrvm",
|
||||
[0xa8] = "||fmadd213pHXrvm",[0xa9] = "||fmadd213sHXrvm",
|
||||
[0xaa] = "||fmsub213pHXrvm",[0xab] = "||fmsub213sHXrvm",
|
||||
[0xac] = "||fnmadd213pHXrvm",[0xad] = "||fnmadd213sHXrvm",
|
||||
[0xae] = "||fnmsub213pHXrvm",[0xaf] = "||fnmsub213sHXrvm",
|
||||
--Bx
|
||||
[0xb6] = "||fmaddsub231pHXrvm",[0xb7] = "||fmsubadd231pHXrvm",
|
||||
[0xb8] = "||fmadd231pHXrvm",[0xb9] = "||fmadd231sHXrvm",
|
||||
[0xba] = "||fmsub231pHXrvm",[0xbb] = "||fmsub231sHXrvm",
|
||||
[0xbc] = "||fnmadd231pHXrvm",[0xbd] = "||fnmadd231sHXrvm",
|
||||
[0xbe] = "||fnmsub231pHXrvm",[0xbf] = "||fnmsub231sHXrvm",
|
||||
--Dx
|
||||
[0xdc] = "||aesencXrvm", [0xdd] = "||aesenclastXrvm",
|
||||
[0xde] = "||aesdecXrvm", [0xdf] = "||aesdeclastXrvm",
|
||||
--Fx
|
||||
[0xf0] = "|||crc32TrBmt",[0xf1] = "|||crc32TrVmt",
|
||||
[0xf7] = "| sarxVrmv| shlxVrmv| shrxVrmv",
|
||||
},
|
||||
|
||||
["3a"] = { -- [66] 0f 3a xx
|
||||
--0x
|
||||
[0x00]="||permqXrmu","||permpdXrmu","||pblenddXrvmu",nil,
|
||||
"||permilpsXrmu","||permilpdXrmu","||perm2f128Xrvmu",nil,
|
||||
"||roundpsXrmu","||roundpdXrmu","||roundssXrvmu","||roundsdXrvmu",
|
||||
"||blendpsXrvmu","||blendpdXrvmu","||pblendwXrvmu","palignrPrvmu",
|
||||
--1x
|
||||
nil,nil,nil,nil,
|
||||
"||pextrbVmXru","||pextrwVmXru","||pextrVmSXru","||extractpsVmXru",
|
||||
"||insertf128XrvlXmu","||extractf128XlXmYru",nil,nil,
|
||||
nil,nil,nil,nil,
|
||||
--2x
|
||||
"||pinsrbXrvVmu","||insertpsXrvmu","||pinsrXrvVmuS",nil,
|
||||
--3x
|
||||
[0x38] = "||inserti128Xrvmu",[0x39] = "||extracti128XlXmYru",
|
||||
--4x
|
||||
[0x40] = "||dppsXrvmu",
|
||||
[0x41] = "||dppdXrvmu",
|
||||
[0x42] = "||mpsadbwXrvmu",
|
||||
[0x44] = "||pclmulqdqXrvmu",
|
||||
[0x46] = "||perm2i128Xrvmu",
|
||||
[0x4a] = "||blendvpsXrvmb",[0x4b] = "||blendvpdXrvmb",
|
||||
[0x4c] = "||pblendvbXrvmb",
|
||||
--6x
|
||||
[0x60] = "||pcmpestrmXrmu",[0x61] = "||pcmpestriXrmu",
|
||||
[0x62] = "||pcmpistrmXrmu",[0x63] = "||pcmpistriXrmu",
|
||||
[0xdf] = "||aeskeygenassistXrmu",
|
||||
--Fx
|
||||
[0xf0] = "||| rorxVrmu",
|
||||
},
|
||||
}
|
||||
|
||||
-- Map for VMX/SVM opcodes 0F 01 C0-FF (sgdt group with register operands).
|
||||
local map_opcvm = {
|
||||
[0xc1]="vmcall",[0xc2]="vmlaunch",[0xc3]="vmresume",[0xc4]="vmxoff",
|
||||
[0xc8]="monitor",[0xc9]="mwait",
|
||||
[0xd8]="vmrun",[0xd9]="vmmcall",[0xda]="vmload",[0xdb]="vmsave",
|
||||
[0xdc]="stgi",[0xdd]="clgi",[0xde]="skinit",[0xdf]="invlpga",
|
||||
[0xf8]="swapgs",[0xf9]="rdtscp",
|
||||
}
|
||||
|
||||
-- Map for FP opcodes. And you thought stack machines are simple?
|
||||
local map_opcfp = {
|
||||
-- D8-DF 00-BF: opcodes with a memory operand.
|
||||
-- D8
|
||||
[0]="faddFm","fmulFm","fcomFm","fcompFm","fsubFm","fsubrFm","fdivFm","fdivrFm",
|
||||
"fldFm",nil,"fstFm","fstpFm","fldenvVm","fldcwWm","fnstenvVm","fnstcwWm",
|
||||
-- DA
|
||||
"fiaddDm","fimulDm","ficomDm","ficompDm",
|
||||
"fisubDm","fisubrDm","fidivDm","fidivrDm",
|
||||
-- DB
|
||||
"fildDm","fisttpDm","fistDm","fistpDm",nil,"fld twordFmp",nil,"fstp twordFmp",
|
||||
-- DC
|
||||
"faddGm","fmulGm","fcomGm","fcompGm","fsubGm","fsubrGm","fdivGm","fdivrGm",
|
||||
-- DD
|
||||
"fldGm","fisttpQm","fstGm","fstpGm","frstorDmp",nil,"fnsaveDmp","fnstswWm",
|
||||
-- DE
|
||||
"fiaddWm","fimulWm","ficomWm","ficompWm",
|
||||
"fisubWm","fisubrWm","fidivWm","fidivrWm",
|
||||
-- DF
|
||||
"fildWm","fisttpWm","fistWm","fistpWm",
|
||||
"fbld twordFmp","fildQm","fbstp twordFmp","fistpQm",
|
||||
-- xx C0-FF: opcodes with a pseudo-register operand.
|
||||
-- D8
|
||||
"faddFf","fmulFf","fcomFf","fcompFf","fsubFf","fsubrFf","fdivFf","fdivrFf",
|
||||
-- D9
|
||||
"fldFf","fxchFf",{"fnop"},nil,
|
||||
{"fchs","fabs",nil,nil,"ftst","fxam"},
|
||||
{"fld1","fldl2t","fldl2e","fldpi","fldlg2","fldln2","fldz"},
|
||||
{"f2xm1","fyl2x","fptan","fpatan","fxtract","fprem1","fdecstp","fincstp"},
|
||||
{"fprem","fyl2xp1","fsqrt","fsincos","frndint","fscale","fsin","fcos"},
|
||||
-- DA
|
||||
"fcmovbFf","fcmoveFf","fcmovbeFf","fcmovuFf",nil,{nil,"fucompp"},nil,nil,
|
||||
-- DB
|
||||
"fcmovnbFf","fcmovneFf","fcmovnbeFf","fcmovnuFf",
|
||||
{nil,nil,"fnclex","fninit"},"fucomiFf","fcomiFf",nil,
|
||||
-- DC
|
||||
"fadd toFf","fmul toFf",nil,nil,
|
||||
"fsub toFf","fsubr toFf","fdivr toFf","fdiv toFf",
|
||||
-- DD
|
||||
"ffreeFf",nil,"fstFf","fstpFf","fucomFf","fucompFf",nil,nil,
|
||||
-- DE
|
||||
"faddpFf","fmulpFf",nil,{nil,"fcompp"},
|
||||
"fsubrpFf","fsubpFf","fdivrpFf","fdivpFf",
|
||||
-- DF
|
||||
nil,nil,nil,nil,{"fnstsw ax"},"fucomipFf","fcomipFf",nil,
|
||||
}
|
||||
assert(map_opcfp[126] == "fcomipFf")
|
||||
|
||||
-- Map for opcode groups. The subkey is sp from the ModRM byte.
|
||||
local map_opcgroup = {
|
||||
arith = { "add", "or", "adc", "sbb", "and", "sub", "xor", "cmp" },
|
||||
shift = { "rol", "ror", "rcl", "rcr", "shl", "shr", "sal", "sar" },
|
||||
testb = { "testBmi", "testBmi", "not", "neg", "mul", "imul", "div", "idiv" },
|
||||
testv = { "testVmi", "testVmi", "not", "neg", "mul", "imul", "div", "idiv" },
|
||||
incb = { "inc", "dec" },
|
||||
incd = { "inc", "dec", "callUmp", "$call farDmp",
|
||||
"jmpUmp", "$jmp farDmp", "pushUm" },
|
||||
sldt = { "sldt", "str", "lldt", "ltr", "verr", "verw" },
|
||||
sgdt = { "vm*$sgdt", "vm*$sidt", "$lgdt", "vm*$lidt",
|
||||
"smsw", nil, "lmsw", "vm*$invlpg" },
|
||||
bt = { nil, nil, nil, nil, "bt", "bts", "btr", "btc" },
|
||||
cmpxchg = { nil, "sz*,cmpxchg8bQmp,cmpxchg16bXmp", nil, nil,
|
||||
nil, nil, "vmptrld|vmxon|vmclear", "vmptrst" },
|
||||
pshiftw = { nil, nil, "psrlw", nil, "psraw", nil, "psllw" },
|
||||
pshiftd = { nil, nil, "psrld", nil, "psrad", nil, "pslld" },
|
||||
pshiftq = { nil, nil, "psrlq", nil, nil, nil, "psllq" },
|
||||
pshiftdq = { nil, nil, "psrlq", "psrldq", nil, nil, "psllq", "pslldq" },
|
||||
fxsave = { "$fxsave", "$fxrstor", "$ldmxcsr", "$stmxcsr",
|
||||
nil, "lfenceDp$", "mfenceDp$", "sfenceDp$clflush" },
|
||||
prefetch = { "prefetch", "prefetchw" },
|
||||
prefetcht = { "prefetchnta", "prefetcht0", "prefetcht1", "prefetcht2" },
|
||||
}
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
-- Maps for register names.
|
||||
local map_regs = {
|
||||
B = { "al", "cl", "dl", "bl", "ah", "ch", "dh", "bh",
|
||||
"r8b", "r9b", "r10b", "r11b", "r12b", "r13b", "r14b", "r15b" },
|
||||
B64 = { "al", "cl", "dl", "bl", "spl", "bpl", "sil", "dil",
|
||||
"r8b", "r9b", "r10b", "r11b", "r12b", "r13b", "r14b", "r15b" },
|
||||
W = { "ax", "cx", "dx", "bx", "sp", "bp", "si", "di",
|
||||
"r8w", "r9w", "r10w", "r11w", "r12w", "r13w", "r14w", "r15w" },
|
||||
D = { "eax", "ecx", "edx", "ebx", "esp", "ebp", "esi", "edi",
|
||||
"r8d", "r9d", "r10d", "r11d", "r12d", "r13d", "r14d", "r15d" },
|
||||
Q = { "rax", "rcx", "rdx", "rbx", "rsp", "rbp", "rsi", "rdi",
|
||||
"r8", "r9", "r10", "r11", "r12", "r13", "r14", "r15" },
|
||||
M = { "mm0", "mm1", "mm2", "mm3", "mm4", "mm5", "mm6", "mm7",
|
||||
"mm0", "mm1", "mm2", "mm3", "mm4", "mm5", "mm6", "mm7" }, -- No x64 ext!
|
||||
X = { "xmm0", "xmm1", "xmm2", "xmm3", "xmm4", "xmm5", "xmm6", "xmm7",
|
||||
"xmm8", "xmm9", "xmm10", "xmm11", "xmm12", "xmm13", "xmm14", "xmm15" },
|
||||
Y = { "ymm0", "ymm1", "ymm2", "ymm3", "ymm4", "ymm5", "ymm6", "ymm7",
|
||||
"ymm8", "ymm9", "ymm10", "ymm11", "ymm12", "ymm13", "ymm14", "ymm15" },
|
||||
}
|
||||
local map_segregs = { "es", "cs", "ss", "ds", "fs", "gs", "segr6", "segr7" }
|
||||
|
||||
-- Maps for size names.
|
||||
local map_sz2n = {
|
||||
B = 1, W = 2, D = 4, Q = 8, M = 8, X = 16, Y = 32,
|
||||
}
|
||||
local map_sz2prefix = {
|
||||
B = "byte", W = "word", D = "dword",
|
||||
Q = "qword",
|
||||
M = "qword", X = "xword", Y = "yword",
|
||||
F = "dword", G = "qword", -- No need for sizes/register names for these two.
|
||||
}
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
-- Output a nicely formatted line with an opcode and operands.
|
||||
local function putop(ctx, text, operands)
|
||||
local code, pos, hex = ctx.code, ctx.pos, ""
|
||||
local hmax = ctx.hexdump
|
||||
if hmax > 0 then
|
||||
for i=ctx.start,pos-1 do
|
||||
hex = hex..format("%02X", byte(code, i, i))
|
||||
end
|
||||
if #hex > hmax then hex = sub(hex, 1, hmax)..". "
|
||||
else hex = hex..rep(" ", hmax-#hex+2) end
|
||||
end
|
||||
if operands then text = text.." "..operands end
|
||||
if ctx.o16 then text = "o16 "..text; ctx.o16 = false end
|
||||
if ctx.a32 then text = "a32 "..text; ctx.a32 = false end
|
||||
if ctx.rep then text = ctx.rep.." "..text; ctx.rep = false end
|
||||
if ctx.rex then
|
||||
local t = (ctx.rexw and "w" or "")..(ctx.rexr and "r" or "")..
|
||||
(ctx.rexx and "x" or "")..(ctx.rexb and "b" or "")..
|
||||
(ctx.vexl and "l" or "")
|
||||
if ctx.vexv and ctx.vexv ~= 0 then t = t.."v"..ctx.vexv end
|
||||
if t ~= "" then text = ctx.rex.."."..t.." "..gsub(text, "^ ", "")
|
||||
elseif ctx.rex == "vex" then text = gsub("v"..text, "^v ", "") end
|
||||
ctx.rexw = false; ctx.rexr = false; ctx.rexx = false; ctx.rexb = false
|
||||
ctx.rex = false; ctx.vexl = false; ctx.vexv = false
|
||||
end
|
||||
if ctx.seg then
|
||||
local text2, n = gsub(text, "%[", "["..ctx.seg..":")
|
||||
if n == 0 then text = ctx.seg.." "..text else text = text2 end
|
||||
ctx.seg = false
|
||||
end
|
||||
if ctx.lock then text = "lock "..text; ctx.lock = false end
|
||||
local imm = ctx.imm
|
||||
if imm then
|
||||
local sym = ctx.symtab[imm]
|
||||
if sym then text = text.."\t->"..sym end
|
||||
end
|
||||
ctx.out(format("%08x %s%s\n", ctx.addr+ctx.start, hex, text))
|
||||
ctx.mrm = false
|
||||
ctx.vexv = false
|
||||
ctx.start = pos
|
||||
ctx.imm = nil
|
||||
end
|
||||
|
||||
-- Clear all prefix flags.
|
||||
local function clearprefixes(ctx)
|
||||
ctx.o16 = false; ctx.seg = false; ctx.lock = false; ctx.rep = false
|
||||
ctx.rexw = false; ctx.rexr = false; ctx.rexx = false; ctx.rexb = false
|
||||
ctx.rex = false; ctx.a32 = false; ctx.vexl = false
|
||||
end
|
||||
|
||||
-- Fallback for incomplete opcodes at the end.
|
||||
local function incomplete(ctx)
|
||||
ctx.pos = ctx.stop+1
|
||||
clearprefixes(ctx)
|
||||
return putop(ctx, "(incomplete)")
|
||||
end
|
||||
|
||||
-- Fallback for unknown opcodes.
|
||||
local function unknown(ctx)
|
||||
clearprefixes(ctx)
|
||||
return putop(ctx, "(unknown)")
|
||||
end
|
||||
|
||||
-- Return an immediate of the specified size.
|
||||
local function getimm(ctx, pos, n)
|
||||
if pos+n-1 > ctx.stop then return incomplete(ctx) end
|
||||
local code = ctx.code
|
||||
if n == 1 then
|
||||
local b1 = byte(code, pos, pos)
|
||||
return b1
|
||||
elseif n == 2 then
|
||||
local b1, b2 = byte(code, pos, pos+1)
|
||||
return b1+b2*256
|
||||
else
|
||||
local b1, b2, b3, b4 = byte(code, pos, pos+3)
|
||||
local imm = b1+b2*256+b3*65536+b4*16777216
|
||||
ctx.imm = imm
|
||||
return imm
|
||||
end
|
||||
end
|
||||
|
||||
-- Process pattern string and generate the operands.
|
||||
local function putpat(ctx, name, pat)
|
||||
local operands, regs, sz, mode, sp, rm, sc, rx, sdisp
|
||||
local code, pos, stop, vexl = ctx.code, ctx.pos, ctx.stop, ctx.vexl
|
||||
|
||||
-- Chars used: 1DFGHIMPQRSTUVWXYabcdfgijlmoprstuvwxyz
|
||||
for p in gmatch(pat, ".") do
|
||||
local x = nil
|
||||
if p == "V" or p == "U" then
|
||||
if ctx.rexw then sz = "Q"; ctx.rexw = false
|
||||
elseif ctx.o16 then sz = "W"; ctx.o16 = false
|
||||
elseif p == "U" and ctx.x64 then sz = "Q"
|
||||
else sz = "D" end
|
||||
regs = map_regs[sz]
|
||||
elseif p == "T" then
|
||||
if ctx.rexw then sz = "Q"; ctx.rexw = false else sz = "D" end
|
||||
regs = map_regs[sz]
|
||||
elseif p == "B" then
|
||||
sz = "B"
|
||||
regs = ctx.rex and map_regs.B64 or map_regs.B
|
||||
elseif match(p, "[WDQMXYFG]") then
|
||||
sz = p
|
||||
if sz == "X" and vexl then sz = "Y"; ctx.vexl = false end
|
||||
regs = map_regs[sz]
|
||||
elseif p == "P" then
|
||||
sz = ctx.o16 and "X" or "M"; ctx.o16 = false
|
||||
if sz == "X" and vexl then sz = "Y"; ctx.vexl = false end
|
||||
regs = map_regs[sz]
|
||||
elseif p == "H" then
|
||||
name = name..(ctx.rexw and "d" or "s")
|
||||
ctx.rexw = false
|
||||
elseif p == "S" then
|
||||
name = name..lower(sz)
|
||||
elseif p == "s" then
|
||||
local imm = getimm(ctx, pos, 1); if not imm then return end
|
||||
x = imm <= 127 and format("+0x%02x", imm)
|
||||
or format("-0x%02x", 256-imm)
|
||||
pos = pos+1
|
||||
elseif p == "u" then
|
||||
local imm = getimm(ctx, pos, 1); if not imm then return end
|
||||
x = format("0x%02x", imm)
|
||||
pos = pos+1
|
||||
elseif p == "b" then
|
||||
local imm = getimm(ctx, pos, 1); if not imm then return end
|
||||
x = regs[imm/16+1]
|
||||
pos = pos+1
|
||||
elseif p == "w" then
|
||||
local imm = getimm(ctx, pos, 2); if not imm then return end
|
||||
x = format("0x%x", imm)
|
||||
pos = pos+2
|
||||
elseif p == "o" then -- [offset]
|
||||
if ctx.x64 then
|
||||
local imm1 = getimm(ctx, pos, 4); if not imm1 then return end
|
||||
local imm2 = getimm(ctx, pos+4, 4); if not imm2 then return end
|
||||
x = format("[0x%08x%08x]", imm2, imm1)
|
||||
pos = pos+8
|
||||
else
|
||||
local imm = getimm(ctx, pos, 4); if not imm then return end
|
||||
x = format("[0x%08x]", imm)
|
||||
pos = pos+4
|
||||
end
|
||||
elseif p == "i" or p == "I" then
|
||||
local n = map_sz2n[sz]
|
||||
if n == 8 and ctx.x64 and p == "I" then
|
||||
local imm1 = getimm(ctx, pos, 4); if not imm1 then return end
|
||||
local imm2 = getimm(ctx, pos+4, 4); if not imm2 then return end
|
||||
x = format("0x%08x%08x", imm2, imm1)
|
||||
else
|
||||
if n == 8 then n = 4 end
|
||||
local imm = getimm(ctx, pos, n); if not imm then return end
|
||||
if sz == "Q" and (imm < 0 or imm > 0x7fffffff) then
|
||||
imm = (0xffffffff+1)-imm
|
||||
x = format(imm > 65535 and "-0x%08x" or "-0x%x", imm)
|
||||
else
|
||||
x = format(imm > 65535 and "0x%08x" or "0x%x", imm)
|
||||
end
|
||||
end
|
||||
pos = pos+n
|
||||
elseif p == "j" then
|
||||
local n = map_sz2n[sz]
|
||||
if n == 8 then n = 4 end
|
||||
local imm = getimm(ctx, pos, n); if not imm then return end
|
||||
if sz == "B" and imm > 127 then imm = imm-256
|
||||
elseif imm > 2147483647 then imm = imm-4294967296 end
|
||||
pos = pos+n
|
||||
imm = imm + pos + ctx.addr
|
||||
if imm > 4294967295 and not ctx.x64 then imm = imm-4294967296 end
|
||||
ctx.imm = imm
|
||||
if sz == "W" then
|
||||
x = format("word 0x%04x", imm%65536)
|
||||
elseif ctx.x64 then
|
||||
local lo = imm % 0x1000000
|
||||
x = format("0x%02x%06x", (imm-lo) / 0x1000000, lo)
|
||||
else
|
||||
x = "0x"..tohex(imm)
|
||||
end
|
||||
elseif p == "R" then
|
||||
local r = byte(code, pos-1, pos-1)%8
|
||||
if ctx.rexb then r = r + 8; ctx.rexb = false end
|
||||
x = regs[r+1]
|
||||
elseif p == "a" then x = regs[1]
|
||||
elseif p == "c" then x = "cl"
|
||||
elseif p == "d" then x = "dx"
|
||||
elseif p == "1" then x = "1"
|
||||
else
|
||||
if not mode then
|
||||
mode = ctx.mrm
|
||||
if not mode then
|
||||
if pos > stop then return incomplete(ctx) end
|
||||
mode = byte(code, pos, pos)
|
||||
pos = pos+1
|
||||
end
|
||||
rm = mode%8; mode = (mode-rm)/8
|
||||
sp = mode%8; mode = (mode-sp)/8
|
||||
sdisp = ""
|
||||
if mode < 3 then
|
||||
if rm == 4 then
|
||||
if pos > stop then return incomplete(ctx) end
|
||||
sc = byte(code, pos, pos)
|
||||
pos = pos+1
|
||||
rm = sc%8; sc = (sc-rm)/8
|
||||
rx = sc%8; sc = (sc-rx)/8
|
||||
if ctx.rexx then rx = rx + 8; ctx.rexx = false end
|
||||
if rx == 4 then rx = nil end
|
||||
end
|
||||
if mode > 0 or rm == 5 then
|
||||
local dsz = mode
|
||||
if dsz ~= 1 then dsz = 4 end
|
||||
local disp = getimm(ctx, pos, dsz); if not disp then return end
|
||||
if mode == 0 then rm = nil end
|
||||
if rm or rx or (not sc and ctx.x64 and not ctx.a32) then
|
||||
if dsz == 1 and disp > 127 then
|
||||
sdisp = format("-0x%x", 256-disp)
|
||||
elseif disp >= 0 and disp <= 0x7fffffff then
|
||||
sdisp = format("+0x%x", disp)
|
||||
else
|
||||
sdisp = format("-0x%x", (0xffffffff+1)-disp)
|
||||
end
|
||||
else
|
||||
sdisp = format(ctx.x64 and not ctx.a32 and
|
||||
not (disp >= 0 and disp <= 0x7fffffff)
|
||||
and "0xffffffff%08x" or "0x%08x", disp)
|
||||
end
|
||||
pos = pos+dsz
|
||||
end
|
||||
end
|
||||
if rm and ctx.rexb then rm = rm + 8; ctx.rexb = false end
|
||||
if ctx.rexr then sp = sp + 8; ctx.rexr = false end
|
||||
end
|
||||
if p == "m" then
|
||||
if mode == 3 then x = regs[rm+1]
|
||||
else
|
||||
local aregs = ctx.a32 and map_regs.D or ctx.aregs
|
||||
local srm, srx = "", ""
|
||||
if rm then srm = aregs[rm+1]
|
||||
elseif not sc and ctx.x64 and not ctx.a32 then srm = "rip" end
|
||||
ctx.a32 = false
|
||||
if rx then
|
||||
if rm then srm = srm.."+" end
|
||||
srx = aregs[rx+1]
|
||||
if sc > 0 then srx = srx.."*"..(2^sc) end
|
||||
end
|
||||
x = format("[%s%s%s]", srm, srx, sdisp)
|
||||
end
|
||||
if mode < 3 and
|
||||
(not match(pat, "[aRrgp]") or match(pat, "t")) then -- Yuck.
|
||||
x = map_sz2prefix[sz].." "..x
|
||||
end
|
||||
elseif p == "r" then x = regs[sp+1]
|
||||
elseif p == "g" then x = map_segregs[sp+1]
|
||||
elseif p == "p" then -- Suppress prefix.
|
||||
elseif p == "f" then x = "st"..rm
|
||||
elseif p == "x" then
|
||||
if sp == 0 and ctx.lock and not ctx.x64 then
|
||||
x = "CR8"; ctx.lock = false
|
||||
else
|
||||
x = "CR"..sp
|
||||
end
|
||||
elseif p == "v" then
|
||||
if ctx.vexv then
|
||||
x = regs[ctx.vexv+1]; ctx.vexv = false
|
||||
end
|
||||
elseif p == "y" then x = "DR"..sp
|
||||
elseif p == "z" then x = "TR"..sp
|
||||
elseif p == "l" then vexl = false
|
||||
elseif p == "t" then
|
||||
else
|
||||
error("bad pattern `"..pat.."'")
|
||||
end
|
||||
end
|
||||
if x then operands = operands and operands..", "..x or x end
|
||||
end
|
||||
ctx.pos = pos
|
||||
return putop(ctx, name, operands)
|
||||
end
|
||||
|
||||
-- Forward declaration.
|
||||
local map_act
|
||||
|
||||
-- Fetch and cache MRM byte.
|
||||
local function getmrm(ctx)
|
||||
local mrm = ctx.mrm
|
||||
if not mrm then
|
||||
local pos = ctx.pos
|
||||
if pos > ctx.stop then return nil end
|
||||
mrm = byte(ctx.code, pos, pos)
|
||||
ctx.pos = pos+1
|
||||
ctx.mrm = mrm
|
||||
end
|
||||
return mrm
|
||||
end
|
||||
|
||||
-- Dispatch to handler depending on pattern.
|
||||
local function dispatch(ctx, opat, patgrp)
|
||||
if not opat then return unknown(ctx) end
|
||||
if match(opat, "%|") then -- MMX/SSE variants depending on prefix.
|
||||
local p
|
||||
if ctx.rep then
|
||||
p = ctx.rep=="rep" and "%|([^%|]*)" or "%|[^%|]*%|[^%|]*%|([^%|]*)"
|
||||
ctx.rep = false
|
||||
elseif ctx.o16 then p = "%|[^%|]*%|([^%|]*)"; ctx.o16 = false
|
||||
else p = "^[^%|]*" end
|
||||
opat = match(opat, p)
|
||||
if not opat then return unknown(ctx) end
|
||||
-- ctx.rep = false; ctx.o16 = false
|
||||
--XXX fails for 66 f2 0f 38 f1 06 crc32 eax,WORD PTR [esi]
|
||||
--XXX remove in branches?
|
||||
end
|
||||
if match(opat, "%$") then -- reg$mem variants.
|
||||
local mrm = getmrm(ctx); if not mrm then return incomplete(ctx) end
|
||||
opat = match(opat, mrm >= 192 and "^[^%$]*" or "%$(.*)")
|
||||
if opat == "" then return unknown(ctx) end
|
||||
end
|
||||
if opat == "" then return unknown(ctx) end
|
||||
local name, pat = match(opat, "^([a-z0-9 ]*)(.*)")
|
||||
if pat == "" and patgrp then pat = patgrp end
|
||||
return map_act[sub(pat, 1, 1)](ctx, name, pat)
|
||||
end
|
||||
|
||||
-- Get a pattern from an opcode map and dispatch to handler.
|
||||
local function dispatchmap(ctx, opcmap)
|
||||
local pos = ctx.pos
|
||||
local opat = opcmap[byte(ctx.code, pos, pos)]
|
||||
pos = pos + 1
|
||||
ctx.pos = pos
|
||||
return dispatch(ctx, opat)
|
||||
end
|
||||
|
||||
-- Map for action codes. The key is the first char after the name.
|
||||
map_act = {
|
||||
-- Simple opcodes without operands.
|
||||
[""] = function(ctx, name, pat)
|
||||
return putop(ctx, name)
|
||||
end,
|
||||
|
||||
-- Operand size chars fall right through.
|
||||
B = putpat, W = putpat, D = putpat, Q = putpat,
|
||||
V = putpat, U = putpat, T = putpat,
|
||||
M = putpat, X = putpat, P = putpat,
|
||||
F = putpat, G = putpat, Y = putpat,
|
||||
H = putpat,
|
||||
|
||||
-- Collect prefixes.
|
||||
[":"] = function(ctx, name, pat)
|
||||
ctx[pat == ":" and name or sub(pat, 2)] = name
|
||||
if ctx.pos - ctx.start > 5 then return unknown(ctx) end -- Limit #prefixes.
|
||||
end,
|
||||
|
||||
-- Chain to special handler specified by name.
|
||||
["*"] = function(ctx, name, pat)
|
||||
return map_act[name](ctx, name, sub(pat, 2))
|
||||
end,
|
||||
|
||||
-- Use named subtable for opcode group.
|
||||
["!"] = function(ctx, name, pat)
|
||||
local mrm = getmrm(ctx); if not mrm then return incomplete(ctx) end
|
||||
return dispatch(ctx, map_opcgroup[name][((mrm-(mrm%8))/8)%8+1], sub(pat, 2))
|
||||
end,
|
||||
|
||||
-- o16,o32[,o64] variants.
|
||||
sz = function(ctx, name, pat)
|
||||
if ctx.o16 then ctx.o16 = false
|
||||
else
|
||||
pat = match(pat, ",(.*)")
|
||||
if ctx.rexw then
|
||||
local p = match(pat, ",(.*)")
|
||||
if p then pat = p; ctx.rexw = false end
|
||||
end
|
||||
end
|
||||
pat = match(pat, "^[^,]*")
|
||||
return dispatch(ctx, pat)
|
||||
end,
|
||||
|
||||
-- Two-byte opcode dispatch.
|
||||
opc2 = function(ctx, name, pat)
|
||||
return dispatchmap(ctx, map_opc2)
|
||||
end,
|
||||
|
||||
-- Three-byte opcode dispatch.
|
||||
opc3 = function(ctx, name, pat)
|
||||
return dispatchmap(ctx, map_opc3[pat])
|
||||
end,
|
||||
|
||||
-- VMX/SVM dispatch.
|
||||
vm = function(ctx, name, pat)
|
||||
return dispatch(ctx, map_opcvm[ctx.mrm])
|
||||
end,
|
||||
|
||||
-- Floating point opcode dispatch.
|
||||
fp = function(ctx, name, pat)
|
||||
local mrm = getmrm(ctx); if not mrm then return incomplete(ctx) end
|
||||
local rm = mrm%8
|
||||
local idx = pat*8 + ((mrm-rm)/8)%8
|
||||
if mrm >= 192 then idx = idx + 64 end
|
||||
local opat = map_opcfp[idx]
|
||||
if type(opat) == "table" then opat = opat[rm+1] end
|
||||
return dispatch(ctx, opat)
|
||||
end,
|
||||
|
||||
-- REX prefix.
|
||||
rex = function(ctx, name, pat)
|
||||
if ctx.rex then return unknown(ctx) end -- Only 1 REX or VEX prefix allowed.
|
||||
for p in gmatch(pat, ".") do ctx["rex"..p] = true end
|
||||
ctx.rex = "rex"
|
||||
end,
|
||||
|
||||
-- VEX prefix.
|
||||
vex = function(ctx, name, pat)
|
||||
if ctx.rex then return unknown(ctx) end -- Only 1 REX or VEX prefix allowed.
|
||||
ctx.rex = "vex"
|
||||
local pos = ctx.pos
|
||||
if ctx.mrm then
|
||||
ctx.mrm = nil
|
||||
pos = pos-1
|
||||
end
|
||||
local b = byte(ctx.code, pos, pos)
|
||||
if not b then return incomplete(ctx) end
|
||||
pos = pos+1
|
||||
if b < 128 then ctx.rexr = true end
|
||||
local m = 1
|
||||
if pat == "3" then
|
||||
m = b%32; b = (b-m)/32
|
||||
local nb = b%2; b = (b-nb)/2
|
||||
if nb == 0 then ctx.rexb = true end
|
||||
local nx = b%2
|
||||
if nx == 0 then ctx.rexx = true end
|
||||
b = byte(ctx.code, pos, pos)
|
||||
if not b then return incomplete(ctx) end
|
||||
pos = pos+1
|
||||
if b >= 128 then ctx.rexw = true end
|
||||
end
|
||||
ctx.pos = pos
|
||||
local map
|
||||
if m == 1 then map = map_opc2
|
||||
elseif m == 2 then map = map_opc3["38"]
|
||||
elseif m == 3 then map = map_opc3["3a"]
|
||||
else return unknown(ctx) end
|
||||
local p = b%4; b = (b-p)/4
|
||||
if p == 1 then ctx.o16 = "o16"
|
||||
elseif p == 2 then ctx.rep = "rep"
|
||||
elseif p == 3 then ctx.rep = "repne" end
|
||||
local l = b%2; b = (b-l)/2
|
||||
if l ~= 0 then ctx.vexl = true end
|
||||
ctx.vexv = (-1-b)%16
|
||||
return dispatchmap(ctx, map)
|
||||
end,
|
||||
|
||||
-- Special case for nop with REX prefix.
|
||||
nop = function(ctx, name, pat)
|
||||
return dispatch(ctx, ctx.rex and pat or "nop")
|
||||
end,
|
||||
|
||||
-- Special case for 0F 77.
|
||||
emms = function(ctx, name, pat)
|
||||
if ctx.rex ~= "vex" then
|
||||
return putop(ctx, "emms")
|
||||
elseif ctx.vexl then
|
||||
ctx.vexl = false
|
||||
return putop(ctx, "zeroall")
|
||||
else
|
||||
return putop(ctx, "zeroupper")
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
-- Disassemble a block of code.
|
||||
local function disass_block(ctx, ofs, len)
|
||||
if not ofs then ofs = 0 end
|
||||
local stop = len and ofs+len or #ctx.code
|
||||
ofs = ofs + 1
|
||||
ctx.start = ofs
|
||||
ctx.pos = ofs
|
||||
ctx.stop = stop
|
||||
ctx.imm = nil
|
||||
ctx.mrm = false
|
||||
clearprefixes(ctx)
|
||||
while ctx.pos <= stop do dispatchmap(ctx, ctx.map1) end
|
||||
if ctx.pos ~= ctx.start then incomplete(ctx) end
|
||||
end
|
||||
|
||||
-- Extended API: create a disassembler context. Then call ctx:disass(ofs, len).
|
||||
local function create(code, addr, out)
|
||||
local ctx = {}
|
||||
ctx.code = code
|
||||
ctx.addr = (addr or 0) - 1
|
||||
ctx.out = out or io.write
|
||||
ctx.symtab = {}
|
||||
ctx.disass = disass_block
|
||||
ctx.hexdump = 16
|
||||
ctx.x64 = false
|
||||
ctx.map1 = map_opc1_32
|
||||
ctx.aregs = map_regs.D
|
||||
return ctx
|
||||
end
|
||||
|
||||
local function create64(code, addr, out)
|
||||
local ctx = create(code, addr, out)
|
||||
ctx.x64 = true
|
||||
ctx.map1 = map_opc1_64
|
||||
ctx.aregs = map_regs.Q
|
||||
return ctx
|
||||
end
|
||||
|
||||
-- Simple API: disassemble code (a string) at address and output via out.
|
||||
local function disass(code, addr, out)
|
||||
create(code, addr, out):disass()
|
||||
end
|
||||
|
||||
local function disass64(code, addr, out)
|
||||
create64(code, addr, out):disass()
|
||||
end
|
||||
|
||||
-- Return register name for RID.
|
||||
local function regname(r)
|
||||
if r < 8 then return map_regs.D[r+1] end
|
||||
return map_regs.X[r-7]
|
||||
end
|
||||
|
||||
local function regname64(r)
|
||||
if r < 16 then return map_regs.Q[r+1] end
|
||||
return map_regs.X[r-15]
|
||||
end
|
||||
|
||||
-- Public module functions.
|
||||
return {
|
||||
create = create,
|
||||
create64 = create64,
|
||||
disass = disass,
|
||||
disass64 = disass64,
|
||||
regname = regname,
|
||||
regname64 = regname64
|
||||
}
|
||||
|
||||
@@ -0,0 +1,725 @@
|
||||
----------------------------------------------------------------------------
|
||||
-- LuaJIT compiler dump module.
|
||||
--
|
||||
-- Copyright (C) 2005-2023 Mike Pall. All rights reserved.
|
||||
-- Released under the MIT license. See Copyright Notice in luajit.h
|
||||
----------------------------------------------------------------------------
|
||||
--
|
||||
-- This module can be used to debug the JIT compiler itself. It dumps the
|
||||
-- code representations and structures used in various compiler stages.
|
||||
--
|
||||
-- Example usage:
|
||||
--
|
||||
-- luajit -jdump -e "local x=0; for i=1,1e6 do x=x+i end; print(x)"
|
||||
-- luajit -jdump=im -e "for i=1,1000 do for j=1,1000 do end end" | less -R
|
||||
-- luajit -jdump=is myapp.lua | less -R
|
||||
-- luajit -jdump=-b myapp.lua
|
||||
-- luajit -jdump=+aH,myapp.html myapp.lua
|
||||
-- luajit -jdump=ixT,myapp.dump myapp.lua
|
||||
--
|
||||
-- The first argument specifies the dump mode. The second argument gives
|
||||
-- the output file name. Default output is to stdout, unless the environment
|
||||
-- variable LUAJIT_DUMPFILE is set. The file is overwritten every time the
|
||||
-- module is started.
|
||||
--
|
||||
-- Different features can be turned on or off with the dump mode. If the
|
||||
-- mode starts with a '+', the following features are added to the default
|
||||
-- set of features; a '-' removes them. Otherwise the features are replaced.
|
||||
--
|
||||
-- The following dump features are available (* marks the default):
|
||||
--
|
||||
-- * t Print a line for each started, ended or aborted trace (see also -jv).
|
||||
-- * b Dump the traced bytecode.
|
||||
-- * i Dump the IR (intermediate representation).
|
||||
-- r Augment the IR with register/stack slots.
|
||||
-- s Dump the snapshot map.
|
||||
-- * m Dump the generated machine code.
|
||||
-- x Print each taken trace exit.
|
||||
-- X Print each taken trace exit and the contents of all registers.
|
||||
-- a Print the IR of aborted traces, too.
|
||||
--
|
||||
-- The output format can be set with the following characters:
|
||||
--
|
||||
-- T Plain text output.
|
||||
-- A ANSI-colored text output
|
||||
-- H Colorized HTML + CSS output.
|
||||
--
|
||||
-- The default output format is plain text. It's set to ANSI-colored text
|
||||
-- if the COLORTERM variable is set. Note: this is independent of any output
|
||||
-- redirection, which is actually considered a feature.
|
||||
--
|
||||
-- You probably want to use less -R to enjoy viewing ANSI-colored text from
|
||||
-- a pipe or a file. Add this to your ~/.bashrc: export LESS="-R"
|
||||
--
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
-- Cache some library functions and objects.
|
||||
local jit = require("jit")
|
||||
local jutil = require("jit.util")
|
||||
local vmdef = require("jit.vmdef")
|
||||
local funcinfo, funcbc = jutil.funcinfo, jutil.funcbc
|
||||
local traceinfo, traceir, tracek = jutil.traceinfo, jutil.traceir, jutil.tracek
|
||||
local tracemc, tracesnap = jutil.tracemc, jutil.tracesnap
|
||||
local traceexitstub, ircalladdr = jutil.traceexitstub, jutil.ircalladdr
|
||||
local bit = require("bit")
|
||||
local band, shr, tohex = bit.band, bit.rshift, bit.tohex
|
||||
local sub, gsub, format = string.sub, string.gsub, string.format
|
||||
local byte, rep = string.byte, string.rep
|
||||
local type, tostring = type, tostring
|
||||
local stdout, stderr = io.stdout, io.stderr
|
||||
|
||||
-- Load other modules on-demand.
|
||||
local bcline, disass
|
||||
|
||||
-- Active flag, output file handle and dump mode.
|
||||
local active, out, dumpmode
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
local symtabmt = { __index = false }
|
||||
local symtab = {}
|
||||
local nexitsym = 0
|
||||
|
||||
-- Fill nested symbol table with per-trace exit stub addresses.
|
||||
local function fillsymtab_tr(tr, nexit)
|
||||
local t = {}
|
||||
symtabmt.__index = t
|
||||
if jit.arch:sub(1, 4) == "mips" then
|
||||
t[traceexitstub(tr, 0)] = "exit"
|
||||
return
|
||||
end
|
||||
for i=0,nexit-1 do
|
||||
local addr = traceexitstub(tr, i)
|
||||
if addr < 0 then addr = addr + 2^32 end
|
||||
t[addr] = tostring(i)
|
||||
end
|
||||
local addr = traceexitstub(tr, nexit)
|
||||
if addr then t[addr] = "stack_check" end
|
||||
end
|
||||
|
||||
-- Fill symbol table with trace exit stub addresses.
|
||||
local function fillsymtab(tr, nexit)
|
||||
local t = symtab
|
||||
if nexitsym == 0 then
|
||||
local maskaddr = jit.arch == "arm" and -2
|
||||
local ircall = vmdef.ircall
|
||||
for i=0,#ircall do
|
||||
local addr = ircalladdr(i)
|
||||
if addr ~= 0 then
|
||||
if maskaddr then addr = band(addr, maskaddr) end
|
||||
if addr < 0 then addr = addr + 2^32 end
|
||||
t[addr] = ircall[i]
|
||||
end
|
||||
end
|
||||
end
|
||||
if nexitsym == 1000000 then -- Per-trace exit stubs.
|
||||
fillsymtab_tr(tr, nexit)
|
||||
elseif nexit > nexitsym then -- Shared exit stubs.
|
||||
for i=nexitsym,nexit-1 do
|
||||
local addr = traceexitstub(i)
|
||||
if addr == nil then -- Fall back to per-trace exit stubs.
|
||||
fillsymtab_tr(tr, nexit)
|
||||
setmetatable(symtab, symtabmt)
|
||||
nexit = 1000000
|
||||
break
|
||||
end
|
||||
if addr < 0 then addr = addr + 2^32 end
|
||||
t[addr] = tostring(i)
|
||||
end
|
||||
nexitsym = nexit
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
local function dumpwrite(s)
|
||||
out:write(s)
|
||||
end
|
||||
|
||||
-- Disassemble machine code.
|
||||
local function dump_mcode(tr)
|
||||
local info = traceinfo(tr)
|
||||
if not info then return end
|
||||
local mcode, addr, loop = tracemc(tr)
|
||||
if not mcode then return end
|
||||
if not disass then disass = require("jit.dis_"..jit.arch) end
|
||||
if addr < 0 then addr = addr + 2^32 end
|
||||
out:write("---- TRACE ", tr, " mcode ", #mcode, "\n")
|
||||
local ctx = disass.create(mcode, addr, dumpwrite)
|
||||
ctx.hexdump = 0
|
||||
ctx.symtab = fillsymtab(tr, info.nexit)
|
||||
if loop ~= 0 then
|
||||
symtab[addr+loop] = "LOOP"
|
||||
ctx:disass(0, loop)
|
||||
out:write("->LOOP:\n")
|
||||
ctx:disass(loop, #mcode-loop)
|
||||
symtab[addr+loop] = nil
|
||||
else
|
||||
ctx:disass(0, #mcode)
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
local irtype_text = {
|
||||
[0] = "nil",
|
||||
"fal",
|
||||
"tru",
|
||||
"lud",
|
||||
"str",
|
||||
"p32",
|
||||
"thr",
|
||||
"pro",
|
||||
"fun",
|
||||
"p64",
|
||||
"cdt",
|
||||
"tab",
|
||||
"udt",
|
||||
"flt",
|
||||
"num",
|
||||
"i8 ",
|
||||
"u8 ",
|
||||
"i16",
|
||||
"u16",
|
||||
"int",
|
||||
"u32",
|
||||
"i64",
|
||||
"u64",
|
||||
"sfp",
|
||||
}
|
||||
|
||||
local colortype_ansi = {
|
||||
[0] = "%s",
|
||||
"%s",
|
||||
"%s",
|
||||
"\027[36m%s\027[m",
|
||||
"\027[32m%s\027[m",
|
||||
"%s",
|
||||
"\027[1m%s\027[m",
|
||||
"%s",
|
||||
"\027[1m%s\027[m",
|
||||
"%s",
|
||||
"\027[33m%s\027[m",
|
||||
"\027[31m%s\027[m",
|
||||
"\027[36m%s\027[m",
|
||||
"\027[34m%s\027[m",
|
||||
"\027[34m%s\027[m",
|
||||
"\027[35m%s\027[m",
|
||||
"\027[35m%s\027[m",
|
||||
"\027[35m%s\027[m",
|
||||
"\027[35m%s\027[m",
|
||||
"\027[35m%s\027[m",
|
||||
"\027[35m%s\027[m",
|
||||
"\027[35m%s\027[m",
|
||||
"\027[35m%s\027[m",
|
||||
"\027[35m%s\027[m",
|
||||
}
|
||||
|
||||
local function colorize_text(s)
|
||||
return s
|
||||
end
|
||||
|
||||
local function colorize_ansi(s, t, extra)
|
||||
local out = format(colortype_ansi[t], s)
|
||||
if extra then out = "\027[3m"..out end
|
||||
return out
|
||||
end
|
||||
|
||||
local irtype_ansi = setmetatable({},
|
||||
{ __index = function(tab, t)
|
||||
local s = colorize_ansi(irtype_text[t], t); tab[t] = s; return s; end })
|
||||
|
||||
local html_escape = { ["<"] = "<", [">"] = ">", ["&"] = "&", }
|
||||
|
||||
local function colorize_html(s, t, extra)
|
||||
s = gsub(s, "[<>&]", html_escape)
|
||||
return format('<span class="irt_%s%s">%s</span>',
|
||||
irtype_text[t], extra and " irt_extra" or "", s)
|
||||
end
|
||||
|
||||
local irtype_html = setmetatable({},
|
||||
{ __index = function(tab, t)
|
||||
local s = colorize_html(irtype_text[t], t); tab[t] = s; return s; end })
|
||||
|
||||
local header_html = [[
|
||||
<style type="text/css">
|
||||
background { background: #ffffff; color: #000000; }
|
||||
pre.ljdump {
|
||||
font-size: 10pt;
|
||||
background: #f0f4ff;
|
||||
color: #000000;
|
||||
border: 1px solid #bfcfff;
|
||||
padding: 0.5em;
|
||||
margin-left: 2em;
|
||||
margin-right: 2em;
|
||||
}
|
||||
span.irt_str { color: #00a000; }
|
||||
span.irt_thr, span.irt_fun { color: #404040; font-weight: bold; }
|
||||
span.irt_tab { color: #c00000; }
|
||||
span.irt_udt, span.irt_lud { color: #00c0c0; }
|
||||
span.irt_num { color: #4040c0; }
|
||||
span.irt_int, span.irt_i8, span.irt_u8, span.irt_i16, span.irt_u16 { color: #b040b0; }
|
||||
span.irt_extra { font-style: italic; }
|
||||
</style>
|
||||
]]
|
||||
|
||||
local colorize, irtype
|
||||
|
||||
-- Lookup tables to convert some literals into names.
|
||||
local litname = {
|
||||
["SLOAD "] = setmetatable({}, { __index = function(t, mode)
|
||||
local s = ""
|
||||
if band(mode, 1) ~= 0 then s = s.."P" end
|
||||
if band(mode, 2) ~= 0 then s = s.."F" end
|
||||
if band(mode, 4) ~= 0 then s = s.."T" end
|
||||
if band(mode, 8) ~= 0 then s = s.."C" end
|
||||
if band(mode, 16) ~= 0 then s = s.."R" end
|
||||
if band(mode, 32) ~= 0 then s = s.."I" end
|
||||
if band(mode, 64) ~= 0 then s = s.."K" end
|
||||
t[mode] = s
|
||||
return s
|
||||
end}),
|
||||
["XLOAD "] = { [0] = "", "R", "V", "RV", "U", "RU", "VU", "RVU", },
|
||||
["CONV "] = setmetatable({}, { __index = function(t, mode)
|
||||
local s = irtype[band(mode, 31)]
|
||||
s = irtype[band(shr(mode, 5), 31)].."."..s
|
||||
if band(mode, 0x800) ~= 0 then s = s.." sext" end
|
||||
local c = shr(mode, 12)
|
||||
if c == 1 then s = s.." none"
|
||||
elseif c == 2 then s = s.." index"
|
||||
elseif c == 3 then s = s.." check" end
|
||||
t[mode] = s
|
||||
return s
|
||||
end}),
|
||||
["FLOAD "] = vmdef.irfield,
|
||||
["FREF "] = vmdef.irfield,
|
||||
["FPMATH"] = vmdef.irfpm,
|
||||
["TMPREF"] = { [0] = "", "IN", "OUT", "INOUT", "", "", "OUT2", "INOUT2" },
|
||||
["BUFHDR"] = { [0] = "RESET", "APPEND", "WRITE" },
|
||||
["TOSTR "] = { [0] = "INT", "NUM", "CHAR" },
|
||||
}
|
||||
|
||||
local function ctlsub(c)
|
||||
if c == "\n" then return "\\n"
|
||||
elseif c == "\r" then return "\\r"
|
||||
elseif c == "\t" then return "\\t"
|
||||
else return format("\\%03d", byte(c))
|
||||
end
|
||||
end
|
||||
|
||||
local function fmtfunc(func, pc)
|
||||
local fi = funcinfo(func, pc)
|
||||
if fi.loc then
|
||||
return fi.loc
|
||||
elseif fi.ffid then
|
||||
return vmdef.ffnames[fi.ffid]
|
||||
elseif fi.addr then
|
||||
return format("C:%x", fi.addr)
|
||||
else
|
||||
return "(?)"
|
||||
end
|
||||
end
|
||||
|
||||
local function formatk(tr, idx, sn)
|
||||
local k, t, slot = tracek(tr, idx)
|
||||
local tn = type(k)
|
||||
local s
|
||||
if tn == "number" then
|
||||
if t < 12 then
|
||||
s = k == 0 and "NULL" or format("[0x%08x]", k)
|
||||
elseif band(sn or 0, 0x30000) ~= 0 then
|
||||
s = band(sn, 0x20000) ~= 0 and "contpc" or "ftsz"
|
||||
elseif k == 2^52+2^51 then
|
||||
s = "bias"
|
||||
else
|
||||
s = format(0 < k and k < 0x1p-1026 and "%+a" or "%+.14g", k)
|
||||
end
|
||||
elseif tn == "string" then
|
||||
s = format(#k > 20 and '"%.20s"~' or '"%s"', gsub(k, "%c", ctlsub))
|
||||
elseif tn == "function" then
|
||||
s = fmtfunc(k)
|
||||
elseif tn == "table" then
|
||||
s = format("{%p}", k)
|
||||
elseif tn == "userdata" then
|
||||
if t == 12 then
|
||||
s = format("userdata:%p", k)
|
||||
else
|
||||
s = format("[%p]", k)
|
||||
if s == "[NULL]" then s = "NULL" end
|
||||
end
|
||||
elseif t == 21 then -- int64_t
|
||||
s = sub(tostring(k), 1, -3)
|
||||
if sub(s, 1, 1) ~= "-" then s = "+"..s end
|
||||
elseif sn == 0x1057fff then -- SNAP(1, SNAP_FRAME | SNAP_NORESTORE, REF_NIL)
|
||||
return "----" -- Special case for LJ_FR2 slot 1.
|
||||
else
|
||||
s = tostring(k) -- For primitives.
|
||||
end
|
||||
s = colorize(format("%-4s", s), t, band(sn or 0, 0x100000) ~= 0)
|
||||
if slot then
|
||||
s = format("%s @%d", s, slot)
|
||||
end
|
||||
return s
|
||||
end
|
||||
|
||||
local function printsnap(tr, snap)
|
||||
local n = 2
|
||||
for s=0,snap[1]-1 do
|
||||
local sn = snap[n]
|
||||
if shr(sn, 24) == s then
|
||||
n = n + 1
|
||||
local ref = band(sn, 0xffff) - 0x8000 -- REF_BIAS
|
||||
if ref < 0 then
|
||||
out:write(formatk(tr, ref, sn))
|
||||
elseif band(sn, 0x80000) ~= 0 then -- SNAP_SOFTFPNUM
|
||||
out:write(colorize(format("%04d/%04d", ref, ref+1), 14))
|
||||
else
|
||||
local m, ot, op1, op2 = traceir(tr, ref)
|
||||
out:write(colorize(format("%04d", ref), band(ot, 31), band(sn, 0x100000) ~= 0))
|
||||
end
|
||||
out:write(band(sn, 0x10000) == 0 and " " or "|") -- SNAP_FRAME
|
||||
else
|
||||
out:write("---- ")
|
||||
end
|
||||
end
|
||||
out:write("]\n")
|
||||
end
|
||||
|
||||
-- Dump snapshots (not interleaved with IR).
|
||||
local function dump_snap(tr)
|
||||
out:write("---- TRACE ", tr, " snapshots\n")
|
||||
for i=0,1000000000 do
|
||||
local snap = tracesnap(tr, i)
|
||||
if not snap then break end
|
||||
out:write(format("#%-3d %04d [ ", i, snap[0]))
|
||||
printsnap(tr, snap)
|
||||
end
|
||||
end
|
||||
|
||||
-- Return a register name or stack slot for a rid/sp location.
|
||||
local function ridsp_name(ridsp, ins)
|
||||
if not disass then disass = require("jit.dis_"..jit.arch) end
|
||||
local rid, slot = band(ridsp, 0xff), shr(ridsp, 8)
|
||||
if rid == 253 or rid == 254 then
|
||||
return (slot == 0 or slot == 255) and " {sink" or format(" {%04d", ins-slot)
|
||||
end
|
||||
if ridsp > 255 then return format("[%x]", slot*4) end
|
||||
if rid < 128 then return disass.regname(rid) end
|
||||
return ""
|
||||
end
|
||||
|
||||
-- Dump CALL* function ref and return optional ctype.
|
||||
local function dumpcallfunc(tr, ins)
|
||||
local ctype
|
||||
if ins > 0 then
|
||||
local m, ot, op1, op2 = traceir(tr, ins)
|
||||
if band(ot, 31) == 0 then -- nil type means CARG(func, ctype).
|
||||
ins = op1
|
||||
ctype = formatk(tr, op2)
|
||||
end
|
||||
end
|
||||
if ins < 0 then
|
||||
out:write(format("[0x%x](", tonumber((tracek(tr, ins)))))
|
||||
else
|
||||
out:write(format("%04d (", ins))
|
||||
end
|
||||
return ctype
|
||||
end
|
||||
|
||||
-- Recursively gather CALL* args and dump them.
|
||||
local function dumpcallargs(tr, ins)
|
||||
if ins < 0 then
|
||||
out:write(formatk(tr, ins))
|
||||
else
|
||||
local m, ot, op1, op2 = traceir(tr, ins)
|
||||
local oidx = 6*shr(ot, 8)
|
||||
local op = sub(vmdef.irnames, oidx+1, oidx+6)
|
||||
if op == "CARG " then
|
||||
dumpcallargs(tr, op1)
|
||||
if op2 < 0 then
|
||||
out:write(" ", formatk(tr, op2))
|
||||
else
|
||||
out:write(" ", format("%04d", op2))
|
||||
end
|
||||
else
|
||||
out:write(format("%04d", ins))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Dump IR and interleaved snapshots.
|
||||
local function dump_ir(tr, dumpsnap, dumpreg)
|
||||
local info = traceinfo(tr)
|
||||
if not info then return end
|
||||
local nins = info.nins
|
||||
out:write("---- TRACE ", tr, " IR\n")
|
||||
local irnames = vmdef.irnames
|
||||
local snapref = 65536
|
||||
local snap, snapno
|
||||
if dumpsnap then
|
||||
snap = tracesnap(tr, 0)
|
||||
snapref = snap[0]
|
||||
snapno = 0
|
||||
end
|
||||
for ins=1,nins do
|
||||
if ins >= snapref then
|
||||
if dumpreg then
|
||||
out:write(format(".... SNAP #%-3d [ ", snapno))
|
||||
else
|
||||
out:write(format(".... SNAP #%-3d [ ", snapno))
|
||||
end
|
||||
printsnap(tr, snap)
|
||||
snapno = snapno + 1
|
||||
snap = tracesnap(tr, snapno)
|
||||
snapref = snap and snap[0] or 65536
|
||||
end
|
||||
local m, ot, op1, op2, ridsp = traceir(tr, ins)
|
||||
local oidx, t = 6*shr(ot, 8), band(ot, 31)
|
||||
local op = sub(irnames, oidx+1, oidx+6)
|
||||
if op == "LOOP " then
|
||||
if dumpreg then
|
||||
out:write(format("%04d ------------ LOOP ------------\n", ins))
|
||||
else
|
||||
out:write(format("%04d ------ LOOP ------------\n", ins))
|
||||
end
|
||||
elseif op ~= "NOP " and op ~= "CARG " and
|
||||
(dumpreg or op ~= "RENAME") then
|
||||
local rid = band(ridsp, 255)
|
||||
if dumpreg then
|
||||
out:write(format("%04d %-6s", ins, ridsp_name(ridsp, ins)))
|
||||
else
|
||||
out:write(format("%04d ", ins))
|
||||
end
|
||||
out:write(format("%s%s %s %s ",
|
||||
(rid == 254 or rid == 253) and "}" or
|
||||
(band(ot, 128) == 0 and " " or ">"),
|
||||
band(ot, 64) == 0 and " " or "+",
|
||||
irtype[t], op))
|
||||
local m1, m2 = band(m, 3), band(m, 3*4)
|
||||
if sub(op, 1, 4) == "CALL" then
|
||||
local ctype
|
||||
if m2 == 1*4 then -- op2 == IRMlit
|
||||
out:write(format("%-10s (", vmdef.ircall[op2]))
|
||||
else
|
||||
ctype = dumpcallfunc(tr, op2)
|
||||
end
|
||||
if op1 ~= -1 then dumpcallargs(tr, op1) end
|
||||
out:write(")")
|
||||
if ctype then out:write(" ctype ", ctype) end
|
||||
elseif op == "CNEW " and op2 == -1 then
|
||||
out:write(formatk(tr, op1))
|
||||
elseif m1 ~= 3 then -- op1 != IRMnone
|
||||
if op1 < 0 then
|
||||
out:write(formatk(tr, op1))
|
||||
else
|
||||
out:write(format(m1 == 0 and "%04d" or "#%-3d", op1))
|
||||
end
|
||||
if m2 ~= 3*4 then -- op2 != IRMnone
|
||||
if m2 == 1*4 then -- op2 == IRMlit
|
||||
local litn = litname[op]
|
||||
if litn and litn[op2] then
|
||||
out:write(" ", litn[op2])
|
||||
elseif op == "UREFO " or op == "UREFC " then
|
||||
out:write(format(" #%-3d", shr(op2, 8)))
|
||||
else
|
||||
out:write(format(" #%-3d", op2))
|
||||
end
|
||||
elseif op2 < 0 then
|
||||
out:write(" ", formatk(tr, op2))
|
||||
else
|
||||
out:write(format(" %04d", op2))
|
||||
end
|
||||
end
|
||||
end
|
||||
out:write("\n")
|
||||
end
|
||||
end
|
||||
if snap then
|
||||
if dumpreg then
|
||||
out:write(format(".... SNAP #%-3d [ ", snapno))
|
||||
else
|
||||
out:write(format(".... SNAP #%-3d [ ", snapno))
|
||||
end
|
||||
printsnap(tr, snap)
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
local recprefix = ""
|
||||
local recdepth = 0
|
||||
|
||||
-- Format trace error message.
|
||||
local function fmterr(err, info)
|
||||
if type(err) == "number" then
|
||||
if type(info) == "function" then info = fmtfunc(info) end
|
||||
err = format(vmdef.traceerr[err], info)
|
||||
end
|
||||
return err
|
||||
end
|
||||
|
||||
-- Dump trace states.
|
||||
local function dump_trace(what, tr, func, pc, otr, oex)
|
||||
if what == "stop" or (what == "abort" and dumpmode.a) then
|
||||
if dumpmode.i then dump_ir(tr, dumpmode.s, dumpmode.r and what == "stop")
|
||||
elseif dumpmode.s then dump_snap(tr) end
|
||||
if dumpmode.m then dump_mcode(tr) end
|
||||
end
|
||||
if what == "start" then
|
||||
if dumpmode.H then out:write('<pre class="ljdump">\n') end
|
||||
out:write("---- TRACE ", tr, " ", what)
|
||||
if otr then out:write(" ", otr, "/", oex == -1 and "stitch" or oex) end
|
||||
out:write(" ", fmtfunc(func, pc), "\n")
|
||||
elseif what == "stop" or what == "abort" then
|
||||
out:write("---- TRACE ", tr, " ", what)
|
||||
if what == "abort" then
|
||||
out:write(" ", fmtfunc(func, pc), " -- ", fmterr(otr, oex), "\n")
|
||||
else
|
||||
local info = traceinfo(tr)
|
||||
local link, ltype = info.link, info.linktype
|
||||
if link == tr or link == 0 then
|
||||
out:write(" -> ", ltype, "\n")
|
||||
elseif ltype == "root" then
|
||||
out:write(" -> ", link, "\n")
|
||||
else
|
||||
out:write(" -> ", link, " ", ltype, "\n")
|
||||
end
|
||||
end
|
||||
if dumpmode.H then out:write("</pre>\n\n") else out:write("\n") end
|
||||
else
|
||||
if what == "flush" then symtab, nexitsym = {}, 0 end
|
||||
out:write("---- TRACE ", what, "\n\n")
|
||||
end
|
||||
out:flush()
|
||||
end
|
||||
|
||||
-- Dump recorded bytecode.
|
||||
local function dump_record(tr, func, pc, depth)
|
||||
if depth ~= recdepth then
|
||||
recdepth = depth
|
||||
recprefix = rep(" .", depth)
|
||||
end
|
||||
local line
|
||||
if pc >= 0 then
|
||||
line = bcline(func, pc, recprefix)
|
||||
if dumpmode.H then line = gsub(line, "[<>&]", html_escape) end
|
||||
else
|
||||
line = "0000 "..recprefix.." FUNCC \n"
|
||||
end
|
||||
if pc <= 0 then
|
||||
out:write(sub(line, 1, -2), " ; ", fmtfunc(func), "\n")
|
||||
else
|
||||
out:write(line)
|
||||
end
|
||||
if pc >= 0 and band(funcbc(func, pc), 0xff) < 16 then -- ORDER BC
|
||||
out:write(bcline(func, pc+1, recprefix)) -- Write JMP for cond.
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
local gpr64 = jit.arch:match("64")
|
||||
local fprmips32 = jit.arch == "mips" or jit.arch == "mipsel"
|
||||
|
||||
-- Dump taken trace exits.
|
||||
local function dump_texit(tr, ex, ngpr, nfpr, ...)
|
||||
out:write("---- TRACE ", tr, " exit ", ex, "\n")
|
||||
if dumpmode.X then
|
||||
local regs = {...}
|
||||
if gpr64 then
|
||||
for i=1,ngpr do
|
||||
out:write(format(" %016x", regs[i]))
|
||||
if i % 4 == 0 then out:write("\n") end
|
||||
end
|
||||
else
|
||||
for i=1,ngpr do
|
||||
out:write(" ", tohex(regs[i]))
|
||||
if i % 8 == 0 then out:write("\n") end
|
||||
end
|
||||
end
|
||||
if fprmips32 then
|
||||
for i=1,nfpr,2 do
|
||||
out:write(format(" %+17.14g", regs[ngpr+i]))
|
||||
if i % 8 == 7 then out:write("\n") end
|
||||
end
|
||||
else
|
||||
for i=1,nfpr do
|
||||
out:write(format(" %+17.14g", regs[ngpr+i]))
|
||||
if i % 4 == 0 then out:write("\n") end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
-- Detach dump handlers.
|
||||
local function dumpoff()
|
||||
if active then
|
||||
active = false
|
||||
jit.attach(dump_texit)
|
||||
jit.attach(dump_record)
|
||||
jit.attach(dump_trace)
|
||||
if out and out ~= stdout and out ~= stderr then out:close() end
|
||||
out = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- Open the output file and attach dump handlers.
|
||||
local function dumpon(opt, outfile)
|
||||
if active then dumpoff() end
|
||||
|
||||
local term = os.getenv("TERM")
|
||||
local colormode = (term and term:match("color") or os.getenv("COLORTERM")) and "A" or "T"
|
||||
if opt then
|
||||
opt = gsub(opt, "[TAH]", function(mode) colormode = mode; return ""; end)
|
||||
end
|
||||
|
||||
local m = { t=true, b=true, i=true, m=true, }
|
||||
if opt and opt ~= "" then
|
||||
local o = sub(opt, 1, 1)
|
||||
if o ~= "+" and o ~= "-" then m = {} end
|
||||
for i=1,#opt do m[sub(opt, i, i)] = (o ~= "-") end
|
||||
end
|
||||
dumpmode = m
|
||||
|
||||
if m.t or m.b or m.i or m.s or m.m then
|
||||
jit.attach(dump_trace, "trace")
|
||||
end
|
||||
if m.b then
|
||||
jit.attach(dump_record, "record")
|
||||
if not bcline then bcline = require("jit.bc").line end
|
||||
end
|
||||
if m.x or m.X then
|
||||
jit.attach(dump_texit, "texit")
|
||||
end
|
||||
|
||||
if not outfile then outfile = os.getenv("LUAJIT_DUMPFILE") end
|
||||
if outfile then
|
||||
out = outfile == "-" and stdout or assert(io.open(outfile, "w"))
|
||||
else
|
||||
out = stdout
|
||||
end
|
||||
|
||||
m[colormode] = true
|
||||
if colormode == "A" then
|
||||
colorize = colorize_ansi
|
||||
irtype = irtype_ansi
|
||||
elseif colormode == "H" then
|
||||
colorize = colorize_html
|
||||
irtype = irtype_html
|
||||
out:write(header_html)
|
||||
else
|
||||
colorize = colorize_text
|
||||
irtype = irtype_text
|
||||
end
|
||||
|
||||
active = true
|
||||
end
|
||||
|
||||
-- Public module functions.
|
||||
return {
|
||||
on = dumpon,
|
||||
off = dumpoff,
|
||||
start = dumpon -- For -j command line option.
|
||||
}
|
||||
|
||||
@@ -0,0 +1,311 @@
|
||||
----------------------------------------------------------------------------
|
||||
-- LuaJIT profiler.
|
||||
--
|
||||
-- Copyright (C) 2005-2023 Mike Pall. All rights reserved.
|
||||
-- Released under the MIT license. See Copyright Notice in luajit.h
|
||||
----------------------------------------------------------------------------
|
||||
--
|
||||
-- This module is a simple command line interface to the built-in
|
||||
-- low-overhead profiler of LuaJIT.
|
||||
--
|
||||
-- The lower-level API of the profiler is accessible via the "jit.profile"
|
||||
-- module or the luaJIT_profile_* C API.
|
||||
--
|
||||
-- Example usage:
|
||||
--
|
||||
-- luajit -jp myapp.lua
|
||||
-- luajit -jp=s myapp.lua
|
||||
-- luajit -jp=-s myapp.lua
|
||||
-- luajit -jp=vl myapp.lua
|
||||
-- luajit -jp=G,profile.txt myapp.lua
|
||||
--
|
||||
-- The following dump features are available:
|
||||
--
|
||||
-- f Stack dump: function name, otherwise module:line. Default mode.
|
||||
-- F Stack dump: ditto, but always prepend module.
|
||||
-- l Stack dump: module:line.
|
||||
-- <number> stack dump depth (callee < caller). Default: 1.
|
||||
-- -<number> Inverse stack dump depth (caller > callee).
|
||||
-- s Split stack dump after first stack level. Implies abs(depth) >= 2.
|
||||
-- p Show full path for module names.
|
||||
-- v Show VM states. Can be combined with stack dumps, e.g. vf or fv.
|
||||
-- z Show zones. Can be combined with stack dumps, e.g. zf or fz.
|
||||
-- r Show raw sample counts. Default: show percentages.
|
||||
-- a Annotate excerpts from source code files.
|
||||
-- A Annotate complete source code files.
|
||||
-- G Produce raw output suitable for graphical tools (e.g. flame graphs).
|
||||
-- m<number> Minimum sample percentage to be shown. Default: 3.
|
||||
-- i<number> Sampling interval in milliseconds. Default: 10.
|
||||
--
|
||||
----------------------------------------------------------------------------
|
||||
|
||||
-- Cache some library functions and objects.
|
||||
local jit = require("jit")
|
||||
local profile = require("jit.profile")
|
||||
local vmdef = require("jit.vmdef")
|
||||
local math = math
|
||||
local pairs, ipairs, tonumber, floor = pairs, ipairs, tonumber, math.floor
|
||||
local sort, format = table.sort, string.format
|
||||
local stdout = io.stdout
|
||||
local zone -- Load jit.zone module on demand.
|
||||
|
||||
-- Output file handle.
|
||||
local out
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
local prof_ud
|
||||
local prof_states, prof_split, prof_min, prof_raw, prof_fmt, prof_depth
|
||||
local prof_ann, prof_count1, prof_count2, prof_samples
|
||||
|
||||
local map_vmmode = {
|
||||
N = "Compiled",
|
||||
I = "Interpreted",
|
||||
C = "C code",
|
||||
G = "Garbage Collector",
|
||||
J = "JIT Compiler",
|
||||
}
|
||||
|
||||
-- Profiler callback.
|
||||
local function prof_cb(th, samples, vmmode)
|
||||
prof_samples = prof_samples + samples
|
||||
local key_stack, key_stack2, key_state
|
||||
-- Collect keys for sample.
|
||||
if prof_states then
|
||||
if prof_states == "v" then
|
||||
key_state = map_vmmode[vmmode] or vmmode
|
||||
else
|
||||
key_state = zone:get() or "(none)"
|
||||
end
|
||||
end
|
||||
if prof_fmt then
|
||||
key_stack = profile.dumpstack(th, prof_fmt, prof_depth)
|
||||
key_stack = key_stack:gsub("%[builtin#(%d+)%]", function(x)
|
||||
return vmdef.ffnames[tonumber(x)]
|
||||
end)
|
||||
if prof_split == 2 then
|
||||
local k1, k2 = key_stack:match("(.-) [<>] (.*)")
|
||||
if k2 then key_stack, key_stack2 = k1, k2 end
|
||||
elseif prof_split == 3 then
|
||||
key_stack2 = profile.dumpstack(th, "l", 1)
|
||||
end
|
||||
end
|
||||
-- Order keys.
|
||||
local k1, k2
|
||||
if prof_split == 1 then
|
||||
if key_state then
|
||||
k1 = key_state
|
||||
if key_stack then k2 = key_stack end
|
||||
end
|
||||
elseif key_stack then
|
||||
k1 = key_stack
|
||||
if key_stack2 then k2 = key_stack2 elseif key_state then k2 = key_state end
|
||||
end
|
||||
-- Coalesce samples in one or two levels.
|
||||
if k1 then
|
||||
local t1 = prof_count1
|
||||
t1[k1] = (t1[k1] or 0) + samples
|
||||
if k2 then
|
||||
local t2 = prof_count2
|
||||
local t3 = t2[k1]
|
||||
if not t3 then t3 = {}; t2[k1] = t3 end
|
||||
t3[k2] = (t3[k2] or 0) + samples
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
-- Show top N list.
|
||||
local function prof_top(count1, count2, samples, indent)
|
||||
local t, n = {}, 0
|
||||
for k in pairs(count1) do
|
||||
n = n + 1
|
||||
t[n] = k
|
||||
end
|
||||
sort(t, function(a, b) return count1[a] > count1[b] end)
|
||||
for i=1,n do
|
||||
local k = t[i]
|
||||
local v = count1[k]
|
||||
local pct = floor(v*100/samples + 0.5)
|
||||
if pct < prof_min then break end
|
||||
if not prof_raw then
|
||||
out:write(format("%s%2d%% %s\n", indent, pct, k))
|
||||
elseif prof_raw == "r" then
|
||||
out:write(format("%s%5d %s\n", indent, v, k))
|
||||
else
|
||||
out:write(format("%s %d\n", k, v))
|
||||
end
|
||||
if count2 then
|
||||
local r = count2[k]
|
||||
if r then
|
||||
prof_top(r, nil, v, (prof_split == 3 or prof_split == 1) and " -- " or
|
||||
(prof_depth < 0 and " -> " or " <- "))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Annotate source code
|
||||
local function prof_annotate(count1, samples)
|
||||
local files = {}
|
||||
local ms = 0
|
||||
for k, v in pairs(count1) do
|
||||
local pct = floor(v*100/samples + 0.5)
|
||||
ms = math.max(ms, v)
|
||||
if pct >= prof_min then
|
||||
local file, line = k:match("^(.*):(%d+)$")
|
||||
if not file then file = k; line = 0 end
|
||||
local fl = files[file]
|
||||
if not fl then fl = {}; files[file] = fl; files[#files+1] = file end
|
||||
line = tonumber(line)
|
||||
fl[line] = prof_raw and v or pct
|
||||
end
|
||||
end
|
||||
sort(files)
|
||||
local fmtv, fmtn = " %3d%% | %s\n", " | %s\n"
|
||||
if prof_raw then
|
||||
local n = math.max(5, math.ceil(math.log10(ms)))
|
||||
fmtv = "%"..n.."d | %s\n"
|
||||
fmtn = (" "):rep(n).." | %s\n"
|
||||
end
|
||||
local ann = prof_ann
|
||||
for _, file in ipairs(files) do
|
||||
local f0 = file:byte()
|
||||
if f0 == 40 or f0 == 91 then
|
||||
out:write(format("\n====== %s ======\n[Cannot annotate non-file]\n", file))
|
||||
break
|
||||
end
|
||||
local fp, err = io.open(file)
|
||||
if not fp then
|
||||
out:write(format("====== ERROR: %s: %s\n", file, err))
|
||||
break
|
||||
end
|
||||
out:write(format("\n====== %s ======\n", file))
|
||||
local fl = files[file]
|
||||
local n, show = 1, false
|
||||
if ann ~= 0 then
|
||||
for i=1,ann do
|
||||
if fl[i] then show = true; out:write("@@ 1 @@\n"); break end
|
||||
end
|
||||
end
|
||||
for line in fp:lines() do
|
||||
if line:byte() == 27 then
|
||||
out:write("[Cannot annotate bytecode file]\n")
|
||||
break
|
||||
end
|
||||
local v = fl[n]
|
||||
if ann ~= 0 then
|
||||
local v2 = fl[n+ann]
|
||||
if show then
|
||||
if v2 then show = n+ann elseif v then show = n
|
||||
elseif show+ann < n then show = false end
|
||||
elseif v2 then
|
||||
show = n+ann
|
||||
out:write(format("@@ %d @@\n", n))
|
||||
end
|
||||
if not show then goto next end
|
||||
end
|
||||
if v then
|
||||
out:write(format(fmtv, v, line))
|
||||
else
|
||||
out:write(format(fmtn, line))
|
||||
end
|
||||
::next::
|
||||
n = n + 1
|
||||
end
|
||||
fp:close()
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
-- Finish profiling and dump result.
|
||||
local function prof_finish()
|
||||
if prof_ud then
|
||||
profile.stop()
|
||||
local samples = prof_samples
|
||||
if samples == 0 then
|
||||
if prof_raw ~= true then out:write("[No samples collected]\n") end
|
||||
return
|
||||
end
|
||||
if prof_ann then
|
||||
prof_annotate(prof_count1, samples)
|
||||
else
|
||||
prof_top(prof_count1, prof_count2, samples, "")
|
||||
end
|
||||
prof_count1 = nil
|
||||
prof_count2 = nil
|
||||
prof_ud = nil
|
||||
if out ~= stdout then out:close() end
|
||||
end
|
||||
end
|
||||
|
||||
-- Start profiling.
|
||||
local function prof_start(mode)
|
||||
local interval = ""
|
||||
mode = mode:gsub("i%d*", function(s) interval = s; return "" end)
|
||||
prof_min = 3
|
||||
mode = mode:gsub("m(%d+)", function(s) prof_min = tonumber(s); return "" end)
|
||||
prof_depth = 1
|
||||
mode = mode:gsub("%-?%d+", function(s) prof_depth = tonumber(s); return "" end)
|
||||
local m = {}
|
||||
for c in mode:gmatch(".") do m[c] = c end
|
||||
prof_states = m.z or m.v
|
||||
if prof_states == "z" then zone = require("jit.zone") end
|
||||
local scope = m.l or m.f or m.F or (prof_states and "" or "f")
|
||||
local flags = (m.p or "")
|
||||
prof_raw = m.r
|
||||
if m.s then
|
||||
prof_split = 2
|
||||
if prof_depth == -1 or m["-"] then prof_depth = -2
|
||||
elseif prof_depth == 1 then prof_depth = 2 end
|
||||
elseif mode:find("[fF].*l") then
|
||||
scope = "l"
|
||||
prof_split = 3
|
||||
else
|
||||
prof_split = (scope == "" or mode:find("[zv].*[lfF]")) and 1 or 0
|
||||
end
|
||||
prof_ann = m.A and 0 or (m.a and 3)
|
||||
if prof_ann then
|
||||
scope = "l"
|
||||
prof_fmt = "pl"
|
||||
prof_split = 0
|
||||
prof_depth = 1
|
||||
elseif m.G and scope ~= "" then
|
||||
prof_fmt = flags..scope.."Z;"
|
||||
prof_depth = -100
|
||||
prof_raw = true
|
||||
prof_min = 0
|
||||
elseif scope == "" then
|
||||
prof_fmt = false
|
||||
else
|
||||
local sc = prof_split == 3 and m.f or m.F or scope
|
||||
prof_fmt = flags..sc..(prof_depth >= 0 and "Z < " or "Z > ")
|
||||
end
|
||||
prof_count1 = {}
|
||||
prof_count2 = {}
|
||||
prof_samples = 0
|
||||
profile.start(scope:lower()..interval, prof_cb)
|
||||
prof_ud = newproxy(true)
|
||||
getmetatable(prof_ud).__gc = prof_finish
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
local function start(mode, outfile)
|
||||
if not outfile then outfile = os.getenv("LUAJIT_PROFILEFILE") end
|
||||
if outfile then
|
||||
out = outfile == "-" and stdout or assert(io.open(outfile, "w"))
|
||||
else
|
||||
out = stdout
|
||||
end
|
||||
prof_start(mode or "f")
|
||||
end
|
||||
|
||||
-- Public module functions.
|
||||
return {
|
||||
start = start, -- For -j command line option.
|
||||
stop = prof_finish
|
||||
}
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
----------------------------------------------------------------------------
|
||||
-- Verbose mode of the LuaJIT compiler.
|
||||
--
|
||||
-- Copyright (C) 2005-2023 Mike Pall. All rights reserved.
|
||||
-- Released under the MIT license. See Copyright Notice in luajit.h
|
||||
----------------------------------------------------------------------------
|
||||
--
|
||||
-- This module shows verbose information about the progress of the
|
||||
-- JIT compiler. It prints one line for each generated trace. This module
|
||||
-- is useful to see which code has been compiled or where the compiler
|
||||
-- punts and falls back to the interpreter.
|
||||
--
|
||||
-- Example usage:
|
||||
--
|
||||
-- luajit -jv -e "for i=1,1000 do for j=1,1000 do end end"
|
||||
-- luajit -jv=myapp.out myapp.lua
|
||||
--
|
||||
-- Default output is to stderr. To redirect the output to a file, pass a
|
||||
-- filename as an argument (use '-' for stdout) or set the environment
|
||||
-- variable LUAJIT_VERBOSEFILE. The file is overwritten every time the
|
||||
-- module is started.
|
||||
--
|
||||
-- The output from the first example should look like this:
|
||||
--
|
||||
-- [TRACE 1 (command line):1 loop]
|
||||
-- [TRACE 2 (1/3) (command line):1 -> 1]
|
||||
--
|
||||
-- The first number in each line is the internal trace number. Next are
|
||||
-- the file name ('(command line)') and the line number (':1') where the
|
||||
-- trace has started. Side traces also show the parent trace number and
|
||||
-- the exit number where they are attached to in parentheses ('(1/3)').
|
||||
-- An arrow at the end shows where the trace links to ('-> 1'), unless
|
||||
-- it loops to itself.
|
||||
--
|
||||
-- In this case the inner loop gets hot and is traced first, generating
|
||||
-- a root trace. Then the last exit from the 1st trace gets hot, too,
|
||||
-- and triggers generation of the 2nd trace. The side trace follows the
|
||||
-- path along the outer loop and *around* the inner loop, back to its
|
||||
-- start, and then links to the 1st trace. Yes, this may seem unusual,
|
||||
-- if you know how traditional compilers work. Trace compilers are full
|
||||
-- of surprises like this -- have fun! :-)
|
||||
--
|
||||
-- Aborted traces are shown like this:
|
||||
--
|
||||
-- [TRACE --- foo.lua:44 -- leaving loop in root trace at foo:lua:50]
|
||||
--
|
||||
-- Don't worry -- trace aborts are quite common, even in programs which
|
||||
-- can be fully compiled. The compiler may retry several times until it
|
||||
-- finds a suitable trace.
|
||||
--
|
||||
-- Of course this doesn't work with features that are not-yet-implemented
|
||||
-- (NYI error messages). The VM simply falls back to the interpreter. This
|
||||
-- may not matter at all if the particular trace is not very high up in
|
||||
-- the CPU usage profile. Oh, and the interpreter is quite fast, too.
|
||||
--
|
||||
-- Also check out the -jdump module, which prints all the gory details.
|
||||
--
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
-- Cache some library functions and objects.
|
||||
local jit = require("jit")
|
||||
local jutil = require("jit.util")
|
||||
local vmdef = require("jit.vmdef")
|
||||
local funcinfo, traceinfo = jutil.funcinfo, jutil.traceinfo
|
||||
local type, format = type, string.format
|
||||
local stdout, stderr = io.stdout, io.stderr
|
||||
|
||||
-- Active flag and output file handle.
|
||||
local active, out
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
local startloc, startex
|
||||
|
||||
local function fmtfunc(func, pc)
|
||||
local fi = funcinfo(func, pc)
|
||||
if fi.loc then
|
||||
return fi.loc
|
||||
elseif fi.ffid then
|
||||
return vmdef.ffnames[fi.ffid]
|
||||
elseif fi.addr then
|
||||
return format("C:%x", fi.addr)
|
||||
else
|
||||
return "(?)"
|
||||
end
|
||||
end
|
||||
|
||||
-- Format trace error message.
|
||||
local function fmterr(err, info)
|
||||
if type(err) == "number" then
|
||||
if type(info) == "function" then info = fmtfunc(info) end
|
||||
err = format(vmdef.traceerr[err], info)
|
||||
end
|
||||
return err
|
||||
end
|
||||
|
||||
-- Dump trace states.
|
||||
local function dump_trace(what, tr, func, pc, otr, oex)
|
||||
if what == "start" then
|
||||
startloc = fmtfunc(func, pc)
|
||||
startex = otr and "("..otr.."/"..(oex == -1 and "stitch" or oex)..") " or ""
|
||||
else
|
||||
if what == "abort" then
|
||||
local loc = fmtfunc(func, pc)
|
||||
if loc ~= startloc then
|
||||
out:write(format("[TRACE --- %s%s -- %s at %s]\n",
|
||||
startex, startloc, fmterr(otr, oex), loc))
|
||||
else
|
||||
out:write(format("[TRACE --- %s%s -- %s]\n",
|
||||
startex, startloc, fmterr(otr, oex)))
|
||||
end
|
||||
elseif what == "stop" then
|
||||
local info = traceinfo(tr)
|
||||
local link, ltype = info.link, info.linktype
|
||||
if ltype == "interpreter" then
|
||||
out:write(format("[TRACE %3s %s%s -- fallback to interpreter]\n",
|
||||
tr, startex, startloc))
|
||||
elseif ltype == "stitch" then
|
||||
out:write(format("[TRACE %3s %s%s %s %s]\n",
|
||||
tr, startex, startloc, ltype, fmtfunc(func, pc)))
|
||||
elseif link == tr or link == 0 then
|
||||
out:write(format("[TRACE %3s %s%s %s]\n",
|
||||
tr, startex, startloc, ltype))
|
||||
elseif ltype == "root" then
|
||||
out:write(format("[TRACE %3s %s%s -> %d]\n",
|
||||
tr, startex, startloc, link))
|
||||
else
|
||||
out:write(format("[TRACE %3s %s%s -> %d %s]\n",
|
||||
tr, startex, startloc, link, ltype))
|
||||
end
|
||||
else
|
||||
out:write(format("[TRACE %s]\n", what))
|
||||
end
|
||||
out:flush()
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
-- Detach dump handlers.
|
||||
local function dumpoff()
|
||||
if active then
|
||||
active = false
|
||||
jit.attach(dump_trace)
|
||||
if out and out ~= stdout and out ~= stderr then out:close() end
|
||||
out = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- Open the output file and attach dump handlers.
|
||||
local function dumpon(outfile)
|
||||
if active then dumpoff() end
|
||||
if not outfile then outfile = os.getenv("LUAJIT_VERBOSEFILE") end
|
||||
if outfile then
|
||||
out = outfile == "-" and stdout or assert(io.open(outfile, "w"))
|
||||
else
|
||||
out = stderr
|
||||
end
|
||||
jit.attach(dump_trace, "trace")
|
||||
active = true
|
||||
end
|
||||
|
||||
-- Public module functions.
|
||||
return {
|
||||
on = dumpon,
|
||||
off = dumpoff,
|
||||
start = dumpon -- For -j command line option.
|
||||
}
|
||||
|
||||
@@ -0,0 +1,396 @@
|
||||
-- This is a generated file. DO NOT EDIT!
|
||||
|
||||
assert(require("jit").version == "LuaJIT 2.1.1700008891", "LuaJIT core/library version mismatch")
|
||||
|
||||
return {
|
||||
|
||||
bcnames = "ISLT ISGE ISLE ISGT ISEQV ISNEV ISEQS ISNES ISEQN ISNEN ISEQP ISNEP ISTC ISFC IST ISF ISTYPEISNUM MOV NOT UNM LEN ADDVN SUBVN MULVN DIVVN MODVN ADDNV SUBNV MULNV DIVNV MODNV ADDVV SUBVV MULVV DIVVV MODVV POW CAT KSTR KCDATAKSHORTKNUM KPRI KNIL UGET USETV USETS USETN USETP UCLO FNEW TNEW TDUP GGET GSET TGETV TGETS TGETB TGETR TSETV TSETS TSETB TSETM TSETR CALLM CALL CALLMTCALLT ITERC ITERN VARG ISNEXTRETM RET RET0 RET1 FORI JFORI FORL IFORL JFORL ITERL IITERLJITERLLOOP ILOOP JLOOP JMP FUNCF IFUNCFJFUNCFFUNCV IFUNCVJFUNCVFUNCC FUNCCW",
|
||||
|
||||
irnames = "LT GE LE GT ULT UGE ULE UGT EQ NE ABC RETF NOP BASE PVAL GCSTEPHIOP LOOP USE PHI RENAMEPROF KPRI KINT KGC KPTR KKPTR KNULL KNUM KINT64KSLOT BNOT BSWAP BAND BOR BXOR BSHL BSHR BSAR BROL BROR ADD SUB MUL DIV MOD POW NEG ABS LDEXP MIN MAX FPMATHADDOV SUBOV MULOV AREF HREFK HREF NEWREFUREFO UREFC FREF TMPREFSTRREFLREF ALOAD HLOAD ULOAD FLOAD XLOAD SLOAD VLOAD ALEN ASTOREHSTOREUSTOREFSTOREXSTORESNEW XSNEW TNEW TDUP CNEW CNEWI BUFHDRBUFPUTBUFSTRTBAR OBAR XBAR CONV TOBIT TOSTR STRTO CALLN CALLA CALLL CALLS CALLXSCARG ",
|
||||
|
||||
irfpm = { [0]="floor", "ceil", "trunc", "sqrt", "log", "log2", "other", },
|
||||
|
||||
irfield = { [0]="str.len", "func.env", "func.pc", "func.ffid", "thread.env", "tab.meta", "tab.array", "tab.node", "tab.asize", "tab.hmask", "tab.nomm", "udata.meta", "udata.udtype", "udata.file", "sbuf.w", "sbuf.e", "sbuf.b", "sbuf.l", "sbuf.ref", "sbuf.r", "cdata.ctypeid", "cdata.ptr", "cdata.int", "cdata.int64", "cdata.int64_4", },
|
||||
|
||||
ircall = {
|
||||
[0]="lj_str_cmp",
|
||||
"lj_str_find",
|
||||
"lj_str_new",
|
||||
"lj_strscan_num",
|
||||
"lj_strfmt_int",
|
||||
"lj_strfmt_num",
|
||||
"lj_strfmt_char",
|
||||
"lj_strfmt_putint",
|
||||
"lj_strfmt_putnum",
|
||||
"lj_strfmt_putquoted",
|
||||
"lj_strfmt_putfxint",
|
||||
"lj_strfmt_putfnum_int",
|
||||
"lj_strfmt_putfnum_uint",
|
||||
"lj_strfmt_putfnum",
|
||||
"lj_strfmt_putfstr",
|
||||
"lj_strfmt_putfchar",
|
||||
"lj_buf_putmem",
|
||||
"lj_buf_putstr",
|
||||
"lj_buf_putchar",
|
||||
"lj_buf_putstr_reverse",
|
||||
"lj_buf_putstr_lower",
|
||||
"lj_buf_putstr_upper",
|
||||
"lj_buf_putstr_rep",
|
||||
"lj_buf_puttab",
|
||||
"lj_bufx_set",
|
||||
"lj_bufx_more",
|
||||
"lj_serialize_put",
|
||||
"lj_serialize_get",
|
||||
"lj_serialize_encode",
|
||||
"lj_serialize_decode",
|
||||
"lj_buf_tostr",
|
||||
"lj_tab_new_ah",
|
||||
"lj_tab_new1",
|
||||
"lj_tab_dup",
|
||||
"lj_tab_clear",
|
||||
"lj_tab_newkey",
|
||||
"lj_tab_keyindex",
|
||||
"lj_vm_next",
|
||||
"lj_tab_len",
|
||||
"lj_tab_len_hint",
|
||||
"lj_gc_step_jit",
|
||||
"lj_gc_barrieruv",
|
||||
"lj_mem_newgco",
|
||||
"lj_prng_u64d",
|
||||
"lj_vm_modi",
|
||||
"log10",
|
||||
"exp",
|
||||
"sin",
|
||||
"cos",
|
||||
"tan",
|
||||
"asin",
|
||||
"acos",
|
||||
"atan",
|
||||
"sinh",
|
||||
"cosh",
|
||||
"tanh",
|
||||
"fputc",
|
||||
"fwrite",
|
||||
"fflush",
|
||||
"lj_vm_floor",
|
||||
"lj_vm_ceil",
|
||||
"lj_vm_trunc",
|
||||
"sqrt",
|
||||
"log",
|
||||
"lj_vm_log2",
|
||||
"pow",
|
||||
"atan2",
|
||||
"ldexp",
|
||||
"lj_vm_tobit",
|
||||
"softfp_add",
|
||||
"softfp_sub",
|
||||
"softfp_mul",
|
||||
"softfp_div",
|
||||
"softfp_cmp",
|
||||
"softfp_i2d",
|
||||
"softfp_d2i",
|
||||
"lj_vm_sfmin",
|
||||
"lj_vm_sfmax",
|
||||
"lj_vm_tointg",
|
||||
"softfp_ui2d",
|
||||
"softfp_f2d",
|
||||
"softfp_d2ui",
|
||||
"softfp_d2f",
|
||||
"softfp_i2f",
|
||||
"softfp_ui2f",
|
||||
"softfp_f2i",
|
||||
"softfp_f2ui",
|
||||
"fp64_l2d",
|
||||
"fp64_ul2d",
|
||||
"fp64_l2f",
|
||||
"fp64_ul2f",
|
||||
"fp64_d2l",
|
||||
"fp64_d2ul",
|
||||
"fp64_f2l",
|
||||
"fp64_f2ul",
|
||||
"lj_carith_divi64",
|
||||
"lj_carith_divu64",
|
||||
"lj_carith_modi64",
|
||||
"lj_carith_modu64",
|
||||
"lj_carith_powi64",
|
||||
"lj_carith_powu64",
|
||||
"lj_cdata_newv",
|
||||
"lj_cdata_setfin",
|
||||
"strlen",
|
||||
"memcpy",
|
||||
"memset",
|
||||
"lj_vm_errno",
|
||||
"lj_carith_mul64",
|
||||
"lj_carith_shl64",
|
||||
"lj_carith_shr64",
|
||||
"lj_carith_sar64",
|
||||
"lj_carith_rol64",
|
||||
"lj_carith_ror64",
|
||||
},
|
||||
|
||||
traceerr = {
|
||||
[0]="error thrown or hook called during recording",
|
||||
"trace too short",
|
||||
"trace too long",
|
||||
"trace too deep",
|
||||
"too many snapshots",
|
||||
"blacklisted",
|
||||
"retry recording",
|
||||
"NYI: bytecode %d",
|
||||
"leaving loop in root trace",
|
||||
"inner loop in root trace",
|
||||
"loop unroll limit reached",
|
||||
"bad argument type",
|
||||
"JIT compilation disabled for function",
|
||||
"call unroll limit reached",
|
||||
"down-recursion, restarting",
|
||||
"NYI: unsupported variant of FastFunc %s",
|
||||
"NYI: return to lower frame",
|
||||
"store with nil or NaN key",
|
||||
"missing metamethod",
|
||||
"looping index lookup",
|
||||
"NYI: mixed sparse/dense table",
|
||||
"symbol not in cache",
|
||||
"NYI: unsupported C type conversion",
|
||||
"NYI: unsupported C function type",
|
||||
"guard would always fail",
|
||||
"too many PHIs",
|
||||
"persistent type instability",
|
||||
"failed to allocate mcode memory",
|
||||
"machine code too long",
|
||||
"hit mcode limit (retrying)",
|
||||
"too many spill slots",
|
||||
"inconsistent register allocation",
|
||||
"NYI: cannot assemble IR instruction %d",
|
||||
"NYI: PHI shuffling too complex",
|
||||
"NYI: register coalescing too complex",
|
||||
},
|
||||
|
||||
ffnames = {
|
||||
[0]="Lua",
|
||||
"C",
|
||||
"assert",
|
||||
"type",
|
||||
"next",
|
||||
"pairs",
|
||||
"ipairs_aux",
|
||||
"ipairs",
|
||||
"getmetatable",
|
||||
"setmetatable",
|
||||
"getfenv",
|
||||
"setfenv",
|
||||
"rawget",
|
||||
"rawset",
|
||||
"rawequal",
|
||||
"unpack",
|
||||
"select",
|
||||
"tonumber",
|
||||
"tostring",
|
||||
"error",
|
||||
"pcall",
|
||||
"xpcall",
|
||||
"loadfile",
|
||||
"load",
|
||||
"loadstring",
|
||||
"dofile",
|
||||
"gcinfo",
|
||||
"collectgarbage",
|
||||
"newproxy",
|
||||
"print",
|
||||
"coroutine.status",
|
||||
"coroutine.running",
|
||||
"coroutine.isyieldable",
|
||||
"coroutine.create",
|
||||
"coroutine.yield",
|
||||
"coroutine.resume",
|
||||
"coroutine.wrap_aux",
|
||||
"coroutine.wrap",
|
||||
"math.abs",
|
||||
"math.floor",
|
||||
"math.ceil",
|
||||
"math.sqrt",
|
||||
"math.log10",
|
||||
"math.exp",
|
||||
"math.sin",
|
||||
"math.cos",
|
||||
"math.tan",
|
||||
"math.asin",
|
||||
"math.acos",
|
||||
"math.atan",
|
||||
"math.sinh",
|
||||
"math.cosh",
|
||||
"math.tanh",
|
||||
"math.frexp",
|
||||
"math.modf",
|
||||
"math.log",
|
||||
"math.atan2",
|
||||
"math.pow",
|
||||
"math.fmod",
|
||||
"math.ldexp",
|
||||
"math.min",
|
||||
"math.max",
|
||||
"math.random",
|
||||
"math.randomseed",
|
||||
"bit.tobit",
|
||||
"bit.bnot",
|
||||
"bit.bswap",
|
||||
"bit.lshift",
|
||||
"bit.rshift",
|
||||
"bit.arshift",
|
||||
"bit.rol",
|
||||
"bit.ror",
|
||||
"bit.band",
|
||||
"bit.bor",
|
||||
"bit.bxor",
|
||||
"bit.tohex",
|
||||
"string.byte",
|
||||
"string.char",
|
||||
"string.sub",
|
||||
"string.rep",
|
||||
"string.reverse",
|
||||
"string.lower",
|
||||
"string.upper",
|
||||
"string.dump",
|
||||
"string.find",
|
||||
"string.match",
|
||||
"string.gmatch_aux",
|
||||
"string.gmatch",
|
||||
"string.gsub",
|
||||
"string.format",
|
||||
"table.maxn",
|
||||
"table.insert",
|
||||
"table.concat",
|
||||
"table.sort",
|
||||
"table.new",
|
||||
"table.clear",
|
||||
"io.method.close",
|
||||
"io.method.read",
|
||||
"io.method.write",
|
||||
"io.method.flush",
|
||||
"io.method.seek",
|
||||
"io.method.setvbuf",
|
||||
"io.method.lines",
|
||||
"io.method.__gc",
|
||||
"io.method.__tostring",
|
||||
"io.open",
|
||||
"io.popen",
|
||||
"io.tmpfile",
|
||||
"io.close",
|
||||
"io.read",
|
||||
"io.write",
|
||||
"io.flush",
|
||||
"io.input",
|
||||
"io.output",
|
||||
"io.lines",
|
||||
"io.type",
|
||||
"os.execute",
|
||||
"os.remove",
|
||||
"os.rename",
|
||||
"os.tmpname",
|
||||
"os.getenv",
|
||||
"os.exit",
|
||||
"os.clock",
|
||||
"os.date",
|
||||
"os.time",
|
||||
"os.difftime",
|
||||
"os.setlocale",
|
||||
"debug.getregistry",
|
||||
"debug.getmetatable",
|
||||
"debug.setmetatable",
|
||||
"debug.getfenv",
|
||||
"debug.setfenv",
|
||||
"debug.getinfo",
|
||||
"debug.getlocal",
|
||||
"debug.setlocal",
|
||||
"debug.getupvalue",
|
||||
"debug.setupvalue",
|
||||
"debug.upvalueid",
|
||||
"debug.upvaluejoin",
|
||||
"debug.sethook",
|
||||
"debug.gethook",
|
||||
"debug.debug",
|
||||
"debug.traceback",
|
||||
"jit.on",
|
||||
"jit.off",
|
||||
"jit.flush",
|
||||
"jit.status",
|
||||
"jit.security",
|
||||
"jit.attach",
|
||||
"jit.util.funcinfo",
|
||||
"jit.util.funcbc",
|
||||
"jit.util.funck",
|
||||
"jit.util.funcuvname",
|
||||
"jit.util.traceinfo",
|
||||
"jit.util.traceir",
|
||||
"jit.util.tracek",
|
||||
"jit.util.tracesnap",
|
||||
"jit.util.tracemc",
|
||||
"jit.util.traceexitstub",
|
||||
"jit.util.ircalladdr",
|
||||
"jit.opt.start",
|
||||
"jit.profile.start",
|
||||
"jit.profile.stop",
|
||||
"jit.profile.dumpstack",
|
||||
"ffi.meta.__index",
|
||||
"ffi.meta.__newindex",
|
||||
"ffi.meta.__eq",
|
||||
"ffi.meta.__len",
|
||||
"ffi.meta.__lt",
|
||||
"ffi.meta.__le",
|
||||
"ffi.meta.__concat",
|
||||
"ffi.meta.__call",
|
||||
"ffi.meta.__add",
|
||||
"ffi.meta.__sub",
|
||||
"ffi.meta.__mul",
|
||||
"ffi.meta.__div",
|
||||
"ffi.meta.__mod",
|
||||
"ffi.meta.__pow",
|
||||
"ffi.meta.__unm",
|
||||
"ffi.meta.__tostring",
|
||||
"ffi.meta.__pairs",
|
||||
"ffi.meta.__ipairs",
|
||||
"ffi.clib.__index",
|
||||
"ffi.clib.__newindex",
|
||||
"ffi.clib.__gc",
|
||||
"ffi.callback.free",
|
||||
"ffi.callback.set",
|
||||
"ffi.cdef",
|
||||
"ffi.new",
|
||||
"ffi.cast",
|
||||
"ffi.typeof",
|
||||
"ffi.typeinfo",
|
||||
"ffi.istype",
|
||||
"ffi.sizeof",
|
||||
"ffi.alignof",
|
||||
"ffi.offsetof",
|
||||
"ffi.errno",
|
||||
"ffi.string",
|
||||
"ffi.copy",
|
||||
"ffi.fill",
|
||||
"ffi.abi",
|
||||
"ffi.metatype",
|
||||
"ffi.gc",
|
||||
"ffi.load",
|
||||
"buffer.method.free",
|
||||
"buffer.method.reset",
|
||||
"buffer.method.skip",
|
||||
"buffer.method.set",
|
||||
"buffer.method.put",
|
||||
"buffer.method.putf",
|
||||
"buffer.method.get",
|
||||
"buffer.method.putcdata",
|
||||
"buffer.method.reserve",
|
||||
"buffer.method.commit",
|
||||
"buffer.method.ref",
|
||||
"buffer.method.encode",
|
||||
"buffer.method.decode",
|
||||
"buffer.method.__gc",
|
||||
"buffer.method.__tostring",
|
||||
"buffer.method.__len",
|
||||
"buffer.new",
|
||||
"buffer.encode",
|
||||
"buffer.decode",
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
----------------------------------------------------------------------------
|
||||
-- LuaJIT profiler zones.
|
||||
--
|
||||
-- Copyright (C) 2005-2023 Mike Pall. All rights reserved.
|
||||
-- Released under the MIT license. See Copyright Notice in luajit.h
|
||||
----------------------------------------------------------------------------
|
||||
--
|
||||
-- This module implements a simple hierarchical zone model.
|
||||
--
|
||||
-- Example usage:
|
||||
--
|
||||
-- local zone = require("jit.zone")
|
||||
-- zone("AI")
|
||||
-- ...
|
||||
-- zone("A*")
|
||||
-- ...
|
||||
-- print(zone:get()) --> "A*"
|
||||
-- ...
|
||||
-- zone()
|
||||
-- ...
|
||||
-- print(zone:get()) --> "AI"
|
||||
-- ...
|
||||
-- zone()
|
||||
--
|
||||
----------------------------------------------------------------------------
|
||||
|
||||
local remove = table.remove
|
||||
|
||||
return setmetatable({
|
||||
flush = function(t)
|
||||
for i=#t,1,-1 do t[i] = nil end
|
||||
end,
|
||||
get = function(t)
|
||||
return t[#t]
|
||||
end
|
||||
}, {
|
||||
__call = function(t, zone)
|
||||
if zone then
|
||||
t[#t+1] = zone
|
||||
else
|
||||
return (assert(remove(t), "empty zone stack"))
|
||||
end
|
||||
end
|
||||
})
|
||||
|
||||