diff --git a/src/LibKenjinx/LibKenjinx.cs b/src/LibKenjinx/LibKenjinx.cs index f33774437..8ab0e582b 100644 --- a/src/LibKenjinx/LibKenjinx.cs +++ b/src/LibKenjinx/LibKenjinx.cs @@ -35,6 +35,7 @@ using System.Globalization; using System.IO; using System.Runtime.InteropServices; using System.Text; +using System.Text.Json; using Path = System.IO.Path; namespace LibKenjinx @@ -209,13 +210,13 @@ namespace LibKenjinx GetControlFsAndTitleId(pfs, out IFileSystem? controlFs, out string? id); gameInfo.TitleId = id; - + if (controlFs == null) { Logger.Error?.Print(LogClass.Application, $"No control FS was returned. Unable to process game any further: {gameInfo.TitleName}"); return null; } - + // Check if there is an update available. if (IsUpdateApplied(gameInfo.TitleId, out IFileSystem? updatedControlFs)) { @@ -516,17 +517,17 @@ namespace LibKenjinx { FileStream file = new(updatePath, FileMode.Open, FileAccess.Read); IFileSystem pfs = null; - - if(Path.GetExtension(updatePath).ToLower() == ".xci") + + if (Path.GetExtension(updatePath).ToLower() == ".xci") { - pfs = new Xci(fileSystem.KeySet, file.AsStorage()).OpenPartition(XciPartitionType.Secure); + pfs = new Xci(fileSystem.KeySet, file.AsStorage()).OpenPartition(XciPartitionType.Secure); } else { - var pfsTemp = new PartitionFileSystem(); - - pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure(); - pfs = pfsTemp; + var pfsTemp = new PartitionFileSystem(); + + pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure(); + pfs = pfsTemp; } return GetGameUpdateDataFromPartition(fileSystem, pfs, titleIdBase.ToString("x16"), programIndex); @@ -625,7 +626,7 @@ namespace LibKenjinx { return new Nca(SwitchDevice?.VirtualFileSystem.KeySet, ncaStorage); } - catch (Exception ex) + catch (Exception) { } @@ -879,21 +880,152 @@ namespace LibKenjinx control.SaveDataOwnerId = applicationId.Value; } - LibHac.Result resultCode = LibHacHorizonManager.RyujinxClient.Fs.EnsureApplicationCacheStorage(out _, out _, applicationId, in control); - if (resultCode.IsFailure()) + // --- Prepare paths for the physical save directory (Android sandbox) + string savesRoot = Path.Combine( + AppDataManager.BaseDirPath, + Ryujinx.HLE.FileSystem.VirtualFileSystem.UserNandPath, + "save" + ); + + // Remember the previous list of existing save dirs (to recognize new creation) + string[] before = Array.Empty(); + try { - Logger.Error?.Print(LogClass.Application, $"Error calling EnsureApplicationCacheStorage. Result code {resultCode.ToStringWithName()}"); + if (Directory.Exists(savesRoot)) + before = Directory.GetDirectories(savesRoot); + } + catch { /* ignore */ } + + // Call existing Horizon APIs to create/secure the saves + var rc = LibHacHorizonManager.RyujinxClient.Fs.EnsureApplicationCacheStorage(out _, out _, applicationId, in control); + if (rc.IsFailure()) + { + Logger.Error?.Print(LogClass.Application, $"Error calling EnsureApplicationCacheStorage. Result code {rc.ToStringWithName()}"); } Uid userId = AccountManager.LastOpenedUser.UserId.ToLibHacUid(); - - resultCode = LibHacHorizonManager.RyujinxClient.Fs.EnsureApplicationSaveData(out _, applicationId, in control, in userId); - if (resultCode.IsFailure()) + rc = LibHacHorizonManager.RyujinxClient.Fs.EnsureApplicationSaveData(out _, applicationId, in control, in userId); + if (rc.IsFailure()) { - Logger.Error?.Print(LogClass.Application, $"Error calling EnsureApplicationSaveData. Result code {resultCode.ToStringWithName()}"); + Logger.Error?.Print(LogClass.Application, $"Error calling EnsureApplicationSaveData. Result code {rc.ToStringWithName()}"); + } + + // Get the after-list of save dirs and calculate the difference + string? createdSaveDirName = null; + try + { + Directory.CreateDirectory(savesRoot); + + var after = Directory.GetDirectories(savesRoot); + var beforeSet = new HashSet(before, StringComparer.OrdinalIgnoreCase); + + foreach (var d in after) + { + if (!beforeSet.Contains(d)) + { + // This is most likely the newly created save folder + createdSaveDirName = Path.GetFileName(d); + break; + } + } + } + catch + { + // If the listing fails, we simply continue without detection. + } + + // TitleId string normalized + string titleIdHex = titleId.ToString("x16"); + + // Marker file in the save folder + central mapping under .../save/_titleid_map.json + try + { + // 1) If we have recognized the newly created folder: Put a marker in + if (!string.IsNullOrEmpty(createdSaveDirName)) + { + string markerFile = Path.Combine(savesRoot, createdSaveDirName, "TITLEID.txt"); + File.WriteAllText(markerFile, titleIdHex); + } + + // 2) Load/update mapping file + WriteOrUpdateTitleMapJson(savesRoot, titleIdHex, createdSaveDirName); + } + catch (Exception ex) + { + Logger.Warning?.Print(LogClass.Application, $"Save TitleId mapping write failed: {ex.Message}"); } } + // ---------- Helper for TitleId→SaveId mapping ---------- + + private sealed class TitleMap + { + public Dictionary Map { get; set; } = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + private static void WriteOrUpdateTitleMapJson(string savesRoot, string titleIdHex, string? createdSaveDirName) + { + string mapFile = Path.Combine(savesRoot, "_titleid_map.json"); + + TitleMap map; + try + { + if (File.Exists(mapFile)) + { + var json = File.ReadAllText(mapFile); + map = JsonSerializer.Deserialize(json) ?? new TitleMap(); + } + else + { + map = new TitleMap(); + } + } + catch + { + map = new TitleMap(); + } + + // If we know the newly created folder, use this information. + // Otherwise, do not overwrite anything (existing mapping remains intact). + if (!string.IsNullOrEmpty(createdSaveDirName)) + { + map.Map[titleIdHex] = createdSaveDirName!; + } + else if (!map.Map.ContainsKey(titleIdHex)) + { + // Heuristic: Scan folder with TITLEID.txt and assign if necessary + try + { + foreach (var dir in Directory.GetDirectories(savesRoot)) + { + var marker = Path.Combine(dir, "TITLEID.txt"); + if (File.Exists(marker)) + { + var txt = File.ReadAllText(marker).Trim(); + if (string.Equals(txt, titleIdHex, StringComparison.OrdinalIgnoreCase)) + { + map.Map[titleIdHex] = Path.GetFileName(dir); + break; + } + } + } + } + catch { /* ignore */ } + } + + try + { + var json = JsonSerializer.Serialize(map, new JsonSerializerOptions { WriteIndented = true }); + File.WriteAllText(mapFile, json); + } + catch + { + // ignore – mapping is “best effort”. + } + } + + // ------------------------------------------------------- + internal void ReloadFileSystem() { VirtualFileSystem.ReloadKeySet();