use netsblox_vm::real_time::*; use netsblox_vm::std_system::*; use netsblox_vm::std_util::*; use netsblox_vm::bytecode::*; use netsblox_vm::process::*; use netsblox_vm::runtime::*; use netsblox_vm::project::*; use netsblox_vm::gc::*; use netsblox_vm::ast; use netsblox_vm::compact_str::CompactString; use std::io::Read; use std::time::Duration; use std::sync::Arc; use std::rc::Rc; // ----------------------------------------------------------------- const BASE_URL: &'static str = "https://cloud.netsblox.org"; const CLOCK_INTERVAL: Duration = Duration::from_millis(10); const COLLECT_INTERVAL: Duration = Duration::from_secs(60); const YIELDS_BEFORE_SLEEP: usize = 64; const IDLE_SLEEP_TIME: Duration = Duration::from_millis(1); // ----------------------------------------------------------------- #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum NativeType {} // type enum for a NativeValue - we don't have any native values we want to expose, so just use an empty enum #[derive(Debug)] enum NativeValue {} // enum for native values that are exposed to the vm - we don't have any we want to expose, so just use an empty enum impl GetType for NativeValue { type Output = NativeType; fn get_type(&self) -> Self::Output { unreachable!() // because we don't have any native values to get the type of } } struct EntityState; // a type to hold custom entity (sprite or stage) state - we don't have any, so just use a unit struct impl From>> for EntityState { fn from(_: EntityKind<'_, '_, C, StdSystem>) -> Self { EntityState } } struct ProcessState; // a type to hold custom process (script) state - we don't have any, so just use a unit struct impl From>> for ProcessState { fn from(_: ProcessKind<'_, '_, C, StdSystem>) -> Self { ProcessState } } impl Unwindable for ProcessState { type UnwindPoint = (); // a type to represent process (script) state unwind points - we don't have any process state, so just use a unit struct fn get_unwind_point(&self) -> Self::UnwindPoint { } fn unwind_to(&mut self, _: &Self::UnwindPoint) { } } struct C; // a type to hold all of our custom type definitions for the vm to use impl CustomTypes> for C { type NativeValue = NativeValue; // a type to hold any native rust values exposed to the vm type Intermediate = SimpleValue; // a Send type that serves as an intermediate between vm gc values and normal rust type EntityState = EntityState; // a type to hold the custom state for an entity (sprite or stage) type ProcessState = ProcessState; // a type to hold the custom state for a process (script) // a function to convert intermediate values into native vm values fn from_intermediate<'gc>(mc: &Mutation<'gc>, value: Self::Intermediate) -> Value<'gc, C, StdSystem> { Value::from_simple(mc, value) } } // our top-level gc arena - this will hold our gc-allocated project and everything it contains #[derive(Collect)] #[collect(no_drop)] struct Env<'gc> { proj: Gc<'gc, RefLock>>>, #[collect(require_static)] locs: Locations, // bytecode locations info for generating error traces } type EnvArena = Arena]>; // converts a netsblox xml project containing a single role into a new gc environment object containing a running project fn get_running_project(xml: &str, system: Rc>) -> EnvArena { EnvArena::new(|mc| { let parser = ast::Parser::default(); let ast = parser.parse(xml).unwrap(); assert_eq!(ast.roles.len(), 1); // this should be handled more elegantly in practice - for the sake of this example, we only allow one role let (bytecode, init_info, locs, _) = ByteCode::compile(&ast.roles[0]).unwrap(); let mut proj = Project::from_init(mc, &init_info, Rc::new(bytecode), Settings::default(), system); proj.input(mc, Input::Start); // this is equivalent to clicking the green flag button Env { proj: Gc::new(mc, RefLock::new(proj)), locs } }) } fn main() { // read in an xml file whose path is given as a command line argument let args = std::env::args().collect::>(); if args.len() != 2 { panic!("usage: {} [xml file]", &args[0]); } let mut xml = String::new(); std::fs::File::open(&args[1]).expect("failed to open file").read_to_string(&mut xml).expect("failed to read file"); // create a new shared clock and start a thread that updates it at our desired interval let clock = Arc::new(Clock::new(UtcOffset::UTC, Some(Precision::Medium))); let clock_clone = clock.clone(); std::thread::spawn(move || loop { std::thread::sleep(CLOCK_INTERVAL); clock_clone.update(); }); // create a custom config for the system - in this simple example we just implement the say/think blocks to print to stdout let config = Config::> { request: None, command: Some(Rc::new(|_mc, key, command, _proc| match command { Command::Print { style: _, value } => { if let Some(value) = value { println!("{value:?}"); } key.complete(Ok(())); // any request that you handle must be completed - otherwise the calling process will hang forever CommandStatus::Handled } _ => CommandStatus::UseDefault { key, command }, // anything you don't handle should return the key and command to invoke the default behavior instead })), }; // initialize our system with all the info we've put together let system = Rc::new(StdSystem::new_sync(CompactString::new(BASE_URL), None, config, clock.clone())); let mut env = get_running_project(&xml, system); // begin running the code - these are some helpers to make things more efficient in terms of memory and cpu resources let mut idle_sleeper = IdleAction::new(YIELDS_BEFORE_SLEEP, Box::new(|| std::thread::sleep(IDLE_SLEEP_TIME))); let mut next_collect = clock.read(Precision::Medium) + COLLECT_INTERVAL; loop { env.mutate(|mc, env| { let mut proj = env.proj.borrow_mut(mc); for _ in 0..1024 { // step the virtual machine forward by one bytecode instruction let res = proj.step(mc); if let ProjectStep::Error { error, proc } = &res { // if we get an error, we can generate an error summary including a stack trace - here we just print out the result let trace = ErrorSummary::extract(error, proc, &env.locs); println!("error: {error:?}\ntrace: {trace:?}"); } // this takes care of performing thread sleep if we get a bunch of no-ops from proj.step back to back idle_sleeper.consume(&res); } }); // if it's time for us to do garbage collection, do it and reset the next collection time if clock.read(Precision::Low) >= next_collect { env.collect_all(); next_collect = clock.read(Precision::Medium) + COLLECT_INTERVAL; } } }