diff options
author | Ben Bridle <bridle.benjamin@gmail.com> | 2025-07-03 15:26:07 +1200 |
---|---|---|
committer | Ben Bridle <ben@derelict.engineering> | 2025-07-03 21:24:07 +1200 |
commit | 2accc78948fa4a18e37ab0bc405f9b2758acaa3e (patch) | |
tree | 2551180ef7fb8f67bfc826de4ad3daf2dd24942e /src/bin/br | |
download | bedrock-pc-2accc78948fa4a18e37ab0bc405f9b2758acaa3e.zip |
Initial commit
Diffstat (limited to 'src/bin/br')
-rw-r--r-- | src/bin/br/config.rs | 122 | ||||
-rw-r--r-- | src/bin/br/load.rs | 81 | ||||
-rw-r--r-- | src/bin/br/main.rs | 217 |
3 files changed, 420 insertions, 0 deletions
diff --git a/src/bin/br/config.rs b/src/bin/br/config.rs new file mode 100644 index 0000000..56d5190 --- /dev/null +++ b/src/bin/br/config.rs @@ -0,0 +1,122 @@ +use crate::*; + + +#[derive(Copy, Clone, PartialEq)] +pub enum Mode { + Graphical, + Headless, + Dynamic, +} + +impl Mode { + pub fn from_str(string: &str) -> Self { + match string { + "g" | "graphical" => Self::Graphical, + "h" | "headless" => Self::Headless, + "d" | "dynamic" => Self::Dynamic, + _ => fatal!("Invalid mode string '{string}'"), + } + } +} + + +pub fn parse_dimensions(string: &str) -> ScreenDimensions { + fn parse_inner(string: &str) -> Option<ScreenDimensions> { + let (w_str, h_str) = string.trim().split_once('x')?; + Some( ScreenDimensions { + width: w_str.parse().ok()?, + height: h_str.parse().ok()?, + } ) + } + let dimensions = parse_inner(string).unwrap_or_else(|| { + fatal!("Invalid dimensions string '{string}'"); + }); + if dimensions.is_zero() { + fatal!("Screen dimensions must be greater than zero"); + } + return dimensions; +} + + +pub fn parse_palette(string: &str) -> [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 } + } + } + fn parse_inner(string: &str) -> Option<[Colour; 16]> { + 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, + }) + } + parse_inner(string).unwrap_or_else(|| { + fatal!("Invalid palette string '{string}'"); + }) +} + + +pub fn parse_metadata_colour(colour: Option<MetadataColour>) -> Option<Colour> { + let c = colour?; + Some(Colour::from_rgb(c.red, c.green, c.blue)) +} + + +pub fn parse_small_icon(bytes: Option<Vec<u8>>, bg: Colour, fg: Colour) -> Option<Icon> { + let rgba = sprite_data_to_rgb(&bytes?, 3, bg, fg); + match Icon::from_rgba(rgba, 24, 24) { + Ok(icon) => Some(icon), + Err(err) => unreachable!("Error while parsing small icon data: {err}"), + } +} + +pub fn parse_large_icon(bytes: Option<Vec<u8>>, bg: Colour, fg: Colour) -> Option<Icon> { + let rgba = sprite_data_to_rgb(&bytes?, 8, bg, fg); + match Icon::from_rgba(rgba, 64, 64) { + Ok(icon) => Some(icon), + Err(err) => unreachable!("Error while parsing large icon data: {err}"), + } +} + +fn sprite_data_to_rgb(bytes: &[u8], size: usize, bg: Colour, fg: Colour) -> Vec<u8> { + let sprites: Vec<&[u8]> = bytes.chunks_exact(8).collect(); + let mut rgba = Vec::new(); + for sprite_row in 0..size { + for pixel_row in 0..8 { + for sprite_column in 0..size { + let sprite = &sprites[sprite_column + (sprite_row * size)]; + let row = &sprite[pixel_row]; + for bit in 0..8 { + let state = row & (0x80 >> bit); + let colour = match state != 0 { + true => fg, + false => bg, + }; + rgba.extend_from_slice(&colour.as_rgba_array()); + } + } + } + } + return rgba; +} diff --git a/src/bin/br/load.rs b/src/bin/br/load.rs new file mode 100644 index 0000000..93a748c --- /dev/null +++ b/src/bin/br/load.rs @@ -0,0 +1,81 @@ +use crate::*; + +use std::io::Read; +use std::path::{Path, PathBuf}; + + +pub struct LoadedProgram { + pub bytecode: Vec<u8>, + pub path: Option<PathBuf>, +} + +/// Load program from path or standard input. +pub fn load_program(path: Option<&PathBuf>) -> LoadedProgram { + if let Some(path) = path { + if let Ok(program) = load_program_from_file(path) { + let length = program.bytecode.len(); + info!("Loaded program from {path:?} ({length} bytes)"); + return program; + } else if let Some(program) = load_program_from_env(path) { + let length = program.bytecode.len(); + info!("Loaded program from {path:?} ({length} bytes)"); + return program; + } else { + fatal!("Could not read program from {path:?}"); + } + } else { + info!("Reading program from standard input..."); + if let Ok(program) = load_program_from_stdin() { + let length = program.bytecode.len(); + info!("Loaded program from standard input ({length} bytes)"); + return program; + } else { + fatal!("Could not read program from standard input"); + } + } +} + +/// Attempt to load program from a directory in the BEDROCK_PATH environment variable. +fn load_program_from_env(path: &Path) -> Option<LoadedProgram> { + // Check that the path is a bare program name. + 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); + // Skip relative paths. + if !base_path.is_absolute() { continue; } + base_path.push(path); + if let Ok(program) = load_program_from_file(&base_path) { + return Some(program); + } + if path.extension().is_some() { continue; } + base_path.set_extension("br"); + if let Ok(program) = load_program_from_file(&base_path) { + return Some(program); + } + } + } + return None; +} + +/// Attempt to load program from a file path. +fn load_program_from_file(path: &Path) -> Result<LoadedProgram, 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_program_from_readable_source(std::fs::File::open(&path)?, Some(&path)) +} + +/// Attempt to load program from standard input. +fn load_program_from_stdin() -> Result<LoadedProgram, std::io::Error> { + load_program_from_readable_source(std::io::stdin(), None) +} + +/// Attempt to load program from a source that implements std::io::Read. +fn load_program_from_readable_source(source: impl Read, path: Option<&Path>) -> Result<LoadedProgram, std::io::Error> { + let mut bytecode = Vec::<u8>::new(); + source.take(65536).read_to_end(&mut bytecode)?; + return Ok(LoadedProgram { bytecode, path: path.map(|p| p.to_path_buf()) }); +} diff --git a/src/bin/br/main.rs b/src/bin/br/main.rs new file mode 100644 index 0000000..81c5ec9 --- /dev/null +++ b/src/bin/br/main.rs @@ -0,0 +1,217 @@ +#![feature(path_add_extension)] + +mod config; +mod load; +pub use config::*; +pub use load::*; + +use bedrock_asm::*; +use bedrock_core::*; +use bedrock_pc::*; +use log::*; +use switchboard::*; +use phosphor::*; + +use std::cmp::{min, max}; +use std::num::NonZeroU32; + + +fn main() { + let mut args = Switchboard::from_env(); + if let Some("asm") = args.peek() { + args.pop(); + assemble(args, "br asm"); + } + + // ----------------------------------------------------------------------- + + args.named("help").short('h'); + args.named("version"); + args.named("verbose").short('v'); + + if args.get("help").as_bool() { + print_help(); + std::process::exit(0); + } + if args.get("version").as_bool() { + let name = env!("CARGO_PKG_NAME"); + let version = env!("CARGO_PKG_VERSION"); + eprintln!("{name} v{version}"); + eprintln!("Written by Ben Bridle."); + std::process::exit(0); + } + if args.get("verbose").as_bool() { + log::set_log_level(log::LogLevel::Info); + } + + args.positional("source"); + args.named("debug").short('d'); + args.named("mode").default("dynamic"); + + args.named("palette"); + args.named("fullscreen").short('f'); + args.named("show-cursor").short('c'); + args.named("zoom").short('z').quick("3").default("1"); + args.named("size").short('s'); + args.named("decode-stdin").short('i'); + args.named("encode-stdout").short('o'); + args.raise_errors(); + + let source = args.get("source").as_path_opt(); + let debug = args.get("debug").as_bool(); + let mode = Mode::from_str(args.get("mode").as_str()); + let palette = args.get("palette").as_str_opt().map(parse_palette); + let fullscreen = args.get("fullscreen").as_bool(); + let show_cursor = args.get("show-cursor").as_bool(); + let zoom_raw = min(10, max(1, args.get("zoom").as_u32())); + let zoom = unsafe { NonZeroU32::new_unchecked(zoom_raw) }; + let dimensions = match args.get("size").as_str_opt() { + Some(string) => parse_dimensions(string), + None => DEFAULT_SCREEN_SIZE / (zoom_raw as u16), + }; + let decode_stdin = args.get("decode-stdin").as_bool(); + let encode_stdout = args.get("encode-stdout").as_bool(); + + // ----------------------------------------------------------------------- + + let LoadedProgram { bytecode, path } = load_program(source.as_ref()); + let mut title = String::from("Bedrock program"); + let mut icon = None; + + if let Some(metadata) = Metadata::from(&bytecode) { + let name = metadata.name().unwrap_or("unnamed".to_string()); + let authors = metadata.authors().unwrap_or_else(Vec::new); + let mut metadata_string = format!("Program is '{name}'"); + if authors.len() > 0 { + metadata_string.push_str(&format!(", by {}", authors[0])); } + if authors.len() > 1 { + metadata_string.push_str(" and others"); } + info!("{metadata_string}"); + + match name.split_once('/') { + Some((name, _version)) if !name.is_empty() => title = name.to_string(), + _ => title = name.to_string(), + } + let bg = parse_metadata_colour(metadata.bg_colour()).unwrap_or(Colour::rgb(0x1E1C26)); + let fg = parse_metadata_colour(metadata.fg_colour()).unwrap_or(Colour::rgb(0xED614F)); + match parse_large_icon(metadata.large_icon(), bg, fg) { + Some(large_icon) => icon = Some(large_icon), + None => match parse_small_icon(metadata.small_icon(), bg, fg) { + Some(small_icon) => icon = Some(small_icon), + None => (), + } + } + } else { + info!("Program does not contain metadata"); + } + + let symbols_path = path.as_ref().map(|p| { + let mut path = p.to_path_buf(); + path.add_extension("sym"); path + }); + + let config = EmulatorConfig { + dimensions, fullscreen, zoom, palette, show_cursor, + decode_stdin, encode_stdout, + symbols_path, title, icon, + }; + + if let Ok(phosphor) = Phosphor::new() { + match mode { + Mode::Dynamic => { + info!("Starting graphical emulator"); + let mut emulator = GraphicalEmulator::new(config, debug); + emulator.load_program(&bytecode); + emulator.run(phosphor, false); + } + Mode::Graphical => { + info!("Starting graphical emulator"); + let mut emulator = GraphicalEmulator::new(config, debug); + emulator.load_program(&bytecode); + emulator.run(phosphor, false); + } + Mode::Headless => { + info!("Starting headless emulator"); + let mut emulator = HeadlessEmulator::new(&config, debug); + emulator.load_program(&bytecode); + emulator.run(); + } + } + } else { + match mode { + Mode::Dynamic => { + info!("Could not start graphical event loop"); + info!("Starting headless emulator"); + let mut emulator = HeadlessEmulator::new(&config, debug); + emulator.load_program(&bytecode); + emulator.run(); + } + Mode::Graphical => { + fatal!("Could not start graphical event loop"); + } + Mode::Headless => { + info!("Starting headless emulator"); + let mut emulator = HeadlessEmulator::new(&config, debug); + emulator.load_program(&bytecode); + emulator.run(); + } + } + } +} + + +fn print_help() { + eprintln!("\ +Usage: br [source] + br asm [source] [destination] + +Emulator and assembler for the Bedrock computer system. + +To access the assembler, run `br asm`. To learn how to use the +assembler, run `br asm --help`. + +Usage: + To load a Bedrock program from a file, run `br <path>`, where + <path> is the path to an assembled Bedrock program. + + To load a Bedrock program from piped input, run `<command> | br`, + where <command> is a command that generates Bedrock bytecode. + + To assemble a Bedrock program from a source file and write to an + output file, run `br asm <source> <output>`, where <source> is the + path of the source file and <output> is the path to write to. + + To assemble and run a Bedrock program without saving to a file, + run `br asm <source> | br`, where <source> is the path of the + source file. + +Environment variables: + BEDROCK_PATH + A list of colon-separated paths that will be searched to find + a Bedrock program when the program doesn't exist in the current + directory. This allows the user to run frequently-used programs + from any directory. + BEDROCK_LIBS + A list of colon-separated paths that will be searched to find + Bedrock source code files to use as libraries when assembling a + Bedrock program. If a library file resolves an unresolved symbol + in the program being assembled, the library file will be merged + into the program. + +Arguments: + [program] Path to a Bedrock program to run + +Switches: + --fullscreen (-f) Start the program in fullscreen mode (toggle with F11) + --size=<dim> (-s) Set the initial window size in the format <width>x<height> + --zoom=<scale> (-z) Set the pixel size for the screen (change with F5/F6) + --show-cursor (-c) Show the operating system cursor over the window + --palette=<pal> Set a debug colour palette in the format <rgb>,... (toggle with F2) + --debug, (-d) Show debug information while the program is running + --decode-stdin (-i) Decode transmissions on standard input from text lines. + --encode-stdout (-o) Encode transmissions on standard output as text lines. + --help (-h) Print this help information + --verbose, (-v) Print additional information + --version Print the program version and exit +"); +} |