diff --git a/crates/lobby_model/src/games.rs b/crates/lobby_model/src/games.rs index c388e1bd..9cd8a41e 100644 --- a/crates/lobby_model/src/games.rs +++ b/crates/lobby_model/src/games.rs @@ -36,7 +36,7 @@ impl Game { } } -#[derive(Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GamePlayer { username: String, @@ -57,7 +57,7 @@ impl GamePlayer { } } -#[derive(Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GamePlayerInfo { ordinal: u8, diff --git a/crates/menu/src/multiplayer/joined.rs b/crates/menu/src/multiplayer/joined.rs deleted file mode 100644 index d8424064..00000000 --- a/crates/menu/src/multiplayer/joined.rs +++ /dev/null @@ -1,19 +0,0 @@ -use bevy::prelude::*; -use de_core::state::AppState; -use de_multiplayer::ShutdownMultiplayerEvent; - -use super::MultiplayerState; - -pub(crate) struct JoinedGamePlugin; - -impl Plugin for JoinedGamePlugin { - fn build(&self, app: &mut App) { - app.add_systems(OnExit(MultiplayerState::GameJoined), cleanup); - } -} - -fn cleanup(state: Res>, mut shutdown: EventWriter) { - if state.as_ref() != &AppState::InGame { - shutdown.send(ShutdownMultiplayerEvent); - } -} diff --git a/crates/menu/src/multiplayer/joined/mod.rs b/crates/menu/src/multiplayer/joined/mod.rs new file mode 100644 index 00000000..6cce1247 --- /dev/null +++ b/crates/menu/src/multiplayer/joined/mod.rs @@ -0,0 +1,14 @@ +use bevy::prelude::*; + +use self::{state::JoinedGameStatePlugin, ui::JoinedGameUiPlugin}; + +mod state; +mod ui; + +pub(super) struct JoinedGamePlugin; + +impl Plugin for JoinedGamePlugin { + fn build(&self, app: &mut App) { + app.add_plugins((JoinedGameStatePlugin, JoinedGameUiPlugin)); + } +} diff --git a/crates/menu/src/multiplayer/joined/state.rs b/crates/menu/src/multiplayer/joined/state.rs new file mode 100644 index 00000000..77c5708f --- /dev/null +++ b/crates/menu/src/multiplayer/joined/state.rs @@ -0,0 +1,60 @@ +use bevy::prelude::*; +use de_core::state::AppState; +use de_gui::ToastEvent; +use de_lobby_client::GetGameRequest; +use de_multiplayer::{PeerJoinedEvent, PeerLeftEvent, ShutdownMultiplayerEvent}; + +use super::ui::RefreshPlayersEvent; +use crate::multiplayer::{ + current::GameNameRes, + requests::{Receiver, Sender}, + MultiplayerState, +}; + +pub(super) struct JoinedGameStatePlugin; + +impl Plugin for JoinedGameStatePlugin { + fn build(&self, app: &mut App) { + app.add_systems(OnEnter(MultiplayerState::GameJoined), refresh) + .add_systems(OnExit(MultiplayerState::GameJoined), cleanup) + .add_systems( + Update, + ( + refresh + .run_if(on_event::().or_else(on_event::())), + handle_get_response, + ) + .run_if(in_state(MultiplayerState::GameJoined)), + ); + } +} + +fn cleanup(state: Res>, mut shutdown: EventWriter) { + if state.as_ref() != &AppState::InGame { + shutdown.send(ShutdownMultiplayerEvent); + } +} + +fn refresh(game_name: Res, mut sender: Sender) { + info!("Refreshing game info..."); + sender.send(GetGameRequest::new(game_name.name_owned())); +} + +fn handle_get_response( + mut multi_state: ResMut>, + mut receiver: Receiver, + mut refresh: EventWriter, + mut toasts: EventWriter, +) { + while let Some(result) = receiver.receive() { + match result { + Ok(game) => { + refresh.send(RefreshPlayersEvent::from_slice(game.players())); + } + Err(error) => { + toasts.send(ToastEvent::new(error)); + multi_state.set(MultiplayerState::SignIn); + } + } + } +} diff --git a/crates/menu/src/multiplayer/joined/ui.rs b/crates/menu/src/multiplayer/joined/ui.rs new file mode 100644 index 00000000..d0069916 --- /dev/null +++ b/crates/menu/src/multiplayer/joined/ui.rs @@ -0,0 +1,121 @@ +use bevy::prelude::*; +use de_gui::{GuiCommands, LabelCommands, OuterStyle}; +use de_lobby_model::GamePlayer; + +use crate::{menu::Menu, multiplayer::MultiplayerState}; + +pub(super) struct JoinedGameUiPlugin; + +impl Plugin for JoinedGameUiPlugin { + fn build(&self, app: &mut App) { + app.add_event::() + .add_systems(OnEnter(MultiplayerState::GameJoined), setup) + .add_systems(OnExit(MultiplayerState::GameJoined), cleanup) + .add_systems( + PostUpdate, + refresh + .run_if(in_state(MultiplayerState::GameJoined)) + .run_if(on_event::()), + ); + } +} + +#[derive(Event)] +pub(super) struct RefreshPlayersEvent(Vec); + +impl RefreshPlayersEvent { + pub(super) fn from_slice(players: &[GamePlayer]) -> Self { + Self(players.to_vec()) + } +} + +#[derive(Resource)] +struct PlayersBoxRes(Entity); + +fn setup(mut commands: GuiCommands, menu: Res) { + let players_box_id = players_box(&mut commands, menu.root_node()); + commands.insert_resource(PlayersBoxRes(players_box_id)); +} + +fn cleanup(mut commands: Commands) { + commands.remove_resource::(); +} + +fn players_box(commands: &mut GuiCommands, parent_id: Entity) -> Entity { + let column_id = commands + .spawn(NodeBundle { + style: Style { + flex_direction: FlexDirection::Column, + width: Val::Percent(80.), + height: Val::Percent(80.), + margin: UiRect::all(Val::Auto), + align_items: AlignItems::Center, + justify_content: JustifyContent::FlexStart, + ..default() + }, + ..default() + }) + .id(); + commands.entity(parent_id).add_child(column_id); + column_id +} + +fn refresh( + mut commands: GuiCommands, + mut events: EventReader, + box_id: Res, +) { + let Some(event) = events.iter().last() else { + return; + }; + + commands.entity(box_id.0).despawn_descendants(); + + for player in event.0.iter() { + let row_id = row(&mut commands, player); + commands.entity(box_id.0).add_child(row_id); + } +} + +fn row(commands: &mut GuiCommands, player: &GamePlayer) -> Entity { + let row_id = commands + .spawn(NodeBundle { + style: Style { + flex_direction: FlexDirection::Row, + width: Val::Percent(100.), + height: Val::Percent(8.), + margin: UiRect::vertical(Val::Percent(0.5)), + align_items: AlignItems::Center, + justify_content: JustifyContent::FlexStart, + ..default() + }, + ..default() + }) + .id(); + + let ordinal_id = commands + .spawn_label( + OuterStyle { + width: Val::Percent(25.), + height: Val::Percent(100.), + margin: UiRect::right(Val::Percent(5.)), + }, + format!("P{}", player.info().ordinal()), + ) + .id(); + commands.entity(row_id).add_child(ordinal_id); + + let username_id = commands + .spawn_label( + OuterStyle { + width: Val::Percent(70.), + height: Val::Percent(100.), + ..default() + }, + player.username(), + ) + .id(); + commands.entity(row_id).add_child(username_id); + + row_id +} diff --git a/crates/multiplayer/src/game.rs b/crates/multiplayer/src/game.rs index ebee85cd..fde6a457 100644 --- a/crates/multiplayer/src/game.rs +++ b/crates/multiplayer/src/game.rs @@ -20,6 +20,8 @@ impl Plugin for GamePlugin { fn build(&self, app: &mut App) { app.add_event::() .add_event::() + .add_event::() + .add_event::() .add_systems(OnEnter(NetState::Connected), open_or_join) .add_systems( PreMovement, @@ -56,6 +58,24 @@ impl GameJoinedEvent { } } +#[derive(Event)] +pub struct PeerJoinedEvent(u8); + +impl PeerJoinedEvent { + pub fn id(&self) -> u8 { + self.0 + } +} + +#[derive(Event)] +pub struct PeerLeftEvent(u8); + +impl PeerLeftEvent { + pub fn id(&self) -> u8 { + self.0 + } +} + fn open_or_join( conf: Res, mut main_server: EventWriter, @@ -118,6 +138,8 @@ fn process_from_game( mut fatals: EventWriter, state: Res>, mut joined_events: EventWriter, + mut peer_joined_events: EventWriter, + mut peer_left_events: EventWriter, mut next_state: ResMut>, ) { for event in inputs.iter() { @@ -169,9 +191,11 @@ fn process_from_game( } FromGame::PeerJoined(id) => { info!("Peer {id} joined."); + peer_joined_events.send(PeerJoinedEvent(*id)); } FromGame::PeerLeft(id) => { info!("Peer {id} left."); + peer_left_events.send(PeerLeftEvent(*id)); } FromGame::GameReadiness(readiness) => { info!("Game readiness changed to: {readiness:?}"); diff --git a/crates/multiplayer/src/lib.rs b/crates/multiplayer/src/lib.rs index f8604f49..dbea8dc1 100644 --- a/crates/multiplayer/src/lib.rs +++ b/crates/multiplayer/src/lib.rs @@ -15,7 +15,7 @@ use stats::StatsPlugin; pub use crate::{ config::{ConnectionType, NetGameConf}, - game::{GameJoinedEvent, GameOpenedEvent}, + game::{GameJoinedEvent, GameOpenedEvent, PeerJoinedEvent, PeerLeftEvent}, lifecycle::{MultiplayerShuttingDownEvent, ShutdownMultiplayerEvent, StartMultiplayerEvent}, netstate::NetState, };