warn fixes for server and proto

This commit is contained in:
emilis 2025-10-03 00:00:39 +01:00
parent 242691a05f
commit 8c2791054a
No known key found for this signature in database
24 changed files with 70 additions and 744 deletions

View File

@ -1,7 +1,7 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use thiserror::Error; use thiserror::Error;
use crate::{player::CharacterId, role::RoleTitle}; use crate::role::RoleTitle;
#[derive(Debug, Clone, PartialEq, Error, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Error, Serialize, Deserialize)]
pub enum GameError { pub enum GameError {

View File

@ -1,13 +1,10 @@
use core::{ use core::{num::NonZeroU8, ops::Not};
num::NonZeroU8,
ops::{Deref, Not},
};
use super::Result; use super::Result;
use crate::{ use crate::{
diedto::DiedTo, diedto::DiedTo,
error::GameError, error::GameError,
game::{Village, kill::taken::Taken, night::NightChange}, game::{Village, night::NightChange},
player::{CharacterId, Protection}, player::{CharacterId, Protection},
}; };
@ -26,10 +23,13 @@ pub enum KillOutcome {
impl KillOutcome { impl KillOutcome {
pub fn apply_to_village(self, village: &mut Village) -> Result<()> { pub fn apply_to_village(self, village: &mut Village) -> Result<()> {
match self { match self {
KillOutcome::Single(character_id, died_to) => Ok(village KillOutcome::Single(character_id, died_to) => {
village
.character_by_id_mut(&character_id) .character_by_id_mut(&character_id)
.ok_or(GameError::InvalidTarget)? .ok_or(GameError::InvalidTarget)?
.kill(died_to)), .kill(died_to);
Ok(())
}
KillOutcome::Guarding { KillOutcome::Guarding {
original_killer, original_killer,
original_target, original_target,
@ -146,7 +146,7 @@ pub fn resolve_kill(
None => return Ok(Some(KillOutcome::Single(target.clone(), died_to.clone()))), None => return Ok(Some(KillOutcome::Single(target.clone(), died_to.clone()))),
}; };
match protection.deref() { match protection {
Protection::Guardian { Protection::Guardian {
source, source,
guarding: true, guarding: true,
@ -188,17 +188,7 @@ impl<'a> ChangesLookup<'a> {
}) })
} }
pub fn release<T>(&mut self, taken: Taken<'a, T>) { pub fn protected_take(&mut self, target: &CharacterId) -> Option<Protection> {
self.1.swap_remove(
self.1
.iter()
.enumerate()
.find_map(|(idx, c)| (*c == taken.idx()).then_some(idx))
.unwrap(),
);
}
pub fn protected_take(&mut self, target: &CharacterId) -> Option<Taken<'a, Protection>> {
if let Some((idx, c)) = self.0.iter().enumerate().find_map(|(idx, c)| { if let Some((idx, c)) = self.0.iter().enumerate().find_map(|(idx, c)| {
self.1 self.1
.contains(&idx) .contains(&idx)
@ -213,7 +203,7 @@ impl<'a> ChangesLookup<'a> {
.flatten() .flatten()
}) { }) {
self.1.push(idx); self.1.push(idx);
Some(Taken::new(idx, c)) Some(c.clone())
} else { } else {
None None
} }
@ -267,26 +257,3 @@ impl<'a> ChangesLookup<'a> {
}) })
} }
} }
mod taken {
use core::ops::Deref;
pub struct Taken<'a, T>(usize, &'a T);
impl<'a, T> Taken<'a, T> {
pub const fn new(idx: usize, item: &'a T) -> Self {
Self(idx, item)
}
pub const fn idx(&self) -> usize {
self.0
}
}
impl<'a, T> Deref for Taken<'a, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.1
}
}
}

View File

