Skip to content

Commit f6af601

Browse files
authored
Add files via upload
1 parent a7d14ba commit f6af601

11 files changed

Lines changed: 294 additions & 21 deletions

File tree

src-tauri/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "zgamelib"
3-
version = "1.2.0"
3+
version = "1.2.1"
44
description = "ZGameLib - Personal Game Library"
55
authors = ["TheHolyOneZ"]
66
license = "MIT"

src-tauri/src/commands/games.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ pub fn create_game(state: State<DbState>, payload: CreateGamePayload) -> Result<
3636
date_added: now,
3737
steam_app_id: payload.steam_app_id,
3838
epic_app_name: payload.epic_app_name,
39+
ubisoft_game_id: payload.ubisoft_game_id,
3940
tags: vec![],
4041
sort_order: 0,
4142
deleted_at: None,
@@ -404,13 +405,14 @@ pub fn get_library_growth(state: State<DbState>) -> Result<Vec<LibraryGrowthEntr
404405
for (month, platform, cnt) in rows {
405406
let entry = month_map.entry(month.clone()).or_insert(LibraryGrowthEntry {
406407
month,
407-
steam: 0, epic: 0, gog: 0, custom: 0,
408+
steam: 0, epic: 0, gog: 0, custom: 0, ubisoft: 0,
408409
});
409410
match platform.as_str() {
410-
"steam" => entry.steam += cnt,
411-
"epic" => entry.epic += cnt,
412-
"gog" => entry.gog += cnt,
413-
"custom" => entry.custom += cnt,
411+
"steam" => entry.steam += cnt,
412+
"epic" => entry.epic += cnt,
413+
"gog" => entry.gog += cnt,
414+
"custom" => entry.custom += cnt,
415+
"ubisoft" => entry.ubisoft += cnt,
414416
_ => {}
415417
}
416418
}

src-tauri/src/commands/launcher.rs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -798,6 +798,114 @@ pub fn launch_epic_game(
798798
Ok(())
799799
}
800800

