~starkingdoms/starkingdoms

75f5204cc96240f4c96bb0205786462a6633b18c — core 22 days ago 7573667
feat: worky thrusters
M crates/unified/assets/config/parts/hearty.part.toml => crates/unified/assets/config/parts/hearty.part.toml +4 -4
@@ 11,22 11,22 @@ mass = 100
[[thruster]]
id = "bottom left"
apply_force_at_local = [ -25.0, -25.0 ]
thrust_vector = [ 0.0, 20.0 ]
thrust_vector = [ 0.0, 2500.0 ]

[[thruster]]
id = "bottom right"
apply_force_at_local = [ 25.0, -25.0 ]
thrust_vector = [ 0.0, 20.0 ]
thrust_vector = [ 0.0, 2500.0 ]

[[thruster]]
id = "top left"
apply_force_at_local = [ -25.0, 25.0 ]
thrust_vector = [ 0.0, -20.0 ]
thrust_vector = [ 0.0, -2500.0 ]

[[thruster]]
id = "top right"
apply_force_at_local = [ 25.0, 25.0 ]
thrust_vector = [ 0.0, -20.0 ]
thrust_vector = [ 0.0, -2500.0 ]

[[joint]]
id = "Top"

M crates/unified/src/client/ship/thrusters.rs => crates/unified/src/client/ship/thrusters.rs +14 -12
@@ 11,7 11,7 @@ use crate::client::input::ClientAction;
use crate::ecs::thruster::{PartThrusters, Thruster, ThrusterOfPart};
use crate::prelude::*;
use crate::client::input::util::ActionStateExt;
use crate::ecs::{Me, ThrustEvent};
use crate::ecs::Me;
use crate::thrust::ThrustSolution;

