Add tick / delay / untick timer commands.
This commit is contained in:
		
							parent
							
								
									cfe2e6cd6d
								
							
						
					
					
						commit
						56edc3d484
					
				| @ -181,6 +181,7 @@ pub fn install_lua_globals( | ||||
|             register_command!(connect_mud); | ||||
|             register_stateless_command!(create_match_table); | ||||
|             register_command!(cmd_delete_logs, "deletelogs"); | ||||
|             register_command!(delay); | ||||
|             register_command!(delete_mud); | ||||
|             register_command!(cmd_download_logs, "downloadlogs"); | ||||
|             register_command!(echo); | ||||
| @ -191,9 +192,12 @@ pub fn install_lua_globals( | ||||
|             register_command!(mud_log, "log"); | ||||
|             register_command!(panel_merge); | ||||
|             register_command!(sendmud_raw); | ||||
|             register_command!(tick); | ||||
|             register_stateless_command!(mud_trigger, "untrigger"); | ||||
|             register_stateless_command!(mud_untrigger, "unact"); | ||||
|             register_stateless_command!(mud_untrigger, "unaction"); | ||||
|             register_command!(untick, "undelay"); | ||||
|             register_command!(untick); | ||||
|             register_stateless_command!(mud_untrigger, "untrigger"); | ||||
|             register_command!(vsplit); | ||||
|             ctx.set_global("commands", cmd_table); | ||||
|  | ||||
| @ -2,6 +2,7 @@ use crate::{ | ||||
|     echo_to_term_frame, | ||||
|     id_intern::intern_id, | ||||
|     match_table::{create_match_table, match_table_add, match_table_remove}, | ||||
|     timer_host::TimerHostAccessContext, | ||||
|     GlobalLayoutCell, GlobalLayoutState, GlobalMemoCell, TermFrame, | ||||
| }; | ||||
| use gc_arena::{Gc, Rootable}; | ||||
| @ -439,3 +440,238 @@ pub(super) fn frame_input<'gc>(ctx: Context<'gc>, _global_memo: &GlobalMemoCell) | ||||
|         callback_return | ||||
|     }) | ||||
| } | ||||
| 
 | ||||
| fn list_timers<'gc>( | ||||
|     ctx: Context<'gc>, | ||||
|     global_memo: &GlobalMemoCell, | ||||
|     cur_frame_id: TermFrame, | ||||
| ) -> Result<(), Error<'gc>> { | ||||
|     let timer_val = match global_memo.frame_registry.borrow().get(&cur_frame_id) { | ||||
|         None => Err(anyhow::Error::msg("Frame no longer exists"))?, | ||||
|         Some(frame_dat) => Table::from_value(ctx, frame_dat.timer_host.to_value(ctx)?)?, | ||||
|     }; | ||||
|     if timer_val.iter().next().is_none() { | ||||
|         let _ = echo_to_term_frame( | ||||
|             global_memo, | ||||
|             &cur_frame_id, | ||||
|             "No timers are currently active.\r\n", | ||||
|         ); | ||||
|     } else { | ||||
|         let _ = echo_to_term_frame( | ||||
|             global_memo, | ||||
|             &cur_frame_id, | ||||
|             &timer_val | ||||
|                 .iter() | ||||
|                 .map(|(_k, v)| { | ||||
|                     let t = Table::from_value(ctx, v)?; | ||||
|                     Ok(format!( | ||||
|                         "{}: {} {} {{{}}}\r\n", | ||||
|                         t.get::<&str, piccolo::String>(ctx, "name")?.to_str()?, | ||||
|                         if t.get::<&str, bool>(ctx, "recurring")? { | ||||
|                             "tick" | ||||
|                         } else { | ||||
|                             "delay" | ||||
|                         }, | ||||
|                         t.get::<&str, i64>(ctx, "interval_millis")? as f64 / 1000.0, | ||||
|                         t.get::<&str, piccolo::String>(ctx, "commands")?.to_str()?, | ||||
|                     )) | ||||
|                 }) | ||||
|                 .collect::<Result<Vec<String>, Error<'gc>>>()? | ||||
|                 .iter() | ||||
|                 .join(""), | ||||
|         ); | ||||
|     } | ||||
|     Ok(()) | ||||
| } | ||||
| 
 | ||||
