--- /dev/null
+using System.Numerics;
+using System.Text;
+using Content.Shared.Parallax.Biomes;
+using Robust.Client.Graphics;
+using Robust.Client.Input;
+using Robust.Client.ResourceManagement;
+using Robust.Shared.Enums;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+
+namespace Content.Client.Parallax;
+
+public sealed class BiomeDebugOverlay : Overlay
+{
+ public override OverlaySpace Space => OverlaySpace.ScreenSpace;
+
+ [Dependency] private readonly IEntityManager _entManager = default!;
+ [Dependency] private readonly IEyeManager _eyeManager = default!;
+ [Dependency] private readonly IInputManager _inputManager = default!;
+ [Dependency] private readonly IResourceCache _cache = default!;
+ [Dependency] private readonly ITileDefinitionManager _tileDefManager = default!;
+
+ private BiomeSystem _biomes;
+ private SharedMapSystem _maps;
+
+ private Font _font;
+
+ public BiomeDebugOverlay()
+ {
+ IoCManager.InjectDependencies(this);
+
+ _biomes = _entManager.System<BiomeSystem>();
+ _maps = _entManager.System<SharedMapSystem>();
+
+ _font = new VectorFont(_cache.GetResource<FontResource>("/EngineFonts/NotoSans/NotoSans-Regular.ttf"), 12);
+ }
+
+ protected override bool BeforeDraw(in OverlayDrawArgs args)
+ {
+ var mapUid = _maps.GetMapOrInvalid(args.MapId);
+
+ return _entManager.HasComponent<BiomeComponent>(mapUid);
+ }
+
+ protected override void Draw(in OverlayDrawArgs args)
+ {
+ var mouseScreenPos = _inputManager.MouseScreenPosition;
+ var mousePos = _eyeManager.ScreenToMap(mouseScreenPos);
+
+ if (mousePos.MapId == MapId.Nullspace || mousePos.MapId != args.MapId)
+ return;
+
+ var mapUid = _maps.GetMapOrInvalid(args.MapId);
+
+ if (!_entManager.TryGetComponent(mapUid, out BiomeComponent? biomeComp) || !_entManager.TryGetComponent(mapUid, out MapGridComponent? grid))
+ return;
+
+ var sb = new StringBuilder();
+ var nodePos = _maps.WorldToTile(mapUid, grid, mousePos.Position);
+
+ if (_biomes.TryGetEntity(nodePos, biomeComp, (mapUid, grid), out var ent))
+ {
+ var text = $"Entity: {ent}";
+ sb.AppendLine(text);
+ }
+
+ if (_biomes.TryGetDecals(nodePos, biomeComp.Layers, biomeComp.Seed, (mapUid, grid), out var decals))
+ {
+ var text = $"Decals: {decals.Count}";
+ sb.AppendLine(text);
+
+ foreach (var decal in decals)
+ {
+ var decalText = $"- {decal.ID}";
+ sb.AppendLine(decalText);
+ }
+ }
+
+ if (_biomes.TryGetBiomeTile(nodePos, biomeComp.Layers, biomeComp.Seed, (mapUid, grid), out var tile))
+ {
+ var tileText = $"Tile: {_tileDefManager[tile.Value.TypeId].ID}";
+ sb.AppendLine(tileText);
+ }
+
+ args.ScreenHandle.DrawString(_font, mouseScreenPos.Position + new Vector2(0f, 32f), sb.ToString());
+ }
+}
--- /dev/null
+using Content.Shared.Parallax.Biomes;
+
+namespace Content.Client.Parallax;
+
+public sealed class BiomeSystem : SharedBiomeSystem
+{
+
+}
--- /dev/null
+using Robust.Client.Graphics;
+using Robust.Shared.Console;
+
+namespace Content.Client.Parallax.Commands;
+
+public sealed class ShowBiomeCommand : LocalizedCommands
+{
+ [Dependency] private readonly IOverlayManager _overlayMgr = default!;
+
+ public override string Command => "showbiome";
+ public override void Execute(IConsoleShell shell, string argStr, string[] args)
+ {
+ if (_overlayMgr.HasOverlay<BiomeDebugOverlay>())
+ {
+ _overlayMgr.RemoveOverlay<BiomeDebugOverlay>();
+ }
+ else
+ {
+ _overlayMgr.AddOverlay(new BiomeDebugOverlay());
+ }
+ }
+}
using System.Numerics;
using Content.Client.Parallax.Managers;
using Content.Shared.CCVar;
+using Content.Shared.Parallax.Biomes;
+using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly IParallaxManager _manager = default!;
+ private readonly SharedMapSystem _mapSystem;
private readonly ParallaxSystem _parallax;
public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowWorld;
{
ZIndex = ParallaxSystem.ParallaxZIndex;
IoCManager.InjectDependencies(this);
+ _mapSystem = _entManager.System<SharedMapSystem>();
_parallax = _entManager.System<ParallaxSystem>();
}
protected override bool BeforeDraw(in OverlayDrawArgs args)
{
- if (args.MapId == MapId.Nullspace)
+ if (args.MapId == MapId.Nullspace || _entManager.HasComponent<BiomeComponent>(_mapSystem.GetMapOrInvalid(args.MapId)))
return false;
return true;
+using System.Linq;
+using Content.Client.Computer;
using Content.Client.Stylesheets;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.CCVar;
+using Content.Shared.Parallax.Biomes;
+using Content.Shared.Procedural;
+using Content.Shared.Salvage;
+using Content.Shared.Salvage.Expeditions;
+using Content.Shared.Salvage.Expeditions.Modifiers;
+using Content.Shared.Shuttles.BUIStates;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Configuration;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
namespace Content.Client.Salvage.UI;
-using Content.Shared.Procedural;
+using Content.Shared.Parallax.Biomes.Markers;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
/// Mob layers to pick from.
/// </summary>
[DataField]
- public List<ProtoId<DungeonConfigPrototype>> MobLayers = new()
+ public List<ProtoId<BiomeMarkerLayerPrototype>> MobLayers = new()
{
"Carps",
"Xenos",
/// <summary>
/// Loot layers to pick from.
/// </summary>
- public List<ProtoId<DungeonConfigPrototype>> LootLayers = new()
+ public List<ProtoId<BiomeMarkerLayerPrototype>> LootLayers = new()
{
"OreIron",
"OreQuartz",
using System.Linq;
using Content.Server.Gateway.Components;
+using Content.Server.Parallax;
using Content.Server.Procedural;
using Content.Shared.CCVar;
using Content.Shared.Dataset;
using Content.Shared.Maps;
+using Content.Shared.Parallax.Biomes;
using Content.Shared.Procedural;
-using Content.Shared.Procedural.Components;
using Content.Shared.Salvage;
using Robust.Shared.Configuration;
using Robust.Shared.Map;
};
AddComp(mapUid, restricted);
- _biome.EnsurePlanet(mapUid, _protoManager.Index("BiomeGrasslands"), seed);
+ _biome.EnsurePlanet(mapUid, _protoManager.Index<BiomeTemplatePrototype>("Continental"), seed);
var grid = Comp<MapGridComponent>(mapUid);
var layer = lootLayers[layerIdx];
lootLayers.RemoveSwap(layerIdx);
- _biome.AddLayer((ent.Owner, biomeComp), $"{layer.Id}-{i}", layer.Id);
+ _biome.AddMarkerLayer(ent.Owner, biomeComp, layer.Id);
}
// - Mobs
var layer = mobLayers[layerIdx];
mobLayers.RemoveSwap(layerIdx);
- _biome.AddLayer((ent.Owner, biomeComp), $"{layer.Id}-{i}", layer.Id);
+ _biome.AddMarkerLayer(ent.Owner, biomeComp, layer.Id);
}
}
}
using System.Linq;
using Content.Server.Administration;
-using Content.Server.Procedural;
+using Content.Server.Atmos;
+using Content.Server.Atmos.Components;
+using Content.Server.Atmos.EntitySystems;
+using Content.Server.Parallax;
using Content.Shared.Administration;
-using Content.Shared.Procedural.Components;
+using Content.Shared.Atmos;
+using Content.Shared.Gravity;
+using Content.Shared.Movement.Components;
+using Content.Shared.Parallax.Biomes;
+using Robust.Shared.Audio;
using Robust.Shared.Console;
using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
namespace Content.Server.Maps;
return;
}
- if (!_protoManager.TryIndex<EntityPrototype>(args[1], out var biomeTemplate))
+ if (!_protoManager.TryIndex<BiomeTemplatePrototype>(args[1], out var biomeTemplate))
{
shell.WriteError(Loc.GetString("cmd-planet-map-prototype", ("prototype", args[1])));
return;
if (args.Length == 1)
return CompletionResult.FromHintOptions(CompletionHelper.MapIds(_entManager), "Map Id");
- var biomeName = _entManager.ComponentFactory.GetComponentName<BiomeComponent>();
-
if (args.Length == 2)
{
- var options = _protoManager.EnumeratePrototypes<EntityPrototype>()
- .Where(o => o.Components.ContainsKey(biomeName))
+ var options = _protoManager.EnumeratePrototypes<BiomeTemplatePrototype>()
.Select(o => new CompletionOption(o.ID, "Biome"));
return CompletionResult.FromOptions(options);
}
{
for (var y = -1; y <= 1; y++)
{
- if (x == 0 && y == 0)
- continue;
-
var neighbor = node + new Vector2i(x, y);
var neighborCost = OctileDistance(node, neighbor) * args.TileCost?.Invoke(neighbor) ?? 1f;
cameFrom[neighbor] = node;
costSoFar[neighbor] = gScore;
- var hScore = ManhattanDistance(args.End, neighbor);
+ // Still use octile even for manhattan distance.
+ var hScore = OctileDistance(args.End, neighbor) * 1.001f;
var fScore = gScore + hScore;
frontier.Enqueue(neighbor, fScore);
}
--- /dev/null
+using Content.Server.Administration;
+using Content.Shared.Administration;
+using Content.Shared.Parallax.Biomes;
+using Content.Shared.Parallax.Biomes.Layers;
+using Content.Shared.Parallax.Biomes.Markers;
+using Robust.Shared.Console;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+
+namespace Content.Server.Parallax;
+
+public sealed partial class BiomeSystem
+{
+ private void InitializeCommands()
+ {
+ _console.RegisterCommand("biome_clear", Loc.GetString("cmd-biome_clear-desc"), Loc.GetString("cmd-biome_clear-help"), BiomeClearCallback, BiomeClearCallbackHelper);
+ _console.RegisterCommand("biome_addlayer", Loc.GetString("cmd-biome_addlayer-desc"), Loc.GetString("cmd-biome_addlayer-help"), AddLayerCallback, AddLayerCallbackHelp);
+ _console.RegisterCommand("biome_addmarkerlayer", Loc.GetString("cmd-biome_addmarkerlayer-desc"), Loc.GetString("cmd-biome_addmarkerlayer-desc"), AddMarkerLayerCallback, AddMarkerLayerCallbackHelper);
+ }
+
+ [AdminCommand(AdminFlags.Fun)]
+ private void BiomeClearCallback(IConsoleShell shell, string argstr, string[] args)
+ {
+ if (args.Length != 1)
+ {
+ return;
+ }
+
+ int.TryParse(args[0], out var mapInt);
+ var mapId = new MapId(mapInt);
+ var mapUid = _mapSystem.GetMapOrInvalid(mapId);
+
+ if (_mapSystem.MapExists(mapId) ||
+ !TryComp<BiomeComponent>(mapUid, out var biome))
+ {
+ return;
+ }
+
+ ClearTemplate(mapUid, biome);
+ }
+
+ private CompletionResult BiomeClearCallbackHelper(IConsoleShell shell, string[] args)
+ {
+ if (args.Length == 1)
+ {
+ return CompletionResult.FromHintOptions(CompletionHelper.Components<BiomeComponent>(args[0], EntityManager), "Biome");
+ }
+
+ return CompletionResult.Empty;
+ }
+
+ [AdminCommand(AdminFlags.Fun)]
+ private void AddLayerCallback(IConsoleShell shell, string argstr, string[] args)
+ {
+ if (args.Length < 3 || args.Length > 4)
+ {
+ return;
+ }
+
+ if (!int.TryParse(args[0], out var mapInt))
+ {
+ return;
+ }
+
+ var mapId = new MapId(mapInt);
+ var mapUid = _mapSystem.GetMapOrInvalid(mapId);
+
+ if (!_mapSystem.MapExists(mapId) || !TryComp<BiomeComponent>(mapUid, out var biome))
+ {
+ return;
+ }
+
+ if (!ProtoManager.TryIndex<BiomeTemplatePrototype>(args[1], out var template))
+ {
+ return;
+ }
+
+ var offset = 0;
+
+ if (args.Length == 4)
+ {
+ int.TryParse(args[3], out offset);
+ }
+
+ AddTemplate(mapUid, biome, args[2], template, offset);
+ }
+
+ private CompletionResult AddLayerCallbackHelp(IConsoleShell shell, string[] args)
+ {
+ if (args.Length == 1)
+ {
+ return CompletionResult.FromHintOptions(CompletionHelper.MapIds(EntityManager), "Map ID");
+ }
+
+ if (args.Length == 2)
+ {
+ return CompletionResult.FromHintOptions(
+ CompletionHelper.PrototypeIDs<BiomeTemplatePrototype>(proto: ProtoManager), "Biome template");
+ }
+
+ if (args.Length == 3)
+ {
+ if (int.TryParse(args[0], out var mapInt))
+ {
+ var mapId = new MapId(mapInt);
+
+ if (TryComp<BiomeComponent>(_mapSystem.GetMapOrInvalid(mapId), out var biome))
+ {
+ var results = new List<string>();
+
+ foreach (var layer in biome.Layers)
+ {
+ if (layer is not BiomeDummyLayer dummy)
+ continue;
+
+ results.Add(dummy.ID);
+ }
+
+ return CompletionResult.FromHintOptions(results, "Dummy layer ID");
+ }
+ }
+ }
+
+ if (args.Length == 4)
+ {
+ return CompletionResult.FromHint("Seed offset");
+ }
+
+ return CompletionResult.Empty;
+ }
+
+ [AdminCommand(AdminFlags.Fun)]
+ private void AddMarkerLayerCallback(IConsoleShell shell, string argstr, string[] args)
+ {
+ if (args.Length != 2)
+ {
+ return;
+ }
+
+ if (!int.TryParse(args[0], out var mapInt))
+ {
+ return;
+ }
+
+ var mapId = new MapId(mapInt);
+
+ if (!_mapSystem.MapExists(mapId) || !TryComp<BiomeComponent>(_mapSystem.GetMapOrInvalid(mapId), out var biome))
+ {
+ return;
+ }
+
+ if (!ProtoManager.HasIndex<BiomeMarkerLayerPrototype>(args[1]))
+ {
+ return;
+ }
+
+ if (!biome.MarkerLayers.Add(args[1]))
+ {
+ return;
+ }
+
+ biome.ForcedMarkerLayers.Add(args[1]);
+ }
+
+ private CompletionResult AddMarkerLayerCallbackHelper(IConsoleShell shell, string[] args)
+ {
+ if (args.Length == 1)
+ {
+ var allQuery = AllEntityQuery<MapComponent, BiomeComponent>();
+ var options = new List<CompletionOption>();
+
+ while (allQuery.MoveNext(out var mapComp, out _))
+ {
+ options.Add(new CompletionOption(mapComp.MapId.ToString()));
+ }
+
+ return CompletionResult.FromHintOptions(options, "Biome");
+ }
+
+ if (args.Length == 2)
+ {
+ return CompletionResult.FromHintOptions(
+ CompletionHelper.PrototypeIDs<BiomeMarkerLayerPrototype>(proto: ProtoManager), "Marker");
+ }
+
+ return CompletionResult.Empty;
+ }
+}
--- /dev/null
+using System.Linq;
+using System.Numerics;
+using System.Threading.Tasks;
+using Content.Server.Atmos;
+using Content.Server.Atmos.Components;
+using Content.Server.Atmos.EntitySystems;
+using Content.Server.Decals;
+using Content.Server.Ghost.Roles.Components;
+using Content.Server.Shuttles.Events;
+using Content.Server.Shuttles.Systems;
+using Content.Shared.Atmos;
+using Content.Shared.Decals;
+using Content.Shared.Ghost;
+using Content.Shared.Gravity;
+using Content.Shared.Light.Components;
+using Content.Shared.Parallax.Biomes;
+using Content.Shared.Parallax.Biomes.Layers;
+using Content.Shared.Parallax.Biomes.Markers;
+using Content.Shared.Tag;
+using Microsoft.Extensions.ObjectPool;
+using Robust.Server.Player;
+using Robust.Shared;
+using Robust.Shared.Collections;
+using Robust.Shared.Configuration;
+using Robust.Shared.Console;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Physics;
+using Robust.Shared.Physics.Systems;
+using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+using Robust.Shared.Threading;
+using Robust.Shared.Utility;
+using ChunkIndicesEnumerator = Robust.Shared.Map.Enumerators.ChunkIndicesEnumerator;
+
+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 IParallelManager _parallel = default!;
+ [Dependency] private readonly IPrototypeManager _proto = default!;
+ [Dependency] private readonly IPlayerManager _playerManager = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly AtmosphereSystem _atmos = default!;
+ [Dependency] private readonly DecalSystem _decals = default!;
+ [Dependency] private readonly SharedMapSystem _mapSystem = default!;
+ [Dependency] private readonly SharedPhysicsSystem _physics = default!;
+ [Dependency] private readonly SharedTransformSystem _transform = default!;
+ [Dependency] private readonly ShuttleSystem _shuttles = default!;
+ [Dependency] private readonly TagSystem _tags = default!;
+
+ private EntityQuery<BiomeComponent> _biomeQuery;
+ private EntityQuery<FixturesComponent> _fixturesQuery;
+ private EntityQuery<GhostComponent> _ghostQuery;
+ private EntityQuery<TransformComponent> _xformQuery;
+
+ private readonly HashSet<EntityUid> _handledEntities = new();
+ private const float DefaultLoadRange = 16f;
+ private float _loadRange = DefaultLoadRange;
+ private static readonly ProtoId<TagPrototype> AllowBiomeLoadingTag = "AllowBiomeLoading";
+
+ private List<(Vector2i, Tile)> _tiles = new();
+
+ private ObjectPool<HashSet<Vector2i>> _tilePool =
+ new DefaultObjectPool<HashSet<Vector2i>>(new SetPolicy<Vector2i>(), 256);
+
+ /// <summary>
+ /// Load area for chunks containing tiles, decals etc.
+ /// </summary>
+ private Box2 _loadArea = new(-DefaultLoadRange, -DefaultLoadRange, DefaultLoadRange, DefaultLoadRange);
+
+ /// <summary>
+ /// Stores the chunks active for this tick temporarily.
+ /// </summary>
+ private readonly Dictionary<BiomeComponent, HashSet<Vector2i>> _activeChunks = new();
+
+ private readonly Dictionary<BiomeComponent,
+ Dictionary<string, HashSet<Vector2i>>> _markerChunks = new();
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ Log.Level = LogLevel.Debug;
+ _biomeQuery = GetEntityQuery<BiomeComponent>();
+ _fixturesQuery = GetEntityQuery<FixturesComponent>();
+ _ghostQuery = GetEntityQuery<GhostComponent>();
+ _xformQuery = GetEntityQuery<TransformComponent>();
+ SubscribeLocalEvent<BiomeComponent, MapInitEvent>(OnBiomeMapInit);
+ SubscribeLocalEvent<FTLStartedEvent>(OnFTLStarted);
+ SubscribeLocalEvent<ShuttleFlattenEvent>(OnShuttleFlatten);
+ Subs.CVar(_configManager, CVars.NetMaxUpdateRange, SetLoadRange, true);
+ InitializeCommands();
+ SubscribeLocalEvent<PrototypesReloadedEventArgs>(ProtoReload);
+ }
+
+ private void ProtoReload(PrototypesReloadedEventArgs obj)
+ {
+ if (!obj.ByType.TryGetValue(typeof(BiomeTemplatePrototype), out var reloads))
+ return;
+
+ var query = AllEntityQuery<BiomeComponent>();
+
+ while (query.MoveNext(out var uid, out var biome))
+ {
+ if (biome.Template == null || !reloads.Modified.TryGetValue(biome.Template, out var proto))
+ continue;
+
+ SetTemplate(uid, 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 OnBiomeMapInit(EntityUid uid, BiomeComponent component, MapInitEvent args)
+ {
+ if (component.Seed == -1)
+ {
+ SetSeed(uid, component, _random.Next());
+ }
+
+ if (_proto.TryIndex(component.Template, out var biome))
+ SetTemplate(uid, component, biome);
+
+ var xform = Transform(uid);
+ var mapId = xform.MapID;
+
+ if (mapId != MapId.Nullspace && HasComp<MapGridComponent>(uid))
+ {
+ var setTiles = new List<(Vector2i Index, Tile tile)>();
+
+ foreach (var grid in _mapManager.GetAllGrids(mapId))
+ {
+ if (!_fixturesQuery.TryGetComponent(grid.Owner, out var fixtures))
+ continue;
+
+ // Don't want shuttles flying around now do we.
+ _shuttles.Disable(grid.Owner);
+ var pTransform = _physics.GetPhysicsTransform(grid.Owner);
+
+ foreach (var fixture in fixtures.Fixtures.Values)
+ {
+ for (var i = 0; i < fixture.Shape.ChildCount; i++)
+ {
+ var aabb = fixture.Shape.ComputeAABB(pTransform, i);
+
+ setTiles.Clear();
+ ReserveTiles(uid, aabb, setTiles);
+ }
+ }
+ }
+ }
+ }
+
+ public void SetEnabled(Entity<BiomeComponent?> ent, bool enabled = true)
+ {
+ if (!Resolve(ent, ref ent.Comp) || ent.Comp.Enabled == enabled)
+ return;
+
+ ent.Comp.Enabled = enabled;
+ Dirty(ent, ent.Comp);
+ }
+
+ public void SetSeed(EntityUid uid, BiomeComponent component, int seed, bool dirty = true)
+ {
+ component.Seed = seed;
+
+ if (dirty)
+ Dirty(uid, component);
+ }
+
+ public void ClearTemplate(EntityUid uid, BiomeComponent component, bool dirty = true)
+ {
+ component.Layers.Clear();
+ component.Template = null;
+
+ if (dirty)
+ Dirty(uid, component);
+ }
+
+ /// <summary>
+ /// Sets the <see cref="BiomeComponent.Template"/> and refreshes layers.
+ /// </summary>
+ public void SetTemplate(EntityUid uid, BiomeComponent component, BiomeTemplatePrototype template, bool dirty = true)
+ {
+ component.Layers.Clear();
+ component.Template = template.ID;
+
+ foreach (var layer in template.Layers)
+ {
+ component.Layers.Add(layer);
+ }
+
+ if (dirty)
+ Dirty(uid, component);
+ }
+
+ /// <summary>
+ /// Adds the specified layer at the specified marker if it exists.
+ /// </summary>
+ public void AddLayer(EntityUid uid, 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(uid, component);
+ }
+
+ public void AddMarkerLayer(EntityUid uid, BiomeComponent component, string marker)
+ {
+ component.MarkerLayers.Add(marker);
+ Dirty(uid, component);
+ }
+
+ /// <summary>
+ /// Adds the specified template at the specified marker if it exists, withour overriding every layer.
+ /// </summary>
+ public void AddTemplate(EntityUid uid, 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(uid, component);
+ }
+
+ private void OnFTLStarted(ref FTLStartedEvent ev)
+ {
+ var targetMap = _transform.ToMapCoordinates(ev.TargetCoordinates);
+ var targetMapUid = _mapSystem.GetMapOrInvalid(targetMap.MapId);
+
+ if (!TryComp<BiomeComponent>(targetMapUid, out var biome))
+ return;
+
+ var preloadArea = new Vector2(32f, 32f);
+ var targetArea = new Box2(targetMap.Position - preloadArea, targetMap.Position + preloadArea);
+ Preload(targetMapUid, biome, targetArea);
+ }
+
+ private void OnShuttleFlatten(ref ShuttleFlattenEvent ev)
+ {
+ if (!TryComp<BiomeComponent>(ev.MapUid, out var biome) ||
+ !TryComp<MapGridComponent>(ev.MapUid, out var grid))
+ {
+ return;
+ }
+
+ var tiles = new List<(Vector2i Index, Tile Tile)>();
+
+ foreach (var aabb in ev.AABBs)
+ {
+ for (var x = Math.Floor(aabb.Left); x <= Math.Ceiling(aabb.Right); x++)
+ {
+ for (var y = Math.Floor(aabb.Bottom); y <= Math.Ceiling(aabb.Top); y++)
+ {
+ var index = new Vector2i((int)x, (int)y);
+ var chunk = SharedMapSystem.GetChunkIndices(index, ChunkSize);
+
+ var mod = biome.ModifiedTiles.GetOrNew(chunk * ChunkSize);
+
+ if (!mod.Add(index) || !TryGetBiomeTile(index, biome.Layers, biome.Seed, (ev.MapUid, grid), out var tile))
+ continue;
+
+ // If we flag it as modified then the tile is never set so need to do it ourselves.
+ tiles.Add((index, tile.Value));
+ }
+ }
+ }
+
+ _mapSystem.SetTiles(ev.MapUid, grid, tiles);
+ }
+
+ /// <summary>
+ /// Preloads biome for the specified area.
+ /// </summary>
+ 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 = ProtoManager.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);
+ }
+ }
+ }
+
+ private bool CanLoad(EntityUid uid)
+ {
+ return !_ghostQuery.HasComp(uid) || _tags.HasTag(uid, AllowBiomeLoadingTag);
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+ var biomes = AllEntityQuery<BiomeComponent>();
+
+ while (biomes.MoveNext(out var biome))
+ {
+ if (biome.LifeStage < ComponentLifeStage.Running)
+ continue;
+
+ _activeChunks.Add(biome, _tilePool.Get());
+ _markerChunks.GetOrNew(biome);
+ }
+
+ // Get chunks in range
+ foreach (var pSession in Filter.GetAllPlayers(_playerManager))
+ {
+ if (_xformQuery.TryGetComponent(pSession.AttachedEntity, out var xform) &&
+ _handledEntities.Add(pSession.AttachedEntity.Value) &&
+ _biomeQuery.TryGetComponent(xform.MapUid, out var biome) &&
+ biome.Enabled &&
+ CanLoad(pSession.AttachedEntity.Value))
+ {
+ var worldPos = _transform.GetWorldPosition(xform);
+ AddChunksInRange(biome, worldPos);
+
+ foreach (var layer in biome.MarkerLayers)
+ {
+ var layerProto = ProtoManager.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) ||
+ !biome.Enabled ||
+ !CanLoad(viewer))
+ {
+ continue;
+ }
+
+ var worldPos = _transform.GetWorldPosition(xform);
+ AddChunksInRange(biome, worldPos);
+
+ foreach (var layer in biome.MarkerLayers)
+ {
+ var layerProto = ProtoManager.Index(layer);
+ AddMarkerChunksInRange(biome, worldPos, layerProto);
+ }
+ }
+ }
+
+ var loadBiomes = AllEntityQuery<BiomeComponent, MapGridComponent>();
+
+ while (loadBiomes.MoveNext(out var gridUid, out var biome, out var grid))
+ {
+ // If not MapInit don't run it.
+ if (biome.LifeStage < ComponentLifeStage.Running)
+ continue;
+
+ if (!biome.Enabled)
+ continue;
+
+ // Load new chunks
+ LoadChunks(biome, gridUid, grid, biome.Seed);
+ // Unload old chunks
+ UnloadChunks(biome, gridUid, grid, biome.Seed);
+ }
+
+ _handledEntities.Clear();
+
+ foreach (var tiles in _activeChunks.Values)
+ {
+ _tilePool.Return(tiles);
+ }
+
+ _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 halfLayer = new Vector2(layer.Size / 2f);
+
+ var enumerator = new ChunkIndicesEnumerator(loadArea.Translated(worldPos - halfLayer), layer.Size);
+
+ while (enumerator.MoveNext(out var chunkOrigin))
+ {
+ var lay = _markerChunks[biome].GetOrNew(layer.ID);
+ lay.Add(chunkOrigin.Value * layer.Size);
+ }
+ }
+
+ #region Load
+
+ /// <summary>
+ /// Loads all of the chunks for a particular biome, as well as handle any marker chunks.
+ /// </summary>
+ private void LoadChunks(
+ BiomeComponent component,
+ EntityUid gridUid,
+ MapGridComponent grid,
+ int seed)
+ {
+ BuildMarkerChunks(component, gridUid, grid, seed);
+
+ var active = _activeChunks[component];
+
+ foreach (var chunk in active)
+ {
+ LoadChunkMarkers(component, gridUid, grid, chunk, seed);
+
+ if (!component.LoadedChunks.Add(chunk))
+ continue;
+
+ // Load NOW!
+ LoadChunk(component, gridUid, grid, chunk, seed);
+ }
+ }
+
+ /// <summary>
+ /// Goes through all marker chunks that haven't been calculated, then calculates what spawns there are and
+ /// allocates them to the relevant actual chunks in the biome (marker chunks may be many times larger than biome chunks).
+ /// </summary>
+ private void BuildMarkerChunks(BiomeComponent component, EntityUid gridUid, MapGridComponent grid, int seed)
+ {
+ var markers = _markerChunks[component];
+ var loadedMarkers = component.LoadedMarkers;
+ var idx = 0;
+
+ foreach (var (layer, chunks) in markers)
+ {
+ // I know dictionary ordering isn't guaranteed but I just need something to differentiate seeds.
+ idx++;
+ var localIdx = idx;
+
+ Parallel.ForEach(chunks, new ParallelOptions() { MaxDegreeOfParallelism = _parallel.ParallelProcessCount }, chunk =>
+ {
+ if (loadedMarkers.TryGetValue(layer, out var mobChunks) && mobChunks.Contains(chunk))
+ return;
+
+ var forced = component.ForcedMarkerLayers.Contains(layer);
+
+ // Make a temporary version and copy back in later.
+ var pending = new Dictionary<Vector2i, Dictionary<string, List<Vector2i>>>();
+
+ // Essentially get the seed + work out a buffer to adjacent chunks so we don't
+ // inadvertantly spawn too many near the edges.
+ var layerProto = ProtoManager.Index<BiomeMarkerLayerPrototype>(layer);
+ var markerSeed = seed + chunk.X * ChunkSize + chunk.Y + localIdx;
+ var rand = new Random(markerSeed);
+ var buffer = (int)(layerProto.Radius / 2f);
+ var bounds = new Box2i(chunk + buffer, chunk + layerProto.Size - buffer);
+ var count = (int)(bounds.Area / (layerProto.Radius * layerProto.Radius));
+ count = Math.Min(count, layerProto.MaxCount);
+
+ GetMarkerNodes(gridUid, component, grid, layerProto, forced, bounds, count, rand,
+ out var spawnSet, out var existing);
+
+ // Forcing markers to spawn so delete any that were found to be in the way.
+ if (forced && existing.Count > 0)
+ {
+ // Lock something so we can delete these safely.
+ lock (component.PendingMarkers)
+ {
+ foreach (var ent in existing)
+ {
+ Del(ent);
+ }
+ }
+ }
+
+ foreach (var node in spawnSet.Keys)
+ {
+ var chunkOrigin = SharedMapSystem.GetChunkIndices(node, 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;
+ }
+
+ layerMarkers.Add(node);
+ }
+
+ lock (loadedMarkers)
+ {
+ 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;
+ }
+ }
+ }
+ });
+ }
+
+ component.ForcedMarkerLayers.Clear();
+ }
+
+ /// <summary>
+ /// Gets the marker nodes for the specified area.
+ /// </summary>
+ /// <param name="emptyTiles">Should we include empty tiles when determine markers (e.g. if they are yet to be loaded)</param>
+ public void GetMarkerNodes(
+ EntityUid gridUid,
+ BiomeComponent biome,
+ MapGridComponent grid,
+ BiomeMarkerLayerPrototype layerProto,
+ bool forced,
+ Box2i bounds,
+ int count,
+ Random rand,
+ out Dictionary<Vector2i, string?> spawnSet,
+ out HashSet<EntityUid> existingEnts,
+ bool emptyTiles = true)
+ {
+ DebugTools.Assert(count > 0);
+ var remainingTiles = _tilePool.Get();
+ var nodeEntities = new Dictionary<Vector2i, EntityUid?>();
+ var nodeMask = new Dictionary<Vector2i, string?>();
+
+ // Okay so originally we picked a random tile and BFS outwards
+ // the problem is if you somehow get a cooked frontier then it might drop entire veins
+ // hence we'll grab all valid tiles up front and use that as possible seeds.
+ // It's hella more expensive but stops issues.
+ for (var x = bounds.Left; x < bounds.Right; x++)
+ {
+ for (var y = bounds.Bottom; y < bounds.Top; y++)
+ {
+ var node = new Vector2i(x, y);
+
+ // Empty tile, skip if relevant.
+ if (!emptyTiles && (!_mapSystem.TryGetTile(grid, node, out var tile) || tile.IsEmpty))
+ continue;
+
+ // Check if it's a valid spawn, if so then use it.
+ var enumerator = _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, node);
+ enumerator.MoveNext(out var existing);
+
+ if (!forced && existing != null)
+ continue;
+
+ // Check if mask matches // anything blocking.
+ TryGetEntity(node, biome, (gridUid, grid), out var proto);
+
+ // If there's an existing entity and it doesn't match the mask then skip.
+ if (layerProto.EntityMask.Count > 0 &&
+ (proto == null ||
+ !layerProto.EntityMask.ContainsKey(proto)))
+ {
+ continue;
+ }
+
+ // If it's just a flat spawn then just check for anything blocking.
+ if (proto != null && layerProto.Prototype != null)
+ {
+ continue;
+ }
+
+ DebugTools.Assert(layerProto.EntityMask.Count == 0 || !string.IsNullOrEmpty(proto));
+ remainingTiles.Add(node);
+ nodeEntities.Add(node, existing);
+ nodeMask.Add(node, proto);
+ }
+ }
+
+ var frontier = new ValueList<Vector2i>(32);
+ // TODO: Need poisson but crashes whenever I use moony's due to inputs or smth idk
+ // Get the total amount of groups to spawn across the entire chunk.
+ // We treat a null entity mask as requiring nothing else on the tile
+
+ spawnSet = new Dictionary<Vector2i, string?>();
+ existingEnts = new HashSet<EntityUid>();
+
+ // Iterate the group counts and pathfind out each group.
+ for (var i = 0; i < count; i++)
+ {
+ var groupSize = rand.Next(layerProto.MinGroupSize, layerProto.MaxGroupSize + 1);
+
+ // While we have remaining tiles keep iterating
+ while (groupSize > 0 && remainingTiles.Count > 0)
+ {
+ var startNode = rand.PickAndTake(remainingTiles);
+ frontier.Clear();
+ frontier.Add(startNode);
+
+ // This essentially may lead to a vein being split in multiple areas but the count matters more than position.
+ while (frontier.Count > 0 && groupSize > 0)
+ {
+ // Need to pick a random index so we don't just get straight lines of ores.
+ var frontierIndex = rand.Next(frontier.Count);
+ var node = frontier[frontierIndex];
+ frontier.RemoveSwap(frontierIndex);
+ remainingTiles.Remove(node);
+
+ // Add neighbors if they're valid, worst case we add no more and pick another random seed tile.
+ for (var x = -1; x <= 1; x++)
+ {
+ for (var y = -1; y <= 1; y++)
+ {
+ var neighbor = new Vector2i(node.X + x, node.Y + y);
+
+ if (frontier.Contains(neighbor) || !remainingTiles.Contains(neighbor))
+ continue;
+
+ frontier.Add(neighbor);
+ }
+ }
+
+ // Tile valid salad so add it.
+ var mask = nodeMask[node];
+ spawnSet.Add(node, mask);
+ groupSize--;
+
+ if (nodeEntities.TryGetValue(node, out var existing))
+ {
+ Del(existing);
+ }
+ }
+ }
+
+ if (groupSize > 0)
+ {
+ Log.Warning($"Found remaining group size for ore veins!");
+ }
+ }
+
+ _tilePool.Return(remainingTiles);
+ }
+
+ /// <summary>
+ /// Loads the pre-deteremined marker nodes for a particular chunk.
+ /// This is calculated in <see cref="BuildMarkerChunks"/>
+ /// </summary>
+ /// <remarks>
+ /// Note that the marker chunks do not correspond to this chunk.
+ /// </remarks>
+ private void LoadChunkMarkers(
+ BiomeComponent component,
+ EntityUid gridUid,
+ MapGridComponent grid,
+ Vector2i chunk,
+ int seed)
+ {
+ // Load any pending marker tiles first.
+ if (!component.PendingMarkers.TryGetValue(chunk, out var layers))
+ return;
+
+ // This needs to be done separately in case we try to add a marker layer and want to force it on existing
+ // loaded chunks.
+ component.ModifiedTiles.TryGetValue(chunk, out var modified);
+ modified ??= _tilePool.Get();
+
+ foreach (var (layer, nodes) in layers)
+ {
+ var layerProto = ProtoManager.Index<BiomeMarkerLayerPrototype>(layer);
+
+ foreach (var node in nodes)
+ {
+ if (modified.Contains(node))
+ continue;
+
+ // Need to ensure the tile under it has loaded for anchoring.
+ if (TryGetBiomeTile(node, component.Layers, seed, (gridUid, grid), out var tile))
+ {
+ _mapSystem.SetTile(gridUid, grid, node, tile.Value);
+ }
+
+ string? prototype;
+
+ if (TryGetEntity(node, component, (gridUid, grid), out var proto) &&
+ layerProto.EntityMask.TryGetValue(proto, out var maskedProto))
+ {
+ prototype = maskedProto;
+ }
+ else
+ {
+ prototype = layerProto.Prototype;
+ }
+
+ // 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(prototype, _mapSystem.GridTileToLocal(gridUid, grid, node));
+ RemComp<GhostTakeoverAvailableComponent>(uid);
+ RemComp<GhostRoleComponent>(uid);
+ EntityManager.InitializeAndStartEntity(uid);
+ modified.Add(node);
+ }
+ }
+
+ if (modified.Count == 0)
+ {
+ component.ModifiedTiles.Remove(chunk);
+ _tilePool.Return(modified);
+ }
+
+ component.PendingMarkers.Remove(chunk);
+ }
+
+ /// <summary>
+ /// Loads a particular queued chunk for a biome.
+ /// </summary>
+ private void LoadChunk(
+ BiomeComponent component,
+ EntityUid gridUid,
+ MapGridComponent grid,
+ Vector2i chunk,
+ int seed)
+ {
+ component.ModifiedTiles.TryGetValue(chunk, out var modified);
+ modified ??= _tilePool.Get();
+ _tiles.Clear();
+
+ // 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);
+
+ // Pass in null so we don't try to get the tileref.
+ if (modified.Contains(indices))
+ continue;
+
+ // If there's existing data then don't overwrite it.
+ if (_mapSystem.TryGetTileRef(gridUid, grid, indices, out var tileRef) && !tileRef.Tile.IsEmpty)
+ continue;
+
+ if (!TryGetBiomeTile(indices, component.Layers, seed, (gridUid, grid), out var biomeTile))
+ continue;
+
+ _tiles.Add((indices, biomeTile.Value));
+ }
+ }
+
+ _mapSystem.SetTiles(gridUid, grid, _tiles);
+ _tiles.Clear();
+
+ // Now do entities
+ var loadedEntities = new Dictionary<EntityUid, Vector2i>();
+ 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 = _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, indices);
+
+ if (anchored.MoveNext(out _) || !TryGetEntity(indices, component, (gridUid, grid), out var entPrototype))
+ continue;
+
+ // TODO: Fix non-anchored ents spawning.
+ // Just track loaded chunks for now.
+ var ent = Spawn(entPrototype, _mapSystem.GridTileToLocal(gridUid, grid, 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, indices);
+ }
+ }
+
+ // Decals
+ var loadedDecals = new Dictionary<uint, Vector2i>();
+ 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 = _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, indices);
+
+ if (anchored.MoveNext(out _) || !TryGetDecals(indices, component.Layers, seed, (gridUid, 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)
+ {
+ _tilePool.Return(modified);
+ component.ModifiedTiles.Remove(chunk);
+ }
+ else
+ {
+ component.ModifiedTiles[chunk] = modified;
+ }
+ }
+
+ #endregion
+
+ #region Unload
+
+ /// <summary>
+ /// Handles all of the queued chunk unloads for a particular biome.
+ /// </summary>
+ private void UnloadChunks(BiomeComponent component, EntityUid gridUid, MapGridComponent grid, int seed)
+ {
+ 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, seed, tiles);
+ }
+ }
+
+ /// <summary>
+ /// Unloads a specific biome chunk.
+ /// </summary>
+ private void UnloadChunk(BiomeComponent component, EntityUid gridUid, MapGridComponent grid, Vector2i chunk, int seed, List<(Vector2i, Tile)> tiles)
+ {
+ // Reverse order to loading
+ component.ModifiedTiles.TryGetValue(chunk, out var modified);
+ modified ??= new HashSet<Vector2i>();
+
+ // 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
+ // 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
+ var xformQuery = GetEntityQuery<TransformComponent>();
+
+ foreach (var (ent, tile) in component.LoadedEntities[chunk])
+ {
+ if (Deleted(ent) || !xformQuery.TryGetComponent(ent, out var xform))
+ {
+ modified.Add(tile);
+ continue;
+ }
+
+ // It's moved
+ var entTile = _mapSystem.LocalToTile(gridUid, grid, xform.Coordinates);
+
+ if (!xform.Anchored || entTile != tile)
+ {
+ modified.Add(tile);
+ continue;
+ }
+
+ if (!EntityManager.IsDefault(ent))
+ {
+ modified.Add(tile);
+ continue;
+ }
+
+ Del(ent);
+ }
+
+ 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 = _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, indices);
+
+ if (anchored.MoveNext(out _))
+ {
+ modified.Add(indices);
+ continue;
+ }
+
+ // If it's default data unload the tile.
+ if (!TryGetBiomeTile(indices, component.Layers, seed, null, out var biomeTile) ||
+ _mapSystem.TryGetTileRef(gridUid, grid, indices, out var tileRef) && tileRef.Tile != biomeTile.Value)
+ {
+ modified.Add(indices);
+ continue;
+ }
+
+ tiles.Add((indices, Tile.Empty));
+ }
+ }
+
+ _mapSystem.SetTiles(gridUid, grid, tiles);
+ tiles.Clear();
+ component.LoadedChunks.Remove(chunk);
+
+ if (modified.Count == 0)
+ {
+ component.ModifiedTiles.Remove(chunk);
+ }
+ else
+ {
+ component.ModifiedTiles[chunk] = modified;
+ }
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Creates a simple planet setup for a map.
+ /// </summary>
+ public void EnsurePlanet(EntityUid mapUid, BiomeTemplatePrototype biomeTemplate, int? seed = null, MetaDataComponent? metadata = null, Color? mapLight = null)
+ {
+ if (!Resolve(mapUid, ref metadata))
+ return;
+
+ EnsureComp<MapGridComponent>(mapUid);
+ var biome = EntityManager.ComponentFactory.GetComponent<BiomeComponent>();
+ seed ??= _random.Next();
+ SetSeed(mapUid, biome, seed.Value, false);
+ SetTemplate(mapUid, biome, biomeTemplate, false);
+ AddComp(mapUid, biome, true);
+ Dirty(mapUid, biome, metadata);
+
+ var gravity = EnsureComp<GravityComponent>(mapUid);
+ gravity.Enabled = true;
+ gravity.Inherent = true;
+ Dirty(mapUid, gravity, metadata);
+
+ // Day lighting
+ // Daylight: #D8B059
+ // Midday: #E6CB8B
+ // Moonlight: #2b3143
+ // Lava: #A34931
+ var light = EnsureComp<MapLightComponent>(mapUid);
+ light.AmbientLightColor = mapLight ?? Color.FromHex("#D8B059");
+ Dirty(mapUid, light, metadata);
+
+ EnsureComp<RoofComponent>(mapUid);
+
+ EnsureComp<LightCycleComponent>(mapUid);
+
+ EnsureComp<SunShadowComponent>(mapUid);
+ EnsureComp<SunShadowCycleComponent>(mapUid);
+
+ var moles = new float[Atmospherics.AdjustedNumberOfGases];
+ moles[(int)Gas.Oxygen] = 21.824779f;
+ moles[(int)Gas.Nitrogen] = 82.10312f;
+
+ var mixture = new GasMixture(moles, Atmospherics.T20C);
+
+ _atmos.SetMapAtmosphere(mapUid, false, mixture);
+ }
+
+ /// <summary>
+ /// Sets the specified tiles as relevant and marks them as modified.
+ /// </summary>
+ public void ReserveTiles(EntityUid mapUid, Box2 bounds, List<(Vector2i Index, Tile Tile)> tiles, BiomeComponent? biome = null, MapGridComponent? mapGrid = null)
+ {
+ if (!Resolve(mapUid, ref biome, ref mapGrid, false))
+ return;
+
+ foreach (var tileSet in _mapSystem.GetLocalTilesIntersecting(mapUid, mapGrid, bounds, false))
+ {
+ Vector2i chunkOrigin;
+ HashSet<Vector2i> modified;
+
+ // Existing, ignore
+ if (_mapSystem.TryGetTileRef(mapUid, mapGrid, tileSet.GridIndices, out var existingRef) && !existingRef.Tile.IsEmpty)
+ {
+ chunkOrigin = SharedMapSystem.GetChunkIndices(tileSet.GridIndices, ChunkSize) * ChunkSize;
+ modified = biome.ModifiedTiles.GetOrNew(chunkOrigin);
+ modified.Add(tileSet.GridIndices);
+ continue;
+ }
+
+ if (!TryGetBiomeTile(tileSet.GridIndices, biome.Layers, biome.Seed, (mapUid, mapGrid), out var tile))
+ {
+ continue;
+ }
+
+ chunkOrigin = SharedMapSystem.GetChunkIndices(tileSet.GridIndices, ChunkSize) * ChunkSize;
+ modified = biome.ModifiedTiles.GetOrNew(chunkOrigin);
+ modified.Add(tileSet.GridIndices);
+ tiles.Add((tileSet.GridIndices, tile.Value));
+ }
+
+ _mapSystem.SetTiles(mapUid, mapGrid, tiles);
+ }
+}
+++ /dev/null
-using Content.Server.Administration;
-using Content.Shared.Administration;
-using Content.Shared.Procedural;
-using Content.Shared.Procedural.Components;
-using Robust.Shared.Console;
-using Robust.Shared.Map;
-using Robust.Shared.Prototypes;
-
-namespace Content.Server.Procedural;
-
-public sealed partial class BiomeSystem
-{
- [AdminCommand(AdminFlags.Mapping)]
- public sealed class BiomeAddLayerCommand : LocalizedEntityCommands
- {
- [Dependency] private readonly IPrototypeManager _protoManager = default!;
-
- public override string Command => "biome_addlayer";
- public override void Execute(IConsoleShell shell, string argStr, string[] args)
- {
- if (args.Length != 3)
- {
- shell.WriteError(Help);
- return;
- }
-
- if (!int.TryParse(args[0], out var entInt))
- {
- return;
- }
-
- var entId = new EntityUid(entInt);
-
- if (!EntityManager.TryGetComponent(entId, out BiomeComponent? biome))
- {
- return;
- }
-
- if (!_protoManager.TryIndex(args[2], out DungeonConfigPrototype? config))
- {
- return;
- }
-
- var system = EntityManager.System<BiomeSystem>();
- system.AddLayer((entId, biome), args[1], config);
- }
-
- public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
- {
- switch (args.Length)
- {
- case 1:
- return CompletionResult.FromOptions(CompletionHelper.Components<BiomeComponent>(args[0]));
- case 2:
- return CompletionResult.FromHint("layerId");
- case 3:
- return CompletionResult.FromOptions(CompletionHelper.PrototypeIDs<DungeonConfigPrototype>());
- }
-
- return CompletionResult.Empty;
- }
- }
-
- [AdminCommand(AdminFlags.Mapping)]
- public sealed class BiomeRemoveLayerCommand : LocalizedEntityCommands
- {
- public override string Command => "biome_removelayer";
- public override void Execute(IConsoleShell shell, string argStr, string[] args)
- {
- if (args.Length != 2)
- {
- shell.WriteError(Help);
- return;
- }
-
- if (!int.TryParse(args[0], out var entInt))
- {
- return;
- }
-
- var entId = new EntityUid(entInt);
-
- if (!EntityManager.TryGetComponent(entId, out BiomeComponent? biome))
- {
- return;
- }
-
- var system = EntityManager.System<BiomeSystem>();
- system.RemoveLayer((entId, biome), args[1]);
- }
-
- public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
- {
- switch (args.Length)
- {
- case 0:
- return CompletionResult.FromOptions(CompletionHelper.Components<BiomeComponent>(args[0]));
- case 1:
- return CompletionResult.FromHint("layerId");
- }
-
- return CompletionResult.Empty;
- }
- }
-}
+++ /dev/null
-using Content.Shared.Atmos;
-using Content.Shared.Gravity;
-using Content.Shared.Light.Components;
-using Content.Shared.Procedural.Components;
-using Robust.Shared.Map.Components;
-using Robust.Shared.Prototypes;
-
-namespace Content.Server.Procedural;
-
-public sealed partial class BiomeSystem
-{
- /// <summary>
- /// Copies the biomecomponent to the specified map.
- /// </summary>
- public BiomeComponent? AddBiome(Entity<BiomeComponent?> mapUid, EntProtoId biomeTemplate, int? seed = null)
- {
- if (!_protomanager.Index(biomeTemplate).Components.TryGetComponent(Factory.GetComponentName<BiomeComponent>(), out var template))
- {
- return null;
- }
-
- var biome = Factory.GetComponent<BiomeComponent>();
- var biomeObj = (object)biome;
- _serManager.CopyTo(template, ref biomeObj, notNullableOverride: true);
- seed ??= _random.Next();
- biome.Seed = seed.Value;
- AddComp(mapUid, biome, true);
- return biome;
- }
-
- /// <summary>
- /// Creates a simple planet setup for a map.
- /// </summary>
- public void EnsurePlanet(EntityUid mapUid, EntProtoId biomeTemplate, int? seed = null, MetaDataComponent? metadata = null, Color? mapLight = null)
- {
- if (!Resolve(mapUid, ref metadata))
- return;
-
- EnsureComp<MapGridComponent>(mapUid);
- AddBiome(mapUid, biomeTemplate, seed);
- var gravity = EnsureComp<GravityComponent>(mapUid);
- gravity.Enabled = true;
- gravity.Inherent = true;
- Dirty(mapUid, gravity, metadata);
-
- var light = EnsureComp<MapLightComponent>(mapUid);
- light.AmbientLightColor = mapLight ?? Color.FromHex("#D8B059");
- Dirty(mapUid, light, metadata);
-
- EnsureComp<RoofComponent>(mapUid);
-
- EnsureComp<LightCycleComponent>(mapUid);
-
- EnsureComp<SunShadowComponent>(mapUid);
- EnsureComp<SunShadowCycleComponent>(mapUid);
-
- var moles = new float[Atmospherics.AdjustedNumberOfGases];
- moles[(int)Gas.Oxygen] = 21.824779f;
- moles[(int)Gas.Nitrogen] = 82.10312f;
-
- var mixture = new GasMixture(moles, Atmospherics.T20C);
-
- _atmos.SetMapAtmosphere(mapUid, false, mixture);
- }
-}
+++ /dev/null
-using System.Numerics;
-using System.Threading;
-using System.Threading.Tasks;
-using Content.Server.Atmos.EntitySystems;
-using Content.Server.Decals;
-using Content.Server.Shuttles.Events;
-using Content.Shared.CCVar;
-using Content.Shared.Decals;
-using Content.Shared.Ghost;
-using Content.Shared.Procedural;
-using Content.Shared.Procedural.Components;
-using Content.Shared.Procedural.DungeonGenerators;
-using Content.Shared.Sprite;
-using Content.Shared.Tag;
-using Robust.Server.Player;
-using Robust.Shared.Configuration;
-using Robust.Shared.CPUJob.JobQueues;
-using Robust.Shared.CPUJob.JobQueues.Queues;
-using Robust.Shared.Map;
-using Robust.Shared.Map.Components;
-using Robust.Shared.Map.Enumerators;
-using Robust.Shared.Physics.Components;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Random;
-using Robust.Shared.Serialization.Manager;
-using Robust.Shared.Timing;
-using Robust.Shared.Utility;
-
-namespace Content.Server.Procedural;
-
-public sealed partial class BiomeSystem : EntitySystem
-{
- /*
- * Handles loading in biomes around players.
- * These are essentially chunked-areas that load in dungeons and can also be unloaded.
- */
-
- [Dependency] private readonly IConfigurationManager _cfgManager = default!;
- [Dependency] private readonly IPlayerManager _player = default!;
- [Dependency] private readonly IPrototypeManager _protomanager = default!;
- [Dependency] private readonly IRobustRandom _random = default!;
- [Dependency] private readonly ISerializationManager _serManager = default!;
- [Dependency] private readonly AtmosphereSystem _atmos = default!;
- [Dependency] private readonly SharedMapSystem _maps = default!;
- [Dependency] private readonly TagSystem _tags = default!;
- [Dependency] private readonly SharedTransformSystem _xforms = default!;
-
- /// <summary>
- /// Ignored components for default checks
- /// </summary>
- public static readonly List<string> IgnoredComponents = new();
-
- /// <summary>
- /// Jobs for biomes to load.
- /// </summary>
- private JobQueue _biomeQueue = default!;
-
- private float _loadRange = 1f;
- private float _loadTime;
-
- private EntityQuery<GhostComponent> _ghostQuery;
- private EntityQuery<BiomeComponent> _biomeQuery;
-
- private static readonly ProtoId<TagPrototype> AllowBiomeLoadingTag = "AllowBiomeLoading";
-
- public override void Initialize()
- {
- base.Initialize();
- _ghostQuery = GetEntityQuery<GhostComponent>();
- _biomeQuery = GetEntityQuery<BiomeComponent>();
-
- IgnoredComponents.Add(Factory.GetComponentName<RandomSpriteComponent>());
-
- Subs.CVar(_cfgManager, CCVars.BiomeLoadRange, OnLoadRange, true);
- Subs.CVar(_cfgManager, CCVars.BiomeLoadTime, OnLoadTime, true);
-
- SubscribeLocalEvent<FTLStartedEvent>(OnFTLStarted);
- }
-
- private void OnLoadTime(float obj)
- {
- _biomeQueue = new JobQueue(obj);
- _loadTime = obj;
- }
-
- private void OnLoadRange(float obj)
- {
- _loadRange = obj;
- }
-
- private void OnFTLStarted(ref FTLStartedEvent ev)
- {
- var targetMap = _xforms.ToMapCoordinates(ev.TargetCoordinates);
- var targetMapUid = _maps.GetMapOrInvalid(targetMap.MapId);
-
- if (!TryComp<BiomeComponent>(targetMapUid, out var biome))
- return;
-
- var preloadArea = new Vector2(32f, 32f);
- var targetArea = new Box2(targetMap.Position - preloadArea, targetMap.Position + preloadArea);
- Preload(targetMapUid, biome, (Box2i) targetArea);
- }
-
- /// <summary>
- /// Preloads biome for the specified area.
- /// </summary>
- public void Preload(EntityUid uid, BiomeComponent component, Box2i area)
- {
- component.PreloadAreas.Add(area);
- }
-
- private bool CanLoad(EntityUid uid)
- {
- return !_ghostQuery.HasComp(uid) || _tags.HasTag(uid, AllowBiomeLoadingTag);
- }
-
- public bool RemoveLayer(Entity<BiomeComponent?> biome, string label)
- {
- if (!Resolve(biome.Owner, ref biome.Comp))
- return false;
-
- if (!biome.Comp.Layers.ContainsKey(label))
- {
- return false;
- }
-
- // Technically this can race-condition with adds but uhh tell people to not do that.
- biome.Comp.PendingRemovals.Add(label);
- return true;
- }
-
- public void AddLayer(Entity<BiomeComponent?> biome, string label, BiomeMetaLayer layer)
- {
- if (!Resolve(biome.Owner, ref biome.Comp))
- return;
-
- if (!biome.Comp.Layers.TryAdd(label, layer))
- {
- Log.Warning($"Tried to add layer {label} to biome {ToPrettyString(biome)} that already has it?");
- return;
- }
- }
-
- public void AddLayer(Entity<BiomeComponent?> biome, string label, ProtoId<DungeonConfigPrototype> layer)
- {
- if (!Resolve(biome.Owner, ref biome.Comp))
- return;
-
- var metaLayer = new BiomeMetaLayer()
- {
- Dungeon = layer,
- };
-
- AddLayer(biome, label, metaLayer);
- }
-
- public override void Update(float frameTime)
- {
- base.Update(frameTime);
-
- var query = AllEntityQuery<BiomeComponent>();
-
- while (query.MoveNext(out var biome))
- {
- // If it's still loading then don't touch the observer bounds.
- if (biome.Loading)
- continue;
-
- biome.LoadedBounds.Clear();
-
- // Make sure preloads go in.
- foreach (var preload in biome.PreloadAreas)
- {
- biome.LoadedBounds.Add(preload);
- }
- }
-
- // Get all relevant players.
- foreach (var player in _player.Sessions)
- {
- if (player.AttachedEntity != null)
- {
- TryAddBiomeBounds(player.AttachedEntity.Value);
- }
-
- foreach (var viewer in player.ViewSubscriptions)
- {
- TryAddBiomeBounds(viewer);
- }
- }
-
- // Unload first in case we can't catch up.
- UnloadChunks();
-
- var loadQuery = AllEntityQuery<BiomeComponent, MapGridComponent>();
-
- // Check if any biomes are intersected and queue up loads.
- while (loadQuery.MoveNext(out var uid, out var biome, out var grid))
- {
- if (biome.Loading || biome.LoadedBounds.Count == 0)
- continue;
-
- biome.Loading = true;
- var job = new BiomeLoadJob(_loadTime)
- {
- Grid = (uid, biome, grid),
- };
- _biomeQueue.EnqueueJob(job);
- }
-
- // Process jobs.
- _biomeQueue.Process();
- }
-
- private void UnloadChunks()
- {
- var query = AllEntityQuery<BiomeComponent, MapGridComponent>();
-
- while (query.MoveNext(out var uid, out var biome, out var grid))
- {
- // Only start unloading if it's currently not loading anything.
- if (biome.Loading)
- continue;
-
- var toUnload = new Dictionary<string, List<Vector2i>>();
-
- foreach (var (layerId, loadedLayer) in biome.LoadedData)
- {
- var layer = biome.Layers[layerId];
-
- // If it can't unload then ignore it.
- if (!layer.CanUnload)
- continue;
-
- var size = GetSize(_protomanager.Index(layer.Dungeon), layer);
-
- if (size == null)
- continue;
-
- // Go through each loaded chunk and check if they can be unloaded by checking if any players are in range.
- foreach (var chunk in loadedLayer.Keys)
- {
- var chunkBounds = new Box2i(chunk, chunk + size.Value);
- var canUnload = true;
-
- foreach (var playerView in biome.LoadedBounds)
- {
- // Give a buffer range so we don't immediately unload if we wiggle, we'll just double the load area.
- var enlarged = playerView.Enlarged((int) _loadRange);
-
- // Still relevant
- if (chunkBounds.Intersects(enlarged))
- {
- canUnload = false;
- break;
- }
- }
-
- if (!canUnload)
- continue;
-
- toUnload.GetOrNew(layerId).Add(chunk);
- }
- }
-
- if (biome.PendingRemovals.Count > 0)
- {
- foreach (var label in biome.PendingRemovals)
- {
- var bounds = toUnload.GetOrNew(label);
-
- foreach (var chunkOrigin in biome.LoadedData[label].Keys)
- {
- bounds.Add(chunkOrigin);
- }
- }
- }
-
- if (toUnload.Count == 0)
- continue;
-
- // Queue up unloads.
- biome.Loading = true;
- var job = new BiomeUnloadJob(_loadTime)
- {
- Biome = (uid, grid, biome),
- ToUnload = toUnload,
- };
- _biomeQueue.EnqueueJob(job);
- }
- }
-
- /// <summary>
- /// Gets the full bounds to be loaded. Considers layer dependencies where they may have different chunk sizes.
- /// </summary>
- private Box2i GetFullBounds(BiomeComponent component, Box2i bounds)
- {
- var result = bounds;
-
- foreach (var layer in component.Layers.Values)
- {
- var layerBounds = GetLayerBounds(layer, result);
-
- if (layer.DependsOn != null)
- {
- foreach (var sub in layer.DependsOn)
- {
- var depLayer = component.Layers[sub];
-
- layerBounds = layerBounds.Union(GetLayerBounds(depLayer, layerBounds));
- }
- }
-
- result = result.Union(layerBounds);
- }
-
- return result;
- }
-
- /// <summary>
- /// Tries to add the viewer bounds of this entity for loading.
- /// </summary>
- private void TryAddBiomeBounds(EntityUid uid)
- {
- if (!CanLoad(uid))
- return;
-
- var xform = Transform(uid);
-
- // No biome to load
- if (!_biomeQuery.TryComp(xform.MapUid, out var biome))
- return;
-
- // Currently already loading.
- if (biome.Loading)
- return;
-
- var center = _xforms.GetWorldPosition(uid);
-
- var bounds = new Box2i((center - new Vector2(_loadRange, _loadRange)).Floored(), (center + new Vector2(_loadRange, _loadRange)).Floored());
-
- // If it's moving then preload in that direction
- if (TryComp(uid, out PhysicsComponent? physics))
- {
- bounds = bounds.Union(bounds.Translated((physics.LinearVelocity * 2f).Floored()));
- }
-
- var adjustedBounds = GetFullBounds(biome, bounds);
- biome.LoadedBounds.Add(adjustedBounds);
- }
-
- public int? GetSize(DungeonConfigPrototype config, BiomeMetaLayer layer)
- {
- var size = layer.Size;
-
- if (size == null && config.Layers[0] is ChunkDunGen chunkGen)
- {
- size = chunkGen.Size;
- }
- // No size
- else
- {
- Log.Warning($"Unable to infer chunk size for biome {layer} / config {config.ID}");
- return null;
- }
-
- return size.Value;
- }
-
- public Box2i GetLayerBounds(BiomeMetaLayer layer, Box2i layerBounds)
- {
- var size = GetSize(_protomanager.Index(layer.Dungeon), layer);
-
- if (size == null)
- return Box2i.Empty;
-
- var chunkSize = new Vector2(size.Value, size.Value);
-
- // Need to round the bounds to our chunk size to ensure we load whole chunks.
- // We also need to know the minimum bounds for our dependencies to load.
- var layerBL = (layerBounds.BottomLeft / chunkSize).Floored() * chunkSize;
- var layerTR = (layerBounds.TopRight / chunkSize).Ceiled() * chunkSize;
-
- var loadBounds = new Box2i(layerBL.Floored(), layerTR.Ceiled());
- return loadBounds;
- }
-}
-
- public sealed class BiomeLoadJob : Job<bool>
- {
- [Dependency] private IEntityManager _entManager = default!;
- [Dependency] private IPrototypeManager _protoManager = default!;
-
- private BiomeSystem System = default!;
- private DungeonSystem DungeonSystem = default!;
-
- public Entity<BiomeComponent, MapGridComponent> Grid;
-
- internal ISawmill _sawmill = default!;
-
- public BiomeLoadJob(double maxTime, CancellationToken cancellation = default) : base(maxTime, cancellation)
- {
- IoCManager.InjectDependencies(this);
- System = _entManager.System<BiomeSystem>();
- DungeonSystem = _entManager.System<DungeonSystem>();
- }
-
- protected override async Task<bool> Process()
- {
- try
- {
- foreach (var bound in Grid.Comp1.LoadedBounds)
- {
- foreach (var (layerId, layer) in Grid.Comp1.Layers)
- {
- await LoadLayer(layerId, layer, bound);
- }
- }
- }
- finally
- {
- // Finished
- DebugTools.Assert(Grid.Comp1.Loading);
- Grid.Comp1.Loading = false;
- }
-
- // If we have any preloads then mark those as modified so they persist.
- foreach (var preload in Grid.Comp1.PreloadAreas)
- {
- for (var x = preload.Left; x <= preload.Right; x++)
- {
- for (var y = preload.Bottom; y <= preload.Top; y++)
- {
- var index = new Vector2i(x, y);
- Grid.Comp1.ModifiedTiles.Add(index);
- }
- }
-
- await SuspendIfOutOfTime();
- }
-
- Grid.Comp1.PreloadAreas.Clear();
-
- return true;
- }
-
- private async Task LoadLayer(string layerId, BiomeMetaLayer layer, Box2i parentBounds)
- {
- // Nothing to do
- var dungeon = _protoManager.Index(layer.Dungeon);
-
- if (dungeon.Layers.Count == 0)
- return;
-
- var loadBounds = System.GetLayerBounds(layer, parentBounds);
-
- // Make sure our dependencies are loaded first.
- if (layer.DependsOn != null)
- {
- foreach (var sub in layer.DependsOn)
- {
- var actualLayer = Grid.Comp1.Layers[sub];
-
- await LoadLayer(sub, actualLayer, loadBounds);
- }
- }
-
- var size = System.GetSize(dungeon, layer);
-
- if (size == null)
- return;
-
- // The reason we do this is so if we dynamically add similar layers (e.g. we add 3 mob layers at runtime)
- // they don't all have the same seeds.
- var layerSeed = Grid.Comp1.Seed + layerId.GetHashCode();
-
- // Okay all of our dependencies loaded so we can send it.
- var chunkEnumerator = new NearestChunkEnumerator(loadBounds, size.Value);
-
- while (chunkEnumerator.MoveNext(out var chunk))
- {
- var chunkOrigin = chunk.Value;
- var layerLoaded = Grid.Comp1.LoadedData.GetOrNew(layerId);
-
- // Layer already loaded for this chunk.
- // This can potentially happen if we're moving and the player's bounds changed but some existing chunks remain.
- if (layerLoaded.ContainsKey(chunkOrigin))
- {
- continue;
- }
-
- // Load dungeon here async await and all that jaz.
- var (_, data) = await WaitAsyncTask(DungeonSystem
- .GenerateDungeonAsync(dungeon, Grid.Owner, Grid.Comp2, chunkOrigin, layerSeed, reservedTiles: Grid.Comp1.ModifiedTiles));
-
- // If we can unload it then store the data to check for later.
- if (layer.CanUnload)
- {
- layerLoaded.Add(chunkOrigin, data);
- }
- }
- }
-}
-
-public sealed class BiomeUnloadJob : Job<bool>
-{
- [Dependency] private EntityManager _entManager = default!;
-
- public Entity<MapGridComponent, BiomeComponent> Biome;
- public Dictionary<string, List<Vector2i>> ToUnload = default!;
-
- public BiomeUnloadJob(double maxTime, CancellationToken cancellation = default) : base(maxTime, cancellation)
- {
- IoCManager.InjectDependencies(this);
- }
-
- public BiomeUnloadJob(double maxTime, IStopwatch stopwatch, CancellationToken cancellation = default) : base(maxTime, stopwatch, cancellation)
- {
- }
-
- protected override async Task<bool> Process()
- {
- try
- {
- var grid = Biome.Comp1;
- var biome = Biome.Comp2;
- DebugTools.Assert(biome.Loading);
- var maps = _entManager.System<SharedMapSystem>();
- var decals = _entManager.System<DecalSystem>();
- var lookup = _entManager.System<EntityLookupSystem>();
- _entManager.TryGetComponent(Biome.Owner, out DecalGridComponent? decalGrid);
- var forceUnload = _entManager.GetEntityQuery<BiomeForceUnloadComponent>();
- var entities = new HashSet<EntityUid>();
- var tiles = new List<(Vector2i, Tile)>();
-
- foreach (var (layer, chunkOrigins) in ToUnload)
- {
- if (!biome.Layers.TryGetValue(layer, out var meta))
- continue;
-
- if (!biome.LoadedData.TryGetValue(layer, out var data))
- continue;
-
- DebugTools.Assert(meta.CanUnload);
-
- foreach (var chunk in chunkOrigins)
- {
- // Not loaded anymore?
- if (!data.Remove(chunk, out var loaded))
- continue;
-
- tiles.Clear();
-
- foreach (var (ent, pos) in loaded.Entities)
- {
- // Already flagged as modified so go next.
- if (biome.ModifiedTiles.Contains(pos))
- {
- continue;
- }
-
- // IsDefault is actually super expensive so really need to run this check in the loop.
- await SuspendIfOutOfTime();
-
- if (forceUnload.HasComp(ent))
- {
- _entManager.DeleteEntity(ent);
- continue;
- }
-
- // Deleted so counts as modified.
- if (!_entManager.TransformQuery.TryComp(ent, out var xform))
- {
- biome.ModifiedTiles.Add(pos);
- continue;
- }
-
- // If it stayed still and had no data change then keep it.
- if (pos == xform.LocalPosition.Floored() && xform.GridUid == Biome.Owner && _entManager.IsDefault(ent, BiomeSystem.IgnoredComponents))
- {
- _entManager.DeleteEntity(ent);
- continue;
- }
-
- // Need the entity's current tile to be flagged for unloading.
- if (Biome.Owner == xform.GridUid)
- {
- var entTile = maps.LocalToTile(Biome.Owner, grid, xform.Coordinates);
- biome.ModifiedTiles.Add(entTile);
- }
- }
-
- foreach (var (decal, pos) in loaded.Decals)
- {
- // Modified go NEXT
- if (biome.ModifiedTiles.Contains(pos.Floored()))
- continue;
-
- // Should just be able to remove them as you can't actually edit a decal.
- if (!decals.RemoveDecal(Biome.Owner, decal, decalGrid))
- {
- biome.ModifiedTiles.Add(pos.Floored());
- }
- }
-
- await SuspendIfOutOfTime();
-
- foreach (var (index, tile) in loaded.Tiles)
- {
- await SuspendIfOutOfTime();
-
- if (Biome.Comp2.ModifiedTiles.Contains(index))
- {
- continue;
- }
-
- if (!maps.TryGetTileRef(Biome.Owner, Biome.Comp1, index, out var tileRef) ||
- tileRef.Tile != tile)
- {
- Biome.Comp2.ModifiedTiles.Add(index);
- continue;
- }
-
- entities.Clear();
- var tileBounds = lookup.GetLocalBounds(index, Biome.Comp1.TileSize).Enlarged(-0.05f);
-
- lookup.GetEntitiesIntersecting(Biome.Owner,
- tileBounds,
- entities);
-
- // Still entities remaining so just leave the tile.
- if (entities.Count > 0)
- {
- Biome.Comp2.ModifiedTiles.Add(index);
- continue;
- }
-
- if (decals.GetDecalsIntersecting(Biome.Owner, tileBounds, component: decalGrid).Count > 0)
- {
- Biome.Comp2.ModifiedTiles.Add(index);
- continue;
- }
-
- // Clear it
- tiles.Add((index, Tile.Empty));
- }
-
- maps.SetTiles(Biome.Owner, Biome.Comp1, tiles);
- }
- }
- }
- finally
- {
- Biome.Comp2.Loading = false;
- }
-
- Biome.Comp2.PendingRemovals.Clear();
-
- return true;
- }
-}
if (found)
continue;
- var ent = _entManager.SpawnEntity(gen.Entity, _maps.GridTileToLocal(_gridUid, _grid, tile));
- AddLoadedEntity(tile, ent);
+ _entManager.SpawnEntity(gen.Entity, _maps.GridTileToLocal(_gridUid, _grid, tile));
}
}
}
--- /dev/null
+using System.Threading.Tasks;
+using Content.Server.Parallax;
+using Content.Shared.Maps;
+using Content.Shared.Parallax.Biomes;
+using Content.Shared.Procedural;
+using Content.Shared.Procedural.PostGeneration;
+using Robust.Shared.Map;
+using Robust.Shared.Utility;
+
+namespace Content.Server.Procedural.DungeonJob;
+
+public sealed partial class DungeonJob
+{
+ /// <summary>
+ /// <see cref="BiomeDunGen"/>
+ /// </summary>
+ private async Task PostGen(BiomeDunGen dunGen, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
+ {
+ if (!_prototype.TryIndex(dunGen.BiomeTemplate, out var indexedBiome))
+ return;
+
+ var biomeSystem = _entManager.System<BiomeSystem>();
+
+ var seed = random.Next();
+ var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
+
+ var tiles = _maps.GetAllTilesEnumerator(_gridUid, _grid);
+ while (tiles.MoveNext(out var tileRef))
+ {
+ var node = tileRef.Value.GridIndices;
+
+ if (reservedTiles.Contains(node))
+ continue;
+
+ if (dunGen.TileMask is not null)
+ {
+ if (!dunGen.TileMask.Contains(((ContentTileDefinition)_tileDefManager[tileRef.Value.Tile.TypeId]).ID))
+ continue;
+ }
+
+ // Need to set per-tile to override data.
+ if (biomeSystem.TryGetTile(node, indexedBiome.Layers, seed, (_gridUid, _grid), out var tile))
+ {
+ _maps.SetTile(_gridUid, _grid, node, tile.Value);
+ }
+
+ if (biomeSystem.TryGetDecals(node, indexedBiome.Layers, seed, (_gridUid, _grid), out var decals))
+ {
+ foreach (var decal in decals)
+ {
+ _decals.TryAddDecal(decal.ID, new EntityCoordinates(_gridUid, decal.Position), out _);
+ }
+ }
+
+ if (biomeSystem.TryGetEntity(node, indexedBiome.Layers, tile ?? tileRef.Value.Tile, seed, (_gridUid, _grid), out var entityProto))
+ {
+ var ent = _entManager.SpawnEntity(entityProto, new EntityCoordinates(_gridUid, node + _grid.TileSizeHalfVector));
+ var xform = xformQuery.Get(ent);
+
+ if (!xform.Comp.Anchored)
+ {
+ _transform.AnchorEntity(ent, xform);
+ }
+
+ // TODO: Engine bug with SpawnAtPosition
+ DebugTools.Assert(xform.Comp.Anchored);
+ }
+
+ await SuspendDungeon();
+ if (!ValidateResume())
+ return;
+ }
+ }
+}
--- /dev/null
+using System.Threading.Tasks;
+using Content.Server.Parallax;
+using Content.Shared.Parallax.Biomes;
+using Content.Shared.Parallax.Biomes.Markers;
+using Content.Shared.Procedural;
+using Content.Shared.Procedural.PostGeneration;
+using Content.Shared.Random.Helpers;
+using Robust.Shared.Map;
+using Robust.Shared.Utility;
+
+namespace Content.Server.Procedural.DungeonJob;
+
+public sealed partial class DungeonJob
+{
+ /// <summary>
+ /// <see cref="BiomeMarkerLayerDunGen"/>
+ /// </summary>
+ private async Task PostGen(BiomeMarkerLayerDunGen dunGen, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
+ {
+ // If we're adding biome then disable it and just use for markers.
+ if (_entManager.EnsureComponent(_gridUid, out BiomeComponent biomeComp))
+ {
+ biomeComp.Enabled = false;
+ }
+
+ var biomeSystem = _entManager.System<BiomeSystem>();
+ var weightedRandom = _prototype.Index(dunGen.MarkerTemplate);
+ var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
+ var templates = new Dictionary<string, int>();
+
+ for (var i = 0; i < dunGen.Count; i++)
+ {
+ var template = weightedRandom.Pick(random);
+ var count = templates.GetOrNew(template);
+ count++;
+ templates[template] = count;
+ }
+
+ foreach (var (template, count) in templates)
+ {
+ var markerTemplate = _prototype.Index<BiomeMarkerLayerPrototype>(template);
+
+ var bounds = new Box2i();
+
+ foreach (var tile in dungeon.RoomTiles)
+ {
+ bounds = bounds.UnionTile(tile);
+ }
+
+ await SuspendDungeon();
+ if (!ValidateResume())
+ return;
+
+ biomeSystem.GetMarkerNodes(_gridUid, biomeComp, _grid, markerTemplate, true, bounds, count,
+ random, out var spawnSet, out var existing, false);
+
+ await SuspendDungeon();
+ if (!ValidateResume())
+ return;
+
+ var checkTile = reservedTiles.Count > 0;
+
+ foreach (var ent in existing)
+ {
+ if (checkTile && reservedTiles.Contains(_maps.LocalToTile(_gridUid, _grid, _xformQuery.GetComponent(ent).Coordinates)))
+ {
+ continue;
+ }
+
+ _entManager.DeleteEntity(ent);
+
+ await SuspendDungeon();
+ if (!ValidateResume())
+ return;
+ }
+
+ foreach (var (node, mask) in spawnSet)
+ {
+ if (reservedTiles.Contains(node))
+ continue;
+
+ string? proto;
+
+ if (mask != null && markerTemplate.EntityMask.TryGetValue(mask, out var maskedProto))
+ {
+ proto = maskedProto;
+ }
+ else
+ {
+ proto = markerTemplate.Prototype;
+ }
+
+ var ent = _entManager.SpawnAtPosition(proto, new EntityCoordinates(_gridUid, node + _grid.TileSizeHalfVector));
+ var xform = xformQuery.Get(ent);
+
+ if (!xform.Comp.Anchored)
+ _transform.AnchorEntity(ent, xform);
+
+ await SuspendDungeon();
+ if (!ValidateResume())
+ return;
+ }
+ }
+ }
+}
if (!_anchorable.TileFree((_gridUid, _grid), neighbor, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
continue;
- var tile = _tile.GetVariantTile((ContentTileDefinition)tileDef, random);
- tiles.Add((neighbor, tile));
- AddLoadedTile(neighbor, tile);
- DebugTools.Assert(dungeon.AllTiles.Contains(neighbor));
+ tiles.Add((neighbor, _tile.GetVariantTile((ContentTileDefinition) tileDef, random)));
}
foreach (var index in dungeon.CorridorExteriorTiles)
if (!_anchorable.TileFree((_gridUid, _grid), index, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
continue;
- var tile = _tile.GetVariantTile((ContentTileDefinition)tileDef, random);
- tiles.Add((index, tile));
- AddLoadedTile(index, tile);
- DebugTools.Assert(dungeon.AllTiles.Contains(index));
+ tiles.Add((index, _tile.GetVariantTile((ContentTileDefinition)tileDef, random)));
}
_maps.SetTiles(_gridUid, _grid, tiles);
}
if (isCorner)
- {
- var uid = _entManager.SpawnEntity(cornerWall, _maps.GridTileToLocal(_gridUid, _grid, index.Index));
- AddLoadedEntity(index.Index, uid);
- }
+ _entManager.SpawnEntity(cornerWall, _maps.GridTileToLocal(_gridUid, _grid, index.Index));
if (!isCorner)
- {
- var uid = _entManager.SpawnEntity(wall, _maps.GridTileToLocal(_gridUid, _grid, index.Index));
- AddLoadedEntity(index.Index, uid);
- }
+ _entManager.SpawnEntity(wall, _maps.GridTileToLocal(_gridUid, _grid, index.Index));
- await SuspendDungeon();
+ if (i % 20 == 0)
+ {
+ await SuspendDungeon();
- if (!ValidateResume())
- return;
+ if (!ValidateResume())
+ return;
+ }
}
}
}
+++ /dev/null
-using System.Threading.Tasks;
-using Content.Shared.Procedural;
-using Content.Shared.Procedural.DungeonGenerators;
-using Content.Shared.Procedural.PostGeneration;
-
-namespace Content.Server.Procedural.DungeonJob;
-
-public sealed partial class DungeonJob
-{
- /// <summary>
- /// <see cref="BiomeDunGen"/>
- /// </summary>
- private async Task<Dungeon> PostGen(ChunkDunGen dunGen, HashSet<Vector2i> reservedTiles, Random random)
- {
- var dungeon = new Dungeon();
- var tiles = new HashSet<Vector2i>();
- var tr = _position + new Vector2i(dunGen.Size, dunGen.Size);
- var oldSeed = dunGen.Noise?.GetSeed() ?? 0;
- dunGen.Noise?.SetSeed(_seed + oldSeed);
-
- for (var x = 0; x < dunGen.Size; x++)
- {
- for (var y = 0; y < dunGen.Size; y++)
- {
- var index = new Vector2i(_position.X + x, _position.Y + y);
-
- if (reservedTiles.Contains(index))
- continue;
-
- if (dunGen.Noise?.GetNoise(x, y) < dunGen.Threshold)
- continue;
-
- tiles.Add(index);
- }
- }
-
- dunGen.Noise?.SetSeed(oldSeed);
- var room = new DungeonRoom(tiles, (tr - _position) / 2 + _position, new Box2i(_position, tr), new HashSet<Vector2i>());
- dungeon.AddRoom(room);
- return dungeon;
- }
-}
var setTiles = new List<(Vector2i, Tile)>();
var tileDef = (ContentTileDefinition) _tileDefManager[gen.Tile];
- foreach (var node in corridorTiles)
+ foreach (var tile in corridorTiles)
{
- if (reservedTiles.Contains(node))
+ if (reservedTiles.Contains(tile))
continue;
- var tile = _tile.GetVariantTile(tileDef, random);
- setTiles.Add((node, tile));
- AddLoadedTile(node, tile);
+ setTiles.Add((tile, _tile.GetVariantTile(tileDef, random)));
}
_maps.SetTiles(_gridUid, _grid, setTiles);
var protos = _entTable.GetSpawns(contents, random);
var coords = _maps.ToCenterCoordinates(_gridUid, tile, _grid);
- var uids = _entManager.SpawnEntitiesAttachedTo(coords, protos);
-
- foreach (var uid in uids)
- {
- AddLoadedEntity(tile, uid);
- }
-
+ _entManager.SpawnEntitiesAttachedTo(coords, protos);
await SuspendIfOutOfTime();
if (!ValidateResume())
{
// Decals not being centered biting my ass again
var gridPos = _maps.GridTileToLocal(_gridUid, _grid, tile).Offset(offset);
- _decals.TryAddDecal(cDir, gridPos, out var did, color: decks.Color);
- AddLoadedDecal(tile, did);
+ _decals.TryAddDecal(cDir, gridPos, out _, color: decks.Color);
}
}
{
// Decals not being centered biting my ass again
var gridPos = _maps.GridTileToLocal(_gridUid, _grid, tile).Offset(offset);
- _decals.TryAddDecal(cDir, gridPos, out var did, color: decks.Color);
- AddLoadedDecal(tile, did);
+ _decals.TryAddDecal(cDir, gridPos, out _, color: decks.Color);
}
continue;
if (decks.CornerDecals.TryGetValue(dirFlag, out var cDir))
{
var gridPos = _maps.GridTileToLocal(_gridUid, _grid, tile).Offset(offset);
- _decals.TryAddDecal(cDir, gridPos, out var did, color: decks.Color);
- AddLoadedDecal(tile, did);
+ _decals.TryAddDecal(cDir, gridPos, out _, color: decks.Color);
}
}
}
Random random)
{
var tiles = new List<(Vector2i, Tile)>();
- var matrix = Matrix3Helpers.CreateTranslation(_position + position);
+ var matrix = Matrix3Helpers.CreateTranslation(position);
foreach (var layer in dungen.Layers)
{
break;
}
- var tile = new Tile(tileDef.TileId, variant: variant);
- tiles.Add((adjusted, tile));
- AddLoadedTile(adjusted, tile);
+ tiles.Add((adjusted, new Tile(tileDef.TileId, variant: variant)));
roomTiles.Add(adjusted);
break;
}
{
switch (distance)
{
- case DunGenDistanceSquared:
- return dx * dx + dy * dy;
case DunGenEuclideanSquaredDistance:
return MathF.Min(1f, (dx * dx + dy * dy) / MathF.Sqrt(2));
case DunGenSquareBump:
if (reservedTiles.Contains(index))
continue;
- var tile = new Tile(_tileDefManager[fallbackTile.Value].TileId);
- tiles.Add((index, tile));
- AddLoadedTile(index, tile);
+ tiles.Add((index, new Tile(_tileDefManager[fallbackTile.Value].TileId)));
}
}
var dungeonMatty = Matrix3x2.Multiply(matty, dungeonTransform);
// The expensive bit yippy.
- var data = _dungeon.SpawnRoom(_gridUid, _grid, dungeonMatty, room, reservedTiles);
-
- _data.Merge(data);
-
- await SuspendDungeon();
-
- if (!ValidateResume())
- return dungeon;
+ _dungeon.SpawnRoom(_gridUid, _grid, dungeonMatty, room, reservedTiles);
var roomCenter = (room.Offset + room.Size / 2f) * _grid.TileSize;
var roomTiles = new HashSet<Vector2i>(room.Size.X * room.Size.Y);
}
replacements.Add((node, tile));
- AddLoadedTile(node, tile);
break;
}
isValid = true;
// Entrance wew
- var tileVariant = _tile.GetVariantTile(tileDef, random);
- _maps.SetTile(_gridUid, _grid, tile, tileVariant);
- AddLoadedTile(tile, tileVariant);
+ _maps.SetTile(_gridUid, _grid, tile, _tile.GetVariantTile(tileDef, random));
ClearDoor(dungeon, _grid, tile);
var gridCoords = _maps.GridTileToLocal(_gridUid, _grid, tile);
// Need to offset the spawn to avoid spawning in the room.
foreach (var ent in _entTable.GetSpawns(contents, random))
{
- var uid = _entManager.SpawnAtPosition(ent, gridCoords);
- AddLoadedEntity(tile, uid);
+ _entManager.SpawnAtPosition(ent, gridCoords);
}
// Clear out any biome tiles nearby to avoid blocking it
foreach (var ent in entities)
{
var uid = _entManager.SpawnAtPosition(ent, _maps.GridTileToLocal(_gridUid, _grid, tile));
- AddLoadedEntity(tile, uid);
_entManager.RemoveComponent<GhostRoleComponent>(uid);
_entManager.RemoveComponent<GhostTakeoverAvailableComponent>(uid);
npcs.SleepNPC(uid);
if (reservedTiles.Contains(neighbor))
continue;
- var tile = _tile.GetVariantTile((ContentTileDefinition)tileDef, random);
- tiles.Add((neighbor, tile));
- AddLoadedTile(neighbor, tile);
+ tiles.Add((neighbor, _tile.GetVariantTile((ContentTileDefinition) tileDef, random)));
spawnPositions.Add(neighbor);
}
}
foreach (var entrance in spawnPositions)
{
- var uids = _entManager.SpawnEntitiesAttachedTo(_maps.GridTileToLocal(_gridUid, _grid, entrance), _entTable.GetSpawns(contents, random));
-
- foreach (var uid in uids)
- {
- AddLoadedEntity(entrance, uid);
- }
+ _entManager.SpawnEntitiesAttachedTo(_maps.GridTileToLocal(_gridUid, _grid, entrance), _entTable.GetSpawns(contents, random));
}
}
}
-using System.Numerics;
using System.Threading.Tasks;
using Content.Shared.Maps;
using Content.Shared.NPC;
/// <summary>
/// <see cref="ExteriorDunGen"/>
/// </summary>
- private async Task<List<Dungeon>> GenerateExteriorDungen(int runCount, int maxRuns, Vector2i position, ExteriorDunGen dungen, HashSet<Vector2i> reservedTiles, Random random)
+ private async Task<List<Dungeon>> GenerateExteriorDungen(Vector2i position, ExteriorDunGen dungen, HashSet<Vector2i> reservedTiles, Random random)
{
DebugTools.Assert(_grid.ChunkCount > 0);
var aabb = new Box2i(_grid.LocalAABB.BottomLeft.Floored(), _grid.LocalAABB.TopRight.Floored());
- // TODO: Cross-layer seeding. Need this because we need to be able to spread the dungeons out.
- var angle = new Random(_seed).NextAngle();
- var divisors = new Angle(Angle.FromDegrees(360) / maxRuns);
-
- // Offset each dungeon so they don't generate on top of each other.
- for (var i = 0; i < runCount; i++)
- {
- angle += (random.NextFloat(0.6f, 1.4f)) * divisors;
- }
+ var angle = random.NextAngle();
var distance = Math.Max(aabb.Width / 2f + 1f, aabb.Height / 2f + 1f);
+
var startTile = new Vector2i(0, (int) distance).Rotate(angle);
Vector2i? dungeonSpawn = null;
};
}
- // Move it further in based on the spawn angle.
- if (dungen.Penetration.Y > 0)
- {
- var penetration = random.Next(dungen.Penetration.X, dungen.Penetration.Y);
- var diff = dungeonSpawn.Value - startTile;
- var diffVec = new Vector2(diff.X, diff.Y);
- dungeonSpawn = (diffVec.Normalized() * (penetration + diffVec.Length())).Floored() + startTile;
- }
-
- var subConfig = _prototype.Index(dungen.Proto);
+ var config = _prototype.Index(dungen.Proto);
var nextSeed = random.Next();
- var (dungeons, newReserved) = await GetDungeons(dungeonSpawn.Value, subConfig, subConfig.Layers, nextSeed, new Random(nextSeed), reserved: reservedTiles);
- reservedTiles.UnionWith(newReserved);
+ var dungeons = await GetDungeons(dungeonSpawn.Value, config, config.Layers, reservedTiles, nextSeed, new Random(nextSeed));
return dungeons;
}
if (reservedTiles.Contains(neighbor))
continue;
- var tileVariant = _tile.GetVariantTile((ContentTileDefinition)tileDef, random);
- tiles.Add((neighbor, tileVariant));
- AddLoadedTile(neighbor, tileVariant);
+ tiles.Add((neighbor, _tile.GetVariantTile((ContentTileDefinition) tileDef, random)));
index++;
takenTiles.Add(neighbor);
}
{
var gridPos = _maps.GridTileToLocal(_gridUid, _grid, tile.Item1);
- var uids = _entManager.SpawnEntitiesAttachedTo(gridPos, _entTable.GetSpawns(contents, random));
-
- foreach (var uid in uids)
- {
- AddLoadedEntity(tile.Item1, uid);
- }
-
+ _entManager.SpawnEntitiesAttachedTo(gridPos, _entTable.GetSpawns(contents, random));
await SuspendDungeon();
if (!ValidateResume())
if (reservedTiles.Contains(tile))
continue;
- await SuspendDungeon();
- if (!ValidateResume())
- return;
-
if (!_maps.TryGetTileDef(_grid, tile, out var tileDef))
continue;
if (fill.AllowedTiles != null && !fill.AllowedTiles.Contains(tileDef.ID))
continue;
- // If noise then check it matches.
- if (fill.ReservedNoise != null)
- {
- var value = fill.ReservedNoise.GetNoise(tile.X, tile.Y);
-
- if (fill.DistanceConfig != null)
- {
- // Need to get dx - dx in a range from -1 -> 1
- var dx = 2 * tile.X / fill.Size.X;
- var dy = 2 * tile.Y / fill.Size.Y;
-
- var distance = GetDistance(dx, dy, fill.DistanceConfig);
-
- value = MathHelper.Lerp(value, 1f - distance, fill.DistanceConfig.BlendWeight);
- }
-
- value *= (fill.Invert ? -1 : 1);
-
- if (value < fill.Threshold)
- continue;
- }
-
if (!_anchorable.TileFree((_gridUid, _grid), tile, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
continue;
var gridPos = _maps.GridTileToLocal(_gridUid, _grid, tile);
- var uid = _entManager.SpawnEntity(fill.Entity, gridPos);
+ _entManager.SpawnEntity(fill.Entity, gridPos);
- AddLoadedEntity(tile, uid);
+ await SuspendDungeon();
+ if (!ValidateResume())
+ break;
}
}
}
}
}
}
-
- dungeon.RefreshAllTiles();
}
private void WidenCorridor(Dungeon dungeon, float width, ICollection<Vector2i> corridorTiles)
{
var tile = validTiles[j];
var gridPos = _maps.GridTileToLocal(_gridUid, _grid, tile);
- var tileVariant = _tile.GetVariantTile((ContentTileDefinition)tileDef, random);
- _maps.SetTile(_gridUid, _grid, tile, tileVariant);
- AddLoadedTile(tile, tileVariant);
+ _maps.SetTile(_gridUid, _grid, tile, _tile.GetVariantTile((ContentTileDefinition) tileDef, random));
- var uids = _entManager.SpawnEntitiesAttachedTo(gridPos, _entTable.GetSpawns(contents, random));
-
- foreach (var uid in uids)
- {
- AddLoadedEntity(tile, uid);
- }
+ _entManager.SpawnEntitiesAttachedTo(gridPos, _entTable.GetSpawns(contents, random));
}
if (validTiles.Count > 0)
if (reservedTiles.Contains(weh))
continue;
- var tileVariant = _tile.GetVariantTile((ContentTileDefinition)tileDef, random);
- _maps.SetTile(_gridUid, _grid, weh, tileVariant);
- AddLoadedTile(weh, tileVariant);
+ _maps.SetTile(_gridUid, _grid, weh, _tile.GetVariantTile((ContentTileDefinition) tileDef, random));
var coords = _maps.GridTileToLocal(_gridUid, _grid, weh);
- var uids = _entManager.SpawnEntitiesAttachedTo(coords, _entTable.GetSpawns(contents, random));
-
- foreach (var uid in uids)
- {
- AddLoadedEntity(weh, uid);
- }
+ _entManager.SpawnEntitiesAttachedTo(coords, _entTable.GetSpawns(contents, random));
}
break;
// Grab all of the room bounds
// Then, work out connections between them
var roomBorders = new Dictionary<DungeonRoom, HashSet<Vector2i>>(dungeon.Rooms.Count);
- var flank = gen.Flank;
foreach (var room in dungeon.Rooms)
{
continue;
width--;
- var tileVariant = _tile.GetVariantTile((ContentTileDefinition)tileDef, random);
- _maps.SetTile(_gridUid, _grid, node, tileVariant);
- AddLoadedTile(node, tileVariant);
+ _maps.SetTile(_gridUid, _grid, node, _tile.GetVariantTile((ContentTileDefinition) tileDef, random));
if (flankContents != null && nodeDistances.Count - i <= 2)
{
- var uids = _entManager.SpawnEntitiesAttachedTo(gridPos, _entTable.GetSpawns(flankContents, random));
-
- foreach (var uid in uids)
- {
- AddLoadedEntity(node, uid);
- }
+ _entManager.SpawnEntitiesAttachedTo(gridPos, _entTable.GetSpawns(flankContents, random));
}
else
{
// Iterate neighbors and check for blockers, if so bulldoze
ClearDoor(dungeon, _grid, node);
- var uids = _entManager.SpawnEntitiesAttachedTo(gridPos, _entTable.GetSpawns(contents, random));
-
- foreach (var uid in uids)
- {
- AddLoadedEntity(node, uid);
- }
+ _entManager.SpawnEntitiesAttachedTo(gridPos, _entTable.GetSpawns(contents, random));
}
if (width == 0)
_entManager.RemoveComponent<GhostRoleComponent>(uid);
_entManager.RemoveComponent<GhostTakeoverAvailableComponent>(uid);
npcs.SleepNPC(uid);
- AddLoadedEntity(tile, uid);
}
break;
var variant = _tile.PickVariant((ContentTileDefinition) tileDef, random);
var adjusted = Vector2.Transform(node + _grid.TileSizeHalfVector, matrix).Floored();
- var tileVariant = new Tile(tileDef.TileId, variant: variant);
- tiles.Add((adjusted, tileVariant));
- AddLoadedTile(adjusted, tileVariant);
+ tiles.Add((adjusted, new Tile(tileDef.TileId, variant: variant)));
roomTiles.Add(adjusted);
tileCount++;
break;
}
}
- await SuspendDungeon();
+ await SuspendIfOutOfTime();
+ ValidateResume();
}
var center = Vector2.Zero;
center /= roomTiles.Count;
rooms.Add(new DungeonRoom(roomTiles, center, roomArea, new HashSet<Vector2i>()));
- await SuspendDungeon();
+ await SuspendIfOutOfTime();
+ ValidateResume();
}
_maps.SetTiles(_gridUid, _grid, tiles);
HashSet<Vector2i> reservedTiles,
Random random)
{
- var emptyTiles = false;
- var replaceEntities = new Dictionary<Vector2i, EntityUid>();
- var availableTiles = new List<Vector2i>();
- var remapName = _entManager.ComponentFactory.GetComponentName<EntityRemapComponent>();
- var replacementRemapping = new Dictionary<EntProtoId, EntProtoId>();
-
- if (_prototype.TryIndex(gen.Replacement, out var replacementProto) &&
- replacementProto.Components.TryGetComponent(remapName, out var replacementComps))
- {
- var remappingComp = (EntityRemapComponent) replacementComps;
- replacementRemapping = remappingComp.Mask;
- }
-
- if (gen.Replacement != null)
- {
- replacementRemapping[gen.Replacement.Value] = gen.Entity;
- }
-
foreach (var dungeon in dungeons)
{
+ var emptyTiles = false;
+ var replaceEntities = new Dictionary<Vector2i, EntityUid>();
+ var availableTiles = new List<Vector2i>();
+
foreach (var node in dungeon.AllTiles)
{
if (reservedTiles.Contains(node))
// We use existing entities as a mark to spawn in place
// OR
// We check for any existing entities to see if we can spawn there.
- // We can't replace so just stop here.
- if (gen.Replacement != null)
+ while (enumerator.MoveNext(out var uid))
{
- while (enumerator.MoveNext(out var uid))
- {
- var prototype = _entManager.GetComponent<MetaDataComponent>(uid.Value).EntityPrototype;
+ // We can't replace so just stop here.
+ if (gen.Replacement == null)
+ break;
- if (string.IsNullOrEmpty(prototype?.ID))
- continue;
+ var prototype = _entManager.GetComponent<MetaDataComponent>(uid.Value).EntityPrototype;
- // It has a valid remapping so take it over.
- if (replacementRemapping.ContainsKey(prototype.ID))
- {
- replaceEntities[node] = uid.Value;
- found = true;
- break;
- }
+ if (prototype?.ID == gen.Replacement)
+ {
+ replaceEntities[node] = uid.Value;
+ found = true;
+ break;
}
}
if (!ValidateResume())
return;
}
- }
- var remapping = new Dictionary<EntProtoId, EntProtoId>();
+ var remapping = new Dictionary<EntProtoId, EntProtoId>();
- // TODO: Move this to engine
- if (_prototype.TryIndex(gen.Entity, out var proto) &&
- proto.Components.TryGetComponent(remapName, out var comps))
- {
- var remappingComp = (EntityRemapComponent) comps;
- remapping = remappingComp.Mask;
- }
-
- var frontier = new ValueList<Vector2i>(32);
+ // TODO: Move this to engine
+ if (_prototype.TryIndex(gen.Entity, out var proto) &&
+ proto.Components.TryGetComponent("EntityRemap", out var comps))
+ {
+ var remappingComp = (EntityRemapComponent) comps;
+ remapping = remappingComp.Mask;
+ }
- // Iterate the group counts and pathfind out each group.
- for (var i = 0; i < gen.Count; i++)
- {
- var groupSize = random.Next(gen.MinGroupSize, gen.MaxGroupSize + 1);
+ var frontier = new ValueList<Vector2i>(32);
- // While we have remaining tiles keep iterating
- while (groupSize > 0 && availableTiles.Count > 0)
+ // Iterate the group counts and pathfind out each group.
+ for (var i = 0; i < gen.Count; i++)
{
- var startNode = random.PickAndTake(availableTiles);
- frontier.Clear();
- frontier.Add(startNode);
+ await SuspendDungeon();
+
+ if (!ValidateResume())
+ return;
+
+ var groupSize = random.Next(gen.MinGroupSize, gen.MaxGroupSize + 1);
- // This essentially may lead to a vein being split in multiple areas but the count matters more than position.
- while (frontier.Count > 0 && groupSize > 0)
+ // While we have remaining tiles keep iterating
+ while (groupSize > 0 && availableTiles.Count > 0)
{
- // Need to pick a random index so we don't just get straight lines of ores.
- var frontierIndex = random.Next(frontier.Count);
- var node = frontier[frontierIndex];
- frontier.RemoveSwap(frontierIndex);
- availableTiles.Remove(node);
-
- // Add neighbors if they're valid, worst case we add no more and pick another random seed tile.
- for (var x = -1; x <= 1; x++)
+ var startNode = random.PickAndTake(availableTiles);
+ frontier.Clear();
+ frontier.Add(startNode);
+
+ // This essentially may lead to a vein being split in multiple areas but the count matters more than position.
+ while (frontier.Count > 0 && groupSize > 0)
{
- for (var y = -1; y <= 1; y++)
+ // Need to pick a random index so we don't just get straight lines of ores.
+ var frontierIndex = random.Next(frontier.Count);
+ var node = frontier[frontierIndex];
+ frontier.RemoveSwap(frontierIndex);
+ availableTiles.Remove(node);
+
+ // Add neighbors if they're valid, worst case we add no more and pick another random seed tile.
+ for (var x = -1; x <= 1; x++)
{
- var neighbor = new Vector2i(node.X + x, node.Y + y);
+ for (var y = -1; y <= 1; y++)
+ {
+ var neighbor = new Vector2i(node.X + x, node.Y + y);
- if (frontier.Contains(neighbor) || !availableTiles.Contains(neighbor))
- continue;
+ if (frontier.Contains(neighbor) || !availableTiles.Contains(neighbor))
+ continue;
- frontier.Add(neighbor);
+ frontier.Add(neighbor);
+ }
}
- }
- var prototype = gen.Entity;
-
- // May have been deleted while iteration was suspended.
- if (replaceEntities.TryGetValue(node, out var existingEnt) && _entManager.TryGetComponent(existingEnt, out MetaDataComponent? metadata))
- {
- var existingProto = metadata.EntityPrototype;
- _entManager.DeleteEntity(existingEnt);
+ var prototype = gen.Entity;
- if (existingProto != null && remapping.TryGetValue(existingProto.ID, out var remapped))
+ if (replaceEntities.TryGetValue(node, out var existingEnt))
{
- prototype = remapped;
- }
- }
-
- // Tile valid salad so add it.
- var uid = _entManager.SpawnAtPosition(prototype, _maps.GridTileToLocal(_gridUid, _grid, node));
- AddLoadedEntity(node, uid);
+ var existingProto = _entManager.GetComponent<MetaDataComponent>(existingEnt).EntityPrototype;
+ _entManager.DeleteEntity(existingEnt);
- groupSize--;
+ if (existingProto != null && remapping.TryGetValue(existingProto.ID, out var remapped))
+ {
+ prototype = remapped;
+ }
+ }
- await SuspendDungeon();
+ // Tile valid salad so add it.
+ _entManager.SpawnAtPosition(prototype, _maps.GridTileToLocal(_gridUid, _grid, node));
- if (!ValidateResume())
- return;
+ groupSize--;
+ }
}
- }
- if (groupSize > 0)
- {
- // Not super worried depending on the gen it's fine.
- _sawmill.Debug($"Found remaining group size for ore veins of {gen.Replacement ?? "null"} / {gen.Entity}!");
+ if (groupSize > 0)
+ {
+ _sawmill.Warning($"Found remaining group size for ore veins of {gen.Replacement ?? "null"}!");
+ }
}
}
}
+++ /dev/null
-using System.Threading.Tasks;
-using Content.Server.Light.EntitySystems;
-using Content.Shared.Light.Components;
-using Content.Shared.Procedural;
-using Content.Shared.Procedural.DungeonLayers;
-
-namespace Content.Server.Procedural.DungeonJob;
-
-public sealed partial class DungeonJob
-{
- public async Task RoofGen(RoofDunGen roof, List<Dungeon> dungeons, HashSet<Vector2i> reservedTiles, Random random)
- {
- var roofComp = _entManager.EnsureComponent<RoofComponent>(_gridUid);
-
- var noise = roof.Noise;
- var oldSeed = noise?.GetSeed() ?? 0;
- noise?.SetSeed(_seed + oldSeed);
- var rooves = _entManager.System<RoofSystem>();
-
- foreach (var dungeon in dungeons)
- {
- foreach (var tile in dungeon.AllTiles)
- {
- if (reservedTiles.Contains(tile))
- continue;
-
- var value = noise?.GetNoise(tile.X, tile.Y) ?? 1f;
-
- if (value < roof.Threshold)
- continue;
-
- rooves.SetRoof((_gridUid, _grid, roofComp), tile, true);
- }
- }
-
- noise?.SetSeed(oldSeed);
- }
-}
if (reservedTiles.Contains(entrance))
continue;
- var tileVariant = _tile.GetVariantTile((ContentTileDefinition)tileDef, random);
- setTiles.Add((entrance, tileVariant));
- AddLoadedTile(entrance, tileVariant);
+ setTiles.Add((entrance, _tile.GetVariantTile((ContentTileDefinition) tileDef, random)));
}
}
if (reservedTiles.Contains(entrance))
continue;
- var uids = _entManager.SpawnEntitiesAttachedTo(
+ _entManager.SpawnEntitiesAttachedTo(
_maps.GridTileToLocal(_gridUid, _grid, entrance),
_entTable.GetSpawns(contents, random));
- foreach (var uid in uids)
- {
- AddLoadedEntity(entrance, uid);
- }
-
await SuspendDungeon();
if (!ValidateResume())
+++ /dev/null
-using System.Threading.Tasks;
-using Content.Shared.Procedural;
-using Content.Shared.Procedural.DungeonLayers;
-using Robust.Shared.Map;
-using Robust.Shared.Random;
-
-namespace Content.Server.Procedural.DungeonJob;
-
-public sealed partial class DungeonJob
-{
- /// <summary>
- /// <see cref="SampleDecalDunGen"/>
- /// </summary>
- private async Task PostGen(SampleDecalDunGen gen,
- List<Dungeon> dungeons,
- HashSet<Vector2i> reservedTiles,
- Random random)
- {
- var oldSeed = gen.Noise.GetSeed();
- gen.Noise.SetSeed(_seed + oldSeed);
-
- foreach (var dungeon in dungeons)
- {
- foreach (var tile in dungeon.AllTiles)
- {
- if (reservedTiles.Contains(tile))
- continue;
-
- var invert = gen.Invert;
- var value = gen.Noise.GetNoise(tile.X, tile.Y);
- value = invert ? value * -1 : value;
-
- if (value < gen.Threshold)
- continue;
-
- // Not allowed
- if (!_maps.TryGetTileRef(_gridUid, _grid, tile, out var tileRef) ||
- !gen.AllowedTiles.Contains(_tileDefManager[tileRef.Tile.TypeId].ID))
- {
- continue;
- }
-
- // Occupied?
- if (!_anchorable.TileFree(_grid, tile, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
- continue;
-
- _decals.TryAddDecal(random.Pick(gen.Decals), new EntityCoordinates(_gridUid, tile), out var did);
- AddLoadedDecal(tile, did);
-
- if (gen.ReserveTiles)
- {
- reservedTiles.Add(tile);
- }
-
- await SuspendDungeon();
-
- if (!ValidateResume())
- return;
- }
- }
-
- gen.Noise.SetSeed(oldSeed);
- }
-}
+++ /dev/null
-using System.Threading.Tasks;
-using Content.Shared.Procedural;
-using Content.Shared.Procedural.DungeonLayers;
-using Robust.Shared.Map;
-using Robust.Shared.Random;
-
-namespace Content.Server.Procedural.DungeonJob;
-
-public sealed partial class DungeonJob
-{
- /// <summary>
- /// <see cref="SampleEntityDunGen"/>
- /// </summary>
- private async Task PostGen(
- SampleEntityDunGen gen,
- List<Dungeon> dungeons,
- HashSet<Vector2i> reservedTiles,
- Random random)
- {
- var oldSeed = gen.Noise.GetSeed();
- gen.Noise.SetSeed(_seed + oldSeed);
-
- foreach (var dungeon in dungeons)
- {
- foreach (var tile in dungeon.AllTiles)
- {
- if (reservedTiles.Contains(tile))
- continue;
-
- var invert = gen.Invert;
- var value = gen.Noise.GetNoise(tile.X, tile.Y);
- value = invert ? value * -1 : value;
-
- if (value < gen.Threshold)
- continue;
-
- // Not allowed
- if (!_maps.TryGetTileRef(_gridUid, _grid, tile, out var tileRef) ||
- !gen.AllowedTiles.Contains(_tileDefManager[tileRef.Tile.TypeId].ID))
- {
- continue;
- }
-
- var gridTile = _maps.GridTileToLocal(_gridUid, _grid, tile);
- var uid = _entManager.SpawnAttachedTo(random.Pick(gen.Entities), gridTile);
- AddLoadedEntity(tile, uid);
-
- if (gen.ReserveTiles)
- {
- reservedTiles.Add(tile);
- }
-
- await SuspendDungeon();
-
- if (!ValidateResume())
- return;
- }
- }
-
- gen.Noise.SetSeed(oldSeed);
- }
-}
+++ /dev/null
-using System.Threading.Tasks;
-using Content.Shared.Procedural;
-using Content.Shared.Procedural.DungeonLayers;
-using Robust.Shared.Map;
-using Robust.Shared.Noise;
-using Robust.Shared.Random;
-
-namespace Content.Server.Procedural.DungeonJob;
-
-public sealed partial class DungeonJob
-{
- /// <summary>
- /// <see cref="SampleTileDunGen"/>
- /// </summary>
- private async Task PostGen(SampleTileDunGen gen,
- List<Dungeon> dungeons,
- HashSet<Vector2i> reservedTiles,
- Random random)
- {
- var noise = gen.Noise;
- var oldSeed = noise.GetSeed();
- noise.SetSeed(_seed + oldSeed);
- var tiles = new List<(Vector2i Index, Tile Tile)>();
- var tileDef = _prototype.Index(gen.Tile);
- var variants = tileDef.PlacementVariants.Length;
-
- foreach (var dungeon in dungeons)
- {
- foreach (var tile in dungeon.AllTiles)
- {
- if (reservedTiles.Contains(tile))
- continue;
-
- var invert = gen.Invert;
- var value = noise.GetNoise(tile.X, tile.Y);
- value = invert ? value * -1 : value;
-
- if (value < gen.Threshold)
- continue;
-
- var variantValue = (noise.GetNoise(tile.X * 8, tile.Y * 8, variants) + 1f) * 100;
- var variant = _tile.PickVariant(tileDef, (int)variantValue);
- var tileVariant = new Tile(tileDef.TileId, variant: variant);
-
- tiles.Add((tile, tileVariant));
- AddLoadedTile(tile, tileVariant);
-
- await SuspendDungeon();
-
- if (!ValidateResume())
- return;
- }
- }
-
- gen.Noise.SetSeed(oldSeed);
- _maps.SetTiles(_gridUid, _grid, tiles);
-
- if (gen.ReserveTiles)
- {
- foreach (var tile in tiles)
- {
- reservedTiles.Add(tile.Index);
- }
- }
- }
-}
using System.Threading.Tasks;
using Content.Server.NPC.Pathfinding;
using Content.Shared.Procedural;
-using Content.Shared.Procedural.DungeonLayers;
using Content.Shared.Procedural.PostGeneration;
using Robust.Shared.Map;
using Robust.Shared.Random;
public sealed partial class DungeonJob
{
/// <summary>
- /// <see cref="Shared.Procedural.DungeonLayers.SplineDungeonConnectorDunGen"/>
+ /// <see cref="SplineDungeonConnectorDunGen"/>
/// </summary>
private async Task<Dungeon> PostGen(
- Shared.Procedural.DungeonLayers.SplineDungeonConnectorDunGen gen,
+ SplineDungeonConnectorDunGen gen,
List<Dungeon> dungeons,
HashSet<Vector2i> reservedTiles,
Random random)
{
Start = pair.Start,
End = pair.End,
- Diagonals = false,
TileCost = node =>
{
// We want these to get prioritised internally and into space if it's a space dungeon.
}
tiles.Add((node, tile));
- AddLoadedTile(node, tile);
}
_maps.SetTiles(_gridUid, _grid, tiles);
allTiles.Add(node);
tiles.Add((node, pathTile));
- AddLoadedTile(node, pathTile);
}
_maps.SetTiles(_gridUid, _grid, tiles);
if (reservedTiles.Contains(neighbor))
continue;
- var tileVariant = _tile.GetVariantTile(tileDef, random);
- _maps.SetTile(_gridUid, _grid, neighbor, tileVariant);
- AddLoadedTile(neighbor, tileVariant);
+ _maps.SetTile(_gridUid, _grid, neighbor, _tile.GetVariantTile(tileDef, random));
var gridPos = _maps.GridTileToLocal(_gridUid, _grid, neighbor);
var protoNames = _entTable.GetSpawns(contents, random);
- var uids = _entManager.SpawnEntitiesAttachedTo(gridPos, protoNames);
-
- foreach (var uid in uids)
- {
- AddLoadedEntity(neighbor, uid);
- }
+ _entManager.SpawnEntitiesAttachedTo(gridPos, protoNames);
await SuspendDungeon();
if (!ValidateResume())
-using System.Numerics;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Content.Server.Decals;
+using Content.Server.NPC.Components;
using Content.Server.NPC.HTN;
using Content.Server.NPC.Systems;
using Content.Server.Shuttles.Systems;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Utility;
+using IDunGenLayer = Content.Shared.Procedural.IDunGenLayer;
namespace Content.Server.Procedural.DungeonJob;
-public sealed partial class DungeonJob : Job<(List<Dungeon>, DungeonData)>
+public sealed partial class DungeonJob : Job<List<Dungeon>>
{
public bool TimeSlice = true;
private readonly ISawmill _sawmill;
- private DungeonData _data = new();
-
- private HashSet<Vector2i>? _reservedTiles;
-
public DungeonJob(
ISawmill sawmill,
double maxTime,
int seed,
Vector2i position,
EntityCoordinates? targetCoordinates = null,
- CancellationToken cancellation = default,
- HashSet<Vector2i>? reservedTiles = null) : base(maxTime, cancellation)
+ CancellationToken cancellation = default) : base(maxTime, cancellation)
{
_sawmill = sawmill;
_entManager = entManager;
_prototype = prototype;
_tileDefManager = tileDefManager;
- _reservedTiles = reservedTiles;
_anchorable = anchorable;
_decals = decals;
/// <summary>
/// Gets the relevant dungeon, running recursively as relevant.
/// </summary>
- /// <param name="reservedTiles">Should we reserve tiles even if the config doesn't specify.</param>
- private async Task<(List<Dungeon>, HashSet<Vector2i>)> GetDungeons(
+ /// <param name="reserve">Should we reserve tiles even if the config doesn't specify.</param>
+ private async Task<List<Dungeon>> GetDungeons(
Vector2i position,
DungeonConfig config,
List<IDunGenLayer> layers,
+ HashSet<Vector2i> reservedTiles,
int seed,
Random random,
- HashSet<Vector2i>? reserved = null,
List<Dungeon>? existing = null)
{
var dungeons = new List<Dungeon>();
- var reservedTiles = reserved == null ? new HashSet<Vector2i>() : new HashSet<Vector2i>(reserved);
// Don't pass dungeons back up the "stack". They are ref types though it's a caller problem if they start trying to mutate it.
if (existing != null)
foreach (var layer in layers)
{
- var dungCount = dungeons.Count;
- await RunLayer(i, count, config, dungeons, position, layer, reservedTiles, seed, random);
+ var dungCount = dungeons.Count;
+ await RunLayer(dungeons, position, layer, reservedTiles, seed, random);
if (config.ReserveTiles)
{
await SuspendDungeon();
if (!ValidateResume())
- return (new List<Dungeon>(), new HashSet<Vector2i>());
+ return new List<Dungeon>();
}
}
- // Only return the new dungeons and applicable reserved tiles.
- return (dungeons[(existing?.Count ?? 0)..], config.ReturnReserved ? reservedTiles : new HashSet<Vector2i>());
+ return dungeons;
}
- protected override async Task<(List<Dungeon>, DungeonData)> Process()
+ protected override async Task<List<Dungeon>?> Process()
{
_sawmill.Info($"Generating dungeon {_gen} with seed {_seed} on {_entManager.ToPrettyString(_gridUid)}");
_grid.CanSplit = false;
var random = new Random(_seed);
- var oldTileCount = _reservedTiles?.Count ?? 0;
var position = (_position + random.NextPolarVector2(_gen.MinOffset, _gen.MaxOffset)).Floored();
- var (dungeons, _) = await GetDungeons(position, _gen, _gen.Layers, _seed, random, reserved: _reservedTiles);
+ // Tiles we can no longer generate on due to being reserved elsewhere.
+ var reservedTiles = new HashSet<Vector2i>();
+
+ var dungeons = await GetDungeons(position, _gen, _gen.Layers, reservedTiles, _seed, random);
// To make it slightly more deterministic treat this RNG as separate ig.
// Post-processing after finishing loading.
}
// Defer splitting so they don't get spammed and so we don't have to worry about tracking the grid along the way.
- DebugTools.Assert(oldTileCount == (_reservedTiles?.Count ?? 0));
_grid.CanSplit = true;
_entManager.System<GridFixtureSystem>().CheckSplits(_gridUid);
var npcSystem = _entManager.System<NPCSystem>();
}
_sawmill.Info($"Finished generating dungeon {_gen} with seed {_seed}");
- return (dungeons, _data);
+ return dungeons;
}
private async Task RunLayer(
- int runCount,
- int maxRuns,
- DungeonConfig config,
List<Dungeon> dungeons,
Vector2i position,
IDunGenLayer layer,
int seed,
Random random)
{
- // _sawmill.Debug($"Doing postgen {layer.GetType()} for {_gen} with seed {_seed}");
+ _sawmill.Debug($"Doing postgen {layer.GetType()} for {_gen} with seed {_seed}");
// If there's a way to just call the methods directly for the love of god tell me.
// Some of these don't care about reservedtiles because they only operate on dungeon tiles (which should
case AutoCablingDunGen cabling:
await PostGen(cabling, dungeons[^1], reservedTiles, random);
break;
+ case BiomeMarkerLayerDunGen markerPost:
+ await PostGen(markerPost, dungeons[^1], reservedTiles, random);
+ break;
+ case BiomeDunGen biome:
+ await PostGen(biome, dungeons[^1], reservedTiles, random);
+ break;
case BoundaryWallDunGen boundary:
await PostGen(boundary, dungeons[^1], reservedTiles, random);
break;
- case ChunkDunGen chunk:
- dungeons.Add(await PostGen(chunk, reservedTiles, random));
- break;
case CornerClutterDunGen clutter:
await PostGen(clutter, dungeons[^1], reservedTiles, random);
break;
await PostGen(flank, dungeons[^1], reservedTiles, random);
break;
case ExteriorDunGen exterior:
- dungeons.AddRange(await GenerateExteriorDungen(runCount, maxRuns, position, exterior, reservedTiles, random));
+ dungeons.AddRange(await GenerateExteriorDungen(position, exterior, reservedTiles, random));
break;
case FillGridDunGen fill:
await GenerateFillDunGen(fill, dungeons, reservedTiles);
case PrototypeDunGen prototypo:
var groupConfig = _prototype.Index(prototypo.Proto);
position = (position + random.NextPolarVector2(groupConfig.MinOffset, groupConfig.MaxOffset)).Floored();
- List<Dungeon>? inheritedDungeons = null;
- HashSet<Vector2i>? inheritedReserved = null;
-
- switch (prototypo.InheritReserved)
- {
- case ReservedInheritance.All:
- inheritedReserved = new HashSet<Vector2i>(reservedTiles);
- break;
- case ReservedInheritance.None:
- break;
- default:
- throw new NotImplementedException();
- }
switch (prototypo.InheritDungeons)
{
case DungeonInheritance.All:
- inheritedDungeons = dungeons;
+ dungeons.AddRange(await GetDungeons(position, groupConfig, groupConfig.Layers, reservedTiles, seed, random, existing: dungeons));
break;
case DungeonInheritance.Last:
- inheritedDungeons = dungeons.GetRange(dungeons.Count - 1, 1);
+ dungeons.AddRange(await GetDungeons(position, groupConfig, groupConfig.Layers, reservedTiles, seed, random, existing: dungeons.GetRange(dungeons.Count - 1, 1)));
break;
case DungeonInheritance.None:
+ dungeons.AddRange(await GetDungeons(position, groupConfig, groupConfig.Layers, reservedTiles, seed, random));
break;
- default:
- throw new NotImplementedException();
- }
-
- var (newDungeons, newReserved) = await GetDungeons(position,
- groupConfig,
- groupConfig.Layers,
- seed,
- random,
- reserved: inheritedReserved,
- existing: inheritedDungeons);
- dungeons.AddRange(newDungeons);
-
- if (groupConfig.ReturnReserved)
- {
- reservedTiles.UnionWith(newReserved);
}
break;
case ReplaceTileDunGen replace:
await GenerateTileReplacementDunGen(replace, dungeons, reservedTiles, random);
break;
- case RoofDunGen roof:
- await RoofGen(roof, dungeons, reservedTiles, random);
- break;
case RoomEntranceDunGen rEntrance:
await PostGen(rEntrance, dungeons[^1], reservedTiles, random);
break;
- case SampleDecalDunGen sdec:
- await PostGen(sdec, dungeons, reservedTiles, random);
- break;
- case SampleEntityDunGen sent:
- await PostGen(sent, dungeons, reservedTiles, random);
- break;
- case SampleTileDunGen stile:
- await PostGen(stile, dungeons, reservedTiles, random);
- break;
case SplineDungeonConnectorDunGen spline:
dungeons.Add(await PostGen(spline, dungeons, reservedTiles, random));
break;
}
}
+ private void LogDataError(Type type)
+ {
+ _sawmill.Error($"Unable to find dungeon data keys for {type}");
+ }
+
[Pure]
private bool ValidateResume()
{
await SuspendIfOutOfTime();
}
-
- private void AddLoadedEntity(Vector2i tile, EntityUid ent)
- {
- _data.Entities[ent] = tile;
- }
-
- private void AddLoadedDecal(Vector2 tile, uint decal)
- {
- _data.Decals[decal] = tile;
- }
-
- private void AddLoadedTile(Vector2i index, Tile tile)
- {
- _data.Tiles[index] = tile;
- }
}
return roomRotation;
}
- public DungeonData SpawnRoom(
+ public void SpawnRoom(
EntityUid gridUid,
MapGridComponent grid,
Matrix3x2 roomTransform,
var templateMapUid = _maps.GetMapOrInvalid(roomMap);
var templateGrid = Comp<MapGridComponent>(templateMapUid);
var roomDimensions = room.Size;
- var data = new DungeonData();
var finalRoomRotation = roomTransform.Rotation();
}
_tiles.Add((rounded, tileRef.Tile));
- data.Tiles[rounded] = tileRef.Tile;
if (clearExisting)
{
// TODO: Copy the templated entity as is with serv
var ent = Spawn(protoId, new EntityCoordinates(gridUid, childPos));
- data.Entities.Add(ent, childPos.Floored());
var childXform = _xformQuery.GetComponent(ent);
var anchored = templateXform.Anchored;
var result = _decals.TryAddDecal(
decal.Id,
new EntityCoordinates(gridUid, position),
- out var did,
+ out _,
decal.Color,
angle,
decal.ZIndex,
decal.Cleanable);
- data.Decals.Add(did, position);
-
DebugTools.Assert(result);
}
}
-
- return data;
}
}
MapGridComponent grid,
Vector2i position,
int seed,
- EntityCoordinates? coordinates = null,
- HashSet<Vector2i>? reservedTiles = null)
+ EntityCoordinates? coordinates = null)
{
var cancelToken = new CancellationTokenSource();
var job = new DungeonJob.DungeonJob(
seed,
position,
coordinates,
- cancelToken.Token,
- reservedTiles);
+ cancelToken.Token);
_dungeonJobs.Add(job, cancelToken);
_dungeonJobQueue.EnqueueJob(job);
}
- public async Task<(List<Dungeon>, DungeonData)> GenerateDungeonAsync(
+ public async Task<List<Dungeon>> GenerateDungeonAsync(
DungeonConfig gen,
EntityUid gridUid,
MapGridComponent grid,
Vector2i position,
- int seed,
- HashSet<Vector2i>? reservedTiles = null)
+ int seed)
{
var cancelToken = new CancellationTokenSource();
var job = new DungeonJob.DungeonJob(
seed,
position,
null,
- cancelToken.Token,
- reservedTiles);
+ cancelToken.Token);
_dungeonJobs.Add(job, cancelToken);
_dungeonJobQueue.EnqueueJob(job);
throw job.Exception;
}
- return job.Result;
+ return job.Result!;
}
public Angle GetDungeonRotation(int seed)
+using System.Collections;
using System.Linq;
using System.Numerics;
using System.Threading;
using System.Threading.Tasks;
+using Content.Server.Atmos;
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Robust.Shared.CPUJob.JobQueues;
using Content.Server.Ghost.Roles.Components;
+using Content.Server.Parallax;
using Content.Server.Procedural;
using Content.Server.Salvage.Expeditions;
+using Content.Server.Salvage.Expeditions.Structure;
using Content.Shared.Atmos;
using Content.Shared.Construction.EntitySystems;
using Content.Shared.Dataset;
using Content.Shared.Gravity;
+using Content.Shared.Parallax.Biomes;
using Content.Shared.Physics;
using Content.Shared.Procedural;
using Content.Shared.Procedural.Loot;
using Content.Shared.Salvage.Expeditions;
using Content.Shared.Salvage.Expeditions.Modifiers;
using Content.Shared.Shuttles.Components;
+using Content.Shared.Storage;
using Robust.Shared.Collections;
+using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Content.Server.Shuttles.Components;
-using Content.Shared.Procedural.Components;
namespace Content.Server.Salvage;
.GetMission(difficultyProto, _missionParams.Seed);
var missionBiome = _prototypeManager.Index<SalvageBiomeModPrototype>(mission.Biome);
- BiomeComponent? biome = null;
if (missionBiome.BiomePrototype != null)
{
+ var biome = _entManager.AddComponent<BiomeComponent>(mapUid);
var biomeSystem = _entManager.System<BiomeSystem>();
-
- biome = biomeSystem.AddBiome(mapUid, missionBiome.BiomePrototype.Value, mission.Seed);
+ biomeSystem.SetTemplate(mapUid, biome, _prototypeManager.Index<BiomeTemplatePrototype>(missionBiome.BiomePrototype));
+ biomeSystem.SetSeed(mapUid, biome, mission.Seed);
+ _entManager.Dirty(mapUid, biome);
// Gravity
var gravity = _entManager.EnsureComponent<GravityComponent>(mapUid);
dungeonOffset = dungeonRotation.RotateVec(dungeonOffset);
var dungeonMod = _prototypeManager.Index<SalvageDungeonModPrototype>(mission.Dungeon);
var dungeonConfig = _prototypeManager.Index(dungeonMod.Proto);
- var (dungeons, data) = await WaitAsyncTask(_dungeon.GenerateDungeonAsync(dungeonConfig, mapUid, grid, (Vector2i)dungeonOffset,
+ var dungeons = await WaitAsyncTask(_dungeon.GenerateDungeonAsync(dungeonConfig, mapUid, grid, (Vector2i)dungeonOffset,
_missionParams.Seed));
var dungeon = dungeons.First();
return false;
}
- // Don't modify any dungeon tiles with chunk gen.
- // Have to defer biome loading until the primo dungen is generated.
- if (biome != null)
+ expedition.DungeonLocation = dungeonOffset;
+
+ List<Vector2i> reservedTiles = new();
+
+ foreach (var tile in _map.GetTilesIntersecting(mapUid, grid, new Circle(Vector2.Zero, landingPadRadius), false))
{
- foreach (var tile in dungeon.AllTiles)
- {
- biome.ModifiedTiles.Add(tile);
- }
+ if (!_biome.TryGetBiomeTile(mapUid, grid, tile.GridIndices, out _))
+ continue;
- biome.Enabled = true;
+ reservedTiles.Add(tile.GridIndices);
}
- expedition.DungeonLocation = dungeonOffset;
-
var budgetEntries = new List<IBudgetEntry>();
/*
if (!lootProto.Guaranteed)
continue;
- foreach (var rule in lootProto.LootRules)
+ try
{
- switch (rule)
- {
- case BiomeLoot biomeLoot:
- _biome.AddLayer(mapUid, $"{rule}", biomeLoot.Proto);
- break;
- }
+ await SpawnDungeonLoot(lootProto, mapUid);
+ }
+ catch (Exception e)
+ {
+ _sawmill.Error($"Failed to spawn guaranteed loot {lootProto.ID}: {e}");
}
}
// oh noooooooooooo
}
+
+ private async Task SpawnDungeonLoot(SalvageLootPrototype loot, EntityUid gridUid)
+ {
+ for (var i = 0; i < loot.LootRules.Count; i++)
+ {
+ var rule = loot.LootRules[i];
+
+ switch (rule)
+ {
+ case BiomeMarkerLoot biomeLoot:
+ {
+ if (_entManager.TryGetComponent<BiomeComponent>(gridUid, out var biome))
+ {
+ _biome.AddMarkerLayer(gridUid, biome, biomeLoot.Prototype);
+ }
+ }
+ break;
+ case BiomeTemplateLoot biomeLoot:
+ {
+ if (_entManager.TryGetComponent<BiomeComponent>(gridUid, out var biome))
+ {
+ _biome.AddTemplate(gridUid, biome, "Loot", _prototypeManager.Index<BiomeTemplatePrototype>(biomeLoot.Prototype), i);
+ }
+ }
+ break;
+ }
+ }
+ }
}
using Content.Server.GameTicking;
using Content.Server.GameTicking.Events;
using Content.Server.Parallax;
-using Content.Server.Procedural;
using Content.Server.Screens.Components;
using Content.Server.Shuttles.Components;
using Content.Server.Shuttles.Events;
using Content.Shared.GameTicking;
using Content.Shared.Mobs.Components;
using Content.Shared.Movement.Components;
+using Content.Shared.Parallax.Biomes;
using Content.Shared.Salvage;
using Content.Shared.Shuttles.Components;
using Content.Shared.Tiles;
/// </summary>
private const float RoundStartFTLDuration = 10f;
- private readonly List<EntProtoId> _arrivalsBiomeOptions = new()
+ private readonly List<ProtoId<BiomeTemplatePrototype>> _arrivalsBiomeOptions = new()
{
- "BiomeGrasslands",
- "BiomeLowDesert",
- "BiomeSnow",
+ "Grasslands",
+ "LowDesert",
+ "Snow",
};
public override void Initialize()
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
-using Content.Server.Decals;
using Content.Server.Shuttles.Components;
using Content.Server.Shuttles.Events;
using Content.Server.Station.Events;
using Content.Shared.Body.Components;
using Content.Shared.CCVar;
using Content.Shared.Database;
-using Content.Shared.Decals;
using Content.Shared.Ghost;
using Content.Shared.Maps;
using Content.Shared.Parallax;
var transform = _physics.GetRelativePhysicsTransform((uid, xform), xform.MapUid.Value);
var aabbs = new List<Box2>(manager.Fixtures.Count);
var tileSet = new List<(Vector2i, Tile)>();
- TryComp(xform.MapUid.Value, out DecalGridComponent? decalGrid);
foreach (var fixture in manager.Fixtures.Values)
{
aabb = aabb.Enlarged(0.2f);
aabbs.Add(aabb);
- if (decalGrid != null)
- {
- foreach (var decal in _decals.GetDecalsIntersecting(xform.MapUid.Value, aabb))
- {
- _decals.RemoveDecal(xform.MapUid.Value, decal.Index, decalGrid);
- }
- }
-
+ // Handle clearing biome stuff as relevant.
tileSet.Clear();
+ _biomes.ReserveTiles(xform.MapUid.Value, aabb, tileSet);
_lookupEnts.Clear();
_immuneEnts.Clear();
// TODO: Ideally we'd query first BEFORE moving grid but needs adjustments above.
using Content.Server.Administration.Logs;
using Content.Server.Body.Systems;
using Content.Server.Buckle.Systems;
-using Content.Server.Decals;
using Content.Server.Parallax;
using Content.Server.Procedural;
using Content.Server.Shuttles.Components;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IPrototypeManager _protoManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
- [Dependency] private readonly ITileDefinitionManager _tileDefManager = default!;
[Dependency] private readonly BiomeSystem _biomes = default!;
[Dependency] private readonly BodySystem _bobby = default!;
[Dependency] private readonly BuckleSystem _buckle = default!;
[Dependency] private readonly DamageableSystem _damageSys = default!;
- [Dependency] private readonly DecalSystem _decals = default!;
[Dependency] private readonly DockingSystem _dockSystem = default!;
[Dependency] private readonly DungeonSystem _dungeon = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
using Content.Server.Station.Systems;
+using Content.Shared.Parallax.Biomes;
using Robust.Shared.Prototypes;
namespace Content.Server.Station.Components;
public sealed partial class StationBiomeComponent : Component
{
[DataField(required: true)]
- public EntProtoId Biome = "BiomeGrasslands";
+ public ProtoId<BiomeTemplatePrototype> Biome = "Grasslands";
// If null, its random
[DataField]
using Content.Server.Parallax;
-using Content.Server.Procedural;
using Content.Server.Station.Components;
using Content.Server.Station.Events;
using Robust.Shared.Prototypes;
if (!TryComp(uid, out ActorComponent? actor))
{
RemComp<TabletopGamerComponent>(uid);
- continue;
+ return;
}
if (actor.PlayerSession.Status != SessionStatus.InGame || !CanSeeTable(uid, gamer.Tabletop))
+++ /dev/null
-using Robust.Shared.Configuration;
-
-namespace Content.Shared.CCVar;
-
-public sealed partial class CCVars
-{
- /// <summary>
- /// Load range for biomes. Set this higher than PVS so server can have some time to load in before the client arrives.
- /// </summary>
- public static readonly CVarDef<float> BiomeLoadRange =
- CVarDef.Create("biome.load_range", 20f, CVar.SERVERONLY);
-
- /// <summary>
- /// Time allocation (ms) for how long biomes are allowed to load.
- /// </summary>
- public static readonly CVarDef<float> BiomeLoadTime =
- CVarDef.Create("biome.load_time", 0.03f, CVar.SERVERONLY);
-}
--- /dev/null
+using Content.Shared.Parallax.Biomes.Layers;
+using Content.Shared.Parallax.Biomes.Markers;
+using Robust.Shared.GameStates;
+using Robust.Shared.Noise;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
+
+namespace Content.Shared.Parallax.Biomes;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true), Access(typeof(SharedBiomeSystem))]
+public sealed partial class BiomeComponent : Component
+{
+ /// <summary>
+ /// Do we load / deload.
+ /// </summary>
+ [DataField, ViewVariables(VVAccess.ReadWrite), Access(Other = AccessPermissions.ReadWriteExecute)]
+ public bool Enabled = true;
+
+ [ViewVariables(VVAccess.ReadWrite), DataField("seed")]
+ [AutoNetworkedField]
+ public int Seed = -1;
+
+ /// <summary>
+ /// The underlying entity, decal, and tile layers for the biome.
+ /// </summary>
+ [DataField("layers")]
+ [AutoNetworkedField]
+ public List<IBiomeLayer> Layers = new();
+
+ /// <summary>
+ /// Templates to use for <see cref="Layers"/>.
+ /// If this is set on mapinit, it will fill out layers automatically.
+ /// If not set, use <c>BiomeSystem</c> to do it.
+ /// Prototype reloading will also use this.
+ /// </summary>
+ [DataField]
+ public ProtoId<BiomeTemplatePrototype>? Template;
+
+ /// <summary>
+ /// If we've already generated a tile and couldn't deload it then we won't ever reload it in future.
+ /// Stored by [Chunkorigin, Tiles]
+ /// </summary>
+ [DataField("modifiedTiles")]
+ public Dictionary<Vector2i, HashSet<Vector2i>> ModifiedTiles = new();
+
+ /// <summary>
+ /// Decals that have been loaded as a part of this biome.
+ /// </summary>
+ [DataField("decals")]
+ public Dictionary<Vector2i, Dictionary<uint, Vector2i>> LoadedDecals = new();
+
+ [DataField("entities")]
+ public Dictionary<Vector2i, Dictionary<EntityUid, Vector2i>> LoadedEntities = new();
+
+ /// <summary>
+ /// Currently active chunks
+ /// </summary>
+ [DataField("loadedChunks")]
+ public HashSet<Vector2i> LoadedChunks = new();
+
+ #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>
+ /// Track what markers we've loaded already to avoid double-loading.
+ /// </summary>
+ [DataField("loadedMarkers", customTypeSerializer:typeof(PrototypeIdDictionarySerializer<HashSet<Vector2i>, BiomeMarkerLayerPrototype>))]
+ public Dictionary<string, HashSet<Vector2i>> LoadedMarkers = new();
+
+ [DataField]
+ public HashSet<ProtoId<BiomeMarkerLayerPrototype>> MarkerLayers = new();
+
+ /// <summary>
+ /// One-tick forcing of marker layers to bulldoze any entities in the way.
+ /// </summary>
+ [DataField]
+ public HashSet<ProtoId<BiomeMarkerLayerPrototype>> ForcedMarkerLayers = new();
+
+ #endregion
+}
--- /dev/null
+using Content.Shared.Parallax.Biomes.Layers;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Parallax.Biomes;
+
+/// <summary>
+/// A preset group of biome layers to be used for a <see cref="BiomeComponent"/>
+/// </summary>
+[Prototype]
+public sealed partial class BiomeTemplatePrototype : IPrototype
+{
+ [IdDataField] public string ID { get; private set; } = default!;
+
+ [DataField("layers")]
+ public List<IBiomeLayer> Layers = new();
+}
--- /dev/null
+using Content.Shared.Decals;
+using Content.Shared.Maps;
+using Robust.Shared.Noise;
+using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
+
+namespace Content.Shared.Parallax.Biomes.Layers;
+
+[Serializable, NetSerializable]
+public sealed partial class BiomeDecalLayer : IBiomeWorldLayer
+{
+ /// <inheritdoc/>
+ [DataField("allowedTiles", customTypeSerializer:typeof(PrototypeIdListSerializer<ContentTileDefinition>))]
+ public List<string> AllowedTiles { get; private set; } = new();
+
+ /// <summary>
+ /// Divide each tile up by this amount.
+ /// </summary>
+ [DataField("divisions")]
+ public float Divisions = 1f;
+
+ [DataField("noise")]
+ public FastNoiseLite Noise { get; private set; } = new(0);
+
+ /// <inheritdoc/>
+ [DataField("threshold")]
+ public float Threshold { get; private set; } = 0.8f;
+
+ /// <inheritdoc/>
+ [DataField("invert")] public bool Invert { get; private set; } = false;
+
+ [DataField("decals", required: true, customTypeSerializer:typeof(PrototypeIdListSerializer<DecalPrototype>))]
+ public List<string> Decals = new();
+}
--- /dev/null
+using Robust.Shared.Noise;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Parallax.Biomes.Layers;
+
+/// <summary>
+/// Dummy layer that specifies a marker to be replaced by external code.
+/// For example if they wish to add their own layers at specific points across different templates.
+/// </summary>
+[Serializable, NetSerializable]
+public sealed partial class BiomeDummyLayer : IBiomeLayer
+{
+ [DataField("id", required: true)] public string ID = string.Empty;
+
+ public FastNoiseLite Noise { get; } = new();
+ public float Threshold { get; }
+ public bool Invert { get; }
+}
--- /dev/null
+using Content.Shared.Maps;
+using Robust.Shared.Noise;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
+
+namespace Content.Shared.Parallax.Biomes.Layers;
+
+[Serializable, NetSerializable]
+public sealed partial class BiomeEntityLayer : IBiomeWorldLayer
+{
+ /// <inheritdoc/>
+ [DataField("allowedTiles", customTypeSerializer:typeof(PrototypeIdListSerializer<ContentTileDefinition>))]
+ public List<string> AllowedTiles { get; private set; } = new();
+
+ [DataField("noise")] public FastNoiseLite Noise { get; private set; } = new(0);
+
+ /// <inheritdoc/>
+ [DataField("threshold")]
+ public float Threshold { get; private set; } = 0.5f;
+
+ /// <inheritdoc/>
+ [DataField("invert")] public bool Invert { get; private set; } = false;
+
+ [DataField("entities", required: true, customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
+ public List<string> Entities = new();
+}
--- /dev/null
+using Robust.Shared.Noise;
+using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Shared.Parallax.Biomes.Layers;
+
+/// <summary>
+/// Contains more biome layers recursively via a biome template.
+/// Can be used for sub-biomes.
+/// </summary>
+[Serializable, NetSerializable]
+public sealed partial class BiomeMetaLayer : IBiomeLayer
+{
+ [DataField("noise")]
+ public FastNoiseLite Noise { get; private set; } = new(0);
+
+ /// <inheritdoc/>
+ [DataField("threshold")]
+ public float Threshold { get; private set; } = -1f;
+
+ /// <inheritdoc/>
+ [DataField("invert")]
+ public bool Invert { get; private set; }
+
+ [DataField("template", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<BiomeTemplatePrototype>))]
+ public string Template = string.Empty;
+}
using Robust.Shared.Noise;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
-namespace Content.Shared.Procedural.DungeonLayers;
+namespace Content.Shared.Parallax.Biomes.Layers;
-/// <summary>
-/// Samples noise and spawns the specified tile in the dungeon area.
-/// </summary>
[Serializable, NetSerializable]
-public sealed partial class SampleTileDunGen : IDunGenLayer
+public sealed partial class BiomeTileLayer : IBiomeLayer
{
- /// <summary>
- /// Reserve any tiles we update.
- /// </summary>
- [DataField]
- public bool ReserveTiles = true;
-
[DataField] public FastNoiseLite Noise { get; private set; } = new(0);
+ /// <inheritdoc/>
[DataField]
public float Threshold { get; private set; } = 0.5f;
+ /// <inheritdoc/>
[DataField] public bool Invert { get; private set; } = false;
/// <summary>
--- /dev/null
+using Robust.Shared.Noise;
+
+namespace Content.Shared.Parallax.Biomes.Layers;
+
+[ImplicitDataDefinitionForInheritors]
+public partial interface IBiomeLayer
+{
+ /// <summary>
+ /// Seed is used an offset from the relevant BiomeComponent's seed.
+ /// </summary>
+ FastNoiseLite Noise { get; }
+
+ /// <summary>
+ /// Threshold for this layer to be present. If set to 0 forces it for every tile.
+ /// </summary>
+ float Threshold { get; }
+
+ /// <summary>
+ /// Is the thresold inverted so we need to be lower than it.
+ /// </summary>
+ public bool Invert { get; }
+}
--- /dev/null
+namespace Content.Shared.Parallax.Biomes.Layers;
+
+/// <summary>
+/// Handles actual objects such as decals and entities.
+/// </summary>
+public partial interface IBiomeWorldLayer : IBiomeLayer
+{
+ /// <summary>
+ /// What tiles we're allowed to spawn on, real or biome.
+ /// </summary>
+ List<string> AllowedTiles { get; }
+}
--- /dev/null
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Shared.Parallax.Biomes.Markers;
+
+/// <summary>
+/// Spawns entities inside of the specified area with the minimum specified radius.
+/// </summary>
+[Prototype]
+public sealed partial class BiomeMarkerLayerPrototype : IBiomeMarkerLayer
+{
+ [IdDataField] public string ID { get; private set; } = default!;
+
+ /// <summary>
+ /// Checks for the relevant entity for the tile before spawning. Useful for substituting walls with ore veins for example.
+ /// </summary>
+ [DataField]
+ public Dictionary<EntProtoId, EntProtoId> EntityMask { get; private set; } = new();
+
+ /// <summary>
+ /// Default prototype to spawn. If null will fall back to entity mask.
+ /// </summary>
+ [DataField]
+ public string? Prototype { get; private set; }
+
+ /// <summary>
+ /// Minimum radius between 2 points
+ /// </summary>
+ [DataField("radius")]
+ public float Radius = 32f;
+
+ /// <summary>
+ /// Maximum amount of group spawns
+ /// </summary>
+ [DataField("maxCount")]
+ public int MaxCount = int.MaxValue;
+
+ /// <summary>
+ /// Minimum entities to spawn in one group.
+ /// </summary>
+ [DataField]
+ public int MinGroupSize = 1;
+
+ /// <summary>
+ /// Maximum entities to spawn in one group.
+ /// </summary>
+ [DataField]
+ public int MaxGroupSize = 1;
+
+ /// <inheritdoc />
+ [DataField("size")]
+ public int Size { get; private set; } = 128;
+}
--- /dev/null
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Parallax.Biomes.Markers;
+
+/// <summary>
+/// Specifies one-off marker points to be used. This could be for dungeon markers, mob markers, etc.
+/// These are run outside of the tile / decal / entity layers.
+/// </summary>
+public interface IBiomeMarkerLayer : IPrototype
+{
+ /// <summary>
+ /// Biome template to use as a mask for this layer.
+ /// </summary>
+ public Dictionary<EntProtoId, EntProtoId> EntityMask { get; }
+
+ public string? Prototype { get; }
+
+ /// <summary>
+ /// How large the pre-generated points area is.
+ /// </summary>
+ public int Size { get; }
+}
--- /dev/null
+using System.Diagnostics.CodeAnalysis;
+using System.Numerics;
+using Content.Shared.Maps;
+using Content.Shared.Parallax.Biomes.Layers;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Noise;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.Manager;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Parallax.Biomes;
+
+public abstract class SharedBiomeSystem : EntitySystem
+{
+ [Dependency] protected readonly IPrototypeManager ProtoManager = default!;
+ [Dependency] private readonly ISerializationManager _serManager = default!;
+ [Dependency] protected readonly ITileDefinitionManager TileDefManager = default!;
+ [Dependency] private readonly TileSystem _tile = default!;
+ [Dependency] private readonly SharedMapSystem _map = default!;
+
+ protected const byte ChunkSize = 8;
+
+ private T Pick<T>(List<T> collection, float value)
+ {
+ // 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)
+ return collection[0];
+
+ var randValue = value * collection.Count;
+
+ foreach (var item in collection)
+ {
+ randValue -= 1f;
+
+ if (randValue <= 0f)
+ {
+ return item;
+ }
+ }
+
+ throw new ArgumentOutOfRangeException();
+ }
+
+ private int Pick(int count, float value)
+ {
+ value %= 1f;
+ value = Math.Clamp(value, 0f, 1f);
+
+ if (count == 1)
+ return 0;
+
+ value *= count;
+
+ for (var i = 0; i < count; i++)
+ {
+ value -= 1f;
+
+ if (value <= 0f)
+ {
+ return i;
+ }
+ }
+
+ throw new ArgumentOutOfRangeException();
+ }
+
+ public bool TryGetBiomeTile(EntityUid uid, MapGridComponent grid, Vector2i indices, [NotNullWhen(true)] out Tile? tile)
+ {
+ if (_map.TryGetTileRef(uid, grid, indices, out var tileRef) && !tileRef.Tile.IsEmpty)
+ {
+ tile = tileRef.Tile;
+ return true;
+ }
+
+ if (!TryComp<BiomeComponent>(uid, out var biome))
+ {
+ tile = null;
+ return false;
+ }
+
+ return TryGetBiomeTile(indices, biome.Layers, biome.Seed, (uid, grid), out tile);
+ }
+
+ /// <summary>
+ /// Tries to get the tile, real or otherwise, for the specified indices.
+ /// </summary>
+ public bool TryGetBiomeTile(Vector2i indices, List<IBiomeLayer> layers, int seed, Entity<MapGridComponent>? grid, [NotNullWhen(true)] out Tile? tile)
+ {
+ if (grid is { } gridEnt && _map.TryGetTileRef(gridEnt, gridEnt.Comp, indices, out var tileRef) && !tileRef.Tile.IsEmpty)
+ {
+ tile = tileRef.Tile;
+ return true;
+ }
+
+ return TryGetTile(indices, layers, seed, grid, out tile);
+ }
+
+ /// <summary>
+ /// Tries to get the tile, real or otherwise, for the specified indices.
+ /// </summary>
+ [Obsolete("Use the Entity<MapGridComponent>? overload")]
+ public bool TryGetBiomeTile(Vector2i indices, List<IBiomeLayer> layers, int seed, MapGridComponent? grid, [NotNullWhen(true)] out Tile? tile)
+ {
+ return TryGetBiomeTile(indices, layers, seed, grid == null ? null : (grid.Owner, grid), out tile);
+ }
+
+ /// <summary>
+ /// Gets the underlying biome tile, ignoring any existing tile that may be there.
+ /// </summary>
+ public bool TryGetTile(Vector2i indices, List<IBiomeLayer> layers, int seed, Entity<MapGridComponent>? grid, [NotNullWhen(true)] out Tile? tile)
+ {
+ for (var i = layers.Count - 1; i >= 0; i--)
+ {
+ var layer = layers[i];
+ var noiseCopy = GetNoise(layer.Noise, seed);
+
+ var invert = layer.Invert;
+ var value = noiseCopy.GetNoise(indices.X, indices.Y);
+ value = invert ? value * -1 : value;
+
+ if (value < layer.Threshold)
+ continue;
+
+ // Check if the tile is from meta layer, otherwise fall back to default layers.
+ if (layer is BiomeMetaLayer meta)
+ {
+ if (TryGetBiomeTile(indices, ProtoManager.Index<BiomeTemplatePrototype>(meta.Template).Layers, seed, grid, out tile))
+ {
+ return true;
+ }
+
+ continue;
+ }
+
+ if (layer is not BiomeTileLayer tileLayer)
+ continue;
+
+ if (TryGetTile(indices, noiseCopy, tileLayer.Invert, tileLayer.Threshold, ProtoManager.Index(tileLayer.Tile), tileLayer.Variants, out tile))
+ {
+ return true;
+ }
+ }
+
+ tile = null;
+ return false;
+ }
+
+ /// <summary>
+ /// Gets the underlying biome tile, ignoring any existing tile that may be there.
+ /// </summary>
+ [Obsolete("Use the Entity<MapGridComponent>? overload")]
+ public bool TryGetTile(Vector2i indices, List<IBiomeLayer> layers, int seed, MapGridComponent? grid, [NotNullWhen(true)] out Tile? tile)
+ {
+ return TryGetTile(indices, layers, seed, grid == null ? null : (grid.Owner, grid), out tile);
+ }
+
+ /// <summary>
+ /// Gets the underlying biome tile, ignoring any existing tile that may be there.
+ /// </summary>
+ private bool TryGetTile(Vector2i indices, FastNoiseLite noise, bool invert, float threshold, ContentTileDefinition tileDef, List<byte>? variants, [NotNullWhen(true)] out Tile? tile)
+ {
+ var found = noise.GetNoise(indices.X, indices.Y);
+ found = invert ? found * -1 : found;
+
+ if (found < threshold)
+ {
+ tile = null;
+ return false;
+ }
+
+ byte variant = 0;
+ var variantCount = variants?.Count ?? tileDef.Variants;
+
+ // Pick a variant tile if they're available as well
+ if (variantCount > 1)
+ {
+ var variantValue = (noise.GetNoise(indices.X * 8, indices.Y * 8, variantCount) + 1f) * 100;
+ variant = _tile.PickVariant(tileDef, (int)variantValue);
+ }
+
+ tile = new Tile(tileDef.TileId, variant);
+ return true;
+ }
+
+ /// <summary>
+ /// Tries to get the relevant entity for this tile.
+ /// </summary>
+ public bool TryGetEntity(Vector2i indices, BiomeComponent component, Entity<MapGridComponent>? grid,
+ [NotNullWhen(true)] out string? entity)
+ {
+ if (!TryGetBiomeTile(indices, component.Layers, component.Seed, grid, out var tile))
+ {
+ entity = null;
+ return false;
+ }
+
+ return TryGetEntity(indices, component.Layers, tile.Value, component.Seed, grid, out entity);
+ }
+
+ /// <summary>
+ /// Tries to get the relevant entity for this tile.
+ /// </summary>
+ [Obsolete("Use the Entity<MapGridComponent>? overload")]
+ public bool TryGetEntity(Vector2i indices, BiomeComponent component, MapGridComponent grid,
+ [NotNullWhen(true)] out string? entity)
+ {
+ return TryGetEntity(indices, component, grid == null ? null : (grid.Owner, grid), out entity);
+ }
+
+ public bool TryGetEntity(Vector2i indices, List<IBiomeLayer> layers, Tile tileRef, int seed, Entity<MapGridComponent>? grid,
+ [NotNullWhen(true)] out string? entity)
+ {
+ var tileId = TileDefManager[tileRef.TypeId].ID;
+
+ for (var i = layers.Count - 1; i >= 0; i--)
+ {
+ var layer = layers[i];
+
+ switch (layer)
+ {
+ case BiomeDummyLayer:
+ continue;
+ case IBiomeWorldLayer worldLayer:
+ if (!worldLayer.AllowedTiles.Contains(tileId))
+ continue;
+
+ break;
+ case BiomeMetaLayer:
+ break;
+ default:
+ continue;
+ }
+
+ var noiseCopy = GetNoise(layer.Noise, seed);
+
+ var invert = layer.Invert;
+ var value = noiseCopy.GetNoise(indices.X, indices.Y);
+ value = invert ? value * -1 : value;
+
+ if (value < layer.Threshold)
+ continue;
+
+ if (layer is BiomeMetaLayer meta)
+ {
+ if (TryGetEntity(indices, ProtoManager.Index<BiomeTemplatePrototype>(meta.Template).Layers, tileRef, seed, grid, out entity))
+ {
+ return true;
+ }
+
+ continue;
+ }
+
+ // Decals might block entity so need to check if there's one in front of us.
+ if (layer is not BiomeEntityLayer biomeLayer)
+ {
+ entity = null;
+ return false;
+ }
+
+ var noiseValue = noiseCopy.GetNoise(indices.X, indices.Y, i);
+ entity = Pick(biomeLayer.Entities, (noiseValue + 1f) / 2f);
+ return true;
+ }
+
+ entity = null;
+ return false;
+ }
+
+ [Obsolete("Use the Entity<MapGridComponent>? overload")]
+ public bool TryGetEntity(Vector2i indices, List<IBiomeLayer> layers, Tile tileRef, int seed, MapGridComponent grid,
+ [NotNullWhen(true)] out string? entity)
+ {
+ return TryGetEntity(indices, layers, tileRef, seed, grid == null ? null : (grid.Owner, grid), out entity);
+ }
+
+ /// <summary>
+ /// Tries to get the relevant decals for this tile.
+ /// </summary>
+ public bool TryGetDecals(Vector2i indices, List<IBiomeLayer> layers, int seed, Entity<MapGridComponent>? grid,
+ [NotNullWhen(true)] out List<(string ID, Vector2 Position)>? decals)
+ {
+ if (!TryGetBiomeTile(indices, layers, seed, grid, out var tileRef))
+ {
+ decals = null;
+ return false;
+ }
+
+ var tileId = TileDefManager[tileRef.Value.TypeId].ID;
+
+ for (var i = layers.Count - 1; i >= 0; i--)
+ {
+ var layer = layers[i];
+
+ // Entities might block decal so need to check if there's one in front of us.
+ switch (layer)
+ {
+ case BiomeDummyLayer:
+ continue;
+ case IBiomeWorldLayer worldLayer:
+ if (!worldLayer.AllowedTiles.Contains(tileId))
+ continue;
+
+ break;
+ case BiomeMetaLayer:
+ break;
+ default:
+ continue;
+ }
+
+ var invert = layer.Invert;
+ var noiseCopy = GetNoise(layer.Noise, seed);
+ var value = noiseCopy.GetNoise(indices.X, indices.Y);
+ value = invert ? value * -1 : value;
+
+ if (value < layer.Threshold)
+ continue;
+
+ if (layer is BiomeMetaLayer meta)
+ {
+ if (TryGetDecals(indices, ProtoManager.Index<BiomeTemplatePrototype>(meta.Template).Layers, seed, grid, out decals))
+ {
+ return true;
+ }
+
+ continue;
+ }
+
+ // Check if the other layer should even render, if not then keep going.
+ if (layer is not BiomeDecalLayer decalLayer)
+ {
+ decals = null;
+ return false;
+ }
+
+ decals = new List<(string ID, Vector2 Position)>();
+
+ for (var x = 0; x < decalLayer.Divisions; x++)
+ {
+ for (var y = 0; y < decalLayer.Divisions; y++)
+ {
+ var index = new Vector2(indices.X + x * 1f / decalLayer.Divisions, indices.Y + y * 1f / decalLayer.Divisions);
+ var decalValue = noiseCopy.GetNoise(index.X, index.Y);
+ decalValue = invert ? decalValue * -1 : decalValue;
+
+ if (decalValue < decalLayer.Threshold)
+ continue;
+
+ decals.Add((Pick(decalLayer.Decals, (noiseCopy.GetNoise(indices.X, indices.Y, x + y * decalLayer.Divisions) + 1f) / 2f), index));
+ }
+ }
+
+ // Check other layers
+ if (decals.Count == 0)
+ continue;
+
+ return true;
+ }
+
+ decals = null;
+ return false;
+ }
+
+ /// <summary>
+ /// Tries to get the relevant decals for this tile.
+ /// </summary>
+ [Obsolete("Use the Entity<MapGridComponent>? overload")]
+ public bool TryGetDecals(Vector2i indices, List<IBiomeLayer> layers, int seed, MapGridComponent grid,
+ [NotNullWhen(true)] out List<(string ID, Vector2 Position)>? decals)
+ {
+ return TryGetDecals(indices, layers, seed, grid == null ? null : (grid.Owner, grid), out decals);
+ }
+
+ private FastNoiseLite GetNoise(FastNoiseLite seedNoise, int seed)
+ {
+ var noiseCopy = new FastNoiseLite();
+ _serManager.CopyTo(seedNoise, ref noiseCopy, notNullableOverride: true);
+ noiseCopy.SetSeed(noiseCopy.GetSeed() + seed);
+ // Ensure re-calculate is run.
+ noiseCopy.SetFractalOctaves(noiseCopy.GetFractalOctaves());
+ return noiseCopy;
+ }
+}
+++ /dev/null
-using Robust.Shared.Prototypes;
-
-namespace Content.Shared.Procedural.Components;
-
-/// <summary>
-/// A layer inside of <see cref="BiomeComponent"/>
-/// </summary>
-[DataRecord]
-public sealed record BiomeMetaLayer
-{
- /// <summary>
- /// Chunk dimensions for this meta layer. Will try to infer it from the first layer of the dungeon if null.
- /// </summary>
- [DataField]
- public int? Size;
-
- /// <summary>
- /// Meta layers that this one requires to be loaded first.
- /// Will ensure all of the chunks for our corresponding area are loaded.
- /// </summary>
- public List<string>? DependsOn;
-
- /// <summary>
- /// Can this layer be unloaded if no one is in range.
- /// </summary>
- public bool CanUnload = true;
-
- /// <summary>
- /// Dungeon config to load inside the specified area.
- /// </summary>
- [DataField(required: true)]
- public ProtoId<DungeonConfigPrototype> Dungeon = new();
-}
-
-[RegisterComponent]
-public sealed partial class BiomeComponent : Component
-{
- /// <summary>
- /// Can we load / unload chunks.
- /// </summary>
- [DataField]
- public bool Enabled = true;
-
- /// <summary>
- /// Areas queued for preloading. Will add these during <see cref="BiomeLoadJob"/> and then flag as modified so they retain.
- /// </summary>
- [DataField]
- public List<Box2i> PreloadAreas = new();
-
- /// <summary>
- /// Is there currently a job that's loading.
- /// </summary>
- public bool Loading = false;
-
- [DataField]
- public int Seed;
-
- /// <summary>
- /// Layer key and associated data.
- /// </summary>
- [DataField(required: true)]
- public Dictionary<string, BiomeMetaLayer> Layers = new();
-
- /// <summary>
- /// Layer removals that are pending.
- /// </summary>
- [DataField]
- public List<string> PendingRemovals = new();
-
- /// <summary>
- /// Data that is currently loaded.
- /// </summary>
- [DataField]
- public Dictionary<string, Dictionary<Vector2i, DungeonData>> LoadedData = new();
-
- /// <summary>
- /// Flag modified tiles so we don't try and unload / reload them.
- /// </summary>
- [DataField]
- public HashSet<Vector2i> ModifiedTiles = new();
-
- /// <summary>
- /// Bounds loaded by players for this tick.
- /// </summary>
- public List<Box2i> LoadedBounds = new();
-}
+++ /dev/null
-using Robust.Shared.GameStates;
-
-namespace Content.Shared.Procedural.Components;
-
-/// <summary>
-/// Will forcibly unload an entity no matter what. Useful if you have consistent entities that will never be default or the likes.
-/// </summary>
-[RegisterComponent, NetworkedComponent]
-public sealed partial class BiomeForceUnloadComponent : Component;
+++ /dev/null
-namespace Content.Shared.Procedural.Distance;
-
-public sealed partial class DunGenDistanceSquared : IDunGenDistance
-{
- [DataField]
- public float BlendWeight { get; set; } = 0.50f;
-}
public List<IDunGenLayer> Layers = new();
/// <summary>
- /// Should we reserve the tiles generated by this config so no other layers at the same level can spawn on this tile?
+ /// Should we reserve the tiles generated by this config so no other dungeons can spawn on it within the same job?
/// </summary>
[DataField]
public bool ReserveTiles;
- /// <summary>
- /// Should we return the reserved tiles to the upper level.
- /// Set to false if you don't care if this dungeon has its tiles overwritten at higher levels.
- /// </summary>
- [DataField]
- public bool ReturnReserved = true;
-
/// <summary>
/// Minimum times to run the config.
/// </summary>
+++ /dev/null
-using System.Linq;
-using System.Numerics;
-using Robust.Shared.Map;
-
-namespace Content.Shared.Procedural;
-
-/// <summary>
-/// Contains the loaded data for a dungeon.
-/// </summary>
-[DataDefinition]
-public sealed partial class DungeonData
-{
- [DataField]
- public Dictionary<uint, Vector2> Decals = new();
-
- [DataField]
- public Dictionary<EntityUid, Vector2i> Entities = new();
-
- [DataField]
- public Dictionary<Vector2i, Tile> Tiles = new();
-
- public static DungeonData Empty = new();
-
- public void Merge(DungeonData data)
- {
- foreach (var did in data.Decals)
- {
- Decals[did.Key] = did.Value;
- }
-
- foreach (var ent in data.Entities)
- {
- Entities[ent.Key] = ent.Value;
- }
-
- foreach (var tile in data.Tiles)
- {
- Tiles[tile.Key] = tile.Value;
- }
- }
-}
+++ /dev/null
-using Robust.Shared.Noise;
-
-namespace Content.Shared.Procedural.DungeonGenerators;
-
-/// <summary>
-/// Turns a chunked area into a dungeon for layer purposes. Assumes the position is the BL origin.
-/// </summary>
-public sealed partial class ChunkDunGen : IDunGenLayer
-{
- [DataField]
- public int Size = 16;
-
- /// <summary>
- /// Noise to apply for each tile conditionally.
- /// </summary>
- [DataField]
- public FastNoiseLite? Noise;
-
- /// <summary>
- /// Threshold for noise. Does nothing if <see cref="Noise"/> is null.
- /// </summary>
- [DataField]
- public float Threshold = -1f;
-}
{
[DataField(required: true)]
public ProtoId<DungeonConfigPrototype> Proto;
-
- /// <summary>
- /// Minimum and maximum penetration.
- /// </summary>
- [DataField]
- public Vector2i Penetration = new Vector2i(5, 15);
}
[DataField]
public DungeonInheritance InheritDungeons = DungeonInheritance.None;
- /// <summary>
- /// Should we pass in the current level's reserved tiles to the prototype.
- /// </summary>
- [DataField]
- public ReservedInheritance InheritReserved = ReservedInheritance.All;
-
[DataField(required: true)]
public ProtoId<DungeonConfigPrototype> Proto;
}
/// </summary>
All,
}
-
-public enum ReservedInheritance : byte
-{
- /// <summary>
- /// Don't inherit any reserved tiles.
- /// </summary>
- None,
-
- /// <summary>
- /// Inherit reserved tiles,
- /// </summary>
- All,
-}
-using System.Numerics;
using Content.Shared.Maps;
-using Content.Shared.Procedural.Distance;
-using Robust.Shared.Noise;
using Robust.Shared.Prototypes;
namespace Content.Shared.Procedural.DungeonLayers;
/// <summary>
/// Fills unreserved tiles with the specified entity prototype.
/// </summary>
+/// <remarks>
+/// DungeonData keys are:
+/// - Fill
+/// </remarks>
public sealed partial class FillGridDunGen : IDunGenLayer
{
/// <summary>
[DataField(required: true)]
public EntProtoId Entity;
-
- #region Noise
-
- [DataField]
- public bool Invert;
-
- /// <summary>
- /// Optionally don't spawn entities if the noise value matches.
- /// </summary>
- [DataField]
- public FastNoiseLite? ReservedNoise;
-
- /// <summary>
- /// Noise threshold for <see cref="ReservedNoise"/>. Does nothing without it.
- /// </summary>
- [DataField]
- public float Threshold = -1f;
-
- [DataField]
- public IDunGenDistance? DistanceConfig;
-
- [DataField]
- public Vector2 Size;
-
- #endregion
}
using Content.Shared.EntityTable;
+using Content.Shared.Storage;
using Robust.Shared.Prototypes;
namespace Content.Shared.Procedural.DungeonLayers;
+
/// <summary>
/// Spawns mobs inside of the dungeon randomly.
/// </summary>
+++ /dev/null
-using Robust.Shared.Noise;
-
-namespace Content.Shared.Procedural.DungeonLayers;
-
-/// <summary>
-/// Sets tiles as rooved.
-/// </summary>
-public sealed partial class RoofDunGen : IDunGenLayer
-{
- [DataField]
- public float Threshold = -1f;
-
- [DataField]
- public FastNoiseLite? Noise;
-}
+++ /dev/null
-using Content.Shared.Decals;
-using Content.Shared.Maps;
-using Robust.Shared.Noise;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
-
-namespace Content.Shared.Procedural.DungeonLayers;
-
-public sealed partial class SampleDecalDunGen : IDunGenLayer
-{
- /// <summary>
- /// Reserve any tiles we update.
- /// </summary>
- [DataField]
- public bool ReserveTiles = true;
-
- [DataField(customTypeSerializer:typeof(PrototypeIdListSerializer<ContentTileDefinition>))]
- public List<string> AllowedTiles { get; private set; } = new();
-
- /// <summary>
- /// Divide each tile up by this amount.
- /// </summary>
- [DataField]
- public float Divisions = 1f;
-
- [DataField]
- public FastNoiseLite Noise { get; private set; } = new(0);
-
- [DataField]
- public float Threshold { get; private set; } = 0.8f;
-
- [DataField] public bool Invert { get; private set; } = false;
-
- [DataField(required: true)]
- public List<ProtoId<DecalPrototype>> Decals = new();
-}
+++ /dev/null
-using Content.Shared.Maps;
-using Robust.Shared.Noise;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
-
-namespace Content.Shared.Procedural.DungeonLayers;
-
-/// <summary>
-/// Samples noise to spawn the specified entity
-/// </summary>
-public sealed partial class SampleEntityDunGen : IDunGenLayer
-{
- /// <summary>
- /// Reserve any tiles we update.
- /// </summary>
- [DataField]
- public bool ReserveTiles = true;
-
- [DataField(customTypeSerializer:typeof(PrototypeIdListSerializer<ContentTileDefinition>))]
- public List<string> AllowedTiles { get; private set; } = new();
-
- [DataField] public FastNoiseLite Noise { get; private set; } = new(0);
-
- [DataField]
- public float Threshold { get; private set; } = 0.5f;
-
- [DataField] public bool Invert { get; private set; } = false;
-
- [DataField]
- public List<EntProtoId> Entities = new();
-}
+++ /dev/null
-using Robust.Shared.Prototypes;
-
-namespace Content.Shared.Procedural.Loot;
-
-/// <summary>
-/// Adds the prototype as a biome layer.
-/// </summary>
-public sealed partial class BiomeLoot : IDungeonLoot
-{
- [DataField(required: true)]
- public ProtoId<DungeonConfigPrototype> Proto;
-}
--- /dev/null
+using Content.Shared.Parallax.Biomes.Markers;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
+
+namespace Content.Shared.Procedural.Loot;
+
+/// <summary>
+/// Adds a biome marker layer for dungeon loot.
+/// </summary>
+public sealed partial class BiomeMarkerLoot : IDungeonLoot
+{
+ [DataField("proto", required: true)]
+ public ProtoId<BiomeMarkerLayerPrototype> Prototype = new();
+}
--- /dev/null
+using Content.Shared.Parallax.Biomes;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Shared.Procedural.Loot;
+
+/// <summary>
+/// Adds a biome template layer for dungeon loot.
+/// </summary>
+public sealed partial class BiomeTemplateLoot : IDungeonLoot
+{
+ [DataField("proto", required: true)]
+ public ProtoId<BiomeTemplatePrototype> Prototype = string.Empty;
+}
--- /dev/null
+using Content.Shared.Maps;
+using Content.Shared.Parallax.Biomes;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Procedural.PostGeneration;
+
+/// <summary>
+/// Generates a biome on top of valid tiles, then removes the biome when done.
+/// Only works if no existing biome is present.
+/// </summary>
+public sealed partial class BiomeDunGen : IDunGenLayer
+{
+ [DataField(required: true)]
+ public ProtoId<BiomeTemplatePrototype> BiomeTemplate;
+
+ /// <summary>
+ /// creates a biome only on the specified tiles
+ /// </summary>
+ [DataField]
+ public HashSet<ProtoId<ContentTileDefinition>>? TileMask;
+}
--- /dev/null
+using Content.Shared.Random;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Procedural.PostGeneration;
+
+/// <summary>
+/// Spawns the specified marker layer on top of the dungeon rooms.
+/// </summary>
+public sealed partial class BiomeMarkerLayerDunGen : IDunGenLayer
+{
+ /// <summary>
+ /// How many times to spawn marker layers; can duplicate.
+ /// </summary>
+ [DataField]
+ public int Count = 6;
+
+ [DataField(required: true)]
+ public ProtoId<WeightedRandomPrototype> MarkerTemplate;
+}
using Content.Shared.EntityTable;
using Content.Shared.Maps;
-using Content.Shared.Storage;
using Robust.Shared.Prototypes;
namespace Content.Shared.Procedural.PostGeneration;
using Content.Shared.EntityTable;
using Content.Shared.Maps;
+using Content.Shared.Storage;
using Robust.Shared.Prototypes;
namespace Content.Shared.Procedural.PostGeneration;
using Content.Shared.EntityTable;
using Content.Shared.Maps;
+using Content.Shared.Storage;
using Robust.Shared.Prototypes;
namespace Content.Shared.Procedural.PostGeneration;
using Content.Shared.Maps;
using Robust.Shared.Prototypes;
-namespace Content.Shared.Procedural.DungeonLayers;
+namespace Content.Shared.Procedural.PostGeneration;
/// <summary>
/// Connects dungeons via points that get subdivided.
/// Will divide the distance between the start and end points so that no subdivision is more than these metres away.
/// </summary>
[DataField]
- public int DivisionDistance = 20;
+ public int DivisionDistance = 10;
/// <summary>
/// How much each subdivision can vary from the middle.
/// </summary>
[DataField]
- public float VarianceMax = 0.15f;
+ public float VarianceMax = 0.35f;
}
+using Content.Shared.Parallax.Biomes;
using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Salvage.Expeditions.Modifiers;
[DataField("weather")]
public bool Weather = true;
- [DataField("biome", required: true)]
- public EntProtoId? BiomePrototype;
+ [DataField("biome", required: true, customTypeSerializer:typeof(PrototypeIdSerializer<BiomeTemplatePrototype>))]
+ public string? BiomePrototype;
}
+cmd-biome_clear-desc = Clears a biome entirely
+cmd-biome_clear-help = biome_clear <biomecomponent>
cmd-biome_addlayer-desc = Adds another biome layer
cmd-biome_addlayer-help = biome_addlayer <mapid> <biometemplate> [seed offset]
+cmd-biome_addmarkerlayer-desc = Adds another biome marker layer
+cmd-biome_addmarkerlayer-help = biome_addmarkerlayer <mapid> <biomemarkerlayer>
drawdepth: BelowFloor
layers:
- state: shoreline_water
- - type: BiomeForceUnload
- #- type: ContainerContainer
- # containers:
- # solution@pool: !type:ContainerSlot
- type: SolutionContainerManager
solutions:
pool:
- maxVol: 9999999 #.inf seems to break the whole yaml file, but would definitely be preferable.
+ maxVol: 9999999 #.inf seems to break the whole yaml file, but would definitely be preferable.
reagents:
- ReagentId: Water
Quantity: 9999999
lacunarity: 2
# Generate biome
- - !type:PrototypeDunGen
- proto: Asteroid
- inheritDungeons: All
+ - !type:BiomeDunGen
+ biomeTemplate: Asteroid
- !type:OreDunGen
replacement: AsteroidRock
entity: AsteroidRockGibtonite
lacunarity: 2
# Generate biome
- - !type:PrototypeDunGen
- proto: Asteroid
- inheritDungeons: All
+ - !type:BiomeDunGen
+ biomeTemplate: Asteroid
- !type:OreDunGen
replacement: AsteroidRock
entity: AsteroidRockGibtonite
cellularDistanceFunction: Euclidean
# Generate biome
- - !type:PrototypeDunGen
- proto: Asteroid
- inheritDungeons: All
+ - !type:BiomeDunGen
+ biomeTemplate: Asteroid
- !type:OreDunGen
replacement: AsteroidRock
entity: AsteroidRockGibtonite
lacunarity: 2
# Generate biome
- - !type:PrototypeDunGen
- proto: Asteroid
- inheritDungeons: All
+ - !type:BiomeDunGen
+ biomeTemplate: Asteroid
- !type:OreDunGen
replacement: AsteroidRock
entity: AsteroidRockGibtonite
gain: 0.5
# Generate biome
- - !type:PrototypeDunGen
- proto: SpaceDebris
- inheritDungeons: All
+ - !type:BiomeDunGen
+ biomeTemplate: SpaceDebris
- type: dungeonConfig
id: ChunkDebrisSmall
gain: 0.5
# Generate biome
- - !type:PrototypeDunGen
- proto: SpaceDebris
- inheritDungeons: All
+ - !type:BiomeDunGen
+ biomeTemplate: SpaceDebris
# Asteroid
-- type: dungeonConfig
+- type: biomeTemplate
id: SpaceDebris
layers:
- - !type:SampleEntityDunGen
+ - !type:BiomeEntityLayer
threshold: 0.20
noise:
seed: 0
- WallReinforced
- WallSolid
- WallSolid
- - !type:SampleEntityDunGen
+ - !type:BiomeEntityLayer
threshold: 0.5
noise:
seed: 0
- Grille
- Grille
- GrilleBroken
- - !type:SampleDecalDunGen
+ - !type:BiomeDecalLayer
allowedTiles:
- FloorSteel
threshold: -0.5
- DirtMedium
- DirtMedium
- DirtLight
- - !type:SampleEntityDunGen
+ - !type:BiomeEntityLayer
threshold: 0.45
noise:
seed: 1
- FloorSteel
entities:
- SalvageSpawnerStructuresVarious
- - !type:SampleEntityDunGen
+ - !type:BiomeEntityLayer
allowedTiles:
- FloorSteel
- Plating
- SalvageSpawnerScrapCommon
- SalvageSpawnerScrapCommon
- SalvageSpawnerScrapCommon75
- - !type:SampleEntityDunGen
+ - !type:BiomeEntityLayer
allowedTiles:
- FloorSteel
- Plating
- SalvageSpawnerTreasure
- SalvageSpawnerTreasure
- SalvageSpawnerTreasureValuable
- - !type:SampleEntityDunGen
+ - !type:BiomeEntityLayer
allowedTiles:
- FloorSteel
- Plating
-- type: dungeonConfig
+- type: biomeMarkerLayer
id: Lizards
- layers:
- - !type:ChunkDunGen
- size: 128
- - !type:EntityTableDunGen
- table: !type:EntSelector
- id: MobLizard
- amount: !type:RangeNumberSelector
- range: 3, 5
+ prototype: MobLizard
+ minGroupSize: 3
+ maxGroupSize: 5
-
-- type: dungeonConfig
+- type: biomeMarkerLayer
id: WatchersLavaland
- layers:
- - !type:ChunkDunGen
- size: 128
- - !type:EntityTableDunGen
- table: !type:EntSelector
- id: MobWatcherLavaland
- amount: !type:RangeNumberSelector
- range: 3, 3
+ prototype: MobWatcherLavaland
+ minGroupSize: 3
+ maxGroupSize: 3
-- type: dungeonConfig
+- type: biomeMarkerLayer
id: WatchersIcewing
- layers:
- - !type:ChunkDunGen
- size: 128
- - !type:EntityTableDunGen
- table: !type:EntSelector
- id: MobWatcherIcewing
- amount: !type:RangeNumberSelector
- range: 3, 3
+ prototype: MobWatcherIcewing
+ minGroupSize: 3
+ maxGroupSize: 3
-- type: dungeonConfig
+- type: biomeMarkerLayer
id: WatchersMagmawing
- layers:
- - !type:ChunkDunGen
- size: 128
- - !type:EntityTableDunGen
- table: !type:EntSelector
- id: MobWatcherMagmawing
- amount: !type:RangeNumberSelector
- range: 3, 3
+ prototype: MobWatcherMagmawing
+ minGroupSize: 3
+ maxGroupSize: 3
-- type: dungeonConfig
+- type: biomeMarkerLayer
id: Cows
- layers:
- - !type:ChunkDunGen
- size: 128
- - !type:EntityTableDunGen
- table: !type:EntSelector
- id: MobCow
- amount: !type:RangeNumberSelector
- range: 1, 2
+ prototype: MobCow
+ minGroupSize: 1
+ maxGroupSize: 2
-- type: dungeonConfig
+- type: biomeMarkerLayer
id: Chickens
- layers:
- - !type:ChunkDunGen
- size: 128
- - !type:EntityTableDunGen
- table: !type:EntSelector
- id: MobChicken
- amount: !type:RangeNumberSelector
- range: 1, 2
+ prototype: MobChicken
+ minGroupSize: 1
+ maxGroupSize: 2
-- type: dungeonConfig
+- type: biomeMarkerLayer
id: Pigs
- layers:
- - !type:ChunkDunGen
- size: 128
- - !type:EntityTableDunGen
- table: !type:EntSelector
- id: MobPig
- amount: !type:RangeNumberSelector
- range: 1, 2
+ prototype: MobPig
+ minGroupSize: 1
+ maxGroupSize: 2
-- type: dungeonConfig
+- type: biomeMarkerLayer
id: Foxes
- layers:
- - !type:ChunkDunGen
- size: 128
- - !type:EntityTableDunGen
- table: !type:EntSelector
- id: MobFox
- amount: !type:RangeNumberSelector
- range: 1, 1
+ prototype: MobFox
+ minGroupSize: 1
+ maxGroupSize: 1
-- type: dungeonConfig
+- type: biomeMarkerLayer
id: Goats
- layers:
- - !type:ChunkDunGen
- size: 128
- - !type:EntityTableDunGen
- table: !type:EntSelector
- id: MobGoat
- amount: !type:RangeNumberSelector
- range: 1, 1
+ prototype: MobGoat
+ minGroupSize: 1
+ maxGroupSize: 1
+
+# TODO: Needs to be more robust
+- type: biomeMarkerLayer
+ id: Xenos
+ prototype: MobXeno
-- type: dungeonConfig
+- type: biomeMarkerLayer
id: Carps
- layers:
- - !type:ChunkDunGen
- size: 128
- - !type:EntityTableDunGen
- table: !type:EntSelector
- id: MobCarp
- amount: !type:RangeNumberSelector
- range: 1, 2
+ prototype: MobCarpDungeon
-- type: dungeonConfig
- id: Xenos
- layers:
- - !type:ChunkDunGen
- size: 128
- - !type:EntityTableDunGen
- table: !type:EntSelector
- id: MobXeno
- amount: !type:RangeNumberSelector
- range: 1, 2
+#- type: biomeMarkerLayer
+# id: Experiment
+# proto: DungeonMarkerExperiment
# Low value
-- type: dungeonConfig
+- type: biomeMarkerLayer
id: OreIron
- layers:
- - !type:ChunkDunGen
- size: 128
- - !type:OreDunGen
- replacement: WallRock
- entity: WallRockTin
- count: 30
- minGroupSize: 10
- maxGroupSize: 15
+ entityMask:
+ AsteroidRock: AsteroidRockTin
+ WallRock: WallRockTin
+ WallRockBasalt: WallRockBasaltTin
+ WallRockChromite: WallRockChromiteTin
+ WallRockSand: WallRockSandTin
+ WallRockSnow: WallRockSnowTin
+ maxCount: 30
+ minGroupSize: 10
+ maxGroupSize: 15
+ radius: 4
-- type: dungeonConfig
+- type: biomeMarkerLayer
id: OreQuartz
- layers:
- - !type:ChunkDunGen
- size: 128
- - !type:OreDunGen
- replacement: WallRock
- entity: WallRockQuartz
- count: 30
- minGroupSize: 10
- maxGroupSize: 15
+ entityMask:
+ AsteroidRock: AsteroidRockQuartz
+ WallRock: WallRockQuartz
+ WallRockBasalt: WallRockBasaltQuartz
+ WallRockChromite: WallRockChromiteQuartz
+ WallRockSnow: WallRockSnowQuartz
+ maxCount: 30
+ minGroupSize: 10
+ maxGroupSize: 15
+ radius: 4
-- type: dungeonConfig
+- type: biomeMarkerLayer
id: OreCoal
- layers:
- - !type:ChunkDunGen
- size: 128
- - !type:OreDunGen
- replacement: WallRock
- entity: WallRockCoal
- count: 30
- minGroupSize: 8
- maxGroupSize: 12
+ entityMask:
+ AsteroidRock: AsteroidRockCoal
+ WallRock: WallRockCoal
+ WallRockBasalt: WallRockBasaltCoal
+ WallRockChromite: WallRockChromiteCoal
+ WallRockSand: WallRockSandCoal
+ WallRockSnow: WallRockSnowCoal
+ maxCount: 30
+ minGroupSize: 8
+ maxGroupSize: 12
+ radius: 4
-- type: dungeonConfig
+- type: biomeMarkerLayer
id: OreSalt
- layers:
- - !type:ChunkDunGen
- size: 128
- - !type:OreDunGen
- replacement: WallRock
- entity: WallRockSalt
- count: 30
- minGroupSize: 8
- maxGroupSize: 12
+ entityMask:
+ AsteroidRock: AsteroidRockSalt
+ WallRock: WallRockSalt
+ WallRockBasalt: WallRockBasaltSalt
+ WallRockChromite: WallRockChromiteSalt
+ WallRockSand: WallRockSandSalt
+ WallRockSnow: WallRockSnowSalt
+ maxCount: 30
+ minGroupSize: 8
+ maxGroupSize: 12
+ radius: 4
# Medium value
# Gold
-- type: dungeonConfig
+- type: biomeMarkerLayer
id: OreGold
- layers:
- - !type:ChunkDunGen
- size: 128
- - !type:OreDunGen
- replacement: WallRock
- entity: WallRockGold
- count: 20
- minGroupSize: 5
- maxGroupSize: 10
+ entityMask:
+ AsteroidRock: AsteroidRockGold
+ WallRock: WallRockGold
+ WallRockBasalt: WallRockBasaltGold
+ WallRockChromite: WallRockChromiteGold
+ WallRockSand: WallRockSandGold
+ WallRockSnow: WallRockSnowGold
+ maxCount: 20
+ minGroupSize: 5
+ maxGroupSize: 10
+ radius: 4
# Silver
-- type: dungeonConfig
+- type: biomeMarkerLayer
id: OreSilver
- layers:
- - !type:ChunkDunGen
- size: 128
- - !type:OreDunGen
- replacement: WallRock
- entity: WallRockSilver
- count: 20
- minGroupSize: 5
- maxGroupSize: 10
+ entityMask:
+ AsteroidRock: AsteroidRockSilver
+ WallRock: WallRockSilver
+ WallRockBasalt: WallRockBasaltSilver
+ WallRockChromite: WallRockChromiteSilver
+ WallRockSand: WallRockSandSilver
+ WallRockSnow: WallRockSnowSilver
+ maxCount: 20
+ minGroupSize: 5
+ maxGroupSize: 10
+ radius: 4
# High value
# Plasma
-- type: dungeonConfig
+- type: biomeMarkerLayer
id: OrePlasma
- layers:
- - !type:ChunkDunGen
- size: 128
- - !type:OreDunGen
- replacement: WallRock
- entity: WallRockPlasma
- count: 12
- minGroupSize: 4
- maxGroupSize: 8
+ entityMask:
+ AsteroidRock: AsteroidRockPlasma
+ WallRock: WallRockPlasma
+ WallRockBasalt: WallRockBasaltPlasma
+ WallRockChromite: WallRockChromitePlasma
+ WallRockSand: WallRockSandPlasma
+ WallRockSnow: WallRockSnowPlasma
+ maxCount: 12
+ minGroupSize: 4
+ maxGroupSize: 8
+ radius: 4
# Uranium
-- type: dungeonConfig
+- type: biomeMarkerLayer
id: OreUranium
- layers:
- - !type:ChunkDunGen
- size: 128
- - !type:OreDunGen
- replacement: WallRock
- entity: WallRockUranium
- count: 15
- minGroupSize: 4
- maxGroupSize: 8
+ entityMask:
+ AsteroidRock: AsteroidRockUranium
+ WallRock: WallRockUranium
+ WallRockBasalt: WallRockBasaltUranium
+ WallRockChromite: WallRockChromiteUranium
+ WallRockSand: WallRockSandUranium
+ WallRockSnow: WallRockSnowUranium
+ maxCount: 15
+ minGroupSize: 4
+ maxGroupSize: 8
+ radius: 4
-- type: dungeonConfig
+- type: biomeMarkerLayer
id: OreDiamond
- layers:
- - !type:ChunkDunGen
- size: 128
- - !type:OreDunGen
- replacement: WallRock
- entity: WallRockDiamond
- count: 6
- minGroupSize: 1
- maxGroupSize: 2
+ entityMask:
+ AsteroidRock: AsteroidRockDiamond
+ WallRock: WallRockDiamond
+ WallRockBasalt: WallRockBasaltDiamond
+ WallRockChromite: WallRockChromiteDiamond
+ WallRockSand: WallRockSandDiamond
+ WallRockSnow: WallRockSnowDiamond
+ maxCount: 6
+ minGroupSize: 1
+ maxGroupSize: 2
+ radius: 4
# Artifact Fragment
-- type: dungeonConfig
+- type: biomeMarkerLayer
id: OreArtifactFragment
- layers:
- - !type:ChunkDunGen
- size: 128
- - !type:OreDunGen
- replacement: WallRock
- entity: WallRockArtifactFragment
- count: 6
- minGroupSize: 1
- maxGroupSize: 2
+ entityMask:
+ AsteroidRock: AsteroidRockArtifactFragment
+ WallRock: WallRockArtifactFragment
+ WallRockBasalt: WallRockBasaltArtifactFragment
+ WallRockChromite: WallRockChromiteArtifactFragment
+ WallRockSand: WallRockSandArtifactFragment
+ WallRockSnow: WallRockSnowArtifactFragment
+ maxCount: 6
+ minGroupSize: 1
+ maxGroupSize: 2
+ radius: 4
# Low value
-- type: dungeonConfig
+- type: biomeMarkerLayer
id: OreIronLow
- layers:
- - !type:ChunkDunGen
- size: 128
- - !type:OreDunGen
- replacement: WallRock
- entity: WallRockTin
- count: 20
- minGroupSize: 4
- maxGroupSize: 8
+ entityMask:
+ AsteroidRock: AsteroidRockTin
+ WallRock: WallRockTin
+ WallRockBasalt: WallRockBasaltTin
+ WallRockChromite: WallRockChromiteTin
+ WallRockSand: WallRockSandTin
+ WallRockSnow: WallRockSnowTin
+ maxCount: 20
+ minGroupSize: 4
+ maxGroupSize: 8
+ radius: 4
-- type: dungeonConfig
+- type: biomeMarkerLayer
id: OreQuartzLow
- layers:
- - !type:ChunkDunGen
- size: 128
- - !type:OreDunGen
- replacement: WallRock
- entity: WallRockQuartz
- count: 20
- minGroupSize: 4
- maxGroupSize: 8
+ entityMask:
+ AsteroidRock: AsteroidRockQuartz
+ WallRock: WallRockQuartz
+ WallRockBasalt: WallRockBasaltQuartz
+ WallRockChromite: WallRockChromiteQuartz
+ WallRockSnow: WallRockSnowQuartz
+ maxCount: 20
+ minGroupSize: 4
+ maxGroupSize: 8
+ radius: 4
-- type: dungeonConfig
+- type: biomeMarkerLayer
id: OreCoalLow
- layers:
- - !type:ChunkDunGen
- size: 128
- - !type:OreDunGen
- replacement: WallRock
- entity: WallRockCoal
- count: 20
- minGroupSize: 4
- maxGroupSize: 6
+ entityMask:
+ AsteroidRock: AsteroidRockCoal
+ WallRock: WallRockCoal
+ WallRockBasalt: WallRockBasaltCoal
+ WallRockChromite: WallRockChromiteCoal
+ WallRockSand: WallRockSandCoal
+ WallRockSnow: WallRockSnowCoal
+ maxCount: 20
+ minGroupSize: 4
+ maxGroupSize: 6
+ radius: 4
-- type: dungeonConfig
+- type: biomeMarkerLayer
id: OreSaltLow
- layers:
- - !type:ChunkDunGen
- size: 128
- - !type:OreDunGen
- replacement: WallRock
- entity: WallRockSalt
- count: 15
- minGroupSize: 4
- maxGroupSize: 6
+ entityMask:
+ AsteroidRock: AsteroidRockSalt
+ WallRock: WallRockSalt
+ WallRockBasalt: WallRockBasaltSalt
+ WallRockChromite: WallRockChromiteSalt
+ WallRockSand: WallRockSandSalt
+ WallRockSnow: WallRockSnowSalt
+ maxCount: 15
+ minGroupSize: 4
+ maxGroupSize: 6
+ radius: 4
# Medium value
# Gold
-- type: dungeonConfig
+- type: biomeMarkerLayer
id: OreGoldLow
- layers:
- - !type:ChunkDunGen
- size: 128
- - !type:OreDunGen
- replacement: WallRock
- entity: WallRockGold
- count: 12
- minGroupSize: 2
- maxGroupSize: 5
+ entityMask:
+ AsteroidRock: AsteroidRockGold
+ WallRock: WallRockGold
+ WallRockBasalt: WallRockBasaltGold
+ WallRockChromite: WallRockChromiteGold
+ WallRockSand: WallRockSandGold
+ WallRockSnow: WallRockSnowGold
+ maxCount: 10
+ minGroupSize: 2
+ maxGroupSize: 5
+ radius: 4
# Silver
-- type: dungeonConfig
+- type: biomeMarkerLayer
id: OreSilverLow
- layers:
- - !type:ChunkDunGen
- size: 128
- - !type:OreDunGen
- replacement: WallRock
- entity: WallRockSilver
- count: 12
- minGroupSize: 2
- maxGroupSize: 5
+ entityMask:
+ AsteroidRock: AsteroidRockSilver
+ WallRock: WallRockSilver
+ WallRockBasalt: WallRockBasaltSilver
+ WallRockChromite: WallRockChromiteSilver
+ WallRockSand: WallRockSandSilver
+ WallRockSnow: WallRockSnowSilver
+ maxCount: 10
+ minGroupSize: 2
+ maxGroupSize: 5
+ radius: 4
# High value
# Plasma
-- type: dungeonConfig
+- type: biomeMarkerLayer
id: OrePlasmaLow
- layers:
- - !type:ChunkDunGen
- size: 128
- - !type:OreDunGen
- replacement: WallRock
- entity: WallRockPlasma
- count: 6
- minGroupSize: 2
- maxGroupSize: 4
+ entityMask:
+ AsteroidRock: AsteroidRockPlasma
+ WallRock: WallRockPlasma
+ WallRockBasalt: WallRockBasaltPlasma
+ WallRockChromite: WallRockChromitePlasma
+ WallRockSand: WallRockSandPlasma
+ WallRockSnow: WallRockSnowPlasma
+ maxCount: 6
+ minGroupSize: 2
+ maxGroupSize: 4
+ radius: 4
# Uranium
-- type: dungeonConfig
+- type: biomeMarkerLayer
id: OreUraniumLow
- layers:
- - !type:ChunkDunGen
- size: 128
- - !type:OreDunGen
- replacement: WallRock
- entity: WallRockUranium
- count: 7
- minGroupSize: 2
- maxGroupSize: 4
+ entityMask:
+ AsteroidRock: AsteroidRockUranium
+ WallRock: WallRockUranium
+ WallRockBasalt: WallRockBasaltUranium
+ WallRockChromite: WallRockChromiteUranium
+ WallRockSand: WallRockSandUranium
+ WallRockSnow: WallRockSnowUranium
+ maxCount: 7
+ minGroupSize: 2
+ maxGroupSize: 4
+ radius: 4
-- type: dungeonConfig
+- type: biomeMarkerLayer
id: OreDiamondLow
- layers:
- - !type:ChunkDunGen
- size: 128
- - !type:OreDunGen
- replacement: WallRock
- entity: WallRockDiamond
- count: 3
- minGroupSize: 1
- maxGroupSize: 2
+ entityMask:
+ AsteroidRock: AsteroidRockDiamond
+ WallRock: WallRockDiamond
+ WallRockBasalt: WallRockBasaltDiamond
+ WallRockChromite: WallRockChromiteDiamond
+ WallRockSand: WallRockSandDiamond
+ WallRockSnow: WallRockSnowDiamond
+ maxCount: 3
+ minGroupSize: 1
+ maxGroupSize: 2
+ radius: 4
# Artifact Fragment
-- type: dungeonConfig
+- type: biomeMarkerLayer
id: OreArtifactFragmentLow
- layers:
- - !type:ChunkDunGen
- size: 128
- - !type:OreDunGen
- replacement: WallRock
- entity: WallRockArtifactFragment
- count: 3
- minGroupSize: 1
- maxGroupSize: 2
+ entityMask:
+ AsteroidRock: AsteroidRockArtifactFragment
+ WallRock: WallRockArtifactFragment
+ WallRockBasalt: WallRockBasaltArtifactFragment
+ WallRockChromite: WallRockChromiteArtifactFragment
+ WallRockSand: WallRockSandArtifactFragment
+ WallRockSnow: WallRockSnowArtifactFragment
+ maxCount: 3
+ minGroupSize: 1
+ maxGroupSize: 2
+ radius: 4
-# Desert
-- type: entity
- id: BiomeLowDesert
- categories: [ HideSpawnMenu ]
- components:
- - type: Biome
- layers:
- terrain:
- dungeon: LowDesertTerrain
-
-- type: dungeonConfig
- id: LowDesertTerrain
- layers:
- - !type:ChunkDunGen
- - !type:PrototypeDunGen
- proto: LowDesertTiles
- inheritDungeons: All
- - !type:PrototypeDunGen
- proto: LowDesertEntities
- inheritDungeons: All
-
-- type: dungeonConfig
- id: LowDesertTiles
- returnReserved: false
+# Contains several biomes
+- type: biomeTemplate
+ id: Continental
layers:
- - !type:SampleTileDunGen
- threshold: -1
- tile: FloorAsteroidSand
-
-- type: dungeonConfig
- id: LowDesertEntities
- reserveTiles: true
- layers:
- - !type:SampleEntityDunGen
- threshold: 0.95
- noise:
- seed: 0
- frequency: 2
- noiseType: OpenSimplex2
- allowedTiles:
- - FloorAsteroidSand
- entities:
- - FloraRockSolid
- # Large rock areas
- - !type:SampleEntityDunGen
- threshold: -0.20
- noise:
- seed: 0
- frequency: 0.04
- noiseType: Cellular
- fractalType: FBm
- octaves: 5
- lacunarity: 2
- cellularDistanceFunction: Euclidean
- cellularReturnType: Distance2
- allowedTiles:
- - FloorAsteroidSand
- entities:
- - WallRockSand
-
-
-- type: entity
- id: BiomeGrasslands
- categories: [ HideSpawnMenu ]
- components:
- - type: Biome
- layers:
- terrain:
- dungeon: GrasslandsTerrain
-
-- type: dungeonConfig
- id: GrasslandsTerrain
- layers:
- - !type:ChunkDunGen
- - !type:PrototypeDunGen
- proto: GrasslandsTiles
- inheritDungeons: All
- - !type:PrototypeDunGen
- proto: GrasslandsEntities
- inheritDungeons: All
- - !type:PrototypeDunGen
- proto: GrasslandsDecals
- inheritDungeons: All
-
-- type: dungeonConfig
- id: GrasslandsTiles
- returnReserved: false
- layers:
- # Water sand
- - !type:SampleTileDunGen
- tile: FloorPlanetDirt
- threshold: 0.95
- noise:
- seed: 3
- noiseType: OpenSimplex2
- frequency: 0.003
- lacunarity: 1.50
- fractalType: Ridged
- octaves: 1
- # Rock formation sand
- - !type:SampleTileDunGen
- tile: FloorPlanetDirt
- threshold: -0.30
- noise:
- seed: 0
- noiseType: Cellular
- frequency: 0.05
- lacunarity: 2
- fractalType: FBm
- octaves: 5
- cellularDistanceFunction: Euclidean
- cellularReturnType: Distance2
- - !type:SampleTileDunGen
- threshold: -0.80
- tile: FloorPlanetGrass
- noise:
- seed: 0
- noiseType: OpenSimplex2
- lacunarity: 1.50
- frequency: 0.02
- fractalType: None
- octaves: 1
- # Fill remainder with dirt.
- - !type:SampleTileDunGen
- threshold: -1.0
- tile: FloorPlanetDirt
+ - !type:BiomeMetaLayer
+ template: Lava
+ - !type:BiomeMetaLayer
+ template: Caves
+ threshold: -0.5
+ noise:
+ frequency: 0.001
+ noiseType: OpenSimplex2
+ fractalType: FBm
+ octaves: 2
+ lacunarity: 2
+ - !type:BiomeMetaLayer
+ template: Grasslands
+ threshold: 0
+ noise:
+ frequency: 0.001
+ noiseType: OpenSimplex2
+ fractalType: FBm
+ octaves: 2
+ lacunarity: 2
+ - !type:BiomeMetaLayer
+ template: Snow
+ threshold: 0.5
+ noise:
+ frequency: 0.001
+ noiseType: OpenSimplex2
+ fractalType: FBm
+ octaves: 2
+ lacunarity: 2
-- type: dungeonConfig
- id: GrasslandsEntities
- reserveTiles: true
+# Desert
+# TODO: Water in desert
+- type: biomeTemplate
+ id: LowDesert
layers:
- # Water
- - !type:SampleEntityDunGen
- allowedTiles:
- - FloorPlanetGrass
- - FloorPlanetDirt
- threshold: 0.95
- noise:
- seed: 3
- noiseType: OpenSimplex2
- frequency: 0.003
- lacunarity: 1.50
- fractalType: Ridged
- octaves: 1
- entities:
- - FloorWaterEntity
- # Rock formations
- - !type:SampleEntityDunGen
- allowedTiles:
- - FloorPlanetGrass
- - FloorPlanetDirt
- threshold: -0.30
- noise:
- seed: 0
- noiseType: Cellular
- frequency: 0.05
- lacunarity: 2
- fractalType: FBm
- octaves: 5
- cellularDistanceFunction: Euclidean
- cellularReturnType: Distance2
- entities:
- - WallRock
- - !type:SampleEntityDunGen
- threshold: 0.5
- noise:
- seed: 0
- noiseType: OpenSimplex2
- fractalType: FBm
- frequency: 2
- allowedTiles:
- - FloorPlanetGrass
- entities:
- - FloraTree
- - FloraTreeLarge
+ - !type:BiomeEntityLayer
+ threshold: 0.95
+ noise:
+ seed: 0
+ frequency: 2
+ noiseType: OpenSimplex2
+ allowedTiles:
+ - FloorAsteroidSand
+ entities:
+ - FloraRockSolid
+ # Large rock areas
+ - !type:BiomeEntityLayer
+ threshold: -0.20
+ noise:
+ seed: 0
+ frequency: 0.04
+ noiseType: Cellular
+ fractalType: FBm
+ octaves: 5
+ lacunarity: 2
+ cellularDistanceFunction: Euclidean
+ cellularReturnType: Distance2
+ allowedTiles:
+ - FloorAsteroidSand
+ entities:
+ - WallRockSand
+ - !type:BiomeDummyLayer
+ id: Loot
+ # Fill layer
+ - !type:BiomeTileLayer
+ threshold: -1
+ tile: FloorAsteroidSand
-- type: dungeonConfig
- id: GrasslandsDecals
- returnReserved: false
+# Grass
+- type: biomeTemplate
+ id: Grasslands
layers:
- # Dense vegetation
- - !type:SampleDecalDunGen
- allowedTiles:
- - FloorPlanetGrass
- divisions: 1
- threshold: -0.35
- noise:
- seed: 0
- noiseType: Cellular
- frequency: 0.2
- fractalType: FBm
- octaves: 5
- lacunarity: 2
- cellularDistanceFunction: Euclidean
- cellularReturnType: Distance2
- decals:
- - BushAOne
- - BushATwo
- - BushAThree
- - BushCOne
- - BushCTwo
- - BushCThree
- - !type:SampleDecalDunGen
- allowedTiles:
- - FloorPlanetGrass
- noise:
- seed: 0
- noiseType: OpenSimplex2
- frequency: 1
- divisions: 1
- threshold: 0.8
- decals:
- - FlowersBROne
- - FlowersBRTwo
- - FlowersBRThree
- # Sparse vegetation
- - !type:SampleDecalDunGen
- allowedTiles:
- - FloorPlanetGrass
- divisions: 2
- threshold: -0.50
- noise:
- seed: 0
- noiseType: Cellular
- frequency: 1
- decals:
- - BushDOne
- - BushDTwo
- - BushDThree
+ # Sparse vegetation
+ - !type:BiomeDecalLayer
+ allowedTiles:
+ - FloorPlanetGrass
+ divisions: 2
+ threshold: -0.50
+ noise:
+ seed: 0
+ noiseType: Cellular
+ frequency: 1
+ decals:
+ - BushDOne
+ - BushDTwo
+ - BushDThree
+ - !type:BiomeDecalLayer
+ allowedTiles:
+ - FloorPlanetGrass
+ noise:
+ seed: 0
+ noiseType: OpenSimplex2
+ frequency: 1
+ divisions: 1
+ threshold: 0.8
+ decals:
+ - FlowersBROne
+ - FlowersBRTwo
+ - FlowersBRThree
+ # Dense vegetation
+ - !type:BiomeDecalLayer
+ allowedTiles:
+ - FloorPlanetGrass
+ divisions: 1
+ threshold: -0.35
+ noise:
+ seed: 0
+ noiseType: Cellular
+ frequency: 0.2
+ fractalType: FBm
+ octaves: 5
+ lacunarity: 2
+ cellularDistanceFunction: Euclidean
+ cellularReturnType: Distance2
+ decals:
+ - BushAOne
+ - BushATwo
+ - BushAThree
+ - BushCOne
+ - BushCTwo
+ - BushCThree
+ - !type:BiomeEntityLayer
+ threshold: 0.5
+ noise:
+ seed: 0
+ noiseType: OpenSimplex2
+ fractalType: FBm
+ frequency: 2
+ allowedTiles:
+ - FloorPlanetGrass
+ entities:
+ - FloraTree
+ - FloraTreeLarge
+ # Rock formations
+ - !type:BiomeEntityLayer
+ allowedTiles:
+ - FloorPlanetGrass
+ - FloorPlanetDirt
+ threshold: -0.30
+ noise:
+ seed: 0
+ noiseType: Cellular
+ frequency: 0.05
+ lacunarity: 2
+ fractalType: FBm
+ octaves: 5
+ cellularDistanceFunction: Euclidean
+ cellularReturnType: Distance2
+ entities:
+ - WallRock
+ - !type:BiomeDummyLayer
+ id: Loot
+ # Water
+ - !type:BiomeEntityLayer
+ allowedTiles:
+ - FloorPlanetGrass
+ - FloorPlanetDirt
+ threshold: 0.95
+ noise:
+ seed: 3
+ noiseType: OpenSimplex2
+ frequency: 0.003
+ lacunarity: 1.50
+ fractalType: Ridged
+ octaves: 1
+ entities:
+ - FloorWaterEntity
+ # Fill remainder with dirt.
+ - !type:BiomeTileLayer
+ threshold: -1.0
+ tile: FloorPlanetDirt
+ - !type:BiomeTileLayer
+ threshold: -0.90
+ tile: FloorPlanetGrass
+ noise:
+ seed: 0
+ frequency: 0.02
+ fractalType: None
+ # Water sand
+ - !type:BiomeTileLayer
+ tile: FloorPlanetDirt
+ threshold: 0.95
+ noise:
+ seed: 3
+ noiseType: OpenSimplex2
+ frequency: 0.003
+ lacunarity: 1.50
+ fractalType: Ridged
+ octaves: 1
+ # Rock formation sand
+ - !type:BiomeTileLayer
+ tile: FloorPlanetDirt
+ threshold: -0.30
+ noise:
+ seed: 0
+ noiseType: Cellular
+ frequency: 0.05
+ lacunarity: 2
+ fractalType: FBm
+ octaves: 5
+ cellularDistanceFunction: Euclidean
+ cellularReturnType: Distance2
# Lava
-- type: entity
- id: BiomeLava
- categories: [ HideSpawnMenu ]
- components:
- - type: Biome
- layers:
- terrain:
- dungeon: LavaTerrain
-
-- type: dungeonConfig
- id: LavaTerrain
- layers:
- - !type:ChunkDunGen
- - !type:PrototypeDunGen
- proto: LavaTiles
- inheritDungeons: All
- - !type:PrototypeDunGen
- proto: LavaEntities
- inheritDungeons: All
- - !type:PrototypeDunGen
- proto: LavaDecals
- inheritDungeons: All
- - !type:PrototypeDunGen
- proto: LavaBasalt
- inheritDungeons: All
-
-- type: dungeonConfig
- id: LavaTiles
- returnReserved: false
- layers:
- # Fill basalt
- - !type:SampleTileDunGen
- threshold: -1
- tile: FloorBasalt
-
-- type: dungeonConfig
- id: LavaEntities
- reserveTiles: true
+- type: biomeTemplate
+ id: Lava
layers:
- - !type:SampleEntityDunGen
- threshold: 0.2
- noise:
- seed: 0
- frequency: 0.02
- fractalType: FBm
- octaves: 5
- lacunarity: 2
- gain: 0.4
- allowedTiles:
- - FloorBasalt
- entities:
- - FloorLavaEntity
- # Rock formations
- - !type:SampleEntityDunGen
- allowedTiles:
- - FloorBasalt
- threshold: -0.30
- noise:
- seed: 0
- noiseType: Cellular
- frequency: 0.05
- lacunarity: 2
- fractalType: FBm
- octaves: 5
- cellularDistanceFunction: Euclidean
- cellularReturnType: Distance2
- entities:
- - WallRockBasalt
- - !type:SampleEntityDunGen
- threshold: 0.95
- noise:
- seed: 0
- noiseType: OpenSimplex2
- frequency: 1
- allowedTiles:
- - FloorBasalt
- entities:
- - FloraRockSolid
-
-- type: dungeonConfig
- id: LavaDecals
- returnReserved: false
- layers:
- - !type:SampleDecalDunGen
- allowedTiles:
- - FloorBasalt
- threshold: 0.9
- divisions: 1
- noise:
- seed: 1
- frequency: 1
- decals:
- - Basalt1
- - Basalt2
- - Basalt3
- - Basalt4
- - Basalt5
- - Basalt6
- - Basalt7
- - Basalt8
- - Basalt9
-
-- type: dungeonConfig
- id: LavaBasalt
- returnReserved: false
- layers:
- - !type:SampleEntityDunGen
- threshold: 0.9
- noise:
- frequency: 1
- seed: 2
- allowedTiles:
- - FloorBasalt
- entities:
- - BasaltOne
- - BasaltTwo
- - BasaltThree
- - BasaltFour
- - BasaltFive
+ - !type:BiomeEntityLayer
+ threshold: 0.9
+ noise:
+ frequency: 1
+ seed: 2
+ allowedTiles:
+ - FloorBasalt
+ entities:
+ - BasaltOne
+ - BasaltTwo
+ - BasaltThree
+ - BasaltFour
+ - BasaltFive
+ - !type:BiomeDecalLayer
+ allowedTiles:
+ - FloorBasalt
+ threshold: 0.9
+ divisions: 1
+ noise:
+ seed: 1
+ frequency: 1
+ decals:
+ - Basalt1
+ - Basalt2
+ - Basalt3
+ - Basalt4
+ - Basalt5
+ - Basalt6
+ - Basalt7
+ - Basalt8
+ - Basalt9
+ - !type:BiomeEntityLayer
+ threshold: 0.95
+ noise:
+ seed: 0
+ noiseType: OpenSimplex2
+ frequency: 1
+ allowedTiles:
+ - FloorBasalt
+ entities:
+ - FloraRockSolid
+ - !type:BiomeEntityLayer
+ threshold: 0.2
+ noise:
+ seed: 0
+ frequency: 0.02
+ fractalType: FBm
+ octaves: 5
+ lacunarity: 2
+ gain: 0.4
+ allowedTiles:
+ - FloorBasalt
+ entities:
+ - FloorLavaEntity
+ # Rock formations
+ - !type:BiomeEntityLayer
+ allowedTiles:
+ - FloorBasalt
+ threshold: -0.30
+ noise:
+ seed: 0
+ noiseType: Cellular
+ frequency: 0.05
+ lacunarity: 2
+ fractalType: FBm
+ octaves: 5
+ cellularDistanceFunction: Euclidean
+ cellularReturnType: Distance2
+ entities:
+ - WallRockBasalt
+ - !type:BiomeDummyLayer
+ id: Loot
+ # Fill basalt
+ - !type:BiomeTileLayer
+ threshold: -1
+ tile: FloorBasalt
# Snow
-- type: entity
- id: BiomeSnow
- categories: [ HideSpawnMenu ]
- components:
- - type: Biome
- layers:
- terrain:
- dungeon: SnowTerrain
-
-- type: dungeonConfig
- id: SnowTerrain
- layers:
- - !type:ChunkDunGen
- - !type:PrototypeDunGen
- proto: SnowTiles
- inheritDungeons: All
- - !type:PrototypeDunGen
- proto: SnowEntities
- inheritDungeons: All
- - !type:PrototypeDunGen
- proto: SnowDecals
- inheritDungeons: All
-
-- type: dungeonConfig
- id: SnowTiles
- returnReserved: false
+- type: biomeTemplate
+ id: Snow # Similar to Grasslands... but snow
layers:
- - !type:SampleTileDunGen
- threshold: -0.7
- tile: FloorSnow
- noise:
- seed: 0
- frequency: 0.02
- fractalType: None
- - !type:SampleTileDunGen
- tile: FloorIce
- threshold: -0.9
- noise:
- seed: 0
- noiseType: Cellular
- frequency: 0.03
- lacunarity: 2
- fractalType: FBm
- octaves: 5
- cellularDistanceFunction: Euclidean
- cellularReturnType: Distance2
-
-
-- type: dungeonConfig
- id: SnowEntities
- reserveTiles: true
- layers:
- # Liquid plasma rivers. Ice moon baby
- - !type:SampleEntityDunGen
- allowedTiles:
- - FloorSnow
- - FloorIce
- threshold: 0.95
- noise:
- seed: 3
- noiseType: OpenSimplex2
- frequency: 0.003
- lacunarity: 1.50
- fractalType: Ridged
- octaves: 1
- entities:
- - FloorLiquidPlasmaEntity
- # Rock formations
- - !type:SampleEntityDunGen
- allowedTiles:
- - FloorSnow
- threshold: -0.30
- noise:
- seed: 0
- noiseType: Cellular
- frequency: 0.05
- lacunarity: 2
- fractalType: FBm
- octaves: 5
- cellularDistanceFunction: Euclidean
- cellularReturnType: Distance2
- entities:
- - WallRockSnow
- - !type:SampleEntityDunGen
- threshold: 0.5
- noise:
- seed: 0
- noiseType: OpenSimplex2
- fractalType: FBm
- frequency: 2
- allowedTiles:
- - FloorSnow
- entities:
- - FloraTreeSnow
-
-- type: dungeonConfig
- id: SnowDecals
- returnReserved: false
- layers:
- # Sparse vegetation
- - !type:SampleDecalDunGen
- allowedTiles:
- - FloorSnow
- divisions: 2
- threshold: -0.50
- noise:
- seed: 0
- noiseType: Cellular
- frequency: 1
- decals:
- - grasssnowa1
- - grasssnowa2
- - grasssnowa3
- - grasssnowb1
- - grasssnowb2
- - grasssnowb3
- - grasssnowc1
- - grasssnowc2
- - grasssnowc3
- # Dense, bland grass
- - !type:SampleDecalDunGen
- allowedTiles:
- - FloorSnow
- divisions: 1
- threshold: -0.35
- noise:
- seed: 0
- noiseType: Cellular
- frequency: 0.2
- fractalType: FBm
- octaves: 5
- lacunarity: 2
- cellularDistanceFunction: Euclidean
- cellularReturnType: Distance2
- decals:
- - grasssnow
- - grasssnow01
- - grasssnow02
- - grasssnow03
- - grasssnow04
- - grasssnow05
- - grasssnow06
- - grasssnow07
- - grasssnow08
- - grasssnow09
- - grasssnow10
- - grasssnow11
- - grasssnow12
- - grasssnow13
- # Little bit of coloured grass
- - !type:SampleDecalDunGen
- allowedTiles:
- - FloorSnow
- divisions: 1
- threshold: -0.0
- noise:
- seed: 0
- noiseType: Cellular
- frequency: 1
- fractalType: None
- cellularDistanceFunction: Euclidean
- cellularReturnType: Distance2
- decals:
- - bushsnowa1
- - bushsnowa2
- - bushsnowa3
- - bushsnowb3
- - bushsnowb2
- - bushsnowb3
+ # Sparse vegetation
+ - !type:BiomeDecalLayer
+ allowedTiles:
+ - FloorSnow
+ divisions: 2
+ threshold: -0.50
+ noise:
+ seed: 0
+ noiseType: Cellular
+ frequency: 1
+ decals:
+ - grasssnowa1
+ - grasssnowa2
+ - grasssnowa3
+ - grasssnowb1
+ - grasssnowb2
+ - grasssnowb3
+ - grasssnowc1
+ - grasssnowc2
+ - grasssnowc3
+ # Dense, bland grass
+ - !type:BiomeDecalLayer
+ allowedTiles:
+ - FloorSnow
+ divisions: 1
+ threshold: -0.35
+ noise:
+ seed: 0
+ noiseType: Cellular
+ frequency: 0.2
+ fractalType: FBm
+ octaves: 5
+ lacunarity: 2
+ cellularDistanceFunction: Euclidean
+ cellularReturnType: Distance2
+ decals:
+ - grasssnow
+ - grasssnow01
+ - grasssnow02
+ - grasssnow03
+ - grasssnow04
+ - grasssnow05
+ - grasssnow06
+ - grasssnow07
+ - grasssnow08
+ - grasssnow09
+ - grasssnow10
+ - grasssnow11
+ - grasssnow12
+ - grasssnow13
+ # Little bit of coloured grass
+ - !type:BiomeDecalLayer
+ allowedTiles:
+ - FloorSnow
+ divisions: 1
+ threshold: -0.0
+ noise:
+ seed: 0
+ noiseType: Cellular
+ frequency: 1
+ fractalType: None
+ cellularDistanceFunction: Euclidean
+ cellularReturnType: Distance2
+ decals:
+ - bushsnowa1
+ - bushsnowa2
+ - bushsnowa3
+ - bushsnowb3
+ - bushsnowb2
+ - bushsnowb3
+ - !type:BiomeEntityLayer
+ threshold: 0.5
+ noise:
+ seed: 0
+ noiseType: OpenSimplex2
+ fractalType: FBm
+ frequency: 2
+ allowedTiles:
+ - FloorSnow
+ entities:
+ - FloraTreeSnow
+ # Rock formations
+ - !type:BiomeEntityLayer
+ allowedTiles:
+ - FloorSnow
+ threshold: -0.30
+ noise:
+ seed: 0
+ noiseType: Cellular
+ frequency: 0.05
+ lacunarity: 2
+ fractalType: FBm
+ octaves: 5
+ cellularDistanceFunction: Euclidean
+ cellularReturnType: Distance2
+ entities:
+ - WallRockSnow
+ # Ice tiles
+ - !type:BiomeTileLayer
+ tile: FloorIce
+ threshold: -0.9
+ noise:
+ seed: 0
+ noiseType: Cellular
+ frequency: 0.03
+ lacunarity: 2
+ fractalType: FBm
+ octaves: 5
+ cellularDistanceFunction: Euclidean
+ cellularReturnType: Distance2
+ # Liquid plasma rivers. Ice moon baby
+ - !type:BiomeEntityLayer
+ allowedTiles:
+ - FloorSnow
+ - FloorIce
+ threshold: 0.95
+ noise:
+ seed: 3
+ noiseType: OpenSimplex2
+ frequency: 0.003
+ lacunarity: 1.50
+ fractalType: Ridged
+ octaves: 1
+ entities:
+ - FloorLiquidPlasmaEntity
+ - !type:BiomeDummyLayer
+ id: Loot
+ - !type:BiomeTileLayer
+ threshold: -0.7
+ tile: FloorSnow
+ noise:
+ seed: 0
+ frequency: 0.02
+ fractalType: None
# Shadow -> Derived from lava
-- type: entity
- id: BiomeShadow
- categories: [ HideSpawnMenu ]
- components:
- - type: Biome
- layers:
- terrain:
- dungeon: ShadowTerrain
-
-- type: dungeonConfig
- id: ShadowTerrain
+- type: biomeTemplate
+ id: Shadow
layers:
- - !type:ChunkDunGen
- - !type:PrototypeDunGen
- proto: ShadowTiles
- inheritDungeons: All
- - !type:PrototypeDunGen
- proto: ShadowEntities
- inheritDungeons: All
-
-- type: dungeonConfig
- id: ShadowEntities
- reserveTiles: true
- layers:
- - !type:SampleEntityDunGen
+ - !type:BiomeEntityLayer
threshold: 0.70
noise:
frequency: 1
- ShadowBasaltThree
- ShadowBasaltFour
- ShadowBasaltFive
- - !type:SampleEntityDunGen
+ - !type:BiomeEntityLayer
threshold: 0.97
noise:
frequency: 1
- FloorChromite
entities:
- CrystalPink
- - !type:SampleEntityDunGen
+ - !type:BiomeEntityLayer
threshold: 0.97
noise:
seed: 1
entities:
- ShadowTree
# Rock formations
- - !type:SampleEntityDunGen
+ - !type:BiomeEntityLayer
threshold: -0.2
invert: true
noise:
entities:
- WallRockChromite
# chasm time
- - !type:SampleEntityDunGen
+ - !type:BiomeEntityLayer
allowedTiles:
- FloorChromite
threshold: 0.2
gain: 0.4
entities:
- FloorChromiteChasm
-
-- type: dungeonConfig
- id: ShadowTiles
- returnReserved: false
- layers:
- # Fill chromite
- - !type:SampleTileDunGen
- threshold: -1
- tile: FloorChromite
+ - !type:BiomeDummyLayer
+ id: Loot
+ # Fill chromite
+ - !type:BiomeTileLayer
+ threshold: -1
+ tile: FloorChromite
# Caves
-- type: entity
- id: BiomeCaves
- categories: [ HideSpawnMenu ]
- components:
- - type: Biome
- layers:
- terrain:
- dungeon: CavesTerrain
-
-- type: dungeonConfig
- id: CavesTerrain
- layers:
- - !type:ChunkDunGen
- - !type:PrototypeDunGen
- proto: CavesTiles
- inheritDungeons: All
- - !type:RoofDunGen
- - !type:PrototypeDunGen
- proto: CavesEntities
- inheritDungeons: All
-
-- type: dungeonConfig
- id: CavesEntities
- reserveTiles: true
+- type: biomeTemplate
+ id: Caves
layers:
- - !type:SampleEntityDunGen
+ - !type:BiomeEntityLayer
threshold: 0.85
noise:
seed: 2
- CrystalBlue
- CrystalYellow
- CrystalCyan
- - !type:SampleEntityDunGen
+ - !type:BiomeEntityLayer
threshold: 0.95
noise:
seed: 1
- FloorAsteroidSand
entities:
- FloraStalagmite
- - !type:SampleEntityDunGen
+ - !type:BiomeEntityLayer
threshold: -0.5
invert: true
noise:
- FloorAsteroidSand
entities:
- WallRock
-
-- type: dungeonConfig
- id: CavesTiles
- returnReserved: false
- layers:
- - !type:SampleTileDunGen
- threshold: -1.0
- tile: FloorAsteroidSand
+ - !type:BiomeDummyLayer
+ id: Loot
+ - !type:BiomeTileLayer
+ threshold: -1.0
+ tile: FloorAsteroidSand
# Asteroid
-- type: dungeonConfig
+- type: biomeTemplate
id: Asteroid
layers:
- - !type:SampleTileDunGen
- threshold: -1.0
- tile: FloorAsteroidSand
- reserveTiles: false
- - !type:SampleEntityDunGen
+ - !type:BiomeEntityLayer
threshold: 0.85
noise:
seed: 2
- CrystalBlue
- CrystalYellow
- CrystalCyan
- - !type:SampleEntityDunGen
+ - !type:BiomeEntityLayer
threshold: 0.95
noise:
seed: 1
- FloorAsteroidSand
entities:
- FloraStalagmite
- - !type:SampleEntityDunGen
- reserveTiles: false
+ - !type:BiomeEntityLayer
threshold: -0.6
invert: true
noise:
- FloorAsteroidSand
entities:
- AsteroidRock
+ - !type:BiomeTileLayer
+ threshold: -1.0
+ tile: FloorAsteroidSand
id: Haunted
layers:
- !type:PrefabDunGen
- roomWhitelist:
- tags:
- - Haunted
presets:
- Bucket
- Wow
- type: salvageLoot
id: SalvageLoot
loots:
- - !type:RandomSpawnsLoot
- entries:
- - proto: AdvMopItem
- prob: 0.5
- - proto: AmmoTechFabCircuitboard
- cost: 2
- - proto: AutolatheMachineCircuitboard
- cost: 2
- - proto: BiomassReclaimerMachineCircuitboard
- cost: 2
- - proto: BluespaceBeaker
- cost: 2
- - proto: CyborgEndoskeleton
- cost: 3
- prob: 0.5
- - proto: ChemDispenserMachineCircuitboard
- cost: 2
- - proto: CircuitImprinter
- cost: 2
- - proto: CloningConsoleComputerCircuitboard
- cost: 2
- - proto: CloningPodMachineCircuitboard
- cost: 2
- - proto: ChemistryBottleCognizine
- - proto: FoodBoxDonkpocketCarp
- prob: 0.5
- - proto: CrateSalvageEquipment
- cost: 3
- prob: 0.5
- - proto: GasRecycler
- cost: 2
- - proto: GeneratorRTG
- cost: 5
- - proto: GravityGeneratorMini
- cost: 2
- - proto: GyroscopeUnanchored
- cost: 2
- prob: 0.1
- - proto: MedicalScannerMachineCircuitboard
- cost: 2
- - proto: NuclearBombKeg
- cost: 5
- - proto: ChemistryBottleOmnizine
- prob: 0.5
- - proto: PortableGeneratorPacman
- cost: 2
- - proto: PortableGeneratorSuperPacman
- cost: 3
- - proto: PowerCellAntiqueProto
- cost: 5
- prob: 0.5
- - proto: ProtolatheMachineCircuitboard
- - proto: RandomArtifactSpawner
- cost: 2
- - proto: RandomCargoCorpseSpawner
- cost: 2
- prob: 0.5
- - proto: RandomCommandCorpseSpawner
- cost: 5
- prob: 0.5
- - proto: RandomEngineerCorpseSpawner
- cost: 2
- prob: 0.5
- - proto: RandomMedicCorpseSpawner
- cost: 2
- prob: 0.5
- - proto: RandomScienceCorpseSpawner
- cost: 2
- prob: 0.5
- - proto: RandomSecurityCorpseSpawner
- cost: 2
- prob: 0.5
- - proto: RandomServiceCorpseSpawner
- cost: 2
- prob: 0.5
- - proto: ResearchAndDevelopmentServerMachineCircuitboard
- cost: 5
- prob: 0.5
- - proto: ResearchDisk10000
- prob: 0.5
- - proto: ResearchDisk5000
- prob: 0.5
- - proto: RipleyHarness
- cost: 3
- prob: 0.5
- - proto: SpaceCash1000
- - proto: SpaceCash10000
- cost: 10
- - proto: SpaceCash2500
- cost: 3
- - proto: SpaceCash5000
- cost: 5
- - proto: TechnologyDiskRare
- cost: 5
- prob: 0.5
- - proto: ThrusterUnanchored
- - proto: WaterTankHighCapacity
- - proto: WeldingFuelTankHighCapacity
- cost: 3
- - proto: WeaponTeslaGun
- prob: 0.1
- cost: 2
+ - !type:RandomSpawnsLoot
+ entries:
+ - proto: AdvMopItem
+ prob: 0.5
+ - proto: AmmoTechFabCircuitboard
+ cost: 2
+ - proto: AutolatheMachineCircuitboard
+ cost: 2
+ - proto: BiomassReclaimerMachineCircuitboard
+ cost: 2
+ - proto: BluespaceBeaker
+ cost: 2
+ - proto: CyborgEndoskeleton
+ cost: 3
+ prob: 0.5
+ - proto: ChemDispenserMachineCircuitboard
+ cost: 2
+ - proto: CircuitImprinter
+ cost: 2
+ - proto: CloningConsoleComputerCircuitboard
+ cost: 2
+ - proto: CloningPodMachineCircuitboard
+ cost: 2
+ - proto: ChemistryBottleCognizine
+ - proto: FoodBoxDonkpocketCarp
+ prob: 0.5
+ - proto: CrateSalvageEquipment
+ cost: 3
+ prob: 0.5
+ - proto: GasRecycler
+ cost: 2
+ - proto: GeneratorRTG
+ cost: 5
+ - proto: GravityGeneratorMini
+ cost: 2
+ - proto: GyroscopeUnanchored
+ cost: 2
+ prob: 0.1
+ - proto: MedicalScannerMachineCircuitboard
+ cost: 2
+ - proto: NuclearBombKeg
+ cost: 5
+ - proto: ChemistryBottleOmnizine
+ prob: 0.5
+ - proto: PortableGeneratorPacman
+ cost: 2
+ - proto: PortableGeneratorSuperPacman
+ cost: 3
+ - proto: PowerCellAntiqueProto
+ cost: 5
+ prob: 0.5
+ - proto: ProtolatheMachineCircuitboard
+ - proto: RandomArtifactSpawner
+ cost: 2
+ - proto: RandomCargoCorpseSpawner
+ cost: 2
+ prob: 0.5
+ - proto: RandomCommandCorpseSpawner
+ cost: 5
+ prob: 0.5
+ - proto: RandomEngineerCorpseSpawner
+ cost: 2
+ prob: 0.5
+ - proto: RandomMedicCorpseSpawner
+ cost: 2
+ prob: 0.5
+ - proto: RandomScienceCorpseSpawner
+ cost: 2
+ prob: 0.5
+ - proto: RandomSecurityCorpseSpawner
+ cost: 2
+ prob: 0.5
+ - proto: RandomServiceCorpseSpawner
+ cost: 2
+ prob: 0.5
+ - proto: ResearchAndDevelopmentServerMachineCircuitboard
+ cost: 5
+ prob: 0.5
+ - proto: ResearchDisk10000
+ prob: 0.5
+ - proto: ResearchDisk5000
+ prob: 0.5
+ - proto: RipleyHarness
+ cost: 3
+ prob: 0.5
+ - proto: SpaceCash1000
+ - proto: SpaceCash10000
+ cost: 10
+ - proto: SpaceCash2500
+ cost: 3
+ - proto: SpaceCash5000
+ cost: 5
+ - proto: TechnologyDiskRare
+ cost: 5
+ prob: 0.5
+ - proto: ThrusterUnanchored
+ - proto: WaterTankHighCapacity
+ - proto: WeldingFuelTankHighCapacity
+ cost: 3
+ - proto: WeaponTeslaGun
+ prob: 0.1
+ cost: 2
# Mob loot table
id: OreIron
guaranteed: true
loots:
- - !type:BiomeLoot
- proto: OreIron
+ - !type:BiomeMarkerLoot
+ proto: OreIron
- type: salvageLoot
id: OreCoal
guaranteed: true
loots:
- - !type:BiomeLoot
- proto: OreCoal
+ - !type:BiomeMarkerLoot
+ proto: OreCoal
- type: salvageLoot
id: OreQuartz
guaranteed: true
loots:
- - !type:BiomeLoot
- proto: OreQuartz
+ - !type:BiomeMarkerLoot
+ proto: OreQuartz
- type: salvageLoot
id: OreSalt
guaranteed: true
loots:
- - !type:BiomeLoot
- proto: OreSalt
+ - !type:BiomeMarkerLoot
+ proto: OreSalt
# - Medium value
- type: salvageLoot
id: OreGold
guaranteed: true
loots:
- - !type:BiomeLoot
- proto: OreGold
+ - !type:BiomeMarkerLoot
+ proto: OreGold
- type: salvageLoot
id: OreSilver
guaranteed: true
loots:
- - !type:BiomeLoot
- proto: OreSilver
+ - !type:BiomeMarkerLoot
+ proto: OreSilver
# - High value
- type: salvageLoot
id: OrePlasma
guaranteed: true
loots:
- - !type:BiomeLoot
- proto: OrePlasma
+ - !type:BiomeMarkerLoot
+ proto: OrePlasma
- type: salvageLoot
id: OreUranium
guaranteed: true
loots:
- - !type:BiomeLoot
- proto: OreUranium
+ - !type:BiomeMarkerLoot
+ proto: OreUranium
- type: salvageLoot
id: OreDiamond
guaranteed: true
loots:
- - !type:BiomeLoot
- proto: OreDiamond
+ - !type:BiomeMarkerLoot
+ proto: OreDiamond
- type: salvageLoot
id: OreArtifactFragment
guaranteed: true
loots:
- - !type:BiomeLoot
- proto: OreArtifactFragment
+ - !type:BiomeMarkerLoot
+ proto: OreArtifactFragment
- type: salvageBiomeMod
id: Caves
desc: salvage-biome-mod-caves
- biome: BiomeCaves
+ biome: Caves
- type: salvageBiomeMod
id: Grasslands
desc: salvage-biome-mod-grasslands
- biome: BiomeGrasslands
+ biome: Grasslands
- type: salvageBiomeMod
id: Snow
desc: salvage-biome-mod-snow
cost: 1
- biome: BiomeSnow
+ biome: Snow
- type: salvageBiomeMod
id: Lava
desc: salvage-biome-mod-lava
cost: 2
- biome: BiomeLava
+ biome: Lava
#- type: salvageBiomeMod
# id: Space
- type: dungeonConfig
id: VGRoidSmaller
- minOffset: 60
- maxOffset: 80
+ minOffset: 40
+ maxOffset: 60
layers:
- !type:NoiseDistanceDunGen
size: 150, 150
maxCount: 3
layers:
- !type:ExteriorDunGen
- penetration: 5,15
proto: Experiment
- !type:EntityTableDunGen
minCount: 25
layers:
- !type:FillGridDunGen
entity: IronRock
- threshold: -0.95
- size: 350, 350
- distanceConfig: !type:DunGenEuclideanSquaredDistance
- blendWeight: -0.50
- reservedNoise:
- frequency: 0.080
- noiseType: OpenSimplex2
- fractalType: FBm
- octaves: 5
- lacunarity: 1.5
- gain: 0.5
allowedTiles:
- FloorAsteroidSand