mod memory_dir;
pub use memory_dir::*;
mod loader;
pub use loader::*;
pub mod front_matter;
use crate::{
run::RunState,
pico8::{self, Pico8Handle, Palettes},
};
use bevy::prelude::*;
#[cfg(feature = "scripting")]
use bevy_mod_scripting::core::{
event::Recipients,
asset::{ScriptAsset}, script::ScriptComponent};
use serde::{Deserialize, Serialize};
use merge2::Merge;
#[cfg(feature = "gameboy")]
pub mod gameboy;
pub const DEFAULT_CANVAS_SIZE: UVec2 = UVec2::splat(128);
pub const DEFAULT_SCREEN_SIZE: UVec2 = UVec2::splat(512);
pub(crate) fn plugin(app: &mut App) {
app
.add_systems(Update, update_asset)
.add_plugins(loader::plugin);
#[cfg(feature = "gameboy")]
app
.add_plugins(gameboy::plugin);
}
#[derive(Debug, Clone, Deserialize, Serialize, Default, Merge, PartialEq)]
pub struct Config {
pub name: Option<String>,
pub frames_per_second: Option<u8>,
pub description: Option<String>,
pub template: Option<String>,
pub author: Option<String>,
pub license: Option<String>,
pub screen: Option<Screen>,
pub defaults: Option<Defaults>,
#[serde(default, rename = "palette")]
pub palettes: Vec<Palette>,
#[serde(default, rename = "font")]
pub fonts: Vec<Font>,
#[serde(default, rename = "sprite_sheet")]
pub sprite_sheets: Vec<SpriteSheet>,
#[serde(default)]
#[cfg(feature = "scripting")]
pub scripts: Vec<String>,
#[serde(default, rename = "audio_bank")]
pub audio_banks: Vec<AudioBank>,
#[serde(default, rename = "map")]
pub maps: Vec<SpriteMap>,
#[serde(default, rename = "mesh")]
pub meshes: Vec<Mesh>,
}
#[derive(Default, Debug, Clone, Deserialize, Serialize, Merge, PartialEq)]
pub struct Defaults {
pub initial_pen_color: Option<usize>,
pub initial_transparent_color: Option<usize>,
pub clear_color: Option<usize>,
pub font_size: Option<f32>,
pub time_to_live: Option<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(untagged)]
pub enum AudioBank {
Paths { paths: Vec<String> },
Path { path: String },
}
impl AudioBank {
pub fn paths(&self) -> impl Iterator<Item = &str> {
let (a, b) = match self {
AudioBank::Paths { paths: v } => (Some(v), None),
AudioBank::Path { path: s } => (None, Some(s.as_str())),
};
a.into_iter().flatten().map(|x| x.as_str()).chain(b)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Merge)]
pub struct Screen {
#[merge(skip)]
pub canvas_size: UVec2,
pub screen_size: Option<UVec2>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default, Merge)]
pub struct SpriteSheet {
pub path: String,
pub sprite_size: Option<UVec2>,
pub sprite_counts: Option<UVec2>,
pub padding: Option<UVec2>,
pub offset: Option<UVec2>,
pub index_color: Option<bool>,
#[serde(default)]
pub extract_palette: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub enum ImagePalette {
#[serde(rename = "no-index")]
#[default]
NoIndex,
#[serde(rename = "index")]
Index,
#[serde(rename = "extract")]
Extract,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SpriteMap {
path: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum Font {
Default { default: bool },
Path {
path: String,
height: Option<f32>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Merge)]
pub struct Palette {
pub path: String,
pub row: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum Mesh {
Path { path: String },
Cuboid { cuboid: [f32; 3] },
}
pub fn update_asset(
mut reader: EventReader<AssetEvent<pico8::Pico8Asset>>,
assets: Res<Assets<pico8::Pico8Asset>>,
mut next_state: ResMut<NextState<RunState>>,
mut palettes: ResMut<Palettes>,
mut pico8_handle: Option<ResMut<Pico8Handle>>,
#[cfg(feature = "scripting")] mut commands: Commands,
#[cfg(feature = "scripting")] scripts: ResMut<Assets<ScriptAsset>>,
) {
for e in reader.read() {
if let AssetEvent::LoadedWithDependencies { id } = e {
if let Some(ref mut pico8_handle) = pico8_handle {
if let Some(pico8_asset) = assets.get(*id) {
if pico8_handle.handle.id() != *id {
warn!("Script loaded but does not match Pico8Handle.");
continue;
}
palettes.0 = pico8_asset.palettes.clone();
#[cfg(feature = "scripting")]
{
if !pico8_asset.scripts.is_empty() && pico8_handle.main_script.is_none() {
let entity = commands.spawn((Name::new("scripts"),
ScriptComponent(pico8_asset.scripts.clone()))).id();
info!("Add scripts to entity {}", &entity);
pico8_handle.main_script = Some(Recipients::AllContexts);
}
}
info!("Goto Loaded state");
next_state.set(RunState::Loaded);
} else {
debug!("Pico8Asset not available for loaded {:?}.", id);
}
} else {
warn!("Script loaded but no Pico8Handle is loaded.");
}
}
}
}
pub fn run_pico8_when_loaded(
state: Res<State<RunState>>,
mut next_state: ResMut<NextState<RunState>>,
) {
match **state {
RunState::Loaded => {
info!("Goto Init state.");
next_state.set(RunState::Init);
}
RunState::Init => {
info!("Goto Run state.");
next_state.set(RunState::Run);
}
_ => (),
}
}
pub fn pause_pico8_when_loaded(
state: Res<State<RunState>>,
mut next_state: ResMut<NextState<RunState>>,
) {
match **state {
RunState::Loaded => {
next_state.set(RunState::Init);
}
RunState::Init => {
next_state.set(RunState::Pause);
}
_ => (),
}
}
impl std::str::FromStr for Config {
type Err = ConfigError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(toml::from_str::<Config>(s)?)
}
}
impl Config {
pub fn pico8() -> Self {
Config {
frames_per_second: Some(30),
screen: Some(Screen {
canvas_size: UVec2::splat(128),
screen_size: Some(UVec2::splat(512)),
}),
palettes: vec![Palette {
path: pico8::PICO8_PALETTE.into(),
row: None,
}],
fonts: vec![Font::Path {
path: pico8::PICO8_FONT.into(),
height: None,
}],
defaults: Some(Defaults {
font_size: Some(5.0),
initial_pen_color: Some(6),
clear_color: Some(0),
initial_transparent_color: Some(0),
time_to_live: Some(1),
}),
..default()
}
}
#[cfg(feature = "gameboy")]
pub fn gameboy() -> Self {
Config {
frames_per_second: Some(60),
screen: Some(Screen {
canvas_size: UVec2::new(240, 160),
screen_size: Some(UVec2::new(480, 320)),
}),
palettes: vec![Palette {
path: gameboy::PALETTES.into(),
row: Some(15),
}],
fonts: vec![Font::Path {
path: gameboy::FONT.into(),
height: None,
}],
defaults: Some(Defaults {
font_size: Some(5.0),
initial_pen_color: Some(1),
clear_color: Some(3),
initial_transparent_color: None,
time_to_live: Some(1),
}),
..default()
}
}
pub fn inject_template(&mut self, template_name: Option<&str>) -> Result<(), ConfigError> {
if let Some(template_name) = template_name.or(self.template.as_deref()) {
let mut template = match template_name {
#[cfg(feature = "gameboy")]
"gameboy" => Config::gameboy(),
"pico8" => Config::pico8(),
x => {
return Err(ConfigError::InvalidTemplate(x.to_string()));
}
};
self.merge(&mut template)
}
Ok(())
}
pub fn with_default_font(mut self) -> Self {
if self.fonts.is_empty() {
self.fonts.push(Font::Default { default: true });
}
self
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_config_0() {
let config: Config = toml::from_str(
r#"
image = []
"#,
)
.unwrap();
assert_eq!(config.sprite_sheets.len(), 0);
assert!(config.screen.is_none());
}
#[test]
fn test_config_1() {
let config: Config = toml::from_str(
r#"
[[sprite_sheet]]
path = "sprites.png"
sprite_size = [8, 8]
"#,
)
.unwrap();
assert_eq!(config.sprite_sheets.len(), 1);
assert_eq!(config.sprite_sheets[0].path, "sprites.png");
assert_eq!(config.sprite_sheets[0].sprite_size, Some(UVec2::splat(8)));
}
#[test]
fn test_palete_0() {
let config: Config = toml::from_str(
r#"
[[palette]]
path = "sprites.png"
"#,
)
.unwrap();
assert_eq!(
config.palettes,
vec![Palette {
path: "sprites.png".into(),
row: None
}]
);
}
#[test]
fn test_config_2() {
let config: Config = toml::from_str(
r#"
[screen]
canvas_size = [128,128]
[[sprite_sheet]]
path = "sprites.png"
sprite_size = [8, 8]
"#,
)
.unwrap();
assert_eq!(
config.screen.map(|s| s.canvas_size),
Some(UVec2::splat(128))
);
assert_eq!(config.sprite_sheets.len(), 1);
assert_eq!(config.sprite_sheets[0].path, "sprites.png");
assert_eq!(config.sprite_sheets[0].sprite_size, Some(UVec2::splat(8)));
}
#[test]
fn test_config_3() {
let config: Config = toml::from_str(
r#"
[[audio_bank]]
paths = ["blah.p8"]
"#,
)
.unwrap();
assert_eq!(config.audio_banks.len(), 1);
assert_eq!(
config.audio_banks[0].paths().collect::<Vec<_>>(),
vec!["blah.p8"],
);
}
#[test]
fn test_config_4() {
let config: Config = toml::from_str(
r#"
[[audio_bank]]
paths = [
"blah.mp3"
]
"#,
)
.unwrap();
assert_eq!(config.audio_banks.len(), 1);
assert_eq!(
config.audio_banks[0].paths().collect::<Vec<_>>(),
vec!["blah.mp3"],
);
}
#[test]
fn test_config_5() {
let config: Config = toml::from_str(
r#"
[[font]]
path = "blah.tff"
[[font]]
path = "dee.tff"
height = 3.0
[[font]]
default = true
"#,
)
.unwrap();
assert_eq!(config.fonts.len(), 3);
}
#[test]
#[cfg(feature = "level")]
fn test_config_6() {
let config: Config = toml::from_str(
r#"
[[map]]
path = "blah.ldtk"
[[map]]
path = "blah.p8"
"#,
)
.unwrap();
assert_eq!(config.maps.len(), 2);
assert_eq!(config.maps[0].path, PathBuf::from("blah.ldtk"));
}
#[test]
fn test_config_7() {
let config: Config = toml::from_str(
r#"
frames_per_second = 70
blah = 7
"#,
)
.unwrap();
assert_eq!(config.frames_per_second, Some(70));
}
#[test]
fn test_config_8() {
let config: Config = toml::from_str(
r#"
[screen]
canvas_size = [128, 128]
screen_size = [512, 512]
"#,
)
.unwrap();
assert!(config.screen.is_some());
let screen = config.screen.unwrap();
assert_eq!(screen.canvas_size, UVec2::splat(128));
assert_eq!(screen.screen_size, Some(UVec2::splat(512)));
}
#[test]
fn test_config_9() {
let config: Config = toml::from_str(
r#"
[defaults]
font_size = 5
initial_pen_color = 6
initial_transparent_color = 7
clear_color = 8
"#,
)
.unwrap();
assert!(config.defaults.is_some());
let defaults = config.defaults.unwrap();
assert_eq!(defaults.font_size.unwrap(), 5.0);
assert_eq!(defaults.initial_pen_color.unwrap(), 6);
assert_eq!(defaults.initial_transparent_color.unwrap(), 7);
assert_eq!(defaults.clear_color.unwrap(), 8);
}
#[test]
fn test_inject0() {
let mut a = Config::default();
a.inject_template(Some("pico8")).unwrap();
let mut b = Config::default();
b.merge(&mut Config::pico8());
assert_eq!(a, b);
}
#[test]
fn test_inject1() {
let mut a = Config::default();
a.frames_per_second = Some(60);
a.inject_template(Some("pico8")).unwrap();
let mut b = Config::default();
b.frames_per_second = Some(60);
b.merge(&mut Config::pico8());
assert_eq!(a, b);
}
#[test]
fn test_image_palette0() {
let config: Config = toml::from_str(
r#"
[[sprite_sheet]]
path = "sprites.png"
index_color = true
"#,
)
.unwrap();
assert_eq!(config.sprite_sheets[0].index_color, Some(true));
}
#[test]
fn test_image_palette1() {
let config: Config = toml::from_str(
r#"
[[sprite_sheet]]
path = "sprites.png"
"#,
)
.unwrap();
assert_eq!(config.sprite_sheets[0].index_color, None);
}
#[test]
fn test_image_palette2() {
let config: Config = toml::from_str(
r#"
[[sprite_sheet]]
path = "sprites.png"
extract_palette = true
"#,
)
.unwrap();
assert_eq!(config.sprite_sheets[0].extract_palette, true);
}
#[test]
fn test_mesh0() {
let config: Config = toml::from_str(
r#"
[[mesh]]
path = "teapot.glb"
"#,
)
.unwrap();
assert_eq!(config.meshes[0], Mesh::Path { path: "teapot.glb".into() });
}
#[test]
fn test_mesh1() {
let config: Config = toml::from_str(
r#"
[[mesh]]
cuboid = [0.1, 0.2, 0.3]
"#,
)
.unwrap();
assert_eq!(config.meshes[0], Mesh::Cuboid { cuboid: [0.1, 0.2, 0.3] });
}
}