| struct GlobalCellTermFrameTimerHostAccess { | ||||
|     global_memo: GlobalMemoCell, | ||||
|     frame: TermFrame, | ||||
| } | ||||
| impl TimerHostAccessContext for GlobalCellTermFrameTimerHostAccess { | ||||
|     fn with_ref<F>(&self, f: F) | ||||
|     where | ||||
|         F: Fn(&mut crate::timer_host::TimerHost), | ||||
|     { | ||||
|         if let Some(frame_dat) = self | ||||
|             .global_memo | ||||
|             .frame_registry | ||||
|             .borrow_mut() | ||||
|             .get_mut(&self.frame) | ||||
|         { | ||||
|             f(&mut frame_dat.timer_host); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub(super) fn tick<'gc>( | ||||
|     ctx: Context<'gc>, | ||||
|     global_memo: &GlobalMemoCell, | ||||
|     _global_layout: &UseStateSetter<GlobalLayoutCell>, | ||||
| ) -> Callback<'gc> { | ||||
|     let global_memo = global_memo.clone(); | ||||
|     Callback::from_fn(&ctx, move |ctx, _ex, mut stack| { | ||||
|         let info: Table = ctx.get_global("info")?; | ||||
|         let cur_frame_id: TermFrame = | ||||
|             try_unwrap_frame(ctx, &info.get(ctx, ctx.intern_static(b"current_frame"))?)?; | ||||
| 
 | ||||
|         if stack.is_empty() { | ||||
|             list_timers(ctx, &global_memo, cur_frame_id)?; | ||||
|             return Ok(CallbackReturn::Return); | ||||
|         } | ||||
| 
 | ||||
|         let secs: f64 = f64::from_value( | ||||
|             ctx, | ||||
|             stack | ||||
|                 .pop_front() | ||||
|                 .ok_or_else(|| anyhow::Error::msg("Missing timer frequency"))?, | ||||
|         )?; | ||||
|         if secs < 0.001 { | ||||
|             Err(anyhow::Error::msg("Minimum timer duration is 0.001s"))?; | ||||
|         } | ||||
|         if secs >= (2.0_f64).powi(31) { | ||||
|             Err(anyhow::Error::msg("Timer duration is too long"))?; | ||||
|         } | ||||
|         let millis: u32 = (secs * 1000.0) as u32; | ||||
| 
 | ||||
|         let commands = piccolo::String::from_value( | ||||
|             ctx, | ||||
|             stack | ||||
|                 .pop_front() | ||||
|                 .ok_or_else(|| anyhow::Error::msg("Missing timer commands"))?, | ||||
|         )? | ||||
|         .to_str()?; | ||||
| 
 | ||||
|         let timer_name = match stack.len() { | ||||
|             0 => None, | ||||
|             1 => Some(stack.consume::<piccolo::String>(ctx)?.to_str()?.to_owned()), | ||||
|             _ => Err(anyhow::Error::msg( | ||||
|                 "Extra arguments given; expecting #tick duration {command} optional_name", | ||||
|             ))?, | ||||
|         }; | ||||
| 
 | ||||
|         let new_id = match global_memo | ||||
|             .frame_registry | ||||
|             .borrow_mut() | ||||
|             .get_mut(&cur_frame_id) | ||||
|         { | ||||
|             None => Err(anyhow::Error::msg("Frame no longer exists"))?, | ||||
|             Some(frame_dat) => frame_dat.timer_host.add_record( | ||||
|                 GlobalCellTermFrameTimerHostAccess { | ||||
|                     global_memo: global_memo.clone(), | ||||
|                     frame: cur_frame_id.clone(), | ||||
|                 }, | ||||
|                 &global_memo, | ||||
|                 true, | ||||
|                 millis, | ||||
|                 commands.to_owned(), | ||||
|                 timer_name, | ||||
|             )?, | ||||
|         }; | ||||
| 
 | ||||
|         let new_id = new_id.into_value(ctx); | ||||
|         ctx.get_global::<Table>("info")? | ||||
|             .set(ctx, "last_timer", new_id)?; | ||||
|         stack.push_back(new_id); | ||||
| 
 | ||||
|         Ok(piccolo::CallbackReturn::Return) | ||||
|     }) | ||||
| } | ||||
| 
 | ||||
| pub(super) fn delay<'gc>( | ||||
|     ctx: Context<'gc>, | ||||
|     global_memo: &GlobalMemoCell, | ||||
|     _global_layout: &UseStateSetter<GlobalLayoutCell>, | ||||
| ) -> Callback<'gc> { | ||||
|     let global_memo = global_memo.clone(); | ||||
|     Callback::from_fn(&ctx, move |ctx, _ex, mut stack| { | ||||
|         let info: Table = ctx.get_global("info")?; | ||||
|         let cur_frame_id: TermFrame = | ||||
|             try_unwrap_frame(ctx, &info.get(ctx, ctx.intern_static(b"current_frame"))?)?; | ||||
| 
 | ||||
|         if stack.is_empty() { | ||||
|             list_timers(ctx, &global_memo, cur_frame_id)?; | ||||
|             return Ok(CallbackReturn::Return); | ||||
|         } | ||||
| 
 | ||||
|         let secs: f64 = f64::from_value( | ||||
|             ctx, | ||||
|             stack | ||||
|                 .pop_front() | ||||
|                 .ok_or_else(|| anyhow::Error::msg("Missing timer frequency"))?, | ||||
|         )?; | ||||
|         if secs < 0.001 { | ||||
|             Err(anyhow::Error::msg("Minimum timer duration is 0.001s"))?; | ||||
|         } | ||||
|         if secs >= (2.0_f64).powi(31) { | ||||
|             Err(anyhow::Error::msg("Timer duration is too long"))?; | ||||
|         } | ||||
|         let millis: u32 = (secs * 1000.0) as u32; | ||||
| 
 | ||||
|         let commands = piccolo::String::from_value( | ||||
|             ctx, | ||||
|             stack | ||||
|                 .pop_front() | ||||
|                 .ok_or_else(|| anyhow::Error::msg("Missing timer commands"))?, | ||||
|         )? | ||||
|         .to_str()?; | ||||
| 
 | ||||
|         let timer_name = match stack.len() { | ||||
|             0 => None, | ||||
|             1 => Some(stack.consume::<piccolo::String>(ctx)?.to_str()?.to_owned()), | ||||
|             _ => Err(anyhow::Error::msg( | ||||
|                 "Extra arguments given; expecting #delay duration {command} optional_name", | ||||
|             ))?, | ||||
|         }; | ||||
| 
 | ||||
|         let new_id = match global_memo | ||||
|             .frame_registry | ||||
|             .borrow_mut() | ||||
|             .get_mut(&cur_frame_id) | ||||
|         { | ||||
|             None => Err(anyhow::Error::msg("Frame no longer exists"))?, | ||||
|             Some(frame_dat) => frame_dat.timer_host.add_record( | ||||
|                 GlobalCellTermFrameTimerHostAccess { | ||||
|                     global_memo: global_memo.clone(), | ||||
|                     frame: cur_frame_id.clone(), | ||||
|                 }, | ||||
|                 &global_memo, | ||||
|                 false, | ||||
|                 millis, | ||||
|                 commands.to_owned(), | ||||
|                 timer_name, | ||||
|             )?, | ||||
|         }; | ||||
| 
 | ||||
|         let new_id = new_id.into_value(ctx); | ||||
|         ctx.get_global::<Table>("info")? | ||||
|             .set(ctx, "last_timer", new_id)?; | ||||
|         stack.push_back(new_id); | ||||
| 
 | ||||
|         Ok(piccolo::CallbackReturn::Return) | ||||
|     }) | ||||
| } | ||||
| 
 | ||||
