use std::{io::ErrorKind, net::TcpStream, time::Duration}; use bevy::{ ecs::world::CommandQueue, prelude::*, tasks::{AsyncComputeTaskPool, Task, block_on, futures_lite::future}, }; use thiserror::Error; use tungstenite::{WebSocket, http::Response, stream::MaybeTlsStream}; use crate::ui::{despawn_main_menu, spawn_main_menu}; mod ui; const PADDLE_GAP: f32 = 20.0; // gap between the paddles and the wall (window border) fn main() { App::new() .add_plugins(DefaultPlugins) .insert_state(GameState::MainMenu) .add_systems(Startup, spawn_camera) .add_systems(OnEnter(GameState::MainMenu), spawn_main_menu) .add_systems(OnExit(GameState::MainMenu), despawn_main_menu) .add_observer(ui::button_hover_start) .add_observer(ui::button_hover_stop) // TODO: System to operate buttons & other UI widgets // .add_systems(Update, ) .add_systems(OnEnter(GameState::Playing), setup_game) .add_systems( Update, ( input_handler, setup_connection, handle_tasks, send_info, recv_info, ) .run_if(in_state(GameState::ConnectionDemo)), ) .add_message::() .insert_resource(ChatTimer { timer: Timer::new(Duration::from_secs(1), TimerMode::Repeating), }) .run(); } /// Main game state indicator #[derive(Clone, Debug, Eq, Hash, PartialEq, States)] enum GameState { MainMenu, Playing, ConnectionDemo, // TODO: Remove this state. } /// Utility to despawn entities with a given component. Useful for scene /// changes. /// /// I'm using this primarily with marker components to delete UI elements when /// they are no longer needed. E.g.: Removing the main menu after starting. pub fn despawn(mut commands: Commands, targets: Query>) { targets .iter() .for_each(|entt| commands.entity(entt).despawn()); } fn spawn_camera(mut commands: Commands) { commands.spawn(Camera2d); } /// Initialize the scene fn setup_game( mut commands: Commands, mut meshes: ResMut>, mut materials: ResMut>, window: Single<&Window>, ) { // ball commands.spawn(( Ball, Mesh2d(meshes.add(Circle::new(10.0))), MeshMaterial2d(materials.add(Color::srgb(1.0, 0.0, 0.0))), Transform::default(), )); let paddle_mesh = meshes.add(Rectangle::new(10.0, 100.0)); let paddle_material = materials.add(Color::WHITE); // Player 1 commands.spawn(( Paddle, Mesh2d(paddle_mesh.clone()), MeshMaterial2d(paddle_material.clone()), Transform::from_xyz(-window.width() / 2.0 + PADDLE_GAP, 0.0, 1.0), )); // Player 2 commands.spawn(( Paddle, Mesh2d(paddle_mesh), MeshMaterial2d(paddle_material), Transform::from_xyz(window.width() / 2.0 - PADDLE_GAP, 0.0, 1.0), )); } #[derive(Component)] struct Ball; /// Marker component for player paddles /// /// Maybe one for each player? /// Maybe it can hold the WebSocket, too. /// *Maybe* I can have one struct with an Option to know which /// player is the local one (the one with a socket). #[derive(Component)] struct Paddle; /// ECS Component to hold the WebSocket. I guess there's going to be a magic /// entity that controls the networking. #[derive(Component)] struct WsClient( ( WebSocket>, Response>>, ), ); /// Container component for a bevy task. Specifically, the async Websocket /// setup. #[derive(Component)] struct WsSetupTask(Task>); /// Used to signal user input to other systems -- in particular, the notice to /// open a connection to the server. #[derive(Message)] enum WebSocketConnectionMessage { SetupConnection, // TODO: Presumably a TeardownConnection, right? } #[derive(Debug, Error)] enum ConnSetupError { #[error("IO")] Io(#[from] std::io::Error), #[error("WebSocket")] WebSocket(#[from] tungstenite::Error), } /// The keyboard input handler (mainly to trigger WS connection) fn input_handler( input: Res>, mut messages: MessageWriter, ) { if input.just_pressed(KeyCode::Space) { messages.write(WebSocketConnectionMessage::SetupConnection); } } /// The system which establishes the WS connection. It responds to [`WebSocketConnectionMessage`]s. /// /// When a [`WebSocketConnectionMessage::SetupConnection`] message is emitted, /// this system responds by creating a new (bevy) [`Task`]. This task is stored /// in the component [`WsSetupTask`] which is attached to a new entity. /// /// The closure executed by this task attempts to reach out to a WebSocket URL. /// Upon successful connection, the socket is placed in a [`WsClient`] /// component and added to the same entity. At the same time, the [`WsSetupTask`] /// is removed since the task has now finished. /// /// Monitoring for completion of the task is done by the system [`handle_tasks()`]. fn setup_connection( mut commands: Commands, mut messages: MessageReader, ) { for msg in messages.read() { match msg { WebSocketConnectionMessage::SetupConnection => { info!("Connecting to server..."); let url = "ws://localhost:4000/websocket"; let entity = commands.spawn_empty().id(); { let pool = AsyncComputeTaskPool::get(); let task = pool.spawn(async move { let mut client = tungstenite::connect(url)?; match client.0.get_mut() { MaybeTlsStream::Plain(p) => p.set_nonblocking(true)?, MaybeTlsStream::Rustls(stream_owned) => { stream_owned.get_mut().set_nonblocking(true)? } _ => todo!(), }; info!("Connected!"); // First message is this client's username. I'll hard-code // a value for this PoC. client.0.send(tungstenite::Message::Text("Dingus".into()))?; let mut command_queue = CommandQueue::default(); command_queue.push(move |world: &mut World| { world .entity_mut(entity) .insert(WsClient(client)) .remove::(); }); Ok(command_queue) }); commands.entity(entity).insert(WsSetupTask(task)); } } // _ => { there are no other variants right now, so this is pretty silly } } } } /// Polls the websocket setup task(s) to check for progress. /// /// If the task has succeeded, it will give back a [`CommandQueue`]. This is /// appended to the current [`Commands`] so they are acted upon this tick. /// /// If it has failed, an error is printed to the console. /// /// The task is self-removing, so we don't need to delete the [`WsSetupTask`] /// component here. fn handle_tasks(mut commands: Commands, mut transform_tasks: Query<&mut WsSetupTask>) { for mut task in &mut transform_tasks { if let Some(result) = block_on(future::poll_once(&mut task.0)) { match result { Ok(mut commands_queue) => { commands.append(&mut commands_queue); } Err(e) => info!("Connection failed. Err: {e:?}"), } } } } #[derive(Resource)] struct ChatTimer { timer: Timer, } /// Unused, left over from example code. /// /// It *would* tick a timer, sending a message to the echo server each time the /// timer expires. But I have a chatroom not an echo server, so this bit /// doesn't make sense. /// /// If I were to make a full chat client, I would replace this with some kind /// of event handler. fn send_info( some_data: Query<(&Transform,)>, time: Res