diff --git a/src/data/kotlin/ru/dbotthepony/mc/otm/datagen/OreGen.kt b/src/data/kotlin/ru/dbotthepony/mc/otm/datagen/WorldGen.kt
similarity index 77%
rename from src/data/kotlin/ru/dbotthepony/mc/otm/datagen/OreGen.kt
rename to src/data/kotlin/ru/dbotthepony/mc/otm/datagen/WorldGen.kt
index 42c075079..4bb408e4a 100644
--- a/src/data/kotlin/ru/dbotthepony/mc/otm/datagen/OreGen.kt
+++ b/src/data/kotlin/ru/dbotthepony/mc/otm/datagen/WorldGen.kt
@@ -19,10 +19,14 @@ import net.minecraft.world.level.levelgen.placement.PlacedFeature
 import net.minecraft.world.level.levelgen.structure.templatesystem.TagMatchTest
 import net.neoforged.neoforge.common.world.BiomeModifier
 import net.neoforged.neoforge.registries.NeoForgeRegistries
+import ru.dbotthepony.mc.otm.core.math.Decimal
 import ru.dbotthepony.mc.otm.registry.MBlocks
+import ru.dbotthepony.mc.otm.registry.MWorldGenFeatures
+import ru.dbotthepony.mc.otm.worldgen.feature.BlackHolePlacerConfiguration
 
 private object ConfiguredFeatures {
 	val TRITANIUM_ORE = key("tritanium_ore")
+	val BLACK_HOLE = key("black_hole")
 
 	private fun key(name: String): ResourceKey<ConfiguredFeature<*, *>> {
 		return ResourceKey.create(Registries.CONFIGURED_FEATURE, modLocation(name))
@@ -39,11 +43,14 @@ fun registerConfiguredFeatures(context: BootstrapContext<ConfiguredFeature<*, *>
 	)
 
 	context.register(ConfiguredFeatures.TRITANIUM_ORE, ConfiguredFeature(Feature.ORE, OreConfiguration(target, 9)))
+	context.register(ConfiguredFeatures.BLACK_HOLE, ConfiguredFeature(MWorldGenFeatures.BLACK_HOLE_PLACER,
+		BlackHolePlacerConfiguration(0.001f, Decimal(125_000), Decimal(500_000))))
 }
 
 private object PlacedFeatures {
 	val NORMAL_TRITANIUM = key("normal_tritanium")
 	val DEEP_TRITANIUM = key("deep_tritanium")
+	val BLACK_HOLE = key("black_hole")
 
 	private fun key(name: String): ResourceKey<PlacedFeature> {
 		return ResourceKey.create(Registries.PLACED_FEATURE, modLocation(name))
@@ -71,10 +78,21 @@ fun registerPlacedFeatures(context: BootstrapContext<PlacedFeature>) {
 			HeightRangePlacement.uniform(VerticalAnchor.aboveBottom(8), VerticalAnchor.absolute(0))
 		)
 	))
+
+	val blackHole = configured.getOrThrow(ConfiguredFeatures.BLACK_HOLE)
+
+	context.register(PlacedFeatures.BLACK_HOLE, PlacedFeature(
+		blackHole,
+		listOf(
+			CountPlacement.of(1),
+			HeightRangePlacement.uniform(VerticalAnchor.absolute(64), VerticalAnchor.absolute(128))
+		)
+	))
 }
 
 private object BiomeModifiers {
 	val TRITANIUM_ORE = key("tritanium_ore")
+	val BLACK_HOLE = key("black_hole")
 
 	private fun key(name: String): ResourceKey<BiomeModifier> {
 		return ResourceKey.create(NeoForgeRegistries.Keys.BIOME_MODIFIERS, modLocation(name))
@@ -96,4 +114,15 @@ fun registerBiomeModifiers(context: BootstrapContext<BiomeModifier>) {
 			GenerationStep.Decoration.UNDERGROUND_ORES
 		)
 	)
+
+	context.register(
+		BiomeModifiers.BLACK_HOLE,
+		net.neoforged.neoforge.common.world.BiomeModifiers.AddFeaturesBiomeModifier(
+			biomes.getOrThrow(BiomeTags.IS_OVERWORLD),
+			HolderSet.direct(
+				placed.getOrThrow(PlacedFeatures.BLACK_HOLE)
+			),
+			GenerationStep.Decoration.SURFACE_STRUCTURES
+		)
+	)
 }
