#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
use std::{
collections::HashSet,
fs::{self},
io::{self, BufRead, Write},
path::{Path, PathBuf},
};
pub mod game;
pub mod path;
mod preset;
#[cfg(test)]
mod test_utils;
pub use preset::Preset;
pub type Result<T> = core::result::Result<T, Error>;
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Directory {dir} not found.")]
DirNotFound { dir: PathBuf },
#[error("Game directory could not automatically be found. Try launching the game first.")]
GameDirNotFound,
#[error("%LocalAppData% variable could not be found.")]
MissingLocalAppdata,
#[error("Could not parse BeamNG.drive's version.txt for game version.")]
VersionError,
#[error("Could not find preset {preset} in {dir}")]
MissingPreset { dir: PathBuf, preset: String },
#[error("Mods not found: {mods:?}")]
MissingMods { mods: Vec<String> },
#[error("Presets failed to enable: {presets:?}, missing these mods: {mods:?}")]
PresetsFailed {
mods: HashSet<String>,
presets: HashSet<String>,
},
#[error("Preset `{preset}` already exists.")]
PresetExists { preset: String },
#[error("There was an IO error. {0}")]
IO(#[from] std::io::Error),
#[error("There was a JSON error. {0}")]
JSON(#[from] serde_json::Error),
}
use Error::*;
pub fn game_version(data_dir: &Path) -> Result<String> {
if !data_dir.try_exists()? {
return Err(DirNotFound {
dir: data_dir.to_owned(),
});
}
let version_path = data_dir.join("version.txt");
if version_path.try_exists()? {
let full_version = fs::read_to_string(version_path)?;
let mut split_version = full_version.trim().split(".");
let major_version = split_version.next().ok_or(VersionError)?;
let minor_version = split_version.next().ok_or(VersionError)?;
Ok(format!("{}.{}", major_version, minor_version))
} else {
fs::read_dir(data_dir)?
.filter_map(|f| f.ok().map(|f| f.path())) .filter(|f| f.is_dir()) .filter_map(
|d| {
d.file_name()
.and_then(|d| d.to_str()) .map(|d| d.trim().parse::<f32>()) .filter(|n| n.is_ok()) .map(|n| n.unwrap())
}, )
.reduce(f32::max) .map(|n| n.to_string()) .ok_or(VersionError) }
}
pub fn confirm<R: BufRead, W: Write>(
mut reader: R,
mut writer: W,
msg: &str,
default: bool,
confirm_all: bool,
) -> Result<bool> {
if confirm_all {
Ok(true)
} else {
let y_n = String::from(if default { "(Y/n)" } else { "(y/N)" });
writeln!(&mut writer, "{} {}", msg.trim(), y_n)?;
let mut input = String::new();
reader.read_line(&mut input)?;
input = input.trim().to_lowercase();
if default {
Ok(input != "n")
} else {
Ok(input == "y")
}
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
pub fn confirm_cli(msg: &str, default: bool, confirm_all: bool) -> Result<bool> {
confirm(io::stdin().lock(), io::stdout(), msg, default, confirm_all)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_confirm() {
let input_y = b"y\n";
let input_n = b"n\n";
let input_nothing = b"\n";
let mut writer = Vec::new();
let msg = "Are you sure?";
let confirm_all = false;
{
let mut reader_n = io::BufReader::new(&input_n[..]);
let result = confirm(&mut reader_n, &mut writer, msg, true, confirm_all).unwrap();
assert!(!result);
}
{
let mut reader_y = io::BufReader::new(&input_y[..]);
let result = confirm(&mut reader_y, &mut writer, msg, true, confirm_all).unwrap();
assert!(result);
}
{
let mut reader_nothing = io::BufReader::new(&input_nothing[..]);
let result = confirm(&mut reader_nothing, &mut writer, msg, true, confirm_all).unwrap();
assert!(result);
}
{
let mut reader_n = io::BufReader::new(&input_n[..]);
let result = confirm(&mut reader_n, &mut writer, msg, false, confirm_all).unwrap();
assert!(!result);
}
{
let mut reader_y = io::BufReader::new(&input_y[..]);
let result = confirm(&mut reader_y, &mut writer, msg, false, confirm_all).unwrap();
assert!(result);
}
{
let mut reader_nothing = io::BufReader::new(&input_nothing[..]);
let result =
confirm(&mut reader_nothing, &mut writer, msg, false, confirm_all).unwrap();
assert!(!result);
}
{
let mut reader_n = io::BufReader::new(&input_n[..]);
let result = confirm(&mut reader_n, &mut writer, msg, true, true).unwrap();
assert!(result);
}
}
#[test]
fn test_game_version() {
let temp_dir = tempdir().unwrap();
let game_dir = temp_dir.path();
let version_file = game_dir.join("version.txt");
std::fs::write(&version_file, "0.32.0").unwrap();
let version = game_version(game_dir).unwrap();
assert_eq!(version, "0.32");
}
#[test]
fn test_discover_game_version() {
let temp_dir = tempdir().unwrap();
let game_dir = temp_dir.path();
std::fs::create_dir(game_dir.join("0.31")).unwrap();
std::fs::create_dir(game_dir.join("0.32")).unwrap();
std::fs::create_dir(game_dir.join("0.33")).unwrap();
let version = game_version(game_dir).unwrap();
assert_eq!(version, "0.33");
}
#[test]
fn test_game_version_bad_directory() {
let game_dir = Path::new("nonexistent");
let version = game_version(game_dir);
assert!(matches!(version, Err(DirNotFound { .. })));
}
}