pub fn client_thrusters_plugin(app: &mut App) {


@@ 46,9 46,10 @@ fn draw_thruster_debug(
        if !thrust_solution.converged {
            color = RED;
        }
        let rescaled_thrust_vector = thruster.0.thrust_vector / 200.0;
        gizmos.arrow_2d(
            thruster.2.translation().xy(),
            thruster.2.translation().xy() + thruster.2.rotation().mul_vec3(thruster.0.thrust_vector.extend(0.0)).xy(),
            thruster.2.translation().xy() + thruster.2.rotation().mul_vec3(rescaled_thrust_vector.extend(0.0)).xy(),
            color
        );
    }


@@ 60,7 61,7 @@ fn solve_thrust(
    thrusters: Query<(&Thruster, &GlobalTransform)>,
    input: Res<ActionState<ClientAction>>,
    mut solution: ResMut<ThrustSolution>,
    mut events: MessageWriter<ThrustEvent>,
    mut events: MessageWriter<ThrustSolution>,
) {
    if !(
        input.button_changed(&ClientAction::ThrustForward)


@@ 71,7 72,7 @@ fn solve_thrust(
            || input.button_changed(&ClientAction::ThrustLeft)
    ) { return; /* no changes, existing thrust solution is valid */ }

    debug!("input changed, recalculating thrust solution");
    trace!("input changed, recalculating thrust solution");
    let start = Instant::now();
    solution.thrusters_on.clear();
    solution.converged = false;


@@ 111,10 112,10 @@ fn solve_thrust(
    }

    if target_unit_vector == Vec3::ZERO {
        debug!("no buttons are pressed; zeroing thrust solution");
        debug!("solved thrust in {}ms", start.elapsed().as_millis());
        trace!("no buttons are pressed; zeroing thrust solution");
        trace!("solved thrust in {}ms", start.elapsed().as_millis());
        solution.converged = true;
        events.write(ThrustEvent(solution.clone()));
        events.write(solution.clone());
        return;
    }



@@ 154,7 155,7 @@ fn solve_thrust(
    }

    // calculate thrust and torque values
    debug!("found {} thrusters, computing coefficients", all_thrusters.len());
    trace!("found {} thrusters, computing coefficients", all_thrusters.len());

    for thruster in &all_thrusters {
        trace!("thruster on ship: {:?}", thruster);


@@ 197,8 198,8 @@ fn solve_thrust(
        }
    };

    debug!("found thrust solution!");
    debug!("solution alignment (higher is better): {}", ssolution.objective());
    trace!("found thrust solution!");
    trace!("solution alignment (higher is better): {}", ssolution.objective());

    let mut new_soln = ThrustSolution {
        thrusters_on: BTreeSet::default(),


@@ 212,8 213,9 @@ fn solve_thrust(
        }
    }

    debug!("found thrust solution in {:?}", start.elapsed());
    let elapsed = start.elapsed();
    debug!(?elapsed, ?target_unit_vector, "solved for thrust");
    *solution = new_soln;
    events.write(ThrustEvent(solution.clone()));
    events.write(solution.clone());
    return;
}

M crates/unified/src/server/player/thrust.rs => crates/unified/src/server/player/thrust.rs +68 -35
@@ 1,23 1,36 @@
//! # Server thrust handling
//! The bulk of the actual thrust work is done in the thrust solver on the client.
//! It sends us it's `ThrustSolution` when it's done; in this file we process it
//! and apply it to the physics simulation.

use crate::attachment::Parts;
use crate::ecs::Part;
use crate::ecs::thruster::{PartThrusters, Thruster, ThrusterOfPart};
use crate::prelude::*;
use crate::server::ConnectedNetworkEntity;
use crate::thrust::ThrustSolution;

pub fn server_thrust_plugin(app: &mut App) {
    app
        .add_systems(Update, process_thrust_events);
        .add_systems(Update, process_thrust_events)
        .add_systems(Update, apply_thrust_solutions);
}

pub fn process_thrust_events(
/// Handle new `ThrustSolution`s from clients as they come in
fn process_thrust_events(
    mut events: MessageReader<FromClient<ThrustSolution>>,

    clients: Query<&ConnectedNetworkEntity>,
    q_ls_me: Query<Entity, With<crate::ecs::Me>>,
    mut commands: Commands
) {
    // For each event from a client...
    for FromClient {
        client_id,
        message: thrust_solution,
    } in events.read()
    {
        // Find the hearty entity of the player...
        let player_hearty_entity = match client_id {
            ClientId::Client(client_entity) => {
                let ConnectedNetworkEntity {


@@ 27,44 40,64 @@ pub fn process_thrust_events(
            },
            ClientId::Server => &q_ls_me.iter().next().unwrap()
        };
/
        commands.entity(player_hearty_entity).insert(thrust_solution);

        // and apply the new thrust solution
        commands.entity(*player_hearty_entity).insert(thrust_solution.clone());
        trace!("installed thrust solution {:?}", thrust_solution);
    }
}

        /* TODO: @tm85: have fun!
           TODO: The ThrustSolution contains a set of thrusters that should be on.
           TODO: All other thrusters should be off.
           TODO: If a thruster should be on, apply it's force vector at it's point
           TODO: (RigidBodyForces::apply_force_at_point,
           TODO: note this is worldspace!! [GlobalTransform is your friend])
/// Find all players, and apply their current `ThrustSolution`s
fn apply_thrust_solutions(
    players: Query<(Entity, &ThrustSolution, Option<&Parts>)>,
    thrusters: Query<(&Thruster, &ThrusterOfPart, &GlobalTransform)>,
    mut parts: Query<Forces, With<Part>>,
) {
    //gizmos.arrow_2d(
    //             thruster.2.translation().xy(),
    //             thruster.2.translation().xy() + thruster.2.rotation().mul_vec3(thruster.0.thrust_vector.extend(0.0)).xy(),
    //             color
    //         );

           TODO: Note that this is only an event handler, and will only run
           TODO: when the client sends us a NEW thrust solution.
    // Iterate through all players with a ThrustSolution
    for (player_entity, thrust_solution, maybe_parts) in players {
        // If their reported thrust solution didn't converge, do nothing
        if !thrust_solution.converged {
            debug!(?player_entity, "ignoring unconverged thrust solution");
        }
        // If it's empty, exit early (no reason to waste time)
        if thrust_solution.thrusters_on.is_empty() { continue }

           TODO: You probably want to add a Component to the player entity (player_hearty_entity,
           TODO: it's also the hearty part entity) that stores the current ThrustSolution
           TODO: and applies it every tick, since avian's ExternalForces only apply for a single
           TODO: physics tick and must be applied every tick to be continuous.
           TODO: Hint: you probably want a mut commands: Commands in this system,
           TODO: and then insert a component with commands.entity(hearty).insert(YourNewComponent)
        // Collect a list of all parts in the player's ship, to validate
        // the thrusters the player sends
        let attached_parts = if let Some(parts) = maybe_parts {
            parts.as_slice()
        } else {
            &[]
        };

           TODO: To do this, create a new system (e.g. apply_thrust) or something, and apply thrust
           TODO: there, every tick (Update). Register it with the plugin above.
        // Go over each active thruster in the thrust solution...
        for thruster_entity in &thrust_solution.thrusters_on {
            // ...load it's data...
            let Ok((thruster_info, parent_part, thruster_transform)) = thrusters.get(*thruster_entity) else {
                debug!(?thruster_entity, "couldn't find thruster to apply force");
                continue
            };

           TODO: I'll leave you to that. Have fun! ping me if you've got questions, I'll be up
           TODO: a while so can help you decipher the attachment system if need be.
            // ...verify this user is allowed to control this thruster...
            let parent_is_hearty = parent_part.0 == player_entity;
            let parent_is_in_ship = attached_parts.contains(&parent_part.0);
            if !(parent_is_hearty || parent_is_in_ship) {
                debug!(?thruster_entity, "ignoring disallowed thruster action");
                continue
            } // not permitted

           TODO: Overall, your goal is:
           TODO: - find all parts in the ship (hint: query for Parts on the ship (hearty))
           TODO: - for each part, find all thrusters (hint: query for PartThrusters on the part)
           TODO: - for each thruster, if it's on (check if it's in the player's current thrust soln)
           TODO: - apply a force at the correct point
           TODO: Much of the same logic is done in the client counterpart, to calculate effective
           TODO: force for the solver. Take a look at client::ship::thrusters.
           TODO: Note that relationship components (eg Parts, PartThrusters) wont exist if there are
           TODO: no children - eg hearty won't have Parts if nothing is attached, a part won't have
           TODO: PartThrusters if it has no thrusters.
           TODO: Handle this with Option<&Component> in the query
         */
            // great, it's valid; apply the force
            let mut part_forces = parts.get_mut(parent_part.0).unwrap();
            part_forces.apply_force_at_point(
                (thruster_transform.rotation() * thruster_info.thrust_vector.extend(0.0)).xy(),
                thruster_transform.translation().xy()
            );
        }
    }
}
}
\ No newline at end of file

M crates/unified/src/thrust.rs => crates/unified/src/thrust.rs +4 -2
@@ 1,11 1,13 @@
use std::collections::BTreeSet;
use bevy::prelude::{Entity, Resource, Compenent};
use bevy::ecs::entity::MapEntities;
use bevy::prelude::{Entity, Resource};
use serde::{Deserialize, Serialize};
use crate::prelude::{Component, Message};

/// A thrust solution, found by the thrust solver on the client.
/// `thrusters_on` is the set of thrusters that should be on.
/// Any thrusters not in this set should be off.
#[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone, Resource, Component, MapEntities)]
#[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone, Resource, Component, MapEntities, Message)]
pub struct ThrustSolution {
    #[entities]
    pub thrusters_on: BTreeSet<Entity>,