M crates/client/src/components.rs => crates/client/src/components.rs +26 -5
@@ 1,7 1,8 @@
-use bevy_ecs::{component::Component, system::Resource};
-use nalgebra::{Matrix4, Rotation3, Scale3, Translation3};
+use bevy_ecs::{bundle::Bundle, component::Component, event::Event, system::Resource};
+use nalgebra::{Matrix4, Rotation2, Scale3, Translation3};
+use starkingdoms_common::packet::Packet;
-#[derive(Component)]
+#[derive(Component, Debug)]
pub struct Texture {
pub name: String,
}
@@ 9,15 10,21 @@ pub struct Texture {
#[derive(Component, Debug)]
pub struct Transform {
pub translation: Translation3<f32>,
- pub rotation: Rotation3<f32>,
+ pub rotation: Rotation2<f32>,
pub scale: Scale3<f32>,
}
impl Transform {
pub fn to_matrix(&self) -> Matrix4<f32> {
- self.translation.to_homogeneous() * self.rotation.to_homogeneous() * self.scale.to_homogeneous()
+ self.translation.to_homogeneous() * self.rotation.to_homogeneous().to_homogeneous() * self.scale.to_homogeneous()
}
}
+#[derive(Bundle, Debug)]
+pub struct SpriteBundle {
+ pub transform: Transform,
+ pub texture: Texture,
+}
+
#[derive(Resource, Debug)]
pub struct Camera {
pub x: f32,
@@ 26,3 33,17 @@ pub struct Camera {
pub width: u32, // screen width (these are for aspect ratio)
pub height: u32, // screen height
}
+
+#[derive(Component, Debug, Clone, Copy)]
+pub struct Player;
+#[derive(Component, Debug, Clone, Copy)]
+pub struct Planet;
+#[derive(Component, Debug, Clone, Copy)]
+pub struct Part;
+#[derive(Component, Debug, Clone, Copy)]
+pub struct ServerId(pub u32);
+
+#[derive(Event, Clone, PartialEq)]
+pub struct SendPacket(pub Packet);
+#[derive(Event, Clone, PartialEq)]
+pub struct RecvPacket(pub Packet);
M crates/client/src/lib.rs => crates/client/src/lib.rs +13 -6
@@ 1,7 1,9 @@
-use bevy_ecs::world::World;
-use components::{Camera, Texture, Transform};
+use bevy_ecs::{event::Events, world::World};
+use components::{Camera, Part, Player, RecvPacket, SendPacket, Texture, Transform};
use nalgebra::{Rotation2, Rotation3, Scale2, Scale3, Translation2, Translation3, Vector3};
+use networking::ws::Ws;
use rendering::{assets::Assets, App};
+use starkingdoms_common::packet::Packet;
use tracing::info;
use winit::event_loop::{ControlFlow, EventLoop};
@@ 14,6 16,7 @@ pub mod platform;
pub mod rendering;
pub mod components;
+pub mod networking;
// Hi, you've found the real main function! This is called AFTER platform-specific initialization code.
pub fn start() {
@@ 39,15 42,19 @@ pub fn start() {
});
world.insert_resource(Assets::new());
+ world.insert_resource(Ws::new());
+
+ let mut send_packet_events = Events::<SendPacket>::default();
+ let recv_packet_events = Events::<RecvPacket>::default();
world.spawn((Transform {
translation: Translation3::new(0.0, 0.0, 0.0),
- rotation: Rotation3::from_axis_angle(&Vector3::z_axis(), 0.0),
- scale: Scale3::new(20.0, 20.0, 1.0),
- }, Texture { name: "hearty.svg".to_string() }));
+ rotation: Rotation2::new(0.0),
+ scale: Scale3::new(25.0, 25.0, 1.0),
+ }, Texture { name: "hearty.svg".to_string() }, Player, Part));
let event_loop = EventLoop::new().unwrap();
event_loop.set_control_flow(ControlFlow::Wait);
- event_loop.run_app(&mut App::new(world)).unwrap();
+ event_loop.run_app(&mut App::new(world, send_packet_events, recv_packet_events)).unwrap();
}
A crates/client/src/networking/mod.rs => crates/client/src/networking/mod.rs +114 -0
@@ 0,0 1,114 @@
+use std::collections::HashMap;
+
+use bevy_ecs::{entity::Entity, event::Events, query::With, world::World};
+use nalgebra::{Rotation2, Scale2, Scale3, Translation3};
+use starkingdoms_common::{packet::Packet, PartType, PlanetType};
+
+use crate::components::{Camera, Part, Planet, Player, RecvPacket, SendPacket, ServerId, SpriteBundle, Texture, Transform};
+
+#[cfg(target_arch = "wasm32")]
+#[path = "ws_wasm.rs"]
+pub mod ws;
+#[cfg(not(target_arch = "wasm32"))]
+#[path = "ws_native.rs"]
+pub mod ws;
+
+pub fn process_packets(
+ world: &mut World,
+ send_packet_events: &mut Events<SendPacket>,
+ recv_packet_events: &mut Events<RecvPacket>,
+ planet_types: &mut HashMap<PlanetType, (Entity, u32)>,
+) {
+ use Packet::*;
+ let mut recv_cursor = recv_packet_events.get_cursor();
+ for recv in recv_cursor.read(&recv_packet_events) {
+ match &recv.0 {
+ SpawnPlayer { id, username } => {
+ let mut player_query = world.query_filtered::<Entity, With<Player>>();
+ let entity = player_query.single(world);
+ world.entity_mut(entity).insert(ServerId(*id));
+ }
+ SpawnPart { id, part } => {
+ use PartType::*;
+ world.spawn((Transform {
+ translation: Translation3::new(part.transform.x, part.transform.y, 0.0),
+ rotation: Rotation2::new(part.transform.rot),
+ scale: Scale3::new(25.0, 25.0, 1.0),
+ }, Texture {
+ name: match part.part_type {
+ Placeholder => panic!("AHHHH PLACEHOLDER PANIC"),
+ Hearty => "hearty.svg",
+ Cargo => "cargo_off.svg",
+ Hub => "hub_off.svg",
+ LandingThruster => "landingthruster_off.svg",
+ LandingThrusterSuspension => "landingleg.svg",
+ }.to_string()
+ }, ServerId(*id), Part));
+ tracing::info!("here");
+ }
+ PartPositions { parts } => {
+ for (id, part) in parts {
+ if part.part_type == PartType::Hearty {
+ let mut player_query = world.query_filtered::<&ServerId, With<Player>>();
+ let server_id = player_query.single(world);
+ if server_id.0 == *id {
+ let mut camera = world.resource_mut::<Camera>();
+ camera.x = -part.transform.x;
+ camera.y = -part.transform.y;
+ }
+ }
+ let mut part_query = world.query_filtered::<(&ServerId, &mut Transform), With<Part>>();
+ for (server_id, mut transform) in part_query.iter_mut(world) {
+ if server_id.0 == *id {
+ transform.translation.x = part.transform.x;
+ transform.translation.y = part.transform.y;
+ transform.rotation = Rotation2::new(part.transform.rot);
+ }
+ }
+ }
+ }
+ PlanetPositions { planets } => {
+ for (server_id, planet) in planets {
+ let mut planet_query = world.query_filtered::<&mut Transform, With<Planet>>();
+ if !planet_types.contains_key(&planet.planet_type) {
+ let entity = world.spawn(SpriteBundle {
+ transform: Transform {
+ translation: Translation3::new(planet.transform.x, planet.transform.y, 0.0),
+ rotation: Rotation2::new(planet.transform.rot),
+ scale: Scale3::new(planet.radius, planet.radius, 1.0),
+ },
+ texture: Texture {
+ name: match planet.planet_type {
+ /*PlanetType::Sun => "sun.svg",
+ PlanetType::Mercury => "mercury.svg",
+ PlanetType::Venus => "venus.svg",
+ PlanetType::Earth => "earth.svg",
+ PlanetType::Moon => "moon.svg",
+ PlanetType::Mars => "mars.svg",
+ PlanetType::Jupiter => "jupiter.svg",
+ PlanetType::Saturn => "saturn.svg",
+ PlanetType::Uranus => "uranus.svg",
+ PlanetType::Neptune => "neptune.svg",
+ PlanetType::Pluto => "pluto.svg",*/
+ PlanetType::Sun => "sun.svg",
+ PlanetType::Mercury => "moon.svg",
+ PlanetType::Venus => "mars.svg",
+ PlanetType::Earth => "earth.svg",
+ PlanetType::Moon => "moon.svg",
+ PlanetType::Mars => "mars.svg",
+ PlanetType::Jupiter => "sun.svg",
+ PlanetType::Saturn => "moon.svg",
+ PlanetType::Uranus => "sun.svg",
+ PlanetType::Neptune => "mars.svg",
+ PlanetType::Pluto => "earth.svg",
+ }.to_string()
+ },
+ });
+ planet_types.insert(planet.planet_type, (entity.id(), *server_id));
+ }
+ }
+ }
+ _ => {}
+ }
+ }
+}
A crates/client/src/networking/ws_native.rs => crates/client/src/networking/ws_native.rs +61 -0
@@ 0,0 1,61 @@
+use std::{net::TcpStream, sync::{Arc, Mutex}};
+
+use bevy_ecs::system::Resource;
+use crossbeam::channel::{unbounded, Receiver, Sender};
+use starkingdoms_common::packet::{MsgFromError, Packet};
+use tungstenite::{connect, stream::MaybeTlsStream, Message, WebSocket};
+
+pub trait PacketMessageConvert {
+ fn from_message(value: &Message) -> Result<Packet, MsgFromError>;
+ fn into_message(&self) -> Message;
+}
+
+impl PacketMessageConvert for Packet {
+ fn from_message(value: &Message) -> Result<Packet, MsgFromError> {
+ match value {
+ Message::Text(s) => serde_json::from_str(s).map_err(MsgFromError::JSONError),
+ Message::Binary(b) => serde_json::from_slice(b).map_err(MsgFromError::JSONError),
+ Message::Close(_) => Ok(Packet::_SpecialDisconnect {}),
+ Message::Frame(_) | Message::Pong(_) | Message::Ping(_) => {
+ Err(MsgFromError::InvalidMessageType)
+ }
+ }
+ }
+ fn into_message(&self) -> Message {
+ Message::Text(serde_json::to_string(self).expect("failed to serialize packet to json").into())
+ }
+}
+
+#[derive(Resource, Debug)]
+pub struct Ws {
+ socket: Arc<Mutex<WebSocket<MaybeTlsStream<TcpStream>>>>,
+ pub sender: Sender<Packet>,
+ pub receiver: Receiver<Packet>,
+ packet_receiver: Receiver<Packet>,
+}
+impl Ws {
+ pub fn new() -> Self {
+ let (socket, _) = connect("ws://localhost:3000").expect("Failed to connect to server");
+ let socket = Arc::new(Mutex::new(socket));
+ let (packet_sender, receiver) = unbounded();
+ let (sender, packet_receiver) = unbounded();
+ let socket_clone = socket.clone();
+ std::thread::spawn(move || {
+ let socket = socket_clone;
+ loop {
+ let message = socket.lock().unwrap().read().expect("Failed to reading message");
+ let packet = Packet::from_message(&message).expect("Server sent invalid packet");
+ packet_sender.send(packet).expect("Couldn't send packet to server");
+ }
+ });
+ Ws {
+ socket,
+ sender,
+ receiver,
+ packet_receiver,
+ }
+ }
+ pub fn send_packet(&mut self, packet: &Packet) {
+ self.socket.lock().unwrap().send(packet.into_message()).expect("Couldn't send packet to server");
+ }
+}
A crates/client/src/networking/ws_wasm.rs => crates/client/src/networking/ws_wasm.rs +78 -0
@@ 0,0 1,78 @@
+use std::thread::yield_now;
+
+use bevy_ecs::system::Resource;
+//use crossbeam::channel::{unbounded, Receiver, Sender};
+use futures::{channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender}, SinkExt};
+use wasm_bindgen::{prelude::Closure, JsCast, JsValue};
+use wasm_bindgen_futures::spawn_local;
+use web_sys::{MessageEvent, WebSocket};
+use starkingdoms_common::packet::Packet;
+
+const PORT: u16 = 3000;
+
+#[derive(Debug)]
+pub struct Socket(WebSocket);
+unsafe impl Send for Socket {}
+unsafe impl Sync for Socket {}
+
+#[derive(Resource, Debug)]
+pub struct Ws {
+ socket: Socket,
+ pub sender: UnboundedSender<Packet>,
+ pub receiver: UnboundedReceiver<Packet>,
+ packet_receiver: UnboundedReceiver<Packet>,
+}
+
+impl Ws {
+ pub fn new() -> Self {
+ let window = web_sys::window().unwrap();
+ let ws = WebSocket::new(&format!("ws://{}:{}",
+ window.location().hostname().unwrap(), PORT)).expect("Couldn't connect to server");
+ let (packet_sender, receiver) = unbounded();
+ //let packet_sender = Rc::new(RwLock::new(packet_sender));
+ let (sender, packet_receiver) = unbounded();
+
+ let ws_clone = ws.clone();
+ let onopen_callback = Closure::<dyn FnMut()>::new(move || {
+ let packet = Packet::ClientLogin {
+ username: String::new(),
+ save: None,
+ jwt: None,
+ };
+
+ ws_clone.send_with_str(&serde_json::to_string(&packet).expect("Couldn't convert packet to string")).expect("Failed to send packet");
+ });
+ ws.set_onopen(Some(onopen_callback.as_ref().unchecked_ref()));
+ onopen_callback.forget();
+ let onmessage_callback = Closure::<dyn FnMut(_)>::new(move |e: MessageEvent| {
+ //tracing::error!("{}", ws.ready_state());
+ let data = e.data().as_string().expect("Expected string, found some other type");
+ let data: Packet = serde_json::from_str(&data).expect("Received invalid json from server");
+ let mut sender_clone = packet_sender.clone();
+ spawn_local(async move {
+ sender_clone.send(data).await.expect("Couldn't transmit packet to client");
+ });
+ });
+ ws.set_onmessage(Some(onmessage_callback.as_ref().unchecked_ref()));
+ onmessage_callback.forget();
+ Ws {
+ socket: Socket(ws),
+ sender,
+ receiver,
+ packet_receiver,
+ }
+ }
+ pub fn send_all_packets_from_channel(&mut self) {
+ //for packet in self.packet_receiver.iter() {
+ while let Ok(Some(packet)) = self.packet_receiver.try_next() {
+ self.socket.0.send_with_str(&serde_json::to_string(&packet).expect("Couldn't convert packet to json")).expect("Couldn't send packet to server");
+ }
+ }
+ pub fn send_packet(&mut self, packet: Packet) {
+ let socket = self.socket.0.clone();
+ spawn_local(async move {
+ //while socket.ready_state() != 1 { }
+ socket.send_with_str(&serde_json::to_string(&packet).expect("Couldn't convert packet to json")).expect("Couldn't send packet to server");
+ });
+ }
+}
M crates/client/src/rendering/assets_native.rs => crates/client/src/rendering/assets_native.rs +43 -2
@@ 1,4 1,14 @@
+use std::io::Read;
+
use bevy_ecs::system::Resource;
+use resvg::{tiny_skia, usvg};
+
+#[derive(Debug, Clone)]
+pub struct ImgData {
+ pub bytes: Vec<u8>,
+ pub width: u32,
+ pub height: u32,
+}
#[derive(Resource)]
pub struct Assets {
@@ 8,8 18,39 @@ impl Assets {
pub fn new() -> Self {
Assets { }
}
- pub fn get(&self, local_path: impl Into<String>) -> Option<Vec<u8>> {
- std::fs::read(format!("src/textures/{}", local_path.into())).ok()
+ pub fn get(&self, local_path: impl Into<String>) -> Option<ImgData> {
+ let local_path = local_path.into();
+ let bytes = std::fs::read(format!("src/assets/{}", local_path)).unwrap();
+ if local_path.ends_with(".svg") {
+ let opt = usvg::Options {
+ default_size: usvg::Size::from_wh(20.0, 20.0).unwrap(),
+ ..Default::default()
+ };
+ let tree = usvg::Tree::from_data(&bytes, &opt).expect(&format!("Couldn't parse svg {}", local_path));
+ let tree_size = tree.size().to_int_size();
+ let size = usvg::Size::from_wh(200.0, 200.0).unwrap().to_int_size();
+ assert!(size.width() > 0 && size.height() > 0);
+ let mut pixmap = tiny_skia::Pixmap::new(size.width(), size.height()).expect("Failed to construct pixmap");
+ resvg::render(&tree, tiny_skia::Transform::from_scale((size.width() as f32)/(tree_size.height() as f32), (size.height() as f32)/(tree_size.height() as f32)), &mut pixmap.as_mut());
+ let data = ImgData {
+ bytes: pixmap.data().to_vec(),
+ width: size.width(),
+ height: size.height(),
+ };
+
+ Some(data)
+ } else if local_path.ends_with(".png") {
+ let img = image::load_from_memory(&bytes).unwrap();
+ let rgba = img.to_rgba8();
+ let data = ImgData {
+ bytes: rgba.bytes().map(|byte| byte.unwrap()).collect::<Vec<u8>>(),
+ width: rgba.width(),
+ height: rgba.height(),
+ };
+ Some(data)
+ } else {
+ panic!("Unsupported sprite type");
+ }
}
}
M crates/client/src/rendering/assets_wasm.rs => crates/client/src/rendering/assets_wasm.rs +1 -1
@@ 66,7 66,7 @@ impl Assets {
};
let tree = usvg::Tree::from_data(&response.bytes, &opt).expect(&format!("Couldn't parse svg {}", local_path_clone));
let tree_size = tree.size().to_int_size();
- let size = usvg::Size::from_wh(200.0, 200.0).unwrap().to_int_size();
+ let size = usvg::Size::from_wh(500.0, 500.0).unwrap().to_int_size();
assert!(size.width() > 0 && size.height() > 0);
let mut pixmap = tiny_skia::Pixmap::new(size.width(), size.height()).expect("Failed to construct pixmap");
resvg::render(&tree, tiny_skia::Transform::from_scale((size.width() as f32)/(tree_size.height() as f32), (size.height() as f32)/(tree_size.height() as f32)), &mut pixmap.as_mut());
M crates/client/src/rendering/mod.rs => crates/client/src/rendering/mod.rs +57 -3
@@ 3,6 3,8 @@ use std::num::NonZeroU32;
use std::sync::Arc;
use assets::Assets;
+use bevy_ecs::entity::Entity;
+use bevy_ecs::event::Events;
use bevy_ecs::world::World;
use egui_glow::EguiGlow;
use glow::{HasContext, PixelUnpackData};
@@ 12,16 14,20 @@ use glutin::surface::{Surface, WindowSurface, GlSurface, SwapInterval};
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 starkingdoms_common::PlanetType;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::{prelude::Closure, JsCast};
#[cfg(target_arch = "wasm32")]
use web_sys::{Event, HtmlCanvasElement};
+use winit::event::MouseScrollDelta;
use winit::event_loop::ControlFlow;
#[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, WindowAttributes}};
-use crate::components::{Camera, Texture, Transform};
+use crate::components::{Camera, RecvPacket, SendPacket, Texture, Transform};
+use crate::networking::process_packets;
+use crate::networking::ws::Ws;
#[cfg(not(target_arch="wasm32"))]
#[path = "assets_native.rs"]
pub mod assets;
@@ 45,6 51,10 @@ pub struct App {
egui_glow: Option<EguiGlow>,
gl: Option<Arc<glow::Context>>,
textures: HashMap<String, glow::Texture>,
+
+ send_packet_events: Events<SendPacket>,
+ recv_packet_events: Events<RecvPacket>,
+ planet_types: HashMap<PlanetType, (Entity, u32)>, // (world entity, server id)
}
const VERTICES: [f32; 16] = [
@@ 59,9 69,15 @@ const INDICES: [u32; 6] = [
];
impl App {
- pub fn new(world: World) -> Self {
+ pub fn new(
+ world: World,
+ send_packet_events: Events<SendPacket>,
+ recv_packet_events: Events<RecvPacket>
+ ) -> Self {
Self {
world,
+ send_packet_events,
+ recv_packet_events,
..Default::default()
}
}
@@ 178,7 194,7 @@ impl ApplicationHandler for App {
gl.enable_vertex_attrib_array(1);
- gl.clear_color(0.0, 0.0, 0.0, 0.0);
+ gl.clear_color(0.7, 0.7, 0.7, 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);
@@ 225,13 241,44 @@ impl ApplicationHandler for App {
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::<Camera>().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 *= 1.0 / delta;
+ } else {
+ camera.zoom *= delta;
+ }
+ }
_ => {}
}
let event_response = self.egui_glow.as_mut().unwrap()
.on_window_event(self.window.as_ref().unwrap(), &event);
}
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
+ let mut ws = self.world.get_resource_mut::<Ws>().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 window = self.window.as_ref().unwrap();
let gl = self.gl.as_ref().unwrap();
@@ 288,6 335,7 @@ impl ApplicationHandler for App {
glow::UNSIGNED_BYTE, PixelUnpackData::Slice(Some(&image.bytes)));
gl.generate_mipmap(glow::TEXTURE_2D);
+ tracing::info!("{}", texture.name);
self.textures.insert(texture.name.clone(), texture_object);
}
// now the texture must exist
@@ 306,6 354,12 @@ impl ApplicationHandler for App {
#[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::<Ws>().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()));
}
}
M crates/server/src/player/client_login.rs => crates/server/src/player/client_login.rs +7 -5
@@ 314,11 314,13 @@ pub fn packet_stream(
flags: ProtoPartFlags { attached: false },
},
));
- let packet = Packet::PartPositions { parts };
- event_queue.push(WsEvent::Send {
- to: *from,
- message: packet.into_message(),
- });
+ for part in parts {
+ let packet = Packet::SpawnPart { id: part.0, part: part.1 };
+ event_queue.push(WsEvent::Send {
+ to: *from,
+ message: packet.into_message(),
+ });
+ }
// and send the welcome message :)
let packet = Packet::Message {