pub mod join;
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::server::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)]
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 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::<PartInShip>(); // 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::<PartiallyDisconnected>();
}
}
/// 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<Option<&Peer>>,
mut q_joint_of_part: Query<&JointOf>,
mut q_is_hearty: Query<Option<&Player>>,
visited_joints: &mut Vec<Entity>,
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<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) {
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::<PartInShip>();
commands.entity(entity).insert(PartiallyDisconnected);
}
fn dragging(
mut events: MessageReader<FromClient<DragRequestEvent>>,
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>,
clients: Query<&ConnectedNetworkEntity>,
q_ls_me: Query<Entity, With<crate::client::Me>>,
world_config: Res<WorldConfigResource>,
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");
#[allow(unused_assignments, reason = "false positive")]
let mut teleport_to_translation = Vec2::new(0.0, 0.0);
#[allow(unused_assignments, reason = "false positive")]
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
// 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<Player>>, time: Res<Time>) {
for mut storage in players {
storage.fuel = (storage.fuel + 5.0 * time.delta_secs()).min(storage.fuel_capacity);
}
}