use std::collections::HashMap; use std::num::NonZeroU32; use std::sync::Arc; use bevy_ecs::entity::Entity; use bevy_ecs::event::Events; use bevy_ecs::query::With; use bevy_ecs::world::World; use egui_glow::EguiGlow; use glow::{HasContext, PixelUnpackData}; #[cfg(not(target_arch = "wasm32"))] use glutin::surface::{GlSurface, Surface, SwapInterval, WindowSurface}; #[cfg(not(target_arch = "wasm32"))] use glutin::{ config::{ConfigTemplateBuilder, GlConfig}, context::{ContextApi, ContextAttributesBuilder, PossiblyCurrentContext}, display::GetGlDisplay, prelude::{GlDisplay, NotCurrentGlContext}, }; #[cfg(not(target_arch = "wasm32"))] use glutin_winit::{DisplayBuilder, GlWindow}; use nalgebra::{Scale3, Translation3, Vector3}; use starkingdoms_common::packet::{ButtonType, Packet}; use starkingdoms_common::PlanetType; #[cfg(target_arch = "wasm32")] use wasm_bindgen::{prelude::Closure, JsCast}; #[cfg(target_arch = "wasm32")] use web_sys::{Event, HtmlCanvasElement}; use winit::dpi::PhysicalPosition; use winit::event::{ElementState, MouseButton, MouseScrollDelta}; use winit::event_loop::ControlFlow; use winit::keyboard::{KeyCode, PhysicalKey}; #[cfg(target_arch = "wasm32")] use winit::platform::web::{WindowAttributesExtWebSys, WindowExtWebSys}; use winit::{ application::ApplicationHandler, dpi::LogicalSize, event::WindowEvent, event_loop::ActiveEventLoop, raw_window_handle::HasWindowHandle, window::{Window}, }; use crate::components::{Camera, Menu, Player, RecvPacket, SendPacket, Texture, Transform}; use crate::networking::process_packets; use crate::networking::ws::Ws; use crate::ui::{draw_ui, init_ui}; use assets::AssetLoader; pub mod assets; #[derive(Default)] pub struct App { window: Option, world: World, program: Option, vertex_array: Option, vertex_buffer: Option, element_buffer: Option, #[cfg(not(target_arch = "wasm32"))] gl_surface: Option>, #[cfg(not(target_arch = "wasm32"))] gl_context: Option, egui_glow: Option, gl: Option>, textures: HashMap, send_packet_events: Events, recv_packet_events: Events, planet_types: HashMap, // (world entity, server id) up: bool, down: bool, left: bool, right: bool, mouse_pos: PhysicalPosition, } const VERTICES: [f32; 16] = [ -1.0, -1.0, 0.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, -1.0, 1.0, 0.0, 0.0, ]; const INDICES: [u32; 6] = [0, 1, 2, 2, 3, 0]; impl App { pub fn new( world: World, send_packet_events: Events, recv_packet_events: Events, ) -> Self { Self { world, send_packet_events, recv_packet_events, ..Default::default() } } } impl ApplicationHandler for App { fn resumed(&mut self, event_loop: &ActiveEventLoop) { #[cfg(target_arch = "wasm32")] let attributes = { let document = web_sys::window().unwrap().document().unwrap(); let canvas = document.get_element_by_id("canvas").unwrap(); let canvas = canvas .dyn_into::() .map_err(|_| ()) .unwrap(); canvas.set_width( web_sys::window() .unwrap() .inner_width() .unwrap() .as_f64() .unwrap() as u32, ); canvas.set_height( web_sys::window() .unwrap() .inner_height() .unwrap() .as_f64() .unwrap() as u32, ); Window::default_attributes() .with_title("StarKingdoms.TK") .with_canvas(Some(canvas)) }; #[cfg(not(target_arch = "wasm32"))] let attributes = { Window::default_attributes() .with_transparent(true) .with_title("StarKingdoms.TK") .with_inner_size(LogicalSize::new(400, 300)) }; self.window = Some(event_loop.create_window(attributes.clone()).unwrap()); let window = self.window.as_ref().unwrap(); #[cfg(target_arch = "wasm32")] let context = window .canvas() .unwrap() .get_context("webgl2") .unwrap() .unwrap() .dyn_into::() .unwrap(); #[cfg(target_arch = "wasm32")] let (gl, shader_version) = ( Arc::new(glow::Context::from_webgl2_context(context)), "#version 300 es", ); #[cfg(not(target_arch = "wasm32"))] let (gl, shader_version) = unsafe { let template = ConfigTemplateBuilder::new().with_transparency(true); let display_builder = DisplayBuilder::new().with_window_attributes(Some(attributes)); let (window, gl_config) = display_builder .build(event_loop, template, |configs| { configs .reduce(|accum, config| { let supports_transparency = config.supports_transparency().unwrap_or(false) && !accum.supports_transparency().unwrap_or(false); if supports_transparency || config.num_samples() > accum.num_samples() { config } else { accum } }) .unwrap() }) .unwrap(); let raw_handle = window.as_ref().map(|window| { window .window_handle() .unwrap() .window_handle() .unwrap() .as_raw() }); let gl_display = gl_config.display(); let context_attributes = ContextAttributesBuilder::new() .with_context_api(ContextApi::OpenGl(Some(glutin::context::Version { major: 3, minor: 0, }))) .build(raw_handle); let not_current_gl_context = gl_display .create_context(&gl_config, &context_attributes) .unwrap(); let window = window.unwrap(); let surface_attributes = window.build_surface_attributes(Default::default()).unwrap(); let gl_surface = gl_display .create_window_surface(&gl_config, &surface_attributes) .unwrap(); let gl_context = not_current_gl_context.make_current(&gl_surface).unwrap(); let gl = glow::Context::from_loader_function_cstr(|s| gl_display.get_proc_address(s)); gl_surface .set_swap_interval(&gl_context, SwapInterval::Wait(NonZeroU32::new(1).unwrap())) .unwrap(); self.gl_surface = Some(gl_surface); self.gl_context = Some(gl_context); (Arc::new(gl), "#version 300 es") }; unsafe { let shaders = [ ( "vertex", include_str!("../shaders/vertex.glsl"), glow::VERTEX_SHADER, ), ( "fragment", include_str!("../shaders/fragment.glsl"), glow::FRAGMENT_SHADER, ), ]; let program = gl.create_program().expect("Failed to create program"); for (name, source, shader_type) in shaders { let shader = gl .create_shader(shader_type) .expect("Failed to create vertex shader"); gl.shader_source(shader, &format!("{}\n{}", shader_version, source)); gl.compile_shader(shader); if !gl.get_shader_compile_status(shader) { tracing::error!( "error in {} shader: {}", name, gl.get_shader_info_log(shader) ); } gl.attach_shader(program, shader); gl.delete_shader(shader); } gl.link_program(program); gl.use_program(Some(program)); let vertex_array = gl .create_vertex_array() .expect("Failed to create vertex array"); gl.bind_vertex_array(Some(vertex_array)); let vertex_buffer = gl.create_buffer().expect("Failed to create vertex buffer"); gl.bind_buffer(glow::ARRAY_BUFFER, Some(vertex_buffer)); let element_buffer = gl.create_buffer().expect("Failed to create element buffer"); gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(element_buffer)); gl.buffer_data_u8_slice( glow::ARRAY_BUFFER, std::slice::from_raw_parts(VERTICES.as_ptr() as *const u8, size_of_val(&VERTICES)), glow::STATIC_DRAW, ); gl.buffer_data_u8_slice( glow::ELEMENT_ARRAY_BUFFER, std::slice::from_raw_parts(INDICES.as_ptr() as *const u8, size_of_val(&INDICES)), glow::STATIC_DRAW, ); gl.vertex_attrib_pointer_f32(0, 2, glow::FLOAT, false, 4 * size_of::() as i32, 0); gl.enable_vertex_attrib_array(0); gl.vertex_attrib_pointer_f32( 1, 2, glow::FLOAT, false, 4 * size_of::() as i32, 2 * size_of::() as i32, ); gl.enable_vertex_attrib_array(1); gl.clear_color(0.1, 0.1, 0.1, 1.0); gl.viewport( 0, 0, window.inner_size().width as i32, window.inner_size().height as i32, ); gl.enable(glow::BLEND); gl.blend_func(glow::SRC_ALPHA, glow::ONE_MINUS_SRC_ALPHA); self.program = Some(program); self.vertex_array = Some(vertex_array); self.vertex_buffer = Some(vertex_buffer); self.element_buffer = Some(element_buffer); } #[cfg(target_arch = "wasm32")] web_sys::window().unwrap().set_onresize(Some( Closure::::new(move |_| { let document = web_sys::window().unwrap().document().unwrap(); let canvas = document.get_element_by_id("canvas").unwrap(); let canvas = canvas .dyn_into::() .map_err(|_| ()) .unwrap(); canvas.set_width( web_sys::window() .unwrap() .inner_width() .unwrap() .as_f64() .unwrap() as u32, ); canvas.set_height( web_sys::window() .unwrap() .inner_height() .unwrap() .as_f64() .unwrap() as u32, ); }) .into_js_value() .as_ref() .unchecked_ref(), )); let egui_glow = egui_glow::EguiGlow::new(event_loop, gl.clone(), None, None, true); init_ui(egui_glow.egui_ctx.clone()); self.egui_glow = Some(egui_glow); self.gl = Some(gl); } fn window_event( &mut self, event_loop: &ActiveEventLoop, _window_id: winit::window::WindowId, event: winit::event::WindowEvent, ) { let event_response = self .egui_glow .as_mut() .unwrap() .on_window_event(self.window.as_ref().unwrap(), &event); if event_response.consumed { return; } match event { WindowEvent::CloseRequested => { event_loop.exit(); } WindowEvent::Resized(size) => { #[cfg(not(target_arch = "wasm32"))] self.gl_surface.as_ref().unwrap().resize( self.gl_context.as_ref().unwrap(), NonZeroU32::new(size.width).unwrap(), NonZeroU32::new(size.height).unwrap(), ); let mut camera = self.world.get_resource_mut::().unwrap(); camera.width = size.width; camera.height = size.height; unsafe { self.gl .as_ref() .unwrap() .viewport(0, 0, size.width as i32, size.height as i32); } } WindowEvent::MouseWheel { delta, .. } => { let mut camera = self.world.get_resource_mut::().unwrap(); let raw_delta = match delta { MouseScrollDelta::PixelDelta(pos) => pos.y as f32, MouseScrollDelta::LineDelta(y, ..) => y, }; let delta = 1.1; if raw_delta < 0.0 { camera.zoom = (camera.zoom * 1.0 / delta).max(0.13); } else { camera.zoom = (camera.zoom * delta).min(5.0); } } WindowEvent::KeyboardInput { ref event, .. } => { match event.physical_key { PhysicalKey::Code(key) => { let matched: bool; match event.state { ElementState::Pressed => match key { KeyCode::KeyW => { self.up = true; matched = true; } KeyCode::KeyS => { self.down = true; matched = true; } KeyCode::KeyA => { self.left = true; matched = true; } KeyCode::KeyD => { self.right = true; matched = true; } _ => matched = false, }, ElementState::Released => match key { KeyCode::KeyW => { self.up = false; matched = true; } KeyCode::KeyS => { self.down = false; matched = true; } KeyCode::KeyA => { self.left = false; matched = true; } KeyCode::KeyD => { self.right = false; matched = true; } _ => matched = false, }, } if matched { self.send_packet_events .send(SendPacket(Packet::PlayerInput { up: self.up, down: self.down, left: self.left, right: self.right, })); } } PhysicalKey::Unidentified(_) => {} // unsupported } } WindowEvent::CursorMoved { position, .. } => self.mouse_pos = position, WindowEvent::MouseInput { state, button, .. } => { let button = match button { MouseButton::Left => ButtonType::Left, MouseButton::Middle => ButtonType::Middle, MouseButton::Right => ButtonType::Right, _ => return, }; if state.is_pressed() { let mut menu_query = self.world.query_filtered::>(); let mut menus = Vec::new(); for menu in menu_query.iter(&self.world) { menus.push(menu); } for menu in menus { self.world.entity_mut(menu).remove::(); } } let camera = self.world.get_resource::().unwrap(); let view = camera.to_cursor_matrix(); let pos = view * Vector3::new(self.mouse_pos.x as f32, self.mouse_pos.y as f32, 1.0); let pos = pos / pos.z; self.send_packet_events .send(SendPacket(Packet::PlayerMouseInput { x: pos.x, y: pos.y, released: !state.is_pressed(), button, })); } _ => {} } } fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { let ws = self .world .get_resource_mut::() .expect("Failed to get Ws resource"); #[cfg(target_arch = "wasm32")] while let Ok(Some(packet)) = ws.receiver.try_next() { self.recv_packet_events.send(RecvPacket(packet)); } #[cfg(not(target_arch = "wasm32"))] for packet in ws.receiver.iter() { self.recv_packet_events.send(RecvPacket(packet)); } self.send_packet_events.update(); self.recv_packet_events.update(); process_packets( &mut self.world, &mut self.send_packet_events, &mut self.recv_packet_events, &mut self.planet_types, ); let gl = self.gl.as_ref().unwrap(); let player = self.world.query_filtered::<&Transform, With>(); self.egui_glow .as_mut() .unwrap() .run(self.window.as_ref().unwrap(), |ctx| { draw_ui(ctx, &mut self.world, &mut self.send_packet_events); }); let camera = self.world.get_resource::().unwrap(); let x_scale = camera.zoom / camera.width as f32 * 2.0; let y_scale = camera.zoom / camera.height as f32 * 2.0; let view = &[ x_scale, 0.0, 0.0, camera.x * x_scale, 0.0, y_scale, 0.0, camera.y * y_scale, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, ]; let mut sprite_query = self.world.query::<(&Transform, &mut Texture)>(); let mut sprites = Vec::new(); for (transform, texture) in sprite_query.iter(&self.world) { sprites.push((transform, texture)); } unsafe { gl.clear(glow::COLOR_BUFFER_BIT); gl.use_program(self.program); gl.bind_vertex_array(self.vertex_array); gl.bind_buffer(glow::ARRAY_BUFFER, self.vertex_buffer); gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, self.element_buffer); gl.active_texture(glow::TEXTURE0); let view_loc = gl.get_uniform_location(self.program.unwrap(), "view"); let model_loc = gl.get_uniform_location(self.program.unwrap(), "model"); gl.uniform_matrix_4_f32_slice(view_loc.as_ref(), true, view); if !self.textures.contains_key("starfield.svg") { let assets = self.world.resource::(); match assets.get("starfield.svg") { Some(image) => { let texture_object = gl.create_texture().expect("Failed to create texture object"); gl.bind_texture(glow::TEXTURE_2D, Some(texture_object)); gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MIN_FILTER, glow::LINEAR_MIPMAP_LINEAR as i32); gl.tex_image_2d(glow::TEXTURE_2D, 0, glow::RGBA as i32, image.width as i32, image.height as i32, 0, glow::RGBA, glow::UNSIGNED_BYTE, PixelUnpackData::Slice(Some(&image.bytes))); gl.generate_mipmap(glow::TEXTURE_2D); self.textures.insert("starfield.svg".to_string(), texture_object); } None => {} } } if self.textures.contains_key("starfield.svg") { gl.bind_texture(glow::TEXTURE_2D, self.textures.get("starfield.svg").copied()); let camera = self.world.get_resource::().unwrap(); let x = -(camera.x + camera.x.signum() * 200.0) + camera.x % 400.0; let y = -(camera.y + camera.y.signum() * 200.0) + camera.y % 400.0; let x_range = camera.width as f32 / camera.zoom / 400.0; let y_range = camera.height as f32 / camera.zoom / 400.0; for i in ((-x_range/2.0) as i32 - 1)..=((x_range/2.0) as i32 + 1) { for j in ((-y_range/2.0) as i32 - 1)..=((y_range/2.0) as i32 + 1) { let model = Translation3::new(x + (i*400) as f32, y + (j*400) as f32, 0.0).to_homogeneous() * Scale3::new(200.0, 200.0, 1.0).to_homogeneous(); gl.uniform_matrix_4_f32_slice(model_loc.as_ref(), false, model.as_slice()); gl.draw_elements(glow::TRIANGLES, 6, glow::UNSIGNED_INT, 0); } } } for (transform, texture) in sprites { if !self.textures.contains_key(&texture.name) { let assets = self.world.resource::(); let image = match assets.get(texture.name.clone()) { Some(t) => t, None => continue, }; let texture_object = gl .create_texture() .expect("Failed to create texture object"); gl.bind_texture(glow::TEXTURE_2D, Some(texture_object)); gl.tex_parameter_i32( glow::TEXTURE_2D, glow::TEXTURE_MIN_FILTER, glow::LINEAR_MIPMAP_LINEAR as i32, ); gl.tex_image_2d( glow::TEXTURE_2D, 0, glow::RGBA as i32, image.width as i32, image.height as i32, 0, glow::RGBA, glow::UNSIGNED_BYTE, PixelUnpackData::Slice(Some(&image.bytes)), ); gl.generate_mipmap(glow::TEXTURE_2D); self.textures.insert(texture.name.clone(), texture_object); } // now the texture must exist let model = transform.to_matrix(); let model = model.as_slice(); gl.uniform_matrix_4_f32_slice(model_loc.as_ref(), false, model); gl.bind_texture(glow::TEXTURE_2D, self.textures.get(&texture.name).copied()); gl.draw_elements(glow::TRIANGLES, 6, glow::UNSIGNED_INT, 0); } } self.egui_glow .as_mut() .unwrap() .paint(self.window.as_ref().unwrap()); #[cfg(not(target_arch = "wasm32"))] self.gl_surface .as_ref() .unwrap() .swap_buffers(self.gl_context.as_ref().unwrap()) .unwrap(); let mut ws = self .world .get_resource_mut::() .expect("Failed to get Ws resource"); let mut send_event_cursor = self.send_packet_events.get_cursor(); for event in send_event_cursor.read(&self.send_packet_events) { ws.send_packet(event.0.clone()); } event_loop.set_control_flow(ControlFlow::WaitUntil( web_time::Instant::now() .checked_add(web_time::Duration::from_millis(16)) .unwrap(), )); } }