Skip to content

Commit 945db4e

Browse files
Merge pull request #120 from PassivePicasso/tpk-improvements
Tpk improvements
2 parents ab66b05 + e35b4f0 commit 945db4e

18 files changed

Lines changed: 977 additions & 369 deletions

CHANGELOG.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,45 @@
1+
## 9.4.0
2+
3+
### New Features
4+
5+
* Added an **Installed Unity Games** window (`Tools/ThunderKit/Installed Unity
6+
Games`)
7+
* Scans installed Steam libraries for Unity games and reads the Unity
8+
version each was built with
9+
* Highlights games whose `major.minor.patch` matches the running Editor and
10+
flags games older than Unity 2018.4 as unsupported
11+
12+
* Class data (`classdata.tpk`) resolution is now driven by version coverage
13+
instead of a fixed cache age
14+
* `ClassDataManager` validates whether the cached tpk actually contains a
15+
class database for the current Unity version; a tpk that covers the
16+
version is reused indefinitely and never re-downloaded on age alone
17+
* When the current version is not covered, it attempts a download and
18+
re-checks coverage; if the freshly downloaded tpk still lacks the
19+
version, it deletes the local copy, records the attempt date to throttle
20+
further downloads, and reports that no published tpk yet supports this
21+
Unity version
22+
* The class data is now fetched on demand from the AssetRipper Tpk release
23+
rather than shipped with the package; the bundled `classdata.tpk` (~1.3 MB)
24+
and its license file were removed
25+
* Removed the dead `Constants.BundledClassDataPath` / `Constants.ClassDataPath`
26+
fallbacks, which pointed at the removed bundled `classdata.tpk`;
27+
`ImportProjectSettings` now skips gracefully (with a clear error) when no
28+
class data is available rather than failing on a missing file
29+
30+
### Tests
31+
32+
* Added EditMode unit coverage for the class data acquisition policy
33+
([ClassDataManagerTests](Tests/Editor/ClassDataManagerTests.cs)): the
34+
`PlanAcquisition` coverage/throttle/download decision branches, Unity-version
35+
parsing, the re-download throttle, and attempt-marker parsing — all pure and
36+
offline
37+
* Added an opt-in (`[Explicit]`) integration test
38+
([ClassDataVersionCoverageTests](Tests/Editor/ClassDataVersionCoverageTests.cs))
39+
that downloads the tpk and verifies it covers the Unity version under test
40+
* Added `[assembly: InternalsVisibleTo("ThunderKit.Core.Tests")]` to
41+
`ThunderKit.Core`, exposing internal seams to the test assembly
42+
143
## 9.3.3
244

345
### Fixes

Editor/Common/Constants.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@ public static class Constants
2626
public const string PackageSourceSettingsTemplatePath = SettingsTemplatesPath + "/PackageSourceSettings.uxml";
2727
public const string ThunderKitSettingsTemplatePath = SettingsTemplatesPath + "/ThunderKitSettings.uxml";
2828

