~starkingdoms/starkingdoms

086b9dd63882d67e66ae92c36ac6e7b31d076743 — core 5 months ago bfce7aa
fix: parts
M crates/unified/assets/config/parts/chassis.part.toml => crates/unified/assets/config/parts/chassis.part.toml +19 -3
@@ 8,7 8,23 @@ width = 50
height = 50
mass = 100

# North
[[joints]]
translation = [ 0.0, 50.0, 0.0 ]
rotation = 0.0
\ No newline at end of file
id = "Top"
target = { translation = [ 0.0, 50.0, 0.0 ], rotation = 0.0 }
snap = { translation = [ 0.0, 25.0, 0.0 ], rotation = 0.0 }


[[joints]]
id = "Right"
target = { translation = [ 50.0, 0.0, 0.0 ], rotation = 90.0 }
snap = { translation = [ 25.0, 0.0, 0.0 ], rotation = 90.0 }

[[joints]]
id = "Bottom"
target = { translation = [ 0.0, -50.0, 0.0 ], rotation = 180.0 }
snap = { translation = [ 0.0, -25.0, 0.0 ], rotation = 180.0 }

[[joints]]
id = "Left"
target = { translation = [ -50.0, 0.0, 0.0 ], rotation = 270.0 }
snap = { translation = [ -25.0, 0.0, 0.0 ], rotation = 270.0 }
\ No newline at end of file

M crates/unified/assets/config/parts/hearty.part.toml => crates/unified/assets/config/parts/hearty.part.toml +19 -3
@@ 8,7 8,23 @@ width = 50
height = 50
mass = 100

# North
[[joints]]
translation = [ 0.0, 50.0, 0.0 ]
rotation = 0.0
\ No newline at end of file
id = "Top"
target = { translation = [ 0.0, 50.0, 0.0 ], rotation = 0.0 }
snap = { translation = [ 0.0, 25.0, 0.0 ], rotation = 0.0 }


[[joints]]
id = "Right"
target = { translation = [ 50.0, 0.0, 0.0 ], rotation = 90.0 }
snap = { translation = [ 25.0, 0.0, 0.0 ], rotation = 90.0 }

[[joints]]
id = "Bottom"
target = { translation = [ 0.0, -50.0, 0.0 ], rotation = 180.0 }
snap = { translation = [ 0.0, -25.0, 0.0 ], rotation = 180.0 }

[[joints]]
id = "Left"
target = { translation = [ -50.0, 0.0, 0.0 ], rotation = 270.0 }
snap = { translation = [ -25.0, 0.0, 0.0 ], rotation = 270.0 }
\ No newline at end of file

M crates/unified/src/attachment.rs => crates/unified/src/attachment.rs +32 -3
@@ 1,3 1,6 @@
use std::ops::Deref;
use bevy::asset::processor::ErasedProcessor;
use bevy::ecs::entity::MapEntities;
use bevy::prelude::*;
use serde::{Deserialize, Serialize};



@@ 7,15 10,41 @@ pub struct Ship;

