pub mod join; pub mod thrust; const FUEL_REGEN_RATE: f32 = 5.0; use crate::shared::attachment::{Joint, JointOf, Joints, PartInShip, Peer, SnapOf, SnapOfJoint}; use crate::shared::ecs::{DragAction, DragRequestEvent, Part, Player, PlayerStorage}; use crate::server::damping::ModuleJointDamping; use crate::server::system_sets::PlayerInputSet; use crate::prelude::*; use crate::shared::world_config::WorldConfigResource; use std::f64::consts::PI; use crate::client::components::Me; pub fn player_management_plugin(app: &mut App) { app.add_systems( Update, ( join::handle_pending_players, join::handle_new_players, magic_fuel_regen, dragging, ) .in_set(PlayerInputSet), ); app.add_systems(Update, complete_partial_disconnects); app.add_systems(FixedUpdate, complete_partial_disconnects); } #[derive(Component)] struct PartiallyDisconnected; /// Partial Disconnects are created when a part is disconnected to indicate that a disconnect has started. /// Disconnects cannot be completed until the next tick, after relevant Peer relationships have been removed. /// This system does this by performing a flood search to determine if the 'partial-disconnected' parts still have /// a valid path to hearty; if they do not, they are removed from the ship. fn complete_partial_disconnects( partial_disconnected_parts: Query>, mut q_joints: Query<&Joints>, mut q_maybe_peer: Query>, mut q_joint_of_part: Query<&JointOf>, mut q_is_hearty: Query>, dc_q_joints: Query<(&Joint, &JointOf, &Transform, Option<&Peer>, Entity)>, dc_q_only_joints: Query<&Joints>, mut commands: Commands, ) { for partially_disconnected_part in &partial_disconnected_parts { trace!(?partially_disconnected_part, "completing partial disconnect from previous tick"); commands.entity(partially_disconnected_part).remove::(); // is it still connected to hearty? let mut search_visited_joints = vec![]; let connected = is_connected_to_hearty( partially_disconnected_part, q_joints.reborrow(), q_maybe_peer.reborrow(), q_joint_of_part.reborrow(), q_is_hearty.reborrow(), &mut search_visited_joints, ); if connected { // great, leave them alone continue; } debug!("partial detach DFS: visited {} joints => not connected", search_visited_joints.len()); // this part cannot reach hearty; propagate the disconnection let mut disconnect_queue = vec![]; commands.entity(partially_disconnected_part).remove::(); // they're no longer in the ship, remove them from the meta propagate_disconnect( (partially_disconnected_part, if let Ok(j) = q_joints.get(partially_disconnected_part) { j } else { warn!(?partially_disconnected_part, "part does not have a Joints? this should be impossible..."); continue; }), &dc_q_joints, &dc_q_only_joints, &mut disconnect_queue, commands.reborrow() ); commands.entity(partially_disconnected_part).remove::(); } } /// Determine if a part is connected to hearty by performing a depth-first search. /// TODO: This can be very slow on large ships- the path propagation model will be significantly faster /// TODO: Ask core for an explanation of what the path propagation model is if you want to implement this fn is_connected_to_hearty( part: Entity, mut q_joints: Query<&Joints>, mut q_maybe_peer: Query>, mut q_joint_of_part: Query<&JointOf>, mut q_is_hearty: Query>, visited_joints: &mut Vec, ) -> bool { // Are we hearty? if let Ok(Some(_)) = q_is_hearty.get(part) { debug!("partial detach DFS: visited {} joints => we are hearty! @ {:?}", visited_joints.len(), part); return true; } // Get the joints of this entity let Ok(our_joints) = q_joints.get(part).cloned() else { warn!("part does not have a Joints? this should be impossible..."); return false; }; // Iterate over each joint: // if it's Hearty: great! we're connected // if it's another part: recurse to it // if it's not connected: lame, move on 'to_next_joint: for joint in &**our_joints { // mark that we've visited this joint, so we don't come back to it later visited_joints.push(*joint); // does this joint have a peer? let maybe_peer = q_maybe_peer.get(*joint).expect("cannot fail"); if let Some(peer_info) = maybe_peer { // we have a peer! figure out what it's connected to... let other_parts_joint = peer_info.peer_joint_entity_id; // have we visited this joint already? if visited_joints.contains(&other_parts_joint) { // if so, move on continue 'to_next_joint; } // we have not, find it's parent part let other_part = q_joint_of_part.get(other_parts_joint).expect("joint is missing JointOf").0; // is this part Hearty? let maybe_is_hearty = q_is_hearty.get(other_part).expect("cannot fail"); if maybe_is_hearty.is_some() { // yay! found hearty debug!("partial detach DFS: visited {} joints => found hearty @ {:?}", visited_joints.len(), other_part); debug!("-> via {:?}", part); return true; } // not hearty, but can the other part reach hearty? let other_connected = is_connected_to_hearty( other_part, q_joints.reborrow(), q_maybe_peer.reborrow(), q_joint_of_part.reborrow(), q_is_hearty.reborrow(), visited_joints, ); if other_connected { debug!("-> via {:?}", part); return true; } } } false } fn propagate_disconnect( (entity, joints): (Entity, &Joints), q_joints: &Query<(&Joint, &JointOf, &Transform, Option<&Peer>, Entity)>, q_only_joints: &Query<&Joints>, processed_peers: &mut Vec, mut commands: Commands, ) { //trace!(?entity, ?joints, ?processed_peers, "recursive disconnect"); // loop the joints in the entity for joint in joints.iter() { // now we have the entity "joint" let Ok((_, _, _, peer, _)) = q_joints.get(joint) else { continue; }; // don't want to double process a joint if processed_peers.contains(&joint) { continue; } // we have the peer that holds the physics joint let Some(peer) = peer else { continue; }; commands.entity(joint).remove::(); processed_peers.push(joint); let mut physics_joint = commands.entity(peer.physics_joint); // the physics joint shouldnt exist anymore physics_joint.despawn(); // delete the other side's peer that exists for some reason let Ok((_, other_joint_of, _, _, _)) = q_joints.get(peer.peer_joint_entity_id) else { continue; }; commands.entity(peer.peer_joint_entity_id).remove::(); let Ok(_) = q_only_joints.get(other_joint_of.0) else { continue }; //commands.entity(other_joint_of.0).remove::(); if !processed_peers.contains(&peer.peer_joint_entity_id) { propagate_disconnect((other_joint_of.0, joints), q_joints, q_only_joints, processed_peers, commands.reborrow()); } processed_peers.push(peer.peer_joint_entity_id); } // recursive disconnect part //commands.entity(entity).remove::(); commands.entity(entity).insert(PartiallyDisconnected); } fn dragging( mut events: MessageReader, me: Query>, mut parts: Query< ( &mut Transform, Option<&PartInShip>, Entity, &mut LinearVelocity, &Joints, &mut AngularVelocity, &Part, ), Without, >, snaps: Query<(&SnapOf, &SnapOfJoint)>, joints: Query<(&Joint, &JointOf, &Transform, Option<&Peer>, Entity)>, q_joints: Query<&Joints>, world_config: Res, mut commands: Commands, ) { let Some(world_config) = &world_config.config else { return; }; let Ok(player_hearty_entity) = me.single() else { return }; for event in events.read() { debug!(?event, "got drag request event"); let teleport_to_translation; let teleport_to_rotation; let mut new_linvel = None; let mut new_angvel = None; match &event.action { DragAction::Attach { snap_target, peer_snap } => { let Ok((target_snap_part, target_snap_joint)) = snaps.get(*snap_target) else { continue; }; let Ok((source_snap_part, source_snap_joint)) = snaps.get(*peer_snap) else { continue; }; let Ok((target_jt, target_jt_of, target_jt_xform, target_jt_peer, target_jt_id)) = joints.get(target_snap_joint.0) else { continue; }; let Ok((source_jt, source_jt_of, _source_jt_xform, source_jt_peer, source_jt_id)) = joints.get(source_snap_joint.0) else { continue; }; // validation: snap ownership must agree with joint ownership if target_snap_part.0 != target_jt_of.0 { warn!("drag request: mismatched target entities (potential manipulation?), ignoring"); continue; } if source_snap_part.0 != source_jt_of.0 { warn!("drag request: mismatched source entities (potential manipulation?), ignoring request"); continue; } // do not allow drags where either joint already has a peer (is attached) if target_jt_peer.is_some() { warn!("drag request: cannot attach to a joint that already has a peer, ignoring request"); continue; } if source_jt_peer.is_some() { warn!("drag request: dragging from a part that is already attached is currently not supported, ignoring request"); continue; } // getting attached to (hearty) let Ok((target_xform, target_in_ship, target_entity, target_linvel, _, target_angvel, _)) = parts.get(target_jt_of.0) else { continue; }; // attached (housing) let Ok((_, _, source_entity, _, source_joints, _, _)) = parts.get(source_jt_of.0) else { continue; }; let allowed = target_jt_of.0 == player_hearty_entity || target_in_ship.is_some_and(|u| u.0 == player_hearty_entity); if !allowed { warn!("drag request: this player cannot move this part, ignoring request"); continue; } let mut processed = vec![source_jt_id]; propagate_disconnect( (source_entity, source_joints), &joints, &q_joints, &mut processed, commands.reborrow(), ); // create the physics joint let fixed_joint = FixedJoint::new(target_entity, source_entity) .with_local_anchor1(target_jt_xform.translation.xy().into()) .with_local_basis1(target_jt.transform.rotation.to_euler(EulerRot::ZYX).0 as f64 + PI - source_jt.transform.rotation.to_euler(EulerRot::ZYX).0 as f64) .with_point_compliance(world_config.part.joint_point_compliance) .with_angle_compliance(world_config.part.joint_angle_compliance); let joint_damping = ModuleJointDamping { distance: world_config.part.joint_distance_damping, angular: world_config.part.joint_angular_damping, }; let joint_id = commands.spawn((fixed_joint, joint_damping)).id(); // create the peering components commands.entity(source_jt_id).insert(Peer { peer_joint_entity_id: target_jt_id, physics_joint: joint_id, }); commands.entity(target_jt_id).insert(Peer { peer_joint_entity_id: source_jt_id, physics_joint: joint_id, }); // propagate PartInShip let part_in_ship = if target_jt_of.0 == player_hearty_entity { PartInShip(player_hearty_entity) } else { PartInShip(target_in_ship.unwrap().0) // unwrap: checked above (during 'allowed' calculation) }; commands.entity(source_entity).insert(part_in_ship); let target_position = target_xform.mul_transform(*target_jt_xform); teleport_to_translation = target_position.translation.xy(); teleport_to_rotation = target_position.rotation * source_jt.transform.rotation.inverse() * Quat::from_rotation_z(PI as f32); new_linvel = Some(*target_linvel); new_angvel = Some(*target_angvel); } DragAction::Free { position, rotation } => { warn!("blindly accepting non-attachment request, someone should change this eventually"); warn!("dragging already attached entities may cause inconsistent behavior!!"); let Ok((_, _, free_entity, _, free_joints, _, _)) = parts.get(event.drag_target) else { continue; }; let mut processed = vec![]; propagate_disconnect( (free_entity, free_joints), &joints, &q_joints, &mut processed, commands.reborrow(), ); teleport_to_translation = *position; teleport_to_rotation = *rotation; } } let Ok((mut xform, _, _, mut linvel, _, mut angvel, _)) = parts.get_mut(event.drag_target) else { continue; }; xform.translation.x = teleport_to_translation.x; xform.translation.y = teleport_to_translation.y; xform.rotation = teleport_to_rotation; // client calculates this; no reason to recalculate if let Some(vel) = new_linvel { *linvel = vel; } if let Some(vel) = new_angvel { *angvel = vel; } } } fn magic_fuel_regen(players: Query<&mut PlayerStorage, With>, time: Res