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

@@ -1,214 +0,0 @@
mod backpack;
mod client;
mod config;
mod control;
mod debug;
mod enemy;
mod heal_effect;
mod player;
mod sounds;
mod steam;
mod ui;
use avian3d::prelude::*;
use bevy::{
audio::{PlaybackMode, Volume},
core_pipeline::tonemapping::Tonemapping,
prelude::*,
render::view::ColorGrading,
};
use bevy_common_assets::ron::RonAssetPlugin;
use bevy_sprite3d::Sprite3dPlugin;
use bevy_trenchbroom::prelude::*;
use bevy_trenchbroom_avian::AvianPhysicsBackend;
use camera::MainCamera;
use heads_database::HeadDatabaseAsset;
use loading_assets::AudioAssets;
use shared::*;
fn main() {
let mut app = App::new();
app.register_type::<DebugVisuals>()
.register_type::<TransformInterpolation>();
app.insert_resource(DebugVisuals {
unlit: false,
tonemapping: Tonemapping::None,
exposure: 1.,
shadows: true,
cam_follow: true,
});
app.add_plugins(
DefaultPlugins
.set(WindowPlugin {
primary_window: Some(Window {
title: "HEDZ Reloaded".into(),
// resolution: (1024., 768.).into(),
..default()
}),
..default()
})
.set(bevy::log::LogPlugin {
filter: "info,lightyear_replication=off,bevy_ecs::hierarchy=off".into(),
level: bevy::log::Level::INFO,
// provide custom log layer to receive logging events
custom_layer: bevy_debug_log::log_capture_layer,
..default()
}),
);
app.add_plugins(steam::plugin);
app.add_plugins(
bevy_debug_log::LogViewerPlugin::default()
.auto_open_threshold(bevy::log::tracing::level_filters::LevelFilter::OFF),
);
app.add_plugins(PhysicsPlugins::default());
app.add_plugins(Sprite3dPlugin);
app.add_plugins(TrenchBroomPlugins(
TrenchBroomConfig::new("hedz")
.icon(None)
.default_solid_spawn_hooks(|| SpawnHooks::new().convex_collider()),
));
app.add_plugins(TrenchBroomPhysicsPlugin::new(AvianPhysicsBackend));
app.add_plugins(RonAssetPlugin::<HeadDatabaseAsset>::new(&["headsdb.ron"]));
#[cfg(feature = "dbg")]
{
app.add_plugins(bevy_inspector_egui::bevy_egui::EguiPlugin::default());
app.add_plugins(bevy_inspector_egui::quick::WorldInspectorPlugin::new());
app.add_plugins(PhysicsDebugPlugin::default());
// app.add_plugins(bevy::pbr::wireframe::WireframePlugin)
// .insert_resource(bevy::pbr::wireframe::WireframeConfig {
// global: true,
// default_color: bevy::color::palettes::css::WHITE.into(),
// });
}
app.add_plugins(shared::ai::plugin);
app.add_plugins(shared::animation::plugin);
app.add_plugins(shared::character::plugin);
app.add_plugins(shared::cash::plugin);
app.add_plugins(shared::player::plugin);
app.add_plugins(shared::gates::plugin);
app.add_plugins(shared::platforms::plugin);
app.add_plugins(shared::movables::plugin);
app.add_plugins(shared::utils::billboards::plugin);
app.add_plugins(shared::aim::plugin);
app.add_plugins(shared::npc::plugin);
app.add_plugins(shared::keys::plugin);
app.add_plugins(shared::utils::squish_animation::plugin);
app.add_plugins(shared::cutscene::plugin);
app.add_plugins(shared::control::plugin);
app.add_plugins(shared::camera::plugin);
app.add_plugins(shared::backpack::plugin);
app.add_plugins(shared::loading_assets::LoadingPlugin);
app.add_plugins(shared::loading_map::plugin);
app.add_plugins(shared::utils::sprite_3d_animation::plugin);
app.add_plugins(shared::abilities::plugin);
app.add_plugins(shared::heads::plugin);
app.add_plugins(shared::hitpoints::plugin);
app.add_plugins(shared::cash_heal::plugin);
app.add_plugins(shared::utils::plugin);
app.add_plugins(shared::water::plugin);
app.add_plugins(shared::head_drop::plugin);
app.add_plugins(shared::utils::trail::plugin);
app.add_plugins(shared::utils::auto_rotate::plugin);
app.add_plugins(shared::tb_entities::plugin);
app.add_plugins(shared::tick::plugin);
app.add_plugins(shared::utils::explosions::plugin);
// Networking
// The client/server plugin must go before the protocol, or else `ProtocolHasher` will not be available.
app.add_plugins(client::plugin);
app.add_plugins(shared::protocol::plugin);
app.add_plugins(backpack::plugin);
app.add_plugins(config::plugin);
app.add_plugins(control::plugin);
app.add_plugins(debug::plugin);
app.add_plugins(enemy::plugin);
app.add_plugins(heal_effect::plugin);
app.add_plugins(player::plugin);
app.add_plugins(sounds::plugin);
app.add_plugins(ui::plugin);
app.init_state::<GameState>();
app.insert_resource(AmbientLight {
color: Color::WHITE,
brightness: 400.,
..Default::default()
});
app.insert_resource(ClearColor(Color::BLACK));
//TODO: let user control this
app.insert_resource(GlobalVolume::new(Volume::Linear(0.4)));
app.add_systems(Startup, write_trenchbroom_config);
app.add_systems(OnEnter(GameState::Playing), music);
app.add_systems(Update, (set_materials_unlit, set_tonemapping, set_shadows));
app.run();
}
fn music(assets: Res<AudioAssets>, mut commands: Commands) {
commands.spawn((
Name::new("sfx-music"),
AudioPlayer::new(assets.music.clone()),
PlaybackSettings {
mode: PlaybackMode::Loop,
volume: Volume::Linear(0.6),
..default()
},
));
commands.spawn((
Name::new("sfx-ambient"),
AudioPlayer::new(assets.ambient.clone()),
PlaybackSettings {
mode: PlaybackMode::Loop,
volume: Volume::Linear(0.8),
..default()
},
));
}
fn write_trenchbroom_config(server: Res<TrenchBroomServer>, type_registry: Res<AppTypeRegistry>) {
if let Err(e) = server
.config
.write_game_config("trenchbroom/hedz", &type_registry.read())
{
warn!("Failed to write trenchbroom config: {}", e);
}
}
fn set_tonemapping(
mut cams: Query<(&mut Tonemapping, &mut ColorGrading), With<MainCamera>>,
visuals: Res<DebugVisuals>,
) {
for (mut tm, mut color) in cams.iter_mut() {
*tm = visuals.tonemapping;
color.global.exposure = visuals.exposure;
}
}
fn set_materials_unlit(
mut materials: ResMut<Assets<StandardMaterial>>,
visuals: Res<DebugVisuals>,
) {
if !materials.is_changed() {
return;
}
for (_, material) in materials.iter_mut() {
material.unlit = visuals.unlit;
}
}
fn set_shadows(mut lights: Query<&mut DirectionalLight>, visuals: Res<DebugVisuals>) {
for mut l in lights.iter_mut() {
l.shadows_enabled = visuals.shadows;
}
}

View File

@@ -4,27 +4,37 @@ version = "0.1.0"
edition = "2024"
build = "build.rs"
[[bin]]
name = "hedz_reloaded_server"
[features]
default = ["shared/client"]
dbg = ["avian3d/debug-plugin", "dep:bevy-inspector-egui", "shared/dbg"]
default = ["client"]
client = [
"bevy/bevy_audio",
"bevy/bevy_window",
# depend on `winit`
"bevy/bevy_winit",
"bevy/x11",
"bevy/custom_cursor",
"bevy_replicon/client",
"bevy_replicon_renet/client",
"bevy_trenchbroom/client",
]
dbg = ["avian3d/debug-plugin", "dep:bevy-inspector-egui"]
[dependencies]
avian3d = { workspace = true }
bevy = { workspace = true, default-features = false, features = [
"bevy_audio",
"bevy_window",
"bevy_winit",
] }
bevy = { workspace = true }
bevy-inspector-egui = { workspace = true, optional = true }
bevy-steamworks = { workspace = true }
bevy_asset_loader = { workspace = true }
bevy_ballistic = { workspace = true }
bevy_common_assets = { workspace = true }
bevy_debug_log = { workspace = true }
bevy_replicon = { workspace = true, features = ["client"] }
bevy_replicon_renet = { workspace = true, features = ["client"] }
bevy_replicon = { workspace = true }
bevy_replicon_renet = { workspace = true }
bevy_sprite3d = { workspace = true }
bevy_trenchbroom = { workspace = true, features = ["client"] }
bevy_trenchbroom = { workspace = true }
bevy_trenchbroom_avian = { workspace = true }
clap = { workspace = true }
happy_feet = { workspace = true }
@@ -32,8 +42,11 @@ nil = { workspace = true }
rand = { workspace = true }
ron = { workspace = true }
serde = { workspace = true }
shared = { workspace = true }
steamworks = { workspace = true }
[build-dependencies]
vergen-gitcl = "1.0"
[lints.clippy]
too_many_arguments = "allow"
type_complexity = "allow"

View File

@@ -6,17 +6,19 @@ pub mod missile;
pub mod thrown;
use crate::{
GameState, global_observer,
GameState,
aim::AimTarget,
character::CharacterHierarchy,
control::Inputs,
global_observer,
heads::ActiveHeads,
heads_database::HeadsDatabase,
loading_assets::GameAssets,
physics_layers::GameLayer,
player::Player,
protocol::PlaySound,
utils::{billboards::Billboard, explosions::Explosion, sprite_3d_animation::AnimationTimer},
};
use crate::{
aim::AimTarget, character::CharacterHierarchy, control::Inputs, heads::ActiveHeads,
heads_database::HeadsDatabase,
};
use bevy::{light::NotShadowCaster, prelude::*};
use bevy_replicon::prelude::{SendMode, ServerTriggerExt, Signature, ToClients};
use bevy_sprite3d::Sprite3d;

View File

@@ -0,0 +1,3 @@
pub fn main() {
hedz_reloaded::launch();
}

View File

