1use serde::{Deserialize, Serialize};
2pub use string::FixedStr20 as RpsData;
3use uuid::Uuid;
4
5use super::{
6 blake3::{B3Hash, B3Key, Blake3Hash},
7 hex::OwnedHexStr,
8 utils::hash_users,
9};
10
11type Version = u32;
12pub const PROTOCOL_VERSION: Version = 0;
14
15pub type CommitHash = OwnedHexStr<32, 64>;
16
17pub type RoomId = u64;
18
19#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
20pub enum Hash {
21 B3sum(B3Hash),
22}
23
24impl Hash {
25 #[allow(unused)]
26 pub fn get_algorithm(&self) -> &'static str {
27 match self {
28 Hash::B3sum(_) => "blake3",
29 }
30 }
31}
32
33impl AsRef<str> for Hash {
34 fn as_ref(&self) -> &str {
35 match self {
36 Hash::B3sum(owned_hex_str) => owned_hex_str.as_ref(),
37 }
38 }
39}
40
41mod string {
42 #![expect(clippy::partialeq_ne_impl)]
43 use fixed_len_str::fixed_len_str;
44
45 fixed_len_str!(20);
46
47 impl FixedStr20 {
48 #[allow(unused)]
49 pub const LEN: usize = 20;
50 }
51}
52
53impl Blake3Hash for RpsData {
54 fn hash_keyed(&self, key: &B3Key, round: &Round) -> B3Hash {
55 blake3::Hasher::new_keyed(key)
56 .update(&round.round_id.into_original_bytes())
57 .update(self.trim_end_matches('\0').as_bytes())
58 .finalize()
59 .into()
60 }
61}
62
63#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
64pub enum HashWithData {
65 B3sum {
66 data: RpsData,
67 hash: B3Hash,
68 key: B3Key,
69 },
70}
71
72#[allow(dead_code)]
73impl HashWithData {
74 pub fn verify(&self, round: &Round) -> Result<(), String> {
75 match self {
76 HashWithData::B3sum { hash, key, data } => {
77 let real_hash = data.hash_keyed(key, round);
78
79 if &real_hash != hash {
80 Err(format!("Verifying {hash} failed. Got {real_hash}"))
81 } else {
82 Ok(())
83 }
84 }
85 }
86 }
87
88 pub fn get_data(&self) -> &str {
89 match self {
90 HashWithData::B3sum {
91 hash: _,
92 key: _,
93 data,
94 } => data.as_ref().trim_end_matches('\0'),
95 }
96 }
97
98 pub fn get_hash(&self) -> String {
99 match self {
100 HashWithData::B3sum {
101 hash,
102 key: _,
103 data: _,
104 } => format!("{hash}"),
105 }
106 }
107
108 pub fn as_hash(&self) -> Hash {
109 match self {
110 HashWithData::B3sum {
111 data: _,
112 hash,
113 key: _,
114 } => Hash::B3sum(*hash),
115 }
116 }
117}
118
119#[derive(Serialize, Deserialize, Debug)]
120pub enum ClientMessage {
121 Ping { c: u8 },
122 Join { room_id: RoomId, user: Uuid },
124 Play { value: Hash, round: Round }, ConfirmPlay(HashWithData),
126 RoundFinished { round_id: RoundId },
127}
128
129#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
130pub struct UserMove {
131 pub user: Uuid,
132 pub data: HashWithData,
133}
134
135#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
136pub enum UserState {
137 InRoom,
138 Played(Hash),
139 Confirmed(HashWithData),
140}
141
142#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
143pub struct User {
144 pub id: Uuid,
145 pub state: UserState,
146}
147
148pub type RoundId = B3Hash;
149
150#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
151pub struct Round {
152 pub round_id: RoundId,
153 pub users: Box<[Uuid]>,
154}
155
156impl Round {
157 pub fn validate(&self) -> Result<(), String> {
158 if !self.users.is_sorted() {
159 return Err("Invalid round: users are not sorted".into());
160 }
161 let hash = hash_users(self.users.iter().copied());
162 if hash == self.round_id {
163 Ok(())
164 } else {
165 Err(format!(
166 "Invalid round: got id {}, expected {hash}",
167 self.round_id
168 ))
169 }
170 }
171}
172
173#[derive(Serialize, Deserialize, Debug, Clone)]
174pub struct RoomState {
175 pub id: RoomId,
176 pub users: Box<[User]>,
177 pub round: Option<Round>,
178}
179
180#[derive(Serialize, Deserialize, Debug)]
181pub enum ServerMessage {
182 Hello(Version, String, Option<CommitHash>),
183 Pong { c: u8 },
185 RoomUpdate { new_state: RoomState },
186 Error(String),
187}