use super::*;
use crate::app::i18n::fill;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum HostStep {
Ask,
Go,
Nothing,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(super) struct HostAsk {
pub answered: bool,
pub pressed_h: bool,
pub busy: bool,
pub auto: bool,
}
#[cfg(test)]
pub(super) fn idle() -> HostAsk {
HostAsk {
answered: false,
pressed_h: false,
busy: false,
auto: false,
}
}
#[cfg(test)]
pub(super) fn peers_named(names: &[&str], watchers: &[usize]) -> PeerBook {
let mut peers = PeerBook::default();
for (i, name) in names.iter().enumerate() {
peers.row(i).name = name.to_string();
}
for watcher in watchers {
peers.row(*watcher).watch = true;
}
peers
}
pub(super) fn address_here(transport: &UdpTransport) -> Option<String> {
let port = transport.local_addr().ok()?.port();
let ip = crate::transport::local_ip()?;
Some(SocketAddr::new(ip, port).to_string())
}
pub(super) fn host_step(ask: HostAsk) -> HostStep {
if ask.answered {
return HostStep::Go;
}
if ask.busy {
return HostStep::Nothing;
}
match (ask.pressed_h || ask.auto, ask.auto) {
(true, true) => HostStep::Go,
(true, false) => HostStep::Ask,
(false, _) => HostStep::Nothing,
}
}
#[derive(bevy::ecs::system::SystemParam)]
pub struct IntoArena<'w> {
online: ResMut<'w, Online>,
tournament: ResMut<'w, crate::app::tournament::Tournament>,
next_screen: ResMut<'w, NextState<Screen>>,
next_vphase: ResMut<'w, NextState<VersusPhase>>,
bots: ResMut<'w, crate::app::bot_seats::BotSeats>,
}
impl IntoArena<'_> {
fn walk_in(&mut self, session: OnlineSession, series: crate::app::tournament::Tournament) {
self.online.0 = Some(session);
*self.tournament = series;
self.next_vphase.set(VersusPhase::Countdown);
self.next_screen.set(Screen::Versus);
}
}
pub fn host_tick(
time: Res<Time>,
keys: Res<ButtonInput<KeyCode>>,
settings: Res<GameSettings>,
config: Res<MatchConfig>,
beaches: Res<crate::app::match_setup::CustomBeaches>,
mut state: ResMut<LobbyState>,
mut arena: IntoArena,
) {
let tr = settings.tr();
let game_name = state.game_name.clone();
let Some(hosted) = state.hosted_mut() else {
return;
};
let arrived = host_the_bots(&mut arena.bots, &config, tr, hosted.players_aboard());
let taken = 1 + hosted.players_aboard() as u8 + arrived.len() as u8;
let on_air = crate::transport::OnAir {
name: &game_name,
host: &settings.names[0],
taken,
seats: MAX_PLAYERS as u8,
bots: config.bots_welcome,
full: hosted.transport.peer_count() >= crate::transport::MAX_PEERS,
};
let picked = work_the_socket(hosted, time.delta_secs(), on_air);
let do_announce = picked.announced;
for (_, name, text) in picked.said {
state.hear(&name, &text);
}
note_arrivals_and_departures(&mut state, tr, picked.greeted);
let joined = state.hosted().map_or(0, |hosted| hosted.peers.len());
state.table = state.roster(tr, &settings.names[0]);
state.table_kinds = state.roster_kinds();
for (_, name) in &arrived {
if state.table.len() < MAX_PLAYERS {
state.table.push(name.clone());
state.table_kinds.push(crate::sim::SeatKind::Bot);
}
}
state.bot_string = arena
.bots
.door
.as_ref()
.and_then(crate::app::bot_seats::Doorway::open_string);
if do_announce && let Some(hosted) = state.hosted() {
let names = crate::transport::wire_table(&state.table);
let terms = crate::app::match_setup::terms(&config, settings.team_mode, 0);
hosted.transport.send(NetMsg::Roster {
seats: state.table.len().min(MAX_PLAYERS) as u8,
names,
terms,
kinds: crate::transport::wire_kinds(&state.table_kinds),
});
}
let (players_aboard, watchers_aboard, port) = state.hosted().map_or((0, 0, None), |hosted| {
(
hosted.players_aboard(),
hosted.watchers_aboard(),
hosted.transport.local_addr().ok().map(|addr| addr.port()),
)
});
let rivals = joined + arrived.len();
if rivals > 0 {
let aboard = players_aboard + arrived.len();
state.feedback = gathering_feedback(tr, &config, aboard, watchers_aboard);
} else {
state.feedback = match port {
Some(port) => fill(tr.lobby_hosting, &[("p", &port.to_string())]),
None => tr.lobby_hosting_noport.to_string(),
};
}
let quota = crate::app::dev::auto_host_quota();
let launch = should_launch(
rivals,
state.typing.is_some() || state.kicking,
crate::app::menu_ui::enter(&keys),
quota,
);
if launch {
launch_the_match(&mut state, &settings, &config, &beaches, joined, &mut arena);
}
}
fn host_the_bots(
bots: &mut crate::app::bot_seats::BotSeats,
config: &MatchConfig,
tr: &'static crate::app::i18n::Tr,
people: usize,
) -> Vec<(crate::bots::listener::BotId, String)> {
if !config.bots_welcome {
bots.door = None;
return Vec::new();
}
if bots.door.is_none() {
match crate::app::bot_seats::Doorway::open_lan() {
Ok(door) => bots.door = Some(door),
Err(e) => {
bots.feedback = fill(tr.door_could_not_listen, &[("e", &e.to_string())]);
return Vec::new();
}
}
}
let Some(door) = &mut bots.door else {
return Vec::new();
};
door.poll();
let arrived = door.arrived(tr);
let room = 1 + people + arrived.len() < MAX_PLAYERS;
door.keep_a_chair_open(room);
arrived
}
fn launch_the_match(
state: &mut LobbyState,
settings: &GameSettings,
config: &MatchConfig,
beaches: &crate::app::match_setup::CustomBeaches,
joined: usize,
arena: &mut IntoArena,
) {
let Standing::Hosting(Hosted {
announcer,
transport,
peers,
announce_in: _,
}) = std::mem::take(&mut state.standing)
else {
unreachable!("launching a match from a lobby that is not hosting");
};
debug_assert_eq!(joined, peers.len());
let plan = seat_plan(&peers);
let people = 1 + plan.iter().filter(|seat| seat.is_some()).count() as u8;
let bots_here: Vec<(u8, crate::bots::listener::BotId, String)> = arena
.bots
.door
.as_ref()
.map(|door| door.arrived(settings.tr()))
.unwrap_or_default()
.into_iter()
.take(MAX_PLAYERS - usize::from(people))
.enumerate()
.map(|(i, (id, name))| (people + i as u8, id, name))
.collect();
let humans = people + bots_here.len() as u8;
let bots = config
.bots
.min(MAX_PLAYERS as u8 - humans)
.max(2u8.saturating_sub(humans));
let seats = humans + bots;
let seed = crate::app::clock::fresh_seed();
let beach = crate::app::match_setup::beach_bytes(config, seats, beaches);
let terms = MatchTerms {
bots,
map: map_for(config, seats).index() as u8,
..crate::app::match_setup::terms(config, settings.team_mode, seed)
};
let mut names: [String; MAX_PLAYERS] = Default::default();
names[0].clone_from(&settings.names[0]);
for (peer, slot) in plan.iter().enumerate() {
if let Some(seat) = slot
&& let Some(peer) = peers.get(peer)
{
names[usize::from(*seat)].clone_from(&peer.name);
}
}
for (seat, _, name) in &bots_here {
names[usize::from(*seat)].clone_from(name);
}
let wire_names = crate::transport::wire_table(&names);
let mut kinds = [crate::sim::SeatKind::Human; MAX_PLAYERS];
for (seat, ..) in &bots_here {
kinds[usize::from(*seat)] = crate::sim::SeatKind::Bot;
}
for (peer, slot) in plan.iter().enumerate() {
if let Some(seat) = slot
&& peers.get(peer).is_some_and(|peer| peer.bot)
{
kinds[usize::from(*seat)] = crate::sim::SeatKind::Bot;
}
}
for seat in humans..seats {
kinds[usize::from(seat)] = crate::sim::SeatKind::Ai;
}
let wire_kinds = crate::transport::wire_kinds(&kinds);
let standing = terms
.is_series()
.then(crate::transport::SeriesStanding::opening);
for (peer, slot) in plan.iter().enumerate() {
transport.send_to(
peer,
NetMsg::Start {
seats,
seat: *slot,
terms,
names: wire_names,
kinds: wire_kinds,
standing,
beach: beach.clone(),
},
);
}
let mut session = OnlineSession::new(
transport,
crate::app::net::lockstep_for(Some(0), humans),
seats,
terms,
);
session.beach = beach;
session.peers = peers;
session.peers.deal(&plan);
session.names = names;
session.kinds = kinds;
let spoken: Vec<u8> = bots_here.iter().map(|(seat, ..)| *seat).collect();
session.session.speak_for(&spoken);
session.bots_here = bots_here;
session.stay_on_air(announcer);
session.home.from_lobby = true;
session.home.game_name = state.game_name.clone();
session.home.bots_welcome = config.bots_welcome;
let series = crate::app::tournament::Tournament::from_terms(terms, standing);
arena.walk_in(session, series);
}
fn note_arrivals_and_departures(
state: &mut LobbyState,
tr: &'static crate::app::i18n::Tr,
greeted: Vec<(usize, String)>,
) {
for (from, told) in greeted {
let Some(hosted) = state.hosted_mut() else {
return;
};
let row = hosted.peers.row(from);
let arrived = row.name != told;
let newcomer = row.name.is_empty();
let watching = row.watch;
row.name.clone_from(&told);
if newcomer {
catch_up_on_the_feed(state, from);
}
if arrived {
let line = match watching {
true => tr.lobby_watching_joined,
false => tr.lobby_joined,
};
let notice = fill(line, &[("p", &told)]);
state.say("", ¬ice);
announce_to_table(state, "", ¬ice);
}
}
let gone: Vec<usize> = state.hosted().map_or(Vec::new(), |hosted| {
(0..hosted.peers.len())
.rev()
.filter(|peer| {
hosted
.peers
.get(*peer)
.is_some_and(|p| p.silence > PEER_TTL)
})
.collect()
});
for peer in gone {
let Some(hosted) = state.hosted_mut() else {
return;
};
let who = hosted
.peers
.get(peer)
.map_or_else(String::new, |p| p.name.clone());
hosted.forget_peer(peer);
if !who.is_empty() {
let notice = fill(tr.lobby_left, &[("p", &who)]);
state.say("", ¬ice);
announce_to_table(state, "", ¬ice);
}
}
}
#[derive(Default)]
struct Picked {
announced: bool,
greeted: Vec<(usize, String)>,
unwelcome: Vec<usize>,
said: Vec<(
usize,
crate::transport::WireName,
crate::transport::WireChat,
)>,
}
fn work_the_socket(hosted: &mut Hosted, delta: f32, on_air: crate::transport::OnAir<'_>) -> Picked {
let Hosted {
announcer,
transport,
peers,
announce_in,
} = hosted;
let mut picked = Picked {
announced: once_a_second(announce_in, delta),
..Picked::default()
};
peers.age(delta);
if picked.announced
&& let Ok(addr) = transport.local_addr()
{
announcer.announce(addr.port(), on_air);
}
for (msg, from) in transport.recv_all() {
peers.reach(transport.peer_count());
peers.heard(from);
match msg {
NetMsg::Watch { name } => {
peers.row(from).watch = true;
let told = crate::transport::name_from_wire(&name);
if !told.is_empty() {
picked.greeted.push((from, told));
}
}
NetMsg::SpectatorVote { .. } | NetMsg::SpectatorTally { .. } => {}
NetMsg::Hello { bot: true, .. } if !on_air.bots => {
if !picked.unwelcome.contains(&from) {
picked.unwelcome.push(from);
}
}
NetMsg::Hello { name, bot } => {
peers.row(from).bot = bot;
let told = crate::transport::name_from_wire(&name);
if !told.is_empty() {
picked.greeted.push((from, told));
}
}
NetMsg::Chat { name, text } => {
let known = peers.get(from).map_or("", |peer| peer.name.as_str());
let who = match known.is_empty() {
false => crate::transport::wire_name(known),
true => name,
};
if !crate::transport::name_from_wire(&who).is_empty() {
picked.said.push((from, who, text));
}
}
NetMsg::Inputs(_)
| NetMsg::Hash { .. }
| NetMsg::Start { .. }
| NetMsg::Pause { .. }
| NetMsg::Resume { .. }
| NetMsg::Queued { .. }
| NetMsg::Roster { .. }
| NetMsg::Abandoned { .. }
| NetMsg::CatchUp { .. }
| NetMsg::SpectatorPick { .. }
| NetMsg::CrowdPicks { .. }
| NetMsg::Incompatible { .. }
| NetMsg::Kicked => {}
}
}
picked.unwelcome.sort_unstable();
for &peer in picked.unwelcome.iter().rev() {
transport.turn_away(peer);
peers.forget(peer);
}
let unwelcome = &picked.unwelcome;
let moved = |from: usize| from - unwelcome.iter().filter(|&&gone| gone < from).count();
picked.greeted.retain(|(from, _)| !unwelcome.contains(from));
picked.said.retain(|(from, ..)| !unwelcome.contains(from));
for (from, _) in &mut picked.greeted {
*from = moved(*from);
}
for (from, ..) in &mut picked.said {
*from = moved(*from);
}
for (from, name, text) in picked.said.iter() {
crate::app::net::relay(
transport,
*from,
NetMsg::Chat {
name: *name,
text: *text,
},
);
}
picked
}
pub(super) fn gathering_feedback(
tr: &'static crate::app::i18n::Tr,
config: &MatchConfig,
players_aboard: usize,
watching: usize,
) -> String {
let mut line = match players_aboard {
1 => tr.lobby_rivals_one.to_string(),
n => fill(tr.lobby_rivals_many, &[("n", &n.to_string())]),
};
if watching > 0 {
line += &fill(tr.lobby_watchers, &[("n", &watching.to_string())]);
}
let bots = config
.bots
.min((MAX_PLAYERS as u8).saturating_sub((players_aboard + 1) as u8));
if bots > 0 {
line += &fill(tr.lobby_ai_seats, &[("n", &bots.to_string())]);
}
line
}
pub(super) fn should_launch(
joined: usize,
typing: bool,
enter: bool,
quota: Option<usize>,
) -> bool {
if joined == 0 {
return false;
}
match quota {
Some(quota) if joined >= quota => true,
_ => enter && !typing,
}
}
fn catch_up_on_the_feed(state: &LobbyState, peer: usize) {
let Some(hosted) = state.hosted() else {
return;
};
for said in &state.chat {
hosted
.transport
.send_to(peer, NetMsg::chat(&said.who, &said.line));
}
}
pub(super) fn ask_to_leave(
state: &mut LobbyState,
bots: &mut crate::app::bot_seats::BotSeats,
tr: &'static crate::app::i18n::Tr,
at: usize,
) {
let Some(hosted) = state.hosted_mut() else {
return;
};
let people: Vec<usize> = (0..hosted.peers.len())
.filter(|&peer| hosted.peers.get(peer).is_some_and(|p| !p.watch))
.collect();
if let Some(bot) = at.checked_sub(2 + people.len())
&& let Some(door) = &mut bots.door
&& let Some((id, who)) = door.arrived(tr).get(bot).cloned()
{
door.let_go(id);
let notice = fill(tr.lobby_kicked_feed, &[("p", &who)]);
state.say("", ¬ice);
announce_to_table(state, "", ¬ice);
return;
}
let Some(&peer) = people.get(at.wrapping_sub(2)) else {
return;
};
let who = hosted
.peers
.get(peer)
.map_or_else(String::new, |p| p.name.clone());
hosted.turn_away(peer);
let notice = fill(tr.lobby_kicked_feed, &[("p", &who)]);
state.say("", ¬ice);
announce_to_table(state, "", ¬ice);
}
pub(super) fn announce_to_table(state: &LobbyState, who: &str, line: &str) {
if let Some(hosted) = state.hosted() {
hosted.transport.send(NetMsg::chat(who, line));
}
}
pub(super) fn seat_plan(peers: &PeerBook) -> Vec<Option<u8>> {
let mut next = 1u8; peers
.iter()
.map(|peer| {
if peer.watch || usize::from(next) >= MAX_PLAYERS {
return None;
}
let seat = next;
next += 1;
Some(seat)
})
.collect()
}
pub(super) fn say_goodbye(state: &LobbyState) {
if let Some(hosted) = state.hosted()
&& let Ok(addr) = hosted.transport.local_addr()
{
hosted.announcer.closing(addr.port());
}
}
#[cfg(test)]
mod tests {
use super::*;
fn table(n: usize, watchers: &[usize]) -> PeerBook {
let names = vec![""; n];
peers_named(&names, watchers)
}
#[test]
fn the_table_is_dealt_in_arrival_order_behind_the_host() {
assert_eq!(seat_plan(&table(0, &[])), Vec::<Option<u8>>::new());
assert_eq!(
seat_plan(&table(1, &[])),
vec![Some(1)],
"the host keeps zero"
);
assert_eq!(seat_plan(&table(3, &[])), vec![Some(1), Some(2), Some(3)]);
}
#[test]
fn watchers_are_stepped_over_and_the_seats_close_up() {
assert_eq!(seat_plan(&table(3, &[0])), vec![None, Some(1), Some(2)]);
assert_eq!(seat_plan(&table(3, &[1])), vec![Some(1), None, Some(2)]);
assert_eq!(seat_plan(&table(3, &[0, 1, 2])), vec![None, None, None]);
}
#[test]
fn hosting_asks_before_it_announces() {
assert_eq!(host_step(idle()), HostStep::Nothing);
assert_eq!(
host_step(HostAsk {
pressed_h: true,
..idle()
}),
HostStep::Ask
);
assert_eq!(
host_step(HostAsk {
answered: true,
..idle()
}),
HostStep::Go
);
assert_eq!(
host_step(HostAsk {
pressed_h: true,
busy: true,
..idle()
}),
HostStep::Nothing
);
assert_eq!(
host_step(HostAsk {
busy: true,
..idle()
}),
HostStep::Nothing
);
assert_eq!(
host_step(HostAsk {
auto: true,
..idle()
}),
HostStep::Go
);
assert_eq!(
host_step(HostAsk {
pressed_h: true,
auto: true,
..idle()
}),
HostStep::Go
);
}
#[test]
fn the_table_runs_out_of_chairs_before_the_socket_runs_out_of_peers() {
use crate::sim::MAX_PLAYERS;
assert_eq!(
seat_plan(&table(4, &[])),
vec![Some(1), Some(2), Some(3), Some(4)]
);
assert_eq!(
seat_plan(&table(4, &[1, 3])),
vec![Some(1), None, Some(2), None]
);
let full = seat_plan(&table(crate::transport::MAX_PEERS, &[]));
assert_eq!(
full.iter().filter(|seat| seat.is_some()).count(),
MAX_PLAYERS - 1,
"the host holds the sixth chair"
);
assert!(
full.iter()
.flatten()
.all(|&seat| usize::from(seat) < MAX_PLAYERS),
"no peer is given a seat the sim has no slot for"
);
assert!(full.last().unwrap().is_none(), "the late ones watch");
}
#[test]
fn a_half_written_sentence_does_not_start_the_match() {
assert!(should_launch(1, false, true, None));
assert!(
!should_launch(1, false, false, None),
"and nothing without it"
);
assert!(
!should_launch(0, false, true, None),
"nor with an empty table"
);
assert!(!should_launch(1, true, true, None));
assert!(!should_launch(3, true, true, None));
assert!(should_launch(2, false, false, Some(2)));
assert!(should_launch(2, true, false, Some(2)));
assert!(!should_launch(1, false, false, Some(2)), "not filled yet");
}
#[test]
fn a_peer_asked_to_leave_is_told_and_forgotten() {
let mut state = LobbyState {
standing: Standing::hosting(
Announcer::new(0xC0FFEE).expect("announcer"),
UdpTransport::host(0).expect("game socket"),
),
..LobbyState::default()
};
let port = state
.hosted()
.expect("hosting")
.transport
.local_addr()
.expect("addr")
.port();
let mut bot = UdpTransport::join(("127.0.0.1", port)).expect("join");
bot.send(NetMsg::hello_bot("Greedy (Ana)"));
for _ in 0..40 {
std::thread::sleep(std::time::Duration::from_millis(5));
let hosted = state.hosted_mut().expect("hosting");
let welcome = crate::transport::OnAir {
bots: true,
full: false,
..crate::transport::OnAir::default()
};
let picked = work_the_socket(hosted, 0.0, welcome);
for (from, told) in picked.greeted {
hosted.peers.row(from).name = told;
}
if hosted
.peers
.get(0)
.is_some_and(|peer| peer.name == "Greedy (Ana)")
{
break;
}
}
assert!(
state
.hosted()
.expect("hosting")
.peers
.get(0)
.is_some_and(|p| p.bot),
"the greeting said a bot drives this seat"
);
assert_eq!(
state.roster_kinds(),
vec![crate::sim::SeatKind::Human, crate::sim::SeatKind::Bot]
);
ask_to_leave(
&mut state,
&mut crate::app::bot_seats::BotSeats::default(),
&crate::app::i18n::EN,
2,
);
assert_eq!(state.hosted().expect("hosting").peers.len(), 0, "forgotten");
assert!(
state
.chat
.iter()
.any(|said| said.line.contains("asked to leave"))
);
let mut told = false;
for _ in 0..40 {
std::thread::sleep(std::time::Duration::from_millis(5));
told |= bot
.recv_all()
.into_iter()
.any(|(msg, _)| msg == NetMsg::Kicked);
if told {
break;
}
}
assert!(told, "the peer was told why");
}
#[test]
fn a_bot_is_not_seated_where_bots_are_not_welcome() {
let mut state = LobbyState {
standing: Standing::hosting(
Announcer::new(0xB07).expect("announcer"),
UdpTransport::host(0).expect("game socket"),
),
..LobbyState::default()
};
let port = state
.hosted()
.expect("hosting")
.transport
.local_addr()
.expect("addr")
.port();
let mut bot = UdpTransport::join(("127.0.0.1", port)).expect("join");
let person = UdpTransport::join(("127.0.0.1", port)).expect("join");
bot.send(NetMsg::hello_bot("Greedy (Ana)"));
std::thread::sleep(std::time::Duration::from_millis(20));
person.send(NetMsg::hello("Bo"));
let unwelcome = crate::transport::OnAir {
bots: false,
full: false,
..crate::transport::OnAir::default()
};
for _ in 0..40 {
std::thread::sleep(std::time::Duration::from_millis(5));
let hosted = state.hosted_mut().expect("hosting");
let picked = work_the_socket(hosted, 0.0, unwelcome);
for (from, told) in picked.greeted {
hosted.peers.row(from).name = told;
}
if hosted.peers.get(0).is_some_and(|peer| peer.name == "Bo") {
break;
}
}
let hosted = state.hosted().expect("hosting");
assert_eq!(hosted.peers.len(), 1, "only the person is at the table");
assert_eq!(hosted.transport.peer_count(), 1);
assert!(
hosted
.peers
.get(0)
.is_some_and(|p| p.name == "Bo" && !p.bot)
);
let mut told = false;
for _ in 0..40 {
std::thread::sleep(std::time::Duration::from_millis(5));
told |= bot
.recv_all()
.into_iter()
.any(|(msg, _)| msg == NetMsg::Kicked);
if told {
break;
}
}
assert!(told, "the bot was told to go");
}
#[test]
fn a_peer_speaks_under_the_name_it_greeted_with() {
let mut state = LobbyState {
standing: Standing::hosting(
Announcer::new(0xC0FFEE).expect("announcer"),
UdpTransport::host(0).expect("game socket"),
),
..LobbyState::default()
};
let port = state
.hosted()
.expect("hosting")
.transport
.local_addr()
.expect("addr")
.port();
let bo = UdpTransport::join(("127.0.0.1", port)).expect("join");
bo.send(NetMsg::hello("Bo"));
let mut said = Vec::new();
for _ in 0..40 {
std::thread::sleep(std::time::Duration::from_millis(5));
let hosted = state.hosted_mut().expect("hosting");
let picked = work_the_socket(hosted, 0.0, crate::transport::OnAir::default());
for (from, told) in picked.greeted {
hosted.peers.row(from).name = told;
}
said.extend(picked.said);
if state
.hosted()
.expect("hosting")
.peers
.get(0)
.is_some_and(|peer| peer.name == "Bo")
{
break;
}
}
assert_eq!(
state
.hosted()
.expect("hosting")
.peers
.get(0)
.map(|p| p.name.as_str()),
Some("Bo"),
"the greeting is on file"
);
bo.send(NetMsg::chat("Bo", "ready?"));
bo.send(NetMsg::chat("Anna", "I am Anna, honest"));
bo.send(NetMsg::chat("", "Anna has left the beach"));
said.clear();
for _ in 0..40 {
std::thread::sleep(std::time::Duration::from_millis(5));
let hosted = state.hosted_mut().expect("hosting");
said.extend(work_the_socket(hosted, 0.0, crate::transport::OnAir::default()).said);
if said.len() >= 2 {
break;
}
}
let heard: Vec<(String, String)> = said
.iter()
.map(|(_, name, text)| {
(
crate::transport::name_from_wire(name),
crate::transport::chat_from_wire(text),
)
})
.collect();
assert_eq!(
heard,
[
("Bo".to_string(), "ready?".to_string()),
("Bo".to_string(), "I am Anna, honest".to_string()),
("Bo".to_string(), "Anna has left the beach".to_string()),
],
"every line under the name Bo greeted with: no rival's name to \
wear, and no forging the room's"
);
}
#[test]
fn a_late_arrival_is_shown_what_the_room_already_said() {
let mut state = LobbyState {
standing: Standing::hosting(
Announcer::new(0xC0FFEE).expect("announcer"),
UdpTransport::host(0).expect("game socket"),
),
..LobbyState::default()
};
state.say("Ann", "anyone up for a round?");
state.say("", "Bo joined");
let port = state
.hosted()
.expect("hosting")
.transport
.local_addr()
.expect("addr")
.port();
let mut latecomer = UdpTransport::join(("127.0.0.1", port)).expect("join");
latecomer.send(NetMsg::hello("Cy"));
let mut greeted = Vec::new();
for _ in 0..40 {
std::thread::sleep(std::time::Duration::from_millis(5));
let hosted = state.hosted_mut().expect("hosting");
greeted
.extend(work_the_socket(hosted, 0.0, crate::transport::OnAir::default()).greeted);
if !greeted.is_empty() {
break;
}
}
assert_eq!(greeted, [(0, "Cy".to_string())], "Cy is at the door");
note_arrivals_and_departures(&mut state, &crate::app::i18n::EN, greeted);
let mut heard = Vec::new();
for _ in 0..40 {
std::thread::sleep(std::time::Duration::from_millis(5));
for (msg, _) in latecomer.recv_all() {
if let NetMsg::Chat { name, text } = msg {
heard.push((
crate::transport::name_from_wire(&name),
crate::transport::chat_from_wire(&text),
));
}
}
if heard.len() >= 3 {
break;
}
}
assert_eq!(
heard,
[
("Ann".to_string(), "anyone up for a round?".to_string()),
(String::new(), "Bo joined".to_string()),
(String::new(), "Cy joined".to_string()),
],
"the room as it stands, oldest first, and then their own arrival"
);
}
#[test]
fn a_spectator_is_announced_by_name_and_then_held_to_it() {
let mut state = LobbyState {
standing: Standing::hosting(
Announcer::new(0xDECAF).expect("announcer"),
UdpTransport::host(0).expect("game socket"),
),
..LobbyState::default()
};
let port = state
.hosted()
.expect("hosting")
.transport
.local_addr()
.expect("addr")
.port();
let watcher = UdpTransport::join(("127.0.0.1", port)).expect("join");
watcher.send(NetMsg::watch("Dee"));
let mut greeted = Vec::new();
for _ in 0..40 {
std::thread::sleep(std::time::Duration::from_millis(5));
let hosted = state.hosted_mut().expect("hosting");
greeted
.extend(work_the_socket(hosted, 0.0, crate::transport::OnAir::default()).greeted);
if !greeted.is_empty() {
break;
}
}
assert_eq!(
greeted,
[(0, "Dee".to_string())],
"a spectator says what to call it, like everybody else"
);
note_arrivals_and_departures(&mut state, &crate::app::i18n::EN, greeted);
assert!(
state.chat.iter().any(|said| said.line == "Dee is watching"),
"and the room is told, in the words for watching rather than \
for taking a chair: {:?}",
state.chat
);
watcher.send(NetMsg::chat("", "Anna has left the beach"));
watcher.send(NetMsg::chat("Cy", "good luck!"));
let mut said = Vec::new();
for _ in 0..40 {
std::thread::sleep(std::time::Duration::from_millis(5));
let hosted = state.hosted_mut().expect("hosting");
said.extend(work_the_socket(hosted, 0.0, crate::transport::OnAir::default()).said);
if !said.is_empty() {
break;
}
}
let heard: Vec<(String, String)> = said
.iter()
.map(|(_, name, text)| {
(
crate::transport::name_from_wire(name),
crate::transport::chat_from_wire(text),
)
})
.collect();
assert_eq!(
heard,
[
("Dee".to_string(), "Anna has left the beach".to_string()),
("Dee".to_string(), "good luck!".to_string()),
],
"both lines under the name it greeted with: no rival's name to \
wear, and the room's own voice no longer on offer either, \
which it was for as long as a spectator had no name to give \
back"
);
}
}