WIP version of 8.12...

This commit is contained in:
Max Bradbury 2024-09-09 22:46:51 +01:00
parent a36313341d
commit 4388c75bb8
15 changed files with 286 additions and 69 deletions

View File

@ -1,6 +1,6 @@
[package] [package]
name = "bitsy-parser" name = "bitsy-parser"
version = "0.710.1" version = "0.812.0"
authors = ["Max Bradbury <max@tinybird.info>"] authors = ["Max Bradbury <max@tinybird.info>"]
edition = "2021" edition = "2021"
description = "A parser and utilities for working with Bitsy game data" description = "A parser and utilities for working with Bitsy game data"
@ -12,6 +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]
data-encoding = "^2.3.1" data-encoding = "^2.6.0"
radix_fmt = "^1.0.0" radix_fmt = "^1.0.0"
loe = "^0.2.0" loe = "0.3.0"

View File

@ -1,6 +1,6 @@
MIT License MIT License
Copyright © 2021 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

@ -2,6 +2,7 @@ use core::fmt;
use std::fmt::Formatter; use std::fmt::Formatter;
use crate::note::Note; use crate::note::Note;
#[derive(Clone, Debug, PartialEq)]
pub enum PulseWidth { pub enum PulseWidth {
/// 50% duty cycle /// 50% duty cycle
Half, Half,
@ -43,9 +44,8 @@ pub struct Blip {
/// first value is milliseconds per note; /// first value is milliseconds per note;
/// second value is a modifier to the first note (add or subtract milliseconds) /// second value is a modifier to the first note (add or subtract milliseconds)
beat: [i16; 2], beat: [i16; 2],
/// potential values are P2, P4 and P8.
pulse_width: PulseWidth, pulse_width: PulseWidth,
/// Notes can sound repeatedly, or just once as the blip fades out. /// Notes can cycle repeatedly, or just play once
repeat: bool, repeat: bool,
} }

View File

@ -1,6 +1,6 @@
use std::fmt; use std::fmt;
#[derive(Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub enum NotFound { pub enum NotFound {
Anything, Anything,
Avatar, Avatar,
@ -21,13 +21,13 @@ impl fmt::Display for NotFound {
} }
} }
#[derive(Debug)] #[derive(Clone, Debug)]
pub enum ImageError { pub enum ImageError {
MalformedPixel, MalformedPixel,
WrongSize, WrongSize,
} }
#[derive(Debug)] #[derive(Clone, Debug)]
pub enum Error { pub enum Error {
Colour, Colour,
Dialogue, Dialogue,
@ -43,6 +43,8 @@ pub enum Error {
Item, Item,
Palette, Palette,
Position, Position,
PulseWidth,
RelativeNote,
Room, Room,
Sprite, Sprite,
Text, Text,

View File

@ -1,11 +1,15 @@
use crate::{Dialogue, Ending, Font, Image, Item, Palette, Room, Sprite, TextDirection, Tile, Variable, transform_line_endings, new_unique_id, try_id, Instance, Error}; 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::collections::HashMap; use std::collections::HashMap;
use std::borrow::BorrowMut; use std::borrow::BorrowMut;
use std::fmt; use std::fmt;
use crate::error::NotFound; 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.
@ -26,12 +30,12 @@ impl RoomFormat {
} }
} }
impl fmt::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())
} }
} }
@ -39,12 +43,13 @@ impl fmt::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)
} }
} }
@ -61,7 +66,7 @@ pub enum VersionError {
MalformedInteger, MalformedInteger,
} }
impl fmt::Display for VersionError { impl Display for VersionError {
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 {
VersionError::MissingParts => "Not enough parts supplied for version", VersionError::MissingParts => "Not enough parts supplied for version",
@ -93,7 +98,14 @@ impl Version {
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,
/// used if font is `Font::Custom` /// used if font is `Font::Custom`
@ -107,15 +119,18 @@ 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
/// true if CRLF (Windows), otherwise LF (unix/mac) /// true if CRLF (Windows), otherwise LF (unix/mac)
/// todo use the enum?
pub(crate) line_endings_crlf: bool, pub(crate) line_endings_crlf: bool,
} }
impl Game { impl Game {
pub fn from(string: String) -> Result<(Game, Vec<crate::Error>), crate::error::NotFound> { pub fn from(string: String) -> Result<(Game, Vec<Error>), NotFound> {
if string.trim() == "" { if string.trim() == "" {
return Err(crate::error::NotFound::Anything); return Err(NotFound::Anything);
} }
let mut warnings = Vec::new(); let mut warnings = Vec::new();
@ -123,7 +138,7 @@ impl Game {
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();
@ -163,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;
@ -186,11 +205,44 @@ impl Game {
} else { } else {
warnings.push(Error::Version); warnings.push(Error::Version);
} }
} else if segment.starts_with("! ROOM_FORMAT") { } else if segment.starts_with("! ") {
let segment = segment.replace("! ROOM_FORMAT ", ""); // this is (potentially?) an entire block,
room_format = Some( // so we need to split it into lines and deal with the lines individually
RoomFormat::from(&segment).unwrap_or(RoomFormat::CommaSeparated) 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") { } else if segment.starts_with("DEFAULT_FONT") {
let segment = segment.replace("DEFAULT_FONT ", ""); let segment = segment.replace("DEFAULT_FONT ", "");
@ -229,8 +281,9 @@ impl Game {
} else if segment.starts_with("ITM ") { } else if segment.starts_with("ITM ") {
let result = Item::from_str(&segment); let result = Item::from_str(&segment);
if let Ok(item) = result { if let Ok((item, mut item_warnings)) = result {
items.push(item); items.push(item);
warnings.append(&mut item_warnings);
} else { } else {
warnings.push(result.unwrap_err()); warnings.push(result.unwrap_err());
} }
@ -258,7 +311,7 @@ impl Game {
} }
if ! avatar_exists { if ! avatar_exists {
warnings.push(crate::Error::Game { missing: NotFound::Avatar }); warnings.push(Error::Game { missing: NotFound::Avatar });
} }
Ok( Ok(
@ -266,7 +319,11 @@ impl Game {
Game { Game {
name, name,
version, version,
version_major,
version_minor,
room_format, room_format,
dialogue_compatibility,
text_mode,
room_type, room_type,
font, font,
custom_font, custom_font,
@ -279,6 +336,8 @@ impl Game {
dialogues, dialogues,
endings, endings,
variables, variables,
tunes: vec![],
blips: vec![],
font_data, font_data,
line_endings_crlf, line_endings_crlf,
}, },
@ -288,40 +347,40 @@ impl Game {
} }
/// todo refactor this into "get T by ID", taking a Vec<T> and an ID name? /// todo refactor this into "get T by ID", taking a Vec<T> and an ID name?
pub fn get_sprite_by_id(&self, id: String) -> Result<&Sprite, crate::error::NotFound> { pub fn get_sprite_by_id(&self, id: String) -> Result<&Sprite, NotFound> {
let index = self.sprites.iter().position( let index = self.sprites.iter().position(
|sprite| sprite.id == id |sprite| sprite.id == id
); );
match index { match index {
Some(index) => Ok(&self.sprites[index]), Some(index) => Ok(&self.sprites[index]),
None => Err(crate::error::NotFound::Sprite), None => Err(NotFound::Sprite),
} }
} }
pub fn get_tile_by_id(&self, id: String) -> Result<&Tile, crate::error::NotFound> { pub fn get_tile_by_id(&self, id: String) -> Result<&Tile, NotFound> {
let index = self.tiles.iter().position( let index = self.tiles.iter().position(
|tile| tile.id == id |tile| tile.id == id
); );
match index { match index {
Some(index) => Ok(&self.tiles[index]), Some(index) => Ok(&self.tiles[index]),
None => Err(crate::error::NotFound::Tile), None => Err(NotFound::Tile),
} }
} }
pub fn get_room_by_id(&self, id: String) -> Result<&Room, crate::error::NotFound> { pub fn get_room_by_id(&self, id: String) -> Result<&Room, NotFound> {
let index = self.rooms.iter().position( let index = self.rooms.iter().position(
|room| room.id == id |room| room.id == id
); );
match index { match index {
Some(index) => Ok(&self.rooms[index]), Some(index) => Ok(&self.rooms[index]),
None => Err(crate::error::NotFound::Room), None => Err(NotFound::Room),
} }
} }
pub fn get_avatar(&self) -> Result<&Sprite, crate::error::NotFound> { pub fn get_avatar(&self) -> Result<&Sprite, NotFound> {
self.get_sprite_by_id("A".to_string()) self.get_sprite_by_id("A".to_string())
} }
@ -338,7 +397,7 @@ impl Game {
tiles tiles
} }
pub fn get_tiles_for_room(&self, id: String) -> Result<Vec<&Tile>, crate::error::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)?;
let mut tile_ids = room.tiles.clone(); let mut tile_ids = room.tiles.clone();
tile_ids.sort(); tile_ids.sort();
@ -552,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
@ -597,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)
} }
} }
@ -825,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()
} }
@ -907,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);
} }
} }

