Skip to content

Commit

Permalink
play sound blocks
Browse files Browse the repository at this point in the history
  • Loading branch information
dragazo committed Dec 21, 2023
1 parent 8288ffa commit c38e03c
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 33 deletions.
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "netsblox-vm"
version = "0.4.0"
version = "0.4.1"
edition = "2021"
license = "MIT OR Apache-2.0"
authors = ["Devin Jean <[email protected]>"]
Expand Down Expand Up @@ -72,7 +72,7 @@ rustls-tls-webpki-roots = [
# core deps
serde_json = { version = "1.0", default-features = false, features = ["alloc"] }
gc-arena = { version = "=0.4.0", default-features = false }
netsblox-ast = { version = "=0.5.0", default-features = false }
netsblox-ast = { version = "=0.5.1", default-features = false }
# netsblox-ast = { path = "../netsblox-ast", default-features = false }
num-traits = { version = "0.2.17", default-features = false }
num-derive = { version = "0.4.1", default-features = false }
Expand Down
65 changes: 38 additions & 27 deletions src/bytecode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,13 @@ pub(crate) enum Instruction<'a> {
/// If using dynamic costumes or no costume, does nothing.
NextCostume,

/// Pushes a shallow copy of the entity's list of static sounds onto the value stack.
PushSoundList,
/// Consumes 1 value, `sound`, from the value stack and attempts to play it.
/// This can be an audio object or the name of a static sound on the entity.
/// Empty string can be used as a no-op.
PlaySound { blocking: bool },

/// Pops one value, `target`, from the value stack and pushes a clone of entity `target` onto the value stack.
Clone,
/// Deletes the current active entity if it is a clone, and requests to abort all of its associated processes.
Expand Down Expand Up @@ -851,21 +858,25 @@ impl<'a> BinaryRead<'a> for Instruction<'a> {
122 => read_prefixed!(Instruction::SetCostume),
123 => read_prefixed!(Instruction::NextCostume),

124 => read_prefixed!(Instruction::Clone),
125 => read_prefixed!(Instruction::DeleteClone),
124 => read_prefixed!(Instruction::PushSoundList),
125 => read_prefixed!(Instruction::PlaySound { blocking: true }),
126 => read_prefixed!(Instruction::PlaySound { blocking: false }),

127 => read_prefixed!(Instruction::Clone),
128 => read_prefixed!(Instruction::DeleteClone),

126 => read_prefixed!(Instruction::ClearEffects),
127 => read_prefixed!(Instruction::ClearDrawings),
129 => read_prefixed!(Instruction::ClearEffects),
130 => read_prefixed!(Instruction::ClearDrawings),

128 => read_prefixed!(Instruction::GotoXY),
129 => read_prefixed!(Instruction::Goto),
131 => read_prefixed!(Instruction::GotoXY),
132 => read_prefixed!(Instruction::Goto),

130 => read_prefixed!(Instruction::PointTowardsXY),
131 => read_prefixed!(Instruction::PointTowards),
133 => read_prefixed!(Instruction::PointTowardsXY),
134 => read_prefixed!(Instruction::PointTowards),

132 => read_prefixed!(Instruction::Forward),
135 => read_prefixed!(Instruction::Forward),

133 => read_prefixed!(Instruction::UnknownBlock {} : name, args),
136 => read_prefixed!(Instruction::UnknownBlock {} : name, args),

_ => unreachable!(),
}
Expand Down Expand Up @@ -1055,21 +1066,25 @@ impl BinaryWrite for Instruction<'_> {
Instruction::SetCostume => append_prefixed!(122),
Instruction::NextCostume => append_prefixed!(123),

Instruction::Clone => append_prefixed!(124),
Instruction::DeleteClone => append_prefixed!(125),
Instruction::PushSoundList => append_prefixed!(124),
Instruction::PlaySound { blocking: true } => append_prefixed!(125),
Instruction::PlaySound { blocking: false } => append_prefixed!(126),

Instruction::Clone => append_prefixed!(127),
Instruction::DeleteClone => append_prefixed!(128),

