use crate::{GamePlatform, errors::FilesystemError, filesystem};
use std::path::{Path, PathBuf};
const GOOD_LAUNCH_FILENAMES: &[&str] = &[
"start", "launch", "play", "run", "game", "launcher", "rungame",
];
const ARCHITECTURE_SUFFIXES: &[&str] = {
#[cfg(target_pointer_width = "64")]
{
&["64"]
}
#[cfg(not(target_pointer_width = "64"))]
{
&["32"]
}
};
const BEST_PROXIMITY_MULTIPLIER: f64 = 0.34;
const MAX_DIRECTORY_LEVEL_DEPTH: usize = 2;
impl GamePlatform {
fn get_best_filenames(self) -> &'static [&'static str] {
match self {
GamePlatform::Linux
| GamePlatform::Windows
| GamePlatform::OSX
| GamePlatform::Android
| GamePlatform::Flash
| GamePlatform::Java
| GamePlatform::UnityWebPlayer => &[],
GamePlatform::Web => &["index"],
}
}
}
pub async fn get_game_executable(
upload_folder: &Path,
platform: GamePlatform,
game_title: String,
) -> Result<PathBuf, String> {
filesystem::ensure_is_dir(upload_folder).await?;
let game_title = make_alphanumeric_lowercase(game_title);
let mut best_executable: (Option<PathBuf>, i64) = (None, i64::MIN);
let mut queue: std::collections::VecDeque<(PathBuf, usize)> = std::collections::VecDeque::new();
queue.push_back((upload_folder.to_path_buf(), 0));
while let Some((folder, depth)) = queue.pop_front() {
let mut entries = filesystem::read_dir(&folder).await?;
while let Some(entry) = filesystem::next_entry(&mut entries, &folder).await? {
let entry_path = entry.path();
if filesystem::file_type(&entry, &folder).await?.is_dir() {
if depth < MAX_DIRECTORY_LEVEL_DEPTH {
queue.push_back((entry_path, depth + 1));
}
} else {
let rating = rate_executable(&entry_path, depth, platform, &game_title)?;
if rating > best_executable.1 {
best_executable = (Some(entry_path), rating);
}
}
}
}
if let Some(executable) = best_executable.0 {
Ok(executable)
} else {
Err(format!(
"Couldn't find any game file executable in: \"{}\"",
upload_folder.to_string_lossy()
))
}
}
fn rate_executable(
file_path: &Path,
directory_levels: usize,
platform: GamePlatform,
game_title: &str,
) -> Result<i64, FilesystemError> {
let mut rating: i64 = 0;
assert!(directory_levels <= MAX_DIRECTORY_LEVEL_DEPTH);
rating -= (directory_levels as i64).saturating_pow(2) * 1000;
let filename: String =
make_alphanumeric_lowercase(filesystem::get_file_stem(file_path)?.to_owned());
let extension = make_alphanumeric_lowercase(
filesystem::get_file_extension(file_path)
.unwrap_or_default()
.to_owned(),
);
rating += rate_extension(&extension, platform);
if platform
.get_best_filenames()
.iter()
.any(|filen| filename.eq_ignore_ascii_case(filen))
{
rating += 2300;
}
rating += proximity_rating_with_suffixes(
game_title,
&filename,
ARCHITECTURE_SUFFIXES,
2,
700,
550,
BEST_PROXIMITY_MULTIPLIER,
);
for n in GOOD_LAUNCH_FILENAMES {
rating += proximity_rating(n, &filename, 1, 1200, 500, BEST_PROXIMITY_MULTIPLIER);
}
Ok(rating)
}
fn rate_extension(extension: &str, platform: GamePlatform) -> i64 {
match (platform, extension) {
#[cfg(target_pointer_width = "64")]
(GamePlatform::Linux, "x8664") => 1000,
#[cfg(target_pointer_width = "64")]
(GamePlatform::Linux, "x86") => 400,
#[cfg(not(target_pointer_width = "64"))]
(GamePlatform::Linux, "x86") => 1000,
(GamePlatform::Linux, "sh") => 600,
(GamePlatform::Linux, "") => 400,
(GamePlatform::Linux, "bin") => 200,
(GamePlatform::Linux, "run") => 250,
(GamePlatform::Windows, "exe") => 1000,
(GamePlatform::Windows, "bat") => 700,
(GamePlatform::Windows, "msi") => 500,
(GamePlatform::OSX, "app") => 1000,
(GamePlatform::OSX, "sh") => 700,
(GamePlatform::OSX, "dmg") => 500,
(GamePlatform::OSX, "pkg") => 500,
(GamePlatform::Android, "apk") => 1000,
(GamePlatform::Web, "html") => 1000,
(GamePlatform::Flash, "swf") => 1000,
(GamePlatform::Java, "jar") => 1000,
(GamePlatform::UnityWebPlayer, "unity3d") => 1000,
(_, _) => -10_000_000,
}
}
fn make_alphanumeric_lowercase(mut string: String) -> String {
string.retain(|c| c.is_ascii_alphanumeric());
string.make_ascii_lowercase();
string
}
fn proximity_rating(
a: &str,
b: &str,
max_distance: usize,
base_points: i64,
extra_points: i64,
proximity_multiplier: f64,
) -> i64 {
if strsim::levenshtein(a, b) >= max_distance {
return 0;
}
base_points
+ (strsim::normalized_levenshtein(a, b).powf(1.0 / proximity_multiplier.powf(2.0))
* extra_points as f64) as i64
}
fn proximity_rating_with_suffixes(
a: &str,
b: &str,
suffixes: &[&str],
max_distance: usize,
base_points: i64,
extra_points: i64,
proximity_multiplier: f64,
) -> i64 {
let mut rating: i64 = 0;
for e in suffixes.iter().chain(std::iter::once(&"")) {
rating = rating.max(proximity_rating(
a,
&format!("{b}{e}"),
max_distance,
base_points,
extra_points,
proximity_multiplier,
));
}
rating
}