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 { 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 { // 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 { 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 { let mut bytes = Vec::::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, path: Option, } impl Bytecode { fn path(&self) -> String { match &self.path { Some(path) => path.as_os_str().to_string_lossy().to_string(), None => String::from(""), } } } fn parse_dimensions(string: &str) -> Option { 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 { 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, }) }