use crate::client::crafting::ui::crafting_ui_plugin; use crate::client::key_input::key_input_plugin; use crate::client::parts::parts_plugin; use crate::client::planet::indicators::indicators_plugin; use crate::client::starfield::starfield_plugin; use crate::client::starguide::orbit::starguide_orbit_plugin; use crate::client::ui::ui_plugin; use crate::client::zoom::zoom_plugin; use crate::client::starguide::init::starguide_init_plugin; use crate::client::starguide::input::starguide_input_plugin; use starguide::components::StarguideGizmos; use bevy::dev_tools::picking_debug::DebugPickingMode; use crate::prelude::*; use planet::incoming_planets::incoming_planets_plugin; use crate::client::components::{Me, ServerClock, ServerTimeOffset}; use crate::client::ship::attachment::client_attachment_plugin; use crate::shared::ecs::GameplayState; use crate::shared::net::{parse_management_address, ManagementInfo, Hi, STARKINGDOMS_PROTOCOL_MAGIC}; use bevy_replicon::prelude::RepliconChannels; use bevy_replicon_renet2::RenetChannelsExt; use bevy_replicon_renet2::netcode::{ClientAuthentication, NetcodeClientTransport}; use bevy_replicon_renet2::renet2::{ConnectionConfig, RenetClient}; use std::net::{IpAddr, SocketAddr}; use web_time::SystemTime; #[cfg(not(target_arch = "wasm32"))] use bevy_replicon_renet2::netcode::NativeSocket; #[cfg(not(target_arch = "wasm32"))] use std::io::{Read, Write}; #[cfg(not(target_arch = "wasm32"))] use std::net::{TcpStream, UdpSocket}; #[cfg(target_arch = "wasm32")] use bevy_replicon_renet2::netcode::{ServerCertHash, WebServerDestination, WebTransportClient, WebTransportClientConfig}; #[cfg(target_arch = "wasm32")] use crate::shared::net::decode_cert_hash; #[cfg(target_arch = "wasm32")] use std::cell::RefCell; #[cfg(target_arch = "wasm32")] use std::rc::Rc; #[cfg(target_arch = "wasm32")] use wasm_bindgen::JsCast; #[cfg(target_arch = "wasm32")] use wasm_bindgen_futures::JsFuture; use crate::client::interpolation::interpolation_plugin; use crate::client::server_clock::server_clock_plugin; use crate::shared::config::planet::Planet; pub mod colors; pub mod key_input; pub mod parts; pub mod planet; pub mod starfield; pub mod ui; pub mod zoom; pub mod ship; pub mod rendering; pub mod input; pub mod starguide; pub mod crafting; pub mod components; pub mod plugins; pub mod interpolation; pub mod server_clock; pub struct ClientPlugin { pub server: Option } impl Plugin for ClientPlugin { fn build(&self, app: &mut App) { app .init_gizmo_group::() .add_plugins(rendering::render_plugin) .add_plugins(input::input_plugin) .add_plugins(ship::thrusters::client_thrusters_plugin) .add_plugins((incoming_planets_plugin, indicators_plugin)) .add_plugins(parts_plugin) .add_plugins(key_input_plugin) .add_plugins(starfield_plugin) .add_plugins(ui_plugin) .add_plugins(zoom_plugin) .add_plugins(client_attachment_plugin) .add_plugins(starguide_init_plugin) .add_plugins(starguide_input_plugin) //.add_plugins(starguide_orbit_plugin) .add_plugins(crafting_ui_plugin) .add_plugins(interpolation_plugin) .add_plugins(server_clock_plugin) .add_systems(Update, find_me) .insert_state(GameplayState::Main) .insert_resource(DebugPickingMode::Disabled) .insert_resource(ServerTimeOffset::default()); let server = self.server.clone(); #[cfg(not(target_arch = "wasm32"))] app.add_systems(PostStartup, move |mut commands: Commands, channels: Res| { let Some(server) = server.as_ref() else { return }; let management_addr = parse_management_address(server).expect("invalid server connection string"); let info = fetch_management_info(management_addr); let (client, transport) = build_client(&channels, management_addr.ip(), &info); commands.insert_resource(client); commands.insert_resource(transport); }); #[cfg(target_arch = "wasm32")] { app.add_systems(PostStartup, move || { let Some(server) = server.as_ref() else { return }; let management_addr = parse_management_address(server).expect("invalid server connection string"); let info = Rc::new(RefCell::new(None)); let info_handle = info.clone(); wasm_bindgen_futures::spawn_local(async move { *info_handle.borrow_mut() = Some(fetch_management_info(management_addr).await); }); PENDING_SERVER_INFO.with(|pending| { *pending.borrow_mut() = Some(PendingServerInfo { addr: management_addr, info }); }); }); app.add_systems(Update, connect_when_ready); } } } fn build_client(channels: &RepliconChannels, host: IpAddr, info: &ManagementInfo) -> (RenetClient, NetcodeClientTransport) { let connection_config = ConnectionConfig::from_channels(channels.server_configs(), channels.client_configs()); let client = RenetClient::new(connection_config, false); let client_id = rand::random::(); let current_time = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap(); #[cfg(not(target_arch = "wasm32"))] let (socket_id, server_addr, transport) = { let bind_addr = if host.is_ipv6() { "[::]:0" } else { "0.0.0.0:0" }; let socket = NativeSocket::new(UdpSocket::bind(bind_addr).expect("failed to bind client UDP socket")) .expect("failed to create native socket"); let socket_id = 0; let server_addr = SocketAddr::new(host, info.native_port); let authentication = ClientAuthentication::Unsecure { protocol_id: STARKINGDOMS_PROTOCOL_MAGIC, client_id, socket_id, server_addr, user_data: None, }; let transport = NetcodeClientTransport::new(current_time, authentication, socket) .expect("failed to create client transport"); (socket_id, server_addr, transport) }; #[cfg(target_arch = "wasm32")] let (socket_id, server_addr, transport) = { let server_addr = SocketAddr::new(host, info.wt_port); let cert_hash = ServerCertHash { hash: decode_cert_hash(&info.cert_hash).expect("invalid cert_hash") }; let socket = WebTransportClient::new(WebTransportClientConfig::new_with_certs( WebServerDestination::Addr(server_addr), vec![cert_hash], )); let socket_id = 1; let authentication = ClientAuthentication::Unsecure { protocol_id: STARKINGDOMS_PROTOCOL_MAGIC, client_id, socket_id, server_addr, user_data: None, }; let transport = NetcodeClientTransport::new(current_time, authentication, socket) .expect("failed to create client transport"); (socket_id, server_addr, transport) }; info!(?socket_id, ?server_addr, "connecting to server"); (client, transport) } #[cfg(not(target_arch = "wasm32"))] fn fetch_management_info(addr: SocketAddr) -> ManagementInfo { let mut stream = TcpStream::connect(addr).expect("failed to connect to management port"); stream.write_all(format!("GET / HTTP/1.1\r\nHost: {addr}\r\nConnection: close\r\n\r\n").as_bytes()) .expect("failed to send management request"); let mut response = Vec::new(); stream.read_to_end(&mut response).expect("failed to read management response"); let body_start = response.windows(4).position(|w| w == b"\r\n\r\n") .map(|i| i + 4) .expect("malformed management response"); serde_json::from_slice(&response[body_start..]).expect("invalid management response body") } #[cfg(target_arch = "wasm32")] async fn fetch_management_info(addr: SocketAddr) -> ManagementInfo { let window = web_sys::window().expect("no window"); let response = JsFuture::from(window.fetch_with_str(&format!("http://{addr}/"))) .await .expect("management fetch failed"); let response: web_sys::Response = response.dyn_into().expect("fetch did not return a Response"); let text = JsFuture::from(response.text().expect("failed to read response body")) .await .expect("failed to read response body"); let text = text.as_string().expect("response body was not a string"); serde_json::from_str(&text).expect("invalid management response body") } #[cfg(target_arch = "wasm32")] struct PendingServerInfo { addr: SocketAddr, info: Rc>>, } #[cfg(target_arch = "wasm32")] thread_local! { static PENDING_SERVER_INFO: RefCell> = RefCell::new(None); } #[cfg(target_arch = "wasm32")] fn connect_when_ready(mut commands: Commands, channels: Res) { let ready = PENDING_SERVER_INFO.with(|pending| { let mut pending = pending.borrow_mut(); let info = pending.as_ref()?.info.borrow_mut().take()?; let addr = pending.take().unwrap().addr; Some((addr, info)) }); let Some((addr, info)) = ready else { return }; let (client, transport) = build_client(&channels, addr.ip(), &info); commands.insert_resource(client); commands.insert_resource(transport); } pub fn find_me( mut msgs: MessageReader, mut commands: Commands, mut time_offset: ResMut, time: Res