~starkingdoms/starkingdoms

66e5b4bcfec6d5c81b80e1d2b61e6d1fed199e3f — ghostlyzsh 1 year, 4 months ago 4daa8be
oops now basic organizational fixes are done
M server/src/main.rs => server/src/main.rs +18 -760
@@ 17,51 17,44 @@
#![allow(clippy::too_many_arguments)] // bevy :(
#![allow(clippy::only_used_in_recursion)] // todo: remove this

use crate::mathutil::rot2d;
use crate::ws::{StkTungsteniteServerConfig, StkTungsteniteServerPlugin, WsEvent};
use bevy::math::{vec2, vec3};
use crate::ws::{StkTungsteniteServerConfig, StkTungsteniteServerPlugin};
use bevy::{
    app::{PluginGroupBuilder, ScheduleRunnerPlugin},
    ecs::event::ManualEventReader,
    prelude::*,
    time::TimePlugin,
};
use bevy_rapier2d::prelude::*;
use component::*;
use hmac::{Hmac, Mac};
use jwt::VerifyWithKey;
use module::component::{Attach, CanAttach, LooseAttach, ModuleTimer, PartBundle, PartFlags, PartType};
use module::save::{construct_save_data, load_savefile};
use module::thruster::search_thrusters;
use module::{attach_on_module_tree, despawn_module_tree, detach_recursive};
use module::component::{
    Attach, CanAttach, LooseAttach, ModuleTimer, PartBundle, PartFlags, PartType,
};
use packet::*;
use planet::PlanetType;
use rand::Rng;
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use starkingdoms_common::{pack_savefile, unpack_savefile, SaveData};
use std::fs;

use crate::config::{PartsConfig, PhysicsSolver, PlanetsConfig, StkConfig};
use std::sync::OnceLock;
use std::time::Duration;

pub mod component;
mod config;
#[macro_use]
pub mod config;
pub mod macros;
pub mod mathutil;
pub mod module;
pub mod packet;
pub mod ws;
pub mod planet;
pub mod module;
pub mod player;
pub mod ws;

struct StkPluginGroup;

#[derive(Resource)]
pub struct AppKeys {
    pub app_key: Vec<u8>,
}

// factor to multiply positions by to send to the client
pub static CLIENT_SCALE: f32 = 50.0;
// half size of hearty
pub static PART_HALF_SIZE: f32 = 25.0;

// good ol' classic almost useless but still necessary code
static _SERVER_CONFIG: OnceLock<StkConfig> = OnceLock::new();


@@ 160,13 153,13 @@ fn main() {
        .add_systems(Startup, setup_integration_parameters)
        .add_systems(Startup, planet::spawn_planets)
        .add_systems(FixedUpdate, module::module_spawn)
        .add_systems(Update, on_message)
        .add_systems(Update, on_close)
        .add_systems(FixedUpdate, send_player_energy)
        .add_systems(FixedUpdate, on_position_change)
        .add_systems(Update, player::on_message)
        .add_systems(Update, player::packet::on_close)
        .add_systems(FixedUpdate, player::packet::send_player_energy)
        .add_systems(FixedUpdate, player::packet::on_position_change)
        .add_systems(
            FixedUpdate,
            (module::break_modules, gravity_update, player_input_update).chain(),
            (module::break_modules, gravity_update, player::player_input_update).chain(),
        )
        .add_systems(FixedUpdate, module::save::save_eligibility)
        .add_systems(FixedUpdate, module::convert_modules)


@@ 196,741 189,6 @@ fn setup_integration_parameters(mut context: ResMut<RapierContext>, server_confi
    }
}