@ -18,7 +18,6 @@ use crate::{
message::{ message::{
CharacterState, Identification, CharacterState, Identification,
host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage}, host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage},
night::ActionResponse,
}, },
player::CharacterId, player::CharacterId,
}; };
@ -193,6 +192,7 @@ impl Game {
} }
} }
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub enum GameState { pub enum GameState {
Day { Day {

View File

@ -12,7 +12,7 @@ use crate::{
DateTime, Village, DateTime, Village,
kill::{self, ChangesLookup}, kill::{self, ChangesLookup},
}, },
message::night::{ActionPrompt, ActionResponse, ActionResult, ActionType}, message::night::{ActionPrompt, ActionResponse, ActionResult},
player::{Character, CharacterId, Protection}, player::{Character, CharacterId, Protection},
role::{PreviousGuardianAction, Role, RoleBlock, RoleTitle}, role::{PreviousGuardianAction, Role, RoleBlock, RoleTitle},
}; };

View File

@ -61,7 +61,7 @@ impl Village {
.map(|(player, role)| { .map(|(player, role)| {
let player_str = player.public.to_string(); let player_str = player.public.to_string();
Character::new(player, role) Character::new(player, role)
.ok_or_else(|| GameError::PlayerNotAssignedNumber(player_str)) .ok_or(GameError::PlayerNotAssignedNumber(player_str))
}) })
.collect::<Result<Box<[_]>>>()?, .collect::<Result<Box<[_]>>>()?,
date_time: DateTime::Night { number: 0 }, date_time: DateTime::Night { number: 0 },

View File

@ -50,7 +50,6 @@ trait GameExt {
fn next(&mut self) -> ActionPrompt; fn next(&mut self) -> ActionPrompt;
fn next_expect_day(&mut self) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8); fn next_expect_day(&mut self) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8);
fn response(&mut self, resp: ActionResponse) -> ActionResult; fn response(&mut self, resp: ActionResponse) -> ActionResult;
fn get_state(&mut self) -> ServerToHostMessage;
fn execute(&mut self) -> ActionPrompt; fn execute(&mut self) -> ActionPrompt;
fn mark_for_execution( fn mark_for_execution(
&mut self, &mut self,
@ -111,10 +110,6 @@ impl GameExt for Game {
.unwrap() .unwrap()
.prompt() .prompt()
} }
fn get_state(&mut self) -> ServerToHostMessage {
self.process(HostGameMessage::GetState).unwrap()
}
} }
fn init_log() { fn init_log() {

View File

@ -1,9 +1,5 @@
#![allow(clippy::new_without_default)] #![allow(clippy::new_without_default)]
use error::GameError;
use serde::{Deserialize, Serialize};
use thiserror::Error;
// pub mod action;
pub mod diedto; pub mod diedto;
pub mod error; pub mod error;
pub mod game; pub mod game;

View File

@ -2,18 +2,12 @@ pub mod host;
mod ident; mod ident;
pub mod night; pub mod night;
use core::{fmt::Display, num::NonZeroU8}; use core::num::NonZeroU8;
pub use ident::*; pub use ident::*;
use night::{ActionPrompt, ActionResponse, ActionResult};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{game::GameOver, player::CharacterId, role::RoleTitle};
error::GameError,
game::GameOver,
player::{Character, CharacterId},
role::RoleTitle,
};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ClientMessage { pub enum ClientMessage {

View File

@ -6,7 +6,7 @@ use crate::{
error::GameError, error::GameError,
game::{GameOver, GameSettings}, game::{GameOver, GameSettings},
message::{ message::{
CharacterIdentity, PublicIdentity, CharacterIdentity,
night::{ActionPrompt, ActionResponse, ActionResult}, night::{ActionPrompt, ActionResponse, ActionResult},
}, },
player::{CharacterId, PlayerId}, player::{CharacterId, PlayerId},

View File

@ -58,7 +58,7 @@ pub async fn handler(
recv, recv,
connection_id.clone(), connection_id.clone(),
ident.public.name.clone(), ident.public.name.clone(),
ident.public.number.clone(), ident.public.number,
ident.public.pronouns.clone(), ident.public.pronouns.clone(),
), ),
) )
@ -161,11 +161,7 @@ impl Client {
} }
} }
async fn on_recv( async fn on_recv(&mut self, msg: Result<Message, axum::Error>) -> Result<(), anyhow::Error> {
&mut self,
msg: Result<Message, axum::Error>,
conn_id: ConnectionId,
) -> Result<(), anyhow::Error> {
use crate::LogError; use crate::LogError;
let msg = match msg { let msg = match msg {
@ -231,7 +227,7 @@ impl Client {
if let Err(err) = tokio::select! { if let Err(err) = tokio::select! {
msg = self.socket.recv() => { msg = self.socket.recv() => {
match msg { match msg {
Some(msg) => self.on_recv(msg, self.connection_id.clone()).await, Some(msg) => self.on_recv(msg).await,
None => { None => {
return; return;
}, },

View File

@ -22,10 +22,7 @@ impl HostComms {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
pub async fn recv(&mut self) -> Option<HostMessage> { pub async fn recv(&mut self) -> Option<HostMessage> {
match self.recv.recv().await { self.recv.recv().await
Some(msg) => Some(msg),
None => None,
}
} }
#[cfg(not(debug_assertions))] #[cfg(not(debug_assertions))]

View File

@ -22,6 +22,7 @@ impl LobbyComms {
(self.comms, self.connect_recv) (self.comms, self.connect_recv)
} }
#[allow(unused)]
pub const fn player(&mut self) -> &mut PlayerIdComms { pub const fn player(&mut self) -> &mut PlayerIdComms {
self.comms.player() self.comms.player()
} }

View File

@ -23,6 +23,7 @@ impl Comms {
&mut self.host &mut self.host
} }
#[allow(unused)]
pub const fn player(&mut self) -> &mut PlayerIdComms { pub const fn player(&mut self) -> &mut PlayerIdComms {
&mut self.player &mut self.player
} }

View File

@ -1,8 +1,8 @@
use colored::Colorize; use colored::Colorize;
use tokio::sync::broadcast::Receiver; use tokio::sync::broadcast::Receiver;
use werewolves_proto::{error::GameError, player::PlayerId}; use werewolves_proto::error::GameError;
use crate::{connection::JoinedPlayers, runner::IdentifiedClientMessage}; use crate::runner::IdentifiedClientMessage;
pub struct PlayerIdComms { pub struct PlayerIdComms {
// joined_players: JoinedPlayers, // joined_players: JoinedPlayers,

View File

@ -9,7 +9,6 @@ use tokio::{
time::Instant, time::Instant,
}; };
use werewolves_proto::{ use werewolves_proto::{
error::GameError,
message::{PublicIdentity, ServerMessage}, message::{PublicIdentity, ServerMessage},
player::PlayerId, player::PlayerId,
}; };
@ -142,30 +141,6 @@ impl JoinedPlayers {
None None
} }
pub async fn start_game_with(&self, players: &[PlayerId]) -> Result<InGameToken, GameError> {
let mut map = self.players.lock().await;
for player in players {
if let Some(player) = map.get_mut(player) {
player.in_game = true;
};
}
Ok(InGameToken::new(
self.clone(),
players.iter().cloned().collect(),
))
}
pub async fn release_from_game(&self, players: &[PlayerId]) {
self.players
.lock()
.await
.iter_mut()
.filter(|(p, _)| players.contains(*p))
.for_each(|(_, p)| p.in_game = false)
}
pub async fn get_sender(&self, player_id: &PlayerId) -> Option<Sender<ServerMessage>> { pub async fn get_sender(&self, player_id: &PlayerId) -> Option<Sender<ServerMessage>> {
self.players self.players
.lock() .lock()
@ -196,27 +171,3 @@ impl JoinedPlayers {
} }
} }
} }
pub struct InGameToken {
joined_players: JoinedPlayers,
players_in_game: Option<Box<[PlayerId]>>,
}
impl InGameToken {
const fn new(joined_players: JoinedPlayers, players_in_game: Box<[PlayerId]>) -> Self {
Self {
joined_players,
players_in_game: Some(players_in_game),
}
}
}
impl Drop for InGameToken {
fn drop(&mut self) {
let joined_players = self.joined_players.clone();
if let Some(players) = self.players_in_game.take() {
tokio::spawn(async move {
let players_in_game = players;
joined_players.release_from_game(&players_in_game).await;
});
}
}
}

View File

@ -7,7 +7,6 @@ use crate::{
lobby::{Lobby, LobbyPlayers}, lobby::{Lobby, LobbyPlayers},
runner::{IdentifiedClientMessage, Message}, runner::{IdentifiedClientMessage, Message},
}; };
use futures::SinkExt;
use tokio::{sync::broadcast::Receiver, time::Instant}; use tokio::{sync::broadcast::Receiver, time::Instant};
use werewolves_proto::{ use werewolves_proto::{
error::GameError, error::GameError,
@ -53,7 +52,6 @@ impl GameRunner {
} }
pub fn into_lobby(self) -> Lobby { pub fn into_lobby(self) -> Lobby {
// core::mem::drop(self._release_token);
let mut lobby = Lobby::new( let mut lobby = Lobby::new(
self.joined_players, self.joined_players,
LobbyComms::new(self.comms, self.connect_recv), LobbyComms::new(self.comms, self.connect_recv),
@ -214,6 +212,12 @@ impl GameRunner {
} }
} }
for char in self.game.village().characters() {
if let Some(sender) = self.joined_players.get_sender(char.player_id()).await {
let _ = sender.send(ServerMessage::Disconnect);
}
}
self.roles_revealed = true; self.roles_revealed = true;
} }

View File

@ -1,18 +1,11 @@
use core::{ use core::ops::{Deref, DerefMut};
num::NonZeroU8,
ops::{Deref, DerefMut},
time::Duration,
};
use std::{collections::HashMap, os::unix::raw::pid_t};
use serde::{Deserialize, Serialize};
use tokio::sync::broadcast::Sender; use tokio::sync::broadcast::Sender;
use werewolves_proto::{ use werewolves_proto::{
error::GameError, error::GameError,
game::{Game, GameSettings, Village}, game::{Game, GameSettings},
message::{ message::{
CharacterState, ClientMessage, DayCharacter, Identification, PlayerState, ServerMessage, ClientMessage, Identification, PlayerState, ServerMessage,
UpdateSelf,
host::{HostLobbyMessage, HostMessage, ServerToHostMessage}, host::{HostLobbyMessage, HostMessage, ServerToHostMessage},
}, },
player::PlayerId, player::PlayerId,
@ -353,10 +346,4 @@ impl LobbyPlayers {
Ok(()) Ok(())
} }
} }
pub fn drain(&mut self) -> Self {
let mut swapped = Self(vec![]);
core::mem::swap(&mut swapped, self);
swapped
}
} }

View File

@ -17,13 +17,9 @@ use axum_extra::headers;
use communication::lobby::LobbyComms; use communication::lobby::LobbyComms;
use connection::JoinedPlayers; use connection::JoinedPlayers;
use core::{fmt::Display, net::SocketAddr, str::FromStr}; use core::{fmt::Display, net::SocketAddr, str::FromStr};
use log::Record;
use runner::IdentifiedClientMessage; use runner::IdentifiedClientMessage;
use std::{env, io::Write, path::Path}; use std::{env, io::Write, path::Path};
use tokio::{ use tokio::sync::{broadcast, mpsc};
sync::{broadcast, mpsc},
time::Instant,
};
use crate::{ use crate::{
communication::{Comms, host::HostComms, player::PlayerIdComms}, communication::{Comms, host::HostComms, player::PlayerIdComms},
@ -114,7 +110,7 @@ async fn main() {
let path = Path::new(option_env!("SAVE_PATH").unwrap_or(DEFAULT_SAVE_DIR)); let path = Path::new(option_env!("SAVE_PATH").unwrap_or(DEFAULT_SAVE_DIR));
if let Err(err) = std::fs::create_dir(&path) if let Err(err) = std::fs::create_dir(path)
&& !matches!(err.kind(), std::io::ErrorKind::AlreadyExists) && !matches!(err.kind(), std::io::ErrorKind::AlreadyExists)
{ {
panic!("creating save dir at [{path:?}]: {err}") panic!("creating save dir at [{path:?}]: {err}")

View File

@ -586,7 +586,7 @@ clients {
margin-left: 5vw; margin-left: 5vw;
margin-right: 5vw; margin-right: 5vw;
margin-top: 30px; margin-top: 30px;
display: flexbox; display: flex;
flex-basis: content; flex-basis: content;
} }

View File

@ -1,32 +1,17 @@
use core::{num::NonZeroU8, sync::atomic::AtomicBool, time::Duration}; use std::rc::Rc;
use std::{collections::VecDeque, rc::Rc};
use futures::{ use gloo::storage::errors::StorageError;
SinkExt, StreamExt,
channel::mpsc::{Receiver, Sender},
};
use gloo::{
net::websocket::{self, futures::WebSocket},
storage::{LocalStorage, Storage, errors::StorageError},
};
use instant::Instant;
use serde::Serialize;
use werewolves_proto::{ use werewolves_proto::{
error::GameError, message::{ClientMessage, Identification, PublicIdentity},
game::GameOver,
message::{
ClientMessage, DayCharacter, Identification, PlayerUpdate, PublicIdentity, ServerMessage,
night::{ActionPrompt, ActionResponse, ActionResult},
},
player::PlayerId, player::PlayerId,
role::RoleTitle, role::RoleTitle,
}; };
use yew::{html::Scope, prelude::*, suspense::use_future}; use yew::prelude::*;
use crate::{ use crate::{
clients::client::connection::{Connection2, ConnectionError}, clients::client::connection::{Connection2, ConnectionError},
components::{ components::{
Button, Identity, Notification, Button, Identity,
client::{ClientNav, Signin}, client::{ClientNav, Signin},
}, },
storage::StorageKey, storage::StorageKey,
@ -34,197 +19,10 @@ use crate::{
use crate::WerewolfError; use crate::WerewolfError;
#[derive(Debug)]
pub enum Message {
SetErrorCallback(Callback<Option<WerewolfError>>),
SetPublicIdentity(PublicIdentity),
RecvServerMessage(ServerMessage),
Connect,
ForceIdentity(Identification),
}
pub struct Connection {
scope: Scope<Client>,
ident: Identification,
recv: Receiver<ClientMessage>,
}
fn url() -> String {
format!(
"{}client",
option_env!("LOCAL")
.map(|_| crate::clients::DEBUG_URL)
.unwrap_or(crate::clients::LIVE_URL)
)
}
impl Connection {
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
})
}
}
async fn run(mut self) {
const CONNECT_WAIT: Duration = Duration::from_secs(3);
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 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}");
if let Err(err) = ws.send(Self::encode_message(&self.ident)).await {
log::error!("websocket identification send: {err}");
continue 'outer;
};
if let Err(err) = ws
.send(Self::encode_message(&ClientMessage::GetState))
.await
{
log::error!("websocket identification send: {err}");
continue 'outer;
};
loop {
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 = self.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;
},
}
},
};
let parse = {
#[cfg(feature = "json")]
{
match msg {
websocket::Message::Text(text) => {
serde_json::from_str::<ServerMessage>(&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::<ServerMessage, _>(bytes.as_slice())
}
}
}
};
match parse {
Ok(msg) => self.scope.send_message(Message::RecvServerMessage(msg)),
Err(err) => {
log::error!("parsing server message: {err}; ignoring.")
}
}
}
}
}
}
#[derive(PartialEq, Debug, Clone)]
pub enum ClientEvent {
Disconnected,
Waiting,
ShowRole(RoleTitle),
NotInLobby(Box<[PublicIdentity]>),
InLobby(Box<[PublicIdentity]>),
GameInProgress,
GameOver(GameOver),
}
impl TryFrom<ServerMessage> for ClientEvent {
type Error = ServerMessage;
fn try_from(msg: ServerMessage) -> Result<Self, Self::Error> {
Ok(match msg {
ServerMessage::Disconnect => Self::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)));
let players = players.into_iter().collect();
match joined {
true => Self::InLobby(players),
false => Self::NotInLobby(players),
}
}
ServerMessage::GameInProgress => Self::GameInProgress,
_ => return Err(msg),
})
}
}
#[derive(PartialEq, Debug, Clone)] #[derive(PartialEq, Debug, Clone)]
pub enum ClientEvent2 { pub enum ClientEvent2 {
Disconnected, Disconnected,
Connecting, Connecting,
Waiting,
ShowRole(RoleTitle), ShowRole(RoleTitle),
Lobby { Lobby {
@ -299,7 +97,6 @@ pub fn Client2(ClientProps { auto_join }: &ClientProps) -> Html {
} }
html! {<p>{"connecting..."}</p>} html! {<p>{"connecting..."}</p>}
} }
ClientEvent2::Waiting => html! {<p>{"waiting..."}</p>},
ClientEvent2::ShowRole(role_title) => { ClientEvent2::ShowRole(role_title) => {
let send = (*send).clone(); let send = (*send).clone();
let error_cb = error_cb.clone(); let error_cb = error_cb.clone();
@ -386,346 +183,8 @@ pub fn Client2(ClientProps { auto_join }: &ClientProps) -> Html {
} }
} }
pub struct Client {
player: Option<Identification>,
send: Sender<ClientMessage>,
recv: Option<Receiver<ClientMessage>>,
current_event: Option<ClientEvent>,
auto_join: bool,
error_callback: Callback<Option<WerewolfError>>,
}
impl Client {
fn error(&self, err: WerewolfError) {
self.error_callback.emit(Some(err))
}
fn clear_error(&self) {
self.error_callback.emit(None)
}
fn bug(&self, msg: &str) {
log::warn!("BUG: {msg}")
}
}
#[derive(Debug, Clone, PartialEq, Copy, Properties)] #[derive(Debug, Clone, PartialEq, Copy, Properties)]
pub struct ClientProps { pub struct ClientProps {
#[prop_or_default] #[prop_or_default]
pub auto_join: bool, pub auto_join: bool,
} }
impl Component for Client {
type Message = Message;
type Properties = ClientProps;
fn create(ctx: &Context<Self>) -> Self {
gloo::utils::document().set_title("Werewolves Player");
let player = PlayerId::load_from_storage()
.ok()
.and_then(|p| PublicIdentity::load_from_storage().ok().map(|n| (p, n)))
.map(|(player_id, public)| Identification { player_id, public });
let (send, recv) = futures::channel::mpsc::channel::<ClientMessage>(100);
Self {
player,
send,
recv: Some(recv),
auto_join: ctx.props().auto_join,
error_callback: Callback::from(|err| {
if let Some(err) = err {
log::error!("{err}")
}
}),
current_event: None,
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
if self.player.is_none() {
let scope = ctx.link().clone();
let callback = Callback::from(move |public: PublicIdentity| {
scope.send_message(Message::SetPublicIdentity(public));
});
return html! {
<Signin callback={callback}/>
};
} else if self.recv.is_some() {
// Player info loaded, but connection isn't started
ctx.link().send_message(Message::Connect);
return html! {};
}
let msg = match self.current_event.as_ref() {
Some(msg) => msg,
None => {
return html! {
<div class="connecting">
<p>{"Connecting..."}</p>
</div>
};
}
};
let content = match msg {
ClientEvent::Disconnected => html! {
<div class="disconnected">
<p>{"You were disconnected"}</p>
</div>
},
ClientEvent::Waiting => {
html! {
<div class="waiting-lobby">
<p>{"Waiting"}</p>
</div>
}
}
ClientEvent::ShowRole(role_title) => {
let send = self.send.clone();
let on_click =
Callback::from(
move |_| {
while send.clone().try_send(ClientMessage::RoleAck).is_err() {}
},
);
html! {
<div class="game-start-role">
<p>{format!("Your role: {role_title}")}</p>
<button onclick={on_click}>{"got it"}</button>
</div>
}
}
ClientEvent::NotInLobby(players) => {
let send = self.send.clone();
let on_click =
Callback::from(
move |_| {
while send.clone().try_send(ClientMessage::Hello).is_err() {}
},
);
html! {
<div class="lobby">
<Button on_click={on_click}>{"Join"}</Button>
<p>{format!("Players in lobby: {}", players.len())}</p>
<ul class="players">
{players.iter().map(|p| html!{<p>{p.to_string()}</p>}).collect::<Html>()}
</ul>
</div>
}
}
ClientEvent::InLobby(players) => {
let send = self.send.clone();
let on_click =
Callback::from(
move |_| {
while send.clone().try_send(ClientMessage::Goodbye).is_err() {}
},
);
html! {
<div class="lobby">
<Button on_click={on_click}>{"Leave"}</Button>
<p>{format!("Players in lobby: {}", players.len())}</p>
<ul class="players">
{players.iter().map(|p| html!{<p>{p.to_string()}</p>}).collect::<Html>()}
</ul>
</div>
}
}
ClientEvent::GameInProgress => html! {
<div class="waiting-lobby">
<p>{"game in progress"}</p>
</div>
},
ClientEvent::GameOver(result) => html! {
<div>
<h2>{"game over"}</h2>
<p>{
match result {
GameOver::VillageWins => "village wins",
GameOver::WolvesWin => "wolves win",
}}</p>
</div>
},
};
let player = self
.player
.as_ref()
.map(|player| {
html! {
<Identity ident={player.public.clone()} class="zoom">
</Identity>
}
})
.unwrap_or(html!());
let send = self.send.clone();
let client_nav_msg_cb = move |msg| {
let mut send = send.clone();
yew::platform::spawn_local(async move {
if let Err(err) = send.send(msg).await {
log::error!("sending nav message: {err}");
}
});
};
let nav = self.player.as_ref().map(|_| {
html! {
<ClientNav message_callback={client_nav_msg_cb} />
}
});
html! {
<>
{nav}
<client>
{player}
{content}
</client>
</>
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
log::info!("update: {msg:?}");
match msg {
Message::ForceIdentity(id) => {
self.player.replace(id);
true
}
Message::SetErrorCallback(cb) => {
self.error_callback = cb;
false
}
Message::SetPublicIdentity(public) => {
match self.player.as_mut() {
Some(p) => {
if let Err(err) = public.save_to_storage() {
self.error(err.into());
return false;
}
p.public = public;
}
None => {
let player_id = match PlayerId::load_from_storage() {
Ok(pid) => pid,
Err(StorageError::KeyNotFound(_)) => {
let pid = PlayerId::new();
if let Err(err) = pid.save_to_storage() {
self.error(err.into());
return false;
}
pid
}
Err(err) => {
self.error(err.into());
return false;
}
};
if let Err(err) = public.save_to_storage() {
self.error(err.into());
return false;
}
let ident = Identification { player_id, public };
self.player = Some(ident.clone());
if let Some(recv) = self.recv.take() {
yew::platform::spawn_local(
Connection {
recv,
ident,
scope: ctx.link().clone(),
}
.run(),
);
}
}
}
true
}
Message::RecvServerMessage(msg) => {
if let ServerMessage::LobbyInfo {
joined: false,
players: _,
} = &msg
&& self.auto_join
{
let mut send = self.send.clone();
yew::platform::spawn_local(async move {
if let Err(err) = send.send(ClientMessage::Hello).await {
log::error!("send: {err}");
}
});
self.auto_join = false;
return false;
}
let msg = match msg.try_into() {
Ok(event) => {
self.current_event.replace(event);
return true;
}
Err(msg) => msg,
};
match msg {
ServerMessage::GameStart { role } => {
self.current_event.replace(ClientEvent::ShowRole(role));
}
ServerMessage::InvalidMessageForGameState => self.error(
WerewolfError::GameError(GameError::InvalidMessageForGameState),
),
ServerMessage::NoSuchTarget => {
self.error(WerewolfError::InvalidTarget);
}
ServerMessage::GameInProgress
| ServerMessage::LobbyInfo {
joined: _,
players: _,
}
| ServerMessage::Disconnect => return false,
ServerMessage::GameOver(game_over) => {
self.current_event = Some(ClientEvent::GameOver(game_over));
}
ServerMessage::Reset => {
let mut send = self.send.clone();
self.current_event = Some(ClientEvent::Disconnected);
yew::platform::spawn_local(async move {
if let Err(err) = send.send(ClientMessage::GetState).await {
log::error!("send: {err}");
}
});
}
ServerMessage::Sleep => self.current_event = Some(ClientEvent::Waiting),
ServerMessage::Update(update) => match (update, self.player.as_mut()) {
(PlayerUpdate::Number(num), Some(player)) => {
player.public.number = Some(num);
return true;
}
(_, None) => return false,
},
};
true
}
Message::Connect => {
if let Some(player) = self.player.as_ref()
&& let Some(recv) = self.recv.take()
{
yew::platform::spawn_local(
Connection {
scope: ctx.link().clone(),
ident: player.clone(),
recv,
}
.run(),
);
return true;
}
while let Err(err) = self.send.try_send(ClientMessage::GetState) {
log::error!("send IsThereALobby: {err}")
}
false
}
}
}
}

View File

@ -94,6 +94,7 @@ impl Connection2 {
.map_err(|_| ConnectionError::ConnectionAlreadyActive)?; .map_err(|_| ConnectionError::ConnectionAlreadyActive)?;
core::mem::drop(active); core::mem::drop(active);
let mut conn = self.clone(); let mut conn = self.clone();
#[allow(clippy::await_holding_refcell_ref)]
yew::platform::spawn_local(async move { yew::platform::spawn_local(async move {
let active = conn.active.clone(); let active = conn.active.clone();
conn.active = Rc::new(RefCell::new(())); conn.active = Rc::new(RefCell::new(()));
@ -105,6 +106,7 @@ impl Connection2 {
Ok(()) Ok(())
} }
#[allow(clippy::await_holding_refcell_ref)]
async fn run(&mut self) { async fn run(&mut self) {
const CONNECT_WAIT: Duration = Duration::from_secs(3); const CONNECT_WAIT: Duration = Duration::from_secs(3);
let url = url(); let url = url();
@ -138,7 +140,8 @@ impl Connection2 {
}; };
log::debug!("beginning listening loop"); log::debug!("beginning listening loop");
loop { let mut quit = false;
while !quit {
let mut recv = self.receiver.borrow_mut(); let mut recv = self.receiver.borrow_mut();
let msg = futures::select! { let msg = futures::select! {
r = ws.next() => { r = ws.next() => {
@ -199,6 +202,7 @@ impl Connection2 {
}; };
match parse { match parse {
Ok(msg) => { Ok(msg) => {
quit = matches!(msg, ServerMessage::Disconnect);
if let Some(state) = self.message_to_client_state(msg) { if let Some(state) = self.message_to_client_state(msg) {
self.state.set(state); self.state.set(state);
} }
@ -245,6 +249,7 @@ impl Connection2 {
| ServerMessage::Sleep | ServerMessage::Sleep
| ServerMessage::Reset | ServerMessage::Reset
| ServerMessage::GameInProgress => { | ServerMessage::GameInProgress => {
log::info!("ignoring: {msg:?}");
return None; return None;
} }
}) })

View File

@ -19,11 +19,10 @@ pub struct TwoTargetProps {
pub target_selection: Option<Callback<(CharacterId, CharacterId)>>, pub target_selection: Option<Callback<(CharacterId, CharacterId)>>,
} }
#[derive(ChecksAs, Clone)] #[derive(Clone)]
enum TwoTargetSelection { enum TwoTargetSelection {
None, None,
One(CharacterId), One(CharacterId),
#[checks]
Two(CharacterId, CharacterId), Two(CharacterId, CharacterId),
} }

View File

@ -1,19 +1,19 @@
use yew::prelude::*; use yew::prelude::*;
#[derive(Debug, PartialEq, Properties)] // #[derive(Debug, PartialEq, Properties)]
pub struct NotificationProps { // pub struct NotificationProps {
pub text: String, // pub text: String,
pub callback: Callback<()>, // pub callback: Callback<()>,
} // }
#[function_component] // #[function_component]
pub fn Notification(props: &NotificationProps) -> Html { // pub fn Notification(props: &NotificationProps) -> Html {
let cb = props.callback.clone(); // let cb = props.callback.clone();
let on_click = Callback::from(move |_| cb.clone().emit(())); // let on_click = Callback::from(move |_| cb.clone().emit(()));
html! { // html! {
<stack> // <stack>
<h2>{props.text.clone()}</h2> // <h2>{props.text.clone()}</h2>
<button class="confirm" onclick={on_click}>{"Ok"}</button> // <button class="confirm" onclick={on_click}>{"Ok"}</button>
</stack> // </stack>
} // }
} // }

View File

@ -27,7 +27,7 @@ use werewolves_proto::{
use yew::{context::ContextProviderProps, prelude::*}; use yew::{context::ContextProviderProps, prelude::*};
use crate::clients::{ use crate::clients::{
client::{Client, Client2, ClientContext, ClientProps, Message}, client::{Client2, ClientContext},
host::{Host, HostEvent}, host::{Host, HostEvent},
}; };
@ -70,7 +70,7 @@ fn main() {
clients.append_child(&dupe).unwrap(); clients.append_child(&dupe).unwrap();
} }
let client = yew::Renderer::<ContextProvider<ClientContext>>::with_root_and_props( yew::Renderer::<ContextProvider<ClientContext>>::with_root_and_props(
dupe, dupe,
ContextProviderProps { ContextProviderProps {
context: ClientContext { context: ClientContext {
@ -90,25 +90,9 @@ fn main() {
}, },
) )
.render(); .render();
// let client = yew::Renderer::<Client2>::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 { } else {
let client = yew::Renderer::<ContextProvider<ClientContext>>::with_root_and_props( yew::Renderer::<ContextProvider<ClientContext>>::with_root_and_props(
app_element, app_element,
ContextProviderProps { ContextProviderProps {
context: ClientContext { context: ClientContext {
@ -121,11 +105,5 @@ fn main() {
}, },
) )
.render(); .render();
// let client = yew::Renderer::<Client>::with_root_and_props(
// app_element,
// ClientProps { auto_join: false },
// )
// .render();
// client.send_message(Message::SetErrorCallback(error_callback));
} }
} }