PORTCLUB
Cabecera de Knife Edge: Nose Gunner (Knife Edge Recompiled)
Port nativo

Knife Edge: Nose Gunner (Knife Edge Recompiled)

Port nativo de Knife Edge: Nose Gunner (Knife Edge Recompiled) para PC.

Knife Edge: Nose Gunner (Knife Edge Recompiled)

Port nativo basado en disi33/ke-ng_reversing.

README del repositorio

Disclaimer: This is what those in the decomp/recomp community would call an "AI Slop" recomp. It is almost entirely LLM-generated by a mix of Claude Fable, Opus 5, and Sonnet.

As a career Software Engineer, I think it's remarkable that an LLM can leverage the amazing tools built by the retro reverse-engineering community to derive a static recompilation of an N64 game.

However, this project has not been built, reviewed, or endorsed by anyone with relevant expertise in the area.

I can make no guarantees of the quality, accuracy, stability, or security of the code in this repository, since I did not write it, and I lack the knowledge to properly review it.

This was merely an exercise in curiosity to see if I could get an LLM to pop out a PC port of an obscure N64 title I remember from my childhood.

I hope to some day develop the skills necessary to create a proper decomp project.

For examples of decomps/recomps that were built by communities of experts, check out:

Knife Edge Recompiled

A static-recompilation PC port of Knife Edge – Nose Gunner (N64, Kemco, 1998), built on N64Recomp / N64ModernRuntime and RT64 — the same toolchain used by Zelda64Recomp.

Knife Edge's N64 machine code is translated to C ahead of time (no emulation), linked against a runtime that HLEs the OS/libultra layer, and rendered through RT64's modern graphics backend.

Status

Playable and stable. Boots through the KEMCO logo, title screen and attract loop into the menus and gameplay — verified on real hardware through stage 4 including an alternate stage path, with correct music and sound effects, correct game speed, and working results/stage-overview screens. No known defects.

Not yet verified: full campaign completion, and high-score persistence across runs (analyzed and almost certainly absent — which is faithful, since the game only probes for a Controller Pak; see analysis/docs/enhancements.md).

All planned enhancements are built: high resolution, widescreen, full height, HUD relocation, high framerate (experimental), plus gamepad and mouse-aim input support and Flatpak packaging. enhanced is the default profile; vanilla is the regression baseline (see "Configuration" below and analysis/docs/enhancements.md).

Development notes, working practices and a documentation index live in CLAUDE.md.

Repository policy

