From a17bb2a7321bfce7f2005c153e97a5927b44c225 Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Sat, 13 Jul 2024 18:05:17 +0700 Subject: [PATCH] Don't know why, but all monsters do is constantly walk to left --- .../ru/dbotthepony/kstarbound/Globals.kt | 13 +- .../ru/dbotthepony/kstarbound/Registries.kt | 37 ++ .../kstarbound/defs/ColorReplacements.kt | 2 +- .../ru/dbotthepony/kstarbound/defs/Damage.kt | 19 +- .../defs/actor/behavior/NodeParameter.kt | 4 + .../defs/actor/behavior/NodeParameterValue.kt | 7 + .../defs/monster/MonsterTypeDefinition.kt | 1 + .../kstarbound/defs/monster/MonsterVariant.kt | 4 +- .../kstarbound/defs/world/Biome.kt | 1 + .../kstarbound/defs/world/BiomeDefinition.kt | 7 +- .../dbotthepony/kstarbound/defs/world/Sky.kt | 1 + .../kstarbound/defs/world/SpawnArea.kt | 18 + .../kstarbound/defs/world/SpawnParameters.kt | 53 +++ .../kstarbound/defs/world/SpawnProfile.kt | 39 ++ .../kstarbound/defs/world/SpawnRegion.kt | 25 ++ .../kstarbound/defs/world/SpawnTime.kt | 25 ++ .../kstarbound/defs/world/SpawnType.kt | 42 +++ .../kstarbound/defs/world/SpawnerConfig.kt | 48 +++ .../defs/{ => world}/WorldServerConfig.kt | 3 +- ...Adapter.kt => Immutable2MapTypeAdapter.kt} | 20 +- .../ImmutableCollectionAdapterFactory.kt | 2 +- .../dbotthepony/kstarbound/lua/Functions.kt | 12 + .../kstarbound/lua/LuaEnvironment.kt | 16 +- .../lua/bindings/MonsterBindings.kt | 3 +- .../kstarbound/lua/bindings/RootBindings.kt | 41 +- .../lua/bindings/StatusControllerBindings.kt | 14 +- .../lua/bindings/UtilityBindings.kt | 7 + .../kstarbound/lua/bindings/WorldBindings.kt | 6 + .../kstarbound/lua/userdata/BehaviorState.kt | 5 + .../ru/dbotthepony/kstarbound/math/AABB.kt | 12 + .../kstarbound/server/StarboundServer.kt | 10 +- .../kstarbound/server/world/MonsterSpawner.kt | 357 ++++++++++++++++++ .../kstarbound/server/world/ServerWorld.kt | 2 + .../dbotthepony/kstarbound/world/ChunkPos.kt | 9 +- .../ru/dbotthepony/kstarbound/world/Sky.kt | 44 +++ .../ru/dbotthepony/kstarbound/world/World.kt | 17 + .../kstarbound/world/entities/ActorEntity.kt | 2 +- .../world/entities/ActorMovementController.kt | 16 +- .../world/entities/MonsterEntity.kt | 11 +- .../world/entities/StatusController.kt | 8 +- .../entities/behavior/AbstractBehaviorNode.kt | 10 +- .../world/entities/behavior/ActionNode.kt | 39 +- .../world/entities/behavior/Blackboard.kt | 15 +- .../world/entities/behavior/DecoratorNode.kt | 46 ++- .../world/entities/behavior/DynamicNode.kt | 4 + .../world/entities/behavior/ParallelNode.kt | 13 + .../world/entities/behavior/RandomizeNode.kt | 7 + .../world/entities/behavior/SequenceNode.kt | 4 + .../world/entities/tile/WorldObject.kt | 4 +- 49 files changed, 1011 insertions(+), 94 deletions(-) create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/SpawnArea.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/SpawnParameters.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/SpawnProfile.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/SpawnRegion.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/SpawnTime.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/SpawnType.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/SpawnerConfig.kt rename src/main/kotlin/ru/dbotthepony/kstarbound/defs/{ => world}/WorldServerConfig.kt (80%) rename src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/{ImmutableArrayMapTypeAdapter.kt => Immutable2MapTypeAdapter.kt} (60%) create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/server/world/MonsterSpawner.kt diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Globals.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Globals.kt index 0db1f5d9..be905901 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Globals.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Globals.kt @@ -13,9 +13,11 @@ import org.apache.logging.log4j.LogManager import ru.dbotthepony.kstarbound.defs.ActorMovementParameters import ru.dbotthepony.kstarbound.defs.ClientConfig import ru.dbotthepony.kstarbound.defs.CurrencyDefinition +import ru.dbotthepony.kstarbound.defs.ElementalDamageType import ru.dbotthepony.kstarbound.defs.MovementParameters +import ru.dbotthepony.kstarbound.defs.world.SpawnerConfig import ru.dbotthepony.kstarbound.defs.UniverseServerConfig -import ru.dbotthepony.kstarbound.defs.WorldServerConfig +import ru.dbotthepony.kstarbound.defs.world.WorldServerConfig import ru.dbotthepony.kstarbound.defs.actor.player.PlayerConfig import ru.dbotthepony.kstarbound.defs.actor.player.ShipUpgrades import ru.dbotthepony.kstarbound.defs.item.ItemDropConfig @@ -34,7 +36,6 @@ import ru.dbotthepony.kstarbound.defs.world.SystemWorldConfig import ru.dbotthepony.kstarbound.defs.world.SystemWorldObjectConfig import ru.dbotthepony.kstarbound.defs.world.WorldTemplateConfig import ru.dbotthepony.kstarbound.json.listAdapter -import ru.dbotthepony.kstarbound.json.mapAdapter import ru.dbotthepony.kstarbound.util.AssetPathStack import java.util.concurrent.CompletableFuture import java.util.concurrent.Future @@ -122,6 +123,12 @@ object Globals { var quests by Delegates.notNull() private set + var spawner by Delegates.notNull() + private set + + var elementalTypes by Delegates.notNull>() + private set + private var profanityFilterInternal by Delegates.notNull>() val profanityFilter: ImmutableSet by lazy { @@ -229,11 +236,13 @@ object Globals { tasks.add(load("/tiles/defaultDamage.config", ::tileDamage)) tasks.add(load("/ships/shipupgrades.config", ::shipUpgrades)) tasks.add(load("/quests/quests.config", ::quests)) + tasks.add(load("/spawning.config", ::spawner)) tasks.add(Starbound.GLOBAL_SCOPE.launch { load("/dungeon_worlds.config", ::dungeonWorlds, mapAdapter("/dungeon_worlds.config")) }.asCompletableFuture()) tasks.add(Starbound.GLOBAL_SCOPE.launch { load("/currencies.config", ::currencies, mapAdapter("/currencies.config")) }.asCompletableFuture()) tasks.add(Starbound.GLOBAL_SCOPE.launch { load("/system_objects.config", ::systemObjects, mapAdapter("/system_objects.config")) }.asCompletableFuture()) tasks.add(Starbound.GLOBAL_SCOPE.launch { load("/instance_worlds.config", ::instanceWorlds, mapAdapter("/instance_worlds.config")) }.asCompletableFuture()) + tasks.add(Starbound.GLOBAL_SCOPE.launch { load("/damage/elementaltypes.config", ::elementalTypes, mapAdapter("/damage/elementaltypes.config")) }.asCompletableFuture()) tasks.add(Starbound.GLOBAL_SCOPE.launch { load("/names/profanityfilter.config", ::profanityFilterInternal, lazy { Starbound.gson.listAdapter() }) }.asCompletableFuture()) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt index da2a03b8..b75dce63 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt @@ -3,6 +3,7 @@ package ru.dbotthepony.kstarbound import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableMap import com.google.gson.GsonBuilder +import com.google.gson.JsonArray import com.google.gson.JsonObject import com.google.gson.JsonSyntaxException import com.google.gson.TypeAdapterFactory @@ -15,6 +16,7 @@ import kotlinx.coroutines.launch import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kstarbound.defs.AssetReference +import ru.dbotthepony.kstarbound.defs.DamageKind import ru.dbotthepony.kstarbound.defs.Json2Function import ru.dbotthepony.kstarbound.defs.JsonConfigFunction import ru.dbotthepony.kstarbound.defs.JsonFunction @@ -47,6 +49,7 @@ import ru.dbotthepony.kstarbound.defs.world.BushVariant import ru.dbotthepony.kstarbound.defs.world.GrassVariant import ru.dbotthepony.kstarbound.defs.world.TreeVariant import ru.dbotthepony.kstarbound.defs.world.BiomeDefinition +import ru.dbotthepony.kstarbound.defs.world.SpawnType import ru.dbotthepony.kstarbound.item.ItemRegistry import ru.dbotthepony.kstarbound.json.JsonPatch import ru.dbotthepony.kstarbound.json.builder.JsonFactory @@ -91,6 +94,7 @@ object Registries { val treasureChests = Registry("treasure chest").also(registriesInternal::add).also { adapters.add(it.adapter()) } val monsterSkills = Registry("monster skill").also(registriesInternal::add).also { adapters.add(it.adapter()) } val monsterTypes = Registry("monster type").also(registriesInternal::add).also { adapters.add(it.adapter()) } + val spawnTypes = Registry("spawn type").also(registriesInternal::add).also { adapters.add(it.adapter()) } val monsterPalettes = Registry("monster palette").also(registriesInternal::add).also { adapters.add(it.adapter()) } val behavior = Registry("behavior").also(registriesInternal::add).also { adapters.add(it.adapter()) } val behaviorNodes = Registry("behavior node").also(registriesInternal::add).also { adapters.add(it.adapter()) } @@ -103,6 +107,7 @@ object Registries { val bushVariants = Registry("bush variant").also(registriesInternal::add).also { adapters.add(it.adapter()) } val dungeons = Registry("dungeon").also(registriesInternal::add).also { adapters.add(it.adapter()) } val markovGenerators = Registry("markov text generator").also(registriesInternal::add).also { adapters.add(it.adapter()) } + val damageKinds = Registry("damage kind").also(registriesInternal::add).also { adapters.add(it.adapter()) } private val monsterParts = HashMap, HashMap>>() private val loggedMisses = Collections.synchronizedSet(ObjectOpenHashSet>()) @@ -240,6 +245,7 @@ object Registries { tasks.addAll(loadRegistry(markovGenerators, patchTree, fileTree["namesource"] ?: listOf(), key(MarkovTextGenerator::name))) tasks.addAll(loadRegistry(projectiles, patchTree, fileTree["projectile"] ?: listOf(), key(ProjectileDefinition::projectileName))) tasks.addAll(loadRegistry(behavior, patchTree, fileTree["behavior"] ?: listOf(), key(BehaviorDefinition::name))) + tasks.addAll(loadRegistry(damageKinds, patchTree, fileTree["damage"] ?: listOf(), key(DamageKind::kind))) tasks.addAll(loadCombined(behaviorNodes, fileTree["nodes"] ?: listOf(), patchTree)) @@ -249,6 +255,10 @@ object Registries { tasks.addAll(loadCombined(treasureChests, fileTree["treasurechests"] ?: listOf(), patchTree) { name = it }) tasks.addAll(loadCombined(treasurePools, fileTree["treasurepools"] ?: listOf(), patchTree) { name = it }) + // because someone couldn't handle their mushroom vine that day, and decided to make third way of + // declaring game data + tasks.addAll(loadMixed(spawnTypes, fileTree["spawntypes"] ?: listOf(), patchTree, SpawnType::name)) + return tasks } @@ -279,6 +289,33 @@ object Registries { } } + private inline fun loadMixed(registry: Registry, files: Collection, patches: Map>, noinline key: T.() -> String): List> { + 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>) { try { val json = JsonPatch.applyAsync(listedFile.asyncJsonReader(), patches[listedFile.computeFullPath()]) as JsonObject diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ColorReplacements.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ColorReplacements.kt index 20a95f6b..93338b52 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ColorReplacements.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ColorReplacements.kt @@ -20,7 +20,7 @@ class ColorReplacements private constructor(private val mapping: Int2IntOpenHash } fun toImageOperator(): String { - return "replace;${mapping.int2IntEntrySet().joinToString { "${RGBAColor.rgb(it.intKey).toHexStringRGB()}=${RGBAColor.rgb(it.intValue).toHexStringRGB()}" }}" + return "replace;${mapping.int2IntEntrySet().joinToString(";") { "${RGBAColor.rgb(it.intKey).toHexStringRGB().substring(1)}=${RGBAColor.rgb(it.intValue).toHexStringRGB().substring(1)}" }}" } class Adapter(gson: Gson) : TypeAdapter() { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Damage.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Damage.kt index 79aff28e..244cc997 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Damage.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Damage.kt @@ -1,8 +1,10 @@ package ru.dbotthepony.kstarbound.defs import com.google.common.collect.ImmutableList +import com.google.common.collect.ImmutableMap import com.google.common.collect.ImmutableSet import com.google.gson.Gson +import com.google.gson.JsonObject import com.google.gson.JsonSyntaxException import com.google.gson.TypeAdapter import com.google.gson.annotations.JsonAdapter @@ -81,6 +83,13 @@ enum class DamageType(override val jsonName: String) : IStringSerializable { STATUS("Environment"); } +@JsonFactory +data class DamageKind( + val kind: String, + val elementalType: String = "default", + val effects: JsonObject = JsonObject() +) + @JsonFactory data class EntityDamageTeam(val type: TeamType = TeamType.NULL, val team: Int = 0) { constructor(stream: DataInputStream, isLegacy: Boolean) : this(TeamType.entries[stream.readUnsignedByte()], stream.readUnsignedShort()) @@ -189,10 +198,10 @@ data class DamageData( val hitType: HitType, val damageType: DamageType, val damage: Double, - val knockback: Vector2d, + val knockbackMomentum: Vector2d, val sourceEntityId: Int, val inflictorEntityId: Int = 0, - val kind: String, + val damageSourceKind: String, val statusEffects: Collection, ) { constructor(stream: DataInputStream, isLegacy: Boolean, inflictorEntityId: Int) : this( @@ -213,9 +222,9 @@ data class DamageData( stream.writeEnumStupid(hitType.ordinal, isLegacy) stream.writeByte(damageType.ordinal) stream.writeDouble(damage, isLegacy) - stream.writeStruct2d(knockback, isLegacy) + stream.writeStruct2d(knockbackMomentum, isLegacy) stream.writeInt(sourceEntityId) - stream.writeBinaryString(kind) + stream.writeBinaryString(damageSourceKind) stream.writeCollection(statusEffects) { it.write(this, isLegacy) } } } @@ -363,3 +372,5 @@ data class DamageSource( val LEGACY_CODEC = legacyCodec(::DamageSource, DamageSource::write) } } + +data class ElementalDamageType(val resistanceStat: String, val damageNumberParticles: ImmutableMap) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/NodeParameter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/NodeParameter.kt index 4efa4c91..4a489615 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/NodeParameter.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/NodeParameter.kt @@ -13,6 +13,10 @@ import ru.dbotthepony.kstarbound.world.entities.behavior.NodeParameterType @JsonAdapter(NodeParameter.Adapter::class) data class NodeParameter(val type: NodeParameterType, val value: NodeParameterValue) { + override fun toString(): String { + return "[$type as $value]" + } + class Adapter : TypeAdapter() { override fun write(out: JsonWriter, value: NodeParameter) { out.beginObject() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/NodeParameterValue.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/NodeParameterValue.kt index 9d575d17..97f46b9c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/NodeParameterValue.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/NodeParameterValue.kt @@ -17,6 +17,13 @@ import ru.dbotthepony.kstarbound.json.popObject */ @JsonAdapter(NodeParameterValue.Adapter::class) data class NodeParameterValue(val key: String?, val value: JsonElement?) { + override fun toString(): String { + if (key != null) + return "key=$key" + else + return "value=$value" + } + class Adapter : TypeAdapter() { override fun write(out: JsonWriter, value: NodeParameterValue) { if (value.key != null) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/monster/MonsterTypeDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/monster/MonsterTypeDefinition.kt index ac5b0254..22b950c6 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/monster/MonsterTypeDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/monster/MonsterTypeDefinition.kt @@ -256,6 +256,7 @@ data class MonsterTypeDefinition( if (skillNames.isNotEmpty()) { animationConfig = animationConfig.deepCopy() val allParameters = ArrayList() + allParameters.add(parameters) for (skillName in skillNames) { val skill = Registries.monsterSkills[skillName] ?: continue diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/monster/MonsterVariant.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/monster/MonsterVariant.kt index 230abdcb..02aa59d5 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/monster/MonsterVariant.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/monster/MonsterVariant.kt @@ -37,7 +37,7 @@ import java.io.DataOutputStream import java.util.random.RandomGenerator @JsonFactory -class MonsterVariant( +data class MonsterVariant( val type: String, val shortDescription: String? = null, val description: String? = null, @@ -116,7 +116,7 @@ class MonsterVariant( val actualDropPools: ImmutableList>, Registry.Ref>> get() = commonParameters.dropPools ?: dropPools - val chosenDropPool: Either>, Registry.Ref>? = if (actualDropPools.isEmpty()) null else actualDropPools[staticRandomInt(0, actualDropPools.size, seed, "MonsterDropPool")] + val chosenDropPool: Either>, Registry.Ref>? = if (actualDropPools.isEmpty()) null else actualDropPools[staticRandomInt(0, actualDropPools.size - 1, seed, "MonsterDropPool")] fun write(stream: DataOutputStream, isLegacy: Boolean) { if (isLegacy) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Biome.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Biome.kt index 06c39cb9..4d748db0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Biome.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Biome.kt @@ -21,6 +21,7 @@ data class Biome( val surfacePlaceables: BiomePlaceables = BiomePlaceables(), val undergroundPlaceables: BiomePlaceables = BiomePlaceables(), val parallax: Parallax? = null, + val spawnProfile: SpawnProfile? = null, ) { @JvmName("hueShiftTile") fun hueShift(block: Registry.Entry): Float { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BiomeDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BiomeDefinition.kt index c420a6be..315997b8 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BiomeDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BiomeDefinition.kt @@ -2,6 +2,8 @@ package ru.dbotthepony.kstarbound.defs.world import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableSet +import com.google.gson.JsonElement +import com.google.gson.JsonObject import ru.dbotthepony.kommons.collect.filterNotNull import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Registry @@ -35,6 +37,7 @@ data class BiomeDefinition( val surfacePlaceables: BiomePlaceablesDefinition = BiomePlaceablesDefinition(), val undergroundPlaceables: BiomePlaceablesDefinition = BiomePlaceablesDefinition(), val parallax: AssetReference? = null, + val spawnProfile: JsonObject? = null, ) { data class CreationParams( val hueShift: Double, @@ -83,7 +86,9 @@ data class BiomeDefinition( .map { NativeLegacy.TileMod(Registries.tileModifiers.ref(it.first)) to it.second } .filter { it.first.native.isPresent } .collect(ImmutableList.toImmutableList()) - }?.orElse(ImmutableList.of()) ?: ImmutableList.of()) + }?.orElse(ImmutableList.of()) ?: ImmutableList.of()), + + spawnProfile = spawnProfile?.let { SpawnProfile.create(it, random) } ) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Sky.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Sky.kt index 2d750a13..3fcb10a6 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Sky.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Sky.kt @@ -438,6 +438,7 @@ data class SkyGlobalConfig( val starVelocityFactor: Double = 0.0, val flyingTimer: Double = 0.0, val flashTimer: Double = 1.0, + val dayTransitionTime: Double = 200.0, ) { @JsonFactory data class Stars( diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/SpawnArea.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/SpawnArea.kt new file mode 100644 index 00000000..85bcd742 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/SpawnArea.kt @@ -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 = Collections.unmodifiableSet(EnumSet.allOf(SpawnArea::class.java)) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/SpawnParameters.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/SpawnParameters.kt new file mode 100644 index 00000000..ede11e0d --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/SpawnParameters.kt @@ -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.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() { + 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) + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/SpawnProfile.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/SpawnProfile.kt new file mode 100644 index 00000000..8f0d0890 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/SpawnProfile.kt @@ -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> = ImmutableSet.of(), val monsterParameters: JsonObject = JsonObject()) { + companion object { + fun create(json: JsonObject, random: RandomGenerator): SpawnProfile { + val spawnTypes = ArrayList>() + + 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()) + } + } +} + diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/SpawnRegion.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/SpawnRegion.kt new file mode 100644 index 00000000..bc880656 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/SpawnRegion.kt @@ -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 +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/SpawnTime.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/SpawnTime.kt new file mode 100644 index 00000000..1bb86a71 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/SpawnTime.kt @@ -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 +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/SpawnType.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/SpawnType.kt new file mode 100644 index 00000000..14963fbc --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/SpawnType.kt @@ -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>, Registry.Ref>, + 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() + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/SpawnerConfig.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/SpawnerConfig.kt new file mode 100644 index 00000000..51179b34 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/SpawnerConfig.kt @@ -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>> = 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" } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/WorldServerConfig.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldServerConfig.kt similarity index 80% rename from src/main/kotlin/ru/dbotthepony/kstarbound/defs/WorldServerConfig.kt rename to src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldServerConfig.kt index cb4c1aa4..ac5b3c91 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/WorldServerConfig.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldServerConfig.kt @@ -1,7 +1,6 @@ -package ru.dbotthepony.kstarbound.defs +package ru.dbotthepony.kstarbound.defs.world import ru.dbotthepony.kstarbound.math.vector.Vector2d -import ru.dbotthepony.kstarbound.math.vector.Vector2i class WorldServerConfig( val playerSpaceStartRegionSize: Vector2d, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/ImmutableArrayMapTypeAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/Immutable2MapTypeAdapter.kt similarity index 60% rename from src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/ImmutableArrayMapTypeAdapter.kt rename to src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/Immutable2MapTypeAdapter.kt index dd6a2114..385e4519 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/ImmutableArrayMapTypeAdapter.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/Immutable2MapTypeAdapter.kt @@ -1,15 +1,16 @@ package ru.dbotthepony.kstarbound.json.factory -import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableMap +import com.google.gson.JsonPrimitive import com.google.gson.JsonSyntaxException import com.google.gson.TypeAdapter import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonWriter import ru.dbotthepony.kommons.gson.consumeNull +import ru.dbotthepony.kstarbound.json.FastJsonTreeReader -class ImmutableArrayMapTypeAdapter(val keyAdapter: TypeAdapter, val elementAdapter: TypeAdapter) : TypeAdapter>() { +class Immutable2MapTypeAdapter(val keyAdapter: TypeAdapter, val elementAdapter: TypeAdapter) : TypeAdapter>() { override fun write(out: JsonWriter, value: ImmutableMap?) { if (value == null) { out.nullValue() @@ -30,6 +31,21 @@ class ImmutableArrayMapTypeAdapter(val keyAdapter: TypeAdapter, val ele if (reader.consumeNull()) return null + if (reader.peek() == JsonToken.BEGIN_OBJECT) { + reader.beginObject() + + val builder = ImmutableMap.Builder() + + while (reader.peek() !== JsonToken.END_OBJECT) { + builder.put( + keyAdapter.read(FastJsonTreeReader(JsonPrimitive(reader.nextName()))) ?: throw JsonSyntaxException("Nulls are not allowed, near ${reader.path}"), + elementAdapter.read(reader) ?: throw JsonSyntaxException("Nulls are not allowed, near ${reader.path}")) + } + + reader.endObject() + return builder.build() + } + reader.beginArray() val builder = ImmutableMap.Builder() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/ImmutableCollectionAdapterFactory.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/ImmutableCollectionAdapterFactory.kt index fe6527b0..59738b34 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/ImmutableCollectionAdapterFactory.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/ImmutableCollectionAdapterFactory.kt @@ -31,7 +31,7 @@ class ImmutableCollectionAdapterFactory(val stringInterner: Interner = I return ImmutableMapTypeAdapter(stringInterner, gson.getAdapter(TypeToken.get(elementType1))) as TypeAdapter } - return ImmutableArrayMapTypeAdapter( + return Immutable2MapTypeAdapter( gson.getAdapter(TypeToken.get(elementType0)), gson.getAdapter(TypeToken.get(elementType1)) ) as TypeAdapter diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Functions.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Functions.kt index f341784d..0c934f13 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Functions.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Functions.kt @@ -18,6 +18,10 @@ import org.classdump.luna.runtime.Dispatch import org.classdump.luna.runtime.ExecutionContext import org.classdump.luna.runtime.LuaFunction import org.classdump.luna.runtime.UnresolvedControlThrowable +import java.util.Spliterator +import java.util.Spliterators +import java.util.stream.Stream +import java.util.stream.StreamSupport import kotlin.math.max import kotlin.math.min @@ -111,6 +115,14 @@ operator fun Table.iterator(): Iterator> { } } +fun Table.spliterator(): Spliterator> { + return Spliterators.spliteratorUnknownSize(iterator(), Spliterator.ORDERED) +} + +fun Table.stream(): Stream> { + return StreamSupport.stream(spliterator(), false) +} + /** * to be used in places where we need to "unpack" table, like this: * diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaEnvironment.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaEnvironment.kt index fdf17c7c..56a41052 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaEnvironment.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaEnvironment.kt @@ -235,7 +235,11 @@ class LuaEnvironment : StateContext { } fun attach(script: ChunkFactory) { - scripts.add(script) + if (initCalled) { + executor.call(this@LuaEnvironment, script.newInstance(Variable(globals))) + } else { + scripts.add(script) + } } fun attach(scripts: Collection) { @@ -248,7 +252,9 @@ class LuaEnvironment : StateContext { } } else { for (script in scripts) { - attach(Starbound.loadScript(script.fullPath)) + if (loadedScripts.add(script.fullPath)) { + this.scripts.add(Starbound.loadScript(script.fullPath)) + } } } } @@ -274,7 +280,7 @@ class LuaEnvironment : StateContext { try { executor.call(this, script.newInstance(Variable(globals))) } catch (err: Throwable) { - errorState = true + // errorState = true LOGGER.error("Failed to attach script to environment", err) scripts.clear() return false @@ -290,7 +296,7 @@ class LuaEnvironment : StateContext { try { executor.call(this, init) } catch (err: Throwable) { - errorState = true + // errorState = true LOGGER.error("Exception on init()", err) return false } @@ -310,7 +316,7 @@ class LuaEnvironment : StateContext { return try { executor.call(this, load, *arguments) } catch (err: Throwable) { - errorState = true + // errorState = true LOGGER.error("Exception while calling global $name", err) arrayOf() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/MonsterBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/MonsterBindings.kt index 8d5ed346..28c9f268 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/MonsterBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/MonsterBindings.kt @@ -16,6 +16,7 @@ import ru.dbotthepony.kstarbound.lua.get import ru.dbotthepony.kstarbound.lua.iterator import ru.dbotthepony.kstarbound.lua.luaFunction import ru.dbotthepony.kstarbound.lua.set +import ru.dbotthepony.kstarbound.lua.stream import ru.dbotthepony.kstarbound.lua.toJson import ru.dbotthepony.kstarbound.lua.toJsonFromLua import ru.dbotthepony.kstarbound.lua.toVector2d @@ -76,7 +77,7 @@ fun provideMonsterBindings(self: MonsterEntity, lua: LuaEnvironment) { if (parts == null) { self.animationDamageParts.clear() } else { - val strings = parts.iterator().map { (_, v) -> v.toString() }.collect(ImmutableSet.toImmutableSet()) + val strings = parts.stream().map { (_, v) -> v.toString() }.collect(ImmutableSet.toImmutableSet()) self.animationDamageParts.removeIf { it !in strings } for (v in strings) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/RootBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/RootBindings.kt index b53699a1..a60e20a5 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/RootBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/RootBindings.kt @@ -1,5 +1,7 @@ package ru.dbotthepony.kstarbound.lua.bindings +import com.google.gson.JsonNull +import com.google.gson.JsonObject import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap import org.apache.logging.log4j.LogManager import org.classdump.luna.ByteString @@ -415,6 +417,32 @@ private val itemHasTag = luaFunction { identifier: ByteString, tag: ByteString - returnBuffer.setTo(tag.decode() in ItemRegistry[identifier.decode()].itemTags) } +private val monsterSkillParameter = luaFunction { skillName: ByteString, configParameterName: ByteString -> + val skill = Registries.monsterSkills[skillName.decode()] + + if (skill != null) { + returnBuffer.setTo(from(skill.value.config[configParameterName.decode()] ?: JsonNull.INSTANCE)) + } +} + +private val monsterParameters = luaFunction { monsterType: ByteString, seed: Number? -> + returnBuffer.setTo(from(Registries.monsterTypes.getOrThrow(monsterType.decode()).value.create(seed?.toLong() ?: 0L, JsonObject()).parameters)) +} + +private val monsterMovementSettings = luaFunction { monsterType: ByteString, seed: Number? -> + returnBuffer.setTo(from(Registries.monsterTypes.getOrThrow(monsterType.decode()).value.create(seed?.toLong() ?: 0L, JsonObject()).parameters["movementSettings"] ?: JsonObject())) +} + +private val elementalResistance = luaFunction { damageKindName: ByteString -> + returnBuffer.setTo(Globals.elementalTypes[Registries.damageKinds.getOrThrow(damageKindName.decode()).value.elementalType]!!.resistanceStat.toByteString()) +} + +private val dungeonMetadata = luaFunction { dungeon: ByteString -> + returnBuffer.setTo(from(Registries.dungeons.getOrThrow(dungeon.decode()).jsonObject["metadata"])) +} + +private val hasTech = registryDefExists(Registries.techs) + fun provideRootBindings(lua: LuaEnvironment) { val table = lua.newTable() lua.globals["root"] = table @@ -481,11 +509,11 @@ fun provideRootBindings(lua: LuaEnvironment) { table["createBiome"] = createBiome - table["monsterSkillParameter"] = luaStub("monsterSkillParameter") - table["monsterParameters"] = luaStub("monsterParameters") - table["monsterMovementSettings"] = luaStub("monsterMovementSettings") + table["monsterSkillParameter"] = monsterSkillParameter + table["monsterParameters"] = monsterParameters + table["monsterMovementSettings"] = monsterMovementSettings - table["hasTech"] = registryDefExists(Registries.techs) + table["hasTech"] = hasTech table["techType"] = techType table["techConfig"] = techConfig @@ -494,7 +522,6 @@ fun provideRootBindings(lua: LuaEnvironment) { table["collection"] = luaStub("collection") table["collectables"] = luaStub("collectables") - table["elementalResistance"] = luaStub("elementalResistance") - table["dungeonMetadata"] = luaStub("dungeonMetadata") - table["behavior"] = luaStub("behavior") + table["elementalResistance"] = elementalResistance + table["dungeonMetadata"] = dungeonMetadata } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/StatusControllerBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/StatusControllerBindings.kt index b0774c42..1f3a5c0b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/StatusControllerBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/StatusControllerBindings.kt @@ -63,12 +63,12 @@ fun provideStatusControllerBindings(self: StatusController, lua: LuaEnvironment) } callbacks["resource"] = luaFunction { name: ByteString -> - val resource = self.resources[name.decode()] ?: throw LuaRuntimeException("No such resource $name") + val resource = self.resources[name.decode()] ?: return@luaFunction returnBuffer.setTo(0.0) returnBuffer.setTo(resource.value) } callbacks["resourcePositive"] = luaFunction { name: ByteString -> - val resource = self.resources[name.decode()] ?: throw LuaRuntimeException("No such resource $name") + val resource = self.resources[name.decode()] ?: return@luaFunction returnBuffer.setTo(false) returnBuffer.setTo(resource.value > 0.0) } @@ -189,20 +189,20 @@ fun provideStatusControllerBindings(self: StatusController, lua: LuaEnvironment) callbacks["damageTakenSince"] = luaFunction { since: Number? -> val (list, newSince) = self.recentDamageReceived(since?.toLong() ?: 0L) - returnBuffer.setTo(tableOf(*list.map { Starbound.gson.toJsonTree(it) }.toTypedArray()), newSince) + returnBuffer.setTo(tableOf(*list.map { from(Starbound.gson.toJsonTree(it)) }.toTypedArray()), newSince) } callbacks["inflictedHitsSince"] = luaFunction { since: Number? -> val (list, newSince) = self.recentHitsDealt(since?.toLong() ?: 0L) returnBuffer.setTo(tableOf(*list.map { p -> - Starbound.gson.toJsonTree(p.second).also { it as JsonObject; it["targetEntityId"] = p.first } + from(Starbound.gson.toJsonTree(p.second).also { it as JsonObject; it["targetEntityId"] = p.first }) }.toTypedArray()), newSince) } callbacks["inflictedDamageSince"] = luaFunction { since: Number? -> val (list, newSince) = self.recentDamageDealt(since?.toLong() ?: 0L) - returnBuffer.setTo(tableOf(*list.map { Starbound.gson.toJsonTree(it) }.toTypedArray()), newSince) + returnBuffer.setTo(tableOf(*list.map { from(Starbound.gson.toJsonTree(it)) }.toTypedArray()), newSince) } callbacks["activeUniqueStatusEffectSummary"] = luaFunction { @@ -217,8 +217,8 @@ fun provideStatusControllerBindings(self: StatusController, lua: LuaEnvironment) returnBuffer.setTo(self.primaryDirectives.toByteString()) } - callbacks["setPrimaryDirectives"] = luaFunction { directives: ByteString -> - self.primaryDirectives = directives.decode().sbIntern() + callbacks["setPrimaryDirectives"] = luaFunction { directives: ByteString? -> + self.primaryDirectives = directives?.decode()?.sbIntern() ?: "" } callbacks["applySelfDamageRequest"] = luaFunction { damage: Table -> diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/UtilityBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/UtilityBindings.kt index f427e4fa..44171986 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/UtilityBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/UtilityBindings.kt @@ -19,6 +19,7 @@ import ru.dbotthepony.kstarbound.lua.nextOptionalFloat import ru.dbotthepony.kstarbound.lua.set import ru.dbotthepony.kstarbound.lua.toByteString import ru.dbotthepony.kstarbound.lua.toJson +import ru.dbotthepony.kstarbound.lua.toJsonFromLua import ru.dbotthepony.kstarbound.lua.toVector2d import ru.dbotthepony.kstarbound.lua.userdata.LuaPerlinNoise import ru.dbotthepony.kstarbound.lua.userdata.LuaRandomGenerator @@ -98,6 +99,10 @@ private val staticRandomI32Range = luaFunctionN("staticRandomI32Range") { returnBuffer.setTo(staticRandomLong(min, max, *it.copyRemaining())) } +private val mergeJson = luaFunction { a: Any?, b: Any? -> + returnBuffer.setTo(from(ru.dbotthepony.kstarbound.json.mergeJson(toJsonFromLua(a), toJsonFromLua(b)))) +} + fun provideUtilityBindings(lua: LuaEnvironment) { val table = lua.newTable() lua.globals["sb"] = table @@ -133,6 +138,8 @@ fun provideUtilityBindings(lua: LuaEnvironment) { table["staticRandomDoubleRange"] = staticRandomDoubleRange table["staticRandomI32Range"] = staticRandomI32Range table["staticRandomI64Range"] = staticRandomI32Range + + table["jsonMerge"] = mergeJson } fun provideConfigBindings(lua: LuaEnvironment, lookup: ExecutionContext.(path: JsonPath, ifMissing: Any?) -> Any?) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldBindings.kt index 7325a4f8..4f754d34 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldBindings.kt @@ -644,4 +644,10 @@ fun provideWorldBindings(self: World<*, *>, lua: LuaEnvironment) { if (self is ServerWorld) { provideServerWorldBindings(self, callbacks, lua) } + + // TODO + callbacks["debugPoint"] = luaFunction { } + callbacks["debugLine"] = luaFunction { } + callbacks["debugPoly"] = luaFunction { } + callbacks["debugText"] = luaFunction { } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/BehaviorState.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/BehaviorState.kt index 72e33f6f..5cf51ce8 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/BehaviorState.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/BehaviorState.kt @@ -25,6 +25,7 @@ import ru.dbotthepony.kstarbound.lua.toJsonFromLua import ru.dbotthepony.kstarbound.world.entities.behavior.AbstractBehaviorNode import ru.dbotthepony.kstarbound.world.entities.behavior.BehaviorTree import ru.dbotthepony.kstarbound.world.entities.behavior.Blackboard +import java.util.concurrent.atomic.AtomicLong class BehaviorState(val tree: BehaviorTree) : Userdata() { val blackboard get() = tree.blackboard @@ -71,6 +72,10 @@ class BehaviorState(val tree: BehaviorTree) : Userdata() { } companion object { + // required for Lua code + // in original engine, passes directly 32/64 bit pointer of *Node struct + val NODE_GARBAGE_INDEX = AtomicLong() + private fun __index(): Table { return metatable } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/math/AABB.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/math/AABB.kt index d6724ec3..3d69f981 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/math/AABB.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/math/AABB.kt @@ -47,6 +47,11 @@ data class AABB(val mins: Vector2d, val maxs: Vector2d) { operator fun times(other: Vector2d) = AABB(mins * other, maxs * other) operator fun div(other: Vector2d) = AABB(mins / other, maxs / other) + operator fun plus(other: Vector2i) = AABB(mins + other, maxs + other) + operator fun minus(other: Vector2i) = AABB(mins - other, maxs - other) + operator fun times(other: Vector2i) = AABB(mins * other, maxs * other) + operator fun div(other: Vector2i) = AABB(mins / other, maxs / other) + operator fun times(other: Double) = AABB(mins * other, maxs * other) operator fun div(other: Double) = AABB(mins / other, maxs / other) @@ -314,6 +319,13 @@ data class AABB(val mins: Vector2d, val maxs: Vector2d) { ) } + fun of(aabb: AABBi): AABB { + return AABB( + mins = Vector2d(aabb.mins.x.toDouble(), aabb.mins.y.toDouble()), + maxs = Vector2d(aabb.maxs.x.toDouble(), aabb.maxs.y.toDouble()), + ) + } + @JvmField val ZERO = AABB(Vector2d.ZERO, Vector2d.ZERO) @JvmField val NEVER = AABB(Vector2d(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY), Vector2d(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY)) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt index 97109712..27d29ca2 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt @@ -382,6 +382,8 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread if (config.useUniverseClock) world.sky.referenceClock = universeClock + world.spawner.active = config.spawningEnabled + world.eventLoop.start() world.prepare(true).await() } catch (err: Throwable) { @@ -439,14 +441,15 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread try { val structure = Globals.universeServer.speciesShips[connection.playerSpecies]?.firstOrNull()?.value?.get() ?: throw NoSuchElementException("No ship structure for species ${connection.playerSpecies}") - world.eventLoop.start() - world.replaceCentralStructure(structure).join() + + world.spawner.active = false val currentUpgrades = connection.shipUpgrades .apply(Globals.shipUpgrades) .apply(Starbound.gson.fromJson(structure.config.get("shipUpgrades") ?: throw NoSuchElementException("No shipUpgrades element in world structure config for species ${connection.playerSpecies}")) ?: throw NullPointerException("World structure config.shipUpgrades is null for species ${connection.playerSpecies}")) connection.shipUpgrades = currentUpgrades + world.setProperty("invinciblePlayers", JsonPrimitive(true)) world.setProperty("ship.level", JsonPrimitive(0)) world.setProperty("ship.fuel", JsonPrimitive(0)) @@ -454,6 +457,9 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread world.setProperty("ship.crewSize", JsonPrimitive(currentUpgrades.crewSize)) world.setProperty("ship.fuelEfficiency", JsonPrimitive(currentUpgrades.fuelEfficiency)) + world.eventLoop.start() + world.replaceCentralStructure(structure).join() + world.saveMetadata() } catch (err: Throwable) { world.eventLoop.shutdown() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/MonsterSpawner.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/MonsterSpawner.kt new file mode 100644 index 00000000..13884c08 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/MonsterSpawner.kt @@ -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() + private val spawnedEntities = ArrayList() + 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, 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() + + 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() + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt index a2c942da..7cbbc6dc 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt @@ -96,6 +96,7 @@ class ServerWorld private constructor( val clients = CopyOnWriteArrayList() val shouldStopOnIdle = worldID !is WorldID.ShipWorld + val spawner = MonsterSpawner(this) private suspend fun doAcceptClient(client: ServerConnection, action: WarpAction?) { try { @@ -670,6 +671,7 @@ class ServerWorld private constructor( wireProcessor.tick() } + spawner.tick() super.tick(delta) val packet = StepUpdatePacket(ticks) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/ChunkPos.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/ChunkPos.kt index 00ed4750..4ed31586 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/ChunkPos.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/ChunkPos.kt @@ -79,7 +79,14 @@ data class ChunkPos(val x: Int, val y: Int) : IStruct2i, Comparable { } 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 { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Sky.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Sky.kt index abd8dc22..d40f6105 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Sky.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Sky.kt @@ -298,4 +298,48 @@ class Sky() { return true } + + val dayLevel: Double get() { + // Turn the dayCycle value into a value that blends evenly between 0.0 at + // mid-night and 1.0 at mid-day and then back again. + + val dayCycle = dayCycle + + if (dayCycle < 1.0) + return dayCycle / 2.0 + 0.5 + else if (dayCycle > 3.0) + return (dayCycle - 3.0) / 2.0 + else + return 1.0 - (dayCycle - 1.0) / 2.0 + } + + val dayCycle: Double get() { + // Always middle of the night in orbit or warp space. + if (skyType == SkyType.ORBITAL || skyType == SkyType.WARP) + return 3.0 + + var transitionTime = Globals.sky.dayTransitionTime + val dayLength = dayLength + val timeOfDay = timeOfDay + + // Original sources put this situation as follows: + // This will misbehave badly if dayTransitionTime is greater than dayLength / 2 + // So, let's fix it then. + // If sunset/sunrise duration is greater than third of day length, then + // clamp sunset/sunrise to fourth of day length + if (transitionTime > dayLength / 3.0) { + transitionTime = dayLength / 4.0 + } + + // timeOfDay() is defined such that 0.0 is mid-dawn. For convenience, shift + // the time of day forwards such that 0.0 is the beginning of the morning. + val shiftedTime = (timeOfDay + transitionTime / 2.0) % dayLength + + // There are 5 times here, beginning of the morning, end of the morning, + // beginning of the evening, end of the evening, and then the beginning of + // the morning again (wrapping around). + + // TODO() + return 1.0 + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt index 61ad5480..f1da74dc 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt @@ -87,6 +87,22 @@ abstract class World, ChunkType : Chunk, ChunkType : Chunk) { super.onJoinWorld(world) - statusController.init(world) + statusController.init() } override fun tick(delta: Double) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ActorMovementController.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ActorMovementController.kt index c5ad9b99..016d48eb 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ActorMovementController.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ActorMovementController.kt @@ -152,15 +152,15 @@ class ActorMovementController() : MovementController() { fun calculateMovementParameters(base: ActorMovementParameters): MovementParameters { val mass = base.mass - val gravityMultiplier = base.gravityMultiplier + val gravityMultiplier = base.gravityMultiplier ?: Globals.movementParameters.gravityMultiplier - val liquidBuoyancy = base.liquidBuoyancy - val airBuoyancy = base.airBuoyancy - val bounceFactor = base.bounceFactor - val stopOnFirstBounce = base.stopOnFirstBounce - val enableSurfaceSlopeCorrection = base.enableSurfaceSlopeCorrection - val slopeSlidingFactor = base.slopeSlidingFactor - val maxMovementPerStep = base.maxMovementPerStep + val liquidBuoyancy = base.liquidBuoyancy ?: Globals.movementParameters.liquidBuoyancy + val airBuoyancy = base.airBuoyancy ?: Globals.movementParameters.airBuoyancy + val bounceFactor = base.bounceFactor ?: Globals.movementParameters.bounceFactor + val stopOnFirstBounce = base.stopOnFirstBounce ?: Globals.movementParameters.stopOnFirstBounce + val enableSurfaceSlopeCorrection = base.enableSurfaceSlopeCorrection ?: Globals.movementParameters.enableSurfaceSlopeCorrection + val slopeSlidingFactor = base.slopeSlidingFactor ?: Globals.movementParameters.slopeSlidingFactor + val maxMovementPerStep = base.maxMovementPerStep ?: Globals.movementParameters.maxMovementPerStep val collisionPoly = if (isCrouching) base.crouchingPoly else base.standingPoly diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MonsterEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MonsterEntity.kt index c91f2f9a..8fd41088 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MonsterEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MonsterEntity.kt @@ -253,8 +253,7 @@ class MonsterEntity(val variant: MonsterVariant, level: Double? = null) : ActorE newChatMessageEvent.trigger() } - override val isPersistent: Boolean - get() = variant.commonParameters.persistent + override var isPersistent: Boolean = variant.commonParameters.persistent override fun writeNetwork(stream: DataOutputStream, isLegacy: Boolean) { variant.write(stream, isLegacy) @@ -287,7 +286,7 @@ class MonsterEntity(val variant: MonsterVariant, level: Double? = null) : ActorE } override val metaBoundingBox: AABB - get() = variant.commonParameters.metaBoundBox + get() = variant.commonParameters.metaBoundBox + position override val mouthPosition: Vector2d get() = movement.getAbsolutePosition(variant.commonParameters.mouthOffset) @@ -325,12 +324,12 @@ class MonsterEntity(val variant: MonsterVariant, level: Double? = null) : ActorE "sourceId" to damage.request.sourceEntityId, "damage" to totalDamage, "sourceDamage" to damage.request.damage, - "sourceKind" to damage.request.kind + "sourceKind" to damage.request.damageSourceKind )) } if (health <= 0.0) { - deathDamageKinds.add(damage.request.kind) + deathDamageKinds.add(damage.request.damageSourceKind) } return notifications @@ -338,7 +337,7 @@ class MonsterEntity(val variant: MonsterVariant, level: Double? = null) : ActorE private val shouldDie: Boolean get() { val result = lua.invokeGlobal("shouldDie") - return result.isNotEmpty() && result[0] is Boolean && result[0] as Boolean || health <= 0.0 || lua.errorState + return result.isNotEmpty() && result[0] is Boolean && result[0] as Boolean || health <= 0.0 //|| lua.errorState } override fun tick(delta: Double) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/StatusController.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/StatusController.kt index 38a6f94a..9f1a6ebf 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/StatusController.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/StatusController.kt @@ -152,6 +152,7 @@ class StatusController(val entity: ActorEntity, val config: StatusControllerConf // TODO: Once we have brand new object-oriented Lua API, expose proper entity bindings here // TODO: Expose world bindings + lua.init() } } @@ -216,13 +217,6 @@ class StatusController(val entity: ActorEntity, val config: StatusControllerConf } } - fun init(world: World<*, *>) { - if (!entity.isRemote) { - provideWorldBindings(world, lua) - lua.init() - } - } - // used as place to store random data by Lua scripts, because global `storage` table wasn't enough, it seems, // to Chucklefish devs private val statusProperties = networkedJsonObject(config.statusProperties.deepCopy()).also { networkGroup.add(it) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/AbstractBehaviorNode.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/AbstractBehaviorNode.kt index 386779f4..339079f6 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/AbstractBehaviorNode.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/AbstractBehaviorNode.kt @@ -46,15 +46,15 @@ abstract class AbstractBehaviorNode { else if (parameter.value is JsonPrimitive && parameter.value.isString) str = parameter.value.asString - if (str != null) { + if (!str.isNullOrEmpty()) { if (str.first() == '<' && str.last() == '>') { - val treeKey = str.substring(1, str.length - 2) + val treeKey = str.substring(1, str.length - 1) val param = treeParameters[treeKey] if (param != null) { return param } else { - throw NoSuchElementException("No parameter specified for tag '$str'") + throw NoSuchElementException("No parameter specified for tag '$treeKey'") } } } @@ -66,7 +66,7 @@ abstract class AbstractBehaviorNode { if (output == null) { return null } else if (output.first() == '<' && output.last() == '>') { - val replacement = treeParameters[output.substring(1, output.length - 2)] ?: throw NoSuchElementException("No parameter specified for tag '$output'") + val replacement = treeParameters[output.substring(1, output.length - 1)] ?: throw NoSuchElementException("No parameter specified for tag '$output'") if (replacement.key != null) return replacement.key @@ -96,7 +96,7 @@ abstract class AbstractBehaviorNode { val module = BehaviorTree(tree.blackboard, Registries.behavior.getOrThrow(name).value, moduleParameters) tree.scripts.addAll(module.scripts) tree.functions.addAll(module.functions) - return tree.root + return module.root } val parameters = LinkedHashMap(Registries.behaviorNodes.getOrThrow(name).value.properties) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/ActionNode.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/ActionNode.kt index b062dae4..de7691b6 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/ActionNode.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/ActionNode.kt @@ -11,6 +11,7 @@ import ru.dbotthepony.kstarbound.lua.userdata.BehaviorState class ActionNode(val name: String, val parameters: Map, val outputs: Map) : AbstractBehaviorNode() { private var coroutine: Coroutine? = null + private val nodeID = BehaviorState.NODE_GARBAGE_INDEX.getAndIncrement() override fun run(delta: Double, state: BehaviorState): Status { var coroutine = coroutine @@ -27,30 +28,34 @@ class ActionNode(val name: String, val parameters: Map, v try { val result = if (firstTime) { val parameters = state.blackboard.parameters(parameters, this) - state.lua.call(CoroutineLib.resume(), coroutine, parameters, state.blackboard, this, delta) + state.lua.call(CoroutineLib.resume(), coroutine, parameters, state.blackboard, nodeID, delta) } else { state.lua.call(CoroutineLib.resume(), coroutine, delta) } - val status = result[0] as Boolean + val coroutineStatus = result[0] as Boolean - if (result.size >= 2) { - val second = result[1] as? Table - - if (second != null) { - state.blackboard.setOutput(this, second) - } + if (!coroutineStatus) { + LOGGER.warn("Behavior ActionNode '$name' failed: ${result[1]}") + return Status.FAILURE } - val isDead = state.lua.call(CoroutineLib.status(), coroutine)[0] == "dead" - - if (!status) { - return Status.FAILURE - } else if (isDead) { - return Status.SUCCESS - } else { + if (result.size == 1) { return Status.RUNNING } + + val nodeStatus = result.getOrNull(1) as? Boolean ?: false + val nodeExtra = result.getOrNull(2) as? Table + + if (nodeExtra != null) { + state.blackboard.setOutput(this, nodeExtra) + } + + if (!nodeStatus) { + return Status.FAILURE + } else { + return Status.SUCCESS + } } catch (err: CallPausedException) { LOGGER.error("Behavior ActionNode '$name' called blocking code, which initiated pause. This is not supported.") return Status.FAILURE @@ -61,6 +66,10 @@ class ActionNode(val name: String, val parameters: Map, v coroutine = null } + override fun toString(): String { + return "ActionNode[$name, parameters=$parameters, outputs=$outputs, coroutine=$coroutine]" + } + companion object { private val LOGGER = LogManager.getLogger() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/Blackboard.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/Blackboard.kt index ce71e861..0a861eee 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/Blackboard.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/Blackboard.kt @@ -9,13 +9,11 @@ import org.classdump.luna.Table import org.classdump.luna.Userdata import org.classdump.luna.impl.ImmutableTable import ru.dbotthepony.kstarbound.defs.actor.behavior.NodeParameter -import ru.dbotthepony.kstarbound.json.stream import ru.dbotthepony.kstarbound.lua.LuaEnvironment import ru.dbotthepony.kstarbound.lua.from import ru.dbotthepony.kstarbound.lua.get import ru.dbotthepony.kstarbound.lua.luaFunction import ru.dbotthepony.kstarbound.lua.set -import ru.dbotthepony.kstarbound.lua.tableOf import ru.dbotthepony.kstarbound.util.valueOf import java.util.EnumMap import java.util.HashSet @@ -36,7 +34,7 @@ import kotlin.collections.HashMap */ class Blackboard(val lua: LuaEnvironment) : Userdata() { private val board = EnumMap>(NodeParameterType::class.java) - private val input = EnumMap>>>(NodeParameterType::class.java) + private val input = EnumMap>>>(NodeParameterType::class.java) private val parameters = HashMap() // key -> list of Lua tables and their indices private val vectorNumberInput = HashMap>>() @@ -45,6 +43,7 @@ class Blackboard(val lua: LuaEnvironment) : Userdata() { init { for (v in NodeParameterType.entries) { board[v] = HashMap() + input[v] = HashMap() } } @@ -58,8 +57,8 @@ class Blackboard(val lua: LuaEnvironment) : Userdata() { val input = input[type]!![key] if (input != null) { - for ((k1, k2) in input) { - parameters[k1]!![k2] = value + for ((index, table) in input) { + table[index] = value } } @@ -90,8 +89,8 @@ class Blackboard(val lua: LuaEnvironment) : Userdata() { for ((name, parameter) in parameters.entries) { if (parameter.value.key != null) { val typeInput = input[parameter.type]!!.computeIfAbsent(parameter.value.key) { ArrayList() } - typeInput.add(nodeID to name) - table[name] = this[parameter.type, name] + typeInput.add(name to table) + table[name] = this[parameter.type, parameter.value.key] } else { val value = parameter.value.value ?: JsonNull.INSTANCE @@ -167,6 +166,7 @@ class Blackboard(val lua: LuaEnvironment) : Userdata() { companion object { private val metatable: ImmutableTable + private fun __index() = metatable init { val builder = ImmutableTable.Builder() @@ -187,6 +187,7 @@ class Blackboard(val lua: LuaEnvironment) : Userdata() { }) } + builder.add("__index", luaFunction { _: Any?, index: Any -> returnBuffer.setTo(__index()[index]) }) metatable = builder.build() } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/DecoratorNode.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/DecoratorNode.kt index 3ef06be3..170640ec 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/DecoratorNode.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/DecoratorNode.kt @@ -1,14 +1,18 @@ package ru.dbotthepony.kstarbound.world.entities.behavior import org.apache.logging.log4j.LogManager +import org.classdump.luna.ByteString +import org.classdump.luna.Table import org.classdump.luna.exec.CallPausedException import org.classdump.luna.lib.CoroutineLib import org.classdump.luna.runtime.Coroutine import ru.dbotthepony.kstarbound.defs.actor.behavior.NodeParameter import ru.dbotthepony.kstarbound.lua.userdata.BehaviorState +import java.util.concurrent.atomic.AtomicLong class DecoratorNode(val name: String, val parameters: Map, val child: AbstractBehaviorNode) : AbstractBehaviorNode() { private var coroutine: Coroutine? = null + private val nodeID = BehaviorState.NODE_GARBAGE_INDEX.getAndIncrement() override fun run(delta: Double, state: BehaviorState): Status { var coroutine = coroutine @@ -19,14 +23,28 @@ class DecoratorNode(val name: String, val parameters: Map try { coroutine = state.lua.call(CoroutineLib.create(), fn)[0] as Coroutine - val result = state.lua.call(CoroutineLib.resume(), coroutine, parameters, state.blackboard, this) + val result = state.lua.call(CoroutineLib.resume(), coroutine, parameters, state.blackboard, nodeID) val status = result[0] as Boolean - if (!status && result.size >= 2) { + if (!status) { LOGGER.warn("Behavior DecoratorNode '$name' failed: ${result[1]}") + return Status.FAILURE } - return if (status) Status.SUCCESS else Status.FAILURE + if (result.size == 1) { + val coroutineStatus = state.lua.call(CoroutineLib.status(), coroutine)[0] as String + + if (coroutineStatus == "dead") { + // quite unexpected, but whatever + return Status.SUCCESS + } else { + this.coroutine = coroutine + } + } else { + val nodeStatus = result.getOrNull(1) as? Boolean ?: false + // val nodeExtra = result.getOrNull(3) as? Table + return if (nodeStatus) Status.SUCCESS else Status.FAILURE + } } catch (err: CallPausedException) { LOGGER.error("Behavior DecoratorNode '$name' called blocking code, which initiated pause. This is not supported.") return Status.FAILURE @@ -48,7 +66,23 @@ class DecoratorNode(val name: String, val parameters: Map LOGGER.warn("Behavior DecoratorNode '$name' failed: ${result[1]}") } - status = if (execStatus) Status.SUCCESS else Status.FAILURE + if (result.size == 1) { + // another yield OR unexpected return? + val coroutineStatus = state.lua.call(CoroutineLib.status(), coroutine)[0] as String + + if (coroutineStatus == "dead") { + this.coroutine = null + status = Status.SUCCESS + } else + status = Status.RUNNING + } else { + // yield or return with status + val nodeStatus = result.getOrNull(1) as? Boolean ?: false + // val nodeExtra = result.getOrNull(3) as? Table + + status = if (nodeStatus) Status.SUCCESS else Status.FAILURE + this.coroutine = null + } } catch (err: CallPausedException) { LOGGER.error("Behavior DecoratorNode '$name' called blocking code on children return, which initiated pause. This is not supported.") status = Status.FAILURE @@ -65,6 +99,10 @@ class DecoratorNode(val name: String, val parameters: Map coroutine = null } + override fun toString(): String { + return "DecoratorNode[$name, coroutine=$coroutine, parameters=$parameters, children=$child]" + } + companion object { private val LOGGER = LogManager.getLogger() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/DynamicNode.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/DynamicNode.kt index 0dd0e7cd..7318253e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/DynamicNode.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/DynamicNode.kt @@ -26,6 +26,10 @@ class DynamicNode(val children: ImmutableList) : AbstractB return Status.RUNNING } + override fun toString(): String { + return "DynamicNode[${children.size}, index=$index, current=${children.getOrNull(index)}]" + } + override fun reset() { children.forEach { it.reset() } index = 0 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/ParallelNode.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/ParallelNode.kt index 09a8121b..441dcd5f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/ParallelNode.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/ParallelNode.kt @@ -30,6 +30,9 @@ class ParallelNode(parameters: Map, val children: Immutab } } + private var lastFailed = -1 + private var lastSucceeded = -1 + override fun run(delta: Double, state: BehaviorState): Status { var failed = 0 var succeeded = 0 @@ -43,14 +46,24 @@ class ParallelNode(parameters: Map, val children: Immutab failed++ if (succeeded >= successLimit || failed >= failLimit) { + lastFailed = failed + lastSucceeded = succeeded return if (succeeded >= successLimit) Status.SUCCESS else Status.FAILURE } } + lastFailed = failed + lastSucceeded = succeeded return Status.RUNNING } + override fun toString(): String { + return "ParallelNode[children=${children.size}, successLimit=$successLimit, failLimit=$failLimit, lastFailed=$lastFailed, lastSucceeded=$lastSucceeded]" + } + override fun reset() { children.forEach { it.reset() } + lastSucceeded = -1 + lastFailed = -1 } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/RandomizeNode.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/RandomizeNode.kt index 5a7e821f..96856559 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/RandomizeNode.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/RandomizeNode.kt @@ -17,6 +17,13 @@ class RandomizeNode(val children: ImmutableList) : Abstrac return children[index].runAndReset(delta, state) } + override fun toString(): String { + if (index == -1) + return "RandomizeNode[${children.size}, not chosen]" + else + return "RandomizeNode[${children.size}, ${children[index]}]" + } + override fun reset() { children.forEach { it.reset() } index = -1 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/SequenceNode.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/SequenceNode.kt index 7a2d9376..220d9b6d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/SequenceNode.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/SequenceNode.kt @@ -21,6 +21,10 @@ class SequenceNode(val children: ImmutableList) : Abstract return Status.SUCCESS } + override fun toString(): String { + return "SequenceNode[${children.size}, index=$index, current=${children.getOrNull(index)}]" + } + override fun reset() { children.forEach { it.reset() } index = 0 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt index 5880ddb9..f073007a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt @@ -70,8 +70,6 @@ import ru.dbotthepony.kstarbound.lua.LuaMessageHandlerComponent import ru.dbotthepony.kstarbound.lua.LuaUpdateComponent import ru.dbotthepony.kstarbound.lua.bindings.provideAnimatorBindings import ru.dbotthepony.kstarbound.lua.bindings.provideEntityBindings -import ru.dbotthepony.kstarbound.lua.bindings.provideWorldBindings -import ru.dbotthepony.kstarbound.lua.bindings.provideWorldObjectBindings import ru.dbotthepony.kstarbound.lua.from import ru.dbotthepony.kstarbound.lua.get import ru.dbotthepony.kstarbound.lua.set @@ -849,7 +847,7 @@ open class WorldObject(val config: Registry.Entry) : TileEntit damage.request.damage, dmg, if (health <= 0.0) HitType.KILL else HitType.HIT, - damage.request.kind, + damage.request.damageSourceKind, config.value.damageMaterialKind ) )