From 692316fa401bf76212da734ff631cf1d259e6410 Mon Sep 17 00:00:00 2001 From: Danielle Voznyy Date: Sat, 30 Mar 2024 19:00:02 -0400 Subject: [PATCH] feat(spawning): Spawn categories feat(spawning): Rework spawn system to iterate many chunks per tick, only checking one block instead of slice to be more similar to vanilla rollback(spawning): Remove checks for bounding box space until performance issues resolved --- .../living/SetRemoveWhenFarAway.kt | 13 +- .../mobzy/spawning/GlobalSpawnInfo.kt | 6 +- .../mobzy/spawning/MobzySpawnFeature.kt | 4 +- .../mobzy/spawning/PlayerGroups.kt | 38 ----- .../mobzy/spawning/PlayerSpawnInfo.kt | 24 +++ .../mineinabyss/mobzy/spawning/SpawnConfig.kt | 34 ++--- .../mobzy/spawning/SpawnDefinition.kt | 111 ++++++++++---- .../mobzy/spawning/SpawnRegistry.kt | 65 +++++--- .../mineinabyss/mobzy/spawning/SpawnTask.kt | 144 +++++++++--------- .../mobzy/spawning/SpawnableChunks.kt | 23 +++ .../mobzy/spawning/WorldGuardSpawnFlags.kt | 7 - .../conditions/BlockCompositionCondition.kt | 12 +- .../conditions/LocalGroupConditions.kt | 6 +- .../conditions/MobCapHasSpaceCondition.kt | 36 ++--- .../conditions/MobSuffocateCondition.kt | 29 ++++ .../spawning/conditions/SpawnGapCondition.kt | 26 ++-- .../mobzy/spawning/event/MobzySpawnEvent.kt | 13 +- .../mobzy/spawning/event/SpawnStrip.kt | 143 +++++------------ .../com/mineinabyss/mobzy/DebugCommands.kt | 26 ---- .../mobzy/spawning/PlayerGroupsTest.kt | 50 ------ .../spawning/vertical/VerticalSpawnTest.kt | 59 ------- 21 files changed, 377 insertions(+), 492 deletions(-) create mode 100644 mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/PlayerSpawnInfo.kt create mode 100644 mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/SpawnableChunks.kt create mode 100644 mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/conditions/MobSuffocateCondition.kt delete mode 100644 src/test/kotlin/com/mineinabyss/mobzy/spawning/PlayerGroupsTest.kt delete mode 100644 src/test/kotlin/com/mineinabyss/mobzy/spawning/vertical/VerticalSpawnTest.kt diff --git a/mobzy-features/src/main/kotlin/com/mineinabyss/mobzy/features/initializers/living/SetRemoveWhenFarAway.kt b/mobzy-features/src/main/kotlin/com/mineinabyss/mobzy/features/initializers/living/SetRemoveWhenFarAway.kt index b325cb126..3f0c97d5c 100644 --- a/mobzy-features/src/main/kotlin/com/mineinabyss/mobzy/features/initializers/living/SetRemoveWhenFarAway.kt +++ b/mobzy-features/src/main/kotlin/com/mineinabyss/mobzy/features/initializers/living/SetRemoveWhenFarAway.kt @@ -5,19 +5,26 @@ import com.mineinabyss.geary.datatypes.ComponentDefinition import com.mineinabyss.geary.modules.GearyModule import com.mineinabyss.geary.papermc.bridge.events.EventHelpers import com.mineinabyss.geary.papermc.bridge.events.entities.OnSpawn +import com.mineinabyss.geary.serialization.serializers.InnerSerializer import com.mineinabyss.geary.systems.builders.listener import com.mineinabyss.geary.systems.query.ListenerQuery import com.mineinabyss.idofront.typealiases.BukkitEntity -import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlinx.serialization.builtins.serializer import org.bukkit.entity.LivingEntity /** * Specifies this entity should get removed when it is far away from any player. */ -@Serializable -@SerialName("mobzy:set.remove_when_far_away") +@Serializable(with = SetRemoveWhenFarAway.Serializer::class) class SetRemoveWhenFarAway(val value: Boolean = true) { + class Serializer : InnerSerializer( + "mobzy:set.remove_when_far_away", + Boolean.serializer(), + { SetRemoveWhenFarAway(it) }, + { it.value }, + ) + companion object : ComponentDefinition by EventHelpers.defaultTo() } diff --git a/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/GlobalSpawnInfo.kt b/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/GlobalSpawnInfo.kt index 8bca14426..fcacfcc02 100644 --- a/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/GlobalSpawnInfo.kt +++ b/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/GlobalSpawnInfo.kt @@ -1,9 +1,7 @@ package com.mineinabyss.mobzy.spawning object GlobalSpawnInfo { - var iterationNumber: Int = 0 + var iterationNumber: Long = 0 internal set - var playerGroupCount = 0 - internal set -} \ No newline at end of file +} diff --git a/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/MobzySpawnFeature.kt b/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/MobzySpawnFeature.kt index 509eeb924..982d36c9b 100644 --- a/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/MobzySpawnFeature.kt +++ b/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/MobzySpawnFeature.kt @@ -1,6 +1,5 @@ package com.mineinabyss.mobzy.spawning -import com.mineinabyss.geary.modules.geary import com.mineinabyss.idofront.config.config import com.mineinabyss.idofront.di.DI import com.mineinabyss.idofront.features.Configurable @@ -13,7 +12,8 @@ val mobzySpawning by DI.observe() class MobzySpawnFeature : FeatureWithContext(::Context) { class Context : Configurable { - override val configManager = config("spawning", mobzy.plugin.dataFolder.toPath(), SpawnConfig()) + override val configManager = + config("spawning", mobzy.plugin.dataFolder.toPath(), SpawnConfig(), mergeUpdates = false) val spawnTask = SpawnTask() val spawnRegistry = SpawnRegistry() val worldGuardFlags: WorldGuardSpawnFlags = DI.get() diff --git a/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/PlayerGroups.kt b/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/PlayerGroups.kt index 9ec6cd897..93efbff0c 100644 --- a/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/PlayerGroups.kt +++ b/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/PlayerGroups.kt @@ -1,7 +1,5 @@ package com.mineinabyss.mobzy.spawning -import com.google.common.math.IntMath.pow -import org.bukkit.Chunk import org.bukkit.entity.Entity import org.nield.kotlinstatistics.dbScanCluster @@ -19,40 +17,4 @@ object PlayerGroups { ySelector = { it.location.z } ) }.map { it.points } - - - @Suppress("FunctionName") - private infix fun Int.`+-`(other: Int) = - this + setOf(-1, 1).random() * other - - /** Returns a random [Chunk] that is further than [mobzyConfig.Data.chunkSpawnRad] from all the players in this - * list, and at least within [mobzyConfig.Data.maxChunkSpawnRad] to one of them. */ - fun randomChunkNear(group: List): Chunk? { - val chunk = group.random().location.chunk - val positions = group.mapTo(mutableSetOf()) { it.location.chunk.x to it.location.chunk.z } - //TODO proper min max y for 3d space - for (i in 0..10) { - val distX = config.chunkSpawnRad.random() - val distZ = config.chunkSpawnRad.random() - val newX = chunk.x `+-` distX - val newZ = chunk.z `+-` distZ - if ( - positions.none { (x, z) -> - distanceSquared(newX, newZ, x, z) < pow(config.chunkSpawnRad.first, 2) - } - ) { - val newChunk = chunk.world.getChunkAt(newX, newZ) - if (!newChunk.isLoaded) continue - return newChunk - } - } - return null - } - - /** Gets the distance squared between between two points */ - private fun distanceSquared(x: Number, z: Number, otherX: Number, otherZ: Number): Double { - val dx = (x.toDouble() + otherX.toDouble()) - val dz = (z.toDouble() + otherZ.toDouble()) - return dx * dx + dz * dz - } } diff --git a/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/PlayerSpawnInfo.kt b/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/PlayerSpawnInfo.kt new file mode 100644 index 000000000..54bbc74ff --- /dev/null +++ b/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/PlayerSpawnInfo.kt @@ -0,0 +1,24 @@ +package com.mineinabyss.mobzy.spawning + +import com.mineinabyss.geary.datatypes.GearyEntity +import com.mineinabyss.geary.helpers.fastForEach +import com.mineinabyss.geary.papermc.tracking.entities.toGearyOrNull +import com.mineinabyss.mobzy.spawning.conditions.collectPrefabs +import org.bukkit.Chunk + +class PlayerSpawnInfo { + companion object { + fun countIn(chunks: List): MutableMap { + val count = mutableMapOf() + chunks.forEach { chunk -> + chunk.entities.fastForEach { bukkitEntity -> + val entity = bukkitEntity.toGearyOrNull() ?: return@fastForEach + entity.collectPrefabs().forEach { prefab -> + count[prefab] = count.getOrDefault(prefab, 0) + 1 + } + } + } + return count + } + } +} diff --git a/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/SpawnConfig.kt b/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/SpawnConfig.kt index 7d657f279..fd4330789 100644 --- a/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/SpawnConfig.kt +++ b/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/SpawnConfig.kt @@ -1,17 +1,14 @@ package com.mineinabyss.mobzy.spawning -import com.mineinabyss.geary.datatypes.GearyEntity import com.mineinabyss.geary.prefabs.PrefabKey import com.mineinabyss.idofront.serialization.DurationSerializer -import com.mineinabyss.idofront.serialization.IntRangeSerializer import com.mineinabyss.idofront.time.ticks import kotlinx.serialization.Serializable import kotlinx.serialization.Transient import kotlin.time.Duration /** - * @property chunkSpawnRad the minimum number of chunks away from the player in which a mob can spawn - * @property maxCommandSpawns the maximum number of mobs to spawn with /mobzy spawn + * @property spawnChunksAroundPlayer the minimum number of chunks away from the player in which a mob can spawn * @property playerGroupRadius the radius around which players will count mobs towards the local mob cap * @property spawnTaskDelay the delay in ticks between each attempted mob spawn * @property creatureTypeCaps Per-player mob caps for spawning of [NMSCreatureType]s on the server. @@ -19,24 +16,21 @@ import kotlin.time.Duration */ @Serializable class SpawnConfig( - @Serializable(with = IntRangeSerializer::class) - val chunkSpawnRad: IntRange = 2..4, - val maxCommandSpawns: Int = 20, val playerGroupRadius: Double = 96.0, - @Serializable(with = DurationSerializer::class) - val spawnTaskDelay: Duration = 40.ticks, - val globalMobCap: Map = mapOf(), - val localMobCap: Map = mapOf(), - val localMobCapRadius: Double = 256.0, - val spawnHeightRange: Int = 40, - val preventSpawningInsideBlock: Boolean = true, - val retriesUpWhenInsideBlock: Int = 3 + val spawnHeightRange: Int = 48, + val spawnChunksAroundPlayer: Int = 4, + val spawnCategories: Map = mapOf(), + val retriesUpWhenInsideBlock: Int = 3, + val maxSpawnAttemptsPerCategoryPerTick: Int = 64, ) { + @Serializable + data class SpawnCategory( + val localMax: Int, + val every: @Serializable(with = DurationSerializer::class) Duration = 1.ticks, + val position: SpawnPosition = SpawnPosition.GROUND, + val minDistanceFromPlayer: Int = 24, + ) @Transient - val localMobCapEntities = localMobCap.mapKeys { it.key.toEntity() } - - fun capsToCheckFor(types: Set): Map = types - .intersect(localMobCapEntities.keys) - .associateWith { localMobCapEntities[it]!! } + val spawnCategoryEntities = spawnCategories.mapKeys { it.key.toEntity() } } diff --git a/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/SpawnDefinition.kt b/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/SpawnDefinition.kt index 559d182e8..bf521694f 100644 --- a/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/SpawnDefinition.kt +++ b/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/SpawnDefinition.kt @@ -4,6 +4,7 @@ package com.mineinabyss.mobzy.spawning import com.mineinabyss.geary.autoscan.AutoScan +import com.mineinabyss.geary.datatypes.GearyEntity import com.mineinabyss.geary.modules.GearyModule import com.mineinabyss.geary.papermc.tracking.entities.helpers.spawnFromPrefab import com.mineinabyss.geary.prefabs.PrefabKey @@ -13,6 +14,7 @@ import com.mineinabyss.geary.systems.builders.listener import com.mineinabyss.geary.systems.query.ListenerQuery import com.mineinabyss.idofront.serialization.IntRangeSerializer import com.mineinabyss.idofront.util.randomOrMin +import com.mineinabyss.mobzy.spawning.conditions.collectPrefabs import com.mineinabyss.mobzy.spawning.event.MobzySpawnEvent import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -20,7 +22,7 @@ import kotlinx.serialization.UseSerializers import kotlinx.serialization.builtins.SetSerializer import kotlinx.serialization.builtins.serializer import org.bukkit.Location -import org.bukkit.entity.LivingEntity +import org.bukkit.util.BoundingBox import org.bukkit.util.Vector import kotlin.math.ceil import kotlin.math.floor @@ -85,6 +87,10 @@ class SpawnPriority( { SpawnPriority(it) }, { it.priority }, ) + + companion object { + val DEFAULT = SpawnPriority(50.0) + } } /** @@ -95,7 +101,11 @@ class SpawnPriority( @Serializable @SerialName("mobzy:spawn.position") enum class SpawnPosition { - AIR, GROUND, OVERHANG + AIR, GROUND, WATER; + + companion object { + val DEFAULT = GROUND + } } @@ -114,6 +124,11 @@ data class DoSpawn( val location: Location ) +data class Spawned( + val amount: Int, + val prefabs: Collection +) + @AutoScan fun GearyModule.spawnRequestListener() = listener(object : ListenerQuery() { val type by get() @@ -125,43 +140,73 @@ fun GearyModule.spawnRequestListener() = listener(object : ListenerQuery() { }).exec { val location = spawnEvent.location val spawns = amount?.randomOrMin() ?: 1 - val config = mobzySpawning.config - repeat(spawns) { + var spawnCount = 0 + val prefab = type.prefab.toEntity() + val boundingBox = prefab.get() + + // Original location always gets a spawn, we assume all conditions, including no suffocation are met there. + if (spawns > 0) { + location.spawnFromPrefab(prefab).onSuccess { spawnCount++ } + } + + // Other locations only need to meet the suffocation condition + repeat(spawns - 1) { val chosenLoc = if (spawnPos != SpawnPosition.AIR) getSpawnInRadius(location, radius) ?: location else location - val prefab = type.prefab.toEntity() - chosenLoc.spawnFromPrefab(prefab).onSuccess { spawned -> - if (spawned !is LivingEntity || !config.preventSpawningInsideBlock) return@onSuccess - val bb = spawned.boundingBox - // We shrink the box by a bit since overlap checks are strict inequalities - val bbShrunk = spawned.boundingBox.apply { - expand(-0.1, -0.1, -0.1, -0.1, -0.1, -0.1) - } - - repeat(config.retriesUpWhenInsideBlock + 1) { offsetY -> - checkLoop@ for (x in floor(bb.minX).toInt()..ceil(bb.maxX).toInt()) - for (y in floor(bb.minY).toInt()..ceil(bb.maxY).toInt()) - for (z in floor(bb.minZ).toInt()..ceil(bb.maxZ).toInt()) - if (chosenLoc.world.getBlockAt(x, y, z).collisionShape.boundingBoxes.any { shape -> - shape.shift(x.toDouble(), y.toDouble(), z.toDouble()) - shape.overlaps(bbShrunk) - }) { - bb.shift(0.0, 1.0, 0.0) - bbShrunk.shift(0.0, 1.0, 0.0) - return@repeat - } - if (offsetY != 0) { - spawned.teleport(chosenLoc.clone().add(0.0, offsetY.toDouble(), 0.0)) - } - return@onSuccess - } - logger.v("Failed to find a spawn that would not cause suffocation for ${type.prefab}") - spawned.remove() - } + val suitableLoc = ensureSuitableLocationOrNull( + chosenLoc, + (boundingBox?.clone()?.shift(chosenLoc)) ?: BoundingBox.of(chosenLoc.toVector(), 1.0, 2.0, 1.0) + ) ?: return@repeat + + suitableLoc.spawnFromPrefab(prefab).onSuccess { spawnCount++ } } + event.entity.set( + Spawned( + spawnCount, + prefab.collectPrefabs().intersect(mobzySpawning.spawnRegistry.spawnCategories.keys) + ) + ) +} + +/** + * Ensures that the given location is suitable for spawning an entity by checking if it is inside any solid blocks. + * If it is, it will attempt to find a suitable location by shifting the bounding box upwards. + * + * @param chosenLoc The chosen location for spawning the entity. + * @param bb The bounding box of the entity. + * @param extraAttemptsUp The number of additional attempts to find a suitable location when inside a block. Defaults to the value specified in the mobzySpawning config. + * @return A suitable location for spawning the entity, or null if no suitable location is found. + */ +fun ensureSuitableLocationOrNull( + chosenLoc: Location, + bb: BoundingBox, + extraAttemptsUp: Int = mobzySpawning.config.retriesUpWhenInsideBlock +): Location? { + return chosenLoc + val bb = bb.clone() + // We shrink the box by a bit since overlap checks are strict inequalities + val bbShrunk = bb.clone().apply { + expand(-0.1, -0.1, -0.1, -0.1, -0.1, -0.1) + } + + repeat(extraAttemptsUp + 1) { offsetY -> + checkLoop@ for (x in floor(bb.minX).toInt()..ceil(bb.maxX).toInt()) + for (y in floor(bb.minY).toInt()..ceil(bb.maxY).toInt()) + for (z in floor(bb.minZ).toInt()..ceil(bb.maxZ).toInt()) + if (chosenLoc.world.getBlockAt(x, y, z).collisionShape.boundingBoxes.any { shape -> + shape.shift(x.toDouble(), y.toDouble(), z.toDouble()) + shape.overlaps(bbShrunk) + }) { + bb.shift(0.0, 1.0, 0.0) + bbShrunk.shift(0.0, 1.0, 0.0) + return@repeat + } + return chosenLoc.clone().add(0.0, offsetY.toDouble(), 0.0) + } + return null } diff --git a/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/SpawnRegistry.kt b/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/SpawnRegistry.kt index a6f7b2181..214a50f98 100644 --- a/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/SpawnRegistry.kt +++ b/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/SpawnRegistry.kt @@ -2,11 +2,14 @@ package com.mineinabyss.mobzy.spawning import com.mineinabyss.geary.annotations.optin.UnsafeAccessors import com.mineinabyss.geary.datatypes.Entity +import com.mineinabyss.geary.datatypes.GearyEntity import com.mineinabyss.geary.modules.geary +import com.mineinabyss.geary.prefabs.PrefabKey import com.mineinabyss.geary.prefabs.prefabs import com.mineinabyss.geary.systems.builders.cachedQuery import com.mineinabyss.geary.systems.query.GearyQuery import com.mineinabyss.geary.systems.query.Query +import com.mineinabyss.mobzy.mobzy import com.sk89q.worldedit.bukkit.BukkitAdapter import com.sk89q.worldguard.WorldGuard import com.sk89q.worldguard.protection.regions.ProtectedRegion @@ -23,17 +26,20 @@ class SpawnRegistry { private val spawnsWithWGRegion = geary.cachedQuery(object : Query() { val parentRegions by get() val spawnType by get() - val priority by get().orDefault { SpawnPriority(1.0) } - val position by get().orDefault { SpawnPosition.GROUND } + val priority by get().orDefault { SpawnPriority.DEFAULT } + val position by get().orDefault { SpawnPosition.DEFAULT } }) private val regionContainer = WorldGuard.getInstance().platform.regionContainer - private var regionSpawns: Map> = mapOf() + + var spawnCategories = mapOf() + private set + val spawnConfigsQuery = geary.cachedQuery(SpawnConfigs()) /** Clears [regionSpawns] */ fun unregisterSpawns() { - regionSpawns = mapOf() + spawnCategories = mapOf() } fun reloadSpawns() { @@ -48,38 +54,45 @@ class SpawnRegistry { loadSpawns() } - class SpawnDef( - val entity: Entity, - val priority: Double, - val type: SpawnType, - val position: SpawnPosition, - ) @OptIn(UnsafeAccessors::class) fun loadSpawns() { - val map = mutableMapOf>() + val categoryKeys = mobzySpawning.config.spawnCategoryEntities.keys + val categories = mutableMapOf>>() spawnsWithWGRegion.map { parentRegions.keys to SpawnDef(unsafeEntity, priority.priority, spawnType, position) }.forEach { (regions, spawn) -> + val category = spawn.type.prefab.toEntity().prefabs.firstOrNull { it in categoryKeys } ?: run { + mobzy.logger.w("Failed to find category for ${spawn.type.prefab}") + return@forEach + } + val categoryRegionSpawns = categories.getOrPut(category) { mutableMapOf() } + regions.forEach { regionName -> - map.getOrPut(regionName) { mutableSetOf() }.add(spawn) + categoryRegionSpawns.getOrPut(regionName) { mutableSetOf() }.add(spawn) } } - regionSpawns = map - + spawnCategories = categories.mapValues { + val config = mobzySpawning.config.spawnCategoryEntities[it.key]!! + SpawnCategory( + config = config, + name = it.key.get()!!.toString(), + prefab = it.key, + regionSpawns = it.value + ) + } } /** Takes a list of spawn region names and converts to a list of [SpawnDefinition]s from those regions */ - fun getAllowedSpawnsForRegions(regions: List): List = - regions.flatMap { this.regionSpawns[it.id] ?: setOf() } + fun getAllowedSpawnsForRegions(category: SpawnCategory, regions: List): List = + regions.flatMap { category.regionSpawns[it.id] ?: setOf() } - fun getAllowedSpawnsAt(location: Location): List = - getAllowedSpawnsForRegions(getRegionsAt(location)) + fun getAllowedSpawnsAt(category: SpawnCategory, location: Location): List = + getAllowedSpawnsForRegions(category, getRegionsAt(location)) class SpawnConfigs : GearyQuery() { val config by get() } - private fun getRegionsAt(location: Location): List { return regionContainer .createQuery() @@ -100,4 +113,18 @@ class SpawnRegistry { ?.let { return listOf(it) } ?: this + + class SpawnCategory( + val config: SpawnConfig.SpawnCategory, + val name: String, + val prefab: Entity, + val regionSpawns: Map> = mapOf() + ) + + class SpawnDef( + val entity: Entity, + val priority: Double, + val type: SpawnType, + val position: SpawnPosition, + ) } diff --git a/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/SpawnTask.kt b/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/SpawnTask.kt index 25233cc41..892a404b4 100644 --- a/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/SpawnTask.kt +++ b/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/SpawnTask.kt @@ -1,50 +1,33 @@ package com.mineinabyss.mobzy.spawning import co.touchlab.kermit.Severity -import com.github.shynixn.mccoroutine.bukkit.asyncDispatcher import com.github.shynixn.mccoroutine.bukkit.launch import com.github.shynixn.mccoroutine.bukkit.minecraftDispatcher import com.mineinabyss.geary.components.KeepArchetype import com.mineinabyss.geary.components.RequestCheck import com.mineinabyss.geary.components.events.FailedCheck +import com.mineinabyss.geary.datatypes.GearyEntity +import com.mineinabyss.geary.helpers.fastForEach import com.mineinabyss.geary.helpers.temporaryEntity import com.mineinabyss.geary.papermc.bridge.config.inputs.Input import com.mineinabyss.geary.papermc.bridge.config.inputs.Variables import com.mineinabyss.idofront.time.inWholeTicks -import com.mineinabyss.mobzy.* +import com.mineinabyss.idofront.time.ticks +import com.mineinabyss.idofront.typealiases.BukkitEntity +import com.mineinabyss.mobzy.mobzy import com.mineinabyss.mobzy.spawning.event.MobzySpawnEvent import com.mineinabyss.mobzy.spawning.event.SpawnStrip -import com.sk89q.worldguard.WorldGuard -import it.unimi.dsi.fastutil.objects.Object2DoubleOpenHashMap -import kotlinx.coroutines.* +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import org.bukkit.Bukkit -import org.bukkit.Chunk +import org.bukkit.block.Block import org.nield.kotlinstatistics.WeightedDice /** * An asynchronous repeating task that finds areas to spawn mobs in. - * - * ### STEP 1: Get mobs - * Get all custom mobs in all worlds on the server, group them by entity type, and creature type. - * - * Getting all entities through Bukkit and grouping them every time is a little slow, but nothing - * compared to the reading info from chunks for finding a spawn area that runs for every player group. - * - * ### STEP 2: Every player group picks a random chunk around them - * Uses [toPlayerGroups] to groups nearby players together and picks a spawn around them with [randomChunkNearby] - * - * ### STEP 3: Calculate all mobs that can spawn in this area - * Gets all valid [SpawnRegion]s inside a chosen location in the chunk, then gets all the spawns that can spawn - * there. - * - * ### STEP 4: Pick a random valid spawn and spawn it - * Does a weighted random decision based on each spawn's priority, and schedules a sync task that will spawn mobs in - * the chosen region */ class SpawnTask { - private val config get() = mobzySpawning.config private var runningTask: Job? = null - private val regionContainer = WorldGuard.getInstance().platform.regionContainer fun stopTask() { runningTask?.cancel() @@ -53,7 +36,7 @@ class SpawnTask { fun startTask() { if (runningTask != null) return - runningTask = mobzy.plugin.launch(mobzy.plugin.asyncDispatcher) { + runningTask = mobzy.plugin.launch(mobzy.plugin.minecraftDispatcher) { while (mobzy.config.doMobSpawns && mobzy.plugin.isEnabled) { try { GlobalSpawnInfo.iterationNumber++ @@ -65,68 +48,87 @@ class SpawnTask { } catch (e: RuntimeException) { e.printStackTrace() } - delay(config.spawnTaskDelay.inWholeTicks) + delay(1.ticks) } stopTask() } } - private val assumeTotalN = 5 - val successPreference = Object2DoubleOpenHashMap() - - private suspend fun runSpawnTask() { + private fun runSpawnTask() { val onlinePlayers = Bukkit.getOnlinePlayers().filter { !it.isDead } if (onlinePlayers.isEmpty()) return val playerGroups = PlayerGroups.group(onlinePlayers) - GlobalSpawnInfo.playerGroupCount = playerGroups.size - //TODO sorted by least mobs around + val runnableCategories = + mobzySpawning.spawnRegistry.spawnCategories.values // Find spawn categories that should attempt spawn this tick + .filter { GlobalSpawnInfo.iterationNumber % it.config.every.inWholeTicks == 0L } + playerGroups.shuffled().forEach playerLoop@{ playerGroup -> - // Every player group picks a random chunk around them - val strip = SpawnStrip.findNearPlayers(playerGroup) ?: run { - mobzy.logger.v("Could not find spawn strip for player group $playerGroup") - return@playerLoop + val chunks = SpawnableChunks.getChunksAround(playerGroup) + val categoryCounts = PlayerSpawnInfo.countIn(chunks) + val restrictions = SpawnStrip.getRestrictionsFor(playerGroup) + runnableCategories.fastForEach { category -> + chunks.asSequence().shuffled().take(mobzySpawning.config.maxSpawnAttemptsPerCategoryPerTick) + .forEach { chunk -> + if (categoryCounts.getOrDefault(category.prefab, 0) > category.config.localMax) + return@forEach + + val spawnBlock = SpawnStrip.findAndVerify(chunk, restrictions, category.config.position) + ?: return@forEach + + val spawned = attemptSpawn(spawnBlock, category, playerGroup, categoryCounts) ?: return@forEach + spawned.prefabs.forEach { prefab -> + categoryCounts[prefab] = categoryCounts.getOrDefault(prefab, 0) + 1 + } + } } + } + } - withContext(mobzy.plugin.minecraftDispatcher) { - val allowedSpawns = mobzySpawning.spawnRegistry.getAllowedSpawnsAt(strip.bottom) - if (allowedSpawns.isEmpty()) { - mobzy.logger.v("Could not find spawns for player group $playerGroup") - return@withContext - } - val attemptSpawn = WeightedDice(allowedSpawns.associateWith { it.priority }).roll() - val spawnLoc = strip.getLocationFor(attemptSpawn.position) - val spawnCheckLoc = spawnLoc.clone().add(0.0, -1.0, 0.0) + fun attemptSpawn( + spawnBlock: Block, + category: SpawnRegistry.SpawnCategory, + playerGroup: List, + categoryCounts: Map + ): Spawned? { + val packSpawnLoc = spawnBlock.location + val minDist = category.config.minDistanceFromPlayer + if (playerGroup.any { it.location.distanceSquared(packSpawnLoc) < (minDist * minDist) }) return null + val allowedSpawns = mobzySpawning.spawnRegistry.getAllowedSpawnsAt(category, packSpawnLoc) + if (allowedSpawns.isEmpty()) return null - mobzy.logger.v("Attempting spawn ${attemptSpawn.type.prefab} with chunk preference ${successPreference[spawnLoc.chunk]} at ${spawnLoc.toVector()}") - val success = temporaryEntity { target -> //TODO update once geary supports events without targets - target.add() - target.callEvent( - init = { - add() - set(MobzySpawnEvent(strip, playerGroup)) - set( - Variables.Evaluated( - entries = mapOf("location" to Input.of(spawnCheckLoc)) - ) - ) - add() - }, - source = attemptSpawn.entity - ) { !it.has() } - } + val attemptSpawn = WeightedDice(allowedSpawns.associateWith { it.priority }).roll() + val spawnCheckLoc = packSpawnLoc.clone().add(0.0, -1.0, 0.0) + + mobzy.logger.v("Attempting spawn ${attemptSpawn.type.prefab} from ${category.name} at ${packSpawnLoc.toVector()}") + //TODO update once geary supports events without targets + val success = temporaryEntity { target -> + target.callEvent( + init = { + add() + set(MobzySpawnEvent(playerGroup, spawnCheckLoc, categoryCounts)) + set( + Variables.Evaluated( + entries = mapOf("location" to Input.of(spawnCheckLoc)) + ) + ) + add() + }, + source = attemptSpawn.entity + ) { !it.has() } + } - if (success) { - mobzy.logger.s("Checks passed for ${attemptSpawn.type.prefab}", Severity.Verbose) - attemptSpawn.entity.callEvent(strip, DoSpawn(spawnLoc)) - successPreference[spawnLoc.chunk] = - (successPreference.getOrElse(spawnLoc.chunk) { 5.0 } + 1).coerceAtMost(10.0) - } else { - successPreference[spawnLoc.chunk] = - (successPreference.getOrElse(spawnLoc.chunk) { 5.0 } - 1).coerceAtLeast(1.0) + if (success) { + mobzy.logger.s("Checks passed for ${attemptSpawn.type.prefab}", Severity.Verbose) + temporaryEntity { event -> + event.apply { + set(DoSpawn(packSpawnLoc)) } + attemptSpawn.entity.callEvent(event) + return event.get() } } + return null } } diff --git a/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/SpawnableChunks.kt b/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/SpawnableChunks.kt new file mode 100644 index 000000000..59cc498d4 --- /dev/null +++ b/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/SpawnableChunks.kt @@ -0,0 +1,23 @@ +package com.mineinabyss.mobzy.spawning + +import org.bukkit.Chunk +import org.bukkit.entity.Entity + +object SpawnableChunks { + fun getChunksAround(players: List): List { + val playerChunks = players.mapTo(mutableSetOf()) { player -> player.chunk } + val world = players.first().world + + val chunkRadius = mobzySpawning.config.spawnChunksAroundPlayer + val positions = mutableSetOf>() + for (playerChunk in playerChunks) { + for (x in -chunkRadius..chunkRadius) { + for (z in -chunkRadius..chunkRadius) { + positions += playerChunk.x + x to playerChunk.z + z + } + } + } + return positions + .map { (x, z) -> world.getChunkAt(x, z) } + } +} diff --git a/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/WorldGuardSpawnFlags.kt b/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/WorldGuardSpawnFlags.kt index 6adb77536..f2a33f7fb 100644 --- a/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/WorldGuardSpawnFlags.kt +++ b/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/WorldGuardSpawnFlags.kt @@ -34,11 +34,4 @@ class WorldGuardSpawnFlags { e.printStackTrace() } } - - /*fun disablePluginSpawnsErrorMessage() { - //TODO try to allow plugin spawning in WorldGuard's config automatically - //onCreatureSpawn in WorldGuardEntityListener throws errors if we don't enable custom entity spawns - WorldGuard.getInstance().platform.globalStateManager.get(BukkitAdapter.adapt(server.worlds.first())) - .blockPluginSpawning = false - }*/ } diff --git a/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/conditions/BlockCompositionCondition.kt b/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/conditions/BlockCompositionCondition.kt index 726264670..7ea383cfa 100644 --- a/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/conditions/BlockCompositionCondition.kt +++ b/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/conditions/BlockCompositionCondition.kt @@ -6,7 +6,6 @@ import com.mineinabyss.geary.systems.builders.listener import com.mineinabyss.geary.systems.query.ListenerQuery import com.mineinabyss.idofront.serialization.DoubleRangeSerializer import com.mineinabyss.idofront.util.DoubleRange -import com.mineinabyss.mobzy.mobzy import com.mineinabyss.mobzy.spawning.event.MobzySpawnEvent import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -28,9 +27,10 @@ fun GearyModule.blockCompositionChecker() = listener(object : ListenerQuery() { val blockComposition by source.get() val spawnEvent by event.get() }).check { - blockComposition.materials.all { (material, range) -> - (spawnEvent.blockComposition.percent(material) in range).also { - if(!it) mobzy.logger.v("Failed block composition check for $material in range $range") - } - } + true +// blockComposition.materials.all { (material, range) -> +// (spawnEvent.blockComposition.percent(material) in range).also { +// if(!it) mobzy.logger.v("Failed block composition check for $material in range $range") +// } +// } } diff --git a/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/conditions/LocalGroupConditions.kt b/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/conditions/LocalGroupConditions.kt index 2f0084ebd..4bfaccf1c 100644 --- a/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/conditions/LocalGroupConditions.kt +++ b/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/conditions/LocalGroupConditions.kt @@ -4,7 +4,6 @@ import com.mineinabyss.geary.autoscan.AutoScan import com.mineinabyss.geary.modules.GearyModule import com.mineinabyss.geary.systems.builders.listener import com.mineinabyss.geary.systems.query.ListenerQuery -import com.mineinabyss.mobzy.mobzy import com.mineinabyss.mobzy.spawning.SpawnType import com.mineinabyss.mobzy.spawning.event.MobzySpawnEvent import kotlinx.serialization.SerialName @@ -29,8 +28,5 @@ fun GearyModule.localGroupChecker() = listener(object : ListenerQuery() { val spawnType by source.get() val spawnEvent by event.get() }).check { - val nearby = spawnEvent.strip.nearbyEntities(spawnType.prefab.toEntity(), conf.radius) - (nearby < conf.max).also { - if(!it) mobzy.logger.v("Failed local group check for ${spawnType.prefab} in radius ${conf.radius}, $nearby > ${conf.max}.") - } + (spawnEvent.categoryCounts[spawnType.prefab.toEntity()] ?: 0) < conf.max } diff --git a/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/conditions/MobCapHasSpaceCondition.kt b/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/conditions/MobCapHasSpaceCondition.kt index 20e97497e..698d1b4c6 100644 --- a/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/conditions/MobCapHasSpaceCondition.kt +++ b/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/conditions/MobCapHasSpaceCondition.kt @@ -1,38 +1,30 @@ package com.mineinabyss.mobzy.spawning.conditions -import com.mineinabyss.geary.autoscan.AutoScan import com.mineinabyss.geary.datatypes.Entity import com.mineinabyss.geary.datatypes.GearyEntity -import com.mineinabyss.geary.modules.GearyModule -import com.mineinabyss.geary.systems.builders.listener -import com.mineinabyss.geary.systems.query.ListenerQuery -import com.mineinabyss.mobzy.spawning.SpawnType -import com.mineinabyss.mobzy.spawning.event.MobzySpawnEvent -import com.mineinabyss.mobzy.spawning.mobzySpawning -@AutoScan -fun GearyModule.mobCapChecker() = listener(object : ListenerQuery() { - val spawnType by source.get() - val spawnEvent by event.get() -}).check { - mobzySpawning.config - .capsToCheckFor(spawnType.prefab.toEntity().collectPrefabs()) - .all { (prefab, max) -> - val nearby = spawnEvent.strip.nearbyEntities(prefab, mobzySpawning.config.localMobCapRadius) - (nearby < max).also { - if(!it) logger.v("Failed mob cap check for $prefab in radius ${mobzySpawning.config.localMobCapRadius}, $nearby > $max.") - } - } -} //TODO move into geary fun GearyEntity.collectPrefabs(): Set { return collectPrefabs(mutableSetOf(), listOf(this)) } -private fun collectPrefabs(collected: MutableSet, search: List): Set { +private tailrec fun collectPrefabs(collected: MutableSet, search: List): Set { if (search.isEmpty()) return collected val new = search.flatMap { it.prefabs } - collected collected.addAll(new) return collectPrefabs(collected, new) } + +fun GearyEntity.deepInstanceOf(prefab: Entity): Boolean { + return if (instanceOf(prefab)) return true + else deepInstanceOf(mutableSetOf(), prefabs, prefab) +} + + +private tailrec fun deepInstanceOf(seen: MutableSet, search: List, prefab: Entity): Boolean { + if (search.isEmpty()) return false + if (search.any { it.instanceOf(prefab) }) return true + seen.addAll(search) + return deepInstanceOf(seen, search.flatMap { it.prefabs } - seen, prefab) +} diff --git a/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/conditions/MobSuffocateCondition.kt b/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/conditions/MobSuffocateCondition.kt new file mode 100644 index 000000000..dd22c93d5 --- /dev/null +++ b/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/conditions/MobSuffocateCondition.kt @@ -0,0 +1,29 @@ +package com.mineinabyss.mobzy.spawning.conditions + +import com.mineinabyss.geary.autoscan.AutoScan +import com.mineinabyss.geary.modules.GearyModule +import com.mineinabyss.geary.systems.builders.listener +import com.mineinabyss.geary.systems.query.ListenerQuery +import com.mineinabyss.mobzy.spawning.SpawnType +import com.mineinabyss.mobzy.spawning.ensureSuitableLocationOrNull +import com.mineinabyss.mobzy.spawning.event.MobzySpawnEvent +import org.bukkit.util.BoundingBox + +/** + * # `mobzy:spawn.local_group` + * + * Checks that no more than [max] Bukkit entities of the same type as this one are within [radius] blocks during a + * mob spawn. + */ +@AutoScan +fun GearyModule.mobSuffocationChecker() = listener(object : ListenerQuery() { + val spawnType by source.get() + val spawnEvent by event.get() +}).check { + val bb = spawnType.prefab.toEntity().get() ?: return@check true + ensureSuitableLocationOrNull( + spawnEvent.spawnLocation, + (bb.clone().shift(spawnEvent.spawnLocation)), + extraAttemptsUp = 0 + ) != null +} diff --git a/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/conditions/SpawnGapCondition.kt b/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/conditions/SpawnGapCondition.kt index 4a12a8fe2..73c14a284 100644 --- a/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/conditions/SpawnGapCondition.kt +++ b/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/conditions/SpawnGapCondition.kt @@ -1,13 +1,7 @@ package com.mineinabyss.mobzy.spawning.conditions -import com.mineinabyss.geary.autoscan.AutoScan -import com.mineinabyss.geary.modules.GearyModule import com.mineinabyss.geary.serialization.serializers.InnerSerializer -import com.mineinabyss.geary.systems.builders.listener -import com.mineinabyss.geary.systems.query.ListenerQuery import com.mineinabyss.idofront.serialization.IntRangeSerializer -import com.mineinabyss.mobzy.mobzy -import com.mineinabyss.mobzy.spawning.event.MobzySpawnEvent import kotlinx.serialization.Serializable /** @@ -28,13 +22,13 @@ class SpawnGap( ) } -@AutoScan -fun GearyModule.spawnGroupChecker() = listener(object : ListenerQuery() { - val spawnGap by source.get() - - val spawnEvent by event.get() -}).check { - (spawnEvent.strip.gap in spawnGap.range).also { - if(!it) mobzy.logger.v("Failed spawn gap check, ${spawnEvent.strip.gap} not in ${spawnGap.range}.") - } -} +//@AutoScan +//fun GearyModule.spawnGroupChecker() = listener(object : ListenerQuery() { +// val spawnGap by source.get() +// +// val spawnEvent by event.get() +//}).check { +// (spawnEvent.strip.gap in spawnGap.range).also { +// if(!it) mobzy.logger.v("Failed spawn gap check, ${spawnEvent.strip.gap} not in ${spawnGap.range}.") +// } +//} diff --git a/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/event/MobzySpawnEvent.kt b/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/event/MobzySpawnEvent.kt index 85d3f211b..b5ff7e996 100644 --- a/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/event/MobzySpawnEvent.kt +++ b/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/event/MobzySpawnEvent.kt @@ -1,12 +1,8 @@ package com.mineinabyss.mobzy.spawning.event -import com.mineinabyss.geary.papermc.tracking.entities.toGearyOrNull -import com.mineinabyss.geary.prefabs.PrefabKey +import com.mineinabyss.geary.datatypes.GearyEntity import com.mineinabyss.idofront.typealiases.BukkitEntity -import com.mineinabyss.mobzy.spawning.components.SubChunkBlockComposition -import org.bukkit.ChunkSnapshot import org.bukkit.Location -import org.bukkit.entity.Entity //TODO name could be confused with SpawnRegion /** @@ -17,9 +13,8 @@ import org.bukkit.entity.Entity * @property gap The gap in the y-axis between the two of them. */ data class MobzySpawnEvent( - val strip: SpawnStrip, val playerGroup: List, -) { - val blockComposition by lazy { SubChunkBlockComposition(strip.chunkSnapshot, strip.bottom.blockY) } -} + val spawnLocation: Location, + val categoryCounts: Map, +) diff --git a/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/event/SpawnStrip.kt b/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/event/SpawnStrip.kt index fc5e0bb71..94b9f0449 100644 --- a/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/event/SpawnStrip.kt +++ b/mobzy-spawning/src/main/kotlin/com/mineinabyss/mobzy/spawning/event/SpawnStrip.kt @@ -1,18 +1,11 @@ package com.mineinabyss.mobzy.spawning.event -import com.mineinabyss.geary.datatypes.GearyEntity -import com.mineinabyss.geary.papermc.tracking.entities.toGearyOrNull -import com.mineinabyss.idofront.location.down -import com.mineinabyss.idofront.location.up -import com.mineinabyss.mobzy.spawning.PlayerGroups import com.mineinabyss.mobzy.spawning.SpawnPosition import com.mineinabyss.mobzy.spawning.mobzySpawning import org.bukkit.Chunk -import org.bukkit.ChunkSnapshot -import org.bukkit.Location +import org.bukkit.block.Block import org.bukkit.entity.Entity -import org.nield.kotlinstatistics.WeightedDice -import kotlin.random.Random +import kotlin.math.min /** * A vertical strip inside a chunk, with x and z located at [loc], spanning from [minY] to [maxY]. @@ -20,38 +13,17 @@ import kotlin.random.Random * Will generate a list of [MobzySpawnEvent]s, from all air gaps within this vertical strip. */ -data class SpawnStrip( - val bottom: Location, - val top: Location, - val chunkSnapshot: ChunkSnapshot, -) { - //adding one since if the blocks are on the same block, they still have a gap of 1 from top to bottom - val gap: Int = top.blockY - bottom.blockY + 1 +class SpawnStrip { - /** - * Get a [Location] to spawn in depending on the specified [SpawnPosition] - * - * @return - * - If [SpawnPosition.AIR], a random location between [top] and [bottom] - * - If [SpawnPosition.GROUND], [bottom] - * - If [SpawnPosition.OVERHANG], [top] - */ - fun getLocationFor(position: SpawnPosition): Location = - when (position) { - //pick some position between the bottom and top when spawn position is in air - SpawnPosition.AIR -> bottom.clone().apply { if (gap > 1) y += Random.nextInt(gap - 1).toDouble() } - SpawnPosition.GROUND -> bottom.clone().up(1) - SpawnPosition.OVERHANG -> top.clone().down(1) - } - - //TODO deep instanceOf check to let count nearby entities of a certain type - fun nearbyEntities(prefab: GearyEntity, radius: Double): Int { - return bottom.world.getNearbyEntities(bottom, radius * 2, radius * 2, radius * 2) - .count { it.toGearyOrNull()?.instanceOf(prefab) == true } - } + data class Restrictions( + val minY: Int, + val maxY: Int, + val startY: Int, + ) companion object { - fun findNearPlayers(playerGroup: List): SpawnStrip? { + + fun getRestrictionsFor(playerGroup: List): Restrictions { val heights = playerGroup.map { it.location.y.toInt() } val world = playerGroup.first().world val heightRange = mobzySpawning.config.spawnHeightRange @@ -59,84 +31,51 @@ data class SpawnStrip( val minY = (heights.minOrNull()!! - heightRange).coerceIn(worldHeight) val maxY = (heights.maxOrNull()!! + heightRange).coerceIn(worldHeight) val startY = (minY..maxY).random() - val chunks = (1..5).mapNotNull { PlayerGroups.randomChunkNear(playerGroup) } ?: return null - val chunk = WeightedDice(chunks.associateWith { - mobzySpawning.spawnTask.successPreference.getOrDefault(key = it, 5.0) - }).roll() - return findAt( - chunk = chunk, - snapshot = chunk.chunkSnapshot, + return Restrictions( minY = minY, maxY = maxY, - x = (0..15).random(), - z = (0..15).random(), startY = startY ) } - fun findAt( + fun findAndVerify( chunk: Chunk, - snapshot: ChunkSnapshot = chunk.chunkSnapshot, - minY: Int, - maxY: Int, - x: Int, - z: Int, - startY: Int, - ): SpawnStrip { - - fun Int.getBlock() = snapshot.getBlockType(x, this, z) - - val startIsEmpty = startY.getBlock().isAir - - class BlocLoc(val add: Int) { - lateinit var opposite: BlocLoc - var y = startY - var isEmpty: Boolean = startIsEmpty - var foundBlock = false - - fun next(): Boolean { - if (foundBlock) return false + restrictions: Restrictions, + positionType: SpawnPosition, +// snapshot: ChunkSnapshot = chunk.chunkSnapshot, + x: Int = (0..15).random(), + z: Int = (0..15).random(), + ): Block? { + val max = when (positionType) { + SpawnPosition.GROUND -> min( + // We want the air block above the max block to be a potential spawn location + chunk.world.getHighestBlockYAt(chunk.x shl 4 or x, chunk.z shl 4 or z) + 1, + restrictions.maxY + ) + + else -> restrictions.maxY + } + val min = restrictions.minY - if (y !in minY..maxY) { - y -= add - foundBlock = true - return false - } + if (max <= min) return null // Possible for GROUND - val nextIsEmpty = y.getBlock().isAir + val block = chunk.getBlock(x, (min..max).random(), z) - when { - isEmpty && !nextIsEmpty -> { - foundBlock = true - return false - } + if (block.isSolid) return null - !isEmpty && nextIsEmpty -> { - opposite.foundBlock = true - opposite.y = y - add - } - } - isEmpty = nextIsEmpty - y += add - return true + when (positionType) { + // Make sure a solid block underneath the spawn location + SpawnPosition.GROUND -> { + if (block.y == (block.world.minHeight)) return null + val blockBelow = chunk.getBlock(x, block.y - 1, z) + if (!blockBelow.isSolid && !blockBelow.isLiquid) return null } - } - val up = BlocLoc(1) - val down = BlocLoc(-1) - up.opposite = down - down.opposite = up - - do { - val searchUp = up.next() - val searchDown = down.next() - } while (searchUp || searchDown) + SpawnPosition.WATER -> if (!block.isLiquid) return null + SpawnPosition.AIR -> if (block.isSolid) return null + } - return SpawnStrip( - bottom = chunk.getBlock(x, down.y, z).location, - top = chunk.getBlock(x, up.y, z).location, - chunkSnapshot = snapshot - ) + return block } } } diff --git a/src/main/kotlin/com/mineinabyss/mobzy/DebugCommands.kt b/src/main/kotlin/com/mineinabyss/mobzy/DebugCommands.kt index 4307ebb5b..c2d68ba3e 100644 --- a/src/main/kotlin/com/mineinabyss/mobzy/DebugCommands.kt +++ b/src/main/kotlin/com/mineinabyss/mobzy/DebugCommands.kt @@ -10,7 +10,6 @@ import com.mineinabyss.idofront.messaging.broadcastVal import com.mineinabyss.idofront.messaging.info import com.mineinabyss.idofront.messaging.success import com.mineinabyss.mobzy.spawning.* -import com.mineinabyss.mobzy.spawning.event.SpawnStrip import org.bukkit.Bukkit import kotlin.system.measureTimeMillis @@ -39,31 +38,6 @@ internal fun Command.createDebugCommands() { sender.info(PlayerGroups.group(Bukkit.getOnlinePlayers())) } } - "conditions" { - val spawnName by stringArg() - - playerAction { - val loc = player.location - val x = loc.blockX.toChunkLoc() - val z = loc.blockZ.toChunkLoc() - val spawnInfo = SpawnStrip.findNearPlayers( - playerGroup = listOf(player), - ) ?: return@playerAction - PrefabKey.of(spawnName).toEntityOrNull()?.callEvent(spawnInfo, DoSpawn(player.location)) - //TODO list all failed conditions - } - } - "find" { - val miny by intArg() - val maxy by intArg() - playerAction { - val loc = player.location - val info = SpawnStrip.findNearPlayers( - playerGroup = listOf(player), - ) ?: return@playerAction - sender.info("${info.bottom.y} and ${info.top.y}") - } - } } "pdc" { playerAction { diff --git a/src/test/kotlin/com/mineinabyss/mobzy/spawning/PlayerGroupsTest.kt b/src/test/kotlin/com/mineinabyss/mobzy/spawning/PlayerGroupsTest.kt deleted file mode 100644 index cb623025f..000000000 --- a/src/test/kotlin/com/mineinabyss/mobzy/spawning/PlayerGroupsTest.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.mineinabyss.mobzy.spawning - -import be.seeseemelk.mockbukkit.entity.PlayerMock -import com.mineinabyss.idofront.di.DI -import com.mineinabyss.idofront.time.ticks -import io.kotest.matchers.collections.shouldContain -import io.mockk.every -import io.mockk.mockk -import org.bukkit.Location -import org.junit.jupiter.api.BeforeAll -import org.junit.jupiter.api.TestInstance - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -internal class PlayerGroupsTest: BukkitTest() { - @BeforeAll - fun setupConfig() { - DI.add(mockk { - every { config } returns SpawnConfig( - chunkSpawnRad = 0..1, - maxCommandSpawns = 3, - playerGroupRadius = 20.0, - spawnTaskDelay = 10.ticks, - spawnHeightRange = 100, - ) - }) - } - - private fun playerAt(location: Location) = server.addPlayer().apply { this.location = location } - - fun populatePlayers(): List { - val p1 = playerAt(Location(world, 0.0, 10.0, 0.0)) - val p2 = playerAt(Location(world, -10.0, 10.0, 0.0)) - - val p3 = playerAt(Location(world, -31.0, 10.0, 0.0)) - - val p4 = playerAt(Location(world, 15.0, 10.0, 15.0)) - val p5 = playerAt(Location(world, 30.0, 10.0, 5.0)) - - return listOf(p1, p2, p3, p4, p5) - } - -// @Test //TODO MockBukkit is broken here - fun group() { - val (p1, p2, p3, p4, p5) = populatePlayers() - val groups = PlayerGroups.group(server.onlinePlayers) - groups shouldContain listOf(p1, p2) - groups shouldContain listOf(p3) - groups shouldContain listOf(p4, p5) - } -} diff --git a/src/test/kotlin/com/mineinabyss/mobzy/spawning/vertical/VerticalSpawnTest.kt b/src/test/kotlin/com/mineinabyss/mobzy/spawning/vertical/VerticalSpawnTest.kt deleted file mode 100644 index 06401d9ca..000000000 --- a/src/test/kotlin/com/mineinabyss/mobzy/spawning/vertical/VerticalSpawnTest.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.mineinabyss.mobzy.spawning.vertical - -import com.mineinabyss.mobzy.spawning.BukkitTest -import com.mineinabyss.mobzy.spawning.Helpers.mockSnapshot -import com.mineinabyss.mobzy.spawning.event.MobzySpawnEvent -import com.mineinabyss.mobzy.spawning.event.SpawnStrip -import io.kotest.matchers.shouldBe -import org.bukkit.Material -import org.junit.jupiter.api.Test - -internal class VerticalSpawnTest : BukkitTest() { - val chunk by lazy { world.getChunkAt(0, 0).mockSnapshot() } - val snapshot by lazy { chunk.chunkSnapshot } - - fun gap(min: Int, max: Int) = - SpawnStrip(chunk.getBlock(0, min, 0).location, chunk.getBlock(0, max, 0).location, snapshot) - - fun findGapAt(min: Int, max: Int, start: Int) = - SpawnStrip.findAt( - chunk = chunk, - snapshot = snapshot, - minY = min, - maxY = max, - x = 0, - z = 0, - startY = start - ) - - @Test - fun findGap() { - //World: 10 GRASS_BLOCK, AIR - - // Single gap tests - findGapAt(0, 127, 100) shouldBe gap(10, 127) - findGapAt(0, 127, 0) shouldBe gap(10, 127) - findGapAt(0, 127, 127) shouldBe gap(10, 127) - - // Two gaps - world.getBlockAt(0, 20, 0).type = Material.STONE - findGapAt(0, 127, 15) shouldBe gap(10, 20) - findGapAt(0, 127, 30) shouldBe gap(20, 127) - - // Find nearest gap with two blocks - world.getBlockAt(0, 21, 0).type = Material.STONE - findGapAt(0, 127, 20) shouldBe gap(10, 20) - findGapAt(0, 127, 21) shouldBe gap(21, 127) - - // min/max tests - findGapAt(15, 127, 20) shouldBe gap(15, 20) - findGapAt(20, 40, 20) shouldBe gap(21, 40) - findGapAt(40, 50, 45) shouldBe gap(40, 50) - - // Water - world.getBlockAt(0, 10, 0).type = Material.WATER - world.getBlockAt(0, 9, 0).type = Material.WATER - findGapAt(0, 127, 0) shouldBe gap(10, 20) - findGapAt(0, 127, 15) shouldBe gap(10, 20) - } -}