mirror of
https://git.ryujinx.app/projects/Kenji-NX.git
synced 2026-10-08 12:39:03 +02:00
linked titleId to Save File
This commit is contained in:
+149
-17
@@ -35,6 +35,7 @@ using System.Globalization;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
using Path = System.IO.Path;
|
using Path = System.IO.Path;
|
||||||
|
|
||||||
namespace LibKenjinx
|
namespace LibKenjinx
|
||||||
@@ -209,13 +210,13 @@ namespace LibKenjinx
|
|||||||
GetControlFsAndTitleId(pfs, out IFileSystem? controlFs, out string? id);
|
GetControlFsAndTitleId(pfs, out IFileSystem? controlFs, out string? id);
|
||||||
|
|
||||||
gameInfo.TitleId = id;
|
gameInfo.TitleId = id;
|
||||||
|
|
||||||
if (controlFs == null)
|
if (controlFs == null)
|
||||||
{
|
{
|
||||||
Logger.Error?.Print(LogClass.Application, $"No control FS was returned. Unable to process game any further: {gameInfo.TitleName}");
|
Logger.Error?.Print(LogClass.Application, $"No control FS was returned. Unable to process game any further: {gameInfo.TitleName}");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if there is an update available.
|
// Check if there is an update available.
|
||||||
if (IsUpdateApplied(gameInfo.TitleId, out IFileSystem? updatedControlFs))
|
if (IsUpdateApplied(gameInfo.TitleId, out IFileSystem? updatedControlFs))
|
||||||
{
|
{
|
||||||
@@ -516,17 +517,17 @@ namespace LibKenjinx
|
|||||||
{
|
{
|
||||||
FileStream file = new(updatePath, FileMode.Open, FileAccess.Read);
|
FileStream file = new(updatePath, FileMode.Open, FileAccess.Read);
|
||||||
IFileSystem pfs = null;
|
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
|
else
|
||||||
{
|
{
|
||||||
var pfsTemp = new PartitionFileSystem();
|
var pfsTemp = new PartitionFileSystem();
|
||||||
|
|
||||||
pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure();
|
pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure();
|
||||||
pfs = pfsTemp;
|
pfs = pfsTemp;
|
||||||
}
|
}
|
||||||
|
|
||||||
return GetGameUpdateDataFromPartition(fileSystem, pfs, titleIdBase.ToString("x16"), programIndex);
|
return GetGameUpdateDataFromPartition(fileSystem, pfs, titleIdBase.ToString("x16"), programIndex);
|
||||||
@@ -625,7 +626,7 @@ namespace LibKenjinx
|
|||||||
{
|
{
|
||||||
return new Nca(SwitchDevice?.VirtualFileSystem.KeySet, ncaStorage);
|
return new Nca(SwitchDevice?.VirtualFileSystem.KeySet, ncaStorage);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -879,21 +880,152 @@ namespace LibKenjinx
|
|||||||
control.SaveDataOwnerId = applicationId.Value;
|
control.SaveDataOwnerId = applicationId.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
LibHac.Result resultCode = LibHacHorizonManager.RyujinxClient.Fs.EnsureApplicationCacheStorage(out _, out _, applicationId, in control);
|
// --- Prepare paths for the physical save directory (Android sandbox)
|
||||||
if (resultCode.IsFailure())
|
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<string>();
|
||||||
|
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();
|
Uid userId = AccountManager.LastOpenedUser.UserId.ToLibHacUid();
|
||||||
|
rc = LibHacHorizonManager.RyujinxClient.Fs.EnsureApplicationSaveData(out _, applicationId, in control, in userId);
|
||||||
resultCode = LibHacHorizonManager.RyujinxClient.Fs.EnsureApplicationSaveData(out _, applicationId, in control, in userId);
|
if (rc.IsFailure())
|
||||||
if (resultCode.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<string>(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<string, string> Map { get; set; } = new Dictionary<string, string>(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<TitleMap>(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()
|
internal void ReloadFileSystem()
|
||||||
{
|
{
|
||||||
VirtualFileSystem.ReloadKeySet();
|
VirtualFileSystem.ReloadKeySet();
|
||||||
|
|||||||
Reference in New Issue
Block a user