Skip to content

Commit b00d567

Browse files
authored
Fixed prefab resource cache not storing all required data (SubnauticaNitrox#2665)
1 parent 13e8037 commit b00d567

2 files changed

Lines changed: 57 additions & 57 deletions

File tree

Nitrox.Server.Subnautica/Models/Resources/Parsers/EntityDistributionsResource.cs

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,12 @@ internal class EntityDistributionsResource(SubnauticaAssetsManager assetsManager
1212
private readonly SubnauticaAssetsManager assetsManager = assetsManager;
1313
private readonly IOptions<ServerStartOptions> options = options;
1414

15-
private ValueTask<LootDistributionData> lootDistribution;
16-
public LootDistributionData LootDistribution => GetLootDistributionDataAsync().GetAwaiter().GetResult();
15+
private readonly TaskCompletionSource<LootDistributionData> lootDistributionTcs = new();
16+
public LootDistributionData LootDistribution => lootDistributionTcs.Task.GetAwaiter().GetResult();
1717

18-
public Task LoadAsync(CancellationToken cancellationToken)
18+
public async Task LoadAsync(CancellationToken cancellationToken)
1919
{
20-
lootDistribution = GetLootDistributionDataAsync(cancellationToken);
21-
return Task.CompletedTask;
20+
lootDistributionTcs.TrySetResult(await GetLootDistributionDataAsync(cancellationToken));
2221
}
2322

2423
public Task CleanupAsync()
@@ -27,13 +26,8 @@ public Task CleanupAsync()
2726
return Task.CompletedTask;
2827
}
2928

30-
private async ValueTask<LootDistributionData> GetLootDistributionDataAsync(CancellationToken cancellationToken = default)
29+
private async Task<LootDistributionData> GetLootDistributionDataAsync(CancellationToken cancellationToken = default)
3130
{
32-
if (lootDistribution is { IsCompletedSuccessfully : true, Result: not null })
33-
{
34-
return await lootDistribution;
35-
}
36-
3731
// TODO: Do not depend on game code; use custom types to map to game JSON files.
3832
LootDictionary result = JsonSerializer.Deserialize<LootDictionary>(await GetJsonAsync(cancellationToken),
3933
new JsonSerializerOptions

Nitrox.Server.Subnautica/Models/Resources/Parsers/PrefabPlaceholderGroupsResource.cs

Lines changed: 52 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
using Nitrox.Server.Subnautica.Models.Helper;
1111
using Nitrox.Server.Subnautica.Models.Resources.AddressablesTools.Catalog;
1212
using Nitrox.Server.Subnautica.Models.Resources.Core;
13+
using ClassIdByRuntimeKeyDictionary = System.Collections.Generic.Dictionary<string, string>;
14+
using AddressableCatalogDictionary = System.Collections.Generic.Dictionary<string, string[]>;
1315

1416
namespace Nitrox.Server.Subnautica.Models.Resources.Parsers;
1517

@@ -23,18 +25,17 @@ internal sealed class PrefabPlaceholderGroupsResource(SubnauticaAssetsManager as
2325
/// the cache is rebuilt
2426
/// </para>
2527
/// </summary>
26-
private const int CACHE_VERSION = 3;
28+
private const int CACHE_VERSION = 4;
29+
2730
private const string CACHE_FILENAME = "PrefabPlaceholdersGroupAssetsCache.json";
2831

29-
private readonly ConcurrentDictionary<string, string[]> addressableCatalog = new();
3032
private readonly SubnauticaAssetsManager assetsManager = assetsManager;
31-
private readonly ConcurrentDictionary<string, string> classIdByRuntimeKey = new();
32-
private readonly ConcurrentDictionary<string, PrefabPlaceholdersGroupAsset> groupsByClassId = new();
3333
private readonly ILogger<PrefabPlaceholderGroupsResource> logger = logger;
3434
private readonly IOptions<ServerStartOptions> options = options;
35-
private readonly ConcurrentDictionary<string, PrefabPlaceholderAsset> placeholdersByClassId = [];
3635
private readonly TaskCompletionSource resourceLoadFinished = new();
3736
private readonly JsonSerializer serializer = new() { TypeNameHandling = TypeNameHandling.Auto };
37+
private ConcurrentDictionary<string, PrefabPlaceholdersGroupAsset> groupsByClassId = [];
38+
private ConcurrentDictionary<string, PrefabPlaceholderAsset> placeholdersByClassId = [];
3839
private ConcurrentDictionary<string, string[]> randomPossibilitiesByClassId = [];
3940

4041
public ConcurrentDictionary<string, PrefabPlaceholdersGroupAsset> GroupsByClassId
@@ -62,10 +63,6 @@ public ConcurrentDictionary<string, string[]> RandomPossibilitiesByClassId
6263
resourceLoadFinished.Task.GetAwaiter().GetResult();
6364
return randomPossibilitiesByClassId;
6465
}
65-
private set
66-
{
67-
randomPossibilitiesByClassId = value;
68-
}
6966
}
7067

7168
public async Task LoadAsync(CancellationToken cancellationToken)
@@ -89,7 +86,7 @@ public void PickRandomClassIdIfRequired(ref string classId)
8986
}
9087
}
9188

