use std::path::{Path, PathBuf};
use crate::itch_api_types::*;
use crate::GamePlatform;
const GOOD_LAUNCH_FILENAMES: &[&'static str] = &["start", "launch", "play", "run", "game", "launcher", "rungame"];
const ARCHITECTURE_SUFFIXES: &[&'static 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_allowed_extensions(&self) -> &'static [&'static str] {
match self {
GamePlatform::Linux => &["x8664", "x86", "bin", "sh", "run", ""],
GamePlatform::Windows => &["exe", "msi", "bat"],
GamePlatform::OSX => &["dmg", "app", "pkg"],
GamePlatform::Android => &["apk"],
GamePlatform::Web => &["html"],
GamePlatform::Flash => &["swf"],
GamePlatform::Java => &["jar"],
GamePlatform::UnityWebPlayer => &["unity3d"],
}
}
fn get_best_filenames(&self) -> &'static [&'static str] {
match self {
GamePlatform::Linux => &[],
GamePlatform::Windows => &[],
GamePlatform::OSX => &[],
GamePlatform::Android => &[],
GamePlatform::Web => &["index"],
GamePlatform::Flash => &[],
GamePlatform::Java => &[],
GamePlatform::UnityWebPlayer => &[],
}
}
}
pub async fn get_game_executable(upload_folder: &Path, platform: &GamePlatform, game_info: &Game) -> Result<PathBuf, String> {
if !upload_folder.is_dir() {
return Err(format!("Not a folder: \"{}\"", upload_folder.to_string_lossy()));
}
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 = tokio::fs::read_dir(&folder).await
.map_err(|e| format!("Couldn't read dir \"{}\": {e}", folder.as_path().to_string_lossy()))?;
while let Some(entry) = entries.next_entry().await.map_err(|e| e.to_string())? {
let entry_path = entry.path();
if entry.file_type().await.map_err(|e| e.to_string())?.is_dir() {
if depth < MAX_DIRECTORY_LEVEL_DEPTH {
queue.push_back((entry_path, depth + 1));
}
} else {
let rating = rate_executable(entry_path.as_path(), depth, platform, game_info)?;
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_info: &Game) -> Result<i64, String> {
let mut rating: i64 = 0;
rating -= (directory_levels as i64).saturating_pow(2) * 1000;
let filename: String = make_alphanumeric_lowercase(
file_path.file_stem()
.expect("File doesn't have a filename????")
.to_string_lossy()
.to_string()
);
let extension = make_alphanumeric_lowercase(
file_path.extension()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_default()
);
if !platform.get_allowed_extensions().iter().any(|ext| extension.eq_ignore_ascii_case(ext)) {
rating -= 10000000;
}
if platform.get_best_filenames().iter().any(|filen| filename.eq_ignore_ascii_case(filen)) {
rating += 2300;
}
let game_title = make_alphanumeric_lowercase(game_info.title.clone());
rating += proximity_rating_with_suffixes(game_title.as_str(), filename.as_str(), ARCHITECTURE_SUFFIXES, 2, 700, 550, BEST_PROXIMITY_MULTIPLIER);
for n in GOOD_LAUNCH_FILENAMES {
rating += proximity_rating(n, filename.as_str(), 1, 1200, 500, BEST_PROXIMITY_MULTIPLIER);
}
Ok(rating)
}
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}").as_str(), max_distance, base_points, extra_points, proximity_multiplier));
}
rating
}