Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ecc66561d1 |
+5
-3
@@ -1,11 +1,13 @@
|
|||||||
# Build output
|
# Build output
|
||||||
/target/
|
/target/
|
||||||
|
|
||||||
# The game reads its data from the original ROM at runtime; never commit it.
|
# Generated from the ROM by ../recompile.py. These are derived from the
|
||||||
*.gb
|
# 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
|
# Headless test output
|
||||||
/out/
|
|
||||||
*.wav
|
*.wav
|
||||||
|
|
||||||
# Editors / OS
|
# Editors / OS
|
||||||
|
|||||||
Generated
+9
-9
@@ -391,6 +391,15 @@ version = "1.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f"
|
checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "daedalian"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"cpal",
|
||||||
|
"pixels",
|
||||||
|
"winit",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dasp_sample"
|
name = "dasp_sample"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
@@ -1288,15 +1297,6 @@ version = "1.21.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "opus"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"cpal",
|
|
||||||
"pixels",
|
|
||||||
"winit",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "orbclient"
|
name = "orbclient"
|
||||||
version = "0.3.55"
|
version = "0.3.55"
|
||||||
|
|||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "opus"
|
name = "daedalian"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Daedalian Opus, ported from its Game Boy disassembly to Rust"
|
description = "Daedalian Opus, statically recompiled from its Game Boy disassembly"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
winit = "0.30"
|
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
|
A proof of concept: the Game Boy game's code, statically translated from the
|
||||||
disassembly in `../disassembly/`. Unlike `../rust/` (a static recompilation that
|
disassembly into Rust, running on a tiny hand-written hardware layer.
|
||||||
still runs the original instructions against emulated registers and memory),
|
|
||||||
this is the game itself: the screens, the puzzle rules, the map, the story and
|
|
||||||
the music sequencer are Rust functions over named state, so they can be read
|
|
||||||
and changed.
|
|
||||||
|
|
||||||
```
|
```
|
||||||
cargo run --release # looks for "Daedalian Opus (USA).gb" here or in ..
|
cargo run --release
|
||||||
cargo run --release -- --rom PATH
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Controls: arrows = D-pad, Z = A, X = B, Enter = Start, Right Shift / Backspace = Select, Esc = quit.
|
Controls: arrows = D-pad, Z = A, X = B, Enter = Start, Right Shift/Backspace = Select, Esc = quit.
|
||||||
|
|
||||||
## What comes from where
|
## How it works
|
||||||
|
|
||||||
The program contains no game content. At startup `rom.rs` parses the ROM
|
- `src/game.rs` is **generated** by `../recompile.py` from the traced
|
||||||
into typed data: levels (`Level`: starting pieces, board layout, cursor
|
disassembly: every SM83 instruction becomes Rust statements, grouped into
|
||||||
start, music, pieces needed, which piece drops on the map), piece shapes,
|
basic blocks (`match m.pc { 0x0164 => { ... } }`), each commented with the
|
||||||
screen layouts (tilemap draw scripts), story text, passwords, tile graphics
|
original assembly. No SM83 code is decoded at runtime. The game's stack lives
|
||||||
and the music byte code. Small numbers that are really code (coordinates,
|
in emulated memory because the game manipulates return addresses itself.
|
||||||
tile numbers for digits, timings) live in the Rust source as named constants.
|
- `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 |
|
```
|
||||||
|---|---|
|
python3 ../recompile.py "../Daedalian Opus (USA).gb" src/game.rs assets/rom_data.bin
|
||||||
| `src/game/mod.rs` | `Game` state, the top-level flow (title → story → map → puzzle → ...), the per-frame tasks, the clear-time clock |
|
```
|
||||||
| `src/game/puzzle.rs` | the puzzle: picking up, moving, rotating, flipping and placing pieces; the win check; reset; the level-clear screen |
|
|
||||||
| `src/game/map.rs` | the map between levels: walking, castle doors, the bridge, the password bird, the piece that drops from the sky |
|
|
||||||
| `src/game/menus.rs` | title, how to play, password entry, puzzle list |
|
|
||||||
| `src/game/story.rs` | story screens and the ending |
|
|
||||||
| `src/sound.rs` | the game's sound driver: an 8-channel byte-code sequencer (format documented at the top) |
|
|
||||||
| `src/apu.rs` | the Game Boy sound chip, as the instrument the driver plays |
|
|
||||||
| `src/video.rs` | tile graphics, two background maps, 40 sprites, palettes, and the renderer |
|
|
||||||
| `src/rom.rs` | ROM loading and parsing |
|
|
||||||
| `src/input.rs` | buttons, with the game's d-pad auto-repeat |
|
|
||||||
| `src/main.rs` | window (`winit` + `pixels`), audio (`cpal`), frame pacing |
|
|
||||||
| `src/headless.rs` | scripted runs for testing |
|
|
||||||
|
|
||||||
The game code is written the way the original is structured: straight-line
|
|
||||||
code that calls `wait_frame()` whenever it needs the next frame (to animate,
|
|
||||||
or to wait for a button). It runs on its own thread; `wait_frame` sends the
|
|
||||||
finished picture and sound to the host and blocks until the host sends the
|
|
||||||
next frame's buttons.
|
|
||||||
|
|
||||||
The display model (tiles, maps, sprites) is kept because it's what the art is
|
|
||||||
made of, so the game still draws by placing tiles and sprites. Map addresses
|
|
||||||
like `0x9800` remain the way cells are named, since the screen layout scripts
|
|
||||||
in the ROM use them.
|
|
||||||
|
|
||||||
## Faithfulness
|
|
||||||
|
|
||||||
It's checked against the recompiled build in `../rust/` with the headless
|
|
||||||
runner. Same inputs give the same screens (title, every menu, story pages,
|
|
||||||
map walks, levels 1-3 and 36 solved, the ending), and the sound driver's chip
|
|
||||||
register writes match frame for frame. Known differences:
|
|
||||||
|
|
||||||
- Loading a screen takes no time here, where the original spent a few frames
|
|
||||||
copying graphics with the display off, so the port runs a few frames ahead.
|
|
||||||
- The original queued tile updates to the next vblank; here they happen
|
|
||||||
immediately, so some changes appear one frame sooner.
|
|
||||||
- The original's unused "resume music" sound (id $96) points at garbage; it
|
|
||||||
just resumes.
|
|
||||||
|
|
||||||
Original quirks are kept on purpose and marked `(sic)` in the source. For
|
|
||||||
example, undoing a Select flip that doesn't fit uses Start's rule.
|
|
||||||
|
|
||||||
## Headless testing
|
## Headless testing
|
||||||
|
|
||||||
```
|
```
|
||||||
cargo run --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
|
Tokens: `N` run N frames, `BUTTON:N` hold a button N frames, `shot:NAME` save a
|
||||||
right a b start select); `shot:NAME` save a PNG; `probe` print the level and
|
PNG, `peek:ADDR:N` print memory, `goto:YY:XX` steer the puzzle cursor. All
|
||||||
cursor; `goto:YY:XX` steer the puzzle cursor (hex sprite coordinates). Audio
|
audio is written to `OUT_DIR/audio.wav`.
|
||||||
goes to `OUT_DIR/audio.wav`.
|
|
||||||
|
|||||||
+12
-39
@@ -1,34 +1,9 @@
|
|||||||
//! The Game Boy sound chip: two pulse channels (one with sweep), the wave
|
//! Game Boy APU: two pulse channels (one with sweep), the wave channel, and
|
||||||
//! channel and the noise channel, mixed to stereo f32 samples.
|
//! 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
|
||||||
//! This is the instrument the game's music was written for; the game's own
|
//! before every register write, so timing follows the recompiled code.
|
||||||
//! 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.
|
|
||||||
|
|
||||||
/// Clock the chip's timing is expressed in (the Game Boy CPU clock).
|
use crate::machine::CPU_HZ;
|
||||||
pub const CLOCK_HZ: u64 = 4_194_304;
|
|
||||||
const CPU_HZ: u64 = CLOCK_HZ;
|
|
||||||
|
|
||||||
pub const NR10: u16 = 0xFF10;
|
|
||||||
pub const NR11: u16 = 0xFF11;
|
|
||||||
pub const NR12: u16 = 0xFF12;
|
|
||||||
pub const NR13: u16 = 0xFF13;
|
|
||||||
pub const NR14: u16 = 0xFF14;
|
|
||||||
pub const NR21: u16 = 0xFF16;
|
|
||||||
pub const NR22: u16 = 0xFF17;
|
|
||||||
pub const NR23: u16 = 0xFF18;
|
|
||||||
pub const NR24: u16 = 0xFF19;
|
|
||||||
pub const NR30: u16 = 0xFF1A;
|
|
||||||
pub const NR32: u16 = 0xFF1C;
|
|
||||||
pub const NR33: u16 = 0xFF1D;
|
|
||||||
pub const NR34: u16 = 0xFF1E;
|
|
||||||
pub const NR42: u16 = 0xFF21;
|
|
||||||
pub const NR43: u16 = 0xFF22;
|
|
||||||
pub const NR44: u16 = 0xFF23;
|
|
||||||
pub const NR50: u16 = 0xFF24;
|
|
||||||
pub const NR51: u16 = 0xFF25;
|
|
||||||
pub const NR52: u16 = 0xFF26;
|
|
||||||
pub const WAVE_RAM: u16 = 0xFF30;
|
|
||||||
|
|
||||||
const DUTY: [[u8; 8]; 4] = [
|
const DUTY: [[u8; 8]; 4] = [
|
||||||
[0, 0, 0, 0, 0, 0, 0, 1],
|
[0, 0, 0, 0, 0, 0, 0, 1],
|
||||||
@@ -109,8 +84,6 @@ pub struct Apu {
|
|||||||
wave: Wave,
|
wave: Wave,
|
||||||
noise: Noise,
|
noise: Noise,
|
||||||
sample_rate: u32,
|
sample_rate: u32,
|
||||||
/// clock cycles synthesized so far
|
|
||||||
now: u64,
|
|
||||||
samples_made: u64,
|
samples_made: u64,
|
||||||
last_cycle: u64,
|
last_cycle: u64,
|
||||||
seq_cycles: u64,
|
seq_cycles: u64,
|
||||||
@@ -130,7 +103,6 @@ impl Apu {
|
|||||||
wave: Wave::default(),
|
wave: Wave::default(),
|
||||||
noise: Noise::default(),
|
noise: Noise::default(),
|
||||||
sample_rate,
|
sample_rate,
|
||||||
now: 0,
|
|
||||||
samples_made: 0,
|
samples_made: 0,
|
||||||
last_cycle: 0,
|
last_cycle: 0,
|
||||||
seq_cycles: 0,
|
seq_cycles: 0,
|
||||||
@@ -149,7 +121,8 @@ impl Apu {
|
|||||||
self.regs[(addr - 0xFF10) as usize]
|
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 {
|
if addr == 0xFF26 {
|
||||||
return (self.reg(0xFF26) & 0x80)
|
return (self.reg(0xFF26) & 0x80)
|
||||||
| 0x70
|
| 0x70
|
||||||
@@ -161,7 +134,8 @@ impl Apu {
|
|||||||
self.reg(addr)
|
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;
|
let powered = self.reg(0xFF26) & 0x80 != 0;
|
||||||
if !powered && addr != 0xFF26 && addr < 0xFF30 {
|
if !powered && addr != 0xFF26 && addr < 0xFF30 {
|
||||||
return;
|
return;
|
||||||
@@ -303,11 +277,10 @@ impl Apu {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Synthesize the next `cycles` clock cycles of sound into `out`.
|
/// Synthesize samples up to CPU cycle `cycles`.
|
||||||
pub fn run(&mut self, cycles: u64) {
|
pub fn catch_up(&mut self, cycles: u64) {
|
||||||
self.now += cycles;
|
|
||||||
let rate = self.sample_rate as f32;
|
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 {
|
while self.samples_made < target {
|
||||||
self.samples_made += 1;
|
self.samples_made += 1;
|
||||||
let cyc = self.samples_made * CPU_HZ / self.sample_rate as u64;
|
let cyc = self.samples_made * CPU_HZ / self.sample_rate as u64;
|
||||||
|
|||||||
-370
@@ -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
|
//! Script tokens: `N` run N frames; `BUTTON:N` hold a button for N frames
|
||||||
//! (up down left right a b start select); `shot:NAME` save OUT_DIR/NAME.png;
|
//! (up down left right a b start select); `shot:NAME` save OUT_DIR/NAME.png.
|
||||||
//! `probe` print the level and cursor; `goto:YY:XX` steer the puzzle cursor
|
//! All audio is written to OUT_DIR/audio.wav.
|
||||||
//! (hex sprite coordinates). All audio is written to OUT_DIR/audio.wav.
|
|
||||||
|
|
||||||
use crate::game::Frame;
|
use crate::machine::*;
|
||||||
use crate::input;
|
|
||||||
use crate::rom::GameData;
|
|
||||||
use crate::video::{SCREEN_H, SCREEN_W};
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
@@ -84,53 +78,60 @@ fn write_wav(path: &Path, rate: u32, samples: &[f32]) {
|
|||||||
fs::write(path, d).unwrap();
|
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]);
|
let out = Path::new(&args[0]);
|
||||||
fs::create_dir_all(out).unwrap();
|
fs::create_dir_all(out).unwrap();
|
||||||
let rate = 44_100;
|
let rate = 44_100;
|
||||||
let host = crate::start_game(data, rate);
|
let mut m = Machine::new(rom, rate);
|
||||||
let mut audio = Vec::new();
|
let mut audio = Vec::new();
|
||||||
let mut frames = 0u64;
|
let mut frames = 0u64;
|
||||||
let mut frame: Frame = host.frames.recv().expect("game thread");
|
let run = |m: &mut Machine, n: u64, audio: &mut Vec<f32>, frames: &mut u64| {
|
||||||
let mut step = |buttons: u8, n: u64, frame: &mut Frame| {
|
|
||||||
for _ in 0..n {
|
for _ in 0..n {
|
||||||
host.buttons.send(buttons).expect("game thread");
|
m.run_frame();
|
||||||
*frame = host.frames.recv().expect("game thread");
|
audio.append(&mut m.apu.out);
|
||||||
audio.extend_from_slice(&frame.audio);
|
*frames += 1;
|
||||||
frames += 1;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
for tok in &args[1..] {
|
for tok in &args[1..] {
|
||||||
if let Ok(n) = tok.parse::<u64>() {
|
if let Ok(n) = tok.parse::<u64>() {
|
||||||
step(0, n, &mut frame);
|
run(&mut m, n, &mut audio, &mut frames);
|
||||||
} else if let Some(name) = tok.strip_prefix("shot:") {
|
} 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);
|
write_png(&out.join(format!("{name}.png")), SCREEN_W, SCREEN_H, &gray);
|
||||||
println!("{name}.png: {:?}", frame.probe);
|
println!("frame {frames}: {name}.png (pc ${:04X})", m.pc);
|
||||||
} else if tok == "probe" {
|
|
||||||
println!("{:?}", frame.probe);
|
|
||||||
} else if let Some(spec) = tok.strip_prefix("goto:") {
|
} else if let Some(spec) = tok.strip_prefix("goto:") {
|
||||||
|
// steer the puzzle cursor (wPieceY/wPieceX at $C0A1/$C0A2) to a position
|
||||||
let (ty, tx) = spec.split_once(':').unwrap();
|
let (ty, tx) = spec.split_once(':').unwrap();
|
||||||
let (ty, tx) = (u8::from_str_radix(ty, 16).unwrap(), u8::from_str_radix(tx, 16).unwrap());
|
let (ty, tx) = (u8::from_str_radix(ty, 16).unwrap(), u8::from_str_radix(tx, 16).unwrap());
|
||||||
for _ in 0..100 {
|
for _ in 0..100 {
|
||||||
let p = frame.probe;
|
let (cy, cx) = (m.r8(0xC0A1), m.r8(0xC0A2));
|
||||||
let bit = if p.cursor_y < ty { input::DOWN } else if p.cursor_y > ty { input::UP }
|
let bit = if cy < ty { BTN_DOWN } else if cy > ty { BTN_UP }
|
||||||
else if p.cursor_x < tx { input::RIGHT } else if p.cursor_x > tx { input::LEFT } else { break };
|
else if cx < tx { BTN_RIGHT } else if cx > tx { BTN_LEFT } else { break };
|
||||||
step(bit, 1, &mut frame);
|
m.buttons |= bit;
|
||||||
step(0, 5, &mut frame);
|
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(':') {
|
} else if let Some((btn, n)) = tok.split_once(':') {
|
||||||
let bit = match btn {
|
let bit = match btn {
|
||||||
"up" => input::UP, "down" => input::DOWN, "left" => input::LEFT, "right" => input::RIGHT,
|
"up" => BTN_UP, "down" => BTN_DOWN, "left" => BTN_LEFT, "right" => BTN_RIGHT,
|
||||||
"a" => input::A, "b" => input::B, "start" => input::START, "select" => input::SELECT,
|
"a" => BTN_A, "b" => BTN_B, "start" => BTN_START, "select" => BTN_SELECT,
|
||||||
_ => panic!("unknown token {tok}"),
|
_ => 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 {
|
} else {
|
||||||
panic!("unknown token {tok}");
|
panic!("unknown token {tok}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
drop(step);
|
|
||||||
write_wav(&out.join("audio.wav"), rate, &audio);
|
write_wav(&out.join("audio.wav"), rate, &audio);
|
||||||
println!("{frames} frames, {:.1}s of audio", audio.len() as f32 / 2.0 / rate as f32);
|
println!("{frames} frames, {:.1}s of audio", audio.len() as f32 / 2.0 / rate as f32);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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...]`
|
//! Controls: arrows = D-pad, Z = A, X = B, Enter = Start, Right Shift / Backspace = Select
|
||||||
//!
|
|
||||||
//! The game's data is read from the original ROM ("Daedalian Opus (USA).gb"),
|
|
||||||
//! looked for in the current directory and its parent unless given.
|
|
||||||
//!
|
|
||||||
//! Controls: arrows = D-pad, Z = A, X = B, Enter = Start,
|
|
||||||
//! Right Shift / Backspace = Select, Esc = quit.
|
|
||||||
|
|
||||||
mod apu;
|
mod apu;
|
||||||
mod game;
|
mod game;
|
||||||
mod headless;
|
mod headless;
|
||||||
mod input;
|
mod machine;
|
||||||
mod rom;
|
|
||||||
mod sound;
|
|
||||||
mod video;
|
|
||||||
|
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::sync::mpsc::{sync_channel, Receiver, SyncSender};
|
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
@@ -31,10 +20,10 @@ use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
|
|||||||
use winit::keyboard::{KeyCode, PhysicalKey};
|
use winit::keyboard::{KeyCode, PhysicalKey};
|
||||||
use winit::window::{Window, WindowId};
|
use winit::window::{Window, WindowId};
|
||||||
|
|
||||||
use game::{Frame, FRAME_HZ};
|
use machine::*;
|
||||||
use video::{SCREEN_H, SCREEN_W};
|
|
||||||
|
|
||||||
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] = [
|
const SHADES: [[u8; 4]; 4] = [
|
||||||
[0xFF, 0xFF, 0xFF, 0xFF],
|
[0xFF, 0xFF, 0xFF, 0xFF],
|
||||||
[0xAA, 0xAA, 0xAA, 0xFF],
|
[0xAA, 0xAA, 0xAA, 0xFF],
|
||||||
@@ -44,27 +33,11 @@ const SHADES: [[u8; 4]; 4] = [
|
|||||||
|
|
||||||
type AudioQueue = Arc<Mutex<VecDeque<f32>>>;
|
type AudioQueue = Arc<Mutex<VecDeque<f32>>>;
|
||||||
|
|
||||||
/// The host's end of the connection to the game thread.
|
|
||||||
pub struct Host {
|
|
||||||
pub frames: Receiver<Frame>,
|
|
||||||
pub buttons: SyncSender<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn start_game(data: Arc<rom::GameData>, sample_rate: u32) -> Host {
|
|
||||||
let (frame_tx, frame_rx) = sync_channel(1);
|
|
||||||
let (button_tx, button_rx) = sync_channel(1);
|
|
||||||
game::spawn(data, sample_rate, game::Link { frames: frame_tx, buttons: button_rx });
|
|
||||||
Host { frames: frame_rx, buttons: button_tx }
|
|
||||||
}
|
|
||||||
|
|
||||||
struct App {
|
struct App {
|
||||||
window: Option<Arc<Window>>,
|
window: Option<Arc<Window>>,
|
||||||
pixels: Option<Pixels<'static>>,
|
pixels: Option<Pixels<'static>>,
|
||||||
host: Host,
|
machine: Machine,
|
||||||
buttons: u8,
|
|
||||||
screen: Vec<u8>,
|
|
||||||
next_frame: Instant,
|
next_frame: Instant,
|
||||||
frame_time: Duration,
|
|
||||||
audio: AudioQueue,
|
audio: AudioQueue,
|
||||||
audio_rate: u32,
|
audio_rate: u32,
|
||||||
_stream: Option<cpal::Stream>,
|
_stream: Option<cpal::Stream>,
|
||||||
@@ -103,36 +76,42 @@ fn start_audio(queue: AudioQueue) -> Option<(cpal::Stream, u32)> {
|
|||||||
impl App {
|
impl App {
|
||||||
fn key(&mut self, code: KeyCode, pressed: bool) {
|
fn key(&mut self, code: KeyCode, pressed: bool) {
|
||||||
let bit = match code {
|
let bit = match code {
|
||||||
KeyCode::ArrowRight => input::RIGHT,
|
KeyCode::ArrowRight => BTN_RIGHT,
|
||||||
KeyCode::ArrowLeft => input::LEFT,
|
KeyCode::ArrowLeft => BTN_LEFT,
|
||||||
KeyCode::ArrowUp => input::UP,
|
KeyCode::ArrowUp => BTN_UP,
|
||||||
KeyCode::ArrowDown => input::DOWN,
|
KeyCode::ArrowDown => BTN_DOWN,
|
||||||
KeyCode::KeyZ => input::A,
|
KeyCode::KeyZ => BTN_A,
|
||||||
KeyCode::KeyX => input::B,
|
KeyCode::KeyX => BTN_B,
|
||||||
KeyCode::ShiftRight | KeyCode::Backspace => input::SELECT,
|
KeyCode::ShiftRight | KeyCode::Backspace => BTN_SELECT,
|
||||||
KeyCode::Enter => input::START,
|
KeyCode::Enter => BTN_START,
|
||||||
_ => return,
|
_ => return,
|
||||||
};
|
};
|
||||||
if pressed {
|
if pressed {
|
||||||
self.buttons |= bit;
|
self.machine.buttons |= bit;
|
||||||
} else {
|
} else {
|
||||||
self.buttons &= !bit;
|
self.machine.buttons &= !bit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Advance the game one frame. False if the game thread has stopped.
|
fn run_due_frames(&mut self) {
|
||||||
fn step(&mut self) -> bool {
|
let now = Instant::now();
|
||||||
let Ok(frame) = self.host.frames.recv() else { return false };
|
let mut ran = 0;
|
||||||
self.screen = frame.pixels;
|
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();
|
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
|
let max = self.audio_rate as usize / 5 * 2; // keep at most ~200 ms queued
|
||||||
if q.len() > max {
|
if q.len() > max {
|
||||||
let excess = q.len() - max;
|
let excess = q.len() - max;
|
||||||
q.drain(..excess);
|
q.drain(..excess);
|
||||||
}
|
}
|
||||||
drop(q);
|
|
||||||
self.host.buttons.send(self.buttons).is_ok()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +121,7 @@ impl ApplicationHandler for App {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let attrs = Window::default_attributes()
|
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));
|
.with_inner_size(LogicalSize::new(SCREEN_W as f64 * 4.0, SCREEN_H as f64 * 4.0));
|
||||||
let window = Arc::new(event_loop.create_window(attrs).expect("create window"));
|
let window = Arc::new(event_loop.create_window(attrs).expect("create window"));
|
||||||
let size = window.inner_size();
|
let size = window.inner_size();
|
||||||
@@ -171,7 +150,7 @@ impl ApplicationHandler for App {
|
|||||||
}
|
}
|
||||||
WindowEvent::RedrawRequested => {
|
WindowEvent::RedrawRequested => {
|
||||||
if let Some(p) = &mut self.pixels {
|
if let Some(p) = &mut self.pixels {
|
||||||
for (dst, &shade) in p.frame_mut().chunks_exact_mut(4).zip(&self.screen) {
|
for (dst, &shade) in p.frame_mut().chunks_exact_mut(4).zip(&self.machine.framebuffer) {
|
||||||
dst.copy_from_slice(&SHADES[shade as usize]);
|
dst.copy_from_slice(&SHADES[shade as usize]);
|
||||||
}
|
}
|
||||||
if let Err(e) = p.render() {
|
if let Err(e) = p.render() {
|
||||||
@@ -188,20 +167,7 @@ impl ApplicationHandler for App {
|
|||||||
if self.window.is_none() {
|
if self.window.is_none() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let now = Instant::now();
|
self.run_due_frames();
|
||||||
let mut ran = 0;
|
|
||||||
while self.next_frame <= now && ran < 4 {
|
|
||||||
if !self.step() {
|
|
||||||
eprintln!("the game stopped");
|
|
||||||
event_loop.exit();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
self.next_frame += self.frame_time;
|
|
||||||
ran += 1;
|
|
||||||
}
|
|
||||||
if self.next_frame < now {
|
|
||||||
self.next_frame = now; // fell behind; don't try to catch up forever
|
|
||||||
}
|
|
||||||
if let Some(w) = &self.window {
|
if let Some(w) = &self.window {
|
||||||
w.request_redraw();
|
w.request_redraw();
|
||||||
}
|
}
|
||||||
@@ -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() {
|
fn main() {
|
||||||
let mut args: Vec<String> = std::env::args().skip(1).collect();
|
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||||
let mut rom_path = None;
|
|
||||||
if let Some(i) = args.iter().position(|a| a == "--rom") {
|
|
||||||
if i + 1 < args.len() {
|
|
||||||
rom_path = Some(args.remove(i + 1));
|
|
||||||
}
|
|
||||||
args.remove(i);
|
|
||||||
}
|
|
||||||
let Some(rom_path) = find_rom(rom_path) else {
|
|
||||||
eprintln!("can't find \"{ROM_NAME}\"; pass its path with --rom");
|
|
||||||
std::process::exit(1);
|
|
||||||
};
|
|
||||||
let rom = match rom::Rom::load(&rom_path) {
|
|
||||||
Ok(r) => r,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("{e}");
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let data = Arc::new(rom::GameData::parse(rom));
|
|
||||||
|
|
||||||
if args.first().map(|s| s.as_str()) == Some("--headless") {
|
if args.first().map(|s| s.as_str()) == Some("--headless") {
|
||||||
headless::run(data, &args[1..]);
|
headless::run(ROM_DATA, &args[1..]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let audio: AudioQueue = Arc::new(Mutex::new(VecDeque::new()));
|
let audio: AudioQueue = Arc::new(Mutex::new(VecDeque::new()));
|
||||||
let (stream, rate) = match start_audio(audio.clone()) {
|
let (stream, rate) = match start_audio(audio.clone()) {
|
||||||
Some((s, r)) => (Some(s), r),
|
Some((s, r)) => (Some(s), r),
|
||||||
@@ -254,11 +192,8 @@ fn main() {
|
|||||||
let mut app = App {
|
let mut app = App {
|
||||||
window: None,
|
window: None,
|
||||||
pixels: None,
|
pixels: None,
|
||||||
host: start_game(data, rate),
|
machine: Machine::new(ROM_DATA, rate),
|
||||||
buttons: 0,
|
|
||||||
screen: vec![0; SCREEN_W * SCREEN_H],
|
|
||||||
next_frame: Instant::now(),
|
next_frame: Instant::now(),
|
||||||
frame_time: Duration::from_secs_f64(1.0 / FRAME_HZ),
|
|
||||||
audio,
|
audio,
|
||||||
audio_rate: rate,
|
audio_rate: rate,
|
||||||
_stream: stream,
|
_stream: stream,
|
||||||
|
|||||||
-367
@@ -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