using Content.Server.Decals; using Content.Server.Shuttles.Events; using Content.Shared.Decals; using Content.Shared.Parallax.Biomes; using Content.Shared.Parallax.Biomes.Layers; using Content.Shared.Parallax.Biomes.Markers; using Content.Shared.Parallax.Biomes.Points; using Robust.Server.Player; using Robust.Shared; using Robust.Shared.Configuration; using Robust.Shared.Console; using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Noise; using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Utility; namespace Content.Server.Parallax; public sealed partial class BiomeSystem : SharedBiomeSystem { [Dependency] private readonly IConfigurationManager _configManager = default!; [Dependency] private readonly IConsoleHost _console = default!; [Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IPrototypeManager _proto = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly DecalSystem _decals = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; private readonly HashSet _handledEntities = new(); private const float DefaultLoadRange = 16f; private float _loadRange = DefaultLoadRange; /// /// Load area for chunks containing tiles, decals etc. /// private Box2 _loadArea = new(-DefaultLoadRange, -DefaultLoadRange, DefaultLoadRange, DefaultLoadRange); /// /// Stores the chunks active for this tick temporarily. /// private readonly Dictionary> _activeChunks = new(); private readonly Dictionary>> _markerChunks = new(); public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnBiomeStartup); SubscribeLocalEvent(OnBiomeMapInit); SubscribeLocalEvent(OnFTLStarted); _configManager.OnValueChanged(CVars.NetMaxUpdateRange, SetLoadRange, true); InitializeCommands(); _proto.PrototypesReloaded += ProtoReload; } public override void Shutdown() { base.Shutdown(); _configManager.UnsubValueChanged(CVars.NetMaxUpdateRange, SetLoadRange); _proto.PrototypesReloaded -= ProtoReload; } private void ProtoReload(PrototypesReloadedEventArgs obj) { if (!obj.ByType.TryGetValue(typeof(BiomeTemplatePrototype), out var reloads)) return; var query = AllEntityQuery(); while (query.MoveNext(out var biome)) { if (biome.Template == null || !reloads.Modified.TryGetValue(biome.Template, out var proto)) continue; SetTemplate(biome, (BiomeTemplatePrototype) proto); } } private void SetLoadRange(float obj) { // Round it up _loadRange = MathF.Ceiling(obj / ChunkSize) * ChunkSize; _loadArea = new Box2(-_loadRange, -_loadRange, _loadRange, _loadRange); } private void OnBiomeStartup(EntityUid uid, BiomeComponent component, ComponentStartup args) { component.Noise.SetSeed(component.Seed); } private void OnBiomeMapInit(EntityUid uid, BiomeComponent component, MapInitEvent args) { SetSeed(component, _random.Next()); } public void SetSeed(BiomeComponent component, int seed) { component.Seed = seed; component.Noise.SetSeed(seed); Dirty(component); } public void ClearTemplate(BiomeComponent component) { component.Layers.Clear(); component.Template = null; Dirty(component); } /// /// Sets the and refreshes layers. /// public void SetTemplate(BiomeComponent component, BiomeTemplatePrototype template) { component.Layers.Clear(); component.Template = template.ID; foreach (var layer in template.Layers) { component.Layers.Add(layer); } Dirty(component); } /// /// Adds the specified layer at the specified marker if it exists. /// public void AddLayer(BiomeComponent component, string id, IBiomeLayer addedLayer, int seedOffset = 0) { for (var i = 0; i < component.Layers.Count; i++) { var layer = component.Layers[i]; if (layer is not BiomeDummyLayer dummy || dummy.ID != id) continue; addedLayer.Noise.SetSeed(addedLayer.Noise.GetSeed() + seedOffset); component.Layers.Insert(i, addedLayer); break; } Dirty(component); } public void AddMarkerLayer(BiomeComponent component, string marker) { if (!_proto.HasIndex(marker)) { // TODO: Log when we get a sawmill return; } component.MarkerLayers.Add(marker); Dirty(component); } /// /// Adds the specified template at the specified marker if it exists, withour overriding every layer. /// public void AddTemplate(BiomeComponent component, string id, BiomeTemplatePrototype template, int seedOffset = 0) { for (var i = 0; i < component.Layers.Count; i++) { var layer = component.Layers[i]; if (layer is not BiomeDummyLayer dummy || dummy.ID != id) continue; for (var j = template.Layers.Count - 1; j >= 0; j--) { var addedLayer = template.Layers[j]; addedLayer.Noise.SetSeed(addedLayer.Noise.GetSeed() + seedOffset); component.Layers.Insert(i, addedLayer); } break; } Dirty(component); } private void OnFTLStarted(ref FTLStartedEvent ev) { var targetMap = ev.TargetCoordinates.ToMap(EntityManager, _transform); var targetMapUid = _mapManager.GetMapEntityId(targetMap.MapId); if (!TryComp(targetMapUid, out var biome)) return; var targetArea = new Box2(targetMap.Position - 32f, targetMap.Position + 32f); Preload(targetMapUid, biome, targetArea); } /// /// Preloads biome for the specified area. /// public void Preload(EntityUid uid, BiomeComponent component, Box2 area) { var markers = component.MarkerLayers; var goobers = _markerChunks.GetOrNew(component); foreach (var layer in markers) { var proto = _proto.Index(layer); var enumerator = new ChunkIndicesEnumerator(area, proto.Size); while (enumerator.MoveNext(out var chunk)) { var chunkOrigin = chunk * proto.Size; var layerChunks = goobers.GetOrNew(proto.ID); layerChunks.Add(chunkOrigin.Value); } } } public override void Update(float frameTime) { base.Update(frameTime); var biomeQuery = GetEntityQuery(); var xformQuery = GetEntityQuery(); var biomes = AllEntityQuery(); while (biomes.MoveNext(out var biome)) { _activeChunks.Add(biome, new HashSet()); _markerChunks.GetOrNew(biome); } // Get chunks in range foreach (var client in Filter.GetAllPlayers(_playerManager)) { var pSession = (IPlayerSession) client; if (xformQuery.TryGetComponent(pSession.AttachedEntity, out var xform) && _handledEntities.Add(pSession.AttachedEntity.Value) && biomeQuery.TryGetComponent(xform.MapUid, out var biome)) { var worldPos = _transform.GetWorldPosition(xform, xformQuery); AddChunksInRange(biome, worldPos); foreach (var layer in biome.MarkerLayers) { var layerProto = _proto.Index(layer); AddMarkerChunksInRange(biome, worldPos, layerProto); } } foreach (var viewer in pSession.ViewSubscriptions) { if (!_handledEntities.Add(viewer) || !xformQuery.TryGetComponent(viewer, out xform) || !biomeQuery.TryGetComponent(xform.MapUid, out biome)) { continue; } var worldPos = _transform.GetWorldPosition(xform, xformQuery); AddChunksInRange(biome, worldPos); foreach (var layer in biome.MarkerLayers) { var layerProto = _proto.Index(layer); AddMarkerChunksInRange(biome, worldPos, layerProto); } } } var loadBiomes = AllEntityQuery(); while (loadBiomes.MoveNext(out var gridUid, out var biome, out var grid)) { var noise = biome.Noise; // Load new chunks LoadChunks(biome, gridUid, grid, noise, xformQuery); // Unload old chunks UnloadChunks(biome, gridUid, grid, noise); } _handledEntities.Clear(); _activeChunks.Clear(); _markerChunks.Clear(); } private void AddChunksInRange(BiomeComponent biome, Vector2 worldPos) { var enumerator = new ChunkIndicesEnumerator(_loadArea.Translated(worldPos), ChunkSize); while (enumerator.MoveNext(out var chunkOrigin)) { _activeChunks[biome].Add(chunkOrigin.Value * ChunkSize); } } private void AddMarkerChunksInRange(BiomeComponent biome, Vector2 worldPos, IBiomeMarkerLayer layer) { // Offset the load area so it's centralised. var loadArea = new Box2(0, 0, layer.Size, layer.Size); var enumerator = new ChunkIndicesEnumerator(loadArea.Translated(worldPos - layer.Size / 2f), layer.Size); while (enumerator.MoveNext(out var chunkOrigin)) { var lay = _markerChunks[biome].GetOrNew(layer.ID); lay.Add(chunkOrigin.Value * layer.Size); } } #region Load private void LoadChunks( BiomeComponent component, EntityUid gridUid, MapGridComponent grid, FastNoiseLite noise, EntityQuery xformQuery) { var markers = _markerChunks[component]; var loadedMarkers = component.LoadedMarkers; foreach (var (layer, chunks) in markers) { foreach (var chunk in chunks) { if (loadedMarkers.TryGetValue(layer, out var mobChunks) && mobChunks.Contains(chunk)) continue; var layerProto = _proto.Index(layer); var buffer = layerProto.Radius / 2f; mobChunks ??= new HashSet(); mobChunks.Add(chunk); loadedMarkers[layer] = mobChunks; var rand = new Random(noise.GetSeed() + chunk.X * 8 + chunk.Y); // Load NOW // TODO: Need poisson but crashes whenever I use moony's due to inputs or smth var count = (int) ((layerProto.Size - buffer) * (layerProto.Size - buffer) / (layerProto.Radius * layerProto.Radius)); for (var i = 0; i < count; i++) { for (var j = 0; j < 5; j++) { var point = new Vector2( chunk.X + buffer + rand.NextFloat() * (layerProto.Size - buffer), chunk.Y + buffer + rand.NextFloat() * (layerProto.Size - buffer)); var coords = new EntityCoordinates(gridUid, point); var tile = grid.LocalToTile(coords); // Blocked spawn, try again. if (grid.GetAnchoredEntitiesEnumerator(tile).MoveNext(out _)) continue; for (var k = 0; k < layerProto.GroupCount; k++) { Spawn(layerProto.Prototype, new EntityCoordinates(gridUid, point)); } break; } } } } var active = _activeChunks[component]; List<(Vector2i, Tile)>? tiles = null; foreach (var chunk in active) { if (!component.LoadedChunks.Add(chunk)) continue; tiles ??= new List<(Vector2i, Tile)>(ChunkSize * ChunkSize); // Load NOW! LoadChunk(component, gridUid, grid, chunk, noise, tiles, xformQuery); } } private void LoadChunk( BiomeComponent component, EntityUid gridUid, MapGridComponent grid, Vector2i chunk, FastNoiseLite noise, List<(Vector2i, Tile)> tiles, EntityQuery xformQuery) { component.ModifiedTiles.TryGetValue(chunk, out var modified); modified ??= new HashSet(); // Set tiles first for (var x = 0; x < ChunkSize; x++) { for (var y = 0; y < ChunkSize; y++) { var indices = new Vector2i(x + chunk.X, y + chunk.Y); if (modified.Contains(indices)) continue; // If there's existing data then don't overwrite it. if (grid.TryGetTileRef(indices, out var tileRef) && !tileRef.Tile.IsEmpty) continue; // Pass in null so we don't try to get the tileref. if (!TryGetBiomeTile(indices, component.Layers, noise, null, out var biomeTile) || biomeTile.Value == tileRef.Tile) continue; tiles.Add((indices, biomeTile.Value)); } } grid.SetTiles(tiles); tiles.Clear(); // Now do entities var loadedEntities = new List(); component.LoadedEntities.Add(chunk, loadedEntities); for (var x = 0; x < ChunkSize; x++) { for (var y = 0; y < ChunkSize; y++) { var indices = new Vector2i(x + chunk.X, y + chunk.Y); if (modified.Contains(indices)) continue; // Don't mess with anything that's potentially anchored. var anchored = grid.GetAnchoredEntitiesEnumerator(indices); if (anchored.MoveNext(out _) || !TryGetEntity(indices, component.Layers, noise, grid, out var entPrototype)) continue; // TODO: Fix non-anchored ents spawning. // Just track loaded chunks for now. var ent = Spawn(entPrototype, grid.GridTileToLocal(indices)); // At least for now unless we do lookups or smth, only work with anchoring. if (xformQuery.TryGetComponent(ent, out var xform) && !xform.Anchored) { _transform.AnchorEntity(ent, xform, gridUid, grid, indices); } loadedEntities.Add(ent); } } // Decals var loadedDecals = new Dictionary(); component.LoadedDecals.Add(chunk, loadedDecals); for (var x = 0; x < ChunkSize; x++) { for (var y = 0; y < ChunkSize; y++) { var indices = new Vector2i(x + chunk.X, y + chunk.Y); if (modified.Contains(indices)) continue; // Don't mess with anything that's potentially anchored. var anchored = grid.GetAnchoredEntitiesEnumerator(indices); if (anchored.MoveNext(out _) || !TryGetDecals(indices, component.Layers, noise, grid, out var decals)) continue; foreach (var decal in decals) { if (!_decals.TryAddDecal(decal.ID, new EntityCoordinates(gridUid, decal.Position), out var dec)) continue; loadedDecals.Add(dec, indices); } } } if (modified.Count == 0) { component.ModifiedTiles.Remove(chunk); } else { component.ModifiedTiles[chunk] = modified; } } #endregion #region Unload private void UnloadChunks(BiomeComponent component, EntityUid gridUid, MapGridComponent grid, FastNoiseLite noise) { var active = _activeChunks[component]; List<(Vector2i, Tile)>? tiles = null; foreach (var chunk in component.LoadedChunks) { if (active.Contains(chunk) || !component.LoadedChunks.Remove(chunk)) continue; // Unload NOW! tiles ??= new List<(Vector2i, Tile)>(ChunkSize * ChunkSize); UnloadChunk(component, gridUid, grid, chunk, noise, tiles); } } private void UnloadChunk(BiomeComponent component, EntityUid gridUid, MapGridComponent grid, Vector2i chunk, FastNoiseLite noise, List<(Vector2i, Tile)> tiles) { // Reverse order to loading component.ModifiedTiles.TryGetValue(chunk, out var modified); modified ??= new HashSet(); // Delete decals foreach (var (dec, indices) in component.LoadedDecals[chunk]) { // If we couldn't remove it then flag the tile to never be touched. if (!_decals.RemoveDecal(gridUid, dec)) { modified.Add(indices); } } component.LoadedDecals.Remove(chunk); // Delete entities // This is a TODO // Ideally any entities that aren't modified just get deleted and re-generated later // This is because if we want to save the map (e.g. persistent server) it makes the file much smaller // and also if the map is enormous will make stuff like physics broadphase much faster // For now we'll just leave them because no entity diffs. component.LoadedEntities.Remove(chunk); // Unset tiles (if the data is custom) for (var x = 0; x < ChunkSize; x++) { for (var y = 0; y < ChunkSize; y++) { var indices = new Vector2i(x + chunk.X, y + chunk.Y); if (modified.Contains(indices)) continue; // Don't mess with anything that's potentially anchored. var anchored = grid.GetAnchoredEntitiesEnumerator(indices); if (anchored.MoveNext(out _)) { modified.Add(indices); continue; } // If it's default data unload the tile. if (!TryGetBiomeTile(indices, component.Layers, noise, null, out var biomeTile) || grid.TryGetTileRef(indices, out var tileRef) && tileRef.Tile != biomeTile.Value) { modified.Add(indices); continue; } tiles.Add((indices, Tile.Empty)); } } grid.SetTiles(tiles); tiles.Clear(); component.LoadedChunks.Remove(chunk); if (modified.Count == 0) { component.ModifiedTiles.Remove(chunk); } else { component.ModifiedTiles[chunk] = modified; } } #endregion }