M crates/unified/assets/config/world.wc.toml => crates/unified/assets/config/world.wc.toml +1 -1
@@ 3,7 3,7 @@
gravity = 500.0
gravity_iterations = 8
spawn_parts_at = "Earth"
-spawn_parts_interval_secs = 1
+spawn_parts_interval_secs = 1000
orbit_scale_factor = 4.0
[part]
M crates/unified/src/client/components/mod.rs => crates/unified/src/client/components/mod.rs +13 -0
@@ 1,3 1,5 @@
+use std::collections::VecDeque;
+use bevy::prelude::{Deref, DerefMut};
use crate::prelude::{Component, Resource};
#[derive(Component)]
@@ 11,3 13,14 @@ pub struct PowerText;
#[derive(Component)]
pub struct Me;
+
+#[derive(Resource, Default)]
+pub struct ServerClock {
+ pub rtt_queue: VecDeque<f64>,
+ pub rtt: f64,
+ pub time_offset_queue: VecDeque<f64>,
+ pub time_offset: f64,
+}
+
+#[derive(Resource, Default, Deref, DerefMut)]
+pub struct ServerTimeOffset(f64);<
\ No newline at end of file
M crates/unified/src/client/interpolation.rs => crates/unified/src/client/interpolation.rs +53 -15
@@ 1,7 1,14 @@
+use std::collections::VecDeque;
use std::f32::consts::PI;
use std::time::{Duration};
use avian2d::parry::transformation::utils::transform;
+use bevy_replicon::client::confirm_history::ConfirmHistory;
+use crate::client::components::{ServerClock, ServerTimeOffset};
use crate::prelude::*;
+use crate::shared::plugins::TICK_RATE;
+
+/// interpolation period in seconds
+const INTERP: f64 = 0.150;
pub fn interpolation_plugin(app: &mut App) {
app
@@ 12,10 19,7 @@ pub fn interpolation_plugin(app: &mut App) {
#[derive(Component, Debug)]
pub struct TranslationInterpolationInfo {
- pub last_dt: Duration,
- pub this_tick_start: bevy::platform::time::Instant,
- pub latest_position: Vec2,
- pub last_position: Vec2,
+ pub positions: VecDeque<(f64, Vec2)>,
}
#[derive(Component, Debug)]
pub struct RotationInterpolationInfo {
@@ 27,14 31,30 @@ pub struct RotationInterpolationInfo {
}
fn update_interpolation_info(
- mut interpolation_pos_query: Query<(Entity, &Position, &mut TranslationInterpolationInfo), Changed<Position>>,
+ mut interpolation_pos_query: Query<(Entity, &Position, &mut TranslationInterpolationInfo, &ConfirmHistory), Changed<Position>>,
mut interpolation_rot_query: Query<(&Rotation, &AngularVelocity, &mut RotationInterpolationInfo), Changed<Rotation>>,
+ server_clock: Res<ServerClock>,
+ server_time_offset: Res<ServerTimeOffset>,
) {
- for (entity, position, mut info) in interpolation_pos_query.iter_mut() {
- info.last_dt = info.this_tick_start.elapsed();
- info.this_tick_start = bevy::platform::time::Instant::now();
- info.last_position = info.latest_position;
- info.latest_position = position.as_vec2();
+ for (entity, position, mut info, confirm_history) in interpolation_pos_query.iter_mut() {
+ let now = confirm_history.last_tick().get() as f64 / TICK_RATE - **server_time_offset/* - server_clock.rtt/2.0 - server_clock.time_offset*/;
+ //let now = confirm_history.last_tick().get() as f32 / TICK_RATE as f32;
+ info.positions.push_back((now, position.as_vec2()));
+ let mut last_over_time = 0;
+ for (i, (time, _)) in info.positions.iter().enumerate() {
+ if *time < now - INTERP {
+ last_over_time = i;
+ }
+ }
+ if last_over_time > 0 {
+ //debug!("pop");
+ info.positions.drain(..last_over_time);
+ }
+ //debug!("{:?}", info.positions)
+ /*if let Some((time, _)) = info.positions.get(0) && now.duration_since(*time) > INTERP {
+ debug!("pop");
+ pos_info.positions.pop_front();
+ }*/
}
for (rotation, angular_velocity, mut info) in interpolation_rot_query.iter_mut() {
info.last_dt = info.this_tick_start.elapsed();
@@ 62,12 82,30 @@ fn sync_non_interpolated_transforms(
}
}
fn do_interpolation(
- mut interpolation_query: Query<(Entity, &mut Transform, &TranslationInterpolationInfo, &RotationInterpolationInfo)>,
+ mut interpolation_query: Query<(Entity, &mut Transform, &mut TranslationInterpolationInfo, &RotationInterpolationInfo)>,
+ time: Res<Time>,
) {
- for (entity, mut transform, pos_info, rot_info) in &mut interpolation_query {
- let dt = bevy::platform::time::Instant::now() - pos_info.this_tick_start;
- let progress = dt.as_secs_f32() / pos_info.last_dt.as_secs_f32(); // should be between 0.0 and 1.0
- transform.translation = (pos_info.last_position + progress * (pos_info.latest_position - pos_info.last_position)).extend(0.0);
+ for (entity, mut transform, mut pos_info, rot_info) in &mut interpolation_query {
+ let now = time.elapsed().as_secs_f64();
+
+ let mut time_after_now = 0;
+ for (i, (time, _)) in pos_info.positions.iter().enumerate() {
+ if *time > now - INTERP {
+ time_after_now = i;
+ break;
+ }
+ }
+ // this should not happen, but is necessary to prevent a panic
+ if time_after_now == 0 { continue }
+ // we need to have 2 packets to reference, so continuing if we don't have that is a-ok
+ let Some(first_time) = pos_info.positions.get(time_after_now-1) else { continue };
+ let Some(second_time) = pos_info.positions.get(time_after_now) else { continue };
+ let elapsed = now - INTERP - first_time.0;
+ let dt = second_time.0 - first_time.0;
+ let progress = elapsed / dt; // should be between 0.0 and 1.0
+ if progress < 0.0 || progress > 1.0 { continue }
+ //debug!("{:?} {:?} {:?} {}", entity, first_time.1, second_time.1, progress);
+ transform.translation = (first_time.1 + progress as f32 * (second_time.1 - first_time.1)).extend(0.0);
let dt = bevy::platform::time::Instant::now() - rot_info.this_tick_start;
let progress = dt.as_secs_f32() / rot_info.last_dt.as_secs_f32(); // should be between 0.0 and 1.0
M crates/unified/src/client/mod.rs => crates/unified/src/client/mod.rs +13 -6
@@ 12,9 12,9 @@ use starguide::components::StarguideGizmos;
use bevy::dev_tools::picking_debug::DebugPickingMode;
use crate::prelude::*;
use planet::incoming_planets::incoming_planets_plugin;
-use crate::client::components::Me;
+use crate::client::components::{Me, ServerClock, ServerTimeOffset};
use crate::client::ship::attachment::client_attachment_plugin;
-use crate::shared::ecs::{GameplayState, TimeOffset};
+use crate::shared::ecs::GameplayState;
use crate::shared::net::{parse_management_address, ManagementInfo, Hi, STARKINGDOMS_PROTOCOL_MAGIC};
use bevy_replicon::prelude::RepliconChannels;
use bevy_replicon_renet2::RenetChannelsExt;
@@ 43,6 43,7 @@ use wasm_bindgen::JsCast;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_futures::JsFuture;
use crate::client::interpolation::interpolation_plugin;
+use crate::client::server_clock::server_clock_plugin;
use crate::shared::config::planet::Planet;
pub mod colors;
@@ 60,6 61,7 @@ pub mod crafting;
pub mod components;
pub mod plugins;
pub mod interpolation;
+pub mod server_clock;
pub struct ClientPlugin {
pub server: Option<String>
@@ 84,11 86,11 @@ impl Plugin for ClientPlugin {
//.add_plugins(starguide_orbit_plugin)
.add_plugins(crafting_ui_plugin)
.add_plugins(interpolation_plugin)
+ .add_plugins(server_clock_plugin)
.add_systems(Update, find_me)
.insert_state(GameplayState::Main)
.insert_resource(DebugPickingMode::Disabled)
- .insert_resource(TimeOffset::default());
-
+ .insert_resource(ServerTimeOffset::default());
let server = self.server.clone();
#[cfg(not(target_arch = "wasm32"))]
@@ 233,12 235,17 @@ fn connect_when_ready(mut commands: Commands, channels: Res<RepliconChannels>) {
pub fn find_me(
mut msgs: MessageReader<Hi>,
mut commands: Commands,
- mut time_offset: ResMut<TimeOffset>,
+ mut time_offset: ResMut<ServerTimeOffset>,
+ time: Res<Time>,
) {
for msg in msgs.read() {
let we_are = msg.you_are;
info!(?we_are, "joined successfully");
commands.entity(we_are).insert(Me);
- time_offset.0 = msg.time_offset;
+ let Ok(now) = web_time::SystemTime::now().duration_since(web_time::UNIX_EPOCH) else {
+ warn!("Could not get system time");
+ continue
+ };
+ **time_offset = msg.time_offset - time.elapsed_secs_f64();
}
}
M crates/unified/src/client/parts.rs => crates/unified/src/client/parts.rs +2 -4
@@ 1,3 1,4 @@
+use std::collections::VecDeque;
use std::f32::consts::PI;
use std::time::Duration;
use crate::shared::attachment::{Joint, JointOf, Joints, PartInShip, Peer, SnapOf, SnapOfJoint};
@@ 61,10 62,7 @@ fn handle_incoming_parts(
.insert(sprite)
.insert(Pickable::default())
.insert(TranslationInterpolationInfo {
- last_dt: Duration::from_millis(50), // assume it was 20tps because we don't know
- this_tick_start: bevy::platform::time::Instant::now(),
- latest_position: transform.translation.truncate(),
- last_position: transform.translation.truncate(),
+ positions: VecDeque::new(),
})
.insert(RotationInterpolationInfo {
last_dt: Duration::from_millis(50), // assume it was 20tps because we don't know
M crates/unified/src/client/planet/incoming_planets.rs => crates/unified/src/client/planet/incoming_planets.rs +2 -4
@@ 1,3 1,4 @@
+use std::collections::VecDeque;
use std::time::Duration;
use crate::shared::config::planet::{Planet, SpecialSpriteProperties};
use crate::prelude::*;
@@ 28,10 29,7 @@ fn handle_incoming_planets(
.insert(MAIN_STAR_LAYERS.clone())
.insert(build_planet_sprite(new_planet, &asset_server))
.insert(TranslationInterpolationInfo {
- last_dt: Duration::from_millis(50), // assume it was 20tps because we don't know
- this_tick_start: bevy::platform::time::Instant::now(),
- latest_position: transform.translation.truncate(),
- last_position: transform.translation.truncate(),
+ positions: VecDeque::new(),
})
.insert(RotationInterpolationInfo {
last_dt: Duration::from_millis(50), // assume it was 20tps because we don't know
A crates/unified/src/client/server_clock.rs => crates/unified/src/client/server_clock.rs +67 -0
@@ 0,0 1,67 @@
+use std::collections::VecDeque;
+use crate::client::components::{ServerClock, ServerTimeOffset};
+use crate::prelude::*;
+use crate::shared::ecs::clock_sync::{ClientTiming, ServerTiming};
+
+pub fn server_clock_plugin(app: &mut App) {
+ app
+ .insert_resource(SyncTimer(Timer::from_seconds(1.0, TimerMode::Repeating)))
+ .insert_resource(ServerClock {
+ rtt_queue: VecDeque::with_capacity(10), // 10 seconds
+ time_offset_queue: VecDeque::with_capacity(10),
+ ..default()
+ })
+ .add_systems(Update, send_timing)
+ .add_systems(Update, recv_timing);
+}
+
+#[derive(Resource)]
+struct SyncTimer(Timer);
+
+fn send_timing(
+ mut sync_timer: ResMut<SyncTimer>,
+ mut client_timing: MessageWriter<ClientTiming>,
+ time: Res<Time>,
+) {
+ if !sync_timer.0.tick(time.delta()).is_finished() {
+ return
+ }
+ sync_timer.0.reset();
+
+ let Ok(now) = web_time::SystemTime::now().duration_since(web_time::UNIX_EPOCH) else {
+ warn!("Could not get system time");
+ return
+ };
+ client_timing.write(ClientTiming {
+ time: now.as_secs_f64(),
+ });
+}
+
+fn recv_timing(
+ mut server_timings: MessageReader<ServerTiming>,
+ mut server_clock: ResMut<ServerClock>,
+ mut server_time_offset: ResMut<ServerTimeOffset>,
+ time: Res<Time>,
+) {
+ for ServerTiming { client_tx, server, server_time_elapsed } in server_timings.read() {
+ let Ok(now) = web_time::SystemTime::now().duration_since(web_time::UNIX_EPOCH) else {
+ warn!("Could not get system time");
+ return
+ };
+ let client_rx = now.as_secs_f64();
+ let time_offset = 2.0*server - client_tx - client_rx;
+ let rtt = client_rx - client_tx;
+
+ if server_clock.rtt_queue.len() >= 10 {
+ server_clock.rtt_queue.drain(10..);
+ }
+ if server_clock.time_offset_queue.len() >= 10 {
+ server_clock.time_offset_queue.drain(10..);
+ }
+ server_clock.rtt_queue.push_back(rtt);
+ server_clock.time_offset_queue.push_back(time_offset);
+ server_clock.rtt = server_clock.rtt_queue.iter().sum::<f64>() / server_clock.rtt_queue.len() as f64;
+ server_clock.time_offset = server_clock.time_offset_queue.iter().sum::<f64>() / server_clock.time_offset_queue.len() as f64;
+ **server_time_offset = *server_time_elapsed - time.elapsed_secs_f64();
+ }
+}<
\ No newline at end of file
A crates/unified/src/server/client_timing.rs => crates/unified/src/server/client_timing.rs +28 -0
@@ 0,0 1,28 @@
+use bevy_replicon::prelude::{FromClient, SendTargets, ToClients};
+use crate::prelude::*;
+use crate::shared::ecs::clock_sync::{ClientTiming, ServerTiming};
+
+pub fn client_timing_plugin(app: &mut App) {
+ app.add_systems(Update, echo_timing);
+}
+
+fn echo_timing(
+ mut client_timings: MessageReader<FromClient<ClientTiming>>,
+ mut server_timings: MessageWriter<ToClients<ServerTiming>>,
+ time: Res<Time>,
+) {
+ for client_timing in client_timings.read() {
+ let Ok(now) = web_time::SystemTime::now().duration_since(web_time::UNIX_EPOCH) else {
+ warn!("Could not get system time");
+ return
+ };
+ server_timings.write(ToClients {
+ targets: SendTargets::Single(client_timing.client_id),
+ message: ServerTiming {
+ client_tx: client_timing.time,
+ server: now.as_secs_f64(),
+ server_time_elapsed: time.elapsed_secs_f64(),
+ }
+ });
+ }
+}<
\ No newline at end of file
M crates/unified/src/server/mod.rs => crates/unified/src/server/mod.rs +3 -0
@@ 14,6 14,7 @@ pub mod orbit;
pub mod plugins;
pub mod components;
pub mod visibility;
+pub mod client_timing;
use std::net::{SocketAddr, UdpSocket};
use bevy_replicon::prelude::{ConnectedClient, Replicated, RepliconChannels};
@@ 37,6 38,7 @@ use crate::server::planets::planets_plugin;
use crate::server::player::player_management_plugin;
use crate::server::system_sets::{PlayerInputSet, WorldUpdateSet};
use crate::prelude::*;
+use crate::server::client_timing::client_timing_plugin;
use crate::server::orbit::OrbitPlugin;
use crate::server::player::thrust::server_thrust_plugin;
use crate::shared::net::{encode_cert_hash, ManagementInfo, STARKINGDOMS_PROTOCOL_MAGIC};
@@ 66,6 68,7 @@ impl Plugin for ServerPlugin {
.add_plugins(OrbitPlugin)
.add_plugins(damping_plugin)
.add_plugins(replication_priority_plugin)
+ .add_plugins(client_timing_plugin)
.configure_sets(Update, WorldUpdateSet.before(PlayerInputSet))
.add_systems(Update, handle_authorized)
.add_observer(on_client_disconnected);
M crates/unified/src/server/orbit/mod.rs => crates/unified/src/server/orbit/mod.rs +1 -3
@@ 6,7 6,6 @@ use bevy::prelude::{App, Plugin, Transform, Update};
use bevy::time::Time;
use crate::shared::config::planet::{Planet, PlanetSpring};
use crate::prelude::{Query, Res, Without};
-use crate::shared::ecs::TimeOffset;
use crate::shared::world_config::WorldConfigResource;
pub struct OrbitPlugin;
@@ 22,7 21,6 @@ fn update_orbits(
mut planet_springs: Query<(&PlanetSpring, &mut Transform, &mut LinearVelocity), Without<Planet>>,
world_config: Res<WorldConfigResource>,
time: Res<Time>,
- time_offset: Res<TimeOffset>,
) {
let Some(ref world_config) = world_config.config else {
return;
@@ 37,7 35,7 @@ fn update_orbits(
let e = orbit_data.eccentricity;
let t = world_config.world.orbit_scale_factor * 2.0*PI*((a*a*a)/(world_config.world.gravity*(**parent_mass as f64))).sqrt();
- let time = time.elapsed_secs_f64() + time_offset.0;
+ let time = time.elapsed_secs_f64();
let m = (TAU / t) * time;
let e_k = iterative_kepler(m, e);
M crates/unified/src/server/player/join.rs => crates/unified/src/server/player/join.rs +5 -0
@@ 82,11 82,16 @@ pub fn handle_new_players(
};
for joined_player in &q_new_clients {
debug!(?joined_player, "new player!");
+ let Ok(now) = web_time::SystemTime::now().duration_since(web_time::UNIX_EPOCH) else {
+ warn!("Could not get system time");
+ continue
+ };
welcome_messages.write(ToClients {
targets: SendTargets::Single(ClientId::Client(joined_player.1.network_entity)),
message: Hi {
you_are: joined_player.0,
time_offset: time.elapsed_secs_f64(),
+ server_time: now.as_secs_f64(),
},
});
join_player(joined_player.0, commands.reborrow(), wc, planets, &asset_server);
M crates/unified/src/shared/ecs.rs => crates/unified/src/shared/ecs.rs +1 -3
@@ 1,4 1,5 @@
pub mod thruster;
+pub mod clock_sync;
use crate::shared::config::part::PartConfig;
use bevy::math::{Quat, Vec2};
@@ 36,9 37,6 @@ pub struct Part {
#[derive(Component, Debug)]
pub struct PartHandle(pub Handle<PartConfig>);
-#[derive(Resource, Default)]
-pub struct TimeOffset(pub f64);
-
#[derive(Component, Serialize, Deserialize, Debug)]
pub struct Player {
pub client: Entity,
A crates/unified/src/shared/ecs/clock_sync.rs => crates/unified/src/shared/ecs/clock_sync.rs +14 -0
@@ 0,0 1,14 @@
+use serde::{Deserialize, Serialize};
+use crate::prelude::Message;
+
+#[derive(Message, Serialize, Deserialize)]
+pub struct ClientTiming {
+ pub time: f64,
+}
+#[derive(Message, Serialize, Deserialize)]
+pub struct ServerTiming {
+ pub client_tx: f64,
+ pub server: f64, // rx and tx are the same (happens in the same system)
+ pub server_time_elapsed: f64,
+ // client_rx is filled in by the client as it receives it
+}<
\ No newline at end of file
M crates/unified/src/shared/net.rs => crates/unified/src/shared/net.rs +4 -0
@@ 9,6 9,7 @@ use url::{Host, Url};
use crate::shared::attachment::{Joint, JointOf, PartInShip, Peer, Ship, SnapOf, SnapOfJoint};
use crate::shared::config::planet::{Planet, PlanetSpring, PlanetSpringJoint};
use crate::shared::ecs::{CanCraft, Drill, Part, Player, PlayerStorage, SingleStorage, Temperature};
+use crate::shared::ecs::clock_sync::{ClientTiming, ServerTiming};
use crate::shared::ecs::thruster::{Thruster, ThrusterOfPart};
pub const STARKINGDOMS_PROTOCOL_MAGIC: u64 = 0x5a5a_e37e_4aaa;
@@ 16,6 17,8 @@ pub const STARKINGDOMS_PROTOCOL_MAGIC: u64 = 0x5a5a_e37e_4aaa;
pub fn register_replication(app: &mut App) {
app
.add_mapped_server_message::<Hi>(Channel::Ordered)
+ .add_client_message::<ClientTiming>(Channel::Ordered)
+ .add_server_message::<ServerTiming>(Channel::Ordered)
.replicate_once::<Transform>()
.replicate_once::<GlobalTransform>()
@@ 56,6 59,7 @@ pub struct Hi {
#[entities]
pub you_are: Entity,
pub time_offset: f64,
+ pub server_time: f64,
}
#[derive(Serialize, Deserialize)]
M crates/unified/src/shared/plugins.rs => crates/unified/src/shared/plugins.rs +1 -2
@@ 1,4 1,4 @@
-use crate::shared::ecs::{CraftPartRequest, DragRequestEvent, TimeOffset, ToggleDrillEvent};
+use crate::shared::ecs::{CraftPartRequest, DragRequestEvent, ToggleDrillEvent};
use crate::shared::thrust::ThrustSolution;
use bevy::app::{App, PluginGroup, PluginGroupBuilder};
use bevy::diagnostic::DiagnosticsPlugin;
@@ 27,7 27,6 @@ impl PluginGroup for SharedPluginGroup {
.add(DiagnosticsPlugin)
.add(|app: &mut App| {
app.insert_resource(Time::from_hz(TICK_RATE));
- app.insert_resource(TimeOffset::default());
})
.add(register_replication)
.add(register_everything)