diff --git a/Cargo.toml b/Cargo.toml index 4852f52..59619bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,9 +13,14 @@ repository = "https://github.com/PROMETHIA-27/bevy_mod_wanderlust" version = "0.4.0" [features] -default = ["rapier"] +default = ["rapier3d"] debug_lines = [] -rapier = ["bevy_rapier3d"] +rapier = [] +rapier3d = ["rapier", "bevy_rapier3d"] +rapier2d = ["rapier", "bevy_rapier2d"] +xpbd = [] +xpbd3d = ["xpbd", "bevy_xpbd_3d"] +xpbd2d = ["xpbd", "bevy_xpbd_2d"] [dependencies] bevy = { version = "0.11", default-features = false, features = [ @@ -23,16 +28,22 @@ bevy = { version = "0.11", default-features = false, features = [ "bevy_gizmos", ] } bevy_rapier3d = { version = "0.22", default-features = false, features = [ + "debug-render", "async-collider", "dim3", ], optional = true } +bevy_rapier2d = { version = "0.22", default-features = false, features = [ + "async-collider", + "dim2", +], optional = true } +bevy_xpbd_3d = { version = "0.2", optional = true } +bevy_xpbd_2d = { version = "0.2", optional = true } [dev-dependencies] bevy = "0.11" aether_spyglass = "0.2" bevy-inspector-egui = "0.19" bevy_framepace = "0.13" -bevy_rapier3d = { version = "0.22", features = ["debug-render"] } # Enable a small amount of optimization in debug mode [profile.dev] @@ -44,4 +55,4 @@ opt-level = 3 [patch.crates-io] #bevy_rapier3d = { path = "../bevy_rapier/bevy_rapier3d" } -bevy_rapier3d = { git = "https://github.com/dimforge/bevy_rapier", rev = "0ea000b" } \ No newline at end of file +bevy_rapier3d = { git = "https://github.com/dimforge/bevy_rapier", rev = "0ea000b" } diff --git a/examples/playground.rs b/examples/playground.rs index 1c81f54..6f10c8b 100644 --- a/examples/playground.rs +++ b/examples/playground.rs @@ -88,11 +88,10 @@ fn main() { } }, ) - // Add to PreUpdate to ensure updated before movement is calculated .add_systems( Update, ( - movement_input.before(bevy_mod_wanderlust::movement_force), + movement_input.before(bevy_mod_wanderlust::WanderlustSet::Sync), toggle_cursor_lock, oscillating, controlled_platform, @@ -154,7 +153,7 @@ pub fn player( translation: Vec3::new(0.0, 3.0, 0.0), ..default() }, - rapier_physics: RapierPhysicsBundle { + rapier: RapierPhysicsBundle { // Lock the axes to prevent camera shake whilst moving up slopes //locked_axes: LockedAxes::ROTATION_LOCKED, //locked_axes: LockedAxes::all(), diff --git a/src/backend/mod.rs b/src/backend/mod.rs new file mode 100644 index 0000000..893d471 --- /dev/null +++ b/src/backend/mod.rs @@ -0,0 +1,18 @@ +#[cfg(feature = "rapier")] +pub mod rapier; +pub use rapier::*; + +#[cfg(feature = "xpbd")] +mod xpbd; +#[cfg(feature = "xpbd")] +pub use xpbd::{ + apply_forces, + apply_ground_forces, + cast_ray, + //cast_shape, + setup_physics_context, + Mass, + SpatialQuery, + Velocity, + XpbdPhysicsBundle as BackendPhysicsBundle, +}; diff --git a/src/backend/query.rs b/src/backend/query.rs new file mode 100644 index 0000000..b75b538 --- /dev/null +++ b/src/backend/query.rs @@ -0,0 +1,22 @@ + +#[derive(Debug, Copy, Clone, Reflect)] +pub struct RayCastResult { + pub entity: Entity, + pub toi: f32, + pub normal: Vec3, + pub point: Vec3, +} + +#[derive(Debug, Copy, Clone, Reflect)] +pub struct ShapeCastResult { + pub entity: Entity, + pub toi: f32, + pub normal1: Vec3, + pub normal2: Vec3, + pub point1: Vec3, + pub point2: Vec3, +} + +pub struct QueryFilter { + pub exclude: HashSet, +} \ No newline at end of file diff --git a/src/rapier.rs b/src/backend/rapier/bundle.rs similarity index 50% rename from src/rapier.rs rename to src/backend/rapier/bundle.rs index 4e84ba6..b370096 100644 --- a/src/rapier.rs +++ b/src/backend/rapier/bundle.rs @@ -1,6 +1,5 @@ -use crate::{controller::*, physics::*}; +use super::rapier::prelude::*; use bevy::prelude::*; -use bevy_rapier3d::prelude::*; /// Contains common physics settings for character controllers. #[derive(Bundle)] @@ -58,49 +57,3 @@ impl Default for RapierPhysicsBundle { } } } - -/// Apply forces to the controller to make it float, move, jump, etc. -pub fn apply_forces( - mut forces: Query<(&mut ExternalImpulse, &ControllerForce)>, - ctx: Res, -) { - let dt = ctx.integration_parameters.dt; - for (mut impulse, force) in &mut forces { - impulse.impulse += force.linear * dt; - impulse.torque_impulse += force.angular * dt; - } -} - -/// Apply the opposing ground force to the entity we are pushing off of to float. -pub fn apply_ground_forces( - mut impulses: Query<&mut ExternalImpulse>, - ground_forces: Query<(&GroundForce, &ViableGroundCast)>, - ctx: Res, -) { - let dt = ctx.integration_parameters.dt; - for (force, viable_ground) in &ground_forces { - if let Some(ground) = viable_ground.current() { - if let Ok(mut impulse) = impulses.get_mut(ground.entity) { - impulse.impulse += force.linear * dt; - impulse.torque_impulse += force.angular * dt; - } - } - } -} - -/// Sync rapier masses over to our masses. -pub fn get_mass_from_rapier(mut query: Query<(&mut ControllerMass, &ReadMassProperties)>) { - for (mut mass, rapier_mass) in &mut query { - mass.mass = rapier_mass.mass; - mass.inertia = rapier_mass.principal_inertia; - mass.com = rapier_mass.local_center_of_mass; - } -} - -/// Sync rapier velocities over to our velocities. -pub fn get_velocity_from_rapier(mut query: Query<(&mut ControllerVelocity, &Velocity)>) { - for (mut vel, rapier_vel) in &mut query { - vel.linear = rapier_vel.linvel; - vel.angular = rapier_vel.angvel; - } -} diff --git a/src/backend/rapier/mass.rs b/src/backend/rapier/mass.rs new file mode 100644 index 0000000..593bdf1 --- /dev/null +++ b/src/backend/rapier/mass.rs @@ -0,0 +1,19 @@ +use super::rapier::prelude::*; +use crate::*; +use bevy::{ecs::query::WorldQuery, prelude::*}; + +pub fn get_mass_from_backend(mut query: Query<(&mut ControllerMass, &ReadMassProperties)>) { + for (mut mass, rapier) in &mut query { + *mass = ControllerMass::from_rapier(&*rapier); + } +} + +impl ControllerMass { + pub fn from_rapier(rapier: &MassProperties) -> Self { + Self { + mass: rapier.mass, + inertia: rapier.principal_inertia, + local_center_of_mass: rapier.local_center_of_mass, + } + } +} diff --git a/src/backend/rapier/mod.rs b/src/backend/rapier/mod.rs new file mode 100644 index 0000000..4eeb1c4 --- /dev/null +++ b/src/backend/rapier/mod.rs @@ -0,0 +1,74 @@ +//use crate::{controller::*, physics::*}; +use crate::*; +use bevy::prelude::*; + +#[cfg(feature = "rapier2d")] +pub use bevy_rapier2d as rapier; +#[cfg(feature = "rapier3d")] +pub use bevy_rapier3d as rapier; + +use rapier::prelude::*; + +pub fn backend_label() -> PhysicsSet { + PhysicsSet::SyncBackend +} + +mod bundle; +pub use bundle::RapierPhysicsBundle; +mod mass; +pub use mass::get_mass_from_backend; +mod velocity; +pub use velocity::get_velocity_from_backend; +mod plugin; +mod query; +pub use plugin::WanderlustRapierPlugin; + +use rapier::prelude::Collider; + +/// Apply forces to the controller to make it float, move, jump, etc. +pub fn apply_forces( + ctx: Res, + mut forces: Query<(&mut ExternalImpulse, &ControllerForce)>, +) { + let dt = ctx.integration_parameters.dt; + for (mut impulse, force) in &mut forces { + impulse.impulse += force.linear * dt; + impulse.torque_impulse += force.angular * dt; + } +} + +/// Apply the opposing ground force to the entity we are pushing off of to float. +pub fn apply_ground_forces( + ctx: Res, + mut impulses: Query<&mut ExternalImpulse>, + ground_forces: Query<(&GroundForce, &ViableGroundCast)>, +) { + let dt = ctx.integration_parameters.dt; + for (force, cast) in &ground_forces { + if let Some(ground) = cast.current() { + if let Some(ground_body) = ctx.collider_parent(ground.entity) { + if let Ok(mut impulse) = impulses.get_mut(ground_body) { + impulse.impulse += force.linear * dt; + impulse.torque_impulse += force.angular * dt; + } + } + } + } +} + +pub fn update_delta_time(mut physics_dt: ResMut, ctx: Res) { + physics_dt.0 = ctx.integration_parameters.dt; +} + +/// *Note: Most users will not need to use this directly. Use [`WanderlustPlugin`](crate::plugins::WanderlustPlugin) instead. +/// Alternatively, if one only wants to disable the system, use [`WanderlustPhysicsTweaks`](WanderlustPhysicsTweaks).* +/// +/// This system adds some tweaks to rapier's physics settings that make the character controller behave better. +pub fn setup_physics_context(mut ctx: ResMut) { + let params = &mut ctx.integration_parameters; + // This prevents any noticeable jitter when running facefirst into a wall. + params.erp = 0.99; + // This prevents (most) noticeable jitter when running facefirst into an inverted corner. + params.max_velocity_iterations = 16; + // TODO: Fix jitter that occurs when running facefirst into a normal corner. +} diff --git a/src/backend/rapier/plugin.rs b/src/backend/rapier/plugin.rs new file mode 100644 index 0000000..9d0d6c1 --- /dev/null +++ b/src/backend/rapier/plugin.rs @@ -0,0 +1,38 @@ +use crate::*; +use bevy::{ecs::schedule::ScheduleLabel, prelude::*, utils::HashSet}; + +pub struct WanderlustRapierPlugin { + pub tweaks: bool, + pub schedule: Box, +} + +impl Plugin for WanderlustRapierPlugin { + fn build(&self, app: &mut App) { + if self.tweaks { + app.add_systems(Startup, super::setup_physics_context); + } + + app.configure_sets( + self.schedule.clone(), + (WanderlustSet::Apply,).before(crate::rapier::PhysicsSet::SyncBackend), + ); + + app.add_systems( + self.schedule.clone(), + ( + super::update_delta_time, + super::get_mass_from_backend, + super::get_velocity_from_backend, + ) + .chain() + .in_set(WanderlustSet::Sync), + ); + + app.add_systems( + self.schedule.clone(), + (super::apply_forces, super::apply_ground_forces) + .chain() + .in_set(WanderlustSet::Apply), + ); + } +} diff --git a/src/backend/rapier/query.rs b/src/backend/rapier/query.rs new file mode 100644 index 0000000..e504eeb --- /dev/null +++ b/src/backend/rapier/query.rs @@ -0,0 +1,4 @@ +use super::rapier::prelude::*; +use bevy::prelude::*; + +pub type SpatialQuery<'w, 's> = Res<'s, RapierContext>; diff --git a/src/backend/rapier/velocity.rs b/src/backend/rapier/velocity.rs new file mode 100644 index 0000000..4416cfd --- /dev/null +++ b/src/backend/rapier/velocity.rs @@ -0,0 +1,10 @@ +use super::rapier::prelude::*; +use crate::*; +use bevy::{ecs::query::WorldQuery, prelude::*}; + +pub fn get_velocity_from_backend(mut query: Query<(&mut ControllerVelocity, &Velocity)>) { + for (mut velocity, rapier) in &mut query { + velocity.linear = rapier.linvel; + velocity.angular = rapier.angvel; + } +} diff --git a/src/backend/xpbd/mass.rs b/src/backend/xpbd/mass.rs new file mode 100644 index 0000000..baea0f4 --- /dev/null +++ b/src/backend/xpbd/mass.rs @@ -0,0 +1,23 @@ +use super::xpbd; +use bevy::{ecs::query::WorldQuery, prelude::*}; + +#[derive(WorldQuery)] +pub struct Mass { + mass: &'static xpbd::prelude::Mass, + inertia: &'static xpbd::prelude::Inertia, + center_of_mass: &'static xpbd::prelude::CenterOfMass, +} + +impl<'a> MassItem<'a> { + pub fn mass(&self) -> f32 { + self.mass.0 + } + + pub fn inertia(&self) -> Mat3 { + self.inertia.0 + } + + pub fn local_center_of_mass(&self) -> Vec3 { + self.center_of_mass.0 + } +} diff --git a/src/backend/xpbd/mod.rs b/src/backend/xpbd/mod.rs new file mode 100644 index 0000000..84f58d0 --- /dev/null +++ b/src/backend/xpbd/mod.rs @@ -0,0 +1,83 @@ +use bevy::prelude::*; + +#[cfg(feature = "xpbd_2d")] +pub use bevy_xpbd_2d as xpbd; +#[cfg(feature = "xpbd_3d")] +pub use bevy_xpbd_3d as xpbd; + +use xpbd::prelude::*; + +mod mass; +pub use mass::*; +mod velocity; +pub use velocity::*; + +pub use xpbd::prelude::Collider; + +/// Contains common physics settings for character controllers. +#[derive(Bundle)] +pub struct XpbdPhysicsBundle { + /// See [`RigidBody`]. + pub rigidbody: RigidBody, + /// See [`Collider`]. + pub collider: Collider, + /// See [`GravityScale`]. + pub gravity: GravityScale, + /// See [`Friction`]. + pub friction: Friction, + /// See [`Restitution`]. + pub restitution: Restitution, +} + +impl Default for XpbdPhysicsBundle { + fn default() -> Self { + Self { + rigidbody: default(), + collider: Collider::capsule_endpoints( + Vec3::new(0.0, 0.0, 0.0), + Vec3::new(0.0, 0.5, 0.0), + 0.5, + ), + gravity: GravityScale(0.0), + friction: Friction::new(0.0).with_combine_rule(CoefficientCombine::Min), + restitution: Restitution::new(0.0).with_combine_rule(CoefficientCombine::Min), + } + } +} + +pub fn apply_forces() {} +pub fn apply_ground_forces() {} +pub fn setup_physics_context() {} + +pub type SpatialQuery<'w, 's> = xpbd::prelude::SpatialQuery<'w, 's>; + +use crate::backend::{Filter, RayCastResult}; +pub fn cast_ray( + spatial_query: &SpatialQuery, + origin: Vec3, + direction: Vec3, + max_toi: f32, + solid: bool, + filter: Filter, +) -> Option { + spatial_query + .cast_ray( + origin, + direction, + max_toi, + solid, + SpatialQueryFilter { + excluded_entities: filter.exclude, + ..default() + }, + ) + .map(|result| { + let point = origin + direction * result.time_of_impact; + RayCastResult { + entity: result.entity, + normal: result.normal, + point: point, + toi: result.time_of_impact, + } + }) +} diff --git a/src/backend/xpbd/velocity.rs b/src/backend/xpbd/velocity.rs new file mode 100644 index 0000000..47e1080 --- /dev/null +++ b/src/backend/xpbd/velocity.rs @@ -0,0 +1,18 @@ +use super::xpbd; +use bevy::{ecs::query::WorldQuery, prelude::*}; + +#[derive(WorldQuery)] +pub struct Velocity { + linear: &'static xpbd::prelude::LinearVelocity, + angular: &'static xpbd::prelude::AngularVelocity, +} + +impl<'a> VelocityItem<'a> { + pub fn linear(&self) -> Vec3 { + **self.linear + } + + pub fn angular(&self) -> Vec3 { + **self.angular + } +} diff --git a/src/bundles.rs b/src/bundles.rs index 8af59c0..2a69742 100644 --- a/src/bundles.rs +++ b/src/bundles.rs @@ -1,4 +1,4 @@ -use crate::{Controller, ControllerInput, ControllerPhysicsBundle, RapierPhysicsBundle}; +use crate::{Controller, ControllerInput, ControllerPhysicsBundle}; use bevy::prelude::*; @@ -14,8 +14,8 @@ pub struct ControllerBundle { /// See [`PhysicsBundle`] pub physics: ControllerPhysicsBundle, #[cfg(feature = "rapier")] - /// See [`RapierPhysicsBundle`] - pub rapier_physics: RapierPhysicsBundle, + /// See [`RapierPhysicsBundle`]. + pub rapier: crate::backend::RapierPhysicsBundle, /// See [`Transform`] pub transform: Transform, /// See [`GlobalTransform`] @@ -33,7 +33,7 @@ impl Default for ControllerBundle { input: default(), physics: default(), #[cfg(feature = "rapier")] - rapier_physics: default(), + rapier: default(), transform: default(), global_transform: default(), visibility: default(), diff --git a/src/controller/ground.rs b/src/controller/ground.rs index ca8b252..b3d17f9 100644 --- a/src/controller/ground.rs +++ b/src/controller/ground.rs @@ -319,12 +319,10 @@ pub fn find_ground( MassProperties::default() }; - let local_com = mass.local_center_of_mass; - - let ground_velocity = velocities + let (ground_linear_vel, ground_angular_vel) = velocities .get(ground_entity) - .copied() - .unwrap_or(Velocity::default()); + .map(|velocity| (velocity.linear(), velocity.angular())) + .unwrap_or((Vec3::ZERO, Vec3::ZERO)); let global = globals .get(ground_entity) diff --git a/src/controller/mod.rs b/src/controller/mod.rs index f96c7ed..d174068 100644 --- a/src/controller/mod.rs +++ b/src/controller/mod.rs @@ -7,8 +7,9 @@ mod input; mod movement; mod orientation; +use crate::backend::*; use crate::physics::*; -use crate::Spring; +use crate::spring::Spring; pub use {gravity::*, ground::*, input::*, movement::*, orientation::*}; diff --git a/src/controller/movement.rs b/src/controller/movement.rs index 7d13868..bbfe994 100644 --- a/src/controller/movement.rs +++ b/src/controller/movement.rs @@ -347,6 +347,7 @@ pub struct JumpForce { /// Calculate the jump force for the controller. pub fn jump_force( + physics_dt: Res, mut query: Query<( &mut JumpForce, &mut FloatForce, @@ -360,9 +361,8 @@ pub fn jump_force( &ControllerVelocity, &ControllerMass, )>, - ctx: Res, ) { - let dt = ctx.integration_parameters.dt; + let dt = **physics_dt; for ( mut force, mut float_force, diff --git a/src/controller/orientation.rs b/src/controller/orientation.rs index 0e7d3d1..be9b81a 100644 --- a/src/controller/orientation.rs +++ b/src/controller/orientation.rs @@ -1,5 +1,5 @@ use crate::controller::*; -use crate::SpringStrength; +use crate::spring::SpringStrength; /// Keeps the controller properly oriented in a floating state. #[derive(Component, Reflect)] @@ -64,8 +64,10 @@ pub fn float_force( let up_vector = gravity.up_vector; - let controller_point_velocity = - velocity.linear + velocity.angular.cross(Vec3::ZERO - mass.com); + let controller_point_velocity = velocity.linear + + velocity + .angular + .cross(Vec3::ZERO - mass.local_center_of_mass); let vel_align = up_vector.dot(controller_point_velocity); let ground_vel_align = up_vector.dot(ground.point_velocity); diff --git a/src/lib.rs b/src/lib.rs index 1b8ed9d..902ac7d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,10 +11,11 @@ mod physics; mod plugins; mod spring; -#[cfg(feature = "rapier")] -mod rapier; +pub mod backend; -pub use { - bundles::ControllerBundle, controller::*, physics::*, plugins::WanderlustPlugin, rapier::*, - spring::*, -}; +pub use backend::*; +pub use bundles::*; +pub use controller::*; +pub use physics::*; +pub use plugins::*; +pub use spring::*; diff --git a/src/physics.rs b/src/physics.rs index 237c3df..bf974a9 100644 --- a/src/physics.rs +++ b/src/physics.rs @@ -1,54 +1,57 @@ use bevy::prelude::*; -/// Mass/inertia properties for controller. -#[derive(Component, Clone, Default, Reflect)] -#[reflect(Component, Default)] -pub struct ControllerMass { - /// The mass of a character - pub mass: f32, - /// The rotational inertia of a character - pub inertia: Vec3, - /// The center of mass of a character - pub com: Vec3, -} +#[derive(Resource, Copy, Clone, Deref)] +pub struct PhysicsDeltaTime(pub f32); -/// Current velocity of the controller. +/// Force applied to the controller. #[derive(Copy, Clone, Component, Default, Reflect)] #[reflect(Component, Default)] -pub struct ControllerVelocity { - /// How fast this character is currently moving linearly in 3D space +pub struct ControllerForce { + /// Change in linear velocity. pub linear: Vec3, - /// How fast this character is currently moving angularly in 3D space + /// Change in angular velocity. pub angular: Vec3, } -/// Force applied to the controller. +/// Mass of the controller #[derive(Copy, Clone, Component, Default, Reflect)] #[reflect(Component, Default)] -pub struct ControllerForce { - /// Change in linear velocity. +pub struct ControllerMass { + /// Mass of the controller. + pub mass: f32, + /// Principal inertia of the controller. + pub inertia: Vec3, + /// Local center of mass of the controller. + pub local_center_of_mass: Vec3, +} + +/// Velocity of the controller +#[derive(Copy, Clone, Component, Default, Reflect)] +#[reflect(Component, Default)] +pub struct ControllerVelocity { + /// Linear velocity of the controller. pub linear: Vec3, - /// Change in angular velocity. + /// Angular velocity of the controller. pub angular: Vec3, } /// Components for computing forces/applying to physics engines. #[derive(Bundle)] pub struct ControllerPhysicsBundle { - /// Mass of the controller. - pub mass: ControllerMass, - /// Current velocity of the controller. - pub velocity: ControllerVelocity, /// Accumulated force of various controller constraints. pub force: ControllerForce, + /// Mass of the controller + pub mass: ControllerMass, + /// Velocity of the controller + pub velocity: ControllerVelocity, } impl Default for ControllerPhysicsBundle { fn default() -> Self { Self { + force: default(), mass: default(), velocity: default(), - force: default(), } } } diff --git a/src/plugins.rs b/src/plugins.rs index c00dec6..c350cc9 100644 --- a/src/plugins.rs +++ b/src/plugins.rs @@ -1,6 +1,5 @@ -use crate::controller::*; +use crate::*; use bevy::{ecs::schedule::ScheduleLabel, prelude::*, utils::HashSet}; -use bevy_rapier3d::prelude::*; /// The [character controller](CharacterController) plugin. Necessary to have the character controller /// work. @@ -48,6 +47,16 @@ impl Default for WanderlustPlugin { } } +#[derive(SystemSet, Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum WanderlustSet { + /// Sync anything between backends and such before computing forces. + Sync, + /// Compute forces for the controller. + Compute, + /// Apply forces for the controller. + Apply, +} + impl Plugin for WanderlustPlugin { fn build(&self, app: &mut App) { app.register_type::() @@ -76,16 +85,30 @@ impl Plugin for WanderlustPlugin { .register_type::() .register_type::>(); - if self.tweaks { - app.add_systems(Startup, setup_physics_context); - } + app.insert_resource(PhysicsDeltaTime(0.016)); if self.default_system_setup { + #[cfg(feature = "rapier")] + { + app.add_plugins(crate::backend::rapier::WanderlustRapierPlugin { + tweaks: self.tweaks, + schedule: self.schedule.clone(), + }); + }; + + app.configure_sets( + self.schedule.clone(), + ( + WanderlustSet::Sync, + WanderlustSet::Compute, + WanderlustSet::Apply, + ) + .chain(), + ); + app.add_systems( self.schedule.clone(), ( - crate::get_mass_from_rapier, - crate::get_velocity_from_rapier, find_ground, determine_groundedness, gravity_force, @@ -94,11 +117,9 @@ impl Plugin for WanderlustPlugin { upright_force, jump_force, accumulate_forces, - crate::apply_forces, - crate::apply_ground_forces, ) .chain() - .before(PhysicsSet::SyncBackend), + .in_set(WanderlustSet::Compute), ); }