View File

@ -1,4 +1,5 @@
use std::fmt; use std::fmt;
use crate::Error;
use crate::error::ImageError; use crate::error::ImageError;
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
@ -86,7 +87,7 @@ impl Image {
if [64, 256].contains(&pixels.len()) { if [64, 256].contains(&pixels.len()) {
Ok((Image { pixels }, warnings)) Ok((Image { pixels }, warnings))
} else { } else {
Err(crate::Error::Image { err: ImageError::WrongSize }) Err(Error::Image { err: ImageError::WrongSize })
} }
} }
} }
@ -110,13 +111,37 @@ impl fmt::Display for Image {
} }
/// todo return Result<(Vec<Image>, Vec<crate::Error>), crate::Error> /// todo return Result<(Vec<Image>, Vec<crate::Error>), crate::Error>
pub fn animation_frames_from_str(str: &str) -> Vec<Image> { pub fn animation_frames_from_str(str: &str) -> Result<(Vec<Image>, Vec<crate::Error>), crate::Error> {
str let mut warnings: Vec<Error> = Vec::new();
let results: Vec<Result<Image, crate::Error>> = str
.split('>') .split('>')
.collect::<Vec<&str>>() .collect::<Vec<&str>>()
.iter() .iter()
.map(|&frame| Image::from_str(frame).unwrap().0) .map(|&frame| {
.collect() 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)]
@ -155,7 +180,7 @@ mod test {
fn test_animation_frames_from_string() { fn test_animation_frames_from_string() {
let output = animation_frames_from_str( let output = animation_frames_from_str(
include_str!("test-resources/animation_frames") include_str!("test-resources/animation_frames")
); ).unwrap().0;
let expected = mock::image::animation_frames(); let expected = mock::image::animation_frames();

View File

@ -1,9 +1,12 @@
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;
@ -20,8 +23,11 @@ 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;
@ -36,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)]

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,
} }
} }
@ -530,14 +532,19 @@ pub fn room() -> Room {
id: "undefined".to_string(), id: "undefined".to_string(),
}], }],
walls: None, 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: 10 }), 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,
@ -829,8 +836,9 @@ pub fn game_default() -> Game {
exits: vec![], exits: vec![],
endings: vec![], endings: vec![],
walls: None, 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
} }

