Skip to content

Commit bdb2890

Browse files
authored
fix: server-side entity spawn race conditions (SubnauticaNitrox#2682)
1 parent d92f28d commit bdb2890

3 files changed

Lines changed: 22 additions & 26 deletions

File tree

Nitrox.Server.Subnautica/Models/GameLogic/Entities/Spawning/BatchEntitySpawner.cs

Lines changed: 13 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Collections.Concurrent;
12
using System.Collections.Generic;
23
using System.Linq;
34
using Nitrox.Model.DataStructures;
@@ -37,34 +38,28 @@ internal sealed class BatchEntitySpawner(
3738
private readonly XorRandom random = randomFactory.GetUnityLikeRandom();
3839
private readonly SubnauticaUweWorldEntityFactory worldEntityFactory = worldEntityFactory;
3940

40-
private readonly Lock parsedBatchesLock = new();
41+
private readonly ConcurrentDictionary<NitroxInt3, Lazy<Task<List<Entity>>>> batchLoadTasks = new();
4142
private readonly Lock emptyBatchesLock = new();
42-
private HashSet<NitroxInt3> parsedBatches = [];
4343

4444
public List<NitroxInt3> SerializableParsedBatches
4545
{
4646
get
4747
{
48-
List<NitroxInt3> parsed;
4948
List<NitroxInt3> empty;
5049

51-
lock (parsedBatchesLock)
52-
{
53-
parsed = [.. parsedBatches];
54-
}
55-
5650
lock (emptyBatchesLock)
5751
{
5852
empty = [.. emptyBatches];
5953
}
6054

61-
return [.. parsed.Except(empty)];
55+
return [.. batchLoadTasks.Keys.Except(empty)];
6256
}
6357
set
6458
{
65-
lock (parsedBatchesLock)
59+
batchLoadTasks.Clear();
60+
foreach (NitroxInt3 batchId in value)
6661
{
67-
parsedBatches = [.. value];
62+
batchLoadTasks.TryAdd(batchId, new Lazy<Task<List<Entity>>>(() => Task.FromResult(new List<Entity>())));
6863
}
6964
}
7065
}
@@ -73,22 +68,16 @@ public List<NitroxInt3> SerializableParsedBatches
7368

7469
public bool IsBatchSpawned(NitroxInt3 batchId)
7570
{
76-
lock (parsedBatches)
77-
{
78-
return parsedBatches.Contains(batchId);
79-
}
71+
return batchLoadTasks.ContainsKey(batchId);
8072
}
8173

82-
public async Task<List<Entity>> LoadUnspawnedEntitiesAsync(NitroxInt3 batchId, bool fullCacheCreation = false)
74+
public Task<List<Entity>> LoadUnspawnedEntitiesAsync(NitroxInt3 batchId, bool fullCacheCreation = false)
8375
{
84-
lock (parsedBatches)
85-
{
86-
if (!parsedBatches.Add(batchId))
87-
{
88-
return [];
89-
}
90-
}
76+
return batchLoadTasks.GetOrAdd(batchId, id => new Lazy<Task<List<Entity>>>(() => LoadBatchInternalAsync(id, fullCacheCreation))).Value;
77+
}
9178

79+
private async Task<List<Entity>> LoadBatchInternalAsync(NitroxInt3 batchId, bool fullCacheCreation)
80+
{
9281
DeterministicGenerator deterministicBatchGenerator = new(options.Value.Seed, batchId.ToString());
9382
List<EntitySpawnPoint> spawnPoints = batchCellsParser.ParseBatchData(batchId);
9483
List<Entity> entities = await SpawnEntitiesAsync(spawnPoints, deterministicBatchGenerator);
@@ -105,7 +94,7 @@ public async Task<List<Entity>> LoadUnspawnedEntitiesAsync(NitroxInt3 batchId, b
10594
logger.ZLogInformation($"Spawning {entities.Count} entities from {spawnPoints.Count} spawn points in batch {batchId}");
10695
}
10796

108-
for (int x = 0; x < entities.Count; x++) // Throws on duplicate Entities already but nice to know which ones
97+
for (int x = 0; x < entities.Count; x++)
10998
{
11099
for (int y = 0; y < entities.Count; y++)
111100
{

Nitrox.Server.Subnautica/Models/GameLogic/Entities/WorldEntityManager.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Collections.Concurrent;
12
using System.Collections.Generic;
23
using System.Diagnostics.CodeAnalysis;
34
using System.Linq;
@@ -35,6 +36,7 @@ internal sealed class WorldEntityManager
3536
private readonly PlayerManager playerManager;
3637

3738
private readonly Lock worldEntitiesLock = new();
39+
private readonly ConcurrentDictionary<NitroxInt3, Lazy<Task<int>>> batchRegistrationTasks = new();
3840

3941
/// <summary>
4042
/// World entities can disappear if you go out of range.
@@ -245,7 +247,12 @@ public async Task LoadAllUnspawnedEntitiesAsync(CancellationToken token)
245247
}
246248
}
247249

248-
public async Task<int> LoadUnspawnedEntitiesAsync(NitroxInt3 batchId, bool suppressLogs)
250+
public Task<int> LoadUnspawnedEntitiesAsync(NitroxInt3 batchId, bool suppressLogs)
251+
{
252+
return batchRegistrationTasks.GetOrAdd(batchId, id => new Lazy<Task<int>>(() => LoadAndRegisterBatchInternalAsync(id, suppressLogs))).Value;
253+
}
254+
255+
private async Task<int> LoadAndRegisterBatchInternalAsync(NitroxInt3 batchId, bool suppressLogs)
249256
{
250257
List<Entity> spawnedEntities = await batchEntitySpawner.LoadUnspawnedEntitiesAsync(batchId, suppressLogs);
251258

Nitrox.Server.Subnautica/Models/Packets/Processors/EntityDestroyedPacketProcessor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public async Task Process(AuthProcessorContext context, EntityDestroyed packet)
2828
bool isOtherPlayer = player != context.Sender;
2929
if (isOtherPlayer && player.CanSee(entity))
3030
{
31-
await context.ReplyAsync(packet);
31+
await context.SendAsync(packet, player.SessionId);
3232
}
3333
}
3434
}

0 commit comments

Comments
 (0)