From 0030f5b794dc2f6730f193579acac861a852c5d7 Mon Sep 17 00:00:00 2001 From: cneicy Date: Sat, 5 Sep 2026 02:33:55 +0800 Subject: [PATCH] feat: add cross-engine platform services --- Runtime/LocalStorageProvider.cs | 28 ++++---- Runtime/MigrationChain.cs | 9 ++- Runtime/SaveTypes.cs | 4 ++ Runtime/ShrinkDataSaver.Runtime.asmdef | 5 +- Runtime/ShrinkDataSaverPlatform.cs | 85 +++++++++++++++++++++++++ Runtime/ShrinkDataSaverPlatform.cs.meta | 11 ++++ Runtime/ShrinkDataSaverRuntime.cs | 3 + Runtime/ShrinkSave.cs | 47 ++++++++++---- Runtime/ShrinkSettings.cs | 16 ++--- package.json | 5 +- 10 files changed, 171 insertions(+), 42 deletions(-) create mode 100644 Runtime/ShrinkDataSaverPlatform.cs create mode 100644 Runtime/ShrinkDataSaverPlatform.cs.meta diff --git a/Runtime/LocalStorageProvider.cs b/Runtime/LocalStorageProvider.cs index c1d9c67..9541cfb 100644 --- a/Runtime/LocalStorageProvider.cs +++ b/Runtime/LocalStorageProvider.cs @@ -3,8 +3,8 @@ using System.Collections.Concurrent; using System.IO; using System.Linq; using System.Threading; +using System.Threading.Tasks; using Cysharp.Threading.Tasks; -using UnityEngine; namespace ShrinkDataSaver { @@ -21,7 +21,11 @@ namespace ShrinkDataSaver public LocalStorageProvider(string rootPath = null) { - _rootPath = rootPath ?? Application.persistentDataPath; + _rootPath = rootPath ?? ShrinkDataSaverPlatform.Paths?.PersistentDataPath +#if UNITY_5_3_OR_NEWER + ?? UnityEngine.Application.persistentDataPath +#endif + ?? throw new InvalidOperationException("ShrinkDataSaver persistent path is not configured."); } private string Resolve(string path) => @@ -33,7 +37,7 @@ namespace ShrinkDataSaver var dir = Path.GetDirectoryName(fullPath); if (!string.IsNullOrEmpty(dir)) { - await UniTask.RunOnThreadPool(() => Directory.CreateDirectory(dir), cancellationToken: ct); + await Task.Run(() => Directory.CreateDirectory(dir), ct); } var tempPath = fullPath + TempWriteSuffix; @@ -45,7 +49,7 @@ namespace ShrinkDataSaver try { await WriteFileBytesAsync(tempPath, data, ct); - await UniTask.RunOnThreadPool(() => + await Task.Run(() => { if (File.Exists(fullPath)) { @@ -65,17 +69,17 @@ namespace ShrinkDataSaver { File.Move(tempPath, fullPath); } - }, cancellationToken: ct); + }, ct); } catch { - await UniTask.RunOnThreadPool(() => + await Task.Run(() => { if (File.Exists(tempPath)) { File.Delete(tempPath); } - }, cancellationToken: CancellationToken.None); + }, CancellationToken.None); throw; } finally @@ -105,7 +109,7 @@ namespace ShrinkDataSaver } public UniTask ExistsAsync(string path, CancellationToken ct = default) => - UniTask.RunOnThreadPool(() => File.Exists(Resolve(path)), cancellationToken: ct); + Task.Run(() => File.Exists(Resolve(path)), ct).AsUniTask(); public async UniTask DeleteAsync(string path, CancellationToken ct = default) { @@ -114,13 +118,13 @@ namespace ShrinkDataSaver await pathLock.WaitAsync(ct); try { - await UniTask.RunOnThreadPool(() => + await Task.Run(() => { if (File.Exists(fullPath)) { File.Delete(fullPath); } - }, cancellationToken: ct); + }, ct); } finally { @@ -130,7 +134,7 @@ namespace ShrinkDataSaver public async UniTask ListAsync(string prefix = "", CancellationToken ct = default) { - return await UniTask.RunOnThreadPool(() => + return await Task.Run(() => { var dir = string.IsNullOrEmpty(prefix) ? _rootPath : Path.Combine(_rootPath, prefix); if (!Directory.Exists(dir)) @@ -141,7 +145,7 @@ namespace ShrinkDataSaver return Directory.GetFiles(dir) .Select(f => Path.GetRelativePath(_rootPath, f)) .ToArray(); - }, cancellationToken: ct); + }, ct); } private static SemaphoreSlim GetPathLock(string fullPath) diff --git a/Runtime/MigrationChain.cs b/Runtime/MigrationChain.cs index dbc0ecb..c84e75b 100644 --- a/Runtime/MigrationChain.cs +++ b/Runtime/MigrationChain.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using Newtonsoft.Json.Linq; -using UnityEngine; namespace ShrinkDataSaver { @@ -33,7 +32,7 @@ namespace ShrinkDataSaver { if (!Migrations.TryGetValue(version, out var migration)) { - Debug.LogWarning($"[ShrinkDataSaver] 未找到 v{version} → v{version + 1} 的迁移逻辑,数据可能不完整。"); + ShrinkDataSaverPlatform.Warning($"[ShrinkDataSaver] 未找到 v{version} → v{version + 1} 的迁移逻辑,数据可能不完整。"); break; } @@ -42,11 +41,11 @@ namespace ShrinkDataSaver var fromV = version; data = migration.Migrate(data) ?? data; version = migration.ToVersion; - Debug.Log($"[ShrinkDataSaver] 存档已迁移 v{fromV} → v{version}"); + ShrinkDataSaverPlatform.Info($"[ShrinkDataSaver] 存档已迁移 v{fromV} → v{version}"); } catch (Exception e) { - Debug.LogError( + ShrinkDataSaverPlatform.Error( $"[ShrinkDataSaver] 迁移 v{version} → v{migration.ToVersion} 失败: {e.Message},已回滚至 v{currentVersion}"); return (backup, currentVersion); } @@ -57,4 +56,4 @@ namespace ShrinkDataSaver internal static void Clear() => Migrations.Clear(); } -} \ No newline at end of file +} diff --git a/Runtime/SaveTypes.cs b/Runtime/SaveTypes.cs index 3b4c42e..849f382 100644 --- a/Runtime/SaveTypes.cs +++ b/Runtime/SaveTypes.cs @@ -63,7 +63,11 @@ namespace ShrinkDataSaver { public string SlotName = "New Save"; public bool CaptureScreenshot = false; +#if UNITY_5_3_OR_NEWER public UnityEngine.Texture2D Screenshot = null; +#else + public byte[] Screenshot; +#endif public bool Encrypt = false; public string EncryptionKey = null; } diff --git a/Runtime/ShrinkDataSaver.Runtime.asmdef b/Runtime/ShrinkDataSaver.Runtime.asmdef index 27597bb..a25bcf5 100644 --- a/Runtime/ShrinkDataSaver.Runtime.asmdef +++ b/Runtime/ShrinkDataSaver.Runtime.asmdef @@ -3,7 +3,8 @@ "rootNamespace": "ShrinkDataSaver", "references": [ "UniTask", - "Newtonsoft.Json" + "Newtonsoft.Json", + "ShrinkRuntime.Abstractions" ], "optionalUnityReferences": [], "includePlatforms": [], @@ -14,4 +15,4 @@ "defineConstraints": [], "versionDefines": [], "noEngineReferences": false -} \ No newline at end of file +} diff --git a/Runtime/ShrinkDataSaverPlatform.cs b/Runtime/ShrinkDataSaverPlatform.cs new file mode 100644 index 0000000..cf6c2ba --- /dev/null +++ b/Runtime/ShrinkDataSaverPlatform.cs @@ -0,0 +1,85 @@ +#nullable enable + +using System; +using ShrinkSDK.Runtime; + +namespace ShrinkDataSaver +{ + public static class ShrinkDataSaverPlatform + { + public static IShrinkLogger? Logger { get; private set; } + public static IShrinkClock? Clock { get; private set; } + public static IShrinkPathProvider? Paths { get; private set; } + public static IShrinkScreenshotProvider? Screenshots { get; private set; } + public static float SettingsWriteDebounceSeconds { get; set; } = 0.5f; + public static int MaxSlots { get; set; } + + public static void Configure(IShrinkPathProvider paths, IShrinkClock clock, + IShrinkLogger? logger = null, IShrinkScreenshotProvider? screenshots = null) + { + Paths = paths ?? throw new ArgumentNullException(nameof(paths)); + Clock = clock ?? throw new ArgumentNullException(nameof(clock)); + Logger = logger; + Screenshots = screenshots; + } + + public static void Initialize(string? rootPath = null, string saveFileExtension = ".json", + string settingsFileName = "settings.json", int currentSaveVersion = 1) + { + rootPath ??= Paths?.PersistentDataPath; +#if UNITY_5_3_OR_NEWER + rootPath ??= UnityEngine.Application.persistentDataPath; +#endif + 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); + } + + internal static double TimeSeconds + { + get + { + if (Clock != null) return Clock.UnscaledTimeSeconds; +#if UNITY_5_3_OR_NEWER + return UnityEngine.Time.realtimeSinceStartupAsDouble; +#else + return 0d; +#endif + } + } + + internal static void Info(string message) + { + if (Logger != null) Logger.Log(ShrinkLogLevel.Information, message); +#if UNITY_5_3_OR_NEWER + else UnityEngine.Debug.Log(message); +#endif + } + + internal static void Warning(string message) + { + if (Logger != null) Logger.Log(ShrinkLogLevel.Warning, message); +#if UNITY_5_3_OR_NEWER + else UnityEngine.Debug.LogWarning(message); +#endif + } + + internal static void Error(string message) + { + if (Logger != null) Logger.Log(ShrinkLogLevel.Error, message); +#if UNITY_5_3_OR_NEWER + else UnityEngine.Debug.LogError(message); +#endif + } + + internal static void Exception(Exception exception) + { + if (Logger != null) Logger.Log(ShrinkLogLevel.Error, exception.Message, exception); +#if UNITY_5_3_OR_NEWER + else UnityEngine.Debug.LogException(exception); +#endif + } + } +} diff --git a/Runtime/ShrinkDataSaverPlatform.cs.meta b/Runtime/ShrinkDataSaverPlatform.cs.meta new file mode 100644 index 0000000..cb35060 --- /dev/null +++ b/Runtime/ShrinkDataSaverPlatform.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: eab4e1dc3c9ba874896f3ec4f39a7ab7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/ShrinkDataSaverRuntime.cs b/Runtime/ShrinkDataSaverRuntime.cs index aaa8cbb..63b8ccc 100644 --- a/Runtime/ShrinkDataSaverRuntime.cs +++ b/Runtime/ShrinkDataSaverRuntime.cs @@ -38,6 +38,9 @@ namespace ShrinkDataSaver var settingsPath = Path.Combine(rootPath, cfg.settingsFileName); var storage = new LocalStorageProvider(rootPath); + ShrinkDataSaverPlatform.SettingsWriteDebounceSeconds = cfg.settingsWriteDebounceSeconds; + ShrinkDataSaverPlatform.MaxSlots = cfg.maxSlots; + ShrinkSettings.Initialize(storage, settingsPath); ShrinkSave.Initialize(storage, savesDir, cfg.saveFileExtension, config.CurrentSaveVersion); ShrinkSettings.LoadAsync().Forget(); diff --git a/Runtime/ShrinkSave.cs b/Runtime/ShrinkSave.cs index ecda16b..99a8291 100644 --- a/Runtime/ShrinkSave.cs +++ b/Runtime/ShrinkSave.cs @@ -4,7 +4,9 @@ using System.IO; using System.Threading; using Cysharp.Threading.Tasks; using Newtonsoft.Json.Linq; +#if UNITY_5_3_OR_NEWER using UnityEngine; +#endif namespace ShrinkDataSaver { @@ -141,7 +143,7 @@ namespace ShrinkDataSaver } catch (Exception e) { - Debug.LogWarning($"[ShrinkDataSaver] 读取元数据失败 slot_{slotIndex}: {e.Message}"); + ShrinkDataSaverPlatform.Warning($"[ShrinkDataSaver] 读取元数据失败 slot_{slotIndex}: {e.Message}"); } } @@ -200,7 +202,11 @@ namespace ShrinkDataSaver } }; +#if UNITY_5_3_OR_NEWER if (options.Screenshot || options.CaptureScreenshot) +#else + if (options.Screenshot != null || options.CaptureScreenshot) +#endif { packet.Meta.ScreenshotBase64 = await CaptureScreenshotAsync(options, ct); } @@ -223,7 +229,7 @@ namespace ShrinkDataSaver throw new InvalidOperationException($"关键模块 '{module.Key}' 序列化失败: {e.Message}", e); } - Debug.LogError($"[ShrinkDataSaver] 模块 '{module.Key}' 序列化失败(已跳过): {e.Message}"); + ShrinkDataSaverPlatform.Error($"[ShrinkDataSaver] 模块 '{module.Key}' 序列化失败(已跳过): {e.Message}"); } } @@ -252,7 +258,7 @@ namespace ShrinkDataSaver ModuleNames = moduleNames.ToArray(), Timestamp = timestamp }); - Debug.Log($"[ShrinkDataSaver] 槽位 {slotIndex} 已保存。({moduleNames.Count} 个模块)"); + ShrinkDataSaverPlatform.Info($"[ShrinkDataSaver] 槽位 {slotIndex} 已保存。({moduleNames.Count} 个模块)"); } catch (Exception e) { @@ -293,13 +299,13 @@ namespace ShrinkDataSaver } catch (Exception e) { - Debug.LogError($"[ShrinkDataSaver] 反序列化模块 '{module.Key}' 失败: {e.Message}"); + ShrinkDataSaverPlatform.Error($"[ShrinkDataSaver] 反序列化模块 '{module.Key}' 失败: {e.Message}"); } } _loadedSlot = slotIndex; _storedPlaytime = packet.Meta.PlaytimeSeconds; - _sessionStart = Time.realtimeSinceStartup; + _sessionStart = (float)ShrinkDataSaverPlatform.TimeSeconds; await PersistRecentSlotIndexAsync(slotIndex, ct); OnLoadCompleted?.Invoke(new LoadCompletedEventArgs @@ -309,7 +315,7 @@ namespace ShrinkDataSaver Version = packet.Meta.SaveVersion, Timestamp = timestamp }); - Debug.Log($"[ShrinkDataSaver] 槽位 {slotIndex} 已加载。(v{packet.Meta.SaveVersion}, {moduleNames.Count} 个模块)"); + ShrinkDataSaverPlatform.Info($"[ShrinkDataSaver] 槽位 {slotIndex} 已加载。(v{packet.Meta.SaveVersion}, {moduleNames.Count} 个模块)"); } catch (Exception e) { @@ -344,7 +350,7 @@ namespace ShrinkDataSaver } OnDeleteCompleted?.Invoke(new SlotDeletedEventArgs { SlotIndex = slotIndex }); - Debug.Log($"[ShrinkDataSaver] 槽位 {slotIndex} 已删除。"); + ShrinkDataSaverPlatform.Info($"[ShrinkDataSaver] 槽位 {slotIndex} 已删除。"); } public static float GetMinAutoSaveInterval() @@ -355,7 +361,7 @@ namespace ShrinkDataSaver { if (cfg.AutoSaveIntervalSeconds > 0f) { - min = Mathf.Min(min, cfg.AutoSaveIntervalSeconds); + min = Math.Min(min, cfg.AutoSaveIntervalSeconds); hasAny = true; } } @@ -406,7 +412,11 @@ namespace ShrinkDataSaver throw new ArgumentOutOfRangeException(nameof(slotIndex), "槽位索引不能为负数。"); } +#if UNITY_5_3_OR_NEWER var max = ShrinkDataSaverSettings.Instance.maxSlots; +#else + var max = ShrinkDataSaverPlatform.MaxSlots; +#endif if (max > 0 && slotIndex >= max) { throw new ArgumentOutOfRangeException(nameof(slotIndex), $"超出最大槽位数 ({max})。"); @@ -529,7 +539,7 @@ namespace ShrinkDataSaver if (!string.Equals(candidatePath, primaryPath, StringComparison.Ordinal)) { - Debug.LogWarning($"[ShrinkDataSaver] 槽位 {slotIndex} 主文件损坏或缺失,已从备份恢复:{candidatePath}"); + ShrinkDataSaverPlatform.Warning($"[ShrinkDataSaver] 槽位 {slotIndex} 主文件损坏或缺失,已从备份恢复:{candidatePath}"); await _storage.WriteAsync(primaryPath, bytes, ct); } @@ -538,7 +548,7 @@ namespace ShrinkDataSaver catch (Exception ex) { lastError = ex; - Debug.LogWarning($"[ShrinkDataSaver] 读取槽位副本失败:{candidatePath} / {ex.Message}"); + ShrinkDataSaverPlatform.Warning($"[ShrinkDataSaver] 读取槽位副本失败:{candidatePath} / {ex.Message}"); } } @@ -612,7 +622,7 @@ namespace ShrinkDataSaver if (!string.Equals(candidatePath, primaryPath, StringComparison.Ordinal)) { - Debug.LogWarning($"[ShrinkDataSaver] 槽位 {slotIndex} 主文件损坏或缺失,已从备份恢复:{candidatePath}"); + ShrinkDataSaverPlatform.Warning($"[ShrinkDataSaver] 槽位 {slotIndex} 主文件损坏或缺失,已从备份恢复:{candidatePath}"); await _storage.WriteAsync(primaryPath, bytes, ct); } @@ -621,7 +631,7 @@ namespace ShrinkDataSaver catch (Exception ex) { lastError = ex; - Debug.LogWarning($"[ShrinkDataSaver] 加载槽位副本失败:{candidatePath} / {ex.Message}"); + ShrinkDataSaverPlatform.Warning($"[ShrinkDataSaver] 加载槽位副本失败:{candidatePath} / {ex.Message}"); } } @@ -633,10 +643,11 @@ namespace ShrinkDataSaver throw new FileNotFoundException($"槽位 {slotIndex} 不存在。"); } - private static float GetCurrentPlaytime() => _loadedSlot < 0 ? 0f : _storedPlaytime + (Time.realtimeSinceStartup - _sessionStart); + private static float GetCurrentPlaytime() => _loadedSlot < 0 ? 0f : _storedPlaytime + ((float)ShrinkDataSaverPlatform.TimeSeconds - _sessionStart); private static async UniTask CaptureScreenshotAsync(SaveOptions options, CancellationToken ct) { +#if UNITY_5_3_OR_NEWER var tex = options.Screenshot; if (!tex && options.CaptureScreenshot) { @@ -668,6 +679,16 @@ namespace ShrinkDataSaver } return Convert.ToBase64String(tex.EncodeToJPG(75)); +#else + var pngBytes = options.Screenshot; + if (pngBytes == null && options.CaptureScreenshot) + { + var provider = ShrinkDataSaverPlatform.Screenshots ?? + throw new InvalidOperationException("Screenshot capture was requested but no platform provider is configured."); + pngBytes = await provider.CapturePngAsync(ct); + } + return pngBytes == null ? string.Empty : Convert.ToBase64String(pngBytes); +#endif } } } diff --git a/Runtime/ShrinkSettings.cs b/Runtime/ShrinkSettings.cs index 6bcb650..76209cc 100644 --- a/Runtime/ShrinkSettings.cs +++ b/Runtime/ShrinkSettings.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; using System.Threading; +using System.Threading.Tasks; using Cysharp.Threading.Tasks; using Newtonsoft.Json.Linq; -using UnityEngine; namespace ShrinkDataSaver { @@ -112,7 +112,7 @@ namespace ShrinkDataSaver if (_watchers.TryGetValue(key, out var list)) foreach (var cb in list) - try { cb(value); } catch (Exception e) { Debug.LogException(e); } + try { cb(value); } catch (Exception e) { ShrinkDataSaverPlatform.Exception(e); } } private static async UniTaskVoid ScheduleWrite() @@ -123,12 +123,12 @@ namespace ShrinkDataSaver try { - var delay = (int)(ShrinkDataSaverSettings.Instance.settingsWriteDebounceSeconds * 1000); - await UniTask.Delay(delay, cancellationToken: token); + var delay = (int)(ShrinkDataSaverPlatform.SettingsWriteDebounceSeconds * 1000); + await Task.Delay(delay, token); await SaveAsync(token); } catch (OperationCanceledException) { } - catch (Exception e) { Debug.LogException(e); } + catch (Exception e) { ShrinkDataSaverPlatform.Exception(e); } } private static async UniTask TryLoadWithFallbackAsync(CancellationToken ct) @@ -149,7 +149,7 @@ namespace ShrinkDataSaver var loaded = DataSerializer.Deserialize(bytes) ?? new SettingsData(); if (!string.Equals(candidatePath, primaryPath, StringComparison.Ordinal)) { - Debug.LogWarning($"[ShrinkDataSaver] Settings 主文件损坏或缺失,已从备份恢复:{candidatePath}"); + ShrinkDataSaverPlatform.Warning($"[ShrinkDataSaver] Settings 主文件损坏或缺失,已从备份恢复:{candidatePath}"); await _storage.WriteAsync(primaryPath, bytes, ct); } @@ -158,13 +158,13 @@ namespace ShrinkDataSaver catch (Exception ex) { lastError = ex; - Debug.LogWarning($"[ShrinkDataSaver] 读取 Settings 副本失败:{candidatePath} / {ex.Message}"); + ShrinkDataSaverPlatform.Warning($"[ShrinkDataSaver] 读取 Settings 副本失败:{candidatePath} / {ex.Message}"); } } if (lastError != null) { - Debug.LogWarning("[ShrinkDataSaver] 所有 Settings 副本均不可用,已回退为空设置。"); + ShrinkDataSaverPlatform.Warning("[ShrinkDataSaver] 所有 Settings 副本均不可用,已回退为空设置。"); } return new SettingsData(); diff --git a/package.json b/package.json index 38754e3..2deb19d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "com.cneicy.shrink-datasaver", - "version": "2.2.2", + "version": "2.3.0", "displayName": "ShrinkDataSaver", "description": "模块化 Unity 存档与设置管理系统,支持多槽位、链式版本迁移、AES-256 加密、关键模块保护、跨模块查询与事件驱动架构。", "unity": "2022.3", @@ -11,7 +11,8 @@ "com.unity.nuget.newtonsoft-json": "3.2.1", "com.cysharp.unitask": "2.5.0", "com.unity.modules.imageconversion": "1.0.0", - "com.unity.modules.screencapture": "1.0.0" + "com.unity.modules.screencapture": "1.0.0", + "com.cneicy.shrink-runtime-abstractions": "0.1.0" }, "keywords": [ "save",