From ae1f11282570ea37c3bb0b5f7af12a1b6f634b01 Mon Sep 17 00:00:00 2001 From: Devin Jean Date: Wed, 15 Nov 2023 17:06:49 -0600 Subject: [PATCH] add example --- examples/basic.rs | 152 ++++++++++++++++++++++++++++++++++++++++++++ src/cli.rs | 37 ++++++++--- src/main.rs | 10 +-- src/runtime.rs | 18 +++--- src/std_system.rs | 10 +-- src/test/mod.rs | 8 +-- src/test/process.rs | 10 +-- src/test/project.rs | 2 +- 8 files changed, 208 insertions(+), 39 deletions(-) create mode 100644 examples/basic.rs diff --git a/examples/basic.rs b/examples/basic.rs new file mode 100644 index 0000000..c7bd568 --- /dev/null +++ b/examples/basic.rs @@ -0,0 +1,152 @@ +use netsblox_vm::real_time::*; +use netsblox_vm::std_system::*; +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 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<&Entity<'_, C, StdSystem>> for ProcessState { + fn from(_: &Entity<'_, C, StdSystem>) -> Self { + ProcessState + } +} + +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(BASE_URL.to_owned(), 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; + } + } +} diff --git a/src/cli.rs b/src/cli.rs index 1447a12..c550521 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -51,6 +51,8 @@ const STEPS_PER_IO_ITER: usize = 64; const MAX_REQUEST_SIZE_BYTES: usize = 1024 * 1024 * 1024; const YIELDS_BEFORE_IDLE_SLEEP: usize = 256; const IDLE_SLEEP_TIME: Duration = Duration::from_micros(500); +const CLOCK_INTERVAL: Duration = Duration::from_millis(10); +const COLLECT_INTERVAL: Duration = Duration::from_secs(60); macro_rules! crash { ($ret:literal : $($tt:tt)*) => {{ @@ -189,7 +191,7 @@ fn run_proj_tty>>(project_name: &str, server: String let config = overrides.fallback(&Config { command: { let update_flag = update_flag.clone(); - Some(Rc::new(move |_, _, key, command, proc| match command { + Some(Rc::new(move |_, key, command, proc| match command { Command::Print { style: _, value } => { let entity = &*proc.get_call_stack().last().unwrap().entity.borrow(); if let Some(value) = value { @@ -205,7 +207,7 @@ fn run_proj_tty>>(project_name: &str, server: String request: { let update_flag = update_flag.clone(); let input_queries = input_queries.clone(); - Some(Rc::new(move |_, _, key, request, proc| match request { + Some(Rc::new(move |_, key, request, proc| match request { Request::Input { prompt } => { let entity = &*proc.get_call_stack().last().unwrap().entity.borrow(); input_queries.borrow_mut().push_back((format!("{entity:?} {prompt:?} > "), key)); @@ -217,11 +219,11 @@ fn run_proj_tty>>(project_name: &str, server: String }, }); - let system = Rc::new(StdSystem::new_sync(server, Some(project_name), config, clock)); + let system = Rc::new(StdSystem::new_sync(server, Some(project_name), config, clock.clone())); let mut idle_sleeper = IdleAction::new(YIELDS_BEFORE_IDLE_SLEEP, Box::new(|| thread::sleep(IDLE_SLEEP_TIME))); print!("public id: {}\r\n", system.get_public_id()); - let env = match get_env(role, system) { + let mut env = match get_env(role, system) { Ok(x) => x, Err(e) => { print!("error loading project: {e:?}\r\n"); @@ -230,6 +232,7 @@ fn run_proj_tty>>(project_name: &str, server: String }; env.mutate(|mc, env| env.proj.borrow_mut(mc).input(mc, Input::Start)); + let mut next_collect = clock.read(Precision::Medium) + COLLECT_INTERVAL; let mut input_sequence = Vec::with_capacity(16); let in_input_mode = || !input_queries.borrow().is_empty(); 'program: loop { @@ -273,6 +276,10 @@ fn run_proj_tty>>(project_name: &str, server: String idle_sleeper.consume(&res); } }); + if clock.read(Precision::Low) > next_collect { + env.collect_all(); + next_collect = clock.read(Precision::Medium) + COLLECT_INTERVAL; + } if update_flag.get() { update_flag.set(false); @@ -299,7 +306,7 @@ fn run_proj_tty>>(project_name: &str, server: String fn run_proj_non_tty>>(project_name: &str, server: String, role: &ast::Role, overrides: Config>, clock: Arc) { let config = overrides.fallback(&Config { request: None, - command: Some(Rc::new(move |_, _, key, command, proc| match command { + command: Some(Rc::new(move |_, key, command, proc| match command { Command::Print { style: _, value } => { let entity = &*proc.get_call_stack().last().unwrap().entity.borrow(); if let Some(value) = value { println!("{entity:?} > {value:?}") } @@ -310,11 +317,11 @@ fn run_proj_non_tty>>(project_name: &str, server: St })), }); - let system = Rc::new(StdSystem::new_sync(server, Some(project_name), config, clock)); + let system = Rc::new(StdSystem::new_sync(server, Some(project_name), config, clock.clone())); let mut idle_sleeper = IdleAction::new(YIELDS_BEFORE_IDLE_SLEEP, Box::new(|| thread::sleep(IDLE_SLEEP_TIME))); println!(">>> public id: {}\n", system.get_public_id()); - let env = match get_env(role, system) { + let mut env = match get_env(role, system) { Ok(x) => x, Err(e) => { println!(">>> error loading project: {e:?}"); @@ -323,6 +330,7 @@ fn run_proj_non_tty>>(project_name: &str, server: St }; env.mutate(|mc, env| env.proj.borrow_mut(mc).input(mc, Input::Start)); + let mut next_collect = clock.read(Precision::Medium) + COLLECT_INTERVAL; loop { env.mutate(|mc, env| { let mut proj = env.proj.borrow_mut(mc); @@ -334,6 +342,10 @@ fn run_proj_non_tty>>(project_name: &str, server: St idle_sleeper.consume(&res); } }); + if clock.read(Precision::Low) > next_collect { + env.collect_all(); + next_collect = clock.read(Precision::Medium) + COLLECT_INTERVAL; + } } } fn run_server>>(nb_server: String, addr: String, port: u16, overrides: Config>, clock: Arc, syscalls: &[SyscallMenu]) { @@ -385,7 +397,7 @@ fn run_server>>(nb_server: String, addr: String, por let weak_state = Arc::downgrade(&state); let config = overrides.fallback(&Config { request: None, - command: Some(Rc::new(move |_, _, key, command, proc| match command { + command: Some(Rc::new(move |_, key, command, proc| match command { Command::Print { style: _, value } => { let entity = &*proc.get_call_stack().last().unwrap().entity.borrow(); if let Some(value) = value { tee_println!(weak_state.upgrade() => "{entity:?} > {value:?}") } @@ -395,7 +407,7 @@ fn run_server>>(nb_server: String, addr: String, por _ => CommandStatus::UseDefault { key, command }, })), }); - let system = Rc::new(StdSystem::new_sync(nb_server, Some("native-server"), config, clock)); + let system = Rc::new(StdSystem::new_sync(nb_server, Some("native-server"), config, clock.clone())); let mut idle_sleeper = IdleAction::new(YIELDS_BEFORE_IDLE_SLEEP, Box::new(|| thread::sleep(IDLE_SLEEP_TIME))); println!("public id: {}", system.get_public_id()); @@ -473,6 +485,7 @@ fn run_server>>(nb_server: String, addr: String, por let (_, empty_role) = open_project(EMPTY_PROJECT, None).unwrap_or_else(|_| crash!(666: "default project failed to load")); let mut env = get_env(&empty_role, system.clone()).unwrap(); + let mut next_collect = clock.read(Precision::Medium) + COLLECT_INTERVAL; 'program: loop { 'input: loop { match proj_receiver.try_recv() { @@ -544,6 +557,10 @@ fn run_server>>(nb_server: String, addr: String, por idle_sleeper.consume(&res); } }); + if clock.read(Precision::Low) > next_collect { + env.collect_all(); + next_collect = clock.read(Precision::Medium) + COLLECT_INTERVAL; + } } } @@ -553,7 +570,7 @@ pub fn run>>(mode: Mode, config: Config> for C { type EntityState = EntityState; type ProcessState = ProcessState; - fn from_intermediate<'gc>(mc: &Mutation<'gc>, value: Self::Intermediate) -> Result>, ErrorCause>> { - Ok(match value { + fn from_intermediate<'gc>(mc: &Mutation<'gc>, value: Self::Intermediate) -> Value<'gc, C, StdSystem> { + match value { Intermediate::Simple(x) => Value::from_simple(mc, x), Intermediate::Native(x) => Value::Native(Rc::new(x)), - }) + } } } @@ -98,7 +98,7 @@ fn main() { if args.fs { let new_config = Config::> { - request: Some(Rc::new(move |_, _, key, request, _| match &request { + request: Some(Rc::new(move |_, key, request, _| match &request { Request::Syscall { name, args } => match name.as_str() { "open" => { let (path, mode) = match args.as_slice() { diff --git a/src/runtime.rs b/src/runtime.rs index 066312d..d262807 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -1675,9 +1675,9 @@ pub enum CommandStatus<'gc, 'a, C: CustomTypes, S: System> { #[educe(Clone)] pub struct Config, S: System> { /// A function used to perform asynchronous requests that yield a value back to the runtime. - pub request: Option Fn(&S, &Mutation<'gc>, S::RequestKey, Request<'gc, C, S>, &mut Process<'gc, C, S>) -> RequestStatus<'gc, C, S>>>, + pub request: Option Fn(&Mutation<'gc>, S::RequestKey, Request<'gc, C, S>, &mut Process<'gc, C, S>) -> RequestStatus<'gc, C, S>>>, /// A function used to perform asynchronous tasks whose completion is awaited by the runtime. - pub command: Option Fn(&S, &Mutation<'gc>, S::CommandKey, Command<'gc, 'a, C, S>, &mut Process<'gc, C, S>) -> CommandStatus<'gc, 'a, C, S>>>, + pub command: Option Fn(&Mutation<'gc>, S::CommandKey, Command<'gc, 'a, C, S>, &mut Process<'gc, C, S>) -> CommandStatus<'gc, 'a, C, S>>>, } impl, S: System> Default for Config { fn default() -> Self { @@ -1692,20 +1692,20 @@ impl, S: System> Config { pub fn fallback(&self, other: &Self) -> Self { Self { request: match (self.request.clone(), other.request.clone()) { - (Some(a), Some(b)) => Some(Rc::new(move |system, mc, key, request, proc| { - match a(system, mc, key, request, proc) { + (Some(a), Some(b)) => Some(Rc::new(move |mc, key, request, proc| { + match a(mc, key, request, proc) { RequestStatus::Handled => RequestStatus::Handled, - RequestStatus::UseDefault { key, request } => b(system, mc, key, request, proc), + RequestStatus::UseDefault { key, request } => b(mc, key, request, proc), } })), (Some(a), None) | (None, Some(a)) => Some(a), (None, None) => None, }, command: match (self.command.clone(), other.command.clone()) { - (Some(a), Some(b)) => Some(Rc::new(move |system, mc, key, command, proc| { - match a(system, mc, key, command, proc) { + (Some(a), Some(b)) => Some(Rc::new(move |mc, key, command, proc| { + match a(mc, key, command, proc) { CommandStatus::Handled => CommandStatus::Handled, - CommandStatus::UseDefault { key, command } => b(system, mc, key, command, proc), + CommandStatus::UseDefault { key, command } => b(mc, key, command, proc), } })), (Some(a), None) | (None, Some(a)) => Some(a), @@ -1740,7 +1740,7 @@ pub trait CustomTypes>: 'static + Sized { type ProcessState: 'static + for<'gc, 'a> From<&'a Entity<'gc, Self, S>>; /// Converts a [`Value`] into a [`CustomTypes::Intermediate`] for use outside of gc context. - fn from_intermediate<'gc>(mc: &Mutation<'gc>, value: Self::Intermediate) -> Result, ErrorCause>; + fn from_intermediate<'gc>(mc: &Mutation<'gc>, value: Self::Intermediate) -> Value<'gc, Self, S>; } /// The time as determined by an implementation of [`System`]. diff --git a/src/std_system.rs b/src/std_system.rs index b704c00..265a54b 100644 --- a/src/std_system.rs +++ b/src/std_system.rs @@ -358,7 +358,7 @@ impl>> StdSystem { let context_clone = context.clone(); let config = config.fallback(&Config { - request: Some(Rc::new(move |system, _, key, request, _| { + request: Some(Rc::new(move |_, key, request, proc| { match request { Request::Rpc { service, rpc, args } => match (service.as_str(), rpc.as_str(), args.as_slice()) { ("PublicRoles", "getPublicRoleId", []) => { @@ -367,7 +367,7 @@ impl>> StdSystem { } _ => { match args.into_iter().map(|(k, v)| Ok((k, v.to_simple()?.into_netsblox_json()?))).collect::>>() { - Ok(args) => system.rpc_request_pipe.send(RpcRequest { service, rpc, args, key }).unwrap(), + Ok(args) => proc.global_context.borrow().system.rpc_request_pipe.send(RpcRequest { service, rpc, args, key }).unwrap(), Err(err) => key.complete(Err(format!("failed to convert RPC args to json: {err:?}"))), } RequestStatus::Handled @@ -455,7 +455,7 @@ impl>> System for StdSystem { Ok(match self.config.request.as_ref() { Some(handler) => { let key = RequestKey(Arc::new(Mutex::new(AsyncResult::new()))); - match handler(self, mc, RequestKey(key.0.clone()), request, proc) { + match handler(mc, RequestKey(key.0.clone()), request, proc) { RequestStatus::Handled => key, RequestStatus::UseDefault { key: _, request } => return Err(ErrorCause::NotSupported { feature: request.feature() }), } @@ -468,7 +468,7 @@ impl>> System for StdSystem { Self::check_runtime_borrows(mc, proc); Ok(match key.poll() { - AsyncResult::Completed(Ok(x)) => AsyncResult::Completed(Ok(C::from_intermediate(mc, x)?)), + AsyncResult::Completed(Ok(x)) => AsyncResult::Completed(Ok(C::from_intermediate(mc, x))), AsyncResult::Completed(Err(x)) => AsyncResult::Completed(Err(x)), AsyncResult::Pending => AsyncResult::Pending, AsyncResult::Consumed => AsyncResult::Consumed, @@ -482,7 +482,7 @@ impl>> System for StdSystem { Ok(match self.config.command.as_ref() { Some(handler) => { let key = CommandKey(Arc::new(Mutex::new(AsyncResult::new()))); - match handler(self, mc, CommandKey(key.0.clone()), command, proc) { + match handler(mc, CommandKey(key.0.clone()), command, proc) { CommandStatus::Handled => key, CommandStatus::UseDefault { key: _, command } => return Err(ErrorCause::NotSupported { feature: command.feature() }), } diff --git a/src/test/mod.rs b/src/test/mod.rs index 80b8041..f01ef8b 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -46,14 +46,14 @@ impl From<&Entity<'_, C, StdSystem>> for ProcessState { fn default_properties_config() -> Config> { Config { - request: Some(Rc::new(|_, _, key, request, proc| { + request: Some(Rc::new(|_, key, request, proc| { let entity = proc.get_call_stack().last().unwrap().entity.borrow(); match request { Request::Property { prop } => entity.state.props.perform_get_property(key, prop), _ => RequestStatus::UseDefault { key, request }, } })), - command: Some(Rc::new(|_, mc, key, command, proc| { + command: Some(Rc::new(|mc, key, command, proc| { let mut entity = proc.get_call_stack().last().unwrap().entity.borrow_mut(mc); match command { Command::SetProperty { prop, value } => entity.state.props.perform_set_property(key, prop, value), @@ -76,8 +76,8 @@ impl CustomTypes> for C { type EntityState = EntityState; type ProcessState = ProcessState; - fn from_intermediate<'gc>(mc: &Mutation<'gc>, value: Self::Intermediate) -> Result>, ErrorCause>> { - Ok(Value::from_simple(mc, value)) + fn from_intermediate<'gc>(mc: &Mutation<'gc>, value: Self::Intermediate) -> Value<'gc, C, StdSystem> { + Value::from_simple(mc, value) } } diff --git a/src/test/process.rs b/src/test/process.rs index 2e95e39..50f3cb4 100644 --- a/src/test/process.rs +++ b/src/test/process.rs @@ -641,7 +641,7 @@ fn test_proc_say() { let output_cpy = output.clone(); let config = Config::> { request: None, - command: Some(Rc::new(move |_, _, key, command, _| match command { + command: Some(Rc::new(move |_, key, command, _| match command { Command::Print { style: _, value } => { if let Some(value) = value { writeln!(*output_cpy.borrow_mut(), "{value:?}").unwrap() } key.complete(Ok(())); @@ -667,7 +667,7 @@ fn test_proc_syscall() { let buffer = Rc::new(RefCell::new(String::new())); let buffer_cpy = buffer.clone(); let config = Config::> { - request: Some(Rc::new(move |_, _, key, request, _| match &request { + request: Some(Rc::new(move |_, key, request, _| match &request { Request::Syscall { name, args } => match name.as_str() { "bar" => match args.is_empty() { false => { @@ -1866,7 +1866,7 @@ fn test_proc_basic_motion() { let config = Config::> { command: { let sequence = sequence.clone(); - Some(Rc::new(move |_, _, key, command, _| { + Some(Rc::new(move |_, key, command, _| { match command { Command::Forward { distance } => sequence.borrow_mut().push(Action::Forward(to_i32(distance))), Command::ChangeProperty { prop: Property::Heading, delta } => sequence.borrow_mut().push(Action::Turn(to_i32(delta.as_number().unwrap()))), @@ -1878,7 +1878,7 @@ fn test_proc_basic_motion() { }, request: { let sequence = sequence.clone(); - Some(Rc::new(move |_, _, key, request, _| { + Some(Rc::new(move |_, key, request, _| { match request { Request::Property { prop: Property::XPos } => { sequence.borrow_mut().push(Action::Position); @@ -2315,7 +2315,7 @@ fn test_proc_extra_blocks() { let actions = Rc::new(RefCell::new(vec![])); let actions_clone = actions.clone(); let config = Config::> { - request: Some(Rc::new(move |_, _, key, request, _| match &request { + request: Some(Rc::new(move |_, key, request, _| match &request { Request::UnknownBlock { name, args } => { match name.as_str() { "tuneScopeSetInstrument" => { diff --git a/src/test/project.rs b/src/test/project.rs index c03803c..8251967 100644 --- a/src/test/project.rs +++ b/src/test/project.rs @@ -436,7 +436,7 @@ fn test_proj_stop_my_others_context() { fn test_proj_costumes() { let config = Config::> { request: None, - command: Some(Rc::new(move |_, _, key, command, _| match command { + command: Some(Rc::new(move |_, key, command, _| match command { Command::SetCostume => { key.complete(Ok(())); CommandStatus::Handled