Revert biome rework (#38724)

* Revert "Fix world generation (#38713)"

This reverts commit 10fa6ff4af.

* Revert "Biome rework (#37735)"

This reverts commit fe7b96147c.
This commit is contained in:
Pieter-Jan Briers
2025-07-03 20:48:04 +02:00
committed by GitHub
parent 047a49a505
commit e99fc501a6
116 changed files with 3541 additions and 3009 deletions

View File

@@ -0,0 +1,87 @@
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());
}
}

View File

@@ -0,0 +1,8 @@
using Content.Shared.Parallax.Biomes;
namespace Content.Client.Parallax;
public sealed class BiomeSystem : SharedBiomeSystem
{
}

View File

@@ -0,0 +1,22 @@
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());
}
}
}

View File

@@ -1,6 +1,8 @@
using System.Numerics; using System.Numerics;
using Content.Client.Parallax.Managers; using Content.Client.Parallax.Managers;
using Content.Shared.CCVar; using Content.Shared.CCVar;
using Content.Shared.Parallax.Biomes;
using Robust.Client.GameObjects;
using Robust.Client.Graphics; using Robust.Client.Graphics;
using Robust.Shared.Configuration; using Robust.Shared.Configuration;
using Robust.Shared.Enums; using Robust.Shared.Enums;
@@ -17,6 +19,7 @@ public sealed class ParallaxOverlay : Overlay
[Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!; [Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly IParallaxManager _manager = default!; [Dependency] private readonly IParallaxManager _manager = default!;
private readonly SharedMapSystem _mapSystem;
private readonly ParallaxSystem _parallax; private readonly ParallaxSystem _parallax;
public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowWorld; public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowWorld;
@@ -25,12 +28,13 @@ public sealed class ParallaxOverlay : Overlay
{ {
ZIndex = ParallaxSystem.ParallaxZIndex; ZIndex = ParallaxSystem.ParallaxZIndex;
IoCManager.InjectDependencies(this); IoCManager.InjectDependencies(this);
_mapSystem = _entManager.System<SharedMapSystem>();
_parallax = _entManager.System<ParallaxSystem>(); _parallax = _entManager.System<ParallaxSystem>();
} }
protected override bool BeforeDraw(in OverlayDrawArgs args) 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 false;
return true; return true;

View File

@@ -1,9 +1,23 @@
using System.Linq;
using Content.Client.Computer;
using Content.Client.Stylesheets; 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.AutoGenerated;
using Robust.Client.Graphics; using Robust.Client.Graphics;
using Robust.Client.UserInterface; using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML; 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; namespace Content.Client.Salvage.UI;

View File

@@ -1,4 +1,4 @@
using Content.Shared.Procedural; using Content.Shared.Parallax.Biomes.Markers;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
@@ -42,7 +42,7 @@ public sealed partial class GatewayGeneratorComponent : Component
/// Mob layers to pick from. /// Mob layers to pick from.
/// </summary> /// </summary>
[DataField] [DataField]
public List<ProtoId<DungeonConfigPrototype>> MobLayers = new() public List<ProtoId<BiomeMarkerLayerPrototype>> MobLayers = new()
{ {
"Carps", "Carps",
"Xenos", "Xenos",
@@ -54,7 +54,7 @@ public sealed partial class GatewayGeneratorComponent : Component
/// <summary> /// <summary>
/// Loot layers to pick from. /// Loot layers to pick from.
/// </summary> /// </summary>
public List<ProtoId<DungeonConfigPrototype>> LootLayers = new() public List<ProtoId<BiomeMarkerLayerPrototype>> LootLayers = new()
{ {
"OreIron", "OreIron",
"OreQuartz", "OreQuartz",

View File

@@ -1,11 +1,12 @@
using System.Linq; using System.Linq;
using Content.Server.Gateway.Components; using Content.Server.Gateway.Components;
using Content.Server.Parallax;
using Content.Server.Procedural; using Content.Server.Procedural;
using Content.Shared.CCVar; using Content.Shared.CCVar;
using Content.Shared.Dataset; using Content.Shared.Dataset;
using Content.Shared.Maps; using Content.Shared.Maps;
using Content.Shared.Parallax.Biomes;
using Content.Shared.Procedural; using Content.Shared.Procedural;
using Content.Shared.Procedural.Components;
using Content.Shared.Salvage; using Content.Shared.Salvage;
using Robust.Shared.Configuration; using Robust.Shared.Configuration;
using Robust.Shared.Map; using Robust.Shared.Map;
@@ -110,7 +111,7 @@ public sealed class GatewayGeneratorSystem : EntitySystem
}; };
AddComp(mapUid, restricted); AddComp(mapUid, restricted);
_biome.EnsurePlanet(mapUid, _protoManager.Index("BiomeGrasslands"), seed); _biome.EnsurePlanet(mapUid, _protoManager.Index<BiomeTemplatePrototype>("Continental"), seed);
var grid = Comp<MapGridComponent>(mapUid); var grid = Comp<MapGridComponent>(mapUid);
@@ -198,7 +199,7 @@ public sealed class GatewayGeneratorSystem : EntitySystem
var layer = lootLayers[layerIdx]; var layer = lootLayers[layerIdx];
lootLayers.RemoveSwap(layerIdx); lootLayers.RemoveSwap(layerIdx);
_biome.AddLayer((ent.Owner, biomeComp), $"{layer.Id}-{i}", layer.Id); _biome.AddMarkerLayer(ent.Owner, biomeComp, layer.Id);
} }
// - Mobs // - Mobs
@@ -210,7 +211,7 @@ public sealed class GatewayGeneratorSystem : EntitySystem
var layer = mobLayers[layerIdx]; var layer = mobLayers[layerIdx];
mobLayers.RemoveSwap(layerIdx); mobLayers.RemoveSwap(layerIdx);
_biome.AddLayer((ent.Owner, biomeComp), $"{layer.Id}-{i}", layer.Id); _biome.AddMarkerLayer(ent.Owner, biomeComp, layer.Id);
} }
} }
} }

View File

@@ -1,11 +1,20 @@
using System.Linq; using System.Linq;
using Content.Server.Administration; 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.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.Console;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.Maps; namespace Content.Server.Maps;
@@ -43,7 +52,7 @@ public sealed class PlanetCommand : LocalizedEntityCommands
return; 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]))); shell.WriteError(Loc.GetString("cmd-planet-map-prototype", ("prototype", args[1])));
return; return;
@@ -61,12 +70,9 @@ public sealed class PlanetCommand : LocalizedEntityCommands
if (args.Length == 1) if (args.Length == 1)
return CompletionResult.FromHintOptions(CompletionHelper.MapIds(_entManager), "Map Id"); return CompletionResult.FromHintOptions(CompletionHelper.MapIds(_entManager), "Map Id");
var biomeName = _entManager.ComponentFactory.GetComponentName<BiomeComponent>();
if (args.Length == 2) if (args.Length == 2)
{ {
var options = _protoManager.EnumeratePrototypes<EntityPrototype>() var options = _protoManager.EnumeratePrototypes<BiomeTemplatePrototype>()
.Where(o => o.Components.ContainsKey(biomeName))
.Select(o => new CompletionOption(o.ID, "Biome")); .Select(o => new CompletionOption(o.ID, "Biome"));
return CompletionResult.FromOptions(options); return CompletionResult.FromOptions(options);
} }

View File

@@ -65,9 +65,6 @@ public sealed partial class PathfindingSystem
{ {
for (var y = -1; y <= 1; y++) for (var y = -1; y <= 1; y++)
{ {
if (x == 0 && y == 0)
continue;
var neighbor = node + new Vector2i(x, y); var neighbor = node + new Vector2i(x, y);
var neighborCost = OctileDistance(node, neighbor) * args.TileCost?.Invoke(neighbor) ?? 1f; var neighborCost = OctileDistance(node, neighbor) * args.TileCost?.Invoke(neighbor) ?? 1f;
@@ -124,7 +121,8 @@ public sealed partial class PathfindingSystem
cameFrom[neighbor] = node; cameFrom[neighbor] = node;
costSoFar[neighbor] = gScore; 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; var fScore = gScore + hScore;
frontier.Enqueue(neighbor, fScore); frontier.Enqueue(neighbor, fScore);
} }

View File

@@ -0,0 +1,188 @@
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;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,105 +0,0 @@
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;
}
}
}

View File

@@ -1,65 +0,0 @@
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);
}
}

View File

@@ -1,661 +0,0 @@
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;
}
}

View File

@@ -151,8 +151,7 @@ public sealed partial class DungeonJob
if (found) if (found)
continue; continue;
var ent = _entManager.SpawnEntity(gen.Entity, _maps.GridTileToLocal(_gridUid, _grid, tile)); _entManager.SpawnEntity(gen.Entity, _maps.GridTileToLocal(_gridUid, _grid, tile));
AddLoadedEntity(tile, ent);
} }
} }
} }

View File

@@ -0,0 +1,74 @@
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;
}
}
}

View File

@@ -0,0 +1,105 @@
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;
}
}
}
}

View File