#[derive(Component, Serialize, Deserialize)]
#[relationship_target(relationship = PartInShip, linked_spawn)]
pub struct Parts(Vec<Entity>);
pub struct Parts(#[entities] Vec<Entity>);

#[derive(Component, Serialize, Deserialize)]
#[relationship(relationship_target = Parts)]
pub struct PartInShip(Entity);
pub struct PartInShip(#[entities] Entity);


#[derive(Component, Serialize, Deserialize)]
pub struct Joint {
    pub id: JointId,
    pub transform: Transform
}
#[derive(Component, Serialize, Deserialize)]
pub struct Peer(Entity);
\ No newline at end of file
pub struct Peer(#[entities] Entity);

#[derive(Component, Serialize, Deserialize)]
#[relationship(relationship_target = Joints)]
pub struct JointOf(#[entities] pub Entity);
#[derive(Component, Serialize, Deserialize)]
#[relationship_target(relationship = JointOf)]
pub struct Joints(#[entities] Vec<Entity>);
#[derive(Component, Serialize, Deserialize)]
#[relationship(relationship_target = JointSnaps)]
pub struct JointSnapOf(#[entities] pub Entity);
#[derive(Component, Serialize, Deserialize)]
#[relationship_target(relationship = JointSnapOf)]
pub struct JointSnaps(#[entities] Vec<Entity>);

#[derive(Serialize, Deserialize)]
pub struct JointId(pub String);
impl JointId {
    pub fn from_part_and_joint_id(part: String, joint: String) -> Self {
        Self(format!("{part}:{joint}"))
    }
}

#[derive(Serialize, Deserialize, Component)]
pub struct JointSnapFor(#[entities] pub Entity);
\ No newline at end of file

M crates/unified/src/client/key_input.rs => crates/unified/src/client/key_input.rs +36 -6
@@ 3,28 3,40 @@ use bevy::{
    ecs::{event::EventWriter, system::Res},
    input::{ButtonInput, keyboard::KeyCode},
};
use bevy::color::palettes::css::{FUCHSIA, GREEN};
use bevy::dev_tools::picking_debug::DebugPickingMode;
use bevy::log::info;
use bevy::prelude::ResMut;
use bevy::gizmos::AppGizmoBuilder;
use bevy::log::{debug, info};
use bevy::math::Vec3Swizzles;
use bevy::prelude::{ChildOf, GizmoConfigGroup, Gizmos, GlobalTransform, IntoScheduleConfigs, Query, Reflect, ResMut, Resource, Transform, With};
use bevy_rapier2d::render::DebugRenderContext;
use crate::ecs::ThrustEvent;
use crate::attachment::{Joint, JointSnapFor};
use crate::ecs::{Part, ThrustEvent};

pub fn key_input_plugin(app: &mut App) {
    app.add_systems(Update, directional_keys)
        .add_systems(Update, debug_render_keybind);
        .add_systems(Update, debug_render_keybind)
        .init_resource::<AttachmentDebugRes>()
        .add_systems(Update, draw_attachment_debug);
}

#[derive(Resource, Default)]
struct AttachmentDebugRes(bool);

fn debug_render_keybind(
    keys: Res<ButtonInput<KeyCode>>,
    mut debug_render: ResMut<DebugRenderContext>,
    mut picking_debug_mode: ResMut<DebugPickingMode>
    mut picking_debug_mode: ResMut<DebugPickingMode>,
    mut attachment_debug: ResMut<AttachmentDebugRes>,
) {
    if keys.just_pressed(KeyCode::F3) {
        debug_render.enabled = !debug_render.enabled;
    }
    if keys.just_pressed(KeyCode::F4) {
        *picking_debug_mode = DebugPickingMode::Noisy;
        info!("{:?}", picking_debug_mode);
    }
    if keys.just_pressed(KeyCode::F5) {
        attachment_debug.0 = !attachment_debug.0;
    }
}



@@ 57,3 69,21 @@ fn directional_keys(
        thrust_event.write(ThrustEvent::Right(false));
    }
}

fn draw_attachment_debug(joints: Query<&GlobalTransform, With<Joint>>, snaps: Query<&GlobalTransform, With<JointSnapFor>>, mut gizmos: Gizmos, mut state: ResMut<AttachmentDebugRes>) {
    if !state.0 { return; }
    for joint_target in joints.iter() {
        gizmos.cross_2d(
            joint_target.translation().xy(),
            4.0,
            FUCHSIA
        );
    }
    for joint_snap in snaps.iter() {
        gizmos.cross_2d(
            joint_snap.translation().xy(),
            4.0,
            GREEN
        );
    }
}
\ No newline at end of file

M crates/unified/src/client/mod.rs => crates/unified/src/client/mod.rs +0 -1
@@ 70,7 70,6 @@ fn find_me(
    for (entity, player, part) in q_clients.iter() {
        let this_id_clientside = entity_map.to_client().get(&player.client).unwrap();
        if *this_id_clientside == entity {
            info!("hi! i've been found!");
            commands.entity(entity).insert(Me);
            let mut heart_sprite = Sprite::from_image(asset_server.load("sprites/heart_sprite.png"));
            heart_sprite.custom_size = Some(Vec2::new(part.width, part.height));

M crates/unified/src/client/parts.rs => crates/unified/src/client/parts.rs +3 -7
@@ 18,7 18,7 @@ fn handle_incoming_parts(
    asset_server: Res<AssetServer>,
) {
    for (new_entity, new_part) in new_parts.iter() {
        info!(?new_entity, ?new_part, "new part");
        trace!(?new_entity, ?new_part, "new part");
        let mut sprite = Sprite::from_image(asset_server.load(&new_part.sprite));
        sprite.custom_size = Some(Vec2::new(new_part.width, new_part.height));



@@ 35,7 35,7 @@ fn handle_incoming_parts(
            .insert(Pickable::default())
            .observe(on_part_click);

        info!(?new_part, ?new_entity, "prepared new part");
        trace!(?new_part, ?new_entity, "prepared new part");
    }
}
fn handle_updated_parts(


@@ 57,7 57,7 @@ fn handle_updated_parts(
                mass: updated_part.mass,
                principal_inertia: 7.5,
            }));
        info!(?updated_part, ?updated_entity, "updated part");
        trace!(?updated_part, ?updated_entity, "updated part");
    }
}



@@ 72,8 72,6 @@ fn on_part_click(ev: Trigger<Pointer<Pressed>>, sprites: Query<&Sprite, Without<
    drag.0 = Some(ev.target());

    events.write(PartDragControlEvent::Start(ev.target()));

    info!(?sprite, ?ev, "start drag");
}
fn on_part_release(ev: Trigger<Pointer<Released>>, mut drag: ResMut<DragResource>, mut events: EventWriter<PartDragControlEvent>) {
    if ev.button != PointerButton::Primary { return; };


@@ 83,8 81,6 @@ fn on_part_release(ev: Trigger<Pointer<Released>>, mut drag: ResMut<DragResource
    }

    drag.0 = None;

    info!(?ev, "stop drag");
}
fn send_drag(drag: ResMut<DragResource>, coords: Res<CursorWorldCoordinates>, mut events: EventWriter<PartDragEvent>) {
    let Some(dragging) = drag.0 else { return; };

M crates/unified/src/client/planet/incoming_planets.rs => crates/unified/src/client/planet/incoming_planets.rs +2 -2
@@ 27,7 27,7 @@ fn handle_incoming_planets(
    commands.insert(AdditionalMassProperties::Mass(new_planet.mass))
    .insert(sprite);

        info!(?new_planet, "prepared new planet");
        trace!(?new_planet, "prepared new planet");
    }
}
fn handle_updated_planets(


@@ 56,6 56,6 @@ fn handle_updated_planets(



        info!(?updated_planet, "updated planet");
        trace!(?updated_planet, "updated planet");
    }
}

M crates/unified/src/config/part.rs => crates/unified/src/config/part.rs +22 -7
@@ 1,6 1,6 @@
use bevy::asset::Asset;
use bevy::math::Vec3;
use bevy::prelude::TypePath;
use bevy::math::{Quat, Vec3};
use bevy::prelude::{Component, Transform, TypePath};
use serde::{Deserialize, Serialize};

#[derive(Deserialize, TypePath, Serialize, Clone, Debug, PartialEq, Asset)]


@@ 9,20 9,35 @@ pub struct PartConfig {
    pub physics: PartPhysicsConfig,
    pub joints: Vec<JointConfig>
}
#[derive(Deserialize, TypePath, Serialize, Clone, Debug, PartialEq, Asset)]
#[derive(Deserialize, TypePath, Serialize, Clone, Debug, PartialEq)]
pub struct PartPartConfig {
    pub name: String,
    pub sprite_connected: String,
    pub sprite_disconnected: String,
}
#[derive(Deserialize, TypePath, Serialize, Clone, Debug, PartialEq, Asset)]
#[derive(Deserialize, TypePath, Serialize, Clone, Debug, PartialEq)]
pub struct PartPhysicsConfig {
    pub width: f32,
    pub height: f32,
    pub mass: f32
}
#[derive(Deserialize, TypePath, Serialize, Clone, Debug, PartialEq, Asset)]
#[derive(Deserialize, TypePath, Serialize, Clone, Debug, PartialEq)]
pub struct JointConfig {
    pub id: String,
    pub target: JointOffset,
    pub snap: JointOffset,
}
#[derive(Deserialize, Serialize, Clone, Debug, TypePath, PartialEq, Copy)]
pub struct JointOffset {
    pub translation: Vec3,
    pub rotation: f32
}
\ No newline at end of file
    pub rotation: f32,
}
impl From<JointOffset> for Transform {
    fn from(value: JointOffset) -> Self {
        Transform {
            translation: value.translation,
            rotation: Quat::from_rotation_z(value.rotation),
            ..Default::default()
        }
    }
    }
\ No newline at end of file

M crates/unified/src/server/gravity.rs => crates/unified/src/server/gravity.rs +0 -2
@@ 23,8 23,6 @@ fn update_gravity(
        let part_mass = part_mass.mass;
        let part_translation = part_transform.translation;

        debug!(?part_transform, "part transform");

        for (planet_transform, planet_mass) in &planet_query {
            let planet_mass = planet_mass.mass;
            let planet_translation = planet_transform.translation;

M crates/unified/src/server/part.rs => crates/unified/src/server/part.rs +54 -13
@@ 2,7 2,8 @@ use bevy::asset::Handle;
use bevy::prelude::*;
use bevy_rapier2d::prelude::{AdditionalMassProperties, Collider};
use bevy_replicon::prelude::Replicated;
use crate::config::part::PartConfig;
use crate::attachment::{Joint, JointId, JointOf, JointSnapFor, JointSnaps, Joints};
use crate::config::part::{JointOffset, PartConfig};
use crate::ecs::Part;

pub fn part_config_plugin(app: &mut App) {


@@ 21,9 22,9 @@ struct PartType(AssetId<PartConfig>);
struct LiveConfigHandle(Handle<PartConfig>);

// watch for new SpawnPart components and start loading their config files
fn handle_spawn_part_requests(new_parts: Query<(Entity, &SpawnPart), Added<SpawnPart>>, mut commands: Commands, asset_server: Res<AssetServer>, assets: Res<Assets<PartConfig>>) {
fn handle_spawn_part_requests(new_parts: Query<(Entity, &SpawnPart), Added<SpawnPart>>, mut commands: Commands, asset_server: Res<AssetServer>, assets: Res<Assets<PartConfig>>, parts: Query<(&Joints, &JointSnaps), With<Part>>,) {
    for (new_part, request) in &new_parts {
        info!(?new_part, ?request, "answering part request");
        trace!(?new_part, ?request, "answering part request");

        let hdl: Handle<PartConfig> = asset_server.load(request.0.clone());



@@ 32,7 33,7 @@ fn handle_spawn_part_requests(new_parts: Query<(Entity, &SpawnPart), Added<Spawn
            .insert(LiveConfigHandle(hdl.clone()));

        if let Some(cfg) = assets.get(&hdl) {
            spawn_part(commands.reborrow(), new_part, cfg, &hdl.id(), hdl.clone());
            spawn_part(commands.reborrow(), new_part, cfg, &hdl.id(), parts, false);
        } else {
            commands.entity(new_part)
                .insert(LoadingPart(hdl.clone()));


@@ 44,29 45,27 @@ fn update_part_requests(
    loading_parts: Query<(Entity, &LoadingPart)>,
    existing_parts: Query<(Entity, &PartType)>,
    mut assets: ResMut<Assets<PartConfig>>,
    mut commands: Commands
    mut commands: Commands,
    parts: Query<(&Joints, &JointSnaps), With<Part>>,
) {
    for ev in ev_config.read() {
        match ev {
            AssetEvent::Added { id } => {
                info!(?id, "asset added");
                trace!(?id, "asset added");
                for (loading_part, req) in &loading_parts {
                    if req.0.id() == *id {
                        let strong_handle = assets.get_strong_handle(*id).unwrap();
                        let Some(asset) = assets.get(*id) else { continue; };

                        spawn_part(commands.reborrow(), loading_part, asset, id, strong_handle);
                        spawn_part(commands.reborrow(), loading_part, asset, id, parts, false);
                    }
                }
            },
            AssetEvent::Modified { id } => {
                info!(?id, "updating part");
                trace!(?id, "updating part");
                for (existing_part, ptype) in &existing_parts {
                    if ptype.0 == *id {
                        let strong_handle = assets.get_strong_handle(*id).unwrap();
                        let Some(asset) = assets.get(ptype.0) else { continue; };

                        spawn_part(commands.reborrow(), existing_part, asset, id, strong_handle);
                        spawn_part(commands.reborrow(), existing_part, asset, id, parts, true);
                    }
                }
            }


@@ 75,7 74,7 @@ fn update_part_requests(
    }
}

fn spawn_part(mut commands: Commands, entity: Entity, part: &PartConfig, id: &AssetId<PartConfig>, strong_handle: Handle<PartConfig>) {
fn spawn_part(mut commands: Commands, entity: Entity, part: &PartConfig, id: &AssetId<PartConfig>, parts: Query<(&Joints, &JointSnaps), With<Part>>, is_update: bool) {
    commands.entity(entity)
        .remove::<LoadingPart>()
        .insert(Part {


@@ 88,4 87,46 @@ fn spawn_part(mut commands: Commands, entity: Entity, part: &PartConfig, id: &As
        .insert(AdditionalMassProperties::Mass(part.physics.mass))
        .insert(PartType(*id))
        .insert(Replicated);

    for joint in &part.joints {
        if is_update {
            // find all entities
            for (joints, snaps) in &parts {
                for joint_entity in joints.iter() {
                    commands.entity(joint_entity).insert((
                        ChildOf(entity),
                        Joint {
                            id: JointId::from_part_and_joint_id(part.part.name.clone(), joint.id.clone()),
                            transform: joint.target.into()
                        },
                        <JointOffset as Into<Transform>>::into(joint.target),
                        JointOf(entity),
                        Replicated,
                    ));
                }
                for snap_entity in snaps.iter() {
                    commands.entity(snap_entity).insert((
                        <JointOffset as Into<Transform>>::into(joint.snap),
                    ));
                }
            }
        } else {
            let e = commands.spawn((
                ChildOf(entity),
                Joint {
                    id: JointId::from_part_and_joint_id(part.part.name.clone(), joint.id.clone()),
                    transform: joint.target.into()
                },
                <JointOffset as Into<Transform>>::into(joint.target),
                JointOf(entity),
                Replicated,
            )).id();
            commands.spawn((
                ChildOf(entity),
                JointSnapFor(e),
                <JointOffset as Into<Transform>>::into(joint.snap),
                Replicated,
            ));
        }
    }
}
\ No newline at end of file

M crates/unified/src/server/part_dragging.rs => crates/unified/src/server/part_dragging.rs +2 -4
@@ 29,12 29,10 @@ fn handle_start_stop_drag(mut colliders: Query<&Collider, With<Part>>, mut event
        match &event.event {
            PartDragControlEvent::Start(_) => {
                commands.entity(entity)
                    .insert(BeingDragged)
                    .insert(ColliderDisabled);
                    .insert(BeingDragged);
            },
            PartDragControlEvent::Stop(_) => {
                commands.entity(entity).remove::<BeingDragged>()
                    .remove::<ColliderDisabled>();
                commands.entity(entity).remove::<BeingDragged>();
            }
        }
    }

M crates/unified/src/server/planets.rs => crates/unified/src/server/planets.rs +5 -5
@@ 42,7 42,7 @@ pub fn update_planets(
        match ev {
            AssetEvent::Added { id } => {
                if *id == waiting_for_asset_id {
                    info!("planet config loaded - creating planets");
                    debug!("planet config loaded - creating planets");
                    let planet_config = assets.get(*id).unwrap();
                    for planet in &planet_config.planets {
                        commands


@@ 59,13 59,13 @@ pub fn update_planets(
                                ),
                            })
                            .insert(Replicated);
                        info!(?planet, "new planet spawned");
                        trace!(?planet, "new planet spawned");
                    }
                }
            }
            AssetEvent::Modified { id } => {
                if *id == waiting_for_asset_id {
                    info!("planet config modified - reloading planets");
                    trace!("planet config modified - reloading planets");
                    let planet_config = assets.get(*id).unwrap();

                    for planet in &planet_config.planets {


@@ 87,7 87,7 @@ pub fn update_planets(
                                planet.default_transform[2],
                            );
                            *e_mass = AdditionalMassProperties::Mass(planet.mass);
                            info!(?planet, "planet hot-reloaded");
                            trace!(?planet, "planet hot-reloaded");
                        } else {
                            commands
                                .spawn(PlanetBundle {


@@ 103,7 103,7 @@ pub fn update_planets(
                                    ),
                                })
                                .insert(Replicated);
                            info!(?planet, "new planet spawned");
                            trace!(?planet, "new planet spawned");
                        }
                    }
                }

M crates/unified/src/server/player.rs => crates/unified/src/server/player.rs +1 -87
@@ 35,7 35,7 @@ fn handle_new_players(
        return;
    };
    for joined_player in &q_new_clients {
        info!(?joined_player, "detected joined player!");
        trace!(?joined_player, "detected joined player!");
        // find earth
        let (spawn_planet_pos, spawn_planet) = planets
            .iter()


@@ 57,92 57,6 @@ fn handle_new_players(
            .insert(Player {
                client: 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,
                },
                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,
                    },
                ),
            })
            .insert(Replicated)
            .insert(ExternalForce::default())
            .insert(PlayerThrust::default())
            .insert(Player {
                client: joined_player,
            })
            .insert(children![
                // bottom left
                (
                    Particles {
                        effect: "particles/ship_thruster.particle.ron".to_string(),
                        active: false
                    },
                    Transform::from_xyz(
                        -wc.part.default_width / 2.0 + 5.0,
                        -wc.part.default_height / 2.0,
                        0.0
                    )
                    .with_rotation(Quat::from_rotation_z(180.0f32.to_radians())),
                    Replicated
                ),
                // bottom right
                (
                    Particles {
                        effect: "particles/ship_thruster.particle.ron".to_string(),
                        active: false
                    },
                    Transform::from_xyz(
                        wc.part.default_width / 2.0 - 5.0,
                        -wc.part.default_height / 2.0,
                        0.0
                    )
                    .with_rotation(Quat::from_rotation_z(180.0f32.to_radians())),
                    Replicated
                ),
                // top left
                (
                    Particles {
                        effect: "particles/ship_thruster.particle.ron".to_string(),
                        active: false
                    },
                    Transform::from_xyz(
                        -wc.part.default_width / 2.0 + 5.0,
                        wc.part.default_height / 2.0,
                        0.0
                    ),
                    Replicated
                ),
                // top right
                (
                    Particles {
                        effect: "particles/ship_thruster.particle.ron".to_string(),
                        active: false
                    },
                    Transform::from_xyz(
                        wc.part.default_width / 2.0 - 5.0,
                        wc.part.default_height / 2.0,
                        0.0
                    ),
                    Replicated
                ),
            ]);*/
    }
}


M crates/unified/src/server_plugins.rs => crates/unified/src/server_plugins.rs +8 -1
@@ 2,7 2,7 @@ use crate::config::planet::PlanetConfigCollection;
use crate::config::world::{GlobalWorldConfig};
use aeronet_replicon::server::AeronetRepliconServerPlugin;
use aeronet_websocket::server::WebSocketServerPlugin;
use bevy::app::{PluginGroup, PluginGroupBuilder, ScheduleRunnerPlugin, TaskPoolPlugin};
use bevy::app::{App, PluginGroup, PluginGroupBuilder, ScheduleRunnerPlugin, Startup, TaskPoolPlugin};
use bevy::asset::AssetPlugin;
use bevy::diagnostic::FrameCountPlugin;
use bevy::time::TimePlugin;


@@ 10,6 10,9 @@ use bevy_common_assets::toml::TomlAssetPlugin;
use bevy_replicon::RepliconPlugins;
use std::net::SocketAddr;
use std::time::Duration;
use bevy::math::Vec2;
use bevy::prelude::Query;
use bevy_rapier2d::plugin::{NoUserData, RapierConfiguration, RapierPhysicsPlugin};
use crate::config::part::PartConfig;

pub struct ServerPluginGroup {


@@ 29,14 32,18 @@ impl PluginGroup for ServerPluginGroup {
            .add_group(RepliconPlugins)
            .add(WebSocketServerPlugin)
            .add(AeronetRepliconServerPlugin)

            /* Assets */
            .add(AssetPlugin::default())
            .add(TomlAssetPlugin::<GlobalWorldConfig>::new(&["wc.toml"]))
            .add(TomlAssetPlugin::<PlanetConfigCollection>::new(&["pc.toml"]))
            .add(TomlAssetPlugin::<PartConfig>::new(&["part.toml"]))

            .add(crate::server::ServerPlugin {
                bind: self.bind,
                max_clients: self.max_clients,
            })
    }
}



M crates/unified/src/shared_plugins.rs => crates/unified/src/shared_plugins.rs +6 -3
@@ 4,7 4,7 @@ use bevy::app::{App, PluginGroup, PluginGroupBuilder};
use bevy::prelude::*;
use bevy_rapier2d::prelude::*;
use bevy_replicon::prelude::{AppRuleExt, Channel, ClientEventAppExt};
use crate::attachment::{Joint, PartInShip, Peer, Ship};
use crate::attachment::{Joint, JointOf, JointSnapFor, PartInShip, Peer, Ship};
use crate::clientevent::{PartDragControlEvent, PartDragEvent};

pub struct SharedPluginGroup;


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

            .add(register_everything)
    }
}



@@ 35,7 36,9 @@ pub fn register_everything(app: &mut App) {
        .replicate::<Ship>()
        .replicate::<PartInShip>()
        .replicate::<Joint>()
        .replicate::<Peer>();
        .replicate::<Peer>()
        .replicate::<JointOf>()
        .replicate::<JointSnapFor>();
}

fn physics_setup_plugin(app: &mut App) {