mod app;
use std::fs;
use anyhow::{Context, Result, ensure};
use clap::{CommandFactory, FromArgMatches, Parser};
use sha2::{Digest, Sha256};
use winit::event_loop::EventLoop;
use crate::app::audio::AudioSystem;
use crate::app::{App, AppConfig, MachineConfig};
use apogee_rs::core::debug::{ReplayMetadata, ReplayPlayer, ReplayRecorder};
use apogee_rs::core::machine::Machine;
use apogee_rs::core::video::{ColorMode, VideoRenderer};
const SYSTEM_ROM: &[u8] = include_bytes!("../dist/roms/apogee.rom");
const FONT_ROM: &[u8] = include_bytes!("../dist/fonts/sga.bin");
const SYSTEM_ROM_HASH: &str = include_str!("../dist/roms/apogee.rom.sha256").trim_ascii();
const FONT_ROM_HASH: &str = include_str!("../dist/fonts/sga.bin.sha256").trim_ascii();
fn check_integrity() -> Result<()> {
let verify = |name: &str, data: &[u8], expected: &str| -> Result<()> {
let hash = Sha256::digest(data);
let actual = hex::encode(hash);
ensure!(
actual == expected,
"integrity check failed for asset '{}'",
name
);
Ok(())
};
verify("apogee.rom", SYSTEM_ROM, SYSTEM_ROM_HASH)?;
verify("sga.bin", FONT_ROM, FONT_ROM_HASH)?;
Ok(())
}
#[derive(Parser, Debug)]
#[command(
name = "apogee",
version,
override_usage = "apogee [options] [file]",
disable_help_flag = true,
disable_version_flag = true,
next_line_help = true,
help_template = "Usage: {usage}\n\n{all-args}"
)]
struct Args {
#[arg(value_name = "file", hide = true)]
file: Option<String>,
#[arg(long, value_name = "file", help_heading = "General options")]
rka: Option<String>,
#[arg(long, value_name = "file", help_heading = "General options")]
rom: Option<String>,
#[arg(short = 'a', long = "autorun", help_heading = "General options")]
autorun: bool,
#[arg(short = 'f', long = "force", help_heading = "General options")]
force: bool,
#[arg(
short = 'h',
long = "help",
action = clap::ArgAction::Help,
help_heading = "General options"
)]
help: Option<bool>,
#[arg(
short = 'V',
long = "version",
action = clap::ArgAction::Version,
help_heading = "General options"
)]
version: Option<bool>,
#[arg(long, conflicts_with = "grayscale", help_heading = "Display options")]
bw: bool,
#[arg(short, long, conflicts_with = "bw", help_heading = "Display options")]
grayscale: bool,
#[arg(short, long, help_heading = "Display options")]
crt: bool,
#[arg(long, num_args = 0..=1, default_missing_value = "", help_heading = "MIDI options")]
midi: Option<String>,
#[arg(long, help_heading = "MIDI options")]
midi_list: bool,
#[arg(long, help_heading = "Debug options")]
debug: bool,
#[arg(long, requires = "debug", help_heading = "Debug options")]
record: bool,
#[arg(long, conflicts_with = "record", help_heading = "Debug options")]
play: Option<String>,
}
fn main() -> Result<()> {
check_integrity()?;
let mut cmd = Args::command();
if !std::env::args_os().any(|arg| arg == "--debug") {
cmd = cmd
.mut_arg("debug", |a| a.hide(true))
.mut_arg("record", |a| a.hide(true))
.mut_arg("play", |a| a.hide(true));
}
let matches = cmd.get_matches();
let args = Args::from_arg_matches(&matches).unwrap_or_else(|e| e.exit());
if args.midi_list {
if let Ok(midi_out) = midir::MidiOutput::new("Apogee BK-01") {
for (i, port) in midi_out.ports().iter().enumerate() {
if let Ok(name) = midi_out.port_name(port) {
println!("{}: {}", i, name);
}
}
}
return Ok(());
}
let (rka_path, rom_path) = match args.file {
Some(file) => {
let path = std::path::Path::new(&file);
match path
.extension()
.and_then(|e| e.to_str())
.map(|e| e.to_ascii_lowercase())
.as_deref()
{
Some("rka") => (args.rka.or(Some(file)), args.rom),
Some("rom") => (args.rka, args.rom.or(Some(file))),
_ => anyhow::bail!(
"unsupported file extension for '{}': only .rka and .rom are allowed",
file
),
}
}
None => (args.rka, args.rom),
};
ensure!(
rom_path.is_none() || args.midi.is_none(),
"a ROM disk cannot be plugged in simultaneously with the MIDI"
);
let (rka_data, rom_sha256, rom_name) = if let Some(path) = &rka_path {
let data = fs::read(path).with_context(|| format!("could not read '{}'", path))?;
Machine::validate_rka(&data, args.force)
.with_context(|| format!("invalid RKA file '{}'", path))?;
let sha256 = hex::encode(Sha256::digest(&data));
let name = std::path::Path::new(path)
.file_stem()
.unwrap_or(std::ffi::OsStr::new("unknown"))
.to_string_lossy()
.into_owned();
(Some(data), sha256, name)
} else {
(None, String::from(SYSTEM_ROM_HASH), String::from("monitor"))
};
let rom_payload = if let Some(rom_path) = &rom_path {
let data = fs::read(rom_path).with_context(|| format!("could not read '{}'", rom_path))?;
Some(std::sync::Arc::from(data))
} else {
None
};
let player = if let Some(path) = &args.play {
let player = ReplayPlayer::from_file(path)?;
player.verify_rom_hash(&rom_sha256)?;
Some(player)
} else {
None
};
let autorun = player
.as_ref()
.map(|p| p.replay.metadata.autorun)
.unwrap_or(args.autorun);
let color_mode = player
.as_ref()
.map(|p| p.replay.metadata.color_mode)
.unwrap_or_else(|| {
if args.bw {
ColorMode::Bw
} else if args.grayscale {
ColorMode::Grayscale
} else {
ColorMode::Color
}
});
let is_crt = player
.as_ref()
.map(|p| p.replay.metadata.is_crt)
.unwrap_or(args.crt);
let rka_payload = rka_data.map(|data| (std::sync::Arc::from(data), autorun, args.force));
let event_loop = EventLoop::new().context("Failed to create winit event loop")?;
let audio = AudioSystem::new().context("Failed to initialize audio system")?;
let video = VideoRenderer::new(FONT_ROM.to_vec(), color_mode, is_crt);
let sample_rate = player
.as_ref()
.map(|p| p.replay.metadata.sample_rate)
.unwrap_or(audio.sample_rate);
let midi_conn = if rom_payload.is_none()
&& let Some(midi_arg) = &args.midi
{
midir::MidiOutput::new("Apogee BK-01")
.ok()
.and_then(|midi_out| {
let ports = midi_out.ports();
let target_port = if midi_arg.is_empty() {
ports.first().cloned()
} else {
ports
.iter()
.find(|p| midi_out.port_name(p).is_ok_and(|name| name == *midi_arg))
.or_else(|| {
midi_arg
.parse::<usize>()
.ok()
.and_then(|idx| ports.get(idx))
})
.cloned()
};
if let Some(port) = target_port {
let conn_name = midi_out
.port_name(&port)
.unwrap_or_else(|_| "Apogee BK-01 MIDI Out".to_string());
midi_out.connect(&port, &conn_name).ok()
} else {
#[cfg(unix)]
{
use midir::os::unix::VirtualOutput;
midi_out.create_virtual(midi_arg).ok()
}
#[cfg(not(unix))]
{
None
}
}
})
} else {
None
};
let machine_config = MachineConfig {
system_rom: std::sync::Arc::from(SYSTEM_ROM),
sample_rate,
rka: rka_payload,
romdisk: rom_payload,
midi_enabled: midi_conn.is_some() || args.midi.is_some(),
rom_name: rom_name.clone(),
};
let recorder = args.record.then(|| {
ReplayRecorder::new(ReplayMetadata {
rom_name,
rom_sha256,
autorun,
sample_rate,
color_mode,
is_crt,
})
});
let mut app = App::new(
machine_config,
video,
audio,
AppConfig {
debug_mode: args.debug,
recorder,
player,
midi_out: midi_conn,
},
);
event_loop
.run_app(&mut app)
.context("Application execution failed")?;
if let Some(err) = app.fatal_error.take() {
return Err(err);
}
Ok(())
}