Initial commit

This commit is contained in:
2026-10-31 18:12:15 -05:00
commit 623e4aeda7
16 changed files with 6387 additions and 0 deletions
+388
View File
@@ -0,0 +1,388 @@
//! The Game Boy sound chip: two pulse channels (one with sweep), the wave
//! channel and the noise channel, mixed to stereo f32 samples.
//!
//! This is the instrument the game's music was written for; the game's own
//! sound driver (`sound.rs`) plays it by setting its registers (NR10-NR52 and
//! wave RAM, addressed $FF10-$FF3F as on the hardware) once per frame.
/// Clock the chip's timing is expressed in (the Game Boy CPU clock).
pub const CLOCK_HZ: u64 = 4_194_304;
const CPU_HZ: u64 = CLOCK_HZ;
pub const NR10: u16 = 0xFF10;
pub const NR11: u16 = 0xFF11;
pub const NR12: u16 = 0xFF12;
pub const NR13: u16 = 0xFF13;
pub const NR14: u16 = 0xFF14;
pub const NR21: u16 = 0xFF16;
pub const NR22: u16 = 0xFF17;
pub const NR23: u16 = 0xFF18;
pub const NR24: u16 = 0xFF19;
pub const NR30: u16 = 0xFF1A;
pub const NR32: u16 = 0xFF1C;
pub const NR33: u16 = 0xFF1D;
pub const NR34: u16 = 0xFF1E;
pub const NR42: u16 = 0xFF21;
pub const NR43: u16 = 0xFF22;
pub const NR44: u16 = 0xFF23;
pub const NR50: u16 = 0xFF24;
pub const NR51: u16 = 0xFF25;
pub const NR52: u16 = 0xFF26;
pub const WAVE_RAM: u16 = 0xFF30;
const DUTY: [[u8; 8]; 4] = [
[0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 1, 1, 1],
[0, 1, 1, 1, 1, 1, 1, 0],
];
#[derive(Default)]
struct Envelope {
volume: u8,
increase: bool,
period: u8,
timer: u8,
}
impl Envelope {
fn load(&mut self, reg: u8) {
self.volume = reg >> 4;
self.increase = reg & 0x08 != 0;
self.period = reg & 0x07;
self.timer = self.period;
}
fn clock(&mut self) {
if self.period == 0 {
return;
}
if self.timer > 1 {
self.timer -= 1;
return;
}
self.timer = self.period;
if self.increase && self.volume < 15 {
self.volume += 1;
} else if !self.increase && self.volume > 0 {
self.volume -= 1;
}
}
}
#[derive(Default)]
struct Pulse {
on: bool,
length: u16,
length_enable: bool,
env: Envelope,
freq: u16,
phase: f32,
// sweep (channel 1 only)
sweep_timer: u8,
sweep_enabled: bool,
shadow_freq: u16,
}
#[derive(Default)]
struct Wave {
on: bool,
length: u16,
length_enable: bool,
freq: u16,
phase: f32,
}
#[derive(Default)]
struct Noise {
on: bool,
length: u16,
length_enable: bool,
env: Envelope,
lfsr: u16,
acc: f32,
}
pub struct Apu {
regs: [u8; 0x30], // $FF10-$FF3F
sq1: Pulse,
sq2: Pulse,
wave: Wave,
noise: Noise,
sample_rate: u32,
/// clock cycles synthesized so far
now: u64,
samples_made: u64,
last_cycle: u64,
seq_cycles: u64,
seq_step: u8,
hp_l: (f32, f32),
hp_r: (f32, f32),
/// interleaved stereo samples waiting to be played
pub out: Vec<f32>,
}
impl Apu {
pub fn new(sample_rate: u32) -> Self {
let mut a = Apu {
regs: [0; 0x30],
sq1: Pulse::default(),
sq2: Pulse::default(),
wave: Wave::default(),
noise: Noise::default(),
sample_rate,
now: 0,
samples_made: 0,
last_cycle: 0,
seq_cycles: 0,
seq_step: 0,
hp_l: (0.0, 0.0),
hp_r: (0.0, 0.0),
out: Vec::new(),
};
a.regs[0x16] = 0xF1; // NR52: on
a.regs[0x14] = 0x77; // NR50
a.regs[0x15] = 0xF3; // NR51
a
}
fn reg(&self, addr: u16) -> u8 {
self.regs[(addr - 0xFF10) as usize]
}
pub fn read(&self, addr: u16) -> u8 {
if addr == 0xFF26 {
return (self.reg(0xFF26) & 0x80)
| 0x70
| self.sq1.on as u8
| (self.sq2.on as u8) << 1
| (self.wave.on as u8) << 2
| (self.noise.on as u8) << 3;
}
self.reg(addr)
}
pub fn write(&mut self, addr: u16, v: u8) {
let powered = self.reg(0xFF26) & 0x80 != 0;
if !powered && addr != 0xFF26 && addr < 0xFF30 {
return;
}
self.regs[(addr - 0xFF10) as usize] = v;
match addr {
0xFF11 => self.sq1.length = 64 - (v & 0x3F) as u16,
0xFF12 => if v & 0xF8 == 0 { self.sq1.on = false },
0xFF13 => self.sq1.freq = (self.sq1.freq & 0x700) | v as u16,
0xFF14 => {
self.sq1.freq = (self.sq1.freq & 0xFF) | ((v as u16 & 7) << 8);
self.sq1.length_enable = v & 0x40 != 0;
if v & 0x80 != 0 {
let (env, nr10) = (self.reg(0xFF12), self.reg(0xFF10));
Self::trigger_pulse(&mut self.sq1, env);
self.sq1.shadow_freq = self.sq1.freq;
let period = (nr10 >> 4) & 7;
self.sq1.sweep_timer = if period == 0 { 8 } else { period };
self.sq1.sweep_enabled = period != 0 || nr10 & 7 != 0;
}
}
0xFF16 => self.sq2.length = 64 - (v & 0x3F) as u16,
0xFF17 => if v & 0xF8 == 0 { self.sq2.on = false },
0xFF18 => self.sq2.freq = (self.sq2.freq & 0x700) | v as u16,
0xFF19 => {
self.sq2.freq = (self.sq2.freq & 0xFF) | ((v as u16 & 7) << 8);
self.sq2.length_enable = v & 0x40 != 0;
if v & 0x80 != 0 {
let env = self.reg(0xFF17);
Self::trigger_pulse(&mut self.sq2, env);
}
}
0xFF1A => if v & 0x80 == 0 { self.wave.on = false },
0xFF1B => self.wave.length = 256 - v as u16,
0xFF1D => self.wave.freq = (self.wave.freq & 0x700) | v as u16,
0xFF1E => {
self.wave.freq = (self.wave.freq & 0xFF) | ((v as u16 & 7) << 8);
self.wave.length_enable = v & 0x40 != 0;
if v & 0x80 != 0 {
self.wave.on = self.reg(0xFF1A) & 0x80 != 0;
if self.wave.length == 0 {
self.wave.length = 256;
}
self.wave.phase = 0.0;
}
}
0xFF20 => self.noise.length = 64 - (v & 0x3F) as u16,
0xFF21 => if v & 0xF8 == 0 { self.noise.on = false },
0xFF23 => {
self.noise.length_enable = v & 0x40 != 0;
if v & 0x80 != 0 {
let env = self.reg(0xFF21);
self.noise.on = env & 0xF8 != 0;
if self.noise.length == 0 {
self.noise.length = 64;
}
self.noise.env.load(env);
self.noise.lfsr = 0x7FFF;
}
}
0xFF26 => {
if v & 0x80 == 0 {
for r in &mut self.regs[..0x16] {
*r = 0;
}
self.sq1.on = false;
self.sq2.on = false;
self.wave.on = false;
self.noise.on = false;
}
}
_ => {}
}
}
fn trigger_pulse(p: &mut Pulse, env: u8) {
p.on = env & 0xF8 != 0;
if p.length == 0 {
p.length = 64;
}
p.env.load(env);
}
fn sweep_calc(&mut self) -> u16 {
let nr10 = self.reg(0xFF10);
let delta = self.sq1.shadow_freq >> (nr10 & 7);
let f = if nr10 & 0x08 != 0 {
self.sq1.shadow_freq.wrapping_sub(delta)
} else {
self.sq1.shadow_freq + delta
};
if f > 2047 {
self.sq1.on = false;
}
f
}
/// 512 Hz frame sequencer: lengths, sweep, envelopes.
fn sequencer_step(&mut self) {
let step = self.seq_step;
self.seq_step = (self.seq_step + 1) & 7;
if step % 2 == 0 {
for (on, len, en) in [
(&mut self.sq1.on, &mut self.sq1.length, self.sq1.length_enable),
(&mut self.sq2.on, &mut self.sq2.length, self.sq2.length_enable),
(&mut self.wave.on, &mut self.wave.length, self.wave.length_enable),
(&mut self.noise.on, &mut self.noise.length, self.noise.length_enable),
] {
if en && *len > 0 {
*len -= 1;
if *len == 0 {
*on = false;
}
}
}
}
if step == 2 || step == 6 {
let nr10 = self.reg(0xFF10);
let period = (nr10 >> 4) & 7;
if self.sq1.sweep_timer > 0 {
self.sq1.sweep_timer -= 1;
}
if self.sq1.sweep_timer == 0 {
self.sq1.sweep_timer = if period == 0 { 8 } else { period };
if self.sq1.sweep_enabled && period != 0 {
let f = self.sweep_calc();
if f <= 2047 && nr10 & 7 != 0 {
self.sq1.shadow_freq = f;
self.sq1.freq = f;
self.sweep_calc();
}
}
}
}
if step == 7 {
self.sq1.env.clock();
self.sq2.env.clock();
self.noise.env.clock();
}
}
/// Synthesize the next `cycles` clock cycles of sound into `out`.
pub fn run(&mut self, cycles: u64) {
self.now += cycles;
let rate = self.sample_rate as f32;
let target = self.now * self.sample_rate as u64 / CPU_HZ;
while self.samples_made < target {
self.samples_made += 1;
let cyc = self.samples_made * CPU_HZ / self.sample_rate as u64;
// frame sequencer runs every 8192 CPU cycles
self.seq_cycles += cyc - self.last_cycle;
self.last_cycle = cyc;
while self.seq_cycles >= 8192 {
self.seq_cycles -= 8192;
self.sequencer_step();
}
let mut ch = [0.0f32; 4];
let duty1 = (self.reg(0xFF11) >> 6) as usize;
let duty2 = (self.reg(0xFF16) >> 6) as usize;
for (i, (p, duty)) in [(&mut self.sq1, duty1), (&mut self.sq2, duty2)].into_iter().enumerate() {
let hz = 131072.0 / (2048 - p.freq) as f32;
p.phase = (p.phase + hz * 8.0 / rate) % 8.0;
if p.on {
let high = DUTY[duty][p.phase as usize] != 0;
ch[i] = p.env.volume as f32 / 15.0 * if high { 1.0 } else { -1.0 };
}
}
{
let hz = 65536.0 / (2048 - self.wave.freq) as f32;
self.wave.phase = (self.wave.phase + hz * 32.0 / rate) % 32.0;
let shift = match (self.reg(0xFF1C) >> 5) & 3 {
0 => 8,
n => n - 1,
};
if self.wave.on && shift < 8 {
let idx = self.wave.phase as usize;
let byte = self.regs[0x20 + idx / 2];
let nib = if idx % 2 == 0 { byte >> 4 } else { byte & 0x0F };
ch[2] = (nib >> shift) as f32 / 7.5 - 1.0;
}
}
{
let nr43 = self.reg(0xFF22);
let div = match nr43 & 7 { 0 => 0.5, d => d as f32 };
let hz = 524288.0 / div / (1u32 << ((nr43 >> 4) + 1)) as f32;
self.noise.acc += hz / rate;
while self.noise.acc >= 1.0 {
self.noise.acc -= 1.0;
let x = (self.noise.lfsr ^ (self.noise.lfsr >> 1)) & 1;
self.noise.lfsr = (self.noise.lfsr >> 1) | (x << 14);
if nr43 & 0x08 != 0 {
self.noise.lfsr = (self.noise.lfsr & !0x40) | (x << 6);
}
}
if self.noise.on {
let high = self.noise.lfsr & 1 == 0;
ch[3] = self.noise.env.volume as f32 / 15.0 * if high { 1.0 } else { -1.0 };
}
}
let nr50 = self.reg(0xFF24);
let nr51 = self.reg(0xFF25);
let (mut l, mut r) = (0.0, 0.0);
for (i, v) in ch.iter().enumerate() {
if nr51 & (0x10 << i) != 0 { l += v; }
if nr51 & (0x01 << i) != 0 { r += v; }
}
l *= ((nr50 >> 4) & 7) as f32 + 1.0;
r *= (nr50 & 7) as f32 + 1.0;
let scale = 0.25 / 8.0;
let l = Self::highpass(&mut self.hp_l, l * scale);
let r = Self::highpass(&mut self.hp_r, r * scale);
self.out.push(l);
self.out.push(r);
}
}
fn highpass(state: &mut (f32, f32), x: f32) -> f32 {
let y = x - state.0 + 0.996 * state.1;
*state = (x, y);
y
}
}
+370
View File
@@ -0,0 +1,370 @@
//! The map screen between levels: the hero walks from the last castle to the
//! next one. On the way a bridge may build itself, a bird flies over writing
//! the level's password, and on some levels a new piece drops from the sky.
use super::*;
use crate::input::*;
use crate::rom::addr::*;
use crate::sound::*;
use crate::video::map_cell;
const LAST_LEVEL: u8 = 35;
// hero shapes
const FACE_UP: u8 = 0x41;
const STAND_RIGHT: u8 = 0x42;
const STAND_LEFT: u8 = 0x43;
const WALK_RIGHT: u8 = 0x44;
const WALK_LEFT: u8 = 0x45;
const LEAVE_CASTLE: u8 = 0x48;
const ENTER_CASTLE: u8 = 0x4C;
const HERO_Y: u8 = 0x6C;
const STEP: u8 = 4;
const STEP_FRAMES: u8 = 11;
/// where the bridge starts building, and where a new piece drops
const BRIDGE_TRIGGER_X: u8 = 0x28;
const PIECE_DROP_X: u8 = 0x50;
const BRIDGE_AT: u16 = map_cell(MAP0, 6, 13);
const BRIDGE_TILES: [u8; 8] = [0x30, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x32];
/// The bird flies in a 16-bit fixed-point x position; it writes one letter
/// at each of these x positions.
const MESSENGER_LETTER_X: [u8; 4] = [0x44, 0x4C, 0x54, 0x5C];
const MESSENGER_DONE_X: u8 = 0xC0;
const PASSWORD_AT: u16 = map_cell(MAP0, 8, 3);
/// tile = ASCII + this
const PASSWORD_TILE_OFFSET: u8 = 0x9D;
#[derive(Clone, Copy, Default, PartialEq)]
pub(super) enum BridgeState {
#[default]
Idle,
Triggered,
Building,
}
#[derive(Clone, Copy, Default)]
pub(super) struct Bridge {
pub state: BridgeState,
built: u8,
delay: u8,
}
#[derive(Clone, Copy, Default, PartialEq)]
pub(super) enum MessengerState {
#[default]
Idle,
Launch,
Flying,
}
#[derive(Clone, Copy, Default)]
pub(super) struct Messenger {
pub state: MessengerState,
x: u16,
}
impl Game {
pub(super) fn map_screen(&mut self) {
let data = self.data.clone();
let level = &data.levels[self.level as usize];
self.clear_actors();
if level.new_piece != 0 {
self.s.actors[MARKER] = Actor::at(0x68, 0x56, 0x56);
}
self.video.load(0x8000, data.gfx(GFX_SPRITES, 0x800));
self.video.load(0x8800, data.gfx(GFX_FONT, 0x800));
self.video.load(0x9000, data.gfx(GFX_MAP_TERRAIN, 0x800));
self.video.fill_maps(0x33);
let scripts = &data.scripts;
self.video.draw(&scripts.map);
let scene = match self.level {
0 => 0,
LAST_LEVEL => 2,
_ => 1,
};
self.video.draw(&scripts.map_scenes[scene]);
self.video.draw(&scripts.map_castles[level.map_castle as usize]);
self.video.draw(&scripts.map_next_castles[level.map_next_castle as usize]);
if self.level != 0 && self.level != LAST_LEVEL {
// castle 12 is special and has no number on it
if self.level != 12 {
self.draw_castle_number(1, self.level);
}
if self.level + 1 != 12 {
self.draw_castle_number(17, self.level + 1);
}
}
self.set_palettes(PALETTE);
self.play(MUSIC_MAP);
self.show(MAP0);
}
/// Two-digit number, two tiles tall, at row 7.
fn draw_castle_number(&mut self, col: u16, n: u8) {
let (tens, ones) = (n / 10, n % 10);
for (c, tile) in [(col, 0x6C + 2 * tens), (col + 1, 0x58 + 2 * ones)] {
self.video.poke(map_cell(MAP0, c, 7), tile);
self.video.poke(map_cell(MAP0, c, 8), tile + 1);
}
}
/// Walk around until the hero goes into a castle.
pub(super) fn map_walk(&mut self) {
match self.level {
0 => {
self.s.actors[HERO] = Actor::at(HERO_Y, 0x38, STAND_RIGHT);
(self.s.walk_left, self.s.walk_right) = (0x38, 0x78);
}
LAST_LEVEL => {
(self.s.walk_left, self.s.walk_right) = (0x08, 0x64);
self.leave_castle(Actor::at(HERO_Y, 0x08, LEAVE_CASTLE));
self.s.messenger.state = MessengerState::Launch;
}
_ => {
(self.s.walk_left, self.s.walk_right) = (0x10, 0x90);
self.leave_castle(Actor::at(HERO_Y, 0x10, LEAVE_CASTLE));
self.s.messenger.state = MessengerState::Launch;
}
}
loop {
self.wait_frame();
if self.try_enter_castle() {
return;
}
self.walk();
}
}
/// Up at a castle door: forward to the next level, or back to the
/// previous one (when facing left at the left castle).
fn try_enter_castle(&mut self) -> bool {
if self.joy.pressed & UP == 0 {
return false;
}
let hero = self.s.actors[HERO];
if hero.x == self.s.walk_left {
if self.level == 0 || hero.shape == STAND_RIGHT {
return false;
}
self.enter_castle();
self.level -= 1;
return true;
}
if hero.x == self.s.walk_right {
if self.level == LAST_LEVEL {
self.climb_into_tower();
} else {
self.enter_castle();
}
return true;
}
false
}
/// Walks while the d-pad is held (this blocks, a step at a time).
fn walk(&mut self) {
if self.joy.held & RIGHT != 0 {
self.s.actors[HERO].shape = WALK_RIGHT;
loop {
self.step(self.s.walk_right, STEP);
if self.s.actors[HERO].x == BRIDGE_TRIGGER_X {
self.s.bridge.state = BridgeState::Triggered;
}
if self.joy.held & RIGHT == 0 {
break;
}
}
self.s.actors[HERO].shape = STAND_RIGHT;
} else if self.joy.held & LEFT != 0 {
self.s.actors[HERO].shape = WALK_LEFT;
loop {
self.step(self.s.walk_left, STEP.wrapping_neg());
if self.joy.held & LEFT == 0 {
break;
}
}
self.s.actors[HERO].shape = STAND_LEFT;
}
}
fn step(&mut self, limit: u8, dx: u8) {
self.play(SFX_STEP);
self.wait_frames(STEP_FRAMES);
let hero = &mut self.s.actors[HERO];
hero.shape ^= 2; // swing legs
if hero.x == limit {
return;
}
hero.x = hero.x.wrapping_add(dx);
self.check_piece_drop();
}
/// Two frames of animation, four times each, then the next pair.
fn animate_pair(&mut self, times: u8) {
for _ in 0..times {
self.play(SFX_STEP);
self.wait_frames(12);
self.s.actors[HERO].shape ^= 1;
}
}
fn leave_castle(&mut self, hero: Actor) {
self.wait_frames(30);
self.s.actors[HERO] = hero;
self.animate_pair(4);
self.s.actors[HERO].shape += 2;
self.animate_pair(4);
self.s.actors[HERO].shape = STAND_RIGHT;
}
fn enter_castle(&mut self) {
self.s.actors[HERO].shape = ENTER_CASTLE;
self.animate_pair(4);
self.s.actors[HERO].shape += 2;
self.animate_pair(4);
self.clear_actors();
self.s.messenger.state = MessengerState::Idle;
self.wait_frames(30);
}
/// The last level's door is up a wall.
fn climb_into_tower(&mut self) {
self.s.actors[HERO].shape = ENTER_CASTLE;
self.climb();
self.s.actors[HERO].shape += 2;
self.s.actors[HERO].y += 2;
self.climb();
self.s.actors[HERO].visible = false;
self.wait_frames(30);
}
fn climb(&mut self) {
for _ in 0..6 {
self.play(SFX_STEP);
self.wait_frames(12);
let hero = &mut self.s.actors[HERO];
hero.shape ^= 1;
hero.y -= 3;
}
}
/// Halfway along, some levels drop a new piece from the sky.
fn check_piece_drop(&mut self) {
if self.s.piece_drop_done || self.s.actors[HERO].x != PIECE_DROP_X {
return;
}
let piece = self.data.levels[self.level as usize].new_piece;
if piece != 0 {
self.drop_piece(piece);
}
self.s.piece_drop_done = true;
}
fn drop_piece(&mut self, piece: u8) {
let facing = self.s.actors[HERO].shape;
self.s.actors[HERO].shape = FACE_UP;
self.s.actors[CURSOR] = Actor::at(0xE0, PIECE_DROP_X, 0);
self.s.held = piece;
self.play(SFX_CONFIRM);
self.wait_frames(30);
self.s.actors[MARKER].visible = false;
loop {
// tumble down
self.play(SFX_MOVE_PIECE);
self.wait_frames(4);
self.s.held = (self.s.held & 0xFC) | (self.s.held.wrapping_add(1) & 3);
let p = &mut self.s.actors[CURSOR];
p.y = p.y.wrapping_add(4);
if p.y == 0x50 {
break;
}
}
self.play(SFX_PIECE_LANDS);
for _ in 0..2 {
self.set_palettes(0xE4); // flash
self.wait_frames(10);
self.set_palettes(PALETTE);
self.wait_frames(10);
}
self.wait_frames(30);
self.s.actors[CURSOR].visible = false;
self.play(JINGLE_LEVEL_CLEAR);
for _ in 0..2 {
self.s.actors[HERO].shape = STAND_RIGHT;
self.wait_frames(20);
self.s.actors[HERO].shape = FACE_UP;
self.wait_frames(20);
}
self.s.actors[HERO].shape = facing;
}
// ---- per-frame map animations ----
pub(super) fn update_bridge(&mut self) {
let b = &mut self.s.bridge;
match b.state {
BridgeState::Idle => {}
BridgeState::Triggered => {
// the first and last maps have no river, and it only builds once
if self.level == 0 || self.level == LAST_LEVEL || b.built != 0 {
b.state = BridgeState::Idle;
} else {
b.state = BridgeState::Building;
b.delay = 1;
self.sound.play(SFX_BRIDGE);
}
}
BridgeState::Building => {
b.delay -= 1;
if b.delay != 0 {
return;
}
b.delay = 5;
let i = b.built;
self.video.poke(BRIDGE_AT + i as u16, BRIDGE_TILES[i as usize]);
if i + 1 == BRIDGE_TILES.len() as u8 {
b.state = BridgeState::Idle;
} else {
b.built += 1;
}
}
}
}
pub(super) fn update_marker_blink(&mut self) {
self.s.marker_blink = self.s.marker_blink.wrapping_add(1);
if self.s.marker_blink & 0x1F == 0 {
self.s.actors[MARKER].shape ^= 1;
}
}
pub(super) fn update_messenger(&mut self) {
let m = &mut self.s.messenger;
match m.state {
MessengerState::Idle => {}
MessengerState::Launch => {
m.state = MessengerState::Flying;
m.x = 0xF000;
self.s.actors[MESSENGER] = Actor::at(0x24, 0xE0, 0x58);
}
MessengerState::Flying => {
m.x = m.x.wrapping_add(0x80);
let x = (m.x >> 8) as u8;
let bird = &mut self.s.actors[MESSENGER];
bird.x = x;
if let Some(i) = MESSENGER_LETTER_X.iter().position(|&lx| lx == x) {
let letter = self.data.passwords[self.level as usize - 1][i];
self.video.poke(PASSWORD_AT + i as u16, letter.wrapping_add(PASSWORD_TILE_OFFSET));
self.sound.play(SFX_PASSWORD_LETTER);
} else if x == MESSENGER_DONE_X {
m.state = MessengerState::Idle;
} else {
bird.shape ^= (x >> 4) & 1; // flap
}
}
}
}
}
+287
View File
@@ -0,0 +1,287 @@
//! Title screen, how-to-play, password entry and the puzzle list.
use super::*;
use crate::input::*;
use crate::rom::addr::*;
use crate::sound::*;
use crate::video::{MAP0, MAP1};
pub enum TitleChoice {
Start,
Password,
HowToPlay,
}
/// Title menu arrow rows.
const TITLE_START_Y: u8 = 0x60;
const TITLE_PASSWORD_Y: u8 = 0x70;
/// Password screen: a 7x4 grid of letters A-Z with a cursor.
const LETTER_TOP: u8 = 0x40;
const LETTER_BOTTOM: u8 = 0x70;
const LETTER_LEFT: u8 = 0x30;
const LETTER_RIGHT: u8 = 0x90;
const PASSWORD_ENTRY: u16 = 0x9868; // where the typed letters appear
const BLANK_LETTER: u8 = 0x7D;
const FIRST_LETTER_TILE: u8 = 0x5E; // 'A'
/// Puzzle list: two pages (maps 0 and 1) of 6 rows x 3 columns.
const LIST_TOP: u8 = 0x30;
const LIST_BOTTOM: u8 = 0x80;
const LIST_LEFT: u8 = 0x10;
const LIST_RIGHT: u8 = 0x70;
const LIST_PAGE: u8 = 18;
const PUZZLE_LIST_LEVEL: u8 = 36;
impl Game {
pub(super) fn title_screen(&mut self) -> TitleChoice {
let data = self.data.clone();
self.video.load(0x8000, data.gfx(GFX_SPRITES, 0x1800));
self.clear_actors();
self.s.actors[MENU_CURSOR] = Actor::at(TITLE_START_Y, 0x38, 0x55);
// map 0 is the title; map 1 is the same without "PUSH START"
self.video.fill_maps(0xEB);
self.video.draw(&data.scripts.title);
self.video.copy_map(MAP0, MAP1);
self.video.draw(&data.scripts.title_alt);
self.set_palettes(PALETTE);
self.play(MUSIC_TITLE);
self.show(MAP0);
self.joy.held = 0xFF; // ignore buttons already down
let mut blink: u8 = 0x60;
loop {
self.wait_frame();
let p = self.joy.pressed;
let arrow = &mut self.s.actors[MENU_CURSOR].y;
if p & A != 0 {
return TitleChoice::HowToPlay;
} else if p & SELECT != 0 {
*arrow = if *arrow == TITLE_START_Y { TITLE_PASSWORD_Y } else { TITLE_START_Y };
} else if p & START != 0 {
self.play(SFX_CONFIRM);
return if self.s.actors[MENU_CURSOR].y == TITLE_PASSWORD_Y {
TitleChoice::Password
} else {
TitleChoice::Start
};
} else if p & UP != 0 {
*arrow = TITLE_START_Y;
} else if p & DOWN != 0 {
*arrow = TITLE_PASSWORD_Y;
} else {
// flash "PUSH START": 32 frames on, 32 off
blink = blink.wrapping_add(1);
if blink == 0 {
blink = 0x60;
self.show(MAP0);
} else if blink == 0x80 {
blink = 0xE0;
self.show(MAP1);
}
continue;
}
self.play(SFX_MENU_MOVE);
}
}
pub(super) fn how_to_play(&mut self) {
let data = self.data.clone();
self.video.load(0x8800, data.gfx(GFX_HOW_TO_PLAY, 0x800));
self.video.load(0x9000, data.gfx(GFX_FONT, 0x800));
self.video.fill_maps(0x7F);
self.video.draw(&data.scripts.how_to_play);
self.clear_actors();
self.set_palettes(PALETTE);
self.show(MAP0);
self.play(MUSIC_MAP);
loop {
self.wait_frame();
if self.joy.pressed & (A | START) != 0 {
return;
}
}
}
/// Sets `self.level` from a valid password.
pub(super) fn password_screen(&mut self) {
let data = self.data.clone();
self.load_menu_tiles();
self.video.fill_maps(0x7F);
self.clear_actors();
self.video.draw(&data.scripts.password);
self.set_palettes(PALETTE);
self.play(MUSIC_TITLE);
self.show(MAP0);
self.wait_frame();
loop {
let mut entry = [BLANK_LETTER; 4];
let mut len = 0;
self.s.actors[HERO] = Actor::at(LETTER_TOP, LETTER_LEFT, 0x53);
loop {
for (i, &t) in entry.iter().enumerate() {
self.video.poke(PASSWORD_ENTRY + i as u16, t);
}
self.wait_frame();
if len == 4 {
self.wait_frames(30);
if let Some(level) = self.check_password(&entry) {
self.level = level;
return;
}
self.play(SFX_ERROR);
break;
}
self.move_letter_cursor();
let cursor = self.s.actors[HERO];
if self.joy.pressed & A != 0 {
entry[len] = letter_at(cursor.y, cursor.x).wrapping_add(FIRST_LETTER_TILE);
len += 1;
} else if self.joy.pressed & B != 0 && len > 0 {
len -= 1;
entry[len] = BLANK_LETTER;
}
}
}
}
fn load_menu_tiles(&mut self) {
let font = self.data.clone();
self.video.load(0x8000, font.gfx(GFX_FONT, 0x800));
self.video.load(0x9000, font.gfx(GFX_FONT, 0x800));
}
fn move_letter_cursor(&mut self) {
let r = self.joy.repeat;
let Actor { mut y, mut x, .. } = self.s.actors[HERO];
let mut moved = 0;
if r & RIGHT != 0 {
if x != LETTER_RIGHT {
x += 0x10;
moved += 1;
if letter_at(y, x) >= 26 {
y = 0x60; // off the end of the last row: go up one
}
}
} else if r & LEFT != 0 && x != LETTER_LEFT {
x -= 0x10;
moved += 1;
}
if r & UP != 0 {
if y != LETTER_TOP {
y -= 0x10;
moved += 1;
}
} else if r & DOWN != 0 && y != LETTER_BOTTOM {
y += 0x10;
moved += 1;
if letter_at(y, x) >= 26 {
x = 0x70; // 'Z'
}
}
for _ in 0..moved {
self.play(SFX_MENU_MOVE); // once per axis moved
}
let c = &mut self.s.actors[HERO];
(c.y, c.x) = (y, x);
}
fn check_password(&self, entry: &[u8; 4]) -> Option<u8> {
let typed = entry.map(|t| t.wrapping_sub(FIRST_LETTER_TILE).wrapping_add(b'A'));
self.data.passwords.iter().position(|p| *p == typed).map(|i| i as u8 + 1)
}
/// Level select, reached with the password after the last level.
pub(super) fn puzzle_list(&mut self) {
if self.level != PUZZLE_LIST_LEVEL {
return;
}
let data = self.data.clone();
self.load_menu_tiles();
self.video.fill_maps(0x7F);
self.clear_actors();
self.video.draw(&data.scripts.puzzle_list);
self.set_palettes(PALETTE);
self.play(MUSIC_TITLE);
self.show(MAP0);
self.s.actors[MENU_CURSOR] = Actor::at(LIST_TOP, LIST_LEFT, 0x54);
let mut page = 0;
loop {
self.wait_frame();
if let Some(p) = self.move_list_cursor(page) {
page = p;
continue;
}
let p = self.joy.pressed;
if p & A != 0 || (p & SELECT == 0 && p & START != 0) {
let Actor { y, x, .. } = self.s.actors[MENU_CURSOR];
let col = match x {
LIST_LEFT => 0,
0x40 => 1,
_ => 2,
};
self.level = page + ((y - LIST_TOP) >> 4) * 3 + col;
self.play(SFX_CONFIRM);
return;
}
if p & SELECT != 0 {
self.play(SFX_MENU_MOVE);
if page != 0 {
self.show(MAP0);
page = 0;
} else {
self.show(MAP1);
page = LIST_PAGE;
}
}
}
}
/// Returns the new page if the cursor moved off the top/bottom of one.
fn move_list_cursor(&mut self, page: u8) -> Option<u8> {
let r = self.joy.repeat;
let c = &mut self.s.actors[MENU_CURSOR];
let mut moved = 0;
if r & DOWN != 0 {
if c.y != LIST_BOTTOM {
c.y += 0x10;
moved += 1;
} else if page == 0 {
c.y = LIST_TOP;
self.video.display = Display::On(MAP1);
self.sound.play(SFX_MENU_MOVE);
return Some(LIST_PAGE);
}
} else if r & UP != 0 {
if c.y != LIST_TOP {
c.y -= 0x10;
moved += 1;
} else if page != 0 {
c.y = LIST_BOTTOM;
self.video.display = Display::On(MAP0);
self.sound.play(SFX_MENU_MOVE);
return Some(0);
}
}
if r & LEFT != 0 {
if c.x != LIST_LEFT {
c.x -= 0x30;
moved += 1;
}
} else if r & RIGHT != 0 && c.x != LIST_RIGHT {
c.x += 0x30;
moved += 1;
}
for _ in 0..moved {
self.sound.play(SFX_MENU_MOVE);
}
None
}
}
/// Letter number (0 = A) under the password cursor.
fn letter_at(y: u8, x: u8) -> u8 {
let row = y.wrapping_sub(LETTER_TOP) >> 4;
let col = x.wrapping_sub(LETTER_LEFT) >> 4;
row.wrapping_mul(7).wrapping_add(col)
}
+422
View File
@@ -0,0 +1,422 @@
//! The game itself.
//!
//! The game is written the way the original is: as straight-line code that
//! calls [`Game::wait_frame`] whenever it wants the next frame (to animate, to
//! wait for a button, ...). It runs on its own thread; `wait_frame` hands the
//! finished picture and sound to the host and blocks until the host sends the
//! next frame's buttons.
//!
//! Modules: `menus` (title, how to play, password, puzzle list), `story`
//! (story screens and the ending), `map` (the overworld walk between levels)
//! and `puzzle` (the puzzle itself and the level-clear screen).
mod map;
mod menus;
mod puzzle;
mod story;
use std::sync::mpsc::{Receiver, SyncSender};
use std::sync::Arc;
use crate::apu::{Apu, CLOCK_HZ};
use crate::input::Joypad;
use crate::rom::{GameData, Shape};
use crate::sound::SoundDriver;
use crate::video::{Display, Sprite, Video, MAP0, SCREEN_H, SCREEN_W};
/// Clock cycles per frame (the Game Boy runs at ~59.73 frames per second).
pub const FRAME_CYCLES: u64 = 70_224;
pub const FRAME_HZ: f64 = CLOCK_HZ as f64 / FRAME_CYCLES as f64;
/// The normal palette: shade n for color n, reversed (the game's art is
/// stored with 0 = black).
const PALETTE: u8 = 0x1B;
// ---- actor slots (multi-tile sprites, drawn every frame) ----
/// puzzle cursor / held piece; also the piece that drops on the map
const CURSOR: usize = 0;
/// menu arrow on the title screen and puzzle list
const MENU_CURSOR: usize = 1;
/// the hero on the map, the storyteller, the password cursor
const HERO: usize = 2;
/// "something here" marker on the map; the second figure in story scenes
const MARKER: usize = 3;
/// the bird that brings the password on the map
const MESSENGER: usize = 4;
const ACTOR_SLOTS: usize = 12;
/// A sprite made of one shape's cells, positioned at (y, x) in sprite
/// coordinates (screen + 16, screen + 8).
#[derive(Clone, Copy, Default)]
pub struct Actor {
pub visible: bool,
pub y: u8,
pub x: u8,
pub shape: u8,
}
impl Actor {
const fn at(y: u8, x: u8, shape: u8) -> Actor {
Actor { visible: true, y, x, shape }
}
}
/// Where a placed piece is and which board tiles it covers.
#[derive(Clone, Copy, Default)]
struct PieceSlot {
y: u8,
x: u8,
/// the board tiles under the piece's cells
under: [u8; 6],
}
/// The puzzle board: which piece id covers each background map cell.
type Board = Box<[u8; 0x400]>;
/// Scratch state that the original keeps in $C000-$CFFF and wipes at the
/// start of each puzzle (and at the title screen).
struct Scratch {
actors: [Actor; ACTOR_SLOTS],
/// id of the piece in hand (type << 4 | orientation), 0 = none
held: u8,
/// one slot per piece type
slots: [PieceSlot; 16],
slots_backup: [PieceSlot; 16],
board: Board,
board_backup: Board,
bridge: map::Bridge,
marker_blink: u8,
story: u8,
piece_drop_done: bool,
timer: Timer,
messenger: map::Messenger,
/// how far the hero can walk on the map
walk_left: u8,
walk_right: u8,
}
impl Default for Scratch {
fn default() -> Self {
Scratch {
actors: Default::default(),
held: 0,
slots: Default::default(),
slots_backup: Default::default(),
board: Box::new([0; 0x400]),
board_backup: Box::new([0; 0x400]),
bridge: Default::default(),
marker_blink: 0,
story: 0,
piece_drop_done: false,
timer: Timer::default(),
messenger: Default::default(),
walk_left: 0,
walk_right: 0,
}
}
}
#[derive(Clone, Copy, Default, PartialEq)]
enum TimerMode {
#[default]
Hidden,
Running,
Stopped,
}
/// The clear-time clock in the top right corner.
#[derive(Clone, Copy, Default)]
struct Timer {
mode: TimerMode,
frames: u8,
/// seconds, tens of seconds, minutes, tens of minutes, hours
digits: [u8; 5],
}
#[derive(Clone, Copy, Default)]
struct Palettes {
bg: u8,
obj0: u8,
obj1: u8,
/// palette 1 inverts every 16 frames (makes the cursor blink)
blink: u8,
}
/// What the game hands the host each frame.
pub struct Frame {
/// one shade (0 = white .. 3 = black) per pixel
pub pixels: Vec<u8>,
/// interleaved stereo samples
pub audio: Vec<f32>,
/// a peek at the game state, for testing
pub probe: Probe,
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Probe {
pub level: u8,
pub cursor_y: u8,
pub cursor_x: u8,
pub held: u8,
}
/// The game thread's end of the connection to the host.
pub struct Link {
pub frames: SyncSender<Frame>,
pub buttons: Receiver<u8>,
}
/// Unwinds the game thread when the host goes away.
struct Quit;
pub struct Game {
data: Arc<GameData>,
link: Link,
video: Video,
apu: Apu,
sound: SoundDriver,
joy: Joypad,
level: u8,
pieces_needed: u8,
palettes: Palettes,
/// sprites built this frame; the display picks them up at the next vblank
oam: [Sprite; 40],
s: Scratch,
}
/// Run the game on a new thread.
pub fn spawn(data: Arc<GameData>, sample_rate: u32, link: Link) -> std::thread::JoinHandle<()> {
std::thread::spawn(move || {
let mut game = Game::new(data, sample_rate, link);
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| game.run()));
if let Err(e) = result {
if !e.is::<Quit>() {
std::panic::resume_unwind(e);
}
}
})
}
impl Game {
fn new(data: Arc<GameData>, sample_rate: u32, link: Link) -> Game {
Game {
sound: SoundDriver::new(data.clone()),
data,
link,
video: Video::new(),
apu: Apu::new(sample_rate),
joy: Joypad::default(),
level: 0,
pieces_needed: 0,
palettes: Palettes::default(),
oam: [Sprite::default(); 40],
s: Scratch::default(),
}
}
fn run(&mut self) -> ! {
loop {
self.reset();
match self.title_screen() {
menus::TitleChoice::HowToPlay => {
self.how_to_play();
continue;
}
menus::TitleChoice::Password => {
self.password_screen();
self.puzzle_list();
}
menus::TitleChoice::Start => {}
}
self.play_levels();
self.ending();
}
}
/// Story, map, puzzle, repeat; returns after the last level.
fn play_levels(&mut self) {
loop {
self.s.timer.mode = TimerMode::Hidden;
self.story_screen();
self.story_text();
self.map_screen();
self.map_walk();
self.puzzle_screen();
self.s.timer.mode = TimerMode::Running;
loop {
self.wait_frame();
self.check_reset_puzzle();
self.move_cursor();
self.piece_action();
if self.pieces_in_place() == self.pieces_needed {
break;
}
}
self.s.timer.mode = TimerMode::Stopped;
self.level_clear();
if self.level as usize + 1 >= crate::rom::LEVEL_COUNT {
self.s.timer.mode = TimerMode::Hidden;
return;
}
self.level += 1;
}
}
/// Back to power-on state (the title screen clears all of RAM).
fn reset(&mut self) {
self.video.display = Display::Off;
self.s = Scratch::default();
self.level = 0;
self.pieces_needed = 0;
self.palettes = Palettes::default();
self.joy = Joypad::default();
self.sound = SoundDriver::new(self.data.clone());
}
// ---------------------------------------------------------------- frames
/// Finish this frame and wait for the next one.
fn wait_frame(&mut self) {
let mut pixels = vec![0; SCREEN_W * SCREEN_H];
self.video.render(&mut pixels);
let probe = Probe {
level: self.level,
cursor_y: self.s.actors[CURSOR].y,
cursor_x: self.s.actors[CURSOR].x,
held: self.s.held,
};
let frame = Frame { pixels, audio: std::mem::take(&mut self.apu.out), probe };
if self.link.frames.send(frame).is_err() {
std::panic::resume_unwind(Box::new(Quit));
}
let buttons = match self.link.buttons.recv() {
Ok(b) => b,
Err(_) => std::panic::resume_unwind(Box::new(Quit)),
};
// things the original does every vblank
self.video.sprites = self.oam;
self.update_palettes();
self.update_cursor_shape();
self.build_sprites();
self.joy.update(buttons);
self.update_bridge();
self.update_timer();
self.draw_timer();
self.update_marker_blink();
self.update_messenger();
self.sound.update(&mut self.apu);
self.apu.run(FRAME_CYCLES);
}
fn wait_frames(&mut self, n: u8) {
for _ in 0..n {
self.wait_frame();
}
}
fn update_palettes(&mut self) {
let p = &mut self.palettes;
self.video.bg_palette = p.bg;
self.video.obj_palettes = [p.obj0, p.obj1];
let (blink, wrapped) = p.blink.overflowing_add(0x10);
p.blink = blink;
if wrapped {
p.obj1 ^= 0xFF;
}
}
fn set_palettes(&mut self, v: u8) {
self.palettes.bg = v;
self.palettes.obj0 = v;
self.palettes.obj1 = v;
}
/// Turn the screen on showing the given background map.
fn show(&mut self, map: u16) {
self.video.display = Display::On(map);
}
fn play(&mut self, sound: u8) {
self.sound.play(sound);
}
fn shape(&self, n: u8) -> &Shape {
&self.data.shapes[n as usize]
}
fn clear_actors(&mut self) {
self.s.actors = Default::default();
self.s.held = 0;
}
/// The cursor actor shows the held piece, or the cursor itself (which
/// changes shape over the piece tray on the right).
fn update_cursor_shape(&mut self) {
let a = &mut self.s.actors[CURSOR];
a.shape = if self.s.held != 0 {
self.data.piece_shape(self.s.held)
} else if a.x >= 0xA4 {
0x5D
} else {
0
};
}
fn build_sprites(&mut self) {
let mut sprites = [Sprite::default(); 40];
let mut n = 0;
for a in self.s.actors.iter().filter(|a| a.visible) {
let shape = &self.data.shapes[a.shape as usize];
for cell in &shape.cells {
let (dy, dx) = cell.sprite_offset();
if let Some(s) = sprites.get_mut(n) {
*s = Sprite { y: a.y.wrapping_add(dy), x: a.x.wrapping_add(dx), tile: cell.tile, attr: shape.attr };
}
n += 1;
}
}
self.oam = sprites;
}
// ----------------------------------------------------------------- timer
fn update_timer(&mut self) {
let t = &mut self.s.timer;
if t.mode != TimerMode::Running {
return;
}
t.frames += 1;
if t.frames < 60 {
return;
}
t.frames = 0;
for (digit, limit) in t.digits.iter_mut().zip([10, 6, 10, 6, 10]) {
*digit += 1;
if *digit < limit {
return;
}
*digit = 0;
}
}
/// Two-tile-tall digits in the last 10 sprite slots: h mm ss.
fn draw_timer(&mut self) {
let t = self.s.timer;
if t.mode == TimerMode::Hidden {
return;
}
for (i, x) in [0x88, 0x80, 0x70, 0x68, 0x58].into_iter().enumerate() {
let tile = t.digits[i] * 2 + 0x64;
self.oam[30 + 2 * i] = Sprite { y: 0x10, x, tile, attr: 0 };
self.oam[31 + 2 * i] = Sprite { y: 0x18, x, tile: tile + 1, attr: 0 };
}
}
}
/// Map cell under a point in sprite coordinates.
fn cell_at(y: u8, x: u8) -> u16 {
let row = (y.wrapping_sub(16) >> 3) as u16;
let col = (x.wrapping_sub(8) >> 3) as u16;
MAP0 + row * 32 + col
}
+341
View File
@@ -0,0 +1,341 @@
//! The puzzle: pick up pieces, turn and flip them, and fit them all into the
//! board. Then the level-clear screen.
//!
//! The board is drawn into background map 0. Board floor tiles are $80 and
//! up, and the area to fill is the floor tiles at $F0 and up; everything below
//! $80 is a wall. `Scratch::board` records which piece covers each map cell,
//! and each piece type's slot remembers where it is and the floor tiles it
//! covers, so they can be put back when it's picked up.
use super::*;
use crate::input::*;
use crate::rom::addr::*;
use crate::sound::*;
use crate::video::{map_cell, MAP1};
const FLOOR: u8 = 0x80;
const TARGET_FLOOR: u8 = 0xF0;
/// Pieces must stay within this area (sprite coordinates).
const AREA_TOP: u8 = 0x20;
const AREA_BOTTOM: u8 = 0x99;
const AREA_LEFT: u8 = 0x08;
const AREA_RIGHT: u8 = 0xA1;
/// The empty cursor's range.
const CURSOR_MAX_Y: u8 = 0x9C;
const CURSOR_MIN_Y: u8 = 0x25;
const CURSOR_MIN_X: u8 = 0x0D;
const CURSOR_MAX_X: u8 = 0xA4;
/// Slot for a piece id (one per piece type).
fn slot(piece_id: u8) -> usize {
(piece_id.wrapping_sub(0x10) >> 4) as usize
}
/// A board cell covered by a shape: the map address and the tile.
struct Cell {
addr: u16,
tile: u8,
y: u8,
x: u8,
}
impl Game {
pub(super) fn puzzle_screen(&mut self) {
let data = self.data.clone();
let level = &data.levels[self.level as usize];
self.wait_frame();
self.video.load(0x8000, data.gfx(GFX_SPRITES, 0x1800));
self.s = Scratch::default();
self.video.fill_maps(0x84);
self.video.draw(&data.scripts.puzzle_hud);
self.draw_puzzle_number();
self.video.draw(&level.board);
for p in &level.pieces {
self.s.held = p.id;
self.s.actors[CURSOR].y = p.y.wrapping_add(16);
self.s.actors[CURSOR].x = p.x.wrapping_add(8);
self.update_cursor_shape();
let cells = self.held_cells();
self.cover(&cells);
self.put_down(&cells);
}
self.init_cursor();
// keep the starting position for "reset"
self.s.slots_backup = self.s.slots;
self.s.board_backup = self.s.board.clone();
self.video.copy_map(MAP0, MAP1);
self.pieces_needed = level.pieces_needed;
self.set_palettes(PALETTE);
self.play(level.music);
self.show(MAP0);
}
/// Puzzle number in the HUD, two tiles tall, left aligned.
fn draw_puzzle_number(&mut self) {
let n = self.level + 1;
let (tens, ones) = (n / 10, n % 10);
let digits: &[u8] = if tens != 0 { &[tens, ones] } else { &[ones] };
let mut cols = [(0x21, 0x29); 2]; // blank
for (i, d) in digits.iter().enumerate() {
cols[i] = (0x60 + 2 * d, 0x61 + 2 * d);
}
for (i, (top, bottom)) in cols.into_iter().enumerate() {
self.video.poke(map_cell(MAP0, 5 + i as u16, 0), top);
self.video.poke(map_cell(MAP0, 5 + i as u16, 1), bottom);
}
}
fn init_cursor(&mut self) {
let (y, x) = self.data.levels[self.level as usize].cursor;
self.s.actors[CURSOR] = Actor::at((y & 0xF8) | 4, (x & 0xF8) | 4, 0);
self.s.held = 0;
}
/// The held piece's cells, at its position snapped to the 8px grid.
fn held_cells(&self) -> Vec<Cell> {
let a = self.s.actors[CURSOR];
let shape = a.shape & 0x7F;
if shape == 0 {
return Vec::new();
}
let (y, x) = (a.y.wrapping_add(4) & 0xF8, a.x.wrapping_add(4) & 0xF8);
self.shape(shape)
.cells
.iter()
.map(|c| {
let (dy, dx) = c.board_offset();
let (cy, cx) = (y.wrapping_add(dy), x.wrapping_add(dx));
Cell { addr: cell_at(cy, cx), tile: c.tile, y: cy, x: cx }
})
.collect()
}
/// Record the floor tiles under the held piece in its slot. Fails (true)
/// on the first cell that isn't floor.
fn cover(&mut self, cells: &[Cell]) -> bool {
let slot = slot(self.s.held);
for (i, c) in cells.iter().enumerate() {
let under = self.video.peek(c.addr);
if under < FLOOR {
return true;
}
if let Some(u) = self.s.slots[slot].under.get_mut(i) {
*u = under;
}
}
false
}
/// Stamp the held piece onto the board.
fn put_down(&mut self, cells: &[Cell]) {
if cells.is_empty() {
return;
}
let a = self.s.actors[CURSOR];
let id = self.data.shape_piece_id(a.shape & 0x7F);
for c in cells {
self.s.board[(c.addr - MAP0) as usize] = id;
self.video.poke(c.addr, c.tile);
}
let slot = &mut self.s.slots[slot(id)];
(slot.y, slot.x) = (a.y.wrapping_add(4) & 0xF8, a.x.wrapping_add(4) & 0xF8);
self.play(SFX_PLACE);
}
/// Would `shape` at (y, x) stick out of the play area?
fn out_of_bounds(&self, shape: u8, y: u8, x: u8) -> bool {
self.shape(shape).cells.iter().any(|c| {
let (dy, dx) = c.board_offset();
let (cy, cx) = (y.wrapping_add(dy), x.wrapping_add(dx));
!(AREA_TOP..AREA_BOTTOM).contains(&cy) || !(AREA_LEFT..AREA_RIGHT).contains(&cx)
})
}
/// A pressed while B is held: put every piece back where it started.
pub(super) fn check_reset_puzzle(&mut self) {
if self.joy.pressed & A == 0 || self.joy.held & B == 0 {
return;
}
self.play(SFX_ROTATE);
self.s.actors[CURSOR].visible = false;
self.wipe_rows(MAP1, MAP0);
self.s.slots = self.s.slots_backup;
self.s.board = self.s.board_backup.clone();
self.init_cursor();
}
/// Copy the play area from one map to another, a row every two frames.
fn wipe_rows(&mut self, from: u16, to: u16) {
for row in 0..18 {
let row_tiles: Vec<u8> = (0..20).map(|c| self.video.peek(from + row * 32 + c)).collect();
self.wait_frame();
for (c, &t) in row_tiles.iter().enumerate() {
self.video.poke(to + row * 32 + c as u16, t);
}
self.wait_frame();
}
}
pub(super) fn move_cursor(&mut self) {
let r = self.joy.repeat;
if self.s.held == 0 {
let c = &mut self.s.actors[CURSOR];
if r & DOWN != 0 && c.y < CURSOR_MAX_Y {
c.y += 8;
}
if r & UP != 0 && c.y >= CURSOR_MIN_Y {
c.y -= 8;
}
if r & LEFT != 0 && c.x >= CURSOR_MIN_X {
c.x -= 8;
}
if r & RIGHT != 0 && c.x < CURSOR_MAX_X {
c.x += 8;
}
return;
}
// carrying a piece: it can't leave the play area
let shape = self.s.actors[CURSOR].shape;
let Actor { mut y, mut x, .. } = self.s.actors[CURSOR];
for (button, dy, dx) in [(DOWN, 8, 0), (UP, 0xF8, 0), (LEFT, 0, 0xF8), (RIGHT, 0, 8)] {
if r & button != 0 {
self.play(SFX_MOVE_PIECE);
let (ny, nx) = (y.wrapping_add(dy), x.wrapping_add(dx));
if !self.out_of_bounds(shape, ny, nx) {
(y, x) = (ny, nx);
}
}
}
let c = &mut self.s.actors[CURSOR];
(c.y, c.x) = (y, x);
}
/// A: pick up / put down. B: rotate. Select / Start: flip.
pub(super) fn piece_action(&mut self) {
let p = self.joy.pressed;
if p & A != 0 {
if self.s.held == 0 {
self.pick_up();
} else {
self.drop_held();
}
} else if p & B != 0 {
if self.s.held != 0 {
self.rotate();
}
} else if p & SELECT != 0 {
if self.s.held != 0 {
self.flip(true);
}
} else if p & START != 0 && self.s.held != 0 {
self.flip(false);
}
}
fn pick_up(&mut self) {
let a = self.s.actors[CURSOR];
let id = self.s.board[(cell_at(a.y, a.x) - MAP0) as usize];
if id == 0 {
return;
}
self.s.held = id;
let slot = self.s.slots[slot(id)];
(self.s.actors[CURSOR].y, self.s.actors[CURSOR].x) = (slot.y, slot.x);
self.update_cursor_shape();
for (c, under) in self.held_cells().iter().zip(slot.under) {
self.s.board[(c.addr - MAP0) as usize] = 0;
self.video.poke(c.addr, under);
}
self.play(SFX_PICK_UP);
}
fn drop_held(&mut self) {
let cells = self.held_cells();
self.wait_frame();
if self.cover(&cells) {
self.play(SFX_ERROR);
return;
}
self.put_down(&cells);
// the cursor moves to the piece's last cell
if let Some(last) = cells.last() {
(self.s.actors[CURSOR].y, self.s.actors[CURSOR].x) = (last.y + 4, last.x + 4);
}
self.s.held = 0;
self.update_cursor_shape();
}
fn rotate(&mut self) {
let held = self.s.held;
self.s.held = (held & 0xFC) | (held.wrapping_add(1) & 3);
self.update_cursor_shape();
let a = self.s.actors[CURSOR];
if self.out_of_bounds(a.shape, a.y, a.x) {
self.s.held = held;
} else {
self.play(SFX_ROTATE);
}
}
/// Bit 2 of the id mirrors the piece; bit 1 is adjusted so the mirror
/// is about the expected axis. Select and Start mirror about different
/// axes.
fn flip(&mut self, select: bool) {
let mut id = self.s.held ^ 4;
if (id & 1 == 0) == select {
id ^= 2;
}
self.s.held = id;
self.update_cursor_shape();
let a = self.s.actors[CURSOR];
if !self.out_of_bounds(a.shape, a.y, a.x) {
self.play(SFX_FLIP);
return;
}
// (sic) undoing always uses Start's rule, so undoing a Select flip
// can leave the piece turned
id ^= 4;
if id & 1 != 0 {
id ^= 2;
}
self.s.held = id;
}
/// Number of piece types whose cells all sit on the target area.
pub(super) fn pieces_in_place(&self) -> u8 {
self.s.slots[..13].iter().filter(|s| s.under[..5].iter().all(|&t| t >= TARGET_FLOOR)).count() as u8
}
pub(super) fn level_clear(&mut self) {
self.play(JINGLE_LEVEL_CLEAR);
self.clear_actors();
self.wait_frame();
// map 1 keeps the finished board; the banner goes over map 0
self.wipe_rows(MAP0, MAP1);
let data = self.data.clone();
for script in &data.scripts.level_clear_banner {
self.video.draw(script);
self.wait_frame();
}
let mut showing_board = false;
let mut t: u8 = 0;
loop {
self.wait_frame();
t = t.wrapping_add(1);
if t & 0x1F == 0 {
// sparkle the banner
let frame = if t & 0x20 != 0 { 0x20 } else { 0 };
self.video.load(0x8900, data.gfx(GFX_BANNER_SPARKLE + frame, 0x20));
}
if self.joy.pressed & START != 0 {
return;
}
if self.joy.pressed & A != 0 {
self.show(if showing_board { MAP0 } else { MAP1 });
showing_board = !showing_board;
}
}
}
}
+185
View File
@@ -0,0 +1,185 @@
//! The story screens shown before some levels, and the ending.
use super::*;
use crate::input::*;
use crate::rom::addr::*;
use crate::rom::Text;
use crate::sound::*;
use crate::video::map_cell;
const TEXT_START: u16 = map_cell(MAP0, 2, 3);
const ENDING_TEXT_START: u16 = map_cell(MAP0, 2, 1);
const TEXT_ROWS: u16 = 8;
const TEXT_WIDTH: u16 = 16;
const BLANK: u8 = 0xFE;
/// frames per letter
const TYPE_DELAY: u8 = 8;
/// Next text position: one cell right (wrapping within the 256-byte page,
/// as the original does).
fn advance(at: u16) -> u16 {
(at & 0xFF00) | (at as u8).wrapping_add(1) as u16
}
/// Start of the line after next (text is double spaced).
fn newline(at: u16) -> u16 {
(at & 0xFFC0) + 0x62
}
impl Game {
/// Story backdrop for the current level, if it has one.
pub(super) fn story_screen(&mut self) {
self.s.story = self.data.story_for(self.level);
if self.s.story == 0 {
return;
}
let data = self.data.clone();
self.video.load(0x8800, data.gfx(GFX_STORY, 0x800));
self.video.load(0x9000, data.gfx(GFX_FONT, 0x800));
self.video.fill_maps(BLANK);
self.video.draw(&data.scripts.intermission);
self.clear_actors();
self.s.actors[HERO] = Actor::at(0x00, 0x50, 0x52); // the storyteller, shown while talking
self.s.actors[MARKER] = Actor::at(0x80, 0x28, 0x50);
self.set_palettes(PALETTE);
self.play(MUSIC_MAP);
self.show(MAP0);
self.wait_frame();
}
/// Type out the story a page at a time. A skips the rest of a page.
pub(super) fn story_text(&mut self) {
let story = self.s.story;
if story == 0 || story > 4 {
return;
}
let data = self.data.clone();
let mut at = TEXT_START;
for &t in &data.stories[story as usize - 1] {
at = advance(at);
match t {
Text::End => {
self.s.actors[HERO].y = 0;
while self.joy.pressed & (A | START) == 0 {
self.wait_frame();
}
return;
}
Text::PageBreak => {
self.s.actors[HERO].y = 0;
while self.joy.pressed & A == 0 {
self.wait_frame();
}
for row in 0..TEXT_ROWS {
self.video.fill_row(TEXT_START + row * 32, BLANK, TEXT_WIDTH);
self.wait_frame();
}
at = TEXT_START;
}
Text::Newline => at = newline(at),
Text::Char(c) => {
// while A is down (it stays "pressed" until the next
// frame) letters are skipped without being drawn
if self.joy.pressed & A != 0 {
continue;
}
self.s.actors[HERO].y ^= 0x80; // mouth moves
self.video.poke(at, c);
for _ in 0..TYPE_DELAY {
self.wait_frame();
if self.joy.pressed & A != 0 {
break;
}
}
}
}
}
}
pub(super) fn ending(&mut self) {
let data = self.data.clone();
let gfx = |a, len| data.gfx(a, len);
self.video.load(0x8000, gfx(GFX_STORY, 0x800));
self.video.load(0x8800, gfx(GFX_STORY, 0x800));
self.video.load(0x9000, gfx(GFX_FONT, 0x800));
self.video.load(0x9600, gfx(GFX_ENDING_C, 0x140));
self.video.load(0x8B00, gfx(GFX_ENDING_D, 0x100));
self.video.load(0x8A40, gfx(GFX_ENDING_B, 0xC0));
self.video.load(0x8C00, gfx(GFX_ENDING_A, 0x80));
self.video.fill_maps(BLANK);
self.video.draw(&data.scripts.ending);
self.clear_actors();
self.s.actors[HERO] = Actor::at(0xF0, 0x78, 0x5C);
self.s.actors[MARKER] = Actor::at(0x60, 0x50, 0x5A);
self.set_palettes(PALETTE);
self.show(MAP0);
self.play(MUSIC_TITLE);
let art = &data.scripts.ending_art;
self.type_ending(&data.ending_text[0]);
self.wait_for_button();
self.draw_and_wait(&art[0]);
self.draw_and_wait(&art[1]);
self.open_door();
self.wait_for_button();
self.clear_ending_text();
self.draw_and_wait(&art[2]);
self.draw_and_wait(&art[3]);
self.type_ending(&data.ending_text[1]);
self.wait_for_button();
self.clear_ending_text();
self.type_ending(&data.ending_text[2]);
self.draw_and_wait(&art[0]);
self.draw_and_wait(&art[4]);
self.wait_for_button();
}
fn type_ending(&mut self, text: &[Text]) {
let mut at = ENDING_TEXT_START;
for &t in text {
at = advance(at);
let c = match t {
Text::End => break,
Text::Newline => {
at = newline(at);
continue;
}
Text::PageBreak => 0x81, // not a code in the ending's printer
Text::Char(c) => c,
};
self.video.poke(at, c);
self.s.actors[HERO].y ^= 0x80;
self.wait_frames(TYPE_DELAY);
}
self.s.actors[HERO].y = 0xF0;
}
fn wait_for_button(&mut self) {
loop {
self.wait_frame();
if self.joy.pressed & (A | START) != 0 {
return;
}
}
}
fn draw_and_wait(&mut self, script: &crate::rom::DrawScript) {
self.video.draw(script);
self.wait_frame();
}
fn open_door(&mut self) {
self.wait_frames(12);
for b in (1..=4).rev() {
self.video.poke(map_cell(MAP0, 12, 9) - b, 0x68 - b as u8);
self.wait_frames(12);
}
}
fn clear_ending_text(&mut self) {
for row in 0..TEXT_ROWS {
self.video.fill_row(ENDING_TEXT_START + row * 32, BLANK, TEXT_WIDTH);
self.wait_frame();
}
}
}
+136
View File
@@ -0,0 +1,136 @@
//! Headless runner for testing: `opus --headless OUT_DIR SCRIPT...`
//!
//! Script tokens: `N` run N frames; `BUTTON:N` hold a button for N frames
//! (up down left right a b start select); `shot:NAME` save OUT_DIR/NAME.png;
//! `probe` print the level and cursor; `goto:YY:XX` steer the puzzle cursor
//! (hex sprite coordinates). All audio is written to OUT_DIR/audio.wav.
use crate::game::Frame;
use crate::input;
use crate::rom::GameData;
use crate::video::{SCREEN_H, SCREEN_W};
use std::sync::Arc;
use std::fs;
use std::path::Path;
fn crc32(data: &[u8]) -> u32 {
let mut crc = 0xFFFF_FFFFu32;
for &b in data {
crc ^= b as u32;
for _ in 0..8 {
crc = if crc & 1 != 0 { 0xEDB8_8320 ^ (crc >> 1) } else { crc >> 1 };
}
}
!crc
}
fn write_png(path: &Path, w: usize, h: usize, gray: &[u8]) {
let mut raw = Vec::new();
for y in 0..h {
raw.push(0);
raw.extend_from_slice(&gray[y * w..(y + 1) * w]);
}
// zlib stream with stored (uncompressed) deflate blocks
let mut z = vec![0x78, 0x01];
for (i, chunk) in raw.chunks(65535).enumerate() {
let last = (i + 1) * 65535 >= raw.len();
z.push(last as u8);
z.extend_from_slice(&(chunk.len() as u16).to_le_bytes());
z.extend_from_slice(&(!(chunk.len() as u16)).to_le_bytes());
z.extend_from_slice(chunk);
}
let (mut a, mut b) = (1u32, 0u32);
for &x in &raw {
a = (a + x as u32) % 65521;
b = (b + a) % 65521;
}
z.extend_from_slice(&(b << 16 | a).to_be_bytes());
let mut png = b"\x89PNG\r\n\x1a\n".to_vec();
let mut chunk = |t: &[u8], d: &[u8]| {
png.extend_from_slice(&(d.len() as u32).to_be_bytes());
let mut td = t.to_vec();
td.extend_from_slice(d);
png.extend_from_slice(&td);
png.extend_from_slice(&crc32(&td).to_be_bytes());
};
let mut ihdr = Vec::new();
ihdr.extend_from_slice(&(w as u32).to_be_bytes());
ihdr.extend_from_slice(&(h as u32).to_be_bytes());
ihdr.extend_from_slice(&[8, 0, 0, 0, 0]);
chunk(b"IHDR", &ihdr);
chunk(b"IDAT", &z);
chunk(b"IEND", &[]);
fs::write(path, png).unwrap();
}
fn write_wav(path: &Path, rate: u32, samples: &[f32]) {
let mut d = Vec::new();
let data_len = samples.len() as u32 * 2;
d.extend_from_slice(b"RIFF");
d.extend_from_slice(&(36 + data_len).to_le_bytes());
d.extend_from_slice(b"WAVEfmt ");
d.extend_from_slice(&16u32.to_le_bytes());
d.extend_from_slice(&1u16.to_le_bytes());
d.extend_from_slice(&2u16.to_le_bytes());
d.extend_from_slice(&rate.to_le_bytes());
d.extend_from_slice(&(rate * 4).to_le_bytes());
d.extend_from_slice(&4u16.to_le_bytes());
d.extend_from_slice(&16u16.to_le_bytes());
d.extend_from_slice(b"data");
d.extend_from_slice(&data_len.to_le_bytes());
for s in samples {
d.extend_from_slice(&((s.clamp(-1.0, 1.0) * 32767.0) as i16).to_le_bytes());
}
fs::write(path, d).unwrap();
}
pub fn run(data: Arc<GameData>, args: &[String]) {
let out = Path::new(&args[0]);
fs::create_dir_all(out).unwrap();
let rate = 44_100;
let host = crate::start_game(data, rate);
let mut audio = Vec::new();
let mut frames = 0u64;
let mut frame: Frame = host.frames.recv().expect("game thread");
let mut step = |buttons: u8, n: u64, frame: &mut Frame| {
for _ in 0..n {
host.buttons.send(buttons).expect("game thread");
*frame = host.frames.recv().expect("game thread");
audio.extend_from_slice(&frame.audio);
frames += 1;
}
};
for tok in &args[1..] {
if let Ok(n) = tok.parse::<u64>() {
step(0, n, &mut frame);
} 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();
write_png(&out.join(format!("{name}.png")), SCREEN_W, SCREEN_H, &gray);
println!("{name}.png: {:?}", frame.probe);
} else if tok == "probe" {
println!("{:?}", frame.probe);
} else if let Some(spec) = tok.strip_prefix("goto:") {
let (ty, tx) = spec.split_once(':').unwrap();
let (ty, tx) = (u8::from_str_radix(ty, 16).unwrap(), u8::from_str_radix(tx, 16).unwrap());
for _ in 0..100 {
let p = frame.probe;
let bit = if p.cursor_y < ty { input::DOWN } else if p.cursor_y > ty { input::UP }
else if p.cursor_x < tx { input::RIGHT } else if p.cursor_x > tx { input::LEFT } else { break };
step(bit, 1, &mut frame);
step(0, 5, &mut frame);
}
} else if let Some((btn, n)) = tok.split_once(':') {
let bit = match btn {
"up" => input::UP, "down" => input::DOWN, "left" => input::LEFT, "right" => input::RIGHT,
"a" => input::A, "b" => input::B, "start" => input::START, "select" => input::SELECT,
_ => panic!("unknown token {tok}"),
};
step(bit, n.parse().unwrap(), &mut frame);
} else {
panic!("unknown token {tok}");
}
}
drop(step);
write_wav(&out.join("audio.wav"), rate, &audio);
println!("{frames} frames, {:.1}s of audio", audio.len() as f32 / 2.0 / rate as f32);
}
+44
View File
@@ -0,0 +1,44 @@
//! Buttons, as the game reads them once per frame.
pub const A: u8 = 0x01;
pub const B: u8 = 0x02;
pub const SELECT: u8 = 0x04;
pub const START: u8 = 0x08;
pub const RIGHT: u8 = 0x10;
pub const LEFT: u8 = 0x20;
pub const UP: u8 = 0x40;
pub const DOWN: u8 = 0x80;
pub const DPAD: u8 = 0xF0;
#[derive(Clone, Copy, Default)]
pub struct Joypad {
/// buttons down this frame
pub held: u8,
/// buttons that went down this frame
pub pressed: u8,
/// d-pad with auto-repeat: fires on press, then after 10 frames, then
/// every 4 frames while held
pub repeat: u8,
repeat_timer: u8,
}
impl Joypad {
pub fn update(&mut self, now: u8) {
self.pressed = (self.held ^ now) & now;
self.held = now;
self.repeat = 0;
if now == 0 {
return;
}
if self.pressed & DPAD != 0 {
self.repeat = self.pressed & DPAD;
self.repeat_timer = 10;
return;
}
self.repeat_timer = self.repeat_timer.wrapping_sub(1);
if self.repeat_timer == 0 {
self.repeat_timer = 4;
self.repeat = now & DPAD;
}
}
}
+268
View File
@@ -0,0 +1,268 @@
//! Daedalian Opus, ported to Rust.
//!
//! Usage: `opus [--rom PATH] [--headless OUT_DIR SCRIPT...]`
//!
//! The game's data is read from the original ROM ("Daedalian Opus (USA).gb"),
//! looked for in the current directory and its parent unless given.
//!
//! Controls: arrows = D-pad, Z = A, X = B, Enter = Start,
//! Right Shift / Backspace = Select, Esc = quit.
mod apu;
mod game;
mod headless;
mod input;
mod rom;
mod sound;
mod video;
use std::collections::VecDeque;
use std::path::PathBuf;
use std::sync::mpsc::{sync_channel, Receiver, SyncSender};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use pixels::{Pixels, SurfaceTexture};
use winit::application::ApplicationHandler;
use winit::dpi::LogicalSize;
use winit::event::{ElementState, WindowEvent};
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
use winit::keyboard::{KeyCode, PhysicalKey};
use winit::window::{Window, WindowId};
use game::{Frame, FRAME_HZ};
use video::{SCREEN_H, SCREEN_W};
const ROM_NAME: &str = "Daedalian Opus (USA).gb";
const SHADES: [[u8; 4]; 4] = [
[0xFF, 0xFF, 0xFF, 0xFF],
[0xAA, 0xAA, 0xAA, 0xFF],
[0x55, 0x55, 0x55, 0xFF],
[0x00, 0x00, 0x00, 0xFF],
];
type AudioQueue = Arc<Mutex<VecDeque<f32>>>;
/// The host's end of the connection to the game thread.
pub struct Host {
pub frames: Receiver<Frame>,
pub buttons: SyncSender<u8>,
}
pub fn start_game(data: Arc<rom::GameData>, sample_rate: u32) -> Host {
let (frame_tx, frame_rx) = sync_channel(1);
let (button_tx, button_rx) = sync_channel(1);
game::spawn(data, sample_rate, game::Link { frames: frame_tx, buttons: button_rx });
Host { frames: frame_rx, buttons: button_tx }
}
struct App {
window: Option<Arc<Window>>,
pixels: Option<Pixels<'static>>,
host: Host,
buttons: u8,
screen: Vec<u8>,
next_frame: Instant,
frame_time: Duration,
audio: AudioQueue,
audio_rate: u32,
_stream: Option<cpal::Stream>,
}
fn start_audio(queue: AudioQueue) -> Option<(cpal::Stream, u32)> {
let device = cpal::default_host().default_output_device()?;
let config = device.default_output_config().ok()?;
if config.sample_format() != cpal::SampleFormat::F32 {
eprintln!("audio: unsupported sample format {:?}, running silent", config.sample_format());
return None;
}
let rate = config.sample_rate();
let channels = config.channels() as usize;
let stream = device
.build_output_stream(
config.config(),
move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
let mut q = queue.lock().unwrap();
for frame in data.chunks_mut(channels) {
let l = q.pop_front().unwrap_or(0.0);
let r = q.pop_front().unwrap_or(l);
for (i, s) in frame.iter_mut().enumerate() {
*s = if i % 2 == 0 { l } else { r };
}
}
},
|e| eprintln!("audio error: {e}"),
None,
)
.ok()?;
stream.play().ok()?;
Some((stream, rate))
}
impl App {
fn key(&mut self, code: KeyCode, pressed: bool) {
let bit = match code {
KeyCode::ArrowRight => input::RIGHT,
KeyCode::ArrowLeft => input::LEFT,
KeyCode::ArrowUp => input::UP,
KeyCode::ArrowDown => input::DOWN,
KeyCode::KeyZ => input::A,
KeyCode::KeyX => input::B,
KeyCode::ShiftRight | KeyCode::Backspace => input::SELECT,
KeyCode::Enter => input::START,
_ => return,
};
if pressed {
self.buttons |= bit;
} else {
self.buttons &= !bit;
}
}
/// Advance the game one frame. False if the game thread has stopped.
fn step(&mut self) -> bool {
let Ok(frame) = self.host.frames.recv() else { return false };
self.screen = frame.pixels;
let mut q = self.audio.lock().unwrap();
q.extend(frame.audio);
let max = self.audio_rate as usize / 5 * 2; // keep at most ~200 ms queued
if q.len() > max {
let excess = q.len() - max;
q.drain(..excess);
}
drop(q);
self.host.buttons.send(self.buttons).is_ok()
}
}
impl ApplicationHandler for App {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if self.window.is_some() {
return;
}
let attrs = Window::default_attributes()
.with_title("Daedalian Opus")
.with_inner_size(LogicalSize::new(SCREEN_W as f64 * 4.0, SCREEN_H as f64 * 4.0));
let window = Arc::new(event_loop.create_window(attrs).expect("create window"));
let size = window.inner_size();
let surface = SurfaceTexture::new(size.width, size.height, window.clone());
let pixels = Pixels::new(SCREEN_W as u32, SCREEN_H as u32, surface).expect("pixels");
self.window = Some(window);
self.pixels = Some(pixels);
self.next_frame = Instant::now();
}
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
match event {
WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::KeyboardInput { event, .. } => {
if let PhysicalKey::Code(code) = event.physical_key {
if code == KeyCode::Escape {
event_loop.exit();
}
self.key(code, event.state == ElementState::Pressed);
}
}
WindowEvent::Resized(size) => {
if let Some(p) = &mut self.pixels {
let _ = p.resize_surface(size.width.max(1), size.height.max(1));
}
}
WindowEvent::RedrawRequested => {
if let Some(p) = &mut self.pixels {
for (dst, &shade) in p.frame_mut().chunks_exact_mut(4).zip(&self.screen) {
dst.copy_from_slice(&SHADES[shade as usize]);
}
if let Err(e) = p.render() {
eprintln!("render error: {e}");
event_loop.exit();
}
}
}
_ => {}
}
}
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
if self.window.is_none() {
return;
}
let now = Instant::now();
let mut ran = 0;
while self.next_frame <= now && ran < 4 {
if !self.step() {
eprintln!("the game stopped");
event_loop.exit();
return;
}
self.next_frame += self.frame_time;
ran += 1;
}
if self.next_frame < now {
self.next_frame = now; // fell behind; don't try to catch up forever
}
if let Some(w) = &self.window {
w.request_redraw();
}
event_loop.set_control_flow(ControlFlow::WaitUntil(self.next_frame));
}
}
fn find_rom(explicit: Option<String>) -> Option<PathBuf> {
if let Some(p) = explicit {
return Some(p.into());
}
[PathBuf::from(ROM_NAME), PathBuf::from("..").join(ROM_NAME)].into_iter().find(|p| p.exists())
}
fn main() {
let mut args: Vec<String> = std::env::args().skip(1).collect();
let mut rom_path = None;
if let Some(i) = args.iter().position(|a| a == "--rom") {
if i + 1 < args.len() {
rom_path = Some(args.remove(i + 1));
}
args.remove(i);
}
let Some(rom_path) = find_rom(rom_path) else {
eprintln!("can't find \"{ROM_NAME}\"; pass its path with --rom");
std::process::exit(1);
};
let rom = match rom::Rom::load(&rom_path) {
Ok(r) => r,
Err(e) => {
eprintln!("{e}");
std::process::exit(1);
}
};
let data = Arc::new(rom::GameData::parse(rom));
if args.first().map(|s| s.as_str()) == Some("--headless") {
headless::run(data, &args[1..]);
return;
}
let audio: AudioQueue = Arc::new(Mutex::new(VecDeque::new()));
let (stream, rate) = match start_audio(audio.clone()) {
Some((s, r)) => (Some(s), r),
None => {
eprintln!("audio: no output device, running silent");
(None, 48_000)
}
};
let mut app = App {
window: None,
pixels: None,
host: start_game(data, rate),
buttons: 0,
screen: vec![0; SCREEN_W * SCREEN_H],
next_frame: Instant::now(),
frame_time: Duration::from_secs_f64(1.0 / FRAME_HZ),
audio,
audio_rate: rate,
_stream: stream,
};
let event_loop = EventLoop::new().expect("event loop");
event_loop.run_app(&mut app).expect("run");
}
+367
View File
@@ -0,0 +1,367 @@
//! The game's data, parsed out of the original ROM at startup.
//!
//! Nothing copyrighted is compiled into this program: levels, piece shapes,
//! screen layouts, story text, graphics and music all come from the .gb file
//! the player supplies. This module turns the raw bytes into typed structures;
//! the addresses below are where each table lives in the ROM.
/// Where things live in the ROM.
pub mod addr {
// ---- levels ----
pub const LEVEL_PIECES: u16 = 0x0210; // 36 pointers to [id, y, x]* 0-terminated lists
pub const PIECE_SHAPE_INDEX: u16 = 0x07FA; // (type-1)*8 + orientation -> shape number
pub const LEVEL_STORY: u16 = 0x0EA5; // story number shown before each level (0 = none)
pub const LEVEL_BOARDS: u16 = 0x19E4; // 36 pointers to board draw scripts
pub const LEVEL_NEW_PIECE: u16 = 0x255B; // piece that drops from the sky on the map
pub const SHAPE_PIECE_ID: u16 = 0x264B; // shape number -> piece id (inverse of the above)
pub const LEVEL_PIECES_NEEDED: u16 = 0x2771;
pub const LEVEL_MUSIC: u16 = 0x27A0;
pub const LEVEL_CURSOR_Y: u16 = 0x27F3;
pub const LEVEL_CURSOR_X: u16 = 0x2817;
pub const PASSWORDS: u16 = 0x3433; // 36 x 4 ASCII letters
pub const SHAPES: u16 = 0x350B; // pointers to [attr|count, (yx, tile)*count]
pub const SHAPE_COUNT: usize = 0x5E;
// ---- screen layouts (tilemap draw scripts) ----
pub const ENDING_SCREEN: u16 = 0x0ABB;
pub const ENDING_ART: [u16; 5] = [0x0B7C, 0x0B8E, 0x0BAB, 0x0BBD, 0x0BDA];
pub const LEVEL_CLEAR_BANNER: u16 = 0x0D46; // 5-byte scripts, drawn one per frame
pub const INTERMISSION_SCREEN: u16 = 0x0ED6;
pub const HOW_TO_PLAY_SCREEN: u16 = 0x0FE2;
pub const MAP_SCREEN: u16 = 0x12D4;
pub const MAP_SCENES: u16 = 0x1303; // 3 pointers: first level, middle, last level
pub const MAP_CASTLE_INDEX: u16 = 0x15C7;
pub const MAP_CASTLES: u16 = 0x15EB; // 5 pointers
pub const MAP_NEXT_CASTLE_INDEX: u16 = 0x1692;
pub const MAP_NEXT_CASTLES: u16 = 0x16B6; // 4 pointers
pub const PUZZLE_LIST_SCREEN: u16 = 0x18BE;
pub const PUZZLE_HUD: u16 = 0x27C4;
pub const TITLE_SCREEN: u16 = 0x2F2D;
pub const TITLE_SCREEN_ALT: u16 = 0x2FFE; // drawn over a copy of the title in map 1
pub const PASSWORD_SCREEN: u16 = 0x33DF;
// ---- text ----
pub const STORIES: u16 = 0x2A5A; // 4 pointers
pub const ENDING_TEXT: [u16; 3] = [0x0BEF, 0x0C22, 0x0C51];
// ---- graphics (uncompressed 2bpp tiles) ----
pub const GFX_MAP_TERRAIN: u16 = 0x5000;
pub const GFX_ENDING_A: u16 = 0x5010;
pub const GFX_ENDING_B: u16 = 0x5340;
pub const GFX_STORY: u16 = 0x5800;
pub const GFX_SPRITES: u16 = 0x6000; // sprites, then puzzle tiles from $6800
pub const GFX_ENDING_C: u16 = 0x6200;
pub const GFX_BANNER_SPARKLE: u16 = 0x6900; // two 2-tile frames
pub const GFX_HOW_TO_PLAY: u16 = 0x7000;
pub const GFX_ENDING_D: u16 = 0x7300;
pub const GFX_FONT: u16 = 0x7800;
// ---- sound ----
pub const SOUND_HEADERS: u16 = 0x39FA; // indexed by (sound id - $80) * 2
pub const SILENCE_HEADER: u16 = 0x3BF0;
pub const VIBRATO_TABLES: u16 = 0x40E5; // indexed by vibrato number * 2 (number >= 1)
pub const WAVEFORMS: u16 = 0x4157;
pub const NOTE_PERIODS: u16 = 0x419F;
}
/// The raw ROM image.
pub struct Rom {
bytes: Vec<u8>,
}
impl Rom {
pub fn load(path: &std::path::Path) -> Result<Rom, String> {
let bytes = std::fs::read(path).map_err(|e| format!("{}: {e}", path.display()))?;
if bytes.len() != 0x8000 || &bytes[0x134..0x142] != b"DAEDALIAN OPUS" {
return Err(format!("{} is not a Daedalian Opus (USA) ROM", path.display()));
}
Ok(Rom { bytes })
}
pub fn byte(&self, addr: u16) -> u8 {
self.bytes[addr as usize]
}
pub fn word(&self, addr: u16) -> u16 {
u16::from_le_bytes([self.byte(addr), self.byte(addr.wrapping_add(1))])
}
pub fn bytes(&self, addr: u16, len: usize) -> &[u8] {
&self.bytes[addr as usize..addr as usize + len]
}
fn table(&self, addr: u16, len: usize) -> Vec<u8> {
self.bytes(addr, len).to_vec()
}
fn pointers(&self, addr: u16, count: usize) -> Vec<u16> {
(0..count).map(|i| self.word(addr + 2 * i as u16)).collect()
}
}
/// A tilemap draw script: the game's format for screen layouts. Each command
/// writes a run of bytes into VRAM, either along a row or down a column.
#[derive(Clone, Default)]
pub struct DrawScript(pub Vec<DrawCmd>);
#[derive(Clone)]
pub struct DrawCmd {
pub addr: u16,
pub vertical: bool,
pub tiles: Tiles,
}
#[derive(Clone)]
pub enum Tiles {
/// the same tile repeated
Run(u8, usize),
/// one tile per position
List(Vec<u8>),
}
impl DrawScript {
/// Command byte: bit 6 = vertical, bit 5 = repeat one tile, the rest
/// ($9F mask) is the address high byte. Then address low, count, data.
/// A zero command byte ends the script.
pub fn parse(rom: &Rom, mut a: u16) -> DrawScript {
let mut cmds = Vec::new();
loop {
let c = rom.byte(a);
if c == 0 {
return DrawScript(cmds);
}
let addr = u16::from(c & 0x9F) << 8 | u16::from(rom.byte(a + 1));
let count = match rom.byte(a + 2) {
0 => 256,
n => n as usize,
};
a += 3;
let tiles = if c & 0x20 != 0 {
a += 1;
Tiles::Run(rom.byte(a - 1), count)
} else {
a += count as u16;
Tiles::List(rom.bytes(a - count as u16, count).to_vec())
};
cmds.push(DrawCmd { addr, vertical: c & 0x40 != 0, tiles });
}
}
}
/// One cell of a shape: packed y/x offsets in 8-pixel units, and a tile.
#[derive(Clone, Copy)]
pub struct ShapeCell {
pub yx: u8,
pub tile: u8,
}
impl ShapeCell {
/// Offset used when stamping a piece onto the board (unsigned nibbles).
pub fn board_offset(self) -> (u8, u8) {
((self.yx >> 4) * 8, (self.yx & 0x0F) * 8)
}
/// Offset used when drawing the shape as sprites (signed nibbles).
pub fn sprite_offset(self) -> (u8, u8) {
let dy = ((self.yx & 0xF0) as i8) >> 1;
let dx = (((self.yx << 4) & 0xF0) as i8) >> 1;
(dy as u8, dx as u8)
}
}
/// A shape: the cells of a puzzle piece in one orientation, or a multi-tile
/// sprite (cursor, hero, messenger...). Pieces and sprites share the format.
#[derive(Clone)]
pub struct Shape {
/// OAM attribute bits used when drawn as sprites (palette, flips)
pub attr: u8,
pub cells: Vec<ShapeCell>,
}
/// A piece as it sits on the board when a level starts.
#[derive(Clone, Copy)]
pub struct StartPiece {
pub id: u8,
pub y: u8,
pub x: u8,
}
#[derive(Clone)]
pub struct Level {
pub pieces: Vec<StartPiece>,
pub board: DrawScript,
pub pieces_needed: u8,
pub music: u8,
/// cursor start position, sprite coordinates
pub cursor: (u8, u8),
/// piece id that falls from the sky on the map screen (0 = none)
pub new_piece: u8,
/// story shown before the level (0 = none, 1-4)
pub story: u8,
pub map_castle: u8,
pub map_next_castle: u8,
}
/// A piece of story text, as tile numbers plus layout codes.
#[derive(Clone, Copy, PartialEq)]
pub enum Text {
Char(u8),
Newline,
PageBreak,
End,
}
fn parse_text(rom: &Rom, mut a: u16) -> Vec<Text> {
let mut out = Vec::new();
loop {
let t = match rom.byte(a) {
0x80 => Text::Newline,
0x81 => Text::PageBreak,
0x82 => Text::End,
c => Text::Char(c),
};
out.push(t);
a += 1;
if t == Text::End {
return out;
}
}
}
pub struct Scripts {
pub title: DrawScript,
pub title_alt: DrawScript,
pub password: DrawScript,
pub puzzle_list: DrawScript,
pub how_to_play: DrawScript,
pub intermission: DrawScript,
pub ending: DrawScript,
pub ending_art: Vec<DrawScript>,
pub puzzle_hud: DrawScript,
pub level_clear_banner: Vec<DrawScript>,
pub map: DrawScript,
pub map_scenes: Vec<DrawScript>,
pub map_castles: Vec<DrawScript>,
pub map_next_castles: Vec<DrawScript>,
}
/// Everything the game needs, parsed from the ROM.
pub struct GameData {
pub rom: Rom,
pub levels: Vec<Level>,
pub shapes: Vec<Shape>,
piece_shape_index: Vec<u8>,
shape_piece_id: Vec<u8>,
/// 36 four-letter ASCII passwords; entry n starts level n+1 (0-based),
/// and the last one opens the puzzle list.
pub passwords: Vec<[u8; 4]>,
pub stories: Vec<Vec<Text>>,
pub ending_text: Vec<Vec<Text>>,
pub scripts: Scripts,
}
pub const LEVEL_COUNT: usize = 36;
impl GameData {
pub fn parse(rom: Rom) -> GameData {
let script = |a| DrawScript::parse(&rom, a);
let piece_lists = rom.pointers(addr::LEVEL_PIECES, LEVEL_COUNT);
let boards = rom.pointers(addr::LEVEL_BOARDS, LEVEL_COUNT);
let levels = (0..LEVEL_COUNT)
.map(|n| {
let mut pieces = Vec::new();
let mut a = piece_lists[n];
while rom.byte(a) != 0 {
pieces.push(StartPiece { id: rom.byte(a), y: rom.byte(a + 1), x: rom.byte(a + 2) });
a += 3;
}
let n16 = n as u16;
Level {
pieces,
board: script(boards[n]),
pieces_needed: rom.byte(addr::LEVEL_PIECES_NEEDED + n16),
music: rom.byte(addr::LEVEL_MUSIC + n16),
cursor: (rom.byte(addr::LEVEL_CURSOR_Y + n16), rom.byte(addr::LEVEL_CURSOR_X + n16)),
new_piece: rom.byte(addr::LEVEL_NEW_PIECE + n16),
story: rom.byte(addr::LEVEL_STORY + n16),
map_castle: rom.byte(addr::MAP_CASTLE_INDEX + n16),
map_next_castle: rom.byte(addr::MAP_NEXT_CASTLE_INDEX + n16),
}
})
.collect();
let shapes = rom
.pointers(addr::SHAPES, addr::SHAPE_COUNT)
.into_iter()
.map(|a| {
let head = rom.byte(a);
let cells = (0..(head & 0x0F) as u16)
.map(|i| ShapeCell { yx: rom.byte(a + 1 + 2 * i), tile: rom.byte(a + 2 + 2 * i) })
.collect();
Shape { attr: head & 0xF0, cells }
})
.collect();
let passwords = (0..LEVEL_COUNT as u16)
.map(|i| rom.bytes(addr::PASSWORDS + 4 * i, 4).try_into().unwrap())
.collect();
let scripts = Scripts {
title: script(addr::TITLE_SCREEN),
title_alt: script(addr::TITLE_SCREEN_ALT),
password: script(addr::PASSWORD_SCREEN),
puzzle_list: script(addr::PUZZLE_LIST_SCREEN),
how_to_play: script(addr::HOW_TO_PLAY_SCREEN),
intermission: script(addr::INTERMISSION_SCREEN),
ending: script(addr::ENDING_SCREEN),
ending_art: addr::ENDING_ART.iter().map(|&a| script(a)).collect(),
puzzle_hud: script(addr::PUZZLE_HUD),
level_clear_banner: {
let mut v = Vec::new();
let mut a = addr::LEVEL_CLEAR_BANNER;
while rom.byte(a) != 0 {
v.push(script(a));
a += 5;
}
v
},
map: script(addr::MAP_SCREEN),
map_scenes: rom.pointers(addr::MAP_SCENES, 3).into_iter().map(script).collect(),
map_castles: rom.pointers(addr::MAP_CASTLES, 5).into_iter().map(script).collect(),
map_next_castles: rom.pointers(addr::MAP_NEXT_CASTLES, 4).into_iter().map(script).collect(),
};
GameData {
levels,
shapes,
piece_shape_index: rom.table(addr::PIECE_SHAPE_INDEX, 0x80),
shape_piece_id: rom.table(addr::SHAPE_PIECE_ID, 0x41),
passwords,
stories: rom.pointers(addr::STORIES, 4).into_iter().map(|a| parse_text(&rom, a)).collect(),
ending_text: addr::ENDING_TEXT.iter().map(|&a| parse_text(&rom, a)).collect(),
scripts,
rom,
}
}
/// Piece id is `type << 4 | orientation`: bits 0-1 rotate, bit 2 flips.
pub fn piece_shape(&self, piece_id: u8) -> u8 {
let d = piece_id.wrapping_sub(0x10);
self.piece_shape_index[((d & 0xF0) >> 1 | (d & 0x07)) as usize]
}
/// The piece id a placed shape is recorded under on the board.
pub fn shape_piece_id(&self, shape: u8) -> u8 {
self.shape_piece_id.get(shape as usize).copied().unwrap_or(0)
}
/// Story number for a level index (index 36 exists in the table too).
pub fn story_for(&self, level: u8) -> u8 {
self.rom.byte(addr::LEVEL_STORY + level as u16)
}
/// Raw tile graphics.
pub fn gfx(&self, addr: u16, len: usize) -> &[u8] {
self.rom.bytes(addr, len)
}
}
+579
View File
@@ -0,0 +1,579 @@
//! The game's sound driver: a byte-code music sequencer with 8 channels
//! (4 for music, 4 for sound effects that temporarily take over the music's
//! hardware voices). Runs once per frame and plays the sound chip in `apu.rs`.
//!
//! Songs and effects are byte code in the ROM. A sound's header is a channel
//! count followed by 8 bytes per channel: flags, voice (0-1 pulse, 2 wave,
//! 3 noise), stereo mask, start address, transpose, vibrato, envelope.
//! Channel byte code:
//!
//! | byte | meaning |
//! |-----------|------------------------------------------------------------|
//! | $00-$7F | set note length (frames) and replay the current note |
//! | $80 | rest |
//! | $81-$DF | note (+ transpose); then an optional slide step if sliding, |
//! | | then an optional length byte |
//! | $E0/$E1 a | call subroutine at a |
//! | $E2 | return |
//! | $E3 | end of song |
//! | $E4 i n a | loop: counter i starts at n; jump to a until it runs out |
//! | $E5 a | jump |
//! | $E6 v | envelope | $E7 t | transpose += t |
//! | $E8 d | duty cycle | $E9 m | stereo mask |
//! | $EA w | load waveform w | $EB | end of sound effect |
//! | $EC / $ED | pitch slide on/off | $EE | tie (don't retrigger) |
//! | $EF v | master volume | $F0 v | vibrato table v |
//! | $F1 p | set period low byte directly |
use std::collections::VecDeque;
use std::sync::Arc;
use crate::apu::*;
use crate::rom::{addr, GameData};
// ---- sound ids ----
pub const MUSIC_TITLE: u8 = 0x81;
pub const MUSIC_MAP: u8 = 0x83; // also the story and how-to-play screens
pub const JINGLE_LEVEL_CLEAR: u8 = 0x86;
pub const SFX_CONFIRM: u8 = 0x87;
pub const SFX_MENU_MOVE: u8 = 0x88;
pub const SFX_ROTATE: u8 = 0x89; // also: puzzle reset
pub const SFX_PASSWORD_LETTER: u8 = 0x8A;
pub const SFX_FLIP: u8 = 0x8B;
pub const SFX_PLACE: u8 = 0x8C;
pub const SFX_BRIDGE: u8 = 0x8D;
pub const SFX_PICK_UP: u8 = 0x8E;
pub const SFX_ERROR: u8 = 0x8F;
pub const SFX_STEP: u8 = 0x90;
pub const SFX_MOVE_PIECE: u8 = 0x91;
pub const SFX_PIECE_LANDS: u8 = 0x92;
pub const PAUSE_MUSIC: u8 = 0x95;
pub const RESUME_MUSIC: u8 = 0x96;
/// "no sound waiting to start"
const IDLE: u8 = 0x80;
// channel flag bits
const ACTIVE: u8 = 0x80;
const SLIDING: u8 = 0x20;
const PAUSED: u8 = 0x10;
/// a sound effect is using this music channel's voice
const OVERRIDDEN: u8 = 0x04;
/// the current note should restart its envelope
const RETRIGGER: u8 = 0x01;
const PULSE1: u8 = 0;
const PULSE2: u8 = 1;
const WAVE: u8 = 2;
const NOISE: u8 = 3;
#[derive(Clone, Default)]
struct Channel {
flags: u8,
voice: u8,
stereo: u8,
pc: u16,
transpose: u8,
vibrato: u8,
envelope: u8,
return_stack: Vec<u16>,
length: u8,
timer: u8,
vibrato_pos: u8,
/// period of the current note (0 = rest)
base: u16,
/// slid period
slide: u16,
/// period actually played this frame
period: u16,
slide_step: u8,
duty: u8,
loop_counters: [u8; 11],
}
/// Last values written to the chip, so unchanged values aren't rewritten
/// (rewriting a period would restart the note).
#[derive(Clone, Copy, Default)]
struct Written {
env: [u8; 4],
lo: [u8; 4],
hi: [u8; 4],
}
pub struct SoundDriver {
data: Arc<GameData>,
queue: VecDeque<u8>,
pending: u8,
music_priority: u8,
sfx_priority: u8,
music_wave: u8,
sfx_wave: u8,
loaded_wave: u8,
master_volume: u8,
/// 0-3 music, 4-7 sound effects
channels: [Channel; 8],
written: Written,
}
impl SoundDriver {
pub fn new(data: Arc<GameData>) -> SoundDriver {
SoundDriver {
data,
queue: VecDeque::new(),
pending: 0,
music_priority: 0,
sfx_priority: 0,
music_wave: 0,
sfx_wave: 0,
loaded_wave: 0,
master_volume: 0,
channels: Default::default(),
written: Written::default(),
}
}
/// Queue a song or sound effect; one queued sound starts per frame.
pub fn play(&mut self, id: u8) {
self.queue.push_back(id);
}
fn rom(&self, a: u16) -> u8 {
self.data.rom.byte(a)
}
fn rom_word(&self, a: u16) -> u16 {
self.data.rom.word(a)
}
/// Once per frame.
pub fn update(&mut self, apu: &mut Apu) {
if let Some(id) = self.queue.pop_front() {
self.pending = id;
}
self.start_pending(apu);
self.mark_overridden_voices();
for ch in 0..8 {
if self.channels[ch].flags & ACTIVE != 0 {
self.update_channel(ch, apu);
}
}
}
fn start_pending(&mut self, apu: &mut Apu) {
let id = self.pending;
if id & 0x80 == 0 || id == IDLE {
return;
}
if id >= 0x97 {
// out-of-range ids silence the music, whatever is playing
self.music_priority = 0;
self.reset_chip(apu);
self.load_channels(addr::SILENCE_HEADER, 0);
return;
}
let header = self.rom_word(addr::SOUND_HEADERS + 2 * (id - 0x80) as u16);
match id {
0x81..=0x85 => self.start_music(header, 1, apu),
0x86..=0x94 => self.start_sfx(header, 1),
PAUSE_MUSIC => {
self.sfx_priority += 1;
for ch in &mut self.channels[..4] {
ch.flags |= PAUSED;
}
self.start_sfx(header, 2);
}
RESUME_MUSIC => {
// The original then starts a "sound effect" whose header
// pointer is garbage (it points into code); just resume.
self.written = Written::default();
self.sfx_priority += 1;
for ch in &mut self.channels[..4] {
ch.flags &= !PAUSED;
}
self.pending = IDLE;
}
_ => self.pending = IDLE,
}
}
fn start_music(&mut self, header: u16, priority: u8, apu: &mut Apu) {
if self.music_priority > priority {
self.pending = IDLE;
return;
}
self.music_priority = priority;
self.reset_chip(apu);
self.load_channels(header, 0);
}
fn start_sfx(&mut self, header: u16, priority: u8) {
if self.sfx_priority > priority {
self.pending = IDLE;
return;
}
self.sfx_priority = priority;
self.written = Written::default();
for ch in &mut self.channels[4..] {
ch.flags = 0;
}
self.load_channels(header, 4);
}
fn load_channels(&mut self, header: u16, first: usize) {
let count = self.rom(header) as usize;
for i in 0..count.min(8 - first) {
let a = header + 1 + 8 * i as u16;
self.channels[first + i] = Channel {
flags: self.rom(a),
voice: self.rom(a + 1),
stereo: self.rom(a + 2),
pc: self.rom_word(a + 3),
transpose: self.rom(a + 5),
vibrato: self.rom(a + 6),
envelope: self.rom(a + 7),
length: 1,
..Default::default()
};
}
self.pending = IDLE;
}
/// Stop everything and put the chip in a known state (done before each song).
fn reset_chip(&mut self, apu: &mut Apu) {
for ch in &mut self.channels {
ch.flags = 0;
}
self.load_waveform(1, apu);
apu.write(NR52, 0x8F);
self.master_volume = 0x77;
self.music_wave = 0;
self.sfx_wave = 0;
self.loaded_wave = 0;
self.init_voices(apu);
}
fn init_voices(&mut self, apu: &mut Apu) {
apu.write(NR32, 0);
apu.write(NR51, 0);
apu.write(NR10, 0x08);
apu.write(NR12, 0xF0);
apu.write(NR22, 0xF0);
apu.write(NR42, 0xF0);
apu.write(NR30, 0x80);
for r in [NR13, NR23, NR33, NR43] {
apu.write(r, 0xFF);
}
apu.write(NR50, self.master_volume);
for r in [NR14, NR24, NR34, NR44] {
apu.write(r, 0x87);
}
self.written = Written::default();
}
fn load_waveform(&mut self, n: u8, apu: &mut Apu) {
let src = self.rom_word(addr::WAVEFORMS + 2 * n as u16);
let power = apu.read(NR52);
apu.write(NR52, 0);
for i in 0..16 {
apu.write(WAVE_RAM + i, self.rom(src + i));
}
apu.write(NR52, power);
self.init_voices(apu);
}
/// A sound effect channel takes over the music channel with the same voice.
fn mark_overridden_voices(&mut self) {
for ch in &mut self.channels[..4] {
ch.flags &= !OVERRIDDEN;
}
for i in 4..8 {
if self.channels[i].flags & ACTIVE != 0 {
let voice = self.channels[i].voice as usize;
if let Some(m) = self.channels.get_mut(voice) {
m.flags |= OVERRIDDEN;
}
}
}
}
fn update_channel(&mut self, n: usize, apu: &mut Apu) {
if self.channels[n].flags & PAUSED != 0 {
return;
}
let ch = &mut self.channels[n];
ch.timer = ch.timer.wrapping_add(1);
if ch.timer == ch.length {
if !self.next_event(n, apu) {
return; // channel ended
}
let ch = &mut self.channels[n];
if ch.flags & RETRIGGER != 0 {
ch.vibrato_pos = 0;
}
}
if self.channels[n].flags & OVERRIDDEN != 0 {
return;
}
self.update_period(n);
match self.channels[n].voice {
PULSE1 => self.write_pulse(n, 0, apu),
PULSE2 => self.write_pulse(n, 1, apu),
WAVE => self.write_wave(n, apu),
NOISE => self.write_noise(n, apu),
_ => {}
}
}
/// Apply pitch slide and vibrato to the note's period.
fn update_period(&mut self, n: usize) {
let ch = &mut self.channels[n];
ch.period = ch.base;
if ch.base == 0 {
return;
}
if ch.flags & SLIDING != 0 {
ch.slide = add_signed(ch.slide, ch.slide_step);
ch.period = ch.slide;
}
if ch.vibrato == 0 {
return;
}
let table = self.rom_word(addr::VIBRATO_TABLES + 2 * self.channels[n].vibrato as u16);
loop {
let ch = &mut self.channels[n];
let pos = ch.vibrato_pos;
ch.vibrato_pos = pos.wrapping_add(1);
match self.data.rom.byte(table + pos as u16) {
0x80 => ch.vibrato_pos = 0, // restart the table
0x81 => ch.vibrato_pos = ch.vibrato_pos.wrapping_sub(2), // repeat previous entry
0x82 => {
ch.vibrato_pos = ch.vibrato_pos.wrapping_sub(1); // hold
return;
}
0x83 => ch.vibrato_pos = self.data.rom.byte(table + pos as u16 + 1), // jump
delta => {
ch.period = add_signed(ch.period, delta);
return;
}
}
}
}
fn silence(voice: u8, written: &mut Written, apu: &mut Apu) {
match voice {
PULSE1 | PULSE2 => {
let (lo, hi) = if voice == PULSE1 { (NR13, NR14) } else { (NR23, NR24) };
apu.write(lo, 0xFF);
apu.write(hi, 0x87);
written.lo[voice as usize] = 0xFF;
written.hi[voice as usize] = 0x87;
}
WAVE => {
apu.write(NR33, 0xFF);
apu.write(NR34, 0x07);
written.lo[2] = 0xFF;
written.hi[2] = 0x07;
}
NOISE => {
apu.write(NR43, 0xFF);
apu.write(NR44, 0x80);
written.lo[3] = 0xFF;
}
_ => {}
}
}
fn write_pulse(&mut self, n: usize, voice: usize, apu: &mut Apu) {
let [nr1, nr2, nr3, nr4] = if voice == 0 { [NR11, NR12, NR13, NR14] } else { [NR21, NR22, NR23, NR24] };
let w = &mut self.written;
let ch = &mut self.channels[n];
apu.write(nr1, ch.duty & 0xC0);
if w.env[voice] != ch.envelope {
w.env[voice] = ch.envelope;
apu.write(nr2, ch.envelope);
}
if ch.period == 0 {
Self::silence(voice as u8, w, apu);
return;
}
let pan = if voice == 0 { 0x11 } else { 0x22 };
apu.write(NR51, (apu.read(NR51) | pan) & ch.stereo);
let [lo, hi] = ch.period.to_le_bytes();
let retrigger = ch.flags & RETRIGGER != 0;
if !retrigger && w.lo[voice] == lo && w.hi[voice] == hi {
return;
}
apu.write(nr3, lo);
w.lo[voice] = lo;
w.hi[voice] = hi;
ch.flags &= !RETRIGGER;
apu.write(nr4, (hi & 7) | if retrigger { 0x80 } else { 0 });
}
fn write_wave(&mut self, n: usize, apu: &mut Apu) {
let w = &mut self.written;
let ch = &mut self.channels[n];
if ch.period == 0 {
Self::silence(WAVE, w, apu);
return;
}
if w.env[2] != ch.envelope {
w.env[2] = ch.envelope;
apu.write(NR32, ch.envelope & 0x60);
}
apu.write(NR51, (apu.read(NR51) | 0x44) & ch.stereo);
let [lo, hi] = ch.period.to_le_bytes();
if w.lo[2] == lo && w.hi[2] == hi {
return;
}
apu.write(NR33, lo);
w.lo[2] = lo;
w.hi[2] = hi;
apu.write(NR34, hi & 7); // the wave voice is never restarted
}
fn write_noise(&mut self, n: usize, apu: &mut Apu) {
let w = &mut self.written;
let ch = &mut self.channels[n];
if w.env[3] != ch.envelope {
w.env[3] = ch.envelope;
apu.write(NR42, ch.envelope);
}
let poly = ch.period as u8;
if poly == 0 {
Self::silence(NOISE, w, apu);
return;
}
apu.write(NR51, (apu.read(NR51) | 0x88) & ch.stereo);
if w.lo[3] == poly {
return;
}
apu.write(NR43, poly);
w.lo[3] = poly;
// (sic) noise restarts only when the note is *not* flagged to retrigger
let restart = if ch.flags & RETRIGGER != 0 { 0 } else { 0x80 };
ch.flags &= !RETRIGGER;
apu.write(NR44, restart);
}
/// Run byte code up to the next note. Returns false if the channel ended.
fn next_event(&mut self, n: usize, apu: &mut Apu) -> bool {
self.channels[n].flags |= RETRIGGER;
let mut pc = self.channels[n].pc;
loop {
let op = self.rom(pc);
pc += 1;
if op >= 0xE0 {
match self.command(n, op, &mut pc, apu) {
Some(()) => continue,
None => return false,
}
}
let ch = &mut self.channels[n];
if op < 0x80 {
ch.length = op;
} else {
let note = if op == 0x80 { op } else { op.wrapping_add(ch.transpose) };
ch.base = self.data.rom.word(addr::NOTE_PERIODS + (note as u16 * 2 & 0xFF));
ch.slide = ch.base;
if ch.flags & SLIDING != 0 {
ch.slide_step = self.data.rom.byte(pc);
pc += 1;
}
let next = self.data.rom.byte(pc);
if next & 0x80 == 0 {
ch.length = next;
pc += 1;
}
}
ch.pc = pc;
ch.timer = 0;
return true;
}
}
/// Execute one $E0-$F1 command. None = the channel stopped.
fn command(&mut self, n: usize, op: u8, pc: &mut u16, apu: &mut Apu) -> Option<()> {
let arg = self.rom(*pc);
let target = self.rom_word(*pc);
let ch = &mut self.channels[n];
match op {
0xE0 | 0xE1 => {
ch.return_stack.push(*pc + 2);
*pc = target;
}
0xE2 => *pc = ch.return_stack.pop().unwrap_or(*pc),
0xE3 => {
self.music_priority = 0;
self.stop_channel(n, apu);
return None;
}
0xE4 => {
let counter = &mut ch.loop_counters[arg as usize];
if *counter == 0 {
*counter = self.data.rom.byte(*pc + 1);
}
*counter -= 1;
*pc = if *counter == 0 { *pc + 4 } else { self.data.rom.word(*pc + 2) };
}
0xE5 => *pc = target,
0xE6 => (ch.envelope, *pc) = (arg, *pc + 1),
0xE7 => (ch.transpose, *pc) = (ch.transpose.wrapping_add(arg), *pc + 1),
0xE8 => (ch.duty, *pc) = (arg, *pc + 1),
0xE9 => (ch.stereo, *pc) = (arg, *pc + 1),
0xEA => {
*pc += 1;
self.select_waveform(n, arg, apu);
}
0xEB => {
self.sfx_priority = 0;
if self.music_wave != self.loaded_wave {
self.loaded_wave = self.music_wave;
self.load_waveform(self.music_wave, apu);
}
self.stop_channel(n, apu);
return None;
}
0xEC => ch.flags |= SLIDING,
0xED => ch.flags &= !SLIDING,
0xEE => ch.flags &= !RETRIGGER,
0xEF => {
*pc += 1;
self.master_volume = arg;
apu.write(NR50, arg);
}
0xF0 => (ch.vibrato, *pc) = (arg, *pc + 1),
0xF1 => (ch.base, *pc) = ((ch.base & 0xFF00) | arg as u16, *pc + 1),
_ => {} // unused opcodes
}
Some(())
}
fn select_waveform(&mut self, n: usize, w: u8, apu: &mut Apu) {
if self.channels[n].flags & OVERRIDDEN != 0 {
self.music_wave = w;
return;
}
if w == self.loaded_wave {
return;
}
self.loaded_wave = w;
// (sic) the original counts only channels 0-2 as music here
if n < 3 {
self.music_wave = w;
} else {
self.sfx_wave = w;
}
self.load_waveform(w, apu);
}
fn stop_channel(&mut self, n: usize, apu: &mut Apu) {
self.channels[n].flags = 0;
Self::silence(self.channels[n].voice, &mut self.written, apu);
}
}
/// period + signed 8-bit delta
fn add_signed(v: u16, delta: u8) -> u16 {
v.wrapping_add(delta as i8 as u16)
}
+167
View File
@@ -0,0 +1,167 @@
//! The display: tile graphics, two 32x32 background maps, 40 hardware
//! sprites and three palettes, rendered to a 160x144 image of 4 shades.
//!
//! This is the model the game's art is built for, so the game draws by
//! writing tiles into maps and placing sprites, exactly as the original did.
//! The game never scrolls and never uses the window layer, so neither exists
//! here.
use crate::rom::{DrawScript, Tiles};
pub const SCREEN_W: usize = 160;
pub const SCREEN_H: usize = 144;
/// Background map base addresses (the game addresses tiles by VRAM address,
/// and so do its draw scripts).
pub const MAP0: u16 = 0x9800;
pub const MAP1: u16 = 0x9C00;
pub const MAP_SIZE: usize = 0x400;
/// Address of the map cell at (column, row).
pub const fn map_cell(map: u16, col: u16, row: u16) -> u16 {
map + row * 32 + col
}
#[derive(Clone, Copy, Default)]
pub struct Sprite {
/// screen y + 16 (0 hides the sprite)
pub y: u8,
/// screen x + 8
pub x: u8,
pub tile: u8,
/// bit 4 = palette 1, bit 5 = x flip, bit 6 = y flip, bit 7 = behind background
pub attr: u8,
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum Display {
Off,
/// background (from the given map) and sprites
On(u16),
}
pub struct Video {
vram: Box<[u8; 0x2000]>,
pub sprites: [Sprite; 40],
pub bg_palette: u8,
pub obj_palettes: [u8; 2],
pub display: Display,
}
impl Video {
pub fn new() -> Video {
Video {
vram: Box::new([0; 0x2000]),
sprites: [Sprite::default(); 40],
bg_palette: 0,
obj_palettes: [0; 2],
display: Display::Off,
}
}
pub fn peek(&self, addr: u16) -> u8 {
self.vram[(addr & 0x1FFF) as usize]
}
pub fn poke(&mut self, addr: u16, v: u8) {
if (0x8000..0xA000).contains(&addr) {
self.vram[(addr - 0x8000) as usize] = v;
}
}
/// Copy tile graphics (or anything else) into VRAM.
pub fn load(&mut self, addr: u16, bytes: &[u8]) {
for (i, &b) in bytes.iter().enumerate() {
self.poke(addr.wrapping_add(i as u16), b);
}
}
/// Fill both background maps with one tile.
pub fn fill_maps(&mut self, tile: u8) {
self.vram[0x1800..0x2000].fill(tile);
}
pub fn copy_map(&mut self, from: u16, to: u16) {
let (f, t) = ((from - 0x8000) as usize, (to - 0x8000) as usize);
self.vram.copy_within(f..f + MAP_SIZE, t);
}
/// Write `n` copies of `tile` along a row.
pub fn fill_row(&mut self, addr: u16, tile: u8, n: u16) {
for i in 0..n {
self.poke(addr + i, tile);
}
}
pub fn draw(&mut self, script: &DrawScript) {
for cmd in &script.0 {
let step = if cmd.vertical { 32 } else { 1 };
let mut a = cmd.addr;
let mut put = |t| {
self.poke(a, t);
a = a.wrapping_add(step);
};
match &cmd.tiles {
Tiles::Run(t, n) => (0..*n).for_each(|_| put(*t)),
Tiles::List(ts) => ts.iter().for_each(|&t| put(t)),
}
}
}
fn tile_pixel(&self, tile_addr: usize, x: usize, y: usize) -> u8 {
let lo = self.vram[tile_addr + y * 2];
let hi = self.vram[tile_addr + y * 2 + 1];
((lo >> (7 - x)) & 1) | (((hi >> (7 - x)) & 1) << 1)
}
/// Background tiles 0-127 come from $9000, 128-255 from $8800.
fn bg_tile_addr(tile: u8) -> usize {
(0x1000 + (tile as i8 as isize) * 16) as usize
}
/// Render the frame as shades 0 (white) to 3 (black).
pub fn render(&self, out: &mut [u8]) {
let map = match self.display {
Display::Off => {
out.fill(0);
return;
}
Display::On(map) => (map - 0x8000) as usize,
};
let shade = |pal: u8, c: u8| (pal >> (c * 2)) & 3;
let mut bg_color = [0u8; SCREEN_W * SCREEN_H];
for y in 0..SCREEN_H {
for x in 0..SCREEN_W {
let tile = self.vram[map + (y / 8) * 32 + x / 8];
let c = self.tile_pixel(Self::bg_tile_addr(tile), x % 8, y % 8);
bg_color[y * SCREEN_W + x] = c;
out[y * SCREEN_W + x] = shade(self.bg_palette, c);
}
}
// lower sprite numbers win, so draw from the end
for s in self.sprites.iter().rev() {
let (sy, sx) = (s.y as i32 - 16, s.x as i32 - 8);
let pal = self.obj_palettes[(s.attr >> 4 & 1) as usize];
for ty in 0..8 {
let py = sy + ty as i32;
if !(0..SCREEN_H as i32).contains(&py) {
continue;
}
let row = if s.attr & 0x40 != 0 { 7 - ty } else { ty };
for tx in 0..8 {
let px = sx + tx as i32;
if !(0..SCREEN_W as i32).contains(&px) {
continue;
}
let col = if s.attr & 0x20 != 0 { 7 - tx } else { tx };
let c = self.tile_pixel(s.tile as usize * 16, col, row);
let i = py as usize * SCREEN_W + px as usize;
if c == 0 || (s.attr & 0x80 != 0 && bg_color[i] != 0) {
continue;
}
out[i] = shade(pal, c);
}
}
}
}
}