TileEntities, WorldObject

This commit is contained in:
DBotThePony 2024-04-02 20:07:11 +07:00
parent cb63b47b12
commit 209c1a5776
Signed by: DBot
GPG Key ID: DCC23B5715498507
65 changed files with 3038 additions and 729 deletions

View File

@ -8,12 +8,8 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectMap
import it.unimi.dsi.fastutil.ints.Int2ObjectMaps import it.unimi.dsi.fastutil.ints.Int2ObjectMaps
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.Object2ObjectFunction 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 org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kstarbound.util.traverseJsonPath
import java.util.Collections import java.util.Collections
import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
@ -63,10 +59,6 @@ class Registry<T : Any>(val name: String) {
val value: T? val value: T?
get() = entry?.value get() = entry?.value
fun traverseJsonPath(path: String): JsonElement? {
return traverseJsonPath(path, entry?.json ?: return null)
}
final override fun get(): Entry<T>? { final override fun get(): Entry<T>? {
return entry return entry
} }
@ -82,10 +74,6 @@ class Registry<T : Any>(val name: String) {
abstract val isBuiltin: Boolean abstract val isBuiltin: Boolean
abstract val ref: Ref<T> abstract val ref: Ref<T>
fun traverseJsonPath(path: String): JsonElement? {
return traverseJsonPath(path, json)
}
final override fun get(): T { final override fun get(): T {
return value return value
} }

View File

@ -35,6 +35,7 @@ import ru.dbotthepony.kstarbound.defs.`object`.ObjectOrientation
import ru.dbotthepony.kstarbound.defs.actor.player.BlueprintLearnList import ru.dbotthepony.kstarbound.defs.actor.player.BlueprintLearnList
import ru.dbotthepony.kstarbound.defs.animation.Particle import ru.dbotthepony.kstarbound.defs.animation.Particle
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor 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.CelestialParameters
import ru.dbotthepony.kstarbound.defs.world.VisitableWorldParametersType import ru.dbotthepony.kstarbound.defs.world.VisitableWorldParametersType
import ru.dbotthepony.kstarbound.defs.world.BiomePlaceables 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.EnumAdapter
import ru.dbotthepony.kstarbound.json.builder.BuilderAdapter import ru.dbotthepony.kstarbound.json.builder.BuilderAdapter
import ru.dbotthepony.kstarbound.json.builder.FactoryAdapter 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.CollectionAdapterFactory
import ru.dbotthepony.kstarbound.json.factory.ImmutableCollectionAdapterFactory import ru.dbotthepony.kstarbound.json.factory.ImmutableCollectionAdapterFactory
import ru.dbotthepony.kstarbound.json.factory.PairAdapterFactory 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.math.*
import ru.dbotthepony.kstarbound.server.world.UniverseChunk import ru.dbotthepony.kstarbound.server.world.UniverseChunk
import ru.dbotthepony.kstarbound.item.ItemStack 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.json.NativeLegacy
import ru.dbotthepony.kstarbound.util.Directives import ru.dbotthepony.kstarbound.util.Directives
import ru.dbotthepony.kstarbound.util.ExceptionLogger import ru.dbotthepony.kstarbound.util.ExceptionLogger
import ru.dbotthepony.kstarbound.util.SBPattern import ru.dbotthepony.kstarbound.util.SBPattern
import ru.dbotthepony.kstarbound.util.HashTableInterner import ru.dbotthepony.kstarbound.util.HashTableInterner
import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise
import ru.dbotthepony.kstarbound.util.traverseJsonPath
import ru.dbotthepony.kstarbound.world.UniversePos import ru.dbotthepony.kstarbound.world.UniversePos
import ru.dbotthepony.kstarbound.world.physics.Poly import ru.dbotthepony.kstarbound.world.physics.Poly
import java.io.* import java.io.*
@ -157,9 +159,14 @@ object Starbound : ISBFileLocator {
@JvmField @JvmField
val STRINGS: Interner<String> = interner(5) val STRINGS: Interner<String> = 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 } var IS_WRITING_LEGACY_JSON: Boolean by ThreadLocal.withInitial { false }
private set private set
var IS_WRITING_STORE_JSON: Boolean by ThreadLocal.withInitial { false }
private set
fun writeLegacyJson(data: Any): JsonElement { fun writeLegacyJson(data: Any): JsonElement {
try { try {
@ -197,6 +204,7 @@ object Starbound : ISBFileLocator {
// Обработчик @JsonImplementation // Обработчик @JsonImplementation
registerTypeAdapterFactory(JsonImplementationTypeFactory) registerTypeAdapterFactory(JsonImplementationTypeFactory)
registerTypeAdapterFactory(JsonAdapterTypeFactory)
// списки, наборы, т.п. // списки, наборы, т.п.
registerTypeAdapterFactory(CollectionAdapterFactory) registerTypeAdapterFactory(CollectionAdapterFactory)
@ -294,6 +302,7 @@ object Starbound : ISBFileLocator {
registerTypeAdapter(CelestialParameters::Adapter) registerTypeAdapter(CelestialParameters::Adapter)
registerTypeAdapter(Particle::Adapter) registerTypeAdapter(Particle::Adapter)
registerTypeAdapter(QuestParameter::Adapter)
registerTypeAdapterFactory(BiomePlacementDistributionType.DEFINITION_ADAPTER) registerTypeAdapterFactory(BiomePlacementDistributionType.DEFINITION_ADAPTER)
registerTypeAdapterFactory(BiomePlacementItemType.DATA_ADAPTER) registerTypeAdapterFactory(BiomePlacementItemType.DATA_ADAPTER)
@ -385,11 +394,11 @@ object Starbound : ISBFileLocator {
val file = locate(filename) val file = locate(filename)
if (!file.isFile) { if (!file.isFile)
return null 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<File>() private val archivePaths = ArrayList<File>()

View File

@ -7,7 +7,7 @@ import ru.dbotthepony.kstarbound.client.ClientConnection
import ru.dbotthepony.kstarbound.json.readJsonObject import ru.dbotthepony.kstarbound.json.readJsonObject
import ru.dbotthepony.kstarbound.json.writeJsonObject import ru.dbotthepony.kstarbound.json.writeJsonObject
import ru.dbotthepony.kstarbound.network.IClientPacket 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.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream
import java.util.UUID import java.util.UUID

View File

@ -0,0 +1,41 @@
package ru.dbotthepony.kstarbound.collect
class RandomListIterator<E>(private val elements: MutableList<E>, index: Int = 0) : MutableListIterator<E> {
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
}
}

View File

@ -0,0 +1,177 @@
package ru.dbotthepony.kstarbound.collect
class RandomSubList<E>(private val elements: MutableList<E>, private val fromIndex: Int, private var toIndex: Int) : MutableList<E> {
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<E>): 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<E> {
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<E>): 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<E>): Boolean {
return addAll(toIndex, elements)
}
override fun clear() {
for (i in fromIndex until toIndex) {
elements.removeAt(fromIndex)
}
toIndex = fromIndex
}
override fun listIterator(): MutableListIterator<E> {
return RandomListIterator(this)
}
override fun listIterator(index: Int): MutableListIterator<E> {
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<E>): 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<E>): 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<E> {
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<E> = 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()}]"
}
}

View File

@ -2,14 +2,42 @@ package ru.dbotthepony.kstarbound.defs
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableSet import com.google.common.collect.ImmutableSet
import com.google.gson.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.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.IStringSerializable
import ru.dbotthepony.kstarbound.json.builder.JsonFactory 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.legacyCodec
import ru.dbotthepony.kstarbound.network.syncher.nativeCodec import ru.dbotthepony.kstarbound.network.syncher.nativeCodec
import ru.dbotthepony.kstarbound.world.physics.Poly
import java.io.DataInputStream import java.io.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream
// uint8_t
enum class TeamType(override val jsonName: String) : IStringSerializable { enum class TeamType(override val jsonName: String) : IStringSerializable {
NULL("null"), NULL("null"),
// non-PvP-enabled players and player allied NPCs // non-PvP-enabled players and player allied NPCs
@ -31,6 +59,7 @@ enum class TeamType(override val jsonName: String) : IStringSerializable {
ASSISTANT("assistant"); ASSISTANT("assistant");
} }
// int32_t
enum class HitType(override val jsonName: String) : IStringSerializable { enum class HitType(override val jsonName: String) : IStringSerializable {
HIT("Hit"), HIT("Hit"),
STRONG_HIT("StrongHit"), STRONG_HIT("StrongHit"),
@ -39,6 +68,7 @@ enum class HitType(override val jsonName: String) : IStringSerializable {
KILL("Kill"); KILL("Kill");
} }
// uint8_t
enum class DamageType(override val jsonName: String) : IStringSerializable { enum class DamageType(override val jsonName: String) : IStringSerializable {
NO_DAMAGE("NoDamage"), NO_DAMAGE("NoDamage"),
DAMAGE("Damage"), DAMAGE("Damage"),
@ -58,6 +88,8 @@ data class EntityDamageTeam(val type: TeamType = TeamType.NULL, val team: Int =
} }
companion object { companion object {
val NULL = EntityDamageTeam()
val PASSIVE = EntityDamageTeam(TeamType.PASSIVE)
val CODEC = nativeCodec(::EntityDamageTeam, EntityDamageTeam::write) val CODEC = nativeCodec(::EntityDamageTeam, EntityDamageTeam::write)
val LEGACY_CODEC = legacyCodec(::EntityDamageTeam, EntityDamageTeam::write) val LEGACY_CODEC = legacyCodec(::EntityDamageTeam, EntityDamageTeam::write)
} }
@ -72,3 +104,116 @@ data class TouchDamage(
val knockback: Double = 0.0, val knockback: Double = 0.0,
val statusEffects: ImmutableSet<String> = ImmutableSet.of(), val statusEffects: ImmutableSet<String> = 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<Poly, Pair<Vector2d, Vector2d>>,
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<EphemeralStatusEffect> = ImmutableList.of(),
val knockback: Either<Double, Vector2d>,
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<Vector2d, Vector2d>? = 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<EphemeralStatusEffect> = ImmutableList.of(),
val knockback: Either<Double, Vector2d> = 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<DamageSource>() {
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)
}
}

View File

@ -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<EphemeralStatusEffect>() {
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)
}
}

View File

@ -1,18 +1,18 @@
package ru.dbotthepony.kstarbound.defs package ru.dbotthepony.kstarbound.defs
import com.google.gson.JsonArray
import com.google.gson.JsonElement import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonObject import com.google.gson.JsonObject
import com.google.gson.TypeAdapter import com.google.gson.TypeAdapter
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.util.AssetPathStack import ru.dbotthepony.kstarbound.util.AssetPathStack
import ru.dbotthepony.kommons.gson.set import ru.dbotthepony.kommons.util.Delegate
import java.util.function.Consumer import ru.dbotthepony.kstarbound.json.JsonPath
import java.util.function.Function import java.util.function.Function
import java.util.function.Supplier import java.util.function.Supplier
import kotlin.properties.Delegates
import kotlin.properties.ReadWriteProperty import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
import kotlin.reflect.javaType import kotlin.reflect.javaType
@ -22,36 +22,32 @@ import kotlin.reflect.javaType
*/ */
abstract class JsonDriven(val path: String) { abstract class JsonDriven(val path: String) {
private val delegates = ArrayList<Property<*>>() private val delegates = ArrayList<Property<*>>()
private val delegatesMap = HashMap<String, ArrayList<Property<*>>>()
private val lazies = ArrayList<LazyData<*>>() private val lazies = ArrayList<LazyData<*>>()
private val namedLazies = HashMap<String, ArrayList<LazyData<*>>>()
protected val properties = JsonObject()
/** /**
* [JsonObject]s which define behavior of properties * [JsonObject]s which define behavior of properties
*/ */
protected abstract fun defs(): Collection<JsonObject> 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() { protected open fun invalidate() {
delegates.forEach { it.invalidate() } delegates.forEach { it.invalidate() }
lazies.forEach { it.invalidate() } lazies.forEach { it.invalidate() }
} }
protected open fun invalidate(name: String) { inner class LazyData<T>(private val initializer: () -> T) : Lazy<T> {
delegatesMap[name]?.forEach { it.invalidate() }
namedLazies[name]?.forEach { it.invalidate() }
lazies.forEach { it.invalidate() }
}
inner class LazyData<T>(names: Iterable<String> = listOf(), private val initializer: () -> T) : Lazy<T> {
constructor(initializer: () -> T) : this(listOf(), initializer)
init { init {
for (name in names) { lazies.add(this)
namedLazies.computeIfAbsent(name, Function { ArrayList() }).add(this)
}
} }
private var _value: Any? = mark private var _value: Any? = mark
@ -78,50 +74,35 @@ abstract class JsonDriven(val path: String) {
} }
inner class Property<T>( inner class Property<T>(
name: String? = null, val name: JsonPath,
val default: Either<Supplier<T>, JsonElement>? = null, val default: Either<Supplier<T>, JsonElement>? = null,
private var adapter: TypeAdapter<T>? = null, private var adapter: TypeAdapter<T>? = null,
) : Supplier<T>, Consumer<T>, ReadWriteProperty<Any?, T> { ) : Delegate<T>, ReadWriteProperty<Any?, T> {
constructor(name: String, default: T, adapter: TypeAdapter<T>? = null) : this(name, Either.left(Supplier { default }), adapter) constructor(name: JsonPath, default: T, adapter: TypeAdapter<T>? = null) : this(name, Either.left(Supplier { default }), adapter)
constructor(name: String, default: Supplier<T>, adapter: TypeAdapter<T>? = null) : this(name, Either.left(default), adapter) constructor(name: JsonPath, default: Supplier<T>, adapter: TypeAdapter<T>? = null) : this(name, Either.left(default), adapter)
constructor(name: String, default: JsonElement, adapter: TypeAdapter<T>? = null) : this(name, Either.right(default), adapter) constructor(name: JsonPath, default: JsonElement, adapter: TypeAdapter<T>? = null) : this(name, Either.right(default), adapter)
constructor(default: T, adapter: TypeAdapter<T>? = null) : this(null, Either.left(Supplier { default }), adapter)
constructor(default: Supplier<T>, adapter: TypeAdapter<T>? = null) : this(null, Either.left(default), adapter)
constructor(default: JsonElement, adapter: TypeAdapter<T>? = 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)
}
init { init {
delegates.add(this) delegates.add(this)
if (name != null)
delegatesMap.computeIfAbsent(name, Function { ArrayList() }).add(this)
} }
private var value: Supplier<T> = never as Supplier<T> private var value: Supplier<T> by Delegates.notNull()
private fun compute(): T { private fun compute(): T {
val value = dataValue(checkNotNull(name)) val value = lookupProperty(name)
if (value == null) { if (value.isJsonNull) {
if (default == null) { if (default == null) {
throw NoSuchElementException("No json value present at '$name', and no default value was provided") throw NoSuchElementException("No json value present at '$name', and no default value was provided")
} else if (default.isLeft) { } else if (default.isLeft) {
return default.left().get() return default.left().get()
} else { } else {
AssetPathStack.block(path) { AssetPathStack(this@JsonDriven.path) {
return adapter!!.fromJsonTree(default.right()) return adapter!!.fromJsonTree(default.right())
} }
} }
} else { } else {
AssetPathStack.block(path) { AssetPathStack(this@JsonDriven.path) {
return adapter!!.fromJsonTree(value) return adapter!!.fromJsonTree(value)
} }
} }
@ -144,12 +125,11 @@ abstract class JsonDriven(val path: String) {
} }
override fun accept(t: T) { override fun accept(t: T) {
AssetPathStack.block(path) { AssetPathStack(this@JsonDriven.path) {
properties[checkNotNull(name)] = adapter!!.toJsonTree(t) setProperty0(name, adapter!!.toJsonTree(t))
} }
// value = Supplier { t } value = Supplier { t }
invalidate(name!!)
} }
@OptIn(ExperimentalStdlibApi::class) @OptIn(ExperimentalStdlibApi::class)
@ -158,10 +138,6 @@ abstract class JsonDriven(val path: String) {
adapter = Starbound.gson.getAdapter(TypeToken.get(property.returnType.javaType)) as TypeAdapter<T> adapter = Starbound.gson.getAdapter(TypeToken.get(property.returnType.javaType)) as TypeAdapter<T>
} }
if (name == null) {
name = property.name
}
return value.get() return value.get()
} }
@ -171,63 +147,11 @@ abstract class JsonDriven(val path: String) {
adapter = Starbound.gson.getAdapter(TypeToken.get(property.returnType.javaType)) as TypeAdapter<T> adapter = Starbound.gson.getAdapter(TypeToken.get(property.returnType.javaType)) as TypeAdapter<T>
} }
if (name == null) {
name = property.name
}
return accept(value) 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 { companion object {
private val mark = Any() 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
}
} }
} }

View File

@ -52,7 +52,7 @@ sealed class SpawnTarget {
} }
override fun resolve(world: ServerWorld): Vector2d? { 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 { override fun toString(): String {

View File

@ -2,10 +2,12 @@ package ru.dbotthepony.kstarbound.defs.actor
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
// uint8_t
enum class Gender(override val jsonName: String) : IStringSerializable { enum class Gender(override val jsonName: String) : IStringSerializable {
MALE("Male"), FEMALE("Female"); MALE("Male"), FEMALE("Female");
} }
// int32_t
enum class HumanoidEmote(override val jsonName: String) : IStringSerializable { enum class HumanoidEmote(override val jsonName: String) : IStringSerializable {
IDLE("Idle"), IDLE("Idle"),
BLABBERING("Blabbering"), BLABBERING("Blabbering"),

View File

@ -1,10 +1,12 @@
package ru.dbotthepony.kstarbound.defs.animation package ru.dbotthepony.kstarbound.defs.animation
import com.google.common.collect.ImmutableMap import com.google.common.collect.ImmutableMap
import com.google.gson.JsonArray
import com.google.gson.JsonObject import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.objects.Object2ObjectAVLTreeMap import it.unimi.dsi.fastutil.objects.Object2ObjectAVLTreeMap
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import kotlin.properties.Delegates
@JsonFactory @JsonFactory
data class AnimatedPartsDefinition( data class AnimatedPartsDefinition(
@ -25,6 +27,10 @@ data class AnimatedPartsDefinition(
val states: ImmutableMap<String, State> = ImmutableMap.of(), val states: ImmutableMap<String, State> = ImmutableMap.of(),
val properties: JsonObject = JsonObject(), val properties: JsonObject = JsonObject(),
) { ) {
val sortedStates: ImmutableMap<String, State> = states.entries.stream()
.sorted { o1, o2 -> o1.key.compareTo(o2.key) }
.collect(ImmutableMap.toImmutableMap({ it.key }, { it.value }))
@JsonFactory @JsonFactory
data class State( data class State(
val frames: Int = 1, val frames: Int = 1,
@ -32,8 +38,24 @@ data class AnimatedPartsDefinition(
val mode: AnimationMode = AnimationMode.END, val mode: AnimationMode = AnimationMode.END,
val transition: String = "", val transition: String = "",
val properties: JsonObject = JsonObject(), val properties: JsonObject = JsonObject(),
val frameProperties: JsonObject = JsonObject(), val frameProperties: ImmutableMap<String, JsonArray> = ImmutableMap.of(),
) ) {
var name by Delegates.notNull<String>()
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 @JsonFactory

View File

@ -256,37 +256,40 @@ class Image private constructor(
fun worldSpaces(pixelOffset: Vector2i, spaceScan: Double, flip: Boolean): List<Vector2i> { fun worldSpaces(pixelOffset: Vector2i, spaceScan: Double, flip: Boolean): List<Vector2i> {
if (amountOfChannels != 3 && amountOfChannels != 4) throw IllegalStateException("Can not check world space taken by image with $amountOfChannels color channels") 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 minX = pixelOffset.x / PIXELS_IN_STARBOUND_UNITi
val yDivB = pixelOffset.y % 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 xDivR = (pixelOffset.x + width) % PIXELS_IN_STARBOUND_UNITi val maxY = (height + pixelOffset.y + PIXELS_IN_STARBOUND_UNITi - 1) / 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 result = ArrayList<Vector2i>() val result = ArrayList<Vector2i>()
for (y in bottomMostY .. topMostY) { // this is weird, but that's how original game handles this
for (x in leftMostX .. rightMostX) { // also we don't cache this info since that's a waste of precious ram
val left = x * PIXELS_IN_STARBOUND_UNITi
val bottom = y * PIXELS_IN_STARBOUND_UNITi
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 (y in 0 until PIXELS_IN_STARBOUND_UNITi) {
for (sY in 0 until PIXELS_IN_STARBOUND_UNITi) { val ypixel = (yspace * PIXELS_IN_STARBOUND_UNITi + y - pixelOffset.y)
if (isTransparent(xDivL + sX + left, yDivB + sY + bottom, flip)) {
transparentPixels++ 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) { if (fillRatio >= spaceScan) {
result.add(Vector2i(x, y)) result.add(Vector2i(xspace, yspace))
} }
} }
} }

View File

@ -15,6 +15,7 @@ import org.classdump.luna.LuaRuntimeException
import org.classdump.luna.Table import org.classdump.luna.Table
import org.classdump.luna.TableFactory import org.classdump.luna.TableFactory
import ru.dbotthepony.kommons.gson.consumeNull import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.lua.StateMachine import ru.dbotthepony.kstarbound.lua.StateMachine
import ru.dbotthepony.kstarbound.lua.from import ru.dbotthepony.kstarbound.lua.from
@ -47,12 +48,21 @@ fun ItemDescriptor(data: JsonElement): ItemDescriptor {
val parameters = data.get(2, ::JsonObject) val parameters = data.get(2, ::JsonObject)
return ItemDescriptor(name, count, parameters) return ItemDescriptor(name, count, parameters)
} else if (data is JsonObject) { } else if (data is JsonObject) {
val name = (data.get("name") ?: data.get("item") ?: throw JsonSyntaxException("Missing item name")).asString if ("id" in data && "version" in data && "content" in data) {
val count = data.get("count", 1L) // loading versioned json from original engine
val parameters = data.get("parameters") { data.get("parameters", ::JsonObject) } if (data["id"].asString != "Item")
return ItemDescriptor(name, count, parameters) 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) { } else if (data is JsonNull) {
return ItemDescriptor("air", 0L) return ItemDescriptor.EMPTY
} else { } else {
throw JsonSyntaxException("Invalid item descriptor: $data") throw JsonSyntaxException("Invalid item descriptor: $data")
} }

View File

@ -28,6 +28,8 @@ import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.gson.get import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.gson.getArray import ru.dbotthepony.kommons.gson.getArray
import ru.dbotthepony.kommons.gson.set 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 import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
data class ObjectDefinition( data class ObjectDefinition(
@ -46,8 +48,8 @@ data class ObjectDefinition(
val breakDropOptions: ImmutableList<ImmutableList<ItemDescriptor>>? = null, val breakDropOptions: ImmutableList<ImmutableList<ItemDescriptor>>? = null,
val smashDropPool: Registry.Ref<TreasurePoolDefinition>? = null, val smashDropPool: Registry.Ref<TreasurePoolDefinition>? = null,
val smashDropOptions: ImmutableList<ImmutableList<ItemDescriptor>> = ImmutableList.of(), val smashDropOptions: ImmutableList<ImmutableList<ItemDescriptor>> = ImmutableList.of(),
//val animation: AssetReference<AnimationDefinition>? = null, val animation: AssetReference<AnimationDefinition>? = null,
val animation: AssetPath? = null, //val animation: AssetPath? = null,
val smashSounds: ImmutableSet<AssetPath> = ImmutableSet.of(), val smashSounds: ImmutableSet<AssetPath> = ImmutableSet.of(),
val smashParticles: JsonArray? = null, val smashParticles: JsonArray? = null,
val smashable: Boolean = false, val smashable: Boolean = false,
@ -83,7 +85,7 @@ data class ObjectDefinition(
class Adapter(gson: Gson) : TypeAdapter<ObjectDefinition>() { class Adapter(gson: Gson) : TypeAdapter<ObjectDefinition>() {
@JsonFactory(logMisses = false) @JsonFactory(logMisses = false)
class PlainData( data class PlainData(
val objectName: String, val objectName: String,
val objectType: ObjectType = ObjectType.OBJECT, val objectType: ObjectType = ObjectType.OBJECT,
val race: String = "generic", val race: String = "generic",
@ -99,8 +101,8 @@ data class ObjectDefinition(
val breakDropOptions: ImmutableList<ImmutableList<ItemDescriptor>>? = null, val breakDropOptions: ImmutableList<ImmutableList<ItemDescriptor>>? = null,
val smashDropPool: Registry.Ref<TreasurePoolDefinition>? = null, val smashDropPool: Registry.Ref<TreasurePoolDefinition>? = null,
val smashDropOptions: ImmutableList<ImmutableList<ItemDescriptor>> = ImmutableList.of(), val smashDropOptions: ImmutableList<ImmutableList<ItemDescriptor>> = ImmutableList.of(),
//val animation: AssetReference<AnimationDefinition>? = null, val animation: AssetReference<AnimationDefinition>? = null,
val animation: AssetPath? = null, //val animation: AssetPath? = null,
val smashSounds: ImmutableSet<AssetPath> = ImmutableSet.of(), val smashSounds: ImmutableSet<AssetPath> = ImmutableSet.of(),
val smashParticles: JsonArray? = null, val smashParticles: JsonArray? = null,
val smashable: Boolean = false, val smashable: Boolean = false,

View File

@ -27,6 +27,9 @@ import ru.dbotthepony.kstarbound.json.setAdapter
import ru.dbotthepony.kommons.gson.contains import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.gson.get import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.gson.set 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 ru.dbotthepony.kstarbound.world.Side
import kotlin.math.PI import kotlin.math.PI
@ -45,7 +48,7 @@ data class ObjectOrientation(
val anchors: ImmutableSet<Anchor>, val anchors: ImmutableSet<Anchor>,
val anchorAny: Boolean, val anchorAny: Boolean,
val directionAffinity: Side?, val directionAffinity: Side?,
val materialSpaces: ImmutableList<Pair<Vector2i, String>>, val materialSpaces: ImmutableList<Pair<Vector2i, Registry.Ref<TileDefinition>>>,
val interactiveSpaces: ImmutableSet<Vector2i>, val interactiveSpaces: ImmutableSet<Vector2i>,
val lightPosition: Vector2i, val lightPosition: Vector2i,
val beamAngle: Double, val beamAngle: Double,
@ -170,11 +173,11 @@ data class ObjectOrientation(
} }
} }
var boundingBox = AABBi(Vector2i.ZERO, Vector2i.ZERO) val minX = occupySpaces.minOf { it.x }
val minY = occupySpaces.minOf { it.y }
for (vec in occupySpaces) { val maxX = occupySpaces.maxOf { it.x }
boundingBox = boundingBox.expand(vec) val maxY = occupySpaces.maxOf { it.y }
} val boundingBox = AABBi(Vector2i(minX, minY), Vector2i(maxX, maxY))
val metaBoundBox = obj["metaBoundBox"]?.let { aabbs.fromJsonTree(it) } val metaBoundBox = obj["metaBoundBox"]?.let { aabbs.fromJsonTree(it) }
val requireTilledAnchors = obj.get("requireTilledAnchors", false) val requireTilledAnchors = obj.get("requireTilledAnchors", false)
@ -249,7 +252,7 @@ data class ObjectOrientation(
anchors = anchors.build(), anchors = anchors.build(),
anchorAny = anchorAny, anchorAny = anchorAny,
directionAffinity = directionAffinity, directionAffinity = directionAffinity,
materialSpaces = materialSpaces, materialSpaces = materialSpaces.stream().map { it.first to Registries.tiles.ref(it.second) }.collect(ImmutableList.toImmutableList()),
interactiveSpaces = interactiveSpaces, interactiveSpaces = interactiveSpaces,
lightPosition = lightPosition, lightPosition = lightPosition,
beamAngle = beamAngle, beamAngle = beamAngle,

View File

@ -1,10 +1,12 @@
package ru.dbotthepony.kstarbound.defs.`object` package ru.dbotthepony.kstarbound.defs.`object`
enum class ObjectType { import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
OBJECT,
LOUNGEABLE, enum class ObjectType(override val jsonName: String) : IStringSerializable {
CONTAINER, OBJECT("object"),
FARMABLE, LOUNGEABLE("loungeable"),
TELEPORTER, CONTAINER("container"),
PHYSICS; FARMABLE("farmable"),
TELEPORTER("teleporter"),
PHYSICS("physics");
} }

View File

@ -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<QuestDescriptor> = 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)
}
}

View File

@ -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<String, QuestParameter> = 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())
}
}
}

View File

@ -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<out Detail>) : 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 <bandage>" or
// "any <bandage>", 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<ItemDescriptor>) : 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<QuestParameter>() {
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)
}
}
}
}

View File

@ -1,8 +1,7 @@
package ru.dbotthepony.kstarbound.defs.tile package ru.dbotthepony.kstarbound.defs.tile
import com.google.common.collect.ImmutableMap import com.google.common.collect.ImmutableMap
import it.unimi.dsi.fastutil.objects.Object2DoubleMap import it.unimi.dsi.fastutil.objects.ObjectArraySet
import it.unimi.dsi.fastutil.objects.Object2DoubleMaps
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
@ -29,6 +28,32 @@ data class TileDamageConfig(
return (damageFactorsMapped[damage.type] ?: 1.0) * damage.amount 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<String>()
combinedKeys.addAll(damageFactors.keys)
combinedKeys.addAll(other.damageFactors.keys)
val builder = ImmutableMap.Builder<String, Double>()
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 { companion object {
val EMPTY = TileDamageConfig() val EMPTY = TileDamageConfig()

View File

@ -2,19 +2,19 @@ package ru.dbotthepony.kstarbound.defs.tile
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable 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 // Damage done that will not actually kill the target
PROTECTED("protected"), PROTECTED("protected", false),
// Best at chopping down trees, things made of wood, etc. // Best at chopping down trees, things made of wood, etc.
PLANT("plantish"), PLANT("plantish", false),
// For digging / drilling through materials // For digging / drilling through materials
BLOCK("blockish"), BLOCK("blockish", false),
// Gravity gun etc // Gravity gun etc
BEAM("beamish"), BEAM("beamish", false),
// Penetrating damage done passivly by explosions. // Penetrating damage done passivly by explosions.
EXPLOSIVE("explosive"), EXPLOSIVE("explosive", true),
// Can melt certain block types // Can melt certain block types
FIRE("fire"), FIRE("fire", false),
// Can "till" certain materials into others // Can "till" certain materials into others
TILLING("tilling"); TILLING("tilling", false);
} }

View File

@ -45,6 +45,10 @@ data class TileDefinition(
override val renderTemplate: AssetReference<RenderTemplate>, override val renderTemplate: AssetReference<RenderTemplate>,
override val renderParameters: RenderParameters, override val renderParameters: RenderParameters,
) : IRenderableTile, IThingWithDescription by descriptionData { ) : IRenderableTile, IThingWithDescription by descriptionData {
init {
require(materialId > 0) { "Invalid tile ID $materialId" }
}
val actualDamageTable: TileDamageConfig by lazy { val actualDamageTable: TileDamageConfig by lazy {
val dmg = damageTable.value ?: TileDamageConfig.EMPTY val dmg = damageTable.value ?: TileDamageConfig.EMPTY

View File

@ -1,6 +1,7 @@
package ru.dbotthepony.kstarbound.defs.tile package ru.dbotthepony.kstarbound.defs.tile
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
import ru.dbotthepony.kstarbound.GlobalDefaults
import ru.dbotthepony.kstarbound.defs.AssetReference import ru.dbotthepony.kstarbound.defs.AssetReference
import ru.dbotthepony.kstarbound.defs.IThingWithDescription import ru.dbotthepony.kstarbound.defs.IThingWithDescription
import ru.dbotthepony.kstarbound.defs.ThingDescription import ru.dbotthepony.kstarbound.defs.ThingDescription
@ -12,8 +13,8 @@ data class TileModifierDefinition(
val modId: Int, val modId: Int,
val modName: String, val modName: String,
val itemDrop: String? = null, val itemDrop: String? = null,
val health: Double = 0.0, val health: Double? = null,
val harvestLevel: Double = 0.0, val requiredHarvestLevel: Int? = null,
val breaksWithTile: Boolean = true, val breaksWithTile: Boolean = true,
val grass: Boolean = false, val grass: Boolean = false,
val miningParticle: String? = null, val miningParticle: String? = null,
@ -21,6 +22,9 @@ data class TileModifierDefinition(
val footstepSound: String? = null, val footstepSound: String? = null,
val miningSounds: ImmutableList<String> = ImmutableList.of(), val miningSounds: ImmutableList<String> = ImmutableList.of(),
@Deprecated("", replaceWith = ReplaceWith("this.actualDamageTable"))
val damageTable: AssetReference<TileDamageConfig> = AssetReference(GlobalDefaults::tileDamage),
@JsonFlat @JsonFlat
val descriptionData: ThingDescription, val descriptionData: ThingDescription,
@ -28,6 +32,20 @@ data class TileModifierDefinition(
override val renderParameters: RenderParameters override val renderParameters: RenderParameters
) : IRenderableTile, IThingWithDescription by descriptionData { ) : IRenderableTile, IThingWithDescription by descriptionData {
init { 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!!)
}
} }
} }

View File

@ -23,6 +23,7 @@ import ru.dbotthepony.kstarbound.collect.WeightedList
import ru.dbotthepony.kstarbound.defs.JsonDriven import ru.dbotthepony.kstarbound.defs.JsonDriven
import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.mergeJson
import ru.dbotthepony.kstarbound.json.pairAdapter import ru.dbotthepony.kstarbound.json.pairAdapter
import ru.dbotthepony.kstarbound.json.stream import ru.dbotthepony.kstarbound.json.stream
import ru.dbotthepony.kstarbound.util.binnedChoice import ru.dbotthepony.kstarbound.util.binnedChoice
@ -276,8 +277,8 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
fun generate(typeName: String, sizeName: String, random: RandomGenerator): TerrestrialWorldParameters { fun generate(typeName: String, sizeName: String, random: RandomGenerator): TerrestrialWorldParameters {
val config = GlobalDefaults.terrestrialWorlds.planetDefaults.deepCopy() val config = GlobalDefaults.terrestrialWorlds.planetDefaults.deepCopy()
JsonDriven.mergeNoCopy(config, GlobalDefaults.terrestrialWorlds.planetSizes[sizeName] ?: throw NoSuchElementException("Unknown world size name $sizeName")) mergeJson(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.planetTypes[typeName] ?: throw NoSuchElementException("Unknown world type name $typeName"))
val params = Starbound.gson.fromJson(config, Generic::class.java) val params = Starbound.gson.fromJson(config, Generic::class.java)
@ -352,7 +353,7 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
fun makeRegion(name: String, baseHeight: Int): Pair<Region, Region> { fun makeRegion(name: String, baseHeight: Int): Pair<Region, Region> {
val primaryRegionJson = GlobalDefaults.terrestrialWorlds.regionDefaults.deepCopy() 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 region = readRegion(primaryRegionJson, baseHeight)
val subRegionList = primaryRegionJson.getArray("subRegion") val subRegionList = primaryRegionJson.getArray("subRegion")
@ -361,7 +362,7 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
primaryRegionJson primaryRegionJson
} else { } else {
val result = GlobalDefaults.terrestrialWorlds.regionDefaults.deepCopy() 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 result
}, baseHeight) }, baseHeight)
@ -372,8 +373,7 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
if (layerName !in layers) if (layerName !in layers)
return null return null
val layerConfig = config.getObject("layerDefaults").deepCopy() val layerConfig = mergeJson(config.getObject("layerDefaults").deepCopy(), layers.getObject(layerName))
JsonDriven.mergeNoCopy(layerConfig, layers.getObject(layerName))
if (!layerConfig.get("enabled", false)) if (!layerConfig.get("enabled", false))
return null return null

View File

@ -10,6 +10,7 @@ import ru.dbotthepony.kommons.io.readLong
import ru.dbotthepony.kommons.io.readSignedVarInt import ru.dbotthepony.kommons.io.readSignedVarInt
import ru.dbotthepony.kommons.io.readVector2d import ru.dbotthepony.kommons.io.readVector2d
import ru.dbotthepony.kommons.io.readVector2f import ru.dbotthepony.kommons.io.readVector2f
import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.io.writeDouble import ru.dbotthepony.kommons.io.writeDouble
import ru.dbotthepony.kommons.io.writeFloat import ru.dbotthepony.kommons.io.writeFloat
import ru.dbotthepony.kommons.io.writeLong 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.io.writeStruct2f
import ru.dbotthepony.kommons.math.RGBAColor import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.util.AABB import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kommons.vector.Vector2i
@ -25,6 +27,7 @@ import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.ChunkPos
import java.io.DataInput import java.io.DataInput
import java.io.DataOutput import java.io.DataOutput
import java.io.EOFException
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
@ -111,3 +114,65 @@ fun OutputStream.writeAABB(value: AABB) {
writeStruct2d(value.mins) writeStruct2d(value.mins)
writeStruct2d(value.maxs) 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 : InputStream, T> S.readNullable(reader: S.() -> T): T? {
if (readBoolean())
return reader(this)
else
return null
}
fun <S : OutputStream, T> S.writeNullable(value: T?, writer: S.(T) -> Unit) {
if (value == null)
write(0)
else {
write(1)
writer(value)
}
}
fun <S : InputStream, L, R> S.readMVariant2(left: S.() -> L, right: S.() -> R): Either<L, R>? {
return when (val type = readUnsignedByte()) {
0 -> null
1 -> Either.left(left())
2 -> Either.right(right())
else -> throw IllegalArgumentException("Unknown variant type $type")
}
}
fun <S : OutputStream, L, R> S.writeMVariant2(value: Either<L, R>?, 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()) }

View File

@ -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 <T : Any?> wrap(input: Any, annotation: JsonAdapter): TypeAdapter<T> {
input as TypeAdapter<T>
if (annotation.nullSafe) {
return NullSafeTypeAdapter(input)
} else {
return input
}
}
override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
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}")
}
}

View File

@ -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 <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
val delegate = type.rawType.getAnnotation(JsonImplementation::class.java)
if (delegate != null) {
return gson.getAdapter(delegate.implementingClass.java) as TypeAdapter<T>?
}
return null
}
}

View File

@ -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<Piece>) {
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 "<empty json path>"
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 "<root>"
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<Piece>()
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<Piece>()
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))
}
}
}

View File

@ -8,15 +8,15 @@ import com.google.gson.JsonArray
import com.google.gson.JsonElement import com.google.gson.JsonElement
import com.google.gson.JsonNull import com.google.gson.JsonNull
import com.google.gson.JsonObject import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapter import com.google.gson.TypeAdapter
import com.google.gson.TypeAdapterFactory
import com.google.gson.reflect.TypeToken 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 it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kstarbound.Starbound
inline fun <reified T> Gson.getAdapter(): TypeAdapter<T> {
return getAdapter(object : TypeToken<T>() {})
}
inline fun <reified C : Collection<E>, reified E> Gson.collectionAdapter(): TypeAdapter<C> { inline fun <reified C : Collection<E>, reified E> Gson.collectionAdapter(): TypeAdapter<C> {
return getAdapter(TypeToken.getParameterized(C::class.java, E::class.java)) as TypeAdapter<C> return getAdapter(TypeToken.getParameterized(C::class.java, E::class.java)) as TypeAdapter<C>
@ -53,3 +53,73 @@ inline fun <reified A, reified B> Gson.pairSetAdapter(): TypeAdapter<ImmutableSe
inline fun <reified E> Gson.mutableSetAdapter(): TypeAdapter<ObjectOpenHashSet<E>> { inline fun <reified E> Gson.mutableSetAdapter(): TypeAdapter<ObjectOpenHashSet<E>> {
return collectionAdapter() 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<String, JsonElement>): 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
}

View File

@ -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<T>(private val parent: TypeAdapter<T>) : TypeAdapter<T>() {
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`)
}
}

View File

@ -1,9 +1,5 @@
package ru.dbotthepony.kstarbound.json.builder 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 import kotlin.reflect.KClass
/** /**
@ -91,15 +87,3 @@ annotation class JsonImplementation(val implementingClass: KClass<*>)
@Target(AnnotationTarget.CLASS) @Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)
annotation class JsonSingleton annotation class JsonSingleton
object JsonImplementationTypeFactory : TypeAdapterFactory {
override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
val delegate = type.rawType.getAnnotation(JsonImplementation::class.java)
if (delegate != null) {
return gson.getAdapter(delegate.implementingClass.java) as TypeAdapter<T>?
}
return null
}
}

View File

@ -484,7 +484,7 @@ class FactoryAdapter<T : Any> private constructor(
companion object { companion object {
private val LOGGER = LogManager.getLogger() private val LOGGER = LogManager.getLogger()
fun <T : Any> createFor(kclass: KClass<T>, config: JsonFactory, gson: Gson, stringInterner: Interner<String> = Starbound.STRINGS): TypeAdapter<T> { fun <T : Any> createFor(kclass: KClass<T>, config: JsonFactory = JsonFactory(), gson: Gson, stringInterner: Interner<String> = Starbound.STRINGS): TypeAdapter<T> {
val builder = Builder(kclass) val builder = Builder(kclass)
val properties = kclass.declaredMembers.filterIsInstance<KProperty1<T, *>>() val properties = kclass.declaredMembers.filterIsInstance<KProperty1<T, *>>()

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.kstarbound.math package ru.dbotthepony.kstarbound.math
import kotlin.math.PI
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
/** /**
@ -77,3 +78,25 @@ fun weakDoubleZeroing(value: Double, epsilon: Double = EPSILON): Double {
return value 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))
}

View File

@ -7,7 +7,7 @@ import java.io.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream
class TileDamageUpdatePacket(val x: Int, val y: Int, val isBackground: Boolean, val health: TileHealth) : IClientPacket { 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) { override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeInt(x) stream.writeInt(x)

View File

@ -20,6 +20,7 @@ import ru.dbotthepony.kommons.io.readVarInt
import ru.dbotthepony.kommons.io.readVarLong import ru.dbotthepony.kommons.io.readVarLong
import ru.dbotthepony.kommons.io.writeBinaryString import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.io.writeByteArray import ru.dbotthepony.kommons.io.writeByteArray
import ru.dbotthepony.kommons.io.writeShort
import ru.dbotthepony.kommons.io.writeVarInt import ru.dbotthepony.kommons.io.writeVarInt
import ru.dbotthepony.kommons.io.writeVarLong import ru.dbotthepony.kommons.io.writeVarLong
import ru.dbotthepony.kommons.math.RGBAColor 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 AABBCodecLegacyOptional = StreamCodec.Impl(DataInputStream::readAABBLegacyOptional, DataOutputStream::writeAABBLegacyOptional)
val AABBCodecNative = StreamCodec.Impl(DataInputStream::readAABB, DataOutputStream::writeAABB) val AABBCodecNative = StreamCodec.Impl(DataInputStream::readAABB, DataOutputStream::writeAABB)
val UnsignedShortCodec = StreamCodec.Impl(DataInputStream::readUnsignedShort, DataOutputStream::writeShort)
val ValidatingBooleanCodec = StreamCodec.Impl({ val ValidatingBooleanCodec = StreamCodec.Impl({
when (val read = it.readUnsignedByte()) { when (val read = it.readUnsignedByte()) {
0 -> false 0 -> false
@ -117,6 +120,7 @@ fun networkedAABBNullable(value: KOptional<AABB> = KOptional()) = networkedData(
// this is ugly because of invariant generics, but we must bear with it. // this is ugly because of invariant generics, but we must bear with it.
fun <E> networkedList(codec: StreamCodec<E>): BasicNetworkedElement<List<E>, List<E>> = networkedData(ArrayList(), StreamCodec.Collection(codec, ::ArrayList)) as BasicNetworkedElement<List<E>, List<E>> fun <E> networkedList(codec: StreamCodec<E>): BasicNetworkedElement<List<E>, List<E>> = networkedData(ArrayList(), StreamCodec.Collection(codec, ::ArrayList)) as BasicNetworkedElement<List<E>, List<E>>
fun <E> networkedList(codec: StreamCodec<E>, legacyCodec: StreamCodec<E>): BasicNetworkedElement<List<E>, List<E>> = networkedData(ArrayList(), StreamCodec.Collection(codec, ::ArrayList), StreamCodec.Collection(legacyCodec, ::ArrayList)) as BasicNetworkedElement<List<E>, List<E>>
fun networkedItem(value: ItemStack = ItemStack.EMPTY) = NetworkedItemStack(value) fun networkedItem(value: ItemStack = ItemStack.EMPTY) = NetworkedItemStack(value)
fun networkedStatefulItem(value: ItemStack = ItemStack.EMPTY) = NetworkedStatefulItemStack(value) fun networkedStatefulItem(value: ItemStack = ItemStack.EMPTY) = NetworkedStatefulItemStack(value)

View File

@ -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<E>(
val codec: StreamCodec<E>,
val legacyCodec: StreamCodec<E> = codec,
private val maxBacklogSize: Int = 100,
private val elementsFactory: (Int) -> MutableList<E> = ::ArrayList
) : NetworkedElement(), MutableList<E> {
private val backlog = ArrayDeque<Pair<Long, Entry<E>>>()
private val queue = ArrayDeque<Pair<Double, Entry<E>>>()
private val elements = elementsFactory(10)
private enum class Type {
ADD, REMOVE, CLEAR;
}
private data class Entry<E>(val type: Type, val index: Int, val value: KOptional<E>) {
constructor(index: Int) : this(Type.REMOVE, index, KOptional())
constructor(index: Int, value: E) : this(Type.REMOVE, index, KOptional(value))
fun apply(list: MutableList<E>) {
when (type) {
Type.ADD -> list.add(index, value.value)
Type.REMOVE -> list.removeAt(index)
Type.CLEAR -> list.clear()
}
}
}
private val clearEntry = Entry<E>(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<E> {
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<E>): 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<E> {
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<E>): Boolean {
var newIndex = index
elements.forEach { add(newIndex++, it) }
return true
}
override fun addAll(elements: Collection<E>): 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<E> {
return listIterator(0)
}
override fun listIterator(index: Int): MutableListIterator<E> {
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<E>): 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<E>): 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<E> {
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<E> = 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()}]"
}
}

View File

@ -5,8 +5,10 @@ import ru.dbotthepony.kommons.io.StreamCodec
import ru.dbotthepony.kommons.io.readVarInt import ru.dbotthepony.kommons.io.readVarInt
import ru.dbotthepony.kommons.io.writeVarInt import ru.dbotthepony.kommons.io.writeVarInt
import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.util.Listenable
import java.io.DataInputStream import java.io.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream
import java.util.concurrent.CopyOnWriteArrayList
/** /**
* [isDumb] is responsible for specifying whenever legacy protocol networks entire map each time * [isDumb] is responsible for specifying whenever legacy protocol networks entire map each time
@ -57,6 +59,7 @@ class NetworkedMap<K, V>(
backlog.add(currentVersion() to clearEntry) backlog.add(currentVersion() to clearEntry)
purgeBacklog() purgeBacklog()
listeners.forEach { it.listener.onClear() }
} }
override fun onValueAdded(key: K, value: V) { override fun onValueAdded(key: K, value: V) {
@ -64,6 +67,7 @@ class NetworkedMap<K, V>(
check(!isRemote) { "This map is not owned by this side" } 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)))) backlog.add(currentVersion() to Entry(Action.ADD, KOptional(nativeKey.copy(key)), KOptional(nativeValue.copy(value))))
purgeBacklog() purgeBacklog()
listeners.forEach { it.listener.onValueAdded(key, value) }
} }
override fun onValueRemoved(key: K, value: V) { override fun onValueRemoved(key: K, value: V) {
@ -71,10 +75,31 @@ class NetworkedMap<K, V>(
check(!isRemote) { "This map is not owned by this side" } check(!isRemote) { "This map is not owned by this side" }
backlog.add(currentVersion() to Entry(Action.REMOVE, KOptional(nativeKey.copy(key)), KOptional())) backlog.add(currentVersion() to Entry(Action.REMOVE, KOptional(nativeKey.copy(key)), KOptional()))
purgeBacklog() purgeBacklog()
listeners.forEach { it.listener.onValueRemoved(key, value) }
} }
}) })
} }
private val listeners = CopyOnWriteArrayList<Listener>()
private inner class Listener(val listener: ListenableMap.MapListener<K, V>) : Listenable.L {
init {
listeners.add(this)
}
override fun remove() {
listeners.remove(this)
}
}
fun addListener(listener: ListenableMap.MapListener<K, V>): Listenable.L {
return Listener(listener)
}
fun addListener(listener: Runnable): Listenable.L {
return Listener(ListenableMap.RunnableAdapter(listener))
}
private val dumbCodec by lazy { private val dumbCodec by lazy {
StreamCodec.Map(keyCodec.second, valueCodec.second, ::HashMap) StreamCodec.Map(keyCodec.second, valueCodec.second, ::HashMap)
} }
@ -253,7 +278,7 @@ class NetworkedMap<K, V>(
val change = if (isLegacy) readLegacyEntry(data) else readNativeEntry(data) val change = if (isLegacy) readLegacyEntry(data) else readNativeEntry(data)
backlog.add(currentVersion() to change) backlog.add(currentVersion() to change)
if (isInterpolating && interpolationDelay > 0.0) { if (isInterpolating) {
val actualDelay = interpolationDelay + currentTime val actualDelay = interpolationDelay + currentTime
if (delayed.isNotEmpty() && delayed.last().first > actualDelay) { if (delayed.isNotEmpty() && delayed.last().first > actualDelay) {
@ -261,7 +286,10 @@ class NetworkedMap<K, V>(
delayed.clear() delayed.clear()
} }
delayed.add(actualDelay to change) if (interpolationDelay > 0.0)
delayed.add(actualDelay to change)
else
change.apply(this)
} else { } else {
change.apply(this) change.apply(this)
} }

View File

@ -4,12 +4,12 @@ import com.google.gson.JsonElement
import com.google.gson.JsonObject import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive import com.google.gson.JsonPrimitive
import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import ru.dbotthepony.kommons.guava.immutableMap import ru.dbotthepony.kommons.guava.immutableMap
import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.quest.QuestDescriptor
import ru.dbotthepony.kstarbound.defs.actor.player.TechDefinition import ru.dbotthepony.kstarbound.defs.actor.player.TechDefinition
import ru.dbotthepony.kstarbound.lua.NewLuaState import ru.dbotthepony.kstarbound.lua.NewLuaState
import ru.dbotthepony.kstarbound.lua.luaFunction 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 questId = value["questId"]?.asString ?: throw IllegalArgumentException("Invalid 'questId' in quest descriptor")
val templateId = value["templateId"]?.asString ?: questId val templateId = value["templateId"]?.asString ?: questId
val params = value["parameters"] as? JsonObject ?: JsonObject() val params = value["parameters"] as? JsonObject ?: JsonObject()
val quest = QuestInstance(this, descriptor = QuestDescriptor(questId, templateId, seed, params), serverID = serverID?.let(UUID::fromString), worldID = worldID) //val quest = QuestInstance(this, descriptor = QuestDescriptor(questId, templateId, params, seed), serverID = serverID?.let(UUID::fromString), worldID = worldID)
addQuest(quest) //addQuest(quest)
return quest.id //return quest.id
TODO()
} else { } else {
throw IllegalArgumentException("Invalid quest descriptor: $value") throw IllegalArgumentException("Invalid quest descriptor: $value")
} }

View File

@ -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())
}
}
}

View File

@ -3,7 +3,6 @@ package ru.dbotthepony.kstarbound.player
import com.google.gson.JsonArray import com.google.gson.JsonArray
import com.google.gson.JsonElement import com.google.gson.JsonElement
import com.google.gson.JsonObject import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Starbound 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.lua.NewLuaState
import ru.dbotthepony.kstarbound.item.ItemStack import ru.dbotthepony.kstarbound.item.ItemStack
import ru.dbotthepony.kommons.gson.set import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kstarbound.defs.quest.QuestDescriptor
import java.util.HashMap import java.util.HashMap
import java.util.UUID import java.util.UUID
@ -46,7 +46,7 @@ class QuestInstance(
var compassDirection: Double? = null var compassDirection: Double? = null
private val portraits = JsonObject() private val portraits = JsonObject()
private val params = descriptor.parameters.deepCopy() //private val params = descriptor.parameters.deepCopy()
private val portraitTitles = HashMap<String, String>() private val portraitTitles = HashMap<String, String>()
@ -78,9 +78,9 @@ class QuestInstance(
} }
init { init {
for ((k, v) in descriptor.parameters.entrySet()) { //for ((k, v) in descriptor.parameters.entrySet()) {
params[k] = v.deepCopy() // params[k] = v.deepCopy()
} //}
} }
companion object { companion object {

View File

@ -159,7 +159,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
if (world == null) { if (world == null) {
send(PlayerWarpResultPacket(false, request, false)) send(PlayerWarpResultPacket(false, request, false))
} else { } else {
currentWarpStatus = world.acceptClient(this).exceptionally { currentWarpStatus = world.acceptClient(this, request).exceptionally {
send(PlayerWarpResultPacket(false, request, false)) send(PlayerWarpResultPacket(false, request, false))
if (world == shipWorld) { if (world == shipWorld) {
@ -244,7 +244,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
enqueueWarp(WarpAlias.OwnShip) enqueueWarp(WarpAlias.OwnShip)
warpingAllowed = true warpingAllowed = true
if (server.channels.connections.size == 2) { if (server.channels.connections.size > 1) {
enqueueWarp(WarpAction.Player(server.channels.connections.first().uuid!!)) enqueueWarp(WarpAction.Player(server.channels.connections.first().uuid!!))
} }
} }

View File

@ -18,7 +18,7 @@ import ru.dbotthepony.kstarbound.world.api.AbstractCell
import ru.dbotthepony.kstarbound.world.api.ImmutableCell import ru.dbotthepony.kstarbound.world.api.ImmutableCell
import ru.dbotthepony.kstarbound.world.api.MutableCell import ru.dbotthepony.kstarbound.world.api.MutableCell
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity 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.BufferedInputStream
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.Closeable import java.io.Closeable

View File

@ -5,17 +5,22 @@ import it.unimi.dsi.fastutil.ints.IntArraySet
import it.unimi.dsi.fastutil.longs.Long2ObjectFunction import it.unimi.dsi.fastutil.longs.Long2ObjectFunction
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet
import it.unimi.dsi.fastutil.objects.ObjectArraySet
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.util.IStruct2i import ru.dbotthepony.kommons.util.IStruct2i
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.WarpAction import ru.dbotthepony.kstarbound.defs.WarpAction
import ru.dbotthepony.kstarbound.defs.WorldID import ru.dbotthepony.kstarbound.defs.WorldID
import ru.dbotthepony.kstarbound.defs.tile.TileDamage import ru.dbotthepony.kstarbound.defs.tile.TileDamage
import ru.dbotthepony.kstarbound.defs.tile.TileDamageResult 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.WorldStructure
import ru.dbotthepony.kstarbound.defs.world.WorldTemplate import ru.dbotthepony.kstarbound.defs.world.WorldTemplate
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.jsonArrayOf
import ru.dbotthepony.kstarbound.network.IPacket import ru.dbotthepony.kstarbound.network.IPacket
import ru.dbotthepony.kstarbound.network.packets.StepUpdatePacket import ru.dbotthepony.kstarbound.network.packets.StepUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.PlayerWarpResultPacket 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.WorldGeometry
import ru.dbotthepony.kstarbound.world.api.ImmutableCell import ru.dbotthepony.kstarbound.world.api.ImmutableCell
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity 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.CompletableFuture
import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.RejectedExecutionException import java.util.concurrent.RejectedExecutionException
@ -167,12 +174,63 @@ class ServerWorld private constructor(
if (damage.amount <= 0.0) if (damage.amount <= 0.0)
return TileDamageResult.NONE 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 var topMost = TileDamageResult.NONE
for (pos in actualPositions) { val damagedEntities = ObjectArraySet<TileEntity>()
val chunk = chunkMap[geometry.chunkFromCell(pos)] ?: continue
topMost = topMost.coerceAtLeast(chunk.damageTile(pos - chunk.pos.tile, isBackground, sourcePosition, damage, source)) 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 return topMost

View File

@ -33,6 +33,7 @@ import ru.dbotthepony.kstarbound.world.TileHealth
import ru.dbotthepony.kstarbound.world.api.ImmutableCell import ru.dbotthepony.kstarbound.world.api.ImmutableCell
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.player.PlayerEntity import ru.dbotthepony.kstarbound.world.entities.player.PlayerEntity
import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject
import java.io.DataOutputStream import java.io.DataOutputStream
import java.util.HashMap import java.util.HashMap
import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.ConcurrentLinkedQueue
@ -221,26 +222,24 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p
val id = entity.entityID val id = entity.entityID
unseen.rem(id) unseen.rem(id)
if (entity is PlayerEntity) { if (entityVersions.get(id) == -1L) {
if (entityVersions.get(id) == -1L) { // never networked
// never networked val initial = FastByteArrayOutputStream()
val initial = FastByteArrayOutputStream() entity.writeNetwork(DataOutputStream(initial), client.isLegacy)
entity.writeNetwork(DataOutputStream(initial), client.isLegacy) val (data, version) = entity.networkGroup.write(isLegacy = client.isLegacy)
val (data, version) = entity.networkGroup.write(isLegacy = client.isLegacy)
entityVersions.put(id, version) entityVersions.put(id, version)
send(EntityCreatePacket( send(EntityCreatePacket(
entity.type, entity.type,
ByteArrayList.wrap(initial.array, initial.length), ByteArrayList.wrap(initial.array, initial.length),
data, data,
entity.entityID entity.entityID
)) ))
} else if (entity.networkGroup.upstream.hasChangedSince(entityVersions.get(id))) { } else if (entity.networkGroup.upstream.hasChangedSince(entityVersions.get(id))) {
val (data, version) = entity.networkGroup.write(remoteVersion = entityVersions.get(id), isLegacy = client.isLegacy) val (data, version) = entity.networkGroup.write(remoteVersion = entityVersions.get(id), isLegacy = client.isLegacy)
entityVersions.put(id, version) entityVersions.put(id, version)
send(EntityUpdateSetPacket(entity.connectionID, Int2ObjectMaps.singleton(entity.entityID, data))) send(EntityUpdateSetPacket(entity.connectionID, Int2ObjectMaps.singleton(entity.entityID, data)))
}
} }
} }

View File

@ -21,6 +21,7 @@ import ru.dbotthepony.kstarbound.defs.world.CelestialParameters
import ru.dbotthepony.kstarbound.defs.world.CelestialPlanet import ru.dbotthepony.kstarbound.defs.world.CelestialPlanet
import ru.dbotthepony.kstarbound.defs.JsonDriven import ru.dbotthepony.kstarbound.defs.JsonDriven
import ru.dbotthepony.kstarbound.io.BTreeDB5 import ru.dbotthepony.kstarbound.io.BTreeDB5
import ru.dbotthepony.kstarbound.json.mergeJson
import ru.dbotthepony.kstarbound.json.readJsonElement import ru.dbotthepony.kstarbound.json.readJsonElement
import ru.dbotthepony.kstarbound.json.writeJsonElement import ru.dbotthepony.kstarbound.json.writeJsonElement
import ru.dbotthepony.kstarbound.math.Line2d import ru.dbotthepony.kstarbound.math.Line2d
@ -192,7 +193,7 @@ class NativeUniverseSource(private val db: BTreeDB6?, private val universe: Serv
systemPos, systemPos,
systemSeed, systemSeed,
systemName, systemName,
JsonDriven.mergeNoCopy(system.baseParameters.deepCopy(), system.variationParameters.random(random)) mergeJson(system.baseParameters.deepCopy(), system.variationParameters.random(random))
) )
if ("typeName" !in systemParams.parameters) { if ("typeName" !in systemParams.parameters) {
@ -228,7 +229,7 @@ class NativeUniverseSource(private val db: BTreeDB6?, private val universe: Serv
planetCoordinate, planetCoordinate,
planetSeed, planetSeed,
planetName, planetName,
JsonDriven.mergeNoCopy(planetaryType.baseParameters.deepCopy(), planetaryType.variationParameters.random(random)) mergeJson(planetaryType.baseParameters.deepCopy(), planetaryType.variationParameters.random(random))
) )
val satellites = Int2ObjectArrayMap<CelestialParameters>() val satellites = Int2ObjectArrayMap<CelestialParameters>()
@ -245,11 +246,11 @@ class NativeUniverseSource(private val db: BTreeDB6?, private val universe: Serv
val satelliteCoordinate = UniversePos(location, planetOrbitIndex, satelliteOrbitIndex) val satelliteCoordinate = UniversePos(location, planetOrbitIndex, satelliteOrbitIndex)
val merge = JsonObject() val merge = JsonObject()
JsonDriven.mergeNoCopy(merge, satelliteType.baseParameters) mergeJson(merge, satelliteType.baseParameters)
JsonDriven.mergeNoCopy(merge, satelliteType.variationParameters.random(random)) mergeJson(merge, satelliteType.variationParameters.random(random))
if (systemOrbitRegion.regionName in satelliteType.orbitParameters) { 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( satellites[satelliteOrbitIndex] = CelestialParameters(

View File

@ -1,8 +1,6 @@
package ru.dbotthepony.kstarbound.util package ru.dbotthepony.kstarbound.util
import com.google.gson.JsonArray
import com.google.gson.JsonElement import com.google.gson.JsonElement
import com.google.gson.JsonObject
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import java.util.* import java.util.*
import java.util.stream.Stream import java.util.stream.Stream
@ -18,38 +16,8 @@ fun String.sbIntern2(): String {
val JsonElement.asStringOrNull: String? val JsonElement.asStringOrNull: String?
get() = if (isJsonNull) null else asString get() = if (isJsonNull) null else asString
fun traverseJsonPath(path: String?, element: JsonElement?): JsonElement? { val JsonElement.coalesceNull: JsonElement?
element ?: return null get() = if (isJsonNull) null else this
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
}
}
}
fun UUID.toStarboundString(): String { fun UUID.toStarboundString(): String {
val builder = StringBuilder(32) val builder = StringBuilder(32)

View File

@ -1,12 +1,11 @@
package ru.dbotthepony.kstarbound.world 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.ObjectArraySet
import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet
import ru.dbotthepony.kommons.arrays.Object2DArray import ru.dbotthepony.kommons.arrays.Object2DArray
import ru.dbotthepony.kommons.util.AABB import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i 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.TileDamage
import ru.dbotthepony.kstarbound.defs.tile.TileDamageResult import ru.dbotthepony.kstarbound.defs.tile.TileDamageResult
import ru.dbotthepony.kstarbound.defs.tile.TileDamageType 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.ICellAccess
import ru.dbotthepony.kstarbound.world.api.ImmutableCell import ru.dbotthepony.kstarbound.world.api.ImmutableCell
import ru.dbotthepony.kstarbound.world.api.OffsetCellAccess 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.api.TileView
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity 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 java.util.concurrent.CopyOnWriteArraySet
import kotlin.concurrent.withLock
/** /**
* Чанк мира * Чанк мира
@ -70,43 +67,77 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
} }
protected val tileHealthForeground = lazy { protected val tileHealthForeground = lazy {
Object2DArray(CHUNK_SIZE, CHUNK_SIZE) { _, _ -> TileHealth() } Object2DArray(CHUNK_SIZE, CHUNK_SIZE) { _, _ -> TileHealth.Tile() }
} }
protected val tileHealthBackground = lazy { 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()) { 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) { if (cell.isIndestructible || cell.tile(isBackground).material.isBuiltin) {
return TileDamageResult.NONE return DamageResult(TileDamageResult.NONE)
} }
var damage = damage var damage = damage
var result = TileDamageResult.NORMAL var result = TileDamageResult.NORMAL
if (tile.dungeonId in world.protectedDungeonIDs) { if (cell.dungeonId in world.protectedDungeonIDs) {
damage = damage.copy(type = TileDamageType.PROTECTED) damage = damage.copy(type = TileDamageType.PROTECTED)
result = TileDamageResult.PROTECTED result = TileDamageResult.PROTECTED
} }
val health = (if (isBackground) tileHealthBackground else tileHealthForeground).value[pos.x, pos.y] val health = (if (isBackground) tileHealthBackground else tileHealthForeground).value[pos.x, pos.y]
health.damage(tile.tile(isBackground).material.value.actualDamageTable, sourcePosition, damage) val tile = cell.tile(isBackground)
subscribers.forEach { it.onTileHealthUpdate(pos.x, pos.y, isBackground, health) }
if (isBackground) { val params = if (!damage.type.isPenetrating && tile.modifier != null && tile.modifier!!.value.breaksWithTile) {
damagedTilesBackground.add(pos) tile.material.value.actualDamageTable + tile.modifier!!.value.actualDamageTable
} else { } 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<Vector2i>() protected val damagedTilesForeground = ObjectArraySet<Vector2i>()

View File

@ -4,44 +4,36 @@ import ru.dbotthepony.kommons.io.readVector2d
import ru.dbotthepony.kommons.io.readVector2f import ru.dbotthepony.kommons.io.readVector2f
import ru.dbotthepony.kommons.io.writeStruct2d import ru.dbotthepony.kommons.io.writeStruct2d
import ru.dbotthepony.kommons.io.writeStruct2f 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.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.tile.TileDamage import ru.dbotthepony.kstarbound.defs.tile.TileDamage
import ru.dbotthepony.kstarbound.defs.tile.TileDamageConfig import ru.dbotthepony.kstarbound.defs.tile.TileDamageConfig
import ru.dbotthepony.kstarbound.defs.tile.TileDamageType 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.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream
class TileHealth() { sealed class TileHealth() {
constructor(stream: DataInputStream, isLegacy: Boolean) : this() {
read(stream, isLegacy)
}
var isHarvested: Boolean = false
private set
var damageSource: Vector2d = Vector2d.ZERO var damageSource: Vector2d = Vector2d.ZERO
private set protected set
var damageType: TileDamageType = TileDamageType.PROTECTED abstract var damagePercent: Double
private set protected set
var damagePercent: Double = 0.0 abstract var damageEffectTimeFactor: Double
private set protected set
var damageEffectTimeFactor: Double = 0.0 abstract var isHarvested: Boolean
private set protected set
abstract var damageType: TileDamageType
protected set
var damageEffectPercentage: Double = 0.0 var damageEffectPercentage: Double = 0.0
private set protected set
fun copy(): TileHealth { abstract 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
}
val isHealthy: Boolean val isHealthy: Boolean
get() = damagePercent <= 0.0 get() = damagePercent <= 0.0
@ -119,4 +111,54 @@ class TileHealth() {
damageEffectPercentage = damageEffectTimeFactor.coerceIn(0.0, 1.0) * damagePercent damageEffectPercentage = damageEffectTimeFactor.coerceIn(0.0, 1.0) * damagePercent
return damagePercent > 0.0 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
}
}
} }

View File

@ -5,15 +5,16 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
import it.unimi.dsi.fastutil.ints.IntArraySet import it.unimi.dsi.fastutil.ints.IntArraySet
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.ObjectArraySet import it.unimi.dsi.fastutil.objects.ObjectArraySet
import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.arrays.Object2DArray import ru.dbotthepony.kommons.arrays.Object2DArray
import ru.dbotthepony.kommons.collect.filterNotNull import ru.dbotthepony.kommons.collect.filterNotNull
import ru.dbotthepony.kommons.util.IStruct2d import ru.dbotthepony.kommons.util.IStruct2d
import ru.dbotthepony.kommons.util.IStruct2i import ru.dbotthepony.kommons.util.IStruct2i
import ru.dbotthepony.kommons.util.AABB import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.util.MailboxExecutorService import ru.dbotthepony.kommons.util.MailboxExecutorService
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.world.WorldStructure import ru.dbotthepony.kstarbound.defs.world.WorldStructure
import ru.dbotthepony.kstarbound.defs.world.WorldTemplate 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.network.packets.clientbound.SetPlayerStartPacket
import ru.dbotthepony.kstarbound.util.ExceptionLogger import ru.dbotthepony.kstarbound.util.ExceptionLogger
import ru.dbotthepony.kstarbound.util.ParallelPerform 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.ICellAccess
import ru.dbotthepony.kstarbound.world.api.AbstractCell import ru.dbotthepony.kstarbound.world.api.AbstractCell
import ru.dbotthepony.kstarbound.world.api.TileView import ru.dbotthepony.kstarbound.world.api.TileView
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.DynamicEntity 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.CollisionPoly
import ru.dbotthepony.kstarbound.world.physics.CollisionType import ru.dbotthepony.kstarbound.world.physics.CollisionType
import ru.dbotthepony.kstarbound.world.physics.Poly import ru.dbotthepony.kstarbound.world.physics.Poly
@ -208,7 +210,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
val chunkMap: ChunkMap = if (geometry.size.x <= 32000 && geometry.size.y <= 32000) ArrayChunkMap() else SparseChunkMap() val chunkMap: ChunkMap = if (geometry.size.x <= 32000 && geometry.size.y <= 32000) ArrayChunkMap() else SparseChunkMap()
val random: RandomGenerator = RandomGenerator.of("Xoroshiro128PlusPlus") val random: RandomGenerator = random()
var gravity = Vector2d(0.0, -80.0) var gravity = Vector2d(0.0, -80.0)
abstract val isRemote: Boolean abstract val isRemote: Boolean
@ -279,6 +281,14 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
mailbox.shutdownNow() mailbox.shutdownNow()
} }
fun entitiesAtTile(pos: Vector2i, filter: Predicate<TileEntity> = Predicate { true }, distinct: Boolean = true): List<TileEntity> {
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<TileEntity>
}
fun queryTileCollisions(aabb: AABB): MutableList<CollisionPoly> { fun queryTileCollisions(aabb: AABB): MutableList<CollisionPoly> {
val result = ArrayList<CollisionPoly>() val result = ArrayList<CollisionPoly>()
val tiles = aabb.encasingIntAABB() val tiles = aabb.encasingIntAABB()

View File

@ -25,12 +25,7 @@ sealed class AbstractCell {
return LegacyNetworkCellState(background.toLegacyNet(), foreground.toLegacyNet(), foreground.material.value.collisionKind, biome, envBiome, liquid.toLegacyNet(), dungeonId) return LegacyNetworkCellState(background.toLegacyNet(), foreground.toLegacyNet(), foreground.material.value.collisionKind, biome, envBiome, liquid.toLegacyNet(), dungeonId)
} }
fun tile(background: Boolean): AbstractTileState { abstract fun tile(background: Boolean): AbstractTileState
if (background)
return this.background
else
return this.foreground
}
fun write(stream: DataOutputStream) { fun write(stream: DataOutputStream) {
foreground.write(stream) foreground.write(stream)

View File

@ -22,6 +22,13 @@ data class ImmutableCell(
return legacyNet return legacyNet
} }
override fun tile(background: Boolean): ImmutableTileState {
if (background)
return this.background
else
return this.foreground
}
override fun mutable(): MutableCell { override fun mutable(): MutableCell {
return MutableCell(foreground.mutable(), background.mutable(), liquid.mutable(), dungeonId, biome, envBiome, isIndestructible) return MutableCell(foreground.mutable(), background.mutable(), liquid.mutable(), dungeonId, biome, envBiome, isIndestructible)
} }

View File

@ -28,6 +28,13 @@ data class MutableCell(
return this return this
} }
override fun tile(background: Boolean): MutableTileState {
if (background)
return this.background
else
return this.foreground
}
override fun immutable(): ImmutableCell { override fun immutable(): ImmutableCell {
return POOL.intern(ImmutableCell(foreground.immutable(), background.immutable(), liquid.immutable(), dungeonId, biome, envBiome, isIndestructible)) return POOL.intern(ImmutableCell(foreground.immutable(), background.immutable(), liquid.immutable(), dungeonId, biome, envBiome, isIndestructible))
} }

View File

@ -1,17 +1,24 @@
package ru.dbotthepony.kstarbound.world.entities package ru.dbotthepony.kstarbound.world.entities
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import it.unimi.dsi.fastutil.bytes.ByteArrayList import it.unimi.dsi.fastutil.bytes.ByteArrayList
import org.apache.logging.log4j.LogManager 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.util.MailboxExecutorService
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.client.StarboundClient import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.client.render.LayeredRenderer 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.EntityType
import ru.dbotthepony.kstarbound.defs.JsonDriven import ru.dbotthepony.kstarbound.defs.JsonDriven
import ru.dbotthepony.kstarbound.network.packets.EntityDestroyPacket 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.MasterElement
import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup 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.server.world.ServerWorld
import ru.dbotthepony.kstarbound.world.LightCalculator import ru.dbotthepony.kstarbound.world.LightCalculator
import ru.dbotthepony.kstarbound.world.SpatialIndex import ru.dbotthepony.kstarbound.world.SpatialIndex
@ -53,6 +60,9 @@ abstract class AbstractEntity(path: String) : JsonDriven(path), Comparable<Abstr
val world: World<*, *> val world: World<*, *>
get() = innerWorld ?: throw IllegalStateException("Not in world") get() = innerWorld ?: throw IllegalStateException("Not in world")
inline val clientWorld get() = world as ClientWorld
inline val serverWorld get() = world as ServerWorld
val isSpawned: Boolean val isSpawned: Boolean
get() = innerWorld != null get() = innerWorld != null
@ -63,8 +73,7 @@ abstract class AbstractEntity(path: String) : JsonDriven(path), Comparable<Abstr
* indexed in the stored world. Unique ids must be different across all * indexed in the stored world. Unique ids must be different across all
* entities in a single world. * entities in a single world.
*/ */
var uniqueID: String? = null val uniqueID = networkedData(KOptional(), InternedStringCodec.koptional())
protected set
var description = "" var description = ""
@ -90,6 +99,10 @@ abstract class AbstractEntity(path: String) : JsonDriven(path), Comparable<Abstr
} }
open fun receiveMessage(name: String, arguments: JsonArray): JsonElement? {
return null
}
fun joinWorld(world: World<*, *>) { fun joinWorld(world: World<*, *>) {
if (innerWorld != null) if (innerWorld != null)
throw IllegalStateException("Already spawned (in world $innerWorld)") throw IllegalStateException("Already spawned (in world $innerWorld)")
@ -130,30 +143,14 @@ abstract class AbstractEntity(path: String) : JsonDriven(path), Comparable<Abstr
var isRemote: Boolean = false var isRemote: Boolean = false
fun tick() { open fun tick() {
tickShared()
if (isRemote) {
tickRemote()
} else {
tickLocal()
}
}
protected open fun tickShared() {
mailbox.executeQueuedTasks() mailbox.executeQueuedTasks()
}
protected open fun tickRemote() {
if (networkGroup.upstream.isInterpolating) { if (networkGroup.upstream.isInterpolating) {
networkGroup.upstream.tickInterpolation(Starbound.TIMESTEP) networkGroup.upstream.tickInterpolation(Starbound.TIMESTEP)
} }
} }
protected open fun tickLocal() {
}
open fun render(client: StarboundClient, layers: LayeredRenderer) { open fun render(client: StarboundClient, layers: LayeredRenderer) {
} }

View File

@ -1,59 +0,0 @@
package ru.dbotthepony.kstarbound.world.entities
import it.unimi.dsi.fastutil.objects.Object2ObjectAVLTreeMap
import ru.dbotthepony.kstarbound.defs.animation.AnimatedPartsDefinition
class AnimatedParts {
private class StateType(config: AnimatedPartsDefinition.StateType) {
var enabled = config.enabled
var activeStateDirty = true
val priority = config.priority
val stateTypeProperties = config.properties
val default: String
// sorted by key
val states = Object2ObjectAVLTreeMap<String, AnimatedPartsDefinition.StateType.State>()
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<String, StateType>()
// sorted by key
private val parts = Object2ObjectAVLTreeMap<String, Part>()
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<String> {
return parts.keys
}
fun stateTypes(): Collection<String> {
return stateTypes.keys
}
}

View File

@ -1,30 +1,27 @@
package ru.dbotthepony.kstarbound.world.entities package ru.dbotthepony.kstarbound.world.entities
import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.objects.Object2ObjectAVLTreeMap import it.unimi.dsi.fastutil.objects.Object2ObjectAVLTreeMap
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import org.apache.logging.log4j.LogManager 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.IntValueCodec
import ru.dbotthepony.kommons.io.map 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.matrix.Matrix3f
import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.util.getValue import ru.dbotthepony.kommons.util.getValue
import ru.dbotthepony.kommons.util.setValue import ru.dbotthepony.kommons.util.setValue
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.Starbound 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.AnimationDefinition
import ru.dbotthepony.kstarbound.defs.animation.ParticleConfig
import ru.dbotthepony.kstarbound.defs.animation.ParticleFactory import ru.dbotthepony.kstarbound.defs.animation.ParticleFactory
import ru.dbotthepony.kstarbound.fromJson import ru.dbotthepony.kstarbound.json.mergeJson
import ru.dbotthepony.kstarbound.io.readInternedString
import ru.dbotthepony.kstarbound.math.Interpolator import ru.dbotthepony.kstarbound.math.Interpolator
import ru.dbotthepony.kstarbound.math.PeriodicFunction import ru.dbotthepony.kstarbound.math.PeriodicFunction
import ru.dbotthepony.kstarbound.network.syncher.AABBCodecLegacy import ru.dbotthepony.kstarbound.math.approachAngle
import ru.dbotthepony.kstarbound.network.syncher.AABBCodecNative
import ru.dbotthepony.kstarbound.network.syncher.InternedStringCodec import ru.dbotthepony.kstarbound.network.syncher.InternedStringCodec
import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup
import ru.dbotthepony.kstarbound.network.syncher.NetworkedElement 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.networkedAABBNullable
import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean
import ru.dbotthepony.kstarbound.network.syncher.networkedColor 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.networkedEventCounter
import ru.dbotthepony.kstarbound.network.syncher.networkedFixedPoint import ru.dbotthepony.kstarbound.network.syncher.networkedFixedPoint
import ru.dbotthepony.kstarbound.network.syncher.networkedFloat 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.networkedSignedInt
import ru.dbotthepony.kstarbound.network.syncher.networkedString import ru.dbotthepony.kstarbound.network.syncher.networkedString
import ru.dbotthepony.kstarbound.network.syncher.networkedUnsignedInt import ru.dbotthepony.kstarbound.network.syncher.networkedUnsignedInt
import java.io.DataInputStream import ru.dbotthepony.kstarbound.util.random.random
import java.io.DataOutputStream import ru.dbotthepony.kstarbound.world.positiveModulo
import java.util.Collections import java.util.Collections
import java.util.function.Consumer
import kotlin.math.atan2 import kotlin.math.atan2
import kotlin.math.cos import kotlin.math.cos
import kotlin.math.roundToInt
import kotlin.math.sin import kotlin.math.sin
import kotlin.math.sqrt 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 Animator() {
class Light { private class Light {
private val elements = ArrayList<NetworkedElement>() private val elements = ArrayList<NetworkedElement>()
fun addTo(group: NetworkedGroup) { fun addTo(group: NetworkedGroup) {
@ -76,7 +108,7 @@ class Animator() {
var beamAmbience: Float = 0f var beamAmbience: Float = 0f
} }
enum class SoundSignal { private enum class SoundSignal {
PLAY, STOP_ALL; PLAY, STOP_ALL;
companion object { companion object {
@ -84,7 +116,7 @@ class Animator() {
} }
} }
class Sound { private class Sound {
private val elements = ArrayList<NetworkedElement>() private val elements = ArrayList<NetworkedElement>()
fun addTo(group: NetworkedGroup) { fun addTo(group: NetworkedGroup) {
@ -104,25 +136,181 @@ class Animator() {
val signals = NetworkedSignal(SoundSignal.CODEC).also { elements.add(it) } 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() val enabled = networkedBoolean()
var timer: Double = 0.0 var timer: Double = 0.0
fun tick(delta: Double) {
if (timer <= 0.0)
timer = time
else
timer -= delta
}
} }
class StateInfo { private class StateType(config: AnimatedPartsDefinition.StateType) {
val stateIndex = networkedPointer() // NetworkedAnimator
private var noPropagate = false
val stateIndex = networkedPointer(-1L)
val startedEvent = networkedEventCounter() 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 angularVelocity = 0.0
var rotationCenter = Vector2d.ZERO var rotationCenter = Vector2d.ZERO
val targetAngle = networkedFloat() val targetAngle = networkedFloat()
var currentAngle = 0.0 var currentAngle = 0.0
val immediateEvent = networkedEventCounter() 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<NetworkedElement>() private val elements = ArrayList<NetworkedElement>()
fun addTo(group: NetworkedGroup) { 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) data class Config(val count: Int, val offset: Vector2d, val flip: Boolean, val factory: ParticleFactory)
private val elements = ArrayList<NetworkedElement>() private val elements = ArrayList<NetworkedElement>()
@ -189,9 +377,6 @@ class Animator() {
private val elements = ArrayList<NetworkedElement>() private val elements = ArrayList<NetworkedElement>()
var animatedParts = AnimatedParts()
private set
var processingDirectives by networkedString().also { elements.add(it) } var processingDirectives by networkedString().also { elements.add(it) }
var zoom by networkedFloat().also { elements.add(it) } var zoom by networkedFloat().also { elements.add(it) }
var isFlipped by networkedBoolean().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 } var animationRate by networkedFloat(1.0).also { elements.add(it); it.interpolator = Interpolator.Linear }
private val globalTags = NetworkedMap(InternedStringCodec, InternedStringCodec) private val globalTags = NetworkedMap(InternedStringCodec, InternedStringCodec)
private val parts = Object2ObjectAVLTreeMap<String, Part>()
private val partTags = HashMap<String, NetworkedMap<String, String>>() private val partTags = HashMap<String, NetworkedMap<String, String>>()
private val stateInfo = Object2ObjectAVLTreeMap<String, StateInfo>() // iterate by priority, network by sorted key
private val stateTypes = LinkedHashMap<String, StateType>()
private val rotationGroups = Object2ObjectAVLTreeMap<String, RotationGroup>() private val rotationGroups = Object2ObjectAVLTreeMap<String, RotationGroup>()
private val transformationGroups = Object2ObjectAVLTreeMap<String, TransformationGroup>() private val transformationGroups = Object2ObjectAVLTreeMap<String, TransformationGroup>()
private val particleEmitters = Object2ObjectAVLTreeMap<String, ParticleEmitter>() private val particleEmitters = Object2ObjectAVLTreeMap<String, ParticleEmitter>()
@ -209,14 +396,13 @@ class Animator() {
private val sounds = Object2ObjectAVLTreeMap<String, Sound>() private val sounds = Object2ObjectAVLTreeMap<String, Sound>()
private val effects = Object2ObjectAVLTreeMap<String, Effect>() private val effects = Object2ObjectAVLTreeMap<String, Effect>()
private val random = random()
init { init {
setupNetworkElements() setupNetworkElements()
} }
constructor(config: AnimationDefinition) : this() { constructor(config: AnimationDefinition) : this() {
if (config.animatedParts != null)
animatedParts = AnimatedParts(config.animatedParts)
for ((k, v) in config.globalTagDefaults) { for ((k, v) in config.globalTagDefaults) {
globalTags[k] = v globalTags[k] = v
} }
@ -308,11 +494,17 @@ class Animator() {
effects[k] = Effect(v.type, v.time, v.directives) effects[k] = Effect(v.type, v.time, v.directives)
} }
for (k in animatedParts.stateTypes()) { if (config.animatedParts != null) {
stateInfo[k] = StateInfo() 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) } partTags.computeIfAbsent(k) { NetworkedMap(InternedStringCodec, InternedStringCodec) }
} }
@ -345,13 +537,16 @@ class Animator() {
networkGroup.add(globalTags) networkGroup.add(globalTags)
// animated part set // animated part set
for (v in animatedParts.parts()) { for (v in parts.keys) {
networkGroup.add(partTags[v] ?: throw RuntimeException("Missing animated part $v!")) networkGroup.add(partTags[v] ?: throw RuntimeException("Missing animated part $v!"))
} }
for (v in stateInfo.values) { stateTypes.entries.stream()
networkGroup.add(v.stateIndex) .sorted { o1, o2 -> o1.key.compareTo(o2.key) }
networkGroup.add(v.startedEvent) .map { it.value }
.forEach {
networkGroup.add(it.stateIndex)
networkGroup.add(it.startedEvent)
} }
for (v in transformationGroups.values) { 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 { companion object {
// lame // lame
fun load(path: String): Animator { fun load(path: String): Animator {

View File

@ -27,10 +27,10 @@ abstract class DynamicEntity(path: String) : AbstractEntity(path) {
movement.updateFixtures() movement.updateFixtures()
} }
override fun tickRemote() { override fun tick() {
super.tickRemote() super.tick()
if (networkGroup.upstream.isInterpolating) { if (isRemote && networkGroup.upstream.isInterpolating) {
movement.updateFixtures() movement.updateFixtures()
} }
} }

View File

@ -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) {
}
}

View File

@ -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<ObjectDefinition>,
) : 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, () -> 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<String, SpriteReference> 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<Drawable> 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<JsonObject> {
val get = orientation
return if (get == null) listOf(prototype.jsonObject) else listOf(get.json, prototype.jsonObject)
}
val lightColors: ImmutableMap<String, RGBAColor> 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<ImmutableMap<String, RGBAColor>> }
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
}
}
}

View File

@ -1,9 +1,11 @@
package ru.dbotthepony.kstarbound.world.entities.player package ru.dbotthepony.kstarbound.world.entities.player
import com.google.gson.JsonElement
import com.google.gson.JsonObject import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.bytes.ByteArrayList import it.unimi.dsi.fastutil.bytes.ByteArrayList
import ru.dbotthepony.kommons.io.writeBinaryString import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.util.AABB import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.util.getValue import ru.dbotthepony.kommons.util.getValue
import ru.dbotthepony.kommons.util.setValue import ru.dbotthepony.kommons.util.setValue
import ru.dbotthepony.kommons.vector.Vector2d 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.HumanoidEmote
import ru.dbotthepony.kstarbound.defs.actor.player.PlayerGamemode import ru.dbotthepony.kstarbound.defs.actor.player.PlayerGamemode
import ru.dbotthepony.kstarbound.io.readInternedString import ru.dbotthepony.kstarbound.io.readInternedString
import ru.dbotthepony.kstarbound.json.JsonPath
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
import ru.dbotthepony.kstarbound.math.Interpolator import ru.dbotthepony.kstarbound.math.Interpolator
import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup
@ -52,7 +55,7 @@ class PlayerEntity() : HumanoidActorEntity("/") {
} }
constructor(data: DataInputStream, isLegacy: Boolean) : this() { constructor(data: DataInputStream, isLegacy: Boolean) : this() {
uniqueID = data.readInternedString() uniqueID.accept(KOptional(data.readInternedString()))
description = data.readInternedString() description = data.readInternedString()
gamemode = PlayerGamemode.entries[if (isLegacy) data.readInt() else data.readUnsignedByte()] gamemode = PlayerGamemode.entries[if (isLegacy) data.readInt() else data.readUnsignedByte()]
humanoidData = HumanoidData.read(data, isLegacy) humanoidData = HumanoidData.read(data, isLegacy)
@ -64,7 +67,12 @@ class PlayerEntity() : HumanoidActorEntity("/") {
var gamemode = PlayerGamemode.CASUAL var gamemode = PlayerGamemode.CASUAL
override fun writeNetwork(stream: DataOutputStream, isLegacy: Boolean) { override fun writeNetwork(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeBinaryString(uniqueID!!) uniqueID.get().ifPresent {
stream.writeBinaryString(it)
}.ifNotPresent {
stream.writeBinaryString("")
}
stream.writeBinaryString(description) stream.writeBinaryString(description)
if (isLegacy) stream.writeInt(gamemode.ordinal) else stream.writeByte(gamemode.ordinal) if (isLegacy) stream.writeInt(gamemode.ordinal) else stream.writeByte(gamemode.ordinal)
humanoidData.write(stream, isLegacy) humanoidData.write(stream, isLegacy)
@ -113,8 +121,8 @@ class PlayerEntity() : HumanoidActorEntity("/") {
metaFixture = null metaFixture = null
} }
override fun tickShared() { override fun tick() {
super.tickShared() super.tick()
if (fixturesChangeset != movement.fixturesChangeset) { if (fixturesChangeset != movement.fixturesChangeset) {
fixturesChangeset = movement.fixturesChangeset fixturesChangeset = movement.fixturesChangeset
@ -128,10 +136,14 @@ class PlayerEntity() : HumanoidActorEntity("/") {
override val isApplicableForUnloading: Boolean override val isApplicableForUnloading: Boolean
get() = false get() = false
override fun defs(): Collection<JsonObject> {
return emptyList()
}
var uuid: UUID by Delegates.notNull() var uuid: UUID by Delegates.notNull()
private set 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")
}
} }

View File

@ -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<ObjectDefinition>) : 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) }
}

View File

@ -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<ObjectDefinition>) : WorldObject(config) {
init {
isInteractive = true
}
}

View File

@ -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<Vector2i>
abstract fun damage(damageSpaces: List<Vector2i>, source: Vector2d, damage: TileDamage): Boolean
override fun onJoinWorld(world: World<*, *>) {
updateSpatialIndex()
}
override fun onRemove(world: World<*, *>, isDeath: Boolean) {
}
}

View File

@ -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<ObjectDefinition>) : 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<String, String>()
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<WireNode> = lookupProperty(JsonPath("inputNodes")) { JsonArray() }
.asJsonArray
.stream()
.map { WireNode(vectors.fromJsonTree(it)) }
.collect(ImmutableList.toImmutableList())
val outputNodes: ImmutableList<WireNode> = 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<String, SpriteReference> 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<Drawable> 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<Vector2i>, source: Vector2d, damage: TileDamage): Boolean {
if (unbreakable)
return false
tileHealth.damage(config.value.damageConfig, source, damage)
return tileHealth.isDead
}
val lightColors: ImmutableMap<String, RGBAColor> 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<ImmutableMap<String, RGBAColor>> }
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
}
}
}

View File

@ -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) })
}
}

View File

@ -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<Int>()
val list2 = ArrayList<Int>()
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)
}
}