Compare commits

1 Commits
Author SHA1 Message Date
randrews 623e4aeda7 Initial commit 2026-10-31 18:12:15 -05:00
17 changed files with 3013 additions and 523 deletions
+3 -5
View File
@@ -1,13 +1,11 @@
# Build output # Build output
/target/ /target/
# Generated from the ROM by ../recompile.py. These are derived from the # The game reads its data from the original ROM at runtime; never commit it.
# copyrighted game, so keep them out of the repo; regenerate with: *.gb
# python3 ../recompile.py "../Daedalian Opus (USA).gb" src/game.rs assets/rom_data.bin
/src/game.rs
/assets/rom_data.bin
# Headless test output # Headless test output
/out/
*.wav *.wav
# Editors / OS # Editors / OS
Generated
+9 -9
View File
@@ -391,15 +391,6 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f"
[[package]]
name = "daedalian"
version = "0.1.0"
dependencies = [
"cpal",
"pixels",
"winit",
]
[[package]] [[package]]
name = "dasp_sample" name = "dasp_sample"
version = "0.11.0" version = "0.11.0"
@@ -1297,6 +1288,15 @@ version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "opus"
version = "0.1.0"
dependencies = [
"cpal",
"pixels",
"winit",
]
[[package]] [[package]]
name = "orbclient" name = "orbclient"
version = "0.3.55" version = "0.3.55"
+2 -2
View File
@@ -1,8 +1,8 @@
[package] [package]
name = "daedalian" name = "opus"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
description = "Daedalian Opus, statically recompiled from its Game Boy disassembly" description = "Daedalian Opus, ported from its Game Boy disassembly to Rust"
[dependencies] [dependencies]
winit = "0.30" winit = "0.30"
+64 -26
View File
@@ -1,42 +1,80 @@
# Daedalian Opus — recompiled to Rust # Daedalian Opus, ported to Rust
A proof of concept: the Game Boy game's code, statically translated from the A rewrite of the Game Boy game as ordinary Rust, translated by hand from the
disassembly into Rust, running on a tiny hand-written hardware layer. disassembly in `../disassembly/`. Unlike `../rust/` (a static recompilation that
still runs the original instructions against emulated registers and memory),
this is the game itself: the screens, the puzzle rules, the map, the story and
the music sequencer are Rust functions over named state, so they can be read
and changed.
``` ```
cargo run --release cargo run --release # looks for "Daedalian Opus (USA).gb" here or in ..
cargo run --release -- --rom PATH
``` ```
Controls: arrows = D-pad, Z = A, X = B, Enter = Start, Right Shift / Backspace = Select, Esc = quit. Controls: arrows = D-pad, Z = A, X = B, Enter = Start, Right Shift / Backspace = Select, Esc = quit.
## How it works ## What comes from where
- `src/game.rs` is **generated** by `../recompile.py` from the traced The program contains no game content. At startup `rom.rs` parses the ROM
disassembly: every SM83 instruction becomes Rust statements, grouped into into typed data: levels (`Level`: starting pieces, board layout, cursor
basic blocks (`match m.pc { 0x0164 => { ... } }`), each commented with the start, music, pieces needed, which piece drops on the map), piece shapes,
original assembly. No SM83 code is decoded at runtime. The game's stack lives screen layouts (tilemap draw scripts), story text, passwords, tile graphics
in emulated memory because the game manipulates return addresses itself. and the music byte code. Small numbers that are really code (coordinates,
- `assets/rom_data.bin` is the ROM with every code byte zeroed — only the tile numbers for digits, timings) live in the Rust source as named constants.
data (levels, graphics, music, text) is used. The runtime warns if the game
ever reads a code byte as data.
- `src/machine.rs`: registers/flags/ALU, memory map, joypad, OAM DMA, the
vblank interrupt, and a once-per-frame LCD renderer.
- `src/apu.rs`: the four sound channels, synthesized in step with the
recompiled code; played through `cpal`.
- `src/main.rs`: `winit` window + `pixels` framebuffer, 59.73 Hz pacing.
Regenerate after changing the recompiler: ## Layout
``` | file | what |
python3 ../recompile.py "../Daedalian Opus (USA).gb" src/game.rs assets/rom_data.bin |---|---|
``` | `src/game/mod.rs` | `Game` state, the top-level flow (title → story → map → puzzle → ...), the per-frame tasks, the clear-time clock |
| `src/game/puzzle.rs` | the puzzle: picking up, moving, rotating, flipping and placing pieces; the win check; reset; the level-clear screen |
| `src/game/map.rs` | the map between levels: walking, castle doors, the bridge, the password bird, the piece that drops from the sky |
| `src/game/menus.rs` | title, how to play, password entry, puzzle list |
| `src/game/story.rs` | story screens and the ending |
| `src/sound.rs` | the game's sound driver: an 8-channel byte-code sequencer (format documented at the top) |
| `src/apu.rs` | the Game Boy sound chip, as the instrument the driver plays |
| `src/video.rs` | tile graphics, two background maps, 40 sprites, palettes, and the renderer |
| `src/rom.rs` | ROM loading and parsing |
| `src/input.rs` | buttons, with the game's d-pad auto-repeat |
| `src/main.rs` | window (`winit` + `pixels`), audio (`cpal`), frame pacing |
| `src/headless.rs` | scripted runs for testing |
The game code is written the way the original is structured: straight-line
code that calls `wait_frame()` whenever it needs the next frame (to animate,
or to wait for a button). It runs on its own thread; `wait_frame` sends the
finished picture and sound to the host and blocks until the host sends the
next frame's buttons.
The display model (tiles, maps, sprites) is kept because it's what the art is
made of, so the game still draws by placing tiles and sprites. Map addresses
like `0x9800` remain the way cells are named, since the screen layout scripts
in the ROM use them.
## Faithfulness
It's checked against the recompiled build in `../rust/` with the headless
runner. Same inputs give the same screens (title, every menu, story pages,
map walks, levels 1-3 and 36 solved, the ending), and the sound driver's chip
register writes match frame for frame. Known differences:
- Loading a screen takes no time here, where the original spent a few frames
copying graphics with the display off, so the port runs a few frames ahead.
- The original queued tile updates to the next vblank; here they happen
immediately, so some changes appear one frame sooner.
- The original's unused "resume music" sound (id $96) points at garbage; it
just resumes.
Original quirks are kept on purpose and marked `(sic)` in the source. For
example, undoing a Select flip that doesn't fit uses Start's rule.
## Headless testing ## Headless testing
``` ```
cargo run -- --headless OUT_DIR 120 start:3 90 shot:title peek:C0A1:2 goto:44:54 cargo run --release -- --headless OUT_DIR 120 start:2 90 shot:title probe goto:44:54
``` ```
Tokens: `N` run N frames, `BUTTON:N` hold a button N frames, `shot:NAME` save a Tokens: `N` run N frames; `BUTTON:N` hold a button for N frames (up down left
PNG, `peek:ADDR:N` print memory, `goto:YY:XX` steer the puzzle cursor. All right a b start select); `shot:NAME` save a PNG; `probe` print the level and
audio is written to `OUT_DIR/audio.wav`. cursor; `goto:YY:XX` steer the puzzle cursor (hex sprite coordinates). Audio
goes to `OUT_DIR/audio.wav`.
+39 -12
View File
@@ -1,9 +1,34 @@
//! Game Boy APU: two pulse channels (one with sweep), the wave channel, and //! The Game Boy sound chip: two pulse channels (one with sweep), the wave
//! the noise channel, mixed to stereo f32 samples. Registers are updated as //! channel and the noise channel, mixed to stereo f32 samples.
//! the game writes them; samples are synthesized up to the current CPU cycle //!
//! before every register write, so timing follows the recompiled code. //! This is the instrument the game's music was written for; the game's own
//! sound driver (`sound.rs`) plays it by setting its registers (NR10-NR52 and
//! wave RAM, addressed $FF10-$FF3F as on the hardware) once per frame.
use crate::machine::CPU_HZ; /// Clock the chip's timing is expressed in (the Game Boy CPU clock).
pub const CLOCK_HZ: u64 = 4_194_304;
const CPU_HZ: u64 = CLOCK_HZ;
pub const NR10: u16 = 0xFF10;
pub const NR11: u16 = 0xFF11;
pub const NR12: u16 = 0xFF12;
pub const NR13: u16 = 0xFF13;
pub const NR14: u16 = 0xFF14;
pub const NR21: u16 = 0xFF16;
pub const NR22: u16 = 0xFF17;
pub const NR23: u16 = 0xFF18;
pub const NR24: u16 = 0xFF19;
pub const NR30: u16 = 0xFF1A;
pub const NR32: u16 = 0xFF1C;
pub const NR33: u16 = 0xFF1D;
pub const NR34: u16 = 0xFF1E;
pub const NR42: u16 = 0xFF21;
pub const NR43: u16 = 0xFF22;
pub const NR44: u16 = 0xFF23;
pub const NR50: u16 = 0xFF24;
pub const NR51: u16 = 0xFF25;
pub const NR52: u16 = 0xFF26;
pub const WAVE_RAM: u16 = 0xFF30;
const DUTY: [[u8; 8]; 4] = [ const DUTY: [[u8; 8]; 4] = [
[0, 0, 0, 0, 0, 0, 0, 1], [0, 0, 0, 0, 0, 0, 0, 1],
@@ -84,6 +109,8 @@ pub struct Apu {
wave: Wave, wave: Wave,
noise: Noise, noise: Noise,
sample_rate: u32, sample_rate: u32,
/// clock cycles synthesized so far
now: u64,
samples_made: u64, samples_made: u64,
last_cycle: u64, last_cycle: u64,
seq_cycles: u64, seq_cycles: u64,
@@ -103,6 +130,7 @@ impl Apu {
wave: Wave::default(), wave: Wave::default(),
noise: Noise::default(), noise: Noise::default(),
sample_rate, sample_rate,
now: 0,
samples_made: 0, samples_made: 0,
last_cycle: 0, last_cycle: 0,
seq_cycles: 0, seq_cycles: 0,
@@ -121,8 +149,7 @@ impl Apu {
self.regs[(addr - 0xFF10) as usize] self.regs[(addr - 0xFF10) as usize]
} }
pub fn read(&mut self, addr: u16, cycles: u64) -> u8 { pub fn read(&self, addr: u16) -> u8 {
self.catch_up(cycles);
if addr == 0xFF26 { if addr == 0xFF26 {
return (self.reg(0xFF26) & 0x80) return (self.reg(0xFF26) & 0x80)
| 0x70 | 0x70
@@ -134,8 +161,7 @@ impl Apu {
self.reg(addr) self.reg(addr)
} }
pub fn write(&mut self, addr: u16, v: u8, cycles: u64) { pub fn write(&mut self, addr: u16, v: u8) {
self.catch_up(cycles);
let powered = self.reg(0xFF26) & 0x80 != 0; let powered = self.reg(0xFF26) & 0x80 != 0;
if !powered && addr != 0xFF26 && addr < 0xFF30 { if !powered && addr != 0xFF26 && addr < 0xFF30 {
return; return;
@@ -277,10 +303,11 @@ impl Apu {
} }
} }
/// Synthesize samples up to CPU cycle `cycles`. /// Synthesize the next `cycles` clock cycles of sound into `out`.
pub fn catch_up(&mut self, cycles: u64) { pub fn run(&mut self, cycles: u64) {
self.now += cycles;
let rate = self.sample_rate as f32; let rate = self.sample_rate as f32;
let target = cycles * self.sample_rate as u64 / CPU_HZ; let target = self.now * self.sample_rate as u64 / CPU_HZ;
while self.samples_made < target { while self.samples_made < target {
self.samples_made += 1; self.samples_made += 1;
let cyc = self.samples_made * CPU_HZ / self.sample_rate as u64; let cyc = self.samples_made * CPU_HZ / self.sample_rate as u64;
+370
View File
@@ -0,0 +1,370 @@
//! The map screen between levels: the hero walks from the last castle to the
//! next one. On the way a bridge may build itself, a bird flies over writing
//! the level's password, and on some levels a new piece drops from the sky.
use super::*;
use crate::input::*;
use crate::rom::addr::*;
use crate::sound::*;
use crate::video::map_cell;
const LAST_LEVEL: u8 = 35;
// hero shapes
const FACE_UP: u8 = 0x41;
const STAND_RIGHT: u8 = 0x42;
const STAND_LEFT: u8 = 0x43;
const WALK_RIGHT: u8 = 0x44;
const WALK_LEFT: u8 = 0x45;
const LEAVE_CASTLE: u8 = 0x48;
const ENTER_CASTLE: u8 = 0x4C;
const HERO_Y: u8 = 0x6C;
const STEP: u8 = 4;
const STEP_FRAMES: u8 = 11;
/// where the bridge starts building, and where a new piece drops
const BRIDGE_TRIGGER_X: u8 = 0x28;
const PIECE_DROP_X: u8 = 0x50;
const BRIDGE_AT: u16 = map_cell(MAP0, 6, 13);
const BRIDGE_TILES: [u8; 8] = [0x30, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x32];
/// The bird flies in a 16-bit fixed-point x position; it writes one letter
/// at each of these x positions.
const MESSENGER_LETTER_X: [u8; 4] = [0x44, 0x4C, 0x54, 0x5C];
const MESSENGER_DONE_X: u8 = 0xC0;
const PASSWORD_AT: u16 = map_cell(MAP0, 8, 3);
/// tile = ASCII + this
const PASSWORD_TILE_OFFSET: u8 = 0x9D;
#[derive(Clone, Copy, Default, PartialEq)]
pub(super) enum BridgeState {
#[default]
Idle,
Triggered,
Building,
}
#[derive(Clone, Copy, Default)]
pub(super) struct Bridge {
pub state: BridgeState,
built: u8,
delay: u8,
}
#[derive(Clone, Copy, Default, PartialEq)]
pub(super) enum MessengerState {
#[default]
Idle,
Launch,
Flying,
}
#[derive(Clone, Copy, Default)]
pub(super) struct Messenger {
pub state: MessengerState,
x: u16,
}
impl Game {
pub(super) fn map_screen(&mut self) {
let data = self.data.clone();
let level = &data.levels[self.level as usize];
self.clear_actors();
if level.new_piece != 0 {
self.s.actors[MARKER] = Actor::at(0x68, 0x56, 0x56);
}
self.video.load(0x8000, data.gfx(GFX_SPRITES, 0x800));
self.video.load(0x8800, data.gfx(GFX_FONT, 0x800));
self.video.load(0x9000, data.gfx(GFX_MAP_TERRAIN, 0x800));
self.video.fill_maps(0x33);
let scripts = &data.scripts;
self.video.draw(&scripts.map);
let scene = match self.level {
0 => 0,
LAST_LEVEL => 2,
_ => 1,
};
self.video.draw(&scripts.map_scenes[scene]);
self.video.draw(&scripts.map_castles[level.map_castle as usize]);
self.video.draw(&scripts.map_next_castles[level.map_next_castle as usize]);
if self.level != 0 && self.level != LAST_LEVEL {
// castle 12 is special and has no number on it
if self.level != 12 {
self.draw_castle_number(1, self.level);
}
if self.level + 1 != 12 {
self.draw_castle_number(17, self.level + 1);
}
}
self.set_palettes(PALETTE);
self.play(MUSIC_MAP);
self.show(MAP0);
}
/// Two-digit number, two tiles tall, at row 7.
fn draw_castle_number(&mut self, col: u16, n: u8) {
let (tens, ones) = (n / 10, n % 10);
for (c, tile) in [(col, 0x6C + 2 * tens), (col + 1, 0x58 + 2 * ones)] {
self.video.poke(map_cell(MAP0, c, 7), tile);
self.video.poke(map_cell(MAP0, c, 8), tile + 1);
}
}
/// Walk around until the hero goes into a castle.
pub(super) fn map_walk(&mut self) {
match self.level {
0 => {
self.s.actors[HERO] = Actor::at(HERO_Y, 0x38, STAND_RIGHT);
(self.s.walk_left, self.s.walk_right) = (0x38, 0x78);
}
LAST_LEVEL => {
(self.s.walk_left, self.s.walk_right) = (0x08, 0x64);
self.leave_castle(Actor::at(HERO_Y, 0x08, LEAVE_CASTLE));
self.s.messenger.state = MessengerState::Launch;
}
_ => {
(self.s.walk_left, self.s.walk_right) = (0x10, 0x90);
self.leave_castle(Actor::at(HERO_Y, 0x10, LEAVE_CASTLE));
self.s.messenger.state = MessengerState::Launch;
}
}
loop {
self.wait_frame();
if self.try_enter_castle() {
return;
}
self.walk();
}
}
/// Up at a castle door: forward to the next level, or back to the
/// previous one (when facing left at the left castle).
fn try_enter_castle(&mut self) -> bool {
if self.joy.pressed & UP == 0 {
return false;
}
let hero = self.s.actors[HERO];
if hero.x == self.s.walk_left {
if self.level == 0 || hero.shape == STAND_RIGHT {
return false;
}
self.enter_castle();
self.level -= 1;
return true;
}
if hero.x == self.s.walk_right {
if self.level == LAST_LEVEL {
self.climb_into_tower();
} else {
self.enter_castle();
}
return true;
}
false
}
/// Walks while the d-pad is held (this blocks, a step at a time).
fn walk(&mut self) {
if self.joy.held & RIGHT != 0 {
self.s.actors[HERO].shape = WALK_RIGHT;
loop {
self.step(self.s.walk_right, STEP);
if self.s.actors[HERO].x == BRIDGE_TRIGGER_X {
self.s.bridge.state = BridgeState::Triggered;
}
if self.joy.held & RIGHT == 0 {
break;
}
}
self.s.actors[HERO].shape = STAND_RIGHT;
} else if self.joy.held & LEFT != 0 {
self.s.actors[HERO].shape = WALK_LEFT;
loop {
self.step(self.s.walk_left, STEP.wrapping_neg());
if self.joy.held & LEFT == 0 {
break;
}
}
self.s.actors[HERO].shape = STAND_LEFT;
}
}
fn step(&mut self, limit: u8, dx: u8) {
self.play(SFX_STEP);
self.wait_frames(STEP_FRAMES);
let hero = &mut self.s.actors[HERO];
hero.shape ^= 2; // swing legs
if hero.x == limit {
return;
}
hero.x = hero.x.wrapping_add(dx);
self.check_piece_drop();
}
/// Two frames of animation, four times each, then the next pair.
fn animate_pair(&mut self, times: u8) {
for _ in 0..times {
self.play(SFX_STEP);
self.wait_frames(12);
self.s.actors[HERO].shape ^= 1;
}
}
fn leave_castle(&mut self, hero: Actor) {
self.wait_frames(30);
self.s.actors[HERO] = hero;
self.animate_pair(4);
self.s.actors[HERO].shape += 2;
self.animate_pair(4);
self.s.actors[HERO].shape = STAND_RIGHT;
}
fn enter_castle(&mut self) {
self.s.actors[HERO].shape = ENTER_CASTLE;
self.animate_pair(4);
self.s.actors[HERO].shape += 2;
self.animate_pair(4);
self.clear_actors();
self.s.messenger.state = MessengerState::Idle;
self.wait_frames(30);
}
/// The last level's door is up a wall.
fn climb_into_tower(&mut self) {
self.s.actors[HERO].shape = ENTER_CASTLE;
self.climb();
self.s.actors[HERO].shape += 2;
self.s.actors[HERO].y += 2;
self.climb();
self.s.actors[HERO].visible = false;
self.wait_frames(30);
}
fn climb(&mut self) {
for _ in 0..6 {
self.play(SFX_STEP);
self.wait_frames(12);
let hero = &mut self.s.actors[HERO];
hero.shape ^= 1;
hero.y -= 3;
}
}
/// Halfway along, some levels drop a new piece from the sky.
fn check_piece_drop(&mut self) {
if self.s.piece_drop_done || self.s.actors[HERO].x != PIECE_DROP_X {
return;
}
let piece = self.data.levels[self.level as usize].new_piece;
if piece != 0 {
self.drop_piece(piece);
}
self.s.piece_drop_done = true;
}
fn drop_piece(&mut self, piece: u8) {
let facing = self.s.actors[HERO].shape;
self.s.actors[HERO].shape = FACE_UP;
self.s.actors[CURSOR] = Actor::at(0xE0, PIECE_DROP_X, 0);
self.s.held = piece;
self.play(SFX_CONFIRM);
self.wait_frames(30);
self.s.actors[MARKER].visible = false;
loop {
// tumble down
self.play(SFX_MOVE_PIECE);
self.wait_frames(4);
self.s.held = (self.s.held & 0xFC) | (self.s.held.wrapping_add(1) & 3);
let p = &mut self.s.actors[CURSOR];
p.y = p.y.wrapping_add(4);
if p.y == 0x50 {
break;
}
}
self.play(SFX_PIECE_LANDS);
for _ in 0..2 {
self.set_palettes(0xE4); // flash
self.wait_frames(10);
self.set_palettes(PALETTE);
self.wait_frames(10);
}
self.wait_frames(30);
self.s.actors[CURSOR].visible = false;
self.play(JINGLE_LEVEL_CLEAR);
for _ in 0..2 {
self.s.actors[HERO].shape = STAND_RIGHT;
self.wait_frames(20);
self.s.actors[HERO].shape = FACE_UP;
self.wait_frames(20);
}
self.s.actors[HERO].shape = facing;
}
// ---- per-frame map animations ----
pub(super) fn update_bridge(&mut self) {
let b = &mut self.s.bridge;
match b.state {
BridgeState::Idle => {}
BridgeState::Triggered => {
// the first and last maps have no river, and it only builds once
if self.level == 0 || self.level == LAST_LEVEL || b.built != 0 {
b.state = BridgeState::Idle;
} else {
b.state = BridgeState::Building;
b.delay = 1;
self.sound.play(SFX_BRIDGE);
}
}
BridgeState::Building => {
b.delay -= 1;
if b.delay != 0 {
return;
}
b.delay = 5;
let i = b.built;
self.video.poke(BRIDGE_AT + i as u16, BRIDGE_TILES[i as usize]);
if i + 1 == BRIDGE_TILES.len() as u8 {
b.state = BridgeState::Idle;
} else {
b.built += 1;
}
}
}
}
pub(super) fn update_marker_blink(&mut self) {
self.s.marker_blink = self.s.marker_blink.wrapping_add(1);
if self.s.marker_blink & 0x1F == 0 {
self.s.actors[MARKER].shape ^= 1;
}
}
pub(super) fn update_messenger(&mut self) {
let m = &mut self.s.messenger;
match m.state {
MessengerState::Idle => {}
MessengerState::Launch => {
m.state = MessengerState::Flying;
m.x = 0xF000;
self.s.actors[MESSENGER] = Actor::at(0x24, 0xE0, 0x58);
}
MessengerState::Flying => {
m.x = m.x.wrapping_add(0x80);
let x = (m.x >> 8) as u8;
let bird = &mut self.s.actors[MESSENGER];
bird.x = x;
if let Some(i) = MESSENGER_LETTER_X.iter().position(|&lx| lx == x) {
let letter = self.data.passwords[self.level as usize - 1][i];
self.video.poke(PASSWORD_AT + i as u16, letter.wrapping_add(PASSWORD_TILE_OFFSET));
self.sound.play(SFX_PASSWORD_LETTER);
} else if x == MESSENGER_DONE_X {
m.state = MessengerState::Idle;
} else {
bird.shape ^= (x >> 4) & 1; // flap
}
}
}
}
}
+287
View File
@@ -0,0 +1,287 @@
//! Title screen, how-to-play, password entry and the puzzle list.
use super::*;
use crate::input::*;
use crate::rom::addr::*;
use crate::sound::*;
use crate::video::{MAP0, MAP1};
pub enum TitleChoice {
Start,
Password,
HowToPlay,
}
/// Title menu arrow rows.
const TITLE_START_Y: u8 = 0x60;
const TITLE_PASSWORD_Y: u8 = 0x70;
/// Password screen: a 7x4 grid of letters A-Z with a cursor.
const LETTER_TOP: u8 = 0x40;
const LETTER_BOTTOM: u8 = 0x70;
const LETTER_LEFT: u8 = 0x30;
const LETTER_RIGHT: u8 = 0x90;
const PASSWORD_ENTRY: u16 = 0x9868; // where the typed letters appear
const BLANK_LETTER: u8 = 0x7D;
const FIRST_LETTER_TILE: u8 = 0x5E; // 'A'
/// Puzzle list: two pages (maps 0 and 1) of 6 rows x 3 columns.
const LIST_TOP: u8 = 0x30;
const LIST_BOTTOM: u8 = 0x80;
const LIST_LEFT: u8 = 0x10;
const LIST_RIGHT: u8 = 0x70;
const LIST_PAGE: u8 = 18;
const PUZZLE_LIST_LEVEL: u8 = 36;
impl Game {
pub(super) fn title_screen(&mut self) -> TitleChoice {
let data = self.data.clone();
self.video.load(0x8000, data.gfx(GFX_SPRITES, 0x1800));
self.clear_actors();
self.s.actors[MENU_CURSOR] = Actor::at(TITLE_START_Y, 0x38, 0x55);
// map 0 is the title; map 1 is the same without "PUSH START"
self.video.fill_maps(0xEB);
self.video.draw(&data.scripts.title);
self.video.copy_map(MAP0, MAP1);
self.video.draw(&data.scripts.title_alt);
self.set_palettes(PALETTE);
self.play(MUSIC_TITLE);
self.show(MAP0);
self.joy.held = 0xFF; // ignore buttons already down
let mut blink: u8 = 0x60;
loop {
self.wait_frame();
let p = self.joy.pressed;
let arrow = &mut self.s.actors[MENU_CURSOR].y;
if p & A != 0 {
return TitleChoice::HowToPlay;
} else if p & SELECT != 0 {
*arrow = if *arrow == TITLE_START_Y { TITLE_PASSWORD_Y } else { TITLE_START_Y };
} else if p & START != 0 {
self.play(SFX_CONFIRM);
return if self.s.actors[MENU_CURSOR].y == TITLE_PASSWORD_Y {
TitleChoice::Password
} else {
TitleChoice::Start
};
} else if p & UP != 0 {
*arrow = TITLE_START_Y;
} else if p & DOWN != 0 {
*arrow = TITLE_PASSWORD_Y;
} else {
// flash "PUSH START": 32 frames on, 32 off
blink = blink.wrapping_add(1);
if blink == 0 {
blink = 0x60;
self.show(MAP0);
} else if blink == 0x80 {
blink = 0xE0;
self.show(MAP1);
}
continue;
}
self.play(SFX_MENU_MOVE);
}
}
pub(super) fn how_to_play(&mut self) {
let data = self.data.clone();
self.video.load(0x8800, data.gfx(GFX_HOW_TO_PLAY, 0x800));
self.video.load(0x9000, data.gfx(GFX_FONT, 0x800));
self.video.fill_maps(0x7F);
self.video.draw(&data.scripts.how_to_play);
self.clear_actors();
self.set_palettes(PALETTE);
self.show(MAP0);
self.play(MUSIC_MAP);
loop {
self.wait_frame();
if self.joy.pressed & (A | START) != 0 {
return;
}
}
}
/// Sets `self.level` from a valid password.
pub(super) fn password_screen(&mut self) {
let data = self.data.clone();
self.load_menu_tiles();
self.video.fill_maps(0x7F);
self.clear_actors();
self.video.draw(&data.scripts.password);
self.set_palettes(PALETTE);
self.play(MUSIC_TITLE);
self.show(MAP0);
self.wait_frame();
loop {
let mut entry = [BLANK_LETTER; 4];
let mut len = 0;
self.s.actors[HERO] = Actor::at(LETTER_TOP, LETTER_LEFT, 0x53);
loop {
for (i, &t) in entry.iter().enumerate() {
self.video.poke(PASSWORD_ENTRY + i as u16, t);
}
self.wait_frame();
if len == 4 {
self.wait_frames(30);
if let Some(level) = self.check_password(&entry) {
self.level = level;
return;
}
self.play(SFX_ERROR);
break;
}
self.move_letter_cursor();
let cursor = self.s.actors[HERO];
if self.joy.pressed & A != 0 {
entry[len] = letter_at(cursor.y, cursor.x).wrapping_add(FIRST_LETTER_TILE);
len += 1;
} else if self.joy.pressed & B != 0 && len > 0 {
len -= 1;
entry[len] = BLANK_LETTER;
}
}
}
}
fn load_menu_tiles(&mut self) {
let font = self.data.clone();
self.video.load(0x8000, font.gfx(GFX_FONT, 0x800));
self.video.load(0x9000, font.gfx(GFX_FONT, 0x800));
}
fn move_letter_cursor(&mut self) {
let r = self.joy.repeat;
let Actor { mut y, mut x, .. } = self.s.actors[HERO];
let mut moved = 0;
if r & RIGHT != 0 {
if x != LETTER_RIGHT {
x += 0x10;
moved += 1;
if letter_at(y, x) >= 26 {
y = 0x60; // off the end of the last row: go up one
}
}
} else if r & LEFT != 0 && x != LETTER_LEFT {
x -= 0x10;
moved += 1;
}
if r & UP != 0 {
if y != LETTER_TOP {
y -= 0x10;
moved += 1;
}
} else if r & DOWN != 0 && y != LETTER_BOTTOM {
y += 0x10;
moved += 1;
if letter_at(y, x) >= 26 {
x = 0x70; // 'Z'
}
}
for _ in 0..moved {
self.play(SFX_MENU_MOVE); // once per axis moved
}
let c = &mut self.s.actors[HERO];
(c.y, c.x) = (y, x);
}
fn check_password(&self, entry: &[u8; 4]) -> Option<u8> {
let typed = entry.map(|t| t.wrapping_sub(FIRST_LETTER_TILE).wrapping_add(b'A'));
self.data.passwords.iter().position(|p| *p == typed).map(|i| i as u8 + 1)
}
/// Level select, reached with the password after the last level.
pub(super) fn puzzle_list(&mut self) {
if self.level != PUZZLE_LIST_LEVEL {
return;
}
let data = self.data.clone();
self.load_menu_tiles();
self.video.fill_maps(0x7F);
self.clear_actors();
self.video.draw(&data.scripts.puzzle_list);
self.set_palettes(PALETTE);
self.play(MUSIC_TITLE);
self.show(MAP0);
self.s.actors[MENU_CURSOR] = Actor::at(LIST_TOP, LIST_LEFT, 0x54);
let mut page = 0;
loop {
self.wait_frame();
if let Some(p) = self.move_list_cursor(page) {
page = p;
continue;
}
let p = self.joy.pressed;
if p & A != 0 || (p & SELECT == 0 && p & START != 0) {
let Actor { y, x, .. } = self.s.actors[MENU_CURSOR];
let col = match x {
LIST_LEFT => 0,
0x40 => 1,
_ => 2,
};
self.level = page + ((y - LIST_TOP) >> 4) * 3 + col;
self.play(SFX_CONFIRM);
return;
}
if p & SELECT != 0 {
self.play(SFX_MENU_MOVE);
if page != 0 {
self.show(MAP0);
page = 0;
} else {
self.show(MAP1);
page = LIST_PAGE;
}
}
}
}
/// Returns the new page if the cursor moved off the top/bottom of one.
fn move_list_cursor(&mut self, page: u8) -> Option<u8> {
let r = self.joy.repeat;
let c = &mut self.s.actors[MENU_CURSOR];
let mut moved = 0;
if r & DOWN != 0 {
if c.y != LIST_BOTTOM {
c.y += 0x10;
moved += 1;
} else if page == 0 {
c.y = LIST_TOP;
self.video.display = Display::On(MAP1);
self.sound.play(SFX_MENU_MOVE);
return Some(LIST_PAGE);
}
} else if r & UP != 0 {
if c.y != LIST_TOP {
c.y -= 0x10;
moved += 1;
} else if page != 0 {
c.y = LIST_BOTTOM;
self.video.display = Display::On(MAP0);
self.sound.play(SFX_MENU_MOVE);
return Some(0);
}
}
if r & LEFT != 0 {
if c.x != LIST_LEFT {
c.x -= 0x30;
moved += 1;
}
} else if r & RIGHT != 0 && c.x != LIST_RIGHT {
c.x += 0x30;
moved += 1;
}
for _ in 0..moved {
self.sound.play(SFX_MENU_MOVE);
}
None
}
}
/// Letter number (0 = A) under the password cursor.
fn letter_at(y: u8, x: u8) -> u8 {
let row = y.wrapping_sub(LETTER_TOP) >> 4;
let col = x.wrapping_sub(LETTER_LEFT) >> 4;
row.wrapping_mul(7).wrapping_add(col)
}
+422
View File
@@ -0,0 +1,422 @@
//! The game itself.
//!
//! The game is written the way the original is: as straight-line code that
//! calls [`Game::wait_frame`] whenever it wants the next frame (to animate, to
//! wait for a button, ...). It runs on its own thread; `wait_frame` hands the
//! finished picture and sound to the host and blocks until the host sends the
//! next frame's buttons.
//!
//! Modules: `menus` (title, how to play, password, puzzle list), `story`
//! (story screens and the ending), `map` (the overworld walk between levels)
//! and `puzzle` (the puzzle itself and the level-clear screen).
mod map;
mod menus;
mod puzzle;
mod story;
use std::sync::mpsc::{Receiver, SyncSender};
use std::sync::Arc;
use crate::apu::{Apu, CLOCK_HZ};
use crate::input::Joypad;
use crate::rom::{GameData, Shape};
use crate::sound::SoundDriver;
use crate::video::{Display, Sprite, Video, MAP0, SCREEN_H, SCREEN_W};
/// Clock cycles per frame (the Game Boy runs at ~59.73 frames per second).
pub const FRAME_CYCLES: u64 = 70_224;
pub const FRAME_HZ: f64 = CLOCK_HZ as f64 / FRAME_CYCLES as f64;
/// The normal palette: shade n for color n, reversed (the game's art is
/// stored with 0 = black).
const PALETTE: u8 = 0x1B;
// ---- actor slots (multi-tile sprites, drawn every frame) ----
/// puzzle cursor / held piece; also the piece that drops on the map
const CURSOR: usize = 0;
/// menu arrow on the title screen and puzzle list
const MENU_CURSOR: usize = 1;
/// the hero on the map, the storyteller, the password cursor
const HERO: usize = 2;
/// "something here" marker on the map; the second figure in story scenes
const MARKER: usize = 3;
/// the bird that brings the password on the map
const MESSENGER: usize = 4;
const ACTOR_SLOTS: usize = 12;
/// A sprite made of one shape's cells, positioned at (y, x) in sprite
/// coordinates (screen + 16, screen + 8).
#[derive(Clone, Copy, Default)]
pub struct Actor {
pub visible: bool,
pub y: u8,
pub x: u8,
pub shape: u8,
}
impl Actor {
const fn at(y: u8, x: u8, shape: u8) -> Actor {
Actor { visible: true, y, x, shape }
}
}
/// Where a placed piece is and which board tiles it covers.
#[derive(Clone, Copy, Default)]
struct PieceSlot {
y: u8,
x: u8,
/// the board tiles under the piece's cells
under: [u8; 6],
}
/// The puzzle board: which piece id covers each background map cell.
type Board = Box<[u8; 0x400]>;
/// Scratch state that the original keeps in $C000-$CFFF and wipes at the
/// start of each puzzle (and at the title screen).
struct Scratch {
actors: [Actor; ACTOR_SLOTS],
/// id of the piece in hand (type << 4 | orientation), 0 = none
held: u8,
/// one slot per piece type
slots: [PieceSlot; 16],
slots_backup: [PieceSlot; 16],
board: Board,
board_backup: Board,
bridge: map::Bridge,
marker_blink: u8,
story: u8,
piece_drop_done: bool,
timer: Timer,
messenger: map::Messenger,
/// how far the hero can walk on the map
walk_left: u8,
walk_right: u8,
}
impl Default for Scratch {
fn default() -> Self {
Scratch {
actors: Default::default(),
held: 0,
slots: Default::default(),
slots_backup: Default::default(),
board: Box::new([0; 0x400]),
board_backup: Box::new([0; 0x400]),
bridge: Default::default(),
marker_blink: 0,
story: 0,
piece_drop_done: false,
timer: Timer::default(),
messenger: Default::default(),
walk_left: 0,
walk_right: 0,
}
}
}
#[derive(Clone, Copy, Default, PartialEq)]
enum TimerMode {
#[default]
Hidden,
Running,
Stopped,
}
/// The clear-time clock in the top right corner.
#[derive(Clone, Copy, Default)]
struct Timer {
mode: TimerMode,
frames: u8,
/// seconds, tens of seconds, minutes, tens of minutes, hours
digits: [u8; 5],
}
#[derive(Clone, Copy, Default)]
struct Palettes {
bg: u8,
obj0: u8,
obj1: u8,
/// palette 1 inverts every 16 frames (makes the cursor blink)
blink: u8,
}
/// What the game hands the host each frame.
pub struct Frame {
/// one shade (0 = white .. 3 = black) per pixel
pub pixels: Vec<u8>,
/// interleaved stereo samples
pub audio: Vec<f32>,
/// a peek at the game state, for testing
pub probe: Probe,
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Probe {
pub level: u8,
pub cursor_y: u8,
pub cursor_x: u8,
pub held: u8,
}
/// The game thread's end of the connection to the host.
pub struct Link {
pub frames: SyncSender<Frame>,
pub buttons: Receiver<u8>,
}
/// Unwinds the game thread when the host goes away.
struct Quit;
pub struct Game {
data: Arc<GameData>,
link: Link,
video: Video,
apu: Apu,
sound: SoundDriver,
joy: Joypad,
level: u8,
pieces_needed: u8,
palettes: Palettes,
/// sprites built this frame; the display picks them up at the next vblank
oam: [Sprite; 40],
s: Scratch,
}
/// Run the game on a new thread.
pub fn spawn(data: Arc<GameData>, sample_rate: u32, link: Link) -> std::thread::JoinHandle<()> {
std::thread::spawn(move || {
let mut game = Game::new(data, sample_rate, link);
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| game.run()));
if let Err(e) = result {
if !e.is::<Quit>() {
std::panic::resume_unwind(e);
}
}
})
}
impl Game {
fn new(data: Arc<GameData>, sample_rate: u32, link: Link) -> Game {
Game {
sound: SoundDriver::new(data.clone()),
data,
link,
video: Video::new(),
apu: Apu::new(sample_rate),
joy: Joypad::default(),
level: 0,
pieces_needed: 0,
palettes: Palettes::default(),
oam: [Sprite::default(); 40],
s: Scratch::default(),
}
}
fn run(&mut self) -> ! {
loop {
self.reset();
match self.title_screen() {
menus::TitleChoice::HowToPlay => {
self.how_to_play();
continue;
}
menus::TitleChoice::Password => {
self.password_screen();
self.puzzle_list();
}
menus::TitleChoice::Start => {}
}
self.play_levels();
self.ending();
}
}
/// Story, map, puzzle, repeat; returns after the last level.
fn play_levels(&mut self) {
loop {
self.s.timer.mode = TimerMode::Hidden;
self.story_screen();
self.story_text();
self.map_screen();
self.map_walk();
self.puzzle_screen();
self.s.timer.mode = TimerMode::Running;
loop {
self.wait_frame();
self.check_reset_puzzle();
self.move_cursor();
self.piece_action();
if self.pieces_in_place() == self.pieces_needed {
break;
}
}
self.s.timer.mode = TimerMode::Stopped;
self.level_clear();
if self.level as usize + 1 >= crate::rom::LEVEL_COUNT {
self.s.timer.mode = TimerMode::Hidden;
return;
}
self.level += 1;
}
}
/// Back to power-on state (the title screen clears all of RAM).
fn reset(&mut self) {
self.video.display = Display::Off;
self.s = Scratch::default();
self.level = 0;
self.pieces_needed = 0;
self.palettes = Palettes::default();
self.joy = Joypad::default();
self.sound = SoundDriver::new(self.data.clone());
}
// ---------------------------------------------------------------- frames
/// Finish this frame and wait for the next one.
fn wait_frame(&mut self) {
let mut pixels = vec![0; SCREEN_W * SCREEN_H];
self.video.render(&mut pixels);
let probe = Probe {
level: self.level,
cursor_y: self.s.actors[CURSOR].y,
cursor_x: self.s.actors[CURSOR].x,
held: self.s.held,
};
let frame = Frame { pixels, audio: std::mem::take(&mut self.apu.out), probe };
if self.link.frames.send(frame).is_err() {
std::panic::resume_unwind(Box::new(Quit));
}
let buttons = match self.link.buttons.recv() {
Ok(b) => b,
Err(_) => std::panic::resume_unwind(Box::new(Quit)),
};
// things the original does every vblank
self.video.sprites = self.oam;
self.update_palettes();
self.update_cursor_shape();
self.build_sprites();
self.joy.update(buttons);
self.update_bridge();
self.update_timer();
self.draw_timer();
self.update_marker_blink();
self.update_messenger();
self.sound.update(&mut self.apu);
self.apu.run(FRAME_CYCLES);
}
fn wait_frames(&mut self, n: u8) {
for _ in 0..n {
self.wait_frame();
}
}
fn update_palettes(&mut self) {
let p = &mut self.palettes;
self.video.bg_palette = p.bg;
self.video.obj_palettes = [p.obj0, p.obj1];
let (blink, wrapped) = p.blink.overflowing_add(0x10);
p.blink = blink;
if wrapped {
p.obj1 ^= 0xFF;
}
}
fn set_palettes(&mut self, v: u8) {
self.palettes.bg = v;
self.palettes.obj0 = v;
self.palettes.obj1 = v;
}
/// Turn the screen on showing the given background map.
fn show(&mut self, map: u16) {
self.video.display = Display::On(map);
}
fn play(&mut self, sound: u8) {
self.sound.play(sound);
}
fn shape(&self, n: u8) -> &Shape {
&self.data.shapes[n as usize]
}
fn clear_actors(&mut self) {
self.s.actors = Default::default();
self.s.held = 0;
}
/// The cursor actor shows the held piece, or the cursor itself (which
/// changes shape over the piece tray on the right).
fn update_cursor_shape(&mut self) {
let a = &mut self.s.actors[CURSOR];
a.shape = if self.s.held != 0 {
self.data.piece_shape(self.s.held)
} else if a.x >= 0xA4 {
0x5D
} else {
0
};
}
fn build_sprites(&mut self) {
let mut sprites = [Sprite::default(); 40];
let mut n = 0;
for a in self.s.actors.iter().filter(|a| a.visible) {
let shape = &self.data.shapes[a.shape as usize];
for cell in &shape.cells {
let (dy, dx) = cell.sprite_offset();
if let Some(s) = sprites.get_mut(n) {
*s = Sprite { y: a.y.wrapping_add(dy), x: a.x.wrapping_add(dx), tile: cell.tile, attr: shape.attr };
}
n += 1;
}
}
self.oam = sprites;
}
// ----------------------------------------------------------------- timer
fn update_timer(&mut self) {
let t = &mut self.s.timer;
if t.mode != TimerMode::Running {
return;
}
t.frames += 1;
if t.frames < 60 {
return;
}
t.frames = 0;
for (digit, limit) in t.digits.iter_mut().zip([10, 6, 10, 6, 10]) {
*digit += 1;
if *digit < limit {
return;
}
*digit = 0;
}
}
/// Two-tile-tall digits in the last 10 sprite slots: h mm ss.
fn draw_timer(&mut self) {
let t = self.s.timer;
if t.mode == TimerMode::Hidden {
return;
}
for (i, x) in [0x88, 0x80, 0x70, 0x68, 0x58].into_iter().enumerate() {
let tile = t.digits[i] * 2 + 0x64;
self.oam[30 + 2 * i] = Sprite { y: 0x10, x, tile, attr: 0 };
self.oam[31 + 2 * i] = Sprite { y: 0x18, x, tile: tile + 1, attr: 0 };
}
}
}
/// Map cell under a point in sprite coordinates.
fn cell_at(y: u8, x: u8) -> u16 {
let row = (y.wrapping_sub(16) >> 3) as u16;
let col = (x.wrapping_sub(8) >> 3) as u16;
MAP0 + row * 32 + col
}
+341
View File
@@ -0,0 +1,341 @@
//! The puzzle: pick up pieces, turn and flip them, and fit them all into the
//! board. Then the level-clear screen.
//!
//! The board is drawn into background map 0. Board floor tiles are $80 and
//! up, and the area to fill is the floor tiles at $F0 and up; everything below
//! $80 is a wall. `Scratch::board` records which piece covers each map cell,
//! and each piece type's slot remembers where it is and the floor tiles it
//! covers, so they can be put back when it's picked up.
use super::*;
use crate::input::*;
use crate::rom::addr::*;
use crate::sound::*;
use crate::video::{map_cell, MAP1};
const FLOOR: u8 = 0x80;
const TARGET_FLOOR: u8 = 0xF0;
/// Pieces must stay within this area (sprite coordinates).
const AREA_TOP: u8 = 0x20;
const AREA_BOTTOM: u8 = 0x99;
const AREA_LEFT: u8 = 0x08;
const AREA_RIGHT: u8 = 0xA1;
/// The empty cursor's range.
const CURSOR_MAX_Y: u8 = 0x9C;
const CURSOR_MIN_Y: u8 = 0x25;
const CURSOR_MIN_X: u8 = 0x0D;
const CURSOR_MAX_X: u8 = 0xA4;
/// Slot for a piece id (one per piece type).
fn slot(piece_id: u8) -> usize {
(piece_id.wrapping_sub(0x10) >> 4) as usize
}
/// A board cell covered by a shape: the map address and the tile.
struct Cell {
addr: u16,
tile: u8,
y: u8,
x: u8,
}
impl Game {
pub(super) fn puzzle_screen(&mut self) {
let data = self.data.clone();
let level = &data.levels[self.level as usize];
self.wait_frame();
self.video.load(0x8000, data.gfx(GFX_SPRITES, 0x1800));
self.s = Scratch::default();
self.video.fill_maps(0x84);
self.video.draw(&data.scripts.puzzle_hud);
self.draw_puzzle_number();
self.video.draw(&level.board);
for p in &level.pieces {
self.s.held = p.id;
self.s.actors[CURSOR].y = p.y.wrapping_add(16);
self.s.actors[CURSOR].x = p.x.wrapping_add(8);
self.update_cursor_shape();
let cells = self.held_cells();
self.cover(&cells);
self.put_down(&cells);
}
self.init_cursor();
// keep the starting position for "reset"
self.s.slots_backup = self.s.slots;
self.s.board_backup = self.s.board.clone();
self.video.copy_map(MAP0, MAP1);
self.pieces_needed = level.pieces_needed;
self.set_palettes(PALETTE);
self.play(level.music);
self.show(MAP0);
}
/// Puzzle number in the HUD, two tiles tall, left aligned.
fn draw_puzzle_number(&mut self) {
let n = self.level + 1;
let (tens, ones) = (n / 10, n % 10);
let digits: &[u8] = if tens != 0 { &[tens, ones] } else { &[ones] };
let mut cols = [(0x21, 0x29); 2]; // blank
for (i, d) in digits.iter().enumerate() {
cols[i] = (0x60 + 2 * d, 0x61 + 2 * d);
}
for (i, (top, bottom)) in cols.into_iter().enumerate() {
self.video.poke(map_cell(MAP0, 5 + i as u16, 0), top);
self.video.poke(map_cell(MAP0, 5 + i as u16, 1), bottom);
}
}
fn init_cursor(&mut self) {
let (y, x) = self.data.levels[self.level as usize].cursor;
self.s.actors[CURSOR] = Actor::at((y & 0xF8) | 4, (x & 0xF8) | 4, 0);
self.s.held = 0;
}
/// The held piece's cells, at its position snapped to the 8px grid.
fn held_cells(&self) -> Vec<Cell> {
let a = self.s.actors[CURSOR];
let shape = a.shape & 0x7F;
if shape == 0 {
return Vec::new();
}
let (y, x) = (a.y.wrapping_add(4) & 0xF8, a.x.wrapping_add(4) & 0xF8);
self.shape(shape)
.cells
.iter()
.map(|c| {
let (dy, dx) = c.board_offset();
let (cy, cx) = (y.wrapping_add(dy), x.wrapping_add(dx));
Cell { addr: cell_at(cy, cx), tile: c.tile, y: cy, x: cx }
})
.collect()
}
/// Record the floor tiles under the held piece in its slot. Fails (true)
/// on the first cell that isn't floor.
fn cover(&mut self, cells: &[Cell]) -> bool {
let slot = slot(self.s.held);
for (i, c) in cells.iter().enumerate() {
let under = self.video.peek(c.addr);
if under < FLOOR {
return true;
}
if let Some(u) = self.s.slots[slot].under.get_mut(i) {
*u = under;
}
}
false
}
/// Stamp the held piece onto the board.
fn put_down(&mut self, cells: &[Cell]) {
if cells.is_empty() {
return;
}
let a = self.s.actors[CURSOR];
let id = self.data.shape_piece_id(a.shape & 0x7F);
for c in cells {
self.s.board[(c.addr - MAP0) as usize] = id;
self.video.poke(c.addr, c.tile);
}
let slot = &mut self.s.slots[slot(id)];
(slot.y, slot.x) = (a.y.wrapping_add(4) & 0xF8, a.x.wrapping_add(4) & 0xF8);
self.play(SFX_PLACE);
}
/// Would `shape` at (y, x) stick out of the play area?
fn out_of_bounds(&self, shape: u8, y: u8, x: u8) -> bool {
self.shape(shape).cells.iter().any(|c| {
let (dy, dx) = c.board_offset();
let (cy, cx) = (y.wrapping_add(dy), x.wrapping_add(dx));
!(AREA_TOP..AREA_BOTTOM).contains(&cy) || !(AREA_LEFT..AREA_RIGHT).contains(&cx)
})
}
/// A pressed while B is held: put every piece back where it started.
pub(super) fn check_reset_puzzle(&mut self) {
if self.joy.pressed & A == 0 || self.joy.held & B == 0 {
return;
}
self.play(SFX_ROTATE);
self.s.actors[CURSOR].visible = false;
self.wipe_rows(MAP1, MAP0);
self.s.slots = self.s.slots_backup;
self.s.board = self.s.board_backup.clone();
self.init_cursor();
}
/// Copy the play area from one map to another, a row every two frames.
fn wipe_rows(&mut self, from: u16, to: u16) {
for row in 0..18 {
let row_tiles: Vec<u8> = (0..20).map(|c| self.video.peek(from + row * 32 + c)).collect();
self.wait_frame();
for (c, &t) in row_tiles.iter().enumerate() {
self.video.poke(to + row * 32 + c as u16, t);
}
self.wait_frame();
}
}
pub(super) fn move_cursor(&mut self) {
let r = self.joy.repeat;
if self.s.held == 0 {
let c = &mut self.s.actors[CURSOR];
if r & DOWN != 0 && c.y < CURSOR_MAX_Y {
c.y += 8;
}
if r & UP != 0 && c.y >= CURSOR_MIN_Y {
c.y -= 8;
}
if r & LEFT != 0 && c.x >= CURSOR_MIN_X {
c.x -= 8;
}
if r & RIGHT != 0 && c.x < CURSOR_MAX_X {
c.x += 8;
}
return;
}
// carrying a piece: it can't leave the play area
let shape = self.s.actors[CURSOR].shape;
let Actor { mut y, mut x, .. } = self.s.actors[CURSOR];
for (button, dy, dx) in [(DOWN, 8, 0), (UP, 0xF8, 0), (LEFT, 0, 0xF8), (RIGHT, 0, 8)] {
if r & button != 0 {
self.play(SFX_MOVE_PIECE);
let (ny, nx) = (y.wrapping_add(dy), x.wrapping_add(dx));
if !self.out_of_bounds(shape, ny, nx) {
(y, x) = (ny, nx);
}
}
}
let c = &mut self.s.actors[CURSOR];
(c.y, c.x) = (y, x);
}
/// A: pick up / put down. B: rotate. Select / Start: flip.
pub(super) fn piece_action(&mut self) {
let p = self.joy.pressed;
if p & A != 0 {
if self.s.held == 0 {
self.pick_up();
} else {
self.drop_held();
}
} else if p & B != 0 {
if self.s.held != 0 {
self.rotate();
}
} else if p & SELECT != 0 {
if self.s.held != 0 {
self.flip(true);
}
} else if p & START != 0 && self.s.held != 0 {
self.flip(false);
}
}
fn pick_up(&mut self) {
let a = self.s.actors[CURSOR];
let id = self.s.board[(cell_at(a.y, a.x) - MAP0) as usize];
if id == 0 {
return;
}
self.s.held = id;
let slot = self.s.slots[slot(id)];
(self.s.actors[CURSOR].y, self.s.actors[CURSOR].x) = (slot.y, slot.x);
self.update_cursor_shape();
for (c, under) in self.held_cells().iter().zip(slot.under) {
self.s.board[(c.addr - MAP0) as usize] = 0;
self.video.poke(c.addr, under);
}
self.play(SFX_PICK_UP);
}
fn drop_held(&mut self) {
let cells = self.held_cells();
self.wait_frame();
if self.cover(&cells) {
self.play(SFX_ERROR);
return;
}
self.put_down(&cells);
// the cursor moves to the piece's last cell
if let Some(last) = cells.last() {
(self.s.actors[CURSOR].y, self.s.actors[CURSOR].x) = (last.y + 4, last.x + 4);
}
self.s.held = 0;
self.update_cursor_shape();
}
fn rotate(&mut self) {
let held = self.s.held;
self.s.held = (held & 0xFC) | (held.wrapping_add(1) & 3);
self.update_cursor_shape();
let a = self.s.actors[CURSOR];
if self.out_of_bounds(a.shape, a.y, a.x) {
self.s.held = held;
} else {
self.play(SFX_ROTATE);
}
}
/// Bit 2 of the id mirrors the piece; bit 1 is adjusted so the mirror
/// is about the expected axis. Select and Start mirror about different
/// axes.
fn flip(&mut self, select: bool) {
let mut id = self.s.held ^ 4;
if (id & 1 == 0) == select {
id ^= 2;
}
self.s.held = id;
self.update_cursor_shape();
let a = self.s.actors[CURSOR];
if !self.out_of_bounds(a.shape, a.y, a.x) {
self.play(SFX_FLIP);
return;
}
// (sic) undoing always uses Start's rule, so undoing a Select flip
// can leave the piece turned
id ^= 4;
if id & 1 != 0 {
id ^= 2;
}
self.s.held = id;
}
/// Number of piece types whose cells all sit on the target area.
pub(super) fn pieces_in_place(&self) -> u8 {
self.s.slots[..13].iter().filter(|s| s.under[..5].iter().all(|&t| t >= TARGET_FLOOR)).count() as u8
}
pub(super) fn level_clear(&mut self) {
self.play(JINGLE_LEVEL_CLEAR);
self.clear_actors();
self.wait_frame();
// map 1 keeps the finished board; the banner goes over map 0
self.wipe_rows(MAP0, MAP1);
let data = self.data.clone();
for script in &data.scripts.level_clear_banner {
self.video.draw(script);
self.wait_frame();
}
let mut showing_board = false;
let mut t: u8 = 0;
loop {
self.wait_frame();
t = t.wrapping_add(1);
if t & 0x1F == 0 {
// sparkle the banner
let frame = if t & 0x20 != 0 { 0x20 } else { 0 };
self.video.load(0x8900, data.gfx(GFX_BANNER_SPARKLE + frame, 0x20));
}
if self.joy.pressed & START != 0 {
return;
}
if self.joy.pressed & A != 0 {
self.show(if showing_board { MAP0 } else { MAP1 });
showing_board = !showing_board;
}
}
}
}
+185
View File
@@ -0,0 +1,185 @@
//! The story screens shown before some levels, and the ending.
use super::*;
use crate::input::*;
use crate::rom::addr::*;
use crate::rom::Text;
use crate::sound::*;
use crate::video::map_cell;
const TEXT_START: u16 = map_cell(MAP0, 2, 3);
const ENDING_TEXT_START: u16 = map_cell(MAP0, 2, 1);
const TEXT_ROWS: u16 = 8;
const TEXT_WIDTH: u16 = 16;
const BLANK: u8 = 0xFE;
/// frames per letter
const TYPE_DELAY: u8 = 8;
/// Next text position: one cell right (wrapping within the 256-byte page,
/// as the original does).
fn advance(at: u16) -> u16 {
(at & 0xFF00) | (at as u8).wrapping_add(1) as u16
}
/// Start of the line after next (text is double spaced).
fn newline(at: u16) -> u16 {
(at & 0xFFC0) + 0x62
}
impl Game {
/// Story backdrop for the current level, if it has one.
pub(super) fn story_screen(&mut self) {
self.s.story = self.data.story_for(self.level);
if self.s.story == 0 {
return;
}
let data = self.data.clone();
self.video.load(0x8800, data.gfx(GFX_STORY, 0x800));
self.video.load(0x9000, data.gfx(GFX_FONT, 0x800));
self.video.fill_maps(BLANK);
self.video.draw(&data.scripts.intermission);
self.clear_actors();
self.s.actors[HERO] = Actor::at(0x00, 0x50, 0x52); // the storyteller, shown while talking
self.s.actors[MARKER] = Actor::at(0x80, 0x28, 0x50);
self.set_palettes(PALETTE);
self.play(MUSIC_MAP);
self.show(MAP0);
self.wait_frame();
}
/// Type out the story a page at a time. A skips the rest of a page.
pub(super) fn story_text(&mut self) {
let story = self.s.story;
if story == 0 || story > 4 {
return;
}
let data = self.data.clone();
let mut at = TEXT_START;
for &t in &data.stories[story as usize - 1] {
at = advance(at);
match t {
Text::End => {
self.s.actors[HERO].y = 0;
while self.joy.pressed & (A | START) == 0 {
self.wait_frame();
}
return;
}
Text::PageBreak => {
self.s.actors[HERO].y = 0;
while self.joy.pressed & A == 0 {
self.wait_frame();
}
for row in 0..TEXT_ROWS {
self.video.fill_row(TEXT_START + row * 32, BLANK, TEXT_WIDTH);
self.wait_frame();
}
at = TEXT_START;
}
Text::Newline => at = newline(at),
Text::Char(c) => {
// while A is down (it stays "pressed" until the next
// frame) letters are skipped without being drawn
if self.joy.pressed & A != 0 {
continue;
}
self.s.actors[HERO].y ^= 0x80; // mouth moves
self.video.poke(at, c);
for _ in 0..TYPE_DELAY {
self.wait_frame();
if self.joy.pressed & A != 0 {
break;
}
}
}
}
}
}
pub(super) fn ending(&mut self) {
let data = self.data.clone();
let gfx = |a, len| data.gfx(a, len);
self.video.load(0x8000, gfx(GFX_STORY, 0x800));
self.video.load(0x8800, gfx(GFX_STORY, 0x800));
self.video.load(0x9000, gfx(GFX_FONT, 0x800));
self.video.load(0x9600, gfx(GFX_ENDING_C, 0x140));
self.video.load(0x8B00, gfx(GFX_ENDING_D, 0x100));
self.video.load(0x8A40, gfx(GFX_ENDING_B, 0xC0));
self.video.load(0x8C00, gfx(GFX_ENDING_A, 0x80));
self.video.fill_maps(BLANK);
self.video.draw(&data.scripts.ending);
self.clear_actors();
self.s.actors[HERO] = Actor::at(0xF0, 0x78, 0x5C);
self.s.actors[MARKER] = Actor::at(0x60, 0x50, 0x5A);
self.set_palettes(PALETTE);
self.show(MAP0);
self.play(MUSIC_TITLE);
let art = &data.scripts.ending_art;
self.type_ending(&data.ending_text[0]);
self.wait_for_button();
self.draw_and_wait(&art[0]);
self.draw_and_wait(&art[1]);
self.open_door();
self.wait_for_button();
self.clear_ending_text();
self.draw_and_wait(&art[2]);
self.draw_and_wait(&art[3]);
self.type_ending(&data.ending_text[1]);
self.wait_for_button();
self.clear_ending_text();
self.type_ending(&data.ending_text[2]);
self.draw_and_wait(&art[0]);
self.draw_and_wait(&art[4]);
self.wait_for_button();
}
fn type_ending(&mut self, text: &[Text]) {
let mut at = ENDING_TEXT_START;
for &t in text {
at = advance(at);
let c = match t {
Text::End => break,
Text::Newline => {
at = newline(at);
continue;
}
Text::PageBreak => 0x81, // not a code in the ending's printer
Text::Char(c) => c,
};
self.video.poke(at, c);
self.s.actors[HERO].y ^= 0x80;
self.wait_frames(TYPE_DELAY);
}
self.s.actors[HERO].y = 0xF0;
}
fn wait_for_button(&mut self) {
loop {
self.wait_frame();
if self.joy.pressed & (A | START) != 0 {
return;
}
}
}
fn draw_and_wait(&mut self, script: &crate::rom::DrawScript) {
self.video.draw(script);
self.wait_frame();
}
fn open_door(&mut self) {
self.wait_frames(12);
for b in (1..=4).rev() {
self.video.poke(map_cell(MAP0, 12, 9) - b, 0x68 - b as u8);
self.wait_frames(12);
}
}
fn clear_ending_text(&mut self) {
for row in 0..TEXT_ROWS {
self.video.fill_row(ENDING_TEXT_START + row * 32, BLANK, TEXT_WIDTH);
self.wait_frame();
}
}
}
+32 -33
View File
@@ -1,9 +1,15 @@
//! Headless runner for testing: `daedalian --headless OUT_DIR SCRIPT...` //! Headless runner for testing: `opus --headless OUT_DIR SCRIPT...`
//!
//! Script tokens: `N` run N frames; `BUTTON:N` hold a button for N frames //! Script tokens: `N` run N frames; `BUTTON:N` hold a button for N frames
//! (up down left right a b start select); `shot:NAME` save OUT_DIR/NAME.png. //! (up down left right a b start select); `shot:NAME` save OUT_DIR/NAME.png;
//! All audio is written to OUT_DIR/audio.wav. //! `probe` print the level and cursor; `goto:YY:XX` steer the puzzle cursor
//! (hex sprite coordinates). All audio is written to OUT_DIR/audio.wav.
use crate::machine::*; use crate::game::Frame;
use crate::input;
use crate::rom::GameData;
use crate::video::{SCREEN_H, SCREEN_W};
use std::sync::Arc;
use std::fs; use std::fs;
use std::path::Path; use std::path::Path;
@@ -78,60 +84,53 @@ fn write_wav(path: &Path, rate: u32, samples: &[f32]) {
fs::write(path, d).unwrap(); fs::write(path, d).unwrap();
} }
pub fn run(rom: &[u8], args: &[String]) { pub fn run(data: Arc<GameData>, args: &[String]) {
let out = Path::new(&args[0]); let out = Path::new(&args[0]);
fs::create_dir_all(out).unwrap(); fs::create_dir_all(out).unwrap();
let rate = 44_100; let rate = 44_100;
let mut m = Machine::new(rom, rate); let host = crate::start_game(data, rate);
let mut audio = Vec::new(); let mut audio = Vec::new();
let mut frames = 0u64; let mut frames = 0u64;
let run = |m: &mut Machine, n: u64, audio: &mut Vec<f32>, frames: &mut u64| { let mut frame: Frame = host.frames.recv().expect("game thread");
let mut step = |buttons: u8, n: u64, frame: &mut Frame| {
for _ in 0..n { for _ in 0..n {
m.run_frame(); host.buttons.send(buttons).expect("game thread");
audio.append(&mut m.apu.out); *frame = host.frames.recv().expect("game thread");
*frames += 1; audio.extend_from_slice(&frame.audio);
frames += 1;
} }
}; };
for tok in &args[1..] { for tok in &args[1..] {
if let Ok(n) = tok.parse::<u64>() { if let Ok(n) = tok.parse::<u64>() {
run(&mut m, n, &mut audio, &mut frames); step(0, n, &mut frame);
} else if let Some(name) = tok.strip_prefix("shot:") { } else if let Some(name) = tok.strip_prefix("shot:") {
let gray: Vec<u8> = m.framebuffer.iter().map(|&s| [0xFF, 0xAA, 0x55, 0x00][s as usize]).collect(); let gray: Vec<u8> = frame.pixels.iter().map(|&s| [0xFF, 0xAA, 0x55, 0x00][s as usize]).collect();
write_png(&out.join(format!("{name}.png")), SCREEN_W, SCREEN_H, &gray); write_png(&out.join(format!("{name}.png")), SCREEN_W, SCREEN_H, &gray);
println!("frame {frames}: {name}.png (pc ${:04X})", m.pc); println!("{name}.png: {:?}", frame.probe);
} else if tok == "probe" {
println!("{:?}", frame.probe);
} else if let Some(spec) = tok.strip_prefix("goto:") { } else if let Some(spec) = tok.strip_prefix("goto:") {
// steer the puzzle cursor (wPieceY/wPieceX at $C0A1/$C0A2) to a position
let (ty, tx) = spec.split_once(':').unwrap(); let (ty, tx) = spec.split_once(':').unwrap();
let (ty, tx) = (u8::from_str_radix(ty, 16).unwrap(), u8::from_str_radix(tx, 16).unwrap()); let (ty, tx) = (u8::from_str_radix(ty, 16).unwrap(), u8::from_str_radix(tx, 16).unwrap());
for _ in 0..100 { for _ in 0..100 {
let (cy, cx) = (m.r8(0xC0A1), m.r8(0xC0A2)); let p = frame.probe;
let bit = if cy < ty { BTN_DOWN } else if cy > ty { BTN_UP } let bit = if p.cursor_y < ty { input::DOWN } else if p.cursor_y > ty { input::UP }
else if cx < tx { BTN_RIGHT } else if cx > tx { BTN_LEFT } else { break }; else if p.cursor_x < tx { input::RIGHT } else if p.cursor_x > tx { input::LEFT } else { break };
m.buttons |= bit; step(bit, 1, &mut frame);
run(&mut m, 1, &mut audio, &mut frames); step(0, 5, &mut frame);
m.buttons &= !bit;
run(&mut m, 5, &mut audio, &mut frames);
} }
} else if let Some(spec) = tok.strip_prefix("peek:") {
let (addr, n) = spec.split_once(':').unwrap_or((spec, "1"));
let addr = u16::from_str_radix(addr, 16).unwrap();
let vals: Vec<String> = (0..n.parse::<u16>().unwrap())
.map(|i| format!("{:02X}", m.r8(addr + i)))
.collect();
println!("frame {frames}: ${addr:04X}: {}", vals.join(" "));
} else if let Some((btn, n)) = tok.split_once(':') { } else if let Some((btn, n)) = tok.split_once(':') {
let bit = match btn { let bit = match btn {
"up" => BTN_UP, "down" => BTN_DOWN, "left" => BTN_LEFT, "right" => BTN_RIGHT, "up" => input::UP, "down" => input::DOWN, "left" => input::LEFT, "right" => input::RIGHT,
"a" => BTN_A, "b" => BTN_B, "start" => BTN_START, "select" => BTN_SELECT, "a" => input::A, "b" => input::B, "start" => input::START, "select" => input::SELECT,
_ => panic!("unknown token {tok}"), _ => panic!("unknown token {tok}"),
}; };
m.buttons |= bit; step(bit, n.parse().unwrap(), &mut frame);
run(&mut m, n.parse().unwrap(), &mut audio, &mut frames);
m.buttons &= !bit;
} else { } else {
panic!("unknown token {tok}"); panic!("unknown token {tok}");
} }
} }
drop(step);
write_wav(&out.join("audio.wav"), rate, &audio); write_wav(&out.join("audio.wav"), rate, &audio);
println!("{frames} frames, {:.1}s of audio", audio.len() as f32 / 2.0 / rate as f32); println!("{frames} frames, {:.1}s of audio", audio.len() as f32 / 2.0 / rate as f32);
} }
+44
View File
@@ -0,0 +1,44 @@
//! Buttons, as the game reads them once per frame.
pub const A: u8 = 0x01;
pub const B: u8 = 0x02;
pub const SELECT: u8 = 0x04;
pub const START: u8 = 0x08;
pub const RIGHT: u8 = 0x10;
pub const LEFT: u8 = 0x20;
pub const UP: u8 = 0x40;
pub const DOWN: u8 = 0x80;
pub const DPAD: u8 = 0xF0;
#[derive(Clone, Copy, Default)]
pub struct Joypad {
/// buttons down this frame
pub held: u8,
/// buttons that went down this frame
pub pressed: u8,
/// d-pad with auto-repeat: fires on press, then after 10 frames, then
/// every 4 frames while held
pub repeat: u8,
repeat_timer: u8,
}
impl Joypad {
pub fn update(&mut self, now: u8) {
self.pressed = (self.held ^ now) & now;
self.held = now;
self.repeat = 0;
if now == 0 {
return;
}
if self.pressed & DPAD != 0 {
self.repeat = self.pressed & DPAD;
self.repeat_timer = 10;
return;
}
self.repeat_timer = self.repeat_timer.wrapping_sub(1);
if self.repeat_timer == 0 {
self.repeat_timer = 4;
self.repeat = now & DPAD;
}
}
}
-399
View File
@@ -1,399 +0,0 @@
#![allow(dead_code)]
//! The hardware the recompiled code runs against: SM83 registers and flags,
//! the memory map, the LCD (rendered once per frame at vblank), the joypad,
//! OAM DMA, the vblank interrupt, and the APU (in apu.rs).
use crate::apu::Apu;
use crate::game;
pub const SCREEN_W: usize = 160;
pub const SCREEN_H: usize = 144;
pub const CPU_HZ: u64 = 4_194_304;
const CYCLES_PER_LINE: u32 = 456;
const CYCLES_PER_FRAME: u32 = 70_224;
const VBLANK_START: u32 = 144 * CYCLES_PER_LINE;
// joypad bits (1 = pressed)
pub const BTN_RIGHT: u8 = 0x01;
pub const BTN_LEFT: u8 = 0x02;
pub const BTN_UP: u8 = 0x04;
pub const BTN_DOWN: u8 = 0x08;
pub const BTN_A: u8 = 0x10;
pub const BTN_B: u8 = 0x20;
pub const BTN_SELECT: u8 = 0x40;
pub const BTN_START: u8 = 0x80;
pub struct Machine {
pub a: u8,
pub f: u8,
pub b: u8,
pub c: u8,
pub d: u8,
pub e: u8,
pub h: u8,
pub l: u8,
pub sp: u16,
pub pc: u16,
pub ime: bool,
halted: bool,
mem: Vec<u8>,
pub cycles: u64,
frame_cycle: u32,
vblank_fired: bool,
frame_ready: bool,
pub buttons: u8,
pub apu: Apu,
/// shade index 0 (lightest) .. 3 (darkest) per pixel
pub framebuffer: Vec<u8>,
warned_code_read: bool,
}
impl Machine {
pub fn new(rom_data: &[u8], sample_rate: u32) -> Self {
let mut mem = vec![0u8; 0x10000];
mem[..0x8000].copy_from_slice(&rom_data[..0x8000]);
let mut m = Machine {
// register state left by the boot ROM
a: 0x01, f: 0xB0, b: 0x00, c: 0x13, d: 0x00, e: 0xD8, h: 0x01, l: 0x4D,
sp: 0xFFFE,
pc: 0x0100,
ime: false,
halted: false,
mem,
cycles: 0,
frame_cycle: 0,
vblank_fired: false,
frame_ready: false,
buttons: 0,
apu: Apu::new(sample_rate),
framebuffer: vec![0; SCREEN_W * SCREEN_H],
warned_code_read: false,
};
m.mem[0xFF40] = 0x91;
m.mem[0xFF47] = 0xFC;
m.mem[0xFF00] = 0xCF;
m
}
/// Run recompiled code until the next vblank (one frame).
pub fn run_frame(&mut self) {
self.frame_ready = false;
while !self.frame_ready {
let pending = self.mem[0xFFFF] & self.mem[0xFF0F] & 0x1F;
if self.halted {
if pending != 0 {
self.halted = false;
} else {
self.tick(4);
continue;
}
}
if self.ime && pending != 0 {
// only vblank is ever enabled by this game
let bit = pending.trailing_zeros() as u16;
self.mem[0xFF0F] &= !(1 << bit);
self.ime = false;
let pc = self.pc;
self.push(pc);
self.pc = 0x40 + bit * 8;
self.tick(20);
}
game::step(self);
}
let cycles = self.cycles;
self.apu.catch_up(cycles);
}
pub fn tick(&mut self, t: u32) {
self.cycles += t as u64;
self.frame_cycle += t;
loop {
if !self.vblank_fired && self.frame_cycle >= VBLANK_START {
self.vblank_fired = true;
if self.mem[0xFF40] & 0x80 != 0 {
self.mem[0xFF0F] |= 0x01;
}
self.render();
self.frame_ready = true;
} else if self.frame_cycle >= CYCLES_PER_FRAME {
self.frame_cycle -= CYCLES_PER_FRAME;
self.vblank_fired = false;
} else {
break;
}
}
}
pub fn halt(&mut self) {
self.halted = true;
}
// ---------------- memory bus ----------------
pub fn r8(&mut self, addr: u16) -> u8 {
match addr {
0x0000..=0x7FFF => {
if !self.warned_code_read
&& !(0x1141..0x114B).contains(&addr) // OAM DMA routine, copied to HRAM
&& game::CODE_RANGES.iter().any(|&(s, e)| addr >= s && addr < e)
{
self.warned_code_read = true;
eprintln!("warning: game read code byte ${:04X} as data (pc ${:04X})", addr, self.pc);
}
self.mem[addr as usize]
}
0xE000..=0xFDFF => self.mem[addr as usize - 0x2000],
0xFF00 => {
let sel = self.mem[0xFF00] & 0x30;
let mut low = 0x0F;
if sel & 0x10 == 0 {
low &= !(self.buttons & 0x0F);
}
if sel & 0x20 == 0 {
low &= !(self.buttons >> 4);
}
0xC0 | sel | low
}
0xFF44 => (self.frame_cycle / CYCLES_PER_LINE) as u8,
0xFF10..=0xFF3F => {
let cycles = self.cycles;
self.apu.read(addr, cycles)
}
_ => self.mem[addr as usize],
}
}
pub fn w8(&mut self, addr: u16, v: u8) {
match addr {
0x0000..=0x7FFF => {} // ROM-only cart: writes do nothing
0xE000..=0xFDFF => self.mem[addr as usize - 0x2000] = v,
0xFF00 => self.mem[0xFF00] = v & 0x30,
0xFF04 | 0xFF44 => {}
0xFF46 => {
let src = (v as usize) << 8;
for i in 0..0xA0 {
self.mem[0xFE00 + i] = self.mem[src + i];
}
}
0xFF10..=0xFF3F => {
let cycles = self.cycles;
self.apu.write(addr, v, cycles);
}
_ => self.mem[addr as usize] = v,
}
}
pub fn push(&mut self, v: u16) {
self.sp = self.sp.wrapping_sub(1);
self.w8(self.sp, (v >> 8) as u8);
self.sp = self.sp.wrapping_sub(1);
self.w8(self.sp, v as u8);
}
pub fn pop(&mut self) -> u16 {
let lo = self.r8(self.sp) as u16;
self.sp = self.sp.wrapping_add(1);
let hi = self.r8(self.sp) as u16;
self.sp = self.sp.wrapping_add(1);
hi << 8 | lo
}
// ---------------- register pairs ----------------
pub fn af(&self) -> u16 { (self.a as u16) << 8 | self.f as u16 }
pub fn bc(&self) -> u16 { (self.b as u16) << 8 | self.c as u16 }
pub fn de(&self) -> u16 { (self.d as u16) << 8 | self.e as u16 }
pub fn hl(&self) -> u16 { (self.h as u16) << 8 | self.l as u16 }
pub fn set_af(&mut self, v: u16) { self.a = (v >> 8) as u8; self.f = v as u8 & 0xF0; }
pub fn set_bc(&mut self, v: u16) { self.b = (v >> 8) as u8; self.c = v as u8; }
pub fn set_de(&mut self, v: u16) { self.d = (v >> 8) as u8; self.e = v as u8; }
pub fn set_hl(&mut self, v: u16) { self.h = (v >> 8) as u8; self.l = v as u8; }
// ---------------- flags ----------------
pub fn zf(&self) -> bool { self.f & 0x80 != 0 }
pub fn cf(&self) -> bool { self.f & 0x10 != 0 }
fn hf(&self) -> bool { self.f & 0x20 != 0 }
fn nf(&self) -> bool { self.f & 0x40 != 0 }
fn flags(&mut self, z: bool, n: bool, h: bool, c: bool) {
self.f = (z as u8) << 7 | (n as u8) << 6 | (h as u8) << 5 | (c as u8) << 4;
}
// ---------------- ALU ----------------
pub fn add(&mut self, v: u8) {
let (a, r) = (self.a, self.a as u16 + v as u16);
self.a = r as u8;
self.flags(self.a == 0, false, (a & 0xF) + (v & 0xF) > 0xF, r > 0xFF);
}
pub fn adc(&mut self, v: u8) {
let c = self.cf() as u16;
let (a, r) = (self.a, self.a as u16 + v as u16 + c);
self.a = r as u8;
self.flags(self.a == 0, false, (a & 0xF) as u16 + (v & 0xF) as u16 + c > 0xF, r > 0xFF);
}
pub fn sub(&mut self, v: u8) {
let a = self.a;
self.a = a.wrapping_sub(v);
self.flags(self.a == 0, true, (a & 0xF) < (v & 0xF), a < v);
}
pub fn sbc(&mut self, v: u8) {
let c = self.cf() as u16;
let a = self.a;
let r = (a as u16).wrapping_sub(v as u16).wrapping_sub(c);
self.a = r as u8;
self.flags(self.a == 0, true, ((a & 0xF) as u16) < (v & 0xF) as u16 + c, (a as u16) < v as u16 + c);
}
pub fn and(&mut self, v: u8) { self.a &= v; self.flags(self.a == 0, false, true, false); }
pub fn xor(&mut self, v: u8) { self.a ^= v; self.flags(self.a == 0, false, false, false); }
pub fn or(&mut self, v: u8) { self.a |= v; self.flags(self.a == 0, false, false, false); }
pub fn cp(&mut self, v: u8) {
let a = self.a;
self.flags(a == v, true, (a & 0xF) < (v & 0xF), a < v);
}
pub fn inc(&mut self, v: u8) -> u8 {
let r = v.wrapping_add(1);
let c = self.cf();
self.flags(r == 0, false, v & 0xF == 0xF, c);
r
}
pub fn dec(&mut self, v: u8) -> u8 {
let r = v.wrapping_sub(1);
let c = self.cf();
self.flags(r == 0, true, v & 0xF == 0, c);
r
}
pub fn add_hl(&mut self, v: u16) {
let hl = self.hl();
let r = hl as u32 + v as u32;
let z = self.zf();
self.flags(z, false, (hl & 0xFFF) + (v & 0xFFF) > 0xFFF, r > 0xFFFF);
self.set_hl(r as u16);
}
/// sp + signed e, with the flags of add sp,e / ld hl,sp+e
pub fn add_sp(&mut self, e: i8) -> u16 {
let sp = self.sp;
let v = e as i16 as u16;
self.flags(false, false, (sp & 0xF) + (v & 0xF) > 0xF, (sp & 0xFF) + (v & 0xFF) > 0xFF);
sp.wrapping_add(v)
}
pub fn rlca(&mut self) { let c = self.a >> 7; self.a = self.a.rotate_left(1); self.flags(false, false, false, c != 0); }
pub fn rrca(&mut self) { let c = self.a & 1; self.a = self.a.rotate_right(1); self.flags(false, false, false, c != 0); }
pub fn rla(&mut self) { let c = self.a >> 7; self.a = self.a << 1 | self.cf() as u8; self.flags(false, false, false, c != 0); }
pub fn rra(&mut self) { let c = self.a & 1; self.a = self.a >> 1 | (self.cf() as u8) << 7; self.flags(false, false, false, c != 0); }
pub fn daa(&mut self) {
let mut a = self.a;
let mut c = self.cf();
if !self.nf() {
if c || a > 0x99 { a = a.wrapping_add(0x60); c = true; }
if self.hf() || a & 0x0F > 0x09 { a = a.wrapping_add(0x06); }
} else {
if c { a = a.wrapping_sub(0x60); }
if self.hf() { a = a.wrapping_sub(0x06); }
}
self.a = a;
let n = self.nf();
self.flags(a == 0, n, false, c);
}
pub fn cpl(&mut self) { self.a = !self.a; self.f |= 0x60; }
pub fn scf(&mut self) { self.f = (self.f & 0x80) | 0x10; }
pub fn ccf(&mut self) { self.f = (self.f & 0x80) | if self.cf() { 0 } else { 0x10 }; }
// CB-prefixed
pub fn rlc(&mut self, v: u8) -> u8 { let r = v.rotate_left(1); self.flags(r == 0, false, false, v & 0x80 != 0); r }
pub fn rrc(&mut self, v: u8) -> u8 { let r = v.rotate_right(1); self.flags(r == 0, false, false, v & 1 != 0); r }
pub fn rl(&mut self, v: u8) -> u8 { let r = v << 1 | self.cf() as u8; self.flags(r == 0, false, false, v & 0x80 != 0); r }
pub fn rr(&mut self, v: u8) -> u8 { let r = v >> 1 | (self.cf() as u8) << 7; self.flags(r == 0, false, false, v & 1 != 0); r }
pub fn sla(&mut self, v: u8) -> u8 { let r = v << 1; self.flags(r == 0, false, false, v & 0x80 != 0); r }
pub fn sra(&mut self, v: u8) -> u8 { let r = (v as i8 >> 1) as u8; self.flags(r == 0, false, false, v & 1 != 0); r }
pub fn swap(&mut self, v: u8) -> u8 { let r = v.rotate_left(4); self.flags(r == 0, false, false, false); r }
pub fn srl(&mut self, v: u8) -> u8 { let r = v >> 1; self.flags(r == 0, false, false, v & 1 != 0); r }
pub fn bit(&mut self, n: u8, v: u8) {
let c = self.cf();
self.flags(v & (1 << n) == 0, false, true, c);
}
// ---------------- LCD ----------------
fn tile_pixel(&self, tile_addr: usize, x: usize, y: usize) -> u8 {
let lo = self.mem[tile_addr + y * 2];
let hi = self.mem[tile_addr + y * 2 + 1];
((lo >> (7 - x)) & 1) | (((hi >> (7 - x)) & 1) << 1)
}
fn bg_tile_addr(&self, tile: u8) -> usize {
if self.mem[0xFF40] & 0x10 != 0 {
0x8000 + tile as usize * 16
} else {
(0x9000i32 + (tile as i8 as i32) * 16) as usize
}
}
/// Draw the whole frame from the current VRAM/OAM/register state.
fn render(&mut self) {
let lcdc = self.mem[0xFF40];
if lcdc & 0x80 == 0 {
self.framebuffer.fill(0);
return;
}
let shade = |pal: u8, c: u8| (pal >> (c * 2)) & 3;
let bgp = self.mem[0xFF47];
let (scy, scx) = (self.mem[0xFF42] as usize, self.mem[0xFF43] as usize);
let (wy, wx) = (self.mem[0xFF4A] as usize, self.mem[0xFF4B] as usize);
let mut bg_color = vec![0u8; SCREEN_W * SCREEN_H];
for y in 0..SCREEN_H {
for x in 0..SCREEN_W {
let mut c = 0;
if lcdc & 0x01 != 0 {
let (map, mx, my) = if lcdc & 0x20 != 0 && y >= wy && x + 7 >= wx {
(if lcdc & 0x40 != 0 { 0x9C00 } else { 0x9800 }, x + 7 - wx, y - wy)
} else {
(if lcdc & 0x08 != 0 { 0x9C00 } else { 0x9800 }, (x + scx) & 255, (y + scy) & 255)
};
let tile = self.mem[map + (my / 8) * 32 + mx / 8];
c = self.tile_pixel(self.bg_tile_addr(tile), mx % 8, my % 8);
}
bg_color[y * SCREEN_W + x] = c;
self.framebuffer[y * SCREEN_W + x] = shade(bgp, c);
}
}
if lcdc & 0x02 != 0 {
let tall = lcdc & 0x04 != 0;
let height = if tall { 16 } else { 8 };
// lower OAM index wins: draw from the end
for i in (0..40).rev() {
let o = 0xFE00 + i * 4;
let sy = self.mem[o] as i32 - 16;
let sx = self.mem[o + 1] as i32 - 8;
let mut tile = self.mem[o + 2];
let attr = self.mem[o + 3];
if tall {
tile &= 0xFE;
}
let pal = if attr & 0x10 != 0 { self.mem[0xFF49] } else { self.mem[0xFF48] };
for ty in 0..height {
let py = sy + ty as i32;
if py < 0 || py >= SCREEN_H as i32 {
continue;
}
let row = if attr & 0x40 != 0 { height - 1 - ty } else { ty };
for tx in 0..8 {
let px = sx + tx as i32;
if px < 0 || px >= SCREEN_W as i32 {
continue;
}
let col = if attr & 0x20 != 0 { 7 - tx } else { tx };
let c = self.tile_pixel(0x8000 + tile as usize * 16, col, row);
let idx = py as usize * SCREEN_W + px as usize;
if c == 0 || (attr & 0x80 != 0 && bg_color[idx] != 0) {
continue;
}
self.framebuffer[idx] = shade(pal, c);
}
}
}
}
}
}
+101 -36
View File
@@ -1,13 +1,24 @@
//! Daedalian Opus, running as recompiled Rust. //! Daedalian Opus, ported to Rust.
//! //!
//! Controls: arrows = D-pad, Z = A, X = B, Enter = Start, Right Shift / Backspace = Select //! Usage: `opus [--rom PATH] [--headless OUT_DIR SCRIPT...]`
//!
//! The game's data is read from the original ROM ("Daedalian Opus (USA).gb"),
//! looked for in the current directory and its parent unless given.
//!
//! Controls: arrows = D-pad, Z = A, X = B, Enter = Start,
//! Right Shift / Backspace = Select, Esc = quit.
mod apu; mod apu;
mod game; mod game;
mod headless; mod headless;
mod machine; mod input;
mod rom;
mod sound;
mod video;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::path::PathBuf;
use std::sync::mpsc::{sync_channel, Receiver, SyncSender};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
@@ -20,10 +31,10 @@ use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
use winit::keyboard::{KeyCode, PhysicalKey}; use winit::keyboard::{KeyCode, PhysicalKey};
use winit::window::{Window, WindowId}; use winit::window::{Window, WindowId};
use machine::*; use game::{Frame, FRAME_HZ};
use video::{SCREEN_H, SCREEN_W};
const ROM_DATA: &[u8] = include_bytes!("../assets/rom_data.bin"); const ROM_NAME: &str = "Daedalian Opus (USA).gb";
const FRAME: Duration = Duration::from_nanos(1_000_000_000 * 70_224 / CPU_HZ);
const SHADES: [[u8; 4]; 4] = [ const SHADES: [[u8; 4]; 4] = [
[0xFF, 0xFF, 0xFF, 0xFF], [0xFF, 0xFF, 0xFF, 0xFF],
[0xAA, 0xAA, 0xAA, 0xFF], [0xAA, 0xAA, 0xAA, 0xFF],
@@ -33,11 +44,27 @@ const SHADES: [[u8; 4]; 4] = [
type AudioQueue = Arc<Mutex<VecDeque<f32>>>; type AudioQueue = Arc<Mutex<VecDeque<f32>>>;
/// The host's end of the connection to the game thread.
pub struct Host {
pub frames: Receiver<Frame>,
pub buttons: SyncSender<u8>,
}
pub fn start_game(data: Arc<rom::GameData>, sample_rate: u32) -> Host {
let (frame_tx, frame_rx) = sync_channel(1);
let (button_tx, button_rx) = sync_channel(1);
game::spawn(data, sample_rate, game::Link { frames: frame_tx, buttons: button_rx });
Host { frames: frame_rx, buttons: button_tx }
}
struct App { struct App {
window: Option<Arc<Window>>, window: Option<Arc<Window>>,
pixels: Option<Pixels<'static>>, pixels: Option<Pixels<'static>>,
machine: Machine, host: Host,
buttons: u8,
screen: Vec<u8>,
next_frame: Instant, next_frame: Instant,
frame_time: Duration,
audio: AudioQueue, audio: AudioQueue,
audio_rate: u32, audio_rate: u32,
_stream: Option<cpal::Stream>, _stream: Option<cpal::Stream>,
@@ -76,42 +103,36 @@ fn start_audio(queue: AudioQueue) -> Option<(cpal::Stream, u32)> {
impl App { impl App {
fn key(&mut self, code: KeyCode, pressed: bool) { fn key(&mut self, code: KeyCode, pressed: bool) {
let bit = match code { let bit = match code {
KeyCode::ArrowRight => BTN_RIGHT, KeyCode::ArrowRight => input::RIGHT,
KeyCode::ArrowLeft => BTN_LEFT, KeyCode::ArrowLeft => input::LEFT,
KeyCode::ArrowUp => BTN_UP, KeyCode::ArrowUp => input::UP,
KeyCode::ArrowDown => BTN_DOWN, KeyCode::ArrowDown => input::DOWN,
KeyCode::KeyZ => BTN_A, KeyCode::KeyZ => input::A,
KeyCode::KeyX => BTN_B, KeyCode::KeyX => input::B,
KeyCode::ShiftRight | KeyCode::Backspace => BTN_SELECT, KeyCode::ShiftRight | KeyCode::Backspace => input::SELECT,
KeyCode::Enter => BTN_START, KeyCode::Enter => input::START,
_ => return, _ => return,
}; };
if pressed { if pressed {
self.machine.buttons |= bit; self.buttons |= bit;
} else { } else {
self.machine.buttons &= !bit; self.buttons &= !bit;
} }
} }
fn run_due_frames(&mut self) { /// Advance the game one frame. False if the game thread has stopped.
let now = Instant::now(); fn step(&mut self) -> bool {
let mut ran = 0; let Ok(frame) = self.host.frames.recv() else { return false };
while self.next_frame <= now && ran < 4 { self.screen = frame.pixels;
self.machine.run_frame();
self.next_frame += FRAME;
ran += 1;
}
if self.next_frame < now {
self.next_frame = now; // fell behind; don't try to catch up forever
}
let samples = std::mem::take(&mut self.machine.apu.out);
let mut q = self.audio.lock().unwrap(); let mut q = self.audio.lock().unwrap();
q.extend(samples); q.extend(frame.audio);
let max = self.audio_rate as usize / 5 * 2; // keep at most ~200 ms queued let max = self.audio_rate as usize / 5 * 2; // keep at most ~200 ms queued
if q.len() > max { if q.len() > max {
let excess = q.len() - max; let excess = q.len() - max;
q.drain(..excess); q.drain(..excess);
} }
drop(q);
self.host.buttons.send(self.buttons).is_ok()
} }
} }
@@ -121,7 +142,7 @@ impl ApplicationHandler for App {
return; return;
} }
let attrs = Window::default_attributes() let attrs = Window::default_attributes()
.with_title("Daedalian Opus (recompiled)") .with_title("Daedalian Opus")
.with_inner_size(LogicalSize::new(SCREEN_W as f64 * 4.0, SCREEN_H as f64 * 4.0)); .with_inner_size(LogicalSize::new(SCREEN_W as f64 * 4.0, SCREEN_H as f64 * 4.0));
let window = Arc::new(event_loop.create_window(attrs).expect("create window")); let window = Arc::new(event_loop.create_window(attrs).expect("create window"));
let size = window.inner_size(); let size = window.inner_size();
@@ -150,7 +171,7 @@ impl ApplicationHandler for App {
} }
WindowEvent::RedrawRequested => { WindowEvent::RedrawRequested => {
if let Some(p) = &mut self.pixels { if let Some(p) = &mut self.pixels {
for (dst, &shade) in p.frame_mut().chunks_exact_mut(4).zip(&self.machine.framebuffer) { for (dst, &shade) in p.frame_mut().chunks_exact_mut(4).zip(&self.screen) {
dst.copy_from_slice(&SHADES[shade as usize]); dst.copy_from_slice(&SHADES[shade as usize]);
} }
if let Err(e) = p.render() { if let Err(e) = p.render() {
@@ -167,7 +188,20 @@ impl ApplicationHandler for App {
if self.window.is_none() { if self.window.is_none() {
return; return;
} }
self.run_due_frames(); let now = Instant::now();
let mut ran = 0;
while self.next_frame <= now && ran < 4 {
if !self.step() {
eprintln!("the game stopped");
event_loop.exit();
return;
}
self.next_frame += self.frame_time;
ran += 1;
}
if self.next_frame < now {
self.next_frame = now; // fell behind; don't try to catch up forever
}
if let Some(w) = &self.window { if let Some(w) = &self.window {
w.request_redraw(); w.request_redraw();
} }
@@ -175,12 +209,40 @@ impl ApplicationHandler for App {
} }
} }
fn find_rom(explicit: Option<String>) -> Option<PathBuf> {
if let Some(p) = explicit {
return Some(p.into());
}
[PathBuf::from(ROM_NAME), PathBuf::from("..").join(ROM_NAME)].into_iter().find(|p| p.exists())
}
fn main() { fn main() {
let args: Vec<String> = std::env::args().skip(1).collect(); let mut args: Vec<String> = std::env::args().skip(1).collect();
let mut rom_path = None;
if let Some(i) = args.iter().position(|a| a == "--rom") {
if i + 1 < args.len() {
rom_path = Some(args.remove(i + 1));
}
args.remove(i);
}
let Some(rom_path) = find_rom(rom_path) else {
eprintln!("can't find \"{ROM_NAME}\"; pass its path with --rom");
std::process::exit(1);
};
let rom = match rom::Rom::load(&rom_path) {
Ok(r) => r,
Err(e) => {
eprintln!("{e}");
std::process::exit(1);
}
};
let data = Arc::new(rom::GameData::parse(rom));
if args.first().map(|s| s.as_str()) == Some("--headless") { if args.first().map(|s| s.as_str()) == Some("--headless") {
headless::run(ROM_DATA, &args[1..]); headless::run(data, &args[1..]);
return; return;
} }
let audio: AudioQueue = Arc::new(Mutex::new(VecDeque::new())); let audio: AudioQueue = Arc::new(Mutex::new(VecDeque::new()));
let (stream, rate) = match start_audio(audio.clone()) { let (stream, rate) = match start_audio(audio.clone()) {
Some((s, r)) => (Some(s), r), Some((s, r)) => (Some(s), r),
@@ -192,8 +254,11 @@ fn main() {
let mut app = App { let mut app = App {
window: None, window: None,
pixels: None, pixels: None,
machine: Machine::new(ROM_DATA, rate), host: start_game(data, rate),
buttons: 0,
screen: vec![0; SCREEN_W * SCREEN_H],
next_frame: Instant::now(), next_frame: Instant::now(),
frame_time: Duration::from_secs_f64(1.0 / FRAME_HZ),
audio, audio,
audio_rate: rate, audio_rate: rate,
_stream: stream, _stream: stream,
+367
View File
@@ -0,0 +1,367 @@
//! The game's data, parsed out of the original ROM at startup.
//!
//! Nothing copyrighted is compiled into this program: levels, piece shapes,
//! screen layouts, story text, graphics and music all come from the .gb file
//! the player supplies. This module turns the raw bytes into typed structures;
//! the addresses below are where each table lives in the ROM.
/// Where things live in the ROM.
pub mod addr {
// ---- levels ----
pub const LEVEL_PIECES: u16 = 0x0210; // 36 pointers to [id, y, x]* 0-terminated lists
pub const PIECE_SHAPE_INDEX: u16 = 0x07FA; // (type-1)*8 + orientation -> shape number
pub const LEVEL_STORY: u16 = 0x0EA5; // story number shown before each level (0 = none)
pub const LEVEL_BOARDS: u16 = 0x19E4; // 36 pointers to board draw scripts
pub const LEVEL_NEW_PIECE: u16 = 0x255B; // piece that drops from the sky on the map
pub const SHAPE_PIECE_ID: u16 = 0x264B; // shape number -> piece id (inverse of the above)
pub const LEVEL_PIECES_NEEDED: u16 = 0x2771;
pub const LEVEL_MUSIC: u16 = 0x27A0;
pub const LEVEL_CURSOR_Y: u16 = 0x27F3;
pub const LEVEL_CURSOR_X: u16 = 0x2817;
pub const PASSWORDS: u16 = 0x3433; // 36 x 4 ASCII letters
pub const SHAPES: u16 = 0x350B; // pointers to [attr|count, (yx, tile)*count]
pub const SHAPE_COUNT: usize = 0x5E;
// ---- screen layouts (tilemap draw scripts) ----
pub const ENDING_SCREEN: u16 = 0x0ABB;
pub const ENDING_ART: [u16; 5] = [0x0B7C, 0x0B8E, 0x0BAB, 0x0BBD, 0x0BDA];
pub const LEVEL_CLEAR_BANNER: u16 = 0x0D46; // 5-byte scripts, drawn one per frame
pub const INTERMISSION_SCREEN: u16 = 0x0ED6;
pub const HOW_TO_PLAY_SCREEN: u16 = 0x0FE2;
pub const MAP_SCREEN: u16 = 0x12D4;
pub const MAP_SCENES: u16 = 0x1303; // 3 pointers: first level, middle, last level
pub const MAP_CASTLE_INDEX: u16 = 0x15C7;
pub const MAP_CASTLES: u16 = 0x15EB; // 5 pointers
pub const MAP_NEXT_CASTLE_INDEX: u16 = 0x1692;
pub const MAP_NEXT_CASTLES: u16 = 0x16B6; // 4 pointers
pub const PUZZLE_LIST_SCREEN: u16 = 0x18BE;
pub const PUZZLE_HUD: u16 = 0x27C4;
pub const TITLE_SCREEN: u16 = 0x2F2D;
pub const TITLE_SCREEN_ALT: u16 = 0x2FFE; // drawn over a copy of the title in map 1
pub const PASSWORD_SCREEN: u16 = 0x33DF;
// ---- text ----
pub const STORIES: u16 = 0x2A5A; // 4 pointers
pub const ENDING_TEXT: [u16; 3] = [0x0BEF, 0x0C22, 0x0C51];
// ---- graphics (uncompressed 2bpp tiles) ----
pub const GFX_MAP_TERRAIN: u16 = 0x5000;
pub const GFX_ENDING_A: u16 = 0x5010;
pub const GFX_ENDING_B: u16 = 0x5340;
pub const GFX_STORY: u16 = 0x5800;
pub const GFX_SPRITES: u16 = 0x6000; // sprites, then puzzle tiles from $6800
pub const GFX_ENDING_C: u16 = 0x6200;
pub const GFX_BANNER_SPARKLE: u16 = 0x6900; // two 2-tile frames
pub const GFX_HOW_TO_PLAY: u16 = 0x7000;
pub const GFX_ENDING_D: u16 = 0x7300;
pub const GFX_FONT: u16 = 0x7800;
// ---- sound ----
pub const SOUND_HEADERS: u16 = 0x39FA; // indexed by (sound id - $80) * 2
pub const SILENCE_HEADER: u16 = 0x3BF0;
pub const VIBRATO_TABLES: u16 = 0x40E5; // indexed by vibrato number * 2 (number >= 1)
pub const WAVEFORMS: u16 = 0x4157;
pub const NOTE_PERIODS: u16 = 0x419F;
}
/// The raw ROM image.
pub struct Rom {
bytes: Vec<u8>,
}
impl Rom {
pub fn load(path: &std::path::Path) -> Result<Rom, String> {
let bytes = std::fs::read(path).map_err(|e| format!("{}: {e}", path.display()))?;
if bytes.len() != 0x8000 || &bytes[0x134..0x142] != b"DAEDALIAN OPUS" {
return Err(format!("{} is not a Daedalian Opus (USA) ROM", path.display()));
}
Ok(Rom { bytes })
}
pub fn byte(&self, addr: u16) -> u8 {
self.bytes[addr as usize]
}
pub fn word(&self, addr: u16) -> u16 {
u16::from_le_bytes([self.byte(addr), self.byte(addr.wrapping_add(1))])
}
pub fn bytes(&self, addr: u16, len: usize) -> &[u8] {
&self.bytes[addr as usize..addr as usize + len]
}
fn table(&self, addr: u16, len: usize) -> Vec<u8> {
self.bytes(addr, len).to_vec()
}
fn pointers(&self, addr: u16, count: usize) -> Vec<u16> {
(0..count).map(|i| self.word(addr + 2 * i as u16)).collect()
}
}
/// A tilemap draw script: the game's format for screen layouts. Each command
/// writes a run of bytes into VRAM, either along a row or down a column.
#[derive(Clone, Default)]
pub struct DrawScript(pub Vec<DrawCmd>);
#[derive(Clone)]
pub struct DrawCmd {
pub addr: u16,
pub vertical: bool,
pub tiles: Tiles,
}
#[derive(Clone)]
pub enum Tiles {
/// the same tile repeated
Run(u8, usize),
/// one tile per position
List(Vec<u8>),
}
impl DrawScript {
/// Command byte: bit 6 = vertical, bit 5 = repeat one tile, the rest
/// ($9F mask) is the address high byte. Then address low, count, data.
/// A zero command byte ends the script.
pub fn parse(rom: &Rom, mut a: u16) -> DrawScript {
let mut cmds = Vec::new();
loop {
let c = rom.byte(a);
if c == 0 {
return DrawScript(cmds);
}
let addr = u16::from(c & 0x9F) << 8 | u16::from(rom.byte(a + 1));
let count = match rom.byte(a + 2) {
0 => 256,
n => n as usize,
};
a += 3;
let tiles = if c & 0x20 != 0 {
a += 1;
Tiles::Run(rom.byte(a - 1), count)
} else {
a += count as u16;
Tiles::List(rom.bytes(a - count as u16, count).to_vec())
};
cmds.push(DrawCmd { addr, vertical: c & 0x40 != 0, tiles });
}
}
}
/// One cell of a shape: packed y/x offsets in 8-pixel units, and a tile.
#[derive(Clone, Copy)]
pub struct ShapeCell {
pub yx: u8,
pub tile: u8,
}
impl ShapeCell {
/// Offset used when stamping a piece onto the board (unsigned nibbles).
pub fn board_offset(self) -> (u8, u8) {
((self.yx >> 4) * 8, (self.yx & 0x0F) * 8)
}
/// Offset used when drawing the shape as sprites (signed nibbles).
pub fn sprite_offset(self) -> (u8, u8) {
let dy = ((self.yx & 0xF0) as i8) >> 1;
let dx = (((self.yx << 4) & 0xF0) as i8) >> 1;
(dy as u8, dx as u8)
}
}
/// A shape: the cells of a puzzle piece in one orientation, or a multi-tile
/// sprite (cursor, hero, messenger...). Pieces and sprites share the format.
#[derive(Clone)]
pub struct Shape {
/// OAM attribute bits used when drawn as sprites (palette, flips)
pub attr: u8,
pub cells: Vec<ShapeCell>,
}
/// A piece as it sits on the board when a level starts.
#[derive(Clone, Copy)]
pub struct StartPiece {
pub id: u8,
pub y: u8,
pub x: u8,
}
#[derive(Clone)]
pub struct Level {
pub pieces: Vec<StartPiece>,
pub board: DrawScript,
pub pieces_needed: u8,
pub music: u8,
/// cursor start position, sprite coordinates
pub cursor: (u8, u8),
/// piece id that falls from the sky on the map screen (0 = none)
pub new_piece: u8,
/// story shown before the level (0 = none, 1-4)
pub story: u8,
pub map_castle: u8,
pub map_next_castle: u8,
}
/// A piece of story text, as tile numbers plus layout codes.
#[derive(Clone, Copy, PartialEq)]
pub enum Text {
Char(u8),
Newline,
PageBreak,
End,
}
fn parse_text(rom: &Rom, mut a: u16) -> Vec<Text> {
let mut out = Vec::new();
loop {
let t = match rom.byte(a) {
0x80 => Text::Newline,
0x81 => Text::PageBreak,
0x82 => Text::End,
c => Text::Char(c),
};
out.push(t);
a += 1;
if t == Text::End {
return out;
}
}
}
pub struct Scripts {
pub title: DrawScript,
pub title_alt: DrawScript,
pub password: DrawScript,
pub puzzle_list: DrawScript,
pub how_to_play: DrawScript,
pub intermission: DrawScript,
pub ending: DrawScript,
pub ending_art: Vec<DrawScript>,
pub puzzle_hud: DrawScript,
pub level_clear_banner: Vec<DrawScript>,
pub map: DrawScript,
pub map_scenes: Vec<DrawScript>,
pub map_castles: Vec<DrawScript>,
pub map_next_castles: Vec<DrawScript>,
}
/// Everything the game needs, parsed from the ROM.
pub struct GameData {
pub rom: Rom,
pub levels: Vec<Level>,
pub shapes: Vec<Shape>,
piece_shape_index: Vec<u8>,
shape_piece_id: Vec<u8>,
/// 36 four-letter ASCII passwords; entry n starts level n+1 (0-based),
/// and the last one opens the puzzle list.
pub passwords: Vec<[u8; 4]>,
pub stories: Vec<Vec<Text>>,
pub ending_text: Vec<Vec<Text>>,
pub scripts: Scripts,
}
pub const LEVEL_COUNT: usize = 36;
impl GameData {
pub fn parse(rom: Rom) -> GameData {
let script = |a| DrawScript::parse(&rom, a);
let piece_lists = rom.pointers(addr::LEVEL_PIECES, LEVEL_COUNT);
let boards = rom.pointers(addr::LEVEL_BOARDS, LEVEL_COUNT);
let levels = (0..LEVEL_COUNT)
.map(|n| {
let mut pieces = Vec::new();
let mut a = piece_lists[n];
while rom.byte(a) != 0 {
pieces.push(StartPiece { id: rom.byte(a), y: rom.byte(a + 1), x: rom.byte(a + 2) });
a += 3;
}
let n16 = n as u16;
Level {
pieces,
board: script(boards[n]),
pieces_needed: rom.byte(addr::LEVEL_PIECES_NEEDED + n16),
music: rom.byte(addr::LEVEL_MUSIC + n16),
cursor: (rom.byte(addr::LEVEL_CURSOR_Y + n16), rom.byte(addr::LEVEL_CURSOR_X + n16)),
new_piece: rom.byte(addr::LEVEL_NEW_PIECE + n16),
story: rom.byte(addr::LEVEL_STORY + n16),
map_castle: rom.byte(addr::MAP_CASTLE_INDEX + n16),
map_next_castle: rom.byte(addr::MAP_NEXT_CASTLE_INDEX + n16),
}
})
.collect();
let shapes = rom
.pointers(addr::SHAPES, addr::SHAPE_COUNT)
.into_iter()
.map(|a| {
let head = rom.byte(a);
let cells = (0..(head & 0x0F) as u16)
.map(|i| ShapeCell { yx: rom.byte(a + 1 + 2 * i), tile: rom.byte(a + 2 + 2 * i) })
.collect();
Shape { attr: head & 0xF0, cells }
})
.collect();
let passwords = (0..LEVEL_COUNT as u16)
.map(|i| rom.bytes(addr::PASSWORDS + 4 * i, 4).try_into().unwrap())
.collect();
let scripts = Scripts {
title: script(addr::TITLE_SCREEN),
title_alt: script(addr::TITLE_SCREEN_ALT),
password: script(addr::PASSWORD_SCREEN),
puzzle_list: script(addr::PUZZLE_LIST_SCREEN),
how_to_play: script(addr::HOW_TO_PLAY_SCREEN),
intermission: script(addr::INTERMISSION_SCREEN),
ending: script(addr::ENDING_SCREEN),
ending_art: addr::ENDING_ART.iter().map(|&a| script(a)).collect(),
puzzle_hud: script(addr::PUZZLE_HUD),
level_clear_banner: {
let mut v = Vec::new();
let mut a = addr::LEVEL_CLEAR_BANNER;
while rom.byte(a) != 0 {
v.push(script(a));
a += 5;
}
v
},
map: script(addr::MAP_SCREEN),
map_scenes: rom.pointers(addr::MAP_SCENES, 3).into_iter().map(script).collect(),
map_castles: rom.pointers(addr::MAP_CASTLES, 5).into_iter().map(script).collect(),
map_next_castles: rom.pointers(addr::MAP_NEXT_CASTLES, 4).into_iter().map(script).collect(),
};
GameData {
levels,
shapes,
piece_shape_index: rom.table(addr::PIECE_SHAPE_INDEX, 0x80),
shape_piece_id: rom.table(addr::SHAPE_PIECE_ID, 0x41),
passwords,
stories: rom.pointers(addr::STORIES, 4).into_iter().map(|a| parse_text(&rom, a)).collect(),
ending_text: addr::ENDING_TEXT.iter().map(|&a| parse_text(&rom, a)).collect(),
scripts,
rom,
}
}
/// Piece id is `type << 4 | orientation`: bits 0-1 rotate, bit 2 flips.
pub fn piece_shape(&self, piece_id: u8) -> u8 {
let d = piece_id.wrapping_sub(0x10);
self.piece_shape_index[((d & 0xF0) >> 1 | (d & 0x07)) as usize]
}
/// The piece id a placed shape is recorded under on the board.
pub fn shape_piece_id(&self, shape: u8) -> u8 {
self.shape_piece_id.get(shape as usize).copied().unwrap_or(0)
}
/// Story number for a level index (index 36 exists in the table too).
pub fn story_for(&self, level: u8) -> u8 {
self.rom.byte(addr::LEVEL_STORY + level as u16)
}
/// Raw tile graphics.
pub fn gfx(&self, addr: u16, len: usize) -> &[u8] {
self.rom.bytes(addr, len)
}
}
+579
View File
@@ -0,0 +1,579 @@
//! The game's sound driver: a byte-code music sequencer with 8 channels
//! (4 for music, 4 for sound effects that temporarily take over the music's
//! hardware voices). Runs once per frame and plays the sound chip in `apu.rs`.
//!
//! Songs and effects are byte code in the ROM. A sound's header is a channel
//! count followed by 8 bytes per channel: flags, voice (0-1 pulse, 2 wave,
//! 3 noise), stereo mask, start address, transpose, vibrato, envelope.
//! Channel byte code:
//!
//! | byte | meaning |
//! |-----------|------------------------------------------------------------|
//! | $00-$7F | set note length (frames) and replay the current note |
//! | $80 | rest |
//! | $81-$DF | note (+ transpose); then an optional slide step if sliding, |
//! | | then an optional length byte |
//! | $E0/$E1 a | call subroutine at a |
//! | $E2 | return |
//! | $E3 | end of song |
//! | $E4 i n a | loop: counter i starts at n; jump to a until it runs out |
//! | $E5 a | jump |
//! | $E6 v | envelope | $E7 t | transpose += t |
//! | $E8 d | duty cycle | $E9 m | stereo mask |
//! | $EA w | load waveform w | $EB | end of sound effect |
//! | $EC / $ED | pitch slide on/off | $EE | tie (don't retrigger) |
//! | $EF v | master volume | $F0 v | vibrato table v |
//! | $F1 p | set period low byte directly |
use std::collections::VecDeque;
use std::sync::Arc;
use crate::apu::*;
use crate::rom::{addr, GameData};
// ---- sound ids ----
pub const MUSIC_TITLE: u8 = 0x81;
pub const MUSIC_MAP: u8 = 0x83; // also the story and how-to-play screens
pub const JINGLE_LEVEL_CLEAR: u8 = 0x86;
pub const SFX_CONFIRM: u8 = 0x87;
pub const SFX_MENU_MOVE: u8 = 0x88;
pub const SFX_ROTATE: u8 = 0x89; // also: puzzle reset
pub const SFX_PASSWORD_LETTER: u8 = 0x8A;
pub const SFX_FLIP: u8 = 0x8B;
pub const SFX_PLACE: u8 = 0x8C;
pub const SFX_BRIDGE: u8 = 0x8D;
pub const SFX_PICK_UP: u8 = 0x8E;
pub const SFX_ERROR: u8 = 0x8F;
pub const SFX_STEP: u8 = 0x90;
pub const SFX_MOVE_PIECE: u8 = 0x91;
pub const SFX_PIECE_LANDS: u8 = 0x92;
pub const PAUSE_MUSIC: u8 = 0x95;
pub const RESUME_MUSIC: u8 = 0x96;
/// "no sound waiting to start"
const IDLE: u8 = 0x80;
// channel flag bits
const ACTIVE: u8 = 0x80;
const SLIDING: u8 = 0x20;
const PAUSED: u8 = 0x10;
/// a sound effect is using this music channel's voice
const OVERRIDDEN: u8 = 0x04;
/// the current note should restart its envelope
const RETRIGGER: u8 = 0x01;
const PULSE1: u8 = 0;
const PULSE2: u8 = 1;
const WAVE: u8 = 2;
const NOISE: u8 = 3;
#[derive(Clone, Default)]
struct Channel {
flags: u8,
voice: u8,
stereo: u8,
pc: u16,
transpose: u8,
vibrato: u8,
envelope: u8,
return_stack: Vec<u16>,
length: u8,
timer: u8,
vibrato_pos: u8,
/// period of the current note (0 = rest)
base: u16,
/// slid period
slide: u16,
/// period actually played this frame
period: u16,
slide_step: u8,
duty: u8,
loop_counters: [u8; 11],
}
/// Last values written to the chip, so unchanged values aren't rewritten
/// (rewriting a period would restart the note).
#[derive(Clone, Copy, Default)]
struct Written {
env: [u8; 4],
lo: [u8; 4],
hi: [u8; 4],
}
pub struct SoundDriver {
data: Arc<GameData>,
queue: VecDeque<u8>,
pending: u8,
music_priority: u8,
sfx_priority: u8,
music_wave: u8,
sfx_wave: u8,
loaded_wave: u8,
master_volume: u8,
/// 0-3 music, 4-7 sound effects
channels: [Channel; 8],
written: Written,
}
impl SoundDriver {
pub fn new(data: Arc<GameData>) -> SoundDriver {
SoundDriver {
data,
queue: VecDeque::new(),
pending: 0,
music_priority: 0,
sfx_priority: 0,
music_wave: 0,
sfx_wave: 0,
loaded_wave: 0,
master_volume: 0,
channels: Default::default(),
written: Written::default(),
}
}
/// Queue a song or sound effect; one queued sound starts per frame.
pub fn play(&mut self, id: u8) {
self.queue.push_back(id);
}
fn rom(&self, a: u16) -> u8 {
self.data.rom.byte(a)
}
fn rom_word(&self, a: u16) -> u16 {
self.data.rom.word(a)
}
/// Once per frame.
pub fn update(&mut self, apu: &mut Apu) {
if let Some(id) = self.queue.pop_front() {
self.pending = id;
}
self.start_pending(apu);
self.mark_overridden_voices();
for ch in 0..8 {
if self.channels[ch].flags & ACTIVE != 0 {
self.update_channel(ch, apu);
}
}
}
fn start_pending(&mut self, apu: &mut Apu) {
let id = self.pending;
if id & 0x80 == 0 || id == IDLE {
return;
}
if id >= 0x97 {
// out-of-range ids silence the music, whatever is playing
self.music_priority = 0;
self.reset_chip(apu);
self.load_channels(addr::SILENCE_HEADER, 0);
return;
}
let header = self.rom_word(addr::SOUND_HEADERS + 2 * (id - 0x80) as u16);
match id {
0x81..=0x85 => self.start_music(header, 1, apu),
0x86..=0x94 => self.start_sfx(header, 1),
PAUSE_MUSIC => {
self.sfx_priority += 1;
for ch in &mut self.channels[..4] {
ch.flags |= PAUSED;
}
self.start_sfx(header, 2);
}
RESUME_MUSIC => {
// The original then starts a "sound effect" whose header
// pointer is garbage (it points into code); just resume.
self.written = Written::default();
self.sfx_priority += 1;
for ch in &mut self.channels[..4] {
ch.flags &= !PAUSED;
}
self.pending = IDLE;
}
_ => self.pending = IDLE,
}
}
fn start_music(&mut self, header: u16, priority: u8, apu: &mut Apu) {
if self.music_priority > priority {
self.pending = IDLE;
return;
}
self.music_priority = priority;
self.reset_chip(apu);
self.load_channels(header, 0);
}
fn start_sfx(&mut self, header: u16, priority: u8) {
if self.sfx_priority > priority {
self.pending = IDLE;
return;
}
self.sfx_priority = priority;
self.written = Written::default();
for ch in &mut self.channels[4..] {
ch.flags = 0;
}
self.load_channels(header, 4);
}
fn load_channels(&mut self, header: u16, first: usize) {
let count = self.rom(header) as usize;
for i in 0..count.min(8 - first) {
let a = header + 1 + 8 * i as u16;
self.channels[first + i] = Channel {
flags: self.rom(a),
voice: self.rom(a + 1),
stereo: self.rom(a + 2),
pc: self.rom_word(a + 3),
transpose: self.rom(a + 5),
vibrato: self.rom(a + 6),
envelope: self.rom(a + 7),
length: 1,
..Default::default()
};
}
self.pending = IDLE;
}
/// Stop everything and put the chip in a known state (done before each song).
fn reset_chip(&mut self, apu: &mut Apu) {
for ch in &mut self.channels {
ch.flags = 0;
}
self.load_waveform(1, apu);
apu.write(NR52, 0x8F);
self.master_volume = 0x77;
self.music_wave = 0;
self.sfx_wave = 0;
self.loaded_wave = 0;
self.init_voices(apu);
}
fn init_voices(&mut self, apu: &mut Apu) {
apu.write(NR32, 0);
apu.write(NR51, 0);
apu.write(NR10, 0x08);
apu.write(NR12, 0xF0);
apu.write(NR22, 0xF0);
apu.write(NR42, 0xF0);
apu.write(NR30, 0x80);
for r in [NR13, NR23, NR33, NR43] {
apu.write(r, 0xFF);
}
apu.write(NR50, self.master_volume);
for r in [NR14, NR24, NR34, NR44] {
apu.write(r, 0x87);
}
self.written = Written::default();
}
fn load_waveform(&mut self, n: u8, apu: &mut Apu) {
let src = self.rom_word(addr::WAVEFORMS + 2 * n as u16);
let power = apu.read(NR52);
apu.write(NR52, 0);
for i in 0..16 {
apu.write(WAVE_RAM + i, self.rom(src + i));
}
apu.write(NR52, power);
self.init_voices(apu);
}
/// A sound effect channel takes over the music channel with the same voice.
fn mark_overridden_voices(&mut self) {
for ch in &mut self.channels[..4] {
ch.flags &= !OVERRIDDEN;
}
for i in 4..8 {
if self.channels[i].flags & ACTIVE != 0 {
let voice = self.channels[i].voice as usize;
if let Some(m) = self.channels.get_mut(voice) {
m.flags |= OVERRIDDEN;
}
}
}
}
fn update_channel(&mut self, n: usize, apu: &mut Apu) {
if self.channels[n].flags & PAUSED != 0 {
return;
}
let ch = &mut self.channels[n];
ch.timer = ch.timer.wrapping_add(1);
if ch.timer == ch.length {
if !self.next_event(n, apu) {
return; // channel ended
}
let ch = &mut self.channels[n];
if ch.flags & RETRIGGER != 0 {
ch.vibrato_pos = 0;
}
}
if self.channels[n].flags & OVERRIDDEN != 0 {
return;
}
self.update_period(n);
match self.channels[n].voice {
PULSE1 => self.write_pulse(n, 0, apu),
PULSE2 => self.write_pulse(n, 1, apu),
WAVE => self.write_wave(n, apu),
NOISE => self.write_noise(n, apu),
_ => {}
}
}
/// Apply pitch slide and vibrato to the note's period.
fn update_period(&mut self, n: usize) {
let ch = &mut self.channels[n];
ch.period = ch.base;
if ch.base == 0 {
return;
}
if ch.flags & SLIDING != 0 {
ch.slide = add_signed(ch.slide, ch.slide_step);
ch.period = ch.slide;
}
if ch.vibrato == 0 {
return;
}
let table = self.rom_word(addr::VIBRATO_TABLES + 2 * self.channels[n].vibrato as u16);
loop {
let ch = &mut self.channels[n];
let pos = ch.vibrato_pos;
ch.vibrato_pos = pos.wrapping_add(1);
match self.data.rom.byte(table + pos as u16) {
0x80 => ch.vibrato_pos = 0, // restart the table
0x81 => ch.vibrato_pos = ch.vibrato_pos.wrapping_sub(2), // repeat previous entry
0x82 => {
ch.vibrato_pos = ch.vibrato_pos.wrapping_sub(1); // hold
return;
}
0x83 => ch.vibrato_pos = self.data.rom.byte(table + pos as u16 + 1), // jump
delta => {
ch.period = add_signed(ch.period, delta);
return;
}
}
}
}
fn silence(voice: u8, written: &mut Written, apu: &mut Apu) {
match voice {
PULSE1 | PULSE2 => {
let (lo, hi) = if voice == PULSE1 { (NR13, NR14) } else { (NR23, NR24) };
apu.write(lo, 0xFF);
apu.write(hi, 0x87);
written.lo[voice as usize] = 0xFF;
written.hi[voice as usize] = 0x87;
}
WAVE => {
apu.write(NR33, 0xFF);
apu.write(NR34, 0x07);
written.lo[2] = 0xFF;
written.hi[2] = 0x07;
}
NOISE => {
apu.write(NR43, 0xFF);
apu.write(NR44, 0x80);
written.lo[3] = 0xFF;
}
_ => {}
}
}
fn write_pulse(&mut self, n: usize, voice: usize, apu: &mut Apu) {
let [nr1, nr2, nr3, nr4] = if voice == 0 { [NR11, NR12, NR13, NR14] } else { [NR21, NR22, NR23, NR24] };
let w = &mut self.written;
let ch = &mut self.channels[n];
apu.write(nr1, ch.duty & 0xC0);
if w.env[voice] != ch.envelope {
w.env[voice] = ch.envelope;
apu.write(nr2, ch.envelope);
}
if ch.period == 0 {
Self::silence(voice as u8, w, apu);
return;
}
let pan = if voice == 0 { 0x11 } else { 0x22 };
apu.write(NR51, (apu.read(NR51) | pan) & ch.stereo);
let [lo, hi] = ch.period.to_le_bytes();
let retrigger = ch.flags & RETRIGGER != 0;
if !retrigger && w.lo[voice] == lo && w.hi[voice] == hi {
return;
}
apu.write(nr3, lo);
w.lo[voice] = lo;
w.hi[voice] = hi;
ch.flags &= !RETRIGGER;
apu.write(nr4, (hi & 7) | if retrigger { 0x80 } else { 0 });
}
fn write_wave(&mut self, n: usize, apu: &mut Apu) {
let w = &mut self.written;
let ch = &mut self.channels[n];
if ch.period == 0 {
Self::silence(WAVE, w, apu);
return;
}
if w.env[2] != ch.envelope {
w.env[2] = ch.envelope;
apu.write(NR32, ch.envelope & 0x60);
}
apu.write(NR51, (apu.read(NR51) | 0x44) & ch.stereo);
let [lo, hi] = ch.period.to_le_bytes();
if w.lo[2] == lo && w.hi[2] == hi {
return;
}
apu.write(NR33, lo);
w.lo[2] = lo;
w.hi[2] = hi;
apu.write(NR34, hi & 7); // the wave voice is never restarted
}
fn write_noise(&mut self, n: usize, apu: &mut Apu) {
let w = &mut self.written;
let ch = &mut self.channels[n];
if w.env[3] != ch.envelope {
w.env[3] = ch.envelope;
apu.write(NR42, ch.envelope);
}
let poly = ch.period as u8;
if poly == 0 {
Self::silence(NOISE, w, apu);
return;
}
apu.write(NR51, (apu.read(NR51) | 0x88) & ch.stereo);
if w.lo[3] == poly {
return;
}
apu.write(NR43, poly);
w.lo[3] = poly;
// (sic) noise restarts only when the note is *not* flagged to retrigger
let restart = if ch.flags & RETRIGGER != 0 { 0 } else { 0x80 };
ch.flags &= !RETRIGGER;
apu.write(NR44, restart);
}
/// Run byte code up to the next note. Returns false if the channel ended.
fn next_event(&mut self, n: usize, apu: &mut Apu) -> bool {
self.channels[n].flags |= RETRIGGER;
let mut pc = self.channels[n].pc;
loop {
let op = self.rom(pc);
pc += 1;
if op >= 0xE0 {
match self.command(n, op, &mut pc, apu) {
Some(()) => continue,
None => return false,
}
}
let ch = &mut self.channels[n];
if op < 0x80 {
ch.length = op;
} else {
let note = if op == 0x80 { op } else { op.wrapping_add(ch.transpose) };
ch.base = self.data.rom.word(addr::NOTE_PERIODS + (note as u16 * 2 & 0xFF));
ch.slide = ch.base;
if ch.flags & SLIDING != 0 {
ch.slide_step = self.data.rom.byte(pc);
pc += 1;
}
let next = self.data.rom.byte(pc);
if next & 0x80 == 0 {
ch.length = next;
pc += 1;
}
}
ch.pc = pc;
ch.timer = 0;
return true;
}
}
/// Execute one $E0-$F1 command. None = the channel stopped.
fn command(&mut self, n: usize, op: u8, pc: &mut u16, apu: &mut Apu) -> Option<()> {
let arg = self.rom(*pc);
let target = self.rom_word(*pc);
let ch = &mut self.channels[n];
match op {
0xE0 | 0xE1 => {
ch.return_stack.push(*pc + 2);
*pc = target;
}
0xE2 => *pc = ch.return_stack.pop().unwrap_or(*pc),
0xE3 => {
self.music_priority = 0;
self.stop_channel(n, apu);
return None;
}
0xE4 => {
let counter = &mut ch.loop_counters[arg as usize];
if *counter == 0 {
*counter = self.data.rom.byte(*pc + 1);
}
*counter -= 1;
*pc = if *counter == 0 { *pc + 4 } else { self.data.rom.word(*pc + 2) };
}
0xE5 => *pc = target,
0xE6 => (ch.envelope, *pc) = (arg, *pc + 1),
0xE7 => (ch.transpose, *pc) = (ch.transpose.wrapping_add(arg), *pc + 1),
0xE8 => (ch.duty, *pc) = (arg, *pc + 1),
0xE9 => (ch.stereo, *pc) = (arg, *pc + 1),
0xEA => {
*pc += 1;
self.select_waveform(n, arg, apu);
}
0xEB => {
self.sfx_priority = 0;
if self.music_wave != self.loaded_wave {
self.loaded_wave = self.music_wave;
self.load_waveform(self.music_wave, apu);
}
self.stop_channel(n, apu);
return None;
}
0xEC => ch.flags |= SLIDING,
0xED => ch.flags &= !SLIDING,
0xEE => ch.flags &= !RETRIGGER,
0xEF => {
*pc += 1;
self.master_volume = arg;
apu.write(NR50, arg);
}
0xF0 => (ch.vibrato, *pc) = (arg, *pc + 1),
0xF1 => (ch.base, *pc) = ((ch.base & 0xFF00) | arg as u16, *pc + 1),
_ => {} // unused opcodes
}
Some(())
}
fn select_waveform(&mut self, n: usize, w: u8, apu: &mut Apu) {
if self.channels[n].flags & OVERRIDDEN != 0 {
self.music_wave = w;
return;
}
if w == self.loaded_wave {
return;
}
self.loaded_wave = w;
// (sic) the original counts only channels 0-2 as music here
if n < 3 {
self.music_wave = w;
} else {
self.sfx_wave = w;
}
self.load_waveform(w, apu);
}
fn stop_channel(&mut self, n: usize, apu: &mut Apu) {
self.channels[n].flags = 0;
Self::silence(self.channels[n].voice, &mut self.written, apu);
}
}
/// period + signed 8-bit delta
fn add_signed(v: u16, delta: u8) -> u16 {
v.wrapping_add(delta as i8 as u16)
}
+167
View File
@@ -0,0 +1,167 @@
//! The display: tile graphics, two 32x32 background maps, 40 hardware
//! sprites and three palettes, rendered to a 160x144 image of 4 shades.
//!
//! This is the model the game's art is built for, so the game draws by
//! writing tiles into maps and placing sprites, exactly as the original did.
//! The game never scrolls and never uses the window layer, so neither exists
//! here.
use crate::rom::{DrawScript, Tiles};
pub const SCREEN_W: usize = 160;
pub const SCREEN_H: usize = 144;
/// Background map base addresses (the game addresses tiles by VRAM address,
/// and so do its draw scripts).
pub const MAP0: u16 = 0x9800;
pub const MAP1: u16 = 0x9C00;
pub const MAP_SIZE: usize = 0x400;
/// Address of the map cell at (column, row).
pub const fn map_cell(map: u16, col: u16, row: u16) -> u16 {
map + row * 32 + col
}
#[derive(Clone, Copy, Default)]
pub struct Sprite {
/// screen y + 16 (0 hides the sprite)
pub y: u8,
/// screen x + 8
pub x: u8,
pub tile: u8,
/// bit 4 = palette 1, bit 5 = x flip, bit 6 = y flip, bit 7 = behind background
pub attr: u8,
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum Display {
Off,
/// background (from the given map) and sprites
On(u16),
}
pub struct Video {
vram: Box<[u8; 0x2000]>,
pub sprites: [Sprite; 40],
pub bg_palette: u8,
pub obj_palettes: [u8; 2],
pub display: Display,
}
impl Video {
pub fn new() -> Video {
Video {
vram: Box::new([0; 0x2000]),
sprites: [Sprite::default(); 40],
bg_palette: 0,
obj_palettes: [0; 2],
display: Display::Off,
}
}
pub fn peek(&self, addr: u16) -> u8 {
self.vram[(addr & 0x1FFF) as usize]
}
pub fn poke(&mut self, addr: u16, v: u8) {
if (0x8000..0xA000).contains(&addr) {
self.vram[(addr - 0x8000) as usize] = v;
}
}
/// Copy tile graphics (or anything else) into VRAM.
pub fn load(&mut self, addr: u16, bytes: &[u8]) {
for (i, &b) in bytes.iter().enumerate() {
self.poke(addr.wrapping_add(i as u16), b);
}
}
/// Fill both background maps with one tile.
pub fn fill_maps(&mut self, tile: u8) {
self.vram[0x1800..0x2000].fill(tile);
}
pub fn copy_map(&mut self, from: u16, to: u16) {
let (f, t) = ((from - 0x8000) as usize, (to - 0x8000) as usize);
self.vram.copy_within(f..f + MAP_SIZE, t);
}
/// Write `n` copies of `tile` along a row.
pub fn fill_row(&mut self, addr: u16, tile: u8, n: u16) {
for i in 0..n {
self.poke(addr + i, tile);
}
}
pub fn draw(&mut self, script: &DrawScript) {
for cmd in &script.0 {
let step = if cmd.vertical { 32 } else { 1 };
let mut a = cmd.addr;
let mut put = |t| {
self.poke(a, t);
a = a.wrapping_add(step);
};
match &cmd.tiles {
Tiles::Run(t, n) => (0..*n).for_each(|_| put(*t)),
Tiles::List(ts) => ts.iter().for_each(|&t| put(t)),
}
}
}
fn tile_pixel(&self, tile_addr: usize, x: usize, y: usize) -> u8 {
let lo = self.vram[tile_addr + y * 2];
let hi = self.vram[tile_addr + y * 2 + 1];
((lo >> (7 - x)) & 1) | (((hi >> (7 - x)) & 1) << 1)
}
/// Background tiles 0-127 come from $9000, 128-255 from $8800.
fn bg_tile_addr(tile: u8) -> usize {
(0x1000 + (tile as i8 as isize) * 16) as usize
}
/// Render the frame as shades 0 (white) to 3 (black).
pub fn render(&self, out: &mut [u8]) {
let map = match self.display {
Display::Off => {
out.fill(0);
return;
}
Display::On(map) => (map - 0x8000) as usize,
};
let shade = |pal: u8, c: u8| (pal >> (c * 2)) & 3;
let mut bg_color = [0u8; SCREEN_W * SCREEN_H];
for y in 0..SCREEN_H {
for x in 0..SCREEN_W {
let tile = self.vram[map + (y / 8) * 32 + x / 8];
let c = self.tile_pixel(Self::bg_tile_addr(tile), x % 8, y % 8);
bg_color[y * SCREEN_W + x] = c;
out[y * SCREEN_W + x] = shade(self.bg_palette, c);
}
}
// lower sprite numbers win, so draw from the end
for s in self.sprites.iter().rev() {
let (sy, sx) = (s.y as i32 - 16, s.x as i32 - 8);
let pal = self.obj_palettes[(s.attr >> 4 & 1) as usize];
for ty in 0..8 {
let py = sy + ty as i32;
if !(0..SCREEN_H as i32).contains(&py) {
continue;
}
let row = if s.attr & 0x40 != 0 { 7 - ty } else { ty };
for tx in 0..8 {
let px = sx + tx as i32;
if !(0..SCREEN_W as i32).contains(&px) {
continue;
}
let col = if s.attr & 0x20 != 0 { 7 - tx } else { tx };
let c = self.tile_pixel(s.tile as usize * 16, col, row);
let i = py as usize * SCREEN_W + px as usize;
if c == 0 || (s.attr & 0x80 != 0 && bg_color[i] != 0) {
continue;
}
out[i] = shade(pal, c);
}
}
}
}
}