From f95bc9762f323bcc704e17035621c4cdbd945c73 Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Fri, 28 Jun 2024 22:44:13 +0700 Subject: [PATCH] Minimally working Monster entities PathController, PathFinder Actor movement controller Lua bindings Game loading no longer block Universe thread, more efficient registry population synchronization Environmental status effects now can be stat modifiers --- ADDITIONS.md | 55 +- gradle.properties | 2 +- .../kotlin/ru/dbotthepony/kstarbound/Ext.kt | 2 + .../ru/dbotthepony/kstarbound/Globals.kt | 5 + .../kotlin/ru/dbotthepony/kstarbound/Main.kt | 5 + .../ru/dbotthepony/kstarbound/Registries.kt | 90 +- .../ru/dbotthepony/kstarbound/Registry.kt | 113 ++- .../kstarbound/StarboundFileSystem.kt | 8 +- .../network/packets/SpawnWorldObjectPacket.kt | 31 - .../kstarbound/defs/ActorMovementModifiers.kt | 30 - .../dbotthepony/kstarbound/defs/EntityType.kt | 39 +- .../kstarbound/defs/EphemeralStatusEffect.kt | 16 +- .../kstarbound/defs/PlayerWarping.kt | 2 +- .../ru/dbotthepony/kstarbound/defs/WorldID.kt | 2 +- .../{player => }/ActorMovementModifiers.kt | 10 +- .../defs/actor/StatusControllerConfig.kt | 4 +- .../kstarbound/defs/actor/Types.kt | 4 +- .../defs/actor/behavior/BehaviorDefinition.kt | 22 + .../actor/behavior/BehaviorNodeDefinition.kt | 19 + .../defs/actor/behavior/CompositeNodeType.kt | 20 + .../defs/actor/behavior/NodeOutput.kt | 42 + .../defs/actor/behavior/NodeParameter.kt | 45 + .../defs/actor/behavior/NodeParameterValue.kt | 41 + .../kstarbound/defs/item/ItemDescriptor.kt | 3 +- .../defs/monster/ActionDefinition.kt | 7 - .../defs/monster/MonsterPaletteSwap.kt | 11 + .../defs/monster/MonsterPartDefinition.kt | 17 + .../defs/monster/MonsterSkillDefinition.kt | 14 +- .../defs/monster/MonsterTypeDefinition.kt | 288 ++++++- .../kstarbound/defs/monster/MonsterVariant.kt | 145 ++++ .../defs/quest/QuestGlobalConfig.kt | 7 + .../defs/world/AsteroidWorldsConfig.kt | 3 +- .../kstarbound/defs/world/BiomeDefinition.kt | 3 +- .../defs/world/DungeonWorldsConfig.kt | 3 +- .../defs/world/TerrestrialWorldParameters.kt | 2 +- .../defs/world/VisitableWorldParameters.kt | 13 +- .../kstarbound/defs/world/WorldTemplate.kt | 4 + .../defs/world/WorldTemplateConfig.kt | 8 +- .../ru/dbotthepony/kstarbound/io/BTreeDB5.kt | 2 - .../ru/dbotthepony/kstarbound/io/ByteKey.kt | 103 +++ .../ru/dbotthepony/kstarbound/io/Streams.kt | 17 + .../dbotthepony/kstarbound/item/ItemStack.kt | 2 +- .../kstarbound/item/RecipeRegistry.kt | 63 +- .../dbotthepony/kstarbound/lua/Conversions.kt | 26 + .../kstarbound/lua/LuaEnvironment.kt | 25 +- .../lua/LuaMessageHandlerComponent.kt | 6 +- .../kstarbound/lua/LuaUpdateComponent.kt | 13 + .../lua/bindings/AnimatorBindings.kt | 3 +- .../kstarbound/lua/bindings/EntityBindings.kt | 47 +- .../lua/bindings/MonsterBindings.kt | 192 +++++ .../bindings/MovementControllerBindings.kt | 328 ++++++++ .../kstarbound/lua/bindings/RootBindings.kt | 29 +- .../lua/bindings/ServerWorldBindings.kt | 5 +- .../lua/bindings/StatusControllerBindings.kt | 251 ++++++ .../lua/bindings/UtilityBindings.kt | 5 +- .../kstarbound/lua/bindings/WorldBindings.kt | 70 +- .../lua/bindings/WorldEntityBindings.kt | 13 +- .../bindings/WorldEnvironmentalBindings.kt | 20 +- .../lua/bindings/WorldObjectBindings.kt | 12 +- .../kstarbound/lua/userdata/BehaviorState.kt | 121 +++ .../kstarbound/lua/userdata/LuaFuture.kt | 6 + .../kstarbound/lua/userdata/LuaPathFinder.kt | 101 +++ .../kstarbound/lua/userdata/LuaPerlinNoise.kt | 7 + .../lua/userdata/LuaRandomGenerator.kt | 6 + .../ru/dbotthepony/kstarbound/math/AABB.kt | 10 +- .../ru/dbotthepony/kstarbound/math/AABBi.kt | 25 +- .../ru/dbotthepony/kstarbound/math/Line2d.kt | 10 + .../kstarbound/math/vector/Vector2d.kt | 6 + .../kstarbound/network/PacketRegistry.kt | 2 - .../packets/ClientContextUpdatePacket.kt | 4 +- .../serverbound/ClientConnectPacket.kt | 6 +- .../kstarbound/server/ServerConnection.kt | 4 +- .../server/world/LegacyWorldStorage.kt | 2 +- .../kstarbound/server/world/ServerWorld.kt | 43 +- .../kstarbound/util/BlockableEventLoop.kt | 2 +- .../kstarbound/util/ExceptionLogger.kt | 10 - .../kstarbound/util/ExecutionSpinner.kt | 144 ---- .../kstarbound/util/HistoryQueue.kt | 52 ++ .../kstarbound/util/random/RandomUtils.kt | 2 +- .../dbotthepony/kstarbound/world/Direction.kt | 12 + .../kstarbound/world/EntityIndex.kt | 3 - .../kstarbound/world/TileModification.kt | 8 +- .../ru/dbotthepony/kstarbound/world/World.kt | 458 ++++++---- .../world/entities/AbstractEntity.kt | 23 +- .../kstarbound/world/entities/ActorEntity.kt | 66 +- .../world/entities/ActorMovementController.kt | 149 +++- .../world/entities/DynamicEntity.kt | 20 +- .../world/entities/EffectEmitter.kt | 68 +- .../world/entities/ItemDropEntity.kt | 4 + .../world/entities/MonsterEntity.kt | 439 ++++++++++ .../world/entities/MovementController.kt | 52 +- .../world/entities/PathController.kt | 326 +++++++- .../kstarbound/world/entities/PathFinder.kt | 716 ++++++++++++++++ .../world/entities/ProjectileEntity.kt | 5 + .../world/entities/StatusController.kt | 780 +++++++++++++++++- .../world/entities/api/StatusEffectEntity.kt | 9 + .../entities/behavior/AbstractBehaviorNode.kt | 153 ++++ .../world/entities/behavior/ActionNode.kt | 67 ++ .../entities/behavior/BehaviorNodeType.kt | 10 + .../world/entities/behavior/BehaviorTree.kt | 31 + .../world/entities/behavior/Blackboard.kt | 193 +++++ .../world/entities/behavior/DecoratorNode.kt | 71 ++ .../world/entities/behavior/DynamicNode.kt | 33 + .../entities/behavior/NodeParameterType.kt | 15 + .../world/entities/behavior/ParallelNode.kt | 56 ++ .../world/entities/behavior/RandomizeNode.kt | 24 + .../world/entities/behavior/SequenceNode.kt | 28 + .../world/entities/player/PlayerEntity.kt | 21 +- .../world/entities/tile/LoungeableObject.kt | 2 +- .../world/entities/tile/PlantEntity.kt | 6 + .../world/entities/tile/PlantPieceEntity.kt | 8 +- .../world/entities/tile/WorldObject.kt | 26 +- .../kstarbound/world/physics/CollisionType.kt | 64 +- .../dbotthepony/kstarbound/test/WorldTests.kt | 4 + 114 files changed, 6091 insertions(+), 789 deletions(-) delete mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/SpawnWorldObjectPacket.kt delete mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/ActorMovementModifiers.kt rename src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/{player => }/ActorMovementModifiers.kt (88%) create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/BehaviorDefinition.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/BehaviorNodeDefinition.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/CompositeNodeType.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/NodeOutput.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/NodeParameter.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/NodeParameterValue.kt delete mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/monster/ActionDefinition.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/monster/MonsterPaletteSwap.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/monster/MonsterPartDefinition.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/monster/MonsterVariant.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/quest/QuestGlobalConfig.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/io/ByteKey.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/MonsterBindings.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/MovementControllerBindings.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/StatusControllerBindings.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/BehaviorState.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/LuaPathFinder.kt delete mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/util/ExceptionLogger.kt delete mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/util/ExecutionSpinner.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/util/HistoryQueue.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MonsterEntity.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PathFinder.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/api/StatusEffectEntity.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/AbstractBehaviorNode.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/ActionNode.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/BehaviorNodeType.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/BehaviorTree.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/Blackboard.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/DecoratorNode.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/DynamicNode.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/NodeParameterType.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/ParallelNode.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/RandomizeNode.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/SequenceNode.kt diff --git a/ADDITIONS.md b/ADDITIONS.md index fc8d689f..42f1b977 100644 --- a/ADDITIONS.md +++ b/ADDITIONS.md @@ -4,7 +4,7 @@ This document briefly documents what have been added (or removed) regarding modding capabilities or engine behavior(s) This document is non-exhaustive, engine contains way more behavior change bits than documented here, -but listing all of them will be a hassle, and will pollute actually useful changes. +but listing all of them will be a hassle, and will pollute this document. --------------- @@ -12,6 +12,8 @@ but listing all of them will be a hassle, and will pollute actually useful chang * `treasurechests` now can specify `treasurePool` as array * `damageTable` can be defined directly, without referencing other JSON file (experimental feature) + * `environmentStatusEffects` of visitable world parameters now accept `StatModifier`s and not just unique status effects as `String`s + * Keep in mind, putting stat modifiers there will blow up original clients due to Json deserializer expecting only strings (despite `StatusController` treating `environmentStatusEffects` as field containing either `StatModifier` or `String`) ## Biomes * Tree biome placeables now have `variantsRange` (defaults to `[1, 1]`) and `subVariantsRange` (defaults to `[2, 2]`) @@ -32,7 +34,6 @@ val color: TileColor = TileColor.DEFAULT ``` * `item` brush now can accept proper item descriptors (in json object tag), * Previous behavior remains unchanged (if specified as string, creates _randomized_ item, if as object, creates _exactly_ what have been specified) - * To stop randomizing as Tiled tileset brush, specify `"dont_randomize"` as anything (e.g. as `""`) * `liquid` brush now can accept 'level' as second argument * Previous behavior is unchanged, `["liquid", "water", true]` will result into infinite water as before, but `["liquid", "water", 0.5, false]` will spawn half-filled water * In tiled, you already can do this using `"quantity"` property @@ -43,7 +44,7 @@ val color: TileColor = TileColor.DEFAULT ## .terrain Please keep in mind that if you use new format or new terrain selectors original clients will -probably explode upon joining worlds where new terrain selectors are utilized. +explode upon joining worlds where new terrain selectors are utilized. * All composing terrain selectors (such as `min`, `displacement`, `rotate`, etc) now can reference other terrain selectors by name (the `.terrain` files) instead of embedding entire config inside them * They can be referenced by either specifying corresponding field as string, or as object like so: `{"name": "namedselector"}` @@ -96,13 +97,30 @@ probably explode upon joining worlds where new terrain selectors are utilized. ## .matmod * `modId` is no longer essential and can be skipped, or specified as any number in 1 -> 2^31 range, with notes of `materialId` and `liquidId` apply. +## Monster Definitions + +### New parameters merge rules + +In addition to `add`, `multiply`, `merge` and `override` new merge methods are accepted: + * `sub` (a - b) + * `divide` (a / b) + * `inverse` (b / a) + * `min` + * `max` + * `pow` (a in power of b) + * `invpow` (b in power of a) + --------------- # Scripting * In DamageSource, `sourceEntityId` combination with `rayCheck` has been fixed, and check for tile collision between victim and inflictor (this entity), not between victim and attacker (`sourceEntityId`) + * Contexts, where previously only `entity` bindings were available, now have entity-specific bindings exposed + * Example: Status controller scripts now get `monster` bindings when running in context of Monster's status controller, etc + * `behavior.behavior` third argument (specified commonly as `_ENV`) is ignored and can be omitted (set to nil) + * It was used solely to get Lua engine (Lua execution context), and could have been deprecated long time ago even in original engine, because there is now a way in original engine to get Lua engine when binding is called -### Random +## Random * Added `random:randn(deviation: double, mean: double): double`, returns normally distributed double, where `deviation` stands for [Standard deviation](https://en.wikipedia.org/wiki/Standard_deviation), and `mean` specifies middle point * Removed `random:addEntropy` @@ -121,6 +139,30 @@ probably explode upon joining worlds where new terrain selectors are utilized. * Added `animator.hasEffect(effect: string): boolean` * Added `animator.parts(): List` +## mcontroller + + * Added `mcontroller.collisionPolies(): List`, since engine technically supports multiple convex bodies attached to one movement controller + * Added `mcontroller.collisionBodies(): List`, since engine technically supports multiple convex bodies attached to one movement controller + * Added `mcontroller.liquidName(): String?`, returns nil if not touching any liquid + * This addition marks `mcontroller.liquidId(): Int` deprecated + +## monster + + * Added `monster.seedNumber(): Long`, use this instead of `monster.seed(): String` + * `monster.level(): Double?` returns nil if no monster level was specified + * `monster.setDamageParts(parts: Table?)` now accepts nil as equivalent of empty table (consistency fix) + * `monster.setPhysicsForces(forces: Table?)` now accepts nil as equivalent of empty table (consistency fix) + * `mosnter.setName(name: String?)` now accepts nil to reset custom name + +## status + + * Implemented `status.appliesEnvironmentStatusEffects(): Boolean`, which exists in original engine's code but was never hooked up to Lua bindings + * Implemented `status.appliesWeatherStatusEffects(): Boolean`, which exists in original engine's code but was never hooked up to Lua bindings + * Implemented `status.setAppliesEnvironmentStatusEffects(should: Boolean)`, which exists in original engine's code but was never hooked up to Lua bindings + * Implemented `status.setAppliesWeatherStatusEffects(should: Boolean)`, which exists in original engine's code but was never hooked up to Lua bindings + * Added `status.minimumLiquidStatusEffectPercentage(): Double` + * Added `status.setMinimumLiquidStatusEffectPercentage(value: Double)` + ## world #### Additions @@ -244,7 +286,10 @@ _slightly_ different results from execution to execution, such as one microdungeon taking precedence over another microdungeon if they happen to generate in proximity on chunk border (one dungeon generated in chunk A, second generated in chunk B, and they happened to overlap each other), -and which one gets placed is determined by who finishes generating first. +and which one gets placed is determined by who finishes generating first; as well as case +of approaching same chunk in world from different sides (exploring world left to right can yield +different generation result when exploring from right to left, and this is not something that can be fixed, +unless world is pre-generated in its entirety). --------------- diff --git a/gradle.properties b/gradle.properties index 2bea4cbb..a2c909e1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m kotlinVersion=1.9.10 kotlinCoroutinesVersion=1.8.0 -kommonsVersion=2.17.0 +kommonsVersion=3.0.1 ffiVersion=2.2.13 lwjglVersion=3.3.0 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Ext.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Ext.kt index 541b2d6e..7387e7a7 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Ext.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Ext.kt @@ -51,3 +51,5 @@ inline fun Gson.fromJson(reader: JsonReader): T? = fromJson(reade inline fun Gson.fromJson(reader: JsonElement): T? = getAdapter(T::class.java).read(FastJsonTreeReader(reader)) fun Gson.fromJsonFast(reader: JsonElement, type: Class): T = getAdapter(type).read(FastJsonTreeReader(reader)) +fun Gson.fromJsonFast(reader: JsonElement, type: TypeToken): T = getAdapter(type).read(FastJsonTreeReader(reader)) +inline fun Gson.fromJsonFast(reader: JsonElement): T = getAdapter(object : TypeToken() {}).read(FastJsonTreeReader(reader)) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Globals.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Globals.kt index e4abc6f2..0db1f5d9 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Globals.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Globals.kt @@ -20,6 +20,7 @@ import ru.dbotthepony.kstarbound.defs.actor.player.PlayerConfig import ru.dbotthepony.kstarbound.defs.actor.player.ShipUpgrades import ru.dbotthepony.kstarbound.defs.item.ItemDropConfig import ru.dbotthepony.kstarbound.defs.item.ItemGlobalConfig +import ru.dbotthepony.kstarbound.defs.quest.QuestGlobalConfig import ru.dbotthepony.kstarbound.defs.tile.TileDamageParameters import ru.dbotthepony.kstarbound.defs.world.TerrestrialWorldsConfig import ru.dbotthepony.kstarbound.defs.world.AsteroidWorldsConfig @@ -118,6 +119,9 @@ object Globals { var shipUpgrades by Delegates.notNull() private set + var quests by Delegates.notNull() + private set + private var profanityFilterInternal by Delegates.notNull>() val profanityFilter: ImmutableSet by lazy { @@ -224,6 +228,7 @@ object Globals { tasks.add(load("/plants/bushDamage.config", ::bushDamage)) tasks.add(load("/tiles/defaultDamage.config", ::tileDamage)) tasks.add(load("/ships/shipupgrades.config", ::shipUpgrades)) + tasks.add(load("/quests/quests.config", ::quests)) 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()) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt index f824194d..ee538362 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt @@ -1,12 +1,16 @@ package ru.dbotthepony.kstarbound import org.apache.logging.log4j.LogManager +import org.classdump.luna.lib.CoroutineLib +import org.classdump.luna.runtime.Coroutine import org.lwjgl.Version import picocli.CommandLine import picocli.CommandLine.Command import picocli.CommandLine.Option import picocli.CommandLine.Parameters import ru.dbotthepony.kstarbound.client.StarboundClient +import ru.dbotthepony.kstarbound.lua.LuaEnvironment +import ru.dbotthepony.kstarbound.lua.get import ru.dbotthepony.kstarbound.server.IntegratedStarboundServer import java.io.File import java.net.InetSocketAddress @@ -43,6 +47,7 @@ class StartClientCommand : Callable { } Starbound.addArchive(file) + //Starbound.addPath(File("K:\\git\\kstarbound\\unpacked_assets")) /*for (f in File("J:\\Steam\\steamapps\\workshop\\content\\211820").listFiles()!!) { for (f2 in f.listFiles()!!) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt index 3eb6b478..da2a03b8 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt @@ -7,6 +7,7 @@ import com.google.gson.JsonObject import com.google.gson.JsonSyntaxException import com.google.gson.TypeAdapterFactory import com.google.gson.reflect.TypeToken +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet import kotlinx.coroutines.async import kotlinx.coroutines.future.asCompletableFuture import kotlinx.coroutines.future.await @@ -32,6 +33,10 @@ import ru.dbotthepony.kstarbound.defs.animation.ParticleConfig import ru.dbotthepony.kstarbound.defs.dungeon.DungeonDefinition import ru.dbotthepony.kstarbound.defs.item.TreasureChestDefinition import ru.dbotthepony.kstarbound.defs.ProjectileDefinition +import ru.dbotthepony.kstarbound.defs.actor.behavior.BehaviorDefinition +import ru.dbotthepony.kstarbound.defs.actor.behavior.BehaviorNodeDefinition +import ru.dbotthepony.kstarbound.defs.monster.MonsterPaletteSwap +import ru.dbotthepony.kstarbound.defs.monster.MonsterPartDefinition import ru.dbotthepony.kstarbound.defs.quest.QuestTemplate import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition import ru.dbotthepony.kstarbound.defs.tile.RenderParameters @@ -48,11 +53,14 @@ import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.fromJsonTreeFast import ru.dbotthepony.kstarbound.world.terrain.TerrainSelectorType import ru.dbotthepony.kstarbound.util.AssetPathStack +import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.world.physics.CollisionType import java.util.* import java.util.concurrent.CompletableFuture import java.util.concurrent.Future +import java.util.random.RandomGenerator import kotlin.collections.ArrayList +import kotlin.collections.HashMap object Registries { private val LOGGER = LogManager.getLogger() @@ -83,6 +91,9 @@ 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 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()) } val worldObjects = Registry("world object").also(registriesInternal::add).also { adapters.add(it.adapter()) } val biomes = Registry("biome").also(registriesInternal::add).also { adapters.add(it.adapter()) } val terrainSelectors = Registry>("terrain selector").also(registriesInternal::add).also { adapters.add(it.adapter()) } @@ -93,6 +104,54 @@ object Registries { 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()) } + private val monsterParts = HashMap, HashMap>>() + private val loggedMisses = Collections.synchronizedSet(ObjectOpenHashSet>()) + + fun selectMonsterPart(category: String, type: String, random: RandomGenerator): MonsterPartDefinition? { + val key = category to type + val get = monsterParts[key] + + if (get.isNullOrEmpty()) { + if (loggedMisses.add(key)) { + LOGGER.error("No such monster part combination of category '$category' and type '$type'") + } + + return null + } + + return get.values.random(random, true).first + } + + fun getMonsterPart(category: String, type: String, name: String): MonsterPartDefinition? { + return monsterParts[category to type]?.get(name)?.first + } + + private fun loadMonsterParts(files: Collection, patches: Map>): List> { + val adapter by lazy { Starbound.gson.getAdapter(MonsterPartDefinition::class.java) } + + return files.map { listedFile -> + Starbound.GLOBAL_SCOPE.launch { + try { + val elem = JsonPatch.applyAsync(listedFile.asyncJsonReader(), patches[listedFile.computeFullPath()]) + val next = AssetPathStack(listedFile.computeFullPath()) { adapter.fromJsonTreeFast(elem) } + + Starbound.submit { + val map = monsterParts.computeIfAbsent(next.category to next.type) { HashMap() } + val old = map[next.name] + + if (old != null) { + LOGGER.error("Duplicate monster part '${next.name}' of category '${next.category}' and type '${next.type}', originating from $listedFile (old originate from ${old.second})") + } else { + map[next.name] = next to listedFile + } + } + } catch (err: Throwable) { + LOGGER.error("Loading monster part definition file $listedFile", err) + } + }.asCompletableFuture() + } + } + private fun key(mapper: (T) -> String): (T) -> Pair> { return { mapper.invoke(it) to KOptional() } } @@ -122,7 +181,7 @@ object Registries { return files.map { listedFile -> Starbound.GLOBAL_SCOPE.launch { try { - val elem = JsonPatch.applyAsync(Starbound.ELEMENTS_ADAPTER.read(listedFile.asyncJsonReader().await()), patches[listedFile.computeFullPath()]) + val elem = JsonPatch.applyAsync(listedFile.asyncJsonReader(), patches[listedFile.computeFullPath()]) AssetPathStack(listedFile.computeFullPath()) { val read = adapter.fromJsonTreeFast(elem) @@ -130,7 +189,7 @@ object Registries { after(read, listedFile) - registry.add { + Starbound.submit { registry.add( key = keys.first, value = read, @@ -138,7 +197,7 @@ object Registries { json = elem, file = listedFile ) - } + }.exceptionally { err -> LOGGER.error("Loading ${registry.name} definition file $listedFile", err); null } } } catch (err: Throwable) { LOGGER.error("Loading ${registry.name} definition file $listedFile", err) @@ -147,10 +206,6 @@ object Registries { } } - fun finishLoad() { - registriesInternal.forEach { it.finishLoad() } - } - fun load(fileTree: Map>, patchTree: Map>): List> { val tasks = ArrayList>() @@ -165,14 +220,18 @@ object Registries { tasks.add(loadMetaMaterials()) tasks.addAll(loadRegistry(dungeons, patchTree, fileTree["dungeon"] ?: listOf(), key(DungeonDefinition::name))) + tasks.addAll(loadMonsterParts(fileTree["monsterpart"] ?: listOf(), patchTree)) + tasks.addAll(loadRegistry(worldObjects, patchTree, fileTree["object"] ?: listOf(), key(ObjectDefinition::objectName))) + tasks.addAll(loadRegistry(monsterTypes, patchTree, fileTree["monstertype"] ?: listOf(), key(MonsterTypeDefinition::type))) + tasks.addAll(loadRegistry(monsterPalettes, patchTree, fileTree["monstercolors"] ?: listOf(), key(MonsterPaletteSwap::name))) + tasks.addAll(loadRegistry(monsterSkills, patchTree, fileTree["monsterskill"] ?: listOf(), key(MonsterSkillDefinition::name))) tasks.addAll(loadRegistry(statusEffects, patchTree, fileTree["statuseffect"] ?: listOf(), key(StatusEffectDefinition::name))) tasks.addAll(loadRegistry(species, patchTree, fileTree["species"] ?: listOf(), key(Species::kind))) tasks.addAll(loadRegistry(particles, patchTree, fileTree["particle"] ?: listOf(), { (it.kind ?: throw NullPointerException("Missing 'kind' value")) to KOptional() })) tasks.addAll(loadRegistry(questTemplates, patchTree, fileTree["questtemplate"] ?: listOf(), key(QuestTemplate::id))) tasks.addAll(loadRegistry(techs, patchTree, fileTree["tech"] ?: listOf(), key(TechDefinition::name))) tasks.addAll(loadRegistry(npcTypes, patchTree, fileTree["npctype"] ?: listOf(), key(NpcTypeDefinition::type))) - tasks.addAll(loadRegistry(monsterSkills, patchTree, fileTree["monsterskill"] ?: listOf(), key(MonsterSkillDefinition::name))) tasks.addAll(loadRegistry(biomes, patchTree, fileTree["biome"] ?: listOf(), key(BiomeDefinition::name))) tasks.addAll(loadRegistry(grassVariants, patchTree, fileTree["grass"] ?: listOf(), key(GrassVariant.Data::name))) tasks.addAll(loadRegistry(treeStemVariants, patchTree, fileTree["modularstem"] ?: listOf(), key(TreeVariant.StemData::name))) @@ -180,6 +239,9 @@ object Registries { tasks.addAll(loadRegistry(bushVariants, patchTree, fileTree["bush"] ?: listOf(), key(BushVariant.Data::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(behavior, patchTree, fileTree["behavior"] ?: listOf(), key(BehaviorDefinition::name))) + + tasks.addAll(loadCombined(behaviorNodes, fileTree["nodes"] ?: listOf(), patchTree)) tasks.addAll(loadCombined(jsonFunctions, fileTree["functions"] ?: listOf(), patchTree)) tasks.addAll(loadCombined(json2Functions, fileTree["2functions"] ?: listOf(), patchTree)) @@ -196,16 +258,16 @@ object Registries { return files.map { listedFile -> Starbound.GLOBAL_SCOPE.launch { try { - val json = JsonPatch.applyAsync(Starbound.ELEMENTS_ADAPTER.read(listedFile.asyncJsonReader().await()), patches[listedFile.computeFullPath()]) as JsonObject + val json = JsonPatch.applyAsync(listedFile.asyncJsonReader(), patches[listedFile.computeFullPath()]) as JsonObject for ((k, v) in json.entrySet()) { try { val value = adapter.fromJsonTreeFast(v) transform(value, k) - registry.add { + Starbound.submit { registry.add(k, value, v, listedFile) - } + }.exceptionally { err -> LOGGER.error("Loading ${registry.name} definition $k from file $listedFile", err); null } } catch (err: Exception) { LOGGER.error("Loading ${registry.name} definition $k from file $listedFile", err) } @@ -219,11 +281,11 @@ object Registries { private suspend fun loadTerrainSelector(listedFile: IStarboundFile, type: TerrainSelectorType?, patches: Map>) { try { - val json = JsonPatch.applyAsync(Starbound.ELEMENTS_ADAPTER.read(listedFile.asyncJsonReader().await()), patches[listedFile.computeFullPath()]) as JsonObject + val json = JsonPatch.applyAsync(listedFile.asyncJsonReader(), patches[listedFile.computeFullPath()]) as JsonObject val name = json["name"]?.asString ?: throw JsonSyntaxException("Missing 'name' field") val factory = TerrainSelectorType.factory(json, false, type) - terrainSelectors.add { + Starbound.submit { terrainSelectors.add(name, factory) } } catch (err: Exception) { @@ -268,7 +330,7 @@ object Registries { val read2 = Starbound.gson.getAdapter(object : TypeToken>() {}).fromJsonTreeFast(read) for (def in read2) { - tiles.add { + Starbound.submit { tiles.add(key = "metamaterial:${def.name}", id = KOptional(def.materialId), value = TileDefinition( isMeta = true, materialId = def.materialId, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt index f4f03272..7561d004 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt @@ -7,48 +7,34 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectFunction import it.unimi.dsi.fastutil.ints.Int2ObjectMap import it.unimi.dsi.fastutil.ints.Int2ObjectMaps import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap +import it.unimi.dsi.fastutil.longs.LongOpenHashSet import it.unimi.dsi.fastutil.objects.Object2ObjectFunction -import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kommons.util.KOptional +import ru.dbotthepony.kommons.util.XXHash64 +import ru.dbotthepony.kstarbound.util.limit import java.util.Collections -import java.util.concurrent.ConcurrentLinkedQueue -import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantReadWriteLock import java.util.function.Supplier import kotlin.collections.set import kotlin.concurrent.read -import kotlin.concurrent.withLock import kotlin.concurrent.write -class Registry(val name: String) { +class Registry(val name: String, val storeJson: Boolean = true) { private val keysInternal = HashMap() private val idsInternal = Int2ObjectOpenHashMap() private val keyRefs = HashMap() private val idRefs = Int2ObjectOpenHashMap() - private val backlog = ConcurrentLinkedQueue() private val lock = ReentrantReadWriteLock() private var hasBeenValidated = false - private val loggedMisses = ObjectOpenHashSet() - // it is much cheaper to queue registry additions rather than locking during high congestion - fun add(task: Runnable) { - backlog.add(task) - } - - fun finishLoad() { - lock.write { - var next = backlog.poll() - - while (next != null) { - next.run() - next = backlog.poll() - } - } - } + // idiot-proof miss lookup. Surely, it will cause some entries to be never logged + // if they are missing, but at least if malicious actor spams with long-ass invalid data + // it won't explode memory usage of server + private val loggedMisses = LongOpenHashSet() val keys: Map> = Collections.unmodifiableMap(keysInternal) val ids: Int2ObjectMap> = Int2ObjectMaps.unmodifiable(idsInternal) @@ -102,8 +88,21 @@ class Registry(val name: String) { return this === other } + private val hash: Int + + init { + var x = key.hashCode() + // 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) + hash = x + } + override fun hashCode(): Int { - return key.hashCode() + return hash } override fun toString(): String { @@ -122,8 +121,21 @@ class Registry(val name: String) { return this === other || other is Registry<*>.RefImpl && other.key == key && other.registry == registry } + private val hash: Int + + init { + var x = key.hashCode() + // 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) + hash = x + } + override fun hashCode(): Int { - return key.hashCode() + return hash } override fun toString(): String { @@ -138,9 +150,13 @@ class Registry(val name: String) { val result = lock.read { keysInternal[index] } if (result == null && hasBeenValidated) { + val hasher = XXHash64() + hasher.update(index.toByteArray()) + val missIndex = hasher.digestAsLong() + lock.write { - if (loggedMisses.add(index)) { - LOGGER.warn("No such $name: $index") + if (loggedMisses.add(missIndex)) { + LOGGER.warn("No such $name: ${index.limit()}") } } } @@ -152,8 +168,15 @@ class Registry(val name: String) { val result = lock.read { idsInternal[index] } if (result == null && hasBeenValidated) { + val hasher = XXHash64() + hasher.update(index.toByte()) + hasher.update((index ushr 8).toByte()) + hasher.update((index ushr 16).toByte()) + hasher.update((index ushr 24).toByte()) + val missIndex = hasher.digestAsLong() + lock.write { - if (loggedMisses.add(index.toString())) { + if (loggedMisses.add(missIndex)) { LOGGER.warn("No such $name: ID $index") } } @@ -170,13 +193,23 @@ class Registry(val name: String) { return get(index) ?: throw NoSuchElementException("No such $name: $index") } + /** + * To be used inside network readers, so clients sending invalid data will get kicked + */ + fun refOrThrow(index: String): Ref = get(index)?.ref ?: throw NoSuchElementException("No such $name: ${index.limit()}") + fun ref(index: String): Ref = lock.write { keyRefs.computeIfAbsent(index, Object2ObjectFunction { val ref = RefImpl(Either.left(it as String)) ref.entry = keysInternal[it] - if (hasBeenValidated && ref.entry == null && loggedMisses.add(it)) { - LOGGER.warn("No such $name: $it") + if (hasBeenValidated && ref.entry == null) { + val hasher = XXHash64() + hasher.update(index.toByteArray()) + + if (loggedMisses.add(hasher.digestAsLong())) { + LOGGER.warn("No such $name: ${it.limit()}") + } } ref @@ -185,13 +218,26 @@ class Registry(val name: String) { } } + /** + * To be used inside network readers, so clients sending invalid data will get kicked + */ + fun refOrThrow(index: Int): Ref = get(index)?.ref ?: throw NoSuchElementException("No such $name: ID $index") + fun ref(index: Int): Ref = lock.write { idRefs.computeIfAbsent(index, Int2ObjectFunction { val ref = RefImpl(Either.right(it)) ref.entry = idsInternal[it] - if (hasBeenValidated && ref.entry == null && loggedMisses.add(it.toString())) { - LOGGER.warn("No such $name: ID $it") + if (hasBeenValidated && ref.entry == null) { + val hasher = XXHash64() + hasher.update(index.toByte()) + hasher.update((index ushr 8).toByte()) + hasher.update((index ushr 16).toByte()) + hasher.update((index ushr 24).toByte()) + + if (loggedMisses.add(hasher.digestAsLong())) { + LOGGER.warn("No such $name: ID $it") + } } ref @@ -261,7 +307,10 @@ class Registry(val name: String) { } entry.value = value - entry.json = json + + if (storeJson) + entry.json = json + entry.file = file entry.isBuiltin = isBuiltin diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/StarboundFileSystem.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/StarboundFileSystem.kt index 74629090..3cb864fe 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/StarboundFileSystem.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/StarboundFileSystem.kt @@ -109,9 +109,9 @@ interface IStarboundFile : ISBFileLocator { val children = file.children ?: return NonExistingFile(name = split.last(), fullPath = computeFullPath() + "/" + path) val find = children[split[splitIndex]] - if (find is StarboundPak.SBDirectory) { + if (find is StarboundPak.SBDirectory || find != null && find.isDirectory) { file = find - } else if (find is StarboundPak.SBFile) { + } else if (find is StarboundPak.SBFile || find != null && find.isFile) { if (splitIndex + 1 != split.size) { return NonExistingFile(name = split.last(), fullPath = computeFullPath() + "/" + path) } @@ -270,10 +270,10 @@ class PhysicalFile(val real: File, override val parent: PhysicalFile? = null) : get() = real.isFile override val children: Map? get() { - return real.list()?.associate { it to PhysicalFile(File(it), this) } + return real.list()?.associate { it.lowercase() to PhysicalFile(File(real, it), this) } } override val name: String - get() = real.name + get() = if (parent == null) "" else real.name.lowercase() private val fullPatch by lazy { super.computeFullPath().sbIntern() } private val directory by lazy { super.computeDirectory(false).sbIntern() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/SpawnWorldObjectPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/SpawnWorldObjectPacket.kt deleted file mode 100644 index 1d8cbd92..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/SpawnWorldObjectPacket.kt +++ /dev/null @@ -1,31 +0,0 @@ -package ru.dbotthepony.kstarbound.client.network.packets - -import com.google.gson.JsonObject -import ru.dbotthepony.kommons.io.readUUID -import ru.dbotthepony.kommons.io.writeUUID -import ru.dbotthepony.kstarbound.client.ClientConnection -import ru.dbotthepony.kstarbound.json.readJsonObject -import ru.dbotthepony.kstarbound.json.writeJsonObject -import ru.dbotthepony.kstarbound.network.IClientPacket -import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject -import java.io.DataInputStream -import java.io.DataOutputStream -import java.util.UUID - -class SpawnWorldObjectPacket(val uuid: UUID, val data: JsonObject) : IClientPacket { - constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readUUID(), stream.readJsonObject()) - - override fun write(stream: DataOutputStream, isLegacy: Boolean) { - stream.writeUUID(uuid) - stream.writeJsonObject(data) - } - - override fun play(connection: ClientConnection) { - connection.client.mailbox.execute { - val world = connection.client.world ?: return@execute - val obj = WorldObject.fromJson(data) - //obj.uuid = uuid - obj.joinWorld(world) - } - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ActorMovementModifiers.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ActorMovementModifiers.kt deleted file mode 100644 index 6d088a8c..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ActorMovementModifiers.kt +++ /dev/null @@ -1,30 +0,0 @@ -package ru.dbotthepony.kstarbound.defs - -import ru.dbotthepony.kstarbound.json.builder.JsonFactory - -@JsonFactory -data class ActorMovementModifiers( - val groundMovementModifier: Double = 0.0, - val liquidMovementModifier: Double = 0.0, - val speedModifier: Double = 0.0, - val airJumpModifier: Double = 0.0, - val liquidJumpModifier: Double = 0.0, - val runningSuppressed: Boolean = false, - val jumpingSuppressed: Boolean = false, - val movementSuppressed: Boolean = false, - val facingSuppressed: Boolean = false, -) { - fun combine(other: ActorMovementModifiers): ActorMovementModifiers { - return ActorMovementModifiers( - groundMovementModifier = groundMovementModifier * other.groundMovementModifier, - liquidMovementModifier = liquidMovementModifier * other.liquidMovementModifier, - speedModifier = speedModifier * other.speedModifier, - airJumpModifier = airJumpModifier * other.airJumpModifier, - liquidJumpModifier = liquidJumpModifier * other.liquidJumpModifier, - runningSuppressed = runningSuppressed || other.runningSuppressed, - jumpingSuppressed = jumpingSuppressed || other.jumpingSuppressed, - movementSuppressed = movementSuppressed || other.movementSuppressed, - facingSuppressed = facingSuppressed || other.facingSuppressed, - ) - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/EntityType.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/EntityType.kt index f24a83e2..02e59c64 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/EntityType.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/EntityType.kt @@ -1,16 +1,22 @@ package ru.dbotthepony.kstarbound.defs -import com.google.gson.JsonElement import com.google.gson.JsonObject import ru.dbotthepony.kommons.io.readBinaryString import ru.dbotthepony.kstarbound.Registries +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.monster.MonsterVariant +import ru.dbotthepony.kstarbound.defs.`object`.ObjectType +import ru.dbotthepony.kstarbound.fromJsonFast import ru.dbotthepony.kstarbound.io.readInternedString import ru.dbotthepony.kstarbound.json.builder.IStringSerializable import ru.dbotthepony.kstarbound.json.readJsonElement import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.ItemDropEntity +import ru.dbotthepony.kstarbound.world.entities.MonsterEntity import ru.dbotthepony.kstarbound.world.entities.ProjectileEntity import ru.dbotthepony.kstarbound.world.entities.player.PlayerEntity +import ru.dbotthepony.kstarbound.world.entities.tile.ContainerObject +import ru.dbotthepony.kstarbound.world.entities.tile.LoungeableObject import ru.dbotthepony.kstarbound.world.entities.tile.PlantEntity import ru.dbotthepony.kstarbound.world.entities.tile.PlantPieceEntity import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject @@ -36,7 +42,19 @@ enum class EntityType(override val jsonName: String, val storeName: String, val } override fun fromStorage(data: JsonObject): AbstractEntity { - return WorldObject.fromJson(data) + val prototype = Registries.worldObjects[data["name"]?.asString ?: throw IllegalArgumentException("Missing object name")] ?: throw IllegalArgumentException("No such object defined for '${data["name"]}'") + + val result = when (prototype.value.objectType) { + ObjectType.OBJECT -> WorldObject(prototype) + ObjectType.LOUNGEABLE -> LoungeableObject(prototype) + ObjectType.CONTAINER -> ContainerObject(prototype) + ObjectType.FARMABLE -> TODO("ObjectType.FARMABLE") + ObjectType.TELEPORTER -> TODO("ObjectType.TELEPORTER") + ObjectType.PHYSICS -> TODO("ObjectType.PHYSICS") + } + + result.deserialize(data) + return result } }, @@ -92,11 +110,24 @@ enum class EntityType(override val jsonName: String, val storeName: String, val MONSTER("monster", "MonsterEntity", false, false) { override fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity { - TODO("MONSTER") + return MonsterEntity(MonsterVariant.read(stream, isLegacy)) } override fun fromStorage(data: JsonObject): AbstractEntity { - TODO("MONSTER") + val variantJson = data["monsterVariant"].asJsonObject + + val variant = if (variantJson.size() == 3) { + val type = variantJson["type"].asString + val seed = variantJson["seed"].asLong + val uniqueParameters = variantJson["uniqueParameters"].asJsonObject + Registries.monsterTypes.getOrThrow(type).value.create(seed, uniqueParameters) + } else { + Starbound.gson.fromJsonFast(variantJson, MonsterVariant::class.java) + } + + val entity = MonsterEntity(variant) + entity.deserialize(data) + return entity } }, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/EphemeralStatusEffect.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/EphemeralStatusEffect.kt index 57eceafc..65b3a922 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/EphemeralStatusEffect.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/EphemeralStatusEffect.kt @@ -6,39 +6,41 @@ import com.google.gson.annotations.JsonAdapter import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonWriter +import ru.dbotthepony.kommons.io.readBinaryString import ru.dbotthepony.kommons.io.writeBinaryString -import ru.dbotthepony.kstarbound.io.readInternedString +import ru.dbotthepony.kstarbound.Registries +import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.io.readNullableDouble -import ru.dbotthepony.kstarbound.io.writeNullable import ru.dbotthepony.kstarbound.io.writeNullableDouble import ru.dbotthepony.kstarbound.json.builder.FactoryAdapter import java.io.DataInputStream import java.io.DataOutputStream @JsonAdapter(EphemeralStatusEffect.Adapter::class) -data class EphemeralStatusEffect(val effect: String, val duration: Double? = null) { - constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readInternedString(), stream.readNullableDouble()) +data class EphemeralStatusEffect(val effect: Registry.Ref, val duration: Double? = null) { + constructor(stream: DataInputStream, isLegacy: Boolean) : this(Registries.statusEffects.refOrThrow(stream.readBinaryString()), stream.readNullableDouble()) + constructor(effect: Registry.Entry, duration: Double? = null) : this(effect.ref, duration) class Adapter(gson: Gson) : TypeAdapter() { private val factory = FactoryAdapter.createFor(EphemeralStatusEffect::class, gson = gson) override fun write(out: JsonWriter, value: EphemeralStatusEffect) { if (value.duration == null) - out.value(value.effect) + out.value(value.effect.key.left()) else factory.write(out, value) } override fun read(`in`: JsonReader): EphemeralStatusEffect { if (`in`.peek() == JsonToken.STRING) - return EphemeralStatusEffect(`in`.nextString()) + return EphemeralStatusEffect(Registries.statusEffects.ref(`in`.nextString())) else return factory.read(`in`) } } fun write(stream: DataOutputStream, isLegacy: Boolean) { - stream.writeBinaryString(effect) + stream.writeBinaryString(effect.key.left()) stream.writeNullableDouble(duration, isLegacy) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PlayerWarping.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PlayerWarping.kt index e73110e9..b9d2236b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PlayerWarping.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PlayerWarping.kt @@ -118,7 +118,7 @@ sealed class SpawnTarget { tickets.addAll(world.permanentChunkTicket(testRect).await()) tickets.forEach { it.chunk.await() } - if (!world.anyCellSatisfies(testRect) { x, y, cell -> cell.foreground.material.value.collisionKind.isSolidCollision }) + if (!world.chunkMap.anyCellSatisfies(testRect) { x, y, cell -> cell.foreground.material.value.collisionKind.isSolidCollision }) return testPos } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/WorldID.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/WorldID.kt index ecf22450..f57208de 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/WorldID.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/WorldID.kt @@ -21,7 +21,7 @@ import java.util.UUID @JsonAdapter(WorldID.Adapter::class) sealed class WorldID { abstract fun write(stream: DataOutputStream, isLegacy: Boolean) - val isLimbo: Boolean get() = this is Limbo + val isLimbo: Boolean get() = this === Limbo object Limbo : WorldID() { override fun write(stream: DataOutputStream, isLegacy: Boolean) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/ActorMovementModifiers.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/ActorMovementModifiers.kt similarity index 88% rename from src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/ActorMovementModifiers.kt rename to src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/ActorMovementModifiers.kt index d83ac72f..293a2c45 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/ActorMovementModifiers.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/ActorMovementModifiers.kt @@ -1,9 +1,9 @@ -package ru.dbotthepony.kstarbound.defs.actor.player +package ru.dbotthepony.kstarbound.defs.actor import ru.dbotthepony.kstarbound.json.builder.JsonFactory @JsonFactory -class ActorMovementModifiers( +data class ActorMovementModifiers( val groundMovementModifier: Double = 1.0, val liquidMovementModifier: Double = 1.0, val speedModifier: Double = 1.0, @@ -11,10 +11,10 @@ class ActorMovementModifiers( val liquidJumpModifier: Double = 1.0, val runningSuppressed: Boolean = false, val jumpingSuppressed: Boolean = false, - val facingSuppressed: Boolean = false, val movementSuppressed: Boolean = false, + val facingSuppressed: Boolean = false, ) { - fun combine(other: ActorMovementModifiers): ActorMovementModifiers { + fun merge(other: ActorMovementModifiers): ActorMovementModifiers { return ActorMovementModifiers( groundMovementModifier = groundMovementModifier * other.groundMovementModifier, liquidMovementModifier = liquidMovementModifier * other.liquidMovementModifier, @@ -23,8 +23,8 @@ class ActorMovementModifiers( liquidJumpModifier = liquidJumpModifier * other.liquidJumpModifier, runningSuppressed = runningSuppressed || other.runningSuppressed, jumpingSuppressed = jumpingSuppressed || other.jumpingSuppressed, - facingSuppressed = facingSuppressed || other.facingSuppressed, movementSuppressed = movementSuppressed || other.movementSuppressed, + facingSuppressed = facingSuppressed || other.facingSuppressed, ) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/StatusControllerConfig.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/StatusControllerConfig.kt index ed8b2b5c..6af9576c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/StatusControllerConfig.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/StatusControllerConfig.kt @@ -16,13 +16,13 @@ data class StatusControllerConfig( val environmentStatusEffectUpdateTimer: Double = 0.15, val primaryAnimationConfig: AssetPath? = null, val primaryScriptSources: ImmutableList = ImmutableList.of(), - val primaryScriptDelta: Int = 1, + val primaryScriptDelta: Double = 1.0, val keepDamageNotificationSteps: Int = 120, val stats: ImmutableMap = ImmutableMap.of(), val resources: ImmutableMap = ImmutableMap.of(), ) { init { - require(primaryScriptDelta >= 1) { "Non-positive primaryScriptDelta: $primaryScriptDelta" } + require(primaryScriptDelta > 0.0) { "Non-positive primaryScriptDelta: $primaryScriptDelta" } require(keepDamageNotificationSteps >= 1) { "Non-positive keepDamageNotificationSteps: $keepDamageNotificationSteps" } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/Types.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/Types.kt index aab5bff1..ec04a208 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/Types.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/Types.kt @@ -1,10 +1,12 @@ package ru.dbotthepony.kstarbound.defs.actor import ru.dbotthepony.kommons.util.Either +import ru.dbotthepony.kstarbound.Registry +import ru.dbotthepony.kstarbound.defs.StatusEffectDefinition import ru.dbotthepony.kstarbound.json.builder.IStringSerializable // stat modifier or named status effect -typealias PersistentStatusEffect = Either +typealias PersistentStatusEffect = Either> // uint8_t enum class Gender(override val jsonName: String) : IStringSerializable { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/BehaviorDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/BehaviorDefinition.kt new file mode 100644 index 00000000..1cb18d6f --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/BehaviorDefinition.kt @@ -0,0 +1,22 @@ +package ru.dbotthepony.kstarbound.defs.actor.behavior + +import com.google.common.collect.ImmutableList +import com.google.common.collect.ImmutableMap +import com.google.common.collect.ImmutableSet +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import ru.dbotthepony.kstarbound.defs.AssetPath +import ru.dbotthepony.kstarbound.json.builder.JsonFactory + +@JsonFactory +data class BehaviorDefinition( + val name: String, + val parameters: ImmutableMap = ImmutableMap.of(), + val scripts: ImmutableSet = ImmutableSet.of(), + val root: JsonObject, +) { + val mappedParameters: ImmutableMap = parameters.entries + .stream() + .map { it.key to NodeParameterValue(null, it.value) } + .collect(ImmutableMap.toImmutableMap({ it.first }, { it.second })) +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/BehaviorNodeDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/BehaviorNodeDefinition.kt new file mode 100644 index 00000000..1493f436 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/BehaviorNodeDefinition.kt @@ -0,0 +1,19 @@ +package ru.dbotthepony.kstarbound.defs.actor.behavior + +import com.google.common.collect.ImmutableMap +import ru.dbotthepony.kstarbound.json.builder.JsonFactory + +/** + * Reflects node on behavior graph. + * + * Behavior graph is not what most would expect, since this is code flow graph + * (nodes represent Lua functions), and not decision tree. + */ +@JsonFactory +data class BehaviorNodeDefinition( + /** + * actually should be called "parameters" + */ + val properties: ImmutableMap = ImmutableMap.of(), + val output: ImmutableMap = ImmutableMap.of(), +) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/CompositeNodeType.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/CompositeNodeType.kt new file mode 100644 index 00000000..a2360165 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/CompositeNodeType.kt @@ -0,0 +1,20 @@ +package ru.dbotthepony.kstarbound.defs.actor.behavior + +import com.google.common.collect.ImmutableList +import ru.dbotthepony.kstarbound.json.builder.IStringSerializable +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 ru.dbotthepony.kstarbound.world.entities.behavior.DynamicNode +import ru.dbotthepony.kstarbound.world.entities.behavior.ParallelNode +import ru.dbotthepony.kstarbound.world.entities.behavior.RandomizeNode +import ru.dbotthepony.kstarbound.world.entities.behavior.SequenceNode + +enum class CompositeNodeType(override val jsonName: String, val factory: (Map, ImmutableList) -> AbstractBehaviorNode) : IStringSerializable { + SEQUENCE("Sequence", { _, c -> SequenceNode(c) }), + // in original engine, selector and sequence nodes are identical code-wise + SELECTOR("Selector", { _, c -> SequenceNode(c) }), + PARALLEL("Parallel", { p, c -> ParallelNode(p, c) }), + DYNAMIC("Dynamic", { _, c -> DynamicNode(c) }), + RANDOMIZE("Randomize", { _, c -> RandomizeNode(c) }); +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/NodeOutput.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/NodeOutput.kt new file mode 100644 index 00000000..e47ce4b7 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/NodeOutput.kt @@ -0,0 +1,42 @@ +package ru.dbotthepony.kstarbound.defs.actor.behavior + +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.get +import ru.dbotthepony.kstarbound.json.popObject +import ru.dbotthepony.kstarbound.util.asStringOrNull +import ru.dbotthepony.kstarbound.util.valueOf +import ru.dbotthepony.kstarbound.world.entities.behavior.NodeParameterType + +@JsonAdapter(NodeOutput.Adapter::class) +data class NodeOutput(val type: NodeParameterType, val key: String? = null, val ephemeral: Boolean = false) { + class Adapter : TypeAdapter() { + override fun write(out: JsonWriter, value: NodeOutput) { + out.beginObject() + out.name("type") + out.value(value.type.jsonName) + + if (value.key != null) { + out.name("key") + out.value(value.key) + } + + out.name("ephemeral") + out.value(value.ephemeral) + + out.endObject() + } + + override fun read(`in`: JsonReader): NodeOutput { + val json = `in`.popObject() + + return NodeOutput( + NodeParameterType.entries.valueOf(json["type"].asString), + json["key"]?.asStringOrNull, + json.get("ephemeral", false) + ) + } + } +} 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 new file mode 100644 index 00000000..4efa4c91 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/NodeParameter.kt @@ -0,0 +1,45 @@ +package ru.dbotthepony.kstarbound.defs.actor.behavior + +import com.google.gson.JsonNull +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.kommons.gson.value +import ru.dbotthepony.kstarbound.json.popObject +import ru.dbotthepony.kstarbound.util.valueOf +import ru.dbotthepony.kstarbound.world.entities.behavior.NodeParameterType + +@JsonAdapter(NodeParameter.Adapter::class) +data class NodeParameter(val type: NodeParameterType, val value: NodeParameterValue) { + class Adapter : TypeAdapter() { + override fun write(out: JsonWriter, value: NodeParameter) { + out.beginObject() + + out.name("type") + out.value(value.type.jsonName) + + if (value.value.key != null) { + out.name("key") + out.value(value.value.key) + } else { + out.name("value") + out.value(value.value.value) + } + + out.endObject() + } + + override fun read(`in`: JsonReader): NodeParameter { + val json = `in`.popObject() + val type = NodeParameterType.entries.valueOf(json["type"].asString) + + if ("key" in json) { + return NodeParameter(type, NodeParameterValue(json["key"].asString, null)) + } else { + return NodeParameter(type, NodeParameterValue(null, json["value"] ?: JsonNull.INSTANCE)) + } + } + } +} 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 new file mode 100644 index 00000000..9d575d17 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/behavior/NodeParameterValue.kt @@ -0,0 +1,41 @@ +package ru.dbotthepony.kstarbound.defs.actor.behavior + +import com.google.gson.JsonElement +import com.google.gson.JsonNull +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.kommons.gson.value +import ru.dbotthepony.kstarbound.json.popObject + +/** + * [key] - references earlier value + * + * [value] - carries a value + */ +@JsonAdapter(NodeParameterValue.Adapter::class) +data class NodeParameterValue(val key: String?, val value: JsonElement?) { + class Adapter : TypeAdapter() { + override fun write(out: JsonWriter, value: NodeParameterValue) { + if (value.key != null) { + out.name("key") + out.value(value.key) + } else { + out.name("value") + out.value(value.value) + } + } + + override fun read(`in`: JsonReader): NodeParameterValue { + val json = `in`.popObject() + + if ("key" in json) { + return NodeParameterValue(json["key"].asString, null) + } else { + return NodeParameterValue(null, json["value"] ?: JsonNull.INSTANCE) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDescriptor.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDescriptor.kt index 8c79cefc..1b31f715 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDescriptor.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDescriptor.kt @@ -38,6 +38,7 @@ import ru.dbotthepony.kstarbound.lua.StateMachine import ru.dbotthepony.kstarbound.lua.from import ru.dbotthepony.kstarbound.lua.get import ru.dbotthepony.kstarbound.lua.indexNoYield +import ru.dbotthepony.kstarbound.lua.toByteString import ru.dbotthepony.kstarbound.lua.toJson import ru.dbotthepony.kstarbound.lua.toJsonFromLua import java.io.DataInputStream @@ -198,7 +199,7 @@ data class ItemDescriptor( } return allocator.newTable(0, 3).also { - it.rawset("name", name) + it.rawset("name", name.toByteString()) it.rawset("count", count) it.rawset("parameters", allocator.from(parameters)) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/monster/ActionDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/monster/ActionDefinition.kt deleted file mode 100644 index 6c0db657..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/monster/ActionDefinition.kt +++ /dev/null @@ -1,7 +0,0 @@ -package ru.dbotthepony.kstarbound.defs.monster - -data class ActionDefinition( - val name: String, // ссылается на .nodes? - val cooldown: Double = -1.0, - // val parameters -) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/monster/MonsterPaletteSwap.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/monster/MonsterPaletteSwap.kt new file mode 100644 index 00000000..5161d6e2 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/monster/MonsterPaletteSwap.kt @@ -0,0 +1,11 @@ +package ru.dbotthepony.kstarbound.defs.monster + +import com.google.common.collect.ImmutableList +import ru.dbotthepony.kstarbound.defs.ColorReplacements +import ru.dbotthepony.kstarbound.json.builder.JsonFactory + +@JsonFactory +class MonsterPaletteSwap( + val name: String, + val swaps: ImmutableList, +) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/monster/MonsterPartDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/monster/MonsterPartDefinition.kt new file mode 100644 index 00000000..64373abe --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/monster/MonsterPartDefinition.kt @@ -0,0 +1,17 @@ +package ru.dbotthepony.kstarbound.defs.monster + +import com.google.common.collect.ImmutableMap +import com.google.gson.JsonObject +import ru.dbotthepony.kstarbound.defs.AssetPath +import ru.dbotthepony.kstarbound.json.builder.JsonFactory + +@JsonFactory +data class MonsterPartDefinition( + val name: String, + val category: String, + val type: String, + + // key -> image + val frames: ImmutableMap, + val parameters: JsonObject = JsonObject(), +) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/monster/MonsterSkillDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/monster/MonsterSkillDefinition.kt index 7dbe9c65..12bb822e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/monster/MonsterSkillDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/monster/MonsterSkillDefinition.kt @@ -1,15 +1,15 @@ package ru.dbotthepony.kstarbound.defs.monster -import com.google.common.collect.ImmutableMap -import com.google.gson.JsonElement -import ru.dbotthepony.kstarbound.defs.image.SpriteReference +import com.google.gson.JsonObject import ru.dbotthepony.kstarbound.json.builder.JsonFactory @JsonFactory data class MonsterSkillDefinition( val name: String, - val label: String? = null, - val image: SpriteReference? = null, - val config: ImmutableMap = ImmutableMap.of(), - val animationParameters: ImmutableMap = ImmutableMap.of(), + val label: String, + val image: String, + + val config: JsonObject = JsonObject(), + val parameters: JsonObject = JsonObject(), + val animationParameters: JsonObject = JsonObject(), ) 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 59337ab5..ac5b0254 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/monster/MonsterTypeDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/monster/MonsterTypeDefinition.kt @@ -3,34 +3,284 @@ package ru.dbotthepony.kstarbound.defs.monster import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableMap import com.google.common.collect.ImmutableSet +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonPrimitive +import ru.dbotthepony.kommons.gson.JsonArrayCollector +import ru.dbotthepony.kommons.gson.contains +import ru.dbotthepony.kommons.gson.get +import ru.dbotthepony.kommons.gson.getArray +import ru.dbotthepony.kommons.gson.set import ru.dbotthepony.kommons.util.Either +import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Registry -import ru.dbotthepony.kstarbound.defs.ActorMovementParameters +import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.AssetReference -import ru.dbotthepony.kstarbound.defs.IScriptable -import ru.dbotthepony.kstarbound.defs.IThingWithDescription import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition import ru.dbotthepony.kstarbound.defs.item.TreasurePoolDefinition +import ru.dbotthepony.kstarbound.fromJsonFast import ru.dbotthepony.kstarbound.json.builder.JsonFactory -import ru.dbotthepony.kstarbound.json.builder.JsonFlat +import ru.dbotthepony.kstarbound.json.mergeJson +import ru.dbotthepony.kstarbound.util.AssetPathStack +import ru.dbotthepony.kstarbound.util.random.MWCRandom +import ru.dbotthepony.kstarbound.util.random.nextUInt +import ru.dbotthepony.kstarbound.util.random.random +import java.util.concurrent.CompletableFuture +import kotlin.math.max +import kotlin.math.min +import kotlin.math.pow + +private val specialMerge = ImmutableSet.of("scripts", "skills", "specialSkills", "baseSkills") + +private fun mergeFinalParameters(params: List): JsonObject { + val parameters = JsonObject() + + for (baseParameters in params) { + if (baseParameters !is JsonObject) + continue + + for ((k, v) in baseParameters.entrySet()) { + // Hard-coded merge for scripts and skills parameters, otherwise merge. + if (k in specialMerge) { + v as JsonArray + val v2 = parameters[k] + + if (v2 !is JsonArray) { + parameters[k] = v.deepCopy() + } else { + val result = JsonArray(v.size() + v2.size()) + result.addAll(v2) + result.addAll(v) + parameters[k] = result + } + } else { + parameters[k] = mergeJson((parameters[k] ?: JsonNull.INSTANCE).deepCopy(), v) + } + } + } + + return parameters +} @JsonFactory data class MonsterTypeDefinition( val type: String, - @JsonFlat - val desc: IThingWithDescription, - val categories: ImmutableSet = ImmutableSet.of(), - val parts: ImmutableSet = ImmutableSet.of(), + val shortdescription: String? = null, + val description: String? = null, + val categories: ImmutableSet, + val parts: ImmutableSet, val animation: AssetReference, - // [ { "default" : "poptopTreasure", "bow" : "poptopHunting" } ], - // "dropPools" : [ "smallRobotTreasure" ], - val dropPools: Either>>, ImmutableList>>, - val baseParameters: BaseParameters -) : IThingWithDescription by desc { - @JsonFactory - data class BaseParameters( - val movementSettings: ActorMovementParameters? = null, - @JsonFlat - val script: IScriptable, - ) : IScriptable by script + val colors: String = "default", + val reversed: Boolean = false, + val dropPools: ImmutableList>, Registry.Ref>> = ImmutableList.of(), + val baseParameters: JsonElement = JsonNull.INSTANCE, + + @Deprecated("Raw property, use processed one", replaceWith = ReplaceWith("this.paramsOverrides")) + val partParameters: JsonElement = JsonNull.INSTANCE, + + @Deprecated("Raw property, use processed one", replaceWith = ReplaceWith("this.paramsDesc")) + val partParameterDescription: JsonElement = JsonObject() +) { + val paramsDesc: CompletableFuture + val paramsOverrides: CompletableFuture + + init { + if (partParameters.isJsonNull) { + // outdated monsters still have partParameterDescription defined + // directly in the + // .monstertype file + paramsDesc = CompletableFuture.completedFuture(partParameterDescription.asJsonObject) + paramsOverrides = CompletableFuture.completedFuture(JsonObject()) + } else { + // for updated monsters, use the partParameterDescription from the + // .partparams file + val json = Starbound.loadJsonAsset(partParameters, AssetPathStack.lastFolder()) + + paramsDesc = json.thenApply { + it.asJsonObject.get("partParameterDescription").asJsonObject + } + + paramsOverrides = json.thenApply { + it.asJsonObject.get("partParameters").asJsonObject + } + } + } + + fun create(seed: Long, uniqueParameters: JsonObject): MonsterVariant { + // MWCRandom since this MUST product exactly the same result + // between legacy client and new server (or new client and legacy server) + // Thanks Chuckleclusterfuck + val random = MWCRandom(seed.toULong(), 256, 32) + + val copyParameters = uniqueParameters.deepCopy() + + // select a list of monster parts + val monsterParts = ArrayList() + val categoryName = categories.random(random, true) + var selectedParts = uniqueParameters["selectedParts"] + + // key -> image + val animatorPartTags = HashMap() + + if (selectedParts !is JsonObject) { + selectedParts = JsonObject() + uniqueParameters["selectedParts"] = selectedParts + } + + for (partTypeName in parts) { + val randPart = Registries.selectMonsterPart(categoryName, partTypeName, random) + val selectedPart = selectedParts[partTypeName]?.asString + + if (selectedPart != null) { + val get = Registries.getMonsterPart(categoryName, partTypeName, selectedPart) + + if (get != null) { + monsterParts.add(get) + } + } else if (randPart != null) { + monsterParts.add(randPart) + + // cheat the system; while this will surely poison uniqueParameters, at least + // we will make sure selected parts are chosen deterministically across sessions / sides + // However, this hack will only work if we are server in both meanings; + // e.g. we are fully in charge of created monster. If monster is spawned by player, + // then, well, it sucks. + selectedParts[partTypeName] = randPart.name + } + } + + for (partConfig in monsterParts) { + for ((k, v) in partConfig.frames) { + animatorPartTags[k] = v.fullPath + } + } + + val partParameterList = ArrayList() + + for (partConfig in monsterParts) { + partParameterList.add(partConfig.parameters) + + // Include part parameter overrides + if (partConfig.name in paramsOverrides.get()) { + partParameterList.add(paramsOverrides.get()[partConfig.name].asJsonObject) + } + } + + // merge part parameters and unique parameters into base parameters + var parameters = JsonObject() + + // First assign all the defaults. + for ((k, v) in paramsDesc.get().entrySet()) { + parameters[k] = v.asJsonArray.get(1).deepCopy() + } + + // Then go through parameter list and merge based on the merge rules. + for (applyParams in partParameterList) { + for ((k, v) in applyParams.entrySet()) { + val mergeMethod = paramsDesc.get()[k]?.asJsonArray?.get(0)?.asString ?: "override" + var value = parameters[k] ?: JsonNull.INSTANCE + + when (mergeMethod.lowercase()) { + "add" -> value = JsonPrimitive(value.asDouble + v.asDouble) + "sub" -> value = JsonPrimitive(value.asDouble - v.asDouble) + "multiply" -> value = JsonPrimitive(value.asDouble * v.asDouble) + "divide" -> value = JsonPrimitive(value.asDouble / v.asDouble) + "inverse" -> value = JsonPrimitive(v.asDouble / value.asDouble) + "min" -> value = JsonPrimitive(min(value.asDouble, v.asDouble)) + "max" -> value = JsonPrimitive(max(value.asDouble, v.asDouble)) + "pow" -> value = JsonPrimitive(value.asDouble.pow(v.asDouble)) + "invpow" -> value = JsonPrimitive(v.asDouble.pow(value.asDouble)) + + "override" -> { + if (!v.isJsonNull) // why does original engine check this? + value = v + } + + "merge" -> { + // "merge" means to either merge maps, or *append* lists together + if (!v.isJsonNull) { + if (value.isJsonNull) { + value = v.deepCopy() + } else if (value::class != v::class) { + value = v.deepCopy() + } else { + if (v is JsonArray) { + value = value.asJsonArray.also { it.addAll(v) } + } else if (v is JsonObject) { + value = mergeJson(value, v) + } + } + } + } + } + + parameters[k] = value + } + } + + parameters = mergeFinalParameters(listOf(baseParameters, parameters)) + mergeJson(parameters, uniqueParameters) + + var animationConfig = animation.json.get() ?: JsonNull.INSTANCE + val skillNames = ArrayList() + + if ("baseSkills" in parameters || "specialSkills" in parameters) { + val skillCount = parameters.get("skillCount", 2) + + val baseSkillNames = parameters.getArray("baseSkills").deepCopy() + val specialSkillNames = parameters.getArray("specialSkills").deepCopy() + + // First, pick from base skills + while (baseSkillNames.size() > 0 && skillNames.size < skillCount) { + val pick = random.nextUInt(baseSkillNames.size().toULong() - 1UL).toInt() + skillNames.add(baseSkillNames.remove(pick).asString) + } + + // ...then fill in from special skills as needed + while (specialSkillNames.size() > 0 && skillNames.size < skillCount) { + val pick = random.nextUInt(specialSkillNames.size().toULong() - 1UL).toInt() + skillNames.add(specialSkillNames.remove(pick).asString) + } + } else if ("skills" in parameters) { + val availableSkillNames = parameters.getArray("skills").deepCopy() + val skillCount = min(parameters.get("skillCount", 2), availableSkillNames.size()) + + while (skillNames.size < skillCount) { + val pick = random.nextUInt(availableSkillNames.size().toULong() - 1UL).toInt() + skillNames.add(availableSkillNames.remove(pick).asString) + } + } + + if (skillNames.isNotEmpty()) { + animationConfig = animationConfig.deepCopy() + val allParameters = ArrayList() + + for (skillName in skillNames) { + val skill = Registries.monsterSkills[skillName] ?: continue + allParameters.add(skill.value.parameters) + animationConfig = mergeJson(animationConfig, skill.value.animationParameters) + } + + // Need to override the final list of skills, instead of merging the lists + parameters = mergeFinalParameters(allParameters) + parameters["skills"] = skillNames.stream().map { JsonPrimitive(it) }.collect(JsonArrayCollector) + } + + val variant = MonsterVariant( + type = type, + description = description, + shortDescription = shortdescription, + seed = seed, + uniqueParameters = copyParameters, + animationConfig = Starbound.gson.fromJsonFast(animationConfig, AnimationDefinition::class.java), + reversed = reversed, + animatorPartTags = ImmutableMap.copyOf(animatorPartTags), + parameters = parameters, + dropPools = dropPools, + ) + + return variant + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/monster/MonsterVariant.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/monster/MonsterVariant.kt new file mode 100644 index 00000000..230abdcb --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/monster/MonsterVariant.kt @@ -0,0 +1,145 @@ +package ru.dbotthepony.kstarbound.defs.monster + +import com.google.common.collect.ImmutableList +import com.google.common.collect.ImmutableMap +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import ru.dbotthepony.kommons.io.readBinaryString +import ru.dbotthepony.kommons.io.writeBinaryString +import ru.dbotthepony.kommons.math.RGBAColor +import ru.dbotthepony.kommons.util.Either +import ru.dbotthepony.kstarbound.Registries +import ru.dbotthepony.kstarbound.Registry +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.client.render.RenderLayer +import ru.dbotthepony.kstarbound.defs.ActorMovementParameters +import ru.dbotthepony.kstarbound.defs.AssetPath +import ru.dbotthepony.kstarbound.defs.ClientEntityMode +import ru.dbotthepony.kstarbound.defs.ColorReplacements +import ru.dbotthepony.kstarbound.defs.JsonFunction +import ru.dbotthepony.kstarbound.defs.TeamType +import ru.dbotthepony.kstarbound.defs.actor.StatusControllerConfig +import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition +import ru.dbotthepony.kstarbound.defs.item.TreasurePoolDefinition +import ru.dbotthepony.kstarbound.fromJsonFast +import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.json.readJsonElement +import ru.dbotthepony.kstarbound.json.writeJsonElement +import ru.dbotthepony.kstarbound.math.AABB +import ru.dbotthepony.kstarbound.math.vector.Vector2d +import ru.dbotthepony.kstarbound.util.random.random +import ru.dbotthepony.kstarbound.util.random.staticRandomInt +import ru.dbotthepony.kstarbound.world.PIXELS_IN_STARBOUND_UNIT +import ru.dbotthepony.kstarbound.world.physics.Poly +import java.io.DataInputStream +import java.io.DataOutputStream +import java.util.random.RandomGenerator + +@JsonFactory +class MonsterVariant( + val type: String, + val shortDescription: String? = null, + val description: String? = null, + val seed: Long = 0L, + val uniqueParameters: JsonObject = JsonObject(), + val animationConfig: AnimationDefinition, + val reversed: Boolean = false, + val animatorPartTags: ImmutableMap, + val parameters: JsonObject, + + @Deprecated("Raw property", replaceWith = ReplaceWith("this.actualDropPools")) + val dropPools: ImmutableList>, Registry.Ref>> = ImmutableList.of(), +) { + @JsonFactory + data class CommonParameters( + val shortDescription: String? = null, + val description: String? = null, + val scripts: ImmutableList, + val animationScripts: ImmutableList = ImmutableList.of(), + val animationCustom: JsonElement = JsonNull.INSTANCE, + val initialScriptDelta: Double = 5.0, + val metaBoundBox: AABB, + val scale: Double, + val renderLayer: RenderLayer.Point = RenderLayer.Point(RenderLayer.Monster), + val movementSettings: ActorMovementParameters = ActorMovementParameters.EMPTY, + + val dropPools: ImmutableList>, Registry.Ref>>? = null, + + val walkMultiplier: Double = 1.0, + val runMultiplier: Double = 1.0, + val jumpMultiplier: Double = 1.0, + val weightMultiplier: Double = 1.0, + val healthMultiplier: Double = 1.0, + val touchDamageMultiplier: Double = 1.0, + val touchDamage: JsonElement = JsonNull.INSTANCE, + val animationDamageParts: JsonObject = JsonObject(), + val statusSettings: StatusControllerConfig? = null, + val knockoutTime: Double = 1.0, + val knockoutEffect: String? = null, + val knockoutAnimationStates: ImmutableMap = ImmutableMap.of(), + + // in pixels + val mouthOffset: Vector2d, + + // in pixels + val feetOffset: Vector2d, + + val powerLevelFunction: Registry.Ref = Registries.jsonFunctions.ref("monsterLevelPowerMultiplier"), + val healthLevelFunction: Registry.Ref = Registries.jsonFunctions.ref("monsterLevelHealthMultiplier"), + + val clientEntityMode: ClientEntityMode = ClientEntityMode.CLIENT_SLAVE_ONLY, + val persistent: Boolean = false /* WHAT. */, + val damageTeamType: TeamType = TeamType.ENEMY, + val damageTeam: Int = 2, + + val selfDamagePoly: Poly = movementSettings.standingPoly?.map({ it }, { it.firstOrNull() }) ?: Poly.EMPTY, + val portraitIcon: String? = null, + + val damageReceivedAggressiveDuration: Double = 1.0, + val onDamagedOthersAggressiveDuration: Double = 5.0, + val onFireAggressiveDuration: Double = 5.0, + + val nametagColor: RGBAColor = RGBAColor.WHITE, + val colorSwap: ColorReplacements? = null, + ) { + val mouthOffsetTiles = mouthOffset / PIXELS_IN_STARBOUND_UNIT + val feetOffsetTiles = feetOffset / PIXELS_IN_STARBOUND_UNIT + } + + val entry = Registries.monsterTypes.getOrThrow(type) + val commonParameters = Starbound.gson.fromJsonFast(parameters, CommonParameters::class.java) + + val animatorZoom: Double + get() = commonParameters.scale + + 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")] + + fun write(stream: DataOutputStream, isLegacy: Boolean) { + if (isLegacy) { + stream.writeBinaryString(type) + stream.writeLong(seed) + stream.writeJsonElement(uniqueParameters) + } else { + TODO("Native") + } + } + + companion object { + fun read(stream: DataInputStream, isLegacy: Boolean): MonsterVariant { + if (isLegacy) { + val name = stream.readBinaryString() + val seed = stream.readLong() + val uniqueParameters = stream.readJsonElement() as JsonObject + + val base = Registries.monsterTypes.getOrThrow(name) + return base.value.create(seed, uniqueParameters) + } else { + TODO("Native") + } + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/quest/QuestGlobalConfig.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/quest/QuestGlobalConfig.kt new file mode 100644 index 00000000..eaeac786 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/quest/QuestGlobalConfig.kt @@ -0,0 +1,7 @@ +package ru.dbotthepony.kstarbound.defs.quest + +import ru.dbotthepony.kstarbound.math.vector.Vector2d + +data class QuestGlobalConfig( + val defaultIndicatorOffset: Vector2d = Vector2d.ZERO +) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/AsteroidWorldsConfig.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/AsteroidWorldsConfig.kt index 05da6328..b36d9fc6 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/AsteroidWorldsConfig.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/AsteroidWorldsConfig.kt @@ -4,6 +4,7 @@ import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableMap import com.google.common.collect.ImmutableSet import ru.dbotthepony.kommons.math.RGBAColor +import ru.dbotthepony.kstarbound.defs.actor.PersistentStatusEffect import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.math.vector.Vector2i import ru.dbotthepony.kstarbound.json.builder.JsonFactory @@ -15,7 +16,7 @@ data class AsteroidWorldsConfig( val gravityRange: Vector2d, val worldSize: Vector2i, val threatRange: Vector2d, - val environmentStatusEffects: ImmutableSet = ImmutableSet.of(), + val environmentStatusEffects: ImmutableSet = ImmutableSet.of(), val overrideTech: ImmutableSet? = null, val globalDirectives: ImmutableSet? = null, val beamUpRule: BeamUpRule = BeamUpRule.SURFACE, // TODO: ??? why surface? in asteroid field. 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 c7ef6ccf..c420a6be 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BiomeDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BiomeDefinition.kt @@ -9,6 +9,7 @@ import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.collect.WeightedList import ru.dbotthepony.kstarbound.defs.AssetReference import ru.dbotthepony.kstarbound.defs.JsonConfigFunction +import ru.dbotthepony.kstarbound.defs.actor.PersistentStatusEffect import ru.dbotthepony.kstarbound.defs.tile.TileDefinition import ru.dbotthepony.kstarbound.json.NativeLegacy import ru.dbotthepony.kstarbound.json.builder.JsonFactory @@ -21,7 +22,7 @@ import java.util.random.RandomGenerator data class BiomeDefinition( val airless: Boolean = false, val name: String, - val statusEffects: ImmutableSet = ImmutableSet.of(), + val statusEffects: ImmutableSet = ImmutableSet.of(), val weather: ImmutableList>>>> = ImmutableList.of(), // binned reference to other assets val hueShiftOptions: ImmutableList = ImmutableList.of(), val skyOptions: ImmutableList = ImmutableList.of(), diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/DungeonWorldsConfig.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/DungeonWorldsConfig.kt index df5412cb..c71d6dce 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/DungeonWorldsConfig.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/DungeonWorldsConfig.kt @@ -8,6 +8,7 @@ import ru.dbotthepony.kstarbound.math.vector.Vector2i import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.collect.WeightedList import ru.dbotthepony.kstarbound.defs.AssetPath +import ru.dbotthepony.kstarbound.defs.actor.PersistentStatusEffect import ru.dbotthepony.kstarbound.defs.dungeon.DungeonDefinition import ru.dbotthepony.kstarbound.json.builder.JsonFactory @@ -17,7 +18,7 @@ data class DungeonWorldsConfig( val worldSize: Vector2i, val gravity: Either, val airless: Boolean = false, - val environmentStatusEffects: ImmutableSet = ImmutableSet.of(), + val environmentStatusEffects: ImmutableSet = ImmutableSet.of(), val overrideTech: ImmutableSet? = null, val globalDirectives: ImmutableSet? = null, val beamUpRule: BeamUpRule = BeamUpRule.SURFACE, // TODO: ??? why surface? in floating dungeon. diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TerrestrialWorldParameters.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TerrestrialWorldParameters.kt index d927dd21..d2e172de 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TerrestrialWorldParameters.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TerrestrialWorldParameters.kt @@ -540,7 +540,7 @@ class TerrestrialWorldParameters : VisitableWorldParameters() { while (indices.isNotEmpty()) { val v = indices.random(random) shuffled.add(v) - indices.removeInt(indices.indexOf(v)) + indices.rem(v) } while (secondaryRegionCount-- > 0 && shuffled.isNotEmpty()) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/VisitableWorldParameters.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/VisitableWorldParameters.kt index c3373198..0c5cbc9b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/VisitableWorldParameters.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/VisitableWorldParameters.kt @@ -23,10 +23,15 @@ import ru.dbotthepony.kommons.io.writeByteArray import ru.dbotthepony.kommons.io.writeCollection import ru.dbotthepony.kommons.io.writeStruct2i import ru.dbotthepony.kommons.util.Either +import ru.dbotthepony.kstarbound.Registries +import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.math.vector.Vector2i import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.collect.WeightedList +import ru.dbotthepony.kstarbound.defs.EphemeralStatusEffect +import ru.dbotthepony.kstarbound.defs.StatusEffectDefinition +import ru.dbotthepony.kstarbound.defs.actor.PersistentStatusEffect import ru.dbotthepony.kstarbound.fromJson import ru.dbotthepony.kstarbound.fromJsonFast import ru.dbotthepony.kstarbound.io.readDouble @@ -110,7 +115,7 @@ abstract class VisitableWorldParameters { protected set var airless: Boolean = false protected set - var environmentStatusEffects: Set by Delegates.notNull() + var environmentStatusEffects: Set by Delegates.notNull() protected set var overrideTech: Set? = null protected set @@ -140,7 +145,7 @@ abstract class VisitableWorldParameters { val worldSize: Vector2i, val gravity: Either, val airless: Boolean, - val environmentStatusEffects: Set, + val environmentStatusEffects: Set, val overrideTech: Set? = null, val globalDirectives: Set? = null, val beamUpRule: BeamUpRule, @@ -211,7 +216,7 @@ abstract class VisitableWorldParameters { if (collection.isNotEmpty()) weatherPool = WeightedList(ImmutableList.copyOf(collection)) - environmentStatusEffects = ImmutableSet.copyOf(stream.readCollection { readInternedString() }) + environmentStatusEffects = ImmutableSet.copyOf(stream.readCollection { Either.right(Registries.statusEffects.ref(readInternedString())) }) overrideTech = stream.readNullable { ImmutableSet.copyOf(readCollection { readInternedString() }) } globalDirectives = stream.readNullable { ImmutableSet.copyOf(readCollection { readInternedString() }) } @@ -234,7 +239,7 @@ abstract class VisitableWorldParameters { else stream.writeCollection(weatherPool!!.parent) { writeDouble(it.first); writeBinaryString(it.second) } - stream.writeCollection(environmentStatusEffects) { writeBinaryString(it) } + stream.writeCollection(environmentStatusEffects.filter { it.isRight }.map { it.right() }) { writeBinaryString(it.key.left()) } stream.writeNullable(overrideTech) { writeCollection(it) { writeBinaryString(it) } } stream.writeNullable(globalDirectives) { writeCollection(it) { writeBinaryString(it) } } stream.writeByte(beamUpRule.ordinal) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt index 70d6d6fd..5916cc3a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt @@ -6,11 +6,15 @@ import it.unimi.dsi.fastutil.objects.ObjectArrayList import org.apache.logging.log4j.LogManager import ru.dbotthepony.kstarbound.math.AABBi import ru.dbotthepony.kommons.util.Either +import ru.dbotthepony.kommons.util.IStruct2d +import ru.dbotthepony.kommons.util.IStruct2i import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.math.vector.Vector2i import ru.dbotthepony.kstarbound.Globals import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.StatusEffectDefinition +import ru.dbotthepony.kstarbound.defs.actor.PersistentStatusEffect import ru.dbotthepony.kstarbound.defs.dungeon.DungeonDefinition import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplateConfig.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplateConfig.kt index db413d3a..ca45e3fb 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplateConfig.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplateConfig.kt @@ -1,6 +1,8 @@ package ru.dbotthepony.kstarbound.defs.world +import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.math.vector.Vector2d @JsonFactory data class WorldTemplateConfig( @@ -14,4 +16,8 @@ data class WorldTemplateConfig( val customTerrainBlendSize: Double = 0.0, val surfaceCaveAttenuationDist: Double = 0.0, val surfaceCaveAttenuationFactor: Double = 1.0, -) + + val defaultGravity: Either, +) { + val defaultGravityVector = defaultGravity.map({ it }, { Vector2d(0.0, it) }) +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/BTreeDB5.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/BTreeDB5.kt index b22d44f5..f9ff3ccb 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/io/BTreeDB5.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/BTreeDB5.kt @@ -2,8 +2,6 @@ package ru.dbotthepony.kstarbound.io import it.unimi.dsi.fastutil.io.FastByteArrayInputStream import it.unimi.dsi.fastutil.longs.LongArrayList -import ru.dbotthepony.kommons.io.ByteKey -import ru.dbotthepony.kommons.io.readByteKeyRaw import ru.dbotthepony.kommons.io.readVarInt import ru.dbotthepony.kommons.util.KOptional import java.io.Closeable diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/ByteKey.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/ByteKey.kt new file mode 100644 index 00000000..fb442099 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/ByteKey.kt @@ -0,0 +1,103 @@ +package ru.dbotthepony.kstarbound.io + +import ru.dbotthepony.kommons.io.writeVarInt +import java.io.InputStream +import java.io.OutputStream +import java.util.* + +private fun toBytes(key: UUID): ByteArray { + val value = ByteArray(16) + + value[0] = ((key.mostSignificantBits ushr 56) and 0xFFL).toByte() + value[1] = ((key.mostSignificantBits ushr 48) and 0xFFL).toByte() + value[2] = ((key.mostSignificantBits ushr 40) and 0xFFL).toByte() + value[3] = ((key.mostSignificantBits ushr 32) and 0xFFL).toByte() + value[4] = ((key.mostSignificantBits ushr 24) and 0xFFL).toByte() + value[5] = ((key.mostSignificantBits ushr 16) and 0xFFL).toByte() + value[6] = ((key.mostSignificantBits ushr 8) and 0xFFL).toByte() + value[7] = ((key.mostSignificantBits ushr 0) and 0xFFL).toByte() + + value[8 + 0] = ((key.leastSignificantBits ushr 56) and 0xFFL).toByte() + value[8 + 1] = ((key.leastSignificantBits ushr 48) and 0xFFL).toByte() + value[8 + 2] = ((key.leastSignificantBits ushr 40) and 0xFFL).toByte() + value[8 + 3] = ((key.leastSignificantBits ushr 32) and 0xFFL).toByte() + value[8 + 4] = ((key.leastSignificantBits ushr 24) and 0xFFL).toByte() + value[8 + 5] = ((key.leastSignificantBits ushr 16) and 0xFFL).toByte() + value[8 + 6] = ((key.leastSignificantBits ushr 8) and 0xFFL).toByte() + value[8 + 7] = ((key.leastSignificantBits ushr 0) and 0xFFL).toByte() + + return value +} + +class ByteKey private constructor(private val bytes: ByteArray, mark: Nothing?) : Comparable { + constructor(vararg bytes: Byte) : this(bytes, null) + constructor(key: String) : this(key.toByteArray(), null) + constructor(key: UUID) : this(toBytes(key), null) + + override fun equals(other: Any?): Boolean { + return this === other || other is ByteKey && other.bytes.contentEquals(bytes) + } + + val size: Int + get() = bytes.size + + operator fun get(index: Int): Byte { + return bytes[index] + } + + fun write(stream: OutputStream) { + stream.writeVarInt(bytes.size) + stream.write(bytes) + } + + fun writeRaw(stream: OutputStream) { + stream.write(bytes) + } + + fun toByteArray(): ByteArray { + return bytes.clone() + } + + override fun hashCode(): Int { + return bytes.contentHashCode() + } + + override fun toString(): String { + return "ByteKey[${bytes.map { it.toInt() and 0xFF }.joinToString(", ")}]" + } + + override fun compareTo(other: ByteKey): Int { + val cmp = size.compareTo(other.size) + if (cmp != 0) return cmp + + for (i in bytes.indices) { + if (bytes[i].toInt() and 0xFF > other.bytes[i].toInt() and 0xFF) { + return 1 + } else if (bytes[i].toInt() and 0xFF < other.bytes[i].toInt() and 0xFF) { + return -1 + } + } + + return 0 + } + + companion object { + /** + * Constructs [ByteKey] without any copying of provided array + */ + @JvmStatic + fun wrap(bytes: ByteArray): ByteKey { + return ByteKey(bytes, null) + } + + @JvmStatic + fun read(stream: InputStream): ByteKey { + return stream.readByteKey() + } + + @JvmStatic + fun readRaw(stream: InputStream, size: Int): ByteKey { + return stream.readByteKeyRaw(size) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/Streams.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/Streams.kt index c7bfe473..4bf9a4d1 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/io/Streams.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/Streams.kt @@ -10,6 +10,7 @@ import ru.dbotthepony.kommons.io.readFloat import ru.dbotthepony.kommons.io.readInt import ru.dbotthepony.kommons.io.readSignedVarInt import ru.dbotthepony.kommons.io.readSignedVarLong +import ru.dbotthepony.kommons.io.readVarInt import ru.dbotthepony.kommons.io.writeBinaryString import ru.dbotthepony.kommons.io.writeByteArray import ru.dbotthepony.kommons.io.writeDouble @@ -322,3 +323,19 @@ fun InputStream.readDouble(precision: Double, isLegacy: Boolean): Double { return readDouble() } } + +fun InputStream.readByteKey(): ByteKey { + return ByteKey(*ByteArray(readVarInt()).also { read(it) }) +} + +fun InputStream.readByteKeyRaw(size: Int): ByteKey { + return ByteKey(*ByteArray(size).also { read(it) }) +} + +fun OutputStream.writeByteKey(key: ByteKey) { + key.write(this) +} + +fun OutputStream.writeRawByteKey(key: ByteKey) { + key.writeRaw(this) +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemStack.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemStack.kt index 9bf22988..8f87bdb1 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemStack.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemStack.kt @@ -58,7 +58,7 @@ import kotlin.properties.Delegates @JsonAdapter(ItemStack.Adapter::class) open class ItemStack(val entry: ItemRegistry.Entry, val config: JsonObject, parameters: JsonObject, size: Long) { /** - * unique number utilized to determine whenever stack has changed + * Monotonically increasing global number utilized to determine whenever stack has changed */ var changeset: Long = CHANGESET.incrementAndGet() private set diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/item/RecipeRegistry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/item/RecipeRegistry.kt index ecc2aa2a..491d780a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/item/RecipeRegistry.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/item/RecipeRegistry.kt @@ -32,40 +32,6 @@ object RecipeRegistry { val output2recipes: Map> = Collections.unmodifiableMap(output2recipesBacking) val input2recipes: Map> = Collections.unmodifiableMap(input2recipesBacking) - private val tasks = ConcurrentLinkedQueue() - - private fun add(recipe: Entry) { - val value = recipe.value - recipesInternal.add(recipe) - - for (group in value.groups) { - group2recipesInternal.computeIfAbsent(group, Object2ObjectFunction { p -> - ArrayList(1).also { - group2recipesBacking[p as String] = Collections.unmodifiableList(it) - } - }).add(recipe) - } - - output2recipesInternal.computeIfAbsent(value.output.name, Object2ObjectFunction { p -> - ArrayList(1).also { - output2recipesBacking[p as String] = Collections.unmodifiableList(it) - } - }).add(recipe) - - for (input in value.input) { - input2recipesInternal.computeIfAbsent(input.name, Object2ObjectFunction { p -> - ArrayList(1).also { - input2recipesBacking[p as String] = Collections.unmodifiableList(it) - } - }).add(recipe) - } - } - - fun finishLoad() { - tasks.forEach { add(it) } - tasks.clear() - } - fun load(fileTree: Map>, patchTree: Map>): List> { val files = fileTree["recipe"] ?: return emptyList() @@ -75,9 +41,34 @@ object RecipeRegistry { Starbound.EXECUTOR.submit { try { val json = JsonPatch.apply(Starbound.ELEMENTS_ADAPTER.read(listedFile.jsonReader()), patchTree[listedFile.computeFullPath()]) - val value = recipes.fromJsonTree(json) - tasks.add(Entry(value, json, listedFile)) + val recipe = Entry(value, json, listedFile) + + Starbound.submit { + recipesInternal.add(recipe) + + for (group in value.groups) { + group2recipesInternal.computeIfAbsent(group, Object2ObjectFunction { p -> + ArrayList(1).also { + group2recipesBacking[p as String] = Collections.unmodifiableList(it) + } + }).add(recipe) + } + + output2recipesInternal.computeIfAbsent(value.output.name, Object2ObjectFunction { p -> + ArrayList(1).also { + output2recipesBacking[p as String] = Collections.unmodifiableList(it) + } + }).add(recipe) + + for (input in value.input) { + input2recipesInternal.computeIfAbsent(input.name, Object2ObjectFunction { p -> + ArrayList(1).also { + input2recipesBacking[p as String] = Collections.unmodifiableList(it) + } + }).add(recipe) + } + } } catch (err: Throwable) { LOGGER.error("Loading recipe definition file $listedFile", err) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Conversions.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Conversions.kt index ee3055aa..fad09907 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Conversions.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Conversions.kt @@ -35,6 +35,7 @@ import ru.dbotthepony.kstarbound.world.physics.Poly fun ExecutionContext.toVector2i(table: Any): Vector2i { val x = indexNoYield(table, 1L) val y = indexNoYield(table, 2L) + returnBuffer.setTo() if (x !is Number) throw ClassCastException("Expected table representing a vector, but value at [1] is not a number: $x") if (y !is Number) throw ClassCastException("Expected table representing a vector, but value at [2] is not a number: $y") @@ -45,6 +46,7 @@ fun ExecutionContext.toVector2i(table: Any): Vector2i { fun ExecutionContext.toVector2d(table: Any): Vector2d { val x = indexNoYield(table, 1L) val y = indexNoYield(table, 2L) + returnBuffer.setTo() if (x !is Number) throw ClassCastException("Expected table representing a vector, but value at [1] is not a number: $x") if (y !is Number) throw ClassCastException("Expected table representing a vector, but value at [2] is not a number: $y") @@ -58,6 +60,10 @@ fun ExecutionContext.toLine2d(table: Any): Line2d { return Line2d(p0, p1) } +fun String?.toByteString(): ByteString? { + return if (this == null) null else ByteString.of(this) +} + fun ExecutionContext.toPoly(table: Table): Poly { val vertices = ArrayList() @@ -71,6 +77,7 @@ fun ExecutionContext.toPoly(table: Table): Poly { fun ExecutionContext.toVector2f(table: Any): Vector2f { val x = indexNoYield(table, 1L) val y = indexNoYield(table, 2L) + returnBuffer.setTo() if (x !is Number) throw ClassCastException("Expected table representing a vector, but value at [1] is not a number: $x") if (y !is Number) throw ClassCastException("Expected table representing a vector, but value at [2] is not a number: $y") @@ -83,6 +90,7 @@ fun ExecutionContext.toColor(table: Any): RGBAColor { val y = indexNoYield(table, 2L) val z = indexNoYield(table, 3L) val w = indexNoYield(table, 4L) ?: 255 + returnBuffer.setTo() if (x !is Number) throw ClassCastException("Expected table representing a Color, but value at [1] is not a number: $x") if (y !is Number) throw ClassCastException("Expected table representing a Color, but value at [2] is not a number: $y") @@ -97,6 +105,7 @@ fun ExecutionContext.toAABB(table: Any): AABB { val y = indexNoYield(table, 2L) val z = indexNoYield(table, 3L) val w = indexNoYield(table, 4L) + returnBuffer.setTo() if (x !is Number) throw ClassCastException("Expected table representing a AABB, but value at [1] is not a number: $x") if (y !is Number) throw ClassCastException("Expected table representing a AABB, but value at [2] is not a number: $y") @@ -111,6 +120,7 @@ fun ExecutionContext.toAABBi(table: Any): AABBi { val y = indexNoYield(table, 2L) val z = indexNoYield(table, 3L) val w = indexNoYield(table, 4L) + returnBuffer.setTo() if (x !is Number) throw ClassCastException("Expected table representing a AABBi, but value at [1] is not a number: $x") if (y !is Number) throw ClassCastException("Expected table representing a AABBi, but value at [2] is not a number: $y") @@ -321,6 +331,22 @@ fun TableFactory.from(value: JsonObject): Table { return data } +fun TableFactory.from(value: Int?): Long? { + return value?.toLong() +} + +fun TableFactory.from(value: Long?): Long? { + return value +} + +fun TableFactory.from(value: String?): ByteString? { + return value.toByteString() +} + +fun TableFactory.from(value: ByteString?): ByteString? { + return value +} + fun TableFactory.from(value: JsonArray): Table { val (_, nils, data) = createJsonTable(LUA_HINT_ARRAY, 0, value.size()) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaEnvironment.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaEnvironment.kt index 5bbb3617..fdf17c7c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaEnvironment.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaEnvironment.kt @@ -12,6 +12,8 @@ import org.classdump.luna.Variable import org.classdump.luna.compiler.CompilerChunkLoader import org.classdump.luna.compiler.CompilerSettings import org.classdump.luna.env.RuntimeEnvironments +import org.classdump.luna.exec.CallPausedException +import org.classdump.luna.exec.Continuation import org.classdump.luna.exec.DirectCallExecutor import org.classdump.luna.impl.DefaultTable import org.classdump.luna.impl.NonsuspendableFunctionException @@ -24,6 +26,7 @@ import org.classdump.luna.lib.TableLib import org.classdump.luna.lib.Utf8Lib import org.classdump.luna.load.ChunkFactory import org.classdump.luna.runtime.AbstractFunction1 +import org.classdump.luna.runtime.Coroutine import org.classdump.luna.runtime.ExecutionContext import org.classdump.luna.runtime.LuaFunction import ru.dbotthepony.kstarbound.Starbound @@ -158,8 +161,6 @@ class LuaEnvironment : StateContext { globals["print"] = PrintFunction(globals) - globals["require"] = LuaRequire() - // why not use _ENV anyway lol globals["self"] = newTable() @@ -212,6 +213,7 @@ class LuaEnvironment : StateContext { private val scripts = ObjectArraySet() private var initCalled = false private val loadedScripts = ObjectArraySet() + val require = LuaRequire() inner class LuaRequire : AbstractFunction1() { override fun resume(context: ExecutionContext?, suspendedState: Any?) { @@ -228,13 +230,26 @@ class LuaEnvironment : StateContext { } } + init { + globals["require"] = require + } + fun attach(script: ChunkFactory) { scripts.add(script) } fun attach(scripts: Collection) { - for (script in scripts) { - attach(Starbound.loadScript(script.fullPath)) + if (initCalled) { + for (name in scripts) { + if (loadedScripts.add(name.fullPath)) { + val script = Starbound.loadScript(name.fullPath) + executor.call(this@LuaEnvironment, script.newInstance(Variable(globals))) + } + } + } else { + for (script in scripts) { + attach(Starbound.loadScript(script.fullPath)) + } } } @@ -249,6 +264,8 @@ class LuaEnvironment : StateContext { errorState = true } + fun call(fn: Any, vararg args: Any?) = executor.call(this, fn, *args) + fun init(callInit: Boolean = true): Boolean { check(!initCalled) { "Already called init()" } initCalled = true diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaMessageHandlerComponent.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaMessageHandlerComponent.kt index ad315fde..82727167 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaMessageHandlerComponent.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaMessageHandlerComponent.kt @@ -30,15 +30,15 @@ class LuaMessageHandlerComponent(val lua: LuaEnvironment, val nameProvider: () - private val logPacer = ActionPacer(1, 5) - fun handle(message: String, isLocal: Boolean, arguments: JsonArray): JsonElement { - val handler = handlers[message] ?: throw World.MessageCallException("No registered handler for $message") + fun handle(message: String, isLocal: Boolean, arguments: JsonArray): JsonElement? { + val handler = handlers[message] ?: return null try { val unpack = arguments.map { lua.from(it) }.toTypedArray() val result = lua.executor.call(lua, handler, isLocal, *unpack) if (result.isEmpty()) { - return JsonNull.INSTANCE + return null } else { return toJsonFromLua(result[0]) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaUpdateComponent.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaUpdateComponent.kt index 0b394899..1c26cda6 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaUpdateComponent.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaUpdateComponent.kt @@ -19,6 +19,19 @@ class LuaUpdateComponent(val lua: LuaEnvironment) { } } + fun update(delta: Double, preRun: () -> Unit) { + if (stepCount == 0.0) + return + + steps += delta / Starbound.TIMESTEP + + if (steps >= stepCount) { + steps %= stepCount + preRun() + lua.invokeGlobal("update", stepCount * Starbound.TIMESTEP) + } + } + fun update(delta: Double, vararg arguments: Any?) { if (stepCount == 0.0) return diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/AnimatorBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/AnimatorBindings.kt index 8e7c4655..ce7d2a92 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/AnimatorBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/AnimatorBindings.kt @@ -13,6 +13,7 @@ import ru.dbotthepony.kstarbound.lua.luaFunction import ru.dbotthepony.kstarbound.lua.luaFunctionArray import ru.dbotthepony.kstarbound.lua.set import ru.dbotthepony.kstarbound.lua.toAABB +import ru.dbotthepony.kstarbound.lua.toByteString import ru.dbotthepony.kstarbound.lua.toColor import ru.dbotthepony.kstarbound.lua.toPoly import ru.dbotthepony.kstarbound.lua.toVector2d @@ -71,7 +72,7 @@ fun provideAnimatorBindings(self: Animator, lua: LuaEnvironment) { callbacks["rotationGroups"] = luaFunction { val groups = self.rotationGroups() val keys = newTable(groups.size, 0) - groups.withIndex().forEach { (i, v) -> keys[i + 1L] = v } + groups.withIndex().forEach { (i, v) -> keys[i + 1L] = v.toByteString() } returnBuffer.setTo(keys) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/EntityBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/EntityBindings.kt index 97169f24..47255ca0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/EntityBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/EntityBindings.kt @@ -6,26 +6,59 @@ 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.toByteString +import ru.dbotthepony.kstarbound.math.Line2d import ru.dbotthepony.kstarbound.world.entities.AbstractEntity +import ru.dbotthepony.kstarbound.world.entities.ActorEntity +import ru.dbotthepony.kstarbound.world.entities.MonsterEntity +import ru.dbotthepony.kstarbound.world.entities.player.PlayerEntity +import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject fun provideEntityBindings(self: AbstractEntity, lua: LuaEnvironment) { + if (self is WorldObject) + provideWorldObjectBindings(self, lua) + + if (self is MonsterEntity) + provideMonsterBindings(self, lua) + + if (self is ActorEntity) + provideStatusControllerBindings(self.statusController, lua) + + provideWorldBindings(self.world, lua) + val table = lua.newTable() lua.globals["entity"] = table - table["id"] = luaFunction { returnBuffer.setTo(self.entityID) } + table["id"] = luaFunction { returnBuffer.setTo(self.entityID.toLong()) } table["position"] = luaFunction { returnBuffer.setTo(from(self.position)) } - table["entityType"] = luaFunction { returnBuffer.setTo(self.type.jsonName) } - table["uniqueId"] = luaFunction { returnBuffer.setTo(self.uniqueID.get()) } + table["entityType"] = luaFunction { returnBuffer.setTo(self.type.jsonName.toByteString()) } + table["uniqueId"] = luaFunction { returnBuffer.setTo(self.uniqueID.get().toByteString()) } table["persistent"] = luaFunction { returnBuffer.setTo(self.isPersistent) } - table["entityInSight"] = luaFunction { TODO() } - table["isValidTarget"] = luaFunction { TODO() } + table["entityInSight"] = luaFunction { target: Number -> + val entity = self.world.entities[target.toInt()] ?: return@luaFunction returnBuffer.setTo(false) + returnBuffer.setTo(self.world.chunkMap.collide(Line2d(self.position, entity.position)) { it.type.isSolidCollision } == null) + } + + table["isValidTarget"] = luaFunction { target: Number -> + val entity = self.world.entities[target.toInt()] ?: return@luaFunction returnBuffer.setTo(false) + + if (!self.team.get().canDamage(entity.team.get(), self == entity)) // original engine always passes `false` to `isSelfDamage` + return@luaFunction returnBuffer.setTo(false) + + if (entity is MonsterEntity) + return@luaFunction returnBuffer.setTo(entity.isAggressive) + + // TODO: NPC handling here + + returnBuffer.setTo(entity is PlayerEntity) + } table["damageTeam"] = luaFunction { val result = newTable() - result["team"] = self.team.get().team - result["type"] = self.type.jsonName + result["team"] = self.team.get().team.toLong() + result["type"] = self.type.jsonName.toByteString() returnBuffer.setTo(result) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/MonsterBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/MonsterBindings.kt new file mode 100644 index 00000000..8d5ed346 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/MonsterBindings.kt @@ -0,0 +1,192 @@ +package ru.dbotthepony.kstarbound.lua.bindings + +import com.google.common.collect.ImmutableSet +import org.classdump.luna.ByteString +import org.classdump.luna.Table +import ru.dbotthepony.kommons.collect.collect +import ru.dbotthepony.kommons.collect.map +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.DamageSource +import ru.dbotthepony.kstarbound.defs.EntityDamageTeam +import ru.dbotthepony.kstarbound.defs.PhysicsForceRegion +import ru.dbotthepony.kstarbound.fromJsonFast +import ru.dbotthepony.kstarbound.lua.LuaEnvironment +import ru.dbotthepony.kstarbound.lua.from +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.toJson +import ru.dbotthepony.kstarbound.lua.toJsonFromLua +import ru.dbotthepony.kstarbound.lua.toVector2d +import ru.dbotthepony.kstarbound.util.SBPattern +import ru.dbotthepony.kstarbound.util.sbIntern +import ru.dbotthepony.kstarbound.util.valueOf +import ru.dbotthepony.kstarbound.world.entities.ActorEntity +import ru.dbotthepony.kstarbound.world.entities.MonsterEntity + +fun provideMonsterBindings(self: MonsterEntity, lua: LuaEnvironment) { + val config = lua.newTable() + lua.globals["config"] = config + + config["getParameter"] = createConfigBinding { key, default -> + key.find(self.variant.parameters) ?: default + } + + val callbacks = lua.newTable() + lua.globals["monster"] = callbacks + + callbacks["type"] = luaFunction { + returnBuffer.setTo(self.variant.type) + } + + callbacks["seed"] = luaFunction { + // what the fuck. + returnBuffer.setTo(self.variant.seed.toString()) + } + + callbacks["seedNumber"] = luaFunction { + returnBuffer.setTo(self.variant.seed) + } + + callbacks["uniqueParameters"] = luaFunction { + returnBuffer.setTo(from(self.variant.uniqueParameters)) + } + + callbacks["level"] = luaFunction { + // TODO: this makes half sense + returnBuffer.setTo(self.monsterLevel ?: 0.0) + } + + callbacks["setDamageOnTouch"] = luaFunction { damage: Boolean -> + self.damageOnTouch = damage + } + + callbacks["setDamageSources"] = luaFunction { sources: Table? -> + self.customDamageSources.clear() + + if (sources != null) { + for ((_, v) in sources) { + self.customDamageSources.add(Starbound.gson.fromJson((v as Table).toJson(), DamageSource::class.java)) + } + } + } + + callbacks["setDamageParts"] = luaFunction { parts: Table? -> + if (parts == null) { + self.animationDamageParts.clear() + } else { + val strings = parts.iterator().map { (_, v) -> v.toString() }.collect(ImmutableSet.toImmutableSet()) + self.animationDamageParts.removeIf { it !in strings } + + for (v in strings) { + if (v !in self.animationDamageParts) { + self.animationDamageParts.add(v) + } + } + } + } + + callbacks["setAggressive"] = luaFunction { isAggressive: Boolean -> + self.isAggressive = isAggressive + } + + callbacks["setActiveSkillName"] = luaFunction { name: ByteString -> + self.activeSkillName = name.decode().sbIntern() + } + + callbacks["setDropPool"] = luaFunction { pool: Any -> + self.dropPool = Starbound.gson.fromJsonFast(toJsonFromLua(pool)) + } + + callbacks["toAbsolutePosition"] = luaFunction { position: Table -> + returnBuffer.setTo(from(self.movement.getAbsolutePosition(toVector2d(position)))) + } + + callbacks["mouthPosition"] = luaFunction { + returnBuffer.setTo(from(self.mouthPosition)) + } + + // This callback is registered here rather than in + // makeActorMovementControllerCallbacks + // because it requires access to world + callbacks["flyTo"] = luaFunction { position: Table -> + self.movement.controlFly = self.world.geometry.diff(toVector2d(position), self.movement.position) + } + + callbacks["setDeathParticleBurst"] = luaFunction { value: ByteString? -> + self.deathParticlesBurst = value?.decode()?.sbIntern() ?: "" + } + + callbacks["setDeathSound"] = luaFunction { value: ByteString? -> + self.deathSound = value?.decode()?.sbIntern() ?: "" + } + + callbacks["setPhysicsForces"] = luaFunction { forces: Table? -> + if (forces == null) { + self.forceRegions.clear() + } else { + self.forceRegions.clear() + + for ((_, v) in forces) { + self.forceRegions.add(Starbound.gson.fromJsonFast(toJsonFromLua(v), PhysicsForceRegion::class.java)) + } + } + } + + callbacks["setName"] = luaFunction { name: ByteString? -> + self.networkName = name?.decode() + } + + callbacks["setDisplayNametag"] = luaFunction { shouldDisplay: Boolean -> + self.displayNameTag = shouldDisplay + } + + callbacks["say"] = luaFunction { line: ByteString, tags: Table? -> + var actualLine = line.decode() + + if (tags != null) { + actualLine = SBPattern.of(actualLine).resolveOrSkip({ tags[it]?.toString() }) + } + + if (actualLine.isNotBlank()) { + self.addChatMessage(actualLine.sbIntern()) + } + + returnBuffer.setTo(actualLine.isNotBlank()) + } + + callbacks["sayPortrait"] = luaFunction { line: ByteString, portrait: ByteString, tags: Table? -> + var actualLine = line.decode() + + if (tags != null) { + actualLine = SBPattern.of(actualLine).resolveOrSkip({ tags[it]?.toString() }) + } + + if (actualLine.isNotBlank()) { + self.addChatMessage(actualLine.sbIntern(), portrait.decode().sbIntern()) + } + + returnBuffer.setTo(actualLine.isNotBlank()) + } + + callbacks["setDamageTeam"] = luaFunction { team: Any -> + self.team.accept(Starbound.gson.fromJsonFast(toJsonFromLua(team), EntityDamageTeam::class.java)) + } + + callbacks["setUniqueId"] = luaFunction { name: ByteString? -> + self.uniqueID.accept(name?.decode()?.sbIntern()) + } + + callbacks["setDamageBar"] = luaFunction { type: ByteString -> + self.damageBarType = ActorEntity.DamageBarType.entries.valueOf(type.decode()) + } + + callbacks["setInteractive"] = luaFunction { isInteractive: Boolean -> + self.isInteractive = isInteractive + } + + callbacks["setAnimationParameter"] = luaFunction { name: ByteString, value: Any? -> + self.scriptedAnimationParameters[name.decode().sbIntern()] = toJsonFromLua(value) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/MovementControllerBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/MovementControllerBindings.kt new file mode 100644 index 00000000..f55cf474 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/MovementControllerBindings.kt @@ -0,0 +1,328 @@ +package ru.dbotthepony.kstarbound.lua.bindings + +import org.classdump.luna.Table +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.actor.ActorMovementModifiers +import ru.dbotthepony.kstarbound.defs.ActorMovementParameters +import ru.dbotthepony.kstarbound.fromJsonFast +import ru.dbotthepony.kstarbound.lua.LuaEnvironment +import ru.dbotthepony.kstarbound.lua.from +import ru.dbotthepony.kstarbound.lua.luaFunction +import ru.dbotthepony.kstarbound.lua.set +import ru.dbotthepony.kstarbound.lua.tableFrom +import ru.dbotthepony.kstarbound.lua.tableOf +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.math.vector.Vector2d +import ru.dbotthepony.kstarbound.world.Direction +import ru.dbotthepony.kstarbound.world.entities.ActorMovementController +import ru.dbotthepony.kstarbound.world.entities.AnchorState +import ru.dbotthepony.kstarbound.world.physics.Poly +import kotlin.math.PI + +class MovementControllerBindings(val self: ActorMovementController) { + fun init(lua: LuaEnvironment) { + val callbacks = lua.newTable() + lua.globals["mcontroller"] = callbacks + + // pass-through + callbacks["mass"] = luaFunction { + returnBuffer.setTo(self.mass) + } + + callbacks["localBoundBox"] = luaFunction { + returnBuffer.setTo(from(self.computeLocalCollisionAABB())) + } + + callbacks["boundBox"] = luaFunction { + returnBuffer.setTo(from(self.computeLocalCollisionAABB())) + } + + callbacks["collisionBoundBox"] = luaFunction { + returnBuffer.setTo(from(self.computeGlobalCollisionAABB())) + } + + callbacks["collisionPoly"] = luaFunction { + returnBuffer.setTo(from(self.movementParameters.collisionPoly?.map({ it }, { it.firstOrNull() }) ?: Poly.EMPTY)) + } + + callbacks["collisionPolies"] = luaFunction { + returnBuffer.setTo(tableFrom((self.movementParameters.collisionPoly?.map({ listOf(it) }, { it }) ?: listOf(Poly.EMPTY)).map { from(it) })) + } + + callbacks["collisionBody"] = luaFunction { + returnBuffer.setTo(from(self.computeGlobalHitboxes().firstOrNull() ?: Poly.EMPTY)) + } + + callbacks["collisionBodies"] = luaFunction { + returnBuffer.setTo(tableFrom(self.computeGlobalHitboxes().map { from(it) })) + } + + callbacks["position"] = luaFunction { returnBuffer.setTo(tableOf(self.xPosition, self.yPosition)) } + callbacks["xPosition"] = luaFunction { returnBuffer.setTo(self.xPosition) } + callbacks["yPosition"] = luaFunction { returnBuffer.setTo(self.yPosition) } + callbacks["velocity"] = luaFunction { returnBuffer.setTo(tableOf(self.xVelocity, self.yVelocity)) } + callbacks["xVelocity"] = luaFunction { returnBuffer.setTo(self.xVelocity) } + callbacks["yVelocity"] = luaFunction { returnBuffer.setTo(self.yVelocity) } + callbacks["rotation"] = luaFunction { returnBuffer.setTo(self.rotation) } + callbacks["isColliding"] = luaFunction { returnBuffer.setTo(self.isColliding) } + callbacks["isNullColliding"] = luaFunction { returnBuffer.setTo(self.isCollidingWithNull) } + callbacks["isCollisionStuck"] = luaFunction { returnBuffer.setTo(self.isCollisionStuck) } + callbacks["stickingDirection"] = luaFunction { returnBuffer.setTo(self.stickingDirection) } + callbacks["liquidPercentage"] = luaFunction { returnBuffer.setTo(self.liquidPercentage) } + callbacks["liquidId"] = luaFunction { returnBuffer.setTo(self.liquid?.id ?: 0) } + callbacks["liquidName"] = luaFunction { returnBuffer.setTo(self.liquid?.key.toByteString()) } + callbacks["onGround"] = luaFunction { returnBuffer.setTo(self.isOnGround) } + callbacks["zeroG"] = luaFunction { returnBuffer.setTo(self.isZeroGravity) } + + callbacks["atWorldLimit"] = luaFunction { bottomOnly: Boolean -> + returnBuffer.setTo(self.isAtWorldLimit(bottomOnly)) + } + + callbacks["setAnchorState"] = luaFunction { anchor: Number, index: Number -> + self.anchorState = AnchorState(anchor.toInt(), index.toInt()) + } + + callbacks["resetAnchorState"] = luaFunction { + self.anchorState = null + } + + callbacks["anchorState"] = luaFunction { + val anchorState = self.anchorState + + if (anchorState != null) { + returnBuffer.setTo(anchorState.entityID, anchorState.positionIndex) + } + } + + callbacks["setPosition"] = luaFunction { value: Table -> + resetPathMove = true + self.position = toVector2d(value) + } + + callbacks["setXPosition"] = luaFunction { value: Number -> + resetPathMove = true + self.xPosition = value.toDouble() + } + + callbacks["setYPosition"] = luaFunction { value: Number -> + resetPathMove = true + self.yPosition = value.toDouble() + } + + callbacks["translate"] = luaFunction { value: Table -> + resetPathMove = true + self.position += toVector2d(value) + } + + callbacks["setVelocity"] = luaFunction { value: Table -> + resetPathMove = true + self.velocity = toVector2d(value) + } + + callbacks["setXVelocity"] = luaFunction { value: Number -> + resetPathMove = true + self.xVelocity = value.toDouble() + } + + callbacks["setYVelocity"] = luaFunction { value: Number -> + resetPathMove = true + self.yVelocity = value.toDouble() + } + + callbacks["addMomentum"] = luaFunction { value: Table -> + resetPathMove = true + if (self.mass != 0.0) // let's not collapse into black hole + self.velocity += toVector2d(value) / self.mass + } + + callbacks["setRotation"] = luaFunction { value: Number -> + resetPathMove = true + self.rotation = value.toDouble() + } + + callbacks["baseParameters"] = luaFunction { returnBuffer.setTo(from(Starbound.gson.toJsonTree(self.actorMovementParameters))) } + callbacks["walking"] = luaFunction { returnBuffer.setTo(self.isWalking) } + callbacks["running"] = luaFunction { returnBuffer.setTo(self.isRunning) } + callbacks["movingDirection"] = luaFunction { returnBuffer.setTo(self.movingDirection.luaValue) } + callbacks["facingDirection"] = luaFunction { returnBuffer.setTo(self.facingDirection.luaValue) } + callbacks["crouching"] = luaFunction { returnBuffer.setTo(self.isCrouching) } + callbacks["flying"] = luaFunction { returnBuffer.setTo(self.isFlying) } + callbacks["falling"] = luaFunction { returnBuffer.setTo(self.isFalling) } + callbacks["canJump"] = luaFunction { returnBuffer.setTo(self.canJump) } + callbacks["jumping"] = luaFunction { returnBuffer.setTo(self.isJumping) } + callbacks["groundMovement"] = luaFunction { returnBuffer.setTo(self.isGroundMovement) } + callbacks["liquidMovement"] = luaFunction { returnBuffer.setTo(self.isLiquidMovement) } + + // controls, stored locally, reset automatically or are persistent + callbacks["controlRotation"] = luaFunction { value: Number -> + controlRotationRate += value.toDouble() + } + + callbacks["controlAcceleration"] = luaFunction { value: Table -> + controlAcceleration += toVector2d(value) + } + + callbacks["controlForce"] = luaFunction { value: Table -> + controlForce += toVector2d(value) + } + + callbacks["controlApproachVelocity"] = luaFunction { value: Table, rate: Number -> + controlApproachVelocity = ActorMovementController.ApproachVelocityCommand(toVector2d(value), rate.toDouble()) + } + + callbacks["controlApproachVelocityAlongAngle"] = luaFunction { angle: Number, targetVelocity: Number, maxControlForce: Number, positiveOnly: Boolean -> + controlApproachVelocityAlongAngle = ActorMovementController.ApproachVelocityAngleCommand(angle.toDouble(), targetVelocity.toDouble(), maxControlForce.toDouble(), positiveOnly) + } + + callbacks["controlApproachXVelocity"] = luaFunction { targetVelocity: Number, maxControlForce: Number -> + controlApproachVelocityAlongAngle = ActorMovementController.ApproachVelocityAngleCommand(0.0, targetVelocity.toDouble(), maxControlForce.toDouble(), false) + } + + callbacks["controlApproachYVelocity"] = luaFunction { targetVelocity: Number, maxControlForce: Number -> + controlApproachVelocityAlongAngle = ActorMovementController.ApproachVelocityAngleCommand(PI / 2.0, targetVelocity.toDouble(), maxControlForce.toDouble(), false) + } + + callbacks["controlParameters"] = luaFunction { data: Table -> + controlParameters = controlParameters.merge(Starbound.gson.fromJsonFast(data.toJson(true), ActorMovementParameters::class.java)) + } + + callbacks["controlModifiers"] = luaFunction { data: Table -> + controlModifiers = controlModifiers.merge(Starbound.gson.fromJsonFast(data.toJson(true), ActorMovementModifiers::class.java)) + } + + callbacks["controlMove"] = luaFunction { direction: Number, shouldRun: Boolean? -> + // why? + val new = Direction.valueOf(direction) + + if (new != null) { + controlMove = new + controlShouldRun = shouldRun ?: false + } + } + + callbacks["controlFace"] = luaFunction { direction: Number -> + // why? + val new = Direction.valueOf(direction) + + if (new != null) + controlFace = new + } + + callbacks["controlDown"] = luaFunction { controlDown = true } + callbacks["controlCrouch"] = luaFunction { controlCrouch = true } + callbacks["controlJump"] = luaFunction { should: Boolean -> controlJump = should } + callbacks["controlHoldJump"] = luaFunction { controlHoldJump = true } + callbacks["controlFly"] = luaFunction { value: Table -> controlFly = toVector2d(value) } + + callbacks["controlPathMove"] = luaFunction { position: Table, run: Boolean?, parameters: Table? -> + if (pathMoveResult?.first == toVector2d(position)) { + val pathMoveResult = pathMoveResult!! + this@MovementControllerBindings.pathMoveResult = null + returnBuffer.setTo(pathMoveResult.second) + } else { + pathMoveResult = null + val result = self.pathMove(toVector2d(position), run == true, Starbound.gson.fromJsonFast(toJsonFromLua(parameters))) + + if (result == null) { + controlPathMove = toVector2d(position) to (run == true) + } + + returnBuffer.setTo(result?.second) + } + } + + callbacks["pathfinding"] = luaFunction { + returnBuffer.setTo(self.pathController.isPathfinding) + } + + callbacks["autoClearControls"] = luaFunction { + returnBuffer.setTo(autoClearControls) + } + + callbacks["setAutoClearControls"] = luaFunction { should: Boolean -> + autoClearControls = should + } + + callbacks["clearControls"] = luaFunction { clearControls() } + } + + private var autoClearControls = true + + private var controlRotationRate = 0.0 + private var controlAcceleration = Vector2d.ZERO + private var controlForce = Vector2d.ZERO + private var controlApproachVelocity: ActorMovementController.ApproachVelocityCommand? = null + private var controlApproachVelocityAlongAngle: ActorMovementController.ApproachVelocityAngleCommand? = null + private var controlParameters = ActorMovementParameters.EMPTY + private var controlModifiers = ActorMovementModifiers.EMPTY + + private var controlMove: Direction? = null + private var controlFace: Direction? = null + private var controlShouldRun: Boolean? = null + + private var controlDown = false + private var controlCrouch = false + private var controlJump = false + private var controlHoldJump = false + private var controlFly: Vector2d? = null + + private var resetPathMove = false + + private var pathMoveResult: Pair? = null + private var controlPathMove: Pair? = null + + fun apply() { + self.controlRotationRate += controlRotationRate + self.controlAcceleration += controlAcceleration + self.controlForce += controlForce + + if (controlApproachVelocity != null) self.approachVelocities.add(controlApproachVelocity!!) + if (controlApproachVelocityAlongAngle != null) self.approachVelocityAngles.add(controlApproachVelocityAlongAngle!!) + self.controlActorMovementParameters = self.controlActorMovementParameters.merge(controlParameters) + self.controlMovementModifiers = self.controlMovementModifiers.merge(controlModifiers) + if (controlMove != null) self.controlMove = controlMove + if (controlShouldRun != null) self.controlRun = controlShouldRun!! + if (controlFace != null) self.controlFace = controlFace + if (controlDown) self.controlDown = true + if (controlCrouch) self.controlCrouch = true + if (controlJump) self.controlJump = true + if (controlHoldJump && !self.isOnGround) self.controlJump = true + if (controlFly != null) self.controlFly = controlFly + + // some action was taken that has priority over pathing, setting position or velocity + if (resetPathMove) + controlPathMove = null + + if (controlPathMove != null && pathMoveResult == null) + pathMoveResult = self.controlPathMove(controlPathMove!!.first, controlPathMove!!.second) + } + + fun clearControlsIfNeeded() { + if (autoClearControls) + clearControls() + } + + private fun clearControls() { + controlRotationRate = 0.0 + controlAcceleration = Vector2d.ZERO + controlForce = Vector2d.ZERO + controlApproachVelocity = null + controlApproachVelocityAlongAngle = null + controlParameters = ActorMovementParameters.EMPTY + controlModifiers = ActorMovementModifiers.EMPTY + controlMove = null + controlFace = null + controlShouldRun = null + controlDown = false + controlCrouch = false + controlJump = false + controlHoldJump = false + controlFly = null + pathMoveResult = null + controlPathMove = null + resetPathMove = false + } +} 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 dd278e8d..b53699a1 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/RootBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/RootBindings.kt @@ -37,6 +37,7 @@ import ru.dbotthepony.kstarbound.lua.nextOptionalInteger import ru.dbotthepony.kstarbound.lua.set import ru.dbotthepony.kstarbound.lua.tableMapOf import ru.dbotthepony.kstarbound.lua.tableOf +import ru.dbotthepony.kstarbound.lua.toByteString import ru.dbotthepony.kstarbound.lua.toJsonFromLua import ru.dbotthepony.kstarbound.lua.toLuaInteger import ru.dbotthepony.kstarbound.util.random.random @@ -125,7 +126,7 @@ private fun registryDef2(registry: Registry<*>): LuaFunction { if (def != null) { returnBuffer.setTo(newTable(0, 2).also { - it["path"] = def.file?.computeFullPath() + it["path"] = def.file?.computeFullPath().toByteString() it["config"] = from(def.json) }) } else { @@ -144,7 +145,7 @@ private val recipesForItem = luaFunction { name: ByteString -> val list = RecipeRegistry.output2recipes[name.decode()] if (list == null) { - returnBuffer.setTo(newTable()) + returnBuffer.setTo(tableOf()) } else { returnBuffer.setTo(newTable(list.size, 0).also { for ((i, v) in list.withIndex()) { @@ -192,17 +193,17 @@ private fun materialMiningSound(context: ExecutionContext, arguments: ArgumentIt val mod = lookup(Registries.tiles, arguments.nextOptionalAny(null)) if (mod != null && mod.value.miningSounds.map({ it.isNotEmpty() }, { true })) { - context.returnBuffer.setTo(mod.value.miningSounds.map({ it.random() }, { it })) + context.returnBuffer.setTo(mod.value.miningSounds.map({ it.random() }, { it }).toByteString()) return } if (tile != null && tile.value.miningSounds.map({ it.isNotEmpty() }, { true })) { - context.returnBuffer.setTo(tile.value.miningSounds.map({ it.random() }, { it })) + context.returnBuffer.setTo(tile.value.miningSounds.map({ it.random() }, { it }).toByteString()) return } // original engine parity - context.returnBuffer.setTo("") + context.returnBuffer.setTo("".toByteString()) } private fun materialFootstepSound(context: ExecutionContext, arguments: ArgumentIterator) { @@ -210,16 +211,16 @@ private fun materialFootstepSound(context: ExecutionContext, arguments: Argument val mod = lookup(Registries.tiles, arguments.nextOptionalAny(null)) if (mod != null && mod.value.footstepSound.map({ it.isNotEmpty() }, { true })) { - context.returnBuffer.setTo(mod.value.footstepSound.map({ it.random() }, { it })) + context.returnBuffer.setTo(mod.value.footstepSound.map({ it.random() }, { it }).toByteString()) return } if (tile != null && tile.value.footstepSound.map({ it.isNotEmpty() }, { true })) { - context.returnBuffer.setTo(tile.value.footstepSound.map({ it.random() }, { it })) + context.returnBuffer.setTo(tile.value.footstepSound.map({ it.random() }, { it }).toByteString()) return } - context.returnBuffer.setTo(Globals.client.defaultFootstepSound.map({ it }, { it.random() })) + context.returnBuffer.setTo(Globals.client.defaultFootstepSound.map({ it }, { it.random() }).toByteString()) } private val materialHealth = luaFunction { id: Any -> @@ -227,7 +228,7 @@ private val materialHealth = luaFunction { id: Any -> } private val liquidName = luaFunction { id: Any -> - returnBuffer.setTo(lookupStrict(Registries.liquid, id).key) + returnBuffer.setTo(lookupStrict(Registries.liquid, id).key.toByteString()) } private val liquidId = luaFunction { id: Any -> @@ -235,11 +236,11 @@ private val liquidId = luaFunction { id: Any -> } private val techType = luaFunction { id: Any -> - returnBuffer.setTo(lookupStrict(Registries.techs, id).value.type) + returnBuffer.setTo(lookupStrict(Registries.techs, id).value.type.toByteString()) } private val techConfig = luaFunction { id: Any -> - returnBuffer.setTo(lookupStrict(Registries.techs, id).json) + returnBuffer.setTo(from(lookupStrict(Registries.techs, id).json)) } private val jobject = luaFunction { returnBuffer.setTo(createJsonObject()) } @@ -362,11 +363,11 @@ private val createBiome = luaFunction { name: ByteString, seed: Number, vertical } private val treeStemDirectory = luaFunction { name: ByteString -> - returnBuffer.setTo(Registries.treeStemVariants[name.decode()]?.file?.computeDirectory(true) ?: "/") + returnBuffer.setTo(Registries.treeStemVariants[name.decode()]?.file?.computeDirectory(true).toByteString() ?: "/".toByteString()) } private val treeFoliageDirectory = luaFunction { name: ByteString -> - returnBuffer.setTo(Registries.treeFoliageVariants[name.decode()]?.file?.computeDirectory(true) ?: "/") + returnBuffer.setTo(Registries.treeFoliageVariants[name.decode()]?.file?.computeDirectory(true).toByteString() ?: "/".toByteString()) } private val itemConfig = luaFunction { descriptor: Any, level: Number?, seed: Number? -> @@ -403,7 +404,7 @@ private val createItem = luaFunction { descriptor: Any, level: Number?, seed: Nu } private val itemType = luaFunction { identifier: ByteString -> - returnBuffer.setTo(ItemRegistry[identifier.decode()].type.jsonName) + returnBuffer.setTo(ItemRegistry[identifier.decode()].type.jsonName.toByteString()) } private val itemTags = luaFunction { identifier: ByteString -> diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/ServerWorldBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/ServerWorldBindings.kt index f676154f..03115f01 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/ServerWorldBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/ServerWorldBindings.kt @@ -23,6 +23,7 @@ import ru.dbotthepony.kstarbound.lua.set import ru.dbotthepony.kstarbound.lua.tableOf import ru.dbotthepony.kstarbound.lua.toAABB import ru.dbotthepony.kstarbound.lua.toAABBi +import ru.dbotthepony.kstarbound.lua.toByteString import ru.dbotthepony.kstarbound.lua.toJsonFromLua import ru.dbotthepony.kstarbound.lua.toVector2d import ru.dbotthepony.kstarbound.lua.toVector2i @@ -234,7 +235,7 @@ fun provideServerWorldBindings(self: ServerWorld, callbacks: Table, lua: LuaEnvi } callbacks["fidelity"] = luaFunction { - returnBuffer.setTo("high") + returnBuffer.setTo("high".toByteString()) } callbacks["setSkyTime"] = luaFunction { newTime: Number -> @@ -247,7 +248,7 @@ fun provideServerWorldBindings(self: ServerWorld, callbacks: Table, lua: LuaEnvi } callbacks["setUniverseFlag"] = luaFunction { flag: ByteString -> - returnBuffer.setTo(self.server.addUniverseFlag(flag.decode())) + returnBuffer.setTo(self.server.addUniverseFlag(flag.decode().sbIntern())) } callbacks["unsetUniverseFlag"] = luaFunction { flag: ByteString -> diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/StatusControllerBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/StatusControllerBindings.kt new file mode 100644 index 00000000..b0774c42 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/StatusControllerBindings.kt @@ -0,0 +1,251 @@ +package ru.dbotthepony.kstarbound.lua.bindings + +import com.google.gson.JsonObject +import com.google.gson.reflect.TypeToken +import org.classdump.luna.ByteString +import org.classdump.luna.LuaRuntimeException +import org.classdump.luna.Table +import ru.dbotthepony.kommons.gson.set +import ru.dbotthepony.kstarbound.Registries +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.DamageData +import ru.dbotthepony.kstarbound.defs.EphemeralStatusEffect +import ru.dbotthepony.kstarbound.defs.actor.PersistentStatusEffect +import ru.dbotthepony.kstarbound.fromJsonFast +import ru.dbotthepony.kstarbound.lua.LuaEnvironment +import ru.dbotthepony.kstarbound.lua.from +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.tableOf +import ru.dbotthepony.kstarbound.lua.toByteString +import ru.dbotthepony.kstarbound.lua.toJsonFromLua +import ru.dbotthepony.kstarbound.util.sbIntern +import ru.dbotthepony.kstarbound.world.entities.StatusController +import kotlin.math.absoluteValue + +private object PersistentStatusEffectToken : TypeToken() +private object CPersistentStatusEffectToken : TypeToken>() + +fun provideStatusControllerBindings(self: StatusController, lua: LuaEnvironment) { + val callbacks = lua.newTable() + lua.globals["status"] = callbacks + + callbacks["statusProperty"] = luaFunction { key: ByteString, ifMissing: Any? -> + val get = self.getProperty(key.decode()) + + if (get == null) { + returnBuffer.setTo(ifMissing) + } else { + returnBuffer.setTo(from(get)) + } + } + + callbacks["setStatusProperty"] = luaFunction { key: ByteString, value: Any? -> + self.setProperty(key.decode().sbIntern(), toJsonFromLua(value)) + } + + callbacks["stat"] = luaFunction { name: ByteString -> + returnBuffer.setTo(self.liveStats[name.decode()]?.effectiveModifiedValue ?: 0.0) + } + + callbacks["statPositive"] = luaFunction { name: ByteString -> + returnBuffer.setTo(self.statPositive(name.decode())) + } + + callbacks["resourceNames"] = luaFunction { + returnBuffer.setTo(tableOf(self.resources.keys.toTypedArray())) + } + + callbacks["isResource"] = luaFunction { name: ByteString -> + returnBuffer.setTo(name.decode() in self.resources.keys) + } + + callbacks["resource"] = luaFunction { name: ByteString -> + val resource = self.resources[name.decode()] ?: throw LuaRuntimeException("No such resource $name") + returnBuffer.setTo(resource.value) + } + + callbacks["resourcePositive"] = luaFunction { name: ByteString -> + val resource = self.resources[name.decode()] ?: throw LuaRuntimeException("No such resource $name") + returnBuffer.setTo(resource.value > 0.0) + } + + callbacks["setResource"] = luaFunction { name: ByteString, value: Number -> + val resource = self.resources[name.decode()] ?: throw LuaRuntimeException("No such resource $name") + resource.value = value.toDouble() + } + + callbacks["modifyResource"] = luaFunction { name: ByteString, value: Number -> + val resource = self.resources[name.decode()] ?: throw LuaRuntimeException("No such resource $name") + resource.value += value.toDouble() + } + + callbacks["giveResource"] = luaFunction { name: ByteString, value: Number -> + // while other functions throw an exception if resource does not exist + // this one returns 0 + // Consistency is my first, second, and last name + val resource = self.resources[name.decode()] ?: return@luaFunction returnBuffer.setTo(0.0) + returnBuffer.setTo(resource.give(value.toDouble())) + } + + callbacks["consumeResource"] = luaFunction { name: ByteString, value: Number -> + // while other functions throw an exception if resource does not exist + // this one returns 0 + // Consistency is my first, second, and last name + val resource = self.resources[name.decode()] ?: return@luaFunction returnBuffer.setTo(false) + returnBuffer.setTo(resource.consume(value.toDouble())) + } + + callbacks["overConsumeResource"] = luaFunction { name: ByteString, value: Number -> + // while other functions throw an exception if resource does not exist + // this one returns 0 + // Consistency is my first, second, and last name + val resource = self.resources[name.decode()] ?: return@luaFunction returnBuffer.setTo(false) + returnBuffer.setTo(resource.consume(value.toDouble(), allowOverdraw = true)) + } + + callbacks["resourceLocked"] = luaFunction { name: ByteString -> + val resource = self.resources[name.decode()] ?: throw LuaRuntimeException("No such resource $name") + returnBuffer.setTo(resource.isLocked) + } + + callbacks["setResourceLocked"] = luaFunction { name: ByteString, isLocked: Boolean -> + val resource = self.resources[name.decode()] ?: throw LuaRuntimeException("No such resource $name") + resource.isLocked = isLocked + } + + callbacks["resetResource"] = luaFunction { name: ByteString, isLocked: Boolean -> + val resource = self.resources[name.decode()] ?: throw LuaRuntimeException("No such resource $name") + resource.reset() + } + + callbacks["resetAllResources"] = luaFunction { name: ByteString, isLocked: Boolean -> + self.resetResources() + } + + callbacks["resourceMax"] = luaFunction { name: ByteString -> + val resource = self.resources[name.decode()] ?: throw LuaRuntimeException("No such resource $name") + returnBuffer.setTo(resource.maxValue) + } + + callbacks["resourcePercentage"] = luaFunction { name: ByteString -> + val resource = self.resources[name.decode()] ?: throw LuaRuntimeException("No such resource $name") + returnBuffer.setTo(resource.percentage) + } + + callbacks["setResourcePercentage"] = luaFunction { name: ByteString, value: Number -> + val resource = self.resources[name.decode()] ?: throw LuaRuntimeException("No such resource $name") + resource.setAsPercentage(value.toDouble()) + } + + callbacks["modifyResourcePercentage"] = luaFunction { name: ByteString, value: Number -> + val resource = self.resources[name.decode()] ?: throw LuaRuntimeException("No such resource $name") + resource.modifyPercentage(value.toDouble()) + } + + callbacks["getPersistentEffects"] = luaFunction { category: ByteString -> + returnBuffer.setTo(tableOf(self.getPersistentEffects(category.decode()).map { it.map({ from(Starbound.gson.toJsonTree(it)) }, { it }) })) + } + + callbacks["addPersistentEffect"] = luaFunction { category: ByteString, effect: Any -> + self.addPersistentEffect(category.decode().sbIntern(), Starbound.gson.fromJsonFast(toJsonFromLua(effect), PersistentStatusEffectToken)) + } + + callbacks["addPersistentEffects"] = luaFunction { category: ByteString, effect: Any -> + self.addPersistentEffects(category.decode().sbIntern(), Starbound.gson.fromJsonFast(toJsonFromLua(effect), CPersistentStatusEffectToken)) + } + + callbacks["setPersistentEffects"] = luaFunction { category: ByteString, effect: Any -> + self.setPersistentEffects(category.decode().sbIntern(), Starbound.gson.fromJsonFast(toJsonFromLua(effect), CPersistentStatusEffectToken)) + } + + callbacks["clearPersistentEffects"] = luaFunction { category: ByteString -> + self.removePersistentEffects(category.decode()) + } + + callbacks["clearAllPersistentEffects"] = luaFunction { + self.removeAllPersistentEffects() + } + + callbacks["addEphemeralEffect"] = luaFunction { name: ByteString, duration: Number?, source: Number? -> + self.addEphemeralEffect(EphemeralStatusEffect(Registries.statusEffects.ref(name.decode().sbIntern()), duration?.toDouble()), source?.toInt()) + } + + callbacks["addEphemeralEffects"] = luaFunction { effects: Table, source: Number? -> + for ((_, effect) in effects) { + self.addEphemeralEffect(Starbound.gson.fromJsonFast(toJsonFromLua(effect), EphemeralStatusEffect::class.java), source?.toInt()) + } + } + + callbacks["removeEphemeralEffect"] = luaFunction { name: ByteString -> + self.removeEphemeralEffect(name.decode()) + } + + callbacks["clearEphemeralEffects"] = luaFunction { + self.removeEphemeralEffects() + } + + callbacks["damageTakenSince"] = luaFunction { since: Number? -> + val (list, newSince) = self.recentDamageReceived(since?.toLong() ?: 0L) + returnBuffer.setTo(tableOf(*list.map { 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 } + }.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) + } + + callbacks["activeUniqueStatusEffectSummary"] = luaFunction { + returnBuffer.setTo(tableOf(*self.activeUniqueStatusEffectSummary().map { tableOf(it.first.key, it.second) }.toTypedArray())) + } + + callbacks["uniqueStatusEffectActive"] = luaFunction { name: ByteString -> + returnBuffer.setTo(self.uniqueStatusEffectActive(name.decode())) + } + + callbacks["primaryDirectives"] = luaFunction { + returnBuffer.setTo(self.primaryDirectives.toByteString()) + } + + callbacks["setPrimaryDirectives"] = luaFunction { directives: ByteString -> + self.primaryDirectives = directives.decode().sbIntern() + } + + callbacks["applySelfDamageRequest"] = luaFunction { damage: Table -> + self.inflictSelfDamage(Starbound.gson.fromJsonFast(toJsonFromLua(damage), DamageData::class.java)) + } + + callbacks["appliesEnvironmentStatusEffects"] = luaFunction { + returnBuffer.setTo(self.appliesEnvironmentStatusEffects) + } + + callbacks["appliesWeatherStatusEffects"] = luaFunction { + returnBuffer.setTo(self.appliesWeatherStatusEffects) + } + + callbacks["minimumLiquidStatusEffectPercentage"] = luaFunction { + returnBuffer.setTo(self.minimumLiquidStatusEffectPercentage) + } + + callbacks["setAppliesEnvironmentStatusEffects"] = luaFunction { should: Boolean -> + self.appliesEnvironmentStatusEffects = should + } + + callbacks["setAppliesWeatherStatusEffects"] = luaFunction { should: Boolean -> + self.appliesWeatherStatusEffects = should + } + + callbacks["setMinimumLiquidStatusEffectPercentage"] = luaFunction { value: Number -> + self.minimumLiquidStatusEffectPercentage = value.toDouble() + } +} 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 887c8364..f427e4fa 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/UtilityBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/UtilityBindings.kt @@ -17,6 +17,7 @@ import ru.dbotthepony.kstarbound.lua.luaFunctionArray import ru.dbotthepony.kstarbound.lua.luaFunctionN 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.toVector2d import ru.dbotthepony.kstarbound.lua.userdata.LuaPerlinNoise @@ -70,7 +71,7 @@ private val interpolateSinEase = luaFunctionArray { args -> } private val replaceTags = luaFunction { string: ByteString, tags: Table -> - returnBuffer.setTo(SBPattern.of(string.toString()).resolveOrSkip({ tags[it]?.toString() })) + returnBuffer.setTo(SBPattern.of(string.toString()).resolveOrSkip({ tags[it]?.toString() }).toByteString()) } private val makePerlinSource = luaFunction { settings: Table -> @@ -102,7 +103,7 @@ fun provideUtilityBindings(lua: LuaEnvironment) { lua.globals["sb"] = table table["makeUuid"] = luaFunction { - returnBuffer.setTo(UUID(lua.random.nextLong(), lua.random.nextLong()).toStarboundString()) + returnBuffer.setTo(UUID(lua.random.nextLong(), lua.random.nextLong()).toStarboundString().toByteString()) } table["logInfo"] = logInfo 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 66087876..7325a4f8 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldBindings.kt @@ -12,12 +12,15 @@ import org.classdump.luna.runtime.LuaFunction import ru.dbotthepony.kommons.collect.map import ru.dbotthepony.kommons.collect.toList import ru.dbotthepony.kstarbound.Registries +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.ActorMovementParameters import ru.dbotthepony.kstarbound.defs.EntityType import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyLiquid import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyTile import ru.dbotthepony.kstarbound.defs.world.TerrestrialWorldParameters +import ru.dbotthepony.kstarbound.fromJsonFast import ru.dbotthepony.kstarbound.json.builder.IStringSerializable import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.lua.LuaEnvironment @@ -33,6 +36,7 @@ import ru.dbotthepony.kstarbound.lua.nextOptionalInteger import ru.dbotthepony.kstarbound.lua.set import ru.dbotthepony.kstarbound.lua.tableOf import ru.dbotthepony.kstarbound.lua.toAABB +import ru.dbotthepony.kstarbound.lua.toByteString import ru.dbotthepony.kstarbound.lua.toJson import ru.dbotthepony.kstarbound.lua.toJsonFromLua import ru.dbotthepony.kstarbound.lua.toLine2d @@ -41,9 +45,11 @@ import ru.dbotthepony.kstarbound.lua.toVector2d import ru.dbotthepony.kstarbound.lua.toVector2i import ru.dbotthepony.kstarbound.lua.unpackAsArray import ru.dbotthepony.kstarbound.lua.userdata.LuaFuture +import ru.dbotthepony.kstarbound.lua.userdata.LuaPathFinder import ru.dbotthepony.kstarbound.math.AABB import ru.dbotthepony.kstarbound.math.Line2d import ru.dbotthepony.kstarbound.server.world.ServerWorld +import ru.dbotthepony.kstarbound.util.CarriedExecutor import ru.dbotthepony.kstarbound.util.GameTimer import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.util.random.shuffle @@ -57,6 +63,7 @@ import ru.dbotthepony.kstarbound.world.api.AbstractLiquidState import ru.dbotthepony.kstarbound.world.castRay import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.ItemDropEntity +import ru.dbotthepony.kstarbound.world.entities.PathFinder import ru.dbotthepony.kstarbound.world.entities.api.ScriptedEntity import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject import ru.dbotthepony.kstarbound.world.physics.CollisionType @@ -82,7 +89,7 @@ private fun ExecutionContext.resolvePolyCollision(self: World<*, *>, originalPol val tiles = ObjectArrayList() - for (tile in self.queryTileCollisions(poly.aabb.enlarge(maximumCorrection + 1.0, maximumCorrection + 1.0))) { + for (tile in self.chunkMap.queryTileCollisions(poly.aabb.enlarge(maximumCorrection + 1.0, maximumCorrection + 1.0))) { if (tile.type in collisions) { tiles.add(Entry(tile.poly, tile.poly.centre)) } @@ -185,6 +192,10 @@ fun provideWorldBindings(self: World<*, *>, lua: LuaEnvironment) { } } + callbacks["distance"] = luaFunction { arg1: Table, arg2: Table -> + returnBuffer.setTo(from(self.geometry.diff(toVector2d(arg1), toVector2d(arg2)))) + } + callbacks["polyContains"] = luaFunction { poly: Table, position: Table -> returnBuffer.setTo(self.geometry.polyContains(toPoly(poly), toVector2d(position))) } @@ -217,19 +228,19 @@ fun provideWorldBindings(self: World<*, *>, lua: LuaEnvironment) { callbacks["rectCollision"] = luaFunction { rect: Table, collisions: Table? -> if (collisions == null) { - returnBuffer.setTo(self.collide(toPoly(rect), Predicate { it.type.isSolidCollision }).findAny().isPresent) + returnBuffer.setTo(self.chunkMap.collide(toPoly(rect), Predicate { it.type.isSolidCollision }).findAny().isPresent) } else { val actualCollisions = EnumSet.copyOf(collisions.iterator().map { CollisionType.entries.valueOf((it.value as ByteString).decode()) }.toList()) - returnBuffer.setTo(self.collide(toPoly(rect), Predicate { it.type in actualCollisions }).findAny().isPresent) + returnBuffer.setTo(self.chunkMap.collide(toPoly(rect), Predicate { it.type in actualCollisions }).findAny().isPresent) } } callbacks["pointCollision"] = luaFunction { rect: Table, collisions: Table? -> if (collisions == null) { - returnBuffer.setTo(self.collide(toVector2d(rect), Predicate { it.type.isSolidCollision })) + returnBuffer.setTo(self.chunkMap.collide(toVector2d(rect), Predicate { it.type.isSolidCollision })) } else { val actualCollisions = EnumSet.copyOf(collisions.iterator().map { CollisionType.entries.valueOf((it.value as ByteString).decode()) }.toList()) - returnBuffer.setTo(self.collide(toVector2d(rect), Predicate { it.type in actualCollisions })) + returnBuffer.setTo(self.chunkMap.collide(toVector2d(rect), Predicate { it.type in actualCollisions })) } } @@ -270,17 +281,18 @@ fun provideWorldBindings(self: World<*, *>, lua: LuaEnvironment) { callbacks["rectTileCollision"] = luaFunction { rect: Table, collisions: Table? -> if (collisions == null) { - returnBuffer.setTo(self.anyCellSatisfies(toAABB(rect), World.CellPredicate { x, y, cell -> cell.foreground.material.value.collisionKind.isSolidCollision })) + returnBuffer.setTo(self.chunkMap.anyCellSatisfies(toAABB(rect), World.CellPredicate { x, y, cell -> cell.foreground.material.value.collisionKind.isSolidCollision })) } else { val actualCollisions = EnumSet.copyOf(collisions.iterator().map { CollisionType.entries.valueOf((it.value as ByteString).decode()) }.toList()) - returnBuffer.setTo(self.anyCellSatisfies(toAABB(rect), World.CellPredicate { x, y, cell -> cell.foreground.material.value.collisionKind in actualCollisions })) + val a = self.chunkMap.anyCellSatisfies(toAABB(rect), World.CellPredicate { x, y, cell -> cell.foreground.material.value.collisionKind in actualCollisions }) + returnBuffer.setTo(a) } } callbacks["lineCollision"] = luaFunction { pos0: Table, pos1: Table, collisions: Table? -> val actualCollisions = if (collisions == null) CollisionType.SOLID else EnumSet.copyOf(collisions.iterator().map { CollisionType.entries.valueOf((it.value as ByteString).decode()) }.toList()) - val result = self.collide(Line2d(toVector2d(pos0), toVector2d(pos1))) { it.type in actualCollisions } + val result = self.chunkMap.collide(Line2d(toVector2d(pos0), toVector2d(pos1))) { it.type in actualCollisions } if (result == null) { returnBuffer.setTo() @@ -291,10 +303,10 @@ fun provideWorldBindings(self: World<*, *>, lua: LuaEnvironment) { callbacks["polyCollision"] = luaFunction { rect: Table, translate: Table?, collisions: Table? -> if (collisions == null) { - returnBuffer.setTo(self.collide(toPoly(rect).let { if (translate != null) it + toVector2d(translate) else it }, Predicate { it.type.isSolidCollision }).findAny().isPresent) + returnBuffer.setTo(self.chunkMap.collide(toPoly(rect).let { if (translate != null) it + toVector2d(translate) else it }, Predicate { it.type.isSolidCollision }).findAny().isPresent) } else { val actualCollisions = EnumSet.copyOf(collisions.iterator().map { CollisionType.entries.valueOf((it.value as ByteString).decode()) }.toList()) - returnBuffer.setTo(self.collide(toPoly(rect).let { if (translate != null) it + toVector2d(translate) else it }, Predicate { it.type in actualCollisions }).findAny().isPresent) + returnBuffer.setTo(self.chunkMap.collide(toPoly(rect).let { if (translate != null) it + toVector2d(translate) else it }, Predicate { it.type in actualCollisions }).findAny().isPresent) } } @@ -461,7 +473,7 @@ fun provideWorldBindings(self: World<*, *>, lua: LuaEnvironment) { callbacks["spawnMonster"] = luaFunction { // TODO - returnBuffer.setTo(0) + returnBuffer.setTo(0L) } callbacks["spawnNpc"] = luaStub("spawnNpc") callbacks["spawnStagehand"] = luaStub("spawnStagehand") @@ -483,7 +495,7 @@ fun provideWorldBindings(self: World<*, *>, lua: LuaEnvironment) { } callbacks["liquidAt"] = luaFunction { posOrRect: Table -> - if (posOrRect[1L] is Number) { + if (posOrRect[3L] !is Number) { val cell = self.getCell(toVector2i(posOrRect)) if (cell.liquid.state.isNotEmptyLiquid) { @@ -499,7 +511,7 @@ fun provideWorldBindings(self: World<*, *>, lua: LuaEnvironment) { } callbacks["liquidNameAt"] = luaFunction { posOrRect: Table -> - if (posOrRect[1L] is Number) { + if (posOrRect[3L] !is Number) { val cell = self.getCell(toVector2i(posOrRect)) if (cell.liquid.state.isNotEmptyLiquid) { @@ -515,11 +527,11 @@ fun provideWorldBindings(self: World<*, *>, lua: LuaEnvironment) { } callbacks["gravity"] = luaFunction { pos: Table -> - returnBuffer.setTo(self.gravityAt(toVector2d(pos)).y) + returnBuffer.setTo(self.chunkMap.gravityAt(toVector2d(pos)).y) } callbacks["gravityVector"] = luaFunction { pos: Table -> - returnBuffer.setTo(from(self.gravityAt(toVector2d(pos)))) + returnBuffer.setTo(from(self.chunkMap.gravityAt(toVector2d(pos)))) } callbacks["spawnLiquidPromise"] = luaFunction { pos: Table, liquid: Any, quantity: Number -> @@ -559,10 +571,32 @@ fun provideWorldBindings(self: World<*, *>, lua: LuaEnvironment) { callbacks["isTileProtected"] = luaFunction { pos: Table -> returnBuffer.setTo(self.isDungeonIDProtected(self.getCell(toVector2i(pos)).dungeonId)) } - callbacks["findPlatformerPath"] = luaStub("findPlatformerPath") - callbacks["platformerPathStart"] = luaStub("platformerPathStart") + callbacks["findPlatformerPath"] = luaFunction { start: Table, end: Table, actorParams: Table, searchParams: Table -> + LOGGER.warn("world.findPlatformerPath() was called, this will cause world lag. Consider switching to world.platformerPathStart(), since it is multithreaded in new engine.") - callbacks["type"] = luaFunction { returnBuffer.setTo(self.template.worldParameters?.typeName ?: "unknown") } + val finder = PathFinder( + self, + toVector2d(start), + toVector2d(end), + Starbound.gson.fromJsonFast(toJsonFromLua(actorParams), ActorMovementParameters::class.java), + Starbound.gson.fromJsonFast(toJsonFromLua(searchParams), PathFinder.Parameters::class.java)) + + finder.run(Int.MAX_VALUE) + returnBuffer.setTo(LuaPathFinder.convertPath(this, finder.result.orNull())) + } + + val pacer = CarriedExecutor(Starbound.EXECUTOR) + + callbacks["platformerPathStart"] = luaFunction { start: Table, end: Table, actorParams: Table, searchParams: Table -> + returnBuffer.setTo(LuaPathFinder(pacer, PathFinder( + self, + toVector2d(start), + toVector2d(end), + Starbound.gson.fromJsonFast(toJsonFromLua(actorParams), ActorMovementParameters::class.java), + Starbound.gson.fromJsonFast(toJsonFromLua(searchParams), PathFinder.Parameters::class.java)))) + } + + callbacks["type"] = luaFunction { returnBuffer.setTo(self.template.worldParameters?.typeName.toByteString() ?: "unknown".toByteString()) } callbacks["size"] = luaFunction { returnBuffer.setTo(from(self.geometry.size)) } callbacks["inSurfaceLayer"] = luaFunction { pos: Table -> returnBuffer.setTo(self.template.isSurfaceLayer(toVector2i(pos))) } callbacks["surfaceLevel"] = luaFunction { returnBuffer.setTo(self.template.surfaceLevel) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEntityBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEntityBindings.kt index aadf9218..038b4831 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEntityBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEntityBindings.kt @@ -22,6 +22,7 @@ import ru.dbotthepony.kstarbound.lua.set import ru.dbotthepony.kstarbound.lua.tableMapOf import ru.dbotthepony.kstarbound.lua.tableOf import ru.dbotthepony.kstarbound.lua.toAABB +import ru.dbotthepony.kstarbound.lua.toByteString import ru.dbotthepony.kstarbound.lua.toJsonFromLua import ru.dbotthepony.kstarbound.lua.toLine2d import ru.dbotthepony.kstarbound.lua.toPoly @@ -345,12 +346,12 @@ fun provideWorldEntitiesBindings(self: World<*, *>, callbacks: Table, lua: LuaEn callbacks["entitySpecies"] = luaFunction { id: Number -> val entity = self.entities[id.toInt()] as? HumanoidActorEntity ?: return@luaFunction returnBuffer.setTo() - returnBuffer.setTo(entity.species) + returnBuffer.setTo(entity.species.toByteString()) } callbacks["entityGender"] = luaFunction { id: Number -> val entity = self.entities[id.toInt()] as? HumanoidActorEntity ?: return@luaFunction returnBuffer.setTo() - returnBuffer.setTo(entity.gender.jsonName) + returnBuffer.setTo(entity.gender.jsonName.toByteString()) } callbacks["entityName"] = luaFunction { id: Number -> @@ -358,8 +359,8 @@ fun provideWorldEntitiesBindings(self: World<*, *>, callbacks: Table, lua: LuaEn // TODO when (entity) { - is ActorEntity -> returnBuffer.setTo(entity.name) - is WorldObject -> returnBuffer.setTo(entity.config.key) + is ActorEntity -> returnBuffer.setTo(entity.name.toByteString()) + is WorldObject -> returnBuffer.setTo(entity.config.key.toByteString()) } } @@ -383,8 +384,8 @@ fun provideWorldEntitiesBindings(self: World<*, *>, callbacks: Table, lua: LuaEn val entity = self.entities[id.toInt()] as? HumanoidActorEntity ?: return@luaFunction returnBuffer.setTo() when (val gethand = hand.decode().lowercase()) { - "primary" -> returnBuffer.setTo(entity.primaryHandItem.entry.nameOrNull) - "alt", "secondary" -> returnBuffer.setTo(entity.secondaryHandItem.entry.nameOrNull) + "primary" -> returnBuffer.setTo(entity.primaryHandItem.entry.nameOrNull.toByteString()) + "alt", "secondary" -> returnBuffer.setTo(entity.secondaryHandItem.entry.nameOrNull.toByteString()) else -> throw LuaRuntimeException("Unknown tool hand $gethand") } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEnvironmentalBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEnvironmentalBindings.kt index 4781d6bf..36e92688 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEnvironmentalBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEnvironmentalBindings.kt @@ -8,6 +8,7 @@ import org.classdump.luna.runtime.ExecutionContext import ru.dbotthepony.kommons.collect.map import ru.dbotthepony.kommons.collect.toList import ru.dbotthepony.kstarbound.Registries +import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.tile.TileDamage import ru.dbotthepony.kstarbound.defs.tile.TileDamageResult import ru.dbotthepony.kstarbound.defs.tile.TileDamageType @@ -26,6 +27,7 @@ import ru.dbotthepony.kstarbound.lua.nextOptionalInteger import ru.dbotthepony.kstarbound.lua.set import ru.dbotthepony.kstarbound.lua.tableFrom import ru.dbotthepony.kstarbound.lua.tableOf +import ru.dbotthepony.kstarbound.lua.toByteString import ru.dbotthepony.kstarbound.lua.toVector2d import ru.dbotthepony.kstarbound.lua.toVector2i import ru.dbotthepony.kstarbound.lua.userdata.LuaFuture @@ -107,7 +109,7 @@ fun provideWorldEnvironmentalBindings(self: World<*, *>, callbacks: Table, lua: callbacks["windLevel"] = luaStub("windLevel") callbacks["breathable"] = luaFunction { pos: Table -> - returnBuffer.setTo(self.isBreathable(toVector2i(pos))) + returnBuffer.setTo(self.chunkMap.isBreathable(toVector2i(pos))) } callbacks["underground"] = luaFunction { pos: Table -> @@ -123,7 +125,7 @@ fun provideWorldEnvironmentalBindings(self: World<*, *>, callbacks: Table, lua: } else if (tile.material.isEmptyTile) { returnBuffer.setTo(false) } else { - returnBuffer.setTo(tile.material.key) + returnBuffer.setTo(tile.material.key.toByteString()) } } @@ -132,32 +134,32 @@ fun provideWorldEnvironmentalBindings(self: World<*, *>, callbacks: Table, lua: val tile = self.getCell(toVector2i(pos)).tile(isBackground) if (tile.modifier.isNotEmptyModifier) { - returnBuffer.setTo(tile.modifier.key) + returnBuffer.setTo(tile.modifier.key.toByteString()) } } callbacks["materialHueShift"] = luaFunction { pos: Table, layer: ByteString -> val isBackground = isBackground(layer) val tile = self.getCell(toVector2i(pos)).tile(isBackground) - returnBuffer.setTo(tile.hueShift) + returnBuffer.setTo(tile.hueShift.toDouble()) } callbacks["modHueShift"] = luaFunction { pos: Table, layer: ByteString -> val isBackground = isBackground(layer) val tile = self.getCell(toVector2i(pos)).tile(isBackground) - returnBuffer.setTo(tile.modifierHueShift) + returnBuffer.setTo(tile.modifierHueShift.toDouble()) } callbacks["materialColor"] = luaFunction { pos: Table, layer: ByteString -> val isBackground = isBackground(layer) val tile = self.getCell(toVector2i(pos)).tile(isBackground) - returnBuffer.setTo(tile.color.ordinal) + returnBuffer.setTo(tile.color.ordinal.toLong()) } callbacks["materialColorName"] = luaFunction { pos: Table, layer: ByteString -> val isBackground = isBackground(layer) val tile = self.getCell(toVector2i(pos)).tile(isBackground) - returnBuffer.setTo(tile.color.jsonName) + returnBuffer.setTo(tile.color.jsonName.toByteString()) } callbacks["setMaterialColor"] = luaFunction { pos: Table, layer: ByteString, color: Any -> @@ -177,11 +179,11 @@ fun provideWorldEnvironmentalBindings(self: World<*, *>, callbacks: Table, lua: } callbacks["oceanLevel"] = luaFunction { pos: Table -> - returnBuffer.setTo(self.template.cellInfo(toVector2i(pos)).oceanLiquidLevel) + returnBuffer.setTo(self.template.cellInfo(toVector2i(pos)).oceanLiquidLevel.toLong()) } callbacks["environmentStatusEffects"] = luaFunction { pos: Table -> - returnBuffer.setTo(tableFrom(self.environmentStatusEffects(toVector2i(pos)))) + returnBuffer.setTo(tableFrom(self.environmentStatusEffects(toVector2i(pos)).map { from(Starbound.gson.toJsonTree(it)) })) } callbacks["damageTiles"] = luaFunctionN("damageTiles") { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldObjectBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldObjectBindings.kt index 3cdce01c..efa8f919 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldObjectBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldObjectBindings.kt @@ -21,12 +21,14 @@ import ru.dbotthepony.kstarbound.lua.iterator import ru.dbotthepony.kstarbound.lua.luaFunction import ru.dbotthepony.kstarbound.lua.set import ru.dbotthepony.kstarbound.lua.tableOf +import ru.dbotthepony.kstarbound.lua.toByteString import ru.dbotthepony.kstarbound.lua.toColor import ru.dbotthepony.kstarbound.lua.toJson import ru.dbotthepony.kstarbound.lua.toJsonFromLua import ru.dbotthepony.kstarbound.lua.toVector2d import ru.dbotthepony.kstarbound.lua.toVector2i import ru.dbotthepony.kstarbound.util.SBPattern +import ru.dbotthepony.kstarbound.util.sbIntern import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject @@ -48,12 +50,12 @@ fun provideWorldObjectBindings(self: WorldObject, lua: LuaEnvironment) { val table = lua.newTable() lua.globals["object"] = table - table["name"] = luaFunction { returnBuffer.setTo(self.config.key) } + table["name"] = luaFunction { returnBuffer.setTo(self.config.key.toByteString()) } table["direction"] = luaFunction { returnBuffer.setTo(self.direction.luaValue) } table["position"] = luaFunction { returnBuffer.setTo(from(self.tilePosition)) } table["setInteractive"] = luaFunction { interactive: Boolean -> self.isInteractive = interactive } - table["uniqueId"] = luaFunction { returnBuffer.setTo(self.uniqueID.get()) } - table["setUniqueId"] = luaFunction { id: ByteString? -> self.uniqueID.accept(id?.decode()) } + table["uniqueId"] = luaFunction { returnBuffer.setTo(self.uniqueID.get().toByteString()) } + table["setUniqueId"] = luaFunction { id: ByteString? -> self.uniqueID.accept(id?.decode()?.sbIntern()) } table["boundBox"] = luaFunction { returnBuffer.setTo(from(self.metaBoundingBox)) } // original engine parity, it returns occupied spaces in local coordinates @@ -127,8 +129,8 @@ fun provideWorldObjectBindings(self: WorldObject, lua: LuaEnvironment) { returnBuffer.setTo(from(self.lightSourceColor)) } - table["inputNodeCount"] = luaFunction { returnBuffer.setTo(self.inputNodes.size) } - table["outputNodeCount"] = luaFunction { returnBuffer.setTo(self.outputNodes.size) } + table["inputNodeCount"] = luaFunction { returnBuffer.setTo(self.inputNodes.size.toLong()) } + table["outputNodeCount"] = luaFunction { returnBuffer.setTo(self.outputNodes.size.toLong()) } table["getInputNodePosition"] = luaFunction { index: Long -> returnBuffer.setTo(from(self.inputNodes[index.toInt()].position)) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/BehaviorState.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/BehaviorState.kt new file mode 100644 index 00000000..72e33f6f --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/BehaviorState.kt @@ -0,0 +1,121 @@ +package ru.dbotthepony.kstarbound.lua.userdata + +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import org.classdump.luna.ByteString +import org.classdump.luna.LuaRuntimeException +import org.classdump.luna.Table +import org.classdump.luna.Userdata +import org.classdump.luna.impl.ImmutableTable +import org.classdump.luna.runtime.LuaFunction +import ru.dbotthepony.kommons.gson.set +import ru.dbotthepony.kstarbound.Registries +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.actor.behavior.BehaviorDefinition +import ru.dbotthepony.kstarbound.fromJsonFast +import ru.dbotthepony.kstarbound.json.mergeJson +import ru.dbotthepony.kstarbound.lua.LuaEnvironment +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.tableMapOf +import ru.dbotthepony.kstarbound.lua.toJson +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 + +class BehaviorState(val tree: BehaviorTree) : Userdata() { + val blackboard get() = tree.blackboard + val lua get() = blackboard.lua + val functions = HashMap>() + + init { + lua.attach(tree.scripts) + + for (name in tree.functions) { + val get = lua.globals[name] + + if (get == null) { + throw LuaRuntimeException("No such function for behavior: $name") + } else if (get !is LuaFunction<*, *, *, *, *>) { + throw LuaRuntimeException("Not a Lua function for behavior: $name") + } else { + this.functions[name] = get + } + } + } + + fun run(delta: Double): AbstractBehaviorNode.Status { + val ephemerals = blackboard.takeEphemerals() + val status = tree.runAndReset(delta, this) + blackboard.clearEphemerals(ephemerals) + return status + } + + override fun getMetatable(): Table { + return Companion.metatable + } + + override fun setMetatable(mt: Table?): Table { + throw UnsupportedOperationException() + } + + override fun getUserValue(): BehaviorState { + return this + } + + override fun setUserValue(value: BehaviorState?): BehaviorState? { + throw UnsupportedOperationException() + } + + companion object { + private fun __index(): Table { + return metatable + } + + private val metatable = ImmutableTable.Builder() + .add("__index", luaFunction { _: Any?, index: Any -> returnBuffer.setTo(__index()[index]) }) + .add("run", luaFunction { self: BehaviorState, delta: Number -> + returnBuffer.setTo(self.run(delta.toDouble())) + }) + .add("clear", luaFunction { self: BehaviorState -> + self.tree.reset() + }) + .add("blackboard", luaFunction { self: BehaviorState -> + returnBuffer.setTo(self.blackboard) + }) + .build() + + fun provideBindings(lua: LuaEnvironment) { + lua.globals["behavior"] = lua.tableMapOf( + "behavior" to luaFunction { config: Any, parameters: Table, _: Any?, blackboard: Blackboard? -> + val tree: BehaviorTree + + if (config is ByteString) { + val base = Registries.behavior.getOrThrow(config.decode()) + + if (!parameters.iterator().hasNext()) { + tree = BehaviorTree(blackboard ?: Blackboard(lua), base.value) + } else { + tree = BehaviorTree(blackboard ?: Blackboard(lua), Starbound.gson.fromJsonFast(base.json.deepCopy().also { + it as JsonObject + it["parameters"] = mergeJson(it["parameters"] ?: JsonNull.INSTANCE, toJsonFromLua(parameters)) + }, BehaviorDefinition::class.java)) + } + } else { + val cast = (config as Table).toJson(true) as JsonObject + + if (parameters.iterator().hasNext()) + cast["parameters"] = mergeJson(cast["parameters"] ?: JsonNull.INSTANCE, toJsonFromLua(parameters)) + + tree = BehaviorTree(blackboard ?: Blackboard(lua), Starbound.gson.fromJsonFast(cast, BehaviorDefinition::class.java)) + } + + returnBuffer.setTo(BehaviorState(tree)) + } + ) + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/LuaFuture.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/LuaFuture.kt index 5e9ed5a1..382696fb 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/LuaFuture.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/LuaFuture.kt @@ -3,6 +3,7 @@ package ru.dbotthepony.kstarbound.lua.userdata import org.classdump.luna.Table import org.classdump.luna.Userdata import org.classdump.luna.impl.ImmutableTable +import ru.dbotthepony.kstarbound.lua.get import ru.dbotthepony.kstarbound.lua.luaFunction import java.util.concurrent.CancellationException import java.util.concurrent.CompletableFuture @@ -32,7 +33,12 @@ class LuaFuture(val future: CompletableFuture, val isLocal: Boolean) : } companion object { + private fun __index(): Table { + return metadata + } + private val metadata = ImmutableTable.Builder() + .add("__index", luaFunction { _: Any?, index: Any -> returnBuffer.setTo(__index()[index]) }) .add("finished", luaFunction { self: LuaFuture -> returnBuffer.setTo(self.future.isDone) }) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/LuaPathFinder.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/LuaPathFinder.kt new file mode 100644 index 00000000..074d9377 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/LuaPathFinder.kt @@ -0,0 +1,101 @@ +package ru.dbotthepony.kstarbound.lua.userdata + +import org.classdump.luna.Table +import org.classdump.luna.TableFactory +import org.classdump.luna.Userdata +import org.classdump.luna.impl.ImmutableTable +import ru.dbotthepony.kstarbound.Starbound +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.lua.toByteString +import ru.dbotthepony.kstarbound.lua.userdata.BehaviorState.Companion +import ru.dbotthepony.kstarbound.util.CarriedExecutor +import ru.dbotthepony.kstarbound.util.supplyAsync +import ru.dbotthepony.kstarbound.world.entities.PathFinder +import java.util.concurrent.CompletableFuture + +class LuaPathFinder(val pacer: CarriedExecutor, val self: PathFinder) : Userdata() { + override fun getMetatable(): Table { + return Companion.metatable + } + + override fun setMetatable(mt: Table?): Table { + throw UnsupportedOperationException() + } + + override fun getUserValue(): PathFinder { + return self + } + + override fun setUserValue(value: PathFinder?): PathFinder { + throw UnsupportedOperationException() + } + + private var isRunning = false + + companion object { + private val cost = "const".toByteString()!! + private val action = "action".toByteString()!! + private val jumpVelocity = "jumpVelocity".toByteString()!! + private val source = "source".toByteString()!! + private val target = "target".toByteString()!! + + fun convertPath(tables: TableFactory, list: List?): Table? { + list ?: return null + val table = tables.newTable(list.size, 0) + var i = 1L + + for (edge in list) { + val edgeTable = tables.tableOf() + edgeTable[cost] = edge.cost + edgeTable[action] = edge.action.luaName + edgeTable[jumpVelocity] = tables.from(edge.velocity) + edgeTable[source] = edge.source.toTable(tables) + edgeTable[target] = edge.target.toTable(tables) + table[i++] = edgeTable + } + + return table + } + + private fun __index(): Table { + return metatable + } + + private val metatable = ImmutableTable.Builder() + .add("__index", luaFunction { _: Any?, index: Any -> returnBuffer.setTo(__index()[index]) }) + .add("explore", luaFunction { self: LuaPathFinder, maxExplore: Number? -> + if (self.isRunning) { + if (self.self.result.isEmpty) + returnBuffer.setTo(null) + else + returnBuffer.setTo(self.self.result.value != null) + } else if (self.self.result.isPresent) { + returnBuffer.setTo(self.self.result.value != null) + } else { + val immediateMaxExplore = maxExplore?.toInt()?.times(4)?.coerceAtMost(800) ?: 800 + val result = self.self.run(immediateMaxExplore) + + if (result != null) { + returnBuffer.setTo(result) + } else if (self.self.result.isEmpty) { + // didn't explore enough, run in separate thread to not block main thread + self.pacer.supplyAsync(self.self) + self.isRunning = true + returnBuffer.setTo(null) + } + } + }) + .add("result", luaFunction { self: LuaPathFinder -> + if (self.self.result.isEmpty) + return@luaFunction returnBuffer.setTo() + + val list = self.self.result.value ?: return@luaFunction returnBuffer.setTo() + returnBuffer.setTo(convertPath(this, list)) + }) + .build() + } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/LuaPerlinNoise.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/LuaPerlinNoise.kt index 99eaca3b..49f568ce 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/LuaPerlinNoise.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/LuaPerlinNoise.kt @@ -3,7 +3,9 @@ package ru.dbotthepony.kstarbound.lua.userdata import org.classdump.luna.Table import org.classdump.luna.Userdata import org.classdump.luna.impl.ImmutableTable +import ru.dbotthepony.kstarbound.lua.get import ru.dbotthepony.kstarbound.lua.luaFunction +import ru.dbotthepony.kstarbound.lua.userdata.LuaPathFinder.Companion import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise class LuaPerlinNoise(val noise: AbstractPerlinNoise) : Userdata() { @@ -28,7 +30,12 @@ class LuaPerlinNoise(val noise: AbstractPerlinNoise) : Userdata returnBuffer.setTo(__index()[index]) }) .add("get", luaFunction { self: LuaPerlinNoise, x: Number, y: Number?, z: Number? -> if (y != null && z != null) { returnBuffer.setTo(self.noise[x.toDouble(), y.toDouble(), z.toDouble()]) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/LuaRandomGenerator.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/LuaRandomGenerator.kt index 14cc671b..ab03ecce 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/LuaRandomGenerator.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/LuaRandomGenerator.kt @@ -3,6 +3,7 @@ package ru.dbotthepony.kstarbound.lua.userdata import org.classdump.luna.Table import org.classdump.luna.Userdata import org.classdump.luna.impl.ImmutableTable +import ru.dbotthepony.kstarbound.lua.get import ru.dbotthepony.kstarbound.lua.luaFunction import ru.dbotthepony.kstarbound.util.random.nextNormalDouble import ru.dbotthepony.kstarbound.util.random.random @@ -63,7 +64,12 @@ class LuaRandomGenerator(var random: RandomGenerator) : Userdata returnBuffer.setTo(__index()[index]) }) .add("init", luaFunction { self: LuaRandomGenerator, seed: Long? -> self.random = random(seed ?: System.nanoTime()) }) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/math/AABB.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/math/AABB.kt index 6c1228ca..d6724ec3 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/math/AABB.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/math/AABB.kt @@ -225,9 +225,15 @@ data class AABB(val mins: Vector2d, val maxs: Vector2d) { ) } + fun padded(x: Double, y: Double): AABB { + return AABB( + mins - Vector2d(x, y), + maxs + Vector2d(x, y) + ) + } + fun expand(value: IStruct2d) = expand(value.component1(), value.component2()) - fun padded(value: IStruct2d) = expand(value.component1(), value.component2()) - fun padded(x: Double, y: Double) = expand(x, y) + fun padded(value: IStruct2d) = padded(value.component1(), value.component2()) /** * Returns AABB which edges are expanded by [x] and [y] along their normals diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/math/AABBi.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/math/AABBi.kt index 9a42d6e7..768f7259 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/math/AABBi.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/math/AABBi.kt @@ -4,6 +4,7 @@ import ru.dbotthepony.kommons.math.intersectRectangles import ru.dbotthepony.kommons.math.rectangleContainsRectangle import ru.dbotthepony.kommons.util.IStruct2i import ru.dbotthepony.kstarbound.math.vector.Vector2i +import kotlin.math.roundToInt data class AABBi(val mins: Vector2i, val maxs: Vector2i) { init { @@ -99,8 +100,14 @@ data class AABBi(val mins: Vector2i, val maxs: Vector2i) { ) } - fun padded(x: Int, y: Int) = expand(x, y) - fun padded(value: IStruct2i) = expand(value.component1(), value.component2()) + fun padded(x: Int, y: Int): AABBi { + return AABBi( + mins - Vector2i(x, y), + maxs + Vector2i(x, y) + ) + } + + fun padded(value: IStruct2i) = padded(value.component1(), value.component2()) fun expand(value: IStruct2i) = expand(value.component1(), value.component2()) /** @@ -151,5 +158,19 @@ data class AABBi(val mins: Vector2i, val maxs: Vector2i) { } @JvmField val ZERO = AABBi(Vector2i.ZERO, Vector2i.ZERO) + + fun of(aabb: AABB): AABBi { + return AABBi( + Vector2i(aabb.mins.x.toInt(), aabb.mins.y.toInt()), + Vector2i(aabb.maxs.x.toInt(), aabb.maxs.y.toInt()), + ) + } + + fun ofRound(aabb: AABB): AABBi { + return AABBi( + Vector2i(aabb.mins.x.roundToInt(), aabb.mins.y.roundToInt()), + Vector2i(aabb.maxs.x.roundToInt(), aabb.maxs.y.roundToInt()), + ) + } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/math/Line2d.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Line2d.kt index bef12a02..a5208a13 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/math/Line2d.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Line2d.kt @@ -69,6 +69,9 @@ data class Line2d(val p0: Vector2d, val p1: Vector2d) { val difference: Vector2d get() = p1 - p0 + val direction: Vector2d + get() = difference.unitVector + fun reverse(): Line2d { return Line2d(p1, p0) } @@ -157,6 +160,13 @@ data class Line2d(val p0: Vector2d, val p1: Vector2d) { return ((x - p0.x) * diff.x + (y - p0.y) * diff.y) / diff.lengthSquared } + fun rotate(angle: Double): Line2d { + if (angle == 0.0) + return this + + return Line2d(p0.rotate(angle), p1.rotate(angle)) + } + fun distanceTo(other: IStruct2d, infinite: Boolean = false): Double { var proj = project(other) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/math/vector/Vector2d.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/math/vector/Vector2d.kt index eb129369..080fff11 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/math/vector/Vector2d.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/math/vector/Vector2d.kt @@ -186,6 +186,9 @@ data class Vector2d( fun coerceAtMost(value: Vector2d): Vector2d { val (x, y) = value; return coerceAtMost(x, y) } fun rotate(angle: Double): Vector2d { + if (angle == 0.0) + return this + val s = sin(angle) val c = cos(angle) @@ -193,6 +196,9 @@ data class Vector2d( } fun toAngle(zeroAngle: Vector2d): Double { + if (x == 0.0 && y == 0.0) + return 0.0 // assume angle is zero + val dot = unitVector.dot(zeroAngle.unitVector) return if (y > 0.0) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt index 7df86afb..9d33e1d3 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt @@ -17,7 +17,6 @@ import ru.dbotthepony.kstarbound.client.network.packets.ForgetChunkPacket import ru.dbotthepony.kstarbound.client.network.packets.ChunkCellsPacket import ru.dbotthepony.kstarbound.client.network.packets.ForgetEntityPacket import ru.dbotthepony.kstarbound.client.network.packets.JoinWorldPacket -import ru.dbotthepony.kstarbound.client.network.packets.SpawnWorldObjectPacket import ru.dbotthepony.kstarbound.network.packets.ClientContextUpdatePacket import ru.dbotthepony.kstarbound.network.packets.DamageNotificationPacket import ru.dbotthepony.kstarbound.network.packets.HitRequestPacket @@ -399,7 +398,6 @@ class PacketRegistry(val isLegacy: Boolean) { NATIVE.add(::JoinWorldPacket) NATIVE.add(::ChunkCellsPacket) NATIVE.add(::ForgetChunkPacket) - NATIVE.add(::SpawnWorldObjectPacket) NATIVE.add(::ForgetEntityPacket) NATIVE.add(::UniverseTimeUpdatePacket) NATIVE.add(::StepUpdatePacket) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/ClientContextUpdatePacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/ClientContextUpdatePacket.kt index 68d9a737..0da3be27 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/ClientContextUpdatePacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/ClientContextUpdatePacket.kt @@ -4,9 +4,7 @@ import it.unimi.dsi.fastutil.bytes.ByteArrayList import it.unimi.dsi.fastutil.io.FastByteArrayInputStream import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap -import ru.dbotthepony.kommons.io.ByteKey import ru.dbotthepony.kommons.io.readByteArray -import ru.dbotthepony.kommons.io.readByteKey import ru.dbotthepony.kommons.io.readCollection import ru.dbotthepony.kommons.io.readKOptional import ru.dbotthepony.kommons.io.readMap @@ -17,6 +15,8 @@ import ru.dbotthepony.kommons.io.writeMap import ru.dbotthepony.kommons.io.writeVarInt import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kstarbound.client.ClientConnection +import ru.dbotthepony.kstarbound.io.ByteKey +import ru.dbotthepony.kstarbound.io.readByteKey import ru.dbotthepony.kstarbound.json.readJsonElement import ru.dbotthepony.kstarbound.network.ConnectionSide import ru.dbotthepony.kstarbound.network.IClientPacket diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ClientConnectPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ClientConnectPacket.kt index ba46db88..1b47f2e1 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ClientConnectPacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ClientConnectPacket.kt @@ -1,21 +1,21 @@ package ru.dbotthepony.kstarbound.network.packets.serverbound import org.apache.logging.log4j.LogManager -import ru.dbotthepony.kommons.io.ByteKey import ru.dbotthepony.kommons.io.readBinaryString import ru.dbotthepony.kommons.io.readByteArray -import ru.dbotthepony.kommons.io.readByteKey import ru.dbotthepony.kommons.io.readKOptional import ru.dbotthepony.kommons.io.readMap import ru.dbotthepony.kommons.io.readUUID import ru.dbotthepony.kommons.io.writeBinaryString import ru.dbotthepony.kommons.io.writeByteArray -import ru.dbotthepony.kommons.io.writeByteKey import ru.dbotthepony.kommons.io.writeKOptional import ru.dbotthepony.kommons.io.writeMap import ru.dbotthepony.kommons.io.writeUUID import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kstarbound.defs.actor.player.ShipUpgrades +import ru.dbotthepony.kstarbound.io.ByteKey +import ru.dbotthepony.kstarbound.io.readByteKey +import ru.dbotthepony.kstarbound.io.writeByteKey import ru.dbotthepony.kstarbound.network.IServerPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.ConnectFailurePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.ConnectSuccessPacket diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt index be02c430..0b53b5ae 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt @@ -13,7 +13,6 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.future.await import kotlinx.coroutines.launch import org.apache.logging.log4j.LogManager -import ru.dbotthepony.kommons.io.ByteKey import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kstarbound.math.vector.Vector2i @@ -29,6 +28,7 @@ import ru.dbotthepony.kstarbound.defs.world.CelestialParameters import ru.dbotthepony.kstarbound.defs.world.SkyType import ru.dbotthepony.kstarbound.defs.world.VisitableWorldParameters import ru.dbotthepony.kstarbound.fromJson +import ru.dbotthepony.kstarbound.io.ByteKey import ru.dbotthepony.kstarbound.json.InternedJsonElementAdapter import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.network.Connection @@ -389,7 +389,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn } world = server.loadSystemWorld(system).await() ?: world - shipCoordinate = UniversePos(world.location) // update ship coordinate after we have successfully travelled to destination + shipCoordinate = UniversePos(world.location) // update ship coordinate *after* we have successfully travelled to destination this.systemWorld = world ship = world.addClient(this).await() systemWorldShip = ship diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyWorldStorage.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyWorldStorage.kt index f5a361c6..9ca33bfa 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyWorldStorage.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyWorldStorage.kt @@ -12,7 +12,6 @@ import kotlinx.coroutines.future.await import kotlinx.coroutines.launch import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.arrays.Object2DArray -import ru.dbotthepony.kommons.io.ByteKey import ru.dbotthepony.kommons.io.readBinaryString import ru.dbotthepony.kommons.io.readCollection import ru.dbotthepony.kommons.io.readVarInt @@ -26,6 +25,7 @@ import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.VersionRegistry import ru.dbotthepony.kstarbound.defs.EntityType import ru.dbotthepony.kstarbound.io.BTreeDB5 +import ru.dbotthepony.kstarbound.io.ByteKey import ru.dbotthepony.kstarbound.io.readInternedString import ru.dbotthepony.kstarbound.io.readVector2f import ru.dbotthepony.kstarbound.json.VersionedJson 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 7e85a33c..a2c942da 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt @@ -1002,21 +1002,33 @@ class ServerWorld private constructor( return ServerWorld(server, WorldTemplate(geometry), storage, worldID) } + private fun applyMeta(world: ServerWorld, meta: MetadataJson) { + world.playerSpawnPosition = meta.playerStart + world.respawnInWorld = meta.respawnInWorld + world.adjustPlayerSpawn = meta.adjustPlayerStart + world.centralStructure = meta.centralStructure + + for ((k, v) in meta.worldProperties.entrySet()) { + world.setProperty(k, v) + } + + for ((id, gravity) in meta.dungeonIdGravity) { + world.dungeonGravityInternal[id] = gravity.map({ Vector2d(0.0, it) }, { it }) + } + + for ((id, breathable) in meta.dungeonIdBreathable) { + world.dungeonBreathableInternal[id] = breathable + } + + world.protectedDungeonIDsInternal.addAll(meta.protectedDungeonIds) + } + fun create(server: StarboundServer, metadata: WorldStorage.Metadata, storage: WorldStorage, worldID: WorldID = WorldID.Limbo): ServerWorld { return AssetPathStack("/") { _ -> val meta = Starbound.gson.fromJson(metadata.data.content, MetadataJson::class.java) val world = ServerWorld(server, WorldTemplate.fromJson(meta.worldTemplate), storage, worldID) - world.playerSpawnPosition = meta.playerStart - world.respawnInWorld = meta.respawnInWorld - world.adjustPlayerSpawn = meta.adjustPlayerStart - world.centralStructure = meta.centralStructure - - for ((k, v) in meta.worldProperties.entrySet()) { - world.setProperty(k, v) - } - - world.protectedDungeonIDsInternal.addAll(meta.protectedDungeonIds) + applyMeta(world, meta) world } } @@ -1032,16 +1044,7 @@ class ServerWorld private constructor( val meta = Starbound.gson.fromJson(it.data.content, MetadataJson::class.java) val world = ServerWorld(server, WorldTemplate.fromJson(meta.worldTemplate), storage, worldID) - world.playerSpawnPosition = meta.playerStart - world.respawnInWorld = meta.respawnInWorld - world.adjustPlayerSpawn = meta.adjustPlayerStart - world.centralStructure = meta.centralStructure - - for ((k, v) in meta.worldProperties.entrySet()) { - world.setProperty(k, v) - } - - world.protectedDungeonIDsInternal.addAll(meta.protectedDungeonIds) + applyMeta(world, meta) world } }.also { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/BlockableEventLoop.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/BlockableEventLoop.kt index 87c03aff..d20d5e91 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/BlockableEventLoop.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/BlockableEventLoop.kt @@ -131,7 +131,7 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer (next.future as CompletableFuture).complete(callable.call()) } } catch (err: Throwable) { - LOGGER.error("Error executing scheduled task", err) + LOGGER.error("Exception executing scheduled task", err) try { next.future.completeExceptionally(err) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/ExceptionLogger.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/ExceptionLogger.kt deleted file mode 100644 index d976e037..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/ExceptionLogger.kt +++ /dev/null @@ -1,10 +0,0 @@ -package ru.dbotthepony.kstarbound.util - -import org.apache.logging.log4j.Logger -import java.util.function.Consumer - -class ExceptionLogger(private val logger: Logger) : Consumer { - override fun accept(t: Throwable) { - logger.error("Error while executing queued task", t) - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/ExecutionSpinner.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/ExecutionSpinner.kt deleted file mode 100644 index 03ec23f4..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/ExecutionSpinner.kt +++ /dev/null @@ -1,144 +0,0 @@ -package ru.dbotthepony.kstarbound.util - -import org.apache.logging.log4j.LogManager -import org.lwjgl.system.MemoryStack -import ru.dbotthepony.kstarbound.Starbound -import ru.dbotthepony.kstarbound.WindowsBindings -import java.util.concurrent.atomic.AtomicInteger -import java.util.concurrent.locks.LockSupport -import java.util.function.BooleanSupplier - -class ExecutionSpinner(private val waiter: Runnable, private val spinner: BooleanSupplier, private val timeBetweenFrames: Long) : Runnable { - init { - Companion - } - - private var lastRender = System.nanoTime() - private var frameRenderTime = 0L - private val frameRenderTimes = LongArray(60) { 1L } - private var frameRenderIndex = 0 - private val renderWaitTimes = LongArray(60) { 1L } - private var renderWaitIndex = 0 - - val averageRenderWait: Double get() { - var sum = 0.0 - - for (value in renderWaitTimes) - sum += value - - if (sum == 0.0) - return 0.0 - - sum /= 1_000_000_000.0 - return sum / renderWaitTimes.size - } - - val averageRenderTime: Double get() { - var sum = 0.0 - - for (value in frameRenderTimes) - sum += value - - if (sum == 0.0) - return 0.0 - - sum /= 1_000_000_000.0 - return sum / frameRenderTimes.size - } - - private fun timeUntilNextFrame(): Long { - return Starbound.TIMESTEP_NANOS - (System.nanoTime() - lastRender) - frameRenderTime - compensate - } - - private var compensate = 0L - - private var carrier: Thread? = null - private val pause = AtomicInteger() - - fun pause() { - pause.incrementAndGet() - } - - fun unpause() { - if (pause.addAndGet(-100) <= 0) { - carrier?.let { LockSupport.unpark(it) } - } - } - - fun spin(): Boolean { - carrier = Thread.currentThread() - - while (pause.get() > 0) { - waiter.run() - LockSupport.park() - } - - var diff = timeUntilNextFrame() - - while (diff > 0L) { - waiter.run() - diff = timeUntilNextFrame() - LockSupport.parkNanos(diff) - } - - compensate = -diff - - val mark = System.nanoTime() - val result = spinner.asBoolean - frameRenderTime = System.nanoTime() - mark - frameRenderTimes[++frameRenderIndex % frameRenderTimes.size] = frameRenderTime - renderWaitTimes[++renderWaitIndex % renderWaitTimes.size] = System.nanoTime() - lastRender - lastRender = System.nanoTime() - - return result - } - - override fun run() { - while (spin()) {} - } - - companion object { - private val LOGGER = LogManager.getLogger() - private var SYSTEM_SCHEDULER_RESOLUTION = 1_000_000L - - init { - val bindings = WindowsBindings.INSTANCE - - if (bindings != null) { - MemoryStack.stackPush().use { stack -> - val minimum = stack.mallocLong(1) - val maximum = stack.mallocLong(1) - val current = stack.mallocLong(1) - - minimum.put(0L) - maximum.put(0L) - current.put(0L) - - minimum.position(0) - maximum.position(0) - current.position(0) - - bindings.NtQueryTimerResolution(minimum, maximum, current) - - SYSTEM_SCHEDULER_RESOLUTION = current[0] * 100L - LOGGER.info("NtQueryTimerResolution(): {} ns/{} ns/{} ns min/max/current", minimum[0] * 100L, maximum[0] * 100L, current[0] * 100L) - } - } - - val thread = object : Thread("Process scheduler timer hack thread") { - override fun run() { - while (true) { - try { - sleep(Int.MAX_VALUE.toLong()) - } catch (err: InterruptedException) { - LOGGER.error("Timer hack thread was interrupted, ignoring.") - } - } - } - } - - thread.isDaemon = true - thread.start() - } - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/HistoryQueue.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/HistoryQueue.kt new file mode 100644 index 00000000..95cc227d --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/HistoryQueue.kt @@ -0,0 +1,52 @@ +package ru.dbotthepony.kstarbound.util + +import ru.dbotthepony.kstarbound.Starbound + +class HistoryQueue(val timeToLive: Double, val capacity: Int) { + constructor(normalTickCapacity: Int) : this(Starbound.TIMESTEP * normalTickCapacity, normalTickCapacity) + + init { + require(capacity > 0) { "Invalid capacity: $capacity" } + require(timeToLive > 0.0) { "Invalid time to live: $timeToLive" } + } + + private val entries = ArrayDeque>() + private var time = 0.0 + private var index = 0L + + private data class Entry(val value: T, val time: Double, val index: Long) + + fun add(entry: T) { + entries.add(Entry(entry, time, index++)) + + while (entries.size > capacity) { + entries.removeFirst() + } + } + + fun tick(delta: Double) { + this.time += delta + + while (entries.isNotEmpty() && this.time - entries.first().time > timeToLive) { + entries.removeFirst() + } + } + + fun query(since: Long = 0L): Pair, Long> { + // TODO: binary search? + val results = ArrayList() + + entries.forEach { + if (it.index >= since) { + results.add(it.value) + } + } + + return results to index + } + + fun clear() { + time = 0.0 + entries.clear() + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/RandomUtils.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/RandomUtils.kt index 2a6a4215..45b064c1 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/RandomUtils.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/RandomUtils.kt @@ -224,7 +224,7 @@ fun Collection.random(random: RandomGenerator, legacyCompatibleSelection: if (legacyCompatibleSelection) { // marvelous - return elementAt(random.nextUInt(size.toULong()).toInt()) + return elementAt(random.nextUInt(size.toULong() - 1UL).toInt()) } else { return elementAt(random.nextInt(size)) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Direction.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Direction.kt index 242c62cf..34e65250 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Direction.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Direction.kt @@ -25,5 +25,17 @@ enum class Direction(val normal: Vector2d, override val jsonName: String, val lu companion object { val CODEC = StreamCodec.Enum(Direction::class.java) + + fun valueOf(number: Number): Direction? { + val value = number.toDouble() + + if (value == 0.0) { + return null + } else if (value < 0.0) { + return LEFT + } else { + return RIGHT + } + } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/EntityIndex.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/EntityIndex.kt index 41866461..8e44b38e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/EntityIndex.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/EntityIndex.kt @@ -182,7 +182,6 @@ class EntityIndex(val geometry: WorldGeometry) { val get = map.computeIfAbsent(sector, factory) addSectors.add(get) ref(get) - // println("Entered sector ${values(sector)}") if (!newItr.hasNext()) { break @@ -204,7 +203,6 @@ class EntityIndex(val geometry: WorldGeometry) { // `existing` references sector which is no longer valid for this fixture deref(existing) existingItr.remove() - // println("Left sector ${values(existing.index)}") existing = if (existingItr.hasNext()) existingItr.next() else null } @@ -213,7 +211,6 @@ class EntityIndex(val geometry: WorldGeometry) { existing = existingItr.next() if (existing!!.index > newSectors0.last()) { - // println("Left sector ${values(existing.index)}") existingItr.remove() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/TileModification.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/TileModification.kt index bbc0753e..a229777f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/TileModification.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/TileModification.kt @@ -57,7 +57,7 @@ sealed class TileModification { abstract fun apply(world: World<*, *>, position: Vector2i, allowEntityOverlap: Boolean) data class PlaceMaterial(val isBackground: Boolean, val material: Registry.Ref, val hueShift: Float?) : TileModification() { - constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readBoolean(), if (isLegacy) Registries.tiles.ref(stream.readUnsignedShort()) else TODO(), if (isLegacy) stream.readNullable { stream.readUnsignedByte() / 255f * 360f } else stream.readNullableFloat()) + constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readBoolean(), if (isLegacy) Registries.tiles.refOrThrow(stream.readUnsignedShort()) else TODO(), if (isLegacy) stream.readNullable { stream.readUnsignedByte() / 255f * 360f } else stream.readNullableFloat()) override fun write(stream: DataOutputStream, isLegacy: Boolean) { stream.writeByte(1) @@ -121,7 +121,7 @@ sealed class TileModification { world.geometry.y.cell(x + 1) == x || world.geometry.y.cell(y - 1) == y || world.geometry.y.cell(y + 1) == y || - world.anyCellSatisfies(x, y, 1) { tx, ty, tcell -> + world.chunkMap.anyCellSatisfies(x, y, 1) { tx, ty, tcell -> (tx != x || ty != y) && (tcell.foreground.material.value.isConnectable || tcell.background.material.value.isConnectable) } } @@ -155,7 +155,7 @@ sealed class TileModification { } data class PlaceModifier(val isBackground: Boolean, val modifier: Registry.Ref, val hueShift: Float?) : TileModification() { - constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readBoolean(), if (isLegacy) Registries.tileModifiers.ref(stream.readUnsignedShort()) else TODO(), if (isLegacy) stream.readNullable { stream.readUnsignedByte() / 255f * 360f } else stream.readNullableFloat()) + constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readBoolean(), if (isLegacy) Registries.tileModifiers.refOrThrow(stream.readUnsignedShort()) else TODO(), if (isLegacy) stream.readNullable { stream.readUnsignedByte() / 255f * 360f } else stream.readNullableFloat()) override fun write(stream: DataOutputStream, isLegacy: Boolean) { stream.writeByte(2) @@ -235,7 +235,7 @@ sealed class TileModification { } data class Pour(val state: Registry.Ref, val level: Float) : TileModification() { - constructor(stream: DataInputStream, isLegacy: Boolean) : this(if (isLegacy) Registries.liquid.ref(stream.readUnsignedByte()) else TODO(), stream.readFloat()) + constructor(stream: DataInputStream, isLegacy: Boolean) : this(if (isLegacy) Registries.liquid.refOrThrow(stream.readUnsignedByte()) else TODO(), stream.readFloat()) override fun write(stream: DataOutputStream, isLegacy: Boolean) { stream.writeByte(4) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt index 02638fb1..61ad5480 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt @@ -21,12 +21,15 @@ import ru.dbotthepony.kommons.gson.set import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kommons.util.IStruct2d import ru.dbotthepony.kommons.util.IStruct2i +import ru.dbotthepony.kstarbound.Globals import ru.dbotthepony.kstarbound.Registry 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.Starbound +import ru.dbotthepony.kstarbound.defs.EphemeralStatusEffect +import ru.dbotthepony.kstarbound.defs.actor.PersistentStatusEffect import ru.dbotthepony.kstarbound.defs.tile.ARTIFICIAL_DUNGEON_ID import ru.dbotthepony.kstarbound.defs.tile.DESTROYED_DUNGEON_ID import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition @@ -44,7 +47,6 @@ import ru.dbotthepony.kstarbound.network.packets.DamageRequestPacket import ru.dbotthepony.kstarbound.network.packets.EntityMessagePacket import ru.dbotthepony.kstarbound.network.packets.HitRequestPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.SetPlayerStartPacket -import ru.dbotthepony.kstarbound.util.ActionPacer import ru.dbotthepony.kstarbound.util.BlockableEventLoop import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.world.api.ICellAccess @@ -69,9 +71,17 @@ import java.util.random.RandomGenerator import java.util.stream.Stream import kotlin.collections.ArrayList import kotlin.collections.HashMap +import kotlin.math.PI import kotlin.math.roundToInt abstract class World, ChunkType : Chunk>(val template: WorldTemplate) : ICellAccess { + fun interface CellPredicate { + fun test(x: Int, y: Int, cell: AbstractCell): Boolean + } + + data class LineCollisionResult(val poly: CollisionPoly, val border: Vector2d, val normal: Vector2d) + data class LiquidLevel(val type: Registry.Entry, val average: Double) + val background = TileView.Background(this) val foreground = TileView.Foreground(this) val sky = Sky(template.skyParameters) @@ -100,6 +110,11 @@ abstract class World, ChunkType : Chunk abstract fun remove(x: Int, y: Int) + /** + * takes a snapshot of this ChunkMap, for use in a different thread + */ + abstract fun snapshot(): ChunkMap + operator fun get(pos: ChunkPos) = get(pos.x, pos.y) fun compute(pos: ChunkPos) = compute(pos.x, pos.y) @@ -128,15 +143,197 @@ abstract class World, ChunkType : Chunk>() + var area = 0.0 + + anyCellSatisfies(rect) { x, y, cell -> + val blockIncidence = AABB.leftCorner(Vector2d(x.toDouble(), y.toDouble()), 1.0, 1.0).overlap(rect).volume + area += blockIncidence + + if (cell.liquid.state.isNotEmptyLiquid && cell.liquid.level > 0f) { + liquidLevels.put(cell.liquid.state, liquidLevels.getDouble(cell.liquid.state) + cell.liquid.level.coerceAtMost(1f) * blockIncidence) + } + + false + } + + if (liquidLevels.isEmpty()) { + return null + } + + val max = liquidLevels.object2DoubleEntrySet().maxBy { it.doubleValue }!! + return LiquidLevel(max.key, max.doubleValue / area) + } + + fun anyCellSatisfies(aabb: AABBi, predicate: CellPredicate): Boolean { + for (x in aabb.mins.x .. aabb.maxs.x) { + for (y in aabb.mins.y .. aabb.maxs.y) { + val ix = geometry.x.cell(x) + val iy = geometry.y.cell(y) + + if (predicate.test(ix, iy, getCellDirect(ix, iy))) { + return true + } + } + } + + return false + } + + fun anyCellSatisfies(aabb: AABB, predicate: CellPredicate): Boolean { + for (x in aabb.mins.x.toInt() .. aabb.maxs.x.toInt()) { + for (y in aabb.mins.y.toInt() .. aabb.maxs.y.toInt()) { + val ix = geometry.x.cell(x) + val iy = geometry.y.cell(y) + + if (predicate.test(ix, iy, getCellDirect(ix, iy))) { + return true + } + } + } + + return false + } + + fun anyCellSatisfies(x: Int, y: Int, distance: Int, predicate: CellPredicate): Boolean { + for (tx in (x - distance) .. (x + distance)) { + for (ty in (y - distance) .. (y + distance)) { + val ix = geometry.x.cell(tx) + val iy = geometry.y.cell(ty) + + if (predicate.test(ix, iy, getCellDirect(ix, iy))) { + return true + } + } + } + + return false + } + + fun queryTileCollisions(aabb: AABB): MutableList { + val result = ObjectArrayList() // no CME checks + val tiles = aabb.encasingIntAABB() + + for (x in tiles.mins.x .. tiles.maxs.x) { + for (y in tiles.mins.y .. tiles.maxs.y) { + val cx = geometry.x.cell(x) + val cy = geometry.y.cell(y) + + val chunk = this[geometry.x.chunkFromCell(cx), geometry.y.chunkFromCell(cy)] ?: continue + chunk.getCollisions(cx - chunk.pos.tileX, cy - chunk.pos.tileY, result) + } + } + + return result + } + + fun collide(with: Poly, filter: Predicate): Stream { + return queryTileCollisions(with.aabb.enlarge(1.0, 1.0)).stream() + .filter(filter) + .map { with.intersect(it.poly) } + .filterNotNull() + } + + fun collide(point: Vector2d, filter: Predicate): Boolean { + return queryTileCollisions(AABB.withSide(point, 2.0)).any { filter.test(it) && point in it.poly } + } + + // ugly. + // but original code is way worse than this, because it considers A FUCKING RECTANGLE + // holy shiet + // we, on other hand, consider only tiles along line + fun collide(line: Line2d, filter: Predicate): LineCollisionResult? { + var found: LineCollisionResult? = null + + castRay(line.p0, line.p1) { cell, fraction, x, y, normal, borderX, borderY -> + val query = queryTileCollisions(AABB.withSide(Vector2d(borderX, borderY), 2.0)) + + for (poly in query) { + if (filter.test(poly)) { + val intersect = poly.poly.intersect(line) + + if (intersect != null) { + found = LineCollisionResult(poly, intersect.second.point!!, intersect.first) + return@castRay RayFilterResult.BREAK + } + } + } + + RayFilterResult.SKIP + } + + return found + } + + fun polyIntersects(with: Poly, filter: Predicate = Predicate { true }, tolerance: Double = 0.0): Boolean { + return collide(with, filter).anyMatch { it.penetration >= tolerance } + } + + fun polyIntersects(with: Poly, filter: Collection, tolerance: Double = 0.0): Boolean { + return polyIntersects(with, Predicate { it.type in filter }, tolerance) + } + + fun gravityAt(pos: IStruct2i): Vector2d { + val cell = getCell(pos) + return dungeonGravityInternal[cell.dungeonId] ?: template.worldParameters?.gravity ?: Globals.worldTemplate.defaultGravityVector + } + + fun gravityAt(pos: IStruct2d): Vector2d { + val cell = getCell(pos.component1().toInt(), pos.component2().toInt()) + return dungeonGravityInternal[cell.dungeonId] ?: template.worldParameters?.gravity ?: Globals.worldTemplate.defaultGravityVector + } + + fun isBreathable(pos: IStruct2i): Boolean { + val cell = getCell(pos) + return dungeonBreathableInternal.getOrDefault(cell.dungeonId, template.worldParameters?.airless != true) + } + + fun isBreathable(pos: IStruct2d): Boolean { + val cell = getCell(pos.component1().toInt(), pos.component2().toInt()) + return dungeonBreathableInternal.getOrDefault(cell.dungeonId, template.worldParameters?.airless != true) + } + + /** + * Positive - vector point along gravity's force (usually downwards) + */ + fun dotGravity(pos: IStruct2d, vector: IStruct2d): Double { + var gravity = gravityAt(pos) + + if (gravity.lengthSquared == 0.0) + gravity = Vector2d.POSITIVE_Y + + return -gravity.dot(vector) + } + + fun dotGravityRight(pos: IStruct2d, vector: IStruct2d): Double { + var gravity = gravityAt(pos).rotate(-PI / 2.0) + + if (gravity.lengthSquared == 0.0) + gravity = Vector2d.POSITIVE_X + + return gravity.dot(vector) + } } // around 30% slower than ArrayChunkMap, but can support insanely large worlds inner class SparseChunkMap : ChunkMap() { - private val map = Long2ObjectOpenHashMap() + private var hasSharedState = false + private var map = Long2ObjectOpenHashMap() // see CONCURRENT_SPARSE_CHUNK_MAP private val lock = Any() private val list = CopyOnWriteArrayList() + override fun snapshot(): SparseChunkMap { + hasSharedState = true + val new = SparseChunkMap() + new.map = map + new.hasSharedState = true + new.list.addAll(list) + return new + } + override fun get(x: Int, y: Int): ChunkType? { if (!geometry.x.inBoundsChunk(x) || !geometry.y.inBoundsChunk(y)) return null @@ -155,6 +352,12 @@ abstract class World, ChunkType : Chunk, ChunkType : Chunk, ChunkType : Chunk, ChunkType : Chunk, ChunkType : Chunk(divideUp(geometry.size.x, CHUNK_SIZE), divideUp(geometry.size.y, CHUNK_SIZE)) + private var hasSharedState = false + private var map = Object2DArray.nulls(divideUp(geometry.size.x, CHUNK_SIZE), divideUp(geometry.size.y, CHUNK_SIZE)) private val list = CopyOnWriteArrayList() + override fun snapshot(): ChunkMap { + hasSharedState = true + val new = ArrayChunkMap() + new.map = map + new.hasSharedState = true + new.list.addAll(list) + return new + } + override fun compute(x: Int, y: Int): ChunkType? { if (!geometry.x.inBoundsChunk(x) || !geometry.y.inBoundsChunk(y)) return null + + if (hasSharedState) { + map = Object2DArray(map) + hasSharedState = false + } + return map[x, y] ?: chunkFactory(ChunkPos(x, y)).also { list.add(it) map[x, y] = it @@ -226,9 +463,15 @@ abstract class World, ChunkType : Chunk, ChunkType : Chunk>() - var batch = ArrayList() + var batch = ArrayList() for (entity in dynamicEntities) { - batch.add(entity.movement) + batch.add(entity) if (batch.size == 32) { val b = batch - tasks.add(CompletableFuture.runAsync(Runnable { b.forEach { it.move(delta) } }, Starbound.EXECUTOR)) + tasks.add(CompletableFuture.runAsync(Runnable { b.forEach { if (!it.isRemote) it.move(delta) else it.moveRemote(delta) } }, Starbound.EXECUTOR)) batch = ArrayList() } } if (batch.isNotEmpty()) { - tasks.add(CompletableFuture.runAsync(Runnable { batch.forEach { it.move(delta) } }, Starbound.EXECUTOR)) + tasks.add(CompletableFuture.runAsync(Runnable { batch.forEach { if (!it.isRemote) it.move(delta) else it.moveRemote(delta) } }, Starbound.EXECUTOR)) } CompletableFuture.allOf(*tasks.toTypedArray()).join() @@ -475,164 +718,8 @@ abstract class World, ChunkType : Chunk } - fun interface CellPredicate { - fun test(x: Int, y: Int, cell: AbstractCell): Boolean - } - - fun anyCellSatisfies(aabb: AABBi, predicate: CellPredicate): Boolean { - for (x in aabb.mins.x .. aabb.maxs.x) { - for (y in aabb.mins.x .. aabb.maxs.x) { - val ix = geometry.x.cell(x) - val iy = geometry.x.cell(y) - - - if (predicate.test(ix, iy, chunkMap.getCellDirect(ix, iy))) { - return true - } - } - } - - return false - } - - fun anyCellSatisfies(aabb: AABB, predicate: CellPredicate): Boolean { - for (x in aabb.mins.x.toInt() .. aabb.maxs.x.roundToInt()) { - for (y in aabb.mins.y.toInt() .. aabb.maxs.y.roundToInt()) { - val ix = geometry.x.cell(x) - val iy = geometry.x.cell(y) - - if (predicate.test(ix, iy, chunkMap.getCellDirect(ix, iy))) { - return true - } - } - } - - return false - } - - fun anyCellSatisfies(x: Int, y: Int, distance: Int, predicate: CellPredicate): Boolean { - for (tx in (x - distance) .. (x + distance)) { - for (ty in (y - distance) .. (y + distance)) { - val ix = geometry.x.cell(tx) - val iy = geometry.y.cell(ty) - - if (predicate.test(ix, iy, chunkMap.getCellDirect(ix, iy))) { - return true - } - } - } - - return false - } - - fun queryTileCollisions(aabb: AABB): MutableList { - val result = ObjectArrayList() // no CME checks - val tiles = aabb.encasingIntAABB() - - for (x in tiles.mins.x .. tiles.maxs.x) { - for (y in tiles.mins.y .. tiles.maxs.y) { - val cx = geometry.x.cell(x) - val cy = geometry.y.cell(y) - - val chunk = chunkMap[geometry.x.chunkFromCell(cx), geometry.y.chunkFromCell(cy)] ?: continue - chunk.getCollisions(cx - chunk.pos.tileX, cy - chunk.pos.tileY, result) - } - } - - return result - } - - fun collide(with: Poly, filter: Predicate): Stream { - return queryTileCollisions(with.aabb.enlarge(1.0, 1.0)).stream() - .filter(filter) - .map { with.intersect(it.poly) } - .filterNotNull() - } - - fun collide(point: Vector2d, filter: Predicate): Boolean { - return queryTileCollisions(AABB.withSide(point, 2.0)).any { filter.test(it) && point in it.poly } - } - - data class LineCollisionResult(val poly: CollisionPoly, val border: Vector2d, val normal: Vector2d) - - // ugly. - // but original code is way worse than this, because it considers A FUCKING RECTANGLE - // holy shiet - // we, on other hand, consider only tiles along line - fun collide(line: Line2d, filter: Predicate): LineCollisionResult? { - var found: LineCollisionResult? = null - - castRay(line.p0, line.p1) { cell, fraction, x, y, normal, borderX, borderY -> - val query = queryTileCollisions(AABB.withSide(Vector2d(borderX, borderY), 2.0)) - - for (poly in query) { - if (filter.test(poly)) { - val intersect = poly.poly.intersect(line) - - if (intersect != null) { - found = LineCollisionResult(poly, intersect.second.point!!, intersect.first) - return@castRay RayFilterResult.BREAK - } - } - } - - RayFilterResult.SKIP - } - - return found - } - - fun polyIntersects(with: Poly, filter: Predicate = Predicate { true }, tolerance: Double = 0.0): Boolean { - return collide(with, filter).anyMatch { it.penetration >= tolerance } - } - - fun polyIntersects(with: Poly, filter: Collection, tolerance: Double = 0.0): Boolean { - return polyIntersects(with, Predicate { it.type in filter }, tolerance) - } - - fun gravityAt(pos: IStruct2i): Vector2d { - val cell = getCell(pos) - return dungeonGravityInternal[cell.dungeonId] ?: template.worldParameters?.gravity ?: Vector2d.ZERO - } - - fun gravityAt(pos: IStruct2d): Vector2d { - val cell = getCell(pos.component1().toInt(), pos.component2().toInt()) - return dungeonGravityInternal[cell.dungeonId] ?: template.worldParameters?.gravity ?: Vector2d.ZERO - } - - fun isBreathable(pos: IStruct2i): Boolean { - val cell = getCell(pos) - return dungeonBreathableInternal.getOrDefault(cell.dungeonId, template.worldParameters?.airless != true) - } - - fun isBreathable(pos: IStruct2d): Boolean { - val cell = getCell(pos.component1().toInt(), pos.component2().toInt()) - return dungeonBreathableInternal.getOrDefault(cell.dungeonId, template.worldParameters?.airless != true) - } - - data class LiquidLevel(val type: Registry.Entry, val average: Double) - fun averageLiquidLevel(rect: AABB): LiquidLevel? { - val liquidLevels = Object2DoubleOpenHashMap>() - var area = 0.0 - - anyCellSatisfies(rect) { x, y, cell -> - val blockIncidence = AABB.leftCorner(Vector2d(x.toDouble(), y.toDouble()), 1.0, 1.0).overlap(rect).volume - area += blockIncidence - - if (cell.liquid.state.isNotEmptyLiquid && cell.liquid.level > 0f) { - liquidLevels.put(cell.liquid.state, liquidLevels.getDouble(cell.liquid.state) + cell.liquid.level.coerceAtMost(1f) * blockIncidence) - } - - false - } - - if (liquidLevels.isEmpty()) { - return null - } - - val max = liquidLevels.object2DoubleEntrySet().maxBy { it.doubleValue }!! - return LiquidLevel(max.key, max.doubleValue / area) + return chunkMap.averageLiquidLevel(rect) } /** @@ -730,16 +817,49 @@ abstract class World, ChunkType : Chunk fun isPlayerModified(region: AABBi): Boolean { - return anyCellSatisfies(region) { _, _, cell -> + return chunkMap.anyCellSatisfies(region) { _, _, cell -> cell.dungeonId == ARTIFICIAL_DUNGEON_ID || cell.dungeonId == DESTROYED_DUNGEON_ID } } - fun environmentStatusEffects(pos: Vector2i): Set { + fun environmentStatusEffects(x: Int, y: Int): Collection { // TODO: judging by original code, they wanted to allow definition // of custom environmental effects per dungeon id // But it never happened - return template.worldParameters?.environmentStatusEffects ?: setOf() + return template.worldParameters?.environmentStatusEffects ?: listOf() + } + + fun environmentStatusEffects(x: Double, y: Double): Collection { + return environmentStatusEffects(x.toInt(), y.toInt()) + } + + fun environmentStatusEffects(pos: IStruct2i): Collection { + val (x, y) = pos + return environmentStatusEffects(x, y) + } + + fun environmentStatusEffects(pos: IStruct2d): Collection { + val (x, y) = pos + return environmentStatusEffects(x, y) + } + + fun weatherStatusEffects(x: Int, y: Int): Collection { + // TODO + return listOf() + } + + fun weatherStatusEffects(x: Double, y: Double): Collection { + return weatherStatusEffects(x.toInt(), y.toInt()) + } + + fun weatherStatusEffects(pos: IStruct2i): Collection { + val (x, y) = pos + return weatherStatusEffects(x, y) + } + + fun weatherStatusEffects(pos: IStruct2d): Collection { + val (x, y) = pos + return weatherStatusEffects(x, y) } abstract fun damageTiles(positions: Collection, isBackground: Boolean, sourcePosition: Vector2d, damage: TileDamage, source: AbstractEntity? = null): CompletableFuture diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt index e838a0e2..2b6e5afb 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt @@ -10,8 +10,6 @@ import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.io.nullable import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kstarbound.Starbound -import ru.dbotthepony.kstarbound.math.AABB -import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.client.StarboundClient import ru.dbotthepony.kstarbound.client.render.LayeredRenderer import ru.dbotthepony.kstarbound.client.world.ClientWorld @@ -22,28 +20,25 @@ import ru.dbotthepony.kstarbound.defs.DamageType import ru.dbotthepony.kstarbound.defs.EntityDamageTeam import ru.dbotthepony.kstarbound.defs.EntityType import ru.dbotthepony.kstarbound.defs.HitType -import ru.dbotthepony.kstarbound.defs.InteractAction -import ru.dbotthepony.kstarbound.defs.InteractRequest +import ru.dbotthepony.kstarbound.math.AABB +import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.network.packets.DamageNotificationPacket +import ru.dbotthepony.kstarbound.network.packets.DamageRequestPacket import ru.dbotthepony.kstarbound.network.packets.EntityCreatePacket import ru.dbotthepony.kstarbound.network.packets.EntityDestroyPacket import ru.dbotthepony.kstarbound.network.packets.HitRequestPacket -import ru.dbotthepony.kstarbound.network.packets.DamageRequestPacket -import ru.dbotthepony.kstarbound.network.packets.EntityMessagePacket import ru.dbotthepony.kstarbound.network.syncher.InternedStringCodec import ru.dbotthepony.kstarbound.network.syncher.MasterElement import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup import ru.dbotthepony.kstarbound.network.syncher.networkedData import ru.dbotthepony.kstarbound.server.world.ServerWorld -import ru.dbotthepony.kstarbound.world.LightCalculator import ru.dbotthepony.kstarbound.world.EntityIndex +import ru.dbotthepony.kstarbound.world.LightCalculator import ru.dbotthepony.kstarbound.world.TileRayFilter import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.castRay import ru.dbotthepony.kstarbound.world.physics.Poly import java.io.DataOutputStream -import java.util.PriorityQueue -import java.util.UUID import java.util.concurrent.CompletableFuture import java.util.concurrent.ScheduledFuture import java.util.concurrent.TimeUnit @@ -136,8 +131,6 @@ abstract class AbstractEntity : Comparable { } } - var description = "" - /** * Whenever this entity should be saved to disk when chunk containing it is being unloaded */ @@ -177,6 +170,9 @@ abstract class AbstractEntity : Comparable { */ abstract val metaBoundingBox: AABB + abstract val name: String + abstract val description: String + open val collisionArea: AABB get() = AABB.NEVER @@ -309,10 +305,15 @@ abstract class AbstractEntity : Comparable { } var isRemote: Boolean = false + val isLocal: Boolean + get() = !isRemote open val mouthPosition: Vector2d get() = position + open val feetPosition: Vector2d + get() = position + private fun isDamageAuthoritative(target: AbstractEntity): Boolean { // Damage manager is authoritative if either one of the entities is // masterOnly, OR the manager is server-side and both entities are diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ActorEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ActorEntity.kt index a8cd1157..080529c7 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ActorEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ActorEntity.kt @@ -1,24 +1,63 @@ package ru.dbotthepony.kstarbound.world.entities +import ru.dbotthepony.kommons.util.IStruct2d +import ru.dbotthepony.kstarbound.defs.DamageNotification import ru.dbotthepony.kstarbound.defs.Drawable import ru.dbotthepony.kstarbound.json.builder.IStringSerializable +import ru.dbotthepony.kstarbound.math.vector.Vector2d +import ru.dbotthepony.kstarbound.network.packets.DamageNotificationPacket +import ru.dbotthepony.kstarbound.network.packets.DamageRequestPacket +import ru.dbotthepony.kstarbound.network.packets.HitRequestPacket +import ru.dbotthepony.kstarbound.world.Direction +import ru.dbotthepony.kstarbound.world.World /** * Monsters, NPCs, Players */ -abstract class ActorEntity() : DynamicEntity() { +abstract class ActorEntity : DynamicEntity() { final override val movement: ActorMovementController = ActorMovementController() abstract val statusController: StatusController - enum class DamageBarType { - DEFAULT, NONE, SPECIAL + override fun move(delta: Double) { + statusController.applyMovementControls() + super.move(delta) } - abstract val health: Double - abstract val maxHealth: Double + // uint8_t + enum class DamageBarType(override val jsonName: String) : IStringSerializable { + DEFAULT("Default"), NONE("None"), SPECIAL("Special") + } + + open val health: Double + get() = statusController.resources["health"]!!.value + open val maxHealth: Double + get() = statusController.resources["health"]!!.maxValue!! + abstract val damageBarType: DamageBarType - abstract val name: String + override fun onJoinWorld(world: World<*, *>) { + super.onJoinWorld(world) + statusController.init(world) + } + + override fun tick(delta: Double) { + super.tick(delta) + + if (isLocal) + statusController.tick(delta) + else + statusController.tickRemote(delta) + } + + override fun hitOther(damage: HitRequestPacket) { + super.hitOther(damage) + statusController.notifyHitDealt(damage.target, damage.request) + } + + override fun damagedOther(notification: DamageNotificationPacket) { + super.damagedOther(notification) + statusController.notifyDamageDealt(notification.notification) + } enum class PortraitMode(override val jsonName: String) : IStringSerializable { HEAD("head"), @@ -29,6 +68,21 @@ abstract class ActorEntity() : DynamicEntity() { FULL_NEUTRAL_NUDE("fullneutralnude"); } + @Suppress("NAME_SHADOWING") + fun toAbsolutePosition(pos: Vector2d): Vector2d { + var pos = pos + + if (movement.facingDirection == Direction.LEFT) { + pos = pos.copy(x = -pos.x) + } + + if (movement.rotation != 0.0) { + pos = pos.rotate(movement.rotation) + } + + return movement.position + pos + } + open fun portrait(mode: PortraitMode): List { return emptyList() } 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 4323940b..c5ad9b99 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ActorMovementController.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ActorMovementController.kt @@ -1,15 +1,15 @@ package ru.dbotthepony.kstarbound.world.entities -import ru.dbotthepony.kommons.io.koptional -import ru.dbotthepony.kommons.util.KOptional +import ru.dbotthepony.kommons.io.nullable import ru.dbotthepony.kommons.util.getValue import ru.dbotthepony.kommons.util.setValue import ru.dbotthepony.kstarbound.Globals +import ru.dbotthepony.kstarbound.defs.actor.ActorMovementModifiers import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.defs.ActorMovementParameters import ru.dbotthepony.kstarbound.defs.JumpProfile import ru.dbotthepony.kstarbound.defs.MovementParameters -import ru.dbotthepony.kstarbound.defs.actor.player.ActorMovementModifiers +import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean import ru.dbotthepony.kstarbound.network.syncher.networkedData import ru.dbotthepony.kstarbound.network.syncher.networkedEnum @@ -56,8 +56,7 @@ class ActorMovementController() : MovementController() { var isLiquidMovement: Boolean by networkGroup.add(networkedBoolean()) private set - var anchorState by networkGroup.add(networkedData(KOptional(), AnchorState.CODEC.koptional(), AnchorState.LEGACY_CODEC.koptional())) - private set + var anchorState by networkGroup.add(networkedData(null, AnchorState.CODEC.nullable(), AnchorState.LEGACY_CODEC.nullable())) var controlJump: Boolean = false var controlJumpAnyway: Boolean = false @@ -94,7 +93,7 @@ class ActorMovementController() : MovementController() { var anchorEntity: DynamicEntity? = null - var pathController: PathController? = null + val pathController = PathController(this) var groundMovementSustainTimer: GameTimer = GameTimer(0.0) var reJumpTimer: GameTimer = GameTimer(0.0) var jumpHoldTimer: GameTimer? = null @@ -111,6 +110,46 @@ class ActorMovementController() : MovementController() { val positiveOnly: Boolean ) + @JsonFactory + data class SerializedData( + val position: Vector2d = Vector2d.ZERO, + val velocity: Vector2d = Vector2d.ZERO, + val rotation: Double = 0.0, + val movingDirection: Direction = Direction.RIGHT, + val facingDirection: Direction = Direction.RIGHT, + val crouching: Boolean = false, + ) + + fun serialize(): SerializedData { + return SerializedData( + position, velocity, rotation, movingDirection, facingDirection, isCrouching + ) + } + + fun deserialize(data: SerializedData) { + val (position, velocity, rotation, movingDirection, facingDirection, isCrouching) = data + this.position = position + this.velocity = velocity + this.rotation = rotation + this.movingDirection = movingDirection + this.facingDirection = facingDirection + this.isCrouching = isCrouching + } + + fun getAbsolutePosition(relative: Vector2d): Vector2d { + var relativePosition = relative + + if (facingDirection == Direction.LEFT) { + relativePosition *= Vector2d.NEGATIVE_X + } + + if (rotation != 0.0) { + relativePosition = relativePosition.rotate(rotation) + } + + return position + relativePosition + } + fun calculateMovementParameters(base: ActorMovementParameters): MovementParameters { val mass = base.mass val gravityMultiplier = base.gravityMultiplier @@ -200,18 +239,58 @@ class ActorMovementController() : MovementController() { updateParameters(calculateMovementParameters(actorMovementParameters)) } + // TODO: run is unsed + fun pathMove(position: Vector2d, run: Boolean, parameters: PathFinder.Parameters? = null): Pair? { + // FIXME: code flow is stupid + + // set new parameters if they have changed + if (pathController.endPosition == null || (parameters != null && pathController.parameters != parameters)) { + if (parameters != null) { + pathController.parameters = parameters + } + + pathMoveResult = pathController.findPath(position)?.let { position to it } + } else { + // update target position if it has changed + pathController.findPath(position) + } + + if (pathMoveResult != null) { + // path controller failed or succeeded, return the result and reset the controller + pathController.reset() + } + + val pathMoveResult = pathMoveResult + this.pathMoveResult = null + return pathMoveResult + } + + fun controlPathMove(position: Vector2d, run: Boolean, parameters: PathFinder.Parameters? = null): Pair? { + val result = pathMove(position, run, parameters) + + if (result == null) + controlPathMove = position to run + + return result + } + fun clearControls() { controlRotationRate = 0.0 controlAcceleration = Vector2d.ZERO controlForce = Vector2d.ZERO + controlMove = null controlRun = false controlCrouch = false controlJump = false controlJumpAnyway = false controlFly = null controlPathMove = null + controlFace = null controlActorMovementParameters = ActorMovementParameters.EMPTY controlMovementModifiers = ActorMovementModifiers.EMPTY + + approachVelocities.clear() + approachVelocityAngles.clear() } override fun move(delta: Double) { @@ -237,7 +316,7 @@ class ActorMovementController() : MovementController() { position = anchorEntity.position } else { val movementParameters = actorMovementParameters.merge(controlActorMovementParameters) - val movementModifiers = movementModifiers.combine(controlMovementModifiers) + val movementModifiers = movementModifiers.merge(controlMovementModifiers) if (movementModifiers.movementSuppressed) { controlMove = null @@ -255,14 +334,52 @@ class ActorMovementController() : MovementController() { if (controlPathMove != null && pathMoveResult == null) { if (appliedForceRegion) { - pathController?.reset() - } else if (!pathController!!.isPathfinding) { - // TODO: path move code - } else { + pathController.reset() + } else if (!pathController.isPathfinding) { + pathMoveResult = pathController.move(movementParameters, movementModifiers, controlPathMove!!.second, delta)?.let { controlPathMove!!.first to it } + isOnGround = false + val action = pathController.currentAction + + if (action != null) { + isWalking = action == PathFinder.Action.WALK && !controlPathMove!!.second + isRunning = action == PathFinder.Action.WALK && controlPathMove!!.second + isFlying = action == PathFinder.Action.FLY || action == PathFinder.Action.SWIM + isFalling = action == PathFinder.Action.ARC && world.chunkMap.dotGravity(position, velocity) >= 0.0 || action == PathFinder.Action.DROP + isJumping = action == PathFinder.Action.ARC && world.chunkMap.dotGravity(position, velocity) < 0.0 + + isOnGround = action.isOnGround + + if (action == PathFinder.Action.LAND || action == PathFinder.Action.JUMP) { + val inLiquid = liquidPercentage >= (movementParameters.minimumLiquidPercentage ?: 1.0) + isLiquidMovement = inLiquid + isGroundMovement = !inLiquid + isOnGround = !inLiquid && isOnGround + } else { + isLiquidMovement = action == PathFinder.Action.SWIM + isGroundMovement = action != PathFinder.Action.ARC && action != PathFinder.Action.SWIM + } + } else { + isWalking = false + isRunning = false + } + + facingDirection = controlFace ?: pathController.controlFace ?: facingDirection + movingDirection = pathController.controlFace ?: facingDirection + + updateMovementParameters(movementParameters) + + // MovementController still handles updating liquid percentage and updating force regions + updateLiquidPercentage() + updateForceRegions() + + clearControls() + return + } else { + pathMoveResult = pathController.findPath(controlPathMove!!.first)?.let { controlPathMove!!.first to it } } } else { - pathController = null + pathController.reset() } if (controlFly != null) @@ -340,8 +457,8 @@ class ActorMovementController() : MovementController() { if (isOnGround) { groundMovementSustainTimer = GameTimer(maxGroundSustain) } else if (!groundMovementSustainTimer.hasFinished && groundCheckDistance > 0.0 && maxGroundSustain - groundMovementSustainTimer.timer > minGroundSustain) { - val collideAny = computeLocalHitboxes() - .any { world.polyIntersects(it + Vector2d(0.0, -groundCheckDistance), { it.type >= CollisionType.PLATFORM }) } + val collideAny = computeGlobalHitboxes() + .any { world.chunkMap.polyIntersects(it + Vector2d(0.0, -groundCheckDistance), { it.type >= CollisionType.PLATFORM }) } if (collideAny) groundMovementSustainTimer = GameTimer(0.0) @@ -419,8 +536,8 @@ class ActorMovementController() : MovementController() { facingDirection = controlFace!! else if (updatedMovingDirection != null) facingDirection = updatedMovingDirection - else if (controlPathMove != null && pathController?.controlFace != null) - facingDirection = pathController?.controlFace!! + else if (controlPathMove != null && pathController.controlFace != null) + facingDirection = pathController.controlFace!! } isGroundMovement = !groundMovementSustainTimer.hasFinished diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/DynamicEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/DynamicEntity.kt index 1ff9bc8d..28067d9d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/DynamicEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/DynamicEntity.kt @@ -20,8 +20,22 @@ abstract class DynamicEntity() : AbstractEntity() { abstract val movement: MovementController + /** + * Called in multiple threads + */ + open fun move(delta: Double) { + movement.move(delta) + } + + /** + * Called in multiple threads + */ + open fun moveRemote(delta: Double) { + movement.tickRemote(delta) + } + override val collisionArea: AABB - get() = movement.computeCollisionAABB() + get() = movement.computeGlobalCollisionAABB() override fun onNetworkUpdate() { super.onNetworkUpdate() @@ -63,12 +77,12 @@ abstract class DynamicEntity() : AbstractEntity() { override fun render(client: StarboundClient, layers: LayeredRenderer) { layers.add(RenderLayer.Overlay.point()) { - val hitboxes = movement.computeLocalHitboxes() + val hitboxes = movement.computeGlobalHitboxes() if (hitboxes.isEmpty()) return@add hitboxes.forEach { it.render(client) } - world.queryTileCollisions( + world.chunkMap.queryTileCollisions( hitboxes.stream().map { it.aabb }.reduce(AABB::combine).get().enlarge(2.0, 2.0) ).filter(movement::shouldCollideWithBody).forEach { it.poly.render(client, BLOCK_COLLISION_COLOR) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/EffectEmitter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/EffectEmitter.kt index a39ef86e..ed90f611 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/EffectEmitter.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/EffectEmitter.kt @@ -1,23 +1,81 @@ package ru.dbotthepony.kstarbound.world.entities +import com.google.gson.JsonArray +import com.google.gson.JsonElement import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet -import ru.dbotthepony.kommons.io.BinaryStringCodec import ru.dbotthepony.kommons.io.StreamCodec import ru.dbotthepony.kommons.util.getValue import ru.dbotthepony.kommons.util.setValue -import ru.dbotthepony.kstarbound.network.syncher.InternedStringCodec +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.fromJsonFast +import ru.dbotthepony.kstarbound.io.readInternedString +import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup +import ru.dbotthepony.kstarbound.network.syncher.legacyCodec +import ru.dbotthepony.kstarbound.network.syncher.nativeCodec import ru.dbotthepony.kstarbound.network.syncher.networkedData -import java.util.function.Consumer +import ru.dbotthepony.kstarbound.world.Direction +import java.io.DataInputStream +import java.io.DataOutputStream class EffectEmitter(val entity: AbstractEntity) { val networkGroup = NetworkedGroup() // stoopid - var currentEffects by networkGroup.add(networkedData(setOf(), pairCodec)) + var currentEffects by networkGroup.add(networkedData(setOf(), pairCodec, pairLegacyCodec)) private set + var direction = Direction.RIGHT + + fun setSourcePosition(name: String, position: Vector2d) { + + } + + fun tick(isRemote: Boolean, visibleToRemotes: Boolean) { + + } + + @JsonFactory + data class ActiveSource( + val position: String, + val source: String + ) { + constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readInternedString(), stream.readInternedString()) + + fun write(stream: DataOutputStream, isLegacy: Boolean) { + + } + + companion object { + val CODEC = nativeCodec(::ActiveSource, ActiveSource::write) + val LEGACY_CODEC = legacyCodec(::ActiveSource, ActiveSource::write) + } + } + + fun serialize(): JsonElement { + return JsonArray(currentEffects.size).also { + for (data in currentEffects) { + it.add(Starbound.gson.toJsonTree(data)) + } + } + } + + fun deserialize(json: JsonElement) { + if (json !is JsonArray) + return + + val new = ObjectOpenHashSet() + + for (element in json) { + new.add(Starbound.gson.fromJsonFast(element, ActiveSource::class.java)) + } + + currentEffects = new + } + companion object { - private val pairCodec = StreamCodec.Collection(StreamCodec.Pair(InternedStringCodec, InternedStringCodec), ::ObjectOpenHashSet) as StreamCodec>> + private val pairCodec = StreamCodec.Collection(ActiveSource.CODEC, ::ObjectOpenHashSet) as StreamCodec> + private val pairLegacyCodec = StreamCodec.Collection(ActiveSource.LEGACY_CODEC, ::ObjectOpenHashSet) as StreamCodec> } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ItemDropEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ItemDropEntity.kt index 300d02f6..2e6e6e89 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ItemDropEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ItemDropEntity.kt @@ -51,6 +51,10 @@ class ItemDropEntity() : DynamicEntity() { var item by networkedItem().also { networkGroup.upstream.add(it) } private set + override val name: String + get() = item.shortDescription + override val description: String + get() = item.description var shouldNotExpire = false val age = RelativeClock() val ageItemsTimer = RelativeClock() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MonsterEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MonsterEntity.kt new file mode 100644 index 00000000..c91f2f9a --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MonsterEntity.kt @@ -0,0 +1,439 @@ +package ru.dbotthepony.kstarbound.world.entities + +import com.google.common.collect.ImmutableList +import com.google.common.collect.ImmutableMap +import com.google.common.collect.ImmutableSet +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import it.unimi.dsi.fastutil.objects.ObjectArraySet +import org.classdump.luna.ByteString +import org.classdump.luna.Table +import ru.dbotthepony.kommons.gson.get +import ru.dbotthepony.kommons.gson.set +import ru.dbotthepony.kommons.io.DoubleValueCodec +import ru.dbotthepony.kommons.io.FloatValueCodec +import ru.dbotthepony.kommons.io.map +import ru.dbotthepony.kommons.io.nullable +import ru.dbotthepony.kommons.util.Either +import ru.dbotthepony.kommons.util.getValue +import ru.dbotthepony.kommons.util.setValue +import ru.dbotthepony.kommons.util.value +import ru.dbotthepony.kstarbound.Registries +import ru.dbotthepony.kstarbound.Registry +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.ActorMovementParameters +import ru.dbotthepony.kstarbound.defs.DamageNotification +import ru.dbotthepony.kstarbound.defs.DamageSource +import ru.dbotthepony.kstarbound.defs.EntityDamageTeam +import ru.dbotthepony.kstarbound.defs.EntityType +import ru.dbotthepony.kstarbound.defs.HitType +import ru.dbotthepony.kstarbound.defs.InteractAction +import ru.dbotthepony.kstarbound.defs.InteractRequest +import ru.dbotthepony.kstarbound.defs.JumpProfile +import ru.dbotthepony.kstarbound.defs.PhysicsForceRegion +import ru.dbotthepony.kstarbound.defs.actor.StatModifier +import ru.dbotthepony.kstarbound.defs.actor.StatModifierType +import ru.dbotthepony.kstarbound.defs.actor.StatusControllerConfig +import ru.dbotthepony.kstarbound.defs.item.TreasurePoolDefinition +import ru.dbotthepony.kstarbound.defs.monster.MonsterVariant +import ru.dbotthepony.kstarbound.fromJsonFast +import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.lua.LuaEnvironment +import ru.dbotthepony.kstarbound.lua.LuaMessageHandlerComponent +import ru.dbotthepony.kstarbound.lua.LuaUpdateComponent +import ru.dbotthepony.kstarbound.lua.bindings.MovementControllerBindings +import ru.dbotthepony.kstarbound.lua.bindings.provideAnimatorBindings +import ru.dbotthepony.kstarbound.lua.bindings.provideConfigBindings +import ru.dbotthepony.kstarbound.lua.bindings.provideEntityBindings +import ru.dbotthepony.kstarbound.lua.from +import ru.dbotthepony.kstarbound.lua.get +import ru.dbotthepony.kstarbound.lua.set +import ru.dbotthepony.kstarbound.lua.tableMapOf +import ru.dbotthepony.kstarbound.lua.toJsonFromLua +import ru.dbotthepony.kstarbound.lua.userdata.BehaviorState +import ru.dbotthepony.kstarbound.math.AABB +import ru.dbotthepony.kstarbound.math.vector.Vector2d +import ru.dbotthepony.kstarbound.network.packets.DamageRequestPacket +import ru.dbotthepony.kstarbound.network.syncher.InternedStringCodec +import ru.dbotthepony.kstarbound.network.syncher.JsonElementCodec +import ru.dbotthepony.kstarbound.network.syncher.NetworkedList +import ru.dbotthepony.kstarbound.network.syncher.NetworkedMap +import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean +import ru.dbotthepony.kstarbound.network.syncher.networkedData +import ru.dbotthepony.kstarbound.network.syncher.networkedEnum +import ru.dbotthepony.kstarbound.network.syncher.networkedEventCounter +import ru.dbotthepony.kstarbound.network.syncher.networkedJsonElement +import ru.dbotthepony.kstarbound.network.syncher.networkedString +import ru.dbotthepony.kstarbound.util.random.MWCRandom +import ru.dbotthepony.kstarbound.util.random.random +import ru.dbotthepony.kstarbound.world.Direction +import ru.dbotthepony.kstarbound.world.World +import ru.dbotthepony.kstarbound.world.entities.api.InteractiveEntity +import ru.dbotthepony.kstarbound.world.entities.api.ScriptedEntity +import ru.dbotthepony.kstarbound.world.physics.Poly +import java.io.DataOutputStream + +class MonsterEntity(val variant: MonsterVariant, level: Double? = null) : ActorEntity(), ScriptedEntity, InteractiveEntity { + override val type: EntityType + get() = EntityType.MONSTER + + init { + team.accept(EntityDamageTeam(variant.commonParameters.damageTeamType, variant.commonParameters.damageTeam)) + } + + val lua = LuaEnvironment() + val luaUpdate = LuaUpdateComponent(lua) + val luaMovement = MovementControllerBindings(movement) + val luaMessages = LuaMessageHandlerComponent(lua) { toString() } + val animator = Animator(variant.animationConfig) + + init { + lua.globals["storage"] = lua.newTable() + } + + override fun move(delta: Double) { + luaMovement.apply() + super.move(delta) + } + + init { + for ((k, v) in variant.animatorPartTags) { + animator.setPartTag(k, "partImage", v) + } + + animator.zoom = variant.commonParameters.scale + + val colorSwap = variant.commonParameters.colorSwap + ?: Registries.monsterPalettes[variant.parameters.get("colors", "default")]?.value?.swaps?.random(MWCRandom(variant.seed.toULong(), 256, 32)) + + if (colorSwap != null) { + animator.processingDirectives = colorSwap.toImageOperator() + } + + movement.applyParameters(variant.commonParameters.movementSettings) + + movement.applyParameters(ActorMovementParameters( + standingPoly = variant.commonParameters.movementSettings.standingPoly?.flatMap({ it * variant.commonParameters.scale }, { it.stream().map { it * variant.commonParameters.scale }.collect(ImmutableList.toImmutableList()) }), + crouchingPoly = variant.commonParameters.movementSettings.crouchingPoly?.flatMap({ it * variant.commonParameters.scale }, { it.stream().map { it * variant.commonParameters.scale }.collect(ImmutableList.toImmutableList()) }), + walkSpeed = (movement.actorMovementParameters.walkSpeed ?: 0.0) * variant.commonParameters.walkMultiplier, + runSpeed = (movement.actorMovementParameters.runSpeed ?: 0.0) * variant.commonParameters.runMultiplier, + + airJumpProfile = JumpProfile( + jumpSpeed = (movement.actorMovementParameters.airJumpProfile.jumpSpeed ?: 0.0) * variant.commonParameters.jumpMultiplier, + ), + + liquidJumpProfile = JumpProfile( + jumpSpeed = (movement.actorMovementParameters.liquidJumpProfile.jumpSpeed ?: 0.0) * variant.commonParameters.jumpMultiplier, + ), + + mass = (movement.actorMovementParameters.mass ?: 0.0) * variant.commonParameters.weightMultiplier, + + physicsEffectCategories = if (movement.actorMovementParameters.physicsEffectCategories == null) ImmutableSet.of("monster") else null, + )) + + networkGroup.upstream.add(uniqueID) + networkGroup.upstream.add(team) + } + + @JsonFactory + data class SerializedData( + val movementState: ActorMovementController.SerializedData, + val statusController: StatusController.SerializedData, + val monsterLevel: Double? = null, + val damageOnTouch: Boolean, + val aggressive: Boolean, + val deathParticleBurst: String, + val deathSound: String, + val activeSkillName: String, + val dropPool: Either>, Registry.Ref>? = null, + val uniqueId: String? = null, + val team: EntityDamageTeam, + val scriptStorage: JsonElement = JsonNull.INSTANCE, + val effectEmitter: JsonElement = JsonNull.INSTANCE, + ) + + override fun deserialize(data: JsonObject) { + super.deserialize(data) + + val deserialized = Starbound.gson.fromJsonFast(data, SerializedData::class.java) + movement.deserialize(deserialized.movementState) + statusController.deserialize(deserialized.statusController) + this.monsterLevel = deserialized.monsterLevel + this.damageOnTouch = deserialized.damageOnTouch + this.isAggressive = deserialized.aggressive + this.deathParticlesBurst = deserialized.deathParticleBurst + this.deathSound = deserialized.deathSound + this.activeSkillName = deserialized.activeSkillName + this.dropPool = deserialized.dropPool + this.uniqueID.value = deserialized.uniqueId + this.team.value = deserialized.team + + this.lua.globals["storage"] = lua.from(deserialized.scriptStorage) + } + + override fun serialize(data: JsonObject) { + super.serialize(data) + + val moreData = SerializedData( + movementState = movement.serialize(), + statusController = statusController.serialize(), + monsterLevel = monsterLevel, + damageOnTouch = damageOnTouch, + aggressive = isAggressive, + deathParticleBurst = deathParticlesBurst, + deathSound, + activeSkillName, + dropPool, + uniqueID.value, + team.value, + effectEmitter = effectEmitter.serialize(), + scriptStorage = toJsonFromLua(lua.globals["storage"]), + ) + + for ((k, v) in Starbound.gson.toJsonTree(moreData).asJsonObject.entrySet()) { + data[k] = v + } + + data["monsterVariant"] = Starbound.gson.toJsonTree(variant) + } + + var monsterLevel by networkedData(level, DoubleValueCodec.nullable(), FloatValueCodec.map({ toDouble() }, { toFloat() }).nullable()).also { networkGroup.upstream.add(it) } + private set + var damageOnTouch by networkedBoolean().also { networkGroup.upstream.add(it) } + + val customDamageSources = NetworkedList(DamageSource.CODEC, DamageSource.LEGACY_CODEC).also { networkGroup.upstream.add(it) } + + var dropPool = variant.chosenDropPool + var isAggressive by networkedBoolean().also { networkGroup.upstream.add(it) } + var knockedOut by networkedBoolean().also { networkGroup.upstream.add(it) } + var knockoutTimer = 0.0 + private set + + var deathParticlesBurst by networkedString().also { networkGroup.upstream.add(it) } + var deathSound by networkedString().also { networkGroup.upstream.add(it) } + var activeSkillName by networkedString().also { networkGroup.upstream.add(it) } + var networkName by networkedData(null, InternedStringCodec.nullable()).also { networkGroup.upstream.add(it) } + + var displayNameTag by networkedBoolean().also { networkGroup.upstream.add(it) } + + init { + // why the fuck is this networked + // anyway. + /* val dropPool =*/ networkedJsonElement().also { networkGroup.upstream.add(it) } + } + + val forceRegions = NetworkedList(PhysicsForceRegion.CODEC, PhysicsForceRegion.LEGACY_CODEC).also { networkGroup.upstream.add(it) } + override val statusController: StatusController = StatusController(this, variant.commonParameters.statusSettings ?: StatusControllerConfig.EMPTY) + + init { + networkGroup.upstream.add(animator.networkGroup) + networkGroup.upstream.add(movement.networkGroup) + networkGroup.upstream.add(statusController) + } + + val effectEmitter = EffectEmitter(this).also { networkGroup.upstream.add(it.networkGroup) } + + val newChatMessageEvent = networkedEventCounter().also { networkGroup.upstream.add(it) } + var chatMessage by networkedString().also { networkGroup.upstream.add(it) } + var chatPortrait by networkedString().also { networkGroup.upstream.add(it) } + + // despite being networked as data on original engine, it is 1 byte wide + override var damageBarType: DamageBarType by networkedEnum(DamageBarType.DEFAULT).also { networkGroup.upstream.add(it) } + override var isInteractive by networkedBoolean().also { networkGroup.upstream.add(it) } + + // don't interpolate scripted animation parameters or animationdamageparts + val animationDamageParts = NetworkedList(InternedStringCodec).also { networkGroup.upstream.add(it, propagateInterpolation = false) } + val scriptedAnimationParameters = NetworkedMap(InternedStringCodec, JsonElementCodec).also { networkGroup.upstream.add(it, propagateInterpolation = false) } + + fun addChatMessage(message: String, portrait: String? = null) { + chatMessage = message + chatPortrait = portrait ?: "" + newChatMessageEvent.trigger() + } + + override val isPersistent: Boolean + get() = variant.commonParameters.persistent + + override fun writeNetwork(stream: DataOutputStream, isLegacy: Boolean) { + variant.write(stream, isLegacy) + } + + override fun onJoinWorld(world: World<*, *>) { + super.onJoinWorld(world) + + monsterLevel = monsterLevel ?: world.template.threatLevel + + if (!isRemote) { + val healthMultiplier = (variant.commonParameters.healthLevelFunction.value?.evaluate(monsterLevel!!) ?: 1.0) * variant.commonParameters.healthMultiplier + statusController.setPersistentEffects("innate", listOf(Either.left(StatModifier("maxHealth", healthMultiplier, StatModifierType.BASE_MULTIPLICATION)))) + + provideEntityBindings(this, lua) + provideAnimatorBindings(animator, lua) + + provideConfigBindings(lua) { key, default -> + key.find(variant.parameters) ?: default + } + + lua.attach(variant.commonParameters.scripts) + luaUpdate.stepCount = variant.commonParameters.initialScriptDelta + + BehaviorState.provideBindings(lua) + + luaMovement.init(lua) + lua.init() + } + } + + override val metaBoundingBox: AABB + get() = variant.commonParameters.metaBoundBox + + override val mouthPosition: Vector2d + get() = movement.getAbsolutePosition(variant.commonParameters.mouthOffset) + + override val feetPosition: Vector2d + get() = movement.getAbsolutePosition(variant.commonParameters.feetOffset) + + override val name: String + get() = networkName ?: variant.shortDescription ?: "" + + override val description: String + get() = variant.description ?: "Some indescribable horror" + + override fun queryHit(source: DamageSource, attacker: AbstractEntity?, inflictor: AbstractEntity?): HitType? { + if (knockedOut || statusController.statPositive("invulnerable")) + return null + + if (source.intersect(world.geometry, damageHitbox)) + return HitType.HIT + + return null + } + + override val damageHitbox: List + get() = listOf(variant.commonParameters.selfDamagePoly.rotate(movement.rotation) + position) + + private val deathDamageKinds = ObjectArraySet() + + override fun experienceDamage(damage: DamageRequestPacket): List { + val notifications = statusController.experienceDamage(damage.request) + val totalDamage = notifications.sumOf { it.healthLost } + + if (totalDamage > 0.0) { + lua.invokeGlobal("damage", lua.tableMapOf( + "sourceId" to damage.request.sourceEntityId, + "damage" to totalDamage, + "sourceDamage" to damage.request.damage, + "sourceKind" to damage.request.kind + )) + } + + if (health <= 0.0) { + deathDamageKinds.add(damage.request.kind) + } + + return notifications + } + + 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 + } + + override fun tick(delta: Double) { + super.tick(delta) + + if (isLocal) { + animator.isFlipped = (movement.facingDirection == Direction.LEFT) != variant.reversed + + if (knockedOut) { + knockoutTimer -= delta + + if (knockoutTimer <= 0.0) { + val dropPool = if (dropPool == null) null else if (dropPool!!.isRight) { + dropPool!!.right() + } else { + // Check to see whether any of the damage types that were used to cause + // death are in the damage pool map, if so spawn treasure from that, + // otherwise set the treasure pool to the "default" entry. + + val map = dropPool!!.left() + deathDamageKinds.firstNotNullOfOrNull { map[it] } ?: map["default"] + } + + if (dropPool?.value != null) { + for (stack in dropPool.value!!.evaluate(world.random, monsterLevel ?: world.template.threatLevel)) { + val entity = ItemDropEntity(stack) + entity.movement.position = movement.computeGlobalHitboxes().stream().map { it.centre }.reduce { t, u -> (t + u) / 2.0 }.orElse(position) + entity.movement.velocity = movement.velocity + entity.joinWorld(world) + } + } + + remove(RemovalReason.DYING) + return + } + } else { + luaUpdate.update(delta) { + luaMovement.clearControlsIfNeeded() + forceRegions.clear() + } + + if (shouldDie) { + knockedOut = true + knockoutTimer = variant.commonParameters.knockoutTime + damageOnTouch = false + + if (!variant.commonParameters.knockoutEffect.isNullOrBlank()) { + animator.setEffectActive(variant.commonParameters.knockoutEffect, true) + } + + for ((k, v) in variant.commonParameters.knockoutAnimationStates) { + animator.setActiveState(k, v) + } + } + } + } + + effectEmitter.setSourcePosition("normal", position) + effectEmitter.setSourcePosition("mouth", mouthPosition) + effectEmitter.setSourcePosition("feet", mouthPosition) + effectEmitter.direction = movement.facingDirection + effectEmitter.tick(isRemote, visibleToRemotes) + + if (world.isServer) { + animator.tick(delta, world.random) + } + } + + override fun handleMessage(connection: Int, message: String, arguments: JsonArray): JsonElement? { + return luaMessages.handle(message, connection == connectionID, arguments) ?: statusController.handleMessage(message, connection == connectionID, arguments) + } + + override fun callScript(fnName: String, vararg arguments: Any?): Array { + return lua.invokeGlobal(fnName, *arguments) + } + + override fun evalScript(code: String): Array { + return lua.eval(code) + } + + override fun interact(request: InteractRequest): InteractAction { + val result = lua.invokeGlobal("interact", lua.tableMapOf( + "sourceId" to request.source, + "sourcePosition" to lua.from(request.sourcePos) + )) + + if (result.isEmpty() || result[0] == null) + return InteractAction.NONE + + val value = result[0] + + if (value is ByteString) + return InteractAction(value.decode(), entityID) + + value as Table + return InteractAction((value[1L] as ByteString).decode(), entityID, toJsonFromLua(value[2L])) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt index 243adb8f..0b05c751 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt @@ -6,7 +6,6 @@ import ru.dbotthepony.kommons.io.FloatValueCodec import ru.dbotthepony.kommons.io.StreamCodec import ru.dbotthepony.kommons.io.koptional import ru.dbotthepony.kommons.io.map -import ru.dbotthepony.kommons.math.linearInterpolation import ru.dbotthepony.kstarbound.math.AABB import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.util.Listenable @@ -15,7 +14,9 @@ import ru.dbotthepony.kommons.util.setValue import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.math.vector.times import ru.dbotthepony.kstarbound.Globals +import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.defs.MovementParameters +import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition import ru.dbotthepony.kstarbound.math.Interpolator import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean @@ -53,7 +54,7 @@ open class MovementController { world0 = null } - fun computeLocalHitboxes(): List { + fun computeGlobalHitboxes(): List { val poly = movementParameters.collisionPoly ?: return listOf() if (poly.isLeft) { @@ -69,27 +70,31 @@ open class MovementController { } } - fun computeCollisionAABB(): AABB { - val poly = movementParameters.collisionPoly ?: return AABB.ZERO + position + fun computeGlobalCollisionAABB(): AABB { + return computeLocalCollisionAABB() + position + } + + fun computeLocalCollisionAABB(): AABB { + val poly = movementParameters.collisionPoly ?: return AABB.ZERO if (poly.isLeft) { if (poly.left().isEmpty) - return AABB.ZERO + position + return AABB.ZERO - return (poly.left().rotate(rotation) + position).aabb + return poly.left().rotate(rotation).aabb } else { if (poly.right().isEmpty()) - return AABB.ZERO + position + return AABB.ZERO else if (poly.right().first().isEmpty) - return AABB.ZERO + position + return AABB.ZERO - var build = (poly.right().first().rotate(rotation) + position).aabb + var build = poly.right().first().rotate(rotation).aabb for (i in 1 until poly.right().size) { val gPoly = poly.right()[i] if (gPoly.isNotEmpty) - build = build.combine((gPoly.rotate(rotation) + position).aabb) + build = build.combine(gPoly.rotate(rotation).aabb) } return build @@ -110,10 +115,10 @@ open class MovementController { var mass by networkGroup.add(networkedFloat()) - private var xPosition by networkGroup.add(networkedFixedPoint(0.0125)) - private var yPosition by networkGroup.add(networkedFixedPoint(0.0125)) - private var xVelocity by networkGroup.add(networkedFixedPoint(0.00625).also { it.interpolator = Interpolator.Linear }) - private var yVelocity by networkGroup.add(networkedFixedPoint(0.00625).also { it.interpolator = Interpolator.Linear }) + var xPosition by networkGroup.add(networkedFixedPoint(0.0125)) + var yPosition by networkGroup.add(networkedFixedPoint(0.0125)) + var xVelocity by networkGroup.add(networkedFixedPoint(0.00625).also { it.interpolator = Interpolator.Linear }) + var yVelocity by networkGroup.add(networkedFixedPoint(0.00625).also { it.interpolator = Interpolator.Linear }) var rotation by networkGroup.add(networkedFixedPoint(0.01).also { it.interpolator = Interpolator.Linear }) @@ -146,7 +151,7 @@ open class MovementController { if (poly.isLeft && poly.left().isEmpty) return if (poly.isRight && poly.right().none { it.isNotEmpty }) return - val localHitboxes = computeLocalHitboxes() + val localHitboxes = computeGlobalHitboxes() while (fixtures.size > localHitboxes.size) { fixtures.last().remove() @@ -195,6 +200,10 @@ open class MovementController { var gravityMultiplier = 1.0 var isGravityDisabled = false + open fun isAtWorldLimit(bottomOnly: Boolean = false): Boolean { + return false // TODO + } + var velocity: Vector2d get() = Vector2d(xVelocity, yVelocity) set(value) { @@ -203,12 +212,15 @@ open class MovementController { } fun determineGravity(): Vector2d { - if (isZeroGravity || isGravityDisabled) + if (isGravityDisabled) return Vector2d.ZERO - return world.gravityAt(position) + return world.chunkMap.gravityAt(position) } + var liquid: Registry.Entry? = null + private set + open fun updateLiquidPercentage() { } @@ -263,7 +275,7 @@ open class MovementController { } /** - * this function is executed in parallel + * Called in multiple threads */ // TODO: Ghost collisions occur, where objects trip on edges open fun move(delta: Double) { @@ -312,7 +324,7 @@ open class MovementController { val maximumPlatformCorrection = (movementParameters.maximumPlatformCorrection ?: Double.POSITIVE_INFINITY) + (movementParameters.maximumPlatformCorrectionVelocityFactor ?: 0.0) * velocityMagnitude - val localHitboxes = computeLocalHitboxes() + val localHitboxes = computeGlobalHitboxes() if (localHitboxes.isEmpty()) return // whut @@ -326,7 +338,7 @@ open class MovementController { var queryBounds = aabb.enlarge(maximumCorrection, maximumCorrection) queryBounds = queryBounds.combine(queryBounds + movement) - val polies = world.queryTileCollisions(queryBounds) + val polies = world.chunkMap.queryTileCollisions(queryBounds) polies.removeIf { !shouldCollideWithBody(it) } val results = ArrayList(localHitboxes.size) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PathController.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PathController.kt index b2a3fe87..7e762160 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PathController.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PathController.kt @@ -1,10 +1,23 @@ package ru.dbotthepony.kstarbound.world.entities +import com.google.gson.JsonArray +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.actor.ActorMovementModifiers +import ru.dbotthepony.kstarbound.defs.ActorMovementParameters +import ru.dbotthepony.kstarbound.math.AABB import ru.dbotthepony.kstarbound.math.vector.Vector2d +import ru.dbotthepony.kstarbound.util.supplyAsync import ru.dbotthepony.kstarbound.world.Direction -import ru.dbotthepony.kstarbound.world.World +import ru.dbotthepony.kstarbound.world.entities.api.ScriptedEntity +import ru.dbotthepony.kstarbound.world.physics.CollisionType +import ru.dbotthepony.kstarbound.world.physics.Poly +import java.util.concurrent.CompletableFuture +import java.util.function.Predicate +import kotlin.math.PI +import kotlin.math.min +import kotlin.math.roundToInt -class PathController(val world: World<*, *>, var edgeTimer: Double = 0.0) { +class PathController(val controller: ActorMovementController, var edgeTimer: Double = 0.0) { var startPosition: Vector2d? = null private set var endPosition: Vector2d? = null @@ -12,13 +25,320 @@ class PathController(val world: World<*, *>, var edgeTimer: Double = 0.0) { var controlFace: Direction? = null private set + val world get() = controller.world + fun reset() { edgeTimer = 0.0 startPosition = null endPosition = null controlFace = null + path.clear() + pathFinder?.signalStop() + pathFinder = null + pathFinderResult = null + target = null + } + + private var pathFinder: PathFinder? = null + private var pathFinderResult: CompletableFuture?>? = null + private val path = ArrayDeque() + private var target: Vector2d? = null + var parameters = PathFinder.Parameters() + + val currentEdge: PathFinder.Edge? + get() = path.firstOrNull() + + val currentAction: PathFinder.Action? + get() = currentEdge?.action + + private fun onGround(position: Vector2d, type: Predicate): Boolean { + val box = controller.computeGlobalCollisionAABB() + + return controller.world.chunkMap.anyCellSatisfies(AABB(box.mins - Vector2d.POSITIVE_Y, box.mins)) + { _, _, cell -> type.test(cell.foreground.material.value.collisionKind) } + } + + private fun updatePathFinding(): Boolean? { + if (this.pathFinderResult != null && this.pathFinderResult!!.isDone) { + val pathFinder = this.pathFinderResult!! + this.pathFinder = null + this.pathFinderResult = null + val newPath = pathFinder.get() + + if (newPath == null) { + reset() + return false + } + + var newEdgeTimer = 0.0 + var merged = false + + // if we have a path already, see if our paths can be merged either by splicing or fast forwarding + if (this.path.isNotEmpty() && newPath.isNotEmpty()) { + // try to fast-forward + val firstNode = this.path[0] + var ffIndex = newPath.indexOfFirst { firstNode.isSame(it) } + + if (ffIndex != -1) { + merged = true + this.path.clear() + + for (i in ffIndex until newPath.size) { + this.path.add(newPath[i]) + } + } else { + // try to splice the new path onto the current path + ffIndex = this.path.indexOfFirst { path[0].source == it.target } + + if (ffIndex != -1) { + // splice the new path onto our current path up to this index + merged = true + newEdgeTimer = this.edgeTimer + + while (this.path.size > ffIndex) { + this.path.removeLast() + } + + this.path.addAll(newPath) + } + } + } + + if (!merged && controller.position != this.startPosition) { + // merging the paths failed, and the entity has moved from the path start position + // try to bridge the gap from the current position to the new path + val bridgePathFinder = PathFinder(this, this.startPosition ?: controller.position) + bridgePathFinder.run(controller.actorMovementParameters.pathExploreRate?.roundToInt() ?: 100) + val bridgePath = bridgePathFinder.result.orNull() + + if (bridgePath == null) { + // if the gap isn't bridged in a single tick, reset and start over + reset() + return null + } else { + // concatenate the bridge path with the new path + this.path.clear() + this.path.addAll(bridgePath) + this.path.addAll(newPath) + } + } + + if (this.path.isNotEmpty() && !validateEdge(this.path[0])) { + // reset if the first edge is invalid + reset() + return false + } + + this.edgeTimer = newEdgeTimer + return this.path.isEmpty() + } else { + return null + } + } + + fun findPath(target: Vector2d): Boolean? { + // reached the end of the last path and we have a new target position to move toward + + if (path.isEmpty() && controller.world.geometry.diff(this.target!!, target).lengthSquared > 0.1) { + reset() + this.target = target + } + + // starting a new path, or the target position moved by more than 2 blocks + if (this.target == null || (this.path.isEmpty() && this.pathFinder == null) || controller.world.geometry.diff(this.target!!, target).lengthSquared > 4.0) { + var grounded = controller.isOnGround + + // if already moving on a path, collision will be disabled and we can't use MovementController::onGround() to check for ground collision + if (this.path.isNotEmpty()) { + grounded = onGround(controller.position) { it.isFloorCollision } + } + + if (controller.actorMovementParameters.gravityEnabled == true && !grounded && !controller.isLiquidMovement) { + return null + } + + // interrupt ongoing pathfinding + this.pathFinder?.signalStop() + this.startPosition = controller.position + this.target = target + this.pathFinder = PathFinder(this, target) + this.pathFinderResult = Starbound.EXECUTOR.supplyAsync(this.pathFinder!!) + } + + if (this.pathFinderResult == null && this.path.isEmpty()) + return true // Reached goal + + return updatePathFinding() + } + + private fun openDoors(bounds: AABB): Boolean { + val entities = world.entityIndex + .query(bounds, Predicate { + it is ScriptedEntity && !it.isRemote + }) + + // preserve original engine behavior where it will open ALL potential doors on Actor's path + var any = false + + for (it in entities) { + val scripted = it as ScriptedEntity + val result = scripted.callScript("hasCapability", "closedDoor") + + if (result.isNotEmpty() && result[0] is Boolean && result[0] as Boolean) { + it.dispatchMessage(world.connectionID, "openDoor", JsonArray()) + any = true + } + } + + return any + } + + private fun movingCollision(poly: Poly): Boolean { + // TODO + return false + } + + private fun inLiquid(position: Vector2d): Boolean { + val liquidLevel = world.chunkMap.averageLiquidLevel(controller.computeGlobalCollisionAABB() + position) ?: return false + return liquidLevel.average >= (controller.actorMovementParameters.minimumLiquidPercentage ?: 1.0) + } + + private fun validateEdge(edge: PathFinder.Edge): Boolean { + val polies = controller.actorMovementParameters.standingPoly?.map({ listOf(it + edge.target.position) }, { it.map { it + edge.target.position } }) ?: listOf() + + for (poly in polies) { + if (world.chunkMap.collide(poly, Predicate { it.type.isSolidCollision }).findAny().isPresent || movingCollision(poly)) { + if ( + // check if we can't pass through because something blocks our path + world.chunkMap.anyCellSatisfies(poly.aabb) { _, _, cell -> cell.foreground.material.value.collisionKind.isSolidCollision } && + // AND check if we CAN pass through, if DYNAMIC tiles were to be removed + !world.chunkMap.anyCellSatisfies(poly.aabb) { _, _, cell -> cell.foreground.material.value.collisionKind.isSolidNonDynamic } + ) { + if (!openDoors(poly.aabb)) { + return false + } + } else { + return false + } + } + } + + return when (edge.action) { + PathFinder.Action.WALK -> onGround(edge.source.position, Predicate { it.isFloorCollision }) + PathFinder.Action.DROP -> onGround(edge.target.position, Predicate { it.isFloorCollision }) && !onGround(edge.target.position, Predicate { it.isSolidCollision }) + PathFinder.Action.SWIM -> inLiquid(edge.target.position) + PathFinder.Action.LAND -> onGround(edge.target.position, Predicate { it.isFloorCollision }) || inLiquid(edge.target.position) + else -> true + } + } + + fun move(parameters: ActorMovementParameters, modifiers: ActorMovementModifiers, run: Boolean, dt: Double): Boolean? { + // pathfind to a new target position in the background while moving on the current path + updatePathFinding() + + if (path.isEmpty()) + return null + + controlFace = null + + while (path.isNotEmpty()) { + val edge = path.first() + val delta = world.geometry.diff(edge.target.position, edge.source.position) + + var sourceVelocity: Vector2d = Vector2d.ZERO + var targetVelocity: Vector2d = Vector2d.ZERO + + when (edge.action) { + PathFinder.Action.WALK -> { + sourceVelocity = delta.unitVector * (if (run) parameters.runSpeed ?: 0.0 else parameters.walkSpeed ?: 0.0) * modifiers.speedModifier + targetVelocity = sourceVelocity + } + + PathFinder.Action.JUMP -> { + if (modifiers.jumpingSuppressed) { + reset() + return null + } + } + + PathFinder.Action.ARC -> { + sourceVelocity = edge.source.velocity ?: Vector2d.ZERO + targetVelocity = edge.target.velocity ?: Vector2d.ZERO + } + + PathFinder.Action.DROP -> { + targetVelocity = edge.target.velocity ?: Vector2d.ZERO + } + + PathFinder.Action.SWIM -> { + targetVelocity = delta.unitVector * (parameters.flySpeed ?: 0.0) * (1.0 - (parameters.liquidImpedance ?: 0.0)) + sourceVelocity = targetVelocity + } + + PathFinder.Action.FLY -> { + // accelerate along path using airForce + val angleFactor = (controller.velocity.unitVector * delta.unitVector).toAngle() + val speedAlongAngle = angleFactor * controller.velocity.length + var acc = (parameters.airForce ?: 0.0) / controller.mass + + if (acc.isInfinite() || acc.isNaN()) + acc = 1.0 + + sourceVelocity = delta.unitVector * min(parameters.flySpeed ?: 0.0, speedAlongAngle + acc * dt) + targetVelocity = sourceVelocity + } + + PathFinder.Action.LAND -> {} + } + + val avgVelocity = (sourceVelocity + targetVelocity) / 2.0 + val avgSpeed = avgVelocity.length + val edgeTime = if (avgSpeed > 0.0) delta.length / avgSpeed else 0.2 + val edgeProgress = edgeTimer / edgeTime + + // original: if (edgeProgress > 1.0f) + if (edgeProgress >= 1.0) { + edgeTimer -= edgeTime + path.removeFirst() + + if (path.isNotEmpty() && !validateEdge(path.first())) { + reset() + return null + } + + continue + } + + controller.velocity = sourceVelocity + (targetVelocity - sourceVelocity) * edgeProgress + controller.position = edge.source.position + (controller.velocity + sourceVelocity) / 2.0 * edgeTimer + + var gravity = world.chunkMap.gravityAt(controller.position) + + if (gravity.lengthSquared == 0.0) { + // zero-g, assume left/right as gravity was pointing straight downwards + gravity = Vector2d.POSITIVE_Y + } + + // rotate to right, dot product with delta + val dot = gravity.rotate(PI / -2.0).dot(delta) + + if (dot > 0.0) { + controlFace = Direction.RIGHT + } else if (dot < 0.0) { + controlFace = Direction.LEFT + } + + edgeTimer += dt + return null + } + + // reached the end of the path, success unless we're also currently pathfinding to a new position + if (pathFinder != null) + return null + + return true } val isPathfinding: Boolean - get() = TODO() + get() = path.isEmpty() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PathFinder.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PathFinder.kt new file mode 100644 index 00000000..f33cded2 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PathFinder.kt @@ -0,0 +1,716 @@ +package ru.dbotthepony.kstarbound.world.entities + +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet +import org.apache.logging.log4j.LogManager +import org.classdump.luna.Table +import org.classdump.luna.TableFactory +import ru.dbotthepony.kommons.collect.map +import ru.dbotthepony.kommons.collect.reduce +import ru.dbotthepony.kommons.util.KOptional +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.ActorMovementParameters +import ru.dbotthepony.kstarbound.json.builder.IStringSerializable +import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.lua.from +import ru.dbotthepony.kstarbound.lua.set +import ru.dbotthepony.kstarbound.lua.tableOf +import ru.dbotthepony.kstarbound.lua.toByteString +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.CarriedExecutor +import ru.dbotthepony.kstarbound.util.supplyAsync +import ru.dbotthepony.kstarbound.world.World +import ru.dbotthepony.kstarbound.world.physics.CollisionType +import java.util.PriorityQueue +import java.util.function.Supplier +import kotlin.math.PI +import kotlin.math.absoluteValue +import kotlin.math.min +import kotlin.math.roundToInt +import kotlin.math.sign +import kotlin.math.sqrt + +class PathFinder(val world: World<*, *>, val start: Vector2d, val goal: Vector2d, val actorMovementParameters: ActorMovementParameters, val parameters: Parameters) : Supplier?> { + constructor(controller: PathController, goal: Vector2d) : this(controller.world, controller.startPosition!!, goal, controller.controller.actorMovementParameters, controller.parameters) + + // make snapshot of current things + val chunkMap = world.chunkMap.snapshot() + + enum class Action(val isOnGround: Boolean, override val jsonName: String) : IStringSerializable { + WALK(true, "Walk"), + JUMP(true, "Jump"), + ARC(false, "Arc"), + DROP(true, "Drop"), + SWIM(false, "Swim"), + FLY(false, "Fly"), + LAND(false, "Land"); + + val luaName = jsonName.toByteString()!! + } + + data class Edge( + var cost: Double, + var action: Action, + var velocity: Vector2d, + val source: Node, + val target: Node + ) { + fun isSame(other: Edge): Boolean { + return action == other.action && source == other.source && target == other.target + } + } + + private fun defaultCostEdge(action: Action, source: Node, target: Node): Edge { + return Edge( + action = action, + source = source, + target = target, + cost = world.geometry.diff(source.position, target.position).length, + velocity = Vector2d.ZERO + ) + } + + private enum class BoundingBoxType { + FULL, DROP, STAND + } + + private val standingBox by lazy { + parameters.boundBox + ?: (actorMovementParameters.standingPoly?.map({ it.aabb }, { it.iterator().map { it.aabb }.reduce(AABB::combine).orNull() }) ?: AABB.withSide(Vector2d.ZERO, 0.5)) + } + + private fun boundingBox(pos: Vector2d, type: BoundingBoxType = BoundingBoxType.FULL): AABB { + var bbox = if (type == BoundingBoxType.DROP && parameters.droppingBoundBox != null) + parameters.droppingBoundBox + else if (type == BoundingBoxType.STAND && parameters.standingBoundBox != null) + parameters.standingBoundBox + else standingBox + + bbox *= 0.99 + bbox += pos + return bbox + } + + private fun inLiquid(pos: Vector2d): Boolean { + return (chunkMap.averageLiquidLevel(boundingBox(pos))?.average ?: 0.0) >= (actorMovementParameters.minimumLiquidPercentage ?: 0.5) + } + + private fun groundCollisionRect(pos: Vector2d, type: BoundingBoxType = BoundingBoxType.FULL): AABBi { + val bounds = AABBi.of(boundingBox(pos, type)) + return AABBi(bounds.mins, bounds.mins) + } + + private fun onGround(pos: Vector2d, type: BoundingBoxType = BoundingBoxType.FULL): Boolean { + val groundRect = groundCollisionRect(pos, type) + + // Check there is something under the feet. + // We allow walking over the tops of objects (e.g. trapdoors) without being + // able to float inside objects. + if (chunkMap.anyCellSatisfies(boundingBox(pos, type)) { _, _, cell -> cell.foreground.material.value.collisionKind == CollisionType.DYNAMIC }) { + // We're inside an object. Don't collide with object directly below our + // feet: + return chunkMap.anyCellSatisfies(groundRect) { _, _, cell -> cell.foreground.material.value.collisionKind.isFloorCollision } + } + + // Not inside an object, allow colliding with objects below our feet: + // We need to be for sure above platforms, but can be up to a full tile + // below the top of solid blocks because rounded collision polys + return chunkMap.anyCellSatisfies(groundRect) { _, _, cell -> cell.foreground.material.value.collisionKind != CollisionType.NONE } || + chunkMap.anyCellSatisfies(groundRect - Vector2i.POSITIVE_Y) { _, _, cell -> cell.foreground.material.value.collisionKind.isSolidNonDynamic } + } + + private fun onSolidGround(pos: Vector2d): Boolean { + return chunkMap.anyCellSatisfies(groundCollisionRect(pos, BoundingBoxType.DROP)) { _, _, cell -> cell.foreground.material.value.collisionKind.isSolidNonDynamic } + } + + private fun roundToNode(pos: Vector2d): Vector2d { + // Round pos to the nearest node. + + // Work out the distance from the entity's origin to the bottom of its + // feet. We round Y relative to this so that we ensure we're able to + // generate + // paths through gaps that are *just* tall enough for the entity to fit + // through. + val bottom = standingBox.mins.y + + val x = (pos.x / NODE_GRANULARITY).roundToInt() * NODE_GRANULARITY + val y = ((pos.y + bottom) / NODE_GRANULARITY).roundToInt() * NODE_GRANULARITY - bottom + return Vector2d(x, y) + } + + private fun validPosition(pos: Vector2d, type: BoundingBoxType = BoundingBoxType.FULL): Boolean { + return !chunkMap.anyCellSatisfies(boundingBox(pos, type)) { _, _, cell -> + cell.foreground.material.value.collisionKind.isSolidNonDynamic + } + } + + private data class ArcCollisionSimResult(val position: Vector2d, val collisionX: Boolean, val collisionY: Boolean) + + inner class Node(val position: Vector2d, val velocity: Vector2d? = null) : Comparable { + var totalCost: Double = 0.0 + var parent: Edge? = null + + fun toTable(tables: TableFactory): Table { + val table = tables.newTable(2, 0) + table[positionIndex] = tables.from(position) + table[velocityIndex] = tables.from(velocity) + return table + } + + fun reconstructPath(): MutableList { + val result = ArrayList() + var parent = parent + + while (parent != null) { + result.add(parent) + parent = parent.source.parent + } + + result.reverse() + return result + } + + override fun compareTo(other: Node): Int { + return combinedCost.compareTo(other.combinedCost) + } + + /** + * from this node to GOOOOOOOOOAAAAAAL + */ + val heuristicCost: Double + + init { + val diff = world.geometry.diff(position, goal) + heuristicCost = 2.0 * (diff.x.absoluteValue + diff.y.absoluteValue) + } + + val combinedCost: Double + get() = totalCost + heuristicCost + + override fun equals(other: Any?): Boolean { + return other === this || other is Node && other.position == position && other.velocity == velocity + } + + override fun hashCode(): Int { + return position.hashCode() + velocity.hashCode() * 31 + } + + override fun toString(): String { + return "Node[$position, $velocity]" + } + + private fun simulateArcCollision(position: Vector2d, velocity: Vector2d, dt: Double): ArcCollisionSimResult { + // Returns the new position and whether a collision in the Y axis occurred. + // We avoid actual collision detection / resolution as that would make + // pathfinding very expensive. + + val newPosition = position + velocity * dt + + if (validPosition(newPosition)) { + return ArcCollisionSimResult(newPosition, false, false) + } else { + if (validPosition(Vector2d(newPosition.x, position.y))) { + return ArcCollisionSimResult(Vector2d(newPosition.x, position.y), false, true) + } else if (validPosition(Vector2d(position.x, newPosition.y))) { + return ArcCollisionSimResult(Vector2d(position.x, newPosition.y), true, false) + } else { + return ArcCollisionSimResult(position, true, true) + } + } + } + + // FIXME: it gives very rough estimation for directional gravity, if any at all + @Suppress("NAME_SHADOWING") + private fun simulateArc(callback: (target: Node, ground: Boolean) -> Unit) { + var position = position + var velocity = velocity!! + + var isJumping = chunkMap.gravityAt(position).dot(velocity) > 0.0 // gravity is positive + val acceleration = acceleration + + if (acceleration == Vector2d.ZERO) + return + + // Simulate until we're roughly NodeGranularity distance from the previous + // node + var rounded: Vector2d = roundToNode(position) + val start = rounded + + while (rounded == start) { + val speed = velocity.length + val dt = min(0.2, if (speed != 0.0) ARC_SIMULATION_FIDELTITY / speed else sqrt(ARC_SIMULATION_FIDELTITY * 2.0 * acceleration.lengthSquared)) + + val (simPosition, collidedX, collidedY) = simulateArcCollision(position, velocity, dt) + position = simPosition + rounded = roundToNode(simPosition) + + val upVector by lazy { + var result = chunkMap.gravityAt(position).unitVector + if (result == Vector2d.ZERO) result = Vector2d.POSITIVE_Y + result + } + + val rightVector by lazy { + var result = chunkMap.gravityAt(position).rotate(-PI / 2.0).unitVector + if (result == Vector2d.ZERO) result = Vector2d.POSITIVE_X + result + } + + if (collidedY) { + // We've either landed or hit our head on the ceiling + if (!isJumping) { + // Landed + if (velocity.dot(chunkMap.gravityAt(position).unitVector) < parameters.maxLandingVelocity) { + callback(Node(rounded), true) + } + + return + } else if (onGround(rounded, BoundingBoxType.STAND)) { + // Simultaneously hit head and landed -- this is a gap we can *just* + // fit through. No checking of the maxLandingVelocity, since the tiles' + // polygons are rounded, making this an easier target to hit than it + // seems. + callback(Node(rounded, velocity), true) + return + } + + // Hit ceiling. Remove y velocity + velocity *= rightVector + } else if (collidedX) { + // Hit a wall, just fall down + velocity *= upVector + + if (isJumping) { + isJumping = false + velocity = Vector2d.ZERO + } + } + + velocity += acceleration * dt + + if (isJumping && velocity.y <= 0.0) { + // We've reached a peak in the jump and the entity can now choose to + // change direction. + + val walkSpeed = actorMovementParameters.walkSpeed + val runSpeed = actorMovementParameters.runSpeed + val dot = this.velocity!!.dot(rightVector) + + if (dot != 0.0 || parameters.enableVerticalJumpAirControl) { + val unit = velocity.unitVector + + if (walkSpeed != null && parameters.enableWalkSpeedJumps) { + callback(Node(position, unit * walkSpeed), false) + callback(Node(position, unit * walkSpeed * parameters.jumpDropXMultiplier), false) + } + + if (runSpeed != null) { + callback(Node(position, unit * runSpeed), false) + } + } + + // Only fall straight down if we were going straight up originally. + // Going from an arc to falling straight down looks unnatural. + + if (dot == 0.0) { + callback(Node(position, Vector2d.ZERO), false) + } + + return + } + } + + if (!isJumping) { + if (velocity.dot(chunkMap.gravityAt(position).unitVector) < parameters.maxLandingVelocity) { + if (onGround(rounded, BoundingBoxType.STAND) || inLiquid(rounded)) { + // Collision with platform + callback(Node(rounded, velocity), true) + return + } + } + } + + check(velocity.dot(chunkMap.gravityAt(position)) != 0.0) + callback(Node(position, velocity), false) + } + + private fun forEachArcVelocity(velocity: Vector2d, callback: (result: Vector2d) -> Unit) { + callback(velocity) + + val walkSpeed = actorMovementParameters.walkSpeed + val runSpeed = actorMovementParameters.runSpeed + val angle = -chunkMap.gravityAt(position).toAngle(Vector2d.POSITIVE_Y) + + if (parameters.enableWalkSpeedJumps && walkSpeed != null) { + callback((Vector2d(walkSpeed) + velocity).rotate(angle)) + callback((Vector2d(-walkSpeed) + velocity).rotate(angle)) + } + + if (runSpeed != null) { + callback((Vector2d(runSpeed) + velocity).rotate(angle)) + callback((Vector2d(-runSpeed) + velocity).rotate(angle)) + } + } + + private fun getJumpingNeighbours(result: MutableList) { + val inLiquid = inLiquid(position) + + val jumpSpeed = if (inLiquid) + // It is unknown if patching this to use liquid jump profile if applicable have undesired consequences + actorMovementParameters.liquidJumpProfile.jumpSpeed ?: actorMovementParameters.airJumpProfile.jumpSpeed + else + actorMovementParameters.airJumpProfile.jumpSpeed + + if (jumpSpeed != null) { + val jumpCost = if (inLiquid) parameters.liquidJumpCost else parameters.jumpCost + + forEachArcVelocity(chunkMap.gravityAt(position).unitVector * jumpSpeed) { velocity -> + result.add(Edge(jumpCost, Action.JUMP, velocity, this, Node(position, velocity))) + } + + forEachArcVelocity(chunkMap.gravityAt(position).unitVector * jumpSpeed * parameters.smallJumpMultiplier) { velocity -> + result.add(Edge(jumpCost, Action.JUMP, velocity, this, Node(position, velocity))) + } + } + } + + private fun getFlyingNeighbors(result: MutableList) { + val rounded = roundToNode(position) + + for (dx in -1 .. 1) { + for (dy in -1 .. 1) { + val pos = rounded + Vector2d(dx * NODE_GRANULARITY, dy * NODE_GRANULARITY) + + if (validPosition(pos)) { + result.add(defaultCostEdge(Action.FLY, this, Node(pos))) + } + } + } + } + + private fun getWalkingNeighborsInDirection(result: MutableList, direction: Double) { + // gravity is positive vector, so it "points" up + val angle = gravityAngle + val up = Vector2d.POSITIVE_Y.rotate(angle) + + // rotate all vectors, so we work with local angles instead of global ones + var forward = position + Vector2d(direction, 0.0).rotate(angle) + val forwardAndUp = forward + up + val forwardAndDown = forward - up + + var bounds = boundingBox(position) + + var slopeDown = false + var slopeUp = false + + // TODO: this doesn't work with directional gravity + val forwardGroundPos = if (direction > 0.0) Vector2d(bounds.maxs.x, bounds.mins.y) else Vector2d(bounds.mins.x, bounds.mins.y) + val backGroundPos = if (direction > 0.0) Vector2d(bounds.mins.x, bounds.mins.y) else Vector2d(bounds.maxs.x, bounds.mins.y) + + val collisionCells = chunkMap.queryTileCollisions(groundCollisionRect(position).toDoubleAABB().padded(1.0, 1.0)) + + outer@for (block in collisionCells) { + for (side in block.poly.edges) { + val rotSide = side.rotate(angle) + val (p0, p1) = rotSide + val sideDir = rotSide.direction + + val lower = if (p0.y < p1.y) p0 else p1 + val upper = if (p0.y < p1.y) p1 else p0 + + if (sideDir.x != 0.0 && sideDir.y != 0.0 && (lower.y == forwardGroundPos.y || upper.y == forwardGroundPos.y)) { + val yDir = direction * (sideDir.y / sideDir.x) + + if (world.geometry.diff(forwardGroundPos, lower).x.absoluteValue < 0.5 && yDir > 0.0) { + slopeUp = true + break@outer + } else if (world.geometry.diff(backGroundPos, upper).x.absoluteValue < 0.5 && yDir < 0.0) { + slopeDown = true + break@outer + } + } + } + } + + // Check if it's possible to walk up a block like a ramp first + if (slopeUp && onGround(forwardAndUp) && validPosition(forwardAndUp)) { + // Walk up a slope + result.add(defaultCostEdge(Action.WALK, this, Node(forwardAndUp))) + } else if (validPosition(forward) && onGround(forward)) { + // Walk along a flat plane + result.add(defaultCostEdge(Action.WALK, this, Node(forward))) + } else if (slopeDown && validPosition(forward) && validPosition(forwardAndDown) && onGround(forwardAndDown)) { + // Walk down a slope + result.add(defaultCostEdge(Action.WALK, this, Node(forwardAndDown))) + } else if (validPosition(forward)) { + // Fall off a ledge + bounds = standingBox + val back = if (direction > 0.0) bounds.mins.x else bounds.maxs.x + forward -= Vector2d((1.0 - back.absoluteValue % 1.0) * direction).rotate(angle) + + val walkSpeed = actorMovementParameters.walkSpeed + val runSpeed = actorMovementParameters.runSpeed + + if (walkSpeed != null) + result.add(defaultCostEdge(Action.WALK, this, Node(Vector2d(walkSpeed * direction.sign).rotate(angle)))) + + if (runSpeed != null) + result.add(defaultCostEdge(Action.WALK, this, Node(Vector2d(runSpeed * direction.sign).rotate(angle)))) + } + } + + private val acceleration: Vector2d get() { + var gravity = chunkMap.gravityAt(position) * (actorMovementParameters.gravityMultiplier ?: 1.0) + + if (actorMovementParameters.gravityEnabled == false || actorMovementParameters.mass == 0.0) { + gravity = Vector2d.ZERO + } + + val buoyancy = actorMovementParameters.airBuoyancy ?: 0.0 + return -gravity * (1.0 - buoyancy) + } + + private val gravityAngle: Double get() { + // 0 -> points down + // 90 -> points LEFT + // -90 -> points RIGHT + // (in radians) + return -chunkMap.gravityAt(position).toAngle(Vector2d.POSITIVE_Y) + } + + fun computeEdges(): List { + val result = ArrayList() + + if (velocity != null) { + // Follow the current trajectory. Most of the time, this will only produce + // one neighbor to avoid massive search space explosion, however one + // change of X velocity is allowed at the peak of a jump. + + simulateArc { target, ground -> + result.add(defaultCostEdge(Action.ARC, this, target)) + + if (ground) { + result.add(defaultCostEdge(Action.LAND, target, Node(target.position))) + } + } + } else if (inLiquid(position)) { + // TODO avoid damaging liquids, e.g. lava + + // We assume when we're swimming we can move freely against gravity + getFlyingNeighbors(result) + + // Also allow jumping out of the water if we're at the surface: + val box = AABB.withSide(position, 0.5) + + if (acceleration.y != 0.0 && (chunkMap.averageLiquidLevel(box)?.average ?: 0.0) < 1.0) { + getJumpingNeighbours(result) + } + + // why do they do this in original code? + result.removeIf { !inLiquid(it.target.position) } + + for (edge in result) { + if (edge.action == Action.FLY) + edge.action = Action.SWIM + + edge.cost *= parameters.swimCost + } + } else if (acceleration.y == 0.0) { + getFlyingNeighbors(result) + } else if (onGround(position)) { + getWalkingNeighborsInDirection(result, NODE_GRANULARITY) + getWalkingNeighborsInDirection(result, -NODE_GRANULARITY) + + if (onSolidGround(position)) { + // Add a node for dropping through a platform. + // When that node is explored, if it's not onGround, its neighbors will + // be falling to the ground. + + val dropPosition = position - chunkMap.gravityAt(position).unitVector + + // The physics of platforms don't allow us to drop through platforms resting + // directly on solid surfaces. So if there is solid ground below the + // platform, don't allow dropping through the platform: + + // TODO: KStarbound: This seems to be completely false? You can drop through platforms which rest on + // solid tiles just like through any other platform. + + if (!onSolidGround(dropPosition)) { + result.add(Edge( + parameters.dropCost, + Action.DROP, + Vector2d.ZERO, + this, + Node(dropPosition, acceleration * sqrt(2.0 / acceleration.length)) + )) + } + } + + getJumpingNeighbours(result) + } else { + val position = roundToNode(position) + + // We're in the air, and can only fall now + forEachArcVelocity(Vector2d.ZERO) { resVel -> + Node(position, resVel).simulateArc { target, ground -> + result.add(defaultCostEdge(Action.ARC, this, target)) + + if (ground) { + result.add(defaultCostEdge(Action.LAND, target, Node(target.position))) + } + } + } + } + + return result + } + } + + @JsonFactory + data class Parameters( + // Maximum distance from the start node to search for a path to the target + // node + val maxDistance: Double = 50.0, + // If true, returns the path to the closest node to the target found, if a + // path to the target itself could not be found. + // Otherwise, findPath will return a None value. + val returnBest: Boolean = false, + // If true, end the path only on ground + val mustEndOnGround: Boolean = false, + // If true, allows jumps to have the entity's walk speed as horizontal + // velocity + val enableWalkSpeedJumps: Boolean = false, + // if true, allows perfectly vertical jumps to change horizontal velocity at + // the peak + val enableVerticalJumpAirControl: Boolean = false, + // Multiplies the cost of edges going through liquids. Can be used to + // penalize or promote paths involving swiming. + val swimCost: Double = 40.0, + // The cost of jump edges. + val jumpCost: Double = 3.0, + // The cost of jump edges that start in liquids. + val liquidJumpCost: Double = 10.0, + // The cost of dropping through a platform. + val dropCost: Double = 3.0, + // If set, will be the default bounding box, otherwise will use + // movementParameters.standingPoly. + val boundBox: AABB? = null, + // The bound box used for checking if the entity can stand at a position + // Should be thinner than the full bound box + val standingBoundBox: AABB? = null, + // The bound box used for checking if the entity can drop at a position + // Should be wider than the full bound box + val droppingBoundBox: AABB? = null, + // Pathing simulates jump arcs for two Y velocities: 1.0 * jumpSpeed and + // smallJumpMultiplier * jumpSpeed. This value should be in the range + // 0 < smallJumpMultiplier < 1.0 + val smallJumpMultiplier: Double = 0.75, + // Mid-jump, at the peak, entities can choose to change horizontal velocity. + // The velocities they can switch to are runSpeed, walkSpeed, and + // (walkSpeed * jumpDropXMultiplier). The purpose of the latter option is to + // make a vertical drop (if 0) or disable dropping (if 1). Inbetween values + // can be used to make less angular-looking arcs. + val jumpDropXMultiplier: Double = 0.125, + // If provided, the following fields can be supplied to put a limit on how + // long findPath calls can take: + val maxFScore: Double = Double.POSITIVE_INFINITY, + + val maxNodesToSearch: Int = Int.MAX_VALUE, + // Upper bound on the (negative) velocity that entities can land on + // platforms + // and ledges with. This is used to ensure there is a small amount of + // clearance + // over ledges to improve the scripts' chances of landing the same way we + // simulated the jump. + val maxLandingVelocity: Double = -5.0, + ) + + @Volatile + private var signalStop = false + + fun signalStop() { + signalStop = true + } + + private var openNodes = PriorityQueue() + private var closedNodes = HashSet() + private var closest = Node(roundToNode(start)) + private val actualMaxNodesToSearch = parameters.maxNodesToSearch.coerceAtMost(40000) // 40000 should be more than enough for all cases + + var result = KOptional?>() + private set(value) { + field = value + // release memory associated with structures + openNodes = PriorityQueue() + closedNodes = HashSet() + closest = Node(roundToNode(start)) + } + + init { + openNodes.add(closest) + closedNodes.add(closest) + } + + private fun canContinue(): Boolean { + return openNodes.isNotEmpty() && (closedNodes.size - openNodes.size) < actualMaxNodesToSearch && !signalStop + } + + fun run(maxExplore: Int): Boolean? { + if (result.isPresent) + return result.value != null + + val openNodes = openNodes + val closedNodes = closedNodes + var budget = maxExplore + + while (canContinue() && budget-- > 0) { + val node = openNodes.remove() + + if (closest > node) + closest = node + + if (node.position.distance(goal) <= NODE_GRANULARITY && (!parameters.mustEndOnGround || onGround(node.position) && node.velocity == null)) { + closest = node + result = KOptional(closest.reconstructPath()) + return true + } + + for (edge in node.computeEdges()) { + edge.target.parent = edge + edge.target.totalCost = node.totalCost + edge.cost + + if (edge.target.combinedCost <= parameters.maxFScore && closedNodes.add(edge.target)) { + openNodes.add(edge.target) + } + } + } + + if (closedNodes.size >= 40000) + LOGGER.warn("Path finder tried to consider more than 40000 nodes (goal $goal, closest take $closest, distance ${closest.position.distance(goal)}, onGround ${onGround(closest.position)})") + + if (!canContinue()) { + result = KOptional(null as List?) + } + + if (result.isPresent) + return result.value != null + else + return null + } + + override fun get(): List? { + run(Int.MAX_VALUE) + return if (result.isPresent) result.value else null + } + + companion object { + private val LOGGER = LogManager.getLogger() + const val ARC_SIMULATION_FIDELTITY = 0.5 + const val NODE_GRANULARITY = 1.0 + + private val positionIndex = "position".toByteString()!! + private val velocityIndex = "velocity".toByteString()!! + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ProjectileEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ProjectileEntity.kt index 64045023..5ffe318a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ProjectileEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ProjectileEntity.kt @@ -54,6 +54,11 @@ class ProjectileEntity private constructor(val config: Registry.Entry>(config.keepDamageNotificationSteps) + private val recentDamageReceived = HistoryQueue(config.keepDamageNotificationSteps) + private val recentDamageDealt = HistoryQueue(config.keepDamageNotificationSteps) + + fun notifyHitDealt(victimID: Int, damage: DamageData) { + recentHitsDealt.add(victimID to damage) + } + + fun notifyDamageDealt(damage: DamageNotification) { + recentDamageDealt.add(damage) + } + + fun recentHitsDealt(since: Long = 0L) = recentHitsDealt.query(since) + fun recentDamageReceived(since: Long = 0L) = recentDamageReceived.query(since) + fun recentDamageDealt(since: Long = 0L) = recentDamageDealt.query(since) + + fun experienceDamage(damage: DamageData): List { + val results = lua.invokeGlobal("applyDamageRequest", lua.from(Starbound.gson.toJsonTree(damage))) + + if (results.isNotEmpty() && results[0] is Table) { + val parsed = ArrayList() + + for ((_, v) in (results[0] as Table)) { + try { + parsed.add(Starbound.gson.fromJsonFast(toJsonFromLua(v), DamageNotification::class.java)) + } catch (err: Throwable) { + LOGGER.error("Exception while parsing results returned by applyDamageRequest (primary scripts: ${config.primaryScriptSources})", err) + } + } + + for (notification in parsed) { + recentDamageReceived.add(notification) + } + + return parsed + } + + return listOf() + } + + // since experienceDamage is expected to be called externally (when someone damaged us) + // inflicting self damage requires little more work (notifying everyone else that we received damage) + fun inflictSelfDamage(damage: DamageData) { + val notifications = experienceDamage(damage) + + for (notification in notifications) { + // addDamageNotification instead of networkDamageNotification for original engine parity + // (inflicting self-damage calls "damagedOther" functions on original engine, so as well must we) + entity.world.addDamageNotification(DamageNotificationPacket(entity.entityID, notification)) + } + } + + 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) } + + fun setProperty(key: String, value: JsonElement) { + val old = statusProperties.value[key] + statusProperties.value[key] = value + + if (old != value) { + statusProperties.bumpVersion() + } + } + + fun getProperty(key: String): JsonElement? { + return statusProperties.value[key] + } var parentDirectives by networkedString().also { networkGroup.add(it) } private set - private val uniqueEffectMetadata = NetworkedDynamicGroup(::UniqueEffectMetadata, UniqueEffectMetadata::networkGroup).also { networkGroup.add(it) } + var primaryDirectives = "" + + var appliesEnvironmentStatusEffects = config.appliesEnvironmentStatusEffects + var appliesWeatherStatusEffects = config.appliesWeatherStatusEffects + var minimumLiquidStatusEffectPercentage = config.minimumLiquidStatusEffectPercentage + + private val uniqueEffectMetadata = NetworkedDynamicGroup(::UniqueEffectNetworkedValues, UniqueEffectNetworkedValues::networkGroup).also { networkGroup.add(it) } private val effectAnimators = NetworkedDynamicGroup(::EffectAnimator, { it }).also { networkGroup.add(it) } - fun update(delta: Double) { - updateStats(delta) + init { + if (config.primaryAnimationConfig == null) { + animator = null + animatorID = null + } else { + animator = EffectAnimator(KOptional(config.primaryAnimationConfig.fullPath)) + animatorID = effectAnimators.add(animator) + + // FIXME: effect animator's animator is not set in stone because of legacy protocol + // god damn it + // But at least it shouldn't change in this context + if (!entity.isRemote) { + provideAnimatorBindings(animator.animator, lua) + } + } + } + + fun handleMessage(message: String, isLocal: Boolean, arguments: JsonArray): JsonElement? { + return luaMessages.handle(message, isLocal, arguments) ?: uniqueStatusEffects.values.firstNotNullOfOrNull { it.luaMessages.handle(message, isLocal, arguments) } + } + + @JsonFactory + data class SerializedData( + val statusProperties: JsonObject, + val resourceValues: ImmutableMap, + val resourcesLocked: ImmutableMap, + val persistentEffectCategories: ImmutableMap>, + val ephemeralEffects: ImmutableList, + ) { + @JsonFactory + data class UniqueEffect( + val effect: Registry.Ref, + val duration: Double, + val maxDuration: Double = duration, + ) + } + + fun serialize(): SerializedData { + return SerializedData( + statusProperties = statusProperties.get(), + resourceValues = resourcesInternal.entries.stream().map { it.key to it.value.value }.collect(ImmutableMap.toImmutableMap({ it.first }, { it.second })), + resourcesLocked = resourcesInternal.entries.stream().map { it.key to it.value.isLocked }.collect(ImmutableMap.toImmutableMap({ it.first }, { it.second })), + + ephemeralEffects = uniqueStatusEffects + .values + .stream() + .filter { !it.isPersistent } + .map { + SerializedData.UniqueEffect( + effect = it.effect.ref, + duration = it.metadata.duration, + maxDuration = it.metadata.maxDuration, + ) + } + .collect(ImmutableList.toImmutableList()), + + persistentEffectCategories = persistentStatusEffects + .entries + .stream() + .map { + val result = ImmutableList.Builder() + it.value.statModifiers.forEach { result.add(Either.left(it)) } + it.value.uniqueEffects.forEach { result.add(Either.right(it.ref)) } + it.key to result.build() + } + .collect(ImmutableMap.toImmutableMap({ it.first }, { it.second })), + ) + } + + fun deserialize(data: SerializedData) { + + } + + // ----- Persistent status effects + // These effects have no duration and are typically sourced to actor's equipment OR + // the environment. Sometimes, they are also sources to actor's current action, such as sleeping on bed. + private class PersistentEffectCategory(val statGroupID: Int) { + val statModifiers = ArrayList() + val uniqueEffects = HashSet>() + } + + // there are two magic keys used for this map: 'entities' and 'environment' for StatusEffectEntity + // and environmentally applied persistent status effects, respectively + private val persistentStatusEffects = HashMap() + + fun addPersistentEffect(category: String, effect: PersistentStatusEffect) { + addPersistentEffects(category, listOf(effect)) + } + + fun addPersistentEffects(category: String, effects: Collection) { + val cat = persistentStatusEffects.computeIfAbsent(category) { + val index = statModifiers.nextFreeIndex() + val value = PersistentEffectCategory(index) + // avoid double-copy + statModifiersNetworkMap[index] = value.statModifiers + value + } + + for (effect in effects) { + effect.map({ cat.statModifiers.add(it) }, { if (it.isPresent) cat.uniqueEffects.add(it.entry!!) }) + } + + statModifiersNetworkMap.markIndexDirty(cat.statGroupID) + updatePersistentUniqueEffects() + } + + fun setPersistentEffects(category: String, effects: Collection) { + var changes = false + + if (effects.isEmpty() && category in persistentStatusEffects) { + statModifiersNetworkMap.remove(persistentStatusEffects.remove(category)!!.statGroupID) + changes = true + } else if (effects.isNotEmpty()) { + val cat = persistentStatusEffects[category] + + if (cat != null) { + cat.uniqueEffects.clear() + cat.statModifiers.clear() + } + + addPersistentEffects(category, effects) + changes = true + } + + if (changes) + updatePersistentUniqueEffects() + } + + fun removePersistentEffects(category: String) { + setPersistentEffects(category, listOf()) + } + + fun removeAllPersistentEffects() { + if (persistentStatusEffects.isNotEmpty()) { + persistentStatusEffects.values.forEach { + statModifiersNetworkMap.remove(it.statGroupID) + } + + persistentStatusEffects.clear() + updatePersistentUniqueEffects() + } + } + + private fun updatePersistentUniqueEffects() { + val activePersistentEffects = HashSet>() + + persistentStatusEffects.values.forEach { + activePersistentEffects.addAll(it.uniqueEffects) + } + + for (effect in activePersistentEffects) { + val existing = uniqueStatusEffects[effect.key] + + if (existing == null) { + addUniqueEffect(effect, null, null) + } else { + existing.promoteToPersistent() + } + } + + // Again, here we are using "durationless" to mean "persistent" + val keys = ArrayList(uniqueStatusEffects.keys) + + for (key in keys) { + val meta = uniqueStatusEffects[key]!! + + if (meta.metadata.duration == 0.0 && meta.effect !in activePersistentEffects) { + meta.remove() + } + } + } + + fun getPersistentEffects(category: String): List { + val cat = persistentStatusEffects[category] ?: return emptyList() + val result = ArrayList() + + cat.statModifiers.forEach { + result.add(Either.left(it)) + } + + cat.uniqueEffects.forEach { + result.add(Either.right(it.ref)) + } + + return result + } + + // ----- Unique status effects + // These are the effect player see on their status bar (under health). + // Difference between StatModifiers and these are that Unique status effect can have Lua logic attached, + // and don't necessarily affect stats (but frequently come with stat modifiers, such as providing health regeneration + // through providing positive 'delta' value for 'health' resource) + private val uniqueStatusEffects = LinkedHashMap() + + val isStatusEffectsImmune: Boolean + get() = statPositive("statusImmunity") + + private inner class UniqueEffectInstance(val effect: Registry.Entry, duration: Double?, source: Int?) { + init { + if (effect.key in uniqueStatusEffects) { + throw IllegalStateException("Already has unique effect $effect") + } + + uniqueStatusEffects[effect.key] = this + } + + var parentDirectives: String = "" + val modifierGroups = IntArrayList() + + val lua = LuaEnvironment() + val luaMessages = LuaMessageHandlerComponent(lua) { "Unique effect" } + val luaMovement = MovementControllerBindings(entity.movement) + val luaUpdate = LuaUpdateComponent(lua) + + val metadata = UniqueEffectNetworkedValues() + val metadataNetworkID = uniqueEffectMetadata.add(metadata) + + var isPersistent = duration == null + private set + + init { + metadata.duration = duration ?: 0.0 + metadata.sourceEntity = source + } + + val animator: EffectAnimator? + val animatorNetworkID: Int? + + init { + if (effect.value.animationConfig?.fullPath != null) { + animator = EffectAnimator(KOptional(effect.value.animationConfig!!.fullPath!!)) + animatorNetworkID = effectAnimators.add(animator) + } else { + animator = null + animatorNetworkID = null + } + + if (!entity.isRemote && effect.value.scripts.isNotEmpty()) { + // unique effects shouldn't be initialized on remotes in first place + // but whatever + lua.attach(effect.value.scripts) + + // provideStatusControllerBindings(this@StatusController, lua) // provided through provideEntityBindings + provideEntityBindings(entity, lua) + luaMovement.init(lua) + provideEffectBindings() + + if (animator != null) { + // FIXME: effect animator's animator is not set in stone because of legacy protocol + // god damn it + // But at least it shouldn't change in this context + provideAnimatorBindings(animator.animator, lua) + } + + provideConfigBindings(lua) { path, default -> + path.find(effect.json) ?: default + } + + lua.init() + } + } + + private fun provideEffectBindings() { + val callbacks = lua.newTable() + lua.globals["effect"] = callbacks + + callbacks["duration"] = luaFunction { + returnBuffer.setTo(metadata.duration) + } + + callbacks["modifyDuration"] = luaFunction { duration: Number -> + val value = duration.toDouble() + check(value.isFinite()) { "Infinite duration provided" } + check(!value.isNaN()) { "NaN duration provided" } + + if (!isPersistent) { + metadata.duration += value + } + } + + callbacks["expire"] = luaFunction { + if (!isPersistent) { + metadata.duration = 0.0 + } + } + + callbacks["sourceEntity"] = luaFunction { + returnBuffer.setTo(metadata.sourceEntity ?: entity.entityID) + } + + callbacks["setParentDirectives"] = luaFunction { directives: ByteString -> + parentDirectives = directives.decode().sbIntern() + } + + callbacks["getParameter"] = createConfigBinding { path, default -> + path.find(effect.json) ?: default + } + + callbacks["addStatModifierGroup"] = luaFunction { modifiers: Table -> + val actual = modifiers + .iterator() + .map { (_, v) -> Starbound.gson.fromJsonFast(toJsonFromLua(v), StatModifier::class.java) } + .collect(Collectors.toCollection(::ArrayList)) + + val id = statModifiers.add(actual) + modifierGroups.add(id) + returnBuffer.setTo(id) + } + + callbacks["setStatModifierGroup"] = luaFunction { groupID: Number, modifiers: Table -> + val id = groupID.toInt() + + if (id !in modifierGroups) + throw LuaRuntimeException("$groupID stat modifier group does not belong to this effect") + + val actual = modifiers + .iterator() + .map { (_, v) -> Starbound.gson.fromJsonFast(toJsonFromLua(v), StatModifier::class.java) } + .collect(Collectors.toCollection(::ArrayList)) + + statModifiers[id] = actual + } + + callbacks["removeStatModifierGroup"] = luaFunction { groupID: Number -> + val id = groupID.toInt() + + if (modifierGroups.rem(id)) { + statModifiers.remove(id) + } else { + throw LuaRuntimeException("$groupID stat modifier group does not belong to this effect") + } + } + } + + fun promoteToPersistent() { + // It is important to note here that if a unique effect exists, it *may* + // not come from a persistent effect, it *may* be from an ephemeral effect. + // Here, when a persistent effect overrides an ephemeral effect, it is + // clearing the duration making it into a solely persistent effect. This + // means that by applying a persistent effect and then clearing it, you can + // remove an ephemeral effect. + + // Original engine only updates the duration (setting it to null), + // but for proper promotion we also need to reset cause entity, otherwise + // persistent effect will be sourced to whoever put ephemeral effect + metadata.duration = 0.0 + metadata.sourceEntity = null + isPersistent = true + } + + fun remove() { + lua.invokeGlobal("onExpire") + uniqueEffectMetadata.remove(metadataNetworkID) + + for (group in modifierGroups) { + statModifiersNetworkMap.remove(group) + } + + if (animatorNetworkID != null) { + effectAnimators.remove(animatorNetworkID) + } + + check(uniqueStatusEffects.remove(effect.key) === this) + } + } + + fun addEphemeralEffect(effect: EphemeralStatusEffect, source: Int? = null) { + val entry = effect.effect.entry ?: return LOGGER.warn("Tried to add invalid ephemeral effect $effect to status controller") + val existing = uniqueStatusEffects[entry.key] + + if (existing == null) { + addUniqueEffect(entry, effect.duration, source) + } else { + // If the effect exists and does not have a null duration, then refresh + // the duration to the max + if (!existing.isPersistent) { + val newDuration = effect.duration ?: entry.value.defaultDuration + + if (newDuration > existing.metadata.duration) { + // Only overwrite the sourceEntityId if the duration is *extended* + existing.metadata.sourceEntity = source + existing.metadata.duration = newDuration + } + + existing.metadata.maxDuration = existing.metadata.maxDuration.coerceAtLeast(newDuration) + } + } + } + + fun removeEphemeralEffect(effect: String): Boolean { + val get = uniqueStatusEffects[effect] + + if (get != null && !get.isPersistent) { + get.remove() + return true + } + + return false + } + + fun removeEphemeralEffect(effect: Registry.Entry): Boolean { + return removeEphemeralEffect(effect.key) + } + + fun removeEphemeralEffects() { + uniqueStatusEffects.values.filter { !it.isPersistent }.forEach { it.remove() } + } + + private fun addUniqueEffect(effect: Registry.Entry, duration: Double?, source: Int?) { + if (effect.key in uniqueStatusEffects) { + throw IllegalStateException("Already has unique effect $effect") + } + + if ( + (duration == null || isStatusEffectsImmune) && // ephemeral status effect + (effect.value.blockingStat == null || !statPositive(effect.value.blockingStat!!)) // effect can't be combined with + ) { + UniqueEffectInstance(effect, duration, source) + } + } + + fun activeUniqueStatusEffectSummary(): List, Double>> { + val result = ArrayList, Double>>() + + for (data in uniqueStatusEffects.values) { + if (data.isPersistent) { + result.add(data.effect to 1.0) + } else { + result.add(data.effect to (data.metadata.duration / data.metadata.maxDuration).let { + if (it.isInfinite() || it.isNaN()) + 1.0 + else + it + }) + } + } + + return result + } + + fun uniqueStatusEffectActive(name: String): Boolean { + return name in uniqueStatusEffects } private class EffectAnimator(var config: KOptional = KOptional()) : Passthrough() { @@ -125,15 +719,18 @@ class StatusController(val entity: ActorEntity, val config: StatusControllerConf } } - private class UniqueEffectMetadata { + private class UniqueEffectNetworkedValues { val networkGroup = NetworkedGroup() var duration by networkedFixedPoint(0.01).also { networkGroup.add(it); it.interpolator = Interpolator.Linear } var maxDuration by networkedFloat().also { networkGroup.add(it) } - var sourceEntity by networkedData(KOptional(), KOptionalIntValueCodec).also { networkGroup.add(it) } + var sourceEntity by networkedData(null, IntValueCodec.nullable()).also { networkGroup.add(it) } } - // stats + // ----- Stats + // The heart of actor's information, such as health, energy, player's hunger, etc. + // Stats are ephemeral, meaning their values are updated frequently, and max values are calculated + // based on other values, resources, or StatModifiers sealed class LiveStat { abstract val baseValue: Double @@ -168,6 +765,40 @@ class StatusController(val entity: ActorEntity, val config: StatusControllerConf abstract val maxValue: Double? + abstract fun reset() + + val isPositive: Boolean + get() = value > 0.0 + + val percentage: Double? + get() { + val max = maxValue ?: return null + if (max == 0.0) return null + return (value / max).coerceIn(0.0, 1.0) + } + + fun give(amount: Double): Double { + val old = value + value += amount + return value - old + } + + fun consume(amount: Double, allowOverdraw: Boolean = false): Boolean { + require(amount >= 0.0) { "Tried to consume negative amount $amount of resource $name" } + + if (isLocked) { + return false + } else if (value >= amount) { + value -= amount + return true + } else if (value > 0.0 && allowOverdraw) { + value = 0.0 + return true + } else { + return false + } + } + fun setAsPercentage(percent: Double) { val maxValue = maxValue @@ -181,13 +812,20 @@ class StatusController(val entity: ActorEntity, val config: StatusControllerConf this.value = maxValue * percent } } + + fun modifyPercentage(delta: Double) { + val max = maxValue ?: throw IllegalArgumentException("$name does not have max value") + + if (max == 0.0) { + LOGGER.debug("Tried to divide by zero resource {} because its max value is zero", name) + return + } + + setAsPercentage(value / max + delta) + } } - private class ResourceImpl( - override val name: String, - override val max: Either?, - override val delta: Either? - ) : Resource() { + private class ResourceImpl(override val name: String, override val max: Either?, override val delta: Either?) : Resource() { val actualValue = networkedFloat() override var value: Double @@ -209,14 +847,13 @@ class StatusController(val entity: ActorEntity, val config: StatusControllerConf set(value) { actualIsLocked.value = value } override var maxValue: Double? = null - private var defaultValue: Double = 0.0 fun mark() { defaultValue = value } - fun reset() { + override fun reset() { value = defaultValue } } @@ -224,12 +861,12 @@ class StatusController(val entity: ActorEntity, val config: StatusControllerConf // in original code it is named effectiveStats private val liveStatsInternal = HashMap() - private val statModifiersMap = NetworkedMap( + private val statModifiersNetworkMap = NetworkedMap( IntValueCodec, NATIVE_MODIFIERS_CODEC to LEGACY_MODIFIERS_CODEC, map = ListenableMap(Int2ObjectAVLTreeMap())) - private val statModifiers = IdMap(map = statModifiersMap) + private val statModifiers = IdMap(map = statModifiersNetworkMap) private val resourcesInternal = HashMap() val liveStats: Map = Collections.unmodifiableMap(liveStatsInternal) @@ -267,7 +904,7 @@ class StatusController(val entity: ActorEntity, val config: StatusControllerConf resource.mark() } - statNetworkGroup.add(statModifiersMap) + statNetworkGroup.add(statModifiersNetworkMap) for (k in resourcesInternal.keys.sorted()) { val resource = resourcesInternal[k]!! @@ -283,12 +920,8 @@ class StatusController(val entity: ActorEntity, val config: StatusControllerConf } } - fun addStatModifiers(modifiers: Collection): Int { - return statModifiers.add(ArrayList(modifiers)) - } - - fun removeStatModifiers(index: Int): Boolean { - return statModifiers.remove(index) != null + fun statPositive(name: String): Boolean { + return (liveStatsInternal[name]?.effectiveModifiedValue ?: 0.0) > 0.0 } private fun updateStats(delta: Double) { @@ -299,7 +932,7 @@ class StatusController(val entity: ActorEntity, val config: StatusControllerConf // modifiers successively on the baseModifiedValue, causing them to stack with // each other in addition to base multipliers and value modifiers - val neverVisited = ObjectArraySet(liveStats.keys) + val neverVisited = ObjectOpenHashSet(liveStats.keys) for ((statName, stat) in config.stats) { val live = liveStatsInternal[statName]!! @@ -377,6 +1010,101 @@ class StatusController(val entity: ActorEntity, val config: StatusControllerConf } } + fun tick(delta: Double) { + updateStats(delta) + animator?.animator?.tick(delta, entity.world.random) + + recentHitsDealt.tick(delta) + recentDamageDealt.tick(delta) + recentDamageReceived.tick(delta) + + luaUpdate.update(delta) { + luaMovement.clearControlsIfNeeded() + } + + val isStatusEffectsImmune = isStatusEffectsImmune + + if (!isStatusEffectsImmune && entity.movement.liquidPercentage > minimumLiquidStatusEffectPercentage) { + entity.movement.liquid?.value?.statusEffects?.forEach { + val entry = it.entry + + if (entry != null) + addEphemeralEffect(EphemeralStatusEffect(entry, entry.value.defaultDuration)) + } + } + + if (environmentalUpdateTimer.wrapTick(delta)) { + val entityEffects = ArrayList() + + if (!isStatusEffectsImmune) { + val entities = ReferenceOpenHashSet() + + for (poly in entity.movement.computeGlobalHitboxes()) { + entity.world.entityIndex.iterate(poly.aabb, { + if ( + it is StatusEffectEntity && + entities.add(it) && + it.statusEffectArea.any { p -> entity.world.geometry.polyIntersectsPoly(p, poly) } + ) { + entityEffects.addAll(it.entityEffects) + } + }) + } + } + + setPersistentEffects("entities", entityEffects) + + if (!isStatusEffectsImmune && appliesEnvironmentStatusEffects) { + setPersistentEffects("environment", entity.world.environmentStatusEffects(entity.movement.position)) + } else { + setPersistentEffects("environment", listOf()) + } + + if (!isStatusEffectsImmune && appliesWeatherStatusEffects) { + entity.world.weatherStatusEffects(entity.movement.position).forEach { + addEphemeralEffect(it) + } + } + } + + val toRemove = ArrayList() + + for (effect in uniqueStatusEffects.values) { + effect.luaUpdate.update(delta) { + luaMovement.clearControlsIfNeeded() + } + + if (!effect.isPersistent) { + effect.metadata.duration -= delta + } + + if ( + !effect.isPersistent && effect.metadata.duration <= 0.0 || + !effect.isPersistent && isStatusEffectsImmune || + effect.effect.value.blockingStat != null && statPositive(effect.effect.value.blockingStat!!) + ) { + toRemove.add(effect) + } + } + + toRemove.forEach { + it.remove() + } + + val parentDirectives = StringBuilder(primaryDirectives) + + for (effect in uniqueStatusEffects.values) { + parentDirectives.append("?") + parentDirectives.append(effect.parentDirectives) + } + + this.parentDirectives = parentDirectives.toString() + } + + fun tickRemote(delta: Double) { + + } + companion object { private val LEGACY_MODIFIERS_CODEC = StreamCodec.Collection(StatModifier.LEGACY_CODEC, ::ArrayList) private val NATIVE_MODIFIERS_CODEC = StreamCodec.Collection(StatModifier.CODEC, ::ArrayList) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/api/StatusEffectEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/api/StatusEffectEntity.kt new file mode 100644 index 00000000..b8a40416 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/api/StatusEffectEntity.kt @@ -0,0 +1,9 @@ +package ru.dbotthepony.kstarbound.world.entities.api + +import ru.dbotthepony.kstarbound.defs.actor.PersistentStatusEffect +import ru.dbotthepony.kstarbound.world.physics.Poly + +interface StatusEffectEntity { + val statusEffectArea: List + val entityEffects: List +} 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 new file mode 100644 index 00000000..386779f4 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/AbstractBehaviorNode.kt @@ -0,0 +1,153 @@ +package ru.dbotthepony.kstarbound.world.entities.behavior + +import com.google.common.collect.ImmutableList +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import com.google.gson.JsonPrimitive +import ru.dbotthepony.kommons.gson.contains +import ru.dbotthepony.kommons.gson.get +import ru.dbotthepony.kommons.gson.stream +import ru.dbotthepony.kstarbound.Registries +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.actor.behavior.CompositeNodeType +import ru.dbotthepony.kstarbound.defs.actor.behavior.NodeParameterValue +import ru.dbotthepony.kstarbound.fromJsonFast +import ru.dbotthepony.kstarbound.lua.userdata.BehaviorState +import ru.dbotthepony.kstarbound.util.valueOf + +abstract class AbstractBehaviorNode { + enum class Status(val asBoolean: Boolean?) { + INVALID(null), + SUCCESS(true), + FAILURE(false), + RUNNING(null) + } + + abstract fun run(delta: Double, state: BehaviorState): Status + abstract fun reset() + + fun runAndReset(delta: Double, state: BehaviorState): Status { + val status = run(delta, state) + + if (status != Status.RUNNING) + reset() + + return status + } + + companion object { + fun replaceBehaviorTag(parameter: NodeParameterValue, treeParameters: Map): NodeParameterValue { + var str: String? = null + + if (parameter.key != null) + str = parameter.key + + // original engine does this, and i don't know why this make any sense + else if (parameter.value is JsonPrimitive && parameter.value.isString) + str = parameter.value.asString + + if (str != null) { + if (str.first() == '<' && str.last() == '>') { + val treeKey = str.substring(1, str.length - 2) + val param = treeParameters[treeKey] + + if (param != null) { + return param + } else { + throw NoSuchElementException("No parameter specified for tag '$str'") + } + } + } + + return parameter + } + + fun replaceOutputBehaviorTag(output: String?, treeParameters: Map): String? { + 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'") + + if (replacement.key != null) + return replacement.key + else if (replacement.value is JsonPrimitive && replacement.value.isString) + return replacement.value.asString + else + return null + } else { + return output + } + } + + fun create(data: JsonObject, treeParameters: Map, tree: BehaviorTree): AbstractBehaviorNode { + val type = BehaviorNodeType.entries.valueOf(data["type"].asString) + val name = data["name"].asString + val parameterConfig = data.get("parameters") { JsonObject() } + + if (type == BehaviorNodeType.MODULE) { + // merge in module parameters to a copy of the treeParameters to propagate + // tree parameters into the sub-tree, but allow modules to override + val moduleParameters = LinkedHashMap(treeParameters) + + for ((k, v) in parameterConfig.entrySet()) { + moduleParameters[k] = replaceBehaviorTag(Starbound.gson.fromJsonFast(v, NodeParameterValue::class.java), treeParameters) + } + + val module = BehaviorTree(tree.blackboard, Registries.behavior.getOrThrow(name).value, moduleParameters) + tree.scripts.addAll(module.scripts) + tree.functions.addAll(module.functions) + return tree.root + } + + val parameters = LinkedHashMap(Registries.behaviorNodes.getOrThrow(name).value.properties) + + for ((k, v) in parameters.entries) { + if (k in parameterConfig) { + parameters[k] = v.copy(value = replaceBehaviorTag(Starbound.gson.fromJsonFast(parameterConfig[k], NodeParameterValue::class.java), treeParameters)) + } else { + val replaced = replaceBehaviorTag(v.value, treeParameters) + + if (replaced != v.value) { + parameters[k] = v.copy(value = replaced) + } + } + } + + when (type) { + BehaviorNodeType.ACTION -> { + tree.functions.add(name) + + val outputConfig = data.get("output") { JsonObject() } + val output = LinkedHashMap(Registries.behaviorNodes.getOrThrow(name).value.output) + + for ((k, v) in output.entries) { + val replaced = replaceOutputBehaviorTag(outputConfig[k]?.asString ?: v.key, treeParameters) + + if (replaced != v.key) { + output[k] = v.copy(key = replaced) + } + } + + return ActionNode(name, parameters, output) + } + + BehaviorNodeType.DECORATOR -> { + tree.functions.add(name) + val sacrifice = create(data["child"] as JsonObject, treeParameters, tree) + return DecoratorNode(name, parameters, sacrifice) + } + + BehaviorNodeType.COMPOSITE -> { + val children = data.get("children") { JsonArray() } + .stream() + .map { create(it as JsonObject, treeParameters, tree) } + .collect(ImmutableList.toImmutableList()) + + return CompositeNodeType.entries.valueOf(name).factory(parameters, children) + } + + BehaviorNodeType.MODULE -> throw RuntimeException() + } + } + } +} 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 new file mode 100644 index 00000000..b062dae4 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/ActionNode.kt @@ -0,0 +1,67 @@ +package ru.dbotthepony.kstarbound.world.entities.behavior + +import org.apache.logging.log4j.LogManager +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.NodeOutput +import ru.dbotthepony.kstarbound.defs.actor.behavior.NodeParameter +import ru.dbotthepony.kstarbound.lua.userdata.BehaviorState + +class ActionNode(val name: String, val parameters: Map, val outputs: Map) : AbstractBehaviorNode() { + private var coroutine: Coroutine? = null + + override fun run(delta: Double, state: BehaviorState): Status { + var coroutine = coroutine + var firstTime = false + + if (coroutine == null) { + firstTime = true + + val fn = state.functions[name] ?: throw RuntimeException("How? $name") + + coroutine = state.lua.call(CoroutineLib.create(), fn)[0] as Coroutine + } + + try { + val result = if (firstTime) { + val parameters = state.blackboard.parameters(parameters, this) + state.lua.call(CoroutineLib.resume(), coroutine, parameters, state.blackboard, this, delta) + } else { + state.lua.call(CoroutineLib.resume(), coroutine, delta) + } + + val status = result[0] as Boolean + + if (result.size >= 2) { + val second = result[1] as? Table + + if (second != null) { + state.blackboard.setOutput(this, second) + } + } + + val isDead = state.lua.call(CoroutineLib.status(), coroutine)[0] == "dead" + + if (!status) { + return Status.FAILURE + } else if (isDead) { + return Status.SUCCESS + } else { + return Status.RUNNING + } + } catch (err: CallPausedException) { + LOGGER.error("Behavior ActionNode '$name' called blocking code, which initiated pause. This is not supported.") + return Status.FAILURE + } + } + + override fun reset() { + coroutine = null + } + + companion object { + private val LOGGER = LogManager.getLogger() + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/BehaviorNodeType.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/BehaviorNodeType.kt new file mode 100644 index 00000000..60b44120 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/BehaviorNodeType.kt @@ -0,0 +1,10 @@ +package ru.dbotthepony.kstarbound.world.entities.behavior + +import ru.dbotthepony.kstarbound.json.builder.IStringSerializable + +enum class BehaviorNodeType(override val jsonName: String) : IStringSerializable { + ACTION("Action"), + DECORATOR("Decorator"), + COMPOSITE("Composite"), + MODULE("Module"); +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/BehaviorTree.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/BehaviorTree.kt new file mode 100644 index 00000000..970aaba4 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/BehaviorTree.kt @@ -0,0 +1,31 @@ +package ru.dbotthepony.kstarbound.world.entities.behavior + +import ru.dbotthepony.kstarbound.defs.actor.behavior.BehaviorDefinition +import ru.dbotthepony.kstarbound.defs.actor.behavior.NodeParameterValue +import ru.dbotthepony.kstarbound.lua.userdata.BehaviorState + +class BehaviorTree(val blackboard: Blackboard, val data: BehaviorDefinition, overrides: Map = mapOf()) : AbstractBehaviorNode() { + val scripts = HashSet(data.scripts) + val functions = HashSet() + + val root: AbstractBehaviorNode + + init { + if (overrides.isEmpty()) { + root = create(data.root, data.mappedParameters, this) + } else { + val parameters = LinkedHashMap(data.mappedParameters) + parameters.putAll(overrides) + root = create(data.root, parameters, this) + } + } + + override fun run(delta: Double, state: BehaviorState): Status { + return root.run(delta, state) + } + + override fun reset() { + root.reset() + } +} + 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 new file mode 100644 index 00000000..ce71e861 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/Blackboard.kt @@ -0,0 +1,193 @@ +package ru.dbotthepony.kstarbound.world.entities.behavior + +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonPrimitive +import org.classdump.luna.ByteString +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 +import kotlin.collections.HashMap + +/** + * Blackboard (Greenboard, if you will) for tracking inputs and outputs of function nodes in behavior tree. + * + * While this is obviously flawed, and inputs/outputs should _generally_ be stored inside nodes themselves + * (since we create/clone behavior tree each time we make AI actor), this class is exposed to Lua scripts, + * so we must keep this class + * + * On side note, this architecture also employs global inputs/outputs of functions, hence you can't make + * an actual functional tree out of behavior nodes (because variable flow is global, and not local, so you + * can't have, per say, function "calculate sin out of x" because both input "x" and "sin result" are + * defined as global variables). + * Bravo, Chucklefish. + */ +class Blackboard(val lua: LuaEnvironment) : Userdata() { + private val board = 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>>() + private var ephemeral = HashSet>() + + init { + for (v in NodeParameterType.entries) { + board[v] = HashMap() + } + } + + operator fun set(type: NodeParameterType, key: String, value: Any?) { + if (value == null) { + board[type]!!.remove(key) + } else { + board[type]!![key] = value + } + + val input = input[type]!![key] + + if (input != null) { + for ((k1, k2) in input) { + parameters[k1]!![k2] = value + } + } + + // dumb special case for setting number outputs to vec2 inputs + if (type == NodeParameterType.NUMBER) { + val mappings = vectorNumberInput[key] + + if (mappings != null) { + for ((index, table) in mappings) { + table[index] = value + } + } + } + } + + operator fun get(type: NodeParameterType, key: String): Any? { + return board[type]!![key] + } + + fun parameters(parameters: Map, nodeID: Any): Table { + val get = this.parameters[nodeID] + + if (get != null) + return get + + val table = lua.newTable() + + 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] + } else { + val value = parameter.value.value ?: JsonNull.INSTANCE + + if (value.isJsonNull) + continue + + // dumb special case for allowing a vec2 of blackboard number keys + if (parameter.type == NodeParameterType.VEC2) { + if (value !is JsonArray) + throw IllegalArgumentException("Expected '$name' to be VEC2 array, got $value") + + val vector = lua.newTable(value.size(), 0) + + for ((i, element) in value.withIndex()) { + if (element is JsonPrimitive && element.isString) { + vectorNumberInput.computeIfAbsent(element.asString) { HashSet() }.add(i + 1L to vector) + vector[i + 1L] = this[NodeParameterType.NUMBER, element.asString] + } else { + vector[i + 1L] = lua.from(element) + } + } + + table[name] = vector + } else { + table[name] = lua.from(value) + } + } + } + + this.parameters[nodeID] = table + return table + } + + fun setOutput(node: ActionNode, output: Table) { + for ((tableKey, out) in node.outputs) { + if (out.key != null) { + this[out.type, out.key] = output[tableKey] + + if (out.ephemeral) { + ephemeral.add(out.type to out.key) + } + } + } + } + + fun takeEphemerals(): Set> { + val ephemeral = ephemeral + this.ephemeral = HashSet() + return ephemeral + } + + fun clearEphemerals(ephemerals: Collection>) { + for ((type, key) in ephemerals) { + this[type, key] = null + } + } + + override fun getMetatable(): Table { + return Companion.metatable + } + + override fun setMetatable(mt: Table?): Table { + throw UnsupportedOperationException() + } + + override fun getUserValue(): Blackboard { + return this + } + + override fun setUserValue(value: Blackboard?): Blackboard { + throw UnsupportedOperationException() + } + + companion object { + private val metatable: ImmutableTable + + init { + val builder = ImmutableTable.Builder() + .add("get", luaFunction { self: Blackboard, type: ByteString, key: ByteString -> + returnBuffer.setTo(self[NodeParameterType.entries.valueOf(type.decode()), key.decode()]) + }) + .add("set", luaFunction { self: Blackboard, type: ByteString, key: ByteString, value: Any? -> + self[NodeParameterType.entries.valueOf(type.decode()), key.decode()] = value + }) + + for (type in NodeParameterType.entries) { + builder.add("get${type.funcName}", luaFunction { self: Blackboard, key: ByteString -> + returnBuffer.setTo(self[type, key.decode()]) + }) + + builder.add("set${type.funcName}", luaFunction { self: Blackboard, key: ByteString, value: Any? -> + self[type, key.decode()] = value + }) + } + + 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 new file mode 100644 index 00000000..3ef06be3 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/DecoratorNode.kt @@ -0,0 +1,71 @@ +package ru.dbotthepony.kstarbound.world.entities.behavior + +import org.apache.logging.log4j.LogManager +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 + +class DecoratorNode(val name: String, val parameters: Map, val child: AbstractBehaviorNode) : AbstractBehaviorNode() { + private var coroutine: Coroutine? = null + + override fun run(delta: Double, state: BehaviorState): Status { + var coroutine = coroutine + + if (coroutine == null) { + val parameters = state.blackboard.parameters(parameters, this) + val fn = state.functions[name] ?: throw RuntimeException("How? $name") + + try { + coroutine = state.lua.call(CoroutineLib.create(), fn)[0] as Coroutine + val result = state.lua.call(CoroutineLib.resume(), coroutine, parameters, state.blackboard, this) + val status = result[0] as Boolean + + if (!status && result.size >= 2) { + LOGGER.warn("Behavior DecoratorNode '$name' failed: ${result[1]}") + } + + return if (status) 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 + } + } + + // decorator runs its child on yield and is resumed with the child's status on success or failure + var status = Status.RUNNING + + while (status == Status.RUNNING) { + val childStatus = child.runAndReset(delta, state) + + if (childStatus == Status.SUCCESS || childStatus == Status.FAILURE) { + try { + val result = state.lua.call(CoroutineLib.resume(), coroutine, childStatus.asBoolean) + val execStatus = result[0] as Boolean + + if (!execStatus && result.size >= 2) { + LOGGER.warn("Behavior DecoratorNode '$name' failed: ${result[1]}") + } + + status = if (execStatus) Status.SUCCESS else Status.FAILURE + } catch (err: CallPausedException) { + LOGGER.error("Behavior DecoratorNode '$name' called blocking code on children return, which initiated pause. This is not supported.") + status = Status.FAILURE + } + } else { + return Status.RUNNING + } + } + + return status + } + + override fun reset() { + coroutine = null + } + + 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 new file mode 100644 index 00000000..0dd0e7cd --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/DynamicNode.kt @@ -0,0 +1,33 @@ +package ru.dbotthepony.kstarbound.world.entities.behavior + +import com.google.common.collect.ImmutableList +import org.classdump.luna.runtime.ExecutionContext +import ru.dbotthepony.kstarbound.lua.userdata.BehaviorState + +class DynamicNode(val children: ImmutableList) : AbstractBehaviorNode() { + private var index = 0 + + override fun run(delta: Double, state: BehaviorState): Status { + for ((i, node) in children.withIndex()) { + val status = node.runAndReset(delta, state) + + if (status == Status.FAILURE && index == i) + index++ + else if (i < index && (status == Status.SUCCESS || status == Status.FAILURE)) { + node.reset() + index = i + } + + if (status == Status.SUCCESS || index >= children.size) { + return status + } + } + + return Status.RUNNING + } + + override fun reset() { + children.forEach { it.reset() } + index = 0 + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/NodeParameterType.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/NodeParameterType.kt new file mode 100644 index 00000000..cb147b4a --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/NodeParameterType.kt @@ -0,0 +1,15 @@ +package ru.dbotthepony.kstarbound.world.entities.behavior + +import ru.dbotthepony.kstarbound.json.builder.IStringSerializable + +enum class NodeParameterType(override val jsonName: String, val funcName: String) : IStringSerializable { + JSON("json", "Json"), + ENTITY("entity", "Entity"), + POSITION("position", "Position"), + VEC2("vec2", "Vec2"), + NUMBER("number", "Number"), + BOOL("bool", "Bool"), + LIST("list", "List"), + TABLE("table", "Table"), + STRING("string", "String"); +} 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 new file mode 100644 index 00000000..09a8121b --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/ParallelNode.kt @@ -0,0 +1,56 @@ +package ru.dbotthepony.kstarbound.world.entities.behavior + +import com.google.common.collect.ImmutableList +import com.google.gson.JsonPrimitive +import org.classdump.luna.runtime.ExecutionContext +import ru.dbotthepony.kstarbound.defs.actor.behavior.NodeParameter +import ru.dbotthepony.kstarbound.lua.userdata.BehaviorState + +class ParallelNode(parameters: Map, val children: ImmutableList) : AbstractBehaviorNode() { + val successLimit: Int + val failLimit: Int + + init { + val value = (parameters["success"]?.value?.value as? JsonPrimitive)?.asInt ?: -1 + + if (value == -1) { + successLimit = children.size + } else { + successLimit = value + } + } + + init { + val value = (parameters["fail"]?.value?.value as? JsonPrimitive)?.asInt ?: -1 + + if (value == -1) { + failLimit = children.size + } else { + failLimit = value + } + } + + override fun run(delta: Double, state: BehaviorState): Status { + var failed = 0 + var succeeded = 0 + + for (node in children) { + val status = node.runAndReset(delta, state) + + if (status == Status.SUCCESS) + succeeded++ + else if (status == Status.FAILURE) + failed++ + + if (succeeded >= successLimit || failed >= failLimit) { + return if (succeeded >= successLimit) Status.SUCCESS else Status.FAILURE + } + } + + return Status.RUNNING + } + + override fun reset() { + children.forEach { it.reset() } + } +} 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 new file mode 100644 index 00000000..5a7e821f --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/RandomizeNode.kt @@ -0,0 +1,24 @@ +package ru.dbotthepony.kstarbound.world.entities.behavior + +import com.google.common.collect.ImmutableList +import org.classdump.luna.runtime.ExecutionContext +import ru.dbotthepony.kstarbound.lua.userdata.BehaviorState + +class RandomizeNode(val children: ImmutableList) : AbstractBehaviorNode() { + var index = -1 + + override fun run(delta: Double, state: BehaviorState): Status { + if (index == -1 && children.isNotEmpty()) + index = state.lua.random.nextInt(children.size) + + if (index == -1) + return Status.FAILURE + + return children[index].runAndReset(delta, state) + } + + 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 new file mode 100644 index 00000000..7a2d9376 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/behavior/SequenceNode.kt @@ -0,0 +1,28 @@ +package ru.dbotthepony.kstarbound.world.entities.behavior + +import com.google.common.collect.ImmutableList +import org.classdump.luna.runtime.ExecutionContext +import ru.dbotthepony.kstarbound.lua.userdata.BehaviorState + +class SequenceNode(val children: ImmutableList) : AbstractBehaviorNode() { + private var index = 0 + + override fun run(delta: Double, state: BehaviorState): Status { + while (index < children.size) { + val child = children[index] + val status = child.runAndReset(delta, state) + + if (status == Status.FAILURE || status == Status.RUNNING) + return status + + index++ + } + + return Status.SUCCESS + } + + override fun reset() { + children.forEach { it.reset() } + index = 0 + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/PlayerEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/PlayerEntity.kt index e0e7710e..6865280d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/PlayerEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/PlayerEntity.kt @@ -5,6 +5,7 @@ import ru.dbotthepony.kommons.io.writeBinaryString import ru.dbotthepony.kommons.util.getValue import ru.dbotthepony.kommons.util.setValue import ru.dbotthepony.kstarbound.Globals +import ru.dbotthepony.kstarbound.defs.DamageNotification import ru.dbotthepony.kstarbound.defs.DamageSource import ru.dbotthepony.kstarbound.defs.EntityType import ru.dbotthepony.kstarbound.defs.HitType @@ -17,6 +18,7 @@ import ru.dbotthepony.kstarbound.json.builder.IStringSerializable import ru.dbotthepony.kstarbound.math.AABB import ru.dbotthepony.kstarbound.math.Interpolator import ru.dbotthepony.kstarbound.math.vector.Vector2d +import ru.dbotthepony.kstarbound.network.packets.DamageRequestPacket import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean import ru.dbotthepony.kstarbound.network.syncher.networkedData import ru.dbotthepony.kstarbound.network.syncher.networkedEnum @@ -65,6 +67,8 @@ class PlayerEntity() : HumanoidActorEntity() { var gamemode = PlayerGamemode.CASUAL + override var description: String = "This guy has nothing to say to himself." + override fun writeNetwork(stream: DataOutputStream, isLegacy: Boolean) { stream.writeBinaryString(uniqueID.get() ?: "") stream.writeBinaryString(description) @@ -107,10 +111,6 @@ class PlayerEntity() : HumanoidActorEntity() { movement.resetBaseParameters(Globals.player.movementParameters) } - override val health: Double - get() = statusController.resources["health"]!!.value - override val maxHealth: Double - get() = statusController.resources["health"]!!.maxValue!! override val damageBarType: DamageBarType get() = DamageBarType.DEFAULT override val name: String @@ -138,23 +138,30 @@ class PlayerEntity() : HumanoidActorEntity() { get() = state == State.TELEPORT_IN || state == State.TELEPORT_OUT override val damageHitbox: List - get() = movement.computeLocalHitboxes() + get() = movement.computeGlobalHitboxes() override fun potentiallyCanBeHit( source: DamageSource, attacker: AbstractEntity?, inflictor: AbstractEntity? ): Boolean { - return !isAdmin && !isDead && !isTeleporting && (statusController.resources["invulnerable"]?.value ?: 0.0) <= 0.0 + return !isAdmin && !isDead && !isTeleporting && !statusController.statPositive("invulnerable") } override fun queryHit(source: DamageSource, attacker: AbstractEntity?, inflictor: AbstractEntity?): HitType? { - if (source.intersect(world.geometry, movement.computeLocalHitboxes())) + if (source.intersect(world.geometry, movement.computeGlobalHitboxes())) return HitType.HIT return null } + override fun experienceDamage(damage: DamageRequestPacket): List { + if (!isAdmin && !isDead && !isTeleporting && !statusController.statPositive("invulnerable")) + return statusController.experienceDamage(damage.request) + + return listOf() + } + var uuid: UUID by Delegates.notNull() private set } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/LoungeableObject.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/LoungeableObject.kt index e34d3cc6..b277cf64 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/LoungeableObject.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/LoungeableObject.kt @@ -116,6 +116,6 @@ class LoungeableObject(config: Registry.Entry) : WorldObject(c companion object { private val vectors by lazy { Starbound.gson.getAdapter(Vector2d::class.java) } private val vectorsList by lazy { Starbound.gson.getAdapter(object : TypeToken>() {}) } - private val statusEffects by lazy { Starbound.gson.getAdapter(object : TypeToken>>() {}) } + private val statusEffects by lazy { Starbound.gson.getAdapter(object : TypeToken>() {}) } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/PlantEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/PlantEntity.kt index b0594d19..32af9a48 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/PlantEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/PlantEntity.kt @@ -61,6 +61,7 @@ import ru.dbotthepony.kstarbound.network.syncher.networkedEventCounter import ru.dbotthepony.kstarbound.network.syncher.networkedFloat import ru.dbotthepony.kstarbound.server.world.ServerWorld import ru.dbotthepony.kstarbound.util.AssetPathStack +import ru.dbotthepony.kstarbound.util.asStringOrNull import ru.dbotthepony.kstarbound.util.random.nextRange import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.world.PIXELS_IN_STARBOUND_UNIT @@ -147,6 +148,11 @@ class PlantEntity() : TileEntity() { } } + override val name: String + get() = descriptions["default"]?.asStringOrNull ?: "Some indescribable horror" + override val description: String + get() = "" + override fun deserialize(data: JsonObject) { super.deserialize(data) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/PlantPieceEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/PlantPieceEntity.kt index ea8c9447..3077a08a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/PlantPieceEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/PlantPieceEntity.kt @@ -76,6 +76,10 @@ class PlantPieceEntity() : DynamicEntity() { var isFirst = false private set + override val name: String + get() = TODO("Not yet implemented") + override var description: String = "" + var stemConfig: JsonObject = JsonObject() private set var foliageConfig: JsonObject = JsonObject() @@ -267,7 +271,7 @@ class PlantPieceEntity() : DynamicEntity() { if (!isRemote) { // TODO: think up a better curve then sin - val rotationAcceleration = 0.01 * world.gravityAt(position).length * rotationRate.sign * delta + val rotationAcceleration = 0.01 * world.chunkMap.gravityAt(position).length * rotationRate.sign * delta if (movement.rotation.absoluteValue > rotationCap) rotationRate -= rotationAcceleration @@ -284,7 +288,7 @@ class PlantPieceEntity() : DynamicEntity() { } } - if ((timeToLive <= 0.0 || world.gravityAt(position).lengthSquared == 0.0) && !spawnedDrops) { + if ((timeToLive <= 0.0 || world.chunkMap.gravityAt(position).lengthSquared == 0.0) && !spawnedDrops) { spawnedDrops = true for (piece in piecesInternal) { 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 72badc6d..5880ddb9 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 @@ -146,6 +146,11 @@ open class WorldObject(val config: Registry.Entry) : TileEntit } } + override val name: String + get() = "" + override val description: String + get() = "" + override fun serialize(data: JsonObject) { super.serialize(data) data["name"] = config.key @@ -338,8 +343,6 @@ open class WorldObject(val config: Registry.Entry) : TileEntit } } - - fun serialize(): JsonObject { return JsonObject().also { it["connections"] = connectionsInternal.stream().map { jsonArrayOf(it.entityLocation, it.index) }.collect(JsonArrayCollector) @@ -545,9 +548,6 @@ open class WorldObject(val config: Registry.Entry) : TileEntit } updateMaterialSpacesNow() - - provideWorldBindings(world, lua) - provideWorldObjectBindings(this, lua) provideEntityBindings(this, lua) provideAnimatorBindings(animator, lua) lua.attach(config.value.scripts) @@ -888,22 +888,6 @@ open class WorldObject(val config: Registry.Entry) : TileEntit private val directions by lazy { Starbound.gson.getAdapter(Direction::class.java) } private val vectors by lazy { Starbound.gson.getAdapter(Vector2i::class.java) } - fun fromJson(content: JsonObject): WorldObject { - val prototype = Registries.worldObjects[content["name"]?.asString ?: throw IllegalArgumentException("Missing object name")] ?: throw IllegalArgumentException("No such object defined for '${content["name"]}'") - - val result = when (prototype.value.objectType) { - ObjectType.OBJECT -> WorldObject(prototype) - ObjectType.LOUNGEABLE -> LoungeableObject(prototype) - ObjectType.CONTAINER -> ContainerObject(prototype) - ObjectType.FARMABLE -> TODO("ObjectType.FARMABLE") - ObjectType.TELEPORTER -> TODO("ObjectType.TELEPORTER") - ObjectType.PHYSICS -> TODO("ObjectType.PHYSICS") - } - - result.deserialize(content) - return result - } - fun create(prototype: Registry.Entry, position: Vector2i = Vector2i.ZERO, parameters: JsonObject = JsonObject()): WorldObject? { val result = when (prototype.value.objectType) { ObjectType.OBJECT -> WorldObject(prototype) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/CollisionType.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/CollisionType.kt index 67ad8c2a..2b1a5675 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/CollisionType.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/CollisionType.kt @@ -3,16 +3,59 @@ package ru.dbotthepony.kstarbound.world.physics import ru.dbotthepony.kstarbound.json.builder.IStringSerializable import java.util.* -enum class CollisionType(override val jsonName: String, val isEmpty: Boolean, val isSolidCollision: Boolean, val isTileCollision: Boolean) : IStringSerializable { - // not loaded, block collisions by default - NULL("Null", true, true, false), - // air - NONE("None", true, false, false), - // including stairs made of platforms - PLATFORM("Platform", false, false, false), - DYNAMIC("Dynamic", false, true, false), - SLIPPERY("Slippery", false, true, true), - BLOCK("Block", false, true, true); +enum class CollisionType( + override val jsonName: String, + + /** + * Consider as solid when calculating collisions + */ + val isSolidCollision: Boolean, + + /** + * Consider as solid if we are standing on it when calculating collisions + * + * We deviate from original engine here, we consider [DYNAMIC] as solid floor, + * where original engine does not (despite closing trapdoors should make complete sense to be used as floor; + * [DYNAMIC] not being considered floor is the reason why crewmates jump over vertical ship hatches on BYOS ships and similar) + */ + val isFloorCollision: Boolean, + + /** + * Consider as solid when pathfinding + */ + val isSolidNonDynamic: Boolean +) : IStringSerializable { + /** + * Not loaded, block collisions by default + */ + NULL("Null", true, true, true), + + /** + * Air + */ + NONE("None", false, false, false), + + /** + * Platforms themselves and including stairs made of platforms + */ + PLATFORM("Platform", false, true, false), + + /** + * Collision formed dynamically and can be changed at any time, e.g. by doors + * + * Locked doors use [BLOCK] instead of [DYNAMIC] though, because their "open" condition can't be realistically predicted + */ + DYNAMIC("Dynamic", true, true, false), + + /** + * Solid collision; by default used by bottom and top world borders + */ + SLIPPERY("Slippery", true, true, true), + + /** + * Solid collision by any means + */ + BLOCK("Block", true, true, true); fun maxOf(other: CollisionType): CollisionType { if (this === NULL || other === NULL) @@ -25,6 +68,5 @@ enum class CollisionType(override val jsonName: String, val isEmpty: Boolean, va companion object { val SOLID: Set = Collections.unmodifiableSet(EnumSet.copyOf(entries.filter { it.isSolidCollision }.toSet())) - val TILE: Set = Collections.unmodifiableSet(EnumSet.copyOf(entries.filter { it.isTileCollision }.toSet())) } } diff --git a/src/test/kotlin/ru/dbotthepony/kstarbound/test/WorldTests.kt b/src/test/kotlin/ru/dbotthepony/kstarbound/test/WorldTests.kt index cd23096a..e759745d 100644 --- a/src/test/kotlin/ru/dbotthepony/kstarbound/test/WorldTests.kt +++ b/src/test/kotlin/ru/dbotthepony/kstarbound/test/WorldTests.kt @@ -54,6 +54,10 @@ object WorldTests { get() = TODO("Not yet implemented") override val type: EntityType get() = TODO("Not yet implemented") + override val name: String + get() = TODO("Not yet implemented") + override val description: String + get() = TODO("Not yet implemented") override fun writeNetwork(stream: DataOutputStream, isLegacy: Boolean) { TODO("Not yet implemented")