use crate::attachment::{Joint, JointOf, Joints, PartInShip, Peer, SnapOf, SnapOfJoint}; use crate::config::planet::Planet; use crate::ecs::{DragRequestEvent, Part, Player, PlayerStorage, PlayerThrust, ThrustEvent}; use crate::server::part::SpawnPartRequest; use crate::server::system_sets::PlayerInputSet; use crate::server::world_config::WorldConfigResource; use crate::server::{ConnectedGameEntity, ConnectedNetworkEntity}; use crate::prelude::*; use bevy_replicon::prelude::{ClientId, FromClient}; use std::f32::consts::PI; pub fn player_management_plugin(app: &mut App) { app.add_systems( Update, ( handle_new_players, player_thrust, magic_fuel_regen, dragging, ) .in_set(PlayerInputSet), ); } fn disconnect_part( entity: Entity, recursed_entity: Entity, joints: &Joints, q_joints: Query<&Joints>, q_peer: Query<(Entity, &Peer, &JointOf)>, processed_peers: &mut Vec, mut commands: Commands, ) { trace!(?entity, ?joints, ?processed_peers, "recursive disconnect"); // recursive disconnect part for joint in &**joints { let Ok((p_e, our_peer_object, _)) = q_peer.get(*joint) else { continue; }; if processed_peers.contains(&p_e) { continue } let other_joint = our_peer_object.peer_joint_entity_id; commands.entity(*joint).remove::(); commands.entity(our_peer_object.physics_joint).despawn(); processed_peers.push(p_e); let Ok((p_e_2, _, other_joint_of)) = q_peer.get(other_joint) else { continue; }; commands.entity(other_joint).remove::(); processed_peers.push(p_e_2); let Ok(joints) = q_joints.get(other_joint_of.0) else { continue; }; if other_joint != recursed_entity { disconnect_part(other_joint, entity, joints, q_joints, q_peer, processed_peers, commands.reborrow()); } } for ppeer in processed_peers { commands.entity(*ppeer).remove::(); commands.entity(*ppeer).remove::(); } commands.entity(entity).remove::(); } fn dragging( mut events: MessageReader>, mut parts: Query< ( &mut Transform, Option<&PartInShip>, Entity, &mut LinearVelocity, &Joints, &mut AngularVelocity, ), (With, Without), >, snaps: Query<(&SnapOf, &SnapOfJoint)>, joints: Query<(&Joint, &JointOf, &Transform, Option<&Peer>, Entity)>, peer: Query<(Entity, &Peer, &JointOf)>, q_joints: Query<&Joints>, q_joint: Query<&FixedJoint>, clients: Query<&ConnectedNetworkEntity>, mut commands: Commands, ) { for FromClient { client_id, message: event, } in events.read() { let client_entity = match client_id { ClientId::Client(e) => e, _ => continue, }; let ConnectedNetworkEntity { game_entity: player_hearty_entity, } = clients.get(*client_entity).unwrap(); debug!(?event, "got drag request event"); let mut teleport_to_translation = Vec2::new(0.0, 0.0); let mut teleport_to_rotation = Quat::from_rotation_z(0.0); let mut new_linvel = None; let mut new_angvel = None; if let Some(snap_to) = event.snap_target && let Some(peer_snap) = event.peer_snap { let Ok(snap_on_target) = snaps.get(snap_to) else { continue; }; let Ok(snap_on_source) = snaps.get(peer_snap) else { continue; }; let Ok(target_joint) = joints.get(snap_on_target.1.0) else { continue; }; let Ok(source_joint) = joints.get(snap_on_source.1.0) else { continue; }; // validation step 1: everything must match. if not, ignore the request if snap_on_target.0.0 != target_joint.1.0 { warn!( "drag request: mismatched target entities (potential manipulation?), ignoring" ); continue; } if snap_on_source.0.0 != source_joint.1.0 { warn!( "drag request: mismatched source entities (potential manipulation?), ignoring request" ); continue; } // we've passed initial validation. // do not allow drags with the source or destination if they already have a peer (are attached) if target_joint.3.is_some() { warn!( "drag request: cannot attach to a joint that already has a peer, ignoring request" ); continue; } if source_joint.3.is_some() { warn!( "drag request: dragging from a part that is already attached is currently not supported, ignoring request" ); continue; } // great, the attachment appears to be valid // let's make sure this player is allowed to drag onto this part let target_part = { let Ok(target_part) = parts.get(target_joint.1.0) else { continue; }; target_part }; let source_part = { let Ok(source_part) = parts.get(source_joint.1.0) else { continue; }; source_part }; let allowed = target_joint.1.0 == *player_hearty_entity || target_part.1.is_some_and(|u| u.0 == *player_hearty_entity); if !allowed { warn!("drag request: this player cannot move this part, ignoring request"); continue; } // TODO - validate source_part? // great, we have a valid peering request let did_disconnect = q_joint.get(source_part.2).is_ok(); let mut dc_queue = vec![]; disconnect_part( source_part.2, Entity::PLACEHOLDER, source_part.4, q_joints, peer, &mut dc_queue, commands.reborrow(), ); // create the joint... let joint = FixedJoint::new(target_part.2, source_part.2) .with_local_anchor1(target_joint.2.translation.xy()) .with_local_basis1(target_joint.0.transform.rotation.to_euler(EulerRot::ZYX).0 + PI - source_joint.0.transform.rotation.to_euler(EulerRot::ZYX).0); let joint_id = commands.spawn(joint).id(); // create the peering component... commands.entity(source_joint.4).insert(Peer { peer_joint_entity_id: target_joint.4, processed: true, physics_joint: joint_id }); commands.entity(target_joint.4).insert(Peer { peer_joint_entity_id: source_joint.4, processed: true, physics_joint: joint_id }); // propagate PartInShip... let part_in_ship = if target_joint.1.0 == *player_hearty_entity { PartInShip(*player_hearty_entity) } else { PartInShip(target_part.1.unwrap().0) // unwrap: checked above (during 'allowed' calculation) }; commands.entity(source_part.2).insert(part_in_ship); let target_position = target_part.0.mul_transform(*target_joint.2); let rotation = event.set_rotation.to_scaled_axis().z; let rotation = Rot2::radians(rotation); teleport_to_translation = target_position.translation.xy(); teleport_to_rotation = target_position.rotation * source_joint.0.transform.rotation.inverse() * Quat::from_rotation_z(PI); new_linvel = Some(*target_part.3); new_angvel = Some(*target_part.5); // and we're done! } else { warn!( "blindly accepting non-attachment request, someone should change this eventually" ); warn!("dragging already attached entities may cause inconsistent behavior!!"); let source_part = parts.get(event.drag_target).unwrap(); let mut dc_queue = vec![]; disconnect_part( source_part.2, Entity::PLACEHOLDER, source_part.4, q_joints, peer, &mut dc_queue, commands.reborrow(), ); teleport_to_translation = event.drag_to; teleport_to_rotation = event.set_rotation; } let mut part = parts.get_mut(event.drag_target).unwrap(); part.0.translation.x = teleport_to_translation.x; part.0.translation.y = teleport_to_translation.y; part.0.rotation = teleport_to_rotation; // client calculates this; no reason to recalculate if let Some(new_vel) = new_linvel { *part.3 = new_vel; } if let Some(new_vel) = new_angvel { *part.5 = new_vel; } // ( the math sucks ) } } fn handle_new_players( mut commands: Commands, q_new_clients: Query>, world_config: Res, planets: Query<(&Transform, &Planet)>, asset_server: Res, ) { let Some(wc) = &world_config.config else { return; }; for joined_player in &q_new_clients { trace!(?joined_player, "detected joined player!"); // find earth let (spawn_planet_pos, spawn_planet) = planets .iter() .find(|p| p.1.name == wc.hearty.spawn_at) .unwrap_or_else(|| { panic!( "spawn planet {} is missing? (check that the planet is named exactly '{}')", wc.hearty.spawn_at, wc.hearty.spawn_at ) }); let angle = rand::random::() * std::f32::consts::TAU; let offset = spawn_planet.radius + 150.0; let mut new_transform = Transform::from_xyz(angle.cos() * offset, angle.sin() * offset, 0.0); new_transform.rotate_z(angle); new_transform.translation += spawn_planet_pos.translation; info!(?new_transform, ?joined_player, "set player's position!"); commands .entity(joined_player) .insert(new_transform) .insert(SpawnPartRequest( asset_server.load("config/parts/hearty.part.toml"), )) .insert(PlayerThrust::default()) .insert(PlayerStorage { fuel_capacity: 25.0, fuel: 25.0, power_capacity: 25.0, power: 25.0, }) .insert(Player { client: joined_player, }); } } fn magic_fuel_regen(players: Query<&mut PlayerStorage, With>, time: Res