~starkingdoms/starkingdoms

4ccbefd5580d250ce9105d94129613957cdb1c0d — core 5 months ago 50606a2
chore(fmt,lint): pass clippy
M crates/unified/src/attachment.rs => crates/unified/src/attachment.rs +3 -2
@@ 1,7 1,7 @@
use std::ops::Deref;
use bevy::ecs::entity::MapEntities;
use bevy::prelude::*;
use serde::{Deserialize, Serialize};
use std::ops::Deref;

#[derive(Component, Serialize, Deserialize)]
/// The primary component for a ship structure


@@ 47,7 47,8 @@ impl Deref for Snaps {
#[derive(Serialize, Deserialize)]
pub struct JointId(pub String);
impl JointId {
    #[must_use] pub fn from_part_and_joint_id(part: String, joint: String) -> Self {
    #[must_use]
    pub fn from_part_and_joint_id(part: String, joint: String) -> Self {
        Self(format!("{part}:{joint}"))
    }
}

M crates/unified/src/client/key_input.rs => crates/unified/src/client/key_input.rs +8 -5
@@ 1,5 1,4 @@
use std::ops::Deref;
use crate::attachment::{Joint, JointOf, SnapOf, SnapOfJoint};
use crate::attachment::{JointOf, SnapOf};
use crate::ecs::{Part, ThrustEvent};
use bevy::color::palettes::css::{FUCHSIA, GREEN};
use bevy::dev_tools::picking_debug::DebugPickingMode;


@@ 10,8 9,8 @@ use bevy::{
    ecs::{event::EventWriter, system::Res},
    input::{ButtonInput, keyboard::KeyCode},
};
use bevy::log::debug;
use bevy_rapier2d::render::DebugRenderContext;
use std::ops::Deref;

pub fn key_input_plugin(app: &mut App) {
    app.add_systems(Update, directional_keys)


@@ 84,12 83,16 @@ fn draw_attachment_debug(
        return;
    }
    for (offset, parent) in joints.iter() {
        let Ok(parent_pos) = parts.get(parent.0) else { continue; };
        let Ok(parent_pos) = parts.get(parent.0) else {
            continue;
        };
        let joint_target = parent_pos.transform_point(offset.translation);
        gizmos.cross_2d(joint_target.xy(), 4.0, FUCHSIA);
    }
    for (offset, parent) in snaps.iter() {
        let Ok(parent_pos) = parts.get(parent.0) else { continue; };
        let Ok(parent_pos) = parts.get(parent.0) else {
            continue;
        };
        let joint_snap = parent_pos.transform_point(offset.translation);
        gizmos.cross_2d(joint_snap.xy(), 4.0, GREEN);
    }

M crates/unified/src/client/mod.rs => crates/unified/src/client/mod.rs +4 -1
@@ 70,7 70,10 @@ fn find_me(
            commands.entity(entity).insert(Me);
            let mut heart_sprite =
                Sprite::from_image(asset_server.load("sprites/hearty_heart.png"));
            heart_sprite.custom_size = Some(Vec2::new(part.strong_config.physics.width, part.strong_config.physics.height));
            heart_sprite.custom_size = Some(Vec2::new(
                part.strong_config.physics.width,
                part.strong_config.physics.height,
            ));
            heart_sprite.color = Color::srgb(20.0, 0.0, 0.0);

            commands.spawn((

M crates/unified/src/client/parts.rs => crates/unified/src/client/parts.rs +74 -37
@@ 1,18 1,24 @@
use bevy::color::palettes::css::{ORANGE, RED};
use bevy::color::palettes::tailwind::{CYAN_400, CYAN_800};
use crate::attachment::{JointOf, PartInShip, Peer, SnapOf, SnapOfJoint};
use crate::client::Me;
use crate::client::colors::GREEN;
use crate::client::key_input::AttachmentDebugRes;
use crate::ecs::{CursorWorldCoordinates, DragRequestEvent, Part};
use bevy::color::palettes::css::{ORANGE, RED};
use bevy::prelude::*;
use bevy_rapier2d::dynamics::MassProperties;
use bevy_rapier2d::prelude::AdditionalMassProperties;
use crate::attachment::{JointOf, PartInShip, Peer, SnapOf, SnapOfJoint};
use crate::client::colors::GREEN;
use crate::client::key_input::AttachmentDebugRes;

pub fn parts_plugin(app: &mut App) {
    app.insert_resource(DragResource(None));
    app.insert_resource(SnapResource(None));
    app.add_systems(Update, (handle_incoming_parts, handle_updated_parts, update_drag_ghosts));
    app.add_systems(
        Update,
        (
            handle_incoming_parts,
            handle_updated_parts,
            update_drag_ghosts,
        ),
    );
    app.add_observer(on_part_release);
}



@@ 22,8 28,12 @@ fn handle_incoming_parts(
    asset_server: Res<AssetServer>,
) {
    for (new_entity, new_part) in new_parts.iter() {
        let mut sprite = Sprite::from_image(asset_server.load(&new_part.strong_config.part.sprite_disconnected));
        sprite.custom_size = Some(Vec2::new(new_part.strong_config.physics.width, new_part.strong_config.physics.height));
        let mut sprite =
            Sprite::from_image(asset_server.load(&new_part.strong_config.part.sprite_disconnected));
        sprite.custom_size = Some(Vec2::new(
            new_part.strong_config.physics.width,
            new_part.strong_config.physics.height,
        ));

        commands
            .entity(new_entity)


@@ 43,8 53,13 @@ fn handle_updated_parts(
    asset_server: Res<AssetServer>,
) {
    for (updated_entity, updated_part) in updated_parts.iter() {
        let mut sprite = Sprite::from_image(asset_server.load(&updated_part.strong_config.part.sprite_disconnected));
        sprite.custom_size = Some(Vec2::new(updated_part.strong_config.physics.width, updated_part.strong_config.physics.height));
        let mut sprite = Sprite::from_image(
            asset_server.load(&updated_part.strong_config.part.sprite_disconnected),
        );
        sprite.custom_size = Some(Vec2::new(
            updated_part.strong_config.physics.width,
            updated_part.strong_config.physics.height,
        ));

        commands
            .entity(updated_entity)


@@ 71,7 86,7 @@ fn on_part_click(
    ev: Trigger<Pointer<Pressed>>,
    sprites: Query<(&Sprite, &Transform), Without<Me>>,
    mut drag: ResMut<DragResource>,
    mut commands: Commands
    mut commands: Commands,
) {
    if ev.button != PointerButton::Primary {
        return;


@@ 81,11 96,7 @@ fn on_part_click(
    };
    let mut s = sprite.0.clone();
    s.color = Color::srgba(0.7, 0.7, 0.7, 1.0);
    commands.spawn((
        DragGhost,
        sprite.1.clone(),
        s
    ));
    commands.spawn((DragGhost, *sprite.1, s));

    drag.0 = Some(ev.target());
}


@@ 97,14 108,14 @@ fn on_part_release(
    cursor: Res<CursorWorldCoordinates>,
    mut commands: Commands,
    ghost: Single<(Entity, &Transform), With<DragGhost>>,
    snap: Res<SnapResource>
    snap: Res<SnapResource>,
) {
    if ev.button != PointerButton::Primary {
        return;
    }

    if let Some(e) = drag.0 {
        let mut rotation = ghost.1.rotation;
        let rotation = ghost.1.rotation;
        commands.entity(ghost.0).despawn();

        if let Some(c) = cursor.0 {


@@ 121,14 132,21 @@ fn on_part_release(
    drag.0 = None;
}


/// !IMPORTANT!
/// This function forms the bulk of the attachment system.
/// PLEASE DO NOT MODIFY OR MOVE
///
/// This code is super cursed, and it will break at the lightest breeze
fn update_drag_ghosts(
    mut ghost: Single<&mut Transform, (With<DragGhost>, Without<SnapOf>, Without<JointOf>, Without<Part>)>,
    mut ghost: Single<
        &mut Transform,
        (
            With<DragGhost>,
            Without<SnapOf>,
            Without<JointOf>,
            Without<Part>,
        ),
    >,
    cursor: Res<CursorWorldCoordinates>,
    snaps: Query<(&Transform, &SnapOfJoint, &SnapOf, Entity)>,
    joints: Query<(&Transform, &JointOf, Entity), Without<Peer>>,


@@ 141,46 159,63 @@ fn update_drag_ghosts(
    keys: Res<ButtonInput<KeyCode>>,
    time: Res<Time>,
) {
    let Some(cursor) = cursor.0 else { return };

    const CUTOFF: f32 = 25.0; // px

    let Some(cursor) = cursor.0 else { return };

    let mut best_distance = f32::INFINITY;
    let mut best_target = Transform::from_xyz(cursor.x, cursor.y, 0.0);
    best_target.rotation = ghost.rotation;
    let mut snap = None;
    let mut target_gpos = None;

    for (snap_local_transform, snap_joint, snap_part, snap_id) in &snaps {
        if Some(snap_part.0) == drag.0 { continue; }
        if Some(snap_part.0) == drag.0 {
            continue;
        }

        // only snap to ourselves

        let Ok((parent_position, maybe_me, maybe_parent_ship)) = parts.get(snap_part.0) else { continue; };
        let Ok((parent_position, maybe_me, maybe_parent_ship)) = parts.get(snap_part.0) else {
            continue;
        };

        let allowed = maybe_me.is_some() || maybe_parent_ship.is_some_and(|u| u.0 == *me);
        if !allowed { continue }
        if !allowed {
            continue;
        }

        let snap_global_translation = parent_position.transform_point(snap_local_transform.translation).xy();
        let snap_global_translation = parent_position
            .transform_point(snap_local_transform.translation)
            .xy();

        let distance_to_cursor = cursor.distance(snap_global_translation);

        if distance_to_cursor > best_distance {
            if debug.0 {gizmos.circle_2d(snap_global_translation, 3.0, RED); }
            if debug.0 {
                gizmos.circle_2d(snap_global_translation, 3.0, RED);
            }
            continue;
        }
        if distance_to_cursor > CUTOFF {
            if debug.0 {gizmos.circle_2d(snap_global_translation, 3.0, ORANGE); }
            if debug.0 {
                gizmos.circle_2d(snap_global_translation, 3.0, ORANGE);
            }
            continue;
        }

        if debug.0 { gizmos.circle_2d(snap_global_translation, 3.0, GREEN); }
        if debug.0 {
            gizmos.circle_2d(snap_global_translation, 3.0, GREEN);
        }

        let Ok((offset, _, _)) = joints.get(snap_joint.0) else { continue; };
        let Ok((offset, _, _)) = joints.get(snap_joint.0) else {
            continue;
        };

        let joint_target = parent_position.transform_point(offset.translation);

        if debug.0 { gizmos.circle_2d(joint_target.xy(), 3.0, GREEN); }
        if debug.0 {
            gizmos.circle_2d(joint_target.xy(), 3.0, GREEN);
        }

        let target_transform = Transform {
            translation: joint_target,


@@ 191,18 226,20 @@ fn update_drag_ghosts(
        best_target = target_transform;

        snap = Some(snap_id);
        target_gpos = Some(joint_target);
    }

    **ghost = best_target;

    if keys.pressed(KeyCode::KeyQ) {
        ghost.rotation = ghost.rotation.mul_quat(Quat::from_rotation_z(180.0_f32.to_radians() * time.delta_secs()));
        ghost.rotation = ghost.rotation.mul_quat(Quat::from_rotation_z(
            180.0_f32.to_radians() * time.delta_secs(),
        ));
    }
    if keys.pressed(KeyCode::KeyE) {
        ghost.rotation = ghost.rotation.mul_quat(Quat::from_rotation_z(-180.0_f32.to_radians() * time.delta_secs()));
        ghost.rotation = ghost.rotation.mul_quat(Quat::from_rotation_z(
            -180.0_f32.to_radians() * time.delta_secs(),
        ));
    }

    rsnap.0 = snap;

}
\ No newline at end of file
}

M crates/unified/src/ecs.rs => crates/unified/src/ecs.rs +3 -4
@@ 1,3 1,4 @@
use crate::config::part::PartConfig;
use bevy::ecs::entity::MapEntities;
use bevy::math::{Quat, Vec2};
use bevy::prelude::{Component, Entity, Event, Handle, Resource};


@@ 5,7 6,6 @@ use bevy_rapier2d::dynamics::RigidBody;
use bevy_rapier2d::prelude::*;
use bevy_replicon::prelude::Replicated;
use serde::{Deserialize, Serialize};
use crate::config::part::PartConfig;

#[derive(Component)]
pub struct MainCamera;


@@ 37,7 37,7 @@ pub enum ThrustEvent {
    Replicated
)]
pub struct Part {
    pub strong_config: PartConfig
    pub strong_config: PartConfig,
}
#[derive(Component, Debug)]
pub struct PartHandle(pub Handle<PartConfig>);


@@ 70,6 70,5 @@ pub struct DragRequestEvent {
    pub drag_to: Vec2,
    pub set_rotation: Quat,
    #[entities]
    pub snap_target: Option<Entity>
    pub snap_target: Option<Entity>,
}


M crates/unified/src/lib.rs => crates/unified/src/lib.rs +11 -1
@@ 6,8 6,18 @@
#![warn(clippy::if_then_some_else_none)]
#![allow(clippy::type_complexity, reason = "Bevy makes this a nightmare")]
#![allow(clippy::needless_pass_by_value, reason = "Bevy makes this a nightmare")]
#![allow(clippy::cast_precision_loss, reason = "We cast ints to floats a lot")]
#![allow(
    clippy::cast_precision_loss,
    clippy::cast_possible_truncation,
    clippy::cast_sign_loss,
    reason = "We cast ints to floats a lot"
)]
#![allow(clippy::missing_panics_doc, reason = "Gamedev! We panic a lot")]
#![allow(clippy::too_many_arguments, reason = "Le Bevy:tm:")]
#![allow(
    clippy::too_many_lines,
    reason = "With the three of us, this is impossible"
)]

//! Primary entrypoint for the lib... mostly useful for wasm
#[cfg(target_arch = "wasm32")]

M crates/unified/src/particle_editor/ecs.rs => crates/unified/src/particle_editor/ecs.rs +1 -0
@@ 16,6 16,7 @@ pub struct Particle;
#[derive(Component)]
pub struct LifetimeTimer(pub Timer);
impl LifetimeTimer {
    #[allow(dead_code, reason = "ghostly, what is this for?")]
    pub fn new(lifetime: f32) -> LifetimeTimer {
        LifetimeTimer(Timer::new(
            Duration::from_secs_f32(lifetime),

M crates/unified/src/particle_editor/mod.rs => crates/unified/src/particle_editor/mod.rs +44 -28
@@ 171,9 171,13 @@ fn editor_ui(
                }
                ui.end_row();

                effect.scale = LifetimeCurve(BTreeMap::from_iter(
                    editor_resource.scale_curve.iter().copied(),
                ));
                effect.scale = LifetimeCurve(
                    editor_resource
                        .scale_curve
                        .iter()
                        .copied()
                        .collect::<BTreeMap<_, _>>(),
                );

                ui.separator();
                ui.end_row();


@@ 220,30 224,10 @@ fn editor_ui(
                ui.end_row();

                let curve_copied = editor_resource.color_curve.clone();
                effect.color =
                    LifetimeCurve(BTreeMap::from_iter(curve_copied.iter().map(|(u, v)| {
                        (
                            *u,
                            Color::Srgba(Srgba::new(
                                f32::from(v[0]) / 256.0,
                                f32::from(v[1]) / 256.0,
                                f32::from(v[2]) / 256.0,
                                f32::from(v[3]) / 256.0,
                            )),
                        )
                    })));

                ui.separator();
                ui.end_row();
            });
            ui.horizontal(|ui| {
                if ui.button("Generate").clicked() {
                    effect.scale = LifetimeCurve(BTreeMap::from_iter(
                        editor_resource.scale_curve.iter().copied(),
                    ));
                    let curve_copied = editor_resource.color_curve.clone();
                    effect.color =
                        LifetimeCurve(BTreeMap::from_iter(curve_copied.iter().map(|(u, v)| {
                effect.color = LifetimeCurve(
                    curve_copied
                        .iter()
                        .map(|(u, v)| {
                            (
                                *u,
                                Color::Srgba(Srgba::new(


@@ 253,7 237,39 @@ fn editor_ui(
                                    f32::from(v[3]) / 256.0,
                                )),
                            )
                        })));
                        })
                        .collect::<BTreeMap<_, _>>(),
                );

                ui.separator();
                ui.end_row();
            });
            ui.horizontal(|ui| {
                if ui.button("Generate").clicked() {
                    effect.scale = LifetimeCurve(
                        editor_resource
                            .scale_curve
                            .iter()
                            .copied()
                            .collect::<BTreeMap<_, _>>(),
                    );
                    let curve_copied = editor_resource.color_curve.clone();
                    effect.color = LifetimeCurve(
                        curve_copied
                            .iter()
                            .map(|(u, v)| {
                                (
                                    *u,
                                    Color::Srgba(Srgba::new(
                                        f32::from(v[0]) / 256.0,
                                        f32::from(v[1]) / 256.0,
                                        f32::from(v[2]) / 256.0,
                                        f32::from(v[3]) / 256.0,
                                    )),
                                )
                            })
                            .collect::<BTreeMap<_, _>>(),
                    );

                    editor_resource.ser_field = ron::ser::to_string(effect.as_ref()).unwrap();
                    editor_resource.status = "Ready; Generated OK".to_string();

M crates/unified/src/particles.rs => crates/unified/src/particles.rs +15 -13
@@ 41,19 41,23 @@ impl<P: Lerp + Copy> LifetimeCurve<P> {
    where
        P: 'a,
    {
        Self(BTreeMap::from_iter(
            points.into_iter().map(|u| (OrderedFloat(u.0), u.1)),
        ))
        Self(
            points
                .into_iter()
                .map(|u| (OrderedFloat(u.0), u.1))
                .collect::<BTreeMap<_, _>>(),
        )
    }

    /// Sample for the value at T. Returns None if the curve has no points, or if T is outside
    /// the domain of the points specified.
    #[must_use] pub fn sample(&self, at: f32) -> Option<P> {
    #[must_use]
    pub fn sample(&self, at: f32) -> Option<P> {
        if self.0.is_empty() {
            return None;
        }

        if self.0.len() == 1 && at == self.0.iter().nth(0).unwrap().0.0 {
        if self.0.len() == 1 && (at - self.0.iter().nth(0).unwrap().0.0).abs() < 0.01 {
            return Some(*self.0.iter().nth(0).unwrap().1);
        }



@@ 61,7 65,7 @@ impl<P: Lerp + Copy> LifetimeCurve<P> {
        let mut r1 = self.0.range((Unbounded, Included(OrderedFloat(at))));
        let (a_key, a_val) = r1.next_back()?;

        let (b_key, b_val) = if at == self.0.iter().last().unwrap().0.0 {
        let (b_key, b_val) = if (at - self.0.iter().last().unwrap().0.0).abs() < 0.01 {
            self.0.iter().last().unwrap()
        } else {
            // Find B, the point "to the right" of `at`


@@ 82,19 86,17 @@ impl<P: Lerp + Copy> LifetimeCurve<P> {

    /// Given an input time value, use the ends of the spline's time to clamp
    /// the input value.
    #[must_use] pub fn clamp_time(&self, time: f32) -> Option<f32> {
        let Some(first) = self.0.iter().nth(0) else {
            return None;
        };
        let Some(last) = self.0.iter().last() else {
            return None;
        };
    #[must_use]
    pub fn clamp_time(&self, time: f32) -> Option<f32> {
        let first = self.0.iter().nth(0)?;
        let last = self.0.iter().last()?;

        Some(time.clamp(first.0.0, last.0.0))
    }
}

pub trait Lerp {
    #[must_use]
    fn lerp(&self, other: &Self, t: f32) -> Self;
}
impl Lerp for f32 {

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

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

M crates/unified/src/server/mod.rs => crates/unified/src/server/mod.rs +3 -3
@@ 3,14 3,15 @@ mod gravity;
mod part;
pub mod planets;
pub mod player;
mod world_config;
mod system_sets;
mod world_config;

use crate::server::earth_parts::spawn_parts_plugin;
use crate::server::gravity::newtonian_gravity_plugin;
use crate::server::part::part_management_plugin;
use crate::server::planets::planets_plugin;
use crate::server::player::player_management_plugin;
use crate::server::system_sets::{PlayerInputSet, WorldUpdateSet};
use crate::server::world_config::world_config_plugin;
use aeronet::io::Session;
use aeronet::io::connection::{Disconnected, LocalAddr};


@@ 18,10 19,9 @@ use aeronet::io::server::Server;
use aeronet_replicon::server::AeronetRepliconServer;
use aeronet_websocket::server::WebSocketServer;
use bevy::prelude::*;
use bevy_rapier2d::prelude::PhysicsSet;
use bevy_replicon::prelude::Replicated;
use std::net::SocketAddr;
use bevy_rapier2d::prelude::PhysicsSet;
use crate::server::system_sets::{PlayerInputSet, WorldUpdateSet};

pub struct ServerPlugin {
    pub bind: SocketAddr,

M crates/unified/src/server/part.rs => crates/unified/src/server/part.rs +31 -32
@@ 1,11 1,10 @@
use bevy::ecs::spawn::SpawnRelatedBundle;
use crate::attachment::{Joint, JointId, JointOf, SnapOf, SnapOfJoint};
use crate::config::part::{JointConfig, PartConfig};
use crate::ecs::{Part, PartHandle};
use bevy::prelude::Component;
use bevy::prelude::*;
use bevy_rapier2d::prelude::{AdditionalMassProperties, Collider};
use bevy_replicon::prelude::Replicated;
use crate::attachment::{Joint, JointId, JointOf, SnapOf, SnapOfJoint};
use crate::config::part::{JointConfig, PartConfig};
use crate::ecs::{Part, PartHandle};

pub fn part_management_plugin(app: &mut App) {
    app.add_systems(PreUpdate, (handle_ready_parts, handle_part_reloading));


@@ 21,30 20,38 @@ pub struct SpawnPartBundle {
pub struct SpawnPartRequest(pub Handle<PartConfig>);

// wait for parts assets to be ready, then spawn the full part
fn handle_ready_parts(loading_parts: Query<(Entity, &SpawnPartRequest)>, mut commands: Commands, assets: Res<Assets<PartConfig>>) {
fn handle_ready_parts(
    loading_parts: Query<(Entity, &SpawnPartRequest)>,
    mut commands: Commands,
    assets: Res<Assets<PartConfig>>,
) {
    for (entity, loading_part) in &loading_parts {
        if let Some(strong_config) = assets.get(&loading_part.0) {
            // config is strong; spawn 'er in!
            commands.entity(entity)
            commands
                .entity(entity)
                .insert(calculate_bundle(strong_config, &loading_part.0))
                .remove::<SpawnPartRequest>();
            spawn_joints(strong_config, entity, commands.reborrow());
        }
    }
}
fn handle_part_reloading(existing_parts: Query<(Entity, &PartHandle)>, assets: Res<Assets<PartConfig>>, mut asset_events: EventReader<AssetEvent<PartConfig>>, mut commands: Commands) {
fn handle_part_reloading(
    existing_parts: Query<(Entity, &PartHandle)>,
    assets: Res<Assets<PartConfig>>,
    mut asset_events: EventReader<AssetEvent<PartConfig>>,
    mut commands: Commands,
) {
    for event in asset_events.read() {
        match event {
            AssetEvent::Modified { id } => {
                let config = assets.get(*id).unwrap();
                for existing_part in existing_parts.iter() {
                    if existing_part.1.0.id() == *id {
                        commands.entity(existing_part.0)
                            .insert(calculate_bundle(config, &existing_part.1.0));
                    }
        if let AssetEvent::Modified { id } = event {
            let config = assets.get(*id).unwrap();
            for existing_part in existing_parts.iter() {
                if existing_part.1.0.id() == *id {
                    commands
                        .entity(existing_part.0)
                        .insert(calculate_bundle(config, &existing_part.1.0));
                }
            },
            _ => {}
            }
        }
    }
}


@@ 62,7 69,7 @@ fn calculate_bundle(config: &PartConfig, handle: &Handle<PartConfig>) -> impl Bu
        part_handle,
        collider,
        additional_mass_properties,
        Replicated
        Replicated,
    )
}
fn spawn_joint_bundle(joint: &JointConfig, part: &PartConfig, parent: &Entity) -> impl Bundle {


@@ 73,28 80,20 @@ fn spawn_joint_bundle(joint: &JointConfig, part: &PartConfig, parent: &Entity) -
    let joint_transform: Transform = j_comp.transform;
    let joint_of = JointOf(*parent);

    (
        j_comp,
        joint_transform,
        joint_of,
        Replicated
    )
    (j_comp, joint_transform, joint_of, Replicated)
}
fn spawn_snap_bundle(joint: &JointConfig, parent: &Entity, p_joint: &Entity) -> impl Bundle {
    let snap_transform: Transform = joint.snap.into();
    let snap_for = SnapOf(*parent);
    let snap_of = SnapOfJoint(*p_joint);

    (
        snap_transform,
        snap_for,
        snap_of,
        Replicated
    )
    (snap_transform, snap_for, snap_of, Replicated)
}
fn spawn_joints(config: &PartConfig, parent: Entity, mut commands: Commands) {
    for joint in &config.joints {
        let joint_id = commands.spawn(spawn_joint_bundle(joint, config, &parent)).id();
        let joint_id = commands
            .spawn(spawn_joint_bundle(joint, config, &parent))
            .id();
        commands.spawn(spawn_snap_bundle(joint, &parent, &joint_id));
    }
}
\ No newline at end of file
}

M crates/unified/src/server/player.rs => crates/unified/src/server/player.rs +9 -19
@@ 1,36 1,26 @@
use std::f32::consts::PI;
use std::ops::Deref;
use crate::config::planet::Planet;
use crate::ecs::{
    DragRequestEvent, Part, Player, PlayerThrust, ThrustEvent,
};
use crate::ecs::{DragRequestEvent, Part, Player, PlayerThrust, ThrustEvent};
use crate::server::part::SpawnPartRequest;
use crate::server::system_sets::PlayerInputSet;
use crate::server::world_config::WorldConfigResource;
use crate::server::{ConnectedGameEntity, ConnectedNetworkEntity};
use bevy::prelude::*;
use bevy_rapier2d::prelude::ExternalForce;
use bevy_replicon::prelude::FromClient;
use crate::attachment::{JointOf, PartInShip, Peer, SnapOf, SnapOfJoint, Snaps};
use crate::client::Me;
use crate::server::system_sets::PlayerInputSet;

use std::f32::consts::PI;

pub fn player_management_plugin(app: &mut App) {
    app.add_systems(Update, (handle_new_players, player_thrust, dragging).in_set(PlayerInputSet));
    app.add_systems(
        Update,
        (handle_new_players, player_thrust, dragging).in_set(PlayerInputSet),
    );
}

fn dragging(
    mut events: EventReader<FromClient<DragRequestEvent>>,
    mut parts: Query<&mut Transform, With<Part>>,
    clients: Query<&ConnectedNetworkEntity>,
    snap_parts: Query<(&GlobalTransform, Option<&Me>, Option<&PartInShip>, &Snaps), With<Part>>,
) {
    for FromClient {
        client_entity,
        event,
    } in events.read() {
        let ConnectedNetworkEntity { game_entity } = clients.get(*client_entity).unwrap();

    for FromClient { event, .. } in events.read() {
        let mut part = parts.get_mut(event.drag_target).unwrap();
        part.translation.x = event.drag_to.x;
        part.translation.y = event.drag_to.y;


@@ 83,7 73,7 @@ fn handle_new_players(
}

fn player_thrust(
    mut players: Query<(&Transform, &mut ExternalForce, &mut PlayerThrust), >,
    mut players: Query<(&Transform, &mut ExternalForce, &mut PlayerThrust)>,
    clients: Query<&ConnectedNetworkEntity>,
    mut thrust_event: EventReader<FromClient<ThrustEvent>>,
    world_config: Res<WorldConfigResource>,

M crates/unified/src/server/system_sets.rs => crates/unified/src/server/system_sets.rs +1 -1
@@ 3,4 3,4 @@ use bevy::prelude::SystemSet;
#[derive(SystemSet, Clone, PartialEq, Eq, Debug, Hash)]
pub struct WorldUpdateSet;
#[derive(SystemSet, Clone, PartialEq, Eq, Debug, Hash)]
pub struct PlayerInputSet;
\ No newline at end of file
pub struct PlayerInputSet;

M crates/unified/src/server_plugins.rs => crates/unified/src/server_plugins.rs +1 -3
@@ 3,9 3,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::{PluginGroup, PluginGroupBuilder, ScheduleRunnerPlugin, TaskPoolPlugin};
use bevy::asset::AssetPlugin;
use bevy::diagnostic::FrameCountPlugin;
use bevy::time::TimePlugin;

M crates/unified/src/shared_plugins.rs => crates/unified/src/shared_plugins.rs +1 -1
@@ 1,4 1,4 @@
use crate::attachment::{Joint, JointOf, SnapOfJoint, PartInShip, Peer, Ship, SnapOf};
use crate::attachment::{Joint, JointOf, PartInShip, Peer, Ship, SnapOf, SnapOfJoint};
use crate::config::planet::Planet;
use crate::ecs::{DragRequestEvent, Part, Particles, Player, ThrustEvent};
use bevy::app::{App, PluginGroup, PluginGroupBuilder};