Skip to content

Commit 221edc7

Browse files
Enhance TPK Handling and Error Reporting
- Added error handling for unsupported class data formats in ImportProjectSettings.cs to prevent game import failures. - Introduced TpkState and TpkDownloadResult enums in ClassDataManager.cs to better manage TPK states and download results. - Refactored PlanAcquisition method to utilize TpkState for cache checks and download logic. - Implemented detailed logging for TPK format issues and download failures. - Updated tests in ClassDataManagerTests.cs to cover new TPK state handling and ensure proper behavior when TPKs are unreadable or incompatible. - Adjusted ClassDataVersionCoverageTests.cs to validate TPK readability and coverage more robustly. - Updated AssetsTools.NET.dll and AssetsTools.NET.xml to reflect changes in the underlying library.
1 parent 331a866 commit 221edc7

7 files changed

Lines changed: 516 additions & 104 deletions

File tree

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,28 @@
22

33
### Fixes
44

5+
* Updated the bundled `AssetsTools.NET` to read AssetRipper's version 2
6+
`classdata.tpk` container, fixing an `Unsupported or invalid file version 2`
7+
exception that failed game import outright
8+
* [ClassDataManager](Editor/Core/Utilities/ClassDataManager.cs) now stages each
9+
download and promotes it over the cache only once the bundled `AssetsTools.NET`
10+
proves it can read it, so a future format bump can no longer replace a working
11+
`classdata.tpk`
12+
* A tpk this build cannot parse is never returned from `GetClassDataPath`, and
13+
[ImportProjectSettings](Editor/Core/Config/Common/ImportProjectSettings.cs)
14+
raises `UnsupportedClassDataException` on an unreadable package — skipping the
15+
settings import instead of aborting the game import
16+
17+
### Tests
18+
19+
* [ClassDataManagerTests](Tests/Editor/ClassDataManagerTests.cs) now cover the
20+
unreadable-tpk guard — an incompatible download leaves the cache untouched, and no
21+
unusable tpk resolves to a usable path
22+
23+
## 9.4.4
24+
25+
### Fixes
26+
527
* [ImportConfiguration](Editor/Core/Config/ImportConfiguration.cs) no longer loses its
628
executor list when the Editor is closed and reopened — `ConfigurationExecutors` is now
729
saved after it is populated rather than before

Editor/Core/Config/Common/ImportProjectSettings.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,16 @@ internal List<string> ExportProjectSettings(string classDataPath, string globalG
127127
};
128128

129129
assetsManager = new AssetsManager();
130-
assetsManager.LoadClassPackage(classDataPath);
130+
// A tpk in a container format newer than the bundled AssetsTools.NET throws
131+
// here; skip the settings import rather than failing the whole game import.
132+
try
133+
{
134+
assetsManager.LoadClassPackage(classDataPath);
135+
}
136+
catch (Exception e)
137+
{
138+
throw new UnsupportedClassDataException($"class data at '{classDataPath}' could not be read ({e.Message}).");
139+
}
131140

132141
// Prefer the version that built the game, recorded in globalgamemanagers, over the Editor's.
133142
var globalGameManagersFile = assetsManager.LoadAssetsFile(globalGameManagersPath, true);

Editor/Core/Utilities/ClassDataManager.cs

Lines changed: 162 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,42 @@ internal static class ClassDataManager
2424
static readonly string MetadataPath = Path.Combine("Library", "ThunderKit", "classdata.tpk.json");
2525
static readonly TimeSpan RetryThrottle = TimeSpan.FromDays(1);
2626

27+
// How a tpk stands relative to the bundled AssetsTools.NET and a Unity version.
28+
internal enum TpkState
29+
{
30+
Missing,
31+
// Container format newer than the bundled AssetsTools.NET, or corrupt.
32+
// Unusable regardless of which Unity versions it covers.
33+
Unreadable,
34+
Uncovered,
35+
Covered,
36+
}
37+
38+
internal enum TpkDownloadResult
39+
{
40+
Failed,
41+
// Fetched, but unreadable here. Discarded instead of promoted.
42+
Incompatible,
43+
Downloaded,
44+
}
45+
2746
internal enum ClassDataStatus
2847
{
2948
CacheSupported,
3049
DownloadedSupported,
3150
Throttled,
3251
UnsupportedAfterDownload,
52+
DownloadIncompatible,
3353
DownloadFailed,
3454
}
3555

