// 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 . use std::net::Ipv4Addr; use crate::mathutil::rot2d; use bevy::math::{vec2, vec3}; use bevy::utils::tracing; use bevy::{ecs::event::ManualEventReader, prelude::*}; use bevy_rapier2d::prelude::*; use bevy_twite::{twite::frame::MessageType, ServerEvent, TwiteServerConfig, TwiteServerPlugin}; use component::*; use packet::*; use rand::Rng; pub mod component; pub mod macros; pub mod mathutil; pub mod packet; const SCALE: f32 = 10.0; const EARTH_SIZE: f32 = 1000.0; const GRAVITY: f32 = 0.02; const PART_HALF_SIZE: f32 = 25.0; const THRUSTER_FORCE: f32 = 0.08; fn main() { let subscriber = tracing_subscriber::FmtSubscriber::new(); tracing::subscriber::set_global_default(subscriber).unwrap(); info!( "StarKingdoms server v{} starting up", env!("CARGO_PKG_VERSION") ); App::new() .insert_resource(TwiteServerConfig { addr: Ipv4Addr::new(0, 0, 0, 0), port: 3000, }) .add_plugins(MinimalPlugins) .insert_resource(RapierConfiguration { gravity: Vect { x: 0.0, y: 0.0 }, ..Default::default() }) .add_plugins(RapierPhysicsPlugin::::pixels_per_meter(SCALE)) .add_plugins(TwiteServerPlugin) .add_systems(Startup, setup_integration_parameters) .add_systems(Startup, spawn_planets) .add_systems(Startup, module_spawn) .add_systems(Update, on_message) .add_systems(Update, on_close) .add_systems(FixedUpdate, on_position_change) .add_systems(FixedUpdate, gravity_update) .add_systems(FixedUpdate, player_input_update) //.insert_resource(Time::::from_seconds(1.0/20.0)) .run(); info!("Goodbye!"); } fn setup_integration_parameters(mut context: ResMut) { context.integration_parameters.dt = 1.0 / 60.0; context.integration_parameters.joint_erp = 0.2; context.integration_parameters.erp = 0.5; context.integration_parameters.max_stabilization_iterations = 16; } fn spawn_planets(mut commands: Commands) { info!("Spawning planets"); let earth_pos = Transform::from_xyz(0.0, 0.0, 0.0); commands .spawn(PlanetBundle { planet_type: PlanetType::Earth, transform: TransformBundle::from(earth_pos), }) .insert(Collider::ball(EARTH_SIZE / SCALE)) .insert(AdditionalMassProperties::Mass(10000.0)) .insert(ReadMassProperties::default()) .insert(RigidBody::Fixed); } fn module_spawn(mut commands: Commands) { commands .spawn(PartBundle { part_type: PartType::Cargo, transform: TransformBundle::from(Transform::from_xyz(1100. / SCALE, 0., 0.)), }) //.insert(Collider::cuboid(18.75 / SCALE, 23.4375 / SCALE)) .insert(RigidBody::Dynamic) .with_children(|children| { children .spawn(Collider::cuboid(18.75 / SCALE, 23.4375 / SCALE)) .insert(TransformBundle::from(Transform::from_xyz( 0., 1.5625 / SCALE, 0., ))); }) .insert(ExternalForce::default()) .insert(ExternalImpulse::default()) .insert(Velocity::default()) .insert(ReadMassProperties::default()); } fn on_message( mut commands: Commands, planet_query: Query<(Entity, &PlanetType, &Transform)>, mut part_query: Query<(Entity, &PartType, &mut Transform, &mut Velocity), (Without, Without, Without)>, mut attached_query: Query<(Entity, &PartType, &mut Transform, &Attach), (Without, Without)>, mut player_query: Query<(Entity, &mut Player, &Transform, &Velocity, &mut Attach), Without>, mut packet_recv: Local>, mut packet_event_send: ResMut>, ) { let mut event_queue = Vec::new(); for ev in packet_recv.read(&packet_event_send) { if let ServerEvent::Recv(addr, MessageType::Text, data) = ev { let data = String::from_utf8_lossy(&data); let packet: Packet = err_or_cont!(serde_json::from_str(&data)); match packet { Packet::ClientLogin { username, .. } => { let angle: f32 = { let mut rng = rand::thread_rng(); rng.gen::() * std::f32::consts::PI * 2. }; let mut transform = Transform::from_xyz( angle.cos() * 1100.0 / SCALE, angle.sin() * 1100.0 / SCALE, 0.0, ); transform.rotate_z(angle); let entity_id = commands .spawn(PlayerBundle { part: PartBundle { part_type: PartType::Hearty, transform: TransformBundle::from(transform), }, player: Player { addr: *addr, username: username.to_string(), input: component::Input::default(), selected: None, }, attach: Attach { associated_player: None, children: [None, None, None, None], }, }) .insert(Collider::cuboid( PART_HALF_SIZE / SCALE, PART_HALF_SIZE / SCALE, )) .insert(AdditionalMassProperties::MassProperties(MassProperties { local_center_of_mass: vec2(0.0, 0.0), mass: 0.0001, principal_inertia: 0.005, })) .insert(ExternalImpulse { impulse: Vec2::ZERO, torque_impulse: 0.0, }) .insert(ExternalForce::default()) .insert(ReadMassProperties::default()) .insert(Velocity::default()) .insert(RigidBody::Dynamic).id(); let id = entity_id.index(); // 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 * SCALE )), radius: match *planet_type { PlanetType::Earth => EARTH_SIZE, }, }, )); } let packet = Packet::PlanetPositions { planets }; let buf = serde_json::to_vec(&packet).unwrap(); event_queue.push(ServerEvent::Send(*addr, MessageType::Text, buf)); // 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 }; let buf = serde_json::to_vec(&packet).unwrap(); event_queue.push(ServerEvent::Send(*addr, MessageType::Text, buf)); // tell other players that a player has spawned in let packet = Packet::SpawnPlayer { id, username: username.to_string(), }; let buf = serde_json::to_vec(&packet).unwrap(); event_queue.push(ServerEvent::Broadcast(MessageType::Text, buf)); let packet = Packet::Message { message_type: packet::MessageType::Server, actor: "SERVER".to_string(), content: format!("{} has joined the server!", username), }; let buf = serde_json::to_vec(&packet).unwrap(); event_queue.push(ServerEvent::Broadcast(MessageType::Text, buf)); // tell the player where parts are let mut parts = Vec::new(); for (entity, part_type, transform, _) in &part_query { parts.push(( entity.index(), Part { part_type: *part_type, transform: proto_transform!(Transform::from_translation( transform.translation * SCALE )), }, )); } for (entity, part_type, transform, _) in &attached_query { parts.push(( entity.index(), Part { part_type: *part_type, transform: proto_transform!(Transform::from_translation( transform.translation * SCALE )), }, )); } parts.push(( id, Part { part_type: PartType::Hearty, transform: proto_transform!(Transform::from_translation( transform.translation * SCALE ) .with_rotation(transform.rotation)), }, )); let packet = Packet::PartPositions { parts }; let buf = serde_json::to_vec(&packet).unwrap(); event_queue.push(ServerEvent::Send(*addr, MessageType::Text, buf)); // and send the welcome message :) let packet = Packet::Message { message_type: packet::MessageType::Server, actor: "SERVER".to_string(), content: "Welcome to StarKingdoms.IO! Have fun!".to_string(), }; let buf = serde_json::to_vec(&packet).unwrap(); event_queue.push(ServerEvent::Send(*addr, MessageType::Text, buf)); } Packet::SendMessage { target, content } => { // find our player let mut player = None; for (_, q_player, _, _, _) in &player_query { if q_player.addr == *addr { 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, }; let buf = serde_json::to_vec(&packet).unwrap(); event_queue.push(ServerEvent::Send( target_player.addr, MessageType::Text, buf.clone(), )); event_queue.push(ServerEvent::Send(*addr, MessageType::Text, buf)); } else { // send to general chat let packet = Packet::Message { message_type: packet::MessageType::Chat, actor: player.username.clone(), content, }; let buf = serde_json::to_vec(&packet).unwrap(); event_queue.push(ServerEvent::Broadcast(MessageType::Text, buf)); } } Packet::PlayerInput { up, down, left, right, } => { for (_, mut q_player, _, _, _) in &mut player_query { if q_player.addr == *addr { 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: _ } => { for (entity, mut q_player, transform, velocity, mut attach) in &mut player_query { if q_player.addr == *addr { if released { let select = if let Some(s) = q_player.selected { s } else { break; }; q_player.selected = None; let mut module = part_query.get_mut(select).unwrap(); // attach module let p_pos = transform.translation; let (rel_x, rel_y) = ( p_pos.x - x / SCALE, p_pos.y - y / SCALE, ); let angle = transform.rotation.to_euler(EulerRot::ZYX).0; let (rel_x, rel_y) = ( rel_x * (-angle).cos() - rel_y * (-angle).sin(), rel_x * (-angle).sin() + rel_y * (-angle).cos(), ); if 15./SCALE < rel_y && rel_y < 30./SCALE && -20./SCALE < rel_x && rel_x < 20./SCALE { module.2.translation = vec3(p_pos.x + 53./SCALE*angle.sin(), p_pos.y - 53./SCALE*angle.cos(), 0.); module.2.rotation = Quat::from_euler(EulerRot::ZYX, angle, 0., 0.); module.3.linvel = velocity.linvel; let joint = FixedJointBuilder::new().local_anchor1(vec2(0. / SCALE, -53. / SCALE)); let mut module_entity = commands.entity(module.0); module_entity.insert(ImpulseJoint::new(entity, joint)); attach.children[2] = Some(module.0); break; } else if -30./SCALE < rel_y && rel_y < -15./SCALE && -20./SCALE < rel_x && rel_x < 20./SCALE { module.2.translation = vec3(p_pos.x - 53./SCALE*angle.sin(), p_pos.y + 53./SCALE*angle.cos(), 0.); module.2.rotation = Quat::from_euler(EulerRot::ZYX, angle + std::f32::consts::PI, 0., 0.); module.3.linvel = velocity.linvel; let joint = FixedJointBuilder::new().local_anchor1(vec2(0. / SCALE, 53. / SCALE)); let mut module_entity = commands.entity(module.0); module_entity.insert(ImpulseJoint::new(entity, joint)); attach.children[0] = Some(module.0); break; } else if -30./SCALE < rel_x && rel_x < -15./SCALE && -20./SCALE < rel_y && rel_y < 20./SCALE { module.2.translation = vec3(p_pos.x + 53./SCALE*angle.cos(), p_pos.y + 53./SCALE*angle.sin(), 0.); module.2.rotation = Quat::from_euler(EulerRot::ZYX, angle + (std::f32::consts::PI/2.), 0., 0.); module.3.linvel = velocity.linvel; let joint = FixedJointBuilder::new() .local_anchor1(vec2(53. / SCALE, 0. / SCALE)) .local_basis2(std::f32::consts::PI/2.); let mut module_entity = commands.entity(module.0); module_entity.insert(ImpulseJoint::new(entity, joint)); attach.children[1] = Some(module.0); break; } else if 15./SCALE < rel_x && rel_x < 30./SCALE && -20./SCALE < rel_y && rel_y < 20./SCALE { module.2.translation = vec3(p_pos.x - 53./SCALE*angle.cos(), p_pos.y - 53./SCALE*angle.sin(), 0.); module.2.rotation = Quat::from_euler(EulerRot::ZYX, angle - (std::f32::consts::PI/2.), 0., 0.); module.3.linvel = velocity.linvel; let joint = FixedJointBuilder::new() .local_anchor1(vec2(-53. / SCALE, 0. / SCALE)) .local_basis2(-std::f32::consts::PI/2.); let mut module_entity = commands.entity(module.0); module_entity.insert(ImpulseJoint::new(entity, joint)); attach.children[1] = Some(module.0); break; } module.2.translation = vec3(x / SCALE, y / SCALE, 0.); break; } for (entity, part_type, transform, _) in &part_query { let pos = transform.translation; let rel_x = pos.x - x / SCALE; let rel_y = pos.y - y / SCALE; 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 = [-25. / SCALE, 25. / SCALE, -25. / SCALE, 25. / SCALE]; // left, right, top, bottom if let PartType::Cargo = part_type { bound = [-18.75 / SCALE, 18.75 / SCALE, -25. / SCALE, 21.875 / SCALE]; } if bound[0] < x && x < bound[1] { if bound[2] < y && y < bound[3] { q_player.selected = Some(entity); break; } } } } } } _ => 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 ServerEvent::Close(addr) = packet { for (entity, player, attach) in &player_query { if player.addr == *addr { despawn_module_tree(&mut commands, attach, &attached_query, &part_query); commands.entity(entity).despawn_recursive(); let packet = Packet::PlayerLeave { id: entity.index() }; let buf = serde_json::to_vec(&packet).unwrap(); for (in_entity, player, _) in &player_query { if entity != in_entity { packets.push(ServerEvent::Send( player.addr, MessageType::Text, buf.clone(), )); } } } } } } for packet in packets { packet_send.send(packet); } } fn despawn_module_tree( commands: &mut Commands, attach: &Attach, attached_query: &Query<&Attach, With>, part_query: &Query<&PartType>, ) { for child in attach.children { if let Some(child) = child { commands.entity(child).despawn_recursive(); let attach = match attached_query.get(child) { Ok(s) => s, Err(_) => { match part_query.get(child) { Ok(_) => { continue; } Err(e) => panic!("{}", e) } } }; despawn_module_tree(commands, attach, attached_query, part_query); } } } fn on_position_change( mut commands: Commands, part_query: Query<(Entity, &PartType, &Transform), Changed>, planet_query: Query<(Entity, &PlanetType, &Transform), Changed>, mut packet_send: EventWriter, ) { let mut updated_parts = Vec::new(); for (entity, part_type, transform) 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 * SCALE ) .with_rotation(transform.rotation)), }, )); } if !updated_parts.is_empty() { let packet = Packet::PartPositions { parts: updated_parts, }; let buf = serde_json::to_vec(&packet).unwrap(); packet_send.send(ServerEvent::Broadcast(MessageType::Text, buf)); } 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 * SCALE )), radius: match *planet_type { PlanetType::Earth => EARTH_SIZE, }, }, )); } if !planets.is_empty() { let packet = Packet::PlanetPositions { planets }; let buf = serde_json::to_vec(&packet).unwrap(); packet_send.send(ServerEvent::Broadcast(MessageType::Text, buf)); } } fn player_input_update( mut player_and_body_query: Query<(Entity, &mut Player, &mut ExternalForce, &Transform)>, ) { for (_, player, 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 -= 60.0; fmul_bottom_right_thruster -= 60.0; } if player.input.down { fmul_top_left_thruster += 60.0; fmul_top_right_thruster += 60.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 { let thruster_pos_uncast = vec2(x_offset / SCALE, y_offset / SCALE); let thruster_pos_cast = rot2d(thruster_pos_uncast, rot) + transform.translation.xy(); let thruster_force = force_multiplier * THRUSTER_FORCE; let thruster_vec = vec2( -thruster_force / SCALE * rot.sin(), thruster_force / SCALE * 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; } } } } fn gravity_update( mut part_query: Query<(&Transform, &ReadMassProperties, &mut ExternalImpulse), With>, planet_query: Query<(&Transform, &ReadMassProperties), With>, ) { for (part_transform, part_mp, mut impulses) in &mut part_query { impulses.impulse = Vec2::ZERO; 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 = GRAVITY * ((part_mass * planet_mass) / (distance * distance)); let direction = (planet_translate - part_translate).normalize() * force; impulses.impulse += direction.xy(); } } }