Creamlinux Refactor

This commit is contained in:
Novattz
2025-12-22 20:21:06 +01:00
parent 6f4f53f7f5
commit c484c8958c
12 changed files with 2042 additions and 1240 deletions
+246
View File
@@ -0,0 +1,246 @@
mod storage;
mod version;
pub use storage::{
get_creamlinux_version_dir, get_smokeapi_version_dir, is_cache_initialized,
list_creamlinux_files, list_smokeapi_dlls, read_versions, update_creamlinux_version,
update_smokeapi_version,
};
pub use version::{
read_manifest, remove_creamlinux_version, remove_smokeapi_version,
update_creamlinux_version as update_game_creamlinux_version,
update_smokeapi_version as update_game_smokeapi_version,
};
use crate::unlockers::{CreamLinux, SmokeAPI, Unlocker};
use log::{error, info, warn};
use std::collections::HashMap;
// Initialize the cache on app startup
// Downloads both unlockers if they don't exist
pub async fn initialize_cache() -> Result<(), String> {
info!("Initializing cache...");
// Check if cache is already initialized
if is_cache_initialized()? {
info!("Cache already initialized");
return Ok(());
}
info!("Cache not initialized, downloading unlockers...");
// Download SmokeAPI
match SmokeAPI::download_to_cache().await {
Ok(version) => {
info!("Downloaded SmokeAPI version: {}", version);
update_smokeapi_version(&version)?;
}
Err(e) => {
error!("Failed to download SmokeAPI: {}", e);
return Err(format!("Failed to download SmokeAPI: {}", e));
}
}
// Download CreamLinux
match CreamLinux::download_to_cache().await {
Ok(version) => {
info!("Downloaded CreamLinux version: {}", version);
update_creamlinux_version(&version)?;
}
Err(e) => {
error!("Failed to download CreamLinux: {}", e);
return Err(format!("Failed to download CreamLinux: {}", e));
}
}
info!("Cache initialization complete");
Ok(())
}
// Check for updates and download new versions if available
pub async fn check_and_update_cache() -> Result<UpdateResult, String> {
info!("Checking for unlocker updates...");
let mut result = UpdateResult::default();
// Check SmokeAPI
let current_smokeapi = read_versions()?.smokeapi.latest;
match SmokeAPI::get_latest_version().await {
Ok(latest_version) => {
if current_smokeapi != latest_version {
info!(
"SmokeAPI update available: {} -> {}",
current_smokeapi, latest_version
);
match SmokeAPI::download_to_cache().await {
Ok(version) => {
update_smokeapi_version(&version)?;
result.smokeapi_updated = true;
result.new_smokeapi_version = Some(version);
info!("SmokeAPI updated successfully");
}
Err(e) => {
error!("Failed to download SmokeAPI update: {}", e);
return Err(format!("Failed to download SmokeAPI update: {}", e));
}
}
} else {
info!("SmokeAPI is up to date: {}", current_smokeapi);
}
}
Err(e) => {
warn!("Failed to check SmokeAPI version: {}", e);
}
}
// Check CreamLinux
let current_creamlinux = read_versions()?.creamlinux.latest;
match CreamLinux::get_latest_version().await {
Ok(latest_version) => {
if current_creamlinux != latest_version {
info!(
"CreamLinux update available: {} -> {}",
current_creamlinux, latest_version
);
match CreamLinux::download_to_cache().await {
Ok(version) => {
update_creamlinux_version(&version)?;
result.creamlinux_updated = true;
result.new_creamlinux_version = Some(version);
info!("CreamLinux updated successfully");
}
Err(e) => {
error!("Failed to download CreamLinux update: {}", e);
return Err(format!("Failed to download CreamLinux update: {}", e));
}
}
} else {
info!("CreamLinux is up to date: {}", current_creamlinux);
}
}
Err(e) => {
warn!("Failed to check CreamLinux version: {}", e);
}
}
Ok(result)
}
// Update all games that have outdated unlocker versions
pub async fn update_outdated_games(
games: &HashMap<String, crate::installer::Game>,
) -> Result<GameUpdateStats, String> {
info!("Checking for outdated game installations...");
let cached_versions = read_versions()?;
let mut stats = GameUpdateStats::default();
for (game_id, game) in games {
// Read the game's manifest
let manifest = match read_manifest(&game.path) {
Ok(m) => m,
Err(e) => {
warn!("Failed to read manifest for {}: {}", game.title, e);
continue;
}
};
// Check if SmokeAPI needs updating
if manifest.has_smokeapi()
&& manifest.is_smokeapi_outdated(&cached_versions.smokeapi.latest)
{
info!(
"Game '{}' has outdated SmokeAPI, updating...",
game.title
);
// Convert api_files Vec to comma-separated string
let api_files_str = game.api_files.join(",");
match SmokeAPI::install_to_game(&game.path, &api_files_str).await {
Ok(_) => {
update_game_smokeapi_version(&game.path, cached_versions.smokeapi.latest.clone())?;
stats.smokeapi_updated += 1;
info!("Updated SmokeAPI for '{}'", game.title);
}
Err(e) => {
error!("Failed to update SmokeAPI for '{}': {}", game.title, e);
stats.smokeapi_failed += 1;
}
}
}
// Check if CreamLinux needs updating
if manifest.has_creamlinux()
&& manifest.is_creamlinux_outdated(&cached_versions.creamlinux.latest)
{
info!(
"Game '{}' has outdated CreamLinux, updating...",
game.title
);
// For CreamLinux, we need to preserve the DLC configuration
match CreamLinux::install_to_game(&game.path, game_id).await {
Ok(_) => {
update_game_creamlinux_version(&game.path, cached_versions.creamlinux.latest.clone())?;
stats.creamlinux_updated += 1;
info!("Updated CreamLinux for '{}'", game.title);
}
Err(e) => {
error!("Failed to update CreamLinux for '{}': {}", game.title, e);
stats.creamlinux_failed += 1;
}
}
}
}
info!(
"Game update complete - SmokeAPI: {} updated, {} failed | CreamLinux: {} updated, {} failed",
stats.smokeapi_updated,
stats.smokeapi_failed,
stats.creamlinux_updated,
stats.creamlinux_failed
);
Ok(stats)
}
// Result of checking for cache updates
#[derive(Debug, Default, Clone)]
pub struct UpdateResult {
pub smokeapi_updated: bool,
pub creamlinux_updated: bool,
pub new_smokeapi_version: Option<String>,
pub new_creamlinux_version: Option<String>,
}
impl UpdateResult {
pub fn any_updated(&self) -> bool {
self.smokeapi_updated || self.creamlinux_updated
}
}
// Statistics about game updates
#[derive(Debug, Default, Clone)]
pub struct GameUpdateStats {
pub smokeapi_updated: u32,
pub smokeapi_failed: u32,
pub creamlinux_updated: u32,
pub creamlinux_failed: u32,
}
impl GameUpdateStats {
pub fn total_updated(&self) -> u32 {
self.smokeapi_updated + self.creamlinux_updated
}
pub fn total_failed(&self) -> u32 {
self.smokeapi_failed + self.creamlinux_failed
}
pub fn has_failures(&self) -> bool {
self.total_failed() > 0
}
}
+292
View File
@@ -0,0 +1,292 @@
use log::{info, warn};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
// Represents the versions.json file in the cache root
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct CacheVersions {
pub smokeapi: VersionInfo,
pub creamlinux: VersionInfo,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct VersionInfo {
pub latest: String,
}
impl Default for CacheVersions {
fn default() -> Self {
Self {
smokeapi: VersionInfo {
latest: String::new(),
},
creamlinux: VersionInfo {
latest: String::new(),
},
}
}
}
// Get the cache directory path (~/.cache/creamlinux)
pub fn get_cache_dir() -> Result<PathBuf, String> {
let xdg_dirs = xdg::BaseDirectories::with_prefix("creamlinux")
.map_err(|e| format!("Failed to get XDG directories: {}", e))?;
let cache_dir = xdg_dirs
.get_cache_home()
.parent()
.ok_or_else(|| "Failed to get cache parent directory".to_string())?
.join("creamlinux");
// Create the directory if it doesn't exist
if !cache_dir.exists() {
fs::create_dir_all(&cache_dir)
.map_err(|e| format!("Failed to create cache directory: {}", e))?;
info!("Created cache directory: {}", cache_dir.display());
}
Ok(cache_dir)
}
// Get the SmokeAPI cache directory path
pub fn get_smokeapi_dir() -> Result<PathBuf, String> {
let cache_dir = get_cache_dir()?;
let smokeapi_dir = cache_dir.join("smokeapi");
if !smokeapi_dir.exists() {
fs::create_dir_all(&smokeapi_dir)
.map_err(|e| format!("Failed to create SmokeAPI directory: {}", e))?;
info!("Created SmokeAPI directory: {}", smokeapi_dir.display());
}
Ok(smokeapi_dir)
}
// Get the CreamLinux cache directory path
pub fn get_creamlinux_dir() -> Result<PathBuf, String> {
let cache_dir = get_cache_dir()?;
let creamlinux_dir = cache_dir.join("creamlinux");
if !creamlinux_dir.exists() {
fs::create_dir_all(&creamlinux_dir)
.map_err(|e| format!("Failed to create CreamLinux directory: {}", e))?;
info!("Created CreamLinux directory: {}", creamlinux_dir.display());
}
Ok(creamlinux_dir)
}
// Get the path to a versioned SmokeAPI directory
pub fn get_smokeapi_version_dir(version: &str) -> Result<PathBuf, String> {
let smokeapi_dir = get_smokeapi_dir()?;
let version_dir = smokeapi_dir.join(version);
if !version_dir.exists() {
fs::create_dir_all(&version_dir)
.map_err(|e| format!("Failed to create SmokeAPI version directory: {}", e))?;
info!(
"Created SmokeAPI version directory: {}",
version_dir.display()
);
}
Ok(version_dir)
}
// Get the path to a versioned CreamLinux directory
pub fn get_creamlinux_version_dir(version: &str) -> Result<PathBuf, String> {
let creamlinux_dir = get_creamlinux_dir()?;
let version_dir = creamlinux_dir.join(version);
if !version_dir.exists() {
fs::create_dir_all(&version_dir)
.map_err(|e| format!("Failed to create CreamLinux version directory: {}", e))?;
info!(
"Created CreamLinux version directory: {}",
version_dir.display()
);
}
Ok(version_dir)
}
// Read the versions.json file from cache
pub fn read_versions() -> Result<CacheVersions, String> {
let cache_dir = get_cache_dir()?;
let versions_path = cache_dir.join("versions.json");
if !versions_path.exists() {
info!("versions.json doesn't exist, creating default");
return Ok(CacheVersions::default());
}
let content = fs::read_to_string(&versions_path)
.map_err(|e| format!("Failed to read versions.json: {}", e))?;
let versions: CacheVersions = serde_json::from_str(&content)
.map_err(|e| format!("Failed to parse versions.json: {}", e))?;
info!(
"Read cached versions - SmokeAPI: {}, CreamLinux: {}",
versions.smokeapi.latest, versions.creamlinux.latest
);
Ok(versions)
}
// Write the versions.json file to cache
pub fn write_versions(versions: &CacheVersions) -> Result<(), String> {
let cache_dir = get_cache_dir()?;
let versions_path = cache_dir.join("versions.json");
let content = serde_json::to_string_pretty(versions)
.map_err(|e| format!("Failed to serialize versions: {}", e))?;
fs::write(&versions_path, content)
.map_err(|e| format!("Failed to write versions.json: {}", e))?;
info!(
"Wrote versions.json - SmokeAPI: {}, CreamLinux: {}",
versions.smokeapi.latest, versions.creamlinux.latest
);
Ok(())
}
// Update the SmokeAPI version in versions.json and clean old version directories
pub fn update_smokeapi_version(new_version: &str) -> Result<(), String> {
let mut versions = read_versions()?;
let old_version = versions.smokeapi.latest.clone();
versions.smokeapi.latest = new_version.to_string();
write_versions(&versions)?;
// Delete old version directory if it exists and is different
if !old_version.is_empty() && old_version != new_version {
let old_dir = get_smokeapi_dir()?.join(&old_version);
if old_dir.exists() {
match fs::remove_dir_all(&old_dir) {
Ok(_) => info!("Deleted old SmokeAPI version directory: {}", old_version),
Err(e) => warn!(
"Failed to delete old SmokeAPI version directory: {}",
e
),
}
}
}
Ok(())
}
// Update the CreamLinux version in versions.json and clean old version directories
pub fn update_creamlinux_version(new_version: &str) -> Result<(), String> {
let mut versions = read_versions()?;
let old_version = versions.creamlinux.latest.clone();
versions.creamlinux.latest = new_version.to_string();
write_versions(&versions)?;
// Delete old version directory if it exists and is different
if !old_version.is_empty() && old_version != new_version {
let old_dir = get_creamlinux_dir()?.join(&old_version);
if old_dir.exists() {
match fs::remove_dir_all(&old_dir) {
Ok(_) => info!("Deleted old CreamLinux version directory: {}", old_version),
Err(e) => warn!(
"Failed to delete old CreamLinux version directory: {}",
e
),
}
}
}
Ok(())
}
// Check if the cache is initialized (has both unlockers cached)
pub fn is_cache_initialized() -> Result<bool, String> {
let versions = read_versions()?;
Ok(!versions.smokeapi.latest.is_empty() && !versions.creamlinux.latest.is_empty())
}
// Get the SmokeAPI DLL path for the latest cached version
#[allow(dead_code)]
pub fn get_smokeapi_dll_path() -> Result<PathBuf, String> {
let versions = read_versions()?;
if versions.smokeapi.latest.is_empty() {
return Err("SmokeAPI is not cached".to_string());
}
let version_dir = get_smokeapi_version_dir(&versions.smokeapi.latest)?;
Ok(version_dir.join("SmokeAPI.dll"))
}
// Get the CreamLinux files directory path for the latest cached version
#[allow(dead_code)]
pub fn get_creamlinux_files_dir() -> Result<PathBuf, String> {
let versions = read_versions()?;
if versions.creamlinux.latest.is_empty() {
return Err("CreamLinux is not cached".to_string());
}
get_creamlinux_version_dir(&versions.creamlinux.latest)
}
// List all SmokeAPI DLL files in the cached version directory
pub fn list_smokeapi_dlls() -> Result<Vec<PathBuf>, String> {
let versions = read_versions()?;
if versions.smokeapi.latest.is_empty() {
return Ok(Vec::new());
}
let version_dir = get_smokeapi_version_dir(&versions.smokeapi.latest)?;
if !version_dir.exists() {
return Ok(Vec::new());
}
let entries = fs::read_dir(&version_dir)
.map_err(|e| format!("Failed to read SmokeAPI directory: {}", e))?;
let mut dlls = Vec::new();
for entry in entries {
if let Ok(entry) = entry {
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("dll") {
dlls.push(path);
}
}
}
Ok(dlls)
}
// List all CreamLinux files in the cached version directory
pub fn list_creamlinux_files() -> Result<Vec<PathBuf>, String> {
let versions = read_versions()?;
if versions.creamlinux.latest.is_empty() {
return Ok(Vec::new());
}
let version_dir = get_creamlinux_version_dir(&versions.creamlinux.latest)?;
if !version_dir.exists() {
return Ok(Vec::new());
}
let entries = fs::read_dir(&version_dir)
.map_err(|e| format!("Failed to read CreamLinux directory: {}", e))?;
let mut files = Vec::new();
for entry in entries {
if let Ok(entry) = entry {
let path = entry.path();
if path.is_file() {
files.push(path);
}
}
}
Ok(files)
}
+177
View File
@@ -0,0 +1,177 @@
use log::{info};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
// Represents the version manifest stored in each game directory
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct GameManifest {
pub smokeapi_version: Option<String>,
pub creamlinux_version: Option<String>,
}
#[allow(dead_code)]
impl GameManifest {
// Create a new manifest with SmokeAPI version
pub fn with_smokeapi(version: String) -> Self {
Self {
smokeapi_version: Some(version),
creamlinux_version: None,
}
}
// Create a new manifest with CreamLinux version
pub fn with_creamlinux(version: String) -> Self {
Self {
smokeapi_version: None,
creamlinux_version: Some(version),
}
}
// Check if SmokeAPI is installed
pub fn has_smokeapi(&self) -> bool {
self.smokeapi_version.is_some()
}
// Check if CreamLinux is installed
pub fn has_creamlinux(&self) -> bool {
self.creamlinux_version.is_some()
}
// Check if SmokeAPI version is outdated
pub fn is_smokeapi_outdated(&self, latest_version: &str) -> bool {
match &self.smokeapi_version {
Some(version) => version != latest_version,
None => false,
}
}
// Check if CreamLinux version is outdated
pub fn is_creamlinux_outdated(&self, latest_version: &str) -> bool {
match &self.creamlinux_version {
Some(version) => version != latest_version,
None => false,
}
}
}
// Read the creamlinux.json manifest from a game directory
pub fn read_manifest(game_path: &str) -> Result<GameManifest, String> {
let manifest_path = Path::new(game_path).join("creamlinux.json");
if !manifest_path.exists() {
return Ok(GameManifest::default());
}
let content = fs::read_to_string(&manifest_path)
.map_err(|e| format!("Failed to read manifest: {}", e))?;
let manifest: GameManifest = serde_json::from_str(&content)
.map_err(|e| format!("Failed to parse manifest: {}", e))?;
info!(
"Read manifest from {}: SmokeAPI: {:?}, CreamLinux: {:?}",
game_path, manifest.smokeapi_version, manifest.creamlinux_version
);
Ok(manifest)
}
// Write the creamlinux.json manifest to a game directory
pub fn write_manifest(game_path: &str, manifest: &GameManifest) -> Result<(), String> {
let manifest_path = Path::new(game_path).join("creamlinux.json");
let content = serde_json::to_string_pretty(manifest)
.map_err(|e| format!("Failed to serialize manifest: {}", e))?;
fs::write(&manifest_path, content)
.map_err(|e| format!("Failed to write manifest: {}", e))?;
info!(
"Wrote manifest to {}: SmokeAPI: {:?}, CreamLinux: {:?}",
game_path, manifest.smokeapi_version, manifest.creamlinux_version
);
Ok(())
}
// Update the SmokeAPI version in the manifest
pub fn update_smokeapi_version(game_path: &str, version: String) -> Result<(), String> {
let mut manifest = read_manifest(game_path)?;
manifest.smokeapi_version = Some(version);
write_manifest(game_path, &manifest)
}
// Update the CreamLinux version in the manifest
pub fn update_creamlinux_version(game_path: &str, version: String) -> Result<(), String> {
let mut manifest = read_manifest(game_path)?;
manifest.creamlinux_version = Some(version);
write_manifest(game_path, &manifest)
}
// Remove SmokeAPI version from the manifest
pub fn remove_smokeapi_version(game_path: &str) -> Result<(), String> {
let mut manifest = read_manifest(game_path)?;
manifest.smokeapi_version = None;
// If both versions are None, delete the manifest file
if manifest.smokeapi_version.is_none() && manifest.creamlinux_version.is_none() {
let manifest_path = Path::new(game_path).join("creamlinux.json");
if manifest_path.exists() {
fs::remove_file(&manifest_path)
.map_err(|e| format!("Failed to delete manifest: {}", e))?;
info!("Deleted empty manifest from {}", game_path);
}
} else {
write_manifest(game_path, &manifest)?;
}
Ok(())
}
// Remove CreamLinux version from the manifest
pub fn remove_creamlinux_version(game_path: &str) -> Result<(), String> {
let mut manifest = read_manifest(game_path)?;
manifest.creamlinux_version = None;
// If both versions are None, delete the manifest file
if manifest.smokeapi_version.is_none() && manifest.creamlinux_version.is_none() {
let manifest_path = Path::new(game_path).join("creamlinux.json");
if manifest_path.exists() {
fs::remove_file(&manifest_path)
.map_err(|e| format!("Failed to delete manifest: {}", e))?;
info!("Deleted empty manifest from {}", game_path);
}
} else {
write_manifest(game_path, &manifest)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_manifest_creation() {
let manifest = GameManifest::with_smokeapi("v1.0.0".to_string());
assert_eq!(manifest.smokeapi_version, Some("v1.0.0".to_string()));
assert_eq!(manifest.creamlinux_version, None);
let manifest = GameManifest::with_creamlinux("v2.0.0".to_string());
assert_eq!(manifest.smokeapi_version, None);
assert_eq!(manifest.creamlinux_version, Some("v2.0.0".to_string()));
}
#[test]
fn test_outdated_check() {
let mut manifest = GameManifest::with_smokeapi("v1.0.0".to_string());
assert!(manifest.is_smokeapi_outdated("v2.0.0"));
assert!(!manifest.is_smokeapi_outdated("v1.0.0"));
manifest.creamlinux_version = Some("v1.5.0".to_string());
assert!(manifest.is_creamlinux_outdated("v2.0.0"));
assert!(!manifest.is_creamlinux_outdated("v1.5.0"));
}
}