@@ -32,10 +32,7 @@ public sealed partial class DungeonJob
if (!_anchorable.TileFree((_gridUid, _grid), neighbor, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask)) if (!_anchorable.TileFree((_gridUid, _grid), neighbor, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
continue; continue;
var tile = _tile.GetVariantTile((ContentTileDefinition)tileDef, random); tiles.Add((neighbor, _tile.GetVariantTile((ContentTileDefinition) tileDef, random)));
tiles.Add((neighbor, tile));
AddLoadedTile(neighbor, tile);
DebugTools.Assert(dungeon.AllTiles.Contains(neighbor));
} }
foreach (var index in dungeon.CorridorExteriorTiles) foreach (var index in dungeon.CorridorExteriorTiles)
@@ -46,10 +43,7 @@ public sealed partial class DungeonJob
if (!_anchorable.TileFree((_gridUid, _grid), index, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask)) if (!_anchorable.TileFree((_gridUid, _grid), index, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
continue; continue;
var tile = _tile.GetVariantTile((ContentTileDefinition)tileDef, random); tiles.Add((index, _tile.GetVariantTile((ContentTileDefinition)tileDef, random)));
tiles.Add((index, tile));
AddLoadedTile(index, tile);
DebugTools.Assert(dungeon.AllTiles.Contains(index));
} }
_maps.SetTiles(_gridUid, _grid, tiles); _maps.SetTiles(_gridUid, _grid, tiles);
@@ -88,21 +82,18 @@ public sealed partial class DungeonJob
} }
if (isCorner) if (isCorner)
{ _entManager.SpawnEntity(cornerWall, _maps.GridTileToLocal(_gridUid, _grid, index.Index));
var uid = _entManager.SpawnEntity(cornerWall, _maps.GridTileToLocal(_gridUid, _grid, index.Index));
AddLoadedEntity(index.Index, uid);
}
if (!isCorner) if (!isCorner)
{ _entManager.SpawnEntity(wall, _maps.GridTileToLocal(_gridUid, _grid, index.Index));
var uid = _entManager.SpawnEntity(wall, _maps.GridTileToLocal(_gridUid, _grid, index.Index));
AddLoadedEntity(index.Index, uid);
}
if (i % 20 == 0)
{
await SuspendDungeon(); await SuspendDungeon();
if (!ValidateResume()) if (!ValidateResume())
return; return;
} }
} }
}
} }

View File

@@ -1,42 +0,0 @@
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;
}
}

View File

@@ -94,14 +94,12 @@ public sealed partial class DungeonJob
var setTiles = new List<(Vector2i, Tile)>(); var setTiles = new List<(Vector2i, Tile)>();
var tileDef = (ContentTileDefinition) _tileDefManager[gen.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; continue;
var tile = _tile.GetVariantTile(tileDef, random); setTiles.Add((tile, _tile.GetVariantTile(tileDef, random)));
setTiles.Add((node, tile));
AddLoadedTile(node, tile);
} }
_maps.SetTiles(_gridUid, _grid, setTiles); _maps.SetTiles(_gridUid, _grid, setTiles);

View File

@@ -48,13 +48,7 @@ public sealed partial class DungeonJob
var protos = _entTable.GetSpawns(contents, random); var protos = _entTable.GetSpawns(contents, random);
var coords = _maps.ToCenterCoordinates(_gridUid, tile, _grid); var coords = _maps.ToCenterCoordinates(_gridUid, tile, _grid);
var uids = _entManager.SpawnEntitiesAttachedTo(coords, protos); _entManager.SpawnEntitiesAttachedTo(coords, protos);
foreach (var uid in uids)
{
AddLoadedEntity(tile, uid);
}
await SuspendIfOutOfTime(); await SuspendIfOutOfTime();
if (!ValidateResume()) if (!ValidateResume())

View File

@@ -83,8 +83,7 @@ public sealed partial class DungeonJob
{ {
// Decals not being centered biting my ass again // Decals not being centered biting my ass again
var gridPos = _maps.GridTileToLocal(_gridUid, _grid, tile).Offset(offset); var gridPos = _maps.GridTileToLocal(_gridUid, _grid, tile).Offset(offset);
_decals.TryAddDecal(cDir, gridPos, out var did, color: decks.Color); _decals.TryAddDecal(cDir, gridPos, out _, color: decks.Color);
AddLoadedDecal(tile, did);
} }
} }
@@ -97,8 +96,7 @@ public sealed partial class DungeonJob
{ {
// Decals not being centered biting my ass again // Decals not being centered biting my ass again
var gridPos = _maps.GridTileToLocal(_gridUid, _grid, tile).Offset(offset); var gridPos = _maps.GridTileToLocal(_gridUid, _grid, tile).Offset(offset);
_decals.TryAddDecal(cDir, gridPos, out var did, color: decks.Color); _decals.TryAddDecal(cDir, gridPos, out _, color: decks.Color);
AddLoadedDecal(tile, did);
} }
continue; continue;
@@ -113,8 +111,7 @@ public sealed partial class DungeonJob
if (decks.CornerDecals.TryGetValue(dirFlag, out var cDir)) if (decks.CornerDecals.TryGetValue(dirFlag, out var cDir))
{ {
var gridPos = _maps.GridTileToLocal(_gridUid, _grid, tile).Offset(offset); var gridPos = _maps.GridTileToLocal(_gridUid, _grid, tile).Offset(offset);
_decals.TryAddDecal(cDir, gridPos, out var did, color: decks.Color); _decals.TryAddDecal(cDir, gridPos, out _, color: decks.Color);
AddLoadedDecal(tile, did);
} }
} }
} }

View File

@@ -28,7 +28,7 @@ public sealed partial class DungeonJob
Random random) Random random)
{ {
var tiles = new List<(Vector2i, Tile)>(); var tiles = new List<(Vector2i, Tile)>();
var matrix = Matrix3Helpers.CreateTranslation(_position + position); var matrix = Matrix3Helpers.CreateTranslation(position);
foreach (var layer in dungen.Layers) foreach (var layer in dungen.Layers)
{ {
@@ -76,9 +76,7 @@ public sealed partial class DungeonJob
break; break;
} }
var tile = new Tile(tileDef.TileId, variant: variant); tiles.Add((adjusted, new Tile(tileDef.TileId, variant: variant)));
tiles.Add((adjusted, tile));
AddLoadedTile(adjusted, tile);
roomTiles.Add(adjusted); roomTiles.Add(adjusted);
break; break;
} }
@@ -103,8 +101,6 @@ public sealed partial class DungeonJob
{ {
switch (distance) switch (distance)
{ {
case DunGenDistanceSquared:
return dx * dx + dy * dy;
case DunGenEuclideanSquaredDistance: case DunGenEuclideanSquaredDistance:
return MathF.Min(1f, (dx * dx + dy * dy) / MathF.Sqrt(2)); return MathF.Min(1f, (dx * dx + dy * dy) / MathF.Sqrt(2));
case DunGenSquareBump: case DunGenSquareBump:

View File

@@ -196,9 +196,7 @@ public sealed partial class DungeonJob
if (reservedTiles.Contains(index)) if (reservedTiles.Contains(index))
continue; continue;
var tile = new Tile(_tileDefManager[fallbackTile.Value].TileId); tiles.Add((index, new Tile(_tileDefManager[fallbackTile.Value].TileId)));
tiles.Add((index, tile));
AddLoadedTile(index, tile);
} }
} }
@@ -232,14 +230,7 @@ public sealed partial class DungeonJob
var dungeonMatty = Matrix3x2.Multiply(matty, dungeonTransform); var dungeonMatty = Matrix3x2.Multiply(matty, dungeonTransform);
// The expensive bit yippy. // The expensive bit yippy.
var data = _dungeon.SpawnRoom(_gridUid, _grid, dungeonMatty, room, reservedTiles); _dungeon.SpawnRoom(_gridUid, _grid, dungeonMatty, room, reservedTiles);
_data.Merge(data);
await SuspendDungeon();
if (!ValidateResume())
return dungeon;
var roomCenter = (room.Offset + room.Size / 2f) * _grid.TileSize; var roomCenter = (room.Offset + room.Size / 2f) * _grid.TileSize;
var roomTiles = new HashSet<Vector2i>(room.Size.X * room.Size.Y); var roomTiles = new HashSet<Vector2i>(room.Size.X * room.Size.Y);

View File

@@ -42,7 +42,6 @@ public sealed partial class DungeonJob
} }
replacements.Add((node, tile)); replacements.Add((node, tile));
AddLoadedTile(node, tile);
break; break;
} }

View File

@@ -71,17 +71,14 @@ public sealed partial class DungeonJob
isValid = true; isValid = true;
// Entrance wew // Entrance wew
var tileVariant = _tile.GetVariantTile(tileDef, random); _maps.SetTile(_gridUid, _grid, tile, _tile.GetVariantTile(tileDef, random));
_maps.SetTile(_gridUid, _grid, tile, tileVariant);
AddLoadedTile(tile, tileVariant);
ClearDoor(dungeon, _grid, tile); ClearDoor(dungeon, _grid, tile);
var gridCoords = _maps.GridTileToLocal(_gridUid, _grid, tile); var gridCoords = _maps.GridTileToLocal(_gridUid, _grid, tile);
// Need to offset the spawn to avoid spawning in the room. // Need to offset the spawn to avoid spawning in the room.
foreach (var ent in _entTable.GetSpawns(contents, random)) foreach (var ent in _entTable.GetSpawns(contents, random))
{ {
var uid = _entManager.SpawnAtPosition(ent, gridCoords); _entManager.SpawnAtPosition(ent, gridCoords);
AddLoadedEntity(tile, uid);
} }
// Clear out any biome tiles nearby to avoid blocking it // Clear out any biome tiles nearby to avoid blocking it

View File

