diff --git a/Cargo.toml b/Cargo.toml index bc4a3bb..d5f60a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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.5", default-features = false } +netsblox-ast = { version = "=0.5.6", 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 } diff --git a/src/bytecode.rs b/src/bytecode.rs index 3a26970..8d79028 100644 --- a/src/bytecode.rs +++ b/src/bytecode.rs @@ -413,6 +413,9 @@ pub(crate) enum Instruction<'a> { PushCostumeNumber, /// Pushes a shallow copy of the entity's list of static costumes onto the value stack. PushCostumeList, + /// Consumes 1 value, `costume`, from the value stack and pushes its name onto the value stack. + PushCostumeName, + /// Consumes 1 value, `costume`, from the value stack and assigns it as the current costume. /// This can be an image or the name of a static costume on the entity. /// Empty string can be used to remove the current costume. @@ -860,31 +863,33 @@ impl<'a> BinaryRead<'a> for Instruction<'a> { 119 => read_prefixed!(Instruction::PushCostume), 120 => read_prefixed!(Instruction::PushCostumeNumber), 121 => read_prefixed!(Instruction::PushCostumeList), - 122 => read_prefixed!(Instruction::SetCostume), - 123 => read_prefixed!(Instruction::NextCostume), + 122 => read_prefixed!(Instruction::PushCostumeName), + + 123 => read_prefixed!(Instruction::SetCostume), + 124 => read_prefixed!(Instruction::NextCostume), - 124 => read_prefixed!(Instruction::PushSoundList), - 125 => read_prefixed!(Instruction::PlaySound { blocking: true }), - 126 => read_prefixed!(Instruction::PlaySound { blocking: false }), - 127 => read_prefixed!(Instruction::PlayNotes { blocking: true }), - 128 => read_prefixed!(Instruction::PlayNotes { blocking: false }), - 129 => read_prefixed!(Instruction::StopSounds), + 125 => read_prefixed!(Instruction::PushSoundList), + 126 => read_prefixed!(Instruction::PlaySound { blocking: true }), + 127 => read_prefixed!(Instruction::PlaySound { blocking: false }), + 128 => read_prefixed!(Instruction::PlayNotes { blocking: true }), + 129 => read_prefixed!(Instruction::PlayNotes { blocking: false }), + 130 => read_prefixed!(Instruction::StopSounds), - 130 => read_prefixed!(Instruction::Clone), - 131 => read_prefixed!(Instruction::DeleteClone), + 131 => read_prefixed!(Instruction::Clone), + 132 => read_prefixed!(Instruction::DeleteClone), - 132 => read_prefixed!(Instruction::ClearEffects), - 133 => read_prefixed!(Instruction::ClearDrawings), + 133 => read_prefixed!(Instruction::ClearEffects), + 134 => read_prefixed!(Instruction::ClearDrawings), - 134 => read_prefixed!(Instruction::GotoXY), - 135 => read_prefixed!(Instruction::Goto), + 135 => read_prefixed!(Instruction::GotoXY), + 136 => read_prefixed!(Instruction::Goto), - 136 => read_prefixed!(Instruction::PointTowardsXY), - 137 => read_prefixed!(Instruction::PointTowards), + 137 => read_prefixed!(Instruction::PointTowardsXY), + 138 => read_prefixed!(Instruction::PointTowards), - 138 => read_prefixed!(Instruction::Forward), + 139 => read_prefixed!(Instruction::Forward), - 139 => read_prefixed!(Instruction::UnknownBlock {} : name, args), + 140 => read_prefixed!(Instruction::UnknownBlock {} : name, args), _ => unreachable!(), } @@ -1071,31 +1076,33 @@ impl BinaryWrite for Instruction<'_> { Instruction::PushCostume => append_prefixed!(119), Instruction::PushCostumeNumber => append_prefixed!(120), Instruction::PushCostumeList => append_prefixed!(121), - Instruction::SetCostume => append_prefixed!(122), - Instruction::NextCostume => append_prefixed!(123), + Instruction::PushCostumeName => append_prefixed!(122), + + Instruction::SetCostume => append_prefixed!(123), + Instruction::NextCostume => append_prefixed!(124), - Instruction::PushSoundList => append_prefixed!(124), - Instruction::PlaySound { blocking: true } => append_prefixed!(125), - Instruction::PlaySound { blocking: false } => append_prefixed!(126), - Instruction::PlayNotes { blocking: true } => append_prefixed!(127), - Instruction::PlayNotes { blocking: false } => append_prefixed!(128), - Instruction::StopSounds => append_prefixed!(129), + Instruction::PushSoundList => append_prefixed!(125), + Instruction::PlaySound { blocking: true } => append_prefixed!(126), + Instruction::PlaySound { blocking: false } => append_prefixed!(127), + Instruction::PlayNotes { blocking: true } => append_prefixed!(128), + Instruction::PlayNotes { blocking: false } => append_prefixed!(129), + Instruction::StopSounds => append_prefixed!(130), - Instruction::Clone => append_prefixed!(130), - Instruction::DeleteClone => append_prefixed!(131), + Instruction::Clone => append_prefixed!(131), + Instruction::DeleteClone => append_prefixed!(132), - Instruction::ClearEffects => append_prefixed!(132), - Instruction::ClearDrawings => append_prefixed!(133), + Instruction::ClearEffects => append_prefixed!(133), + Instruction::ClearDrawings => append_prefixed!(134), - Instruction::GotoXY => append_prefixed!(134), - Instruction::Goto => append_prefixed!(135), + Instruction::GotoXY => append_prefixed!(135), + Instruction::Goto => append_prefixed!(136), - Instruction::PointTowardsXY => append_prefixed!(136), - Instruction::PointTowards => append_prefixed!(137), + Instruction::PointTowardsXY => append_prefixed!(137), + Instruction::PointTowards => append_prefixed!(138), - Instruction::Forward => append_prefixed!(138), + Instruction::Forward => append_prefixed!(139), - Instruction::UnknownBlock { name, args } => append_prefixed!(139: move str name, args), + Instruction::UnknownBlock { name, args } => append_prefixed!(140: move str name, args), } } } @@ -1127,8 +1134,8 @@ pub(crate) enum InitValue { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub(crate) enum RefValue { List(Vec), - Image(Vec, Option<(Number, Number)>), - Audio(Vec), + Image(Vec, Option<(Number, Number)>, CompactString), + Audio(Vec, CompactString), String(CompactString), } #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -1552,6 +1559,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::CostumeName { costume } => self.append_simple_ins(entity, &[costume], Instruction::PushCostumeName)?, 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()), @@ -2532,7 +2540,7 @@ impl ByteCode { } } - fn get_value<'a>(value: &'a ast::Value, ref_values: &mut Vec<(Option, &'a ast::Value)>, refs: &BTreeMap, string_refs: &mut BTreeMap<&'a str, usize>, image_refs: &mut BTreeMap<*const (Vec, Option<(f64, f64)>), usize>, audio_refs: &mut BTreeMap<*const Vec, usize>) -> Result> { + fn get_value<'a>(value: &'a ast::Value, ref_values: &mut Vec<(Option, &'a ast::Value)>, refs: &BTreeMap, string_refs: &mut BTreeMap<&'a str, usize>, image_refs: &mut BTreeMap<*const (Vec, Option<(f64, f64)>, CompactString), usize>, audio_refs: &mut BTreeMap<*const (Vec, CompactString), usize>) -> Result> { Ok(match value { ast::Value::Bool(x) => InitValue::Bool(*x), ast::Value::Number(x) => InitValue::Number(Number::new(*x)?), @@ -2554,14 +2562,14 @@ impl ByteCode { ast::Value::Image(x) => { let center = x.1.map(|(x, y)| Ok::<_,NumberError>((Number::new(x)?, Number::new(y)?))).transpose()?; let idx = *image_refs.entry(Rc::as_ptr(x)).or_insert_with(|| { - ref_values.push((Some(RefValue::Image(x.0.clone(), center)), value)); + ref_values.push((Some(RefValue::Image(x.0.clone(), center, x.2.clone())), value)); ref_values.len() - 1 }); InitValue::Ref(idx) } ast::Value::Audio(x) => { let idx = *audio_refs.entry(Rc::as_ptr(x)).or_insert_with(|| { - ref_values.push((Some(RefValue::Audio((**x).clone())), value)); + ref_values.push((Some(RefValue::Audio(x.0.clone(), x.1.clone())), value)); ref_values.len() - 1 }); InitValue::Ref(idx) diff --git a/src/process.rs b/src/process.rs index 1ebcd83..d3e9f6c 100644 --- a/src/process.rs +++ b/src/process.rs @@ -1230,6 +1230,15 @@ impl<'gc, C: CustomTypes, S: System> Process<'gc, C, S> { self.value_stack.push(Value::List(Gc::new(mc, RefLock::new(entity.costume_list.iter().map(|x| Value::Image(x.1.clone())).collect())))); self.pos = aft_pos; } + Instruction::PushCostumeName => { + let costume = self.value_stack.pop().unwrap(); + self.value_stack.push(match costume { + Value::String(x) if x.is_empty() => empty_string().into(), + Value::Image(x) => Rc::new(x.name.clone()).into(), + x => return Err(ErrorCause::ConversionError { got: x.get_type(), expected: Type::Image }), + }); + self.pos = aft_pos; + } Instruction::SetCostume => { let mut entity_raw = self.call_stack.last().unwrap().entity.borrow_mut(mc); let entity = &mut *entity_raw; diff --git a/src/runtime.rs b/src/runtime.rs index 7102de0..dfdfb9f 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -644,7 +644,9 @@ pub struct Image { pub content: Vec, /// The center `(x, y)` of the image as used for NetsBlox sprites. /// [`None`] is implied to represent `(w / 2, h / 2)` based on the true image size (size decoding cannot be done in no-std). - pub center: Option<(Number, Number)> + pub center: Option<(Number, Number)>, + /// The user-level name of the image + pub name: CompactString, } /// An audio clip type that can be used in the VM. @@ -652,6 +654,8 @@ pub struct Image { pub struct Audio { /// The raw binary content of the audio clip pub content: Vec, + /// The user-level name of the audio clip + pub name: CompactString, } /// An error produced by [`Value::to_simple`] @@ -739,7 +743,7 @@ impl SimpleValue { SimpleValue::List(x) => Json::Array(x.into_iter().map(SimpleValue::into_netsblox_json).collect()), SimpleValue::Image(img) => { let center_attrs = img.center.map(|(x, y)| format!(" center-x=\"{x}\" center-y=\"{y}\"")).unwrap_or_default(); - Json::String(format!("", crate::util::base64_encode(&img.content))) + Json::String(format!("", ast::util::xml_escape(&img.name), crate::util::base64_encode(&img.content))) }, SimpleValue::Audio(audio) => Json::String(format!("", crate::util::base64_encode(&audio.content))), } @@ -762,11 +766,13 @@ impl SimpleValue { let mut center_x = None; let mut center_y = None; let mut content = None; + let mut name = "untitled".into(); loop { match tokenizer.next() { Some(Ok(xmlparser::Token::Attribute { local, value, .. })) => match local.as_str() { "center-x" => center_x = Some(value.as_str().parse().ok().and_then(|x| Number::new(x).ok()).ok_or(FromNetsBloxJsonError::BadImage)?), "center-y" => center_y = Some(value.as_str().parse().ok().and_then(|y| Number::new(y).ok()).ok_or(FromNetsBloxJsonError::BadImage)?), + "name" => name = value.as_str().into(), "image" => match value.as_str().split(";base64,").nth(1) { Some(raw) if value.as_str().starts_with("data:image/") => content = Some(crate::util::base64_decode(raw).map_err(|_| FromNetsBloxJsonError::BadImage)?), _ => return Err(FromNetsBloxJsonError::BadImage), @@ -774,7 +780,7 @@ impl SimpleValue { _ => (), } Some(Ok(xmlparser::Token::ElementEnd { .. })) => match content { - Some(content) => return Ok(SimpleValue::Image(Image { content, center: center_x.zip(center_y) })), + Some(content) => return Ok(SimpleValue::Image(Image { content, center: center_x.zip(center_y), name })), None => return Ok(SimpleValue::String(x.into())), } Some(Ok(_)) => (), @@ -784,9 +790,11 @@ impl SimpleValue { } "sound" => { let mut content = None; + let mut name = "untitled".into(); loop { match tokenizer.next() { Some(Ok(xmlparser::Token::Attribute { local, value, .. })) => match local.as_str() { + "name" => name = value.as_str().into(), "sound" => match value.as_str().split(";base64,").nth(1) { Some(raw) if value.as_str().starts_with("data:audio/") => content = Some(crate::util::base64_decode(raw).map_err(|_| FromNetsBloxJsonError::BadAudio)?), _ => return Err(FromNetsBloxJsonError::BadAudio), @@ -794,7 +802,7 @@ impl SimpleValue { _ => (), } Some(Ok(xmlparser::Token::ElementEnd { .. })) => match content { - Some(content) => return Ok(SimpleValue::Audio(Audio { content })), + Some(content) => return Ok(SimpleValue::Audio(Audio { content, name })), None => return Ok(SimpleValue::String(x.into())), } Some(Ok(_)) => (), @@ -861,10 +869,10 @@ fn test_netsblox_json() { SimpleValue::String("".into()), SimpleValue::String("".into()), SimpleValue::String("".into()), - SimpleValue::Image(Image { content: vec![], center: None }), - SimpleValue::Image(Image { content: vec![], center: Some((Number::new(0.0).unwrap(), Number::new(4.5).unwrap())) }), - SimpleValue::Image(Image { content: vec![0, 1, 2, 255, 254, 253, 127, 128], center: None }), - SimpleValue::Image(Image { content: vec![0, 1, 2, 255, 254, 253, 127, 128, 6, 9], center: Some((Number::new(12.5).unwrap(), Number::new(-54.0).unwrap())) }), + SimpleValue::Image(Image { content: vec![], center: None, name: "test".into() }), + SimpleValue::Image(Image { content: vec![], center: Some((Number::new(0.0).unwrap(), Number::new(4.5).unwrap())), name: "another one".into() }), + SimpleValue::Image(Image { content: vec![0, 1, 2, 255, 254, 253, 127, 128], center: None, name: "untitled".into() }), + SimpleValue::Image(Image { content: vec![0, 1, 2, 255, 254, 253, 127, 128, 6, 9], center: Some((Number::new(12.5).unwrap(), Number::new(-54.0).unwrap())), name: "last one i swear".into() }), ]); let js = val.clone().into_netsblox_json(); let back = SimpleValue::from_netsblox_json(js).unwrap(); @@ -1372,8 +1380,8 @@ impl<'gc, C: CustomTypes, S: System> GlobalContext<'gc, C, S> { pub fn from_init(mc: &Mutation<'gc>, init_info: &InitInfo, bytecode: Rc, settings: Settings, system: Rc) -> Self { let allocated_refs = init_info.ref_values.iter().map(|ref_value| match ref_value { RefValue::String(value) => Value::String(Rc::new(value.clone())), - RefValue::Image(content, center) => Value::Image(Rc::new(Image {content: content.clone(), center: *center })), - RefValue::Audio(content) => Value::Audio(Rc::new(Audio { content: content.clone() })), + RefValue::Image(content, center, name) => Value::Image(Rc::new(Image {content: content.clone(), center: *center, name: name.clone() })), + RefValue::Audio(content, name) => Value::Audio(Rc::new(Audio { content: content.clone(), name: name.clone() })), RefValue::List(_) => Value::List(Gc::new(mc, Default::default())), }).collect::>(); @@ -1387,7 +1395,7 @@ impl<'gc, C: CustomTypes, S: System> GlobalContext<'gc, C, S> { for (allocated_ref, ref_value) in iter::zip(&allocated_refs, &init_info.ref_values) { match ref_value { - RefValue::String(_) | RefValue::Image(_, _) | RefValue::Audio(_) => continue, // we already populated these values in the first pass + RefValue::String(_) | RefValue::Image(_, _, _) | RefValue::Audio(_, _) => continue, // we already populated these values in the first pass RefValue::List(values) => { let allocated_ref = match allocated_ref { Value::List(x) => x, diff --git a/src/std_system.rs b/src/std_system.rs index af462e4..7e07188 100644 --- a/src/std_system.rs +++ b/src/std_system.rs @@ -55,9 +55,9 @@ async fn call_rpc_async, S: System>(context: &NetsBloxConte } if content_type.contains("image/") { - Ok(SimpleValue::Image(Image { content: res, center: None })) + Ok(SimpleValue::Image(Image { content: res, center: None, name: "untitled".into() })) } else if content_type.contains("audio/") { - Ok(SimpleValue::Audio(Audio { content: res })) + Ok(SimpleValue::Audio(Audio { content: res, name: "untitled".into() })) } else if let Some(x) = parse_json_slice::(&res).ok() { SimpleValue::from_netsblox_json(x).map_err(|e| format_compact!("Received ill-formed success value: {e:?}")) } else if let Ok(x) = CompactString::from_utf8(res) { diff --git a/src/test/project.rs b/src/test/project.rs index 3e8c507..6a3d032 100644 --- a/src/test/project.rs +++ b/src/test/project.rs @@ -483,6 +483,39 @@ fn test_proj_delete_clone() { }); } +#[test] +fn test_proj_costume_names() { + let config = Config::> { + command: Some(Rc::new(|_, key, command, _| match command { + Command::SetCostume => { + key.complete(Ok(())); + CommandStatus::Handled + } + _ => CommandStatus::UseDefault { key, command }, + })), + request: None, + }; + 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/costume-names.xml"), system); + 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 expected = Value::from_simple(mc, SimpleValue::from_json(json!([ + 0, + "", + "", + "", + "IndexOutOfBounds { index: 0, len: 3 }", + ["squiggle", "squiggle", "squiggle", "squiggle", "squiggle", 1], + ["zap", "zap", "zap", "zap", "zap", 3], + ["zip", "zip", "zip", "zip", "zip", 2], + ])).unwrap()); + assert_values_eq(&global_context.globals.lookup("res").unwrap().get(), &expected, 1e-10, "res"); + }); +} + #[test] fn test_proj_sounds() { let sound_events = Rc::new(RefCell::new(vec![])); diff --git a/src/test/projects/costume-names.xml b/src/test/projects/costume-names.xml new file mode 100644 index 0000000..6e4edee --- /dev/null +++ b/src/test/projects/costume-names.xml @@ -0,0 +1 @@ +messagemsgerrsquiggleSprite(2)#1Sprite(2)#1zapSprite(2)#1Sprite(2)#1zipSprite(2)#1Sprite(2)#1 \ No newline at end of file