// 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 . #![allow(clippy::type_complexity)] // bevy :( #![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 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 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; pub mod macros; pub mod mathutil; pub mod packet; pub mod ws; pub mod planet; pub mod module; pub mod player; struct StkPluginGroup; // 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 = OnceLock::new(); #[inline] pub fn server_config() -> StkConfig { _SERVER_CONFIG.get().unwrap().clone() } static _PARTS_CONFIG: OnceLock = OnceLock::new(); #[inline] pub fn parts_config() -> PartsConfig { _PARTS_CONFIG.get().unwrap().clone() } static _PLANETS_CONFIG: OnceLock = OnceLock::new(); #[inline] pub fn planets_config() -> PlanetsConfig { _PLANETS_CONFIG.get().unwrap().clone() } // group main stk plugins together #[cfg(debug_assertions)] impl PluginGroup for StkPluginGroup { fn build(self) -> PluginGroupBuilder { PluginGroupBuilder::start::() .add(TaskPoolPlugin::default()) .add(TypeRegistrationPlugin) .add(FrameCountPlugin) .add(TimePlugin) .add(ScheduleRunnerPlugin::run_loop(Duration::from_millis( server_config().server.tick_time_ms, ))) .add(bevy::log::LogPlugin { level: bevy::log::Level::DEBUG, filter: "wgpu=error,bevy_render=info,bevy_ecs=trace".to_string(), update_subscriber: None, }) } } #[cfg(not(debug_assertions))] impl PluginGroup for StkPluginGroup { fn build(self) -> PluginGroupBuilder { PluginGroupBuilder::start::() .add(TaskPoolPlugin::default()) .add(TypeRegistrationPlugin) .add(FrameCountPlugin) .add(TimePlugin) .add(ScheduleRunnerPlugin::run_loop(Duration::from_millis(1))) } } // auth token #[derive(Serialize, Deserialize, Debug, Clone)] pub struct UserToken { pub id: i64, pub username: String, pub permission_level: i32, pub expires: std::time::SystemTime, } fn main() { // read the server main config let server_config = fs::read_to_string("/etc/starkingdoms/config.toml").unwrap(); let parts_config = fs::read_to_string("/etc/starkingdoms/parts.toml").unwrap(); let planets_config = fs::read_to_string("/etc/starkingdoms/planets.toml").unwrap(); // put config in variables let server_config: StkConfig = toml::from_str(&server_config).unwrap(); _SERVER_CONFIG.set(server_config.clone()).unwrap(); let parts_config: PartsConfig = toml::from_str(&parts_config).unwrap(); _PARTS_CONFIG.set(parts_config.clone()).unwrap(); let planets_config: PlanetsConfig = toml::from_str(&planets_config).unwrap(); _PLANETS_CONFIG.set(planets_config.clone()).unwrap(); // make the game, start it in .run() App::new() .insert_resource(AppKeys { app_key: server_config.security.app_key.as_bytes().to_vec(), }) .insert_resource(StkTungsteniteServerConfig { addr: server_config.server.bind.ip, port: server_config.server.bind.port, }) .insert_resource(server_config.clone()) .insert_resource(parts_config) .insert_resource(planets_config) .add_plugins(StkPluginGroup) .insert_resource(RapierConfiguration { gravity: Vect { x: 0.0, y: 0.0 }, ..Default::default() }) .init_resource::() .add_plugins(RapierPhysicsPlugin::::pixels_per_meter( server_config.world.pixels_per_meter, )) .add_plugins(StkTungsteniteServerPlugin) .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( FixedUpdate, (module::break_modules, gravity_update, player_input_update).chain(), ) .add_systems(FixedUpdate, module::save::save_eligibility) .add_systems(FixedUpdate, module::convert_modules) .insert_resource(Time::::from_seconds( server_config.server.world_fixed_timestep, )) .run(); // game is done running info!("Goodbye!"); } fn setup_integration_parameters(mut context: ResMut, server_config: Res) { context.integration_parameters = server_config.physics.parameters; match server_config.physics.solver { PhysicsSolver::SmallstepPGS => { context .integration_parameters .switch_to_small_steps_pgs_solver(); } PhysicsSolver::OldPGS => { context .integration_parameters .switch_to_standard_pgs_solver(); } } } 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, Without, Without), >, mut attached_query: Query< ( Entity, &PartType, &mut Transform, &mut Attach, &Velocity, Option<&CanAttach>, Option<&LooseAttach>, &mut PartFlags, ), (Without, Without), >, mut player_query: Query< ( Entity, &mut Player, &Transform, &Velocity, &mut Attach, &mut PartFlags, ), Without, >, mut packet_recv: Local>, mut packet_event_send: ResMut>, app_keys: Res, server_config: Res, ) { 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 = 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::() * 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>, part_query: Query<&PartType>, mut commands: Commands, mut packet_recv: Local>, mut packet_send: ResMut>, ) { 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) { 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>, planet_query: Query<(Entity, &PlanetType, &Transform), Changed>, mut packet_send: EventWriter, ) { 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, >, ) { 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< ( &Transform, &ReadMassProperties, &mut ExternalForce, &mut ExternalImpulse, ), With, >, planet_query: Query<(&Transform, &ReadMassProperties), With>, server_config: Res, ) { for (part_transform, part_mp, mut forces, mut impulses) in &mut part_query { impulses.impulse = Vec2::ZERO; forces.force = Vec2::ZERO; forces.torque = 0.; let part_mp = part_mp.get(); let part_mass = part_mp.mass; let part_translate = part_transform.translation; for (planet_transform, planet_mp) in &planet_query { let planet_mp = planet_mp.get(); let planet_mass = planet_mp.mass; let planet_translate = planet_transform.translation; let distance = planet_translate.distance(part_translate); let force = server_config.world.gravity * ((part_mass * planet_mass) / (distance * distance)); let direction = (planet_translate - part_translate).normalize() * force; /*let gravity_force = ExternalForce::at_point( direction.xy(), part_transform.translation.xy(), part_transform.translation.xy(), ); forces.force += gravity_force.force; forces.torque += gravity_force.torque;*/ impulses.impulse += direction.xy(); } } }