netsblox_vm/
project.rs

1use alloc::vec::Vec;
2use alloc::boxed::Box;
3use alloc::collections::{VecDeque, BTreeMap};
4use alloc::rc::Rc;
5
6use crate::*;
7use crate::gc::*;
8use crate::slotmap::*;
9use crate::runtime::*;
10use crate::bytecode::*;
11use crate::process::*;
12use crate::compact_str::*;
13use crate::vecmap::*;
14
15new_key! {
16    struct ProcessKey;
17}
18
19/// A state machine that performs an action during idle periods.
20/// 
21/// This could be used to invoke [`std::thread::sleep`] during idle periods to save CPU time.
22pub struct IdleAction {
23    count: usize,
24    thresh: usize,
25    action: Box<dyn FnMut()>,
26}
27impl IdleAction {
28    /// Creates a new [`IdleAction`] that triggers automatically after `max_yields` idle steps.
29    pub fn new(thresh: usize, action: Box<dyn FnMut()>) -> Self {
30        Self { count: 0, thresh, action }
31    }
32    /// Consumes a step result and advances the state machine.
33    /// If the step resulting in an idle action, this may trigger the idle action to fire and reset the state machine.
34    pub fn consume<C: CustomTypes<S>, S: System<C>>(&mut self, res: &ProjectStep<'_, C, S>) {
35        match res {
36            ProjectStep::Idle | ProjectStep::Yield | ProjectStep::Pause => {
37                self.count += 1;
38                if self.count >= self.thresh {
39                    self.trigger();
40                }
41            }
42            ProjectStep::Normal | ProjectStep::ProcessTerminated { .. } | ProjectStep::Error { .. } | ProjectStep::Watcher { .. } => self.count = 0,
43        }
44    }
45    /// Explicitly triggers the idle action and reset the state machine.
46    pub fn trigger(&mut self) {
47        self.count = 0;
48        self.action.as_mut()();
49    }
50}
51
52/// Simulates input from the user.
53#[derive(Debug)]
54pub enum Input {
55    /// Simulate pressing the start (green flag) button.
56    /// This has the effect of interrupting any running "on start" scripts and restarting them (with an empty context).
57    /// Any other running processes are not affected.
58    Start,
59    /// Simulate pressing the stop button.
60    /// This has the effect of stopping all currently-running processes.
61    /// Note that some hat blocks could cause new processes to spin up after this operation.
62    Stop,
63    /// Simulates a key down hat from the keyboard.
64    /// This should be repeated if the button is held down.
65    KeyDown { key: KeyCode },
66    /// Simulates a key up hat from the keyboard.
67    /// Due to the nature of the TTY interface, key up events are not always available, so this hat does not need to be sent.
68    /// If not sent, a timeout is used to determine when a key is released (sending this hat can short-circuit the timeout).
69    KeyUp { key: KeyCode },
70    /// Trigger the execution of a custom event (hat) block script with the given set of message-style input variables.
71    /// The `interrupt` flag can be set to cause any running scripts to stop and wipe their current queues, placing this new execution front and center.
72    /// The `max_queue` field controls the maximum size of the context/schedule execution queue; beyond this size, this (and only this) execution will be dropped.
73    CustomEvent { name: CompactString, args: VecMap<CompactString, SimpleValue, false>, interrupt: bool, max_queue: usize },
74}
75
76/// Result of stepping through the execution of a [`Project`].
77pub enum ProjectStep<'gc, C: CustomTypes<S>, S: System<C>> {
78    /// There were no running processes to execute.
79    Idle,
80    /// The project had a running process, which yielded.
81    Yield,
82    /// The project had a running process, which did any non-yielding operation.
83    Normal,
84    /// The project had a running process which terminated successfully.
85    /// This can be though of as a special case of [`ProjectStep::Normal`],
86    /// but also returns the result and process so it can be queried for state information if needed.
87    ProcessTerminated { result: Value<'gc, C, S>, proc: Process<'gc, C, S> },
88    /// The project had a running process, which encountered a runtime error.
89    /// The dead process is returned, which can be queried for diagnostic information.
90    Error { error: ExecError<C, S>, proc: Process<'gc, C, S> },
91    /// The project had a running process that requested to create or destroy a watcher.
92    /// See [`ProcessStep::Watcher`] for more details.
93    Watcher { create: bool, watcher: Watcher<'gc, C, S> },
94    /// The project had a running process that requested to pause execution of the (entire) project.
95    /// See [`ProcessStep::Pause`] for more details.
96    Pause,
97}
98
99#[derive(Collect)]
100#[collect(no_drop, bound = "")]
101pub struct PartialProcContext<'gc, C: CustomTypes<S>, S: System<C>> {
102                               pub locals: SymbolTable<'gc, C, S>,
103    #[collect(require_static)] pub state: C::ProcessState,
104    #[collect(require_static)] pub barrier: Option<Barrier>,
105    #[collect(require_static)] pub reply_key: Option<InternReplyKey>,
106    #[collect(require_static)] pub local_message: Option<Text>,
107}
108
109#[derive(Collect)]
110#[collect(no_drop, bound = "")]
111struct Script<'gc, C: CustomTypes<S>, S: System<C>> {
112    #[collect(require_static)] event: Rc<(Event, usize)>, // event and bytecode start pos
113                               entity: Gc<'gc, RefLock<Entity<'gc, C, S>>>,
114    #[collect(require_static)] process: Option<ProcessKey>,
115                               context_queue: VecDeque<PartialProcContext<'gc, C, S>>,
116}
117impl<'gc, C: CustomTypes<S>, S: System<C>> Script<'gc, C, S> {
118    fn consume_context(&mut self, state: &mut State<'gc, C, S>) {
119        let process = self.process.and_then(|key| Some((key, state.processes.get_mut(key)?)));
120        match process {
121            Some(proc) => match proc.1.is_running() {
122                true => return,
123                false => unreachable!(), // should have already been cleaned up by the scheduler
124            }
125            None => match self.context_queue.pop_front() {
126                None => return,
127                Some(context) => {
128                    let proc = Process::new(ProcContext { global_context: state.global_context, entity: self.entity, state: context.state, start_pos: self.event.1, locals: context.locals, barrier: context.barrier, reply_key: context.reply_key, local_message: context.local_message });
129                    let key = state.processes.insert(proc);
130                    state.process_queue.push_back(key);
131                    self.process = Some(key);
132                }
133            },
134        }
135    }
136    fn stop_all(&mut self, state: &mut State<'gc, C, S>) {
137        if let Some(process) = self.process.take() {
138            state.processes.remove(process);
139        }
140        self.context_queue.clear();
141    }
142    fn schedule(&mut self, state: &mut State<'gc, C, S>, context: PartialProcContext<'gc, C, S>, max_queue: usize) {
143        self.context_queue.push_back(context);
144        self.consume_context(state);
145        if self.context_queue.len() > max_queue {
146            self.context_queue.pop_back();
147        }
148    }
149}
150
151struct AllContextsConsumer {
152    did_it: bool,
153}
154impl AllContextsConsumer {
155    fn new() -> Self {
156        Self { did_it: false }
157    }
158    fn do_once<C: CustomTypes<S>, S: System<C>>(&mut self, proj: &mut Project<C, S>) {
159        if !core::mem::replace(&mut self.did_it, true) {
160            for script in proj.scripts.iter_mut() {
161                script.consume_context(&mut proj.state);
162            }
163        }
164    }
165}
166
167#[derive(Collect)]
168#[collect(no_drop, bound = "")]
169struct State<'gc, C: CustomTypes<S>, S: System<C>> {
170                               global_context: Gc<'gc, RefLock<GlobalContext<'gc, C, S>>>,
171                               processes: SlotMap<ProcessKey, Process<'gc, C, S>>,
172    #[collect(require_static)] process_queue: VecDeque<ProcessKey>,
173}
174#[derive(Collect)]
175#[collect(no_drop, bound = "")]
176pub struct Project<'gc, C: CustomTypes<S>, S: System<C>> {
177    state: State<'gc, C, S>,
178    scripts: Vec<Script<'gc, C, S>>,
179}
180impl<'gc, C: CustomTypes<S>, S: System<C>> Project<'gc, C, S> {
181    pub fn from_init(mc: &Mutation<'gc>, init_info: &InitInfo, bytecode: Rc<ByteCode>, settings: Settings, system: Rc<S>) -> Self {
182        let global_context = GlobalContext::from_init(mc, init_info, bytecode, settings, system);
183        let mut project = Self::new(Gc::new(mc, RefLock::new(global_context)));
184
185        for entity_info in init_info.entities.iter() {
186            let entity = *project.state.global_context.borrow().entities.get(entity_info.name.as_str()).unwrap();
187            for (event, pos) in entity_info.scripts.iter() {
188                project.add_script(*pos, entity, event.clone());
189            }
190        }
191
192        project
193    }
194    pub fn new(global_context: Gc<'gc, RefLock<GlobalContext<'gc, C, S>>>) -> Self {
195        Self {
196            state: State {
197                global_context,
198                processes: Default::default(),
199                process_queue: Default::default(),
200            },
201            scripts: Default::default(),
202        }
203    }
204    pub fn add_script(&mut self, start_pos: usize, entity: Gc<'gc, RefLock<Entity<'gc, C, S>>>, event: Event) {
205        self.scripts.push(Script {
206            event: Rc::new((event, start_pos)),
207            entity,
208            process: None,
209            context_queue: Default::default(),
210        });
211    }
212    pub fn input(&mut self, mc: &Mutation<'gc>, input: Input) {
213        let mut all_contexts_consumer = AllContextsConsumer::new();
214        match input {
215            Input::Start => {
216                for i in 0..self.scripts.len() {
217                    if let Event::OnFlag = &self.scripts[i].event.0 {
218                        let state = C::ProcessState::from(ProcessKind { entity: self.scripts[i].entity, dispatcher: None });
219
220                        all_contexts_consumer.do_once(self); // need to consume all contexts before scheduling things in the future
221                        self.scripts[i].stop_all(&mut self.state);
222                        self.scripts[i].schedule(&mut self.state, PartialProcContext { state, locals: Default::default(), barrier: None, reply_key: None, local_message: None }, 0);
223                    }
224                }
225            }
226            Input::CustomEvent { name, args, interrupt, max_queue } => {
227                for i in 0..self.scripts.len() {
228                    if let Event::Custom { name: script_event_name, fields } = &self.scripts[i].event.0 {
229                        if name != *script_event_name { continue }
230
231                        let mut locals = SymbolTable::default();
232                        for field in fields.iter() {
233                            let value = args.get(field).map(|x| Value::from_simple(mc, x.clone())).unwrap_or_else(|| Number::new(0.0).unwrap().into());
234                            locals.define_or_redefine(field, value.into());
235                        }
236
237                        let state = C::ProcessState::from(ProcessKind { entity: self.scripts[i].entity, dispatcher: None });
238
239                        all_contexts_consumer.do_once(self); // need to consume all contexts before scheduling things in the future
240                        if interrupt { self.scripts[i].stop_all(&mut self.state); }
241                        self.scripts[i].schedule(&mut self.state, PartialProcContext { locals, state, barrier: None, reply_key: None, local_message: None }, max_queue);
242                    }
243                }
244            }
245            Input::Stop => {
246                for script in self.scripts.iter_mut() {
247                    script.stop_all(&mut self.state);
248                }
249                self.state.processes.clear();
250                self.state.process_queue.clear();
251            }
252            Input::KeyDown { key: input_key } => {
253                for i in 0..self.scripts.len() {
254                    if let Event::OnKey { key_filter } = &self.scripts[i].event.0 {
255                        if key_filter.map(|x| x == input_key).unwrap_or(true) {
256                            let state = C::ProcessState::from(ProcessKind { entity: self.scripts[i].entity, dispatcher: None });
257
258                            all_contexts_consumer.do_once(self); // need to consume all contexts before scheduling things in the future
259                            self.scripts[i].schedule(&mut self.state, PartialProcContext { state, locals: Default::default(), barrier:None, reply_key: None, local_message: None }, 0);
260                        }
261                    }
262                }
263            }
264            Input::KeyUp { .. } => unimplemented!(),
265        }
266    }
267    pub fn step(&mut self, mc: &Mutation<'gc>) -> ProjectStep<'gc, C, S> {
268        let mut all_contexts_consumer = AllContextsConsumer::new();
269
270        let msg = self.state.global_context.borrow().system.receive_message();
271        if let Some(IncomingMessage { msg_type, values, reply_key }) = msg {
272            let values: BTreeMap<_,_> = values.into_iter().collect();
273            for i in 0..self.scripts.len() {
274                if let Event::NetworkMessage { msg_type: script_msg_type, fields } = &self.scripts[i].event.0 {
275                    if msg_type != *script_msg_type { continue }
276
277                    let mut locals = SymbolTable::default();
278                    for field in fields.iter() {
279                        let value = values.get(field).map(|x| Value::from_simple(mc, x.clone())).unwrap_or_else(|| Number::new(0.0).unwrap().into());
280                        locals.define_or_redefine(field, value.into());
281                    }
282
283                    let state = C::ProcessState::from(ProcessKind { entity: self.scripts[i].entity, dispatcher: None });
284
285                    all_contexts_consumer.do_once(self); // need to consume all contexts before scheduling things in the future
286                    self.scripts[i].schedule(&mut self.state, PartialProcContext { locals, state, barrier: None, reply_key: reply_key.clone(), local_message: None }, usize::MAX);
287                }
288            }
289        }
290
291        let (proc_key, proc) = loop {
292            match self.state.process_queue.pop_front() {
293                None => {
294                    debug_assert!(self.scripts.iter().all(|x| x.context_queue.is_empty()));
295                    return ProjectStep::Idle;
296                }
297                Some(proc_key) => if let Some(proc) = self.state.processes.get_mut(proc_key) { break (proc_key, proc) }
298            }
299        };
300
301        match proc.step(mc) {
302            Ok(x) => match x {
303                ProcessStep::Normal => {
304                    self.state.process_queue.push_front(proc_key);
305                    ProjectStep::Normal
306                }
307                ProcessStep::Yield => {
308                    all_contexts_consumer.do_once(self); // need to consume all contexts before scheduling things in the future
309                    self.state.process_queue.push_back(proc_key);
310                    ProjectStep::Yield
311                }
312                ProcessStep::Watcher { create, watcher } => {
313                    self.state.process_queue.push_front(proc_key);
314                    ProjectStep::Watcher { create, watcher }
315                }
316                ProcessStep::Pause => {
317                    self.state.process_queue.push_front(proc_key);
318                    ProjectStep::Pause
319                }
320                ProcessStep::Fork { pos, locals, entity } => {
321                    let state = C::ProcessState::from(ProcessKind { entity, dispatcher: Some(proc) });
322                    let proc = Process::new(ProcContext { global_context: self.state.global_context, entity, state, start_pos: pos, locals, barrier: None, reply_key: None, local_message: None });
323                    let fork_proc_key = self.state.processes.insert(proc);
324
325                    all_contexts_consumer.do_once(self); // need to consume all contexts before scheduling things in the future
326                    self.state.process_queue.push_back(fork_proc_key); // forked process starts at end of exec queue
327                    self.state.process_queue.push_front(proc_key); // keep executing the same process as before
328                    ProjectStep::Normal
329                }
330                ProcessStep::CreatedClone { clone } => {
331                    let original = clone.borrow().original.unwrap();
332                    let mut new_scripts = vec![];
333                    for script in self.scripts.iter() {
334                        if Gc::ptr_eq(script.entity, original) {
335                            new_scripts.push(Script {
336                                event: script.event.clone(),
337                                entity: clone,
338                                process: None,
339                                context_queue: Default::default(),
340                            });
341                        }
342                    }
343                    for script in new_scripts.iter_mut() {
344                        if let Event::OnClone = &script.event.0 {
345                            let state = C::ProcessState::from(ProcessKind { entity: script.entity, dispatcher: Some(self.state.processes.get(proc_key).unwrap()) });
346
347                            all_contexts_consumer.do_once(self); // need to consume all contexts before scheduling things in the future
348                            script.schedule(&mut self.state, PartialProcContext { state, locals: Default::default(), barrier: None, reply_key: None, local_message: None }, 0);
349                        }
350                    }
351                    self.scripts.extend(new_scripts);
352                    self.state.process_queue.push_front(proc_key); // keep executing the same process as before
353                    ProjectStep::Normal
354                }
355                ProcessStep::DeletedClone { clone } => {
356                    debug_assert!(clone.borrow().original.is_some());
357                    self.scripts.retain_mut(|script| !Gc::ptr_eq(script.entity, clone));
358                    self.state.processes.retain_mut(|_, proc| !Gc::ptr_eq(proc.get_call_stack().first().unwrap().entity, clone));
359                    self.state.process_queue.push_front(proc_key); // keep executing the same process as before
360                    ProjectStep::Normal
361                }
362                ProcessStep::Broadcast { msg_type, barrier, targets } => {
363                    for i in 0..self.scripts.len() {
364                        if let Event::LocalMessage { msg_type: recv_type } = &self.scripts[i].event.0 {
365                            if recv_type.as_deref().map(|x| x == msg_type).unwrap_or(true) {
366                                if let Some(targets) = &targets {
367                                    if !targets.iter().any(|&target| Gc::ptr_eq(self.scripts[i].entity, target)) {
368                                        continue
369                                    }
370                                }
371
372                                let state = C::ProcessState::from(ProcessKind { entity: self.scripts[i].entity, dispatcher: Some(self.state.processes.get(proc_key).unwrap()) });
373
374                                all_contexts_consumer.do_once(self); // need to consume all contexts before scheduling things in the future
375                                self.scripts[i].stop_all(&mut self.state);
376                                self.scripts[i].schedule(&mut self.state, PartialProcContext { state, locals: Default::default(), barrier: barrier.clone(), reply_key: None, local_message: Some(msg_type.clone()) }, 0);
377                            }
378                        }
379                    }
380                    self.state.process_queue.push_front(proc_key); // keep executing same process, if it was a wait, it'll yield next step
381                    ProjectStep::Normal
382                }
383                ProcessStep::Terminate { result } => {
384                    let proc = self.state.processes.remove(proc_key).unwrap();
385                    all_contexts_consumer.do_once(self); // need to consume all contexts after dropping a process
386                    ProjectStep::ProcessTerminated { result, proc }
387                }
388                ProcessStep::Abort { mode } => match mode {
389                    AbortMode::Current => {
390                        debug_assert!(!proc.is_running());
391                        self.state.processes.remove(proc_key);
392                        ProjectStep::Normal
393                    }
394                    AbortMode::All => {
395                        debug_assert!(!proc.is_running());
396                        self.state.processes.clear();
397                        self.state.process_queue.clear();
398                        ProjectStep::Normal
399                    }
400                    AbortMode::Others => {
401                        debug_assert!(proc.is_running());
402                        self.state.processes.retain_mut(|k, _| k == proc_key);
403                        debug_assert_eq!(self.state.processes.len(), 1);
404                        self.state.process_queue.clear();
405                        self.state.process_queue.push_front(proc_key); // keep executing the calling process
406                        ProjectStep::Normal
407                    }
408                    AbortMode::MyOthers => {
409                        debug_assert!(proc.is_running());
410                        let entity = proc.get_call_stack().last().unwrap().entity;
411                        self.state.processes.retain_mut(|k, v| k == proc_key || !Gc::ptr_eq(entity, v.get_call_stack().first().unwrap().entity));
412                        self.state.process_queue.push_front(proc_key); // keep executing the calling process
413                        ProjectStep::Normal
414                    }
415                }
416                ProcessStep::Idle => unreachable!(),
417            }
418            Err(error) => {
419                let proc = self.state.processes.remove(proc_key).unwrap();
420                all_contexts_consumer.do_once(self); // need to consume all contexts after dropping a process
421                ProjectStep::Error { error, proc }
422            }
423        }
424    }
425    pub fn get_global_context(&self) -> Gc<'gc, RefLock<GlobalContext<'gc, C, S>>> {
426        self.state.global_context
427    }
428}