add renet_steam support (#92)

This commit is contained in:
extrawurst
2025-12-20 19:19:13 +01:00
committed by GitHub
parent 7ea9046414
commit 7d280af821
9 changed files with 214 additions and 73 deletions

13
Cargo.lock generated
View File

@@ -1519,6 +1519,7 @@ dependencies = [
"bevy_time", "bevy_time",
"renet", "renet",
"renet_netcode", "renet_netcode",
"renet_steam",
] ]
[[package]] [[package]]
@@ -5564,6 +5565,18 @@ dependencies = [
"renetcode", "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]] [[package]]
name = "renetcode" name = "renetcode"
version = "1.0.0" version = "1.0.0"

View File

@@ -61,7 +61,8 @@ bevy_pkv = { version = "0.14", default-features = false, features = [
"redb", "redb",
] } ] }
bevy_replicon = "0.37.1" 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_sprite3d = "7.0.0"
bevy_trenchbroom = { version = "0.10", default-features = false, features = [ bevy_trenchbroom = { version = "0.10", default-features = false, features = [
"physics-integration", "physics-integration",
@@ -76,6 +77,7 @@ rand = "=0.8.5"
ron = "0.8" ron = "0.8"
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
shared = { path = "crates/shared" } shared = { path = "crates/shared" }
# TODO: i dont think we need this in dedicated server mode
steamworks = "0.12" steamworks = "0.12"
[profile.dev.package."*"] [profile.dev.package."*"]

View File

@@ -1,6 +1,6 @@
use crate::{ use crate::{
GameState, GameState,
config::NetworkingConfig, config::NetConfig,
protocol::{ protocol::{
ClientEnteredPlaying, TbMapEntityId, TbMapEntityMapping, messages::DespawnTbMapEntity, ClientEnteredPlaying, TbMapEntityId, TbMapEntityMapping, messages::DespawnTbMapEntity,
}, },
@@ -17,14 +17,10 @@ use bevy_replicon::{
}; };
use bevy_replicon_renet::{ use bevy_replicon_renet::{
RenetChannelsExt, RenetChannelsExt,
netcode::{ClientAuthentication, NetcodeClientTransport, NetcodeError},
renet::{ConnectionConfig, RenetClient}, renet::{ConnectionConfig, RenetClient},
}; };
use bevy_steamworks::Client;
use bevy_trenchbroom::geometry::Brushes; use bevy_trenchbroom::geometry::Brushes;
use std::{
net::{Ipv4Addr, UdpSocket},
time::SystemTime,
};
pub mod audio; pub mod audio;
pub mod backpack; pub mod backpack;
@@ -55,7 +51,7 @@ pub fn plugin(app: &mut App) {
app.add_systems( app.add_systems(
OnEnter(GameState::Connecting), OnEnter(GameState::Connecting),
connect_to_server.run_if(|config: Res<NetworkingConfig>| config.server.is_some()), connect_to_server.run_if(|config: Res<NetConfig>| config.is_client()),
); );
app.add_systems(Update, despawn_absent_map_entities); app.add_systems(Update, despawn_absent_map_entities);
app.add_systems( app.add_systems(
@@ -89,8 +85,9 @@ fn on_disconnect() {
fn connect_to_server( fn connect_to_server(
mut commands: Commands, mut commands: Commands,
config: Res<NetworkingConfig>, config: Res<NetConfig>,
channels: Res<RepliconChannels>, channels: Res<RepliconChannels>,
steam_client: Option<Res<Client>>,
) -> Result { ) -> Result {
let server_channels_config = channels.server_configs(); let server_channels_config = channels.server_configs();
let client_channels_config = channels.client_configs(); let client_channels_config = channels.client_configs();
@@ -102,32 +99,47 @@ fn connect_to_server(
}); });
commands.insert_resource(client); 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(()) Ok(())
} }
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, 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)] #[allow(clippy::type_complexity)]
fn migrate_remote_entities( fn migrate_remote_entities(
query: Query<(Entity, &TbMapEntityId), (Added<TbMapEntityId>, With<ConfirmHistory>)>, query: Query<(Entity, &TbMapEntityId), (Added<TbMapEntityId>, With<ConfirmHistory>)>,

View File

@@ -4,10 +4,11 @@ use bevy_pkv::prelude::*;
use crate::{client::audio::SoundSettings, utils::Debounce}; use crate::{client::audio::SoundSettings, utils::Debounce};
pub fn plugin(app: &mut App) { pub fn plugin(app: &mut App) {
#[cfg(not(feature = "dbg"))]
app.insert_resource(PkvStore::new("Rustunit", "HEDZ")); app.insert_resource(PkvStore::new("Rustunit", "HEDZ"));
app.add_systems(Update, persist_settings); app.add_systems(Update, persist_settings.run_if(resource_exists::<PkvStore>));
app.add_systems(Startup, load_settings); app.add_systems(Startup, load_settings.run_if(resource_exists::<PkvStore>));
} }
fn persist_settings( fn persist_settings(

View File

@@ -61,6 +61,14 @@ fn test_steam_system(steam_client: Res<Client>) {
}, },
); );
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) { for friend in steam_client.friends().get_friends(FriendFlags::IMMEDIATE) {
info!( info!(
"Steam Friend: {:?} - {}({:?})", "Steam Friend: {:?} - {}({:?})",

View File

@@ -1,25 +1,92 @@
use std::net::SocketAddr;
use bevy::prelude::*; use bevy::prelude::*;
use clap::Parser; use clap::Parser;
use std::net::SocketAddr; use steamworks::SteamId;
pub fn plugin(app: &mut App) { pub fn plugin(app: &mut App) {
let config = NetworkingConfig::parse(); let config = NetworkingConfig::parse();
let config: NetConfig = config.into();
info!("net config: {:?}", config);
app.insert_resource(config); app.insert_resource(config);
} }
#[derive(Resource, Parser, Debug)] #[derive(Parser, Debug)]
#[command(version, about, long_about = None)] #[command(version, about, long_about = None)]
pub struct NetworkingConfig { struct NetworkingConfig {
/// The IP/port to connect to. /// Steam id of the host 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.
#[arg(long)] #[arg(long)]
pub server: Option<Option<SocketAddr>>, pub steam_host_id: Option<String>,
/// Whether or not to open a port when opening the client, for other clients
/// to connect. Does nothing if `server` is set. /// Act as steam host
#[arg(long)] #[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<Option<u16>>,
/// Host address we connect to as a client
#[arg(long)]
pub netcode_client: Option<Option<String>>,
}
#[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<NetworkingConfig> 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<String>) -> 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)
} }

View File

@@ -34,7 +34,7 @@ pub mod utils;
pub mod water; pub mod water;
use crate::{ use crate::{
config::NetworkingConfig, config::NetConfig,
heads_database::{HeadDatabaseAsset, HeadsDatabase}, heads_database::{HeadDatabaseAsset, HeadsDatabase},
protocol::{PlayerIdCounter, messages::AssignClientPlayer}, protocol::{PlayerIdCounter, messages::AssignClientPlayer},
tb_entities::SpawnPoint, tb_entities::SpawnPoint,
@@ -47,6 +47,7 @@ use bevy_common_assets::ron::RonAssetPlugin;
use bevy_replicon::{RepliconPlugins, prelude::ClientId}; use bevy_replicon::{RepliconPlugins, prelude::ClientId};
use bevy_replicon_renet::RepliconRenetPlugins; use bevy_replicon_renet::RepliconRenetPlugins;
use bevy_sprite3d::Sprite3dPlugin; use bevy_sprite3d::Sprite3dPlugin;
use bevy_steamworks::SteamworksEvent;
use bevy_trenchbroom::{ use bevy_trenchbroom::{
TrenchBroomPlugins, config::TrenchBroomConfig, prelude::TrenchBroomPhysicsPlugin, TrenchBroomPlugins, config::TrenchBroomConfig, prelude::TrenchBroomPhysicsPlugin,
}; };
@@ -148,21 +149,21 @@ pub fn plugin(app: &mut App) {
if cfg!(feature = "client") { if cfg!(feature = "client") {
app.add_systems( app.add_systems(
OnEnter(GameState::Waiting), OnEnter(GameState::Waiting),
start_solo_client start_solo_client.run_if(|config: Res<NetConfig>| config.is_singleplayer()),
.run_if(|config: Res<NetworkingConfig>| config.server.is_none() && !config.host),
); );
app.add_systems( app.add_systems(
OnEnter(GameState::Waiting), OnEnter(GameState::Waiting),
start_listen_server start_listen_server.run_if(|config: Res<NetConfig>| config.is_host()),
.run_if(|config: Res<NetworkingConfig>| config.server.is_none() && config.host),
); );
app.add_systems( app.add_systems(
OnEnter(GameState::Waiting), OnEnter(GameState::Waiting),
start_client.run_if(|config: Res<NetworkingConfig>| config.server.is_some()), start_client.run_if(|config: Res<NetConfig>| config.is_client()),
); );
} else { } else {
app.add_systems(OnEnter(GameState::Waiting), start_dedicated_server); app.add_systems(OnEnter(GameState::Waiting), start_dedicated_server);
} }
app.add_systems(Update, log_steam_events);
} }
#[derive(Resource, Reflect, Debug)] #[derive(Resource, Reflect, Debug)]
@@ -192,6 +193,17 @@ pub enum GameState {
Playing, Playing,
} }
fn log_steam_events(events: Option<MessageReader<SteamworksEvent>>) {
let Some(mut events) = events else {
return;
};
for event in events.read() {
let SteamworksEvent::CallbackResult(result) = event;
info!("steam: {:?}", result);
}
}
fn start_solo_client( fn start_solo_client(
commands: Commands, commands: Commands,
mut next: ResMut<NextState<GameState>>, mut next: ResMut<NextState<GameState>>,

View File

@@ -1,5 +1,7 @@
use crate::{ use crate::{
GameState, global_observer, GameState,
config::NetConfig,
global_observer,
heads_database::HeadsDatabase, heads_database::HeadsDatabase,
player::ClientPlayerId, player::ClientPlayerId,
protocol::{ClientEnteredPlaying, PlayerIdCounter, SetGameTick, messages::AssignClientPlayer}, protocol::{ClientEnteredPlaying, PlayerIdCounter, SetGameTick, messages::AssignClientPlayer},
@@ -16,12 +18,8 @@ use bevy_replicon::{
}; };
use bevy_replicon_renet::{ use bevy_replicon_renet::{
RenetChannelsExt, RenetChannelsExt,
netcode::{NetcodeServerTransport, ServerAuthentication},
renet::{ConnectionConfig, RenetServer}, renet::{ConnectionConfig, RenetServer},
}; steam::SteamServerTransport,
use std::{
net::{Ipv4Addr, UdpSocket},
time::SystemTime,
}; };
pub fn plugin(app: &mut App) { pub fn plugin(app: &mut App) {
@@ -59,6 +57,8 @@ fn open_renet_server(
mut commands: Commands, mut commands: Commands,
channels: Res<RepliconChannels>, channels: Res<RepliconChannels>,
mut next: ResMut<NextState<GameState>>, mut next: ResMut<NextState<GameState>>,
steam_client: Option<Res<bevy_steamworks::Client>>,
config: Res<NetConfig>,
) -> Result<(), BevyError> { ) -> Result<(), BevyError> {
info!("opening server"); info!("opening server");
@@ -71,22 +71,48 @@ fn open_renet_server(
..Default::default() ..Default::default()
}); });
let current_time = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?; if let NetConfig::SteamHost = *config {
let port = 31111; let Some(steam_client) = steam_client else {
let socket = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, port))?; return Err("Steam client not found".into());
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)?;
commands.insert_resource(server); let steam_config = bevy_replicon_renet::steam::SteamServerConfig {
commands.insert_resource(transport); 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); next.set(GameState::Playing);

View File

@@ -13,7 +13,7 @@ run *args:
RUST_BACKTRACE=1 cargo r {{ client_args }} -- {{ args }} RUST_BACKTRACE=1 cargo r {{ client_args }} -- {{ args }}
server: server:
RUST_BACKTRACE=1 cargo r {{ server_args }} RUST_BACKTRACE=1 cargo r {{ server_args }} -- --netcode-host
dbg *args: dbg *args:
RUST_BACKTRACE=1 cargo r {{ client_args }} --features 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 RUST_BACKTRACE=1 cargo r {{ server_args }} --features dbg
sort: sort:
cargo sort --check --workspace cargo sort --workspace
check: check:
cargo sort --check --workspace cargo sort --check --workspace