92-
private static Dictionary<string, string> LoadPrefabDatabase(string fullFilename)
89+
private static ClassIdByRuntimeKeyDictionary LoadPrefabDatabase(string fullFilename)
9390
{
9491
Dictionary<string, string> prefabFiles = new();
9592
if (!File.Exists(fullFilename))
@@ -122,40 +119,29 @@ private static void GetPrefabGameObjectInfoFromBundle(SubnauticaAssetsManager am
122119
prefabGameObjectInfo = assetFileInst.file.Metadata.GetAssetInfo(rootAssetPathId);
123120
}
124121

125-
private Task<Dictionary<string, PrefabPlaceholdersGroupAsset>> LoadPrefabsAndSpawnPossibilitiesAsync(CancellationToken cancellationToken = default)
122+
private async Task LoadPrefabsAndSpawnPossibilitiesAsync(CancellationToken cancellationToken = default)
126123
{
127-
string prefabDatabasePath = Path.Combine(options.Value.GetSubnauticaResourcesPath(), "StreamingAssets", "SNUnmanagedData", "prefabs.db");
128-
129-
// Get all prefab-classIds linked to the (partial) bundle path
130-
Dictionary<string, string> prefabDatabase = LoadPrefabDatabase(prefabDatabasePath);
131-
cancellationToken.ThrowIfCancellationRequested();
132-
133124
// Loading all prefabs by their classId and file paths (first the path to the prefab then the dependencies)
134-
LoadAddressableCatalog(options.Value.GetSubnauticaAaResourcePath(), prefabDatabase);
135125
cancellationToken.ThrowIfCancellationRequested();
136-
Dictionary<string, PrefabPlaceholdersGroupAsset> result = CreateOrLoadPrefabCache(options.Value.GetServerCachePath());
126+
await CreateOrLoadPrefabCacheAsync(options.Value.GetServerCachePath());
137127
cancellationToken.ThrowIfCancellationRequested();
138128

139129
// Select only prefabs with a PrefabPlaceholdersGroups component in the root and link them with their dependencyPaths
140130
// Do not remove: the internal cache list is slowing down the process more than loading a few assets again. There maybe is a better way in the new AssetToolsNetVersion but, we need a byte to texture library bc ATNs sub-package is only for netstandard.
141131
assetsManager.UnloadAll(true);
142-
// Clear private collections that were used temporarily to parse the files.
143-
addressableCatalog.Clear();
144-
classIdByRuntimeKey.Clear();
145132

146133
// Get all needed data for the filtered PrefabPlaceholdersGroups to construct PrefabPlaceholdersGroupAssets and add them to the dictionary by classId
147134
Validate.IsFalse(randomPossibilitiesByClassId.IsEmpty);
148-
return Task.FromResult(result);
149135
}
150136

151-
private Dictionary<string, PrefabPlaceholdersGroupAsset> CreateOrLoadPrefabCache(string nitroxCachePath)
137+
private async Task CreateOrLoadPrefabCacheAsync(string nitroxCachePath)
152138
{
153-
Dictionary<string, PrefabPlaceholdersGroupAsset> prefabPlaceholdersGroupPaths = null;
139+
Dictionary<string, PrefabPlaceholdersGroupAsset> prefabPlaceholdersGroupPaths;
154140
string cacheFilePath = Path.Combine(nitroxCachePath, CACHE_FILENAME);
155141
Cache? cache = null;
156142
try
157143
{
158-
cache = Cache.Deserialize(serializer, cacheFilePath);
144+
cache = await Cache.DeserializeAsync(serializer, cacheFilePath);
159145
}
160146
catch (Exception ex)
161147
{
@@ -169,24 +155,33 @@ private Dictionary<string, PrefabPlaceholdersGroupAsset> CreateOrLoadPrefabCache
169155
}
170156
prefabPlaceholdersGroupPaths = cache.Value.PrefabPlaceholdersGroupPaths;
171157
randomPossibilitiesByClassId = cache.Value.RandomPossibilitiesByClassId;
158+
groupsByClassId = cache.Value.GroupsByClassId;
159+
placeholdersByClassId = cache.Value.PlaceholdersByClassId;
172160
logger.ZLogDebug($"Successfully loaded cache with {prefabPlaceholdersGroupPaths.Count:@PrefabPlaceholdersCount} prefab placeholder groups and {randomPossibilitiesByClassId.Count:@RandomPossibilitiesCount} random spawn behaviours.");
173161
}
174162
// Fallback solution
175-
if (prefabPlaceholdersGroupPaths is null)
163+
else
176164
{
177165
logger.ZLogInformation($"Building cache, this may take a while...");
178-
prefabPlaceholdersGroupPaths = new(GetPrefabPlaceholderGroupAssetsByGroupClassId(assetsManager, GetAllPrefabPlaceholdersGroupsFast(assetsManager)));
179-
Cache.Serialize(serializer, new Cache(CACHE_VERSION, prefabPlaceholdersGroupPaths, randomPossibilitiesByClassId), cacheFilePath);
166+
// Get all prefab-classIds linked to the (partial) bundle path
167+
string prefabDatabasePath = Path.Combine(options.Value.GetSubnauticaResourcesPath(), "StreamingAssets", "SNUnmanagedData", "prefabs.db");
168+
Dictionary<string, string> prefabDatabase = LoadPrefabDatabase(prefabDatabasePath);
169+
(AddressableCatalogDictionary addressableCatalog, ClassIdByRuntimeKeyDictionary classIdByRuntimeKey) = LoadAddressableCatalog(options.Value.GetSubnauticaAaResourcePath(), prefabDatabase);
170+
prefabPlaceholdersGroupPaths = new(GetPrefabPlaceholderGroupAssetsByGroupClassId(assetsManager, GetAllPrefabPlaceholdersGroupsFast(assetsManager, addressableCatalog, classIdByRuntimeKey), addressableCatalog, classIdByRuntimeKey));
171+
await Cache.SerializeAsync(serializer, new Cache(CACHE_VERSION, prefabPlaceholdersGroupPaths, randomPossibilitiesByClassId, groupsByClassId, placeholdersByClassId), cacheFilePath);
180172
logger.ZLogDebug(
181173
$"Successfully built cache with {prefabPlaceholdersGroupPaths.Count:@PrefabPlaceholdersCount} prefab placeholder groups and {randomPossibilitiesByClassId.Count:@RandomPossibilitiesCount} random spawn behaviours. Future server starts will take less time.");
182174
}
183175
Validate.IsTrue(prefabPlaceholdersGroupPaths.Count > 0);
184176
Validate.IsTrue(randomPossibilitiesByClassId.Count > 0);
185-
return prefabPlaceholdersGroupPaths;
177+
Validate.IsTrue(groupsByClassId.Count > 0);
178+
Validate.IsTrue(placeholdersByClassId.Count > 0);
186179
}
187180

