Add tick / delay / untick timer commands.

This commit is contained in:
Condorra 2024-10-01 23:02:26 +10:00
parent cfe2e6cd6d
commit 56edc3d484
5 changed files with 417 additions and 0 deletions

View File

@ -181,6 +181,7 @@ pub fn install_lua_globals(
register_command!(connect_mud); register_command!(connect_mud);
register_stateless_command!(create_match_table); register_stateless_command!(create_match_table);
register_command!(cmd_delete_logs, "deletelogs"); register_command!(cmd_delete_logs, "deletelogs");
register_command!(delay);
register_command!(delete_mud); register_command!(delete_mud);
register_command!(cmd_download_logs, "downloadlogs"); register_command!(cmd_download_logs, "downloadlogs");
register_command!(echo); register_command!(echo);
@ -191,9 +192,12 @@ pub fn install_lua_globals(
register_command!(mud_log, "log"); register_command!(mud_log, "log");
register_command!(panel_merge); register_command!(panel_merge);
register_command!(sendmud_raw); register_command!(sendmud_raw);
register_command!(tick);
register_stateless_command!(mud_trigger, "untrigger"); register_stateless_command!(mud_trigger, "untrigger");
register_stateless_command!(mud_untrigger, "unact"); register_stateless_command!(mud_untrigger, "unact");
register_stateless_command!(mud_untrigger, "unaction"); register_stateless_command!(mud_untrigger, "unaction");
register_command!(untick, "undelay");
register_command!(untick);
register_stateless_command!(mud_untrigger, "untrigger"); register_stateless_command!(mud_untrigger, "untrigger");
register_command!(vsplit); register_command!(vsplit);
ctx.set_global("commands", cmd_table); ctx.set_global("commands", cmd_table);

View File

@ -2,6 +2,7 @@ use crate::{
echo_to_term_frame, echo_to_term_frame,
id_intern::intern_id, id_intern::intern_id,
match_table::{create_match_table, match_table_add, match_table_remove}, match_table::{create_match_table, match_table_add, match_table_remove},
timer_host::TimerHostAccessContext,
GlobalLayoutCell, GlobalLayoutState, GlobalMemoCell, TermFrame, GlobalLayoutCell, GlobalLayoutState, GlobalMemoCell, TermFrame,
}; };
use gc_arena::{Gc, Rootable}; use gc_arena::{Gc, Rootable};
@ -439,3 +440,238 @@ pub(super) fn frame_input<'gc>(ctx: Context<'gc>, _global_memo: &GlobalMemoCell)
callback_return 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)
})
}

View File

@ -18,6 +18,7 @@ pub mod split_panel;
pub mod telnet; pub mod telnet;
pub mod term_split; pub mod term_split;
pub mod term_view; pub mod term_view;
pub mod timer_host;
pub mod websocket; pub mod websocket;
use crate::lua_engine::{install_lua_globals, LuaState}; use crate::lua_engine::{install_lua_globals, LuaState};
use crate::split_panel::*; use crate::split_panel::*;

View File

@ -9,6 +9,7 @@ use crate::{
command_handler::command_handler, command_handler::command_handler,
lineengine::line::{Readline, ReadlineEvent}, lineengine::line::{Readline, ReadlineEvent},
term_split::TermSplit, term_split::TermSplit,
timer_host::TimerHost,
GlobalLayoutCell, GlobalMemoCell, PanelDirection, SplitPanel, GlobalLayoutCell, GlobalMemoCell, PanelDirection, SplitPanel,
}; };
@ -77,6 +78,7 @@ pub struct TermFrameData {
pub fit: FitAddon, pub fit: FitAddon,
pub node: Node, pub node: Node,
pub readline: Readline, pub readline: Readline,
pub timer_host: TimerHost,
pub retained_closures: Option<( pub retained_closures: Option<(
Closure<dyn FnMut(String)>, Closure<dyn FnMut(String)>,
Closure<dyn FnMut(Dims)>, Closure<dyn FnMut(Dims)>,
@ -136,6 +138,7 @@ fn get_or_make_term_frame<'a>(
}), }),
initial_size, initial_size,
), ),
timer_host: TimerHost::new(frame.clone()),
retained_closures: None, retained_closures: None,
}; };

173
src/timer_host.rs Normal file
View 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()>,
}