56+
internal enum ClassDataResolution
57+
{
58+
UseCache,
59+
UseCacheWithWarning,
60+
None,
61+
}
62+
3663
[Serializable]
3764
class TpkMetadata
3865
{
@@ -42,14 +69,13 @@ class TpkMetadata
4269
public static string GetClassDataPath()
4370
{
4471
var unityVersion = Application.unityVersion;
45-
var cacheSupports = SupportsVersion(CachedTpkPath, unityVersion);
46-
var throttled = IsThrottledNow(DateTime.UtcNow);
72+
var cacheState = InspectTpk(CachedTpkPath, unityVersion);
4773

4874
var status = PlanAcquisition(
49-
cacheSupports,
50-
throttled,
51-
tryDownload: TryDownloadTpk,
52-
cacheSupportsAfterDownload: () => SupportsVersion(CachedTpkPath, unityVersion));
75+
cacheState,
76+
IsThrottledNow(DateTime.UtcNow),
77+
tryDownload: () => TryDownloadTpk(unityVersion),
78+
cacheStateAfterDownload: () => InspectTpk(CachedTpkPath, unityVersion));
5379

5480
switch (status)
5581
{
@@ -58,75 +84,112 @@ public static string GetClassDataPath()
5884
ClearAttemptMarker();
5985
return CachedTpkPath;
6086

87+
case ClassDataStatus.DownloadIncompatible:
88+
WarnTpkFormatUnsupported();
89+
WriteAttemptMarker(DateTime.UtcNow);
90+
return PathFor(cacheState, unityVersion);
91+
6192
case ClassDataStatus.UnsupportedAfterDownload:
93+
WriteAttemptMarker(DateTime.UtcNow);
94+
return PathFor(InspectTpk(CachedTpkPath, unityVersion), unityVersion);
95+
6296
case ClassDataStatus.DownloadFailed:
6397
WriteAttemptMarker(DateTime.UtcNow);
64-
return BestAvailableOrNull(unityVersion);
98+
return PathFor(cacheState, unityVersion);
6599

66100
case ClassDataStatus.Throttled:
67-
return BestAvailableOrNull(unityVersion);
101+
return PathFor(cacheState, unityVersion);
68102

69103
default:
70104
return null;
71105
}
72106
}
73107

74-
// Returns the cached tpk (with a warning that it may not fully cover the running
75-
// Unity version) when one exists, or null with an error when none is available.
76-
static string BestAvailableOrNull(string unityVersion)
108+
internal static ClassDataStatus PlanAcquisition(TpkState cacheState, bool throttled,
109+
Func<TpkDownloadResult> tryDownload, Func<TpkState> cacheStateAfterDownload)
77110
{
78-
if (File.Exists(CachedTpkPath))
111+
if (cacheState == TpkState.Covered)
112+
return ClassDataStatus.CacheSupported;
113+
114+
if (throttled)
115+
return ClassDataStatus.Throttled;
116+
117+
switch (tryDownload())
79118
{
80-
WarnVersionNotCovered(unityVersion);
81-
return CachedTpkPath;
119+
case TpkDownloadResult.Failed:
120+
return ClassDataStatus.DownloadFailed;
121+
122+
case TpkDownloadResult.Incompatible:
123+
return ClassDataStatus.DownloadIncompatible;
82124
}
83125

84-
Debug.LogError($"[ThunderKit] No classdata.tpk is available and one could not be downloaded for Unity {unityVersion}. ProjectSettings import will be skipped.");
85-
return null;
126+
return cacheStateAfterDownload() == TpkState.Covered
127+
? ClassDataStatus.DownloadedSupported
128+
: ClassDataStatus.UnsupportedAfterDownload;
86129
}
87130

