Crate unification (#88)

* move client/server/config into shared

* move platforms into shared

* move head drops into shared

* move tb_entities to shared

* reduce server to just a call into shared

* get solo play working

* fix server opening window

* fix fmt

* extracted a few more modules from client

* near completely migrated client

* fixed duplicate CharacterInputEnabled definition

* simplify a few things related to builds

* more simplifications

* fix warnings/check

* ci update

* address comments

* try fixing macos steam build

* address comments

* address comments

* CI tweaks with default client feature

---------

Co-authored-by: PROMETHIA-27 <electriccobras@gmail.com>
This commit is contained in:
extrawurst
2025-12-18 18:31:22 +01:00
committed by GitHub
parent c80129dac1
commit 7cfae285ed
100 changed files with 1099 additions and 1791 deletions

View File

@@ -0,0 +1,58 @@
use crate::{
GameState,
control::{ControllerSet, Inputs, LookDirMovement},
player::{LocalPlayer, PlayerBodyMesh},
};
use bevy::prelude::*;
use std::f32::consts::PI;
pub fn plugin(app: &mut App) {
app.add_systems(
FixedUpdate,
rotate_rig
.before(crate::control::controller_flying::apply_controls)
.in_set(ControllerSet::ApplyControlsFly)
.run_if(in_state(GameState::Playing)),
);
}
fn rotate_rig(
inputs: Single<&Inputs, With<LocalPlayer>>,
look_dir: Res<LookDirMovement>,
local_player: Single<&Children, With<LocalPlayer>>,
mut player_mesh: Query<&mut Transform, With<PlayerBodyMesh>>,
) {
if inputs.view_mode {
return;
}
local_player.iter().find(|&child| {
if let Ok(mut rig_transform) = player_mesh.get_mut(child) {
let look_dir = look_dir.0;
// todo: Make consistent with the running controller
let sensitivity = 0.001;
let max_pitch = 35.0 * PI / 180.0;
let min_pitch = -25.0 * PI / 180.0;
rig_transform.rotate_y(look_dir.x * -sensitivity);
let euler_rot = rig_transform.rotation.to_euler(EulerRot::YXZ);
let yaw = euler_rot.0;
let pitch = euler_rot.1 + look_dir.y * -sensitivity;
let pitch_clamped = pitch.clamp(min_pitch, max_pitch);
rig_transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch_clamped, 0.0);
// The following can be used to limit the amount of rotation per frame
// let target_rotation = rig_transform.rotation
// * Quat::from_rotation_x(controls.keyboard_state.look_dir.y * sensitivity);
// let clamped_rotation = rig_transform.rotation.rotate_towards(target_rotation, 0.01);
// rig_transform.rotation = clamped_rotation;
true
} else {
false
}
});
}

View File

