use piccolo::{Context, Table, Value}; use serde::{Deserialize, Serialize}; use wasm_bindgen::JsValue; use web_sys::console; use crate::{ telnet::{DO, DONT, ENDSUB, IAC, STARTSUB, WILL, WONT}, websocket::{send_message_to_mud, WebSocketId}, GlobalMemoCell, }; // These enums use the terminology of RFC1143. #[derive(Serialize, Deserialize, Eq, PartialEq, Debug)] pub enum OptionState { No, WantNo, WantNoOpposite, WantYes, WantYesOpposite, Yes, } #[derive(Eq, PartialEq)] pub enum Side { Us, Him, } #[derive(Eq, PartialEq, Clone)] pub struct Telopt(pub u8); #[derive(Clone)] pub struct MudWithMemo { pub memo: GlobalMemoCell, pub mud: WebSocketId, } pub trait SendRaw { fn send_bytes(&mut self, msg: &[u8]); } impl SendRaw for MudWithMemo { fn send_bytes(&mut self, msg: &[u8]) { let _ = send_message_to_mud(&self.mud, msg, &self.memo); } } pub trait SendOptNeg { fn send_will(&mut self, opt: &Telopt); fn send_wont(&mut self, opt: &Telopt); fn send_do(&mut self, opt: &Telopt); fn send_dont(&mut self, opt: &Telopt); } impl SendOptNeg for T { fn send_will(&mut self, opt: &Telopt) { self.send_bytes(&[IAC, WILL, opt.0]); } fn send_wont(&mut self, opt: &Telopt) { self.send_bytes(&[IAC, WONT, opt.0]); } fn send_do(&mut self, opt: &Telopt) { self.send_bytes(&[IAC, DO, opt.0]); } fn send_dont(&mut self, opt: &Telopt) { self.send_bytes(&[IAC, DONT, opt.0]); } } pub struct OptionWithActiveSide { option_active_on: Side, option: Telopt, } fn send_positive(mud: &mut T, opt: &OptionWithActiveSide) { match opt.option_active_on { Side::Him => mud.send_do(&opt.option), Side::Us => mud.send_will(&opt.option), } } fn send_negative(mud: &mut T, opt: &OptionWithActiveSide) { match opt.option_active_on { Side::Him => mud.send_dont(&opt.option), Side::Us => mud.send_wont(&opt.option), } } pub fn get_option_supported<'gc>( ctx: Context<'gc>, input: &Table<'gc>, opt: &Telopt, side: &Side, ) -> bool { let support_table: Table<'gc> = match input.get( ctx, match side { Side::Us => "suppported_options_local", Side::Him => "supported_options_remote", }, ) { Err(_) => return false, Ok(v) => v, }; for (_k, v) in support_table.iter() { if v.to_integer() == Some(opt.0 as i64) { return true; } } false } pub fn set_option_supported<'gc>(ctx: Context<'gc>, input: &Table<'gc>, opt: &Telopt, side: &Side) { let table_name = match side { Side::Us => "suppported_options_local", Side::Him => "supported_options_remote", }; let support_table: Table<'gc> = match input.get(ctx, table_name) { Err(_) => { let t = Table::new(&ctx); let _ = input.set(ctx, table_name, t); t } Ok(v) => v, }; for (_k, v) in support_table.iter() { if v.to_integer() == Some(opt.0 as i64) { return; } } let _ = support_table.set(ctx, opt.0, opt.0); } pub fn set_option_unsupported<'gc>( ctx: Context<'gc>, input: &Table<'gc>, opt: &Telopt, side: &Side, ) { let table_name = match side { Side::Us => "suppported_options_local", Side::Him => "supported_options_remote", }; let support_table: Table<'gc> = match input.get(ctx, table_name) { Err(_) => { let t = Table::new(&ctx); let _ = input.set(ctx, table_name, t); t } Ok(v) => v, }; for (k, v) in support_table.iter() { if v.to_integer() == Some(opt.0 as i64) { let _ = support_table.set(ctx, k, Value::Nil); } } } pub fn get_option_state<'gc>( ctx: Context<'gc>, input: &Table<'gc>, opt: &Telopt, side: &Side, ) -> OptionState { let state_table: Table<'gc> = match input.get( ctx, match side { Side::Us => "negotiation_state_local", Side::Him => "negotiation_state_remote", }, ) { Err(_) => return OptionState::No, Ok(v) => v, }; let v: String = match state_table.get(ctx, opt.0) { Err(_) => return OptionState::No, Ok(v) => v, }; match serde_json::from_value(serde_json::Value::String(v)) { Err(_) => OptionState::No, Ok(v) => v, } } pub fn send_subnegotiation_if_allowed<'gc, T: SendRaw>( ctx: Context<'gc>, mud_table: &Table<'gc>, opt: &Telopt, side: &Side, mud: &mut T, msg: &[u8], ) { if get_option_state(ctx, mud_table, opt, side) == OptionState::Yes { let mut buf: Vec = Vec::with_capacity(msg.len() + 4); buf.extend_from_slice(&[IAC, STARTSUB]); for c in msg { if *c == IAC { buf.extend_from_slice(b"\xff\xff"); } else { buf.push(*c); } } buf.extend_from_slice(&[IAC, ENDSUB]); mud.send_bytes(&buf); } } pub fn set_option_state<'gc>( ctx: Context<'gc>, input: &Table<'gc>, opt: &Telopt, side: &Side, state: &OptionState, ) { let table_name = match side { Side::Us => "negotiation_state_local", Side::Him => "negotiation_state_remote", }; let state_table: Table<'gc> = match input.get(ctx, table_name) { Err(_) => { let t = Table::new(&ctx); let _ = input.set(ctx, table_name, t); t } Ok(v) => v, }; let json_val = serde_json::to_value(state).expect("couldn't serialize state"); let _ = state_table.set( ctx, opt.0, json_val.as_str().expect("state wasn't a string").to_owned(), ); } fn handle_incoming_positive<'gc, T: SendOptNeg>( ctx: Context<'gc>, mud: &mut T, table: &Table<'gc>, opt: &OptionWithActiveSide, ) -> bool { match get_option_state(ctx, table, &opt.option, &opt.option_active_on) { OptionState::No => { if get_option_supported(ctx, table, &opt.option, &opt.option_active_on) { set_option_state( ctx, table, &opt.option, &opt.option_active_on, &OptionState::Yes, ); send_positive(mud, opt); return true; } else { send_negative(mud, opt); } } OptionState::Yes => {} OptionState::WantNo => { console::log_1(&JsValue::from_str(&format!( "DONT answered by WILL for option {}", opt.option.0 ))); set_option_state( ctx, table, &opt.option, &opt.option_active_on, &OptionState::No, ) } OptionState::WantNoOpposite => { console::log_1(&JsValue::from_str(&format!( "DONT answered by WILL for option {}", opt.option.0 ))); set_option_state( ctx, table, &opt.option, &opt.option_active_on, &OptionState::Yes, ); return true; } OptionState::WantYes => { set_option_state( ctx, table, &opt.option, &opt.option_active_on, &OptionState::Yes, ); return true; } OptionState::WantYesOpposite => { set_option_state( ctx, table, &opt.option, &opt.option_active_on, &OptionState::WantNo, ); send_negative(mud, opt); } } false } fn handle_incoming_negative<'gc, T: SendOptNeg>( ctx: Context<'gc>, mud: &mut T, table: &Table<'gc>, opt: &OptionWithActiveSide, ) { match get_option_state(ctx, table, &opt.option, &opt.option_active_on) { OptionState::No => {} OptionState::Yes => { set_option_state( ctx, table, &opt.option, &opt.option_active_on, &OptionState::No, ); send_negative(mud, opt) } OptionState::WantNo => set_option_state( ctx, table, &opt.option, &opt.option_active_on, &OptionState::No, ), OptionState::WantNoOpposite => { set_option_state( ctx, table, &opt.option, &opt.option_active_on, &OptionState::WantYes, ); send_positive(mud, opt); } OptionState::WantYes => set_option_state( ctx, table, &opt.option, &opt.option_active_on, &OptionState::No, ), OptionState::WantYesOpposite => set_option_state( ctx, table, &opt.option, &opt.option_active_on, &OptionState::No, ), } } pub fn handle_incoming_will<'gc, T: SendOptNeg>( ctx: Context<'gc>, mud: &mut T, table: &Table<'gc>, option: &Telopt, ) -> bool { handle_incoming_positive( ctx, mud, table, &OptionWithActiveSide { option: option.clone(), option_active_on: Side::Him, }, ) } pub fn handle_incoming_wont<'gc, T: SendOptNeg>( ctx: Context<'gc>, mud: &mut T, table: &Table<'gc>, option: &Telopt, ) { handle_incoming_negative( ctx, mud, table, &OptionWithActiveSide { option: option.clone(), option_active_on: Side::Him, }, ); } pub fn handle_incoming_do<'gc, T: SendOptNeg>( ctx: Context<'gc>, mud: &mut T, table: &Table<'gc>, option: &Telopt, ) -> bool { handle_incoming_positive( ctx, mud, table, &OptionWithActiveSide { option: option.clone(), option_active_on: Side::Us, }, ) } pub fn handle_incoming_dont<'gc, T: SendOptNeg>( ctx: Context<'gc>, mud: &mut T, table: &Table<'gc>, option: &Telopt, ) { handle_incoming_negative( ctx, mud, table, &OptionWithActiveSide { option: option.clone(), option_active_on: Side::Us, }, ); } pub const GMCP_TELOPT: Telopt = Telopt(201); pub const NAWS_TELOPT: Telopt = Telopt(31); fn negotiate_option_on<'gc, T: SendOptNeg>( ctx: Context<'gc>, mud: &mut T, table: &Table<'gc>, opt: &OptionWithActiveSide, ) { match get_option_state(ctx, table, &opt.option, &opt.option_active_on) { OptionState::No => { set_option_state( ctx, table, &opt.option, &opt.option_active_on, &OptionState::WantYes, ); send_positive(mud, opt); } OptionState::Yes => {} OptionState::WantNo => set_option_state( ctx, table, &opt.option, &opt.option_active_on, &OptionState::WantNoOpposite, ), OptionState::WantNoOpposite => {} OptionState::WantYes => {} OptionState::WantYesOpposite => set_option_state( ctx, table, &opt.option, &opt.option_active_on, &OptionState::WantYes, ), } } fn negotiate_option_off<'gc, T: SendOptNeg>( ctx: Context<'gc>, mud: &mut T, table: &Table<'gc>, opt: &OptionWithActiveSide, ) { match get_option_state(ctx, table, &opt.option, &opt.option_active_on) { OptionState::No => {} OptionState::Yes => { set_option_state( ctx, table, &opt.option, &opt.option_active_on, &OptionState::WantNo, ); send_negative(mud, opt); } OptionState::WantNo => {} OptionState::WantNoOpposite => set_option_state( ctx, table, &opt.option, &opt.option_active_on, &OptionState::WantNo, ), OptionState::WantYes => set_option_state( ctx, table, &opt.option, &opt.option_active_on, &OptionState::WantYesOpposite, ), OptionState::WantYesOpposite => {} } } pub fn negotiate_option<'gc, T: SendOptNeg>( ctx: Context<'gc>, mud: &mut T, table: &Table<'gc>, opt: &OptionWithActiveSide, desired_state: bool, ) { if desired_state { negotiate_option_on(ctx, mud, table, opt); } else { negotiate_option_off(ctx, mud, table, opt); } } pub fn configure_telopt_table<'gc>(ctx: Context<'gc>, table: &Table<'gc>) { table .set(ctx, "naws", NAWS_TELOPT.0) .expect("Can't set NAWS in telopt table"); table .set(ctx, "gmcp", GMCP_TELOPT.0) .expect("Can't set GMCP in telopt table"); } #[cfg(test)] mod tests { use piccolo::{Context, Lua, Table}; use crate::{ lua_engine::telopt::{set_option_state, OptionState, Telopt}, telnet::{parse_telnet_buf, TelnetOutput}, }; use super::{ get_option_state, handle_incoming_do, handle_incoming_dont, handle_incoming_will, handle_incoming_wont, negotiate_option, set_option_supported, OptionWithActiveSide, SendRaw, Side, GMCP_TELOPT, }; #[test] fn get_option_defaults_no() { let mut lua = Lua::empty(); lua.enter(|ctx| { let root_tab = Table::new(&ctx); assert_eq!( get_option_state(ctx, &root_tab, &GMCP_TELOPT, &Side::Him), OptionState::No ); }); } #[test] fn get_set_option_state_roundtrips() { let mut lua = Lua::empty(); lua.enter(|ctx| { let root_tab = Table::new(&ctx); set_option_state(ctx, &root_tab, &GMCP_TELOPT, &Side::Him, &OptionState::Yes); set_option_state( ctx, &root_tab, &GMCP_TELOPT, &Side::Us, &OptionState::WantNoOpposite, ); set_option_state( ctx, &root_tab, &Telopt(123), &Side::Us, &OptionState::WantYes, ); assert_eq!( get_option_state(ctx, &root_tab, &GMCP_TELOPT, &Side::Him), OptionState::Yes ); assert_eq!( get_option_state(ctx, &root_tab, &GMCP_TELOPT, &Side::Us), OptionState::WantNoOpposite ); assert_eq!( get_option_state(ctx, &root_tab, &Telopt(123), &Side::Us), OptionState::WantYes ); }); } pub enum SideTestAction { SetOptionSupported { option: OptionWithActiveSide, }, SetOptionEnabled { state: bool, option: OptionWithActiveSide, }, AssertOptionState { expected_state: OptionState, option: OptionWithActiveSide, }, } pub enum TestEvent { SideAAction(SideTestAction), SideBAction(SideTestAction), SyncUntilStatic, } pub struct FakeMudBuffer(Vec); impl SendRaw for FakeMudBuffer { fn send_bytes(&mut self, msg: &[u8]) { self.0.extend_from_slice(msg); } } pub struct TwoPartySim<'gc> { table_a: Table<'gc>, table_b: Table<'gc>, buffer_a: FakeMudBuffer, buffer_b: FakeMudBuffer, } fn run_test_event<'gc>(ctx: Context<'gc>, sim_state: &mut TwoPartySim<'gc>, event: TestEvent) { match event { TestEvent::SideAAction(act) => { run_side_action(ctx, act, sim_state.table_a, &mut sim_state.buffer_a) } TestEvent::SideBAction(act) => { run_side_action(ctx, act, sim_state.table_b, &mut sim_state.buffer_b) } TestEvent::SyncUntilStatic => sync_until_static(ctx, sim_state), } } fn run_simulation<'gc>(ctx: Context<'gc>, simulation: Vec) { let mut state = TwoPartySim { table_a: Table::new(&ctx), table_b: Table::new(&ctx), buffer_a: FakeMudBuffer(Vec::new()), buffer_b: FakeMudBuffer(Vec::new()), }; for event in simulation { run_test_event(ctx, &mut state, event); } } fn run_side_action<'gc>( ctx: Context<'gc>, act: SideTestAction, table: Table<'gc>, buffer: &mut FakeMudBuffer, ) { match act { SideTestAction::SetOptionEnabled { state, option } => { negotiate_option(ctx, buffer, &table, &option, state) } SideTestAction::AssertOptionState { expected_state, option, } => { assert_eq!( get_option_state(ctx, &table, &option.option, &option.option_active_on), expected_state ); } SideTestAction::SetOptionSupported { option } => { set_option_supported(ctx, &table, &option.option, &option.option_active_on) } } } fn sync_until_static<'gc>(ctx: Context<'gc>, sim_state: &mut TwoPartySim<'gc>) { loop { if sim_state.buffer_a.0.is_empty() && sim_state.buffer_b.0.is_empty() { break; } let (remaining, msg_a_to_b) = parse_telnet_buf(&sim_state.buffer_a.0); sim_state.buffer_a.0 = remaining; let (remaining, msg_b_to_a) = parse_telnet_buf(&sim_state.buffer_b.0); sim_state.buffer_b.0 = remaining; println!( "Message exchange: A->B: {:#?}, B->A: {:#?}", &msg_a_to_b, &msg_b_to_a ); simulate_msg_to(ctx, msg_a_to_b, sim_state.table_b, &mut sim_state.buffer_b); simulate_msg_to(ctx, msg_b_to_a, sim_state.table_a, &mut sim_state.buffer_a); } } fn simulate_msg_to<'gc>( ctx: Context<'gc>, msg: Option, table: Table<'gc>, buffer: &mut FakeMudBuffer, ) { match msg { Some(TelnetOutput::Will(n)) => handle_incoming_will(ctx, buffer, &table, &Telopt(n)), Some(TelnetOutput::Wont(n)) => handle_incoming_wont(ctx, buffer, &table, &Telopt(n)), Some(TelnetOutput::Do(n)) => handle_incoming_do(ctx, buffer, &table, &Telopt(n)), Some(TelnetOutput::Dont(n)) => handle_incoming_dont(ctx, buffer, &table, &Telopt(n)), _ => {} } } use SideTestAction::*; use TestEvent::*; #[test] fn negotiating_unsupported_local_converges_both_off() { let mut lua = Lua::empty(); lua.enter(|ctx| { run_simulation( ctx, vec![ SideAAction(SetOptionEnabled { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Us, }, state: true, }), SyncUntilStatic, SideAAction(AssertOptionState { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Us, }, expected_state: OptionState::No, }), SideBAction(AssertOptionState { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Him, }, expected_state: OptionState::No, }), ], ) }); } #[test] fn negotiating_unsupported_remote_converges_both_off() { let mut lua = Lua::empty(); lua.enter(|ctx| { run_simulation( ctx, vec![ SideAAction(SetOptionEnabled { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Him, }, state: true, }), SyncUntilStatic, SideAAction(AssertOptionState { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Him, }, expected_state: OptionState::No, }), SideBAction(AssertOptionState { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Us, }, expected_state: OptionState::No, }), ], ) }); } #[test] fn negotiating_bothways_local_converges_both_on() { let mut lua = Lua::empty(); lua.enter(|ctx| { run_simulation( ctx, vec![ SideAAction(SetOptionEnabled { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Us, }, state: true, }), SideBAction(SetOptionEnabled { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Him, }, state: true, }), SyncUntilStatic, SideAAction(AssertOptionState { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Us, }, expected_state: OptionState::Yes, }), SideBAction(AssertOptionState { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Him, }, expected_state: OptionState::Yes, }), ], ) }); } #[test] fn negotiating_supported_local_converges_both_on() { let mut lua = Lua::empty(); lua.enter(|ctx| { run_simulation( ctx, vec![ SideAAction(SetOptionSupported { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Us, }, }), SideBAction(SetOptionEnabled { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Him, }, state: true, }), SyncUntilStatic, SideAAction(AssertOptionState { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Us, }, expected_state: OptionState::Yes, }), SideBAction(AssertOptionState { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Him, }, expected_state: OptionState::Yes, }), ], ) }); } #[test] fn negotiating_supported_local_then_changing_locally_converges_both_off() { let mut lua = Lua::empty(); lua.enter(|ctx| { run_simulation( ctx, vec![ SideAAction(SetOptionEnabled { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Us, }, state: true, }), SideBAction(SetOptionEnabled { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Him, }, state: true, }), SyncUntilStatic, SideAAction(SetOptionEnabled { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Us, }, state: false, }), SyncUntilStatic, SideAAction(AssertOptionState { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Us, }, expected_state: OptionState::No, }), SideBAction(AssertOptionState { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Him, }, expected_state: OptionState::No, }), ], ) }); } #[test] fn negotiating_supported_local_then_changing_remotely_converges_both_off() { let mut lua = Lua::empty(); lua.enter(|ctx| { run_simulation( ctx, vec![ SideAAction(SetOptionEnabled { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Us, }, state: true, }), SideBAction(SetOptionEnabled { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Him, }, state: true, }), SyncUntilStatic, SideBAction(SetOptionEnabled { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Him, }, state: false, }), SyncUntilStatic, SideAAction(AssertOptionState { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Us, }, expected_state: OptionState::No, }), SideBAction(AssertOptionState { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Him, }, expected_state: OptionState::No, }), ], ) }); } #[test] fn negotiating_supported_local_and_changing_locally_converges_both_off() { let mut lua = Lua::empty(); lua.enter(|ctx| { run_simulation( ctx, vec![ SideAAction(SetOptionEnabled { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Us, }, state: true, }), SideBAction(SetOptionEnabled { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Him, }, state: true, }), SideAAction(SetOptionEnabled { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Us, }, state: false, }), SyncUntilStatic, SideAAction(AssertOptionState { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Us, }, expected_state: OptionState::No, }), SideBAction(AssertOptionState { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Him, }, expected_state: OptionState::No, }), ], ) }); } #[test] fn negotiating_supported_local_and_changing_remotely_converges_both_off() { let mut lua = Lua::empty(); lua.enter(|ctx| { run_simulation( ctx, vec![ SideAAction(SetOptionEnabled { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Us, }, state: true, }), SideBAction(SetOptionEnabled { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Him, }, state: true, }), SideBAction(SetOptionEnabled { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Him, }, state: false, }), SyncUntilStatic, SideAAction(AssertOptionState { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Us, }, expected_state: OptionState::No, }), SideBAction(AssertOptionState { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Him, }, expected_state: OptionState::No, }), ], ) }); } #[test] fn negotiating_supported_local_and_changing_remotely_twice_converges_both_on() { let mut lua = Lua::empty(); lua.enter(|ctx| { run_simulation( ctx, vec![ SideAAction(SetOptionEnabled { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Us, }, state: true, }), SideBAction(SetOptionEnabled { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Him, }, state: true, }), SideBAction(SetOptionEnabled { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Him, }, state: false, }), SideBAction(SetOptionEnabled { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Him, }, state: true, }), SyncUntilStatic, SideAAction(AssertOptionState { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Us, }, expected_state: OptionState::Yes, }), SideBAction(AssertOptionState { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Him, }, expected_state: OptionState::Yes, }), ], ) }); } #[test] fn negotiating_supported_local_and_changing_locally_once_remotely_twice_converges_both_off() { let mut lua = Lua::empty(); lua.enter(|ctx| { run_simulation( ctx, vec![ SideAAction(SetOptionEnabled { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Us, }, state: true, }), SideBAction(SetOptionEnabled { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Him, }, state: true, }), SideBAction(SetOptionEnabled { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Him, }, state: false, }), SideBAction(SetOptionEnabled { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Him, }, state: true, }), SideAAction(SetOptionEnabled { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Us, }, state: false, }), SyncUntilStatic, SideAAction(AssertOptionState { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Us, }, expected_state: OptionState::No, }), SideBAction(AssertOptionState { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Him, }, expected_state: OptionState::No, }), ], ) }); } #[test] fn negotiating_supported_local_then_changing_locally_and_reversing_converges_both_on() { let mut lua = Lua::empty(); lua.enter(|ctx| { run_simulation( ctx, vec![ SideBAction(SetOptionSupported { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Him, }, }), SideAAction(SetOptionEnabled { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Us, }, state: true, }), SyncUntilStatic, SideAAction(SetOptionEnabled { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Us, }, state: false, }), SideAAction(SetOptionEnabled { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Us, }, state: true, }), SyncUntilStatic, SideAAction(AssertOptionState { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Us, }, expected_state: OptionState::Yes, }), SideBAction(AssertOptionState { option: OptionWithActiveSide { option: Telopt(123), option_active_on: Side::Him, }, expected_state: OptionState::Yes, }), ], ) }); } }