pub mod join;
pub mod thrust;
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<Entity, With<PartiallyDisconnected>>,
mut q_joints: Query<&Joints>,
mut q_maybe_peer: Query<Option<&Peer>>,
mut q_joint_of_part: Query<&JointOf>,
mut q_is_hearty: Query<Option<&Player>>,
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::<PartiallyDisconnected>();
// 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::<PartInShip>(); // 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::<PartiallyDisconnected>();
}
}
/// 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<Option<&Peer>>,
mut q_joint_of_part: Query<&JointOf>,
mut q_is_hearty: Query<Option<&Player>>,
visited_joints: &mut Vec<Entity>,
) -> 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<Entity>,
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::<Peer>();
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::<Peer>();
let Ok(_) = q_only_joints.get(other_joint_of.0) else {
continue
};
//commands.entity(other_joint_of.0).remove::<PartInShip>();
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::<PartInShip>();
commands.entity(entity).insert(PartiallyDisconnected);
}
fn dragging(
mut events: MessageReader<DragRequestEvent>,
me: Query<Entity, With<Me>>,
mut parts: Query<
(
&mut Transform,
Option<&PartInShip>,
Entity,
&mut LinearVelocity,
&Joints,
&mut AngularVelocity,
&Part,
),
Without<Joint>,
>,
snaps: Query<(&SnapOf, &SnapOfJoint)>,
joints: Query<(&Joint, &JointOf, &Transform, Option<&Peer>, Entity)>,
q_joints: Query<&Joints>,
world_config: Res<WorldConfigResource>,
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<Player>>, time: Res<Time>) {
for mut storage in players {
storage.fuel = (storage.fuel + 5.0 * time.delta_secs()).min(storage.fuel_capacity);
}
}