29-
public static readonly string ClassDataPath = Path.Combine("Packages", "com.passivepicasso.thunderkit", "Editor", "ThirdParty", "AssetsTools.NET", "classdata.tpk");
30-
3129
public static class Priority
3230
{
3331
public const int AssemblyImport = 3_000_000;

Editor/Core/AssemblyInfo.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1-
using ThunderKit.Core.Config;
1+
using System.Runtime.CompilerServices;
2+
using ThunderKit.Core.Config;
23

3-
[assembly: ImportExtensions]
4+
[assembly: ImportExtensions]
5+
[assembly: InternalsVisibleTo("ThunderKit.Core.Tests")]

Editor/Core/Config/Common/ImportProjectSettings.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.IO;
1010
using ThunderKit.Common;
1111
using ThunderKit.Core.Data;
12+
using ThunderKit.Core.Utilities;
1213
using UnityEditor;
1314
using UnityEngine;
1415

@@ -52,7 +53,12 @@ public override bool Execute()
5253
if (IncludedSettings == 0) return true;
5354

5455
var settings = ThunderKitSetting.GetOrCreateSettings<ThunderKitSettings>();
55-
var classDataPath = Path.GetFullPath(Path.Combine(Constants.ThunderKitRoot, "Editor", "ThirdParty", "AssetsTools.NET", "classdata.tpk"));
56+
var classDataPath = ClassDataManager.GetClassDataPath();
57+
if (string.IsNullOrEmpty(classDataPath) || !File.Exists(classDataPath))
58+
{
59+
Debug.LogError("[ThunderKit] Skipping ProjectSettings import: no class data (classdata.tpk) is available for this Unity version.");
60+
return true;
61+
}
5662

5763
var unityVersion = Application.unityVersion;
5864
var editorDirectory = Path.GetDirectoryName(EditorApplication.applicationPath);
Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
using AssetsTools.NET.Extra;
2+
using SharpCompress.Archives;
3+
using SharpCompress.Readers;
4+
using System;
5+
using System.Globalization;
6+
using System.IO;
7+
using System.Linq;
8+
using System.Net;
9+
using ThunderKit.Common;
10+
using UnityEngine;
11+
12+
namespace ThunderKit.Core.Utilities
13+
{
14+
internal static class ClassDataManager
15+
{
16+
const string TpkDownloadUrl =
17+
"https://nightly.link/AssetRipper/Tpk/workflows/type_tree_tpk/master/uncompressed_file.zip";
18+
19+
static readonly string CacheDir = Path.Combine("Library", "ThunderKit");
20+
static readonly string CachedTpkPath = Path.Combine("Library", "ThunderKit", "classdata.tpk");
21+
// Marker recording the last time a download attempt failed to yield support
22+
// for the current Unity version. Throttles re-downloads (see RetryThrottle).
23+
static readonly string MetadataPath = Path.Combine("Library", "ThunderKit", "classdata.tpk.json");
24+
static readonly TimeSpan RetryThrottle = TimeSpan.FromDays(1);
25+
26+
internal enum ClassDataStatus
27+
{
28+
CacheSupported,
29+
DownloadedSupported,
30+
Throttled,
31+
UnsupportedAfterDownload,
32+
DownloadFailed,
33+
}
34+
35+
[Serializable]
36+
class TpkMetadata
37+
{
38+
public string lastAttemptUtc;
39+
}
40+
41+
public static string GetClassDataPath()
42+
{
43+
var unityVersion = Application.unityVersion;
44+
var cacheSupports = SupportsVersion(CachedTpkPath, unityVersion);
45+
var throttled = IsThrottledNow(DateTime.UtcNow);
46+
47+
var status = PlanAcquisition(
48+
cacheSupports,
49+
throttled,
50+
tryDownload: TryDownloadTpk,
51+
cacheSupportsAfterDownload: () => SupportsVersion(CachedTpkPath, unityVersion));
52+
53+
switch (status)
54+
{
55+
case ClassDataStatus.CacheSupported:
56+
case ClassDataStatus.DownloadedSupported:
57+
ClearAttemptMarker();
58+
return CachedTpkPath;
59+
60+
case ClassDataStatus.UnsupportedAfterDownload:
61+
SafeDelete(CachedTpkPath);
62+
WriteAttemptMarker(DateTime.UtcNow);
63+
ReportUnsupported(unityVersion);
64+
return null;
65+
66+
case ClassDataStatus.Throttled:
67+
ReportUnsupported(unityVersion);
68+
return null;
69+
70+
case ClassDataStatus.DownloadFailed:
71+
WriteAttemptMarker(DateTime.UtcNow);
72+
Debug.LogError($"[ThunderKit] Could not download class data and no cached classdata.tpk supports Unity {unityVersion}.");
73+
return null;
74+
75+
default:
76+
return null;
77+
}
78+
}
79+
80+
internal static ClassDataStatus PlanAcquisition(bool cacheSupports, bool throttled,
81+
Func<bool> tryDownload, Func<bool> cacheSupportsAfterDownload)
82+
{
83+
if (cacheSupports)
84+
return ClassDataStatus.CacheSupported;
85+
86+
if (throttled)
87+
return ClassDataStatus.Throttled;
88+
89+
if (!tryDownload())
90+
return ClassDataStatus.DownloadFailed;
91+
92+
return cacheSupportsAfterDownload()
93+
? ClassDataStatus.DownloadedSupported
94+
: ClassDataStatus.UnsupportedAfterDownload;
95+
}
96+
97+
internal static bool SupportsVersion(string tpkPath, string unityVersion)
98+
{
99+
if (!File.Exists(tpkPath))
100+
return false;
101+
if (!TryParseUnityVersion(unityVersion, out var major, out var minor, out var patch))
102+
return false;
103+
104+
try
105+
{
106+
var manager = new AssetsManager();
107+
var package = manager.LoadClassPackage(tpkPath);
108+
var versions = package?.TpkTypeTree?.Versions;
109+
if (versions == null)
110+
return false;
111+
112+
return versions.Any(v => v.major == major && v.minor == minor && v.patch == patch);
113+
}
114+
catch (Exception e)
115+
{
116+
Debug.LogWarning($"[ThunderKit] Failed to inspect classdata.tpk versions: {e.Message}");
117+
return false;
118+
}
119+
}
120+
121+
internal static bool TryParseUnityVersion(string unityVersion, out int major, out int minor, out int patch)
122+
{
123+
major = minor = patch = 0;
124+
if (string.IsNullOrEmpty(unityVersion))
125+
return false;
126+
127+
var parts = unityVersion.Split('.');
128+
if (parts.Length < 3)
129+
return false;
130+
131+
if (!int.TryParse(parts[0], out major))
132+
return false;
133+
if (!int.TryParse(parts[1], out minor))
134+
return false;
135+
136+
var patchDigits = new string(parts[2].TakeWhile(char.IsDigit).ToArray());
137+
return int.TryParse(patchDigits, out patch);
138+
}
139+
140+
static bool IsThrottledNow(DateTime nowUtc)
141+
{
142+
if (!File.Exists(MetadataPath))
143+
return false;
144+
145+
try
146+
{
147+
if (!TryReadLastAttemptUtc(File.ReadAllText(MetadataPath), out var lastAttemptUtc))
148+
return false;
149+
150+
return IsThrottled(lastAttemptUtc, nowUtc, RetryThrottle);
151+
}
152+
catch
153+
{
154+
return false;
155+
}
156+
}
157+
158+
internal static bool IsThrottled(DateTime lastAttemptUtc, DateTime nowUtc, TimeSpan window)
159+
{
160+
return (nowUtc - lastAttemptUtc) < window;
161+
}
162+
163+
internal static bool TryReadLastAttemptUtc(string json, out DateTime lastAttemptUtc)
164+
{
165+
lastAttemptUtc = default;
166+
try
167+
{
168+
var metadata = JsonUtility.FromJson<TpkMetadata>(json);
169+
if (metadata == null || string.IsNullOrEmpty(metadata.lastAttemptUtc))
170+
return false;
171+
172+
lastAttemptUtc = DateTime.Parse(metadata.lastAttemptUtc, CultureInfo.InvariantCulture,
173+
DateTimeStyles.RoundtripKind).ToUniversalTime();
174+
return true;
175+
}
176+
catch
177+
{
178+
return false;
179+
}
180+
}
181+
182+
static bool TryDownloadTpk()
183+
{
184+
try
185+
{
186+
Debug.LogWarning("[ThunderKit] Downloading tpk archive");
187+
Directory.CreateDirectory(CacheDir);
188+
Directory.CreateDirectory(Constants.TempDir);
189+
190+
var tempZipPath = Path.Combine(Constants.TempDir, "classdata_download.zip");
191+
192+
using (var client = new WebClient())
193+
{
194+
client.DownloadFile(TpkDownloadUrl, tempZipPath);
195+
}
196+
197+
if (ExtractTpkFromArchive(tempZipPath, CacheDir, CachedTpkPath) == null)
198+
{
199+
Debug.LogWarning("[ThunderKit] Downloaded archive does not contain a .tpk file");
200+
return false;
201+
}
202+
203+
if (File.Exists(tempZipPath))
204+
File.Delete(tempZipPath);
205+
206+
Debug.Log("[ThunderKit] Successfully downloaded updated classdata.tpk");
207+
return true;
208+
}
209+
catch (Exception e)
210+
{
211+
Debug.LogWarning($"[ThunderKit] Failed to download updated classdata.tpk: {e.Message}");
212+
return false;
213+
}
214+
}
215+
216+
internal static string ExtractTpkFromArchive(string archivePath, string destDir, string finalTpkPath)
217+
{
218+
using (var archive = ArchiveFactory.Open(archivePath))
219+
{
220+
var tpkEntry = archive.Entries
221+
.FirstOrDefault(e => !e.IsDirectory && e.Key.EndsWith(".tpk", StringComparison.OrdinalIgnoreCase));
222+
223+
if (tpkEntry == null)
224+
return null;
225+
226+
tpkEntry.WriteToDirectory(destDir, new ExtractionOptions
227+
{
228+
ExtractFullPath = false,
229+
Overwrite = true
230+
});
231+
232+
// The extracted file may have a different name (e.g. "uncompressed.tpk")
233+
var extractedName = Path.GetFileName(tpkEntry.Key);
234+
var extractedPath = Path.Combine(destDir, extractedName);
235+
var finalName = Path.GetFileName(finalTpkPath);
236+
if (!string.Equals(extractedName, finalName, StringComparison.OrdinalIgnoreCase)
237+
&& File.Exists(extractedPath))
238+
{
239+
if (File.Exists(finalTpkPath))
240+
File.Delete(finalTpkPath);
241+
File.Move(extractedPath, finalTpkPath);
242+
}
243+
244+
return finalTpkPath;
245+
}
246+
}
247+
248+
static void ReportUnsupported(string unityVersion)
249+
{
250+
Debug.LogError($"[ThunderKit] The available class data (tpk) does not yet support the current version of Unity ({unityVersion}). " +
251+
"ProjectSettings import that relies on class data will be unavailable until AssetRipper publishes type data for this version.");
252+
}
253+
254+
static void WriteAttemptMarker(DateTime nowUtc)
255+
{
256+
try
257+
{
258+
Directory.CreateDirectory(CacheDir);
259+
var metadata = new TpkMetadata { lastAttemptUtc = nowUtc.ToString("o") };
260+
File.WriteAllText(MetadataPath, JsonUtility.ToJson(metadata));
261+
}
262+
catch (Exception e)
263+
{
264+
Debug.LogWarning($"[ThunderKit] Failed to write class data attempt marker: {e.Message}");
265+
}
266+
}
267+
268+
static void ClearAttemptMarker()
269+
{
270+
SafeDelete(MetadataPath);
271+
}
272+
273+
static void SafeDelete(string path)
274+
{
275+
try
276+
{
277+
if (File.Exists(path))
278+
File.Delete(path);
279+
}
280+
catch (Exception e)
281+
{
282+
Debug.LogWarning($"[ThunderKit] Failed to delete {path}: {e.Message}");
283+
}
284+
}
285+
}
286+
}

Editor/Core/Utilities/ClassDataManager.cs.meta

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)