feat: WIP deserialize SB2 project JSON

* switch from SB3 format to simpler SB2 format
* move SB2 code into its own module
* implement deserializing more fields, including stage children
This commit is contained in:
Christopher Willis-Ford 2023-03-03 16:45:29 -08:00
parent c554586172
commit a024b1c932
9 changed files with 238 additions and 283 deletions

Binary file not shown.

Binary file not shown.

View file

@ -1,7 +1,6 @@
mod sb2;
mod loading_screen;
mod project;
mod project_bundle;
mod sb3;
mod sprite;
mod stage;

View file

@ -4,11 +4,9 @@ use bevy::{
};
use futures_lite::future;
use std::{fs, path::Path};
use zip::result::ZipError;
use crate::AppState;
use crate::project_bundle::ProjectBundle;
use crate::sb3::SB3;
use crate::sb2;
pub struct ScratchDemoProjectPlugin;
@ -23,51 +21,14 @@ impl Plugin for ScratchDemoProjectPlugin {
}
}
type ProjectLoadResult = Result<ProjectBundle, ProjectLoadError>;
#[derive(Debug)]
pub enum ProjectLoadError {
IoError(std::io::Error),
ParseError(serde_json::Error),
ZipError(ZipError),
}
impl From<std::io::Error> for ProjectLoadError {
fn from(err: std::io::Error) -> Self {
Self::IoError(err)
}
}
impl From<serde_json::Error> for ProjectLoadError {
fn from(err: serde_json::Error) -> Self {
Self::ParseError(err)
}
}
impl From<ZipError> for ProjectLoadError {
fn from(err: ZipError) -> Self {
Self::ZipError(err)
}
}
impl std::fmt::Display for ProjectLoadError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::IoError(err) => write!(f, "{}", err),
Self::ParseError(err) => write!(f, "{}", err),
Self::ZipError(err) => write!(f, "{}", err),
}
}
}
#[derive(Resource)]
struct ProjectLoadTask(Task<ProjectLoadResult>);
struct ProjectLoadTask(Task<sb2::load::ProjectLoadResult>);
fn project_load(mut commands: Commands) {
info!("Starting project load");
let thread_pool = AsyncComputeTaskPool::get();
let load_task = thread_pool.spawn(async move {
load_sb3("assets/Infinite ToeBeans.sb3").await
load_sb2("assets/Infinite ToeBeans.sb2").await
});
commands.insert_resource(ProjectLoadTask(load_task));
}
@ -85,8 +46,8 @@ fn project_check_load(mut app_state: ResMut<State<AppState>>, mut project_task:
}
}
async fn load_sb3(path: impl AsRef<Path>) -> ProjectLoadResult {
let project_content = deserialize_sb3(path).await;
async fn load_sb2(path: impl AsRef<Path>) -> sb2::load::ProjectLoadResult {
let project_content = deserialize_sb2(path).await;
// validate project content
// stop the VM
@ -100,19 +61,20 @@ async fn load_sb3(path: impl AsRef<Path>) -> ProjectLoadResult {
while start_time.elapsed() < std::time::Duration::from_secs_f32(4.2)
{
// spin to pretend we're loading lots of stuff
break; // or don't
}
}
project_content
}
async fn deserialize_sb3(path: impl AsRef<Path>) -> ProjectLoadResult {
async fn deserialize_sb2(path: impl AsRef<Path>) -> sb2::load::ProjectLoadResult {
// TODO: would it make sense to use async_zip instead? ...but will tokio conflict with bevy?
let file = fs::File::open(&path)?;
let project_bundle = SB3::from_reader(file)?;
let project_bundle = sb2::Project::from_reader(file)?;
Ok(project_bundle) // return hydrated project
}

View file

@ -1,201 +0,0 @@
use std::{collections::HashMap, fmt::Debug};
use serde::{Deserialize,Serialize};
#[derive(Serialize, Deserialize, Debug, Eq, Hash, PartialEq)]
#[repr(transparent)]
pub struct BlockId(String);
#[derive(Serialize, Deserialize, Debug, Eq, Hash, PartialEq)]
#[repr(transparent)]
pub struct ListId(String);
#[derive(Serialize, Deserialize, Debug, Eq, Hash, PartialEq)]
#[repr(transparent)]
pub struct VariableId(String);
#[derive(Serialize, Deserialize, Debug)]
pub struct ProjectBundle {
pub targets: Vec<Target>,
pub monitors: Vec<Monitor>,
pub extensions: Vec<String>,
pub meta: HashMap<String, String>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all="camelCase")]
pub struct Target {
pub is_stage: bool,
pub name: String,
pub variables: HashMap<VariableId, VariableNameAndValue>,
pub lists: HashMap<ListId, ListNameAndValues>,
pub broadcasts: serde_json::Value, // TODO
pub blocks: HashMap<BlockId, Block>,
pub comments: serde_json::Value, // TODO
pub current_costume: i32,
pub costumes: serde_json::Value, // TODO
pub sounds: serde_json::Value, // TODO
pub volume: f64,
pub layer_order: i32,
#[serde(default)]
pub tempo: f64,
#[serde(default)]
pub video_transparency: f64,
#[serde(default)]
pub video_state: serde_json::Value, // TODO
#[serde(default)]
pub text_to_speech_language: serde_json::Value, // TODO
#[serde(default)]
pub visible: bool,
#[serde(default)]
pub x: f64,
#[serde(default)]
pub y: f64,
#[serde(default)]
pub size: f64,
#[serde(default)]
pub direction: f64,
#[serde(default)]
pub draggable: bool,
#[serde(default)]
pub rotation_style: RotationStyle,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct VariableNameAndValue {
pub name: String,
pub value: serde_json::Value,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct ListNameAndValues {
pub name: String,
pub values: Vec<serde_json::Value>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Block {
pub opcode: String,
pub next: Option<BlockId>,
pub parent: Option<BlockId>,
pub inputs: HashMap<String, serde_json::Value>, // TODO
pub fields: serde_json::Value, // TODO
pub shadow: bool,
#[serde(default)]
pub top_level: bool,
}
/*
#[derive(Serialize, Deserialize, Debug)]
#[serde(untagged)]
pub struct Input {
// see deserializeInputs in sb3.js
}
*/
#[derive(Serialize, Deserialize, Debug, Default)]
pub enum RotationStyle {
#[default]
#[serde(rename = "all around")]
AllAround,
#[serde(rename = "left-right")]
LeftRight,
#[serde(rename = "don't rotate")]
DoNotRotate,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all="camelCase")]
pub struct Monitor {
pub id: String,
pub mode: MonitorMode,
pub opcode: String,
pub params: serde_json::Value, // TODO
pub sprite_name: Option<String>,
pub value: serde_json::Value,
pub width: f64,
pub height: f64,
pub x: f64,
pub y: f64,
pub visible: bool,
#[serde(default)]
pub slider_min: f64,
#[serde(default)]
pub slider_max: f64,
#[serde(default)]
pub is_discrete: bool,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all="camelCase")]
pub enum MonitorMode {
Default,
List,
}
impl Default for Target {
fn default() -> Self {
Self {
is_stage: false,
name: Default::default(),
variables: Default::default(),
lists: Default::default(),
broadcasts: serde_json::Value::Null,
blocks: Default::default(),
comments: serde_json::Value::Null,
current_costume: 0,
costumes: serde_json::Value::Null,
sounds: serde_json::Value::Null,
volume: 100.0,
layer_order: 0,
tempo: 60.0,
video_transparency: 50.0,
video_state: serde_json::Value::from("on"),
text_to_speech_language: serde_json::Value::Null,
visible: true,
x: 0.0,
y: 0.0,
size: 100.0,
direction: 0.0,
draggable: true,
rotation_style: RotationStyle::AllAround,
}
}
}
impl Default for Monitor {
fn default() -> Self {
Self {
id: String::new(),
mode: MonitorMode::Default,
opcode: "data_variable".to_string(),
params: serde_json::Value::Null,
sprite_name: None,
value: serde_json::Value::from(0),
width: 0.0,
height: 0.0,
x: 0.0,
y: 0.0,
visible: true,
slider_min: 0.0,
slider_max: 100.0,
is_discrete: true,
}
}
}

68
src/sb2/load.rs Normal file
View file

@ -0,0 +1,68 @@
use std::io;
use crate::sb2::*;
use zip::{ZipArchive, result::ZipError};
pub type ProjectLoadResult = Result<Project, ProjectLoadError>;
#[derive(Debug)]
pub enum ProjectLoadError {
IoError(std::io::Error),
ParseError(serde_json::Error),
ZipError(ZipError),
}
impl From<std::io::Error> for ProjectLoadError {
fn from(err: std::io::Error) -> Self {
Self::IoError(err)
}
}
impl From<serde_json::Error> for ProjectLoadError {
fn from(err: serde_json::Error) -> Self {
Self::ParseError(err)
}
}
impl From<ZipError> for ProjectLoadError {
fn from(err: ZipError) -> Self {
Self::ZipError(err)
}
}
impl std::fmt::Display for ProjectLoadError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::IoError(err) => write!(f, "{}", err),
Self::ParseError(err) => write!(f, "{}", err),
Self::ZipError(err) => write!(f, "{}", err),
}
}
}
impl Project {
pub fn from_reader<R>(sb2_reader: R) -> Result<Project, ProjectLoadError>
where
R: io::Read + std::io::Seek,
{
// this will open the ZIP and read the central directory
let mut sb2_zip = ZipArchive::new(sb2_reader)?;
let project_json_reader = sb2_zip.by_name("project.json")?;
// let project_json: serde_json::Value = serde_json::from_reader(project_json_reader)?;
// info!("Project loaded data: {:#?}", project_json);
// Ok(ProjectBundle {
// title: "hi".to_string(),
// extensions: vec![],
// meta: serde_json::Value::Null,
// monitors: vec![],
// targets: vec![],
// })
let project_bundle: Project = serde_json::from_reader(project_json_reader)?;
Ok(project_bundle)
}
}

