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.ActorMovementParameters
import ru.dbotthepony.kstarbound.defs.ClientConfig import ru.dbotthepony.kstarbound.defs.ClientConfig
import ru.dbotthepony.kstarbound.defs.CurrencyDefinition import ru.dbotthepony.kstarbound.defs.CurrencyDefinition
import ru.dbotthepony.kstarbound.defs.ElementalDamageType
import ru.dbotthepony.kstarbound.defs.MovementParameters import ru.dbotthepony.kstarbound.defs.MovementParameters
import ru.dbotthepony.kstarbound.defs.world.SpawnerConfig
import ru.dbotthepony.kstarbound.defs.UniverseServerConfig 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.PlayerConfig
import ru.dbotthepony.kstarbound.defs.actor.player.ShipUpgrades import ru.dbotthepony.kstarbound.defs.actor.player.ShipUpgrades
import ru.dbotthepony.kstarbound.defs.item.ItemDropConfig 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.SystemWorldObjectConfig
import ru.dbotthepony.kstarbound.defs.world.WorldTemplateConfig import ru.dbotthepony.kstarbound.defs.world.WorldTemplateConfig
import ru.dbotthepony.kstarbound.json.listAdapter import ru.dbotthepony.kstarbound.json.listAdapter
import ru.dbotthepony.kstarbound.json.mapAdapter
import ru.dbotthepony.kstarbound.util.AssetPathStack import ru.dbotthepony.kstarbound.util.AssetPathStack
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.Future import java.util.concurrent.Future
@ -122,6 +123,12 @@ object Globals {
var quests by Delegates.notNull<QuestGlobalConfig>() var quests by Delegates.notNull<QuestGlobalConfig>()
private set 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>>() private var profanityFilterInternal by Delegates.notNull<ImmutableList<String>>()
val profanityFilter: ImmutableSet<String> by lazy { val profanityFilter: ImmutableSet<String> by lazy {
@ -229,11 +236,13 @@ object Globals {
tasks.add(load("/tiles/defaultDamage.config", ::tileDamage)) tasks.add(load("/tiles/defaultDamage.config", ::tileDamage))
tasks.add(load("/ships/shipupgrades.config", ::shipUpgrades)) tasks.add(load("/ships/shipupgrades.config", ::shipUpgrades))
tasks.add(load("/quests/quests.config", ::quests)) 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("/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("/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("/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("/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()) 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.ImmutableList
import com.google.common.collect.ImmutableMap import com.google.common.collect.ImmutableMap
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.google.gson.JsonArray
import com.google.gson.JsonObject import com.google.gson.JsonObject
import com.google.gson.JsonSyntaxException import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapterFactory import com.google.gson.TypeAdapterFactory
@ -15,6 +16,7 @@ import kotlinx.coroutines.launch
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.defs.AssetReference import ru.dbotthepony.kstarbound.defs.AssetReference
import ru.dbotthepony.kstarbound.defs.DamageKind
import ru.dbotthepony.kstarbound.defs.Json2Function import ru.dbotthepony.kstarbound.defs.Json2Function
import ru.dbotthepony.kstarbound.defs.JsonConfigFunction import ru.dbotthepony.kstarbound.defs.JsonConfigFunction
import ru.dbotthepony.kstarbound.defs.JsonFunction 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.GrassVariant
import ru.dbotthepony.kstarbound.defs.world.TreeVariant import ru.dbotthepony.kstarbound.defs.world.TreeVariant
import ru.dbotthepony.kstarbound.defs.world.BiomeDefinition import ru.dbotthepony.kstarbound.defs.world.BiomeDefinition
import ru.dbotthepony.kstarbound.defs.world.SpawnType
import ru.dbotthepony.kstarbound.item.ItemRegistry import ru.dbotthepony.kstarbound.item.ItemRegistry
import ru.dbotthepony.kstarbound.json.JsonPatch import ru.dbotthepony.kstarbound.json.JsonPatch
import ru.dbotthepony.kstarbound.json.builder.JsonFactory 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 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 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 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 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 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()) } 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 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 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 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 monsterParts = HashMap<Pair<String, String>, HashMap<String, Pair<MonsterPartDefinition, IStarboundFile>>>()
private val loggedMisses = Collections.synchronizedSet(ObjectOpenHashSet<Pair<String, String>>()) 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(markovGenerators, patchTree, fileTree["namesource"] ?: listOf(), key(MarkovTextGenerator::name)))
tasks.addAll(loadRegistry(projectiles, patchTree, fileTree["projectile"] ?: listOf(), key(ProjectileDefinition::projectileName))) 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(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)) 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(treasureChests, fileTree["treasurechests"] ?: listOf(), patchTree) { name = it })
tasks.addAll(loadCombined(treasurePools, fileTree["treasurepools"] ?: 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 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>>) { private suspend fun loadTerrainSelector(listedFile: IStarboundFile, type: TerrainSelectorType?, patches: Map<String, Collection<IStarboundFile>>) {
try { try {
val json = JsonPatch.applyAsync(listedFile.asyncJsonReader(), patches[listedFile.computeFullPath()]) as JsonObject 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 { 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>() { class Adapter(gson: Gson) : TypeAdapter<ColorReplacements>() {

View File

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

View File

@ -17,6 +17,13 @@ import ru.dbotthepony.kstarbound.json.popObject
*/ */
@JsonAdapter(NodeParameterValue.Adapter::class) @JsonAdapter(NodeParameterValue.Adapter::class)
data class NodeParameterValue(val key: String?, val value: JsonElement?) { 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>() { class Adapter : TypeAdapter<NodeParameterValue>() {
override fun write(out: JsonWriter, value: NodeParameterValue) { override fun write(out: JsonWriter, value: NodeParameterValue) {
if (value.key != null) { if (value.key != null) {

View File

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

View File

@ -37,7 +37,7 @@ import java.io.DataOutputStream
import java.util.random.RandomGenerator import java.util.random.RandomGenerator
@JsonFactory @JsonFactory
class MonsterVariant( data class MonsterVariant(
val type: String, val type: String,
val shortDescription: String? = null, val shortDescription: String? = null,
val description: String? = null, val description: String? = null,
@ -116,7 +116,7 @@ class MonsterVariant(
val actualDropPools: ImmutableList<Either<ImmutableMap<String, Registry.Ref<TreasurePoolDefinition>>, Registry.Ref<TreasurePoolDefinition>>> val actualDropPools: ImmutableList<Either<ImmutableMap<String, Registry.Ref<TreasurePoolDefinition>>, Registry.Ref<TreasurePoolDefinition>>>
get() = commonParameters.dropPools ?: dropPools 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) { fun write(stream: DataOutputStream, isLegacy: Boolean) {
if (isLegacy) { if (isLegacy) {

View File

@ -21,6 +21,7 @@ data class Biome(
val surfacePlaceables: BiomePlaceables = BiomePlaceables(), val surfacePlaceables: BiomePlaceables = BiomePlaceables(),
val undergroundPlaceables: BiomePlaceables = BiomePlaceables(), val undergroundPlaceables: BiomePlaceables = BiomePlaceables(),
val parallax: Parallax? = null, val parallax: Parallax? = null,
val spawnProfile: SpawnProfile? = null,
) { ) {
@JvmName("hueShiftTile") @JvmName("hueShiftTile")
fun hueShift(block: Registry.Entry<TileDefinition>): Float { 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.ImmutableList
import com.google.common.collect.ImmutableSet 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.kommons.collect.filterNotNull
import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Registry
@ -35,6 +37,7 @@ data class BiomeDefinition(
val surfacePlaceables: BiomePlaceablesDefinition = BiomePlaceablesDefinition(), val surfacePlaceables: BiomePlaceablesDefinition = BiomePlaceablesDefinition(),
val undergroundPlaceables: BiomePlaceablesDefinition = BiomePlaceablesDefinition(), val undergroundPlaceables: BiomePlaceablesDefinition = BiomePlaceablesDefinition(),
val parallax: AssetReference<Parallax.Data>? = null, val parallax: AssetReference<Parallax.Data>? = null,
val spawnProfile: JsonObject? = null,
) { ) {
data class CreationParams( data class CreationParams(
val hueShift: Double, val hueShift: Double,
@ -83,7 +86,9 @@ data class BiomeDefinition(
.map { NativeLegacy.TileMod(Registries.tileModifiers.ref(it.first)) to it.second } .map { NativeLegacy.TileMod(Registries.tileModifiers.ref(it.first)) to it.second }
.filter { it.first.native.isPresent } .filter { it.first.native.isPresent }
.collect(ImmutableList.toImmutableList()) .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 starVelocityFactor: Double = 0.0,
val flyingTimer: Double = 0.0, val flyingTimer: Double = 0.0,
val flashTimer: Double = 1.0, val flashTimer: Double = 1.0,
val dayTransitionTime: Double = 200.0,
) { ) {
@JsonFactory @JsonFactory
data class Stars( 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.Vector2d
import ru.dbotthepony.kstarbound.math.vector.Vector2i
class WorldServerConfig( class WorldServerConfig(
val playerSpaceStartRegionSize: Vector2d, val playerSpaceStartRegionSize: Vector2d,

View File

@ -1,15 +1,16 @@
package ru.dbotthepony.kstarbound.json.factory package ru.dbotthepony.kstarbound.json.factory
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap import com.google.common.collect.ImmutableMap
import com.google.gson.JsonPrimitive
import com.google.gson.JsonSyntaxException import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapter import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.gson.consumeNull 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>?) { override fun write(out: JsonWriter, value: ImmutableMap<K, V>?) {
if (value == null) { if (value == null) {
out.nullValue() out.nullValue()
@ -30,6 +31,21 @@ class ImmutableArrayMapTypeAdapter<K, V>(val keyAdapter: TypeAdapter<K>, val ele
if (reader.consumeNull()) if (reader.consumeNull())
return null 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() reader.beginArray()
val builder = ImmutableMap.Builder<K, V>() 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 ImmutableMapTypeAdapter(stringInterner, gson.getAdapter(TypeToken.get(elementType1))) as TypeAdapter<T>
} }
return ImmutableArrayMapTypeAdapter( return Immutable2MapTypeAdapter(
gson.getAdapter(TypeToken.get(elementType0)), gson.getAdapter(TypeToken.get(elementType0)),
gson.getAdapter(TypeToken.get(elementType1)) gson.getAdapter(TypeToken.get(elementType1))
) as TypeAdapter<T> ) 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.ExecutionContext
import org.classdump.luna.runtime.LuaFunction import org.classdump.luna.runtime.LuaFunction
import org.classdump.luna.runtime.UnresolvedControlThrowable 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.max
import kotlin.math.min 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: * 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) { 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>) { fun attach(scripts: Collection<AssetPath>) {
@ -248,7 +252,9 @@ class LuaEnvironment : StateContext {
} }
} else { } else {
for (script in scripts) { 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 { try {
executor.call(this, script.newInstance(Variable(globals))) executor.call(this, script.newInstance(Variable(globals)))
} catch (err: Throwable) { } catch (err: Throwable) {
errorState = true // errorState = true
LOGGER.error("Failed to attach script to environment", err) LOGGER.error("Failed to attach script to environment", err)
scripts.clear() scripts.clear()
return false return false
@ -290,7 +296,7 @@ class LuaEnvironment : StateContext {
try { try {
executor.call(this, init) executor.call(this, init)
} catch (err: Throwable) { } catch (err: Throwable) {
errorState = true // errorState = true
LOGGER.error("Exception on init()", err) LOGGER.error("Exception on init()", err)
return false return false
} }
@ -310,7 +316,7 @@ class LuaEnvironment : StateContext {
return try { return try {
executor.call(this, load, *arguments) executor.call(this, load, *arguments)
} catch (err: Throwable) { } catch (err: Throwable) {
errorState = true // errorState = true
LOGGER.error("Exception while calling global $name", err) LOGGER.error("Exception while calling global $name", err)
arrayOf() arrayOf()
} }

View File

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

View File

@ -1,5 +1,7 @@
package ru.dbotthepony.kstarbound.lua.bindings package ru.dbotthepony.kstarbound.lua.bindings
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import org.classdump.luna.ByteString 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) 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) { fun provideRootBindings(lua: LuaEnvironment) {
val table = lua.newTable() val table = lua.newTable()
lua.globals["root"] = table lua.globals["root"] = table
@ -481,11 +509,11 @@ fun provideRootBindings(lua: LuaEnvironment) {
table["createBiome"] = createBiome table["createBiome"] = createBiome
table["monsterSkillParameter"] = luaStub("monsterSkillParameter") table["monsterSkillParameter"] = monsterSkillParameter
table["monsterParameters"] = luaStub("monsterParameters") table["monsterParameters"] = monsterParameters
table["monsterMovementSettings"] = luaStub("monsterMovementSettings") table["monsterMovementSettings"] = monsterMovementSettings
table["hasTech"] = registryDefExists(Registries.techs) table["hasTech"] = hasTech
table["techType"] = techType table["techType"] = techType
table["techConfig"] = techConfig table["techConfig"] = techConfig
@ -494,7 +522,6 @@ fun provideRootBindings(lua: LuaEnvironment) {
table["collection"] = luaStub("collection") table["collection"] = luaStub("collection")
table["collectables"] = luaStub("collectables") table["collectables"] = luaStub("collectables")
table["elementalResistance"] = luaStub("elementalResistance") table["elementalResistance"] = elementalResistance
table["dungeonMetadata"] = luaStub("dungeonMetadata") table["dungeonMetadata"] = dungeonMetadata
table["behavior"] = luaStub("behavior")
} }

View File

@ -63,12 +63,12 @@ fun provideStatusControllerBindings(self: StatusController, lua: LuaEnvironment)
} }
callbacks["resource"] = luaFunction { name: ByteString -> 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) returnBuffer.setTo(resource.value)
} }
callbacks["resourcePositive"] = luaFunction { name: ByteString -> 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) returnBuffer.setTo(resource.value > 0.0)
} }
@ -189,20 +189,20 @@ fun provideStatusControllerBindings(self: StatusController, lua: LuaEnvironment)
callbacks["damageTakenSince"] = luaFunction { since: Number? -> callbacks["damageTakenSince"] = luaFunction { since: Number? ->
val (list, newSince) = self.recentDamageReceived(since?.toLong() ?: 0L) 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? -> callbacks["inflictedHitsSince"] = luaFunction { since: Number? ->
val (list, newSince) = self.recentHitsDealt(since?.toLong() ?: 0L) val (list, newSince) = self.recentHitsDealt(since?.toLong() ?: 0L)
returnBuffer.setTo(tableOf(*list.map { p -> 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) }.toTypedArray()), newSince)
} }
callbacks["inflictedDamageSince"] = luaFunction { since: Number? -> callbacks["inflictedDamageSince"] = luaFunction { since: Number? ->
val (list, newSince) = self.recentDamageDealt(since?.toLong() ?: 0L) 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 { callbacks["activeUniqueStatusEffectSummary"] = luaFunction {
@ -217,8 +217,8 @@ fun provideStatusControllerBindings(self: StatusController, lua: LuaEnvironment)
returnBuffer.setTo(self.primaryDirectives.toByteString()) returnBuffer.setTo(self.primaryDirectives.toByteString())
} }
callbacks["setPrimaryDirectives"] = luaFunction { directives: ByteString -> callbacks["setPrimaryDirectives"] = luaFunction { directives: ByteString? ->
self.primaryDirectives = directives.decode().sbIntern() self.primaryDirectives = directives?.decode()?.sbIntern() ?: ""
} }
callbacks["applySelfDamageRequest"] = luaFunction { damage: Table -> 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.set
import ru.dbotthepony.kstarbound.lua.toByteString import ru.dbotthepony.kstarbound.lua.toByteString
import ru.dbotthepony.kstarbound.lua.toJson import ru.dbotthepony.kstarbound.lua.toJson
import ru.dbotthepony.kstarbound.lua.toJsonFromLua
import ru.dbotthepony.kstarbound.lua.toVector2d import ru.dbotthepony.kstarbound.lua.toVector2d
import ru.dbotthepony.kstarbound.lua.userdata.LuaPerlinNoise import ru.dbotthepony.kstarbound.lua.userdata.LuaPerlinNoise
import ru.dbotthepony.kstarbound.lua.userdata.LuaRandomGenerator import ru.dbotthepony.kstarbound.lua.userdata.LuaRandomGenerator
@ -98,6 +99,10 @@ private val staticRandomI32Range = luaFunctionN("staticRandomI32Range") {
returnBuffer.setTo(staticRandomLong(min, max, *it.copyRemaining())) 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) { fun provideUtilityBindings(lua: LuaEnvironment) {
val table = lua.newTable() val table = lua.newTable()
lua.globals["sb"] = table lua.globals["sb"] = table
@ -133,6 +138,8 @@ fun provideUtilityBindings(lua: LuaEnvironment) {
table["staticRandomDoubleRange"] = staticRandomDoubleRange table["staticRandomDoubleRange"] = staticRandomDoubleRange
table["staticRandomI32Range"] = staticRandomI32Range table["staticRandomI32Range"] = staticRandomI32Range
table["staticRandomI64Range"] = staticRandomI32Range table["staticRandomI64Range"] = staticRandomI32Range
table["jsonMerge"] = mergeJson
} }
fun provideConfigBindings(lua: LuaEnvironment, lookup: ExecutionContext.(path: JsonPath, ifMissing: Any?) -> Any?) { 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) { if (self is ServerWorld) {
provideServerWorldBindings(self, callbacks, lua) 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.AbstractBehaviorNode
import ru.dbotthepony.kstarbound.world.entities.behavior.BehaviorTree import ru.dbotthepony.kstarbound.world.entities.behavior.BehaviorTree
import ru.dbotthepony.kstarbound.world.entities.behavior.Blackboard import ru.dbotthepony.kstarbound.world.entities.behavior.Blackboard
import java.util.concurrent.atomic.AtomicLong
class BehaviorState(val tree: BehaviorTree) : Userdata<BehaviorState>() { class BehaviorState(val tree: BehaviorTree) : Userdata<BehaviorState>() {
val blackboard get() = tree.blackboard val blackboard get() = tree.blackboard
@ -71,6 +72,10 @@ class BehaviorState(val tree: BehaviorTree) : Userdata<BehaviorState>() {
} }
companion object { 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 { private fun __index(): Table {
return metatable 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 times(other: Vector2d) = AABB(mins * other, maxs * other)
operator fun div(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 times(other: Double) = AABB(mins * other, maxs * other)
operator fun div(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 ZERO = AABB(Vector2d.ZERO, Vector2d.ZERO)
@JvmField val NEVER = AABB(Vector2d(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY), Vector2d(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY)) @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) if (config.useUniverseClock)
world.sky.referenceClock = universeClock world.sky.referenceClock = universeClock
world.spawner.active = config.spawningEnabled
world.eventLoop.start() world.eventLoop.start()
world.prepare(true).await() world.prepare(true).await()
} catch (err: Throwable) { } catch (err: Throwable) {
@ -439,14 +441,15 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
try { try {
val structure = Globals.universeServer.speciesShips[connection.playerSpecies]?.firstOrNull()?.value?.get() ?: throw NoSuchElementException("No ship structure for species ${connection.playerSpecies}") 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 val currentUpgrades = connection.shipUpgrades
.apply(Globals.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}")) .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 connection.shipUpgrades = currentUpgrades
world.setProperty("invinciblePlayers", JsonPrimitive(true)) world.setProperty("invinciblePlayers", JsonPrimitive(true))
world.setProperty("ship.level", JsonPrimitive(0)) world.setProperty("ship.level", JsonPrimitive(0))
world.setProperty("ship.fuel", 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.crewSize", JsonPrimitive(currentUpgrades.crewSize))
world.setProperty("ship.fuelEfficiency", JsonPrimitive(currentUpgrades.fuelEfficiency)) world.setProperty("ship.fuelEfficiency", JsonPrimitive(currentUpgrades.fuelEfficiency))
world.eventLoop.start()
world.replaceCentralStructure(structure).join()
world.saveMetadata() world.saveMetadata()
} catch (err: Throwable) { } catch (err: Throwable) {
world.eventLoop.shutdown() 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 clients = CopyOnWriteArrayList<ServerWorldTracker>()
val shouldStopOnIdle = worldID !is WorldID.ShipWorld val shouldStopOnIdle = worldID !is WorldID.ShipWorld
val spawner = MonsterSpawner(this)
private suspend fun doAcceptClient(client: ServerConnection, action: WarpAction?) { private suspend fun doAcceptClient(client: ServerConnection, action: WarpAction?) {
try { try {
@ -670,6 +671,7 @@ class ServerWorld private constructor(
wireProcessor.tick() wireProcessor.tick()
} }
spawner.tick()
super.tick(delta) super.tick(delta)
val packet = StepUpdatePacket(ticks) 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 { 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 { override fun toString(): String {

View File

@ -298,4 +298,48 @@ class Sky() {
return true 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 sky = Sky(template.skyParameters)
val geometry: WorldGeometry = template.geometry 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() val nextEntityID = AtomicInteger()
override fun getCell(x: Int, y: Int): AbstractCell { 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) { open fun tick(delta: Double) {
ticks++ ticks++
simulationTime += delta
if (dynamicEntities.size < 128) { if (dynamicEntities.size < 128) {
dynamicEntities.forEach { dynamicEntities.forEach {

View File

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

View File

@ -152,15 +152,15 @@ class ActorMovementController() : MovementController() {
fun calculateMovementParameters(base: ActorMovementParameters): MovementParameters { fun calculateMovementParameters(base: ActorMovementParameters): MovementParameters {
val mass = base.mass val mass = base.mass
val gravityMultiplier = base.gravityMultiplier val gravityMultiplier = base.gravityMultiplier ?: Globals.movementParameters.gravityMultiplier
val liquidBuoyancy = base.liquidBuoyancy val liquidBuoyancy = base.liquidBuoyancy ?: Globals.movementParameters.liquidBuoyancy
val airBuoyancy = base.airBuoyancy val airBuoyancy = base.airBuoyancy ?: Globals.movementParameters.airBuoyancy
val bounceFactor = base.bounceFactor val bounceFactor = base.bounceFactor ?: Globals.movementParameters.bounceFactor
val stopOnFirstBounce = base.stopOnFirstBounce val stopOnFirstBounce = base.stopOnFirstBounce ?: Globals.movementParameters.stopOnFirstBounce
val enableSurfaceSlopeCorrection = base.enableSurfaceSlopeCorrection val enableSurfaceSlopeCorrection = base.enableSurfaceSlopeCorrection ?: Globals.movementParameters.enableSurfaceSlopeCorrection
val slopeSlidingFactor = base.slopeSlidingFactor val slopeSlidingFactor = base.slopeSlidingFactor ?: Globals.movementParameters.slopeSlidingFactor
val maxMovementPerStep = base.maxMovementPerStep val maxMovementPerStep = base.maxMovementPerStep ?: Globals.movementParameters.maxMovementPerStep
val collisionPoly = if (isCrouching) base.crouchingPoly else base.standingPoly 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() newChatMessageEvent.trigger()
} }
override val isPersistent: Boolean override var isPersistent: Boolean = variant.commonParameters.persistent
get() = variant.commonParameters.persistent
override fun writeNetwork(stream: DataOutputStream, isLegacy: Boolean) { override fun writeNetwork(stream: DataOutputStream, isLegacy: Boolean) {
variant.write(stream, isLegacy) variant.write(stream, isLegacy)
@ -287,7 +286,7 @@ class MonsterEntity(val variant: MonsterVariant, level: Double? = null) : ActorE
} }
override val metaBoundingBox: AABB override val metaBoundingBox: AABB
get() = variant.commonParameters.metaBoundBox get() = variant.commonParameters.metaBoundBox + position
override val mouthPosition: Vector2d override val mouthPosition: Vector2d
get() = movement.getAbsolutePosition(variant.commonParameters.mouthOffset) get() = movement.getAbsolutePosition(variant.commonParameters.mouthOffset)
@ -325,12 +324,12 @@ class MonsterEntity(val variant: MonsterVariant, level: Double? = null) : ActorE
"sourceId" to damage.request.sourceEntityId, "sourceId" to damage.request.sourceEntityId,
"damage" to totalDamage, "damage" to totalDamage,
"sourceDamage" to damage.request.damage, "sourceDamage" to damage.request.damage,
"sourceKind" to damage.request.kind "sourceKind" to damage.request.damageSourceKind
)) ))
} }
if (health <= 0.0) { if (health <= 0.0) {
deathDamageKinds.add(damage.request.kind) deathDamageKinds.add(damage.request.damageSourceKind)
} }
return notifications return notifications
@ -338,7 +337,7 @@ class MonsterEntity(val variant: MonsterVariant, level: Double? = null) : ActorE
private val shouldDie: Boolean get() { private val shouldDie: Boolean get() {
val result = lua.invokeGlobal("shouldDie") 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) { 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: Once we have brand new object-oriented Lua API, expose proper entity bindings here
// TODO: Expose world bindings // 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, // used as place to store random data by Lua scripts, because global `storage` table wasn't enough, it seems,
// to Chucklefish devs // to Chucklefish devs
private val statusProperties = networkedJsonObject(config.statusProperties.deepCopy()).also { networkGroup.add(it) } 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) else if (parameter.value is JsonPrimitive && parameter.value.isString)
str = parameter.value.asString str = parameter.value.asString
if (str != null) { if (!str.isNullOrEmpty()) {
if (str.first() == '<' && str.last() == '>') { 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] val param = treeParameters[treeKey]
if (param != null) { if (param != null) {
return param return param
} else { } 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) { if (output == null) {
return null return null
} else if (output.first() == '<' && output.last() == '>') { } 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) if (replacement.key != null)
return replacement.key return replacement.key
@ -96,7 +96,7 @@ abstract class AbstractBehaviorNode {
val module = BehaviorTree(tree.blackboard, Registries.behavior.getOrThrow(name).value, moduleParameters) val module = BehaviorTree(tree.blackboard, Registries.behavior.getOrThrow(name).value, moduleParameters)
tree.scripts.addAll(module.scripts) tree.scripts.addAll(module.scripts)
tree.functions.addAll(module.functions) tree.functions.addAll(module.functions)
return tree.root return module.root
} }
val parameters = LinkedHashMap(Registries.behaviorNodes.getOrThrow(name).value.properties) 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() { class ActionNode(val name: String, val parameters: Map<String, NodeParameter>, val outputs: Map<String, NodeOutput>) : AbstractBehaviorNode() {
private var coroutine: Coroutine? = null private var coroutine: Coroutine? = null
private val nodeID = BehaviorState.NODE_GARBAGE_INDEX.getAndIncrement()
override fun run(delta: Double, state: BehaviorState): Status { override fun run(delta: Double, state: BehaviorState): Status {
var coroutine = coroutine var coroutine = coroutine
@ -27,30 +28,34 @@ class ActionNode(val name: String, val parameters: Map<String, NodeParameter>, v
try { try {
val result = if (firstTime) { val result = if (firstTime) {
val parameters = state.blackboard.parameters(parameters, this) 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 { } else {
state.lua.call(CoroutineLib.resume(), coroutine, delta) state.lua.call(CoroutineLib.resume(), coroutine, delta)
} }
val status = result[0] as Boolean val coroutineStatus = result[0] as Boolean
if (result.size >= 2) { if (!coroutineStatus) {
val second = result[1] as? Table LOGGER.warn("Behavior ActionNode '$name' failed: ${result[1]}")
return Status.FAILURE
if (second != null) {
state.blackboard.setOutput(this, second)
}
} }
val isDead = state.lua.call(CoroutineLib.status(), coroutine)[0] == "dead" if (result.size == 1) {
if (!status) {
return Status.FAILURE
} else if (isDead) {
return Status.SUCCESS
} else {
return Status.RUNNING 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) { } catch (err: CallPausedException) {
LOGGER.error("Behavior ActionNode '$name' called blocking code, which initiated pause. This is not supported.") LOGGER.error("Behavior ActionNode '$name' called blocking code, which initiated pause. This is not supported.")
return Status.FAILURE return Status.FAILURE
@ -61,6 +66,10 @@ class ActionNode(val name: String, val parameters: Map<String, NodeParameter>, v
coroutine = null coroutine = null
} }
override fun toString(): String {
return "ActionNode[$name, parameters=$parameters, outputs=$outputs, coroutine=$coroutine]"
}
companion object { companion object {
private val LOGGER = LogManager.getLogger() 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.Userdata
import org.classdump.luna.impl.ImmutableTable import org.classdump.luna.impl.ImmutableTable
import ru.dbotthepony.kstarbound.defs.actor.behavior.NodeParameter 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.LuaEnvironment
import ru.dbotthepony.kstarbound.lua.from import ru.dbotthepony.kstarbound.lua.from
import ru.dbotthepony.kstarbound.lua.get import ru.dbotthepony.kstarbound.lua.get
import ru.dbotthepony.kstarbound.lua.luaFunction import ru.dbotthepony.kstarbound.lua.luaFunction
import ru.dbotthepony.kstarbound.lua.set import ru.dbotthepony.kstarbound.lua.set
import ru.dbotthepony.kstarbound.lua.tableOf
import ru.dbotthepony.kstarbound.util.valueOf import ru.dbotthepony.kstarbound.util.valueOf
import java.util.EnumMap import java.util.EnumMap
import java.util.HashSet import java.util.HashSet
@ -36,7 +34,7 @@ import kotlin.collections.HashMap
*/ */
class Blackboard(val lua: LuaEnvironment) : Userdata<Blackboard>() { class Blackboard(val lua: LuaEnvironment) : Userdata<Blackboard>() {
private val board = EnumMap<NodeParameterType, HashMap<String, Any>>(NodeParameterType::class.java) 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>() private val parameters = HashMap<Any, Table>()
// key -> list of Lua tables and their indices // key -> list of Lua tables and their indices
private val vectorNumberInput = HashMap<String, HashSet<Pair<Any, Table>>>() private val vectorNumberInput = HashMap<String, HashSet<Pair<Any, Table>>>()
@ -45,6 +43,7 @@ class Blackboard(val lua: LuaEnvironment) : Userdata<Blackboard>() {
init { init {
for (v in NodeParameterType.entries) { for (v in NodeParameterType.entries) {
board[v] = HashMap() board[v] = HashMap()
input[v] = HashMap()
} }
} }
@ -58,8 +57,8 @@ class Blackboard(val lua: LuaEnvironment) : Userdata<Blackboard>() {
val input = input[type]!![key] val input = input[type]!![key]
if (input != null) { if (input != null) {
for ((k1, k2) in input) { for ((index, table) in input) {
parameters[k1]!![k2] = value table[index] = value
} }
} }
@ -90,8 +89,8 @@ class Blackboard(val lua: LuaEnvironment) : Userdata<Blackboard>() {
for ((name, parameter) in parameters.entries) { for ((name, parameter) in parameters.entries) {
if (parameter.value.key != null) { if (parameter.value.key != null) {
val typeInput = input[parameter.type]!!.computeIfAbsent(parameter.value.key) { ArrayList() } val typeInput = input[parameter.type]!!.computeIfAbsent(parameter.value.key) { ArrayList() }
typeInput.add(nodeID to name) typeInput.add(name to table)
table[name] = this[parameter.type, name] table[name] = this[parameter.type, parameter.value.key]
} else { } else {
val value = parameter.value.value ?: JsonNull.INSTANCE val value = parameter.value.value ?: JsonNull.INSTANCE
@ -167,6 +166,7 @@ class Blackboard(val lua: LuaEnvironment) : Userdata<Blackboard>() {
companion object { companion object {
private val metatable: ImmutableTable private val metatable: ImmutableTable
private fun __index() = metatable
init { init {
val builder = ImmutableTable.Builder() 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() metatable = builder.build()
} }
} }

View File

@ -1,14 +1,18 @@
package ru.dbotthepony.kstarbound.world.entities.behavior package ru.dbotthepony.kstarbound.world.entities.behavior
import org.apache.logging.log4j.LogManager 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.exec.CallPausedException
import org.classdump.luna.lib.CoroutineLib import org.classdump.luna.lib.CoroutineLib
import org.classdump.luna.runtime.Coroutine import org.classdump.luna.runtime.Coroutine
import ru.dbotthepony.kstarbound.defs.actor.behavior.NodeParameter import ru.dbotthepony.kstarbound.defs.actor.behavior.NodeParameter
import ru.dbotthepony.kstarbound.lua.userdata.BehaviorState 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() { class DecoratorNode(val name: String, val parameters: Map<String, NodeParameter>, val child: AbstractBehaviorNode) : AbstractBehaviorNode() {
private var coroutine: Coroutine? = null private var coroutine: Coroutine? = null
private val nodeID = BehaviorState.NODE_GARBAGE_INDEX.getAndIncrement()
override fun run(delta: Double, state: BehaviorState): Status { override fun run(delta: Double, state: BehaviorState): Status {
var coroutine = coroutine var coroutine = coroutine
@ -19,14 +23,28 @@ class DecoratorNode(val name: String, val parameters: Map<String, NodeParameter>
try { try {
coroutine = state.lua.call(CoroutineLib.create(), fn)[0] as Coroutine 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 val status = result[0] as Boolean
if (!status && result.size >= 2) { if (!status) {
LOGGER.warn("Behavior DecoratorNode '$name' failed: ${result[1]}") 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) { } catch (err: CallPausedException) {
LOGGER.error("Behavior DecoratorNode '$name' called blocking code, which initiated pause. This is not supported.") LOGGER.error("Behavior DecoratorNode '$name' called blocking code, which initiated pause. This is not supported.")
return Status.FAILURE 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]}") 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) { } catch (err: CallPausedException) {
LOGGER.error("Behavior DecoratorNode '$name' called blocking code on children return, which initiated pause. This is not supported.") LOGGER.error("Behavior DecoratorNode '$name' called blocking code on children return, which initiated pause. This is not supported.")
status = Status.FAILURE status = Status.FAILURE
@ -65,6 +99,10 @@ class DecoratorNode(val name: String, val parameters: Map<String, NodeParameter>
coroutine = null coroutine = null
} }
override fun toString(): String {
return "DecoratorNode[$name, coroutine=$coroutine, parameters=$parameters, children=$child]"
}
companion object { companion object {
private val LOGGER = LogManager.getLogger() private val LOGGER = LogManager.getLogger()
} }

View File

@ -26,6 +26,10 @@ class DynamicNode(val children: ImmutableList<AbstractBehaviorNode>) : AbstractB
return Status.RUNNING return Status.RUNNING
} }
override fun toString(): String {
return "DynamicNode[${children.size}, index=$index, current=${children.getOrNull(index)}]"
}
override fun reset() { override fun reset() {
children.forEach { it.reset() } children.forEach { it.reset() }
index = 0 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 { override fun run(delta: Double, state: BehaviorState): Status {
var failed = 0 var failed = 0
var succeeded = 0 var succeeded = 0
@ -43,14 +46,24 @@ class ParallelNode(parameters: Map<String, NodeParameter>, val children: Immutab
failed++ failed++
if (succeeded >= successLimit || failed >= failLimit) { if (succeeded >= successLimit || failed >= failLimit) {
lastFailed = failed
lastSucceeded = succeeded
return if (succeeded >= successLimit) Status.SUCCESS else Status.FAILURE return if (succeeded >= successLimit) Status.SUCCESS else Status.FAILURE
} }
} }
lastFailed = failed
lastSucceeded = succeeded
return Status.RUNNING return Status.RUNNING
} }
override fun toString(): String {
return "ParallelNode[children=${children.size}, successLimit=$successLimit, failLimit=$failLimit, lastFailed=$lastFailed, lastSucceeded=$lastSucceeded]"
}
override fun reset() { override fun reset() {
children.forEach { it.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) 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() { override fun reset() {
children.forEach { it.reset() } children.forEach { it.reset() }
index = -1 index = -1

View File

@ -21,6 +21,10 @@ class SequenceNode(val children: ImmutableList<AbstractBehaviorNode>) : Abstract
return Status.SUCCESS return Status.SUCCESS
} }
override fun toString(): String {
return "SequenceNode[${children.size}, index=$index, current=${children.getOrNull(index)}]"
}
override fun reset() { override fun reset() {
children.forEach { it.reset() } children.forEach { it.reset() }
index = 0 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.LuaUpdateComponent
import ru.dbotthepony.kstarbound.lua.bindings.provideAnimatorBindings import ru.dbotthepony.kstarbound.lua.bindings.provideAnimatorBindings
import ru.dbotthepony.kstarbound.lua.bindings.provideEntityBindings 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.from
import ru.dbotthepony.kstarbound.lua.get import ru.dbotthepony.kstarbound.lua.get
import ru.dbotthepony.kstarbound.lua.set import ru.dbotthepony.kstarbound.lua.set
@ -849,7 +847,7 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
damage.request.damage, damage.request.damage,
dmg, dmg,
if (health <= 0.0) HitType.KILL else HitType.HIT, if (health <= 0.0) HitType.KILL else HitType.HIT,
damage.request.kind, damage.request.damageSourceKind,
config.value.damageMaterialKind config.value.damageMaterialKind
) )
) )