View File

@ -24,6 +24,7 @@ pub struct Room {
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: Option<Vec<String>>, pub walls: Option<Vec<String>>,
pub tune: Option<String>,
} }
impl Room { impl Room {
@ -45,6 +46,13 @@ impl Room {
None => "".to_string(), 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 { impl From<String> for Room {
@ -59,11 +67,14 @@ impl From<String> for Room {
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 = None; 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 = Some(ids.iter().map(|&id| id.to_string()).collect()); walls = Some(ids.iter().map(|&id| id.to_string()).collect());
@ -154,6 +165,7 @@ impl From<String> for Room {
exits, exits,
endings, endings,
walls, walls,
tune,
} }
} }
} }
@ -213,7 +225,7 @@ impl Room {
} }
format!( format!(
"{} {}\n{}{}{}{}{}{}{}", "{} {}\n{}{}{}{}{}{}{}{}",
room_type.to_string(), room_type.to_string(),
self.id, self.id,
tiles, tiles,
@ -222,7 +234,8 @@ impl Room {
items, items,
exits, exits,
endings, endings,
self.palette_line() self.palette_line(),
self.tune_line(),
) )
} }

View File

@ -51,7 +51,7 @@ impl Sprite {
format!("\n{}", lines.join("\n")) format!("\n{}", lines.join("\n"))
} }
} }
pub fn from_str(str: &str) -> Result<Sprite, crate::Error> { pub fn from_str(str: &str) -> Result<Sprite, crate::Error> {
let mut lines: Vec<&str> = str.lines().collect(); let mut lines: Vec<&str> = str.lines().collect();
@ -100,9 +100,14 @@ impl Sprite {
items.reverse(); items.reverse();
let animation_frames = animation_frames_from_str( 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,

View File

@ -1,9 +1,9 @@
Write your game's title here Write your game's title here
# BITSY VERSION 8.4 # BITSY VERSION 8.12
! VER_MAJ 8 ! VER_MAJ 8
! VER_MIN 4 ! VER_MIN 12
! ROOM_FORMAT 1 ! ROOM_FORMAT 1
! DLG_COMPAT 0 ! DLG_COMPAT 0
! TXT_MODE 0 ! TXT_MODE 0

View File

@ -68,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_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_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");} #[test] fn test_010beb39() {str(include_str!("test-resources/omnibus/010BEB39.bitsy.txt"), "010BEB39");}
@ -520,4 +521,5 @@ mod test {
#[test] fn test_fe6547de() {str(include_str!("test-resources/omnibus/FE6547DE.bitsy.txt"), "FE6547DE");} #[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_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_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");}
} }

View File

@ -109,9 +109,14 @@ impl From<String> for Tile {
} }
} }
let animation_frames = animation_frames_from_str( 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,

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?)
}