fix client connections getting stuck idle

This commit is contained in:
emilis 2025-11-04 18:23:15 +00:00
parent ee1d3c8b8e
commit 15a6454ae2
No known key found for this signature in database
13 changed files with 266 additions and 134 deletions

1
Cargo.lock generated
View File

@ -2471,6 +2471,7 @@ dependencies = [
"atom_syndication", "atom_syndication",
"axum", "axum",
"axum-extra", "axum-extra",
"bytes",
"chrono", "chrono",
"ciborium", "ciborium",
"colored", "colored",

View File

@ -25,6 +25,7 @@ ciborium = { version = "0.2", optional = true }
colored = { version = "3.0" } colored = { version = "3.0" }
fast_qr = { version = "0.13", features = ["svg"] } fast_qr = { version = "0.13", features = ["svg"] }
ron = "0.8" ron = "0.8"
bytes = { version = "1.10" }
[features] [features]

View File

@ -1,7 +1,7 @@
use core::net::SocketAddr; use core::{net::SocketAddr, time::Duration};
use crate::{ use crate::{
AppState, XForwardedFor, AppState, LogError, XForwardedFor,
connection::{ConnectionId, JoinedPlayer}, connection::{ConnectionId, JoinedPlayer},
runner::IdentifiedClientMessage, runner::IdentifiedClientMessage,
}; };
@ -13,6 +13,7 @@ use axum::{
response::IntoResponse, response::IntoResponse,
}; };
use axum_extra::{TypedHeader, headers}; use axum_extra::{TypedHeader, headers};
use chrono::Utc;
use colored::Colorize; use colored::Colorize;
use tokio::sync::broadcast::{Receiver, Sender}; use tokio::sync::broadcast::{Receiver, Sender};
use werewolves_proto::message::{ClientMessage, Identification, ServerMessage, UpdateSelf}; use werewolves_proto::message::{ClientMessage, Identification, ServerMessage, UpdateSelf};
@ -174,7 +175,25 @@ impl Client {
self.socket.send(Message::Pong(ping)).await.log_debug(); self.socket.send(Message::Pong(ping)).await.log_debug();
return Ok(()); return Ok(());
} }
Message::Pong(_) => return Ok(()), Message::Pong(pong) => {
const TOO_SLOW_SECONDS: i64 = 30;
let mut pong_ts = [0u8; core::mem::size_of::<u64>()];
pong.iter()
.take(pong_ts.len())
.enumerate()
.for_each(|(idx, b)| pong_ts[idx] = *b);
let ts = i64::from_le_bytes(pong_ts);
let now = Utc::now().timestamp();
let rtt_seconds = now - ts;
if rtt_seconds > TOO_SLOW_SECONDS {
self.socket.send(Message::Close(None)).await.log_debug();
return Err(anyhow::anyhow!(
"rtt (seconds): {rtt_seconds} > {TOO_SLOW_SECONDS}"
));
}
return Ok(());
}
Message::Close(Some(close_frame)) => { Message::Close(Some(close_frame)) => {
// log::debug!("sent close frame: {close_frame:?}"); // log::debug!("sent close frame: {close_frame:?}");
return Ok(()); return Ok(());
@ -221,8 +240,19 @@ impl Client {
} }
async fn run(mut self) { async fn run(mut self) {
const PING_TIME: Duration = Duration::from_secs(3);
loop { loop {
let sleep_fut = tokio::time::sleep(PING_TIME);
if let Err(err) = tokio::select! { if let Err(err) = tokio::select! {
_ = sleep_fut => {
let _ = self.socket
.send(Message::Ping(bytes::Bytes::from_owner(
Utc::now().timestamp().to_le_bytes(),
)))
.await;
continue;
}
msg = self.socket.recv() => { msg = self.socket.recv() => {
match msg { match msg {
Some(msg) => self.on_recv(msg).await, Some(msg) => self.on_recv(msg).await,

View File

@ -77,6 +77,24 @@ impl JoinedPlayers {
} }
} }
pub async fn send_to_all_filter(
&self,
message: ServerMessage,
filter: impl Fn(PlayerId) -> bool,
) {
let players: tokio::sync::MutexGuard<'_, HashMap<PlayerId, JoinedPlayer>> =
self.players.lock().await;
let senders = players
.iter()
.filter(|(pid, _)| filter(**pid))
.map(|(pid, p)| (*pid, p.sender.clone()))
.collect::<Box<[_]>>();
core::mem::drop(players);
for (_, send) in senders {
send.send(message.clone()).log_debug();
}
}
pub async fn send_to(&self, player_ids: &[PlayerId], message: ServerMessage) { pub async fn send_to(&self, player_ids: &[PlayerId], message: ServerMessage) {
let players: tokio::sync::MutexGuard<'_, HashMap<PlayerId, JoinedPlayer>> = let players: tokio::sync::MutexGuard<'_, HashMap<PlayerId, JoinedPlayer>> =
self.players.lock().await; self.players.lock().await;

View File

@ -68,20 +68,32 @@ impl GameRunner {
} }
pub async fn role_reveal(&mut self) { pub async fn role_reveal(&mut self) {
for char in self.game.village().characters() { let characters = self.game.village().characters();
if let Err(err) = self.player_sender.send_if_present( for char in characters.iter() {
match self.player_sender.send_if_present(
char.player_id(), char.player_id(),
ServerMessage::GameStart { ServerMessage::GameStart {
role: char.initial_shown_role(), role: char.initial_shown_role(),
}, },
) { ) {
log::warn!( Ok(true) => {}
"failed sending role info to [{}]({}): {err}", Ok(false) => {
char.player_id(), log::warn!("couldn't notify player {}", char.identity().into_public());
char.name() }
) Err(err) => {
log::warn!(
"failed sending role info to [{}]({}): {err}",
char.player_id(),
char.name()
)
}
} }
} }
self.joined_players
.send_to_all_filter(ServerMessage::GameInProgress, |pid| {
!characters.iter().any(|c| c.player_id() == pid)
})
.await;
let mut acks = self let mut acks = self
.game .game
.village() .village()
@ -153,12 +165,9 @@ impl GameRunner {
}, },
message: ClientMessage::GetState, message: ClientMessage::GetState,
}) => { }) => {
let sender = let Some(sender) = self.joined_players.get_sender(player_id).await else {
if let Some(sender) = self.joined_players.get_sender(player_id).await { continue;
sender };
} else {
continue;
};
if acks.iter().any(|(c, d)| c.player_id() == player_id && *d) { if acks.iter().any(|(c, d)| c.player_id() == player_id && *d) {
// already ack'd just sleep // already ack'd just sleep
sender.send(ServerMessage::Sleep).log_debug(); sender.send(ServerMessage::Sleep).log_debug();
@ -176,9 +185,11 @@ impl GameRunner {
role: char.initial_shown_role(), role: char.initial_shown_role(),
}) })
.log_debug(); .log_debug();
} else if let Some(sender) = self.joined_players.get_sender(player_id).await { } else {
log::info!("game in progress for {player_id}");
sender.send(ServerMessage::GameInProgress).log_debug(); sender.send(ServerMessage::GameInProgress).log_debug();
} }
log::info!("player {player_id} end");
} }
Message::Client(IdentifiedClientMessage { Message::Client(IdentifiedClientMessage {
identity: identity:
@ -235,6 +246,7 @@ impl GameRunner {
identity: Identification { player_id, .. }, identity: Identification { player_id, .. },
.. ..
})) => { })) => {
log::info!("client message from player {player_id}");
if let Some(send) = self.joined_players.get_sender(player_id).await { if let Some(send) = self.joined_players.get_sender(player_id).await {
send.send(ServerMessage::GameInProgress).log_debug(); send.send(ServerMessage::GameInProgress).log_debug();
} }

View File

@ -389,14 +389,14 @@ impl LobbyPlayers {
&self, &self,
player_id: PlayerId, player_id: PlayerId,
message: ServerMessage, message: ServerMessage,
) -> Result<(), GameError> { ) -> Result<bool, GameError> {
if let Some(sender) = self.find(player_id) { if let Some(sender) = self.find(player_id) {
sender sender
.send(message) .send(message)
.map(|_| ()) .map_err(|err| GameError::GenericError(err.to_string()))?;
.map_err(|err| GameError::GenericError(err.to_string())) Ok(true)
} else { } else {
Ok(()) Ok(false)
} }
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -4,8 +4,8 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>website</title> <title>werewolves</title>
<link rel="icon" href="/img/puffidle.webp" /> <link rel="icon" href="/img/wolf.svg" />
<link data-trunk rel="sass" href="index.scss" /> <link data-trunk rel="sass" href="index.scss" />
<link data-trunk rel="copy-dir" href="img"> <link data-trunk rel="copy-dir" href="img">
<link data-trunk rel="copy-dir" href="assets"> <link data-trunk rel="copy-dir" href="assets">
@ -13,14 +13,6 @@
<body> <body>
<app></app> <app></app>
<clients>
<!-- <dupe1></dupe1>
<dupe2></dupe2>
<dupe3></dupe3>
<dupe4></dupe4>
<dupe5></dupe5>
<dupe6></dupe6> -->
</clients>
<error></error> <error></error>
</body> </body>

View File

@ -73,14 +73,25 @@ body {
body { body {
min-height: 100vh; min-height: 100vh;
font-size: 1.5rem;
max-width: 100vw; max-width: 100vw;
min-width: 100vw; top: 0;
left: 0;
font-size: 1.5vh;
user-select: none; user-select: none;
color: rgba(255, 255, 255, 1); color: rgba(255, 255, 255, 1);
background: black; background: black;
} }
app {
max-width: 100vw;
width: 100vw;
top: 0;
left: 0;
display: block;
position: absolute;
}
.big-screen { .big-screen {
align-content: center; align-content: center;
align-items: center; align-items: center;
@ -747,17 +758,23 @@ clients {
.cover-of-darkness { .cover-of-darkness {
background-color: #000; background-color: #000;
// background-color: purple;
color: #fff; color: #fff;
font-size: 3rem; font-size: 3rem;
position: fixed; position: fixed;
bottom: 0; top: 0;
right: 0; left: 0;
height: 100vh; height: 100vh;
width: 100vw; width: 100vw;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
text-align: center; text-align: center;
text-wrap: wrap;
p {
padding: 30px;
}
& button { & button {
width: fit-content; width: fit-content;
@ -774,13 +791,18 @@ clients {
// position: absolute; // position: absolute;
// left: 0; // left: 0;
// top: 0; // top: 0;
width: 100%; max-width: 100%;
padding: 10px; padding: 10px;
// background-color: rgba(255, 107, 255, 0.2); // background-color: rgba(255, 107, 255, 0.2);
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: baseline; justify-content: baseline;
gap: 10px; gap: 10px;
& button {
text-wrap: nowrap;
overflow: hidden;
}
} }
.ident { .ident {
@ -851,6 +873,8 @@ error {
p { p {
margin: 0px; margin: 0px;
padding: 0px; padding: 0px;
text-wrap: nowrap;
overflow: hidden;
} }
} }
@ -909,9 +933,12 @@ input {
} }
.game-start-role { .game-start-role {
@extend .column-list; display: flex;
flex-direction: column;
flex-wrap: nowrap;
text-align: center; text-align: center;
align-items: center; align-items: center;
width: 100%;
&>button { &>button {
font-size: 1.5rem; font-size: 1.5rem;
@ -1501,16 +1528,15 @@ input {
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
gap: 10px; gap: 10px;
&>span {
display: flex;
flex-direction: row;
align-items: baseline;
}
} }
} }
.night { .night {
&>label {
margin-left: 10vw;
font-size: 2rem;
font-weight: lighter;
}
ul.changes, ul.changes,
ul.choices { ul.choices {
@ -1533,7 +1559,7 @@ input {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
align-items: baseline; align-items: center;
gap: 10px; gap: 10px;
} }
} }
@ -1596,10 +1622,7 @@ input {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: nowrap; flex-wrap: nowrap;
align-items: baseline; align-items: center;
align-content: baseline;
justify-content: baseline;
justify-items: baseline;
gap: 5px; gap: 5px;
.number { .number {
@ -1610,7 +1633,8 @@ input {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: nowrap; flex-wrap: nowrap;
align-items: baseline; align-items: center;
font-size: 1.5vh;
img { img {
vertical-align: sub; vertical-align: sub;
@ -1790,3 +1814,27 @@ input {
padding: 20px; padding: 20px;
margin: 0px; margin: 0px;
} }
.joined {
$joined_color: rgba(0, 255, 0, 0.7);
$joined_border: color.change($joined_color, $alpha: 1);
$joined_bg: color.change($joined_color, $alpha: 0.3);
color: white;
border: 1px solid $joined_border;
background-color: $joined_bg;
padding: 5px 10px 5px 10px;
font-size: 3rem;
text-align: center;
}
.not-joined {
$joined_color: rgba(255, 0, 0, 0.7);
$joined_border: color.change($joined_color, $alpha: 1);
$joined_bg: color.change($joined_color, $alpha: 0.3);
color: white;
border: 1px solid $joined_border;
background-color: $joined_bg;
padding: 5px 10px 5px 10px;
font-size: 3rem;
text-align: center;
}

View File

@ -1,6 +1,9 @@
use core::sync::atomic::{AtomicBool, AtomicI64, AtomicPtr, AtomicU64, AtomicUsize, Ordering};
use std::rc::Rc; use std::rc::Rc;
use chrono::{DateTime, TimeDelta, Utc};
use gloo::storage::errors::StorageError; use gloo::storage::errors::StorageError;
use wasm_bindgen::{JsCast, prelude::Closure};
use werewolves_proto::{ use werewolves_proto::{
game::story::GameStory, game::story::GameStory,
message::{ClientMessage, Identification, PublicIdentity}, message::{ClientMessage, Identification, PublicIdentity},
@ -31,6 +34,7 @@ pub enum ClientEvent2 {
players: Rc<[PublicIdentity]>, players: Rc<[PublicIdentity]>,
}, },
Story(GameStory), Story(GameStory),
GameInProgress,
} }
#[derive(Default, Clone, PartialEq)] #[derive(Default, Clone, PartialEq)]
@ -39,8 +43,41 @@ pub struct ClientContext {
pub forced_identity: Option<Identification>, pub forced_identity: Option<Identification>,
} }
static LOST_FOCUS: AtomicI64 = AtomicI64::new(0);
static IS_FOCUSED: AtomicBool = AtomicBool::new(true);
pub(super) fn time_spent_unfocused() -> Option<TimeDelta> {
if !IS_FOCUSED.load(Ordering::SeqCst) {
return None;
}
let lost_focus_ts = LOST_FOCUS.swap(0, Ordering::SeqCst);
if lost_focus_ts <= 0 {
return None;
}
let lost_focus = DateTime::from_timestamp_millis(lost_focus_ts)?;
Some(Utc::now() - lost_focus)
}
#[function_component] #[function_component]
pub fn Client2(ClientProps { auto_join }: &ClientProps) -> Html { pub fn Client2(ClientProps { auto_join }: &ClientProps) -> Html {
if gloo::utils::window().onfocus().is_none() {
let on_focus = {
Closure::wrap(Box::new(move || {
IS_FOCUSED.store(true, Ordering::Relaxed);
LOST_FOCUS.store(0, Ordering::Relaxed);
}) as Box<dyn FnMut()>)
.into_js_value()
};
gloo::utils::window().set_onfocus(Some(on_focus.as_ref().unchecked_ref()));
let on_blur = {
Closure::wrap(Box::new(move || {
LOST_FOCUS.store(Utc::now().timestamp_millis(), Ordering::SeqCst);
IS_FOCUSED.store(false, Ordering::SeqCst);
}) as Box<dyn FnMut()>)
.into_js_value()
};
gloo::utils::window().set_onblur(Some(on_blur.as_ref().unchecked_ref()));
}
let client_state = use_state(|| ClientEvent2::Connecting); let client_state = use_state(|| ClientEvent2::Connecting);
let ClientContext { let ClientContext {
error_cb, error_cb,
@ -81,6 +118,9 @@ pub fn Client2(ClientProps { auto_join }: &ClientProps) -> Html {
let connection = use_mut_ref(|| Connection2::new(client_state.setter(), ident.clone(), recv)); let connection = use_mut_ref(|| Connection2::new(client_state.setter(), ident.clone(), recv));
let content = match &*client_state { let content = match &*client_state {
ClientEvent2::GameInProgress => html! {
<CoverOfDarkness message={"game in progress".to_string()}/>
},
ClientEvent2::Story(story) => html! { ClientEvent2::Story(story) => html! {
<div class="post-game"> <div class="post-game">
<Story story={story.clone()} /> <Story story={story.clone()} />
@ -158,11 +198,21 @@ pub fn Client2(ClientProps { auto_join }: &ClientProps) -> Html {
<Button on_click={cb}>{"join"}</Button> <Button on_click={cb}>{"join"}</Button>
} }
}; };
let join_status = if *joined {
html! {
<span class="joined">{"you are in the lobby"}</span>
}
} else {
html! {
<span class="not-joined">{"you have not joined"}</span>
}
};
html! { html! {
<div class="client-lobby-player-list"> <div class="client-lobby-player-list">
{player} {player}
<h2>{"there are currently "}{players.len()}{" players in the lobby"}</h2> <h4>{"there are currently "}{players.len()}{" players in the lobby"}</h4>
{join_status}
{button} {button}
<div class="list-actual"> <div class="list-actual">
{player_list} {player_list}
@ -181,7 +231,7 @@ pub fn Client2(ClientProps { auto_join }: &ClientProps) -> Html {
} }
}; };
html! { html! {
<ClientNav message_callback={client_nav_msg_cb} /> <ClientNav identity={ident.clone()} message_callback={client_nav_msg_cb} />
} }
}; };

View File

@ -3,7 +3,8 @@ use core::time::Duration;
use std::cell::RefCell; use std::cell::RefCell;
use std::rc::Rc; use std::rc::Rc;
use futures::{SinkExt, StreamExt}; use chrono::TimeDelta;
use futures::{FutureExt, SinkExt, StreamExt};
use gloo::net::websocket::{self, futures::WebSocket}; use gloo::net::websocket::{self, futures::WebSocket};
use instant::Instant; use instant::Instant;
use serde::Serialize; use serde::Serialize;
@ -112,10 +113,19 @@ impl Connection2 {
let url = url(); let url = url();
let mut last_connect: Option<Instant> = None; let mut last_connect: Option<Instant> = None;
'outer: loop { 'outer: loop {
if let Some(last_connect) = last_connect.as_ref() { if let Some(last_conn) = last_connect.as_ref() {
let time_since_last = Instant::now() - *last_connect; let time_since_last = Instant::now() - *last_conn;
if time_since_last <= CONNECT_WAIT { if time_since_last <= CONNECT_WAIT {
yew::platform::time::sleep(CONNECT_WAIT.saturating_sub(time_since_last)).await; let remaining = CONNECT_WAIT.saturating_sub(time_since_last);
if let Some(unfocused_time) = crate::clients::client::time_spent_unfocused()
&& let Ok(remaining) = TimeDelta::from_std(remaining)
&& remaining < unfocused_time
{
log::debug!("unfocused time: {unfocused_time}");
last_connect = None;
continue;
}
yew::platform::time::sleep(remaining).await;
continue; continue;
} }
} }
@ -141,9 +151,22 @@ impl Connection2 {
log::debug!("beginning listening loop"); log::debug!("beginning listening loop");
let mut quit = false; let mut quit = false;
const GET_CONNECTION_STATE_INTERVAL: Duration = Duration::from_secs(1);
while !quit { while !quit {
let mut recv = self.receiver.borrow_mut(); let mut recv = self.receiver.borrow_mut();
let msg = futures::select! { let msg = futures::select! {
_ = gloo::timers::future::sleep(GET_CONNECTION_STATE_INTERVAL).fuse() => {
if matches!(
ws.get_ref().state(),
websocket::State::Closing | websocket::State::Closed
) {
log::error!("connection closed on timeout check; reconnecting");
continue 'outer;
} else {
continue;
}
}
r = ws.next() => { r = ws.next() => {
match r { match r {
Some(Ok(msg)) => msg, Some(Ok(msg)) => msg,
@ -247,7 +270,8 @@ impl Connection2 {
self.ident.set((pid, ident)); self.ident.set((pid, ident));
return None; return None;
} }
ServerMessage::GameOver(_) | ServerMessage::Reset | ServerMessage::GameInProgress => { ServerMessage::GameInProgress => ClientEvent2::GameInProgress,
ServerMessage::GameOver(_) | ServerMessage::Reset => {
log::info!("ignoring: {msg:?}"); log::info!("ignoring: {msg:?}");
return None; return None;
} }

View File

@ -13,16 +13,19 @@ use crate::{
#[derive(Debug, Clone, PartialEq, Properties)] #[derive(Debug, Clone, PartialEq, Properties)]
pub struct ClientNavProps { pub struct ClientNavProps {
pub identity: UseStateHandle<(PlayerId, PublicIdentity)>,
pub message_callback: Callback<ClientMessage>, pub message_callback: Callback<ClientMessage>,
} }
#[function_component] #[function_component]
pub fn ClientNav(ClientNavProps { message_callback }: &ClientNavProps) -> Html { pub fn ClientNav(
let ident = use_state(|| { ClientNavProps {
PublicIdentity::load_from_storage().expect("don't call client nav without an identity") identity,
}); message_callback,
}: &ClientNavProps,
let pronouns = ident ) -> Html {
let pronouns = identity
.1
.pronouns .pronouns
.as_ref() .as_ref()
.map(|pronouns| { .map(|pronouns| {
@ -40,8 +43,9 @@ pub fn ClientNav(ClientNavProps { message_callback }: &ClientNavProps) -> Html {
let current_value = use_state(String::new); let current_value = use_state(String::new);
let message_callback = message_callback.clone(); let message_callback = message_callback.clone();
let submit_ident = ident.clone(); let submit_ident = identity.clone();
let current_num = ident let current_num = identity
.1
.number .number
.map(|v| v.to_string()) .map(|v| v.to_string())
.unwrap_or_else(|| String::from("???")); .unwrap_or_else(|| String::from("???"));
@ -56,11 +60,11 @@ pub fn ClientNav(ClientNavProps { message_callback }: &ClientNavProps) -> Html {
}; };
message_callback.emit(ClientMessage::UpdateSelf(UpdateSelf::Number(num))); message_callback.emit(ClientMessage::UpdateSelf(UpdateSelf::Number(num)));
let new_ident = PublicIdentity { let new_ident = PublicIdentity {
name: submit_ident.name.clone(), name: submit_ident.1.name.clone(),
pronouns: submit_ident.pronouns.clone(), pronouns: submit_ident.1.pronouns.clone(),
number: Some(num), number: Some(num),
}; };
submit_ident.set(new_ident.clone()); submit_ident.set((submit_ident.0, new_ident.clone()));
if let Err(err) = new_ident.save_to_storage() { if let Err(err) = new_ident.save_to_storage() {
log::error!("saving public identity after change: {err}"); log::error!("saving public identity after change: {err}");
} }
@ -82,7 +86,7 @@ pub fn ClientNav(ClientNavProps { message_callback }: &ClientNavProps) -> Html {
let open = use_state(|| false); let open = use_state(|| false);
let name = use_state(String::new); let name = use_state(String::new);
let on_submit = { let on_submit = {
let ident = ident.clone(); let ident = identity.clone();
let message_callback = message_callback.clone(); let message_callback = message_callback.clone();
Callback::from(move |value: String| -> Option<PublicIdentity> { Callback::from(move |value: String| -> Option<PublicIdentity> {
value.trim().is_empty().not().then(|| { value.trim().is_empty().not().then(|| {
@ -91,8 +95,8 @@ pub fn ClientNav(ClientNavProps { message_callback }: &ClientNavProps) -> Html {
.emit(ClientMessage::UpdateSelf(UpdateSelf::Name(name.clone()))); .emit(ClientMessage::UpdateSelf(UpdateSelf::Name(name.clone())));
PublicIdentity { PublicIdentity {
name, name,
number: ident.number, number: ident.1.number,
pronouns: ident.pronouns.clone(), pronouns: ident.1.pronouns.clone(),
} }
}) })
}) })
@ -100,12 +104,12 @@ pub fn ClientNav(ClientNavProps { message_callback }: &ClientNavProps) -> Html {
html! { html! {
<ClickableTextEdit <ClickableTextEdit
value={name.clone()} value={name.clone()}
submit_ident={ident.clone()} submit_ident={identity.clone()}
field_name="pronouns" field_name="pronouns"
on_submit={on_submit} on_submit={on_submit}
state={open} state={open}
> >
<div class="name">{ident.name.as_str()}</div> <div class="name">{identity.1.name.as_str()}</div>
</ClickableTextEdit> </ClickableTextEdit>
} }
}; };
@ -113,7 +117,7 @@ pub fn ClientNav(ClientNavProps { message_callback }: &ClientNavProps) -> Html {
let pronouns_state = use_state(String::new); let pronouns_state = use_state(String::new);
let on_submit = { let on_submit = {
let ident = ident.clone(); let ident = identity.clone();
let message_callback = message_callback.clone(); let message_callback = message_callback.clone();
Callback::from(move |value: String| -> Option<PublicIdentity> { Callback::from(move |value: String| -> Option<PublicIdentity> {
let pronouns = value.trim().is_empty().not().then_some(value); let pronouns = value.trim().is_empty().not().then_some(value);
@ -122,8 +126,8 @@ pub fn ClientNav(ClientNavProps { message_callback }: &ClientNavProps) -> Html {
))); )));
Some(PublicIdentity { Some(PublicIdentity {
pronouns, pronouns,
name: ident.name.clone(), name: ident.1.name.clone(),
number: ident.number, number: ident.1.number,
}) })
}) })
}; };
@ -131,7 +135,7 @@ pub fn ClientNav(ClientNavProps { message_callback }: &ClientNavProps) -> Html {
html! { html! {
<ClickableTextEdit <ClickableTextEdit
value={pronouns_state} value={pronouns_state}
submit_ident={ident.clone()} submit_ident={identity.clone()}
field_name="pronouns" field_name="pronouns"
on_submit={on_submit} on_submit={on_submit}
state={open} state={open}
@ -175,7 +179,7 @@ struct ClickableTextEditProps {
#[prop_or_default] #[prop_or_default]
pub children: Html, pub children: Html,
pub value: UseStateHandle<String>, pub value: UseStateHandle<String>,
pub submit_ident: UseStateHandle<PublicIdentity>, pub submit_ident: UseStateHandle<(PlayerId, PublicIdentity)>,
pub on_submit: Callback<String, Option<PublicIdentity>>, pub on_submit: Callback<String, Option<PublicIdentity>>,
pub field_name: &'static str, pub field_name: &'static str,
pub state: UseStateHandle<bool>, pub state: UseStateHandle<bool>,
@ -200,13 +204,16 @@ fn ClickableTextEdit(
let message_callback = on_submit.clone(); let message_callback = on_submit.clone();
let value = value.clone(); let value = value.clone();
let open_set = state.setter(); let open_set = state.setter();
let submit = move |_| { let submit = {
if let Some(new_ident) = message_callback.emit(value.trim().to_string()) { let submit_ident = submit_ident.clone();
submit_ident.set(new_ident.clone()); move |_| {
if let Err(err) = new_ident.save_to_storage() { if let Some(new_ident) = message_callback.emit(value.trim().to_string()) {
log::error!("saving public identity after change: {err}"); submit_ident.set((submit_ident.0, new_ident.clone()));
if let Err(err) = new_ident.save_to_storage() {
log::error!("saving public identity after change: {err}");
}
open_set.set(false);
} }
open_set.set(false);
} }
}; };
let options = html! { let options = html! {

View File

@ -21,14 +21,8 @@ mod pages {
} }
mod callback; mod callback;
use core::num::NonZeroU8;
use pages::{ErrorComponent, WerewolfError}; use pages::{ErrorComponent, WerewolfError};
use web_sys::Url; use web_sys::Url;
use werewolves_proto::{
message::{Identification, PublicIdentity},
player::PlayerId,
};
use yew::{context::ContextProviderProps, prelude::*}; use yew::{context::ContextProviderProps, prelude::*};
use crate::clients::{ use crate::clients::{
@ -50,12 +44,6 @@ fn main() {
let error_callback = let error_callback =
Callback::from(move |err: Option<WerewolfError>| cb_clone.send_message(err)); Callback::from(move |err: Option<WerewolfError>| cb_clone.send_message(err));
gloo::utils::document().set_title("werewolves");
if path.starts_with("/host/story") {
let host = yew::Renderer::<clients::story_test::StoryTest>::with_root(app_element).render();
return;
}
if path.starts_with("/host") { if path.starts_with("/host") {
let host = yew::Renderer::<Host>::with_root(app_element).render(); let host = yew::Renderer::<Host>::with_root(app_element).render();
if path.starts_with("/host/big") { if path.starts_with("/host/big") {
@ -63,45 +51,6 @@ fn main() {
} else { } else {
host.send_message(HostEvent::SetErrorCallback(error_callback)); host.send_message(HostEvent::SetErrorCallback(error_callback));
} }
} else if path.starts_with("/many-client") {
let clients = document.query_selector("clients").unwrap().unwrap();
let client_count = option_env!("CLIENTS")
.and_then(|c| c.parse::<NonZeroU8>().ok())
.unwrap_or(NonZeroU8::new(7).unwrap())
.get();
for (player_id, name, num, dupe) in (1..=client_count).map(|num| {
(
PlayerId::from_u128(num as u128),
format!("player {num}"),
NonZeroU8::new(num).unwrap(),
document.create_element("autoclient").unwrap(),
)
}) {
if dupe.tag_name() == "AUTOCLIENT" {
clients.append_child(&dupe).unwrap();
}
yew::Renderer::<ContextProvider<ClientContext>>::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: Some(num),
},
}),
},
children: html! {
<Client2 auto_join=true/>
},
},
)
.render();
}
} else { } else {
yew::Renderer::<ContextProvider<ClientContext>>::with_root_and_props( yew::Renderer::<ContextProvider<ClientContext>>::with_root_and_props(
app_element, app_element,