//! Recovered pre-movement client policies with explicit captured callback effects. use crate::native_policy::Error; #[derive(Clone, Debug, PartialEq, Eq)] pub enum ProcessingEffect { Intermission { slot: i32, }, Spectator { slot: i32, command_time: u32, }, Team { slot: i32, text: Vec, force: u32, }, Send { slot: i32, text: Vec, }, Drop { slot: i32, text: Vec, }, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ClientRoute { Continue, Intermission, Spectator, Return, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct RoutingOutcome { pub route: ClientRoute, pub effects: Vec, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct RoutingInput { pub intermission: u32, pub team: u32, pub follow: u32, pub spectator: u32, pub slot: i32, pub command_time: u32, } /// Original154210 routing, stopping before command rewriting or at early return. pub fn route_client(input: RoutingInput) -> Result { if !(0..64).contains(&input.slot) { return Err(Error::InvalidIndex); } let (route, effects) = if input.intermission != 0 { ( ClientRoute::Intermission, vec![ProcessingEffect::Intermission { slot: input.slot }], ) } else if input.team == 3 || input.follow != 0 { if input.spectator == 3 { (ClientRoute::Return, vec![]) } else { ( ClientRoute::Spectator, vec![ProcessingEffect::Spectator { slot: input.slot, command_time: input.command_time, }], ) } } else { (ClientRoute::Continue, vec![]) }; Ok(RoutingOutcome { route, effects }) } #[cfg(test)] mod routing_tests { use super::*; fn input() -> RoutingInput { RoutingInput { intermission: 0, team: 0, follow: 0, spectator: 0, slot: 2, command_time: 12345, } } #[test] fn intermission_precedes_spectator_state() { let p = route_client(RoutingInput { intermission: 1, team: 3, follow: 1, spectator: 3, ..input() }) .unwrap(); assert_eq!(p.route, ClientRoute::Intermission); assert_eq!(p.effects, [ProcessingEffect::Intermission { slot: 2 }]); } #[test] fn spectator_word_only_suppresses_with_outer_gate() { assert_eq!( route_client(RoutingInput { spectator: 3, ..input() }) .unwrap() .route, ClientRoute::Continue ); let p = route_client(RoutingInput { team: 3, spectator: 3, ..input() }) .unwrap(); assert_eq!(p.route, ClientRoute::Return); assert!(p.effects.is_empty()); assert_eq!( route_client(RoutingInput { follow: 1, ..input() }) .unwrap() .route, ClientRoute::Spectator ); } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct CommandState { pub angles: [u32; 3], pub saved: [u32; 3], pub buttons: u32, pub move_word: u32, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct CommandControl { pub pause: u32, pub mode: u32, pub flags: u32, pub block: u32, } /// Original154256: lock restores saved angles; otherwise refresh the saved copy. /// Byte stores clear movement while preserving the adjacent low command byte. pub fn rewrite_client_command(state: &mut CommandState, control: CommandControl) { let locked = control.pause & 1 != 0 || control.mode == 10 && control.flags & 16 != 0; if locked { state.angles = state.saved; } else { state.saved = state.angles; } if locked || control.block != 0 { state.buttons = 0; state.move_word &= 255; } } #[cfg(test)] mod command_tests { use super::*; fn state() -> CommandState { CommandState { angles: [1, 2, 3], saved: [4, 5, 6], buttons: 7, move_word: 0x807fff42, } } #[test] fn pause_bit_selection_and_angle_direction_are_preserved() { let mut s = state(); rewrite_client_command( &mut s, CommandControl { pause: 2, mode: 0, flags: 16, block: 0, }, ); assert_eq!(s.saved, [1, 2, 3]); assert_eq!(s.move_word, 0x807fff42); let mut s = state(); rewrite_client_command( &mut s, CommandControl { pause: 1, mode: 0, flags: 0, block: 0, }, ); assert_eq!(s.angles, [4, 5, 6]); assert_eq!(s.saved, [4, 5, 6]); assert_eq!(s.move_word, 0x42); assert_eq!(s.buttons, 0); } #[test] fn server_override_masks_bytes_after_saving_angles() { let mut s = state(); s.move_word = 0x010203ff; rewrite_client_command( &mut s, CommandControl { pause: 0, mode: 0, flags: 0, block: 1, }, ); assert_eq!(s.saved, [1, 2, 3]); assert_eq!(s.angles, [1, 2, 3]); assert_eq!(s.move_word, 255); assert_eq!(s.buttons, 0); } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct InactivityState { pub deadline: u32, pub warned: u32, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct InactivityInput { pub entity_flags: u32, pub idle_seconds: u32, pub pause: u32, pub pause_anchor: u32, pub tick: u32, pub moves: [u8; 3], pub buttons: u32, pub health: u32, pub exempt: u32, pub mode: u32, pub msec: u32, pub slot: i32, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct InactivityOutcome { pub accepted: bool, pub effects: Vec, } const WARN_DROP: &[u8] = b"cp \"Ten seconds until inactivity drop!\n\""; const WARN_SPEC: &[u8] = b"cp \"Ten seconds until forced spectator mode!\n\""; const IDLE_DROP: &[u8] = b"Dropped due to inactivity"; const IDLE_MOVED: &[u8] = b"cp \"You were moved to spectator due to inactivity!\n\""; /// Original150382 with native team/send/drop effects captured. Arithmetic wraps /// as words before signed comparisons; dead-client time extension precedes exemption. pub fn client_inactivity( state: &mut InactivityState, input: InactivityInput, ) -> Result { if !(0..64).contains(&input.slot) { return Err(Error::InvalidIndex); } let mut accepted = true; let mut effects = vec![]; if input.entity_flags & 8 != 0 { } else if input.idle_seconds == 0 { state.deadline = input.tick.wrapping_add(60000); state.warned = 0; } else if input.pause != 0 { let limit = input .pause_anchor .wrapping_add(input.idle_seconds.wrapping_mul(1000)); if (state.deadline as i32) < limit as i32 { state.deadline = limit; } } else if input.moves.iter().any(|v| *v != 0) || input.buttons & 1 != 0 { let limit = input .tick .wrapping_add(input.idle_seconds.wrapping_mul(1000)); if (state.deadline as i32) < limit as i32 { state.deadline = limit; } state.warned = 0; } else if input.health as i32 <= 0 { state.deadline = state.deadline.wrapping_add(input.msec); } else if input.exempt != 0 { } else if input.tick as i32 > state.deadline as i32 { accepted = false; if input.mode == 1 { effects.push(ProcessingEffect::Team { slot: input.slot, text: b"s".to_vec(), force: 0, }); effects.push(ProcessingEffect::Send { slot: input.slot, text: IDLE_MOVED.to_vec(), }); } else { effects.push(ProcessingEffect::Drop { slot: input.slot, text: IDLE_DROP.to_vec(), }); } } else if input.tick as i32 > state.deadline.wrapping_sub(10000) as i32 && state.warned == 0 { state.warned = 1; effects.push(ProcessingEffect::Send { slot: input.slot, text: if input.mode == 1 { WARN_SPEC.to_vec() } else { WARN_DROP.to_vec() }, }); } Ok(InactivityOutcome { accepted, effects }) } #[cfg(test)] mod inactivity_tests { use super::*; fn input() -> InactivityInput { InactivityInput { entity_flags: 0, idle_seconds: 60, pause: 0, pause_anchor: 3000, tick: 10000, moves: [0; 3], buttons: 0, health: 1, exempt: 0, mode: 1, msec: 100, slot: 2, } } #[test] fn warning_boundary_repeat_suppression_and_timeout_order() { let mut s = InactivityState { deadline: 20000, warned: 0, }; assert!( client_inactivity(&mut s, input()) .unwrap() .effects .is_empty() ); let i = InactivityInput { tick: 10001, ..input() }; assert_eq!(client_inactivity(&mut s, i).unwrap().effects.len(), 1); assert_eq!(s.warned, 1); assert!(client_inactivity(&mut s, i).unwrap().effects.is_empty()); let out = client_inactivity(&mut s, InactivityInput { tick: 20001, ..i }).unwrap(); assert!(!out.accepted); assert!(matches!( &out.effects[..], [ProcessingEffect::Team { .. }, ProcessingEffect::Send { .. }] )); } #[test] fn pause_and_activity_preserve_later_deadlines_and_dead_extension_wraps() { let mut s = InactivityState { deadline: 0, warned: 1, }; client_inactivity( &mut s, InactivityInput { pause: 2, ..input() }, ) .unwrap(); assert_eq!( s, InactivityState { deadline: 63000, warned: 1 } ); s.deadline = 90000; client_inactivity( &mut s, InactivityInput { moves: [0, 128, 0], ..input() }, ) .unwrap(); assert_eq!(s.deadline, 90000); assert_eq!(s.warned, 0); s.deadline = 20; client_inactivity( &mut s, InactivityInput { health: 0, exempt: 1, msec: u32::MAX, ..input() }, ) .unwrap(); assert_eq!(s.deadline, 19); } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct MovementState { pub pm_type: u32, pub gravity: u32, pub speed: u32, pub controller: bool, pub packed: [u32; 2], pub event_sequence: u32, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct MovementInput { pub pause: u32, pub noclip: u32, pub health: u32, pub gravity: u32, pub factor: u32, pub packs: [u32; 4], } fn trunc_word(value: f32) -> Result { if !value.is_finite() { return Err(Error::NonFinite); } if !(-2147483648.0..2147483648.0).contains(&value) { return Err(Error::InvalidInput); } Ok((value as i32) as u32) } /// Original154405 finite movement setup through154544. A controller is consumed /// only on unpaused paths; raw packed words are not narrowed to bytes. pub fn setup_client_movement( state: &mut MovementState, input: MovementInput, ) -> Result { let mut next = *state; next.pm_type = if input.noclip != 0 { 1 } else if input.health as i32 <= 0 { 3 } else { 0 }; if input.pause & 1 != 0 { next.gravity = 0; next.speed = 0; } else { let base = f32::from_bits(input.gravity); let value = if next.controller { base * f32::from_bits(input.factor) } else { base }; next.gravity = trunc_word(value)?; next.speed = 220; next.controller = false; } next.packed = [ input.packs[0] | input.packs[1].wrapping_shl(8), input.packs[2] | input.packs[3].wrapping_shl(8), ]; let old = next.event_sequence; *state = next; Ok(old) } #[cfg(test)] mod movement_tests { use super::*; fn state() -> MovementState { MovementState { pm_type: 99, gravity: 111, speed: 222, controller: true, packed: [333, 444], event_sequence: 0xdeadbeef, } } fn input() -> MovementInput { MovementInput { pause: 0, noclip: 0, health: 1, gravity: 800.75f32.to_bits(), factor: (-0.5f32).to_bits(), packs: [0x1ff, 0x102, 0x80000000, 0xff0001], } } #[test] fn truncation_controller_consumption_and_unmasked_packing() { let mut s = state(); assert_eq!(setup_client_movement(&mut s, input()).unwrap(), 0xdeadbeef); assert_eq!(s.gravity, (-400i32) as u32); assert_eq!(s.speed, 220); assert!(!s.controller); assert_eq!(s.packed, [0x103ff, 0xff000100]); } #[test] fn paused_path_ignores_float_inputs_and_unsupported_unpaused_input_is_atomic() { let mut s = state(); let i = MovementInput { pause: 1, noclip: 1, health: 0, gravity: f32::NAN.to_bits(), factor: f32::NAN.to_bits(), ..input() }; setup_client_movement(&mut s, i).unwrap(); assert_eq!([s.pm_type, s.gravity, s.speed], [1, 0, 0]); assert!(s.controller); let old = s; assert!(setup_client_movement(&mut s, MovementInput { pause: 0, ..i }).is_err()); assert_eq!(s, old); } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct ClientProcessingState { pub timing: crate::client_timer::TimerPrepareState, pub command: CommandState, pub inactivity: InactivityState, pub movement: MovementState, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct ClientProcessingInput { pub slot: i32, pub tick: u32, pub intermission: u32, pub team: u32, pub follow: u32, pub pause: u32, pub game_mode: u32, pub client_flags: u32, pub block: u32, pub entity_flags: u32, pub idle_seconds: u32, pub pause_anchor: u32, pub health: u32, pub exempt: u32, pub idle_mode: u32, pub noclip: u32, pub gravity: u32, pub factor: u32, pub packs: [u32; 4], } #[derive(Clone, Debug, PartialEq, Eq)] pub struct ClientProcessingOutcome { pub proceed: bool, pub msec: Option, pub idle_result: Option, pub idle_msec: Option, pub event_sequence: Option, pub effects: Vec, } fn process_inner( state: &mut ClientProcessingState, input: ClientProcessingInput, ) -> Result { let prepared = crate::client_timer::prepare_client_timing(&mut state.timing, input.tick); let mut out = ClientProcessingOutcome { proceed: false, msec: prepared.msec, idle_result: None, idle_msec: None, event_sequence: None, effects: vec![], }; if !prepared.proceed { return Ok(out); } let routed = route_client(RoutingInput { intermission: input.intermission, team: input.team, follow: input.follow, spectator: state.timing.exception, slot: input.slot, command_time: state.timing.command_time, })?; out.effects.extend(routed.effects); if routed.route != ClientRoute::Continue { return Ok(out); } rewrite_client_command( &mut state.command, CommandControl { pause: input.pause, mode: input.game_mode, flags: input.client_flags, block: input.block, }, ); let bytes = state.command.move_word.to_le_bytes(); let msec = prepared .msec .expect("proceeding source preparation has a delta"); let idle = client_inactivity( &mut state.inactivity, InactivityInput { entity_flags: input.entity_flags, idle_seconds: input.idle_seconds, pause: input.pause, pause_anchor: input.pause_anchor, tick: input.tick, moves: [bytes[1], bytes[2], bytes[3]], buttons: state.command.buttons, health: input.health, exempt: input.exempt, mode: input.idle_mode, msec, slot: input.slot, }, )?; out.idle_result = Some(idle.accepted); out.idle_msec = Some(msec); out.effects.extend(idle.effects); if !idle.accepted { return Ok(out); } out.event_sequence = Some(setup_client_movement( &mut state.movement, MovementInput { pause: input.pause, noclip: input.noclip, health: input.health, gravity: input.gravity, factor: input.factor, packs: input.packs, }, )?); out.proceed = true; Ok(out) } /// Original154089 through154544 with captured callback effects. Ordinary early /// exits commit preceding writes; unsupported typed domains roll back together. pub fn client_pre_movement( state: &mut ClientProcessingState, input: ClientProcessingInput, ) -> Result { if !(0..64).contains(&input.slot) { return Err(Error::InvalidIndex); } let mut next = *state; let out = process_inner(&mut next, input)?; *state = next; Ok(out) } #[cfg(test)] mod pipeline_tests { use super::*; fn state() -> ClientProcessingState { ClientProcessingState { timing: crate::client_timer::TimerPrepareState { connection: 2, exception: 0, command_time: 10050, current_time: 10000, quantize: 1, copied_tick: 17, copied_command: 18, }, command: CommandState { angles: [11, 22, 33], saved: [44, 55, 66], buttons: 0, move_word: 0x12a, }, inactivity: InactivityState { deadline: 9999, warned: 0, }, movement: MovementState { pm_type: 99, gravity: 111, speed: 222, controller: false, packed: [333, 444], event_sequence: 9, }, } } fn input() -> ClientProcessingInput { ClientProcessingInput { slot: 2, tick: 10000, intermission: 0, team: 0, follow: 0, pause: 0, game_mode: 0, client_flags: 0, block: 0, entity_flags: 0, idle_seconds: 60, pause_anchor: 3000, health: 1, exempt: 0, idle_mode: 0, noclip: 0, gravity: 800.75f32.to_bits(), factor: 0.5f32.to_bits(), packs: [0x1ff, 0x102, 0x80000000, 0xff0001], } } #[test] fn blocked_movement_times_out_after_preserving_earlier_writes() { let mut s = state(); let p = client_pre_movement( &mut s, ClientProcessingInput { block: 1, ..input() }, ) .unwrap(); assert!(!p.proceed); assert_eq!(p.idle_result, Some(false)); assert_eq!(p.idle_msec, Some(50)); assert!(matches!(&p.effects[..], [ProcessingEffect::Drop { .. }])); assert_eq!(s.command.saved, [11, 22, 33]); assert_eq!(s.command.move_word, 42); assert_eq!(s.timing.command_time, 10056); assert_eq!(s.movement.pm_type, 99); let mut s = state(); assert!(client_pre_movement(&mut s, input()).unwrap().proceed); assert_eq!(s.inactivity.deadline, 70000); } #[test] fn routed_exit_commits_timestamps_but_unsupported_late_input_is_atomic() { let mut s = state(); let command = s.command; let p = client_pre_movement( &mut s, ClientProcessingInput { intermission: 1, ..input() }, ) .unwrap(); assert!(!p.proceed); assert_eq!(s.command, command); assert_eq!(s.timing.copied_command, 10050); assert!(matches!( &p.effects[..], [ProcessingEffect::Intermission { .. }] )); let mut s = state(); s.command.move_word = 42; s.inactivity.deadline = 19999; let old = s; assert!( client_pre_movement( &mut s, ClientProcessingInput { gravity: f32::NAN.to_bits(), ..input() } ) .is_err() ); assert_eq!(s, old); } }