From 94b1b4226b5c91d94b827ff384b1d75e29fb6342 Mon Sep 17 00:00:00 2001 From: core Date: Fri, 18 Jul 2025 21:55:59 -0400 Subject: [PATCH] drag reattachment --- crates/unified/assets/config/world.wc.toml | 4 +- crates/unified/src/client/parts.rs | 15 ++++- crates/unified/src/server/player.rs | 66 ++++++++++++++++++++-- 3 files changed, 76 insertions(+), 9 deletions(-) diff --git a/crates/unified/assets/config/world.wc.toml b/crates/unified/assets/config/world.wc.toml index 3cfcbbe9498eb041965a536b323122e03bd047f1..b4ac55ac9d033ff63bebff34242bde7de857b6cf 100644 --- a/crates/unified/assets/config/world.wc.toml +++ b/crates/unified/assets/config/world.wc.toml @@ -1,5 +1,5 @@ [world] -gravity = 0.001 +gravity = 0.005 spawn_parts_at = "Earth" spawn_parts_interval_secs = 10 @@ -9,5 +9,5 @@ default_width = 50 default_mass = 100 [hearty] -thrust = 50000 +thrust = 500000 spawn_at = "Earth" diff --git a/crates/unified/src/client/parts.rs b/crates/unified/src/client/parts.rs index 62349f41fad722324f6c5897637065bb0a80fa99..a961b6add8f31ab23843f487687c86dbc023e536 100644 --- a/crates/unified/src/client/parts.rs +++ b/crates/unified/src/client/parts.rs @@ -1,4 +1,4 @@ -use crate::attachment::{JointOf, PartInShip, Peer, SnapOf, SnapOfJoint}; +use crate::attachment::{Joint, JointOf, Joints, PartInShip, Peer, SnapOf, SnapOfJoint}; use crate::client::Me; use crate::client::colors::GREEN; use crate::client::key_input::AttachmentDebugRes; @@ -89,7 +89,8 @@ const ROTATION_SMOOTH: f32 = 0.1; fn on_part_click( ev: Trigger>, - sprites: Query<(&Sprite, &Transform), Without>, + sprites: Query<(&Sprite, &Transform, &Joints), Without>, + joints: Query<&Joint, Without>, mut drag: ResMut, mut commands: Commands, ) { @@ -99,6 +100,16 @@ fn on_part_click( let Ok(sprite) = sprites.get(ev.target()) else { return; }; + // make sure it has at least 1 valid unpeered joint + let mut valid_peers = 0; + for joint in &**sprite.2 { + if joints.get(*joint).is_ok() { + valid_peers += 1; + } + } + if valid_peers == 0 { + return; // ignore + } let mut s = sprite.0.clone(); s.color = Color::srgba(0.7, 0.7, 0.7, 1.0); commands.spawn((DragGhost, GhostTarget(sprite.1.rotation), *sprite.1, s)); diff --git a/crates/unified/src/server/player.rs b/crates/unified/src/server/player.rs index 9ae87685ec3341c349a25fddc366a737e30f7d85..543861963014b7dcc90d16ddc36fe0e6bc8a8892 100644 --- a/crates/unified/src/server/player.rs +++ b/crates/unified/src/server/player.rs @@ -8,20 +8,24 @@ use bevy::prelude::*; use bevy_rapier2d::prelude::{ExternalForce, FixedJointBuilder, ImpulseJoint, Velocity}; use bevy_replicon::prelude::FromClient; use std::f32::consts::PI; -use crate::attachment::{Joint, JointOf, PartInShip, Peer, SnapOf, SnapOfJoint}; +use crate::attachment::{Joint, JointOf, Joints, PartInShip, Peer, SnapOf, SnapOfJoint}; pub fn player_management_plugin(app: &mut App) { app.add_systems( Update, - (handle_new_players, player_thrust, magic_fuel_regen, dragging).in_set(PlayerInputSet), + (handle_new_players, player_thrust, magic_fuel_regen, (reprocess_reattached_parts, dragging).chain()).in_set(PlayerInputSet), ); } +#[derive(Component)] +struct JointNeedsCreation(ImpulseJoint); + fn dragging( mut events: EventReader>, - mut parts: Query<(&mut Transform, Option<&PartInShip>, Entity, &mut Velocity), (With, Without)>, + mut parts: Query<(&mut Transform, Option<&PartInShip>, Entity, &mut Velocity, &Joints), (With, Without)>, snaps: Query<(&SnapOf, &SnapOfJoint)>, joints: Query<(&Joint, &JointOf, &Transform, Option<&Peer>, Entity)>, + peer: Query<&Peer>, clients: Query<&ConnectedNetworkEntity>, mut commands: Commands ) { @@ -83,6 +87,17 @@ fn dragging( // TODO - validate source_part? // great, we have a valid peering request + + let mut did_disconnect = false; + // disconnect it, if it is connected + for joint in &**source_part.4 { + let Ok(joint_peer) = peer.get(*joint) else { continue }; + did_disconnect = true; + commands.entity(*joint).remove::(); + commands.entity(joint_peer.0).remove::(); + } + commands.entity(source_part.2).remove::(); + // create the peering component... commands.entity(source_joint.4) .insert(Peer(target_joint.4)); @@ -111,8 +126,28 @@ fn dragging( .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) - .insert(ImpulseJoint::new(target_part.2, joint)); + commands.entity(source_part.2).remove::(); + if !did_disconnect { + commands.entity(source_part.2) + .insert(ImpulseJoint::new(target_part.2, joint)); + } else { + // 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))); + } teleport_to_translation = target_position.translation.xy(); teleport_to_rotation = target_position.rotation * source_joint.0.transform.rotation.inverse() * Quat::from_rotation_z(PI); @@ -121,6 +156,14 @@ fn dragging( } 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(); + // disconnect it, if it is connected + for joint in &**source_part.4 { + let Ok(joint_peer) = peer.get(*joint) else { continue }; + commands.entity(*joint).remove::(); + commands.entity(joint_peer.0).remove::(); + } + commands.entity(source_part.2).remove::(); teleport_to_translation = event.drag_to; teleport_to_rotation = event.set_rotation; } @@ -136,6 +179,19 @@ fn dragging( } } +// 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::() + .insert(j.0); + } +} + + fn handle_new_players( mut commands: Commands, q_new_clients: Query>,