From 918b6ff95f6e1d44186607e89d78e3ba2f7f02e1 Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Sat, 30 Nov 2024 16:46:43 +0700 Subject: [PATCH] More work on NPCs, async network messages handling support --- ADDITIONS.md | 5 + build.gradle.kts | 2 - .../kotlin/ru/dbotthepony/kstarbound/Ext.kt | 4 +- .../ru/dbotthepony/kstarbound/Globals.kt | 5 + .../ru/dbotthepony/kstarbound/Registries.kt | 3 + .../ru/dbotthepony/kstarbound/Registry.kt | 58 ++- .../ru/dbotthepony/kstarbound/Starbound.kt | 39 +- .../kstarbound/client/ClientConnection.kt | 13 +- .../network/packets/ChunkCellsPacket.kt | 2 +- .../network/packets/ForgetChunkPacket.kt | 2 +- .../network/packets/ForgetEntityPacket.kt | 2 +- .../client/network/packets/JoinWorldPacket.kt | 2 +- .../network/packets/LeaveWorldPacket.kt | 2 +- .../kstarbound/defs/ColorReplacements.kt | 6 +- .../ru/dbotthepony/kstarbound/defs/Damage.kt | 31 +- .../dbotthepony/kstarbound/defs/EntityType.kt | 30 +- .../kstarbound/defs/actor/DanceDefinition.kt | 28 ++ .../kstarbound/defs/actor/GlobalNPCConfig.kt | 15 + .../kstarbound/defs/actor/HumanoidConfig.kt | 62 +++ .../{HumanoidData.kt => HumanoidIdentity.kt} | 39 +- .../kstarbound/defs/actor/MoveControlType.kt | 11 + .../kstarbound/defs/actor/NPCVariant.kt | 387 +++++++++++++++- .../kstarbound/defs/actor/Personality.kt | 38 +- .../kstarbound/defs/actor/Species.kt | 102 ++++- .../kstarbound/defs/actor/Types.kt | 6 + .../defs/actor/player/PlayerConfig.kt | 2 + .../kstarbound/defs/dungeon/DungeonBrush.kt | 90 +++- .../kstarbound/defs/dungeon/DungeonWorld.kt | 84 +++- .../kstarbound/defs/item/ItemDescriptor.kt | 8 +- .../defs/quest/QuestArcDescriptor.kt | 17 +- .../kstarbound/defs/quest/QuestDescriptor.kt | 8 +- .../kstarbound/defs/tile/RenderTemplate.kt | 6 +- .../kstarbound/defs/world/BushVariant.kt | 2 +- .../dbotthepony/kstarbound/io/StreamCodec.kt | 18 +- .../dbotthepony/kstarbound/item/ItemStack.kt | 7 + .../dbotthepony/kstarbound/json/JsonPatch.kt | 7 +- .../kstarbound/json/VersionedAdapter.kt | 2 +- .../kstarbound/json/builder/Annotations.kt | 7 +- .../kstarbound/json/builder/FactoryAdapter.kt | 43 +- .../kstarbound/lua/bindings/EntityBindings.kt | 4 + .../lua/bindings/MonsterBindings.kt | 7 - .../bindings/MovementControllerBindings.kt | 12 +- .../kstarbound/lua/bindings/NPCBindings.kt | 222 +++++++++ .../lua/bindings/StatusControllerBindings.kt | 4 +- .../lua/bindings/WorldEntityBindings.kt | 4 +- .../lua/bindings/WorldObjectBindings.kt | 3 +- .../ru/dbotthepony/kstarbound/network/API.kt | 8 +- .../kstarbound/network/Connection.kt | 2 +- .../kstarbound/network/PacketRegistry.kt | 12 + .../packets/ClientContextUpdatePacket.kt | 4 +- .../packets/DamageNotificationPacket.kt | 4 +- .../network/packets/DamageRequestPacket.kt | 6 +- .../network/packets/EntityCreatePacket.kt | 6 +- .../network/packets/EntityDestroyPacket.kt | 4 +- .../network/packets/EntityMessagePacket.kt | 5 +- .../packets/EntityMessageResponsePacket.kt | 5 +- .../network/packets/EntityUpdateSetPacket.kt | 6 +- .../network/packets/HitRequestPacket.kt | 6 +- .../kstarbound/network/packets/PingPong.kt | 10 +- .../network/packets/ProtocolRequestPacket.kt | 5 +- .../network/packets/ProtocolResponsePacket.kt | 2 +- .../network/packets/StepUpdatePacket.kt | 7 +- .../clientbound/CelestialResponsePacket.kt | 2 +- .../CentralStructureUpdatePacket.kt | 2 +- .../packets/clientbound/ChatReceivePacket.kt | 2 +- .../clientbound/ConnectFailurePacket.kt | 2 +- .../clientbound/ConnectSuccessPacket.kt | 2 +- .../clientbound/EntityInteractResultPacket.kt | 4 +- .../clientbound/EnvironmentUpdatePacket.kt | 2 +- .../FindUniqueEntityResponsePacket.kt | 2 +- .../packets/clientbound/GiveItemPacket.kt | 2 +- .../clientbound/HandshakeChallengePacket.kt | 2 +- .../clientbound/LegacyTileUpdatePacket.kt | 4 +- .../clientbound/PlayerWarpResultPacket.kt | 2 +- .../clientbound/ServerDisconnectPacket.kt | 2 +- .../packets/clientbound/ServerInfoPacket.kt | 2 +- .../clientbound/SetPlayerStartPacket.kt | 2 +- .../clientbound/SystemObjectCreatePacket.kt | 2 +- .../clientbound/SystemObjectDestroyPacket.kt | 2 +- .../clientbound/SystemShipCreatePacket.kt | 2 +- .../clientbound/SystemShipDestroyPacket.kt | 2 +- .../clientbound/SystemWorldStartPacket.kt | 2 +- .../clientbound/SystemWorldUpdatePacket.kt | 2 +- .../clientbound/TileDamageUpdatePacket.kt | 2 +- .../TileModificationFailurePacket.kt | 2 +- .../clientbound/UniverseTimeUpdatePacket.kt | 2 +- .../UpdateDungeonBreathablePacket.kt | 2 +- .../clientbound/UpdateDungeonGravityPacket.kt | 2 +- .../UpdateDungeonProtectionPacket.kt | 2 +- .../UpdateWorldPropertiesPacket.kt | 4 +- .../packets/clientbound/WorldStartPacket.kt | 2 +- .../packets/clientbound/WorldStopPacket.kt | 2 +- .../serverbound/CelestialRequestPacket.kt | 5 +- .../packets/serverbound/ChatSendPacket.kt | 5 +- .../serverbound/ClientConnectPacket.kt | 2 +- .../ClientDisconnectRequestPacket.kt | 2 +- .../packets/serverbound/ConnectWirePacket.kt | 12 +- .../serverbound/DamageTileGroupPacket.kt | 2 +- .../serverbound/DisconnectAllWiresPacket.kt | 2 +- .../serverbound/EntityInteractPacket.kt | 4 +- .../serverbound/FindUniqueEntityPacket.kt | 7 +- .../packets/serverbound/FlyShipPacket.kt | 5 +- .../serverbound/HandshakeResponsePacket.kt | 5 +- .../serverbound/ModifyTileListPacket.kt | 2 +- .../packets/serverbound/PlayerWarpPacket.kt | 5 +- .../packets/serverbound/RequestDropPacket.kt | 8 +- .../packets/serverbound/SpawnEntityPacket.kt | 4 +- .../WorldClientStateUpdatePacket.kt | 2 +- .../WorldStartAcknowledgePacket.kt | 2 +- .../kstarbound/server/ServerConnection.kt | 95 +++- .../kstarbound/server/world/ServerWorld.kt | 4 +- .../server/world/ServerWorldTracker.kt | 52 +-- .../kstarbound/util/AssetPathStack.kt | 20 + .../ru/dbotthepony/kstarbound/util/Clocks.kt | 13 +- .../kstarbound/util/HashTableInterner.kt | 1 - .../ru/dbotthepony/kstarbound/util/Utils.kt | 13 + .../kstarbound/util/random/RandomUtils.kt | 37 +- .../dbotthepony/kstarbound/world/Direction.kt | 2 +- .../ru/dbotthepony/kstarbound/world/World.kt | 23 +- .../world/entities/AbstractEntity.kt | 60 ++- .../kstarbound/world/entities/ActorEntity.kt | 58 ++- .../world/entities/ActorMovementController.kt | 73 ++- .../world/entities/AnchorNetworkState.kt | 22 + .../kstarbound/world/entities/AnchorState.kt | 62 ++- .../world/entities/DynamicEntity.kt | 16 +- .../world/entities/EffectEmitter.kt | 8 +- .../world/entities/HumanoidActorEntity.kt | 381 +++++++++++++++- .../world/entities/MonsterEntity.kt | 69 ++- .../kstarbound/world/entities/NPCEntity.kt | 421 +++++++++++++++++- .../world/entities/StatusController.kt | 49 +- .../world/entities/api/LoungeableEntity.kt | 15 + .../world/entities/player/PlayerEntity.kt | 48 +- .../world/entities/tile/LoungeableObject.kt | 12 +- .../world/entities/tile/TileEntity.kt | 2 +- 134 files changed, 2802 insertions(+), 437 deletions(-) create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/DanceDefinition.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/GlobalNPCConfig.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/HumanoidConfig.kt rename src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/{HumanoidData.kt => HumanoidIdentity.kt} (78%) create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/MoveControlType.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/NPCBindings.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AnchorNetworkState.kt diff --git a/ADDITIONS.md b/ADDITIONS.md index 42f1b977..f3ca40ee 100644 --- a/ADDITIONS.md +++ b/ADDITIONS.md @@ -154,6 +154,11 @@ In addition to `add`, `multiply`, `merge` and `override` new merge methods are a * `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 +## npc + * `npc.setDropPools(dropPools: Table?)` now accepts `nil` + * Added `npc.beginSecondaryFire()` which is alias for `npc.beginAltFire()` + * Added `npc.endSecondaryFire()` which is alias for `npc.endAltFire()` + ## status * Implemented `status.appliesEnvironmentStatusEffects(): Boolean`, which exists in original engine's code but was never hooked up to Lua bindings diff --git a/build.gradle.kts b/build.gradle.kts index 0af64b6d..215558a9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,4 @@ -import org.gradle.internal.jvm.Jvm - plugins { kotlin("jvm") version "1.9.10" id("me.champeau.jmh") version "0.7.1" diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Ext.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Ext.kt index 7387e7a7..f67be762 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Ext.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Ext.kt @@ -37,11 +37,11 @@ inline fun GsonBuilder.registerTypeAdapter(noinline factory: (Gson) fun Array.stream(): Stream = Arrays.stream(this) -operator fun ThreadLocal.getValue(thisRef: Any, property: KProperty<*>): T { +operator fun ThreadLocal.getValue(thisRef: Any?, property: KProperty<*>): T { return get() } -operator fun ThreadLocal.setValue(thisRef: Any, property: KProperty<*>, value: T) { +operator fun ThreadLocal.setValue(thisRef: Any?, property: KProperty<*>, value: T) { set(value) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Globals.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Globals.kt index be905901..92eba13a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Globals.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Globals.kt @@ -17,6 +17,7 @@ import ru.dbotthepony.kstarbound.defs.ElementalDamageType import ru.dbotthepony.kstarbound.defs.MovementParameters import ru.dbotthepony.kstarbound.defs.world.SpawnerConfig import ru.dbotthepony.kstarbound.defs.UniverseServerConfig +import ru.dbotthepony.kstarbound.defs.actor.GlobalNPCConfig import ru.dbotthepony.kstarbound.defs.world.WorldServerConfig import ru.dbotthepony.kstarbound.defs.actor.player.PlayerConfig import ru.dbotthepony.kstarbound.defs.actor.player.ShipUpgrades @@ -129,6 +130,9 @@ object Globals { var elementalTypes by Delegates.notNull>() private set + var npcs by Delegates.notNull() + private set + private var profanityFilterInternal by Delegates.notNull>() val profanityFilter: ImmutableSet by lazy { @@ -237,6 +241,7 @@ object Globals { tasks.add(load("/ships/shipupgrades.config", ::shipUpgrades)) tasks.add(load("/quests/quests.config", ::quests)) tasks.add(load("/spawning.config", ::spawner)) + tasks.add(load("/npcs/npc.config", ::npcs)) 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/Registries.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt index 1e56b13c..af2e067f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt @@ -38,6 +38,7 @@ 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.DanceDefinition import ru.dbotthepony.kstarbound.defs.actor.behavior.BehaviorDefinition import ru.dbotthepony.kstarbound.defs.actor.behavior.BehaviorNodeDefinition import ru.dbotthepony.kstarbound.defs.monster.MonsterPaletteSwap @@ -112,6 +113,7 @@ 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()) } val damageKinds = Registry("damage kind").also(registriesInternal::add).also { adapters.add(it.adapter()) } + val dance = Registry("dance").also(registriesInternal::add).also { adapters.add(it.adapter()) } private val monsterParts = HashMap, HashMap>>() private val loggedMonsterPartMisses = Collections.synchronizedSet(ObjectOpenHashSet>()) @@ -292,6 +294,7 @@ object Registries { tasks.addAll(loadRegistry(projectiles, patchTree, fileTree["projectile"] ?: listOf(), key(ProjectileDefinition::projectileName))) tasks.addAll(loadRegistry(behavior, patchTree, fileTree["behavior"] ?: listOf(), key(BehaviorDefinition::name))) tasks.addAll(loadRegistry(damageKinds, patchTree, fileTree["damage"] ?: listOf(), key(DamageKind::kind))) + tasks.addAll(loadRegistry(dance, patchTree, fileTree["dance"] ?: listOf(), key(DanceDefinition::name))) tasks.addAll(loadCombined(behaviorNodes, fileTree["nodes"] ?: listOf(), patchTree)) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt index 7561d004..3a083fd0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt @@ -10,10 +10,15 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap import it.unimi.dsi.fastutil.longs.LongOpenHashSet import it.unimi.dsi.fastutil.objects.Object2ObjectFunction import org.apache.logging.log4j.LogManager +import ru.dbotthepony.kommons.io.writeBinaryString import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.util.XXHash64 +import ru.dbotthepony.kstarbound.io.StreamCodec +import ru.dbotthepony.kstarbound.io.readInternedString import ru.dbotthepony.kstarbound.util.limit +import java.io.DataInputStream +import java.io.DataOutputStream import java.util.Collections import java.util.concurrent.locks.ReentrantReadWriteLock import java.util.function.Supplier @@ -33,7 +38,7 @@ class Registry(val name: String, val storeJson: Boolean = true) { // 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 + // it won't easily explode memory usage of server private val loggedMisses = LongOpenHashSet() val keys: Map> = Collections.unmodifiableMap(keysInternal) @@ -44,6 +49,9 @@ class Registry(val name: String, val storeJson: Boolean = true) { abstract val entry: Entry? abstract val registry: Registry + val entryOrThrow: Entry + get() = entry ?: throw NoSuchElementException("No such ${registry.name}: ${key.map({ it }, { it.toString() }).limit()}") + val isPresent: Boolean get() = value != null @@ -53,6 +61,18 @@ class Registry(val name: String, val storeJson: Boolean = true) { val value: T? get() = entry?.value + inline fun ifPresent(block: (Entry) -> Unit) { + entry?.let(block) + } + + inline fun map(block: (Entry) -> R): KOptional { + return entry?.let { KOptional(block(it)) } ?: KOptional() + } + + inline fun mapOrThrow(block: (Entry) -> R): R { + return block(entryOrThrow) + } + final override fun get(): Entry? { return entry } @@ -106,7 +126,7 @@ class Registry(val name: String, val storeJson: Boolean = true) { } override fun toString(): String { - return "Entry of $name at $key/${id ?: "-"}" + return "E/$name/$key/${id ?: "-"}" } override val registry: Registry @@ -139,7 +159,11 @@ class Registry(val name: String, val storeJson: Boolean = true) { } override fun toString(): String { - return "Ref of $name at $key/${if (entry != null) "bound" else "missing"}" + if (entry == null) { + return "R/$name/${key.map({ "'$it'" }, { it.toString() })}/!" + } else { + return "R/$name/${entry!!.key}/${entry?.id ?: "-"}" + } } override val registry: Registry @@ -329,6 +353,34 @@ class Registry(val name: String, val storeJson: Boolean = true) { val emptyRef = ref("") + val nameRefCodec = object : StreamCodec> { + override fun read(stream: DataInputStream): Ref { + return ref(stream.readInternedString()) + } + + override fun write(stream: DataOutputStream, value: Ref) { + stream.writeBinaryString(value.key.left()) + } + + override fun copy(value: Ref): Ref { + return value + } + } + + val nameEntryCodec = object : StreamCodec> { + override fun read(stream: DataInputStream): Entry { + return getOrThrow(stream.readInternedString()) + } + + override fun write(stream: DataOutputStream, value: Entry) { + stream.writeBinaryString(value.key) + } + + override fun copy(value: Entry): Entry { + return value + } + } + companion object { private val LOGGER = LogManager.getLogger() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt index 8d06f39f..176033f9 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt @@ -4,19 +4,18 @@ import com.github.benmanes.caffeine.cache.AsyncCacheLoader import com.github.benmanes.caffeine.cache.Caffeine import com.github.benmanes.caffeine.cache.Interner import com.github.benmanes.caffeine.cache.Scheduler -import com.google.common.base.Predicate import com.google.gson.* import com.google.gson.stream.JsonReader import it.unimi.dsi.fastutil.objects.ObjectArraySet import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Runnable import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.async import kotlinx.coroutines.delay import kotlinx.coroutines.future.asCompletableFuture import kotlinx.coroutines.future.await import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import org.apache.logging.log4j.LogManager import org.classdump.luna.compiler.CompilerChunkLoader import org.classdump.luna.compiler.CompilerSettings @@ -34,6 +33,7 @@ import ru.dbotthepony.kstarbound.defs.image.Image import ru.dbotthepony.kstarbound.defs.image.SpriteReference import ru.dbotthepony.kstarbound.defs.actor.player.BlueprintLearnList import ru.dbotthepony.kstarbound.defs.animation.Particle +import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor import ru.dbotthepony.kstarbound.defs.quest.QuestParameter import ru.dbotthepony.kstarbound.defs.world.CelestialParameters import ru.dbotthepony.kstarbound.defs.world.VisitableWorldParametersType @@ -97,17 +97,11 @@ import java.util.concurrent.ForkJoinPool import java.util.concurrent.ForkJoinPool.ForkJoinWorkerThreadFactory import java.util.concurrent.ForkJoinWorkerThread import java.util.concurrent.Future -import java.util.concurrent.LinkedBlockingQueue -import java.util.concurrent.ThreadFactory -import java.util.concurrent.ThreadPoolExecutor import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger -import java.util.concurrent.locks.LockSupport import java.util.random.RandomGenerator import kotlin.NoSuchElementException import kotlin.collections.ArrayList -import kotlin.math.max -import kotlin.math.min object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLocator { const val ENGINE_VERSION = "0.0.1" @@ -122,6 +116,11 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca const val DEDUP_CELL_STATES = true const val USE_CAFFEINE_INTERNER = false const val USE_INTERNER = true + // enables a fuckton of runtime checks for data which doesn't make much sense + // especially for data which will explode legacy client + // also changes some constants + const val DEBUG_BUILD = true + // ---- fun interner(): Interner { if (!USE_INTERNER) @@ -417,6 +416,8 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca registerTypeAdapter(LongRangeAdapter) + registerTypeAdapter(ItemDescriptor.Adapter) + create() } @@ -782,10 +783,22 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca return result.toString() } - fun generateName(asset: String, random: RandomGenerator): String { - val load = loadJsonAsset(asset).get() as? JsonArray ?: return "missingasset" + @Deprecated("Blocks thread it is running in, consider generateNameAsync instead", replaceWith = ReplaceWith("this.generateNameAsync")) + fun generateName(asset: String, random: RandomGenerator, maxTries: Int = 500): String { + return runBlocking { + generateNameAsync(asset, random, maxTries) + } + } - var tries = 500 + @Deprecated("Blocks thread it is running in, consider generateNameAsync instead", replaceWith = ReplaceWith("this.generateNameAsync")) + fun generateName(asset: String, seed: Long, maxTries: Int = 500) = generateName(asset, random(seed), maxTries) + @Deprecated("Blocks thread it is running in, consider generateNameAsync instead", replaceWith = ReplaceWith("this.generateNameAsync")) + fun generateName(asset: String, maxTries: Int = 500) = generateName(asset, System.nanoTime(), maxTries) + + suspend fun generateNameAsync(asset: String, random: RandomGenerator, maxTries: Int = 500): String { + val load = loadJsonAsset(asset).await() as? JsonArray ?: return "missingasset:$asset" + + var tries = maxTries var result = "" while (tries-- > 0 && (result.isEmpty() || result.lowercase() in Globals.profanityFilter)) @@ -794,7 +807,7 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca return result } - fun generateName(asset: String, seed: Long) = generateName(asset, random(seed)) - fun generateName(asset: String) = generateName(asset, System.nanoTime()) + suspend fun generateNameAsync(asset: String, seed: Long, maxTries: Int = 500) = generateNameAsync(asset, random(seed), maxTries) + suspend fun generateNameAsync(asset: String, maxTries: Int = 500) = generateNameAsync(asset, System.nanoTime(), maxTries) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientConnection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientConnection.kt index eca4fdac..4c5824fe 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientConnection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientConnection.kt @@ -8,6 +8,7 @@ import io.netty.channel.local.LocalAddress import io.netty.channel.local.LocalChannel import io.netty.channel.socket.nio.NioSocketChannel import it.unimi.dsi.fastutil.ints.IntAVLTreeSet +import kotlinx.coroutines.runBlocking import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kstarbound.Starbound @@ -78,11 +79,13 @@ class ClientConnection(val client: StarboundClient, type: ConnectionType) : Conn override fun channelRead(ctx: ChannelHandlerContext, msg: Any) { if (msg is IClientPacket) { - try { - msg.play(this) - } catch (err: Throwable) { - LOGGER.error("Failed to read incoming packet $msg", err) - disconnect(err.toString()) + runBlocking { + try { + msg.play(this@ClientConnection) + } catch (err: Throwable) { + LOGGER.error("Failed to read incoming packet $msg", err) + disconnect(err.toString()) + } } } else { LOGGER.error("Unknown incoming packet type $msg") diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/ChunkCellsPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/ChunkCellsPacket.kt index f9cea229..18dca17d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/ChunkCellsPacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/ChunkCellsPacket.kt @@ -29,7 +29,7 @@ class ChunkCellsPacket(val pos: ChunkPos, val data: List) : IClie stream.writeCollection(data) { it.writeLegacy(stream) } } - override fun play(connection: ClientConnection) { + override suspend fun play(connection: ClientConnection) { connection.client.mailbox.execute { val chunk = connection.client.world?.chunkMap?.compute(pos.x, pos.y) ?: return@execute val itr = data.iterator() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/ForgetChunkPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/ForgetChunkPacket.kt index 90a75e5f..ec0a21ad 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/ForgetChunkPacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/ForgetChunkPacket.kt @@ -15,7 +15,7 @@ class ForgetChunkPacket(val pos: ChunkPos) : IClientPacket { stream.writeStruct2i(pos) } - override fun play(connection: ClientConnection) { + override suspend fun play(connection: ClientConnection) { connection.client.mailbox.execute { val world = connection.client.world ?: return@execute world.chunkMap.remove(pos) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/ForgetEntityPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/ForgetEntityPacket.kt index 84c013f4..e30f4f62 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/ForgetEntityPacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/ForgetEntityPacket.kt @@ -15,7 +15,7 @@ class ForgetEntityPacket(val uuid: UUID) : IClientPacket { stream.writeUUID(uuid) } - override fun play(connection: ClientConnection) { + override suspend fun play(connection: ClientConnection) { val world = connection.client.world ?: return } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/JoinWorldPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/JoinWorldPacket.kt index 0c75423f..d9e78860 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/JoinWorldPacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/JoinWorldPacket.kt @@ -21,7 +21,7 @@ data class JoinWorldPacket(val uuid: UUID, val geometry: WorldGeometry) : IClien geometry.write(stream) } - override fun play(connection: ClientConnection) { + override suspend fun play(connection: ClientConnection) { connection.client.mailbox.execute { connection.client.world = ClientWorld(connection.client, WorldTemplate(geometry)) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/LeaveWorldPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/LeaveWorldPacket.kt index 840444dd..854fd8a8 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/LeaveWorldPacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/LeaveWorldPacket.kt @@ -9,7 +9,7 @@ object LeaveWorldPacket : IClientPacket { } - override fun play(connection: ClientConnection) { + override suspend fun play(connection: ClientConnection) { connection.client.mailbox.execute { connection.client.world = null } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ColorReplacements.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ColorReplacements.kt index 93338b52..26445375 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ColorReplacements.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ColorReplacements.kt @@ -19,8 +19,12 @@ class ColorReplacements private constructor(private val mapping: Int2IntOpenHash return mapping.getOrDefault(color, color) } + private val asImageOperator by lazy { + "?replace;${mapping.int2IntEntrySet().joinToString(";") { "${RGBAColor.rgb(it.intKey).toHexStringRGB().substring(1)}=${RGBAColor.rgb(it.intValue).toHexStringRGB().substring(1)}" }}" + } + fun toImageOperator(): String { - return "replace;${mapping.int2IntEntrySet().joinToString(";") { "${RGBAColor.rgb(it.intKey).toHexStringRGB().substring(1)}=${RGBAColor.rgb(it.intValue).toHexStringRGB().substring(1)}" }}" + return asImageOperator } class Adapter(gson: Gson) : TypeAdapter() { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Damage.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Damage.kt index 244cc997..42770705 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Damage.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Damage.kt @@ -15,6 +15,7 @@ import ru.dbotthepony.kommons.io.readSignedVarInt import ru.dbotthepony.kommons.io.writeBinaryString import ru.dbotthepony.kommons.io.writeCollection import ru.dbotthepony.kommons.io.writeSignedVarInt +import ru.dbotthepony.kommons.io.writeStruct2d import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.io.readDouble @@ -153,7 +154,35 @@ data class TouchDamage( val damageSourceKind: String = "", val knockback: Double = 0.0, val statusEffects: ImmutableSet = ImmutableSet.of(), -) +) { + /** + * new protocol only + */ + constructor(stream: DataInputStream) : this( + ImmutableList.copyOf(stream.readCollection { readVector2d() }), + TeamType.entries[stream.readUnsignedByte()], + stream.readDouble(), + stream.readInternedString(), + stream.readDouble(), + ImmutableSet.copyOf(stream.readCollection { readInternedString() }) + ) + + /** + * new protocol only + */ + fun write(stream: DataOutputStream) { + stream.writeCollection(poly) { writeStruct2d(it) } + stream.writeByte(teamType.ordinal) + stream.writeDouble(damage) + stream.writeBinaryString(damageSourceKind) + stream.writeDouble(knockback) + stream.writeCollection(statusEffects) { writeBinaryString(it) } + } + + companion object { + val EMPTY = TouchDamage() + } +} @JsonFactory data class DamageNotification( diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/EntityType.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/EntityType.kt index 02e59c64..0692eece 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/EntityType.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/EntityType.kt @@ -4,6 +4,7 @@ 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.actor.NPCVariant import ru.dbotthepony.kstarbound.defs.monster.MonsterVariant import ru.dbotthepony.kstarbound.defs.`object`.ObjectType import ru.dbotthepony.kstarbound.fromJsonFast @@ -13,6 +14,7 @@ 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.NPCEntity import ru.dbotthepony.kstarbound.world.entities.ProjectileEntity import ru.dbotthepony.kstarbound.world.entities.player.PlayerEntity import ru.dbotthepony.kstarbound.world.entities.tile.ContainerObject @@ -24,7 +26,7 @@ import java.io.DataInputStream enum class EntityType(override val jsonName: String, val storeName: String, val canBeCreatedByClient: Boolean, val canBeSpawnedByClient: Boolean, val ephemeralIfSpawnedByClient: Boolean = true) : IStringSerializable { PLANT("plant", "PlantEntity", false, false) { - override fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity { + override suspend fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity { return PlantEntity(stream, isLegacy) } @@ -34,7 +36,7 @@ enum class EntityType(override val jsonName: String, val storeName: String, val }, OBJECT("object", "ObjectEntity", false, true) { - override fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity { + override suspend fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity { val name = stream.readInternedString() val parameters = stream.readJsonElement() val config = Registries.worldObjects.getOrThrow(name) @@ -59,7 +61,7 @@ enum class EntityType(override val jsonName: String, val storeName: String, val }, VEHICLE("vehicle", "VehicleEntity", false, true, false) { - override fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity { + override suspend fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity { TODO("VEHICLE") } @@ -69,7 +71,7 @@ enum class EntityType(override val jsonName: String, val storeName: String, val }, ITEM_DROP("itemDrop", "ItemDropEntity", false, true, false) { - override fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity { + override suspend fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity { return ItemDropEntity(stream, isLegacy) } @@ -79,7 +81,7 @@ enum class EntityType(override val jsonName: String, val storeName: String, val }, PLANT_DROP("plantDrop", "PlantDropEntity", false, false) { - override fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity { + override suspend fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity { return PlantPieceEntity(stream, isLegacy) } @@ -89,7 +91,7 @@ enum class EntityType(override val jsonName: String, val storeName: String, val }, PROJECTILE("projectile", "ProjectileEntity", true, true) { - override fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity { + override suspend fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity { return ProjectileEntity(Registries.projectiles.getOrThrow(stream.readBinaryString()), stream, isLegacy) } @@ -99,7 +101,7 @@ enum class EntityType(override val jsonName: String, val storeName: String, val }, STAGEHAND("stagehand", "StagehandEntity", true, true) { - override fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity { + override suspend fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity { TODO("STAGEHAND") } @@ -109,7 +111,7 @@ enum class EntityType(override val jsonName: String, val storeName: String, val }, MONSTER("monster", "MonsterEntity", false, false) { - override fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity { + override suspend fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity { return MonsterEntity(MonsterVariant.read(stream, isLegacy)) } @@ -132,17 +134,19 @@ enum class EntityType(override val jsonName: String, val storeName: String, val }, NPC("npc", "NpcEntity", false, false) { - override fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity { - TODO("NPC") + override suspend fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity { + return NPCEntity(NPCVariant.read(stream, isLegacy)) } override fun fromStorage(data: JsonObject): AbstractEntity { - TODO("NPC") + val entity = NPCEntity(Starbound.gson.fromJsonFast(data["npcVariant"], NPCVariant::class.java)) + entity.deserialize(data) + return entity } }, PLAYER("player", "PlayerEntity", true, false) { - override fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity { + override suspend fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity { return PlayerEntity(stream, isLegacy) } @@ -151,6 +155,6 @@ enum class EntityType(override val jsonName: String, val storeName: String, val } }; - abstract fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity + abstract suspend fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity abstract fun fromStorage(data: JsonObject): AbstractEntity } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/DanceDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/DanceDefinition.kt new file mode 100644 index 00000000..779dedcd --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/DanceDefinition.kt @@ -0,0 +1,28 @@ +package ru.dbotthepony.kstarbound.defs.actor + +import com.google.common.collect.ImmutableList +import com.google.common.collect.ImmutableSet +import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.math.vector.Vector2d + +@JsonFactory +data class DanceDefinition( + val name: String, + val states: ImmutableSet, + val cycle: Double, + val cyclic: Boolean, + val duration: Double, + val steps: ImmutableList +) { + @JsonFactory(asList = 0) + data class Step( + val bodyFrame: String? = null, + val frontArmFrame: String? = null, + val backArmFrame: String? = null, + val headOffset: Vector2d = Vector2d.ZERO, + val frontArmOffset: Vector2d = Vector2d.ZERO, + val backArmOffset: Vector2d = Vector2d.ZERO, + val frontArmRotation: Double = 0.0, + val backArmRotation: Double = 0.0, + ) +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/GlobalNPCConfig.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/GlobalNPCConfig.kt new file mode 100644 index 00000000..590e4a1f --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/GlobalNPCConfig.kt @@ -0,0 +1,15 @@ +package ru.dbotthepony.kstarbound.defs.actor + +import ru.dbotthepony.kstarbound.math.vector.Vector2d + +data class GlobalNPCConfig( + val hitDamageNotificationLimit: Int, + val emoteCooldown: Double, + val danceCooldown: Double, + val shieldHitSoundLimit: Double, + val blinkInterval: Vector2d, +) { + init { + require(hitDamageNotificationLimit >= 1) { "Pointless hitDamageNotificationLimit: $hitDamageNotificationLimit" } + } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/HumanoidConfig.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/HumanoidConfig.kt new file mode 100644 index 00000000..f63a01ab --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/HumanoidConfig.kt @@ -0,0 +1,62 @@ +package ru.dbotthepony.kstarbound.defs.actor + +import com.google.common.collect.ImmutableList +import com.google.gson.JsonObject +import ru.dbotthepony.kstarbound.defs.ActorMovementParameters +import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.math.vector.Vector2d + +@JsonFactory +data class HumanoidConfig( + val globalOffset: Vector2d, // in pixels + val headRunOffset: Vector2d, // in pixels + val headSwimOffset: Vector2d, // in pixels + val runFallOffset: Double, // in pixels + val duckOffset: Double, // in pixels + val headDuckOffset: Vector2d, // in pixels + val sitOffset: Double, // in pixels + val layOffset: Double, // in pixels + val headSitOffset: Vector2d, // in pixels + val headLayOffset: Vector2d, // in pixels + val recoilOffset: Vector2d, // in pixels + val mouthOffset: Vector2d, // in pixels + val feetOffset: Vector2d, // in pixels + + val bodyFullbright: Boolean = false, + + val headArmorOffset: Vector2d, // in pixels + val chestArmorOffset: Vector2d, // in pixels + val legsArmorOffset: Vector2d, // in pixels + val backArmorOffset: Vector2d, // in pixels + + val bodyHidden: Boolean = false, + + val armWalkSeq: ImmutableList, + val armRunSeq: ImmutableList, + + val walkBob: ImmutableList, // in pixels + val runBob: ImmutableList, // in pixels + val swimBob: ImmutableList, // in pixels + + val jumpBob: Double, + val frontArmRotationCenter: Vector2d, // in pixels + val backArmRotationCenter: Vector2d, // in pixels + val frontHandPosition: Vector2d, // in pixels + val backArmOffset: Vector2d, // in pixels + val vaporTrailFrames: Int, + val vaporTrailCycle: Double, + + val deathParticles: String, + val particleEmitters: JsonObject, + + val movementParameters: ActorMovementParameters, + + val personalities: ImmutableList, +) { + data class Timing( + val stateCycle: ImmutableList? = null, + val emoteCycle: ImmutableList? = null, + val stateFrames: ImmutableList? = null, + val emoteFrames: ImmutableList? = null, + ) +} \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/HumanoidData.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/HumanoidIdentity.kt similarity index 78% rename from src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/HumanoidData.kt rename to src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/HumanoidIdentity.kt index a335b6bb..e4b0b391 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/HumanoidData.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/HumanoidIdentity.kt @@ -6,6 +6,9 @@ import ru.dbotthepony.kommons.io.writeBinaryString import ru.dbotthepony.kommons.io.writeStruct2d import ru.dbotthepony.kommons.io.writeStruct2f import ru.dbotthepony.kommons.math.RGBAColor +import ru.dbotthepony.kstarbound.Registries +import ru.dbotthepony.kstarbound.Registry +import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.io.readColor import ru.dbotthepony.kstarbound.io.readInternedString @@ -17,9 +20,9 @@ import java.io.DataInputStream import java.io.DataOutputStream @JsonFactory -data class HumanoidData( +data class HumanoidIdentity( val name: String = "Humanoid", - val species: String = "human", + val species: Registry.Ref = Registries.species.ref("human"), val gender: Gender = Gender.MALE, val hairGroup: String = "hair", @@ -37,8 +40,8 @@ data class HumanoidData( val color: RGBAColor = RGBAColor.BLACK, - val personalityIdle: String = "", - val personalityArmIdle: String = "", + val personalityIdle: String = "idle.1", + val personalityArmIdle: String = "idle.1", val personalityHeadOffset: Vector2d = Vector2d.ZERO, val personalityArmOffset: Vector2d = Vector2d.ZERO, @@ -48,14 +51,28 @@ data class HumanoidData( get() = personalityIdle override val armIdle: String get() = personalityArmIdle - override val handOffset: Vector2d + override val headOffset: Vector2d get() = personalityHeadOffset override val armOffset: Vector2d get() = personalityArmOffset + fun check() { + if (Starbound.DEBUG_BUILD) { + check(hairType.isNotEmpty()) { "'hairType' is an empty string" } + check(hairGroup.isNotEmpty()) { "'hairGroup' is an empty string" } + // check(facialHairType.isNotEmpty()) { "'facialHairType' is an empty string" } + // check(facialMaskGroup.isNotEmpty()) { "'facialMaskGroup' is an empty string" } + // check(facialMaskType.isNotEmpty()) { "'facialMaskType' is an empty string" } + check(personalityIdle.isNotEmpty()) { "'personalityIdle' is an empty string" } + check(personalityArmIdle.isNotEmpty()) { "'personalityArmIdle' is an empty string" } + } + } + fun write(stream: DataOutputStream, isLegacy: Boolean) { + check() + stream.writeBinaryString(name) - stream.writeBinaryString(species) + stream.writeBinaryString(species.key.left()) stream.writeByte(gender.ordinal) stream.writeBinaryString(hairGroup) stream.writeBinaryString(hairType) @@ -91,12 +108,12 @@ data class HumanoidData( } companion object { - val CODEC = nativeCodec(::read, HumanoidData::write) - val LEGACY_CODEC = legacyCodec(::read, HumanoidData::write) + val CODEC = nativeCodec(::read, HumanoidIdentity::write) + val LEGACY_CODEC = legacyCodec(::read, HumanoidIdentity::write) - fun read(stream: DataInputStream, isLegacy: Boolean): HumanoidData { + fun read(stream: DataInputStream, isLegacy: Boolean): HumanoidIdentity { val name: String = stream.readInternedString() - val species: String = stream.readInternedString() + val species = Registries.species.ref(stream.readInternedString()) val gender: Gender = Gender.entries[stream.readUnsignedByte()] val hairGroup = stream.readInternedString() @@ -119,7 +136,7 @@ data class HumanoidData( val color: RGBAColor = if (isLegacy) RGBAColor(stream.readUnsignedByte(), stream.readUnsignedByte(), stream.readUnsignedByte(), stream.readUnsignedByte()) else stream.readColor() val imagePath: String? = if (stream.readBoolean()) stream.readInternedString() else null - return HumanoidData( + return HumanoidIdentity( name, species, gender, hairGroup, hairType, hairDirectives, bodyDirectives, emoteDirectives, facialHairGroup, facialHairType, facialHairDirectives, facialMaskGroup, facialMaskType, facialMaskDirectives, color, personalityIdle, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/MoveControlType.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/MoveControlType.kt new file mode 100644 index 00000000..d69aec72 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/MoveControlType.kt @@ -0,0 +1,11 @@ +package ru.dbotthepony.kstarbound.defs.actor + +import ru.dbotthepony.kstarbound.json.builder.IStringSerializable + +enum class MoveControlType(override val jsonName: String) : IStringSerializable { + LEFT("left"), + RIGHT("right"), + DOWN("down"), + UP("up"), + JUMP("jump"); +} \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/NPCVariant.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/NPCVariant.kt index a393cbb0..7adcf97f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/NPCVariant.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/NPCVariant.kt @@ -1,44 +1,415 @@ package ru.dbotthepony.kstarbound.defs.actor import com.google.common.collect.ImmutableList +import com.google.common.collect.ImmutableMap +import com.google.gson.Gson +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 com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter +import kotlinx.coroutines.future.await +import kotlinx.coroutines.runBlocking +import ru.dbotthepony.kommons.gson.contains +import ru.dbotthepony.kommons.gson.set +import ru.dbotthepony.kommons.gson.stream +import ru.dbotthepony.kommons.io.readBinaryString +import ru.dbotthepony.kommons.io.readMap +import ru.dbotthepony.kommons.io.writeBinaryString +import ru.dbotthepony.kommons.io.writeMap +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.defs.ActorMovementParameters import ru.dbotthepony.kstarbound.defs.AssetPath +import ru.dbotthepony.kstarbound.defs.EntityDamageTeam +import ru.dbotthepony.kstarbound.defs.TeamType +import ru.dbotthepony.kstarbound.defs.TouchDamage +import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor +import ru.dbotthepony.kstarbound.defs.item.TreasurePoolDefinition +import ru.dbotthepony.kstarbound.io.readColor +import ru.dbotthepony.kstarbound.io.readDouble +import ru.dbotthepony.kstarbound.io.readInternedString +import ru.dbotthepony.kstarbound.io.writeColor +import ru.dbotthepony.kstarbound.io.writeDouble import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.json.mergeJson +import ru.dbotthepony.kstarbound.json.readJsonElement +import ru.dbotthepony.kstarbound.json.writeJsonElement import ru.dbotthepony.kstarbound.math.vector.Vector2d +import ru.dbotthepony.kstarbound.util.coalesceNull import ru.dbotthepony.kstarbound.util.random.nextRange import ru.dbotthepony.kstarbound.util.random.random -import java.util.random.RandomGenerator +import java.io.DataInputStream +import java.io.DataOutputStream +import kotlin.jvm.optionals.getOrNull import kotlin.math.max +@JsonAdapter(NPCVariant.Adapter::class) data class NPCVariant( val species: Registry.Entry, + val seed: Long, + val typeName: String, + val level: Double, + val overrides: JsonElement, + val humanoidConfig: HumanoidConfig, + val humanoidIdentity: HumanoidIdentity, val scripts: ImmutableList, - val initialScriptDelta: Int = 5, + val initialScriptDelta: Double = 5.0, + val scriptConfig: JsonElement = JsonNull.INSTANCE, + val movementParameters: ActorMovementParameters = ActorMovementParameters.EMPTY, + val statusControllerSettings: StatusControllerConfig = StatusControllerConfig.EMPTY, + val innateStatusEffects: ImmutableList = ImmutableList.of(), + val touchDamage: TouchDamage = TouchDamage.EMPTY, + val disableWornArmor: Boolean = true, + val dropPools: ImmutableList> = ImmutableList.of(), + val persistent: Boolean = false, + val keepAlive: Boolean = false, + val damageTeam: Int = 0, + val damageTeamType: TeamType = TeamType.ENEMY, + val nametagColor: RGBAColor = RGBAColor.WHITE, + val items: ImmutableMap = ImmutableMap.of(), ) { @JsonFactory data class SerializedData( val levelVariance: Vector2d = Vector2d.ZERO, val scripts: ImmutableList, - val initialScriptDelta: Int = 5, + val initialScriptDelta: Double = 5.0, val scriptConfig: JsonElement = JsonNull.INSTANCE, val humanoidConfig: String? = null, + val npcname: String? = null, + val nameGen: ImmutableList? = null, + val movementParameters: ActorMovementParameters = ActorMovementParameters.EMPTY, + val statusControllerSettings: StatusControllerConfig = StatusControllerConfig.EMPTY, + val innateStatusEffects: ImmutableList = ImmutableList.of(), + val touchDamage: TouchDamage = TouchDamage.EMPTY, + val disableWornArmor: Boolean = true, + val dropPools: ImmutableList> = ImmutableList.of(), + val persistent: Boolean = false, + val keepAlive: Boolean = false, + val damageTeam: Int = 0, + val damageTeamType: TeamType = TeamType.ENEMY, + val nametagColor: RGBAColor = RGBAColor.WHITE, + val matchColorIndices: Boolean = false, ) + val team: EntityDamageTeam + get() = EntityDamageTeam(damageTeamType, damageTeam) + + fun check() { + if (Starbound.DEBUG_BUILD) { + humanoidIdentity.check() + } + } + + fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeBinaryString(species.key) + stream.writeBinaryString(typeName) + stream.writeDouble(level, isLegacy) + stream.writeLong(seed) + stream.writeJsonElement(overrides) + + if (isLegacy) + stream.writeInt(initialScriptDelta.toInt()) + else + stream.writeDouble(initialScriptDelta) + + humanoidIdentity.write(stream, isLegacy) + stream.writeMap(items, { writeBinaryString(it) }, { it.write(this) }) + stream.writeBoolean(persistent) + stream.writeBoolean(keepAlive) + stream.writeByte(damageTeam) + stream.writeByte(damageTeamType.ordinal) + + if (!isLegacy) { + stream.writeBoolean(disableWornArmor) + touchDamage.write(stream) + stream.writeColor(nametagColor) + } + } + + @JsonFactory + data class DiskSerializedData( + val species: Registry.Entry, + val typeName: String, + val level: Double, + val seed: Long, + val overrides: JsonElement, + val initialScriptDelta: Double = 5.0, + val humanoidIdentity: HumanoidIdentity, + val items: ImmutableMap, + val persistent: Boolean, + val keepAlive: Boolean, + val damageTeam: Int, + val damageTeamType: TeamType, + val touchDamage: TouchDamage? = null, + val disableWornArmor: Boolean? = null, + val nametagColor: RGBAColor? = null, + ) + + class Adapter(gson: Gson) : TypeAdapter() { + private val parent = gson.getAdapter(DiskSerializedData::class.java) + + override fun write(out: JsonWriter, value: NPCVariant) { + parent.write(out, DiskSerializedData( + species = value.species, + typeName = value.typeName, + level = value.level, + seed = value.seed, + overrides = value.overrides, + initialScriptDelta = value.initialScriptDelta, + humanoidIdentity = value.humanoidIdentity, + items = value.items, + persistent = value.persistent, + keepAlive = value.keepAlive, + damageTeam = value.damageTeam, + damageTeamType = value.damageTeamType, + touchDamage = value.touchDamage, + disableWornArmor = value.disableWornArmor, + nametagColor = value.nametagColor, + )) + } + + override fun read(`in`: JsonReader): NPCVariant { + val ( + species, + typeName, + level, + seed, + overrides, + initialScriptDelta, + humanoidIdentity, + items, + persistent, + keepAlive, + damageTeam, + damageTeamType, + touchDamage, + disableWornArmor, + nametagColor, + ) = parent.read(`in`) + + val config = Registries.buildNPCConfig(typeName, overrides) + val serialized = Starbound.gson.fromJson(config, SerializedData::class.java) + + val humanoidConfig = runBlocking { humanoidConfig(serialized, species) } + + val movementParameters = humanoidConfig + .movementParameters + .merge(serialized.movementParameters) + + return NPCVariant( + species = species, + seed = seed, + typeName = typeName, + level = level, + humanoidConfig = humanoidConfig, + humanoidIdentity = humanoidIdentity, + overrides = overrides.deepCopy(), + scripts = serialized.scripts, + initialScriptDelta = initialScriptDelta, + scriptConfig = serialized.scriptConfig, + movementParameters = movementParameters, + statusControllerSettings = serialized.statusControllerSettings, + innateStatusEffects = ImmutableList.copyOf(innates(serialized, level)), + touchDamage = touchDamage ?: serialized.touchDamage, + disableWornArmor = disableWornArmor ?: serialized.disableWornArmor, + dropPools = serialized.dropPools, + persistent = persistent, + keepAlive = keepAlive, + damageTeam = damageTeam, + damageTeamType = damageTeamType, + nametagColor = nametagColor ?: serialized.nametagColor, + items = ImmutableMap.copyOf(items) + ).also { it.check() } + } + } + companion object { - suspend fun create(species: Registry.Entry, type: String, level: Double, random: RandomGenerator, overrides: JsonElement = JsonNull.INSTANCE): NPCVariant { + suspend fun humanoidConfig(serialized: SerializedData, species: Registry.Entry): HumanoidConfig { + if (serialized.humanoidConfig != null) + return Starbound.gson.fromJson(Starbound.loadJsonAsset(serialized.humanoidConfig).await() as JsonObject, HumanoidConfig::class.java) + else + return species.value.humanoidConfig() + } + + fun innates(serialized: SerializedData, level: Double): ArrayList { + val innateStatusEffects = ArrayList(serialized.innateStatusEffects) + + innateStatusEffects.add( + Either.left(StatModifier.value("powerMultiplier", Registries.jsonFunctions.getOrThrow("npcLevelPowerMultiplierModifier").value.evaluate(level))) + ) + + innateStatusEffects.add( + Either.left(StatModifier.multBase("protection", Registries.jsonFunctions.getOrThrow("npcLevelProtectionMultiplier").value.evaluate(level))) + ) + + innateStatusEffects.add( + Either.left(StatModifier.multBase("maxHealth", Registries.jsonFunctions.getOrThrow("npcLevelHealthMultiplier").value.evaluate(level))) + ) + + innateStatusEffects.add( + Either.left(StatModifier.multBase("maxEnergy", Registries.jsonFunctions.getOrThrow("npcLevelEnergyMultiplier").value.evaluate(level))) + ) + + return innateStatusEffects + } + + @Suppress("BlockingMethodInNonBlockingContext") + suspend fun read(stream: DataInputStream, isLegacy: Boolean): NPCVariant { + val species = Registries.species.getOrThrow(stream.readBinaryString()) + val type = stream.readInternedString() + val level = stream.readDouble(isLegacy) + val seed = stream.readLong() + val overrides = stream.readJsonElement() + + val initialScriptDelta = if (isLegacy) stream.readInt().toDouble() else stream.readDouble() + val identity = HumanoidIdentity.read(stream, isLegacy) + val items = stream.readMap({ readInternedString() }, { ItemDescriptor(this) }) + val persistent = stream.readBoolean() + val keepAlive = stream.readBoolean() + val damageTeam = stream.readUnsignedByte() + val damageTeamType = TeamType.entries[stream.readUnsignedByte()] + + val config = Registries.buildNPCConfig(type, overrides) + val serialized = Starbound.gson.fromJson(config, SerializedData::class.java) + + val humanoidConfig = humanoidConfig(serialized, species) + + val movementParameters = humanoidConfig + .movementParameters + .merge(serialized.movementParameters) + + val disableWornArmor = if (isLegacy) serialized.disableWornArmor else stream.readBoolean() + val touchDamage = if (isLegacy) serialized.touchDamage else TouchDamage(stream) + val nametagColor = if (isLegacy) serialized.nametagColor else stream.readColor() + + return NPCVariant( + species = species, + seed = seed, + typeName = type, + level = level, + humanoidConfig = humanoidConfig, + humanoidIdentity = identity, + overrides = overrides.deepCopy(), + scripts = serialized.scripts, + initialScriptDelta = initialScriptDelta, + scriptConfig = serialized.scriptConfig, + movementParameters = movementParameters, + statusControllerSettings = serialized.statusControllerSettings, + innateStatusEffects = ImmutableList.copyOf(innates(serialized, level)), + touchDamage = touchDamage, + disableWornArmor = disableWornArmor, + dropPools = serialized.dropPools, + persistent = persistent, + keepAlive = keepAlive, + damageTeam = damageTeam, + damageTeamType = damageTeamType, + nametagColor = nametagColor, + items = ImmutableMap.copyOf(items) + ).also { it.check() } + } + + suspend fun create(species: Registry.Entry, type: String, level: Double, seed: Long, overrides: JsonElement = JsonNull.INSTANCE): NPCVariant { + val random = random(seed) val config = Registries.buildNPCConfig(type, overrides) val serialized = Starbound.gson.fromJson(config, SerializedData::class.java) val finalLevel = max(0.0, random.nextRange(serialized.levelVariance) + level) - TODO() - } - suspend fun create(species: Registry.Entry, type: String, level: Double, seed: Long, overrides: JsonElement = JsonNull.INSTANCE): NPCVariant { - return create(species, type, level, random(seed), overrides) + val humanoidConfig = humanoidConfig(serialized, species) + + var identity = species.value.generateHumanoid(random) + + val name = serialized.npcname ?: serialized.nameGen?.let { Starbound.generateNameAsync(it[identity.gender.ordinal], random) } ?: "" + val personality = humanoidConfig.personalities.random(random) + + identity = identity.copy( + name = name, + personalityIdle = personality.idle, + personalityArmIdle = personality.armIdle, + personalityHeadOffset = personality.headOffset, + personalityArmOffset = personality.armOffset + ) + + if ("identity" in config) + identity = Starbound.gson.fromJson(mergeJson(Starbound.gson.toJsonTree(identity), config["identity"]!!), HumanoidIdentity::class.java) + + val movementParameters = humanoidConfig + .movementParameters + .merge(serialized.movementParameters) + + val items = ImmutableMap.Builder() + + if ("items" in config) { + val itemsConfig = config.getAsJsonObject("items") + + val speciesItemsConfig = itemsConfig["override"]?.coalesceNull?.asJsonArray + ?: itemsConfig[species.key]?.coalesceNull?.asJsonArray + ?: itemsConfig["default"]?.coalesceNull?.asJsonArray + + if (speciesItemsConfig != null) { + val highestLevelItemsConfig = speciesItemsConfig.stream() + .map { it.asJsonArray.let { it[0].asDouble to it[1] } } + .filter { it.first <= finalLevel } + .sorted { o1, o2 -> o2.first.compareTo(o1.first) } + .findFirst() + .getOrNull()?.second as JsonArray? + + if (highestLevelItemsConfig != null) { + var randomColorIndex = -1 + + for (itemSlotConfig in highestLevelItemsConfig.random(random).asJsonObject.entrySet()) { + var item = ItemDescriptor(itemSlotConfig.value.asJsonArray.random(random)) + val colorIndex = item.parameters["colorIndex"] + + // Randomize color index if colorIndex is an array + if (colorIndex is JsonArray) { + if (!serialized.matchColorIndices || randomColorIndex == -1) { + randomColorIndex = colorIndex.random(random).asInt + } + + item = item.applyParameters(JsonObject().also { + it["colorIndex"] = JsonPrimitive(randomColorIndex) + }) + } + + items.put(itemSlotConfig.key, item) + } + } + } + } + + return NPCVariant( + species = species, + seed = seed, + typeName = type, + level = finalLevel, + humanoidConfig = humanoidConfig, + humanoidIdentity = identity, + overrides = overrides.deepCopy(), + scripts = serialized.scripts, + initialScriptDelta = serialized.initialScriptDelta, + scriptConfig = serialized.scriptConfig, + movementParameters = movementParameters, + statusControllerSettings = serialized.statusControllerSettings, + innateStatusEffects = ImmutableList.copyOf(innates(serialized, finalLevel)), + touchDamage = serialized.touchDamage, + disableWornArmor = serialized.disableWornArmor, + dropPools = serialized.dropPools, + persistent = serialized.persistent, + keepAlive = serialized.keepAlive, + damageTeam = serialized.damageTeam, + damageTeamType = serialized.damageTeamType, + nametagColor = serialized.nametagColor, + items = items.buildOrThrow() + ).also { it.check() } } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/Personality.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/Personality.kt index c91eb94c..0ad7243a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/Personality.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/Personality.kt @@ -1,20 +1,50 @@ package ru.dbotthepony.kstarbound.defs.actor +import ru.dbotthepony.kommons.io.writeBinaryString +import ru.dbotthepony.kommons.io.writeStruct2d +import ru.dbotthepony.kstarbound.io.readInternedString +import ru.dbotthepony.kstarbound.io.readVector2d import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import java.io.DataInputStream +import java.io.DataOutputStream // this is because personality data structure is used inconsistently across original source code interface IPersonality { val idle: String val armIdle: String - val handOffset: Vector2d + val headOffset: Vector2d val armOffset: Vector2d } -@JsonFactory(asList = true) +@JsonFactory(asList = 1) data class Personality( override val idle: String, override val armIdle: String, - override val handOffset: Vector2d, + override val headOffset: Vector2d, override val armOffset: Vector2d -) : IPersonality +) : IPersonality { + /** + * new protocol only + */ + constructor(stream: DataInputStream) : this( + stream.readInternedString(), + stream.readInternedString(), + stream.readVector2d(), + stream.readVector2d(), + ) + + /** + * new protocol only + */ + fun write(stream: DataOutputStream) { + stream.writeBinaryString(idle) + stream.writeBinaryString(armIdle) + stream.writeStruct2d(headOffset) + stream.writeStruct2d(armOffset) + } + + companion object { + val ORIGINAL_ENGINE_VERY_COMPATIBLE_COMPATIBILITY = Personality("", "", Vector2d.ZERO, Vector2d.ZERO) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/Species.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/Species.kt index cb28283c..950718cc 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/Species.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/Species.kt @@ -2,18 +2,33 @@ package ru.dbotthepony.kstarbound.defs.actor import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableSet +import com.google.gson.JsonObject +import kotlinx.coroutines.future.await +import ru.dbotthepony.kommons.collect.filterNotNull +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.defs.AssetPath import ru.dbotthepony.kstarbound.defs.ColorReplacements import ru.dbotthepony.kstarbound.defs.StatusEffectDefinition import ru.dbotthepony.kstarbound.defs.image.SpriteReference import ru.dbotthepony.kstarbound.defs.actor.player.BlueprintLearnList import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor +import ru.dbotthepony.kstarbound.fromJson import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.json.mergeJson +import ru.dbotthepony.kstarbound.util.getWrapAround +import ru.dbotthepony.kstarbound.util.random.random +import java.util.random.RandomGenerator @JsonFactory data class Species( val kind: String, + @Deprecated("Use humanoidConfig() function", replaceWith = ReplaceWith("this.humanoidConfig()")) + val humanoidConfig: AssetPath = AssetPath("/humanoid.config"), + @Deprecated("Use humanoidConfig() function", replaceWith = ReplaceWith("this.humanoidConfig()")) + val humanoidOverrides: JsonObject = JsonObject(), val charCreationTooltip: Tooltip = Tooltip(), val nameGen: ImmutableList, val ouchNoises: ImmutableList, @@ -21,6 +36,8 @@ data class Species( val skull: AssetPath = AssetPath("/humanoid/any/dead.png"), // ваш свет угасает val defaultBlueprints: BlueprintLearnList, + val effectDirectives: String = "", + val headOptionAsHairColor: Boolean = false, val headOptionAsFacialhair: Boolean = false, val altOptionAsUndyColor: Boolean = false, @@ -44,13 +61,92 @@ data class Species( val name: String = "", val image: SpriteReference = SpriteReference.NEVER, val characterImage: SpriteReference = SpriteReference.NEVER, - val hairGroup: String? = null, + val hairGroup: String = "hair", val hair: ImmutableSet, val shirt: ImmutableSet, val pants: ImmutableSet, - val facialHairGroup: String? = null, + val facialHairGroup: String = "", val facialHair: ImmutableSet = ImmutableSet.of(), - val facialMaskGroup: String? = null, + val facialMaskGroup: String = "", val facialMask: ImmutableList = ImmutableList.of(), ) + + suspend fun humanoidConfig(): HumanoidConfig { + return Starbound.gson.fromJson(mergeJson(Starbound.loadJsonAsset(humanoidConfig.fullPath).thenApply { it ?: JsonObject() }.await().deepCopy(), humanoidOverrides), HumanoidConfig::class.java) + } + + fun buildStatusEffects(): List { + return statusEffects.stream().filter { it.isPresent }.map { Either.right>(it) }.toList() + } + + suspend fun generateHumanoid(random: RandomGenerator): HumanoidIdentity { + val gender = Gender.valueOf(random.nextBoolean()) + val name = Starbound.generateNameAsync(nameGen[gender.ordinal], random) + + val genderSettings = genders[gender.ordinal] + var bodyColor = bodyColor.random(random).toImageOperator() + + var altColor = "" + + val altOpt = random.nextInt() + val headOpt = random.nextInt() + val hairOpt = random.nextInt() + + if (altOptionAsUndyColor) { + altColor = undyColor.getWrapAround(altOpt).toImageOperator() + } + + val hair = genderSettings.hair.getWrapAround(hairOpt) + var hairColor = bodyColor + + if (headOptionAsHairColor) { + hairColor = this.hairColor.getWrapAround(headOpt).toImageOperator() + if (altOptionAsHairColor) hairColor += this.undyColor.getWrapAround(altOpt).toImageOperator() + } + + if (hairColorAsBodySubColor) + bodyColor += hairColor + + var facialHair = "" + var facialHairGroup = "" + var facialHairDirective = "" + + if (headOptionAsFacialhair) { + facialHair = genderSettings.facialHair.getWrapAround(headOpt) + facialHairGroup = genderSettings.facialHairGroup + facialHairDirective = hairColor + } + + var facialMask = "" + var facialMaskGroup = "" + var facialMaskDirectives = "" + + if (altOptionAsFacialMask) { + facialMask = genderSettings.facialMask.getWrapAround(altOpt) + facialMaskGroup = genderSettings.facialMaskGroup + } + + if (bodyColorAsFacialMaskSubColor) + facialMaskDirectives += bodyColor + + if (altColorAsFacialMaskSubColor) + facialMaskDirectives += altColor + + return HumanoidIdentity( + name = name, + species = Registries.species.getOrThrow(kind).ref, + gender = gender, + hairGroup = genderSettings.hairGroup, + hairType = hair, + hairDirectives = hairColor, + bodyDirectives = bodyColor + altColor, + emoteDirectives = bodyColor + altColor, + facialHairGroup = facialHairGroup, + facialHairType = facialHair, + facialHairDirectives = facialHairDirective, + facialMaskGroup = facialMaskGroup, + facialMaskType = facialMask, + facialMaskDirectives = facialMaskDirectives + ).also { it.check() } + } } 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 ec04a208..0befdca7 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/Types.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/Types.kt @@ -11,6 +11,12 @@ typealias PersistentStatusEffect = Either>, val metaBoundBox: AABB, val movementParameters: ActorMovementParameters, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonBrush.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonBrush.kt index 48d6d53e..ff7686eb 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonBrush.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonBrush.kt @@ -1,18 +1,29 @@ package ru.dbotthepony.kstarbound.defs.dungeon import com.google.common.collect.ImmutableList +import com.google.common.collect.ImmutableSet import com.google.gson.Gson import com.google.gson.JsonObject +import com.google.gson.JsonPrimitive import com.google.gson.JsonSyntaxException import com.google.gson.TypeAdapter import com.google.gson.annotations.JsonAdapter import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonWriter import org.apache.logging.log4j.LogManager +import ru.dbotthepony.kommons.collect.collect +import ru.dbotthepony.kommons.collect.filterNotNull +import ru.dbotthepony.kommons.collect.map +import ru.dbotthepony.kommons.gson.contains +import ru.dbotthepony.kommons.gson.set +import ru.dbotthepony.kommons.util.Either +import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.actor.Species import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor +import ru.dbotthepony.kstarbound.defs.monster.MonsterTypeDefinition import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition @@ -23,8 +34,10 @@ import ru.dbotthepony.kstarbound.defs.tile.isRealModifier import ru.dbotthepony.kstarbound.defs.tile.orEmptyLiquid import ru.dbotthepony.kstarbound.defs.tile.orEmptyModifier import ru.dbotthepony.kstarbound.defs.tile.orEmptyTile +import ru.dbotthepony.kstarbound.fromJsonFast import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonIgnore +import ru.dbotthepony.kstarbound.util.AssetPathStack import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.world.Direction import ru.dbotthepony.kstarbound.world.api.AbstractLiquidState @@ -218,10 +231,83 @@ abstract class DungeonBrush { } } - data class NPC(val data: JsonObject) : DungeonBrush() { + class NPC(data: JsonObject) : DungeonBrush() { + @JsonFactory + data class NPCConfig( + val seed: Either? = null, + val species: String, + val typeName: String = "default", + val parameters: JsonObject = JsonObject(), + ) { + private val assetCommentary = AssetPathStack.assetCommentary() + // interpret species as a comma separated list of unquoted strings + val speciesList: ImmutableSet> = ImmutableSet.copyOf(species.trim().replace(" ", "").split(',').map { it.trim() }.filter { it.isNotEmpty() }.map { Registries.species.ref(it) }) + private var triggeredWarning = false + + val speciesListResolved: ImmutableList> get() { + val result = speciesList.iterator().map { it.entry }.filterNotNull().collect(ImmutableList.toImmutableList()) + + if (result.isEmpty() && !triggeredWarning) { + triggeredWarning = true + LOGGER.error("No valid species specified or species string is empty: $species$assetCommentary") + } + + return result + } + + init { + if ("persistent" !in parameters) { + parameters["persistent"] = true + } + } + } + + @JsonFactory + data class MonsterConfig( + val seed: Either? = null, + val typeName: Registry.Ref, + val parameters: JsonObject = JsonObject(), + ) { + init { + if (seed != null && seed.isRight && seed.right() != "stable") + + if ("persistent" !in parameters) { + parameters["persistent"] = true + } + } + } + + val data: Either + + init { + val kind = data["kind"]?.asString?.lowercase() ?: throw JsonSyntaxException("Missing NPC 'kind' in NPC brush data") + + if (kind == "npc") { + this.data = Either.left(Starbound.gson.fromJsonFast(data, NPCConfig::class.java)) + } else if (kind == "monster") { + this.data = Either.right(Starbound.gson.fromJsonFast(data, MonsterConfig::class.java)) + } else { + throw JsonSyntaxException("Unknown NPC type $kind!") + } + } + override fun execute(x: Int, y: Int, phase: Phase, world: DungeonWorld) { if (phase === Phase.PLACE_NPCS) { - LOGGER.warn("NYI: NPC at $x, $y") + data.map({ + val speciesListResolved = it.speciesListResolved + + if (speciesListResolved.isNotEmpty()) { + world.placeNPC(x, y, speciesListResolved.random(world.random), it.typeName, it.seed?.map({ it }, { world.random.nextLong() }) ?: world.random.nextLong(), it.parameters) + } else { + LOGGER.error("Unable to place NPC ${it.typeName}, because species list is empty or contain no valid species") + } + }, { + if (it.typeName.isPresent) { + world.placeMonster(x, y, it.typeName.entryOrThrow, it.seed?.map({ it }, { world.random.nextLong() }) ?: world.random.nextLong(), it.parameters) + } else { + LOGGER.error("Unable to place Monster ${it.typeName} at $x $y, because no such monster exists") + } + }) } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonWorld.kt index 8a91ac80..eb77ced3 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonWorld.kt @@ -1,8 +1,13 @@ package ru.dbotthepony.kstarbound.defs.dungeon +import com.google.gson.JsonElement +import com.google.gson.JsonNull import com.google.gson.JsonObject import it.unimi.dsi.fastutil.longs.Long2ObjectFunction import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.future.await import org.apache.logging.log4j.LogManager import ru.dbotthepony.kstarbound.math.AABBi @@ -10,7 +15,11 @@ import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.math.vector.Vector2i import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.actor.NPCVariant +import ru.dbotthepony.kstarbound.defs.actor.Species import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor +import ru.dbotthepony.kstarbound.defs.monster.MonsterTypeDefinition +import ru.dbotthepony.kstarbound.defs.monster.MonsterVariant import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition @@ -27,16 +36,16 @@ import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.ChunkState import ru.dbotthepony.kstarbound.world.Direction -import ru.dbotthepony.kstarbound.world.api.AbstractCell import ru.dbotthepony.kstarbound.world.api.AbstractLiquidState import ru.dbotthepony.kstarbound.world.api.AbstractTileState import ru.dbotthepony.kstarbound.world.api.MutableTileState import ru.dbotthepony.kstarbound.world.api.TileColor +import ru.dbotthepony.kstarbound.world.entities.MonsterEntity +import ru.dbotthepony.kstarbound.world.entities.NPCEntity import ru.dbotthepony.kstarbound.world.entities.tile.TileEntity import ru.dbotthepony.kstarbound.world.entities.tile.WireConnection import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject import ru.dbotthepony.kstarbound.world.physics.Poly -import java.util.Collections import java.util.LinkedHashSet import java.util.concurrent.CompletableFuture import java.util.function.Consumer @@ -84,6 +93,19 @@ class DungeonWorld( val parameters: JsonObject = JsonObject() ) + private data class NPCData( + val species: Registry.Entry, + val type: String, + val seed: Long, + val overrides: JsonElement = JsonNull.INSTANCE + ) + + private data class MonsterData( + val type: Registry.Entry, + val seed: Long, + val overrides: JsonObject = JsonObject() + ) + var hasGenerated = false private set @@ -168,6 +190,9 @@ class DungeonWorld( private val placedObjects = LinkedHashMap() + private val placedNPCs = LinkedHashMap>() + private val placedMonsters = LinkedHashMap>() + var playerStart: Vector2d? = null fun finishPart() { @@ -189,6 +214,14 @@ class DungeonWorld( table.computeIfAbsent(group) { LinkedHashSet() }.add(geometry.wrap(Vector2i(x, y))) } + fun placeNPC(x: Int, y: Int, species: Registry.Entry, type: String, seed: Long, overrides: JsonElement = JsonNull.INSTANCE) { + placedNPCs.computeIfAbsent(geometry.wrap(Vector2i(x, y))) { ArrayList() }.add(NPCData(species, type, seed, overrides)) + } + + fun placeMonster(x: Int, y: Int, type: Registry.Entry, seed: Long, overrides: JsonObject = JsonObject()) { + placedMonsters.computeIfAbsent(geometry.wrap(Vector2i(x, y))) { ArrayList() }.add(MonsterData(type, seed, overrides)) + } + fun requestLiquid(x: Int, y: Int, liquid: AbstractLiquidState) { pendingLiquids[geometry.wrap(Vector2i(x, y))] = liquid } @@ -495,6 +528,28 @@ class DungeonWorld( val tickets = ArrayList() try { + // construct npc variants as early as possible because they potentially involve disk I/O + val npcVariantsJob = Starbound.GLOBAL_SCOPE.async { + val variants = ArrayList>>(placedNPCs.values.sumOf { it.size }) + + for ((pos, npcs) in placedNPCs) { + for (npc in npcs) { + val variant = async { + try { + NPCVariant.create(npc.species, npc.type, parent.template.threatLevel, npc.seed, npc.overrides) + } catch (err: Throwable) { + LOGGER.error("Unable to place NPC '${npc.type}' with species '${npc.species}' at $pos", err) + null + } + } + + variants.add(pos to variant) + } + } + + variants.map { it.first to it.second.await() } + } + val terrainBlendingVertexes = ArrayList() val spaceBlendingVertexes = ArrayList() @@ -600,6 +655,15 @@ class DungeonWorld( }, Starbound.EXECUTOR) } + val monsterVariants = ArrayList>(placedMonsters.values.sumOf { it.size }) + + for ((pos, monsters) in placedMonsters) { + for (monster in monsters) { + val variant = monster.type.value.create(monster.seed, monster.overrides) + monsterVariants.add(pos to variant) + } + } + val biomeItems = ArrayList<() -> Unit>() for (biomeTree in biomeTreesFutures) { @@ -627,6 +691,8 @@ class DungeonWorld( } } + val npcVariants = npcVariantsJob.await() + // wait for all chunks to be loaded (and cell changes to be applied) // if any of cell change operation fails, entire generation fails... leaving world in inconsistent state, // but also limiting damage by exiting early. @@ -680,6 +746,20 @@ class DungeonWorld( } } + for ((pos, variant) in npcVariants) { + val ent = NPCEntity(variant ?: continue) + ent.movement.xPosition = pos.x.toDouble() + ent.movement.yPosition = pos.y.toDouble() + ent.joinWorld(parent) + } + + for ((pos, variant) in monsterVariants) { + val ent = MonsterEntity(variant) + ent.movement.xPosition = pos.x.toDouble() + ent.movement.yPosition = pos.y.toDouble() + ent.joinWorld(parent) + } + parent.enableDungeonTileProtection = true }.await() } finally { 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 bdde14f2..22e0b527 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDescriptor.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDescriptor.kt @@ -31,6 +31,7 @@ import ru.dbotthepony.kstarbound.VersionRegistry import ru.dbotthepony.kstarbound.io.readInternedString import ru.dbotthepony.kstarbound.item.ItemRegistry import ru.dbotthepony.kstarbound.item.ItemStack +import ru.dbotthepony.kstarbound.json.mergeJson import ru.dbotthepony.kstarbound.json.readJsonElement import ru.dbotthepony.kstarbound.json.writeJsonElement import ru.dbotthepony.kstarbound.lua.LuaEnvironment @@ -162,7 +163,6 @@ fun ItemDescriptor(stream: DataInputStream): ItemDescriptor { * [parameters] is considered to be immutable and should not be modified * directly (must be copied for mutable context) */ -@JsonAdapter(ItemDescriptor.Adapter::class) data class ItemDescriptor( val name: String, val count: Long, @@ -172,6 +172,10 @@ data class ItemDescriptor( val isNotEmpty get() = !isEmpty val ref by lazy { if (name == "") ItemRegistry.AIR else ItemRegistry[name] } + fun applyParameters(parameters: JsonObject): ItemDescriptor { + return copy(parameters = mergeJson(this.parameters.deepCopy(), parameters)) + } + override fun toString(): String { return "ItemDescriptor[$name, $count, $parameters]" } @@ -240,7 +244,7 @@ data class ItemDescriptor( stream.writeJsonElement(parameters) } - class Adapter(gson: Gson) : TypeAdapter() { + object Adapter : TypeAdapter() { override fun write(out: JsonWriter, value: ItemDescriptor) { if (value.isEmpty) out.nullValue() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/quest/QuestArcDescriptor.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/quest/QuestArcDescriptor.kt index edcb080c..8584a1b2 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/quest/QuestArcDescriptor.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/quest/QuestArcDescriptor.kt @@ -5,6 +5,7 @@ import com.google.gson.Gson import com.google.gson.TypeAdapter 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.readCollection import ru.dbotthepony.kommons.io.writeBinaryString @@ -38,7 +39,21 @@ data class QuestArcDescriptor( if (stagehandUniqueId != null) stream.writeBinaryString(stagehandUniqueId) } - class Adapter(gson: Gson) : VersionedAdapter("QuestArcDescriptor", FactoryAdapter.createFor(QuestArcDescriptor::class, gson)) + class Adapter(gson: Gson) : TypeAdapter() { + private val parent = VersionedAdapter("QuestArcDescriptor", FactoryAdapter.createFor(QuestArcDescriptor::class, gson)) + + override fun write(out: JsonWriter, value: QuestArcDescriptor) { + parent.write(out, value) + } + + override fun read(`in`: JsonReader): QuestArcDescriptor { + if (`in`.peek() == JsonToken.STRING) { + return QuestArcDescriptor(ImmutableList.of(QuestDescriptor(`in`.nextString()))) + } else { + return parent.read(`in`) + } + } + } companion object { val CODEC = nativeCodec(::QuestArcDescriptor, QuestArcDescriptor::write) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/quest/QuestDescriptor.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/quest/QuestDescriptor.kt index 830359c4..b6ebac11 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/quest/QuestDescriptor.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/quest/QuestDescriptor.kt @@ -14,6 +14,7 @@ import ru.dbotthepony.kstarbound.json.builder.FactoryAdapter import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.network.syncher.legacyCodec import ru.dbotthepony.kstarbound.network.syncher.nativeCodec +import ru.dbotthepony.kstarbound.util.random.threadLocalRandom import java.io.DataInputStream import java.io.DataOutputStream @@ -22,7 +23,8 @@ data class QuestDescriptor( val questId: String, val templateId: String = questId, val parameters: ImmutableMap = ImmutableMap.of(), - val seed: Long = makeSeed(), + // TODO: this is original engine behavior, and this is stupid + val seed: Long = threadLocalRandom.nextLong(), ) { constructor(stream: DataInputStream, isLegacy: Boolean) : this( stream.readInternedString(), @@ -43,9 +45,5 @@ data class QuestDescriptor( companion object { val CODEC = nativeCodec(::QuestDescriptor, QuestDescriptor::write) val LEGACY_CODEC = legacyCodec(::QuestDescriptor, QuestDescriptor::write) - - fun makeSeed(): Long { - return System.nanoTime().rotateLeft(27).xor(System.currentTimeMillis()) - } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/RenderTemplate.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/RenderTemplate.kt index 12cd933a..4c1f6b8b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/RenderTemplate.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/RenderTemplate.kt @@ -128,7 +128,7 @@ data class RenderMatch( val haltOnMatch: Boolean = false, val haltOnSubMatch: Boolean = false, ) { - @JsonFactory(asList = true) + @JsonFactory(asList = 1) data class Piece( val name: String, val offset: Vector2i @@ -140,7 +140,7 @@ data class RenderMatch( } } - @JsonFactory(asList = true) + @JsonFactory(asList = 1) data class Matcher( val offset: Vector2i, val ruleName: String @@ -204,7 +204,7 @@ data class RenderMatch( } } -@JsonFactory(asList = true) +@JsonFactory(asList = 1) data class RenderMatchList( val name: String, val list: ImmutableList diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BushVariant.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BushVariant.kt index f1a6c0a0..3fe59068 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BushVariant.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BushVariant.kt @@ -31,7 +31,7 @@ class BushVariant( val ephemeral: Boolean, val tileDamageParameters: TileDamageParameters, ) { - @JsonFactory(asList = true) + @JsonFactory(asList = 1) data class Shape(val image: String, val mods: ImmutableList) @JsonFactory diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/StreamCodec.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/StreamCodec.kt index 3a880570..4f18508b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/io/StreamCodec.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/StreamCodec.kt @@ -287,7 +287,7 @@ val BigDecimalValueCodec = StreamCodec.Impl(DataInputStream::readBigDecimal, Dat val UUIDValueCodec = StreamCodec.Impl({ s -> UUID(s.readLong(), s.readLong()) }, { s, v -> s.writeLong(v.mostSignificantBits); s.writeLong(v.leastSignificantBits) }) val VarIntValueCodec = StreamCodec.Impl(DataInputStream::readSignedVarInt, DataOutputStream::writeSignedVarInt) val VarLongValueCodec = StreamCodec.Impl(DataInputStream::readSignedVarLong, DataOutputStream::writeSignedVarLong) -val BinaryStringCodec = StreamCodec.Impl(DataInputStream::readBinaryString, DataOutputStream::writeBinaryString) +val BinaryStringCodec = StreamCodec.Impl(DataInputStream::readInternedString, DataOutputStream::writeBinaryString) val UnsignedVarLongCodec = StreamCodec.Impl(DataInputStream::readVarLong, DataOutputStream::writeVarLong) val UnsignedVarIntCodec = StreamCodec.Impl(DataInputStream::readVarInt, DataOutputStream::writeVarInt) @@ -334,5 +334,21 @@ val KOptionalBinaryStringCodec = StreamCodec.KOptional(BinaryStringCodec) val KOptionalRGBCodec = StreamCodec.KOptional(RGBCodec) val KOptionalRGBACodec = StreamCodec.KOptional(RGBACodec) +val NullableBooleanValueCodec = StreamCodec.Nullable(BooleanValueCodec) +val NullableByteValueCodec = StreamCodec.Nullable(ByteValueCodec) +val NullableShortValueCodec = StreamCodec.Nullable(ShortValueCodec) +val NullableCharValueCodec = StreamCodec.Nullable(CharValueCodec) +val NullableIntValueCodec = StreamCodec.Nullable(IntValueCodec) +val NullableLongValueCodec = StreamCodec.Nullable(LongValueCodec) +val NullableFloatValueCodec = StreamCodec.Nullable(FloatValueCodec) +val NullableDoubleValueCodec = StreamCodec.Nullable(DoubleValueCodec) +val NullableBigDecimalValueCodec = StreamCodec.Nullable(BigDecimalValueCodec) +val NullableUUIDValueCodec = StreamCodec.Nullable(UUIDValueCodec) +val NullableVarIntValueCodec = StreamCodec.Nullable(VarIntValueCodec) +val NullableVarLongValueCodec = StreamCodec.Nullable(VarLongValueCodec) +val NullableBinaryStringCodec = StreamCodec.Nullable(BinaryStringCodec) +val NullableRGBCodec = StreamCodec.Nullable(RGBCodec) +val NullableRGBACodec = StreamCodec.Nullable(RGBACodec) + fun > Class.codec() = StreamCodec.Enum(this) fun > KClass.codec() = StreamCodec.Enum(this.java) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemStack.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemStack.kt index 8f87bdb1..6b1abd65 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemStack.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemStack.kt @@ -31,6 +31,7 @@ import ru.dbotthepony.kstarbound.Globals import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.Drawable +import ru.dbotthepony.kstarbound.defs.actor.PersistentStatusEffect import ru.dbotthepony.kstarbound.defs.image.SpriteReference import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor import ru.dbotthepony.kstarbound.defs.item.ItemRarity @@ -262,6 +263,12 @@ open class ItemStack(val entry: ItemRegistry.Entry, val config: JsonObject, para return AgingResult(null, false) } + open val effects: Collection + get() = listOf() + + open val statusEffects: Collection + get() = listOf() + fun createDescriptor(): ItemDescriptor { if (isEmpty) return ItemDescriptor.EMPTY diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonPatch.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonPatch.kt index 7db33dfb..abc87827 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonPatch.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonPatch.kt @@ -153,15 +153,16 @@ enum class JsonPatch(val key: String) { @Suppress("NAME_SHADOWING") suspend fun applyAsync(base: JsonElement, source: Collection?): JsonElement { source ?: return base + val loaded = source.map { it.asyncJsonReader() to it } var base = base - for (patch in source) { - val read = Starbound.ELEMENTS_ADAPTER.read(patch.asyncJsonReader().await()) + for ((patch, f) in loaded) { + val read = Starbound.ELEMENTS_ADAPTER.read(patch.await()) if (read !is JsonArray) { LOGGER.error("$patch root element is not an array") } else { - base = apply(base, read, patch) + base = apply(base, read, f) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/VersionedAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/VersionedAdapter.kt index 84682443..b6e839bd 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/VersionedAdapter.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/VersionedAdapter.kt @@ -6,7 +6,7 @@ import com.google.gson.stream.JsonWriter import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.VersionRegistry -abstract class VersionedAdapter(val name: String, val parent: TypeAdapter) : TypeAdapter() { +open class VersionedAdapter(val name: String, val parent: TypeAdapter) : TypeAdapter() { override fun write(out: JsonWriter, value: T) { if (Starbound.IS_STORE_JSON) { VersionRegistry.make(name, parent.toJsonTree(value)).toJson(out) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/Annotations.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/Annotations.kt index 700fcb94..f51fb50e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/Annotations.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/Annotations.kt @@ -61,7 +61,12 @@ annotation class JsonBuilder @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) annotation class JsonFactory( - val asList: Boolean = false, + /** + * * -1 - input can be only as object + * * 0 - input may be either an object or a list, output is constructed as object + * * 1 - input can be only as list + */ + val asList: Int = -1, val logMisses: Boolean = false, ) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/FactoryAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/FactoryAdapter.kt index 221167fc..e0bf1da5 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/FactoryAdapter.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/FactoryAdapter.kt @@ -47,15 +47,19 @@ class FactoryAdapter private constructor( val clazz: KClass, val types: ImmutableList>, aliases: Map>, - val asJsonArray: Boolean, + val asJsonArray: AsJsonArray, val stringInterner: Interner, val logMisses: Boolean, ) : TypeAdapter() { + enum class AsJsonArray { + NO, EITHER, YES + } + private val name2index = Object2ObjectArrayMap() private val loggedMisses = Collections.synchronizedSet(ObjectArraySet()) init { - if (asJsonArray && types.any { it.isFlat }) { + if (asJsonArray != AsJsonArray.NO && types.any { it.isFlat }) { throw IllegalArgumentException("Can't have both flat properties and input be as json array") } @@ -159,7 +163,7 @@ class FactoryAdapter private constructor( return } - if (asJsonArray) + if (asJsonArray == AsJsonArray.YES) out.beginArray() else out.beginObject() @@ -169,7 +173,7 @@ class FactoryAdapter private constructor( continue if (type.isFlat) { - check(!asJsonArray) + check(asJsonArray != AsJsonArray.YES) val (field, adapter) = type val result = (adapter as TypeAdapter).toJsonTree((field as KProperty1).get(value)) @@ -189,10 +193,10 @@ class FactoryAdapter private constructor( val getValue = field.get(value) // god fucking damn it - if (!asJsonArray && getValue === null) + if (asJsonArray != AsJsonArray.YES && getValue === null) continue - if (!asJsonArray) + if (asJsonArray != AsJsonArray.YES) out.name(field.name) @Suppress("unchecked_cast") @@ -200,7 +204,7 @@ class FactoryAdapter private constructor( } } - if (asJsonArray) + if (asJsonArray == AsJsonArray.YES) out.endArray() else out.endObject() @@ -216,9 +220,10 @@ class FactoryAdapter private constructor( @Suppress("name_shadowing") var reader = reader + val readingAsArray = asJsonArray == AsJsonArray.YES || asJsonArray == AsJsonArray.EITHER && reader.peek() == JsonToken.BEGIN_ARRAY // Если нам необходимо читать объект как набор данных массива, то давай - if (asJsonArray) { + if (readingAsArray) { reader.beginArray() val iterator = types.iterator() var fieldId = 0 @@ -333,7 +338,7 @@ class FactoryAdapter private constructor( } } - if (asJsonArray) { + if (readingAsArray) { reader.endArray() } else { reader.endObject() @@ -415,8 +420,7 @@ class FactoryAdapter private constructor( * Позволяет построить класс [FactoryAdapter] на основе заданных параметров */ class Builder(val clazz: KClass, vararg fields: KProperty1) : TypeAdapterFactory { - private var asList = false - private var storesJson = false + private var asList = AsJsonArray.NO private val types = ArrayList>() private val aliases = Object2ObjectArrayMap>() var stringInterner: Interner = Interner { it } @@ -440,7 +444,7 @@ class FactoryAdapter private constructor( * Собирает этот [FactoryAdapter] с указанным GSON объектом */ fun build(gson: Gson): TypeAdapter { - check(!asList || types.none { it.isFlat }) { "Can't have both flat properties and json data array layout" } + check(asList == AsJsonArray.NO || types.none { it.isFlat }) { "Can't have both flat properties and json data array layout" } return FactoryAdapter( clazz = clazz, @@ -478,7 +482,12 @@ class FactoryAdapter private constructor( * @see inputAsList */ fun inputAsMap(): Builder { - asList = false + asList = AsJsonArray.NO + return this + } + + fun inputAsMapOrList(): Builder { + asList = AsJsonArray.EITHER return this } @@ -492,7 +501,7 @@ class FactoryAdapter private constructor( * @see inputAsMap */ fun inputAsList(): Builder { - asList = true + asList = AsJsonArray.YES return this } } @@ -504,7 +513,11 @@ class FactoryAdapter private constructor( val builder = Builder(kclass) val properties = kclass.declaredMembers.filterIsInstance>() - if (config.asList) { + if (config.asList < 0) { + builder.inputAsMap() + } else if (config.asList == 0) { + builder.inputAsMapOrList() + } else { builder.inputAsList() } 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 47255ca0..c4641e33 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/EntityBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/EntityBindings.kt @@ -11,6 +11,7 @@ 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.NPCEntity import ru.dbotthepony.kstarbound.world.entities.player.PlayerEntity import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject @@ -24,6 +25,9 @@ fun provideEntityBindings(self: AbstractEntity, lua: LuaEnvironment) { if (self is ActorEntity) provideStatusControllerBindings(self.statusController, lua) + if (self is NPCEntity) + provideNPCBindings(self, lua) + provideWorldBindings(self.world, lua) val table = lua.newTable() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/MonsterBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/MonsterBindings.kt index 28c9f268..d15cd009 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/MonsterBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/MonsterBindings.kt @@ -27,13 +27,6 @@ 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 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/MovementControllerBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/MovementControllerBindings.kt index f55cf474..f4fb5e1b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/MovementControllerBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/MovementControllerBindings.kt @@ -18,7 +18,7 @@ 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.entities.AnchorNetworkState import ru.dbotthepony.kstarbound.world.physics.Poly import kotlin.math.PI @@ -82,15 +82,15 @@ class MovementControllerBindings(val self: ActorMovementController) { } callbacks["setAnchorState"] = luaFunction { anchor: Number, index: Number -> - self.anchorState = AnchorState(anchor.toInt(), index.toInt()) + self.anchorNetworkState = AnchorNetworkState(anchor.toInt(), index.toInt()) } callbacks["resetAnchorState"] = luaFunction { - self.anchorState = null + self.anchorNetworkState = null } callbacks["anchorState"] = luaFunction { - val anchorState = self.anchorState + val anchorState = self.anchorNetworkState if (anchorState != null) { returnBuffer.setTo(anchorState.entityID, anchorState.positionIndex) @@ -146,8 +146,8 @@ class MovementControllerBindings(val self: ActorMovementController) { 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["movingDirection"] = luaFunction { returnBuffer.setTo(self.movingDirection.numericalValue) } + callbacks["facingDirection"] = luaFunction { returnBuffer.setTo(self.facingDirection.numericalValue) } callbacks["crouching"] = luaFunction { returnBuffer.setTo(self.isCrouching) } callbacks["flying"] = luaFunction { returnBuffer.setTo(self.isFlying) } callbacks["falling"] = luaFunction { returnBuffer.setTo(self.isFalling) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/NPCBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/NPCBindings.kt new file mode 100644 index 00000000..02141353 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/NPCBindings.kt @@ -0,0 +1,222 @@ +package ru.dbotthepony.kstarbound.lua.bindings + +import org.classdump.luna.ByteString +import org.classdump.luna.Table +import ru.dbotthepony.kstarbound.Registries +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.EntityDamageTeam +import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor +import ru.dbotthepony.kstarbound.defs.quest.QuestArcDescriptor +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.lua.toVector2d +import ru.dbotthepony.kstarbound.util.SBPattern +import ru.dbotthepony.kstarbound.util.sbIntern +import ru.dbotthepony.kstarbound.util.valueOfOrNull +import ru.dbotthepony.kstarbound.world.entities.AnchorNetworkState +import ru.dbotthepony.kstarbound.world.entities.HumanoidActorEntity +import ru.dbotthepony.kstarbound.world.entities.NPCEntity +import ru.dbotthepony.kstarbound.world.entities.api.LoungeableEntity + +fun provideNPCBindings(self: NPCEntity, lua: LuaEnvironment) { + val callbacks = lua.newTable() + lua.globals["npc"] = callbacks + + callbacks["toAbsolutePosition"] = luaFunction { pos: Table -> + returnBuffer.setTo(from(self.toAbsolutePosition(toVector2d(pos)))) + } + + callbacks["species"] = luaFunction { returnBuffer.setTo(self.variant.species.key.toByteString()) } + callbacks["gender"] = luaFunction { returnBuffer.setTo(self.variant.humanoidIdentity.gender.jsonName.toByteString()) } + callbacks["humanoidIdentity"] = luaFunction { returnBuffer.setTo(from(Starbound.gson.toJsonTree(self.variant.humanoidIdentity))) } + callbacks["npcType"] = luaFunction { returnBuffer.setTo(self.variant.typeName.toByteString()) } + callbacks["seed"] = luaFunction { returnBuffer.setTo(self.variant.seed) } + callbacks["level"] = luaFunction { returnBuffer.setTo(self.variant.level) } + callbacks["dropPools"] = luaFunction { returnBuffer.setTo(tableOf(*self.dropPools.map { it.key.left().toByteString() }.toTypedArray())) } + + callbacks["setDropPools"] = luaFunction { dropPools: Table? -> + self.dropPools.clear() + + if (dropPools != null) { + for ((_, pool) in dropPools) { + self.dropPools.add(Registries.treasurePools.ref(pool.toString())) + } + } + } + + // lol why + callbacks["energy"] = luaFunction { returnBuffer.setTo(self.energy) } + callbacks["maxEnergy"] = luaFunction { returnBuffer.setTo(self.maxEnergy) } + + callbacks["say"] = luaFunction { line: ByteString, tags: Table?, config: Any? -> + val actualLine = if (tags != null) { + SBPattern.of(line.decode()).resolveOrSkip({ tags[it]?.toString() }) + } else { + line.decode() + } + + val isNotEmpty = actualLine.isNotEmpty() + + if (isNotEmpty) + self.addChatMessage(actualLine, toJsonFromLua(config)) + + returnBuffer.setTo(isNotEmpty) + } + + callbacks["sayPortrait"] = luaFunction { line: ByteString, portrait: ByteString, tags: Table?, config: Any? -> + val actualLine = if (tags != null) { + SBPattern.of(line.decode()).resolveOrSkip({ tags[it]?.toString() }) + } else { + line.decode() + } + + val isNotEmpty = actualLine.isNotEmpty() + + if (isNotEmpty) + self.addChatMessage(actualLine, toJsonFromLua(config), portrait.decode()) + + returnBuffer.setTo(isNotEmpty) + } + + callbacks["emote"] = luaFunction { emote: ByteString -> + self.addEmote(emote.decode()) + } + + callbacks["dance"] = luaFunction { dance: ByteString -> + self.setDance(dance.decode()) + } + + callbacks["setInteractive"] = luaFunction { isInteractive: Boolean -> + self.isInteractive = isInteractive + } + + callbacks["setLounging"] = luaFunction { loungeable: Number, oAnchorIndex: Number? -> + val anchorIndex = oAnchorIndex?.toInt() ?: 0 + val entity = self.world.entities[loungeable.toInt()] as? LoungeableEntity + + if (entity == null || anchorIndex !in 0 until entity.sitPositions.size || entity.entitiesLoungingIn(anchorIndex).isNotEmpty()) { + returnBuffer.setTo(false) + } else { + self.movement.anchorNetworkState = AnchorNetworkState(loungeable.toInt(), anchorIndex) + returnBuffer.setTo(true) + } + } + + callbacks["resetLounging"] = luaFunction { + self.movement.anchorNetworkState = null + } + + callbacks["isLounging"] = luaFunction { + returnBuffer.setTo(self.movement.anchorNetworkState != null) + } + + callbacks["loungingIn"] = luaFunction { + returnBuffer.setTo(self.movement.anchorNetworkState?.entityID) + } + + callbacks["setOfferedQuests"] = luaFunction { values: Table? -> + self.offeredQuests.clear() + + if (values != null) { + for ((_, quest) in values) { + self.offeredQuests.add(Starbound.gson.fromJsonFast(toJsonFromLua(quest), QuestArcDescriptor::class.java)) + } + } + } + + callbacks["setTurnInQuests"] = luaFunction { values: Table? -> + self.turnInQuests.clear() + + if (values != null) { + for ((_, value) in values) { + self.turnInQuests.add(value.toString()) + } + } + } + + callbacks["setItemSlot"] = luaFunction { slot: ByteString, descriptor: Any -> + returnBuffer.setTo(self.setItem(slot.decode(), ItemDescriptor(descriptor))) + } + + callbacks["getItemSlot"] = luaFunction { slot: ByteString -> + val decoded = slot.decode() + val slotType = HumanoidActorEntity.ItemSlot.entries.valueOfOrNull(decoded.lowercase()) + + if (slotType == null) { + if (decoded in self.variant.items) { + returnBuffer.setTo(self.variant.items[decoded]!!.toTable(this)) + } else { + returnBuffer.setTo() + } + } else { + returnBuffer.setTo(self.getItem(slotType).toTable(this)) + } + } + + callbacks["disableWornArmor"] = luaFunction { disable: Boolean -> + self.disableWornArmor = disable + } + + callbacks["beginPrimaryFire"] = luaFunction { self.beginPrimaryFire() } + callbacks["beginAltFire"] = luaFunction { self.beginSecondaryFire() } + callbacks["beginSecondaryFire"] = luaFunction { self.beginSecondaryFire() } + callbacks["endPrimaryFire"] = luaFunction { self.endPrimaryFire() } + callbacks["endAltFire"] = luaFunction { self.endSecondaryFire() } + callbacks["endSecondaryFire"] = luaFunction { self.endSecondaryFire() } + + callbacks["setShifting"] = luaFunction { value: Boolean -> + self.isShifting = value + } + + callbacks["setDamageOnTouch"] = luaFunction { value: Boolean -> + self.damageOnTouch = value + } + + callbacks["aimPosition"] = luaFunction { + returnBuffer.setTo(from(self.aimPosition)) + } + + callbacks["setAimPosition"] = luaFunction { pos: Table -> + self.aimPosition = self.world.geometry.diff(toVector2d(pos), self.position) + } + + callbacks["setDeathParticleBurst"] = luaFunction { value: ByteString? -> + self.deathParticleBurst = value?.decode()?.sbIntern() + } + + callbacks["setStatusText"] = luaFunction { value: ByteString? -> + self.statusText = value?.decode()?.sbIntern() + } + + callbacks["setDisplayNametag"] = luaFunction { value: Boolean -> + self.displayNametag = value + } + + callbacks["setPersistent"] = luaFunction { value: Boolean -> + self.isPersistent = value + } + + callbacks["setKeepAlive"] = luaFunction { value: Boolean -> + self.keepAlive = value + } + + callbacks["setAggressive"] = luaFunction { value: Boolean -> + self.isAggressive = value + } + + callbacks["setDamageTeam"] = luaFunction { value: Table -> + self.team.accept(Starbound.gson.fromJsonFast(toJsonFromLua(value), EntityDamageTeam::class.java)) + } + + callbacks["setUniqueId"] = luaFunction { value: ByteString? -> + self.uniqueID.accept(value?.decode()?.sbIntern()) + } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/StatusControllerBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/StatusControllerBindings.kt index 1f3a5c0b..95542a0a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/StatusControllerBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/StatusControllerBindings.kt @@ -14,7 +14,6 @@ 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 @@ -23,7 +22,6 @@ 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>() @@ -47,7 +45,7 @@ fun provideStatusControllerBindings(self: StatusController, lua: LuaEnvironment) } callbacks["stat"] = luaFunction { name: ByteString -> - returnBuffer.setTo(self.liveStats[name.decode()]?.effectiveModifiedValue ?: 0.0) + returnBuffer.setTo(self.effectiveStats[name.decode()]?.effectiveModifiedValue ?: 0.0) } callbacks["statPositive"] = luaFunction { name: ByteString -> 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 038b4831..78ee210a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEntityBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEntityBindings.kt @@ -346,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.toByteString()) + returnBuffer.setTo(entity.humanoidIdentity.species.key.left().toByteString()) } callbacks["entityGender"] = luaFunction { id: Number -> val entity = self.entities[id.toInt()] as? HumanoidActorEntity ?: return@luaFunction returnBuffer.setTo() - returnBuffer.setTo(entity.gender.jsonName.toByteString()) + returnBuffer.setTo(entity.humanoidIdentity.gender.jsonName.toByteString()) } callbacks["entityName"] = luaFunction { id: Number -> 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 efa8f919..dae75a4b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldObjectBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldObjectBindings.kt @@ -6,7 +6,6 @@ import com.google.gson.JsonPrimitive import org.classdump.luna.ByteString import org.classdump.luna.LuaRuntimeException import org.classdump.luna.Table -import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.DamageSource @@ -51,7 +50,7 @@ fun provideWorldObjectBindings(self: WorldObject, lua: LuaEnvironment) { lua.globals["object"] = table table["name"] = luaFunction { returnBuffer.setTo(self.config.key.toByteString()) } - table["direction"] = luaFunction { returnBuffer.setTo(self.direction.luaValue) } + table["direction"] = luaFunction { returnBuffer.setTo(self.direction.numericalValue) } table["position"] = luaFunction { returnBuffer.setTo(from(self.tilePosition)) } table["setInteractive"] = luaFunction { interactive: Boolean -> self.isInteractive = interactive } table["uniqueId"] = luaFunction { returnBuffer.setTo(self.uniqueID.get().toByteString()) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/API.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/API.kt index 496d5174..f61ce36f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/API.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/API.kt @@ -92,9 +92,13 @@ interface IPacket { } interface IServerPacket : IPacket { - fun play(connection: ServerConnection) + suspend fun play(connection: ServerConnection) + val handleImmediatelyOnServer: Boolean + get() = false } interface IClientPacket : IPacket { - fun play(connection: ClientConnection) + suspend fun play(connection: ClientConnection) + val handleImmediatelyOnClient: Boolean + get() = false } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt index c2bef0a8..06f9b8a8 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt @@ -119,7 +119,7 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) : inGame() } - fun bind(channel: Channel) { + open fun bind(channel: Channel) { scope = CoroutineScope(channel.eventLoop().asCoroutineDispatcher() + SupervisorJob() + CoroutineExceptionHandler { coroutineContext, throwable -> disconnect("Uncaught exception in one of connection' coroutines: $throwable") LOGGER.fatal("Uncaught exception in one of $this coroutines", throwable) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt index 9d33e1d3..d2e5aa43 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt @@ -219,6 +219,10 @@ class PacketRegistry(val isLegacy: Boolean) { i++ try { + // TODO: separate "immediate" and "sequential" packets, so immediate packets are decompressed/parsed right away, + // and sequential packets are decompressed/parsed only when they need to be handled + // This way, malicious clients won't be able to fill up server memory by repeatedly sending huge packets + // which can take up long time to process ctx.fireChannelRead(readingType!!.factory.read(DataInputStream(stream), isLegacy, side)) } catch (err: Throwable) { LOGGER.error("Error while reading incoming packet from network (type ${readingType!!.id}; ${readingType!!.type}; packet No. $i in stream)", err) @@ -289,6 +293,13 @@ class PacketRegistry(val isLegacy: Boolean) { networkReadBuffer.size(index + msg.readableBytes()) msg.readBytes(networkReadBuffer.elements(), index, msg.readableBytes()) drainNetworkBuffer(ctx) + + if (networkReadBuffer.size >= MAX_BUFFER_SIZE) { + // if client sends billions of data, tell them to fuck off (the rude way) + networkReadBuffer.clear() + networkReadBuffer.trim() + ctx.channel().close() + } } } finally { msg.release() @@ -383,6 +394,7 @@ class PacketRegistry(val isLegacy: Boolean) { companion object { const val LOG_PACKETS = false const val MAX_PACKET_SIZE = 64L * 1024L * 1024L // 64 MiB + const val MAX_BUFFER_SIZE = 128L * 1024L * 1024L // 128 MiB // this includes both compressed and uncompressed // Original game allows 16 mebibyte packets // but it doesn't account for compression bomb (packets are fully uncompressed 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 0da3be27..aa92c562 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/ClientContextUpdatePacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/ClientContextUpdatePacket.kt @@ -76,11 +76,11 @@ class ClientContextUpdatePacket( } } - override fun play(connection: ServerConnection) { + override suspend fun play(connection: ServerConnection) { connection.rpc.read(rpcEntries) } - override fun play(connection: ClientConnection) { + override suspend fun play(connection: ClientConnection) { connection.rpc.read(rpcEntries) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/DamageNotificationPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/DamageNotificationPacket.kt index fcdfed16..678e0a6d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/DamageNotificationPacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/DamageNotificationPacket.kt @@ -25,7 +25,7 @@ class DamageNotificationPacket(val source: Int, val notification: DamageNotifica client.isTracking(notification.position) } - override fun play(connection: ServerConnection) { + override suspend fun play(connection: ServerConnection) { connection.enqueue { pushRemoteDamageNotification(this@DamageNotificationPacket) @@ -37,7 +37,7 @@ class DamageNotificationPacket(val source: Int, val notification: DamageNotifica } } - override fun play(connection: ClientConnection) { + override suspend fun play(connection: ClientConnection) { connection.enqueue { world?.pushRemoteDamageNotification(this@DamageNotificationPacket) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/DamageRequestPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/DamageRequestPacket.kt index 5119f481..90d909f4 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/DamageRequestPacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/DamageRequestPacket.kt @@ -19,13 +19,13 @@ class DamageRequestPacket(val inflictor: Int, val target: Int, val request: Dama val destinationConnection: Int get() = Connection.connectionForEntityID(target) - override fun play(connection: ServerConnection) { - connection.enqueue { + override suspend fun play(connection: ServerConnection) { + connection.enqueueAndSuspend { pushRemoteDamageRequest(this@DamageRequestPacket) } } - override fun play(connection: ClientConnection) { + override suspend fun play(connection: ClientConnection) { connection.enqueue { world?.pushRemoteDamageRequest(this@DamageRequestPacket) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityCreatePacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityCreatePacket.kt index b7170bee..4ef1f284 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityCreatePacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityCreatePacket.kt @@ -30,7 +30,7 @@ class EntityCreatePacket(val entityType: EntityType, val storeData: ByteArrayLis stream.writeSignedVarInt(entityID) } - override fun play(connection: ServerConnection) { + override suspend fun play(connection: ServerConnection) { if (entityID !in connection.entityIDRange) { LOGGER.error("Player $connection tried to create entity $entityType with ID $entityID, but that's outside of allowed range ${connection.entityIDRange}!") connection.disconnect("Creating entity with ID $entityID outside of allowed range ${connection.entityIDRange}") @@ -46,13 +46,13 @@ class EntityCreatePacket(val entityType: EntityType, val storeData: ByteArrayLis entity.isRemote = true entity.networkGroup.upstream.enableInterpolation(0.0) - connection.enqueue { + connection.enqueueAndSuspend { entity.joinWorld(this) } } } - override fun play(connection: ClientConnection) { + override suspend fun play(connection: ClientConnection) { TODO("Not yet implemented") } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityDestroyPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityDestroyPacket.kt index 2a54bd56..e5a16346 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityDestroyPacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityDestroyPacket.kt @@ -23,7 +23,7 @@ class EntityDestroyPacket(val entityID: Int, val finalNetState: ByteArrayList, v stream.writeBoolean(isDeath) } - override fun play(connection: ServerConnection) { + override suspend fun play(connection: ServerConnection) { if (entityID !in connection.entityIDRange) { LOGGER.error("Client $connection tried to remove entity with ID $entityID, but that's outside of allowed range ${connection.entityIDRange}!") connection.disconnect("Removing entity with ID $entityID outside of allowed range ${connection.entityIDRange}") @@ -34,7 +34,7 @@ class EntityDestroyPacket(val entityID: Int, val finalNetState: ByteArrayList, v } } - override fun play(connection: ClientConnection) { + override suspend fun play(connection: ClientConnection) { TODO("Not yet implemented") } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityMessagePacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityMessagePacket.kt index bf2b3fab..6abad863 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityMessagePacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityMessagePacket.kt @@ -44,17 +44,18 @@ class EntityMessagePacket(val entity: Either, val message: String, stream.writeShort(sourceConnection) } - override fun play(connection: ServerConnection) { + override suspend fun play(connection: ServerConnection) { val tracker = connection.tracker if (tracker == null) { connection.send(EntityMessageResponsePacket(Either.left("Not in world"), this@EntityMessagePacket.id)) } else { + // Don't suspend here, entity messages are generally not reliable tracker.handleEntityMessage(this) } } - override fun play(connection: ClientConnection) { + override suspend fun play(connection: ClientConnection) { connection.enqueue { val world = world diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityMessageResponsePacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityMessageResponsePacket.kt index 9a43c130..c9a9bfe1 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityMessageResponsePacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityMessageResponsePacket.kt @@ -31,7 +31,8 @@ class EntityMessageResponsePacket(val response: Either, val stream.writeUUID(id) } - override fun play(connection: ServerConnection) { + override suspend fun play(connection: ServerConnection) { + // Don't suspend here, entity messages are generally not reliable connection.enqueue { val message = pendingEntityMessages.asMap().remove(id) @@ -45,7 +46,7 @@ class EntityMessageResponsePacket(val response: Either, val } } - override fun play(connection: ClientConnection) { + override suspend fun play(connection: ClientConnection) { connection.enqueue { val message = world?.pendingEntityMessages?.asMap()?.remove(this@EntityMessageResponsePacket.id) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityUpdateSetPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityUpdateSetPacket.kt index b042013b..dc0b8e0c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityUpdateSetPacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityUpdateSetPacket.kt @@ -29,8 +29,8 @@ class EntityUpdateSetPacket(val forConnection: Int, val deltas: Int2ObjectMap, val ships: stream.writeMap(ships, { writeUUID(it) }, { writeByteArray(it.elements(), 0, it.size) }) } - override fun play(connection: ClientConnection) { + override suspend fun play(connection: ClientConnection) { TODO("Not yet implemented") } } \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/TileDamageUpdatePacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/TileDamageUpdatePacket.kt index d6afbb8f..8ced76c3 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/TileDamageUpdatePacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/TileDamageUpdatePacket.kt @@ -16,7 +16,7 @@ class TileDamageUpdatePacket(val x: Int, val y: Int, val isBackground: Boolean, health.write(stream, isLegacy) } - override fun play(connection: ClientConnection) { + override suspend fun play(connection: ClientConnection) { TODO("Not yet implemented") } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/TileModificationFailurePacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/TileModificationFailurePacket.kt index ad7c280a..740404f6 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/TileModificationFailurePacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/TileModificationFailurePacket.kt @@ -18,7 +18,7 @@ class TileModificationFailurePacket(val modifications: Collection } } - override fun play(connection: ServerConnection) { + override val handleImmediatelyOnServer: Boolean + get() = true + + override suspend fun play(connection: ServerConnection) { connection.pushCelestialRequests(requests) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ChatSendPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ChatSendPacket.kt index 814b894e..48c92bd2 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ChatSendPacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ChatSendPacket.kt @@ -11,12 +11,15 @@ import java.io.DataOutputStream class ChatSendPacket(val text: String, val mode: ChatSendMode) : IServerPacket { constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readBinaryString(), ChatSendMode.entries[stream.readUnsignedByte()]) + override val handleImmediatelyOnServer: Boolean + get() = true + override fun write(stream: DataOutputStream, isLegacy: Boolean) { stream.writeBinaryString(text) stream.writeByte(mode.ordinal) } - override fun play(connection: ServerConnection) { + override suspend fun play(connection: ServerConnection) { connection.server.chat.handle(connection, this) } } 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 1b47f2e1..1b080b9e 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 @@ -57,7 +57,7 @@ data class ClientConnectPacket( stream.writeBinaryString(account) } - override fun play(connection: ServerConnection) { + override suspend fun play(connection: ServerConnection) { if (connection.server.clientByUUID(playerUuid) != null) { connection.send(ConnectFailurePacket("Duplicate player UUID $playerUuid")) LOGGER.warn("Unable to accept player $playerName/$playerUuid because such UUID is already taken") diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ClientDisconnectRequestPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ClientDisconnectRequestPacket.kt index 5cde89b0..af339784 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ClientDisconnectRequestPacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ClientDisconnectRequestPacket.kt @@ -10,7 +10,7 @@ object ClientDisconnectRequestPacket : IServerPacket { if (isLegacy) stream.writeBoolean(false) } - override fun play(connection: ServerConnection) { + override suspend fun play(connection: ServerConnection) { connection.disconnect("Disconnect by user.") } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ConnectWirePacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ConnectWirePacket.kt index 66f78e1f..ff0351b5 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ConnectWirePacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ConnectWirePacket.kt @@ -15,13 +15,13 @@ class ConnectWirePacket(val target: WireConnection, val source: WireConnection) source.write(stream) } - override fun play(connection: ServerConnection) { - connection.enqueue { - val target = entityIndex.tileEntityAt(target.entityLocation, WorldObject::class) ?: return@enqueue - val source = entityIndex.tileEntityAt(source.entityLocation, WorldObject::class) ?: return@enqueue + override suspend fun play(connection: ServerConnection) { + connection.enqueueAndSuspend { + val target = entityIndex.tileEntityAt(target.entityLocation, WorldObject::class) ?: return@enqueueAndSuspend + val source = entityIndex.tileEntityAt(source.entityLocation, WorldObject::class) ?: return@enqueueAndSuspend - val targetNode = target.outputNodes.getOrNull(this@ConnectWirePacket.target.index) ?: return@enqueue - val sourceNode = source.inputNodes.getOrNull(this@ConnectWirePacket.source.index) ?: return@enqueue + val targetNode = target.outputNodes.getOrNull(this@ConnectWirePacket.target.index) ?: return@enqueueAndSuspend + val sourceNode = source.inputNodes.getOrNull(this@ConnectWirePacket.source.index) ?: return@enqueueAndSuspend if (this@ConnectWirePacket.source in targetNode.connections && this@ConnectWirePacket.target in sourceNode.connections) { // disconnect diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/DamageTileGroupPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/DamageTileGroupPacket.kt index ed0550ac..1f1f8b11 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/DamageTileGroupPacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/DamageTileGroupPacket.kt @@ -41,7 +41,7 @@ class DamageTileGroupPacket(val tiles: Collection, val isBackground: B stream.writeInt(source) } - override fun play(connection: ServerConnection) { + override suspend fun play(connection: ServerConnection) { connection.tracker?.damageTiles(tiles, isBackground, sourcePosition, damage, source) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/DisconnectAllWiresPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/DisconnectAllWiresPacket.kt index 0b6a53e3..b93f0691 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/DisconnectAllWiresPacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/DisconnectAllWiresPacket.kt @@ -19,7 +19,7 @@ class DisconnectAllWiresPacket(val pos: Vector2i, val node: WireNode) : IServerP node.write(stream, isLegacy) } - override fun play(connection: ServerConnection) { + override suspend fun play(connection: ServerConnection) { connection.enqueue { val target = entityIndex.tileEntityAt(pos, WorldObject::class) ?: return@enqueue val node = if (node.isInput) target.inputNodes.getOrNull(node.index) else target.outputNodes.getOrNull(node.index) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/EntityInteractPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/EntityInteractPacket.kt index e6d1ff35..21f1eb7c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/EntityInteractPacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/EntityInteractPacket.kt @@ -26,7 +26,7 @@ class EntityInteractPacket(val request: InteractRequest, val id: UUID) : IServer stream.writeUUID(id) } - override fun play(connection: ServerConnection) { + override suspend fun play(connection: ServerConnection) { if (request.target >= 0) { connection.enqueue { connection.send(EntityInteractResultPacket((entities[request.target] as? InteractiveEntity)?.interact(request) ?: InteractAction.NONE, id, request.source)) @@ -41,7 +41,7 @@ class EntityInteractPacket(val request: InteractRequest, val id: UUID) : IServer } } - override fun play(connection: ClientConnection) { + override suspend fun play(connection: ClientConnection) { TODO("Not yet implemented") } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/FindUniqueEntityPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/FindUniqueEntityPacket.kt index 7e0969de..eb31d8e3 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/FindUniqueEntityPacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/FindUniqueEntityPacket.kt @@ -11,11 +11,16 @@ import java.io.DataOutputStream class FindUniqueEntityPacket(val name: String) : IServerPacket { constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readInternedString()) + // querying client-self unique entities shouldn't be considered + // so we can handle this request right away + override val handleImmediatelyOnServer: Boolean + get() = true + override fun write(stream: DataOutputStream, isLegacy: Boolean) { stream.writeBinaryString(name) } - override fun play(connection: ServerConnection) { + override suspend fun play(connection: ServerConnection) { val tracker = connection.tracker if (tracker != null) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/FlyShipPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/FlyShipPacket.kt index a7aee40f..d356561d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/FlyShipPacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/FlyShipPacket.kt @@ -17,7 +17,10 @@ class FlyShipPacket(val system: Vector3i, val location: SystemWorldLocation = Sy location.write(stream, isLegacy) } - override fun play(connection: ServerConnection) { + override suspend fun play(connection: ServerConnection) { connection.flyShip(system, location) } + + override val handleImmediatelyOnServer: Boolean + get() = true } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/HandshakeResponsePacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/HandshakeResponsePacket.kt index 87a215c1..c333ddd5 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/HandshakeResponsePacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/HandshakeResponsePacket.kt @@ -10,11 +10,14 @@ import java.io.DataOutputStream class HandshakeResponsePacket(val passwordHash: ByteArray) : IServerPacket { constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readByteArray()) + override val handleImmediatelyOnServer: Boolean + get() = true + override fun write(stream: DataOutputStream, isLegacy: Boolean) { stream.writeByteArray(passwordHash) } - override fun play(connection: ServerConnection) { + override suspend fun play(connection: ServerConnection) { TODO("Not yet implemented") } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ModifyTileListPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ModifyTileListPacket.kt index 848c523d..1ef34e33 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ModifyTileListPacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ModifyTileListPacket.kt @@ -20,7 +20,7 @@ class ModifyTileListPacket(val modifications: Collection Unit): Boolean { + if (isDisconnecting.get()) + return false + + val future = tracker?.enqueue(task) + + if (future == null) + LOGGER.warn("$this tried to interact with world, but they are not in one.") + + future?.await() + return future != null } lateinit var shipWorld: ServerWorld @@ -164,12 +174,12 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn private var remoteVersion = 0L private var saveClientContextTask: Future<*>? = null - private data class WarpRequest(val action: WarpAction, val deploy: Boolean, val ifFailed: WarpAction?) + private data class WarpRequest(val action: WarpAction, val deploy: Boolean, val ifFailed: WarpAction?, val future: CompletableFuture) private val warpQueue = Channel(capacity = 10) private suspend fun warpEventLoop() { while (true) { - var (request, deploy, ifFailed) = warpQueue.receive() + var (request, deploy, ifFailed, future) = warpQueue.receive() if (request is WarpAlias) request = request.remap(this) @@ -183,9 +193,11 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn if (resolve.isLimbo) { send(PlayerWarpResultPacket(false, request, true)) + future.complete(false) } else if (tracker?.world?.worldID == resolve) { LOGGER.info("${alias()} tried to warp into world they are already in.") send(PlayerWarpResultPacket(true, request, false)) + future.complete(true) } else { val world = try { server.loadWorld(resolve).await() @@ -197,6 +209,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn enqueueWarp(ifFailed) } + future.complete(false) continue } @@ -212,6 +225,8 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn } else { orbitalWarpAction = KOptional() } + + future.complete(true) } catch (err: Throwable) { send(PlayerWarpResultPacket(false, request, false)) @@ -219,6 +234,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn disconnect("ShipWorld refused to accept its owner: $err") } else { enqueueWarp(returnWarp ?: WarpAlias.OwnShip) + future.complete(false) } } } @@ -300,7 +316,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn if (tryAnother == null) { // how? - disconnect("Unable to put player in system world") + disconnect("Unable to put player's ship in star system world") return } else { actualInWorldLocation = SystemWorldLocation.Celestial(find) @@ -500,8 +516,15 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn } } - fun enqueueWarp(destination: WarpAction, deploy: Boolean = false, ifFailed: WarpAction? = null) { - warpQueue.trySend(WarpRequest(destination, deploy, ifFailed)) + fun enqueueWarp(destination: WarpAction, deploy: Boolean = false, ifFailed: WarpAction? = null): CompletableFuture { + val future = CompletableFuture() + val status = warpQueue.trySend(WarpRequest(destination, deploy, ifFailed, future)) + + if (!status.isSuccess) { + future.complete(false) + } + + return future } private var sendContextUpdatesTask: Future<*>? = null @@ -635,16 +658,50 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn } } + private val packetHandleQueue = Channel(if (Starbound.DEBUG_BUILD) Int.MAX_VALUE else 128) + + private suspend fun handle(msg: IServerPacket) { + try { + msg.play(this@ServerConnection) + } catch (err: Throwable) { + LOGGER.error("Failed to handle serverbound packet $msg", err) + disconnect("Incoming packet caused an exception: $err") + } + } + + override fun bind(channel: io.netty.channel.Channel) { + super.bind(channel) + + scope.launch { + while (true) { + handle(packetHandleQueue.receive()) + } + } + } + override fun channelRead(ctx: ChannelHandlerContext, msg: Any) { if (!channel.isOpen || isDisconnecting.get()) return if (msg is IServerPacket) { - try { - msg.play(this) - } catch (err: Throwable) { - LOGGER.error("Failed to handle serverbound packet $msg", err) - disconnect("Incoming packet caused an exception: $err") + if (msg.handleImmediatelyOnServer) { + scope.launch { handle(msg) } + } else { + val status = packetHandleQueue.trySend(msg) + + if (!status.isSuccess) { + if (LOGGER.isDebugEnabled) { + var receive = packetHandleQueue.tryReceive() + var i = 0 + + while (receive.isSuccess) { + LOGGER.debug("Message ${i++}: ${receive.getOrThrow()}") + receive = packetHandleQueue.tryReceive() + } + } + + disconnect("Too many network messages in processing queue") + } } } else { LOGGER.error("Unknown serverbound packet type $msg") @@ -685,11 +742,13 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn val startingLocation = findStartingSystem() ?: return scope.launch { shipFlightEventLoop(startingLocation.location, SystemWorldLocation.Celestial(startingLocation)) } } else { - if (context.returnWarp != null) { + /*if (context.returnWarp != null) { enqueueWarp(context.returnWarp, ifFailed = WarpAlias.OwnShip) } else { enqueueWarp(WarpAlias.OwnShip) - } + }*/ + + enqueueWarp(WarpAction.World(WorldID.Instance("outpost"))) scope.launch { shipFlightEventLoop(context.shipCoordinate, context.systemLocation) } } 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 7cbbc6dc..39b8e50e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt @@ -180,7 +180,7 @@ class ServerWorld private constructor( private var uncleanShutdown = false - override val eventLoop = object : BlockableEventLoop("Server World $worldID") { + override val eventLoop = object : BlockableEventLoop(worldID.toString()) { init { isDaemon = false } @@ -859,7 +859,7 @@ class ServerWorld private constructor( } } - LOGGER.warn("Unable to find proper player start location, will use $pos") + LOGGER.error("Unable to find proper player start location, will use $pos") return pos } finally { tickets.forEach { it.cancel() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt index 1e6dd39b..5083d3af 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt @@ -89,33 +89,30 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p client.worldID = world.worldID } - private data class DamageTileEntry(val positions: Collection, val isBackground: Boolean, val sourcePosition: Vector2d, val damage: TileDamage, val source: Int? = null) - private val damageTilesQueue = Channel(64) // 64 pending tile group damage requests should be more than enough - private val tileModificationBudget = ActionPacer(actions = 512, handicap = 2048) // TODO: make this configurable - private val modifyTilesQueue = Channel>, Boolean>>(64) - private val findUniqueEntityQueue = Channel(1024) + private val tileModificationBudget = ActionPacer(actions = 512, handicap = 2048) // TODO: make this configurable, move to ServerConnection + private val findUniqueEntityQueue = Channel(if (Starbound.DEBUG_BUILD) Int.MAX_VALUE else 1024) - private suspend fun damageTilesLoop() { - while (true) { - val (positions, isBackground, sourcePosition, damage, source) = damageTilesQueue.receive() + suspend fun damageTiles(positions: Collection, isBackground: Boolean, sourcePosition: Vector2d, damage: TileDamage, source: Int? = null): Unit? { + if (isRemoved.get()) + return null + scope.launch { try { world.damageTiles(positions, isBackground, sourcePosition, damage, world.entities[source], tileModificationBudget) } catch (err: Throwable) { LOGGER.error("Exception in player damage tiles loop", err) client.disconnect("Exception in player damage tiles loop: $err") } - } + }.join() + + return Unit } - fun damageTiles(positions: Collection, isBackground: Boolean, sourcePosition: Vector2d, damage: TileDamage, source: Int? = null) { - damageTilesQueue.trySend(DamageTileEntry(positions, isBackground, sourcePosition, damage, source)) - } - - private suspend fun modifyTilesLoop() { - while (true) { - val (modifications, allowEntityOverlap) = modifyTilesQueue.receive() + suspend fun modifyTiles(modifications: Collection>, allowEntityOverlap: Boolean): Unit? { + if (isRemoved.get()) + return null + scope.launch { try { val unapplied = world.applyTileModifications(modifications, allowEntityOverlap, pacer = tileModificationBudget) @@ -129,11 +126,9 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p LOGGER.error("Exception in player modify tiles loop", err) client.disconnect("Exception in player modify tiles loop: $err") } - } - } + }.join() - fun modifyTiles(modifications: Collection>, allowEntityOverlap: Boolean) { - modifyTilesQueue.trySend(modifications to allowEntityOverlap) + return Unit } private suspend fun findUniqueEntityLoop() { @@ -148,13 +143,11 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p } init { - scope.launch { damageTilesLoop() } - scope.launch { modifyTilesLoop() } scope.launch { findUniqueEntityLoop() } } // max 4096 pending messages - private val entityMessageQueue = Channel(4096) + private val entityMessageQueue = Channel(if (Starbound.DEBUG_BUILD) Int.MAX_VALUE else 4096) // handle up to 512 messages per second private val messageQueuePacer = ActionPacer(actions = 512, handicap = 512) @@ -202,9 +195,9 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p // packets which interact with world must be // executed on world's thread - fun enqueue(task: ServerWorld.(ServerWorldTracker) -> Unit) { + fun enqueue(task: ServerWorld.(ServerWorldTracker) -> Unit): CompletableFuture { if (!isRemoved.get()) { - tasksQueue.add(world.eventLoop.supplyAsync { + val future = world.eventLoop.supplyAsync { if (!isRemoved.get()) { try { task(world, this) @@ -213,8 +206,13 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p client.disconnect("Exception executing queued player task: $err") } } - }) + } + + tasksQueue.add(future) + return future.thenApply { true } } + + return CompletableFuture.completedFuture(false) } private inner class Ticket(val ticket: ServerChunk.ITicket, val pos: ChunkPos) : IChunkListener { @@ -483,8 +481,6 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p world.eventLoop.execute { remove0() } scope.cancel() - damageTilesQueue.close() - modifyTilesQueue.close() } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/AssetPathStack.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/AssetPathStack.kt index cd96c13d..5b9c4574 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/AssetPathStack.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/AssetPathStack.kt @@ -32,6 +32,26 @@ object AssetPathStack { fun lastFolder() = folderStack.lastOrNull() ?: "/" fun lastFile() = fileStack.lastOrNull() ?: "/" + fun assetCommentary(firstSpace: Boolean = true, secondSpace: Boolean = false): String { + if (fileStack.isEmpty()) { + if (secondSpace) { + return " " + } else { + return "" + } + } else { + if (secondSpace && firstSpace) { + return " (from asset ${fileStack.last()}) ".sbIntern() + } else if (secondSpace) { + return "From asset ${fileStack.last()}; ".sbIntern() + } else if (firstSpace) { + return " (from asset ${fileStack.last()})".sbIntern() + } else { + return "From asset ${fileStack.last()}".sbIntern() + } + } + } + fun pop() { folderStack.removeLast() fileStack.removeLast() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Clocks.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Clocks.kt index e89f1ebf..eeb01193 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Clocks.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Clocks.kt @@ -142,11 +142,14 @@ class JVMClock : IClock { get() = if (isPaused) (baseline / 1_000_000_000.0) else ((System.nanoTime() - origin) + baseline) / 1_000_000_000.0 } -class GameTimer(val time: Double = 0.0) { +class GameTimer(time: Double = 0.0) { constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readDouble(isLegacy)) { timer = stream.readDouble(isLegacy) } + var time = time + private set + fun write(stream: DataOutputStream, isLegacy: Boolean) { stream.writeDouble(time, isLegacy) stream.writeDouble(timer, isLegacy) @@ -155,7 +158,13 @@ class GameTimer(val time: Double = 0.0) { var timer = time private set - fun reset() { + init { + require(time >= 0.0) { "Negative time: $time" } + } + + fun reset(time: Double = this.time) { + require(time >= 0.0) { "Negative time: $time" } + this.time = time timer = time } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/HashTableInterner.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/HashTableInterner.kt index 204860c0..03d8daca 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/HashTableInterner.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/HashTableInterner.kt @@ -207,7 +207,6 @@ class HashTableInterner(private val segmentBits: Int = log(Runtime.getR //.filter { !it.refersTo(null) } .map { val v = it.get(); it.clear(); v } .filter { it != null } - .collect(ObjectArrayList.toList()) val new = Segment(size * 2, lock) for (elem in old) new.insert(elem as T) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Utils.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Utils.kt index 0be722f0..ef1d56e9 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Utils.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Utils.kt @@ -4,6 +4,7 @@ import com.google.gson.JsonElement import ru.dbotthepony.kstarbound.io.StreamCodec import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.json.builder.IStringSerializable +import ru.dbotthepony.kstarbound.world.positiveModulo import java.util.* import java.util.concurrent.CompletableFuture import java.util.concurrent.Executor @@ -11,6 +12,7 @@ import java.util.function.Supplier import java.util.stream.Stream import kotlin.NoSuchElementException import kotlin.collections.Collection +import kotlin.collections.List fun String.sbIntern(): String { return Starbound.STRINGS.intern(this) @@ -79,6 +81,10 @@ fun Collection.valueOf(value: String): E { return firstOrNull { it.match(value) } ?: throw NoSuchElementException("'$value' is not a valid ${first()::class.qualifiedName}") } +fun Collection.valueOfOrNull(value: String): E? { + return firstOrNull { it.match(value) } +} + fun Executor.supplyAsync(block: Supplier): CompletableFuture { return CompletableFuture.supplyAsync(block, this) } @@ -90,3 +96,10 @@ fun String.limit(limit: Int = 40, replacer: String = "..."): String { return substring(0, limit) + replacer } + +fun Collection.getWrapAround(index: Int): T { + if (isEmpty()) + throw NoSuchElementException("List is empty") + + return this.elementAt(positiveModulo(index, size)) +} 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 39de2880..75d4bdff 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/RandomUtils.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/RandomUtils.kt @@ -1,5 +1,6 @@ package ru.dbotthepony.kstarbound.util.random +import com.google.common.collect.ImmutableCollection import com.google.gson.JsonArray import com.google.gson.JsonElement import it.unimi.dsi.fastutil.bytes.ByteConsumer @@ -9,6 +10,7 @@ import ru.dbotthepony.kommons.util.IStruct2f import ru.dbotthepony.kommons.util.IStruct2i import ru.dbotthepony.kommons.util.XXHash32 import ru.dbotthepony.kommons.util.XXHash64 +import ru.dbotthepony.kstarbound.getValue import java.util.* import java.util.random.RandomGenerator import java.util.stream.IntStream @@ -26,6 +28,13 @@ fun random(seed: Long = System.nanoTime()): RandomGenerator { return MWCRandom(seed.toULong()) } +/** + * Starbound's thread-local [random] variable. + * + * In most cases, it should not be used, and instead some other [random] instance should be used + */ +val threadLocalRandom: RandomGenerator by ThreadLocal.withInitial { random() } + private fun toBytes(accept: ByteConsumer, value: Short) { accept.accept(value.toByte()) accept.accept((value.toInt() ushr 8).toByte()) @@ -221,13 +230,22 @@ fun RandomGenerator.nextUInt(max: ULong): ULong { fun Collection.random(random: RandomGenerator, legacyCompatibleSelection: Boolean = false): T { if (isEmpty()) - throw NoSuchElementException("List is empty") + throw NoSuchElementException("Collection is empty") - if (legacyCompatibleSelection) { - // marvelous - return elementAt(random.nextUInt(size.toULong() - 1UL).toInt()) + if (this is ImmutableCollection) { + if (legacyCompatibleSelection) { + // marvelous + return asList()[random.nextUInt(size.toULong() - 1UL).toInt()] + } else { + return asList()[random.nextInt(size)] + } } else { - return elementAt(random.nextInt(size)) + if (legacyCompatibleSelection) { + // marvelous + return elementAt(random.nextUInt(size.toULong() - 1UL).toInt()) + } else { + return elementAt(random.nextInt(size)) + } } } @@ -235,21 +253,24 @@ fun JsonArray.random(random: RandomGenerator): JsonElement { if (isEmpty()) throw NoSuchElementException("List is empty") - return elementAt(random.nextInt(size())) + return this[random.nextInt(size())] } fun Collection.random(random: RandomGenerator, default: () -> T): T { if (isEmpty()) return default.invoke() - return elementAt(random.nextInt(size)) + if (this is ImmutableCollection) + return asList()[random.nextInt(size)] + else + return elementAt(random.nextInt(size)) } fun JsonArray.random(random: RandomGenerator, default: () -> JsonElement): JsonElement { if (isEmpty()) return default.invoke() - return elementAt(random.nextInt(size())) + return this[random.nextInt(size())] } fun RandomGenerator.nextRange(range: IStruct2i): Int { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Direction.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Direction.kt index 8e854bc4..b7965b04 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Direction.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Direction.kt @@ -5,7 +5,7 @@ import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.json.builder.IStringSerializable // uint8_t -enum class Direction(val normal: Vector2d, override val jsonName: String, val luaValue: Long, val isRight: Boolean, val isLeft: Boolean) : IStringSerializable { +enum class Direction(val normal: Vector2d, override val jsonName: String, val numericalValue: Long, val isRight: Boolean, val isLeft: Boolean) : IStringSerializable { LEFT(Vector2d.NEGATIVE_X, "left", -1L, false, true) { override val opposite: Direction get() = RIGHT diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt index f1da74dc..d63c0b9e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt @@ -543,11 +543,6 @@ abstract class World, ChunkType : Chunk() - var playerSpawnPosition = Vector2d.ZERO protected set @@ -675,30 +670,24 @@ abstract class World, ChunkType : Chunk>() - var batch = ArrayList() + var batch = ArrayList() - for (entity in dynamicEntities) { + for (entity in entityList) { batch.add(entity) if (batch.size == 32) { val b = batch - tasks.add(CompletableFuture.runAsync(Runnable { b.forEach { if (!it.isRemote) it.move(delta) else it.moveRemote(delta) } }, Starbound.EXECUTOR)) + tasks.add(CompletableFuture.runAsync(Runnable { b.forEach { it.tickParallel(delta) } }, Starbound.EXECUTOR)) batch = ArrayList() } } if (batch.isNotEmpty()) { - tasks.add(CompletableFuture.runAsync(Runnable { batch.forEach { if (!it.isRemote) it.move(delta) else it.moveRemote(delta) } }, Starbound.EXECUTOR)) + tasks.add(CompletableFuture.runAsync(Runnable { batch.forEach { it.tickParallel(delta) } }, Starbound.EXECUTOR)) } CompletableFuture.allOf(*tasks.toTypedArray()).join() 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 f0a1eb16..7ced04d4 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt @@ -31,7 +31,9 @@ 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.ServerChunk import ru.dbotthepony.kstarbound.server.world.ServerWorld +import ru.dbotthepony.kstarbound.world.ChunkState import ru.dbotthepony.kstarbound.world.EntityIndex import ru.dbotthepony.kstarbound.world.LightCalculator import ru.dbotthepony.kstarbound.world.TileRayFilter @@ -140,17 +142,41 @@ abstract class AbstractEntity : Comparable { val team = networkedData(EntityDamageTeam(), EntityDamageTeam.CODEC, EntityDamageTeam.LEGACY_CODEC) enum class RemovalReason(val removal: Boolean, val dying: Boolean, val remote: Boolean) { + /** + * Being saved to disk (entity is local) + */ UNLOADED(false, false, false), // Being saved to disk + + /** + * Entity is being removed (entity is local) + */ REMOVED(true, false, false), // Got removed from world + + /** + * Entity is being removed in event of demise (entity is local) + */ DYING(true, true, false), // Same as REMOVED, but indicates that entity has died + /** + * Entity is being removed, but we are client for said entity (entity is not local) + */ REMOTE_REMOVAL(true, false, true), // We were client for that entity, // and other side removed that entity + /** + * Entity is being removed in event of demise, but we are client for said entity (entity is not local) + */ REMOTE_DYING(true, true, true); // REMOTE_REMOVAL + DYING } + /** + * Called upon being added to [world], regardless if entity is local or not + */ protected open fun onJoinWorld(world: World<*, *>) { } + + /** + * Called upon being removed from [world], regardless if entity is local or not + */ protected open fun onRemove(world: World<*, *>, reason: RemovalReason) { } val networkGroup = MasterElement(NetworkedGroup()) @@ -308,6 +334,14 @@ abstract class AbstractEntity : Comparable { val isLocal: Boolean get() = !isRemote + fun ensureIsRemote() { + check(isRemote) { "Calling remote-only function on local entity" } + } + + fun ensureIsLocal() { + check(isLocal) { "Calling local-only function on remote entity" } + } + open val mouthPosition: Vector2d get() = position @@ -423,12 +457,36 @@ abstract class AbstractEntity : Comparable { */ open fun damagedOther(notification: DamageNotificationPacket) {} - open fun tick(delta: Double) { + open val keepAlive: Boolean + get() = false + + private var chunkTicket: ServerChunk.ITicket? = null + + /** + * Called in parallel (if deemed necessary), to perform isolated tasks + * + * Rules: + * * Must not interact with other entities (including reading their state, unless it is ensured said state does not update concurrently); + * * Must not update world state (including the use of [World.random]); + * * Can read general world state, such as [World.sky], [World.chunkMap], [World.uniqueEntities], etc.; + * * Can update [World.entityIndex] but NOT [World.entities]; + */ + open fun tickParallel(delta: Double) { if (networkGroup.upstream.isInterpolating) { networkGroup.upstream.tickInterpolation(delta) } damageEvents.removeIf { it.expiresAt <= world.sky.time } + } + + /** + * Called in sequence, to perform general tasks + */ + open fun tick(delta: Double) { + if (keepAlive && isInWorld && world.isServer && chunkTicket?.pos != world.geometry.chunkFromCell(position)) { + chunkTicket?.cancel() + chunkTicket = serverWorld.chunkMap[world.geometry.chunkFromCell(position)]?.permanentTicket(ChunkState.EMPTY) + } val world = world 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 387e6ea7..dcb85d48 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ActorEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ActorEntity.kt @@ -1,23 +1,40 @@ package ru.dbotthepony.kstarbound.world.entities +import org.classdump.luna.ByteString +import org.classdump.luna.Table import ru.dbotthepony.kommons.util.IStruct2d import ru.dbotthepony.kstarbound.defs.DamageNotification +import ru.dbotthepony.kstarbound.defs.DamageSource import ru.dbotthepony.kstarbound.defs.Drawable +import ru.dbotthepony.kstarbound.defs.InteractAction +import ru.dbotthepony.kstarbound.defs.InteractRequest import ru.dbotthepony.kstarbound.json.builder.IStringSerializable +import ru.dbotthepony.kstarbound.lua.LuaEnvironment +import ru.dbotthepony.kstarbound.lua.from +import ru.dbotthepony.kstarbound.lua.get +import ru.dbotthepony.kstarbound.lua.tableMapOf +import ru.dbotthepony.kstarbound.lua.toJsonFromLua 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 +import ru.dbotthepony.kstarbound.world.entities.api.InteractiveEntity +import ru.dbotthepony.kstarbound.world.entities.api.ScriptedEntity /** * Monsters, NPCs, Players */ -abstract class ActorEntity : DynamicEntity() { +abstract class ActorEntity : DynamicEntity(), InteractiveEntity, ScriptedEntity { final override val movement: ActorMovementController = ActorMovementController() abstract val statusController: StatusController + // ticked manually in final classes, to ensure proper order of operations + val effects = EffectEmitter(this) + + abstract val lua: LuaEnvironment + override fun move(delta: Double) { statusController.applyMovementControls() super.move(delta) @@ -28,11 +45,18 @@ abstract class ActorEntity : DynamicEntity() { DEFAULT("Default"), NONE("None"), SPECIAL("Special") } - open val health: Double + var health: Double get() = statusController.resources["health"]!!.value - open val maxHealth: Double + set(value) { statusController.resources["health"]!!.value = value } + val maxHealth: Double get() = statusController.resources["health"]!!.maxValue!! + val isDead: Boolean + get() = health <= 0.0 + + val isInvulnerable: Boolean + get() = statusController.statPositive("invulnerable") + abstract val damageBarType: DamageBarType override fun onJoinWorld(world: World<*, *>) { @@ -86,4 +110,32 @@ abstract class ActorEntity : DynamicEntity() { open fun portrait(mode: PortraitMode): List { return emptyList() } + + override fun callScript(fnName: String, vararg arguments: Any?): Array { + require(isLocal) { "Calling script on remote entity" } + return lua.invokeGlobal(fnName, *arguments) + } + + override fun evalScript(code: String): Array { + require(isLocal) { "Calling script on remote entity" } + 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/ActorMovementController.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ActorMovementController.kt index 701dfbd3..f6ea0a9e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ActorMovementController.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ActorMovementController.kt @@ -3,18 +3,20 @@ package ru.dbotthepony.kstarbound.world.entities 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.client.freetype.InvalidArgumentException import ru.dbotthepony.kstarbound.defs.ActorMovementParameters import ru.dbotthepony.kstarbound.defs.JumpProfile import ru.dbotthepony.kstarbound.defs.MovementParameters +import ru.dbotthepony.kstarbound.defs.actor.ActorMovementModifiers import ru.dbotthepony.kstarbound.io.nullable import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.math.vector.Vector2d 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.util.GameTimer import ru.dbotthepony.kstarbound.world.Direction +import ru.dbotthepony.kstarbound.world.entities.api.LoungeableEntity import ru.dbotthepony.kstarbound.world.physics.CollisionType import kotlin.math.PI import kotlin.math.absoluteValue @@ -56,7 +58,33 @@ class ActorMovementController() : MovementController() { var isLiquidMovement: Boolean by networkGroup.add(networkedBoolean()) private set - var anchorState by networkGroup.add(networkedData(null, AnchorState.CODEC.nullable(), AnchorState.LEGACY_CODEC.nullable())) + private var _anchorNetworkState by networkGroup.add(networkedData(null, AnchorNetworkState.CODEC.nullable(), AnchorNetworkState.LEGACY_CODEC.nullable())) + var anchorState: IAnchorState? = null + private set + + var anchorNetworkState: AnchorNetworkState? + get() = _anchorNetworkState + set(networkState) { + var state: IAnchorState? = null + + if (networkState != null) { + val entity = world.entities[networkState.entityID] as? LoungeableEntity ?: throw InvalidArgumentException("${networkState.entityID} is not a valid entity or not a loungeable one") + state = entity.anchor(networkState.positionIndex) ?: throw IllegalArgumentException("Anchor with index ${networkState.positionIndex} of $entity is either invalid or disabled") + } + + if (state == null && this.anchorState?.exitBottomPosition != null) { + val boundBox = computeLocalCollisionAABB() + val bottomMid = Vector2d(boundBox.centre.x, boundBox.mins.y) + position = this.anchorState!!.exitBottomPosition!! - bottomMid + } + + this._anchorNetworkState = networkState + this.anchorState = state + + if (state != null) { + position = state.position + } + } var controlJump: Boolean = false var controlJumpAnyway: Boolean = false @@ -136,6 +164,9 @@ class ActorMovementController() : MovementController() { this.isCrouching = isCrouching } + /** + * Absolute in-world position, with account for facing direction and rotation + */ fun getAbsolutePosition(relative: Vector2d): Vector2d { var relativePosition = relative @@ -150,6 +181,19 @@ class ActorMovementController() : MovementController() { return position + relativePosition } + /** + * Relative position with ONLY accounting for facing direction + */ + fun getRelativePosition(relative: Vector2d): Vector2d { + var relativePosition = relative + + if (facingDirection == Direction.LEFT) { + relativePosition *= Vector2d.NEGATIVE_X + } + + return relativePosition + } + fun calculateMovementParameters(base: ActorMovementParameters): MovementParameters { val mass = base.mass val gravityMultiplier = base.gravityMultiplier ?: Globals.movementParameters.gravityMultiplier @@ -294,8 +338,31 @@ class ActorMovementController() : MovementController() { approachVelocityAngles.clear() } + override fun tickRemote(delta: Double) { + val anchorNetworkState = anchorNetworkState + + if (anchorNetworkState == null) + this.anchorState = null + else + this.anchorState = (world.entities[anchorNetworkState.entityID] as? LoungeableEntity)?.anchor(anchorNetworkState.positionIndex) + + super.tickRemote(delta) + } + override fun move(delta: Double) { // TODO: anchor entity + var state: IAnchorState? = null + val anchorNetworkState = anchorNetworkState + + if (anchorNetworkState != null) { + state = (world.entities[anchorNetworkState.entityID] as? LoungeableEntity)?.anchor(anchorNetworkState.positionIndex) + } + + if (state == null) { + this.anchorNetworkState = null + } else { + this.anchorState = state + } if (anchorEntity?.isInWorld != true) anchorEntity = null diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AnchorNetworkState.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AnchorNetworkState.kt new file mode 100644 index 00000000..f2b2808d --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AnchorNetworkState.kt @@ -0,0 +1,22 @@ +package ru.dbotthepony.kstarbound.world.entities + +import ru.dbotthepony.kstarbound.network.syncher.legacyCodec +import ru.dbotthepony.kstarbound.network.syncher.nativeCodec +import ru.dbotthepony.kstarbound.network.syncher.readPointer +import ru.dbotthepony.kstarbound.network.syncher.writePointer +import java.io.DataInputStream +import java.io.DataOutputStream + +data class AnchorNetworkState(val entityID: Int, val positionIndex: Int) { + constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readInt(), stream.readPointer().toInt()) + + fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeInt(entityID) + stream.writePointer(positionIndex) + } + + companion object { + val CODEC = nativeCodec(::AnchorNetworkState, AnchorNetworkState::write) + val LEGACY_CODEC = legacyCodec(::AnchorNetworkState, AnchorNetworkState::write) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AnchorState.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AnchorState.kt index 612e85e8..33044a49 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AnchorState.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AnchorState.kt @@ -1,22 +1,48 @@ package ru.dbotthepony.kstarbound.world.entities -import ru.dbotthepony.kstarbound.network.syncher.legacyCodec -import ru.dbotthepony.kstarbound.network.syncher.nativeCodec -import ru.dbotthepony.kstarbound.network.syncher.readPointer -import ru.dbotthepony.kstarbound.network.syncher.writePointer -import java.io.DataInputStream -import java.io.DataOutputStream +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import ru.dbotthepony.kstarbound.client.render.RenderLayer +import ru.dbotthepony.kstarbound.defs.actor.PersistentStatusEffect +import ru.dbotthepony.kstarbound.math.vector.Vector2d +import ru.dbotthepony.kstarbound.world.Direction -data class AnchorState(val entityID: Int, val positionIndex: Int) { - constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readInt(), stream.readPointer().toInt()) - - fun write(stream: DataOutputStream, isLegacy: Boolean) { - stream.writeInt(entityID) - stream.writePointer(positionIndex) - } - - companion object { - val CODEC = nativeCodec(::AnchorState, AnchorState::write) - val LEGACY_CODEC = legacyCodec(::AnchorState, AnchorState::write) - } +interface IAnchorState { + val position: Vector2d + val exitBottomPosition: Vector2d? + val direction: Direction + val angle: Double } + +data class AnchorState( + override val position: Vector2d, + override val exitBottomPosition: Vector2d?, + override val direction: Direction, + override val angle: Double +) : IAnchorState + +// int32_t +enum class LoungeOrientation(val humanoidState: HumanoidActorEntity.HumanoidState) { + NONE(HumanoidActorEntity.HumanoidState.IDLE), + SIT(HumanoidActorEntity.HumanoidState.SIT), + LAY(HumanoidActorEntity.HumanoidState.LAY), + STAND(HumanoidActorEntity.HumanoidState.IDLE); +} + +data class LoungeAnchorState( + override val position: Vector2d, + override val exitBottomPosition: Vector2d?, + override val direction: Direction, + override val angle: Double, + val orientation: LoungeOrientation = LoungeOrientation.NONE, + val loungeRenderLayer: RenderLayer, + val controllable: Boolean = false, + val statusEffects: List = listOf(), + val effectEmitters: Set = setOf(), + val emote: String? = null, + val dance: String? = null, + val directives: String? = null, + val armorCosmeticOverrides: Map = mapOf(), + val cursorOverride: String? = null, + val cameraFocus: Boolean = false, +) : IAnchorState 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 28067d9d..7537e07c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/DynamicEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/DynamicEntity.kt @@ -23,14 +23,14 @@ abstract class DynamicEntity() : AbstractEntity() { /** * Called in multiple threads */ - open fun move(delta: Double) { + protected open fun move(delta: Double) { movement.move(delta) } /** * Called in multiple threads */ - open fun moveRemote(delta: Double) { + protected open fun moveRemote(delta: Double) { movement.tickRemote(delta) } @@ -44,6 +44,16 @@ abstract class DynamicEntity() : AbstractEntity() { private var fixturesChangeset = -1 + override fun tickParallel(delta: Double) { + super.tickParallel(delta) + + if (isLocal) { + move(delta) + } else { + moveRemote(delta) + } + } + override fun tick(delta: Double) { super.tick(delta) @@ -62,14 +72,12 @@ abstract class DynamicEntity() : AbstractEntity() { override fun onJoinWorld(world: World<*, *>) { super.onJoinWorld(world) - world.dynamicEntities.add(this) movement.initialize(world, spatialEntry) metaFixture = spatialEntry!!.Fixture() } override fun onRemove(world: World<*, *>, reason: RemovalReason) { super.onRemove(world, reason) - world.dynamicEntities.remove(this) movement.remove() metaFixture?.remove() metaFixture = null 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 e8cf89cb..d458537a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/EffectEmitter.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/EffectEmitter.kt @@ -29,11 +29,15 @@ class EffectEmitter(val entity: AbstractEntity) { var direction = Direction.RIGHT fun setSourcePosition(name: String, position: Vector2d) { - + // TODO } - fun tick(isRemote: Boolean, visibleToRemotes: Boolean) { + fun tick(delta: Double) { + // TODO + } + fun addEffectSources(group: String, effects: Collection) { + // TODO } @JsonFactory diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/HumanoidActorEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/HumanoidActorEntity.kt index f7fa48a0..c23faebc 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/HumanoidActorEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/HumanoidActorEntity.kt @@ -3,30 +3,95 @@ package ru.dbotthepony.kstarbound.world.entities import it.unimi.dsi.fastutil.objects.ObjectArrayList import ru.dbotthepony.kommons.util.getValue import ru.dbotthepony.kommons.util.setValue +import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.DamageSource -import ru.dbotthepony.kstarbound.defs.actor.Gender +import ru.dbotthepony.kstarbound.defs.actor.HumanoidConfig +import ru.dbotthepony.kstarbound.defs.actor.HumanoidEmote +import ru.dbotthepony.kstarbound.defs.actor.HumanoidIdentity +import ru.dbotthepony.kstarbound.defs.actor.MoveControlType +import ru.dbotthepony.kstarbound.defs.actor.PersistentStatusEffect +import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor +import ru.dbotthepony.kstarbound.io.NullableBinaryStringCodec +import ru.dbotthepony.kstarbound.item.ItemStack import ru.dbotthepony.kstarbound.item.ToolItem +import ru.dbotthepony.kstarbound.json.builder.IStringSerializable +import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.math.Interpolator +import ru.dbotthepony.kstarbound.network.syncher.NetworkedElement import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup 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.networkedFixedPoint import ru.dbotthepony.kstarbound.network.syncher.networkedItem import ru.dbotthepony.kstarbound.network.syncher.networkedStatefulItem +import ru.dbotthepony.kstarbound.util.GameTimer +import ru.dbotthepony.kstarbound.util.random.nextRange +import ru.dbotthepony.kstarbound.util.valueOf +import ru.dbotthepony.kstarbound.world.entities.api.LoungeableEntity +import kotlin.reflect.KMutableProperty1 /** * Players and NPCs */ -abstract class HumanoidActorEntity() : ActorEntity() { - abstract val aimPosition: Vector2d +abstract class HumanoidActorEntity : ActorEntity() { + abstract var xAimPosition: Double + abstract var yAimPosition: Double - abstract val species: String - abstract val gender: Gender + var energy: Double + get() = statusController.resources["energy"]!!.value + set(value) { statusController.resources["energy"]!!.value = value } + val maxEnergy: Double + get() = statusController.resources["energy"]!!.maxValue!! - val effects = EffectEmitter(this) + var aimPosition: Vector2d + get() = Vector2d(xAimPosition, yAimPosition) + set(value) { + xAimPosition = value.x + yAimPosition = value.y + } + + enum class ItemSlot(override val jsonName: String, val property: KMutableProperty1) : IStringSerializable { + HEAD("head", HumanoidActorEntity::headItem), + HEAD_COSMETIC("headcosmetic", HumanoidActorEntity::headCosmeticItem), + CHEST("chest", HumanoidActorEntity::chestItem), + CHEST_COSMETIC("chestcosmetic", HumanoidActorEntity::chestCosmeticItem), + LEGS("legs", HumanoidActorEntity::legsItem), + LEGS_COSMETIC("legscosmetic", HumanoidActorEntity::legsCosmeticItem), + BACK("back", HumanoidActorEntity::backItem), + BACK_COSMETIC("backcosmetic", HumanoidActorEntity::backCosmeticItem), + PRIMARY("primary", HumanoidActorEntity::primaryHandItem), + ALT("alt", HumanoidActorEntity::secondaryHandItem) { + override fun match(name: String): Boolean { + return super.match(name) || name == "secondary" + } + }; + } + + fun setItem(slot: ItemSlot, item: ItemStack) { + slot.property.set(this, item) + } + + fun setItem(slot: String, item: ItemStack): Boolean { + val lower = slot.lowercase() + setItem(ItemSlot.entries.firstOrNull { it.match(lower) } ?: return false, item.copy()) + return true + } + + fun setItem(slot: String, item: ItemDescriptor): Boolean { + val lower = slot.lowercase() + setItem(ItemSlot.entries.firstOrNull { it.match(lower) } ?: return false, item.build()) + return true + } + + fun getItem(slot: ItemSlot): ItemStack { + return slot.property.get(this) + } // it makes no sense to split ToolUser' logic into separate class + // StarToolUser protected val toolsNetworkGroup = NetworkedGroup() var primaryHandItem by toolsNetworkGroup.add(networkedStatefulItem()) @@ -38,13 +103,74 @@ abstract class HumanoidActorEntity() : ActorEntity() { var primaryItemActive by toolsNetworkGroup.add(networkedBoolean()) var secondaryItemActive by toolsNetworkGroup.add(networkedBoolean()) + fun beginPrimaryFire() { + TODO() + } + + fun beginSecondaryFire() { + TODO() + } + + fun endPrimaryFire() { + TODO() + } + + fun endSecondaryFire() { + TODO() + } + private object ToolFiringInterpolator : Interpolator { override fun interpolate(t: Double, a: Double, b: Double): Double { return if (a >= b) b else Interpolator.Linear.interpolate(t, a, b) } } - // same as above + @JsonFactory + data class ToolSerializedData( + val primaryHandItem: ItemDescriptor = ItemDescriptor.EMPTY, + // backwards compatible name + val altHandItem: ItemDescriptor = ItemDescriptor.EMPTY, + + // original engine will ignore these + val primaryFireTimerNetState: Double = 0.0, + val secondaryFireTimerNetState: Double = 0.0, + val primaryTimeFiringNetState: Double = 0.0, + val secondaryTimeFiringNetState: Double = 0.0, + val primaryItemActive: Boolean = false, + val secondaryItemActive: Boolean = false, + ) + + // to match semantics of original engine, and also because these are called *only* by NPCs + // :upside_down: + protected fun serializeToolUser(): ToolSerializedData { + return ToolSerializedData( + primaryHandItem = primaryHandItem.createDescriptor(), + altHandItem = secondaryHandItem.createDescriptor(), + primaryFireTimerNetState = primaryFireTimerNetState, + secondaryFireTimerNetState = secondaryFireTimerNetState, + primaryTimeFiringNetState = primaryTimeFiringNetState, + secondaryTimeFiringNetState = secondaryTimeFiringNetState, + primaryItemActive = primaryItemActive, + secondaryItemActive = secondaryItemActive, + ) + } + + protected fun deserializeToolUser(data: ToolSerializedData) { + primaryHandItem = data.primaryHandItem.build() + secondaryHandItem = data.altHandItem.build() + primaryFireTimerNetState = data.primaryFireTimerNetState + secondaryFireTimerNetState = data.secondaryFireTimerNetState + primaryTimeFiringNetState = data.primaryTimeFiringNetState + secondaryTimeFiringNetState = data.secondaryTimeFiringNetState + primaryItemActive = data.primaryItemActive + secondaryItemActive = data.secondaryItemActive + } + + protected fun queryShieldHit(damage: DamageSource): Boolean { + TODO() + } + + // StarArmorUser protected val armorNetworkGroup = NetworkedGroup() var headItem by armorNetworkGroup.add(networkedItem()) @@ -56,6 +182,46 @@ abstract class HumanoidActorEntity() : ActorEntity() { var legsCosmeticItem by armorNetworkGroup.add(networkedItem()) var backCosmeticItem by armorNetworkGroup.add(networkedItem()) + @JsonFactory + data class ArmorSerializedData( + val headItem: ItemDescriptor = ItemDescriptor.EMPTY, + val chestItem: ItemDescriptor = ItemDescriptor.EMPTY, + val legsItem: ItemDescriptor = ItemDescriptor.EMPTY, + val backItem: ItemDescriptor = ItemDescriptor.EMPTY, + val headCosmeticItem: ItemDescriptor = ItemDescriptor.EMPTY, + val chestCosmeticItem: ItemDescriptor = ItemDescriptor.EMPTY, + val legsCosmeticItem: ItemDescriptor = ItemDescriptor.EMPTY, + val backCosmeticItem: ItemDescriptor = ItemDescriptor.EMPTY, + ) + + // to match semantics of original engine, and also because these are called *only* by NPCs + // :upside_down: + protected fun serializeArmorUser(): ArmorSerializedData { + return ArmorSerializedData( + headItem = headItem.createDescriptor(), + chestItem = chestItem.createDescriptor(), + legsItem = legsItem.createDescriptor(), + backItem = backItem.createDescriptor(), + headCosmeticItem = headCosmeticItem.createDescriptor(), + chestCosmeticItem = chestCosmeticItem.createDescriptor(), + legsCosmeticItem = legsCosmeticItem.createDescriptor(), + backCosmeticItem = backCosmeticItem.createDescriptor(), + ) + } + + protected fun deserializeArmorUser(data: ArmorSerializedData) { + headItem = data.headItem.build() + chestItem = data.chestItem.build() + legsItem = data.legsItem.build() + backItem = data.backItem.build() + headCosmeticItem = data.headCosmeticItem.build() + chestCosmeticItem = data.chestCosmeticItem.build() + legsCosmeticItem = data.legsCosmeticItem.build() + backCosmeticItem = data.backCosmeticItem.build() + } + + // --- + override val damageSources: List get() { val damageSources = ObjectArrayList() val primaryHandItem = primaryHandItem @@ -66,4 +232,205 @@ abstract class HumanoidActorEntity() : ActorEntity() { return damageSources } + + // StarHumanoid + abstract val humanoidIdentity: HumanoidIdentity + abstract val humanoidConfig: HumanoidConfig + + final override val mouthPosition: Vector2d + get() = movement.getAbsolutePosition(humanoidConfig.mouthOffset) + + final override val feetPosition: Vector2d + get() = movement.getAbsolutePosition(humanoidConfig.feetOffset) + + // TODO: Original engine does not account for rotation here, was this intended, or did we fix a bug? + val headArmorPosition: Vector2d + get() = movement.getAbsolutePosition(humanoidConfig.headArmorOffset) + + // TODO: Original engine does not account for rotation here, was this intended, or did we fix a bug? + val chestArmorPosition: Vector2d + get() = movement.getAbsolutePosition(humanoidConfig.chestArmorOffset) + + // TODO: Original engine does not account for rotation here, was this intended, or did we fix a bug? + val legsArmorPosition: Vector2d + get() = movement.getAbsolutePosition(humanoidConfig.legsArmorOffset) + + // TODO: Original engine does not account for rotation here, was this intended, or did we fix a bug? + val backArmorPosition: Vector2d + get() = movement.getAbsolutePosition(humanoidConfig.backArmorOffset) + + inner class Hand { + var angle = 0.0 + } + + var isTwoHanded = false + val primaryHand = Hand() + val secondaryHand = Hand() + + var isMovingBackwards = false + + enum class HumanoidState(override val jsonName: String) : IStringSerializable { + IDLE("idle"), // 1 idle frame + WALK("walk"), // 8 walking frames + RUN("run"), // 8 run frames + JUMP("jump"), // 4 jump frames + FALL("fall"), // 4 fall frames + SWIM("swim"), // 7 swim frames + SWIM_IDLE("swimIdle"), // 2 swim idle frame + DUCK("duck"), // 1 ducking frame + SIT("sit"), // 1 sitting frame + LAY("lay"); // 1 laying frame + } + + protected val humanoidNetStates = ArrayList() + var humanoidState by networkedEnum(HumanoidState.entries).also { humanoidNetStates.add(it) } + var humanoidEmote by networkedEnum(HumanoidEmote.entries).also { humanoidNetStates.add(it) } + var humanoidDance by networkedData(null, NullableBinaryStringCodec).also { humanoidNetStates.add(it) } + + protected fun inConflictingLoungeAnchor(): Boolean { + val state = movement.anchorNetworkState + + if (state != null) { + val entity = world.entities[state.entityID] as? LoungeableEntity + + if (entity != null) { + val loungingIn = entity.entitiesLoungingIn(state.positionIndex) + return loungingIn.size > 1 || this !in loungingIn + } + } + + return false + } + + /** + * Whenever to display any armor on wearer + */ + open val forceNude: Boolean + get() = false + + open val suppressUseOfTools: Boolean + get() = false + + /** + * Whenever worn armor has effect on stats + * + * If false, armor will always behave like cosmetic one, + * no matter in which slot it is worn in + */ + open val disableWornArmor: Boolean + get() = false + + protected abstract val emoteCooldownTimer: GameTimer + protected abstract val danceTimer: GameTimer? + protected val blinkTimer = GameTimer() + protected abstract val blinkInterval: Vector2d + + fun setDance(dance: String?) { + ensureIsLocal() + this.humanoidDance = dance + + if (dance != null) { + danceTimer?.reset(Registries.dance.getOrThrow(dance).value.duration) + } + } + + fun addEmote(emote: String) { + return addEmote(HumanoidEmote.entries.valueOf(emote)) + } + + fun addEmote(emote: HumanoidEmote) { + this.humanoidEmote = emote + this.emoteCooldownTimer.reset() + } + + fun requestEmote(emote: String) { + if (emote.isNotBlank()) { + return requestEmote(HumanoidEmote.entries.valueOf(emote)) + } + } + + fun requestEmote(emote: HumanoidEmote) { + if (emote != HumanoidEmote.IDLE && (humanoidEmote == HumanoidEmote.IDLE || humanoidEmote == HumanoidEmote.BLINK)) { + addEmote(emote) + } + } + + override fun tickParallel(delta: Double) { + if (isLocal) { + if (!forceNude) { + if (headCosmeticItem.isNotEmpty) + effects.addEffectSources("headArmor", headCosmeticItem.effects) + else if (headItem.isNotEmpty) + effects.addEffectSources("headArmor", headItem.effects) + + if (chestCosmeticItem.isNotEmpty) + effects.addEffectSources("chestArmor", chestCosmeticItem.effects) + else if (chestItem.isNotEmpty) + effects.addEffectSources("chestArmor", chestItem.effects) + + if (legsCosmeticItem.isNotEmpty) + effects.addEffectSources("legsArmor", legsCosmeticItem.effects) + else if (legsItem.isNotEmpty) + effects.addEffectSources("legsArmor", legsItem.effects) + + if (backCosmeticItem.isNotEmpty) + effects.addEffectSources("backArmor", backCosmeticItem.effects) + else if (backItem.isNotEmpty) + effects.addEffectSources("backArmor", backItem.effects) + } + + if (!disableWornArmor) { + val effects = ArrayList() + effects.addAll(headItem.statusEffects) + effects.addAll(chestItem.statusEffects) + effects.addAll(legsItem.statusEffects) + effects.addAll(backItem.statusEffects) + statusController.setPersistentEffects("armor", effects) + } + + if (!suppressUseOfTools) { + effects.addEffectSources("primary", primaryHandItem.effects) + effects.addEffectSources("alt", secondaryHandItem.effects) + + val effects = ArrayList() + effects.addAll(primaryHandItem.statusEffects) + effects.addAll(secondaryHandItem.statusEffects) + statusController.setPersistentEffects("tools", effects) + } + + if (emoteCooldownTimer.tick(delta)) + humanoidEmote = HumanoidEmote.IDLE + } + + super.tickParallel(delta) + + effects.setSourcePosition("normal", movement.position) + effects.setSourcePosition("mouth", mouthPosition) + effects.setSourcePosition("feet", feetPosition) + effects.setSourcePosition("headArmor", headArmorPosition) + effects.setSourcePosition("chestArmor", chestArmorPosition) + effects.setSourcePosition("legsArmor", legsArmorPosition) + effects.setSourcePosition("backArmor", backArmorPosition) + + // TODO: HERE: humanoid.animate + } + + protected fun tickTools(delta: Double, controls: Set) { + // TODO + } + + override fun tick(delta: Double) { + super.tick(delta) + + if (isLocal) { + if (blinkTimer.tick(delta)) { + blinkTimer.reset(world.random.nextRange(blinkInterval)) + val anchor = movement.anchorState as? LoungeAnchorState + + if (humanoidEmote == HumanoidEmote.IDLE && anchor?.emote == null) { + addEmote(HumanoidEmote.BLINK) + } + } + } + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MonsterEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MonsterEntity.kt index c01ecd4f..d69d8cd9 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MonsterEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MonsterEntity.kt @@ -70,12 +70,11 @@ 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 { +class MonsterEntity(val variant: MonsterVariant, level: Double? = null) : ActorEntity() { override val type: EntityType get() = EntityType.MONSTER @@ -83,7 +82,7 @@ class MonsterEntity(val variant: MonsterVariant, level: Double? = null) : ActorE team.accept(EntityDamageTeam(variant.commonParameters.damageTeamType, variant.commonParameters.damageTeam)) } - val lua = LuaEnvironment() + override val lua = LuaEnvironment() val luaUpdate = LuaUpdateComponent(lua) val luaMovement = MovementControllerBindings(movement) val luaMessages = LuaMessageHandlerComponent(lua) { toString() } @@ -188,7 +187,7 @@ class MonsterEntity(val variant: MonsterVariant, level: Double? = null) : ActorE dropPool, uniqueID.value, team.value, - effectEmitter = effectEmitter.serialize(), + effectEmitter = effects.serialize(), scriptStorage = toJsonFromLua(lua.globals["storage"]), ) @@ -231,9 +230,9 @@ class MonsterEntity(val variant: MonsterVariant, level: Double? = null) : ActorE networkGroup.upstream.add(animator.networkGroup) networkGroup.upstream.add(movement.networkGroup) networkGroup.upstream.add(statusController) - } - val effectEmitter = EffectEmitter(this).also { networkGroup.upstream.add(it.networkGroup) } + networkGroup.upstream.add(effects.networkGroup) + } val newChatMessageEvent = networkedEventCounter().also { networkGroup.upstream.add(it) } var chatMessage by networkedString().also { networkGroup.upstream.add(it) } @@ -319,6 +318,7 @@ class MonsterEntity(val variant: MonsterVariant, level: Double? = null) : ActorE val notifications = statusController.experienceDamage(damage.request) val totalDamage = notifications.sumOf { it.healthLost } + // TODO: hitDamageNotificationLimiter++ < Globals.npcs.hitDamageNotificationLimit maybe? if (totalDamage > 0.0) { lua.invokeGlobal("damage", lua.tableMapOf( "sourceId" to damage.request.sourceEntityId, @@ -340,12 +340,29 @@ class MonsterEntity(val variant: MonsterVariant, level: Double? = null) : ActorE return result.isNotEmpty() && result[0] is Boolean && result[0] as Boolean || health <= 0.0 //|| lua.errorState } + override fun tickParallel(delta: Double) { + effects.setSourcePosition("normal", position) + effects.setSourcePosition("mouth", mouthPosition) + effects.setSourcePosition("feet", mouthPosition) + effects.direction = movement.facingDirection + + super.tickParallel(delta) + + if (isLocal) { + animator.isFlipped = (movement.facingDirection == Direction.LEFT) != variant.reversed + } + + if (world.isServer) { + animator.tick(delta, world.random) + } + + effects.tick(delta) + } + override fun tick(delta: Double) { super.tick(delta) if (isLocal) { - animator.isFlipped = (movement.facingDirection == Direction.LEFT) != variant.reversed - if (knockedOut) { knockoutTimer -= delta @@ -394,45 +411,9 @@ class MonsterEntity(val variant: MonsterVariant, level: Double? = null) : ActorE } } } - - 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/NPCEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/NPCEntity.kt index c5826e25..b69efc98 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/NPCEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/NPCEntity.kt @@ -1,35 +1,432 @@ package ru.dbotthepony.kstarbound.world.entities +import com.google.common.collect.ImmutableList +import com.google.common.collect.ImmutableSet +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.google.gson.JsonPrimitive +import org.classdump.luna.ByteString +import org.classdump.luna.Table +import ru.dbotthepony.kommons.collect.filterNotNull +import ru.dbotthepony.kommons.gson.set +import ru.dbotthepony.kommons.util.getValue +import ru.dbotthepony.kommons.util.setValue +import ru.dbotthepony.kstarbound.Globals +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.actor.HumanoidConfig +import ru.dbotthepony.kstarbound.defs.actor.HumanoidEmote +import ru.dbotthepony.kstarbound.defs.actor.HumanoidIdentity +import ru.dbotthepony.kstarbound.defs.actor.NPCVariant +import ru.dbotthepony.kstarbound.defs.item.TreasurePoolDefinition +import ru.dbotthepony.kstarbound.defs.quest.QuestArcDescriptor +import ru.dbotthepony.kstarbound.fromJsonFast +import ru.dbotthepony.kstarbound.io.BinaryStringCodec +import ru.dbotthepony.kstarbound.io.NullableBinaryStringCodec +import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.json.mergeJson +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.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.tableOf +import ru.dbotthepony.kstarbound.lua.toJsonFromLua +import ru.dbotthepony.kstarbound.lua.userdata.BehaviorState 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.EventCounterElement +import ru.dbotthepony.kstarbound.network.syncher.FloatingNetworkedElement +import ru.dbotthepony.kstarbound.network.syncher.NetworkedList +import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean +import ru.dbotthepony.kstarbound.network.syncher.networkedData +import ru.dbotthepony.kstarbound.network.syncher.networkedJsonElement +import ru.dbotthepony.kstarbound.network.syncher.networkedString +import ru.dbotthepony.kstarbound.util.GameTimer +import ru.dbotthepony.kstarbound.util.random.staticRandomInt +import ru.dbotthepony.kstarbound.util.valueOf +import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.entities.api.InteractiveEntity import ru.dbotthepony.kstarbound.world.entities.api.ScriptedEntity import java.io.DataOutputStream +import kotlin.math.absoluteValue -class NPCEntity : ActorEntity(), InteractiveEntity, ScriptedEntity { +class NPCEntity(val variant: NPCVariant) : HumanoidActorEntity() { override val type: EntityType get() = EntityType.NPC - override val statusController: StatusController - get() = TODO("Not yet implemented") override val damageBarType: DamageBarType - get() = TODO("Not yet implemented") + get() = DamageBarType.DEFAULT override fun writeNetwork(stream: DataOutputStream, isLegacy: Boolean) { - TODO("Not yet implemented") + variant.write(stream, isLegacy) } override val metaBoundingBox: AABB - get() = TODO("Not yet implemented") + get() = AABB.withSide(position, 4.0, 4.0) override val name: String - get() = TODO("Not yet implemented") + get() = variant.humanoidIdentity.name override val description: String - get() = TODO("Not yet implemented") + get() = "Some funny looking person" // FIXME: this is how it is in original engine - override fun callScript(fnName: String, vararg arguments: Any?): Array { - TODO("Not yet implemented") + override val humanoidIdentity: HumanoidIdentity + get() = variant.humanoidIdentity + override val humanoidConfig: HumanoidConfig + get() = variant.humanoidConfig + + override val blinkInterval: Vector2d + get() = Globals.npcs.blinkInterval + + override var xAimPosition by networkGroup.upstream.add(FloatingNetworkedElement.fixed(0.0625).also { it.interpolator = Interpolator.Linear }) + override var yAimPosition by networkGroup.upstream.add(FloatingNetworkedElement.fixed(0.0625).also { it.interpolator = Interpolator.Linear }) + + init { + networkGroup.upstream.add(uniqueID) + networkGroup.upstream.add(team) + humanoidNetStates.forEach { networkGroup.upstream.add(it) } } - override fun evalScript(code: String): Array { - TODO("Not yet implemented") + val newChatMessageEvent = networkGroup.upstream.add(EventCounterElement()) + var chatMessage by networkGroup.upstream.add(networkedString()) + var chatPortrait by networkGroup.upstream.add(networkedString()) + var chatConfig by networkGroup.upstream.add(networkedJsonElement()) + + var statusText by networkGroup.upstream.add(networkedData(null, NullableBinaryStringCodec)) + var displayNametag by networkGroup.upstream.add(networkedBoolean()) + override var isInteractive by networkGroup.upstream.add(networkedBoolean()) + + val offeredQuests = networkGroup.upstream.add(NetworkedList(QuestArcDescriptor.CODEC, QuestArcDescriptor.LEGACY_CODEC)) + val turnInQuests = networkGroup.upstream.add(NetworkedList(BinaryStringCodec)) + + var isShifting by networkGroup.upstream.add(networkedBoolean()) + var damageOnTouch by networkGroup.upstream.add(networkedBoolean()) + override var disableWornArmor by networkGroup.upstream.add(networkedBoolean(variant.disableWornArmor)) + var deathParticleBurst by networkGroup.upstream.add(networkedData(variant.humanoidConfig.deathParticles, NullableBinaryStringCodec)) + + val dropPools = networkGroup.upstream.add(NetworkedList(Registries.treasurePools.nameRefCodec)) + var isAggressive by networkGroup.upstream.add(networkedBoolean()) + + override val statusController = StatusController(this, variant.statusControllerSettings) + + override val emoteCooldownTimer = GameTimer(Globals.npcs.emoteCooldown) + override val danceTimer = GameTimer() + + init { + networkGroup.upstream.add(movement.networkGroup) + networkGroup.upstream.add(effects.networkGroup) + networkGroup.upstream.add(statusController) + networkGroup.upstream.add(armorNetworkGroup) + networkGroup.upstream.add(toolsNetworkGroup) + + dropPools.addAll(variant.dropPools) + team.accept(variant.team) + + movement.applyParameters(variant.movementParameters) + + if (variant.movementParameters.physicsEffectCategories == null) { + movement.applyParameters(ActorMovementParameters(physicsEffectCategories = ImmutableSet.of("npc"))) + } + + statusController.addPersistentEffects("innate", variant.innateStatusEffects) + statusController.addPersistentEffects("species", variant.species.value.buildStatusEffects()) + statusController.setProperty("species", JsonPrimitive(variant.species.key)) + + if (!statusController.hasProperty("effectDirectives")) + statusController.setProperty("effectDirectives", JsonPrimitive(variant.species.value.effectDirectives)) } + + private var hitDamageNotificationLimiter = 0 + override var isPersistent: Boolean = variant.persistent + override var keepAlive: Boolean = variant.keepAlive + + override val lua = LuaEnvironment() + val luaUpdate = LuaUpdateComponent(lua) + val luaMovement = MovementControllerBindings(movement) + val luaMessages = LuaMessageHandlerComponent(lua) { toString() } + + init { + lua.globals["storage"] = lua.tableOf() + } + + 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 move(delta: Double) { + luaMovement.apply() + + // This differs from original engine because it may allow 1 tick long conflicting + // lounging even for local entities because they are updated in parallel + // Shouldn't cause issues considering such situation is also possible on original engine + // when seat is contested in multiplayer by two players + if (isLocal && inConflictingLoungeAnchor()) + movement.anchorNetworkState = null + + super.move(delta) + } + + @JsonFactory + data class SerializedData( + // val npcVariant: NPCVariant, + val movementController: ActorMovementController.SerializedData, + val statusController: StatusController.SerializedData, + val aimPosition: Vector2d = Vector2d.ZERO, + val humanoidState: HumanoidState = HumanoidState.IDLE, + val humanoidEmoteState: HumanoidEmote = HumanoidEmote.IDLE, + val isInteractive: Boolean = false, + val shifting: Boolean = false, + val damageOnTouch: Boolean = false, + val effectEmitter: JsonElement, + val armor: ArmorSerializedData = ArmorSerializedData(), + val tools: ToolSerializedData = ToolSerializedData(), + val disableWornArmor: Boolean, + val uniqueId: String? = null, + val team: EntityDamageTeam? = null, + val deathParticleBurst: String? = null, + val dropPools: ImmutableList> = ImmutableList.of(), + val aggressive: Boolean = false, + ) + + override fun deserialize(data: JsonObject) { + super.deserialize(data) + + val serialized = Starbound.gson.fromJsonFast(data, SerializedData::class.java) + + movement.deserialize(serialized.movementController) + statusController.deserialize(serialized.statusController) + aimPosition = serialized.aimPosition + humanoidState = serialized.humanoidState + humanoidEmote = serialized.humanoidEmoteState + isInteractive = serialized.isInteractive + isShifting = serialized.shifting + damageOnTouch = serialized.damageOnTouch + + deserializeArmorUser(serialized.armor) + deserializeToolUser(serialized.tools) + + disableWornArmor = serialized.disableWornArmor + uniqueID.accept(serialized.uniqueId) + + if (serialized.team != null) + team.accept(serialized.team) + + deathParticleBurst = serialized.deathParticleBurst + dropPools.clear() + dropPools.addAll(serialized.dropPools.filter { it.isPresent }) + isAggressive = serialized.aggressive + blinkTimer.reset(0.0) + } + + override fun serialize(data: JsonObject) { + super.serialize(data) + + // Very ugly + data["npcVariant"] = Starbound.gson.toJsonTree(variant) + + val serialized = SerializedData( + movementController = movement.serialize(), + statusController = statusController.serialize(), + aimPosition = aimPosition, + humanoidState = humanoidState, + humanoidEmoteState = humanoidEmote, + isInteractive = isInteractive, + shifting = isShifting, + damageOnTouch = damageOnTouch, + effectEmitter = effects.serialize(), + armor = serializeArmorUser(), + tools = serializeToolUser(), + disableWornArmor = disableWornArmor, + uniqueId = uniqueID.get(), + team = team.get(), + deathParticleBurst = deathParticleBurst, + dropPools = dropPools.stream().filter { it.isPresent }.collect(ImmutableList.toImmutableList()), + aggressive = isAggressive, + ) + + mergeJson(data, Starbound.gson.toJsonTree(serialized)) + } + + override fun onJoinWorld(world: World<*, *>) { + super.onJoinWorld(world) + + if (!isRemote) { + for ((slot, item) in variant.items) { + setItem(slot, item) + } + + provideEntityBindings(this, lua) + + provideConfigBindings(lua) { key, default -> + key.find(variant.scriptConfig) ?: default + } + + BehaviorState.provideBindings(lua) + + luaUpdate.stepCount = variant.initialScriptDelta + lua.attach(variant.scripts) + + luaMovement.init(lua) + lua.init() + } + } + + fun addChatMessage(message: String, config: JsonElement, portrait: String = "") { + chatMessage = message + chatPortrait = portrait + chatConfig = config + newChatMessageEvent.trigger() + } + + override fun potentiallyCanBeHit( + source: DamageSource, + attacker: AbstractEntity?, + inflictor: AbstractEntity? + ): Boolean { + return !isInvulnerable && !isDead + } + + override fun queryHit(source: DamageSource, attacker: AbstractEntity?, inflictor: AbstractEntity?): HitType? { + if (isInvulnerable || isDead) + return null + + if (queryShieldHit(source)) + return HitType.SHIELD_HIT + + if (source.intersect(world.geometry, movement.computeGlobalHitboxes())) + return HitType.HIT + + return null + } + + override fun experienceDamage(damage: DamageRequestPacket): List { + val notifications = statusController.experienceDamage(damage.request) + val totalDamage = notifications.sumOf { it.healthLost } + + if (totalDamage > 0.0 && hitDamageNotificationLimiter++ < Globals.npcs.hitDamageNotificationLimit) { + lua.invokeGlobal("damage", lua.tableMapOf( + "sourceId" to damage.request.sourceEntityId, + "damage" to totalDamage, + "sourceDamage" to damage.request.damage, + "sourceKind" to damage.request.damageSourceKind + )) + } + + return notifications + } + + override fun tickParallel(delta: Double) { + super.tickParallel(delta) + + if (hitDamageNotificationLimiter > 0) + hitDamageNotificationLimiter-- + + // HERE: movementSuppressed/runningSuppressed by FireableItem + // Implementing these skipped because they don't actually apply these suppressions to movement controller + // because this logic was moved to Lua scripts + + if (isLocal) { + val anchor = movement.anchorState + + if (anchor is LoungeAnchorState) { + if (anchor.emote != null) { + requestEmote(anchor.emote) + } + + humanoidState = anchor.orientation.humanoidState + statusController.setPersistentEffects("lounging", anchor.statusEffects) + effects.addEffectSources("normal", anchor.effectEmitters) + } else { + statusController.removePersistentEffects("lounging") + + if (movement.isGroundMovement) { + if (movement.isRunning) + humanoidState = HumanoidState.RUN + else if (movement.isWalking) + humanoidState = HumanoidState.WALK + else if (movement.isCrouching) + humanoidState = HumanoidState.DUCK + else + humanoidState = HumanoidState.IDLE + } else if (movement.isLiquidMovement) { + if (movement.velocity.dot(movement.determineGravity().unitVector).absoluteValue != 1.0) + humanoidState = HumanoidState.SWIM + else + humanoidState = HumanoidState.SWIM_IDLE + } else { + if (movement.velocity.dot(movement.determineGravity().unitVector).absoluteValue > 0.01) + humanoidState = HumanoidState.JUMP + else + humanoidState = HumanoidState.FALL + } + } + + if (danceTimer.tick(delta)) + humanoidDance = null + + effects.tick(delta) + } + + // TODO: HERE: setupHumanoidClothingDrawables + } + + override fun tick(delta: Double) { + super.tick(delta) + + if (!isRemote) { + if (isDead || lua.errorState) { + remove(RemovalReason.DYING) + return + } else { + val shouldDie = lua.invokeGlobal("shouldDie") + + if (shouldDie.isNotEmpty() && shouldDie[0] is Boolean && shouldDie[0] as Boolean) { + remove(RemovalReason.DYING) + return + } + } + + // luaUpdate.update(delta) + } + + tickTools(delta, setOf()) + } + + override fun onRemove(world: World<*, *>, reason: RemovalReason) { + super.onRemove(world, reason) + + lua.invokeGlobal("die") + + val dropPools by lazy { dropPools.stream().map { it.entry }.filterNotNull().toList() } + + if (!isRemote && reason.dying && dropPools.isNotEmpty()) { + // TODO: what. + val pool = dropPools[staticRandomInt(0, dropPools.size - 1, variant.seed)] + + for (item in pool.value.evaluate(world.random, variant.level)) { + val entity = ItemDropEntity(item) + entity.position = position + entity.movement.xVelocity += world.random.nextDouble(-10.0, 10.0) + entity.movement.yVelocity += world.random.nextDouble(-10.0, 10.0) + entity.joinWorld(world) + } + } + } + + } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/StatusController.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/StatusController.kt index d8b32784..01695feb 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/StatusController.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/StatusController.kt @@ -7,7 +7,6 @@ import com.google.gson.JsonElement import com.google.gson.JsonObject import it.unimi.dsi.fastutil.ints.Int2ObjectAVLTreeMap import it.unimi.dsi.fastutil.ints.IntArrayList -import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet import org.apache.logging.log4j.LogManager import org.classdump.luna.ByteString @@ -16,9 +15,8 @@ import org.classdump.luna.Table import ru.dbotthepony.kommons.collect.ListenableMap import ru.dbotthepony.kommons.collect.collect import ru.dbotthepony.kommons.collect.map +import ru.dbotthepony.kommons.gson.contains import ru.dbotthepony.kommons.gson.set -import ru.dbotthepony.kstarbound.io.IntValueCodec -import ru.dbotthepony.kstarbound.io.StreamCodec import ru.dbotthepony.kommons.io.readKOptional import ru.dbotthepony.kommons.io.writeBinaryString import ru.dbotthepony.kommons.io.writeKOptional @@ -39,6 +37,8 @@ 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.fromJsonFast +import ru.dbotthepony.kstarbound.io.IntValueCodec +import ru.dbotthepony.kstarbound.io.StreamCodec import ru.dbotthepony.kstarbound.io.nullable import ru.dbotthepony.kstarbound.io.readInternedString import ru.dbotthepony.kstarbound.json.builder.JsonFactory @@ -50,7 +50,6 @@ import ru.dbotthepony.kstarbound.lua.bindings.createConfigBinding 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.bindings.provideWorldBindings import ru.dbotthepony.kstarbound.lua.from import ru.dbotthepony.kstarbound.lua.iterator import ru.dbotthepony.kstarbound.lua.luaFunction @@ -59,8 +58,8 @@ import ru.dbotthepony.kstarbound.lua.toJsonFromLua import ru.dbotthepony.kstarbound.math.Interpolator import ru.dbotthepony.kstarbound.network.packets.DamageNotificationPacket import ru.dbotthepony.kstarbound.network.syncher.NetworkedDynamicGroup -import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup import ru.dbotthepony.kstarbound.network.syncher.NetworkedElement +import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup import ru.dbotthepony.kstarbound.network.syncher.NetworkedMap import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean import ru.dbotthepony.kstarbound.network.syncher.networkedData @@ -71,11 +70,10 @@ import ru.dbotthepony.kstarbound.network.syncher.networkedString import ru.dbotthepony.kstarbound.util.GameTimer import ru.dbotthepony.kstarbound.util.HistoryQueue import ru.dbotthepony.kstarbound.util.sbIntern -import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.entities.api.StatusEffectEntity import java.io.DataInputStream import java.io.DataOutputStream -import java.util.Collections +import java.util.* import java.util.stream.Collectors // this is unnatural to have this class separated, but since it contains @@ -234,6 +232,10 @@ class StatusController(val entity: ActorEntity, val config: StatusControllerConf return statusProperties.value[key] } + fun hasProperty(key: String): Boolean { + return key in statusProperties.value + } + var parentDirectives by networkedString().also { networkGroup.add(it) } private set @@ -725,7 +727,7 @@ class StatusController(val entity: ActorEntity, val config: StatusControllerConf // 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 { + sealed class EffectiveStat { abstract val baseValue: Double // Value with just the base percent modifiers applied and the value @@ -736,7 +738,7 @@ class StatusController(val entity: ActorEntity, val config: StatusControllerConf abstract val effectiveModifiedValue: Double } - private class LiveStatImpl : LiveStat() { + private class EffectiveStatImpl : EffectiveStat() { override var baseValue: Double = 0.0 var tickVisited = -1 @@ -854,8 +856,7 @@ class StatusController(val entity: ActorEntity, val config: StatusControllerConf } } - // in original code it is named effectiveStats - private val liveStatsInternal = HashMap() + private val effectiveStatsInternal = HashMap() private val statModifiersNetworkMap = NetworkedMap( IntValueCodec, @@ -865,13 +866,13 @@ class StatusController(val entity: ActorEntity, val config: StatusControllerConf private val statModifiers = IdMap(map = statModifiersNetworkMap) private val resourcesInternal = HashMap() - val liveStats: Map = Collections.unmodifiableMap(liveStatsInternal) + val effectiveStats: Map = Collections.unmodifiableMap(effectiveStatsInternal) val resources: Map = Collections.unmodifiableMap(resourcesInternal) init { for ((statName, stat) in config.stats) { - val live = LiveStatImpl() - liveStatsInternal[statName] = live + val live = EffectiveStatImpl() + effectiveStatsInternal[statName] = live live.baseValue = stat.baseValue live.baseModifiedValue = stat.baseValue } @@ -917,7 +918,7 @@ class StatusController(val entity: ActorEntity, val config: StatusControllerConf } fun statPositive(name: String): Boolean { - return (liveStatsInternal[name]?.effectiveModifiedValue ?: 0.0) > 0.0 + return (effectiveStatsInternal[name]?.effectiveModifiedValue ?: 0.0) > 0.0 } private var statsTick = 0 @@ -933,7 +934,7 @@ class StatusController(val entity: ActorEntity, val config: StatusControllerConf statsTick++ for ((statName, stat) in config.stats) { - val live = liveStatsInternal[statName]!! + val live = effectiveStatsInternal[statName]!! live.baseValue = stat.baseValue live.baseModifiedValue = stat.baseValue live.tickVisited = statsTick @@ -941,11 +942,11 @@ class StatusController(val entity: ActorEntity, val config: StatusControllerConf for (group in statModifiers.values) { for (modifier in group) { - var live = liveStatsInternal[modifier.stat] + var live = effectiveStatsInternal[modifier.stat] if (live == null) { - live = LiveStatImpl() - liveStatsInternal[modifier.stat] = live + live = EffectiveStatImpl() + effectiveStatsInternal[modifier.stat] = live } live.tickVisited = statsTick @@ -961,12 +962,12 @@ class StatusController(val entity: ActorEntity, val config: StatusControllerConf // Then we do all the StatEffectiveMultipliers and compute the // final effectiveModifiedValue - for (value in liveStatsInternal.values) + for (value in effectiveStatsInternal.values) value.effectiveModifiedValue = value.baseModifiedValue for (group in statModifiers.values) { for (modifier in group) { - val live = liveStatsInternal[modifier.stat]!! + val live = effectiveStatsInternal[modifier.stat]!! if (modifier.type == StatModifierType.OVERALL_MULTIPLICATION) { live.effectiveModifiedValue *= modifier.value @@ -976,7 +977,7 @@ class StatusController(val entity: ActorEntity, val config: StatusControllerConf } } - for (live in liveStatsInternal.values) { + for (live in effectiveStatsInternal.values) { if (live.tickVisited != statsTick) { live.baseValue = 0.0 live.effectiveModifiedValue = 0.0 @@ -989,7 +990,7 @@ class StatusController(val entity: ActorEntity, val config: StatusControllerConf for (resource in resourcesInternal.values) { val oldMax = resource.maxValue - resource.maxValue = resource.max?.map({ liveStatsInternal[it]?.effectiveModifiedValue }, { it }) + resource.maxValue = resource.max?.map({ effectiveStatsInternal[it]?.effectiveModifiedValue }, { it }) // If the resource has a maximum value, rather than keeping the absolute // value of the resource the same between updates, the resource value @@ -1003,7 +1004,7 @@ class StatusController(val entity: ActorEntity, val config: StatusControllerConf } if (delta > 0.0) { - resource.value += delta * (resource.delta?.map({ liveStatsInternal[it]?.effectiveModifiedValue }, { it }) ?: 0.0) + resource.value += delta * (resource.delta?.map({ effectiveStatsInternal[it]?.effectiveModifiedValue }, { it }) ?: 0.0) } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/api/LoungeableEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/api/LoungeableEntity.kt index 57140e96..311e64ac 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/api/LoungeableEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/api/LoungeableEntity.kt @@ -1,4 +1,19 @@ package ru.dbotthepony.kstarbound.world.entities.api +import ru.dbotthepony.kstarbound.math.vector.Vector2d +import ru.dbotthepony.kstarbound.world.entities.ActorEntity +import ru.dbotthepony.kstarbound.world.entities.IAnchorState + interface LoungeableEntity { + val sitPositions: List + + /** + * Determines entities currently lounging at specified anchor index + */ + fun entitiesLoungingIn(index: Int): List + + /** + * Returns anchor information with given index, or null if index is invalid + */ + fun anchor(index: Int): IAnchorState? } 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 6865280d..fc8700ee 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 @@ -1,6 +1,7 @@ package ru.dbotthepony.kstarbound.world.entities.player import com.google.gson.JsonObject +import kotlinx.coroutines.runBlocking import ru.dbotthepony.kommons.io.writeBinaryString import ru.dbotthepony.kommons.util.getValue import ru.dbotthepony.kommons.util.setValue @@ -9,12 +10,13 @@ import ru.dbotthepony.kstarbound.defs.DamageNotification import ru.dbotthepony.kstarbound.defs.DamageSource import ru.dbotthepony.kstarbound.defs.EntityType import ru.dbotthepony.kstarbound.defs.HitType -import ru.dbotthepony.kstarbound.defs.actor.Gender -import ru.dbotthepony.kstarbound.defs.actor.HumanoidData +import ru.dbotthepony.kstarbound.defs.actor.HumanoidConfig +import ru.dbotthepony.kstarbound.defs.actor.HumanoidIdentity import ru.dbotthepony.kstarbound.defs.actor.HumanoidEmote import ru.dbotthepony.kstarbound.defs.actor.player.PlayerGamemode import ru.dbotthepony.kstarbound.io.readInternedString import ru.dbotthepony.kstarbound.json.builder.IStringSerializable +import ru.dbotthepony.kstarbound.lua.LuaEnvironment import ru.dbotthepony.kstarbound.math.AABB import ru.dbotthepony.kstarbound.math.Interpolator import ru.dbotthepony.kstarbound.math.vector.Vector2d @@ -26,6 +28,7 @@ import ru.dbotthepony.kstarbound.network.syncher.networkedEnumExtraStupid import ru.dbotthepony.kstarbound.network.syncher.networkedEventCounter import ru.dbotthepony.kstarbound.network.syncher.networkedFixedPoint import ru.dbotthepony.kstarbound.network.syncher.networkedString +import ru.dbotthepony.kstarbound.util.GameTimer import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.Animator import ru.dbotthepony.kstarbound.world.entities.HumanoidActorEntity @@ -55,7 +58,8 @@ class PlayerEntity() : HumanoidActorEntity() { uniqueID.accept(data.readInternedString()) description = data.readInternedString() gamemode = PlayerGamemode.entries[if (isLegacy) data.readInt() else data.readUnsignedByte()] - humanoidData = HumanoidData.read(data, isLegacy) + humanoidIdentity = HumanoidIdentity.read(data, isLegacy) + humanoidConfig = runBlocking { humanoidIdentity.species.value!!.humanoidConfig() } } constructor(data: JsonObject) : this() { @@ -73,7 +77,7 @@ class PlayerEntity() : HumanoidActorEntity() { stream.writeBinaryString(uniqueID.get() ?: "") stream.writeBinaryString(description) if (isLegacy) stream.writeInt(gamemode.ordinal) else stream.writeByte(gamemode.ordinal) - humanoidData.write(stream, isLegacy) + humanoidIdentity.write(stream, isLegacy) } val inventory = PlayerInventory() @@ -82,11 +86,11 @@ class PlayerEntity() : HumanoidActorEntity() { override val statusController = StatusController(this, Globals.player.statusControllerSettings) val techController = TechController(this) - var state by networkGroup.upstream.add(networkedEnum(State.IDLE)) + var playerState by networkGroup.upstream.add(networkedEnum(State.IDLE)) var shifting by networkGroup.upstream.add(networkedBoolean()) - private var xAimPosition by networkGroup.upstream.add(networkedFixedPoint(0.003125)) - private var yAimPosition by networkGroup.upstream.add(networkedFixedPoint(0.003125).also { it.interpolator = Interpolator.Linear }) - var humanoidData by networkGroup.upstream.add(networkedData(HumanoidData(), HumanoidData.CODEC, HumanoidData.LEGACY_CODEC)) + override var xAimPosition by networkGroup.upstream.add(networkedFixedPoint(0.003125)) + override var yAimPosition by networkGroup.upstream.add(networkedFixedPoint(0.003125).also { it.interpolator = Interpolator.Linear }) + override var humanoidIdentity: HumanoidIdentity by networkGroup.upstream.add(networkedData(HumanoidIdentity(), HumanoidIdentity.CODEC, HumanoidIdentity.LEGACY_CODEC)) init { networkGroup.upstream.add(team) @@ -97,6 +101,15 @@ class PlayerEntity() : HumanoidActorEntity() { val newChatMessage = networkGroup.upstream.add(networkedEventCounter()) var emote by networkGroup.upstream.add(networkedEnumExtraStupid(HumanoidEmote.IDLE)) + override val lua: LuaEnvironment + get() = TODO("Not yet implemented") + override val emoteCooldownTimer: GameTimer = GameTimer() + override val danceTimer: GameTimer? + get() = null + + override val blinkInterval: Vector2d + get() = Globals.player.blinkInterval + init { networkGroup.upstream.add(inventory.networkGroup) networkGroup.upstream.add(toolsNetworkGroup) @@ -111,31 +124,23 @@ class PlayerEntity() : HumanoidActorEntity() { movement.resetBaseParameters(Globals.player.movementParameters) } + override var humanoidConfig: HumanoidConfig by Delegates.notNull() + private set override val damageBarType: DamageBarType get() = DamageBarType.DEFAULT override val name: String - get() = humanoidData.name - override val species: String - get() = humanoidData.species - override val gender: Gender - get() = humanoidData.gender + get() = humanoidIdentity.name override val metaBoundingBox: AABB get() = Globals.player.metaBoundBox + position - override val aimPosition: Vector2d - get() = Vector2d(xAimPosition, yAimPosition) - override val isPersistent: Boolean get() = false var isAdmin = false - val isDead: Boolean - get() = health <= 0.0 - val isTeleporting: Boolean - get() = state == State.TELEPORT_IN || state == State.TELEPORT_OUT + get() = playerState == State.TELEPORT_IN || playerState == State.TELEPORT_OUT override val damageHitbox: List get() = movement.computeGlobalHitboxes() @@ -152,6 +157,9 @@ class PlayerEntity() : HumanoidActorEntity() { if (source.intersect(world.geometry, movement.computeGlobalHitboxes())) return HitType.HIT + if (queryShieldHit(source)) + return HitType.SHIELD_HIT + return null } 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 b277cf64..c3482020 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 @@ -21,6 +21,8 @@ import ru.dbotthepony.kstarbound.util.coalesceNull import ru.dbotthepony.kstarbound.util.valueOf import ru.dbotthepony.kstarbound.world.Direction import ru.dbotthepony.kstarbound.world.PIXELS_IN_STARBOUND_UNIT +import ru.dbotthepony.kstarbound.world.entities.ActorEntity +import ru.dbotthepony.kstarbound.world.entities.IAnchorState import ru.dbotthepony.kstarbound.world.entities.api.LoungeableEntity import java.lang.Math.toRadians @@ -29,7 +31,7 @@ class LoungeableObject(config: Registry.Entry) : WorldObject(c isInteractive = true } - val sitPositions = ArrayList() + override val sitPositions = ArrayList() var sitFlipDirection = false private set @@ -84,6 +86,14 @@ class LoungeableObject(config: Registry.Entry) : WorldObject(c sitCursorOverride = lookupProperty("sitCursorOverride").coalesceNull?.asString } + override fun entitiesLoungingIn(index: Int): List { + TODO("Not yet implemented") + } + + override fun anchor(index: Int): IAnchorState? { + TODO("Not yet implemented") + } + override fun parametersUpdated() { super.parametersUpdated() updateSitParams() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/TileEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/TileEntity.kt index ca1d087c..f01f4907 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/TileEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/TileEntity.kt @@ -28,7 +28,7 @@ import ru.dbotthepony.kstarbound.world.entities.ItemDropEntity /** * (Hopefully) Static world entities (Plants, Objects, etc), which reside on cell grid */ -abstract class TileEntity : AbstractEntity() { +abstract class TileEntity() : AbstractEntity() { override fun deserialize(data: JsonObject) { super.deserialize(data) tilePosition = data.get("tilePosition", vectors)