feat: impl From<sb2::Project> for VM::VirtualMachine

This commit is contained in:
Christopher Willis-Ford 2023-03-24 19:55:31 -07:00
parent 693d357813
commit 7609d99eab
5 changed files with 370 additions and 7 deletions

View file

@ -3,6 +3,7 @@ mod loading_screen;
mod project;
mod sprite;
mod stage;
mod virtual_machine;
use bevy::{
prelude::*,

View file

@ -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 {

View file

@ -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,
}

View 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
View 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,
}