diff --git a/werewolves/index.scss b/werewolves/index.scss index cfedac1..d72b047 100644 --- a/werewolves/index.scss +++ b/werewolves/index.scss @@ -737,3 +737,20 @@ input { .zoom { zoom: 200%; } + +.client-lobby-player-list { + @extend .row-list; + + gap: 10px; + + &>* { + border: 1px solid white; + padding: 10px; + text-align: center; + + &:hover { + background-color: #fff; + color: #000; + } + } +} diff --git a/werewolves/src/clients/client/client.rs b/werewolves/src/clients/client/client.rs index 32244c6..a51c5fd 100644 --- a/werewolves/src/clients/client/client.rs +++ b/werewolves/src/clients/client/client.rs @@ -1,5 +1,5 @@ use core::{num::NonZeroU8, sync::atomic::AtomicBool, time::Duration}; -use std::collections::VecDeque; +use std::{collections::VecDeque, rc::Rc}; use futures::{ SinkExt, StreamExt, @@ -21,12 +21,13 @@ use werewolves_proto::{ player::PlayerId, role::RoleTitle, }; -use yew::{html::Scope, prelude::*}; +use yew::{html::Scope, prelude::*, suspense::use_future}; use crate::{ + clients::client::connection::{Connection2, ConnectionError}, components::{ Button, Identity, Notification, - client::{ClientNav, InputName}, + client::{ClientNav, Signin}, }, storage::StorageKey, }; @@ -184,7 +185,7 @@ impl Connection { } } -#[derive(PartialEq)] +#[derive(PartialEq, Debug, Clone)] pub enum ClientEvent { Disconnected, Waiting, @@ -219,6 +220,168 @@ impl TryFrom for ClientEvent { } } +#[derive(PartialEq, Debug, Clone)] +pub enum ClientEvent2 { + Disconnected, + Connecting, + Waiting, + ShowRole(RoleTitle), + + Lobby { + joined: bool, + players: Rc<[PublicIdentity]>, + }, +} + +#[derive(Default, Clone, PartialEq)] +pub struct ClientContext { + pub error_cb: Callback>, + pub forced_identity: Option, +} + +#[function_component] +pub fn Client2(ClientProps { auto_join }: &ClientProps) -> Html { + let client_state = use_state(|| ClientEvent2::Connecting); + let ClientContext { + error_cb, + forced_identity, + } = use_context::().unwrap_or_default(); + let force = use_force_update(); + + let ident = if let Some(Identification { player_id, public }) = forced_identity { + (player_id, public) + } else { + match PlayerId::load_from_storage() + .and_then(|pid| PublicIdentity::load_from_storage().map(|ident| (pid, ident))) + { + Ok((pid, ident)) => (pid, ident), + Err(StorageError::KeyNotFound(_)) => { + let on_signin = Callback::from(move |ident: PublicIdentity| { + PlayerId::new().save_to_storage().expect("saving player id"); + ident.save_to_storage().expect("saving ident"); + force.force_update(); + }); + return html! { + + }; + } + Err(err) => { + error_cb.emit(Some(err.into())); + PlayerId::delete(); + PublicIdentity::delete(); + force.force_update(); + return html! {}; + } + } + }; + let ident = use_state(|| ident); + let (send, recv) = yew::platform::pinned::mpsc::unbounded(); + let send = use_state(|| send); + let recv = use_mut_ref(|| recv); + let connection = use_mut_ref(|| Connection2::new(client_state.setter(), ident.clone(), recv)); + + let content = match &*client_state { + ClientEvent2::Disconnected => html! {

{"disconnected"}

}, + ClientEvent2::Connecting => { + if let Err(err) = connection + .try_borrow_mut() + .map_err(|_| ConnectionError::ConnectionAlreadyActive) + .and_then(|mut conn| conn.start()) + { + error_cb.emit(Some(err.into())); + } + if *auto_join { + let _ = send.send_now(ClientMessage::Hello); + } + html! {

{"connecting..."}

} + } + ClientEvent2::Waiting => html! {

{"waiting..."}

}, + ClientEvent2::ShowRole(role_title) => { + let send = (*send).clone(); + let error_cb = error_cb.clone(); + let on_click = Callback::from(move |_| { + if let Err(err) = send.clone().send_now(ClientMessage::RoleAck) { + error_cb.emit(Some(err.into())); + } + }); + html! { +
+

{format!("Your role: {role_title}")}

+ +
+ } + } + ClientEvent2::Lobby { joined, players } => { + let player = { + html! { + + } + }; + let player_list = players + .iter() + .map(|ident| { + html! { + + } + }) + .collect::(); + + let button_send = (*send).clone(); + let err_cb = error_cb.clone(); + let button = if *joined { + let cb = move |_| { + if let Err(err) = button_send.send_now(ClientMessage::Goodbye) { + err_cb.emit(Some(err.into())); + } + }; + html! { + + } + } else { + let cb = move |_| { + if let Err(err) = button_send.send_now(ClientMessage::Hello) { + err_cb.emit(Some(err.into())); + } + }; + html! { + + } + }; + + html! { +
+ {player} +

{"there are currently "}{players.len()}{" players in the lobby"}

+ {button} +
+ {player_list} +
+
+ } + } + }; + + let nav = { + let send = (*send).clone(); + let error_cb = error_cb.clone(); + let client_nav_msg_cb = move |msg| { + if let Err(err) = send.send_now(msg) { + error_cb.emit(Some(err.into())) + } + }; + html! { + + } + }; + + html! { + <> + {nav} + {content} + + } +} + pub struct Client { player: Option, send: Sender, @@ -284,7 +447,7 @@ impl Component for Client { scope.send_message(Message::SetPublicIdentity(public)); }); return html! { - + }; } else if self.recv.is_some() { // Player info loaded, but connection isn't started diff --git a/werewolves/src/clients/client/connection.rs b/werewolves/src/clients/client/connection.rs new file mode 100644 index 0000000..746877b --- /dev/null +++ b/werewolves/src/clients/client/connection.rs @@ -0,0 +1,252 @@ +use core::num::NonZeroU8; +use core::time::Duration; +use std::cell::RefCell; +use std::rc::Rc; + +use futures::{SinkExt, StreamExt}; +use gloo::net::websocket::{self, futures::WebSocket}; +use instant::Instant; +use serde::Serialize; +use thiserror::Error; +use werewolves_proto::message::{PlayerUpdate, ServerMessage}; +use werewolves_proto::{ + message::{ClientMessage, Identification, PublicIdentity}, + player::PlayerId, +}; +use yew::{platform::pinned::mpsc::UnboundedReceiver, prelude::*}; + +use crate::clients::client::ClientEvent2; + +#[derive(Debug, Clone, PartialEq, Error)] +pub enum ConnectionError { + #[error("connection already active")] + ConnectionAlreadyActive, +} + +fn url() -> String { + format!( + "{}client", + option_env!("LOCAL") + .map(|_| crate::clients::DEBUG_URL) + .unwrap_or(crate::clients::LIVE_URL) + ) +} + +#[derive(Clone)] +pub struct Connection2 { + state: UseStateSetter, + ident: UseStateHandle<(PlayerId, PublicIdentity)>, + receiver: Rc>>, + active: Rc>, +} + +impl Connection2 { + pub fn new( + state: UseStateSetter, + ident: UseStateHandle<(PlayerId, PublicIdentity)>, + receiver: Rc>>, + ) -> Self { + Self { + state, + ident, + receiver, + active: Rc::new(RefCell::new(())), + } + } + fn identification(&self) -> Identification { + Identification { + player_id: self.ident.0.clone(), + public: self.ident.1.clone(), + } + } + async fn connect_ws() -> WebSocket { + let url = url(); + loop { + match WebSocket::open(&url) { + Ok(ws) => break ws, + Err(err) => { + log::error!("connect: {err}"); + yew::platform::time::sleep(Duration::from_secs(1)).await; + } + } + } + } + + fn encode_message(msg: &impl Serialize) -> websocket::Message { + #[cfg(feature = "json")] + { + websocket::Message::Text(serde_json::to_string(msg).expect("message serialization")) + } + #[cfg(feature = "cbor")] + { + websocket::Message::Bytes({ + let mut v = Vec::new(); + ciborium::into_writer(msg, &mut v).expect("serializing message"); + v + }) + } + } + + pub fn start(&mut self) -> Result<(), ConnectionError> { + let active = self + .active + .try_borrow_mut() + .map_err(|_| ConnectionError::ConnectionAlreadyActive)?; + core::mem::drop(active); + let mut conn = self.clone(); + yew::platform::spawn_local(async move { + let active = conn.active.clone(); + conn.active = Rc::new(RefCell::new(())); + let active_borrow = active.borrow_mut(); + conn.run().await; + core::mem::drop(active_borrow); + }); + + Ok(()) + } + + async fn run(&mut self) { + const CONNECT_WAIT: Duration = Duration::from_secs(3); + let url = url(); + let mut last_connect: Option = None; + 'outer: loop { + if let Some(last_connect) = last_connect.as_ref() { + let time_since_last = Instant::now() - *last_connect; + if time_since_last <= CONNECT_WAIT { + yew::platform::time::sleep(CONNECT_WAIT.saturating_sub(time_since_last)).await; + continue; + } + } + last_connect = Some(Instant::now()); + log::info!("connecting to {url}"); + let mut ws = Self::connect_ws().await.fuse(); + log::info!("connected to {url}"); + + log::debug!("sending self ident"); + if let Err(err) = ws.send(Self::encode_message(&self.identification())).await { + log::error!("websocket identification send: {err}"); + continue 'outer; + }; + + log::debug!("sending get state"); + if let Err(err) = ws + .send(Self::encode_message(&ClientMessage::GetState)) + .await + { + log::error!("websocket identification send: {err}"); + continue 'outer; + }; + log::debug!("beginning listening loop"); + + loop { + let mut recv = self.receiver.borrow_mut(); + let msg = futures::select! { + r = ws.next() => { + match r { + Some(Ok(msg)) => msg, + Some(Err(err)) => { + log::error!("websocket recv: {err}"); + continue 'outer; + }, + None => { + log::warn!("websocket closed"); + continue 'outer; + }, + } + } + r = recv.next() => { + match r { + Some(msg) => { + log::info!("sending message: {msg:?}"); + if let Err(err) = ws.send( + Self::encode_message(&msg) + ).await { + log::error!("websocket send error: {err}"); + continue 'outer; + } + continue; + }, + None => { + log::info!("recv channel closed"); + return; + }, + } + }, + }; + core::mem::drop(recv); + let parse = { + #[cfg(feature = "json")] + { + match msg { + websocket::Message::Text(text) => { + serde_json::from_str::(&text) + } + websocket::Message::Bytes(items) => serde_json::from_slice(&items), + } + } + #[cfg(feature = "cbor")] + { + match msg { + websocket::Message::Text(_) => { + log::error!("text messages not supported in cbor mode; discarding"); + continue; + } + websocket::Message::Bytes(bytes) => { + ciborium::from_reader::(bytes.as_slice()) + } + } + } + }; + match parse { + Ok(msg) => { + if let Some(state) = self.message_to_client_state(msg) { + self.state.set(state); + } + } + Err(err) => { + log::error!("parsing server message: {err}; ignoring.") + } + } + } + } + } + + fn message_to_client_state(&self, msg: ServerMessage) -> Option { + log::debug!("received message: {msg:?}"); + Some(match msg { + ServerMessage::Disconnect => ClientEvent2::Disconnected, + ServerMessage::LobbyInfo { + joined, + mut players, + } => { + const LAST: NonZeroU8 = NonZeroU8::new(0xFF).unwrap(); + players.sort_by(|l, r| l.number.unwrap_or(LAST).cmp(&r.number.unwrap_or(LAST))); + ClientEvent2::Lobby { + joined, + players: players.into_iter().collect(), + } + } + ServerMessage::GameStart { role } => ClientEvent2::ShowRole(role), + ServerMessage::InvalidMessageForGameState => { + log::error!("invalid message for game state"); + return None; + } + ServerMessage::NoSuchTarget => { + log::error!("no such target"); + return None; + } + ServerMessage::Update(PlayerUpdate::Number(new_num)) => { + let (pid, mut ident) = (*self.ident).clone(); + ident.number = Some(new_num); + self.ident.set((pid, ident)); + return None; + } + ServerMessage::GameOver(_) + | ServerMessage::Sleep + | ServerMessage::Reset + | ServerMessage::GameInProgress => { + return None; + } + }) + } +} diff --git a/werewolves/src/clients/mod.rs b/werewolves/src/clients/mod.rs index 194a69d..33f3fb9 100644 --- a/werewolves/src/clients/mod.rs +++ b/werewolves/src/clients/mod.rs @@ -1,5 +1,6 @@ pub mod client { mod client; + pub mod connection; pub use client::*; } pub mod host { diff --git a/werewolves/src/components/client/signin.rs b/werewolves/src/components/client/signin.rs index b80b8b8..41e13fe 100644 --- a/werewolves/src/components/client/signin.rs +++ b/werewolves/src/components/client/signin.rs @@ -7,12 +7,12 @@ use yew::prelude::*; use crate::components::Button; #[derive(Debug, PartialEq, Properties)] -pub struct InputProps { - pub callback: Callback, +pub struct SigninProps { + pub callback: Callback, } #[function_component] -pub fn InputName(props: &InputProps) -> Html { +pub fn Signin(props: &SigninProps) -> Html { let callback = props.callback.clone(); let num_value = use_state(String::new); let name_value = use_state(String::new); @@ -44,27 +44,7 @@ pub fn InputName(props: &InputProps) -> Html { number, }); }); - // let num_value = num_value.clone(); - // let on_change = move |ev: InputEvent| { - // let data = ev.data(); - // if let Some(z) = data.as_ref().and_then(|d| d.trim().parse::().ok()) - // && !(z == 0 && num_value.is_empty()) - // { - // let new_value = format!("{}{z}", num_value.as_str()); - // num_value.set(new_value); - // return; - // } else if data.is_none() - // && let Some(target) = ev.target_dyn_into::() - // { - // num_value.set(target.value()); - // return; - // } - - // if let Some(target) = ev.target_dyn_into::() { - // target.set_value(num_value.as_str()); - // } - // }; let on_change = crate::components::input_element_number_oninput(num_value); html! { } } - -#[derive(Debug, Clone, PartialEq, Properties)] -pub struct StatefulIdentityProps { - pub ident: UseStateHandle, - #[prop_or_default] - pub class: Option, -} - -#[function_component] -pub fn StatefulIdentity(props: &StatefulIdentityProps) -> Html { - html! { - - } -} diff --git a/werewolves/src/main.rs b/werewolves/src/main.rs index 88ec53e..b2fee8d 100644 --- a/werewolves/src/main.rs +++ b/werewolves/src/main.rs @@ -24,10 +24,10 @@ use werewolves_proto::{ message::{Identification, PublicIdentity}, player::PlayerId, }; -use yew::prelude::*; +use yew::{context::ContextProviderProps, prelude::*}; use crate::clients::{ - client::{Client, ClientProps, Message}, + client::{Client, Client2, ClientContext, ClientProps, Message}, host::{Host, HostEvent}, }; @@ -70,25 +70,62 @@ fn main() { clients.append_child(&dupe).unwrap(); } - let client = - yew::Renderer::::with_root_and_props(dupe, ClientProps { auto_join: true }) - .render(); - client.send_message(Message::ForceIdentity(Identification { - player_id, - public: PublicIdentity { - name: name.to_string(), - pronouns: Some(String::from("he/him")), - number: None, + let client = yew::Renderer::>::with_root_and_props( + dupe, + ContextProviderProps { + context: ClientContext { + error_cb: error_callback.clone(), + forced_identity: Some(Identification { + player_id, + public: PublicIdentity { + name: name.to_string(), + pronouns: Some(String::from("he/him")), + number: None, + }, + }), + }, + children: html! { + + }, }, - })); - client.send_message(Message::SetErrorCallback(error_callback.clone())); + ) + .render(); + + // let client = yew::Renderer::::with_root_and_props( + // dupe, + // ClientProps { auto_join: true }, + // ) + // .render(); + + // client.send_message(Message::ForceIdentity(Identification { + // player_id, + // public: PublicIdentity { + // name: name.to_string(), + // pronouns: Some(String::from("he/him")), + // number: None, + // }, + // })); + // client.send_message(Message::SetErrorCallback(error_callback.clone())); } } else { - let client = yew::Renderer::::with_root_and_props( + let client = yew::Renderer::>::with_root_and_props( app_element, - ClientProps { auto_join: false }, + ContextProviderProps { + context: ClientContext { + error_cb: error_callback.clone(), + forced_identity: None, + }, + children: html! { + + }, + }, ) .render(); - client.send_message(Message::SetErrorCallback(error_callback)); + // let client = yew::Renderer::::with_root_and_props( + // app_element, + // ClientProps { auto_join: false }, + // ) + // .render(); + // client.send_message(Message::SetErrorCallback(error_callback)); } } diff --git a/werewolves/src/pages/error.rs b/werewolves/src/pages/error.rs index 16291d2..7389631 100644 --- a/werewolves/src/pages/error.rs +++ b/werewolves/src/pages/error.rs @@ -1,9 +1,11 @@ use gloo::storage::errors::StorageError; use thiserror::Error; -use werewolves_proto::error::GameError; +use werewolves_proto::{error::GameError, message::ClientMessage}; use yew::prelude::*; -#[derive(Debug, Clone, PartialEq, Error)] +use crate::clients::client::connection::ConnectionError; + +#[derive(Debug, Error)] pub enum WerewolfError { #[error("{0}")] GameError(#[from] GameError), @@ -13,6 +15,10 @@ pub enum WerewolfError { InvalidTarget, #[error("send error: {0}")] SendError(#[from] futures::channel::mpsc::SendError), + #[error("send error: {0}")] + ClientSendError(#[from] yew::platform::pinned::mpsc::SendError), + #[error("connection error: {0}")] + ConnectionError(#[from] ConnectionError), } impl From for WerewolfError {