This commit is contained in:
+210
-25
@@ -1,49 +1,234 @@
|
||||
//! Bringing up the GGRS session and reporting what it does.
|
||||
//! Bringing up GGRS sessions and reporting their connection lifecycle.
|
||||
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use bevy::prelude::*;
|
||||
use bevy_ggrs::{ggrs::UdpNonBlockingSocket, prelude::*};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::args::Args;
|
||||
use crate::args::{parse_socket_addr, Args};
|
||||
use crate::netcode::GGRSCfg;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum PlayerConnection {
|
||||
Local,
|
||||
Remote(SocketAddr),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Resource)]
|
||||
pub struct SessionConfig {
|
||||
pub local_port: u16,
|
||||
pub players: Vec<PlayerConnection>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error, PartialEq, Eq)]
|
||||
pub enum SessionConfigError {
|
||||
#[error("a session must have at least one player")]
|
||||
NoPlayers,
|
||||
#[error("local UDP port must be nonzero")]
|
||||
LocalPortZero,
|
||||
#[error("invalid network player declaration `{0}`; expected HANDLE=local or HANDLE=ADDRESS")]
|
||||
InvalidPlayerDeclaration(String),
|
||||
#[error("network player handle {0} is outside the session player range")]
|
||||
PlayerHandleOutOfRange(usize),
|
||||
#[error("network player handle {0} was declared more than once")]
|
||||
DuplicatePlayerHandle(usize),
|
||||
#[error("network player {0} has an invalid address: {1}")]
|
||||
InvalidRemoteAddress(usize, String),
|
||||
#[error("at least one player must be local")]
|
||||
NoLocalPlayers,
|
||||
#[error("network player declarations are not supported in sync-test or replay mode")]
|
||||
UnsupportedOfflineNetworkConfig,
|
||||
}
|
||||
|
||||
impl SessionConfig {
|
||||
pub fn from_args(args: &Args, player_count: usize) -> Result<Self, SessionConfigError> {
|
||||
if player_count == 0 {
|
||||
return Err(SessionConfigError::NoPlayers);
|
||||
}
|
||||
if args.local_port == 0 {
|
||||
return Err(SessionConfigError::LocalPortZero);
|
||||
}
|
||||
if (args.synctest || args.play_replay.is_some()) && !args.net_players.is_empty() {
|
||||
return Err(SessionConfigError::UnsupportedOfflineNetworkConfig);
|
||||
}
|
||||
|
||||
let mut players = vec![PlayerConnection::Local; player_count];
|
||||
for declaration in &args.net_players {
|
||||
let Some((handle, address)) = declaration.split_once('=') else {
|
||||
return Err(SessionConfigError::InvalidPlayerDeclaration(
|
||||
declaration.clone(),
|
||||
));
|
||||
};
|
||||
let handle = handle
|
||||
.parse::<usize>()
|
||||
.map_err(|_| SessionConfigError::InvalidPlayerDeclaration(declaration.clone()))?;
|
||||
if handle >= player_count {
|
||||
return Err(SessionConfigError::PlayerHandleOutOfRange(handle));
|
||||
}
|
||||
if !matches!(players[handle], PlayerConnection::Local)
|
||||
|| args.net_players[..]
|
||||
.iter()
|
||||
.take_while(|item| *item != declaration)
|
||||
.any(|item| {
|
||||
item.split_once('=')
|
||||
.and_then(|(handle, _)| handle.parse::<usize>().ok())
|
||||
== Some(handle)
|
||||
})
|
||||
{
|
||||
return Err(SessionConfigError::DuplicatePlayerHandle(handle));
|
||||
}
|
||||
players[handle] = if address == "local" {
|
||||
PlayerConnection::Local
|
||||
} else {
|
||||
PlayerConnection::Remote(
|
||||
parse_socket_addr(address)
|
||||
.map_err(|error| SessionConfigError::InvalidRemoteAddress(handle, error))?,
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
if !players
|
||||
.iter()
|
||||
.any(|player| matches!(player, PlayerConnection::Local))
|
||||
{
|
||||
return Err(SessionConfigError::NoLocalPlayers);
|
||||
}
|
||||
Ok(Self {
|
||||
local_port: args.local_port,
|
||||
players,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_session(args: &Args, player_count: usize) -> Result<Session<GGRSCfg>> {
|
||||
let config = SessionConfig::from_args(args, player_count)
|
||||
.map_err(|error| bevy::ecs::error::BevyError::from(error.to_string()))?;
|
||||
let mut sess_build = SessionBuilder::<GGRSCfg>::new()
|
||||
.with_num_players(player_count)?
|
||||
.with_check_distance(args.check_distance)
|
||||
.with_input_delay(args.input_delay); // (optional) set input delay for the local player
|
||||
.with_input_delay(args.input_delay);
|
||||
|
||||
for i in 0..player_count {
|
||||
sess_build = sess_build.add_player(PlayerType::Local, i)?;
|
||||
for (handle, player) in config.players.iter().enumerate() {
|
||||
let player_type = match player {
|
||||
PlayerConnection::Local => PlayerType::Local,
|
||||
PlayerConnection::Remote(address) => PlayerType::Remote(*address),
|
||||
};
|
||||
sess_build = sess_build.add_player(player_type, handle)?;
|
||||
}
|
||||
|
||||
if args.synctest {
|
||||
println!("Start synctest!");
|
||||
let sess = sess_build.start_synctest_session()?;
|
||||
Ok(Session::SyncTest(sess))
|
||||
info!("starting GGRS sync test");
|
||||
Ok(Session::SyncTest(sess_build.start_synctest_session()?))
|
||||
} else {
|
||||
println!("Start p2p!");
|
||||
let socket = UdpNonBlockingSocket::bind_to_port(6969)?;
|
||||
let sess = sess_build.start_p2p_session(socket)?;
|
||||
Ok(Session::P2P(sess))
|
||||
info!(
|
||||
"starting direct UDP P2P session on port {}",
|
||||
config.local_port
|
||||
);
|
||||
let socket = UdpNonBlockingSocket::bind_to_port(config.local_port)?;
|
||||
Ok(Session::P2P(sess_build.start_p2p_session(socket)?))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn print_events_system(session: Option<ResMut<Session<GGRSCfg>>>) {
|
||||
let Some(mut session) = session else {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Resource)]
|
||||
pub enum ConnectionStatus {
|
||||
#[default]
|
||||
Connecting,
|
||||
Running,
|
||||
Interrupted,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Resource)]
|
||||
pub struct MatchDisconnected {
|
||||
pub address: SocketAddr,
|
||||
}
|
||||
|
||||
pub fn print_events_system(
|
||||
mut commands: Commands,
|
||||
mut next_state: ResMut<NextState<crate::AppState>>,
|
||||
mut status: Option<ResMut<ConnectionStatus>>,
|
||||
session: Option<ResMut<Session<GGRSCfg>>>,
|
||||
) {
|
||||
let (Some(mut session), Some(status)) = (session, status.as_deref_mut()) else {
|
||||
return;
|
||||
};
|
||||
match session.as_mut() {
|
||||
Session::P2P(s) => {
|
||||
for event in s.events() {
|
||||
match event {
|
||||
GgrsEvent::Disconnected { .. } | GgrsEvent::NetworkInterrupted { .. } => {
|
||||
warn!("GGRS event: {event:?}")
|
||||
}
|
||||
GgrsEvent::DesyncDetected { .. } => error!("GGRS event: {event:?}"),
|
||||
_ => info!("GGRS event: {event:?}"),
|
||||
}
|
||||
let Session::P2P(session) = session.as_mut() else {
|
||||
return;
|
||||
};
|
||||
for event in session.events() {
|
||||
match event {
|
||||
GgrsEvent::NetworkInterrupted { addr, .. } => {
|
||||
warn!("GGRS network interrupted: {addr}");
|
||||
*status = ConnectionStatus::Interrupted;
|
||||
}
|
||||
GgrsEvent::NetworkResumed { addr } => {
|
||||
info!("GGRS network resumed: {addr}");
|
||||
*status = ConnectionStatus::Running;
|
||||
}
|
||||
GgrsEvent::Disconnected { addr } => {
|
||||
error!("GGRS peer disconnected: {addr}");
|
||||
commands.insert_resource(MatchDisconnected { address: addr });
|
||||
next_state.set(crate::AppState::MatchDisconnected);
|
||||
}
|
||||
GgrsEvent::Synchronized { .. } => {
|
||||
info!("GGRS session synchronized");
|
||||
*status = ConnectionStatus::Running;
|
||||
}
|
||||
GgrsEvent::DesyncDetected { .. } => error!("GGRS desync detected: {event:?}"),
|
||||
_ => info!("GGRS event: {event:?}"),
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use clap::Parser;
|
||||
|
||||
fn args(values: &[&str]) -> Args {
|
||||
Args::try_parse_from(values).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn defaults_to_local_players() {
|
||||
let config = SessionConfig::from_args(&args(&["game", "--players", "2"]), 2).unwrap();
|
||||
assert_eq!(
|
||||
config.players,
|
||||
[PlayerConnection::Local, PlayerConnection::Local]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configures_remote_players_by_handle() {
|
||||
let config = SessionConfig::from_args(
|
||||
&args(&["game", "--players", "2", "--net-player", "1=127.0.0.1:7001"]),
|
||||
2,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(matches!(config.players[0], PlayerConnection::Local));
|
||||
assert!(matches!(config.players[1], PlayerConnection::Remote(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validates_handles_and_local_ownership() {
|
||||
let duplicate = args(&[
|
||||
"game",
|
||||
"--players",
|
||||
"2",
|
||||
"--net-player",
|
||||
"1=127.0.0.1:7001",
|
||||
"--net-player",
|
||||
"1=127.0.0.1:7002",
|
||||
]);
|
||||
assert!(matches!(
|
||||
SessionConfig::from_args(&duplicate, 2),
|
||||
Err(SessionConfigError::DuplicatePlayerHandle(1))
|
||||
));
|
||||
|
||||
let no_local = args(&["game", "--players", "1", "--net-player", "0=127.0.0.1:7001"]);
|
||||
assert!(matches!(
|
||||
SessionConfig::from_args(&no_local, 1),
|
||||
Err(SessionConfigError::NoLocalPlayers)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user