188-
private void LoadAddressableCatalog(string aaRootPath, Dictionary<string, string> prefabDatabase)
181+
private (AddressableCatalogDictionary, ClassIdByRuntimeKeyDictionary) LoadAddressableCatalog(string aaRootPath, Dictionary<string, string> prefabDatabase)
189182
{
183+
ClassIdByRuntimeKeyDictionary classIdByRuntimeKey = [];
184+
AddressableCatalogDictionary addressableCatalog = [];
190185
ContentCatalogData ccd = ContentCatalogData.FromJson(File.ReadAllText(Path.Combine(aaRootPath, "catalog.json")));
191186
Dictionary<string, string> classIdByPath = prefabDatabase.ToDictionary(m => m.Value, m => m.Key);
192187

@@ -216,13 +211,15 @@ private void LoadAddressableCatalog(string aaRootPath, Dictionary<string, string
216211
break;
217212
}
218213
}
214+
215+
return (addressableCatalog, classIdByRuntimeKey);
219216
}
220217

221218
/// <summary>
222219
/// Gathers bundle paths by class id for prefab placeholder groups.
223220
/// Also fills <see cref="RandomPossibilitiesByClassId" />
224221
/// </summary>
225-
private ConcurrentDictionary<string, string[]> GetAllPrefabPlaceholdersGroupsFast(SubnauticaAssetsManager am)
222+
private ConcurrentDictionary<string, string[]> GetAllPrefabPlaceholdersGroupsFast(SubnauticaAssetsManager am, AddressableCatalogDictionary addressableCatalog, ClassIdByRuntimeKeyDictionary classIdByRuntimeKey)
226223
{
227224
// First step is to find out about the hash of the types PrefabPlaceholdersGroup and SpawnRandom
228225
// to be able to recognize them easily later on
@@ -313,7 +310,8 @@ private ConcurrentDictionary<string, string[]> GetAllPrefabPlaceholdersGroupsFas
313310
return prefabPlaceholdersGroupPaths;
314311
}
315312