fn on_message(
    mut commands: Commands,
    planet_query: Query<(Entity, &PlanetType, &Transform)>,
    mut part_query: Query<
        (
            Entity,
            &PartType,
            &mut Transform,
            &mut Velocity,
            Option<&LooseAttach>,
            &mut PartFlags,
        ),
        (Without<PlanetType>, Without<Player>, Without<Attach>),
    >,
    mut attached_query: Query<
        (
            Entity,
            &PartType,
            &mut Transform,
            &mut Attach,
            &Velocity,
            Option<&CanAttach>,
            Option<&LooseAttach>,
            &mut PartFlags,
        ),
        (Without<PlanetType>, Without<Player>),
    >,
    mut player_query: Query<
        (
            Entity,
            &mut Player,
            &Transform,
            &Velocity,
            &mut Attach,
            &mut PartFlags,
        ),
        Without<PlanetType>,
    >,
    mut packet_recv: Local<ManualEventReader<WsEvent>>,
    mut packet_event_send: ResMut<Events<WsEvent>>,
    app_keys: Res<AppKeys>,
    server_config: Res<StkConfig>,
) {
    let mut event_queue = Vec::new();
    for ev in packet_recv.read(&packet_event_send) {
        if let WsEvent::Recv { from, message } = ev {
            let packet: Packet = err_or_cont!(message.try_into());

            match packet {
                Packet::ClientLogin {
                    username,
                    save,
                    jwt,
                } => {
                    // auth
                    // plz no remove
                    if let Some(token) = jwt {
                        let key: Hmac<Sha256> = Hmac::new_from_slice(&app_keys.app_key).unwrap();
                        let claims: UserToken = match token.verify_with_key(&key) {
                            Ok(c) => c,
                            Err(e) => {
                                event_queue.push(WsEvent::Send {
                                    to: *from,
                                    message: Packet::Message { message_type: MessageType::Error, actor: "SERVER".to_string(), content: format!("Token is invalid or verification failed: {e}. Please log in again, or contact StarKingdoms staff if the problem persists.") }.into(),
                                });
                                event_queue.push(WsEvent::Close { addr: *from });
                                continue;
                            }
                        };

                        if claims.permission_level
                            < server_config.security.required_permission_level
                        {
                            event_queue.push(WsEvent::Send {
                                to: *from,
                                message: Packet::Message { message_type: MessageType::Error, actor: "SERVER".to_string(), content: format!("Permission level {} is too low, {} is required. If your permissions were just changed, you need to log out and log back in for the change to take effect. If you believe this is a mistake, contact StarKingdoms staff.", claims.permission_level, server_config.security.required_permission_level) }.into(),
                            });
                            event_queue.push(WsEvent::Close { addr: *from });
                            continue;
                        }

                        event_queue.push(WsEvent::Send {
                            to: *from,
                            message: Packet::Message { message_type: MessageType::Server, actor: "StarKingdoms Team".to_string(), content: "Thank you for participating in the StarKingdoms private alpha! Your feedback is essential to improving the game, so please give us any feedback you have in the Discord! <3".to_string() }.into(),
                        });
                    } else if server_config.security.required_permission_level != 0 {
                        event_queue.push(WsEvent::Send {
                            to: *from,
                            message: Packet::Message { message_type: MessageType::Error, actor: "SERVER".to_string(), content: "Authentication is required to join this server at the moment. Log in and try again, or try again later.".to_string() }.into(),
                        });
                        event_queue.push(WsEvent::Close { addr: *from });
                        continue;
                    }
                    let angle: f32 = {
                        let mut rng = rand::thread_rng();
                        rng.gen::<f32>() * std::f32::consts::PI * 2.
                    };
                    let mut transform =
                        Transform::from_xyz(angle.cos() * 30.0, angle.sin() * 30.0, 0.0);
                    transform.rotate_z(angle);
                    let mut player_comp = Player {
                        addr: *from,
                        username: username.to_string(),
                        input: component::Input::default(),
                        selected: None,
                        save_eligibility: false,
                        energy_capacity: part!(PartType::Hearty).energy_capacity,
                        energy: part!(PartType::Hearty).energy_capacity,
                    };
                    let mut entity_id = commands.spawn(PartBundle {
                        part_type: PartType::Hearty,
                        transform: TransformBundle::from(transform),
                        flags: PartFlags { attached: false },
                    });
                    entity_id
                        .insert(Collider::cuboid(0.5, 0.5))
                        .insert(AdditionalMassProperties::MassProperties(MassProperties {
                            local_center_of_mass: vec2(0.0, 0.0),
                            mass: part!(PartType::Hearty).mass,
                            principal_inertia: 7.5,
                        }))
                        .insert(ExternalImpulse {
                            impulse: Vec2::ZERO,
                            torque_impulse: 0.0,
                        })
                        .insert(ExternalForce::default())
                        .insert(ReadMassProperties::default())
                        .insert(Velocity::default())
                        .insert(RigidBody::Dynamic);
                    let id = entity_id.id().index();

                    let entity = entity_id.id();
                    let mut attach = Attach {
                        associated_player: None,
                        parent: None,
                        children: [None, None, None, None],
                    };
                    if let Some(save) = save {
                        // attempt to decode the savefile
                        if let Ok(savefile) = unpack_savefile(&app_keys.app_key, save) {
                            // HEY! GHOSTLY! THIS SAVE FILE IS VALID! PLEASE LOAD IT!
                            // THANKS!

                            let children = load_savefile(
                                &mut commands,
                                transform,
                                entity,
                                entity,
                                savefile.children,
                                &mut attached_query,
                                &mut part_query,
                                &mut player_query,
                                &mut player_comp,
                            );
                            player_comp.energy = player_comp.energy_capacity;
                            attach.children = children;
                        } else {
                            let packet = Packet::Message {
                                message_type: packet::MessageType::Error,
                                actor: "SERVER".to_string(),
                                content: "Savefile signature corrupted or inner data invalid. Save was not loaded. Contact StarKingdoms staff for assistance.".to_string(),
                            };
                            event_queue.push(WsEvent::Send {
                                to: *from,
                                message: packet.into(),
                            });
                        }
                    } else {
                        // nothing to do
                    }
                    let mut entity_id = commands.entity(entity);
                    entity_id.insert(player_comp);
                    entity_id.insert(attach);

                    // tell this player the planets
                    let mut planets = Vec::new();
                    for (entity, planet_type, transform) in planet_query.iter() {
                        let translation = transform.translation;
                        planets.push((
                            entity.index(),
                            Planet {
                                planet_type: *planet_type,
                                transform: proto_transform!(Transform::from_translation(
                                    translation * CLIENT_SCALE
                                )),
                                radius: match *planet_type {
                                    PlanetType::Earth => {
                                        planet!(PlanetType::Earth).size * CLIENT_SCALE
                                    }
                                    PlanetType::Moon => {
                                        planet!(PlanetType::Moon).size * CLIENT_SCALE
                                    }
                                    PlanetType::Mars => {
                                        planet!(PlanetType::Mars).size * CLIENT_SCALE
                                    }
                                },
                            },
                        ));
                    }
                    let packet = Packet::PlanetPositions { planets };
                    event_queue.push(WsEvent::Send {
                        to: *from,
                        message: packet.into(),
                    });

                    // tell the player already existing users
                    let mut players = Vec::new();
                    for (entity, player, _, _, _, _) in &player_query {
                        players.push((entity.index(), player.username.clone()));
                    }
                    let packet = Packet::PlayerList { players };
                    event_queue.push(WsEvent::Send {
                        to: *from,
                        message: packet.into(),
                    });

                    // tell other players that a player has spawned in
                    let packet = Packet::SpawnPlayer {
                        id,
                        username: username.to_string(),
                    };
                    event_queue.push(WsEvent::Broadcast {
                        message: packet.into(),
                    });
                    let packet = Packet::Message {
                        message_type: packet::MessageType::Server,
                        actor: "SERVER".to_string(),
                        content: format!("{} has joined the server!", username),
                    };
                    event_queue.push(WsEvent::Broadcast {
                        message: packet.into(),
                    });

                    // tell the player where parts are
                    let mut parts = Vec::new();
                    for (entity, part_type, transform, _, _, flags) in &part_query {
                        parts.push((
                            entity.index(),
                            Part {
                                part_type: *part_type,
                                transform: proto_transform!(Transform::from_translation(
                                    transform.translation * CLIENT_SCALE
                                )),
                                flags: proto_part_flags!(flags),
                            },
                        ));
                    }
                    for (entity, part_type, transform, _, _, _, _, flags) in &attached_query {
                        parts.push((
                            entity.index(),
                            Part {
                                part_type: *part_type,
                                transform: proto_transform!(Transform::from_translation(
                                    transform.translation * CLIENT_SCALE
                                )),
                                flags: proto_part_flags!(flags),
                            },
                        ));
                    }
                    parts.push((
                        id,
                        Part {
                            part_type: PartType::Hearty,
                            transform: proto_transform!(Transform::from_translation(
                                transform.translation
                            )
                            .with_rotation(transform.rotation)),
                            flags: ProtoPartFlags { attached: false },
                        },
                    ));
                    let packet = Packet::PartPositions { parts };
                    event_queue.push(WsEvent::Send {
                        to: *from,
                        message: packet.into(),
                    });

                    // and send the welcome message :)
                    let packet = Packet::Message {
                        message_type: packet::MessageType::Server,
                        actor: "SERVER".to_string(),
                        content: format!(
                            "starkingdoms-server v{} says hello",
                            env!("CARGO_PKG_VERSION")
                        ),
                    };
                    event_queue.push(WsEvent::Send {
                        to: *from,
                        message: packet.into(),
                    });
                    let packet = Packet::Message {
                        message_type: packet::MessageType::Server,
                        actor: "SERVER".to_string(),
                        content: "Welcome to StarKingdoms.IO! Have fun!".to_string(),
                    };
                    event_queue.push(WsEvent::Send {
                        to: *from,
                        message: packet.into(),
                    });
                    let packet = Packet::Message {
                        message_type: packet::MessageType::Server,
                        actor: "SERVER".to_string(),
                        content: "Found a bug? Have a feature request? Please bring this and all other feedback to the game's official Discord server! Join here: https://discord.gg/3u7Yw8DWtQ".to_string(),
                    };
                    event_queue.push(WsEvent::Send {
                        to: *from,
                        message: packet.into(),
                    });
                }
                Packet::SendMessage { target, content } => {
                    // find our player
                    let mut player = None;
                    for (_, q_player, _, _, _, _) in &player_query {
                        if q_player.addr == *from {
                            player = Some(q_player);
                        }
                    }
                    let player = player.unwrap();
                    if let Some(target_username) = target {
                        let mut target_player = None;
                        for (_, q_player, _, _, _, _) in &player_query {
                            if q_player.username == target_username {
                                target_player = Some(q_player);
                            }
                        }
                        let target_player = target_player.unwrap();
                        let packet = Packet::Message {
                            message_type: packet::MessageType::Direct,
                            actor: player.username.clone(),
                            content,
                        };
                        event_queue.push(WsEvent::Send {
                            to: target_player.addr,
                            message: packet.clone().into(),
                        });
                        event_queue.push(WsEvent::Send {
                            to: *from,
                            message: packet.into(),
                        });
                    } else {
                        // send to general chat
                        let packet = Packet::Message {
                            message_type: packet::MessageType::Chat,
                            actor: player.username.clone(),
                            content,
                        };

                        event_queue.push(WsEvent::Broadcast {
                            message: packet.into(),
                        });
                    }
                }
                Packet::PlayerInput {
                    up,
                    down,
                    left,
                    right,
                } => {
                    for (_, mut q_player, _, _, _, _) in &mut player_query {
                        if q_player.addr == *from {
                            q_player.input.up = up;
                            q_player.input.down = down;
                            q_player.input.left = left;
                            q_player.input.right = right;
                        }
                    }
                }
                Packet::PlayerMouseInput {
                    x,
                    y,
                    released,
                    button: _,
                } => {
                    let x = x / CLIENT_SCALE;
                    let y = y / CLIENT_SCALE;
                    for (entity, mut q_player, _transform, _velocity, _attach, _) in
                        &mut player_query
                    {
                        if q_player.addr == *from {
                            if released {
                                let select = if let Some(s) = q_player.selected {
                                    s
                                } else {
                                    break;
                                };
                                q_player.selected = None;
                                if attached_query.contains(select) {
                                    let module = attached_query.get(select).unwrap();
                                    let attach = module.3.clone();
                                    let lost_energy_capacity = detach_recursive(
                                        &mut commands,
                                        module.0,
                                        &mut attached_query,
                                        &mut player_query,
                                    );
                                    let mut module = attached_query.get_mut(select).unwrap();
                                    module.2.translation = vec3(x, y, 0.);
                                    if *module.1 == PartType::LandingThruster {
                                        let sub_entity = attach.children[2].unwrap();
                                        let mut suspension =
                                            attached_query.get_mut(sub_entity).unwrap();
                                        suspension.2.translation = vec3(x, y, 0.);
                                    }
                                    let mut player = player_query.get_mut(entity).unwrap().1;
                                    player.energy_capacity -= lost_energy_capacity;
                                    player.energy =
                                        std::cmp::min(player.energy, player.energy_capacity);
                                    break;
                                }
                                if attach_on_module_tree(
                                    x,
                                    y,
                                    &mut commands,
                                    entity,
                                    select,
                                    entity,
                                    &mut attached_query,
                                    &mut part_query,
                                    &mut player_query,
                                ) {
                                    let mut part = part_query.get_mut(select).unwrap();
                                    part.5.attached = true; // all of this code is cursed. what the hell is it actually doing
                                    break;
                                }
                                // move module to cursor since no attach
                                let mut part = part_query.get_mut(select).unwrap();
                                part.2.translation = vec3(x, y, 0.);
                                if *part.1 == PartType::LandingThruster {
                                    if let Some(loose_attach) = part.4 {
                                        let sub_entity = loose_attach.children[2].unwrap();
                                        let mut part = part_query.get_mut(sub_entity).unwrap();
                                        part.2.translation = vec3(x, y, 0.);
                                    }
                                }
                                break;
                            }
                            for (m_entity, part_type, transform, m_attach, _velocity, _, _, _) in
                                &attached_query
                            {
                                if *part_type == PartType::LandingThrusterSuspension {
                                    continue;
                                }
                                let pos = transform.translation;
                                let rel_x = pos.x - x;
                                let rel_y = pos.y - y;
                                let angle = -transform.rotation.z;
                                let x = rel_x * angle.cos() - rel_y * angle.sin();
                                let y = rel_x * angle.sin() + rel_y * angle.cos();
                                let mut bound = [-0.5, 0.5, -0.5, 0.5]; // left, right, top, bottom
                                if let PartType::Cargo = part_type {
                                    bound = [-0.375, 0.375, -0.5, 0.4375];
                                }

                                if bound[0] < x
                                    && x < bound[1]
                                    && bound[2] < y
                                    && y < bound[3]
                                    && m_attach.associated_player.unwrap() == entity
                                {
                                    q_player.selected = Some(m_entity);
                                    break;
                                }
                            }
                            for (entity, part_type, transform, _, _, _) in &part_query {
                                if *part_type == PartType::LandingThrusterSuspension {
                                    continue;
                                }
                                let pos = transform.translation;
                                let rel_x = pos.x - x;
                                let rel_y = pos.y - y;
                                let angle = -transform.rotation.z;
                                let x = rel_x * angle.cos() - rel_y * angle.sin();
                                let y = rel_x * angle.sin() + rel_y * angle.cos();
                                let mut bound = [-0.5, 0.5, -0.5, 0.5]; // left, right, top, bottom
                                if let PartType::Cargo = part_type {
                                    bound = [-0.375, 0.375, -0.5, 0.4375];
                                }

                                if bound[0] < x && x < bound[1] && bound[2] < y && y < bound[3] {
                                    q_player.selected = Some(entity);
                                    break;
                                }
                            }
                        }
                    }
                }
                Packet::RequestSave { old_save } => {
                    for (_, q_player, _, _, attach, _) in &mut player_query {
                        if q_player.addr == *from {
                            // HEY! GHOSTLY! PLEASE FILL THIS STRUCT WITH DATA!
                            // THANKS!

                            let unused_modules = if let Some(ref old_save) = old_save {
                                if let Ok(old_savedata) =
                                    unpack_savefile(&app_keys.app_key, old_save.to_string())
                                {
                                    old_savedata.unused_modules
                                } else {
                                    Vec::new()
                                }
                            } else {
                                Vec::new()
                            };
                            let save = SaveData {
                                children: construct_save_data(attach.clone(), &attached_query),
                                unused_modules,
                            };
                            let save_string = pack_savefile(&app_keys.app_key, save);
                            let packet = Packet::SaveData {
                                payload: save_string,
                            };

                            event_queue.push(WsEvent::Send {
                                to: *from,
                                message: packet.into(),
                            });
                        }
                    }
                }
                _ => continue,
            }
        }
    }
    for event in event_queue {
        packet_event_send.send(event);
    }
}

