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
#![warn(missing_docs)]
//! Encode and decode Octo cartridges or "Octocarts", CHIP-8 game cartridges for the
//! [Octo](https://github.com/JohnEarnest/Octo) environment.
//!
//! Use cases:
//!
//! * Decoding: You can extract the program source code and runtime settings from an
//! Octocart file. The source code can be assembled into CHIP-8 bytecode with Octo or
//! [`decasm`](https://crates.io/crates/decasm). The runtime settings can be given to a CHIP-8 interpreter like Octo or
//! [`deca`](https://crates.io/crates/deca), or saved as JSON for the [CHIP-8
//! Archive](https://github.com/JohnEarnest/chip8Archive), as an `.octo.rc` file for C-Octo or
//! [`termin-8`](https://crates.io/crates/termin-8), etc.
//! * Encoding: TODO
//!
//! Octo cartridge files are GIF89a images with a payload steganographically
//! embedded in one or more animation frames. Data is stored in the least significant
//! bits of colors, 1 from the red/blue channels and 2 from the green channel,
//! allowing us to pack a hidden byte into every 2 successive pixels.
//!
//! The payload consists of a 32-bit length, followed by a sequence of ASCII bytes
//! consisting of the JSON-encoded options dictionary and source text.
//!
//! An Octo cartridge contains the source code of an Octo program, and a set of
//! options for the Octo runtime on how to run the program.
//!
//! * To compile/assemble the source code, check out the [`decasm`](https://crates.io/crates/decasm) crate.
//! * To interpret an assembled program, check out the [`deca`](https://crates.io/crates/deca) crate (backend) or
//! a program like [`termin-8`](https://crates.io/crates/termin-8) (frontend and graphics).
use octopt::Options;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::fs::File;
use std::io::Read;
use std::path::Path;
use std::str::FromStr;
use std::u8;
use thiserror::Error;
/// Representation of the payload in the Octo cartridge.
#[derive(Serialize, Deserialize, Debug)]
pub struct OctoCart {
/// The source code of the `.8o` file used to generated the Octocart, as a string of ASCII characters
pub program: String,
/// Representation of the Octo runtime settings required to run this program correctly
pub options: Options,
}
/// Represents the types of errors that can occur during decoding of an Octocart.
#[derive(Error, Debug)]
pub enum Error {
/// IO error while reading Octocart file
#[error("Failed to open file")]
IoError(#[from] std::io::Error),
/// Decoding error while reading decoding payload from Octocart
#[error("Failed to decode file")]
DecodingError(#[from] gif::DecodingError),
/// Decoding error while deserializing data from payload
#[error("Failed to parse payload")]
ParsingError(#[from] serde_json::Error),
/// Palette error
#[error("Failed to parse palette")]
PaletteError,
}
impl FromStr for OctoCart {
type Err = serde_json::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_json::from_str(s)
}
}
impl fmt::Display for OctoCart {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match serde_json::to_string(self) {
Ok(string) => write!(f, "{}", string),
_ => Err(fmt::Error),
}
}
}
/// Read and decode Octocart from a file path
///
/// # Errors
///
/// Returns `Err` if opening the file or decoding the Octocart fails.
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<OctoCart, Error> {
let file = File::open(path)?;
let string = decode_octocart(file)?;
Ok(OctoCart::from_str(&string)?)
}
/// Decodes an Octocart, and returns the decoded JSON payload as a string.
///
/// Example
/// ```no_run
/// let file = std::fs::File::open("test_octocart.gif").unwrap();
/// let payload: String = decart::decode_octocart(file).unwrap();
/// ```
/// You can deserialize this string as an [`OctoCart`]:
/// ```no_run
/// # let payload = "{\"tickrate\":7,\"maxSize\":3215,\"screenRotation\":0,\"fontStyle\":\"octo\",\"touchInputMode\":\"none\",\"fillColor\"#FFCC00\",\"fillColor2\":\"#FF6600\",\"blendColor\":\"#662200\",\"backgroundColor\"\"#996600\",\"buzzColor\":\"#FFAA00\",\"quietColor\":\"#000000\",\"shiftQuirks\":0,\"loadStoreQuirks\":0,\"jumpQuirks\":0,\"logicQuirks\":true,\"clipQuirks\":true,\"vBlankQuirks\":true}";
/// # use std::str::FromStr;
/// use decart::OctoCart;
/// let cart: OctoCart = OctoCart::from_str(payload).unwrap();
/// ```
/// Note that you can also deserialize from a file directly with [`from_file`]:
/// ```no_run
/// use decart::*;
/// let cart: OctoCart = from_file("test_octocart.gif").unwrap();
/// ```
/// # Errors
///
/// Returns `Err` if there is a GIF decoding error.
pub fn decode_octocart<R: Read>(input: R) -> Result<String, Error> {
let mut decoder = gif::DecodeOptions::new().read_info(input)?;
let global_palette = decoder
.global_palette()
.ok_or(Error::PaletteError)?
.to_vec();
let mut size: u32 = 0;
let mut first_frame = true;
let mut json_string = String::new();
'frame_loop: while let Some(frame) = decoder.read_next_frame()? {
let palette = frame.palette.as_ref().unwrap_or(&global_palette);
if first_frame {
size = ((u32::from(byte(&frame.buffer, palette, 0))) << 24)
| ((u32::from(byte(&frame.buffer, palette, 2))) << 16)
| ((u32::from(byte(&frame.buffer, palette, 4))) << 8)
| u32::from(byte(&frame.buffer, palette, 6));
json_string = String::with_capacity(size as usize);
}
for pixel in (0..frame.buffer.len()).step_by(2) {
if size == 0 {
break 'frame_loop;
}
if first_frame && pixel >= 8 {
first_frame = false;
}
json_string.push(byte(&frame.buffer, palette, pixel) as char);
size -= 1;
}
}
Ok(json_string)
}
fn nybble((r, g, b): (u8, u8, u8)) -> u8 {
((r << 3) & 8) | ((g << 1) & 6) | b & 1
}
fn byte(buffer: &[u8], palette: &[u8], i: usize) -> u8 {
(nybble(pixel_to_color(buffer[i], palette)) << 4)
| nybble(pixel_to_color(buffer[i + 1], palette))
}
fn pixel_to_color(pixel: u8, palette: &[u8]) -> (u8, u8, u8) {
(
palette[(pixel * 3) as usize],
palette[(pixel * 3) as usize + 1],
palette[(pixel * 3) as usize + 2],
)
}