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
+73
-3
@@ -6,6 +6,7 @@ pub use storage::{
|
||||
list_creamlinux_files, list_smokeapi_files, read_versions,
|
||||
update_creamlinux_version, update_smokeapi_version, validate_smokeapi_cache,
|
||||
validate_creamlinux_cache, get_cache_dir, get_koaloader_version_dir, get_screamapi_version_dir,
|
||||
clear_unlocker_cache,
|
||||
};
|
||||
|
||||
pub use version::{
|
||||
@@ -81,7 +82,7 @@ pub async fn initialize_cache() -> Result<(), String> {
|
||||
}
|
||||
Ok(false) => {
|
||||
info!("ScreamAPI cache incomplete, re-downloading");
|
||||
needs_smokeapi = true;
|
||||
needs_screamapi = true;
|
||||
}
|
||||
Err(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");
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -333,13 +396,20 @@ pub async fn update_outdated_games(
|
||||
pub struct UpdateResult {
|
||||
pub smokeapi_updated: bool,
|
||||
pub creamlinux_updated: bool,
|
||||
pub screamapi_updated: bool,
|
||||
pub koaloader_updated: bool,
|
||||
pub new_smokeapi_version: Option<String>,
|
||||
pub new_creamlinux_version: Option<String>,
|
||||
pub new_screamapi_version: Option<String>,
|
||||
pub new_koaloader_version: Option<String>,
|
||||
}
|
||||
|
||||
impl UpdateResult {
|
||||
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)
|
||||
}
|
||||
|
||||
// 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
|
||||
pub fn get_creamlinux_dir() -> Result<PathBuf, String> {
|
||||
let cache_dir = get_cache_dir()?;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
mod file_ops;
|
||||
|
||||
use crate::cache::{
|
||||
remove_creamlinux_version, remove_smokeapi_version,
|
||||
update_game_creamlinux_version, update_game_smokeapi_version,
|
||||
@@ -524,7 +522,6 @@ pub async fn install_koaloader(
|
||||
|
||||
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 is_64bit = crate::pe_inspector::is_64bit_exe(&exe_path);
|
||||
|
||||
emit_progress(
|
||||
&app_handle,
|
||||
@@ -533,23 +530,17 @@ pub async fn install_koaloader(
|
||||
30.0, false, false, None,
|
||||
);
|
||||
|
||||
let scan = crate::pe_inspector::find_best_proxy(&exe_path);
|
||||
let proxy_stem = scan.proxy_name.trim_end_matches(".dll").to_string();
|
||||
// Detects bitness, scans PE imports, and copies the matching proxy DLL
|
||||
let scan = crate::unlockers::Koaloader::install_proxy(&exe_path)?;
|
||||
let is_fallback = scan.is_fallback;
|
||||
|
||||
info!("Selected proxy: {} (fallback={})", scan.proxy_name, is_fallback);
|
||||
|
||||
emit_progress(
|
||||
&app_handle,
|
||||
&format!("Installing Koaloader for {}", title),
|
||||
&format!("Installing proxy DLL ({})...", scan.proxy_name),
|
||||
&format!("Installed proxy DLL ({})", scan.proxy_name),
|
||||
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(
|
||||
&app_handle,
|
||||
&format!("Installing Koaloader for {}", title),
|
||||
@@ -569,19 +560,7 @@ pub async fn install_koaloader(
|
||||
88.0, false, false, None,
|
||||
);
|
||||
|
||||
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": []
|
||||
});
|
||||
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))?;
|
||||
crate::unlockers::Koaloader::write_koaloader_config(&exe_path)?;
|
||||
|
||||
emit_progress(
|
||||
&app_handle,
|
||||
@@ -609,24 +588,8 @@ pub async fn uninstall_koaloader(game: EpicGame, app_handle: AppHandle) -> Resul
|
||||
let exe_dir = exe_path.parent().ok_or("Failed to get executable directory")?;
|
||||
let exe_dir_str = exe_dir.to_string_lossy().to_string();
|
||||
|
||||
// Remove Koaloader config
|
||||
let koa_config_path = exe_dir.join("Koaloader.config.json");
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
// Removes Koaloader.config.json and any proxy DLL variant
|
||||
crate::unlockers::Koaloader::remove_proxy_and_config(exe_dir)?;
|
||||
|
||||
emit_progress(
|
||||
&app_handle,
|
||||
|
||||
+66
-5
@@ -15,6 +15,7 @@ mod config;
|
||||
mod epic_scanner;
|
||||
mod pe_inspector;
|
||||
mod screamapi_config;
|
||||
mod system_info;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::unlockers::{CreamLinux, SmokeAPI, Unlocker};
|
||||
@@ -83,6 +84,49 @@ fn update_config(config_data: Config) -> Result<Config, String> {
|
||||
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]
|
||||
fn get_all_dlcs_command(game_path: String) -> Result<Vec<DlcInfoWithState>, String> {
|
||||
info!("Getting all DLCs (enabled and disabled) for: {}", game_path);
|
||||
@@ -105,7 +149,18 @@ async fn scan_steam_games(
|
||||
info!("Starting Steam games scan");
|
||||
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);
|
||||
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]
|
||||
fn clear_caches() -> Result<(), String> {
|
||||
info!("Data flush requested - cleaning in-memory state only");
|
||||
Ok(())
|
||||
info!("Clearing cached unlocker binaries");
|
||||
cache::clear_unlocker_cache()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -844,6 +899,11 @@ fn main() {
|
||||
resolve_platform_conflict,
|
||||
load_config,
|
||||
update_config,
|
||||
add_custom_steam_path,
|
||||
remove_custom_steam_path,
|
||||
reset_config,
|
||||
open_config_folder,
|
||||
get_system_info,
|
||||
set_reporting_opt_in,
|
||||
submit_report,
|
||||
get_local_reports,
|
||||
@@ -891,8 +951,9 @@ fn main() {
|
||||
Ok(result) => {
|
||||
if result.any_updated() {
|
||||
info!(
|
||||
"Updates found - SmokeAPI: {:?}, CreamLinux: {:?}",
|
||||
result.new_smokeapi_version, result.new_creamlinux_version
|
||||
"Updates found - SmokeAPI: {:?}, CreamLinux: {:?}, ScreamAPI: {:?}, Koaloader: {:?}",
|
||||
result.new_smokeapi_version, result.new_creamlinux_version,
|
||||
result.new_screamapi_version, result.new_koaloader_version
|
||||
);
|
||||
|
||||
// Step 3: Update outdated games
|
||||
|
||||
@@ -72,6 +72,31 @@ pub fn get_default_steam_paths() -> Vec<PathBuf> {
|
||||
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
|
||||
fn read_steam_registry() -> Option<Vec<PathBuf>> {
|
||||
let home = match std::env::var("HOME") {
|
||||
@@ -743,3 +768,40 @@ pub async fn find_installed_games(steamapps_paths: &[PathBuf]) -> Vec<GameInfo>
|
||||
info!("Found {} installed games", games.len());
|
||||
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;
|
||||
|
||||
const KOALOADER_REPO: &str = "acidicoala/Koaloader";
|
||||
|
||||
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 use crate::pe_inspector::KOA_VARIANTS;
|
||||
|
||||
pub struct Koaloader;
|
||||
|
||||
@@ -159,35 +152,14 @@ impl Unlocker for Koaloader {
|
||||
/// context = relative executable path (e.g. "en_us/Sources/Bin/SnowRunner.exe")
|
||||
/// Progress events are emitted by installer/mod.rs, not here.
|
||||
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)?;
|
||||
Self::install_proxy(&exe_path)?;
|
||||
|
||||
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();
|
||||
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();
|
||||
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))?;
|
||||
Self::write_koaloader_config(&exe_path)?;
|
||||
|
||||
info!("Koaloader installation complete for: {}", game_path);
|
||||
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_str = exe_dir.to_string_lossy().to_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());
|
||||
}
|
||||
}
|
||||
}
|
||||
Self::remove_proxy_and_config(exe_dir)?;
|
||||
|
||||
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> {
|
||||
let versions = crate::cache::read_versions()?;
|
||||
if versions.koaloader.latest.is_empty() {
|
||||
|
||||
Reference in New Issue
Block a user