| pub(super) fn untick<'gc>( | ||||
|     ctx: Context<'gc>, | ||||
|     global_memo: &GlobalMemoCell, | ||||
|     _global_layout: &UseStateSetter<GlobalLayoutCell>, | ||||
| ) -> Callback<'gc> { | ||||
|     let global_memo = global_memo.clone(); | ||||
|     Callback::from_fn(&ctx, move |ctx, _ex, mut stack| { | ||||
|         let info: Table = ctx.get_global("info")?; | ||||
|         let cur_frame_id: TermFrame = | ||||
|             try_unwrap_frame(ctx, &info.get(ctx, ctx.intern_static(b"current_frame"))?)?; | ||||
|         let name = stack.consume::<piccolo::String>(ctx)?.to_str()?.to_owned(); | ||||
| 
 | ||||
|         match global_memo | ||||
|             .frame_registry | ||||
|             .borrow_mut() | ||||
|             .get_mut(&cur_frame_id) | ||||
|         { | ||||
|             None => Err(anyhow::Error::msg("Frame no longer exists"))?, | ||||
|             Some(frame_dat) => frame_dat.timer_host.remove_timer(name)?, | ||||
|         }; | ||||
|         Ok(piccolo::CallbackReturn::Return) | ||||
|     }) | ||||
| } | ||||
|  | ||||
| @ -18,6 +18,7 @@ pub mod split_panel; | ||||
| pub mod telnet; | ||||
| pub mod term_split; | ||||
| pub mod term_view; | ||||
| pub mod timer_host; | ||||
| pub mod websocket; | ||||
| use crate::lua_engine::{install_lua_globals, LuaState}; | ||||
| use crate::split_panel::*; | ||||
|  | ||||
| @ -9,6 +9,7 @@ use crate::{ | ||||
|     command_handler::command_handler, | ||||
|     lineengine::line::{Readline, ReadlineEvent}, | ||||
|     term_split::TermSplit, | ||||
|     timer_host::TimerHost, | ||||
|     GlobalLayoutCell, GlobalMemoCell, PanelDirection, SplitPanel, | ||||
| }; | ||||
| 
 | ||||
