diff --git a/Content.Server/Parallax/BiomeSystem.cs b/Content.Server/Parallax/BiomeSystem.cs index 35bfd7b8ff..2fc9a368cb 100644 --- a/Content.Server/Parallax/BiomeSystem.cs +++ b/Content.Server/Parallax/BiomeSystem.cs @@ -5,9 +5,9 @@ using Content.Shared.Decals; using Content.Shared.Parallax.Biomes; using Content.Shared.Parallax.Biomes.Layers; using Content.Shared.Parallax.Biomes.Markers; -using Content.Shared.Parallax.Biomes.Points; using Robust.Server.Player; using Robust.Shared; +using Robust.Shared.Collections; using Robust.Shared.Configuration; using Robust.Shared.Console; using Robust.Shared.Map; @@ -326,6 +326,9 @@ public sealed partial class BiomeSystem : SharedBiomeSystem { var markers = _markerChunks[component]; var loadedMarkers = component.LoadedMarkers; + var spawnSet = new HashSet(); + var spawns = new List(); + var frontier = new Queue(); foreach (var (layer, chunks) in markers) { @@ -334,45 +337,112 @@ public sealed partial class BiomeSystem : SharedBiomeSystem if (loadedMarkers.TryGetValue(layer, out var mobChunks) && mobChunks.Contains(chunk)) continue; + spawns.Clear(); + spawnSet.Clear(); var layerProto = _proto.Index(layer); var buffer = layerProto.Radius / 2f; mobChunks ??= new HashSet(); mobChunks.Add(chunk); loadedMarkers[layer] = mobChunks; - var rand = new Random(noise.GetSeed() + chunk.X * 8 + chunk.Y); + var rand = new Random(noise.GetSeed() + chunk.X * 8 + chunk.Y + layerProto.GetHashCode()); + + // We treat a null entity mask as requiring nothing else on the tile + var lower = (int) Math.Floor(buffer); + var upper = (int) Math.Ceiling(layerProto.Size - buffer); + + // TODO: Okay this is inefficient as FUCK + // I think the ideal is pick a random tile then BFS outwards from it probably ig + // It will bias edge tiles significantly more but will make the CPU cry less. + for (var x = lower; x <= upper; x++) + { + for (var y = lower; y <= upper; y++) + { + var index = new Vector2i(x + chunk.X, y + chunk.Y); + TryGetEntity(index, component.Layers, component.Noise, grid, out var proto); + + if (proto != layerProto.EntityMask) + { + continue; + } + + spawns.Add(index); + spawnSet.Add(index); + } + } // Load NOW - // TODO: Need poisson but crashes whenever I use moony's due to inputs or smth + // TODO: Need poisson but crashes whenever I use moony's due to inputs or smth idk var count = (int) ((layerProto.Size - buffer) * (layerProto.Size - buffer) / (layerProto.Radius * layerProto.Radius)); + count = Math.Min(count, layerProto.MaxCount); for (var i = 0; i < count; i++) { - for (var j = 0; j < 5; j++) + if (spawns.Count == 0) + break; + + var index = rand.Next(spawns.Count); + var point = spawns[index]; + spawns.RemoveSwap(index); + + // Point was potentially used in BFS search below but we hadn't updated the list yet. + if (!spawnSet.Remove(point)) { - var point = new Vector2( - chunk.X + buffer + rand.NextFloat() * (layerProto.Size - buffer), - chunk.Y + buffer + rand.NextFloat() * (layerProto.Size - buffer)); + i--; + continue; + } - var coords = new EntityCoordinates(gridUid, point); - var tile = grid.LocalToTile(coords); + // BFS search + frontier.Enqueue(point); + var groupCount = layerProto.GroupCount; - // Blocked spawn, try again. - if (grid.GetAnchoredEntitiesEnumerator(tile).MoveNext(out _)) + while (frontier.TryDequeue(out var node) && groupCount > 0) + { + var enumerator = grid.GetAnchoredEntitiesEnumerator(node); + + if (enumerator.MoveNext(out _)) continue; - for (var k = 0; k < layerProto.GroupCount; k++) + // Need to ensure the tile under it has loaded for anchoring. + if (TryGetBiomeTile(node, component.Layers, component.Noise, grid, out var tile)) { - // If it is a ghost role then purge it - // TODO: This is *kind* of a bandaid but natural mobs spawns needs a lot more work. - // Ideally we'd just have ghost role and non-ghost role variants for some stuff. - var uid = EntityManager.CreateEntityUninitialized(layerProto.Prototype, new EntityCoordinates(gridUid, point)); - RemComp(uid); - RemComp(uid); - EntityManager.InitializeAndStartEntity(uid); + grid.SetTile(node, tile.Value); } - break; + // If it is a ghost role then purge it + // TODO: This is *kind* of a bandaid but natural mobs spawns needs a lot more work. + // Ideally we'd just have ghost role and non-ghost role variants for some stuff. + var uid = EntityManager.CreateEntityUninitialized(layerProto.Prototype, new EntityCoordinates(gridUid, node)); + RemComp(uid); + RemComp(uid); + EntityManager.InitializeAndStartEntity(uid); + groupCount--; + + for (var x = -1; x <= 1; x++) + { + for (var y = -1; y <= 1; y++) + { + if (x != 0 && y != 0) + continue; + + var neighbor = new Vector2i(x + node.X, y + node.Y); + + if (!spawnSet.Contains(neighbor)) + continue; + + frontier.Enqueue(neighbor); + // Rather than doing some uggo remove check on the list we'll defer it until later + spawnSet.Remove(neighbor); + } + } } + + // Add the unused nodes back in + foreach (var node in frontier) + { + spawnSet.Add(node); + } + + frontier.Clear(); } } } diff --git a/Content.Server/Salvage/SpawnSalvageMissionJob.cs b/Content.Server/Salvage/SpawnSalvageMissionJob.cs index 2d27927b45..cbc4fb2b04 100644 --- a/Content.Server/Salvage/SpawnSalvageMissionJob.cs +++ b/Content.Server/Salvage/SpawnSalvageMissionJob.cs @@ -13,6 +13,7 @@ using Content.Shared.Atmos; using Content.Shared.Dataset; using Content.Shared.Gravity; using Content.Shared.Parallax.Biomes; +using Content.Shared.Parallax.Biomes.Markers; using Content.Shared.Procedural; using Content.Shared.Procedural.Loot; using Content.Shared.Random; @@ -220,10 +221,20 @@ public sealed class SpawnSalvageMissionJob : Job switch (rule) { - case BiomeTemplateLoot biomeLoot: - if (_entManager.TryGetComponent(gridUid, out var biome)) + case BiomeMarkerLoot biomeLoot: { - _biome.AddTemplate(biome, "Loot", _prototypeManager.Index(biomeLoot.Prototype), i); + if (_entManager.TryGetComponent(gridUid, out var biome)) + { + _biome.AddMarkerLayer(biome, biomeLoot.Prototype); + } + } + break; + case BiomeTemplateLoot biomeLoot: + { + if (_entManager.TryGetComponent(gridUid, out var biome)) + { + _biome.AddTemplate(biome, "Loot", _prototypeManager.Index(biomeLoot.Prototype), i); + } } break; // Spawns a cluster (like an ore vein) nearby. diff --git a/Content.Shared/Parallax/Biomes/Markers/BiomeMarkerLayerPrototype.cs b/Content.Shared/Parallax/Biomes/Markers/BiomeMarkerLayerPrototype.cs index cff75ad917..4ac68364ba 100644 --- a/Content.Shared/Parallax/Biomes/Markers/BiomeMarkerLayerPrototype.cs +++ b/Content.Shared/Parallax/Biomes/Markers/BiomeMarkerLayerPrototype.cs @@ -1,20 +1,36 @@ -using Content.Shared.Parallax.Biomes.Points; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; namespace Content.Shared.Parallax.Biomes.Markers; +/// +/// Spawns entities inside of the specified area with the minimum specified radius. +/// [Prototype("biomeMarkerLayer")] public sealed class BiomeMarkerLayerPrototype : IBiomeMarkerLayer { [IdDataField] public string ID { get; } = default!; [DataField("proto", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))] - public string Prototype = string.Empty; + public string Prototype { get; } = string.Empty; - /// + /// + /// Checks for the relevant entity for the tile before spawning. Useful for substituting walls with ore veins for example. + /// + [DataField("entityMask", customTypeSerializer:typeof(PrototypeIdSerializer))] + public string? EntityMask { get; } + + /// + /// Minimum radius between 2 points + /// [DataField("radius")] - public float Radius { get; } = 32f; + public float Radius = 32f; + + /// + /// Maximum amount of group spawns + /// + [DataField("maxCount")] + public int MaxCount = int.MaxValue; /// /// How many mobs to spawn in one group. diff --git a/Content.Shared/Parallax/Biomes/Markers/IBiomeMarkerLayer.cs b/Content.Shared/Parallax/Biomes/Markers/IBiomeMarkerLayer.cs index 7c47cf9a2f..f92aa68811 100644 --- a/Content.Shared/Parallax/Biomes/Markers/IBiomeMarkerLayer.cs +++ b/Content.Shared/Parallax/Biomes/Markers/IBiomeMarkerLayer.cs @@ -1,6 +1,6 @@ using Robust.Shared.Prototypes; -namespace Content.Shared.Parallax.Biomes.Points; +namespace Content.Shared.Parallax.Biomes.Markers; /// /// Specifies one-off marker points to be used. This could be for dungeon markers, mob markers, etc. @@ -9,14 +9,14 @@ namespace Content.Shared.Parallax.Biomes.Points; public interface IBiomeMarkerLayer : IPrototype { /// - /// Minimum radius between 2 points + /// Biome template to use as a mask for this layer. /// - [DataField("radius")] - public float Radius { get; } + public string? EntityMask { get; } + + public string Prototype { get; } /// /// How large the pre-generated points area is. /// - [DataField("size")] public int Size { get; } } diff --git a/Content.Shared/Procedural/Loot/BiomeMarkerLoot.cs b/Content.Shared/Procedural/Loot/BiomeMarkerLoot.cs new file mode 100644 index 0000000000..9ab51733f2 --- /dev/null +++ b/Content.Shared/Procedural/Loot/BiomeMarkerLoot.cs @@ -0,0 +1,13 @@ +using Content.Shared.Parallax.Biomes.Markers; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Shared.Procedural.Loot; + +/// +/// Adds a biome marker layer for dungeon loot. +/// +public sealed class BiomeMarkerLoot : IDungeonLoot +{ + [DataField("proto", required: true, customTypeSerializer:typeof(PrototypeIdSerializer))] + public string Prototype = string.Empty; +} diff --git a/Content.Shared/Weapons/Reflect/SharedReflectSystem.cs b/Content.Shared/Weapons/Reflect/SharedReflectSystem.cs index baca795bac..7ab56ab8aa 100644 --- a/Content.Shared/Weapons/Reflect/SharedReflectSystem.cs +++ b/Content.Shared/Weapons/Reflect/SharedReflectSystem.cs @@ -3,12 +3,8 @@ using Content.Shared.Audio; using Content.Shared.Hands.Components; using Robust.Shared.GameStates; using Content.Shared.Weapons.Ranged.Events; -using System.Diagnostics.CodeAnalysis; using Robust.Shared.Physics.Components; using Content.Shared.Popups; -using Content.Shared.Projectiles; -using Content.Shared.Weapons.Ranged.Events; -using Robust.Shared.GameStates; using Robust.Shared.Physics.Systems; using Robust.Shared.Random; diff --git a/Resources/Prototypes/Procedural/biome_ore_templates.yml b/Resources/Prototypes/Procedural/biome_ore_templates.yml index 536b97e997..37686eef57 100644 --- a/Resources/Prototypes/Procedural/biome_ore_templates.yml +++ b/Resources/Prototypes/Procedural/biome_ore_templates.yml @@ -1,108 +1,54 @@ -# Allowed -#allowedTiles: -#- FloorPlanetGrass -#- FloorPlanetDirt -#- FloorSnow -#- FloorBasalt -#- FloorAsteroidSand - -- type: biomeTemplate +# Low value +- type: biomeMarkerLayer id: OreTin - layers: - - !type:BiomeEntityLayer - threshold: 0.90 - allowedTiles: - - FloorPlanetGrass - - FloorPlanetDirt - - FloorSnow - - FloorBasalt - - FloorAsteroidSand - noise: - seed: 100 - noiseType: OpenSimplex2 - frequency: 0.04 - fractalType: None - entities: - - WallRockTin + proto: WallRockTin + entityMask: WallRock + maxCount: 5 + groupCount: 10 + radius: 4 # Medium value # Gold -- type: biomeTemplate +- type: biomeMarkerLayer id: OreGold - layers: - - !type:BiomeEntityLayer - threshold: 0.95 - allowedTiles: - - FloorPlanetGrass - - FloorPlanetDirt - - FloorSnow - - FloorBasalt - - FloorAsteroidSand - noise: - seed: 100 - noiseType: OpenSimplex2 - frequency: 0.04 - fractalType: None - entities: - - WallRockGold + proto: WallRockGold + entityMask: WallRock + maxCount: 5 + groupCount: 5 + radius: 4 # Silver -- type: biomeTemplate +- type: biomeMarkerLayer id: OreSilver - layers: - - !type:BiomeEntityLayer - threshold: 0.95 - allowedTiles: - - FloorPlanetGrass - - FloorPlanetDirt - - FloorSnow - - FloorBasalt - - FloorAsteroidSand - noise: - seed: 100 - noiseType: OpenSimplex2 - frequency: 0.05 - fractalType: None - entities: - - WallRockSilver + proto: WallRockSilver + entityMask: WallRock + maxCount: 5 + groupCount: 5 + radius: 4 # High value # Plasma -- type: biomeTemplate +- type: biomeMarkerLayer id: OrePlasma - layers: - - !type:BiomeEntityLayer - threshold: 0.99 - allowedTiles: - - FloorPlanetGrass - - FloorPlanetDirt - - FloorSnow - - FloorBasalt - - FloorAsteroidSand - noise: - seed: 100 - noiseType: OpenSimplex2 - frequency: 0.04 - fractalType: None - entities: - - WallRockPlasma + proto: WallRockPlasma + entityMask: WallRock + maxCount: 2 + groupCount: 5 + radius: 4 # Uranium -- type: biomeTemplate +- type: biomeMarkerLayer id: OreUranium - layers: - - !type:BiomeEntityLayer - threshold: 0.99 - allowedTiles: - - FloorPlanetGrass - - FloorPlanetDirt - - FloorSnow - - FloorBasalt - - FloorAsteroidSand - noise: - seed: 100 - noiseType: OpenSimplex2 - frequency: 0.04 - fractalType: None - entities: - - WallRockUranium + proto: WallRockUranium + entityMask: WallRock + maxCount: 2 + groupCount: 5 + radius: 4 + +- type: biomeMarkerLayer + id: OreBananium + proto: WallRockBananium + entityMask: WallRock + maxCount: 2 + groupCount: 5 + radius: 4 diff --git a/Resources/Prototypes/Procedural/salvage_loot.yml b/Resources/Prototypes/Procedural/salvage_loot.yml index 25b905a4b0..c02177c617 100644 --- a/Resources/Prototypes/Procedural/salvage_loot.yml +++ b/Resources/Prototypes/Procedural/salvage_loot.yml @@ -56,7 +56,7 @@ id: OreTin desc: Veins of steel loots: - - !type:BiomeTemplateLoot + - !type:BiomeMarkerLoot proto: OreTin # - Medium value @@ -64,14 +64,14 @@ id: OreGold desc: Veins of gold ore loots: - - !type:BiomeTemplateLoot + - !type:BiomeMarkerLoot proto: OreGold - type: salvageLoot id: OreSilver desc: Veins of silver ore loots: - - !type:BiomeTemplateLoot + - !type:BiomeMarkerLoot proto: OreSilver # - High value @@ -79,12 +79,12 @@ id: OrePlasma desc: Veins of plasma ore loots: - - !type:BiomeTemplateLoot + - !type:BiomeMarkerLoot proto: OrePlasma - type: salvageLoot id: OreUranium desc: Veins of uranium ore loots: - - !type:BiomeTemplateLoot + - !type:BiomeMarkerLoot proto: OreUranium diff --git a/Resources/Prototypes/ore.yml b/Resources/Prototypes/ore.yml index 8e6d90bfb4..0aa111b9f0 100644 --- a/Resources/Prototypes/ore.yml +++ b/Resources/Prototypes/ore.yml @@ -41,7 +41,7 @@ id: OreBananium oreEntity: BananiumOre1 minOreYield: 1 - maxOreYield: 2 + maxOreYield: 3 - type: weightedRandom id: RandomOreDistributionStandard