From 602773f491e862cd148636ce340fb1c34e059388 Mon Sep 17 00:00:00 2001 From: core Date: Fri, 28 Nov 2025 21:28:01 -0500 Subject: [PATCH] feat: more predictable thrust solver behavior --- crates/unified/src/client/ship/thrusters.rs | 100 +++++++++++++++----- 1 file changed, 74 insertions(+), 26 deletions(-) diff --git a/crates/unified/src/client/ship/thrusters.rs b/crates/unified/src/client/ship/thrusters.rs index ea64c9172a4613a798c860f7ce2d72ad0c87d8ed..8ce72d50571f31baee4f774f8b9721a399c66285 100644 --- a/crates/unified/src/client/ship/thrusters.rs +++ b/crates/unified/src/client/ship/thrusters.rs @@ -91,9 +91,8 @@ fn solve_thrust( // +Z == counterclockwise/ccw // -Z == clockwise/cw - const MAGIC_TORQUE_SCALE_FACTOR: f32 = 100.0; // mystery units - let mut target_unit_vector = Vec3::ZERO; + let mut target_torque_vector = Vec3::ZERO; let mut anything_pressed = false; @@ -115,16 +114,14 @@ fn solve_thrust( } if input.pressed(&ClientAction::TorqueCw) { anything_pressed = true; - target_unit_vector += Vec3::new(0.0, 0.0, -1.0 / MAGIC_TORQUE_SCALE_FACTOR); + target_torque_vector += Vec3::new(0.0, 0.0, -1.0); } if input.pressed(&ClientAction::TorqueCcw) { anything_pressed = true; - target_unit_vector += Vec3::new(0.0, 0.0, 1.0 / MAGIC_TORQUE_SCALE_FACTOR); + target_torque_vector += Vec3::new(0.0, 0.0, 1.0); } - target_unit_vector = target_unit_vector.normalize(); - - if target_unit_vector == Vec3::ZERO || !anything_pressed { + if !anything_pressed { trace!("no buttons are pressed; zeroing thrust solution"); trace!("solved thrust in {}ms", start.elapsed().as_millis()); solution.converged = true; @@ -132,6 +129,13 @@ fn solve_thrust( return; } + if target_unit_vector != Vec3::ZERO { + target_unit_vector = target_unit_vector.normalize(); + } + if target_torque_vector != Vec3::ZERO { + target_torque_vector = target_torque_vector.normalize(); + } + let mut all_parts = vec![hearty]; if let Some(parts) = our_parts { all_parts.extend(parts.iter()); @@ -158,16 +162,27 @@ fn solve_thrust( // determine our rotational offset from hearty let relative_rotation = thruster_transform.rotation() * -hearty_transform.rotation(); - let thruster_torque = relative_translation.extend(0.0).cross(thruster_vector.extend(0.0)).z / MAGIC_TORQUE_SCALE_FACTOR; + let thruster_torque = relative_translation.normalize().extend(0.0).cross(thruster_vector.normalize().extend(0.0)).z; + let renormalized_thruster_torque = if thruster_torque.abs() < 0.1 { + 0.0 + } else if thruster_torque < 0.0 { + -1.0 + } else { + 1.0 + }; + + trace!("thruster: {:?} {}({})", thruster_vector, thruster_torque, renormalized_thruster_torque); // magically assemble the worldspace vector! for the solver (not shipspace) - let target_vector = thruster_vector.extend(thruster_torque); - all_thrusters.push((thruster_id, target_vector)); + all_thrusters.push((thruster_id, + thruster_vector.extend(0.0), + Vec3::new(0.0, 0.0, renormalized_thruster_torque) + )); } } - // calculate thrust and torque values + // calculate thrust ~~and torque~~ values trace!("found {} thrusters, computing coefficients", all_thrusters.len()); for thruster in &all_thrusters { @@ -176,34 +191,43 @@ fn solve_thrust( let coefficients = all_thrusters.iter() .map(|u| { - trace!("{} dot {}", target_unit_vector, u.1.normalize()); - target_unit_vector.dot(u.1.normalize()) + trace!("{} dot {}, {} dot {}", target_unit_vector, u.1.normalize(), target_torque_vector, u.2.normalize()); + ( + target_unit_vector.dot(u.1.normalize()), + target_torque_vector.dot(u.2.normalize()) + ) }) .map(|u| { - trace!("=> {u}"); + trace!("=> {}, {}", u.0, u.1); // improve reliability: // if dot is <0.1, zap it entirely (this thruster is not helping) // TODO: figure out how to make this adjustable - if u.abs() < 0.1 { - 0.0 + if u.0.abs() < 0.1 { + (0.0, u.1) } else { - u + (u.0, u.1) } }) //.map(|u| u.powi(3)) .collect::>(); - trace!("preparing model"); - let mut problem = Problem::new(OptimizationDirection::Maximize); + trace!("preparing models"); + let mut thrust_problem = Problem::new(OptimizationDirection::Maximize); + let mut torque_problem = Problem::new(OptimizationDirection::Maximize); // add variables to problem let variables = coefficients.iter() - .map(|u| problem.add_var(*u as f64, (0.0, 1.0))) + .map(|u| { + ( + thrust_problem.add_var(u.0 as f64, (0.0, 1.0)), + torque_problem.add_var(u.1 as f64, (0.0, 1.0)) + ) + }) .collect::>(); trace!("prepared {} variables; solving", variables.len()); - let ssolution = match problem.solve() { + let thrust_solution = match thrust_problem.solve() { Ok(soln) => soln, Err(e) => { match e { @@ -225,9 +249,32 @@ fn solve_thrust( } } }; + let torque_solution = match torque_problem.solve() { + Ok(soln) => soln, + Err(e) => { + match e { + microlp::Error::Infeasible => { + error!("failed to solve for torque: constraints cannot be satisfied"); + error!("failed to solve for torque after {}ms", start.elapsed().as_millis()); + return; + }, + microlp::Error::Unbounded => { + error!("failed to solve for torque: system is unbounded"); + error!("failed to solve for torque after {}ms", start.elapsed().as_millis()); + return; + }, + microlp::Error::InternalError(e) => { + error!("failed to solve for torque: solver encountered internal error: {e}"); + error!("failed to solve for torque after {}ms", start.elapsed().as_millis()); + return; + } + } + } + }; - trace!("found thrust solution!"); - trace!("solution alignment (higher is better): {}", ssolution.objective()); + trace!("found thrust+torque solution!"); + trace!("thrust solution alignment (higher is better): {}", thrust_solution.objective()); + trace!("torque solution alignment (higher is better): {}", torque_solution.objective()); let mut new_soln = ThrustSolution { thrusters_on: BTreeSet::default(), @@ -235,15 +282,16 @@ fn solve_thrust( }; for thruster in all_thrusters.iter().enumerate() { - trace!("solution: thruster #{} ({:?}): {} @ coeff {}", thruster.0, thruster.1.0, ssolution.var_value(variables[thruster.0]), coefficients[thruster.0]); + trace!("thrust solution: thruster #{} ({:?}): {} @ coeff {}", thruster.0, thruster.1.0, thrust_solution.var_value(variables[thruster.0].0), coefficients[thruster.0].0); + trace!("torque solution: thruster #{} ({:?}): {} @ coeff {}", thruster.0, thruster.1.0, torque_solution.var_value(variables[thruster.0].1), coefficients[thruster.0].1); // TODO: make this more easily adjustable - if *ssolution.var_value(variables[thruster.0]) > 0.8 { + if *thrust_solution.var_value(variables[thruster.0].0) > 0.8 || *torque_solution.var_value(variables[thruster.0].1) > 0.8 { new_soln.thrusters_on.insert(*thruster.1.0); } } let elapsed = start.elapsed(); - debug!(?elapsed, ?target_unit_vector, "solved for thrust"); + debug!(?elapsed, ?target_unit_vector, ?target_torque_vector, "solved for thrust and torque"); *solution = new_soln; events.write(solution.clone()); return;