mirror of
https://github.com/Novattz/creamlinux-installer.git
synced 2026-08-01 19:18:25 -04:00
backend stuff lol
This commit is contained in:
Vendored
+74
-4
@@ -3,9 +3,10 @@ mod version;
|
|||||||
|
|
||||||
pub use storage::{
|
pub use storage::{
|
||||||
get_creamlinux_version_dir, get_smokeapi_version_dir,
|
get_creamlinux_version_dir, get_smokeapi_version_dir,
|
||||||
list_creamlinux_files, list_smokeapi_files, read_versions,
|
list_creamlinux_files, list_smokeapi_files, read_versions,
|
||||||
update_creamlinux_version, update_smokeapi_version, validate_smokeapi_cache,
|
update_creamlinux_version, update_smokeapi_version, validate_smokeapi_cache,
|
||||||
validate_creamlinux_cache, get_cache_dir, get_koaloader_version_dir, get_screamapi_version_dir,
|
validate_creamlinux_cache, get_cache_dir, get_koaloader_version_dir, get_screamapi_version_dir,
|
||||||
|
clear_unlocker_cache,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use version::{
|
pub use version::{
|
||||||
@@ -81,7 +82,7 @@ pub async fn initialize_cache() -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
Ok(false) => {
|
Ok(false) => {
|
||||||
info!("ScreamAPI cache incomplete, re-downloading");
|
info!("ScreamAPI cache incomplete, re-downloading");
|
||||||
needs_smokeapi = true;
|
needs_screamapi = true;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Failed to validate ScreamAPI cache: {}, re-downloading", e);
|
warn!("Failed to validate ScreamAPI cache: {}, re-downloading", e);
|
||||||
@@ -170,7 +171,7 @@ pub async fn initialize_cache() -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !needs_smokeapi && !needs_creamlinux && !needs_smokeapi && !needs_koaloader {
|
if !needs_smokeapi && !needs_creamlinux && !needs_screamapi && !needs_koaloader {
|
||||||
info!("Cache already initialized and validated");
|
info!("Cache already initialized and validated");
|
||||||
} else {
|
} else {
|
||||||
info!("Cache initialization complete");
|
info!("Cache initialization complete");
|
||||||
@@ -247,6 +248,68 @@ pub async fn check_and_update_cache() -> Result<UpdateResult, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check ScreamAPI
|
||||||
|
let current_screamapi = read_versions()?.screamapi.latest;
|
||||||
|
match ScreamAPI::get_latest_version().await {
|
||||||
|
Ok(latest_version) => {
|
||||||
|
if current_screamapi != latest_version {
|
||||||
|
info!(
|
||||||
|
"ScreamAPI update available: {} -> {}",
|
||||||
|
current_screamapi, latest_version
|
||||||
|
);
|
||||||
|
|
||||||
|
match ScreamAPI::download_to_cache().await {
|
||||||
|
Ok(version) => {
|
||||||
|
update_screamapi_version(&version)?;
|
||||||
|
result.screamapi_updated = true;
|
||||||
|
result.new_screamapi_version = Some(version);
|
||||||
|
info!("ScreamAPI updated successfully");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to download ScreamAPI update: {}", e);
|
||||||
|
return Err(format!("Failed to download ScreamAPI update: {}", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
info!("ScreamAPI is up to date: {}", current_screamapi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Failed to check ScreamAPI version: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Koaloader
|
||||||
|
let current_koaloader = read_versions()?.koaloader.latest;
|
||||||
|
match Koaloader::get_latest_version().await {
|
||||||
|
Ok(latest_version) => {
|
||||||
|
if current_koaloader != latest_version {
|
||||||
|
info!(
|
||||||
|
"Koaloader update available: {} -> {}",
|
||||||
|
current_koaloader, latest_version
|
||||||
|
);
|
||||||
|
|
||||||
|
match Koaloader::download_to_cache().await {
|
||||||
|
Ok(version) => {
|
||||||
|
update_koaloader_version(&version)?;
|
||||||
|
result.koaloader_updated = true;
|
||||||
|
result.new_koaloader_version = Some(version);
|
||||||
|
info!("Koaloader updated successfully");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to download Koaloader update: {}", e);
|
||||||
|
return Err(format!("Failed to download Koaloader update: {}", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
info!("Koaloader is up to date: {}", current_koaloader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Failed to check Koaloader version: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,13 +396,20 @@ pub async fn update_outdated_games(
|
|||||||
pub struct UpdateResult {
|
pub struct UpdateResult {
|
||||||
pub smokeapi_updated: bool,
|
pub smokeapi_updated: bool,
|
||||||
pub creamlinux_updated: bool,
|
pub creamlinux_updated: bool,
|
||||||
|
pub screamapi_updated: bool,
|
||||||
|
pub koaloader_updated: bool,
|
||||||
pub new_smokeapi_version: Option<String>,
|
pub new_smokeapi_version: Option<String>,
|
||||||
pub new_creamlinux_version: Option<String>,
|
pub new_creamlinux_version: Option<String>,
|
||||||
|
pub new_screamapi_version: Option<String>,
|
||||||
|
pub new_koaloader_version: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UpdateResult {
|
impl UpdateResult {
|
||||||
pub fn any_updated(&self) -> bool {
|
pub fn any_updated(&self) -> bool {
|
||||||
self.smokeapi_updated || self.creamlinux_updated
|
self.smokeapi_updated
|
||||||
|
|| self.creamlinux_updated
|
||||||
|
|| self.screamapi_updated
|
||||||
|
|| self.koaloader_updated
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
+16
@@ -83,6 +83,22 @@ pub fn get_koaloader_dir() -> Result<PathBuf, String> {
|
|||||||
Ok(dir)
|
Ok(dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete all cached unlocker binaries so they're re-downloaded on next
|
||||||
|
// launch. Deliberately leaves reports.json and the anonymous salt file
|
||||||
|
// alone.
|
||||||
|
pub fn clear_unlocker_cache() -> Result<(), String> {
|
||||||
|
let cache_dir = get_cache_dir()?;
|
||||||
|
for name in ["smokeapi", "screamapi", "koaloader", "creamlinux"] {
|
||||||
|
let dir = cache_dir.join(name);
|
||||||
|
if dir.exists() {
|
||||||
|
fs::remove_dir_all(&dir)
|
||||||
|
.map_err(|e| format!("Failed to clear {} cache: {}", name, e))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info!("Cleared cached unlocker binaries");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
// Get the CreamLinux cache directory path
|
// Get the CreamLinux cache directory path
|
||||||
pub fn get_creamlinux_dir() -> Result<PathBuf, String> {
|
pub fn get_creamlinux_dir() -> Result<PathBuf, String> {
|
||||||
let cache_dir = get_cache_dir()?;
|
let cache_dir = get_cache_dir()?;
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
mod file_ops;
|
|
||||||
|
|
||||||
use crate::cache::{
|
use crate::cache::{
|
||||||
remove_creamlinux_version, remove_smokeapi_version,
|
remove_creamlinux_version, remove_smokeapi_version,
|
||||||
update_game_creamlinux_version, update_game_smokeapi_version,
|
update_game_creamlinux_version, update_game_smokeapi_version,
|
||||||
@@ -524,65 +522,46 @@ pub async fn install_koaloader(
|
|||||||
|
|
||||||
let exe_path = crate::unlockers::Koaloader::resolve_exe_pub(&game.install_path, &game.executable)?;
|
let exe_path = crate::unlockers::Koaloader::resolve_exe_pub(&game.install_path, &game.executable)?;
|
||||||
let exe_dir = exe_path.parent().ok_or("Failed to get executable directory")?;
|
let exe_dir = exe_path.parent().ok_or("Failed to get executable directory")?;
|
||||||
let is_64bit = crate::pe_inspector::is_64bit_exe(&exe_path);
|
|
||||||
|
|
||||||
emit_progress(
|
emit_progress(
|
||||||
&app_handle,
|
&app_handle,
|
||||||
&format!("Installing Koaloader for {}", title),
|
&format!("Installing Koaloader for {}", title),
|
||||||
"Scanning PE imports for best proxy DLL...",
|
"Scanning PE imports for best proxy DLL...",
|
||||||
30.0, false, false, None,
|
30.0, false, false, None,
|
||||||
);
|
);
|
||||||
|
|
||||||
let scan = crate::pe_inspector::find_best_proxy(&exe_path);
|
// Detects bitness, scans PE imports, and copies the matching proxy DLL
|
||||||
let proxy_stem = scan.proxy_name.trim_end_matches(".dll").to_string();
|
let scan = crate::unlockers::Koaloader::install_proxy(&exe_path)?;
|
||||||
let is_fallback = scan.is_fallback;
|
let is_fallback = scan.is_fallback;
|
||||||
|
|
||||||
info!("Selected proxy: {} (fallback={})", scan.proxy_name, is_fallback);
|
|
||||||
|
|
||||||
emit_progress(
|
emit_progress(
|
||||||
&app_handle,
|
&app_handle,
|
||||||
&format!("Installing Koaloader for {}", title),
|
&format!("Installing Koaloader for {}", title),
|
||||||
&format!("Installing proxy DLL ({})...", scan.proxy_name),
|
&format!("Installed proxy DLL ({})", scan.proxy_name),
|
||||||
50.0, false, false, None,
|
50.0, false, false, None,
|
||||||
);
|
);
|
||||||
|
|
||||||
let proxy_src = crate::unlockers::Koaloader::get_proxy_dll(&proxy_stem, is_64bit)?;
|
|
||||||
std::fs::copy(&proxy_src, exe_dir.join(&scan.proxy_name))
|
|
||||||
.map_err(|e| format!("Failed to copy Koaloader proxy DLL: {}", e))?;
|
|
||||||
|
|
||||||
emit_progress(
|
emit_progress(
|
||||||
&app_handle,
|
&app_handle,
|
||||||
&format!("Installing Koaloader for {}", title),
|
&format!("Installing Koaloader for {}", title),
|
||||||
"Installing ScreamAPI payload...",
|
"Installing ScreamAPI payload...",
|
||||||
70.0, false, false, None,
|
70.0, false, false, None,
|
||||||
);
|
);
|
||||||
|
|
||||||
let exe_dir_str = exe_dir.to_string_lossy().to_string();
|
let exe_dir_str = exe_dir.to_string_lossy().to_string();
|
||||||
ScreamAPI::install_to_game(&exe_dir_str, "koaloader")
|
ScreamAPI::install_to_game(&exe_dir_str, "koaloader")
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Failed to install ScreamAPI payload: {}", e))?;
|
.map_err(|e| format!("Failed to install ScreamAPI payload: {}", e))?;
|
||||||
|
|
||||||
emit_progress(
|
emit_progress(
|
||||||
&app_handle,
|
&app_handle,
|
||||||
&format!("Installing Koaloader for {}", title),
|
&format!("Installing Koaloader for {}", title),
|
||||||
"Writing configuration files...",
|
"Writing configuration files...",
|
||||||
88.0, false, false, None,
|
88.0, false, false, None,
|
||||||
);
|
);
|
||||||
|
|
||||||
let exe_name = exe_path.file_name().unwrap_or_default().to_string_lossy().to_string();
|
crate::unlockers::Koaloader::write_koaloader_config(&exe_path)?;
|
||||||
let koa_config = serde_json::json!({
|
|
||||||
"logging": false,
|
|
||||||
"enabled": true,
|
|
||||||
"auto_load": true,
|
|
||||||
"targets": [exe_name],
|
|
||||||
"modules": []
|
|
||||||
});
|
|
||||||
std::fs::write(
|
|
||||||
exe_dir.join("Koaloader.config.json"),
|
|
||||||
serde_json::to_string_pretty(&koa_config).unwrap(),
|
|
||||||
)
|
|
||||||
.map_err(|e| format!("Failed to write Koaloader config: {}", e))?;
|
|
||||||
|
|
||||||
emit_progress(
|
emit_progress(
|
||||||
&app_handle,
|
&app_handle,
|
||||||
&format!("Installation Complete: {}", title),
|
&format!("Installation Complete: {}", title),
|
||||||
@@ -608,26 +587,10 @@ pub async fn uninstall_koaloader(game: EpicGame, app_handle: AppHandle) -> Resul
|
|||||||
let exe_path = crate::unlockers::Koaloader::resolve_exe_pub(&game.install_path, &game.executable)?;
|
let exe_path = crate::unlockers::Koaloader::resolve_exe_pub(&game.install_path, &game.executable)?;
|
||||||
let exe_dir = exe_path.parent().ok_or("Failed to get executable directory")?;
|
let exe_dir = exe_path.parent().ok_or("Failed to get executable directory")?;
|
||||||
let exe_dir_str = exe_dir.to_string_lossy().to_string();
|
let exe_dir_str = exe_dir.to_string_lossy().to_string();
|
||||||
|
|
||||||
// Remove Koaloader config
|
// Removes Koaloader.config.json and any proxy DLL variant
|
||||||
let koa_config_path = exe_dir.join("Koaloader.config.json");
|
crate::unlockers::Koaloader::remove_proxy_and_config(exe_dir)?;
|
||||||
if koa_config_path.exists() {
|
|
||||||
std::fs::remove_file(&koa_config_path)
|
|
||||||
.map_err(|e| format!("Failed to remove Koaloader config: {}", e))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove any Koaloader proxy DLL
|
|
||||||
if let Ok(entries) = std::fs::read_dir(exe_dir) {
|
|
||||||
for entry in entries.filter_map(Result::ok) {
|
|
||||||
let path = entry.path();
|
|
||||||
let name_lower = path.file_name().unwrap_or_default().to_string_lossy().to_lowercase();
|
|
||||||
if crate::unlockers::koaloader::KOA_VARIANTS.contains(&name_lower.as_str()) {
|
|
||||||
std::fs::remove_file(&path).ok();
|
|
||||||
info!("Removed proxy DLL: {}", path.display());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
emit_progress(
|
emit_progress(
|
||||||
&app_handle,
|
&app_handle,
|
||||||
&format!("Uninstalling Koaloader from {}", title),
|
&format!("Uninstalling Koaloader from {}", title),
|
||||||
|
|||||||
+66
-5
@@ -15,6 +15,7 @@ mod config;
|
|||||||
mod epic_scanner;
|
mod epic_scanner;
|
||||||
mod pe_inspector;
|
mod pe_inspector;
|
||||||
mod screamapi_config;
|
mod screamapi_config;
|
||||||
|
mod system_info;
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::unlockers::{CreamLinux, SmokeAPI, Unlocker};
|
use crate::unlockers::{CreamLinux, SmokeAPI, Unlocker};
|
||||||
@@ -83,6 +84,49 @@ fn update_config(config_data: Config) -> Result<Config, String> {
|
|||||||
Ok(config_data)
|
Ok(config_data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add a custom Steam library path (validated to contain a steamapps folder)
|
||||||
|
#[tauri::command]
|
||||||
|
fn add_custom_steam_path(path: String) -> Result<Config, String> {
|
||||||
|
let normalized = searcher::normalize_steam_library_path(std::path::Path::new(&path))?;
|
||||||
|
let normalized_str = normalized.to_string_lossy().to_string();
|
||||||
|
|
||||||
|
info!("Adding custom Steam library path: {}", normalized_str);
|
||||||
|
|
||||||
|
config::update_config(|cfg| {
|
||||||
|
if !cfg.custom_steam_paths.contains(&normalized_str) {
|
||||||
|
cfg.custom_steam_paths.push(normalized_str.clone());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove a custom Steam library path
|
||||||
|
#[tauri::command]
|
||||||
|
fn remove_custom_steam_path(path: String) -> Result<Config, String> {
|
||||||
|
info!("Removing custom Steam library path: {}", path);
|
||||||
|
|
||||||
|
config::update_config(|cfg| {
|
||||||
|
cfg.custom_steam_paths.retain(|p| p != &path);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset configuration to defaults
|
||||||
|
#[tauri::command]
|
||||||
|
fn reset_config() -> Result<Config, String> {
|
||||||
|
config::reset_config()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the config folder in the system's file manager
|
||||||
|
#[tauri::command]
|
||||||
|
fn open_config_folder() -> Result<(), String> {
|
||||||
|
config::open_config_folder()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic host info (OS/CPU/GPU) shown on the Overview page
|
||||||
|
#[tauri::command]
|
||||||
|
fn get_system_info() -> system_info::SystemInfo {
|
||||||
|
system_info::get_system_info()
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn get_all_dlcs_command(game_path: String) -> Result<Vec<DlcInfoWithState>, String> {
|
fn get_all_dlcs_command(game_path: String) -> Result<Vec<DlcInfoWithState>, String> {
|
||||||
info!("Getting all DLCs (enabled and disabled) for: {}", game_path);
|
info!("Getting all DLCs (enabled and disabled) for: {}", game_path);
|
||||||
@@ -105,7 +149,18 @@ async fn scan_steam_games(
|
|||||||
info!("Starting Steam games scan");
|
info!("Starting Steam games scan");
|
||||||
emit_scan_progress(&app_handle, "Locating Steam libraries...", 10);
|
emit_scan_progress(&app_handle, "Locating Steam libraries...", 10);
|
||||||
|
|
||||||
let paths = searcher::get_default_steam_paths();
|
let mut paths = searcher::get_default_steam_paths();
|
||||||
|
|
||||||
|
// Add user-configured custom library paths, in case a library lives
|
||||||
|
// somewhere the auto-detection doesn't know to look.
|
||||||
|
let cfg = config::load_config()?;
|
||||||
|
for custom_path in &cfg.custom_steam_paths {
|
||||||
|
let p = std::path::PathBuf::from(custom_path);
|
||||||
|
if p.exists() && !paths.contains(&p) {
|
||||||
|
info!("Adding custom Steam library path: {}", p.display());
|
||||||
|
paths.push(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
emit_scan_progress(&app_handle, "Finding Steam libraries...", 15);
|
emit_scan_progress(&app_handle, "Finding Steam libraries...", 15);
|
||||||
let libraries = searcher::find_steam_libraries(&paths);
|
let libraries = searcher::find_steam_libraries(&paths);
|
||||||
@@ -454,8 +509,8 @@ async fn stream_game_dlcs(game_id: String, app_handle: tauri::AppHandle) -> Resu
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn clear_caches() -> Result<(), String> {
|
fn clear_caches() -> Result<(), String> {
|
||||||
info!("Data flush requested - cleaning in-memory state only");
|
info!("Clearing cached unlocker binaries");
|
||||||
Ok(())
|
cache::clear_unlocker_cache()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -844,6 +899,11 @@ fn main() {
|
|||||||
resolve_platform_conflict,
|
resolve_platform_conflict,
|
||||||
load_config,
|
load_config,
|
||||||
update_config,
|
update_config,
|
||||||
|
add_custom_steam_path,
|
||||||
|
remove_custom_steam_path,
|
||||||
|
reset_config,
|
||||||
|
open_config_folder,
|
||||||
|
get_system_info,
|
||||||
set_reporting_opt_in,
|
set_reporting_opt_in,
|
||||||
submit_report,
|
submit_report,
|
||||||
get_local_reports,
|
get_local_reports,
|
||||||
@@ -891,8 +951,9 @@ fn main() {
|
|||||||
Ok(result) => {
|
Ok(result) => {
|
||||||
if result.any_updated() {
|
if result.any_updated() {
|
||||||
info!(
|
info!(
|
||||||
"Updates found - SmokeAPI: {:?}, CreamLinux: {:?}",
|
"Updates found - SmokeAPI: {:?}, CreamLinux: {:?}, ScreamAPI: {:?}, Koaloader: {:?}",
|
||||||
result.new_smokeapi_version, result.new_creamlinux_version
|
result.new_smokeapi_version, result.new_creamlinux_version,
|
||||||
|
result.new_screamapi_version, result.new_koaloader_version
|
||||||
);
|
);
|
||||||
|
|
||||||
// Step 3: Update outdated games
|
// Step 3: Update outdated games
|
||||||
|
|||||||
@@ -72,6 +72,31 @@ pub fn get_default_steam_paths() -> Vec<PathBuf> {
|
|||||||
paths
|
paths
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Validates a user-picked directory and resolves it to the library root
|
||||||
|
/// `find_steam_libraries` expects (i.e. the folder that directly contains
|
||||||
|
/// `steamapps`, not the `steamapps` folder itself).
|
||||||
|
pub fn normalize_steam_library_path(path: &Path) -> Result<PathBuf, String> {
|
||||||
|
if !path.is_dir() {
|
||||||
|
return Err(format!("{} is not a valid directory", path.display()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if path.join("steamapps").is_dir() {
|
||||||
|
return Ok(path.to_path_buf());
|
||||||
|
}
|
||||||
|
|
||||||
|
// User picked the steamapps folder itself - use its parent as the root
|
||||||
|
if path.file_name().and_then(|n| n.to_str()) == Some("steamapps") {
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
return Ok(parent.to_path_buf());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(format!(
|
||||||
|
"No Steam library found at {} (expected a 'steamapps' folder inside it)",
|
||||||
|
path.display()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
// Try to read the Steam registry file to find installation paths
|
// Try to read the Steam registry file to find installation paths
|
||||||
fn read_steam_registry() -> Option<Vec<PathBuf>> {
|
fn read_steam_registry() -> Option<Vec<PathBuf>> {
|
||||||
let home = match std::env::var("HOME") {
|
let home = match std::env::var("HOME") {
|
||||||
@@ -742,4 +767,41 @@ pub async fn find_installed_games(steamapps_paths: &[PathBuf]) -> Vec<GameInfo>
|
|||||||
|
|
||||||
info!("Found {} installed games", games.len());
|
info!("Found {} installed games", games.len());
|
||||||
games
|
games
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normalize_accepts_folder_containing_steamapps() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
fs::create_dir(dir.path().join("steamapps")).unwrap();
|
||||||
|
|
||||||
|
let result = normalize_steam_library_path(dir.path()).unwrap();
|
||||||
|
assert_eq!(result, dir.path());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normalize_accepts_steamapps_folder_itself() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let steamapps = dir.path().join("steamapps");
|
||||||
|
fs::create_dir(&steamapps).unwrap();
|
||||||
|
|
||||||
|
let result = normalize_steam_library_path(&steamapps).unwrap();
|
||||||
|
assert_eq!(result, dir.path());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normalize_rejects_folder_without_steamapps() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
assert!(normalize_steam_library_path(dir.path()).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normalize_rejects_nonexistent_path() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let missing = dir.path().join("does-not-exist");
|
||||||
|
assert!(normalize_steam_library_path(&missing).is_err());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
use serde::Serialize;
|
||||||
|
use std::fs;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
// Basic host info shown on the Overview page, purely informational.
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct SystemInfo {
|
||||||
|
pub os_name: String,
|
||||||
|
pub cpu_model: String,
|
||||||
|
pub cpu_cores: usize,
|
||||||
|
pub gpu_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_os_name() -> String {
|
||||||
|
if let Ok(contents) = fs::read_to_string("/etc/os-release") {
|
||||||
|
for line in contents.lines() {
|
||||||
|
if let Some(value) = line.strip_prefix("PRETTY_NAME=") {
|
||||||
|
return value.trim_matches('"').to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
format!("{} (unknown distro)", std::env::consts::OS)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_cpu_model() -> String {
|
||||||
|
if let Ok(contents) = fs::read_to_string("/proc/cpuinfo") {
|
||||||
|
for line in contents.lines() {
|
||||||
|
if let Some((key, value)) = line.split_once(':') {
|
||||||
|
if key.trim() == "model name" {
|
||||||
|
return value.trim().to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"Unknown CPU".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
// This can't tell *which* SKU in that family you actually have (lspci
|
||||||
|
// doesn't expose that either) so it's a best guess, not a precise model.
|
||||||
|
fn simplify_gpu_name(raw: &str) -> String {
|
||||||
|
let without_rev = raw.split(" (rev").next().unwrap_or(raw).trim();
|
||||||
|
|
||||||
|
// The marketing name is almost always the last bracketed group.
|
||||||
|
let brackets: Vec<&str> = without_rev
|
||||||
|
.split('[')
|
||||||
|
.skip(1)
|
||||||
|
.filter_map(|s| s.split(']').next())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let model = brackets
|
||||||
|
.last()
|
||||||
|
.and_then(|group| group.split('/').next())
|
||||||
|
.unwrap_or(without_rev)
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
let vendor = if without_rev.contains("NVIDIA") {
|
||||||
|
Some("NVIDIA")
|
||||||
|
} else if without_rev.contains("AMD") || without_rev.contains("ATI") {
|
||||||
|
Some("AMD")
|
||||||
|
} else if without_rev.contains("Intel") {
|
||||||
|
Some("Intel")
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
match vendor {
|
||||||
|
Some(v) if !model.to_uppercase().contains(&v.to_uppercase()) => format!("{} {}", v, model),
|
||||||
|
_ => model.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Best-effort: parse `lspci` for the primary display controller. Not every
|
||||||
|
// system has `lspci` (minimal containers, some distros), so this degrades
|
||||||
|
// to "Unknown GPU" instead of failing the whole system-info fetch.
|
||||||
|
fn read_gpu_name() -> String {
|
||||||
|
let output = match Command::new("lspci").output() {
|
||||||
|
Ok(output) if output.status.success() => output,
|
||||||
|
_ => return "Unknown GPU".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
for line in stdout.lines() {
|
||||||
|
if line.contains("VGA compatible controller") || line.contains("3D controller") {
|
||||||
|
if let Some((_, rest)) = line.split_once(": ") {
|
||||||
|
return simplify_gpu_name(rest.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"Unknown GPU".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_system_info() -> SystemInfo {
|
||||||
|
SystemInfo {
|
||||||
|
os_name: read_os_name(),
|
||||||
|
cpu_model: read_cpu_model(),
|
||||||
|
cpu_cores: num_cpus::get(),
|
||||||
|
gpu_name: read_gpu_name(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn simplifies_amd_gpu_names() {
|
||||||
|
assert_eq!(
|
||||||
|
simplify_gpu_name(
|
||||||
|
"Advanced Micro Devices, Inc. [AMD/ATI] Navi 33 [Radeon RX 7600/7600 XT/7600M XT/7600S/7700S / PRO W7600] (rev cf)"
|
||||||
|
),
|
||||||
|
"AMD Radeon RX 7600"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn simplifies_nvidia_gpu_names() {
|
||||||
|
assert_eq!(
|
||||||
|
simplify_gpu_name("NVIDIA Corporation GA104 [GeForce RTX 3070/3070 Ti] (rev a1)"),
|
||||||
|
"NVIDIA GeForce RTX 3070"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn simplifies_intel_gpu_names() {
|
||||||
|
assert_eq!(
|
||||||
|
simplify_gpu_name("Intel Corporation TigerLake-LP GT2 [Iris Xe Graphics] (rev 01)"),
|
||||||
|
"Intel Iris Xe Graphics"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,14 +10,7 @@ use tempfile::tempdir;
|
|||||||
use zip::ZipArchive;
|
use zip::ZipArchive;
|
||||||
|
|
||||||
const KOALOADER_REPO: &str = "acidicoala/Koaloader";
|
const KOALOADER_REPO: &str = "acidicoala/Koaloader";
|
||||||
|
pub use crate::pe_inspector::KOA_VARIANTS;
|
||||||
pub const KOA_VARIANTS: &[&str] = &[
|
|
||||||
"version.dll", "winmm.dll", "winhttp.dll", "iphlpapi.dll", "dinput8.dll",
|
|
||||||
"d3d11.dll", "dxgi.dll", "d3d9.dll", "d3d10.dll", "dwmapi.dll", "hid.dll",
|
|
||||||
"msimg32.dll", "mswsock.dll", "opengl32.dll", "profapi.dll", "propsys.dll",
|
|
||||||
"textshaping.dll", "glu32.dll", "audioses.dll", "msasn1.dll", "wldp.dll",
|
|
||||||
"xinput9_1_0.dll",
|
|
||||||
];
|
|
||||||
|
|
||||||
pub struct Koaloader;
|
pub struct Koaloader;
|
||||||
|
|
||||||
@@ -159,35 +152,14 @@ impl Unlocker for Koaloader {
|
|||||||
/// context = relative executable path (e.g. "en_us/Sources/Bin/SnowRunner.exe")
|
/// context = relative executable path (e.g. "en_us/Sources/Bin/SnowRunner.exe")
|
||||||
/// Progress events are emitted by installer/mod.rs, not here.
|
/// Progress events are emitted by installer/mod.rs, not here.
|
||||||
async fn install_to_game(game_path: &str, context: &str) -> Result<(), String> {
|
async fn install_to_game(game_path: &str, context: &str) -> Result<(), String> {
|
||||||
// Install without progress called internally (e.g. from installer/mod.rs
|
|
||||||
// after it has already emitted its own progress steps)
|
|
||||||
let exe_path = Self::resolve_exe(game_path, context)?;
|
let exe_path = Self::resolve_exe(game_path, context)?;
|
||||||
|
Self::install_proxy(&exe_path)?;
|
||||||
|
|
||||||
let exe_dir = exe_path.parent().ok_or("Failed to get executable directory")?;
|
let exe_dir = exe_path.parent().ok_or("Failed to get executable directory")?;
|
||||||
|
|
||||||
let is_64bit = crate::pe_inspector::is_64bit_exe(&exe_path);
|
|
||||||
let scan = crate::pe_inspector::find_best_proxy(&exe_path);
|
|
||||||
let proxy_stem = scan.proxy_name.trim_end_matches(".dll").to_string();
|
|
||||||
|
|
||||||
let proxy_src = Self::get_proxy_dll(&proxy_stem, is_64bit)?;
|
|
||||||
fs::copy(&proxy_src, exe_dir.join(&scan.proxy_name))
|
|
||||||
.map_err(|e| format!("Failed to copy Koaloader proxy DLL: {}", e))?;
|
|
||||||
|
|
||||||
let exe_dir_str = exe_dir.to_string_lossy().to_string();
|
let exe_dir_str = exe_dir.to_string_lossy().to_string();
|
||||||
crate::unlockers::ScreamAPI::install_to_game(&exe_dir_str, "koaloader").await?;
|
crate::unlockers::ScreamAPI::install_to_game(&exe_dir_str, "koaloader").await?;
|
||||||
|
|
||||||
let exe_name = exe_path.file_name().unwrap_or_default().to_string_lossy().to_string();
|
Self::write_koaloader_config(&exe_path)?;
|
||||||
let koa_config = serde_json::json!({
|
|
||||||
"logging": false,
|
|
||||||
"enabled": true,
|
|
||||||
"auto_load": true,
|
|
||||||
"targets": [exe_name],
|
|
||||||
"modules": []
|
|
||||||
});
|
|
||||||
fs::write(
|
|
||||||
exe_dir.join("Koaloader.config.json"),
|
|
||||||
serde_json::to_string_pretty(&koa_config).unwrap(),
|
|
||||||
)
|
|
||||||
.map_err(|e| format!("Failed to write Koaloader config: {}", e))?;
|
|
||||||
|
|
||||||
info!("Koaloader installation complete for: {}", game_path);
|
info!("Koaloader installation complete for: {}", game_path);
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -198,26 +170,7 @@ impl Unlocker for Koaloader {
|
|||||||
let exe_dir = exe_path.parent().ok_or("Failed to get executable directory")?;
|
let exe_dir = exe_path.parent().ok_or("Failed to get executable directory")?;
|
||||||
let exe_dir_str = exe_dir.to_string_lossy().to_string();
|
let exe_dir_str = exe_dir.to_string_lossy().to_string();
|
||||||
|
|
||||||
let koa_config = exe_dir.join("Koaloader.config.json");
|
Self::remove_proxy_and_config(exe_dir)?;
|
||||||
if koa_config.exists() {
|
|
||||||
fs::remove_file(&koa_config)
|
|
||||||
.map_err(|e| format!("Failed to remove Koaloader config: {}", e))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(entries) = fs::read_dir(exe_dir) {
|
|
||||||
for entry in entries.filter_map(Result::ok) {
|
|
||||||
let path = entry.path();
|
|
||||||
let name_lower = path
|
|
||||||
.file_name()
|
|
||||||
.unwrap_or_default()
|
|
||||||
.to_string_lossy()
|
|
||||||
.to_lowercase();
|
|
||||||
if KOA_VARIANTS.contains(&name_lower.as_str()) {
|
|
||||||
fs::remove_file(&path).ok();
|
|
||||||
info!("Removed proxy DLL: {}", path.display());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
crate::unlockers::ScreamAPI::uninstall_from_game(&exe_dir_str, "koaloader").await?;
|
crate::unlockers::ScreamAPI::uninstall_from_game(&exe_dir_str, "koaloader").await?;
|
||||||
|
|
||||||
@@ -266,6 +219,71 @@ impl Koaloader {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Detects bitness, scans for the best-match Koaloader proxy, and copies
|
||||||
|
/// it next to `exe_path`. Returns the scan result so callers can report
|
||||||
|
/// which proxy was used and whether it was a version.dll fallback.
|
||||||
|
/// Shared by the Unlocker trait impl above and installer/mod.rs (which
|
||||||
|
/// wraps this with progress events).
|
||||||
|
pub(crate) fn install_proxy(
|
||||||
|
exe_path: &Path,
|
||||||
|
) -> Result<crate::pe_inspector::ProxyScanResult, String> {
|
||||||
|
let exe_dir = exe_path.parent().ok_or("Failed to get executable directory")?;
|
||||||
|
let is_64bit = crate::pe_inspector::is_64bit_exe(exe_path);
|
||||||
|
let scan = crate::pe_inspector::find_best_proxy(exe_path);
|
||||||
|
let proxy_stem = scan.proxy_name.trim_end_matches(".dll").to_string();
|
||||||
|
|
||||||
|
let proxy_src = Self::get_proxy_dll(&proxy_stem, is_64bit)?;
|
||||||
|
fs::copy(&proxy_src, exe_dir.join(&scan.proxy_name))
|
||||||
|
.map_err(|e| format!("Failed to copy Koaloader proxy DLL: {}", e))?;
|
||||||
|
|
||||||
|
info!("Selected proxy: {} (fallback={})", scan.proxy_name, scan.is_fallback);
|
||||||
|
Ok(scan)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes Koaloader.config.json targeting the given executable, next to it.
|
||||||
|
pub(crate) fn write_koaloader_config(exe_path: &Path) -> Result<(), String> {
|
||||||
|
let exe_dir = exe_path.parent().ok_or("Failed to get executable directory")?;
|
||||||
|
let exe_name = exe_path.file_name().unwrap_or_default().to_string_lossy().to_string();
|
||||||
|
let koa_config = serde_json::json!({
|
||||||
|
"logging": false,
|
||||||
|
"enabled": true,
|
||||||
|
"auto_load": true,
|
||||||
|
"targets": [exe_name],
|
||||||
|
"modules": []
|
||||||
|
});
|
||||||
|
fs::write(
|
||||||
|
exe_dir.join("Koaloader.config.json"),
|
||||||
|
serde_json::to_string_pretty(&koa_config).unwrap(),
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("Failed to write Koaloader config: {}", e))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes Koaloader.config.json and any installed proxy DLL from `exe_dir`.
|
||||||
|
pub(crate) fn remove_proxy_and_config(exe_dir: &Path) -> Result<(), String> {
|
||||||
|
let koa_config = exe_dir.join("Koaloader.config.json");
|
||||||
|
if koa_config.exists() {
|
||||||
|
fs::remove_file(&koa_config)
|
||||||
|
.map_err(|e| format!("Failed to remove Koaloader config: {}", e))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(entries) = fs::read_dir(exe_dir) {
|
||||||
|
for entry in entries.filter_map(Result::ok) {
|
||||||
|
let path = entry.path();
|
||||||
|
let name_lower = path
|
||||||
|
.file_name()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_lowercase();
|
||||||
|
if KOA_VARIANTS.contains(&name_lower.as_str()) {
|
||||||
|
fs::remove_file(&path).ok();
|
||||||
|
info!("Removed proxy DLL: {}", path.display());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_proxy_dll(proxy_stem: &str, is_64bit: bool) -> Result<std::path::PathBuf, String> {
|
pub fn get_proxy_dll(proxy_stem: &str, is_64bit: bool) -> Result<std::path::PathBuf, String> {
|
||||||
let versions = crate::cache::read_versions()?;
|
let versions = crate::cache::read_versions()?;
|
||||||
if versions.koaloader.latest.is_empty() {
|
if versions.koaloader.latest.is_empty() {
|
||||||
|
|||||||
Reference in New Issue
Block a user