Skip to content
This repository has been archived by the owner on May 27, 2024. It is now read-only.

Commit

Permalink
feat(spawning): Spawn categories
Browse files Browse the repository at this point in the history
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
  • Loading branch information
0ffz committed Mar 30, 2024
1 parent ce016d0 commit 692316f
Show file tree
Hide file tree
Showing 21 changed files with 377 additions and 492 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<Boolean, SetRemoveWhenFarAway>(
"mobzy:set.remove_when_far_away",
Boolean.serializer(),
{ SetRemoveWhenFarAway(it) },
{ it.value },
)

companion object : ComponentDefinition by EventHelpers.defaultTo<OnSpawn>()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -13,7 +12,8 @@ val mobzySpawning by DI.observe<MobzySpawnFeature.Context>()

class MobzySpawnFeature : FeatureWithContext<MobzySpawnFeature.Context>(::Context) {
class Context : Configurable<SpawnConfig> {
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<WorldGuardSpawnFlags>()
Expand Down
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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<Entity>): 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
}
}
Original file line number Diff line number Diff line change
@@ -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<Chunk>): MutableMap<GearyEntity, Int> {
val count = mutableMapOf<GearyEntity, Int>()
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
}
}
}
Original file line number Diff line number Diff line change
@@ -1,42 +1,36 @@
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.
* @property spawnHeightRange The maximum amount above or below players that mobs can spawn.
*/
@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<PrefabKey, Int> = mapOf(),
val localMobCap: Map<PrefabKey, Int> = 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<PrefabKey, SpawnCategory> = 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<GearyEntity>): Map<GearyEntity, Int> = types
.intersect(localMobCapEntities.keys)
.associateWith { localMobCapEntities[it]!! }
val spawnCategoryEntities = spawnCategories.mapKeys { it.key.toEntity() }
}

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -13,14 +14,15 @@ 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
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
Expand Down Expand Up @@ -85,6 +87,10 @@ class SpawnPriority(
{ SpawnPriority(it) },
{ it.priority },
)

companion object {
val DEFAULT = SpawnPriority(50.0)
}
}

/**
Expand All @@ -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
}
}


Expand All @@ -114,6 +124,11 @@ data class DoSpawn(
val location: Location
)

data class Spawned(
val amount: Int,
val prefabs: Collection<GearyEntity>
)

@AutoScan
fun GearyModule.spawnRequestListener() = listener(object : ListenerQuery() {
val type by get<SpawnType>()
Expand All @@ -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<BoundingBox>()

// 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
}


Expand Down
Loading

0 comments on commit 692316f

Please sign in to comment.