160
src/sb2/mod.rs Normal file
View file

@ -0,0 +1,160 @@
pub mod load;
use std::{collections::HashMap, fmt::Debug};
use serde::{Deserialize, Serialize};
#[derive(Debug, Eq, Hash, PartialEq)]
#[derive(Deserialize, Serialize)]
#[repr(transparent)]
pub struct BlockId(String);
#[derive(Debug, Eq, Hash, PartialEq)]
#[derive(Deserialize, Serialize)]
#[repr(transparent)]
pub struct ListId(String);
#[derive(Debug, Eq, Hash, PartialEq)]
#[derive(Deserialize, Serialize)]
#[repr(transparent)]
pub struct VariableId(String);
#[derive(Debug)]
#[derive(Deserialize, Serialize)]
pub struct Project {
#[serde(flatten)]
pub stage: Stage,
pub children: Vec<StageChild>,
pub info: HashMap<String, serde_json::Value>,
}
#[derive(Debug)]
#[derive(Deserialize, Serialize)]
#[serde(rename_all="camelCase")]
pub struct Stage {
#[serde(default, rename="penLayerMD5")]
pub pen_layer_md5: String,
#[serde(default, rename="penLayerID")]
pub pen_layer_id: i32,
#[serde(default, rename="tempoBPM")]
pub tempo_bpm: f64,
#[serde(default)]
pub video_alpha: f64,
#[serde(flatten)]
pub target: Target,
}
#[derive(Debug)]
#[derive(Deserialize, Serialize)]
#[serde(untagged)]
pub enum StageChild {
Sprite(Sprite),
Monitor(Monitor),
List(List),
}
#[derive(Debug)]
#[derive(Deserialize, Serialize)]
#[serde(rename_all="camelCase")]
pub struct Sprite {
#[serde(rename="scratchX")]
pub x: f64,
#[serde(rename="scratchY")]
pub y: f64,
pub scale: f64,
pub direction: f64,
pub rotation_style: RotationStyle,
pub is_draggable: bool,
pub index_in_library: i32,
#[serde(rename="visible")]
pub is_visible: bool,
pub sprite_info: serde_json::Value,
#[serde(flatten)]
pub target: Target,
}
#[derive(Debug)]
#[derive(Deserialize, Serialize)]
#[serde(rename_all="camelCase")]
pub struct Target {
#[serde(rename="objName")]
pub name: String,
#[serde(default)]
pub scripts: Vec<serde_json::Value>, // TODO
#[serde(default)]
pub variables: Vec<Variable>, // TODO: HashMap
#[serde(default)]
pub lists: Vec<List>, // TODO: HashMap
#[serde(default)]
pub sounds: Vec<serde_json::Value>, // TODO
#[serde(default)]
pub costumes: Vec<serde_json::Value>, // TODO
pub current_costume_index: i32,
}
#[derive(Debug)]
#[derive(Deserialize, Serialize)]
#[serde(rename_all="camelCase")]
pub struct Monitor {
pub target: String,
pub cmd: String,
pub param: String,
pub color: i32,
pub label: String,
pub mode: i32,
pub slider_min: f64,
pub slider_max: f64,
pub is_discrete: bool,
pub x: f64,
pub y: f64,
pub visible: bool,
}
#[derive(Debug)]
#[derive(Deserialize, Serialize)]
#[serde(rename_all="camelCase")]
pub struct Variable {
pub name: String,
pub value: serde_json::Value,
pub is_persistent: bool,
}
#[derive(Debug)]
#[derive(Deserialize, Serialize)]
#[serde(rename_all="camelCase")]
pub struct List {
#[serde(rename="listName")]
pub name: String,
pub contents: Vec<serde_json::Value>,
pub is_persistent: bool,
}
#[derive(Debug, Default)]
#[derive(Deserialize, Serialize)]
#[serde(rename_all="camelCase")]
pub enum RotationStyle {
#[default]
Normal,
LeftRight,
None,
}

View file

@ -1,34 +0,0 @@
use std::io;
use bevy::prelude::*;
use zip::ZipArchive;
use crate::{project::ProjectLoadError, project_bundle::ProjectBundle};
pub struct SB3;
impl SB3 {
pub fn from_reader<R>(sb3_reader: R) -> Result<ProjectBundle, ProjectLoadError>
where
R: io::Read + std::io::Seek,
{
// this will open the ZIP and read the central directory
let mut sb3_zip = ZipArchive::new(sb3_reader)?;
let project_json_reader = sb3_zip.by_name("project.json")?;
// let project_json: serde_json::Value = serde_json::from_reader(project_json_reader)?;
// info!("Project loaded data: {:#?}", project_json);
// Ok(ProjectBundle {
// title: "hi".to_string(),
// extensions: vec![],
// meta: serde_json::Value::Null,
// monitors: vec![],
// targets: vec![],
// })
let project_bundle: ProjectBundle = serde_json::from_reader(project_json_reader)?;
Ok(project_bundle)
}
}

View file

@ -7,6 +7,7 @@ struct Name(String);
struct Costume(String);
#[derive(Debug)]
#[allow(dead_code)] // until the new thread step function
pub enum ScratchCode {
MoveOneStep,
MoveTwoSteps,