@@ -51,7 +51,6 @@ public sealed partial class DungeonJob
foreach (var ent in entities) foreach (var ent in entities)
{ {
var uid = _entManager.SpawnAtPosition(ent, _maps.GridTileToLocal(_gridUid, _grid, tile)); var uid = _entManager.SpawnAtPosition(ent, _maps.GridTileToLocal(_gridUid, _grid, tile));
AddLoadedEntity(tile, uid);
_entManager.RemoveComponent<GhostRoleComponent>(uid); _entManager.RemoveComponent<GhostRoleComponent>(uid);
_entManager.RemoveComponent<GhostTakeoverAvailableComponent>(uid); _entManager.RemoveComponent<GhostTakeoverAvailableComponent>(uid);
npcs.SleepNPC(uid); npcs.SleepNPC(uid);

View File

@@ -35,9 +35,7 @@ public sealed partial class DungeonJob
if (reservedTiles.Contains(neighbor)) if (reservedTiles.Contains(neighbor))
continue; continue;
var tile = _tile.GetVariantTile((ContentTileDefinition)tileDef, random); tiles.Add((neighbor, _tile.GetVariantTile((ContentTileDefinition) tileDef, random)));
tiles.Add((neighbor, tile));
AddLoadedTile(neighbor, tile);
spawnPositions.Add(neighbor); spawnPositions.Add(neighbor);
} }
} }
@@ -47,12 +45,7 @@ public sealed partial class DungeonJob
foreach (var entrance in spawnPositions) foreach (var entrance in spawnPositions)
{ {
var uids = _entManager.SpawnEntitiesAttachedTo(_maps.GridTileToLocal(_gridUid, _grid, entrance), _entTable.GetSpawns(contents, random)); _entManager.SpawnEntitiesAttachedTo(_maps.GridTileToLocal(_gridUid, _grid, entrance), _entTable.GetSpawns(contents, random));
foreach (var uid in uids)
{
AddLoadedEntity(entrance, uid);
}
} }
} }
} }

View File

@@ -1,4 +1,3 @@
using System.Numerics;
using System.Threading.Tasks; using System.Threading.Tasks;
using Content.Shared.Maps; using Content.Shared.Maps;
using Content.Shared.NPC; using Content.Shared.NPC;
@@ -14,22 +13,15 @@ public sealed partial class DungeonJob
/// <summary> /// <summary>
/// <see cref="ExteriorDunGen"/> /// <see cref="ExteriorDunGen"/>
/// </summary> /// </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); DebugTools.Assert(_grid.ChunkCount > 0);
var aabb = new Box2i(_grid.LocalAABB.BottomLeft.Floored(), _grid.LocalAABB.TopRight.Floored()); 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 = random.NextAngle();
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 distance = Math.Max(aabb.Width / 2f + 1f, aabb.Height / 2f + 1f); var distance = Math.Max(aabb.Width / 2f + 1f, aabb.Height / 2f + 1f);
var startTile = new Vector2i(0, (int) distance).Rotate(angle); var startTile = new Vector2i(0, (int) distance).Rotate(angle);
Vector2i? dungeonSpawn = null; Vector2i? dungeonSpawn = null;
@@ -55,19 +47,9 @@ public sealed partial class DungeonJob
}; };
} }
// Move it further in based on the spawn angle. var config = _prototype.Index(dungen.Proto);
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 nextSeed = random.Next(); var nextSeed = random.Next();
var (dungeons, newReserved) = await GetDungeons(dungeonSpawn.Value, subConfig, subConfig.Layers, nextSeed, new Random(nextSeed), reserved: reservedTiles); var dungeons = await GetDungeons(dungeonSpawn.Value, config, config.Layers, reservedTiles, nextSeed, new Random(nextSeed));
reservedTiles.UnionWith(newReserved);
return dungeons; return dungeons;
} }

View File

@@ -105,9 +105,7 @@ public sealed partial class DungeonJob
if (reservedTiles.Contains(neighbor)) if (reservedTiles.Contains(neighbor))
continue; continue;
var tileVariant = _tile.GetVariantTile((ContentTileDefinition)tileDef, random); tiles.Add((neighbor, _tile.GetVariantTile((ContentTileDefinition) tileDef, random)));
tiles.Add((neighbor, tileVariant));
AddLoadedTile(neighbor, tileVariant);
index++; index++;
takenTiles.Add(neighbor); takenTiles.Add(neighbor);
} }
@@ -121,13 +119,7 @@ public sealed partial class DungeonJob
{ {
var gridPos = _maps.GridTileToLocal(_gridUid, _grid, tile.Item1); var gridPos = _maps.GridTileToLocal(_gridUid, _grid, tile.Item1);
var uids = _entManager.SpawnEntitiesAttachedTo(gridPos, _entTable.GetSpawns(contents, random)); _entManager.SpawnEntitiesAttachedTo(gridPos, _entTable.GetSpawns(contents, random));
foreach (var uid in uids)
{
AddLoadedEntity(tile.Item1, uid);
}
await SuspendDungeon(); await SuspendDungeon();
if (!ValidateResume()) if (!ValidateResume())

View File

@@ -18,45 +18,21 @@ public sealed partial class DungeonJob
if (reservedTiles.Contains(tile)) if (reservedTiles.Contains(tile))
continue; continue;
await SuspendDungeon();
if (!ValidateResume())
return;
if (!_maps.TryGetTileDef(_grid, tile, out var tileDef)) if (!_maps.TryGetTileDef(_grid, tile, out var tileDef))
continue; continue;
if (fill.AllowedTiles != null && !fill.AllowedTiles.Contains(tileDef.ID)) if (fill.AllowedTiles != null && !fill.AllowedTiles.Contains(tileDef.ID))
continue; 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)) if (!_anchorable.TileFree((_gridUid, _grid), tile, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
continue; continue;
var gridPos = _maps.GridTileToLocal(_gridUid, _grid, tile); 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;
} }
} }
} }

View File

@@ -52,8 +52,6 @@ public sealed partial class DungeonJob
} }
} }
} }
dungeon.RefreshAllTiles();
} }
private void WidenCorridor(Dungeon dungeon, float width, ICollection<Vector2i> corridorTiles) private void WidenCorridor(Dungeon dungeon, float width, ICollection<Vector2i> corridorTiles)

View File

@@ -81,16 +81,9 @@ public sealed partial class DungeonJob
{ {
var tile = validTiles[j]; var tile = validTiles[j];
var gridPos = _maps.GridTileToLocal(_gridUid, _grid, tile); var gridPos = _maps.GridTileToLocal(_gridUid, _grid, tile);
var tileVariant = _tile.GetVariantTile((ContentTileDefinition)tileDef, random); _maps.SetTile(_gridUid, _grid, tile, _tile.GetVariantTile((ContentTileDefinition) tileDef, random));
_maps.SetTile(_gridUid, _grid, tile, tileVariant);
AddLoadedTile(tile, tileVariant);
var uids = _entManager.SpawnEntitiesAttachedTo(gridPos, _entTable.GetSpawns(contents, random)); _entManager.SpawnEntitiesAttachedTo(gridPos, _entTable.GetSpawns(contents, random));
foreach (var uid in uids)
{
AddLoadedEntity(tile, uid);
}
} }
if (validTiles.Count > 0) if (validTiles.Count > 0)

View File

@@ -113,17 +113,10 @@ public sealed partial class DungeonJob
if (reservedTiles.Contains(weh)) if (reservedTiles.Contains(weh))
continue; continue;
var tileVariant = _tile.GetVariantTile((ContentTileDefinition)tileDef, random); _maps.SetTile(_gridUid, _grid, weh, _tile.GetVariantTile((ContentTileDefinition) tileDef, random));
_maps.SetTile(_gridUid, _grid, weh, tileVariant);
AddLoadedTile(weh, tileVariant);
var coords = _maps.GridTileToLocal(_gridUid, _grid, weh); var coords = _maps.GridTileToLocal(_gridUid, _grid, weh);
var uids = _entManager.SpawnEntitiesAttachedTo(coords, _entTable.GetSpawns(contents, random)); _entManager.SpawnEntitiesAttachedTo(coords, _entTable.GetSpawns(contents, random));
foreach (var uid in uids)
{
AddLoadedEntity(weh, uid);
}
} }
break; break;

View File

@@ -18,7 +18,6 @@ public sealed partial class DungeonJob
// Grab all of the room bounds // Grab all of the room bounds
// Then, work out connections between them // Then, work out connections between them
var roomBorders = new Dictionary<DungeonRoom, HashSet<Vector2i>>(dungeon.Rooms.Count); var roomBorders = new Dictionary<DungeonRoom, HashSet<Vector2i>>(dungeon.Rooms.Count);
var flank = gen.Flank;
foreach (var room in dungeon.Rooms) foreach (var room in dungeon.Rooms)
{ {
@@ -108,30 +107,18 @@ public sealed partial class DungeonJob
continue; continue;
width--; width--;
var tileVariant = _tile.GetVariantTile((ContentTileDefinition)tileDef, random); _maps.SetTile(_gridUid, _grid, node, _tile.GetVariantTile((ContentTileDefinition) tileDef, random));
_maps.SetTile(_gridUid, _grid, node, tileVariant);
AddLoadedTile(node, tileVariant);
if (flankContents != null && nodeDistances.Count - i <= 2) if (flankContents != null && nodeDistances.Count - i <= 2)
{ {
var uids = _entManager.SpawnEntitiesAttachedTo(gridPos, _entTable.GetSpawns(flankContents, random)); _entManager.SpawnEntitiesAttachedTo(gridPos, _entTable.GetSpawns(flankContents, random));
foreach (var uid in uids)
{
AddLoadedEntity(node, uid);
}
} }
else else
{ {
// Iterate neighbors and check for blockers, if so bulldoze // Iterate neighbors and check for blockers, if so bulldoze
ClearDoor(dungeon, _grid, node); ClearDoor(dungeon, _grid, node);
var uids = _entManager.SpawnEntitiesAttachedTo(gridPos, _entTable.GetSpawns(contents, random)); _entManager.SpawnEntitiesAttachedTo(gridPos, _entTable.GetSpawns(contents, random));
foreach (var uid in uids)
{
AddLoadedEntity(node, uid);
}
} }
if (width == 0) if (width == 0)

View File

@@ -49,7 +49,6 @@ public sealed partial class DungeonJob
_entManager.RemoveComponent<GhostRoleComponent>(uid); _entManager.RemoveComponent<GhostRoleComponent>(uid);
_entManager.RemoveComponent<GhostTakeoverAvailableComponent>(uid); _entManager.RemoveComponent<GhostTakeoverAvailableComponent>(uid);
npcs.SleepNPC(uid); npcs.SleepNPC(uid);
AddLoadedEntity(tile, uid);
} }
break; break;

