use serde::{Deserialize, Serialize};
pub use string::FixedStr20 as RpsData;
use uuid::Uuid;
use super::{
blake3::{B3Hash, B3Key, Blake3Hash},
hex::OwnedHexStr,
utils::hash_users,
};
type Version = u32;
pub const PROTOCOL_VERSION: Version = 0;
pub type CommitHash = OwnedHexStr<32, 64>;
pub type RoomId = u64;
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum Hash {
B3sum(B3Hash),
}
impl Hash {
#[allow(unused)]
pub fn get_algorithm(&self) -> &'static str {
match self {
Hash::B3sum(_) => "blake3",
}
}
}
impl AsRef<str> for Hash {
fn as_ref(&self) -> &str {
match self {
Hash::B3sum(owned_hex_str) => owned_hex_str.as_ref(),
}
}
}
mod string {
#![expect(clippy::partialeq_ne_impl)]
use fixed_len_str::fixed_len_str;
fixed_len_str!(20);
impl FixedStr20 {
#[allow(unused)]
pub const LEN: usize = 20;
}
}
impl Blake3Hash for RpsData {
fn hash_keyed(&self, key: &B3Key, round: &Round) -> B3Hash {
blake3::Hasher::new_keyed(key)
.update(&round.round_id.into_original_bytes())
.update(self.trim_end_matches('\0').as_bytes())
.finalize()
.into()
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum HashWithData {
B3sum {
data: RpsData,
hash: B3Hash,
key: B3Key,
},
}
#[allow(dead_code)]
impl HashWithData {
pub fn verify(&self, round: &Round) -> Result<(), String> {
match self {
HashWithData::B3sum { hash, key, data } => {
let real_hash = data.hash_keyed(key, round);
if &real_hash != hash {
Err(format!("Verifying {hash} failed. Got {real_hash}"))
} else {
Ok(())
}
}
}
}
pub fn get_data(&self) -> &str {
match self {
HashWithData::B3sum {
hash: _,
key: _,
data,
} => data.as_ref().trim_end_matches('\0'),
}
}
pub fn get_hash(&self) -> String {
match self {
HashWithData::B3sum {
hash,
key: _,
data: _,
} => format!("{hash}"),
}
}
pub fn as_hash(&self) -> Hash {
match self {
HashWithData::B3sum {
data: _,
hash,
key: _,
} => Hash::B3sum(*hash),
}
}
}
#[derive(Serialize, Deserialize, Debug)]
pub enum ClientMessage {
Ping { c: u8 },
Join { room_id: RoomId, user: Uuid },
Play { value: Hash, round: Round }, ConfirmPlay(HashWithData),
RoundFinished { round_id: RoundId },
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
pub struct UserMove {
pub user: Uuid,
pub data: HashWithData,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
pub enum UserState {
InRoom,
Played(Hash),
Confirmed(HashWithData),
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
pub struct User {
pub id: Uuid,
pub state: UserState,
}
pub type RoundId = B3Hash;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct Round {
pub round_id: RoundId,
pub users: Box<[Uuid]>,
}
impl Round {
pub fn validate(&self) -> Result<(), String> {
if !self.users.is_sorted() {
return Err("Invalid round: users are not sorted".into());
}
let hash = hash_users(self.users.iter().copied());
if hash == self.round_id {
Ok(())
} else {
Err(format!(
"Invalid round: got id {}, expected {hash}",
self.round_id
))
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct RoomState {
pub id: RoomId,
pub users: Box<[User]>,
pub round: Option<Round>,
}
#[derive(Serialize, Deserialize, Debug)]
pub enum ServerMessage {
Hello(Version, String, Option<CommitHash>),
Pong { c: u8 },
RoomUpdate { new_state: RoomState },
Error(String),
}