fn on_close(
    player_query: Query<(Entity, &Player, &Attach)>,
    attached_query: Query<&Attach, With<PartType>>,
    part_query: Query<&PartType>,
    mut commands: Commands,
    mut packet_recv: Local<ManualEventReader<WsEvent>>,
    mut packet_send: ResMut<Events<WsEvent>>,
) {
    let mut packets = Vec::new();
    for packet in packet_recv.read(&packet_send) {
        if let WsEvent::Close { addr } = packet {
            for (entity, player, attach) in &player_query {
                if player.addr == *addr {
                    despawn_module_tree(
                        &mut commands,
                        attach,
                        &attached_query,
                        &part_query,
                        &mut packets,
                    );
                    commands.entity(entity).despawn_recursive();

                    let packet = Packet::PlayerLeave { id: entity.index() };

                    for (in_entity, player, _) in &player_query {
                        if entity != in_entity {
                            packets.push(WsEvent::Send {
                                to: player.addr,
                                message: packet.clone().into(),
                            });
                        }
                    }
                }
            }
        }
    }
    for packet in packets {
        packet_send.send(packet);
    }
}


fn send_player_energy(player_query: Query<&Player>, mut packet_send: EventWriter<WsEvent>) {
    for player in &player_query {
        let packet = Packet::EnergyUpdate {
            amount: player.energy,
            max: player.energy_capacity,
        };

        packet_send.send(WsEvent::Send {
            to: player.addr,
            message: packet.into(),
        });
    }
}