88-
internal static ClassDataStatus PlanAcquisition(bool cacheSupports, bool throttled,
89-
Func<bool> tryDownload, Func<bool> cacheSupportsAfterDownload)
131+
// An uncovered tpk is still worth using — SelectBestVersion falls back to the
132+
// closest type data — but an unreadable one can only throw at load time.
133+
internal static ClassDataResolution ResolveFromState(TpkState state)
90134
{
91-
if (cacheSupports)
92-
return ClassDataStatus.CacheSupported;
135+
switch (state)
136+
{
137+
case TpkState.Covered:
138+
return ClassDataResolution.UseCache;
93139

94-
if (throttled)
95-
return ClassDataStatus.Throttled;
140+
case TpkState.Uncovered:
141+
return ClassDataResolution.UseCacheWithWarning;
96142

97-
if (!tryDownload())
98-
return ClassDataStatus.DownloadFailed;
143+
default:
144+
return ClassDataResolution.None;
145+
}
146+
}
99147

100-
return cacheSupportsAfterDownload()
101-
? ClassDataStatus.DownloadedSupported
102-
: ClassDataStatus.UnsupportedAfterDownload;
148+
static string PathFor(TpkState state, string unityVersion)
149+
{
150+
switch (ResolveFromState(state))
151+
{
152+
case ClassDataResolution.UseCache:
153+
return CachedTpkPath;
154+
155+
case ClassDataResolution.UseCacheWithWarning:
156+
WarnVersionNotCovered(unityVersion);
157+
return CachedTpkPath;
158+
159+
default:
160+
Debug.LogError($"[ThunderKit] No usable class data (classdata.tpk) is available for Unity {unityVersion}. ProjectSettings import will be skipped.");
161+
return null;
162+
}
103163
}
104164

105-
internal static bool SupportsVersion(string tpkPath, string unityVersion)
165+
internal static TpkState InspectTpk(string tpkPath, string unityVersion)
106166
{
107167
if (!File.Exists(tpkPath))
108-
return false;
109-
// Coverage is decided on major.minor only. Type trees rarely change in a
110-
// patch release, and our use (ProjectSettings) touches a small, stable set
111-
// of types, so requiring an exact patch match would reject usable tpks.
112-
if (!TryParseUnityVersion(unityVersion, out var major, out var minor, out _))
113-
return false;
168+
return TpkState.Missing;
114169

170+
List<UnityVersion> versions;
115171
try
116172
{
117-
var manager = new AssetsManager();
118-
var package = manager.LoadClassPackage(tpkPath);
119-
var versions = package?.TpkTypeTree?.Versions;
120-
if (versions == null)
121-
return false;
122-
123-
return versions.Any(v => v.major == major && v.minor == minor);
173+
versions = new AssetsManager().LoadClassPackage(tpkPath)?.TpkTypeTree?.Versions;
124174
}
125175
catch (Exception e)
126176
{
127-
Debug.LogWarning($"[ThunderKit] Failed to inspect classdata.tpk versions: {e.Message}");
128-
return false;
177+
Debug.LogWarning($"[ThunderKit] Class data at {tpkPath} could not be read by the AssetsTools.NET bundled with this version of ThunderKit: {e.Message}");
178+
return TpkState.Unreadable;
129179
}
180+
181+
if (versions == null)
182+
return TpkState.Unreadable;
183+
184+
// Coverage is decided on major.minor only. Type trees rarely change in a
185+
// patch release, and our use (ProjectSettings) touches a small, stable set
186+
// of types, so requiring an exact patch match would reject usable tpks.
187+
if (!TryParseUnityVersion(unityVersion, out var major, out var minor, out _))
188+
return TpkState.Uncovered;
189+
190+
return versions.Any(v => v.major == major && v.minor == minor)
191+
? TpkState.Covered
192+
: TpkState.Uncovered;
130193
}
131194

132195
// 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
233296
}
234297
}
235298

