~starkingdoms/starkingdoms

9b91629f33034dab27fe10f953bd39b6477fcac2 — core 5 months ago 103444e
chore(lint): lint/format
M crates/unified/Cargo.toml => crates/unified/Cargo.toml +1 -1
@@ 27,6 27,6 @@ getrandom = { version = "0.3", features = [] }
tokio = { version = "1", features = ["rt-multi-thread"] }

[features]
default = []
default = ["native"]
native = ["bevy/file_watcher", "bevy_replicon_renet2/native_transport", "bevy_replicon_renet2/ws_server_transport"]
wasm = ["getrandom/wasm_js", "bevy_replicon_renet2/ws_client_transport"]
\ No newline at end of file

M crates/unified/src/client/incoming_parts.rs => crates/unified/src/client/incoming_parts.rs +15 -6
@@ 1,19 1,23 @@
use crate::ecs::Part;
use bevy::prelude::*;
use bevy_rapier2d::dynamics::MassProperties;
use bevy_rapier2d::prelude::{AdditionalMassProperties, ReadMassProperties, RigidBody};
use crate::config::planet::Planet;
use crate::ecs::Part;

pub fn incoming_parts_plugin(app: &mut App) {
    app.add_systems(Update, (handle_incoming_parts, handle_updated_parts));
}

