From 209c1a577679abd003f233f4c705c3dcea71025b Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Tue, 2 Apr 2024 20:07:11 +0700 Subject: [PATCH] TileEntities, WorldObject --- .../ru/dbotthepony/kstarbound/Registry.kt | 12 - .../ru/dbotthepony/kstarbound/Starbound.kt | 21 +- .../network/packets/SpawnWorldObjectPacket.kt | 2 +- .../kstarbound/collect/RandomListIterator.kt | 41 ++ .../kstarbound/collect/RandomSubList.kt | 177 +++++++++ .../ru/dbotthepony/kstarbound/defs/Damage.kt | 145 +++++++ .../kstarbound/defs/EphemeralStatusEffect.kt | 44 ++ .../dbotthepony/kstarbound/defs/JsonDriven.kt | 138 ++----- .../kstarbound/defs/PlayerWarping.kt | 2 +- .../kstarbound/defs/actor/Types.kt | 2 + .../defs/animation/AnimatedPartsDefinition.kt | 26 +- .../kstarbound/defs/image/Image.kt | 47 ++- .../kstarbound/defs/item/ItemDescriptor.kt | 20 +- .../defs/object/ObjectDefinition.kt | 12 +- .../defs/object/ObjectOrientation.kt | 17 +- .../kstarbound/defs/object/ObjectType.kt | 16 +- .../defs/quest/QuestArcDescriptor.kt | 34 ++ .../kstarbound/defs/quest/QuestDescriptor.kt | 44 ++ .../kstarbound/defs/quest/QuestParameter.kt | 325 +++++++++++++++ .../kstarbound/defs/tile/TileDamageConfig.kt | 29 +- .../kstarbound/defs/tile/TileDamageType.kt | 16 +- .../kstarbound/defs/tile/TileDefinition.kt | 4 + .../defs/tile/TileModifierDefinition.kt | 24 +- .../defs/world/TerrestrialWorldParameters.kt | 12 +- .../ru/dbotthepony/kstarbound/io/Streams.kt | 65 +++ .../kstarbound/json/JsonAdapterTypeFactory.kt | 55 +++ .../json/JsonImplementationTypeFactory.kt | 19 + .../dbotthepony/kstarbound/json/JsonPath.kt | 260 ++++++++++++ .../dbotthepony/kstarbound/json/JsonUtils.kt | 82 +++- .../kstarbound/json/NullSafeTypeAdapter.kt | 23 ++ .../kstarbound/json/builder/Annotations.kt | 16 - .../kstarbound/json/builder/FactoryAdapter.kt | 2 +- .../ru/dbotthepony/kstarbound/math/Utils.kt | 23 ++ .../clientbound/TileDamageUpdatePacket.kt | 2 +- .../kstarbound/network/syncher/Factories.kt | 4 + .../network/syncher/NetworkedList.kt | 343 ++++++++++++++++ .../network/syncher/NetworkedMap.kt | 32 +- .../dbotthepony/kstarbound/player/Avatar.kt | 9 +- .../kstarbound/player/QuestDescriptor.kt | 18 - .../kstarbound/player/QuestInstance.kt | 10 +- .../kstarbound/server/ServerConnection.kt | 4 +- .../server/world/LegacyWorldStorage.kt | 2 +- .../kstarbound/server/world/ServerWorld.kt | 66 ++- .../server/world/ServerWorldTracker.kt | 35 +- .../kstarbound/server/world/UniverseSource.kt | 11 +- .../ru/dbotthepony/kstarbound/util/Utils.kt | 36 +- .../ru/dbotthepony/kstarbound/world/Chunk.kt | 69 +++- .../kstarbound/world/TileHealth.kt | 96 +++-- .../ru/dbotthepony/kstarbound/world/World.kt | 16 +- .../kstarbound/world/api/AbstractCell.kt | 7 +- .../kstarbound/world/api/ImmutableCell.kt | 7 + .../kstarbound/world/api/MutableCell.kt | 7 + .../world/entities/AbstractEntity.kt | 35 +- .../world/entities/AnimatedParts.kt | 59 --- .../kstarbound/world/entities/Animator.kt | 325 +++++++++++++-- .../world/entities/DynamicEntity.kt | 6 +- .../kstarbound/world/entities/TileEntity.kt | 33 -- .../kstarbound/world/entities/WorldObject.kt | 205 ---------- .../world/entities/player/PlayerEntity.kt | 28 +- .../world/entities/tile/ContainerObject.kt | 23 ++ .../world/entities/tile/LoungeableObject.kt | 10 + .../world/entities/tile/TileEntity.kt | 69 ++++ .../world/entities/tile/WorldObject.kt | 375 ++++++++++++++++++ .../world/entities/wire/WireConnection.kt | 23 ++ .../kstarbound/test/CollectionTests.kt | 47 +++ 65 files changed, 3038 insertions(+), 729 deletions(-) create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/collect/RandomListIterator.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/collect/RandomSubList.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/EphemeralStatusEffect.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/quest/QuestArcDescriptor.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/quest/QuestDescriptor.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/quest/QuestParameter.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonAdapterTypeFactory.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonImplementationTypeFactory.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonPath.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/json/NullSafeTypeAdapter.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedList.kt delete mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/player/QuestDescriptor.kt delete mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AnimatedParts.kt delete mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/TileEntity.kt delete mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/WorldObject.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/ContainerObject.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/LoungeableObject.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/TileEntity.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/wire/WireConnection.kt create mode 100644 src/test/kotlin/ru/dbotthepony/kstarbound/test/CollectionTests.kt diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt index efcbdb7c..f7208311 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt @@ -8,12 +8,8 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectMap import it.unimi.dsi.fastutil.ints.Int2ObjectMaps import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap import it.unimi.dsi.fastutil.objects.Object2ObjectFunction -import it.unimi.dsi.fastutil.objects.Object2ObjectMap -import it.unimi.dsi.fastutil.objects.Object2ObjectMaps -import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.util.Either -import ru.dbotthepony.kstarbound.util.traverseJsonPath import java.util.Collections import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.locks.ReentrantLock @@ -63,10 +59,6 @@ class Registry(val name: String) { val value: T? get() = entry?.value - fun traverseJsonPath(path: String): JsonElement? { - return traverseJsonPath(path, entry?.json ?: return null) - } - final override fun get(): Entry? { return entry } @@ -82,10 +74,6 @@ class Registry(val name: String) { abstract val isBuiltin: Boolean abstract val ref: Ref - fun traverseJsonPath(path: String): JsonElement? { - return traverseJsonPath(path, json) - } - final override fun get(): T { return value } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt index 076e4866..e6293f33 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt @@ -35,6 +35,7 @@ import ru.dbotthepony.kstarbound.defs.`object`.ObjectOrientation 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 import ru.dbotthepony.kstarbound.defs.world.BiomePlaceables @@ -50,7 +51,7 @@ import ru.dbotthepony.kstarbound.json.LongRangeAdapter import ru.dbotthepony.kstarbound.json.builder.EnumAdapter import ru.dbotthepony.kstarbound.json.builder.BuilderAdapter import ru.dbotthepony.kstarbound.json.builder.FactoryAdapter -import ru.dbotthepony.kstarbound.json.builder.JsonImplementationTypeFactory +import ru.dbotthepony.kstarbound.json.JsonImplementationTypeFactory import ru.dbotthepony.kstarbound.json.factory.CollectionAdapterFactory import ru.dbotthepony.kstarbound.json.factory.ImmutableCollectionAdapterFactory import ru.dbotthepony.kstarbound.json.factory.PairAdapterFactory @@ -59,13 +60,14 @@ import ru.dbotthepony.kstarbound.json.factory.SingletonTypeAdapterFactory import ru.dbotthepony.kstarbound.math.* import ru.dbotthepony.kstarbound.server.world.UniverseChunk import ru.dbotthepony.kstarbound.item.ItemStack +import ru.dbotthepony.kstarbound.json.JsonAdapterTypeFactory +import ru.dbotthepony.kstarbound.json.JsonPath import ru.dbotthepony.kstarbound.json.NativeLegacy import ru.dbotthepony.kstarbound.util.Directives import ru.dbotthepony.kstarbound.util.ExceptionLogger import ru.dbotthepony.kstarbound.util.SBPattern import ru.dbotthepony.kstarbound.util.HashTableInterner import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise -import ru.dbotthepony.kstarbound.util.traverseJsonPath import ru.dbotthepony.kstarbound.world.UniversePos import ru.dbotthepony.kstarbound.world.physics.Poly import java.io.* @@ -157,9 +159,14 @@ object Starbound : ISBFileLocator { @JvmField val STRINGS: Interner = interner(5) - // immeasurably lazy and fragile solution + // immeasurably lazy and fragile solution, too bad! + // While having four separate Gson instances look like a (much) better solution (and it indeed could have been!), + // we must not forget the fact that 'Starbound' and 'Consistent data format' are opposites, + // and there are cases of where discStore() calls toJson() on children data, despite it having discStore() too. var IS_WRITING_LEGACY_JSON: Boolean by ThreadLocal.withInitial { false } private set + var IS_WRITING_STORE_JSON: Boolean by ThreadLocal.withInitial { false } + private set fun writeLegacyJson(data: Any): JsonElement { try { @@ -197,6 +204,7 @@ object Starbound : ISBFileLocator { // Обработчик @JsonImplementation registerTypeAdapterFactory(JsonImplementationTypeFactory) + registerTypeAdapterFactory(JsonAdapterTypeFactory) // списки, наборы, т.п. registerTypeAdapterFactory(CollectionAdapterFactory) @@ -294,6 +302,7 @@ object Starbound : ISBFileLocator { registerTypeAdapter(CelestialParameters::Adapter) registerTypeAdapter(Particle::Adapter) + registerTypeAdapter(QuestParameter::Adapter) registerTypeAdapterFactory(BiomePlacementDistributionType.DEFINITION_ADAPTER) registerTypeAdapterFactory(BiomePlacementItemType.DATA_ADAPTER) @@ -385,11 +394,11 @@ object Starbound : ISBFileLocator { val file = locate(filename) - if (!file.isFile) { + if (!file.isFile) return null - } - return traverseJsonPath(jsonPath, gson.fromJson(file.reader(), JsonElement::class.java)) + val pathTraverser = if (jsonPath == null) JsonPath.EMPTY else JsonPath.query(jsonPath) + return pathTraverser.get(gson.fromJson(file.reader(), JsonElement::class.java)) } private val archivePaths = ArrayList() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/SpawnWorldObjectPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/SpawnWorldObjectPacket.kt index 74f5bec0..1d8cbd92 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/SpawnWorldObjectPacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/SpawnWorldObjectPacket.kt @@ -7,7 +7,7 @@ import ru.dbotthepony.kstarbound.client.ClientConnection import ru.dbotthepony.kstarbound.json.readJsonObject import ru.dbotthepony.kstarbound.json.writeJsonObject import ru.dbotthepony.kstarbound.network.IClientPacket -import ru.dbotthepony.kstarbound.world.entities.WorldObject +import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject import java.io.DataInputStream import java.io.DataOutputStream import java.util.UUID diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/collect/RandomListIterator.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/collect/RandomListIterator.kt new file mode 100644 index 00000000..03ceeb73 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/collect/RandomListIterator.kt @@ -0,0 +1,41 @@ +package ru.dbotthepony.kstarbound.collect + +class RandomListIterator(private val elements: MutableList, index: Int = 0) : MutableListIterator { + private var index = index - 1 + + override fun hasPrevious(): Boolean { + return this.index > 0 + } + + override fun nextIndex(): Int { + return this.index + 1 + } + + override fun previous(): E { + return elements[--this.index] + } + + override fun previousIndex(): Int { + return (this.index - 1).coerceAtLeast(-1) + } + + override fun add(element: E) { + elements.add(this.index++, element) + } + + override fun hasNext(): Boolean { + return this.index < elements.size - 1 + } + + override fun next(): E { + return elements[++this.index] + } + + override fun remove() { + elements.removeAt(this.index--) + } + + override fun set(element: E) { + elements[this.index] = element + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/collect/RandomSubList.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/collect/RandomSubList.kt new file mode 100644 index 00000000..777c595d --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/collect/RandomSubList.kt @@ -0,0 +1,177 @@ +package ru.dbotthepony.kstarbound.collect + +class RandomSubList(private val elements: MutableList, private val fromIndex: Int, private var toIndex: Int) : MutableList { + override val size: Int + get() = toIndex - fromIndex + + init { + check(size >= 0) { "Invalid sublist range: $fromIndex .. $toIndex" } + } + + override fun contains(element: E): Boolean { + for (i in fromIndex until toIndex) { + if (elements[i] == element) { + return true + } + } + + return false + } + + override fun containsAll(elements: Collection): Boolean { + return elements.all { contains(it) } + } + + override fun get(index: Int): E { + if (index !in 0 until size) + throw IndexOutOfBoundsException(index.toString()) + + return elements[index + fromIndex] + } + + override fun indexOf(element: E): Int { + for (i in fromIndex until toIndex) { + if (elements[i] == element) { + return i - fromIndex + } + } + + return -1 + } + + override fun isEmpty(): Boolean { + return size == 0 + } + + override fun iterator(): MutableIterator { + return listIterator() + } + + override fun lastIndexOf(element: E): Int { + for (i in toIndex - 1 downTo fromIndex) { + if (elements[i] == element) { + return i - fromIndex + } + } + + return -1 + } + + override fun add(element: E): Boolean { + elements.add(toIndex++, element) + return true + } + + override fun add(index: Int, element: E) { + if (index !in 0 .. size) + throw IndexOutOfBoundsException(index.toString()) + + elements.add(index + fromIndex, element) + toIndex++ + } + + override fun addAll(index: Int, elements: Collection): Boolean { + if (index !in 0 .. size) + throw IndexOutOfBoundsException(index.toString()) + + this.elements.addAll(index + fromIndex, elements) + toIndex += elements.size + return true + } + + override fun addAll(elements: Collection): Boolean { + return addAll(toIndex, elements) + } + + override fun clear() { + for (i in fromIndex until toIndex) { + elements.removeAt(fromIndex) + } + + toIndex = fromIndex + } + + override fun listIterator(): MutableListIterator { + return RandomListIterator(this) + } + + override fun listIterator(index: Int): MutableListIterator { + return RandomListIterator(this, index) + } + + override fun remove(element: E): Boolean { + val indexOf = indexOf(element) + + if (indexOf == -1) + return false + + removeAt(indexOf) + return true + } + + override fun removeAll(elements: Collection): Boolean { + var any = false + elements.forEach { any = remove(it) || any } + return any + } + + override fun removeAt(index: Int): E { + if (index !in 0 until size) + throw IndexOutOfBoundsException(index.toString()) + + val result = elements.removeAt(index + fromIndex) + toIndex-- + return result + } + + override fun retainAll(elements: Collection): Boolean { + val itr = iterator() + var modified = false + + for (v in itr) { + if (v !in elements) { + itr.remove() + modified = true + } + } + + return modified + } + + override fun set(index: Int, element: E): E { + if (index !in 0 until size) + throw IndexOutOfBoundsException(index.toString()) + + return elements.set(index + fromIndex, element) + } + + override fun subList(fromIndex: Int, toIndex: Int): MutableList { + return RandomSubList(this, fromIndex, toIndex) + } + + override fun equals(other: Any?): Boolean { + if (other === this) return true + if (other !is List<*>) return false + + val e1: ListIterator = listIterator() + val e2 = other.listIterator() + + while (e1.hasNext() && e2.hasNext()) { + val o1 = e1.next() + val o2 = e2.next() + if (!(if (o1 == null) o2 == null else o1 == o2)) return false + } + + return !(e1.hasNext() || e2.hasNext()) + } + + override fun hashCode(): Int { + var hashCode = 1 + for (e in this) hashCode = 31 * hashCode + e.hashCode() + return hashCode + } + + override fun toString(): String { + return "[${joinToString()}]" + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Damage.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Damage.kt index dfb8b379..1ae5eb97 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Damage.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Damage.kt @@ -2,14 +2,42 @@ package ru.dbotthepony.kstarbound.defs import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableSet +import com.google.gson.Gson +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 ru.dbotthepony.kommons.io.readCollection +import ru.dbotthepony.kommons.io.writeBinaryString +import ru.dbotthepony.kommons.io.writeCollection +import ru.dbotthepony.kommons.io.writeDouble +import ru.dbotthepony.kommons.io.writeStruct2d +import ru.dbotthepony.kommons.io.writeStruct2f +import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.io.readDouble +import ru.dbotthepony.kstarbound.io.readInternedString +import ru.dbotthepony.kstarbound.io.readMVariant2 +import ru.dbotthepony.kstarbound.io.readNullableDouble +import ru.dbotthepony.kstarbound.io.readNullableString +import ru.dbotthepony.kstarbound.io.readVector2d +import ru.dbotthepony.kstarbound.io.writeDouble +import ru.dbotthepony.kstarbound.io.writeMVariant2 +import ru.dbotthepony.kstarbound.io.writeNullable +import ru.dbotthepony.kstarbound.io.writeStruct2d +import ru.dbotthepony.kstarbound.json.builder.FactoryAdapter import ru.dbotthepony.kstarbound.json.builder.IStringSerializable import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.json.getAdapter import ru.dbotthepony.kstarbound.network.syncher.legacyCodec import ru.dbotthepony.kstarbound.network.syncher.nativeCodec +import ru.dbotthepony.kstarbound.world.physics.Poly import java.io.DataInputStream import java.io.DataOutputStream +// uint8_t enum class TeamType(override val jsonName: String) : IStringSerializable { NULL("null"), // non-PvP-enabled players and player allied NPCs @@ -31,6 +59,7 @@ enum class TeamType(override val jsonName: String) : IStringSerializable { ASSISTANT("assistant"); } +// int32_t enum class HitType(override val jsonName: String) : IStringSerializable { HIT("Hit"), STRONG_HIT("StrongHit"), @@ -39,6 +68,7 @@ enum class HitType(override val jsonName: String) : IStringSerializable { KILL("Kill"); } +// uint8_t enum class DamageType(override val jsonName: String) : IStringSerializable { NO_DAMAGE("NoDamage"), DAMAGE("Damage"), @@ -58,6 +88,8 @@ data class EntityDamageTeam(val type: TeamType = TeamType.NULL, val team: Int = } companion object { + val NULL = EntityDamageTeam() + val PASSIVE = EntityDamageTeam(TeamType.PASSIVE) val CODEC = nativeCodec(::EntityDamageTeam, EntityDamageTeam::write) val LEGACY_CODEC = legacyCodec(::EntityDamageTeam, EntityDamageTeam::write) } @@ -72,3 +104,116 @@ data class TouchDamage( val knockback: Double = 0.0, val statusEffects: ImmutableSet = ImmutableSet.of(), ) + +// this shit is a complete mess, because in original code DamageSource::toJson() method +// will create json structure which will not be readable by DamageSource's constructor +// (will always throw an exception) +@JsonAdapter(DamageSource.Adapter::class) +data class DamageSource( + val damageType: DamageType, + val damageArea: Either>, + val damage: Double, + val trackSourceEntity: Boolean, + val sourceEntityId: Int = 0, + val team: EntityDamageTeam = EntityDamageTeam.PASSIVE, + val damageRepeatGroup: String? = null, + val damageRepeatTimeout: Double? = null, + val damageSourceKind: String = "", + val statusEffects: ImmutableList = ImmutableList.of(), + val knockback: Either, + val rayCheck: Boolean = false, +) { + constructor(stream: DataInputStream, isLegacy: Boolean) : this( + DamageType.entries[stream.readUnsignedByte()], + stream.readMVariant2({ Poly.read(this, isLegacy) }, { stream.readVector2d(isLegacy) to stream.readVector2d(isLegacy) }) ?: throw IllegalArgumentException("Empty MVariant damageArea"), + stream.readDouble(isLegacy), + stream.readBoolean(), + stream.readInt(), + EntityDamageTeam(stream, isLegacy), + stream.readNullableString(), + stream.readNullableDouble(), + stream.readInternedString(), + ImmutableList.copyOf(stream.readCollection { EphemeralStatusEffect(stream, isLegacy) }), + stream.readMVariant2({ readDouble(isLegacy) }, { readVector2d(isLegacy) }) ?: throw IllegalArgumentException("Empty MVariant knockback"), + stream.readBoolean() + ) + + data class JsonData( + val poly: Poly? = null, + val line: Pair? = null, + val damage: Double, + val damageType: DamageType = DamageType.DAMAGE, + val trackSourceEntity: Boolean = true, + val sourceEntityId: Int = 0, + val teamType: TeamType = TeamType.PASSIVE, + val teamNumber: Int = 0, + val team: EntityDamageTeam? = null, + val damageRepeatGroup: String? = null, + val damageRepeatTimeout: Double? = null, + val damageSourceKind: String = "", + val statusEffects: ImmutableList = ImmutableList.of(), + val knockback: Either = Either.left(0.0), + val rayCheck: Boolean = false, + ) + + fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeByte(damageType.ordinal) + stream.writeMVariant2(damageArea, { it.write(stream, isLegacy) }, { stream.writeStruct2d(it.first, isLegacy); stream.writeStruct2d(it.second, isLegacy) }) + stream.writeDouble(damage, isLegacy) + stream.writeBoolean(trackSourceEntity) + stream.writeInt(sourceEntityId) + team.write(stream, isLegacy) + stream.writeNullable(damageRepeatGroup, DataOutputStream::writeBinaryString) + stream.writeNullable(damageRepeatTimeout, DataOutputStream::writeDouble) + stream.writeBinaryString(damageSourceKind) + stream.writeCollection(statusEffects) { it.write(stream, isLegacy) } + stream.writeMVariant2(knockback, { writeDouble(it, isLegacy) }, { writeStruct2d(it, isLegacy) }) + stream.writeBoolean(rayCheck) + } + + class Adapter(gson: Gson) : TypeAdapter() { + private val data = FactoryAdapter.createFor(JsonData::class, gson = gson) + + override fun write(out: JsonWriter, value: DamageSource) { + data.write(out, JsonData( + poly = value.damageArea.left.orNull(), + line = value.damageArea.right.orNull(), + damage = value.damage, + damageType = value.damageType, + trackSourceEntity = value.trackSourceEntity, + sourceEntityId = value.sourceEntityId, + team = value.team, + damageRepeatGroup = value.damageRepeatGroup, + damageRepeatTimeout = value.damageRepeatTimeout, + damageSourceKind = value.damageSourceKind, + statusEffects = value.statusEffects, + knockback = value.knockback, + rayCheck = value.rayCheck, + )) + } + + override fun read(`in`: JsonReader): DamageSource { + val read = data.read(`in`) + + return DamageSource( + damageType = read.damageType, + damageArea = if (read.line == null) Either.left(read.poly ?: throw JsonSyntaxException("Missing both 'line' and 'poly' from DamageSource json")) else Either.right(read.line), + damage = read.damage, + trackSourceEntity = read.trackSourceEntity, + sourceEntityId = read.sourceEntityId, + statusEffects = read.statusEffects, + team = read.team ?: EntityDamageTeam(read.teamType, read.teamNumber), + damageRepeatGroup = read.damageRepeatGroup, + damageRepeatTimeout = read.damageRepeatTimeout, + damageSourceKind = read.damageSourceKind, + knockback = read.knockback, + rayCheck = read.rayCheck, + ) + } + } + + companion object { + val CODEC = nativeCodec(::DamageSource, DamageSource::write) + val LEGACY_CODEC = legacyCodec(::DamageSource, DamageSource::write) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/EphemeralStatusEffect.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/EphemeralStatusEffect.kt new file mode 100644 index 00000000..57eceafc --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/EphemeralStatusEffect.kt @@ -0,0 +1,44 @@ +package ru.dbotthepony.kstarbound.defs + +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.writeBinaryString +import ru.dbotthepony.kstarbound.io.readInternedString +import ru.dbotthepony.kstarbound.io.readNullableDouble +import ru.dbotthepony.kstarbound.io.writeNullable +import ru.dbotthepony.kstarbound.io.writeNullableDouble +import ru.dbotthepony.kstarbound.json.builder.FactoryAdapter +import java.io.DataInputStream +import java.io.DataOutputStream + +@JsonAdapter(EphemeralStatusEffect.Adapter::class) +data class EphemeralStatusEffect(val effect: String, val duration: Double? = null) { + constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readInternedString(), stream.readNullableDouble()) + + class Adapter(gson: Gson) : TypeAdapter() { + private val factory = FactoryAdapter.createFor(EphemeralStatusEffect::class, gson = gson) + + override fun write(out: JsonWriter, value: EphemeralStatusEffect) { + if (value.duration == null) + out.value(value.effect) + else + factory.write(out, value) + } + + override fun read(`in`: JsonReader): EphemeralStatusEffect { + if (`in`.peek() == JsonToken.STRING) + return EphemeralStatusEffect(`in`.nextString()) + else + return factory.read(`in`) + } + } + + fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeBinaryString(effect) + stream.writeNullableDouble(duration, isLegacy) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonDriven.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonDriven.kt index 8f177f61..3f7df47d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonDriven.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonDriven.kt @@ -1,18 +1,18 @@ package ru.dbotthepony.kstarbound.defs -import com.google.gson.JsonArray import com.google.gson.JsonElement +import com.google.gson.JsonNull import com.google.gson.JsonObject import com.google.gson.TypeAdapter import com.google.gson.reflect.TypeToken -import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.util.AssetPathStack -import ru.dbotthepony.kommons.gson.set -import java.util.function.Consumer +import ru.dbotthepony.kommons.util.Delegate +import ru.dbotthepony.kstarbound.json.JsonPath import java.util.function.Function import java.util.function.Supplier +import kotlin.properties.Delegates import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty import kotlin.reflect.javaType @@ -22,36 +22,32 @@ import kotlin.reflect.javaType */ abstract class JsonDriven(val path: String) { private val delegates = ArrayList>() - private val delegatesMap = HashMap>>() - private val lazies = ArrayList>() - private val namedLazies = HashMap>>() - - protected val properties = JsonObject() /** * [JsonObject]s which define behavior of properties */ - protected abstract fun defs(): Collection + abstract fun lookupProperty(path: JsonPath, orElse: () -> JsonElement): JsonElement + + fun lookupProperty(key: JsonPath): JsonElement { + return lookupProperty(key) { JsonNull.INSTANCE } + } + + fun setProperty(key: JsonPath, value: JsonElement) { + setProperty0(key, value) + invalidate() + } + + protected abstract fun setProperty0(key: JsonPath, value: JsonElement) protected open fun invalidate() { delegates.forEach { it.invalidate() } lazies.forEach { it.invalidate() } } - protected open fun invalidate(name: String) { - delegatesMap[name]?.forEach { it.invalidate() } - namedLazies[name]?.forEach { it.invalidate() } - lazies.forEach { it.invalidate() } - } - - inner class LazyData(names: Iterable = listOf(), private val initializer: () -> T) : Lazy { - constructor(initializer: () -> T) : this(listOf(), initializer) - + inner class LazyData(private val initializer: () -> T) : Lazy { init { - for (name in names) { - namedLazies.computeIfAbsent(name, Function { ArrayList() }).add(this) - } + lazies.add(this) } private var _value: Any? = mark @@ -78,50 +74,35 @@ abstract class JsonDriven(val path: String) { } inner class Property( - name: String? = null, + val name: JsonPath, val default: Either, JsonElement>? = null, private var adapter: TypeAdapter? = null, - ) : Supplier, Consumer, ReadWriteProperty { - constructor(name: String, default: T, adapter: TypeAdapter? = null) : this(name, Either.left(Supplier { default }), adapter) - constructor(name: String, default: Supplier, adapter: TypeAdapter? = null) : this(name, Either.left(default), adapter) - constructor(name: String, default: JsonElement, adapter: TypeAdapter? = null) : this(name, Either.right(default), adapter) - constructor(default: T, adapter: TypeAdapter? = null) : this(null, Either.left(Supplier { default }), adapter) - constructor(default: Supplier, adapter: TypeAdapter? = null) : this(null, Either.left(default), adapter) - constructor(default: JsonElement, adapter: TypeAdapter? = null) : this(null, Either.right(default), adapter) - - var name: String? = name - private set(value) { - if (field != null || value == null) - throw IllegalStateException() - - field = value - delegatesMap.computeIfAbsent(value, Function { ArrayList() }).add(this) - } + ) : Delegate, ReadWriteProperty { + constructor(name: JsonPath, default: T, adapter: TypeAdapter? = null) : this(name, Either.left(Supplier { default }), adapter) + constructor(name: JsonPath, default: Supplier, adapter: TypeAdapter? = null) : this(name, Either.left(default), adapter) + constructor(name: JsonPath, default: JsonElement, adapter: TypeAdapter? = null) : this(name, Either.right(default), adapter) init { delegates.add(this) - - if (name != null) - delegatesMap.computeIfAbsent(name, Function { ArrayList() }).add(this) } - private var value: Supplier = never as Supplier + private var value: Supplier by Delegates.notNull() private fun compute(): T { - val value = dataValue(checkNotNull(name)) + val value = lookupProperty(name) - if (value == null) { + if (value.isJsonNull) { if (default == null) { throw NoSuchElementException("No json value present at '$name', and no default value was provided") } else if (default.isLeft) { return default.left().get() } else { - AssetPathStack.block(path) { + AssetPathStack(this@JsonDriven.path) { return adapter!!.fromJsonTree(default.right()) } } } else { - AssetPathStack.block(path) { + AssetPathStack(this@JsonDriven.path) { return adapter!!.fromJsonTree(value) } } @@ -144,12 +125,11 @@ abstract class JsonDriven(val path: String) { } override fun accept(t: T) { - AssetPathStack.block(path) { - properties[checkNotNull(name)] = adapter!!.toJsonTree(t) + AssetPathStack(this@JsonDriven.path) { + setProperty0(name, adapter!!.toJsonTree(t)) } - // value = Supplier { t } - invalidate(name!!) + value = Supplier { t } } @OptIn(ExperimentalStdlibApi::class) @@ -158,10 +138,6 @@ abstract class JsonDriven(val path: String) { adapter = Starbound.gson.getAdapter(TypeToken.get(property.returnType.javaType)) as TypeAdapter } - if (name == null) { - name = property.name - } - return value.get() } @@ -171,63 +147,11 @@ abstract class JsonDriven(val path: String) { adapter = Starbound.gson.getAdapter(TypeToken.get(property.returnType.javaType)) as TypeAdapter } - if (name == null) { - name = property.name - } - return accept(value) } } - fun dataValue(name: String, alwaysCopy: Boolean = false): JsonElement? { - val defs = defs() - var value: JsonElement? - - if (defs.isEmpty()) { - value = properties[name]?.let { if (alwaysCopy) it.deepCopy() else it } - } else { - val itr = defs.iterator() - var isCopy = false - value = properties[name] - - while ((value == null || value is JsonObject) && itr.hasNext()) { - val next = itr.next()[name] - - if (value is JsonObject) { - if (next !is JsonObject) continue - value = mergeNoCopy(if (isCopy) value else value.deepCopy(), next) - isCopy = true - } else { - value = next - } - } - } - - return value - } - - fun hasDataValue(name: String): Boolean { - if (properties[name] != null) return true - return defs().any { it[name] != null } - } - companion object { private val mark = Any() - private val never = Supplier { throw NoSuchElementException() } - - @JvmStatic - fun mergeNoCopy(a: JsonObject, b: JsonObject): JsonObject { - for ((k, v) in b.entrySet()) { - val existing = a[k] - - if (existing is JsonObject && v is JsonObject) { - a[k] = mergeNoCopy(existing, v) - } else if (existing !is JsonObject) { - a[k] = v.deepCopy() - } - } - - return a - } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PlayerWarping.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PlayerWarping.kt index 71c7039f..b6444f5b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PlayerWarping.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PlayerWarping.kt @@ -52,7 +52,7 @@ sealed class SpawnTarget { } override fun resolve(world: ServerWorld): Vector2d? { - return world.entities.values.firstOrNull { it.uniqueID == id }?.position + return world.entities.values.firstOrNull { it.uniqueID.get().orNull() == id }?.position } override fun toString(): String { 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 d054a9e9..250c22cc 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/Types.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/Types.kt @@ -2,10 +2,12 @@ package ru.dbotthepony.kstarbound.defs.actor import ru.dbotthepony.kstarbound.json.builder.IStringSerializable +// uint8_t enum class Gender(override val jsonName: String) : IStringSerializable { MALE("Male"), FEMALE("Female"); } +// int32_t enum class HumanoidEmote(override val jsonName: String) : IStringSerializable { IDLE("Idle"), BLABBERING("Blabbering"), diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/animation/AnimatedPartsDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/animation/AnimatedPartsDefinition.kt index a58ac74d..8b16d9dc 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/animation/AnimatedPartsDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/animation/AnimatedPartsDefinition.kt @@ -1,10 +1,12 @@ package ru.dbotthepony.kstarbound.defs.animation import com.google.common.collect.ImmutableMap +import com.google.gson.JsonArray import com.google.gson.JsonObject import it.unimi.dsi.fastutil.objects.Object2ObjectAVLTreeMap import ru.dbotthepony.kstarbound.json.builder.IStringSerializable import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import kotlin.properties.Delegates @JsonFactory data class AnimatedPartsDefinition( @@ -25,6 +27,10 @@ data class AnimatedPartsDefinition( val states: ImmutableMap = ImmutableMap.of(), val properties: JsonObject = JsonObject(), ) { + val sortedStates: ImmutableMap = states.entries.stream() + .sorted { o1, o2 -> o1.key.compareTo(o2.key) } + .collect(ImmutableMap.toImmutableMap({ it.key }, { it.value })) + @JsonFactory data class State( val frames: Int = 1, @@ -32,8 +38,24 @@ data class AnimatedPartsDefinition( val mode: AnimationMode = AnimationMode.END, val transition: String = "", val properties: JsonObject = JsonObject(), - val frameProperties: JsonObject = JsonObject(), - ) + val frameProperties: ImmutableMap = ImmutableMap.of(), + ) { + var name by Delegates.notNull() + var index = 0 + } + + init { + var index = 0 + + for ((k, v) in states) { + v.name = k + v.index = index++ + + if (v.mode == AnimationMode.TRANSITION && v.transition !in states) { + throw IllegalArgumentException("State $k has specified TRANSITION as mode, however, it points to non-existing state ${v.transition}!") + } + } + } } @JsonFactory diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/Image.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/Image.kt index 2392292d..bcda5592 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/Image.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/Image.kt @@ -256,37 +256,40 @@ class Image private constructor( fun worldSpaces(pixelOffset: Vector2i, spaceScan: Double, flip: Boolean): List { if (amountOfChannels != 3 && amountOfChannels != 4) throw IllegalStateException("Can not check world space taken by image with $amountOfChannels color channels") - val xDivL = pixelOffset.x % PIXELS_IN_STARBOUND_UNITi - val yDivB = pixelOffset.y % PIXELS_IN_STARBOUND_UNITi - - val xDivR = (pixelOffset.x + width) % PIXELS_IN_STARBOUND_UNITi - val yDivT = (pixelOffset.y + height) % PIXELS_IN_STARBOUND_UNITi - - val leftMostX = pixelOffset.x / PIXELS_IN_STARBOUND_UNITi - (if (xDivL != 0) 1 else 0) - val bottomMostY = pixelOffset.y / PIXELS_IN_STARBOUND_UNITi - (if (yDivB != 0) 1 else 0) - - val rightMostX = (pixelOffset.x + width) / PIXELS_IN_STARBOUND_UNITi + (if (xDivR != 0) 1 else 0) - val topMostY = (pixelOffset.y + height) / PIXELS_IN_STARBOUND_UNITi + (if (yDivT != 0) 1 else 0) + val minX = pixelOffset.x / PIXELS_IN_STARBOUND_UNITi + val minY = pixelOffset.y / PIXELS_IN_STARBOUND_UNITi + val maxX = (width + pixelOffset.x + PIXELS_IN_STARBOUND_UNITi - 1) / PIXELS_IN_STARBOUND_UNITi + val maxY = (height + pixelOffset.y + PIXELS_IN_STARBOUND_UNITi - 1) / PIXELS_IN_STARBOUND_UNITi val result = ArrayList() - for (y in bottomMostY .. topMostY) { - for (x in leftMostX .. rightMostX) { - val left = x * PIXELS_IN_STARBOUND_UNITi - val bottom = y * PIXELS_IN_STARBOUND_UNITi + // this is weird, but that's how original game handles this + // also we don't cache this info since that's a waste of precious ram - var transparentPixels = 0 + for (yspace in minY until maxY) { + for (xspace in minX until maxX) { + var fillRatio = 0.0 - for (sX in 0 until PIXELS_IN_STARBOUND_UNITi) { - for (sY in 0 until PIXELS_IN_STARBOUND_UNITi) { - if (isTransparent(xDivL + sX + left, yDivB + sY + bottom, flip)) { - transparentPixels++ + for (y in 0 until PIXELS_IN_STARBOUND_UNITi) { + val ypixel = (yspace * PIXELS_IN_STARBOUND_UNITi + y - pixelOffset.y) + + if (ypixel !in 0 until width) + continue + + for (x in 0 until PIXELS_IN_STARBOUND_UNITi) { + val xpixel = (xspace * PIXELS_IN_STARBOUND_UNITi + x - pixelOffset.x) + + if (xpixel !in 0 until width) + continue + + if (isTransparent(xpixel, ypixel, flip)) { + fillRatio += 1.0 / (PIXELS_IN_STARBOUND_UNIT * PIXELS_IN_STARBOUND_UNIT) } } } - if (transparentPixels * FILL_RATIO >= spaceScan) { - result.add(Vector2i(x, y)) + if (fillRatio >= spaceScan) { + result.add(Vector2i(xspace, yspace)) } } } 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 2290a045..7946cc2f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDescriptor.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDescriptor.kt @@ -15,6 +15,7 @@ import org.classdump.luna.LuaRuntimeException import org.classdump.luna.Table import org.classdump.luna.TableFactory import ru.dbotthepony.kommons.gson.consumeNull +import ru.dbotthepony.kommons.gson.contains import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kstarbound.lua.StateMachine import ru.dbotthepony.kstarbound.lua.from @@ -47,12 +48,21 @@ fun ItemDescriptor(data: JsonElement): ItemDescriptor { val parameters = data.get(2, ::JsonObject) return ItemDescriptor(name, count, parameters) } else if (data is JsonObject) { - val name = (data.get("name") ?: data.get("item") ?: throw JsonSyntaxException("Missing item name")).asString - val count = data.get("count", 1L) - val parameters = data.get("parameters") { data.get("parameters", ::JsonObject) } - return ItemDescriptor(name, count, parameters) + if ("id" in data && "version" in data && "content" in data) { + // loading versioned json from original engine + if (data["id"].asString != "Item") + throw JsonSyntaxException("Expected id to be 'Item', ${data["id"]} given") + + return ItemDescriptor(data["content"]) + } else { + // loading regular json + val name = (data.get("name") ?: data.get("item") ?: throw JsonSyntaxException("Missing item name")).asString + val count = data.get("count", 1L) + val parameters = data.get("parameters") { data.get("parameters", ::JsonObject) } + return ItemDescriptor(name, count, parameters) + } } else if (data is JsonNull) { - return ItemDescriptor("air", 0L) + return ItemDescriptor.EMPTY } else { throw JsonSyntaxException("Invalid item descriptor: $data") } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectDefinition.kt index 917ffdd6..a8c083fb 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectDefinition.kt @@ -28,6 +28,8 @@ import ru.dbotthepony.kommons.gson.contains import ru.dbotthepony.kommons.gson.get import ru.dbotthepony.kommons.gson.getArray import ru.dbotthepony.kommons.gson.set +import ru.dbotthepony.kstarbound.defs.AssetReference +import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor data class ObjectDefinition( @@ -46,8 +48,8 @@ data class ObjectDefinition( val breakDropOptions: ImmutableList>? = null, val smashDropPool: Registry.Ref? = null, val smashDropOptions: ImmutableList> = ImmutableList.of(), - //val animation: AssetReference? = null, - val animation: AssetPath? = null, + val animation: AssetReference? = null, + //val animation: AssetPath? = null, val smashSounds: ImmutableSet = ImmutableSet.of(), val smashParticles: JsonArray? = null, val smashable: Boolean = false, @@ -83,7 +85,7 @@ data class ObjectDefinition( class Adapter(gson: Gson) : TypeAdapter() { @JsonFactory(logMisses = false) - class PlainData( + data class PlainData( val objectName: String, val objectType: ObjectType = ObjectType.OBJECT, val race: String = "generic", @@ -99,8 +101,8 @@ data class ObjectDefinition( val breakDropOptions: ImmutableList>? = null, val smashDropPool: Registry.Ref? = null, val smashDropOptions: ImmutableList> = ImmutableList.of(), - //val animation: AssetReference? = null, - val animation: AssetPath? = null, + val animation: AssetReference? = null, + //val animation: AssetPath? = null, val smashSounds: ImmutableSet = ImmutableSet.of(), val smashParticles: JsonArray? = null, val smashable: Boolean = false, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectOrientation.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectOrientation.kt index 8eb5955e..b61fffbc 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectOrientation.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectOrientation.kt @@ -27,6 +27,9 @@ import ru.dbotthepony.kstarbound.json.setAdapter import ru.dbotthepony.kommons.gson.contains import ru.dbotthepony.kommons.gson.get import ru.dbotthepony.kommons.gson.set +import ru.dbotthepony.kstarbound.Registries +import ru.dbotthepony.kstarbound.Registry +import ru.dbotthepony.kstarbound.defs.tile.TileDefinition import ru.dbotthepony.kstarbound.world.Side import kotlin.math.PI @@ -45,7 +48,7 @@ data class ObjectOrientation( val anchors: ImmutableSet, val anchorAny: Boolean, val directionAffinity: Side?, - val materialSpaces: ImmutableList>, + val materialSpaces: ImmutableList>>, val interactiveSpaces: ImmutableSet, val lightPosition: Vector2i, val beamAngle: Double, @@ -170,11 +173,11 @@ data class ObjectOrientation( } } - var boundingBox = AABBi(Vector2i.ZERO, Vector2i.ZERO) - - for (vec in occupySpaces) { - boundingBox = boundingBox.expand(vec) - } + val minX = occupySpaces.minOf { it.x } + val minY = occupySpaces.minOf { it.y } + val maxX = occupySpaces.maxOf { it.x } + val maxY = occupySpaces.maxOf { it.y } + val boundingBox = AABBi(Vector2i(minX, minY), Vector2i(maxX, maxY)) val metaBoundBox = obj["metaBoundBox"]?.let { aabbs.fromJsonTree(it) } val requireTilledAnchors = obj.get("requireTilledAnchors", false) @@ -249,7 +252,7 @@ data class ObjectOrientation( anchors = anchors.build(), anchorAny = anchorAny, directionAffinity = directionAffinity, - materialSpaces = materialSpaces, + materialSpaces = materialSpaces.stream().map { it.first to Registries.tiles.ref(it.second) }.collect(ImmutableList.toImmutableList()), interactiveSpaces = interactiveSpaces, lightPosition = lightPosition, beamAngle = beamAngle, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectType.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectType.kt index 71b0119d..ef7d85c2 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectType.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectType.kt @@ -1,10 +1,12 @@ package ru.dbotthepony.kstarbound.defs.`object` -enum class ObjectType { - OBJECT, - LOUNGEABLE, - CONTAINER, - FARMABLE, - TELEPORTER, - PHYSICS; +import ru.dbotthepony.kstarbound.json.builder.IStringSerializable + +enum class ObjectType(override val jsonName: String) : IStringSerializable { + OBJECT("object"), + LOUNGEABLE("loungeable"), + CONTAINER("container"), + FARMABLE("farmable"), + TELEPORTER("teleporter"), + PHYSICS("physics"); } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/quest/QuestArcDescriptor.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/quest/QuestArcDescriptor.kt new file mode 100644 index 00000000..a5e9f0f9 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/quest/QuestArcDescriptor.kt @@ -0,0 +1,34 @@ +package ru.dbotthepony.kstarbound.defs.quest + +import com.google.common.collect.ImmutableList +import ru.dbotthepony.kommons.io.readCollection +import ru.dbotthepony.kommons.io.writeBinaryString +import ru.dbotthepony.kommons.io.writeCollection +import ru.dbotthepony.kstarbound.io.readInternedString +import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.network.syncher.legacyCodec +import ru.dbotthepony.kstarbound.network.syncher.nativeCodec +import java.io.DataInputStream +import java.io.DataOutputStream + +@JsonFactory +data class QuestArcDescriptor( + val quests: ImmutableList = ImmutableList.of(), + val stagehandUniqueId: String? = null, +) { + constructor(stream: DataInputStream, isLegacy: Boolean) : this( + ImmutableList.copyOf(stream.readCollection { QuestDescriptor(this, isLegacy) }), + if (stream.readBoolean()) stream.readInternedString() else null + ) + + fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeCollection(quests) { it.write(this, isLegacy) } + stream.writeBoolean(stagehandUniqueId != null) + if (stagehandUniqueId != null) stream.writeBinaryString(stagehandUniqueId) + } + + companion object { + val CODEC = nativeCodec(::QuestArcDescriptor, QuestArcDescriptor::write) + val LEGACY_CODEC = legacyCodec(::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 new file mode 100644 index 00000000..352369ab --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/quest/QuestDescriptor.kt @@ -0,0 +1,44 @@ +package ru.dbotthepony.kstarbound.defs.quest + +import com.google.common.collect.ImmutableMap +import com.google.gson.JsonObject +import ru.dbotthepony.kommons.io.readMap +import ru.dbotthepony.kommons.io.writeBinaryString +import ru.dbotthepony.kommons.io.writeMap +import ru.dbotthepony.kstarbound.io.readInternedString +import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.network.syncher.legacyCodec +import ru.dbotthepony.kstarbound.network.syncher.nativeCodec +import java.io.DataInputStream +import java.io.DataOutputStream + +@JsonFactory +data class QuestDescriptor( + val questId: String, + val templateId: String = questId, + val parameters: ImmutableMap = ImmutableMap.of(), + val seed: Long = makeSeed(), +) { + constructor(stream: DataInputStream, isLegacy: Boolean) : this( + stream.readInternedString(), + stream.readInternedString(), + ImmutableMap.copyOf(stream.readMap({ readInternedString() }, { QuestParameter(this, isLegacy) })), + stream.readLong() + ) + + fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeBinaryString(questId) + stream.writeBinaryString(templateId) + stream.writeMap(parameters, { writeBinaryString(it) }, { it.write(this, isLegacy) }) + stream.writeLong(seed) + } + + 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/quest/QuestParameter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/quest/QuestParameter.kt new file mode 100644 index 00000000..f49a9a38 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/quest/QuestParameter.kt @@ -0,0 +1,325 @@ +package ru.dbotthepony.kstarbound.defs.quest + +import com.google.common.collect.ImmutableList +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.google.gson.TypeAdapter +import com.google.gson.TypeAdapterFactory +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter +import ru.dbotthepony.kommons.gson.consumeNull +import ru.dbotthepony.kommons.gson.set +import ru.dbotthepony.kommons.gson.value +import ru.dbotthepony.kommons.io.readCollection +import ru.dbotthepony.kommons.io.readVector2d +import ru.dbotthepony.kommons.io.readVector2f +import ru.dbotthepony.kommons.io.writeBinaryString +import ru.dbotthepony.kommons.io.writeCollection +import ru.dbotthepony.kommons.io.writeStruct2d +import ru.dbotthepony.kommons.io.writeStruct2f +import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kstarbound.defs.actor.Gender +import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor +import ru.dbotthepony.kstarbound.io.readInternedString +import ru.dbotthepony.kstarbound.json.builder.DispatchingAdapter +import ru.dbotthepony.kstarbound.json.builder.IStringSerializable +import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.json.builder.JsonSingleton +import ru.dbotthepony.kstarbound.json.readJsonElement +import ru.dbotthepony.kstarbound.json.readJsonObject +import ru.dbotthepony.kstarbound.json.writeJsonElement +import ru.dbotthepony.kstarbound.json.writeJsonObject +import ru.dbotthepony.kstarbound.network.syncher.legacyCodec +import ru.dbotthepony.kstarbound.network.syncher.nativeCodec +import ru.dbotthepony.kstarbound.util.asStringOrNull +import ru.dbotthepony.kstarbound.util.coalesceNull +import ru.dbotthepony.kstarbound.world.UniversePos +import java.io.DataInputStream +import java.io.DataOutputStream + +class QuestParameter( + val detail: Detail, + val name: String? = null, + val portrait: JsonElement? = null, + val indicator: String? = null +) { + constructor(stream: DataInputStream, isLegacy: Boolean) : this( + readDetail(stream, isLegacy), + if (stream.readBoolean()) stream.readInternedString() else null, + if (stream.readBoolean()) stream.readJsonElement() else null, + if (stream.readBoolean()) stream.readInternedString() else null, + ) + + enum class Type(override val jsonName: String, val token: TypeToken) : IStringSerializable { + NO_DETAIL("noDetail", TypeToken.get(Empty::class.java)), + ITEM("item", TypeToken.get(Item::class.java)), + ITEM_TAG("itemTag", TypeToken.get(ItemTag::class.java)), + ITEM_LIST("itemList", TypeToken.get(ItemList::class.java)), + ENTITY("entity", TypeToken.get(Entity::class.java)), + LOCATION("location", TypeToken.get(Location::class.java)), + MONSTER_TYPE("monsterType", TypeToken.get(MonsterType::class.java)), + NPC_TYPE("npcType", TypeToken.get(NpcType::class.java)), + COORDINATE("coordinate", TypeToken.get(Coordinate::class.java)), + JSON("json", TypeToken.get(JsonData::class.java)); + } + + fun write(stream: DataOutputStream, isLegacy: Boolean) { + detail.write(stream, isLegacy) + stream.writeBoolean(name != null) + if (name != null) stream.writeBinaryString(name) + stream.writeBoolean(portrait != null) + if (portrait != null) stream.writeJsonElement(portrait) + stream.writeBoolean(indicator != null) + if (indicator != null) stream.writeBinaryString(indicator) + } + + override fun hashCode(): Int { + return name.hashCode() * 31 * 31 + portrait.hashCode() * 31 + indicator.hashCode() + } + + override fun equals(other: Any?): Boolean { + return this === other || other is QuestParameter && name == other.name && portrait == other.portrait && indicator == other.indicator + } + + sealed class Detail { + abstract val type: Type + abstract fun write(stream: DataOutputStream, isLegacy: Boolean) + } + + @JsonSingleton + object Empty : Detail() { + override val type: Type + get() = Type.NO_DETAIL + + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeByte(type.ordinal) + } + } + + // Item name - always one single item. QuestItem and QuestItemList are + // distinct due to how the surrounding text interacts with the parameter + // in the quest text. For a single item we might want to say "the " or + // "any ", whereas the text for QuestItemList is always a list, e.g. + // "<1 bandage, 3 apple>." + @JsonFactory + data class Item(val item: ItemDescriptor) : Detail() { + constructor(stream: DataInputStream, isLegacy: Boolean) : this( + if (isLegacy) ItemDescriptor(stream.readInternedString(), 1L, stream.readJsonElement() as JsonObject) else ItemDescriptor(stream)) + + override val type: Type + get() = Type.ITEM + + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeByte(type.ordinal) + + if (isLegacy) { + stream.writeBinaryString(item.name) + stream.writeJsonElement(item.parameters) + } else { + item.write(stream) + } + } + } + + // An item itemTag, indicating a set of possible items + @JsonFactory + data class ItemTag(val tag: String) : Detail() { + constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readInternedString()) + + override val type: Type + get() = Type.ITEM_TAG + + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeByte(type.ordinal) + stream.writeBinaryString(tag) + } + } + + // A collection of items + @JsonFactory + data class ItemList(val items: ImmutableList) : Detail() { + constructor(stream: DataInputStream, isLegacy: Boolean) : this(ImmutableList.copyOf(stream.readCollection { ItemDescriptor(this) })) + + override val type: Type + get() = Type.ITEM_LIST + + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeByte(type.ordinal) + stream.writeCollection(items) { it.write(this) } + } + } + + // The uniqueId of a specific entity + @JsonFactory + data class Entity(val uniqueId: String? = null, val species: String? = null, val gender: Gender? = null) : Detail() { + constructor(stream: DataInputStream, isLegacy: Boolean) : this( + if (stream.readBoolean()) stream.readInternedString() else null, + if (stream.readBoolean()) stream.readInternedString() else null, + if (stream.readBoolean()) Gender.entries[stream.readUnsignedByte()] else null, + ) + + override val type: Type + get() = Type.ENTITY + + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeByte(type.ordinal) + + stream.writeBoolean(uniqueId != null) + if (uniqueId != null) stream.writeBinaryString(uniqueId) + + stream.writeBoolean(species != null) + if (species != null) stream.writeBinaryString(species) + + stream.writeBoolean(gender != null) + if (gender != null) stream.writeByte(gender.ordinal) + } + } + + // A location within the world, which could represent a spawn point or a dungeon + @JsonFactory + data class Location(val uniqueId: String? = null, val region: Vector2d) : Detail() { + constructor(stream: DataInputStream, isLegacy: Boolean) : this( + if (stream.readBoolean()) stream.readInternedString() else null, + if (isLegacy) stream.readVector2f().toDoubleVector() else stream.readVector2d(), + ) + + override val type: Type + get() = Type.LOCATION + + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeByte(type.ordinal) + stream.writeBoolean(uniqueId != null) + if (uniqueId != null) + stream.writeBinaryString(uniqueId) + + if (isLegacy) + stream.writeStruct2f(region.toFloatVector()) + else + stream.writeStruct2d(region) + } + } + + @JsonFactory + data class MonsterType(val typeName: String, val parameters: JsonObject = JsonObject()) : Detail() { + constructor(stream: DataInputStream, isLegacy: Boolean) : this( + stream.readInternedString(), + stream.readJsonObject(), + ) + + override val type: Type + get() = Type.MONSTER_TYPE + + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeByte(type.ordinal) + stream.writeBinaryString(typeName) + stream.writeJsonObject(parameters) + } + } + + @JsonFactory + data class NpcType(val species: String, val typeName: String, val parameters: JsonObject = JsonObject(), val seed: Long? = null) : Detail() { + constructor(stream: DataInputStream, isLegacy: Boolean) : this( + stream.readInternedString(), + stream.readInternedString(), + stream.readJsonObject(), + if (stream.readBoolean()) stream.readLong() else null, + ) + + override val type: Type + get() = Type.NPC_TYPE + + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeByte(type.ordinal) + stream.writeBinaryString(species) + stream.writeBinaryString(typeName) + stream.writeJsonObject(parameters) + stream.writeBoolean(seed != null) + if (seed != null) stream.writeLong(seed) + } + } + + @JsonFactory + data class Coordinate(val coordinate: UniversePos) : Detail() { + constructor(stream: DataInputStream, isLegacy: Boolean) : this(UniversePos(stream, isLegacy)) + + override val type: Type + get() = Type.COORDINATE + + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeByte(type.ordinal) + coordinate.write(stream, isLegacy) + } + } + + class JsonData(val json: JsonObject) : Detail() { + override val type: Type + get() = Type.JSON + + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeJsonElement(json) + } + } + + class Adapter(gson: Gson) : TypeAdapter() { + private val details = DETAIL_ADAPTER.create(gson, TypeToken.get(Detail::class.java))!! + private val objects = gson.getAdapter(JsonObject::class.java) + + override fun write(out: JsonWriter, value: QuestParameter?) { + if (value == null) + out.nullValue() + else { + val base = if (value.detail is JsonData) { + value.detail.json.deepCopy() + } else { + details.toJsonTree(value.detail) as JsonObject + } + + if (value.name != null) base["name"] = value.name + if (value.portrait != null) base["portrait"] = value.portrait + if (value.indicator != null) base["indicator"] = value.indicator + out.value(base) + } + } + + override fun read(`in`: JsonReader): QuestParameter? { + if (`in`.consumeNull()) + return null + + val read = objects.read(`in`) + + val detail = if (read["type"]?.asString == "json") { + JsonData(read) + } else { + details.fromJsonTree(read) + } + + val name = read["name"]?.asStringOrNull + val portrait = read["portrait"]?.coalesceNull + val indicator = read["indicator"]?.asStringOrNull + return QuestParameter(detail, name, portrait, indicator) + } + } + + companion object { + val CODEC = nativeCodec(::QuestParameter, QuestParameter::write) + val LEGACY_CODEC = legacyCodec(::QuestParameter, QuestParameter::write) + private val DETAIL_ADAPTER = DispatchingAdapter("type", { type }, { token }, Type.entries) + + fun readDetail(stream: DataInputStream, isLegacy: Boolean): Detail { + return when (Type.entries[stream.readUnsignedByte()]) { + Type.NO_DETAIL -> Empty + Type.ITEM -> Item(stream, isLegacy) + Type.ITEM_TAG -> ItemTag(stream, isLegacy) + Type.ITEM_LIST -> ItemList(stream, isLegacy) + Type.ENTITY -> Entity(stream, isLegacy) + Type.LOCATION -> Location(stream, isLegacy) + Type.MONSTER_TYPE -> MonsterType(stream, isLegacy) + Type.NPC_TYPE -> NpcType(stream, isLegacy) + Type.COORDINATE -> Coordinate(stream, isLegacy) + Type.JSON -> JsonData(stream.readJsonElement() as JsonObject) + } + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDamageConfig.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDamageConfig.kt index af2fe1e9..d22fdd48 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDamageConfig.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDamageConfig.kt @@ -1,8 +1,7 @@ package ru.dbotthepony.kstarbound.defs.tile import com.google.common.collect.ImmutableMap -import it.unimi.dsi.fastutil.objects.Object2DoubleMap -import it.unimi.dsi.fastutil.objects.Object2DoubleMaps +import it.unimi.dsi.fastutil.objects.ObjectArraySet import org.apache.logging.log4j.LogManager import ru.dbotthepony.kstarbound.json.builder.JsonFactory @@ -29,6 +28,32 @@ data class TileDamageConfig( return (damageFactorsMapped[damage.type] ?: 1.0) * damage.amount } + operator fun plus(other: TileDamageConfig): TileDamageConfig { + val damageRecovery = damageRecovery + other.damageRecovery + val maximumEffectTime = maximumEffectTime.coerceAtLeast(other.maximumEffectTime) + val totalHealth = totalHealth + other.totalHealth + val harvestLevel = harvestLevel.coerceAtLeast(other.harvestLevel) + + // TODO: in original code calculation of damage factors appears to be wrong + // TODO: due to copy-paste error in for() loop argument + // So this code is a little different + val combinedKeys = ObjectArraySet() + combinedKeys.addAll(damageFactors.keys) + combinedKeys.addAll(other.damageFactors.keys) + + val builder = ImmutableMap.Builder() + + for (key in combinedKeys) { + if (key in damageFactors && key in other.damageFactors) { + builder.put(key, totalHealth / ((this.totalHealth / (damageFactors[key] ?: 0.0) + other.totalHealth / (other.damageFactors[key] ?: 0.0)))) + } else { + builder.put(key, damageFactors[key] ?: other.damageFactors[key]!!) + } + } + + return TileDamageConfig(builder.build(), damageRecovery, maximumEffectTime, totalHealth, harvestLevel) + } + companion object { val EMPTY = TileDamageConfig() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDamageType.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDamageType.kt index 13c1b71f..15433ecc 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDamageType.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDamageType.kt @@ -2,19 +2,19 @@ package ru.dbotthepony.kstarbound.defs.tile import ru.dbotthepony.kstarbound.json.builder.IStringSerializable -enum class TileDamageType(override val jsonName: String) : IStringSerializable { +enum class TileDamageType(override val jsonName: String, val isPenetrating: Boolean) : IStringSerializable { // Damage done that will not actually kill the target - PROTECTED("protected"), + PROTECTED("protected", false), // Best at chopping down trees, things made of wood, etc. - PLANT("plantish"), + PLANT("plantish", false), // For digging / drilling through materials - BLOCK("blockish"), + BLOCK("blockish", false), // Gravity gun etc - BEAM("beamish"), + BEAM("beamish", false), // Penetrating damage done passivly by explosions. - EXPLOSIVE("explosive"), + EXPLOSIVE("explosive", true), // Can melt certain block types - FIRE("fire"), + FIRE("fire", false), // Can "till" certain materials into others - TILLING("tilling"); + TILLING("tilling", false); } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDefinition.kt index 923fdb47..a778b866 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDefinition.kt @@ -45,6 +45,10 @@ data class TileDefinition( override val renderTemplate: AssetReference, override val renderParameters: RenderParameters, ) : IRenderableTile, IThingWithDescription by descriptionData { + init { + require(materialId > 0) { "Invalid tile ID $materialId" } + } + val actualDamageTable: TileDamageConfig by lazy { val dmg = damageTable.value ?: TileDamageConfig.EMPTY diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileModifierDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileModifierDefinition.kt index 535d0bd6..aa65da36 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileModifierDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileModifierDefinition.kt @@ -1,6 +1,7 @@ package ru.dbotthepony.kstarbound.defs.tile import com.google.common.collect.ImmutableList +import ru.dbotthepony.kstarbound.GlobalDefaults import ru.dbotthepony.kstarbound.defs.AssetReference import ru.dbotthepony.kstarbound.defs.IThingWithDescription import ru.dbotthepony.kstarbound.defs.ThingDescription @@ -12,8 +13,8 @@ data class TileModifierDefinition( val modId: Int, val modName: String, val itemDrop: String? = null, - val health: Double = 0.0, - val harvestLevel: Double = 0.0, + val health: Double? = null, + val requiredHarvestLevel: Int? = null, val breaksWithTile: Boolean = true, val grass: Boolean = false, val miningParticle: String? = null, @@ -21,6 +22,9 @@ data class TileModifierDefinition( val footstepSound: String? = null, val miningSounds: ImmutableList = ImmutableList.of(), + @Deprecated("", replaceWith = ReplaceWith("this.actualDamageTable")) + val damageTable: AssetReference = AssetReference(GlobalDefaults::tileDamage), + @JsonFlat val descriptionData: ThingDescription, @@ -28,6 +32,20 @@ data class TileModifierDefinition( override val renderParameters: RenderParameters ) : IRenderableTile, IThingWithDescription by descriptionData { init { - require(modId > 0) { "Invalid material modifier ID $modId" } + require(modId > 0) { "Invalid tile modifier ID $modId" } + } + + val actualDamageTable: TileDamageConfig by lazy { + val dmg = damageTable.value ?: TileDamageConfig.EMPTY + + return@lazy if (health == null && requiredHarvestLevel == null) { + dmg + } else if (health != null && requiredHarvestLevel != null) { + dmg.copy(totalHealth = health, harvestLevel = requiredHarvestLevel) + } else if (health != null) { + dmg.copy(totalHealth = health) + } else { + dmg.copy(harvestLevel = requiredHarvestLevel!!) + } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TerrestrialWorldParameters.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TerrestrialWorldParameters.kt index bc346df0..f703f654 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TerrestrialWorldParameters.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TerrestrialWorldParameters.kt @@ -23,6 +23,7 @@ import ru.dbotthepony.kstarbound.collect.WeightedList import ru.dbotthepony.kstarbound.defs.JsonDriven import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.json.mergeJson import ru.dbotthepony.kstarbound.json.pairAdapter import ru.dbotthepony.kstarbound.json.stream import ru.dbotthepony.kstarbound.util.binnedChoice @@ -276,8 +277,8 @@ class TerrestrialWorldParameters : VisitableWorldParameters() { fun generate(typeName: String, sizeName: String, random: RandomGenerator): TerrestrialWorldParameters { val config = GlobalDefaults.terrestrialWorlds.planetDefaults.deepCopy() - JsonDriven.mergeNoCopy(config, GlobalDefaults.terrestrialWorlds.planetSizes[sizeName] ?: throw NoSuchElementException("Unknown world size name $sizeName")) - JsonDriven.mergeNoCopy(config, GlobalDefaults.terrestrialWorlds.planetTypes[typeName] ?: throw NoSuchElementException("Unknown world type name $typeName")) + mergeJson(config, GlobalDefaults.terrestrialWorlds.planetSizes[sizeName] ?: throw NoSuchElementException("Unknown world size name $sizeName")) + mergeJson(config, GlobalDefaults.terrestrialWorlds.planetTypes[typeName] ?: throw NoSuchElementException("Unknown world type name $typeName")) val params = Starbound.gson.fromJson(config, Generic::class.java) @@ -352,7 +353,7 @@ class TerrestrialWorldParameters : VisitableWorldParameters() { fun makeRegion(name: String, baseHeight: Int): Pair { val primaryRegionJson = GlobalDefaults.terrestrialWorlds.regionDefaults.deepCopy() - JsonDriven.mergeNoCopy(primaryRegionJson, GlobalDefaults.terrestrialWorlds.regionTypes[name]!!) + mergeJson(primaryRegionJson, GlobalDefaults.terrestrialWorlds.regionTypes[name]!!) val region = readRegion(primaryRegionJson, baseHeight) val subRegionList = primaryRegionJson.getArray("subRegion") @@ -361,7 +362,7 @@ class TerrestrialWorldParameters : VisitableWorldParameters() { primaryRegionJson } else { val result = GlobalDefaults.terrestrialWorlds.regionDefaults.deepCopy() - JsonDriven.mergeNoCopy(result, GlobalDefaults.terrestrialWorlds.regionTypes[subRegionList.random(random).asString]!!) + mergeJson(result, GlobalDefaults.terrestrialWorlds.regionTypes[subRegionList.random(random).asString]!!) result }, baseHeight) @@ -372,8 +373,7 @@ class TerrestrialWorldParameters : VisitableWorldParameters() { if (layerName !in layers) return null - val layerConfig = config.getObject("layerDefaults").deepCopy() - JsonDriven.mergeNoCopy(layerConfig, layers.getObject(layerName)) + val layerConfig = mergeJson(config.getObject("layerDefaults").deepCopy(), layers.getObject(layerName)) if (!layerConfig.get("enabled", false)) return null diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/Streams.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/Streams.kt index 8c558aeb..b96d2627 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/io/Streams.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/Streams.kt @@ -10,6 +10,7 @@ import ru.dbotthepony.kommons.io.readLong import ru.dbotthepony.kommons.io.readSignedVarInt import ru.dbotthepony.kommons.io.readVector2d import ru.dbotthepony.kommons.io.readVector2f +import ru.dbotthepony.kommons.io.writeBinaryString import ru.dbotthepony.kommons.io.writeDouble import ru.dbotthepony.kommons.io.writeFloat import ru.dbotthepony.kommons.io.writeLong @@ -18,6 +19,7 @@ import ru.dbotthepony.kommons.io.writeStruct2d import ru.dbotthepony.kommons.io.writeStruct2f import ru.dbotthepony.kommons.math.RGBAColor import ru.dbotthepony.kommons.util.AABB +import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2i @@ -25,6 +27,7 @@ import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.world.ChunkPos import java.io.DataInput import java.io.DataOutput +import java.io.EOFException import java.io.IOException import java.io.InputStream import java.io.OutputStream @@ -111,3 +114,65 @@ fun OutputStream.writeAABB(value: AABB) { writeStruct2d(value.mins) writeStruct2d(value.maxs) } + +private fun InputStream.readBoolean(): Boolean { + val read = read() + + if (read == -1) + throw EOFException("End of stream reached") + + return read != 0 +} + +private fun InputStream.readUnsignedByte(): Int { + val read = read() + + if (read == -1) + throw EOFException("End of stream reached") + + return read +} + +fun S.readNullable(reader: S.() -> T): T? { + if (readBoolean()) + return reader(this) + else + return null +} + +fun S.writeNullable(value: T?, writer: S.(T) -> Unit) { + if (value == null) + write(0) + else { + write(1) + writer(value) + } +} + +fun S.readMVariant2(left: S.() -> L, right: S.() -> R): Either? { + return when (val type = readUnsignedByte()) { + 0 -> null + 1 -> Either.left(left()) + 2 -> Either.right(right()) + else -> throw IllegalArgumentException("Unknown variant type $type") + } +} + +fun S.writeMVariant2(value: Either?, left: S.(L) -> Unit, right: S.(R) -> Unit) { + write(if (value == null) 0 else if (value.isLeft) 1 else 2) + value?.map({ left(it) }, { right(it) }) +} + +fun InputStream.readNullableString() = readNullable { readInternedString() } +fun InputStream.readNullableFloat() = readNullable { readFloat() } +fun InputStream.readNullableDouble() = readNullable { readDouble() } +fun InputStream.readNullableDouble(isLegacy: Boolean) = readNullable { if (isLegacy) readFloat().toDouble() else readDouble() } +fun InputStream.readDouble(isLegacy: Boolean) = if (isLegacy) readFloat().toDouble() else readDouble() +fun InputStream.readVector2d(isLegacy: Boolean) = if (isLegacy) readVector2f().toDoubleVector() else readVector2d() + +fun OutputStream.writeNullableString(value: String?) = writeNullable(value) { writeBinaryString(it) } +fun OutputStream.writeNullableFloat(value: Float?) = writeNullable(value) { writeFloat(it) } +fun OutputStream.writeNullableDouble(value: Double?) = writeNullable(value) { writeDouble(it) } +fun OutputStream.writeNullableDouble(value: Double?, isLegacy: Boolean) = writeNullable(value) { if (isLegacy) writeFloat(it.toFloat()) else writeDouble(it) } +fun OutputStream.writeDouble(value: Double, isLegacy: Boolean) = if (isLegacy) writeFloat(value.toFloat()) else writeDouble(value) +fun OutputStream.writeStruct2d(value: IStruct2d, isLegacy: Boolean) = if (isLegacy) { writeFloat(value.component1().toFloat()); writeFloat(value.component2().toFloat()) } else { writeDouble(value.component1()); writeDouble(value.component2()) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonAdapterTypeFactory.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonAdapterTypeFactory.kt new file mode 100644 index 00000000..25bb9e0d --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonAdapterTypeFactory.kt @@ -0,0 +1,55 @@ +package ru.dbotthepony.kstarbound.json + +import com.google.gson.Gson +import com.google.gson.TypeAdapter +import com.google.gson.TypeAdapterFactory +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken + +object JsonAdapterTypeFactory : TypeAdapterFactory { + private fun wrap(input: Any, annotation: JsonAdapter): TypeAdapter { + input as TypeAdapter + + if (annotation.nullSafe) { + return NullSafeTypeAdapter(input) + } else { + return input + } + } + + override fun create(gson: Gson, type: TypeToken): TypeAdapter? { + val annotation = type.rawType.getAnnotation(JsonAdapter::class.java) ?: return null + + val constructor0 = try { + annotation.value.java.getDeclaredConstructor(Gson::class.java, TypeToken::class.java) + } catch (err: NoSuchMethodException) { + null + } + + if (constructor0 != null) { + return wrap(constructor0.newInstance(gson, type), annotation) + } + + val constructor1 = try { + annotation.value.java.getDeclaredConstructor(Gson::class.java) + } catch (err: NoSuchMethodException) { + null + } + + if (constructor1 != null) { + return wrap(constructor1.newInstance(gson), annotation) + } + + val constructor2 = try { + annotation.value.java.getDeclaredConstructor() + } catch (err: NoSuchMethodException) { + null + } + + if (constructor2 != null) { + return wrap(constructor2.newInstance(), annotation) + } + + throw RuntimeException("${type.rawType} has @JsonAdapter annotation, but it doesn't reference legal class ${annotation.value}") + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonImplementationTypeFactory.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonImplementationTypeFactory.kt new file mode 100644 index 00000000..c28b1df9 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonImplementationTypeFactory.kt @@ -0,0 +1,19 @@ +package ru.dbotthepony.kstarbound.json + +import com.google.gson.Gson +import com.google.gson.TypeAdapter +import com.google.gson.TypeAdapterFactory +import com.google.gson.reflect.TypeToken +import ru.dbotthepony.kstarbound.json.builder.JsonImplementation + +object JsonImplementationTypeFactory : TypeAdapterFactory { + override fun create(gson: Gson, type: TypeToken): TypeAdapter? { + val delegate = type.rawType.getAnnotation(JsonImplementation::class.java) + + if (delegate != null) { + return gson.getAdapter(delegate.implementingClass.java) as TypeAdapter? + } + + return null + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonPath.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonPath.kt new file mode 100644 index 00000000..96ea3940 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonPath.kt @@ -0,0 +1,260 @@ +package ru.dbotthepony.kstarbound.json + +import com.google.common.collect.ImmutableList +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import it.unimi.dsi.fastutil.objects.ObjectArrayList +import ru.dbotthepony.kommons.gson.contains +import ru.dbotthepony.kommons.util.KOptional + +fun jsonQuery(path: String) = JsonPath.query(path) +fun jsonPointer(path: String) = JsonPath.pointer(path) + +class JsonPath private constructor(val pieces: ImmutableList) { + constructor(path: String) : this(ImmutableList.of(Piece(path))) + + enum class Hint { + OBJECT, ARRAY; + } + + data class Piece(val value: String, val hint: Hint? = null) { + val int by lazy { value.toIntOrNull() } + } + + class TraversalException(message: String) : Exception(message) + + override fun equals(other: Any?): Boolean { + return this === other || other is JsonPath && pieces == other.pieces + } + + override fun hashCode(): Int { + return pieces.hashCode() + } + + override fun toString(): String { + if (pieces.isEmpty()) + return "" + else + return "/${pieces.joinToString("/") { it.value }}" + } + + fun startsWith(name: String): Boolean { + if (isEmpty) + return true + + return pieces[0].value == name + } + + val isEmpty: Boolean + get() = pieces.isEmpty() + + val hasParent: Boolean + get() = pieces.isNotEmpty() + + fun parent(): JsonPath { + require(hasParent) { "$this has no parent" } + val build = ObjectArrayList(pieces) + build.removeLast() + return JsonPath(ImmutableList.copyOf(build)) + } + + fun child(key: String): JsonPath { + val build = ObjectArrayList(pieces) + build.add(Piece(key)) + return JsonPath(ImmutableList.copyOf(build)) + } + + private fun reconstructPath(at: Int): String { + if (at == 0) + return "" + else + return "/${pieces.joinToString("/", limit = at, truncated = "") { it.value }}" + } + + /** + * Attempts to find given element along path, if can't, throws [TraversalException] + */ + fun get(element: JsonElement): JsonElement { + if (isEmpty) { + return element + } + + var current = element + + for ((i, piece) in pieces.withIndex()) { + if (current is JsonObject) { + if (piece.value !in current) { + throw TraversalException("Path at ${reconstructPath(i)} points at non-existing element") + } + + current = current[piece.value]!! + + if (i != pieces.size - 1 && current !is JsonObject && current !is JsonArray) { + throw TraversalException("Path at ${reconstructPath(i)} expects to get index-able element from underlying object (for ${pieces[i + 1]}), but there is $current") + } + } else if (current is JsonArray) { + if (piece.value == "-") { + throw TraversalException("Path at ${reconstructPath(i)} points at non-existent index") + } + + val key = piece.int + + if (key == null) { + throw TraversalException("Path at ${reconstructPath(i)} can not index an array") + } else if (key < 0) { + throw TraversalException("Path at ${reconstructPath(i)} points at pseudo index") + } + + try { + current = current[key] + } catch (err: IndexOutOfBoundsException) { + throw TraversalException("Path at ${reconstructPath(i)} points at non-existing index") + } + + if (i != pieces.size - 1 && current !is JsonObject && current !is JsonArray) { + throw TraversalException("Path at ${reconstructPath(i)} expects to get index-able element from underlying array (for ${pieces[i + 1]}), but there is $current") + } + } else { + throw TraversalException("Path at ${reconstructPath(i)} can not index $current") + } + } + + return current + } + + /** + * Attempts to find element along path, if can't, returns null + */ + fun find(element: JsonElement): JsonElement? { + if (isEmpty) { + return element + } + + var current = element + + for (piece in pieces) { + if (current is JsonObject) { + if (piece.value !in current) { + return null + } + + current = current[piece.value] + } else if (current is JsonArray) { + if (piece.value == "-") { + return null + } + + val key = piece.int + + if (key == null || key < 0 || key >= current.size()) { + return null + } + + current = current[key] + } else { + return null + } + } + + return current + } + + fun get(element: JsonElement, orElse: () -> JsonElement): JsonElement { + return find(element) ?: orElse.invoke() + } + + fun get(element: JsonElement, orElse: JsonElement): JsonElement { + return find(element) ?: orElse + } + + companion object { + val EMPTY = JsonPath(ImmutableList.of()) + + // https://datatracker.ietf.org/doc/html/rfc6901 + @JvmStatic + fun pointer(path: String): JsonPath { + if (path == "") + return EMPTY + + val split = path.split('/') + + if (split.first() != "") { + throw IllegalArgumentException("Invalid JSON pointer: $path") + } + + val pieces = ImmutableList.Builder() + + for (i in 1 until split.size) { + pieces.add(Piece(split[i].replace("~1", "/").replace("~0", "~"))) + } + + return JsonPath(pieces.build()) + } + + /** + * JSON query path doesn't have a standard, + * Starbound goes with JavaScript-like query path + * + * But this parser is different, original parser will throw + * an exception when you put non-numbers into `[brackets]`, + * like this: + * + * `someValue.a[bb]` + * + * This allows to effectively escape dots (`.`), because + * when reading array index, everything is read up until closing bracket. + * + * Another change is - empty keys + * (sequences like `..a. .key.value` -> `"" -> "" -> "a" -> " " -> "key" -> "value"`) + * are allowed + */ + @JvmStatic + fun query(path: String): JsonPath { + val pieces = ArrayList() + + val current = StringBuilder() + var readingIndex = false + + for (char in path) { + if (readingIndex) { + if (char == ']') { + val finish = current.toString() + val finishInt = finish.toIntOrNull() + pieces.add(Piece(finish, if (finishInt != null) Hint.ARRAY else null)) + current.clear() + readingIndex = false + } else { + current.append(char) + } + } else { + if (char == '.') { + pieces.add(Piece(current.toString())) + current.clear() + } else if (char == '[') { + readingIndex = true + } else { + current.append(char) + } + } + } + + if (readingIndex) { + throw IllegalArgumentException("Expected closing ] in $path") + } + + if (current.isNotEmpty()) { + pieces.add(Piece(current.toString())) + } + + for ((i, piece) in pieces.withIndex()) { + if (piece.hint == null && i != pieces.size - 1) { + pieces[i] = Piece(piece.value, hint = Hint.OBJECT) + } + } + + return JsonPath(ImmutableList.copyOf(pieces)) + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonUtils.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonUtils.kt index 875fb4f3..f4165c4a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonUtils.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonUtils.kt @@ -8,15 +8,15 @@ 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.JsonSyntaxException import com.google.gson.TypeAdapter -import com.google.gson.TypeAdapterFactory import com.google.gson.reflect.TypeToken -import com.google.gson.stream.JsonReader -import com.google.gson.stream.JsonToken -import com.google.gson.stream.JsonWriter import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet +import ru.dbotthepony.kommons.gson.set +import ru.dbotthepony.kstarbound.Starbound + +inline fun Gson.getAdapter(): TypeAdapter { + return getAdapter(object : TypeToken() {}) +} inline fun , reified E> Gson.collectionAdapter(): TypeAdapter { return getAdapter(TypeToken.getParameterized(C::class.java, E::class.java)) as TypeAdapter @@ -53,3 +53,73 @@ inline fun Gson.pairSetAdapter(): TypeAdapter Gson.mutableSetAdapter(): TypeAdapter> { return collectionAdapter() } + +/** + * It is implied that [base] is a copy that can be modified + */ +fun mergeJson(base: JsonElement, with: JsonElement): JsonElement { + if (base is JsonObject && with is JsonObject) { + for ((k, v) in with.entrySet()) { + base[k] = mergeJson(base[k] ?: JsonNull.INSTANCE, v) + } + + return base + } else if (with.isJsonNull) { + return base + } else { + return with.deepCopy() + } +} + +fun mergeJson(base: JsonElement, with: Map): JsonElement { + if (base is JsonObject) { + for ((k, v) in with) { + base[k] = mergeJson(base[k] ?: JsonNull.INSTANCE, v) + } + + return base + } else { + return JsonObject().apply { + for ((k, v) in with) { + add(k, v.deepCopy()) + } + } + } +} + +fun mergeJson(base: JsonElement, with: JsonElement, vararg rest: JsonElement): JsonElement { + var base = mergeJson(base, with) + + for (v in rest) { + base = mergeJson(base, v) + } + + return base +} + +fun mergeJson(base: JsonObject, with: JsonElement): JsonObject { + check(mergeJson(base as JsonElement, with) === base) + return base +} + +fun mergeJson(base: JsonObject, with: JsonElement, vararg rest: JsonElement): JsonObject { + check(mergeJson(base as JsonElement, with) === base) + + for (v in rest) { + check(mergeJson(base as JsonElement, v) === base) + } + + return base +} + +fun jsonArrayOf(vararg elements: JsonElement): JsonArray { + val array = JsonArray(elements.size) + elements.forEach { array.add(it) } + return array +} + +fun jsonArrayOf(vararg elements: Any?): JsonArray { + val array = JsonArray(elements.size) + elements.forEach { array.add(Starbound.gson.toJsonTree(it)) } + return array +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/NullSafeTypeAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/NullSafeTypeAdapter.kt new file mode 100644 index 00000000..269cdca7 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/NullSafeTypeAdapter.kt @@ -0,0 +1,23 @@ +package ru.dbotthepony.kstarbound.json + +import com.google.gson.TypeAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter +import ru.dbotthepony.kommons.gson.consumeNull + +// instead of `.nullSafe()`, so tracebacks don't get polluted with anonymous class names +class NullSafeTypeAdapter(private val parent: TypeAdapter) : TypeAdapter() { + override fun write(out: JsonWriter, value: T?) { + if (value == null) + out.nullValue() + else + parent.write(out, value) + } + + override fun read(`in`: JsonReader): T? { + if (`in`.consumeNull()) + return null + + return parent.read(`in`) + } +} 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 bbbb590e..b0b284a8 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/Annotations.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/Annotations.kt @@ -1,9 +1,5 @@ package ru.dbotthepony.kstarbound.json.builder -import com.google.gson.Gson -import com.google.gson.TypeAdapter -import com.google.gson.TypeAdapterFactory -import com.google.gson.reflect.TypeToken import kotlin.reflect.KClass /** @@ -91,15 +87,3 @@ annotation class JsonImplementation(val implementingClass: KClass<*>) @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) annotation class JsonSingleton - -object JsonImplementationTypeFactory : TypeAdapterFactory { - override fun create(gson: Gson, type: TypeToken): TypeAdapter? { - val delegate = type.rawType.getAnnotation(JsonImplementation::class.java) - - if (delegate != null) { - return gson.getAdapter(delegate.implementingClass.java) as TypeAdapter? - } - - return null - } -} 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 4255a75e..a2c2446f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/FactoryAdapter.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/FactoryAdapter.kt @@ -484,7 +484,7 @@ class FactoryAdapter private constructor( companion object { private val LOGGER = LogManager.getLogger() - fun createFor(kclass: KClass, config: JsonFactory, gson: Gson, stringInterner: Interner = Starbound.STRINGS): TypeAdapter { + fun createFor(kclass: KClass, config: JsonFactory = JsonFactory(), gson: Gson, stringInterner: Interner = Starbound.STRINGS): TypeAdapter { val builder = Builder(kclass) val properties = kclass.declaredMembers.filterIsInstance>() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/math/Utils.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Utils.kt index 4a522e25..d290dc43 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/math/Utils.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Utils.kt @@ -1,5 +1,6 @@ package ru.dbotthepony.kstarbound.math +import kotlin.math.PI import kotlin.math.absoluteValue /** @@ -77,3 +78,25 @@ fun weakDoubleZeroing(value: Double, epsilon: Double = EPSILON): Double { return value } + +fun angleDifference(angle0: Double, angle1: Double): Double { + var diff = (angle1 - angle0 + PI) % (PI * 2.0) + + if (diff < 0.0) + diff += PI * 2.0 + + return diff +} + +fun normalizeAngle(angle: Double): Double { + var value = (angle + PI) % (PI * 2.0) + + if (value < 0.0) + value += PI * 2.0 + + return value +} + +fun approachAngle(target: Double, current: Double, limit: Double): Double { + return normalizeAngle(current + angleDifference(current, target).coerceIn(-limit, limit)) +} 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 ba235847..d6afbb8f 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 @@ -7,7 +7,7 @@ import java.io.DataInputStream import java.io.DataOutputStream class TileDamageUpdatePacket(val x: Int, val y: Int, val isBackground: Boolean, val health: TileHealth) : IClientPacket { - constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readInt(), stream.readInt(), stream.readBoolean(), TileHealth(stream, isLegacy)) + constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readInt(), stream.readInt(), stream.readBoolean(), TileHealth.Tile(stream, isLegacy)) override fun write(stream: DataOutputStream, isLegacy: Boolean) { stream.writeInt(x) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/Factories.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/Factories.kt index 43642c1c..08c50a08 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/Factories.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/Factories.kt @@ -20,6 +20,7 @@ import ru.dbotthepony.kommons.io.readVarInt import ru.dbotthepony.kommons.io.readVarLong import ru.dbotthepony.kommons.io.writeBinaryString import ru.dbotthepony.kommons.io.writeByteArray +import ru.dbotthepony.kommons.io.writeShort import ru.dbotthepony.kommons.io.writeVarInt import ru.dbotthepony.kommons.io.writeVarLong import ru.dbotthepony.kommons.math.RGBAColor @@ -59,6 +60,8 @@ val AABBCodecLegacy = StreamCodec.Impl(DataInputStream::readAABBLegacy, DataOutp val AABBCodecLegacyOptional = StreamCodec.Impl(DataInputStream::readAABBLegacyOptional, DataOutputStream::writeAABBLegacyOptional) val AABBCodecNative = StreamCodec.Impl(DataInputStream::readAABB, DataOutputStream::writeAABB) +val UnsignedShortCodec = StreamCodec.Impl(DataInputStream::readUnsignedShort, DataOutputStream::writeShort) + val ValidatingBooleanCodec = StreamCodec.Impl({ when (val read = it.readUnsignedByte()) { 0 -> false @@ -117,6 +120,7 @@ fun networkedAABBNullable(value: KOptional = KOptional()) = networkedData( // this is ugly because of invariant generics, but we must bear with it. fun networkedList(codec: StreamCodec): BasicNetworkedElement, List> = networkedData(ArrayList(), StreamCodec.Collection(codec, ::ArrayList)) as BasicNetworkedElement, List> +fun networkedList(codec: StreamCodec, legacyCodec: StreamCodec): BasicNetworkedElement, List> = networkedData(ArrayList(), StreamCodec.Collection(codec, ::ArrayList), StreamCodec.Collection(legacyCodec, ::ArrayList)) as BasicNetworkedElement, List> fun networkedItem(value: ItemStack = ItemStack.EMPTY) = NetworkedItemStack(value) fun networkedStatefulItem(value: ItemStack = ItemStack.EMPTY) = NetworkedStatefulItemStack(value) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedList.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedList.kt new file mode 100644 index 00000000..ff3ca10b --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedList.kt @@ -0,0 +1,343 @@ +package ru.dbotthepony.kstarbound.network.syncher + +import ru.dbotthepony.kommons.io.StreamCodec +import ru.dbotthepony.kommons.io.readVarInt +import ru.dbotthepony.kommons.io.writeVarInt +import ru.dbotthepony.kommons.util.KOptional +import ru.dbotthepony.kstarbound.collect.RandomListIterator +import ru.dbotthepony.kstarbound.collect.RandomSubList +import java.io.DataInputStream +import java.io.DataOutputStream + +// original engine does not have "networked list", so it is always networked +// the dumb way on legacy protocol +class NetworkedList( + val codec: StreamCodec, + val legacyCodec: StreamCodec = codec, + private val maxBacklogSize: Int = 100, + private val elementsFactory: (Int) -> MutableList = ::ArrayList +) : NetworkedElement(), MutableList { + private val backlog = ArrayDeque>>() + private val queue = ArrayDeque>>() + private val elements = elementsFactory(10) + + private enum class Type { + ADD, REMOVE, CLEAR; + } + + private data class Entry(val type: Type, val index: Int, val value: KOptional) { + constructor(index: Int) : this(Type.REMOVE, index, KOptional()) + constructor(index: Int, value: E) : this(Type.REMOVE, index, KOptional(value)) + + fun apply(list: MutableList) { + when (type) { + Type.ADD -> list.add(index, value.value) + Type.REMOVE -> list.removeAt(index) + Type.CLEAR -> list.clear() + } + } + } + + private val clearEntry = Entry(Type.CLEAR, 0, KOptional()) + private var isInterpolating = false + private var currentTime = 0.0 + private var isRemote = false + + private fun purgeBacklog() { + while (backlog.size >= maxBacklogSize) { + backlog.removeFirst() + } + } + + private fun latestState(): List { + if (queue.isEmpty()) { + return elements + } else { + val copy = elementsFactory(elements.size) + + for (v in elements) + copy.add(v) + + for ((_, e) in queue) + e.apply(copy) + + return copy + } + } + + override fun readInitial(data: DataInputStream, isLegacy: Boolean) { + isRemote = true + backlog.clear() + backlog.add(currentVersion() to clearEntry) + queue.clear() + elements.clear() + + val count = data.readVarInt() + + if (isLegacy) { + for (i in 0 until count) { + val read = legacyCodec.read(data) + elements.add(read) + backlog.add(currentVersion() to Entry(elements.size - 1, read)) + } + } else { + for (i in 0 until count) { + val read = codec.read(data) + elements.add(read) + backlog.add(currentVersion() to Entry(elements.size - 1, read)) + } + } + + purgeBacklog() + } + + override fun writeInitial(data: DataOutputStream, isLegacy: Boolean) { + val latest = latestState() + data.writeVarInt(latest.size) + + if (isLegacy) { + latest.forEach { legacyCodec.write(data, it) } + } else { + latest.forEach { codec.write(data, it) } + } + } + + override fun readDelta(data: DataInputStream, interpolationDelay: Double, isLegacy: Boolean) { + isRemote = true + + if (isLegacy) { + readInitial(data, true) + } else { + if (data.readBoolean()) { + var action = data.readUnsignedByte() + + while (action != 0) { + val entry = when (Type.entries[action - 1]) { + Type.ADD -> Entry(data.readVarInt(), codec.read(data)) + Type.REMOVE -> Entry(data.readVarInt()) + Type.CLEAR -> clearEntry + } + + if (isInterpolating) { + val actualTime = interpolationDelay + currentTime + + if (queue.isNotEmpty() && queue.last().first >= actualTime) { + queue.forEach { it.second.apply(elements) } + queue.clear() + } + + if (interpolationDelay > 0.0) + queue.add(actualTime to entry) + else + entry.apply(elements) + } else { + entry.apply(elements) + } + + backlog.add(currentVersion() to entry) + action = data.readUnsignedByte() + } + + purgeBacklog() + } else { + readInitial(data, false) + } + } + } + + override fun writeDelta(data: DataOutputStream, remoteVersion: Long, isLegacy: Boolean) { + if (isLegacy) { + writeInitial(data, true) + } else if (backlog.isNotEmpty() && remoteVersion < backlog.first().first) { + data.writeBoolean(false) + writeInitial(data, false) + } else { + data.writeBoolean(true) + + for ((version, entry) in backlog) { + if (version >= remoteVersion) { + data.writeByte(entry.type.ordinal + 1) + + when (entry.type) { + Type.ADD -> { data.writeVarInt(entry.index); codec.write(data, entry.value.value) } + Type.REMOVE -> { data.writeVarInt(entry.index) } + Type.CLEAR -> {} + } + } + } + + data.writeByte(0) + } + } + + override fun readBlankDelta(interpolationDelay: Double) {} + + override fun enableInterpolation(extrapolation: Double) { + isInterpolating = true + } + + override fun disableInterpolation() { + isInterpolating = false + queue.forEach { it.second.apply(elements) } + queue.clear() + } + + override fun tickInterpolation(delta: Double) { + currentTime += delta + + while (queue.isNotEmpty() && queue.first().first <= currentTime) { + queue.removeFirst().second.apply(elements) + } + } + + override fun hasChangedSince(version: Long): Boolean { + return backlog.isNotEmpty() && backlog.first().first >= version + } + + override val size: Int + get() = elements.size + + override fun contains(element: E): Boolean { + return elements.contains(element) + } + + override fun containsAll(elements: Collection): Boolean { + return this.elements.containsAll(elements) + } + + override fun get(index: Int): E { + return elements[index] + } + + override fun indexOf(element: E): Int { + return elements.indexOf(element) + } + + override fun isEmpty(): Boolean { + return elements.isEmpty() + } + + override fun iterator(): MutableIterator { + return listIterator() + } + + override fun lastIndexOf(element: E): Int { + return elements.lastIndexOf(element) + } + + override fun add(element: E): Boolean { + add(size, element) + return true + } + + override fun add(index: Int, element: E) { + check(!isRemote) { "List is not owned by this side" } + elements.add(index, element) + backlog.add(currentVersion() to Entry(index, element)) + purgeBacklog() + } + + override fun addAll(index: Int, elements: Collection): Boolean { + var newIndex = index + elements.forEach { add(newIndex++, it) } + return true + } + + override fun addAll(elements: Collection): Boolean { + return addAll(size, elements) + } + + override fun clear() { + check(!isRemote) { "List is not owned by this side" } + backlog.clear() + backlog.add(currentVersion() to clearEntry) + elements.clear() + } + + override fun listIterator(): MutableListIterator { + return listIterator(0) + } + + override fun listIterator(index: Int): MutableListIterator { + return RandomListIterator(this, index) + } + + override fun remove(element: E): Boolean { + check(!isRemote) { "List is not owned by this side" } + val indexOf = elements.indexOf(element) + + if (indexOf == -1) + return false + + removeAt(indexOf) + return true + } + + override fun removeAll(elements: Collection): Boolean { + check(!isRemote) { "List is not owned by this side" } + var any = false + elements.forEach { any = remove(it) || any } + return any + } + + override fun removeAt(index: Int): E { + check(!isRemote) { "List is not owned by this side" } + val element = elements.removeAt(index) + backlog.add(currentVersion() to Entry(index)) + purgeBacklog() + return element + } + + override fun retainAll(elements: Collection): Boolean { + check(!isRemote) { "List is not owned by this side" } + val itr = iterator() + var modified = false + + for (v in itr) { + if (v !in elements) { + itr.remove() + modified = true + } + } + + return modified + } + + override fun set(index: Int, element: E): E { + check(!isRemote) { "List is not owned by this side" } + val old = elements.set(index, element) + backlog.add(currentVersion() to Entry(index, element)) + purgeBacklog() + return old + } + + override fun subList(fromIndex: Int, toIndex: Int): MutableList { + return RandomSubList(this, fromIndex, toIndex) + } + + override fun equals(other: Any?): Boolean { + if (other === this) return true + if (other !is List<*>) return false + + val e1: ListIterator = listIterator() + val e2 = other.listIterator() + + while (e1.hasNext() && e2.hasNext()) { + val o1 = e1.next() + val o2 = e2.next() + if (!(if (o1 == null) o2 == null else o1 == o2)) return false + } + + return !(e1.hasNext() || e2.hasNext()) + } + + override fun hashCode(): Int { + var hashCode = 1 + for (e in this) hashCode = 31 * hashCode + e.hashCode() + return hashCode + } + + override fun toString(): String { + return "NetworkedList[${joinToString()}]" + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedMap.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedMap.kt index f8767715..d970136d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedMap.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedMap.kt @@ -5,8 +5,10 @@ import ru.dbotthepony.kommons.io.StreamCodec import ru.dbotthepony.kommons.io.readVarInt import ru.dbotthepony.kommons.io.writeVarInt import ru.dbotthepony.kommons.util.KOptional +import ru.dbotthepony.kommons.util.Listenable import java.io.DataInputStream import java.io.DataOutputStream +import java.util.concurrent.CopyOnWriteArrayList /** * [isDumb] is responsible for specifying whenever legacy protocol networks entire map each time @@ -57,6 +59,7 @@ class NetworkedMap( backlog.add(currentVersion() to clearEntry) purgeBacklog() + listeners.forEach { it.listener.onClear() } } override fun onValueAdded(key: K, value: V) { @@ -64,6 +67,7 @@ class NetworkedMap( check(!isRemote) { "This map is not owned by this side" } backlog.add(currentVersion() to Entry(Action.ADD, KOptional(nativeKey.copy(key)), KOptional(nativeValue.copy(value)))) purgeBacklog() + listeners.forEach { it.listener.onValueAdded(key, value) } } override fun onValueRemoved(key: K, value: V) { @@ -71,10 +75,31 @@ class NetworkedMap( check(!isRemote) { "This map is not owned by this side" } backlog.add(currentVersion() to Entry(Action.REMOVE, KOptional(nativeKey.copy(key)), KOptional())) purgeBacklog() + listeners.forEach { it.listener.onValueRemoved(key, value) } } }) } + private val listeners = CopyOnWriteArrayList() + + private inner class Listener(val listener: ListenableMap.MapListener) : Listenable.L { + init { + listeners.add(this) + } + + override fun remove() { + listeners.remove(this) + } + } + + fun addListener(listener: ListenableMap.MapListener): Listenable.L { + return Listener(listener) + } + + fun addListener(listener: Runnable): Listenable.L { + return Listener(ListenableMap.RunnableAdapter(listener)) + } + private val dumbCodec by lazy { StreamCodec.Map(keyCodec.second, valueCodec.second, ::HashMap) } @@ -253,7 +278,7 @@ class NetworkedMap( val change = if (isLegacy) readLegacyEntry(data) else readNativeEntry(data) backlog.add(currentVersion() to change) - if (isInterpolating && interpolationDelay > 0.0) { + if (isInterpolating) { val actualDelay = interpolationDelay + currentTime if (delayed.isNotEmpty() && delayed.last().first > actualDelay) { @@ -261,7 +286,10 @@ class NetworkedMap( delayed.clear() } - delayed.add(actualDelay to change) + if (interpolationDelay > 0.0) + delayed.add(actualDelay to change) + else + change.apply(this) } else { change.apply(this) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/player/Avatar.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/player/Avatar.kt index 044c34c5..4fe60101 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/player/Avatar.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/player/Avatar.kt @@ -4,12 +4,12 @@ import com.google.gson.JsonElement import com.google.gson.JsonObject import com.google.gson.JsonPrimitive import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap -import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet import ru.dbotthepony.kommons.guava.immutableMap import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.quest.QuestDescriptor import ru.dbotthepony.kstarbound.defs.actor.player.TechDefinition import ru.dbotthepony.kstarbound.lua.NewLuaState import ru.dbotthepony.kstarbound.lua.luaFunction @@ -294,9 +294,10 @@ class Avatar(val uniqueId: UUID) { val questId = value["questId"]?.asString ?: throw IllegalArgumentException("Invalid 'questId' in quest descriptor") val templateId = value["templateId"]?.asString ?: questId val params = value["parameters"] as? JsonObject ?: JsonObject() - val quest = QuestInstance(this, descriptor = QuestDescriptor(questId, templateId, seed, params), serverID = serverID?.let(UUID::fromString), worldID = worldID) - addQuest(quest) - return quest.id + //val quest = QuestInstance(this, descriptor = QuestDescriptor(questId, templateId, params, seed), serverID = serverID?.let(UUID::fromString), worldID = worldID) + //addQuest(quest) + //return quest.id + TODO() } else { throw IllegalArgumentException("Invalid quest descriptor: $value") } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/player/QuestDescriptor.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/player/QuestDescriptor.kt deleted file mode 100644 index c1dd6a25..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/player/QuestDescriptor.kt +++ /dev/null @@ -1,18 +0,0 @@ -package ru.dbotthepony.kstarbound.player - -import com.google.gson.JsonObject -import ru.dbotthepony.kstarbound.json.builder.JsonFactory - -@JsonFactory -data class QuestDescriptor( - val questId: String, - val templateId: String = questId, - val seed: Long = makeSeed(), - val parameters: JsonObject = JsonObject() -) { - companion object { - fun makeSeed(): Long { - return System.nanoTime().rotateLeft(27).xor(System.currentTimeMillis()) - } - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/player/QuestInstance.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/player/QuestInstance.kt index 09208c5a..e7444b16 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/player/QuestInstance.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/player/QuestInstance.kt @@ -3,7 +3,6 @@ package ru.dbotthepony.kstarbound.player import com.google.gson.JsonArray import com.google.gson.JsonElement import com.google.gson.JsonObject -import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap import org.apache.logging.log4j.LogManager import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Starbound @@ -11,6 +10,7 @@ import ru.dbotthepony.kstarbound.defs.quest.QuestTemplate import ru.dbotthepony.kstarbound.lua.NewLuaState import ru.dbotthepony.kstarbound.item.ItemStack import ru.dbotthepony.kommons.gson.set +import ru.dbotthepony.kstarbound.defs.quest.QuestDescriptor import java.util.HashMap import java.util.UUID @@ -46,7 +46,7 @@ class QuestInstance( var compassDirection: Double? = null private val portraits = JsonObject() - private val params = descriptor.parameters.deepCopy() + //private val params = descriptor.parameters.deepCopy() private val portraitTitles = HashMap() @@ -78,9 +78,9 @@ class QuestInstance( } init { - for ((k, v) in descriptor.parameters.entrySet()) { - params[k] = v.deepCopy() - } + //for ((k, v) in descriptor.parameters.entrySet()) { + // params[k] = v.deepCopy() + //} } companion object { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt index 766ee4a7..2b50d25d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt @@ -159,7 +159,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn if (world == null) { send(PlayerWarpResultPacket(false, request, false)) } else { - currentWarpStatus = world.acceptClient(this).exceptionally { + currentWarpStatus = world.acceptClient(this, request).exceptionally { send(PlayerWarpResultPacket(false, request, false)) if (world == shipWorld) { @@ -244,7 +244,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn enqueueWarp(WarpAlias.OwnShip) warpingAllowed = true - if (server.channels.connections.size == 2) { + if (server.channels.connections.size > 1) { enqueueWarp(WarpAction.Player(server.channels.connections.first().uuid!!)) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyWorldStorage.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyWorldStorage.kt index 81f28209..5c676b56 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyWorldStorage.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyWorldStorage.kt @@ -18,7 +18,7 @@ import ru.dbotthepony.kstarbound.world.api.AbstractCell import ru.dbotthepony.kstarbound.world.api.ImmutableCell import ru.dbotthepony.kstarbound.world.api.MutableCell import ru.dbotthepony.kstarbound.world.entities.AbstractEntity -import ru.dbotthepony.kstarbound.world.entities.WorldObject +import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject import java.io.BufferedInputStream import java.io.ByteArrayInputStream import java.io.Closeable 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 c5db6ce1..209e17a8 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt @@ -5,17 +5,22 @@ import it.unimi.dsi.fastutil.ints.IntArraySet import it.unimi.dsi.fastutil.longs.Long2ObjectFunction import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet +import it.unimi.dsi.fastutil.objects.ObjectArraySet import org.apache.logging.log4j.LogManager +import ru.dbotthepony.kommons.util.AABBi import ru.dbotthepony.kommons.util.IStruct2i import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.WarpAction import ru.dbotthepony.kstarbound.defs.WorldID import ru.dbotthepony.kstarbound.defs.tile.TileDamage import ru.dbotthepony.kstarbound.defs.tile.TileDamageResult +import ru.dbotthepony.kstarbound.defs.tile.TileDamageType import ru.dbotthepony.kstarbound.defs.world.WorldStructure import ru.dbotthepony.kstarbound.defs.world.WorldTemplate import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.json.jsonArrayOf import ru.dbotthepony.kstarbound.network.IPacket import ru.dbotthepony.kstarbound.network.packets.StepUpdatePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.PlayerWarpResultPacket @@ -30,6 +35,8 @@ import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.WorldGeometry import ru.dbotthepony.kstarbound.world.api.ImmutableCell import ru.dbotthepony.kstarbound.world.entities.AbstractEntity +import ru.dbotthepony.kstarbound.world.entities.tile.TileEntity +import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject import java.util.concurrent.CompletableFuture import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.RejectedExecutionException @@ -167,12 +174,63 @@ class ServerWorld private constructor( if (damage.amount <= 0.0) return TileDamageResult.NONE - val actualPositions = positions.stream().map { geometry.wrap(it) }.distinct().toList() + val actualPositions = positions.stream() + .map { geometry.wrap(it) } + .distinct() + .map { it to chunkMap[geometry.chunkFromCell(it)] } + .toList() + var topMost = TileDamageResult.NONE - for (pos in actualPositions) { - val chunk = chunkMap[geometry.chunkFromCell(pos)] ?: continue - topMost = topMost.coerceAtLeast(chunk.damageTile(pos - chunk.pos.tile, isBackground, sourcePosition, damage, source)) + val damagedEntities = ObjectArraySet() + + for ((pos, chunk) in actualPositions) { + var damage = damage + var tileEntityResult = TileDamageResult.NONE + + if (chunk?.getCell(pos - chunk.pos.tile)?.dungeonId in protectedDungeonIDs) + damage = damage.copy(type = TileDamageType.PROTECTED) + + if (!isBackground) { + for (entity in entitiesAtTile(pos, distinct = false)) { + if (!damagedEntities.add(entity)) continue + + val occupySpaces = entity.occupySpaces.stream() + .map { geometry.wrap(it + entity.tilePosition) } + .filter { it in positions } + .toList() + + val broken = entity.damage(occupySpaces, sourcePosition, damage) + + if (source != null && broken) { + source.receiveMessage("tileEntityBroken", jsonArrayOf(pos, entity.type.jsonName, (entity as? WorldObject)?.config?.key)) + } + + if (damage.type == TileDamageType.PROTECTED) + tileEntityResult = TileDamageResult.PROTECTED + else + tileEntityResult = TileDamageResult.NORMAL + } + } + + // Penetrating damage should carry through to the blocks behind this + // entity. + if (tileEntityResult == TileDamageResult.NONE || damage.type.isPenetrating) { + chunk ?: continue + val (result, health, tile) = chunk.damageTile(pos - chunk.pos.tile, isBackground, sourcePosition, damage, source) + topMost = topMost.coerceAtLeast(result) + + if (source != null && health?.isDead == true) { + source.receiveMessage("tileBroken", jsonArrayOf( + pos, if (isBackground) "background" else "foreground", + tile!!.tile(isBackground).material.id ?: 0, // TODO: string identifiers support + tile.dungeonId, + health.isHarvested + )) + } + } + + topMost = topMost.coerceAtLeast(tileEntityResult) } return topMost 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 d60c9076..bf440f16 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt @@ -33,6 +33,7 @@ import ru.dbotthepony.kstarbound.world.TileHealth import ru.dbotthepony.kstarbound.world.api.ImmutableCell import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.player.PlayerEntity +import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject import java.io.DataOutputStream import java.util.HashMap import java.util.concurrent.ConcurrentLinkedQueue @@ -221,26 +222,24 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p val id = entity.entityID unseen.rem(id) - if (entity is PlayerEntity) { - if (entityVersions.get(id) == -1L) { - // never networked - val initial = FastByteArrayOutputStream() - entity.writeNetwork(DataOutputStream(initial), client.isLegacy) - val (data, version) = entity.networkGroup.write(isLegacy = client.isLegacy) + if (entityVersions.get(id) == -1L) { + // never networked + val initial = FastByteArrayOutputStream() + entity.writeNetwork(DataOutputStream(initial), client.isLegacy) + val (data, version) = entity.networkGroup.write(isLegacy = client.isLegacy) - entityVersions.put(id, version) + entityVersions.put(id, version) - send(EntityCreatePacket( - entity.type, - ByteArrayList.wrap(initial.array, initial.length), - data, - entity.entityID - )) - } else if (entity.networkGroup.upstream.hasChangedSince(entityVersions.get(id))) { - val (data, version) = entity.networkGroup.write(remoteVersion = entityVersions.get(id), isLegacy = client.isLegacy) - entityVersions.put(id, version) - send(EntityUpdateSetPacket(entity.connectionID, Int2ObjectMaps.singleton(entity.entityID, data))) - } + send(EntityCreatePacket( + entity.type, + ByteArrayList.wrap(initial.array, initial.length), + data, + entity.entityID + )) + } else if (entity.networkGroup.upstream.hasChangedSince(entityVersions.get(id))) { + val (data, version) = entity.networkGroup.write(remoteVersion = entityVersions.get(id), isLegacy = client.isLegacy) + entityVersions.put(id, version) + send(EntityUpdateSetPacket(entity.connectionID, Int2ObjectMaps.singleton(entity.entityID, data))) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/UniverseSource.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/UniverseSource.kt index 96e13e37..20408ae0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/UniverseSource.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/UniverseSource.kt @@ -21,6 +21,7 @@ import ru.dbotthepony.kstarbound.defs.world.CelestialParameters import ru.dbotthepony.kstarbound.defs.world.CelestialPlanet import ru.dbotthepony.kstarbound.defs.JsonDriven import ru.dbotthepony.kstarbound.io.BTreeDB5 +import ru.dbotthepony.kstarbound.json.mergeJson import ru.dbotthepony.kstarbound.json.readJsonElement import ru.dbotthepony.kstarbound.json.writeJsonElement import ru.dbotthepony.kstarbound.math.Line2d @@ -192,7 +193,7 @@ class NativeUniverseSource(private val db: BTreeDB6?, private val universe: Serv systemPos, systemSeed, systemName, - JsonDriven.mergeNoCopy(system.baseParameters.deepCopy(), system.variationParameters.random(random)) + mergeJson(system.baseParameters.deepCopy(), system.variationParameters.random(random)) ) if ("typeName" !in systemParams.parameters) { @@ -228,7 +229,7 @@ class NativeUniverseSource(private val db: BTreeDB6?, private val universe: Serv planetCoordinate, planetSeed, planetName, - JsonDriven.mergeNoCopy(planetaryType.baseParameters.deepCopy(), planetaryType.variationParameters.random(random)) + mergeJson(planetaryType.baseParameters.deepCopy(), planetaryType.variationParameters.random(random)) ) val satellites = Int2ObjectArrayMap() @@ -245,11 +246,11 @@ class NativeUniverseSource(private val db: BTreeDB6?, private val universe: Serv val satelliteCoordinate = UniversePos(location, planetOrbitIndex, satelliteOrbitIndex) val merge = JsonObject() - JsonDriven.mergeNoCopy(merge, satelliteType.baseParameters) - JsonDriven.mergeNoCopy(merge, satelliteType.variationParameters.random(random)) + mergeJson(merge, satelliteType.baseParameters) + mergeJson(merge, satelliteType.variationParameters.random(random)) if (systemOrbitRegion.regionName in satelliteType.orbitParameters) { - JsonDriven.mergeNoCopy(merge, satelliteType.orbitParameters[systemOrbitRegion.regionName]!!.random(random)) + mergeJson(merge, satelliteType.orbitParameters[systemOrbitRegion.regionName]!!.random(random)) } satellites[satelliteOrbitIndex] = CelestialParameters( diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Utils.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Utils.kt index 1bc9c68d..ede830f3 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Utils.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Utils.kt @@ -1,8 +1,6 @@ package ru.dbotthepony.kstarbound.util -import com.google.gson.JsonArray import com.google.gson.JsonElement -import com.google.gson.JsonObject import ru.dbotthepony.kstarbound.Starbound import java.util.* import java.util.stream.Stream @@ -18,38 +16,8 @@ fun String.sbIntern2(): String { val JsonElement.asStringOrNull: String? get() = if (isJsonNull) null else asString -fun traverseJsonPath(path: String?, element: JsonElement?): JsonElement? { - element ?: return null - path ?: return element - - if (path.contains('.')) { - var current: JsonElement? = element - - for (name in path.split('.')) { - if (current is JsonObject) { - current = current[name] - } else if (current is JsonArray) { - val toInt = name.toIntOrNull() ?: return null - if (toInt !in 0 until current.size()) return null - current = current.get(toInt) - } else { - return null - } - } - - return current - } else { - if (element is JsonObject) { - return element[path] - } else if (element is JsonArray) { - val toInt = path.toIntOrNull() ?: return null - if (toInt !in 0 until element.size()) return null - return element[toInt] - } else { - return null - } - } -} +val JsonElement.coalesceNull: JsonElement? + get() = if (isJsonNull) null else this fun UUID.toStarboundString(): String { val builder = StringBuilder(32) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt index d496abdf..7b582021 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt @@ -1,12 +1,11 @@ package ru.dbotthepony.kstarbound.world -import it.unimi.dsi.fastutil.objects.ObjectArrayList import it.unimi.dsi.fastutil.objects.ObjectArraySet -import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet import ru.dbotthepony.kommons.arrays.Object2DArray import ru.dbotthepony.kommons.util.AABB import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2i +import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials import ru.dbotthepony.kstarbound.defs.tile.TileDamage import ru.dbotthepony.kstarbound.defs.tile.TileDamageResult import ru.dbotthepony.kstarbound.defs.tile.TileDamageType @@ -16,12 +15,10 @@ import ru.dbotthepony.kstarbound.world.api.AbstractCell import ru.dbotthepony.kstarbound.world.api.ICellAccess import ru.dbotthepony.kstarbound.world.api.ImmutableCell import ru.dbotthepony.kstarbound.world.api.OffsetCellAccess +import ru.dbotthepony.kstarbound.world.api.TileColor import ru.dbotthepony.kstarbound.world.api.TileView import ru.dbotthepony.kstarbound.world.entities.AbstractEntity -import ru.dbotthepony.kstarbound.world.entities.DynamicEntity -import ru.dbotthepony.kstarbound.world.entities.TileEntity import java.util.concurrent.CopyOnWriteArraySet -import kotlin.concurrent.withLock /** * Чанк мира @@ -70,43 +67,77 @@ abstract class Chunk, This : Chunk TileHealth() } + Object2DArray(CHUNK_SIZE, CHUNK_SIZE) { _, _ -> TileHealth.Tile() } } protected val tileHealthBackground = lazy { - Object2DArray(CHUNK_SIZE, CHUNK_SIZE) { _, _ -> TileHealth() } + Object2DArray(CHUNK_SIZE, CHUNK_SIZE) { _, _ -> TileHealth.Tile() } } - fun damageTile(pos: Vector2i, isBackground: Boolean, sourcePosition: Vector2d, damage: TileDamage, source: AbstractEntity? = null): TileDamageResult { + data class DamageResult(val result: TileDamageResult, val health: TileHealth? = null, val stateBefore: AbstractCell? = null) + + fun damageTile(pos: Vector2i, isBackground: Boolean, sourcePosition: Vector2d, damage: TileDamage, source: AbstractEntity? = null): DamageResult { if (!cells.isInitialized()) { - return TileDamageResult.NONE + return DamageResult(TileDamageResult.NONE) } - val tile = cells.value[pos.x, pos.y] + val cell = cells.value[pos.x, pos.y] - if (tile.isIndestructible || tile.tile(isBackground).material.isBuiltin) { - return TileDamageResult.NONE + if (cell.isIndestructible || cell.tile(isBackground).material.isBuiltin) { + return DamageResult(TileDamageResult.NONE) } var damage = damage var result = TileDamageResult.NORMAL - if (tile.dungeonId in world.protectedDungeonIDs) { + if (cell.dungeonId in world.protectedDungeonIDs) { damage = damage.copy(type = TileDamageType.PROTECTED) result = TileDamageResult.PROTECTED } val health = (if (isBackground) tileHealthBackground else tileHealthForeground).value[pos.x, pos.y] - health.damage(tile.tile(isBackground).material.value.actualDamageTable, sourcePosition, damage) - subscribers.forEach { it.onTileHealthUpdate(pos.x, pos.y, isBackground, health) } + val tile = cell.tile(isBackground) - if (isBackground) { - damagedTilesBackground.add(pos) + val params = if (!damage.type.isPenetrating && tile.modifier != null && tile.modifier!!.value.breaksWithTile) { + tile.material.value.actualDamageTable + tile.modifier!!.value.actualDamageTable } else { - damagedTilesForeground.add(pos) + tile.material.value.actualDamageTable } - return result + health.damage(params, sourcePosition, damage) + subscribers.forEach { it.onTileHealthUpdate(pos.x, pos.y, isBackground, health) } + + if (health.isDead) { + if (isBackground) { + damagedTilesBackground.remove(pos) + } else { + damagedTilesForeground.remove(pos) + } + + val copyHealth = health.copy() + val mCell = cell.mutable() + val mTile = mCell.tile(isBackground) + + mTile.material = BuiltinMetaMaterials.EMPTY + mTile.color = TileColor.DEFAULT + mTile.hueShift = 0f + + if (tile.modifier != null && mTile.modifier!!.value.breaksWithTile) { + mTile.modifier = null + } + + setCell(pos.x, pos.y, mCell.immutable()) + health.reset() + return DamageResult(result, copyHealth, cell) + } else { + if (isBackground) { + damagedTilesBackground.add(pos) + } else { + damagedTilesForeground.add(pos) + } + + return DamageResult(result, health, cell) + } } protected val damagedTilesForeground = ObjectArraySet() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/TileHealth.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/TileHealth.kt index 7c1ca216..41fb8b70 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/TileHealth.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/TileHealth.kt @@ -4,44 +4,36 @@ import ru.dbotthepony.kommons.io.readVector2d import ru.dbotthepony.kommons.io.readVector2f import ru.dbotthepony.kommons.io.writeStruct2d import ru.dbotthepony.kommons.io.writeStruct2f +import ru.dbotthepony.kommons.util.getValue +import ru.dbotthepony.kommons.util.setValue import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.tile.TileDamage import ru.dbotthepony.kstarbound.defs.tile.TileDamageConfig import ru.dbotthepony.kstarbound.defs.tile.TileDamageType +import ru.dbotthepony.kstarbound.math.Interpolator +import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup +import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean +import ru.dbotthepony.kstarbound.network.syncher.networkedEnum +import ru.dbotthepony.kstarbound.network.syncher.networkedFixedPoint import java.io.DataInputStream import java.io.DataOutputStream -class TileHealth() { - constructor(stream: DataInputStream, isLegacy: Boolean) : this() { - read(stream, isLegacy) - } - - var isHarvested: Boolean = false - private set +sealed class TileHealth() { var damageSource: Vector2d = Vector2d.ZERO - private set - var damageType: TileDamageType = TileDamageType.PROTECTED - private set - var damagePercent: Double = 0.0 - private set - var damageEffectTimeFactor: Double = 0.0 - private set + protected set + abstract var damagePercent: Double + protected set + abstract var damageEffectTimeFactor: Double + protected set + abstract var isHarvested: Boolean + protected set + abstract var damageType: TileDamageType + protected set var damageEffectPercentage: Double = 0.0 - private set + protected set - fun copy(): TileHealth { - val copy = TileHealth() - - copy.isHarvested = isHarvested - copy.damageSource = damageSource - copy.damageType = damageType - copy.damagePercent = damagePercent - copy.damageEffectTimeFactor = damageEffectTimeFactor - copy.damageEffectPercentage = damageEffectPercentage - - return copy - } + abstract fun copy(): TileHealth val isHealthy: Boolean get() = damagePercent <= 0.0 @@ -119,4 +111,54 @@ class TileHealth() { damageEffectPercentage = damageEffectTimeFactor.coerceIn(0.0, 1.0) * damagePercent return damagePercent > 0.0 } + + class Tile() : TileHealth() { + constructor(stream: DataInputStream, isLegacy: Boolean) : this() { + read(stream, isLegacy) + } + + override var damagePercent: Double = 0.0 + override var damageEffectTimeFactor: Double = 0.0 + override var isHarvested: Boolean = false + override var damageType: TileDamageType = TileDamageType.PROTECTED + + override fun copy(): Tile { + val copy = Tile() + + copy.isHarvested = isHarvested + copy.damageSource = damageSource + copy.damageType = damageType + copy.damagePercent = damagePercent + copy.damageEffectTimeFactor = damageEffectTimeFactor + copy.damageEffectPercentage = damageEffectPercentage + + return copy + } + } + + class TileEntity() : TileHealth() { + constructor(stream: DataInputStream, isLegacy: Boolean) : this() { + read(stream, isLegacy) + } + + val networkGroup = NetworkedGroup() + + override var damagePercent: Double by networkedFixedPoint(0.001).also { networkGroup.add(it); it.interpolator = Interpolator.Linear } + override var damageEffectTimeFactor: Double by networkedFixedPoint(0.001).also { networkGroup.add(it); it.interpolator = Interpolator.Linear } + override var isHarvested: Boolean by networkedBoolean().also { networkGroup.add(it) } + override var damageType: TileDamageType by networkedEnum(TileDamageType.PROTECTED).also { networkGroup.add(it) } + + override fun copy(): TileEntity { + val copy = TileEntity() + + copy.isHarvested = isHarvested + copy.damageSource = damageSource + copy.damageType = damageType + copy.damagePercent = damagePercent + copy.damageEffectTimeFactor = damageEffectTimeFactor + copy.damageEffectPercentage = damageEffectPercentage + + return copy + } + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt index 823fad46..19abab30 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt @@ -5,15 +5,16 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap import it.unimi.dsi.fastutil.ints.IntArraySet import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap import it.unimi.dsi.fastutil.objects.ObjectArraySet -import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.arrays.Object2DArray import ru.dbotthepony.kommons.collect.filterNotNull import ru.dbotthepony.kommons.util.IStruct2d import ru.dbotthepony.kommons.util.IStruct2i import ru.dbotthepony.kommons.util.AABB +import ru.dbotthepony.kommons.util.AABBi import ru.dbotthepony.kommons.util.MailboxExecutorService import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.world.WorldStructure import ru.dbotthepony.kstarbound.defs.world.WorldTemplate @@ -22,12 +23,13 @@ import ru.dbotthepony.kstarbound.network.IPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.SetPlayerStartPacket import ru.dbotthepony.kstarbound.util.ExceptionLogger import ru.dbotthepony.kstarbound.util.ParallelPerform +import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.world.api.ICellAccess import ru.dbotthepony.kstarbound.world.api.AbstractCell import ru.dbotthepony.kstarbound.world.api.TileView import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.DynamicEntity -import ru.dbotthepony.kstarbound.world.entities.TileEntity +import ru.dbotthepony.kstarbound.world.entities.tile.TileEntity import ru.dbotthepony.kstarbound.world.physics.CollisionPoly import ru.dbotthepony.kstarbound.world.physics.CollisionType import ru.dbotthepony.kstarbound.world.physics.Poly @@ -208,7 +210,7 @@ abstract class World, ChunkType : Chunk, ChunkType : Chunk = Predicate { true }, distinct: Boolean = true): List { + return entityIndex.query( + AABBi(pos, pos + Vector2i.POSITIVE_XY), + distinct = distinct, + filter = { it is TileEntity && (pos - it.tilePosition) in it.occupySpaces && filter.test(it) } + ) as List + } + fun queryTileCollisions(aabb: AABB): MutableList { val result = ArrayList() val tiles = aabb.encasingIntAABB() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractCell.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractCell.kt index 129d4f21..a2533e7b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractCell.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractCell.kt @@ -25,12 +25,7 @@ sealed class AbstractCell { return LegacyNetworkCellState(background.toLegacyNet(), foreground.toLegacyNet(), foreground.material.value.collisionKind, biome, envBiome, liquid.toLegacyNet(), dungeonId) } - fun tile(background: Boolean): AbstractTileState { - if (background) - return this.background - else - return this.foreground - } + abstract fun tile(background: Boolean): AbstractTileState fun write(stream: DataOutputStream) { foreground.write(stream) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ImmutableCell.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ImmutableCell.kt index 681826dd..775cd4a4 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ImmutableCell.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ImmutableCell.kt @@ -22,6 +22,13 @@ data class ImmutableCell( return legacyNet } + override fun tile(background: Boolean): ImmutableTileState { + if (background) + return this.background + else + return this.foreground + } + override fun mutable(): MutableCell { return MutableCell(foreground.mutable(), background.mutable(), liquid.mutable(), dungeonId, biome, envBiome, isIndestructible) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/MutableCell.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/MutableCell.kt index 9f7f7614..a562ca0b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/MutableCell.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/MutableCell.kt @@ -28,6 +28,13 @@ data class MutableCell( return this } + override fun tile(background: Boolean): MutableTileState { + if (background) + return this.background + else + return this.foreground + } + override fun immutable(): ImmutableCell { return POOL.intern(ImmutableCell(foreground.immutable(), background.immutable(), liquid.immutable(), dungeonId, biome, envBiome, isIndestructible)) } 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 76639fa8..859994fc 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt @@ -1,17 +1,24 @@ package ru.dbotthepony.kstarbound.world.entities +import com.google.gson.JsonArray +import com.google.gson.JsonElement import it.unimi.dsi.fastutil.bytes.ByteArrayList import org.apache.logging.log4j.LogManager +import ru.dbotthepony.kommons.io.koptional +import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.util.MailboxExecutorService import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.client.StarboundClient import ru.dbotthepony.kstarbound.client.render.LayeredRenderer +import ru.dbotthepony.kstarbound.client.world.ClientWorld import ru.dbotthepony.kstarbound.defs.EntityType import ru.dbotthepony.kstarbound.defs.JsonDriven import ru.dbotthepony.kstarbound.network.packets.EntityDestroyPacket +import ru.dbotthepony.kstarbound.network.syncher.InternedStringCodec import ru.dbotthepony.kstarbound.network.syncher.MasterElement import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup +import ru.dbotthepony.kstarbound.network.syncher.networkedData import ru.dbotthepony.kstarbound.server.world.ServerWorld import ru.dbotthepony.kstarbound.world.LightCalculator import ru.dbotthepony.kstarbound.world.SpatialIndex @@ -53,6 +60,9 @@ abstract class AbstractEntity(path: String) : JsonDriven(path), Comparable get() = innerWorld ?: throw IllegalStateException("Not in world") + inline val clientWorld get() = world as ClientWorld + inline val serverWorld get() = world as ServerWorld + val isSpawned: Boolean get() = innerWorld != null @@ -63,8 +73,7 @@ abstract class AbstractEntity(path: String) : JsonDriven(path), Comparable) { if (innerWorld != null) throw IllegalStateException("Already spawned (in world $innerWorld)") @@ -130,30 +143,14 @@ abstract class AbstractEntity(path: String) : JsonDriven(path), Comparable() - - init { - config.states.forEach { (t, u) -> states[t] = u } - - if (states.isNotEmpty() && config.default.isBlank()) - default = states.firstKey() - else - default = config.default - } - } - - private class Part(config: AnimatedPartsDefinition.Part) { - val partProperties = config.properties - var activePartDirty = true - val partStates = config.partStates - } - - // sorted by priority - private val stateTypes = LinkedHashMap() - // sorted by key - private val parts = Object2ObjectAVLTreeMap() - - constructor() { - - } - - constructor(config: AnimatedPartsDefinition) { - for ((k, v) in config.stateTypes.entries.sortedWith { o1, o2 -> o2.value.priority.compareTo(o1.value.priority) }) { - stateTypes[k] = StateType(v) - } - - for ((k, v) in config.parts) { - parts[k] = Part(v) - } - } - - fun parts(): Collection { - return parts.keys - } - - fun stateTypes(): Collection { - return stateTypes.keys - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Animator.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Animator.kt index a0cedfd1..d2d366f4 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Animator.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Animator.kt @@ -1,30 +1,27 @@ package ru.dbotthepony.kstarbound.world.entities +import com.google.gson.JsonObject import it.unimi.dsi.fastutil.objects.Object2ObjectAVLTreeMap import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet import org.apache.logging.log4j.LogManager -import ru.dbotthepony.kommons.io.BinaryStringCodec +import ru.dbotthepony.kommons.gson.contains +import ru.dbotthepony.kommons.gson.getArray +import ru.dbotthepony.kommons.gson.set import ru.dbotthepony.kommons.io.IntValueCodec import ru.dbotthepony.kommons.io.map -import ru.dbotthepony.kommons.io.readKOptional -import ru.dbotthepony.kommons.io.writeBinaryString -import ru.dbotthepony.kommons.io.writeKOptional import ru.dbotthepony.kommons.matrix.Matrix3f -import ru.dbotthepony.kommons.util.AABB import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.util.getValue import ru.dbotthepony.kommons.util.setValue import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.animation.AnimatedPartsDefinition import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition -import ru.dbotthepony.kstarbound.defs.animation.ParticleConfig import ru.dbotthepony.kstarbound.defs.animation.ParticleFactory -import ru.dbotthepony.kstarbound.fromJson -import ru.dbotthepony.kstarbound.io.readInternedString +import ru.dbotthepony.kstarbound.json.mergeJson import ru.dbotthepony.kstarbound.math.Interpolator import ru.dbotthepony.kstarbound.math.PeriodicFunction -import ru.dbotthepony.kstarbound.network.syncher.AABBCodecLegacy -import ru.dbotthepony.kstarbound.network.syncher.AABBCodecNative +import ru.dbotthepony.kstarbound.math.approachAngle import ru.dbotthepony.kstarbound.network.syncher.InternedStringCodec import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup import ru.dbotthepony.kstarbound.network.syncher.NetworkedElement @@ -33,7 +30,6 @@ import ru.dbotthepony.kstarbound.network.syncher.NetworkedSignal import ru.dbotthepony.kstarbound.network.syncher.networkedAABBNullable import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean import ru.dbotthepony.kstarbound.network.syncher.networkedColor -import ru.dbotthepony.kstarbound.network.syncher.networkedData import ru.dbotthepony.kstarbound.network.syncher.networkedEventCounter import ru.dbotthepony.kstarbound.network.syncher.networkedFixedPoint import ru.dbotthepony.kstarbound.network.syncher.networkedFloat @@ -42,17 +38,53 @@ import ru.dbotthepony.kstarbound.network.syncher.networkedPointer import ru.dbotthepony.kstarbound.network.syncher.networkedSignedInt import ru.dbotthepony.kstarbound.network.syncher.networkedString import ru.dbotthepony.kstarbound.network.syncher.networkedUnsignedInt -import java.io.DataInputStream -import java.io.DataOutputStream +import ru.dbotthepony.kstarbound.util.random.random +import ru.dbotthepony.kstarbound.world.positiveModulo import java.util.Collections +import java.util.function.Consumer import kotlin.math.atan2 import kotlin.math.cos +import kotlin.math.roundToInt import kotlin.math.sin import kotlin.math.sqrt +// NetworkedAnimator + AnimatedPartSet combined into one class +// AnimatedPartSet original docs: +// Defines a "animated" data set constructed in such a way that it is very +// useful for doing generic animations with lots of additional animation data. +// It is made up of two concepts, "states" and "parts". +// +// States: +// +// There are N "state types" defined, which each defines a set of mutually +// exclusive states that each "state type" can be in. For example, one state +// type might be "movement", and the "movement" states might be "idle", "walk", +// and "run. Another state type might be "attack" which could have as its +// states "idle", and "melee". Each state type will have exactly one currently +// active state, so this class may, for example, be in the total state of +// "movement:idle" and "attack:melee". Each state within each state type is +// animated, so that over time the state frame increases and may loop around, +// or transition into another state so that that state type without interaction +// may go from "melee" to "idle" when the "melee" state animation is finished. +// This is defined by the individual state config in the configuration passed +// into the constructor. +// +// Parts: +// +// Each instance of this class also can have N "Parts" defined, which are +// groups of properties that "listen" to active states. Each part can "listen" +// to one or more state types, and the first matching state x state type pair +// (in order of state type priority which is specified in the config) is +// chosen, and the properties from that state type and state are merged into +// the part to produce the final active part information. Rather than having a +// single image or image set for each part, since this class is intended to be +// as generic as possible, all of this data is assumed to be queried from the +// part properties, so that things such as image data as well as other things +// like damage or collision polys can be stored along with the animation +// frames, the part state, the base part, whichever is most applicable. class Animator() { - class Light { + private class Light { private val elements = ArrayList() fun addTo(group: NetworkedGroup) { @@ -76,7 +108,7 @@ class Animator() { var beamAmbience: Float = 0f } - enum class SoundSignal { + private enum class SoundSignal { PLAY, STOP_ALL; companion object { @@ -84,7 +116,7 @@ class Animator() { } } - class Sound { + private class Sound { private val elements = ArrayList() fun addTo(group: NetworkedGroup) { @@ -104,25 +136,181 @@ class Animator() { val signals = NetworkedSignal(SoundSignal.CODEC).also { elements.add(it) } } - class Effect(val type: String, val time: Double, val directives: String) { + private class Effect(val type: String, val time: Double, val directives: String) { val enabled = networkedBoolean() var timer: Double = 0.0 + + fun tick(delta: Double) { + if (timer <= 0.0) + timer = time + else + timer -= delta + } } - class StateInfo { - val stateIndex = networkedPointer() + private class StateType(config: AnimatedPartsDefinition.StateType) { + // NetworkedAnimator + private var noPropagate = false + val stateIndex = networkedPointer(-1L) val startedEvent = networkedEventCounter() + + // AnimatedPartSet + var enabled = config.enabled + var activeStateDirty = true + val priority = config.priority + val stateTypeProperties = config.properties + val default: String + var activeProperties = JsonObject() + private set + + // sorted by key + // Basically, this is definition of each separate state + val states = config.states + + var timer = 0.0 + var frame = 0 + private set(value) { + if (field != value) { + field = value + frameChanged = true + } + } + + init { + if (states.isNotEmpty() && config.default.isBlank()) + default = states.keys.first() + else + default = config.default + } + + private var activeStateChanged = false + private var frameChanged = false + + var activeState: AnimatedPartsDefinition.StateType.State? = null + set(value) { + activeStateChanged = true + + if (value == null) { + if (!noPropagate) { + try { + noPropagate = true + stateIndex.accept(-1L) + } finally { + noPropagate = false + } + } + + field = null + } else { + if (!noPropagate) { + try { + noPropagate = true + stateIndex.accept(value.index.toLong()) + } finally { + noPropagate = false + } + } + + field = value + } + } + + fun set(state: String, alwaysStart: Boolean): Boolean { + val getState = states[state] ?: return false + + if (activeState != getState || alwaysStart) { + activeState = getState + timer = 0.0 + return true + } + + return false + } + + init { + if (default in states) { + activeState = states[default]!! + stateIndex.accept(activeState!!.index.toLong()) + } + + stateIndex.addListener(Consumer { + if (noPropagate) return@Consumer + + if (it == -1L) { + try { + noPropagate = true + activeState = null + } finally { + noPropagate = false + } + } else { + try { + noPropagate = true + set(states.keys.elementAtOrNull(it.toInt()) ?: throw IllegalArgumentException("Unknown animation state $it!"), true) + } finally { + noPropagate = false + } + } + }) + } + + fun tick(delta: Double) { + var activeState = activeState ?: return + timer += delta + + if (timer > activeState.cycle) { + when (activeState.mode) { + AnimatedPartsDefinition.AnimationMode.END -> timer = activeState.cycle + + AnimatedPartsDefinition.AnimationMode.TRANSITION -> { + activeState = states[activeState.transition]!! // validity of 'transition' is checked during json load + this.activeState = activeState + timer = 0.0 + } + + AnimatedPartsDefinition.AnimationMode.LOOP -> timer %= activeState.cycle + } + } + + frame = (timer / activeState.cycle * activeState.frames).toInt().coerceIn(0, activeState.frames - 1) + + if (activeStateChanged || frameChanged) { + activeProperties = mergeJson(stateTypeProperties.deepCopy(), activeState.properties) + frameChanged = false + activeStateChanged = false + + for ((key, values) in activeState.frameProperties) { + if (values.size() >= frame) { + activeProperties[key] = values[frame].deepCopy() + } + } + } + } } - class RotationGroup { + private class Part(config: AnimatedPartsDefinition.Part) { + val partProperties = config.properties + var activePartDirty = true + val partStates = config.partStates + } + + private class RotationGroup { var angularVelocity = 0.0 var rotationCenter = Vector2d.ZERO val targetAngle = networkedFloat() var currentAngle = 0.0 val immediateEvent = networkedEventCounter() + + fun tick(delta: Double) { + if (angularVelocity == 0.0) { + currentAngle = targetAngle.get() + } else { + currentAngle = approachAngle(targetAngle.get(), currentAngle, angularVelocity * delta) + } + } } - class TransformationGroup { + private class TransformationGroup { private val elements = ArrayList() fun addTo(group: NetworkedGroup) { @@ -158,7 +346,7 @@ class Animator() { } } - class ParticleEmitter { + private class ParticleEmitter { data class Config(val count: Int, val offset: Vector2d, val flip: Boolean, val factory: ParticleFactory) private val elements = ArrayList() @@ -189,9 +377,6 @@ class Animator() { private val elements = ArrayList() - var animatedParts = AnimatedParts() - private set - var processingDirectives by networkedString().also { elements.add(it) } var zoom by networkedFloat().also { elements.add(it) } var isFlipped by networkedBoolean().also { elements.add(it) } @@ -199,9 +384,11 @@ class Animator() { var animationRate by networkedFloat(1.0).also { elements.add(it); it.interpolator = Interpolator.Linear } private val globalTags = NetworkedMap(InternedStringCodec, InternedStringCodec) + private val parts = Object2ObjectAVLTreeMap() private val partTags = HashMap>() - private val stateInfo = Object2ObjectAVLTreeMap() + // iterate by priority, network by sorted key + private val stateTypes = LinkedHashMap() private val rotationGroups = Object2ObjectAVLTreeMap() private val transformationGroups = Object2ObjectAVLTreeMap() private val particleEmitters = Object2ObjectAVLTreeMap() @@ -209,14 +396,13 @@ class Animator() { private val sounds = Object2ObjectAVLTreeMap() private val effects = Object2ObjectAVLTreeMap() + private val random = random() + init { setupNetworkElements() } constructor(config: AnimationDefinition) : this() { - if (config.animatedParts != null) - animatedParts = AnimatedParts(config.animatedParts) - for ((k, v) in config.globalTagDefaults) { globalTags[k] = v } @@ -308,11 +494,17 @@ class Animator() { effects[k] = Effect(v.type, v.time, v.directives) } - for (k in animatedParts.stateTypes()) { - stateInfo[k] = StateInfo() + if (config.animatedParts != null) { + for ((k, v) in config.animatedParts.stateTypes.entries.sortedWith { o1, o2 -> o2.value.priority.compareTo(o1.value.priority) }) { + stateTypes[k] = StateType(v) + } + + for ((k, v) in config.animatedParts.parts) { + parts[k] = Part(v) + } } - for (k in animatedParts.parts()) { + for (k in parts.keys) { partTags.computeIfAbsent(k) { NetworkedMap(InternedStringCodec, InternedStringCodec) } } @@ -345,13 +537,16 @@ class Animator() { networkGroup.add(globalTags) // animated part set - for (v in animatedParts.parts()) { + for (v in parts.keys) { networkGroup.add(partTags[v] ?: throw RuntimeException("Missing animated part $v!")) } - for (v in stateInfo.values) { - networkGroup.add(v.stateIndex) - networkGroup.add(v.startedEvent) + stateTypes.entries.stream() + .sorted { o1, o2 -> o1.key.compareTo(o2.key) } + .map { it.value } + .forEach { + networkGroup.add(it.stateIndex) + networkGroup.add(it.startedEvent) } for (v in transformationGroups.values) { @@ -380,6 +575,64 @@ class Animator() { } } + fun setActiveState(type: String, state: String, alwaysStart: Boolean = false): Boolean { + val getType = stateTypes[type] ?: return false + val getState = getType.states[state] ?: return false + + if (getType.activeState != getState || alwaysStart) { + getType.timer = 0.0 + return true + } + + return false + } + + // TODO: Dynamic target + @Suppress("Name_Shadowing") + fun tick(delta: Double = Starbound.TIMESTEP) { + val delta = delta * animationRate + + for (state in stateTypes.values) { + state.tick(delta) + + if ("lightsOn" in state.activeProperties) { + for (name in state.activeProperties.getArray("lightsOn")) { + lights[name.asString]?.active = true + } + } + + if ("lightsOff" in state.activeProperties) { + for (name in state.activeProperties.getArray("lightsOff")) { + lights[name.asString]?.active = false + } + } + + if ("particleEmittersOn" in state.activeProperties) { + for (name in state.activeProperties.getArray("particleEmittersOn")) { + particleEmitters[name.asString]?.active = true + } + } + + if ("particleEmittersOff" in state.activeProperties) { + for (name in state.activeProperties.getArray("particleEmittersOff")) { + particleEmitters[name.asString]?.active = false + } + } + } + + for (rotationGroup in rotationGroups.values) { + rotationGroup.tick(delta) + } + + for (light in lights.values) { + light.flicker?.update(delta, random) + } + + for (effect in effects.values) { + effect.tick(delta) + } + } + companion object { // lame fun load(path: String): Animator { 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 af5e15b2..18b484da 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/DynamicEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/DynamicEntity.kt @@ -27,10 +27,10 @@ abstract class DynamicEntity(path: String) : AbstractEntity(path) { movement.updateFixtures() } - override fun tickRemote() { - super.tickRemote() + override fun tick() { + super.tick() - if (networkGroup.upstream.isInterpolating) { + if (isRemote && networkGroup.upstream.isInterpolating) { movement.updateFixtures() } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/TileEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/TileEntity.kt deleted file mode 100644 index 973eda28..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/TileEntity.kt +++ /dev/null @@ -1,33 +0,0 @@ -package ru.dbotthepony.kstarbound.world.entities - -import com.google.gson.JsonObject -import ru.dbotthepony.kommons.vector.Vector2d -import ru.dbotthepony.kommons.vector.Vector2i -import ru.dbotthepony.kstarbound.world.ChunkPos -import ru.dbotthepony.kstarbound.world.SpatialIndex -import ru.dbotthepony.kstarbound.world.World - -/** - * (Hopefully) Static world entities (Plants, Objects, etc), which reside on cell grid - */ -abstract class TileEntity(path: String) : AbstractEntity(path) { - var tilePosition = Vector2i() - set(value) { - if (isSpawned) { - field = world.geometry.wrap(value) - // spatialEntry?.fixture?.move() - } else { - field = value - } - } - - override val position: Vector2d - get() = tilePosition.toDoubleVector() - - override fun onJoinWorld(world: World<*, *>) { - tilePosition = tilePosition - } - - override fun onRemove(world: World<*, *>, isDeath: Boolean) { - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/WorldObject.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/WorldObject.kt deleted file mode 100644 index 0fc4eaf6..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/WorldObject.kt +++ /dev/null @@ -1,205 +0,0 @@ -package ru.dbotthepony.kstarbound.world.entities - -import com.google.common.collect.ImmutableMap -import com.google.gson.JsonObject -import com.google.gson.TypeAdapter -import com.google.gson.reflect.TypeToken -import ru.dbotthepony.kommons.math.RGBAColor -import ru.dbotthepony.kommons.vector.Vector2i -import ru.dbotthepony.kstarbound.Registries -import ru.dbotthepony.kstarbound.Registry -import ru.dbotthepony.kstarbound.Starbound -import ru.dbotthepony.kstarbound.client.StarboundClient -import ru.dbotthepony.kstarbound.client.render.LayeredRenderer -import ru.dbotthepony.kstarbound.client.world.ClientWorld -import ru.dbotthepony.kstarbound.defs.Drawable -import ru.dbotthepony.kstarbound.defs.image.SpriteReference -import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition -import ru.dbotthepony.kstarbound.defs.`object`.ObjectOrientation -import ru.dbotthepony.kstarbound.server.world.ServerWorld -import ru.dbotthepony.kommons.gson.get -import ru.dbotthepony.kommons.gson.set -import ru.dbotthepony.kstarbound.defs.EntityType -import ru.dbotthepony.kstarbound.util.asStringOrNull -import ru.dbotthepony.kstarbound.world.Side -import ru.dbotthepony.kstarbound.world.LightCalculator -import ru.dbotthepony.kstarbound.world.PIXELS_IN_STARBOUND_UNITf -import ru.dbotthepony.kstarbound.world.api.TileColor -import java.io.DataOutputStream -import java.util.HashMap - -open class WorldObject( - val prototype: Registry.Entry, -) : TileEntity(prototype.file?.computeDirectory() ?: "/") { - fun deserialize(data: JsonObject) { - direction = data.get("direction", directions) { Side.LEFT } - orientationIndex = data.get("orientationIndex", -1) - interactive = data.get("interactive", false) - - uniqueId = data["uniqueId"]?.asStringOrNull - - for ((k, v) in data.get("parameters") { JsonObject() }.entrySet()) { - properties[k] = v.deepCopy() - } - } - - override val type: EntityType - get() = EntityType.OBJECT - - override fun writeNetwork(stream: DataOutputStream, isLegacy: Boolean) { - TODO("Not yet implemented") - } - - fun serialize(): JsonObject { - val into = JsonObject() - into["name"] = prototype.key - into["tilePosition"] = vectors.toJsonTree(tilePosition) - into["direction"] = directions.toJsonTree(direction) - into["orientationIndex"] = orientationIndex - into["interactive"] = interactive - - if (uniqueId != null) { - into["uniqueId"] = uniqueId!! - } - - into["parameters"] = properties.deepCopy() - return into - } - - // - // internal runtime properties - // - inline val clientWorld get() = world as ClientWorld - inline val serverWorld get() = world as ServerWorld - inline val orientations get() = prototype.value.orientations - protected val renderParamLocations = HashMap String?>() - private var frame = 0 - set(value) { - if (field != value) { - field = value - drawablesCache.invalidate() - } - } - - private var frameTimer = 0.0 - val flickerPeriod = prototype.value.flickerPeriod?.copy() - - // - // top level properties - // - var uniqueId: String? = null - var interactive = false - var direction = Side.LEFT - - var orientationIndex = -1 - set(value) { - if (field != value) { - field = value - invalidate() - } - } - - // - // json driven properties - // - var color: TileColor by Property(TileColor.DEFAULT) - - var animationParts: ImmutableMap by Property() - var imagePosition: Vector2i by Property(Vector2i.ZERO) - var animationPosition: Vector2i by Property(Vector2i.ZERO) - - init { - renderParamLocations["color"] = { color.lowercase } - renderParamLocations["frame"] = { frame.toString() } - } - - private val drawablesCache = LazyData { - orientation?.drawables?.map { it.with(::getRenderParam) } ?: listOf() - } - - val drawables: List by drawablesCache - - fun getRenderParam(key: String): String? { - return renderParamLocations[key]?.invoke() ?: "default" - } - - override fun invalidate(name: String) { - super.invalidate(name) - - if (name in renderParamLocations) { - drawablesCache.invalidate() - } - } - - override fun invalidate() { - super.invalidate() - } - - override fun tickShared() { - super.tickShared() - flickerPeriod?.update(Starbound.TIMESTEP, world.random) - } - - override fun tickRemote() { - val orientation = orientation - - if (orientation != null) { - frameTimer = (frameTimer + Starbound.TIMESTEP) % orientation.animationCycle - frame = (frameTimer / orientation.animationCycle * orientation.frames).toInt() - } - } - - val orientation: ObjectOrientation? get() { - return orientations.getOrNull(orientationIndex) - } - - override fun defs(): Collection { - val get = orientation - return if (get == null) listOf(prototype.jsonObject) else listOf(get.json, prototype.jsonObject) - } - - val lightColors: ImmutableMap by LazyData(listOf("lightColor", "lightColors")) { - dataValue("lightColor")?.let { ImmutableMap.of("default", colors0.fromJsonTree(it)) } - ?: dataValue("lightColors")?.let { colors1.fromJsonTree(it) } - ?: ImmutableMap.of() - } - - override fun addLights(lightCalculator: LightCalculator, xOffset: Int, yOffset: Int) { - var color = lightColors[color.lowercase] - - if (color != null) { - if (flickerPeriod != null) { - val sample = flickerPeriod.sinValue().toFloat() - color *= sample - } - - lightCalculator.addPointLight(tilePosition.x - xOffset, tilePosition.y - yOffset, color) - } - } - - override fun render(client: StarboundClient, layers: LayeredRenderer) { - val layer = layers.getLayer(orientation?.renderLayer ?: return) - - drawables.forEach { - val (x, y) = imagePosition - it.render(client, layer, position.x.toFloat() + x / PIXELS_IN_STARBOUND_UNITf, position.y.toFloat() + y / PIXELS_IN_STARBOUND_UNITf) - } - } - - companion object { - private val colors1 by lazy { Starbound.gson.getAdapter(TypeToken.getParameterized(ImmutableMap::class.java, String::class.java, RGBAColor::class.java)) as TypeAdapter> } - private val colors0 by lazy { Starbound.gson.getAdapter(RGBAColor::class.java) } - private val strings by lazy { Starbound.gson.getAdapter(String::class.java) } - private val directions by lazy { Starbound.gson.getAdapter(Side::class.java) } - private val vectors by lazy { Starbound.gson.getAdapter(Vector2i::class.java) } - - fun fromJson(content: JsonObject): WorldObject { - val prototype = Registries.worldObjects[content["name"]?.asString ?: throw IllegalArgumentException("Missing object name")] ?: throw IllegalArgumentException("No such object defined for '${content["name"]}'") - val pos = content.get("tilePosition", vectors) { throw IllegalArgumentException("No tilePosition was present in saved data") } - val result = WorldObject(prototype) - result.tilePosition = pos - result.deserialize(content) - return result - } - } -} 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 7e9a91ca..871e5dc5 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,9 +1,11 @@ package ru.dbotthepony.kstarbound.world.entities.player +import com.google.gson.JsonElement import com.google.gson.JsonObject import it.unimi.dsi.fastutil.bytes.ByteArrayList import ru.dbotthepony.kommons.io.writeBinaryString import ru.dbotthepony.kommons.util.AABB +import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.util.getValue import ru.dbotthepony.kommons.util.setValue import ru.dbotthepony.kommons.vector.Vector2d @@ -14,6 +16,7 @@ import ru.dbotthepony.kstarbound.defs.actor.HumanoidData 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.JsonPath import ru.dbotthepony.kstarbound.json.builder.IStringSerializable import ru.dbotthepony.kstarbound.math.Interpolator import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup @@ -52,7 +55,7 @@ class PlayerEntity() : HumanoidActorEntity("/") { } constructor(data: DataInputStream, isLegacy: Boolean) : this() { - uniqueID = data.readInternedString() + uniqueID.accept(KOptional(data.readInternedString())) description = data.readInternedString() gamemode = PlayerGamemode.entries[if (isLegacy) data.readInt() else data.readUnsignedByte()] humanoidData = HumanoidData.read(data, isLegacy) @@ -64,7 +67,12 @@ class PlayerEntity() : HumanoidActorEntity("/") { var gamemode = PlayerGamemode.CASUAL override fun writeNetwork(stream: DataOutputStream, isLegacy: Boolean) { - stream.writeBinaryString(uniqueID!!) + uniqueID.get().ifPresent { + stream.writeBinaryString(it) + }.ifNotPresent { + stream.writeBinaryString("") + } + stream.writeBinaryString(description) if (isLegacy) stream.writeInt(gamemode.ordinal) else stream.writeByte(gamemode.ordinal) humanoidData.write(stream, isLegacy) @@ -113,8 +121,8 @@ class PlayerEntity() : HumanoidActorEntity("/") { metaFixture = null } - override fun tickShared() { - super.tickShared() + override fun tick() { + super.tick() if (fixturesChangeset != movement.fixturesChangeset) { fixturesChangeset = movement.fixturesChangeset @@ -128,10 +136,14 @@ class PlayerEntity() : HumanoidActorEntity("/") { override val isApplicableForUnloading: Boolean get() = false - override fun defs(): Collection { - return emptyList() - } - var uuid: UUID by Delegates.notNull() private set + + override fun lookupProperty(path: JsonPath, orElse: () -> JsonElement): JsonElement { + TODO("Not yet implemented") + } + + override fun setProperty0(key: JsonPath, value: JsonElement) { + TODO("Not yet implemented") + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/ContainerObject.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/ContainerObject.kt new file mode 100644 index 00000000..35bcdd36 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/ContainerObject.kt @@ -0,0 +1,23 @@ +package ru.dbotthepony.kstarbound.world.entities.tile + +import ru.dbotthepony.kommons.util.getValue +import ru.dbotthepony.kommons.util.setValue +import ru.dbotthepony.kstarbound.Registry +import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition +import ru.dbotthepony.kstarbound.math.Interpolator +import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean +import ru.dbotthepony.kstarbound.network.syncher.networkedBytes +import ru.dbotthepony.kstarbound.network.syncher.networkedFloat +import ru.dbotthepony.kstarbound.network.syncher.networkedSignedInt + +class ContainerObject(config: Registry.Entry) : WorldObject(config) { + var opened by networkedSignedInt().also { networkGroup.upstream.add(it) } + var isCrafting by networkedBoolean().also { networkGroup.upstream.add(it) } + var craftingProgress by networkedFloat().also { networkGroup.upstream.add(it); it.interpolator = Interpolator.Linear } + // i have no words. + // this field embeds ENTIRE net state of 'ItemBag', + // and each time container is updated, its contents are networked fully + // each. damn. time. + // placeholder data, container size 40, read 0 items + var itemsNetState by networkedBytes(byteArrayOf(40, 0)).also { networkGroup.upstream.add(it) } +} 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 new file mode 100644 index 00000000..d483c4f1 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/LoungeableObject.kt @@ -0,0 +1,10 @@ +package ru.dbotthepony.kstarbound.world.entities.tile + +import ru.dbotthepony.kstarbound.Registry +import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition + +class LoungeableObject(config: Registry.Entry) : WorldObject(config) { + init { + isInteractive = true + } +} 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 new file mode 100644 index 00000000..7d57be68 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/TileEntity.kt @@ -0,0 +1,69 @@ +package ru.dbotthepony.kstarbound.world.entities.tile + +import ru.dbotthepony.kommons.util.AABB +import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kommons.vector.Vector2i +import ru.dbotthepony.kstarbound.defs.tile.TileDamage +import ru.dbotthepony.kstarbound.network.syncher.networkedSignedInt +import ru.dbotthepony.kstarbound.world.World +import ru.dbotthepony.kstarbound.world.entities.AbstractEntity + +/** + * (Hopefully) Static world entities (Plants, Objects, etc), which reside on cell grid + */ +abstract class TileEntity(path: String) : AbstractEntity(path) { + protected val xTilePositionNet = networkedSignedInt() + protected val yTilePositionNet = networkedSignedInt() + + init { + xTilePositionNet.addListener(::updateSpatialIndex) + yTilePositionNet.addListener(::updateSpatialIndex) + } + + abstract val metaBoundingBox: AABB + + private fun updateSpatialIndex() { + val spatialEntry = spatialEntry ?: return + spatialEntry.fixture.move(metaBoundingBox + position) + } + + var xTilePosition: Int + get() = xTilePositionNet.get() + set(value) { + if (isSpawned) { + xTilePositionNet.accept(world.geometry.x.cell(value)) + } else { + xTilePositionNet.accept(value) + } + } + + var yTilePosition: Int + get() = yTilePositionNet.get() + set(value) { + if (isSpawned) { + yTilePositionNet.accept(world.geometry.x.cell(value)) + } else { + yTilePositionNet.accept(value) + } + } + + var tilePosition: Vector2i + get() = Vector2i(xTilePosition, yTilePosition) + set(value) { + xTilePosition = value.x + yTilePosition = value.y + } + + override val position: Vector2d + get() = Vector2d(xTilePosition.toDouble(), yTilePosition.toDouble()) + + abstract val occupySpaces: Set + abstract fun damage(damageSpaces: List, source: Vector2d, damage: TileDamage): Boolean + + override fun onJoinWorld(world: World<*, *>) { + updateSpatialIndex() + } + + override fun onRemove(world: World<*, *>, isDeath: Boolean) { + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt new file mode 100644 index 00000000..de552ad4 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt @@ -0,0 +1,375 @@ +package ru.dbotthepony.kstarbound.world.entities.tile + +import com.google.common.collect.ImmutableList +import com.google.common.collect.ImmutableMap +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.reflect.TypeToken +import ru.dbotthepony.kommons.math.RGBAColor +import ru.dbotthepony.kommons.vector.Vector2i +import ru.dbotthepony.kstarbound.Registries +import ru.dbotthepony.kstarbound.Registry +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.client.StarboundClient +import ru.dbotthepony.kstarbound.client.render.LayeredRenderer +import ru.dbotthepony.kstarbound.defs.Drawable +import ru.dbotthepony.kstarbound.defs.image.SpriteReference +import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition +import ru.dbotthepony.kstarbound.defs.`object`.ObjectOrientation +import ru.dbotthepony.kommons.gson.get +import ru.dbotthepony.kommons.gson.set +import ru.dbotthepony.kommons.io.RGBACodec +import ru.dbotthepony.kommons.io.StreamCodec +import ru.dbotthepony.kommons.io.Vector2iCodec +import ru.dbotthepony.kommons.io.map +import ru.dbotthepony.kommons.io.writeBinaryString +import ru.dbotthepony.kommons.util.AABB +import ru.dbotthepony.kommons.util.KOptional +import ru.dbotthepony.kommons.util.getValue +import ru.dbotthepony.kommons.util.setValue +import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kstarbound.client.world.ClientWorld +import ru.dbotthepony.kstarbound.defs.DamageSource +import ru.dbotthepony.kstarbound.defs.EntityType +import ru.dbotthepony.kstarbound.defs.`object`.ObjectType +import ru.dbotthepony.kstarbound.defs.quest.QuestArcDescriptor +import ru.dbotthepony.kstarbound.defs.tile.TileDamage +import ru.dbotthepony.kstarbound.json.JsonPath +import ru.dbotthepony.kstarbound.json.mergeJson +import ru.dbotthepony.kstarbound.json.stream +import ru.dbotthepony.kstarbound.network.syncher.InternedStringCodec +import ru.dbotthepony.kstarbound.network.syncher.JsonElementCodec +import ru.dbotthepony.kstarbound.network.syncher.NetworkedList +import ru.dbotthepony.kstarbound.network.syncher.NetworkedMap +import ru.dbotthepony.kstarbound.network.syncher.UnsignedShortCodec +import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean +import ru.dbotthepony.kstarbound.network.syncher.networkedData +import ru.dbotthepony.kstarbound.network.syncher.networkedEnum +import ru.dbotthepony.kstarbound.network.syncher.networkedEventCounter +import ru.dbotthepony.kstarbound.network.syncher.networkedFloat +import ru.dbotthepony.kstarbound.network.syncher.networkedJsonElement +import ru.dbotthepony.kstarbound.network.syncher.networkedPointer +import ru.dbotthepony.kstarbound.network.syncher.networkedString +import ru.dbotthepony.kstarbound.json.writeJsonElement +import ru.dbotthepony.kstarbound.util.asStringOrNull +import ru.dbotthepony.kstarbound.world.Direction +import ru.dbotthepony.kstarbound.world.LightCalculator +import ru.dbotthepony.kstarbound.world.PIXELS_IN_STARBOUND_UNITf +import ru.dbotthepony.kstarbound.world.TileHealth +import ru.dbotthepony.kstarbound.world.World +import ru.dbotthepony.kstarbound.world.api.TileColor +import ru.dbotthepony.kstarbound.world.entities.Animator +import ru.dbotthepony.kstarbound.world.entities.wire.WireConnection +import java.io.DataOutputStream +import java.util.HashMap + +open class WorldObject(val config: Registry.Entry) : TileEntity(config.file?.computeDirectory() ?: "/") { + open fun deserialize(data: JsonObject) { + direction = data.get("direction", directions) { Direction.LEFT } + orientationIndex = data.get("orientationIndex", -1).toLong() + isInteractive = data.get("interactive", false) + tilePosition = data.get("tilePosition", vectors) + + uniqueID.accept(KOptional.ofNullable(data["uniqueId"]?.asStringOrNull)) + + for ((k, v) in data.get("parameters") { JsonObject() }.entrySet()) { + parameters[k] = v + } + } + + open fun serialize(): JsonObject { + val into = JsonObject() + into["name"] = config.key + into["tilePosition"] = vectors.toJsonTree(tilePosition) + into["direction"] = directions.toJsonTree(direction) + into["orientationIndex"] = orientationIndex + into["interactive"] = isInteractive + + uniqueID.get().ifPresent { + into["uniqueId"] = it + } + + into["parameters"] = JsonObject().also { + for ((k, v) in parameters) { + it[k] = v.deepCopy() + } + } + + return into + } + + override val metaBoundingBox: AABB by LazyData { + orientation?.metaBoundBox ?: orientation?.let { AABB(it.boundingBox.mins.toDoubleVector() + Vector2d.NEGATIVE_XY, it.boundingBox.maxs.toDoubleVector() + Vector2d(2.0, 2.0)) } ?: AABB.ZERO + } + + val parameters = NetworkedMap(InternedStringCodec, JsonElementCodec).also { + networkGroup.upstream.add(it) + it.addListener(Runnable { + invalidate() + }) + } + + val orientation: ObjectOrientation? get() { + return config.value.orientations.getOrNull(orientationIndex.toInt()) + } + + protected val mergedJson = LazyData { + val orientation = orientation + + if (orientation == null) { + mergeJson(config.jsonObject.deepCopy(), parameters) + } else { + mergeJson(mergeJson(config.jsonObject.deepCopy(), orientation.json), parameters) + } + } + + final override fun lookupProperty(path: JsonPath, orElse: () -> JsonElement): JsonElement { + return path.get(mergedJson.value, orElse) + } + + init { + networkGroup.upstream.add(uniqueID) + } + + var isInteractive by networkedBoolean().also { networkGroup.upstream.add(it) } + var materialSpaces = NetworkedList(materialSpacesCodec, materialSpacesCodecLegacy).also { networkGroup.upstream.add(it) } + + init { + networkGroup.upstream.add(xTilePositionNet) + networkGroup.upstream.add(yTilePositionNet) + } + + var direction by networkedEnum(Direction.LEFT).also { networkGroup.upstream.add(it) } + var health by networkedFloat().also { networkGroup.upstream.add(it) } + + var orientationIndex by networkedPointer().also { + networkGroup.upstream.add(it) + it.addListener(Runnable { invalidate() }) + } + + private val networkedRenderKeys = NetworkedMap(InternedStringCodec, InternedStringCodec).also { networkGroup.upstream.add(it) } + private val localRenderKeys = HashMap() + + var soundEffectEnabled by networkedBoolean(true).also { networkGroup.upstream.add(it) } + var lightSourceColor by networkedData(RGBAColor.TRANSPARENT_BLACK, RGBACodec).also { networkGroup.upstream.add(it) } + val newChatMessageEvent = networkedEventCounter().also { networkGroup.upstream.add(it) } + val chatMessage by networkedString().also { networkGroup.upstream.add(it) } + val chatPortrait by networkedString().also { networkGroup.upstream.add(it) } + val chatConfig by networkedJsonElement().also { networkGroup.upstream.add(it) } + + inner class WireNode(val position: Vector2i) { + var state by networkedBoolean().also { networkGroup.upstream.add(it) } + val connections = NetworkedList(WireConnection.CODEC).also { networkGroup.upstream.add(it) } + } + + val inputNodes: ImmutableList = lookupProperty(JsonPath("inputNodes")) { JsonArray() } + .asJsonArray + .stream() + .map { WireNode(vectors.fromJsonTree(it)) } + .collect(ImmutableList.toImmutableList()) + + val outputNodes: ImmutableList = lookupProperty(JsonPath("outputNodes")) { JsonArray() } + .asJsonArray + .stream() + .map { WireNode(vectors.fromJsonTree(it)) } + .collect(ImmutableList.toImmutableList()) + + val offeredQuests = NetworkedList(QuestArcDescriptor.CODEC, QuestArcDescriptor.LEGACY_CODEC).also { networkGroup.upstream.add(it) } + val turnInQuests = NetworkedList(InternedStringCodec).also { networkGroup.upstream.add(it) } + val damageSources = NetworkedList(DamageSource.CODEC, DamageSource.LEGACY_CODEC).also { networkGroup.upstream.add(it) } + + // don't interpolate scripted animation parameters + val scriptedAnimationParameters = NetworkedMap(InternedStringCodec, JsonElementCodec).also { networkGroup.upstream.add(it, false) } + + // Why is this a thing when we have 'health' field??????? Hello chucklefish again? + val tileHealth = TileHealth.TileEntity().also { networkGroup.upstream.add(it.networkGroup) } + val animator: Animator + + init { + if (config.value.animation?.value != null) { + animator = Animator(config.value.animation!!.value!!).also { networkGroup.upstream.add(it.networkGroup) } + } else { + animator = Animator().also { networkGroup.upstream.add(it.networkGroup) } + } + } + + val unbreakable by LazyData { + lookupProperty(JsonPath("unbreakable")) { JsonPrimitive(false) }.asBoolean + } + + override val type: EntityType + get() = EntityType.OBJECT + + override fun setProperty0(key: JsonPath, value: JsonElement) { + TODO("Not yet implemented") + } + + override fun writeNetwork(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeBinaryString(config.key) + stream.writeJsonElement(JsonObject().also { + for ((k, v) in parameters) { + it[k] = v.deepCopy() + } + }) + } + + private var frameTimer = 0.0 + + private var frame = 0 + set(value) { + if (field != value) { + field = value + drawablesCache.invalidate() + } + } + + val flickerPeriod = config.value.flickerPeriod?.copy() + + // + // json driven properties + // + var color: TileColor by Property(JsonPath("color"), TileColor.DEFAULT) + + var animationParts: ImmutableMap by Property(JsonPath("animationParts")) + var imagePosition: Vector2i by Property(JsonPath("imagePosition"), Vector2i.ZERO) + var animationPosition: Vector2i by Property(JsonPath("animationPosition"), Vector2i.ZERO) + + private val drawablesCache = LazyData { + orientation?.drawables?.map { it.with(::getRenderParam) } ?: listOf() + } + + init { + networkedRenderKeys.addListener(Runnable { drawablesCache.invalidate() }) + } + + val drawables: List by drawablesCache + + override val occupySpaces get() = orientation?.occupySpaces ?: setOf() + + fun getRenderParam(key: String): String? { + return localRenderKeys[key] ?: networkedRenderKeys[key] ?: "default" + } + + protected fun setImageKey(key: String, value: String) { + val old = localRenderKeys.put(key, value) + + if (old != value) { + drawablesCache.invalidate() + } + + if (!isRemote && networkedRenderKeys[key] != value) { + networkedRenderKeys[key] = value + } + } + + override fun invalidate() { + super.invalidate() + drawablesCache.invalidate() + } + + override fun tick() { + super.tick() + + flickerPeriod?.update(Starbound.TIMESTEP, world.random) + + if (!isRemote) { + tileHealth.tick(config.value.damageConfig) + animator.tick() + + val orientation = orientation + + if (orientation != null) { + frameTimer = (frameTimer + Starbound.TIMESTEP) % orientation.animationCycle + val oldFrame = frame + frame = (frameTimer / orientation.animationCycle * orientation.frames).toInt().coerceIn(0, orientation.frames) + + if (oldFrame != frame) { + setImageKey("frame", frame.toString()) + } + } + } + } + + override fun onJoinWorld(world: World<*, *>) { + super.onJoinWorld(world) + setImageKey("color", lookupProperty(JsonPath("color")) { JsonPrimitive("default") }.asString) + } + + override fun damage(damageSpaces: List, source: Vector2d, damage: TileDamage): Boolean { + if (unbreakable) + return false + + tileHealth.damage(config.value.damageConfig, source, damage) + return tileHealth.isDead + } + + val lightColors: ImmutableMap by LazyData { + val lightColor = lookupProperty(lightColorPath) + + if (!lightColor.isJsonNull) { + return@LazyData ImmutableMap.of("default", colors0.fromJsonTree(lightColor)) + } + + val lightColors = lookupProperty(lightColorsPath) + + if (!lightColors.isJsonNull) { + return@LazyData colors1.fromJsonTree(lightColors) + } + + ImmutableMap.of() + } + + override fun addLights(lightCalculator: LightCalculator, xOffset: Int, yOffset: Int) { + var color = lightColors[color.lowercase] + + if (color != null) { + if (flickerPeriod != null) { + val sample = flickerPeriod.sinValue().toFloat() + color *= sample + } + + lightCalculator.addPointLight(tilePosition.x - xOffset, tilePosition.y - yOffset, color) + } + } + + override fun render(client: StarboundClient, layers: LayeredRenderer) { + val layer = layers.getLayer(orientation?.renderLayer ?: return) + + drawables.forEach { + val (x, y) = imagePosition + it.render(client, layer, position.x.toFloat() + x / PIXELS_IN_STARBOUND_UNITf, position.y.toFloat() + y / PIXELS_IN_STARBOUND_UNITf) + } + } + + companion object { + private val lightColorPath = JsonPath("lightColor") + private val lightColorsPath = JsonPath("lightColors") + private val materialSpacesCodec = StreamCodec.Pair(Vector2iCodec, InternedStringCodec.map({ Registries.tiles.ref(this) }, { entry?.key ?: key.left.orElse("") })) + private val materialSpacesCodecLegacy = StreamCodec.Pair(Vector2iCodec, UnsignedShortCodec.map({ Registries.tiles.ref(this) }, { entry?.id ?: 65534 })) + + private val colors1 by lazy { Starbound.gson.getAdapter(TypeToken.getParameterized(ImmutableMap::class.java, String::class.java, RGBAColor::class.java)) as TypeAdapter> } + private val colors0 by lazy { Starbound.gson.getAdapter(RGBAColor::class.java) } + private val strings by lazy { Starbound.gson.getAdapter(String::class.java) } + private val directions by lazy { Starbound.gson.getAdapter(Direction::class.java) } + private val vectors by lazy { Starbound.gson.getAdapter(Vector2i::class.java) } + + fun fromJson(content: JsonObject): WorldObject { + val prototype = Registries.worldObjects[content["name"]?.asString ?: throw IllegalArgumentException("Missing object name")] ?: throw IllegalArgumentException("No such object defined for '${content["name"]}'") + + val result = when (prototype.value.objectType) { + ObjectType.OBJECT -> WorldObject(prototype) + ObjectType.LOUNGEABLE -> LoungeableObject(prototype) + ObjectType.CONTAINER -> ContainerObject(prototype) + ObjectType.FARMABLE -> TODO("ObjectType.FARMABLE") + ObjectType.TELEPORTER -> TODO("ObjectType.TELEPORTER") + ObjectType.PHYSICS -> TODO("ObjectType.PHYSICS") + } + + result.deserialize(content) + return result + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/wire/WireConnection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/wire/WireConnection.kt new file mode 100644 index 00000000..06aa0755 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/wire/WireConnection.kt @@ -0,0 +1,23 @@ +package ru.dbotthepony.kstarbound.world.entities.wire + +import ru.dbotthepony.kommons.io.StreamCodec +import ru.dbotthepony.kommons.io.readVector2i +import ru.dbotthepony.kommons.io.writeStruct2i +import ru.dbotthepony.kommons.io.writeVarLong +import ru.dbotthepony.kommons.vector.Vector2i +import ru.dbotthepony.kstarbound.network.syncher.SizeTCodec +import java.io.DataInputStream +import java.io.DataOutputStream + +data class WireConnection(val entityLocation: Vector2i, val index: Int = -1) { + constructor(stream: DataInputStream) : this(stream.readVector2i(), SizeTCodec.read(stream).toInt()) + + fun write(stream: DataOutputStream) { + stream.writeStruct2i((entityLocation)) + SizeTCodec.write(stream, index.toLong()) + } + + companion object { + val CODEC = StreamCodec.Impl(::WireConnection, { a, b -> b.write(a) }) + } +} diff --git a/src/test/kotlin/ru/dbotthepony/kstarbound/test/CollectionTests.kt b/src/test/kotlin/ru/dbotthepony/kstarbound/test/CollectionTests.kt new file mode 100644 index 00000000..abb9a067 --- /dev/null +++ b/src/test/kotlin/ru/dbotthepony/kstarbound/test/CollectionTests.kt @@ -0,0 +1,47 @@ +package ru.dbotthepony.kstarbound.test + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import ru.dbotthepony.kstarbound.collect.RandomSubList + +object CollectionTests { + @Test + @DisplayName("Sublists test") + fun subLists() { + val list = ArrayList() + val list2 = ArrayList() + + for (i in 0 .. 1000) { + list.add(i) + list2.add(i) + } + + val sub0 = list.subList(40, 80) + val sub1 = RandomSubList(list2, 40, 80) + + assertEquals(list, list2) + + assertEquals(40, sub0.size) + assertEquals(40, sub1.size) + + assertEquals(sub0, sub1) + + assertEquals(45, sub0.removeAt(5)) + assertEquals(45, sub1.removeAt(5)) + + assertEquals(list, list2) + assertEquals(sub0, sub1) + + assertEquals(46, sub0.removeAt(5)) + assertEquals(46, sub1.removeAt(5)) + + assertEquals(list, list2) + assertEquals(sub0, sub1) + + assertEquals(38, sub0.size) + assertEquals(38, sub1.size) + + assertEquals(list, list2) + } +} \ No newline at end of file