fn on_position_change(
    mut commands: Commands,
    part_query: Query<(Entity, &PartType, &Transform, &PartFlags), Changed<Transform>>,
    planet_query: Query<(Entity, &PlanetType, &Transform), Changed<Transform>>,
    mut packet_send: EventWriter<WsEvent>,
) {
    let mut updated_parts = Vec::new();
    for (entity, part_type, transform, flags) in part_query.iter() {
        let id = commands.entity(entity).id().index();
        updated_parts.push((
            id,
            Part {
                part_type: *part_type,
                transform: proto_transform!(Transform::from_translation(
                    transform.translation * CLIENT_SCALE,
                )
                .with_rotation(transform.rotation)),
                flags: proto_part_flags!(flags),
            },
        ));
    }

    if !updated_parts.is_empty() {
        let packet = Packet::PartPositions {
            parts: updated_parts,
        };

        packet_send.send(WsEvent::Broadcast {
            message: packet.into(),
        });
    }

    let mut planets = Vec::new();
    for (entity, planet_type, transform) in planet_query.iter() {
        let id = commands.entity(entity).id().index();
        planets.push((
            id,
            Planet {
                planet_type: *planet_type,
                transform: proto_transform!(Transform::from_translation(
                    transform.translation * CLIENT_SCALE
                )),
                radius: match *planet_type {
                    PlanetType::Earth => planet!(PlanetType::Earth).size * CLIENT_SCALE,
                    PlanetType::Moon => planet!(PlanetType::Moon).size * CLIENT_SCALE,
                    PlanetType::Mars => planet!(PlanetType::Mars).size * CLIENT_SCALE,
                },
            },
        ));
    }

    if !planets.is_empty() {
        let packet = Packet::PlanetPositions { planets };

        packet_send.send(WsEvent::Broadcast {
            message: packet.into(),
        });
    }
}

fn player_input_update(
    mut player_and_body_query: Query<(
        Entity,
        &mut Player,
        &Attach,
        &mut ExternalForce,
        &Transform,
    )>,
    mut attached_query: Query<
        (&Attach, &PartType, &mut ExternalForce, &Transform),
        Without<Player>,
    >,
) {
    for (_, mut player, attach, mut forces, transform) in &mut player_and_body_query {
        //forces.torque = 0.0;
        //forces.force = Vec2::ZERO;
        if !(player.input.up || player.input.down || player.input.right || player.input.left) {
            continue;
        }

        let mut fmul_bottom_left_thruster: f32 = 0.0;
        let mut fmul_bottom_right_thruster: f32 = 0.0;
        let mut fmul_top_left_thruster: f32 = 0.0;
        let mut fmul_top_right_thruster: f32 = 0.0;
        if player.input.up {
            fmul_bottom_left_thruster -= 1.0;
            fmul_bottom_right_thruster -= 1.0;
        }
        if player.input.down {
            fmul_top_left_thruster += 1.0;
            fmul_top_right_thruster += 1.0;
        }
        if player.input.left {
            fmul_top_left_thruster += 1.0;
            fmul_bottom_right_thruster -= 1.0;
        }
        if player.input.right {
            fmul_top_right_thruster += 1.0;
            fmul_bottom_left_thruster -= 1.0;
        }
        fmul_top_left_thruster = fmul_top_left_thruster.clamp(-1.0, 1.0);
        fmul_top_right_thruster = fmul_top_right_thruster.clamp(-1.0, 1.0);
        fmul_bottom_left_thruster = fmul_bottom_left_thruster.clamp(-1.0, 1.0);
        fmul_bottom_right_thruster = fmul_bottom_right_thruster.clamp(-1.0, 1.0);
        if player.input.up {
            fmul_bottom_left_thruster -= 2.0;
            fmul_bottom_right_thruster -= 2.0;
        }
        if player.input.down {
            fmul_top_left_thruster += 2.0;
            fmul_top_right_thruster += 2.0;
        }

        let rot = transform.rotation.to_euler(EulerRot::ZYX).0;

        let thrusters = [
            (fmul_bottom_left_thruster, -PART_HALF_SIZE, -PART_HALF_SIZE),
            (fmul_bottom_right_thruster, PART_HALF_SIZE, -PART_HALF_SIZE),
            (fmul_top_left_thruster, -PART_HALF_SIZE, PART_HALF_SIZE),
            (fmul_top_right_thruster, PART_HALF_SIZE, PART_HALF_SIZE),
        ];

        for (force_multiplier, x_offset, y_offset) in thrusters {
            if force_multiplier != 0.0 && player.energy >= part!(PartType::Hearty).thruster_energy {
                player.energy -= part!(PartType::Hearty).thruster_energy;
                let thruster_pos_uncast = vec2(x_offset, y_offset);
                let thruster_pos_cast =
                    rot2d(thruster_pos_uncast, rot) + transform.translation.xy();
                let thruster_force = force_multiplier * part!(PartType::Hearty).thruster_force;
                let thruster_vec = vec2(-thruster_force * rot.sin(), thruster_force * rot.cos());
                let thruster_force = ExternalForce::at_point(
                    thruster_vec,
                    thruster_pos_cast,
                    transform.translation.xy(),
                );
                forces.force += thruster_force.force;
                forces.torque += thruster_force.torque;
            }
        }
        // change to support other thruster types later
        if player.energy >= part!(PartType::LandingThruster).thruster_energy {
            search_thrusters(
                player.input,
                attach.clone(),
                *transform,
                &mut player.energy,
                &mut attached_query,
            );
        }
    }
}

