use crate::app::i18n::Tr;
use bevy::prelude::*;
use std::path::{Path, PathBuf};
pub const KEEP_CAP: char = 'G';
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum KeepOffer {
#[default]
Nothing,
Offered,
Kept,
}
#[derive(Resource, Default, Debug)]
pub enum RoundToKeep {
#[default]
Nothing,
Offered(Box<Unkept>),
Kept,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Unkept {
pub text: String,
pub stamp: u64,
pub winner: Option<String>,
}
impl RoundToKeep {
pub fn offer(&self) -> KeepOffer {
match self {
RoundToKeep::Nothing => KeepOffer::Nothing,
RoundToKeep::Offered(_) => KeepOffer::Offered,
RoundToKeep::Kept => KeepOffer::Kept,
}
}
pub fn keep_in(&mut self, dir: &Path, cap: u8) -> Option<std::io::Result<PathBuf>> {
let RoundToKeep::Offered(round) = self else {
return None;
};
let path = super::free_path(dir, round.stamp, round.winner.as_deref());
if let Err(e) = crate::app::paths::write_atomic(&path, &round.text) {
return Some(Err(e));
}
super::prune_in(dir, cap);
*self = RoundToKeep::Kept;
Some(Ok(path))
}
}
pub fn keep_words(tr: &Tr, offer: KeepOffer) -> Option<&'static str> {
match offer {
KeepOffer::Nothing => None,
KeepOffer::Offered => Some(tr.prompt_keep_replay),
KeepOffer::Kept => Some(tr.replay_kept),
}
}
pub fn kept_lead(tr: &Tr, offer: KeepOffer) -> Option<&'static str> {
match offer {
KeepOffer::Kept => Some(tr.replay_kept),
KeepOffer::Nothing | KeepOffer::Offered => None,
}
}
#[derive(SystemSet, Clone, PartialEq, Eq, Hash, Debug)]
pub struct KeepKey;
pub fn let_the_round_go(mut round: ResMut<RoundToKeep>) {
*round = RoundToKeep::Nothing;
}
pub fn keep_input(
keyboard: crate::app::keycaps::Keyboard,
pause: Res<crate::app::pause::PauseMenu>,
chat: Res<crate::app::spectators::SpectatorChat>,
library: Res<super::Library>,
settings: Res<crate::app::settings::GameSettings>,
mut round: ResMut<RoundToKeep>,
) {
if pause.is_open() || chat.open() || !keyboard.just(KEEP_CAP) {
return;
}
match round.keep_in(&library.dir, settings.replay_cap) {
Some(Ok(path)) => info!("replay kept as {}", path.display()),
Some(Err(e)) => warn!("could not keep the replay: {e}"),
None => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::app::gamepad::scheme::{self, Face};
use crate::app::i18n::ALL_LANGS;
use crate::app::play_input::AfterRound;
fn scratch(name: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("pinch-keep-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("scratch shelf");
dir
}
fn offered(text: &str) -> RoundToKeep {
RoundToKeep::Offered(Box::new(Unkept {
text: text.to_string(),
stamp: 1_700_000_000,
winner: Some("Bo".to_string()),
}))
}
#[test]
fn a_kept_round_is_filed_once() {
let dir = scratch("once");
let mut round = offered("the recording");
assert_eq!(round.offer(), KeepOffer::Offered);
let path = round.keep_in(&dir, 35).expect("on offer").expect("written");
assert_eq!(
path,
dir.join(super::super::file_name(1_700_000_000, Some("Bo")))
);
assert_eq!(
std::fs::read_to_string(&path).expect("read"),
"the recording"
);
assert_eq!(round.offer(), KeepOffer::Kept);
assert!(round.keep_in(&dir, 35).is_none(), "nothing more to keep");
assert_eq!(super::super::shelf_in(&dir).len(), 1, "no duplicate");
}
#[test]
fn keeping_trims_the_shelf_to_its_cap() {
let dir = scratch("cap");
for stamp in 1..=3u64 {
let path = super::super::free_path(&dir, stamp, None);
std::fs::write(path, "old").expect("stocked");
}
let mut round = offered("new");
round.keep_in(&dir, 2).expect("on offer").expect("written");
let shelf = super::super::shelf_in(&dir);
assert_eq!(shelf.len(), 2);
assert_eq!(shelf[0].winner, "Bo", "the newest stays");
}
#[test]
fn the_card_says_what_the_key_does() {
for lang in ALL_LANGS {
let tr = lang.tr();
let words = keep_words(tr, KeepOffer::Offered).expect("offered");
assert!(
words.starts_with(&format!("{KEEP_CAP}:"))
|| words.starts_with(&format!("{KEEP_CAP} :")),
"{lang:?} names the key: {words}"
);
assert_eq!(keep_words(tr, KeepOffer::Kept), Some(tr.replay_kept));
assert_eq!(keep_words(tr, KeepOffer::Nothing), None);
}
let results = |keep| Face::Results {
door: AfterRound::Menu,
talk: false,
rematch: None,
keep,
};
let tr = &crate::app::i18n::EN;
let offered = scheme::legend(tr, &results(KeepOffer::Offered));
assert!(offered.contains("X: keep replay"), "{offered}");
for keep in [KeepOffer::Kept, KeepOffer::Nothing] {
let line = scheme::legend(tr, &results(keep));
assert!(!line.contains("X:"), "{keep:?}: {line}");
}
assert_eq!(kept_lead(tr, KeepOffer::Kept), Some("replay kept"));
}
#[test]
fn the_keep_key_is_free_on_the_results_card() {
assert!(!crate::app::binds::GLOBAL_LETTERS.contains(&KEEP_CAP));
let caps = crate::app::keycaps::KeyCaps::default();
let key = caps.key_for(KEEP_CAP).expect("a key");
for seat in 0..2 {
let stock = crate::app::binds::SeatBinds::default_for(seat);
assert!(!stock.0.contains(&key), "seat {seat} plays {key:?}");
}
for face in crate::app::gamepad::face::every_face() {
if !matches!(face, Face::Results { .. } | Face::Interlude { .. }) {
continue;
}
for row in scheme::binds(&face) {
if row.key == scheme::Key::Cap(KEEP_CAP) {
assert_eq!(row.does, scheme::Does::KeepReplay, "{face:?}");
}
}
}
}
mod app {
use super::*;
use crate::app::gamepad::scheme::Pad;
use crate::app::gamepad::{PadFace, PadPick};
use crate::app::replays::Library;
pub(super) fn card(dir: &Path, round: RoundToKeep) -> App {
let mut app = App::new();
app.init_resource::<ButtonInput<KeyCode>>();
app.init_resource::<crate::app::keycaps::KeyCaps>();
app.init_resource::<crate::app::cursor::LiveSeatKeys>();
app.init_resource::<crate::app::pause::PauseMenu>();
app.init_resource::<crate::app::spectators::SpectatorChat>();
app.init_resource::<crate::app::settings::GameSettings>();
app.insert_resource(Library {
dir: dir.to_path_buf(),
..Library::default()
});
app.insert_resource(round);
app.add_systems(Update, keep_input);
app
}
pub(super) fn press_keep(app: &mut App) {
let key = app
.world()
.resource::<crate::app::keycaps::KeyCaps>()
.key_for(KEEP_CAP)
.expect("a key");
let mut keys = app.world_mut().resource_mut::<ButtonInput<KeyCode>>();
keys.clear();
keys.press(key);
app.update();
let mut keys = app.world_mut().resource_mut::<ButtonInput<KeyCode>>();
keys.release(key);
keys.clear();
}
#[test]
fn the_key_keeps_the_round_once() {
let dir = scratch("key");
let mut app = card(&dir, offered("a round"));
app.update();
assert!(
crate::app::replays::shelf_in(&dir).is_empty(),
"not by itself"
);
press_keep(&mut app);
assert_eq!(crate::app::replays::shelf_in(&dir).len(), 1);
press_keep(&mut app);
assert_eq!(crate::app::replays::shelf_in(&dir).len(), 1, "once");
}
#[test]
fn not_under_the_pause_card() {
let dir = scratch("paused");
let mut app = card(&dir, offered("a round"));
app.insert_resource(crate::app::pause::PauseMenu::raised());
press_keep(&mut app);
assert!(crate::app::replays::shelf_in(&dir).is_empty());
}
#[test]
fn not_while_a_spectator_types() {
let dir = scratch("typing");
let mut app = card(&dir, offered("a round"));
app.world_mut()
.resource_mut::<crate::app::spectators::SpectatorChat>()
.0 = Some("good game".to_string());
press_keep(&mut app);
assert!(crate::app::replays::shelf_in(&dir).is_empty());
}
#[test]
fn a_pads_button_keeps_it_through_the_bridge() {
let dir = scratch("pad");
let mut app = card(&dir, offered("a round"));
app.init_resource::<crate::app::gamepad::Bridged>();
app.init_resource::<PadPick>();
app.insert_resource(PadFace(Face::Results {
door: AfterRound::Menu,
talk: false,
rematch: None,
keep: KeepOffer::Offered,
}));
app.add_systems(
PreUpdate,
(
crate::app::gamepad::pad_menu_bridge,
crate::app::gamepad::pad_bridge_release,
)
.chain(),
);
let pad = app.world_mut().spawn(Gamepad::default()).id();
app.update();
let button = Pad::X.button().expect("a button");
app.world_mut()
.entity_mut(pad)
.get_mut::<Gamepad>()
.expect("pad")
.digital_mut()
.press(button);
app.update();
assert_eq!(crate::app::replays::shelf_in(&dir).len(), 1);
assert_eq!(
app.world().resource::<RoundToKeep>().offer(),
KeepOffer::Kept
);
}
}
}