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(clock::fresh_seed, Daily::seed_for);
let (w, h) = config.map.size();
let mut board = if config.map == match_setup::MapChoice::Custom {
beaches
.fitting(config.seats)
.get(config.custom)
.map_or_else(
|| generate_arena(seed, config.seats, w, h),
|beach| beach.level.board(),
)
} else if config.map == match_setup::MapChoice::Classic {
classic_arena_seeded(seed, false, config.seats)
} else {
generate_arena(seed, config.seats, w, h)
};
crate::sim::set_versus_rules(&mut board, config.gulls.period());
board.set_round_length(Some(config.round.ticks()));
board.set_signpost_rule(config.posts, crate::sim::CapPolicy::Evict);
board
}
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(_) => board.castle_owners().max().unwrap_or(1).saturating_add(1),
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 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)
|| session
.bots_here
.iter()
.any(|(bot, ..)| usize::from(*bot) == 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 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) 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,
} = &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;
}
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());
}
#[derive(bevy::ecs::system::SystemParam)]
pub(in crate::app) struct RoundLog<'w> {
recorder: ResMut<'w, Recorder>,
reel_thread: ResMut<'w, ReelThread>,
}
pub(in crate::app) fn end_versus(
mut online: ResMut<net::Online>,
mut playback: ResMut<Playback>,
log: RoundLog,
mut pending: ResMut<PendingActions>,
mut config: ResMut<match_setup::MatchConfig>,
mut controllers: ResMut<Controllers>,
mut next_vphase: ResMut<NextState<VersusPhase>>,
) {
let RoundLog {
mut recorder,
mut reel_thread,
} = log;
if !online.0.as_ref().is_some_and(|session| session.next_round) {
online.0 = None;
}
playback.0 = None;
recorder.0 = None;
reel_thread.0 = None;
config.armed = false;
*controllers = Controllers::default();
pending.0 = [PlayerAction::None; MAX_PLAYERS];
next_vphase.set(VersusPhase::Countdown);
}
pub(in crate::app) fn resolve_seat_names(
online: Res<net::Online>,
playback: Res<Playback>,
settings: Res<settings::GameSettings>,
controllers: Res<Controllers>,
mut recorder: ResMut<Recorder>,
mut names: ResMut<SeatNames>,
mut kinds: ResMut<SeatKinds>,
) {
names.0 = match (&playback.0, &online.0) {
(Some((replay, _)), _) => replay.names.clone(),
(None, Some(session)) => session.names.clone(),
(None, None) => settings.names.clone(),
};
*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;
}
}
#[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,
settings: &settings::GameSettings,
online: &net::Online,
names: &SeatNames,
) -> Option<String> {
let mode = teams::in_play(settings, online, seats.0);
let leaders = side_panels::leading_seats(sim.0.scores(), seats.0, mode);
let seat = leaders.iter().position(|&led| led)?;
Some(names.label(settings.tr(), seat as u8))
}
pub(in crate::app) fn check_versus_over(
sim: Res<Sim>,
seating: side_panels::Seating,
settings: Res<settings::GameSettings>,
log: RoundLog,
mut highlight: ResMut<Highlight>,
mut next_vphase: ResMut<NextState<VersusPhase>>,
) {
let side_panels::Seating {
seats,
names: seat_names,
online,
..
} = seating;
let RoundLog {
mut recorder,
mut reel_thread,
} = log;
if sim.0.round_over() {
highlight.0 = None;
reel_thread.0 = None;
if let Some(replay) = recorder.0.take() {
let library = replays::library_dir();
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 stamp = clock::now_secs();
let winner = winner_name(&sim, &seats, &settings, &online, &seat_names);
let kept = replays::free_path(&library, stamp, winner.as_deref());
if let Err(e) = paths::write_atomic(&kept, &text) {
warn!("could not file the replay: {e}");
}
replays::prune(settings.replay_cap);
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);
}
}