~starkingdoms/starkingdoms

602773f491e862cd148636ce340fb1c34e059388 — core 17 days ago a85763a
feat: more predictable thrust solver behavior
1 files changed, 74 insertions(+), 26 deletions(-)

M crates/unified/src/client/ship/thrusters.rs
M crates/unified/src/client/ship/thrusters.rs => crates/unified/src/client/ship/thrusters.rs +74 -26
@@ 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::<Vec<_>>();

    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::<Vec<_>>();

    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;