View File

@@ -98,9 +98,7 @@ public sealed partial class DungeonJob
var variant = _tile.PickVariant((ContentTileDefinition) tileDef, random); var variant = _tile.PickVariant((ContentTileDefinition) tileDef, random);
var adjusted = Vector2.Transform(node + _grid.TileSizeHalfVector, matrix).Floored(); var adjusted = Vector2.Transform(node + _grid.TileSizeHalfVector, matrix).Floored();
var tileVariant = new Tile(tileDef.TileId, variant: variant); tiles.Add((adjusted, new Tile(tileDef.TileId, variant: variant)));
tiles.Add((adjusted, tileVariant));
AddLoadedTile(adjusted, tileVariant);
roomTiles.Add(adjusted); roomTiles.Add(adjusted);
tileCount++; tileCount++;
break; break;
@@ -129,7 +127,8 @@ public sealed partial class DungeonJob
} }
} }
await SuspendDungeon(); await SuspendIfOutOfTime();
ValidateResume();
} }
var center = Vector2.Zero; var center = Vector2.Zero;
@@ -141,7 +140,8 @@ public sealed partial class DungeonJob
center /= roomTiles.Count; center /= roomTiles.Count;
rooms.Add(new DungeonRoom(roomTiles, center, roomArea, new HashSet<Vector2i>())); rooms.Add(new DungeonRoom(roomTiles, center, roomArea, new HashSet<Vector2i>()));
await SuspendDungeon(); await SuspendIfOutOfTime();
ValidateResume();
} }
_maps.SetTiles(_gridUid, _grid, tiles); _maps.SetTiles(_gridUid, _grid, tiles);

View File

@@ -18,27 +18,13 @@ public sealed partial class DungeonJob
List<Dungeon> dungeons, List<Dungeon> dungeons,
HashSet<Vector2i> reservedTiles, HashSet<Vector2i> reservedTiles,
Random random) Random random)
{
foreach (var dungeon in dungeons)
{ {
var emptyTiles = false; var emptyTiles = false;
var replaceEntities = new Dictionary<Vector2i, EntityUid>(); var replaceEntities = new Dictionary<Vector2i, EntityUid>();
var availableTiles = new List<Vector2i>(); 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)
{
foreach (var node in dungeon.AllTiles) foreach (var node in dungeon.AllTiles)
{ {
if (reservedTiles.Contains(node)) if (reservedTiles.Contains(node))
@@ -55,25 +41,21 @@ public sealed partial class DungeonJob
// We use existing entities as a mark to spawn in place // We use existing entities as a mark to spawn in place
// OR // OR
// We check for any existing entities to see if we can spawn there. // 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))
{ {
// We can't replace so just stop here.
if (gen.Replacement == null)
break;
var prototype = _entManager.GetComponent<MetaDataComponent>(uid.Value).EntityPrototype; var prototype = _entManager.GetComponent<MetaDataComponent>(uid.Value).EntityPrototype;
if (string.IsNullOrEmpty(prototype?.ID)) if (prototype?.ID == gen.Replacement)
continue;
// It has a valid remapping so take it over.
if (replacementRemapping.ContainsKey(prototype.ID))
{ {
replaceEntities[node] = uid.Value; replaceEntities[node] = uid.Value;
found = true; found = true;
break; break;
} }
} }
}
if (!found) if (!found)
continue; continue;
@@ -86,13 +68,12 @@ public sealed partial class DungeonJob
if (!ValidateResume()) if (!ValidateResume())
return; return;
} }
}
var remapping = new Dictionary<EntProtoId, EntProtoId>(); var remapping = new Dictionary<EntProtoId, EntProtoId>();
// TODO: Move this to engine // TODO: Move this to engine
if (_prototype.TryIndex(gen.Entity, out var proto) && if (_prototype.TryIndex(gen.Entity, out var proto) &&
proto.Components.TryGetComponent(remapName, out var comps)) proto.Components.TryGetComponent("EntityRemap", out var comps))
{ {
var remappingComp = (EntityRemapComponent) comps; var remappingComp = (EntityRemapComponent) comps;
remapping = remappingComp.Mask; remapping = remappingComp.Mask;
@@ -103,6 +84,11 @@ public sealed partial class DungeonJob
// Iterate the group counts and pathfind out each group. // Iterate the group counts and pathfind out each group.
for (var i = 0; i < gen.Count; i++) for (var i = 0; i < gen.Count; i++)
{ {
await SuspendDungeon();
if (!ValidateResume())
return;
var groupSize = random.Next(gen.MinGroupSize, gen.MaxGroupSize + 1); var groupSize = random.Next(gen.MinGroupSize, gen.MaxGroupSize + 1);
// While we have remaining tiles keep iterating // While we have remaining tiles keep iterating
@@ -137,10 +123,9 @@ public sealed partial class DungeonJob
var prototype = gen.Entity; var prototype = gen.Entity;
// May have been deleted while iteration was suspended. if (replaceEntities.TryGetValue(node, out var existingEnt))
if (replaceEntities.TryGetValue(node, out var existingEnt) && _entManager.TryGetComponent(existingEnt, out MetaDataComponent? metadata))
{ {
var existingProto = metadata.EntityPrototype; var existingProto = _entManager.GetComponent<MetaDataComponent>(existingEnt).EntityPrototype;
_entManager.DeleteEntity(existingEnt); _entManager.DeleteEntity(existingEnt);
if (existingProto != null && remapping.TryGetValue(existingProto.ID, out var remapped)) if (existingProto != null && remapping.TryGetValue(existingProto.ID, out var remapped))
@@ -150,22 +135,16 @@ public sealed partial class DungeonJob
} }
// Tile valid salad so add it. // Tile valid salad so add it.
var uid = _entManager.SpawnAtPosition(prototype, _maps.GridTileToLocal(_gridUid, _grid, node)); _entManager.SpawnAtPosition(prototype, _maps.GridTileToLocal(_gridUid, _grid, node));
AddLoadedEntity(node, uid);
groupSize--; groupSize--;
await SuspendDungeon();
if (!ValidateResume())
return;
} }
} }
if (groupSize > 0) if (groupSize > 0)
{ {
// Not super worried depending on the gen it's fine. _sawmill.Warning($"Found remaining group size for ore veins of {gen.Replacement ?? "null"}!");
_sawmill.Debug($"Found remaining group size for ore veins of {gen.Replacement ?? "null"} / {gen.Entity}!"); }
} }
} }
} }

View File

@@ -1,38 +0,0 @@
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);
}
}

View File

@@ -25,9 +25,7 @@ public sealed partial class DungeonJob
if (reservedTiles.Contains(entrance)) if (reservedTiles.Contains(entrance))
continue; continue;
var tileVariant = _tile.GetVariantTile((ContentTileDefinition)tileDef, random); setTiles.Add((entrance, _tile.GetVariantTile((ContentTileDefinition) tileDef, random)));
setTiles.Add((entrance, tileVariant));
AddLoadedTile(entrance, tileVariant);
} }
} }
@@ -40,15 +38,10 @@ public sealed partial class DungeonJob
if (reservedTiles.Contains(entrance)) if (reservedTiles.Contains(entrance))
continue; continue;
var uids = _entManager.SpawnEntitiesAttachedTo( _entManager.SpawnEntitiesAttachedTo(
_maps.GridTileToLocal(_gridUid, _grid, entrance), _maps.GridTileToLocal(_gridUid, _grid, entrance),
_entTable.GetSpawns(contents, random)); _entTable.GetSpawns(contents, random));
foreach (var uid in uids)
{
AddLoadedEntity(entrance, uid);
}
await SuspendDungeon(); await SuspendDungeon();
if (!ValidateResume()) if (!ValidateResume())

View File

@@ -1,64 +0,0 @@
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);
}
}

View File

@@ -1,62 +0,0 @@
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);
}
}

View File

@@ -1,66 +0,0 @@
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);
}
}
}
}

View File

