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;
use crate::client::ship::attachment::client_attachment_plugin;
use crate::shared::ecs::{GameplayState, TimeOffset};
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::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 struct ClientPlugin {
pub server: Option<String>
}
impl Plugin for ClientPlugin {
fn build(&self, app: &mut App) {
app
.init_gizmo_group::<StarguideGizmos>()
.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_systems(Update, find_me)
.insert_state(GameplayState::Main)
.insert_resource(DebugPickingMode::Disabled)
.insert_resource(TimeOffset::default());
let server = self.server.clone();
#[cfg(not(target_arch = "wasm32"))]
app.add_systems(PostStartup, move |mut commands: Commands, channels: Res<RepliconChannels>| {
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::<u64>();
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<RefCell<Option<ManagementInfo>>>,
}
#[cfg(target_arch = "wasm32")]
thread_local! {
static PENDING_SERVER_INFO: RefCell<Option<PendingServerInfo>> = RefCell::new(None);
}
#[cfg(target_arch = "wasm32")]
fn connect_when_ready(mut commands: Commands, channels: Res<RepliconChannels>) {
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<Hi>,
mut commands: Commands,
mut time_offset: ResMut<TimeOffset>,
) {
for msg in msgs.read() {
let we_are = msg.you_are;
info!(?we_are, "joined successfully");
commands.entity(we_are).insert(Me);
time_offset.0 = msg.time_offset;
}
}