diff --git a/src/command_handler.rs b/src/command_handler.rs index ecbd3e3..60fea0d 100644 --- a/src/command_handler.rs +++ b/src/command_handler.rs @@ -1,7 +1,12 @@ use crate::{ - echo_to_term_frame, lua_engine::LuaState, parsing::parse_commands, GlobalMemoCell, TermFrame, + echo_to_term_frame, + lua_engine::LuaState, + parsing::{parse_commands, ParsedCommand}, + GlobalMemoCell, TermFrame, }; -use itertools::{join, Itertools}; +use itertools::Itertools; +use wasm_bindgen::JsValue; +use web_sys::console; pub fn debrace(inp: &str) -> &str { let v = inp.trim(); @@ -16,11 +21,10 @@ fn reentrant_command_handler( lua_state: &mut LuaState, globals: &GlobalMemoCell, term_frame: &TermFrame, - command_in: &str, + commands_in: &[ParsedCommand], ) { - echo_to_term_frame(globals, term_frame, "\r").unwrap_or(()); lua_state.set_current_frame(term_frame); - for command in parse_commands(command_in).commands { + for command in commands_in { match command.split_out_command() { None => (), Some((cmd, rest)) => { @@ -34,13 +38,9 @@ fn reentrant_command_handler( } } } else if let Ok(repeat_count) = command_rest.parse::() { + let cmds = &[rest]; for _ in 0..repeat_count { - reentrant_command_handler( - lua_state, - globals, - term_frame, - &join(rest.arguments.iter(), " "), - ); + reentrant_command_handler(lua_state, globals, term_frame, cmds); } } else { match lua_state.execute_command(command_rest, &rest) { @@ -69,15 +69,53 @@ fn reentrant_command_handler( } pub fn command_handler(globals: &GlobalMemoCell, term_frame: &TermFrame, command_in: &str) { - match globals.lua_engine.try_borrow_mut() { - Err(_) => echo_to_term_frame( - globals, - term_frame, - "Attempt to re-enter command handler during processing.\r\n", - ) - .unwrap_or(()), // Ignore error handling error. - Ok(mut lua_state_m) => { - reentrant_command_handler(&mut lua_state_m, globals, term_frame, command_in) + echo_to_term_frame(globals, term_frame, "\r").unwrap_or(()); + { + let mut cq = globals.command_queue.borrow_mut(); + for cmd in parse_commands(command_in).commands { + cq.push_back((term_frame.clone(), cmd)); + } + } + execute_queue(globals); +} + +pub fn execute_queue(globals: &GlobalMemoCell) { + let mut steps: u64 = 0; + const STEP_LIMIT: u64 = 500; + loop { + let queue_head = globals.command_queue.borrow_mut().pop_front(); + match queue_head { + None => return, + Some((frame, command_in)) => { + steps += 1; + match globals.lua_engine.try_borrow_mut() { + Err(_) => console::log_1(&JsValue::from_str( + "Can't borrow lua_engine when executing queue!", + )), + Ok(mut lua_state_m) => { + reentrant_command_handler( + &mut lua_state_m, + globals, + &frame, + &[command_in.clone()], + ); + if steps > STEP_LIMIT { + let new_queue = globals.command_queue.take(); + if !new_queue.is_empty() { + echo_to_term_frame( + globals, + &frame, + &format!("Executing queued actions resulted in more than {} steps. This usually means a command is creating similar commands in a loop. The following commands were dropped from the queue to stop execution: {}.", + STEP_LIMIT, + &new_queue.iter().map( + |(_fr, cmd)| cmd.to_string()) + .join("; "))).unwrap_or(()); + return; + } + } + } + } + } } } } diff --git a/src/lua_engine.rs b/src/lua_engine.rs index aeb522d..c287623 100644 --- a/src/lua_engine.rs +++ b/src/lua_engine.rs @@ -10,7 +10,13 @@ use piccolo::{ use yew::UseStateSetter; use crate::{ - id_intern::intern_id, parsing::ParsedCommand, GlobalLayoutCell, GlobalMemoCell, TermFrame, + id_intern::intern_id, + match_table::{ + create_match_table, match_table_add, match_table_lua_table, match_table_remove, + match_table_try_run_sub, + }, + parsing::ParsedCommand, + GlobalLayoutCell, GlobalMemoCell, TermFrame, }; pub struct LuaState { @@ -145,6 +151,7 @@ pub fn install_lua_globals( register_command!(alias); register_command!(close_mud); register_command!(connect_mud); + register_command!(create_match_table); register_command!(delete_mud); register_command!(echo); register_command!(echo_frame); @@ -244,6 +251,18 @@ pub fn install_lua_globals( register_class_function!(frameroute_class_table, "new", new_frameroute); register_class_function!(frameroute_class_table, "route", frameroute_route); + let match_table_class_table = Table::new(&ctx); + classes_table.set(ctx, "match_table", match_table_class_table)?; + match_table_class_table.set(ctx, MetaMethod::Index, match_table_class_table)?; + register_class_function!(match_table_class_table, "add", match_table_add); + register_class_function!(match_table_class_table, "remove", match_table_remove); + register_class_function!(match_table_class_table, "lua_table", match_table_lua_table); + register_class_function!( + match_table_class_table, + "try_run_sub", + match_table_try_run_sub + ); + Ok(()) }) .map_err(|e| e.to_string())?; diff --git a/src/lua_engine/muds.rs b/src/lua_engine/muds.rs index 1e99333..c8b06bd 100644 --- a/src/lua_engine/muds.rs +++ b/src/lua_engine/muds.rs @@ -9,6 +9,7 @@ use web_sys::console; use yew::UseStateSetter; use crate::{ + command_handler::execute_queue, id_intern::{intern_id, unintern_id}, telnet::{parse_telnet_buf, TelnetOutput}, websocket::{connect_websocket, send_message_to_mud, WebSocketId}, @@ -65,7 +66,12 @@ pub fn handle_websocket_output( .map_err(|err| format!("{}", err)) } -pub fn handle_websocket_output_log_err(socket: &WebSocketId, engine: &mut LuaState, input: &[u8]) { +pub fn handle_websocket_output_log_err( + socket: &WebSocketId, + globals: &GlobalMemoCell, + engine: &mut LuaState, + input: &[u8], +) { match handle_websocket_output(socket, engine, input) { Ok(()) => {} Err(e) => console::log_2( @@ -73,6 +79,7 @@ pub fn handle_websocket_output_log_err(socket: &WebSocketId, engine: &mut LuaSta &JsValue::from_str(&e), ), } + execute_queue(globals); } pub fn handle_websocket_close(socket: &WebSocketId, engine: &mut LuaState) -> Result<(), String> { @@ -95,7 +102,11 @@ pub fn handle_websocket_close(socket: &WebSocketId, engine: &mut LuaState) -> Re .map_err(|err| format!("{}", err)) } -pub fn handle_websocket_close_log_err(socket: &WebSocketId, engine: &mut LuaState) { +pub fn handle_websocket_close_log_err( + socket: &WebSocketId, + globals: &GlobalMemoCell, + engine: &mut LuaState, +) { match handle_websocket_close(socket, engine) { Ok(()) => {} Err(e) => console::log_2( @@ -103,6 +114,7 @@ pub fn handle_websocket_close_log_err(socket: &WebSocketId, engine: &mut LuaStat &JsValue::from_str(&e), ), } + execute_queue(globals); } pub(super) fn sendmud_raw<'gc>( diff --git a/src/main.rs b/src/main.rs index fe41211..099e7a3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,8 @@ use std::cell::RefCell; +use std::collections::VecDeque; use std::rc::Rc; +use parsing::ParsedCommand; use term_split::TermSplit; use yew::prelude::*; @@ -26,6 +28,7 @@ pub struct GlobalMemoState { frame_registry: RefCell, lua_engine: RefCell, ws_registry: RefCell, + command_queue: RefCell>, // A cache of the latest layout info (separate from the state). // Updating this doesn't force a relayout, so only update the cache when @@ -66,6 +69,7 @@ fn app() -> Html { let global_memo = use_memo((), |_| GlobalMemoState { frame_registry: RegisteredTermFrames::new().into(), ws_registry: RegisteredWebSockets::new().into(), + command_queue: VecDeque::new().into(), lua_engine: LuaState::setup().expect("Can create interpreter").into(), layout: RefCell::new((*global_layout).clone()), }); diff --git a/src/match_table.rs b/src/match_table.rs index 65db453..bbed430 100644 --- a/src/match_table.rs +++ b/src/match_table.rs @@ -1,13 +1,23 @@ -use std::{collections::VecDeque, str::FromStr}; +use std::{ + collections::{BTreeSet, VecDeque}, + str::FromStr, +}; use anyhow::bail; +use gc_arena::{Collect, GcRefLock, Rootable}; use itertools::Itertools; -use piccolo::{Context, IntoValue, Table, Value}; +use piccolo::{Callback, Context, IntoValue, Table, UserData, Value}; use regex::Regex; +use yew::UseStateSetter; -use crate::parsing::{parse_commands, quote_string, ArgumentGuard, ParsedArgument, ParsedCommand}; +use crate::{ + lua_engine::frames::try_unwrap_frame, + parsing::{parse_commands, quote_string, ArgumentGuard, ParsedArgument, ParsedCommand}, + GlobalLayoutCell, GlobalMemoCell, +}; -#[derive(Default, Debug)] +#[derive(Default, Debug, Collect)] +#[collect(require_static)] pub struct MatchSubTable { contents: Vec, } @@ -74,6 +84,7 @@ impl MatchSubTable { } pub fn add_record(&mut self, match_text: &str, sub_text: &str) -> anyhow::Result<()> { + self.remove_record(match_text).unwrap_or(()); let rex = Regex::new(match_text)?; let parse_result = parse_commands(sub_text); @@ -91,6 +102,27 @@ impl MatchSubTable { }) .collect::>>()?; + let vars: BTreeSet = sub_commands + .iter() + .flat_map(|sc| { + sc.arguments.iter().flat_map(|arg| { + arg.text_parts.iter().filter_map(|tp| match tp { + SubTextPart::Variable(v) => Some(v.clone()), + _ => None, + }) + }) + }) + .collect(); + let max_captures = rex.captures_len(); + let valid_vars: BTreeSet = (0..max_captures) + .map(|n| n.to_string()) + .chain(rex.capture_names().filter_map(|o| o.map(|n| n.to_string()))) + .collect(); + let invalid_vars: Vec = vars.difference(&valid_vars).cloned().collect(); + if !invalid_vars.is_empty() { + bail!("Invalid variables in substitution: {:?}", invalid_vars); + } + self.contents.push(MatchRecord { match_text: match_text.to_owned(), match_regex: rex, @@ -99,6 +131,19 @@ impl MatchSubTable { }); Ok(()) } + + pub fn remove_record(&mut self, match_text: &str) -> anyhow::Result<()> { + match self + .contents + .iter() + .enumerate() + .find(|(_idx, rec)| rec.match_text == match_text) + { + None => bail!("No matching record found."), + Some((idx, _)) => self.contents.remove(idx), + }; + Ok(()) + } } fn parsedarg_to_subarg(parsedarg: ParsedArgument) -> anyhow::Result { @@ -398,4 +443,106 @@ mod tests { ); assert_eq!(parse_commands(&ser_result).commands, vec![expected]); } + + #[test] + fn matchsubtable_rejects_invalid() { + let mut table: MatchSubTable = Default::default(); + assert!(table + .add_record("^foo (?[a-z]+) baz", "$wrong") + .is_err()) + } +} + +pub fn create_match_table<'gc, 'a>( + ctx: Context<'gc>, + _global_memo: &'a GlobalMemoCell, + _global_layout: &'a UseStateSetter, +) -> Callback<'gc> { + Callback::from_fn(&ctx, move |ctx, _ex, mut stack| { + let _: () = stack.consume(ctx)?; + let user_data = UserData::<'gc>::new:: GcRefLock<'gcb, MatchSubTable>]>( + &ctx, + GcRefLock::new(&ctx, ::default().into()), + ); + let match_table_class: Table = ctx + .get_global::("classes")? + .get(ctx, "match_table")?; + user_data.set_metatable(&ctx, Some(match_table_class)); + + stack.push_back(user_data.into_value(ctx)); + Ok(piccolo::CallbackReturn::Return) + }) +} + +pub fn match_table_add<'gc, 'a>( + ctx: Context<'gc>, + _global_memo: &'a GlobalMemoCell, +) -> Callback<'gc> { + Callback::from_fn(&ctx, move |ctx, _ex, mut stack| { + let (match_table, match_text, sub_text): (UserData, piccolo::String, piccolo::String) = + stack.consume(ctx)?; + match_table + .downcast:: GcRefLock<'gcb, MatchSubTable>]>()? + .borrow_mut(&ctx) + .add_record(match_text.to_str()?, sub_text.to_str()?)?; + Ok(piccolo::CallbackReturn::Return) + }) +} + +pub fn match_table_remove<'gc, 'a>( + ctx: Context<'gc>, + _global_memo: &'a GlobalMemoCell, +) -> Callback<'gc> { + Callback::from_fn(&ctx, move |ctx, _ex, mut stack| { + let (match_table, match_text): (UserData, piccolo::String) = stack.consume(ctx)?; + match_table + .downcast:: GcRefLock<'gcb, MatchSubTable>]>()? + .borrow_mut(&ctx) + .remove_record(match_text.to_str()?)?; + Ok(piccolo::CallbackReturn::Return) + }) +} + +pub fn match_table_lua_table<'gc, 'a>( + ctx: Context<'gc>, + _global_memo: &'a GlobalMemoCell, +) -> Callback<'gc> { + Callback::from_fn(&ctx, move |ctx, _ex, mut stack| { + let match_table: UserData = stack.consume(ctx)?; + stack.push_back( + match_table + .downcast:: GcRefLock<'gcb, MatchSubTable>]>()? + .borrow_mut(&ctx) + .to_value(ctx)?, + ); + Ok(piccolo::CallbackReturn::Return) + }) +} + +pub fn match_table_try_run_sub<'gc, 'a>( + ctx: Context<'gc>, + global_memo: &'a GlobalMemoCell, +) -> Callback<'gc> { + let global_memo = global_memo.clone(); + Callback::from_fn(&ctx, move |ctx, _ex, mut stack| { + let (match_table, sub, frame): (UserData, piccolo::String, Value) = stack.consume(ctx)?; + let frame = try_unwrap_frame(ctx, &frame)?; + + let cmds = match_table + .downcast:: GcRefLock<'gcb, MatchSubTable>]>()? + .borrow() + .try_sub(sub.to_str()?); + + match cmds { + None => stack.push_back(false.into_value(ctx)), + Some(cmds) => { + let mut cq = global_memo.command_queue.borrow_mut(); + for cmd in cmds.into_iter().rev() { + cq.push_front((frame.clone(), cmd)); + } + stack.push_back(Value::Boolean(true)) + } + } + Ok(piccolo::CallbackReturn::Return) + }) } diff --git a/src/websocket.rs b/src/websocket.rs index f7a4576..0672878 100644 --- a/src/websocket.rs +++ b/src/websocket.rs @@ -65,6 +65,7 @@ pub fn connect_websocket( if data.has_type::() { handle_websocket_output_log_err( &data_new_id, + &data_globals, &mut data_globals.lua_engine.borrow_mut(), &Uint8Array::new(&data).to_vec(), ); @@ -100,6 +101,7 @@ pub fn connect_websocket( closed_socket.retained_closures = None; handle_websocket_close_log_err( &close_id, + &close_globals, &mut close_globals.lua_engine.borrow_mut(), ); }