@@ -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}
0 commit comments