fn handle_incoming_parts(mut commands: Commands, mut new_parts: Query<(Entity, &Part), Added<Part>>, asset_server: Res<AssetServer>) {
fn handle_incoming_parts(
    mut commands: Commands,
    new_parts: Query<(Entity, &Part), Added<Part>>,
    asset_server: Res<AssetServer>,
) {
    for (new_entity, new_part) in new_parts.iter() {
        let mut sprite = Sprite::from_image(asset_server.load(&new_part.sprite));
        sprite.custom_size = Some(Vec2::new(new_part.width, new_part.height));

        commands.entity(new_entity)
        commands
            .entity(new_entity)
            .insert(sprite)
            .insert(AdditionalMassProperties::MassProperties(MassProperties {
                local_center_of_mass: Vec2::ZERO,


@@ 25,12 29,17 @@ fn handle_incoming_parts(mut commands: Commands, mut new_parts: Query<(Entity, &
        info!(?new_part, "prepared new part");
    }
}
fn handle_updated_parts(mut commands: Commands, mut updated_parts: Query<(Entity, &Part), Changed<Part>>, asset_server: Res<AssetServer>) {
fn handle_updated_parts(
    mut commands: Commands,
    updated_parts: Query<(Entity, &Part), Changed<Part>>,
    asset_server: Res<AssetServer>,
) {
    for (updated_entity, updated_part) in updated_parts.iter() {
        let mut sprite = Sprite::from_image(asset_server.load(&updated_part.sprite));
        sprite.custom_size = Some(Vec2::new(updated_part.width, updated_part.height));

        commands.entity(updated_entity)
        commands
            .entity(updated_entity)
            .remove::<Sprite>()
            .remove::<AdditionalMassProperties>()
            .insert(sprite)

M crates/unified/src/client/incoming_planets.rs => crates/unified/src/client/incoming_planets.rs +21 -9
@@ 1,33 1,45 @@
use crate::config::planet::Planet;
use bevy::prelude::*;
use bevy_rapier2d::prelude::AdditionalMassProperties;
use crate::config::planet::Planet;

pub fn incoming_planets_plugin(app: &mut App) {
    app.add_systems(Update, (handle_incoming_planets, handle_updated_planets));
}


fn handle_incoming_planets(mut commands: Commands, mut new_planets: Query<(Entity, &Planet), Added<Planet>>, asset_server: Res<AssetServer>) {
fn handle_incoming_planets(
    mut commands: Commands,
    new_planets: Query<(Entity, &Planet), Added<Planet>>,
    asset_server: Res<AssetServer>,
) {
    for (new_entity, new_planet) in new_planets.iter() {
        let mut sprite = Sprite::from_image(asset_server.load(&new_planet.sprite));
        sprite.custom_size = Some(Vec2::new(new_planet.radius*2.0, new_planet.radius*2.0));
        sprite.custom_size = Some(Vec2::new(new_planet.radius * 2.0, new_planet.radius * 2.0));

        commands.entity(new_entity)
        commands
            .entity(new_entity)
            .insert(sprite)
            .insert(AdditionalMassProperties::Mass(new_planet.mass));
        info!(?new_planet, "prepared new planet");
    }
}
fn handle_updated_planets(mut commands: Commands, mut updated_planets: Query<(Entity, &Planet), Changed<Planet>>, asset_server: Res<AssetServer>) {
fn handle_updated_planets(
    mut commands: Commands,
    updated_planets: Query<(Entity, &Planet), Changed<Planet>>,
    asset_server: Res<AssetServer>,
) {
    for (updated_entity, updated_planet) in updated_planets.iter() {
        let mut sprite = Sprite::from_image(asset_server.load(&updated_planet.sprite));
        sprite.custom_size = Some(Vec2::new(updated_planet.radius*2.0, updated_planet.radius*2.0));
        sprite.custom_size = Some(Vec2::new(
            updated_planet.radius * 2.0,
            updated_planet.radius * 2.0,
        ));

        commands.entity(updated_entity)
        commands
            .entity(updated_entity)
            .remove::<Sprite>()
            .remove::<AdditionalMassProperties>()
            .insert(sprite)
            .insert(AdditionalMassProperties::Mass(updated_planet.mass));
        info!(?updated_planet, "updated planet");
    }
}
\ No newline at end of file
}

M crates/unified/src/client/key_input.rs => crates/unified/src/client/key_input.rs +5 -1
@@ 1,4 1,8 @@
use bevy::{app::{App, Update}, ecs::{event::EventWriter, system::Res}, input::{keyboard::KeyCode, ButtonInput}};
use bevy::{
    app::{App, Update},
    ecs::{event::EventWriter, system::Res},
    input::{ButtonInput, keyboard::KeyCode},
};

use crate::ecs::ThrustEvent;


M crates/unified/src/client/mod.rs => crates/unified/src/client/mod.rs +101 -76
@@ 1,88 1,105 @@
mod incoming_planets;
mod incoming_parts;
mod incoming_planets;
mod key_input;
mod starfield;

use std::net::{IpAddr, SocketAddr, UdpSocket};
use std::time::SystemTime;
use bevy::core_pipeline::bloom::Bloom;
use crate::client::incoming_parts::incoming_parts_plugin;
use crate::client::incoming_planets::incoming_planets_plugin;
use crate::client::key_input::key_input_plugin;
use crate::client::starfield::starfield_plugin;
use crate::ecs::{CursorWorldCoordinates, MainCamera, Player};
use bevy::core_pipeline::fxaa::Fxaa;
use bevy::core_pipeline::tonemapping::Tonemapping;
use bevy::prelude::*;
use bevy::window::PrimaryWindow;
use bevy_rapier2d::prelude::RigidBody;
use bevy_replicon::prelude::{ConnectedClient, RepliconChannels};
use bevy_replicon::prelude::RepliconChannels;
use bevy_replicon::shared::server_entity_map::ServerEntityMap;
use bevy_replicon_renet2::netcode::{ClientAuthentication, NetcodeClientTransport};
use bevy_replicon_renet2::RenetChannelsExt;
#[cfg(not(target_arch = "wasm32"))]
use bevy_replicon_renet2::netcode::NativeSocket;
use bevy_replicon_renet2::netcode::{ClientAuthentication, NetcodeClientTransport};
#[cfg(target_arch = "wasm32")]
use bevy_replicon_renet2::netcode::{WebSocketClientConfig, WebSocketClient, ClientSocket};
use bevy_replicon_renet2::netcode::{ClientSocket, WebSocketClient, WebSocketClientConfig};
use bevy_replicon_renet2::renet2::{ConnectionConfig, RenetClient};
use bevy_replicon_renet2::RenetChannelsExt;
use crate::client::incoming_parts::incoming_parts_plugin;
use crate::client::incoming_planets::incoming_planets_plugin;
use crate::client::key_input::key_input_plugin;
use crate::client::starfield::starfield_plugin;
use crate::ecs::{Ball, CursorWorldCoordinates, Ground, MainCamera, Player, SendBallHere};
use std::net::{IpAddr, SocketAddr, UdpSocket};
use std::time::SystemTime;

pub struct ClientPlugin {
    #[cfg(target_arch = "wasm32")]
    pub server: url::Url,
    #[cfg(not(target_arch = "wasm32"))]
    pub server: SocketAddr
    pub server: SocketAddr,
}
impl Plugin for ClientPlugin {
    fn build(&self, app: &mut App) {
        let server = self.server.clone();
        app
            .insert_resource(CursorWorldCoordinates(None))
            .add_systems(Startup, move |mut commands: Commands, channels: Res<RepliconChannels>| {
                let current_time = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap();
                let client_id = current_time.as_millis() as u64;
        let server = self.server;
        app.insert_resource(CursorWorldCoordinates(None))
            .add_systems(
                Startup,
                move |mut commands: Commands, channels: Res<RepliconChannels>| {
                    let current_time = SystemTime::now()
                        .duration_since(SystemTime::UNIX_EPOCH)
                        .unwrap();
                    let client_id = current_time.as_millis() as u64;

                #[cfg(target_arch = "wasm32")] {
                    let socket_config = WebSocketClientConfig {
                        server_url: server.clone(),
                    };
                    let socket = WebSocketClient::new(socket_config).unwrap();
                    let client = RenetClient::new(
                        ConnectionConfig::from_channels(channels.server_configs(), channels.client_configs()),
                        socket.is_reliable()
                    );
                    let authentication = ClientAuthentication::Unsecure {
                        socket_id: 1,
                        server_addr: socket.server_address(),
                        client_id: current_time.as_millis() as u64,
                        user_data: None,
                        protocol_id: 0
                    };
                    let mut transport = NetcodeClientTransport::new(current_time, authentication, socket).unwrap();
                    commands.insert_resource(client);
                    commands.insert_resource(transport);
                }
                #[cfg(not(target_arch = "wasm32"))] {
                    let bind = match server.ip() {
                        IpAddr::V4(_) => "127.0.0.1:0",
                        IpAddr::V6(_) => "[::1]:0",
                    };
                    let client_socket = NativeSocket::new(UdpSocket::bind(bind).unwrap()).unwrap();
                    let authentication = ClientAuthentication::Unsecure {
                        socket_id: 0,
                        server_addr: server,
                        client_id: current_time.as_millis() as u64,
                        user_data: None,
                        protocol_id: 0
                    };
                    let client = RenetClient::new(
                        ConnectionConfig::from_channels(channels.server_configs(), channels.client_configs()),
                        false
                    );
                    let mut transport = NetcodeClientTransport::new(current_time, authentication, client_socket).unwrap();
                    commands.insert_resource(client);
                    commands.insert_resource(transport);
                }
            })
                    #[cfg(target_arch = "wasm32")]
                    {
                        let socket_config = WebSocketClientConfig {
                            server_url: server.clone(),
                        };
                        let socket = WebSocketClient::new(socket_config).unwrap();
                        let client = RenetClient::new(
                            ConnectionConfig::from_channels(
                                channels.server_configs(),
                                channels.client_configs(),
                            ),
                            socket.is_reliable(),
                        );
                        let authentication = ClientAuthentication::Unsecure {
                            socket_id: 1,
                            server_addr: socket.server_address(),
                            client_id: current_time.as_millis() as u64,
                            user_data: None,
                            protocol_id: 0,
                        };
                        let mut transport =
                            NetcodeClientTransport::new(current_time, authentication, socket)
                                .unwrap();
                        commands.insert_resource(client);
                        commands.insert_resource(transport);
                    }
                    #[cfg(not(target_arch = "wasm32"))]
                    {
                        let bind = match server.ip() {
                            IpAddr::V4(_) => "127.0.0.1:0",
                            IpAddr::V6(_) => "[::1]:0",
                        };
                        let client_socket =
                            NativeSocket::new(UdpSocket::bind(bind).unwrap()).unwrap();
                        let authentication = ClientAuthentication::Unsecure {
                            socket_id: 0,
                            server_addr: server,
                            client_id: current_time.as_millis() as u64,
                            user_data: None,
                            protocol_id: 0,
                        };
                        let client = RenetClient::new(
                            ConnectionConfig::from_channels(
                                channels.server_configs(),
                                channels.client_configs(),
                            ),
                            false,
                        );
                        let transport = NetcodeClientTransport::new(
                            current_time,
                            authentication,
                            client_socket,
                        )
                        .unwrap();
                        commands.insert_resource(client);
                        commands.insert_resource(transport);
                    }
                },
            )
            .add_systems(Startup, setup_graphics)
            .add_systems(Update, update_cursor_position)
            .add_systems(Update, follow_camera)


@@ 97,8 114,11 @@ impl Plugin for ClientPlugin {
#[derive(Component)]
pub struct Me;


fn find_me(mut commands: Commands, q_clients: Query<(Entity, &Player), Added<Player>>, entity_map: Res<ServerEntityMap>) {
fn find_me(
    mut commands: Commands,
    q_clients: Query<(Entity, &Player), Added<Player>>,
    entity_map: Res<ServerEntityMap>,
) {
    for (entity, player) in q_clients.iter() {
        let this_id_clientside = entity_map.to_client().get(&player.client).unwrap();
        if *this_id_clientside == entity {


@@ 107,33 127,38 @@ fn find_me(mut commands: Commands, q_clients: Query<(Entity, &Player), Added<Pla
    }
}


fn setup_graphics(mut commands: Commands) {
    commands.spawn(Camera2d)
        .insert(Camera {
            ..default()
        })
    commands
        .spawn(Camera2d)
        .insert(Camera { ..default() })
        .insert(Fxaa::default())
        .insert(MainCamera);
}

fn follow_camera(mut camera: Query<&mut Transform, (With<MainCamera>, Without<Me>)>, mut player: Query<&Transform, With<Me>>) {
fn follow_camera(
    mut camera: Query<&mut Transform, (With<MainCamera>, Without<Me>)>,
    player: Query<&Transform, With<Me>>,
) {
    let mut camera = camera.single_mut().unwrap();
    let Ok(player) = player.single() else { return; };
    let Ok(player) = player.single() else {
        return;
    };
    camera.translation = player.translation;
}

fn update_cursor_position(
    q_windows: Query<&Window, With<PrimaryWindow>>,
    q_camera: Query<(&Camera, &GlobalTransform), With<MainCamera>>,
    mut coords: ResMut<CursorWorldCoordinates>
    mut coords: ResMut<CursorWorldCoordinates>,
) {
    let (camera, camera_transform) = q_camera.single().unwrap();
    let window = q_windows.single().unwrap();

    if let Some(world_position) = window.cursor_position()
    if let Some(world_position) = window
        .cursor_position()
        .and_then(|cursor| camera.viewport_to_world(camera_transform, cursor).ok())
        .map(|ray| ray.origin.truncate()) {
        .map(|ray| ray.origin.truncate())
    {
        coords.0 = Some(world_position);
    } else {
        coords.0 = None;

M crates/unified/src/client/starfield.rs => crates/unified/src/client/starfield.rs +137 -51
@@ 1,14 1,29 @@
use bevy::{app::{App, Startup, Update}, asset::{AssetEvent, AssetServer, Assets}, color::Color, ecs::{event::EventReader, query::{With, Without}, system::{Commands, Query, Res, ResMut}}, image::Image, math::{Vec2, Vec3}, sprite::{Sprite, SpriteImageMode}, transform::components::Transform, window::{Window, WindowResized}};
use bevy::{
    app::{App, Startup, Update},
    asset::{AssetEvent, AssetServer, Assets},
    ecs::{
        event::EventReader,
        query::{With, Without},
        system::{Commands, Query, Res},
    },
    image::Image,
    math::{Vec2, Vec3},
    sprite::{Sprite, SpriteImageMode},
    transform::components::Transform,
    window::{Window, WindowResized},
};

use crate::{client::Me, ecs::{StarfieldBack, StarfieldFront, StarfieldMid}};
use crate::{
    client::Me,
    ecs::{StarfieldBack, StarfieldFront, StarfieldMid},
};

const BACK_STARFIELD_SIZE: f32 = 256.0;
const MID_STARFIELD_SIZE: f32 = 384.0;
const FRONT_STARFIELD_SIZE: f32 = 512.0;

pub fn starfield_plugin(app: &mut App) {
    app
        .add_systems(Startup, setup_starfield)
    app.add_systems(Startup, setup_starfield)
        .add_systems(Update, fix_starfield)
        .add_systems(Update, resize_starfield)
        .add_systems(Update, update_starfield);


@@ 17,53 32,77 @@ pub fn starfield_plugin(app: &mut App) {
pub fn setup_starfield(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    window: Query<&Window>
    window: Query<&Window>,
) {
    let starfield_handle = asset_server.load("textures/starfield.png");
    let starfield_transp_handle = asset_server.load("textures/starfield_transp.png");
    let window = window.iter().next().unwrap();
    commands.spawn(Sprite {
        image: starfield_handle,
        custom_size: Some(window.size() + Vec2::splat(BACK_STARFIELD_SIZE)),
        image_mode: SpriteImageMode::Tiled {
            tile_x: true,
            tile_y: true,
            stretch_value: 1.0,
        },
        ..Default::default()
    })
    commands
        .spawn(Sprite {
            image: starfield_handle,
            custom_size: Some(window.size() + Vec2::splat(BACK_STARFIELD_SIZE)),
            image_mode: SpriteImageMode::Tiled {
                tile_x: true,
                tile_y: true,
                stretch_value: 1.0,
            },
            ..Default::default()
        })
        .insert(Transform::from_xyz(0.0, 0.0, 5.0))
        .insert(StarfieldBack);
    commands.spawn(Sprite {
        image: starfield_transp_handle.clone(),
        custom_size: Some(window.size() + Vec2::splat(MID_STARFIELD_SIZE)),
        image_mode: SpriteImageMode::Tiled {
            tile_x: true,
            tile_y: true,
            stretch_value: 1.0,
        },
        ..Default::default()
    })
    commands
        .spawn(Sprite {
            image: starfield_transp_handle.clone(),
            custom_size: Some(window.size() + Vec2::splat(MID_STARFIELD_SIZE)),
            image_mode: SpriteImageMode::Tiled {
                tile_x: true,
                tile_y: true,
                stretch_value: 1.0,
            },
            ..Default::default()
        })
        .insert(Transform::from_xyz(0.0, 0.0, 4.5))
        .insert(StarfieldMid);
    commands.spawn(Sprite {
        image: starfield_transp_handle,
        custom_size: Some(window.size() + Vec2::splat(FRONT_STARFIELD_SIZE)),
        image_mode: SpriteImageMode::Tiled {
            tile_x: true,
            tile_y: true,
            stretch_value: 1.0,
        },
        ..Default::default()
    })
    commands
        .spawn(Sprite {
            image: starfield_transp_handle,
            custom_size: Some(window.size() + Vec2::splat(FRONT_STARFIELD_SIZE)),
            image_mode: SpriteImageMode::Tiled {
                tile_x: true,
                tile_y: true,
                stretch_value: 1.0,
            },
            ..Default::default()
        })
        .insert(Transform::from_xyz(0.0, 0.0, 4.0))
        .insert(StarfieldFront);
}

pub fn fix_starfield(
    mut starfield_back: Query<&mut Sprite, (With<StarfieldBack>, Without<StarfieldMid>, Without<StarfieldFront>)>,
    mut starfield_mid: Query<&mut Sprite, (With<StarfieldMid>, Without<StarfieldBack>, Without<StarfieldFront>)>,
    mut starfield_front: Query<&mut Sprite, (With<StarfieldFront>, Without<StarfieldBack>, Without<StarfieldMid>)>,
    mut starfield_back: Query<
        &mut Sprite,
        (
            With<StarfieldBack>,
            Without<StarfieldMid>,
            Without<StarfieldFront>,
        ),
    >,
    mut starfield_mid: Query<
        &mut Sprite,
        (
            With<StarfieldMid>,
            Without<StarfieldBack>,
            Without<StarfieldFront>,
        ),
    >,
    mut starfield_front: Query<
        &mut Sprite,
        (
            With<StarfieldFront>,
            Without<StarfieldBack>,
            Without<StarfieldMid>,
        ),
    >,
    assets: Res<Assets<Image>>,
    mut asset_events: EventReader<AssetEvent<Image>>,
) {


@@ 101,39 140,86 @@ pub fn fix_starfield(
}

pub fn resize_starfield(
    mut starfield_back: Query<&mut Sprite, (With<StarfieldBack>, Without<StarfieldMid>, Without<StarfieldFront>)>,
    mut starfield_mid: Query<&mut Sprite, (With<StarfieldMid>, Without<StarfieldBack>, Without<StarfieldFront>)>,
    mut starfield_front: Query<&mut Sprite, (With<StarfieldFront>, Without<StarfieldBack>, Without<StarfieldMid>)>,
    mut starfield_back: Query<
        &mut Sprite,
        (
            With<StarfieldBack>,
            Without<StarfieldMid>,
            Without<StarfieldFront>,
        ),
    >,
    mut starfield_mid: Query<
        &mut Sprite,
        (
            With<StarfieldMid>,
            Without<StarfieldBack>,
            Without<StarfieldFront>,
        ),
    >,
    mut starfield_front: Query<
        &mut Sprite,
        (
            With<StarfieldFront>,
            Without<StarfieldBack>,
            Without<StarfieldMid>,
        ),
    >,
    mut resize_event: EventReader<WindowResized>,
) {
    for event in resize_event.read() {
        starfield_back.single_mut().unwrap().custom_size =
            Some(Vec2::new(event.width, event.height) + Vec2::splat(BACK_STARFIELD_SIZE*2.0));
            Some(Vec2::new(event.width, event.height) + Vec2::splat(BACK_STARFIELD_SIZE * 2.0));
        starfield_mid.single_mut().unwrap().custom_size =
            Some(Vec2::new(event.width, event.height) + Vec2::splat(MID_STARFIELD_SIZE*2.0));
            Some(Vec2::new(event.width, event.height) + Vec2::splat(MID_STARFIELD_SIZE * 2.0));
        starfield_front.single_mut().unwrap().custom_size =
            Some(Vec2::new(event.width, event.height) + Vec2::splat(FRONT_STARFIELD_SIZE*2.0));
            Some(Vec2::new(event.width, event.height) + Vec2::splat(FRONT_STARFIELD_SIZE * 2.0));
    }
}

pub fn update_starfield(
    mut starfield_back: Query<&mut Transform, (With<StarfieldBack>, Without<Me>, Without<StarfieldMid>, Without<StarfieldFront>)>,
    mut starfield_mid: Query<&mut Transform, (With<StarfieldMid>, Without<Me>, Without<StarfieldBack>, Without<StarfieldFront>)>,
    mut starfield_front: Query<&mut Transform, (With<StarfieldFront>, Without<Me>, Without<StarfieldBack>, Without<StarfieldMid>)>,
    mut starfield_back: Query<
        &mut Transform,
        (
            With<StarfieldBack>,
            Without<Me>,
            Without<StarfieldMid>,
            Without<StarfieldFront>,
        ),
    >,
    mut starfield_mid: Query<
        &mut Transform,
        (
            With<StarfieldMid>,
            Without<Me>,
            Without<StarfieldBack>,
            Without<StarfieldFront>,
        ),
    >,
    mut starfield_front: Query<
        &mut Transform,
        (
            With<StarfieldFront>,
            Without<Me>,
            Without<StarfieldBack>,
            Without<StarfieldMid>,
        ),
    >,
    player: Query<&Transform, (With<Me>, Without<StarfieldFront>)>,
) {
    let Some(player) = player.iter().next() else { return };
    let Some(player) = player.iter().next() else {
        return;
    };
    let mut starfield_back_pos = starfield_back.single_mut().unwrap();
    let mut starfield_mid_pos = starfield_mid.single_mut().unwrap();
    let mut starfield_front_pos = starfield_front.single_mut().unwrap();
    //starfield_pos.translation = (player.translation / STARFIELD_SIZE).round() * STARFIELD_SIZE;
    starfield_back_pos.translation = player.translation
        + (-player.translation/3.0) % BACK_STARFIELD_SIZE
        + (-player.translation / 3.0) % BACK_STARFIELD_SIZE
        - Vec3::new(0.0, 0.0, 5.0);
    starfield_mid_pos.translation = player.translation
        + (-player.translation/2.5) % MID_STARFIELD_SIZE
        + (-player.translation / 2.5) % MID_STARFIELD_SIZE
        - Vec3::new(0.0, 0.0, 4.5);
    starfield_front_pos.translation = player.translation
        + (-player.translation/2.0) % FRONT_STARFIELD_SIZE
        + (-player.translation / 2.0) % FRONT_STARFIELD_SIZE
        - Vec3::new(0.0, 0.0, 4.0);
}

M crates/unified/src/client_plugins.rs => crates/unified/src/client_plugins.rs +10 -15
@@ 1,31 1,26 @@
use std::net::SocketAddr;
use bevy::app::{PluginGroup, PluginGroupBuilder};
use crate::client::ClientPlugin;
use bevy::DefaultPlugins;
use bevy::app::{PluginGroup, PluginGroupBuilder};
use bevy::log::LogPlugin;
use bevy_rapier2d::render::RapierDebugRenderPlugin;
use bevy_replicon::RepliconPlugins;
use bevy_replicon_renet2::RepliconRenetClientPlugin;
use crate::client::ClientPlugin;
use std::net::SocketAddr;

pub struct ClientPluginGroup {
    #[cfg(target_arch = "wasm32")]
    pub server: url::Url,
    #[cfg(not(target_arch = "wasm32"))]
    pub server: SocketAddr
    pub server: SocketAddr,
}
impl PluginGroup for ClientPluginGroup {
    fn build(self) -> PluginGroupBuilder {
        PluginGroupBuilder::start::<Self>()
            .add_group(
                DefaultPlugins
                    .build()
                    .disable::<LogPlugin>()
            )
            .add_group(
                RepliconPlugins
            )
            .add_group(DefaultPlugins.build().disable::<LogPlugin>())
            .add_group(RepliconPlugins)
            .add(RepliconRenetClientPlugin)
            .add(ClientPlugin { server: self.server })
            //.add(RapierDebugRenderPlugin::default())
            .add(ClientPlugin {
                server: self.server,
            })
        //.add(RapierDebugRenderPlugin::default())
    }
}

M crates/unified/src/config/mod.rs => crates/unified/src/config/mod.rs +1 -1
@@ 1,2 1,2 @@
pub mod planet;
pub mod world;
pub mod planet;
\ No newline at end of file

M crates/unified/src/config/planet.rs => crates/unified/src/config/planet.rs +3 -8
@@ 1,20 1,16 @@
use bevy::asset::Asset;
use bevy::math::Vec3;
use bevy::prelude::{Bundle, Component, Transform, TypePath};
use bevy_rapier2d::prelude::{AdditionalMassProperties, Collider, ReadMassProperties, RigidBody};
use serde::{Deserialize, Serialize};

#[derive(Deserialize, Asset, TypePath, Component, Serialize, Clone, Debug)]
#[require(
    ReadMassProperties,
    RigidBody::Fixed
)]
#[require(ReadMassProperties, RigidBody::Fixed)]
pub struct Planet {
    pub name: String,
    pub sprite: String,
    pub radius: f32,
    pub mass: f32,
    pub default_transform: [f32; 3]
    pub default_transform: [f32; 3],
}

#[derive(Bundle)]


@@ 25,8 21,7 @@ pub struct PlanetBundle {
    pub additional_mass_properties: AdditionalMassProperties,
}


#[derive(Deserialize, Asset, TypePath)]
pub struct PlanetConfigCollection {
    pub planets: Vec<Planet>,
}
\ No newline at end of file
}

M crates/unified/src/config/world.rs => crates/unified/src/config/world.rs +2 -2
@@ 11,14 11,14 @@ pub struct GlobalWorldConfig {

#[derive(Deserialize, Asset, TypePath, Clone)]
pub struct WorldConfig {
    pub gravity: f32
    pub gravity: f32,
}

#[derive(Deserialize, Asset, TypePath, Clone)]
pub struct PartConfig {
    pub default_width: f32,
    pub default_height: f32,
    pub default_mass: f32
    pub default_mass: f32,
}

#[derive(Deserialize, Asset, TypePath, Clone)]

M crates/unified/src/ecs.rs => crates/unified/src/ecs.rs +2 -3
@@ 1,7 1,6 @@
use bevy::math::Vec2;
use bevy::prelude::{Bundle, Component, Entity, Event, Resource, Transform};
use bevy_rapier2d::dynamics::AdditionalMassProperties;
use bevy_replicon::prelude::Replicated;
use bevy_rapier2d::dynamics::RigidBody;
use bevy_rapier2d::geometry::Collider;
use bevy_rapier2d::prelude::*;


@@ 39,7 38,7 @@ pub struct Part {
    pub sprite: String,
    pub width: f32,
    pub height: f32,
    pub mass: f32
    pub mass: f32,
}
#[derive(Bundle)]
pub struct PartBundle {


@@ 51,7 50,7 @@ pub struct PartBundle {

#[derive(Component, Serialize, Deserialize, Debug)]
pub struct Player {
    pub client: Entity
    pub client: Entity,
}

#[derive(Component, Default, Serialize, Deserialize, Debug)]

M crates/unified/src/main.rs => crates/unified/src/main.rs +24 -22
@@ 1,22 1,21 @@
pub mod server_plugins;
pub mod shared_plugins;
pub mod server;
pub mod client;
pub mod ecs;
pub mod client_plugins;
pub mod config;
pub mod ecs;
pub mod server;
pub mod server_plugins;
pub mod shared_plugins;

use crate::client_plugins::ClientPluginGroup;
use crate::server_plugins::ServerPluginGroup;
use crate::shared_plugins::SharedPluginGroup;
use bevy::log::{Level, tracing_subscriber};
use bevy::prelude::*;
use clap::Parser;
use std::net::SocketAddr;
use std::process::exit;
use bevy::log::{tracing_subscriber, Level, LogPlugin};
use clap::Parser;
use bevy::prelude::*;
use tracing_subscriber::EnvFilter;
use tracing_subscriber::filter::Directive;
use tracing_subscriber::util::SubscriberInitExt;
use crate::client_plugins::ClientPluginGroup;
use crate::server_plugins::ServerPluginGroup;
use crate::shared_plugins::SharedPluginGroup;

#[derive(Parser, Debug)]
#[command(version, about)]


@@ 27,7 26,7 @@ enum Cli {
        server: url::Url,
        #[cfg(not(target_arch = "wasm32"))]
        #[arg(short, long)]
        server: SocketAddr
        server: SocketAddr,
    },
    Server {
        #[arg(short = 'w', long)]


@@ 37,8 36,8 @@ enum Cli {
        #[arg(short = 'r', long)]
        tick_rate: f64,
        #[arg(short = 'C', long)]
        max_clients: usize
    }
        max_clients: usize,
    },
}

fn main() -> AppExit {


@@ 55,18 54,21 @@ fn main() -> AppExit {
                .add_directive("bevy_replicon=warn".parse().unwrap())
                .add_directive("bevy_replicon_renet2=warn".parse().unwrap())
                .add_directive("naga=warn".parse().unwrap())
                .add_directive("wgpu_hal::vulkan=error".parse().unwrap())
                .add_directive("wgpu_hal::vulkan=error".parse().unwrap()),
        )
        .finish()
        .init();

    match cli {
        Cli::Client { server } => {
            app.add_plugins(ClientPluginGroup {
                server
            });
        },
        Cli::Server { bind_ws, bind_native, tick_rate, max_clients } => {
            app.add_plugins(ClientPluginGroup { server });
        }
        Cli::Server {
            bind_ws,
            bind_native,
            tick_rate,
            max_clients,
        } => {
            if cfg!(target_family = "wasm") {
                eprintln!("the server cannot run on webassembly");
                exit(1);


@@ 76,7 78,7 @@ fn main() -> AppExit {
                bind_ws,
                bind_native,
                tick_rate,
                max_clients
                max_clients,
            });
        }
    }


@@ 84,4 86,4 @@ fn main() -> AppExit {
    app.add_plugins(SharedPluginGroup);

    app.run()
}
\ No newline at end of file
}

M crates/unified/src/server/gravity.rs => crates/unified/src/server/gravity.rs +11 -8
@@ 1,9 1,9 @@
use bevy::math::FloatPow;
use bevy::prelude::*;
use bevy_rapier2d::prelude::*;
use crate::config::planet::Planet;
use crate::ecs::Part;
use crate::server::world_config::WorldConfigResource;
use bevy::math::FloatPow;
use bevy::prelude::*;
use bevy_rapier2d::prelude::*;

pub fn newtonian_gravity_plugin(app: &mut App) {
    app.add_systems(Update, update_gravity);


@@ 15,16 15,18 @@ fn update_gravity(
            &Transform,
            &ReadMassProperties,
            &mut ExternalForce,
            &mut ExternalImpulse
            &mut ExternalImpulse,
        ),
        With<Part>
        With<Part>,
    >,
    planet_query: Query<(&Transform, &ReadMassProperties), With<Planet>>,
    world_config: Res<WorldConfigResource>,
) {
    let Some(world_config) = &world_config.config else { return; };
    let Some(world_config) = &world_config.config else {
        return;
    };

    for (part_transform, part_mass, mut forces, mut impulses) in &mut part_query {
    for (part_transform, part_mass, mut forces, impulses) in &mut part_query {
        let part_mass = part_mass.mass;
        let part_translation = part_transform.translation;



@@ 34,7 36,8 @@ fn update_gravity(

            let distance = planet_translation.distance(part_translation);

            let force = world_config.world.gravity * ((part_mass * planet_mass) / distance.squared());
            let force =
                world_config.world.gravity * ((part_mass * planet_mass) / distance.squared());
            let direction = (planet_translation - part_translation).normalize() * force;
            forces.force += direction.xy();
        }

M crates/unified/src/server/mod.rs => crates/unified/src/server/mod.rs +54 -43
@@ 1,25 1,27 @@
mod gravity;
pub mod planets;
pub mod player;
mod world_config;
mod gravity;

use bevy::prelude::*;
use bevy_replicon::prelude::RepliconChannels;
use bevy_replicon_renet2::netcode::{
    BoxedSocket, NetcodeServerTransport, ServerAuthentication, ServerSetupConfig,
};
use std::net::{SocketAddr, UdpSocket};
use std::time::{SystemTime, UNIX_EPOCH};
use bevy::prelude::*;
use bevy_rapier2d::prelude::{Collider, Restitution, RigidBody, Velocity};
use bevy_replicon::prelude::{FromClient, Replicated, RepliconChannels};
use bevy_replicon_renet2::netcode::{NetcodeServerTransport, ServerAuthentication, ServerSetupConfig, BoxedSocket};

#[cfg(not(target_arch = "wasm32"))]
use bevy_replicon_renet2::netcode::{NativeSocket, WebSocketAcceptor, WebSocketServerConfig, WebSocketServer};
use bevy_replicon_renet2::netcode::{
    NativeSocket, WebSocketAcceptor, WebSocketServer, WebSocketServerConfig,
};

use bevy_replicon_renet2::renet2::{ConnectionConfig, RenetServer};
use bevy_replicon_renet2::RenetChannelsExt;
use crate::ecs::{Ball, Ground, SendBallHere};
use crate::server::gravity::newtonian_gravity_plugin;
use crate::server::planets::planets_plugin;
use crate::server::player::player_management_plugin;
use crate::server::world_config::world_config_plugin;
use bevy_replicon_renet2::RenetChannelsExt;
use bevy_replicon_renet2::renet2::{ConnectionConfig, RenetServer};

pub struct ServerPlugin {
    pub bind_ws: SocketAddr,


@@ 28,48 30,57 @@ pub struct ServerPlugin {
}
impl Plugin for ServerPlugin {
    fn build(&self, app: &mut App) {
        let bind_ws = self.bind_ws.clone();
        let bind_native = self.bind_native.clone();
        let max_clients = self.max_clients.clone();

        app
            .add_systems(FixedPreUpdate, bevy_replicon::server::increment_tick) // !!important!! do not remove or move
            .add_systems(Startup, move |mut commands: Commands, channels: Res<RepliconChannels>| {

                let server = RenetServer::new(ConnectionConfig::from_channels(
                    channels.server_configs(),
                    channels.client_configs()
                ));
        let bind_ws = self.bind_ws;
        let bind_native = self.bind_native;
        let max_clients = self.max_clients;

                #[cfg(not(target_arch = "wasm32"))] {
                    let server_config = ServerSetupConfig {
                        current_time: SystemTime::now().duration_since(UNIX_EPOCH).unwrap(),
                        max_clients: max_clients,
                        protocol_id: 0,
                        authentication: ServerAuthentication::Unsecure,
                        socket_addresses: vec![vec![bind_native], vec![bind_ws]]
                    };
        app.add_systems(FixedPreUpdate, bevy_replicon::server::increment_tick) // !!important!! do not remove or move
            .add_systems(
                Startup,
                move |mut commands: Commands, channels: Res<RepliconChannels>| {
                    let server = RenetServer::new(ConnectionConfig::from_channels(
                        channels.server_configs(),
                        channels.client_configs(),
                    ));

                    let rt = tokio::runtime::Runtime::new().unwrap();
                    #[cfg(not(target_arch = "wasm32"))]
                    {
                        let server_config = ServerSetupConfig {
                            current_time: SystemTime::now().duration_since(UNIX_EPOCH).unwrap(),
                            max_clients,
                            protocol_id: 0,
                            authentication: ServerAuthentication::Unsecure,
                            socket_addresses: vec![vec![bind_native], vec![bind_ws]],
                        };

                    let ws_config = WebSocketServerConfig {
                        acceptor: WebSocketAcceptor::Plain { has_tls_proxy: true },
                        listen: bind_ws,
                        max_clients
                    };
                    let ws_server = WebSocketServer::new(ws_config, rt.handle().clone()).unwrap();
                        let rt = tokio::runtime::Runtime::new().unwrap();

                    let native_socket = NativeSocket::new(UdpSocket::bind(bind_native).unwrap()).unwrap();
                        let ws_config = WebSocketServerConfig {
                            acceptor: WebSocketAcceptor::Plain {
                                has_tls_proxy: true,
                            },
                            listen: bind_ws,
                            max_clients,
                        };
                        let ws_server =
                            WebSocketServer::new(ws_config, rt.handle().clone()).unwrap();

                    let transport = NetcodeServerTransport::new_with_sockets(server_config, vec![BoxedSocket::new(native_socket), BoxedSocket::new(ws_server)]).unwrap();
                        let native_socket =
                            NativeSocket::new(UdpSocket::bind(bind_native).unwrap()).unwrap();

                    commands.insert_resource(server);
                    commands.insert_resource(transport);
                        let transport = NetcodeServerTransport::new_with_sockets(
                            server_config,
                            vec![BoxedSocket::new(native_socket), BoxedSocket::new(ws_server)],
                        )
                        .unwrap();

                    info!("websocket/native server listening");
                        commands.insert_resource(server);
                        commands.insert_resource(transport);

                }
            })
                        info!("websocket/native server listening");
                    }
                },
            )
            .add_plugins(planets_plugin)
            .add_plugins(world_config_plugin)
            .add_plugins(newtonian_gravity_plugin)

M crates/unified/src/server/planets.rs => crates/unified/src/server/planets.rs +58 -30
@@ 1,35 1,40 @@
use crate::config::planet::{Planet, PlanetBundle, PlanetConfigCollection};
use bevy::asset::Handle;
use bevy::prelude::*;
use bevy_rapier2d::dynamics::AdditionalMassProperties;
use bevy_rapier2d::prelude::Collider;
use bevy_replicon::prelude::Replicated;
use crate::config::planet::{Planet, PlanetBundle, PlanetConfigCollection};

pub fn planets_plugin(app: &mut App) {
    app
        .init_resource::<PlanetConfigResource>()
    app.init_resource::<PlanetConfigResource>()
        .add_systems(Startup, start_loading_planets)
        .add_systems(Update, update_planets);
}

#[derive(Resource, Default)]
pub struct PlanetConfigResource {
    handle: Option<Handle<PlanetConfigCollection>>
    handle: Option<Handle<PlanetConfigCollection>>,
}

fn start_loading_planets(assets: Res<AssetServer>, mut planets: ResMut<PlanetConfigResource>) {
    planets.handle = Some(assets.load("config/planets.pc.toml"));
}


pub fn update_planets(
    mut commands: Commands,
    mut ev_config: EventReader<AssetEvent<PlanetConfigCollection>>,
    mut assets: ResMut<Assets<PlanetConfigCollection>>,
    mut planets: ResMut<PlanetConfigResource>,
    mut q_planets: Query<(Entity, &mut Planet, &mut Transform, &mut AdditionalMassProperties)>
    assets: ResMut<Assets<PlanetConfigCollection>>,
    planets: ResMut<PlanetConfigResource>,
    mut q_planets: Query<(
        Entity,
        &mut Planet,
        &mut Transform,
        &mut AdditionalMassProperties,
    )>,
) {
    let Some(handle) = planets.handle.as_ref() else { return; };
    let Some(handle) = planets.handle.as_ref() else {
        return;
    };

    let waiting_for_asset_id = handle.id();



@@ 40,47 45,70 @@ pub fn update_planets(
                    info!("planet config loaded - creating planets");
                    let planet_config = assets.get(*id).unwrap();
                    for planet in &planet_config.planets {
                        commands.spawn(PlanetBundle {
                            planet: planet.clone(),
                            transform: Transform::from_xyz(planet.default_transform[0], planet.default_transform[1], planet.default_transform[2]),
                            collider: Collider::ball(planet.radius),
                            additional_mass_properties: AdditionalMassProperties::Mass(planet.mass),
                        }).insert(Replicated);
                        commands
                            .spawn(PlanetBundle {
                                planet: planet.clone(),
                                transform: Transform::from_xyz(
                                    planet.default_transform[0],
                                    planet.default_transform[1],
                                    planet.default_transform[2],
                                ),
                                collider: Collider::ball(planet.radius),
                                additional_mass_properties: AdditionalMassProperties::Mass(
                                    planet.mass,
                                ),
                            })
                            .insert(Replicated);
                        info!(?planet, "new planet spawned");
                    }
                }
            },
            }
            AssetEvent::Modified { id } => {
                if *id == waiting_for_asset_id {
                    info!("planet config modified - reloading planets");
                    let planet_config = assets.get(*id).unwrap();

                    for planet in &planet_config.planets {
                        let existing_planet = q_planets.iter_mut().find(|(_, p, _, _)| p.name == planet.name);
                        let existing_planet = q_planets
                            .iter_mut()
                            .find(|(_, p, _, _)| p.name == planet.name);

                        if let Some((existing, mut e_planet, mut e_transform, mut e_mass)) = existing_planet {
                            commands.entity(existing)
                        if let Some((existing, mut e_planet, mut e_transform, mut e_mass)) =
                            existing_planet
                        {
                            commands
                                .entity(existing)
                                .remove::<Collider>()
                                .insert(Collider::ball(planet.radius));
                            *e_planet = planet.clone();
                            e_transform.translation = Vec3::new(planet.default_transform[0], planet.default_transform[1], planet.default_transform[2]);
                            e_transform.translation = Vec3::new(
                                planet.default_transform[0],
                                planet.default_transform[1],
                                planet.default_transform[2],
                            );
                            *e_mass = AdditionalMassProperties::Mass(planet.mass);
                            info!(?planet, "planet hot-reloaded");
                        } else {
                            commands.spawn(PlanetBundle {
                                planet: planet.clone(),
                                transform: Transform::from_xyz(planet.default_transform[0], planet.default_transform[1], planet.default_transform[2]),
                                collider: Collider::ball(planet.radius),
                                additional_mass_properties: AdditionalMassProperties::Mass(planet.mass),
                            }).insert(Replicated);
                            commands
                                .spawn(PlanetBundle {
                                    planet: planet.clone(),
                                    transform: Transform::from_xyz(
                                        planet.default_transform[0],
                                        planet.default_transform[1],
                                        planet.default_transform[2],
                                    ),
                                    collider: Collider::ball(planet.radius),
                                    additional_mass_properties: AdditionalMassProperties::Mass(
                                        planet.mass,
                                    ),
                                })
                                .insert(Replicated);
                            info!(?planet, "new planet spawned");
                        }


                    }
                }
            },
            }
            _ => {}
        }
    }
}
\ No newline at end of file
}

M crates/unified/src/server/player.rs => crates/unified/src/server/player.rs +67 -33
@@ 1,21 1,20 @@
use std::f32::consts::PI;

use bevy::prelude::*;
use bevy_rapier2d::prelude::{AdditionalMassProperties, Collider, ExternalForce, ExternalImpulse, MassProperties, ReadMassProperties, RigidBody, Sensor};
use bevy_replicon::prelude::{ConnectedClient, FromClient, Replicated};
use crate::config::planet::Planet;
use crate::ecs::{Part, PartBundle, Player, PlayerThrust, ThrustEvent};
use crate::server::world_config::WorldConfigResource;
use bevy::prelude::*;
use bevy_rapier2d::prelude::{
    AdditionalMassProperties, Collider, ExternalForce, ExternalImpulse, MassProperties,
};
use bevy_replicon::prelude::{ConnectedClient, FromClient, Replicated};

pub fn player_management_plugin(app: &mut App) {
    app
        .add_systems(PreUpdate, reset_movement)
    app.add_systems(PreUpdate, reset_movement)
        .add_systems(Update, (handle_new_players, player_thrust));
}

fn reset_movement(
    mut players: Query<(&mut ExternalForce, &mut ExternalImpulse)>
) {
fn reset_movement(mut players: Query<(&mut ExternalForce, &mut ExternalImpulse)>) {
    for (mut force, mut impulse) in &mut players {
        force.force = Vec2::ZERO;
        force.torque = 0.0;


@@ 23,33 22,49 @@ fn reset_movement(
    }
}

fn handle_new_players(mut commands: Commands, q_new_clients: Query<Entity, Added<ConnectedClient>>, world_config: Res<WorldConfigResource>, planets: Query<(&Transform, &Planet)>) {
    let Some(wc) = &world_config.config else { return; };
fn handle_new_players(
    mut commands: Commands,
    q_new_clients: Query<Entity, Added<ConnectedClient>>,
    world_config: Res<WorldConfigResource>,
    planets: Query<(&Transform, &Planet)>,
) {
    let Some(wc) = &world_config.config else {
        return;
    };
    for joined_player in &q_new_clients {
        // find earth
        let (earth_pos, earth_planet) = planets.iter().find(|p| p.1.name == "Earth").expect("earth is missing? (check that the planet is named 'Earth')");
        let (earth_pos, earth_planet) = planets
            .iter()
            .find(|p| p.1.name == "Earth")
            .expect("earth is missing? (check that the planet is named 'Earth')");
        let angle = rand::random::<f32>() * std::f32::consts::TAU;
        let offset = earth_planet.radius + 150.0;
        let mut new_transform = Transform::from_xyz(angle.cos() * offset, angle.sin() * offset, 0.0);
        let mut new_transform =
            Transform::from_xyz(angle.cos() * offset, angle.sin() * offset, 0.0);
        new_transform.rotate_z(angle);
        new_transform.translation += earth_pos.translation;


        commands.entity(joined_player)
        commands
            .entity(joined_player)
            .insert(PartBundle {
                part: Part {
                    sprite: "textures/hearty.png".to_string(),
                    width: wc.part.default_width,
                    height: wc.part.default_height,
                    mass: wc.part.default_mass
                    mass: wc.part.default_mass,
                },
                transform: new_transform,
                collider: Collider::cuboid(wc.part.default_width / 2.0, wc.part.default_height / 2.0),
                additional_mass_properties: AdditionalMassProperties::MassProperties(MassProperties {
                    local_center_of_mass: Vec2::ZERO,
                    mass: wc.part.default_mass,
                    principal_inertia: 7.5,
                })
                collider: Collider::cuboid(
                    wc.part.default_width / 2.0,
                    wc.part.default_height / 2.0,
                ),
                additional_mass_properties: AdditionalMassProperties::MassProperties(
                    MassProperties {
                        local_center_of_mass: Vec2::ZERO,
                        mass: wc.part.default_mass,
                        principal_inertia: 7.5,
                    },
                ),
            })
            .insert(Replicated)
            .insert(ExternalForce::default())


@@ 67,8 82,13 @@ fn player_thrust(
) {
    use ThrustEvent::*;
    for event in thrust_event.read() {
        let FromClient { client_entity, event } = event;
        let Ok((_, _, mut thrust)) = players.get_mut(*client_entity) else { continue };
        let FromClient {
            client_entity,
            event,
        } = event;
        let Ok((_, _, mut thrust)) = players.get_mut(*client_entity) else {
            continue;
        };
        match *event {
            Up(on) => thrust.up = on,
            Down(on) => thrust.down = on,


@@ 77,7 97,9 @@ fn player_thrust(
        }
    }
    for (transform, mut force, thrust) in &mut players {
        let Some(world_config) = &world_config.config else { return; };
        let Some(world_config) = &world_config.config else {
            return;
        };

        let forward = (transform.rotation * Vec3::Y).xy();
        let mut external_force = ExternalForce::default();


@@ 98,25 120,37 @@ fn player_thrust(
            thrusters[1] = 1.0;
            thrusters[3] = 1.0;
        }
        let half_size = Vec2::new(world_config.part.default_width/2.0, world_config.part.default_height/2.0).length();
        let half_size = Vec2::new(
            world_config.part.default_width / 2.0,
            world_config.part.default_height / 2.0,
        )
        .length();
        external_force += ExternalForce::at_point(
            -forward*thrusters[0]*world_config.hearty.thrust,
            transform.translation.xy() + half_size*Vec2::new((1.0*PI/4.0).cos(), (1.0*PI/4.0).sin()).rotate(forward),
            -forward * thrusters[0] * world_config.hearty.thrust,
            transform.translation.xy()
                + half_size
                    * Vec2::new((1.0 * PI / 4.0).cos(), (1.0 * PI / 4.0).sin()).rotate(forward),
            transform.translation.xy(),
        );
        external_force += ExternalForce::at_point(
            forward*thrusters[1]*world_config.hearty.thrust,
            transform.translation.xy() + half_size*Vec2::new((3.0*PI/4.0).cos(), (3.0*PI/4.0).sin()).rotate(forward),
            forward * thrusters[1] * world_config.hearty.thrust,
            transform.translation.xy()
                + half_size
                    * Vec2::new((3.0 * PI / 4.0).cos(), (3.0 * PI / 4.0).sin()).rotate(forward),
            transform.translation.xy(),
        );
        external_force += ExternalForce::at_point(
            forward*thrusters[2]*world_config.hearty.thrust,
            transform.translation.xy() + half_size*Vec2::new((5.0*PI/4.0).cos(), (5.0*PI/4.0).sin()).rotate(forward),
            forward * thrusters[2] * world_config.hearty.thrust,
            transform.translation.xy()
                + half_size
                    * Vec2::new((5.0 * PI / 4.0).cos(), (5.0 * PI / 4.0).sin()).rotate(forward),
            transform.translation.xy(),
        );
        external_force += ExternalForce::at_point(
            -forward*thrusters[3]*world_config.hearty.thrust,
            transform.translation.xy() + half_size*Vec2::new((7.0*PI/4.0).cos(), (7.0*PI/4.0).sin()).rotate(forward),
            -forward * thrusters[3] * world_config.hearty.thrust,
            transform.translation.xy()
                + half_size
                    * Vec2::new((7.0 * PI / 4.0).cos(), (7.0 * PI / 4.0).sin()).rotate(forward),
            transform.translation.xy(),
        );
        *force += external_force;

M crates/unified/src/server/world_config.rs => crates/unified/src/server/world_config.rs +11 -15
@@ 1,14 1,9 @@
use crate::config::world::GlobalWorldConfig;
use bevy::asset::Handle;
use bevy::prelude::*;
use bevy_rapier2d::dynamics::AdditionalMassProperties;
use bevy_rapier2d::prelude::Collider;
use bevy_replicon::prelude::Replicated;
use crate::config::planet::{Planet, PlanetBundle, PlanetConfigCollection};
use crate::config::world::{GlobalWorldConfig, WorldConfig};

pub fn world_config_plugin(app: &mut App) {
    app
        .init_resource::<WorldConfigResource>()
    app.init_resource::<WorldConfigResource>()
        .add_systems(Startup, start_loading_planets)
        .add_systems(Update, update_planets);
}


@@ 16,21 11,22 @@ pub fn world_config_plugin(app: &mut App) {
#[derive(Resource, Default)]
pub struct WorldConfigResource {
    handle: Option<Handle<GlobalWorldConfig>>,
    pub config: Option<GlobalWorldConfig>
    pub config: Option<GlobalWorldConfig>,
}

fn start_loading_planets(assets: Res<AssetServer>, mut planets: ResMut<WorldConfigResource>) {
    planets.handle = Some(assets.load("config/world.wc.toml"));
}


pub fn update_planets(
    mut commands: Commands,
    commands: Commands,
    mut ev_config: EventReader<AssetEvent<GlobalWorldConfig>>,
    mut assets: ResMut<Assets<GlobalWorldConfig>>,
    assets: ResMut<Assets<GlobalWorldConfig>>,
    mut resource: ResMut<WorldConfigResource>,
) {
    let Some(handle) = resource.handle.as_ref() else { return; };
    let Some(handle) = resource.handle.as_ref() else {
        return;
    };

    let waiting_for_asset_id = handle.id();



@@ 42,15 38,15 @@ pub fn update_planets(
                    let world_config = assets.get(*id).unwrap();
                    resource.config = Some(world_config.clone());
                }
            },
            }
            AssetEvent::Modified { id } => {
                if *id == waiting_for_asset_id {
                    info!("world config modified - reloading");
                    let world_config = assets.get(*id).unwrap();
                    resource.config = Some(world_config.clone());
                }
            },
            }
            _ => {}
        }
    }
}
\ No newline at end of file
}

M crates/unified/src/server_plugins.rs => crates/unified/src/server_plugins.rs +11 -15
@@ 1,5 1,5 @@
use std::net::SocketAddr;
use std::time::Duration;
use crate::config::planet::PlanetConfigCollection;
use crate::config::world::GlobalWorldConfig;
use bevy::app::{PluginGroup, PluginGroupBuilder, ScheduleRunnerPlugin, TaskPoolPlugin};
use bevy::asset::AssetPlugin;
use bevy::diagnostic::FrameCountPlugin;


@@ 7,14 7,14 @@ use bevy::time::TimePlugin;
use bevy_common_assets::toml::TomlAssetPlugin;
use bevy_replicon::RepliconPlugins;
use bevy_replicon_renet2::RepliconRenetServerPlugin;
use crate::config::planet::{Planet, PlanetConfigCollection};
use crate::config::world::{GlobalWorldConfig, WorldConfig};
use std::net::SocketAddr;
use std::time::Duration;

pub struct ServerPluginGroup {
    pub bind_ws: SocketAddr,
    pub bind_native: SocketAddr,
    pub tick_rate: f64,
    pub max_clients: usize
    pub max_clients: usize,
}
impl PluginGroup for ServerPluginGroup {
    fn build(self) -> PluginGroupBuilder {


@@ 22,15 22,11 @@ impl PluginGroup for ServerPluginGroup {
            .add(TaskPoolPlugin::default())
            .add(FrameCountPlugin)
            .add(TimePlugin)
            .add(ScheduleRunnerPlugin::run_loop(
                Duration::from_secs_f64(1.0 / self.tick_rate)
            ))
            .add_group(
                RepliconPlugins
            )
            .add(
                RepliconRenetServerPlugin
            )
            .add(ScheduleRunnerPlugin::run_loop(Duration::from_secs_f64(
                1.0 / self.tick_rate,
            )))
            .add_group(RepliconPlugins)
            .add(RepliconRenetServerPlugin)
            /* Assets */
            .add(AssetPlugin::default())
            .add(TomlAssetPlugin::<GlobalWorldConfig>::new(&["wc.toml"]))


@@ 41,4 37,4 @@ impl PluginGroup for ServerPluginGroup {
                max_clients: self.max_clients,
            })
    }
}
\ No newline at end of file
}

M crates/unified/src/shared_plugins.rs => crates/unified/src/shared_plugins.rs +7 -13
@@ 1,26 1,23 @@
use crate::config::planet::Planet;
use crate::ecs::{Ball, Ground, Part, Player, SendBallHere, ThrustEvent};
use bevy::app::{App, PluginGroup, PluginGroupBuilder};
use bevy::prelude::*;
use bevy_rapier2d::prelude::*;
use bevy_replicon::prelude::{AppRuleExt, Channel, ClientEventAppExt, FromClient, ServerEventAppExt};
use crate::config::planet::Planet;
use crate::ecs::{Ball, Ground, Part, Player, SendBallHere, ThrustEvent};
use bevy_replicon::prelude::{AppRuleExt, Channel, ClientEventAppExt};

pub struct SharedPluginGroup;

impl PluginGroup for SharedPluginGroup {
    fn build(self) -> PluginGroupBuilder {
        PluginGroupBuilder::start::<Self>()
            .add(
                RapierPhysicsPlugin::<NoUserData>::pixels_per_meter(100.0)
            )
            .add(RapierPhysicsPlugin::<NoUserData>::pixels_per_meter(100.0))
            .add(register_everything)
            .add(physics_setup_plugin)
    }
}

pub fn register_everything(app: &mut App) {
    app
        .add_client_event::<SendBallHere>(Channel::Ordered)
    app.add_client_event::<SendBallHere>(Channel::Ordered)
        .add_client_event::<ThrustEvent>(Channel::Ordered)
        .replicate::<Transform>()
        .replicate::<Ball>()


@@ 31,14 28,11 @@ pub fn register_everything(app: &mut App) {
        .replicate::<Player>();
}

fn physics_setup_plugin(mut app: &mut App) {
fn physics_setup_plugin(app: &mut App) {
    app.add_systems(Startup, setup_physics);
}


fn setup_physics(
    mut rapier_config: Query<&mut RapierConfiguration>
) {
fn setup_physics(mut rapier_config: Query<&mut RapierConfiguration>) {
    let mut cfg = rapier_config.single_mut().unwrap();
    cfg.gravity = Vec2::ZERO;
}