diff --git a/Cargo.lock b/Cargo.lock index 6df570b7e..14bf3ccb2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2410,6 +2410,7 @@ dependencies = [ "de_lobby_client", "de_lobby_model", "de_map", + "de_multiplayer", "futures-lite", "thiserror", ] diff --git a/crates/lobby_client/src/endpoints.rs b/crates/lobby_client/src/endpoints.rs index b9d0b6308..187f71cfe 100644 --- a/crates/lobby_client/src/endpoints.rs +++ b/crates/lobby_client/src/endpoints.rs @@ -1,6 +1,6 @@ use std::borrow::Cow; -use de_lobby_model::{GameListing, GameSetup, Token, UserWithPassword, UsernameAndPassword}; +use de_lobby_model::{Game, GameListing, GameSetup, Token, UserWithPassword, UsernameAndPassword}; use reqwest::{header::HeaderValue, Method, Request}; use serde::Serialize; use url::Url; @@ -95,6 +95,28 @@ impl LobbyRequestCreator for ListGamesRequest { } } +pub struct GetGameRequest(String); + +impl GetGameRequest { + pub fn new(id: impl ToString) -> Self { + Self(id.to_string()) + } +} + +impl LobbyRequest for GetGameRequest { + type Response = Game; +} + +impl LobbyRequestCreator for GetGameRequest { + fn path(&self) -> Cow { + encode(&["a", "games", self.0.as_str()]) + } + + fn create(&self, url: Url) -> Request { + Request::new(Method::GET, url) + } +} + pub struct JoinGameRequest(String); impl JoinGameRequest { @@ -158,6 +180,8 @@ fn encode(parts: &[&str]) -> Cow<'static, str> { #[cfg(test)] mod tests { + use std::net::{IpAddr, Ipv4Addr, SocketAddr}; + use de_lobby_model::{GameConfig, GameMap, User}; use super::*; @@ -214,6 +238,7 @@ mod tests { ); let request = CreateGameRequest::new(GameSetup::new("127.0.0.1:8082".parse().unwrap(), config)); + assert_eq!(request.path().as_ref(), "/a/games"); let request = request.create(Url::parse("http://example.com/a/games").unwrap()); diff --git a/crates/lobby_model/src/games.rs b/crates/lobby_model/src/games.rs index c0f363800..467291dc3 100644 --- a/crates/lobby_model/src/games.rs +++ b/crates/lobby_model/src/games.rs @@ -104,7 +104,7 @@ impl validation::Validatable for GameSetup { } } -#[derive(Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GameConfig { name: String, diff --git a/crates/menu/Cargo.toml b/crates/menu/Cargo.toml index 4414d2b4d..fbd52f551 100644 --- a/crates/menu/Cargo.toml +++ b/crates/menu/Cargo.toml @@ -18,6 +18,7 @@ de_gui.workspace = true de_lobby_client.workspace = true de_lobby_model.workspace = true de_map.workspace = true +de_multiplayer.workspace = true # Other async-std.workspace = true diff --git a/crates/menu/src/create.rs b/crates/menu/src/create.rs index dccae1da9..f3dd9ceb4 100644 --- a/crates/menu/src/create.rs +++ b/crates/menu/src/create.rs @@ -1,18 +1,15 @@ -use std::net::SocketAddr; - use bevy::prelude::*; use de_gui::{ ButtonCommands, ButtonOps, GuiCommands, LabelCommands, OuterStyle, TextBoxCommands, TextBoxQuery, ToastEvent, }; -use de_lobby_client::CreateGameRequest; -use de_lobby_model::{GameConfig, GameMap, GameSetup, Validatable}; +use de_lobby_model::{GameConfig, GameMap, Validatable}; use de_map::hash::MapHash; use crate::{ mapselection::{MapSelectedEvent, SelectMapEvent}, menu::Menu, - requests::{Receiver, RequestsPlugin, Sender}, + setup::GameConfigRes, MenuState, }; @@ -20,8 +17,7 @@ pub(crate) struct CreateGamePlugin; impl Plugin for CreateGamePlugin { fn build(&self, app: &mut App) { - app.add_plugin(RequestsPlugin::::new()) - .add_event::() + app.add_event::() .add_system(setup.in_schedule(OnEnter(MenuState::GameCreation))) .add_system(cleanup.in_schedule(OnExit(MenuState::GameCreation))) .add_system( @@ -40,8 +36,7 @@ impl Plugin for CreateGamePlugin { .run_if(on_event::()) .after(CreateSet::Buttons) .after(CreateSet::MapSelected), - ) - .add_system(response_system.run_if(in_state(MenuState::GameCreation))); + ); } } @@ -237,11 +232,12 @@ fn map_selected_system( } fn create_game_system( + mut commands: Commands, inputs: Res, texts: TextBoxQuery, selected_map: Option>, mut toasts: EventWriter, - mut sender: Sender, + mut next_state: ResMut>, ) { let Some(selected_map) = selected_map else { toasts.send(ToastEvent::new("No map selected.")); @@ -257,26 +253,12 @@ fn create_game_system( } }; - let game_server: SocketAddr = "127.0.0.1:8082".parse().unwrap(); let game_config = GameConfig::new(name, max_players, selected_map.0.clone()); - let game_setup = GameSetup::new(game_server, game_config); - if let Err(error) = game_setup.validate() { + if let Err(error) = game_config.validate() { toasts.send(ToastEvent::new(format!("{error}"))); return; } - sender.send(CreateGameRequest::new(game_setup)); -} - -fn response_system( - mut next_state: ResMut>, - mut receiver: Receiver, - mut toasts: EventWriter, -) { - if let Some(result) = receiver.receive() { - match result { - Ok(_) => next_state.set(MenuState::MultiPlayerGame), - Err(error) => toasts.send(ToastEvent::new(error)), - } - } + commands.insert_resource(GameConfigRes::new(game_config)); + next_state.set(MenuState::GameSetup); } diff --git a/crates/menu/src/lib.rs b/crates/menu/src/lib.rs index f6c380572..79227c81d 100644 --- a/crates/menu/src/lib.rs +++ b/crates/menu/src/lib.rs @@ -20,6 +20,7 @@ mod mainmenu; mod mapselection; mod menu; mod requests; +mod setup; mod signin; mod singleplayer; @@ -60,6 +61,7 @@ pub(crate) enum MenuState { SignIn, GameListing, GameCreation, + GameSetup, MultiPlayerGame, AfterGame, } diff --git a/crates/menu/src/setup.rs b/crates/menu/src/setup.rs new file mode 100644 index 000000000..3901da700 --- /dev/null +++ b/crates/menu/src/setup.rs @@ -0,0 +1,89 @@ +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; + +use bevy::prelude::*; +use de_gui::ToastEvent; +use de_lobby_client::CreateGameRequest; +use de_lobby_model::{GameConfig, GameSetup}; +use de_multiplayer::{ + GameOpenedEvent, NetGameConf, ServerPort, ShutdownMultiplayerEvent, StartMultiplayerEvent, +}; + +use crate::{ + requests::{Receiver, RequestsPlugin, Sender}, + MenuState, +}; + +pub(crate) struct CreateGamePlugin; + +impl Plugin for CreateGamePlugin { + fn build(&self, app: &mut App) { + app.add_plugin(RequestsPlugin::::new()) + .add_system(setup_network.in_schedule(OnEnter(MenuState::GameSetup))) + .add_system(cleanup.in_schedule(OnExit(MenuState::GameSetup))) + .add_system( + create_game_in_lobby + .run_if(in_state(MenuState::GameSetup)) + .run_if(on_event::()), + ) + .add_system(handle_lobby_response.run_if(in_state(MenuState::GameSetup))); + } +} + +#[derive(Resource)] +pub(crate) struct GameConfigRes(GameConfig); + +impl GameConfigRes { + pub(crate) fn new(config: GameConfig) -> Self { + Self(config) + } +} + +fn setup_network(config: Res, mut multiplayer: EventWriter) { + multiplayer.send(StartMultiplayerEvent::new(NetGameConf::new( + config.0.max_players().try_into().unwrap(), + // TODO not localhost + IpAddr::V4(Ipv4Addr::LOCALHOST), + // TODO not fixed port + ServerPort::Main(8082), + ))); +} + +fn cleanup(mut commands: Commands) { + commands.remove_resource::(); + + // shutdown multiplayer if not entering MenuState::MultiPlayerGame +} + +fn create_game_in_lobby( + config: Res, + mut opened_events: EventReader, + mut sender: Sender, +) { + let Some(opened_event) = opened_events.iter().last() else { + return; + }; + + // TODO not localhost + let server = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), opened_event.0); + let game_setup = GameSetup::new(server, config.0.clone()); + sender.send(CreateGameRequest::new(game_setup)); +} + +fn handle_lobby_response( + mut next_state: ResMut>, + mut receiver: Receiver, + mut multiplayer: EventWriter, + mut toasts: EventWriter, +) { + if let Some(result) = receiver.receive() { + match result { + Ok(_) => next_state.set(MenuState::MultiPlayerGame), + Err(error) => { + multiplayer.send(ShutdownMultiplayerEvent); + toasts.send(ToastEvent::new(error)); + } + } + } +} + +// TODO handle multiplayer errors diff --git a/crates/multiplayer/src/game.rs b/crates/multiplayer/src/game.rs index ecaf4ae38..de3f5e985 100644 --- a/crates/multiplayer/src/game.rs +++ b/crates/multiplayer/src/game.rs @@ -16,7 +16,8 @@ pub(crate) struct GamePlugin; impl Plugin for GamePlugin { fn build(&self, app: &mut App) { - app.add_system(setup.in_schedule(OnEnter(NetState::Connected))) + app.add_event::() + .add_system(setup.in_schedule(OnEnter(NetState::Connected))) .add_system(cleanup.in_schedule(OnEnter(NetState::None))) .add_system(open_or_join.in_schedule(OnEnter(NetState::Connected))) .add_system( @@ -35,6 +36,9 @@ impl Plugin for GamePlugin { } } +/// A new game on the given port was just opened. +pub struct GameOpenedEvent(pub u16); + #[derive(Resource)] pub(crate) struct Players { local: Option, @@ -74,6 +78,7 @@ fn process_from_server( mut ports: ResMut, mut events: EventReader, mut outputs: EventWriter>, + mut opened: EventWriter, mut fatals: EventWriter, ) { for event in events.iter() { @@ -86,6 +91,7 @@ fn process_from_server( info!("Game on port {} opened.", *port); // Send something to open NAT. outputs.send(ToGame::Ping(u32::MAX).into()); + opened.send(GameOpenedEvent(*port)); } Err(err) => { fatals.send(FatalErrorEvent::new(format!("Invalid GameOpened: {err:?}"))); diff --git a/crates/multiplayer/src/lib.rs b/crates/multiplayer/src/lib.rs index c2253a2e6..e29b66262 100644 --- a/crates/multiplayer/src/lib.rs +++ b/crates/multiplayer/src/lib.rs @@ -15,6 +15,7 @@ use stats::StatsPlugin; pub use crate::{ config::{NetGameConf, ServerPort}, + game::GameOpenedEvent, lifecycle::{ShutdownMultiplayerEvent, StartMultiplayerEvent}, netstate::NetState, };