diff --git a/CHANGELOG.md b/CHANGELOG.md index 417fa257..aaabbccb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,28 @@ ### Fixes +* Updated the bundled `AssetsTools.NET` to read AssetRipper's version 2 + `classdata.tpk` container, fixing an `Unsupported or invalid file version 2` + exception that failed game import outright + * [ClassDataManager](Editor/Core/Utilities/ClassDataManager.cs) now stages each + download and promotes it over the cache only once the bundled `AssetsTools.NET` + proves it can read it, so a future format bump can no longer replace a working + `classdata.tpk` + * A tpk this build cannot parse is never returned from `GetClassDataPath`, and + [ImportProjectSettings](Editor/Core/Config/Common/ImportProjectSettings.cs) + raises `UnsupportedClassDataException` on an unreadable package — skipping the + settings import instead of aborting the game import + +### Tests + +* [ClassDataManagerTests](Tests/Editor/ClassDataManagerTests.cs) now cover the + unreadable-tpk guard — an incompatible download leaves the cache untouched, and no + unusable tpk resolves to a usable path + +## 9.4.4 + +### Fixes + * [ImportConfiguration](Editor/Core/Config/ImportConfiguration.cs) no longer loses its executor list when the Editor is closed and reopened — `ConfigurationExecutors` is now saved after it is populated rather than before diff --git a/Editor/Core/Config/Common/ImportProjectSettings.cs b/Editor/Core/Config/Common/ImportProjectSettings.cs index 51c57300..b408636f 100644 --- a/Editor/Core/Config/Common/ImportProjectSettings.cs +++ b/Editor/Core/Config/Common/ImportProjectSettings.cs @@ -127,7 +127,16 @@ internal List ExportProjectSettings(string classDataPath, string globalG }; assetsManager = new AssetsManager(); - assetsManager.LoadClassPackage(classDataPath); + // A tpk in a container format newer than the bundled AssetsTools.NET throws + // here; skip the settings import rather than failing the whole game import. + try + { + assetsManager.LoadClassPackage(classDataPath); + } + catch (Exception e) + { + throw new UnsupportedClassDataException($"class data at '{classDataPath}' could not be read ({e.Message})."); + } // Prefer the version that built the game, recorded in globalgamemanagers, over the Editor's. var globalGameManagersFile = assetsManager.LoadAssetsFile(globalGameManagersPath, true); diff --git a/Editor/Core/Utilities/ClassDataManager.cs b/Editor/Core/Utilities/ClassDataManager.cs index ed7fdd5e..3e0d212b 100644 --- a/Editor/Core/Utilities/ClassDataManager.cs +++ b/Editor/Core/Utilities/ClassDataManager.cs @@ -24,15 +24,42 @@ internal static class ClassDataManager static readonly string MetadataPath = Path.Combine("Library", "ThunderKit", "classdata.tpk.json"); static readonly TimeSpan RetryThrottle = TimeSpan.FromDays(1); + // How a tpk stands relative to the bundled AssetsTools.NET and a Unity version. + internal enum TpkState + { + Missing, + // Container format newer than the bundled AssetsTools.NET, or corrupt. + // Unusable regardless of which Unity versions it covers. + Unreadable, + Uncovered, + Covered, + } + + internal enum TpkDownloadResult + { + Failed, + // Fetched, but unreadable here. Discarded instead of promoted. + Incompatible, + Downloaded, + } + internal enum ClassDataStatus { CacheSupported, DownloadedSupported, Throttled, UnsupportedAfterDownload, + DownloadIncompatible, DownloadFailed, } + internal enum ClassDataResolution + { + UseCache, + UseCacheWithWarning, + None, + } + [Serializable] class TpkMetadata { @@ -42,14 +69,13 @@ class TpkMetadata public static string GetClassDataPath() { var unityVersion = Application.unityVersion; - var cacheSupports = SupportsVersion(CachedTpkPath, unityVersion); - var throttled = IsThrottledNow(DateTime.UtcNow); + var cacheState = InspectTpk(CachedTpkPath, unityVersion); var status = PlanAcquisition( - cacheSupports, - throttled, - tryDownload: TryDownloadTpk, - cacheSupportsAfterDownload: () => SupportsVersion(CachedTpkPath, unityVersion)); + cacheState, + IsThrottledNow(DateTime.UtcNow), + tryDownload: () => TryDownloadTpk(unityVersion), + cacheStateAfterDownload: () => InspectTpk(CachedTpkPath, unityVersion)); switch (status) { @@ -58,75 +84,112 @@ public static string GetClassDataPath() ClearAttemptMarker(); return CachedTpkPath; + case ClassDataStatus.DownloadIncompatible: + WarnTpkFormatUnsupported(); + WriteAttemptMarker(DateTime.UtcNow); + return PathFor(cacheState, unityVersion); + case ClassDataStatus.UnsupportedAfterDownload: + WriteAttemptMarker(DateTime.UtcNow); + return PathFor(InspectTpk(CachedTpkPath, unityVersion), unityVersion); + case ClassDataStatus.DownloadFailed: WriteAttemptMarker(DateTime.UtcNow); - return BestAvailableOrNull(unityVersion); + return PathFor(cacheState, unityVersion); case ClassDataStatus.Throttled: - return BestAvailableOrNull(unityVersion); + return PathFor(cacheState, unityVersion); default: return null; } } - // Returns the cached tpk (with a warning that it may not fully cover the running - // Unity version) when one exists, or null with an error when none is available. - static string BestAvailableOrNull(string unityVersion) + internal static ClassDataStatus PlanAcquisition(TpkState cacheState, bool throttled, + Func tryDownload, Func cacheStateAfterDownload) { - if (File.Exists(CachedTpkPath)) + if (cacheState == TpkState.Covered) + return ClassDataStatus.CacheSupported; + + if (throttled) + return ClassDataStatus.Throttled; + + switch (tryDownload()) { - WarnVersionNotCovered(unityVersion); - return CachedTpkPath; + case TpkDownloadResult.Failed: + return ClassDataStatus.DownloadFailed; + + case TpkDownloadResult.Incompatible: + return ClassDataStatus.DownloadIncompatible; } - Debug.LogError($"[ThunderKit] No classdata.tpk is available and one could not be downloaded for Unity {unityVersion}. ProjectSettings import will be skipped."); - return null; + return cacheStateAfterDownload() == TpkState.Covered + ? ClassDataStatus.DownloadedSupported + : ClassDataStatus.UnsupportedAfterDownload; } - internal static ClassDataStatus PlanAcquisition(bool cacheSupports, bool throttled, - Func tryDownload, Func cacheSupportsAfterDownload) + // An uncovered tpk is still worth using — SelectBestVersion falls back to the + // closest type data — but an unreadable one can only throw at load time. + internal static ClassDataResolution ResolveFromState(TpkState state) { - if (cacheSupports) - return ClassDataStatus.CacheSupported; + switch (state) + { + case TpkState.Covered: + return ClassDataResolution.UseCache; - if (throttled) - return ClassDataStatus.Throttled; + case TpkState.Uncovered: + return ClassDataResolution.UseCacheWithWarning; - if (!tryDownload()) - return ClassDataStatus.DownloadFailed; + default: + return ClassDataResolution.None; + } + } - return cacheSupportsAfterDownload() - ? ClassDataStatus.DownloadedSupported - : ClassDataStatus.UnsupportedAfterDownload; + static string PathFor(TpkState state, string unityVersion) + { + switch (ResolveFromState(state)) + { + case ClassDataResolution.UseCache: + return CachedTpkPath; + + case ClassDataResolution.UseCacheWithWarning: + WarnVersionNotCovered(unityVersion); + return CachedTpkPath; + + default: + Debug.LogError($"[ThunderKit] No usable class data (classdata.tpk) is available for Unity {unityVersion}. ProjectSettings import will be skipped."); + return null; + } } - internal static bool SupportsVersion(string tpkPath, string unityVersion) + internal static TpkState InspectTpk(string tpkPath, string unityVersion) { if (!File.Exists(tpkPath)) - return false; - // Coverage is decided on major.minor only. Type trees rarely change in a - // patch release, and our use (ProjectSettings) touches a small, stable set - // of types, so requiring an exact patch match would reject usable tpks. - if (!TryParseUnityVersion(unityVersion, out var major, out var minor, out _)) - return false; + return TpkState.Missing; + List versions; try { - var manager = new AssetsManager(); - var package = manager.LoadClassPackage(tpkPath); - var versions = package?.TpkTypeTree?.Versions; - if (versions == null) - return false; - - return versions.Any(v => v.major == major && v.minor == minor); + versions = new AssetsManager().LoadClassPackage(tpkPath)?.TpkTypeTree?.Versions; } catch (Exception e) { - Debug.LogWarning($"[ThunderKit] Failed to inspect classdata.tpk versions: {e.Message}"); - return false; + Debug.LogWarning($"[ThunderKit] Class data at {tpkPath} could not be read by the AssetsTools.NET bundled with this version of ThunderKit: {e.Message}"); + return TpkState.Unreadable; } + + if (versions == null) + return TpkState.Unreadable; + + // Coverage is decided on major.minor only. Type trees rarely change in a + // patch release, and our use (ProjectSettings) touches a small, stable set + // of types, so requiring an exact patch match would reject usable tpks. + if (!TryParseUnityVersion(unityVersion, out var major, out var minor, out _)) + return TpkState.Uncovered; + + return versions.Any(v => v.major == major && v.minor == minor) + ? TpkState.Covered + : TpkState.Uncovered; } // Picks the tpk version to build a class database from for the running Unity @@ -233,13 +296,19 @@ internal static bool TryReadLastAttemptUtc(string json, out DateTime lastAttempt } } - static bool TryDownloadTpk() + // Downloads into a staging directory and promotes over the cache only after the + // bundled AssetsTools.NET proves it can read the result, so a tpk published in a + // newer container format cannot destroy a cache that still works. + static TpkDownloadResult TryDownloadTpk(string unityVersion) { + var stagingDir = Path.Combine(Constants.TempDir, "classdata_staging"); try { - Debug.LogWarning("[ThunderKit] Downloading tpk archive"); + Debug.Log("[ThunderKit] Downloading tpk archive"); Directory.CreateDirectory(CacheDir); Directory.CreateDirectory(Constants.TempDir); + SafeDeleteDirectory(stagingDir); + Directory.CreateDirectory(stagingDir); var tempZipPath = Path.Combine(Constants.TempDir, "classdata_download.zip"); @@ -248,25 +317,47 @@ static bool TryDownloadTpk() client.DownloadFile(TpkDownloadUrl, tempZipPath); } - if (ExtractTpkFromArchive(tempZipPath, CacheDir, CachedTpkPath) == null) + var stagedTpkPath = ExtractTpkFromArchive( + tempZipPath, stagingDir, Path.Combine(stagingDir, "classdata.tpk")); + SafeDelete(tempZipPath); + + if (stagedTpkPath == null) { Debug.LogWarning("[ThunderKit] Downloaded archive does not contain a .tpk file"); - return false; + return TpkDownloadResult.Failed; } - if (File.Exists(tempZipPath)) - File.Delete(tempZipPath); + if (InspectTpk(stagedTpkPath, unityVersion) == TpkState.Unreadable) + return TpkDownloadResult.Incompatible; + PromoteToCache(stagedTpkPath); Debug.Log("[ThunderKit] Successfully downloaded updated classdata.tpk"); - return true; + return TpkDownloadResult.Downloaded; } catch (Exception e) { Debug.LogWarning($"[ThunderKit] Failed to download updated classdata.tpk: {e.Message}"); - return false; + return TpkDownloadResult.Failed; + } + finally + { + SafeDeleteDirectory(stagingDir); } } + // Copies alongside the cache and lands it with a same-directory move, so a + // failure part way through leaves the old tpk intact rather than truncated. + static void PromoteToCache(string stagedTpkPath) + { + var pendingPath = CachedTpkPath + ".new"; + File.Copy(stagedTpkPath, pendingPath, true); + + if (File.Exists(CachedTpkPath)) + File.Delete(CachedTpkPath); + + File.Move(pendingPath, CachedTpkPath); + } + internal static string ExtractTpkFromArchive(string archivePath, string destDir, string finalTpkPath) { using (var archive = ArchiveFactory.Open(archivePath)) @@ -307,6 +398,13 @@ static void WarnVersionNotCovered(string unityVersion) "such failures will be reported per-setting."); } + static void WarnTpkFormatUnsupported() + { + Debug.LogWarning("[ThunderKit] The downloaded class data (classdata.tpk) uses a container format newer " + + "than the AssetsTools.NET bundled with this version of ThunderKit. The download was discarded and any " + + "previously cached class data is kept. Update ThunderKit to pick up support for the new format."); + } + static void WriteAttemptMarker(DateTime nowUtc) { try @@ -338,5 +436,18 @@ static void SafeDelete(string path) Debug.LogWarning($"[ThunderKit] Failed to delete {path}: {e.Message}"); } } + + static void SafeDeleteDirectory(string path) + { + try + { + if (Directory.Exists(path)) + Directory.Delete(path, true); + } + catch (Exception e) + { + Debug.LogWarning($"[ThunderKit] Failed to delete {path}: {e.Message}"); + } + } } } diff --git a/Editor/ThirdParty/AssetsTools.NET/AssetsTools.NET.dll b/Editor/ThirdParty/AssetsTools.NET/AssetsTools.NET.dll index d8c11319..18919927 100644 Binary files a/Editor/ThirdParty/AssetsTools.NET/AssetsTools.NET.dll and b/Editor/ThirdParty/AssetsTools.NET/AssetsTools.NET.dll differ diff --git a/Editor/ThirdParty/AssetsTools.NET/AssetsTools.NET.xml b/Editor/ThirdParty/AssetsTools.NET/AssetsTools.NET.xml index 887b16f6..abcd79eb 100644 --- a/Editor/ThirdParty/AssetsTools.NET/AssetsTools.NET.xml +++ b/Editor/ThirdParty/AssetsTools.NET/AssetsTools.NET.xml @@ -60,7 +60,9 @@ - The reader used for locking. + The reader used for locking. This reader shouldn't be used for reading, but instead + will select the top-most reader to lock on so that consumers that use a different + reader but come from the same base stream will lock on the same object. @@ -196,6 +198,32 @@ Load all dependencies immediately? The loaded . + + + Cache template fields, including MonoBehaviours generated from class databases + + + + + Cache MonoBehaviour template fields from type trees and generated mono temp generators + + + + + Cache managed reference type template fields + + + + + Use a dictionary to look up asset infos rather than a simple sequential search + + + + + Original compression type. If this bundle is decompressed, you might + use this value to compress it back to its original compression type. + + List of loaded assets files for this bundle. @@ -398,6 +426,11 @@ Is the replacer non-null and does the replacer has a preview? + + + Is the file serialized? + + Sets the bytes used when the AssetBundleFile is written. @@ -725,6 +758,12 @@ Read from the editor version of this type if available? The new asset info, or null if the type can't be found in the type tree or class database. + + + Get the maximum size of this asset file info for a version. + + The version of the file. + File path of the pointer. If empty or null, FileId will be used. @@ -833,7 +872,7 @@ Get all assets of a specific type ID and script index. The script index of an asset can be - found from or . + found from or . The type ID to search for. The script index to search for. @@ -842,7 +881,7 @@ Get all assets of a specific type ID and script index. The script index of an asset can be - found from or . + found from or . The type ID to search for. The script index to search for. @@ -891,6 +930,11 @@ The writer to use. + + + Get the maximum size of this external. + + Size of the metadata block (not including this header). @@ -928,6 +972,11 @@ The writer to use. + + + Get the size of this header. + + Engine version this file uses. @@ -1035,10 +1084,10 @@ Get all assets of a specific type ID and script index. The script index of an asset can be - found from or . + found from or . The type ID to search for. - The script index to search for. + The script index to search for, or for any. A list of infos for that type ID and script index. @@ -1051,7 +1100,7 @@ Get all assets of a specific type ID and script index. The script index of an asset can be - found from or . + found from or . The type ID to search for. The script index to search for. @@ -1067,7 +1116,7 @@ Get the type tree type by type ID and script index. The script index of an asset can be - found from or . + found from or . For games before 5.5, is ignored since this data is read from the negative value of . In 5.5 and later, MonoBehaviours are always 0x72, so is used instead. @@ -1079,7 +1128,7 @@ Get the type tree type index by type ID and script index. The script index of an asset can be - found from or . + found from or . For games before 5.5, is ignored since this data is read from the negative value of . In 5.5 and later, MonoBehaviours are always 0x72, so is used instead. @@ -1091,7 +1140,7 @@ Get the type tree type by script index. The script index of an asset can be - found from or . + found from or . The script index to search for. The type tree type with this script index. @@ -1110,6 +1159,12 @@ The script index to search for. The type tree ref type with this script index. + + + Get the maximum size of this metadata. + + The version of the file. + Read the with the provided reader, used in @@ -1138,6 +1193,33 @@ The writer to use. + + + Nodes for this type. This list will be empty if the type is stripped. + + + + + String table bytes for this type. + + + + + Read the with the provided reader and format version. + must be set in versions < 23. In versions >= 32, + setting version to will read a version from the header. + should be used when reading external type trees. + + The reader to use. + The version of the file, or max int to auto-detect. + + + + Write the with the provided writer and format version. + + The writer to use. + The version of the file. + Version of the node. @@ -1221,6 +1303,12 @@ The node name. + + + Get the maximum size of this type tree node for a version. + + The version of the file. + Type tree node is an array. @@ -1270,14 +1358,19 @@ Hash of the type's fields. - + - Nodes for this type. This list will be empty if the type is stripped. + Hash for external type tree types. - + - String table bytes for this type. + Is the type blob a definition (includes nodes)? If not, it is an external reference (includes no nodes). + + + + + The type tree blob containing type nodes and the string buffer. @@ -1296,6 +1389,21 @@ Type reference information. Only used when IsRefType is true. + + + Nodes for this type. This list will be empty if the type is stripped. + + + + + String table bytes for this type. + + + + + 's string buffer, decoded to a single string. + + Read the with the provided reader and format version. @@ -1313,6 +1421,13 @@ The version of the file. Is type tree enabled for this file? + + + Get the maximum size of this type tree type. + + The version of the file. + Is type tree enabled for this file? + The string table used for commonly occuring strings in type trees. @@ -1361,9 +1476,16 @@ Read the template field from a type tree type. + This function will not do anything if there is no type information. The type tree type to read from. + + + Read the template field from a type tree blob. + + The type tree blob to read from. + Read the template field from a class database type. @@ -1398,12 +1520,38 @@ The ref type manager to use, if reading a MonoBehaviour using a ref type. The deserialized base field. + + + Deserialize a single primtive field and its children. + This method only works for strings, numbers, and ManagedReferencesRegistry. + + The reader to use. + The empty base value field to use. + The value type of the template field. + The ref type manager to use, if reading a MonoBehaviour using a ref type. + The deserialized base field. + + + + Deserialize a ManagedReferencesRegistry field. + + The reader to use. + The empty base value field to use. + The ref type manager to use, if reading a MonoBehaviour using a ref type. + The deserialized base field. + - Clone the field. + Perform a deep clone of the . The cloned field. + + + Perform a clone of the . + + The cloned value. + Template field corresponding to this value field. @@ -1419,6 +1567,11 @@ Children of this field. + + + Set to true if a field did not exist. + + The field which indicates that a field that was accessed does not exist. @@ -1444,12 +1597,41 @@ Write in big endian? + + + Perform a deep clone of the . + + The cloned field. + + + + Create an AssetTypeValueIterator, which allows walking an asset's field tree without + allocating unnecessary values. If is locked behind a lock, you + should manually take the lock while using the iterator. The current iterating position + is not remembered by this iterator. Instead, the reader's position is relied on. + + The template base field to use. + The reader to use, set the correct position. + The ref type manager to use, if reading a MonoBehaviour using a ref type. + + + + Create an AssetTypeValueIterator, which allows walking an asset's field tree without + allocating unnecessary values. If is locked behind a lock, you + should manually take the lock while using the iterator. The current iterating position + is not remembered by this iterator. Instead, the reader's position is relied on. + + The template base field to use. + The reader to use. + The position to start reading from. + The ref type manager to use, if reading a MonoBehaviour using a ref type. + Clear the ref type lookup dictionaries. - + Load the lookup from the type tree ref types of a serialized file. @@ -1661,27 +1843,40 @@ The version to get the type for. The type at that version. - + Read the with the provided reader. The reader to use. + The version of the file. + The tpk string table. Unused in version > 2. - + Write the with the provided writer. The writer to use. + The version of the file. + The tpk string table. Unused in version > 2. - + - Get the length of the common string for a version.
+ Skip past this class without fully parsing it. This is needed because + is needed to convert the v1 format + to the v2 format, but the string table only exists at the end of the file. +
+ The reader to use. + The version of the file. +
+ + + Get the length of the common string for a Unity version.
Since the common string is only appended in new versions, never edited, only the length of the string for each version needs to be stored rather than the string in its entirety.
- - The length of the common string. + The Unity version to make a string for. + The common string entries for the version.
@@ -1720,22 +1915,26 @@ The writer to use. - + Read the with the provided reader. The reader to use. + The version of the file. - + Write the with the provided writer. The writer to use. + The version of the file. - + Write the content with provided writer. + The writer to use. + Is this the final write? Good for checking when to close a stream. @@ -1755,6 +1954,13 @@ The replacer type such as modified or removed. + + + The maximum size this replacer can write to. + If the size is already known, return that size. Otherwise, + return a worst-case size for the replacer. + + Safe LZ4 codec. diff --git a/Tests/Editor/ClassDataManagerTests.cs b/Tests/Editor/ClassDataManagerTests.cs index cf6fe961..a7ab89fb 100644 --- a/Tests/Editor/ClassDataManagerTests.cs +++ b/Tests/Editor/ClassDataManagerTests.cs @@ -7,9 +7,10 @@ namespace ThunderKitTests { // Tier A — TPK acquisition policy. Pure, deterministic; no network or game data. - // Exercises the version-coverage decision, re-download throttle, version parsing, - // and marker parsing of ClassDataManager (visible via InternalsVisibleTo on - // ThunderKit.Core). The tpk is validated by whether it covers the running Unity + // Exercises the version-coverage decision, the unreadable-tpk guard, re-download + // throttle, version parsing, and marker parsing of ClassDataManager (visible via + // InternalsVisibleTo on ThunderKit.Core). The tpk is validated by whether the + // bundled AssetsTools.NET can read it and whether it covers the running Unity // version, never by age. [TestFixture] public class ClassDataManagerTests @@ -17,7 +18,7 @@ public class ClassDataManagerTests static readonly TimeSpan OneDay = TimeSpan.FromDays(1); static readonly DateTime Now = new DateTime(2024, 1, 10, 0, 0, 0, DateTimeKind.Utc); - static Func Fail(string name) => () => throw new AssertionException($"{name} should not have been invoked"); + static Func Fail(string name) => () => throw new AssertionException($"{name} should not have been invoked"); // --- PlanAcquisition: the full control-flow, effects injected --- @@ -25,10 +26,10 @@ public class ClassDataManagerTests public void Plan_CacheCoversVersion_UsesCacheWithoutDownloading() { var status = ClassDataManager.PlanAcquisition( - cacheSupports: true, + cacheState: ClassDataManager.TpkState.Covered, throttled: false, - tryDownload: Fail("tryDownload"), - cacheSupportsAfterDownload: Fail("cacheSupportsAfterDownload")); + tryDownload: Fail("tryDownload"), + cacheStateAfterDownload: Fail("cacheStateAfterDownload")); Assert.That(status, Is.EqualTo(ClassDataManager.ClassDataStatus.CacheSupported)); } @@ -39,10 +40,10 @@ public void Plan_StaleButCoveringCache_StillUsesCache() // Age is irrelevant: a cache that covers the version is always preferred, // even if a fresher tpk could be downloaded. var status = ClassDataManager.PlanAcquisition( - cacheSupports: true, + cacheState: ClassDataManager.TpkState.Covered, throttled: true, - tryDownload: Fail("tryDownload"), - cacheSupportsAfterDownload: Fail("cacheSupportsAfterDownload")); + tryDownload: Fail("tryDownload"), + cacheStateAfterDownload: Fail("cacheStateAfterDownload")); Assert.That(status, Is.EqualTo(ClassDataManager.ClassDataStatus.CacheSupported)); } @@ -50,35 +51,62 @@ public void Plan_StaleButCoveringCache_StillUsesCache() [Test] public void Plan_NoCoverage_Throttled_DoesNotDownload() { - var status = ClassDataManager.PlanAcquisition( - cacheSupports: false, - throttled: true, - tryDownload: Fail("tryDownload"), - cacheSupportsAfterDownload: Fail("cacheSupportsAfterDownload")); - - Assert.That(status, Is.EqualTo(ClassDataManager.ClassDataStatus.Throttled)); + // TpkState is internal, so the cases are enumerated in-body rather than via + // [TestCase] — an internal parameter type cannot appear on a public test. + var noCoverage = new[] + { + ClassDataManager.TpkState.Missing, + ClassDataManager.TpkState.Unreadable, + ClassDataManager.TpkState.Uncovered, + }; + + foreach (var cacheState in noCoverage) + { + var status = ClassDataManager.PlanAcquisition( + cacheState: cacheState, + throttled: true, + tryDownload: Fail("tryDownload"), + cacheStateAfterDownload: Fail("cacheStateAfterDownload")); + + Assert.That(status, Is.EqualTo(ClassDataManager.ClassDataStatus.Throttled), + $"cache state {cacheState} should not trigger a download while throttled."); + } } [Test] public void Plan_NoCoverage_DownloadFails_ReportsDownloadFailed() { var status = ClassDataManager.PlanAcquisition( - cacheSupports: false, + cacheState: ClassDataManager.TpkState.Missing, throttled: false, - tryDownload: () => false, - cacheSupportsAfterDownload: Fail("cacheSupportsAfterDownload")); + tryDownload: () => ClassDataManager.TpkDownloadResult.Failed, + cacheStateAfterDownload: Fail("cacheStateAfterDownload")); Assert.That(status, Is.EqualTo(ClassDataManager.ClassDataStatus.DownloadFailed)); } + [Test] + public void Plan_DownloadedTpkUnreadable_ReportsIncompatibleWithoutReinspectingCache() + { + // The download was discarded rather than promoted, so the cache is + // unchanged and must not be re-measured. + var status = ClassDataManager.PlanAcquisition( + cacheState: ClassDataManager.TpkState.Uncovered, + throttled: false, + tryDownload: () => ClassDataManager.TpkDownloadResult.Incompatible, + cacheStateAfterDownload: Fail("cacheStateAfterDownload")); + + Assert.That(status, Is.EqualTo(ClassDataManager.ClassDataStatus.DownloadIncompatible)); + } + [Test] public void Plan_NoCoverage_DownloadAddsSupport_UsesDownloaded() { var status = ClassDataManager.PlanAcquisition( - cacheSupports: false, + cacheState: ClassDataManager.TpkState.Uncovered, throttled: false, - tryDownload: () => true, - cacheSupportsAfterDownload: () => true); + tryDownload: () => ClassDataManager.TpkDownloadResult.Downloaded, + cacheStateAfterDownload: () => ClassDataManager.TpkState.Covered); Assert.That(status, Is.EqualTo(ClassDataManager.ClassDataStatus.DownloadedSupported)); } @@ -87,14 +115,49 @@ public void Plan_NoCoverage_DownloadAddsSupport_UsesDownloaded() public void Plan_NoCoverage_DownloadStillUnsupported_ReportsUnsupported() { var status = ClassDataManager.PlanAcquisition( - cacheSupports: false, + cacheState: ClassDataManager.TpkState.Missing, throttled: false, - tryDownload: () => true, - cacheSupportsAfterDownload: () => false); + tryDownload: () => ClassDataManager.TpkDownloadResult.Downloaded, + cacheStateAfterDownload: () => ClassDataManager.TpkState.Uncovered); Assert.That(status, Is.EqualTo(ClassDataManager.ClassDataStatus.UnsupportedAfterDownload)); } + // --- Resolving a tpk state to a usable path --- + + [Test] + public void Resolve_CoveredCache_UsedDirectly() + { + Assert.That(ClassDataManager.ResolveFromState(ClassDataManager.TpkState.Covered), + Is.EqualTo(ClassDataManager.ClassDataResolution.UseCache)); + } + + [Test] + public void Resolve_UncoveredCache_UsedWithWarning() + { + // Readable but missing this Unity version: still usable via closest-version + // selection, so it is offered with a warning rather than discarded. + Assert.That(ClassDataManager.ResolveFromState(ClassDataManager.TpkState.Uncovered), + Is.EqualTo(ClassDataManager.ClassDataResolution.UseCacheWithWarning)); + } + + [Test] + public void Resolve_UnusableCache_YieldsNoPath() + { + // Handing an unreadable tpk back would throw inside LoadClassPackage and + // fail the whole game import; there is no path worth returning. + var unusable = new[] + { + ClassDataManager.TpkState.Unreadable, + ClassDataManager.TpkState.Missing, + }; + + foreach (var state in unusable) + Assert.That(ClassDataManager.ResolveFromState(state), + Is.EqualTo(ClassDataManager.ClassDataResolution.None), + $"cache state {state} must not resolve to a usable path."); + } + // --- Re-download throttle --- [Test] diff --git a/Tests/Editor/ClassDataVersionCoverageTests.cs b/Tests/Editor/ClassDataVersionCoverageTests.cs index 0edc033f..e1e296ef 100644 --- a/Tests/Editor/ClassDataVersionCoverageTests.cs +++ b/Tests/Editor/ClassDataVersionCoverageTests.cs @@ -23,8 +23,9 @@ public void AcquiredTpk_CoversRunningUnityVersion() Assert.That(tpkPath, Is.Not.Null.And.Not.Empty, $"No classdata.tpk could be obtained for Unity {Application.unityVersion} (download failed or version unsupported)."); Assert.That(File.Exists(tpkPath), Is.True, $"Resolved tpk path does not exist: {tpkPath}"); - Assert.That(ClassDataManager.SupportsVersion(tpkPath, Application.unityVersion), Is.True, - $"Acquired classdata.tpk does not contain a class database for Unity {Application.unityVersion}."); + Assert.That(ClassDataManager.InspectTpk(tpkPath, Application.unityVersion), + Is.EqualTo(ClassDataManager.TpkState.Covered), + $"Acquired classdata.tpk is not readable by the bundled AssetsTools.NET, or contains no class database for Unity {Application.unityVersion}."); } } }