diff --git a/Cargo.lock b/Cargo.lock index 4b7dbe4..3c04bbb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1519,6 +1519,7 @@ dependencies = [ "bevy_time", "renet", "renet_netcode", + "renet_steam", ] [[package]] @@ -5564,6 +5565,18 @@ dependencies = [ "renetcode", ] +[[package]] +name = "renet_steam" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f6018afe469d3d2d49fab8fd1cecc46c588ac61498cad879d5781c44b277421" +dependencies = [ + "bevy_ecs", + "log", + "renet", + "steamworks", +] + [[package]] name = "renetcode" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index 888b271..cfeb99b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,7 +61,8 @@ bevy_pkv = { version = "0.14", default-features = false, features = [ "redb", ] } bevy_replicon = "0.37.1" -bevy_replicon_renet = "0.13.0" +# TODO: i dont think we need this in dedicated server mode +bevy_replicon_renet = { version = "0.13.0", features = ["renet_steam"] } bevy_sprite3d = "7.0.0" bevy_trenchbroom = { version = "0.10", default-features = false, features = [ "physics-integration", @@ -76,6 +77,7 @@ rand = "=0.8.5" ron = "0.8" serde = { version = "1.0.219", features = ["derive"] } shared = { path = "crates/shared" } +# TODO: i dont think we need this in dedicated server mode steamworks = "0.12" [profile.dev.package."*"] diff --git a/crates/hedz_reloaded/src/client/mod.rs b/crates/hedz_reloaded/src/client/mod.rs index 7749d2d..8c1b285 100644 --- a/crates/hedz_reloaded/src/client/mod.rs +++ b/crates/hedz_reloaded/src/client/mod.rs @@ -1,6 +1,6 @@ use crate::{ GameState, - config::NetworkingConfig, + config::NetConfig, protocol::{ ClientEnteredPlaying, TbMapEntityId, TbMapEntityMapping, messages::DespawnTbMapEntity, }, @@ -17,14 +17,10 @@ use bevy_replicon::{ }; use bevy_replicon_renet::{ RenetChannelsExt, - netcode::{ClientAuthentication, NetcodeClientTransport, NetcodeError}, renet::{ConnectionConfig, RenetClient}, }; +use bevy_steamworks::Client; use bevy_trenchbroom::geometry::Brushes; -use std::{ - net::{Ipv4Addr, UdpSocket}, - time::SystemTime, -}; pub mod audio; pub mod backpack; @@ -55,7 +51,7 @@ pub fn plugin(app: &mut App) { app.add_systems( OnEnter(GameState::Connecting), - connect_to_server.run_if(|config: Res| config.server.is_some()), + connect_to_server.run_if(|config: Res| config.is_client()), ); app.add_systems(Update, despawn_absent_map_entities); app.add_systems( @@ -89,8 +85,9 @@ fn on_disconnect() { fn connect_to_server( mut commands: Commands, - config: Res, + config: Res, channels: Res, + steam_client: Option>, ) -> Result { let server_channels_config = channels.server_configs(); let client_channels_config = channels.client_configs(); @@ -102,32 +99,47 @@ fn connect_to_server( }); commands.insert_resource(client); - commands.insert_resource(client_transport(&config)?); + + if let NetConfig::SteamClient(host_steam_id) = &*config { + let Some(steam_client) = steam_client else { + return Err("Steam client not found".into()); + }; + + info!("connecting to steam host: {host_steam_id:?}"); + let transport = bevy_replicon_renet::steam::SteamClientTransport::new( + (**steam_client).clone(), + host_steam_id, + )?; + + commands.insert_resource(transport); + } else if let NetConfig::NetcodeClient(host_addr) = &*config { + use std::time::SystemTime; + + info!("connecting to netcode host: {host_addr:?}"); + + let current_time = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(); + let client_id = current_time.as_millis() as u64; + let socket = std::net::UdpSocket::bind((std::net::Ipv4Addr::UNSPECIFIED, 0))?; + let authentication = bevy_replicon_renet::netcode::ClientAuthentication::Unsecure { + client_id, + protocol_id: 0, + server_addr: host_addr.clone(), + user_data: None, + }; + let transport = bevy_replicon_renet::netcode::NetcodeClientTransport::new( + current_time, + authentication, + socket, + )?; + + commands.insert_resource(transport); + } Ok(()) } -fn client_transport(config: &NetworkingConfig) -> Result { - 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, 0))?; - let server_addr = config - .server - .flatten() - .unwrap_or_else(|| "127.0.0.1:31111".parse().unwrap()); - let authentication = ClientAuthentication::Unsecure { - client_id, - protocol_id: 0, - server_addr, - user_data: None, - }; - - info!("attempting connection to {server_addr}"); - NetcodeClientTransport::new(current_time, authentication, socket) -} - #[allow(clippy::type_complexity)] fn migrate_remote_entities( query: Query<(Entity, &TbMapEntityId), (Added, With)>, diff --git a/crates/hedz_reloaded/src/client/settings.rs b/crates/hedz_reloaded/src/client/settings.rs index bdfa561..90d4579 100644 --- a/crates/hedz_reloaded/src/client/settings.rs +++ b/crates/hedz_reloaded/src/client/settings.rs @@ -4,10 +4,11 @@ use bevy_pkv::prelude::*; use crate::{client::audio::SoundSettings, utils::Debounce}; pub fn plugin(app: &mut App) { + #[cfg(not(feature = "dbg"))] app.insert_resource(PkvStore::new("Rustunit", "HEDZ")); - app.add_systems(Update, persist_settings); - app.add_systems(Startup, load_settings); + app.add_systems(Update, persist_settings.run_if(resource_exists::)); + app.add_systems(Startup, load_settings.run_if(resource_exists::)); } fn persist_settings( diff --git a/crates/hedz_reloaded/src/client/steam.rs b/crates/hedz_reloaded/src/client/steam.rs index 7c4f87d..e58e9df 100644 --- a/crates/hedz_reloaded/src/client/steam.rs +++ b/crates/hedz_reloaded/src/client/steam.rs @@ -61,6 +61,14 @@ fn test_steam_system(steam_client: Res) { }, ); + let id = steam_client.user().steam_id(); + + info!("Steam ID: {:?}", id); + + steam_client + .friends() + .set_rich_presence("connect", Some(id.raw().to_string().as_str())); + for friend in steam_client.friends().get_friends(FriendFlags::IMMEDIATE) { info!( "Steam Friend: {:?} - {}({:?})", diff --git a/crates/hedz_reloaded/src/config.rs b/crates/hedz_reloaded/src/config.rs index 8d9970b..582f1f0 100644 --- a/crates/hedz_reloaded/src/config.rs +++ b/crates/hedz_reloaded/src/config.rs @@ -1,25 +1,92 @@ +use std::net::SocketAddr; + use bevy::prelude::*; use clap::Parser; -use std::net::SocketAddr; +use steamworks::SteamId; pub fn plugin(app: &mut App) { let config = NetworkingConfig::parse(); + let config: NetConfig = config.into(); + + info!("net config: {:?}", config); + app.insert_resource(config); } -#[derive(Resource, Parser, Debug)] +#[derive(Parser, Debug)] #[command(version, about, long_about = None)] -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. - /// Does nothing on the server. +struct NetworkingConfig { + /// Steam id of the host to connect to #[arg(long)] - pub server: Option>, - /// Whether or not to open a port when opening the client, for other clients - /// to connect. Does nothing if `server` is set. + pub steam_host_id: Option, + + /// Act as steam host #[arg(long)] - pub host: bool, + pub steam_host: bool, + + /// Act as host using netcode, so we have to define our port + #[arg(long)] + pub netcode_host: Option>, + + /// Host address we connect to as a client + #[arg(long)] + pub netcode_client: Option>, +} + +#[derive(Resource, Debug)] +pub enum NetConfig { + Singleplayer, + SteamHost, + NetcodeHost { port: u16 }, + SteamClient(SteamId), + NetcodeClient(SocketAddr), +} + +impl NetConfig { + pub fn is_client(&self) -> bool { + matches!( + self, + NetConfig::SteamClient(_) | NetConfig::NetcodeClient(_) + ) + } + + pub fn is_host(&self) -> bool { + matches!(self, NetConfig::SteamHost | NetConfig::NetcodeHost { .. }) + } + + pub fn is_singleplayer(&self) -> bool { + !self.is_client() && !self.is_host() + } +} + +impl From for NetConfig { + fn from(config: NetworkingConfig) -> Self { + match ( + config.steam_host, + config.steam_host_id, + config.netcode_host, + config.netcode_client, + ) { + (false, None, None, None) => Self::Singleplayer, + (true, None, None, None) => Self::SteamHost, + (false, Some(id), None, None) => Self::SteamClient(parse_steam_id(id)), + (false, None, Some(port), None) => Self::NetcodeHost { + port: port.unwrap_or(31111), + }, + (false, None, None, Some(addr)) => Self::NetcodeClient(parse_addr(addr)), + _ => panic!("Invalid configuration"), + } + } +} + +fn parse_addr(addr: Option) -> SocketAddr { + addr.map(|addr| addr.parse().ok()) + .flatten() + .unwrap_or_else(|| "127.0.0.1:31111".parse().unwrap()) +} + +fn parse_steam_id(id: String) -> SteamId { + let id: u64 = id.parse().unwrap(); + SteamId::from_raw(id) } diff --git a/crates/hedz_reloaded/src/lib.rs b/crates/hedz_reloaded/src/lib.rs index 62eb293..133b43e 100644 --- a/crates/hedz_reloaded/src/lib.rs +++ b/crates/hedz_reloaded/src/lib.rs @@ -34,7 +34,7 @@ pub mod utils; pub mod water; use crate::{ - config::NetworkingConfig, + config::NetConfig, heads_database::{HeadDatabaseAsset, HeadsDatabase}, protocol::{PlayerIdCounter, messages::AssignClientPlayer}, tb_entities::SpawnPoint, @@ -47,6 +47,7 @@ use bevy_common_assets::ron::RonAssetPlugin; use bevy_replicon::{RepliconPlugins, prelude::ClientId}; use bevy_replicon_renet::RepliconRenetPlugins; use bevy_sprite3d::Sprite3dPlugin; +use bevy_steamworks::SteamworksEvent; use bevy_trenchbroom::{ TrenchBroomPlugins, config::TrenchBroomConfig, prelude::TrenchBroomPhysicsPlugin, }; @@ -148,21 +149,21 @@ pub fn plugin(app: &mut App) { if cfg!(feature = "client") { app.add_systems( OnEnter(GameState::Waiting), - start_solo_client - .run_if(|config: Res| config.server.is_none() && !config.host), + start_solo_client.run_if(|config: Res| config.is_singleplayer()), ); app.add_systems( OnEnter(GameState::Waiting), - start_listen_server - .run_if(|config: Res| config.server.is_none() && config.host), + start_listen_server.run_if(|config: Res| config.is_host()), ); app.add_systems( OnEnter(GameState::Waiting), - start_client.run_if(|config: Res| config.server.is_some()), + start_client.run_if(|config: Res| config.is_client()), ); } else { app.add_systems(OnEnter(GameState::Waiting), start_dedicated_server); } + + app.add_systems(Update, log_steam_events); } #[derive(Resource, Reflect, Debug)] @@ -192,6 +193,17 @@ pub enum GameState { Playing, } +fn log_steam_events(events: Option>) { + let Some(mut events) = events else { + return; + }; + + for event in events.read() { + let SteamworksEvent::CallbackResult(result) = event; + info!("steam: {:?}", result); + } +} + fn start_solo_client( commands: Commands, mut next: ResMut>, diff --git a/crates/hedz_reloaded/src/server.rs b/crates/hedz_reloaded/src/server.rs index dbdcf35..068baf6 100644 --- a/crates/hedz_reloaded/src/server.rs +++ b/crates/hedz_reloaded/src/server.rs @@ -1,5 +1,7 @@ use crate::{ - GameState, global_observer, + GameState, + config::NetConfig, + global_observer, heads_database::HeadsDatabase, player::ClientPlayerId, protocol::{ClientEnteredPlaying, PlayerIdCounter, SetGameTick, messages::AssignClientPlayer}, @@ -16,12 +18,8 @@ use bevy_replicon::{ }; use bevy_replicon_renet::{ RenetChannelsExt, - netcode::{NetcodeServerTransport, ServerAuthentication}, renet::{ConnectionConfig, RenetServer}, -}; -use std::{ - net::{Ipv4Addr, UdpSocket}, - time::SystemTime, + steam::SteamServerTransport, }; pub fn plugin(app: &mut App) { @@ -59,6 +57,8 @@ fn open_renet_server( mut commands: Commands, channels: Res, mut next: ResMut>, + steam_client: Option>, + config: Res, ) -> Result<(), BevyError> { info!("opening server"); @@ -71,22 +71,48 @@ fn open_renet_server( ..Default::default() }); - let current_time = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?; - let port = 31111; - let socket = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, port))?; - let server_config = bevy_replicon_renet::netcode::ServerConfig { - current_time, - max_clients: 1, - protocol_id: 0, - authentication: ServerAuthentication::Unsecure, - public_addresses: Default::default(), - }; - let transport = NetcodeServerTransport::new(server_config, socket)?; + if let NetConfig::SteamHost = *config { + let Some(steam_client) = steam_client else { + return Err("Steam client not found".into()); + }; - commands.insert_resource(server); - commands.insert_resource(transport); + let steam_config = bevy_replicon_renet::steam::SteamServerConfig { + access_permission: bevy_replicon_renet::steam::AccessPermission::FriendsOnly, + max_clients: 16, + }; - info!("hosting a server on port {port}"); + let client = (**steam_client).clone(); + let transport = SteamServerTransport::new(client, steam_config)?; + + commands.queue(|w: &mut World| { + w.insert_resource(server); + w.insert_non_send_resource(transport); + }); + + info!("hosting server: steam"); + } else if let NetConfig::NetcodeHost { port } = *config { + use std::time::SystemTime; + + let current_time = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?; + let socket = std::net::UdpSocket::bind((std::net::Ipv4Addr::UNSPECIFIED, port))?; + + let server_config = bevy_replicon_renet::netcode::ServerConfig { + current_time, + max_clients: 8, + protocol_id: 0, + authentication: bevy_replicon_renet::netcode::ServerAuthentication::Unsecure, + public_addresses: Default::default(), + }; + let transport = + bevy_replicon_renet::netcode::NetcodeServerTransport::new(server_config, socket)?; + + commands.insert_resource(server); + commands.insert_resource(transport); + + info!("hosting server: netcode on port {port}"); + } else { + return Err("Invalid configuration, choose either steam or netcode".into()); + } next.set(GameState::Playing); diff --git a/justfile b/justfile index 2e0a885..c0d1b23 100644 --- a/justfile +++ b/justfile @@ -13,7 +13,7 @@ run *args: RUST_BACKTRACE=1 cargo r {{ client_args }} -- {{ args }} server: - RUST_BACKTRACE=1 cargo r {{ server_args }} + RUST_BACKTRACE=1 cargo r {{ server_args }} -- --netcode-host dbg *args: RUST_BACKTRACE=1 cargo r {{ client_args }} --features dbg -- {{ args }} @@ -22,7 +22,7 @@ dbg-server: RUST_BACKTRACE=1 cargo r {{ server_args }} --features dbg sort: - cargo sort --check --workspace + cargo sort --workspace check: cargo sort --check --workspace