add unit tests for replication system

- make tracy experience better by adding a 0.05 to client local TIME.
- fix an error that the look_dir was wrongly predicted
- add a jump graph for testing
- update in_game code that was commented out in system
- track the simulation ahead on the debug menu
- add simulated lag with `sudo tc qdisc replace dev lo root netem delay 700ms 10ms 25%`
add basic tests for phys
This commit is contained in:
Marcel Märtens 2022-03-08 20:09:37 +01:00
parent 714c346ded
commit 4343dd3aea
9 changed files with 340 additions and 10 deletions

View File

@ -1734,7 +1734,7 @@ impl Client {
prof_span!("handle and send inputs"); prof_span!("handle and send inputs");
self.next_control.inputs = inputs; self.next_control.inputs = inputs;
let con = std::mem::take(&mut self.next_control); let con = std::mem::take(&mut self.next_control);
let time = Duration::from_secs_f64(self.state.ecs().read_resource::<Time>().0); let time = Duration::from_secs_f64(self.state.ecs().read_resource::<Time>().0) + dt;
let monotonic_time = let monotonic_time =
Duration::from_secs_f64(self.state.ecs().read_resource::<MonotonicTime>().0); Duration::from_secs_f64(self.state.ecs().read_resource::<MonotonicTime>().0);
let rcon = self.local_command_gen.gen(time, con); let rcon = self.local_command_gen.gen(time, con);
@ -1748,6 +1748,7 @@ impl Client {
rc.push(rcon); rc.push(rcon);
rc.prepare_commands(monotonic_time) rc.prepare_commands(monotonic_time)
}); });
match commands { match commands {
Ok(commands) => self.send_msg_err(ClientGeneral::Control(commands))?, Ok(commands) => self.send_msg_err(ClientGeneral::Control(commands))?,
Err(e) => { Err(e) => {
@ -2158,15 +2159,16 @@ impl Client {
match msg { match msg {
ServerGeneral::TimeSync(time) => { ServerGeneral::TimeSync(time) => {
let dt = self.state.ecs().read_resource::<DeltaTime>().0 as f64; let dt = self.state.ecs().read_resource::<DeltaTime>().0 as f64;
let latency = self let simulate_ahead = self
.state .state
.ecs() .ecs()
.read_storage::<RemoteController>() .read_storage::<RemoteController>()
.get(self.entity()) .get(self.entity())
.map(|rc| rc.avg_latency()) .map(|rc| rc.simulate_ahead())
.unwrap_or_default(); .unwrap_or_default();
//remove dt as it is applied in state.tick again //remove dt as it is applied in state.tick again
self.state.ecs().write_resource::<Time>().0 = time.0 + latency.as_secs_f64() /* - dt */; self.state.ecs().write_resource::<Time>().0 =
time.0 + simulate_ahead.as_secs_f64() - dt;
}, },
ServerGeneral::AckControl(acked_ids, _time) => { ServerGeneral::AckControl(acked_ids, _time) => {
if let Some(remote_controller) = self if let Some(remote_controller) = self

View File

@ -110,7 +110,7 @@ pub use self::{
player::{AliasError, Player, MAX_ALIAS_LEN}, player::{AliasError, Player, MAX_ALIAS_LEN},
poise::{Poise, PoiseChange, PoiseState}, poise::{Poise, PoiseChange, PoiseState},
projectile::{Projectile, ProjectileConstructor}, projectile::{Projectile, ProjectileConstructor},
remote_controller::{CommandGenerator, ControlCommands, RemoteController}, remote_controller::{CommandGenerator, ControlCommand, ControlCommands, RemoteController},
shockwave::{Shockwave, ShockwaveHitEntities}, shockwave::{Shockwave, ShockwaveHitEntities},
skillset::{ skillset::{
skills::{self, Skill}, skills::{self, Skill},

View File

@ -4,7 +4,6 @@ use serde::{Deserialize, Serialize};
use specs::{Component, DenseVecStorage}; use specs::{Component, DenseVecStorage};
use std::{collections::VecDeque, time::Duration}; use std::{collections::VecDeque, time::Duration};
use tracing::warn; use tracing::warn;
use vek::Vec3;
pub type ControlCommands = VecDeque<ControlCommand>; pub type ControlCommands = VecDeque<ControlCommand>;
@ -164,7 +163,7 @@ impl RemoteController {
return None; return None;
} }
let mut result = Controller::default(); let mut result = Controller::default();
let mut look_dir = Vec3::zero(); let mut look_dir = result.inputs.look_dir.to_vec();
//if self.commands[start_i].source_time //if self.commands[start_i].source_time
// Inputs are averaged over all elements by time // Inputs are averaged over all elements by time
// Queued Inputs are just added // Queued Inputs are just added
@ -182,8 +181,7 @@ impl RemoteController {
let local_dur = local_end - local_start; let local_dur = local_end - local_start;
result.inputs.move_dir += e.msg.inputs.move_dir * local_dur.as_secs_f32(); result.inputs.move_dir += e.msg.inputs.move_dir * local_dur.as_secs_f32();
result.inputs.move_z += e.msg.inputs.move_z * local_dur.as_secs_f32(); result.inputs.move_z += e.msg.inputs.move_z * local_dur.as_secs_f32();
look_dir = result.inputs.look_dir.to_vec() look_dir = look_dir + e.msg.inputs.look_dir.to_vec() * local_dur.as_secs_f32();
+ e.msg.inputs.look_dir.to_vec() * local_dur.as_secs_f32();
//TODO: manually combine 70% up and 30% down to UP //TODO: manually combine 70% up and 30% down to UP
result.inputs.climb = result.inputs.climb.or(e.msg.inputs.climb); result.inputs.climb = result.inputs.climb.or(e.msg.inputs.climb);
result.inputs.break_block_pos = result result.inputs.break_block_pos = result
@ -230,6 +228,11 @@ impl RemoteController {
/// server->client and assume that this is also true for client->server /// server->client and assume that this is also true for client->server
/// latency /// latency
pub fn avg_latency(&self) -> Duration { self.avg_latency } pub fn avg_latency(&self) -> Duration { self.avg_latency }
pub fn simulate_ahead(&self) -> Duration {
const FIXED_OFFSET: Duration = Duration::from_millis(50);
self.avg_latency() + FIXED_OFFSET
}
} }
impl Default for RemoteController { impl Default for RemoteController {

View File

@ -1025,6 +1025,7 @@ pub fn handle_jump(
_update: &mut StateUpdate, _update: &mut StateUpdate,
strength: f32, strength: f32,
) -> bool { ) -> bool {
common_base::plot!("jumps", 0.0);
(input_is_pressed(data, InputKind::Jump) && data.physics.on_ground.is_some()) (input_is_pressed(data, InputKind::Jump) && data.physics.on_ground.is_some())
.then(|| data.body.jump_impulse()) .then(|| data.body.jump_impulse())
.flatten() .flatten()
@ -1033,6 +1034,7 @@ pub fn handle_jump(
data.entity, data.entity,
strength * impulse / data.mass.0 * data.stats.move_speed_modifier, strength * impulse / data.mass.0 * data.stats.move_speed_modifier,
)); ));
common_base::plot!("jumps", 1.0);
}) })
.is_some() .is_some()
} }

View File

@ -0,0 +1,204 @@
use crate::utils;
use approx::assert_relative_eq;
use common::{
comp::{self, CommandGenerator, Controller, InputKind},
resources::Time,
};
use specs::WorldExt;
use std::{error::Error, time::Duration};
use utils::{DT, DTT};
use vek::{approx, Vec2};
use veloren_common_systems::add_local_systems;
#[test]
fn simple_run() {
let mut state = utils::setup();
utils::create_player(&mut state);
state.tick(
DT,
|dispatcher_builder| {
add_local_systems(dispatcher_builder);
},
false,
);
}
#[test]
fn emulate_walk() -> Result<(), Box<dyn Error>> {
let mut state = utils::setup();
let p1 = utils::create_player(&mut state);
utils::tick(&mut state, DT);
assert_eq!(state.ecs_mut().read_resource::<Time>().0, DTT);
let mut generator = CommandGenerator::default();
let mut actions = Controller::default();
actions.inputs.move_dir = Vec2::new(10.0, 0.0);
utils::push_remote(&mut state, p1, generator.gen(DT * 3, actions))?;
let mut actions = Controller::default();
actions.inputs.move_dir = Vec2::new(20.0, 0.0);
utils::push_remote(&mut state, p1, generator.gen(DT * 5, actions))?;
let mut actions = Controller::default();
actions.inputs.move_dir = Vec2::new(30.0, 0.0);
utils::push_remote(&mut state, p1, generator.gen(DT * 6, actions))?;
//DDT 2 - no data yet
utils::tick(&mut state, DT);
assert_relative_eq!(utils::get_controller(&state, p1)?.inputs.move_dir.x, 0.0);
//DDT 3
utils::tick(&mut state, DT);
assert_relative_eq!(utils::get_controller(&state, p1)?.inputs.move_dir.x, 10.0);
//DDT 4
utils::tick(&mut state, DT);
assert_relative_eq!(utils::get_controller(&state, p1)?.inputs.move_dir.x, 10.0);
//DDT 5
utils::tick(&mut state, DT);
assert_relative_eq!(utils::get_controller(&state, p1)?.inputs.move_dir.x, 20.0);
//DDT 6
utils::tick(&mut state, DT);
assert_relative_eq!(utils::get_controller(&state, p1)?.inputs.move_dir.x, 30.0);
//DDT 7
utils::tick(&mut state, DT);
assert_relative_eq!(utils::get_controller(&state, p1)?.inputs.move_dir.x, 30.0);
Ok(())
}
#[test]
fn emulate_partial_client_walk() -> Result<(), Box<dyn Error>> {
let mut state = utils::setup();
let p1 = utils::create_player(&mut state);
utils::tick(&mut state, DT);
assert_eq!(state.ecs_mut().read_resource::<Time>().0, DTT);
let mut generator = CommandGenerator::default();
let mut actions = Controller::default();
actions.inputs.move_dir = Vec2::new(10.0, 0.0);
utils::push_remote(
&mut state,
p1,
generator.gen(Duration::from_secs_f64(2.5 * DTT), actions),
)?;
//DDT 2 - no data yet officially, but we interpret
// Explaination why we interpolate here:
// - Client says move at 2.5, server is at 2.0 currently.
// - Assume there was no interpolation:
// - Client would assume to be at pos 5 at 3.0 and 10 at 3.5
// - Server would notice that client started moving at 3.0 and is at 5 at 3.5
// => Client would lag 5 behind always
// - Now with interpolation:
// - Client would assume to be at pos 5 at 3.0 and 10 at 3.5
// - Server lets client run with speed of 5 at 2.0, so
utils::tick(&mut state, DT);
assert_relative_eq!(utils::get_controller(&state, p1)?.inputs.move_dir.x, 5.0);
//DDT 3
utils::tick(&mut state, DT);
assert_relative_eq!(utils::get_controller(&state, p1)?.inputs.move_dir.x, 10.0);
//DDT 4
utils::tick(&mut state, DT);
assert_relative_eq!(utils::get_controller(&state, p1)?.inputs.move_dir.x, 10.0);
Ok(())
}
#[test]
fn emulate_partial_server_walk() -> Result<(), Box<dyn Error>> {
let mut state = utils::setup();
let p1 = utils::create_player(&mut state);
utils::tick(&mut state, DT);
assert_eq!(state.ecs_mut().read_resource::<Time>().0, DTT);
let mut generator = CommandGenerator::default();
let mut actions = Controller::default();
actions.inputs.move_dir = Vec2::new(10.0, 0.0);
utils::push_remote(
&mut state,
p1,
generator.gen(Duration::from_secs_f64(3.0 * DTT), actions),
)?;
//DDT 1.5 - no data yet
utils::tick(&mut state, Duration::from_secs_f64(0.5 * DTT));
assert_relative_eq!(utils::get_controller(&state, p1)?.inputs.move_dir.x, 0.0);
//DDT 2.5/3.5 - No Data for first 0.5, then 10
// We interpolate here to 5 for the whole tick
utils::tick(&mut state, DT);
assert_relative_eq!(utils::get_controller(&state, p1)?.inputs.move_dir.x, 5.0);
//DDT 3
utils::tick(&mut state, DT);
assert_relative_eq!(utils::get_controller(&state, p1)?.inputs.move_dir.x, 10.0);
//DDT 4
utils::tick(&mut state, DT);
assert_relative_eq!(utils::get_controller(&state, p1)?.inputs.move_dir.x, 10.0);
Ok(())
}
#[test]
fn emulate_jump() -> Result<(), Box<dyn Error>> {
let mut state = utils::setup();
let p1 = utils::create_player(&mut state);
utils::tick(&mut state, DT);
assert_eq!(state.ecs_mut().read_resource::<Time>().0, DTT);
let mut generator = CommandGenerator::default();
let mut actions = Controller::default();
actions
.queued_inputs
.insert(comp::InputKind::Jump, comp::InputAttr {
select_pos: None,
target_entity: None,
});
utils::push_remote(
&mut state,
p1,
generator.gen(Duration::from_secs_f64(5.0 * DTT), actions),
)?;
//DDT 2
utils::tick(&mut state, DT);
assert!(utils::get_controller(&state, p1)?.queued_inputs.is_empty());
//DDT 3
utils::tick(&mut state, DT);
assert!(utils::get_controller(&state, p1)?.queued_inputs.is_empty());
//DDT 4
// TODO: i am NOT sure if this is correct behavior, just adjusted it in a rebase
// to fix the test
utils::tick(&mut state, DT);
let inputs = utils::get_controller(&state, p1)?.queued_inputs;
assert!(!inputs.is_empty());
assert!(inputs.contains_key(&InputKind::Jump));
//DDT 5
utils::tick(&mut state, DT);
let inputs = utils::get_controller(&state, p1)?.queued_inputs;
assert!(!inputs.is_empty());
assert!(inputs.contains_key(&InputKind::Jump));
//DDT 6
utils::tick(&mut state, DT);
assert!(utils::get_controller(&state, p1)?.queued_inputs.is_empty());
//DDT 7
utils::tick(&mut state, DT);
assert!(utils::get_controller(&state, p1)?.queued_inputs.is_empty());
Ok(())
}

View File

@ -0,0 +1,2 @@
mod basic;
mod utils;

View File

@ -0,0 +1,99 @@
use common::{
comp::{
inventory::item::MaterialStatManifest, tool::AbilityMap, ControlCommand, ControlCommands,
Controller, RemoteController,
},
resources::{DeltaTime, GameMode, Time},
terrain::{MapSizeLg, TerrainChunk},
};
use common_ecs::dispatch;
use common_net::sync::WorldSyncExt;
use common_state::State;
use hashbrown::HashSet;
use specs::{Builder, Entity, WorldExt};
use std::{error::Error, sync::Arc, time::Duration};
use vek::Vec2;
use veloren_common_systems::predict_controller;
pub const DTT: f64 = 1.0 / 10.0;
pub const DT: Duration = Duration::from_millis(100);
const DEFAULT_WORLD_CHUNKS_LG: MapSizeLg =
if let Ok(map_size_lg) = MapSizeLg::new(Vec2 { x: 10, y: 10 }) {
map_size_lg
} else {
panic!("Default world chunk size does not satisfy required invariants.");
};
pub fn setup() -> State {
let pools = State::pools(GameMode::Server);
let mut state = State::new(
GameMode::Server,
pools,
DEFAULT_WORLD_CHUNKS_LG,
Arc::new(TerrainChunk::water(0)),
);
state.ecs_mut().insert(MaterialStatManifest::with_empty());
state.ecs_mut().insert(AbilityMap::load().cloned());
state.ecs_mut().read_resource::<Time>();
state.ecs_mut().read_resource::<DeltaTime>();
state
}
pub fn tick(state: &mut State, dt: Duration) {
state.tick(
dt,
|dispatch_builder| {
dispatch::<predict_controller::Sys>(dispatch_builder, &[]);
},
false,
);
}
pub fn push_remote(
state: &mut State,
entity: Entity,
command: ControlCommand,
) -> Result<u64, Box<dyn Error>> {
let mut storage = state.ecs_mut().write_storage::<RemoteController>();
let remote_controller = storage.get_mut(entity).ok_or(Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
"Storage does not contain Entity RemoteController",
)))?;
remote_controller
.push(command)
.ok_or(Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
"Command couldn't be pushed",
)))
}
pub fn get_controller(state: &State, entity: Entity) -> Result<Controller, Box<dyn Error>> {
let storage = state.ecs().read_storage::<Controller>();
let controller = storage.get(entity).ok_or(Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
"Storage does not contain Entity Controller",
)))?;
Ok(controller.clone())
}
#[allow(dead_code)]
pub fn push_remotes(state: &mut State, entity: Entity, commands: ControlCommands) -> HashSet<u64> {
let mut storage = state.ecs_mut().write_storage::<RemoteController>();
let remote_controller = storage.get_mut(entity).unwrap();
remote_controller.append(commands)
}
pub fn create_player(state: &mut State) -> Entity {
let remote_controller = RemoteController::default();
let controller = Controller::default();
state
.ecs_mut()
.create_entity_synced()
.with(remote_controller)
.with(controller)
.build()
}

