Compare commits
40 Commits
fe22e78423
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 4388c75bb8 | |||
| a36313341d | |||
| eee3444c4d | |||
| 5577a05191 | |||
| 9363be8254 | |||
| a7c020f785 | |||
| 8148e3f725 | |||
| b8c30fe873 | |||
| b50bde1f64 | |||
| 4182079a73 | |||
| b95d9d28d4 | |||
| 9188529c9f | |||
| 34bcad51dd | |||
| 81da2960af | |||
| 7199ca30f9 | |||
| 4a05021bdd | |||
| de15ccbaa2 | |||
| b9415ade9e | |||
| e992e41635 | |||
| 46f8831c7b | |||
| 66cb9bdd4d | |||
| 1bbfaceeb4 | |||
| e9738b98b1 | |||
| 8f558a908f | |||
| fb290f07f4 | |||
| 889328f9a9 | |||
| 68ecc64c7b | |||
| 0dcddb9d8e | |||
| 1c5315ddad | |||
| a7a4a34ab8 | |||
| 67d4e28773 | |||
| d8183e29fc | |||
| dba84e01fa | |||
| 7896ef1232 | |||
| f7f08d9aba | |||
| 8068177736 | |||
| 59fc76a2d4 | |||
| 676c71cd45 | |||
| 5aa0c94810 | |||
| ad3eb102be |
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "bitsy-parser"
|
||||
version = "0.72.3"
|
||||
version = "0.812.0"
|
||||
authors = ["Max Bradbury <max@tinybird.info>"]
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
description = "A parser and utilities for working with Bitsy game data"
|
||||
readme = "README.md"
|
||||
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
|
||||
|
||||
[dependencies]
|
||||
radix_fmt = "1.0.0"
|
||||
loe = "0.2.0"
|
||||
data-encoding = "^2.6.0"
|
||||
radix_fmt = "^1.0.0"
|
||||
loe = "0.3.0"
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Max Bradbury
|
||||
Copyright © 2024 Max Bradbury
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
9
TODO.md
Normal file
9
TODO.md
Normal 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
|
||||
@@ -7,7 +7,7 @@ fn main() {
|
||||
let game = env::args().nth(1).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();
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ fn main() {
|
||||
let output = env::args().nth(3).expect(SYNTAX_ERROR);
|
||||
// todo allow numerous additional games
|
||||
|
||||
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 (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();
|
||||
|
||||
game_a.merge(&game_b);
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ fn main() {
|
||||
let input = env::args().nth(1).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");
|
||||
}
|
||||
|
||||
76
src/blip.rs
Normal file
76
src/blip.rs
Normal 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 }
|
||||
}
|
||||
}
|
||||
@@ -5,15 +5,12 @@ pub struct Colour {
|
||||
pub blue: u8,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct InvalidRgb;
|
||||
|
||||
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();
|
||||
|
||||
if values.len() != 3 {
|
||||
return Err(InvalidRgb);
|
||||
return Err(crate::Error::Colour);
|
||||
}
|
||||
|
||||
let red: u8 = values[0].parse().unwrap_or(0);
|
||||
@@ -22,6 +19,13 @@ impl Colour {
|
||||
|
||||
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 {
|
||||
@@ -61,4 +65,18 @@ mod test {
|
||||
fn colour_extraneous_value() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use crate::optional_data_line;
|
||||
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Dialogue {
|
||||
pub id: String,
|
||||
@@ -7,27 +9,35 @@ pub struct Dialogue {
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
impl From<String> for Dialogue {
|
||||
#[inline]
|
||||
fn from(string: String) -> Dialogue {
|
||||
let mut lines: Vec<&str> = string.lines().collect();
|
||||
impl Dialogue {
|
||||
pub fn from_str(str: &str) -> Result<Dialogue, crate::Error> {
|
||||
let mut lines: Vec<&str> = str.lines().collect();
|
||||
|
||||
if lines.is_empty() || !lines[0].starts_with("DLG ") {
|
||||
return Err(crate::Error::Dialogue);
|
||||
}
|
||||
|
||||
let id = lines[0].replace("DLG ", "");
|
||||
|
||||
let name = if lines.last().unwrap().starts_with("NAME ") {
|
||||
Some(lines.pop().unwrap().replace("NAME ", ""))
|
||||
let last_line = lines.pop().unwrap();
|
||||
|
||||
let name = if last_line.starts_with("NAME ") {
|
||||
Some(last_line.replace("NAME ", ""))
|
||||
} else {
|
||||
lines.push(last_line);
|
||||
None
|
||||
};
|
||||
|
||||
let contents = lines[1..].join("\n");
|
||||
|
||||
Dialogue { id, contents, name }
|
||||
Ok(Dialogue { id, contents, name })
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for Dialogue {
|
||||
fn to_string(&self) -> String {
|
||||
format!(
|
||||
impl fmt::Display for Dialogue {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"DLG {}\n{}{}",
|
||||
self.id,
|
||||
self.contents,
|
||||
@@ -41,10 +51,10 @@ mod test {
|
||||
use crate::Dialogue;
|
||||
|
||||
#[test]
|
||||
fn dialogue_from_string() {
|
||||
let output = Dialogue::from(
|
||||
"DLG h\nhello\nNAME not a dialogue name\nNAME a dialogue name".to_string()
|
||||
);
|
||||
fn dialogue_from_str() {
|
||||
let output = Dialogue::from_str(
|
||||
"DLG h\nhello\nNAME not a dialogue name\nNAME a dialogue name"
|
||||
).unwrap();
|
||||
|
||||
let expected = Dialogue {
|
||||
id: "h".to_string(),
|
||||
@@ -63,7 +73,7 @@ mod test {
|
||||
name: Some("a dialogue name".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);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
use std::fmt;
|
||||
use std::error::Error;
|
||||
use std::str::FromStr;
|
||||
|
||||
// same as a dialogue basically
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
@@ -9,13 +7,14 @@ pub struct Ending {
|
||||
pub dialogue: String,
|
||||
}
|
||||
|
||||
impl Error for Ending {}
|
||||
|
||||
impl FromStr for Ending {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
impl Ending {
|
||||
pub fn from_str(s: &str) -> Result<Self, crate::Error> {
|
||||
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 dialogue = lines[1..].join("\n");
|
||||
|
||||
@@ -32,7 +31,6 @@ impl fmt::Display for Ending {
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::Ending;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[test]
|
||||
fn ending_from_string() {
|
||||
|
||||
63
src/error.rs
Normal file
63
src/error.rs
Normal 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 {}
|
||||
60
src/exit.rs
60
src/exit.rs
@@ -1,6 +1,5 @@
|
||||
use crate::Position;
|
||||
use std::str::FromStr;
|
||||
use std::error::Error;
|
||||
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
@@ -16,25 +15,25 @@ pub enum Transition {
|
||||
SlideRight,
|
||||
}
|
||||
|
||||
impl From<&str> for Transition {
|
||||
fn from(str: &str) -> Transition {
|
||||
impl Transition {
|
||||
pub fn from_str(str: &str) -> Result<Transition, crate::Error> {
|
||||
match str {
|
||||
"fade_w" => Transition::FadeToWhite,
|
||||
"fade_b" => Transition::FadeToBlack,
|
||||
"wave" => Transition::Wave,
|
||||
"tunnel" => Transition::Tunnel,
|
||||
"slide_u" => Transition::SlideUp,
|
||||
"slide_d" => Transition::SlideDown,
|
||||
"slide_l" => Transition::SlideLeft,
|
||||
"slide_r" => Transition::SlideRight,
|
||||
_ => Transition::None,
|
||||
"fade_w" => Ok(Transition::FadeToWhite),
|
||||
"fade_b" => Ok(Transition::FadeToBlack),
|
||||
"wave" => Ok(Transition::Wave),
|
||||
"tunnel" => Ok(Transition::Tunnel),
|
||||
"slide_u" => Ok(Transition::SlideUp),
|
||||
"slide_d" => Ok(Transition::SlideDown),
|
||||
"slide_l" => Ok(Transition::SlideLeft),
|
||||
"slide_r" => Ok(Transition::SlideRight),
|
||||
_ => Err(crate::Error::Transition),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for Transition {
|
||||
fn to_string(&self) -> String {
|
||||
match &self {
|
||||
impl fmt::Display for Transition {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", match &self {
|
||||
Transition::FadeToWhite => " FX fade_w",
|
||||
Transition::FadeToBlack => " FX fade_b",
|
||||
Transition::Wave => " FX wave",
|
||||
@@ -44,8 +43,7 @@ impl ToString for Transition {
|
||||
Transition::SlideLeft => " FX slide_l",
|
||||
Transition::SlideRight => " FX slide_r",
|
||||
Transition::None => "",
|
||||
}
|
||||
.to_string()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,24 +56,21 @@ pub struct Exit {
|
||||
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 {
|
||||
type Err = String;
|
||||
|
||||
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());
|
||||
if parts.len() < 2 {
|
||||
return Err(crate::Error::Exit);
|
||||
}
|
||||
|
||||
let position = position.unwrap();
|
||||
let mut parts = parts.iter();
|
||||
|
||||
let effect = if let Some(transition_line) = parts.next() {
|
||||
Transition::from(transition_line)
|
||||
let room_id = parts.next().unwrap().to_string();
|
||||
let position = Position::from_str(parts.next().unwrap())?;
|
||||
|
||||
let effect = if parts.next().is_some() {
|
||||
Transition::from_str(parts.next().unwrap())?
|
||||
} else {
|
||||
Transition::None
|
||||
};
|
||||
@@ -99,7 +94,6 @@ impl fmt::Display for Exit {
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{Transition, Exit, Position};
|
||||
use std::str::FromStr;
|
||||
|
||||
#[test]
|
||||
fn exit_from_string() {
|
||||
|
||||
336
src/game.rs
336
src/game.rs
@@ -1,8 +1,11 @@
|
||||
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 std::str::FromStr;
|
||||
use std::collections::HashMap;
|
||||
use std::borrow::BorrowMut;
|
||||
use std::fmt;
|
||||
@@ -30,9 +33,9 @@ impl RoomFormat {
|
||||
impl Display for RoomFormat {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", match &self {
|
||||
RoomFormat::Contiguous => "0",
|
||||
RoomFormat::CommaSeparated => "1",
|
||||
})
|
||||
RoomFormat::Contiguous => 0,
|
||||
RoomFormat::CommaSeparated => 1,
|
||||
}.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,12 +43,13 @@ impl Display for RoomFormat {
|
||||
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
|
||||
pub enum RoomType {Room, Set}
|
||||
|
||||
impl ToString for RoomType {
|
||||
fn to_string(&self) -> String {
|
||||
match &self {
|
||||
impl Display for RoomType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let str = match &self {
|
||||
RoomType::Set => "SET",
|
||||
RoomType::Room => "ROOM",
|
||||
}.to_string()
|
||||
}.to_string();
|
||||
write!(f, "{}", str)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,53 +60,56 @@ pub struct Version {
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct InvalidVersion;
|
||||
pub enum VersionError {
|
||||
MissingParts,
|
||||
ExtraneousParts,
|
||||
MalformedInteger,
|
||||
}
|
||||
|
||||
impl Display for VersionError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", match self {
|
||||
VersionError::MissingParts => "Not enough parts supplied for version",
|
||||
VersionError::ExtraneousParts => "Too many parts supplied for version",
|
||||
VersionError::MalformedInteger => "Version did not contain valid integers",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for VersionError {}
|
||||
|
||||
impl Version {
|
||||
fn from(str: &str) -> Result<Version, InvalidVersion> {
|
||||
fn from(str: &str) -> Result<Version, VersionError> {
|
||||
let parts: Vec<&str> = str.split('.').collect();
|
||||
|
||||
if parts.len() == 2 {
|
||||
Ok(Version {
|
||||
major: parts[0].parse().unwrap(),
|
||||
minor: parts[1].parse().unwrap(),
|
||||
})
|
||||
if parts.len() < 2 {
|
||||
Err(VersionError::MissingParts)
|
||||
} else if parts.len() > 2 {
|
||||
Err(VersionError::ExtraneousParts)
|
||||
} else if let (Ok(major), Ok(minor)) = (parts[0].parse(), parts[1].parse()) {
|
||||
Ok(Version { major, minor })
|
||||
} else {
|
||||
Err (InvalidVersion)
|
||||
Err(VersionError::MalformedInteger)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum NotFound {
|
||||
/// no game data whatsoever
|
||||
Anything,
|
||||
Avatar,
|
||||
Room,
|
||||
Sprite,
|
||||
Tile,
|
||||
}
|
||||
|
||||
impl 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, PartialEq)]
|
||||
pub struct Game {
|
||||
pub name: String,
|
||||
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>,
|
||||
/// 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 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 palettes: Vec<Palette>,
|
||||
pub rooms: Vec<Room>,
|
||||
@@ -112,31 +119,30 @@ pub struct Game {
|
||||
pub dialogues: Vec<Dialogue>,
|
||||
pub endings: Vec<Ending>,
|
||||
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(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 {
|
||||
// todo return (Result<Game, ?>, Vec<Box<dyn Error>>)?
|
||||
// would be nice to *try* to parse a game, and catalogue any and all errors without crashing,
|
||||
// for display purposes etc.
|
||||
pub fn from(string: String) -> Result<Game, NotFound> {
|
||||
pub fn from(string: String) -> Result<(Game, Vec<Error>), NotFound> {
|
||||
if string.trim() == "" {
|
||||
return Err(NotFound::Anything);
|
||||
}
|
||||
|
||||
let mut warnings = Vec::new();
|
||||
|
||||
let line_endings_crlf = string.contains("\r\n");
|
||||
let mut string = string;
|
||||
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 mut segments = segments_from_string(string);
|
||||
let mut segments = crate::segments_from_str(&string);
|
||||
|
||||
let mut name = "".to_string();
|
||||
|
||||
@@ -172,7 +178,11 @@ impl Game {
|
||||
let mut font_data: Option<String> = None;
|
||||
|
||||
let mut version = None;
|
||||
let mut version_major = None;
|
||||
let mut version_minor = None;
|
||||
let mut room_format = None;
|
||||
let mut dialogue_compatibility = None;
|
||||
let mut text_mode = None;
|
||||
let mut room_type = RoomType::Room;
|
||||
let mut font = Font::AsciiSmall;
|
||||
let mut custom_font = None;
|
||||
@@ -184,18 +194,55 @@ impl Game {
|
||||
let mut items: Vec<Item> = Vec::new();
|
||||
let mut avatar_exists = false;
|
||||
|
||||
// todo can we use multithreading here?
|
||||
for segment in segments {
|
||||
if segment.starts_with("# BITSY VERSION") {
|
||||
let segment = segment.replace("# BITSY VERSION ", "");
|
||||
let segment = Version::from(&segment);
|
||||
if let Ok(segment) = segment {
|
||||
version = Some(segment);
|
||||
let result = Version::from(&segment);
|
||||
|
||||
if let Ok(v) = result {
|
||||
version = Some(v);
|
||||
} else {
|
||||
warnings.push(Error::Version);
|
||||
}
|
||||
} 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("! ") {
|
||||
// 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("DEFAULT_FONT") {
|
||||
let segment = segment.replace("DEFAULT_FONT ", "");
|
||||
|
||||
@@ -207,7 +254,13 @@ impl Game {
|
||||
} else if segment.trim() == "TEXT_DIRECTION RTL" {
|
||||
text_direction = TextDirection::RightToLeft;
|
||||
} 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 ") {
|
||||
if segment.starts_with("SET ") {
|
||||
room_type = RoomType::Set;
|
||||
@@ -216,20 +269,39 @@ impl Game {
|
||||
} else if segment.starts_with("TIL ") {
|
||||
tiles.push(Tile::from(segment));
|
||||
} else if segment.starts_with("SPR ") {
|
||||
let sprite = Sprite::from(segment);
|
||||
let result = Sprite::from_str(&segment);
|
||||
|
||||
if let Ok(sprite) = sprite {
|
||||
if let Ok(sprite) = result {
|
||||
avatar_exists |= sprite.id == "A";
|
||||
|
||||
sprites.push(sprite);
|
||||
} else {
|
||||
warnings.push(result.unwrap_err());
|
||||
}
|
||||
} 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 ") {
|
||||
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 ") {
|
||||
if let Ok(ending) = Ending::from_str(&segment) {
|
||||
let result = Ending::from_str(&segment);
|
||||
|
||||
if let Ok(ending) = result {
|
||||
endings.push(ending);
|
||||
} else {
|
||||
warnings.push(result.unwrap_err());
|
||||
}
|
||||
} else if segment.starts_with("VAR ") {
|
||||
variables.push(Variable::from(segment));
|
||||
@@ -239,14 +311,19 @@ impl Game {
|
||||
}
|
||||
|
||||
if ! avatar_exists {
|
||||
return Err(NotFound::Avatar);
|
||||
warnings.push(Error::Game { missing: NotFound::Avatar });
|
||||
}
|
||||
|
||||
Ok(
|
||||
(
|
||||
Game {
|
||||
name,
|
||||
version,
|
||||
version_major,
|
||||
version_minor,
|
||||
room_format,
|
||||
dialogue_compatibility,
|
||||
text_mode,
|
||||
room_type,
|
||||
font,
|
||||
custom_font,
|
||||
@@ -259,9 +336,13 @@ impl Game {
|
||||
dialogues,
|
||||
endings,
|
||||
variables,
|
||||
tunes: vec![],
|
||||
blips: vec![],
|
||||
font_data,
|
||||
line_endings_crlf,
|
||||
}
|
||||
},
|
||||
warnings
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -317,20 +398,17 @@ impl Game {
|
||||
}
|
||||
|
||||
pub fn get_tiles_for_room(&self, id: String) -> Result<Vec<&Tile>, NotFound> {
|
||||
let room = self.get_room_by_id(id);
|
||||
if room.is_err() {
|
||||
return Err(NotFound::Room);
|
||||
}
|
||||
let mut tile_ids = room.unwrap().tiles.clone();
|
||||
let room = self.get_room_by_id(id)?;
|
||||
let mut tile_ids = room.tiles.clone();
|
||||
tile_ids.sort();
|
||||
tile_ids.dedup();
|
||||
// remove 0 as this isn't a real tile
|
||||
let zero_index = tile_ids.iter()
|
||||
.position(|i| i == "0");
|
||||
if let Some(zero_index) = zero_index {
|
||||
|
||||
// remove "0" as this isn't a real tile
|
||||
if let Some(zero_index) = tile_ids.iter().position(|i| i == "0") {
|
||||
tile_ids.remove(zero_index);
|
||||
}
|
||||
// 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))
|
||||
}
|
||||
|
||||
@@ -505,13 +583,13 @@ impl Game {
|
||||
self.add_room(room);
|
||||
}
|
||||
|
||||
// a sprite has a dialogue ID, so we need to handle these after dialogues
|
||||
// a sprite has a position in a room, so we need to handle these after the rooms
|
||||
// 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 sprites after rooms
|
||||
for sprite in &game.sprites {
|
||||
let mut sprite = sprite.clone();
|
||||
// avoid having two avatars
|
||||
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 let Some(key) = sprite.dialogue_id.clone() {
|
||||
@@ -533,8 +611,8 @@ impl Game {
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for Game {
|
||||
fn to_string(&self) -> String {
|
||||
impl Display for Game {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut segments: Vec<String> = Vec::new();
|
||||
|
||||
// todo refactor
|
||||
@@ -560,7 +638,9 @@ impl ToString for Game {
|
||||
}
|
||||
|
||||
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\"\"\"", ""));
|
||||
}
|
||||
|
||||
@@ -576,18 +656,23 @@ impl ToString for Game {
|
||||
segments.push(self.font_data.to_owned().unwrap())
|
||||
}
|
||||
|
||||
transform_line_endings(
|
||||
let str = transform_line_endings(
|
||||
format!(
|
||||
"{}{}{}{}{}\n\n{}\n\n",
|
||||
"{}{}{}{}{}{}{}{}{}\n\n{}\n\n",
|
||||
&self.name,
|
||||
&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.text_direction_line(),
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -604,6 +689,7 @@ impl Game {
|
||||
pub fn sprite_ids(&self) -> Vec<String> {
|
||||
self.sprites.iter().map(|sprite| sprite.id.clone()).collect()
|
||||
}
|
||||
|
||||
pub fn room_ids(&self) -> Vec<String> {
|
||||
self.rooms.iter().map(|room| room.id.clone()).collect()
|
||||
}
|
||||
@@ -664,6 +750,10 @@ impl Game {
|
||||
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?
|
||||
pub fn get_tile_id(&self, matching_tile: &Tile) -> Option<String> {
|
||||
for tile in &self.tiles {
|
||||
@@ -799,16 +889,49 @@ impl Game {
|
||||
if self.version.is_some() {
|
||||
format!(
|
||||
"\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 {
|
||||
"".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 {
|
||||
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 {
|
||||
"".to_string()
|
||||
}
|
||||
@@ -842,11 +965,14 @@ impl Game {
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{TextDirection, Font, Version, Game, NotFound, Tile, Image};
|
||||
use crate::{TextDirection, Font, Version, Game, Tile, Image, Palette, Colour};
|
||||
|
||||
#[test]
|
||||
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();
|
||||
|
||||
assert_eq!(output, expected);
|
||||
@@ -878,7 +1004,7 @@ mod test {
|
||||
for n in 1..10 {
|
||||
if n != 4 {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -909,7 +1035,7 @@ mod test {
|
||||
|
||||
#[test]
|
||||
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.text_direction, TextDirection::RightToLeft);
|
||||
@@ -1067,7 +1193,25 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn empty_game_data_throws_error() {
|
||||
assert_eq!(Game::from("".to_string() ).err().unwrap(), NotFound::Anything);
|
||||
assert_eq!(Game::from(" \n \r\n".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()).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);
|
||||
}
|
||||
}
|
||||
|
||||
90
src/image.rs
90
src/image.rs
@@ -1,3 +1,7 @@
|
||||
use std::fmt;
|
||||
use crate::Error;
|
||||
use crate::error::ImageError;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Image {
|
||||
pub pixels: Vec<u8>, // 64 for SD, 256 for HD
|
||||
@@ -49,12 +53,16 @@ impl Image {
|
||||
|
||||
self.pixels = pixels;
|
||||
}
|
||||
|
||||
fn from_str(str: &str) -> Result<(Image, Vec<crate::Error>), crate::Error> {
|
||||
let mut warnings = Vec::new();
|
||||
|
||||
if str.contains("NaN") {
|
||||
warnings.push(crate::Error::Image { err: ImageError::MalformedPixel });
|
||||
}
|
||||
|
||||
impl From<String> for Image {
|
||||
fn from(string: String) -> Image {
|
||||
let string = string.replace("NaN", "0");
|
||||
let string = string.trim();
|
||||
let string = str.trim().replace("NaN", "0");
|
||||
|
||||
let lines: Vec<&str> = string.lines().collect();
|
||||
let dimension = lines.len();
|
||||
let mut pixels: Vec<u8> = Vec::new();
|
||||
@@ -62,16 +70,30 @@ impl From<String> for Image {
|
||||
for line in lines {
|
||||
let line = &line[..dimension];
|
||||
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 {
|
||||
fn to_string(&self) -> String {
|
||||
impl fmt::Display for Image {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut string = String::new();
|
||||
|
||||
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
|
||||
write!(f, "{}", string)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn animation_frames_from_string(string: String) -> Vec<Image> {
|
||||
let frames: Vec<&str> = string.split('>').collect();
|
||||
/// todo return Result<(Vec<Image>, Vec<crate::Error>), crate::Error>
|
||||
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)]
|
||||
mod test {
|
||||
use crate::image::{Image, animation_frames_from_string};
|
||||
use crate::image::{Image, animation_frames_from_str};
|
||||
use crate::mock;
|
||||
|
||||
#[test]
|
||||
fn image_from_string() {
|
||||
let output = Image::from(
|
||||
include_str!("test-resources/image").to_string()
|
||||
);
|
||||
let (output, _) = Image::from_str(include_str!("test-resources/image")).unwrap();
|
||||
|
||||
let expected = Image {
|
||||
pixels: vec![
|
||||
@@ -130,9 +178,9 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn test_animation_frames_from_string() {
|
||||
let output = animation_frames_from_string(
|
||||
include_str!("test-resources/animation_frames").to_string()
|
||||
);
|
||||
let output = animation_frames_from_str(
|
||||
include_str!("test-resources/animation_frames")
|
||||
).unwrap().0;
|
||||
|
||||
let expected = mock::image::animation_frames();
|
||||
|
||||
@@ -143,9 +191,7 @@ mod test {
|
||||
/// check that these extraneous pixels are stripped out
|
||||
#[test]
|
||||
fn image_out_of_bounds() {
|
||||
let output = Image::from(
|
||||
include_str!("test-resources/image-oob").to_string()
|
||||
);
|
||||
let (output, _) = Image::from_str(include_str!("test-resources/image-oob")).unwrap();
|
||||
|
||||
let expected = Image {
|
||||
pixels: vec![
|
||||
|
||||
51
src/item.rs
51
src/item.rs
@@ -1,5 +1,6 @@
|
||||
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)]
|
||||
pub struct Item {
|
||||
@@ -8,6 +9,7 @@ pub struct Item {
|
||||
pub name: Option<String>,
|
||||
pub dialogue_id: Option<String>,
|
||||
pub colour_id: Option<u64>,
|
||||
pub blip: Option<String>,
|
||||
}
|
||||
|
||||
impl Item {
|
||||
@@ -22,21 +24,32 @@ impl Item {
|
||||
fn colour_line(&self) -> String {
|
||||
optional_data_line("COL", self.colour_id.as_ref())
|
||||
}
|
||||
|
||||
fn blip_line(&self) -> String {
|
||||
optional_data_line("BLIP", self.blip.as_ref())
|
||||
}
|
||||
|
||||
impl From<String> for Item {
|
||||
fn from(string: String) -> Item {
|
||||
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 mut name = None;
|
||||
let mut dialogue_id = None;
|
||||
let mut colour_id: Option<u64> = None;
|
||||
let mut blip = None;
|
||||
|
||||
let mut warnings = Vec::new();
|
||||
|
||||
loop {
|
||||
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());
|
||||
} else if last_line.starts_with("DLG") {
|
||||
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(
|
||||
lines[1..].join("\n")
|
||||
);
|
||||
let (animation_frames, mut animation_warnings) = animation_frames_from_str(
|
||||
&lines[1..].join("\n")
|
||||
).unwrap();
|
||||
|
||||
Item {
|
||||
id,
|
||||
name,
|
||||
animation_frames,
|
||||
dialogue_id,
|
||||
colour_id,
|
||||
}
|
||||
warnings.append(&mut animation_warnings);
|
||||
|
||||
Ok((Item { id, name, animation_frames, dialogue_id, colour_id, blip }, warnings))
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for Item {
|
||||
fn to_string(&self) -> String {
|
||||
format!(
|
||||
"ITM {}\n{}{}{}{}",
|
||||
impl fmt::Display for Item {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"ITM {}\n{}{}{}{}{}",
|
||||
self.id,
|
||||
self.animation_frames.to_string(),
|
||||
self.name_line(),
|
||||
self.dialogue_line(),
|
||||
self.colour_line(),
|
||||
self.blip_line(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -81,7 +92,7 @@ mod test {
|
||||
|
||||
#[test]
|
||||
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();
|
||||
assert_eq!(output, expected);
|
||||
}
|
||||
|
||||
20
src/lib.rs
20
src/lib.rs
@@ -1,12 +1,16 @@
|
||||
extern crate core;
|
||||
|
||||
use std::fmt::Display;
|
||||
use std::io::Cursor;
|
||||
|
||||
use radix_fmt::radix_36;
|
||||
use loe::{process, Config, TransformMode};
|
||||
|
||||
pub mod blip;
|
||||
pub mod colour;
|
||||
pub mod dialogue;
|
||||
pub mod ending;
|
||||
pub mod error;
|
||||
pub mod exit;
|
||||
pub mod game;
|
||||
pub mod image;
|
||||
@@ -19,11 +23,15 @@ pub mod sprite;
|
||||
pub mod text;
|
||||
pub mod tile;
|
||||
pub mod variable;
|
||||
pub mod note;
|
||||
pub mod tune;
|
||||
pub mod test_omnibus;
|
||||
|
||||
pub use blip::Blip;
|
||||
pub use colour::Colour;
|
||||
pub use dialogue::Dialogue;
|
||||
pub use ending::Ending;
|
||||
pub use error::Error;
|
||||
pub use exit::*;
|
||||
pub use game::*;
|
||||
pub use image::Image;
|
||||
@@ -34,6 +42,7 @@ pub use room::Room;
|
||||
pub use sprite::Sprite;
|
||||
pub use text::*;
|
||||
pub use tile::Tile;
|
||||
pub use tune::Tune;
|
||||
pub use variable::Variable;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
@@ -44,6 +53,7 @@ pub struct Instance {
|
||||
|
||||
/// a Room can have many Exits in different positions,
|
||||
/// optionally with a transition and dialogue
|
||||
/// todo make a from_str() function for this
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ExitInstance {
|
||||
position: Position,
|
||||
@@ -105,11 +115,11 @@ fn transform_line_endings(input: String, mode: TransformMode) -> String {
|
||||
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
|
||||
// however, on entering two empty lines, dialogue will be wrapped in triple quotation marks
|
||||
// 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();
|
||||
// are we inside `"""\n...\n"""`? if so, ignore empty lines
|
||||
@@ -184,7 +194,7 @@ impl Unquote for String {
|
||||
|
||||
#[cfg(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]
|
||||
fn to_base36() {
|
||||
@@ -199,9 +209,7 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn string_to_segments() {
|
||||
let output = segments_from_string(
|
||||
include_str!("./test-resources/segments").to_string()
|
||||
);
|
||||
let output = segments_from_str(include_str!("./test-resources/segments"));
|
||||
|
||||
let expected = vec![
|
||||
"\"\"\"\nthe first segment is a long bit of text\n\n\nit contains empty lines\n\n\"\"\"".to_string(),
|
||||
|
||||
21
src/mock.rs
21
src/mock.rs
@@ -204,7 +204,8 @@ pub mod item {
|
||||
],
|
||||
name: Some("key".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()),
|
||||
dialogue_id: Some("2".to_string()),
|
||||
colour_id: None,
|
||||
blip: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -529,15 +531,20 @@ pub fn room() -> Room {
|
||||
position: Position { x: 8, y: 7 },
|
||||
id: "undefined".to_string(),
|
||||
}],
|
||||
walls: vec![],
|
||||
walls: None,
|
||||
tune: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn game_default() -> Game {
|
||||
Game {
|
||||
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),
|
||||
dialogue_compatibility: Some(0),
|
||||
text_mode: Some(0),
|
||||
room_type: RoomType::Room,
|
||||
font: Font::AsciiSmall,
|
||||
custom_font: None,
|
||||
@@ -828,9 +835,10 @@ pub fn game_default() -> Game {
|
||||
items: vec![],
|
||||
exits: vec![],
|
||||
endings: vec![],
|
||||
walls: vec![],
|
||||
walls: None,
|
||||
tune: None,
|
||||
}],
|
||||
tiles: vec![self::tile_default()],
|
||||
tiles: vec![tile_default()],
|
||||
sprites: vec![
|
||||
Sprite {
|
||||
id: "A".to_string(),
|
||||
@@ -878,6 +886,7 @@ pub fn game_default() -> Game {
|
||||
name: Some("tea".to_string()),
|
||||
dialogue_id: Some("1".to_string()),
|
||||
colour_id: None,
|
||||
blip: None,
|
||||
},
|
||||
item::key()
|
||||
],
|
||||
@@ -903,6 +912,8 @@ pub fn game_default() -> Game {
|
||||
id: "a".to_string(),
|
||||
initial_value: "42".to_string(),
|
||||
}],
|
||||
tunes: vec![],
|
||||
blips: vec![],
|
||||
font_data: None,
|
||||
line_endings_crlf: false
|
||||
}
|
||||
|
||||
109
src/note.rs
Normal file
109
src/note.rs
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::colour::Colour;
|
||||
use crate::Colour;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Palette {
|
||||
@@ -7,25 +7,39 @@ pub struct Palette {
|
||||
pub colours: Vec<Colour>,
|
||||
}
|
||||
|
||||
impl From<String> for Palette {
|
||||
fn from(string: String) -> Palette {
|
||||
let lines: Vec<&str> = string.lines().collect();
|
||||
impl Palette {
|
||||
pub fn from_str(s: &str) -> Result<(Palette, Vec<crate::Error>), crate::Error> {
|
||||
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") {
|
||||
true => Some(lines[1].replace("NAME ", "")),
|
||||
false => None,
|
||||
};
|
||||
let mut id = String::new();
|
||||
let mut name = 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..]
|
||||
.iter()
|
||||
.map(|&line| Colour::from(line).unwrap())
|
||||
.collect();
|
||||
if line.starts_with("PAL ") {
|
||||
id = line.replace("PAL ", "");
|
||||
} else if line.starts_with("NAME ") {
|
||||
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]
|
||||
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 {
|
||||
id: "1".to_string(),
|
||||
@@ -82,7 +96,7 @@ mod test {
|
||||
|
||||
#[test]
|
||||
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 {
|
||||
id: "9".to_string(),
|
||||
@@ -131,9 +145,9 @@ mod test {
|
||||
blue: 128,
|
||||
},
|
||||
],
|
||||
}
|
||||
.to_string();
|
||||
let expected = "PAL g\nNAME moss\n1,2,3\n255,254,253\n126,127,128".to_string();
|
||||
}.to_string();
|
||||
|
||||
let expected = "PAL g\nNAME moss\n1,2,3\n255,254,253\n126,127,128";
|
||||
assert_eq!(output, expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Position {
|
||||
@@ -8,12 +6,8 @@ pub struct Position {
|
||||
pub y: u8,
|
||||
}
|
||||
|
||||
impl Error for Position {}
|
||||
|
||||
impl FromStr for Position {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
impl Position {
|
||||
pub fn from_str(s: &str) -> Result<Self, crate::Error> {
|
||||
let mut parts = s.split(',');
|
||||
|
||||
let x = parts.next().unwrap();
|
||||
@@ -22,7 +16,7 @@ impl FromStr for Position {
|
||||
if let (Ok(x), Ok(y)) = (x.parse(), y.parse()) {
|
||||
Ok(Position { x, y })
|
||||
} else {
|
||||
Err(format!("Malformed position string: {}", s))
|
||||
Err(crate::Error::Position)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +30,6 @@ impl fmt::Display for Position {
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::Position;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[test]
|
||||
fn position_from_str() {
|
||||
|
||||
39
src/room.rs
39
src/room.rs
@@ -9,7 +9,6 @@ use crate::{
|
||||
Transition
|
||||
};
|
||||
|
||||
use std::str::FromStr;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
@@ -24,7 +23,8 @@ pub struct Room {
|
||||
pub exits: Vec<ExitInstance>,
|
||||
pub endings: Vec<Instance>,
|
||||
/// 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 {
|
||||
@@ -33,10 +33,10 @@ impl Room {
|
||||
}
|
||||
|
||||
fn wall_line(&self) -> String {
|
||||
if self.walls.is_empty() {
|
||||
"".to_string()
|
||||
if let Some(walls) = &self.walls {
|
||||
optional_data_line("WAL", Some(walls.join(",")))
|
||||
} else {
|
||||
optional_data_line("WAL", Some(self.walls.join(",")))
|
||||
"".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,13 @@ impl Room {
|
||||
None => "".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn tune_line(&self) -> String {
|
||||
match &self.tune {
|
||||
Some(id) => optional_data_line("TUNE", Some(id.clone())),
|
||||
None => "".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Room {
|
||||
@@ -59,15 +66,18 @@ impl From<String> for Room {
|
||||
let mut items: Vec<Instance> = Vec::new();
|
||||
let mut exits: Vec<ExitInstance> = 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 {
|
||||
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 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") {
|
||||
name = Some(last_line.replace("NAME ", "").to_string());
|
||||
} else if last_line.starts_with("PAL") {
|
||||
@@ -94,14 +104,17 @@ impl From<String> for Room {
|
||||
if let Ok(exit) = exit {
|
||||
let mut transition = None;
|
||||
let mut dialogue_id = None;
|
||||
|
||||
let chunks = parts[3..].chunks(2);
|
||||
|
||||
for chunk in chunks {
|
||||
if chunk[0] == "FX" {
|
||||
transition = Some(Transition::from(chunk[1]));
|
||||
transition = Some(Transition::from_str(chunk[1]).unwrap());
|
||||
} else if chunk[0] == "DLG" {
|
||||
dialogue_id = Some(chunk[1].to_string());
|
||||
}
|
||||
}
|
||||
|
||||
exits.push(ExitInstance { position, exit, transition, dialogue_id });
|
||||
}
|
||||
}
|
||||
@@ -152,6 +165,7 @@ impl From<String> for Room {
|
||||
exits,
|
||||
endings,
|
||||
walls,
|
||||
tune,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -211,7 +225,7 @@ impl Room {
|
||||
}
|
||||
|
||||
format!(
|
||||
"{} {}\n{}{}{}{}{}{}{}",
|
||||
"{} {}\n{}{}{}{}{}{}{}{}",
|
||||
room_type.to_string(),
|
||||
self.id,
|
||||
tiles,
|
||||
@@ -220,7 +234,8 @@ impl Room {
|
||||
items,
|
||||
exits,
|
||||
endings,
|
||||
self.palette_line()
|
||||
self.palette_line(),
|
||||
self.tune_line(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -256,6 +271,6 @@ mod test {
|
||||
fn room_walls_array() {
|
||||
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()]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::{optional_data_line, AnimationFrames, Image, Position};
|
||||
use crate::image::animation_frames_from_string;
|
||||
use std::str::FromStr;
|
||||
use crate::image::animation_frames_from_str;
|
||||
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Sprite {
|
||||
@@ -50,16 +51,14 @@ impl Sprite {
|
||||
format!("\n{}", lines.join("\n"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(str: &str) -> Result<Sprite, crate::Error> {
|
||||
let mut lines: Vec<&str> = str.lines().collect();
|
||||
|
||||
if lines.is_empty() || !lines[0].starts_with("SPR ") {
|
||||
return Err(crate::Error::Sprite);
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SpriteMissingRoomPosition;
|
||||
// todo "malformed sprite ID" or something
|
||||
|
||||
impl Sprite {
|
||||
pub(crate) fn from(string: String) -> Result<Sprite, SpriteMissingRoomPosition> {
|
||||
let mut lines: Vec<&str> = string.lines().collect();
|
||||
|
||||
let id = lines[0].replace("SPR ", "");
|
||||
let mut name = None;
|
||||
let mut dialogue_id: Option<String> = None;
|
||||
@@ -81,13 +80,13 @@ impl Sprite {
|
||||
room_id = Some(room_position[0].to_string());
|
||||
|
||||
if room_position.len() < 2 {
|
||||
return Err(SpriteMissingRoomPosition);
|
||||
return Err(crate::Error::Sprite);
|
||||
}
|
||||
|
||||
if let Ok(pos) = Position::from_str(room_position[1]) {
|
||||
position = Some(pos);
|
||||
} else {
|
||||
return Err(SpriteMissingRoomPosition);
|
||||
return Err(crate::Error::Sprite);
|
||||
}
|
||||
} else if last_line.starts_with("COL") {
|
||||
colour_id = Some(last_line.replace("COL ", "").parse().unwrap());
|
||||
@@ -101,9 +100,14 @@ impl Sprite {
|
||||
|
||||
items.reverse();
|
||||
|
||||
let animation_frames = animation_frames_from_string(
|
||||
lines[1..].join("\n")
|
||||
);
|
||||
let animation_frames = match animation_frames_from_str(&lines[1..].join("\n")) {
|
||||
Ok((frames, _warnings)) => {
|
||||
frames
|
||||
},
|
||||
Err(_e) => {
|
||||
Vec::new()
|
||||
},
|
||||
};
|
||||
|
||||
Ok(Sprite {
|
||||
id,
|
||||
@@ -118,9 +122,10 @@ impl Sprite {
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for Sprite {
|
||||
fn to_string(&self) -> String {
|
||||
format!(
|
||||
impl fmt::Display for Sprite {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"SPR {}\n{}{}{}{}{}{}",
|
||||
self.id,
|
||||
self.animation_frames.to_string(),
|
||||
@@ -139,8 +144,7 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn sprite_from_string() {
|
||||
let string = include_str!("test-resources/sprite").to_string();
|
||||
let output = Sprite::from(string).unwrap();
|
||||
let output = Sprite::from_str(include_str!("test-resources/sprite")).unwrap();
|
||||
let expected = mock::sprite();
|
||||
|
||||
assert_eq!(output, expected);
|
||||
@@ -148,6 +152,6 @@ mod test {
|
||||
|
||||
#[test]
|
||||
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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
Write your game's title here
|
||||
|
||||
# BITSY VERSION 7.2
|
||||
# BITSY VERSION 8.12
|
||||
|
||||
! VER_MAJ 8
|
||||
! VER_MIN 12
|
||||
! ROOM_FORMAT 1
|
||||
! DLG_COMPAT 0
|
||||
! TXT_MODE 0
|
||||
|
||||
PAL 0
|
||||
NAME blueprint
|
||||
0,82,204
|
||||
128,159,255
|
||||
255,255,255
|
||||
NAME blueprint
|
||||
|
||||
ROOM 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
|
||||
NAME example room
|
||||
PAL 0
|
||||
TUNE 2
|
||||
|
||||
TIL a
|
||||
11111111
|
||||
@@ -64,6 +69,7 @@ SPR a
|
||||
NAME cat
|
||||
DLG 0
|
||||
POS 0 8,12
|
||||
BLIP 1
|
||||
|
||||
ITM 0
|
||||
00000000
|
||||
@@ -88,6 +94,7 @@ ITM 1
|
||||
00011000
|
||||
NAME key
|
||||
DLG 2
|
||||
BLIP 2
|
||||
|
||||
DLG 0
|
||||
I'm a cat
|
||||
@@ -104,3 +111,91 @@ NAME key dialog
|
||||
VAR a
|
||||
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
|
||||
|
||||
|
||||
@@ -4,11 +4,12 @@ mod test {
|
||||
|
||||
/// bitsy-parser will parse these games correctly
|
||||
/// but the output does not match the input, due to game data errors.
|
||||
const ACCEPTED_FAILURES: [&str; 31] = [
|
||||
const ACCEPTED_FAILURES: [&str; 32] = [
|
||||
// position out of bounds e.g. "5, -1"
|
||||
"CFE62F11", // "SweetPea Village",
|
||||
"013B3CDE", // "Sunset Shore",
|
||||
"65C2B499", // "==GIRLS OWN THE VOID=={br}a faux platformer",
|
||||
"74E0F6EF", // this one has no name. the files is huge so the test hangs, but it does parse ok apart from a bad item position.
|
||||
// extra tiles in room (this was an old editor bug)
|
||||
"07836D6F", // "I can't run anymore. They won't give up. I need water.",
|
||||
"12490381", // "Picnic at Castle Island",
|
||||
@@ -54,7 +55,7 @@ mod test {
|
||||
|
||||
assert!(result.is_ok());
|
||||
|
||||
let game = result.expect("failed to parse game");
|
||||
let (game, _) = result.expect("failed to parse game");
|
||||
|
||||
if ACCEPTED_FAILURES.contains(&id) {
|
||||
return;
|
||||
@@ -67,6 +68,7 @@ mod test {
|
||||
);
|
||||
}
|
||||
|
||||
#[test] fn test_the_rest_of_your_life() {str(include_str!("test-resources/omnibus/the-rest-of-your-life.bitsy.txt"), "the-rest-of-your-life");}
|
||||
#[test] fn test_0053b32f() {str(include_str!("test-resources/omnibus/0053B32F.bitsy.txt"), "0053B32F");}
|
||||
#[test] fn test_00e45dc5() {str(include_str!("test-resources/omnibus/00E45DC5.bitsy.txt"), "00E45DC5");}
|
||||
#[test] fn test_010beb39() {str(include_str!("test-resources/omnibus/010BEB39.bitsy.txt"), "010BEB39");}
|
||||
@@ -519,4 +521,5 @@ mod test {
|
||||
#[test] fn test_fe6547de() {str(include_str!("test-resources/omnibus/FE6547DE.bitsy.txt"), "FE6547DE");}
|
||||
#[test] fn test_ff3857ae() {str(include_str!("test-resources/omnibus/FF3857AE.bitsy.txt"), "FF3857AE");}
|
||||
#[test] fn test_ff7bcf9c() {str(include_str!("test-resources/omnibus/FF7BCF9C.bitsy.txt"), "FF7BCF9C");}
|
||||
#[test] fn test_goodbyes() {str(include_str!("test-resources/goodbye_summer.bitsy"), "goodbye_summer");}
|
||||
}
|
||||
|
||||
@@ -19,13 +19,13 @@ impl Font {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn to_string(&self) -> Result<String, &'static str> {
|
||||
pub(crate) fn to_string(&self) -> Result<String, crate::Error> {
|
||||
match &self {
|
||||
Font::UnicodeEuropeanSmall => Ok("unicode_european_small".to_string()),
|
||||
Font::UnicodeEuropeanLarge => Ok("unicode_european_large".to_string()),
|
||||
Font::UnicodeAsian => Ok("unicode_asian".to_string()),
|
||||
Font::Arabic => Ok("arabic".to_string()),
|
||||
_ => Err("No string available for this Font"),
|
||||
_ => Err(crate::Error::Font),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
26
src/tile.rs
26
src/tile.rs
@@ -1,18 +1,23 @@
|
||||
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)]
|
||||
pub struct Tile {
|
||||
pub id: String,
|
||||
pub name: Option<String>,
|
||||
/// this is "optional" in that a tile can have `WAL true`, `WAL false` or neither
|
||||
/// obviously Some(false) is the same as None but we want to preserve the original formatting
|
||||
/// Can the player move over this tile?
|
||||
/// 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 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>,
|
||||
}
|
||||
|
||||
impl PartialEq for Tile {
|
||||
/// ignore id and name.
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.wall == other.wall
|
||||
&&
|
||||
@@ -44,6 +49,7 @@ impl Tile {
|
||||
}
|
||||
|
||||
// todo refactor
|
||||
// can we do map_in_place or something?
|
||||
|
||||
pub fn invert(&mut self) {
|
||||
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(
|
||||
lines[1..].join("\n")
|
||||
);
|
||||
let animation_frames = match animation_frames_from_str(&lines[1..].join("\n")) {
|
||||
Ok((animation_frames, _warnings)) => {
|
||||
animation_frames
|
||||
},
|
||||
Err(_) => {
|
||||
Vec::new()
|
||||
},
|
||||
};
|
||||
|
||||
Tile {
|
||||
id,
|
||||
@@ -162,8 +173,7 @@ mod test {
|
||||
mock::image::chequers_2(),
|
||||
],
|
||||
colour_id: None,
|
||||
}
|
||||
.to_string();
|
||||
}.to_string();
|
||||
|
||||
let expected = include_str!("test-resources/tile-chequers").to_string();
|
||||
|
||||
|
||||
41
src/tune.rs
Normal file
41
src/tune.rs
Normal 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?)
|
||||
}
|
||||
Reference in New Issue
Block a user