What this repo contains is the analysis pipeline and its metadata, not build artifacts:

  • analysis/ — the scripts that derive symbols from a ROM (function boundary detection, libultra identification, spin-loop detection).
  • config/*.toml — the symbol/section tables those scripts produce. This is the reviewable source of truth: a change here is human-meaningful.
  • src/ — the runtime shim (window, renderer, audio, input, ROM provisioning).

Deliberately not committed:

  • The ROM, and any asset or data dump taken from it. Knife Edge – Nose Gunner is © 1998 Kemco; you need your own legally-obtained dump to build a playable binary or to regenerate anything.
  • Recompiler output (generated/). It is a build artifact derived mechanically from the ROM, so it is regenerated rather than stored — see "Regenerating from a ROM".

Two consequences worth knowing:

Normal CI needs no ROM and no secrets. With no ROM available, the build compiles src/stub_game/ instead of recompiled game code, so .github/workflows/build.yml still validates the runtime shim, RT64, librecomp, ultramodern, audio and input on every push — and works for pull requests from forks, which cannot receive secrets.

Release binaries contain recompiled code but no game data. The player supplies their own ROM at runtime and the game reads assets, audio and level data directly out of it, exactly as it did on hardware — there is no asset extraction step. The linux-build workflow job asserts this via scripts/assert_no_rom_assets.py, which samples the ROM's data regions and fails if any of those bytes appear in the binary.

Building

Option A: devcontainer (recommended)

Open the repo in the provided devcontainer (.devcontainer/devcontainer.json). Its postCreateCommand installs the full toolchain, sets up a Python venv for the analysis pipeline, and initializes submodules. Then:

git submodule update --init --recursive   # if not already done
cmake -B build -G Ninja \
  -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ \
  -DCMAKE_BUILD_TYPE=Release \
  -DKE_ROM="/path/to/Knife Edge - Nose Gunner (USA).n64"
cmake --build build

The resulting binary is build/KnifeEdgeRecompiled. (Any build directory name works — local development in this repo has also used build-shim; CI uses build.)

KE_ROM makes CMake normalize the ROM, run the analysis pipeline, and invoke N64Recomp/RSPRecomp at configure time; the results land in build/generated/. Omit it to build against src/stub_game/ instead — a placeholder that links and runs but has no real game code, useful for build-system iteration and for CI without a ROM. (-DKE_GENERATED_DIR=<dir> still works if you have recompiler output from elsewhere.)

Option B: native deps

Install (Debian/Ubuntu package names — matches .devcontainer/devcontainer.json's postCreateCommand):

sudo apt-get install -y cmake ninja-build clang lld pkg-config \
  libsdl2-dev libx11-dev libvulkan-dev vulkan-tools \
  glslang-tools spirv-tools mesa-vulkan-drivers xvfb

(xvfb and a Vulkan software rasterizer like mesa-vulkan-drivers' lavapipe are only needed for headless/CI runs without a real GPU/display.) Then clone with submodules and build as in Option A:

git clone --recurse-submodules <this repo>
cmake -B build -G Ninja -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ \
  -DCMAKE_BUILD_TYPE=Release -DKE_ROM="/path/to/knife_edge.n64"
cmake --build build

Windows has a native CMake branch and CI jobs (see Option E below); first CI runs are pending. macOS should work per the top-level CMakeLists.txt's platform branches (this mirrors Zelda64Recomp's supported set) but remains unexercised — Linux is the actively-tested platform (see analysis/docs/build-notes.md).

Option C: containerized (any machine with docker or podman)

No local toolchain install at all — everything (clang, cmake, ninja, SDL2, Vulkan, the analysis pipeline's Python venv, ...) lives in containers/Containerfile, built and run for you by one script:

git submodule update --init --recursive   # if not already done
./scripts/container_build.sh --rom "/path/to/Knife Edge - Nose Gunner (USA).n64"

Output lands in build-container/KnifeEdgeRecompiled — run it on the host exactly as in "Running" below, no container involved at runtime. Omit --rom to build against src/stub_game/ instead, same as Options A/B.

This is exactly what CI runs (.github/workflows/build.yml calls the same script), so a container build is the closest thing to a guaranteed-green build available locally. See ./scripts/container_build.sh --help for the rest of its flags (--check, --smoke, --shell, --exec, --jobs, --rebuild-image), and analysis/docs/build-notes.md's "Containerized build" section for the design.

Option D: Flatpak

Needs recompiled game code first (Option A/B/C above with -DKE_ROM=..., or ./scripts/container_build.sh --rom ...) so generated/us/ exists, plus flatpak-builder on the host:

./scripts/build_flatpak.sh --install

This builds KnifeEdgeRecompiled.flatpak in the repo root and installs it for the current user (drop --install to just produce the bundle; see ./scripts/build_flatpak.sh --help for --bundle <path> and --runtime-install, which fetches the org.freedesktop.Platform/Sdk//25.08 runtime this manifest needs if you don't have it yet). Then:

flatpak run io.github.haydenmc.KnifeEdgeRecompiled

First run opens a portal file-open dialog to pick your ROM (no ROM ships in the bundle, and none is downloaded) — the sandbox has no filesystem permissions at all; the portal is what lets you point it at a file from outside the sandbox for that one pick. App data (config, the normalized ROM cache) lives at ~/.var/app/io.github.haydenmc.KnifeEdgeRecompiled/data/knife-edge-recompiled/, so it persists across app updates. See analysis/docs/build-notes.md, "Flatpak packaging", for the manifest's design and packaging/flatpak/io.github.haydenmc.KnifeEdgeRecompiled.yml's finish-args for the exact sandbox permissions (wayland/X11/pulseaudio sockets, DRI, IPC — no filesystem, no network).

Option E: Windows (clang-cl, native)

Install Visual Studio 2022 Build Tools (or VS with the C++ workload), which provides CMake ≥3.20, Ninja, and LLVM/clang-cl — all available via the Visual Studio installer. Python 3 with rabbitizer, capstone, and tomlkit is needed for ROM-based builds:

pip install rabbitizer capstone tomlkit

CRITICAL: Before cloning, run these once (the pinned-submodule patch is LF-only and configure fails if autocrlf mangles it; long paths are needed by the deep build tree):

git config --global core.autocrlf false
git config --global core.longpaths true

Then clone with submodules and build the host tools:

git clone --recurse-submodules <this repo>
cmake -S deps/N64Recomp -B deps/N64Recomp/build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build deps/N64Recomp/build --target N64RecompCLI RSPRecomp

From a Visual Studio x64 Developer Prompt (not a plain cmd.exe; open it from the Start Menu or via vcvarsall.bat x64), configure and build:

cmake -B build -G Ninja -DCMAKE_C_COMPILER=clang-cl -DCMAKE_CXX_COMPILER=clang-cl "-DCMAKE_CXX_FLAGS=-Xclang -fexceptions -Xclang -fcxx-exceptions" -DKE_ROM="<path to knife_edge.n64>"
cmake --build build

SDL2 and dxc DLLs are staged next to the exe automatically. App data (config, normalized ROM cache) lives in %LOCALAPPDATA%\KnifeEdgeRecompiled (override with KE_DATA_DIR if needed).

Running

You need your own dump of Knife Edge – Nose Gunner (USA) (.z64, .v64, or .n64 byte order — all three are accepted).

./build/KnifeEdgeRecompiled --rom /path/to/knife_edge.z64

The first run with --rom validates the dump (hash-checked against the known-good ROM) and stores a normalized copy under a ke_recomp_data/ directory next to wherever you run the binary from. Subsequent runs can omit --rom and will reuse the stored copy. Running without --rom and without a previously stored ROM prints a clear error and exits (this is what CI's smoke test checks — see below).

Controls

Keyboard, and any SDL-recognized game controller — both work at once, and a gamepad can be hot-plugged in or out at any time.

Keyboard and mouse are always N64 controller 1 (port 0). Pads are handed ports in the order they connect, starting at pad_start_port (default 0), each new pad taking the lowest free port from there; a pad keeps its port until it is unplugged, and unplugging frees that port for the next connection. With the default, a single pad also lands on port 0 and merges with the keyboard and mouse — the same "whichever controller is in hand works" behavior as before — while a second and third pad become controllers 2 and 3. See "Multiplayer" below for the two-player recipe.

N64 inputKey
AX
BZ
ZLeft Shift
StartEnter
D-pad Up/Down/Left/RightArrow keys
C-Up / C-Down / C-Left / C-RightI / K / J / L
L / R shoulderQ / E
Control stickW A S D
N64 inputGamepad
AA
BB
ZEither trigger
StartStart
D-pad Up/Down/Left/RightD-pad
C-Up / C-Down / C-Left / C-RightRight stick
L / R shoulderLeft / right bumper
Control stickLeft stick

Custom gamepad mappings can be supplied the standard SDL way, via the SDL_GAMECONTROLLERCONFIG environment variable.

Mouse aim is also available, on by default. Click anywhere in the window to capture the pointer; Esc releases it. While captured, mouse motion steers the same control stick as the keyboard/gamepad (summed with them, then clamped), and mouse buttons map to N64 buttons:

N64 inputMouse
ZLeft click
ARight click
BMiddle click

There are two ways mouse motion can be turned into aim, chosen with the [input] table's mouse_mode key:

  • positional (default) — the reticle follows the mouse 1:1 and absolutely: move the mouse an inch and the reticle moves the corresponding distance, then stops there. This is a closed loop on the game's own reticle position, so it needs no drift correction and never overshoots. mouse_sensitivity scales how far the reticle travels per unit of mouse movement.
  • velocity — mouse speed maps to stick deflection, so the reticle keeps drifting for as long as the mouse keeps moving. This was the original mapping; it is kept because it needs no knowledge of the game's state at all.

mouse_invert_y (off by default) inverts the vertical axis in both modes. Off means mouse-up aims up; on restores the game's own flight-style inverted aim. Turn mouse aim off entirely with mouse_aim — see "Configuration" below. The full reverse-engineering behind positional mode is in analysis/docs/mouse-aim.md.

One honest caveat that applies to both modes: the game samples input once per game frame (~15 Hz, same as the pad — see below) and moves the reticle by a fixed amount per frame, so the reticle updates at that same game rate no matter how smoothly the mouse itself moves. That's inherent to the current build, not a bug in the mouse handling; fully smooth aim is coupled to the (not yet started) high-framerate enhancement.

The game samples the controller once per game frame (~15 Hz — see analysis/docs/timing-and-mission-debug.md), the same as on console, so a key has to be held for a frame to register. The window disables SDL's text input so a desktop input method cannot swallow those holds; if your desktop still intercepts held keys, try running with XMODIFIERS=@im=none QT_IM_MODULE= GTK_IM_MODULE= to take the input method out of the path entirely.

Analog-stick deadzone, response curve, and sensitivity can be tuned via the [input] table — see "Configuration" below.

Multiplayer

Knife Edge has four-player TEAM (co-op) and BATTLE (team versus) modes on the GAME MODE SELECT carousel. The game only lets you pick them when it sees at least two controllers, and it checks once at startup — so plug pads in before launching, or tell it how many ports to report with ports.

Two [input] keys cover this (full table under "Configuration" below):

  • ports — how many controller ports to report. 0 (default) is automatic: port 0 always, ports 1–3 while a pad is assigned to them. Setting it to 2–4 reports that many regardless of what is plugged in; the ports with nothing on them simply never press anything, which is enough to enter a multiplayer game and play it with fewer humans than players.
  • pad_start_port — which port pads start claiming from. 0 (default) merges the first pad with the keyboard and mouse on controller 1.

Two humans, one keyboard and one pad:

[input]
pad_start_port = 1   # keyboard + mouse stay P1; the pad becomes P2
ports = 2            # report 2 controllers so TEAM/BATTLE are selectable

ports can be left at 0 there if the pad is genuinely plugged in before launch — it is only needed when you want the game to believe in a controller that is not there.

In TEAM mode each player gets their own coloured reticle, and the colour is fixed by port: P1 green, P2 red, P3 yellow, P4 magenta. All four gun for the same aircraft, so there is one shared health dial.

Multiplayer is newly reachable in this port and is still being worked through (analysis/docs/multiplayer.md has the details, including two known cosmetic HUD faults in BATTLE mode on the enhanced profile — vanilla is correct in every mode measured).

Configuration

Settings live in <app folder>/config.toml (the ke_recomp_data/ directory mentioned above, next to wherever you run the binary from), created with commented defaults on first run. It selects a profile:

  • enhanced (default) — a curated set of quality-of-life changes: currently input_latching (see Controls above), high_resolution (renders the 3D scene at window resolution through RT64 instead of native 320x240), widescreen (expands the 3D field of view to the window's aspect ratio instead of 4:3-pillarboxing it), full_height (removes the in-mission 20-line letterbox, revealing the full 320x240 frame), hud_relocation (re-anchors the in-mission HUD to the true window corners that the previous two open up — health cluster bottom-left, S-BOMB gauge bottom-right, each keeping its original margin — instead of leaving it inside the centered 4:3 column), and extended_aim (lets the aiming reticle reach the whole visible viewport instead of stopping at the rails it had for a 4:3 / 320x200 view — ±170 instead of ±128 horizontally at 16:9, ±100 instead of ±84 vertically; the shot goes where the crosshair is out there too, the game's aim math needs no correction for it).
  • vanilla — every enhancement off, faithful to original hardware behavior. This is the regression baseline every measurement and bug investigation in analysis/docs/ assumes unless stated otherwise.
  • custom — reads individual [enhancements] flags from the file.

One additional flag exists outside every curated profile: high_framerate (experimental) renders at the display's refresh rate via RT64 frame interpolation while game logic keeps its original paced rate. It works — motion is visibly smoother — but has known visual artifacts on some effects (the lock-on effect and bullet trails warp/smear, a limitation of RT64's heuristic frame matching; see analysis/docs/high-framerate.md). To try it, set active = "custom" and enable it (plus whichever enhanced-set flags you want) under [enhancements].

Two CLI flags, and nothing else config-related:

./build/KnifeEdgeRecompiled --rom /path/to/knife_edge.z64 --profile vanilla
./build/KnifeEdgeRecompiled --config /path/to/other-config.toml

--profile overrides [profile] active from the file; --config points at a config file somewhere other than the default location. A missing file is created with defaults; a malformed one falls back to vanilla defaults with a warning on stderr rather than failing to start. The active profile and any non-default flags are logged on startup right after the build stamp, so a bug report identifies its config along with its build. See analysis/docs/enhancements.md for the full policy, the current flag table, and how future enhancements get added.

The [input] table tunes how the host analog stick maps to the N64 stick range. It applies in every profile, vanilla included — it's host input-device shaping, not a change to game behavior, so it isn't gated by --profile. Out-of-range values are clamped to the ranges below, with a warning on stderr.

KeyDefaultRangeMeaning
stick_deadzone0.150.0–0.9Radial stick travel around center that's ignored.
stick_curve1.00.25–4.0Response exponent applied after the deadzone rescale; >1.0 gives finer control near center without changing full deflection.
stick_sensitivity1.00.1–3.0Multiplier applied after the curve; >1.0 reaches full deflection before the stick is maxed out, <1.0 caps below full deflection.
mouse_aimtruebooleanMaster switch for mouse aim (see Controls above). false disables capture-on-click entirely.
mouse_sensitivity1.00.05–20.0Multiplier on mouse-derived aiming: reticle travel per unit of mouse movement in positional mode, stick deflection per unit of mouse speed in velocity mode.
mouse_mode"positional""positional" | "velocity"How mouse motion becomes aim — 1:1 absolute reticle following, or mouse-speed-to-stick drift. See Controls above.
mouse_invert_yfalsebooleanInverts the vertical mouse axis in both modes. Off is mouse-up = reticle-up; on restores the game's own flight-style inverted aim.
pad_start_port00–3Which N64 port pads start claiming, lowest free port first, in connection order. 0 merges the first pad with keyboard and mouse on controller 1; 1 keeps controller 1 keyboard-only and makes pads P2–P4. See "Multiplayer" above.
ports00–4How many controller ports to report to the game. 0 = automatic (port 0 always, ports 1–3 while a pad is assigned there). 2–4 reports that many whatever is plugged in, which is what makes the TEAM/BATTLE modes selectable with fewer pads than players. The KE_FORCE_PORTS environment variable overrides this.

Regenerating from a ROM

Recompiler output is not committed, so a playable build always regenerates it. Passing -DKE_ROM=<path> to CMake does all of this for you; the manual recipe below is what that automates, and is what you want when iterating on the analysis pipeline itself:

  1. Build the one-time tools (if you haven't already):
    cmake -S deps/N64Recomp -B deps/N64Recomp/build -G Ninja \
      -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++
    ninja -C deps/N64Recomp/build N64Recomp
    make -C deps/n64sym          # symbol-identification oracle used by stage 2 below
    
  2. Ingest your ROM into the normalized form the pipeline expects:
    .venv/bin/python analysis/byteswap.py /path/to/your/rom build/knife_edge.z64
    
    This accepts z64/v64/n64 byte order, verifies the dump's md5 against the known-good Knife Edge (USA) hash, and writes a normalized big-endian z64 to build/knife_edge.z64 (outside the repo tree — never committed).
  3. Run the analysis pipeline:
    make -C analysis
    
    This runs find_functions.py → ident_libultra.py → gen_syms.py (see analysis/docs/pipeline-spec.md and pipeline-report.md) and (re)writes config/knife_edge.us.syms.toml and config/knife_edge.us.toml.
  4. Run N64Recomp (and RSPRecomp for the audio microcode). Output goes wherever output_func_path in config/knife_edge.us.toml points — a build directory, never the repo:
    deps/N64Recomp/build/N64Recomp config/knife_edge.us.toml
    deps/N64Recomp/build/RSPRecomp config/aspMain.us.toml
    
  5. Check the metadata is unchanged:
    git diff --stat -- config
    
    No diff means the pipeline is reproducible. A diff means the pipeline changed behavior — review it, since config/ is the reviewable record of what the analysis concluded.
  6. Rebuild KnifeEdgeRecompiled against the regenerated output as in "Building" above.

.github/workflows/build.yml has a regen-verify job that automates this recipe and fails if step 5 finds a diff. Unlike the rest of this section (which assumes you're supplying your own local ROM), that job runs on a plain hosted ubuntu-latest runner: it fetches an age-encrypted ROM from owner-configured private storage and decrypts it in-run using repo secrets (see analysis/docs/build-notes.md, "Encrypted ROM in CI (Backblaze B2)" for the full design and setup). It runs on every push to main, every same-repo pull request (so config/ drift is caught before merge, not after), and manual dispatch.

linux-build and flatpak-build fetch the ROM the same way and run on the same events plus version-tag pushes, producing a downloadable Linux tarball and Flatpak bundle artifact respectively on every one of those runs (PR, push to main, tag, or dispatch) — not just tagged releases. A v* tag push additionally triggers publish, which attaches those two artifacts from the same run to the GitHub release for that tag (creating it if it doesn't exist yet); publish is the only job in the workflow with write permissions and does no building of its own. All ROM-needing jobs skip automatically wherever the ROM secrets aren't configured — forks and their pull requests still need no ROM and no secrets, unchanged from the policy above.

Architecture

  • CMakeLists.txt — top-level build; wires up RT64 + N64ModernRuntime (ultramodern/librecomp) + the RecompiledFuncs static lib (either the regenerated output or src/stub_game/) into the KnifeEdgeRecompiled executable. See analysis/docs/build-notes.md for how it was put together, including dependency quirks and what was and wasn't kept from Zelda64Recomp's reference implementation.
  • src/main/ — runtime entry point (main.cpp), RT64 renderer wiring (rt64_render_context.{h,cpp}), overlay registration (register_overlays.cpp), the RCP frame-time model that sets the game's speed (rcp_timing.cpp), small platform helpers (support.{h,cpp}), the enhancement-flag/profile config loader (config.{h,cpp} — see "Configuration" above and analysis/docs/enhancements.md).
  • src/stub_game/ — placeholder game code used when no ROM/generated code is unset, so the executable always links and runs even without a ROM-derived build.
  • analysis/ — the ROM → symbols pipeline (Python): find_functions.py (function boundary detection), ident_libultra.py (libultra/OS function naming via the vendored n64sym tool plus a curated override table), gen_syms.py (emits the config/*.toml N64Recomp consumes). Full pipeline design in analysis/docs/pipeline-spec.md, results in analysis/docs/pipeline-report.md.
  • analysis/docs/n64recomp-formats.md — reference for the N64Recomp input TOML formats and output contract this project relies on.
  • analysis/docs/overlay-tracking.md — how Knife Edge's own code-overlay loader is hooked so librecomp's function-lookup tables stay correct across overlay swaps.
  • analysis/docs/boot-debug.md — the crash-by-crash debugging log from first boot to a stable title/attract loop; the best single source for "what does and doesn't work yet."
  • analysis/docs/timing-and-mission-debug.md — why the game ran 4x too fast and why starting a mission hung, with the measurements and backtraces; also documents KE_PERF (frame-rate instrumentation) and KE_RCP_FRAME_MS (the RCP frame-time budget), and the headless recipe for driving the menus.

Credits

  • N64Recomp and N64ModernRuntime (MIT, N64Recomp org / Wiseguy) — the static recompiler and the ultramodern/librecomp runtime this project's game code and main.cpp are built on.
  • RT64 (MIT, RT64 Contributors) — the rendering backend used for ultramodern::renderer::callbacks_t.
  • n64sym (MIT, shygoo) — the signature- based symbol identification tool used as the seed oracle in analysis/ident_libultra.py.
  • Zelda64Recomp — reference implementation this project's main.cpp, rt64_render_context.cpp, and register_overlays.cpp patterns were adapted from (see analysis/docs/build-notes.md for exactly what was kept vs. dropped).

Knife Edge – Nose Gunner is © 1998 Kemco. This project contains no Kemco assets, game data, or ROM content — only independently reverse-engineered control-flow structure (function boundaries and libultra/OS symbol names) needed to recompile the game's own machine code.

Comentarios (0)

Inicia sesión para dejar un comentario.

Sé el primero en comentar sobre este juego.