801+
#[tauri::command]
802+
pub fn launch_ubisoft_game(
803+
app: AppHandle,
804+
state: State<DbState>,
805+
active_pids: State<ActivePids>,
806+
game_id: String,
807+
game_db_id: String,
808+
) -> Result<(), String> {
809+
let (exe_path, install_dir, game_name, minimize, exclude_idle) = {
810+
let conn = state.0.lock().map_err(|e| e.to_string())?;
811+
let game = queries::get_game_by_id(&conn, &game_db_id).ok().flatten();
812+
let exe = game.as_ref().and_then(|g| g.exe_path.clone());
813+
let dir = game.as_ref().and_then(|g| g.install_dir.clone()).or_else(|| {
814+
let exe_ref = exe.as_ref()?;
815+
let parent = std::path::Path::new(exe_ref).parent()?;
816+
if parent.components().count() <= 1 { return None; }
817+
Some(parent.to_string_lossy().to_string())
818+
});
819+
let name = game.as_ref().map(|g| g.name.clone()).unwrap_or_default();
820+
let min = queries::get_setting(&conn, "minimize_on_launch")
821+
.map(|v| v == "true").unwrap_or(false);
822+
let idle = queries::get_setting(&conn, "exclude_idle_time")
823+
.map(|v| v != "false").unwrap_or(true);
824+
(exe, dir, name, min, idle)
825+
};
826+
827+
if active_pids.game_active.load(Ordering::SeqCst) {
828+
let running_name = {
829+
let ap = app.state::<ActivePids>();
830+
ap.running_game_name.lock().ok()
831+
.map(|n| if n.is_empty() { "another game".to_string() } else { n.clone() })
832+
.unwrap_or_else(|| "another game".to_string())
833+
};
834+
let _ = app.emit("game-already-running", &running_name);
835+
#[cfg(windows)]
836+
show_native_alert("ZGameLib", &format!("\"{}\" is already being tracked.\nClose it first or use Stop Tracking.", running_name));
837+
return Err(format!("GAME_ALREADY_RUNNING:{}", running_name));
838+
}
839+
840+
let uri = format!("ubisoft://launch/{}", game_id);
841+
open::that(&uri).map_err(|e| format!("Failed to launch Ubisoft game: {}", e))?;
842+
843+
let now = Utc::now().to_rfc3339();
844+
{
845+
let conn = state.0.lock().map_err(|e| e.to_string())?;
846+
let _ = conn.execute(
847+
"UPDATE games SET last_played = ?1 WHERE id = ?2",
848+
rusqlite::params![now, game_db_id],
849+
);
850+
}
851+
852+
if minimize {
853+
let app_min = app.clone();
854+
std::thread::spawn(move || {
855+
std::thread::sleep(std::time::Duration::from_millis(400));
856+
if let Some(win) = app_min.get_webview_window("main") {
857+
let _ = win.minimize();
858+
}
859+
});
860+
}
861+
862+
active_pids.game_active.store(true, Ordering::SeqCst);
863+
if let Ok(mut name) = active_pids.running_game_name.lock() {
864+
*name = game_name.clone();
865+
}
866+
867+
let app_clone = app.clone();
868+
let gid = game_db_id.clone();
869+
let pids = active_pids.pids.clone();
870+
let started_at = now.clone();
871+
872+
std::thread::spawn(move || {
873+
#[cfg(windows)]
874+
{
875+
let exe_hint = exe_path.as_ref().and_then(|e| {
876+
std::path::Path::new(e).file_name().map(|n| n.to_string_lossy().to_string())
877+
});
878+
if let Some(dir) = install_dir {
879+
track_by_directory(app_clone, gid, started_at, dir, pids, exclude_idle, 300, None, exe_hint);
880+
} else if let Some(exe) = exe_path {
881+
let exe_name = std::path::Path::new(&exe)
882+
.file_name()
883+
.map(|n| n.to_string_lossy().to_lowercase())
884+
.unwrap_or_default();
885+
if !exe_name.is_empty() {
886+
let mut pid: Option<u32> = None;
887+
for _ in 0..180 {
888+
if let Some(p) = find_pid_by_exe_name(&exe_name) {
889+
pid = Some(p);
890+
break;
891+
}
892+
std::thread::sleep(std::time::Duration::from_secs(1));
893+
}
894+
if let Some(p) = pid {
895+
track_by_pid(app_clone, gid, started_at, p, pids, exclude_idle);
896+
return;
897+
}
898+
}
899+
track_by_timeout(app_clone, gid, started_at);
900+
} else {
901+
track_by_timeout(app_clone, gid, started_at);
902+
}
903+
}
904+
});
905+
906+
Ok(())
907+
}
908+
801909
#[tauri::command]
802910
pub fn open_game_folder(state: State<DbState>, id: String) -> Result<(), String> {
803911
let conn = state.0.lock().map_err(|e| e.to_string())?;

src-tauri/src/commands/scanner.rs

Lines changed: 160 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,7 @@ fn scan_steam_games_inner(app: AppHandle, db: std::sync::Arc<std::sync::Mutex<ru
506506
date_added: now.clone(),
507507
steam_app_id: Some(entry.app_id),
508508
epic_app_name: None,
509+
ubisoft_game_id: None,
509510
tags: vec![],
510511
sort_order: 0,
511512
deleted_at: None,
@@ -648,6 +649,7 @@ fn scan_epic_games_inner(app: AppHandle, db: std::sync::Arc<std::sync::Mutex<rus
648649
date_added: now.clone(),
649650
steam_app_id: None,
650651
epic_app_name: Some(entry.app_name),
652+
ubisoft_game_id: None,
651653
tags: vec![],
652654
sort_order: 0,
653655
deleted_at: None,
@@ -799,7 +801,7 @@ fn scan_gog_games_inner(app: AppHandle, db: std::sync::Arc<std::sync::Mutex<rusq
799801
status: "none".to_string(),
800802
is_favorite: false, playtime_mins: 0,
801803
last_played: None, date_added: now.clone(),
802-
steam_app_id: None, epic_app_name: None,
804+
steam_app_id: None, epic_app_name: None, ubisoft_game_id: None,
803805
tags: vec![], sort_order: 0,
804806
deleted_at: None, is_pinned: false,
805807
custom_fields: std::collections::HashMap::new(),
@@ -881,6 +883,153 @@ pub fn get_game_screenshots(steam_app_id: String) -> Result<Vec<String>, String>
881883
Ok(shots.into_iter().map(|p| p.to_string_lossy().to_string()).collect())
882884
}
883885

886+
#[cfg(windows)]
887+
fn get_ubisoft_games_from_registry() -> Vec<(String, String, String)> {
888+
use winreg::enums::{HKEY_LOCAL_MACHINE, KEY_READ};
889+
use winreg::RegKey;
890+
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
891+
let mut games = vec![];
892+
let installs_paths = [
893+
"SOFTWARE\\WOW6432Node\\Ubisoft\\Launcher\\Installs",
894+
"SOFTWARE\\Ubisoft\\Launcher\\Installs",
895+
];
896+
for installs_path in &installs_paths {
897+
if let Ok(installs_key) = hklm.open_subkey_with_flags(installs_path, KEY_READ) {
898+
for game_id in installs_key.enum_keys().flatten() {
899+
if let Ok(sub) = installs_key.open_subkey_with_flags(&game_id, KEY_READ) {
900+
let install_dir: String = sub.get_value("InstallDir").unwrap_or_default();
901+
if install_dir.is_empty() { continue; }
902+
let name = hklm
903+
.open_subkey_with_flags(
904+
&format!("SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Uplay Install {}", game_id),
905+
KEY_READ,
906+
)
907+
.or_else(|_| hklm.open_subkey_with_flags(
908+
&format!("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Uplay Install {}", game_id),
909+
KEY_READ,
910+
))
911+
.ok()
912+
.and_then(|k| k.get_value::<String, _>("DisplayName").ok())
913+
.unwrap_or_default();
914+
if name.is_empty() { continue; }
915+
games.push((game_id, name, install_dir));
916+
}
917+
}
918+
if !games.is_empty() { break; }
919+
}
920+
}
921+
games
922+
}
923+
924+
#[cfg(not(windows))]
925+
fn get_ubisoft_games_from_registry() -> Vec<(String, String, String)> { vec![] }
926+
927+
#[tauri::command]
928+
pub async fn scan_ubisoft_games(app: AppHandle, state: State<'_, DbState>) -> Result<ScanResult, String> {
929+
let db = state.0.clone();
930+
let app2 = app.clone();
931+
tokio::task::spawn_blocking(move || scan_ubisoft_games_inner(app2, db))
932+
.await
933+
.map_err(|e| e.to_string())?
934+
}
935+
936+
fn scan_ubisoft_games_inner(app: AppHandle, db: std::sync::Arc<std::sync::Mutex<rusqlite::Connection>>) -> Result<ScanResult, String> {
937+
let raw = get_ubisoft_games_from_registry();
938+
let total = raw.len();
939+
log(&app, "info", &format!("Ubisoft Connect: found {} game(s) in registry", total));
940+
941+
struct UbisoftEntry { game_id: String, name: String, exe_path: Option<String>, install_dir: String, cover: Option<String> }
942+
let mut entries = Vec::with_capacity(total);
943+
944+
for (game_id, name, install_dir) in raw {
945+
log(&app, "info", &format!("--- [{}] {}", game_id, name));
946+
let exe_path = find_steam_game_exe(&install_dir, &name);
947+
match &exe_path {
948+
Some(p) => log(&app, "ok", &format!(" exe: {}", p)),
949+
None => log(&app, "warn", " exe: not found"),
950+
}
951+
let cover = if let Some(local) = find_cover_in_dir_internal(&install_dir) {
952+
log(&app, "ok", &format!(" cover: local ({})", local));
953+
Some(local)
954+
} else {
955+
log(&app, "info", &format!(" searching Steam CDN for \"{}\"...", name));
956+
let sc = search_steam_cover_for_name(&name, &app);
957+
match &sc {
958+
Some(_) => log(&app, "ok", " cover: Steam CDN ✓"),
959+
None => log(&app, "warn", " cover: not found"),
960+
}
961+
sc
962+
};
963+
entries.push(UbisoftEntry { game_id, name, exe_path, install_dir, cover });
964+
}
965+
966+
let conn = db.lock().map_err(|e| e.to_string())?;
967+
conn.execute_batch("BEGIN").map_err(|e| e.to_string())?;
968+
let now = chrono::Utc::now().to_rfc3339();
969+
let mut added = 0;
970+
let mut skipped = 0;
971+
972+
let mut ubi_stmt = conn.prepare(
973+
"SELECT ubisoft_game_id, id, cover_path FROM games WHERE ubisoft_game_id IS NOT NULL AND deleted_at IS NULL"
974+
).map_err(|e| e.to_string())?;
975+
let ubi_existing: std::collections::HashMap<String, (String, Option<String>)> =
976+
ubi_stmt.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?, r.get::<_, Option<String>>(2)?)))
977+
.map_err(|e| e.to_string())?
978+
.filter_map(|r| r.ok())
979+
.map(|(gid, id, cover)| (gid, (id, cover)))
980+
.collect();
981+
drop(ubi_stmt);
982+
983+
for entry in entries {
984+
if let Some((existing_id, existing_cover)) = ubi_existing.get(&entry.game_id) {
985+
let existing_id = existing_id.clone();
986+
if existing_cover.is_none() {
987+
if let Some(ref cover) = entry.cover {
988+
let _ = crate::db::queries::update_cover_path(&conn, &existing_id, cover);
989+
}
990+
}
991+
skipped += 1;
992+
continue;
993+
}
994+
let game = Game {
995+
id: Uuid::new_v4().to_string(),
996+
name: entry.name,
997+
platform: "ubisoft".to_string(),
998+
exe_path: entry.exe_path,
999+
install_dir: Some(entry.install_dir),
1000+
cover_path: entry.cover,
1001+
description: None,
1002+
rating: None,
1003+
status: "none".to_string(),
1004+
is_favorite: false,
1005+
playtime_mins: 0,
1006+
last_played: None,
1007+
date_added: now.clone(),
1008+
steam_app_id: None,
1009+
epic_app_name: None,
1010+
ubisoft_game_id: Some(entry.game_id),
1011+
tags: vec![],
1012+
sort_order: 0,
1013+
deleted_at: None,
1014+
is_pinned: false,
1015+
custom_fields: std::collections::HashMap::new(),
1016+
hltb_main_mins: None,
1017+
hltb_extra_mins: None,
1018+
genre: None,
1019+
developer: None,
1020+
publisher: None,
1021+
release_year: None,
1022+
igdb_skipped: false,
1023+
not_installed: false,
1024+
};
1025+
if queries::insert_game(&conn, &game).is_ok() { added += 1; }
1026+
}
1027+
1028+
conn.execute_batch("COMMIT").map_err(|e| e.to_string())?;
1029+
log(&app, "ok", &format!("Ubisoft scan done: {} added, {} skipped", added, skipped));
1030+
Ok(ScanResult { added, skipped, total })
1031+
}
1032+
8841033
#[tauri::command]
8851034
pub async fn scan_all_games(app: AppHandle, state: State<'_, DbState>) -> Result<ScanResult, String> {
8861035
if SCAN_RUNNING.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst).is_err() {
@@ -895,17 +1044,19 @@ pub async fn scan_all_games(app: AppHandle, state: State<'_, DbState>) -> Result
8951044
log(&app2, "info", "=== Scanning Epic ===");
8961045
let epic = scan_epic_games_inner(app2.clone(), db.clone()).unwrap_or(empty.clone());
8971046
log(&app2, "info", "=== Scanning GOG ===");
898-
let gog = scan_gog_games_inner(app2.clone(), db).unwrap_or(empty);
1047+
let gog = scan_gog_games_inner(app2.clone(), db.clone()).unwrap_or(empty.clone());
1048+
log(&app2, "info", "=== Scanning Ubisoft Connect ===");
1049+
let ubi = scan_ubisoft_games_inner(app2.clone(), db).unwrap_or(empty);
8991050
log(&app2, "ok", &format!(
9001051
"=== All done: {} added, {} skipped, {} total ===",
901-
steam.added + epic.added + gog.added,
902-
steam.skipped + epic.skipped + gog.skipped,
903-
steam.total + epic.total + gog.total,
1052+
steam.added + epic.added + gog.added + ubi.added,
1053+
steam.skipped + epic.skipped + gog.skipped + ubi.skipped,
1054+
steam.total + epic.total + gog.total + ubi.total,
9041055
));
9051056
Ok(ScanResult {
906-
added: steam.added + epic.added + gog.added,
907-
skipped: steam.skipped + epic.skipped + gog.skipped,
908-
total: steam.total + epic.total + gog.total,
1057+
added: steam.added + epic.added + gog.added + ubi.added,
1058+
skipped: steam.skipped + epic.skipped + gog.skipped + ubi.skipped,
1059+
total: steam.total + epic.total + gog.total + ubi.total,
9091060
})
9101061
})
9111062
.await
@@ -1082,6 +1233,7 @@ fn scan_folder_for_games_inner(app: AppHandle, db: std::sync::Arc<std::sync::Mut
10821233
date_added: now.clone(),
10831234
steam_app_id: None,
10841235
epic_app_name: None,
1236+
ubisoft_game_id: None,
10851237
tags: vec![],
10861238
sort_order: 0,
10871239
deleted_at: None,

src-tauri/src/commands/settings.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ pub fn import_library(state: State<DbState>, path: String) -> Result<ImportResul
144144
let mut added = 0;
145145
let mut skipped = 0;
146146

147-
const VALID_PLATFORMS: &[&str] = &["steam", "epic", "gog", "custom"];
147+
const VALID_PLATFORMS: &[&str] = &["steam", "epic", "gog", "custom", "ubisoft"];
148148

149149
for mut game in games {
150150
if game.name.is_empty() || game.name.len() > 255 { skipped += 1; continue; }
@@ -588,6 +588,7 @@ pub fn pull_uninstalled_steam_games(state: State<DbState>, api_key: String, stea
588588
date_added: now.clone(),
589589
steam_app_id: Some(appid),
590590
epic_app_name: None,
591+
ubisoft_game_id: None,
591592
tags: vec![],
592593
sort_order: 0,
593594
deleted_at: None,

0 commit comments

Comments
 (0)