Network logic
This commit is contained in:
+3
-5
@@ -8,12 +8,10 @@ audiopoker_core = { path = "../core" }
|
||||
bevy = { version = "0.19", features = ["webgl2"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
# wasm-bindgen/web-sys/js-sys/futures: aktuell nur für audio::tts (Web Speech
|
||||
# API) genutzt. Werden zusätzlich gebraucht, sobald der Client per
|
||||
# web_sys::WebSocket an den `server` (siehe ../server) angebunden wird -
|
||||
# das ist noch offen, siehe PLAN.md.
|
||||
# wasm-bindgen/web-sys/js-sys/futures: für audio::tts (Web Speech API) und
|
||||
# network (WebSocket-Anbindung an ../server), siehe src/network.rs.
|
||||
wasm-bindgen = "0.2"
|
||||
web-sys = { version = "0.3", features = ["SpeechRecognition", "SpeechSynthesis", "SpeechSynthesisUtterance", "Window", "HtmlInputElement", "console", "WebSocket", "MessageEvent"] }
|
||||
web-sys = { version = "0.3", features = ["SpeechRecognition", "SpeechSynthesis", "SpeechSynthesisUtterance", "Window", "HtmlInputElement", "console", "WebSocket", "MessageEvent", "ErrorEvent", "CloseEvent", "Location", "UrlSearchParams"] }
|
||||
js-sys = "0.3"
|
||||
futures = "0.3"
|
||||
rand = "0.8.5"
|
||||
|
||||
+139
-62
@@ -1,10 +1,17 @@
|
||||
//! Verbindet die (bewusst Bevy-unabhängige) Kernlogik aus `core` und `audio`
|
||||
//! mit einer echten Bevy-`App` (siehe PLAN.md, Phase 1 & 3).
|
||||
//! Verbindet die Bevy-`App` mit der Audio-Engine (`audio`) und dem
|
||||
//! `network`-Modul, das die WebSocket-Verbindung zu `audiopoker_server`
|
||||
//! hält (siehe PLAN.md, Phase 2/3).
|
||||
//!
|
||||
//! Architektur: Der Server ist die einzige Quelle der Wahrheit für den
|
||||
//! Spielzustand (siehe `core::network`-Doku). Der Client hält deshalb nur
|
||||
//! eine schlanke `NetworkGameView` (eigene Hole Cards, sichtbare Community
|
||||
//! Cards, wer am Zug ist) statt eines vollen `GameState` - das verhindert
|
||||
//! auch, dass ein manipulierter Client sich selbst fremde Hole Cards zeigen
|
||||
//! könnte, da der Server sie ohnehin nur an den jeweiligen Spieler schickt.
|
||||
//!
|
||||
//! Hinweis zur Bevy-Version: Seit Bevy 0.17 sind gepufferte Events als
|
||||
//! `Message` (statt `Event`) modelliert und laufen über `MessageWriter` /
|
||||
//! `MessageReader` / `app.add_message::<T>()`. `Event` bezeichnet seither
|
||||
//! ausschließlich Observer-Events. Dieser Code ist auf Bevy 0.19 ausgelegt.
|
||||
//! `Message` (statt `Event`) modelliert, siehe `MessageWriter`/`MessageReader`/
|
||||
//! `app.add_message::<T>()`. Dieser Code ist auf Bevy 0.19 ausgelegt.
|
||||
//!
|
||||
//! Steuerung (barrierefrei, tastaturzentriert, siehe PLAN.md):
|
||||
//! - `Tab`: wechselt die ausgewählte Aktion (Fold / Call-Check / Raise).
|
||||
@@ -12,14 +19,11 @@
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
use crate::audio::AudioEngine;
|
||||
use audiopoker_core::game::{BettingAction, GameManager, GameState, Round, RAISE_INCREMENT};
|
||||
use audiopoker_core::logic::Deck;
|
||||
use audiopoker_core::logic::card::Card;
|
||||
use audiopoker_core::network::{ClientMessage, ServerMessage};
|
||||
|
||||
/// Bevy-Resource-Wrapper um den Engine-unabhängigen Spielzustand.
|
||||
/// Bewusst als Newtype, damit `core::game` selbst keine Bevy-Abhängigkeit braucht.
|
||||
#[derive(Resource)]
|
||||
pub struct Table(pub GameState);
|
||||
use crate::audio::AudioEngine;
|
||||
use crate::network::{self, ConnectionConfig, NetworkClient};
|
||||
|
||||
/// Bevy-Resource-Wrapper um die Engine-unabhängige Audio-Engine (`audio::AudioEngine`).
|
||||
#[derive(Resource)]
|
||||
@@ -34,7 +38,19 @@ pub struct ActionCursor {
|
||||
|
||||
const ACTION_LABELS: [&str; 3] = ["Fold", "Call/Check", "Raise"];
|
||||
|
||||
/// Message: Ein Text soll per TTS angesagt werden (z. B. "Spieler 1 setzt 50").
|
||||
/// Der clientseitige Blick auf das Spiel: nur das, was der Server diesem
|
||||
/// Client tatsächlich mitgeteilt hat. Kein autoritativer Zustand - der
|
||||
/// liegt beim Server (`audiopoker_server::table::TableActor`).
|
||||
#[derive(Resource, Default)]
|
||||
pub struct NetworkGameView {
|
||||
pub my_player_id: Option<u32>,
|
||||
pub hole_cards: Vec<Card>,
|
||||
pub community_cards: Vec<Card>,
|
||||
pub to_act: Option<u32>,
|
||||
pub hand_finished: bool,
|
||||
}
|
||||
|
||||
/// Message: Ein Text soll per TTS angesagt werden.
|
||||
#[derive(Message, Debug, Clone)]
|
||||
pub struct Announce(pub String);
|
||||
|
||||
@@ -48,55 +64,119 @@ impl Plugin for AudiopokerPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.insert_resource(AudioRes(AudioEngine::new()))
|
||||
.init_resource::<ActionCursor>()
|
||||
.init_resource::<NetworkGameView>()
|
||||
.add_message::<Announce>()
|
||||
.add_message::<PlaySfx>()
|
||||
.add_systems(Startup, setup_game)
|
||||
.add_systems(Update, (handle_player_input, speak_announcements, play_sfx_cues));
|
||||
.add_systems(Startup, setup_network)
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
receive_network_messages,
|
||||
handle_player_input,
|
||||
speak_announcements,
|
||||
play_sfx_cues,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Startet eine neue Partie, teilt Karten aus und kündigt beides per
|
||||
/// TTS/SFX-Message an. Läuft einmalig im `Startup`-Schedule.
|
||||
fn setup_game(
|
||||
mut commands: Commands,
|
||||
/// Baut beim Start die WebSocket-Verbindung auf. Server-/Tisch-/Spielername
|
||||
/// lassen sich über die Browser-URL steuern (siehe `network::read_query_param`),
|
||||
/// damit mehrere Browser-Tabs bequem als unterschiedliche Spieler denselben
|
||||
/// Tisch testen können.
|
||||
fn setup_network(world: &mut World) {
|
||||
let config = ConnectionConfig {
|
||||
server_url: network::read_query_param("server", "ws://127.0.0.1:9001"),
|
||||
table_name: network::read_query_param("table", "tisch1"),
|
||||
player_name: network::read_query_param(
|
||||
"name",
|
||||
&format!("Spieler{}", network::random_suffix()),
|
||||
),
|
||||
};
|
||||
|
||||
let client = NetworkClient::connect(config);
|
||||
world.insert_non_send_resource(client);
|
||||
}
|
||||
|
||||
/// Übersetzt eine Server-`PlaySfx`-Cue-ID (freier String) in eine der lokal
|
||||
/// bekannten, `'static` Cue-IDs. Unbekannte IDs werden ignoriert, statt das
|
||||
/// Spiel abstürzen zu lassen (z. B. falls Server und Client mal auseinanderlaufen).
|
||||
fn map_sfx_id(id: &str) -> Option<&'static str> {
|
||||
match id {
|
||||
"shuffle" => Some("shuffle"),
|
||||
"chip" => Some("chip"),
|
||||
"fold" => Some("fold"),
|
||||
"flip" => Some("flip"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Leert die eingehende Nachrichten-Queue der Netzwerkverbindung und
|
||||
/// übersetzt jede `ServerMessage` in ein Update von `NetworkGameView`
|
||||
/// und/oder eine `Announce`/`PlaySfx`-Message fürs Audio-Feedback.
|
||||
fn receive_network_messages(
|
||||
net: NonSend<NetworkClient>,
|
||||
mut view: ResMut<NetworkGameView>,
|
||||
mut announcements: MessageWriter<Announce>,
|
||||
mut sfx_cues: MessageWriter<PlaySfx>,
|
||||
) {
|
||||
let mut game = GameManager::start_new_game(vec!["Spieler 1".into(), "Spieler 2".into()]);
|
||||
|
||||
let mut deck = Deck::new();
|
||||
deck.shuffle();
|
||||
for player in game.players.iter_mut() {
|
||||
player.hole_cards = deck.deal_hand(2);
|
||||
for message in net.drain_incoming() {
|
||||
match message {
|
||||
ServerMessage::Welcome { player_id } => {
|
||||
view.my_player_id = Some(player_id);
|
||||
announcements.write(Announce(format!("Verbunden als Spieler {player_id}.")));
|
||||
announcements.write(Announce(
|
||||
"Steuerung: Tab wählt eine Aktion aus, Leertaste oder Eingabetaste bestätigt.".into(),
|
||||
));
|
||||
}
|
||||
ServerMessage::Announce(text) => announcements.write(Announce(text)),
|
||||
ServerMessage::PlaySfx(id) => {
|
||||
if let Some(cue) = map_sfx_id(&id) {
|
||||
sfx_cues.write(PlaySfx(cue));
|
||||
}
|
||||
}
|
||||
ServerMessage::HoleCards(cards) => {
|
||||
let spoken = cards
|
||||
.iter()
|
||||
.map(|c| c.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" und ");
|
||||
announcements.write(Announce(format!("Deine Karten: {spoken}.")));
|
||||
view.hole_cards = cards;
|
||||
}
|
||||
ServerMessage::CommunityCards(cards) => {
|
||||
view.community_cards = cards;
|
||||
}
|
||||
ServerMessage::TurnToAct { player_id } => {
|
||||
view.to_act = Some(player_id);
|
||||
if view.my_player_id == Some(player_id) {
|
||||
announcements.write(Announce("Du bist am Zug.".into()));
|
||||
}
|
||||
}
|
||||
ServerMessage::HandFinished => {
|
||||
view.hand_finished = true;
|
||||
}
|
||||
ServerMessage::Error(reason) => {
|
||||
announcements.write(Announce(format!("Fehler: {reason}")));
|
||||
}
|
||||
}
|
||||
}
|
||||
game.community_cards = deck.deal_hand(5);
|
||||
|
||||
sfx_cues.write(PlaySfx("shuffle"));
|
||||
announcements.write(Announce(format!(
|
||||
"Neues Spiel gestartet mit {} Spielern.",
|
||||
game.players.len()
|
||||
)));
|
||||
announcements.write(Announce(
|
||||
"Steuerung: Tab wählt eine Aktion aus, Leertaste oder Eingabetaste bestätigt.".into(),
|
||||
));
|
||||
announcements.write(Announce(format!("{} ist am Zug.", game.players[game.to_act].name)));
|
||||
|
||||
commands.insert_resource(Table(game));
|
||||
}
|
||||
|
||||
/// Liest Tab/Space/Enter und wendet die ausgewählte Aktion auf den aktuell
|
||||
/// am Zug befindlichen Spieler an. Die eigentliche Spiellogik (Straßen-
|
||||
/// Fortschritt, Showdown) steckt komplett in `GameManager::take_action`.
|
||||
/// Liest Tab/Space/Enter und schickt bei Bestätigung die passende
|
||||
/// `ClientMessage` an den Server. Die eigentliche Spiellogik läuft
|
||||
/// ausschließlich serverseitig (`GameManager::take_action`); der Client
|
||||
/// wartet einfach auf die resultierenden `ServerMessage`s.
|
||||
fn handle_player_input(
|
||||
keys: Res<ButtonInput<KeyCode>>,
|
||||
mut cursor: ResMut<ActionCursor>,
|
||||
mut table: ResMut<Table>,
|
||||
view: Res<NetworkGameView>,
|
||||
net: NonSend<NetworkClient>,
|
||||
mut announcements: MessageWriter<Announce>,
|
||||
mut sfx_cues: MessageWriter<PlaySfx>,
|
||||
) {
|
||||
if table.0.current_round == Round::Showdown {
|
||||
// Hand ist beendet. Eine neue Hand starten ist noch nicht implementiert
|
||||
// (nächster sinnvoller Schritt, z. B. über eine eigene "Neue Hand"-Taste).
|
||||
if view.hand_finished {
|
||||
// Hand ist beendet. Eine neue Hand starten ist noch nicht
|
||||
// implementiert (nächster sinnvoller Schritt, siehe PLAN.md).
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -106,24 +186,21 @@ fn handle_player_input(
|
||||
}
|
||||
|
||||
if keys.just_pressed(KeyCode::Space) || keys.just_pressed(KeyCode::Enter) {
|
||||
let state = &mut table.0;
|
||||
let player_idx = state.to_act;
|
||||
let to_call = GameManager::amount_to_call(state, player_idx);
|
||||
|
||||
let action = match cursor.selected {
|
||||
0 => BettingAction::Fold,
|
||||
1 if to_call == 0 => BettingAction::Check,
|
||||
1 => BettingAction::Call(to_call),
|
||||
_ => BettingAction::Raise(to_call + RAISE_INCREMENT),
|
||||
let Some(my_id) = view.my_player_id else {
|
||||
announcements.write(Announce("Noch nicht mit dem Tisch verbunden.".into()));
|
||||
return;
|
||||
};
|
||||
if view.to_act != Some(my_id) {
|
||||
announcements.write(Announce("Du bist gerade nicht am Zug.".into()));
|
||||
return;
|
||||
}
|
||||
|
||||
let outcome = GameManager::take_action(state, player_idx, action);
|
||||
for line in outcome.announcements {
|
||||
announcements.write(Announce(line));
|
||||
}
|
||||
for cue in outcome.sfx_cues {
|
||||
sfx_cues.write(PlaySfx(cue));
|
||||
}
|
||||
let message = match cursor.selected {
|
||||
0 => ClientMessage::Fold,
|
||||
1 => ClientMessage::Check, // Server wertet das bei fälligem Call automatisch als Call.
|
||||
_ => ClientMessage::Raise,
|
||||
};
|
||||
net.send(&message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
mod audio;
|
||||
mod game_plugin;
|
||||
mod network;
|
||||
|
||||
use bevy::prelude::*;
|
||||
use game_plugin::AudiopokerPlugin;
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
//! 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<RefCell<VecDeque<...>>>`), 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<RefCell<VecDeque<ServerMessage>>>,
|
||||
_on_open: Closure<dyn FnMut()>,
|
||||
_on_message: Closure<dyn FnMut(MessageEvent)>,
|
||||
_on_error: Closure<dyn FnMut(ErrorEvent)>,
|
||||
_on_close: Closure<dyn FnMut(CloseEvent)>,
|
||||
}
|
||||
|
||||
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::<dyn FnMut()>::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::<dyn FnMut(MessageEvent)>::new(move |event: MessageEvent| {
|
||||
let Some(text) = event.data().as_string() else {
|
||||
return;
|
||||
};
|
||||
match serde_json::from_str::<ServerMessage>(&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::<dyn FnMut(ErrorEvent)>::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::<dyn FnMut(CloseEvent)>::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<ServerMessage> {
|
||||
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<ServerMessage> {
|
||||
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::*;
|
||||
Reference in New Issue
Block a user