use super::*;
use std::sync::{Arc, OnceLock};
pub(in crate::app) fn bot_seats(config: &match_setup::MatchConfig) -> Controllers {
let mut table = Controllers::default();
if config.armed {
for seat in 0..config.seats {
table.0[usize::from(seat)] = config.controller(seat);
}
}
table
}
pub(super) enum RoundOrigin<'a> {
Resumed(Box<suspend::Suspended>),
Replay(&'a Replay),
Online(&'a net::OnlineSession),
Configured(&'a match_setup::MatchConfig),
Unconfigured,
}
impl RoundOrigin<'_> {
pub(super) fn board(
&self,
daily: Option<u32>,
beaches: &match_setup::CustomBeaches,
sandbox: bool,
pads: u8,
) -> Board {
match self {
RoundOrigin::Resumed(round) => round.board.clone(),
RoundOrigin::Replay(replay) => replay.level.board(),
RoundOrigin::Online(session) => match &session.catch_up.board {
Some(board) => board.clone(),
None => match_setup::board_from(&session.terms, session.seats, &session.beach),
},
RoundOrigin::Configured(config) => {
let seed = daily.map_or_else(
|| config.seed.unwrap_or_else(clock::fresh_seed),
Daily::seed_for,
);
match_setup::local_board(config, beaches, seed)
}
RoundOrigin::Unconfigured => classic_arena(sandbox, pads.max(2)),
}
}
pub(super) fn table(
&self,
config: &match_setup::MatchConfig,
board: &Board,
pads: u8,
) -> (Controllers, u8) {
let controllers = match self {
RoundOrigin::Resumed(round) => {
return (Controllers::from_levels(round.bots), round.seats);
}
RoundOrigin::Online(session) => online_controllers(session),
RoundOrigin::Replay(_) | RoundOrigin::Configured(_) | RoundOrigin::Unconfigured => {
bot_seats(config)
}
};
(controllers, clamp_seats(self.asked_seats(board, pads)))
}
pub(super) fn asked_seats(&self, board: &Board, pads: u8) -> u8 {
match self {
RoundOrigin::Resumed(round) => round.seats,
RoundOrigin::Replay(_) => replay_seats(board),
RoundOrigin::Online(session) => session.seats,
RoundOrigin::Configured(config) => config.seats,
RoundOrigin::Unconfigured => pads.max(2),
}
}
pub(super) fn counts_in(&self) -> bool {
self.recorded()
}
pub(super) fn opens_on_lineup(&self, set_here: bool) -> bool {
let lineup = match self {
RoundOrigin::Online(_) => true,
RoundOrigin::Configured(_) => set_here,
RoundOrigin::Resumed(_) | RoundOrigin::Replay(_) | RoundOrigin::Unconfigured => false,
};
lineup && self.counts_in()
}
pub(super) fn recorded(&self) -> bool {
match self {
RoundOrigin::Resumed(_) | RoundOrigin::Replay(_) => false,
RoundOrigin::Online(session) => session.catch_up.board.is_none(),
RoundOrigin::Configured(_) | RoundOrigin::Unconfigured => true,
}
}
}
pub(super) fn online_controllers(session: &net::OnlineSession) -> Controllers {
let ai = match_setup::bot_seats_from(&session.terms, session.seats);
let mine = session.session.seat().map(usize::from);
let driven = |seat: usize| {
(Some(seat) == mine && session.bot.is_some())
|| session
.bots_here
.iter()
.any(|bot| usize::from(bot.seat) == seat)
};
Controllers(std::array::from_fn(|seat| match ai[seat] {
Some(level) => SeatController::Ai(level),
None if driven(seat) => SeatController::Bot,
None if Some(seat) == mine => SeatController::Local,
None => SeatController::Remote,
}))
}
pub(in crate::app) fn replay_seats(board: &Board) -> u8 {
clamp_seats(board.seats_in_play())
}
pub(in crate::app) fn clamp_seats(asked: u8) -> u8 {
asked.clamp(2, MAX_PLAYERS as u8)
}
#[derive(bevy::ecs::system::SystemParam)]
pub(in crate::app) struct RoundSource<'w, 's> {
pub(in crate::app) pads: Query<'w, 's, &'static Gamepad>,
pub(in crate::app) sandbox: Res<'w, Sandbox>,
pub(in crate::app) online: Res<'w, net::Online>,
pub(in crate::app) playback: Res<'w, Playback>,
pub(in crate::app) config: Res<'w, match_setup::MatchConfig>,
pub(in crate::app) beaches: Res<'w, match_setup::CustomBeaches>,
pub(in crate::app) daily: ResMut<'w, Daily>,
pub(in crate::app) resuming: ResMut<'w, Resuming>,
pub(in crate::app) table: ResMut<'w, rematch::LocalTable>,
pub(in crate::app) tournament: Res<'w, tournament::Tournament>,
}
pub(in crate::app) fn load_versus(
mut stage: BoardStage,
mut source: RoundSource,
mut controllers: ResMut<Controllers>,
mut seats: ResMut<Seats>,
mut recorder: ResMut<Recorder>,
play: Play,
mut opening: countdown::Opening,
) {
let Play {
mut sim,
mut pending,
mut paused,
} = play;
let RoundSource {
pads,
sandbox,
online,
playback,
config,
beaches,
daily,
resuming,
table,
tournament,
} = &mut source;
let daily_config = match_setup::MatchConfig::daily();
let config: &match_setup::MatchConfig = if daily.active { &daily_config } else { config };
let pad_count = pads.iter().count() as u8;
let origin = if let Some(round) = resuming.0.take() {
RoundOrigin::Resumed(Box::new(round))
} else if let Some((replay, _)) = &playback.0 {
RoundOrigin::Replay(replay)
} else if let Some(session) = &online.0 {
RoundOrigin::Online(session)
} else if config.armed {
RoundOrigin::Configured(config)
} else {
RoundOrigin::Unconfigured
};
let daily_day =
(daily.active && matches!(origin, RoundOrigin::Configured(_))).then(Daily::today);
if let Some(day) = daily_day {
daily.day = day;
}
let set_here = matches!(origin, RoundOrigin::Configured(_)) && daily_day.is_none();
table.note_round(set_here, config, tournament);
sim.0 = origin.board(daily_day, beaches, sandbox.0, pad_count);
(*controllers, seats.0) = origin.table(config, &sim.0, pad_count);
debug_assert!(
(2..=MAX_PLAYERS as u8).contains(&seats.0),
"a table of {} seats",
seats.0
);
recorder.0 = origin
.recorded()
.then(|| Replay::new(Level::from_board("Versus", 3, sim.0.clone())));
stage.lay_out(&sim.0);
pending.0 = [PlayerAction::None; MAX_PLAYERS];
for (mut cur, mut transform) in &mut stage.cursors {
(cur.x, cur.y) = cursor_home(&sim.0, cur.player);
transform.translation = layout::tile_center(&sim.0, cur.x, cur.y).extend(layout::z::CURSOR);
}
paused.0 = false;
opening.open(origin.counts_in(), origin.opens_on_lineup(set_here));
}
pub(in crate::app) fn forget_a_round_not_entered(
mut transitions: MessageReader<StateTransitionEvent<Screen>>,
mut resuming: ResMut<Resuming>,
mut playback: ResMut<Playback>,
mut daily: ResMut<Daily>,
mut again: rematch::PlayAgain,
) {
let Some(entered) = transitions.read().last().map(|t| t.entered) else {
return;
};
if entered != Some(Screen::Versus) {
resuming.0 = None;
playback.0 = None;
daily.active = false;
again.let_go();
}
}
#[derive(bevy::ecs::system::SystemParam)]
pub(in crate::app) struct RoundLog<'w> {
recorder: ResMut<'w, Recorder>,
reel_thread: ResMut<'w, ReelThread>,
keep: ResMut<'w, replays::RoundToKeep>,
}
pub(in crate::app) fn end_versus(
screen: Res<State<Screen>>,
mut online: ResMut<net::Online>,
mut playback: ResMut<Playback>,
log: RoundLog,
mut pending: ResMut<PendingActions>,
mut controllers: ResMut<Controllers>,
again: rematch::PlayAgain,
) {
let RoundLog {
mut recorder,
mut reel_thread,
mut keep,
} = log;
let rematch::PlayAgain {
table,
mut config,
mut next_vphase,
beaches: _,
} = again;
if !online.0.as_ref().is_some_and(|session| session.next_round) {
online.0 = None;
}
playback.0 = None;
recorder.0 = None;
if *screen.get() != Screen::Interlude {
*keep = replays::RoundToKeep::Nothing;
}
reel_thread.0 = None;
config.armed = table.keeps_the_table();
*controllers = Controllers::default();
pending.0 = [PlayerAction::None; MAX_PLAYERS];
next_vphase.set(VersusPhase::Countdown);
}
pub(in crate::app) fn resolve_seat_names(
scoring: teams::Scoring,
config: Res<match_setup::MatchConfig>,
controllers: Res<Controllers>,
seats: Res<Seats>,
mut recorder: ResMut<Recorder>,
mut names: ResMut<SeatNames>,
mut kinds: ResMut<SeatKinds>,
) {
let teams::Scoring {
local,
online,
playback,
} = &scoring;
let settings = &local.settings;
let couch;
let given = match (&playback.0, &online.0) {
(Some((replay, _)), _) => &replay.names,
(None, Some(session)) => &session.names,
(None, None) if local.daily.active => &settings.names,
(None, None) => {
couch = config.seat_names(&settings.names);
&couch
}
};
names.0 = given.each_ref().map(|name| crate::sim::seat_name(name));
*kinds = match (&playback.0, &online.0) {
(Some((replay, _)), _) => SeatKinds(replay.kinds),
(None, Some(session)) => SeatKinds(session.kinds),
(None, None) => SeatKinds::of(&controllers),
};
if let Some(replay) = &mut recorder.0 {
replay.names = names.0.clone();
replay.kinds = kinds.0;
replay.ai = controllers.0.map(SeatController::ai);
replay.teams = scoring.mode(seats.0).index() as u8;
}
}
#[derive(Resource, Default)]
pub struct ReelThread(Option<Arc<OnceLock<Option<String>>>>);
pub(in crate::app) fn poll_reel(
mut reel_thread: ResMut<ReelThread>,
mut highlight: ResMut<Highlight>,
) {
let Some(answer) = reel_thread.0.as_ref().and_then(|slot| slot.get().cloned()) else {
return;
};
reel_thread.0 = None;
if answer.is_some() {
highlight.0 = answer;
}
}
pub(super) fn winner_name(
sim: &Sim,
seats: &Seats,
mode: teams::TeamMode,
names: &SeatNames,
tr: &i18n::Tr,
) -> Option<String> {
teams::winner_label(sim.0.scores(), seats.0, mode, |seat| names.label(tr, seat))
}
pub(in crate::app) fn check_versus_over(
sim: Res<Sim>,
seating: seating::Seating,
scoring: teams::Scoring,
log: RoundLog,
mut highlight: ResMut<Highlight>,
mut next_vphase: ResMut<NextState<VersusPhase>>,
) {
let seating::Seating {
seats,
names: seat_names,
..
} = seating;
let settings = &scoring.local.settings;
let RoundLog {
mut recorder,
mut reel_thread,
mut keep,
} = log;
if sim.0.round_over() {
highlight.0 = None;
reel_thread.0 = None;
if let Some(mut replay) = recorder.0.take() {
replay.scores = Some(*sim.0.scores());
let text = replay.to_text();
let last = replay_path();
match paths::write_atomic(&last, &text) {
Ok(()) => info!("replay saved to {}", last.display()),
Err(e) => warn!("could not save replay: {e}"),
}
let winner = winner_name(
&sim,
&seats,
scoring.mode(seats.0),
&seat_names,
settings.tr(),
);
*keep = replays::RoundToKeep::Offered(Box::new(replays::Unkept {
text,
stamp: clock::now_secs(),
winner,
}));
let reel = highlight_path();
let answer = Arc::new(OnceLock::new());
reel_thread.0 = Some(Arc::clone(&answer));
std::thread::spawn(move || {
let saved = match crate::highlight::reel(&replay) {
Some(bytes) => match paths::write_atomic(&reel, bytes) {
Ok(()) => {
info!("highlight reel saved to {}", reel.display());
Some(reel.display().to_string())
}
Err(e) => {
warn!("could not save the highlight reel: {e}");
None
}
},
None => {
warn!("the round was too short for a highlight reel");
None
}
};
let _ = answer.set(saved);
});
}
next_vphase.set(VersusPhase::Over);
}
}
pub(in crate::app) fn follow_the_called_round(
sim: Res<Sim>,
online: Res<net::Online>,
mut next_screen: ResMut<NextState<Screen>>,
) {
if walked_on(&sim.0, &online) {
next_screen.set(Screen::Interlude);
}
}
pub(in crate::app) fn walked_on(board: &Board, online: &net::Online) -> bool {
let called = online
.0
.as_ref()
.is_some_and(|session| session.next_round && !session.is_host());
called && !board.round_over()
}
pub(in crate::app) fn note_the_crowned(
sim: Res<Sim>,
seats: Res<Seats>,
local: teams::LocalScoring,
mut online: ResMut<net::Online>,
) {
let mode = teams::in_play(local.mode(), &online, None, seats.0);
let Some(session) = online.0.as_mut().filter(|session| session.is_host()) else {
return;
};
let winners = seating::leading_seats(sim.0.scores(), seats.0, mode);
session.crowned = crate::transport::crowned_bits(&winners);
}