mirror of
https://github.com/scratchfoundation/scratch-tech-explorations.git
synced 2025-08-08 11:58:44 -04:00
feat: impl From<sb2::Project> for VM::VirtualMachine
This commit is contained in:
parent
693d357813
commit
7609d99eab
5 changed files with 370 additions and 7 deletions
src
|
@ -3,6 +3,7 @@ mod loading_screen;
|
|||
mod project;
|
||||
mod sprite;
|
||||
mod stage;
|
||||
mod virtual_machine;
|
||||
|
||||
use bevy::{
|
||||
prelude::*,
|
||||
|
|
|
@ -7,6 +7,7 @@ use std::{fs, path::Path};
|
|||
|
||||
use crate::AppState;
|
||||
use crate::sb2;
|
||||
use crate::virtual_machine::VirtualMachine;
|
||||
|
||||
pub struct ScratchDemoProjectPlugin;
|
||||
|
||||
|
@ -22,7 +23,7 @@ impl Plugin for ScratchDemoProjectPlugin {
|
|||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
struct ProjectLoadTask(Task<sb2::load::ProjectLoadResult>);
|
||||
struct ProjectLoadTask(Task<ProjectLoadResult>);
|
||||
|
||||
fn project_load(mut commands: Commands) {
|
||||
info!("Starting project load");
|
||||
|
@ -37,7 +38,7 @@ fn project_check_load(mut app_state: ResMut<State<AppState>>, mut project_task:
|
|||
if let Some(project_task) = &mut project_task {
|
||||
if let Some(project_load_result) = future::block_on(future::poll_once(&mut project_task.0)) {
|
||||
match project_load_result {
|
||||
Ok(project_data) => info!("Project loaded data: {:#?}", project_data),
|
||||
Ok(vm) => info!("Project loaded data: {:#?}", vm),
|
||||
Err(project_error) => error!("Project load failure: {}", project_error),
|
||||
}
|
||||
|
||||
|
@ -46,8 +47,14 @@ fn project_check_load(mut app_state: ResMut<State<AppState>>, mut project_task:
|
|||
}
|
||||
}
|
||||
|
||||
async fn load_sb2(path: impl AsRef<Path>) -> sb2::load::ProjectLoadResult {
|
||||
let project_content = deserialize_sb2(path).await;
|
||||
pub type ProjectLoadResult = Result<VirtualMachine, sb2::load::ProjectLoadError>;
|
||||
|
||||
async fn load_sb2(path: impl AsRef<Path>) -> ProjectLoadResult {
|
||||
let project_content = deserialize_sb2(path).await?;
|
||||
|
||||
let vm: VirtualMachine = project_content.into();
|
||||
|
||||
info!("{:#?}", vm);
|
||||
|
||||
// validate project content
|
||||
// stop the VM
|
||||
|
@ -65,7 +72,7 @@ async fn load_sb2(path: impl AsRef<Path>) -> sb2::load::ProjectLoadResult {
|
|||
}
|
||||
}
|
||||
|
||||
project_content
|
||||
Ok(vm)
|
||||
}
|
||||
|
||||
async fn deserialize_sb2(path: impl AsRef<Path>) -> sb2::load::ProjectLoadResult {
|
||||
|
|
|
@ -135,7 +135,7 @@ pub struct Monitor {
|
|||
#[serde(rename_all="camelCase")]
|
||||
pub struct Variable {
|
||||
pub name: String,
|
||||
pub value: serde_json::Value,
|
||||
pub value: LiteralValue,
|
||||
pub is_persistent: bool,
|
||||
}
|
||||
|
||||
|
@ -145,7 +145,7 @@ pub struct Variable {
|
|||
pub struct List {
|
||||
#[serde(rename="listName")]
|
||||
pub name: String,
|
||||
pub contents: Vec<serde_json::Value>,
|
||||
pub contents: Vec<LiteralValue>,
|
||||
pub is_persistent: bool,
|
||||
}
|
||||
|
||||
|
|
238
src/virtual_machine/from_sb2.rs
Normal file
238
src/virtual_machine/from_sb2.rs
Normal file
|
@ -0,0 +1,238 @@
|
|||
use crate::sb2;
|
||||
|
||||
use crate::virtual_machine as VM;
|
||||
|
||||
use super::TopLevelItem;
|
||||
|
||||
impl From<sb2::Project> for VM::VirtualMachine {
|
||||
fn from(sb2: sb2::Project) -> Self {
|
||||
// 1 stage + other sprites
|
||||
let mut targets = Vec::with_capacity(1 + sb2.children.len());
|
||||
let mut foo: Vec<i32> = vec![]; // project-level container for something
|
||||
|
||||
targets.push(target_from_target(sb2.stage, &mut foo));
|
||||
for child in sb2.children {
|
||||
match child {
|
||||
sb2::StageChild::Sprite(sprite) => targets.push(target_from_sprite(sprite, &mut foo)),
|
||||
sb2::StageChild::Monitor(_) => (/* TODO: monitors not currently supported */),
|
||||
sb2::StageChild::List(_) => (/* ignore: lists are sometimes duplicated here due to a Scratch 2 bug */),
|
||||
}
|
||||
}
|
||||
|
||||
VM::VirtualMachine { targets }
|
||||
}
|
||||
}
|
||||
|
||||
fn target_from_sprite(sprite: sb2::Sprite, foo: &mut Vec<i32>) -> VM::Target {
|
||||
VM::Target {
|
||||
name: sprite.target.name,
|
||||
|
||||
x: sprite.x,
|
||||
y: sprite.y,
|
||||
scale: sprite.scale,
|
||||
direction: sprite.direction,
|
||||
rotation_style: sprite.rotation_style.into(),
|
||||
is_draggable: sprite.is_draggable,
|
||||
is_visible: sprite.is_visible,
|
||||
|
||||
scripts: sprite.target.scripts.into_iter().map(|script| script.into()).collect(),
|
||||
variables: sprite.target.variables.into_iter().map(|variable| variable.into()).collect(),
|
||||
lists: sprite.target.lists.into_iter().map(|list| list.into()).collect(),
|
||||
sounds: sprite.target.sounds.into_iter().map(|sound| sound.into()).collect(),
|
||||
costumes: sprite.target.costumes.into_iter().map(|costume| costume.into()).collect(),
|
||||
current_costume: sprite.target.current_costume_index,
|
||||
}
|
||||
}
|
||||
|
||||
fn target_from_target(stage: sb2::Stage, foo: &mut Vec<i32>) -> VM::Target {
|
||||
VM::Target {
|
||||
name: stage.target.name,
|
||||
|
||||
x: 0.,
|
||||
y: 0.,
|
||||
scale: 100.,
|
||||
direction: 90.,
|
||||
rotation_style: VM::RotationStyle::Normal,
|
||||
is_draggable: false,
|
||||
is_visible: true,
|
||||
|
||||
scripts: stage.target.scripts.into_iter().map(|block| block.into()).collect(),
|
||||
variables: stage.target.variables.into_iter().map(|variable| variable.into()).collect(),
|
||||
lists: stage.target.lists.into_iter().map(|list| list.into()).collect(),
|
||||
sounds: stage.target.sounds.into_iter().map(|sound| sound.into()).collect(),
|
||||
costumes: stage.target.costumes.into_iter().map(|costume| costume.into()).collect(),
|
||||
current_costume: stage.target.current_costume_index,
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sb2::RotationStyle> for VM::RotationStyle {
|
||||
fn from(value: sb2::RotationStyle) -> Self {
|
||||
match value {
|
||||
sb2::RotationStyle::Normal => Self::Normal,
|
||||
sb2::RotationStyle::LeftRight => Self::LeftRight,
|
||||
sb2::RotationStyle::None => Self::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sb2::TopLevelScript> for VM::TopLevelItem {
|
||||
fn from(mut value: sb2::TopLevelScript) -> Self {
|
||||
if value.script.is_empty() {
|
||||
// this isn't really valid...
|
||||
return TopLevelItem {
|
||||
x: value.x,
|
||||
y: value.y,
|
||||
stack: VM::BlockStack::Script(vec![]),
|
||||
};
|
||||
}
|
||||
let first_block = value.script.remove(0);
|
||||
match first_block {
|
||||
sb2::Block::DefineProcedure(definition) =>
|
||||
TopLevelItem {
|
||||
x: value.x,
|
||||
y: value.y,
|
||||
stack: VM::BlockStack::Definition(VM::ProcedureDefinition {
|
||||
spec: definition.spec.clone(),
|
||||
body: value.script
|
||||
.into_iter()
|
||||
.map(|block| block.into())
|
||||
.collect(),
|
||||
parameter_names: definition.parameter_names,
|
||||
default_arguments: definition.default_arg_values
|
||||
.into_iter()
|
||||
.map(|value| value.into())
|
||||
.collect(),
|
||||
run_without_screen_refresh: definition.run_without_screen_refresh
|
||||
}),
|
||||
},
|
||||
_ => TopLevelItem {
|
||||
x: value.x,
|
||||
y: value.y,
|
||||
stack: VM::BlockStack::Script(
|
||||
Some(first_block).into_iter()
|
||||
.chain(
|
||||
value.script.into_iter()
|
||||
)
|
||||
.map(|block| block.into())
|
||||
.collect()
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sb2::LiteralValue> for VM::Value {
|
||||
fn from(value: sb2::LiteralValue) -> Self {
|
||||
match value {
|
||||
sb2::LiteralValue::Boolean(b) => Self::Boolean(b),
|
||||
sb2::LiteralValue::Number(n) => Self::Number(n),
|
||||
sb2::LiteralValue::String(s) => Self::String(s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sb2::Block> for VM::Block {
|
||||
fn from(value: sb2::Block) -> Self {
|
||||
match value {
|
||||
sb2::Block::DefineProcedure(_) => panic!("unexpected procedure definition"),
|
||||
sb2::Block::Basic(b) => b.into(),
|
||||
sb2::Block::C(b) => b.into(),
|
||||
sb2::Block::E(b) => b.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sb2::BasicBlock> for VM::Block {
|
||||
fn from(value: sb2::BasicBlock) -> Self {
|
||||
VM::Block {
|
||||
opcode: value.opcode,
|
||||
arguments: value.args.into_iter().map(|arg| arg.into()).collect(),
|
||||
branches: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sb2::CBlock> for VM::Block {
|
||||
fn from(value: sb2::CBlock) -> Self {
|
||||
VM::Block {
|
||||
opcode: value.opcode,
|
||||
arguments: value.args.into_iter().map(|arg| arg.into()).collect(),
|
||||
branches: vec![
|
||||
value.branch.into_iter().map(|block| block.into()).collect(),
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sb2::EBlock> for VM::Block {
|
||||
fn from(value: sb2::EBlock) -> Self {
|
||||
VM::Block {
|
||||
opcode: value.opcode,
|
||||
arguments: value.args.into_iter().map(|arg| arg.into()).collect(),
|
||||
branches: vec![
|
||||
value.branch0.into_iter().map(|block| block.into()).collect(),
|
||||
value.branch1.into_iter().map(|block| block.into()).collect(),
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<sb2::BlockArgument> for VM::Argument {
|
||||
fn from(value: sb2::BlockArgument) -> Self {
|
||||
match value {
|
||||
sb2::BlockArgument::Boolean(b) => Self::Literal(VM::Value::Boolean(b)),
|
||||
sb2::BlockArgument::Number(n) => Self::Literal(VM::Value::Number(n)),
|
||||
sb2::BlockArgument::String(s) => Self::Literal(VM::Value::String(s)),
|
||||
sb2::BlockArgument::Reporter(r) => Self::Expression(r.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sb2::Variable> for (String, VM::Variable) {
|
||||
fn from(value: sb2::Variable) -> Self {
|
||||
(
|
||||
value.name,
|
||||
VM::Variable {
|
||||
value: value.value.into(),
|
||||
is_cloud: value.is_persistent,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sb2::List> for (String, VM::List) {
|
||||
fn from(value: sb2::List) -> Self {
|
||||
(
|
||||
value.name,
|
||||
VM::List {
|
||||
values: value.contents.into_iter().map(|x| x.into()).collect(),
|
||||
is_cloud: value.is_persistent,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sb2::Sound> for VM::Sound {
|
||||
fn from(value: sb2::Sound) -> Self {
|
||||
VM::Sound {
|
||||
name: value.sound_name,
|
||||
md5: value.md5,
|
||||
format: value.format,
|
||||
sample_rate: value.rate,
|
||||
sample_count: value.sample_count,
|
||||
sound_index: value.sound_id, // TODO: connect references
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sb2::Costume> for VM::Costume {
|
||||
fn from(value: sb2::Costume) -> Self {
|
||||
VM::Costume {
|
||||
name: value.costume_name,
|
||||
md5: value.base_layer_md5,
|
||||
bitmap_resolution: value.bitmap_resolution,
|
||||
rotation_center_x: value.rotation_center_x,
|
||||
rotation_center_y: value.rotation_center_y,
|
||||
layer_index: value.base_layer_id, // TODO: connect references
|
||||
}
|
||||
}
|
||||
}
|
117
src/virtual_machine/mod.rs
Normal file
117
src/virtual_machine/mod.rs
Normal file
|
@ -0,0 +1,117 @@
|
|||
use bevy::utils::HashMap;
|
||||
|
||||
mod from_sb2;
|
||||
|
||||
// This represents the virtual machine state for a Scratch project.
|
||||
// Ideally, loading a new Scratch project should mean throwing this away and replacing it with a new instance.
|
||||
#[derive(Debug)]
|
||||
pub struct VirtualMachine {
|
||||
pub targets: Vec<Target>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Target {
|
||||
pub name: String,
|
||||
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
pub scale: f64,
|
||||
pub direction: f64,
|
||||
pub rotation_style: RotationStyle,
|
||||
pub is_draggable: bool,
|
||||
pub is_visible: bool,
|
||||
|
||||
pub scripts: Vec<TopLevelItem>,
|
||||
pub variables: HashMap<String, Variable>,
|
||||
pub lists: HashMap<String, List>,
|
||||
pub sounds: Vec<Sound>,
|
||||
pub costumes: Vec<Costume>,
|
||||
|
||||
pub current_costume: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub enum RotationStyle {
|
||||
#[default]
|
||||
Normal,
|
||||
LeftRight,
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TopLevelItem {
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
pub stack: BlockStack,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum BlockStack {
|
||||
Script(Script),
|
||||
Definition(ProcedureDefinition),
|
||||
}
|
||||
|
||||
pub type Script = Vec<Block>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Block {
|
||||
pub opcode: String,
|
||||
pub arguments: Vec<Argument>,
|
||||
pub branches: Vec<Script>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Argument {
|
||||
Expression(Block),
|
||||
Literal(Value),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Value {
|
||||
Boolean(bool),
|
||||
Number(f64),
|
||||
String(String),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ProcedureDefinition {
|
||||
pub spec: String,
|
||||
pub body: Script,
|
||||
pub parameter_names: Vec<String>,
|
||||
pub default_arguments: Vec<Value>,
|
||||
pub run_without_screen_refresh: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Variable {
|
||||
pub value: Value,
|
||||
pub is_cloud: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct List {
|
||||
pub values: Vec<Value>,
|
||||
pub is_cloud: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Sound {
|
||||
pub name: String,
|
||||
pub md5: String,
|
||||
pub format: String,
|
||||
pub sample_rate: i32,
|
||||
pub sample_count: i32,
|
||||
|
||||
pub sound_index: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Costume {
|
||||
pub name: String,
|
||||
pub md5: String,
|
||||
pub bitmap_resolution: i32,
|
||||
pub rotation_center_x: f64,
|
||||
pub rotation_center_y: f64,
|
||||
|
||||
pub layer_index: i32,
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue