Download crates/urt-gameplay/src/client_processing.rs from nmarshall/urt-rs: direct link, hf CLI and curl.
- Browser
- Download file 22 kB
-
https://huggingface.co/nmarshall/urt-rs/resolve/main/crates/urt-gameplay/src/client_processing.rs
- Command line
-
hf download hf://nmarshall/urt-rs/crates/urt-gameplay/src/client_processing.rs
-
curl -L -o client_processing.rs https://huggingface.co/nmarshall/urt-rs/resolve/main/crates/urt-gameplay/src/client_processing.rs
22 kB
| //! Recovered pre-movement client policies with explicit captured callback effects. | |
| use crate::native_policy::Error; | |
| pub enum ProcessingEffect { | |
| Intermission { | |
| slot: i32, | |
| }, | |
| Spectator { | |
| slot: i32, | |
| command_time: u32, | |
| }, | |
| Team { | |
| slot: i32, | |
| text: Vec<u8>, | |
| force: u32, | |
| }, | |
| Send { | |
| slot: i32, | |
| text: Vec<u8>, | |
| }, | |
| Drop { | |
| slot: i32, | |
| text: Vec<u8>, | |
| }, | |
| } | |
| pub enum ClientRoute { | |
| Continue, | |
| Intermission, | |
| Spectator, | |
| Return, | |
| } | |
| pub struct RoutingOutcome { | |
| pub route: ClientRoute, | |
| pub effects: Vec<ProcessingEffect>, | |
| } | |
| 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<RoutingOutcome, Error> { | |
| 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 }) | |
| } | |
| mod routing_tests { | |
| use super::*; | |
| fn input() -> RoutingInput { | |
| RoutingInput { | |
| intermission: 0, | |
| team: 0, | |
| follow: 0, | |
| spectator: 0, | |
| slot: 2, | |
| command_time: 12345, | |
| } | |
| } | |
| 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 }]); | |
| } | |
| 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 | |
| ); | |
| } | |
| } | |
| pub struct CommandState { | |
| pub angles: [u32; 3], | |
| pub saved: [u32; 3], | |
| pub buttons: u32, | |
| pub move_word: u32, | |
| } | |
| 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; | |
| } | |
| } | |
| mod command_tests { | |
| use super::*; | |
| fn state() -> CommandState { | |
| CommandState { | |
| angles: [1, 2, 3], | |
| saved: [4, 5, 6], | |
| buttons: 7, | |
| move_word: 0x807fff42, | |
| } | |
| } | |
| 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); | |
| } | |
| 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); | |
| } | |
| } | |
| pub struct InactivityState { | |
| pub deadline: u32, | |
| pub warned: u32, | |
| } | |
| 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, | |
| } | |
| pub struct InactivityOutcome { | |
| pub accepted: bool, | |
| pub effects: Vec<ProcessingEffect>, | |
| } | |
| 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<InactivityOutcome, Error> { | |
| 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 }) | |
| } | |
| 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, | |
| } | |
| } | |
| 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 { .. }] | |
| )); | |
| } | |
| 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); | |
| } | |
| } | |
| pub struct MovementState { | |
| pub pm_type: u32, | |
| pub gravity: u32, | |
| pub speed: u32, | |
| pub controller: bool, | |
| pub packed: [u32; 2], | |
| pub event_sequence: u32, | |
| } | |
| 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<u32, Error> { | |
| 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<u32, Error> { | |
| 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) | |
| } | |
| 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], | |
| } | |
| } | |
| 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]); | |
| } | |
| 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); | |
| } | |
| } | |
| pub struct ClientProcessingState { | |
| pub timing: crate::client_timer::TimerPrepareState, | |
| pub command: CommandState, | |
| pub inactivity: InactivityState, | |
| pub movement: MovementState, | |
| } | |
| 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], | |
| } | |
| pub struct ClientProcessingOutcome { | |
| pub proceed: bool, | |
| pub msec: Option<u32>, | |
| pub idle_result: Option<bool>, | |
| pub idle_msec: Option<u32>, | |
| pub event_sequence: Option<u32>, | |
| pub effects: Vec<ProcessingEffect>, | |
| } | |
| fn process_inner( | |
| state: &mut ClientProcessingState, | |
| input: ClientProcessingInput, | |
| ) -> Result<ClientProcessingOutcome, Error> { | |
| 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<ClientProcessingOutcome, Error> { | |
| 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) | |
| } | |
| 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], | |
| } | |
| } | |
| 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); | |
| } | |
| 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); | |
| } | |
| } | |