@@ -2,7 +2,6 @@ using System.Numerics;
using System.Threading.Tasks; using System.Threading.Tasks;
using Content.Server.NPC.Pathfinding; using Content.Server.NPC.Pathfinding;
using Content.Shared.Procedural; using Content.Shared.Procedural;
using Content.Shared.Procedural.DungeonLayers;
using Content.Shared.Procedural.PostGeneration; using Content.Shared.Procedural.PostGeneration;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Random; using Robust.Shared.Random;
@@ -12,10 +11,10 @@ namespace Content.Server.Procedural.DungeonJob;
public sealed partial class DungeonJob public sealed partial class DungeonJob
{ {
/// <summary> /// <summary>
/// <see cref="Shared.Procedural.DungeonLayers.SplineDungeonConnectorDunGen"/> /// <see cref="SplineDungeonConnectorDunGen"/>
/// </summary> /// </summary>
private async Task<Dungeon> PostGen( private async Task<Dungeon> PostGen(
Shared.Procedural.DungeonLayers.SplineDungeonConnectorDunGen gen, SplineDungeonConnectorDunGen gen,
List<Dungeon> dungeons, List<Dungeon> dungeons,
HashSet<Vector2i> reservedTiles, HashSet<Vector2i> reservedTiles,
Random random) Random random)
@@ -60,7 +59,6 @@ public sealed partial class DungeonJob
{ {
Start = pair.Start, Start = pair.Start,
End = pair.End, End = pair.End,
Diagonals = false,
TileCost = node => TileCost = node =>
{ {
// We want these to get prioritised internally and into space if it's a space dungeon. // We want these to get prioritised internally and into space if it's a space dungeon.
@@ -112,7 +110,6 @@ public sealed partial class DungeonJob
} }
tiles.Add((node, tile)); tiles.Add((node, tile));
AddLoadedTile(node, tile);
} }
_maps.SetTiles(_gridUid, _grid, tiles); _maps.SetTiles(_gridUid, _grid, tiles);
@@ -126,7 +123,6 @@ public sealed partial class DungeonJob
allTiles.Add(node); allTiles.Add(node);
tiles.Add((node, pathTile)); tiles.Add((node, pathTile));
AddLoadedTile(node, pathTile);
} }
_maps.SetTiles(_gridUid, _grid, tiles); _maps.SetTiles(_gridUid, _grid, tiles);

View File

@@ -32,18 +32,11 @@ public sealed partial class DungeonJob
if (reservedTiles.Contains(neighbor)) if (reservedTiles.Contains(neighbor))
continue; continue;
var tileVariant = _tile.GetVariantTile(tileDef, random); _maps.SetTile(_gridUid, _grid, neighbor, _tile.GetVariantTile(tileDef, random));
_maps.SetTile(_gridUid, _grid, neighbor, tileVariant);
AddLoadedTile(neighbor, tileVariant);
var gridPos = _maps.GridTileToLocal(_gridUid, _grid, neighbor); var gridPos = _maps.GridTileToLocal(_gridUid, _grid, neighbor);
var protoNames = _entTable.GetSpawns(contents, random); var protoNames = _entTable.GetSpawns(contents, random);
var uids = _entManager.SpawnEntitiesAttachedTo(gridPos, protoNames); _entManager.SpawnEntitiesAttachedTo(gridPos, protoNames);
foreach (var uid in uids)
{
AddLoadedEntity(neighbor, uid);
}
await SuspendDungeon(); await SuspendDungeon();
if (!ValidateResume()) if (!ValidateResume())

View File

@@ -1,7 +1,8 @@
using System.Numerics; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Content.Server.Decals; using Content.Server.Decals;
using Content.Server.NPC.Components;
using Content.Server.NPC.HTN; using Content.Server.NPC.HTN;
using Content.Server.NPC.Systems; using Content.Server.NPC.Systems;
using Content.Server.Shuttles.Systems; using Content.Server.Shuttles.Systems;
@@ -22,10 +23,11 @@ using Robust.Shared.Physics.Components;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Utility; using Robust.Shared.Utility;
using IDunGenLayer = Content.Shared.Procedural.IDunGenLayer;
namespace Content.Server.Procedural.DungeonJob; 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; public bool TimeSlice = true;
@@ -58,10 +60,6 @@ public sealed partial class DungeonJob : Job<(List<Dungeon>, DungeonData)>
private readonly ISawmill _sawmill; private readonly ISawmill _sawmill;
private DungeonData _data = new();
private HashSet<Vector2i>? _reservedTiles;
public DungeonJob( public DungeonJob(
ISawmill sawmill, ISawmill sawmill,
double maxTime, double maxTime,
@@ -81,14 +79,12 @@ public sealed partial class DungeonJob : Job<(List<Dungeon>, DungeonData)>
int seed, int seed,
Vector2i position, Vector2i position,
EntityCoordinates? targetCoordinates = null, EntityCoordinates? targetCoordinates = null,
CancellationToken cancellation = default, CancellationToken cancellation = default) : base(maxTime, cancellation)
HashSet<Vector2i>? reservedTiles = null) : base(maxTime, cancellation)
{ {
_sawmill = sawmill; _sawmill = sawmill;
_entManager = entManager; _entManager = entManager;
_prototype = prototype; _prototype = prototype;
_tileDefManager = tileDefManager; _tileDefManager = tileDefManager;
_reservedTiles = reservedTiles;
_anchorable = anchorable; _anchorable = anchorable;
_decals = decals; _decals = decals;
@@ -115,18 +111,17 @@ public sealed partial class DungeonJob : Job<(List<Dungeon>, DungeonData)>
/// <summary> /// <summary>
/// Gets the relevant dungeon, running recursively as relevant. /// Gets the relevant dungeon, running recursively as relevant.
/// </summary> /// </summary>
/// <param name="reservedTiles">Should we reserve tiles even if the config doesn't specify.</param> /// <param name="reserve">Should we reserve tiles even if the config doesn't specify.</param>
private async Task<(List<Dungeon>, HashSet<Vector2i>)> GetDungeons( private async Task<List<Dungeon>> GetDungeons(
Vector2i position, Vector2i position,
DungeonConfig config, DungeonConfig config,
List<IDunGenLayer> layers, List<IDunGenLayer> layers,
HashSet<Vector2i> reservedTiles,
int seed, int seed,
Random random, Random random,
HashSet<Vector2i>? reserved = null,
List<Dungeon>? existing = null) List<Dungeon>? existing = null)
{ {
var dungeons = new List<Dungeon>(); 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. // 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) if (existing != null)
@@ -143,7 +138,7 @@ public sealed partial class DungeonJob : Job<(List<Dungeon>, DungeonData)>
foreach (var layer in layers) foreach (var layer in layers)
{ {
var dungCount = dungeons.Count; var dungCount = dungeons.Count;
await RunLayer(i, count, config, dungeons, position, layer, reservedTiles, seed, random); await RunLayer(dungeons, position, layer, reservedTiles, seed, random);
if (config.ReserveTiles) if (config.ReserveTiles)
{ {
@@ -157,23 +152,24 @@ public sealed partial class DungeonJob : Job<(List<Dungeon>, DungeonData)>
await SuspendDungeon(); await SuspendDungeon();
if (!ValidateResume()) if (!ValidateResume())
return (new List<Dungeon>(), new HashSet<Vector2i>()); return new List<Dungeon>();
} }
} }
// Only return the new dungeons and applicable reserved tiles. return dungeons;
return (dungeons[(existing?.Count ?? 0)..], config.ReturnReserved ? reservedTiles : new HashSet<Vector2i>());
} }
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)}"); _sawmill.Info($"Generating dungeon {_gen} with seed {_seed} on {_entManager.ToPrettyString(_gridUid)}");
_grid.CanSplit = false; _grid.CanSplit = false;
var random = new Random(_seed); var random = new Random(_seed);
var oldTileCount = _reservedTiles?.Count ?? 0;
var position = (_position + random.NextPolarVector2(_gen.MinOffset, _gen.MaxOffset)).Floored(); 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. // To make it slightly more deterministic treat this RNG as separate ig.
// Post-processing after finishing loading. // Post-processing after finishing loading.
@@ -185,7 +181,6 @@ public sealed partial class DungeonJob : Job<(List<Dungeon>, DungeonData)>
} }
// Defer splitting so they don't get spammed and so we don't have to worry about tracking the grid along the way. // 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; _grid.CanSplit = true;
_entManager.System<GridFixtureSystem>().CheckSplits(_gridUid); _entManager.System<GridFixtureSystem>().CheckSplits(_gridUid);
var npcSystem = _entManager.System<NPCSystem>(); var npcSystem = _entManager.System<NPCSystem>();
@@ -199,13 +194,10 @@ public sealed partial class DungeonJob : Job<(List<Dungeon>, DungeonData)>
} }
_sawmill.Info($"Finished generating dungeon {_gen} with seed {_seed}"); _sawmill.Info($"Finished generating dungeon {_gen} with seed {_seed}");
return (dungeons, _data); return dungeons;
} }
private async Task RunLayer( private async Task RunLayer(
int runCount,
int maxRuns,
DungeonConfig config,
List<Dungeon> dungeons, List<Dungeon> dungeons,
Vector2i position, Vector2i position,
IDunGenLayer layer, IDunGenLayer layer,
@@ -213,7 +205,7 @@ public sealed partial class DungeonJob : Job<(List<Dungeon>, DungeonData)>
int seed, int seed,
Random random) 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. // 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 // Some of these don't care about reservedtiles because they only operate on dungeon tiles (which should
@@ -227,12 +219,15 @@ public sealed partial class DungeonJob : Job<(List<Dungeon>, DungeonData)>
case AutoCablingDunGen cabling: case AutoCablingDunGen cabling:
await PostGen(cabling, dungeons[^1], reservedTiles, random); await PostGen(cabling, dungeons[^1], reservedTiles, random);
break; 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: case BoundaryWallDunGen boundary:
await PostGen(boundary, dungeons[^1], reservedTiles, random); await PostGen(boundary, dungeons[^1], reservedTiles, random);
break; break;
case ChunkDunGen chunk:
dungeons.Add(await PostGen(chunk, reservedTiles, random));
break;
case CornerClutterDunGen clutter: case CornerClutterDunGen clutter:
await PostGen(clutter, dungeons[^1], reservedTiles, random); await PostGen(clutter, dungeons[^1], reservedTiles, random);
break; break;
@@ -249,7 +244,7 @@ public sealed partial class DungeonJob : Job<(List<Dungeon>, DungeonData)>
await PostGen(flank, dungeons[^1], reservedTiles, random); await PostGen(flank, dungeons[^1], reservedTiles, random);
break; break;
case ExteriorDunGen exterior: case ExteriorDunGen exterior:
dungeons.AddRange(await GenerateExteriorDungen(runCount, maxRuns, position, exterior, reservedTiles, random)); dungeons.AddRange(await GenerateExteriorDungen(position, exterior, reservedTiles, random));
break; break;
case FillGridDunGen fill: case FillGridDunGen fill:
await GenerateFillDunGen(fill, dungeons, reservedTiles); await GenerateFillDunGen(fill, dungeons, reservedTiles);
@@ -290,67 +285,27 @@ public sealed partial class DungeonJob : Job<(List<Dungeon>, DungeonData)>
case PrototypeDunGen prototypo: case PrototypeDunGen prototypo:
var groupConfig = _prototype.Index(prototypo.Proto); var groupConfig = _prototype.Index(prototypo.Proto);
position = (position + random.NextPolarVector2(groupConfig.MinOffset, groupConfig.MaxOffset)).Floored(); 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) switch (prototypo.InheritDungeons)
{ {
case DungeonInheritance.All: case DungeonInheritance.All:
inheritedDungeons = dungeons; dungeons.AddRange(await GetDungeons(position, groupConfig, groupConfig.Layers, reservedTiles, seed, random, existing: dungeons));
break; break;
case DungeonInheritance.Last: 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; break;
case DungeonInheritance.None: case DungeonInheritance.None:
dungeons.AddRange(await GetDungeons(position, groupConfig, groupConfig.Layers, reservedTiles, seed, random));
break; 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; break;
case ReplaceTileDunGen replace: case ReplaceTileDunGen replace:
await GenerateTileReplacementDunGen(replace, dungeons, reservedTiles, random); await GenerateTileReplacementDunGen(replace, dungeons, reservedTiles, random);
break; break;
case RoofDunGen roof:
await RoofGen(roof, dungeons, reservedTiles, random);
break;
case RoomEntranceDunGen rEntrance: case RoomEntranceDunGen rEntrance:
await PostGen(rEntrance, dungeons[^1], reservedTiles, random); await PostGen(rEntrance, dungeons[^1], reservedTiles, random);
break; 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: case SplineDungeonConnectorDunGen spline:
dungeons.Add(await PostGen(spline, dungeons, reservedTiles, random)); dungeons.Add(await PostGen(spline, dungeons, reservedTiles, random));
break; break;
@@ -365,6 +320,11 @@ public sealed partial class DungeonJob : Job<(List<Dungeon>, DungeonData)>
} }
} }
private void LogDataError(Type type)
{
_sawmill.Error($"Unable to find dungeon data keys for {type}");
}
[Pure] [Pure]
private bool ValidateResume() private bool ValidateResume()
{ {
@@ -386,19 +346,4 @@ public sealed partial class DungeonJob : Job<(List<Dungeon>, DungeonData)>
await SuspendIfOutOfTime(); 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;
}
} }

