CI / build-and-test (push) Successful in 1m10s
Closes #24 Co-authored-by: Álex Román Núñez <eirexe@eirteam.moe>
235 lines
7.8 KiB
Rust
235 lines
7.8 KiB
Rust
//! 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::{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);
|
|
|
|
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 {
|
|
info!("starting GGRS sync test");
|
|
Ok(Session::SyncTest(sess_build.start_synctest_session()?))
|
|
} else {
|
|
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)?))
|
|
}
|
|
}
|
|
|
|
#[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;
|
|
};
|
|
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)
|
|
));
|
|
}
|
|
}
|