From 76100ca42516d88511febf1b8ff4c302410eab87 Mon Sep 17 00:00:00 2001 From: ghostly_zsh Date: Sat, 22 Mar 2025 21:00:19 -0500 Subject: [PATCH] networking, parts rendering --- crates/client/src/components.rs | 31 ++++- crates/client/src/lib.rs | 19 +++- crates/client/src/networking/mod.rs | 114 +++++++++++++++++++ crates/client/src/networking/ws_native.rs | 61 ++++++++++ crates/client/src/networking/ws_wasm.rs | 78 +++++++++++++ crates/client/src/rendering/assets_native.rs | 45 +++++++- crates/client/src/rendering/assets_wasm.rs | 2 +- crates/client/src/rendering/mod.rs | 60 +++++++++- crates/server/src/player/client_login.rs | 12 +- 9 files changed, 400 insertions(+), 22 deletions(-) create mode 100644 crates/client/src/networking/mod.rs create mode 100644 crates/client/src/networking/ws_native.rs create mode 100644 crates/client/src/networking/ws_wasm.rs diff --git a/crates/client/src/components.rs b/crates/client/src/components.rs index 0015b147040c1e7a4d2439b6bbe399acdcf29c83..61d5063629cf63e0613c359a10d57fdf40ccf32a 100644 --- a/crates/client/src/components.rs +++ b/crates/client/src/components.rs @@ -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, - pub rotation: Rotation3, + pub rotation: Rotation2, pub scale: Scale3, } impl Transform { pub fn to_matrix(&self) -> Matrix4 { - 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); diff --git a/crates/client/src/lib.rs b/crates/client/src/lib.rs index 164964d2594365513f086d72b5d7541b309744f7..827c16025fdacc77dfa8cb7be318dcdb0fcd699e 100644 --- a/crates/client/src/lib.rs +++ b/crates/client/src/lib.rs @@ -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::::default(); + let recv_packet_events = Events::::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(); } diff --git a/crates/client/src/networking/mod.rs b/crates/client/src/networking/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..047d421d2cca8f1d90a0b44433e301317a868f3e --- /dev/null +++ b/crates/client/src/networking/mod.rs @@ -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, + recv_packet_events: &mut Events, + planet_types: &mut HashMap, +) { + 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::>(); + 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>(); + let server_id = player_query.single(world); + if server_id.0 == *id { + let mut camera = world.resource_mut::(); + camera.x = -part.transform.x; + camera.y = -part.transform.y; + } + } + let mut part_query = world.query_filtered::<(&ServerId, &mut Transform), With>(); + 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>(); + 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)); + } + } + } + _ => {} + } + } +} diff --git a/crates/client/src/networking/ws_native.rs b/crates/client/src/networking/ws_native.rs new file mode 100644 index 0000000000000000000000000000000000000000..61a07169c88058d531241e96b0f4e53e0e888a50 --- /dev/null +++ b/crates/client/src/networking/ws_native.rs @@ -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; + fn into_message(&self) -> Message; +} + +impl PacketMessageConvert for Packet { + fn from_message(value: &Message) -> Result { + 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>>>, + pub sender: Sender, + pub receiver: Receiver, + packet_receiver: Receiver, +} +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"); + } +} diff --git a/crates/client/src/networking/ws_wasm.rs b/crates/client/src/networking/ws_wasm.rs new file mode 100644 index 0000000000000000000000000000000000000000..c438a92aa05f4ea335f5e0f741e5b49666dcb370 --- /dev/null +++ b/crates/client/src/networking/ws_wasm.rs @@ -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, + pub receiver: UnboundedReceiver, + packet_receiver: UnboundedReceiver, +} + +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::::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::::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"); + }); + } +} diff --git a/crates/client/src/rendering/assets_native.rs b/crates/client/src/rendering/assets_native.rs index 2ef454a7c946821caddfe21aa78771a457ef5cd5..e55b80aea13bffce44cc375929c9db58d8f97678 100644 --- a/crates/client/src/rendering/assets_native.rs +++ b/crates/client/src/rendering/assets_native.rs @@ -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, + 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) -> Option> { - std::fs::read(format!("src/textures/{}", local_path.into())).ok() + pub fn get(&self, local_path: impl Into) -> Option { + 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::>(), + width: rgba.width(), + height: rgba.height(), + }; + Some(data) + } else { + panic!("Unsupported sprite type"); + } } } diff --git a/crates/client/src/rendering/assets_wasm.rs b/crates/client/src/rendering/assets_wasm.rs index 24063090370e1c2b1d0afcb5cd661e0fa59afbd1..85de854aac4d43ca45643c7fd0e866c2a9499506 100644 --- a/crates/client/src/rendering/assets_wasm.rs +++ b/crates/client/src/rendering/assets_wasm.rs @@ -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()); diff --git a/crates/client/src/rendering/mod.rs b/crates/client/src/rendering/mod.rs index 41da4acd8305933d4b2fd85f832536220efa74bf..b5022237a3592490e8bd681c77b60c467fd9a507 100644 --- a/crates/client/src/rendering/mod.rs +++ b/crates/client/src/rendering/mod.rs @@ -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, gl: Option>, textures: HashMap, + + send_packet_events: Events, + recv_packet_events: Events, + planet_types: HashMap, // (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, + recv_packet_events: Events + ) -> 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::().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::().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::().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())); } } diff --git a/crates/server/src/player/client_login.rs b/crates/server/src/player/client_login.rs index 0440db9bf2786d30c78e630c250fb5aa00ef86c8..2b01b261b159c8705f123463e05307cc2d26c890 100644 --- a/crates/server/src/player/client_login.rs +++ b/crates/server/src/player/client_login.rs @@ -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 {