WKLib 0.2.3
A modding library for White Knuckle
Loading...
Searching...
No Matches
AssetService.cs
Go to the documentation of this file.
1using System;
2using System.Collections.Generic;
3using System.IO;
4using System.Linq;
5using System.Reflection;
6using System.Threading;
7using System.Threading.Tasks;
8using BepInEx;
9using BepInEx.Bootstrap;
10using UnityEngine;
11using UnityEngine.UIElements;
12using WKLib.API;
13using WKLib.Utilities;
14using Object = UnityEngine.Object;
15
17
21public class AssetService
22{
23 private readonly WKLibAPI _wkLibAPI;
24 public readonly string AssemblyFolder;
25 private readonly Dictionary<string, AssetBundle> _bundleCache = new();
26 private readonly Dictionary<string, Dictionary<string, M_Level>> _loadedLevelsCache = new();
27 private readonly Dictionary<string, M_Gamemode> _gamemodeCache = new();
28
29 private readonly CancellationTokenSource _sQuitCts = new();
30
32 {
33 _wkLibAPI = API ?? throw new ArgumentNullException(nameof(API));
34 if (!Chainloader.PluginInfos.TryGetValue(API.GUID, out var pluginInfo))
35 throw new Exception($"WKLibAPI GUID Mistmatch with PluginInfo, {API.DisplayName}, {API.GUID}");
36
37 var assemblyPath = pluginInfo.Location;
38 AssemblyFolder = Path.GetDirectoryName(assemblyPath) ?? string.Empty;
39
40 Application.quitting += () =>
41 {
42 WKLog.Debug("[AssetService] Quitting...");
43 _sQuitCts.Cancel();
44 };
45 }
46
47 #region Loading Asset Bundles
48
54 public async Task<AssetBundle> LoadBundleRelativeAsync(
55 string relativePath,
56 IProgress<float> progress = null)
57 {
58 progress?.Report(0f);
59 if (string.IsNullOrEmpty(relativePath))
60 {
61 WKLog.Error("[AssetService] Path is null or empty.");
62 progress?.Report(1f);
63 return null;
64 }
65
66 var cacheKey = $"{_wkLibAPI.DisplayName}:{Path.GetFileNameWithoutExtension(relativePath)}";
67 if (_bundleCache.TryGetValue(cacheKey, out var cached) && cached is not null)
68 {
69 WKLog.Debug($"[AssetService] Returning cached bundle: {cacheKey}");
70 progress?.Report(1f);
71 return cached;
72 }
73
74 var fullPath = Path.Combine(AssemblyFolder, relativePath);
75 if (!File.Exists(fullPath))
76 {
77 WKLog.Error($"[AssetService] Bundle not found at {fullPath}");
78 progress?.Report(1f);
79 return null;
80 }
81
82 var request = AssetBundle.LoadFromFileAsync(fullPath);
83 if (request is null)
84 {
85 WKLog.Error($"[AssetService] Failed to load bundle: {fullPath} (request)");
86 progress?.Report(1f);
87 return null;
88 }
89
90 try
91 {
92 while (!request.isDone)
93 {
94 _sQuitCts.Token.ThrowIfCancellationRequested();
95
96 progress?.Report(request.progress * 1.1f);
97 await Task.Yield();
98 }
99 }
100 catch (OperationCanceledException)
101 {
102 WKLog.Debug($"[AssetService] Asset Bundle Load cancelled");
103 return null;
104 }
105
106 var bundle = request.assetBundle;
107 if (bundle is null)
108 {
109 WKLog.Error($"[AssetService] Failed to load bundle at {fullPath} (bundle)");
110 progress?.Report(1f);
111 return null;
112 }
113
114 _bundleCache[cacheKey] = bundle;
115 WKLog.Debug($"[AssetService] Loaded & cached bundle: {cacheKey}");
116 progress?.Report(1f);
117 return bundle;
118 }
119
120 #endregion
121
122 #region Unloading Asset Bundles
123
127 public void UnloadBundleRelative(string relativePath, bool unloadAllLoadedObjects = false)
128 {
129 var cacheKey = $"{_wkLibAPI.DisplayName}:{relativePath}";
130 if (_bundleCache.TryGetValue(cacheKey, out var bundle) && bundle is not null)
131 {
132 try
133 {
134 bundle.Unload(unloadAllLoadedObjects);
135 }
136 catch
137 {
138 WKLog.Error($"[AssetService] Failed to unload bundle: {cacheKey}");
139 return;
140 }
141 WKLog.Debug($"[AssetService] Unloaded bundle: {cacheKey}");
142 _bundleCache.Remove(cacheKey);
143 }
144 }
145
149 public void UnloadAllBundles(bool unloadAllLoadedObjects = false)
150 {
151 var keys = _bundleCache.Keys.Where(k => k.StartsWith(_wkLibAPI.DisplayName + ":")).ToList();
152 foreach (var key in keys)
153 {
154 if (_bundleCache[key] is not null)
155 {
156 try
157 {
158 _bundleCache[key].Unload(unloadAllLoadedObjects);
159 }
160 catch
161 {
162 WKLog.Error($"[AssetService] Failed to unload bundle: {key}");
163 continue;
164 }
165 WKLog.Debug($"[AssetService] Unloaded bundle: {key}");
166 }
167 _bundleCache.Remove(key);
168 }
169 }
170
171 #endregion
172
173 #region Loading Levels
174
182 public async Task<Dictionary<string, M_Level>> LoadAllLevelsFromBundle(
183 AssetBundle bundle,
184 IProgress<float> progress = null)
185 {
186 var foundLevels = new Dictionary<string, M_Level>();
187
188 if (bundle is null)
189 {
190 WKLog.Error("[AssetService] LoadAllLevelsFromBundle called with null bundle.");
191 progress?.Report(1f);
192 return foundLevels;
193 }
194
195 var cacheKey = $"{_wkLibAPI.DisplayName}:{bundle.name}";
196
197 if (_loadedLevelsCache.ContainsKey(cacheKey))
198 {
199 WKLog.Debug($"[AssetService] Returning Cached loaded levels for bundle: {cacheKey}");
200 progress?.Report(1f);
201 return _loadedLevelsCache.GetValueOrDefault(cacheKey);
202 }
203
204 var request = bundle.LoadAllAssetsAsync<GameObject>();
205 try
206 {
207 while (!request.isDone)
208 {
209 _sQuitCts.Token.ThrowIfCancellationRequested();
210
211 progress?.Report(request.progress * 0.5f);
212 await Task.Yield();
213 }
214 }
215 catch (OperationCanceledException)
216 {
217 WKLog.Debug($"[AssetService] LoadAllLevelsFromBundle canceled.");
218 return null;
219 }
220
221
222 var currentDatabase = CL_AssetManager.GetDatabase(_wkLibAPI.DisplayName);
223 if (currentDatabase == null)
224 {
225 WKLog.Info($"[AssetService] Creating new asset database for {_wkLibAPI.DisplayName}");
226 currentDatabase = new WKAssetDatabase();
227
228 CL_AssetManager.AddNewDatabase(_wkLibAPI.DisplayName, currentDatabase);
229 }
230
231 var allGOs = request.allAssets.Cast<GameObject>().ToList();
232 var total = allGOs.Count;
233 const int batchSize = 20;
234
235 for (var i = 0; i < total; i += batchSize)
236 {
237 for (var j = i; j < i + batchSize && j < total; j++)
238 {
239 var go = allGOs[j];
240 if (go.TryGetComponent<M_Level>(out var level))
241 {
242 bool alreadyExists = false;
243 foreach (var levelAsset in currentDatabase.levelAssets)
244 {
245 if (levelAsset == null)
246 continue;
247
248 if (levelAsset.id == level.levelName)
249 {
250 alreadyExists = true;
251 break;
252 }
253 }
254
255 if (!alreadyExists)
256 currentDatabase.levelAssets.Add(M_Level.LevelAssetHolder.GetNewHolderFromLevel(level));
257
258 // Log its name for debugging
259 WKLog.Debug($"[AssetService] Found level: {level.name}");
260
261 if (!foundLevels.TryAdd(level.name, level))
262 WKLog.Debug($"[AssetService] Duplicate prefab.name '{level.name}' found; skipping the duplicate.");
263 }
264 }
265 progress?.Report(0.5f + (i / (float)total) * 0.5f);
266 await Task.Yield();
267 }
268
269 _loadedLevelsCache[cacheKey] = foundLevels;
270 progress?.Report(1f);
271 return foundLevels;
272 }
273
278 public List<M_Level> FindLevelsByName(string nameContains)
279 {
280 return CL_AssetManager.GetFullCombinedAssetDatabase().levelAssets
281 .Select(levelAsset => levelAsset.level)
282 .Where(level => level is not null)
283 .Where(level => level.name.Contains(nameContains))
284 .ToList();
285 }
286
287 #endregion
288
289 #region Loading Gamemodes
290
295 public async Task<M_Gamemode> LoadGameModeFromBundle(
296 AssetBundle bundle,
297 string assetName,
298 IProgress<float> progress = null)
299 {
300 if (bundle is null)
301 {
302 WKLog.Error("[AssetService] Bundle is null; cannot load gamemode.");
303 progress?.Report(1f);
304 return null;
305 }
306
307 var cacheKey = $"{_wkLibAPI.DisplayName}:{assetName}";
308
309 if (_gamemodeCache.TryGetValue(cacheKey, out var cachedGamemode))
310 {
311 WKLog.Debug($"[AssetService] Returning cached gamemode: '{cacheKey}'");
312 progress?.Report(1f);
313 return cachedGamemode;
314 }
315
316 var request = bundle.LoadAssetAsync<ScriptableObject>(assetName);
317 while (!request.isDone)
318 {
319 progress?.Report(request.progress);
320 await Task.Yield();
321 }
322
323 var gamemode = request.asset as M_Gamemode;
324
325 if (gamemode is null)
326 {
327 WKLog.Error($"[AssetService] Gamemode '{assetName}' not found in bundle {bundle.name}");
328 progress?.Report(1f);
329 return null;
330 }
331
332 if (string.IsNullOrEmpty(gamemode.unlockAchievement))
333 gamemode.unlockAchievement = "ACH_TUTORIAL";
334
335 gamemode.gamemodePanel = Resources.FindObjectsOfTypeAll<UI_GamemodeScreen_Panel>()
336 .FirstOrDefault(x => x.name == "Gamemode_Panel_Base");
337 gamemode.loseScreen = Resources.FindObjectsOfTypeAll<UI_ScoreScreen>()
338 .FirstOrDefault(x => x.name == "ScorePanel_Standard_Death");
339 gamemode.winScreen = Resources.FindObjectsOfTypeAll<UI_ScoreScreen>()
340 .FirstOrDefault(x => x.name == "ScorePanel_Standard_Win");
341
342 _gamemodeCache[cacheKey] = gamemode;
343 progress?.Report(1f);
344 WKLog.Debug($"[AssetService] Loaded and cached gamemode: '{assetName}'");
345 return gamemode;
346 }
347
348 #endregion
349
350 #region Helper Methods
351
356 public Sprite LoadPngAsSpriteRelative(string relativePngPath)
357 {
358 var fullPath = Path.Combine(AssemblyFolder, relativePngPath);
359 if (!File.Exists(fullPath))
360 {
361 WKLog.Error($"[AssetService] PNG not found at: {fullPath}");
362 return null;
363 }
364 var data = File.ReadAllBytes(fullPath);
365 var tex = new Texture2D(2, 2, TextureFormat.RGBA32, false);
366 if (!tex.LoadImage(data))
367 {
368 WKLog.Error($"[AssetService] Failed to load image: {relativePngPath}");
369 return null;
370 }
371 var sprite = Sprite.Create(tex, new Rect(0,0,tex.width,tex.height), new Vector2(0.5f,0.5f));
372 sprite.name = Path.GetFileNameWithoutExtension(relativePngPath);
373 return sprite;
374 }
375
379 public Sprite LoadPngAsSprite(string pngFileName)
380 {
381 var tex = LoadPngTexture(pngFileName);
382 if (tex is null) return null;
383
384 var sprite = Sprite.Create(
385 tex,
386 new Rect(0, 0, tex.width, tex.height),
387 new Vector2(0.5f, 0.5f)
388 );
389 sprite.name = pngFileName.Split(".png")[0];
390 return sprite;
391 }
392
397 private Texture2D LoadPngTexture(string pngFileName)
398 {
399 var pngPath = Path.Combine(AssemblyFolder, "Assets", pngFileName);
400 if (!File.Exists(pngPath))
401 {
402 WKLog.Error($"[AssetService] PNG not found at: {pngPath}");
403 return null;
404 }
405
406 var data = File.ReadAllBytes(pngPath);
407 var tex = new Texture2D(2, 2, TextureFormat.RGBA32, false);
408
409 if (tex.LoadImage(data)) return tex;
410
411 WKLog.Error($"[AssetService] Failed to decode PNG: {pngPath}");
412 return null;
413 }
414
415 #endregion
416}
UnityEngine.Object Object
Loads and caches AssetBundles and assets, scoped by ModContext to avoid collisions.
List< M_Level > FindLevelsByName(string nameContains)
Returns all M_Level instances whose prefab names contain the given substring. (Uses levelPrefabs from...
void UnloadAllBundles(bool unloadAllLoadedObjects=false)
Unloads all bundles loaded by this mod.
void UnloadBundleRelative(string relativePath, bool unloadAllLoadedObjects=false)
Unloads a specific bundle for this mod.
Sprite LoadPngAsSpriteRelative(string relativePngPath)
Loads a PNG as a Sprite relative to the mod folder. Sprite | null
async Task< Dictionary< string, M_Level > > LoadAllLevelsFromBundle(AssetBundle bundle, IProgress< float > progress=null)
Given a loaded AssetBundle, iterate over every ".prefab" asset name, Load it as a GameObject,...
async Task< M_Gamemode > LoadGameModeFromBundle(AssetBundle bundle, string assetName, IProgress< float > progress=null)
Loads a gamemode from an asset bundle.
Sprite LoadPngAsSprite(string pngFileName)
Convenience: Create a Sprite from a PNG filename.
async Task< AssetBundle > LoadBundleRelativeAsync(string relativePath, IProgress< float > progress=null)
Loads an AssetBundle given a path relative to the mod assembly folder. Caches under a key combining ...