Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ecc66561d1 |
+5
-3
@@ -1,11 +1,13 @@
|
||||
# Build output
|
||||
/target/
|
||||
|
||||
# The game reads its data from the original ROM at runtime; never commit it.
|
||||
*.gb
|
||||
# Generated from the ROM by ../recompile.py. These are derived from the
|
||||
# copyrighted game, so keep them out of the repo; regenerate with:
|
||||
# python3 ../recompile.py "../Daedalian Opus (USA).gb" src/game.rs assets/rom_data.bin
|
||||
/src/game.rs
|
||||
/assets/rom_data.bin
|
||||
|
||||
# Headless test output
|
||||
/out/
|
||||
*.wav
|
||||
|
||||
# Editors / OS
|
||||
|
||||
Generated
+9
-9
@@ -391,6 +391,15 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f"
|
||||
|
||||
[[package]]
|
||||
name = "daedalian"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cpal",
|
||||
"pixels",
|
||||
"winit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dasp_sample"
|
||||
version = "0.11.0"
|
||||
@@ -1288,15 +1297,6 @@ version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "opus"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cpal",
|
||||
"pixels",
|
||||
"winit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "orbclient"
|
||||
version = "0.3.55"
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "opus"
|
||||
name = "daedalian"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Daedalian Opus, ported from its Game Boy disassembly to Rust"
|
||||
description = "Daedalian Opus, statically recompiled from its Game Boy disassembly"
|
||||
|
||||
[dependencies]
|
||||
winit = "0.30"
|
||||
|
||||
@@ -1,80 +1,42 @@
|
||||
# Daedalian Opus, ported to Rust
|
||||
# Daedalian Opus — recompiled to Rust
|
||||
|
||||
A rewrite of the Game Boy game as ordinary Rust, translated by hand from the
|
||||
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.
|
||||
A proof of concept: the Game Boy game's code, statically translated from the
|
||||
disassembly into Rust, running on a tiny hand-written hardware layer.
|
||||
|
||||
```
|
||||
cargo run --release # looks for "Daedalian Opus (USA).gb" here or in ..
|
||||
cargo run --release -- --rom PATH
|
||||
cargo run --release
|
||||
```
|
||||
|
||||
Controls: arrows = D-pad, Z = A, X = B, Enter = Start, Right Shift/Backspace = Select, Esc = quit.
|
||||
|
||||
## What comes from where
|
||||
## How it works
|
||||
|
||||
The program contains no game content. At startup `rom.rs` parses the ROM
|
||||
into typed data: levels (`Level`: starting pieces, board layout, cursor
|
||||
start, music, pieces needed, which piece drops on the map), piece shapes,
|
||||
screen layouts (tilemap draw scripts), story text, passwords, tile graphics
|
||||
and the music byte code. Small numbers that are really code (coordinates,
|
||||
tile numbers for digits, timings) live in the Rust source as named constants.
|
||||
- `src/game.rs` is **generated** by `../recompile.py` from the traced
|
||||
disassembly: every SM83 instruction becomes Rust statements, grouped into
|
||||
basic blocks (`match m.pc { 0x0164 => { ... } }`), each commented with the
|
||||
original assembly. No SM83 code is decoded at runtime. The game's stack lives
|
||||
in emulated memory because the game manipulates return addresses itself.
|
||||
- `assets/rom_data.bin` is the ROM with every code byte zeroed — only the
|
||||
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.
|
||||
|
||||
## Layout
|
||||
Regenerate after changing the recompiler:
|
||||
|
||||
| file | what |
|
||||
|---|---|
|
||||
| `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.
|
||||
```
|
||||
python3 ../recompile.py "../Daedalian Opus (USA).gb" src/game.rs assets/rom_data.bin
|
||||
```
|
||||
|
||||
## Headless testing
|
||||
|
||||
```
|
||||
cargo run --release -- --headless OUT_DIR 120 start:2 90 shot:title probe goto:44:54
|
||||
cargo run -- --headless OUT_DIR 120 start:3 90 shot:title peek:C0A1:2 goto:44:54
|
||||
```
|
||||
|
||||
Tokens: `N` run N frames; `BUTTON:N` hold a button for N frames (up down left
|
||||
right a b start select); `shot:NAME` save a PNG; `probe` print the level and
|
||||
cursor; `goto:YY:XX` steer the puzzle cursor (hex sprite coordinates). Audio
|
||||
goes to `OUT_DIR/audio.wav`.
|
||||
Tokens: `N` run N frames, `BUTTON:N` hold a button N frames, `shot:NAME` save a
|
||||
PNG, `peek:ADDR:N` print memory, `goto:YY:XX` steer the puzzle cursor. All
|
||||
audio is written to `OUT_DIR/audio.wav`.
|
||||
|
||||
+12
-39
@@ -1,34 +1,9 @@
|
||||
//! The Game Boy sound chip: two pulse channels (one with sweep), the wave
|
||||
//! channel and the noise channel, mixed to stereo f32 samples.
|
||||
//!
|
||||
//! 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.
|
||||
//! Game Boy APU: two pulse channels (one with sweep), the wave channel, and
|
||||
//! the noise channel, mixed to stereo f32 samples. Registers are updated as
|
||||
//! the game writes them; samples are synthesized up to the current CPU cycle
|
||||
//! before every register write, so timing follows the recompiled code.
|
||||
|
||||
/// 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;
|
||||
use crate::machine::CPU_HZ;
|
||||
|
||||
const DUTY: [[u8; 8]; 4] = [
|
||||
[0, 0, 0, 0, 0, 0, 0, 1],
|
||||
@@ -109,8 +84,6 @@ pub struct Apu {
|
||||
wave: Wave,
|
||||
noise: Noise,
|
||||
sample_rate: u32,
|
||||
/// clock cycles synthesized so far
|
||||
now: u64,
|
||||
samples_made: u64,
|
||||
last_cycle: u64,
|
||||
seq_cycles: u64,
|
||||
@@ -130,7 +103,6 @@ impl Apu {
|
||||
wave: Wave::default(),
|
||||
noise: Noise::default(),
|
||||
sample_rate,
|
||||
now: 0,
|
||||
samples_made: 0,
|
||||
last_cycle: 0,
|
||||
seq_cycles: 0,
|
||||
@@ -149,7 +121,8 @@ impl Apu {
|
||||
self.regs[(addr - 0xFF10) as usize]
|
||||
}
|
||||
|
||||
pub fn read(&self, addr: u16) -> u8 {
|
||||
pub fn read(&mut self, addr: u16, cycles: u64) -> u8 {
|
||||
self.catch_up(cycles);
|
||||
if addr == 0xFF26 {
|
||||
return (self.reg(0xFF26) & 0x80)
|
||||
| 0x70
|
||||
@@ -161,7 +134,8 @@ impl Apu {
|
||||
self.reg(addr)
|
||||
}
|
||||
|
||||
pub fn write(&mut self, addr: u16, v: u8) {
|
||||
pub fn write(&mut self, addr: u16, v: u8, cycles: u64) {
|
||||
self.catch_up(cycles);
|
||||
let powered = self.reg(0xFF26) & 0x80 != 0;
|
||||
if !powered && addr != 0xFF26 && addr < 0xFF30 {
|
||||
return;
|
||||
@@ -303,11 +277,10 @@ impl Apu {
|
||||
}
|
||||
}
|
||||
|
||||
/// Synthesize the next `cycles` clock cycles of sound into `out`.
|
||||
pub fn run(&mut self, cycles: u64) {
|
||||
self.now += cycles;
|
||||
/// Synthesize samples up to CPU cycle `cycles`.
|
||||
pub fn catch_up(&mut self, cycles: u64) {
|
||||
let rate = self.sample_rate as f32;
|
||||
let target = self.now * self.sample_rate as u64 / CPU_HZ;
|
||||
let target = cycles * self.sample_rate as u64 / CPU_HZ;
|
||||
while self.samples_made < target {
|
||||
self.samples_made += 1;
|
||||
let cyc = self.samples_made * CPU_HZ / self.sample_rate as u64;
|
||||
|
||||
-370
@@ -1,370 +0,0 @@
|
||||
//! 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,287 +0,0 @@
|
||||
//! 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
@@ -1,422 +0,0 @@
|
||||
//! 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
|
||||
}
|
||||
@@ -1,341 +0,0 @@
|
||||
//! 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
//! 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+33
-32
@@ -1,15 +1,9 @@
|
||||
//! Headless runner for testing: `opus --headless OUT_DIR SCRIPT...`
|
||||
//!
|
||||
//! Headless runner for testing: `daedalian --headless OUT_DIR SCRIPT...`
|
||||
//! 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;
|
||||
//! `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.
|
||||
//! (up down left right a b start select); `shot:NAME` save OUT_DIR/NAME.png.
|
||||
//! All audio is written to OUT_DIR/audio.wav.
|
||||
|
||||
use crate::game::Frame;
|
||||
use crate::input;
|
||||
use crate::rom::GameData;
|
||||
use crate::video::{SCREEN_H, SCREEN_W};
|
||||
use std::sync::Arc;
|
||||
use crate::machine::*;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
@@ -84,53 +78,60 @@ fn write_wav(path: &Path, rate: u32, samples: &[f32]) {
|
||||
fs::write(path, d).unwrap();
|
||||
}
|
||||
|
||||
pub fn run(data: Arc<GameData>, args: &[String]) {
|
||||
pub fn run(rom: &[u8], args: &[String]) {
|
||||
let out = Path::new(&args[0]);
|
||||
fs::create_dir_all(out).unwrap();
|
||||
let rate = 44_100;
|
||||
let host = crate::start_game(data, rate);
|
||||
let mut m = Machine::new(rom, rate);
|
||||
let mut audio = Vec::new();
|
||||
let mut frames = 0u64;
|
||||
let mut frame: Frame = host.frames.recv().expect("game thread");
|
||||
let mut step = |buttons: u8, n: u64, frame: &mut Frame| {
|
||||
let run = |m: &mut Machine, n: u64, audio: &mut Vec<f32>, frames: &mut u64| {
|
||||
for _ in 0..n {
|
||||
host.buttons.send(buttons).expect("game thread");
|
||||
*frame = host.frames.recv().expect("game thread");
|
||||
audio.extend_from_slice(&frame.audio);
|
||||
frames += 1;
|
||||
m.run_frame();
|
||||
audio.append(&mut m.apu.out);
|
||||
*frames += 1;
|
||||
}
|
||||
};
|
||||
for tok in &args[1..] {
|
||||
if let Ok(n) = tok.parse::<u64>() {
|
||||
step(0, n, &mut frame);
|
||||
run(&mut m, n, &mut audio, &mut frames);
|
||||
} else if let Some(name) = tok.strip_prefix("shot:") {
|
||||
let gray: Vec<u8> = frame.pixels.iter().map(|&s| [0xFF, 0xAA, 0x55, 0x00][s as usize]).collect();
|
||||
let gray: Vec<u8> = m.framebuffer.iter().map(|&s| [0xFF, 0xAA, 0x55, 0x00][s as usize]).collect();
|
||||
write_png(&out.join(format!("{name}.png")), SCREEN_W, SCREEN_H, &gray);
|
||||
println!("{name}.png: {:?}", frame.probe);
|
||||
} else if tok == "probe" {
|
||||
println!("{:?}", frame.probe);
|
||||
println!("frame {frames}: {name}.png (pc ${:04X})", m.pc);
|
||||
} 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) = (u8::from_str_radix(ty, 16).unwrap(), u8::from_str_radix(tx, 16).unwrap());
|
||||
for _ in 0..100 {
|
||||
let p = frame.probe;
|
||||
let bit = if p.cursor_y < ty { input::DOWN } else if p.cursor_y > ty { input::UP }
|
||||
else if p.cursor_x < tx { input::RIGHT } else if p.cursor_x > tx { input::LEFT } else { break };
|
||||
step(bit, 1, &mut frame);
|
||||
step(0, 5, &mut frame);
|
||||
let (cy, cx) = (m.r8(0xC0A1), m.r8(0xC0A2));
|
||||
let bit = if cy < ty { BTN_DOWN } else if cy > ty { BTN_UP }
|
||||
else if cx < tx { BTN_RIGHT } else if cx > tx { BTN_LEFT } else { break };
|
||||
m.buttons |= bit;
|
||||
run(&mut m, 1, &mut audio, &mut frames);
|
||||
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(':') {
|
||||
let bit = match btn {
|
||||
"up" => input::UP, "down" => input::DOWN, "left" => input::LEFT, "right" => input::RIGHT,
|
||||
"a" => input::A, "b" => input::B, "start" => input::START, "select" => input::SELECT,
|
||||
"up" => BTN_UP, "down" => BTN_DOWN, "left" => BTN_LEFT, "right" => BTN_RIGHT,
|
||||
"a" => BTN_A, "b" => BTN_B, "start" => BTN_START, "select" => BTN_SELECT,
|
||||
_ => panic!("unknown token {tok}"),
|
||||
};
|
||||
step(bit, n.parse().unwrap(), &mut frame);
|
||||
m.buttons |= bit;
|
||||
run(&mut m, n.parse().unwrap(), &mut audio, &mut frames);
|
||||
m.buttons &= !bit;
|
||||
} else {
|
||||
panic!("unknown token {tok}");
|
||||
}
|
||||
}
|
||||
drop(step);
|
||||
write_wav(&out.join("audio.wav"), rate, &audio);
|
||||
println!("{frames} frames, {:.1}s of audio", audio.len() as f32 / 2.0 / rate as f32);
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
//! 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
@@ -0,0 +1,399 @@
|
||||
#![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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
-101
@@ -1,24 +1,13 @@
|
||||
//! Daedalian Opus, ported to Rust.
|
||||
//! Daedalian Opus, running as recompiled Rust.
|
||||
//!
|
||||
//! 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.
|
||||
//! Controls: arrows = D-pad, Z = A, X = B, Enter = Start, Right Shift / Backspace = Select
|
||||
|
||||
mod apu;
|
||||
mod game;
|
||||
mod headless;
|
||||
mod input;
|
||||
mod rom;
|
||||
mod sound;
|
||||
mod video;
|
||||
mod machine;
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::mpsc::{sync_channel, Receiver, SyncSender};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
@@ -31,10 +20,10 @@ use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
|
||||
use winit::keyboard::{KeyCode, PhysicalKey};
|
||||
use winit::window::{Window, WindowId};
|
||||
|
||||
use game::{Frame, FRAME_HZ};
|
||||
use video::{SCREEN_H, SCREEN_W};
|
||||
use machine::*;
|
||||
|
||||
const ROM_NAME: &str = "Daedalian Opus (USA).gb";
|
||||
const ROM_DATA: &[u8] = include_bytes!("../assets/rom_data.bin");
|
||||
const FRAME: Duration = Duration::from_nanos(1_000_000_000 * 70_224 / CPU_HZ);
|
||||
const SHADES: [[u8; 4]; 4] = [
|
||||
[0xFF, 0xFF, 0xFF, 0xFF],
|
||||
[0xAA, 0xAA, 0xAA, 0xFF],
|
||||
@@ -44,27 +33,11 @@ const SHADES: [[u8; 4]; 4] = [
|
||||
|
||||
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 {
|
||||
window: Option<Arc<Window>>,
|
||||
pixels: Option<Pixels<'static>>,
|
||||
host: Host,
|
||||
buttons: u8,
|
||||
screen: Vec<u8>,
|
||||
machine: Machine,
|
||||
next_frame: Instant,
|
||||
frame_time: Duration,
|
||||
audio: AudioQueue,
|
||||
audio_rate: u32,
|
||||
_stream: Option<cpal::Stream>,
|
||||
@@ -103,36 +76,42 @@ fn start_audio(queue: AudioQueue) -> Option<(cpal::Stream, u32)> {
|
||||
impl App {
|
||||
fn key(&mut self, code: KeyCode, pressed: bool) {
|
||||
let bit = match code {
|
||||
KeyCode::ArrowRight => input::RIGHT,
|
||||
KeyCode::ArrowLeft => input::LEFT,
|
||||
KeyCode::ArrowUp => input::UP,
|
||||
KeyCode::ArrowDown => input::DOWN,
|
||||
KeyCode::KeyZ => input::A,
|
||||
KeyCode::KeyX => input::B,
|
||||
KeyCode::ShiftRight | KeyCode::Backspace => input::SELECT,
|
||||
KeyCode::Enter => input::START,
|
||||
KeyCode::ArrowRight => BTN_RIGHT,
|
||||
KeyCode::ArrowLeft => BTN_LEFT,
|
||||
KeyCode::ArrowUp => BTN_UP,
|
||||
KeyCode::ArrowDown => BTN_DOWN,
|
||||
KeyCode::KeyZ => BTN_A,
|
||||
KeyCode::KeyX => BTN_B,
|
||||
KeyCode::ShiftRight | KeyCode::Backspace => BTN_SELECT,
|
||||
KeyCode::Enter => BTN_START,
|
||||
_ => return,
|
||||
};
|
||||
if pressed {
|
||||
self.buttons |= bit;
|
||||
self.machine.buttons |= bit;
|
||||
} else {
|
||||
self.buttons &= !bit;
|
||||
self.machine.buttons &= !bit;
|
||||
}
|
||||
}
|
||||
|
||||
/// Advance the game one frame. False if the game thread has stopped.
|
||||
fn step(&mut self) -> bool {
|
||||
let Ok(frame) = self.host.frames.recv() else { return false };
|
||||
self.screen = frame.pixels;
|
||||
fn run_due_frames(&mut self) {
|
||||
let now = Instant::now();
|
||||
let mut ran = 0;
|
||||
while self.next_frame <= now && ran < 4 {
|
||||
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();
|
||||
q.extend(frame.audio);
|
||||
q.extend(samples);
|
||||
let max = self.audio_rate as usize / 5 * 2; // keep at most ~200 ms queued
|
||||
if q.len() > max {
|
||||
let excess = q.len() - max;
|
||||
q.drain(..excess);
|
||||
}
|
||||
drop(q);
|
||||
self.host.buttons.send(self.buttons).is_ok()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,7 +121,7 @@ impl ApplicationHandler for App {
|
||||
return;
|
||||
}
|
||||
let attrs = Window::default_attributes()
|
||||
.with_title("Daedalian Opus")
|
||||
.with_title("Daedalian Opus (recompiled)")
|
||||
.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 size = window.inner_size();
|
||||
@@ -171,7 +150,7 @@ impl ApplicationHandler for App {
|
||||
}
|
||||
WindowEvent::RedrawRequested => {
|
||||
if let Some(p) = &mut self.pixels {
|
||||
for (dst, &shade) in p.frame_mut().chunks_exact_mut(4).zip(&self.screen) {
|
||||
for (dst, &shade) in p.frame_mut().chunks_exact_mut(4).zip(&self.machine.framebuffer) {
|
||||
dst.copy_from_slice(&SHADES[shade as usize]);
|
||||
}
|
||||
if let Err(e) = p.render() {
|
||||
@@ -188,20 +167,7 @@ impl ApplicationHandler for App {
|
||||
if self.window.is_none() {
|
||||
return;
|
||||
}
|
||||
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
|
||||
}
|
||||
self.run_due_frames();
|
||||
if let Some(w) = &self.window {
|
||||
w.request_redraw();
|
||||
}
|
||||
@@ -209,40 +175,12 @@ 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() {
|
||||
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));
|
||||
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
if args.first().map(|s| s.as_str()) == Some("--headless") {
|
||||
headless::run(data, &args[1..]);
|
||||
headless::run(ROM_DATA, &args[1..]);
|
||||
return;
|
||||
}
|
||||
|
||||
let audio: AudioQueue = Arc::new(Mutex::new(VecDeque::new()));
|
||||
let (stream, rate) = match start_audio(audio.clone()) {
|
||||
Some((s, r)) => (Some(s), r),
|
||||
@@ -254,11 +192,8 @@ fn main() {
|
||||
let mut app = App {
|
||||
window: None,
|
||||
pixels: None,
|
||||
host: start_game(data, rate),
|
||||
buttons: 0,
|
||||
screen: vec![0; SCREEN_W * SCREEN_H],
|
||||
machine: Machine::new(ROM_DATA, rate),
|
||||
next_frame: Instant::now(),
|
||||
frame_time: Duration::from_secs_f64(1.0 / FRAME_HZ),
|
||||
audio,
|
||||
audio_rate: rate,
|
||||
_stream: stream,
|
||||
|
||||
-367
@@ -1,367 +0,0 @@
|
||||
//! 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
@@ -1,579 +0,0 @@
|
||||
//! 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
@@ -1,167 +0,0 @@
|
||||
//! 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user