feat: update Godot integration and coordinated package dependencies
Publish UPM package / publish (push) Successful in 2s
Publish NuGet packages / publish (push) Successful in 4m2s

This commit is contained in:
cneicy committed 2026-11-06 19:13:16 +08:00
1 parent cdb682f484
commit 67d6814c4b
11 files changed
+525 -49

No files matched your search

+159 -40
View File
@@ -8,6 +8,12 @@ using Cysharp.Threading.Tasks;
namespace ShrinkDataSaver
{
public enum LocalStorageCommitStrategy
{
FileReplace = 0,
RenameOnly = 1
}
public class LocalStorageProvider : IStorageProvider
{
private const string BackupPrimarySuffix = ".bak1";
@@ -18,16 +24,37 @@ namespace ShrinkDataSaver
new(StringComparer.OrdinalIgnoreCase);
private readonly string _rootPath;
private readonly LocalStorageCommitStrategy _commitStrategy;
private readonly Action<string, string> _overwriteMove;
public LocalStorageProvider(string rootPath = null)
: this(rootPath, LocalStorageCommitStrategy.FileReplace)
{
}
public LocalStorageProvider(string rootPath, LocalStorageCommitStrategy commitStrategy)
: this(rootPath, commitStrategy, null)
{
}
public LocalStorageProvider(string rootPath, LocalStorageCommitStrategy commitStrategy,
Action<string, string> overwriteMove)
{
if (!Enum.IsDefined(typeof(LocalStorageCommitStrategy), commitStrategy))
throw new ArgumentOutOfRangeException(nameof(commitStrategy));
if (commitStrategy == LocalStorageCommitStrategy.RenameOnly && overwriteMove == null)
throw new ArgumentException("RenameOnly requires an overwrite-rename implementation.", nameof(overwriteMove));
_overwriteMove = overwriteMove;
_rootPath = rootPath ?? ShrinkDataSaverPlatform.Paths?.PersistentDataPath
#if UNITY_5_3_OR_NEWER
?? UnityEngine.Application.persistentDataPath
#endif
?? throw new InvalidOperationException("ShrinkDataSaver persistent path is not configured.");
_commitStrategy = commitStrategy;
}
public LocalStorageCommitStrategy CommitStrategy => _commitStrategy;
private string Resolve(string path) =>
Path.IsPathRooted(path) ? path : Path.Combine(_rootPath, path);
@@ -40,47 +67,25 @@ namespace ShrinkDataSaver
await Task.Run(() => Directory.CreateDirectory(dir), ct);
}
var tempPath = fullPath + TempWriteSuffix;
var backupPath = fullPath + BackupPrimarySuffix;
var secondaryBackupPath = fullPath + BackupSecondarySuffix;
var pathLock = GetPathLock(fullPath);
await pathLock.WaitAsync(ct);
try
{
await WriteFileBytesAsync(tempPath, data, ct);
await Task.Run(() =>
{
if (File.Exists(fullPath))
{
if (File.Exists(secondaryBackupPath))
{
File.Delete(secondaryBackupPath);
}
await Task.Run(() => RecoverInterruptedWrite(fullPath), CancellationToken.None);
ct.ThrowIfCancellationRequested();
if (File.Exists(backupPath))
{
File.Move(backupPath, secondaryBackupPath);
}
File.Replace(tempPath, fullPath, backupPath, true);
}
else
{
File.Move(tempPath, fullPath);
}
}, ct);
}
catch
{
await Task.Run(() =>
var tempPath = fullPath + TempWriteSuffix;
try
{
if (File.Exists(tempPath))
{
File.Delete(tempPath);
}
}, CancellationToken.None);
throw;
await WriteFileBytesAsync(tempPath, data, ct);
ct.ThrowIfCancellationRequested();
await Task.Run(() => Commit(tempPath, fullPath), CancellationToken.None);
}
catch
{
await Task.Run(() => TryDelete(tempPath), CancellationToken.None);
throw;
}
}
finally
{
@@ -95,6 +100,7 @@ namespace ShrinkDataSaver
await pathLock.WaitAsync(ct);
try
{
await Task.Run(() => RecoverInterruptedWrite(fullPath), CancellationToken.None);
if (!File.Exists(fullPath))
{
throw new FileNotFoundException($"ShrinkDataSaver: file not found: {fullPath}");
@@ -109,7 +115,22 @@ namespace ShrinkDataSaver
}
public async UniTask<bool> ExistsAsync(string path, CancellationToken ct = default) =>
await Task.Run(() => File.Exists(Resolve(path)), ct);
await ExistsCoreAsync(Resolve(path), ct);
private async UniTask<bool> ExistsCoreAsync(string fullPath, CancellationToken ct)
{
var pathLock = GetPathLock(fullPath);
await pathLock.WaitAsync(ct);
try
{
await Task.Run(() => RecoverInterruptedWrite(fullPath), CancellationToken.None);
return File.Exists(fullPath);
}
finally
{
pathLock.Release();
}
}
public async UniTask DeleteAsync(string path, CancellationToken ct = default)
{
@@ -120,10 +141,10 @@ namespace ShrinkDataSaver
{
await Task.Run(() =>
{
if (File.Exists(fullPath))
{
File.Delete(fullPath);
}
File.Delete(fullPath + TempWriteSuffix);
File.Delete(fullPath + BackupPrimarySuffix);
File.Delete(fullPath + BackupSecondarySuffix);
File.Delete(fullPath);
}, ct);
}
finally
@@ -148,6 +169,104 @@ namespace ShrinkDataSaver
}, ct);
}
private void Commit(string tempPath, string fullPath)
{
if (_commitStrategy == LocalStorageCommitStrategy.RenameOnly)
CommitRenameOnly(tempPath, fullPath);
else
CommitFileReplace(tempPath, fullPath);
}
private static void CommitFileReplace(string tempPath, string fullPath)
{
var backupPath = fullPath + BackupPrimarySuffix;
var secondaryBackupPath = fullPath + BackupSecondarySuffix;
if (!File.Exists(fullPath))
{
File.Move(tempPath, fullPath);
return;
}
if (File.Exists(secondaryBackupPath))
{
File.Delete(secondaryBackupPath);
}
if (File.Exists(backupPath))
{
File.Move(backupPath, secondaryBackupPath);
}
File.Replace(tempPath, fullPath, backupPath, true);
}
private void CommitRenameOnly(string tempPath, string fullPath)
{
var backupPath = fullPath + BackupPrimarySuffix;
var secondaryBackupPath = fullPath + BackupSecondarySuffix;
if (File.Exists(fullPath))
{
if (File.Exists(backupPath))
{
if (File.Exists(secondaryBackupPath))
{
File.Delete(secondaryBackupPath);
}
MoveFileReplacing(backupPath, secondaryBackupPath);
}
MoveFileReplacing(fullPath, backupPath);
}
MoveFileReplacing(tempPath, fullPath);
}
private void RecoverInterruptedWrite(string fullPath)
{
if (_commitStrategy != LocalStorageCommitStrategy.RenameOnly)
return;
var tempPath = fullPath + TempWriteSuffix;
var primaryBackupPath = fullPath + BackupPrimarySuffix;
var secondaryBackupPath = fullPath + BackupSecondarySuffix;
if (!File.Exists(fullPath))
{
if (File.Exists(primaryBackupPath))
{
MoveFileReplacing(primaryBackupPath, fullPath);
}
else if (File.Exists(secondaryBackupPath))
{
MoveFileReplacing(secondaryBackupPath, fullPath);
}
}
if (File.Exists(tempPath))
{
TryDelete(tempPath);
}
}
private void MoveFileReplacing(string source, string destination) => _overwriteMove(source, destination);
private static void TryDelete(string path)
{
try
{
if (File.Exists(path))
{
File.Delete(path);
}
}
catch (IOException)
{
}
catch (UnauthorizedAccessException)
{
}
}
private static SemaphoreSlim GetPathLock(string fullPath)
{
return PathLocks.GetOrAdd(fullPath, _ => new SemaphoreSlim(1, 1));
+22 -2
View File
@@ -33,8 +33,28 @@ namespace ShrinkDataSaver
if (string.IsNullOrWhiteSpace(rootPath))
throw new InvalidOperationException("ShrinkDataSaver platform paths are not configured.");
var storage = new LocalStorageProvider(rootPath);
ShrinkSettings.Initialize(storage, System.IO.Path.Combine(rootPath, settingsFileName));
ShrinkSave.Initialize(storage, System.IO.Path.Combine(rootPath, "saves"), saveFileExtension, currentSaveVersion);
Initialize(storage, rootPath, saveFileExtension, settingsFileName, currentSaveVersion);
}
public static void Initialize(IStorageProvider storage, string? rootPath = null,
string saveFileExtension = ".json", string settingsFileName = "settings.json",
int currentSaveVersion = 1)
{
if (storage == null)
throw new ArgumentNullException(nameof(storage));
rootPath ??= Paths?.PersistentDataPath;
#if UNITY_5_3_OR_NEWER
rootPath ??= UnityEngine.Application.persistentDataPath;
#endif
var settingsPath = string.IsNullOrWhiteSpace(rootPath)
? settingsFileName
: System.IO.Path.Combine(rootPath, settingsFileName);
var savesDir = string.IsNullOrWhiteSpace(rootPath)
? "saves"
: System.IO.Path.Combine(rootPath, "saves");
ShrinkSettings.Initialize(storage, settingsPath);
ShrinkSave.Initialize(storage, savesDir, saveFileExtension, currentSaveVersion);
}
internal static double TimeSeconds
+2 -1
View File
@@ -11,6 +11,7 @@ namespace ShrinkDataSaver
public ShrinkDataSaverSettings? SettingsOverride { get; set; }
public int CurrentSaveVersion { get; set; } = 1;
public bool DontDestroyOnLoadDriver { get; set; } = true;
public IStorageProvider? Storage { get; set; }
}
public static class ShrinkDataSaverRuntime
@@ -36,7 +37,7 @@ namespace ShrinkDataSaver
var savesDir = Path.Combine(rootPath, "saves");
var settingsPath = Path.Combine(rootPath, cfg.settingsFileName);
var storage = new LocalStorageProvider(rootPath);
var storage = config.Storage ?? new LocalStorageProvider(rootPath);
ShrinkDataSaverPlatform.SettingsWriteDebounceSeconds = cfg.settingsWriteDebounceSeconds;
ShrinkDataSaverPlatform.MaxSlots = cfg.maxSlots;