Implement direct P2P session scaffolding (#39)
CI / build-and-test (push) Successful in 1m10s
CI / build-and-test (push) Successful in 1m10s
Closes #24 Co-authored-by: Álex Román Núñez <eirexe@eirteam.moe>
This commit was merged in pull request #39.
This commit is contained in:
@@ -1,12 +1,15 @@
|
|||||||
# Game Test
|
# Shinespark
|
||||||
|
|
||||||
A Bevy game, editable in the [jackdaw](https://github.com/jbuehler23/jackdaw) editor.
|
Fancy heavily advanced platform fighting game
|
||||||
|
|
||||||
- `cargo run` plays the game standalone; no editor required.
|
## Direct P2P Debugging
|
||||||
- Open the folder in jackdaw to author scenes, then hit Play. The
|
|
||||||
editor builds its own artifacts into `.jackdaw/` (gitignored) and
|
```text
|
||||||
never modifies your manifest or lockfile.
|
# Process 1 owns player 0 and listens on 7000.
|
||||||
- Gameplay lives in `src/lib.rs` (`GamePlugin`); authored scenes live
|
cargo run -- --debug-match --players 2 --local-port 7000 \
|
||||||
in `assets/*.bsn`.
|
--net-player 0=local --net-player 1=127.0.0.1:7001
|
||||||
- Components you derive `Reflect` on appear in the editor's inspector
|
|
||||||
automatically; no registration code is needed.
|
# Process 2 owns player 1 and listens on 7001.
|
||||||
|
cargo run -- --debug-match --players 2 --local-port 7001 \
|
||||||
|
--net-player 0=127.0.0.1:7000 --net-player 1=local
|
||||||
|
```
|
||||||
|
|||||||
Executable
+8
@@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
FIRST="cargo run -- --debug-match --players 2 --local-port 7000 \
|
||||||
|
--net-player 0=local --net-player 1=127.0.0.1:7001"
|
||||||
|
SECOND="cargo run -- --debug-match --players 2 --local-port 7001 \
|
||||||
|
--net-player 0=127.0.0.1:7000 --net-player 1=local"
|
||||||
|
|
||||||
|
(trap 'kill 0' SIGINT; $FIRST & $SECOND)
|
||||||
+45
@@ -1,5 +1,6 @@
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
use std::net::SocketAddr;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[derive(Parser, Debug, Clone, Resource)]
|
#[derive(Parser, Debug, Clone, Resource)]
|
||||||
@@ -15,6 +16,12 @@ pub struct Args {
|
|||||||
pub check_distance: usize,
|
pub check_distance: usize,
|
||||||
#[clap(long, default_value_t = 1)]
|
#[clap(long, default_value_t = 1)]
|
||||||
pub players: usize,
|
pub players: usize,
|
||||||
|
/// UDP port to bind for a direct peer-to-peer session.
|
||||||
|
#[clap(long, default_value_t = 6969)]
|
||||||
|
pub local_port: u16,
|
||||||
|
/// Player ownership declarations in the form HANDLE=local or HANDLE=ADDRESS.
|
||||||
|
#[clap(long = "net-player", value_name = "HANDLE=local|ADDRESS")]
|
||||||
|
pub net_players: Vec<String>,
|
||||||
/// Record a replay to this path.
|
/// Record a replay to this path.
|
||||||
#[clap(long, conflicts_with = "play_replay")]
|
#[clap(long, conflicts_with = "play_replay")]
|
||||||
pub record_replay: Option<PathBuf>,
|
pub record_replay: Option<PathBuf>,
|
||||||
@@ -25,3 +32,41 @@ pub struct Args {
|
|||||||
#[clap(long, value_enum, default_value_t = crate::replay::ReplayFormat::Binary)]
|
#[clap(long, value_enum, default_value_t = crate::replay::ReplayFormat::Binary)]
|
||||||
pub replay_format: crate::replay::ReplayFormat,
|
pub replay_format: crate::replay::ReplayFormat,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn parse_socket_addr(value: &str) -> Result<SocketAddr, String> {
|
||||||
|
let address = value
|
||||||
|
.parse::<SocketAddr>()
|
||||||
|
.map_err(|_| format!("invalid socket address: {value}"))?;
|
||||||
|
if address.port() == 0 {
|
||||||
|
return Err(format!("socket address must use a nonzero port: {value}"));
|
||||||
|
}
|
||||||
|
Ok(address)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_network_options() {
|
||||||
|
let args = Args::try_parse_from([
|
||||||
|
"game",
|
||||||
|
"--local-port",
|
||||||
|
"7000",
|
||||||
|
"--net-player",
|
||||||
|
"0=local",
|
||||||
|
"--net-player",
|
||||||
|
"1=127.0.0.1:7001",
|
||||||
|
])
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(args.local_port, 7000);
|
||||||
|
assert_eq!(args.net_players, ["0=local", "1=127.0.0.1:7001"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_invalid_socket_addresses() {
|
||||||
|
assert!(parse_socket_addr("127.0.0.1:0").is_err());
|
||||||
|
assert!(parse_socket_addr("not-an-address").is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ pub enum AppState {
|
|||||||
LoadingMatch,
|
LoadingMatch,
|
||||||
PreparingMatch,
|
PreparingMatch,
|
||||||
InMatch,
|
InMatch,
|
||||||
|
MatchDisconnected,
|
||||||
MatchLoadFailed,
|
MatchLoadFailed,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+44
-7
@@ -8,25 +8,28 @@ use bevy_ggrs::prelude::Session;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
AppState,
|
|
||||||
args::Args,
|
args::Args,
|
||||||
fighter::{
|
fighter::{
|
||||||
FighterId, FighterRoster,
|
|
||||||
animation::AnimKind,
|
animation::AnimKind,
|
||||||
attack::{AttackKind, FighterAttackScriptAssets},
|
attack::{AttackKind, FighterAttackScriptAssets},
|
||||||
baked_animation::{BAKED_ANIMATION_FPS, BakedFighterAnimations},
|
baked_animation::{BakedFighterAnimations, BAKED_ANIMATION_FPS},
|
||||||
manifest::{FighterManifest, FighterManifestRegistry},
|
manifest::{FighterManifest, FighterManifestRegistry},
|
||||||
spawn_fighter,
|
spawn_fighter,
|
||||||
visual::FighterAnimations,
|
visual::FighterAnimations,
|
||||||
|
FighterId, FighterRoster,
|
||||||
},
|
},
|
||||||
input::{LocalInputAssignments, LocalInputSource},
|
input::{LocalInputAssignments, LocalInputSource},
|
||||||
math::{int::FGi32, vec::FGVec2},
|
math::{int::FGi32, vec::FGVec2},
|
||||||
netcode::{GGRSCfg, session::create_session},
|
netcode::{
|
||||||
|
session::{create_session, ConnectionStatus, SessionConfig},
|
||||||
|
GGRSCfg,
|
||||||
|
},
|
||||||
scripting::FighterAttackScript,
|
scripting::FighterAttackScript,
|
||||||
stage::{
|
stage::{
|
||||||
self,
|
self,
|
||||||
manifest::{StageId, StageManifest, StageManifestRegistry},
|
manifest::{StageId, StageManifest, StageManifestRegistry},
|
||||||
},
|
},
|
||||||
|
AppState,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
@@ -140,6 +143,7 @@ pub fn initiate_match(
|
|||||||
next_state: &mut NextState<AppState>,
|
next_state: &mut NextState<AppState>,
|
||||||
request: PendingMatch,
|
request: PendingMatch,
|
||||||
) {
|
) {
|
||||||
|
commands.remove_resource::<crate::netcode::session::MatchDisconnected>();
|
||||||
commands.insert_resource(request);
|
commands.insert_resource(request);
|
||||||
next_state.set(AppState::LoadingMatch);
|
next_state.set(AppState::LoadingMatch);
|
||||||
}
|
}
|
||||||
@@ -176,6 +180,14 @@ pub fn initiate_default_match(
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let session_config = match SessionConfig::from_args(&args, args.players) {
|
||||||
|
Ok(config) => config,
|
||||||
|
Err(error) => {
|
||||||
|
error!("Invalid network configuration: {error}");
|
||||||
|
next_state.set(AppState::MatchLoadFailed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
let players = (0..args.players)
|
let players = (0..args.players)
|
||||||
.map(|handle| MatchPlayer {
|
.map(|handle| MatchPlayer {
|
||||||
handle,
|
handle,
|
||||||
@@ -183,10 +195,21 @@ pub fn initiate_default_match(
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
assigments.push((0, LocalInputSource::Keyboard));
|
assigments.clear();
|
||||||
|
let local_handles = session_config
|
||||||
|
.players
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(handle, player)| {
|
||||||
|
matches!(player, crate::netcode::session::PlayerConnection::Local).then_some(handle)
|
||||||
|
});
|
||||||
|
let mut local_handles = local_handles;
|
||||||
|
if let Some(handle) = local_handles.next() {
|
||||||
|
assigments.push((handle, LocalInputSource::Keyboard));
|
||||||
|
}
|
||||||
|
|
||||||
for (idx, entity) in gamepads.iter().take(args.players - 1).enumerate() {
|
for (entity, handle) in gamepads.iter().zip(local_handles) {
|
||||||
assigments.push((idx + 1, LocalInputSource::Gamepad(entity)));
|
assigments.push((handle, LocalInputSource::Gamepad(entity)));
|
||||||
}
|
}
|
||||||
|
|
||||||
initiate_match(
|
initiate_match(
|
||||||
@@ -373,6 +396,16 @@ pub fn prepare_match(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let session_config = match SessionConfig::from_args(&args, request.players.len()) {
|
||||||
|
Ok(config) => config,
|
||||||
|
Err(error) => {
|
||||||
|
fail_match(
|
||||||
|
&mut next_state,
|
||||||
|
&format!("invalid network configuration: {error}"),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
let session = match create_session(&args, request.players.len()) {
|
let session = match create_session(&args, request.players.len()) {
|
||||||
Ok(session) => session,
|
Ok(session) => session,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
@@ -418,6 +451,8 @@ pub fn prepare_match(
|
|||||||
}
|
}
|
||||||
|
|
||||||
commands.insert_resource(FighterRoster(fighter_roster));
|
commands.insert_resource(FighterRoster(fighter_roster));
|
||||||
|
commands.insert_resource(session_config);
|
||||||
|
commands.insert_resource(ConnectionStatus::default());
|
||||||
commands.insert_resource::<Session<GGRSCfg>>(session);
|
commands.insert_resource::<Session<GGRSCfg>>(session);
|
||||||
next_state.set(AppState::InMatch);
|
next_state.set(AppState::InMatch);
|
||||||
}
|
}
|
||||||
@@ -448,6 +483,8 @@ pub fn cleanup_match(
|
|||||||
commands.entity(fighter).despawn();
|
commands.entity(fighter).despawn();
|
||||||
}
|
}
|
||||||
commands.remove_resource::<Session<GGRSCfg>>();
|
commands.remove_resource::<Session<GGRSCfg>>();
|
||||||
|
commands.remove_resource::<SessionConfig>();
|
||||||
|
commands.remove_resource::<ConnectionStatus>();
|
||||||
commands.remove_resource::<stage::line::StageCollision>();
|
commands.remove_resource::<stage::line::StageCollision>();
|
||||||
commands.remove_resource::<MatchAssets>();
|
commands.remove_resource::<MatchAssets>();
|
||||||
commands.remove_resource::<PendingMatch>();
|
commands.remove_resource::<PendingMatch>();
|
||||||
|
|||||||
+206
-21
@@ -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::prelude::*;
|
||||||
use bevy_ggrs::{ggrs::UdpNonBlockingSocket, 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;
|
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>> {
|
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()
|
let mut sess_build = SessionBuilder::<GGRSCfg>::new()
|
||||||
.with_num_players(player_count)?
|
.with_num_players(player_count)?
|
||||||
.with_check_distance(args.check_distance)
|
.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 {
|
for (handle, player) in config.players.iter().enumerate() {
|
||||||
sess_build = sess_build.add_player(PlayerType::Local, i)?;
|
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 {
|
if args.synctest {
|
||||||
println!("Start synctest!");
|
info!("starting GGRS sync test");
|
||||||
let sess = sess_build.start_synctest_session()?;
|
Ok(Session::SyncTest(sess_build.start_synctest_session()?))
|
||||||
Ok(Session::SyncTest(sess))
|
|
||||||
} else {
|
} else {
|
||||||
println!("Start p2p!");
|
info!(
|
||||||
let socket = UdpNonBlockingSocket::bind_to_port(6969)?;
|
"starting direct UDP P2P session on port {}",
|
||||||
let sess = sess_build.start_p2p_session(socket)?;
|
config.local_port
|
||||||
Ok(Session::P2P(sess))
|
);
|
||||||
|
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>>>) {
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Resource)]
|
||||||
let Some(mut session) = session else {
|
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;
|
return;
|
||||||
};
|
};
|
||||||
match session.as_mut() {
|
let Session::P2P(session) = session.as_mut() else {
|
||||||
Session::P2P(s) => {
|
return;
|
||||||
for event in s.events() {
|
};
|
||||||
|
for event in session.events() {
|
||||||
match event {
|
match event {
|
||||||
GgrsEvent::Disconnected { .. } | GgrsEvent::NetworkInterrupted { .. } => {
|
GgrsEvent::NetworkInterrupted { addr, .. } => {
|
||||||
warn!("GGRS event: {event:?}")
|
warn!("GGRS network interrupted: {addr}");
|
||||||
|
*status = ConnectionStatus::Interrupted;
|
||||||
}
|
}
|
||||||
GgrsEvent::DesyncDetected { .. } => error!("GGRS event: {event:?}"),
|
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:?}"),
|
_ => 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