diff --git a/src/main/java/ru/dbotthepony/mc/otm/OverdriveThatMatters.java b/src/main/java/ru/dbotthepony/mc/otm/OverdriveThatMatters.java
index 87ac9a543..c4707e8c6 100644
--- a/src/main/java/ru/dbotthepony/mc/otm/OverdriveThatMatters.java
+++ b/src/main/java/ru/dbotthepony/mc/otm/OverdriveThatMatters.java
@@ -127,6 +127,7 @@ public final class OverdriveThatMatters {
 		MArmorMaterials.INSTANCE.register(bus);
 		MCriteriaTriggers.INSTANCE.register(bus);
 		MStats.INSTANCE.register(bus);
+		MWorldGenFeatures.INSTANCE.register(bus);
 		CommandArgumentTypes.INSTANCE.register(bus);
 
 		StorageStack.Companion.register(bus);
diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/registry/MWorldGenFeatures.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/registry/MWorldGenFeatures.kt
new file mode 100644
index 000000000..e6d47f33d
--- /dev/null
+++ b/src/main/kotlin/ru/dbotthepony/mc/otm/registry/MWorldGenFeatures.kt
@@ -0,0 +1,19 @@
+package ru.dbotthepony.mc.otm.registry
+
+import net.minecraft.core.registries.BuiltInRegistries
+import net.minecraft.world.level.levelgen.feature.Feature
+import net.neoforged.bus.api.IEventBus
+import ru.dbotthepony.mc.otm.worldgen.feature.BlackHolePlacerConfiguration
+import ru.dbotthepony.mc.otm.worldgen.feature.BlackHolePlacerFeature
+
+object MWorldGenFeatures {
+	private val registry = MDeferredRegister(BuiltInRegistries.FEATURE)
+
+	fun register(bus: IEventBus) {
+		registry.register(bus)
+	}
+
+	val BLACK_HOLE_PLACER: Feature<BlackHolePlacerConfiguration> by registry.register("black_hole_placer") {
+		BlackHolePlacerFeature(BlackHolePlacerConfiguration.CODEC)
+	}
+}
diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/worldgen/feature/BlackHolePlacer.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/worldgen/feature/BlackHolePlacer.kt
new file mode 100644
index 000000000..099ecf94a
--- /dev/null
+++ b/src/main/kotlin/ru/dbotthepony/mc/otm/worldgen/feature/BlackHolePlacer.kt
@@ -0,0 +1,49 @@
+package ru.dbotthepony.mc.otm.worldgen.feature
+
+import com.mojang.serialization.Codec
+import com.mojang.serialization.codecs.RecordCodecBuilder
+import net.minecraft.world.level.levelgen.feature.Feature
+import net.minecraft.world.level.levelgen.feature.FeaturePlaceContext
+import net.minecraft.world.level.levelgen.feature.configurations.FeatureConfiguration
+import ru.dbotthepony.mc.otm.block.entity.blackhole.BlackHoleBlockEntity
+import ru.dbotthepony.mc.otm.core.math.Decimal
+import ru.dbotthepony.mc.otm.core.math.nextDecimal
+import ru.dbotthepony.mc.otm.data.DecimalCodec
+import ru.dbotthepony.mc.otm.registry.MBlocks
+
+class BlackHolePlacerConfiguration(val chance: Float, val minMatter: Decimal, val maxMatter: Decimal) : FeatureConfiguration {
+	companion object {
+		val CODEC: Codec<BlackHolePlacerConfiguration> = RecordCodecBuilder.create {
+			t -> t.group(
+				Codec.floatRange(0.0F, 1.0F).fieldOf("chance").forGetter(BlackHolePlacerConfiguration::chance),
+				DecimalCodec.fieldOf("matter_min").forGetter(BlackHolePlacerConfiguration::minMatter),
+				DecimalCodec.fieldOf("matter_max").forGetter(BlackHolePlacerConfiguration::maxMatter)
+			).apply(t, ::BlackHolePlacerConfiguration)
+		}
+	}
+}
+
+class BlackHolePlacerFeature(val codec: Codec<BlackHolePlacerConfiguration>) : Feature<BlackHolePlacerConfiguration>(codec) {
+	override fun place(context: FeaturePlaceContext<BlackHolePlacerConfiguration?>): Boolean {
+		val random = context.random()
+		val level = context.level()
+		val pos = context.origin()
+
+		val config = context.config()
+		if (config == null) return false
+
+		if (level.getBlockState(pos).isAir && random.nextDouble() < config.chance.toDouble()) {
+			if (level.setBlock(pos, MBlocks.BLACK_HOLE.defaultBlockState(), 2)) {
+				val entity = level.getBlockEntity(pos)
+
+				if (entity is BlackHoleBlockEntity) {
+					entity.mass = random.nextDecimal(config.minMatter, config.maxMatter)
+				}
+
+				return true
+			}
+		}
+
+		return false
+	}
+}