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,
}
}
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,
}
}
#[allow(clippy::too_many_arguments)]
pub fn host_tick(
time: Res<Time>,
keys: Res<ButtonInput<KeyCode>>,
settings: Res<GameSettings>,
config: Res<crate::app::match_setup::MatchConfig>,
beaches: Res<crate::app::match_setup::CustomBeaches>,
mut state: ResMut<LobbyState>,
mut online: ResMut<Online>,
mut tournament: ResMut<crate::app::tournament::Tournament>,
mut next_screen: ResMut<NextState<Screen>>,
mut next_vphase: ResMut<NextState<VersusPhase>>,
) {
if state.hosting.is_none() {
return;
}
let tr = settings.tr();
state.announce_in -= time.delta_secs();
let do_announce = state.announce_in <= 0.0;
if do_announce {
state.announce_in = ANNOUNCE_EVERY;
}
let taken = 1 + state.players_aboard() as u8;
let game_name = state.game_name.clone();
let mut watchers = std::mem::take(&mut state.watchers);
let mut picked = Picked::default();
let mut silence = std::mem::take(&mut state.peer_silence);
let delta = time.delta_secs();
for since in silence.iter_mut() {
*since += delta;
}
let on_air = crate::transport::OnAir {
name: &game_name,
host: &settings.names[0],
taken,
seats: MAX_PLAYERS as u8,
};
work_the_socket(
&mut state,
do_announce,
on_air,
&mut watchers,
&mut silence,
&mut picked,
);
for (_, name, text) in picked.said {
let who = crate::transport::name_from_wire(&name);
let line = crate::transport::chat_from_wire(&text);
state.say(&who, &line);
}
note_arrivals_and_departures(&mut state, tr, picked.greeted, silence);
let joined = match &state.hosting {
Some((_, transport)) => transport.peer_count(),
None => 0,
};
state.watchers = watchers;
state.joined_peers = joined;
state.table = state.roster(tr, &settings.names[0]);
if do_announce && let Some((_, transport)) = &state.hosting {
let mut names = [[0u8; crate::transport::WIRE_NAME]; MAX_PLAYERS];
for (slot, who) in names.iter_mut().zip(&state.table) {
*slot = crate::transport::wire_name(who);
}
let terms = crate::app::match_setup::terms(&config, settings.team_mode, 0);
transport.send(NetMsg::Roster {
seats: state.table.len().min(MAX_PLAYERS) as u8,
names,
terms,
});
}
let players_aboard = state.players_aboard();
if joined > 0 {
state.feedback = gathering_feedback(tr, &config, players_aboard, state.watchers.len());
} else {
state.feedback = match &state.hosting {
Some((_, transport)) => match transport.local_addr() {
Ok(addr) => fill(tr.lobby_hosting, &[("p", &addr.port().to_string())]),
Err(_) => tr.lobby_hosting_noport.to_string(),
},
None => tr.lobby_hosting_noport.to_string(),
};
}
let quota = crate::app::dev::auto_host_quota();
let launch = should_launch(
joined,
state.typing.is_some(),
keys.just_pressed(KeyCode::Enter),
quota,
);
if launch {
launch_the_match(
&mut state,
&settings,
&config,
&beaches,
joined,
&mut online,
&mut tournament,
&mut next_screen,
&mut next_vphase,
);
}
}
#[allow(clippy::too_many_arguments)]
fn launch_the_match(
state: &mut LobbyState,
settings: &GameSettings,
config: &MatchConfig,
beaches: &crate::app::match_setup::CustomBeaches,
joined: usize,
online: &mut Online,
tournament: &mut crate::app::tournament::Tournament,
next_screen: &mut NextState<Screen>,
next_vphase: &mut NextState<VersusPhase>,
) {
let watchers = state.watchers.clone();
let (announcer, transport) = state.hosting.take().expect("checked above");
let plan = seat_plan(joined, &watchers);
let humans = 1 + plan.iter().filter(|seat| seat.is_some()).count() 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(name) = state.peer_names.get(peer)
{
names[usize::from(*seat)] = name.clone();
}
}
let wire_names = std::array::from_fn(|i| crate::transport::wire_name(&names[i]));
let (round, wins) = match terms.series == 1 {
true => (1u8, [0u8; MAX_PLAYERS]),
false => (0, [0; MAX_PLAYERS]),
};
for (peer, slot) in plan.iter().enumerate() {
transport.send_to(
peer,
NetMsg::Start {
seats,
seat: *slot,
terms,
names: wire_names,
round,
wins,
beach: beach.clone(),
},
);
}
let peer_seats = plan;
let mut session = OnlineSession::new(
transport,
Lockstep::new(0, (0..humans).collect(), DEFAULT_DELAY),
seats,
terms,
);
session.beach = beach;
session.peer_seats = peer_seats;
session.names = names;
session.announcer = Some(announcer);
session.game_name = state.game_name.clone();
online.0 = Some(session);
*tournament = match terms.series == 1 {
true => crate::app::tournament::Tournament::start(),
false => crate::app::tournament::Tournament::default(),
};
next_vphase.set(VersusPhase::Running);
next_screen.set(Screen::Versus);
}
fn note_arrivals_and_departures(
state: &mut LobbyState,
tr: &'static crate::app::i18n::Tr,
greeted: Vec<(usize, String)>,
silence: Vec<f32>,
) {
for (from, told) in greeted {
if state.peer_names.len() <= from {
state.peer_names.resize(from + 1, String::new());
}
let arrived = state.peer_names[from] != told;
state.peer_names[from].clone_from(&told);
if arrived {
let notice = fill(tr.lobby_joined, &[("p", &told)]);
state.say("", ¬ice);
announce_to_table(state, "", ¬ice);
}
}
state.peer_silence = silence;
if state.peer_names.len() < state.peer_silence.len() {
state
.peer_names
.resize(state.peer_silence.len(), String::new());
}
let gone: Vec<usize> = (0..state.peer_silence.len())
.rev()
.filter(|peer| state.peer_silence[*peer] > PEER_TTL)
.collect();
for peer in gone {
let who = state.peer_names.get(peer).cloned().unwrap_or_default();
state.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 {
greeted: Vec<(usize, String)>,
said: Vec<(
usize,
crate::transport::WireName,
crate::transport::WireChat,
)>,
}
#[cfg_attr(debug_assertions, track_caller)]
fn work_the_socket(
state: &mut LobbyState,
do_announce: bool,
on_air: crate::transport::OnAir<'_>,
watchers: &mut Vec<usize>,
silence: &mut Vec<f32>,
picked: &mut Picked,
) {
let Picked { greeted, said } = picked;
debug_assert!(
state.hosting.is_some(),
"working a socket that is not being hosted"
);
let mut heard_from: Vec<usize> = Vec::new();
if let Some((announcer, transport)) = &mut state.hosting {
if do_announce && let Ok(addr) = transport.local_addr() {
announcer.announce(addr.port(), on_air);
}
for (msg, from) in transport.recv_all() {
heard_from.push(from);
while silence.len() < transport.peer_count() {
silence.push(0.0);
}
match msg {
NetMsg::Watch if !watchers.contains(&from) => watchers.push(from),
NetMsg::Hello { name } => {
let told = crate::transport::name_from_wire(&name);
if !told.is_empty() {
greeted.push((from, told));
}
}
NetMsg::Chat { name, text } => said.push((from, name, text)),
NetMsg::Watch
| NetMsg::Input(_)
| NetMsg::Hash { .. }
| NetMsg::Start { .. }
| NetMsg::Pause { .. }
| NetMsg::Resume { .. }
| NetMsg::Queued { .. }
| NetMsg::Roster { .. }
| NetMsg::Abandoned { .. }
| NetMsg::Incompatible { .. } => {}
}
}
for heard in &heard_from {
if let Some(silence) = silence.get_mut(*heard) {
*silence = 0.0;
}
}
for (from, name, text) in said.iter() {
for other in 0..transport.peer_count() {
if other != *from {
transport.send_to(
other,
NetMsg::Chat {
name: *name,
text: *text,
},
);
}
}
}
}
}
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,
}
}
pub(super) fn announce_to_table(state: &LobbyState, who: &str, line: &str) {
if let Some((_, transport)) = &state.hosting {
transport.send(NetMsg::Chat {
name: crate::transport::wire_name(who),
text: crate::transport::wire_chat(line),
});
}
}
pub(super) fn seat_plan(peers: usize, watchers: &[usize]) -> Vec<Option<u8>> {
debug_assert!(
watchers.iter().all(|w| *w < peers.max(1)),
"a watcher at {watchers:?} among {peers} peers"
);
let mut next = 1u8; (0..peers)
.map(|peer| {
if watchers.contains(&peer) || 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((announcer, transport)) = &state.hosting
&& let Ok(addr) = transport.local_addr()
{
announcer.closing(addr.port());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_table_is_dealt_in_arrival_order_behind_the_host() {
assert_eq!(seat_plan(0, &[]), Vec::<Option<u8>>::new());
assert_eq!(seat_plan(1, &[]), vec![Some(1)], "the host keeps zero");
assert_eq!(seat_plan(3, &[]), vec![Some(1), Some(2), Some(3)]);
}
#[test]
fn watchers_are_stepped_over_and_the_seats_close_up() {
assert_eq!(seat_plan(3, &[0]), vec![None, Some(1), Some(2)]);
assert_eq!(seat_plan(3, &[1]), vec![Some(1), None, Some(2)]);
assert_eq!(seat_plan(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(4, &[]), vec![Some(1), Some(2), Some(3), Some(4)]);
assert_eq!(seat_plan(4, &[1, 3]), vec![Some(1), None, Some(2), None]);
let full = seat_plan(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");
}
}