pub const WIRE_NAME: usize = 24;
pub type WireName = [u8; WIRE_NAME];
pub fn wire_name(name: &str) -> WireName {
let mut out = [0u8; WIRE_NAME];
let mut len = 0;
for ch in name.chars() {
let next = len + ch.len_utf8();
if next > WIRE_NAME {
break;
}
ch.encode_utf8(&mut out[len..next]);
len = next;
}
out
}
pub fn wire_table(names: &[String]) -> [WireName; crate::sim::MAX_PLAYERS] {
std::array::from_fn(|i| {
names
.get(i)
.map_or([0u8; WIRE_NAME], |name| wire_name(name))
})
}
pub fn table_from_wire(
names: &[WireName; crate::sim::MAX_PLAYERS],
) -> [String; crate::sim::MAX_PLAYERS] {
std::array::from_fn(|i| name_from_wire(&names[i]))
}
pub const WIRE_CHAT: usize = 96;
pub type WireChat = [u8; WIRE_CHAT];
pub const CHAT_CHARS: usize = 48;
pub fn wire_chat(line: &str) -> WireChat {
let mut out = [0u8; WIRE_CHAT];
let mut len = 0;
for ch in line.chars().take(CHAT_CHARS) {
let next = len + ch.len_utf8();
if next > WIRE_CHAT {
break;
}
ch.encode_utf8(&mut out[len..next]);
len = next;
}
out
}
pub fn chat_from_wire(bytes: &WireChat) -> String {
let end = bytes.iter().position(|&b| b == 0).unwrap_or(WIRE_CHAT);
let Ok(text) = std::str::from_utf8(&bytes[..end]) else {
return String::new();
};
text.chars()
.filter(|ch| !ch.is_control())
.take(CHAT_CHARS)
.collect::<String>()
.trim()
.to_string()
}
const NAME_CHARS: usize = 12;
pub fn name_from_wire(bytes: &WireName) -> String {
let end = bytes.iter().position(|&b| b == 0).unwrap_or(WIRE_NAME);
let Ok(text) = std::str::from_utf8(&bytes[..end]) else {
return String::new();
};
text.chars()
.filter(|&c| !c.is_control() && c != '|' && c != ':')
.take(NAME_CHARS)
.collect::<String>()
.trim()
.to_string()
}
#[cfg(test)]
mod name_tests {
use super::*;
#[test]
fn a_wide_name_is_cut_at_a_character_and_read_back() {
let wide = "あ".repeat(12);
let bytes = wire_name(&wide);
assert!(!bytes.contains(&0), "eight fill the 24 bytes exactly");
assert_eq!(name_from_wire(&bytes), "あ".repeat(8));
let bytes = wire_name(&format!("{}é", "a".repeat(23)));
assert_eq!(bytes[23], 0, "no half of an 'é' on the wire");
assert_eq!(name_from_wire(&bytes), "a".repeat(12), "then capped");
}
#[test]
fn a_name_that_fills_the_wire_has_no_terminator_and_still_reads_back() {
let full = "abcdefghijklmnopqrstuvwx";
let bytes = wire_name(full);
assert!(bytes.iter().all(|&b| b != 0), "every byte is a letter");
assert_eq!(name_from_wire(&bytes), &full[..NAME_CHARS]);
}
#[test]
fn separators_are_stripped_before_the_cap_and_nothing_left_is_empty() {
assert_eq!(name_from_wire(&wire_name("|||||||||||||Alice")), "Alice");
assert_eq!(name_from_wire(&wire_name("a:b|c")), "abc");
assert_eq!(name_from_wire(&wire_name(" Bob ")), "Bob");
assert_eq!(name_from_wire(&wire_name("Bob\n\u{1b}")), "Bob");
for blank in ["", " ", "|||", ":::", "\t\n", " | : "] {
assert_eq!(name_from_wire(&wire_name(blank)), "", "{blank:?}");
}
let mut junk = [0xFFu8; WIRE_NAME];
junk[WIRE_NAME - 1] = 0;
assert_eq!(name_from_wire(&junk), "");
}
}
#[cfg(test)]
mod chat_tests {
use super::*;
#[test]
fn a_line_of_chat_survives_the_trip_and_is_tidied_on_the_way() {
for line in ["ready?", "wait for me!", "Anna: 3 > 2 | ok", "héllo"] {
assert_eq!(chat_from_wire(&wire_chat(line)), line, "{line}");
}
assert_eq!(chat_from_wire(&wire_chat("one\ntwo\u{1b}[0m")), "onetwo[0m");
assert_eq!(chat_from_wire(&wire_chat(" ")), "");
assert_eq!(chat_from_wire(&wire_chat(" hi ")), "hi");
let long = "é".repeat(400);
let short = chat_from_wire(&wire_chat(&long));
assert!(short.chars().count() <= CHAT_CHARS, "{}", short.len());
let mut junk = [0xFFu8; WIRE_CHAT];
junk[WIRE_CHAT - 1] = 0;
assert_eq!(chat_from_wire(&junk), "");
}
}