@@ -1,6 +1,7 @@
use crate::{GameState, HEDZ_GREEN, loading_assets::UIAssets};
#[cfg(feature = "server")]
use crate::{global_observer, protocol::PlaySound};
use crate::{
GameState, HEDZ_GREEN, global_observer, loading_assets::UIAssets, protocol::PlaySound,
server_observer,
};
use avian3d::prelude::Rotation;
use bevy::prelude::*;
use serde::{Deserialize, Serialize};
@@ -29,11 +30,9 @@ pub fn plugin(app: &mut App) {
(rotate, update_ui).run_if(in_state(GameState::Playing)),
);
#[cfg(feature = "server")]
global_observer!(app, on_cash_collect);
server_observer!(app, on_cash_collect);
}
#[cfg(feature = "server")]
fn on_cash_collect(
_trigger: On<CashCollectEvent>,
mut commands: Commands,

View File

@@ -1,55 +1,60 @@
use crate::config::ClientConfig;
use avian3d::prelude::{
Collider, ColliderAabb, ColliderDensity, ColliderMarker, ColliderOf, ColliderTransform,
CollisionEventsEnabled, CollisionLayers, Sensor,
};
use bevy::{
ecs::bundle::BundleFromComponents, platform::cell::SyncCell, prelude::*, scene::SceneInstance,
};
use bevy_replicon::{
RepliconPlugins,
client::{ClientSystems, confirm_history::ConfirmHistory},
prelude::{ClientState, ClientTriggerExt, RepliconChannels},
};
use bevy_replicon_renet::{
RenetChannelsExt, RepliconRenetPlugins,
netcode::{ClientAuthentication, NetcodeClientTransport, NetcodeError},
renet::{ConnectionConfig, RenetClient},
};
use bevy_trenchbroom::geometry::Brushes;
use nil::prelude::Mutex;
use shared::{
GameState, global_observer,
use crate::{
GameState,
config::NetworkingConfig,
protocol::{
ClientEnteredPlaying, TbMapEntityId, TbMapEntityMapping, messages::DespawnTbMapEntity,
},
tb_entities::{Movable, Platform, PlatformTarget},
};
use avian3d::prelude::{
Collider, ColliderAabb, ColliderDensity, ColliderMarker, ColliderOf, ColliderTransform,
CollisionEventsEnabled, CollisionLayers, Sensor,
};
use bevy::{ecs::bundle::BundleFromComponents, prelude::*, scene::SceneInstance};
use bevy_replicon::{
client::{ClientSystems, confirm_history::ConfirmHistory},
prelude::{ClientState, ClientTriggerExt, RepliconChannels},
};
use bevy_replicon_renet::{
RenetChannelsExt,
netcode::{ClientAuthentication, NetcodeClientTransport, NetcodeError},
renet::{ConnectionConfig, RenetClient},
};
use bevy_trenchbroom::geometry::Brushes;
use std::{
env::current_exe,
fs::File,
io::{BufRead, BufReader},
net::{Ipv4Addr, UdpSocket},
process::Stdio,
sync::{LazyLock, mpsc},
time::SystemTime,
};
/// Cache of server processes to be cleared at process exit
static SERVER_PROCESSES: LazyLock<Mutex<Vec<std::process::Child>>> = LazyLock::new(Mutex::default);
pub mod backpack;
pub mod control;
pub mod debug;
pub mod enemy;
pub mod heal_effect;
pub mod player;
pub mod setup;
pub mod sounds;
pub mod steam;
pub mod ui;
pub fn plugin(app: &mut App) {
app.add_plugins((RepliconPlugins, RepliconRenetPlugins));
app.add_plugins((
backpack::plugin,
control::plugin,
debug::plugin,
enemy::plugin,
heal_effect::plugin,
player::plugin,
setup::plugin,
sounds::plugin,
steam::plugin,
ui::plugin,
));
app.add_systems(
OnEnter(GameState::Connecting),
connect_to_server.run_if(|config: Res<ClientConfig>| config.server.is_some()),
connect_to_server.run_if(|config: Res<NetworkingConfig>| config.server.is_some()),
);
app.add_systems(
Update,
parse_local_server_stdout.run_if(resource_exists::<LocalServerStdout>),
);
app.add_systems(Last, close_server_processes);
app.add_systems(Update, despawn_absent_map_entities);
app.add_systems(
PreUpdate,
@@ -58,15 +63,8 @@ pub fn plugin(app: &mut App) {
.after(ClientSystems::Receive),
);
// TODO: migrate this to connect_on_local_server_started
app.add_systems(OnEnter(ClientState::Connected), on_connected_state);
app.add_systems(OnExit(ClientState::Connected), on_disconnect);
app.add_systems(
OnEnter(GameState::Connecting),
host_local_server.run_if(|config: Res<ClientConfig>| config.server.is_none()),
);
global_observer!(app, connect_on_local_server_started);
}
//
@@ -74,6 +72,7 @@ pub fn plugin(app: &mut App) {
//
fn on_connected_state(mut commands: Commands, mut game_state: ResMut<NextState<GameState>>) {
info!("sent entered playing signal");
commands.client_trigger(ClientEnteredPlaying);
game_state.set(GameState::Playing);
}
@@ -82,24 +81,13 @@ fn on_disconnect() {
info!("disconnected from the server");
}
fn close_server_processes(mut app_exit: MessageReader<AppExit>) {
if app_exit.read().next().is_some() {
let mut lock = SERVER_PROCESSES.lock();
for mut process in lock.drain(..) {
if let Err(err) = process.wait() {
error!("{err}");
}
}
}
}
//
// Renet
//
fn connect_to_server(
mut commands: Commands,
config: Res<ClientConfig>,
config: Res<NetworkingConfig>,
channels: Res<RepliconChannels>,
) -> Result {
let server_channels_config = channels.server_configs();
@@ -117,12 +105,12 @@ fn connect_to_server(
Ok(())
}
fn client_transport(config: &ClientConfig) -> Result<NetcodeClientTransport, NetcodeError> {
fn client_transport(config: &NetworkingConfig) -> Result<NetcodeClientTransport, NetcodeError> {
let current_time = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap();
let client_id = current_time.as_millis() as u64;
let socket = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, config.port))?;
let socket = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0))?;
let server_addr = config
.server
.flatten()
@@ -138,76 +126,6 @@ fn client_transport(config: &ClientConfig) -> Result<NetcodeClientTransport, Net
NetcodeClientTransport::new(current_time, authentication, socket)
}
#[derive(Resource)]
struct LocalServerStdout(SyncCell<mpsc::Receiver<String>>);
fn host_local_server(mut commands: Commands) -> Result {
// the server executable is assumed to be adjacent to the client executable
let mut exe_path = current_exe().expect("failed to get path of client executable");
exe_path.set_file_name("server");
let server_log_file = File::create("server.log")?;
let mut server_process = std::process::Command::new(exe_path)
.args(["--timeout", "60", "--close-on-client-disconnect"])
.env("NO_COLOR", "1")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(server_log_file)
.spawn()
.expect("failed to start server");
let server_stdout = server_process.stdout.take().unwrap();
SERVER_PROCESSES.lock().push(server_process);
let (tx, rx) = std::sync::mpsc::channel();
let stdout = BufReader::new(server_stdout).lines();
std::thread::spawn(move || {
for line in stdout {
match line {
Ok(line) => {
tx.send(line).unwrap();
}
Err(error) => {
error!("error reading local server stdout: `{error}`");
}
}
}
});
commands.insert_resource(LocalServerStdout(SyncCell::new(rx)));
Ok(())
}
#[derive(Event)]
struct LocalServerStarted;
fn parse_local_server_stdout(mut commands: Commands, mut stdout: ResMut<LocalServerStdout>) {
let stdout: &mut LocalServerStdout = &mut stdout;
while let Ok(line) = stdout.0.get().try_recv() {
if let "hedz.server_started" = &line[..] {
commands.trigger(LocalServerStarted);
} else {
info!("SERVER: {line}");
}
}
}
fn connect_on_local_server_started(
_: On<LocalServerStarted>,
commands: Commands,
state: Res<State<GameState>>,
channels: Res<RepliconChannels>,
config: Res<ClientConfig>,
) -> Result<()> {
if *state == GameState::Connecting {
connect_to_server(commands, config, channels)?;
}
Ok(())
}
#[allow(clippy::type_complexity)]
fn migrate_remote_entities(
query: Query<(Entity, &TbMapEntityId), (Added<TbMapEntityId>, With<ConfirmHistory>)>,

View File

@@ -1,9 +1,13 @@
use crate::{GameState, HEDZ_GREEN, heads::HeadsImages, loading_assets::UIAssets};
use bevy::{ecs::spawn::SpawnIter, prelude::*};
use shared::backpack::backpack_ui::{
BACKPACK_HEAD_SLOTS, BackpackCountText, BackpackMarker, BackpackUiState, HeadDamage, HeadImage,
HeadSelector,
use crate::{
GameState, HEDZ_GREEN,
backpack::backpack_ui::{
BACKPACK_HEAD_SLOTS, BackpackCountText, BackpackMarker, BackpackUiState, HeadDamage,
HeadImage, HeadSelector,
},
heads::HeadsImages,
loading_assets::UIAssets,
};
use bevy::{ecs::spawn::SpawnIter, prelude::*};
pub fn plugin(app: &mut App) {
app.add_systems(OnEnter(GameState::Playing), setup);

View File

@@ -1,16 +1,16 @@
use bevy::prelude::*;
use shared::{
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(shared::control::controller_flying::apply_controls)
.before(crate::control::controller_flying::apply_controls)
.in_set(ControllerSet::ApplyControlsFly)
.run_if(in_state(GameState::Playing)),
);

View File

@@ -1,4 +1,12 @@
use crate::{GameState, control::CharacterInputEnabled};
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},
@@ -7,13 +15,6 @@ use bevy::{
prelude::*,
};
use bevy_replicon::client::ClientSystems;
use shared::{
control::{
BackpackButtonPress, CashHealPressed, ClientInputs, ControllerSet, Inputs, LocalInputs,
LookDirMovement, SelectLeftPressed, SelectRightPressed,
},
player::{LocalPlayer, PlayerBodyMesh},
};
pub fn plugin(app: &mut App) {
app.add_systems(

View File

@@ -1,7 +1,6 @@
use crate::GameState;
use crate::{GameState, control::ControllerSet};
use bevy::prelude::*;
use bevy_replicon::client::ClientSystems;
use shared::control::ControllerSet;
mod controller_flying;
pub mod controls;

View File

@@ -1,6 +1,7 @@
use bevy::prelude::*;
use bevy_debug_log::LogViewerVisibility;
// Is supplied by a build script via vergen_gitcl
pub const GIT_HASH: &str = env!("VERGEN_GIT_SHA");
pub fn plugin(app: &mut App) {

View File

@@ -1,8 +1,8 @@
use crate::{GameState, tb_entities::EnemySpawn};
use bevy::prelude::*;
use shared::{GameState, tb_entities::EnemySpawn};
pub fn plugin(app: &mut App) {
app.add_systems(OnExit(GameState::MapLoading), despawn_enemy_spawns);
app.add_systems(OnEnter(GameState::Connecting), despawn_enemy_spawns);
}
/// Despawn enemy spawners because only the server will ever spawn enemies with them, and they have a

View File

@@ -1,9 +1,11 @@
use crate::{
GameState, abilities::Healing, loading_assets::GameAssets, utils::billboards::Billboard,
GameState,
abilities::Healing,
loading_assets::{AudioAssets, GameAssets},
utils::{billboards::Billboard, observers::global_observer},
};
use bevy::prelude::*;
use rand::{Rng, thread_rng};
use shared::{loading_assets::AudioAssets, utils::observers::global_observer};
// Should not be a relationship because lightyear will silently track state for all relationships
// and break if one end of the relationship isn't replicated and is despawned

View File

@@ -2,12 +2,10 @@ use crate::{
global_observer,
heads_database::{HeadControls, HeadsDatabase},
loading_assets::AudioAssets,
player::{LocalPlayer, PlayerBodyMesh},
protocol::{ClientHeadChanged, PlaySound, PlayerId, messages::AssignClientPlayer},
};
use bevy::prelude::*;
use shared::{
player::{LocalPlayer, PlayerBodyMesh},
protocol::{PlaySound, PlayerId, events::ClientHeadChanged, messages::AssignClientPlayer},
};
pub fn plugin(app: &mut App) {
app.init_state::<PlayerAssignmentState>();
@@ -17,10 +15,10 @@ pub fn plugin(app: &mut App) {
receive_player_id.run_if(not(in_state(PlayerAssignmentState::Confirmed))),
);
global_observer!(app, on_update_head_mesh);
global_observer!(app, on_client_update_head_mesh);
}
fn receive_player_id(
pub fn receive_player_id(
mut commands: Commands,
mut client_assignments: MessageReader<AssignClientPlayer>,
mut next: ResMut<NextState<PlayerAssignmentState>>,
@@ -61,7 +59,7 @@ pub enum PlayerAssignmentState {
Confirmed,
}
fn on_update_head_mesh(
fn on_client_update_head_mesh(
trigger: On<ClientHeadChanged>,
mut commands: Commands,
body_mesh: Single<(Entity, &Children), With<PlayerBodyMesh>>,

View File

@@ -0,0 +1,90 @@
use crate::{DebugVisuals, GameState, camera::MainCamera, loading_assets::AudioAssets};
use bevy::{
audio::{PlaybackMode, Volume},
core_pipeline::tonemapping::Tonemapping,
prelude::*,
render::view::ColorGrading,
};
use bevy_trenchbroom::TrenchBroomServer;
pub fn plugin(app: &mut App) {
#[cfg(feature = "dbg")]
{
app.add_plugins(bevy_inspector_egui::bevy_egui::EguiPlugin::default());
app.add_plugins(bevy_inspector_egui::quick::WorldInspectorPlugin::new());
app.add_plugins(avian3d::prelude::PhysicsDebugPlugin::default());
}
app.insert_resource(AmbientLight {
color: Color::WHITE,
brightness: 400.,
..Default::default()
});
app.insert_resource(ClearColor(Color::BLACK));
//TODO: let user control this
app.insert_resource(GlobalVolume::new(Volume::Linear(0.4)));
app.add_systems(Startup, write_trenchbroom_config);
app.add_systems(OnEnter(GameState::Playing), music);
app.add_systems(Update, (set_materials_unlit, set_tonemapping, set_shadows));
}
fn music(assets: Res<AudioAssets>, mut commands: Commands) {
commands.spawn((
Name::new("sfx-music"),
AudioPlayer::new(assets.music.clone()),
PlaybackSettings {
mode: PlaybackMode::Loop,
volume: Volume::Linear(0.6),
..default()
},
));
commands.spawn((
Name::new("sfx-ambient"),
AudioPlayer::new(assets.ambient.clone()),
PlaybackSettings {
mode: PlaybackMode::Loop,
volume: Volume::Linear(0.8),
..default()
},
));
}
fn write_trenchbroom_config(server: Res<TrenchBroomServer>, type_registry: Res<AppTypeRegistry>) {
if let Err(e) = server
.config
.write_game_config("trenchbroom/hedz", &type_registry.read())
{
warn!("Failed to write trenchbroom config: {}", e);
}
}
fn set_tonemapping(
mut cams: Query<(&mut Tonemapping, &mut ColorGrading), With<MainCamera>>,
visuals: Res<DebugVisuals>,
) {
for (mut tm, mut color) in cams.iter_mut() {
*tm = visuals.tonemapping;
color.global.exposure = visuals.exposure;
}
}
fn set_materials_unlit(
mut materials: ResMut<Assets<StandardMaterial>>,
visuals: Res<DebugVisuals>,
) {
if !materials.is_changed() {
return;
}
for (_, material) in materials.iter_mut() {
material.unlit = visuals.unlit;
}
}
fn set_shadows(mut lights: Query<&mut DirectionalLight>, visuals: Res<DebugVisuals>) {
for mut l in lights.iter_mut() {
l.shadows_enabled = visuals.shadows;
}
}

View File

@@ -1,6 +1,5 @@
use crate::{global_observer, loading_assets::AudioAssets};
use crate::{global_observer, loading_assets::AudioAssets, protocol::PlaySound};
use bevy::prelude::*;
use shared::protocol::PlaySound;
pub fn plugin(app: &mut App) {
global_observer!(app, on_spawn_sounds);

View File

@@ -1,9 +1,28 @@
use bevy::prelude::*;
use bevy_steamworks::{Client, FriendFlags, SteamworksEvent};
use bevy_steamworks::{Client, FriendFlags, SteamworksEvent, SteamworksPlugin};
use std::io::{Read, Write};
pub fn plugin(app: &mut App) {
app.add_plugins(shared::steam::plugin);
let app_id = 1603000;
// should only be done in production builds
#[cfg(not(debug_assertions))]
if steamworks::restart_app_if_necessary(app_id.into()) {
info!("Restarting app via steam");
return;
}
info!("steam app init: {app_id}");
match SteamworksPlugin::init_app(app_id) {
Ok(plugin) => {
info!("steam app init done");
app.add_plugins(plugin);
}
Err(e) => {
warn!("steam init error: {e:?}");
}
};
app.add_systems(
Startup,

View File

@@ -1,6 +1,8 @@
use crate::{GameState, control::CharacterInputEnabled};
use crate::{
GameState, HEDZ_GREEN, HEDZ_PURPLE, client::control::CharacterInputEnabled,
loading_assets::UIAssets,
};
use bevy::{color::palettes::css::BLACK, prelude::*};
use shared::{HEDZ_GREEN, HEDZ_PURPLE, loading_assets::UIAssets};
#[derive(States, Default, Clone, Eq, PartialEq, Debug, Hash)]
#[states(scoped_entities)]

View File

@@ -3,21 +3,23 @@ use clap::Parser;
use std::net::SocketAddr;
pub fn plugin(app: &mut App) {
let config = ClientConfig::parse();
let config = NetworkingConfig::parse();
app.insert_resource(config);
}
#[derive(Resource, Parser, Debug)]
#[command(version, about, long_about = None)]
pub struct ClientConfig {
/// The port to use when connecting.
#[arg(long, default_value_t = 0)]
pub port: u16,
pub struct NetworkingConfig {
/// The IP/port to connect to.
/// If `None`, host a local server.
/// If Some(None), connect to the default server (`127.0.0.1:31111`)
/// Otherwise, connect to the given server
/// Otherwise, connect to the given server.
/// Does nothing on the server.
#[arg(long)]
pub server: Option<Option<SocketAddr>>,
/// Whether or not to open a port when opening the client, for other clients
/// to connect. Does nothing if `server` is set.
#[arg(long)]
pub host: bool,
}

View File

@@ -2,6 +2,7 @@ use crate::{
GameState,
animation::AnimationFlags,
control::{ControllerSet, ControllerSettings, Inputs, controller_common::MovementSpeedFactor},
protocol::is_server,
};
#[cfg(feature = "client")]
use crate::{
@@ -9,7 +10,6 @@ use crate::{
player::{LocalPlayer, PlayerBodyMesh},
};
use bevy::prelude::*;
use bevy_replicon::prelude::ClientState;
use happy_feet::prelude::{Grounding, KinematicVelocity, MoveInput};
pub struct CharacterControllerPlugin;
@@ -28,8 +28,7 @@ impl Plugin for CharacterControllerPlugin {
FixedUpdate,
apply_controls
.in_set(ControllerSet::ApplyControlsRun)
.run_if(in_state(GameState::Playing))
.run_if(in_state(ClientState::Disconnected)),
.run_if(in_state(GameState::Playing).and(is_server)),
);
}
}
@@ -82,10 +81,8 @@ fn apply_controls(
move_input.set(direction * move_factor.0);
if inputs.jump && grounding.is_grounded() {
if cfg!(feature = "server") {
flags.jumping = true;
flags.jump_count += 1;
}
flags.jumping = true;
flags.jump_count += 1;
happy_feet::movement::jump(settings.jump_force, &mut velocity, &mut grounding, Dir3::Y)
}
}

View File

@@ -3,13 +3,10 @@ use crate::{
head::ActiveHead,
heads_database::{HeadControls, HeadsDatabase},
player::Player,
protocol::ClientToController,
protocol::{ClientToController, is_server},
};
use bevy::{ecs::entity::MapEntities, prelude::*};
use bevy_replicon::{
client::ClientSystems,
prelude::{ClientState, FromClient},
};
use bevy_replicon::{client::ClientSystems, prelude::FromClient};
use serde::{Deserialize, Serialize};
pub mod controller_common;
@@ -61,7 +58,7 @@ pub fn plugin(app: &mut App) {
app.add_systems(
PreUpdate,
collect_player_inputs
.run_if(in_state(ClientState::Disconnected).and(in_state(GameState::Playing)))
.run_if(is_server.and(in_state(GameState::Playing)))
.after(ClientSystems::Receive),
);
app.add_systems(Update, head_change.run_if(in_state(GameState::Playing)));
@@ -128,12 +125,6 @@ pub struct CashHealPressed;
#[reflect(Resource)]
pub struct LookDirMovement(pub Vec2);
#[derive(Resource, Debug, PartialEq, Eq)]
pub enum CharacterInputEnabled {
On,
Off,
}
#[derive(Component, Clone, PartialEq, Reflect, Serialize, Deserialize)]
#[reflect(Component)]
pub struct ControllerSettings {
@@ -154,7 +145,9 @@ fn collect_player_inputs(
) {
for msg in input_messages.read() {
let player = clients.get_controller(msg.client_id);
let mut inputs = players.get_mut(player).unwrap();
let Ok(mut inputs) = players.get_mut(player) else {
continue;
};
*inputs = msg.message.0;
}

View File

@@ -1,24 +1,108 @@
use avian3d::prelude::*;
use bevy::{
ecs::{relationship::RelatedSpawner, spawn::SpawnWith},
prelude::*,
};
use bevy_replicon::prelude::{Replicated, SendMode, ServerTriggerExt, ToClients};
use shared::{
global_observer,
head_drop::{HeadCollected, HeadDrop, HeadDropEnableTime, HeadDrops, SecretHeadMarker},
use crate::{
GameState, global_observer,
heads_database::HeadsDatabase,
physics_layers::GameLayer,
player::Player,
protocol::{GltfSceneRoot, PlaySound},
protocol::{GltfSceneRoot, NetworkEnv, PlaySound},
server_observer,
tb_entities::SecretHead,
utils::{
billboards::Billboard, one_shot_force::OneShotImpulse, squish_animation::SquishAnimation,
},
};
use avian3d::prelude::*;
use bevy::{ecs::relationship::RelatedSpawner, prelude::*};
use bevy_replicon::prelude::{Replicated, SendMode, ServerTriggerExt, ToClients};
use std::f32::consts::PI;
#[derive(Event, Reflect)]
pub struct HeadDrops {
pub pos: Vec3,
pub head_id: usize,
pub impulse: bool,
}
impl HeadDrops {
pub fn new(pos: Vec3, head_id: usize) -> Self {
Self {
pos,
head_id,
impulse: true,
}
}
fn new_static(pos: Vec3, head_id: usize) -> Self {
Self {
pos,
head_id,
impulse: false,
}
}
}
#[derive(Component, Reflect)]
#[reflect(Component)]
pub struct HeadDrop {
pub head_id: usize,
}
#[derive(Component, Reflect)]
#[reflect(Component)]
pub struct HeadDropEnableTime(pub f32);
#[derive(Component, Reflect)]
#[reflect(Component)]
pub struct SecretHeadMarker;
#[derive(EntityEvent, Reflect)]
pub struct HeadCollected {
pub entity: Entity,
pub head: usize,
}
pub fn plugin(app: &mut App) {
global_observer!(app, on_head_drop);
app.register_type::<HeadDrop>();
app.register_type::<HeadDropEnableTime>();
app.register_type::<SecretHeadMarker>();
app.add_systems(
Update,
enable_collectible.run_if(in_state(GameState::Playing)),
);
app.add_systems(OnEnter(GameState::Playing), spawn);
server_observer!(app, on_head_drop);
}
fn spawn(mut commands: Commands, query: Query<(Entity, &GlobalTransform, &SecretHead)>) {
for (e, t, head) in query {
commands.trigger(HeadDrops::new_static(
t.translation() + Vec3::new(0., 2., 0.),
head.head_id,
));
commands.entity(e).despawn();
}
}
fn enable_collectible(
mut commands: Commands,
query: Query<(Entity, &HeadDropEnableTime)>,
time: Res<Time>,
) {
let now = time.elapsed_secs();
for (e, enable_time) in query.iter() {
if now > enable_time.0 {
commands
.entity(e)
.insert(CollisionLayers::new(
LayerMask(GameLayer::CollectibleSensors.to_bits()),
LayerMask::ALL,
))
.remove::<HeadDropEnableTime>();
}
}
}
fn on_head_drop(
@@ -89,7 +173,12 @@ fn on_collect_head(
query_player: Query<&Player>,
query_collectable: Query<(&HeadDrop, &ChildOf)>,
query_secret: Query<&SecretHeadMarker>,
env: NetworkEnv,
) {
if !env.is_server() {
return;
}
let collectable = trigger.event().collider1;
let collider = trigger.event().collider2;

View File

@@ -1,11 +1,9 @@
#[cfg(feature = "server")]
use super::ActiveHeads;
use super::HEAD_SLOTS;
use super::{ActiveHeads, HEAD_SLOTS};
#[cfg(feature = "client")]
use super::HeadsImages;
#[cfg(feature = "server")]
use crate::player::Player;
use crate::{GameState, backpack::UiHeadState, loading_assets::UIAssets};
use crate::heads::HeadsImages;
use crate::{
GameState, backpack::UiHeadState, loading_assets::UIAssets, player::Player, protocol::is_server,
};
use bevy::{ecs::spawn::SpawnIter, prelude::*};
use serde::{Deserialize, Serialize};
use std::f32::consts::PI;
@@ -34,8 +32,10 @@ pub fn plugin(app: &mut App) {
app.register_type::<UiActiveHeads>();
app.add_systems(OnEnter(GameState::Playing), setup);
#[cfg(feature = "server")]
app.add_systems(FixedUpdate, sync.run_if(in_state(GameState::Playing)));
app.add_systems(
FixedUpdate,
sync.run_if(in_state(GameState::Playing).and(is_server)),
);
#[cfg(feature = "client")]
app.add_systems(
FixedUpdate,
@@ -238,7 +238,6 @@ fn update_health(
}
}
#[cfg(feature = "server")]
fn sync(
active_heads: Query<Ref<ActiveHeads>, With<Player>>,
mut state: Single<&mut UiActiveHeads>,

View File

@@ -7,10 +7,10 @@ use crate::{
heads_database::HeadsDatabase,
hitpoints::Hitpoints,
player::Player,
protocol::{ClientToController, PlaySound},
protocol::{ClientToController, PlaySound, is_server},
};
use bevy::prelude::*;
use bevy_replicon::prelude::{ClientState, FromClient};
use bevy_replicon::prelude::FromClient;
use serde::{Deserialize, Serialize};
pub mod heads_ui;
@@ -192,7 +192,7 @@ pub fn plugin(app: &mut App) {
(reload, sync_hp).run_if(in_state(GameState::Playing)),
on_select_active_head,
)
.run_if(in_state(ClientState::Disconnected)),
.run_if(is_server),
);
global_observer!(app, on_swap_backpack);

View File

@@ -2,10 +2,10 @@ use crate::{
GameState,
animation::AnimationFlags,
character::{CharacterAnimations, HasCharacterAnimations},
protocol::PlaySound,
protocol::{PlaySound, is_server},
};
use bevy::prelude::*;
use bevy_replicon::prelude::{ClientState, SendMode, ServerTriggerExt, ToClients};
use bevy_replicon::prelude::{SendMode, ServerTriggerExt, ToClients};
use serde::{Deserialize, Serialize};
#[derive(EntityEvent, Reflect)]
@@ -66,15 +66,11 @@ impl Hitpoints {
}
pub fn plugin(app: &mut App) {
app.add_systems(
Update,
on_hp_added.run_if(in_state(ClientState::Disconnected)),
)
.add_systems(
PreUpdate,
reset_hit_animation_flag
.run_if(in_state(ClientState::Disconnected).and(in_state(GameState::Playing))),
);
app.add_systems(Update, on_hp_added.run_if(is_server))
.add_systems(
PreUpdate,
reset_hit_animation_flag.run_if(is_server.and(in_state(GameState::Playing))),
);
}
fn on_hp_added(mut commands: Commands, query: Query<Entity, Added<Hitpoints>>) {

View File

@@ -0,0 +1,229 @@
pub mod abilities;
pub mod ai;
pub mod aim;
pub mod animation;
pub mod backpack;
pub mod camera;
pub mod cash;
pub mod cash_heal;
pub mod character;
#[cfg(feature = "client")]
pub mod client;
pub mod config;
pub mod control;
pub mod cutscene;
pub mod gates;
pub mod head;
pub mod head_drop;
pub mod heads;
pub mod heads_database;
pub mod hitpoints;
pub mod keys;
pub mod loading_assets;
pub mod loading_map;
pub mod movables;
pub mod npc;
pub mod physics_layers;
pub mod platforms;
pub mod player;
pub mod protocol;
pub mod server;
pub mod tb_entities;
pub mod tick;
pub mod utils;
pub mod water;
use crate::{
config::NetworkingConfig,
heads_database::{HeadDatabaseAsset, HeadsDatabase},
protocol::{PlayerId, messages::AssignClientPlayer},
tb_entities::SpawnPoint,
};
use avian3d::{PhysicsPlugins, prelude::TransformInterpolation};
#[cfg(not(feature = "client"))]
use bevy::app::ScheduleRunnerPlugin;
use bevy::{core_pipeline::tonemapping::Tonemapping, prelude::*};
use bevy_common_assets::ron::RonAssetPlugin;
use bevy_replicon::{RepliconPlugins, prelude::ClientId};
use bevy_replicon_renet::RepliconRenetPlugins;
use bevy_sprite3d::Sprite3dPlugin;
use bevy_trenchbroom::{
TrenchBroomPlugins, config::TrenchBroomConfig, prelude::TrenchBroomPhysicsPlugin,
};
use bevy_trenchbroom_avian::AvianPhysicsBackend;
use utils::{billboards, squish_animation};
pub const HEDZ_GREEN: Srgba = Srgba::rgb(0.0, 1.0, 0.0);
pub const HEDZ_PURPLE: Srgba = Srgba::rgb(91. / 256., 4. / 256., 138. / 256.);
pub fn launch() {
let mut app = App::new();
app.register_type::<DebugVisuals>()
.register_type::<TransformInterpolation>();
app.insert_resource(DebugVisuals {
unlit: false,
tonemapping: Tonemapping::None,
exposure: 1.,
shadows: true,
cam_follow: true,
});
let default_plugins = DefaultPlugins;
#[cfg(feature = "client")]
let default_plugins = default_plugins.set(WindowPlugin {
primary_window: Some(Window {
title: "HEDZ Reloaded".into(),
..default()
}),
..default()
});
app.add_plugins(default_plugins);
#[cfg(not(feature = "client"))]
app.add_plugins(ScheduleRunnerPlugin::default());
#[cfg(feature = "client")]
app.add_plugins(
bevy_debug_log::LogViewerPlugin::default()
.auto_open_threshold(bevy::log::tracing::level_filters::LevelFilter::OFF),
);
app.add_plugins(PhysicsPlugins::default());
app.add_plugins((RepliconPlugins, RepliconRenetPlugins));
app.add_plugins(Sprite3dPlugin);
app.add_plugins(TrenchBroomPlugins(
TrenchBroomConfig::new("hedz").icon(None),
));
app.add_plugins(TrenchBroomPhysicsPlugin::new(AvianPhysicsBackend));
app.add_plugins(RonAssetPlugin::<HeadDatabaseAsset>::new(&["headsdb.ron"]));
app.add_plugins(plugin);
app.init_state::<GameState>();
app.run();
}
pub fn plugin(app: &mut App) {
app.add_plugins(abilities::plugin);
app.add_plugins(ai::plugin);
app.add_plugins(animation::plugin);
app.add_plugins(character::plugin);
app.add_plugins(cash::plugin);
app.add_plugins(cash_heal::plugin);
app.add_plugins(config::plugin);
app.add_plugins(player::plugin);
app.add_plugins(gates::plugin);
app.add_plugins(platforms::plugin);
app.add_plugins(movables::plugin);
app.add_plugins(utils::billboards::plugin);
app.add_plugins(aim::plugin);
app.add_plugins(npc::plugin);
app.add_plugins(keys::plugin);
app.add_plugins(utils::squish_animation::plugin);
app.add_plugins(camera::plugin);
#[cfg(feature = "client")]
app.add_plugins(client::plugin);
app.add_plugins(control::plugin);
app.add_plugins(cutscene::plugin);
app.add_plugins(backpack::plugin);
app.add_plugins(loading_assets::LoadingPlugin);
app.add_plugins(loading_map::plugin);
app.add_plugins(heads::plugin);
app.add_plugins(hitpoints::plugin);
app.add_plugins(head_drop::plugin);
app.add_plugins(protocol::plugin);
app.add_plugins(server::plugin);
app.add_plugins(tb_entities::plugin);
app.add_plugins(tick::plugin);
app.add_plugins(utils::plugin);
app.add_plugins(utils::auto_rotate::plugin);
app.add_plugins(utils::explosions::plugin);
app.add_plugins(utils::sprite_3d_animation::plugin);
app.add_plugins(utils::trail::plugin);
app.add_plugins(water::plugin);
if cfg!(feature = "client") {
app.add_systems(
OnEnter(GameState::Waiting),
start_solo_client
.run_if(|config: Res<NetworkingConfig>| config.server.is_none() && !config.host),
);
app.add_systems(
OnEnter(GameState::Waiting),
start_listen_server
.run_if(|config: Res<NetworkingConfig>| config.server.is_none() && config.host),
);
app.add_systems(
OnEnter(GameState::Waiting),
start_client.run_if(|config: Res<NetworkingConfig>| config.server.is_some()),
);
} else {
app.add_systems(OnEnter(GameState::Waiting), start_dedicated_server);
}
}
#[derive(Resource, Reflect, Debug)]
#[reflect(Resource)]
pub struct DebugVisuals {
pub unlit: bool,
pub tonemapping: Tonemapping,
pub exposure: f32,
pub shadows: bool,
pub cam_follow: bool,
}
#[derive(States, Default, Clone, Copy, Eq, PartialEq, Debug, Hash)]
pub enum GameState {
/// Loading assets from disk
#[default]
AssetLoading,
/// Loading + constructing map
MapLoading,
/// Waiting to host/connect/play
Waiting,
/// Connecting to server
Connecting,
/// Opening server
Hosting,
/// Running the game
Playing,
}
fn start_solo_client(
commands: Commands,
mut next: ResMut<NextState<GameState>>,
query: Query<&Transform, With<SpawnPoint>>,
heads_db: Res<HeadsDatabase>,
mut assign_player_id: MessageWriter<AssignClientPlayer>,
) {
next.set(GameState::Playing);
player::spawn(commands, ClientId::Server, query, heads_db);
assign_player_id.write(AssignClientPlayer(PlayerId { id: 0 }));
}
fn start_listen_server(
commands: Commands,
mut next: ResMut<NextState<GameState>>,
query: Query<&Transform, With<SpawnPoint>>,
heads_db: Res<HeadsDatabase>,
mut assign_player_id: MessageWriter<AssignClientPlayer>,
) {
next.set(GameState::Hosting);
player::spawn(commands, ClientId::Server, query, heads_db);
assign_player_id.write(AssignClientPlayer(PlayerId { id: 0 }));
}
fn start_client(mut next: ResMut<NextState<GameState>>) {
next.set(GameState::Connecting);
}
fn start_dedicated_server(mut next: ResMut<NextState<GameState>>) {
next.set(GameState::Hosting);
}

View File

@@ -147,4 +147,6 @@ fn on_exit(
.expect("headsdb failed to load");
cmds.insert_resource(HeadsDatabase { heads: asset.0 });
info!("loaded assets");
}

View File

@@ -33,13 +33,7 @@ fn setup_scene(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.entity(t.scene_root_entity).insert(Replicated);
const { assert!(cfg!(feature = "client") ^ cfg!(feature = "server")) };
#[cfg(feature = "client")]
next_game_state.set(GameState::Connecting);
#[cfg(feature = "server")]
next_game_state.set(GameState::Playing);
next_game_state.set(GameState::Waiting);
},
);

View File

@@ -0,0 +1,3 @@
pub fn main() {
hedz_reloaded::launch();
}

View File

@@ -1,23 +1,21 @@
use crate::{
GameState, character::HedzCharacter, global_observer, loading_assets::GameAssets,
utils::billboards::Billboard,
};
#[cfg(feature = "server")]
use crate::{
GameState,
ai::Ai,
character::AnimatedCharacter,
character::{AnimatedCharacter, HedzCharacter},
global_observer,
head::ActiveHead,
head_drop::HeadDrops,
heads::{ActiveHeads, HEAD_COUNT, HeadState},
heads_database::HeadsDatabase,
hitpoints::{Hitpoints, Kill},
keys::KeySpawn,
protocol::PlaySound,
loading_assets::GameAssets,
protocol::{PlaySound, is_server},
tb_entities::EnemySpawn,
utils::billboards::Billboard,
};
use bevy::{light::NotShadowCaster, prelude::*};
use serde::{Deserialize, Serialize};
#[cfg(feature = "server")]
use std::collections::HashMap;
#[derive(Component, Reflect, PartialEq, Serialize, Deserialize)]
@@ -35,7 +33,6 @@ struct NpcSpawning {
#[reflect(Component)]
pub struct SpawningBeam(pub f32);
#[cfg(feature = "server")]
#[derive(Event)]
struct OnCheckSpawns;
@@ -44,16 +41,17 @@ pub struct SpawnCharacter(pub Vec3);
pub fn plugin(app: &mut App) {
app.init_resource::<NpcSpawning>();
#[cfg(feature = "server")]
app.add_systems(FixedUpdate, setup.run_if(in_state(GameState::Playing)));
app.add_systems(
FixedUpdate,
setup.run_if(in_state(GameState::Playing).and(is_server)),
);
app.add_systems(Update, update_beams.run_if(in_state(GameState::Playing)));
#[cfg(feature = "server")]
global_observer!(app, on_spawn_check);
global_observer!(app, on_spawn);
}
#[cfg(feature = "server")]
fn setup(mut commands: Commands, mut spawned: Local<bool>) {
if *spawned {
return;
@@ -65,7 +63,6 @@ fn setup(mut commands: Commands, mut spawned: Local<bool>) {
*spawned = true;
}
#[cfg(feature = "server")]
fn on_spawn_check(
_trigger: On<OnCheckSpawns>,
mut commands: Commands,
@@ -120,7 +117,6 @@ fn on_spawn_check(
}
}
#[cfg(feature = "server")]
fn on_kill(
trigger: On<Kill>,
mut commands: Commands,

View File

@@ -1,6 +1,12 @@
use crate::{GameState, tick::GameTick};
use crate::{
GameState,
protocol::is_server,
tb_entities::{Platform, PlatformTarget},
tick::GameTick,
};
use avian3d::prelude::{LinearVelocity, Position};
use bevy::{math::ops::sin, prelude::*};
use bevy_trenchbroom::prelude::Target;
use serde::{Deserialize, Serialize};
#[derive(Component, Reflect, Default, Debug, Serialize, Deserialize, PartialEq)]
@@ -16,6 +22,8 @@ pub fn plugin(app: &mut App) {
FixedUpdate,
move_active.run_if(in_state(GameState::Playing)),
);
app.add_systems(OnEnter(GameState::Playing), init.run_if(is_server));
}
fn move_active(
@@ -33,3 +41,30 @@ fn move_active(
velocity.0 = (target - prev) / fixed_time.timestep().as_secs_f32();
}
}
#[allow(clippy::type_complexity)]
fn init(
mut commands: Commands,
uninit_platforms: Query<
(Entity, &Target, &Transform),
(Without<ActivePlatform>, With<Platform>),
>,
targets: Query<(&PlatformTarget, &Transform)>,
) {
for (e, target, transform) in uninit_platforms.iter() {
let Some(target) = targets
.iter()
.find(|(t, _)| t.targetname == target.target.clone().unwrap_or_default())
.map(|(_, t)| t.translation)
else {
continue;
};
let platform = ActivePlatform {
start: transform.translation,
target,
};
commands.entity(e).insert(platform);
}
}

View File

@@ -0,0 +1,242 @@
use crate::{
GameState,
abilities::PlayerTriggerState,
backpack::{Backpack, backpack_ui::BackpackUiState},
camera::{CameraArmRotation, CameraTarget},
cash::{Cash, CashCollectEvent, CashInventory},
character::{AnimatedCharacter, HedzCharacter},
control::{Inputs, LocalInputs, controller_common::PlayerCharacterController},
global_observer,
head::ActiveHead,
head_drop::HeadDrops,
heads::{ActiveHeads, HeadChanged, HeadState, heads_ui::UiActiveHeads},
heads_database::HeadsDatabase,
hitpoints::{Hitpoints, Kill},
npc::SpawnCharacter,
protocol::{ClientHeadChanged, OwnedByClient, PlaySound, PlayerId},
tb_entities::SpawnPoint,
};
use avian3d::prelude::*;
use bevy::{
input::common_conditions::input_just_pressed,
prelude::*,
window::{CursorGrabMode, CursorOptions, PrimaryWindow},
};
use bevy_replicon::prelude::{ClientId, Replicated, SendMode, ServerTriggerExt, ToClients};
use happy_feet::debug::DebugInput;
use serde::{Deserialize, Serialize};
#[derive(Component, Default, Serialize, Deserialize, PartialEq)]
#[require(HedzCharacter, DebugInput = DebugInput, PlayerTriggerState)]
pub struct Player;
#[derive(Component, Debug, Reflect)]
#[reflect(Component)]
#[require(LocalInputs, BackpackUiState)]
pub struct LocalPlayer;
#[derive(Component, Default, Serialize, Deserialize, PartialEq)]
#[require(Transform, Visibility)]
pub struct PlayerBodyMesh;
/// Server-side only; inserted on each `client` (not the controller) to track player ids.
#[derive(Component, Clone, Copy)]
pub struct ClientPlayerId(pub PlayerId);
pub fn plugin(app: &mut App) {
app.add_systems(
OnEnter(GameState::Playing),
(toggle_cursor_system, cursor_recenter),
);
app.add_systems(
Update,
(
collect_cash,
setup_animations_marker_for_player,
toggle_cursor_system.run_if(input_just_pressed(KeyCode::Escape)),
)
.run_if(in_state(GameState::Playing)),
);
global_observer!(app, on_update_head_mesh);
}
pub fn spawn(
mut commands: Commands,
owner: ClientId,
query: Query<&Transform, With<SpawnPoint>>,
heads_db: Res<HeadsDatabase>,
) -> Option<Entity> {
let spawn = query.iter().next()?;
let transform = Transform::from_translation(spawn.translation + Vec3::new(0., 3., 0.));
let id = commands
.spawn((
(
Name::from("player"),
Player,
ActiveHead(0),
ActiveHeads::new([
Some(HeadState::new(0, heads_db.as_ref())),
Some(HeadState::new(3, heads_db.as_ref())),
Some(HeadState::new(6, heads_db.as_ref())),
Some(HeadState::new(10, heads_db.as_ref())),
Some(HeadState::new(9, heads_db.as_ref())),
]),
Hitpoints::new(100),
CashInventory::default(),
CameraTarget,
transform,
Visibility::default(),
PlayerCharacterController,
PlayerId { id: 0 },
),
Backpack::default(),
BackpackUiState::default(),
UiActiveHeads::default(),
Inputs::default(),
Replicated,
))
.with_children(|c| {
c.spawn((
Name::new("player-rig"),
PlayerBodyMesh,
CameraArmRotation,
Replicated,
))
.with_child((
Name::new("player-animated-character"),
AnimatedCharacter::new(0),
Replicated,
));
})
.observe(on_kill)
.id();
if let Some(owner) = owner.entity() {
commands.entity(id).insert(OwnedByClient(owner));
}
commands.server_trigger(ToClients {
mode: SendMode::Broadcast,
message: PlaySound::Head("angry demonstrator".to_string()),
});
commands.trigger(SpawnCharacter(transform.translation));
Some(id)
}
fn on_kill(
trigger: On<Kill>,
mut commands: Commands,
mut query: Query<(&Transform, &ActiveHead, &mut ActiveHeads, &mut Hitpoints)>,
) {
let Ok((transform, active, mut heads, mut hp)) = query.get_mut(trigger.event().entity) else {
return;
};
commands.trigger(HeadDrops::new(transform.translation, active.0));
if let Some(new_head) = heads.loose_current() {
hp.set_health(heads.current().unwrap().health);
commands.trigger(HeadChanged(new_head));
}
}
fn on_update_head_mesh(
trigger: On<HeadChanged>,
mut commands: Commands,
mesh_children: Single<&Children, With<PlayerBodyMesh>>,
animated_characters: Query<&AnimatedCharacter>,
mut player: Single<&mut ActiveHead, With<Player>>,
) -> Result {
let animated_char = mesh_children
.iter()
.find(|child| animated_characters.contains(*child))
.ok_or("tried to update head mesh before AnimatedCharacter was readded")?;
player.0 = trigger.0;
commands
.entity(animated_char)
.insert(AnimatedCharacter::new(trigger.0));
commands.server_trigger(ToClients {
mode: SendMode::Broadcast,
message: ClientHeadChanged(trigger.0 as u64),
});
Ok(())
}
fn cursor_recenter(q_windows: Single<&mut Window, With<PrimaryWindow>>) {
let mut primary_window = q_windows;
let center = Vec2::new(
primary_window.resolution.width() / 2.,
primary_window.resolution.height() / 2.,
);
primary_window.set_cursor_position(Some(center));
}
fn toggle_grab_cursor(options: &mut CursorOptions) {
match options.grab_mode {
CursorGrabMode::None => {
options.grab_mode = CursorGrabMode::Confined;
options.visible = false;
}
_ => {
options.grab_mode = CursorGrabMode::None;
options.visible = true;
}
}
}
fn toggle_cursor_system(mut window: Single<&mut CursorOptions, With<PrimaryWindow>>) {
toggle_grab_cursor(&mut window);
}
fn collect_cash(
mut commands: Commands,
mut collision_message_reader: MessageReader<CollisionStart>,
query_player: Query<&Player>,
query_cash: Query<&Cash>,
) {
for CollisionStart {
collider1: e1,
collider2: e2,
..
} in collision_message_reader.read()
{
let collect = if query_player.contains(*e1) && query_cash.contains(*e2) {
Some(*e2)
} else if query_player.contains(*e2) && query_cash.contains(*e1) {
Some(*e1)
} else {
None
};
if let Some(cash) = collect {
commands.trigger(CashCollectEvent);
commands.entity(cash).despawn();
}
}
}
fn setup_animations_marker_for_player(
mut commands: Commands,
animation_handles: Query<Entity, Added<AnimationGraphHandle>>,
child_of: Query<&ChildOf>,
player_rig: Query<&ChildOf, With<PlayerBodyMesh>>,
) {
for animation_rig in animation_handles.iter() {
for ancestor in child_of.iter_ancestors(animation_rig) {
if let Ok(rig_child_of) = player_rig.get(ancestor) {
commands.entity(rig_child_of.parent());
return;
}
}
}
}

View File

@@ -31,9 +31,9 @@ use avian3d::prelude::{
AngularInertia, AngularVelocity, CenterOfMass, Collider, ColliderDensity, CollisionLayers,
LinearVelocity, LockedAxes, Mass, Position, RigidBody, Rotation,
};
use bevy::{platform::collections::HashMap, prelude::*};
use bevy::{ecs::system::SystemParam, platform::collections::HashMap, prelude::*};
use bevy_replicon::prelude::{
AppRuleExt, Channel, ClientEventAppExt, ClientMessageAppExt, ServerEventAppExt,
AppRuleExt, Channel, ClientEventAppExt, ClientMessageAppExt, ClientState, ServerEventAppExt,
ServerMessageAppExt, SyncRelatedAppExt,
};
pub use components::*;
@@ -150,6 +150,23 @@ pub fn plugin(app: &mut App) {
global_observer!(app, components::spawn_gltf_scene_roots);
}
#[derive(SystemParam)]
pub struct NetworkEnv<'w> {
client_state: Res<'w, State<ClientState>>,
}
impl NetworkEnv<'_> {
/// Returns true if this process is currently responsible for being the server/host/"source of truth".
/// May change over time.
pub fn is_server(&self) -> bool {
matches!(**self.client_state, ClientState::Disconnected)
}
}
pub fn is_server(state: Res<State<ClientState>>) -> bool {
matches!(**state, ClientState::Disconnected)
}
fn set_game_tick(on: On<SetGameTick>, mut tick: ResMut<GameTick>) {
tick.0 = on.event().0;
}

View File

@@ -1,19 +1,4 @@
use crate::config::ServerConfig;
use bevy::prelude::*;
use bevy_replicon::{
RepliconPlugins,
prelude::{
ClientId, ConnectedClient, FromClient, RepliconChannels, SendMode, ServerState,
ServerTriggerExt, ToClients,
},
server::AuthorizedClient,
};
use bevy_replicon_renet::{
RenetChannelsExt, RepliconRenetPlugins,
netcode::{NetcodeServerTransport, ServerAuthentication},
renet::{ConnectionConfig, RenetServer},
};
use shared::{
use crate::{
GameState, global_observer,
heads_database::HeadsDatabase,
player::ClientPlayerId,
@@ -21,26 +6,32 @@ use shared::{
tb_entities::SpawnPoint,
tick::GameTick,
};
use bevy::prelude::*;
use bevy_replicon::{
prelude::{
ClientId, ConnectedClient, FromClient, RepliconChannels, SendMode, ServerTriggerExt,
ToClients,
},
server::AuthorizedClient,
};
use bevy_replicon_renet::{
RenetChannelsExt,
netcode::{NetcodeServerTransport, ServerAuthentication},
renet::{ConnectionConfig, RenetServer},
};
use std::{
net::{Ipv4Addr, UdpSocket},
time::SystemTime,
};
pub fn plugin(app: &mut App) {
app.add_plugins((RepliconPlugins, RepliconRenetPlugins));
app.add_systems(OnEnter(GameState::Playing), setup_timeout_timer);
app.add_systems(OnEnter(ServerState::Running), notify_started);
app.add_systems(Update, run_timeout.run_if(in_state(GameState::Playing)));
app.add_systems(OnEnter(GameState::Playing), open_renet_server);
app.add_systems(OnEnter(GameState::Hosting), open_renet_server);
// Replicon
global_observer!(app, on_connected);
global_observer!(app, on_disconnected);
// Server logic
global_observer!(app, cancel_timeout);
global_observer!(app, on_client_playing);
}
@@ -50,13 +41,10 @@ fn on_client_playing(
query: Query<&Transform, With<SpawnPoint>>,
heads_db: Res<HeadsDatabase>,
) -> Result {
info!("Client has entered playing gamestate");
info!("client has entered playing gamestate");
let Some(client) = trigger.client_id.entity() else {
return Ok(());
};
crate::player::spawn(commands, client, query, heads_db).ok_or("failed to spawn player")?;
crate::player::spawn(commands, trigger.client_id, query, heads_db)
.ok_or("failed to spawn player")?;
Ok(())
}
@@ -68,7 +56,10 @@ fn on_client_playing(
fn open_renet_server(
mut commands: Commands,
channels: Res<RepliconChannels>,
mut next: ResMut<NextState<GameState>>,
) -> Result<(), BevyError> {
info!("opening server");
let server_channels_config = channels.server_configs();
let client_channels_config = channels.client_configs();
@@ -93,7 +84,9 @@ fn open_renet_server(
commands.insert_resource(server);
commands.insert_resource(transport);
info!("Hosting a server on port {port}");
info!("hosting a server on port {port}");
next.set(GameState::Playing);
Ok(())
}
@@ -125,44 +118,6 @@ fn on_connected(
});
}
fn on_disconnected(
on: On<Remove, ConnectedClient>,
config: Res<ServerConfig>,
mut writer: MessageWriter<AppExit>,
) {
fn on_disconnected(on: On<Remove, ConnectedClient>) {
info!("client {} disconnected", on.entity);
if config.close_on_client_disconnect {
info!("client disconnected, exiting");
writer.write(AppExit::Success);
}
}
fn notify_started() {
println!("hedz.server_started");
}
#[derive(Resource)]
struct TimeoutTimer(f32);
fn setup_timeout_timer(mut commands: Commands, config: Res<ServerConfig>) {
commands.insert_resource(TimeoutTimer(config.timeout));
}
fn run_timeout(
mut timer: ResMut<TimeoutTimer>,
mut writer: MessageWriter<AppExit>,
time: Res<Time>,
) {
timer.0 -= time.delta_secs();
if timer.0 <= 0.0 {
info!("client timed out, exiting");
writer.write(AppExit::Success);
}
}
fn cancel_timeout(_trigger: On<Add, ConnectedClient>, mut timer: ResMut<TimeoutTimer>) {
info!("client connected, cancelling timeout");
timer.0 = f32::INFINITY;
}

View File

@@ -3,7 +3,9 @@ use crate::{
cash::Cash,
loading_assets::GameAssets,
physics_layers::GameLayer,
protocol::{SkipReplicateColliders, TbMapIdCounter},
protocol::{
SkipReplicateColliders, TbMapEntityId, TbMapIdCounter, messages::DespawnTbMapEntity,
},
utils::global_observer,
};
use avian3d::{
@@ -15,6 +17,7 @@ use bevy::{
math::*,
prelude::*,
};
use bevy_replicon::prelude::{ClientId, ConnectedClient, SendMode, ToClients};
use bevy_trenchbroom::prelude::*;
use serde::{Deserialize, Serialize};
@@ -199,6 +202,7 @@ fn fix_target_tb_entities(
}
pub fn plugin(app: &mut App) {
app.register_type::<DespawnedTbEntityCache>();
app.register_type::<SpawnPoint>();
app.override_class::<Worldspawn>();
app.register_type::<Water>();
@@ -215,8 +219,18 @@ pub fn plugin(app: &mut App) {
app.register_type::<CashSpawn>();
app.register_type::<SecretHead>();
app.init_resource::<DespawnedTbEntityCache>();
app.add_systems(OnExit(GameState::MapLoading), fix_target_tb_entities);
app.add_systems(
OnEnter(GameState::MapLoading),
|mut cache: ResMut<DespawnedTbEntityCache>| cache.0.clear(),
);
global_observer!(app, add_despawned_entities_to_cache);
global_observer!(app, send_new_client_despawned_cache);
global_observer!(app, tb_component_setup::<CashSpawn>);
global_observer!(app, tb_component_setup::<Movable>);
global_observer!(app, tb_component_setup::<Platform>);
@@ -235,3 +249,28 @@ fn tb_component_setup<C: Component>(
.insert_if_new(id)
.insert(SkipReplicateColliders);
}
fn add_despawned_entities_to_cache(
trigger: On<Remove, TbMapEntityId>,
id: Query<&TbMapEntityId>,
mut cache: ResMut<DespawnedTbEntityCache>,
) {
cache.0.push(id.get(trigger.event().entity).unwrap().id);
}
#[derive(Default, Resource, Reflect)]
#[reflect(Resource)]
pub struct DespawnedTbEntityCache(pub Vec<u64>);
fn send_new_client_despawned_cache(
on: On<Add, ConnectedClient>,
cache: Res<DespawnedTbEntityCache>,
mut send: MessageWriter<ToClients<DespawnTbMapEntity>>,
) {
for &id in cache.0.iter() {
send.write(ToClients {
mode: SendMode::Direct(ClientId::Client(on.entity)),
message: DespawnTbMapEntity(id),
});
}
}

View File

@@ -0,0 +1,56 @@
#[macro_export]
macro_rules! global_observer {
($app:expr, $($system:tt)*) => {{
$app.world_mut()
.add_observer($($system)*)
.insert(global_observer!(@name $($system)*))
}};
(@name $system:ident ::< $($param:ident),+ $(,)? >) => {{
let mut name = String::new();
name.push_str(stringify!($system));
name.push_str("::<");
$(
name.push_str(std::any::type_name::<$param>());
)+
name.push_str(">");
Name::new(name)
}};
(@name $system:expr) => {
Name::new(stringify!($system))
};
}
pub use global_observer;
#[macro_export]
macro_rules! server_observer {
($app:expr, $($system:tt)*) => {{
$app.add_systems(OnEnter(::bevy_replicon::prelude::ClientState::Disconnected), |mut commands: Commands| {
commands
.add_observer($($system)*)
.insert((
global_observer!(@name $($system)*),
DespawnOnExit(::bevy_replicon::prelude::ClientState::Disconnected),
));
})
}};
(@name $system:ident ::< $($param:ident),+ $(,)? >) => {{
let mut name = String::new();
name.push_str(stringify!($system));
name.push_str("::<");
$(
name.push_str(std::any::type_name::<$param>());
)+
name.push_str(">");
Name::new(name)
}};
(@name $system:expr) => {
Name::new(stringify!($system))
};
}
pub use server_observer;

View File

@@ -1,26 +0,0 @@
[package]
name = "server"
version = "0.1.0"
edition = "2024"
[features]
default = ["shared/server"]
dbg = ["avian3d/debug-plugin", "shared/dbg"]
[dependencies]
avian3d = { workspace = true }
bevy = { workspace = true, default-features = false }
bevy-steamworks = { workspace = true }
bevy_common_assets = { workspace = true }
bevy_replicon = { workspace = true, features = ["server"] }
bevy_replicon_renet = { workspace = true, features = ["server"] }
bevy_sprite3d = { workspace = true }
bevy_trenchbroom = { workspace = true }
bevy_trenchbroom_avian = { workspace = true }
clap = { workspace = true }
happy_feet = { workspace = true }
rand = { workspace = true }
ron = { workspace = true }
serde = { workspace = true }
shared = { workspace = true }
steamworks = { workspace = true }

View File

@@ -1,20 +0,0 @@
use bevy::prelude::*;
use clap::Parser;
pub fn plugin(app: &mut App) {
let config = ServerConfig::parse();
app.insert_resource(config);
}
/// The server for HEDZ Reloaded
#[derive(Resource, Parser, Debug)]
#[command(version, about, long_about = None)]
pub struct ServerConfig {
#[arg(long, default_value_t = f32::INFINITY)]
/// How long to wait for a client to connect before closing, in seconds
pub timeout: f32,
#[arg(long, default_value_t = false)]
/// Whether to close when a client disconnects
pub close_on_client_disconnect: bool,
}

View File

@@ -1,137 +0,0 @@
use avian3d::prelude::*;
use bevy::{app::plugin_group, core_pipeline::tonemapping::Tonemapping, prelude::*};
use bevy_common_assets::ron::RonAssetPlugin;
use bevy_sprite3d::Sprite3dPlugin;
use bevy_trenchbroom::prelude::*;
use bevy_trenchbroom_avian::AvianPhysicsBackend;
use shared::{DebugVisuals, GameState, heads_database::HeadDatabaseAsset};
mod config;
mod head_drop;
mod platforms;
mod player;
mod server;
mod tb_entities;
mod utils;
plugin_group! {
pub struct DefaultPlugins {
bevy::app:::PanicHandlerPlugin,
bevy::log:::LogPlugin,
bevy::app:::TaskPoolPlugin,
bevy::diagnostic:::FrameCountPlugin,
bevy::time:::TimePlugin,
bevy::transform:::TransformPlugin,
bevy::diagnostic:::DiagnosticsPlugin,
bevy::input:::InputPlugin,
bevy::app:::ScheduleRunnerPlugin,
bevy::window:::WindowPlugin,
bevy::a11y:::AccessibilityPlugin,
bevy::app:::TerminalCtrlCHandlerPlugin,
bevy::asset:::AssetPlugin,
bevy::scene:::ScenePlugin,
bevy::mesh:::MeshPlugin,
bevy::light:::LightPlugin,
bevy::camera:::CameraPlugin,
bevy::render:::RenderPlugin,
bevy::image:::ImagePlugin,
bevy::render::pipelined_rendering:::PipelinedRenderingPlugin,
bevy::core_pipeline:::CorePipelinePlugin,
bevy::sprite:::SpritePlugin,
bevy::text:::TextPlugin,
bevy::ui:::UiPlugin,
bevy::pbr:::PbrPlugin,
bevy::gltf:::GltfPlugin,
bevy::gilrs:::GilrsPlugin,
bevy::animation:::AnimationPlugin,
bevy::gizmos:::GizmoPlugin,
bevy::state::app:::StatesPlugin,
#[plugin_group]
bevy::picking:::DefaultPickingPlugins,
}
}
fn main() {
let mut app = App::new();
app.register_type::<DebugVisuals>()
.register_type::<TransformInterpolation>();
app.insert_resource(DebugVisuals {
unlit: false,
tonemapping: Tonemapping::None,
exposure: 1.,
shadows: true,
cam_follow: true,
});
app.add_plugins(DefaultPlugins.set(bevy::log::LogPlugin {
filter: "info,lightyear_replication=off".into(),
level: bevy::log::Level::INFO,
// provide custom log layer to receive logging events
..default()
}));
app.add_plugins(PhysicsPlugins::default());
app.add_plugins(Sprite3dPlugin);
app.add_plugins(TrenchBroomPlugins(
TrenchBroomConfig::new("hedz").icon(None),
));
app.add_plugins(TrenchBroomPhysicsPlugin::new(AvianPhysicsBackend));
app.add_plugins(RonAssetPlugin::<HeadDatabaseAsset>::new(&["headsdb.ron"]));
app.add_plugins(shared::abilities::plugin);
app.add_plugins(shared::ai::plugin);
app.add_plugins(shared::aim::plugin);
app.add_plugins(shared::animation::plugin);
app.add_plugins(shared::backpack::plugin);
app.add_plugins(shared::camera::plugin);
app.add_plugins(shared::cash::plugin);
app.add_plugins(shared::cash_heal::plugin);
app.add_plugins(shared::character::plugin);
app.add_plugins(shared::control::plugin);
app.add_plugins(shared::cutscene::plugin);
app.add_plugins(shared::gates::plugin);
app.add_plugins(shared::head_drop::plugin);
app.add_plugins(shared::heads::plugin);
app.add_plugins(shared::hitpoints::plugin);
app.add_plugins(shared::keys::plugin);
app.add_plugins(shared::loading_assets::LoadingPlugin);
app.add_plugins(shared::loading_map::plugin);
app.add_plugins(shared::movables::plugin);
app.add_plugins(shared::npc::plugin);
app.add_plugins(shared::platforms::plugin);
app.add_plugins(shared::player::plugin);
app.add_plugins(shared::steam::plugin);
app.add_plugins(shared::tb_entities::plugin);
app.add_plugins(shared::tick::plugin);
app.add_plugins(shared::utils::auto_rotate::plugin);
app.add_plugins(shared::utils::billboards::plugin);
app.add_plugins(shared::utils::explosions::plugin);
app.add_plugins(shared::utils::sprite_3d_animation::plugin);
app.add_plugins(shared::utils::squish_animation::plugin);
app.add_plugins(shared::utils::trail::plugin);
app.add_plugins(shared::utils::plugin);
app.add_plugins(shared::water::plugin);
// Networking
// The client/server plugin must go before the protocol, or else `ProtocolHasher` will not be available.
app.add_plugins(server::plugin);
app.add_plugins(shared::protocol::plugin);
app.add_plugins(config::plugin);
app.add_plugins(head_drop::plugin);
app.add_plugins(platforms::plugin);
app.add_plugins(player::plugin);
app.add_plugins(tb_entities::plugin);
app.add_plugins(utils::plugin);
app.init_state::<GameState>();
app.add_systems(PostStartup, setup_panic_handler);
app.run();
}
fn setup_panic_handler() {
_ = std::panic::take_hook();
}

View File

@@ -1,38 +0,0 @@
use bevy::prelude::*;
use bevy_trenchbroom::prelude::Target;
use shared::{
GameState,
platforms::ActivePlatform,
tb_entities::{Platform, PlatformTarget},
};
pub fn plugin(app: &mut App) {
app.add_systems(OnEnter(GameState::Playing), init);
}
#[allow(clippy::type_complexity)]
fn init(
mut commands: Commands,
uninit_platforms: Query<
(Entity, &Target, &Transform),
(Without<ActivePlatform>, With<Platform>),
>,
targets: Query<(&PlatformTarget, &Transform)>,
) {
for (e, target, transform) in uninit_platforms.iter() {
let Some(target) = targets
.iter()
.find(|(t, _)| t.targetname == target.target.clone().unwrap_or_default())
.map(|(_, t)| t.translation)
else {
continue;
};
let platform = ActivePlatform {
start: transform.translation,
target,
};
commands.entity(e).insert(platform);
}
}

View File

@@ -1,130 +0,0 @@
use bevy::prelude::*;
use bevy_replicon::prelude::{Replicated, SendMode, ServerTriggerExt, ToClients};
use shared::{
backpack::{Backpack, backpack_ui::BackpackUiState},
camera::{CameraArmRotation, CameraTarget},
cash::CashInventory,
character::AnimatedCharacter,
control::{Inputs, controller_common::PlayerCharacterController},
global_observer,
head::ActiveHead,
head_drop::HeadDrops,
heads::{ActiveHeads, HeadChanged, HeadState, heads_ui::UiActiveHeads},
heads_database::HeadsDatabase,
hitpoints::{Hitpoints, Kill},
npc::SpawnCharacter,
player::{Player, PlayerBodyMesh},
protocol::{OwnedByClient, PlaySound, PlayerId, events::ClientHeadChanged},
tb_entities::SpawnPoint,
};
pub fn plugin(app: &mut App) {
global_observer!(app, on_update_head_mesh);
}
pub fn spawn(
mut commands: Commands,
owner: Entity,
query: Query<&Transform, With<SpawnPoint>>,
heads_db: Res<HeadsDatabase>,
) -> Option<Entity> {
let spawn = query.iter().next()?;
let transform = Transform::from_translation(spawn.translation + Vec3::new(0., 3., 0.));
let id = commands
.spawn((
(
Name::from("player"),
Player,
ActiveHead(0),
ActiveHeads::new([
Some(HeadState::new(0, heads_db.as_ref())),
Some(HeadState::new(3, heads_db.as_ref())),
Some(HeadState::new(6, heads_db.as_ref())),
Some(HeadState::new(10, heads_db.as_ref())),
Some(HeadState::new(9, heads_db.as_ref())),
]),
Hitpoints::new(100),
CashInventory::default(),
CameraTarget,
transform,
Visibility::default(),
PlayerCharacterController,
PlayerId { id: 0 },
),
Backpack::default(),
BackpackUiState::default(),
UiActiveHeads::default(),
Inputs::default(),
Replicated,
OwnedByClient(owner),
))
.with_children(|c| {
c.spawn((
Name::new("player-rig"),
PlayerBodyMesh,
CameraArmRotation,
Replicated,
))
.with_child((
Name::new("player-animated-character"),
AnimatedCharacter::new(0),
Replicated,
));
})
.observe(on_kill)
.id();
commands.server_trigger(ToClients {
mode: SendMode::Broadcast,
message: PlaySound::Head("angry demonstrator".to_string()),
});
commands.trigger(SpawnCharacter(transform.translation));
Some(id)
}
fn on_kill(
trigger: On<Kill>,
mut commands: Commands,
mut query: Query<(&Transform, &ActiveHead, &mut ActiveHeads, &mut Hitpoints)>,
) {
let Ok((transform, active, mut heads, mut hp)) = query.get_mut(trigger.event().entity) else {
return;
};
commands.trigger(HeadDrops::new(transform.translation, active.0));
if let Some(new_head) = heads.loose_current() {
hp.set_health(heads.current().unwrap().health);
commands.trigger(HeadChanged(new_head));
}
}
fn on_update_head_mesh(
trigger: On<HeadChanged>,
mut commands: Commands,
mesh_children: Single<&Children, With<PlayerBodyMesh>>,
animated_characters: Query<&AnimatedCharacter>,
mut player: Single<&mut ActiveHead, With<Player>>,
) -> Result {
let animated_char = mesh_children
.iter()
.find(|child| animated_characters.contains(*child))
.ok_or("tried to update head mesh before AnimatedCharacter was readded")?;
player.0 = trigger.0;
commands
.entity(animated_char)
.insert(AnimatedCharacter::new(trigger.0));
commands.server_trigger(ToClients {
mode: SendMode::Broadcast,
message: ClientHeadChanged(trigger.0 as u64),
});
Ok(())
}

View File

@@ -1,45 +0,0 @@
use bevy::prelude::*;
use bevy_replicon::prelude::{ClientId, ConnectedClient, SendMode, ToClients};
use shared::{
GameState, global_observer,
protocol::{TbMapEntityId, messages::DespawnTbMapEntity},
};
pub fn plugin(app: &mut App) {
app.register_type::<DespawnedTbEntityCache>();
app.init_resource::<DespawnedTbEntityCache>();
app.add_systems(
OnEnter(GameState::MapLoading),
|mut cache: ResMut<DespawnedTbEntityCache>| cache.0.clear(),
);
global_observer!(app, add_despawned_entities_to_cache);
global_observer!(app, send_new_client_despawned_cache);
}
fn add_despawned_entities_to_cache(
trigger: On<Remove, TbMapEntityId>,
id: Query<&TbMapEntityId>,
mut cache: ResMut<DespawnedTbEntityCache>,
) {
cache.0.push(id.get(trigger.event().entity).unwrap().id);
}
#[derive(Default, Resource, Reflect)]
#[reflect(Resource)]
pub struct DespawnedTbEntityCache(pub Vec<u64>);
fn send_new_client_despawned_cache(
on: On<Add, ConnectedClient>,
cache: Res<DespawnedTbEntityCache>,
mut send: MessageWriter<ToClients<DespawnTbMapEntity>>,
) {
for &id in cache.0.iter() {
send.write(ToClients {
mode: SendMode::Direct(ClientId::Client(on.entity)),
message: DespawnTbMapEntity(id),
});
}
}

View File

@@ -1,37 +0,0 @@
use bevy::{
ecs::{archetype::Archetypes, component::Components, entity::Entities},
prelude::*,
};
use shared::global_observer;
pub fn plugin(app: &mut App) {
global_observer!(app, report_entity_components);
}
#[derive(Event)]
pub struct ReportEntityComponents(pub Entity);
fn report_entity_components(
trigger: On<ReportEntityComponents>,
entities: &Entities,
components: &Components,
archetypes: &Archetypes,
) {
let Some(location) = entities.get(trigger.event().0) else {
warn!("failed to report entity components; had no location");
return;
};
let Some(archetype) = archetypes.get(location.archetype_id) else {
warn!("failed to report entity components; had no archetype");
return;
};
let mut output = format!("Entity {:?} Components: ", trigger.event().0);
for &component in archetype.components() {
if let Some(name) = components.get_name(component) {
output.push_str(&format!("{name}, "));
}
}
info!("{}; Caller: {}", output, trigger.caller());
}

View File

@@ -1,32 +0,0 @@
[package]
name = "shared"
version = "0.1.0"
edition = "2024"
[features]
client = []
server = []
dbg = []
[dependencies]
avian3d = { workspace = true }
bevy = { workspace = true, default-features = false }
bevy-inspector-egui = { workspace = true, optional = true }
bevy-steamworks = { workspace = true }
bevy_asset_loader = { workspace = true }
bevy_ballistic = { workspace = true }
bevy_common_assets = { workspace = true }
bevy_debug_log = { workspace = true }
bevy_replicon = { workspace = true }
bevy_sprite3d = { workspace = true }
bevy_trenchbroom = { workspace = true }
happy_feet = { workspace = true }
nil = { workspace = true }
rand = { workspace = true }
ron = { workspace = true }
serde = { workspace = true }
steamworks = { workspace = true }
[lints.clippy]
too_many_arguments = "allow"
type_complexity = "allow"

View File

@@ -1,91 +0,0 @@
use crate::{GameState, physics_layers::GameLayer, tb_entities::SecretHead};
use avian3d::prelude::*;
use bevy::prelude::*;
#[derive(Event, Reflect)]
pub struct HeadDrops {
pub pos: Vec3,
pub head_id: usize,
pub impulse: bool,
}
impl HeadDrops {
pub fn new(pos: Vec3, head_id: usize) -> Self {
Self {
pos,
head_id,
impulse: true,
}
}
fn new_static(pos: Vec3, head_id: usize) -> Self {
Self {
pos,
head_id,
impulse: false,
}
}
}
#[derive(Component, Reflect)]
#[reflect(Component)]
pub struct HeadDrop {
pub head_id: usize,
}
#[derive(Component, Reflect)]
#[reflect(Component)]
pub struct HeadDropEnableTime(pub f32);
#[derive(Component, Reflect)]
#[reflect(Component)]
pub struct SecretHeadMarker;
#[derive(EntityEvent, Reflect)]
pub struct HeadCollected {
pub entity: Entity,
pub head: usize,
}
pub fn plugin(app: &mut App) {
app.register_type::<HeadDrop>();
app.register_type::<HeadDropEnableTime>();
app.register_type::<SecretHeadMarker>();
app.add_systems(
Update,
enable_collectible.run_if(in_state(GameState::Playing)),
);
app.add_systems(OnEnter(GameState::Playing), spawn);
}
fn spawn(mut commands: Commands, query: Query<(Entity, &GlobalTransform, &SecretHead)>) {
for (e, t, head) in query {
commands.trigger(HeadDrops::new_static(
t.translation() + Vec3::new(0., 2., 0.),
head.head_id,
));
commands.entity(e).despawn();
}
}
fn enable_collectible(
mut commands: Commands,
query: Query<(Entity, &HeadDropEnableTime)>,
time: Res<Time>,
) {
let now = time.elapsed_secs();
for (e, enable_time) in query.iter() {
if now > enable_time.0 {
commands
.entity(e)
.insert(CollisionLayers::new(
LayerMask(GameLayer::CollectibleSensors.to_bits()),
LayerMask::ALL,
))
.remove::<HeadDropEnableTime>();
}
}
}

View File

@@ -1,56 +0,0 @@
pub mod abilities;
pub mod ai;
pub mod aim;
pub mod animation;
pub mod backpack;
pub mod camera;
pub mod cash;
pub mod cash_heal;
pub mod character;
pub mod control;
pub mod cutscene;
pub mod gates;
pub mod head;
pub mod head_drop;
pub mod heads;
pub mod heads_database;
pub mod hitpoints;
pub mod keys;
pub mod loading_assets;
pub mod loading_map;
pub mod movables;
pub mod npc;
pub mod physics_layers;
pub mod platforms;
pub mod player;
pub mod protocol;
pub mod steam;
pub mod tb_entities;
pub mod tick;
pub mod utils;
pub mod water;
use bevy::{core_pipeline::tonemapping::Tonemapping, prelude::*};
use utils::{billboards, squish_animation};
pub const HEDZ_GREEN: Srgba = Srgba::rgb(0.0, 1.0, 0.0);
pub const HEDZ_PURPLE: Srgba = Srgba::rgb(91. / 256., 4. / 256., 138. / 256.);
#[derive(Resource, Reflect, Debug)]
#[reflect(Resource)]
pub struct DebugVisuals {
pub unlit: bool,
pub tonemapping: Tonemapping,
pub exposure: f32,
pub shadows: bool,
pub cam_follow: bool,
}
#[derive(States, Default, Clone, Copy, Eq, PartialEq, Debug, Hash)]
pub enum GameState {
#[default]
AssetLoading,
MapLoading,
Connecting,
Playing,
}

View File

@@ -1,118 +0,0 @@
use crate::{
GameState,
abilities::PlayerTriggerState,
cash::{Cash, CashCollectEvent},
character::HedzCharacter,
protocol::PlayerId,
};
use crate::{backpack::backpack_ui::BackpackUiState, control::LocalInputs};
use avian3d::prelude::*;
use bevy::{
input::common_conditions::input_just_pressed,
prelude::*,
window::{CursorGrabMode, CursorOptions, PrimaryWindow},
};
use happy_feet::debug::DebugInput;
use serde::{Deserialize, Serialize};
#[derive(Component, Default, Serialize, Deserialize, PartialEq)]
#[require(HedzCharacter, DebugInput = DebugInput, PlayerTriggerState)]
pub struct Player;
#[derive(Component, Debug, Reflect)]
#[reflect(Component)]
#[require(LocalInputs, BackpackUiState)]
pub struct LocalPlayer;
#[derive(Component, Default, Serialize, Deserialize, PartialEq)]
#[require(Transform, Visibility)]
pub struct PlayerBodyMesh;
/// Server-side only; inserted on each `client` (not the controller) to track player ids.
#[derive(Component, Clone, Copy)]
pub struct ClientPlayerId(pub PlayerId);
pub fn plugin(app: &mut App) {
app.add_systems(
OnEnter(GameState::Playing),
(toggle_cursor_system, cursor_recenter),
);
app.add_systems(
Update,
(
collect_cash,
setup_animations_marker_for_player,
toggle_cursor_system.run_if(input_just_pressed(KeyCode::Escape)),
)
.run_if(in_state(GameState::Playing)),
);
}
fn cursor_recenter(q_windows: Single<&mut Window, With<PrimaryWindow>>) {
let mut primary_window = q_windows;
let center = Vec2::new(
primary_window.resolution.width() / 2.,
primary_window.resolution.height() / 2.,
);
primary_window.set_cursor_position(Some(center));
}
fn toggle_grab_cursor(options: &mut CursorOptions) {
match options.grab_mode {
CursorGrabMode::None => {
options.grab_mode = CursorGrabMode::Confined;
options.visible = false;
}
_ => {
options.grab_mode = CursorGrabMode::None;
options.visible = true;
}
}
}
fn toggle_cursor_system(mut window: Single<&mut CursorOptions, With<PrimaryWindow>>) {
toggle_grab_cursor(&mut window);
}
fn collect_cash(
mut commands: Commands,
mut collision_message_reader: MessageReader<CollisionStart>,
query_player: Query<&Player>,
query_cash: Query<&Cash>,
) {
for CollisionStart {
collider1: e1,
collider2: e2,
..
} in collision_message_reader.read()
{
let collect = if query_player.contains(*e1) && query_cash.contains(*e2) {
Some(*e2)
} else if query_player.contains(*e2) && query_cash.contains(*e1) {
Some(*e1)
} else {
None
};
if let Some(cash) = collect {
commands.trigger(CashCollectEvent);
commands.entity(cash).despawn();
}
}
}
fn setup_animations_marker_for_player(
mut commands: Commands,
animation_handles: Query<Entity, Added<AnimationGraphHandle>>,
child_of: Query<&ChildOf>,
player_rig: Query<&ChildOf, With<PlayerBodyMesh>>,
) {
for animation_rig in animation_handles.iter() {
for ancestor in child_of.iter_ancestors(animation_rig) {
if let Ok(rig_child_of) = player_rig.get(ancestor) {
commands.entity(rig_child_of.parent());
return;
}
}
}
}

View File

@@ -1,25 +0,0 @@
use bevy::prelude::*;
use bevy_steamworks::SteamworksPlugin;
pub fn plugin(app: &mut App) {
let app_id = 1603000;
// should only be done in production builds
#[cfg(not(debug_assertions))]
if steamworks::restart_app_if_necessary(app_id.into()) {
info!("Restarting app via steam");
return;
}
info!("steam app init: {app_id}");
match SteamworksPlugin::init_app(app_id) {
Ok(plugin) => {
info!("steam app init done");
app.add_plugins(plugin);
}
Err(e) => {
warn!("steam init error: {e:?}");
}
};
}

View File

@@ -1,25 +0,0 @@
#[macro_export]
macro_rules! global_observer {
($app:expr, $($system:tt)*) => {{
$app.world_mut()
.add_observer($($system)*)
.insert(global_observer!(@name $($system)*))
}};
(@name $system:ident ::< $($param:ident),+ $(,)? >) => {{
let mut name = String::new();
name.push_str(stringify!($system));
name.push_str("::<");
$(
name.push_str(std::any::type_name::<$param>());
)+
name.push_str(">");
Name::new(name)
}};
(@name $system:expr) => {
Name::new(stringify!($system))
};
}
pub use global_observer;