use super::common::*;
use super::config::*;
use super::database::*;
use super::model::*;
use super::progress::*;
use super::prompt::*;
use super::util::*;
use anyhow::Result;
use clap::builder::PossibleValuesParser;
use clap::{Arg, ArgAction, ArgMatches, Command};
use indicatif::ProgressBar;
use itertools::Itertools;
use rayon::prelude::*;
use regex::Regex;
use shiratsu_naming::naming::TokenizedName;
use shiratsu_naming::naming::nointro::{NoIntroName, NoIntroToken, NoIntroVersion};
use shiratsu_naming::region::Region;
use sqlx::sqlite::SqliteConnection;
use std::cmp::Ordering;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::LazyLock;
use std::time::Duration;
use strum::VariantNames;
pub static LANGUAGE_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"[A-Z][a-z]").unwrap());
pub static VARIANT_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"[A-Z]{2}").unwrap());
pub fn subcommand() -> Command {
Command::new("sort-roms")
.about("Sort ROM files according to region and version preferences")
.arg(
Arg::new("REGIONS_ALL")
.short('r')
.long("regions")
.help("Set the regions to keep (unordered)")
.required(false)
.num_args(1..),
)
.arg(
Arg::new("REGIONS_ALL_SUBFOLDERS")
.long("subfolders")
.help("Set the subfolders scheme for games")
.required(false)
.num_args(1)
.value_parser(PossibleValuesParser::new(SubfolderScheme::VARIANTS)),
)
.arg(
Arg::new("REGIONS_ONE")
.short('o')
.long("1g1r")
.help("Set the 1G1R regions to keep (ordered)")
.required(false)
.num_args(1..),
)
.arg(
Arg::new("REGIONS_ONE_SUBFOLDERS")
.long("1g1r-subfolders")
.help("Set the subfolders scheme for 1G1R games")
.required(false)
.num_args(1)
.value_parser(PossibleValuesParser::new(SubfolderScheme::VARIANTS)),
)
.arg(
Arg::new("WANTED")
.short('w')
.long("wanted")
.help("Show wanted games")
.required(false)
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("ALL")
.short('a')
.long("all")
.help("Sort all systems")
.required(false)
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("YES")
.short('y')
.long("yes")
.help("Automatically say yes to prompts")
.required(false)
.action(ArgAction::SetTrue),
)
}
pub async fn main(
connection: &mut SqliteConnection,
matches: &ArgMatches,
progress_bar: &ProgressBar,
) -> Result<()> {
let systems =
prompt_for_systems(connection, None, false, false, matches.get_flag("ALL")).await?;
let all_regions_subfolders = matches
.get_one::<String>("REGIONS_ALL_SUBFOLDERS")
.map(|s| SubfolderScheme::from_str(s.as_str()).unwrap_or(SubfolderScheme::None));
let one_regions_subfolders = matches
.get_one::<String>("REGIONS_ONE_SUBFOLDERS")
.map(|s| SubfolderScheme::from_str(s.as_str()).unwrap_or(SubfolderScheme::None));
let answer_yes = matches.get_flag("YES");
let print_wanted = matches.get_flag("WANTED");
for system in systems {
let system_id = Some(system.id);
let all_regions = get_regions(connection, matches, "REGIONS_ALL", system_id).await;
let one_regions = get_regions(connection, matches, "REGIONS_ONE", system_id).await;
let all_regions_arcade = get_list(connection, "REGIONS_ALL_ARCADE", system_id)
.await
.into_iter()
.map(|s| ArcadeRomType::from_str(s.as_str()).unwrap())
.collect_vec();
let one_regions_arcade = get_list(connection, "REGIONS_ONE_ARCADE", system_id)
.await
.into_iter()
.map(|s| ArcadeRomType::from_str(s.as_str()).unwrap())
.collect_vec();
let languages = get_list(connection, "LANGUAGES", system_id).await;
let ignored_releases = get_list(connection, "DISCARD_RELEASES", system_id).await;
let ignored_flags = get_list(connection, "DISCARD_FLAGS", system_id).await;
let prefer_parents = get_bool(connection, "PREFER_PARENTS", system_id).await;
let preferred_regions = PreferredRegion::from_str(
&get_string(connection, "PREFER_REGIONS", system_id)
.await
.unwrap(),
)
.unwrap();
let preferred_versions = PreferredVersion::from_str(
&get_string(connection, "PREFER_VERSIONS", system_id)
.await
.unwrap(),
)
.unwrap();
let preferred_flags = get_list(connection, "PREFER_FLAGS", system_id).await;
let one_regions_strict = get_bool(connection, "REGIONS_ONE_STRICT", system_id).await;
sort_system(
connection,
progress_bar,
answer_yes,
print_wanted,
&system,
&all_regions,
&one_regions,
&all_regions_arcade,
&one_regions_arcade,
&languages.iter().map(String::as_str).collect_vec(),
&ignored_releases.iter().map(String::as_str).collect_vec(),
&ignored_flags.iter().map(String::as_str).collect_vec(),
prefer_parents,
&preferred_regions,
&preferred_versions,
&preferred_flags.iter().map(String::as_str).collect_vec(),
&all_regions_subfolders,
&one_regions_subfolders,
one_regions_strict,
)
.await?;
print_separator(progress_bar);
}
Ok(())
}
pub async fn get_regions(
connection: &mut SqliteConnection,
matches: &ArgMatches,
key: &str,
system_id: Option<i64>,
) -> Vec<Region> {
let all_regions: Vec<String> = if matches.contains_id(key) {
let mut regions: Vec<String> = matches
.get_many::<String>(key)
.unwrap()
.map(String::to_owned)
.collect();
regions.dedup();
regions
} else {
get_list(connection, key, system_id).await
};
all_regions
.into_iter()
.flat_map(|r| Region::try_from_tosec_region(&r).expect("Failed to parse region code"))
.collect()
}
#[allow(clippy::too_many_arguments)]
async fn sort_system(
connection: &mut SqliteConnection,
progress_bar: &ProgressBar,
answer_yes: bool,
print_wanted: bool,
system: &System,
all_regions: &[Region],
one_regions: &[Region],
all_regions_arcade: &[ArcadeRomType],
one_regions_arcade: &[ArcadeRomType],
languages: &[&str],
ignored_releases: &[&str],
ignored_flags: &[&str],
prefer_parents: bool,
preferred_regions: &PreferredRegion,
preferred_versions: &PreferredVersion,
preferred_flags: &[&str],
all_regions_subfolders: &Option<SubfolderScheme>,
one_regions_subfolders: &Option<SubfolderScheme>,
one_regions_strict: bool,
) -> Result<()> {
progress_bar.enable_steady_tick(Duration::from_millis(100));
print_header(progress_bar, &format!("Sorting \"{}\"", system.name));
let mut games: Vec<Game>;
let mut all_regions_games: Vec<Game> = vec![];
let mut one_region_games: Vec<Game> = vec![];
let mut ignored_games: Vec<Game> = vec![];
let mut incomplete_all_regions_games: Vec<Game> = vec![];
let mut incomplete_one_region_games: Vec<Game> = vec![];
let mut romfile_moves: Vec<(&Romfile, PathBuf)> = vec![];
let romfiles = find_romfiles_by_system_id(connection, system.id).await;
let mut romfiles_by_id: HashMap<i64, Romfile> = romfiles
.into_iter()
.map(|romfile| (romfile.id, romfile))
.collect();
let patch_romfiles = find_patch_romfiles_by_system_id(connection, system.id).await;
for romfile in patch_romfiles {
romfiles_by_id.insert(romfile.id, romfile);
}
let playlist_romfiles = find_playlist_romfiles_by_system_id(connection, system.id).await;
for romfile in playlist_romfiles {
romfiles_by_id.insert(romfile.id, romfile);
}
if !system.arcade && !one_regions.is_empty() {
let parent_games = find_parent_games_by_system_id(connection, system.id).await;
let clone_games = find_clone_games_by_system_id(connection, system.id).await;
let mut clone_games_by_parent_id: HashMap<i64, Vec<Game>> = HashMap::new();
clone_games.into_iter().for_each(|game| {
let group = clone_games_by_parent_id
.entry(game.parent_id.unwrap())
.or_default();
group.push(game);
});
for parent_game in parent_games {
if clone_games_by_parent_id.contains_key(&parent_game.id) {
games = clone_games_by_parent_id.remove(&parent_game.id).unwrap();
} else {
games = vec![];
}
games.push(parent_game);
games.sort_by(|a, b| {
sort_games_by_weight(
a,
b,
prefer_parents,
preferred_regions,
preferred_versions,
preferred_flags,
)
});
if !ignored_releases.is_empty() || !ignored_flags.is_empty() {
let (mut left_games, right_games) = trim_ignored_games(
games,
languages,
ignored_releases,
ignored_flags,
system.arcade,
);
ignored_games.append(&mut left_games);
games = right_games;
}
for region in one_regions {
let i = games.iter().position(|game| {
(game.completion == Completion::Full as i64
|| one_regions_strict
|| games
.iter()
.all(|game| game.completion != Completion::Full as i64))
&& Region::try_from_tosec_region(&game.regions)
.unwrap_or_default()
.contains(region)
});
if let Some(i) = i {
let game = games.remove(i);
if game.completion == Completion::Full as i64 {
one_region_games.push(game);
} else {
incomplete_one_region_games.push(game);
}
break;
}
}
while !games.is_empty() {
let game = games.remove(0);
let region_in_all_regions = all_regions.contains(&Region::Unknown)
|| all_regions.iter().any(|region| {
Region::try_from_tosec_region(&game.regions)
.unwrap_or_default()
.contains(region)
});
if region_in_all_regions {
if game.completion == Completion::Full as i64 {
all_regions_games.push(game);
} else {
incomplete_all_regions_games.push(game);
}
} else {
ignored_games.push(game);
}
}
}
} else if !system.arcade && !all_regions.is_empty() {
games = find_games_by_system_id(connection, system.id).await;
if !ignored_releases.is_empty() || !ignored_flags.is_empty() {
let (mut left_games, right_games) = trim_ignored_games(
games,
languages,
ignored_releases,
ignored_flags,
system.arcade,
);
ignored_games.append(&mut left_games);
games = right_games;
}
for game in games {
let region_in_all_regions = all_regions.iter().any(|region| {
Region::try_from_tosec_region(&game.regions)
.unwrap_or_default()
.contains(region)
});
if region_in_all_regions {
if game.completion == Completion::Full as i64 {
all_regions_games.push(game);
} else {
incomplete_all_regions_games.push(game);
}
} else {
ignored_games.push(game);
}
}
} else if system.arcade && (!one_regions_arcade.is_empty() || !all_regions_arcade.is_empty()) {
let mut parent_games = find_parent_games_by_system_id(connection, system.id).await;
let mut clone_games = find_clone_games_by_system_id(connection, system.id).await;
if !ignored_releases.is_empty() || !ignored_flags.is_empty() {
let (mut left_games, right_games) = trim_ignored_games(
parent_games,
languages,
ignored_releases,
ignored_flags,
system.arcade,
);
ignored_games.append(&mut left_games);
parent_games = right_games;
let (mut left_games, right_games) = trim_ignored_games(
clone_games,
languages,
ignored_releases,
ignored_flags,
system.arcade,
);
ignored_games.append(&mut left_games);
clone_games = right_games;
}
let (mut bios_games, mut parent_games): (Vec<Game>, Vec<Game>) =
parent_games.into_iter().partition(|game| game.bios);
if one_regions_arcade.contains(&ArcadeRomType::Bios) {
one_region_games.append(&mut bios_games);
} else if all_regions_arcade.contains(&ArcadeRomType::Bios) {
all_regions_games.append(&mut bios_games);
} else {
ignored_games.append(&mut bios_games);
}
if one_regions_arcade.contains(&ArcadeRomType::Parent) {
one_region_games.append(&mut parent_games);
} else if all_regions_arcade.contains(&ArcadeRomType::Parent) {
all_regions_games.append(&mut parent_games);
} else {
ignored_games.append(&mut parent_games);
}
if all_regions_arcade.contains(&ArcadeRomType::Clone) {
all_regions_games.append(&mut clone_games);
} else {
ignored_games.append(&mut clone_games);
}
} else {
games = find_games_by_system_id(connection, system.id).await;
if !ignored_releases.is_empty() || !ignored_flags.is_empty() {
let (mut left_games, right_games) = trim_ignored_games(
games,
languages,
ignored_releases,
ignored_flags,
system.arcade,
);
ignored_games.append(&mut left_games);
games = right_games;
}
for game in games {
if game.completion == Completion::Full as i64 {
all_regions_games.push(game);
} else {
incomplete_all_regions_games.push(game)
}
}
}
if print_wanted {
let mut all_incomplete_games: Vec<&Game> = vec![];
all_incomplete_games.extend(incomplete_all_regions_games.iter());
all_incomplete_games.extend(incomplete_one_region_games.iter());
all_incomplete_games.sort_by_key(|game| &game.name);
let mut wanted_roms: Vec<Rom> = find_roms_without_romfile_by_game_ids(
connection,
&all_incomplete_games
.par_iter()
.map(|game| game.id)
.collect::<Vec<i64>>(),
)
.await;
if !wanted_roms.is_empty() {
print_subheader(progress_bar, "Wanted:");
wanted_roms.sort_by_key(|rom| rom.game_id);
for rom in wanted_roms {
let game = all_incomplete_games
.iter()
.find(|&game| game.id == rom.game_id)
.unwrap();
let checksum = vec![&rom.crc, &rom.md5, &rom.sha1]
.into_iter()
.find(|checksum| checksum.is_some())
.map(|checksum| checksum.as_ref().unwrap().as_str());
print_info(
progress_bar,
&format!(
"{} ({}) [{}]",
rom.name,
game.name,
checksum.unwrap_or("n/a")
),
);
}
} else {
print_skip(progress_bar, "No wanted ROMs");
}
}
let system_directory = get_system_directory(connection, system).await?;
let mut transaction = begin_transaction(connection).await;
let mut changes = 0;
for game in all_regions_games.iter_mut() {
if game
.update_sorting(&mut transaction, Sorting::AllRegions)
.await
{
game.sorting = Sorting::AllRegions as i64;
changes += 1;
}
romfile_moves.append(
&mut sort_game(
&mut transaction,
system,
game,
&romfiles_by_id,
all_regions_subfolders,
)
.await?,
);
}
for game in one_region_games.iter_mut() {
if game
.update_sorting(&mut transaction, Sorting::OneRegion)
.await
{
game.sorting = Sorting::OneRegion as i64;
changes += 1;
};
romfile_moves.append(
&mut sort_game(
&mut transaction,
system,
game,
&romfiles_by_id,
one_regions_subfolders,
)
.await?,
);
}
for game in incomplete_all_regions_games.iter_mut() {
if game
.update_sorting(&mut transaction, Sorting::AllRegions)
.await
{
game.sorting = Sorting::AllRegions as i64;
changes += 1;
}
romfile_moves.append(
&mut sort_game(
&mut transaction,
system,
game,
&romfiles_by_id,
all_regions_subfolders,
)
.await?,
);
}
for game in incomplete_one_region_games.iter_mut() {
if game
.update_sorting(&mut transaction, Sorting::OneRegion)
.await
{
game.sorting = Sorting::OneRegion as i64;
changes += 1;
}
romfile_moves.append(
&mut sort_game(
&mut transaction,
system,
game,
&romfiles_by_id,
one_regions_subfolders,
)
.await?,
);
}
for game in ignored_games.iter_mut() {
if game
.update_sorting(&mut transaction, Sorting::Ignored)
.await
{
game.sorting = Sorting::Ignored as i64;
changes += 1;
}
romfile_moves
.append(&mut sort_game(&mut transaction, system, game, &romfiles_by_id, &None).await?);
}
progress_bar.disable_steady_tick();
if !romfile_moves.is_empty() {
romfile_moves.sort_by(|a, b| a.1.cmp(&b.1));
romfile_moves.dedup_by(|a, b| a.1 == b.1);
print_subheader(progress_bar, "Summary:");
for romfile_move in &romfile_moves {
print_action(
progress_bar,
&format!(
"\"{}\" → \"{}\"",
Path::new(&romfile_move.0.path)
.file_name()
.unwrap()
.to_str()
.unwrap(),
romfile_move.1.as_os_str().to_str().unwrap()
),
);
}
if answer_yes || confirm(true)? {
for romfile_move in romfile_moves {
romfile_move
.0
.as_common(&mut transaction)
.await?
.rename(progress_bar, &romfile_move.1, true)
.await?
.update(&mut transaction, progress_bar, romfile_move.0.id)
.await?;
let mut directory = romfile_move
.0
.as_common(&mut transaction)
.await?
.path
.parent()
.unwrap()
.to_path_buf();
while directory.read_dir().unwrap().next().is_none() {
if directory == system_directory {
break;
} else {
remove_directory(progress_bar, &directory, true).await?;
directory = directory.parent().unwrap().to_path_buf();
}
}
}
commit_transaction(transaction).await;
} else {
rollback_transaction(transaction).await;
}
} else {
commit_transaction(transaction).await;
print_skip(progress_bar, "Already sorted, nothing to do");
}
if changes > 0 {
compute_system_completion(connection, progress_bar, system).await?;
}
Ok(())
}
async fn sort_game<'a>(
connection: &mut SqliteConnection,
system: &System,
game: &Game,
romfiles_by_id: &'a HashMap<i64, Romfile>,
subfolder_scheme: &Option<SubfolderScheme>,
) -> Result<Vec<(&'a Romfile, PathBuf)>> {
let mut romfile_moves: Vec<(&Romfile, PathBuf)> = vec![];
let roms = find_roms_with_romfile_by_game_id(connection, game.id).await;
let mut roms_by_game_id: HashMap<i64, Vec<Rom>> = HashMap::new();
roms.into_iter().for_each(|rom| {
let group = roms_by_game_id.entry(rom.game_id).or_default();
group.push(rom);
});
let roms = roms_by_game_id.get(&game.id);
let roms = match roms {
Some(roms) => roms,
None => return Ok(romfile_moves),
};
for rom in roms {
let romfile = romfiles_by_id.get(&rom.romfile_id.unwrap()).unwrap();
let new_romfile_path = romfile
.as_common(connection)
.await?
.get_sorted_path(connection, system, game, rom, subfolder_scheme, &None)
.await?;
if romfile.as_common(connection).await?.path != new_romfile_path {
let patches = find_patches_by_rom_id(connection, rom.id).await;
for patch in patches {
let patch_romfile = romfiles_by_id.get(&patch.romfile_id).unwrap();
let patch_extension = Path::new(&romfile.path)
.extension()
.unwrap()
.to_str()
.unwrap();
let new_patch_romfile_path = new_romfile_path.with_extension(patch_extension);
romfile_moves.push((patch_romfile, new_patch_romfile_path));
}
romfile_moves.push((romfile, new_romfile_path));
}
}
if let Some(playlist_id) = game.playlist_id {
let playlist_romfile = romfiles_by_id.get(&playlist_id).unwrap();
let new_playlist_romfile_path = game
.get_playlist_path(connection, system, subfolder_scheme)
.await?;
if playlist_romfile.as_common(connection).await?.path != new_playlist_romfile_path {
romfile_moves.push((playlist_romfile, new_playlist_romfile_path));
}
}
Ok(romfile_moves)
}
fn trim_ignored_games(
games: Vec<Game>,
languages: &[&str],
ignored_releases: &[&str],
ignored_flags: &[&str],
arcade: bool,
) -> (Vec<Game>, Vec<Game>) {
if arcade {
games.into_iter().partition(|game| {
let comment = match game.comment.as_ref() {
Some(comment) => comment,
None => "",
};
for ignored_release in ignored_releases {
if comment.contains(ignored_release) {
return true;
}
}
false
})
} else {
games.into_iter().partition(|game| {
log::debug!("sort_roms::trim_ignored_games(\"{}\")", game.name);
if let Ok(name) = NoIntroName::try_parse(&game.name) {
for token in name.iter() {
if let NoIntroToken::Languages(parsed_languages) = token {
log::debug!("parsed languages: {:?}", parsed_languages);
if !languages.is_empty() {
let clean_parsed_languages = parsed_languages
.iter()
.filter(|l| {
LANGUAGE_REGEX.is_match(l.code)
&& (l.variant.is_none()
|| VARIANT_REGEX.is_match(l.variant.unwrap()))
})
.map(|l| l.code.to_string())
.collect::<Vec<String>>();
if !clean_parsed_languages.is_empty()
&& !clean_parsed_languages
.iter()
.any(|language| languages.contains(&language.as_str()))
{
return true;
}
}
}
if let NoIntroToken::Release {
status: release,
number: _,
} = token
{
log::debug!("release: {}", release);
if ignored_releases.contains(release) {
log::debug!("ignoring release");
return true;
}
}
if let NoIntroToken::Flag {
flag_type: _,
flag: flags,
} = token
{
log::debug!("flags: {}", flags);
if ignored_flags.contains(flags) {
log::debug!("ignoring flag: {}", flags);
return true;
}
for flag in flags.split(", ") {
if ignored_flags.contains(&flag) {
log::debug!("ignoring flag: {}", flag);
return true;
}
}
}
}
}
false
})
}
}
fn compare_versions(a: &[NoIntroVersion], b: &[NoIntroVersion]) -> Ordering {
let (first_a, first_b) = match (a.first(), b.first()) {
(Some(a), Some(b)) => (a, b),
(Some(_), None) => return Ordering::Greater,
(None, Some(_)) => return Ordering::Less,
(None, None) => return Ordering::Equal,
};
let parse = |v: &NoIntroVersion| -> (u64, u64) {
let major = v.major.parse::<u64>().unwrap_or(0);
let minor = v.minor.and_then(|m| m.parse::<u64>().ok()).unwrap_or(0);
(major, minor)
};
parse(first_a).cmp(&parse(first_b))
}
fn sort_games_by_weight(
game_a: &Game,
game_b: &Game,
prefer_parents: bool,
preferred_regions: &PreferredRegion,
preferred_versions: &PreferredVersion,
preferred_flags: &[&str],
) -> Ordering {
let mut weight_a: u8 = 0;
let mut weight_b: u8 = 0;
if prefer_parents {
if game_a.parent_id.is_none() {
weight_a += 1;
} else if game_b.parent_id.is_none() {
weight_b += 1;
}
}
if preferred_regions != &PreferredRegion::None {
let regions_a = Region::try_from_tosec_region(&game_a.regions).unwrap_or_default();
let regions_b = Region::try_from_tosec_region(&game_b.regions).unwrap_or_default();
match regions_b.len().partial_cmp(®ions_a.len()).unwrap() {
Ordering::Less => match preferred_regions {
PreferredRegion::Broad => weight_a += 1,
PreferredRegion::Narrow => weight_b += 1,
PreferredRegion::None => {}
},
Ordering::Greater => match preferred_regions {
PreferredRegion::Broad => weight_b += 1,
PreferredRegion::Narrow => weight_a += 1,
PreferredRegion::None => {}
},
Ordering::Equal => {}
};
}
if preferred_versions != &PreferredVersion::None {
let mut version_a = None;
let mut version_b = None;
if let Ok(name) = NoIntroName::try_parse(&game_a.name) {
for token in name.iter() {
if let NoIntroToken::Version(version) = token {
version_a = Some(version.to_owned());
}
}
}
if let Ok(name) = NoIntroName::try_parse(&game_b.name) {
for token in name.iter() {
if let NoIntroToken::Version(version) = token {
version_b = Some(version.to_owned());
}
}
}
if let (Some(version_a), Some(version_b)) = (version_a.as_ref(), version_b.as_ref()) {
match compare_versions(version_a, version_b) {
Ordering::Greater => match *preferred_versions {
PreferredVersion::New => weight_a += 1,
PreferredVersion::Old => weight_b += 1,
PreferredVersion::None => {}
},
Ordering::Less => match *preferred_versions {
PreferredVersion::New => weight_b += 1,
PreferredVersion::Old => weight_a += 1,
PreferredVersion::None => {}
},
Ordering::Equal => {}
};
} else if version_a.is_some() {
match *preferred_versions {
PreferredVersion::New => weight_a += 1,
PreferredVersion::Old => weight_b += 1,
PreferredVersion::None => {}
}
} else if version_b.is_some() {
match *preferred_versions {
PreferredVersion::New => weight_b += 1,
PreferredVersion::Old => weight_a += 1,
PreferredVersion::None => {}
}
}
}
if !preferred_flags.is_empty() {
if let Ok(name) = NoIntroName::try_parse(&game_a.name) {
for token in name.iter() {
if let NoIntroToken::Flag {
flag_type: _,
flag: flags,
} = token
{
for flag in flags.split(", ") {
if preferred_flags.contains(&flag) {
weight_a += 1;
}
}
}
}
}
if let Ok(name) = NoIntroName::try_parse(&game_b.name) {
for token in name.iter() {
if let NoIntroToken::Flag {
flag_type: _,
flag: flags,
} = token
{
for flag in flags.split(", ") {
if preferred_flags.contains(&flag) {
weight_b += 1;
}
}
}
}
}
}
weight_b.partial_cmp(&weight_a).unwrap()
}
#[cfg(test)]
mod test_all_regions_from_db;
#[cfg(test)]
mod test_all_regions_from_matches;
#[cfg(test)]
mod test_one_region_from_db;
#[cfg(test)]
mod test_one_region_from_matches;
#[cfg(test)]
mod test_order_no_prefer_parents_clone_vs_clone;
#[cfg(test)]
mod test_order_no_prefer_parents_clone_vs_parent;
#[cfg(test)]
mod test_order_no_prefer_parents_parent_vs_clone;
#[cfg(test)]
mod test_order_prefer_flags;
#[cfg(test)]
mod test_order_prefer_flags_swapped;
#[cfg(test)]
mod test_order_prefer_parents_clone_vs_clone;
#[cfg(test)]
mod test_order_prefer_parents_clone_vs_parent;
#[cfg(test)]
mod test_order_prefer_parents_parent_vs_clone;
#[cfg(test)]
mod test_order_prefer_regions_broad;
#[cfg(test)]
mod test_order_prefer_regions_broad_swapped;
#[cfg(test)]
mod test_order_prefer_regions_narrow;
#[cfg(test)]
mod test_order_prefer_regions_narrow_swapped;
#[cfg(test)]
mod test_order_prefer_versions_new_revision_vs_revision;
#[cfg(test)]
mod test_order_prefer_versions_new_revision_vs_revision_swapped;
#[cfg(test)]
mod test_order_prefer_versions_new_revision_vs_vanilla;
#[cfg(test)]
mod test_order_prefer_versions_new_vanilla_vs_revision;
#[cfg(test)]
mod test_order_prefer_versions_old_revision_vs_revision;
#[cfg(test)]
mod test_order_prefer_versions_old_revision_vs_revision_swapped;
#[cfg(test)]
mod test_order_prefer_versions_old_revision_vs_vanilla;
#[cfg(test)]
mod test_order_prefer_versions_old_vanilla_vs_revision;
#[cfg(test)]
mod test_sort;
#[cfg(test)]
mod test_sort_1g1r;
#[cfg(test)]
mod test_sort_1g1r_catch_all;
#[cfg(test)]
mod test_sort_1g1r_discard_asia_and_beta;
#[cfg(test)]
mod test_sort_1g1r_lenient;
#[cfg(test)]
mod test_sort_1g1r_playlist;
#[cfg(test)]
mod test_sort_1g1r_revisions;
#[cfg(test)]
mod test_sort_1g1r_strict;
#[cfg(test)]
mod test_sort_1g1r_subfolders_alpha;
#[cfg(test)]
mod test_sort_1g1r_without_parent_clone;
#[cfg(test)]
mod test_sort_1g1r_without_roms;
#[cfg(test)]
mod test_sort_archive_custom_extension;
#[cfg(test)]
mod test_sort_custom_extension;
#[cfg(test)]
mod test_sort_discard_asia;
#[cfg(test)]
mod test_sort_discard_asia_and_beta;
#[cfg(test)]
mod test_sort_discard_beta;
#[cfg(test)]
mod test_trim_ignored;