Compare commits
9 Commits
4182079a73
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 4388c75bb8 | |||
| a36313341d | |||
| eee3444c4d | |||
| 5577a05191 | |||
| 9363be8254 | |||
| a7c020f785 | |||
| 8148e3f725 | |||
| b8c30fe873 | |||
| b50bde1f64 |
@@ -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"
|
||||||
|
|||||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2020 Max Bradbury
|
Copyright © 2024 Max Bradbury
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
9
TODO.md
Normal file
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
|
||||||
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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/error.rs
16
src/error.rs
@@ -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,7 +21,13 @@ impl fmt::Display for NotFound {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum ImageError {
|
||||||
|
MalformedPixel,
|
||||||
|
WrongSize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
Colour,
|
Colour,
|
||||||
Dialogue,
|
Dialogue,
|
||||||
@@ -31,10 +37,14 @@ pub enum Error {
|
|||||||
Game {
|
Game {
|
||||||
missing: NotFound,
|
missing: NotFound,
|
||||||
},
|
},
|
||||||
Image,
|
Image {
|
||||||
|
err: ImageError,
|
||||||
|
},
|
||||||
Item,
|
Item,
|
||||||
Palette,
|
Palette,
|
||||||
Position,
|
Position,
|
||||||
|
PulseWidth,
|
||||||
|
RelativeNote,
|
||||||
Room,
|
Room,
|
||||||
Sprite,
|
Sprite,
|
||||||
Text,
|
Text,
|
||||||
|
|||||||
175
src/game.rs
175
src/game.rs
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
53
src/image.rs
53
src/image.rs
@@ -1,4 +1,6 @@
|
|||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
use crate::Error;
|
||||||
|
use crate::error::ImageError;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct Image {
|
pub struct Image {
|
||||||
@@ -56,7 +58,7 @@ impl Image {
|
|||||||
let mut warnings = Vec::new();
|
let mut warnings = Vec::new();
|
||||||
|
|
||||||
if str.contains("NaN") {
|
if str.contains("NaN") {
|
||||||
warnings.push(crate::Error::Image);
|
warnings.push(crate::Error::Image { err: ImageError::MalformedPixel });
|
||||||
}
|
}
|
||||||
|
|
||||||
let string = str.trim().replace("NaN", "0");
|
let string = str.trim().replace("NaN", "0");
|
||||||
@@ -68,15 +70,24 @@ impl Image {
|
|||||||
for line in lines {
|
for line in lines {
|
||||||
let line = &line[..dimension];
|
let line = &line[..dimension];
|
||||||
for char in line.chars().into_iter() {
|
for char in line.chars().into_iter() {
|
||||||
// todo push warning on integers other than 0/1
|
pixels.push(match char {
|
||||||
pixels.push(match char {'1' => 1, _ => 0});
|
'0' => 0,
|
||||||
|
'1' => 1,
|
||||||
|
_ => {
|
||||||
|
warnings.push(
|
||||||
|
crate::Error::Image { err: ImageError::MalformedPixel }
|
||||||
|
);
|
||||||
|
0
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 8×8 (normal) or 16×16 (Bitsy HD)
|
||||||
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(Error::Image { err: ImageError::WrongSize })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,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)]
|
||||||
@@ -145,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();
|
||||||
|
|
||||||
|
|||||||
27
src/item.rs
27
src/item.rs
@@ -9,6 +9,7 @@ pub struct Item {
|
|||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
pub dialogue_id: Option<String>,
|
pub dialogue_id: Option<String>,
|
||||||
pub colour_id: Option<u64>,
|
pub colour_id: Option<u64>,
|
||||||
|
pub blip: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Item {
|
impl Item {
|
||||||
@@ -24,7 +25,11 @@ impl Item {
|
|||||||
optional_data_line("COL", self.colour_id.as_ref())
|
optional_data_line("COL", self.colour_id.as_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_str(str: &str) -> Result<Item, crate::Error> {
|
fn blip_line(&self) -> String {
|
||||||
|
optional_data_line("BLIP", self.blip.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_str(str: &str) -> Result<(Item, Vec<crate::Error>), crate::Error> {
|
||||||
let mut lines: Vec<&str> = str.lines().collect();
|
let mut lines: Vec<&str> = str.lines().collect();
|
||||||
|
|
||||||
if lines.is_empty() || !lines[0].starts_with("ITM ") {
|
if lines.is_empty() || !lines[0].starts_with("ITM ") {
|
||||||
@@ -35,11 +40,16 @@ impl Item {
|
|||||||
let mut name = None;
|
let mut name = None;
|
||||||
let mut dialogue_id = None;
|
let mut dialogue_id = None;
|
||||||
let mut colour_id: Option<u64> = None;
|
let mut colour_id: Option<u64> = None;
|
||||||
|
let mut blip = None;
|
||||||
|
|
||||||
|
let mut warnings = Vec::new();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let last_line = lines.pop().unwrap();
|
let last_line = lines.pop().unwrap();
|
||||||
|
|
||||||
if last_line.starts_with("NAME") {
|
if last_line.starts_with("BLIP") {
|
||||||
|
blip = Some(last_line.replace("BLIP ", "").to_string());
|
||||||
|
} else if last_line.starts_with("NAME") {
|
||||||
name = Some(last_line.replace("NAME ", "").to_string());
|
name = Some(last_line.replace("NAME ", "").to_string());
|
||||||
} else if last_line.starts_with("DLG") {
|
} else if last_line.starts_with("DLG") {
|
||||||
dialogue_id = Some(last_line.replace("DLG ", "").to_string());
|
dialogue_id = Some(last_line.replace("DLG ", "").to_string());
|
||||||
@@ -51,11 +61,13 @@ impl Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let animation_frames = animation_frames_from_str(
|
let (animation_frames, mut animation_warnings) = animation_frames_from_str(
|
||||||
&lines[1..].join("\n")
|
&lines[1..].join("\n")
|
||||||
);
|
).unwrap();
|
||||||
|
|
||||||
Ok(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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,12 +75,13 @@ impl fmt::Display for Item {
|
|||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"ITM {}\n{}{}{}{}",
|
"ITM {}\n{}{}{}{}{}",
|
||||||
self.id,
|
self.id,
|
||||||
self.animation_frames.to_string(),
|
self.animation_frames.to_string(),
|
||||||
self.name_line(),
|
self.name_line(),
|
||||||
self.dialogue_line(),
|
self.dialogue_line(),
|
||||||
self.colour_line(),
|
self.colour_line(),
|
||||||
|
self.blip_line(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,7 +92,7 @@ mod test {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn item_from_string() {
|
fn item_from_string() {
|
||||||
let output = Item::from_str(include_str!("test-resources/item")).unwrap();
|
let (output, _err) = Item::from_str(include_str!("test-resources/item")).unwrap();
|
||||||
let expected = mock::item();
|
let expected = mock::item();
|
||||||
assert_eq!(output, expected);
|
assert_eq!(output, expected);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
17
src/mock.rs
17
src/mock.rs
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/room.rs
19
src/room.rs
@@ -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(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
Write your game's title here
|
Write your game's title here
|
||||||
|
|
||||||
# BITSY VERSION 7.10
|
# BITSY VERSION 8.12
|
||||||
|
|
||||||
|
! VER_MAJ 8
|
||||||
|
! VER_MIN 12
|
||||||
! ROOM_FORMAT 1
|
! ROOM_FORMAT 1
|
||||||
|
! DLG_COMPAT 0
|
||||||
|
! TXT_MODE 0
|
||||||
|
|
||||||
PAL 0
|
PAL 0
|
||||||
NAME blueprint
|
|
||||||
0,82,204
|
0,82,204
|
||||||
128,159,255
|
128,159,255
|
||||||
255,255,255
|
255,255,255
|
||||||
|
NAME blueprint
|
||||||
|
|
||||||
ROOM 0
|
ROOM 0
|
||||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
|
||||||
@@ -29,6 +33,7 @@ ROOM 0
|
|||||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
|
||||||
NAME example room
|
NAME example room
|
||||||
PAL 0
|
PAL 0
|
||||||
|
TUNE 2
|
||||||
|
|
||||||
TIL a
|
TIL a
|
||||||
11111111
|
11111111
|
||||||
@@ -64,6 +69,7 @@ SPR a
|
|||||||
NAME cat
|
NAME cat
|
||||||
DLG 0
|
DLG 0
|
||||||
POS 0 8,12
|
POS 0 8,12
|
||||||
|
BLIP 1
|
||||||
|
|
||||||
ITM 0
|
ITM 0
|
||||||
00000000
|
00000000
|
||||||
@@ -88,6 +94,7 @@ ITM 1
|
|||||||
00011000
|
00011000
|
||||||
NAME key
|
NAME key
|
||||||
DLG 2
|
DLG 2
|
||||||
|
BLIP 2
|
||||||
|
|
||||||
DLG 0
|
DLG 0
|
||||||
I'm a cat
|
I'm a cat
|
||||||
@@ -104,3 +111,91 @@ NAME key dialog
|
|||||||
VAR a
|
VAR a
|
||||||
42
|
42
|
||||||
|
|
||||||
|
TUNE 1
|
||||||
|
3d,0,0,0,3d5,0,0,0,3l,0,0,0,3s,0,0,0
|
||||||
|
16d2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
|
||||||
|
>
|
||||||
|
4l,0,0,0,s,0,3l,0,0,0,2s,0,2m,0,2r,0
|
||||||
|
16m2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
|
||||||
|
>
|
||||||
|
3d,0,0,0,3d5,0,0,0,3l,0,0,0,3s,0,0,0
|
||||||
|
16l2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
|
||||||
|
>
|
||||||
|
3l,0,0,0,s,0,4m,0,0,0,4r,0,0,0,0,0
|
||||||
|
16s2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
|
||||||
|
NAME finale fanfare
|
||||||
|
KEY C,D,E,F,G,A,B d,r,m,s,l
|
||||||
|
TMP XFST
|
||||||
|
SQR P2 P8
|
||||||
|
ARP INT8
|
||||||
|
|
||||||
|
TUNE 2
|
||||||
|
0,0,2G,0,A,0,B,0,2C5,0,B,A,G,0,2G,0
|
||||||
|
G3,0,D,0,G3,0,D,0,2A3,0,E,0,C,0,E,0
|
||||||
|
>
|
||||||
|
2F#,0,G,0,A,0,F#,0,2E,0,F#,E,4D,0,0,0
|
||||||
|
2D,0,E,0,F#,0,D,0,2C,0,2G3,0,2F#3,0,D2,0
|
||||||
|
>
|
||||||
|
0,0,2G,0,A,0,B,0,2C5,0,B,A,G,0,G,0
|
||||||
|
2G2,0,D,D5,G3,G,D,0,2C2,0,E,E5,C3,C,E5,0
|
||||||
|
>
|
||||||
|
2D,0,C5,B,A,0,A,0,4A,0,0,0,F#,0,0,0
|
||||||
|
A2,0,E3,0,C3,0,E3,0,D3,0,A3,0,D,0,0,0
|
||||||
|
>
|
||||||
|
2E5,0,2G,0,2G5,0,2G,0,2F#5,0,2E5,0,2D5,0,2C5,0
|
||||||
|
2C3,0,2E,0,2E5,0,2C,0,2A3,0,2C,0,2F#,0,2E,0
|
||||||
|
>
|
||||||
|
3B,0,0,0,2E5,0,D5,0,4A,0,0,0,G,0,0,0
|
||||||
|
2G3,0,B3,0,2D,0,D3,0,2C3,0,G3,0,D#,0,0,0
|
||||||
|
>
|
||||||
|
0,0,2G,0,A,0,2B,0,C5,0,B,C5,A,0,G,0
|
||||||
|
A2,0,A3,0,C,0,2D,0,D#,0,D,E,C,0,C3,0
|
||||||
|
>
|
||||||
|
8B,0,0,0,0,0,0,0,A,0,2F#,0,E,0,D,0
|
||||||
|
D3,0,A3,0,F#,0,D,0,C,0,2D3,0,C3,0,F#3,0
|
||||||
|
NAME tuneful town
|
||||||
|
TMP FST
|
||||||
|
SQR P4 P2
|
||||||
|
|
||||||
|
TUNE 3
|
||||||
|
3F5,0,0,A#,0,2C#5,0,A#,3F5,0,0,F#5,0,0,2F5,0
|
||||||
|
A#3,C#,F,0,0,F,C#,F,A#3,C#,F,A#,0,A#,C#,F
|
||||||
|
>
|
||||||
|
3F5,0,0,A,0,2C#5,0,A,3F5,0,0,2A#5,0,A#,D#5,0
|
||||||
|
A3,C#,F,A3,0,F,C#,F,A,C#,F,0,D#,0,C#5,0
|
||||||
|
>
|
||||||
|
4F5,0,0,0,G#,2C#5,0,G#,3F5,0,0,D#5,0,F5,2C#5,0
|
||||||
|
G#3,C#,F,3F#,0,0,2F,0,G#3,C#,F,F#,B,A,F,D#
|
||||||
|
>
|
||||||
|
4D#5,0,0,0,0,0,2A#,0,4A#,0,0,0,0,0,A#,C5
|
||||||
|
G3,D#,F,G,0,D#,F,G,G3,D#,G,F,0,F,D#,C#3
|
||||||
|
>
|
||||||
|
4C#5,0,0,0,0,0,2C#5,0,3C#5,0,0,D#5,0,0,2C#5,0
|
||||||
|
F#2,C#,F#3,A#3,0,F#3,C#3,F#3,F#2,A,F#3,C#,0,F#3,A3,F#3
|
||||||
|
>
|
||||||
|
3C#5,0,0,F,3C5,0,0,C,3C5,0,0,D#,3A#,0,0,0
|
||||||
|
F2,D#3,A3,0,0,F3,C#,0,F#2,A#3,D#3,0,0,F#3,C#,C
|
||||||
|
>
|
||||||
|
3A#,0,0,0,C5,0,C#5,0,A#,0,C#,D#,G#,G#3,0,C#3
|
||||||
|
C3,A#3,C,E,A,0,A#,0,0,B2,B3,0,2F,0,0,0
|
||||||
|
>
|
||||||
|
A#,0,A#3,0,C#,0,F,0,A#,0,0,0,0,0,0,0
|
||||||
|
A#2,0,C#3,0,F3,0,A#3,0,D,0,0,0,0,F#3,0,F3
|
||||||
|
NAME rhythmic ruins
|
||||||
|
TMP MED
|
||||||
|
SQR P4 P4
|
||||||
|
|
||||||
|
BLIP 1
|
||||||
|
E5,B5,B5
|
||||||
|
NAME meow
|
||||||
|
ENV 40 99 4 185 138
|
||||||
|
BEAT 61 115
|
||||||
|
SQR P2
|
||||||
|
|
||||||
|
BLIP 2
|
||||||
|
D5,E5,D5
|
||||||
|
NAME pick up key
|
||||||
|
ENV 99 65 6 96 152
|
||||||
|
BEAT 95 0
|
||||||
|
SQR P4
|
||||||
|
|
||||||
|
|||||||
@@ -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");}
|
||||||
}
|
}
|
||||||
|
|||||||
11
src/tile.rs
11
src/tile.rs
@@ -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
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