From 2f5d154d2685a4921dc7a9403e4fb63a3948bfbb Mon Sep 17 00:00:00 2001 From: PROMETHIA-27 <42193387+PROMETHIA-27@users.noreply.github.com> Date: Fri, 26 Sep 2025 21:59:08 -0400 Subject: [PATCH] Client/Server Feature Split (#63) --- .github/workflows/archive_steamos.yml | 7 +- .github/workflows/archive_win.yml | 7 +- .github/workflows/ci.yml | 3 +- .github/workflows/ci_debug.yml | 10 +- .github/workflows/steam_alpha.yml | 27 ++-- .gitignore | 1 + Cargo.lock | 132 ++++++++++++++-- Cargo.toml | 43 ++++- crates/client/Cargo.toml | 9 +- crates/client/src/client.rs | 149 +++++++++++++++--- crates/client/src/enemy.rs | 14 ++ crates/client/src/main.rs | 4 +- crates/server/Cargo.toml | 10 +- crates/server/src/config.rs | 20 +++ crates/server/src/main.rs | 89 ++++++++--- crates/server/src/server.rs | 70 ++++++-- crates/shared/Cargo.toml | 4 +- crates/shared/src/abilities/curver.rs | 54 +++---- crates/shared/src/abilities/missile.rs | 82 +++++----- crates/shared/src/abilities/mod.rs | 5 +- crates/shared/src/abilities/thrown.rs | 57 ++++--- .../shared/src/control/controller_running.rs | 4 +- crates/shared/src/control/controls.rs | 16 +- crates/shared/src/lib.rs | 1 + crates/shared/src/loading_map.rs | 10 +- crates/shared/src/npc.rs | 38 +++-- crates/shared/src/player.rs | 32 ++-- crates/shared/src/utils/commands.rs | 20 +-- crates/shared/src/utils/triggers.rs | 4 +- justfile | 23 ++- 30 files changed, 674 insertions(+), 271 deletions(-) create mode 100644 crates/client/src/enemy.rs create mode 100644 crates/server/src/config.rs diff --git a/.github/workflows/archive_steamos.yml b/.github/workflows/archive_steamos.yml index fbbeb56..d43a820 100644 --- a/.github/workflows/archive_steamos.yml +++ b/.github/workflows/archive_steamos.yml @@ -23,12 +23,13 @@ jobs: - name: Build run: | - cargo build --release --locked --target=x86_64-unknown-linux-gnu + cargo build --release --locked --target=x86_64-unknown-linux-gnu --bin hedz_reloaded --no-default-features --features shared/client + cargo build --release --locked --target=x86_64-unknown-linux-gnu --bin server --no-default-features --features shared/server - name: Archive run: | - cp target/x86_64-unknown-linux-gnu/release/hedz_reloaded hedz_reloaded - tar -czf steamos.tar.gz hedz_reloaded + cp target/x86_64-unknown-linux-gnu/release/hedz_reloaded target/x86_64-unknown-linux-gnu/release/server ./ + tar -czf steamos.tar.gz hedz_reloaded server - uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/archive_win.yml b/.github/workflows/archive_win.yml index 07188d8..426c2f0 100644 --- a/.github/workflows/archive_win.yml +++ b/.github/workflows/archive_win.yml @@ -28,13 +28,14 @@ jobs: - name: Build binaries (Windows) run: | - cargo xwin build --locked --release --target=x86_64-pc-windows-msvc + cargo xwin build --locked --release --target=x86_64-pc-windows-msvc --bin hedz_reloaded --no-default-features --features shared/client + cargo xwin build --locked --release --target=x86_64-pc-windows-msvc --bin server --no-default-features --features shared/server - name: Archive run: | ls -lisa target/x86_64-pc-windows-msvc/release/ - cp target/x86_64-pc-windows-msvc/release/hedz_reloaded.exe hedz_reloaded.exe - tar -czf win.tar.gz hedz_reloaded.exe + cp target/x86_64-pc-windows-msvc/release/hedz_reloaded.exe target/x86_64-pc-windows-msvc/release/server.exe ./ + tar -czf win.tar.gz hedz_reloaded.exe server.exe - uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a200fe3..58f3a51 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,7 +39,8 @@ jobs: - name: Build run: | - cargo build + cargo build --bin hedz_reloaded --no-default-features --features shared/client + cargo build --bin server --no-default-features --features shared/server - name: Tests run: | diff --git a/.github/workflows/ci_debug.yml b/.github/workflows/ci_debug.yml index 21bb7b5..87e5f1c 100644 --- a/.github/workflows/ci_debug.yml +++ b/.github/workflows/ci_debug.yml @@ -23,7 +23,11 @@ jobs: - name: Build client run: | - cargo build --locked --target=x86_64-unknown-linux-gnu + cargo build --locked --target=x86_64-unknown-linux-gnu --bin hedz_reloaded --no-default-features --features shared/client + + - name: Build server + run: | + cargo build --locked --target=x86_64-unknown-linux-gnu --bin server --no-default-features --features shared/server - name: Lints run: | @@ -31,8 +35,8 @@ jobs: - name: Archive run: | - cp target/x86_64-unknown-linux-gnu/debug/hedz_reloaded hedz_reloaded - tar -czf steamos-debug.tar.gz hedz_reloaded + cp target/x86_64-unknown-linux-gnu/debug/hedz_reloaded target/x86_64-unknown-linux-gnu/debug/server ./ + tar -czf steamos-debug.tar.gz hedz_reloaded server - uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/steam_alpha.yml b/.github/workflows/steam_alpha.yml index 523a373..27e251c 100644 --- a/.github/workflows/steam_alpha.yml +++ b/.github/workflows/steam_alpha.yml @@ -22,14 +22,17 @@ jobs: - name: Build (lipo) run: | - cargo build --release --target=x86_64-apple-darwin - cargo build --release --target=aarch64-apple-darwin + cargo build --release --target=x86_64-apple-darwin --bin hedz_reloaded --no-default-features --features shared/client + cargo build --release --target=x86_64-apple-darwin --bin server --no-default-features --features shared/server + cargo build --release --target=aarch64-apple-darwin --bin hedz_reloaded --no-default-features --features shared/client + cargo build --release --target=aarch64-apple-darwin --bin server --no-default-features --features shared/server lipo -create -output target/release/hedz_reloaded target/aarch64-apple-darwin/release/hedz_reloaded target/x86_64-apple-darwin/release/hedz_reloaded + lipo -create -output target/release/server target/aarch64-apple-darwin/release/server target/x86_64-apple-darwin/release/server - name: Archive run: | - cp target/release/hedz_reloaded ./hedz_reloaded - tar -czf hedz-macos.tar.gz hedz_reloaded + cp target/release/hedz_reloaded target/release/server ./ + tar -czf hedz-macos.tar.gz hedz_reloaded server ls -lisah hedz-macos.tar.gz - uses: actions/upload-artifact@v4 @@ -60,7 +63,7 @@ jobs: rm -rf $APP_ROOT/* | true mkdir -p $APP_ROOT/assets cp -r assets/* $APP_ROOT/assets - cp hedz_reloaded $APP_ROOT/ + cp hedz_reloaded server $APP_ROOT/ cp build/macos/libsteam_api.dylib $APP_ROOT/ - uses: ./.github/actions/steamcmd @@ -111,12 +114,13 @@ jobs: - name: Build run: | - cargo build --locked --release --target=x86_64-unknown-linux-gnu + cargo build --locked --release --target=x86_64-unknown-linux-gnu --bin hedz_reloaded --no-default-features --features shared/client + cargo build --locked --release --target=x86_64-unknown-linux-gnu --bin server --no-default-features --features shared/server - name: Archive run: | - cp target/x86_64-unknown-linux-gnu/release/hedz_reloaded hedz_reloaded - tar -czf steamos.tar.gz hedz_reloaded + cp target/x86_64-unknown-linux-gnu/release/hedz_reloaded target/x86_64-unknown-linux-gnu/release/server ./ + tar -czf steamos.tar.gz hedz_reloaded server - uses: actions/upload-artifact@v4 with: @@ -126,7 +130,7 @@ jobs: - name: Copy Binary for SteamOS run: | mkdir -p build/steamos/content - cp target/x86_64-unknown-linux-gnu/release/hedz_reloaded build/steamos/content/hedz_reloaded + cp target/x86_64-unknown-linux-gnu/release/hedz_reloaded target/x86_64-unknown-linux-gnu/release/server build/steamos/content/ - name: Install SteamCMD run: | @@ -193,12 +197,13 @@ jobs: - name: Build binaries (Windows) run: | - cargo xwin build --locked --release --target=x86_64-pc-windows-msvc + cargo xwin build --locked --release --target=x86_64-pc-windows-msvc --bin hedz_reloaded --no-default-features --features shared/client + cargo xwin build --locked --release --target=x86_64-pc-windows-msvc --bin server --no-default-features --features shared/server - name: Move binary run: | ls -lisa target/x86_64-pc-windows-msvc/release/ - cp target/x86_64-pc-windows-msvc/release/hedz_reloaded.exe build/win/hedz_reloaded.exe + cp target/x86_64-pc-windows-msvc/release/hedz_reloaded.exe target/x86_64-pc-windows-msvc/release/server.exe build/win/ - uses: ./.github/actions/steamcmd with: diff --git a/.gitignore b/.gitignore index f8a6636..c020670 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ build/steamos/hedz_reloaded build/steamos/.env build/macos/src/HEDZReloaded.app/Contents/MacOS build/macos/src/Applications +server.log \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index e5d6f0c..76b09f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -280,6 +280,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.60.2", +] + [[package]] name = "anyhow" version = "1.0.98" @@ -2257,6 +2307,46 @@ dependencies = [ "libloading", ] +[[package]] +name = "clap" +version = "4.5.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + [[package]] name = "clipboard-win" version = "5.4.0" @@ -2282,6 +2372,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "combine" version = "4.6.7" @@ -4038,6 +4134,12 @@ dependencies = [ "libc", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itertools" version = "0.12.1" @@ -5900,6 +6002,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + [[package]] name = "oneshot" version = "0.1.11" @@ -6793,9 +6901,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.25" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustc-hash" @@ -6846,9 +6954,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.28" +version = "0.23.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" +checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1" dependencies = [ "once_cell", "ring", @@ -6891,9 +6999,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.3" +version = "0.103.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" dependencies = [ "ring", "rustls-pki-types", @@ -7091,17 +7199,13 @@ dependencies = [ "bevy-inspector-egui", "bevy-steamworks", "bevy-ui-gradients", - "bevy_asset_loader", - "bevy_ballistic", "bevy_common_assets", - "bevy_debug_log", "bevy_sprite3d", "bevy_trenchbroom", + "clap", "happy_feet", "lightyear", "lightyear_avian3d 0.21.1", - "nil 0.14.0", - "rand 0.8.5", "ron", "serde", "shared", @@ -7988,6 +8092,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.17.0" diff --git a/Cargo.toml b/Cargo.toml index 772de66..316a074 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,43 @@ avian3d = { version = "0.3", default-features = false, features = [ "parallel", "serialize", ] } -bevy = { version = "0.16.0", features = ["track_location"] } +bevy = { version = "0.16.0", default-features = false, features = [ + "animation", + "async_executor", + "bevy_asset", + "bevy_audio", + "bevy_color", + "bevy_core_pipeline", + "bevy_gilrs", + "bevy_gizmos", + "bevy_gltf", + "bevy_input_focus", + "bevy_log", + "bevy_mesh_picking_backend", + "bevy_pbr", + "bevy_picking", + "bevy_render", + "bevy_scene", + "bevy_sprite", + "bevy_sprite_picking_backend", + "bevy_state", + "bevy_text", + "bevy_ui", + "bevy_ui_picking_backend", + "custom_cursor", + "default_font", + "hdr", + "multi_threaded", + "png", + "smaa_luts", + "std", + "sysinfo_plugin", + "tonemapping_luts", + "vorbis", + "webgl2", + "x11", + "track_location", +] } bevy-inspector-egui = { version = "0.31" } bevy-steamworks = "0.13.0" bevy-ui-gradients = "0.4.0" @@ -22,7 +58,9 @@ bevy_ballistic = "0.4.0" bevy_common_assets = { version = "0.13.0", features = ["ron"] } bevy_debug_log = "0.6.0" bevy_sprite3d = "5.0.0" -bevy_trenchbroom = { version = "0.8.1", features = ["avian"] } +bevy_trenchbroom = { version = "0.8.1", default-features = false, features = [ + "avian", +] } happy_feet = { git = "https://github.com/atornity/happy_feet.git", rev = "1b24ed95f166e63af35e7b6f9f0053d6d28e1f1a", features = [ "serde", ] } @@ -33,7 +71,6 @@ lightyear = { version = "0.22.4", default-features = false, features = [ "prediction", "replication", "std", - "steam", "udp", ] } lightyear_avian3d = { git = "https://github.com/cBournhonesque/lightyear.git", rev = "03cbf419a2c0595261b64420bc0332fc3fe1cc3f" } diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index 4692710..da5ef21 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -5,12 +5,15 @@ edition = "2024" build = "build.rs" [features] -default = ["lightyear/client"] +default = ["shared/client"] dbg = ["avian3d/debug-plugin", "dep:bevy-inspector-egui", "shared/dbg"] [dependencies] avian3d = { workspace = true } -bevy = { workspace = true } +bevy = { workspace = true, default-features = false, features = [ + "bevy_window", + "bevy_winit", +] } bevy-inspector-egui = { workspace = true, optional = true } bevy-steamworks = { workspace = true } bevy-ui-gradients = { workspace = true } @@ -19,7 +22,7 @@ bevy_ballistic = { workspace = true } bevy_common_assets = { workspace = true } bevy_debug_log = { workspace = true } bevy_sprite3d = { workspace = true } -bevy_trenchbroom = { workspace = true } +bevy_trenchbroom = { workspace = true, features = ["client"] } happy_feet = { workspace = true } lightyear = { workspace = true } nil = { workspace = true } diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 25afb8f..19e5c39 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -1,26 +1,48 @@ -use bevy::prelude::*; +use bevy::{prelude::*, utils::synccell::SyncCell}; use lightyear::{ - connection::client::ClientState, netcode::Key, prelude::{client::NetcodeConfig, input::native::InputMarker, *}, }; -use shared::{ - GameState, control::ControlState, global_observer, heads_database::HeadsDatabase, - player::Player, tb_entities::SpawnPoint, +use nil::prelude::Mutex; +use shared::{GameState, control::ControlState, global_observer, player::Player}; +use std::{ + env::current_exe, + io::{BufRead, BufReader}, + net::{IpAddr, Ipv4Addr, SocketAddr}, + process::Stdio, + sync::{LazyLock, mpsc}, }; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; + +/// Cache of server processes to be cleared at process exit +static SERVER_PROCESSES: LazyLock>> = LazyLock::new(Mutex::default); pub fn plugin(app: &mut App) { - app.add_systems(Startup, temp_connect_on_startup); + app.add_systems(OnEnter(GameState::Connecting), attempt_connection); app.add_systems( - FixedUpdate, - spawn_disconnected_player.run_if(in_state(GameState::Playing)), + Update, + parse_local_server_stdout.run_if(resource_exists::), ); + app.add_systems(Last, close_server_processes); + global_observer!(app, on_connecting); + global_observer!(app, on_connection_failed); + global_observer!(app, on_connection_succeeded); global_observer!(app, temp_give_player_marker); + global_observer!(app, connect_on_local_server_started); } -fn temp_connect_on_startup(mut commands: Commands) -> Result { +fn close_server_processes(mut app_exit: EventReader) { + 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}"); + } + } + } +} + +fn attempt_connection(mut commands: Commands) -> Result { let mut args = std::env::args(); let client_port = loop { match args.next().as_deref() { @@ -44,9 +66,9 @@ fn temp_connect_on_startup(mut commands: Commands) -> Result { .spawn(( Name::from("Client"), Client::default(), + Link::new(None), LocalAddr(client_addr), PeerAddr(server_addr), - Link::new(None), ReplicationReceiver::default(), client::NetcodeClient::new( auth, @@ -62,15 +84,104 @@ fn temp_connect_on_startup(mut commands: Commands) -> Result { Ok(()) } -fn spawn_disconnected_player( - disconnected: Single<&Client, Changed>, - commands: Commands, - asset_server: Res, - query: Query<&Transform, With>, - heads_db: Res, +fn on_connection_succeeded( + _trigger: Trigger, + state: Res>, + mut change_state: ResMut>, ) { - if disconnected.state == ClientState::Disconnected { - shared::player::spawn(commands, Entity::PLACEHOLDER, query, asset_server, heads_db) + if *state == GameState::Connecting { + change_state.set(GameState::Playing); + } +} + +/// A client starts `Disconnected`, so in order to tell if it *actually* failed to connect/disconnected +/// vs. simply having been created, we need some extra state. +#[derive(Component)] +struct ClientActive; + +fn on_connecting(trigger: Trigger, mut commands: Commands) { + commands.entity(trigger.target()).insert(ClientActive); +} + +#[derive(Resource)] +struct LocalServerStdout(SyncCell>); + +fn on_connection_failed( + trigger: Trigger, + disconnected: Query<&Disconnected>, + mut commands: Commands, + client_active: Query<&ClientActive>, + mut opened_server: Local, +) { + let disconnected = disconnected.get(trigger.target()).unwrap(); + if *opened_server { + panic!( + "failed to connect to local server: {:?}", + disconnected.reason + ); + } + + let client = trigger.target(); + if client_active.contains(client) { + commands.entity(client).remove::(); + + // 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 mut server_process = std::process::Command::new(exe_path) + .args(["--timeout", "60", "--close-on-client-disconnect"]) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .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))); + + *opened_server = true; + } +} + +#[derive(Event)] +struct LocalServerStarted; + +fn parse_local_server_stdout(mut commands: Commands, mut stdout: ResMut) { + let stdout: &mut LocalServerStdout = &mut stdout; + + while let Ok(line) = stdout.0.get().try_recv() { + if let "hedz.server_started" = &line[..] { + commands.trigger(LocalServerStarted); + } + } +} + +fn connect_on_local_server_started( + _trigger: Trigger, + state: Res>, + mut commands: Commands, + client: Single>, +) { + if *state == GameState::Connecting { + commands.entity(*client).trigger(Connect); } } diff --git a/crates/client/src/enemy.rs b/crates/client/src/enemy.rs new file mode 100644 index 0000000..b824dac --- /dev/null +++ b/crates/client/src/enemy.rs @@ -0,0 +1,14 @@ +use bevy::prelude::*; +use shared::{GameState, tb_entities::EnemySpawn}; + +pub fn plugin(app: &mut App) { + app.add_systems(OnExit(GameState::MapLoading), despawn_enemy_spawns); +} + +/// Despawn enemy spawners because only the server will ever spawn enemies with them, and they have a +/// collider. +fn despawn_enemy_spawns(mut commands: Commands, enemy_spawns: Query>) { + for spawner in enemy_spawns.iter() { + commands.entity(spawner).despawn(); + } +} diff --git a/crates/client/src/main.rs b/crates/client/src/main.rs index 67d4a47..1ea7764 100644 --- a/crates/client/src/main.rs +++ b/crates/client/src/main.rs @@ -1,5 +1,6 @@ mod client; mod debug; +mod enemy; mod steam; mod ui; @@ -47,7 +48,7 @@ fn main() { ..default() }) .set(bevy::log::LogPlugin { - filter: "info,lightyear_replication=off".into(), + 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, @@ -90,6 +91,7 @@ fn main() { app.add_plugins(animation::plugin); app.add_plugins(character::plugin); app.add_plugins(cash::plugin); + app.add_plugins(enemy::plugin); app.add_plugins(player::plugin); app.add_plugins(gates::plugin); app.add_plugins(platforms::plugin); diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml index 5564d3a..227beeb 100644 --- a/crates/server/Cargo.toml +++ b/crates/server/Cargo.toml @@ -4,26 +4,22 @@ version = "0.1.0" edition = "2024" [features] -default = ["lightyear/server"] +default = ["shared/server"] dbg = ["avian3d/debug-plugin", "dep:bevy-inspector-egui", "shared/dbg"] [dependencies] avian3d = { workspace = true } -bevy = { workspace = true } +bevy = { workspace = true, default-features = false } bevy-inspector-egui = { workspace = true, optional = true } bevy-steamworks = { workspace = true } bevy-ui-gradients = { workspace = true } -bevy_asset_loader = { workspace = true } -bevy_ballistic = { workspace = true } bevy_common_assets = { workspace = true } -bevy_debug_log = { workspace = true } bevy_sprite3d = { workspace = true } bevy_trenchbroom = { workspace = true } +clap = { version = "=4.5.47", features = ["derive"] } happy_feet = { workspace = true } lightyear = { workspace = true } lightyear_avian3d = { workspace = true } -nil = { workspace = true } -rand = { workspace = true } ron = { workspace = true } serde = { workspace = true } shared = { workspace = true } diff --git a/crates/server/src/config.rs b/crates/server/src/config.rs new file mode 100644 index 0000000..6367c9e --- /dev/null +++ b/crates/server/src/config.rs @@ -0,0 +1,20 @@ +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, +} diff --git a/crates/server/src/main.rs b/crates/server/src/main.rs index f5b9b75..c6eb985 100644 --- a/crates/server/src/main.rs +++ b/crates/server/src/main.rs @@ -1,6 +1,12 @@ use crate::utils::{auto_rotate, explosions}; use avian3d::prelude::*; -use bevy::{audio::Volume, core_pipeline::tonemapping::Tonemapping, prelude::*}; +use bevy::{ + app::plugin_group, + audio::Volume, + core_pipeline::tonemapping::Tonemapping, + log::{BoxedLayer, tracing_subscriber::Layer}, + prelude::*, +}; use bevy_common_assets::ron::RonAssetPlugin; use bevy_sprite3d::Sprite3dPlugin; use bevy_trenchbroom::prelude::*; @@ -11,8 +17,61 @@ use shared::*; use std::time::Duration; use utils::{billboards, sprite_3d_animation, squish_animation, trail}; +mod config; mod server; +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::render:::RenderPlugin, + bevy::render::texture:::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::audio:::AudioPlugin, + bevy::gilrs:::GilrsPlugin, + bevy::animation:::AnimationPlugin, + bevy::gizmos:::GizmoPlugin, + bevy::state::app:::StatesPlugin, + #[plugin_group] + bevy::picking:::DefaultPickingPlugins, + } +} + +pub fn log_to_file_layer(_app: &mut App) -> Option { + let file = std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open("server.log") + .ok()?; + Some( + bevy::log::tracing_subscriber::fmt::layer() + .with_writer(std::sync::Mutex::new(file)) + .with_ansi(false) + .with_file(true) + .with_line_number(true) + .boxed(), + ) +} + fn main() { let mut app = App::new(); @@ -26,28 +85,13 @@ fn main() { 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".into(), - level: bevy::log::Level::INFO, - // provide custom log layer to receive logging events - custom_layer: bevy_debug_log::log_capture_layer, - }), - ); + 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 + custom_layer: log_to_file_layer, + })); - app.add_plugins( - bevy_debug_log::LogViewerPlugin::default() - .auto_open_threshold(bevy::log::tracing::level_filters::LevelFilter::OFF), - ); app.add_plugins(ServerPlugins { tick_duration: Duration::from_secs_f32(1.0 / 60.0), }); @@ -78,6 +122,7 @@ fn main() { app.add_plugins(animation::plugin); app.add_plugins(character::plugin); app.add_plugins(cash::plugin); + app.add_plugins(config::plugin); app.add_plugins(player::plugin); app.add_plugins(gates::plugin); app.add_plugins(platforms::plugin); diff --git a/crates/server/src/server.rs b/crates/server/src/server.rs index 2ca8e72..b692ad2 100644 --- a/crates/server/src/server.rs +++ b/crates/server/src/server.rs @@ -1,16 +1,25 @@ +use crate::config::ServerConfig; use bevy::prelude::*; use lightyear::prelude::{ - server::{NetcodeConfig, NetcodeServer, ServerUdpIo}, + server::{NetcodeConfig, NetcodeServer, ServerUdpIo, Started}, *, }; -use shared::{heads_database::HeadsDatabase, tb_entities::SpawnPoint, utils::commands::IsServer}; +use shared::{GameState, global_observer, heads_database::HeadsDatabase, tb_entities::SpawnPoint}; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; pub fn plugin(app: &mut App) { - app.init_resource::(); + app.add_systems(Startup, (start_server, setup_timeout_timer)); + app.add_systems( + Update, + ( + notify_started.run_if(in_state(GameState::Playing)), + run_timeout, + ), + ); - app.add_systems(Startup, start_server); - app.add_observer(handle_new_client); + global_observer!(app, handle_new_client); + global_observer!(app, close_on_disconnect); + global_observer!(app, cancel_timeout); } fn handle_new_client( @@ -34,17 +43,52 @@ fn handle_new_client( Ok(()) } +fn close_on_disconnect( + _trigger: Trigger, + config: Res, + mut writer: EventWriter, +) { + if config.close_on_client_disconnect { + writer.write(AppExit::Success); + } +} + fn start_server(mut commands: Commands) -> Result { let server_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 25565); - commands - .spawn(( - Name::from("Server"), - LocalAddr(server_addr), - ServerUdpIo::default(), - NetcodeServer::new(NetcodeConfig::default()), - )) - .trigger(server::Start); + let mut commands = commands.spawn(( + Name::from("Server"), + LocalAddr(server_addr), + ServerUdpIo::default(), + NetcodeServer::new(NetcodeConfig::default()), + )); + commands.trigger(server::Start); Ok(()) } + +fn notify_started(started: Query<&Started>, mut notified: Local) { + if !*notified && !started.is_empty() { + println!("hedz.server_started"); + *notified = true; + } +} + +#[derive(Resource)] +struct TimeoutTimer(f32); + +fn setup_timeout_timer(mut commands: Commands, config: Res) { + commands.insert_resource(TimeoutTimer(config.timeout)); +} + +fn run_timeout(mut timer: ResMut, mut writer: EventWriter, time: Res