| @ -77,6 +78,7 @@ pub struct TermFrameData { | ||||
|     pub fit: FitAddon, | ||||
|     pub node: Node, | ||||
|     pub readline: Readline, | ||||
|     pub timer_host: TimerHost, | ||||
|     pub retained_closures: Option<( | ||||
|         Closure<dyn FnMut(String)>, | ||||
|         Closure<dyn FnMut(Dims)>, | ||||
| @ -136,6 +138,7 @@ fn get_or_make_term_frame<'a>( | ||||
|                 }), | ||||
|                 initial_size, | ||||
|             ), | ||||
|             timer_host: TimerHost::new(frame.clone()), | ||||
|             retained_closures: None, | ||||
|         }; | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										173
									
								
								src/timer_host.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								src/timer_host.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,173 @@ | ||||
| use anyhow::bail; | ||||
| use piccolo::{Context, IntoValue, Table, Value}; | ||||
| use wasm_bindgen::{closure::Closure, JsCast}; | ||||
| 
 | ||||
| use crate::{ | ||||
|     command_handler::execute_queue, | ||||
|     parsing::{parse_commands, ParsedCommand}, | ||||
|     GlobalMemoCell, TermFrame, | ||||
| }; | ||||
| 
 | ||||
| pub struct TimerHost { | ||||
|     timers: Vec<TimerRecord>, | ||||
|     frame_id: TermFrame, | ||||
|     next_id: u64, | ||||
| } | ||||
| 
 | ||||
