commit 0cd0e5555c1078c2390d1531a2a500981bf5a2a6 Author: setup Date: Fri Oct 30 11:35:37 2026 +0000 Initial LegendOfZeldaALinkToThePastSNESRecomp scaffold diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..424a2d2 --- /dev/null +++ b/.github/workflows/release.yml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7d5e3ed --- /dev/null +++ b/.gitignore @@ -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 diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..5d47c93 --- /dev/null +++ b/.gitmodules @@ -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 diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..c911cc5 --- /dev/null +++ b/CMakeLists.txt @@ -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 "$,${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. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6804d6f --- /dev/null +++ b/LICENSE @@ -0,0 +1,101 @@ +PolyForm Noncommercial License 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 . diff --git a/README.md b/README.md new file mode 100644 index 0000000..b9473c2 --- /dev/null +++ b/README.md @@ -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. diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.1.0 diff --git a/framework_pins.txt b/framework_pins.txt new file mode 100644 index 0000000..86ac0dc --- /dev/null +++ b/framework_pins.txt @@ -0,0 +1,4 @@ +snesrecomp=ef0b3ff7d6965ffcd96d8fd917cb182539b05d76 +recomp-ui=3f1d6f122a58823614abce14f5b86f4d165a629c +recomp-net=8c76fb643339144a3a84d5ba8f5defe6250dcd26 +retcomm-rbengine=2a03e73693acee0fb78076ea058642c931bef12e diff --git a/mods/preloaded/README.md b/mods/preloaded/README.md new file mode 100644 index 0000000..baf1f49 --- /dev/null +++ b/mods/preloaded/README.md @@ -0,0 +1,22 @@ +# Preloaded mods + +Ship reviewed, default-disabled packages here: + +```text +packages/// + 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. diff --git a/mods/preloaded/packages/.gitkeep b/mods/preloaded/packages/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/recomp-ui b/recomp-ui new file mode 160000 index 0000000..3f1d6f1 --- /dev/null +++ b/recomp-ui @@ -0,0 +1 @@ +Subproject commit 3f1d6f122a58823614abce14f5b86f4d165a629c diff --git a/recomp/README.md b/recomp/README.md new file mode 100644 index 0000000..f6e4f2e --- /dev/null +++ b/recomp/README.md @@ -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. diff --git a/recomp/bank00.cfg b/recomp/bank00.cfg new file mode 100644 index 0000000..9ae8350 --- /dev/null +++ b/recomp/bank00.cfg @@ -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 diff --git a/recomp/symbols.toml b/recomp/symbols.toml new file mode 100644 index 0000000..39dd601 --- /dev/null +++ b/recomp/symbols.toml @@ -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)" diff --git a/rom_identity.txt b/rom_identity.txt new file mode 100644 index 0000000..a8fb08c --- /dev/null +++ b/rom_identity.txt @@ -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 diff --git a/scripts/package_release.sh b/scripts/package_release.sh new file mode 100755 index 0000000..9fd81b7 --- /dev/null +++ b/scripts/package_release.sh @@ -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 [] [] [--embed-toolchain] +# +# Both positionals are optional; BUILD_DIR and PLATFORM still work as env vars. +# Defaults: build, and the host's own -. --embed-toolchain copies +# the cmake-clang-v1 pack named by RETCOMM_TOOLCHAIN_DIR into the zip so the +# player downloads nothing. +# +# Writes: dist/legendofzeldaalinktothepast--.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 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[@]}"}" diff --git a/snesrecomp b/snesrecomp new file mode 160000 index 0000000..ef0b3ff --- /dev/null +++ b/snesrecomp @@ -0,0 +1 @@ +Subproject commit ef0b3ff7d6965ffcd96d8fd917cb182539b05d76 diff --git a/src/game_rtl.c b/src/game_rtl.c new file mode 100644 index 0000000..5c4068a --- /dev/null +++ b/src/game_rtl.c @@ -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, +}; diff --git a/src/game_rtl.h b/src/game_rtl.h new file mode 100644 index 0000000..6ff20ac --- /dev/null +++ b/src/game_rtl.h @@ -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 */ diff --git a/src/gen_stubs.c b/src/gen_stubs.c new file mode 100644 index 0000000..c632c97 --- /dev/null +++ b/src/gen_stubs.c @@ -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. + */ diff --git a/src/host_contract.c b/src/host_contract.c new file mode 100644 index 0000000..b68f0c9 --- /dev/null +++ b/src/host_contract.c @@ -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 + +#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 diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..72e51e7 --- /dev/null +++ b/src/main.c @@ -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); +} diff --git a/src/variables.h b/src/variables.h new file mode 100644 index 0000000..cb4ab04 --- /dev/null +++ b/src/variables.h @@ -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 */ diff --git a/tools/regen.sh b/tools/regen.sh new file mode 100755 index 0000000..ca304a0 --- /dev/null +++ b/tools/regen.sh @@ -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 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"