316-
private ConcurrentDictionary<string, PrefabPlaceholdersGroupAsset> GetPrefabPlaceholderGroupAssetsByGroupClassId(SubnauticaAssetsManager am, ConcurrentDictionary<string, string[]> prefabPlaceholdersGroupPaths)
313+
private ConcurrentDictionary<string, PrefabPlaceholdersGroupAsset> GetPrefabPlaceholderGroupAssetsByGroupClassId(SubnauticaAssetsManager am, ConcurrentDictionary<string, string[]> prefabPlaceholdersGroupPaths,
314+
AddressableCatalogDictionary addressableCatalog, ClassIdByRuntimeKeyDictionary classIdByRuntimeKey)
317315
{
318316
ConcurrentDictionary<string, PrefabPlaceholdersGroupAsset> prefabPlaceholderGroupsByGroupClassId = new();
319317

@@ -323,7 +321,7 @@ private ConcurrentDictionary<string, PrefabPlaceholdersGroupAsset> GetPrefabPlac
323321
SubnauticaAssetsManager amInnerClone = amClone.Clone();
324322
AssetsFileInstance assetFileInst = amInnerClone.LoadBundleWithDependencies(keyValuePair.Value);
325323

326-
PrefabPlaceholdersGroupAsset prefabPlaceholderGroup = GetAndCachePrefabPlaceholdersGroupOfBundle(amInnerClone, assetFileInst, keyValuePair.Key);
324+
PrefabPlaceholdersGroupAsset prefabPlaceholderGroup = GetAndCachePrefabPlaceholdersGroupOfBundle(amInnerClone, assetFileInst, keyValuePair.Key, addressableCatalog, classIdByRuntimeKey);
327325
amInnerClone.UnloadAll();
328326

329327
if (!prefabPlaceholderGroupsByGroupClassId.TryAdd(keyValuePair.Key, prefabPlaceholderGroup))
@@ -334,13 +332,15 @@ private ConcurrentDictionary<string, PrefabPlaceholdersGroupAsset> GetPrefabPlac
334332
return prefabPlaceholderGroupsByGroupClassId;
335333
}
336334

337-
private PrefabPlaceholdersGroupAsset GetAndCachePrefabPlaceholdersGroupOfBundle(SubnauticaAssetsManager amInst, AssetsFileInstance assetFileInst, string classId)
335+
private PrefabPlaceholdersGroupAsset GetAndCachePrefabPlaceholdersGroupOfBundle(SubnauticaAssetsManager amInst, AssetsFileInstance assetFileInst, string classId, AddressableCatalogDictionary addressableCatalog,
336+
ClassIdByRuntimeKeyDictionary classIdByRuntimeKey)
338337
{
339338
GetPrefabGameObjectInfoFromBundle(amInst, assetFileInst, out AssetFileInfo prefabGameObjectInfo);
340-
return GetAndCachePrefabPlaceholdersGroupGroup(amInst, assetFileInst, prefabGameObjectInfo, classId);
339+
return GetAndCachePrefabPlaceholdersGroupGroup(amInst, assetFileInst, prefabGameObjectInfo, classId, addressableCatalog, classIdByRuntimeKey);
341340
}
342341

