From 47bded61da8d79a6dedf879d23878ebf5f8a55ff Mon Sep 17 00:00:00 2001 From: Roman Tarasov Date: Sun, 29 Mar 2026 15:49:03 +0300 Subject: [PATCH] Restore missing appmeta on remount and replace access checks with stat --- include/sm_filesystem.h | 6 +- include/sm_path_utils.h | 2 + include/sm_paths.h | 2 + src/main.c | 8 ++- src/sm_filesystem.c | 38 ++++++---- src/sm_game_cache.c | 4 +- src/sm_image.c | 4 +- src/sm_install.c | 151 ++++++++++++++++++++++++++++++++-------- src/sm_mount_device.c | 3 +- src/sm_path_state.c | 6 +- src/sm_path_utils.c | 5 ++ src/sm_scan.c | 32 ++++++++- 12 files changed, 201 insertions(+), 60 deletions(-) diff --git a/include/sm_filesystem.h b/include/sm_filesystem.h index 6ab5920..f9c64c9 100644 --- a/include/sm_filesystem.h +++ b/include/sm_filesystem.h @@ -7,11 +7,13 @@ // Check whether a title is present in the installed app set. bool is_installed(const char *title_id); +// Check whether /user/appmeta//param.json exists. +bool has_appmeta_data(const char *title_id); // Check whether a title currently has mounted data. bool is_data_mounted(const char *title_id); -// Read the mount.lnk source path for an installed title. +// Read the mount.lnk source path for a title tracked under /user/app. bool read_mount_link(const char *title_id, char *out, size_t out_size); -// Validate a /user/app/ entry and optionally build its path. +// Validate a /user/app/ tracker entry and optionally build its path. bool resolve_title_app_dir(const struct dirent *entry, char *app_dir, size_t app_dir_size); // Recover staged mount links and warm image source mappings on startup. diff --git a/include/sm_path_utils.h b/include/sm_path_utils.h index c213abf..0df1b6d 100644 --- a/include/sm_path_utils.h +++ b/include/sm_path_utils.h @@ -8,6 +8,8 @@ // Return the last path component without modifying the input string. const char *get_filename_component(const char *path); +// Return true when stat() can see the path. +bool path_exists(const char *path); // Return true when a path lives under the image mount root. bool is_under_image_mount_base(const char *path); // Build "/backports" for a managed scan root. diff --git a/include/sm_paths.h b/include/sm_paths.h index 500c5af..0b91d2a 100644 --- a/include/sm_paths.h +++ b/include/sm_paths.h @@ -9,6 +9,8 @@ #define LOG_FILE_PREV "/data/shadowmount/debug.log.1" #define CONFIG_FILE "/data/shadowmount/config.ini" #define AUTOTUNE_FILE "/data/shadowmount/autotune.ini" +#define APPMETA_BASE "/user/appmeta" +#define APP_BASE "/user/app" #define KSTUFF_NOAUTOMOUNT_FILE "/data/.kstuff_noautomount" #define KILL_FILE "/data/shadowmount/STOP" #define TOAST_FILE "/data/shadowmount/notify.txt" diff --git a/src/main.c b/src/main.c index dec5b5d..9a141c7 100644 --- a/src/main.c +++ b/src/main.c @@ -14,6 +14,7 @@ #include "sm_mount_device.h" #include "sm_filesystem.h" #include "sm_image.h" +#include "sm_path_utils.h" #include "sm_scan.h" #include "sm_scanner.h" #include "sm_install.h" @@ -32,6 +33,7 @@ #define RESTART_WAIT_MAX_US 60000000u #define KINFO_PID_OFFSET 72 #define KINFO_TDNAME_OFFSET 447 +#define AUTHID_BASE 0x4801000000000013L static volatile sig_atomic_t g_stop_requested = 0; static atomic_bool g_shutdown_on_going_stop_requested = false; @@ -68,7 +70,7 @@ void install_signal_handlers(void) { bool should_stop_requested(void) { if (g_stop_requested) return true; - if (access(KILL_FILE, F_OK) == 0) { + if (path_exists(KILL_FILE)) { remove(KILL_FILE); return true; } @@ -252,7 +254,7 @@ static void log_non_empty_scan_paths(void) { } static void ensure_kstuff_noautomount_file(void) { - if (access(KSTUFF_NOAUTOMOUNT_FILE, F_OK) == 0) + if (path_exists(KSTUFF_NOAUTOMOUNT_FILE)) return; int fd = open(KSTUFF_NOAUTOMOUNT_FILE, O_RDONLY | O_CREAT, 0666); @@ -299,7 +301,7 @@ int main(void) { sceUserServiceInitialize(0); sceAppInstUtilInitialize(); - kernel_set_ucred_authid(-1, 0x4801000000000013L); + kernel_set_ucred_authid(-1, AUTHID_BASE); install_signal_handlers(); mkdir(LOG_DIR, 0777); diff --git a/src/sm_filesystem.c b/src/sm_filesystem.c index b969f6a..deaac93 100644 --- a/src/sm_filesystem.c +++ b/src/sm_filesystem.c @@ -12,15 +12,23 @@ // --- FILESYSTEM --- bool is_installed(const char *title_id) { char path[MAX_PATH]; - snprintf(path, sizeof(path), "/user/app/%s", title_id); + snprintf(path, sizeof(path), "%s/%s", APP_BASE, title_id); struct stat st; return (stat(path, &st) == 0); } + +bool has_appmeta_data(const char *title_id) { + char path[MAX_PATH]; + snprintf(path, sizeof(path), "%s/%s/param.json", APPMETA_BASE, title_id); + struct stat st; + return (stat(path, &st) == 0 && S_ISREG(st.st_mode)); +} + bool is_data_mounted(const char *title_id) { char path[MAX_PATH]; snprintf(path, sizeof(path), "/system_ex/app/%s/sce_sys/param.json", title_id); - return (access(path, F_OK) == 0); + return path_exists(path); } static bool read_mount_link_file(const char *lnk_path, char *out, @@ -47,7 +55,7 @@ static bool read_mount_link_file(const char *lnk_path, char *out, static void build_title_link_path(const char *title_id, const char *filename, char out[MAX_PATH]) { - snprintf(out, MAX_PATH, "/user/app/%s/%s", title_id, filename); + snprintf(out, MAX_PATH, "%s/%s/%s", APP_BASE, title_id, filename); } typedef struct { @@ -71,10 +79,10 @@ typedef bool (*title_app_dir_iter_fn)(const char *title_id, static void for_each_title_app_dir(const char *open_error_context, bool stop_on_signal, title_app_dir_iter_fn fn, void *ctx) { - DIR *d = opendir("/user/app"); + DIR *d = opendir(APP_BASE); if (!d) { if (errno != ENOENT) - log_debug(" [LINK] open /user/app failed%s: %s", + log_debug(" [LINK] open %s failed%s: %s", APP_BASE, open_error_context ? open_error_context : "", strerror(errno)); return; } @@ -122,7 +130,7 @@ bool resolve_title_app_dir(const struct dirent *entry, char *app_dir, path_size = app_dir_size; } - int written = snprintf(path, path_size, "/user/app/%s", entry->d_name); + int written = snprintf(path, path_size, "%s/%s", APP_BASE, entry->d_name); if (written < 0 || (size_t)written >= path_size) { if (app_dir && app_dir_size > 0) app_dir[0] = '\0'; @@ -146,18 +154,18 @@ static bool source_path_needs_cleanup(const char *source_path, char image_source_path[MAX_PATH]; bool has_image_source = resolve_image_source_from_mount_cache( source_path, image_source_path, sizeof(image_source_path)); - if (has_image_source && access(image_source_path, F_OK) != 0) + if (has_image_source && !path_exists(image_source_path)) return true; } - if (access(source_path, F_OK) != 0) + if (!path_exists(source_path)) return true; if (path_matches_root_or_child(source_path, "/system_ex/app")) return true; char eboot_path[MAX_PATH]; snprintf(eboot_path, sizeof(eboot_path), "%s/eboot.bin", source_path); - if (access(eboot_path, F_OK) == 0) + if (path_exists(eboot_path)) return false; if (!*tried_image_recovery && is_under_image_mount_base(source_path)) { @@ -165,7 +173,7 @@ static bool source_path_needs_cleanup(const char *source_path, *tried_image_recovery = true; } - return access(eboot_path, F_OK) != 0; + return !path_exists(eboot_path); } static bool resolve_mount_image_source_path(const title_link_paths_t *paths, @@ -759,7 +767,7 @@ bool mount_title_nullfs(const char *title_id, const char *src_path) { title_id, src_path, (unsigned)(src_st.st_mode & 077777)); return false; } - if (access(src_eboot, F_OK) != 0) { + if (!path_exists(src_eboot)) { bool tried_image_recovery = false; if (source_path_needs_cleanup(src_path, &tried_image_recovery)) { log_debug(" [LINK] source eboot.bin missing for %s: %s", title_id, @@ -777,7 +785,7 @@ bool mount_title_nullfs(const char *title_id, const char *src_path) { if (state.mounted) { if (state.has_our_nullfs && (state.top_is_our_nullfs || state.has_our_backport) && - access(dst_eboot, F_OK) == 0) { + path_exists(dst_eboot)) { log_debug(" [LINK] mount stack already active: %s -> %s", src_path, dst); return true; } @@ -820,7 +828,7 @@ bool mount_title_nullfs(const char *title_id, const char *src_path) { return false; } - if (access(dst_eboot, F_OK) != 0) { + if (!path_exists(dst_eboot)) { log_debug(" [LINK] mounted nullfs but eboot.bin is missing at target: %s", dst_eboot); if (unmount(dst, 0) != 0 && errno != ENOENT && errno != EINVAL) { @@ -880,11 +888,11 @@ static bool cleanup_mount_links_entry(const char *title_id, ctx->removed_source_root)); if (!matches_removed_source) return true; - should_remove = (has_image_source && access(image_source_path, F_OK) != 0) || + should_remove = (has_image_source && !path_exists(image_source_path)) || source_path_needs_cleanup(source_path, &ctx->tried_image_recovery); } else { - should_remove = (has_image_source && access(image_source_path, F_OK) != 0) || + should_remove = (has_image_source && !path_exists(image_source_path)) || source_path_needs_cleanup(source_path, &ctx->tried_image_recovery); } diff --git a/src/sm_game_cache.c b/src/sm_game_cache.c index d62f098..3b5b2dc 100644 --- a/src/sm_game_cache.c +++ b/src/sm_game_cache.c @@ -114,7 +114,7 @@ void prune_game_cache(void) { for (int k = 0; k < MAX_PENDING; k++) { if (!g_game_cache[k].valid) continue; - if (access(g_game_cache[k].path, F_OK) == 0) + if (path_exists(g_game_cache[k].path)) continue; clear_game_cache_slot(k, "source removed"); } @@ -135,7 +135,7 @@ void prune_game_cache_for_root(const char *root) { : g_game_cache[k].path; if (!path_matches_root_or_child(entry_root, root)) continue; - if (access(g_game_cache[k].path, F_OK) == 0) + if (path_exists(g_game_cache[k].path)) continue; clear_game_cache_slot(k, "source removed"); } diff --git a/src/sm_image.c b/src/sm_image.c index 838d1f6..60f4fad 100644 --- a/src/sm_image.c +++ b/src/sm_image.c @@ -858,7 +858,7 @@ void cleanup_stale_image_mounts(void) { if (!get_image_cache_entry(k, &cached_entry)) continue; - if (access(cached_entry.path, F_OK) != 0) { + if (!path_exists(cached_entry.path)) { log_debug(" [IMG][%s] Source removed, unmounting: %s", attach_backend_name(cached_entry.backend), cached_entry.path); if (unmount_image(cached_entry.path, cached_entry.unit_id, @@ -906,7 +906,7 @@ void cleanup_stale_image_mounts_for_root(const char *root) { continue; } - if (access(cached_entry.path, F_OK) != 0) { + if (!path_exists(cached_entry.path)) { log_debug(" [IMG][%s] Source removed, unmounting: %s", attach_backend_name(cached_entry.backend), cached_entry.path); if (unmount_image(cached_entry.path, cached_entry.unit_id, diff --git a/src/sm_install.c b/src/sm_install.c index 8030c33..d624801 100644 --- a/src/sm_install.c +++ b/src/sm_install.c @@ -10,6 +10,7 @@ #include "sm_appdb.h" #include "sm_title_state.h" #include "sm_image_cache.h" +#include "sm_paths.h" static bool write_link_file(const char *path, const char *value) { FILE *f = fopen(path, "w"); @@ -34,17 +35,73 @@ static bool write_link_file(const char *path, const char *value) { return true; } +static bool is_appmeta_file(const char *name) { + if (!name) + return false; + if (strcasecmp(name, "param.json") == 0 || strcasecmp(name, "param.sfo") == 0) + return true; + + const char *ext = strrchr(name, '.'); + if (!ext) + return false; + + return (strcasecmp(ext, ".png") == 0 || strcasecmp(ext, ".dds") == 0 || + strcasecmp(ext, ".at9") == 0); +} + +static bool copy_sce_sys_to_appmeta(const char *src_sce_sys, + const char *user_appmeta_dir) { + DIR *d = opendir(src_sce_sys); + if (!d) + return false; + + bool ok = true; + struct dirent *e; + char src_path[MAX_PATH]; + char dst_path[MAX_PATH]; + struct stat st; + while ((e = readdir(d)) != NULL) { + if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0) + continue; + if (!is_appmeta_file(e->d_name)) + continue; + + snprintf(src_path, sizeof(src_path), "%s/%s", src_sce_sys, e->d_name); + if (stat(src_path, &st) != 0 || !S_ISREG(st.st_mode)) + continue; + + snprintf(dst_path, sizeof(dst_path), "%s/%s", user_appmeta_dir, e->d_name); + if (copy_file(src_path, dst_path) != 0) { + ok = false; + break; + } + } + + if (closedir(d) != 0) + ok = false; + return ok; +} + // --- Install/Remount Action --- static bool mount_and_install(const char *src_path, const char *title_id, const char *title_name, bool is_remount, bool should_register) { + char user_appmeta_dir[MAX_PATH]; char user_app_dir[MAX_PATH]; char user_sce_sys[MAX_PATH]; char src_sce_sys[MAX_PATH]; + char src_snd0[MAX_PATH]; char image_source_path[MAX_PATH]; bool has_image_source = false; + bool appmeta_missing = false; + bool metadata_restaged = false; + bool restage_staging = false; + bool restage_appmeta = false; + bool has_src_snd0 = false; - snprintf(user_app_dir, sizeof(user_app_dir), "/user/app/%s", title_id); + snprintf(user_appmeta_dir, sizeof(user_appmeta_dir), "%s/%s", APPMETA_BASE, + title_id); + snprintf(user_app_dir, sizeof(user_app_dir), "%s/%s", APP_BASE, title_id); if (is_under_image_mount_base(src_path)) { has_image_source = resolve_image_source_from_mount_cache( @@ -55,32 +112,66 @@ static bool mount_and_install(const char *src_path, const char *title_id, } } + appmeta_missing = !has_appmeta_data(title_id); + if (is_remount && appmeta_missing) { + log_debug(" [REG] appmeta missing, restaging metadata only: %s", title_id); + } + + restage_staging = (!is_remount || should_register); + restage_appmeta = (!is_remount || appmeta_missing); + // COPY FILES - if (!is_remount) { - snprintf(user_sce_sys, sizeof(user_sce_sys), "%s/sce_sys", user_app_dir); - mkdir(user_app_dir, 0777); - mkdir(user_sce_sys, 0777); - + if (restage_staging || restage_appmeta) { snprintf(src_sce_sys, sizeof(src_sce_sys), "%s/sce_sys", src_path); - if (copy_dir(src_sce_sys, user_sce_sys) != 0) { - log_debug(" [COPY] Failed to copy sce_sys: %s -> %s", src_sce_sys, - user_sce_sys); - return false; - } - - char icon_src[MAX_PATH], icon_dst[MAX_PATH]; - snprintf(icon_src, sizeof(icon_src), "%s/sce_sys/icon0.png", src_path); - snprintf(icon_dst, sizeof(icon_dst), "/user/app/%s/icon0.png", title_id); - if (copy_file(icon_src, icon_dst) != 0) { - log_debug(" [COPY] Failed to copy icon: %s -> %s", icon_src, icon_dst); - return false; - } + snprintf(src_snd0, sizeof(src_snd0), "%s/snd0.at9", src_sce_sys); + has_src_snd0 = path_exists(src_snd0); } else { log_debug(" [SPEED] Skipping file copy (Assets already exist)"); } + if (restage_staging) { + mkdir(APP_BASE, 0777); + mkdir(user_app_dir, 0777); + snprintf(user_sce_sys, sizeof(user_sce_sys), "%s/sce_sys", user_app_dir); + mkdir(user_sce_sys, 0777); + if (copy_dir(src_sce_sys, user_sce_sys) != 0) { + log_debug(" [COPY] Failed to copy sce_sys staging: %s -> %s", src_sce_sys, + user_sce_sys); + return false; + } + + char icon_src[MAX_PATH]; + char icon_dst[MAX_PATH]; + snprintf(icon_src, sizeof(icon_src), "%s/icon0.png", src_sce_sys); + snprintf(icon_dst, sizeof(icon_dst), "%s/icon0.png", user_app_dir); + if (copy_file(icon_src, icon_dst) != 0) { + log_debug(" [COPY] Failed to copy staged icon: %s -> %s", icon_src, + icon_dst); + return false; + } + } + + if (restage_appmeta) { + mkdir(APPMETA_BASE, 0777); + mkdir(user_appmeta_dir, 0777); + if (!copy_sce_sys_to_appmeta(src_sce_sys, user_appmeta_dir)) { + log_debug(" [COPY] Failed to copy appmeta files: %s -> %s", src_sce_sys, + user_appmeta_dir); + return false; + } + metadata_restaged = true; + } + + if (!mount_title_nullfs(title_id, src_path)) { + log_debug(" [LINK] nullfs mount failed: title=%s src=%s", title_id, + src_path); + return false; + } + // WRITE TRACKER char lnk_path[MAX_PATH]; + mkdir(APP_BASE, 0777); + mkdir(user_app_dir, 0777); snprintf(lnk_path, sizeof(lnk_path), "%s/mount.lnk", user_app_dir); if (!write_link_file(lnk_path, src_path)) return false; @@ -104,25 +195,25 @@ static bool mount_and_install(const char *src_path, const char *title_id, strerror(errno)); } - if (!mount_title_nullfs(title_id, src_path)) { - log_debug(" [LINK] mount.lnk created but nullfs mount failed: title=%s " - "src=%s", - title_id, src_path); - return false; - } - if (!should_register) { + if (metadata_restaged && has_src_snd0) { + int snd0_updates = update_snd0info(title_id); + if (snd0_updates >= 0) + log_debug(" [DB] snd0info force-updated after appmeta refresh rows=%d", + snd0_updates); + } log_debug(" [REG] Skip (already present in app.db)"); return true; } // REGISTER - char src_snd0[MAX_PATH]; - snprintf(src_snd0, sizeof(src_snd0), "%s/sce_sys/snd0.at9", src_path); - bool has_src_snd0 = (access(src_snd0, F_OK) == 0); + if (!metadata_restaged) { + snprintf(src_snd0, sizeof(src_snd0), "%s/sce_sys/snd0.at9", src_path); + has_src_snd0 = path_exists(src_snd0); + } mark_register_attempted(title_id); - int res = sceAppInstUtilAppInstallTitleDir(title_id, "/user/app/", 0); + int res = sceAppInstUtilAppInstallTitleDir(title_id, APP_BASE "/", 0); sceKernelUsleep(200000); if (res == 0) { diff --git a/src/sm_mount_device.c b/src/sm_mount_device.c index 2bf1fc7..eb51d92 100644 --- a/src/sm_mount_device.c +++ b/src/sm_mount_device.c @@ -6,6 +6,7 @@ #include "sm_log.h" #include "sm_config_mount.h" #include "sm_mount_defs.h" +#include "sm_path_utils.h" #include "sm_stability.h" const char *attach_backend_name(attach_backend_t backend) { @@ -19,7 +20,7 @@ const char *attach_backend_name(attach_backend_t backend) { // --- Device Node Wait and Source Stability --- bool wait_for_dev_node_state(const char *devname, bool should_exist) { for (int i = 0; i < LVD_NODE_WAIT_RETRIES; i++) { - if ((access(devname, F_OK) == 0) == should_exist) + if (path_exists(devname) == should_exist) return true; sceKernelUsleep(LVD_NODE_WAIT_US); } diff --git a/src/sm_path_state.c b/src/sm_path_state.c index a33a6d8..61325cb 100644 --- a/src/sm_path_state.c +++ b/src/sm_path_state.c @@ -68,7 +68,7 @@ static struct PathStateEntry *create_path_state(const char *path) { for (int k = 0; k < PATH_STATE_CAPACITY; k++) { if (!g_path_state[k].valid) continue; - if (access(g_path_state[k].path, F_OK) != 0) { + if (!path_exists(g_path_state[k].path)) { evict_k = k; break; } @@ -156,7 +156,7 @@ void prune_path_state(void) { for (int k = 0; k < PATH_STATE_CAPACITY; k++) { if (!g_path_state[k].valid || g_path_state[k].path[0] == '\0') continue; - if (access(g_path_state[k].path, F_OK) == 0) + if (path_exists(g_path_state[k].path)) continue; memset(&g_path_state[k], 0, sizeof(g_path_state[k])); changed = true; @@ -177,7 +177,7 @@ void prune_path_state_for_root(const char *root) { continue; if (!path_matches_root_or_child(g_path_state[k].path, root)) continue; - if (access(g_path_state[k].path, F_OK) == 0) + if (path_exists(g_path_state[k].path)) continue; memset(&g_path_state[k], 0, sizeof(g_path_state[k])); changed = true; diff --git a/src/sm_path_utils.c b/src/sm_path_utils.c index 85efcbf..0f38069 100644 --- a/src/sm_path_utils.c +++ b/src/sm_path_utils.c @@ -9,6 +9,11 @@ const char *get_filename_component(const char *path) { return base ? base + 1 : path; } +bool path_exists(const char *path) { + struct stat st; + return (path && path[0] != '\0' && stat(path, &st) == 0); +} + bool is_under_image_mount_base(const char *path) { size_t image_prefix_len = strlen(IMAGE_MOUNT_BASE); return (strncmp(path, IMAGE_MOUNT_BASE, image_prefix_len) == 0 && diff --git a/src/sm_scan.c b/src/sm_scan.c index c767a7b..85a0c4a 100644 --- a/src/sm_scan.c +++ b/src/sm_scan.c @@ -20,12 +20,35 @@ typedef struct { char discovered_param_roots[MAX_PENDING][MAX_PATH]; + char checked_appmeta_titles[MAX_PENDING][MAX_TITLE_ID]; + bool checked_appmeta_present[MAX_PENDING]; + int checked_appmeta_count; } scan_workspace_t; // Reuse the largest transient scan buffer instead of placing ~512 KiB of path // state on the stack each cycle. static scan_workspace_t g_scan_workspace; +static void reset_scan_workspace(void) { + g_scan_workspace.checked_appmeta_count = 0; +} + +static bool get_appmeta_present_for_scan_cycle(const char *title_id) { + for (int i = 0; i < g_scan_workspace.checked_appmeta_count; i++) { + if (strcmp(g_scan_workspace.checked_appmeta_titles[i], title_id) == 0) + return g_scan_workspace.checked_appmeta_present[i]; + } + + bool present = has_appmeta_data(title_id); + if (g_scan_workspace.checked_appmeta_count < MAX_PENDING) { + int slot = g_scan_workspace.checked_appmeta_count++; + (void)strlcpy(g_scan_workspace.checked_appmeta_titles[slot], title_id, + sizeof(g_scan_workspace.checked_appmeta_titles[slot])); + g_scan_workspace.checked_appmeta_present[slot] = present; + } + return present; +} + static bool is_under_discovered_param_root( const char *path, char discovered_param_roots[][MAX_PATH], int discovered_count) { @@ -147,6 +170,8 @@ static existing_directory_result_t handle_existing_directory_candidate( bool in_app_db = app_db_title_list_contains(app_db_titles, info->title_id); bool installed = in_app_db && is_installed(info->title_id); + bool appmeta_present = + installed ? get_appmeta_present_for_scan_cycle(info->title_id) : false; link_matches_source = installed && link_matches_source; bool source_valid = false; if (link_matches_source) { @@ -155,12 +180,13 @@ static existing_directory_result_t handle_existing_directory_candidate( source_valid = is_data_mounted(info->title_id); } - if (source_valid) { + if (source_valid && appmeta_present) { cache_game_entry(full_path, info->title_id, info->title_name); return EXISTING_DIRECTORY_PREFER_CURRENT; } - if (installed && has_tracked_path && strcmp(tracked_path, full_path) != 0 && + if (installed && appmeta_present && has_tracked_path && + strcmp(tracked_path, full_path) != 0 && is_data_mounted(info->title_id)) { (void)strlcpy(preferred_existing_path_out, tracked_path, MAX_PATH); cache_game_entry(tracked_path, info->title_id, info->title_name); @@ -459,6 +485,7 @@ int collect_scan_candidates_for_scan_root(const char *scan_root, int max_candidates, int *total_found_out, bool *unstable_found_out) { + reset_scan_workspace(); int candidate_count = 0; const struct AppDbTitleList *app_db_titles = NULL; bool app_db_titles_ready = get_app_db_title_list_cached(&app_db_titles); @@ -482,6 +509,7 @@ int collect_scan_candidates_for_scan_root(const char *scan_root, int collect_scan_candidates(scan_candidate_t *candidates, int max_candidates, int *total_found_out, bool *unstable_found_out) { + reset_scan_workspace(); int candidate_count = 0; const struct AppDbTitleList *app_db_titles = NULL; bool app_db_titles_ready = get_app_db_title_list_cached(&app_db_titles);