fix client connections getting stuck idle
This commit is contained in:
parent
ee1d3c8b8e
commit
15a6454ae2
|
|
@ -2471,6 +2471,7 @@ dependencies = [
|
|||
"atom_syndication",
|
||||
"axum",
|
||||
"axum-extra",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"ciborium",
|
||||
"colored",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ ciborium = { version = "0.2", optional = true }
|
|||
colored = { version = "3.0" }
|
||||
fast_qr = { version = "0.13", features = ["svg"] }
|
||||
ron = "0.8"
|
||||
bytes = { version = "1.10" }
|
||||
|
||||
|
||||
[features]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use core::net::SocketAddr;
|
||||
use core::{net::SocketAddr, time::Duration};
|
||||
|
||||
use crate::{
|
||||
AppState, XForwardedFor,
|
||||
AppState, LogError, XForwardedFor,
|
||||
connection::{ConnectionId, JoinedPlayer},
|
||||
runner::IdentifiedClientMessage,
|
||||
};
|
||||
|
|
@ -13,6 +13,7 @@ use axum::{
|
|||
response::IntoResponse,
|
||||
};
|
||||
use axum_extra::{TypedHeader, headers};
|
||||
use chrono::Utc;
|
||||
use colored::Colorize;
|
||||
use tokio::sync::broadcast::{Receiver, Sender};
|
||||
use werewolves_proto::message::{ClientMessage, Identification, ServerMessage, UpdateSelf};
|
||||
|
|
@ -174,7 +175,25 @@ impl Client {
|
|||
self.socket.send(Message::Pong(ping)).await.log_debug();
|
||||
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)) => {
|
||||
// log::debug!("sent close frame: {close_frame:?}");
|
||||
return Ok(());
|
||||
|
|
@ -221,8 +240,19 @@ impl Client {
|
|||
}
|
||||
|
||||
async fn run(mut self) {
|
||||
const PING_TIME: Duration = Duration::from_secs(3);
|
||||
loop {
|
||||
let sleep_fut = tokio::time::sleep(PING_TIME);
|
||||
|
||||
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() => {
|
||||
match msg {
|
||||
Some(msg) => self.on_recv(msg).await,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
let players: tokio::sync::MutexGuard<'_, HashMap<PlayerId, JoinedPlayer>> =
|
||||
self.players.lock().await;
|
||||
|
|
|
|||
|
|
@ -68,13 +68,19 @@ impl GameRunner {
|
|||
}
|
||||
|
||||
pub async fn role_reveal(&mut self) {
|
||||
for char in self.game.village().characters() {
|
||||
if let Err(err) = self.player_sender.send_if_present(
|
||||
let characters = self.game.village().characters();
|
||||
for char in characters.iter() {
|
||||
match self.player_sender.send_if_present(
|
||||
char.player_id(),
|
||||
ServerMessage::GameStart {
|
||||
role: char.initial_shown_role(),
|
||||
},
|
||||
) {
|
||||
Ok(true) => {}
|
||||
Ok(false) => {
|
||||
log::warn!("couldn't notify player {}", char.identity().into_public());
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!(
|
||||
"failed sending role info to [{}]({}): {err}",
|
||||
char.player_id(),
|
||||
|
|
@ -82,6 +88,12 @@ impl GameRunner {
|
|||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
self.joined_players
|
||||
.send_to_all_filter(ServerMessage::GameInProgress, |pid| {
|
||||
!characters.iter().any(|c| c.player_id() == pid)
|
||||
})
|
||||
.await;
|
||||
let mut acks = self
|
||||
.game
|
||||
.village()
|
||||
|
|
@ -153,10 +165,7 @@ impl GameRunner {
|
|||
},
|
||||
message: ClientMessage::GetState,
|
||||
}) => {
|
||||
let sender =
|
||||
if let Some(sender) = self.joined_players.get_sender(player_id).await {
|
||||
sender
|
||||
} else {
|
||||
let Some(sender) = self.joined_players.get_sender(player_id).await else {
|
||||
continue;
|
||||
};
|
||||
if acks.iter().any(|(c, d)| c.player_id() == player_id && *d) {
|
||||
|
|
@ -176,9 +185,11 @@ impl GameRunner {
|
|||
role: char.initial_shown_role(),
|
||||
})
|
||||
.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();
|
||||
}
|
||||
log::info!("player {player_id} end");
|
||||
}
|
||||
Message::Client(IdentifiedClientMessage {
|
||||
identity:
|
||||
|
|
@ -235,6 +246,7 @@ impl GameRunner {
|
|||
identity: Identification { player_id, .. },
|
||||
..
|
||||
})) => {
|
||||
log::info!("client message from player {player_id}");
|
||||
if let Some(send) = self.joined_players.get_sender(player_id).await {
|
||||
send.send(ServerMessage::GameInProgress).log_debug();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -389,14 +389,14 @@ impl LobbyPlayers {
|
|||
&self,
|
||||
player_id: PlayerId,
|
||||
message: ServerMessage,
|
||||
) -> Result<(), GameError> {
|
||||
) -> Result<bool, GameError> {
|
||||
if let Some(sender) = self.find(player_id) {
|
||||
sender
|
||||
.send(message)
|
||||
.map(|_| ())
|
||||
.map_err(|err| GameError::GenericError(err.to_string()))
|
||||
.map_err(|err| GameError::GenericError(err.to_string()))?;
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(())
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 KiB |
|
|
@ -4,8 +4,8 @@
|
|||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>website</title>
|
||||
<link rel="icon" href="/img/puffidle.webp" />
|
||||
<title>werewolves</title>
|
||||
<link rel="icon" href="/img/wolf.svg" />
|
||||
<link data-trunk rel="sass" href="index.scss" />
|
||||
<link data-trunk rel="copy-dir" href="img">
|
||||
<link data-trunk rel="copy-dir" href="assets">
|
||||
|
|
@ -13,14 +13,6 @@
|
|||
|
||||
<body>
|
||||
<app></app>
|
||||
<clients>
|
||||
<!-- <dupe1></dupe1>
|
||||
<dupe2></dupe2>
|
||||
<dupe3></dupe3>
|
||||
<dupe4></dupe4>
|
||||
<dupe5></dupe5>
|
||||
<dupe6></dupe6> -->
|
||||
</clients>
|
||||
<error></error>
|
||||
</body>
|
||||
|
||||
|
|
|
|||
|
|
@ -73,14 +73,25 @@ body {
|
|||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
font-size: 1.5rem;
|
||||
max-width: 100vw;
|
||||
min-width: 100vw;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
font-size: 1.5vh;
|
||||
user-select: none;
|
||||
color: rgba(255, 255, 255, 1);
|
||||
background: black;
|
||||
}
|
||||
|
||||
app {
|
||||
max-width: 100vw;
|
||||
width: 100vw;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: block;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.big-screen {
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
|
|
@ -747,17 +758,23 @@ clients {
|
|||
|
||||
.cover-of-darkness {
|
||||
background-color: #000;
|
||||
// background-color: purple;
|
||||
color: #fff;
|
||||
font-size: 3rem;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
text-wrap: wrap;
|
||||
|
||||
p {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
& button {
|
||||
width: fit-content;
|
||||
|
|
@ -774,13 +791,18 @@ clients {
|
|||
// position: absolute;
|
||||
// left: 0;
|
||||
// top: 0;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
padding: 10px;
|
||||
// background-color: rgba(255, 107, 255, 0.2);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: baseline;
|
||||
gap: 10px;
|
||||
|
||||
& button {
|
||||
text-wrap: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.ident {
|
||||
|
|
@ -851,6 +873,8 @@ error {
|
|||
p {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
text-wrap: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -909,9 +933,12 @@ input {
|
|||
}
|
||||
|
||||
.game-start-role {
|
||||
@extend .column-list;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
&>button {
|
||||
font-size: 1.5rem;
|
||||
|
|
@ -1501,16 +1528,15 @@ input {
|
|||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
|
||||
&>span {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.night {
|
||||
&>label {
|
||||
margin-left: 10vw;
|
||||
font-size: 2rem;
|
||||
font-weight: lighter;
|
||||
}
|
||||
|
||||
ul.changes,
|
||||
ul.choices {
|
||||
|
|
@ -1533,7 +1559,7 @@ input {
|
|||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
|
@ -1596,10 +1622,7 @@ input {
|
|||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
align-items: baseline;
|
||||
align-content: baseline;
|
||||
justify-content: baseline;
|
||||
justify-items: baseline;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
|
||||
.number {
|
||||
|
|
@ -1610,7 +1633,8 @@ input {
|
|||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
align-items: baseline;
|
||||
align-items: center;
|
||||
font-size: 1.5vh;
|
||||
|
||||
img {
|
||||
vertical-align: sub;
|
||||
|
|
@ -1790,3 +1814,27 @@ input {
|
|||
padding: 20px;
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
use core::sync::atomic::{AtomicBool, AtomicI64, AtomicPtr, AtomicU64, AtomicUsize, Ordering};
|
||||
use std::rc::Rc;
|
||||
|
||||
use chrono::{DateTime, TimeDelta, Utc};
|
||||
use gloo::storage::errors::StorageError;
|
||||
use wasm_bindgen::{JsCast, prelude::Closure};
|
||||
use werewolves_proto::{
|
||||
game::story::GameStory,
|
||||
message::{ClientMessage, Identification, PublicIdentity},
|
||||
|
|
@ -31,6 +34,7 @@ pub enum ClientEvent2 {
|
|||
players: Rc<[PublicIdentity]>,
|
||||
},
|
||||
Story(GameStory),
|
||||
GameInProgress,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, PartialEq)]
|
||||
|
|
@ -39,8 +43,41 @@ pub struct ClientContext {
|
|||
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]
|
||||
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 ClientContext {
|
||||
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 content = match &*client_state {
|
||||
ClientEvent2::GameInProgress => html! {
|
||||
<CoverOfDarkness message={"game in progress".to_string()}/>
|
||||
},
|
||||
ClientEvent2::Story(story) => html! {
|
||||
<div class="post-game">
|
||||
<Story story={story.clone()} />
|
||||
|
|
@ -158,11 +198,21 @@ pub fn Client2(ClientProps { auto_join }: &ClientProps) -> Html {
|
|||
<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! {
|
||||
<div class="client-lobby-player-list">
|
||||
{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}
|
||||
<div class="list-actual">
|
||||
{player_list}
|
||||
|
|
@ -181,7 +231,7 @@ pub fn Client2(ClientProps { auto_join }: &ClientProps) -> Html {
|
|||
}
|
||||
};
|
||||
html! {
|
||||
<ClientNav message_callback={client_nav_msg_cb} />
|
||||
<ClientNav identity={ident.clone()} message_callback={client_nav_msg_cb} />
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ use core::time::Duration;
|
|||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use chrono::TimeDelta;
|
||||
use futures::{FutureExt, SinkExt, StreamExt};
|
||||
use gloo::net::websocket::{self, futures::WebSocket};
|
||||
use instant::Instant;
|
||||
use serde::Serialize;
|
||||
|
|
@ -112,10 +113,19 @@ impl Connection2 {
|
|||
let url = url();
|
||||
let mut last_connect: Option<Instant> = None;
|
||||
'outer: loop {
|
||||
if let Some(last_connect) = last_connect.as_ref() {
|
||||
let time_since_last = Instant::now() - *last_connect;
|
||||
if let Some(last_conn) = last_connect.as_ref() {
|
||||
let time_since_last = Instant::now() - *last_conn;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -141,9 +151,22 @@ impl Connection2 {
|
|||
log::debug!("beginning listening loop");
|
||||
|
||||
let mut quit = false;
|
||||
const GET_CONNECTION_STATE_INTERVAL: Duration = Duration::from_secs(1);
|
||||
while !quit {
|
||||
let mut recv = self.receiver.borrow_mut();
|
||||
|
||||
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() => {
|
||||
match r {
|
||||
Some(Ok(msg)) => msg,
|
||||
|
|
@ -247,7 +270,8 @@ impl Connection2 {
|
|||
self.ident.set((pid, ident));
|
||||
return None;
|
||||
}
|
||||
ServerMessage::GameOver(_) | ServerMessage::Reset | ServerMessage::GameInProgress => {
|
||||
ServerMessage::GameInProgress => ClientEvent2::GameInProgress,
|
||||
ServerMessage::GameOver(_) | ServerMessage::Reset => {
|
||||
log::info!("ignoring: {msg:?}");
|
||||
return None;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,16 +13,19 @@ use crate::{
|
|||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct ClientNavProps {
|
||||
pub identity: UseStateHandle<(PlayerId, PublicIdentity)>,
|
||||
pub message_callback: Callback<ClientMessage>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn ClientNav(ClientNavProps { message_callback }: &ClientNavProps) -> Html {
|
||||
let ident = use_state(|| {
|
||||
PublicIdentity::load_from_storage().expect("don't call client nav without an identity")
|
||||
});
|
||||
|
||||
let pronouns = ident
|
||||
pub fn ClientNav(
|
||||
ClientNavProps {
|
||||
identity,
|
||||
message_callback,
|
||||
}: &ClientNavProps,
|
||||
) -> Html {
|
||||
let pronouns = identity
|
||||
.1
|
||||
.pronouns
|
||||
.as_ref()
|
||||
.map(|pronouns| {
|
||||
|
|
@ -40,8 +43,9 @@ pub fn ClientNav(ClientNavProps { message_callback }: &ClientNavProps) -> Html {
|
|||
let current_value = use_state(String::new);
|
||||
let message_callback = message_callback.clone();
|
||||
|
||||
let submit_ident = ident.clone();
|
||||
let current_num = ident
|
||||
let submit_ident = identity.clone();
|
||||
let current_num = identity
|
||||
.1
|
||||
.number
|
||||
.map(|v| v.to_string())
|
||||
.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)));
|
||||
let new_ident = PublicIdentity {
|
||||
name: submit_ident.name.clone(),
|
||||
pronouns: submit_ident.pronouns.clone(),
|
||||
name: submit_ident.1.name.clone(),
|
||||
pronouns: submit_ident.1.pronouns.clone(),
|
||||
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() {
|
||||
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 name = use_state(String::new);
|
||||
let on_submit = {
|
||||
let ident = ident.clone();
|
||||
let ident = identity.clone();
|
||||
let message_callback = message_callback.clone();
|
||||
Callback::from(move |value: String| -> Option<PublicIdentity> {
|
||||
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())));
|
||||
PublicIdentity {
|
||||
name,
|
||||
number: ident.number,
|
||||
pronouns: ident.pronouns.clone(),
|
||||
number: ident.1.number,
|
||||
pronouns: ident.1.pronouns.clone(),
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
@ -100,12 +104,12 @@ pub fn ClientNav(ClientNavProps { message_callback }: &ClientNavProps) -> Html {
|
|||
html! {
|
||||
<ClickableTextEdit
|
||||
value={name.clone()}
|
||||
submit_ident={ident.clone()}
|
||||
submit_ident={identity.clone()}
|
||||
field_name="pronouns"
|
||||
on_submit={on_submit}
|
||||
state={open}
|
||||
>
|
||||
<div class="name">{ident.name.as_str()}</div>
|
||||
<div class="name">{identity.1.name.as_str()}</div>
|
||||
</ClickableTextEdit>
|
||||
}
|
||||
};
|
||||
|
|
@ -113,7 +117,7 @@ pub fn ClientNav(ClientNavProps { message_callback }: &ClientNavProps) -> Html {
|
|||
let pronouns_state = use_state(String::new);
|
||||
|
||||
let on_submit = {
|
||||
let ident = ident.clone();
|
||||
let ident = identity.clone();
|
||||
let message_callback = message_callback.clone();
|
||||
Callback::from(move |value: String| -> Option<PublicIdentity> {
|
||||
let pronouns = value.trim().is_empty().not().then_some(value);
|
||||
|
|
@ -122,8 +126,8 @@ pub fn ClientNav(ClientNavProps { message_callback }: &ClientNavProps) -> Html {
|
|||
)));
|
||||
Some(PublicIdentity {
|
||||
pronouns,
|
||||
name: ident.name.clone(),
|
||||
number: ident.number,
|
||||
name: ident.1.name.clone(),
|
||||
number: ident.1.number,
|
||||
})
|
||||
})
|
||||
};
|
||||
|
|
@ -131,7 +135,7 @@ pub fn ClientNav(ClientNavProps { message_callback }: &ClientNavProps) -> Html {
|
|||
html! {
|
||||
<ClickableTextEdit
|
||||
value={pronouns_state}
|
||||
submit_ident={ident.clone()}
|
||||
submit_ident={identity.clone()}
|
||||
field_name="pronouns"
|
||||
on_submit={on_submit}
|
||||
state={open}
|
||||
|
|
@ -175,7 +179,7 @@ struct ClickableTextEditProps {
|
|||
#[prop_or_default]
|
||||
pub children: Html,
|
||||
pub value: UseStateHandle<String>,
|
||||
pub submit_ident: UseStateHandle<PublicIdentity>,
|
||||
pub submit_ident: UseStateHandle<(PlayerId, PublicIdentity)>,
|
||||
pub on_submit: Callback<String, Option<PublicIdentity>>,
|
||||
pub field_name: &'static str,
|
||||
pub state: UseStateHandle<bool>,
|
||||
|
|
@ -200,14 +204,17 @@ fn ClickableTextEdit(
|
|||
let message_callback = on_submit.clone();
|
||||
let value = value.clone();
|
||||
let open_set = state.setter();
|
||||
let submit = move |_| {
|
||||
let submit = {
|
||||
let submit_ident = submit_ident.clone();
|
||||
move |_| {
|
||||
if let Some(new_ident) = message_callback.emit(value.trim().to_string()) {
|
||||
submit_ident.set(new_ident.clone());
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
let options = html! {
|
||||
<div class="row-list info-update">
|
||||
|
|
|
|||
|
|
@ -21,14 +21,8 @@ mod pages {
|
|||
}
|
||||
mod callback;
|
||||
|
||||
use core::num::NonZeroU8;
|
||||
|
||||
use pages::{ErrorComponent, WerewolfError};
|
||||
use web_sys::Url;
|
||||
use werewolves_proto::{
|
||||
message::{Identification, PublicIdentity},
|
||||
player::PlayerId,
|
||||
};
|
||||
use yew::{context::ContextProviderProps, prelude::*};
|
||||
|
||||
use crate::clients::{
|
||||
|
|
@ -50,12 +44,6 @@ fn main() {
|
|||
let error_callback =
|
||||
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") {
|
||||
let host = yew::Renderer::<Host>::with_root(app_element).render();
|
||||
if path.starts_with("/host/big") {
|
||||
|
|
@ -63,45 +51,6 @@ fn main() {
|
|||
} else {
|
||||
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 {
|
||||
yew::Renderer::<ContextProvider<ClientContext>>::with_root_and_props(
|
||||
app_element,
|
||||
|
|
|
|||
Loading…
Reference in New Issue