View File

@@ -113,7 +113,7 @@ public sealed partial class DungeonSystem
return roomRotation; return roomRotation;
} }
public DungeonData SpawnRoom( public void SpawnRoom(
EntityUid gridUid, EntityUid gridUid,
MapGridComponent grid, MapGridComponent grid,
Matrix3x2 roomTransform, Matrix3x2 roomTransform,
@@ -126,7 +126,6 @@ public sealed partial class DungeonSystem
var templateMapUid = _maps.GetMapOrInvalid(roomMap); var templateMapUid = _maps.GetMapOrInvalid(roomMap);
var templateGrid = Comp<MapGridComponent>(templateMapUid); var templateGrid = Comp<MapGridComponent>(templateMapUid);
var roomDimensions = room.Size; var roomDimensions = room.Size;
var data = new DungeonData();
var finalRoomRotation = roomTransform.Rotation(); var finalRoomRotation = roomTransform.Rotation();
@@ -155,7 +154,6 @@ public sealed partial class DungeonSystem
} }
_tiles.Add((rounded, tileRef.Tile)); _tiles.Add((rounded, tileRef.Tile));
data.Tiles[rounded] = tileRef.Tile;
if (clearExisting) if (clearExisting)
{ {
@@ -188,7 +186,6 @@ public sealed partial class DungeonSystem
// TODO: Copy the templated entity as is with serv // TODO: Copy the templated entity as is with serv
var ent = Spawn(protoId, new EntityCoordinates(gridUid, childPos)); var ent = Spawn(protoId, new EntityCoordinates(gridUid, childPos));
data.Entities.Add(ent, childPos.Floored());
var childXform = _xformQuery.GetComponent(ent); var childXform = _xformQuery.GetComponent(ent);
var anchored = templateXform.Anchored; var anchored = templateXform.Anchored;
@@ -259,18 +256,14 @@ public sealed partial class DungeonSystem
var result = _decals.TryAddDecal( var result = _decals.TryAddDecal(
decal.Id, decal.Id,
new EntityCoordinates(gridUid, position), new EntityCoordinates(gridUid, position),
out var did, out _,
decal.Color, decal.Color,
angle, angle,
decal.ZIndex, decal.ZIndex,
decal.Cleanable); decal.Cleanable);
data.Decals.Add(did, position);
DebugTools.Assert(result); DebugTools.Assert(result);
} }
} }
return data;
} }
} }

View File

@@ -199,8 +199,7 @@ public sealed partial class DungeonSystem : SharedDungeonSystem
MapGridComponent grid, MapGridComponent grid,
Vector2i position, Vector2i position,
int seed, int seed,
EntityCoordinates? coordinates = null, EntityCoordinates? coordinates = null)
HashSet<Vector2i>? reservedTiles = null)
{ {
var cancelToken = new CancellationTokenSource(); var cancelToken = new CancellationTokenSource();
var job = new DungeonJob.DungeonJob( var job = new DungeonJob.DungeonJob(
@@ -222,20 +221,18 @@ public sealed partial class DungeonSystem : SharedDungeonSystem
seed, seed,
position, position,
coordinates, coordinates,
cancelToken.Token, cancelToken.Token);
reservedTiles);
_dungeonJobs.Add(job, cancelToken); _dungeonJobs.Add(job, cancelToken);
_dungeonJobQueue.EnqueueJob(job); _dungeonJobQueue.EnqueueJob(job);
} }
public async Task<(List<Dungeon>, DungeonData)> GenerateDungeonAsync( public async Task<List<Dungeon>> GenerateDungeonAsync(
DungeonConfig gen, DungeonConfig gen,
EntityUid gridUid, EntityUid gridUid,
MapGridComponent grid, MapGridComponent grid,
Vector2i position, Vector2i position,
int seed, int seed)
HashSet<Vector2i>? reservedTiles = null)
{ {
var cancelToken = new CancellationTokenSource(); var cancelToken = new CancellationTokenSource();
var job = new DungeonJob.DungeonJob( var job = new DungeonJob.DungeonJob(
@@ -257,8 +254,7 @@ public sealed partial class DungeonSystem : SharedDungeonSystem
seed, seed,
position, position,
null, null,
cancelToken.Token, cancelToken.Token);
reservedTiles);
_dungeonJobs.Add(job, cancelToken); _dungeonJobs.Add(job, cancelToken);
_dungeonJobQueue.EnqueueJob(job); _dungeonJobQueue.EnqueueJob(job);
@@ -269,7 +265,7 @@ public sealed partial class DungeonSystem : SharedDungeonSystem
throw job.Exception; throw job.Exception;
} }
return job.Result; return job.Result!;
} }
public Angle GetDungeonRotation(int seed) public Angle GetDungeonRotation(int seed)

View File

