mirror of
https://github.com/imjasonh/nescript
synced 2026-07-08 08:55:38 +00:00
Adds `examples/platformer.ne`, a full side-scrolling game that
exercises nearly every subsystem of the compiler in one program:
custom CHR tileset, 32×30 background nametable with per-region
attribute palettes, 2×2 metasprite hero with gravity/jump physics,
wrap-around horizontal scrolling, moving enemies, coin pickups,
user-declared SFX + music, and a Title → Playing state machine
with autopilot so the headless jsnes harness captures real
gameplay at frame 180. Tile art + nametable are generated by
`scripts/gen_platformer_tiles.rs` (`cargo run --bin gen_platformer_tiles`).
Building this out uncovered three independent runtime bugs that
together made the example render as black-on-black smileys. All
three are fixed in this commit:
1. **`gen_init` enabled sprite rendering before the linker's
initial palette/background load runs.** The PPU's v-register
auto-increments on every `$2007` write *during active
rendering*, so the palette load (32 B) and nametable load
(1024 B) were scrambled past the first ~72 bytes — every
existing program with a `background Level { ... }` block was
silently rendering zero-filled VRAM. Fix: leave `PPU_MASK = 0`
at the end of `gen_init` and emit a new `gen_enable_rendering`
call *after* all initial VRAM writes complete.
2. **Audio tick corrupted `ZP_CURRENT_STATE`.** The audio
driver's period-table lookup reused `$02/$03` as a temporary
indirect pointer with a comment claiming the slots were free
because the tick doesn't call mul/div. But `$03` is also
`ZP_CURRENT_STATE` used by the state dispatch loop, so every
music note silently overwrote the state index with the high
byte of `__period_table` (`0xC5` in the platformer ROM),
wedging the state machine forever. Fix: `gen_nmi` now PHAs
`$02/$03` on entry and PLA-restores them on exit, and the
audio tick JSR moves inside that save/restore window (it used
to be spliced by the linker *before* the register saves, so
even A/X/Y were technically being trashed pre-save). Only
`audio_demo`'s audio hash shifts (its note timings move a few
cycles); every other golden is unchanged.
3. **Sub-palette mirroring footgun.** Writing a 32-byte palette
blob sequentially causes the sprite sub-palettes' "index 0"
slots at `$3F10/$3F14/$3F18/$3F1C` to clobber the background
universal colour at `$3F00/$3F04/$3F08/$3F0C` via NES hardware
mirroring. The example's palette sets all eight first bytes
to `$22` (sky blue) for this reason; `docs/future-work.md`
picks up a TODO to warn on inconsistent first-byte values in
the analyzer.
Also:
- `docs/platformer.gif` — 6-second recording of the example
running in jsnes, generated by the new
`tests/emulator/record_gif.mjs` puppeteer helper (encodes via
`gifenc`, committed as a dev-dependency under
`tests/emulator/package.json`).
- README / examples/README tables and the 497-test count are
updated to cover the new example.
https://claude.ai/code/session_01BcCcHi6FUmTh8jC7UgkA3A
55 lines
1.3 KiB
TOML
55 lines
1.3 KiB
TOML
[package]
|
|
name = "nescript"
|
|
version = "0.1.0"
|
|
edition = "2021"
|
|
description = "A statically-typed compiler for NES game development"
|
|
license = "MIT"
|
|
default-run = "nescript"
|
|
|
|
[[bin]]
|
|
name = "nescript"
|
|
path = "src/main.rs"
|
|
|
|
# One-shot tile / nametable generator for examples/platformer.ne. Kept in-tree
|
|
# (rather than as a Python script) so the repo stays single-language.
|
|
[[bin]]
|
|
name = "gen_platformer_tiles"
|
|
path = "scripts/gen_platformer_tiles.rs"
|
|
|
|
[dependencies]
|
|
clap = { version = "4", features = ["derive"] }
|
|
ariadne = "0.4"
|
|
image = { version = "0.25", default-features = false, features = ["png"] }
|
|
|
|
[dev-dependencies]
|
|
pretty_assertions = "1"
|
|
|
|
[profile.release]
|
|
lto = true
|
|
strip = true
|
|
|
|
[lints.rust]
|
|
unsafe_code = "forbid"
|
|
|
|
[lints.clippy]
|
|
all = { level = "warn", priority = -1 }
|
|
pedantic = { level = "warn", priority = -1 }
|
|
# Allow patterns common in compiler code
|
|
module_name_repetitions = "allow"
|
|
must_use_candidate = "allow"
|
|
missing_errors_doc = "allow"
|
|
missing_panics_doc = "allow"
|
|
cast_possible_truncation = "allow"
|
|
wildcard_imports = "allow"
|
|
option_if_let_else = "allow"
|
|
match_same_arms = "allow"
|
|
unnested_or_patterns = "allow"
|
|
missing_const_for_fn = "allow"
|
|
too_many_lines = "allow"
|
|
single_match_else = "allow"
|
|
unnecessary_wraps = "allow"
|
|
unused_self = "allow"
|
|
enum_glob_use = "allow"
|
|
manual_assert = "allow"
|
|
vec_init_then_push = "allow"
|
|
struct_field_names = "allow"
|