Compare commits

...

47 Commits

Author SHA1 Message Date
4388c75bb8 WIP version of 8.12... 2024-09-09 22:46:51 +01:00
a36313341d pulse width 2022-11-01 19:14:19 +00:00
eee3444c4d fix note and add tests 2022-10-30 21:49:16 +00:00
5577a05191 first version of blip 2022-10-30 20:18:27 +00:00
9363be8254 update default game data for bitsy 8.0 2022-09-27 08:45:57 +01:00
a7c020f785 add note 2022-09-27 08:43:12 +01:00
8148e3f725 add blip to item 2022-09-27 08:41:03 +01:00
b8c30fe873 update copyright notice 2021-12-11 17:44:48 +00:00
b50bde1f64 better error handling for image 2021-12-11 17:44:17 +00:00
4182079a73 better documentation 2021-11-17 15:15:33 +00:00
b95d9d28d4 migrate to rust 2021 2021-11-10 15:25:17 +00:00
9188529c9f update for version 7.10 2021-11-06 09:47:07 +00:00
34bcad51dd update for version 7.5 2021-07-03 11:02:51 +01:00
81da2960af version bump 2020-11-07 15:44:51 +00:00
7199ca30f9 colour from hex 2020-11-07 15:44:35 +00:00
4a05021bdd fix broken binaries 2020-11-06 16:04:38 +00:00
de15ccbaa2 get_palette() function and test 2020-11-06 16:00:43 +00:00
b9415ade9e fix segments_from_str; version bump 2020-11-06 15:39:43 +00:00
e992e41635 get segments from borrowed str instead of String for flexibility 2020-11-06 15:37:00 +00:00
46f8831c7b tidyup 2020-10-18 18:05:14 +01:00
66cb9bdd4d font errors 2020-10-18 18:05:04 +01:00
1bbfaceeb4 give warning on missing avatar 2020-10-18 17:03:09 +01:00
e9738b98b1 sprite: from_str and error handling 2020-10-18 17:00:35 +01:00
8f558a908f better error handling for Sprite 2020-10-18 16:05:53 +01:00
fb290f07f4 make walls vec optional 2020-10-18 15:59:56 +01:00
889328f9a9 error handling for Item 2020-10-18 15:47:39 +01:00
68ecc64c7b error handling for Image 2020-10-18 15:24:48 +01:00
0dcddb9d8e make some functions public; from_str functions; impl Display; error handling for Image 2020-10-18 15:14:12 +01:00
1c5315ddad todo 2020-10-18 14:08:47 +01:00
a7a4a34ab8 error handling for exit, position, transition 2020-10-18 14:07:28 +01:00
67d4e28773 actually throw error on bad dialogue 2020-10-18 13:51:42 +01:00
d8183e29fc error handling for dialogues 2020-10-18 13:46:22 +01:00
dba84e01fa error handling for endings 2020-10-18 12:27:54 +01:00
7896ef1232 make a start toward better error handling 2020-10-17 22:47:31 +01:00
f7f08d9aba more concise import 2020-10-17 13:55:33 +01:00
8068177736 remove unused thing 2020-10-17 13:52:07 +01:00
59fc76a2d4 implement Format instead of just ToString 2020-10-17 11:42:37 +01:00
676c71cd45 alignment 2020-10-17 11:38:47 +01:00
5aa0c94810 update this accepted failure 2020-10-16 16:31:49 +01:00
ad3eb102be fix this breaking change 2020-10-16 16:31:35 +01:00
fe22e78423 concise syntax 2020-10-16 16:20:19 +01:00
fd08bff10e rewrite omnibus tests so I can ignore failing games with no title 2020-10-16 16:07:28 +01:00
1967be3635 return error instead of panicking on sprite missing position 2020-10-16 16:04:55 +01:00
eede24b13c String can be compared to &str 2020-10-16 15:35:40 +01:00
22c41fb032 whitespace 2020-10-16 13:10:16 +01:00
acfb0b6c8f tidy up Position::from_str and add test for bad data 2020-10-16 12:14:57 +01:00
a6bcc763e9 more clippy fixes 2020-10-16 11:55:15 +01:00
27 changed files with 1568 additions and 900 deletions

View File

@@ -1,8 +1,8 @@
[package] [package]
name = "bitsy-parser" name = "bitsy-parser"
version = "0.72.3" version = "0.812.0"
authors = ["Max Bradbury <max@tinybird.info>"] authors = ["Max Bradbury <max@tinybird.info>"]
edition = "2018" edition = "2021"
description = "A parser and utilities for working with Bitsy game data" description = "A parser and utilities for working with Bitsy game data"
readme = "README.md" readme = "README.md"
repository = "https://tinybird.dev/max/bitsy-parser" repository = "https://tinybird.dev/max/bitsy-parser"
@@ -12,5 +12,6 @@ keywords = ["gamedev"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
radix_fmt = "1.0.0" data-encoding = "^2.6.0"
loe = "0.2.0" radix_fmt = "^1.0.0"
loe = "0.3.0"

View File

@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2020 Max Bradbury Copyright © 2024 Max Bradbury
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

9
TODO.md Normal file
View File

@@ -0,0 +1,9 @@
# 8.0 changes
* NAME fields have moved underneath the main body of data
* "BGC *" / "BGC 1" property (transparent / palette colour index)
* flags near top of game data ("! VER_MAJ 8" etc.)
* blips
* implement from_str, display
* tunes
* implement from_str, display

View File

@@ -7,7 +7,7 @@ fn main() {
let game = env::args().nth(1).expect(SYNTAX_ERROR); let game = env::args().nth(1).expect(SYNTAX_ERROR);
let output = env::args().nth(2).expect(SYNTAX_ERROR); let output = env::args().nth(2).expect(SYNTAX_ERROR);
let mut game = Game::from(fs::read_to_string(game).unwrap()).unwrap(); let (mut game, _err) = Game::from(fs::read_to_string(game).unwrap()).unwrap();
game.dedupe_tiles(); game.dedupe_tiles();

View File

@@ -9,8 +9,8 @@ fn main() {
let output = env::args().nth(3).expect(SYNTAX_ERROR); let output = env::args().nth(3).expect(SYNTAX_ERROR);
// todo allow numerous additional games // todo allow numerous additional games
let mut game_a = Game::from(fs::read_to_string(game_a).unwrap()).unwrap(); let (mut game_a, _) = Game::from(fs::read_to_string(game_a).unwrap()).unwrap();
let game_b = Game::from(fs::read_to_string(game_b).unwrap()).unwrap(); let ( game_b, _) = Game::from(fs::read_to_string(game_b).unwrap()).unwrap();
game_a.merge(&game_b); game_a.merge(&game_b);

View File

@@ -8,7 +8,7 @@ fn main() {
let input = env::args().nth(1).expect(SYNTAX_ERROR); let input = env::args().nth(1).expect(SYNTAX_ERROR);
let output = env::args().nth(2).expect(SYNTAX_ERROR); let output = env::args().nth(2).expect(SYNTAX_ERROR);
let game = Game::from(fs::read_to_string(input).unwrap()).unwrap(); let (game, _err) = Game::from(fs::read_to_string(input).unwrap()).unwrap();
fs::write(output, game.to_string()).expect("Failed to write output file"); fs::write(output, game.to_string()).expect("Failed to write output file");
} }

76
src/blip.rs Normal file
View File

@@ -0,0 +1,76 @@
use core::fmt;
use std::fmt::Formatter;
use crate::note::Note;
#[derive(Clone, Debug, PartialEq)]
pub enum PulseWidth {
/// 50% duty cycle
Half,
/// 25% duty cycle
Quarter,
/// 12.5% duty cycle
Eighth,
}
impl PulseWidth {
fn from(str: &str) -> Result<PulseWidth, crate::Error> {
match str {
"P2" => Ok(PulseWidth::Half),
"P4" => Ok(PulseWidth::Quarter),
"P8" => Ok(PulseWidth::Eighth),
_ => Err(crate::Error::PulseWidth),
}
}
}
impl fmt::Display for PulseWidth {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "SQR {}", match self {
PulseWidth::Half => "P2",
PulseWidth::Quarter => "P4",
PulseWidth::Eighth => "P8",
})
}
}
/// thanks to Rumple_Frumpkins from Bitsy Talk for his help in figuring out the blip format.
#[derive(Debug, Clone, PartialEq)]
pub struct Blip {
id: String,
notes: Vec<Note>,
name: Option<String>,
/// Attack (ms), Decay (ms), Sustain (level: 1-15), Hold (sustain duration, ms), Release (ms)
envelope: [u8; 5],
/// first value is milliseconds per note;
/// second value is a modifier to the first note (add or subtract milliseconds)
beat: [i16; 2],
pulse_width: PulseWidth,
/// Notes can cycle repeatedly, or just play once
repeat: bool,
}
impl From<&str> for Blip {
fn from(str: &str) -> Self {
let mut id = String::new();
let mut notes = vec![];
let mut name = None;
let mut envelope = [0; 5];
let mut beat = [0; 2];
let mut pulse_width = PulseWidth::Half;
let mut repeat = false;
for line in str.lines() {
if line.starts_with("BLIP ") {
id = line.replace("BLIP ", "");
} else if line.starts_with("NAME ") {
name = Some(line.replace("NAME ", ""));
} else if line.starts_with("ENV ") {
let envelope_temp: Vec<u8> = line.replace("ENV ", "").split(' ').map(|v| v.parse().unwrap()).collect();
} else {
// notes = line.split(',').map(|n| Note::from(n)).collect().unwrap();
}
}
Self { id, notes, name, envelope, beat, pulse_width, repeat }
}
}

View File

