diff options
Diffstat (limited to 'src/bin/br/run.rs')
-rw-r--r-- | src/bin/br/run.rs | 230 |
1 files changed, 230 insertions, 0 deletions
diff --git a/src/bin/br/run.rs b/src/bin/br/run.rs new file mode 100644 index 0000000..728ebda --- /dev/null +++ b/src/bin/br/run.rs @@ -0,0 +1,230 @@ +use log::*; +use crate::*; + +use bedrock_pc::*; +use phosphor::*; + +use std::cmp::max; +use std::io::Read; +use std::path::{Path, PathBuf}; + + +pub fn main(mut args: Switchboard) { + let source = args.positional("source").as_path_opt(); + let debug = args.named("debug").short('d').as_bool(); + let fullscreen = args.named("fullscreen").short('f').as_bool(); + let scale = max(1, args.named("zoom").short('z').default("1").quick("3").as_u32()); + let dimensions = match args.named("size").short('s').as_string_opt() { + Some(string) => parse_dimensions(&string).unwrap_or_else(|| + fatal!("Invalid dimensions string {string:?}")), + None => DEFAULT_SCREEN_SIZE / (scale as u16), + }; + let debug_palette = match args.named("palette").as_string_opt() { + Some(string) => match parse_palette(&string) { + Some(palette) => Some(palette), + None => fatal!("Invalid palette string {string:?}"), + }, + None => None, + }; + let show_cursor = args.named("show-cursor").short('c').as_bool(); + let decode_stdin = args.named("decode-stdin").short('i').as_bool(); + let encode_stdout = args.named("encode-stdout").short('o').as_bool(); + + let Bytecode { bytes: bytecode, path } = load_bytecode(source.as_ref()); + let symbols_path = path.as_ref().map(|p| { + let mut path = p.to_path_buf(); + path.set_extension("br.sym"); + path + }); + + let metadata = parse_metadata(&bytecode); + if metadata.is_none() { + info!("Could not read program metadata"); + } + + let config = EmulatorConfig { + dimensions, + fullscreen, + scale, + debug_palette, + show_cursor, + initial_transmission: None, + decode_stdin, + encode_stdout, + symbols_path, + }; + let phosphor = Phosphor::new(); + + if phosphor.is_ok() && dimensions.area_usize() != 0 { + info!("Starting graphical emulator"); + let mut phosphor = phosphor.unwrap(); + let cursor = match config.show_cursor { + true => Some(CursorIcon::Default), + false => None, + }; + + let mut graphical = GraphicalEmulator::new(&config, debug); + graphical.load_program(&bytecode); + if let EmulatorSignal::Promote = graphical.run() { + let program_name = match &metadata { + Some(metadata) => match &metadata.name { + Some(name) => name.to_string(), + None => String::from("Bedrock"), + } + None => String::from("Bedrock"), + }; + let window = WindowBuilder { + dimensions: Some(graphical.dimensions()), + size_bounds: Some(graphical.size_bounds()), + fullscreen: graphical.fullscreen, + scale: graphical.scale, + title: Some(program_name), + cursor, + icon: None, + program: Box::new(graphical), + }; + + phosphor.create_window(window); + phosphor.run().unwrap(); + } + } else { + info!("Starting headless emulator"); + let mut headless = HeadlessEmulator::new(&config, debug); + headless.load_program(&bytecode); + headless.run(debug); + }; +} + +fn load_bytecode(path: Option<&PathBuf>) -> Bytecode { + // TODO: Etch file location into bytecode as per metadata. + if let Some(path) = path { + if let Ok(bytecode) = load_bytecode_from_file(path) { + let length = bytecode.bytes.len(); + let path = bytecode.path(); + info!("Loaded program from {path:?} ({length} bytes)"); + return bytecode; + } else if let Some(bytecode) = load_bytecode_from_bedrock_path(path) { + let length = bytecode.bytes.len(); + let path = bytecode.path(); + info!("Loaded program from {path:?} ({length} bytes)"); + return bytecode; + } else { + fatal!("Could not read program from {path:?}"); + } + } else { + info!("Reading program from standard input..."); + if let Ok(bytecode) = load_bytecode_from_stdin() { + let length = bytecode.bytes.len(); + info!("Loaded program from standard input ({length} bytes)"); + return bytecode; + } else { + fatal!("Could not read program from standard input"); + } + } +} + +/// Attempt to load bytecode from a directory in the BEDROCK_PATH environment variable. +fn load_bytecode_from_bedrock_path(path: &Path) -> Option<Bytecode> { + if path.is_relative() && path.components().count() == 1 { + for base_path in std::env::var("BEDROCK_PATH").ok()?.split(':') { + let mut base_path = PathBuf::from(base_path); + if !base_path.is_absolute() { continue; } + base_path.push(path); + info!("Attempting to load program from {base_path:?}"); + if let Ok(bytecode) = load_bytecode_from_file(&base_path) { + return Some(bytecode); + } + if path.extension().is_some() { continue; } + base_path.set_extension("br"); + info!("Attempting to load program from {base_path:?}"); + if let Ok(bytecode) = load_bytecode_from_file(&base_path) { + return Some(bytecode); + } + } + } + return None; +} + +/// Attempt to load bytecode from a file path. +fn load_bytecode_from_file(path: &Path) -> Result<Bytecode, std::io::Error> { + // Canonicalize paths so that symbolic links to program files resolve to + // the real program directory, which could contain a symbols file. + let path = match path.canonicalize() { + Ok(canonical) => canonical, + Err(_) => path.to_path_buf(), + }; + load_bytecode_from_readable_source(std::fs::File::open(&path)?, Some(&path)) +} + +/// Attempt to load bytecode from standard input. +fn load_bytecode_from_stdin() -> Result<Bytecode, std::io::Error> { + load_bytecode_from_readable_source(std::io::stdin(), None) +} + +/// Attempt to load bytecode from a source that implements std::io::Read. +fn load_bytecode_from_readable_source(source: impl Read, path: Option<&Path>) -> Result<Bytecode, std::io::Error> { + let mut bytes = Vec::<u8>::new(); + source.take(65536).read_to_end(&mut bytes)?; + return Ok(Bytecode { bytes, path: path.map(|p| p.to_path_buf()) }); +} + +struct Bytecode { + bytes: Vec<u8>, + path: Option<PathBuf>, +} + +impl Bytecode { + fn path(&self) -> String { + match &self.path { + Some(path) => path.as_os_str().to_string_lossy().to_string(), + None => String::from("<unknown>"), + } + } +} + + + + +fn parse_dimensions(string: &str) -> Option<ScreenDimensions> { + if string.trim().to_lowercase() == "none" { + return Some(ScreenDimensions::ZERO); + } + let (w_str, h_str) = string.trim().split_once('x')?; + Some( ScreenDimensions { + width: w_str.parse().ok()?, + height: h_str.parse().ok()?, + } ) +} + +fn parse_palette(string: &str) -> Option<[Colour; 16]> { + fn decode_ascii_hex_digit(ascii: u8) -> Option<u8> { + match ascii { + b'0'..=b'9' => Some(ascii - b'0'), + b'a'..=b'f' => Some(ascii - b'a' + 10), + b'A'..=b'F' => Some(ascii - b'A' + 10), + _ => { None } + } + } + let mut c = Vec::new(); + for token in string.split(',') { + let mut bytes = token.bytes(); + if bytes.len() != 3 { return None; } + let r = decode_ascii_hex_digit(bytes.next().unwrap())?; + let g = decode_ascii_hex_digit(bytes.next().unwrap())?; + let b = decode_ascii_hex_digit(bytes.next().unwrap())?; + c.push(Colour::from_rgb(r*17, g*17, b*17)); + } + Some(match c.len() { + 2 => [ c[0], c[1], c[0], c[1], c[0], c[1], c[0], c[1], + c[0], c[1], c[0], c[1], c[0], c[1], c[0], c[1] ], + 3 => [ c[0], c[1], c[0], c[2], c[0], c[1], c[0], c[2], + c[0], c[1], c[0], c[2], c[0], c[1], c[0], c[2] ], + 4 => [ c[0], c[1], c[2], c[3], c[0], c[1], c[2], c[3], + c[0], c[1], c[2], c[3], c[0], c[1], c[2], c[3] ], + 8 => [ c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7], + c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7] ], + 16 => [ c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7], + c[8], c[9], c[10], c[11], c[12], c[13], c[14], c[15] ], + _ => return None, + }) +} |