Instruction::ClearEffects => append_prefixed!(126),
Instruction::ClearDrawings => append_prefixed!(127),
Instruction::ClearEffects => append_prefixed!(129),
Instruction::ClearDrawings => append_prefixed!(130),

Instruction::GotoXY => append_prefixed!(128),
Instruction::Goto => append_prefixed!(129),
Instruction::GotoXY => append_prefixed!(131),
Instruction::Goto => append_prefixed!(132),

Instruction::PointTowardsXY => append_prefixed!(130),
Instruction::PointTowards => append_prefixed!(131),
Instruction::PointTowardsXY => append_prefixed!(133),
Instruction::PointTowards => append_prefixed!(134),

Instruction::Forward => append_prefixed!(132),
Instruction::Forward => append_prefixed!(135),

Instruction::UnknownBlock { name, args } => append_prefixed!(133: move str name, args),
Instruction::UnknownBlock { name, args } => append_prefixed!(136: move str name, args),
}
}
}
Expand Down Expand Up @@ -1526,6 +1541,7 @@ impl<'a: 'b, 'b> ByteCodeBuilder<'a, 'b> {
ast::ExprKind::Costume => self.ins.push(Instruction::PushCostume.into()),
ast::ExprKind::CostumeNumber => self.ins.push(Instruction::PushCostumeNumber.into()),
ast::ExprKind::CostumeList => self.ins.push(Instruction::PushCostumeList.into()),
ast::ExprKind::SoundList => self.ins.push(Instruction::PushSoundList.into()),
ast::ExprKind::Size => self.ins.push(Instruction::PushProperty { prop: Property::Size }.into()),
ast::ExprKind::IsVisible => self.ins.push(Instruction::PushProperty { prop: Property::Visible }.into()),
ast::ExprKind::Entity { trans_name, .. } => self.ins.push(Instruction::PushEntity { name: trans_name }.into()),
Expand Down Expand Up @@ -1872,17 +1888,12 @@ impl<'a: 'b, 'b> ByteCodeBuilder<'a, 'b> {
ast::StmtKind::Stop { mode: ast::StopMode::All } => self.ins.push(Instruction::Abort { mode: AbortMode::All }.into()),
ast::StmtKind::Stop { mode: ast::StopMode::AllButThisScript } => self.ins.push(Instruction::Abort { mode: AbortMode::Others }.into()),
ast::StmtKind::Stop { mode: ast::StopMode::OtherScriptsInSprite } => self.ins.push(Instruction::Abort { mode: AbortMode::MyOthers }.into()),
ast::StmtKind::SetCostume { costume } => self.append_simple_ins(entity, &[costume], Instruction::SetCostume)?,
ast::StmtKind::PlaySound { sound, blocking } => self.append_simple_ins(entity, &[sound], Instruction::PlaySound { blocking: *blocking })?,
ast::StmtKind::Stop { mode: ast::StopMode::ThisBlock } => {
self.ins.push(Instruction::PushString { value: "" }.into());
self.ins.push(Instruction::Return.into());
}
ast::StmtKind::SetCostume { costume } => match costume {
Some(x) => self.append_simple_ins(entity, &[x], Instruction::SetCostume)?,
None => {
self.ins.push(Instruction::PushString { value: "" }.into());
self.ins.push(Instruction::SetCostume.into());
}
}
ast::StmtKind::SetVisible { value } => {
self.ins.push(Instruction::PushBool { value: *value }.into());
self.ins.push(Instruction::SetProperty { prop: Property::Visible }.into());
Expand Down
36 changes: 34 additions & 2 deletions src/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1234,8 +1234,8 @@ impl<'gc, C: CustomTypes<S>, S: System<C>> Process<'gc, C, S> {
Value::Image(x) => Some(x),
Value::String(x) => match x.as_str() {
"" => None,
x => match entity.costume_list.iter().find(|&c| c.0.as_str() == x) {
Some(c) => Some(c.1.clone()),
x => match entity.costume_list.get(x) {
Some(c) => Some(c.clone()),
None => return Err(ErrorCause::UndefinedCostume { name: x.into() }),
}
}
Expand Down Expand Up @@ -1279,6 +1279,38 @@ impl<'gc, C: CustomTypes<S>, S: System<C>> Process<'gc, C, S> {
None => self.pos = aft_pos,
}
}
Instruction::PushSoundList => {
let entity = self.call_stack.last().unwrap().entity.borrow();
self.value_stack.push(Value::List(Gc::new(mc, RefLock::new(entity.sound_list.iter().map(|x| Value::Audio(x.1.clone())).collect()))));
self.pos = aft_pos;
}
Instruction::PlaySound { blocking } => {
let entity_raw = self.call_stack.last().unwrap().entity.borrow();
let entity = &*entity_raw;

let sound = match self.value_stack.pop().unwrap() {
Value::Audio(x) => Some(x),
Value::String(x) => match x.as_str() {
"" => None,
x => match entity.sound_list.get(x) {
Some(x) => Some(x.clone()),
None => return Err(ErrorCause::UndefinedSound { name: x.into() }),
}
}
x => return Err(ErrorCause::ConversionError { got: x.get_type(), expected: Type::Audio }),
};

if let Some(sound) = sound {
drop(entity_raw);
drop(global_context_raw);
self.defer = Some(Defer::Command {
key: system.perform_command(mc, Command::PlaySound { sound, blocking }, self)?,
aft_pos,
});
} else {
self.pos = aft_pos;
}
}
Instruction::Clone => {
let target_cell = self.value_stack.pop().unwrap().as_entity()?;
let target = target_cell.borrow();
Expand Down
13 changes: 11 additions & 2 deletions src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ pub enum ErrorCause<C: CustomTypes<S>, S: System<C>> {
UndefinedVariable { name: CompactString },
/// A name-based costume lookup operation failed.
UndefinedCostume { name: CompactString },
/// A name-based sound lookup operation failed.
UndefinedSound { name: CompactString },
/// A name-based entity lookup operation failed.
UndefinedEntity { name: CompactString },
/// An upvar was created at the root scope, which is not allowed (it has nothing to refer up to).
Expand Down Expand Up @@ -1569,6 +1571,8 @@ pub enum Feature {

/// The ability of an entity to change the current costume.
SetCostume,
/// The ability of an entity to play a sound, optionally blocking until completion.
PlaySound { blocking: bool },

/// The ability to clear all graphic effects on an entity. This is equivalent to setting all the graphic effect properties to zero.
ClearEffects,
Expand Down Expand Up @@ -1639,6 +1643,10 @@ pub enum Command<'gc, 'a, C: CustomTypes<S>, S: System<C>> {
/// so this is just a hook for any custom update code that is needed for external purposes.
SetCostume,

/// Plays a sound, optionally with a request to block until the sound is finished playing.
/// It is up to the receiver to actually satisfy this blocking aspect, if desired.
PlaySound { sound: Rc<Audio>, blocking: bool },

/// Moves the entity to a specific location.
GotoXY { x: Number, y: Number },
/// Moves the current entity to the same position as the target entity.
Expand All @@ -1659,8 +1667,9 @@ impl<'gc, C: CustomTypes<S>, S: System<C>> Command<'gc, '_, C, S> {
Command::Print { .. } => Feature::Print,
Command::SetProperty { prop, .. } => Feature::SetProperty { prop: *prop },
Command::ChangeProperty { prop, .. } => Feature::ChangeProperty { prop: *prop },
Command::SetCostume { .. } => Feature::SetCostume,
Command::ClearEffects { .. } => Feature::ClearEffects,
Command::SetCostume => Feature::SetCostume,
Command::PlaySound { blocking, .. } => Feature::PlaySound { blocking: *blocking },
Command::ClearEffects => Feature::ClearEffects,
Command::ClearDrawings => Feature::ClearDrawings,
Command::GotoXY { .. } => Feature::GotoXY,
Command::GotoEntity { .. } => Feature::GotoEntity,
Expand Down
79 changes: 79 additions & 0 deletions src/test/project.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use alloc::collections::BTreeSet;
use alloc::rc::Rc;
use alloc::sync::Arc;
use core::cell::RefCell;

use crate::*;
use crate::gc::*;
Expand Down Expand Up @@ -482,6 +483,84 @@ fn test_proj_delete_clone() {
});
}

#[test]
fn test_proj_sounds() {
let sound_events = Rc::new(RefCell::new(vec![]));
let sound_events_clone = sound_events.clone();
let config = Config::<C, StdSystem<C>> {
request: None,
command: Some(Rc::new(move |_, key, command, _| match command {
Command::PlaySound { sound, blocking } => {
sound_events_clone.borrow_mut().push((sound, blocking));
key.complete(Ok(()));
CommandStatus::Handled
}
_ => CommandStatus::UseDefault { key, command },
})),
};
let system = Rc::new(StdSystem::new_sync(CompactString::new(BASE_URL), None, config, Arc::new(Clock::new(UtcOffset::UTC, None))));
let proj = get_running_project(include_str!("projects/sounds.xml"), system);

let expect_prefixes = [
[82, 73, 70, 70, 212, 28, 0, 0].as_slice(),
[73, 68, 51, 3, 0, 0, 0, 0].as_slice(),
[82, 73, 70, 70, 40, 2, 0, 0].as_slice(),
];

proj.mutate(|mc, proj| {
run_till_term(mc, &mut *proj.proj.borrow_mut(mc)).unwrap();
let global_context = proj.proj.borrow().get_global_context();
let global_context = global_context.borrow();

let res = global_context.globals.lookup("res").unwrap().get().clone();
match &res {
Value::List(x) => {
let x = &*x.borrow();
assert_eq!(x.len(), 3);
for (i, v) in x.iter().enumerate() {
match v {
Value::Audio(sound) => {
if !sound.content.starts_with(expect_prefixes[i]) {
panic!("{i}: {:?}", sound.content);
}
}
x => panic!("{x:?}"),
}
}
}
x => panic!("{x:?}"),
}
});

let sound_events = &*sound_events.borrow();
assert_eq!(sound_events.len(), 18);

assert!(sound_events[0].0.content.starts_with(expect_prefixes[1]) && !sound_events[0].1);
assert!(sound_events[1].0.content.starts_with(expect_prefixes[0]) && !sound_events[1].1);
assert!(sound_events[2].0.content.starts_with(expect_prefixes[2]) && !sound_events[2].1);

assert!(sound_events[3].0.content.starts_with(expect_prefixes[0]) && sound_events[3].1);
assert!(sound_events[4].0.content.starts_with(expect_prefixes[1]) && sound_events[4].1);
assert!(sound_events[5].0.content.starts_with(expect_prefixes[2]) && sound_events[5].1);

assert!(sound_events[6].0.content.starts_with(expect_prefixes[0]) && !sound_events[6].1);
assert!(sound_events[7].0.content.starts_with(expect_prefixes[0]) && sound_events[7].1);

assert!(sound_events[8].0.content.starts_with(expect_prefixes[1]) && !sound_events[8].1);
assert!(sound_events[9].0.content.starts_with(expect_prefixes[1]) && sound_events[9].1);

assert!(sound_events[10].0.content.starts_with(expect_prefixes[2]) && !sound_events[10].1);
assert!(sound_events[11].0.content.starts_with(expect_prefixes[2]) && sound_events[11].1);

assert!(sound_events[12].0.content.starts_with(expect_prefixes[1]) && !sound_events[12].1);
assert!(sound_events[13].0.content.starts_with(expect_prefixes[0]) && !sound_events[13].1);
assert!(sound_events[14].0.content.starts_with(expect_prefixes[2]) && !sound_events[14].1);

assert!(sound_events[15].0.content.starts_with(expect_prefixes[1]) && sound_events[15].1);
assert!(sound_events[16].0.content.starts_with(expect_prefixes[0]) && sound_events[16].1);
assert!(sound_events[17].0.content.starts_with(expect_prefixes[2]) && sound_events[17].1);
}

#[test]
fn test_proj_costumes() {
let config = Config::<C, StdSystem<C>> {
Expand Down
1 change: 1 addition & 0 deletions src/test/projects/sounds.xml

Large diffs are not rendered by default.

0 comments on commit c38e03c

Please sign in to comment.