pub mod join; pub mod thrust; use crate::attachment::{Joint, JointOf, Joints, PartInShip, Peer, SnapOf, SnapOfJoint}; use crate::ecs::{DragRequestEvent, Part, Player, PlayerStorage}; use crate::server::system_sets::PlayerInputSet; use crate::server::ConnectedNetworkEntity; use crate::prelude::*; use crate::world_config::WorldConfigResource; use bevy_replicon::prelude::{ClientId, FromClient}; use std::f32::consts::PI; 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); } #[derive(Component)] #[require(Replicated)] 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 can_reach_hearty = can_reach_hearty( partially_disconnected_part, q_joints.reborrow(), q_maybe_peer.reborrow(), q_joint_of_part.reborrow(), q_is_hearty.reborrow(), &mut search_visited_joints, true ); if can_reach_hearty { // great, leave them alone continue; } // this part cannot reach hearty // trigger a disconnect on them to 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 disconnect_part( (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 has a path 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 can_reach_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, is_top_of_recursion: bool ) -> 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 can_other_part_reach = can_reach_hearty( other_part, q_joints.reborrow(), q_maybe_peer.reborrow(), q_joint_of_part.reborrow(), q_is_hearty.reborrow(), visited_joints, false ); if can_other_part_reach { // great, they are connected // log that we're in the path, then bubble up debug!("-> via {:?}", part); return true; } } } // Exhausted all options; we are not connected to hearty bubble up if is_top_of_recursion { // print a debug message debug!("partial detach DFS: visited {} joints => not connected", visited_joints.len()); } false } fn disconnect_part( (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) { disconnect_part((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>, 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>, clients: Query<&ConnectedNetworkEntity>, q_ls_me: Query>, world_config: Res, mut commands: Commands, ) { let Some(world_config) = &world_config.config else { return; }; for FromClient { client_id, message: event, } in events.read() { let player_hearty_entity = match client_id { ClientId::Client(client_entity) => { let ConnectedNetworkEntity { game_entity: player_hearty_entity, } = clients.get(*client_entity).unwrap(); player_hearty_entity }, ClientId::Server => &q_ls_me.iter().next().unwrap() }; debug!(?event, "got drag request event"); let teleport_to_translation; let teleport_to_rotation; 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 // getting attached to (hearty) let target_part = { let Ok(target_part) = parts.get(target_joint.1.0) else { continue; }; target_part }; // attached (housing) 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 mut processed = vec![source_joint.4]; disconnect_part( (source_part.2, source_part.4), &joints, &q_joints, &mut processed, commands.reborrow(), ); // create the joint... let joint = PrismaticJoint::new(target_part.2, source_part.2) .with_slider_axis(source_joint.2.translation.xy().normalize()) .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) .with_limits(0.0, 0.0) .with_align_compliance(world_config.part.joint_align_compliance) .with_angle_compliance(world_config.part.joint_angle_compliance) .with_limit_compliance(world_config.part.joint_limit_compliance); let joint_damping = JointDamping { linear: world_config.part.joint_linear_damping, angular: world_config.part.joint_angular_damping, }; let joint_id = commands.spawn((joint, joint_damping)).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); 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 processed = vec![]; disconnect_part( (source_part.2, source_part.4), &joints, &q_joints, &mut processed, 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 magic_fuel_regen(players: Query<&mut PlayerStorage, With>, time: Res