/* * Copyright (c) Peter Bjorklund. All rights reserved. https://github.com/nimble-rust/nimble * Licensed under the MIT License. See LICENSE in the project root for license information. */ use flood_rs::{BufferDeserializer, Deserialize, Serialize}; use monotonic_time_rs::Millis; use nimble_client_logic::err::ClientLogicError; use nimble_client_logic::{ClientLogic, ClientLogicPhase}; use nimble_participant::ParticipantId; use nimble_protocol::client_to_host::{ConnectRequest, DownloadGameStateRequest}; use nimble_protocol::host_to_client::{ AuthoritativeStepRanges, ConnectionAccepted, GameStepResponse, GameStepResponseHeader, }; use nimble_protocol::prelude::{ClientToHostCommands, CombinedSteps, HostToClientCommands}; use nimble_sample_step::{SampleState, SampleStep}; use nimble_step::Step; use nimble_step::Step::{Custom, Forced}; use nimble_step_map::StepMap; use std::fmt::Debug; use tick_id::TickId; use tick_queue::ItemInfo; #[test_log::test] fn basic_logic() { let simulation_version = app_version::Version::new(0, 1, 2); let mut client_logic = ClientLogic::>::new(simulation_version); feed_connect_response(&mut client_logic); { let now = Millis::new(0); let commands = client_logic.send(now); assert_eq!(commands.len(), 2); if let ClientToHostCommands::DownloadGameState(DownloadGameStateRequest { request_id }) = &commands[1] { assert_eq!(*request_id, 153); } else { panic!("Command did not match expected structure or pattern"); } } } fn feed_connect_response(client_logic: &mut ClientLogic>) { let now = Millis::new(0); let commands = client_logic.send(now); assert_eq!(commands.len(), 1); if let ClientToHostCommands::ConnectType(ConnectRequest { client_request_id, .. }) = commands[0] { let connect_response = ConnectionAccepted { flags: 0, response_to_request: client_request_id, }; client_logic .receive(now, &HostToClientCommands::ConnectType(connect_response)) .expect("TODO: panic message"); } else { panic!("Command should be connect request"); } } fn setup_logic< StateT: BufferDeserializer, StepT: Clone + Deserialize + Serialize + Debug + std::fmt::Display, >() -> ClientLogic { let simulation_version = app_version::Version::new(0, 1, 2); ClientLogic::::new(simulation_version) } #[test_log::test] fn send_steps() -> Result<(), ClientLogicError> { let mut client_logic = setup_logic::>(); client_logic.push_predicted_step( TickId(0), [(ParticipantId(0), Custom(SampleStep::MoveRight(3)))] .as_slice() .into(), )?; feed_connect_response(&mut client_logic); { let now = Millis::new(0); let commands = client_logic.send(now); assert_eq!(commands.len(), 2); if let ClientToHostCommands::DownloadGameState(DownloadGameStateRequest { request_id }) = &commands[1] { assert_eq!(*request_id, 0x99); } else { panic!( "Command did not match expected structure or pattern {}", commands[0] ); } } Ok(()) } fn setup_sample_steps() -> AuthoritativeStepRanges> { let first_steps = vec![ Custom(SampleStep::Jump), Custom(SampleStep::MoveLeft(-10)), Custom(SampleStep::MoveRight(32000)), ]; let first_participant_id = ParticipantId(255); let second_steps = vec![ Custom(SampleStep::MoveLeft(42)), Forced, Custom(SampleStep::Jump), ]; let second_participant_id = ParticipantId(1); let mut auth_steps = Vec::>>::new(); for index in 0..3 { let mut hash_map = StepMap::>::new(); hash_map .insert(first_participant_id, first_steps[index].clone()) .expect("first participant should be unique"); hash_map .insert(second_participant_id, second_steps[index].clone()) .expect("second_participant should be unique"); auth_steps.push(hash_map); } const EXPECTED_TICK_ID: TickId = TickId(0); let range_to_send = CombinedSteps::> { tick_id: EXPECTED_TICK_ID, steps: auth_steps, }; let ranges_to_send = AuthoritativeStepRanges { ranges: vec![range_to_send], }; ranges_to_send } #[test_log::test] fn receive_authoritative_steps() -> Result<(), ClientLogicError> { let mut client_logic = setup_logic::(); // Create a GameStep command let response = GameStepResponse::> { response_header: GameStepResponseHeader { // We ignore the response for now connection_buffer_count: 2, delta_buffer: -2, next_expected_tick_id: TickId(0), }, authoritative_steps: setup_sample_steps(), }; let command = HostToClientCommands::GameStep(response); let now = Millis::new(0); // Receive client_logic.receive(now, &command)?; // Verify let authoritative_steps = client_logic.debug_authoritative_steps(); assert_eq!( authoritative_steps .back_tick_id() .expect("should have end_tick_id by now"), TickId(2) ); // Should have received TickId 0, 1, and 2. let first_participant_id = ParticipantId(255); let second_participant_id = ParticipantId(1); let mut expected_hash_map = StepMap::>::new(); expected_hash_map .insert(first_participant_id, Custom(SampleStep::MoveLeft(-10))) .expect("should be unique"); expected_hash_map .insert(second_participant_id, Forced) .expect("should be unique"); let expected_step = expected_hash_map; let expected_step_with_step_info = ItemInfo::>> { item: expected_step, tick_id: TickId(1), }; let auth_steps = authoritative_steps .debug_get(1) .expect("index 1 should exist"); assert_eq!(authoritative_steps.len(), 3); assert_eq!(*auth_steps, expected_step_with_step_info); // Create a GameStep command let response2 = GameStepResponse::> { response_header: GameStepResponseHeader { // We ignore the response for now connection_buffer_count: 2, delta_buffer: -3, next_expected_tick_id: TickId(0), }, authoritative_steps: setup_sample_steps(), }; let command2 = HostToClientCommands::GameStep(response2); // Receive client_logic.receive(now, &command2)?; assert_eq!(client_logic.server_buffer_delta_ticks(), None); // Create a GameStep command let response3 = GameStepResponse::> { response_header: GameStepResponseHeader { // We ignore the response for now connection_buffer_count: 8, delta_buffer: -4, next_expected_tick_id: TickId(0), }, authoritative_steps: setup_sample_steps(), }; let command3 = HostToClientCommands::GameStep(response3); client_logic.receive(now, &command3)?; assert_eq!( client_logic .server_buffer_delta_ticks() .expect("should work"), -3 ); Ok(()) } fn create_connecting_client( simulation_version: Option, ) -> ClientLogic { let simulation_version = simulation_version.unwrap_or(app_version::Version::new(1, 0, 0)); let mut client = ClientLogic::::new(simulation_version); let now = Millis::new(0); let _ = client.send(now); client } #[test_log::test] fn send_connect_command() { let mut client = create_connecting_client(None); let now = Millis::new(0); let commands = client.send(now); let ClientToHostCommands::ConnectType(connect_cmd) = &commands[0] else { panic!("Wrong command") }; assert_eq!( connect_cmd.application_version, nimble_protocol::Version { major: 1, minor: 0, patch: 0 } ); assert_eq!( connect_cmd.nimble_version, nimble_protocol::Version { major: 0, minor: 0, patch: 5 } ); assert_eq!(connect_cmd.use_debug_stream, false); assert_eq!( connect_cmd.client_request_id, client .debug_connect_request_id() .expect("connect request id not set") ); } #[test_log::test] fn receive_valid_connection_accepted() { let mut client = create_connecting_client(None); let response_nonce = client .debug_connect_request_id() .expect("connect request id not set"); let accepted = ConnectionAccepted { flags: 0, response_to_request: response_nonce, }; let command = HostToClientCommands::>::ConnectType(accepted); let now = Millis::new(0); let _ = client.send(now); // Just make it send once so it can try to accept the connection accepted let result = client.receive(now, &command); assert!(result.is_ok()); assert_eq!( client.phase(), &ClientLogicPhase::RequestDownloadState { download_state_request_id: 0x99 } ); } #[test_log::test] fn receive_invalid_connection_accepted_nonce() { let mut client = create_connecting_client(None); let wrong_request_id = nimble_protocol::ClientRequestId(99); let accepted = ConnectionAccepted { flags: 0, response_to_request: wrong_request_id, }; let command = HostToClientCommands::>::ConnectType(accepted); let now = Millis::new(0); let _ = client.send(now); // Just make it send once so it can try to accept the connection accepted let result = client.receive(now, &command); match result { Err(ClientLogicError::WrongConnectResponseRequestId(n)) => { assert_eq!(n, wrong_request_id); } _ => panic!("Expected WrongConnectResponseNonce error"), } } #[test_log::test] fn receive_response_without_request() { let mut client = create_connecting_client(None); let wrong_request_id = nimble_protocol::ClientRequestId(99); let accepted = ConnectionAccepted { flags: 0, response_to_request: wrong_request_id, }; let command = HostToClientCommands::>::ConnectType(accepted); let now = Millis::new(0); let result = client.receive(now, &command); match result { Err(ClientLogicError::WrongConnectResponseRequestId(_)) => {} _ => panic!("Expected WrongConnectResponseNonce error {result:?}"), } }