summaryrefslogtreecommitdiff
path: root/src/bin/br/run.rs
blob: 728ebda7a23cfdf874d7aef7f3cacf5f99c47ecc (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
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,
    })
}