@@ -0,0 +1,265 @@
use crate::{
GameState,
client::control::CharacterInputEnabled,
control::{
BackpackButtonPress, CashHealPressed, ClientInputs, ControllerSet, Inputs, LocalInputs,
LookDirMovement, SelectLeftPressed, SelectRightPressed,
},
player::{LocalPlayer, PlayerBodyMesh},
};
use bevy::{
input::{
gamepad::{GamepadConnection, GamepadEvent},
mouse::MouseMotion,
},
prelude::*,
};
use bevy_replicon::client::ClientSystems;
pub fn plugin(app: &mut App) {
app.add_systems(
PreUpdate,
(
gamepad_connections.run_if(on_message::<GamepadEvent>),
reset_lookdir,
keyboard_controls,
gamepad_controls,
mouse_rotate,
get_lookdir,
send_inputs,
)
.chain()
.in_set(ControllerSet::CollectInputs)
.before(ClientSystems::Receive)
.run_if(
in_state(GameState::Playing)
.and(resource_exists_and_equals(CharacterInputEnabled::On)),
),
);
// run this deliberately after local input processing ended
// TODO: can and should be ordered using a set to guarantee it gets send out ASAP but after local input processing
app.add_systems(
PreUpdate,
overwrite_local_inputs.after(ClientSystems::Receive).run_if(
in_state(GameState::Playing).and(resource_exists_and_equals(CharacterInputEnabled::On)),
),
);
app.add_systems(
Update,
reset_control_state_on_disable.run_if(in_state(GameState::Playing)),
);
}
/// Overwrite inputs for this client that were replicated from the server with the local inputs
fn overwrite_local_inputs(
mut inputs: Single<&mut Inputs, With<LocalPlayer>>,
local_inputs: Single<&LocalInputs>,
) {
**inputs = local_inputs.0;
}
/// Write inputs from combined keyboard/gamepad state into the networked input buffer
/// for the local player.
fn send_inputs(mut writer: MessageWriter<ClientInputs>, local_inputs: Single<&LocalInputs>) {
writer.write(ClientInputs(local_inputs.0));
}
fn reset_lookdir(mut look_dir: ResMut<LookDirMovement>) {
look_dir.0 = Vec2::ZERO;
}
/// Reset character inputs to default when character input is disabled.
fn reset_control_state_on_disable(
state: Res<CharacterInputEnabled>,
mut inputs: Single<&mut LocalInputs>,
) {
if state.is_changed() && matches!(*state, CharacterInputEnabled::Off) {
inputs.0 = Inputs {
look_dir: inputs.0.look_dir,
..default()
};
}
}
fn get_lookdir(
mut inputs: Single<&mut LocalInputs>,
rig_transform: Option<Single<&GlobalTransform, With<PlayerBodyMesh>>>,
) {
inputs.0.look_dir = if let Some(ref rig_transform) = rig_transform {
rig_transform.forward().as_vec3()
} else {
Vec3::NEG_Z
};
}
/// Applies a square deadzone to a Vec2
fn deadzone_square(v: Vec2, min: f32) -> Vec2 {
Vec2::new(
if v.x.abs() < min { 0. } else { v.x },
if v.y.abs() < min { 0. } else { v.y },
)
}
/// Collect gamepad inputs
#[allow(clippy::too_many_arguments)]
fn gamepad_controls(
gamepads: Query<&Gamepad>,
mut inputs: Single<&mut LocalInputs>,
mut look_dir: ResMut<LookDirMovement>,
mut backpack_inputs: MessageWriter<BackpackButtonPress>,
mut select_left_pressed: MessageWriter<SelectLeftPressed>,
mut select_right_pressed: MessageWriter<SelectRightPressed>,
mut cash_heal_pressed: MessageWriter<CashHealPressed>,
) {
let deadzone_left_stick = 0.15;
let deadzone_right_stick = 0.15;
for gamepad in gamepads.iter() {
let rotate = gamepad
.get(GamepadButton::RightTrigger2)
.unwrap_or_default();
// 8BitDo Ultimate wireless Controller for PC
look_dir.0 = if gamepad.vendor_id() == Some(11720) && gamepad.product_id() == Some(12306) {
const EPSILON: f32 = 0.015;
Vec2::new(
if rotate < 0.5 - EPSILON {
40. * (rotate - 0.5)
} else if rotate > 0.5 + EPSILON {
-40. * (rotate - 0.5)
} else {
0.
},
0.,
)
} else {
deadzone_square(gamepad.right_stick(), deadzone_right_stick) * 40.
};
let move_dir = deadzone_square(gamepad.left_stick(), deadzone_left_stick);
inputs.0.move_dir += move_dir.clamp_length_max(1.0);
inputs.0.jump |= gamepad.pressed(GamepadButton::South);
inputs.0.view_mode |= gamepad.pressed(GamepadButton::LeftTrigger2);
inputs.0.trigger |= gamepad.pressed(GamepadButton::RightTrigger2);
if gamepad.just_pressed(GamepadButton::DPadUp) {
backpack_inputs.write(BackpackButtonPress::Toggle);
}
if gamepad.just_pressed(GamepadButton::DPadDown) {
backpack_inputs.write(BackpackButtonPress::Swap);
}
if gamepad.just_pressed(GamepadButton::DPadLeft) {
backpack_inputs.write(BackpackButtonPress::Left);
}
if gamepad.just_pressed(GamepadButton::DPadRight) {
backpack_inputs.write(BackpackButtonPress::Right);
}
if gamepad.just_pressed(GamepadButton::LeftTrigger) {
select_left_pressed.write(SelectLeftPressed);
}
if gamepad.just_pressed(GamepadButton::RightTrigger) {
select_right_pressed.write(SelectRightPressed);
}
if gamepad.just_pressed(GamepadButton::East) {
cash_heal_pressed.write(CashHealPressed);
}
}
}
/// Collect mouse movement input
fn mouse_rotate(mut mouse: MessageReader<MouseMotion>, mut look_dir: ResMut<LookDirMovement>) {
for ev in mouse.read() {
look_dir.0 += ev.delta;
}
}
/// Collect keyboard input
#[allow(clippy::too_many_arguments)]
fn keyboard_controls(
keyboard: Res<ButtonInput<KeyCode>>,
mouse: Res<ButtonInput<MouseButton>>,
mut inputs: Single<&mut LocalInputs>,
mut backpack_inputs: MessageWriter<BackpackButtonPress>,
mut select_left_pressed: MessageWriter<SelectLeftPressed>,
mut select_right_pressed: MessageWriter<SelectRightPressed>,
mut cash_heal_pressed: MessageWriter<CashHealPressed>,
) {
let up_binds = [KeyCode::KeyW, KeyCode::ArrowUp];
let down_binds = [KeyCode::KeyS, KeyCode::ArrowDown];
let left_binds = [KeyCode::KeyA, KeyCode::ArrowLeft];
let right_binds = [KeyCode::KeyD, KeyCode::ArrowRight];
let up = keyboard.any_pressed(up_binds);
let down = keyboard.any_pressed(down_binds);
let left = keyboard.any_pressed(left_binds);
let right = keyboard.any_pressed(right_binds);
let horizontal = right as i8 - left as i8;
let vertical = up as i8 - down as i8;
let direction = Vec2::new(horizontal as f32, vertical as f32).clamp_length_max(1.0);
inputs.0.move_dir = direction;
inputs.0.jump = keyboard.pressed(KeyCode::Space);
inputs.0.view_mode = keyboard.pressed(KeyCode::Tab);
inputs.0.trigger = mouse.pressed(MouseButton::Left);
if keyboard.just_pressed(KeyCode::KeyB) {
backpack_inputs.write(BackpackButtonPress::Toggle);
}
if keyboard.just_pressed(KeyCode::Enter) {
backpack_inputs.write(BackpackButtonPress::Swap);
}
if keyboard.just_pressed(KeyCode::Comma) {
backpack_inputs.write(BackpackButtonPress::Left);
}
if keyboard.just_pressed(KeyCode::Period) {
backpack_inputs.write(BackpackButtonPress::Right);
}
if keyboard.just_pressed(KeyCode::KeyQ) {
select_left_pressed.write(SelectLeftPressed);
}
if keyboard.just_pressed(KeyCode::KeyE) {
select_right_pressed.write(SelectRightPressed);
}
if keyboard.just_pressed(KeyCode::Enter) {
cash_heal_pressed.write(CashHealPressed);
}
}
/// Receive gamepad connections and disconnections
fn gamepad_connections(mut evr_gamepad: MessageReader<GamepadEvent>) {
for ev in evr_gamepad.read() {
if let GamepadEvent::Connection(connection) = ev {
match &connection.connection {
GamepadConnection::Connected {
name,
vendor_id,
product_id,
} => {
info!(
"New gamepad connected: {:?}, name: {name}, vendor: {vendor_id:?}, product: {product_id:?}",
connection.gamepad,
);
}
GamepadConnection::Disconnected => {
info!("Lost connection with gamepad: {:?}", connection.gamepad);
}
}
}
}
}

View File

@@ -0,0 +1,25 @@
use crate::{GameState, control::ControllerSet};
use bevy::prelude::*;
use bevy_replicon::client::ClientSystems;
mod controller_flying;
pub mod controls;
#[derive(Resource, Debug, PartialEq, Eq)]
pub enum CharacterInputEnabled {
On,
Off,
}
pub fn plugin(app: &mut App) {
app.insert_resource(CharacterInputEnabled::On);
app.add_plugins((controller_flying::plugin, controls::plugin));
app.configure_sets(
PreUpdate,
ControllerSet::CollectInputs
.before(ClientSystems::Receive)
.run_if(in_state(GameState::Playing)),
);
}