mirror of
https://github.com/scratchfoundation/scratch-tech-explorations.git
synced 2025-08-14 06:48:43 -04:00
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:
parent
c554586172
commit
a024b1c932
9 changed files with 238 additions and 283 deletions
BIN
assets/Infinite ToeBeans.sb2
Normal file
BIN
assets/Infinite ToeBeans.sb2
Normal file
Binary file not shown.
Binary file not shown.
|
@ -1,7 +1,6 @@
|
|||
mod sb2;
|
||||
mod loading_screen;
|
||||
mod project;
|
||||
mod project_bundle;
|
||||
mod sb3;
|
||||
mod sprite;
|
||||
mod stage;
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
68
src/sb2/load.rs
Normal 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
160
src/sb2/mod.rs
Normal 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,
|
||||
}
|
34
src/sb3.rs
34
src/sb3.rs
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue