@@ -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]
8851034pub 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 ,
0 commit comments