~starkingdoms/starkingdoms

90fa20b5a97fffd3f5f3bb6c9d8c6586d2ec9692 — ghostly_zsh 4 days ago 0494c19
netcode: interpolation is less dumb and stupid; also some ntp left over just in case
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)