Initial commit

This commit is contained in:
Isaac Mills 2024-02-28 22:56:54 -05:00
commit 47f61c9eab
Signed by: fnmain
GPG key ID: B67D7410F33A0F61
16 changed files with 8455 additions and 0 deletions

116
src/logo.rs Normal file
View 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
View 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
View 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
View 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
View 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());
}