343-
private PrefabPlaceholdersGroupAsset GetAndCachePrefabPlaceholdersGroupGroup(SubnauticaAssetsManager amInst, AssetsFileInstance assetFileInst, AssetFileInfo rootGameObjectInfo, string classId)
342+
private PrefabPlaceholdersGroupAsset GetAndCachePrefabPlaceholdersGroupGroup(SubnauticaAssetsManager amInst, AssetsFileInstance assetFileInst, AssetFileInfo rootGameObjectInfo, string classId, AddressableCatalogDictionary addressableCatalog,
343+
ClassIdByRuntimeKeyDictionary classIdByRuntimeKey)
344344
{
345345
if (!string.IsNullOrEmpty(classId) && groupsByClassId.TryGetValue(classId, out PrefabPlaceholdersGroupAsset cachedGroup))
346346
{
@@ -368,7 +368,7 @@ private PrefabPlaceholdersGroupAsset GetAndCachePrefabPlaceholdersGroupGroup(Sub
368368

369369
AssetTypeValueField gameObjectPtr = prefabPlaceholder["m_GameObject"];
370370
AssetTypeValueField gameObjectField = amInst.GetExtAsset(assetFileInst, gameObjectPtr).baseField;
371-
IPrefabAsset asset = GetAndCacheAsset(amInst, prefabPlaceholder["prefabClassId"].AsString);
371+
IPrefabAsset asset = GetAndCacheAsset(amInst, prefabPlaceholder["prefabClassId"].AsString, addressableCatalog, classIdByRuntimeKey);
372372
bool isEntitySlotAsset = asset is PrefabPlaceholderAsset prefabPlaceholderAsset && prefabPlaceholderAsset.EntitySlot.HasValue;
373373
NitroxTransform transform = amInst.GetTransformFromGameObject(assetFileInst, gameObjectField, rootGameObjectName, isEntitySlotAsset);
374374
string prefabAssetClassId = prefabPlaceholder["prefabClassId"].AsString;
@@ -388,7 +388,7 @@ private PrefabPlaceholdersGroupAsset GetAndCachePrefabPlaceholdersGroupGroup(Sub
388388
return prefabPlaceholdersGroup;
389389
}
390390

391-
private IPrefabAsset? GetAndCacheAsset(SubnauticaAssetsManager am, string classId)
391+
private IPrefabAsset? GetAndCacheAsset(SubnauticaAssetsManager am, string classId, AddressableCatalogDictionary addressableCatalog, ClassIdByRuntimeKeyDictionary classIdByRuntimeKey)
392392
{
393393
if (string.IsNullOrEmpty(classId))
394394
{
@@ -415,7 +415,7 @@ private PrefabPlaceholdersGroupAsset GetAndCachePrefabPlaceholdersGroupGroup(Sub
415415
AssetFileInfo placeholdersGroupInfo = am.GetMonoBehaviourFromGameObject(assetFileInst, prefabGameObjectInfo, "PrefabPlaceholdersGroup");
416416
if (placeholdersGroupInfo != null)
417417
{
418-
PrefabPlaceholdersGroupAsset groupAsset = GetAndCachePrefabPlaceholdersGroupOfBundle(am, assetFileInst, classId);
418+
PrefabPlaceholdersGroupAsset groupAsset = GetAndCachePrefabPlaceholdersGroupOfBundle(am, assetFileInst, classId, addressableCatalog, classIdByRuntimeKey);
419419
groupsByClassId[classId] = groupAsset;
420420
return groupAsset;
421421
}
@@ -469,24 +469,30 @@ private PrefabPlaceholdersGroupAsset GetAndCachePrefabPlaceholdersGroupGroup(Sub
469469
return prefabPlaceholderAsset;
470470
}
471471

472-
private record struct Cache(int Version, Dictionary<string, PrefabPlaceholdersGroupAsset> PrefabPlaceholdersGroupPaths, ConcurrentDictionary<string, string[]> RandomPossibilitiesByClassId)
472+
private record struct Cache(
473+
int Version,
474+
Dictionary<string, PrefabPlaceholdersGroupAsset> PrefabPlaceholdersGroupPaths,
475+
ConcurrentDictionary<string, string[]> RandomPossibilitiesByClassId,
476+
ConcurrentDictionary<string, PrefabPlaceholdersGroupAsset> GroupsByClassId,
477+
ConcurrentDictionary<string, PrefabPlaceholderAsset> PlaceholdersByClassId
478+
)
473479
{
474-
public static void Serialize(JsonSerializer serializer, Cache cache, string filePath)
480+
public static async Task SerializeAsync(JsonSerializer serializer, Cache cache, string filePath)
475481
{
476482
Directory.CreateDirectory(Path.GetDirectoryName(filePath) ?? throw new Exception("Failed to get directory path from cache file path"));
477-
using StreamWriter stream = File.CreateText(filePath);
483+
await using StreamWriter stream = File.CreateText(filePath);
478484
serializer.Serialize(stream, cache);
479485
}
480486

481-
public static Cache? Deserialize(JsonSerializer serializer, string filePath)
487+
public static Task<Cache?> DeserializeAsync(JsonSerializer serializer, string filePath)
482488
{
483489
if (!File.Exists(filePath))
484490
{
485-
return null;
491+
return Task.FromResult<Cache?>(null);
486492
}
487493
Directory.CreateDirectory(Path.GetDirectoryName(filePath) ?? throw new Exception("Failed to get directory path from cache file path"));
488494
using StreamReader reader = File.OpenText(filePath);
489-
return (Cache?)serializer.Deserialize(reader, typeof(Cache));
495+
return Task.FromResult((Cache?)serializer.Deserialize(reader, typeof(Cache)));
490496
}
491497
}
492498
}

0 commit comments

Comments
 (0)