Don't know why, but all monsters do is constantly walk to left

This commit is contained in:
DBotThePony 2024-07-13 18:05:17 +07:00
parent 6f2b8b7bbb
commit a17bb2a732
Signed by: DBot
GPG Key ID: DCC23B5715498507
49 changed files with 1011 additions and 94 deletions

View File

@ -13,9 +13,11 @@ import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.defs.ActorMovementParameters
import ru.dbotthepony.kstarbound.defs.ClientConfig
import ru.dbotthepony.kstarbound.defs.CurrencyDefinition
import ru.dbotthepony.kstarbound.defs.ElementalDamageType
import ru.dbotthepony.kstarbound.defs.MovementParameters
import ru.dbotthepony.kstarbound.defs.world.SpawnerConfig
import ru.dbotthepony.kstarbound.defs.UniverseServerConfig
import ru.dbotthepony.kstarbound.defs.WorldServerConfig
import ru.dbotthepony.kstarbound.defs.world.WorldServerConfig
import ru.dbotthepony.kstarbound.defs.actor.player.PlayerConfig
import ru.dbotthepony.kstarbound.defs.actor.player.ShipUpgrades
import ru.dbotthepony.kstarbound.defs.item.ItemDropConfig
@ -34,7 +36,6 @@ import ru.dbotthepony.kstarbound.defs.world.SystemWorldConfig
import ru.dbotthepony.kstarbound.defs.world.SystemWorldObjectConfig
import ru.dbotthepony.kstarbound.defs.world.WorldTemplateConfig
import ru.dbotthepony.kstarbound.json.listAdapter
import ru.dbotthepony.kstarbound.json.mapAdapter
import ru.dbotthepony.kstarbound.util.AssetPathStack
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Future
@ -122,6 +123,12 @@ object Globals {
var quests by Delegates.notNull<QuestGlobalConfig>()
private set
var spawner by Delegates.notNull<SpawnerConfig>()
private set
var elementalTypes by Delegates.notNull<ImmutableMap<String, ElementalDamageType>>()
private set
private var profanityFilterInternal by Delegates.notNull<ImmutableList<String>>()
val profanityFilter: ImmutableSet<String> by lazy {
@ -229,11 +236,13 @@ object Globals {
tasks.add(load("/tiles/defaultDamage.config", ::tileDamage))
tasks.add(load("/ships/shipupgrades.config", ::shipUpgrades))
tasks.add(load("/quests/quests.config", ::quests))
tasks.add(load("/spawning.config", ::spawner))
tasks.add(Starbound.GLOBAL_SCOPE.launch { load("/dungeon_worlds.config", ::dungeonWorlds, mapAdapter("/dungeon_worlds.config")) }.asCompletableFuture())
tasks.add(Starbound.GLOBAL_SCOPE.launch { load("/currencies.config", ::currencies, mapAdapter("/currencies.config")) }.asCompletableFuture())
tasks.add(Starbound.GLOBAL_SCOPE.launch { load("/system_objects.config", ::systemObjects, mapAdapter("/system_objects.config")) }.asCompletableFuture())
tasks.add(Starbound.GLOBAL_SCOPE.launch { load("/instance_worlds.config", ::instanceWorlds, mapAdapter("/instance_worlds.config")) }.asCompletableFuture())
tasks.add(Starbound.GLOBAL_SCOPE.launch { load("/damage/elementaltypes.config", ::elementalTypes, mapAdapter("/damage/elementaltypes.config")) }.asCompletableFuture())
tasks.add(Starbound.GLOBAL_SCOPE.launch { load("/names/profanityfilter.config", ::profanityFilterInternal, lazy { Starbound.gson.listAdapter() }) }.asCompletableFuture())

View File

@ -3,6 +3,7 @@ package ru.dbotthepony.kstarbound
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap
import com.google.gson.GsonBuilder
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapterFactory
@ -15,6 +16,7 @@ import kotlinx.coroutines.launch
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.defs.AssetReference
import ru.dbotthepony.kstarbound.defs.DamageKind
import ru.dbotthepony.kstarbound.defs.Json2Function
import ru.dbotthepony.kstarbound.defs.JsonConfigFunction
import ru.dbotthepony.kstarbound.defs.JsonFunction
@ -47,6 +49,7 @@ import ru.dbotthepony.kstarbound.defs.world.BushVariant
import ru.dbotthepony.kstarbound.defs.world.GrassVariant
import ru.dbotthepony.kstarbound.defs.world.TreeVariant
import ru.dbotthepony.kstarbound.defs.world.BiomeDefinition
import ru.dbotthepony.kstarbound.defs.world.SpawnType
import ru.dbotthepony.kstarbound.item.ItemRegistry
import ru.dbotthepony.kstarbound.json.JsonPatch
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
@ -91,6 +94,7 @@ object Registries {
val treasureChests = Registry<TreasureChestDefinition>("treasure chest").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val monsterSkills = Registry<MonsterSkillDefinition>("monster skill").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val monsterTypes = Registry<MonsterTypeDefinition>("monster type").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val spawnTypes = Registry<SpawnType>("spawn type").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val monsterPalettes = Registry<MonsterPaletteSwap>("monster palette").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val behavior = Registry<BehaviorDefinition>("behavior").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val behaviorNodes = Registry<BehaviorNodeDefinition>("behavior node").also(registriesInternal::add).also { adapters.add(it.adapter()) }
@ -103,6 +107,7 @@ object Registries {
val bushVariants = Registry<BushVariant.Data>("bush variant").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val dungeons = Registry<DungeonDefinition>("dungeon").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val markovGenerators = Registry<MarkovTextGenerator>("markov text generator").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val damageKinds = Registry<DamageKind>("damage kind").also(registriesInternal::add).also { adapters.add(it.adapter()) }
private val monsterParts = HashMap<Pair<String, String>, HashMap<String, Pair<MonsterPartDefinition, IStarboundFile>>>()
private val loggedMisses = Collections.synchronizedSet(ObjectOpenHashSet<Pair<String, String>>())
@ -240,6 +245,7 @@ object Registries {
tasks.addAll(loadRegistry(markovGenerators, patchTree, fileTree["namesource"] ?: listOf(), key(MarkovTextGenerator::name)))
tasks.addAll(loadRegistry(projectiles, patchTree, fileTree["projectile"] ?: listOf(), key(ProjectileDefinition::projectileName)))
tasks.addAll(loadRegistry(behavior, patchTree, fileTree["behavior"] ?: listOf(), key(BehaviorDefinition::name)))
tasks.addAll(loadRegistry(damageKinds, patchTree, fileTree["damage"] ?: listOf(), key(DamageKind::kind)))
tasks.addAll(loadCombined(behaviorNodes, fileTree["nodes"] ?: listOf(), patchTree))
@ -249,6 +255,10 @@ object Registries {
tasks.addAll(loadCombined(treasureChests, fileTree["treasurechests"] ?: listOf(), patchTree) { name = it })
tasks.addAll(loadCombined(treasurePools, fileTree["treasurepools"] ?: listOf(), patchTree) { name = it })
// because someone couldn't handle their mushroom vine that day, and decided to make third way of
// declaring game data
tasks.addAll(loadMixed(spawnTypes, fileTree["spawntypes"] ?: listOf(), patchTree, SpawnType::name))
return tasks
}
@ -279,6 +289,33 @@ object Registries {
}
}
private inline fun <reified T : Any> loadMixed(registry: Registry<T>, files: Collection<IStarboundFile>, patches: Map<String, Collection<IStarboundFile>>, noinline key: T.() -> String): List<Future<*>> {
val adapter by lazy { Starbound.gson.getAdapter(T::class.java) }
return files.map { listedFile ->
Starbound.GLOBAL_SCOPE.launch {
try {
val json = JsonPatch.applyAsync(listedFile.asyncJsonReader(), patches[listedFile.computeFullPath()]) as JsonArray
for ((i, v) in json.withIndex()) {
try {
val value = adapter.fromJsonTreeFast(v)
val getKey = key(value)
Starbound.submit {
registry.add(getKey, value, v, listedFile)
}.exceptionally { err -> LOGGER.error("Loading ${registry.name} definition at name '$getKey' from file $listedFile", err); null }
} catch (err: Exception) {
LOGGER.error("Loading ${registry.name} definition at index $i from file $listedFile", err)
}
}
} catch (err: Exception) {
LOGGER.error("Loading ${registry.name} definition $listedFile", err)
}
}.asCompletableFuture()
}
}
private suspend fun loadTerrainSelector(listedFile: IStarboundFile, type: TerrainSelectorType?, patches: Map<String, Collection<IStarboundFile>>) {
try {
val json = JsonPatch.applyAsync(listedFile.asyncJsonReader(), patches[listedFile.computeFullPath()]) as JsonObject

View File

@ -20,7 +20,7 @@ class ColorReplacements private constructor(private val mapping: Int2IntOpenHash
}
fun toImageOperator(): String {
return "replace;${mapping.int2IntEntrySet().joinToString { "${RGBAColor.rgb(it.intKey).toHexStringRGB()}=${RGBAColor.rgb(it.intValue).toHexStringRGB()}" }}"
return "replace;${mapping.int2IntEntrySet().joinToString(";") { "${RGBAColor.rgb(it.intKey).toHexStringRGB().substring(1)}=${RGBAColor.rgb(it.intValue).toHexStringRGB().substring(1)}" }}"
}
class Adapter(gson: Gson) : TypeAdapter<ColorReplacements>() {

View File

@ -1,8 +1,10 @@
package ru.dbotthepony.kstarbound.defs
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap
import com.google.common.collect.ImmutableSet
import com.google.gson.Gson
import com.google.gson.JsonObject
import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapter
import com.google.gson.annotations.JsonAdapter
@ -81,6 +83,13 @@ enum class DamageType(override val jsonName: String) : IStringSerializable {
STATUS("Environment");
}
@JsonFactory
data class DamageKind(
val kind: String,
val elementalType: String = "default",
val effects: JsonObject = JsonObject()
)
@JsonFactory
data class EntityDamageTeam(val type: TeamType = TeamType.NULL, val team: Int = 0) {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(TeamType.entries[stream.readUnsignedByte()], stream.readUnsignedShort())
@ -189,10 +198,10 @@ data class DamageData(
val hitType: HitType,
val damageType: DamageType,
val damage: Double,
val knockback: Vector2d,
val knockbackMomentum: Vector2d,
val sourceEntityId: Int,
val inflictorEntityId: Int = 0,
val kind: String,
val damageSourceKind: String,
val statusEffects: Collection<EphemeralStatusEffect>,
) {
constructor(stream: DataInputStream, isLegacy: Boolean, inflictorEntityId: Int) : this(
@ -213,9 +222,9 @@ data class DamageData(
stream.writeEnumStupid(hitType.ordinal, isLegacy)
stream.writeByte(damageType.ordinal)
stream.writeDouble(damage, isLegacy)
stream.writeStruct2d(knockback, isLegacy)
stream.writeStruct2d(knockbackMomentum, isLegacy)
stream.writeInt(sourceEntityId)
stream.writeBinaryString(kind)
stream.writeBinaryString(damageSourceKind)
stream.writeCollection(statusEffects) { it.write(this, isLegacy) }
}
}
@ -363,3 +372,5 @@ data class DamageSource(
val LEGACY_CODEC = legacyCodec(::DamageSource, DamageSource::write)
}
}
data class ElementalDamageType(val resistanceStat: String, val damageNumberParticles: ImmutableMap<HitType, String>)

View File

@ -13,6 +13,10 @@ import ru.dbotthepony.kstarbound.world.entities.behavior.NodeParameterType
@JsonAdapter(NodeParameter.Adapter::class)
data class NodeParameter(val type: NodeParameterType, val value: NodeParameterValue) {
override fun toString(): String {
return "[$type as $value]"
}
class Adapter : TypeAdapter<NodeParameter>() {
override fun write(out: JsonWriter, value: NodeParameter) {
out.beginObject()

View File

@ -17,6 +17,13 @@ import ru.dbotthepony.kstarbound.json.popObject
*/
@JsonAdapter(NodeParameterValue.Adapter::class)
data class NodeParameterValue(val key: String?, val value: JsonElement?) {
override fun toString(): String {
if (key != null)
return "key=$key"
else
return "value=$value"
}
class Adapter : TypeAdapter<NodeParameterValue>() {
override fun write(out: JsonWriter, value: NodeParameterValue) {
if (value.key != null) {

View File

@ -256,6 +256,7 @@ data class MonsterTypeDefinition(
if (skillNames.isNotEmpty()) {
animationConfig = animationConfig.deepCopy()
val allParameters = ArrayList<JsonElement>()
allParameters.add(parameters)
for (skillName in skillNames) {
val skill = Registries.monsterSkills[skillName] ?: continue

View File

@ -37,7 +37,7 @@ import java.io.DataOutputStream
import java.util.random.RandomGenerator
@JsonFactory
class MonsterVariant(
data class MonsterVariant(
val type: String,
val shortDescription: String? = null,
val description: String? = null,
@ -116,7 +116,7 @@ class MonsterVariant(
val actualDropPools: ImmutableList<Either<ImmutableMap<String, Registry.Ref<TreasurePoolDefinition>>, Registry.Ref<TreasurePoolDefinition>>>
get() = commonParameters.dropPools ?: dropPools
val chosenDropPool: Either<ImmutableMap<String, Registry.Ref<TreasurePoolDefinition>>, Registry.Ref<TreasurePoolDefinition>>? = if (actualDropPools.isEmpty()) null else actualDropPools[staticRandomInt(0, actualDropPools.size, seed, "MonsterDropPool")]
val chosenDropPool: Either<ImmutableMap<String, Registry.Ref<TreasurePoolDefinition>>, Registry.Ref<TreasurePoolDefinition>>? = if (actualDropPools.isEmpty()) null else actualDropPools[staticRandomInt(0, actualDropPools.size - 1, seed, "MonsterDropPool")]
fun write(stream: DataOutputStream, isLegacy: Boolean) {
if (isLegacy) {

View File

@ -21,6 +21,7 @@ data class Biome(
val surfacePlaceables: BiomePlaceables = BiomePlaceables(),
val undergroundPlaceables: BiomePlaceables = BiomePlaceables(),
val parallax: Parallax? = null,
val spawnProfile: SpawnProfile? = null,
) {
@JvmName("hueShiftTile")
fun hueShift(block: Registry.Entry<TileDefinition>): Float {

View File

@ -2,6 +2,8 @@ package ru.dbotthepony.kstarbound.defs.world
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableSet
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import ru.dbotthepony.kommons.collect.filterNotNull
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry
@ -35,6 +37,7 @@ data class BiomeDefinition(
val surfacePlaceables: BiomePlaceablesDefinition = BiomePlaceablesDefinition(),
val undergroundPlaceables: BiomePlaceablesDefinition = BiomePlaceablesDefinition(),
val parallax: AssetReference<Parallax.Data>? = null,
val spawnProfile: JsonObject? = null,
) {
data class CreationParams(
val hueShift: Double,
@ -83,7 +86,9 @@ data class BiomeDefinition(
.map { NativeLegacy.TileMod(Registries.tileModifiers.ref(it.first)) to it.second }
.filter { it.first.native.isPresent }
.collect(ImmutableList.toImmutableList())
}?.orElse(ImmutableList.of()) ?: ImmutableList.of())
}?.orElse(ImmutableList.of()) ?: ImmutableList.of()),
spawnProfile = spawnProfile?.let { SpawnProfile.create(it, random) }
)
}

View File

@ -438,6 +438,7 @@ data class SkyGlobalConfig(
val starVelocityFactor: Double = 0.0,
val flyingTimer: Double = 0.0,
val flashTimer: Double = 1.0,
val dayTransitionTime: Double = 200.0,
) {
@JsonFactory
data class Stars(

View File

@ -0,0 +1,18 @@
package ru.dbotthepony.kstarbound.defs.world
import com.google.common.collect.ImmutableSet
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
import java.util.Collections
import java.util.EnumSet
enum class SpawnArea(override val jsonName: String) : IStringSerializable {
SURFACE("surface"),
CEILING("ceiling"),
AIR("air"),
LIQUID("liquid"),
SOLID("solid");
companion object {
val ALL: Set<SpawnArea> = Collections.unmodifiableSet(EnumSet.allOf(SpawnArea::class.java))
}
}

View File

@ -0,0 +1,53 @@
package ru.dbotthepony.kstarbound.defs.world
import com.google.gson.TypeAdapter
import com.google.gson.annotations.JsonAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kstarbound.json.popObject
import ru.dbotthepony.kstarbound.json.stream
import ru.dbotthepony.kstarbound.util.valueOf
import java.util.EnumSet
import java.util.stream.Collectors
@JsonAdapter(SpawnParameters.Adapter::class)
data class SpawnParameters(val areas: Set<SpawnArea> = SpawnArea.ALL, val region: SpawnRegion = SpawnRegion.ALL, val time: SpawnTime = SpawnTime.ALL) {
fun isCompatible(other: SpawnParameters): Boolean {
return region.isCompatible(other.region) && time.isCompatible(other.time) && areas.any { it in other.areas }
}
class Adapter : TypeAdapter<SpawnParameters>() {
override fun write(out: JsonWriter, value: SpawnParameters) {
out.beginObject()
out.name("areas")
out.beginArray()
value.areas.forEach { out.value(it.jsonName) }
out.endArray()
out.name("region")
out.value(value.region.jsonName)
out.name("time")
out.value(value.time.jsonName)
out.endObject()
}
override fun read(`in`: JsonReader): SpawnParameters {
val json = `in`.popObject()
val areas = if ("area" in json) {
if (json["area"].asString == "all")
SpawnArea.ALL
else
setOf(SpawnArea.entries.valueOf(json["area"].asString))
} else if ("areas" in json) {
json["areas"].asJsonArray.stream().map { SpawnArea.entries.valueOf(it.asString) }.collect(Collectors.toCollection { EnumSet.noneOf(SpawnArea::class.java) })
} else {
setOf()
}
val region = SpawnRegion.entries.valueOf(json["region"].asString)
val time = SpawnTime.entries.valueOf(json["time"].asString)
return SpawnParameters(areas, region, time)
}
}
}

View File

@ -0,0 +1,39 @@
package ru.dbotthepony.kstarbound.defs.world
import com.google.common.collect.ImmutableSet
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.fromJsonFast
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import java.util.random.RandomGenerator
@JsonFactory
data class SpawnProfile(val spawnTypes: ImmutableSet<Registry.Ref<SpawnType>> = ImmutableSet.of(), val monsterParameters: JsonObject = JsonObject()) {
companion object {
fun create(json: JsonObject, random: RandomGenerator): SpawnProfile {
val spawnTypes = ArrayList<Registry.Ref<SpawnType>>()
if ("groups" in json) {
for (group in json["groups"].asJsonArray) {
val pool = group.asJsonObject["pool"]
val select = group.asJsonObject["select"].asJsonPrimitive.asInt
val typePool = if (pool is JsonPrimitive) {
Globals.spawner.spawnGroups[pool.asString] ?: throw NoSuchElementException("No such common spawn group with name $pool")
} else {
Starbound.gson.fromJsonFast(pool)
}
spawnTypes.addAll(typePool.sample(select, random))
}
}
return SpawnProfile(ImmutableSet.copyOf(spawnTypes), json["monsterParameters"] as? JsonObject ?: JsonObject())
}
}
}

View File

@ -0,0 +1,25 @@
package ru.dbotthepony.kstarbound.defs.world
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
enum class SpawnRegion(override val jsonName: String, val enclosed: Boolean, val exposed: Boolean) : IStringSerializable {
ALL("all", true, true) {
override fun isCompatible(other: SpawnRegion): Boolean {
return true
}
},
ENCLOSED("enclosed", true, false) {
override fun isCompatible(other: SpawnRegion): Boolean {
return other.enclosed
}
},
EXPOSED("exposed", false, true){
override fun isCompatible(other: SpawnRegion): Boolean {
return other.exposed
}
};
abstract fun isCompatible(other: SpawnRegion): Boolean
}

View File

@ -0,0 +1,25 @@
package ru.dbotthepony.kstarbound.defs.world
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
enum class SpawnTime(override val jsonName: String, val day: Boolean, val night: Boolean) : IStringSerializable {
ALL("all", true, true) {
override fun isCompatible(other: SpawnTime): Boolean {
return true
}
},
DAY("day", true, false) {
override fun isCompatible(other: SpawnTime): Boolean {
return other.day
}
},
NIGHT("night", false, true) {
override fun isCompatible(other: SpawnTime): Boolean {
return other.night
}
};
abstract fun isCompatible(other: SpawnTime): Boolean
}

View File

@ -0,0 +1,42 @@
package ru.dbotthepony.kstarbound.defs.world
import com.google.gson.JsonObject
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kommons.util.XXHash64
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.collect.WeightedList
import ru.dbotthepony.kstarbound.defs.monster.MonsterTypeDefinition
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.math.vector.Vector2i
@JsonFactory
data class SpawnType(
val name: String,
val dayLevelAdjustment: Vector2d = Vector2d.ZERO,
val nightLevelAdjustment: Vector2d = Vector2d.ZERO,
val monsterType: Either<WeightedList<Registry.Ref<MonsterTypeDefinition>>, Registry.Ref<MonsterTypeDefinition>>,
val monsterParameters: JsonObject = JsonObject(),
val spawnParameters: SpawnParameters = SpawnParameters(),
val groupSize: Vector2i = Vector2i.POSITIVE_XY,
val spawnChance: Double,
// tard
@Deprecated("Raw property", replaceWith = ReplaceWith("this.actualSeedMix"))
val seedMix: Long? = null,
) {
val actualSeedMix: Long
init {
if (monsterType.isLeft && monsterType.left().isEmpty)
throw IllegalArgumentException("monsterType is empty")
if (seedMix != null) {
actualSeedMix = seedMix
} else {
val hasher = XXHash64()
hasher.update(name.toByteArray(Charsets.UTF_8))
actualSeedMix = hasher.digestAsLong()
}
}
}

View File

@ -0,0 +1,48 @@
package ru.dbotthepony.kstarbound.defs.world
import com.google.common.collect.ImmutableMap
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.collect.WeightedList
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
@JsonFactory
data class SpawnerConfig(
val spawnCellSize: Int,
val spawnCellMinimumEmptyTiles: Int,
val spawnCellMinimumLiquidTiles: Int,
val spawnCellMinimumNearSurfaceTiles: Int,
val spawnCellMinimumNearCeilingTiles: Int,
val spawnCellMinimumAirTiles: Int,
val spawnCellMinimumExposedTiles: Int,
val spawnCellNearSurfaceDistance: Int,
val spawnCellNearCeilingDistance: Int,
val minimumDayLevel: Double,
val minimumLiquidLevel: Double,
val spawnCheckResolution: Double,
val spawnSurfaceCheckDistance: Int,
val spawnCeilingCheckDistance: Int,
val spawnProhibitedCheckPadding: Double,
val spawnCellLifetime: Double,
val windowActivationBorder: Int,
val defaultActive: Boolean = true,
val debug: Boolean = false,
val spawnGroups: ImmutableMap<String, WeightedList<Registry.Ref<SpawnType>>> = ImmutableMap.of(),
) {
init {
require(spawnCellSize >= 1) { "Bad spawnCellSize: $spawnCellSize" }
require(spawnCellMinimumEmptyTiles >= 0) { "Negative spawnCellMinimumEmptyTiles: $spawnCellMinimumEmptyTiles" }
require(spawnCellMinimumLiquidTiles >= 0) { "Negative spawnCellMinimumLiquidTiles: $spawnCellMinimumLiquidTiles" }
require(spawnCellMinimumNearSurfaceTiles >= 0) { "Negative spawnCellMinimumNearSurfaceTiles: $spawnCellMinimumNearSurfaceTiles" }
require(spawnCellMinimumNearCeilingTiles >= 0) { "Negative spawnCellMinimumNearCeilingTiles: $spawnCellMinimumNearCeilingTiles" }
require(spawnCellMinimumAirTiles >= 0) { "Negative spawnCellMinimumAirTiles: $spawnCellMinimumAirTiles" }
require(spawnCellMinimumExposedTiles >= 0) { "Negative spawnCellMinimumExposedTiles: $spawnCellMinimumExposedTiles" }
require(spawnCellNearSurfaceDistance >= 0) { "Negative spawnCellNearSurfaceDistance: $spawnCellNearSurfaceDistance" }
require(spawnCellNearCeilingDistance >= 0) { "Negative spawnCellNearCeilingDistance: $spawnCellNearCeilingDistance" }
require(windowActivationBorder >= 0) { "Negative windowActivationBorder: $windowActivationBorder" }
}
}

View File

@ -1,7 +1,6 @@
package ru.dbotthepony.kstarbound.defs
package ru.dbotthepony.kstarbound.defs.world
import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.math.vector.Vector2i
class WorldServerConfig(
val playerSpaceStartRegionSize: Vector2d,

View File

@ -1,15 +1,16 @@
package ru.dbotthepony.kstarbound.json.factory
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap
import com.google.gson.JsonPrimitive
import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kstarbound.json.FastJsonTreeReader
class ImmutableArrayMapTypeAdapter<K, V>(val keyAdapter: TypeAdapter<K>, val elementAdapter: TypeAdapter<V>) : TypeAdapter<ImmutableMap<K, V>>() {
class Immutable2MapTypeAdapter<K, V>(val keyAdapter: TypeAdapter<K>, val elementAdapter: TypeAdapter<V>) : TypeAdapter<ImmutableMap<K, V>>() {
override fun write(out: JsonWriter, value: ImmutableMap<K, V>?) {
if (value == null) {
out.nullValue()
@ -30,6 +31,21 @@ class ImmutableArrayMapTypeAdapter<K, V>(val keyAdapter: TypeAdapter<K>, val ele
if (reader.consumeNull())
return null
if (reader.peek() == JsonToken.BEGIN_OBJECT) {
reader.beginObject()
val builder = ImmutableMap.Builder<K, V>()
while (reader.peek() !== JsonToken.END_OBJECT) {
builder.put(
keyAdapter.read(FastJsonTreeReader(JsonPrimitive(reader.nextName()))) ?: throw JsonSyntaxException("Nulls are not allowed, near ${reader.path}"),
elementAdapter.read(reader) ?: throw JsonSyntaxException("Nulls are not allowed, near ${reader.path}"))
}
reader.endObject()
return builder.build()
}
reader.beginArray()
val builder = ImmutableMap.Builder<K, V>()

View File

@ -31,7 +31,7 @@ class ImmutableCollectionAdapterFactory(val stringInterner: Interner<String> = I
return ImmutableMapTypeAdapter(stringInterner, gson.getAdapter(TypeToken.get(elementType1))) as TypeAdapter<T>
}
return ImmutableArrayMapTypeAdapter(
return Immutable2MapTypeAdapter(
gson.getAdapter(TypeToken.get(elementType0)),
gson.getAdapter(TypeToken.get(elementType1))
) as TypeAdapter<T>

View File

@ -18,6 +18,10 @@ import org.classdump.luna.runtime.Dispatch
import org.classdump.luna.runtime.ExecutionContext
import org.classdump.luna.runtime.LuaFunction
import org.classdump.luna.runtime.UnresolvedControlThrowable
import java.util.Spliterator
import java.util.Spliterators
import java.util.stream.Stream
import java.util.stream.StreamSupport
import kotlin.math.max
import kotlin.math.min
@ -111,6 +115,14 @@ operator fun Table.iterator(): Iterator<Map.Entry<Any, Any>> {
}
}
fun Table.spliterator(): Spliterator<Map.Entry<Any, Any>> {
return Spliterators.spliteratorUnknownSize(iterator(), Spliterator.ORDERED)
}
fun Table.stream(): Stream<Map.Entry<Any, Any>> {
return StreamSupport.stream(spliterator(), false)
}
/**
* to be used in places where we need to "unpack" table, like this:
*

View File

@ -235,7 +235,11 @@ class LuaEnvironment : StateContext {
}
fun attach(script: ChunkFactory) {
scripts.add(script)
if (initCalled) {
executor.call(this@LuaEnvironment, script.newInstance(Variable(globals)))
} else {
scripts.add(script)
}
}
fun attach(scripts: Collection<AssetPath>) {
@ -248,7 +252,9 @@ class LuaEnvironment : StateContext {
}
} else {
for (script in scripts) {
attach(Starbound.loadScript(script.fullPath))
if (loadedScripts.add(script.fullPath)) {
this.scripts.add(Starbound.loadScript(script.fullPath))
}
}
}
}
@ -274,7 +280,7 @@ class LuaEnvironment : StateContext {
try {
executor.call(this, script.newInstance(Variable(globals)))
} catch (err: Throwable) {
errorState = true
// errorState = true
LOGGER.error("Failed to attach script to environment", err)
scripts.clear()
return false
@ -290,7 +296,7 @@ class LuaEnvironment : StateContext {
try {
executor.call(this, init)
} catch (err: Throwable) {
errorState = true
// errorState = true
LOGGER.error("Exception on init()", err)
return false
}
@ -310,7 +316,7 @@ class LuaEnvironment : StateContext {
return try {
executor.call(this, load, *arguments)
} catch (err: Throwable) {
errorState = true
// errorState = true
LOGGER.error("Exception while calling global $name", err)
arrayOf()
}

View File

@ -16,6 +16,7 @@ import ru.dbotthepony.kstarbound.lua.get
import ru.dbotthepony.kstarbound.lua.iterator
import ru.dbotthepony.kstarbound.lua.luaFunction
import ru.dbotthepony.kstarbound.lua.set
import ru.dbotthepony.kstarbound.lua.stream
import ru.dbotthepony.kstarbound.lua.toJson
import ru.dbotthepony.kstarbound.lua.toJsonFromLua
import ru.dbotthepony.kstarbound.lua.toVector2d
@ -76,7 +77,7 @@ fun provideMonsterBindings(self: MonsterEntity, lua: LuaEnvironment) {
if (parts == null) {
self.animationDamageParts.clear()
} else {
val strings = parts.iterator().map { (_, v) -> v.toString() }.collect(ImmutableSet.toImmutableSet())
val strings = parts.stream().map { (_, v) -> v.toString() }.collect(ImmutableSet.toImmutableSet())
self.animationDamageParts.removeIf { it !in strings }
for (v in strings) {

View File

@ -1,5 +1,7 @@
package ru.dbotthepony.kstarbound.lua.bindings
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap
import org.apache.logging.log4j.LogManager
import org.classdump.luna.ByteString
@ -415,6 +417,32 @@ private val itemHasTag = luaFunction { identifier: ByteString, tag: ByteString -
returnBuffer.setTo(tag.decode() in ItemRegistry[identifier.decode()].itemTags)
}
private val monsterSkillParameter = luaFunction { skillName: ByteString, configParameterName: ByteString ->
val skill = Registries.monsterSkills[skillName.decode()]
if (skill != null) {
returnBuffer.setTo(from(skill.value.config[configParameterName.decode()] ?: JsonNull.INSTANCE))
}
}
private val monsterParameters = luaFunction { monsterType: ByteString, seed: Number? ->
returnBuffer.setTo(from(Registries.monsterTypes.getOrThrow(monsterType.decode()).value.create(seed?.toLong() ?: 0L, JsonObject()).parameters))
}
private val monsterMovementSettings = luaFunction { monsterType: ByteString, seed: Number? ->
returnBuffer.setTo(from(Registries.monsterTypes.getOrThrow(monsterType.decode()).value.create(seed?.toLong() ?: 0L, JsonObject()).parameters["movementSettings"] ?: JsonObject()))
}
private val elementalResistance = luaFunction { damageKindName: ByteString ->
returnBuffer.setTo(Globals.elementalTypes[Registries.damageKinds.getOrThrow(damageKindName.decode()).value.elementalType]!!.resistanceStat.toByteString())
}
private val dungeonMetadata = luaFunction { dungeon: ByteString ->
returnBuffer.setTo(from(Registries.dungeons.getOrThrow(dungeon.decode()).jsonObject["metadata"]))
}
private val hasTech = registryDefExists(Registries.techs)
fun provideRootBindings(lua: LuaEnvironment) {
val table = lua.newTable()
lua.globals["root"] = table
@ -481,11 +509,11 @@ fun provideRootBindings(lua: LuaEnvironment) {
table["createBiome"] = createBiome
table["monsterSkillParameter"] = luaStub("monsterSkillParameter")
table["monsterParameters"] = luaStub("monsterParameters")
table["monsterMovementSettings"] = luaStub("monsterMovementSettings")
table["monsterSkillParameter"] = monsterSkillParameter
table["monsterParameters"] = monsterParameters
table["monsterMovementSettings"] = monsterMovementSettings
table["hasTech"] = registryDefExists(Registries.techs)
table["hasTech"] = hasTech
table["techType"] = techType
table["techConfig"] = techConfig
@ -494,7 +522,6 @@ fun provideRootBindings(lua: LuaEnvironment) {
table["collection"] = luaStub("collection")
table["collectables"] = luaStub("collectables")
table["elementalResistance"] = luaStub("elementalResistance")
table["dungeonMetadata"] = luaStub("dungeonMetadata")
table["behavior"] = luaStub("behavior")
table["elementalResistance"] = elementalResistance
table["dungeonMetadata"] = dungeonMetadata
}

View File

@ -63,12 +63,12 @@ fun provideStatusControllerBindings(self: StatusController, lua: LuaEnvironment)
}
callbacks["resource"] = luaFunction { name: ByteString ->
val resource = self.resources[name.decode()] ?: throw LuaRuntimeException("No such resource $name")
val resource = self.resources[name.decode()] ?: return@luaFunction returnBuffer.setTo(0.0)
returnBuffer.setTo(resource.value)
}
callbacks["resourcePositive"] = luaFunction { name: ByteString ->
val resource = self.resources[name.decode()] ?: throw LuaRuntimeException("No such resource $name")
val resource = self.resources[name.decode()] ?: return@luaFunction returnBuffer.setTo(false)
returnBuffer.setTo(resource.value > 0.0)
}
@ -189,20 +189,20 @@ fun provideStatusControllerBindings(self: StatusController, lua: LuaEnvironment)
callbacks["damageTakenSince"] = luaFunction { since: Number? ->
val (list, newSince) = self.recentDamageReceived(since?.toLong() ?: 0L)
returnBuffer.setTo(tableOf(*list.map { Starbound.gson.toJsonTree(it) }.toTypedArray()), newSince)
returnBuffer.setTo(tableOf(*list.map { from(Starbound.gson.toJsonTree(it)) }.toTypedArray()), newSince)
}
callbacks["inflictedHitsSince"] = luaFunction { since: Number? ->
val (list, newSince) = self.recentHitsDealt(since?.toLong() ?: 0L)
returnBuffer.setTo(tableOf(*list.map { p ->
Starbound.gson.toJsonTree(p.second).also { it as JsonObject; it["targetEntityId"] = p.first }
from(Starbound.gson.toJsonTree(p.second).also { it as JsonObject; it["targetEntityId"] = p.first })
}.toTypedArray()), newSince)
}
callbacks["inflictedDamageSince"] = luaFunction { since: Number? ->
val (list, newSince) = self.recentDamageDealt(since?.toLong() ?: 0L)
returnBuffer.setTo(tableOf(*list.map { Starbound.gson.toJsonTree(it) }.toTypedArray()), newSince)
returnBuffer.setTo(tableOf(*list.map { from(Starbound.gson.toJsonTree(it)) }.toTypedArray()), newSince)
}
callbacks["activeUniqueStatusEffectSummary"] = luaFunction {
@ -217,8 +217,8 @@ fun provideStatusControllerBindings(self: StatusController, lua: LuaEnvironment)
returnBuffer.setTo(self.primaryDirectives.toByteString())
}
callbacks["setPrimaryDirectives"] = luaFunction { directives: ByteString ->
self.primaryDirectives = directives.decode().sbIntern()
callbacks["setPrimaryDirectives"] = luaFunction { directives: ByteString? ->
self.primaryDirectives = directives?.decode()?.sbIntern() ?: ""
}
callbacks["applySelfDamageRequest"] = luaFunction { damage: Table ->

View File

@ -19,6 +19,7 @@ import ru.dbotthepony.kstarbound.lua.nextOptionalFloat
import ru.dbotthepony.kstarbound.lua.set
import ru.dbotthepony.kstarbound.lua.toByteString
import ru.dbotthepony.kstarbound.lua.toJson
import ru.dbotthepony.kstarbound.lua.toJsonFromLua
import ru.dbotthepony.kstarbound.lua.toVector2d
import ru.dbotthepony.kstarbound.lua.userdata.LuaPerlinNoise
import ru.dbotthepony.kstarbound.lua.userdata.LuaRandomGenerator
@ -98,6 +99,10 @@ private val staticRandomI32Range = luaFunctionN("staticRandomI32Range") {
returnBuffer.setTo(staticRandomLong(min, max, *it.copyRemaining()))
}
private val mergeJson = luaFunction { a: Any?, b: Any? ->
returnBuffer.setTo(from(ru.dbotthepony.kstarbound.json.mergeJson(toJsonFromLua(a), toJsonFromLua(b))))
}
fun provideUtilityBindings(lua: LuaEnvironment) {
val table = lua.newTable()
lua.globals["sb"] = table
@ -133,6 +138,8 @@ fun provideUtilityBindings(lua: LuaEnvironment) {
table["staticRandomDoubleRange"] = staticRandomDoubleRange
table["staticRandomI32Range"] = staticRandomI32Range
table["staticRandomI64Range"] = staticRandomI32Range
table["jsonMerge"] = mergeJson
}
fun provideConfigBindings(lua: LuaEnvironment, lookup: ExecutionContext.(path: JsonPath, ifMissing: Any?) -> Any?) {

View File

@ -644,4 +644,10 @@ fun provideWorldBindings(self: World<*, *>, lua: LuaEnvironment) {
if (self is ServerWorld) {
provideServerWorldBindings(self, callbacks, lua)
}
// TODO
callbacks["debugPoint"] = luaFunction { }
callbacks["debugLine"] = luaFunction { }
callbacks["debugPoly"] = luaFunction { }
callbacks["debugText"] = luaFunction { }
}

View File

@ -25,6 +25,7 @@ import ru.dbotthepony.kstarbound.lua.toJsonFromLua
import ru.dbotthepony.kstarbound.world.entities.behavior.AbstractBehaviorNode
import ru.dbotthepony.kstarbound.world.entities.behavior.BehaviorTree
import ru.dbotthepony.kstarbound.world.entities.behavior.Blackboard
import java.util.concurrent.atomic.AtomicLong
class BehaviorState(val tree: BehaviorTree) : Userdata<BehaviorState>() {
val blackboard get() = tree.blackboard
@ -71,6 +72,10 @@ class BehaviorState(val tree: BehaviorTree) : Userdata<BehaviorState>() {
}
companion object {
// required for Lua code
// in original engine, passes directly 32/64 bit pointer of *Node struct
val NODE_GARBAGE_INDEX = AtomicLong()
private fun __index(): Table {
return metatable
}

View File

@ -47,6 +47,11 @@ data class AABB(val mins: Vector2d, val maxs: Vector2d) {
operator fun times(other: Vector2d) = AABB(mins * other, maxs * other)
operator fun div(other: Vector2d) = AABB(mins / other, maxs / other)
operator fun plus(other: Vector2i) = AABB(mins + other, maxs + other)
operator fun minus(other: Vector2i) = AABB(mins - other, maxs - other)
operator fun times(other: Vector2i) = AABB(mins * other, maxs * other)
operator fun div(other: Vector2i) = AABB(mins / other, maxs / other)
operator fun times(other: Double) = AABB(mins * other, maxs * other)
operator fun div(other: Double) = AABB(mins / other, maxs / other)
@ -314,6 +319,13 @@ data class AABB(val mins: Vector2d, val maxs: Vector2d) {
)
}
fun of(aabb: AABBi): AABB {
return AABB(
mins = Vector2d(aabb.mins.x.toDouble(), aabb.mins.y.toDouble()),
maxs = Vector2d(aabb.maxs.x.toDouble(), aabb.maxs.y.toDouble()),
)
}
@JvmField val ZERO = AABB(Vector2d.ZERO, Vector2d.ZERO)
@JvmField val NEVER = AABB(Vector2d(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY), Vector2d(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY))
}

View File

@ -382,6 +382,8 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
if (config.useUniverseClock)
world.sky.referenceClock = universeClock
world.spawner.active = config.spawningEnabled
world.eventLoop.start()
world.prepare(true).await()
} catch (err: Throwable) {
@ -439,14 +441,15 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
try {
val structure = Globals.universeServer.speciesShips[connection.playerSpecies]?.firstOrNull()?.value?.get() ?: throw NoSuchElementException("No ship structure for species ${connection.playerSpecies}")
world.eventLoop.start()
world.replaceCentralStructure(structure).join()
world.spawner.active = false
val currentUpgrades = connection.shipUpgrades
.apply(Globals.shipUpgrades)
.apply(Starbound.gson.fromJson(structure.config.get("shipUpgrades") ?: throw NoSuchElementException("No shipUpgrades element in world structure config for species ${connection.playerSpecies}")) ?: throw NullPointerException("World structure config.shipUpgrades is null for species ${connection.playerSpecies}"))
connection.shipUpgrades = currentUpgrades
world.setProperty("invinciblePlayers", JsonPrimitive(true))
world.setProperty("ship.level", JsonPrimitive(0))
world.setProperty("ship.fuel", JsonPrimitive(0))
@ -454,6 +457,9 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
world.setProperty("ship.crewSize", JsonPrimitive(currentUpgrades.crewSize))
world.setProperty("ship.fuelEfficiency", JsonPrimitive(currentUpgrades.fuelEfficiency))
world.eventLoop.start()
world.replaceCentralStructure(structure).join()
world.saveMetadata()
} catch (err: Throwable) {
world.eventLoop.shutdown()

View File

@ -0,0 +1,357 @@
package ru.dbotthepony.kstarbound.server.world
import it.unimi.dsi.fastutil.objects.Object2DoubleOpenHashMap
import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.monster.MonsterVariant
import ru.dbotthepony.kstarbound.defs.tile.NO_DUNGEON_ID
import ru.dbotthepony.kstarbound.defs.tile.isEmptyTile
import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyLiquid
import ru.dbotthepony.kstarbound.defs.world.SpawnArea
import ru.dbotthepony.kstarbound.defs.world.SpawnParameters
import ru.dbotthepony.kstarbound.defs.world.SpawnRegion
import ru.dbotthepony.kstarbound.defs.world.SpawnTime
import ru.dbotthepony.kstarbound.json.mergeJson
import ru.dbotthepony.kstarbound.math.AABB
import ru.dbotthepony.kstarbound.math.AABBi
import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.math.vector.Vector2i
import ru.dbotthepony.kstarbound.util.random.nextRange
import ru.dbotthepony.kstarbound.util.random.shuffle
import ru.dbotthepony.kstarbound.util.random.staticRandom64
import ru.dbotthepony.kstarbound.util.random.staticRandomLong
import ru.dbotthepony.kstarbound.util.supplyAsync
import ru.dbotthepony.kstarbound.world.ChunkState
import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.MonsterEntity
import ru.dbotthepony.kstarbound.world.physics.CollisionType
import java.util.Comparator
import java.util.EnumSet
import java.util.concurrent.Future
import java.util.concurrent.TimeUnit
import kotlin.jvm.optionals.getOrNull
import kotlin.math.min
class MonsterSpawner(val world: ServerWorld) {
var active = Globals.spawner.defaultActive
private val activeCells = Object2DoubleOpenHashMap<Vector2i>()
private val spawnedEntities = ArrayList<MonsterEntity>()
private var hasCleanupTask = false
private fun cell2region(cell: Vector2i): AABBi = AABBi(cell * Globals.spawner.spawnCellSize, (cell + Vector2i.POSITIVE_XY) * Globals.spawner.spawnCellSize)
private fun getSpawnParameters(cell: Vector2i, chunkMap: World<*, *>.ChunkMap): SpawnParameters {
var emptyCount = 0
var nearSurfaceCount = 0
var nearCeilingCount = 0
var airCount = 0
var liquidCount = 0
var exposedCount = 0
for (x in cell.x * Globals.spawner.spawnCellSize until (cell.x + 1) * Globals.spawner.spawnCellSize) {
for (y in cell.y * Globals.spawner.spawnCellSize until (cell.y + 1) * Globals.spawner.spawnCellSize) {
// Only empty blocks count towards spawn totals
val cell = chunkMap.getCell(x, y)
if (cell.foreground.material.value.collisionKind == CollisionType.NONE) {
emptyCount++
if (cell.liquid.state.isNotEmptyLiquid && cell.liquid.level > Globals.spawner.minimumLiquidLevel)
liquidCount++
if (cell.background.material.isEmptyTile)
exposedCount++
// The empty block will either count as an air block, a
// "near-surface" block, or a "near-ceiling" block. It will count as a
// near-surface block if it is within the NearSurfaceDistance of a
// CollsionKind::Block or CollisionKind::Platform block. If it is not a
// near-surface block, it will count as a near-ceiling block if it is
// within the NearCeilingDistance of a CollisionKind::Block.
var nearSurface = false
for (delta in 1 .. Globals.spawner.spawnCellNearSurfaceDistance) {
if (chunkMap.getCell(x, y - delta).foreground.material.value.collisionKind.isFloorCollision) {
nearSurface = true
break
}
}
var nearCeiling = false
for (delta in 1 .. Globals.spawner.spawnCellNearCeilingDistance) {
if (chunkMap.getCell(x, y + delta).foreground.material.value.collisionKind.isSolidCollision) {
nearCeiling = true
break
}
}
if (nearSurface)
nearSurfaceCount++
else if (nearCeiling)
nearCeilingCount++
else
airCount++
}
}
}
val spawnAreas = EnumSet.noneOf(SpawnArea::class.java)
if (liquidCount > Globals.spawner.spawnCellMinimumLiquidTiles)
spawnAreas.add(SpawnArea.LIQUID)
if (nearSurfaceCount > Globals.spawner.spawnCellMinimumNearSurfaceTiles)
spawnAreas.add(SpawnArea.SURFACE)
if (nearCeilingCount > Globals.spawner.spawnCellMinimumNearCeilingTiles)
spawnAreas.add(SpawnArea.CEILING)
if (airCount > Globals.spawner.spawnCellMinimumAirTiles)
spawnAreas.add(SpawnArea.AIR)
if (emptyCount < Globals.spawner.spawnCellMinimumEmptyTiles)
spawnAreas.add(SpawnArea.SOLID)
val spawnRegion = if (exposedCount >= Globals.spawner.spawnCellMinimumExposedTiles)
SpawnRegion.EXPOSED
else
SpawnRegion.ENCLOSED
val time = if (world.sky.dayLevel >= Globals.spawner.minimumDayLevel)
SpawnTime.DAY
else
SpawnTime.NIGHT
return SpawnParameters(spawnAreas, spawnRegion, time)
}
private fun selectSpawnPosition(positions: Array<Vector2i>, boundBox: AABB, spawnParameters: SpawnParameters): Vector2d? {
val allowAir = SpawnArea.AIR in spawnParameters.areas
val allowSurface = SpawnArea.SURFACE in spawnParameters.areas
val allowCeiling = SpawnArea.CEILING in spawnParameters.areas
val allowLiquid = SpawnArea.LIQUID in spawnParameters.areas
val allowSolid = SpawnArea.SOLID in spawnParameters.areas
for (position in positions) {
val region = boundBox + position
// Original engine checks this *after* solid/liquid, which seems to be wrong.
if (world.chunkMap.anyCellSatisfies(region.padded(Globals.spawner.spawnProhibitedCheckPadding, Globals.spawner.spawnProhibitedCheckPadding)) { _, _, cell -> cell.foreground.material.value.collisionKind == CollisionType.NULL || cell.dungeonId != NO_DUNGEON_ID })
continue
val isSolidSpace = world.chunkMap.anyCellSatisfies(region) { _, _, cell -> cell.foreground.material.value.collisionKind.isSolidCollision }
if (isSolidSpace) {
if (allowSolid)
return position.toDoubleVector()
else
continue
}
val liquid = world.chunkMap.averageLiquidLevel(region)
if (liquid != null && liquid.average >= Globals.spawner.minimumLiquidLevel) {
if (allowLiquid)
return position.toDoubleVector()
else
continue
}
if (allowAir)
return position.toDoubleVector()
else if (allowSurface) {
for (sd in 0 .. Globals.spawner.spawnSurfaceCheckDistance) {
val cell = world.chunkMap.getCell(region.centre.x.toInt(), (region.mins.y - sd).toInt())
if (cell.foreground.material.value.collisionKind.isFloorCollision)
return position.toDoubleVector()
}
} else if (allowCeiling) {
for (sd in 0 .. Globals.spawner.spawnCeilingCheckDistance) {
val cell = world.chunkMap.getCell(region.centre.x.toInt(), (region.mins.y + sd).toInt())
if (cell.foreground.material.value.collisionKind.isSolidCollision)
return position.toDoubleVector()
}
}
}
return null
}
// TODO: This does not support directional gravity
private suspend fun spawnInCell(cell: Vector2i) {
val spawnRegion = cell2region(cell)
val spawnRegionPositions by lazy(LazyThreadSafetyMode.NONE) {
val result = ArrayList<Vector2i>()
for (x in spawnRegion.mins.x until spawnRegion.maxs.x) {
for (y in spawnRegion.mins.y until spawnRegion.maxs.y) {
result.add(Vector2i(x, y))
}
}
result
}
val tickets = world.permanentChunkTicket(
spawnRegion,
ChunkState.FULL // TODO: avoid chunkloading?
).await()
try {
tickets.forEach { it.chunk.await() }
val chunkMap = world.chunkMap.snapshot()
val spawnParameters = Starbound.EXECUTOR.supplyAsync { getSpawnParameters(cell, chunkMap) }.await()
if (spawnParameters.areas.isEmpty())
return
val sampleX = world.random.nextRange(spawnRegion.mins.x, spawnRegion.maxs.x)
val sampleY = world.random.nextRange(spawnRegion.mins.y, spawnRegion.maxs.y)
val spawnProfile = world.template.cellInfo(sampleX, sampleY).environmentBiome?.spawnProfile ?: return
for (spawnTypeRef in spawnProfile.spawnTypes) {
val spawnType = spawnTypeRef.value ?: continue
if (!spawnType.spawnParameters.isCompatible(spawnParameters))
continue
if (world.random.nextDouble() < spawnType.spawnChance) {
val spawnSeed = staticRandom64(spawnType.actualSeedMix, world.template.seed)
val targetGroupSize = world.random.nextRange(spawnType.groupSize)
for (i in 0 until targetGroupSize) {
val monsterType = spawnType.monsterType.map({ it.sample(world.random).orThrow { RuntimeException() } }, { it })
if (monsterType.isEmpty) {
LOGGER.error("Tried to spawn monster $monsterType, but it is invalid")
continue
}
var variant = monsterType.value!!.create(spawnSeed, spawnType.monsterParameters)
val monsterBoundBox = variant.commonParameters.movementSettings.standingPoly?.map({ it.aabb }, { it.stream().map { it.aabb }.reduce { t, u -> t.combine(u) }.getOrNull() })
if (monsterBoundBox == null) {
LOGGER.error("Tried to spawn monster $monsterType, but it has no valid AABB to check spawn conditions against")
continue
}
spawnRegionPositions.shuffle(world.random)
val position = selectSpawnPosition(spawnRegionPositions.toTypedArray(), monsterBoundBox, spawnType.spawnParameters) ?: continue
var level = world.template.threatLevel
if (world.sky.dayLevel >= Globals.spawner.minimumDayLevel)
level += world.random.nextRange(spawnType.dayLevelAdjustment)
else
level += world.random.nextRange(spawnType.nightLevelAdjustment)
val finalSpawnProfile = world.template.cellInfo(position.x.toInt(), position.y.toInt()).environmentBiome?.spawnProfile ?: continue
val reUniqueParameters = mergeJson(variant.uniqueParameters.deepCopy(), finalSpawnProfile.monsterParameters)
val reParameters = mergeJson(variant.parameters.deepCopy(), reUniqueParameters)
variant = variant.copy(
uniqueParameters = reUniqueParameters,
parameters = reParameters
)
val entity = MonsterEntity(variant, level)
entity.movement.position = position
entity.isPersistent = false
entity.joinWorld(world)
spawnedEntities.add(entity)
}
}
}
} finally {
tickets.forEach { it.cancel() }
activeCells[cell] = world.simulationTime + Globals.spawner.spawnCellLifetime
if (!hasCleanupTask) {
hasCleanupTask = true
world.eventLoop.schedule(::cleanupActiveCells, (Globals.spawner.spawnCellLifetime * 1000.0).toLong(), TimeUnit.MILLISECONDS)
}
}
}
private fun cleanupActiveCells() {
val itr = activeCells.object2DoubleEntrySet().iterator()
var min = Double.MAX_VALUE
while (itr.hasNext()) {
val next = itr.next()
if (next.doubleValue <= world.simulationTime) {
itr.remove()
} else {
min = min(min, next.doubleValue)
}
}
if (min != Double.MAX_VALUE) {
world.eventLoop.schedule(::cleanupActiveCells, ((min - world.simulationTime) * 1000.0).toLong(), TimeUnit.MILLISECONDS)
} else {
hasCleanupTask = false
}
}
fun tick() {
if (!active)
return
for (client in world.clients) {
for (window in client.client.trackingTileRegions()) {
for (splitRange in world.geometry.split(window.padded(Globals.spawner.windowActivationBorder, Globals.spawner.windowActivationBorder)).first) {
val indexes = AABBi.of(AABB.of(splitRange) / Globals.spawner.spawnCellSize.toDouble())
// TODO: original engine has < condition here
// while we have <=
for (x in indexes.mins.x .. indexes.maxs.x) {
for (y in indexes.mins.y .. indexes.maxs.y) {
val cell = Vector2i(x, y)
if (cell !in activeCells) {
activeCells[cell] = Double.MAX_VALUE
world.eventLoop.scope.launch { spawnInCell(cell) }
} else {
val existing = activeCells.getDouble(cell)
if (existing != Double.MAX_VALUE) {
activeCells[cell] = world.simulationTime + Globals.spawner.spawnCellLifetime
}
}
}
}
}
}
}
val itr = spawnedEntities.iterator()
for (entity in itr) {
if (!entity.isInWorld) {
itr.remove()
} else {
val cellX = (entity.movement.xPosition / Globals.spawner.spawnCellSize).toInt()
val cellY = (entity.movement.yPosition / Globals.spawner.spawnCellSize).toInt()
if (Vector2i(cellX, cellY) !in activeCells) {
entity.remove(AbstractEntity.RemovalReason.REMOVED)
itr.remove()
}
}
}
}
companion object {
private val LOGGER = LogManager.getLogger()
}
}

View File

@ -96,6 +96,7 @@ class ServerWorld private constructor(
val clients = CopyOnWriteArrayList<ServerWorldTracker>()
val shouldStopOnIdle = worldID !is WorldID.ShipWorld
val spawner = MonsterSpawner(this)
private suspend fun doAcceptClient(client: ServerConnection, action: WarpAction?) {
try {
@ -670,6 +671,7 @@ class ServerWorld private constructor(
wireProcessor.tick()
}
spawner.tick()
super.tick(delta)
val packet = StepUpdatePacket(ticks)

View File

@ -79,7 +79,14 @@ data class ChunkPos(val x: Int, val y: Int) : IStruct2i, Comparable<ChunkPos> {
}
override fun hashCode(): Int {
return (y shl 16) xor x
var x = x.rotateLeft(16) xor y
// avalanche bits using murmur3 hash
x = x xor (x ushr 16)
x *= -0x7a143595
x = x xor (x ushr 13)
x *= -0x3d4d51cb
x = x xor (x ushr 16)
return x
}
override fun toString(): String {

View File

@ -298,4 +298,48 @@ class Sky() {
return true
}
val dayLevel: Double get() {
// Turn the dayCycle value into a value that blends evenly between 0.0 at
// mid-night and 1.0 at mid-day and then back again.
val dayCycle = dayCycle
if (dayCycle < 1.0)
return dayCycle / 2.0 + 0.5
else if (dayCycle > 3.0)
return (dayCycle - 3.0) / 2.0
else
return 1.0 - (dayCycle - 1.0) / 2.0
}
val dayCycle: Double get() {
// Always middle of the night in orbit or warp space.
if (skyType == SkyType.ORBITAL || skyType == SkyType.WARP)
return 3.0
var transitionTime = Globals.sky.dayTransitionTime
val dayLength = dayLength
val timeOfDay = timeOfDay
// Original sources put this situation as follows:
// This will misbehave badly if dayTransitionTime is greater than dayLength / 2
// So, let's fix it then.
// If sunset/sunrise duration is greater than third of day length, then
// clamp sunset/sunrise to fourth of day length
if (transitionTime > dayLength / 3.0) {
transitionTime = dayLength / 4.0
}
// timeOfDay() is defined such that 0.0 is mid-dawn. For convenience, shift
// the time of day forwards such that 0.0 is the beginning of the morning.
val shiftedTime = (timeOfDay + transitionTime / 2.0) % dayLength
// There are 5 times here, beginning of the morning, end of the morning,
// beginning of the evening, end of the evening, and then the beginning of
// the morning again (wrapping around).
// TODO()
return 1.0
}
}

View File

@ -87,6 +87,22 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
val sky = Sky(template.skyParameters)
val geometry: WorldGeometry = template.geometry
/**
* World's time based on simulation time (accumulates total simulation deltas)
*
* Should be used to schedule simulation events (monster spawning, event timing, etc)
*/
var simulationTime = 0.0
private set
/**
* World's time according to sky, may be local to world, or global to universe
*
* Should be used to schedule persistent events (crops growth, item despawning, etc)
*/
val persistentTime: Double
get() = sky.time
val nextEntityID = AtomicInteger()
override fun getCell(x: Int, y: Int): AbstractCell {
@ -657,6 +673,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
open fun tick(delta: Double) {
ticks++
simulationTime += delta
if (dynamicEntities.size < 128) {
dynamicEntities.forEach {

View File

@ -37,7 +37,7 @@ abstract class ActorEntity : DynamicEntity() {
override fun onJoinWorld(world: World<*, *>) {
super.onJoinWorld(world)
statusController.init(world)
statusController.init()
}
override fun tick(delta: Double) {

View File

@ -152,15 +152,15 @@ class ActorMovementController() : MovementController() {
fun calculateMovementParameters(base: ActorMovementParameters): MovementParameters {
val mass = base.mass
val gravityMultiplier = base.gravityMultiplier
val gravityMultiplier = base.gravityMultiplier ?: Globals.movementParameters.gravityMultiplier
val liquidBuoyancy = base.liquidBuoyancy
val airBuoyancy = base.airBuoyancy
val bounceFactor = base.bounceFactor
val stopOnFirstBounce = base.stopOnFirstBounce
val enableSurfaceSlopeCorrection = base.enableSurfaceSlopeCorrection
val slopeSlidingFactor = base.slopeSlidingFactor
val maxMovementPerStep = base.maxMovementPerStep
val liquidBuoyancy = base.liquidBuoyancy ?: Globals.movementParameters.liquidBuoyancy
val airBuoyancy = base.airBuoyancy ?: Globals.movementParameters.airBuoyancy
val bounceFactor = base.bounceFactor ?: Globals.movementParameters.bounceFactor
val stopOnFirstBounce = base.stopOnFirstBounce ?: Globals.movementParameters.stopOnFirstBounce
val enableSurfaceSlopeCorrection = base.enableSurfaceSlopeCorrection ?: Globals.movementParameters.enableSurfaceSlopeCorrection
val slopeSlidingFactor = base.slopeSlidingFactor ?: Globals.movementParameters.slopeSlidingFactor
val maxMovementPerStep = base.maxMovementPerStep ?: Globals.movementParameters.maxMovementPerStep
val collisionPoly = if (isCrouching) base.crouchingPoly else base.standingPoly

View File

@ -253,8 +253,7 @@ class MonsterEntity(val variant: MonsterVariant, level: Double? = null) : ActorE
newChatMessageEvent.trigger()
}
override val isPersistent: Boolean
get() = variant.commonParameters.persistent
override var isPersistent: Boolean = variant.commonParameters.persistent
override fun writeNetwork(stream: DataOutputStream, isLegacy: Boolean) {
variant.write(stream, isLegacy)
@ -287,7 +286,7 @@ class MonsterEntity(val variant: MonsterVariant, level: Double? = null) : ActorE
}
override val metaBoundingBox: AABB
get() = variant.commonParameters.metaBoundBox
get() = variant.commonParameters.metaBoundBox + position
override val mouthPosition: Vector2d
get() = movement.getAbsolutePosition(variant.commonParameters.mouthOffset)
@ -325,12 +324,12 @@ class MonsterEntity(val variant: MonsterVariant, level: Double? = null) : ActorE
"sourceId" to damage.request.sourceEntityId,
"damage" to totalDamage,
"sourceDamage" to damage.request.damage,
"sourceKind" to damage.request.kind
"sourceKind" to damage.request.damageSourceKind
))
}
if (health <= 0.0) {
deathDamageKinds.add(damage.request.kind)
deathDamageKinds.add(damage.request.damageSourceKind)
}
return notifications
@ -338,7 +337,7 @@ class MonsterEntity(val variant: MonsterVariant, level: Double? = null) : ActorE
private val shouldDie: Boolean get() {
val result = lua.invokeGlobal("shouldDie")
return result.isNotEmpty() && result[0] is Boolean && result[0] as Boolean || health <= 0.0 || lua.errorState
return result.isNotEmpty() && result[0] is Boolean && result[0] as Boolean || health <= 0.0 //|| lua.errorState
}
override fun tick(delta: Double) {

View File

@ -152,6 +152,7 @@ class StatusController(val entity: ActorEntity, val config: StatusControllerConf
// TODO: Once we have brand new object-oriented Lua API, expose proper entity bindings here
// TODO: Expose world bindings
lua.init()
}
}
@ -216,13 +217,6 @@ class StatusController(val entity: ActorEntity, val config: StatusControllerConf
}
}
fun init(world: World<*, *>) {
if (!entity.isRemote) {
provideWorldBindings(world, lua)
lua.init()
}
}
// used as place to store random data by Lua scripts, because global `storage` table wasn't enough, it seems,
// to Chucklefish devs
private val statusProperties = networkedJsonObject(config.statusProperties.deepCopy()).also { networkGroup.add(it) }

View File

@ -46,15 +46,15 @@ abstract class AbstractBehaviorNode {
else if (parameter.value is JsonPrimitive && parameter.value.isString)
str = parameter.value.asString
if (str != null) {
if (!str.isNullOrEmpty()) {
if (str.first() == '<' && str.last() == '>') {
val treeKey = str.substring(1, str.length - 2)
val treeKey = str.substring(1, str.length - 1)
val param = treeParameters[treeKey]
if (param != null) {
return param
} else {
throw NoSuchElementException("No parameter specified for tag '$str'")
throw NoSuchElementException("No parameter specified for tag '$treeKey'")
}
}
}
@ -66,7 +66,7 @@ abstract class AbstractBehaviorNode {
if (output == null) {
return null
} else if (output.first() == '<' && output.last() == '>') {
val replacement = treeParameters[output.substring(1, output.length - 2)] ?: throw NoSuchElementException("No parameter specified for tag '$output'")
val replacement = treeParameters[output.substring(1, output.length - 1)] ?: throw NoSuchElementException("No parameter specified for tag '$output'")
if (replacement.key != null)
return replacement.key
@ -96,7 +96,7 @@ abstract class AbstractBehaviorNode {
val module = BehaviorTree(tree.blackboard, Registries.behavior.getOrThrow(name).value, moduleParameters)
tree.scripts.addAll(module.scripts)
tree.functions.addAll(module.functions)
return tree.root
return module.root
}
val parameters = LinkedHashMap(Registries.behaviorNodes.getOrThrow(name).value.properties)

View File

@ -11,6 +11,7 @@ import ru.dbotthepony.kstarbound.lua.userdata.BehaviorState
class ActionNode(val name: String, val parameters: Map<String, NodeParameter>, val outputs: Map<String, NodeOutput>) : AbstractBehaviorNode() {
private var coroutine: Coroutine? = null
private val nodeID = BehaviorState.NODE_GARBAGE_INDEX.getAndIncrement()
override fun run(delta: Double, state: BehaviorState): Status {
var coroutine = coroutine
@ -27,30 +28,34 @@ class ActionNode(val name: String, val parameters: Map<String, NodeParameter>, v
try {
val result = if (firstTime) {
val parameters = state.blackboard.parameters(parameters, this)
state.lua.call(CoroutineLib.resume(), coroutine, parameters, state.blackboard, this, delta)
state.lua.call(CoroutineLib.resume(), coroutine, parameters, state.blackboard, nodeID, delta)
} else {
state.lua.call(CoroutineLib.resume(), coroutine, delta)
}
val status = result[0] as Boolean
val coroutineStatus = result[0] as Boolean
if (result.size >= 2) {
val second = result[1] as? Table
if (second != null) {
state.blackboard.setOutput(this, second)
}
if (!coroutineStatus) {
LOGGER.warn("Behavior ActionNode '$name' failed: ${result[1]}")
return Status.FAILURE
}
val isDead = state.lua.call(CoroutineLib.status(), coroutine)[0] == "dead"
if (!status) {
return Status.FAILURE
} else if (isDead) {
return Status.SUCCESS
} else {
if (result.size == 1) {
return Status.RUNNING
}
val nodeStatus = result.getOrNull(1) as? Boolean ?: false
val nodeExtra = result.getOrNull(2) as? Table
if (nodeExtra != null) {
state.blackboard.setOutput(this, nodeExtra)
}
if (!nodeStatus) {
return Status.FAILURE
} else {
return Status.SUCCESS
}
} catch (err: CallPausedException) {
LOGGER.error("Behavior ActionNode '$name' called blocking code, which initiated pause. This is not supported.")
return Status.FAILURE
@ -61,6 +66,10 @@ class ActionNode(val name: String, val parameters: Map<String, NodeParameter>, v
coroutine = null
}
override fun toString(): String {
return "ActionNode[$name, parameters=$parameters, outputs=$outputs, coroutine=$coroutine]"
}
companion object {
private val LOGGER = LogManager.getLogger()
}

View File

@ -9,13 +9,11 @@ import org.classdump.luna.Table
import org.classdump.luna.Userdata
import org.classdump.luna.impl.ImmutableTable
import ru.dbotthepony.kstarbound.defs.actor.behavior.NodeParameter
import ru.dbotthepony.kstarbound.json.stream
import ru.dbotthepony.kstarbound.lua.LuaEnvironment
import ru.dbotthepony.kstarbound.lua.from
import ru.dbotthepony.kstarbound.lua.get
import ru.dbotthepony.kstarbound.lua.luaFunction
import ru.dbotthepony.kstarbound.lua.set
import ru.dbotthepony.kstarbound.lua.tableOf
import ru.dbotthepony.kstarbound.util.valueOf
import java.util.EnumMap
import java.util.HashSet
@ -36,7 +34,7 @@ import kotlin.collections.HashMap
*/
class Blackboard(val lua: LuaEnvironment) : Userdata<Blackboard>() {
private val board = EnumMap<NodeParameterType, HashMap<String, Any>>(NodeParameterType::class.java)
private val input = EnumMap<NodeParameterType, HashMap<String, ArrayList<Pair<Any, String>>>>(NodeParameterType::class.java)
private val input = EnumMap<NodeParameterType, HashMap<String, ArrayList<Pair<String, Table>>>>(NodeParameterType::class.java)
private val parameters = HashMap<Any, Table>()
// key -> list of Lua tables and their indices
private val vectorNumberInput = HashMap<String, HashSet<Pair<Any, Table>>>()
@ -45,6 +43,7 @@ class Blackboard(val lua: LuaEnvironment) : Userdata<Blackboard>() {
init {
for (v in NodeParameterType.entries) {
board[v] = HashMap()
input[v] = HashMap()
}
}
@ -58,8 +57,8 @@ class Blackboard(val lua: LuaEnvironment) : Userdata<Blackboard>() {
val input = input[type]!![key]
if (input != null) {
for ((k1, k2) in input) {
parameters[k1]!![k2] = value
for ((index, table) in input) {
table[index] = value
}
}
@ -90,8 +89,8 @@ class Blackboard(val lua: LuaEnvironment) : Userdata<Blackboard>() {
for ((name, parameter) in parameters.entries) {
if (parameter.value.key != null) {
val typeInput = input[parameter.type]!!.computeIfAbsent(parameter.value.key) { ArrayList() }
typeInput.add(nodeID to name)
table[name] = this[parameter.type, name]
typeInput.add(name to table)
table[name] = this[parameter.type, parameter.value.key]
} else {
val value = parameter.value.value ?: JsonNull.INSTANCE
@ -167,6 +166,7 @@ class Blackboard(val lua: LuaEnvironment) : Userdata<Blackboard>() {
companion object {
private val metatable: ImmutableTable
private fun __index() = metatable
init {
val builder = ImmutableTable.Builder()
@ -187,6 +187,7 @@ class Blackboard(val lua: LuaEnvironment) : Userdata<Blackboard>() {
})
}
builder.add("__index", luaFunction { _: Any?, index: Any -> returnBuffer.setTo(__index()[index]) })
metatable = builder.build()
}
}

View File

@ -1,14 +1,18 @@
package ru.dbotthepony.kstarbound.world.entities.behavior
import org.apache.logging.log4j.LogManager
import org.classdump.luna.ByteString
import org.classdump.luna.Table
import org.classdump.luna.exec.CallPausedException
import org.classdump.luna.lib.CoroutineLib
import org.classdump.luna.runtime.Coroutine
import ru.dbotthepony.kstarbound.defs.actor.behavior.NodeParameter
import ru.dbotthepony.kstarbound.lua.userdata.BehaviorState
import java.util.concurrent.atomic.AtomicLong
class DecoratorNode(val name: String, val parameters: Map<String, NodeParameter>, val child: AbstractBehaviorNode) : AbstractBehaviorNode() {
private var coroutine: Coroutine? = null
private val nodeID = BehaviorState.NODE_GARBAGE_INDEX.getAndIncrement()
override fun run(delta: Double, state: BehaviorState): Status {
var coroutine = coroutine
@ -19,14 +23,28 @@ class DecoratorNode(val name: String, val parameters: Map<String, NodeParameter>
try {
coroutine = state.lua.call(CoroutineLib.create(), fn)[0] as Coroutine
val result = state.lua.call(CoroutineLib.resume(), coroutine, parameters, state.blackboard, this)
val result = state.lua.call(CoroutineLib.resume(), coroutine, parameters, state.blackboard, nodeID)
val status = result[0] as Boolean
if (!status && result.size >= 2) {
if (!status) {
LOGGER.warn("Behavior DecoratorNode '$name' failed: ${result[1]}")
return Status.FAILURE
}
return if (status) Status.SUCCESS else Status.FAILURE
if (result.size == 1) {
val coroutineStatus = state.lua.call(CoroutineLib.status(), coroutine)[0] as String
if (coroutineStatus == "dead") {
// quite unexpected, but whatever
return Status.SUCCESS
} else {
this.coroutine = coroutine
}
} else {
val nodeStatus = result.getOrNull(1) as? Boolean ?: false
// val nodeExtra = result.getOrNull(3) as? Table
return if (nodeStatus) Status.SUCCESS else Status.FAILURE
}
} catch (err: CallPausedException) {
LOGGER.error("Behavior DecoratorNode '$name' called blocking code, which initiated pause. This is not supported.")
return Status.FAILURE
@ -48,7 +66,23 @@ class DecoratorNode(val name: String, val parameters: Map<String, NodeParameter>
LOGGER.warn("Behavior DecoratorNode '$name' failed: ${result[1]}")
}
status = if (execStatus) Status.SUCCESS else Status.FAILURE
if (result.size == 1) {
// another yield OR unexpected return?
val coroutineStatus = state.lua.call(CoroutineLib.status(), coroutine)[0] as String
if (coroutineStatus == "dead") {
this.coroutine = null
status = Status.SUCCESS
} else
status = Status.RUNNING
} else {
// yield or return with status
val nodeStatus = result.getOrNull(1) as? Boolean ?: false
// val nodeExtra = result.getOrNull(3) as? Table
status = if (nodeStatus) Status.SUCCESS else Status.FAILURE
this.coroutine = null
}
} catch (err: CallPausedException) {
LOGGER.error("Behavior DecoratorNode '$name' called blocking code on children return, which initiated pause. This is not supported.")
status = Status.FAILURE
@ -65,6 +99,10 @@ class DecoratorNode(val name: String, val parameters: Map<String, NodeParameter>
coroutine = null
}
override fun toString(): String {
return "DecoratorNode[$name, coroutine=$coroutine, parameters=$parameters, children=$child]"
}
companion object {
private val LOGGER = LogManager.getLogger()
}

View File

@ -26,6 +26,10 @@ class DynamicNode(val children: ImmutableList<AbstractBehaviorNode>) : AbstractB
return Status.RUNNING
}
override fun toString(): String {
return "DynamicNode[${children.size}, index=$index, current=${children.getOrNull(index)}]"
}
override fun reset() {
children.forEach { it.reset() }
index = 0

View File

@ -30,6 +30,9 @@ class ParallelNode(parameters: Map<String, NodeParameter>, val children: Immutab
}
}
private var lastFailed = -1
private var lastSucceeded = -1
override fun run(delta: Double, state: BehaviorState): Status {
var failed = 0
var succeeded = 0
@ -43,14 +46,24 @@ class ParallelNode(parameters: Map<String, NodeParameter>, val children: Immutab
failed++
if (succeeded >= successLimit || failed >= failLimit) {
lastFailed = failed
lastSucceeded = succeeded
return if (succeeded >= successLimit) Status.SUCCESS else Status.FAILURE
}
}
lastFailed = failed
lastSucceeded = succeeded
return Status.RUNNING
}
override fun toString(): String {
return "ParallelNode[children=${children.size}, successLimit=$successLimit, failLimit=$failLimit, lastFailed=$lastFailed, lastSucceeded=$lastSucceeded]"
}
override fun reset() {
children.forEach { it.reset() }
lastSucceeded = -1
lastFailed = -1
}
}

View File

@ -17,6 +17,13 @@ class RandomizeNode(val children: ImmutableList<AbstractBehaviorNode>) : Abstrac
return children[index].runAndReset(delta, state)
}
override fun toString(): String {
if (index == -1)
return "RandomizeNode[${children.size}, not chosen]"
else
return "RandomizeNode[${children.size}, ${children[index]}]"
}
override fun reset() {
children.forEach { it.reset() }
index = -1

View File

@ -21,6 +21,10 @@ class SequenceNode(val children: ImmutableList<AbstractBehaviorNode>) : Abstract
return Status.SUCCESS
}
override fun toString(): String {
return "SequenceNode[${children.size}, index=$index, current=${children.getOrNull(index)}]"
}
override fun reset() {
children.forEach { it.reset() }
index = 0

View File

@ -70,8 +70,6 @@ import ru.dbotthepony.kstarbound.lua.LuaMessageHandlerComponent
import ru.dbotthepony.kstarbound.lua.LuaUpdateComponent
import ru.dbotthepony.kstarbound.lua.bindings.provideAnimatorBindings
import ru.dbotthepony.kstarbound.lua.bindings.provideEntityBindings
import ru.dbotthepony.kstarbound.lua.bindings.provideWorldBindings
import ru.dbotthepony.kstarbound.lua.bindings.provideWorldObjectBindings
import ru.dbotthepony.kstarbound.lua.from
import ru.dbotthepony.kstarbound.lua.get
import ru.dbotthepony.kstarbound.lua.set
@ -849,7 +847,7 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
damage.request.damage,
dmg,
if (health <= 0.0) HitType.KILL else HitType.HIT,
damage.request.kind,
damage.request.damageSourceKind,
config.value.damageMaterialKind
)
)