//! Client-seitige WebSocket-Anbindung an `audiopoker_server` (siehe PLAN.md, Phase 2). //! //! `web_sys::WebSocket` ist callback-basiert (onopen/onmessage/onerror/onclose) //! und die dafür nötigen `Closure`s sind nicht `Send`. Deshalb wird die //! Verbindung nicht als normale Bevy-`Resource`, sondern als //! Non-Send-Resource gehalten (`app.insert_non_send_resource`, gelesen über //! `NonSend`/`NonSendMut`) - das ist das übliche Bevy-Muster für JS-Interop. //! Eingehende Nachrichten landen zunächst in einer gemeinsamen Queue //! (`Rc>>`), die ein normales Bevy-System jeden Frame //! leert und in `Announce`/`PlaySfx`/`NetworkGameView`-Updates übersetzt //! (siehe `game_plugin.rs`). //! //! Wichtiger Hinweis zur Verifikation: Dieser Code nutzt `#[cfg(target_arch //! = "wasm32")]`, weil `web_sys::WebSocket` zur Laufzeit eine echte //! Browser-Umgebung braucht. Ein natives `cargo check`/`cargo build` prüft //! ausschließlich den `not(wasm32)`-Dummy-Zweig unten - der eigentliche //! WebSocket-Code wird dabei NICHT typgeprüft. Zur echten Verifikation bitte //! zusätzlich `cargo check --target wasm32-unknown-unknown -p //! audiopoker_client` ausführen. use audiopoker_core::network::{ClientMessage, ServerMessage}; /// Serverurl/Tisch/Spielername lassen sich über die Browser-URL steuern, /// z. B. `index.html?server=ws://localhost:9001&table=tisch1&name=Anna` - /// praktisch, um mehrere Browser-Tabs als unterschiedliche Spieler an /// denselben Tisch zu hängen. pub struct ConnectionConfig { pub server_url: String, pub table_name: String, pub player_name: String, } #[cfg(target_arch = "wasm32")] mod wasm_impl { use std::cell::RefCell; use std::collections::VecDeque; use std::rc::Rc; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; use web_sys::{CloseEvent, ErrorEvent, MessageEvent, WebSocket}; use super::{ClientMessage, ConnectionConfig, ServerMessage}; /// Non-Send-Resource: hält die WebSocket-Verbindung und die Closures, /// die JS beim Aufruf von `set_on...` am Leben halten muss (würden sie /// gedroppt, ruft der Browser sie nicht mehr auf bzw. meldet einen Fehler). pub struct NetworkClient { socket: WebSocket, incoming: Rc>>, _on_open: Closure, _on_message: Closure, _on_error: Closure, _on_close: Closure, } impl NetworkClient { pub fn connect(config: ConnectionConfig) -> Self { let socket = WebSocket::new(&config.server_url) .expect("WebSocket-Verbindung konnte nicht aufgebaut werden"); let incoming = Rc::new(RefCell::new(VecDeque::new())); let on_open = { let socket = socket.clone(); let table_name = config.table_name.clone(); let player_name = config.player_name.clone(); Closure::::new(move || { let join = ClientMessage::JoinTable { table_name: table_name.clone(), player_name: player_name.clone(), }; if let Ok(text) = serde_json::to_string(&join) { let _ = socket.send_with_str(&text); } }) }; socket.set_onopen(Some(on_open.as_ref().unchecked_ref())); let on_message = { let incoming = Rc::clone(&incoming); Closure::::new(move |event: MessageEvent| { let Some(text) = event.data().as_string() else { return; }; match serde_json::from_str::(&text) { Ok(message) => incoming.borrow_mut().push_back(message), Err(err) => web_sys::console::warn_1( &format!("Ungültige Server-Nachricht ignoriert: {err}").into(), ), } }) }; socket.set_onmessage(Some(on_message.as_ref().unchecked_ref())); let on_error = Closure::::new(move |_event: ErrorEvent| { web_sys::console::error_1(&"WebSocket-Fehler.".into()); }); socket.set_onerror(Some(on_error.as_ref().unchecked_ref())); let on_close = Closure::::new(move |event: CloseEvent| { web_sys::console::warn_1( &format!("WebSocket-Verbindung geschlossen ({}).", event.reason()).into(), ); }); socket.set_onclose(Some(on_close.as_ref().unchecked_ref())); Self { socket, incoming, _on_open: on_open, _on_message: on_message, _on_error: on_error, _on_close: on_close, } } /// Schickt eine Client-Nachricht, sofern die Verbindung offen ist. /// Verworfene Nachrichten (z. B. weil noch nicht verbunden) landen /// nur als Konsolen-Warnung, nicht als Panik - ein Verbindungsabbruch /// soll das Spiel nicht crashen lassen. pub fn send(&self, message: &ClientMessage) { if self.socket.ready_state() != WebSocket::OPEN { web_sys::console::warn_1(&"Nicht verbunden, Nachricht verworfen.".into()); return; } match serde_json::to_string(message) { Ok(text) => { let _ = self.socket.send_with_str(&text); } Err(err) => { web_sys::console::error_1(&format!("Konnte Nachricht nicht kodieren: {err}").into()); } } } /// Entnimmt alle bislang eingegangenen Server-Nachrichten (FIFO). pub fn drain_incoming(&self) -> Vec { self.incoming.borrow_mut().drain(..).collect() } } /// Liest einen Query-Parameter aus der aktuellen Browser-URL, mit Fallback. pub fn read_query_param(name: &str, default: &str) -> String { web_sys::window() .and_then(|w| w.location().search().ok()) .and_then(|search| web_sys::UrlSearchParams::new_with_str(&search).ok()) .and_then(|params| params.get(name)) .unwrap_or_else(|| default.to_string()) } /// Zufallszahl für Default-Spielernamen ("Spieler1234"), ohne auf /// `rand`/OS-Entropie angewiesen zu sein (im Browser über `Math.random`). pub fn random_suffix() -> u32 { (js_sys::Math::random() * 10_000.0) as u32 } } #[cfg(not(target_arch = "wasm32"))] mod native_stub { use super::{ClientMessage, ConnectionConfig, ServerMessage}; /// Dummy für native Builds/Tests: `web_sys::WebSocket` funktioniert zur /// Laufzeit nur im Browser. Diese Variante verbindet sich nicht wirklich, /// sondern loggt nur - so bleiben `cargo check`/`cargo test` auf dem /// nativen Target weiterhin nutzbar (siehe Modul-Kommentar oben). pub struct NetworkClient; impl NetworkClient { pub fn connect(config: ConnectionConfig) -> Self { println!( "Networking (Dummy, natives Target): würde zu {} verbinden (Tisch '{}', Name '{}').", config.server_url, config.table_name, config.player_name ); Self } pub fn send(&self, _message: &ClientMessage) {} pub fn drain_incoming(&self) -> Vec { Vec::new() } } pub fn read_query_param(_name: &str, default: &str) -> String { default.to_string() } pub fn random_suffix() -> u32 { 0 } } #[cfg(target_arch = "wasm32")] pub use wasm_impl::*; #[cfg(not(target_arch = "wasm32"))] pub use native_stub::*;