├── wasm_client_example ├── index.scss ├── index.html ├── README.MD ├── Cargo.toml └── src │ ├── shared.rs │ └── main.rs ├── .gitignore ├── README.md ├── Cargo.toml ├── examples ├── shared.rs ├── server.rs └── client.rs └── src └── lib.rs /wasm_client_example/index.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | /wasm_client_example/target 4 | /wasm_client_example/dist 5 | -------------------------------------------------------------------------------- /wasm_client_example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /wasm_client_example/README.MD: -------------------------------------------------------------------------------- 1 | # `BEMW Wasm Client Example` 2 | 3 | This client is a wasm build version of the BEMW client example. It runs in your browser and proves that BEMW and Eventwork are able to communicate between browser and native apps. You can build and run it like any other Bevy WASM app. 4 | 5 | I use [Trunk](https://trunkrs.dev/) for testing. See the [Bevy Cheatbook WASM guide](https://bevy-cheatbook.github.io/platforms/wasm.html) for more information on running Bevy in the browser. 6 | -------------------------------------------------------------------------------- /wasm_client_example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasm_client_example" 3 | version = "0.8.0" 4 | authors = ["James ", "Neikos "] 5 | edition = "2021" 6 | description = "Wasm Client Example project" 7 | readme = "README.md" 8 | repository = "https://github.com/jamescarterbell/bevy_eventwork" 9 | license = "MIT" 10 | categories = ["game-development", "network-programming"] 11 | resolver = "2" 12 | 13 | 14 | [badges] 15 | maintenance = { status = "actively-developed" } 16 | 17 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 18 | 19 | [dependencies] 20 | # This is a bevy plugin 21 | bevy = "0.15.0" 22 | bevy_eventwork = { version = "0.10", default-features = false } 23 | bevy_eventwork_mod_websockets = { path = "../" } 24 | serde = { version = "1.0.190", features = ["derive"] } 25 | url = { version = "2.0.0" } 26 | -------------------------------------------------------------------------------- /wasm_client_example/src/shared.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use bevy_eventwork::NetworkMessage; 3 | use bevy_eventwork_mod_websockets::WebSocketProvider; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[derive(Serialize, Deserialize, Clone, Debug)] 7 | pub struct UserChatMessage { 8 | pub message: String, 9 | } 10 | 11 | impl NetworkMessage for UserChatMessage { 12 | const NAME: &'static str = "example:UserChatMessage"; 13 | } 14 | 15 | #[derive(Serialize, Deserialize, Clone, Debug)] 16 | pub struct NewChatMessage { 17 | pub name: String, 18 | pub message: String, 19 | } 20 | 21 | impl NetworkMessage for NewChatMessage { 22 | const NAME: &'static str = "example:NewChatMessage"; 23 | } 24 | 25 | #[allow(unused)] 26 | pub fn client_register_network_messages(app: &mut App) { 27 | use bevy_eventwork::AppNetworkMessage; 28 | 29 | // The client registers messages that arrives from the server, so that 30 | // it is prepared to handle them. Otherwise, an error occurs. 31 | app.listen_for_message::(); 32 | } 33 | 34 | #[allow(unused)] 35 | pub fn server_register_network_messages(app: &mut App) { 36 | use bevy_eventwork::AppNetworkMessage; 37 | 38 | // The server registers messages that arrives from a client, so that 39 | // it is prepared to handle them. Otherwise, an error occurs. 40 | app.listen_for_message::(); 41 | } 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `bevy_eventwork_mod_websockets` (BEMW) 2 | 3 | [![Following released Bevy versions](https://img.shields.io/badge/Bevy%20tracking-released%20version-lightblue)](https://bevyengine.org/learn/quick-start/plugin-development/#main-branch-tracking) 4 | [![crates.io](https://img.shields.io/crates/v/bevy_eventwork_mod_websockets)](https://crates.io/crates/bevy_eventwork_mod_websockets) 5 | [![docs.rs](https://docs.rs/bevy_eventwork_mod_websockets/badge.svg)](https://docs.rs/bevy_eventwork_mod_websockets) 6 | 7 | A crate that provides a websocket networking transport layer for [Bevy_eventwork](https://github.com/jamescarterbell/bevy_eventwork) that supports WASM and Native. 8 | 9 | ## Supported Platforms 10 | 11 | - WASM 12 | - Windows 13 | - Linux 14 | - Mac 15 | 16 | ## Getting Started 17 | 18 | See [Bevy_eventwork](https://github.com/jamescarterbell/bevy_eventwork) for details on how to use `bevy_eventwork`. 19 | 20 | The only difference from `bevy_eventworks` getting started directions is to disable `bevy_eventworks` default features and use this crates `WebSocketProvider` and `NetworkSettings`. 21 | Other than that the crate functions identically to stock bevy_eventworks. No features, changes, or manual shenanigans are needed to compile for WASM. 22 | It just works. 23 | 24 | ```rust 25 | app.add_plugins(bevy_eventwork::EventworkPlugin::< 26 | WebSocketProvider, 27 | bevy::tasks::TaskPool, 28 | >::default()); 29 | 30 | app.insert_resource(NetworkSettings::default()); 31 | 32 | ``` 33 | 34 | ## Supported Eventwork + Bevy Version 35 | 36 | | EventWork Version | BEMW Version | Bevy Version | 37 | | :---------------: | :----------: | :----------: | 38 | | 0.10 | 0.3 | 0.15 | 39 | | 0.9 | 0.2 | 0.14 | 40 | | 0.8 | 0.1 | 0.13 | 41 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy_eventwork_mod_websockets" 3 | version = "0.3.1" 4 | edition = "2021" 5 | resolver = "2" 6 | description = "A Websocket NetworkProvider for Bevy_eventwork" 7 | readme = "README.md" 8 | repository = "https://github.com/NoahShomette/bevy_eventwork_mod_websockets" 9 | license = "MIT" 10 | categories = ["game-development", "network-programming"] 11 | autoexamples = false 12 | exclude = ["wasm_client_example"] 13 | 14 | [badges] 15 | maintenance = { status = "actively-developed" } 16 | 17 | [[example]] 18 | name = "client" 19 | 20 | [[example]] 21 | name = "server" 22 | 23 | [workspace] 24 | members = ["wasm_client_example"] 25 | 26 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 27 | 28 | [dependencies] 29 | bevy_eventwork = { version = "0.10", default-features = false } 30 | # This is a bevy plugin 31 | bevy = { version = "0.15.0", features = [], default-features = false } 32 | # Used for on wire serialization 33 | bincode = "1.3.3" 34 | # Used for non-tokio dependent threaded message passing 35 | async-channel = "2.3.1" 36 | # Used for providers, which are async in nature 37 | async-trait = "0.1.74" 38 | # Websocket 39 | url = { version = "2.5.4" } 40 | futures = { version = "0.3.29" } 41 | 42 | # Used 1.33.0or Stream type and other ext 43 | futures-lite = "2.5.0" 44 | 45 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 46 | async-tungstenite = { version = "0.28.0", features = [ 47 | "async-std-runtime", 48 | "url", 49 | ] } 50 | async-std = { version = "1.12.0" } 51 | 52 | [target.'cfg(target_arch = "wasm32")'.dependencies] 53 | tokio-tungstenite-wasm = { version = "0.3.1" } 54 | send_wrapper = "^0.6" 55 | 56 | [dev-dependencies] 57 | bevy = { version = "0.15.0", features = ["default_font"] } 58 | serde = { version = "1.0.215", features = ["derive"] } 59 | serde_json = { version = "1.0.133" } 60 | -------------------------------------------------------------------------------- /examples/shared.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use bevy_eventwork::NetworkMessage; 3 | use bevy_eventwork_mod_websockets::WebSocketProvider; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | ///////////////////////////////////////////////////////////////////// 7 | // In this example the client sends `UserChatMessage`s to the server, 8 | // the server then broadcasts to all connected clients. 9 | // 10 | // We use two different types here, because only the server should 11 | // decide the identity of a given connection and thus also sends a 12 | // name. 13 | // 14 | // You can have a single message be sent both ways, it simply needs 15 | // to implement both `NetworkMessage" and both client and server can 16 | // send and recieve 17 | ///////////////////////////////////////////////////////////////////// 18 | 19 | #[derive(Serialize, Deserialize, Clone, Debug)] 20 | pub struct UserChatMessage { 21 | pub message: String, 22 | } 23 | 24 | impl NetworkMessage for UserChatMessage { 25 | const NAME: &'static str = "example:UserChatMessage"; 26 | } 27 | 28 | #[derive(Serialize, Deserialize, Clone, Debug)] 29 | pub struct NewChatMessage { 30 | pub name: String, 31 | pub message: String, 32 | } 33 | 34 | impl NetworkMessage for NewChatMessage { 35 | const NAME: &'static str = "example:NewChatMessage"; 36 | } 37 | 38 | #[allow(unused)] 39 | pub fn client_register_network_messages(app: &mut App) { 40 | use bevy_eventwork::AppNetworkMessage; 41 | 42 | // The client registers messages that arrives from the server, so that 43 | // it is prepared to handle them. Otherwise, an error occurs. 44 | app.listen_for_message::(); 45 | } 46 | 47 | #[allow(unused)] 48 | pub fn server_register_network_messages(app: &mut App) { 49 | use bevy_eventwork::AppNetworkMessage; 50 | 51 | // The server registers messages that arrives from a client, so that 52 | // it is prepared to handle them. Otherwise, an error occurs. 53 | app.listen_for_message::(); 54 | } 55 | -------------------------------------------------------------------------------- /examples/server.rs: -------------------------------------------------------------------------------- 1 | use bevy::tasks::TaskPool; 2 | use bevy::{prelude::*, tasks::TaskPoolBuilder}; 3 | use bevy_eventwork::{ConnectionId, EventworkRuntime, Network, NetworkData, NetworkEvent}; 4 | use bevy_eventwork_mod_websockets::{NetworkSettings, WebSocketProvider}; 5 | use std::net::{IpAddr, Ipv4Addr, SocketAddr}; 6 | 7 | mod shared; 8 | 9 | fn main() { 10 | let mut app = App::new(); 11 | app.add_plugins((MinimalPlugins, bevy::log::LogPlugin::default())); 12 | 13 | // Before we can register the potential message types, we 14 | // need to add the plugin 15 | app.add_plugins(bevy_eventwork::EventworkPlugin::< 16 | WebSocketProvider, 17 | bevy::tasks::TaskPool, 18 | >::default()); 19 | 20 | // Make sure you insert the EventworkRuntime resource with your chosen Runtime 21 | app.insert_resource(EventworkRuntime( 22 | TaskPoolBuilder::new().num_threads(2).build(), 23 | )); 24 | 25 | // A good way to ensure that you are not forgetting to register 26 | // any messages is to register them where they are defined! 27 | shared::server_register_network_messages(&mut app); 28 | 29 | app.add_systems(Startup, setup_networking); 30 | app.add_systems(Update, (handle_connection_events, handle_messages)); 31 | 32 | // We have to insert the WS [`NetworkSettings`] with our chosen settings. 33 | app.insert_resource(NetworkSettings::default()); 34 | 35 | app.run(); 36 | } 37 | 38 | // On the server side, you need to setup networking. You do not need to do so at startup, and can start listening 39 | // at any time. 40 | fn setup_networking( 41 | mut net: ResMut>, 42 | settings: Res, 43 | task_pool: Res>, 44 | ) { 45 | let ip_address = "127.0.0.1".parse().expect("Could not parse ip address"); 46 | 47 | info!("Address of the server: {}", ip_address); 48 | 49 | let _socket_address = SocketAddr::new(ip_address, 8080); 50 | 51 | match net.listen( 52 | SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8081), 53 | &task_pool.0, 54 | &settings, 55 | ) { 56 | Ok(_) => (), 57 | Err(err) => { 58 | error!("Could not start listening: {}", err); 59 | panic!(); 60 | } 61 | } 62 | 63 | info!("Started listening for new connections!"); 64 | } 65 | 66 | #[derive(Component)] 67 | struct Player(ConnectionId); 68 | 69 | fn handle_connection_events( 70 | mut commands: Commands, 71 | net: Res>, 72 | mut network_events: EventReader, 73 | ) { 74 | for event in network_events.read() { 75 | if let NetworkEvent::Connected(conn_id) = event { 76 | commands.spawn((Player(*conn_id),)); 77 | 78 | // Broadcasting sends the message to all connected players! (Including the just connected one in this case) 79 | net.broadcast(shared::NewChatMessage { 80 | name: String::from("SERVER"), 81 | message: format!("New user connected; {}", conn_id), 82 | }); 83 | info!("New player connected: {}", conn_id); 84 | } 85 | } 86 | } 87 | 88 | // Receiving a new message is as simple as listening for events of `NetworkData` 89 | fn handle_messages( 90 | mut new_messages: EventReader>, 91 | net: Res>, 92 | ) { 93 | for message in new_messages.read() { 94 | let user = message.source(); 95 | 96 | info!("Received message from user: {}", message.message); 97 | 98 | net.broadcast(shared::NewChatMessage { 99 | name: format!("{}", user), 100 | message: message.message.clone(), 101 | }); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /examples/client.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::type_complexity)] 2 | 3 | use bevy::{ 4 | color::palettes, 5 | prelude::*, 6 | tasks::{TaskPool, TaskPoolBuilder}, 7 | }; 8 | use bevy_eventwork::{ConnectionId, EventworkRuntime, Network, NetworkData, NetworkEvent}; 9 | use bevy_eventwork_mod_websockets::{NetworkSettings, WebSocketProvider}; 10 | 11 | mod shared; 12 | 13 | fn main() { 14 | let mut app = App::new(); 15 | 16 | app.add_plugins(DefaultPlugins); 17 | 18 | // You need to add the `EventworkPlugin` first before you can register 19 | // `ClientMessage`s 20 | app.add_plugins(bevy_eventwork::EventworkPlugin::< 21 | WebSocketProvider, 22 | bevy::tasks::TaskPool, 23 | >::default()); 24 | 25 | // Make sure you insert the EventworkRuntime resource with your chosen Runtime 26 | app.insert_resource(EventworkRuntime( 27 | TaskPoolBuilder::new().num_threads(2).build(), 28 | )); 29 | 30 | // A good way to ensure that you are not forgetting to register 31 | // any messages is to register them where they are defined! 32 | shared::client_register_network_messages(&mut app); 33 | 34 | app.add_systems(Startup, setup_ui); 35 | 36 | app.add_systems( 37 | Update, 38 | ( 39 | handle_connect_button, 40 | handle_message_button, 41 | handle_incoming_messages, 42 | handle_network_events, 43 | ), 44 | ); 45 | 46 | // We have to insert the WS [`NetworkSettings`] with our chosen settings. 47 | app.insert_resource(NetworkSettings::default()); 48 | 49 | app.init_resource::(); 50 | 51 | app.add_systems(PostUpdate, handle_chat_area); 52 | 53 | app.run(); 54 | } 55 | 56 | /////////////////////////////////////////////////////////////// 57 | ////////////// Incoming Message Handler /////////////////////// 58 | /////////////////////////////////////////////////////////////// 59 | 60 | fn handle_incoming_messages( 61 | mut messages: Query<&mut GameChatMessages>, 62 | mut new_messages: EventReader>, 63 | ) { 64 | let mut messages = messages.get_single_mut().unwrap(); 65 | 66 | for new_message in new_messages.read() { 67 | messages.add(UserMessage::new(&new_message.name, &new_message.message)); 68 | } 69 | } 70 | 71 | fn handle_network_events( 72 | mut new_network_events: EventReader, 73 | connect_query: Query<&Children, With>, 74 | mut text_query: Query<&mut Text>, 75 | mut messages: Query<&mut GameChatMessages>, 76 | ) { 77 | let connect_children = connect_query.get_single().unwrap(); 78 | let mut text = text_query.get_mut(connect_children[0]).unwrap(); 79 | let mut messages = messages.get_single_mut().unwrap(); 80 | 81 | for event in new_network_events.read() { 82 | info!("Received event"); 83 | match event { 84 | NetworkEvent::Connected(_) => { 85 | messages.add(SystemMessage::new( 86 | "Succesfully connected to server!".to_string(), 87 | )); 88 | text.0 = String::from("Disconnect"); 89 | } 90 | 91 | NetworkEvent::Disconnected(_) => { 92 | messages.add(SystemMessage::new("Disconnected from server!".to_string())); 93 | text.0 = String::from("Connect to server"); 94 | } 95 | NetworkEvent::Error(err) => { 96 | messages.add(UserMessage::new(String::from("SYSTEM"), err.to_string())); 97 | } 98 | } 99 | } 100 | } 101 | 102 | /////////////////////////////////////////////////////////////// 103 | ////////////// Data Definitions /////////////////////////////// 104 | /////////////////////////////////////////////////////////////// 105 | 106 | #[derive(Resource)] 107 | struct GlobalChatSettings { 108 | chat_style: (TextFont, TextColor), 109 | author_style: (TextFont, TextColor), 110 | } 111 | 112 | impl FromWorld for GlobalChatSettings { 113 | fn from_world(_world: &mut World) -> Self { 114 | GlobalChatSettings { 115 | chat_style: ( 116 | TextFont::from_font_size(20.0), 117 | TextColor::from(Color::BLACK), 118 | ), 119 | author_style: ( 120 | TextFont::from_font_size(20.0), 121 | TextColor::from(palettes::css::RED), 122 | ), 123 | } 124 | } 125 | } 126 | 127 | enum ChatMessage { 128 | SystemMessage(SystemMessage), 129 | UserMessage(UserMessage), 130 | } 131 | 132 | impl ChatMessage { 133 | fn get_author(&self) -> String { 134 | match self { 135 | ChatMessage::SystemMessage(_) => "SYSTEM".to_string(), 136 | ChatMessage::UserMessage(UserMessage { user, .. }) => user.clone(), 137 | } 138 | } 139 | 140 | fn get_text(&self) -> String { 141 | match self { 142 | ChatMessage::SystemMessage(SystemMessage(msg)) => msg.clone(), 143 | ChatMessage::UserMessage(UserMessage { message, .. }) => message.clone(), 144 | } 145 | } 146 | } 147 | 148 | impl From for ChatMessage { 149 | fn from(other: SystemMessage) -> ChatMessage { 150 | ChatMessage::SystemMessage(other) 151 | } 152 | } 153 | 154 | impl From for ChatMessage { 155 | fn from(other: UserMessage) -> ChatMessage { 156 | ChatMessage::UserMessage(other) 157 | } 158 | } 159 | 160 | struct SystemMessage(String); 161 | 162 | impl SystemMessage { 163 | fn new>(msg: T) -> SystemMessage { 164 | Self(msg.into()) 165 | } 166 | } 167 | 168 | #[derive(Component)] 169 | struct UserMessage { 170 | user: String, 171 | message: String, 172 | } 173 | 174 | impl UserMessage { 175 | fn new, M: Into>(user: U, message: M) -> Self { 176 | UserMessage { 177 | user: user.into(), 178 | message: message.into(), 179 | } 180 | } 181 | } 182 | 183 | #[derive(Component)] 184 | struct ChatMessages { 185 | messages: Vec, 186 | } 187 | 188 | impl ChatMessages { 189 | fn new() -> Self { 190 | ChatMessages { messages: vec![] } 191 | } 192 | 193 | fn add>(&mut self, msg: K) { 194 | let msg = msg.into(); 195 | self.messages.push(msg); 196 | } 197 | } 198 | 199 | type GameChatMessages = ChatMessages; 200 | 201 | /////////////////////////////////////////////////////////////// 202 | ////////////// UI Definitions/Handlers //////////////////////// 203 | /////////////////////////////////////////////////////////////// 204 | 205 | #[derive(Component)] 206 | struct ConnectButton; 207 | 208 | fn handle_connect_button( 209 | net: ResMut>, 210 | settings: Res, 211 | interaction_query: Query< 212 | (&Interaction, &Children), 213 | (Changed, With), 214 | >, 215 | mut text_query: Query<&mut Text>, 216 | mut messages: Query<&mut GameChatMessages>, 217 | task_pool: Res>, 218 | ) { 219 | let mut messages = if let Ok(messages) = messages.get_single_mut() { 220 | messages 221 | } else { 222 | return; 223 | }; 224 | 225 | for (interaction, children) in interaction_query.iter() { 226 | let mut text = text_query.get_mut(children[0]).unwrap(); 227 | if let Interaction::Pressed = interaction { 228 | if net.has_connections() { 229 | net.disconnect(ConnectionId { id: 0 }) 230 | .expect("Couldn't disconnect from server!"); 231 | } else { 232 | text.0 = String::from("Connecting..."); 233 | messages.add(SystemMessage::new("Connecting to server...")); 234 | 235 | net.connect( 236 | url::Url::parse("ws://127.0.0.1:8081").unwrap(), 237 | &task_pool.0, 238 | &settings, 239 | ); 240 | } 241 | } 242 | } 243 | } 244 | 245 | #[derive(Component)] 246 | struct MessageButton; 247 | 248 | fn handle_message_button( 249 | net: Res>, 250 | interaction_query: Query<&Interaction, (Changed, With)>, 251 | mut messages: Query<&mut GameChatMessages>, 252 | ) { 253 | let mut messages = if let Ok(messages) = messages.get_single_mut() { 254 | messages 255 | } else { 256 | return; 257 | }; 258 | 259 | for interaction in interaction_query.iter() { 260 | if let Interaction::Pressed = interaction { 261 | match net.send_message( 262 | ConnectionId { id: 0 }, 263 | shared::UserChatMessage { 264 | message: String::from("Hello there!"), 265 | }, 266 | ) { 267 | Ok(()) => (), 268 | Err(err) => messages.add(SystemMessage::new(format!( 269 | "Could not send message: {}", 270 | err 271 | ))), 272 | } 273 | } 274 | } 275 | } 276 | 277 | #[derive(Component)] 278 | struct ChatArea; 279 | 280 | fn handle_chat_area( 281 | chat_settings: Res, 282 | messages: Query<&GameChatMessages, Changed>, 283 | mut chat_text_query: Query<(Entity, &mut Text), With>, 284 | mut read_messages_index: Local, 285 | mut commands: Commands, 286 | ) { 287 | let messages = if let Ok(messages) = messages.get_single() { 288 | messages 289 | } else { 290 | return; 291 | }; 292 | let (text_entity, _) = chat_text_query.get_single_mut().unwrap(); 293 | 294 | for message_index in *read_messages_index..messages.messages.len() { 295 | let message = &messages.messages[message_index]; 296 | let new_message = commands 297 | .spawn(( 298 | Text::new(format!("{}:", message.get_author())), 299 | chat_settings.author_style.clone(), 300 | )) 301 | .with_child(( 302 | TextSpan::new(format!("{}\n", message.get_text())), 303 | chat_settings.chat_style.clone(), 304 | )) 305 | .id(); 306 | commands.entity(text_entity).add_children(&[new_message]); 307 | } 308 | 309 | *read_messages_index = messages.messages.len(); 310 | } 311 | 312 | fn setup_ui(mut commands: Commands, _materials: ResMut>) { 313 | commands.spawn(Camera2d); 314 | 315 | commands.spawn((GameChatMessages::new(),)); 316 | 317 | commands 318 | .spawn(( 319 | Node { 320 | width: Val::Percent(100.0), 321 | height: Val::Percent(100.0), 322 | justify_content: JustifyContent::SpaceBetween, 323 | flex_direction: FlexDirection::ColumnReverse, 324 | ..default() 325 | }, 326 | Into::::into(Color::NONE), 327 | )) 328 | .with_children(|parent| { 329 | parent 330 | .spawn(Node { 331 | width: Val::Percent(100.0), 332 | height: Val::Percent(90.0), 333 | ..default() 334 | }) 335 | .with_children(|parent| { 336 | parent 337 | .spawn(( 338 | Text::default(), 339 | Node { 340 | flex_direction: FlexDirection::Column, 341 | ..default() 342 | }, 343 | )) 344 | .insert(ChatArea); 345 | }); 346 | parent 347 | .spawn(( 348 | Node { 349 | width: Val::Percent(100.0), 350 | height: Val::Percent(10.0), 351 | ..default() 352 | }, 353 | Into::::into(palettes::css::GRAY), 354 | )) 355 | .with_children(|parent_button_bar| { 356 | parent_button_bar 357 | .spawn(( 358 | Button, 359 | Node { 360 | width: Val::Percent(50.0), 361 | height: Val::Percent(100.0), 362 | align_items: AlignItems::Center, 363 | justify_content: JustifyContent::Center, 364 | ..default() 365 | }, 366 | )) 367 | .insert(MessageButton) 368 | .with_children(|button| { 369 | button.spawn(( 370 | Text::new("Send Message!"), 371 | TextFont::from_font_size(40.0), 372 | TextColor::from(Color::BLACK), 373 | TextLayout::new_with_justify(JustifyText::Center), 374 | )); 375 | }); 376 | 377 | parent_button_bar 378 | .spawn(( 379 | Button, 380 | Node { 381 | width: Val::Percent(50.0), 382 | height: Val::Percent(100.0), 383 | align_items: AlignItems::Center, 384 | justify_content: JustifyContent::Center, 385 | ..default() 386 | }, 387 | )) 388 | .insert(ConnectButton) 389 | .with_children(|button| { 390 | button.spawn(( 391 | Text::new("Connect to server"), 392 | TextFont::from_font_size(40.0), 393 | TextColor::from(Color::BLACK), 394 | TextLayout::new_with_justify(JustifyText::Center), 395 | )); 396 | }); 397 | }); 398 | }); 399 | } 400 | -------------------------------------------------------------------------------- /wasm_client_example/src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::type_complexity)] 2 | 3 | use bevy::{ 4 | color::palettes, 5 | prelude::*, 6 | tasks::{TaskPool, TaskPoolBuilder}, 7 | }; 8 | use bevy_eventwork::{ConnectionId, EventworkRuntime, Network, NetworkData, NetworkEvent}; 9 | 10 | use bevy_eventwork_mod_websockets::{NetworkSettings, WebSocketProvider}; 11 | 12 | mod shared; 13 | 14 | fn main() { 15 | let mut app = App::new(); 16 | 17 | app.add_plugins(DefaultPlugins); 18 | 19 | // You need to add the `ClientPlugin` first before you can register 20 | // `ClientMessage`s 21 | app.add_plugins(bevy_eventwork::EventworkPlugin::< 22 | WebSocketProvider, 23 | bevy::tasks::TaskPool, 24 | >::default()); 25 | 26 | // Make sure you insert the EventworkRuntime resource with your chosen Runtime 27 | app.insert_resource(EventworkRuntime( 28 | TaskPoolBuilder::new().num_threads(2).build(), 29 | )); 30 | 31 | // A good way to ensure that you are not forgetting to register 32 | // any messages is to register them where they are defined! 33 | shared::client_register_network_messages(&mut app); 34 | 35 | app.add_systems(Startup, setup_ui); 36 | 37 | app.add_systems( 38 | Update, 39 | ( 40 | handle_connect_button, 41 | handle_message_button, 42 | handle_incoming_messages, 43 | handle_network_events, 44 | ), 45 | ); 46 | 47 | // We have to insert the TCP [`NetworkSettings`] with our chosen settings. 48 | app.insert_resource(NetworkSettings::default()); 49 | 50 | app.init_resource::(); 51 | 52 | app.add_systems(PostUpdate, handle_chat_area); 53 | 54 | app.run(); 55 | } 56 | 57 | /////////////////////////////////////////////////////////////// 58 | ////////////// Incoming Message Handler /////////////////////// 59 | /////////////////////////////////////////////////////////////// 60 | 61 | fn handle_incoming_messages( 62 | mut messages: Query<&mut GameChatMessages>, 63 | mut new_messages: EventReader>, 64 | ) { 65 | let mut messages = messages.get_single_mut().unwrap(); 66 | 67 | for new_message in new_messages.read() { 68 | messages.add(UserMessage::new(&new_message.name, &new_message.message)); 69 | } 70 | } 71 | 72 | fn handle_network_events( 73 | mut new_network_events: EventReader, 74 | connect_query: Query<&Children, With>, 75 | mut text_query: Query<&mut Text>, 76 | mut messages: Query<&mut GameChatMessages>, 77 | ) { 78 | let connect_children = connect_query.get_single().unwrap(); 79 | let mut text = text_query.get_mut(connect_children[0]).unwrap(); 80 | let mut messages = messages.get_single_mut().unwrap(); 81 | 82 | for event in new_network_events.read() { 83 | info!("Received event"); 84 | match event { 85 | NetworkEvent::Connected(_) => { 86 | messages.add(SystemMessage::new( 87 | "Succesfully connected to server!".to_string(), 88 | )); 89 | text.0 = String::from("Disconnect"); 90 | } 91 | 92 | NetworkEvent::Disconnected(_) => { 93 | messages.add(SystemMessage::new("Disconnected from server!".to_string())); 94 | text.0 = String::from("Connect to server"); 95 | } 96 | NetworkEvent::Error(err) => { 97 | messages.add(UserMessage::new(String::from("SYSTEM"), err.to_string())); 98 | } 99 | } 100 | } 101 | } 102 | 103 | /////////////////////////////////////////////////////////////// 104 | ////////////// Data Definitions /////////////////////////////// 105 | /////////////////////////////////////////////////////////////// 106 | 107 | #[derive(Resource)] 108 | struct GlobalChatSettings { 109 | chat_style: (TextFont, TextColor), 110 | author_style: (TextFont, TextColor), 111 | } 112 | 113 | impl FromWorld for GlobalChatSettings { 114 | fn from_world(_world: &mut World) -> Self { 115 | GlobalChatSettings { 116 | chat_style: ( 117 | TextFont::from_font_size(20.0), 118 | TextColor::from(Color::BLACK), 119 | ), 120 | author_style: ( 121 | TextFont::from_font_size(20.0), 122 | TextColor::from(palettes::css::RED), 123 | ), 124 | } 125 | } 126 | } 127 | 128 | enum ChatMessage { 129 | SystemMessage(SystemMessage), 130 | UserMessage(UserMessage), 131 | } 132 | 133 | impl ChatMessage { 134 | fn get_author(&self) -> String { 135 | match self { 136 | ChatMessage::SystemMessage(_) => "SYSTEM".to_string(), 137 | ChatMessage::UserMessage(UserMessage { user, .. }) => user.clone(), 138 | } 139 | } 140 | 141 | fn get_text(&self) -> String { 142 | match self { 143 | ChatMessage::SystemMessage(SystemMessage(msg)) => msg.clone(), 144 | ChatMessage::UserMessage(UserMessage { message, .. }) => message.clone(), 145 | } 146 | } 147 | } 148 | 149 | impl From for ChatMessage { 150 | fn from(other: SystemMessage) -> ChatMessage { 151 | ChatMessage::SystemMessage(other) 152 | } 153 | } 154 | 155 | impl From for ChatMessage { 156 | fn from(other: UserMessage) -> ChatMessage { 157 | ChatMessage::UserMessage(other) 158 | } 159 | } 160 | 161 | struct SystemMessage(String); 162 | 163 | impl SystemMessage { 164 | fn new>(msg: T) -> SystemMessage { 165 | Self(msg.into()) 166 | } 167 | } 168 | 169 | #[derive(Component)] 170 | struct UserMessage { 171 | user: String, 172 | message: String, 173 | } 174 | 175 | impl UserMessage { 176 | fn new, M: Into>(user: U, message: M) -> Self { 177 | UserMessage { 178 | user: user.into(), 179 | message: message.into(), 180 | } 181 | } 182 | } 183 | 184 | #[derive(Component)] 185 | struct ChatMessages { 186 | messages: Vec, 187 | } 188 | 189 | impl ChatMessages { 190 | fn new() -> Self { 191 | ChatMessages { messages: vec![] } 192 | } 193 | 194 | fn add>(&mut self, msg: K) { 195 | let msg = msg.into(); 196 | self.messages.push(msg); 197 | } 198 | } 199 | 200 | type GameChatMessages = ChatMessages; 201 | 202 | /////////////////////////////////////////////////////////////// 203 | ////////////// UI Definitions/Handlers //////////////////////// 204 | /////////////////////////////////////////////////////////////// 205 | 206 | #[derive(Component)] 207 | struct ConnectButton; 208 | 209 | fn handle_connect_button( 210 | net: ResMut>, 211 | settings: Res, 212 | interaction_query: Query< 213 | (&Interaction, &Children), 214 | (Changed, With), 215 | >, 216 | mut text_query: Query<&mut Text>, 217 | mut messages: Query<&mut GameChatMessages>, 218 | task_pool: Res>, 219 | ) { 220 | let mut messages = if let Ok(messages) = messages.get_single_mut() { 221 | messages 222 | } else { 223 | return; 224 | }; 225 | 226 | for (interaction, children) in interaction_query.iter() { 227 | let mut text = text_query.get_mut(children[0]).unwrap(); 228 | if let Interaction::Pressed = interaction { 229 | if net.has_connections() { 230 | net.disconnect(ConnectionId { id: 0 }) 231 | .expect("Couldn't disconnect from server!"); 232 | } else { 233 | text.0 = String::from("Connecting..."); 234 | messages.add(SystemMessage::new("Connecting to server...")); 235 | 236 | net.connect( 237 | url::Url::parse("ws://127.0.0.1:8081").unwrap(), 238 | &task_pool.0, 239 | &settings, 240 | ); 241 | } 242 | } 243 | } 244 | } 245 | 246 | #[derive(Component)] 247 | struct MessageButton; 248 | 249 | fn handle_message_button( 250 | net: Res>, 251 | interaction_query: Query<&Interaction, (Changed, With)>, 252 | mut messages: Query<&mut GameChatMessages>, 253 | ) { 254 | let mut messages = if let Ok(messages) = messages.get_single_mut() { 255 | messages 256 | } else { 257 | return; 258 | }; 259 | 260 | for interaction in interaction_query.iter() { 261 | if let Interaction::Pressed = interaction { 262 | match net.send_message( 263 | ConnectionId { id: 0 }, 264 | shared::UserChatMessage { 265 | message: String::from("Hello there!"), 266 | }, 267 | ) { 268 | Ok(()) => (), 269 | Err(err) => messages.add(SystemMessage::new(format!( 270 | "Could not send message: {}", 271 | err 272 | ))), 273 | } 274 | } 275 | } 276 | } 277 | 278 | #[derive(Component)] 279 | struct ChatArea; 280 | 281 | fn handle_chat_area( 282 | chat_settings: Res, 283 | messages: Query<&GameChatMessages, Changed>, 284 | mut chat_text_query: Query<(Entity, &mut Text), With>, 285 | mut read_messages_index: Local, 286 | mut commands: Commands, 287 | ) { 288 | let messages = if let Ok(messages) = messages.get_single() { 289 | messages 290 | } else { 291 | return; 292 | }; 293 | let (text_entity, _) = chat_text_query.get_single_mut().unwrap(); 294 | 295 | for message_index in *read_messages_index..messages.messages.len() { 296 | let message = &messages.messages[message_index]; 297 | let new_message = commands 298 | .spawn(( 299 | Text::new(format!("{}:", message.get_author())), 300 | chat_settings.author_style.clone(), 301 | )) 302 | .with_child(( 303 | TextSpan::new(format!("{}\n", message.get_text())), 304 | chat_settings.chat_style.clone(), 305 | )) 306 | .id(); 307 | commands.entity(text_entity).add_children(&[new_message]); 308 | } 309 | 310 | *read_messages_index = messages.messages.len(); 311 | } 312 | 313 | fn setup_ui(mut commands: Commands, _materials: ResMut>) { 314 | commands.spawn(Camera2d); 315 | 316 | commands.spawn((GameChatMessages::new(),)); 317 | 318 | commands 319 | .spawn(( 320 | Node { 321 | width: Val::Percent(100.0), 322 | height: Val::Percent(100.0), 323 | justify_content: JustifyContent::SpaceBetween, 324 | flex_direction: FlexDirection::ColumnReverse, 325 | ..default() 326 | }, 327 | Into::::into(Color::NONE), 328 | )) 329 | .with_children(|parent| { 330 | parent 331 | .spawn(Node { 332 | width: Val::Percent(100.0), 333 | height: Val::Percent(90.0), 334 | ..default() 335 | }) 336 | .with_children(|parent| { 337 | parent 338 | .spawn(( 339 | Text::default(), 340 | Node { 341 | flex_direction: FlexDirection::Column, 342 | ..default() 343 | }, 344 | )) 345 | .insert(ChatArea); 346 | }); 347 | parent 348 | .spawn(( 349 | Node { 350 | width: Val::Percent(100.0), 351 | height: Val::Percent(10.0), 352 | ..default() 353 | }, 354 | Into::::into(palettes::css::GRAY), 355 | )) 356 | .with_children(|parent_button_bar| { 357 | parent_button_bar 358 | .spawn(( 359 | Button, 360 | Node { 361 | width: Val::Percent(50.0), 362 | height: Val::Percent(100.0), 363 | align_items: AlignItems::Center, 364 | justify_content: JustifyContent::Center, 365 | ..default() 366 | }, 367 | )) 368 | .insert(MessageButton) 369 | .with_children(|button| { 370 | button.spawn(( 371 | Text::new("Send Message!"), 372 | TextFont::from_font_size(40.0), 373 | TextColor::from(Color::BLACK), 374 | TextLayout::new_with_justify(JustifyText::Center), 375 | )); 376 | }); 377 | 378 | parent_button_bar 379 | .spawn(( 380 | Button, 381 | Node { 382 | width: Val::Percent(50.0), 383 | height: Val::Percent(100.0), 384 | align_items: AlignItems::Center, 385 | justify_content: JustifyContent::Center, 386 | ..default() 387 | }, 388 | )) 389 | .insert(ConnectButton) 390 | .with_children(|button| { 391 | button.spawn(( 392 | Text::new("Connect to server"), 393 | TextFont::from_font_size(40.0), 394 | TextColor::from(Color::BLACK), 395 | TextLayout::new_with_justify(JustifyText::Center), 396 | )); 397 | }); 398 | }); 399 | }); 400 | } 401 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /// A provider for WebSockets 2 | #[cfg(not(target_arch = "wasm32"))] 3 | pub type WebSocketProvider = native_websocket::NativeWesocketProvider; 4 | 5 | /// A provider for WebSockets 6 | #[cfg(target_arch = "wasm32")] 7 | pub type WebSocketProvider = wasm_websocket::WasmWebSocketProvider; 8 | 9 | #[cfg(not(target_arch = "wasm32"))] 10 | pub use native_websocket::NetworkSettings; 11 | 12 | #[cfg(target_arch = "wasm32")] 13 | pub use wasm_websocket::NetworkSettings; 14 | 15 | #[cfg(not(target_arch = "wasm32"))] 16 | mod native_websocket { 17 | use std::{net::SocketAddr, pin::Pin}; 18 | 19 | use async_channel::{Receiver, Sender}; 20 | use async_std::net::{TcpListener, TcpStream}; 21 | use async_trait::async_trait; 22 | use async_tungstenite::{ 23 | tungstenite::{protocol::WebSocketConfig, Message}, 24 | WebSocketStream, 25 | }; 26 | use bevy::prelude::{error, info, trace, Deref, DerefMut, Resource}; 27 | use bevy_eventwork::{error::NetworkError, managers::NetworkProvider, NetworkPacket}; 28 | use futures::{ 29 | stream::{SplitSink, SplitStream}, 30 | SinkExt, StreamExt, 31 | }; 32 | use futures_lite::{Future, FutureExt, Stream}; 33 | 34 | /// A provider for WebSockets 35 | #[derive(Default, Debug)] 36 | pub struct NativeWesocketProvider; 37 | 38 | #[cfg_attr(not(target_arch = "wasm32"), async_trait)] 39 | #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] 40 | impl NetworkProvider for NativeWesocketProvider { 41 | type NetworkSettings = NetworkSettings; 42 | 43 | type Socket = WebSocketStream; 44 | 45 | type ReadHalf = SplitStream>; 46 | 47 | type WriteHalf = SplitSink, Message>; 48 | 49 | type ConnectInfo = url::Url; 50 | 51 | type AcceptInfo = SocketAddr; 52 | 53 | type AcceptStream = OwnedIncoming; 54 | 55 | async fn accept_loop( 56 | accept_info: Self::AcceptInfo, 57 | _: Self::NetworkSettings, 58 | ) -> Result { 59 | let listener = TcpListener::bind(accept_info) 60 | .await 61 | .map_err(NetworkError::Listen)?; 62 | Ok(OwnedIncoming::new(listener)) 63 | } 64 | 65 | async fn connect_task( 66 | connect_info: Self::ConnectInfo, 67 | network_settings: Self::NetworkSettings, 68 | ) -> Result { 69 | info!("Beginning connection"); 70 | let (stream, _response) = async_tungstenite::async_std::connect_async_with_config( 71 | connect_info, 72 | Some(*network_settings), 73 | ) 74 | .await 75 | .map_err(|error| match error { 76 | async_tungstenite::tungstenite::Error::ConnectionClosed => { 77 | NetworkError::Error(String::from("Connection closed")) 78 | } 79 | async_tungstenite::tungstenite::Error::AlreadyClosed => { 80 | NetworkError::Error(String::from("Connection was already closed")) 81 | } 82 | async_tungstenite::tungstenite::Error::Io(io_error) => { 83 | NetworkError::Error(format!("Io Error: {}", io_error)) 84 | } 85 | async_tungstenite::tungstenite::Error::Tls(tls_error) => { 86 | NetworkError::Error(format!("Tls Error: {}", tls_error)) 87 | } 88 | async_tungstenite::tungstenite::Error::Capacity(cap) => { 89 | NetworkError::Error(format!("Capacity Error: {}", cap)) 90 | } 91 | async_tungstenite::tungstenite::Error::Protocol(proto) => { 92 | NetworkError::Error(format!("Protocol Error: {}", proto)) 93 | } 94 | async_tungstenite::tungstenite::Error::WriteBufferFull(buf) => { 95 | NetworkError::Error(format!("Write Buffer Full Error: {}", buf)) 96 | } 97 | async_tungstenite::tungstenite::Error::Utf8 => { 98 | NetworkError::Error("Utf8 Error".to_string()) 99 | } 100 | async_tungstenite::tungstenite::Error::AttackAttempt => { 101 | NetworkError::Error("Attack Attempt".to_string()) 102 | } 103 | async_tungstenite::tungstenite::Error::Url(url) => { 104 | NetworkError::Error(format!("Url Error: {}", url)) 105 | } 106 | async_tungstenite::tungstenite::Error::Http(http) => { 107 | NetworkError::Error(format!("HTTP Error: {:?}", http)) 108 | } 109 | async_tungstenite::tungstenite::Error::HttpFormat(http_format) => { 110 | NetworkError::Error(format!("HTTP Format Error: {}", http_format)) 111 | } 112 | })?; 113 | info!("Connected!"); 114 | return Ok(stream); 115 | } 116 | 117 | async fn recv_loop( 118 | mut read_half: Self::ReadHalf, 119 | messages: Sender, 120 | _settings: Self::NetworkSettings, 121 | ) { 122 | loop { 123 | let message = match read_half.next().await { 124 | Some(message) => match message { 125 | Ok(message) => message, 126 | Err(err) => match err { 127 | async_tungstenite::tungstenite::Error::ConnectionClosed 128 | | async_tungstenite::tungstenite::Error::AlreadyClosed => { 129 | error!("Connection Closed"); 130 | break; 131 | } 132 | _ => { 133 | error!("Nonfatal error detected: {}", err); 134 | continue; 135 | } 136 | }, 137 | }, 138 | None => { 139 | continue; 140 | } 141 | }; 142 | 143 | let packet = match message { 144 | Message::Text(_) => { 145 | error!("Text Message Received"); 146 | break; 147 | } 148 | Message::Binary(binary) => match bincode::deserialize(&binary) { 149 | Ok(packet) => packet, 150 | Err(err) => { 151 | error!("Failed to decode network packet from: {}", err); 152 | break; 153 | } 154 | }, 155 | Message::Ping(_) => { 156 | error!("Ping Message Received"); 157 | break; 158 | } 159 | Message::Pong(_) => { 160 | error!("Pong Message Received"); 161 | break; 162 | } 163 | Message::Close(_) => { 164 | error!("Connection Closed"); 165 | break; 166 | } 167 | Message::Frame(_) => todo!(), 168 | }; 169 | 170 | if messages.send(packet).await.is_err() { 171 | error!("Failed to send decoded message to eventwork"); 172 | break; 173 | } 174 | info!("Message deserialized and sent to eventwork"); 175 | } 176 | } 177 | 178 | async fn send_loop( 179 | mut write_half: Self::WriteHalf, 180 | messages: Receiver, 181 | _settings: Self::NetworkSettings, 182 | ) { 183 | while let Ok(message) = messages.recv().await { 184 | let encoded = match bincode::serialize(&message) { 185 | Ok(encoded) => encoded, 186 | Err(err) => { 187 | error!("Could not encode packet {:?}: {}", message, err); 188 | continue; 189 | } 190 | }; 191 | 192 | trace!("Sending the content of the message!"); 193 | 194 | match write_half 195 | .send(async_tungstenite::tungstenite::Message::Binary(encoded)) 196 | .await 197 | { 198 | Ok(_) => (), 199 | Err(err) => { 200 | error!("Could not send packet: {:?}: {}", message, err); 201 | break; 202 | } 203 | } 204 | 205 | trace!("Succesfully written all!"); 206 | } 207 | } 208 | 209 | fn split(combined: Self::Socket) -> (Self::ReadHalf, Self::WriteHalf) { 210 | let (write, read) = combined.split(); 211 | (read, write) 212 | } 213 | } 214 | 215 | #[derive(Clone, Debug, Resource, Default, Deref, DerefMut)] 216 | #[allow(missing_copy_implementations)] 217 | /// Settings to configure the network, both client and server 218 | pub struct NetworkSettings(WebSocketConfig); 219 | 220 | /// A special stream for recieving ws connections 221 | #[allow(clippy::type_complexity)] 222 | pub struct OwnedIncoming { 223 | inner: TcpListener, 224 | stream: Option>>>>>, 225 | } 226 | 227 | impl OwnedIncoming { 228 | fn new(listener: TcpListener) -> Self { 229 | Self { 230 | inner: listener, 231 | stream: None, 232 | } 233 | } 234 | } 235 | 236 | impl Stream for OwnedIncoming { 237 | type Item = WebSocketStream; 238 | 239 | fn poll_next( 240 | self: Pin<&mut Self>, 241 | cx: &mut std::task::Context<'_>, 242 | ) -> std::task::Poll> { 243 | let incoming = self.get_mut(); 244 | if incoming.stream.is_none() { 245 | let listener: *const TcpListener = &incoming.inner; 246 | incoming.stream = Some(Box::pin(async move { 247 | let stream = unsafe { 248 | listener 249 | .as_ref() 250 | .expect("Segfault when trying to read listener in OwnedStream") 251 | } 252 | .accept() 253 | .await 254 | .map(|(s, _)| s) 255 | .ok(); 256 | 257 | let stream: WebSocketStream = match stream { 258 | Some(stream) => async_tungstenite::accept_async(stream).await.ok()?, 259 | None => return None, 260 | }; 261 | Some(stream) 262 | })); 263 | } 264 | if let Some(stream) = &mut incoming.stream { 265 | if let std::task::Poll::Ready(res) = stream.poll(cx) { 266 | incoming.stream = None; 267 | return std::task::Poll::Ready(res); 268 | } 269 | } 270 | std::task::Poll::Pending 271 | } 272 | } 273 | 274 | unsafe impl Send for OwnedIncoming {} 275 | } 276 | 277 | #[cfg(target_arch = "wasm32")] 278 | mod wasm_websocket { 279 | use core::panic; 280 | use std::{net::SocketAddr, pin::Pin}; 281 | 282 | use async_channel::{Receiver, Sender}; 283 | use async_trait::async_trait; 284 | use bevy::prelude::{error, info, trace, Deref, DerefMut, Resource}; 285 | use bevy_eventwork::{error::NetworkError, managers::NetworkProvider, NetworkPacket}; 286 | use futures::{ 287 | stream::{SplitSink, SplitStream}, 288 | SinkExt, StreamExt, 289 | }; 290 | use futures_lite::Stream; 291 | use send_wrapper::SendWrapper; 292 | use tokio_tungstenite_wasm::{Message, WebSocketStream}; 293 | 294 | /// A provider for WebSockets 295 | #[derive(Default, Debug)] 296 | pub struct WasmWebSocketProvider; 297 | 298 | #[async_trait(?Send)] 299 | impl NetworkProvider for WasmWebSocketProvider { 300 | type NetworkSettings = NetworkSettings; 301 | 302 | type Socket = SendWrapper; 303 | 304 | type ReadHalf = SendWrapper>; 305 | 306 | type WriteHalf = SendWrapper>; 307 | 308 | type ConnectInfo = url::Url; 309 | 310 | type AcceptInfo = SocketAddr; 311 | 312 | type AcceptStream = OwnedIncoming; 313 | 314 | async fn accept_loop( 315 | _accept_info: Self::AcceptInfo, 316 | _: Self::NetworkSettings, 317 | ) -> Result { 318 | panic!("Can't create servers on WASM"); 319 | } 320 | 321 | async fn connect_task( 322 | connect_info: Self::ConnectInfo, 323 | _network_settings: Self::NetworkSettings, 324 | ) -> Result { 325 | info!("Beginning connection"); 326 | let stream = tokio_tungstenite_wasm::connect(connect_info) 327 | .await 328 | .map_err(|error| match error { 329 | tokio_tungstenite_wasm::Error::ConnectionClosed => { 330 | NetworkError::Error(format!("Connection Closed")) 331 | } 332 | tokio_tungstenite_wasm::Error::AlreadyClosed => { 333 | NetworkError::Error(format!("Connection Already Closed")) 334 | } 335 | tokio_tungstenite_wasm::Error::Io(err) => { 336 | NetworkError::Error(format!("IO Error: {}", err)) 337 | } 338 | tokio_tungstenite_wasm::Error::Tls(err) => { 339 | NetworkError::Error(format!("TLS Error: {}", err)) 340 | } 341 | tokio_tungstenite_wasm::Error::Capacity(err) => { 342 | NetworkError::Error(format!("Capacity Error: {}", err)) 343 | } 344 | tokio_tungstenite_wasm::Error::Protocol(err) => { 345 | NetworkError::Error(format!("Protocol Error: {}", err)) 346 | } 347 | tokio_tungstenite_wasm::Error::WriteBufferFull(err) => { 348 | NetworkError::Error(format!("Write Buffer Full: {}", err)) 349 | } 350 | tokio_tungstenite_wasm::Error::Utf8 => { 351 | NetworkError::Error(format!("UTF8 Encoding Error")) 352 | } 353 | tokio_tungstenite_wasm::Error::AttackAttempt => { 354 | NetworkError::Error(format!("Attack Attempt Detected")) 355 | } 356 | tokio_tungstenite_wasm::Error::Url(err) => { 357 | NetworkError::Error(format!("Url Error: {}", err)) 358 | } 359 | tokio_tungstenite_wasm::Error::Http(err) => { 360 | NetworkError::Error(format!("HTTP Error: {:?}", err)) 361 | } 362 | tokio_tungstenite_wasm::Error::HttpFormat(err) => { 363 | NetworkError::Error(format!("HTTP Format Error: {}", err)) 364 | } 365 | tokio_tungstenite_wasm::Error::BlobFormatUnsupported => { 366 | NetworkError::Error(format!("Blob Format Unsupported")) 367 | } 368 | tokio_tungstenite_wasm::Error::UnknownFormat => { 369 | NetworkError::Error(format!("Invalid Format")) 370 | } 371 | })?; 372 | info!("Connected!"); 373 | return Ok(SendWrapper::new(stream)); 374 | } 375 | 376 | async fn recv_loop( 377 | mut read_half: Self::ReadHalf, 378 | messages: Sender, 379 | _settings: Self::NetworkSettings, 380 | ) { 381 | loop { 382 | let message = match read_half.next().await { 383 | Some(message) => match message { 384 | Ok(message) => message, 385 | Err(err) => match err { 386 | tokio_tungstenite_wasm::Error::ConnectionClosed 387 | | tokio_tungstenite_wasm::Error::AlreadyClosed => { 388 | error!("Connection Closed"); 389 | break; 390 | } 391 | _ => { 392 | error!("Nonfatal error detected: {}", err); 393 | continue; 394 | } 395 | }, 396 | }, 397 | None => { 398 | continue; 399 | } 400 | }; 401 | 402 | let packet = match message { 403 | Message::Text(_) => { 404 | error!("Text Message Received"); 405 | break; 406 | } 407 | Message::Binary(binary) => match bincode::deserialize(&binary) { 408 | Ok(packet) => packet, 409 | Err(err) => { 410 | error!("Failed to decode network packet from: {}", err); 411 | break; 412 | } 413 | }, 414 | 415 | Message::Close(_) => { 416 | error!("Connection Closed"); 417 | break; 418 | } 419 | }; 420 | 421 | if messages.send(packet).await.is_err() { 422 | error!("Failed to send decoded message to eventwork"); 423 | break; 424 | } 425 | info!("Message deserialized and sent to eventwork"); 426 | } 427 | } 428 | 429 | async fn send_loop( 430 | mut write_half: Self::WriteHalf, 431 | messages: Receiver, 432 | _settings: Self::NetworkSettings, 433 | ) { 434 | while let Ok(message) = messages.recv().await { 435 | let encoded = match bincode::serialize(&message) { 436 | Ok(encoded) => encoded, 437 | Err(err) => { 438 | error!("Could not encode packet {:?}: {}", message, err); 439 | continue; 440 | } 441 | }; 442 | 443 | trace!("Sending the content of the message!"); 444 | 445 | match write_half 446 | .send(tokio_tungstenite_wasm::Message::Binary(encoded)) 447 | .await 448 | { 449 | Ok(_) => (), 450 | Err(err) => { 451 | error!("Could not send packet: {:?}: {}", message, err); 452 | break; 453 | } 454 | } 455 | 456 | trace!("Succesfully written all!"); 457 | } 458 | } 459 | 460 | fn split(combined: Self::Socket) -> (Self::ReadHalf, Self::WriteHalf) { 461 | let (write, read) = combined.take().split(); 462 | (SendWrapper::new(read), SendWrapper::new(write)) 463 | } 464 | } 465 | 466 | #[derive(Clone, Debug, Resource, Deref, DerefMut)] 467 | #[allow(missing_copy_implementations)] 468 | /// Settings to configure the network 469 | /// 470 | /// Note that on WASM this is currently ignored and defaults are used 471 | pub struct NetworkSettings { 472 | max_message_size: usize, 473 | } 474 | 475 | impl Default for NetworkSettings { 476 | fn default() -> Self { 477 | Self { 478 | max_message_size: 64 << 20, 479 | } 480 | } 481 | } 482 | 483 | /// A dummy struct as WASM is unable to accept connections and act as a server 484 | pub struct OwnedIncoming; 485 | 486 | impl Stream for OwnedIncoming { 487 | type Item = SendWrapper; 488 | 489 | fn poll_next( 490 | self: Pin<&mut Self>, 491 | _cx: &mut std::task::Context<'_>, 492 | ) -> std::task::Poll> { 493 | panic!("WASM does not support servers"); 494 | } 495 | } 496 | } 497 | --------------------------------------------------------------------------------