Initial commit
This commit is contained in:
commit
47f61c9eab
16 changed files with 8455 additions and 0 deletions
116
src/logo.rs
Normal file
116
src/logo.rs
Normal file
|
@ -0,0 +1,116 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use bevy::{prelude::*, utils::Instant};
|
||||
use bevy_asset_loader::asset_collection::AssetCollection;
|
||||
use keyframe::functions::*;
|
||||
|
||||
use crate::AppState;
|
||||
|
||||
const FADE_IN_TIME: f32 = 2.5;
|
||||
const ZOOM_OUT_TIME: f32 = 2.0;
|
||||
const FULL_FADE_FINISHED: f32 = 4.0;
|
||||
const FADE_OUT_TIME: f32 = 1.0;
|
||||
const QUIT_TIME: f32 = 0.5;
|
||||
const LOGO_FINISHED_SCALE: f32 = 0.2;
|
||||
|
||||
pub struct LogoPlugin;
|
||||
|
||||
#[derive(AssetCollection, Resource)]
|
||||
pub struct LogoAssets {
|
||||
#[asset(path = "logo.ktx2")]
|
||||
logo_texture: Handle<Image>,
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
struct LogoData {
|
||||
logo_entity: Entity,
|
||||
}
|
||||
|
||||
impl Plugin for LogoPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(OnEnter(AppState::Logo), load_logo)
|
||||
.add_systems(Update, fade_in_logo.run_if(in_state(AppState::Logo)))
|
||||
.add_systems(OnExit(AppState::Logo), cleanup_logo);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
struct LogoTimer(Instant, Instant);
|
||||
|
||||
impl LogoTimer {
|
||||
fn elapsed_secs(&self) -> f32 {
|
||||
self.0.elapsed().as_secs_f32()
|
||||
}
|
||||
|
||||
fn finished(&self) -> bool {
|
||||
Instant::now() > self.1
|
||||
}
|
||||
}
|
||||
|
||||
fn load_logo(mut commands: Commands, assets: Res<LogoAssets>) {
|
||||
let now = Instant::now();
|
||||
commands.insert_resource(ClearColor(Color::BLACK));
|
||||
let sprite_entity = commands
|
||||
.spawn((
|
||||
SpriteBundle {
|
||||
texture: assets.logo_texture.clone(),
|
||||
sprite: Sprite {
|
||||
color: Color::rgba(1.0, 1.0, 1.0, 0.0),
|
||||
..Default::default()
|
||||
},
|
||||
transform: Transform::from_scale(Vec3::ONE),
|
||||
..Default::default()
|
||||
},
|
||||
LogoTimer(
|
||||
now,
|
||||
now + Duration::from_secs_f32(QUIT_TIME + FULL_FADE_FINISHED + FADE_OUT_TIME),
|
||||
),
|
||||
))
|
||||
.id();
|
||||
commands.spawn(Camera2dBundle::default());
|
||||
commands.insert_resource(LogoData {
|
||||
logo_entity: sprite_entity,
|
||||
});
|
||||
}
|
||||
|
||||
fn fade_in_logo(
|
||||
mut query: Query<(&mut Sprite, &mut Transform, &LogoTimer)>,
|
||||
mut next_state: ResMut<NextState<AppState>>,
|
||||
keyboard_input: Res<ButtonInput<KeyCode>>,
|
||||
) {
|
||||
let (mut sprite, mut transform, timer) = query.single_mut();
|
||||
let elapsed = timer.elapsed_secs();
|
||||
|
||||
if timer.finished() || keyboard_input.get_pressed().len() > 0 {
|
||||
next_state.set(AppState::LoadingMenu);
|
||||
} else if elapsed > FULL_FADE_FINISHED {
|
||||
sprite.color.set_a(keyframe::ease_with_scaled_time(
|
||||
EaseInCubic,
|
||||
FULL_FADE_FINISHED,
|
||||
0.0,
|
||||
elapsed,
|
||||
FADE_OUT_TIME + FULL_FADE_FINISHED,
|
||||
));
|
||||
} else {
|
||||
sprite.color.set_a(keyframe::ease_with_scaled_time(
|
||||
Linear,
|
||||
0.0,
|
||||
1.0,
|
||||
elapsed,
|
||||
FADE_IN_TIME,
|
||||
));
|
||||
transform.scale = Vec3::splat(keyframe::ease_with_scaled_time(
|
||||
EaseOutQuint,
|
||||
2.0,
|
||||
LOGO_FINISHED_SCALE,
|
||||
elapsed,
|
||||
ZOOM_OUT_TIME,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn cleanup_logo(mut commands: Commands, logo_data: Res<LogoData>) {
|
||||
commands.entity(logo_data.logo_entity).despawn_recursive();
|
||||
commands.remove_resource::<LogoData>();
|
||||
commands.remove_resource::<LogoAssets>();
|
||||
}
|
49
src/main.rs
Normal file
49
src/main.rs
Normal file
|
@ -0,0 +1,49 @@
|
|||
use bevy::prelude::*;
|
||||
use bevy_asset_loader::loading_state::{
|
||||
config::ConfigureLoadingState, LoadingState, LoadingStateAppExt,
|
||||
};
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
mod logo;
|
||||
mod menu;
|
||||
mod networking;
|
||||
mod rooms;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, States)]
|
||||
pub enum AppState {
|
||||
#[default]
|
||||
LoadingLogo,
|
||||
Logo,
|
||||
LoadingMenu,
|
||||
Menu,
|
||||
Rooms,
|
||||
InGame,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let _ = tracing_subscriber::fmt()
|
||||
.with_env_filter(EnvFilter::from_default_env())
|
||||
.try_init();
|
||||
|
||||
App::new()
|
||||
.init_state::<AppState>()
|
||||
.add_loading_state(
|
||||
LoadingState::new(AppState::LoadingLogo)
|
||||
.continue_to_state(AppState::Logo)
|
||||
.load_collection::<logo::LogoAssets>(),
|
||||
)
|
||||
.add_loading_state(
|
||||
LoadingState::new(AppState::LoadingMenu)
|
||||
.continue_to_state(AppState::Menu)
|
||||
.load_collection::<menu::MenuAssets>(),
|
||||
)
|
||||
.add_plugins((
|
||||
DefaultPlugins,
|
||||
logo::LogoPlugin,
|
||||
menu::MenuPlugin,
|
||||
rooms::RoomsPlugin,
|
||||
))
|
||||
.add_plugins(bevy_egui::EguiPlugin)
|
||||
// .add_plugins(EditorPlugin::default())
|
||||
.run();
|
||||
}
|
175
src/menu.rs
Normal file
175
src/menu.rs
Normal file
|
@ -0,0 +1,175 @@
|
|||
use bevy::{app::AppExit, prelude::*};
|
||||
use bevy_asset_loader::asset_collection::AssetCollection;
|
||||
|
||||
use crate::AppState;
|
||||
|
||||
pub struct MenuPlugin;
|
||||
|
||||
#[derive(Resource, AssetCollection)]
|
||||
pub struct MenuAssets {
|
||||
#[asset(path = "logo_bw.ktx2")]
|
||||
pub logo: Handle<Image>,
|
||||
}
|
||||
|
||||
const NORMAL_BUTTON: Color = Color::BLACK;
|
||||
|
||||
impl Plugin for MenuPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(OnEnter(AppState::Menu), setup_menu)
|
||||
.add_systems(Update, menu.run_if(in_state(AppState::Menu)))
|
||||
.add_systems(OnExit(AppState::Menu), cleanup_menu);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
enum MenuButton {
|
||||
Play,
|
||||
Quit,
|
||||
}
|
||||
|
||||
#[derive(Resource, Clone, Copy)]
|
||||
pub struct MenuData {
|
||||
pub button_entity: Entity,
|
||||
pub logo_bw_entity: Entity,
|
||||
}
|
||||
|
||||
fn cleanup_menu(mut commands: Commands, menu_data: Res<MenuData>) {
|
||||
commands.entity(menu_data.button_entity).despawn_recursive();
|
||||
}
|
||||
|
||||
fn menu(
|
||||
mut next_state: ResMut<NextState<AppState>>,
|
||||
mut interaction_query: Query<(&Interaction, &MenuButton), Changed<Interaction>>,
|
||||
mut app_exit_events: EventWriter<AppExit>,
|
||||
) {
|
||||
for (interaction, button_type) in &mut interaction_query {
|
||||
match *interaction {
|
||||
Interaction::Pressed => match *button_type {
|
||||
MenuButton::Play => next_state.set(AppState::Rooms),
|
||||
MenuButton::Quit => {
|
||||
app_exit_events.send(AppExit);
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_menu(
|
||||
mut commands: Commands,
|
||||
assets: Res<MenuAssets>,
|
||||
mut clear_color: ResMut<ClearColor>,
|
||||
) {
|
||||
clear_color.0 = Color::GRAY;
|
||||
let logo_bw_entity = commands
|
||||
.spawn(NodeBundle {
|
||||
style: Style {
|
||||
// center button
|
||||
width: Val::Percent(100.),
|
||||
height: Val::Percent(100.),
|
||||
justify_content: JustifyContent::FlexStart,
|
||||
align_items: AlignItems::End,
|
||||
..default()
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.with_children(|parent| {
|
||||
parent.spawn(ImageBundle {
|
||||
style: Style {
|
||||
width: Val::Auto,
|
||||
height: Val::Auto,
|
||||
justify_content: JustifyContent::FlexStart,
|
||||
align_items: AlignItems::Start,
|
||||
..Default::default()
|
||||
},
|
||||
image: UiImage {
|
||||
texture: assets.logo.clone(),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
})
|
||||
.id();
|
||||
let button_entity = commands
|
||||
.spawn(NodeBundle {
|
||||
style: Style {
|
||||
// center button
|
||||
width: Val::Percent(100.),
|
||||
height: Val::Percent(100.),
|
||||
justify_content: JustifyContent::Center,
|
||||
align_items: AlignItems::Center,
|
||||
flex_direction: FlexDirection::Column,
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
})
|
||||
.with_children(|parent| {
|
||||
parent
|
||||
.spawn((
|
||||
ButtonBundle {
|
||||
style: Style {
|
||||
width: Val::Px(150.),
|
||||
height: Val::Px(65.),
|
||||
// horizontally center child text
|
||||
justify_content: JustifyContent::Center,
|
||||
// vertically center child text
|
||||
align_items: AlignItems::Center,
|
||||
margin: UiRect {
|
||||
bottom: Val::Px(16.0),
|
||||
..Default::default()
|
||||
},
|
||||
..default()
|
||||
},
|
||||
background_color: NORMAL_BUTTON.into(),
|
||||
..default()
|
||||
},
|
||||
MenuButton::Play,
|
||||
))
|
||||
.with_children(|parent| {
|
||||
parent.spawn(TextBundle::from_section(
|
||||
"Play",
|
||||
TextStyle {
|
||||
font_size: 40.0,
|
||||
color: Color::rgb(0.9, 0.9, 0.9),
|
||||
..default()
|
||||
},
|
||||
));
|
||||
});
|
||||
parent
|
||||
.spawn((
|
||||
ButtonBundle {
|
||||
style: Style {
|
||||
width: Val::Px(150.),
|
||||
height: Val::Px(65.),
|
||||
// horizontally center child text
|
||||
justify_content: JustifyContent::Center,
|
||||
// vertically center child text
|
||||
align_items: AlignItems::Center,
|
||||
margin: UiRect {
|
||||
bottom: Val::Px(16.0),
|
||||
..Default::default()
|
||||
},
|
||||
..default()
|
||||
},
|
||||
background_color: NORMAL_BUTTON.into(),
|
||||
..default()
|
||||
},
|
||||
MenuButton::Quit,
|
||||
))
|
||||
.with_children(|parent| {
|
||||
parent.spawn(TextBundle::from_section(
|
||||
"Quit",
|
||||
TextStyle {
|
||||
font_size: 40.0,
|
||||
color: Color::rgb(0.9, 0.9, 0.9),
|
||||
..default()
|
||||
},
|
||||
));
|
||||
});
|
||||
})
|
||||
.id();
|
||||
commands.insert_resource(MenuData {
|
||||
button_entity,
|
||||
logo_bw_entity,
|
||||
});
|
||||
}
|
238
src/networking.rs
Normal file
238
src/networking.rs
Normal file
|
@ -0,0 +1,238 @@
|
|||
use async_std::io;
|
||||
use bevy::ecs::system::Resource;
|
||||
use flume::{Receiver, Sender};
|
||||
use futures::{executor::block_on, future::FutureExt, stream::StreamExt};
|
||||
use libp2p::{
|
||||
core::multiaddr::{Multiaddr, Protocol},
|
||||
dcutr, gossipsub, identify, identity, noise, ping, relay,
|
||||
swarm::{NetworkBehaviour, SwarmEvent},
|
||||
tcp, yamux, PeerId,
|
||||
};
|
||||
use rand::Rng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{borrow::Cow, hash::Hash};
|
||||
use std::{cell::OnceCell, sync::OnceLock, time::Duration};
|
||||
use std::{collections::hash_map::DefaultHasher, hash::Hasher};
|
||||
use yoke::{Yoke, Yokeable};
|
||||
|
||||
#[derive(NetworkBehaviour)]
|
||||
pub struct Behaviour {
|
||||
relay_client: relay::client::Behaviour,
|
||||
ping: ping::Behaviour,
|
||||
identify: identify::Behaviour,
|
||||
gossipsub: gossipsub::Behaviour,
|
||||
dcutr: dcutr::Behaviour,
|
||||
}
|
||||
|
||||
#[derive(Resource, Clone)]
|
||||
pub struct NetworkManager {
|
||||
pub events: Receiver<NetworkEvent>,
|
||||
pub command_sender: Sender<NetworkCommand>,
|
||||
pub peer_id: OnceLock<String>,
|
||||
}
|
||||
|
||||
pub enum NetworkCommand {
|
||||
Dial(PeerId),
|
||||
Publish(Message<'static>),
|
||||
}
|
||||
|
||||
pub enum NetworkEvent {
|
||||
Event(SwarmEvent<BehaviourEvent>),
|
||||
Message(PeerId, Yoke<(u32, Message<'static>), Vec<u8>>),
|
||||
LocalPeerID(PeerId),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Yokeable, Debug)]
|
||||
pub enum Message<'a> {
|
||||
Chat { content: Cow<'a, str> },
|
||||
}
|
||||
|
||||
const RELAY_ID: &'static str =
|
||||
"/dns4/nations.lol/tcp/4001/p2p/12D3KooWMh5tkGf7NDKD2FbKypGBbMyyv4TVmkXRAbWwvyftixU6";
|
||||
|
||||
pub async fn open_network(
|
||||
command_rx: flume::Receiver<NetworkCommand>,
|
||||
event_tx: flume::Sender<NetworkEvent>,
|
||||
) {
|
||||
let relay_address: Multiaddr = RELAY_ID.parse().unwrap();
|
||||
let mut command_rx = command_rx.into_stream();
|
||||
let mut swarm = libp2p::SwarmBuilder::with_existing_identity(generate_ed25519(1))
|
||||
.with_async_std()
|
||||
.with_tcp(
|
||||
tcp::Config::default().port_reuse(true).nodelay(true),
|
||||
noise::Config::new,
|
||||
yamux::Config::default,
|
||||
)
|
||||
.unwrap()
|
||||
.with_dns()
|
||||
.await
|
||||
.unwrap()
|
||||
.with_relay_client(noise::Config::new, yamux::Config::default)
|
||||
.unwrap()
|
||||
.with_behaviour(|keypair, relay_behaviour| {
|
||||
// To content-address message, we can take the hash of message and use it as an ID.
|
||||
let message_id_fn = |message: &gossipsub::Message| {
|
||||
let mut s = DefaultHasher::new();
|
||||
message.data.hash(&mut s);
|
||||
gossipsub::MessageId::from(s.finish().to_string())
|
||||
};
|
||||
// Set a custom gossipsub configuration
|
||||
let gossipsub_config = gossipsub::ConfigBuilder::default()
|
||||
.heartbeat_interval(Duration::from_secs(10)) // This is set to aid debugging by not cluttering the log space
|
||||
.validation_mode(gossipsub::ValidationMode::Strict) // This sets the kind of message validation. The default is Strict (enforce message signing)
|
||||
.message_id_fn(message_id_fn) // content-address messages. No two messages of the same content will be propagated.
|
||||
.build()
|
||||
.map_err(|msg| io::Error::new(io::ErrorKind::Other, msg))?; // Temporary hack because `build` does not return a proper `std::error::Error`.
|
||||
|
||||
// build a gossipsub network behaviour
|
||||
let gossipsub = gossipsub::Behaviour::new(
|
||||
gossipsub::MessageAuthenticity::Signed(keypair.clone()),
|
||||
gossipsub_config,
|
||||
)?;
|
||||
|
||||
Ok(Behaviour {
|
||||
relay_client: relay_behaviour,
|
||||
ping: ping::Behaviour::new(ping::Config::new()),
|
||||
identify: identify::Behaviour::new(identify::Config::new(
|
||||
"/TODO/0.0.1".to_string(),
|
||||
keypair.public(),
|
||||
)),
|
||||
gossipsub,
|
||||
dcutr: dcutr::Behaviour::new(keypair.public().to_peer_id()),
|
||||
})
|
||||
})
|
||||
.unwrap()
|
||||
.with_swarm_config(|c| c.with_idle_connection_timeout(Duration::from_secs(60)))
|
||||
.build();
|
||||
|
||||
// Create a Gossipsub topic
|
||||
let topic = gossipsub::IdentTopic::new("test-net");
|
||||
// subscribes to our topic
|
||||
swarm.behaviour_mut().gossipsub.subscribe(&topic).unwrap();
|
||||
|
||||
swarm
|
||||
.listen_on("/ip4/0.0.0.0/tcp/0".parse().unwrap())
|
||||
.unwrap();
|
||||
|
||||
// Wait to listen on all interfaces.
|
||||
block_on(async {
|
||||
let mut delay = futures_timer::Delay::new(std::time::Duration::from_secs(1)).fuse();
|
||||
loop {
|
||||
futures::select! {
|
||||
event = swarm.next() => {
|
||||
match event.unwrap() {
|
||||
SwarmEvent::NewListenAddr { address, .. } => {
|
||||
tracing::info!(%address, "Listening on address");
|
||||
}
|
||||
event => panic!("{event:?}"),
|
||||
}
|
||||
}
|
||||
_ = delay => {
|
||||
// Likely listening on all interfaces now, thus continuing by breaking the loop.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Connect to the relay server. Not for the reservation or relayed connection, but to (a) learn
|
||||
// our local public address and (b) enable a freshly started relay to learn its public address.
|
||||
swarm.dial(relay_address.clone()).unwrap();
|
||||
block_on(async {
|
||||
let mut learned_observed_addr = false;
|
||||
let mut told_relay_observed_addr = false;
|
||||
|
||||
loop {
|
||||
match swarm.next().await.unwrap() {
|
||||
SwarmEvent::NewListenAddr { .. } => {}
|
||||
SwarmEvent::Dialing { .. } => {}
|
||||
SwarmEvent::ConnectionEstablished { .. } => {}
|
||||
SwarmEvent::Behaviour(BehaviourEvent::Ping(_)) => {}
|
||||
SwarmEvent::Behaviour(BehaviourEvent::Identify(identify::Event::Sent {
|
||||
..
|
||||
})) => {
|
||||
tracing::info!("Told relay its public address");
|
||||
told_relay_observed_addr = true;
|
||||
}
|
||||
SwarmEvent::Behaviour(BehaviourEvent::Identify(identify::Event::Received {
|
||||
info: identify::Info { observed_addr, .. },
|
||||
..
|
||||
})) => {
|
||||
tracing::info!(address=%observed_addr, "Relay told us our observed address");
|
||||
learned_observed_addr = true;
|
||||
}
|
||||
// event => panic!("{event:?}"),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if learned_observed_addr && told_relay_observed_addr {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
swarm
|
||||
.listen_on(relay_address.clone().with(Protocol::P2pCircuit))
|
||||
.unwrap();
|
||||
// match opts.mode {
|
||||
// Mode::Dial => {
|
||||
// swarm
|
||||
// .dial(
|
||||
// opts.relay_address
|
||||
// .with(Protocol::P2pCircuit)
|
||||
// .with(Protocol::P2p(opts.remote_peer_id.unwrap())),
|
||||
// )
|
||||
// .unwrap();
|
||||
// }
|
||||
// Mode::Listen => {
|
||||
// }
|
||||
// }
|
||||
|
||||
event_tx
|
||||
.send(NetworkEvent::LocalPeerID(swarm.local_peer_id().clone()))
|
||||
.unwrap();
|
||||
|
||||
block_on(async {
|
||||
loop {
|
||||
futures::select! {
|
||||
command = command_rx.select_next_some() => match command {
|
||||
NetworkCommand::Dial(addr) => swarm.dial(
|
||||
relay_address.clone()
|
||||
.with(Protocol::P2pCircuit)
|
||||
.with(Protocol::P2p(addr)),
|
||||
).unwrap(),
|
||||
NetworkCommand::Publish(data) => swarm.behaviour_mut().gossipsub.publish(topic.clone(), postcard::to_stdvec(&(rand::thread_rng().gen::<u32>(), data)).unwrap()).map(|_| ()).unwrap(),
|
||||
},
|
||||
event = swarm.select_next_some() => match event {
|
||||
SwarmEvent::ConnectionEstablished {
|
||||
peer_id, endpoint, ..
|
||||
} => {
|
||||
tracing::info!(peer=%peer_id, ?endpoint, "Established new connection");
|
||||
swarm.behaviour_mut().gossipsub.add_explicit_peer(&peer_id);
|
||||
}
|
||||
SwarmEvent::Behaviour(BehaviourEvent::Gossipsub(gossipsub::Event::Message { message, .. })) => {
|
||||
let peer_id = message.source.unwrap();
|
||||
let message: Yoke<(u32, Message<'static>), Vec<u8>> = Yoke::try_attach_to_cart(message.data, |c| postcard::from_bytes(c)).unwrap();
|
||||
event_tx.send(NetworkEvent::Message(peer_id, message)).unwrap();
|
||||
}
|
||||
event => event_tx.send(NetworkEvent::Event(event)).unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_ed25519(key_id: u8) -> identity::Keypair {
|
||||
match std::fs::read(format!("key-{}.protobuf", key_id)) {
|
||||
Ok(key) => identity::Keypair::from_protobuf_encoding(&key).unwrap(),
|
||||
Err(_) => {
|
||||
let key = identity::Keypair::generate_ed25519();
|
||||
std::fs::write(
|
||||
format!("key-{}.protobuf", key_id),
|
||||
key.to_protobuf_encoding().unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
key
|
||||
}
|
||||
}
|
||||
}
|
147
src/rooms.rs
Normal file
147
src/rooms.rs
Normal file
|
@ -0,0 +1,147 @@
|
|||
use std::sync::OnceLock;
|
||||
|
||||
use bevy::{prelude::*, tasks::IoTaskPool};
|
||||
use bevy_egui::egui::Align2;
|
||||
use bevy_egui::EguiContexts;
|
||||
use bevy_egui::{egui, EguiClipboard};
|
||||
use libp2p::{relay, swarm::SwarmEvent, PeerId};
|
||||
|
||||
use crate::{
|
||||
menu::{MenuAssets, MenuData},
|
||||
networking::{BehaviourEvent, NetworkEvent, NetworkManager},
|
||||
AppState,
|
||||
};
|
||||
|
||||
pub struct RoomsPlugin;
|
||||
|
||||
impl Plugin for RoomsPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(OnEnter(AppState::Rooms), setup_rooms)
|
||||
.add_systems(Update, rooms.run_if(in_state(AppState::Rooms)))
|
||||
.add_systems(OnExit(AppState::Rooms), cleanup_rooms);
|
||||
}
|
||||
}
|
||||
|
||||
fn cleanup_rooms(mut commands: Commands, menu_data: Res<MenuData>) {
|
||||
commands
|
||||
.entity(menu_data.logo_bw_entity)
|
||||
.despawn_recursive();
|
||||
commands.remove_resource::<MenuAssets>();
|
||||
}
|
||||
|
||||
#[derive(Resource, Default)]
|
||||
struct ChatBuffer {
|
||||
buffer: String,
|
||||
history: String,
|
||||
}
|
||||
|
||||
fn rooms(
|
||||
mut next_state: ResMut<NextState<AppState>>,
|
||||
mut text_buffer: ResMut<ChatBuffer>,
|
||||
net_manager: Res<NetworkManager>,
|
||||
mut clipboard_ctx: ResMut<EguiClipboard>,
|
||||
mut contexts: EguiContexts,
|
||||
) {
|
||||
egui::Window::new("Chat room")
|
||||
.anchor(Align2::CENTER_CENTER, egui::Vec2::ZERO)
|
||||
.movable(false)
|
||||
.show(contexts.ctx_mut(), |ui| {
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
ui.label(&text_buffer.history);
|
||||
});
|
||||
ui.text_edit_singleline(&mut text_buffer.buffer);
|
||||
if ui.button("Send").clicked() {
|
||||
if text_buffer.buffer.starts_with("/dial") {
|
||||
let address: PeerId = text_buffer
|
||||
.buffer
|
||||
.split(' ')
|
||||
.nth(1)
|
||||
.unwrap()
|
||||
.parse()
|
||||
.unwrap();
|
||||
|
||||
net_manager
|
||||
.command_sender
|
||||
.send(crate::networking::NetworkCommand::Dial(address))
|
||||
.unwrap();
|
||||
} else {
|
||||
let mut buffer = String::new();
|
||||
core::mem::swap(&mut buffer, &mut text_buffer.buffer);
|
||||
net_manager
|
||||
.command_sender
|
||||
.send(crate::networking::NetworkCommand::Publish(
|
||||
crate::networking::Message::Chat {
|
||||
content: std::borrow::Cow::Owned(buffer),
|
||||
},
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
text_buffer.buffer.clear();
|
||||
}
|
||||
if ui.button("Copy Peer ID").clicked() {
|
||||
clipboard_ctx.set_contents(net_manager.peer_id.get().unwrap().as_str());
|
||||
}
|
||||
});
|
||||
|
||||
if let Ok(event) = net_manager.events.try_recv() {
|
||||
match event {
|
||||
NetworkEvent::Event(event) => match event {
|
||||
SwarmEvent::NewListenAddr { address, .. } => {
|
||||
tracing::info!(%address, "Listening on address");
|
||||
}
|
||||
SwarmEvent::Behaviour(BehaviourEvent::RelayClient(
|
||||
relay::client::Event::ReservationReqAccepted { .. },
|
||||
)) => {
|
||||
// assert!(opts.mode == Mode::Listen);
|
||||
tracing::info!("Relay accepted our reservation request");
|
||||
}
|
||||
SwarmEvent::Behaviour(BehaviourEvent::RelayClient(event)) => {
|
||||
tracing::info!(?event)
|
||||
}
|
||||
SwarmEvent::Behaviour(BehaviourEvent::Dcutr(event)) => {
|
||||
tracing::info!(?event)
|
||||
}
|
||||
SwarmEvent::Behaviour(BehaviourEvent::Identify(event)) => {
|
||||
tracing::info!(?event)
|
||||
}
|
||||
SwarmEvent::Behaviour(BehaviourEvent::Ping(_)) => {}
|
||||
SwarmEvent::OutgoingConnectionError { peer_id, error, .. } => {
|
||||
tracing::info!(peer=?peer_id, "Outgoing connection failed: {error}");
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
NetworkEvent::LocalPeerID(id) => {
|
||||
std::fmt::Write::write_fmt(
|
||||
&mut text_buffer.history,
|
||||
format_args!("Peer ID: {}\n", id),
|
||||
)
|
||||
.unwrap();
|
||||
let mut peer_id = String::new();
|
||||
std::fmt::Write::write_fmt(&mut peer_id, format_args!("{}", id)).unwrap();
|
||||
net_manager.peer_id.set(peer_id).unwrap();
|
||||
}
|
||||
NetworkEvent::Message(p, m) => {
|
||||
std::fmt::Write::write_fmt(
|
||||
&mut text_buffer.history,
|
||||
format_args!("{:#?}\n", (p, m)),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_rooms(mut commands: Commands) {
|
||||
let task_pool = IoTaskPool::get();
|
||||
let (command_tx, command_rx) = flume::unbounded::<super::networking::NetworkCommand>();
|
||||
let (event_tx, event_rx) = flume::unbounded::<super::networking::NetworkEvent>();
|
||||
task_pool
|
||||
.spawn(super::networking::open_network(command_rx, event_tx))
|
||||
.detach();
|
||||
commands.insert_resource(NetworkManager {
|
||||
events: event_rx,
|
||||
command_sender: command_tx,
|
||||
peer_id: OnceLock::new(),
|
||||
});
|
||||
commands.insert_resource(ChatBuffer::default());
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue