backend stuff lol

This commit is contained in:
Tickbase
2026-07-11 12:22:17 +02:00
parent 7ba8697cb7
commit 3dd15624de
7 changed files with 434 additions and 114 deletions
+74 -4
View File
@@ -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
} }
} }
+16
View File
@@ -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()?;
+16 -53
View File
@@ -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
View File
@@ -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
+62
View File
@@ -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());
}
} }
+130
View File
@@ -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"
);
}
}
+70 -52
View File
@@ -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() {