TileEntities, WorldObject
This commit is contained in:
parent
cb63b47b12
commit
209c1a5776
@ -8,12 +8,8 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectMap
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMaps
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectFunction
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectMap
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectMaps
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import ru.dbotthepony.kommons.util.Either
|
||||
import ru.dbotthepony.kstarbound.util.traverseJsonPath
|
||||
import java.util.Collections
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
@ -63,10 +59,6 @@ class Registry<T : Any>(val name: String) {
|
||||
val value: T?
|
||||
get() = entry?.value
|
||||
|
||||
fun traverseJsonPath(path: String): JsonElement? {
|
||||
return traverseJsonPath(path, entry?.json ?: return null)
|
||||
}
|
||||
|
||||
final override fun get(): Entry<T>? {
|
||||
return entry
|
||||
}
|
||||
@ -82,10 +74,6 @@ class Registry<T : Any>(val name: String) {
|
||||
abstract val isBuiltin: Boolean
|
||||
abstract val ref: Ref<T>
|
||||
|
||||
fun traverseJsonPath(path: String): JsonElement? {
|
||||
return traverseJsonPath(path, json)
|
||||
}
|
||||
|
||||
final override fun get(): T {
|
||||
return value
|
||||
}
|
||||
|
@ -35,6 +35,7 @@ import ru.dbotthepony.kstarbound.defs.`object`.ObjectOrientation
|
||||
import ru.dbotthepony.kstarbound.defs.actor.player.BlueprintLearnList
|
||||
import ru.dbotthepony.kstarbound.defs.animation.Particle
|
||||
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
|
||||
import ru.dbotthepony.kstarbound.defs.quest.QuestParameter
|
||||
import ru.dbotthepony.kstarbound.defs.world.CelestialParameters
|
||||
import ru.dbotthepony.kstarbound.defs.world.VisitableWorldParametersType
|
||||
import ru.dbotthepony.kstarbound.defs.world.BiomePlaceables
|
||||
@ -50,7 +51,7 @@ import ru.dbotthepony.kstarbound.json.LongRangeAdapter
|
||||
import ru.dbotthepony.kstarbound.json.builder.EnumAdapter
|
||||
import ru.dbotthepony.kstarbound.json.builder.BuilderAdapter
|
||||
import ru.dbotthepony.kstarbound.json.builder.FactoryAdapter
|
||||
import ru.dbotthepony.kstarbound.json.builder.JsonImplementationTypeFactory
|
||||
import ru.dbotthepony.kstarbound.json.JsonImplementationTypeFactory
|
||||
import ru.dbotthepony.kstarbound.json.factory.CollectionAdapterFactory
|
||||
import ru.dbotthepony.kstarbound.json.factory.ImmutableCollectionAdapterFactory
|
||||
import ru.dbotthepony.kstarbound.json.factory.PairAdapterFactory
|
||||
@ -59,13 +60,14 @@ import ru.dbotthepony.kstarbound.json.factory.SingletonTypeAdapterFactory
|
||||
import ru.dbotthepony.kstarbound.math.*
|
||||
import ru.dbotthepony.kstarbound.server.world.UniverseChunk
|
||||
import ru.dbotthepony.kstarbound.item.ItemStack
|
||||
import ru.dbotthepony.kstarbound.json.JsonAdapterTypeFactory
|
||||
import ru.dbotthepony.kstarbound.json.JsonPath
|
||||
import ru.dbotthepony.kstarbound.json.NativeLegacy
|
||||
import ru.dbotthepony.kstarbound.util.Directives
|
||||
import ru.dbotthepony.kstarbound.util.ExceptionLogger
|
||||
import ru.dbotthepony.kstarbound.util.SBPattern
|
||||
import ru.dbotthepony.kstarbound.util.HashTableInterner
|
||||
import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise
|
||||
import ru.dbotthepony.kstarbound.util.traverseJsonPath
|
||||
import ru.dbotthepony.kstarbound.world.UniversePos
|
||||
import ru.dbotthepony.kstarbound.world.physics.Poly
|
||||
import java.io.*
|
||||
@ -157,9 +159,14 @@ object Starbound : ISBFileLocator {
|
||||
@JvmField
|
||||
val STRINGS: Interner<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 }
|
||||
private set
|
||||
var IS_WRITING_STORE_JSON: Boolean by ThreadLocal.withInitial { false }
|
||||
private set
|
||||
|
||||
fun writeLegacyJson(data: Any): JsonElement {
|
||||
try {
|
||||
@ -197,6 +204,7 @@ object Starbound : ISBFileLocator {
|
||||
|
||||
// Обработчик @JsonImplementation
|
||||
registerTypeAdapterFactory(JsonImplementationTypeFactory)
|
||||
registerTypeAdapterFactory(JsonAdapterTypeFactory)
|
||||
|
||||
// списки, наборы, т.п.
|
||||
registerTypeAdapterFactory(CollectionAdapterFactory)
|
||||
@ -294,6 +302,7 @@ object Starbound : ISBFileLocator {
|
||||
|
||||
registerTypeAdapter(CelestialParameters::Adapter)
|
||||
registerTypeAdapter(Particle::Adapter)
|
||||
registerTypeAdapter(QuestParameter::Adapter)
|
||||
|
||||
registerTypeAdapterFactory(BiomePlacementDistributionType.DEFINITION_ADAPTER)
|
||||
registerTypeAdapterFactory(BiomePlacementItemType.DATA_ADAPTER)
|
||||
@ -385,11 +394,11 @@ object Starbound : ISBFileLocator {
|
||||
|
||||
val file = locate(filename)
|
||||
|
||||
if (!file.isFile) {
|
||||
if (!file.isFile)
|
||||
return null
|
||||
}
|
||||
|
||||
return traverseJsonPath(jsonPath, gson.fromJson(file.reader(), JsonElement::class.java))
|
||||
val pathTraverser = if (jsonPath == null) JsonPath.EMPTY else JsonPath.query(jsonPath)
|
||||
return pathTraverser.get(gson.fromJson(file.reader(), JsonElement::class.java))
|
||||
}
|
||||
|
||||
private val archivePaths = ArrayList<File>()
|
||||
|
@ -7,7 +7,7 @@ import ru.dbotthepony.kstarbound.client.ClientConnection
|
||||
import ru.dbotthepony.kstarbound.json.readJsonObject
|
||||
import ru.dbotthepony.kstarbound.json.writeJsonObject
|
||||
import ru.dbotthepony.kstarbound.network.IClientPacket
|
||||
import ru.dbotthepony.kstarbound.world.entities.WorldObject
|
||||
import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject
|
||||
import java.io.DataInputStream
|
||||
import java.io.DataOutputStream
|
||||
import java.util.UUID
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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()}]"
|
||||
}
|
||||
}
|
@ -2,14 +2,42 @@ package ru.dbotthepony.kstarbound.defs
|
||||
|
||||
import com.google.common.collect.ImmutableList
|
||||
import com.google.common.collect.ImmutableSet
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.JsonSyntaxException
|
||||
import com.google.gson.TypeAdapter
|
||||
import com.google.gson.annotations.JsonAdapter
|
||||
import com.google.gson.stream.JsonReader
|
||||
import com.google.gson.stream.JsonWriter
|
||||
import ru.dbotthepony.kommons.io.readCollection
|
||||
import ru.dbotthepony.kommons.io.writeBinaryString
|
||||
import ru.dbotthepony.kommons.io.writeCollection
|
||||
import ru.dbotthepony.kommons.io.writeDouble
|
||||
import ru.dbotthepony.kommons.io.writeStruct2d
|
||||
import ru.dbotthepony.kommons.io.writeStruct2f
|
||||
import ru.dbotthepony.kommons.util.Either
|
||||
import ru.dbotthepony.kommons.vector.Vector2d
|
||||
import ru.dbotthepony.kstarbound.Starbound
|
||||
import ru.dbotthepony.kstarbound.io.readDouble
|
||||
import ru.dbotthepony.kstarbound.io.readInternedString
|
||||
import ru.dbotthepony.kstarbound.io.readMVariant2
|
||||
import ru.dbotthepony.kstarbound.io.readNullableDouble
|
||||
import ru.dbotthepony.kstarbound.io.readNullableString
|
||||
import ru.dbotthepony.kstarbound.io.readVector2d
|
||||
import ru.dbotthepony.kstarbound.io.writeDouble
|
||||
import ru.dbotthepony.kstarbound.io.writeMVariant2
|
||||
import ru.dbotthepony.kstarbound.io.writeNullable
|
||||
import ru.dbotthepony.kstarbound.io.writeStruct2d
|
||||
import ru.dbotthepony.kstarbound.json.builder.FactoryAdapter
|
||||
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
|
||||
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
|
||||
import ru.dbotthepony.kstarbound.json.getAdapter
|
||||
import ru.dbotthepony.kstarbound.network.syncher.legacyCodec
|
||||
import ru.dbotthepony.kstarbound.network.syncher.nativeCodec
|
||||
import ru.dbotthepony.kstarbound.world.physics.Poly
|
||||
import java.io.DataInputStream
|
||||
import java.io.DataOutputStream
|
||||
|
||||
// uint8_t
|
||||
enum class TeamType(override val jsonName: String) : IStringSerializable {
|
||||
NULL("null"),
|
||||
// non-PvP-enabled players and player allied NPCs
|
||||
@ -31,6 +59,7 @@ enum class TeamType(override val jsonName: String) : IStringSerializable {
|
||||
ASSISTANT("assistant");
|
||||
}
|
||||
|
||||
// int32_t
|
||||
enum class HitType(override val jsonName: String) : IStringSerializable {
|
||||
HIT("Hit"),
|
||||
STRONG_HIT("StrongHit"),
|
||||
@ -39,6 +68,7 @@ enum class HitType(override val jsonName: String) : IStringSerializable {
|
||||
KILL("Kill");
|
||||
}
|
||||
|
||||
// uint8_t
|
||||
enum class DamageType(override val jsonName: String) : IStringSerializable {
|
||||
NO_DAMAGE("NoDamage"),
|
||||
DAMAGE("Damage"),
|
||||
@ -58,6 +88,8 @@ data class EntityDamageTeam(val type: TeamType = TeamType.NULL, val team: Int =
|
||||
}
|
||||
|
||||
companion object {
|
||||
val NULL = EntityDamageTeam()
|
||||
val PASSIVE = EntityDamageTeam(TeamType.PASSIVE)
|
||||
val CODEC = nativeCodec(::EntityDamageTeam, EntityDamageTeam::write)
|
||||
val LEGACY_CODEC = legacyCodec(::EntityDamageTeam, EntityDamageTeam::write)
|
||||
}
|
||||
@ -72,3 +104,116 @@ data class TouchDamage(
|
||||
val knockback: Double = 0.0,
|
||||
val statusEffects: ImmutableSet<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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -1,18 +1,18 @@
|
||||
package ru.dbotthepony.kstarbound.defs
|
||||
|
||||
import com.google.gson.JsonArray
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonNull
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.TypeAdapter
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
|
||||
import ru.dbotthepony.kommons.util.Either
|
||||
import ru.dbotthepony.kstarbound.Starbound
|
||||
import ru.dbotthepony.kstarbound.util.AssetPathStack
|
||||
import ru.dbotthepony.kommons.gson.set
|
||||
import java.util.function.Consumer
|
||||
import ru.dbotthepony.kommons.util.Delegate
|
||||
import ru.dbotthepony.kstarbound.json.JsonPath
|
||||
import java.util.function.Function
|
||||
import java.util.function.Supplier
|
||||
import kotlin.properties.Delegates
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KProperty
|
||||
import kotlin.reflect.javaType
|
||||
@ -22,36 +22,32 @@ import kotlin.reflect.javaType
|
||||
*/
|
||||
abstract class JsonDriven(val path: String) {
|
||||
private val delegates = ArrayList<Property<*>>()
|
||||
private val delegatesMap = HashMap<String, ArrayList<Property<*>>>()
|
||||
|
||||
private val lazies = ArrayList<LazyData<*>>()
|
||||
private val namedLazies = HashMap<String, ArrayList<LazyData<*>>>()
|
||||
|
||||
protected val properties = JsonObject()
|
||||
|
||||
/**
|
||||
* [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() {
|
||||
delegates.forEach { it.invalidate() }
|
||||
lazies.forEach { it.invalidate() }
|
||||
}
|
||||
|
||||
protected open fun invalidate(name: String) {
|
||||
delegatesMap[name]?.forEach { it.invalidate() }
|
||||
namedLazies[name]?.forEach { it.invalidate() }
|
||||
lazies.forEach { it.invalidate() }
|
||||
}
|
||||
|
||||
inner class LazyData<T>(names: Iterable<String> = listOf(), private val initializer: () -> T) : Lazy<T> {
|
||||
constructor(initializer: () -> T) : this(listOf(), initializer)
|
||||
|
||||
inner class LazyData<T>(private val initializer: () -> T) : Lazy<T> {
|
||||
init {
|
||||
for (name in names) {
|
||||
namedLazies.computeIfAbsent(name, Function { ArrayList() }).add(this)
|
||||
}
|
||||
lazies.add(this)
|
||||
}
|
||||
|
||||
private var _value: Any? = mark
|
||||
@ -78,50 +74,35 @@ abstract class JsonDriven(val path: String) {
|
||||
}
|
||||
|
||||
inner class Property<T>(
|
||||
name: String? = null,
|
||||
val name: JsonPath,
|
||||
val default: Either<Supplier<T>, JsonElement>? = null,
|
||||
private var adapter: TypeAdapter<T>? = null,
|
||||
) : Supplier<T>, Consumer<T>, ReadWriteProperty<Any?, T> {
|
||||
constructor(name: String, 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: String, 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)
|
||||
}
|
||||
) : Delegate<T>, ReadWriteProperty<Any?, T> {
|
||||
constructor(name: JsonPath, default: T, adapter: TypeAdapter<T>? = null) : this(name, Either.left(Supplier { default }), adapter)
|
||||
constructor(name: JsonPath, default: Supplier<T>, adapter: TypeAdapter<T>? = null) : this(name, Either.left(default), adapter)
|
||||
constructor(name: JsonPath, default: JsonElement, adapter: TypeAdapter<T>? = null) : this(name, Either.right(default), adapter)
|
||||
|
||||
init {
|
||||
delegates.add(this)
|
||||
|
||||
if (name != null)
|
||||
delegatesMap.computeIfAbsent(name, Function { ArrayList() }).add(this)
|
||||
}
|
||||
|
||||
private var value: Supplier<T> = never as Supplier<T>
|
||||
private var value: Supplier<T> by Delegates.notNull()
|
||||
|
||||
private fun compute(): T {
|
||||
val value = dataValue(checkNotNull(name))
|
||||
val value = lookupProperty(name)
|
||||
|
||||
if (value == null) {
|
||||
if (value.isJsonNull) {
|
||||
if (default == null) {
|
||||
throw NoSuchElementException("No json value present at '$name', and no default value was provided")
|
||||
} else if (default.isLeft) {
|
||||
return default.left().get()
|
||||
} else {
|
||||
AssetPathStack.block(path) {
|
||||
AssetPathStack(this@JsonDriven.path) {
|
||||
return adapter!!.fromJsonTree(default.right())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
AssetPathStack.block(path) {
|
||||
AssetPathStack(this@JsonDriven.path) {
|
||||
return adapter!!.fromJsonTree(value)
|
||||
}
|
||||
}
|
||||
@ -144,12 +125,11 @@ abstract class JsonDriven(val path: String) {
|
||||
}
|
||||
|
||||
override fun accept(t: T) {
|
||||
AssetPathStack.block(path) {
|
||||
properties[checkNotNull(name)] = adapter!!.toJsonTree(t)
|
||||
AssetPathStack(this@JsonDriven.path) {
|
||||
setProperty0(name, adapter!!.toJsonTree(t))
|
||||
}
|
||||
|
||||
// value = Supplier { t }
|
||||
invalidate(name!!)
|
||||
value = Supplier { t }
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
@ -158,10 +138,6 @@ abstract class JsonDriven(val path: String) {
|
||||
adapter = Starbound.gson.getAdapter(TypeToken.get(property.returnType.javaType)) as TypeAdapter<T>
|
||||
}
|
||||
|
||||
if (name == null) {
|
||||
name = property.name
|
||||
}
|
||||
|
||||
return value.get()
|
||||
}
|
||||
|
||||
@ -171,63 +147,11 @@ abstract class JsonDriven(val path: String) {
|
||||
adapter = Starbound.gson.getAdapter(TypeToken.get(property.returnType.javaType)) as TypeAdapter<T>
|
||||
}
|
||||
|
||||
if (name == null) {
|
||||
name = property.name
|
||||
}
|
||||
|
||||
return accept(value)
|
||||
}
|
||||
}
|
||||
|
||||
fun dataValue(name: String, alwaysCopy: Boolean = false): JsonElement? {
|
||||
val defs = defs()
|
||||
var value: JsonElement?
|
||||
|
||||
if (defs.isEmpty()) {
|
||||
value = properties[name]?.let { if (alwaysCopy) it.deepCopy() else it }
|
||||
} else {
|
||||
val itr = defs.iterator()
|
||||
var isCopy = false
|
||||
value = properties[name]
|
||||
|
||||
while ((value == null || value is JsonObject) && itr.hasNext()) {
|
||||
val next = itr.next()[name]
|
||||
|
||||
if (value is JsonObject) {
|
||||
if (next !is JsonObject) continue
|
||||
value = mergeNoCopy(if (isCopy) value else value.deepCopy(), next)
|
||||
isCopy = true
|
||||
} else {
|
||||
value = next
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
fun hasDataValue(name: String): Boolean {
|
||||
if (properties[name] != null) return true
|
||||
return defs().any { it[name] != null }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val mark = Any()
|
||||
private val never = Supplier { throw NoSuchElementException() }
|
||||
|
||||
@JvmStatic
|
||||
fun mergeNoCopy(a: JsonObject, b: JsonObject): JsonObject {
|
||||
for ((k, v) in b.entrySet()) {
|
||||
val existing = a[k]
|
||||
|
||||
if (existing is JsonObject && v is JsonObject) {
|
||||
a[k] = mergeNoCopy(existing, v)
|
||||
} else if (existing !is JsonObject) {
|
||||
a[k] = v.deepCopy()
|
||||
}
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ sealed class SpawnTarget {
|
||||
}
|
||||
|
||||
override fun resolve(world: ServerWorld): Vector2d? {
|
||||
return world.entities.values.firstOrNull { it.uniqueID == id }?.position
|
||||
return world.entities.values.firstOrNull { it.uniqueID.get().orNull() == id }?.position
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
|
@ -2,10 +2,12 @@ package ru.dbotthepony.kstarbound.defs.actor
|
||||
|
||||
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
|
||||
|
||||
// uint8_t
|
||||
enum class Gender(override val jsonName: String) : IStringSerializable {
|
||||
MALE("Male"), FEMALE("Female");
|
||||
}
|
||||
|
||||
// int32_t
|
||||
enum class HumanoidEmote(override val jsonName: String) : IStringSerializable {
|
||||
IDLE("Idle"),
|
||||
BLABBERING("Blabbering"),
|
||||
|
@ -1,10 +1,12 @@
|
||||
package ru.dbotthepony.kstarbound.defs.animation
|
||||
|
||||
import com.google.common.collect.ImmutableMap
|
||||
import com.google.gson.JsonArray
|
||||
import com.google.gson.JsonObject
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectAVLTreeMap
|
||||
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
|
||||
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
@JsonFactory
|
||||
data class AnimatedPartsDefinition(
|
||||
@ -25,6 +27,10 @@ data class AnimatedPartsDefinition(
|
||||
val states: ImmutableMap<String, State> = ImmutableMap.of(),
|
||||
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
|
||||
data class State(
|
||||
val frames: Int = 1,
|
||||
@ -32,8 +38,24 @@ data class AnimatedPartsDefinition(
|
||||
val mode: AnimationMode = AnimationMode.END,
|
||||
val transition: String = "",
|
||||
val properties: JsonObject = JsonObject(),
|
||||
val frameProperties: JsonObject = JsonObject(),
|
||||
)
|
||||
val frameProperties: ImmutableMap<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
|
||||
|
@ -256,37 +256,40 @@ class Image private constructor(
|
||||
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")
|
||||
|
||||
val xDivL = pixelOffset.x % PIXELS_IN_STARBOUND_UNITi
|
||||
val yDivB = pixelOffset.y % PIXELS_IN_STARBOUND_UNITi
|
||||
|
||||
val xDivR = (pixelOffset.x + width) % PIXELS_IN_STARBOUND_UNITi
|
||||
val yDivT = (pixelOffset.y + height) % PIXELS_IN_STARBOUND_UNITi
|
||||
|
||||
val leftMostX = pixelOffset.x / PIXELS_IN_STARBOUND_UNITi - (if (xDivL != 0) 1 else 0)
|
||||
val bottomMostY = pixelOffset.y / PIXELS_IN_STARBOUND_UNITi - (if (yDivB != 0) 1 else 0)
|
||||
|
||||
val rightMostX = (pixelOffset.x + width) / PIXELS_IN_STARBOUND_UNITi + (if (xDivR != 0) 1 else 0)
|
||||
val topMostY = (pixelOffset.y + height) / PIXELS_IN_STARBOUND_UNITi + (if (yDivT != 0) 1 else 0)
|
||||
val minX = pixelOffset.x / PIXELS_IN_STARBOUND_UNITi
|
||||
val minY = pixelOffset.y / PIXELS_IN_STARBOUND_UNITi
|
||||
val maxX = (width + pixelOffset.x + PIXELS_IN_STARBOUND_UNITi - 1) / PIXELS_IN_STARBOUND_UNITi
|
||||
val maxY = (height + pixelOffset.y + PIXELS_IN_STARBOUND_UNITi - 1) / PIXELS_IN_STARBOUND_UNITi
|
||||
|
||||
val result = ArrayList<Vector2i>()
|
||||
|
||||
for (y in bottomMostY .. topMostY) {
|
||||
for (x in leftMostX .. rightMostX) {
|
||||
val left = x * PIXELS_IN_STARBOUND_UNITi
|
||||
val bottom = y * PIXELS_IN_STARBOUND_UNITi
|
||||
// this is weird, but that's how original game handles this
|
||||
// also we don't cache this info since that's a waste of precious ram
|
||||
|
||||
var transparentPixels = 0
|
||||
for (yspace in minY until maxY) {
|
||||
for (xspace in minX until maxX) {
|
||||
var fillRatio = 0.0
|
||||
|
||||
for (sX in 0 until PIXELS_IN_STARBOUND_UNITi) {
|
||||
for (sY in 0 until PIXELS_IN_STARBOUND_UNITi) {
|
||||
if (isTransparent(xDivL + sX + left, yDivB + sY + bottom, flip)) {
|
||||
transparentPixels++
|
||||
for (y in 0 until PIXELS_IN_STARBOUND_UNITi) {
|
||||
val ypixel = (yspace * PIXELS_IN_STARBOUND_UNITi + y - pixelOffset.y)
|
||||
|
||||
if (ypixel !in 0 until width)
|
||||
continue
|
||||
|
||||
for (x in 0 until PIXELS_IN_STARBOUND_UNITi) {
|
||||
val xpixel = (xspace * PIXELS_IN_STARBOUND_UNITi + x - pixelOffset.x)
|
||||
|
||||
if (xpixel !in 0 until width)
|
||||
continue
|
||||
|
||||
if (isTransparent(xpixel, ypixel, flip)) {
|
||||
fillRatio += 1.0 / (PIXELS_IN_STARBOUND_UNIT * PIXELS_IN_STARBOUND_UNIT)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (transparentPixels * FILL_RATIO >= spaceScan) {
|
||||
result.add(Vector2i(x, y))
|
||||
if (fillRatio >= spaceScan) {
|
||||
result.add(Vector2i(xspace, yspace))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import org.classdump.luna.LuaRuntimeException
|
||||
import org.classdump.luna.Table
|
||||
import org.classdump.luna.TableFactory
|
||||
import ru.dbotthepony.kommons.gson.consumeNull
|
||||
import ru.dbotthepony.kommons.gson.contains
|
||||
import ru.dbotthepony.kommons.util.KOptional
|
||||
import ru.dbotthepony.kstarbound.lua.StateMachine
|
||||
import ru.dbotthepony.kstarbound.lua.from
|
||||
@ -47,12 +48,21 @@ fun ItemDescriptor(data: JsonElement): ItemDescriptor {
|
||||
val parameters = data.get(2, ::JsonObject)
|
||||
return ItemDescriptor(name, count, parameters)
|
||||
} else if (data is JsonObject) {
|
||||
val name = (data.get("name") ?: data.get("item") ?: throw JsonSyntaxException("Missing item name")).asString
|
||||
val count = data.get("count", 1L)
|
||||
val parameters = data.get("parameters") { data.get("parameters", ::JsonObject) }
|
||||
return ItemDescriptor(name, count, parameters)
|
||||
if ("id" in data && "version" in data && "content" in data) {
|
||||
// loading versioned json from original engine
|
||||
if (data["id"].asString != "Item")
|
||||
throw JsonSyntaxException("Expected id to be 'Item', ${data["id"]} given")
|
||||
|
||||
return ItemDescriptor(data["content"])
|
||||
} else {
|
||||
// loading regular json
|
||||
val name = (data.get("name") ?: data.get("item") ?: throw JsonSyntaxException("Missing item name")).asString
|
||||
val count = data.get("count", 1L)
|
||||
val parameters = data.get("parameters") { data.get("parameters", ::JsonObject) }
|
||||
return ItemDescriptor(name, count, parameters)
|
||||
}
|
||||
} else if (data is JsonNull) {
|
||||
return ItemDescriptor("air", 0L)
|
||||
return ItemDescriptor.EMPTY
|
||||
} else {
|
||||
throw JsonSyntaxException("Invalid item descriptor: $data")
|
||||
}
|
||||
|
@ -28,6 +28,8 @@ import ru.dbotthepony.kommons.gson.contains
|
||||
import ru.dbotthepony.kommons.gson.get
|
||||
import ru.dbotthepony.kommons.gson.getArray
|
||||
import ru.dbotthepony.kommons.gson.set
|
||||
import ru.dbotthepony.kstarbound.defs.AssetReference
|
||||
import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition
|
||||
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
|
||||
|
||||
data class ObjectDefinition(
|
||||
@ -46,8 +48,8 @@ data class ObjectDefinition(
|
||||
val breakDropOptions: ImmutableList<ImmutableList<ItemDescriptor>>? = null,
|
||||
val smashDropPool: Registry.Ref<TreasurePoolDefinition>? = null,
|
||||
val smashDropOptions: ImmutableList<ImmutableList<ItemDescriptor>> = ImmutableList.of(),
|
||||
//val animation: AssetReference<AnimationDefinition>? = null,
|
||||
val animation: AssetPath? = null,
|
||||
val animation: AssetReference<AnimationDefinition>? = null,
|
||||
//val animation: AssetPath? = null,
|
||||
val smashSounds: ImmutableSet<AssetPath> = ImmutableSet.of(),
|
||||
val smashParticles: JsonArray? = null,
|
||||
val smashable: Boolean = false,
|
||||
@ -83,7 +85,7 @@ data class ObjectDefinition(
|
||||
|
||||
class Adapter(gson: Gson) : TypeAdapter<ObjectDefinition>() {
|
||||
@JsonFactory(logMisses = false)
|
||||
class PlainData(
|
||||
data class PlainData(
|
||||
val objectName: String,
|
||||
val objectType: ObjectType = ObjectType.OBJECT,
|
||||
val race: String = "generic",
|
||||
@ -99,8 +101,8 @@ data class ObjectDefinition(
|
||||
val breakDropOptions: ImmutableList<ImmutableList<ItemDescriptor>>? = null,
|
||||
val smashDropPool: Registry.Ref<TreasurePoolDefinition>? = null,
|
||||
val smashDropOptions: ImmutableList<ImmutableList<ItemDescriptor>> = ImmutableList.of(),
|
||||
//val animation: AssetReference<AnimationDefinition>? = null,
|
||||
val animation: AssetPath? = null,
|
||||
val animation: AssetReference<AnimationDefinition>? = null,
|
||||
//val animation: AssetPath? = null,
|
||||
val smashSounds: ImmutableSet<AssetPath> = ImmutableSet.of(),
|
||||
val smashParticles: JsonArray? = null,
|
||||
val smashable: Boolean = false,
|
||||
|
@ -27,6 +27,9 @@ import ru.dbotthepony.kstarbound.json.setAdapter
|
||||
import ru.dbotthepony.kommons.gson.contains
|
||||
import ru.dbotthepony.kommons.gson.get
|
||||
import ru.dbotthepony.kommons.gson.set
|
||||
import ru.dbotthepony.kstarbound.Registries
|
||||
import ru.dbotthepony.kstarbound.Registry
|
||||
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
|
||||
import ru.dbotthepony.kstarbound.world.Side
|
||||
import kotlin.math.PI
|
||||
|
||||
@ -45,7 +48,7 @@ data class ObjectOrientation(
|
||||
val anchors: ImmutableSet<Anchor>,
|
||||
val anchorAny: Boolean,
|
||||
val directionAffinity: Side?,
|
||||
val materialSpaces: ImmutableList<Pair<Vector2i, String>>,
|
||||
val materialSpaces: ImmutableList<Pair<Vector2i, Registry.Ref<TileDefinition>>>,
|
||||
val interactiveSpaces: ImmutableSet<Vector2i>,
|
||||
val lightPosition: Vector2i,
|
||||
val beamAngle: Double,
|
||||
@ -170,11 +173,11 @@ data class ObjectOrientation(
|
||||
}
|
||||
}
|
||||
|
||||
var boundingBox = AABBi(Vector2i.ZERO, Vector2i.ZERO)
|
||||
|
||||
for (vec in occupySpaces) {
|
||||
boundingBox = boundingBox.expand(vec)
|
||||
}
|
||||
val minX = occupySpaces.minOf { it.x }
|
||||
val minY = occupySpaces.minOf { it.y }
|
||||
val maxX = occupySpaces.maxOf { it.x }
|
||||
val maxY = occupySpaces.maxOf { it.y }
|
||||
val boundingBox = AABBi(Vector2i(minX, minY), Vector2i(maxX, maxY))
|
||||
|
||||
val metaBoundBox = obj["metaBoundBox"]?.let { aabbs.fromJsonTree(it) }
|
||||
val requireTilledAnchors = obj.get("requireTilledAnchors", false)
|
||||
@ -249,7 +252,7 @@ data class ObjectOrientation(
|
||||
anchors = anchors.build(),
|
||||
anchorAny = anchorAny,
|
||||
directionAffinity = directionAffinity,
|
||||
materialSpaces = materialSpaces,
|
||||
materialSpaces = materialSpaces.stream().map { it.first to Registries.tiles.ref(it.second) }.collect(ImmutableList.toImmutableList()),
|
||||
interactiveSpaces = interactiveSpaces,
|
||||
lightPosition = lightPosition,
|
||||
beamAngle = beamAngle,
|
||||
|
@ -1,10 +1,12 @@
|
||||
package ru.dbotthepony.kstarbound.defs.`object`
|
||||
|
||||
enum class ObjectType {
|
||||
OBJECT,
|
||||
LOUNGEABLE,
|
||||
CONTAINER,
|
||||
FARMABLE,
|
||||
TELEPORTER,
|
||||
PHYSICS;
|
||||
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
|
||||
|
||||
enum class ObjectType(override val jsonName: String) : IStringSerializable {
|
||||
OBJECT("object"),
|
||||
LOUNGEABLE("loungeable"),
|
||||
CONTAINER("container"),
|
||||
FARMABLE("farmable"),
|
||||
TELEPORTER("teleporter"),
|
||||
PHYSICS("physics");
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,7 @@
|
||||
package ru.dbotthepony.kstarbound.defs.tile
|
||||
|
||||
import com.google.common.collect.ImmutableMap
|
||||
import it.unimi.dsi.fastutil.objects.Object2DoubleMap
|
||||
import it.unimi.dsi.fastutil.objects.Object2DoubleMaps
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArraySet
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
|
||||
|
||||
@ -29,6 +28,32 @@ data class TileDamageConfig(
|
||||
return (damageFactorsMapped[damage.type] ?: 1.0) * damage.amount
|
||||
}
|
||||
|
||||
operator fun plus(other: TileDamageConfig): TileDamageConfig {
|
||||
val damageRecovery = damageRecovery + other.damageRecovery
|
||||
val maximumEffectTime = maximumEffectTime.coerceAtLeast(other.maximumEffectTime)
|
||||
val totalHealth = totalHealth + other.totalHealth
|
||||
val harvestLevel = harvestLevel.coerceAtLeast(other.harvestLevel)
|
||||
|
||||
// TODO: in original code calculation of damage factors appears to be wrong
|
||||
// TODO: due to copy-paste error in for() loop argument
|
||||
// So this code is a little different
|
||||
val combinedKeys = ObjectArraySet<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 {
|
||||
val EMPTY = TileDamageConfig()
|
||||
|
||||
|
@ -2,19 +2,19 @@ package ru.dbotthepony.kstarbound.defs.tile
|
||||
|
||||
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
|
||||
|
||||
enum class TileDamageType(override val jsonName: String) : IStringSerializable {
|
||||
enum class TileDamageType(override val jsonName: String, val isPenetrating: Boolean) : IStringSerializable {
|
||||
// Damage done that will not actually kill the target
|
||||
PROTECTED("protected"),
|
||||
PROTECTED("protected", false),
|
||||
// Best at chopping down trees, things made of wood, etc.
|
||||
PLANT("plantish"),
|
||||
PLANT("plantish", false),
|
||||
// For digging / drilling through materials
|
||||
BLOCK("blockish"),
|
||||
BLOCK("blockish", false),
|
||||
// Gravity gun etc
|
||||
BEAM("beamish"),
|
||||
BEAM("beamish", false),
|
||||
// Penetrating damage done passivly by explosions.
|
||||
EXPLOSIVE("explosive"),
|
||||
EXPLOSIVE("explosive", true),
|
||||
// Can melt certain block types
|
||||
FIRE("fire"),
|
||||
FIRE("fire", false),
|
||||
// Can "till" certain materials into others
|
||||
TILLING("tilling");
|
||||
TILLING("tilling", false);
|
||||
}
|
||||
|
@ -45,6 +45,10 @@ data class TileDefinition(
|
||||
override val renderTemplate: AssetReference<RenderTemplate>,
|
||||
override val renderParameters: RenderParameters,
|
||||
) : IRenderableTile, IThingWithDescription by descriptionData {
|
||||
init {
|
||||
require(materialId > 0) { "Invalid tile ID $materialId" }
|
||||
}
|
||||
|
||||
val actualDamageTable: TileDamageConfig by lazy {
|
||||
val dmg = damageTable.value ?: TileDamageConfig.EMPTY
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
package ru.dbotthepony.kstarbound.defs.tile
|
||||
|
||||
import com.google.common.collect.ImmutableList
|
||||
import ru.dbotthepony.kstarbound.GlobalDefaults
|
||||
import ru.dbotthepony.kstarbound.defs.AssetReference
|
||||
import ru.dbotthepony.kstarbound.defs.IThingWithDescription
|
||||
import ru.dbotthepony.kstarbound.defs.ThingDescription
|
||||
@ -12,8 +13,8 @@ data class TileModifierDefinition(
|
||||
val modId: Int,
|
||||
val modName: String,
|
||||
val itemDrop: String? = null,
|
||||
val health: Double = 0.0,
|
||||
val harvestLevel: Double = 0.0,
|
||||
val health: Double? = null,
|
||||
val requiredHarvestLevel: Int? = null,
|
||||
val breaksWithTile: Boolean = true,
|
||||
val grass: Boolean = false,
|
||||
val miningParticle: String? = null,
|
||||
@ -21,6 +22,9 @@ data class TileModifierDefinition(
|
||||
val footstepSound: String? = null,
|
||||
val miningSounds: ImmutableList<String> = ImmutableList.of(),
|
||||
|
||||
@Deprecated("", replaceWith = ReplaceWith("this.actualDamageTable"))
|
||||
val damageTable: AssetReference<TileDamageConfig> = AssetReference(GlobalDefaults::tileDamage),
|
||||
|
||||
@JsonFlat
|
||||
val descriptionData: ThingDescription,
|
||||
|
||||
@ -28,6 +32,20 @@ data class TileModifierDefinition(
|
||||
override val renderParameters: RenderParameters
|
||||
) : IRenderableTile, IThingWithDescription by descriptionData {
|
||||
init {
|
||||
require(modId > 0) { "Invalid material modifier ID $modId" }
|
||||
require(modId > 0) { "Invalid tile modifier ID $modId" }
|
||||
}
|
||||
|
||||
val actualDamageTable: TileDamageConfig by lazy {
|
||||
val dmg = damageTable.value ?: TileDamageConfig.EMPTY
|
||||
|
||||
return@lazy if (health == null && requiredHarvestLevel == null) {
|
||||
dmg
|
||||
} else if (health != null && requiredHarvestLevel != null) {
|
||||
dmg.copy(totalHealth = health, harvestLevel = requiredHarvestLevel)
|
||||
} else if (health != null) {
|
||||
dmg.copy(totalHealth = health)
|
||||
} else {
|
||||
dmg.copy(harvestLevel = requiredHarvestLevel!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ import ru.dbotthepony.kstarbound.collect.WeightedList
|
||||
import ru.dbotthepony.kstarbound.defs.JsonDriven
|
||||
import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters
|
||||
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
|
||||
import ru.dbotthepony.kstarbound.json.mergeJson
|
||||
import ru.dbotthepony.kstarbound.json.pairAdapter
|
||||
import ru.dbotthepony.kstarbound.json.stream
|
||||
import ru.dbotthepony.kstarbound.util.binnedChoice
|
||||
@ -276,8 +277,8 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
|
||||
|
||||
fun generate(typeName: String, sizeName: String, random: RandomGenerator): TerrestrialWorldParameters {
|
||||
val config = GlobalDefaults.terrestrialWorlds.planetDefaults.deepCopy()
|
||||
JsonDriven.mergeNoCopy(config, GlobalDefaults.terrestrialWorlds.planetSizes[sizeName] ?: throw NoSuchElementException("Unknown world size name $sizeName"))
|
||||
JsonDriven.mergeNoCopy(config, GlobalDefaults.terrestrialWorlds.planetTypes[typeName] ?: throw NoSuchElementException("Unknown world type name $typeName"))
|
||||
mergeJson(config, GlobalDefaults.terrestrialWorlds.planetSizes[sizeName] ?: throw NoSuchElementException("Unknown world size name $sizeName"))
|
||||
mergeJson(config, GlobalDefaults.terrestrialWorlds.planetTypes[typeName] ?: throw NoSuchElementException("Unknown world type name $typeName"))
|
||||
|
||||
val params = Starbound.gson.fromJson(config, Generic::class.java)
|
||||
|
||||
@ -352,7 +353,7 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
|
||||
|
||||
fun makeRegion(name: String, baseHeight: Int): Pair<Region, Region> {
|
||||
val primaryRegionJson = GlobalDefaults.terrestrialWorlds.regionDefaults.deepCopy()
|
||||
JsonDriven.mergeNoCopy(primaryRegionJson, GlobalDefaults.terrestrialWorlds.regionTypes[name]!!)
|
||||
mergeJson(primaryRegionJson, GlobalDefaults.terrestrialWorlds.regionTypes[name]!!)
|
||||
|
||||
val region = readRegion(primaryRegionJson, baseHeight)
|
||||
val subRegionList = primaryRegionJson.getArray("subRegion")
|
||||
@ -361,7 +362,7 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
|
||||
primaryRegionJson
|
||||
} else {
|
||||
val result = GlobalDefaults.terrestrialWorlds.regionDefaults.deepCopy()
|
||||
JsonDriven.mergeNoCopy(result, GlobalDefaults.terrestrialWorlds.regionTypes[subRegionList.random(random).asString]!!)
|
||||
mergeJson(result, GlobalDefaults.terrestrialWorlds.regionTypes[subRegionList.random(random).asString]!!)
|
||||
result
|
||||
}, baseHeight)
|
||||
|
||||
@ -372,8 +373,7 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
|
||||
if (layerName !in layers)
|
||||
return null
|
||||
|
||||
val layerConfig = config.getObject("layerDefaults").deepCopy()
|
||||
JsonDriven.mergeNoCopy(layerConfig, layers.getObject(layerName))
|
||||
val layerConfig = mergeJson(config.getObject("layerDefaults").deepCopy(), layers.getObject(layerName))
|
||||
|
||||
if (!layerConfig.get("enabled", false))
|
||||
return null
|
||||
|
@ -10,6 +10,7 @@ import ru.dbotthepony.kommons.io.readLong
|
||||
import ru.dbotthepony.kommons.io.readSignedVarInt
|
||||
import ru.dbotthepony.kommons.io.readVector2d
|
||||
import ru.dbotthepony.kommons.io.readVector2f
|
||||
import ru.dbotthepony.kommons.io.writeBinaryString
|
||||
import ru.dbotthepony.kommons.io.writeDouble
|
||||
import ru.dbotthepony.kommons.io.writeFloat
|
||||
import ru.dbotthepony.kommons.io.writeLong
|
||||
@ -18,6 +19,7 @@ import ru.dbotthepony.kommons.io.writeStruct2d
|
||||
import ru.dbotthepony.kommons.io.writeStruct2f
|
||||
import ru.dbotthepony.kommons.math.RGBAColor
|
||||
import ru.dbotthepony.kommons.util.AABB
|
||||
import ru.dbotthepony.kommons.util.Either
|
||||
import ru.dbotthepony.kommons.util.KOptional
|
||||
import ru.dbotthepony.kommons.vector.Vector2d
|
||||
import ru.dbotthepony.kommons.vector.Vector2i
|
||||
@ -25,6 +27,7 @@ import ru.dbotthepony.kstarbound.Starbound
|
||||
import ru.dbotthepony.kstarbound.world.ChunkPos
|
||||
import java.io.DataInput
|
||||
import java.io.DataOutput
|
||||
import java.io.EOFException
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
@ -111,3 +114,65 @@ fun OutputStream.writeAABB(value: AABB) {
|
||||
writeStruct2d(value.mins)
|
||||
writeStruct2d(value.maxs)
|
||||
}
|
||||
|
||||
private fun InputStream.readBoolean(): Boolean {
|
||||
val read = read()
|
||||
|
||||
if (read == -1)
|
||||
throw EOFException("End of stream reached")
|
||||
|
||||
return read != 0
|
||||
}
|
||||
|
||||
private fun InputStream.readUnsignedByte(): Int {
|
||||
val read = read()
|
||||
|
||||
if (read == -1)
|
||||
throw EOFException("End of stream reached")
|
||||
|
||||
return read
|
||||
}
|
||||
|
||||
fun <S : 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()) }
|
||||
|
@ -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}")
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
260
src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonPath.kt
Normal file
260
src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonPath.kt
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
@ -8,15 +8,15 @@ import com.google.gson.JsonArray
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonNull
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonPrimitive
|
||||
import com.google.gson.JsonSyntaxException
|
||||
import com.google.gson.TypeAdapter
|
||||
import com.google.gson.TypeAdapterFactory
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import com.google.gson.stream.JsonReader
|
||||
import com.google.gson.stream.JsonToken
|
||||
import com.google.gson.stream.JsonWriter
|
||||
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
|
||||
import ru.dbotthepony.kommons.gson.set
|
||||
import ru.dbotthepony.kstarbound.Starbound
|
||||
|
||||
inline fun <reified T> Gson.getAdapter(): TypeAdapter<T> {
|
||||
return getAdapter(object : TypeToken<T>() {})
|
||||
}
|
||||
|
||||
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>
|
||||
@ -53,3 +53,73 @@ inline fun <reified A, reified B> Gson.pairSetAdapter(): TypeAdapter<ImmutableSe
|
||||
inline fun <reified E> Gson.mutableSetAdapter(): TypeAdapter<ObjectOpenHashSet<E>> {
|
||||
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
|
||||
}
|
||||
|
@ -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`)
|
||||
}
|
||||
}
|
@ -1,9 +1,5 @@
|
||||
package ru.dbotthepony.kstarbound.json.builder
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.TypeAdapter
|
||||
import com.google.gson.TypeAdapterFactory
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
@ -91,15 +87,3 @@ annotation class JsonImplementation(val implementingClass: KClass<*>)
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class JsonSingleton
|
||||
|
||||
object JsonImplementationTypeFactory : TypeAdapterFactory {
|
||||
override fun <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
|
||||
}
|
||||
}
|
||||
|
@ -484,7 +484,7 @@ class FactoryAdapter<T : Any> private constructor(
|
||||
companion object {
|
||||
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 properties = kclass.declaredMembers.filterIsInstance<KProperty1<T, *>>()
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
package ru.dbotthepony.kstarbound.math
|
||||
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
/**
|
||||
@ -77,3 +78,25 @@ fun weakDoubleZeroing(value: Double, epsilon: Double = EPSILON): Double {
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
fun angleDifference(angle0: Double, angle1: Double): Double {
|
||||
var diff = (angle1 - angle0 + PI) % (PI * 2.0)
|
||||
|
||||
if (diff < 0.0)
|
||||
diff += PI * 2.0
|
||||
|
||||
return diff
|
||||
}
|
||||
|
||||
fun normalizeAngle(angle: Double): Double {
|
||||
var value = (angle + PI) % (PI * 2.0)
|
||||
|
||||
if (value < 0.0)
|
||||
value += PI * 2.0
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
fun approachAngle(target: Double, current: Double, limit: Double): Double {
|
||||
return normalizeAngle(current + angleDifference(current, target).coerceIn(-limit, limit))
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import java.io.DataInputStream
|
||||
import java.io.DataOutputStream
|
||||
|
||||
class TileDamageUpdatePacket(val x: Int, val y: Int, val isBackground: Boolean, val health: TileHealth) : IClientPacket {
|
||||
constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readInt(), stream.readInt(), stream.readBoolean(), TileHealth(stream, isLegacy))
|
||||
constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readInt(), stream.readInt(), stream.readBoolean(), TileHealth.Tile(stream, isLegacy))
|
||||
|
||||
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
|
||||
stream.writeInt(x)
|
||||
|
@ -20,6 +20,7 @@ import ru.dbotthepony.kommons.io.readVarInt
|
||||
import ru.dbotthepony.kommons.io.readVarLong
|
||||
import ru.dbotthepony.kommons.io.writeBinaryString
|
||||
import ru.dbotthepony.kommons.io.writeByteArray
|
||||
import ru.dbotthepony.kommons.io.writeShort
|
||||
import ru.dbotthepony.kommons.io.writeVarInt
|
||||
import ru.dbotthepony.kommons.io.writeVarLong
|
||||
import ru.dbotthepony.kommons.math.RGBAColor
|
||||
@ -59,6 +60,8 @@ val AABBCodecLegacy = StreamCodec.Impl(DataInputStream::readAABBLegacy, DataOutp
|
||||
val AABBCodecLegacyOptional = StreamCodec.Impl(DataInputStream::readAABBLegacyOptional, DataOutputStream::writeAABBLegacyOptional)
|
||||
val AABBCodecNative = StreamCodec.Impl(DataInputStream::readAABB, DataOutputStream::writeAABB)
|
||||
|
||||
val UnsignedShortCodec = StreamCodec.Impl(DataInputStream::readUnsignedShort, DataOutputStream::writeShort)
|
||||
|
||||
val ValidatingBooleanCodec = StreamCodec.Impl({
|
||||
when (val read = it.readUnsignedByte()) {
|
||||
0 -> false
|
||||
@ -117,6 +120,7 @@ fun networkedAABBNullable(value: KOptional<AABB> = KOptional()) = networkedData(
|
||||
|
||||
// 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>, 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 networkedStatefulItem(value: ItemStack = ItemStack.EMPTY) = NetworkedStatefulItemStack(value)
|
||||
|
||||
|
@ -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()}]"
|
||||
}
|
||||
}
|
@ -5,8 +5,10 @@ import ru.dbotthepony.kommons.io.StreamCodec
|
||||
import ru.dbotthepony.kommons.io.readVarInt
|
||||
import ru.dbotthepony.kommons.io.writeVarInt
|
||||
import ru.dbotthepony.kommons.util.KOptional
|
||||
import ru.dbotthepony.kommons.util.Listenable
|
||||
import java.io.DataInputStream
|
||||
import java.io.DataOutputStream
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
/**
|
||||
* [isDumb] is responsible for specifying whenever legacy protocol networks entire map each time
|
||||
@ -57,6 +59,7 @@ class NetworkedMap<K, V>(
|
||||
backlog.add(currentVersion() to clearEntry)
|
||||
|
||||
purgeBacklog()
|
||||
listeners.forEach { it.listener.onClear() }
|
||||
}
|
||||
|
||||
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" }
|
||||
backlog.add(currentVersion() to Entry(Action.ADD, KOptional(nativeKey.copy(key)), KOptional(nativeValue.copy(value))))
|
||||
purgeBacklog()
|
||||
listeners.forEach { it.listener.onValueAdded(key, value) }
|
||||
}
|
||||
|
||||
override fun onValueRemoved(key: K, value: V) {
|
||||
@ -71,10 +75,31 @@ class NetworkedMap<K, V>(
|
||||
check(!isRemote) { "This map is not owned by this side" }
|
||||
backlog.add(currentVersion() to Entry(Action.REMOVE, KOptional(nativeKey.copy(key)), KOptional()))
|
||||
purgeBacklog()
|
||||
listeners.forEach { it.listener.onValueRemoved(key, value) }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private val listeners = CopyOnWriteArrayList<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 {
|
||||
StreamCodec.Map(keyCodec.second, valueCodec.second, ::HashMap)
|
||||
}
|
||||
@ -253,7 +278,7 @@ class NetworkedMap<K, V>(
|
||||
val change = if (isLegacy) readLegacyEntry(data) else readNativeEntry(data)
|
||||
backlog.add(currentVersion() to change)
|
||||
|
||||
if (isInterpolating && interpolationDelay > 0.0) {
|
||||
if (isInterpolating) {
|
||||
val actualDelay = interpolationDelay + currentTime
|
||||
|
||||
if (delayed.isNotEmpty() && delayed.last().first > actualDelay) {
|
||||
@ -261,7 +286,10 @@ class NetworkedMap<K, V>(
|
||||
delayed.clear()
|
||||
}
|
||||
|
||||
delayed.add(actualDelay to change)
|
||||
if (interpolationDelay > 0.0)
|
||||
delayed.add(actualDelay to change)
|
||||
else
|
||||
change.apply(this)
|
||||
} else {
|
||||
change.apply(this)
|
||||
}
|
||||
|
@ -4,12 +4,12 @@ import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonPrimitive
|
||||
import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
|
||||
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
|
||||
import ru.dbotthepony.kommons.guava.immutableMap
|
||||
import ru.dbotthepony.kstarbound.Registries
|
||||
import ru.dbotthepony.kstarbound.Registry
|
||||
import ru.dbotthepony.kstarbound.Starbound
|
||||
import ru.dbotthepony.kstarbound.defs.quest.QuestDescriptor
|
||||
import ru.dbotthepony.kstarbound.defs.actor.player.TechDefinition
|
||||
import ru.dbotthepony.kstarbound.lua.NewLuaState
|
||||
import ru.dbotthepony.kstarbound.lua.luaFunction
|
||||
@ -294,9 +294,10 @@ class Avatar(val uniqueId: UUID) {
|
||||
val questId = value["questId"]?.asString ?: throw IllegalArgumentException("Invalid 'questId' in quest descriptor")
|
||||
val templateId = value["templateId"]?.asString ?: questId
|
||||
val params = value["parameters"] as? JsonObject ?: JsonObject()
|
||||
val quest = QuestInstance(this, descriptor = QuestDescriptor(questId, templateId, seed, params), serverID = serverID?.let(UUID::fromString), worldID = worldID)
|
||||
addQuest(quest)
|
||||
return quest.id
|
||||
//val quest = QuestInstance(this, descriptor = QuestDescriptor(questId, templateId, params, seed), serverID = serverID?.let(UUID::fromString), worldID = worldID)
|
||||
//addQuest(quest)
|
||||
//return quest.id
|
||||
TODO()
|
||||
} else {
|
||||
throw IllegalArgumentException("Invalid quest descriptor: $value")
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
@ -3,7 +3,6 @@ package ru.dbotthepony.kstarbound.player
|
||||
import com.google.gson.JsonArray
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonObject
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import ru.dbotthepony.kstarbound.Registries
|
||||
import ru.dbotthepony.kstarbound.Starbound
|
||||
@ -11,6 +10,7 @@ import ru.dbotthepony.kstarbound.defs.quest.QuestTemplate
|
||||
import ru.dbotthepony.kstarbound.lua.NewLuaState
|
||||
import ru.dbotthepony.kstarbound.item.ItemStack
|
||||
import ru.dbotthepony.kommons.gson.set
|
||||
import ru.dbotthepony.kstarbound.defs.quest.QuestDescriptor
|
||||
import java.util.HashMap
|
||||
import java.util.UUID
|
||||
|
||||
@ -46,7 +46,7 @@ class QuestInstance(
|
||||
var compassDirection: Double? = null
|
||||
|
||||
private val portraits = JsonObject()
|
||||
private val params = descriptor.parameters.deepCopy()
|
||||
//private val params = descriptor.parameters.deepCopy()
|
||||
|
||||
private val portraitTitles = HashMap<String, String>()
|
||||
|
||||
@ -78,9 +78,9 @@ class QuestInstance(
|
||||
}
|
||||
|
||||
init {
|
||||
for ((k, v) in descriptor.parameters.entrySet()) {
|
||||
params[k] = v.deepCopy()
|
||||
}
|
||||
//for ((k, v) in descriptor.parameters.entrySet()) {
|
||||
// params[k] = v.deepCopy()
|
||||
//}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -159,7 +159,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
|
||||
if (world == null) {
|
||||
send(PlayerWarpResultPacket(false, request, false))
|
||||
} else {
|
||||
currentWarpStatus = world.acceptClient(this).exceptionally {
|
||||
currentWarpStatus = world.acceptClient(this, request).exceptionally {
|
||||
send(PlayerWarpResultPacket(false, request, false))
|
||||
|
||||
if (world == shipWorld) {
|
||||
@ -244,7 +244,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
|
||||
enqueueWarp(WarpAlias.OwnShip)
|
||||
warpingAllowed = true
|
||||
|
||||
if (server.channels.connections.size == 2) {
|
||||
if (server.channels.connections.size > 1) {
|
||||
enqueueWarp(WarpAction.Player(server.channels.connections.first().uuid!!))
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ import ru.dbotthepony.kstarbound.world.api.AbstractCell
|
||||
import ru.dbotthepony.kstarbound.world.api.ImmutableCell
|
||||
import ru.dbotthepony.kstarbound.world.api.MutableCell
|
||||
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
|
||||
import ru.dbotthepony.kstarbound.world.entities.WorldObject
|
||||
import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.Closeable
|
||||
|
@ -5,17 +5,22 @@ import it.unimi.dsi.fastutil.ints.IntArraySet
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectFunction
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap
|
||||
import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArraySet
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import ru.dbotthepony.kommons.util.AABBi
|
||||
import ru.dbotthepony.kommons.util.IStruct2i
|
||||
import ru.dbotthepony.kommons.vector.Vector2d
|
||||
import ru.dbotthepony.kommons.vector.Vector2i
|
||||
import ru.dbotthepony.kstarbound.Starbound
|
||||
import ru.dbotthepony.kstarbound.defs.WarpAction
|
||||
import ru.dbotthepony.kstarbound.defs.WorldID
|
||||
import ru.dbotthepony.kstarbound.defs.tile.TileDamage
|
||||
import ru.dbotthepony.kstarbound.defs.tile.TileDamageResult
|
||||
import ru.dbotthepony.kstarbound.defs.tile.TileDamageType
|
||||
import ru.dbotthepony.kstarbound.defs.world.WorldStructure
|
||||
import ru.dbotthepony.kstarbound.defs.world.WorldTemplate
|
||||
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
|
||||
import ru.dbotthepony.kstarbound.json.jsonArrayOf
|
||||
import ru.dbotthepony.kstarbound.network.IPacket
|
||||
import ru.dbotthepony.kstarbound.network.packets.StepUpdatePacket
|
||||
import ru.dbotthepony.kstarbound.network.packets.clientbound.PlayerWarpResultPacket
|
||||
@ -30,6 +35,8 @@ import ru.dbotthepony.kstarbound.world.World
|
||||
import ru.dbotthepony.kstarbound.world.WorldGeometry
|
||||
import ru.dbotthepony.kstarbound.world.api.ImmutableCell
|
||||
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
|
||||
import ru.dbotthepony.kstarbound.world.entities.tile.TileEntity
|
||||
import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import java.util.concurrent.RejectedExecutionException
|
||||
@ -167,12 +174,63 @@ class ServerWorld private constructor(
|
||||
if (damage.amount <= 0.0)
|
||||
return TileDamageResult.NONE
|
||||
|
||||
val actualPositions = positions.stream().map { geometry.wrap(it) }.distinct().toList()
|
||||
val actualPositions = positions.stream()
|
||||
.map { geometry.wrap(it) }
|
||||
.distinct()
|
||||
.map { it to chunkMap[geometry.chunkFromCell(it)] }
|
||||
.toList()
|
||||
|
||||
var topMost = TileDamageResult.NONE
|
||||
|
||||
for (pos in actualPositions) {
|
||||
val chunk = chunkMap[geometry.chunkFromCell(pos)] ?: continue
|
||||
topMost = topMost.coerceAtLeast(chunk.damageTile(pos - chunk.pos.tile, isBackground, sourcePosition, damage, source))
|
||||
val damagedEntities = ObjectArraySet<TileEntity>()
|
||||
|
||||
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
|
||||
|
@ -33,6 +33,7 @@ import ru.dbotthepony.kstarbound.world.TileHealth
|
||||
import ru.dbotthepony.kstarbound.world.api.ImmutableCell
|
||||
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
|
||||
import ru.dbotthepony.kstarbound.world.entities.player.PlayerEntity
|
||||
import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject
|
||||
import java.io.DataOutputStream
|
||||
import java.util.HashMap
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
@ -221,26 +222,24 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p
|
||||
val id = entity.entityID
|
||||
unseen.rem(id)
|
||||
|
||||
if (entity is PlayerEntity) {
|
||||
if (entityVersions.get(id) == -1L) {
|
||||
// never networked
|
||||
val initial = FastByteArrayOutputStream()
|
||||
entity.writeNetwork(DataOutputStream(initial), client.isLegacy)
|
||||
val (data, version) = entity.networkGroup.write(isLegacy = client.isLegacy)
|
||||
if (entityVersions.get(id) == -1L) {
|
||||
// never networked
|
||||
val initial = FastByteArrayOutputStream()
|
||||
entity.writeNetwork(DataOutputStream(initial), client.isLegacy)
|
||||
val (data, version) = entity.networkGroup.write(isLegacy = client.isLegacy)
|
||||
|
||||
entityVersions.put(id, version)
|
||||
entityVersions.put(id, version)
|
||||
|
||||
send(EntityCreatePacket(
|
||||
entity.type,
|
||||
ByteArrayList.wrap(initial.array, initial.length),
|
||||
data,
|
||||
entity.entityID
|
||||
))
|
||||
} else if (entity.networkGroup.upstream.hasChangedSince(entityVersions.get(id))) {
|
||||
val (data, version) = entity.networkGroup.write(remoteVersion = entityVersions.get(id), isLegacy = client.isLegacy)
|
||||
entityVersions.put(id, version)
|
||||
send(EntityUpdateSetPacket(entity.connectionID, Int2ObjectMaps.singleton(entity.entityID, data)))
|
||||
}
|
||||
send(EntityCreatePacket(
|
||||
entity.type,
|
||||
ByteArrayList.wrap(initial.array, initial.length),
|
||||
data,
|
||||
entity.entityID
|
||||
))
|
||||
} else if (entity.networkGroup.upstream.hasChangedSince(entityVersions.get(id))) {
|
||||
val (data, version) = entity.networkGroup.write(remoteVersion = entityVersions.get(id), isLegacy = client.isLegacy)
|
||||
entityVersions.put(id, version)
|
||||
send(EntityUpdateSetPacket(entity.connectionID, Int2ObjectMaps.singleton(entity.entityID, data)))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -21,6 +21,7 @@ import ru.dbotthepony.kstarbound.defs.world.CelestialParameters
|
||||
import ru.dbotthepony.kstarbound.defs.world.CelestialPlanet
|
||||
import ru.dbotthepony.kstarbound.defs.JsonDriven
|
||||
import ru.dbotthepony.kstarbound.io.BTreeDB5
|
||||
import ru.dbotthepony.kstarbound.json.mergeJson
|
||||
import ru.dbotthepony.kstarbound.json.readJsonElement
|
||||
import ru.dbotthepony.kstarbound.json.writeJsonElement
|
||||
import ru.dbotthepony.kstarbound.math.Line2d
|
||||
@ -192,7 +193,7 @@ class NativeUniverseSource(private val db: BTreeDB6?, private val universe: Serv
|
||||
systemPos,
|
||||
systemSeed,
|
||||
systemName,
|
||||
JsonDriven.mergeNoCopy(system.baseParameters.deepCopy(), system.variationParameters.random(random))
|
||||
mergeJson(system.baseParameters.deepCopy(), system.variationParameters.random(random))
|
||||
)
|
||||
|
||||
if ("typeName" !in systemParams.parameters) {
|
||||
@ -228,7 +229,7 @@ class NativeUniverseSource(private val db: BTreeDB6?, private val universe: Serv
|
||||
planetCoordinate,
|
||||
planetSeed,
|
||||
planetName,
|
||||
JsonDriven.mergeNoCopy(planetaryType.baseParameters.deepCopy(), planetaryType.variationParameters.random(random))
|
||||
mergeJson(planetaryType.baseParameters.deepCopy(), planetaryType.variationParameters.random(random))
|
||||
)
|
||||
|
||||
val satellites = Int2ObjectArrayMap<CelestialParameters>()
|
||||
@ -245,11 +246,11 @@ class NativeUniverseSource(private val db: BTreeDB6?, private val universe: Serv
|
||||
val satelliteCoordinate = UniversePos(location, planetOrbitIndex, satelliteOrbitIndex)
|
||||
|
||||
val merge = JsonObject()
|
||||
JsonDriven.mergeNoCopy(merge, satelliteType.baseParameters)
|
||||
JsonDriven.mergeNoCopy(merge, satelliteType.variationParameters.random(random))
|
||||
mergeJson(merge, satelliteType.baseParameters)
|
||||
mergeJson(merge, satelliteType.variationParameters.random(random))
|
||||
|
||||
if (systemOrbitRegion.regionName in satelliteType.orbitParameters) {
|
||||
JsonDriven.mergeNoCopy(merge, satelliteType.orbitParameters[systemOrbitRegion.regionName]!!.random(random))
|
||||
mergeJson(merge, satelliteType.orbitParameters[systemOrbitRegion.regionName]!!.random(random))
|
||||
}
|
||||
|
||||
satellites[satelliteOrbitIndex] = CelestialParameters(
|
||||
|
@ -1,8 +1,6 @@
|
||||
package ru.dbotthepony.kstarbound.util
|
||||
|
||||
import com.google.gson.JsonArray
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonObject
|
||||
import ru.dbotthepony.kstarbound.Starbound
|
||||
import java.util.*
|
||||
import java.util.stream.Stream
|
||||
@ -18,38 +16,8 @@ fun String.sbIntern2(): String {
|
||||
val JsonElement.asStringOrNull: String?
|
||||
get() = if (isJsonNull) null else asString
|
||||
|
||||
fun traverseJsonPath(path: String?, element: JsonElement?): JsonElement? {
|
||||
element ?: return null
|
||||
path ?: return element
|
||||
|
||||
if (path.contains('.')) {
|
||||
var current: JsonElement? = element
|
||||
|
||||
for (name in path.split('.')) {
|
||||
if (current is JsonObject) {
|
||||
current = current[name]
|
||||
} else if (current is JsonArray) {
|
||||
val toInt = name.toIntOrNull() ?: return null
|
||||
if (toInt !in 0 until current.size()) return null
|
||||
current = current.get(toInt)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return current
|
||||
} else {
|
||||
if (element is JsonObject) {
|
||||
return element[path]
|
||||
} else if (element is JsonArray) {
|
||||
val toInt = path.toIntOrNull() ?: return null
|
||||
if (toInt !in 0 until element.size()) return null
|
||||
return element[toInt]
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
val JsonElement.coalesceNull: JsonElement?
|
||||
get() = if (isJsonNull) null else this
|
||||
|
||||
fun UUID.toStarboundString(): String {
|
||||
val builder = StringBuilder(32)
|
||||
|
@ -1,12 +1,11 @@
|
||||
package ru.dbotthepony.kstarbound.world
|
||||
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArraySet
|
||||
import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet
|
||||
import ru.dbotthepony.kommons.arrays.Object2DArray
|
||||
import ru.dbotthepony.kommons.util.AABB
|
||||
import ru.dbotthepony.kommons.vector.Vector2d
|
||||
import ru.dbotthepony.kommons.vector.Vector2i
|
||||
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
|
||||
import ru.dbotthepony.kstarbound.defs.tile.TileDamage
|
||||
import ru.dbotthepony.kstarbound.defs.tile.TileDamageResult
|
||||
import ru.dbotthepony.kstarbound.defs.tile.TileDamageType
|
||||
@ -16,12 +15,10 @@ import ru.dbotthepony.kstarbound.world.api.AbstractCell
|
||||
import ru.dbotthepony.kstarbound.world.api.ICellAccess
|
||||
import ru.dbotthepony.kstarbound.world.api.ImmutableCell
|
||||
import ru.dbotthepony.kstarbound.world.api.OffsetCellAccess
|
||||
import ru.dbotthepony.kstarbound.world.api.TileColor
|
||||
import ru.dbotthepony.kstarbound.world.api.TileView
|
||||
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
|
||||
import ru.dbotthepony.kstarbound.world.entities.DynamicEntity
|
||||
import ru.dbotthepony.kstarbound.world.entities.TileEntity
|
||||
import java.util.concurrent.CopyOnWriteArraySet
|
||||
import kotlin.concurrent.withLock
|
||||
|
||||
/**
|
||||
* Чанк мира
|
||||
@ -70,43 +67,77 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
|
||||
}
|
||||
|
||||
protected val tileHealthForeground = lazy {
|
||||
Object2DArray(CHUNK_SIZE, CHUNK_SIZE) { _, _ -> TileHealth() }
|
||||
Object2DArray(CHUNK_SIZE, CHUNK_SIZE) { _, _ -> TileHealth.Tile() }
|
||||
}
|
||||
|
||||
protected val tileHealthBackground = lazy {
|
||||
Object2DArray(CHUNK_SIZE, CHUNK_SIZE) { _, _ -> TileHealth() }
|
||||
Object2DArray(CHUNK_SIZE, CHUNK_SIZE) { _, _ -> TileHealth.Tile() }
|
||||
}
|
||||
|
||||
fun damageTile(pos: Vector2i, isBackground: Boolean, sourcePosition: Vector2d, damage: TileDamage, source: AbstractEntity? = null): TileDamageResult {
|
||||
data class DamageResult(val result: TileDamageResult, val health: TileHealth? = null, val stateBefore: AbstractCell? = null)
|
||||
|
||||
fun damageTile(pos: Vector2i, isBackground: Boolean, sourcePosition: Vector2d, damage: TileDamage, source: AbstractEntity? = null): DamageResult {
|
||||
if (!cells.isInitialized()) {
|
||||
return TileDamageResult.NONE
|
||||
return DamageResult(TileDamageResult.NONE)
|
||||
}
|
||||
|
||||
val tile = cells.value[pos.x, pos.y]
|
||||
val cell = cells.value[pos.x, pos.y]
|
||||
|
||||
if (tile.isIndestructible || tile.tile(isBackground).material.isBuiltin) {
|
||||
return TileDamageResult.NONE
|
||||
if (cell.isIndestructible || cell.tile(isBackground).material.isBuiltin) {
|
||||
return DamageResult(TileDamageResult.NONE)
|
||||
}
|
||||
|
||||
var damage = damage
|
||||
var result = TileDamageResult.NORMAL
|
||||
|
||||
if (tile.dungeonId in world.protectedDungeonIDs) {
|
||||
if (cell.dungeonId in world.protectedDungeonIDs) {
|
||||
damage = damage.copy(type = TileDamageType.PROTECTED)
|
||||
result = TileDamageResult.PROTECTED
|
||||
}
|
||||
|
||||
val health = (if (isBackground) tileHealthBackground else tileHealthForeground).value[pos.x, pos.y]
|
||||
health.damage(tile.tile(isBackground).material.value.actualDamageTable, sourcePosition, damage)
|
||||
subscribers.forEach { it.onTileHealthUpdate(pos.x, pos.y, isBackground, health) }
|
||||
val tile = cell.tile(isBackground)
|
||||
|
||||
if (isBackground) {
|
||||
damagedTilesBackground.add(pos)
|
||||
val params = if (!damage.type.isPenetrating && tile.modifier != null && tile.modifier!!.value.breaksWithTile) {
|
||||
tile.material.value.actualDamageTable + tile.modifier!!.value.actualDamageTable
|
||||
} else {
|
||||
damagedTilesForeground.add(pos)
|
||||
tile.material.value.actualDamageTable
|
||||
}
|
||||
|
||||
return result
|
||||
health.damage(params, sourcePosition, damage)
|
||||
subscribers.forEach { it.onTileHealthUpdate(pos.x, pos.y, isBackground, health) }
|
||||
|
||||
if (health.isDead) {
|
||||
if (isBackground) {
|
||||
damagedTilesBackground.remove(pos)
|
||||
} else {
|
||||
damagedTilesForeground.remove(pos)
|
||||
}
|
||||
|
||||
val copyHealth = health.copy()
|
||||
val mCell = cell.mutable()
|
||||
val mTile = mCell.tile(isBackground)
|
||||
|
||||
mTile.material = BuiltinMetaMaterials.EMPTY
|
||||
mTile.color = TileColor.DEFAULT
|
||||
mTile.hueShift = 0f
|
||||
|
||||
if (tile.modifier != null && mTile.modifier!!.value.breaksWithTile) {
|
||||
mTile.modifier = null
|
||||
}
|
||||
|
||||
setCell(pos.x, pos.y, mCell.immutable())
|
||||
health.reset()
|
||||
return DamageResult(result, copyHealth, cell)
|
||||
} else {
|
||||
if (isBackground) {
|
||||
damagedTilesBackground.add(pos)
|
||||
} else {
|
||||
damagedTilesForeground.add(pos)
|
||||
}
|
||||
|
||||
return DamageResult(result, health, cell)
|
||||
}
|
||||
}
|
||||
|
||||
protected val damagedTilesForeground = ObjectArraySet<Vector2i>()
|
||||
|
@ -4,44 +4,36 @@ import ru.dbotthepony.kommons.io.readVector2d
|
||||
import ru.dbotthepony.kommons.io.readVector2f
|
||||
import ru.dbotthepony.kommons.io.writeStruct2d
|
||||
import ru.dbotthepony.kommons.io.writeStruct2f
|
||||
import ru.dbotthepony.kommons.util.getValue
|
||||
import ru.dbotthepony.kommons.util.setValue
|
||||
import ru.dbotthepony.kommons.vector.Vector2d
|
||||
import ru.dbotthepony.kstarbound.Starbound
|
||||
import ru.dbotthepony.kstarbound.defs.tile.TileDamage
|
||||
import ru.dbotthepony.kstarbound.defs.tile.TileDamageConfig
|
||||
import ru.dbotthepony.kstarbound.defs.tile.TileDamageType
|
||||
import ru.dbotthepony.kstarbound.math.Interpolator
|
||||
import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup
|
||||
import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean
|
||||
import ru.dbotthepony.kstarbound.network.syncher.networkedEnum
|
||||
import ru.dbotthepony.kstarbound.network.syncher.networkedFixedPoint
|
||||
import java.io.DataInputStream
|
||||
import java.io.DataOutputStream
|
||||
|
||||
class TileHealth() {
|
||||
constructor(stream: DataInputStream, isLegacy: Boolean) : this() {
|
||||
read(stream, isLegacy)
|
||||
}
|
||||
|
||||
var isHarvested: Boolean = false
|
||||
private set
|
||||
sealed class TileHealth() {
|
||||
var damageSource: Vector2d = Vector2d.ZERO
|
||||
private set
|
||||
var damageType: TileDamageType = TileDamageType.PROTECTED
|
||||
private set
|
||||
var damagePercent: Double = 0.0
|
||||
private set
|
||||
var damageEffectTimeFactor: Double = 0.0
|
||||
private set
|
||||
protected set
|
||||
abstract var damagePercent: Double
|
||||
protected set
|
||||
abstract var damageEffectTimeFactor: Double
|
||||
protected set
|
||||
abstract var isHarvested: Boolean
|
||||
protected set
|
||||
abstract var damageType: TileDamageType
|
||||
protected set
|
||||
var damageEffectPercentage: Double = 0.0
|
||||
private set
|
||||
protected set
|
||||
|
||||
fun copy(): TileHealth {
|
||||
val copy = TileHealth()
|
||||
|
||||
copy.isHarvested = isHarvested
|
||||
copy.damageSource = damageSource
|
||||
copy.damageType = damageType
|
||||
copy.damagePercent = damagePercent
|
||||
copy.damageEffectTimeFactor = damageEffectTimeFactor
|
||||
copy.damageEffectPercentage = damageEffectPercentage
|
||||
|
||||
return copy
|
||||
}
|
||||
abstract fun copy(): TileHealth
|
||||
|
||||
val isHealthy: Boolean
|
||||
get() = damagePercent <= 0.0
|
||||
@ -119,4 +111,54 @@ class TileHealth() {
|
||||
damageEffectPercentage = damageEffectTimeFactor.coerceIn(0.0, 1.0) * damagePercent
|
||||
return damagePercent > 0.0
|
||||
}
|
||||
|
||||
class Tile() : TileHealth() {
|
||||
constructor(stream: DataInputStream, isLegacy: Boolean) : this() {
|
||||
read(stream, isLegacy)
|
||||
}
|
||||
|
||||
override var damagePercent: Double = 0.0
|
||||
override var damageEffectTimeFactor: Double = 0.0
|
||||
override var isHarvested: Boolean = false
|
||||
override var damageType: TileDamageType = TileDamageType.PROTECTED
|
||||
|
||||
override fun copy(): Tile {
|
||||
val copy = Tile()
|
||||
|
||||
copy.isHarvested = isHarvested
|
||||
copy.damageSource = damageSource
|
||||
copy.damageType = damageType
|
||||
copy.damagePercent = damagePercent
|
||||
copy.damageEffectTimeFactor = damageEffectTimeFactor
|
||||
copy.damageEffectPercentage = damageEffectPercentage
|
||||
|
||||
return copy
|
||||
}
|
||||
}
|
||||
|
||||
class TileEntity() : TileHealth() {
|
||||
constructor(stream: DataInputStream, isLegacy: Boolean) : this() {
|
||||
read(stream, isLegacy)
|
||||
}
|
||||
|
||||
val networkGroup = NetworkedGroup()
|
||||
|
||||
override var damagePercent: Double by networkedFixedPoint(0.001).also { networkGroup.add(it); it.interpolator = Interpolator.Linear }
|
||||
override var damageEffectTimeFactor: Double by networkedFixedPoint(0.001).also { networkGroup.add(it); it.interpolator = Interpolator.Linear }
|
||||
override var isHarvested: Boolean by networkedBoolean().also { networkGroup.add(it) }
|
||||
override var damageType: TileDamageType by networkedEnum(TileDamageType.PROTECTED).also { networkGroup.add(it) }
|
||||
|
||||
override fun copy(): TileEntity {
|
||||
val copy = TileEntity()
|
||||
|
||||
copy.isHarvested = isHarvested
|
||||
copy.damageSource = damageSource
|
||||
copy.damageType = damageType
|
||||
copy.damagePercent = damagePercent
|
||||
copy.damageEffectTimeFactor = damageEffectTimeFactor
|
||||
copy.damageEffectPercentage = damageEffectPercentage
|
||||
|
||||
return copy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,15 +5,16 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
|
||||
import it.unimi.dsi.fastutil.ints.IntArraySet
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArraySet
|
||||
import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import ru.dbotthepony.kommons.arrays.Object2DArray
|
||||
import ru.dbotthepony.kommons.collect.filterNotNull
|
||||
import ru.dbotthepony.kommons.util.IStruct2d
|
||||
import ru.dbotthepony.kommons.util.IStruct2i
|
||||
import ru.dbotthepony.kommons.util.AABB
|
||||
import ru.dbotthepony.kommons.util.AABBi
|
||||
import ru.dbotthepony.kommons.util.MailboxExecutorService
|
||||
import ru.dbotthepony.kommons.vector.Vector2d
|
||||
import ru.dbotthepony.kommons.vector.Vector2i
|
||||
import ru.dbotthepony.kstarbound.Starbound
|
||||
import ru.dbotthepony.kstarbound.defs.world.WorldStructure
|
||||
import ru.dbotthepony.kstarbound.defs.world.WorldTemplate
|
||||
@ -22,12 +23,13 @@ import ru.dbotthepony.kstarbound.network.IPacket
|
||||
import ru.dbotthepony.kstarbound.network.packets.clientbound.SetPlayerStartPacket
|
||||
import ru.dbotthepony.kstarbound.util.ExceptionLogger
|
||||
import ru.dbotthepony.kstarbound.util.ParallelPerform
|
||||
import ru.dbotthepony.kstarbound.util.random.random
|
||||
import ru.dbotthepony.kstarbound.world.api.ICellAccess
|
||||
import ru.dbotthepony.kstarbound.world.api.AbstractCell
|
||||
import ru.dbotthepony.kstarbound.world.api.TileView
|
||||
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
|
||||
import ru.dbotthepony.kstarbound.world.entities.DynamicEntity
|
||||
import ru.dbotthepony.kstarbound.world.entities.TileEntity
|
||||
import ru.dbotthepony.kstarbound.world.entities.tile.TileEntity
|
||||
import ru.dbotthepony.kstarbound.world.physics.CollisionPoly
|
||||
import ru.dbotthepony.kstarbound.world.physics.CollisionType
|
||||
import ru.dbotthepony.kstarbound.world.physics.Poly
|
||||
@ -208,7 +210,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
|
||||
|
||||
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)
|
||||
abstract val isRemote: Boolean
|
||||
|
||||
@ -279,6 +281,14 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
|
||||
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> {
|
||||
val result = ArrayList<CollisionPoly>()
|
||||
val tiles = aabb.encasingIntAABB()
|
||||
|
@ -25,12 +25,7 @@ sealed class AbstractCell {
|
||||
return LegacyNetworkCellState(background.toLegacyNet(), foreground.toLegacyNet(), foreground.material.value.collisionKind, biome, envBiome, liquid.toLegacyNet(), dungeonId)
|
||||
}
|
||||
|
||||
fun tile(background: Boolean): AbstractTileState {
|
||||
if (background)
|
||||
return this.background
|
||||
else
|
||||
return this.foreground
|
||||
}
|
||||
abstract fun tile(background: Boolean): AbstractTileState
|
||||
|
||||
fun write(stream: DataOutputStream) {
|
||||
foreground.write(stream)
|
||||
|
@ -22,6 +22,13 @@ data class ImmutableCell(
|
||||
return legacyNet
|
||||
}
|
||||
|
||||
override fun tile(background: Boolean): ImmutableTileState {
|
||||
if (background)
|
||||
return this.background
|
||||
else
|
||||
return this.foreground
|
||||
}
|
||||
|
||||
override fun mutable(): MutableCell {
|
||||
return MutableCell(foreground.mutable(), background.mutable(), liquid.mutable(), dungeonId, biome, envBiome, isIndestructible)
|
||||
}
|
||||
|
@ -28,6 +28,13 @@ data class MutableCell(
|
||||
return this
|
||||
}
|
||||
|
||||
override fun tile(background: Boolean): MutableTileState {
|
||||
if (background)
|
||||
return this.background
|
||||
else
|
||||
return this.foreground
|
||||
}
|
||||
|
||||
override fun immutable(): ImmutableCell {
|
||||
return POOL.intern(ImmutableCell(foreground.immutable(), background.immutable(), liquid.immutable(), dungeonId, biome, envBiome, isIndestructible))
|
||||
}
|
||||
|
@ -1,17 +1,24 @@
|
||||
package ru.dbotthepony.kstarbound.world.entities
|
||||
|
||||
import com.google.gson.JsonArray
|
||||
import com.google.gson.JsonElement
|
||||
import it.unimi.dsi.fastutil.bytes.ByteArrayList
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import ru.dbotthepony.kommons.io.koptional
|
||||
import ru.dbotthepony.kommons.util.KOptional
|
||||
import ru.dbotthepony.kommons.util.MailboxExecutorService
|
||||
import ru.dbotthepony.kommons.vector.Vector2d
|
||||
import ru.dbotthepony.kstarbound.Starbound
|
||||
import ru.dbotthepony.kstarbound.client.StarboundClient
|
||||
import ru.dbotthepony.kstarbound.client.render.LayeredRenderer
|
||||
import ru.dbotthepony.kstarbound.client.world.ClientWorld
|
||||
import ru.dbotthepony.kstarbound.defs.EntityType
|
||||
import ru.dbotthepony.kstarbound.defs.JsonDriven
|
||||
import ru.dbotthepony.kstarbound.network.packets.EntityDestroyPacket
|
||||
import ru.dbotthepony.kstarbound.network.syncher.InternedStringCodec
|
||||
import ru.dbotthepony.kstarbound.network.syncher.MasterElement
|
||||
import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup
|
||||
import ru.dbotthepony.kstarbound.network.syncher.networkedData
|
||||
import ru.dbotthepony.kstarbound.server.world.ServerWorld
|
||||
import ru.dbotthepony.kstarbound.world.LightCalculator
|
||||
import ru.dbotthepony.kstarbound.world.SpatialIndex
|
||||
@ -53,6 +60,9 @@ abstract class AbstractEntity(path: String) : JsonDriven(path), Comparable<Abstr
|
||||
val world: 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
|
||||
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
|
||||
* entities in a single world.
|
||||
*/
|
||||
var uniqueID: String? = null
|
||||
protected set
|
||||
val uniqueID = networkedData(KOptional(), InternedStringCodec.koptional())
|
||||
|
||||
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<*, *>) {
|
||||
if (innerWorld != null)
|
||||
throw IllegalStateException("Already spawned (in world $innerWorld)")
|
||||
@ -130,30 +143,14 @@ abstract class AbstractEntity(path: String) : JsonDriven(path), Comparable<Abstr
|
||||
|
||||
var isRemote: Boolean = false
|
||||
|
||||
fun tick() {
|
||||
tickShared()
|
||||
|
||||
if (isRemote) {
|
||||
tickRemote()
|
||||
} else {
|
||||
tickLocal()
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun tickShared() {
|
||||
open fun tick() {
|
||||
mailbox.executeQueuedTasks()
|
||||
}
|
||||
|
||||
protected open fun tickRemote() {
|
||||
if (networkGroup.upstream.isInterpolating) {
|
||||
networkGroup.upstream.tickInterpolation(Starbound.TIMESTEP)
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun tickLocal() {
|
||||
|
||||
}
|
||||
|
||||
open fun render(client: StarboundClient, layers: LayeredRenderer) {
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -1,30 +1,27 @@
|
||||
package ru.dbotthepony.kstarbound.world.entities
|
||||
|
||||
import com.google.gson.JsonObject
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectAVLTreeMap
|
||||
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import ru.dbotthepony.kommons.io.BinaryStringCodec
|
||||
import ru.dbotthepony.kommons.gson.contains
|
||||
import ru.dbotthepony.kommons.gson.getArray
|
||||
import ru.dbotthepony.kommons.gson.set
|
||||
import ru.dbotthepony.kommons.io.IntValueCodec
|
||||
import ru.dbotthepony.kommons.io.map
|
||||
import ru.dbotthepony.kommons.io.readKOptional
|
||||
import ru.dbotthepony.kommons.io.writeBinaryString
|
||||
import ru.dbotthepony.kommons.io.writeKOptional
|
||||
import ru.dbotthepony.kommons.matrix.Matrix3f
|
||||
import ru.dbotthepony.kommons.util.AABB
|
||||
import ru.dbotthepony.kommons.util.KOptional
|
||||
import ru.dbotthepony.kommons.util.getValue
|
||||
import ru.dbotthepony.kommons.util.setValue
|
||||
import ru.dbotthepony.kommons.vector.Vector2d
|
||||
import ru.dbotthepony.kstarbound.Starbound
|
||||
import ru.dbotthepony.kstarbound.defs.animation.AnimatedPartsDefinition
|
||||
import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition
|
||||
import ru.dbotthepony.kstarbound.defs.animation.ParticleConfig
|
||||
import ru.dbotthepony.kstarbound.defs.animation.ParticleFactory
|
||||
import ru.dbotthepony.kstarbound.fromJson
|
||||
import ru.dbotthepony.kstarbound.io.readInternedString
|
||||
import ru.dbotthepony.kstarbound.json.mergeJson
|
||||
import ru.dbotthepony.kstarbound.math.Interpolator
|
||||
import ru.dbotthepony.kstarbound.math.PeriodicFunction
|
||||
import ru.dbotthepony.kstarbound.network.syncher.AABBCodecLegacy
|
||||
import ru.dbotthepony.kstarbound.network.syncher.AABBCodecNative
|
||||
import ru.dbotthepony.kstarbound.math.approachAngle
|
||||
import ru.dbotthepony.kstarbound.network.syncher.InternedStringCodec
|
||||
import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup
|
||||
import ru.dbotthepony.kstarbound.network.syncher.NetworkedElement
|
||||
@ -33,7 +30,6 @@ import ru.dbotthepony.kstarbound.network.syncher.NetworkedSignal
|
||||
import ru.dbotthepony.kstarbound.network.syncher.networkedAABBNullable
|
||||
import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean
|
||||
import ru.dbotthepony.kstarbound.network.syncher.networkedColor
|
||||
import ru.dbotthepony.kstarbound.network.syncher.networkedData
|
||||
import ru.dbotthepony.kstarbound.network.syncher.networkedEventCounter
|
||||
import ru.dbotthepony.kstarbound.network.syncher.networkedFixedPoint
|
||||
import ru.dbotthepony.kstarbound.network.syncher.networkedFloat
|
||||
@ -42,17 +38,53 @@ import ru.dbotthepony.kstarbound.network.syncher.networkedPointer
|
||||
import ru.dbotthepony.kstarbound.network.syncher.networkedSignedInt
|
||||
import ru.dbotthepony.kstarbound.network.syncher.networkedString
|
||||
import ru.dbotthepony.kstarbound.network.syncher.networkedUnsignedInt
|
||||
import java.io.DataInputStream
|
||||
import java.io.DataOutputStream
|
||||
import ru.dbotthepony.kstarbound.util.random.random
|
||||
import ru.dbotthepony.kstarbound.world.positiveModulo
|
||||
import java.util.Collections
|
||||
import java.util.function.Consumer
|
||||
import kotlin.math.atan2
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.math.sin
|
||||
import kotlin.math.sqrt
|
||||
|
||||
// NetworkedAnimator + AnimatedPartSet combined into one class
|
||||
|
||||
// AnimatedPartSet original docs:
|
||||
// Defines a "animated" data set constructed in such a way that it is very
|
||||
// useful for doing generic animations with lots of additional animation data.
|
||||
// It is made up of two concepts, "states" and "parts".
|
||||
//
|
||||
// States:
|
||||
//
|
||||
// There are N "state types" defined, which each defines a set of mutually
|
||||
// exclusive states that each "state type" can be in. For example, one state
|
||||
// type might be "movement", and the "movement" states might be "idle", "walk",
|
||||
// and "run. Another state type might be "attack" which could have as its
|
||||
// states "idle", and "melee". Each state type will have exactly one currently
|
||||
// active state, so this class may, for example, be in the total state of
|
||||
// "movement:idle" and "attack:melee". Each state within each state type is
|
||||
// animated, so that over time the state frame increases and may loop around,
|
||||
// or transition into another state so that that state type without interaction
|
||||
// may go from "melee" to "idle" when the "melee" state animation is finished.
|
||||
// This is defined by the individual state config in the configuration passed
|
||||
// into the constructor.
|
||||
//
|
||||
// Parts:
|
||||
//
|
||||
// Each instance of this class also can have N "Parts" defined, which are
|
||||
// groups of properties that "listen" to active states. Each part can "listen"
|
||||
// to one or more state types, and the first matching state x state type pair
|
||||
// (in order of state type priority which is specified in the config) is
|
||||
// chosen, and the properties from that state type and state are merged into
|
||||
// the part to produce the final active part information. Rather than having a
|
||||
// single image or image set for each part, since this class is intended to be
|
||||
// as generic as possible, all of this data is assumed to be queried from the
|
||||
// part properties, so that things such as image data as well as other things
|
||||
// like damage or collision polys can be stored along with the animation
|
||||
// frames, the part state, the base part, whichever is most applicable.
|
||||
class Animator() {
|
||||
class Light {
|
||||
private class Light {
|
||||
private val elements = ArrayList<NetworkedElement>()
|
||||
|
||||
fun addTo(group: NetworkedGroup) {
|
||||
@ -76,7 +108,7 @@ class Animator() {
|
||||
var beamAmbience: Float = 0f
|
||||
}
|
||||
|
||||
enum class SoundSignal {
|
||||
private enum class SoundSignal {
|
||||
PLAY, STOP_ALL;
|
||||
|
||||
companion object {
|
||||
@ -84,7 +116,7 @@ class Animator() {
|
||||
}
|
||||
}
|
||||
|
||||
class Sound {
|
||||
private class Sound {
|
||||
private val elements = ArrayList<NetworkedElement>()
|
||||
|
||||
fun addTo(group: NetworkedGroup) {
|
||||
@ -104,25 +136,181 @@ class Animator() {
|
||||
val signals = NetworkedSignal(SoundSignal.CODEC).also { elements.add(it) }
|
||||
}
|
||||
|
||||
class Effect(val type: String, val time: Double, val directives: String) {
|
||||
private class Effect(val type: String, val time: Double, val directives: String) {
|
||||
val enabled = networkedBoolean()
|
||||
var timer: Double = 0.0
|
||||
|
||||
fun tick(delta: Double) {
|
||||
if (timer <= 0.0)
|
||||
timer = time
|
||||
else
|
||||
timer -= delta
|
||||
}
|
||||
}
|
||||
|
||||
class StateInfo {
|
||||
val stateIndex = networkedPointer()
|
||||
private class StateType(config: AnimatedPartsDefinition.StateType) {
|
||||
// NetworkedAnimator
|
||||
private var noPropagate = false
|
||||
val stateIndex = networkedPointer(-1L)
|
||||
val startedEvent = networkedEventCounter()
|
||||
|
||||
// AnimatedPartSet
|
||||
var enabled = config.enabled
|
||||
var activeStateDirty = true
|
||||
val priority = config.priority
|
||||
val stateTypeProperties = config.properties
|
||||
val default: String
|
||||
var activeProperties = JsonObject()
|
||||
private set
|
||||
|
||||
// sorted by key
|
||||
// Basically, this is definition of each separate state
|
||||
val states = config.states
|
||||
|
||||
var timer = 0.0
|
||||
var frame = 0
|
||||
private set(value) {
|
||||
if (field != value) {
|
||||
field = value
|
||||
frameChanged = true
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
if (states.isNotEmpty() && config.default.isBlank())
|
||||
default = states.keys.first()
|
||||
else
|
||||
default = config.default
|
||||
}
|
||||
|
||||
private var activeStateChanged = false
|
||||
private var frameChanged = false
|
||||
|
||||
var activeState: AnimatedPartsDefinition.StateType.State? = null
|
||||
set(value) {
|
||||
activeStateChanged = true
|
||||
|
||||
if (value == null) {
|
||||
if (!noPropagate) {
|
||||
try {
|
||||
noPropagate = true
|
||||
stateIndex.accept(-1L)
|
||||
} finally {
|
||||
noPropagate = false
|
||||
}
|
||||
}
|
||||
|
||||
field = null
|
||||
} else {
|
||||
if (!noPropagate) {
|
||||
try {
|
||||
noPropagate = true
|
||||
stateIndex.accept(value.index.toLong())
|
||||
} finally {
|
||||
noPropagate = false
|
||||
}
|
||||
}
|
||||
|
||||
field = value
|
||||
}
|
||||
}
|
||||
|
||||
fun set(state: String, alwaysStart: Boolean): Boolean {
|
||||
val getState = states[state] ?: return false
|
||||
|
||||
if (activeState != getState || alwaysStart) {
|
||||
activeState = getState
|
||||
timer = 0.0
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
init {
|
||||
if (default in states) {
|
||||
activeState = states[default]!!
|
||||
stateIndex.accept(activeState!!.index.toLong())
|
||||
}
|
||||
|
||||
stateIndex.addListener(Consumer {
|
||||
if (noPropagate) return@Consumer
|
||||
|
||||
if (it == -1L) {
|
||||
try {
|
||||
noPropagate = true
|
||||
activeState = null
|
||||
} finally {
|
||||
noPropagate = false
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
noPropagate = true
|
||||
set(states.keys.elementAtOrNull(it.toInt()) ?: throw IllegalArgumentException("Unknown animation state $it!"), true)
|
||||
} finally {
|
||||
noPropagate = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun tick(delta: Double) {
|
||||
var activeState = activeState ?: return
|
||||
timer += delta
|
||||
|
||||
if (timer > activeState.cycle) {
|
||||
when (activeState.mode) {
|
||||
AnimatedPartsDefinition.AnimationMode.END -> timer = activeState.cycle
|
||||
|
||||
AnimatedPartsDefinition.AnimationMode.TRANSITION -> {
|
||||
activeState = states[activeState.transition]!! // validity of 'transition' is checked during json load
|
||||
this.activeState = activeState
|
||||
timer = 0.0
|
||||
}
|
||||
|
||||
AnimatedPartsDefinition.AnimationMode.LOOP -> timer %= activeState.cycle
|
||||
}
|
||||
}
|
||||
|
||||
frame = (timer / activeState.cycle * activeState.frames).toInt().coerceIn(0, activeState.frames - 1)
|
||||
|
||||
if (activeStateChanged || frameChanged) {
|
||||
activeProperties = mergeJson(stateTypeProperties.deepCopy(), activeState.properties)
|
||||
frameChanged = false
|
||||
activeStateChanged = false
|
||||
|
||||
for ((key, values) in activeState.frameProperties) {
|
||||
if (values.size() >= frame) {
|
||||
activeProperties[key] = values[frame].deepCopy()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RotationGroup {
|
||||
private class Part(config: AnimatedPartsDefinition.Part) {
|
||||
val partProperties = config.properties
|
||||
var activePartDirty = true
|
||||
val partStates = config.partStates
|
||||
}
|
||||
|
||||
private class RotationGroup {
|
||||
var angularVelocity = 0.0
|
||||
var rotationCenter = Vector2d.ZERO
|
||||
val targetAngle = networkedFloat()
|
||||
var currentAngle = 0.0
|
||||
val immediateEvent = networkedEventCounter()
|
||||
|
||||
fun tick(delta: Double) {
|
||||
if (angularVelocity == 0.0) {
|
||||
currentAngle = targetAngle.get()
|
||||
} else {
|
||||
currentAngle = approachAngle(targetAngle.get(), currentAngle, angularVelocity * delta)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TransformationGroup {
|
||||
private class TransformationGroup {
|
||||
private val elements = ArrayList<NetworkedElement>()
|
||||
|
||||
fun addTo(group: NetworkedGroup) {
|
||||
@ -158,7 +346,7 @@ class Animator() {
|
||||
}
|
||||
}
|
||||
|
||||
class ParticleEmitter {
|
||||
private class ParticleEmitter {
|
||||
data class Config(val count: Int, val offset: Vector2d, val flip: Boolean, val factory: ParticleFactory)
|
||||
|
||||
private val elements = ArrayList<NetworkedElement>()
|
||||
@ -189,9 +377,6 @@ class Animator() {
|
||||
|
||||
private val elements = ArrayList<NetworkedElement>()
|
||||
|
||||
var animatedParts = AnimatedParts()
|
||||
private set
|
||||
|
||||
var processingDirectives by networkedString().also { elements.add(it) }
|
||||
var zoom by networkedFloat().also { elements.add(it) }
|
||||
var isFlipped by networkedBoolean().also { elements.add(it) }
|
||||
@ -199,9 +384,11 @@ class Animator() {
|
||||
var animationRate by networkedFloat(1.0).also { elements.add(it); it.interpolator = Interpolator.Linear }
|
||||
|
||||
private val globalTags = NetworkedMap(InternedStringCodec, InternedStringCodec)
|
||||
private val parts = Object2ObjectAVLTreeMap<String, Part>()
|
||||
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 transformationGroups = Object2ObjectAVLTreeMap<String, TransformationGroup>()
|
||||
private val particleEmitters = Object2ObjectAVLTreeMap<String, ParticleEmitter>()
|
||||
@ -209,14 +396,13 @@ class Animator() {
|
||||
private val sounds = Object2ObjectAVLTreeMap<String, Sound>()
|
||||
private val effects = Object2ObjectAVLTreeMap<String, Effect>()
|
||||
|
||||
private val random = random()
|
||||
|
||||
init {
|
||||
setupNetworkElements()
|
||||
}
|
||||
|
||||
constructor(config: AnimationDefinition) : this() {
|
||||
if (config.animatedParts != null)
|
||||
animatedParts = AnimatedParts(config.animatedParts)
|
||||
|
||||
for ((k, v) in config.globalTagDefaults) {
|
||||
globalTags[k] = v
|
||||
}
|
||||
@ -308,11 +494,17 @@ class Animator() {
|
||||
effects[k] = Effect(v.type, v.time, v.directives)
|
||||
}
|
||||
|
||||
for (k in animatedParts.stateTypes()) {
|
||||
stateInfo[k] = StateInfo()
|
||||
if (config.animatedParts != null) {
|
||||
for ((k, v) in config.animatedParts.stateTypes.entries.sortedWith { o1, o2 -> o2.value.priority.compareTo(o1.value.priority) }) {
|
||||
stateTypes[k] = StateType(v)
|
||||
}
|
||||
|
||||
for ((k, v) in config.animatedParts.parts) {
|
||||
parts[k] = Part(v)
|
||||
}
|
||||
}
|
||||
|
||||
for (k in animatedParts.parts()) {
|
||||
for (k in parts.keys) {
|
||||
partTags.computeIfAbsent(k) { NetworkedMap(InternedStringCodec, InternedStringCodec) }
|
||||
}
|
||||
|
||||
@ -345,13 +537,16 @@ class Animator() {
|
||||
networkGroup.add(globalTags)
|
||||
|
||||
// animated part set
|
||||
for (v in animatedParts.parts()) {
|
||||
for (v in parts.keys) {
|
||||
networkGroup.add(partTags[v] ?: throw RuntimeException("Missing animated part $v!"))
|
||||
}
|
||||
|
||||
for (v in stateInfo.values) {
|
||||
networkGroup.add(v.stateIndex)
|
||||
networkGroup.add(v.startedEvent)
|
||||
stateTypes.entries.stream()
|
||||
.sorted { o1, o2 -> o1.key.compareTo(o2.key) }
|
||||
.map { it.value }
|
||||
.forEach {
|
||||
networkGroup.add(it.stateIndex)
|
||||
networkGroup.add(it.startedEvent)
|
||||
}
|
||||
|
||||
for (v in transformationGroups.values) {
|
||||
@ -380,6 +575,64 @@ class Animator() {
|
||||
}
|
||||
}
|
||||
|
||||
fun setActiveState(type: String, state: String, alwaysStart: Boolean = false): Boolean {
|
||||
val getType = stateTypes[type] ?: return false
|
||||
val getState = getType.states[state] ?: return false
|
||||
|
||||
if (getType.activeState != getState || alwaysStart) {
|
||||
getType.timer = 0.0
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// TODO: Dynamic target
|
||||
@Suppress("Name_Shadowing")
|
||||
fun tick(delta: Double = Starbound.TIMESTEP) {
|
||||
val delta = delta * animationRate
|
||||
|
||||
for (state in stateTypes.values) {
|
||||
state.tick(delta)
|
||||
|
||||
if ("lightsOn" in state.activeProperties) {
|
||||
for (name in state.activeProperties.getArray("lightsOn")) {
|
||||
lights[name.asString]?.active = true
|
||||
}
|
||||
}
|
||||
|
||||
if ("lightsOff" in state.activeProperties) {
|
||||
for (name in state.activeProperties.getArray("lightsOff")) {
|
||||
lights[name.asString]?.active = false
|
||||
}
|
||||
}
|
||||
|
||||
if ("particleEmittersOn" in state.activeProperties) {
|
||||
for (name in state.activeProperties.getArray("particleEmittersOn")) {
|
||||
particleEmitters[name.asString]?.active = true
|
||||
}
|
||||
}
|
||||
|
||||
if ("particleEmittersOff" in state.activeProperties) {
|
||||
for (name in state.activeProperties.getArray("particleEmittersOff")) {
|
||||
particleEmitters[name.asString]?.active = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (rotationGroup in rotationGroups.values) {
|
||||
rotationGroup.tick(delta)
|
||||
}
|
||||
|
||||
for (light in lights.values) {
|
||||
light.flicker?.update(delta, random)
|
||||
}
|
||||
|
||||
for (effect in effects.values) {
|
||||
effect.tick(delta)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
// lame
|
||||
fun load(path: String): Animator {
|
||||
|
@ -27,10 +27,10 @@ abstract class DynamicEntity(path: String) : AbstractEntity(path) {
|
||||
movement.updateFixtures()
|
||||
}
|
||||
|
||||
override fun tickRemote() {
|
||||
super.tickRemote()
|
||||
override fun tick() {
|
||||
super.tick()
|
||||
|
||||
if (networkGroup.upstream.isInterpolating) {
|
||||
if (isRemote && networkGroup.upstream.isInterpolating) {
|
||||
movement.updateFixtures()
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,11 @@
|
||||
package ru.dbotthepony.kstarbound.world.entities.player
|
||||
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonObject
|
||||
import it.unimi.dsi.fastutil.bytes.ByteArrayList
|
||||
import ru.dbotthepony.kommons.io.writeBinaryString
|
||||
import ru.dbotthepony.kommons.util.AABB
|
||||
import ru.dbotthepony.kommons.util.KOptional
|
||||
import ru.dbotthepony.kommons.util.getValue
|
||||
import ru.dbotthepony.kommons.util.setValue
|
||||
import ru.dbotthepony.kommons.vector.Vector2d
|
||||
@ -14,6 +16,7 @@ import ru.dbotthepony.kstarbound.defs.actor.HumanoidData
|
||||
import ru.dbotthepony.kstarbound.defs.actor.HumanoidEmote
|
||||
import ru.dbotthepony.kstarbound.defs.actor.player.PlayerGamemode
|
||||
import ru.dbotthepony.kstarbound.io.readInternedString
|
||||
import ru.dbotthepony.kstarbound.json.JsonPath
|
||||
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
|
||||
import ru.dbotthepony.kstarbound.math.Interpolator
|
||||
import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup
|
||||
@ -52,7 +55,7 @@ class PlayerEntity() : HumanoidActorEntity("/") {
|
||||
}
|
||||
|
||||
constructor(data: DataInputStream, isLegacy: Boolean) : this() {
|
||||
uniqueID = data.readInternedString()
|
||||
uniqueID.accept(KOptional(data.readInternedString()))
|
||||
description = data.readInternedString()
|
||||
gamemode = PlayerGamemode.entries[if (isLegacy) data.readInt() else data.readUnsignedByte()]
|
||||
humanoidData = HumanoidData.read(data, isLegacy)
|
||||
@ -64,7 +67,12 @@ class PlayerEntity() : HumanoidActorEntity("/") {
|
||||
var gamemode = PlayerGamemode.CASUAL
|
||||
|
||||
override fun writeNetwork(stream: DataOutputStream, isLegacy: Boolean) {
|
||||
stream.writeBinaryString(uniqueID!!)
|
||||
uniqueID.get().ifPresent {
|
||||
stream.writeBinaryString(it)
|
||||
}.ifNotPresent {
|
||||
stream.writeBinaryString("")
|
||||
}
|
||||
|
||||
stream.writeBinaryString(description)
|
||||
if (isLegacy) stream.writeInt(gamemode.ordinal) else stream.writeByte(gamemode.ordinal)
|
||||
humanoidData.write(stream, isLegacy)
|
||||
@ -113,8 +121,8 @@ class PlayerEntity() : HumanoidActorEntity("/") {
|
||||
metaFixture = null
|
||||
}
|
||||
|
||||
override fun tickShared() {
|
||||
super.tickShared()
|
||||
override fun tick() {
|
||||
super.tick()
|
||||
|
||||
if (fixturesChangeset != movement.fixturesChangeset) {
|
||||
fixturesChangeset = movement.fixturesChangeset
|
||||
@ -128,10 +136,14 @@ class PlayerEntity() : HumanoidActorEntity("/") {
|
||||
override val isApplicableForUnloading: Boolean
|
||||
get() = false
|
||||
|
||||
override fun defs(): Collection<JsonObject> {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
var uuid: UUID by Delegates.notNull()
|
||||
private set
|
||||
|
||||
override fun lookupProperty(path: JsonPath, orElse: () -> JsonElement): JsonElement {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun setProperty0(key: JsonPath, value: JsonElement) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
|
@ -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) }
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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) {
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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) })
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user