View File

@ -261,6 +261,7 @@ widget_ids! {
debug_bg, debug_bg,
fps_counter, fps_counter,
ping, ping,
simulate_ahead,
coordinates, coordinates,
velocity, velocity,
glide_ratio, glide_ratio,
@ -654,6 +655,7 @@ pub struct DebugInfo {
pub tps: f64, pub tps: f64,
pub frame_time: Duration, pub frame_time: Duration,
pub ping_ms: f64, pub ping_ms: f64,
pub simulate_ahead: f64,
pub coordinates: Option<comp::Pos>, pub coordinates: Option<comp::Pos>,
pub velocity: Option<comp::Vel>, pub velocity: Option<comp::Vel>,
pub ori: Option<comp::Ori>, pub ori: Option<comp::Ori>,
@ -2501,6 +2503,16 @@ impl Hud {
.font_id(self.fonts.cyri.conrod_id) .font_id(self.fonts.cyri.conrod_id)
.font_size(self.fonts.cyri.scale(14)) .font_size(self.fonts.cyri.scale(14))
.set(self.ids.ping, ui_widgets); .set(self.ids.ping, ui_widgets);
// Simulate Ahead timing
Text::new(&format!(
"Simulate Ahead: {:.0}ms",
debug_info.simulate_ahead * 1000.0
))
.color(TEXT_COLOR)
.down_from(self.ids.ping, V_PAD)
.font_id(self.fonts.cyri.conrod_id)
.font_size(self.fonts.cyri.scale(14))
.set(self.ids.simulate_ahead, ui_widgets);
// Player's position // Player's position
let coordinates_text = match debug_info.coordinates { let coordinates_text = match debug_info.coordinates {
Some(coordinates) => format!( Some(coordinates) => format!(
@ -2511,7 +2523,7 @@ impl Hud {
}; };
Text::new(&coordinates_text) Text::new(&coordinates_text)
.color(TEXT_COLOR) .color(TEXT_COLOR)
.down_from(self.ids.ping, V_PAD) .down_from(self.ids.simulate_ahead, V_PAD)
.font_id(self.fonts.cyri.conrod_id) .font_id(self.fonts.cyri.conrod_id)
.font_size(self.fonts.cyri.scale(14)) .font_size(self.fonts.cyri.scale(14))
.set(self.ids.coordinates, ui_widgets); .set(self.ids.coordinates, ui_widgets);

View File

@ -1334,11 +1334,17 @@ impl PlayState for SessionState {
.read_storage::<comp::CharacterState>() .read_storage::<comp::CharacterState>()
.get(entity) .get(entity)
.cloned(); .cloned();
let simulate_ahead = ecs
.read_storage::<comp::RemoteController>()
.get(entity)
.map(|rc| rc.simulate_ahead().as_secs_f64())
.unwrap_or_default();
DebugInfo { DebugInfo {
tps: global_state.clock.stats().average_tps, tps: global_state.clock.stats().average_tps,
frame_time: global_state.clock.stats().average_busy_dt, frame_time: global_state.clock.stats().average_busy_dt,
ping_ms: self.client.borrow().get_ping_ms_rolling_avg(), ping_ms: self.client.borrow().get_ping_ms_rolling_avg(),
simulate_ahead,
coordinates, coordinates,
velocity, velocity,
ori, ori,