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 bevy::prelude::*;
use bevy_rapier2d::prelude::{ExternalForce, FixedJointBuilder, ImpulseJoint, Velocity};
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,
(reprocess_reattached_parts, dragging).chain(),
)
.in_set(PlayerInputSet),
);
}
#[derive(Component)]
struct JointNeedsCreation(ImpulseJoint);
fn disconnect_part(
entity: Entity,
recursed_entity: Entity,
joints: &Joints,
q_joints: Query<&Joints>,
q_peer: Query<(Entity, &Peer, &JointOf)>,
mut processed_peers: &mut Vec<Entity>,
mut commands: Commands,
) {
trace!(?entity, ?joints, ?processed_peers, "recursive disconnect");
// recursive disconnect part
for joint in &**joints {
let Ok((p_e, other_joint_handle, _)) = q_peer.get(*joint) else {
continue;
};
if processed_peers.contains(&p_e) { continue };
let other_joint = other_joint_handle.0;
commands.entity(*joint).remove::<Peer>();
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::<Peer>();
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::<Peer>();
}
commands.entity(entity).remove::<ImpulseJoint>();
commands.entity(entity).remove::<PartInShip>();
}
fn dragging(
mut events: MessageReader<FromClient<DragRequestEvent>>,
mut parts: Query<
(
&mut Transform,
Option<&PartInShip>,
Entity,
&mut Velocity,
&Joints,
),
(With<Part>, Without<Joint>),
>,
snaps: Query<(&SnapOf, &SnapOfJoint)>,
joints: Query<(&Joint, &JointOf, &Transform, Option<&Peer>, Entity)>,
peer: Query<(Entity, &Peer, &JointOf)>,
q_joints: Query<&Joints>,
q_joint: Query<&ImpulseJoint>,
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_vel = 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 peering component...
commands.entity(source_joint.4).insert(Peer(target_joint.4, true));
commands.entity(target_joint.4).insert(Peer(source_joint.4, true));
// 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);
// create the joint...
let joint = FixedJointBuilder::new()
.local_anchor1(target_joint.2.translation.xy())
.local_basis1(
target_joint.0.transform.rotation.to_euler(EulerRot::ZYX).0 + PI
- source_joint.0.transform.rotation.to_euler(EulerRot::ZYX).0,
);
commands.entity(source_part.2).remove::<ImpulseJoint>();
if did_disconnect {
// we disconnected this part this tick, and are performing a "reattachment"
// (dragging an already attached part from peering point A to peering point B)
// If we're reattaching to a different part, rapier will ignore our new attachment
// as it will be seen as a mutation (removing and adding a component in the
// same tick is considered equivalent to mutation by Bevy).
// As Rapier does not allow you to mutate the source/destination of a joint,
// the change will be outright ignored.
// Since there is no (easy) way to get the real joint information back out of Rapier,
// or to force it to accept what it sees as invalid mutation,
// we need to delay the creation of the new joint by 1 tick so it's noticed by Rapier
// This component will be swapped out for a real ImpulseJoint on the next tick
// by `reprocess_reattached_parts` (in this file)
commands
.entity(source_part.2)
.insert(JointNeedsCreation(ImpulseJoint::new(target_part.2, joint)));
} else {
commands
.entity(source_part.2)
.insert(ImpulseJoint::new(target_part.2, joint));
}
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_vel = Some(*target_part.3);
// 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_vel {
*part.3 = new_vel;
}
// ( the math sucks )
}
}
// LOGIC: **MUST** run BEFORE `dragging`
fn reprocess_reattached_parts(
reattached: Query<(Entity, &JointNeedsCreation)>,
mut commands: Commands,
) {
for (e, j) in &reattached {
commands
.entity(e)
.remove::<JointNeedsCreation>()
.insert(j.0);
}
}
fn handle_new_players(
mut commands: Commands,
q_new_clients: Query<Entity, Added<ConnectedGameEntity>>,
world_config: Res<WorldConfigResource>,
planets: Query<(&Transform, &Planet)>,
asset_server: Res<AssetServer>,
) {
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::<f32>() * 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(Velocity::default())
.insert(Player {
client: joined_player,
});
}
}
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);
}
}
fn player_thrust(
mut players: Query<(
&Transform,
&Part,
&mut ExternalForce,
&mut PlayerThrust,
&mut PlayerStorage,
)>,
clients: Query<&ConnectedNetworkEntity>,
mut thrust_event: MessageReader<FromClient<ThrustEvent>>,
world_config: Res<WorldConfigResource>,
) {
for FromClient {
client_id,
message: event,
} in thrust_event.read()
{
let client_entity = match client_id {
ClientId::Client(e) => e,
_ => continue,
};
let ConnectedNetworkEntity { game_entity } = clients.get(*client_entity).unwrap();
let Ok((_, _, _, mut thrust, _)) = players.get_mut(*game_entity) else {
continue;
};
match *event {
ThrustEvent::Up(on) => thrust.up = on,
ThrustEvent::Down(on) => thrust.down = on,
ThrustEvent::Left(on) => thrust.left = on,
ThrustEvent::Right(on) => thrust.right = on,
}
}
for (transform, part, mut force, thrust, mut storage) in &mut players {
let Some(world_config) = &world_config.config else {
return;
};
let forward = (transform.rotation * Vec3::Y).xy();
let mut external_force = ExternalForce::default();
// indices are quadrants:
// 1 | 0
// --+--
// 2 | 3
//
let mut thrusters = [0.0; 4];
if thrust.up {
thrusters[1] = 1.0;
thrusters[2] = 1.0;
}
if thrust.down {
thrusters[0] = 1.0;
thrusters[3] = 1.0;
}
if thrust.left {
thrusters[0] = 1.0;
thrusters[2] = 1.0;
}
if thrust.right {
thrusters[1] = 1.0;
thrusters[3] = 1.0;
}
// prevent fuel wasting when turning while moving forward/reverse
if thrusters[2] == thrusters[3] && thrusters[3] != 0.0 {
(thrusters[2], thrusters[3]) = (0.0, 0.0);
}
if thrusters[0] == thrusters[1] && thrusters[1] != 0.0 {
(thrusters[0], thrusters[1]) = (0.0, 0.0);
}
let half_size = Vec2::new(
world_config.part.default_width / 2.0,
world_config.part.default_height / 2.0,
)
.length();
external_force += ExternalForce::at_point(
-forward * thrusters[0] * world_config.hearty.thrust,
transform.translation.xy()
+ half_size
* Vec2::new((1.0 * PI / 4.0).cos(), (1.0 * PI / 4.0).sin()).rotate(forward),
transform.translation.xy(),
);
external_force += ExternalForce::at_point(
forward * thrusters[1] * world_config.hearty.thrust,
transform.translation.xy()
+ half_size
* Vec2::new((3.0 * PI / 4.0).cos(), (3.0 * PI / 4.0).sin()).rotate(forward),
transform.translation.xy(),
);
external_force += ExternalForce::at_point(
forward * thrusters[2] * world_config.hearty.thrust,
transform.translation.xy()
+ half_size
* Vec2::new((5.0 * PI / 4.0).cos(), (5.0 * PI / 4.0).sin()).rotate(forward),
transform.translation.xy(),
);
external_force += ExternalForce::at_point(
-forward * thrusters[3] * world_config.hearty.thrust,
transform.translation.xy()
+ half_size
* Vec2::new((7.0 * PI / 4.0).cos(), (7.0 * PI / 4.0).sin()).rotate(forward),
transform.translation.xy(),
);
*force += external_force;
storage.fuel -=
thrusters.iter().sum::<f32>() * part.strong_config.thruster.as_ref().unwrap().flow_rate;
}
}