2using System.Collections.Generic;
5using System.Reflection;
7using System.Threading.Tasks;
9using BepInEx.Bootstrap;
11using UnityEngine.UIElements;
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();
29 private readonly CancellationTokenSource _sQuitCts =
new();
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}");
37 var assemblyPath = pluginInfo.Location;
38 AssemblyFolder = Path.GetDirectoryName(assemblyPath) ??
string.Empty;
40 Application.quitting += () =>
42 WKLog.Debug(
"[AssetService] Quitting...");
47 #region Loading Asset Bundles
56 IProgress<float> progress =
null)
59 if (
string.IsNullOrEmpty(relativePath))
61 WKLog.Error(
"[AssetService] Path is null or empty.");
66 var cacheKey = $
"{_wkLibAPI.DisplayName}:{Path.GetFileNameWithoutExtension(relativePath)}";
67 if (_bundleCache.TryGetValue(cacheKey, out var cached) && cached is not
null)
69 WKLog.Debug($
"[AssetService] Returning cached bundle: {cacheKey}");
75 if (!File.Exists(fullPath))
77 WKLog.Error($
"[AssetService] Bundle not found at {fullPath}");
82 var request = AssetBundle.LoadFromFileAsync(fullPath);
85 WKLog.Error($
"[AssetService] Failed to load bundle: {fullPath} (request)");
92 while (!request.isDone)
94 _sQuitCts.Token.ThrowIfCancellationRequested();
96 progress?.Report(request.progress * 1.1f);
100 catch (OperationCanceledException)
102 WKLog.Debug($
"[AssetService] Asset Bundle Load cancelled");
106 var bundle = request.assetBundle;
109 WKLog.Error($
"[AssetService] Failed to load bundle at {fullPath} (bundle)");
110 progress?.Report(1f);
114 _bundleCache[cacheKey] = bundle;
115 WKLog.Debug($
"[AssetService] Loaded & cached bundle: {cacheKey}");
116 progress?.Report(1f);
122 #region Unloading Asset Bundles
129 var cacheKey = $
"{_wkLibAPI.DisplayName}:{relativePath}";
130 if (_bundleCache.TryGetValue(cacheKey, out var bundle) && bundle is not
null)
134 bundle.Unload(unloadAllLoadedObjects);
138 WKLog.Error($
"[AssetService] Failed to unload bundle: {cacheKey}");
141 WKLog.Debug($
"[AssetService] Unloaded bundle: {cacheKey}");
142 _bundleCache.Remove(cacheKey);
151 var keys = _bundleCache.Keys.Where(k => k.StartsWith(_wkLibAPI.DisplayName +
":")).ToList();
152 foreach (var key
in keys)
154 if (_bundleCache[key] is not
null)
158 _bundleCache[key].Unload(unloadAllLoadedObjects);
162 WKLog.Error($
"[AssetService] Failed to unload bundle: {key}");
165 WKLog.Debug($
"[AssetService] Unloaded bundle: {key}");
167 _bundleCache.Remove(key);
173 #region Loading Levels
184 IProgress<float> progress =
null)
186 var foundLevels =
new Dictionary<string, M_Level>();
190 WKLog.Error(
"[AssetService] LoadAllLevelsFromBundle called with null bundle.");
191 progress?.Report(1f);
195 var cacheKey = $
"{_wkLibAPI.DisplayName}:{bundle.name}";
197 if (_loadedLevelsCache.ContainsKey(cacheKey))
199 WKLog.Debug($
"[AssetService] Returning Cached loaded levels for bundle: {cacheKey}");
200 progress?.Report(1f);
201 return _loadedLevelsCache.GetValueOrDefault(cacheKey);
204 var request = bundle.LoadAllAssetsAsync<GameObject>();
207 while (!request.isDone)
209 _sQuitCts.Token.ThrowIfCancellationRequested();
211 progress?.Report(request.progress * 0.5f);
215 catch (OperationCanceledException)
217 WKLog.Debug($
"[AssetService] LoadAllLevelsFromBundle canceled.");
222 var currentDatabase = CL_AssetManager.GetDatabase(_wkLibAPI.DisplayName);
223 if (currentDatabase ==
null)
225 WKLog.Info($
"[AssetService] Creating new asset database for {_wkLibAPI.DisplayName}");
226 currentDatabase =
new WKAssetDatabase();
228 CL_AssetManager.AddNewDatabase(_wkLibAPI.DisplayName, currentDatabase);
231 var allGOs = request.allAssets.Cast<GameObject>().ToList();
232 var total = allGOs.Count;
233 const int batchSize = 20;
235 for (var i = 0; i < total; i += batchSize)
237 for (var j = i; j < i + batchSize && j < total; j++)
240 if (go.TryGetComponent<M_Level>(out var level))
242 bool alreadyExists =
false;
243 foreach (var levelAsset
in currentDatabase.levelAssets)
245 if (levelAsset ==
null)
248 if (levelAsset.id == level.levelName)
250 alreadyExists =
true;
256 currentDatabase.levelAssets.Add(M_Level.LevelAssetHolder.GetNewHolderFromLevel(level));
259 WKLog.Debug($
"[AssetService] Found level: {level.name}");
261 if (!foundLevels.TryAdd(level.name, level))
262 WKLog.Debug($
"[AssetService] Duplicate prefab.name '{level.name}' found; skipping the duplicate.");
265 progress?.Report(0.5f + (i / (
float)total) * 0.5f);
269 _loadedLevelsCache[cacheKey] = foundLevels;
270 progress?.Report(1f);
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))
289 #region Loading Gamemodes
298 IProgress<float> progress =
null)
302 WKLog.Error(
"[AssetService] Bundle is null; cannot load gamemode.");
303 progress?.Report(1f);
307 var cacheKey = $
"{_wkLibAPI.DisplayName}:{assetName}";
309 if (_gamemodeCache.TryGetValue(cacheKey, out var cachedGamemode))
311 WKLog.Debug($
"[AssetService] Returning cached gamemode: '{cacheKey}'");
312 progress?.Report(1f);
313 return cachedGamemode;
316 var request = bundle.LoadAssetAsync<ScriptableObject>(assetName);
317 while (!request.isDone)
319 progress?.Report(request.progress);
323 var gamemode = request.asset as M_Gamemode;
325 if (gamemode is
null)
327 WKLog.Error($
"[AssetService] Gamemode '{assetName}' not found in bundle {bundle.name}");
328 progress?.Report(1f);
332 if (
string.IsNullOrEmpty(gamemode.unlockAchievement))
333 gamemode.unlockAchievement =
"ACH_TUTORIAL";
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");
342 _gamemodeCache[cacheKey] = gamemode;
343 progress?.Report(1f);
344 WKLog.Debug($
"[AssetService] Loaded and cached gamemode: '{assetName}'");
350 #region Helper Methods
359 if (!File.Exists(fullPath))
361 WKLog.Error($
"[AssetService] PNG not found at: {fullPath}");
364 var data = File.ReadAllBytes(fullPath);
365 var tex =
new Texture2D(2, 2, TextureFormat.RGBA32,
false);
366 if (!tex.LoadImage(data))
368 WKLog.Error($
"[AssetService] Failed to load image: {relativePngPath}");
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);
381 var tex = LoadPngTexture(pngFileName);
382 if (tex is
null)
return null;
384 var sprite = Sprite.Create(
386 new Rect(0, 0, tex.width, tex.height),
387 new Vector2(0.5f, 0.5f)
389 sprite.name = pngFileName.Split(
".png")[0];
397 private Texture2D LoadPngTexture(
string pngFileName)
399 var pngPath = Path.Combine(
AssemblyFolder,
"Assets", pngFileName);
400 if (!File.Exists(pngPath))
402 WKLog.Error($
"[AssetService] PNG not found at: {pngPath}");
406 var data = File.ReadAllBytes(pngPath);
407 var tex =
new Texture2D(2, 2, TextureFormat.RGBA32,
false);
409 if (tex.LoadImage(data))
return tex;
411 WKLog.Error($
"[AssetService] Failed to decode PNG: {pngPath}");
UnityEngine.Object Object
Loads and caches AssetBundles and assets, scoped by ModContext to avoid collisions.
AssetService(WKLibAPI API)
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.
readonly string AssemblyFolder
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 ...