From 56edc3d484448244cad35cc1428a69f0ef6a533d Mon Sep 17 00:00:00 2001 From: Condorra Date: Tue, 1 Oct 2024 23:02:26 +1000 Subject: [PATCH] Add tick / delay / untick timer commands. --- src/lua_engine.rs | 4 + src/lua_engine/frames.rs | 236 +++++++++++++++++++++++++++++++++++++++ src/main.rs | 1 + src/term_view.rs | 3 + src/timer_host.rs | 173 ++++++++++++++++++++++++++++ 5 files changed, 417 insertions(+) create mode 100644 src/timer_host.rs diff --git a/src/lua_engine.rs b/src/lua_engine.rs index a876a7c..8921975 100644 --- a/src/lua_engine.rs +++ b/src/lua_engine.rs @@ -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); diff --git a/src/lua_engine/frames.rs b/src/lua_engine/frames.rs index ebd698c..2b78dc1 100644 --- a/src/lua_engine/frames.rs +++ b/src/lua_engine/frames.rs @@ -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::, Error<'gc>>>()? + .iter() + .join(""), + ); + } + Ok(()) +} + +struct GlobalCellTermFrameTimerHostAccess { + global_memo: GlobalMemoCell, + frame: TermFrame, +} +impl TimerHostAccessContext for GlobalCellTermFrameTimerHostAccess { + fn with_ref(&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, +) -> 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::(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::("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, +) -> 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::(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::
("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, +) -> 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::(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) + }) +} diff --git a/src/main.rs b/src/main.rs index 890c26c..f942b33 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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::*; diff --git a/src/term_view.rs b/src/term_view.rs index 1dfb183..d1fda86 100644 --- a/src/term_view.rs +++ b/src/term_view.rs @@ -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, Closure, @@ -136,6 +138,7 @@ fn get_or_make_term_frame<'a>( }), initial_size, ), + timer_host: TimerHost::new(frame.clone()), retained_closures: None, }; diff --git a/src/timer_host.rs b/src/timer_host.rs new file mode 100644 index 0000000..4004773 --- /dev/null +++ b/src/timer_host.rs @@ -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, + 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( + &mut self, + access_context: AC, + global: &GlobalMemoCell, + recurring: bool, + interval_millis: u32, + commands: String, + timer_name: Option, + ) -> anyhow::Result + 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 = parse_commands(&commands).commands; + let callback: Closure = 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::() + .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> { + 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(&self, f: F) + where + F: Fn(&mut TimerHost); +} + +pub struct TimerRecord { + id: u64, + js_id: i32, + timer_name: Option, + recurring: bool, + interval_millis: u32, + commands: String, + #[allow(dead_code)] + retained_closure: Closure, +}