diff --git a/Cargo.toml b/Cargo.toml index a5095c0..b8b2c00 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ rand = "0.8.4" bevy = "0.11" serde = "1.0.130" serde_json = "1.0" -serial_test = "1.0.0" +serial_test = "2.0" # Examples [[example]] diff --git a/tests/integration.rs b/tests/integration.rs new file mode 100644 index 0000000..5212423 --- /dev/null +++ b/tests/integration.rs @@ -0,0 +1,218 @@ +use bevy::{ + input::{keyboard::KeyboardInput, ButtonState, Input, InputPlugin}, + prelude::*, + time::TimeUpdateStrategy, + utils::{Duration, HashMap}, + MinimalPlugins, +}; +use bevy_ggrs::{ + AddRollbackCommandExtension, GgrsConfig, GgrsPlugin, GgrsSchedule, LocalInputs, LocalPlayers, + PlayerInputs, ReadInputs, Rollback, Session, +}; +use bytemuck::{Pod, Zeroable}; +use ggrs::{Config, P2PSession, PlayerHandle, PlayerType, SessionBuilder, UdpNonBlockingSocket}; +use serial_test::serial; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; + +#[test] +#[serial] +fn it_runs_advance_frame_schedule_systems() -> Result<(), Box> { + let (player1, player2) = create_players(); + let session1 = start_session(&player1, &player2)?; + let mut app1 = create_app::(session1); + let session2 = start_session(&player2, &player1)?; + let mut app2 = create_app::(session2); + + let inputs1 = HashMap::from([(player1.handle, BoxInput { inp: 0 })]); + let inputs_resource = LocalInputs::(inputs1); + app1.insert_resource(inputs_resource); + + for _ in 0..50 { + app1.update(); + app2.update(); + } + + let frame_count1 = app1.world.get_resource::().unwrap(); + let frame_count2 = app2.world.get_resource::().unwrap(); + + // We've run Bevy for 50 frames, bevy_ggrs, however needs a couple of frames + // to sync before it starts to run the advance frame schedule, so the + // expected frame count is not 50 as one might expect. + // We just make sure that it started running + assert!(frame_count1.frame > 25); + assert!(frame_count2.frame > 25); + + Ok(()) +} + +#[test] +#[serial] +fn it_syncs_rollback_components() -> Result<(), Box> { + let (player1, player2) = create_players(); + let session1 = start_session(&player1, &player2)?; + let mut app1 = create_app::(session1); + let session2 = start_session(&player2, &player1)?; + let mut app2 = create_app::(session2); + + for _ in 0..50 { + press_key(&mut app1, KeyCode::W); + app1.update(); + app2.update(); + } + + let mut app2_query = app2.world.query::<(&Transform, &PlayerComponent)>(); + for (transform, player) in app2_query.iter(&app2.world) { + if player.handle == player1.handle { + assert!(transform.translation.z < 0., "Remote player moves forward"); + } + } + Ok(()) +} + +fn create_app(session: P2PSession) -> App { + let mut app = App::new(); + app.add_plugins(MinimalPlugins) + .add_plugins(InputPlugin::default()) + .add_plugins(GgrsPlugin::::default()) + .insert_resource(TimeUpdateStrategy::ManualDuration(Duration::from_secs_f64( + 1.0 / 60.0, + ))) + .insert_resource(Session::P2P(session)) + .insert_resource(FrameCount { frame: 0 }) + .add_systems(GgrsSchedule, (move_player_system, increase_frame_system)) + .add_systems(ReadInputs, read_local_inputs) + .add_systems(Startup, spawn_players); + app +} + +type TestConfig = GgrsConfig; + +#[repr(C)] +#[derive(Copy, Clone, PartialEq, Eq, Pod, Zeroable)] +pub struct BoxInput { + pub inp: u8, +} + +pub struct TestPlayer { + handle: PlayerHandle, + address: SocketAddr, +} + +fn create_players() -> (TestPlayer, TestPlayer) { + const PLAYER1_PORT: u16 = 8081; + const REMOTE_PORT: u16 = 8082; + let remote_addr1 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), PLAYER1_PORT); + let remote_addr2 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), REMOTE_PORT); + return ( + TestPlayer { + handle: 0, + address: remote_addr1, + }, + TestPlayer { + handle: 1, + address: remote_addr2, + }, + ); +} + +fn start_session( + local_player: &TestPlayer, + remote_player: &TestPlayer, +) -> Result, Box> { + let mut session_builder = SessionBuilder::::new() + .with_num_players(2) + .with_max_prediction_window(12) // (optional) set max prediction window + .with_input_delay(2); // (optional) set input delay for the local player + session_builder = session_builder.add_player(PlayerType::Local, local_player.handle)?; + session_builder = session_builder.add_player( + PlayerType::Remote(remote_player.address), + remote_player.handle, + )?; + let socket = UdpNonBlockingSocket::bind_to_port(local_player.address.port())?; + let session = session_builder.start_p2p_session(socket)?; + Ok(session) +} + +const INPUT_UP: u8 = 1 << 0; + +pub fn read_local_inputs( + mut commands: Commands, + keyboard_input: Res>, + local_players: Res, +) { + let mut local_inputs = HashMap::new(); + + for handle in &local_players.0 { + let mut input: u8 = 0; + if keyboard_input.pressed(KeyCode::W) { + input |= INPUT_UP; + } + local_inputs.insert(*handle, BoxInput { inp: input }); + } + + commands.insert_resource(LocalInputs::(local_inputs)); +} + +pub fn increase_frame_system(mut frame_count: ResMut) { + frame_count.frame += 1; +} + +fn press_key(app: &mut App, key: KeyCode) { + app.world.send_event(KeyboardInput { + scan_code: 0, + key_code: Option::from(key), + state: ButtonState::Pressed, + window: Entity::PLACEHOLDER, + }); +} + +#[derive(Component, Clone, Copy, Default)] +pub struct PlayerComponent { + pub handle: usize, +} + +#[derive(Resource, Default, Reflect, Hash)] +#[reflect(Hash)] +pub struct FrameCount { + pub frame: u32, +} + +// Components that should be saved/loaded need to implement the `Reflect` trait +#[derive(Default, Reflect, Component)] +pub struct Velocity { + pub x: f32, + pub y: f32, + pub z: f32, +} + +pub fn move_player_system( + mut query: Query<(&mut Transform, &mut Velocity, &PlayerComponent), With>, + inputs: Res>, +) { + const MOVEMENT_SPEED: f32 = 0.1; + for (mut t, mut v, p) in query.iter_mut() { + let input = inputs[p.handle].0.inp; + if input & INPUT_UP != 0 { + v.z -= MOVEMENT_SPEED; + } + t.translation.z += v.z; + } +} + +pub fn spawn_players(mut commands: Commands, session: Res>) { + let num_players = match &*session { + Session::SyncTest(s) => s.num_players(), + Session::P2P(s) => s.num_players(), + Session::Spectator(s) => s.num_players(), + }; + + for handle in 0..num_players { + commands + .spawn(( + PlayerComponent { handle }, + Velocity::default(), + Transform::default(), + )) + .add_rollback(); + } +}