fn gravity_update(
    mut part_query: Query<
        (

M server/src/module/component.rs => server/src/module/component.rs +0 -1
@@ 75,4 75,3 @@ impl Default for ModuleTimer {
        Self::new()
    }
}


M server/src/module/mod.rs => server/src/module/mod.rs +7 -2
@@ 5,11 5,16 @@ use bevy_rapier2d::prelude::*;
use component::*;
use rand::Rng;

use crate::{capacity, config::StkConfig, part, planet::PlanetType, proto_part_flags, proto_transform, ws::WsEvent, Packet, Part, Player};
use crate::{
    capacity, config::StkConfig, part, planet::PlanetType, player::component::Player, proto_part_flags, proto_transform, ws::WsEvent, Packet, Part
};

pub mod thruster;
pub mod component;
pub mod save;
pub mod thruster;

// half size of hearty
pub static PART_HALF_SIZE: f32 = 25.0;

pub fn module_spawn(
    mut commands: Commands,

M server/src/module/save.rs => server/src/module/save.rs +3 -1
@@ 4,7 4,9 @@ use bevy::{math::vec2, prelude::*};
use bevy_rapier2d::prelude::*;
use starkingdoms_common::SaveModule;

use crate::{capacity, mass, planet::PlanetType, ws::WsEvent, Attach, CanAttach, LooseAttach, Packet, PartBundle, PartFlags, PartType, Player};
use crate::{
    capacity, mass, planet::PlanetType, player::component::Player, ws::WsEvent, Attach, CanAttach, LooseAttach, Packet, PartBundle, PartFlags, PartType
};

pub fn load_savefile(
    commands: &mut Commands,

M server/src/module/thruster.rs => server/src/module/thruster.rs +1 -1
@@ 1,8 1,8 @@
use std::f32::consts::PI;

use crate::{part, player::component::{Input, Player}, Attach, PartType};
use bevy::prelude::*;
use bevy_rapier2d::prelude::*;
use crate::{part, Attach, Input, PartType, Player};

pub fn search_thrusters(
    input: Input,

R server/src/component.rs => server/src/player/component.rs +2 -22
@@ 1,25 1,11 @@
// StarKingdoms.IO, a browser game about drifting through space
//     Copyright (C) 2023 ghostly_zsh, TerraMaster85, core
//
//     This program is free software: you can redistribute it and/or modify
//     it under the terms of the GNU Affero General Public License as published by
//     the Free Software Foundation, either version 3 of the License, or
//     (at your option) any later version.
//
//     This program is distributed in the hope that it will be useful,
//     but WITHOUT ANY WARRANTY; without even the implied warranty of
//     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//     GNU Affero General Public License for more details.
//
//     You should have received a copy of the GNU Affero General Public License
//     along with this program.  If not, see <https://www.gnu.org/licenses/>.
use std::net::SocketAddr;

use bevy::prelude::*;
use bevy::prelude::{Bundle, Component, Entity};
use serde::{Deserialize, Serialize};

use crate::module::component::{Attach, PartBundle};


#[derive(Component, Clone, Copy, Serialize, Deserialize, Debug, Default)]
pub struct Input {
    pub up: bool,


@@ 39,15 25,9 @@ pub struct Player {
    pub energy: u32,
}


#[derive(Bundle)]
pub struct PlayerBundle {
    pub part: PartBundle,
    pub player: Player,
    pub attach: Attach,
}

#[derive(Resource)]
pub struct AppKeys {
    pub app_key: Vec<u8>,
}

M server/src/player/mod.rs => server/src/player/mod.rs +636 -0
@@ 0,0 1,636 @@
use bevy::{ecs::event::ManualEventReader, math::{vec2, vec3}, prelude::*};
use bevy_rapier2d::prelude::*;
use component::Player;
use hmac::{Hmac, Mac};
use rand::Rng;
use sha2::Sha256;
use starkingdoms_common::{pack_savefile, unpack_savefile, SaveData};
use jwt::VerifyWithKey;

use crate::{config::StkConfig, err_or_cont, mathutil::rot2d,
            module::{component::{Attach, CanAttach, LooseAttach, PartBundle, PartFlags, PartType}, save::load_savefile, PART_HALF_SIZE},
            part, planet::PlanetType,
            proto_part_flags, proto_transform, ws::WsEvent, AppKeys, MessageType,
            Packet, Part, Planet, ProtoPartFlags, UserToken, CLIENT_SCALE};

pub mod component;
pub mod packet;

pub fn on_message(
    mut commands: Commands,
    planet_query: Query<(Entity, &PlanetType, &Transform)>,
    mut part_query: Query<
        (
            Entity,
            &PartType,
            &mut Transform,
            &mut Velocity,
            Option<&LooseAttach>,
            &mut PartFlags,
        ),
        (Without<PlanetType>, Without<Player>, Without<Attach>),
    >,
    mut attached_query: Query<
        (
            Entity,
            &PartType,
            &mut Transform,
            &mut Attach,
            &Velocity,
            Option<&CanAttach>,
            Option<&LooseAttach>,
            &mut PartFlags,
        ),
        (Without<PlanetType>, Without<Player>),
    >,
    mut player_query: Query<
        (
            Entity,
            &mut Player,
            &Transform,
            &Velocity,
            &mut Attach,
            &mut PartFlags,
        ),
        Without<PlanetType>,
    >,
    mut packet_recv: Local<ManualEventReader<WsEvent>>,
    mut packet_event_send: ResMut<Events<WsEvent>>,
    app_keys: Res<AppKeys>,
    server_config: Res<StkConfig>,
) {
    let mut event_queue = Vec::new();
    for ev in packet_recv.read(&packet_event_send) {
        if let WsEvent::Recv { from, message } = ev {
            let packet: Packet = err_or_cont!(message.try_into());

            match packet {
                Packet::ClientLogin {
                    username,
                    save,
                    jwt,
                } => {
                    // auth
                    // plz no remove
                    if let Some(token) = jwt {
                        let key: Hmac<Sha256> = Hmac::new_from_slice(&app_keys.app_key).unwrap();
                        let claims: UserToken = match token.verify_with_key(&key) {
                            Ok(c) => c,
                            Err(e) => {
                                event_queue.push(WsEvent::Send {
                                    to: *from,
                                    message: Packet::Message { message_type: MessageType::Error, actor: "SERVER".to_string(), content: format!("Token is invalid or verification failed: {e}. Please log in again, or contact StarKingdoms staff if the problem persists.") }.into(),
                                });
                                event_queue.push(WsEvent::Close { addr: *from });
                                continue;
                            }
                        };

                        if claims.permission_level
                            < server_config.security.required_permission_level
                        {
                            event_queue.push(WsEvent::Send {
                                to: *from,
                                message: Packet::Message { message_type: MessageType::Error, actor: "SERVER".to_string(), content: format!("Permission level {} is too low, {} is required. If your permissions were just changed, you need to log out and log back in for the change to take effect. If you believe this is a mistake, contact StarKingdoms staff.", claims.permission_level, server_config.security.required_permission_level) }.into(),
                            });
                            event_queue.push(WsEvent::Close { addr: *from });
                            continue;
                        }

                        event_queue.push(WsEvent::Send {
                            to: *from,
                            message: Packet::Message { message_type: MessageType::Server, actor: "StarKingdoms Team".to_string(), content: "Thank you for participating in the StarKingdoms private alpha! Your feedback is essential to improving the game, so please give us any feedback you have in the Discord! <3".to_string() }.into(),
                        });
                    } else if server_config.security.required_permission_level != 0 {
                        event_queue.push(WsEvent::Send {
                            to: *from,
                            message: Packet::Message { message_type: MessageType::Error, actor: "SERVER".to_string(), content: "Authentication is required to join this server at the moment. Log in and try again, or try again later.".to_string() }.into(),
                        });
                        event_queue.push(WsEvent::Close { addr: *from });
                        continue;
                    }
                    let angle: f32 = {
                        let mut rng = rand::thread_rng();
                        rng.gen::<f32>() * std::f32::consts::PI * 2.
                    };
                    let mut transform =
                        Transform::from_xyz(angle.cos() * 30.0, angle.sin() * 30.0, 0.0);
                    transform.rotate_z(angle);
                    let mut player_comp = Player {
                        addr: *from,
                        username: username.to_string(),
                        input: component::Input::default(),
                        selected: None,
                        save_eligibility: false,
                        energy_capacity: part!(PartType::Hearty).energy_capacity,
                        energy: part!(PartType::Hearty).energy_capacity,
                    };
                    let mut entity_id = commands.spawn(PartBundle {
                        part_type: PartType::Hearty,
                        transform: TransformBundle::from(transform),
                        flags: PartFlags { attached: false },
                    });
                    entity_id
                        .insert(Collider::cuboid(0.5, 0.5))
                        .insert(AdditionalMassProperties::MassProperties(MassProperties {
                            local_center_of_mass: vec2(0.0, 0.0),
                            mass: part!(PartType::Hearty).mass,
                            principal_inertia: 7.5,
                        }))
                        .insert(ExternalImpulse {
                            impulse: Vec2::ZERO,
                            torque_impulse: 0.0,
                        })
                        .insert(ExternalForce::default())
                        .insert(ReadMassProperties::default())
                        .insert(Velocity::default())
                        .insert(RigidBody::Dynamic);
                    let id = entity_id.id().index();

                    let entity = entity_id.id();
                    let mut attach = Attach {
                        associated_player: None,
                        parent: None,
                        children: [None, None, None, None],
                    };
                    if let Some(save) = save {
                        // attempt to decode the savefile
                        if let Ok(savefile) = unpack_savefile(&app_keys.app_key, save) {
                            // HEY! GHOSTLY! THIS SAVE FILE IS VALID! PLEASE LOAD IT!
                            // THANKS!

                            let children = load_savefile(
                                &mut commands,
                                transform,
                                entity,
                                entity,
                                savefile.children,
                                &mut attached_query,
                                &mut part_query,
                                &mut player_query,
                                &mut player_comp,
                            );
                            player_comp.energy = player_comp.energy_capacity;
                            attach.children = children;
                        } else {
                            let packet = Packet::Message {
                                message_type: crate::packet::MessageType::Error,
                                actor: "SERVER".to_string(),
                                content: "Savefile signature corrupted or inner data invalid. Save was not loaded. Contact StarKingdoms staff for assistance.".to_string(),
                            };
                            event_queue.push(WsEvent::Send {
                                to: *from,
                                message: packet.into(),
                            });
                        }
                    } else {
                        // nothing to do
                    }
                    let mut entity_id = commands.entity(entity);
                    entity_id.insert(player_comp);
                    entity_id.insert(attach);

                    // tell this player the planets
                    let mut planets = Vec::new();
                    for (entity, planet_type, transform) in planet_query.iter() {
                        let translation = transform.translation;
                        planets.push((
                            entity.index(),
                            Planet {
                                planet_type: *planet_type,
                                transform: proto_transform!(Transform::from_translation(
                                    translation * CLIENT_SCALE
                                )),
                                radius: match *planet_type {
                                    PlanetType::Earth => {
                                        planet!(PlanetType::Earth).size * CLIENT_SCALE
                                    }
                                    PlanetType::Moon => {
                                        planet!(PlanetType::Moon).size * CLIENT_SCALE
                                    }
                                    PlanetType::Mars => {
                                        planet!(PlanetType::Mars).size * CLIENT_SCALE
                                    }
                                },
                            },
                        ));
                    }
                    let packet = Packet::PlanetPositions { planets };
                    event_queue.push(WsEvent::Send {
                        to: *from,
                        message: packet.into(),
                    });

                    // tell the player already existing users
                    let mut players = Vec::new();
                    for (entity, player, _, _, _, _) in &player_query {
                        players.push((entity.index(), player.username.clone()));
                    }
                    let packet = Packet::PlayerList { players };
                    event_queue.push(WsEvent::Send {
                        to: *from,
                        message: packet.into(),
                    });

                    // tell other players that a player has spawned in
                    let packet = Packet::SpawnPlayer {
                        id,
                        username: username.to_string(),
                    };
                    event_queue.push(WsEvent::Broadcast {
                        message: packet.into(),
                    });
                    let packet = Packet::Message {
                        message_type: crate::packet::MessageType::Server,
                        actor: "SERVER".to_string(),
                        content: format!("{} has joined the server!", username),
                    };
                    event_queue.push(WsEvent::Broadcast {
                        message: packet.into(),
                    });

                    // tell the player where parts are
                    let mut parts = Vec::new();
                    for (entity, part_type, transform, _, _, flags) in &part_query {
                        parts.push((
                            entity.index(),
                            Part {
                                part_type: *part_type,
                                transform: proto_transform!(Transform::from_translation(
                                    transform.translation * CLIENT_SCALE
                                )),
                                flags: proto_part_flags!(flags),
                            },
                        ));
                    }
                    for (entity, part_type, transform, _, _, _, _, flags) in &attached_query {
                        parts.push((
                            entity.index(),
                            Part {
                                part_type: *part_type,
                                transform: proto_transform!(Transform::from_translation(
                                    transform.translation * CLIENT_SCALE
                                )),
                                flags: proto_part_flags!(flags),
                            },
                        ));
                    }
                    parts.push((
                        id,
                        Part {
                            part_type: PartType::Hearty,
                            transform: proto_transform!(Transform::from_translation(
                                transform.translation
                            )
                            .with_rotation(transform.rotation)),
                            flags: ProtoPartFlags { attached: false },
                        },
                    ));
                    let packet = Packet::PartPositions { parts };
                    event_queue.push(WsEvent::Send {
                        to: *from,
                        message: packet.into(),
                    });

                    // and send the welcome message :)
                    let packet = Packet::Message {
                        message_type: crate::packet::MessageType::Server,
                        actor: "SERVER".to_string(),
                        content: format!(
                            "starkingdoms-server v{} says hello",
                            env!("CARGO_PKG_VERSION")
                        ),
                    };
                    event_queue.push(WsEvent::Send {
                        to: *from,
                        message: packet.into(),
                    });
                    let packet = Packet::Message {
                        message_type: crate::packet::MessageType::Server,
                        actor: "SERVER".to_string(),
                        content: "Welcome to StarKingdoms.IO! Have fun!".to_string(),
                    };
                    event_queue.push(WsEvent::Send {
                        to: *from,
                        message: packet.into(),
                    });
                    let packet = Packet::Message {
                        message_type: crate::packet::MessageType::Server,
                        actor: "SERVER".to_string(),
                        content: "Found a bug? Have a feature request? Please bring this and all other feedback to the game's official Discord server! Join here: https://discord.gg/3u7Yw8DWtQ".to_string(),
                    };
                    event_queue.push(WsEvent::Send {
                        to: *from,
                        message: packet.into(),
                    });
                }
                Packet::SendMessage { target, content } => {
                    // find our player
                    let mut player = None;
                    for (_, q_player, _, _, _, _) in &player_query {
                        if q_player.addr == *from {
                            player = Some(q_player);
                        }
                    }
                    let player = player.unwrap();
                    if let Some(target_username) = target {
                        let mut target_player = None;
                        for (_, q_player, _, _, _, _) in &player_query {
                            if q_player.username == target_username {
                                target_player = Some(q_player);
                            }
                        }
                        let target_player = target_player.unwrap();
                        let packet = Packet::Message {
                            message_type: crate::packet::MessageType::Direct,
                            actor: player.username.clone(),
                            content,
                        };
                        event_queue.push(WsEvent::Send {
                            to: target_player.addr,
                            message: packet.clone().into(),
                        });
                        event_queue.push(WsEvent::Send {
                            to: *from,
                            message: packet.into(),
                        });
                    } else {
                        // send to general chat
                        let packet = Packet::Message {
                            message_type: crate::packet::MessageType::Chat,
                            actor: player.username.clone(),
                            content,
                        };

                        event_queue.push(WsEvent::Broadcast {
                            message: packet.into(),
                        });
                    }
                }
                Packet::PlayerInput {
                    up,
                    down,
                    left,
                    right,
                } => {
                    for (_, mut q_player, _, _, _, _) in &mut player_query {
                        if q_player.addr == *from {
                            q_player.input.up = up;
                            q_player.input.down = down;
                            q_player.input.left = left;
                            q_player.input.right = right;
                        }
                    }
                }
                Packet::PlayerMouseInput {
                    x,
                    y,
                    released,
                    button: _,
                } => {
                    let x = x / CLIENT_SCALE;
                    let y = y / CLIENT_SCALE;
                    for (entity, mut q_player, _transform, _velocity, _attach, _) in
                        &mut player_query
                    {
                        if q_player.addr == *from {
                            if released {
                                let select = if let Some(s) = q_player.selected {
                                    s
                                } else {
                                    break;
                                };
                                q_player.selected = None;
                                if attached_query.contains(select) {
                                    let module = attached_query.get(select).unwrap();
                                    let attach = module.3.clone();
                                    let lost_energy_capacity = crate::module::detach_recursive(
                                        &mut commands,
                                        module.0,
                                        &mut attached_query,
                                        &mut player_query,
                                    );
                                    let mut module = attached_query.get_mut(select).unwrap();
                                    module.2.translation = vec3(x, y, 0.);
                                    if *module.1 == PartType::LandingThruster {
                                        let sub_entity = attach.children[2].unwrap();
                                        let mut suspension =
                                            attached_query.get_mut(sub_entity).unwrap();
                                        suspension.2.translation = vec3(x, y, 0.);
                                    }
                                    let mut player = player_query.get_mut(entity).unwrap().1;
                                    player.energy_capacity -= lost_energy_capacity;
                                    player.energy =
                                        std::cmp::min(player.energy, player.energy_capacity);
                                    break;
                                }
                                if crate::module::attach_on_module_tree(
                                    x,
                                    y,
                                    &mut commands,
                                    entity,
                                    select,
                                    entity,
                                    &mut attached_query,
                                    &mut part_query,
                                    &mut player_query,
                                ) {
                                    let mut part = part_query.get_mut(select).unwrap();
                                    part.5.attached = true; // all of this code is cursed. what the hell is it actually doing
                                    break;
                                }
                                // move module to cursor since no attach
                                let mut part = part_query.get_mut(select).unwrap();
                                part.2.translation = vec3(x, y, 0.);
                                if *part.1 == PartType::LandingThruster {
                                    if let Some(loose_attach) = part.4 {
                                        let sub_entity = loose_attach.children[2].unwrap();
                                        let mut part = part_query.get_mut(sub_entity).unwrap();
                                        part.2.translation = vec3(x, y, 0.);
                                    }
                                }
                                break;
                            }
                            for (m_entity, part_type, transform, m_attach, _velocity, _, _, _) in
                                &attached_query
                            {
                                if *part_type == PartType::LandingThrusterSuspension {
                                    continue;
                                }
                                let pos = transform.translation;
                                let rel_x = pos.x - x;
                                let rel_y = pos.y - y;
                                let angle = -transform.rotation.z;
                                let x = rel_x * angle.cos() - rel_y * angle.sin();
                                let y = rel_x * angle.sin() + rel_y * angle.cos();
                                let mut bound = [-0.5, 0.5, -0.5, 0.5]; // left, right, top, bottom
                                if let PartType::Cargo = part_type {
                                    bound = [-0.375, 0.375, -0.5, 0.4375];
                                }

                                if bound[0] < x
                                    && x < bound[1]
                                    && bound[2] < y
                                    && y < bound[3]
                                    && m_attach.associated_player.unwrap() == entity
                                {
                                    q_player.selected = Some(m_entity);
                                    break;
                                }
                            }
                            for (entity, part_type, transform, _, _, _) in &part_query {
                                if *part_type == PartType::LandingThrusterSuspension {
                                    continue;
                                }
                                let pos = transform.translation;
                                let rel_x = pos.x - x;
                                let rel_y = pos.y - y;
                                let angle = -transform.rotation.z;
                                let x = rel_x * angle.cos() - rel_y * angle.sin();
                                let y = rel_x * angle.sin() + rel_y * angle.cos();
                                let mut bound = [-0.5, 0.5, -0.5, 0.5]; // left, right, top, bottom
                                if let PartType::Cargo = part_type {
                                    bound = [-0.375, 0.375, -0.5, 0.4375];
                                }

                                if bound[0] < x && x < bound[1] && bound[2] < y && y < bound[3] {
                                    q_player.selected = Some(entity);
                                    break;
                                }
                            }
                        }
                    }
                }
                Packet::RequestSave { old_save } => {
                    for (_, q_player, _, _, attach, _) in &mut player_query {
                        if q_player.addr == *from {
                            // HEY! GHOSTLY! PLEASE FILL THIS STRUCT WITH DATA!
                            // THANKS!

                            let unused_modules = if let Some(ref old_save) = old_save {
                                if let Ok(old_savedata) =
                                    unpack_savefile(&app_keys.app_key, old_save.to_string())
                                {
                                    old_savedata.unused_modules
                                } else {
                                    Vec::new()
                                }
                            } else {
                                Vec::new()
                            };
                            let save = SaveData {
                                children: crate::module::save::construct_save_data(attach.clone(), &attached_query),
                                unused_modules,
                            };
                            let save_string = pack_savefile(&app_keys.app_key, save);
                            let packet = Packet::SaveData {
                                payload: save_string,
                            };

                            event_queue.push(WsEvent::Send {
                                to: *from,
                                message: packet.into(),
                            });
                        }
                    }
                }
                _ => continue,
            }
        }
    }
    for event in event_queue {
        packet_event_send.send(event);
    }
}

pub fn player_input_update(
    mut player_and_body_query: Query<(
        Entity,
        &mut Player,
        &Attach,
        &mut ExternalForce,
        &Transform,
    )>,
    mut attached_query: Query<
        (&Attach, &PartType, &mut ExternalForce, &Transform),
        Without<Player>,
    >,
) {
    for (_, mut player, attach, mut forces, transform) in &mut player_and_body_query {
        //forces.torque = 0.0;
        //forces.force = Vec2::ZERO;
        if !(player.input.up || player.input.down || player.input.right || player.input.left) {
            continue;
        }

        let mut fmul_bottom_left_thruster: f32 = 0.0;
        let mut fmul_bottom_right_thruster: f32 = 0.0;
        let mut fmul_top_left_thruster: f32 = 0.0;
        let mut fmul_top_right_thruster: f32 = 0.0;
        if player.input.up {
            fmul_bottom_left_thruster -= 1.0;
            fmul_bottom_right_thruster -= 1.0;
        }
        if player.input.down {
            fmul_top_left_thruster += 1.0;
            fmul_top_right_thruster += 1.0;
        }
        if player.input.left {
            fmul_top_left_thruster += 1.0;
            fmul_bottom_right_thruster -= 1.0;
        }
        if player.input.right {
            fmul_top_right_thruster += 1.0;
            fmul_bottom_left_thruster -= 1.0;
        }
        fmul_top_left_thruster = fmul_top_left_thruster.clamp(-1.0, 1.0);
        fmul_top_right_thruster = fmul_top_right_thruster.clamp(-1.0, 1.0);
        fmul_bottom_left_thruster = fmul_bottom_left_thruster.clamp(-1.0, 1.0);
        fmul_bottom_right_thruster = fmul_bottom_right_thruster.clamp(-1.0, 1.0);
        if player.input.up {
            fmul_bottom_left_thruster -= 2.0;
            fmul_bottom_right_thruster -= 2.0;
        }
        if player.input.down {
            fmul_top_left_thruster += 2.0;
            fmul_top_right_thruster += 2.0;
        }

        let rot = transform.rotation.to_euler(EulerRot::ZYX).0;

        let thrusters = [
            (fmul_bottom_left_thruster, -PART_HALF_SIZE, -PART_HALF_SIZE),
            (fmul_bottom_right_thruster, PART_HALF_SIZE, -PART_HALF_SIZE),
            (fmul_top_left_thruster, -PART_HALF_SIZE, PART_HALF_SIZE),
            (fmul_top_right_thruster, PART_HALF_SIZE, PART_HALF_SIZE),
        ];

        for (force_multiplier, x_offset, y_offset) in thrusters {
            if force_multiplier != 0.0 && player.energy >= part!(PartType::Hearty).thruster_energy {
                player.energy -= part!(PartType::Hearty).thruster_energy;
                let thruster_pos_uncast = vec2(x_offset, y_offset);
                let thruster_pos_cast =
                    rot2d(thruster_pos_uncast, rot) + transform.translation.xy();
                let thruster_force = force_multiplier * part!(PartType::Hearty).thruster_force;
                let thruster_vec = vec2(-thruster_force * rot.sin(), thruster_force * rot.cos());
                let thruster_force = ExternalForce::at_point(
                    thruster_vec,
                    thruster_pos_cast,
                    transform.translation.xy(),
                );
                forces.force += thruster_force.force;
                forces.torque += thruster_force.torque;
            }
        }
        // change to support other thruster types later
        if player.energy >= part!(PartType::LandingThruster).thruster_energy {
            crate::module::thruster::search_thrusters(
                player.input,
                attach.clone(),
                *transform,
                &mut player.energy,
                &mut attached_query,
            );
        }
    }
}

A server/src/player/packet.rs => server/src/player/packet.rs +120 -0
@@ 0,0 1,120 @@
use bevy::{ecs::event::ManualEventReader, prelude::*};

use crate::{module::component::{Attach, PartFlags, PartType}, planet::PlanetType, proto_part_flags, proto_transform, ws::WsEvent, Packet, Part, Planet, CLIENT_SCALE};

use super::component::Player;

pub fn send_player_energy(player_query: Query<&Player>, mut packet_send: EventWriter<WsEvent>) {
    for player in &player_query {
        let packet = Packet::EnergyUpdate {
            amount: player.energy,
            max: player.energy_capacity,
        };

        packet_send.send(WsEvent::Send {
            to: player.addr,
            message: packet.into(),
        });
    }
}

pub fn on_position_change(
    mut commands: Commands,
    part_query: Query<(Entity, &PartType, &Transform, &PartFlags), Changed<Transform>>,
    planet_query: Query<(Entity, &PlanetType, &Transform), Changed<Transform>>,
    mut packet_send: EventWriter<WsEvent>,
) {
    let mut updated_parts = Vec::new();
    for (entity, part_type, transform, flags) in part_query.iter() {
        let id = commands.entity(entity).id().index();
        updated_parts.push((
            id,
            Part {
                part_type: *part_type,
                transform: proto_transform!(Transform::from_translation(
                    transform.translation * CLIENT_SCALE,
                )
                .with_rotation(transform.rotation)),
                flags: proto_part_flags!(flags),
            },
        ));
    }

    if !updated_parts.is_empty() {
        let packet = Packet::PartPositions {
            parts: updated_parts,
        };

        packet_send.send(WsEvent::Broadcast {
            message: packet.into(),
        });
    }

    let mut planets = Vec::new();
    for (entity, planet_type, transform) in planet_query.iter() {
        let id = commands.entity(entity).id().index();
        planets.push((
            id,
            Planet {
                planet_type: *planet_type,
                transform: proto_transform!(Transform::from_translation(
                    transform.translation * CLIENT_SCALE
                )),
                radius: match *planet_type {
                    PlanetType::Earth => planet!(PlanetType::Earth).size * CLIENT_SCALE,
                    PlanetType::Moon => planet!(PlanetType::Moon).size * CLIENT_SCALE,
                    PlanetType::Mars => planet!(PlanetType::Mars).size * CLIENT_SCALE,
                },
            },
        ));
    }

    if !planets.is_empty() {
        let packet = Packet::PlanetPositions { planets };

        packet_send.send(WsEvent::Broadcast {
            message: packet.into(),
        });
    }
}

pub fn on_close(
    player_query: Query<(Entity, &Player, &Attach)>,
    attached_query: Query<&Attach, With<PartType>>,
    part_query: Query<&PartType>,
    mut commands: Commands,
    mut packet_recv: Local<ManualEventReader<WsEvent>>,
    mut packet_send: ResMut<Events<WsEvent>>,
) {
    let mut packets = Vec::new();
    for packet in packet_recv.read(&packet_send) {
        if let WsEvent::Close { addr } = packet {
            for (entity, player, attach) in &player_query {
                if player.addr == *addr {
                    crate::module::despawn_module_tree(
                        &mut commands,
                        attach,
                        &attached_query,
                        &part_query,
                        &mut packets,
                    );
                    commands.entity(entity).despawn_recursive();

                    let packet = Packet::PlayerLeave { id: entity.index() };

                    for (in_entity, player, _) in &player_query {
                        if entity != in_entity {
                            packets.push(WsEvent::Send {
                                to: player.addr,
                                message: packet.clone().into(),
                            });
                        }
                    }
                }
            }
        }
    }
    for packet in packets {
        packet_send.send(packet);
    }
}