mirror of
https://github.com/Novattz/creamlinux-installer.git
synced 2026-05-03 21:42:04 -04:00
289 lines
10 KiB
Rust
289 lines
10 KiB
Rust
use super::Unlocker;
|
|
use async_trait::async_trait;
|
|
use log::info;
|
|
use reqwest;
|
|
use std::fs;
|
|
use std::io;
|
|
use std::path::Path;
|
|
use std::time::Duration;
|
|
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 struct Koaloader;
|
|
|
|
#[async_trait]
|
|
impl Unlocker for Koaloader {
|
|
async fn get_latest_version() -> Result<String, String> {
|
|
info!("Fetching latest Koaloader version...");
|
|
|
|
let client = reqwest::Client::new();
|
|
let releases_url = format!(
|
|
"https://api.github.com/repos/{}/releases/latest",
|
|
KOALOADER_REPO
|
|
);
|
|
|
|
let response = client
|
|
.get(&releases_url)
|
|
.header("User-Agent", "CreamLinux")
|
|
.timeout(Duration::from_secs(10))
|
|
.send()
|
|
.await
|
|
.map_err(|e| format!("Failed to fetch Koaloader releases: {}", e))?;
|
|
|
|
if !response.status().is_success() {
|
|
return Err(format!(
|
|
"Failed to fetch Koaloader releases: HTTP {}",
|
|
response.status()
|
|
));
|
|
}
|
|
|
|
let release_info: serde_json::Value = response
|
|
.json()
|
|
.await
|
|
.map_err(|e| format!("Failed to parse release info: {}", e))?;
|
|
|
|
let version = release_info
|
|
.get("tag_name")
|
|
.and_then(|v| v.as_str())
|
|
.ok_or_else(|| "Failed to extract version from release info".to_string())?
|
|
.to_string();
|
|
|
|
info!("Latest Koaloader version: {}", version);
|
|
Ok(version)
|
|
}
|
|
|
|
async fn download_to_cache() -> Result<String, String> {
|
|
let version = Self::get_latest_version().await?;
|
|
info!("Downloading Koaloader version {}...", version);
|
|
|
|
let client = reqwest::Client::new();
|
|
|
|
let releases_url = format!(
|
|
"https://api.github.com/repos/{}/releases/latest",
|
|
KOALOADER_REPO
|
|
);
|
|
let release_info: serde_json::Value = client
|
|
.get(&releases_url)
|
|
.header("User-Agent", "CreamLinux")
|
|
.timeout(Duration::from_secs(10))
|
|
.send()
|
|
.await
|
|
.map_err(|e| format!("Failed to fetch Koaloader release: {}", e))?
|
|
.json()
|
|
.await
|
|
.map_err(|e| format!("Failed to parse release info: {}", e))?;
|
|
|
|
let zip_url = release_info
|
|
.get("assets")
|
|
.and_then(|a| a.as_array())
|
|
.and_then(|assets| {
|
|
assets.iter().find(|asset| {
|
|
asset
|
|
.get("name")
|
|
.and_then(|n| n.as_str())
|
|
.map(|n| n.ends_with(".zip"))
|
|
.unwrap_or(false)
|
|
})
|
|
})
|
|
.and_then(|asset| asset.get("browser_download_url"))
|
|
.and_then(|u| u.as_str())
|
|
.ok_or_else(|| "No zip asset found in Koaloader release".to_string())?
|
|
.to_string();
|
|
|
|
let response = client
|
|
.get(&zip_url)
|
|
.timeout(Duration::from_secs(60))
|
|
.send()
|
|
.await
|
|
.map_err(|e| format!("Failed to download Koaloader: {}", e))?;
|
|
|
|
if !response.status().is_success() {
|
|
return Err(format!(
|
|
"Failed to download Koaloader: HTTP {}",
|
|
response.status()
|
|
));
|
|
}
|
|
|
|
let temp_dir = tempdir().map_err(|e| format!("Failed to create temp dir: {}", e))?;
|
|
let zip_path = temp_dir.path().join("koaloader.zip");
|
|
let content = response
|
|
.bytes()
|
|
.await
|
|
.map_err(|e| format!("Failed to read response bytes: {}", e))?;
|
|
fs::write(&zip_path, &content)
|
|
.map_err(|e| format!("Failed to write zip file: {}", e))?;
|
|
|
|
let version_dir = crate::cache::get_koaloader_version_dir(&version)?;
|
|
let file =
|
|
fs::File::open(&zip_path).map_err(|e| format!("Failed to open zip: {}", e))?;
|
|
let mut archive =
|
|
ZipArchive::new(file).map_err(|e| format!("Failed to read zip archive: {}", e))?;
|
|
|
|
for i in 0..archive.len() {
|
|
let mut file = archive
|
|
.by_index(i)
|
|
.map_err(|e| format!("Failed to access zip entry: {}", e))?;
|
|
|
|
let zip_entry = file.name().to_string();
|
|
if zip_entry.ends_with('/') {
|
|
continue;
|
|
}
|
|
|
|
let out_path = version_dir.join(&zip_entry);
|
|
if let Some(parent) = out_path.parent() {
|
|
fs::create_dir_all(parent)
|
|
.map_err(|e| format!("Failed to create directory: {}", e))?;
|
|
}
|
|
|
|
let mut outfile = fs::File::create(&out_path).map_err(|e| {
|
|
format!("Failed to create output file {}: {}", out_path.display(), e)
|
|
})?;
|
|
io::copy(&mut file, &mut outfile)
|
|
.map_err(|e| format!("Failed to extract file: {}", e))?;
|
|
}
|
|
|
|
info!("Koaloader version {} downloaded to cache successfully", version);
|
|
Ok(version)
|
|
}
|
|
|
|
/// 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)?;
|
|
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))?;
|
|
|
|
info!("Koaloader installation complete for: {}", game_path);
|
|
Ok(())
|
|
}
|
|
|
|
async fn uninstall_from_game(game_path: &str, context: &str) -> Result<(), String> {
|
|
let exe_path = Self::resolve_exe(game_path, context)?;
|
|
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());
|
|
}
|
|
}
|
|
}
|
|
|
|
crate::unlockers::ScreamAPI::uninstall_from_game(&exe_dir_str, "koaloader").await?;
|
|
|
|
info!("Koaloader uninstallation complete for: {}", game_path);
|
|
Ok(())
|
|
}
|
|
|
|
fn name() -> &'static str {
|
|
"Koaloader"
|
|
}
|
|
}
|
|
|
|
impl Koaloader {
|
|
/// Public wrapper for installer/mod.rs to call.
|
|
pub fn resolve_exe_pub(game_path: &str, exe_relative: &str) -> Result<std::path::PathBuf, String> {
|
|
Self::resolve_exe(game_path, exe_relative)
|
|
}
|
|
|
|
fn resolve_exe(game_path: &str, exe_relative: &str) -> Result<std::path::PathBuf, String> {
|
|
use walkdir::WalkDir;
|
|
|
|
let full = Path::new(game_path).join(exe_relative);
|
|
if full.exists() {
|
|
return Ok(full);
|
|
}
|
|
|
|
let exe_name = Path::new(exe_relative)
|
|
.file_name()
|
|
.unwrap_or_default()
|
|
.to_string_lossy()
|
|
.to_string();
|
|
|
|
for entry in WalkDir::new(game_path)
|
|
.max_depth(8)
|
|
.into_iter()
|
|
.filter_map(Result::ok)
|
|
{
|
|
if entry.file_name().to_string_lossy() == exe_name {
|
|
return Ok(entry.path().to_path_buf());
|
|
}
|
|
}
|
|
|
|
Err(format!(
|
|
"Executable not found: {} (searched in {})",
|
|
exe_relative, game_path
|
|
))
|
|
}
|
|
|
|
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() {
|
|
return Err("Koaloader is not cached. Please restart the app.".to_string());
|
|
}
|
|
|
|
let version_dir = crate::cache::get_koaloader_version_dir(&versions.koaloader.latest)?;
|
|
let bitness = if is_64bit { "64" } else { "32" };
|
|
let folder = format!("{}-{}", proxy_stem, bitness);
|
|
let dll_path = version_dir.join(&folder).join(format!("{}.dll", proxy_stem));
|
|
|
|
if !dll_path.exists() {
|
|
return Err(format!(
|
|
"Koaloader proxy DLL not found in cache: {}",
|
|
dll_path.display()
|
|
));
|
|
}
|
|
|
|
Ok(dll_path)
|
|
}
|
|
} |