Initial LegendOfZeldaALinkToThePastSNESRecomp scaffold

This commit is contained in:
setup committed 2026-10-30 11:35:37 +00:00
commit 0cd0e5555c
24 files changed
+1253

No files matched your search

+375
View File
@@ -0,0 +1,375 @@
# Build the Legend of Zelda: A Link to the Past SETUP PACK for Linux, Windows, macOS
# arm64 and macOS Intel, tag the commit, and publish the four zips as a
# GitHub Release.
#
# HUMAN-TRIGGERED ONLY. There is no push, pull_request or schedule trigger and
# there must never be one: a run is a release decision, made by a person on
# the Actions page. Releases across the port set are driven from the studio
# bulk tool, and a per-push run would compete with it for minutes and for the
# meaning of "did CI pass".
#
# WHAT A RUN DOES, with the defaults:
# 1. Picks the next version: the latest v* tag with its patch number bumped
# (or VERSION when there is no tag yet, or whatever `version` says).
# 2. Builds the setup host for all four platforms with that version as the
# lobby pin, and packages it.
# 3. Tags the commit the run was started from as vX.Y.Z and publishes a
# public GitHub Release with the four zips. Uncheck publish_release for a
# dry run that only uploads workflow artifacts.
#
# WHAT THIS BUILDS. Not the game. src/gen/*.c is recompiler output derived
# from ROM bytes: never committed, never in CI, never in a zip. What CI can
# honestly produce is the SETUP HOST -- the same executable built with
# -DSNESRECOMP_SETUP_HOST=ON, i.e. without recompiled code -- packaged with
# the recompiler and this source tree. On the player's machine the launcher
# takes their own ROM, generates, rebuilds, and relaunches into the real game.
#
# THE TOOLCHAIN. Each pack is built with the retcomm-toolchains cmake-clang-v1
# pack, because that is what the player's own rebuild uses: if it cannot build
# this game, CI is where that must fail. By default the pack is embedded in
# the zip (embed_toolchain) so first run downloads nothing; turn it off for a
# lean zip that fetches the pack on demand (Linux pack ~800 MB, Windows
# ~200 MB, macOS ~90 MB).
name: Release
on:
workflow_dispatch:
inputs:
publish_release:
description: 'Tag this commit and publish a public GitHub Release'
type: boolean
default: true
version:
description: 'Version to release, e.g. 0.2.0 (empty = bump the latest v* tag)'
type: string
default: ''
bump:
description: 'When version is empty, which part of the latest tag to bump'
type: choice
options: [patch, minor, major]
default: patch
embed_toolchain:
description: 'Embed the cmake-clang-v1 pack in each zip (else the launcher downloads it)'
type: boolean
default: true
toolchain_tag:
description: 'retcomm-toolchains release to build with (empty = latest)'
type: string
default: ''
verify_pins:
description: 'Fail if the submodules disagree with framework_pins.txt'
type: boolean
default: false
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
env:
EXE_NAME: LegendOfZeldaALinkToThePastSNESRecomp
ZIP_PREFIX: legendofzeldaalinktothepast
jobs:
preflight:
name: Preflight
runs-on: ubuntu-24.04
outputs:
version: ${{ steps.meta.outputs.version }}
tag: ${{ steps.meta.outputs.tag }}
publish: ${{ steps.meta.outputs.publish }}
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0
fetch-tags: true
- name: Record framework pins
run: |
set -euo pipefail
ARGS=()
if [[ "${{ inputs.verify_pins }}" == "true" ]]; then
ARGS+=(--check framework_pins.txt)
fi
bash snesrecomp/tools/ci/record_pins.sh "${ARGS[@]}"
- name: Resolve release version
id: meta
env:
IN_VERSION: ${{ inputs.version }}
IN_BUMP: ${{ inputs.bump || 'patch' }}
IN_PUBLISH: ${{ inputs.publish_release }}
run: |
set -euo pipefail
PUBLISH=true
[[ "${IN_PUBLISH}" == "false" ]] && PUBLISH=false
FILE_VER="$(tr -d '[:space:]' < VERSION)"
RAW="$(printf '%s' "${IN_VERSION}" | tr -d '[:space:]')"
LATEST="$(git tag -l 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | head -n1 || true)"
if [[ -n "${RAW}" ]]; then
VER="${RAW#v}"
echo "version given: ${VER}"
elif [[ "${GITHUB_REF}" == refs/tags/v* ]]; then
# Started from an existing tag: release exactly that.
VER="${GITHUB_REF_NAME#v}"
echo "running on tag ${GITHUB_REF_NAME}: releasing ${VER}"
elif [[ -z "${LATEST}" ]]; then
# First release: VERSION says what it is.
VER="${FILE_VER}"
echo "no v* tag yet: first release is VERSION = ${VER}"
else
BASE="${LATEST#v}"; BASE="${BASE%%[-+]*}"
IFS=. read -r MA MI PA <<<"${BASE}"
case "${IN_BUMP}" in
major) MA=$((MA + 1)); MI=0; PA=0 ;;
minor) MI=$((MI + 1)); PA=0 ;;
*) PA=$((PA + 1)) ;;
esac
VER="${MA}.${MI}.${PA}"
# A deliberate bump of VERSION in the tree outranks the automatic
# patch step, so a maintainer can move to 0.2.0 by editing the file.
if [[ "$(printf '%s\n%s\n' "${FILE_VER}" "${VER}" | sort -V | tail -n1)" == "${FILE_VER}" \
&& "${FILE_VER}" != "${BASE}" ]]; then
echo "VERSION (${FILE_VER}) is ahead of ${LATEST} bumped (${VER}); using VERSION"
VER="${FILE_VER}"
else
echo "auto-bump ${IN_BUMP}: ${LATEST} -> ${VER}"
fi
fi
if [[ ! "${VER}" =~ ^[0-9]+\.[0-9]+\.[0-9]+([.+-][A-Za-z0-9.+-]*)?$ ]]; then
echo "::error::'${VER}' is not a version (want X.Y.Z)"
exit 1
fi
TAG="v${VER}"
if [[ "${PUBLISH}" == "true" && "${GITHUB_REF}" != "refs/tags/${TAG}" ]] \
&& git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
echo "::error::tag ${TAG} already exists at $(git rev-parse --short "${TAG}"). Pass a new version, or start the run from that tag to re-release it."
exit 1
fi
echo "version=${VER}" >>"$GITHUB_OUTPUT"
echo "tag=${TAG}" >>"$GITHUB_OUTPUT"
echo "publish=${PUBLISH}" >>"$GITHUB_OUTPUT"
echo "release ${TAG} from ${GITHUB_SHA::12}; publish=${PUBLISH}"
build:
name: ${{ matrix.artifact }}
needs: preflight
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-24.04
artifact: linux-x64
- os: windows-2022
artifact: windows-x64
- os: macos-15
artifact: macos-arm64
- os: macos-15-intel
artifact: macos-x64
defaults:
run:
shell: bash
env:
# The lobby pin compiled into the binary and written into the pack's
# VERSION, so the tag, the binary and the player's own rebuild agree.
RELEASE_VERSION: ${{ needs.preflight.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 1
- name: Record framework pins
run: bash snesrecomp/tools/ci/record_pins.sh
# A setup host is defined by what it lacks. The checkout must carry no
# generated C; if it ever does, that is committed ROM-derived output and
# the build must stop here rather than package it.
- name: Assert no generated C in the tree
run: |
set -euo pipefail
if compgen -G 'src/gen/*.c' >/dev/null || [[ -f recomp/funcs.h ]]; then
echo "::error::src/gen/*.c or recomp/funcs.h is present in the checkout."
exit 1
fi
echo "src/gen is empty — building the setup host."
- name: Cache toolchain pack
uses: actions/cache@v4
with:
path: .cache/toolchain-pack
key: cmake-clang-v1-${{ matrix.artifact }}-${{ inputs.toolchain_tag || 'latest' }}
- name: Fetch toolchain pack
run: |
set -euo pipefail
ARGS=(--artifact "${{ matrix.artifact }}")
if [[ -n "${{ inputs.toolchain_tag }}" ]]; then
ARGS+=(--tag "${{ inputs.toolchain_tag }}")
fi
bash snesrecomp/tools/ci/fetch_toolchain.sh "${ARGS[@]}"
# The macOS pack is cmake + ninja + python; SDL3 comes from
# runner.cmake's pinned source fallback. Prefetch the tarball with curl
# (HTTP/1.1) and cache it, so configure does not download.
- name: Cache SDL3 source (macOS)
if: runner.os == 'macOS'
uses: actions/cache@v4
with:
path: .cache/sdl3-src
key: sdl3-src-3.4.10
- name: Prefetch SDL3 source (macOS)
if: runner.os == 'macOS'
run: bash snesrecomp/tools/ci/prefetch_sdl3.sh --out-dir .cache/sdl3-src
- name: Configure and build the setup host
run: |
set -euo pipefail
# The pack's env.sh is its own statement of how to use it (PATH,
# CC/CXX, sysroot, SDL3_DIR). Source it rather than restate it.
# shellcheck disable=SC1091
source "${TOOLCHAIN_DIR}/env.sh"
echo "cmake: $(command -v cmake) ($(cmake --version | head -n1))"
echo "cc: ${CC:-$(command -v cc)}"
EXTRA=()
if [[ -n "${SNESRECOMP_SDL3_SOURCE_DIR:-}" ]]; then
EXTRA+=("-DSNESRECOMP_SDL3_SOURCE_DIR=${SNESRECOMP_SDL3_SOURCE_DIR}")
fi
if [[ "${{ runner.os }}" == "macOS" ]]; then
EXTRA+=("-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0")
fi
JOBS="$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)"
cmake -S . -B build-ci -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DSNESRECOMP_SETUP_HOST=ON \
-DSNESRECOMP_ENABLE_TRACE=OFF \
-DSNESRECOMP_BUILD_VERSION="${RELEASE_VERSION}" \
"${EXTRA[@]+"${EXTRA[@]}"}"
cmake --build build-ci -j"${JOBS}" --target "${EXE_NAME}"
- name: Package setup pack
run: |
set -euo pipefail
# The packager inspects the executable's imports and zips the
# stage; Git Bash on Windows has neither tool, the pack does.
# shellcheck disable=SC1091
source "${TOOLCHAIN_DIR}/env.sh"
ARGS=()
if [[ "${{ inputs.embed_toolchain }}" != "false" ]]; then
ARGS+=(--embed-toolchain)
fi
bash scripts/package_release.sh build-ci "${{ matrix.artifact }}" "${ARGS[@]+"${ARGS[@]}"}"
ls -la dist/
- uses: actions/upload-artifact@v4
with:
name: ${{ env.ZIP_PREFIX }}-${{ matrix.artifact }}
path: dist/${{ env.ZIP_PREFIX }}-*-${{ matrix.artifact }}.zip
if-no-files-found: error
retention-days: 7
compression-level: 0
release:
name: Tag and publish
needs: [preflight, build]
if: needs.preflight.outputs.publish == 'true'
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0
fetch-tags: true
- uses: actions/download-artifact@v4
with:
path: artifacts
- name: Collect zips
run: |
set -euo pipefail
mkdir -p dist
find artifacts -type f -name "${ZIP_PREFIX}-*.zip" -exec cp -v {} dist/ \;
# Four platforms in, four zips out. A release that silently carries
# three is the failure this counts for.
count="$(find dist -name "${ZIP_PREFIX}-*.zip" | wc -l | tr -d ' ')"
if [[ "${count}" -ne 4 ]]; then
echo "::error::expected 4 platform zips, found ${count}"
ls -la dist
exit 1
fi
( cd dist && sha256sum "${ZIP_PREFIX}"-*.zip > SHA256SUMS )
cat dist/SHA256SUMS
# The tag is created here, after all four builds succeeded, so a failed
# build never leaves a version number claimed by nothing.
- name: Tag the commit
env:
TAG: ${{ needs.preflight.outputs.tag }}
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
echo "tag ${TAG} already exists at $(git rev-parse --short "${TAG}") (re-release from that tag)"
else
git tag -a "${TAG}" -m "Legend of Zelda: A Link to the Past ${TAG}" "${GITHUB_SHA}"
git push origin "refs/tags/${TAG}"
echo "tagged ${GITHUB_SHA::12} as ${TAG}"
fi
- name: Release notes
env:
TAG: ${{ needs.preflight.outputs.tag }}
run: |
set -euo pipefail
# From rom_identity.txt, not a copy baked into this workflow.
ROM_FILE="$(python3 snesrecomp/tools/rom_identity.py rom_identity.txt --get rom_file)"
ROM_SHA256="$(python3 snesrecomp/tools/rom_identity.py rom_identity.txt --get expected_sha256)"
{
echo "Legend of Zelda: A Link to the Past ${TAG} — setup packs"
echo
echo "These packs do not contain the game. Each holds a setup program, the"
echo "recompiler and the source; on first run it takes your own copy of"
echo "${ROM_FILE} (SHA-256 ${ROM_SHA256}), recompiles it locally, and builds"
echo "the playable game. No ROM data is included or downloaded."
echo
if [[ "${{ inputs.embed_toolchain }}" != "false" ]]; then
echo "The portable build tools (cmake-clang-v1) are embedded in every pack."
else
echo "The portable build tools (cmake-clang-v1) are downloaded on first run."
fi
echo
echo '### Framework revisions'
echo
echo '```'
bash snesrecomp/tools/ci/record_pins.sh
echo '```'
} > notes.md
cat notes.md
- name: Publish release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.preflight.outputs.tag }}
target_commitish: ${{ github.sha }}
name: "Legend of Zelda: A Link to the Past ${{ needs.preflight.outputs.tag }}"
body_path: notes.md
files: |
dist/${{ env.ZIP_PREFIX }}-*.zip
dist/SHA256SUMS
fail_on_unmatched_files: true
make_latest: true
+27
View File
@@ -0,0 +1,27 @@
# Generated recompiler output — derived from a copyrighted ROM. Never commit.
/src/gen/
/recomp/funcs.h
# The ROM itself, in every form. Players supply their own.
*.sfc
*.smc
*.srm
# Build trees and packaged output
/build/
/build-*/
/dist/
/saves/
# Local probe output that names paths on one machine
/rom_probe.json
# Launcher/runtime state written beside the executable (and in the repo root
# when the game is run from there): the cached ROM path, keybinds, settings.
# All of it is one machine's, none of it is the project's.
/rom.cfg
/keybinds.ini
/config.ini
/input.ini
.DS_Store
+8
View File
@@ -0,0 +1,8 @@
[submodule "snesrecomp"]
path = snesrecomp
url = https://github.com/RetroPortingToolKit/snesrecomp.git
branch = main
[submodule "recomp-ui"]
path = recomp-ui
url = https://github.com/mstan/recomp-ui.git
branch = master
+136
View File
@@ -0,0 +1,136 @@
cmake_minimum_required(VERSION 3.20)
project(LegendOfZeldaALinkToThePastSNESRecomp C CXX)
set(CMAKE_C_STANDARD 11)
# Release pin for lobby matching. Release builds stamp the VERSION file so two
# peers can prove they are the same build; everything else stamps "dev".
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/VERSION" _GAME_VER_RAW)
string(STRIP "${_GAME_VER_RAW}" GAME_RELEASE_VERSION)
set(SNESRECOMP_BUILD_VERSION "" CACHE STRING
"Lobby release pin (empty = Release uses VERSION, else dev)")
if(SNESRECOMP_BUILD_VERSION STREQUAL "")
get_property(_is_multi GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
if(_is_multi)
set(SNESRECOMP_BUILD_VERSION "$<IF:$<CONFIG:Release>,${GAME_RELEASE_VERSION},dev>")
elseif(CMAKE_BUILD_TYPE STREQUAL "Release")
set(SNESRECOMP_BUILD_VERSION "${GAME_RELEASE_VERSION}")
else()
set(SNESRECOMP_BUILD_VERSION "dev")
endif()
endif()
set(SNESRECOMP_ROOT "${CMAKE_SOURCE_DIR}/snesrecomp" CACHE PATH
"Path to the snesrecomp checkout used by this game")
if(NOT EXISTS "${SNESRECOMP_ROOT}/runner/runner.cmake")
message(FATAL_ERROR
"snesrecomp submodule missing at ${SNESRECOMP_ROOT}.\n"
"Run: git submodule update --init --recursive")
endif()
# Mod packages are part of every project, not an option. The launcher's Mods
# page, the .snesmod loader, the trusted-plugin registry and netplay's mod-set
# gate all hang off this one switch; a build without it cannot exchange mods
# with a peer that has them, and the framework says so at every lobby init.
# Must precede the include: runner.cmake reads it to add the loader sources.
set(SNESRECOMP_ENABLE_MODS ON CACHE BOOL
"Mod package loader + trusted plugins (required for every title)" FORCE)
include(${SNESRECOMP_ROOT}/runner/runner.cmake)
add_executable(LegendOfZeldaALinkToThePastSNESRecomp
${SNESRECOMP_RUNNER_SOURCES}
src/main.c
src/host_contract.c
src/game_rtl.c
src/gen_stubs.c
)
# ROM identity. rom_identity.txt is the one place the digests live: the build
# turns it into snesrecomp_rom_identity.h (SNESRECOMP_ROM_EXPECTED_SHA256 and
# friends) for src/main.c, and tools/regen.sh, scripts/package_release.sh and
# the release workflow read the same file. A revision bump is one edit there.
snesrecomp_rom_identity(LegendOfZeldaALinkToThePastSNESRecomp ${CMAKE_SOURCE_DIR}/rom_identity.txt)
# Generated recompiler output. Not committed: it is derived from a copyrighted
# ROM, so every developer regenerates it locally from their own copy -- and a
# release does not ship it either. With -DSNESRECOMP_SETUP_HOST=ON and an empty
# src/gen/, this builds the SETUP HOST instead: the same executable minus the
# recompiled code, whose only path is the launcher's Generate & rebuild wizard.
# That is what release zips contain; the player's machine produces the
# playable binary from their own ROM. Without the option an empty src/gen/ is
# still a hard error (the framework owns the message).
snesrecomp_target_generated_code(LegendOfZeldaALinkToThePastSNESRecomp ${CMAKE_SOURCE_DIR}/src/gen)
# The mod catalog. This names WHERE the packages are; the framework owns the
# layout they are staged into beside the executable, and stages them on every
# build OF THE EXECUTABLE -- including the launcher's own Generate & rebuild on
# a player's machine, which builds only this target and would skip a sibling
# ALL target. mods/preloaded/packages/ holds reviewed, default-disabled
# packages; players install .snesmod archives through the launcher.
# snesrecomp/docs/MOD_PACKAGES.md.
#
# It used to be `snesrecomp_target_stage_dir(... ${CMAKE_SOURCE_DIR}/mods mods)`
# -- the destination spelled per title, which is how three ports in the same
# workspace ended up shipping an empty Mods page and nothing said so. Declaring
# it is now also enforced: a project with packages that never calls this fails
# to configure.
snesrecomp_target_mod_catalog(LegendOfZeldaALinkToThePastSNESRecomp ${CMAKE_SOURCE_DIR}/mods/preloaded)
# Any other directory the game reads exe-relative (a translations/ table, ...)
# is staged with snesrecomp_target_stage_dir, never with a separate ALL target.
target_include_directories(LegendOfZeldaALinkToThePastSNESRecomp PRIVATE
${SNESRECOMP_RUNNER_INCLUDE_DIRS}
# common_rtl.c includes "config.h": the desktop key-map / Config contract
# the framework host supplies (snesrecomp_target_desktop_host below).
${SNESRECOMP_ROOT}/runner/src/desktop
${CMAKE_SOURCE_DIR}/src
${CMAKE_SOURCE_DIR}/recomp
)
target_link_libraries(LegendOfZeldaALinkToThePastSNESRecomp PRIVATE ${SNESRECOMP_RUNNER_LIBRARIES})
target_compile_definitions(LegendOfZeldaALinkToThePastSNESRecomp PRIVATE
SNES_GAME_VERSION="${SNESRECOMP_BUILD_VERSION}"
SNES_GAME_TITLE="Legend of Zelda: A Link to the Past"
)
snesrecomp_target_sdl(LegendOfZeldaALinkToThePastSNESRecomp)
# The desktop host: launcher flow, ROM resolution, config.ini + [KeyMap],
# window + SDL/OpenGL presenters, audio, gamepads, the save-state browser and
# rewind filmstrip, the pacing clock, crash handlers and the post-mortem
# report. src/main.c is the shim that names this title and supplies its
# hooks; the host is the framework's and improves on a submodule pull.
# Add TIER2 once this port reads the interpreter-gap manifest after each run.
snesrecomp_target_desktop_host(LegendOfZeldaALinkToThePastSNESRecomp)
# rom.cfg (the ROM the wizard set this project up from; git-ignored) is
# staged beside the executable when none is there yet, so the first launch
# opens on that dump rather than asking for one.
snesrecomp_target_rom_cache(LegendOfZeldaALinkToThePastSNESRecomp)
# Shared Dear ImGui pre-boot launcher (ROM picker, verification, display /
# audio / input settings). Self-contained: it brings its own ImGui, GL link and
# staged assets, and defines RECOMP_LAUNCHER so src/main.c compiles its GUI path.
set(RECOMP_UI_ROOT "${CMAKE_SOURCE_DIR}/recomp-ui" CACHE PATH
"Root directory of the recomp-ui launcher repo" FORCE)
if(NOT EXISTS "${RECOMP_UI_ROOT}/recomp_ui.cmake")
message(FATAL_ERROR
"recomp-ui missing at ${RECOMP_UI_ROOT}.\n"
"Run: git submodule update --init --recursive recomp-ui")
endif()
include(${RECOMP_UI_ROOT}/recomp_ui.cmake)
# BOXART stages launcher_assets/img/boxart.tga beside the exe; the launcher
# shows assets/img/boxart.tga by default. EXISTS-guarded in recomp_ui.cmake,
# so this configures cleanly before any art is fetched.
recomp_target_launcher_ui(LegendOfZeldaALinkToThePastSNESRecomp CONSOLE snes
BOXART "${CMAKE_SOURCE_DIR}/launcher_assets/img/boxart.tga")
# Generate & rebuild from inside the launcher: pick ROM -> recompile locally
# -> cmake --build -> relaunch. The implementation is the framework's; this
# project contributes no configuration (snesrecomp_codegen_host_autowire in
# src/main.c). recomp-ui/src is on the include path because the host speaks
# RecompLauncherCGameInfo.
target_sources(LegendOfZeldaALinkToThePastSNESRecomp PRIVATE
${SNESRECOMP_ROOT}/host/snesrecomp_codegen_host.c)
target_include_directories(LegendOfZeldaALinkToThePastSNESRecomp PRIVATE
${SNESRECOMP_ROOT}/host
${RECOMP_UI_ROOT}/src)
# Netplay is not built into this target.
# No multitap: two seats, one controller per port.
+101
View File
@@ -0,0 +1,101 @@
PolyForm Noncommercial License 1.0.0
<https://polyformproject.org/licenses/noncommercial/1.0.0>
Copyright (c) 2026 Matthew Stanley
## Acceptance
In order to get any license under these terms, you must agree to them as
both strict obligations and conditions to all your licenses.
## Copyright License
The licensor grants you a copyright license for the software to do
everything you might otherwise need the licensor's permission to do. Your
license covers distributing the software, making changes and new works
based on it, and all other uses of the software, subject to the
limitations and conditions in these terms.
## Noncommercial Purposes
Any noncommercial purpose is a permitted purpose.
## Personal Uses
Personal use for research, experiment, and testing for the benefit of
public knowledge, personal study, private entertainment, hobby projects,
amateur pursuits, or religious observance, without any anticipated
commercial application, counts as use for a permitted purpose.
## Noncommercial Organizations
Use by any charitable organization, educational institution, public
research organization, public safety or health organization, environmental
protection organization, or government institution counts as use for a
permitted purpose regardless of the source of funding or obligations
resulting from the funding.
## Fair Use
You may have "fair use" rights for the software under the law. These
terms do not limit them.
## No Other Rights
These terms do not allow you to sublicense or transfer any of your
licenses to anyone else, or prevent the licensor from granting licenses to
anyone else. These terms do not imply any other licenses.
## Patent Defense
If you make any written claim that the software infringes or contributes
to infringement of any patent, your patent license for the software granted
under these terms ends immediately. If your company makes such a claim,
your patent license ends immediately for work on behalf of your company.
## Violations
The first time you are notified in writing that you have violated any of
these terms, or done anything with the software not covered by your
licenses, your licenses can nonetheless continue if you come into full
compliance with these terms, and take practical steps to correct past
violations, within 32 days of receiving notice. Otherwise, all your
licenses end immediately.
## No Liability
As far as the law allows, the software comes as is, without any warranty
or condition, and the licensor will not be liable to you for any damages
arising out of these terms or the use or nature of the software, under any
kind of legal claim.
## Definitions
The **licensor** is the individual or entity offering these terms, and the
**software** is the software the licensor makes available under these
terms.
**You** refers to the individual or entity agreeing to these terms.
**Your company** is any legal entity, sole proprietorship, or other kind
of organization that you work for, plus all organizations that have control
over, are under the control of, or are under common control with that
organization. **Control** means ownership of substantially all the assets
of an entity, or the power to direct its management and policies by vote,
contract, or otherwise. Control can be direct or indirect.
**Your licenses** are all the licenses granted to you for the software
under these terms.
**Use** means anything you do with the software requiring one of your
licenses.
---
For the avoidance of doubt, the licensor's intent is to restrict uses where
profit is derived from this software. Non-profit personal, educational,
or community use is welcome regardless of organizational context.
For commercial licensing inquiries, contact the licensor, Matthew Stanley,
at <https://1379.tech>.
+115
View File
@@ -0,0 +1,115 @@
# Legend of Zelda: A Link to the Past Recomp
A native recompilation of **Legend of Zelda: A Link to the Past** (USA), built on
[snesrecomp](https://github.com/RetroPortingToolKit/snesrecomp).
_Add a short description here._
> **You must legally own a copy of the game.** No ROM data is distributed with
> this project, in the repository or in any release. The recompiled C is
> generated locally from your own copy and is never committed.
## Status
Scaffolded on 2026-10-30 — **not yet a working port.** The layout, build,
regeneration pipeline, CI, and packaging are wired up; the game does not run
until the host work in `src/game_rtl.c` is done. See
[Porting from here](#porting-from-here).
## ROM identity
| | |
|---|---|
| File | `Legend of Zelda, The - A Link to the Past (USA).sfc` |
| Publisher | — |
| Developer | — |
| Year | — |
| Mapping | lorom |
| Region | USA (North America) |
| Coprocessor | none |
| CRC32 | `777aac2f` |
| SHA-256 | `66871d66be19ad2c34c927d6b14cd8eb6fc3181965b6e517cb361f7316009cfb` |
`tools/regen.sh` refuses to run against anything else, so a mismatched
revision fails immediately instead of producing subtly wrong output.
## Build
```sh
git submodule update --init --recursive
bash tools/regen.sh --rom /path/to/Legend of Zelda, The - A Link to the Past (USA).sfc
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j
```
The ROM does not have to live in the repository — keeping it on your own
drive is the better habit, and `SNESRECOMP_ROM` sets the path once for a
shell. Without either, `tools/regen.sh` finds `Legend of Zelda, The - A Link to the Past (USA).sfc` at the repo root,
then the path the setup wizard recorded in `rom.cfg`; `.gitignore` blocks
both from ever being committed.
`tools/regen.sh` verifies the ROM, generates `src/gen/*.c`, and re-syncs
`recomp/funcs.h`. Re-run it whenever you change anything under `recomp/`.
## Run
```sh
./build/LegendOfZeldaALinkToThePastSNESRecomp # launcher picks the ROM
./build/LegendOfZeldaALinkToThePastSNESRecomp /path/to/Legend of Zelda, The - A Link to the Past (USA).sfc # or name it and skip the launcher
```
With no ROM on the command line the build opens the recomp-ui launcher: a ROM
picker with a verification badge, plus display / audio / input settings. Your
choice is cached in `rom.cfg` beside the executable, so the next launch opens
on it and "Skip launcher on boot" makes it immediate. If the launcher is not
built in (`--no-recomp-ui`), the host falls back to a native file picker.
Either way the ROM is checked against the digests above before anything boots
— a dump that could not have produced this build is refused at the door rather
than mis-executing ten frames in.
## Layout
| Path | What lives there |
|---|---|
| `recomp/` | Analysis input: `bank*.cfg`, `symbols.toml`, generated `funcs.h` |
| `rom_identity.txt` | ROM digests — read by the build, `tools/regen.sh` and CI |
| `src/` | Host code you own: `main.c`, `game_rtl.c` |
| `src/gen/` | Generated C. Never committed — regenerate locally |
| `snesrecomp/` | Framework submodule (owns `lib/recomp-net`, `lib/retcomm-rbengine`) |
| `tools/` | `regen.sh` — the ROM → C pipeline |
| `scripts/` | `package_release.sh` — player-facing zip |
| `framework_pins.txt` | Exact framework commits this project was scaffolded against |
## Porting from here
The scaffold stops where the game-specific work starts. In rough order:
1. **Make it boot.** `src/game_rtl.c` holds the frame driver. It starts on
the framework's beam-aligned driver (`snesrecomp/runner/src/beam_frame_driver.h`):
one frame per PPU field, interrupts taken where the beam latches them,
the field rasterized from the raster journal. A title whose main loop it
does not fit replaces those calls with its own driver.
2. **Name things.** Add entries to `recomp/symbols.toml` as you identify
routines, then re-run `tools/regen.sh`. Set `emit = true` to promote one
into ahead-of-time analysis; leave it false to keep it interpreted.
Regen synchronizes the marked blocks in each bank cfg, creating missing
configs. Only proven variants become AOT code. See `recomp/README.md`.
3. **Resolve dispatch misses.** After every run, deal with unresolved
indirect targets before anything else — they are the reason a port
diverges, and they are cheap to fix early.
4. **Never synthesise a result** to get past uncovered code, and never edit
`src/gen/` by hand. Fix the config or the framework and regenerate.
## Multiplayer
Single player.
## License
This project's own source is under the license in `LICENSE`. The framework
carries its own terms — see `snesrecomp/LICENSE` and
`snesrecomp/THIRD_PARTY_ATTRIBUTION.md`. Neither covers the game data, which
is not distributed here.
+1
View File
@@ -0,0 +1 @@
0.1.0
+4
View File
@@ -0,0 +1,4 @@
snesrecomp=ef0b3ff7d6965ffcd96d8fd917cb182539b05d76
recomp-ui=3f1d6f122a58823614abce14f5b86f4d165a629c
recomp-net=8c76fb643339144a3a84d5ba8f5defe6250dcd26
retcomm-rbengine=2a03e73693acee0fb78076ea058642c931bef12e
+22
View File
@@ -0,0 +1,22 @@
# Preloaded mods
Ship reviewed, default-disabled packages here:
```text
packages/<package-id>/<version>/
manifest.toml
...
```
A manifest's `[[target]]` names this title as `game_id = "legendofzeldaalinktothepast-usa"` with the
ROM's SHA-256 (both live in `rom_identity.txt`). CMakeLists.txt declares this
directory with `snesrecomp_target_mod_catalog` and the FRAMEWORK stages
`packages/` beside the executable on every build -- do not add a copy step of
your own, and do not spell the destination: it belongs to snesrecomp, so a
future change to the layout touches one file instead of every port. Nothing
placed beside the executable by hand survives a build.
Players install `.snesmod` archives through the launcher's Mods page, which
the runtime keeps under its own state beside the executable.
See `snesrecomp/docs/MOD_PACKAGES.md` for the manifest format and the
trusted-plugin registration a package can activate.
View File
Whitespace-only changes.
Submodule
+1
Submodule recomp-ui added at 3f1d6f122a.
+35
View File
@@ -0,0 +1,35 @@
# recomp/
Analysis input for Legend of Zelda: A Link to the Past. `tools/regen.sh` reads this directory and
writes `src/gen/`.
| File | Owner | Notes |
|---|---|---|
| `bank*.cfg` | you, plus generated blocks | Per-bank analysis directives |
| `symbols.toml` | you | Progressive name map; the source of truth |
| `funcs.h` | generated | Re-synced by `tools/regen.sh` — do not hand-edit |
The block between `>>> BEGIN symbols.toml` and `<<< END symbols.toml` in
each bank cfg is rewritten from `symbols.toml` on every regen. Edit
`symbols.toml`, not the block.
Python 3.11+ includes the TOML reader. On older Python, install it with
`python -m pip install tomli`. Projects without `symbols.toml` do not need it.
Each `[[func]]` has a C identifier `name`, a 16-bit hexadecimal `addr`
(for example, `"8000"`), and an integer `bank` (for example, `0xc0`).
Missing bank configs are created automatically; hand-written directives outside
the generated blocks are preserved. Removing an entry also removes its generated
directives on the next regen.
- `emit = false` (the default) writes a `symbol` label and `force_lle` boundary:
it stays interpreted, with no generated `func` declaration in `funcs.h`.
- `emit = true` writes a `func` declaration and requests analysis from that
entry, even without `--cfg-roots`. Only variants the analyzer proves safe
become AOT code; others still use the interpreter. Optional `entry_m` and
`entry_x` select the initial register-width flags (0 or 1, both default to 1).
An unchanged cfg file is not by itself evidence of failed code generation:
`src/gen/bank*_v2.c` contains code discovered from the ROM, while `bank*.cfg`
contains analysis inputs. Discovery does not copy every reached function back
into the configs.
+11
View File
@@ -0,0 +1,11 @@
# Legend of Zelda: A Link to the Past (USA) — LLE-first seed
# Regenerate with tools/regen.sh. Promote functions by editing recomp/symbols.toml
# and re-running the regen script, which rewrites the block below.
bank = 0
auto_vectors
tier_down_stubs
# Reset vector $8000, native NMI $80c9 (from the cartridge header).
# >>> BEGIN symbols.toml (generated — do not edit)
# <<< END symbols.toml
+21
View File
@@ -0,0 +1,21 @@
# Progressive symbol map for Legend of Zelda: A Link to the Past.
#
# Each [[func]] names an address the analyzer should treat as a function.
# Set emit = true to promote one into ahead-of-time codegen; leave it false
# to keep it interpreted. tools/regen.sh syncs labels and tier choices into
# each recomp/bankNN.cfg; only emit=true adds a func/funcs.h declaration.
# Do not hand-edit the generated blocks in the bank configs.
[[func]]
name = "I_RESET"
addr = "8000"
bank = 0
emit = false
note = "Emulation RESET vector ($FFFC)"
[[func]]
name = "I_NMI"
addr = "80c9"
bank = 0
emit = false
note = "Native NMI vector ($FFEA)"
+26
View File
@@ -0,0 +1,26 @@
# ROM identity for Legend of Zelda: A Link to the Past — the single source of truth.
#
# Read by three consumers, so a revision change is made once, here:
# - the build (CMake generates snesrecomp_rom_identity.h from this file)
# - tools/regen.sh
# - .github/workflows/release.yml
#
# Format is `key = value`, one per line; `#` starts a comment. Values may be
# quoted. Leaving a digest empty means the build cannot verify a ROM it is
# handed — it will say so rather than accept it.
display_name = Legend of Zelda: A Link to the Past
rom_file = Legend of Zelda, The - A Link to the Past (USA).sfc
# Every digest of the bare image (copier header stripped), so the Retro
# catalog can take a submission's identity from this file rather than ask
# the submitter to hash the ROM again. The build verifies on crc32/sha256.
expected_crc32 = 777aac2f
expected_md5 = 608c22b8ff930c62dc2de54bcd6eba72
expected_sha1 = 6d4f10a8b10e10dbe624cb23cf03b88bb8252973
expected_sha256 = 66871d66be19ad2c34c927d6b14cd8eb6fc3181965b6e517cb361f7316009cfb
rom_size = 1048576
mapping = lorom
region = USA
# What a mod package's [[target]] game_id must name to apply to this title
# (snesrecomp/docs/MOD_PACKAGES.md). Stable across revisions on purpose.
game_id = legendofzeldaalinktothepast-usa
+71
View File
@@ -0,0 +1,71 @@
#!/usr/bin/env bash
# Package a Legend of Zelda: A Link to the Past release: a SETUP PACK.
#
# The zip does not contain the game. It contains a setup program (the host
# built with -DSNESRECOMP_SETUP_HOST=ON, i.e. without recompiled code), the
# recompiler and this source tree. On first run the player picks their own
# ROM and the launcher recompiles and rebuilds locally. No ROM, no src/gen,
# no recomp/funcs.h ever leaves this tree in a zip -- the framework's packager
# enforces that mechanically.
#
# Usage:
# scripts/package_release.sh [<build-dir>] [<platform-tag>] [--embed-toolchain]
#
# Both positionals are optional; BUILD_DIR and PLATFORM still work as env vars.
# Defaults: build, and the host's own <os>-<arch>. --embed-toolchain copies
# the cmake-clang-v1 pack named by RETCOMM_TOOLCHAIN_DIR into the zip so the
# player downloads nothing.
#
# Writes: dist/legendofzeldaalinktothepast-<VERSION>-<platform-tag>.zip
set -euo pipefail
# A silent exit is the worst outcome a packaging step can have: the first
# Windows CI run died here with exit 1 and not one line of output. Name the
# command and line instead of leaving the reader to bisect a shell script.
trap 'echo "::error::$(basename "${BASH_SOURCE[0]}") failed at line ${LINENO}: ${BASH_COMMAND}" >&2' ERR
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$ROOT"
EXTRA=()
POS=()
for a in "$@"; do
case "$a" in
--embed-toolchain|--no-embed-toolchain) EXTRA+=("$a") ;;
*) POS+=("$a") ;;
esac
done
BUILD_DIR="${POS[0]:-${BUILD_DIR:-build}}"
PLATFORM="${POS[1]:-${PLATFORM:-$(uname -s | tr '[:upper:]' '[:lower:]')-$(uname -m)}}"
IMPL="snesrecomp/tools/ci/stage_setup_host.sh"
if [ ! -f "$IMPL" ]; then
echo "package_release.sh: $IMPL missing — the snesrecomp pin predates setup packs." >&2
echo " Update the submodule: git submodule update --init --recursive" >&2
exit 1
fi
# The ROM hint and digests come from rom_identity.txt -- the same file the
# build compiles in and tools/regen.sh verifies against -- so a revision bump
# is one edit. stage_setup_host.sh already needs python3, so this does too.
IDENTITY_TOOL="snesrecomp/tools/rom_identity.py"
ROM_FILE="$(python3 "$IDENTITY_TOOL" rom_identity.txt --get rom_file)"
ROM_SHA256="$(python3 "$IDENTITY_TOOL" rom_identity.txt --get expected_sha256)"
ROM_CRC32="$(python3 "$IDENTITY_TOOL" rom_identity.txt --get expected_crc32)"
# mods/ is the catalog the build stages beside the executable, and the package
# a netplay host transfers to a guest. Add --runtime-dir <name> for any other
# directory the game loads exe-relative (translations, ...).
exec bash "$IMPL" \
--runtime-dir mods \
--build-dir "$BUILD_DIR" \
--artifact "$PLATFORM" \
--exe-name LegendOfZeldaALinkToThePastSNESRecomp \
--zip-prefix legendofzeldaalinktothepast \
--display-name "Legend of Zelda: A Link to the Past" \
--rom-hint "$ROM_FILE" \
--rom-sha256 "$ROM_SHA256" \
--rom-crc32 "$ROM_CRC32" \
"${EXTRA[@]+"${EXTRA[@]}"}"
Submodule
+1
Submodule snesrecomp added at ef0b3ff7d6.
+65
View File
@@ -0,0 +1,65 @@
/*
* Legend of Zelda: A Link to the Past — per-game runtime glue.
*
* THIS FILE IS THE PORT. Everything else the scaffold produced is layout,
* build wiring, and packaging; this is where the actual work happens.
*
* The framework does not, and cannot, drive an arbitrary SNES game on its
* own. Two responsibilities always land on the game host:
*
* 1. Deciding what "one frame" means for THIS title, and returning from
* run_frame() at that boundary. A real ROM's reset vector never
* returns — it enters a main loop that waits on vblank — so somebody
* has to choose the yield point.
* 2. Delivering NMI/IRQ at the hardware edge (see
* snesrecomp/docs/FRAME_MODEL_HOSTS.md).
*
* A new port starts on the framework's beam-aligned driver
* (runner/src/beam_frame_driver.h): one frame is one PPU field, cut at the
* beam's vblank edge; interrupts -- raster splits and a coprocessor's IRQ
* line -- are taken where the beam latches them; parked time still runs the
* beam, APU and coprocessors; and the field is rasterized from the raster
* journal with real HDMA. It is the framework's, so a fix to it reaches this
* port on a submodule pull.
*
* This file used to hold its own copy of a simpler driver: frames measured
* from wherever the CPU last stopped, NMI delivered at an arbitrary beam
* position, the field drawn with end-of-frame register state. A game that
* uploads in vblank between INIDISP=$80 and $0F flickered black, and one
* that chains raster splits fell apart. Replace these calls only for what
* the driver does not do; MetalWarriorsSNESRecomp's src/mw_rtl.c and
* GundamWingEndlessDuelSNESRecomp's src/game_rtl.c are developed examples of
* title-specific drivers.
*/
#include "game_rtl.h"
#include "beam_frame_driver.h"
void GameRunOneFrame(void)
{
snes_beam_frame_driver_run_frame();
}
void GameDrawPpuFrame(void)
{
snes_beam_frame_driver_draw_ppu_frame();
}
void GameSessionReset(void)
{
/* Rematch / soft-return: clear anything that must not survive a new
* session. recomp-ai-rules/NETPLAY.md §3 — sticky state that "has always
* been fine" is the usual desync culprit, because single-player never
* re-enters the boot path twice in one process. */
snes_beam_frame_driver_reset();
}
const RtlGameInfo kGameInfo = {
.title = "legendofzeldaalinktothepast",
.initialize = NULL,
.run_frame = &GameRunOneFrame,
.draw_ppu_frame = &GameDrawPpuFrame,
.save_name_prefix = "save",
.session_reset = &GameSessionReset,
};
+13
View File
@@ -0,0 +1,13 @@
#ifndef GAME_RTL_H
#define GAME_RTL_H
#include "common_cpu_infra.h"
/* One frame of Legend of Zelda: A Link to the Past. See game_rtl.c — this is the port. */
void GameRunOneFrame(void);
void GameDrawPpuFrame(void);
void GameSessionReset(void);
extern const RtlGameInfo kGameInfo;
#endif /* GAME_RTL_H */
+8
View File
@@ -0,0 +1,8 @@
/* Hand-written bodies for code the recompiler could not translate.
*
* Empty at scaffold time. Fill this in as regeneration reports unresolved
* targets — and fill it in with real behaviour, never with a value invented
* to get past the gap. An invented result turns a loud failure into a silent
* wrong one (recomp-ai-rules/PRINCIPLES.md, "No stubs"). If a routine cannot
* be translated yet, leave it to the interpreter tier instead.
*/
+33
View File
@@ -0,0 +1,33 @@
/*
* The host contract for Legend of Zelda: A Link to the Past.
*
* The runner is a library, not an application: a handful of symbols are
* deliberately left for the host to define. The framework's desktop host
* (snesrecomp_target_desktop_host in CMakeLists.txt) defines the ones that
* depend on the frontend -- Die, RtlApuLock/RtlApuUnlock, g_spc_player,
* RtlDrawPpuFrame, MkDir, ChangeWindowScale and the widescreen globals.
*
* What is left is the trace contract: the debug_on_* hooks the generated code
* calls on every block entry and WRAM write. They belong to the trace build
* (-DSNESRECOMP_ENABLE_TRACE=ON supplies real ones through the debug server)
* and are empty here so a production build links.
*/
#include <stdint.h>
#if !defined(SNESRECOMP_TRACE) || !SNESRECOMP_TRACE
void debug_on_block_enter(uint32_t pc, uint32_t a, uint32_t x, uint32_t y)
{
(void)pc; (void)a; (void)x; (void)y;
}
void debug_on_wram_write_byte(uint32_t addr, uint8_t old_val, uint8_t new_val)
{
(void)addr; (void)old_val; (void)new_val;
}
void debug_on_wram_write_word(uint32_t addr, uint16_t old_val, uint16_t new_val)
{
(void)addr; (void)old_val; (void)new_val;
}
#endif
+67
View File
@@ -0,0 +1,67 @@
/*
* Legend of Zelda: A Link to the Past — desktop host shim.
*
* Identity and hooks, nothing else. The host itself is the framework's
* (snesrecomp/runner/src/desktop/host_main.h): the pre-boot launcher, ROM
* resolution and digest checks, config.ini and keybinds.ini, the window and
* the SDL / OpenGL presenters, audio, gamepads, the in-game save-state
* browser and rewind filmstrip, the OSD, the pacing clock, crash handlers and
* the post-mortem report, mod packages, Generate & rebuild, and the netplay
* barrier when the project is built with it. A fix there reaches this
* project on a submodule pull; nothing in this file needs to change for it.
*
* This file used to BE the host -- a ~700-line copy made at scaffold time --
* so every project scaffolded before a fix kept the bug. Two ports of the
* same game, a week apart, had different hosts.
*
* Nothing here ever ships a ROM. The player supplies one; the host makes
* that easy (launcher, then positional argument, then a copy beside the
* executable, then the rom.cfg cache, then a file picker) and checks what it
* is handed against the digests in rom_identity.txt.
*
* Grow this file only with what is specific to THIS title: a custom
* presenter (prepare_frame / draw_frame), an SPC player, a pacing rule
* (keep_pacing_debt), a Mods provider. See SnesDesktopHostGame for the
* complete list of hooks and what each is for.
*/
#include "host_main.h"
#include "game_rtl.h"
#include "snesrecomp_rom_identity.h" /* generated from rom_identity.txt */
#ifndef __ANDROID__
#define SDL_MAIN_HANDLED 1
#endif
#include "desktop/sdl_compat.h"
#ifdef __ANDROID__
/* SDLActivity loads libmain.so and dlsyms its SDL_main. */
#define main SDL_main
#endif
#ifndef SNES_GAME_VERSION
#define SNES_GAME_VERSION "dev"
#endif
static const SnesDesktopHostGame kGameHost = {
.display_name = "Legend of Zelda: A Link to the Past",
.window_title = "Legend of Zelda: A Link to the Past",
.region = SNESRECOMP_ROM_REGION,
.rom_file = SNESRECOMP_ROM_FILE,
.expected_sha256_hex = SNESRECOMP_ROM_EXPECTED_SHA256,
.expected_crc32_hex = SNESRECOMP_ROM_EXPECTED_CRC32,
.game_id = SNESRECOMP_ROM_GAME_ID,
.build_version = SNES_GAME_VERSION,
.game_info = &kGameInfo,
.num_players = 1,
/* Battery-backed SRAM shows the launcher's SAVES panel. Leave NULL for a
* title without one. The path is exe-relative. */
.sram_path = "saves/save.srm",
};
#ifndef __ANDROID__
#undef main /* desktop: keep plain main() even if SDL_main.h remapped it */
#endif
int main(int argc, char **argv)
{
return snesrecomp_desktop_main(&kGameHost, argc, argv);
}
+14
View File
@@ -0,0 +1,14 @@
/* Game-specific RAM variable declarations for Legend of Zelda: A Link to the Past.
*
* The framework includes this by name (snes/cpu.c), so every project must
* provide one even when it is empty. Populate it as WRAM locations are
* identified, so generated code and hand-written host code refer to the same
* address by the same name.
*/
#ifndef VARIABLES_H
#define VARIABLES_H
#include "types.h"
#endif /* VARIABLES_H */
Executable
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env bash
# Regenerate src/gen/*.c for Legend of Zelda: A Link to the Past from a verified ROM.
#
# Generated C is derived from copyrighted ROM data and is never committed;
# every developer regenerates from their own copy. The ROM digests come from
# rom_identity.txt, which the build and the release workflow read too, so
# there is one place to change when a revision changes.
#
# Flags:
# --rom <path> ROM to generate from. Defaults to SNESRECOMP_ROM, a known
# filename at the repo root, then the path in rom.cfg (which
# the setup wizard writes). The ROM does not have to live in
# the repo — keeping it on your own drive is the better
# habit, and SNESRECOMP_ROM sets it once for a shell.
# --no-verify skip the ROM digest check (for a revision this project has
# not been pinned to yet — expect the generated C to differ)
# --cfg-roots seed analysis from every func declaration in recomp/*.cfg
# -h|--help this message
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$ROOT"
VERIFY=1
CFG_ROOTS=0
ROM=""
while [ $# -gt 0 ]; do
case "$1" in
--rom) ROM=$2; shift 2 ;;
--no-verify) VERIFY=0; shift ;;
--cfg-roots) CFG_ROOTS=1; shift ;;
-h|--help) sed -n '2,/^set -euo/p' "$0" | sed -n '/^# /p' | sed 's/^# //'; exit 0 ;;
*) echo "regen.sh: unknown flag: $1 (try --help)" >&2; exit 2 ;;
esac
done
SNESRECOMP_ROOT="${SNESRECOMP_ROOT:-snesrecomp}"
CLI="$SNESRECOMP_ROOT/snesrecomp_cli.py"
if [ ! -f "$CLI" ]; then
echo "regen.sh: $CLI missing — run: git submodule update --init --recursive" >&2
exit 1
fi
PYTHON=${PYTHON:-}
if [ -z "$PYTHON" ]; then
# Windows App Execution Aliases can exist on PATH without running Python.
for _snes_python in python3 python; do
if "$_snes_python" -c 'import sys; sys.exit(sys.version_info[0] != 3)' >/dev/null 2>&1; then
PYTHON=$_snes_python
break
fi
done
fi
if [ -z "$PYTHON" ]; then
echo "regen.sh: no python3 on PATH" >&2
exit 1
fi
# The framework parses rom_identity.txt (the same parser CI uses), so this
# script carries no copy of a digest to forget on a revision bump.
IDENTITY="$SNESRECOMP_ROOT/tools/rom_identity.py"
EXPECTED_CRC32="${SNESRECOMP_EXPECTED_CRC32:-$("$PYTHON" "$IDENTITY" "$ROOT/rom_identity.txt" --get expected_crc32)}"
EXPECTED_SHA256="${SNESRECOMP_EXPECTED_SHA256:-$("$PYTHON" "$IDENTITY" "$ROOT/rom_identity.txt" --get expected_sha256)}"
# The framework owns the search: --rom, then SNESRECOMP_ROM, then a known
# name at the root, then rom.cfg (the path the setup wizard recorded).
RESOLVE=(resolve-rom --project-root "$ROOT"
--name "Legend of Zelda, The - A Link to the Past (USA).sfc" --name "legendofzeldaalinktothepast.sfc" --name "legendofzeldaalinktothepast.smc")
if [ -n "$ROM" ]; then RESOLVE+=(--rom "$ROM"); fi
if ! ROM=$("$PYTHON" "$CLI" "${RESOLVE[@]}"); then
echo "regen.sh: pass --rom /path/to/Legend of Zelda, The - A Link to the Past (USA).sfc, set SNESRECOMP_ROM, put it" >&2
echo " at the repo root, or record its path in rom.cfg. You must" >&2
echo " legally own a copy of Legend of Zelda: A Link to the Past." >&2
exit 1
fi
ROM=${ROM%$'\r'} # Windows Python ends lines with CRLF
VERIFY_ARGS=()
if [ "$VERIFY" -eq 1 ]; then
VERIFY_ARGS=(--expected-crc32 "$EXPECTED_CRC32" --expected-sha256 "$EXPECTED_SHA256")
echo "== Verifying $ROM =="
"$PYTHON" "$CLI" verify-rom --rom "$ROM" "${VERIFY_ARGS[@]}"
else
echo "== Skipping ROM verification (--no-verify) =="
fi
GEN_ARGS=(--rom "$ROM" --cfg-dir recomp --out-dir src/gen
--funcs-h recomp/funcs.h --project-root "$ROOT")
if [ "$CFG_ROOTS" -eq 1 ]; then GEN_ARGS+=(--cfg-roots); fi
if [ "$VERIFY" -eq 1 ]; then GEN_ARGS+=("${VERIFY_ARGS[@]}"); fi
echo "== Generating src/gen =="
"$PYTHON" "$CLI" generate "${GEN_ARGS[@]}"
echo
echo "Done. Build with:"
echo " cmake -S . -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build -j"