@@ -5,15 +5,12 @@ pub struct Colour {
pub blue: u8, pub blue: u8,
} }
#[derive(Debug)]
pub struct InvalidRgb;
impl Colour { impl Colour {
pub(crate) fn from(string: &str) -> Result<Colour, InvalidRgb> { pub fn from(string: &str) -> Result<Colour, crate::Error> {
let values: Vec<&str> = string.trim_matches(',').split(',').collect(); let values: Vec<&str> = string.trim_matches(',').split(',').collect();
if values.len() != 3 { if values.len() != 3 {
return Err(InvalidRgb); return Err(crate::Error::Colour);
} }
let red: u8 = values[0].parse().unwrap_or(0); let red: u8 = values[0].parse().unwrap_or(0);
@@ -22,6 +19,13 @@ impl Colour {
Ok(Colour { red, green, blue }) Ok(Colour { red, green, blue })
} }
pub fn from_hex(hex: &str) -> Result<Colour, crate::Error> {
let hex = hex.to_lowercase().trim_start_matches('#').to_string();
let rgb = data_encoding::HEXLOWER.decode(hex.as_bytes()).unwrap();
Ok(Colour { red: rgb[0], green: rgb[1], blue: rgb[2], })
}
} }
impl ToString for Colour { impl ToString for Colour {
@@ -61,4 +65,18 @@ mod test {
fn colour_extraneous_value() { fn colour_extraneous_value() {
assert!(Colour::from("0,0,0,0").is_err()); assert!(Colour::from("0,0,0,0").is_err());
} }
#[test]
fn colour_from_hex() {
let output = Colour::from_hex("#ffff00").unwrap();
let expected = Colour { red: 255, green: 255, blue: 0 };
assert_eq!(output, expected);
}
#[test]
fn colour_from_hex_upper() {
let output = Colour::from_hex("#ABCDEF").unwrap();
let expected = Colour { red: 171, green: 205, blue: 239 };
assert_eq!(output, expected);
}
} }

View File

@@ -1,5 +1,7 @@
use crate::optional_data_line; use crate::optional_data_line;
use std::fmt;
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub struct Dialogue { pub struct Dialogue {
pub id: String, pub id: String,
@@ -7,27 +9,35 @@ pub struct Dialogue {
pub name: Option<String>, pub name: Option<String>,
} }
impl From<String> for Dialogue { impl Dialogue {
#[inline] pub fn from_str(str: &str) -> Result<Dialogue, crate::Error> {
fn from(string: String) -> Dialogue { let mut lines: Vec<&str> = str.lines().collect();
let mut lines: Vec<&str> = string.lines().collect();
if lines.is_empty() || !lines[0].starts_with("DLG ") {
return Err(crate::Error::Dialogue);
}
let id = lines[0].replace("DLG ", ""); let id = lines[0].replace("DLG ", "");
let name = if lines.last().unwrap().starts_with("NAME ") { let last_line = lines.pop().unwrap();
Some(lines.pop().unwrap().replace("NAME ", ""))
let name = if last_line.starts_with("NAME ") {
Some(last_line.replace("NAME ", ""))
} else { } else {
lines.push(last_line);
None None
}; };
let contents = lines[1..].join("\n"); let contents = lines[1..].join("\n");
Dialogue { id, contents, name } Ok(Dialogue { id, contents, name })
} }
} }
impl ToString for Dialogue { impl fmt::Display for Dialogue {
fn to_string(&self) -> String { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
format!( write!(
f,
"DLG {}\n{}{}", "DLG {}\n{}{}",
self.id, self.id,
self.contents, self.contents,
@@ -41,10 +51,10 @@ mod test {
use crate::Dialogue; use crate::Dialogue;
#[test] #[test]
fn dialogue_from_string() { fn dialogue_from_str() {
let output = Dialogue::from( let output = Dialogue::from_str(
"DLG h\nhello\nNAME not a dialogue name\nNAME a dialogue name".to_string() "DLG h\nhello\nNAME not a dialogue name\nNAME a dialogue name"
); ).unwrap();
let expected = Dialogue { let expected = Dialogue {
id: "h".to_string(), id: "h".to_string(),
@@ -63,7 +73,7 @@ mod test {
name: Some("a dialogue name".to_string()) name: Some("a dialogue name".to_string())
}.to_string(); }.to_string();
let expected = "DLG y\nThis is a bit of dialogue,\nblah blah\nblah blah\nNAME a dialogue name".to_string(); let expected = "DLG y\nThis is a bit of dialogue,\nblah blah\nblah blah\nNAME a dialogue name";
assert_eq!(output, expected); assert_eq!(output, expected);
} }

View File

@@ -1,6 +1,4 @@
use std::fmt; use std::fmt;
use std::error::Error;
use std::str::FromStr;
// same as a dialogue basically // same as a dialogue basically
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
@@ -9,13 +7,14 @@ pub struct Ending {
pub dialogue: String, pub dialogue: String,
} }
impl Error for Ending {} impl Ending {
pub fn from_str(s: &str) -> Result<Self, crate::Error> {
impl FromStr for Ending {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let lines: Vec<&str> = s.lines().collect(); let lines: Vec<&str> = s.lines().collect();
if lines.is_empty() || !lines[0].starts_with("END ") {
return Err(crate::Error::Ending);
}
let id = lines[0].replace("END ", ""); let id = lines[0].replace("END ", "");
let dialogue = lines[1..].join("\n"); let dialogue = lines[1..].join("\n");
@@ -32,7 +31,6 @@ impl fmt::Display for Ending {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::Ending; use crate::Ending;
use std::str::FromStr;
#[test] #[test]
fn ending_from_string() { fn ending_from_string() {

63
src/error.rs Normal file
View File

@@ -0,0 +1,63 @@
use std::fmt;
#[derive(Clone, Debug, PartialEq)]
pub enum NotFound {
Anything,
Avatar,
Room,
Sprite,
Tile,
}
impl fmt::Display for NotFound {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f,"Not found: {} data", match self {
NotFound::Anything => "game",
NotFound::Avatar => "avatar",
NotFound::Room => "room",
NotFound::Sprite => "sprite",
NotFound::Tile => "tile",
})
}
}
#[derive(Clone, Debug)]
pub enum ImageError {
MalformedPixel,
WrongSize,
}
#[derive(Clone, Debug)]
pub enum Error {
Colour,
Dialogue,
Ending,
Exit,
Font,
Game {
missing: NotFound,
},
Image {
err: ImageError,
},
Item,
Palette,
Position,
PulseWidth,
RelativeNote,
Room,
Sprite,
Text,
Tile,
Transition,
Variable,
Version,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "")
}
}
impl std::error::Error for Error {}

View File

@@ -1,6 +1,5 @@
use crate::Position; use crate::Position;
use std::str::FromStr;
use std::error::Error;
use std::fmt; use std::fmt;
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
@@ -16,36 +15,35 @@ pub enum Transition {
SlideRight, SlideRight,
} }
impl From<&str> for Transition { impl Transition {
fn from(str: &str) -> Transition { pub fn from_str(str: &str) -> Result<Transition, crate::Error> {
match str { match str {
"fade_w" => Transition::FadeToWhite, "fade_w" => Ok(Transition::FadeToWhite),
"fade_b" => Transition::FadeToBlack, "fade_b" => Ok(Transition::FadeToBlack),
"wave" => Transition::Wave, "wave" => Ok(Transition::Wave),
"tunnel" => Transition::Tunnel, "tunnel" => Ok(Transition::Tunnel),
"slide_u" => Transition::SlideUp, "slide_u" => Ok(Transition::SlideUp),
"slide_d" => Transition::SlideDown, "slide_d" => Ok(Transition::SlideDown),
"slide_l" => Transition::SlideLeft, "slide_l" => Ok(Transition::SlideLeft),
"slide_r" => Transition::SlideRight, "slide_r" => Ok(Transition::SlideRight),
_ => Transition::None, _ => Err(crate::Error::Transition),
} }
} }
} }
impl ToString for Transition { impl fmt::Display for Transition {
fn to_string(&self) -> String { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self { write!(f, "{}", match &self {
Transition::FadeToWhite => " FX fade_w", Transition::FadeToWhite => " FX fade_w",
Transition::FadeToBlack => " FX fade_b", Transition::FadeToBlack => " FX fade_b",
Transition::Wave => " FX wave", Transition::Wave => " FX wave",
Transition::Tunnel => " FX tunnel", Transition::Tunnel => " FX tunnel",
Transition::SlideUp => " FX slide_u", Transition::SlideUp => " FX slide_u",
Transition::SlideDown => " FX slide_d", Transition::SlideDown => " FX slide_d",
Transition::SlideLeft => " FX slide_l", Transition::SlideLeft => " FX slide_l",
Transition::SlideRight => " FX slide_r", Transition::SlideRight => " FX slide_r",
Transition::None => "", Transition::None => "",
} })
.to_string()
} }
} }
@@ -58,24 +56,21 @@ pub struct Exit {
pub effect: Transition, pub effect: Transition,
} }
impl Error for Exit {} impl Exit {
pub fn from_str(s: &str) -> Result<Self, crate::Error> {
let parts: Vec<&str> = s.split_whitespace().collect();
impl FromStr for Exit { if parts.len() < 2 {
type Err = String; return Err(crate::Error::Exit);
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut parts = s.split_whitespace();
let room_id = parts.next().unwrap().to_string();
let position = Position::from_str(parts.next().unwrap());
if position.is_err() {
return Err("Invalid position for exit".to_string());
} }
let position = position.unwrap(); let mut parts = parts.iter();
let room_id = parts.next().unwrap().to_string();
let position = Position::from_str(parts.next().unwrap())?;
let effect = if parts.next().is_some() { let effect = if parts.next().is_some() {
Transition::from(parts.next().unwrap()) Transition::from_str(parts.next().unwrap())?
} else { } else {
Transition::None Transition::None
}; };
@@ -99,7 +94,6 @@ impl fmt::Display for Exit {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::{Transition, Exit, Position}; use crate::{Transition, Exit, Position};
use std::str::FromStr;
#[test] #[test]
fn exit_from_string() { fn exit_from_string() {

View File

@@ -1,12 +1,15 @@
use crate::{Dialogue, Ending, Font, Image, Item, Palette, Room, Sprite, TextDirection, Tile, Variable, transform_line_endings, segments_from_string, new_unique_id, try_id, Instance}; use crate::{
Blip, Dialogue, Ending, Font, Image, Item, Palette,
Room, Sprite, TextDirection, Tile, Tune, Variable, Instance, Error,
transform_line_endings, new_unique_id, try_id
};
use crate::error::NotFound;
use loe::TransformMode; use loe::TransformMode;
use std::str::FromStr;
use std::collections::HashMap; use std::collections::HashMap;
use std::borrow::BorrowMut; use std::borrow::BorrowMut;
use std::fmt; use std::fmt;
use std::fmt::{Display, Formatter}; use std::fmt::Display;
/// in very early versions of Bitsy, room tiles were defined as single alphanumeric characters - /// in very early versions of Bitsy, room tiles were defined as single alphanumeric characters -
/// so there was a maximum of 36 unique tiles. later versions are comma-separated. /// so there was a maximum of 36 unique tiles. later versions are comma-separated.
@@ -30,9 +33,9 @@ impl RoomFormat {
impl Display for RoomFormat { impl Display for RoomFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", match &self { write!(f, "{}", match &self {
RoomFormat::Contiguous => "0", RoomFormat::Contiguous => 0,
RoomFormat::CommaSeparated => "1", RoomFormat::CommaSeparated => 1,
}) }.to_string())
} }
} }
@@ -40,12 +43,13 @@ impl Display for RoomFormat {
#[derive(Debug, Eq, PartialEq, Copy, Clone)] #[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub enum RoomType {Room, Set} pub enum RoomType {Room, Set}
impl ToString for RoomType { impl Display for RoomType {
fn to_string(&self) -> String { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self { let str = match &self {
RoomType::Set => "SET", RoomType::Set => "SET",
RoomType::Room => "ROOM", RoomType::Room => "ROOM",
}.to_string() }.to_string();
write!(f, "{}", str)
} }
} }
@@ -56,41 +60,37 @@ pub struct Version {
} }
#[derive(Debug)] #[derive(Debug)]
pub struct InvalidVersion; pub enum VersionError {
MissingParts,
ExtraneousParts,
MalformedInteger,
}
impl Version { impl Display for VersionError {
fn from(str: &str) -> Result<Version, InvalidVersion> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let parts: Vec<&str> = str.split(".").collect(); write!(f, "{}", match self {
if parts.len() == 2 { VersionError::MissingParts => "Not enough parts supplied for version",
Ok(Version { VersionError::ExtraneousParts => "Too many parts supplied for version",
major: parts[0].parse().unwrap(), VersionError::MalformedInteger => "Version did not contain valid integers",
minor: parts[1].parse().unwrap(), })
})
} else {
Err (InvalidVersion)
}
} }
} }
#[derive(Debug, PartialEq)] impl std::error::Error for VersionError {}
pub enum NotFound {
/// no game data whatsoever
Anything,
Avatar,
Room,
Sprite,
Tile,
}
impl Display for NotFound { impl Version {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn from(str: &str) -> Result<Version, VersionError> {
write!(f,"Not found: {} data", match self { let parts: Vec<&str> = str.split('.').collect();
&NotFound::Anything => "game",
&NotFound::Avatar => "avatar", if parts.len() < 2 {
&NotFound::Room => "room", Err(VersionError::MissingParts)
&NotFound::Sprite => "sprite", } else if parts.len() > 2 {
&NotFound::Tile => "tile", Err(VersionError::ExtraneousParts)
}) } else if let (Ok(major), Ok(minor)) = (parts[0].parse(), parts[1].parse()) {
Ok(Version { major, minor })
} else {
Err(VersionError::MalformedInteger)
}
} }
} }
@@ -98,10 +98,18 @@ impl Display for NotFound {
pub struct Game { pub struct Game {
pub name: String, pub name: String,
pub version: Option<Version>, pub version: Option<Version>,
/// it's a bit weird that we have the version twice now, but whatever. only in 8.0+
pub version_major: Option<u8>,
pub version_minor: Option<u8>,
pub room_format: Option<RoomFormat>, pub room_format: Option<RoomFormat>,
/// not sure what this does, could be either a boolean or an int
pub dialogue_compatibility: Option<usize>,
/// not sure what this does, could be either a boolean or an int
pub text_mode: Option<usize>,
pub(crate) room_type: RoomType, pub(crate) room_type: RoomType,
pub font: Font, pub font: Font,
pub custom_font: Option<String>, // used if font is Font::Custom /// used if font is `Font::Custom`
pub custom_font: Option<String>,
pub text_direction: TextDirection, pub text_direction: TextDirection,
pub palettes: Vec<Palette>, pub palettes: Vec<Palette>,
pub rooms: Vec<Room>, pub rooms: Vec<Room>,
@@ -111,31 +119,30 @@ pub struct Game {
pub dialogues: Vec<Dialogue>, pub dialogues: Vec<Dialogue>,
pub endings: Vec<Ending>, pub endings: Vec<Ending>,
pub variables: Vec<Variable>, pub variables: Vec<Variable>,
pub tunes: Vec<Tune>,
pub blips: Vec<Blip>,
pub font_data: Option<String>, // todo make this an actual struct for parsing pub font_data: Option<String>, // todo make this an actual struct for parsing
pub(crate) line_endings_crlf: bool, // otherwise lf (unix/mac) /// true if CRLF (Windows), otherwise LF (unix/mac)
/// todo use the enum?
pub(crate) line_endings_crlf: bool,
} }
#[derive(Debug)]
pub struct GameHasNoAvatar;
// todo no tiles? no rooms? no palettes? turn this into an enum?
impl Game { impl Game {
// todo return (Result<Game, ?>, Vec<Box<dyn Error>>)? pub fn from(string: String) -> Result<(Game, Vec<Error>), NotFound> {
// would be nice to *try* to parse a game, and catalogue any and all errors without crashing, if string.trim() == "" {
// for display purposes etc.
pub fn from(string: String) -> Result<Game, NotFound> {
if string.clone().trim() == "".to_string() {
return Err(NotFound::Anything); return Err(NotFound::Anything);
} }
let mut warnings = Vec::new();
let line_endings_crlf = string.contains("\r\n"); let line_endings_crlf = string.contains("\r\n");
let mut string = string; let mut string = string;
if line_endings_crlf { if line_endings_crlf {
string = transform_line_endings(string, TransformMode::LF) string = transform_line_endings(string, TransformMode::Lf)
} }
let string = string.trim_start_matches("\n").to_string(); let string = string.trim_start_matches('\n').to_string();
let mut segments = segments_from_string(string); let mut segments = crate::segments_from_str(&string);
let mut name = "".to_string(); let mut name = "".to_string();
@@ -171,7 +178,11 @@ impl Game {
let mut font_data: Option<String> = None; let mut font_data: Option<String> = None;
let mut version = None; let mut version = None;
let mut version_major = None;
let mut version_minor = None;
let mut room_format = None; let mut room_format = None;
let mut dialogue_compatibility = None;
let mut text_mode = None;
let mut room_type = RoomType::Room; let mut room_type = RoomType::Room;
let mut font = Font::AsciiSmall; let mut font = Font::AsciiSmall;
let mut custom_font = None; let mut custom_font = None;
@@ -183,18 +194,55 @@ impl Game {
let mut items: Vec<Item> = Vec::new(); let mut items: Vec<Item> = Vec::new();
let mut avatar_exists = false; let mut avatar_exists = false;
// todo can we use multithreading here?
for segment in segments { for segment in segments {
if segment.starts_with("# BITSY VERSION") { if segment.starts_with("# BITSY VERSION") {
let segment = segment.replace("# BITSY VERSION ", ""); let segment = segment.replace("# BITSY VERSION ", "");
let segment = Version::from(&segment); let result = Version::from(&segment);
if segment.is_ok() {
version = Some(segment.unwrap()); if let Ok(v) = result {
version = Some(v);
} else {
warnings.push(Error::Version);
}
} else if segment.starts_with("! ") {
// this is (potentially?) an entire block,
// so we need to split it into lines and deal with the lines individually
for line in segment.lines() {
if line.starts_with("! VER_MAJ") {
let line = line.replace("! VER_MAJ ", "");
version_major = Some(
line.parse().expect("Couldn't parse major version")
);
} else if line.starts_with("! VER_MIN") {
let line = line.replace("! VER_MIN ", "");
version_minor = Some(
line.parse().expect("Couldn't parse minor version")
);
} else if line.starts_with("! ROOM_FORMAT") {
let line = line.replace("! ROOM_FORMAT ", "");
room_format = Some(
RoomFormat::from(&line).unwrap_or(RoomFormat::CommaSeparated)
);
} else if line.starts_with("! DLG_COMPAT") {
let line = line.replace("! DLG_COMPAT ", "");
// not sure if this is supposed to be a boolean or a version number
dialogue_compatibility = Some(
line.parse().expect("Couldn't parse dialogue compatibility")
);
} else if line.starts_with("! TXT_MODE") {
let line = line.replace("! TXT_MODE ", "");
// not sure if this is supposed to be a boolean or a version number
text_mode = Some(
line.parse().expect("Couldn't parse text mode")
);
}
} }
} else if segment.starts_with("! ROOM_FORMAT") {
let segment = segment.replace("! ROOM_FORMAT ", "");
room_format = Some(
RoomFormat::from(&segment).unwrap_or(RoomFormat::CommaSeparated)
);
} else if segment.starts_with("DEFAULT_FONT") { } else if segment.starts_with("DEFAULT_FONT") {
let segment = segment.replace("DEFAULT_FONT ", ""); let segment = segment.replace("DEFAULT_FONT ", "");
@@ -203,10 +251,16 @@ impl Game {
if font == Font::Custom { if font == Font::Custom {
custom_font = Some(segment.to_string()); custom_font = Some(segment.to_string());
} }
} else if segment.trim() == "TEXT_DIRECTION RTL".to_string() { } else if segment.trim() == "TEXT_DIRECTION RTL" {
text_direction = TextDirection::RightToLeft; text_direction = TextDirection::RightToLeft;
} else if segment.starts_with("PAL ") { } else if segment.starts_with("PAL ") {
palettes.push(Palette::from(segment)); let result = Palette::from_str(&segment);
if let Ok((palette, mut errors)) = result {
palettes.push(palette);
warnings.append(&mut errors);
} else {
warnings.push(result.unwrap_err());
}
} else if segment.starts_with("ROOM ") || segment.starts_with("SET ") { } else if segment.starts_with("ROOM ") || segment.starts_with("SET ") {
if segment.starts_with("SET ") { if segment.starts_with("SET ") {
room_type = RoomType::Set; room_type = RoomType::Set;
@@ -215,22 +269,39 @@ impl Game {
} else if segment.starts_with("TIL ") { } else if segment.starts_with("TIL ") {
tiles.push(Tile::from(segment)); tiles.push(Tile::from(segment));
} else if segment.starts_with("SPR ") { } else if segment.starts_with("SPR ") {
let sprite = Sprite::from(segment); let result = Sprite::from_str(&segment);
if sprite.is_ok() {
let sprite = sprite.unwrap(); if let Ok(sprite) = result {
if ! avatar_exists && sprite.id == "A".to_string() { avatar_exists |= sprite.id == "A";
avatar_exists = true;
}
sprites.push(sprite); sprites.push(sprite);
} else {
warnings.push(result.unwrap_err());
} }
} else if segment.starts_with("ITM ") { } else if segment.starts_with("ITM ") {
items.push(Item::from(segment)); let result = Item::from_str(&segment);
if let Ok((item, mut item_warnings)) = result {
items.push(item);
warnings.append(&mut item_warnings);
} else {
warnings.push(result.unwrap_err());
}
} else if segment.starts_with("DLG ") { } else if segment.starts_with("DLG ") {
dialogues.push(Dialogue::from(segment)); let result = Dialogue::from_str(&segment);
if let Ok(dialogue) = result {
dialogues.push(dialogue);
} else {
warnings.push(result.unwrap_err());
}
} else if segment.starts_with("END ") { } else if segment.starts_with("END ") {
let ending = Ending::from_str(&segment); let result = Ending::from_str(&segment);
if ending.is_ok() {
endings.push(ending.unwrap()); if let Ok(ending) = result {
endings.push(ending);
} else {
warnings.push(result.unwrap_err());
} }
} else if segment.starts_with("VAR ") { } else if segment.starts_with("VAR ") {
variables.push(Variable::from(segment)); variables.push(Variable::from(segment));
@@ -240,29 +311,38 @@ impl Game {
} }
if ! avatar_exists { if ! avatar_exists {
return Err(NotFound::Avatar); warnings.push(Error::Game { missing: NotFound::Avatar });
} }
Ok( Ok(
Game { (
name, Game {
version, name,
room_format, version,
room_type, version_major,
font, version_minor,
custom_font, room_format,
text_direction, dialogue_compatibility,
palettes, text_mode,
rooms, room_type,
tiles, font,
sprites, custom_font,
items, text_direction,
dialogues, palettes,
endings, rooms,
variables, tiles,
font_data, sprites,
line_endings_crlf, items,
} dialogues,
endings,
variables,
tunes: vec![],
blips: vec![],
font_data,
line_endings_crlf,
},
warnings
)
) )
} }
@@ -272,10 +352,9 @@ impl Game {
|sprite| sprite.id == id |sprite| sprite.id == id
); );
if index.is_some() { match index {
Ok(&self.sprites[index.unwrap()]) Some(index) => Ok(&self.sprites[index]),
} else { None => Err(NotFound::Sprite),
Err(NotFound::Sprite)
} }
} }
@@ -284,10 +363,9 @@ impl Game {
|tile| tile.id == id |tile| tile.id == id
); );
if index.is_some() { match index {
Ok(&self.tiles[index.unwrap()]) Some(index) => Ok(&self.tiles[index]),
} else { None => Err(NotFound::Tile),
Err(NotFound::Tile)
} }
} }
@@ -296,10 +374,9 @@ impl Game {
|room| room.id == id |room| room.id == id
); );
if index.is_some() { match index {
Ok(&self.rooms[index.unwrap()]) Some(index) => Ok(&self.rooms[index]),
} else { None => Err(NotFound::Room),
Err(NotFound::Room)
} }
} }
@@ -312,9 +389,8 @@ impl Game {
let mut tiles: Vec<&Tile> = Vec::new(); let mut tiles: Vec<&Tile> = Vec::new();
for id in ids { for id in ids {
let tile = self.get_tile_by_id(id); if let Ok(tile) = self.get_tile_by_id(id) {
if tile.is_ok() { tiles.push(tile);
tiles.push(tile.unwrap());
} }
} }
@@ -322,20 +398,17 @@ impl Game {
} }
pub fn get_tiles_for_room(&self, id: String) -> Result<Vec<&Tile>, NotFound> { pub fn get_tiles_for_room(&self, id: String) -> Result<Vec<&Tile>, NotFound> {
let room = self.get_room_by_id(id); let room = self.get_room_by_id(id)?;
if room.is_err() { let mut tile_ids = room.tiles.clone();
return Err(NotFound::Room);
}
let mut tile_ids = room.unwrap().tiles.clone();
tile_ids.sort(); tile_ids.sort();
tile_ids.dedup(); tile_ids.dedup();
// remove 0 as this isn't a real tile
let zero_index = tile_ids.iter() // remove "0" as this isn't a real tile
.position(|i| i == &"0".to_string()); if let Some(zero_index) = tile_ids.iter().position(|i| i == "0") {
if zero_index.is_some() { tile_ids.remove(zero_index);
tile_ids.remove(zero_index.unwrap());
} }
// remove Ok once this function returns a result
// todo remove Ok once get_tiles_by_ids returns a Result
Ok(self.get_tiles_by_ids(tile_ids)) Ok(self.get_tiles_by_ids(tile_ids))
} }
@@ -428,11 +501,9 @@ impl Game {
item.id = item_id_changes[&item.id].clone(); item.id = item_id_changes[&item.id].clone();
} }
if item.dialogue_id.is_some() { if let Some(key) = item.dialogue_id.clone() {
let key = item.dialogue_id.clone().unwrap(); if let Some(change) = dialogue_id_changes.get(&key) {
let change = dialogue_id_changes.get(&key); item.dialogue_id = Some(change.clone());
if change.is_some() {
item.dialogue_id = Some(change.unwrap().clone());
} }
} }
@@ -443,7 +514,7 @@ impl Game {
// to insert any new room, we need to know the new IDs of every room // to insert any new room, we need to know the new IDs of every room
// to maintain the integrity of exits and endings // to maintain the integrity of exits and endings
let mut all_room_ids = self.room_ids().clone(); let mut all_room_ids = self.room_ids();
for room in &game.rooms { for room in &game.rooms {
let old = room.id.clone(); let old = room.id.clone();
@@ -457,16 +528,13 @@ impl Game {
for room in &game.rooms { for room in &game.rooms {
let mut room = room.clone(); let mut room = room.clone();
let room_id_change = room_id_changes.get(&room.id); if let Some(room_id_change) = room_id_changes.get(&room.id) {
if room_id_change.is_some() { room.id = room_id_change.clone();
room.id = room_id_change.unwrap().clone();
} }
if room.palette_id.is_some() { if let Some(key) = room.palette_id.clone() {
let key = room.palette_id.clone().unwrap(); if let Some(change) = palette_id_changes.get(&key) {
let change = palette_id_changes.get(&key); room.palette_id = Some(change.clone());
if change.is_some() {
room.palette_id = Some(change.unwrap().clone());
} }
} }
@@ -487,16 +555,14 @@ impl Game {
let mut exit = exit.clone(); let mut exit = exit.clone();
let key = exit.exit.room_id.clone(); let key = exit.exit.room_id.clone();
let change = room_id_changes.get(&key);
if change.is_some() { if let Some(change) = room_id_changes.get(&key) {
exit.exit.room_id = change.unwrap().clone(); exit.exit.room_id = change.clone();
} }
if exit.dialogue_id.is_some() { if let Some(key) = exit.dialogue_id.clone() {
let key = exit.dialogue_id.clone().unwrap(); if let Some(dialogue_change) = dialogue_id_changes.get(&key) {
let dialogue_change = dialogue_id_changes.get(&key); exit.dialogue_id = Some(dialogue_change.clone());
if dialogue_change.is_some() {
exit.dialogue_id = Some(dialogue_change.unwrap().clone());
} }
} }
@@ -506,37 +572,35 @@ impl Game {
room.endings = room.endings.iter().map(|ending| { room.endings = room.endings.iter().map(|ending| {
let mut ending = ending.clone(); let mut ending = ending.clone();
let key = ending.id.clone(); let key = ending.id.clone();
let change = ending_id_changes.get(&key);
if change.is_some() { if let Some(change) = ending_id_changes.get(&key) {
ending.id = change.unwrap().clone(); ending.id = change.clone();
} }
ending ending
}).collect(); }).collect();
self.add_room(room); self.add_room(room);
} }
// a sprite has a dialogue ID, so we need to handle these after dialogues // a sprite has a dialogue ID, so we need to handle sprites after dialogues
// a sprite has a position in a room, so we need to handle these after the rooms // a sprite has a position in a room, so we need to handle sprites after rooms
for sprite in &game.sprites { for sprite in &game.sprites {
let mut sprite = sprite.clone(); let mut sprite = sprite.clone();
// avoid having two avatars // avoid having two avatars
if sprite.id == "A".to_string() { if sprite.id == "A" {
sprite.id = "0".to_string(); // just a default value for replacement sprite.id = "0".to_string(); // just a default value for later replacement
} }
if sprite.dialogue_id.is_some() { if let Some(key) = sprite.dialogue_id.clone() {
let key = sprite.dialogue_id.clone().unwrap();
if dialogue_id_changes.contains_key(&key) { if dialogue_id_changes.contains_key(&key) {
sprite.dialogue_id = Some(dialogue_id_changes[&key].clone()); sprite.dialogue_id = Some(dialogue_id_changes[&key].clone());
} }
} }
if sprite.room_id.is_some() { if let Some(key) = sprite.room_id.clone() {
let key = sprite.room_id.clone().unwrap(); if let Some(change) = room_id_changes.get(&key) {
let change = room_id_changes.get(&key); sprite.room_id = Some(change.clone());
if change.is_some() {
sprite.room_id = Some(change.unwrap().clone());
} }
} }
@@ -547,8 +611,8 @@ impl Game {
} }
} }
impl ToString for Game { impl Display for Game {
fn to_string(&self) -> String { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut segments: Vec<String> = Vec::new(); let mut segments: Vec<String> = Vec::new();
// todo refactor // todo refactor
@@ -574,7 +638,9 @@ impl ToString for Game {
} }
for dialogue in &self.dialogues { for dialogue in &self.dialogues {
// this replacement is silly but see segments_from_string() for explanation // some dialogues are multiline (starting/ending with `"""`) but have no contents
// and this kinda messes things up when trying to export unmodified dialogues
// in their original format for testing purposes
segments.push(dialogue.to_string().replace("\"\"\"\n\"\"\"", "")); segments.push(dialogue.to_string().replace("\"\"\"\n\"\"\"", ""));
} }
@@ -590,18 +656,23 @@ impl ToString for Game {
segments.push(self.font_data.to_owned().unwrap()) segments.push(self.font_data.to_owned().unwrap())
} }
transform_line_endings( let str = transform_line_endings(
format!( format!(
"{}{}{}{}{}\n\n{}\n\n", "{}{}{}{}{}{}{}{}{}\n\n{}\n\n",
&self.name, &self.name,
&self.version_line(), &self.version_line(),
&self.room_format_line(), &self.version_major_line(),
&self.version_minor_line(),
&self.dialogue_compatibility_line(),
&self.text_mode_line(),
&self.version_minor_line(),
&self.font_line(), &self.font_line(),
&self.text_direction_line(), &self.text_direction_line(),
segments.join("\n\n"), segments.join("\n\n"),
), ),
if self.line_endings_crlf {TransformMode::CRLF} else {TransformMode::LF} if self.line_endings_crlf { TransformMode::Crlf } else { TransformMode::Lf }
) );
write!(f, "{}", str)
} }
} }
@@ -618,6 +689,7 @@ impl Game {
pub fn sprite_ids(&self) -> Vec<String> { pub fn sprite_ids(&self) -> Vec<String> {
self.sprites.iter().map(|sprite| sprite.id.clone()).collect() self.sprites.iter().map(|sprite| sprite.id.clone()).collect()
} }
pub fn room_ids(&self) -> Vec<String> { pub fn room_ids(&self) -> Vec<String> {
self.rooms.iter().map(|room| room.id.clone()).collect() self.rooms.iter().map(|room| room.id.clone()).collect()
} }
@@ -678,6 +750,10 @@ impl Game {
new_unique_id(self.variable_ids()) new_unique_id(self.variable_ids())
} }
pub fn get_palette(&self, id: &str) -> Option<&Palette> {
self.palettes.iter().find(|palette| palette.id == id)
}
/// todo refactor? /// todo refactor?
pub fn get_tile_id(&self, matching_tile: &Tile) -> Option<String> { pub fn get_tile_id(&self, matching_tile: &Tile) -> Option<String> {
for tile in &self.tiles { for tile in &self.tiles {
@@ -689,8 +765,8 @@ impl Game {
None None
} }
pub fn find_tile_with_animation(&self, animation: &Vec<Image>) -> Option<&Tile> { pub fn find_tile_with_animation(&self, animation: &[Image]) -> Option<&Tile> {
self.tiles.iter().find(|&tile| &tile.animation_frames == animation) self.tiles.iter().find(|&tile| tile.animation_frames.as_slice() == animation)
} }
/// adds a palette safely and returns the ID /// adds a palette safely and returns the ID
@@ -705,12 +781,13 @@ impl Game {
/// adds a tile safely and returns the ID /// adds a tile safely and returns the ID
pub fn add_tile(&mut self, mut tile: Tile) -> String { pub fn add_tile(&mut self, mut tile: Tile) -> String {
if tile.id == "0".to_string() || self.tile_ids().contains(&tile.id) { if tile.id == "0" || self.tile_ids().contains(&tile.id) {
let new_id = self.new_tile_id(); let new_id = self.new_tile_id();
if new_id != tile.id { if new_id != tile.id {
tile.id = new_id; tile.id = new_id;
} }
} }
let id = tile.id.clone(); let id = tile.id.clone();
self.tiles.push(tile); self.tiles.push(tile);
id id
@@ -784,7 +861,7 @@ impl Game {
let mut unique_tiles: Vec<Tile> = Vec::new(); let mut unique_tiles: Vec<Tile> = Vec::new();
let mut tile_id_changes: HashMap<String, String> = HashMap::new(); let mut tile_id_changes: HashMap<String, String> = HashMap::new();
while tiles_temp.len() > 0 { while !tiles_temp.is_empty() {
let tile = tiles_temp.pop().unwrap(); let tile = tiles_temp.pop().unwrap();
if tile == crate::mock::tile_background() { if tile == crate::mock::tile_background() {
@@ -812,16 +889,49 @@ impl Game {
if self.version.is_some() { if self.version.is_some() {
format!( format!(
"\n\n# BITSY VERSION {}.{}", "\n\n# BITSY VERSION {}.{}",
self.version.as_ref().unwrap().major, self.version.as_ref().unwrap().minor self.version.as_ref().unwrap().major.to_string(),
self.version.as_ref().unwrap().minor.to_string()
) )
} else { } else {
"".to_string() "".to_string()
} }
} }
fn version_major_line(&self) -> String {
if self.version_major.is_some() {
format!("\n! VER_MAJ {}", self.version_major.unwrap().to_string())
} else {
"".to_string()
}
}
fn version_minor_line(&self) -> String {
if self.version_minor.is_some() {
format!("\n! VER_MIN {}", self.version_minor.unwrap().to_string())
} else {
"".to_string()
}
}
fn room_format_line(&self) -> String { fn room_format_line(&self) -> String {
if self.room_format.is_some() { if self.room_format.is_some() {
format!("\n\n! ROOM_FORMAT {}", self.room_format.unwrap().to_string()) format!("\n! ROOM_FORMAT {}", self.room_format.unwrap().to_string())
} else {
"".to_string()
}
}
fn dialogue_compatibility_line(&self) -> String {
if self.dialogue_compatibility.is_some() {
format!("\n! DLG_COMPAT {}", self.dialogue_compatibility.unwrap().to_string())
} else {
"".to_string()
}
}
fn text_mode_line(&self) -> String {
if self.text_mode.is_some() {
format!("\n! TXT_MODE {}", self.text_mode.unwrap().to_string())
} else { } else {
"".to_string() "".to_string()
} }
@@ -855,11 +965,14 @@ impl Game {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::{TextDirection, Font, Version, Game, NotFound, Tile, Image}; use crate::{TextDirection, Font, Version, Game, Tile, Image, Palette, Colour};
#[test] #[test]
fn game_from_string() { fn game_from_string() {
let output = Game::from(include_str!["test-resources/default.bitsy"].to_string()).unwrap(); let (output, _error) = Game::from(
include_str!["test-resources/default.bitsy"].to_string()
).unwrap();
let expected = crate::mock::game_default(); let expected = crate::mock::game_default();
assert_eq!(output, expected); assert_eq!(output, expected);
@@ -891,7 +1004,7 @@ mod test {
for n in 1..10 { for n in 1..10 {
if n != 4 { if n != 4 {
let mut new_tile = crate::mock::tile_default(); let mut new_tile = crate::mock::tile_default();
new_tile.id = format!("{}", n).to_string(); new_tile.id = format!("{}", n.to_string()).to_string();
tiles.push(new_tile); tiles.push(new_tile);
} }
} }
@@ -922,7 +1035,7 @@ mod test {
#[test] #[test]
fn arabic() { fn arabic() {
let game = Game::from(include_str!("test-resources/arabic.bitsy").to_string()).unwrap(); let (game, _) = Game::from(include_str!("test-resources/arabic.bitsy").to_string()).unwrap();
assert_eq!(game.font, Font::Arabic); assert_eq!(game.font, Font::Arabic);
assert_eq!(game.text_direction, TextDirection::RightToLeft); assert_eq!(game.text_direction, TextDirection::RightToLeft);
@@ -1080,7 +1193,25 @@ mod test {
#[test] #[test]
fn empty_game_data_throws_error() { fn empty_game_data_throws_error() {
assert_eq!(Game::from("".to_string() ).err().unwrap(), NotFound::Anything); assert_eq!(Game::from("".to_string() ).unwrap_err(), crate::error::NotFound::Anything);
assert_eq!(Game::from(" \n \r\n".to_string()).err().unwrap(), NotFound::Anything); assert_eq!(Game::from(" \n \r\n".to_string()).unwrap_err(), crate::error::NotFound::Anything);
}
#[test]
fn get_palette() {
let mut game = crate::mock::game_default();
let new_palette = Palette {
id: "1".to_string(),
name: Some("sadness".to_string()),
colours: vec![
Colour { red: 133, green: 131, blue: 111 },
Colour { red: 105, green: 93, blue: 104 },
Colour { red: 62, green: 74, blue: 76 },
]
};
game.add_palette(new_palette.clone());
assert_eq!(game.get_palette("0").unwrap(), &crate::mock::game_default().palettes[0]);
assert_eq!(game.get_palette("1").unwrap(), &new_palette);
assert_eq!(game.get_palette("2"), None);
} }
} }

View File

@@ -1,3 +1,7 @@
use std::fmt;
use crate::Error;
use crate::error::ImageError;
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub struct Image { pub struct Image {
pub pixels: Vec<u8>, // 64 for SD, 256 for HD pub pixels: Vec<u8>, // 64 for SD, 256 for HD
@@ -49,12 +53,16 @@ impl Image {
self.pixels = pixels; self.pixels = pixels;
} }
}
impl From<String> for Image { fn from_str(str: &str) -> Result<(Image, Vec<crate::Error>), crate::Error> {
fn from(string: String) -> Image { let mut warnings = Vec::new();
let string = string.replace("NaN", "0");
let string = string.trim(); if str.contains("NaN") {
warnings.push(crate::Error::Image { err: ImageError::MalformedPixel });
}
let string = str.trim().replace("NaN", "0");
let lines: Vec<&str> = string.lines().collect(); let lines: Vec<&str> = string.lines().collect();
let dimension = lines.len(); let dimension = lines.len();
let mut pixels: Vec<u8> = Vec::new(); let mut pixels: Vec<u8> = Vec::new();
@@ -62,16 +70,30 @@ impl From<String> for Image {
for line in lines { for line in lines {
let line = &line[..dimension]; let line = &line[..dimension];
for char in line.chars().into_iter() { for char in line.chars().into_iter() {
pixels.push(match char {'1' => 1, _ => 0}); pixels.push(match char {
'0' => 0,
'1' => 1,
_ => {
warnings.push(
crate::Error::Image { err: ImageError::MalformedPixel }
);
0
}
});
} }
} }
Image { pixels } // 8×8 (normal) or 16×16 (Bitsy HD)
if [64, 256].contains(&pixels.len()) {
Ok((Image { pixels }, warnings))
} else {
Err(Error::Image { err: ImageError::WrongSize })
}
} }
} }
impl ToString for Image { impl fmt::Display for Image {
fn to_string(&self) -> String { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut string = String::new(); let mut string = String::new();
let sqrt = (self.pixels.len() as f64).sqrt() as usize; // 8 for SD, 16 for HD let sqrt = (self.pixels.len() as f64).sqrt() as usize; // 8 for SD, 16 for HD
@@ -84,26 +106,52 @@ impl ToString for Image {
string.pop(); // remove trailing newline string.pop(); // remove trailing newline
string write!(f, "{}", string)
} }
} }
pub(crate) fn animation_frames_from_string(string: String) -> Vec<Image> { /// todo return Result<(Vec<Image>, Vec<crate::Error>), crate::Error>
let frames: Vec<&str> = string.split(">").collect(); pub fn animation_frames_from_str(str: &str) -> Result<(Vec<Image>, Vec<crate::Error>), crate::Error> {
let mut warnings: Vec<Error> = Vec::new();
frames.iter().map(|&frame| Image::from(frame.to_string())).collect() let results: Vec<Result<Image, crate::Error>> = str
.split('>')
.collect::<Vec<&str>>()
.iter()
.map(|&frame| {
match Image::from_str(frame) {
Ok((frame, mut frame_warnings)) => {
warnings.append(&mut frame_warnings);
Ok(frame)
},
Err(e) => {
warnings.push(e.clone());
Err(e)
}
}
})
.collect();
// this is a pretty stupid way of filtering the results
let mut ok_results = Vec::new();
for result in results {
if let Ok(image) = result {
ok_results.push(image);
}
}
Ok((ok_results, warnings))
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::image::{Image, animation_frames_from_string}; use crate::image::{Image, animation_frames_from_str};
use crate::mock; use crate::mock;
#[test] #[test]
fn image_from_string() { fn image_from_string() {
let output = Image::from( let (output, _) = Image::from_str(include_str!("test-resources/image")).unwrap();
include_str!("test-resources/image").to_string()
);
let expected = Image { let expected = Image {
pixels: vec![ pixels: vec![
@@ -130,9 +178,9 @@ mod test {
#[test] #[test]
fn test_animation_frames_from_string() { fn test_animation_frames_from_string() {
let output = animation_frames_from_string( let output = animation_frames_from_str(
include_str!("test-resources/animation_frames").to_string() include_str!("test-resources/animation_frames")
); ).unwrap().0;
let expected = mock::image::animation_frames(); let expected = mock::image::animation_frames();
@@ -143,9 +191,7 @@ mod test {
/// check that these extraneous pixels are stripped out /// check that these extraneous pixels are stripped out
#[test] #[test]
fn image_out_of_bounds() { fn image_out_of_bounds() {
let output = Image::from( let (output, _) = Image::from_str(include_str!("test-resources/image-oob")).unwrap();
include_str!("test-resources/image-oob").to_string()
);
let expected = Image { let expected = Image {
pixels: vec![ pixels: vec![

View File

@@ -1,5 +1,6 @@
use crate::{optional_data_line, AnimationFrames, Image}; use crate::{optional_data_line, AnimationFrames, Image};
use crate::image::animation_frames_from_string; use crate::image::animation_frames_from_str;
use std::fmt;
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub struct Item { pub struct Item {
@@ -8,6 +9,7 @@ pub struct Item {
pub name: Option<String>, pub name: Option<String>,
pub dialogue_id: Option<String>, pub dialogue_id: Option<String>,
pub colour_id: Option<u64>, pub colour_id: Option<u64>,
pub blip: Option<String>,
} }
impl Item { impl Item {
@@ -22,21 +24,32 @@ impl Item {
fn colour_line(&self) -> String { fn colour_line(&self) -> String {
optional_data_line("COL", self.colour_id.as_ref()) optional_data_line("COL", self.colour_id.as_ref())
} }
}
impl From<String> for Item { fn blip_line(&self) -> String {
fn from(string: String) -> Item { optional_data_line("BLIP", self.blip.as_ref())
let mut lines: Vec<&str> = string.lines().collect(); }
pub fn from_str(str: &str) -> Result<(Item, Vec<crate::Error>), crate::Error> {
let mut lines: Vec<&str> = str.lines().collect();
if lines.is_empty() || !lines[0].starts_with("ITM ") {
return Err(crate::Error::Item);
}
let id = lines[0].replace("ITM ", ""); let id = lines[0].replace("ITM ", "");
let mut name = None; let mut name = None;
let mut dialogue_id = None; let mut dialogue_id = None;
let mut colour_id: Option<u64> = None; let mut colour_id: Option<u64> = None;
let mut blip = None;
let mut warnings = Vec::new();
loop { loop {
let last_line = lines.pop().unwrap(); let last_line = lines.pop().unwrap();
if last_line.starts_with("NAME") { if last_line.starts_with("BLIP") {
blip = Some(last_line.replace("BLIP ", "").to_string());
} else if last_line.starts_with("NAME") {
name = Some(last_line.replace("NAME ", "").to_string()); name = Some(last_line.replace("NAME ", "").to_string());
} else if last_line.starts_with("DLG") { } else if last_line.starts_with("DLG") {
dialogue_id = Some(last_line.replace("DLG ", "").to_string()); dialogue_id = Some(last_line.replace("DLG ", "").to_string());
@@ -48,29 +61,27 @@ impl From<String> for Item {
} }
} }
let animation_frames = animation_frames_from_string( let (animation_frames, mut animation_warnings) = animation_frames_from_str(
lines[1..].join("\n") &lines[1..].join("\n")
); ).unwrap();
Item { warnings.append(&mut animation_warnings);
id,
name, Ok((Item { id, name, animation_frames, dialogue_id, colour_id, blip }, warnings))
animation_frames,
dialogue_id,
colour_id,
}
} }
} }
impl ToString for Item { impl fmt::Display for Item {
fn to_string(&self) -> String { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
format!( write!(
"ITM {}\n{}{}{}{}", f,
"ITM {}\n{}{}{}{}{}",
self.id, self.id,
self.animation_frames.to_string(), self.animation_frames.to_string(),
self.name_line(), self.name_line(),
self.dialogue_line(), self.dialogue_line(),
self.colour_line(), self.colour_line(),
self.blip_line(),
) )
} }
} }
@@ -81,7 +92,7 @@ mod test {
#[test] #[test]
fn item_from_string() { fn item_from_string() {
let output = Item::from(include_str!("test-resources/item").to_string()); let (output, _err) = Item::from_str(include_str!("test-resources/item")).unwrap();
let expected = mock::item(); let expected = mock::item();
assert_eq!(output, expected); assert_eq!(output, expected);
} }

View File

@@ -1,12 +1,16 @@
extern crate core;
use std::fmt::Display; use std::fmt::Display;
use std::io::Cursor; use std::io::Cursor;
use radix_fmt::radix_36; use radix_fmt::radix_36;
use loe::{process, Config, TransformMode}; use loe::{process, Config, TransformMode};
pub mod blip;
pub mod colour; pub mod colour;
pub mod dialogue; pub mod dialogue;
pub mod ending; pub mod ending;
pub mod error;
pub mod exit; pub mod exit;
pub mod game; pub mod game;
pub mod image; pub mod image;
@@ -19,12 +23,15 @@ pub mod sprite;
pub mod text; pub mod text;
pub mod tile; pub mod tile;
pub mod variable; pub mod variable;
pub mod note;
pub mod tune;
pub mod test_omnibus; pub mod test_omnibus;
pub use blip::Blip;
pub use colour::Colour; pub use colour::Colour;
pub use dialogue::Dialogue; pub use dialogue::Dialogue;
pub use ending::Ending; pub use ending::Ending;
pub use error::Error;
pub use exit::*; pub use exit::*;
pub use game::*; pub use game::*;
pub use image::Image; pub use image::Image;
@@ -35,6 +42,7 @@ pub use room::Room;
pub use sprite::Sprite; pub use sprite::Sprite;
pub use text::*; pub use text::*;
pub use tile::Tile; pub use tile::Tile;
pub use tune::Tune;
pub use variable::Variable; pub use variable::Variable;
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
@@ -45,6 +53,7 @@ pub struct Instance {
/// a Room can have many Exits in different positions, /// a Room can have many Exits in different positions,
/// optionally with a transition and dialogue /// optionally with a transition and dialogue
/// todo make a from_str() function for this
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub struct ExitInstance { pub struct ExitInstance {
position: Position, position: Position,
@@ -62,7 +71,7 @@ impl AnimationFrames for Vec<Image> {
let mut string = String::new(); let mut string = String::new();
let last_frame = self.len() - 1; let last_frame = self.len() - 1;
for (i, frame) in self.into_iter().enumerate() { for (i, frame) in self.iter().enumerate() {
string.push_str(&frame.to_string()); string.push_str(&frame.to_string());
if i < last_frame { if i < last_frame {
@@ -106,11 +115,11 @@ fn transform_line_endings(input: String, mode: TransformMode) -> String {
String::from_utf8(output.into_inner()).unwrap() String::from_utf8(output.into_inner()).unwrap()
} }
fn segments_from_string(string: String) -> Vec<String> { fn segments_from_str(str: &str) -> Vec<String> {
// this is pretty weird but a dialogue can just have an empty line followed by a name // this is pretty weird but a dialogue can just have an empty line followed by a name
// however, on entering two empty lines, dialogue will be wrapped in triple quotation marks // however, on entering two empty lines, dialogue will be wrapped in triple quotation marks
// so, handle this here // so, handle this here
let string = string.replace("\n\nNAME", "\n\"\"\"\n\"\"\"\nNAME"); let string = str.replace("\n\nNAME", "\n\"\"\"\n\"\"\"\nNAME");
let mut output:Vec<String> = Vec::new(); let mut output:Vec<String> = Vec::new();
// are we inside `"""\n...\n"""`? if so, ignore empty lines // are we inside `"""\n...\n"""`? if so, ignore empty lines
@@ -185,7 +194,7 @@ impl Unquote for String {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::{ToBase36, optional_data_line, mock, segments_from_string, Quote, Unquote, new_unique_id, try_id}; use crate::{ToBase36, optional_data_line, mock, segments_from_str, Quote, Unquote, new_unique_id, try_id};
#[test] #[test]
fn to_base36() { fn to_base36() {
@@ -195,14 +204,12 @@ mod test {
#[test] #[test]
fn test_optional_data_line() { fn test_optional_data_line() {
let output = optional_data_line("NAME", mock::item().name); let output = optional_data_line("NAME", mock::item().name);
assert_eq!(output, "\nNAME door".to_string()); assert_eq!(output, "\nNAME door");
} }
#[test] #[test]
fn string_to_segments() { fn string_to_segments() {
let output = segments_from_string( let output = segments_from_str(include_str!("./test-resources/segments"));
include_str!("./test-resources/segments").to_string()
);
let expected = vec![ let expected = vec![
"\"\"\"\nthe first segment is a long bit of text\n\n\nit contains empty lines\n\n\"\"\"".to_string(), "\"\"\"\nthe first segment is a long bit of text\n\n\nit contains empty lines\n\n\"\"\"".to_string(),
@@ -217,14 +224,14 @@ mod test {
#[test] #[test]
fn quote() { fn quote() {
let output = "this is a string.\nIt has 2 lines".to_string().quote(); let output = "this is a string.\nIt has 2 lines".to_string().quote();
let expected = "\"\"\"\nthis is a string.\nIt has 2 lines\n\"\"\"".to_string(); let expected = "\"\"\"\nthis is a string.\nIt has 2 lines\n\"\"\"";
assert_eq!(output, expected); assert_eq!(output, expected);
} }
#[test] #[test]
fn unquote() { fn unquote() {
let output = "\"\"\"\nwho the fuck is scraeming \"LOG OFF\" at my house.\nshow yourself, coward.\ni will never log off\n\"\"\"".to_string().unquote(); let output = "\"\"\"\nwho the fuck is scraeming \"LOG OFF\" at my house.\nshow yourself, coward.\ni will never log off\n\"\"\"".to_string().unquote();
let expected = "who the fuck is scraeming \"LOG OFF\" at my house.\nshow yourself, coward.\ni will never log off".to_string(); let expected = "who the fuck is scraeming \"LOG OFF\" at my house.\nshow yourself, coward.\ni will never log off";
assert_eq!(output, expected); assert_eq!(output, expected);
} }
@@ -233,12 +240,12 @@ mod test {
// does a conflict generate a new ID? // does a conflict generate a new ID?
assert_eq!( assert_eq!(
try_id(&vec!["0".to_string(), "1".to_string()], &"1".to_string()), try_id(&vec!["0".to_string(), "1".to_string()], &"1".to_string()),
"2".to_string() "2"
); );
// with no conflict, does the ID remain the same? // with no conflict, does the ID remain the same?
assert_eq!( assert_eq!(
try_id(&vec!["0".to_string(), "1".to_string()], &"3".to_string()), try_id(&vec!["0".to_string(), "1".to_string()], &"3".to_string()),
"3".to_string() "3"
); );
} }

View File

@@ -204,7 +204,8 @@ pub mod item {
], ],
name: Some("key".to_string()), name: Some("key".to_string()),
dialogue_id: Some("2".to_string()), dialogue_id: Some("2".to_string()),
colour_id: None colour_id: None,
blip: None,
} }
} }
} }
@@ -227,6 +228,7 @@ pub fn item() -> Item {
name: Some("door".to_string()), name: Some("door".to_string()),
dialogue_id: Some("2".to_string()), dialogue_id: Some("2".to_string()),
colour_id: None, colour_id: None,
blip: None,
} }
} }
@@ -529,15 +531,20 @@ pub fn room() -> Room {
position: Position { x: 8, y: 7 }, position: Position { x: 8, y: 7 },
id: "undefined".to_string(), id: "undefined".to_string(),
}], }],
walls: vec![], walls: None,
tune: None,
} }
} }
pub fn game_default() -> Game { pub fn game_default() -> Game {
Game { Game {
name: "Write your game's title here".to_string(), name: "Write your game's title here".to_string(),
version: Some(Version { major: 7, minor: 2 }), version: Some(Version { major: 8, minor: 4 }),
version_major: Some(8),
version_minor: Some(12),
room_format: Some(RoomFormat::CommaSeparated), room_format: Some(RoomFormat::CommaSeparated),
dialogue_compatibility: Some(0),
text_mode: Some(0),
room_type: RoomType::Room, room_type: RoomType::Room,
font: Font::AsciiSmall, font: Font::AsciiSmall,
custom_font: None, custom_font: None,
@@ -828,9 +835,10 @@ pub fn game_default() -> Game {
items: vec![], items: vec![],
exits: vec![], exits: vec![],
endings: vec![], endings: vec![],
walls: vec![], walls: None,
tune: None,
}], }],
tiles: vec![self::tile_default()], tiles: vec![tile_default()],
sprites: vec![ sprites: vec![
Sprite { Sprite {
id: "A".to_string(), id: "A".to_string(),
@@ -878,6 +886,7 @@ pub fn game_default() -> Game {
name: Some("tea".to_string()), name: Some("tea".to_string()),
dialogue_id: Some("1".to_string()), dialogue_id: Some("1".to_string()),
colour_id: None, colour_id: None,
blip: None,
}, },
item::key() item::key()
], ],
@@ -903,6 +912,8 @@ pub fn game_default() -> Game {
id: "a".to_string(), id: "a".to_string(),
initial_value: "42".to_string(), initial_value: "42".to_string(),
}], }],
tunes: vec![],
blips: vec![],
font_data: None, font_data: None,
line_endings_crlf: false line_endings_crlf: false
} }

109
src/note.rs Normal file
View File

@@ -0,0 +1,109 @@
use core::fmt;
use std::fmt::Formatter;
use crate::Error;
#[derive(Clone, Debug, PartialEq)]
pub enum RelativeNote {
C,
CSharp,
D,
DSharp,
E,
F,
FSharp,
G,
GSharp,
A,
ASharp,
B,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Note {
relative: RelativeNote,
octave: u8, // upper limit? 8?
}
impl Note {
fn from(str: &str) -> Result<Self, Error> {
let mut chars: Vec<char> = str.chars().collect();
// last char may or may not be present / may or may not be an octave number
let octave = chars.pop().unwrap_or('4').to_string().parse().unwrap_or(4);
let chars: String = chars.into_iter().collect();
let relative = match chars.as_ref() {
"C" => RelativeNote::C,
"C#" => RelativeNote::CSharp,
"D" => RelativeNote::D,
"D#" => RelativeNote::DSharp,
"E" => RelativeNote::E,
"F" => RelativeNote::F,
"F#" => RelativeNote::FSharp,
"G" => RelativeNote::G,
"G#" => RelativeNote::GSharp,
"A" => RelativeNote::A,
"A#" => RelativeNote::ASharp,
"B" => RelativeNote::B,
_ => { return Err(Error::RelativeNote); }
};
Ok(Note { relative, octave })
}
}
impl fmt::Display for Note {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(
f,
"{}{}",
match self.relative {
RelativeNote::C => "C",
RelativeNote::CSharp => "C#",
RelativeNote::D => "D",
RelativeNote::DSharp => "D#",
RelativeNote::E => "E",
RelativeNote::F => "F",
RelativeNote::FSharp => "F#",
RelativeNote::G => "G",
RelativeNote::GSharp => "G#",
RelativeNote::A => "A",
RelativeNote::ASharp => "A#",
RelativeNote::B => "B",
},
self.octave
)
}
}
#[cfg(test)]
mod test {
use crate::note::{Note, RelativeNote};
#[test]
fn c4() {
assert_eq!(Note { relative: RelativeNote::C, octave: 4 }.to_string(), "C4");
}
#[test]
fn a_sharp_0() {
assert_eq!(Note { relative: RelativeNote::ASharp, octave: 0 }.to_string(), "A#0");
}
#[test]
fn c_sharp_3_from_str() {
assert_eq!(
Note::from("C#3").unwrap(),
Note { relative: RelativeNote::CSharp, octave: 3 }
);
}
#[test]
fn b_0_from_str() {
assert_eq!(
Note::from("B0").unwrap(),
Note { relative: RelativeNote::B, octave: 0 }
);
}
}

View File

@@ -1,4 +1,4 @@
use crate::colour::Colour; use crate::Colour;
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub struct Palette { pub struct Palette {
@@ -7,25 +7,39 @@ pub struct Palette {
pub colours: Vec<Colour>, pub colours: Vec<Colour>,
} }
impl From<String> for Palette { impl Palette {
fn from(string: String) -> Palette { pub fn from_str(s: &str) -> Result<(Palette, Vec<crate::Error>), crate::Error> {
let lines: Vec<&str> = string.lines().collect(); let mut lines: Vec<&str> = s.lines().collect();
let id = lines[0].replace("PAL ", ""); if lines.is_empty() {
return Err(crate::Error::Palette);
}
let name = match lines[1].starts_with("NAME") { let mut id = String::new();
true => Some(lines[1].replace("NAME ", "").to_string()), let mut name = None;
false => None, let mut colours = Vec::new();
}; let mut warnings = Vec::new();
let colour_start_index = if name.is_some() { 2 } else { 1 }; while !lines.is_empty() {
let line = lines.pop().unwrap();
let colours = lines[colour_start_index..] if line.starts_with("PAL ") {
.iter() id = line.replace("PAL ", "");
.map(|&line| Colour::from(line).unwrap()) } else if line.starts_with("NAME ") {
.collect(); name = Some(line.replace("NAME ", ""));
} else {
let result = Colour::from(line);
if let Ok(colour) = result {
colours.push(colour)
} else {
warnings.push(result.unwrap_err());
}
}
}
Palette { id, name, colours } colours.reverse();
Ok((Palette { id, name, colours }, warnings))
} }
} }
@@ -53,7 +67,7 @@ mod test {
#[test] #[test]
fn palette_from_string() { fn palette_from_string() {
let output = Palette::from("PAL 1\nNAME lamplight\n45,45,59\n66,60,39\n140,94,1".to_string()); let (output, _) = Palette::from_str("PAL 1\nNAME lamplight\n45,45,59\n66,60,39\n140,94,1").unwrap();
let expected = Palette { let expected = Palette {
id: "1".to_string(), id: "1".to_string(),
@@ -82,7 +96,7 @@ mod test {
#[test] #[test]
fn palette_from_string_no_name() { fn palette_from_string_no_name() {
let output = Palette::from("PAL 9\n45,45,59\n66,60,39\n140,94,1".to_string()); let (output, _) = Palette::from_str("PAL 9\n45,45,59\n66,60,39\n140,94,1").unwrap();
let expected = Palette { let expected = Palette {
id: "9".to_string(), id: "9".to_string(),
@@ -131,9 +145,9 @@ mod test {
blue: 128, blue: 128,
}, },
], ],
} }.to_string();
.to_string();
let expected = "PAL g\nNAME moss\n1,2,3\n255,254,253\n126,127,128".to_string(); let expected = "PAL g\nNAME moss\n1,2,3\n255,254,253\n126,127,128";
assert_eq!(output, expected); assert_eq!(output, expected);
} }
} }

View File

@@ -1,6 +1,4 @@
use std::error::Error;
use std::fmt; use std::fmt;
use std::str::FromStr;
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub struct Position { pub struct Position {
@@ -8,24 +6,17 @@ pub struct Position {
pub y: u8, pub y: u8,
} }
impl Error for Position {} impl Position {
pub fn from_str(s: &str) -> Result<Self, crate::Error> {
impl FromStr for Position {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut parts = s.split(','); let mut parts = s.split(',');
let x = parts.next().unwrap().parse();
let y = parts.next().unwrap().parse();
if x.is_err() { let x = parts.next().unwrap();
Err("bad x supplied for position".to_string()) let y = parts.next().unwrap();
} else if y.is_err() {
Err("bad y supplied for position".to_string()) if let (Ok(x), Ok(y)) = (x.parse(), y.parse()) {
} else {
let x = x.unwrap();
let y = y.unwrap();
Ok(Position { x, y }) Ok(Position { x, y })
} else {
Err(crate::Error::Position)
} }
} }
} }
@@ -39,16 +30,20 @@ impl fmt::Display for Position {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::Position; use crate::Position;
use std::str::FromStr;
#[test] #[test]
fn position_from_str() { fn position_from_str() {
assert_eq!( assert_eq!(
Position::from_str(&"4,12").unwrap(), Position::from_str("4,12").unwrap(),
Position { x: 4, y: 12 } Position { x: 4, y: 12 }
); );
} }
#[test]
fn position_from_malformed_str() {
assert!(Position::from_str("14,-1").is_err())
}
#[test] #[test]
fn position_to_string() { fn position_to_string() {
assert_eq!(Position { x: 4, y: 12 }.to_string(), "4,12".to_string()) assert_eq!(Position { x: 4, y: 12 }.to_string(), "4,12".to_string())

View File

@@ -9,7 +9,6 @@ use crate::{
Transition Transition
}; };
use std::str::FromStr;
use std::collections::HashMap; use std::collections::HashMap;
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
@@ -24,7 +23,8 @@ pub struct Room {
pub exits: Vec<ExitInstance>, pub exits: Vec<ExitInstance>,
pub endings: Vec<Instance>, pub endings: Vec<Instance>,
/// old method of handling walls - a comma-separated list of tile IDs /// old method of handling walls - a comma-separated list of tile IDs
pub walls: Vec<String>, pub walls: Option<Vec<String>>,
pub tune: Option<String>,
} }
impl Room { impl Room {
@@ -33,18 +33,24 @@ impl Room {
} }
fn wall_line(&self) -> String { fn wall_line(&self) -> String {
if self.walls.len() > 0 { if let Some(walls) = &self.walls {
optional_data_line("WAL", Some(self.walls.join(","))) optional_data_line("WAL", Some(walls.join(",")))
} else { } else {
"".to_string() "".to_string()
} }
} }
fn palette_line(&self) -> String { fn palette_line(&self) -> String {
if self.palette_id.is_some() { match &self.palette_id {
optional_data_line("PAL", Some(self.palette_id.as_ref().unwrap())) Some(id) => optional_data_line("PAL", Some(id.clone())),
} else { None => "".to_string(),
"".to_string() }
}
fn tune_line(&self) -> String {
match &self.tune {
Some(id) => optional_data_line("TUNE", Some(id.clone())),
None => "".to_string(),
} }
} }
} }
@@ -60,15 +66,18 @@ impl From<String> for Room {
let mut items: Vec<Instance> = Vec::new(); let mut items: Vec<Instance> = Vec::new();
let mut exits: Vec<ExitInstance> = Vec::new(); let mut exits: Vec<ExitInstance> = Vec::new();
let mut endings: Vec<Instance> = Vec::new(); let mut endings: Vec<Instance> = Vec::new();
let mut walls: Vec<String> = Vec::new(); let mut walls = None;
let mut tune = None;
loop { loop {
let last_line = lines.pop().unwrap(); let last_line = lines.pop().unwrap();
if last_line.starts_with("WAL") { if last_line.starts_with("TUNE") {
tune = Some(last_line.replace("TUNE ", ""));
} else if last_line.starts_with("WAL") {
let last_line = last_line.replace("WAL ", ""); let last_line = last_line.replace("WAL ", "");
let ids: Vec<&str> = last_line.split(',').collect(); let ids: Vec<&str> = last_line.split(',').collect();
walls = ids.iter().map(|&id| id.to_string()).collect(); walls = Some(ids.iter().map(|&id| id.to_string()).collect());
} else if last_line.starts_with("NAME") { } else if last_line.starts_with("NAME") {
name = Some(last_line.replace("NAME ", "").to_string()); name = Some(last_line.replace("NAME ", "").to_string());
} else if last_line.starts_with("PAL") { } else if last_line.starts_with("PAL") {
@@ -78,9 +87,8 @@ impl From<String> for Room {
let item_position: Vec<&str> = last_line.split(' ').collect(); let item_position: Vec<&str> = last_line.split(' ').collect();
let item_id = item_position[0]; let item_id = item_position[0];
let position = item_position[1]; let position = item_position[1];
let position = Position::from_str(position);
if let Ok(position) = position { if let Ok(position) = Position::from_str(position) {
items.push(Instance { position, id: item_id.to_string() }); items.push(Instance { position, id: item_id.to_string() });
} }
} else if last_line.starts_with("EXT") { } else if last_line.starts_with("EXT") {
@@ -96,14 +104,17 @@ impl From<String> for Room {
if let Ok(exit) = exit { if let Ok(exit) = exit {
let mut transition = None; let mut transition = None;
let mut dialogue_id = None; let mut dialogue_id = None;
let chunks = parts[3..].chunks(2); let chunks = parts[3..].chunks(2);
for chunk in chunks { for chunk in chunks {
if chunk[0] == "FX" { if chunk[0] == "FX" {
transition = Some(Transition::from(chunk[1])); transition = Some(Transition::from_str(chunk[1]).unwrap());
} else if chunk[0] == "DLG" { } else if chunk[0] == "DLG" {
dialogue_id = Some(chunk[1].to_string()); dialogue_id = Some(chunk[1].to_string());
} }
} }
exits.push(ExitInstance { position, exit, transition, dialogue_id }); exits.push(ExitInstance { position, exit, transition, dialogue_id });
} }
} }
@@ -154,6 +165,7 @@ impl From<String> for Room {
exits, exits,
endings, endings,
walls, walls,
tune,
} }
} }
} }
@@ -195,9 +207,10 @@ impl Room {
"\nEXT {} {}{}{}{}", "\nEXT {} {}{}{}{}",
instance.position.to_string(), instance.position.to_string(),
instance.exit.to_string(), instance.exit.to_string(),
if instance.transition.is_some() { match &instance.transition {
instance.transition.as_ref().unwrap().to_string() Some(transition) => transition,
} else {"".to_string()}, None => &Transition::None,
}.to_string(),
if instance.dialogue_id.is_some() {" DLG "} else {""}, if instance.dialogue_id.is_some() {" DLG "} else {""},
instance.dialogue_id.as_ref().unwrap_or(&"".to_string()), instance.dialogue_id.as_ref().unwrap_or(&"".to_string()),
)); ));
@@ -212,7 +225,7 @@ impl Room {
} }
format!( format!(
"{} {}\n{}{}{}{}{}{}{}", "{} {}\n{}{}{}{}{}{}{}{}",
room_type.to_string(), room_type.to_string(),
self.id, self.id,
tiles, tiles,
@@ -221,7 +234,8 @@ impl Room {
items, items,
exits, exits,
endings, endings,
self.palette_line() self.palette_line(),
self.tune_line(),
) )
} }
@@ -257,6 +271,6 @@ mod test {
fn room_walls_array() { fn room_walls_array() {
let output = Room::from(include_str!("test-resources/room-with-walls").to_string()); let output = Room::from(include_str!("test-resources/room-with-walls").to_string());
assert_eq!(output.walls, vec!["a".to_string(), "f".to_string()]); assert_eq!(output.walls, Some(vec!["a".to_string(), "f".to_string()]));
} }
} }

View File

@@ -1,6 +1,7 @@
use crate::{optional_data_line, AnimationFrames, Image, Position}; use crate::{optional_data_line, AnimationFrames, Image, Position};
use crate::image::animation_frames_from_string; use crate::image::animation_frames_from_str;
use std::str::FromStr;
use std::fmt;
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub struct Sprite { pub struct Sprite {
@@ -50,15 +51,13 @@ impl Sprite {
format!("\n{}", lines.join("\n")) format!("\n{}", lines.join("\n"))
} }
} }
}
#[derive(Debug)] pub fn from_str(str: &str) -> Result<Sprite, crate::Error> {
pub struct SpriteMissingRoomPosition; let mut lines: Vec<&str> = str.lines().collect();
// todo "malformed sprite ID" or something
impl Sprite { if lines.is_empty() || !lines[0].starts_with("SPR ") {
pub(crate) fn from(string: String) -> Result<Sprite, SpriteMissingRoomPosition> { return Err(crate::Error::Sprite);
let mut lines: Vec<&str> = string.lines().collect(); }
let id = lines[0].replace("SPR ", ""); let id = lines[0].replace("SPR ", "");
let mut name = None; let mut name = None;
@@ -81,10 +80,14 @@ impl Sprite {
room_id = Some(room_position[0].to_string()); room_id = Some(room_position[0].to_string());
if room_position.len() < 2 { if room_position.len() < 2 {
return Err(SpriteMissingRoomPosition); return Err(crate::Error::Sprite);
} }
position = Some(Position::from_str(room_position[1]).unwrap()); if let Ok(pos) = Position::from_str(room_position[1]) {
position = Some(pos);
} else {
return Err(crate::Error::Sprite);
}
} else if last_line.starts_with("COL") { } else if last_line.starts_with("COL") {
colour_id = Some(last_line.replace("COL ", "").parse().unwrap()); colour_id = Some(last_line.replace("COL ", "").parse().unwrap());
} else if last_line.starts_with("ITM") { } else if last_line.starts_with("ITM") {
@@ -97,9 +100,14 @@ impl Sprite {
items.reverse(); items.reverse();
let animation_frames = animation_frames_from_string( let animation_frames = match animation_frames_from_str(&lines[1..].join("\n")) {
lines[1..].join("\n") Ok((frames, _warnings)) => {
); frames
},
Err(_e) => {
Vec::new()
},
};
Ok(Sprite { Ok(Sprite {
id, id,
@@ -114,9 +122,10 @@ impl Sprite {
} }
} }
impl ToString for Sprite { impl fmt::Display for Sprite {
fn to_string(&self) -> String { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
format!( write!(
f,
"SPR {}\n{}{}{}{}{}{}", "SPR {}\n{}{}{}{}{}{}",
self.id, self.id,
self.animation_frames.to_string(), self.animation_frames.to_string(),
@@ -135,8 +144,7 @@ mod test {
#[test] #[test]
fn sprite_from_string() { fn sprite_from_string() {
let string = include_str!("test-resources/sprite").to_string(); let output = Sprite::from_str(include_str!("test-resources/sprite")).unwrap();
let output = Sprite::from(string).unwrap();
let expected = mock::sprite(); let expected = mock::sprite();
assert_eq!(output, expected); assert_eq!(output, expected);
@@ -144,6 +152,6 @@ mod test {
#[test] #[test]
fn sprite_to_string() { fn sprite_to_string() {
assert_eq!(mock::sprite().to_string(), include_str!("test-resources/sprite").to_string()); assert_eq!(mock::sprite().to_string(), include_str!("test-resources/sprite"));
} }
} }

View File

@@ -1,14 +1,18 @@
Write your game's title here Write your game's title here
# BITSY VERSION 7.2 # BITSY VERSION 8.12
! VER_MAJ 8
! VER_MIN 12
! ROOM_FORMAT 1 ! ROOM_FORMAT 1
! DLG_COMPAT 0
! TXT_MODE 0
PAL 0 PAL 0
NAME blueprint
0,82,204 0,82,204
128,159,255 128,159,255
255,255,255 255,255,255
NAME blueprint
ROOM 0 ROOM 0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
@@ -29,6 +33,7 @@ ROOM 0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
NAME example room NAME example room
PAL 0 PAL 0
TUNE 2
TIL a TIL a
11111111 11111111
@@ -64,6 +69,7 @@ SPR a
NAME cat NAME cat
DLG 0 DLG 0
POS 0 8,12 POS 0 8,12
BLIP 1
ITM 0 ITM 0
00000000 00000000
@@ -88,6 +94,7 @@ ITM 1
00011000 00011000
NAME key NAME key
DLG 2 DLG 2
BLIP 2
DLG 0 DLG 0
I'm a cat I'm a cat
@@ -104,3 +111,91 @@ NAME key dialog
VAR a VAR a
42 42
TUNE 1
3d,0,0,0,3d5,0,0,0,3l,0,0,0,3s,0,0,0
16d2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
>
4l,0,0,0,s,0,3l,0,0,0,2s,0,2m,0,2r,0
16m2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
>
3d,0,0,0,3d5,0,0,0,3l,0,0,0,3s,0,0,0
16l2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
>
3l,0,0,0,s,0,4m,0,0,0,4r,0,0,0,0,0
16s2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
NAME finale fanfare
KEY C,D,E,F,G,A,B d,r,m,s,l
TMP XFST
SQR P2 P8
ARP INT8
TUNE 2
0,0,2G,0,A,0,B,0,2C5,0,B,A,G,0,2G,0
G3,0,D,0,G3,0,D,0,2A3,0,E,0,C,0,E,0
>
2F#,0,G,0,A,0,F#,0,2E,0,F#,E,4D,0,0,0
2D,0,E,0,F#,0,D,0,2C,0,2G3,0,2F#3,0,D2,0
>
0,0,2G,0,A,0,B,0,2C5,0,B,A,G,0,G,0
2G2,0,D,D5,G3,G,D,0,2C2,0,E,E5,C3,C,E5,0
>
2D,0,C5,B,A,0,A,0,4A,0,0,0,F#,0,0,0
A2,0,E3,0,C3,0,E3,0,D3,0,A3,0,D,0,0,0
>
2E5,0,2G,0,2G5,0,2G,0,2F#5,0,2E5,0,2D5,0,2C5,0
2C3,0,2E,0,2E5,0,2C,0,2A3,0,2C,0,2F#,0,2E,0
>
3B,0,0,0,2E5,0,D5,0,4A,0,0,0,G,0,0,0
2G3,0,B3,0,2D,0,D3,0,2C3,0,G3,0,D#,0,0,0
>
0,0,2G,0,A,0,2B,0,C5,0,B,C5,A,0,G,0
A2,0,A3,0,C,0,2D,0,D#,0,D,E,C,0,C3,0
>
8B,0,0,0,0,0,0,0,A,0,2F#,0,E,0,D,0
D3,0,A3,0,F#,0,D,0,C,0,2D3,0,C3,0,F#3,0
NAME tuneful town
TMP FST
SQR P4 P2
TUNE 3
3F5,0,0,A#,0,2C#5,0,A#,3F5,0,0,F#5,0,0,2F5,0
A#3,C#,F,0,0,F,C#,F,A#3,C#,F,A#,0,A#,C#,F
>
3F5,0,0,A,0,2C#5,0,A,3F5,0,0,2A#5,0,A#,D#5,0
A3,C#,F,A3,0,F,C#,F,A,C#,F,0,D#,0,C#5,0
>
4F5,0,0,0,G#,2C#5,0,G#,3F5,0,0,D#5,0,F5,2C#5,0
G#3,C#,F,3F#,0,0,2F,0,G#3,C#,F,F#,B,A,F,D#
>
4D#5,0,0,0,0,0,2A#,0,4A#,0,0,0,0,0,A#,C5
G3,D#,F,G,0,D#,F,G,G3,D#,G,F,0,F,D#,C#3
>
4C#5,0,0,0,0,0,2C#5,0,3C#5,0,0,D#5,0,0,2C#5,0
F#2,C#,F#3,A#3,0,F#3,C#3,F#3,F#2,A,F#3,C#,0,F#3,A3,F#3
>
3C#5,0,0,F,3C5,0,0,C,3C5,0,0,D#,3A#,0,0,0
F2,D#3,A3,0,0,F3,C#,0,F#2,A#3,D#3,0,0,F#3,C#,C
>
3A#,0,0,0,C5,0,C#5,0,A#,0,C#,D#,G#,G#3,0,C#3
C3,A#3,C,E,A,0,A#,0,0,B2,B3,0,2F,0,0,0
>
A#,0,A#3,0,C#,0,F,0,A#,0,0,0,0,0,0,0
A#2,0,C#3,0,F3,0,A#3,0,D,0,0,0,0,F#3,0,F3
NAME rhythmic ruins
TMP MED
SQR P4 P4
BLIP 1
E5,B5,B5
NAME meow
ENV 40 99 4 185 138
BEAT 61 115
SQR P2
BLIP 2
D5,E5,D5
NAME pick up key
ENV 99 65 6 96 152
BEAT 95 0
SQR P4

File diff suppressed because it is too large Load Diff

View File

@@ -13,19 +13,19 @@ impl Font {
match str { match str {
"unicode_european_small" => Font::UnicodeEuropeanSmall, "unicode_european_small" => Font::UnicodeEuropeanSmall,
"unicode_european_large" => Font::UnicodeEuropeanLarge, "unicode_european_large" => Font::UnicodeEuropeanLarge,
"unicode_asian" => Font::UnicodeAsian, "unicode_asian" => Font::UnicodeAsian,
"arabic" => Font::Arabic, "arabic" => Font::Arabic,
_ => Font::Custom, _ => Font::Custom,
} }
} }
pub(crate) fn to_string(&self) -> Result<String, &'static str> { pub(crate) fn to_string(&self) -> Result<String, crate::Error> {
match &self { match &self {
Font::UnicodeEuropeanSmall => Ok("unicode_european_small".to_string()), Font::UnicodeEuropeanSmall => Ok("unicode_european_small".to_string()),
Font::UnicodeEuropeanLarge => Ok("unicode_european_large".to_string()), Font::UnicodeEuropeanLarge => Ok("unicode_european_large".to_string()),
Font::UnicodeAsian => Ok("unicode_asian".to_string()), Font::UnicodeAsian => Ok("unicode_asian".to_string()),
Font::Arabic => Ok("arabic".to_string()), Font::Arabic => Ok("arabic".to_string()),
_ => Err("No string available for this Font"), _ => Err(crate::Error::Font),
} }
} }
} }

View File

@@ -1,18 +1,23 @@
use crate::{optional_data_line, AnimationFrames, Image}; use crate::{optional_data_line, AnimationFrames, Image};
use crate::image::animation_frames_from_string; use crate::image::animation_frames_from_str;
#[derive(Clone, Debug, Eq)] #[derive(Clone, Debug, Eq)]
pub struct Tile { pub struct Tile {
pub id: String, pub id: String,
pub name: Option<String>, pub name: Option<String>,
/// this is "optional" in that a tile can have `WAL true`, `WAL false` or neither /// Can the player move over this tile?
/// obviously Some(false) is the same as None but we want to preserve the original formatting /// This is "optional" in that a tile can have `WAL true`, `WAL false` or neither.
/// obviously Some(false) is functionally the same as None
/// but we want to preserve the original formatting where possible.
pub wall: Option<bool>, pub wall: Option<bool>,
pub animation_frames: Vec<Image>, pub animation_frames: Vec<Image>,
/// Bitsy has an undocumented feature where a tile can be rendered
/// in a specific colour (`COL n`) from the current palette.
pub colour_id: Option<u64>, pub colour_id: Option<u64>,
} }
impl PartialEq for Tile { impl PartialEq for Tile {
/// ignore id and name.
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
self.wall == other.wall self.wall == other.wall
&& &&
@@ -44,6 +49,7 @@ impl Tile {
} }
// todo refactor // todo refactor
// can we do map_in_place or something?
pub fn invert(&mut self) { pub fn invert(&mut self) {
self.animation_frames = self.animation_frames.iter().map(|frame: &Image| { self.animation_frames = self.animation_frames.iter().map(|frame: &Image| {
@@ -103,9 +109,14 @@ impl From<String> for Tile {
} }
} }
let animation_frames = animation_frames_from_string( let animation_frames = match animation_frames_from_str(&lines[1..].join("\n")) {
lines[1..].join("\n") Ok((animation_frames, _warnings)) => {
); animation_frames
},
Err(_) => {
Vec::new()
},
};
Tile { Tile {
id, id,
@@ -162,8 +173,7 @@ mod test {
mock::image::chequers_2(), mock::image::chequers_2(),
], ],
colour_id: None, colour_id: None,
} }.to_string();
.to_string();
let expected = include_str!("test-resources/tile-chequers").to_string(); let expected = include_str!("test-resources/tile-chequers").to_string();

41
src/tune.rs Normal file
View File

@@ -0,0 +1,41 @@
use std::fmt;
use std::fmt::{Formatter};
use crate::note::Note;
/// this seems to be complete as of 23/10/2022. there's no ExtraSlow or whatever
#[derive(Debug, Clone, PartialEq)]
pub enum Tempo {
Slow,
Medium,
Fast,
ExtraFast,
}
impl fmt::Display for Tempo {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", match self {
Tempo::Slow => "SLW",
Tempo::Medium => "MED",
Tempo::Fast => "FST",
Tempo::ExtraFast => "XFST",
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Measure {
notes: [Note; 32],
}
#[derive(Debug, Clone, PartialEq)]
pub struct Tune {
id: String,
// how many measures? should this be a slice? default game data tunes always have 8 measures
measures: Vec<Measure>,
name: Option<String>,
// todo key (this seems confusing, maybe just implement as a non-parsed string for now?)
tempo: Tempo,
// but what's the contents? enum?
arpeggio: Option<String>,
// todo sqr (waveform? not sure. maybe just implement as a non-parsed string for now?)
}