| impl Drop for TimerHost { | ||||
|     fn drop(&mut self) { | ||||
|         if let Some(window) = web_sys::window() { | ||||
|             for timer in &self.timers { | ||||
|                 if timer.recurring { | ||||
|                     window.clear_interval_with_handle(timer.js_id); | ||||
|                 } else { | ||||
|                     window.clear_timeout_with_handle(timer.js_id); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl TimerHost { | ||||
|     pub fn new(frame_id: TermFrame) -> Self { | ||||
|         Self { | ||||
|             timers: vec![], | ||||
|             frame_id, | ||||
|             next_id: 1, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     pub fn add_record<AC>( | ||||
|         &mut self, | ||||
|         access_context: AC, | ||||
|         global: &GlobalMemoCell, | ||||
|         recurring: bool, | ||||
|         interval_millis: u32, | ||||
|         commands: String, | ||||
|         timer_name: Option<String>, | ||||
|     ) -> anyhow::Result<String> | ||||
|     where | ||||
|         AC: TimerHostAccessContext + 'static, | ||||
|     { | ||||
|         let window = web_sys::window().ok_or_else(|| anyhow::Error::msg("Can't fetch window"))?; | ||||
|         let new_id = self.next_id; | ||||
|         self.next_id += 1; | ||||
|         let global = global.clone(); | ||||
| 
 | ||||
|         if timer_name | ||||
|             .as_ref() | ||||
|             .map(|tn| tn.starts_with('#')) | ||||
|             .unwrap_or(false) | ||||
|         { | ||||
|             bail!( | ||||
|                 "Timer name can't start with #, it is reserved for referencing timers by number." | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         let frame = self.frame_id.clone(); | ||||
|         let callback_commands: Vec<ParsedCommand> = parse_commands(&commands).commands; | ||||
|         let callback: Closure<dyn FnMut()> = Closure::new(move || { | ||||
|             let mut cq = global.command_queue.borrow_mut(); | ||||
|             for cmd in &callback_commands { | ||||
|                 cq.push_back((frame.clone(), cmd.clone())); | ||||
|             } | ||||
|             drop(cq); | ||||
| 
 | ||||
|             execute_queue(&global); | ||||
| 
 | ||||
|             if !recurring { | ||||
|                 access_context.with_ref(|timer_host| { | ||||
|                     timer_host.timers.retain(|t| t.id != new_id); | ||||
|                 }); | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         let js_id = if recurring { | ||||
|             window | ||||
|                 .set_interval_with_callback_and_timeout_and_arguments_0( | ||||
|                     callback.as_ref().unchecked_ref(), | ||||
|                     interval_millis as i32, | ||||
|                 ) | ||||
|                 .map_err(|_| anyhow::Error::msg("setInterval failed"))? | ||||
|         } else { | ||||
|             window | ||||
|                 .set_timeout_with_callback_and_timeout_and_arguments_0( | ||||
|                     callback.as_ref().unchecked_ref(), | ||||
|                     interval_millis as i32, | ||||
|                 ) | ||||
|                 .map_err(|_| anyhow::Error::msg("setTimeout failed"))? | ||||
|         }; | ||||
| 
 | ||||
|         self.timers.push(TimerRecord { | ||||
|             id: new_id, | ||||
|             js_id, | ||||
|             timer_name, | ||||
|             recurring, | ||||
|             interval_millis, | ||||
|             commands, | ||||
|             retained_closure: callback, | ||||
|         }); | ||||
| 
 | ||||
|         Ok(format!("#{}", new_id)) | ||||
|     } | ||||
| 
 | ||||
|     pub fn remove_timer(&mut self, timer_name: String) -> anyhow::Result<()> { | ||||
|         let timer = if let Some(suffix) = timer_name.strip_prefix('#') { | ||||
|             let id = suffix | ||||
|                 .parse::<u64>() | ||||
|                 .map_err(|_| anyhow::Error::msg("Should follow # with a number"))?; | ||||
|             self.timers.iter().enumerate().find(|t| t.1.id == id) | ||||
|         } else { | ||||
|             self.timers | ||||
|                 .iter() | ||||
|                 .enumerate() | ||||
|                 .find(|t| t.1.timer_name.as_ref() == Some(&timer_name)) | ||||
|         }; | ||||
|         let timer = timer.ok_or_else(|| anyhow::Error::msg("No matching timer found."))?; | ||||
|         let window = web_sys::window().ok_or_else(|| anyhow::Error::msg("Can't fetch window"))?; | ||||
|         if timer.1.recurring { | ||||
|             window.clear_interval_with_handle(timer.1.js_id); | ||||
|         } else { | ||||
|             window.clear_timeout_with_handle(timer.1.js_id); | ||||
|         } | ||||
|         self.timers.remove(timer.0); | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     pub fn to_value<'gc>(&self, ctx: Context<'gc>) -> anyhow::Result<Value<'gc>> { | ||||
|         let tbl = Table::new(&ctx); | ||||
|         for timer in self.timers.iter() { | ||||
|             let timer_tbl = Table::new(&ctx); | ||||
|             timer_tbl.set( | ||||
|                 ctx, | ||||
|                 "name", | ||||
|                 timer | ||||
|                     .timer_name | ||||
|                     .clone() | ||||
|                     .unwrap_or_else(|| format!("#{}", timer.id)), | ||||
|             )?; | ||||
|             timer_tbl.set(ctx, "commands", timer.commands.clone())?; | ||||
|             timer_tbl.set(ctx, "recurring", timer.recurring)?; | ||||
|             timer_tbl.set(ctx, "interval_millis", timer.interval_millis)?; | ||||
|             tbl.set(ctx, timer.id as i64, timer_tbl)?; | ||||
|         } | ||||
|         Ok(tbl.into_value(ctx)) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub trait TimerHostAccessContext { | ||||
|     fn with_ref<F>(&self, f: F) | ||||
|     where | ||||
|         F: Fn(&mut TimerHost); | ||||
| } | ||||
| 
 | ||||
| pub struct TimerRecord { | ||||
|     id: u64, | ||||
|     js_id: i32, | ||||
|     timer_name: Option<String>, | ||||
|     recurring: bool, | ||||
|     interval_millis: u32, | ||||
|     commands: String, | ||||
|     #[allow(dead_code)] | ||||
|     retained_closure: Closure<dyn FnMut()>, | ||||
| } | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user