@@ -1,17 +1,22 @@
using System.Collections;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Content.Server.Atmos;
using Content.Server.Atmos.Components; using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems; using Content.Server.Atmos.EntitySystems;
using Robust.Shared.CPUJob.JobQueues; using Robust.Shared.CPUJob.JobQueues;
using Content.Server.Ghost.Roles.Components; using Content.Server.Ghost.Roles.Components;
using Content.Server.Parallax;
using Content.Server.Procedural; using Content.Server.Procedural;
using Content.Server.Salvage.Expeditions; using Content.Server.Salvage.Expeditions;
using Content.Server.Salvage.Expeditions.Structure;
using Content.Shared.Atmos; using Content.Shared.Atmos;
using Content.Shared.Construction.EntitySystems; using Content.Shared.Construction.EntitySystems;
using Content.Shared.Dataset; using Content.Shared.Dataset;
using Content.Shared.Gravity; using Content.Shared.Gravity;
using Content.Shared.Parallax.Biomes;
using Content.Shared.Physics; using Content.Shared.Physics;
using Content.Shared.Procedural; using Content.Shared.Procedural;
using Content.Shared.Procedural.Loot; using Content.Shared.Procedural.Loot;
@@ -20,14 +25,15 @@ using Content.Shared.Salvage;
using Content.Shared.Salvage.Expeditions; using Content.Shared.Salvage.Expeditions;
using Content.Shared.Salvage.Expeditions.Modifiers; using Content.Shared.Salvage.Expeditions.Modifiers;
using Content.Shared.Shuttles.Components; using Content.Shared.Shuttles.Components;
using Content.Shared.Storage;
using Robust.Shared.Collections; using Robust.Shared.Collections;
using Robust.Shared.Map;
using Robust.Shared.Map.Components; using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Timing; using Robust.Shared.Timing;
using Robust.Shared.Utility; using Robust.Shared.Utility;
using Content.Server.Shuttles.Components; using Content.Server.Shuttles.Components;
using Content.Shared.Procedural.Components;
namespace Content.Server.Salvage; namespace Content.Server.Salvage;
@@ -114,13 +120,14 @@ public sealed class SpawnSalvageMissionJob : Job<bool>
.GetMission(difficultyProto, _missionParams.Seed); .GetMission(difficultyProto, _missionParams.Seed);
var missionBiome = _prototypeManager.Index<SalvageBiomeModPrototype>(mission.Biome); var missionBiome = _prototypeManager.Index<SalvageBiomeModPrototype>(mission.Biome);
BiomeComponent? biome = null;
if (missionBiome.BiomePrototype != null) if (missionBiome.BiomePrototype != null)
{ {
var biome = _entManager.AddComponent<BiomeComponent>(mapUid);
var biomeSystem = _entManager.System<BiomeSystem>(); var biomeSystem = _entManager.System<BiomeSystem>();
biomeSystem.SetTemplate(mapUid, biome, _prototypeManager.Index<BiomeTemplatePrototype>(missionBiome.BiomePrototype));
biome = biomeSystem.AddBiome(mapUid, missionBiome.BiomePrototype.Value, mission.Seed); biomeSystem.SetSeed(mapUid, biome, mission.Seed);
_entManager.Dirty(mapUid, biome);
// Gravity // Gravity
var gravity = _entManager.EnsureComponent<GravityComponent>(mapUid); var gravity = _entManager.EnsureComponent<GravityComponent>(mapUid);
@@ -165,7 +172,7 @@ public sealed class SpawnSalvageMissionJob : Job<bool>
dungeonOffset = dungeonRotation.RotateVec(dungeonOffset); dungeonOffset = dungeonRotation.RotateVec(dungeonOffset);
var dungeonMod = _prototypeManager.Index<SalvageDungeonModPrototype>(mission.Dungeon); var dungeonMod = _prototypeManager.Index<SalvageDungeonModPrototype>(mission.Dungeon);
var dungeonConfig = _prototypeManager.Index(dungeonMod.Proto); 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)); _missionParams.Seed));
var dungeon = dungeons.First(); var dungeon = dungeons.First();
@@ -176,20 +183,18 @@ public sealed class SpawnSalvageMissionJob : Job<bool>
return false; 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)
{
foreach (var tile in dungeon.AllTiles)
{
biome.ModifiedTiles.Add(tile);
}
biome.Enabled = true;
}
expedition.DungeonLocation = dungeonOffset; expedition.DungeonLocation = dungeonOffset;
List<Vector2i> reservedTiles = new();
foreach (var tile in _map.GetTilesIntersecting(mapUid, grid, new Circle(Vector2.Zero, landingPadRadius), false))
{
if (!_biome.TryGetBiomeTile(mapUid, grid, tile.GridIndices, out _))
continue;
reservedTiles.Add(tile.GridIndices);
}
var budgetEntries = new List<IBudgetEntry>(); var budgetEntries = new List<IBudgetEntry>();
/* /*
@@ -203,14 +208,13 @@ public sealed class SpawnSalvageMissionJob : Job<bool>
if (!lootProto.Guaranteed) if (!lootProto.Guaranteed)
continue; continue;
foreach (var rule in lootProto.LootRules) try
{ {
switch (rule) await SpawnDungeonLoot(lootProto, mapUid);
{
case BiomeLoot biomeLoot:
_biome.AddLayer(mapUid, $"{rule}", biomeLoot.Proto);
break;
} }
catch (Exception e)
{
_sawmill.Error($"Failed to spawn guaranteed loot {lootProto.ID}: {e}");
} }
} }
@@ -319,4 +323,32 @@ public sealed class SpawnSalvageMissionJob : Job<bool>
// oh noooooooooooo // 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;
}
}
}
} }

View File

@@ -6,7 +6,6 @@ using Content.Server.DeviceNetwork.Systems;
using Content.Server.GameTicking; using Content.Server.GameTicking;
using Content.Server.GameTicking.Events; using Content.Server.GameTicking.Events;
using Content.Server.Parallax; using Content.Server.Parallax;
using Content.Server.Procedural;
using Content.Server.Screens.Components; using Content.Server.Screens.Components;
using Content.Server.Shuttles.Components; using Content.Server.Shuttles.Components;
using Content.Server.Shuttles.Events; using Content.Server.Shuttles.Events;
@@ -23,6 +22,7 @@ using Content.Shared.DeviceNetwork.Components;
using Content.Shared.GameTicking; using Content.Shared.GameTicking;
using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Components;
using Content.Shared.Movement.Components; using Content.Shared.Movement.Components;
using Content.Shared.Parallax.Biomes;
using Content.Shared.Salvage; using Content.Shared.Salvage;
using Content.Shared.Shuttles.Components; using Content.Shared.Shuttles.Components;
using Content.Shared.Tiles; using Content.Shared.Tiles;
@@ -82,11 +82,11 @@ public sealed class ArrivalsSystem : EntitySystem
/// </summary> /// </summary>
private const float RoundStartFTLDuration = 10f; private const float RoundStartFTLDuration = 10f;
private readonly List<EntProtoId> _arrivalsBiomeOptions = new() private readonly List<ProtoId<BiomeTemplatePrototype>> _arrivalsBiomeOptions = new()
{ {
"BiomeGrasslands", "Grasslands",
"BiomeLowDesert", "LowDesert",
"BiomeSnow", "Snow",
}; };
public override void Initialize() public override void Initialize()

View File

@@ -1,14 +1,12 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using Content.Server.Decals;
using Content.Server.Shuttles.Components; using Content.Server.Shuttles.Components;
using Content.Server.Shuttles.Events; using Content.Server.Shuttles.Events;
using Content.Server.Station.Events; using Content.Server.Station.Events;
using Content.Shared.Body.Components; using Content.Shared.Body.Components;
using Content.Shared.CCVar; using Content.Shared.CCVar;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.Decals;
using Content.Shared.Ghost; using Content.Shared.Ghost;
using Content.Shared.Maps; using Content.Shared.Maps;
using Content.Shared.Parallax; using Content.Shared.Parallax;
@@ -958,7 +956,6 @@ public sealed partial class ShuttleSystem
var transform = _physics.GetRelativePhysicsTransform((uid, xform), xform.MapUid.Value); var transform = _physics.GetRelativePhysicsTransform((uid, xform), xform.MapUid.Value);
var aabbs = new List<Box2>(manager.Fixtures.Count); var aabbs = new List<Box2>(manager.Fixtures.Count);
var tileSet = new List<(Vector2i, Tile)>(); var tileSet = new List<(Vector2i, Tile)>();
TryComp(xform.MapUid.Value, out DecalGridComponent? decalGrid);
foreach (var fixture in manager.Fixtures.Values) foreach (var fixture in manager.Fixtures.Values)
{ {
@@ -972,15 +969,9 @@ public sealed partial class ShuttleSystem
aabb = aabb.Enlarged(0.2f); aabb = aabb.Enlarged(0.2f);
aabbs.Add(aabb); aabbs.Add(aabb);
if (decalGrid != null) // Handle clearing biome stuff as relevant.
{
foreach (var decal in _decals.GetDecalsIntersecting(xform.MapUid.Value, aabb))
{
_decals.RemoveDecal(xform.MapUid.Value, decal.Index, decalGrid);
}
}
tileSet.Clear(); tileSet.Clear();
_biomes.ReserveTiles(xform.MapUid.Value, aabb, tileSet);
_lookupEnts.Clear(); _lookupEnts.Clear();
_immuneEnts.Clear(); _immuneEnts.Clear();
// TODO: Ideally we'd query first BEFORE moving grid but needs adjustments above. // TODO: Ideally we'd query first BEFORE moving grid but needs adjustments above.

View File

@@ -1,7 +1,6 @@
using Content.Server.Administration.Logs; using Content.Server.Administration.Logs;
using Content.Server.Body.Systems; using Content.Server.Body.Systems;
using Content.Server.Buckle.Systems; using Content.Server.Buckle.Systems;
using Content.Server.Decals;
using Content.Server.Parallax; using Content.Server.Parallax;
using Content.Server.Procedural; using Content.Server.Procedural;
using Content.Server.Shuttles.Components; using Content.Server.Shuttles.Components;
@@ -42,12 +41,10 @@ public sealed partial class ShuttleSystem : SharedShuttleSystem
[Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IPrototypeManager _protoManager = default!; [Dependency] private readonly IPrototypeManager _protoManager = default!;
[Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ITileDefinitionManager _tileDefManager = default!;
[Dependency] private readonly BiomeSystem _biomes = default!; [Dependency] private readonly BiomeSystem _biomes = default!;
[Dependency] private readonly BodySystem _bobby = default!; [Dependency] private readonly BodySystem _bobby = default!;
[Dependency] private readonly BuckleSystem _buckle = default!; [Dependency] private readonly BuckleSystem _buckle = default!;
[Dependency] private readonly DamageableSystem _damageSys = default!; [Dependency] private readonly DamageableSystem _damageSys = default!;
[Dependency] private readonly DecalSystem _decals = default!;
[Dependency] private readonly DockingSystem _dockSystem = default!; [Dependency] private readonly DockingSystem _dockSystem = default!;
[Dependency] private readonly DungeonSystem _dungeon = default!; [Dependency] private readonly DungeonSystem _dungeon = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!; [Dependency] private readonly EntityLookupSystem _lookup = default!;

View File

@@ -1,4 +1,5 @@
using Content.Server.Station.Systems; using Content.Server.Station.Systems;
using Content.Shared.Parallax.Biomes;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
namespace Content.Server.Station.Components; namespace Content.Server.Station.Components;
@@ -10,7 +11,7 @@ namespace Content.Server.Station.Components;
public sealed partial class StationBiomeComponent : Component public sealed partial class StationBiomeComponent : Component
{ {
[DataField(required: true)] [DataField(required: true)]
public EntProtoId Biome = "BiomeGrasslands"; public ProtoId<BiomeTemplatePrototype> Biome = "Grasslands";
// If null, its random // If null, its random
[DataField] [DataField]

View File

@@ -1,5 +1,4 @@
using Content.Server.Parallax; using Content.Server.Parallax;
using Content.Server.Procedural;
using Content.Server.Station.Components; using Content.Server.Station.Components;
using Content.Server.Station.Events; using Content.Server.Station.Events;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;

View File

@@ -191,7 +191,7 @@ namespace Content.Server.Tabletop
if (!TryComp(uid, out ActorComponent? actor)) if (!TryComp(uid, out ActorComponent? actor))
{ {
RemComp<TabletopGamerComponent>(uid); RemComp<TabletopGamerComponent>(uid);
continue; return;
} }
if (actor.PlayerSession.Status != SessionStatus.InGame || !CanSeeTable(uid, gamer.Tabletop)) if (actor.PlayerSession.Status != SessionStatus.InGame || !CanSeeTable(uid, gamer.Tabletop))

View File

@@ -1,18 +0,0 @@
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);
}

View File

@@ -0,0 +1,87 @@
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
}

View File

@@ -0,0 +1,16 @@
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();
}

View File

@@ -0,0 +1,34 @@
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();
}

View File

@@ -0,0 +1,18 @@
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; }
}

View File

@@ -0,0 +1,27 @@
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();
}

View File

@@ -0,0 +1,27 @@
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;
}

View File

@@ -2,26 +2,20 @@ using Content.Shared.Maps;
using Robust.Shared.Noise; using Robust.Shared.Noise;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Serialization; 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] [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); [DataField] public FastNoiseLite Noise { get; private set; } = new(0);
/// <inheritdoc/>
[DataField] [DataField]
public float Threshold { get; private set; } = 0.5f; public float Threshold { get; private set; } = 0.5f;
/// <inheritdoc/>
[DataField] public bool Invert { get; private set; } = false; [DataField] public bool Invert { get; private set; } = false;
/// <summary> /// <summary>

View File

@@ -0,0 +1,22 @@
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; }
}

View File

@@ -0,0 +1,12 @@
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; }
}

View File

@@ -0,0 +1,53 @@
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;
}

View File

@@ -0,0 +1,22 @@
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; }
}

View File

@@ -0,0 +1,386 @@
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;
}
}

View File

@@ -1,86 +0,0 @@
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();
}

View File

@@ -1,9 +0,0 @@
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;

View File

@@ -1,7 +0,0 @@
namespace Content.Shared.Procedural.Distance;
public sealed partial class DunGenDistanceSquared : IDunGenDistance
{
[DataField]
public float BlendWeight { get; set; } = 0.50f;
}

View File

@@ -12,18 +12,11 @@ public partial class DungeonConfig
public List<IDunGenLayer> Layers = new(); public List<IDunGenLayer> Layers = new();
/// <summary> /// <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> /// </summary>
[DataField] [DataField]
public bool ReserveTiles; 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> /// <summary>
/// Minimum times to run the config. /// Minimum times to run the config.
/// </summary> /// </summary>

View File

@@ -1,41 +0,0 @@
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;
}
}
}

View File

@@ -1,24 +0,0 @@
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;
}

View File

@@ -10,10 +10,4 @@ public sealed partial class ExteriorDunGen : IDunGenLayer
{ {
[DataField(required: true)] [DataField(required: true)]
public ProtoId<DungeonConfigPrototype> Proto; public ProtoId<DungeonConfigPrototype> Proto;
/// <summary>
/// Minimum and maximum penetration.
/// </summary>
[DataField]
public Vector2i Penetration = new Vector2i(5, 15);
} }

View File

@@ -14,12 +14,6 @@ public sealed partial class PrototypeDunGen : IDunGenLayer
[DataField] [DataField]
public DungeonInheritance InheritDungeons = DungeonInheritance.None; 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)] [DataField(required: true)]
public ProtoId<DungeonConfigPrototype> Proto; public ProtoId<DungeonConfigPrototype> Proto;
} }
@@ -41,16 +35,3 @@ public enum DungeonInheritance : byte
/// </summary> /// </summary>
All, All,
} }
public enum ReservedInheritance : byte
{
/// <summary>
/// Don't inherit any reserved tiles.
/// </summary>
None,
/// <summary>
/// Inherit reserved tiles,
/// </summary>
All,
}

View File

@@ -1,7 +1,4 @@
using System.Numerics;
using Content.Shared.Maps; using Content.Shared.Maps;
using Content.Shared.Procedural.Distance;
using Robust.Shared.Noise;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
namespace Content.Shared.Procedural.DungeonLayers; namespace Content.Shared.Procedural.DungeonLayers;
@@ -9,6 +6,10 @@ namespace Content.Shared.Procedural.DungeonLayers;
/// <summary> /// <summary>
/// Fills unreserved tiles with the specified entity prototype. /// Fills unreserved tiles with the specified entity prototype.
/// </summary> /// </summary>
/// <remarks>
/// DungeonData keys are:
/// - Fill
/// </remarks>
public sealed partial class FillGridDunGen : IDunGenLayer public sealed partial class FillGridDunGen : IDunGenLayer
{ {
/// <summary> /// <summary>
@@ -19,29 +20,4 @@ public sealed partial class FillGridDunGen : IDunGenLayer
[DataField(required: true)] [DataField(required: true)]
public EntProtoId Entity; 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
} }

View File

@@ -1,8 +1,10 @@
using Content.Shared.EntityTable; using Content.Shared.EntityTable;
using Content.Shared.Storage;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
namespace Content.Shared.Procedural.DungeonLayers; namespace Content.Shared.Procedural.DungeonLayers;
/// <summary> /// <summary>
/// Spawns mobs inside of the dungeon randomly. /// Spawns mobs inside of the dungeon randomly.
/// </summary> /// </summary>

View File

@@ -1,15 +0,0 @@
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;
}

View File

@@ -1,36 +0,0 @@
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();
}

View File

@@ -1,31 +0,0 @@
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();
}

View File

@@ -1,12 +0,0 @@
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;
}

View File

@@ -0,0 +1,15 @@
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();
}

View File

@@ -0,0 +1,14 @@
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;
}

View File

@@ -0,0 +1,21 @@
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;
}

View File

@@ -0,0 +1,19 @@
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;
}

View File

@@ -1,6 +1,5 @@
using Content.Shared.EntityTable; using Content.Shared.EntityTable;
using Content.Shared.Maps; using Content.Shared.Maps;
using Content.Shared.Storage;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
namespace Content.Shared.Procedural.PostGeneration; namespace Content.Shared.Procedural.PostGeneration;

View File

@@ -1,5 +1,6 @@
using Content.Shared.EntityTable; using Content.Shared.EntityTable;
using Content.Shared.Maps; using Content.Shared.Maps;
using Content.Shared.Storage;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
namespace Content.Shared.Procedural.PostGeneration; namespace Content.Shared.Procedural.PostGeneration;

View File

@@ -1,5 +1,6 @@
using Content.Shared.EntityTable; using Content.Shared.EntityTable;
using Content.Shared.Maps; using Content.Shared.Maps;
using Content.Shared.Storage;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
namespace Content.Shared.Procedural.PostGeneration; namespace Content.Shared.Procedural.PostGeneration;

View File

@@ -1,7 +1,7 @@
using Content.Shared.Maps; using Content.Shared.Maps;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
namespace Content.Shared.Procedural.DungeonLayers; namespace Content.Shared.Procedural.PostGeneration;
/// <summary> /// <summary>
/// Connects dungeons via points that get subdivided. /// Connects dungeons via points that get subdivided.
@@ -18,11 +18,11 @@ public sealed partial class SplineDungeonConnectorDunGen : IDunGenLayer
/// Will divide the distance between the start and end points so that no subdivision is more than these metres away. /// Will divide the distance between the start and end points so that no subdivision is more than these metres away.
/// </summary> /// </summary>
[DataField] [DataField]
public int DivisionDistance = 20; public int DivisionDistance = 10;
/// <summary> /// <summary>
/// How much each subdivision can vary from the middle. /// How much each subdivision can vary from the middle.
/// </summary> /// </summary>
[DataField] [DataField]
public float VarianceMax = 0.15f; public float VarianceMax = 0.35f;
} }

Some files were not shown because too many files have changed in this diff Show More