236-
static bool TryDownloadTpk()
299+
// Downloads into a staging directory and promotes over the cache only after the
300+
// bundled AssetsTools.NET proves it can read the result, so a tpk published in a
301+
// newer container format cannot destroy a cache that still works.
302+
static TpkDownloadResult TryDownloadTpk(string unityVersion)
237303
{
304+
var stagingDir = Path.Combine(Constants.TempDir, "classdata_staging");
238305
try
239306
{
240-
Debug.LogWarning("[ThunderKit] Downloading tpk archive");
307+
Debug.Log("[ThunderKit] Downloading tpk archive");
241308
Directory.CreateDirectory(CacheDir);
242309
Directory.CreateDirectory(Constants.TempDir);
310+
SafeDeleteDirectory(stagingDir);
311+
Directory.CreateDirectory(stagingDir);
243312

244313
var tempZipPath = Path.Combine(Constants.TempDir, "classdata_download.zip");
245314

@@ -248,25 +317,47 @@ static bool TryDownloadTpk()
248317
client.DownloadFile(TpkDownloadUrl, tempZipPath);
249318
}
250319

251-
if (ExtractTpkFromArchive(tempZipPath, CacheDir, CachedTpkPath) == null)
320+
var stagedTpkPath = ExtractTpkFromArchive(
321+
tempZipPath, stagingDir, Path.Combine(stagingDir, "classdata.tpk"));
322+
SafeDelete(tempZipPath);
323+
324+
if (stagedTpkPath == null)
252325
{
253326
Debug.LogWarning("[ThunderKit] Downloaded archive does not contain a .tpk file");
254-
return false;
327+
return TpkDownloadResult.Failed;
255328
}
256329

257-
if (File.Exists(tempZipPath))
258-
File.Delete(tempZipPath);
330+
if (InspectTpk(stagedTpkPath, unityVersion) == TpkState.Unreadable)
331+
return TpkDownloadResult.Incompatible;
259332

333+
PromoteToCache(stagedTpkPath);
260334
Debug.Log("[ThunderKit] Successfully downloaded updated classdata.tpk");
261-
return true;
335+
return TpkDownloadResult.Downloaded;
262336
}
263337
catch (Exception e)
264338
{
265339
Debug.LogWarning($"[ThunderKit] Failed to download updated classdata.tpk: {e.Message}");
266-
return false;
340+
return TpkDownloadResult.Failed;
341+
}
342+
finally
343+
{
344+
SafeDeleteDirectory(stagingDir);
267345
}
268346
}
269347

348+
// Copies alongside the cache and lands it with a same-directory move, so a
349+
// failure part way through leaves the old tpk intact rather than truncated.
350+
static void PromoteToCache(string stagedTpkPath)
351+
{
352+
var pendingPath = CachedTpkPath + ".new";
353+
File.Copy(stagedTpkPath, pendingPath, true);
354+
355+
if (File.Exists(CachedTpkPath))
356+
File.Delete(CachedTpkPath);
357+
358+
File.Move(pendingPath, CachedTpkPath);
359+
}
360+
270361
internal static string ExtractTpkFromArchive(string archivePath, string destDir, string finalTpkPath)
271362
{
272363
using (var archive = ArchiveFactory.Open(archivePath))
@@ -307,6 +398,13 @@ static void WarnVersionNotCovered(string unityVersion)
307398
"such failures will be reported per-setting.");
308399
}
309400

401+
static void WarnTpkFormatUnsupported()
402+
{
403+
Debug.LogWarning("[ThunderKit] The downloaded class data (classdata.tpk) uses a container format newer " +
404+
"than the AssetsTools.NET bundled with this version of ThunderKit. The download was discarded and any " +
405+
"previously cached class data is kept. Update ThunderKit to pick up support for the new format.");
406+
}
407+
310408
static void WriteAttemptMarker(DateTime nowUtc)
311409
{
312410
try
@@ -338,5 +436,18 @@ static void SafeDelete(string path)
338436
Debug.LogWarning($"[ThunderKit] Failed to delete {path}: {e.Message}");
339437
}
340438
}
439+
440+
static void SafeDeleteDirectory(string path)
441+
{
442+
try
443+
{
444+
if (Directory.Exists(path))
445+
Directory.Delete(path, true);
446+
}
447+
catch (Exception e)
448+
{
449+
Debug.LogWarning($"[ThunderKit] Failed to delete {path}: {e.Message}");
450+
}
451+
}
341452
}
342453
}
15 KB
Binary file not shown.

0 commit comments

Comments
 (0)