mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
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:
parent
714c346ded
commit
4343dd3aea
@ -1734,7 +1734,7 @@ impl Client {
|
||||
prof_span!("handle and send inputs");
|
||||
self.next_control.inputs = inputs;
|
||||
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 =
|
||||
Duration::from_secs_f64(self.state.ecs().read_resource::<MonotonicTime>().0);
|
||||
let rcon = self.local_command_gen.gen(time, con);
|
||||
@ -1748,6 +1748,7 @@ impl Client {
|
||||
rc.push(rcon);
|
||||
rc.prepare_commands(monotonic_time)
|
||||
});
|
||||
|
||||
match commands {
|
||||
Ok(commands) => self.send_msg_err(ClientGeneral::Control(commands))?,
|
||||
Err(e) => {
|
||||
@ -2158,15 +2159,16 @@ impl Client {
|
||||
match msg {
|
||||
ServerGeneral::TimeSync(time) => {
|
||||
let dt = self.state.ecs().read_resource::<DeltaTime>().0 as f64;
|
||||
let latency = self
|
||||
let simulate_ahead = self
|
||||
.state
|
||||
.ecs()
|
||||
.read_storage::<RemoteController>()
|
||||
.get(self.entity())
|
||||
.map(|rc| rc.avg_latency())
|
||||
.map(|rc| rc.simulate_ahead())
|
||||
.unwrap_or_default();
|
||||
//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) => {
|
||||
if let Some(remote_controller) = self
|
||||
|
@ -110,7 +110,7 @@ pub use self::{
|
||||
player::{AliasError, Player, MAX_ALIAS_LEN},
|
||||
poise::{Poise, PoiseChange, PoiseState},
|
||||
projectile::{Projectile, ProjectileConstructor},
|
||||
remote_controller::{CommandGenerator, ControlCommands, RemoteController},
|
||||
remote_controller::{CommandGenerator, ControlCommand, ControlCommands, RemoteController},
|
||||
shockwave::{Shockwave, ShockwaveHitEntities},
|
||||
skillset::{
|
||||
skills::{self, Skill},
|
||||
|
@ -4,7 +4,6 @@ use serde::{Deserialize, Serialize};
|
||||
use specs::{Component, DenseVecStorage};
|
||||
use std::{collections::VecDeque, time::Duration};
|
||||
use tracing::warn;
|
||||
use vek::Vec3;
|
||||
|
||||
pub type ControlCommands = VecDeque<ControlCommand>;
|
||||
|
||||
@ -164,7 +163,7 @@ impl RemoteController {
|
||||
return None;
|
||||
}
|
||||
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
|
||||
// Inputs are averaged over all elements by time
|
||||
// Queued Inputs are just added
|
||||
@ -182,8 +181,7 @@ impl RemoteController {
|
||||
let local_dur = local_end - local_start;
|
||||
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();
|
||||
look_dir = result.inputs.look_dir.to_vec()
|
||||
+ e.msg.inputs.look_dir.to_vec() * local_dur.as_secs_f32();
|
||||
look_dir = look_dir + e.msg.inputs.look_dir.to_vec() * local_dur.as_secs_f32();
|
||||
//TODO: manually combine 70% up and 30% down to UP
|
||||
result.inputs.climb = result.inputs.climb.or(e.msg.inputs.climb);
|
||||
result.inputs.break_block_pos = result
|
||||
@ -230,6 +228,11 @@ impl RemoteController {
|
||||
/// server->client and assume that this is also true for client->server
|
||||
/// 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 {
|
||||
|
@ -1025,6 +1025,7 @@ pub fn handle_jump(
|
||||
_update: &mut StateUpdate,
|
||||
strength: f32,
|
||||
) -> bool {
|
||||
common_base::plot!("jumps", 0.0);
|
||||
(input_is_pressed(data, InputKind::Jump) && data.physics.on_ground.is_some())
|
||||
.then(|| data.body.jump_impulse())
|
||||
.flatten()
|
||||
@ -1033,6 +1034,7 @@ pub fn handle_jump(
|
||||
data.entity,
|
||||
strength * impulse / data.mass.0 * data.stats.move_speed_modifier,
|
||||
));
|
||||
common_base::plot!("jumps", 1.0);
|
||||
})
|
||||
.is_some()
|
||||
}
|
||||
|
204
common/systems/tests/predict_controller/basic.rs
Normal file
204
common/systems/tests/predict_controller/basic.rs
Normal 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(())
|
||||
}
|
2
common/systems/tests/predict_controller/main.rs
Normal file
2
common/systems/tests/predict_controller/main.rs
Normal file
@ -0,0 +1,2 @@
|
||||
mod basic;
|
||||
mod utils;
|
99
common/systems/tests/predict_controller/utils.rs
Normal file
99
common/systems/tests/predict_controller/utils.rs
Normal 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()
|
||||
}
|
@ -261,6 +261,7 @@ widget_ids! {
|
||||
debug_bg,
|
||||
fps_counter,
|
||||
ping,
|
||||
simulate_ahead,
|
||||
coordinates,
|
||||
velocity,
|
||||
glide_ratio,
|
||||
@ -654,6 +655,7 @@ pub struct DebugInfo {
|
||||
pub tps: f64,
|
||||
pub frame_time: Duration,
|
||||
pub ping_ms: f64,
|
||||
pub simulate_ahead: f64,
|
||||
pub coordinates: Option<comp::Pos>,
|
||||
pub velocity: Option<comp::Vel>,
|
||||
pub ori: Option<comp::Ori>,
|
||||
@ -2501,6 +2503,16 @@ impl Hud {
|
||||
.font_id(self.fonts.cyri.conrod_id)
|
||||
.font_size(self.fonts.cyri.scale(14))
|
||||
.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
|
||||
let coordinates_text = match debug_info.coordinates {
|
||||
Some(coordinates) => format!(
|
||||
@ -2511,7 +2523,7 @@ impl Hud {
|
||||
};
|
||||
Text::new(&coordinates_text)
|
||||
.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_size(self.fonts.cyri.scale(14))
|
||||
.set(self.ids.coordinates, ui_widgets);
|
||||
|
@ -1334,11 +1334,17 @@ impl PlayState for SessionState {
|
||||
.read_storage::<comp::CharacterState>()
|
||||
.get(entity)
|
||||
.cloned();
|
||||
let simulate_ahead = ecs
|
||||
.read_storage::<comp::RemoteController>()
|
||||
.get(entity)
|
||||
.map(|rc| rc.simulate_ahead().as_secs_f64())
|
||||
.unwrap_or_default();
|
||||
|
||||
DebugInfo {
|
||||
tps: global_state.clock.stats().average_tps,
|
||||
frame_time: global_state.clock.stats().average_busy_dt,
|
||||
ping_ms: self.client.borrow().get_ping_ms_rolling_avg(),
|
||||
simulate_ahead,
|
||||
coordinates,
|
||||
velocity,
|
||||
ori,
|
||||
|
Loading…
Reference in New Issue
Block a user