Optimise marker spawning (#17922)
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Content.Server.Decals;
|
using Content.Server.Decals;
|
||||||
using Content.Server.Ghost.Roles.Components;
|
using Content.Server.Ghost.Roles.Components;
|
||||||
using Content.Server.Shuttles.Events;
|
using Content.Server.Shuttles.Events;
|
||||||
@@ -6,6 +7,7 @@ using Content.Shared.Decals;
|
|||||||
using Content.Shared.Parallax.Biomes;
|
using Content.Shared.Parallax.Biomes;
|
||||||
using Content.Shared.Parallax.Biomes.Layers;
|
using Content.Shared.Parallax.Biomes.Layers;
|
||||||
using Content.Shared.Parallax.Biomes.Markers;
|
using Content.Shared.Parallax.Biomes.Markers;
|
||||||
|
using Microsoft.Extensions.ObjectPool;
|
||||||
using Robust.Server.Player;
|
using Robust.Server.Player;
|
||||||
using Robust.Shared;
|
using Robust.Shared;
|
||||||
using Robust.Shared.Collections;
|
using Robust.Shared.Collections;
|
||||||
@@ -17,6 +19,8 @@ using Robust.Shared.Noise;
|
|||||||
using Robust.Shared.Player;
|
using Robust.Shared.Player;
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Random;
|
using Robust.Shared.Random;
|
||||||
|
using Robust.Shared.Serialization.Manager;
|
||||||
|
using Robust.Shared.Threading;
|
||||||
using Robust.Shared.Utility;
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
namespace Content.Server.Parallax;
|
namespace Content.Server.Parallax;
|
||||||
@@ -26,9 +30,11 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
|
|||||||
[Dependency] private readonly IConfigurationManager _configManager = default!;
|
[Dependency] private readonly IConfigurationManager _configManager = default!;
|
||||||
[Dependency] private readonly IConsoleHost _console = default!;
|
[Dependency] private readonly IConsoleHost _console = default!;
|
||||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||||
|
[Dependency] private readonly IParallelManager _parallel = default!;
|
||||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||||
[Dependency] private readonly IPrototypeManager _proto = default!;
|
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||||
[Dependency] private readonly IRobustRandom _random = default!;
|
[Dependency] private readonly IRobustRandom _random = default!;
|
||||||
|
[Dependency] private readonly ISerializationManager _serManager = default!;
|
||||||
[Dependency] private readonly DecalSystem _decals = default!;
|
[Dependency] private readonly DecalSystem _decals = default!;
|
||||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||||
|
|
||||||
@@ -36,6 +42,9 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
|
|||||||
private const float DefaultLoadRange = 16f;
|
private const float DefaultLoadRange = 16f;
|
||||||
private float _loadRange = DefaultLoadRange;
|
private float _loadRange = DefaultLoadRange;
|
||||||
|
|
||||||
|
private ObjectPool<HashSet<Vector2i>> _tilePool =
|
||||||
|
new DefaultObjectPool<HashSet<Vector2i>>(new SetPolicy<Vector2i>(), 256);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Load area for chunks containing tiles, decals etc.
|
/// Load area for chunks containing tiles, decals etc.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -265,7 +274,7 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
|
|||||||
|
|
||||||
while (biomes.MoveNext(out var biome))
|
while (biomes.MoveNext(out var biome))
|
||||||
{
|
{
|
||||||
_activeChunks.Add(biome, new HashSet<Vector2i>());
|
_activeChunks.Add(biome, _tilePool.Get());
|
||||||
_markerChunks.GetOrNew(biome);
|
_markerChunks.GetOrNew(biome);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,6 +330,12 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
|
|||||||
}
|
}
|
||||||
|
|
||||||
_handledEntities.Clear();
|
_handledEntities.Clear();
|
||||||
|
|
||||||
|
foreach (var tiles in _activeChunks.Values)
|
||||||
|
{
|
||||||
|
_tilePool.Return(tiles);
|
||||||
|
}
|
||||||
|
|
||||||
_activeChunks.Clear();
|
_activeChunks.Clear();
|
||||||
_markerChunks.Clear();
|
_markerChunks.Clear();
|
||||||
}
|
}
|
||||||
@@ -364,100 +379,53 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
|
|||||||
{
|
{
|
||||||
var markers = _markerChunks[component];
|
var markers = _markerChunks[component];
|
||||||
var loadedMarkers = component.LoadedMarkers;
|
var loadedMarkers = component.LoadedMarkers;
|
||||||
var spawnSet = new HashSet<Vector2i>();
|
|
||||||
var spawns = new List<Vector2i>();
|
|
||||||
var frontier = new ValueList<Vector2i>();
|
|
||||||
|
|
||||||
foreach (var (layer, chunks) in markers)
|
foreach (var (layer, chunks) in markers)
|
||||||
{
|
{
|
||||||
foreach (var chunk in chunks)
|
Parallel.ForEach(chunks, new ParallelOptions() { MaxDegreeOfParallelism = _parallel.ParallelProcessCount }, chunk =>
|
||||||
{
|
{
|
||||||
if (loadedMarkers.TryGetValue(layer, out var mobChunks) && mobChunks.Contains(chunk))
|
if (loadedMarkers.TryGetValue(layer, out var mobChunks) && mobChunks.Contains(chunk))
|
||||||
continue;
|
return;
|
||||||
|
|
||||||
|
var noiseCopy = new FastNoiseLite();
|
||||||
|
_serManager.CopyTo(component.Noise, ref noiseCopy, notNullableOverride: true);
|
||||||
|
var spawnSet = _tilePool.Get();
|
||||||
|
var frontier = new ValueList<Vector2i>(32);
|
||||||
|
|
||||||
|
// Make a temporary version and copy back in later.
|
||||||
|
var pending = new Dictionary<Vector2i, Dictionary<string, List<Vector2i>>>();
|
||||||
|
|
||||||
spawns.Clear();
|
|
||||||
spawnSet.Clear();
|
|
||||||
var layerProto = _proto.Index<BiomeMarkerLayerPrototype>(layer);
|
var layerProto = _proto.Index<BiomeMarkerLayerPrototype>(layer);
|
||||||
var buffer = layerProto.Radius / 2f;
|
var buffer = layerProto.Radius / 2f;
|
||||||
mobChunks ??= new HashSet<Vector2i>();
|
|
||||||
mobChunks.Add(chunk);
|
|
||||||
loadedMarkers[layer] = mobChunks;
|
|
||||||
var rand = new Random(noise.GetSeed() + chunk.X * 8 + chunk.Y + layerProto.GetHashCode());
|
var rand = new Random(noise.GetSeed() + chunk.X * 8 + chunk.Y + layerProto.GetHashCode());
|
||||||
|
|
||||||
// We treat a null entity mask as requiring nothing else on the tile
|
// We treat a null entity mask as requiring nothing else on the tile
|
||||||
var lower = (int) Math.Floor(buffer);
|
var lower = (int) Math.Floor(buffer);
|
||||||
var upper = (int) Math.Ceiling(layerProto.Size - buffer);
|
var upper = (int) Math.Ceiling(layerProto.Size - buffer);
|
||||||
|
|
||||||
// TODO: Okay this is inefficient as FUCK
|
|
||||||
// I think the ideal is pick a random tile then BFS outwards from it probably ig
|
|
||||||
// It will bias edge tiles significantly more but will make the CPU cry less.
|
|
||||||
for (var x = lower; x <= upper; x++)
|
|
||||||
{
|
|
||||||
for (var y = lower; y <= upper; y++)
|
|
||||||
{
|
|
||||||
var index = new Vector2i(x + chunk.X, y + chunk.Y);
|
|
||||||
TryGetEntity(index, component.Layers, component.Noise, grid, out var proto);
|
|
||||||
|
|
||||||
if (proto != layerProto.EntityMask)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
spawns.Add(index);
|
|
||||||
spawnSet.Add(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load NOW
|
|
||||||
// TODO: Need poisson but crashes whenever I use moony's due to inputs or smth idk
|
// TODO: Need poisson but crashes whenever I use moony's due to inputs or smth idk
|
||||||
var count = (int) ((layerProto.Size - buffer) * (layerProto.Size - buffer) / (layerProto.Radius * layerProto.Radius));
|
var count = (int) ((layerProto.Size - buffer) * (layerProto.Size - buffer) /
|
||||||
|
(layerProto.Radius * layerProto.Radius));
|
||||||
count = Math.Min(count, layerProto.MaxCount);
|
count = Math.Min(count, layerProto.MaxCount);
|
||||||
|
|
||||||
|
// Pick a random tile then BFS outwards from it
|
||||||
|
// It will bias edge tiles significantly more but will make the CPU cry less.
|
||||||
for (var i = 0; i < count; i++)
|
for (var i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
if (spawns.Count == 0)
|
|
||||||
break;
|
|
||||||
|
|
||||||
var index = rand.Next(spawns.Count);
|
|
||||||
var point = spawns[index];
|
|
||||||
spawns.RemoveSwap(index);
|
|
||||||
|
|
||||||
// Point was potentially used in BFS search below but we hadn't updated the list yet.
|
|
||||||
if (!spawnSet.Remove(point))
|
|
||||||
{
|
|
||||||
i--;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// BFS search
|
|
||||||
frontier.Add(point);
|
|
||||||
var groupCount = layerProto.GroupCount;
|
var groupCount = layerProto.GroupCount;
|
||||||
|
var startNodeX = rand.Next(lower, upper + 1);
|
||||||
|
var startNodeY = rand.Next(lower, upper + 1);
|
||||||
|
var startNode = new Vector2i(startNodeX, startNodeY);
|
||||||
|
frontier.Clear();
|
||||||
|
frontier.Add(startNode);
|
||||||
|
|
||||||
while (frontier.Count > 0 && groupCount > 0)
|
while (groupCount > 0 && frontier.Count > 0)
|
||||||
{
|
{
|
||||||
var frontierIndex = _random.Next(frontier.Count);
|
var frontierIndex = rand.Next(frontier.Count);
|
||||||
var node = frontier[frontierIndex];
|
var node = frontier[frontierIndex];
|
||||||
frontier.RemoveSwap(frontierIndex);
|
frontier.RemoveSwap(frontierIndex);
|
||||||
var enumerator = grid.GetAnchoredEntitiesEnumerator(node);
|
|
||||||
|
|
||||||
if (enumerator.MoveNext(out _))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// Need to ensure the tile under it has loaded for anchoring.
|
|
||||||
if (TryGetBiomeTile(node, component.Layers, component.Noise, grid, out var tile))
|
|
||||||
{
|
|
||||||
grid.SetTile(node, tile.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it is a ghost role then purge it
|
|
||||||
// TODO: This is *kind* of a bandaid but natural mobs spawns needs a lot more work.
|
|
||||||
// Ideally we'd just have ghost role and non-ghost role variants for some stuff.
|
|
||||||
var uid = EntityManager.CreateEntityUninitialized(layerProto.Prototype, new EntityCoordinates(gridUid, node));
|
|
||||||
RemComp<GhostTakeoverAvailableComponent>(uid);
|
|
||||||
RemComp<GhostRoleComponent>(uid);
|
|
||||||
EntityManager.InitializeAndStartEntity(uid);
|
|
||||||
groupCount--;
|
|
||||||
|
|
||||||
|
// Add neighbors regardless.
|
||||||
for (var x = -1; x <= 1; x++)
|
for (var x = -1; x <= 1; x++)
|
||||||
{
|
{
|
||||||
for (var y = -1; y <= 1; y++)
|
for (var y = -1; y <= 1; y++)
|
||||||
@@ -467,25 +435,85 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
|
|||||||
|
|
||||||
var neighbor = new Vector2i(x + node.X, y + node.Y);
|
var neighbor = new Vector2i(x + node.X, y + node.Y);
|
||||||
|
|
||||||
if (!spawnSet.Contains(neighbor))
|
// Check if it's inbounds.
|
||||||
|
if (neighbor.X < lower ||
|
||||||
|
neighbor.Y < lower ||
|
||||||
|
neighbor.X > upper ||
|
||||||
|
neighbor.Y > upper)
|
||||||
|
{
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
frontier.Add(neighbor);
|
frontier.Add(neighbor);
|
||||||
// Rather than doing some uggo remove check on the list we'll defer it until later
|
|
||||||
spawnSet.Remove(neighbor);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the unused nodes back in
|
var actualNode = node + chunk;
|
||||||
foreach (var node in frontier)
|
|
||||||
|
if (!spawnSet.Add(actualNode))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Check if it's a valid spawn, if so then use it.
|
||||||
|
var enumerator = grid.GetAnchoredEntitiesEnumerator(actualNode);
|
||||||
|
|
||||||
|
if (enumerator.MoveNext(out _))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Check if mask matches.
|
||||||
|
TryGetEntity(actualNode, component.Layers, noiseCopy, grid, out var proto);
|
||||||
|
|
||||||
|
if (proto != layerProto.EntityMask)
|
||||||
{
|
{
|
||||||
spawnSet.Add(node);
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
frontier.Clear();
|
var chunkOrigin = SharedMapSystem.GetChunkIndices(actualNode, ChunkSize) * ChunkSize;
|
||||||
|
|
||||||
|
if (!pending.TryGetValue(chunkOrigin, out var pendingMarkers))
|
||||||
|
{
|
||||||
|
pendingMarkers = new Dictionary<string, List<Vector2i>>();
|
||||||
|
pending[chunkOrigin] = pendingMarkers;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pendingMarkers.TryGetValue(layer, out var layerMarkers))
|
||||||
|
{
|
||||||
|
layerMarkers = new List<Vector2i>();
|
||||||
|
pendingMarkers[layer] = layerMarkers;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log.Info($"Added node at {actualNode} for chunk {chunkOrigin}");
|
||||||
|
layerMarkers.Add(actualNode);
|
||||||
|
groupCount--;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lock (component.PendingMarkers)
|
||||||
|
{
|
||||||
|
if (!loadedMarkers.TryGetValue(layer, out var lockMobChunks))
|
||||||
|
{
|
||||||
|
lockMobChunks = new HashSet<Vector2i>();
|
||||||
|
loadedMarkers[layer] = lockMobChunks;
|
||||||
|
}
|
||||||
|
|
||||||
|
lockMobChunks.Add(chunk);
|
||||||
|
|
||||||
|
foreach (var (chunkOrigin, layers) in pending)
|
||||||
|
{
|
||||||
|
if (!component.PendingMarkers.TryGetValue(chunkOrigin, out var lockMarkers))
|
||||||
|
{
|
||||||
|
lockMarkers = new Dictionary<string, List<Vector2i>>();
|
||||||
|
component.PendingMarkers[chunkOrigin] = lockMarkers;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var (lockLayer, nodes) in layers)
|
||||||
|
{
|
||||||
|
lockMarkers[lockLayer] = nodes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_tilePool.Return(spawnSet);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var active = _activeChunks[component];
|
var active = _activeChunks[component];
|
||||||
@@ -515,7 +543,36 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
|
|||||||
EntityQuery<TransformComponent> xformQuery)
|
EntityQuery<TransformComponent> xformQuery)
|
||||||
{
|
{
|
||||||
component.ModifiedTiles.TryGetValue(chunk, out var modified);
|
component.ModifiedTiles.TryGetValue(chunk, out var modified);
|
||||||
modified ??= new HashSet<Vector2i>();
|
modified ??= _tilePool.Get();
|
||||||
|
|
||||||
|
// Load any pending marker tiles first.
|
||||||
|
if (component.PendingMarkers.TryGetValue(chunk, out var layers))
|
||||||
|
{
|
||||||
|
foreach (var (layer, nodes) in layers)
|
||||||
|
{
|
||||||
|
var layerProto = _proto.Index<BiomeMarkerLayerPrototype>(layer);
|
||||||
|
|
||||||
|
foreach (var node in nodes)
|
||||||
|
{
|
||||||
|
// Need to ensure the tile under it has loaded for anchoring.
|
||||||
|
if (TryGetBiomeTile(node, component.Layers, component.Noise, grid, out var tile))
|
||||||
|
{
|
||||||
|
grid.SetTile(node, tile.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it is a ghost role then purge it
|
||||||
|
// TODO: This is *kind* of a bandaid but natural mobs spawns needs a lot more work.
|
||||||
|
// Ideally we'd just have ghost role and non-ghost role variants for some stuff.
|
||||||
|
var uid = EntityManager.CreateEntityUninitialized(layerProto.Prototype, grid.GridTileToLocal(node));
|
||||||
|
RemComp<GhostTakeoverAvailableComponent>(uid);
|
||||||
|
RemComp<GhostRoleComponent>(uid);
|
||||||
|
EntityManager.InitializeAndStartEntity(uid);
|
||||||
|
modified.Add(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
component.PendingMarkers.Remove(chunk);
|
||||||
|
}
|
||||||
|
|
||||||
// Set tiles first
|
// Set tiles first
|
||||||
for (var x = 0; x < ChunkSize; x++)
|
for (var x = 0; x < ChunkSize; x++)
|
||||||
@@ -606,6 +663,7 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
|
|||||||
|
|
||||||
if (modified.Count == 0)
|
if (modified.Count == 0)
|
||||||
{
|
{
|
||||||
|
_tilePool.Return(modified);
|
||||||
component.ModifiedTiles.Remove(chunk);
|
component.ModifiedTiles.Remove(chunk);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -58,6 +58,12 @@ public sealed partial class BiomeComponent : Component
|
|||||||
|
|
||||||
#region Markers
|
#region Markers
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Work out entire marker tiles in advance but only load the entities when in range.
|
||||||
|
/// </summary>
|
||||||
|
[DataField("pendingMarkers")]
|
||||||
|
public Dictionary<Vector2i, Dictionary<string, List<Vector2i>>> PendingMarkers = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Track what markers we've loaded already to avoid double-loading.
|
/// Track what markers we've loaded already to avoid double-loading.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -28,20 +28,22 @@ public abstract class SharedBiomeSystem : EntitySystem
|
|||||||
component.Noise.SetSeed(component.Seed);
|
component.Noise.SetSeed(component.Seed);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected T Pick<T>(List<T> collection, float value)
|
private T Pick<T>(List<T> collection, float value)
|
||||||
{
|
{
|
||||||
DebugTools.Assert(value is >= 0f and <= 1f);
|
// Listen I don't need this exact and I'm too lazy to finetune just for random ent picking.
|
||||||
|
value %= 1f;
|
||||||
|
value = Math.Clamp(value, 0f, 1f);
|
||||||
|
|
||||||
if (collection.Count == 1)
|
if (collection.Count == 1)
|
||||||
return collection[0];
|
return collection[0];
|
||||||
|
|
||||||
value *= collection.Count;
|
var randValue = value * collection.Count;
|
||||||
|
|
||||||
foreach (var item in collection)
|
foreach (var item in collection)
|
||||||
{
|
{
|
||||||
value -= 1f;
|
randValue -= 1f;
|
||||||
|
|
||||||
if (value <= 0f)
|
if (randValue <= 0f)
|
||||||
{
|
{
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
@@ -50,9 +52,10 @@ public abstract class SharedBiomeSystem : EntitySystem
|
|||||||
throw new ArgumentOutOfRangeException();
|
throw new ArgumentOutOfRangeException();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected int Pick(int count, float value)
|
private int Pick(int count, float value)
|
||||||
{
|
{
|
||||||
DebugTools.Assert(value is >= 0f and <= 1f);
|
value %= 1f;
|
||||||
|
value = Math.Clamp(value, 0f, 1f);
|
||||||
|
|
||||||
if (count == 1)
|
if (count == 1)
|
||||||
return 0;
|
return 0;
|
||||||
@@ -234,7 +237,8 @@ public abstract class SharedBiomeSystem : EntitySystem
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
entity = Pick(biomeLayer.Entities, (noise.GetNoise(indices.X, indices.Y, i) + 1f) / 2f);
|
var noiseValue = noise.GetNoise(indices.X, indices.Y, i);
|
||||||
|
entity = Pick(biomeLayer.Entities, (noiseValue + 1f) / 2f);
|
||||||
noise.SetSeed(oldSeed);
|
noise.SetSeed(oldSeed);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user