1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
use std::collections::HashSet;
use crate::common::{
message::{RoomState, UserState},
rps::state::RpsState,
utils::get_random_bytes,
};
use super::state::ClientState;
pub(super) fn handle_new_room_state(
state: &mut ClientState,
new_room_state: &RoomState,
) -> Result<(), String> {
if let Some(old_state) = &state.room {
assert_eq!(new_room_state.id, old_state.id);
}
// TODO: validate the same state changes in the server
let mut old_cache_keys = state.cache.keys().copied().collect::<HashSet<_>>();
for user in &new_room_state.users {
old_cache_keys.remove(&user.id);
let id = user.id;
match state.cache.get(&user.id) {
None => match user.state {
InRoom => {
state.cache.insert(user.id, InRoom);
}
user_state => {
if state.room.is_some() { // isn't first room update
Err(format!("user {id} joined game in state {user_state:?}"))?
} else {
state.cache.insert(user.id, user_state); // cache as it's the first room update
}
}
},
Some(InRoom) => match user.state {
InRoom => (),
Played(hash) => {
state.cache.insert(user.id, Played(hash));
}
Confirmed(_) => {
Err(format!("user {id} confirmed without playing before"))?
}
},
Some(Played(hash)) => match user.state {
InRoom => {
if new_room_state.round.is_some() {
Err(format!(
"user {id} switched to state InRoom without confirming {hash:?}"
))?
} else {
// TODO: improve behaviour, is a bit weird
// Can be ok, other user in round probably left
state.cache.insert(user.id, InRoom);
}
},
Played(new_hash) => {
if *hash != new_hash {
Err(format!(
"user {id} changed hash from {hash:?} to {new_hash:?} without confirming"
))?
}
},
Confirmed(hash_with_data) => {
if hash_with_data.as_hash() != *hash {
Err(format!(
"user {id} did not confirm {hash:?}, instead played: {hash_with_data:?}"
))?
}
state.cache.insert(user.id, Confirmed(hash_with_data));
hash_with_data.verify(
new_room_state.round.as_ref().ok_or_else(
|| format!("No current round while {id} confirmed")
)?
)?
},
},
Some(Confirmed(hash_with_data)) => {
match user.state {
InRoom => {
state.cache.insert(user.id, InRoom);
},
Played(hash) => Err(format!(
"user {id} played {hash:?} directly after confirming without being in room before"
))?,
Confirmed(new_hash_with_data) => {
if *hash_with_data != new_hash_with_data {
Err(format!(
"user {id} changed confirmation from {hash_with_data:?} to {new_hash_with_data:?}"
))?
}
},
}
},
}
}
for key in old_cache_keys {
state.cache.remove(&key);
}
if let Some(ref round) = new_room_state.round {
round.validate()?;
}
let moves = new_room_state
.users
.iter()
.filter_map(|user| {
Some((
user.id,
match user.state {
UserState::Confirmed(data) => data,
_ => None?,
},
))
})
.collect::<Box<[_]>>();
state.current_plays = None;
if let Some(room) = state.room.as_ref() {
if let Some(round) = room.round.as_ref() {
if round.users.len() == moves.len() {
state.current_plays = Some(moves);
}
}
}
log::trace!("Us: {:?}", state.user);
log::trace!("Users: {:?}", new_room_state.users);
let us = new_room_state
.users
.iter()
.find(|user| user.id == state.user)
.ok_or("we should be in the room we get updates from")?;
use crate::common::message::UserState::*;
let new_rps_state = match us.state {
InRoom => RpsState::InRoom,
Played(_) => RpsState::Played,
Confirmed(_) => {
// TODO: check here
// assert_eq!(data.get_data(), state.curr_play.trim_end_matches('\0'));
RpsState::Confirmed
}
};
if state.state != new_rps_state {
if matches!(new_rps_state, RpsState::InRoom) {
state.secret = get_random_bytes();
}
state.state = new_rps_state;
}
state.room = Some(new_room_state.clone());
Ok(())
}