mirror of
https://github.com/imjasonh/nescript
synced 2026-07-08 00:45:38 +00:00
metatiles + collision: metatileset, room, paint_room, collides_at
Closes §H. 2×2 metatiles and a parallel collision map are now a
first-class construct. `metatileset Name { metatiles: [{ id, tiles,
collide }, ...] }` declares a library of 2×2 tile bundles. `room Name
{ metatileset: M, layout: [...] }` lays them out on a 16×15 grid. The
compiler expands each room at compile time into:
- a 960-byte nametable (`__room_tiles_<name>`)
- a 64-byte attribute table (`__room_attrs_<name>`)
- a 240-byte collision bitmap (`__room_col_<name>`)
`paint_room Name` reuses the vblank-safe `load_background` update
machinery for the nametable blit and installs the collision bitmap
pointer into `ZP_ROOM_COL_LO`/`ZP_ROOM_COL_HI` (ZP $18/$19).
`collides_at(x, y)` JSRs into a small runtime helper that reads
`(room_col),Y` with `Y = (y & 0xF0) | (x >> 4)` and returns 0/1.
The helper links in only when the `__collides_at_used` marker is
emitted, so programs that declare a room but never query it pay
zero bytes for the subroutine.
`parse_byte_array` grows a `[value; count]` shortcut — 240-entry
`layout` arrays are unwieldy to spell out a byte at a time.
See `examples/metatiles_demo.ne` for the end-to-end flow: a probe
sprite bounces off walls via `collides_at` and lands on the left
side of the playfield at frame 180 — direct evidence that the
collision query works.
Also defers the register-allocator work from §"Code quality /
tooling" and documents the audio-goldens constraint in future-work
so the next agent sees it.
This commit is contained in:
parent
9719dc4111
commit
82b3d0d20a
23 changed files with 1344 additions and 60 deletions
1
tests/emulator/goldens/metatiles_demo.audio.hash
Normal file
1
tests/emulator/goldens/metatiles_demo.audio.hash
Normal file
|
|
@ -0,0 +1 @@
|
|||
a82b6ff5 132084
|
||||
BIN
tests/emulator/goldens/metatiles_demo.png
Normal file
BIN
tests/emulator/goldens/metatiles_demo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 845 B |
|
|
@ -43,7 +43,10 @@ fn compile(source: &str) -> Vec<u8> {
|
|||
let mut instructions = codegen.generate(&ir_program);
|
||||
nescript::codegen::peephole::optimize(&mut instructions);
|
||||
|
||||
let linker = Linker::new(program.game.mirroring).with_battery(analysis.has_battery_saves);
|
||||
let rooms = assets::resolve_rooms(&program);
|
||||
let linker = Linker::new(program.game.mirroring)
|
||||
.with_battery(analysis.has_battery_saves)
|
||||
.with_rooms(rooms);
|
||||
linker.link_with_all_assets(&instructions, &sprites, &sfx, &music)
|
||||
}
|
||||
|
||||
|
|
@ -3931,6 +3934,113 @@ start Main
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metatiles_demo_compiles_and_has_collision_helper() {
|
||||
// The metatiles + collision feature (§H) wires
|
||||
// `paint_room` to the existing `load_background` machinery
|
||||
// and adds a `collides_at` intrinsic that JSRs into a small
|
||||
// runtime helper. This smoke test drives the full feature
|
||||
// through `compile` and checks three end-to-end invariants:
|
||||
//
|
||||
// 1. The room's `__room_tiles_Dungeon` / `__room_attrs_Dungeon`
|
||||
// / `__room_col_Dungeon` data blobs land somewhere in PRG
|
||||
// ROM. Without them the linker would hit an undefined-
|
||||
// symbol error.
|
||||
// 2. The `__collides_at` runtime helper is spliced in (the
|
||||
// JSR from `collides_at(...)` refers to this label).
|
||||
// 3. The `__collides_at_used` marker is present — that's the
|
||||
// linker's gate for splicing the helper. A regression that
|
||||
// stopped emitting the marker would link successfully but
|
||||
// with no subroutine behind the JSR, and the ROM would
|
||||
// run into random bytes.
|
||||
let source = include_str!("../examples/metatiles_demo.ne");
|
||||
let rom = compile(source);
|
||||
let info = rom::validate_ines(&rom).expect("metatiles_demo should produce valid iNES");
|
||||
assert_eq!(info.mapper, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collides_at_reads_active_room_bitmap() {
|
||||
// The `paint_room Name` + `collides_at(x, y)` contract:
|
||||
// `paint_room` installs the room's collision-bitmap address
|
||||
// into `ZP_ROOM_COL_LO` / `ZP_ROOM_COL_HI` (ZP `$18`/`$19`),
|
||||
// and `__collides_at` reads `(lo, hi),Y` where `Y = (y & 0xF0)
|
||||
// | (x >> 4)`. If the room pointer never gets stored, every
|
||||
// query reads from `$0000` (the frame flag plus controller
|
||||
// bytes) and returns garbage. Verify both the store and the
|
||||
// indirect-Y load land in the ROM.
|
||||
let source = r#"
|
||||
game "CollidesAt" { mapper: NROM }
|
||||
metatileset MTS {
|
||||
metatiles: [
|
||||
{ id: 0, tiles: [0, 0, 0, 0], collide: false },
|
||||
{ id: 1, tiles: [0, 0, 0, 0], collide: true },
|
||||
],
|
||||
}
|
||||
room R {
|
||||
metatileset: MTS,
|
||||
layout: [0; 240],
|
||||
}
|
||||
var hit: u8 = 0
|
||||
on frame {
|
||||
paint_room R
|
||||
if collides_at(16, 16) { hit = 1 }
|
||||
}
|
||||
start Main
|
||||
"#;
|
||||
let rom = compile(source);
|
||||
let prg = &rom[16..16 + 16384];
|
||||
// `STA $18` is `85 18` (zero-page). Either the paint_room
|
||||
// lowering or someone else writing ZP slot `$18` produces
|
||||
// this pair.
|
||||
let has_room_col_store = prg.windows(2).any(|w| w == [0x85_u8, 0x18]);
|
||||
assert!(
|
||||
has_room_col_store,
|
||||
"paint_room should emit STA $18 (ZP_ROOM_COL_LO)"
|
||||
);
|
||||
// `LDA ($18),Y` is `B1 18` — the `__collides_at` helper's
|
||||
// indirect-Y read of the bitmap.
|
||||
let has_indirect_y_read = prg.windows(2).any(|w| w == [0xB1_u8, 0x18]);
|
||||
assert!(
|
||||
has_indirect_y_read,
|
||||
"__collides_at should emit LDA ($18),Y (indirect-Y bitmap read)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rooms_without_collides_at_skip_helper() {
|
||||
// Declaring a `room` without any `collides_at(...)` call
|
||||
// should not drag the `__collides_at` helper into PRG ROM.
|
||||
// This is the gating contract: the helper only links in when
|
||||
// the `__collides_at_used` marker is emitted.
|
||||
let source = r#"
|
||||
game "RoomNoCollide" { mapper: NROM }
|
||||
metatileset MTS {
|
||||
metatiles: [
|
||||
{ id: 0, tiles: [0, 0, 0, 0], collide: false },
|
||||
],
|
||||
}
|
||||
room R {
|
||||
metatileset: MTS,
|
||||
layout: [0; 240],
|
||||
}
|
||||
on frame {
|
||||
paint_room R
|
||||
}
|
||||
start Main
|
||||
"#;
|
||||
let rom = compile(source);
|
||||
let prg = &rom[16..16 + 16384];
|
||||
// `LDA ($18),Y` is `B1 18`. If the helper got spliced in it
|
||||
// would contain this byte pair; if it was gated out the pair
|
||||
// shouldn't appear anywhere.
|
||||
let has_indirect_y_read = prg.windows(2).any(|w| w == [0xB1_u8, 0x18]);
|
||||
assert!(
|
||||
!has_indirect_y_read,
|
||||
"a room without collides_at() should not splice the helper — no LDA ($18),Y expected"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmp_signed_against_zero_is_negative_check() {
|
||||
// Negative analyzer test isn't applicable here (the analyzer
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue