TileEntities, WorldObject

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

View File

@ -8,12 +8,8 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectMap
import it.unimi.dsi.fastutil.ints.Int2ObjectMaps
import it.unimi.dsi.fastutil.ints.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
}

View File

@ -35,6 +35,7 @@ import ru.dbotthepony.kstarbound.defs.`object`.ObjectOrientation
import ru.dbotthepony.kstarbound.defs.actor.player.BlueprintLearnList
import ru.dbotthepony.kstarbound.defs.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>()

View 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

View File

@ -0,0 +1,41 @@
package ru.dbotthepony.kstarbound.collect
class RandomListIterator<E>(private val elements: MutableList<E>, index: Int = 0) : MutableListIterator<E> {
private var index = index - 1
override fun hasPrevious(): Boolean {
return this.index > 0
}
override fun nextIndex(): Int {
return this.index + 1
}
override fun previous(): E {
return elements[--this.index]
}
override fun previousIndex(): Int {
return (this.index - 1).coerceAtLeast(-1)
}
override fun add(element: E) {
elements.add(this.index++, element)
}
override fun hasNext(): Boolean {
return this.index < elements.size - 1
}
override fun next(): E {
return elements[++this.index]
}
override fun remove() {
elements.removeAt(this.index--)
}
override fun set(element: E) {
elements[this.index] = element
}
}

View File

@ -0,0 +1,177 @@
package ru.dbotthepony.kstarbound.collect
class RandomSubList<E>(private val elements: MutableList<E>, private val fromIndex: Int, private var toIndex: Int) : MutableList<E> {
override val size: Int
get() = toIndex - fromIndex
init {
check(size >= 0) { "Invalid sublist range: $fromIndex .. $toIndex" }
}
override fun contains(element: E): Boolean {
for (i in fromIndex until toIndex) {
if (elements[i] == element) {
return true
}
}
return false
}
override fun containsAll(elements: Collection<E>): Boolean {
return elements.all { contains(it) }
}
override fun get(index: Int): E {
if (index !in 0 until size)
throw IndexOutOfBoundsException(index.toString())
return elements[index + fromIndex]
}
override fun indexOf(element: E): Int {
for (i in fromIndex until toIndex) {
if (elements[i] == element) {
return i - fromIndex
}
}
return -1
}
override fun isEmpty(): Boolean {
return size == 0
}
override fun iterator(): MutableIterator<E> {
return listIterator()
}
override fun lastIndexOf(element: E): Int {
for (i in toIndex - 1 downTo fromIndex) {
if (elements[i] == element) {
return i - fromIndex
}
}
return -1
}
override fun add(element: E): Boolean {
elements.add(toIndex++, element)
return true
}
override fun add(index: Int, element: E) {
if (index !in 0 .. size)
throw IndexOutOfBoundsException(index.toString())
elements.add(index + fromIndex, element)
toIndex++
}
override fun addAll(index: Int, elements: Collection<E>): Boolean {
if (index !in 0 .. size)
throw IndexOutOfBoundsException(index.toString())
this.elements.addAll(index + fromIndex, elements)
toIndex += elements.size
return true
}
override fun addAll(elements: Collection<E>): Boolean {
return addAll(toIndex, elements)
}
override fun clear() {
for (i in fromIndex until toIndex) {
elements.removeAt(fromIndex)
}
toIndex = fromIndex
}
override fun listIterator(): MutableListIterator<E> {
return RandomListIterator(this)
}
override fun listIterator(index: Int): MutableListIterator<E> {
return RandomListIterator(this, index)
}
override fun remove(element: E): Boolean {
val indexOf = indexOf(element)
if (indexOf == -1)
return false
removeAt(indexOf)
return true
}
override fun removeAll(elements: Collection<E>): Boolean {
var any = false
elements.forEach { any = remove(it) || any }
return any
}
override fun removeAt(index: Int): E {
if (index !in 0 until size)
throw IndexOutOfBoundsException(index.toString())
val result = elements.removeAt(index + fromIndex)
toIndex--
return result
}
override fun retainAll(elements: Collection<E>): Boolean {
val itr = iterator()
var modified = false
for (v in itr) {
if (v !in elements) {
itr.remove()
modified = true
}
}
return modified
}
override fun set(index: Int, element: E): E {
if (index !in 0 until size)
throw IndexOutOfBoundsException(index.toString())
return elements.set(index + fromIndex, element)
}
override fun subList(fromIndex: Int, toIndex: Int): MutableList<E> {
return RandomSubList(this, fromIndex, toIndex)
}
override fun equals(other: Any?): Boolean {
if (other === this) return true
if (other !is List<*>) return false
val e1: ListIterator<E> = listIterator()
val e2 = other.listIterator()
while (e1.hasNext() && e2.hasNext()) {
val o1 = e1.next()
val o2 = e2.next()
if (!(if (o1 == null) o2 == null else o1 == o2)) return false
}
return !(e1.hasNext() || e2.hasNext())
}
override fun hashCode(): Int {
var hashCode = 1
for (e in this) hashCode = 31 * hashCode + e.hashCode()
return hashCode
}
override fun toString(): String {
return "[${joinToString()}]"
}
}

View File

@ -2,14 +2,42 @@ package ru.dbotthepony.kstarbound.defs
import com.google.common.collect.ImmutableList
import com.google.common.collect.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)
}
}

View File

@ -0,0 +1,44 @@
package ru.dbotthepony.kstarbound.defs
import com.google.gson.Gson
import com.google.gson.TypeAdapter
import com.google.gson.annotations.JsonAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kstarbound.io.readInternedString
import ru.dbotthepony.kstarbound.io.readNullableDouble
import ru.dbotthepony.kstarbound.io.writeNullable
import ru.dbotthepony.kstarbound.io.writeNullableDouble
import ru.dbotthepony.kstarbound.json.builder.FactoryAdapter
import java.io.DataInputStream
import java.io.DataOutputStream
@JsonAdapter(EphemeralStatusEffect.Adapter::class)
data class EphemeralStatusEffect(val effect: String, val duration: Double? = null) {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readInternedString(), stream.readNullableDouble())
class Adapter(gson: Gson) : TypeAdapter<EphemeralStatusEffect>() {
private val factory = FactoryAdapter.createFor(EphemeralStatusEffect::class, gson = gson)
override fun write(out: JsonWriter, value: EphemeralStatusEffect) {
if (value.duration == null)
out.value(value.effect)
else
factory.write(out, value)
}
override fun read(`in`: JsonReader): EphemeralStatusEffect {
if (`in`.peek() == JsonToken.STRING)
return EphemeralStatusEffect(`in`.nextString())
else
return factory.read(`in`)
}
}
fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeBinaryString(effect)
stream.writeNullableDouble(duration, isLegacy)
}
}

View File

@ -1,18 +1,18 @@
package ru.dbotthepony.kstarbound.defs
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
}
}
}

View File

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

View File

@ -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"),

View File

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

View File

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

View File

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

View File

@ -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,

View File

@ -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,

View File

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

View File

@ -0,0 +1,34 @@
package ru.dbotthepony.kstarbound.defs.quest
import com.google.common.collect.ImmutableList
import ru.dbotthepony.kommons.io.readCollection
import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.io.writeCollection
import ru.dbotthepony.kstarbound.io.readInternedString
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.network.syncher.legacyCodec
import ru.dbotthepony.kstarbound.network.syncher.nativeCodec
import java.io.DataInputStream
import java.io.DataOutputStream
@JsonFactory
data class QuestArcDescriptor(
val quests: ImmutableList<QuestDescriptor> = ImmutableList.of(),
val stagehandUniqueId: String? = null,
) {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(
ImmutableList.copyOf(stream.readCollection { QuestDescriptor(this, isLegacy) }),
if (stream.readBoolean()) stream.readInternedString() else null
)
fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeCollection(quests) { it.write(this, isLegacy) }
stream.writeBoolean(stagehandUniqueId != null)
if (stagehandUniqueId != null) stream.writeBinaryString(stagehandUniqueId)
}
companion object {
val CODEC = nativeCodec(::QuestArcDescriptor, QuestArcDescriptor::write)
val LEGACY_CODEC = legacyCodec(::QuestArcDescriptor, QuestArcDescriptor::write)
}
}

View File

@ -0,0 +1,44 @@
package ru.dbotthepony.kstarbound.defs.quest
import com.google.common.collect.ImmutableMap
import com.google.gson.JsonObject
import ru.dbotthepony.kommons.io.readMap
import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.io.writeMap
import ru.dbotthepony.kstarbound.io.readInternedString
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.network.syncher.legacyCodec
import ru.dbotthepony.kstarbound.network.syncher.nativeCodec
import java.io.DataInputStream
import java.io.DataOutputStream
@JsonFactory
data class QuestDescriptor(
val questId: String,
val templateId: String = questId,
val parameters: ImmutableMap<String, QuestParameter> = ImmutableMap.of(),
val seed: Long = makeSeed(),
) {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(
stream.readInternedString(),
stream.readInternedString(),
ImmutableMap.copyOf(stream.readMap({ readInternedString() }, { QuestParameter(this, isLegacy) })),
stream.readLong()
)
fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeBinaryString(questId)
stream.writeBinaryString(templateId)
stream.writeMap(parameters, { writeBinaryString(it) }, { it.write(this, isLegacy) })
stream.writeLong(seed)
}
companion object {
val CODEC = nativeCodec(::QuestDescriptor, QuestDescriptor::write)
val LEGACY_CODEC = legacyCodec(::QuestDescriptor, QuestDescriptor::write)
fun makeSeed(): Long {
return System.nanoTime().rotateLeft(27).xor(System.currentTimeMillis())
}
}
}

View File

@ -0,0 +1,325 @@
package ru.dbotthepony.kstarbound.defs.quest
import com.google.common.collect.ImmutableList
import com.google.gson.Gson
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.TypeAdapter
import com.google.gson.TypeAdapterFactory
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kommons.gson.value
import ru.dbotthepony.kommons.io.readCollection
import ru.dbotthepony.kommons.io.readVector2d
import ru.dbotthepony.kommons.io.readVector2f
import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.io.writeCollection
import ru.dbotthepony.kommons.io.writeStruct2d
import ru.dbotthepony.kommons.io.writeStruct2f
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.defs.actor.Gender
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import ru.dbotthepony.kstarbound.io.readInternedString
import ru.dbotthepony.kstarbound.json.builder.DispatchingAdapter
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.builder.JsonSingleton
import ru.dbotthepony.kstarbound.json.readJsonElement
import ru.dbotthepony.kstarbound.json.readJsonObject
import ru.dbotthepony.kstarbound.json.writeJsonElement
import ru.dbotthepony.kstarbound.json.writeJsonObject
import ru.dbotthepony.kstarbound.network.syncher.legacyCodec
import ru.dbotthepony.kstarbound.network.syncher.nativeCodec
import ru.dbotthepony.kstarbound.util.asStringOrNull
import ru.dbotthepony.kstarbound.util.coalesceNull
import ru.dbotthepony.kstarbound.world.UniversePos
import java.io.DataInputStream
import java.io.DataOutputStream
class QuestParameter(
val detail: Detail,
val name: String? = null,
val portrait: JsonElement? = null,
val indicator: String? = null
) {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(
readDetail(stream, isLegacy),
if (stream.readBoolean()) stream.readInternedString() else null,
if (stream.readBoolean()) stream.readJsonElement() else null,
if (stream.readBoolean()) stream.readInternedString() else null,
)
enum class Type(override val jsonName: String, val token: TypeToken<out Detail>) : IStringSerializable {
NO_DETAIL("noDetail", TypeToken.get(Empty::class.java)),
ITEM("item", TypeToken.get(Item::class.java)),
ITEM_TAG("itemTag", TypeToken.get(ItemTag::class.java)),
ITEM_LIST("itemList", TypeToken.get(ItemList::class.java)),
ENTITY("entity", TypeToken.get(Entity::class.java)),
LOCATION("location", TypeToken.get(Location::class.java)),
MONSTER_TYPE("monsterType", TypeToken.get(MonsterType::class.java)),
NPC_TYPE("npcType", TypeToken.get(NpcType::class.java)),
COORDINATE("coordinate", TypeToken.get(Coordinate::class.java)),
JSON("json", TypeToken.get(JsonData::class.java));
}
fun write(stream: DataOutputStream, isLegacy: Boolean) {
detail.write(stream, isLegacy)
stream.writeBoolean(name != null)
if (name != null) stream.writeBinaryString(name)
stream.writeBoolean(portrait != null)
if (portrait != null) stream.writeJsonElement(portrait)
stream.writeBoolean(indicator != null)
if (indicator != null) stream.writeBinaryString(indicator)
}
override fun hashCode(): Int {
return name.hashCode() * 31 * 31 + portrait.hashCode() * 31 + indicator.hashCode()
}
override fun equals(other: Any?): Boolean {
return this === other || other is QuestParameter && name == other.name && portrait == other.portrait && indicator == other.indicator
}
sealed class Detail {
abstract val type: Type
abstract fun write(stream: DataOutputStream, isLegacy: Boolean)
}
@JsonSingleton
object Empty : Detail() {
override val type: Type
get() = Type.NO_DETAIL
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeByte(type.ordinal)
}
}
// Item name - always one single item. QuestItem and QuestItemList are
// distinct due to how the surrounding text interacts with the parameter
// in the quest text. For a single item we might want to say "the <bandage>" or
// "any <bandage>", whereas the text for QuestItemList is always a list, e.g.
// "<1 bandage, 3 apple>."
@JsonFactory
data class Item(val item: ItemDescriptor) : Detail() {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(
if (isLegacy) ItemDescriptor(stream.readInternedString(), 1L, stream.readJsonElement() as JsonObject) else ItemDescriptor(stream))
override val type: Type
get() = Type.ITEM
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeByte(type.ordinal)
if (isLegacy) {
stream.writeBinaryString(item.name)
stream.writeJsonElement(item.parameters)
} else {
item.write(stream)
}
}
}
// An item itemTag, indicating a set of possible items
@JsonFactory
data class ItemTag(val tag: String) : Detail() {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readInternedString())
override val type: Type
get() = Type.ITEM_TAG
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeByte(type.ordinal)
stream.writeBinaryString(tag)
}
}
// A collection of items
@JsonFactory
data class ItemList(val items: ImmutableList<ItemDescriptor>) : Detail() {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(ImmutableList.copyOf(stream.readCollection { ItemDescriptor(this) }))
override val type: Type
get() = Type.ITEM_LIST
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeByte(type.ordinal)
stream.writeCollection(items) { it.write(this) }
}
}
// The uniqueId of a specific entity
@JsonFactory
data class Entity(val uniqueId: String? = null, val species: String? = null, val gender: Gender? = null) : Detail() {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(
if (stream.readBoolean()) stream.readInternedString() else null,
if (stream.readBoolean()) stream.readInternedString() else null,
if (stream.readBoolean()) Gender.entries[stream.readUnsignedByte()] else null,
)
override val type: Type
get() = Type.ENTITY
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeByte(type.ordinal)
stream.writeBoolean(uniqueId != null)
if (uniqueId != null) stream.writeBinaryString(uniqueId)
stream.writeBoolean(species != null)
if (species != null) stream.writeBinaryString(species)
stream.writeBoolean(gender != null)
if (gender != null) stream.writeByte(gender.ordinal)
}
}
// A location within the world, which could represent a spawn point or a dungeon
@JsonFactory
data class Location(val uniqueId: String? = null, val region: Vector2d) : Detail() {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(
if (stream.readBoolean()) stream.readInternedString() else null,
if (isLegacy) stream.readVector2f().toDoubleVector() else stream.readVector2d(),
)
override val type: Type
get() = Type.LOCATION
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeByte(type.ordinal)
stream.writeBoolean(uniqueId != null)
if (uniqueId != null)
stream.writeBinaryString(uniqueId)
if (isLegacy)
stream.writeStruct2f(region.toFloatVector())
else
stream.writeStruct2d(region)
}
}
@JsonFactory
data class MonsterType(val typeName: String, val parameters: JsonObject = JsonObject()) : Detail() {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(
stream.readInternedString(),
stream.readJsonObject(),
)
override val type: Type
get() = Type.MONSTER_TYPE
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeByte(type.ordinal)
stream.writeBinaryString(typeName)
stream.writeJsonObject(parameters)
}
}
@JsonFactory
data class NpcType(val species: String, val typeName: String, val parameters: JsonObject = JsonObject(), val seed: Long? = null) : Detail() {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(
stream.readInternedString(),
stream.readInternedString(),
stream.readJsonObject(),
if (stream.readBoolean()) stream.readLong() else null,
)
override val type: Type
get() = Type.NPC_TYPE
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeByte(type.ordinal)
stream.writeBinaryString(species)
stream.writeBinaryString(typeName)
stream.writeJsonObject(parameters)
stream.writeBoolean(seed != null)
if (seed != null) stream.writeLong(seed)
}
}
@JsonFactory
data class Coordinate(val coordinate: UniversePos) : Detail() {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(UniversePos(stream, isLegacy))
override val type: Type
get() = Type.COORDINATE
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeByte(type.ordinal)
coordinate.write(stream, isLegacy)
}
}
class JsonData(val json: JsonObject) : Detail() {
override val type: Type
get() = Type.JSON
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeJsonElement(json)
}
}
class Adapter(gson: Gson) : TypeAdapter<QuestParameter>() {
private val details = DETAIL_ADAPTER.create(gson, TypeToken.get(Detail::class.java))!!
private val objects = gson.getAdapter(JsonObject::class.java)
override fun write(out: JsonWriter, value: QuestParameter?) {
if (value == null)
out.nullValue()
else {
val base = if (value.detail is JsonData) {
value.detail.json.deepCopy()
} else {
details.toJsonTree(value.detail) as JsonObject
}
if (value.name != null) base["name"] = value.name
if (value.portrait != null) base["portrait"] = value.portrait
if (value.indicator != null) base["indicator"] = value.indicator
out.value(base)
}
}
override fun read(`in`: JsonReader): QuestParameter? {
if (`in`.consumeNull())
return null
val read = objects.read(`in`)
val detail = if (read["type"]?.asString == "json") {
JsonData(read)
} else {
details.fromJsonTree(read)
}
val name = read["name"]?.asStringOrNull
val portrait = read["portrait"]?.coalesceNull
val indicator = read["indicator"]?.asStringOrNull
return QuestParameter(detail, name, portrait, indicator)
}
}
companion object {
val CODEC = nativeCodec(::QuestParameter, QuestParameter::write)
val LEGACY_CODEC = legacyCodec(::QuestParameter, QuestParameter::write)
private val DETAIL_ADAPTER = DispatchingAdapter("type", { type }, { token }, Type.entries)
fun readDetail(stream: DataInputStream, isLegacy: Boolean): Detail {
return when (Type.entries[stream.readUnsignedByte()]) {
Type.NO_DETAIL -> Empty
Type.ITEM -> Item(stream, isLegacy)
Type.ITEM_TAG -> ItemTag(stream, isLegacy)
Type.ITEM_LIST -> ItemList(stream, isLegacy)
Type.ENTITY -> Entity(stream, isLegacy)
Type.LOCATION -> Location(stream, isLegacy)
Type.MONSTER_TYPE -> MonsterType(stream, isLegacy)
Type.NPC_TYPE -> NpcType(stream, isLegacy)
Type.COORDINATE -> Coordinate(stream, isLegacy)
Type.JSON -> JsonData(stream.readJsonElement() as JsonObject)
}
}
}
}

View File

@ -1,8 +1,7 @@
package ru.dbotthepony.kstarbound.defs.tile
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()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,55 @@
package ru.dbotthepony.kstarbound.json
import com.google.gson.Gson
import com.google.gson.TypeAdapter
import com.google.gson.TypeAdapterFactory
import com.google.gson.annotations.JsonAdapter
import com.google.gson.reflect.TypeToken
object JsonAdapterTypeFactory : TypeAdapterFactory {
private fun <T : Any?> wrap(input: Any, annotation: JsonAdapter): TypeAdapter<T> {
input as TypeAdapter<T>
if (annotation.nullSafe) {
return NullSafeTypeAdapter(input)
} else {
return input
}
}
override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
val annotation = type.rawType.getAnnotation(JsonAdapter::class.java) ?: return null
val constructor0 = try {
annotation.value.java.getDeclaredConstructor(Gson::class.java, TypeToken::class.java)
} catch (err: NoSuchMethodException) {
null
}
if (constructor0 != null) {
return wrap(constructor0.newInstance(gson, type), annotation)
}
val constructor1 = try {
annotation.value.java.getDeclaredConstructor(Gson::class.java)
} catch (err: NoSuchMethodException) {
null
}
if (constructor1 != null) {
return wrap(constructor1.newInstance(gson), annotation)
}
val constructor2 = try {
annotation.value.java.getDeclaredConstructor()
} catch (err: NoSuchMethodException) {
null
}
if (constructor2 != null) {
return wrap(constructor2.newInstance(), annotation)
}
throw RuntimeException("${type.rawType} has @JsonAdapter annotation, but it doesn't reference legal class ${annotation.value}")
}
}

View File

@ -0,0 +1,19 @@
package ru.dbotthepony.kstarbound.json
import com.google.gson.Gson
import com.google.gson.TypeAdapter
import com.google.gson.TypeAdapterFactory
import com.google.gson.reflect.TypeToken
import ru.dbotthepony.kstarbound.json.builder.JsonImplementation
object JsonImplementationTypeFactory : TypeAdapterFactory {
override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
val delegate = type.rawType.getAnnotation(JsonImplementation::class.java)
if (delegate != null) {
return gson.getAdapter(delegate.implementingClass.java) as TypeAdapter<T>?
}
return null
}
}

View File

@ -0,0 +1,260 @@
package ru.dbotthepony.kstarbound.json
import com.google.common.collect.ImmutableList
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.objects.ObjectArrayList
import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.util.KOptional
fun jsonQuery(path: String) = JsonPath.query(path)
fun jsonPointer(path: String) = JsonPath.pointer(path)
class JsonPath private constructor(val pieces: ImmutableList<Piece>) {
constructor(path: String) : this(ImmutableList.of(Piece(path)))
enum class Hint {
OBJECT, ARRAY;
}
data class Piece(val value: String, val hint: Hint? = null) {
val int by lazy { value.toIntOrNull() }
}
class TraversalException(message: String) : Exception(message)
override fun equals(other: Any?): Boolean {
return this === other || other is JsonPath && pieces == other.pieces
}
override fun hashCode(): Int {
return pieces.hashCode()
}
override fun toString(): String {
if (pieces.isEmpty())
return "<empty json path>"
else
return "/${pieces.joinToString("/") { it.value }}"
}
fun startsWith(name: String): Boolean {
if (isEmpty)
return true
return pieces[0].value == name
}
val isEmpty: Boolean
get() = pieces.isEmpty()
val hasParent: Boolean
get() = pieces.isNotEmpty()
fun parent(): JsonPath {
require(hasParent) { "$this has no parent" }
val build = ObjectArrayList(pieces)
build.removeLast()
return JsonPath(ImmutableList.copyOf(build))
}
fun child(key: String): JsonPath {
val build = ObjectArrayList(pieces)
build.add(Piece(key))
return JsonPath(ImmutableList.copyOf(build))
}
private fun reconstructPath(at: Int): String {
if (at == 0)
return "<root>"
else
return "/${pieces.joinToString("/", limit = at, truncated = "") { it.value }}"
}
/**
* Attempts to find given element along path, if can't, throws [TraversalException]
*/
fun get(element: JsonElement): JsonElement {
if (isEmpty) {
return element
}
var current = element
for ((i, piece) in pieces.withIndex()) {
if (current is JsonObject) {
if (piece.value !in current) {
throw TraversalException("Path at ${reconstructPath(i)} points at non-existing element")
}
current = current[piece.value]!!
if (i != pieces.size - 1 && current !is JsonObject && current !is JsonArray) {
throw TraversalException("Path at ${reconstructPath(i)} expects to get index-able element from underlying object (for ${pieces[i + 1]}), but there is $current")
}
} else if (current is JsonArray) {
if (piece.value == "-") {
throw TraversalException("Path at ${reconstructPath(i)} points at non-existent index")
}
val key = piece.int
if (key == null) {
throw TraversalException("Path at ${reconstructPath(i)} can not index an array")
} else if (key < 0) {
throw TraversalException("Path at ${reconstructPath(i)} points at pseudo index")
}
try {
current = current[key]
} catch (err: IndexOutOfBoundsException) {
throw TraversalException("Path at ${reconstructPath(i)} points at non-existing index")
}
if (i != pieces.size - 1 && current !is JsonObject && current !is JsonArray) {
throw TraversalException("Path at ${reconstructPath(i)} expects to get index-able element from underlying array (for ${pieces[i + 1]}), but there is $current")
}
} else {
throw TraversalException("Path at ${reconstructPath(i)} can not index $current")
}
}
return current
}
/**
* Attempts to find element along path, if can't, returns null
*/
fun find(element: JsonElement): JsonElement? {
if (isEmpty) {
return element
}
var current = element
for (piece in pieces) {
if (current is JsonObject) {
if (piece.value !in current) {
return null
}
current = current[piece.value]
} else if (current is JsonArray) {
if (piece.value == "-") {
return null
}
val key = piece.int
if (key == null || key < 0 || key >= current.size()) {
return null
}
current = current[key]
} else {
return null
}
}
return current
}
fun get(element: JsonElement, orElse: () -> JsonElement): JsonElement {
return find(element) ?: orElse.invoke()
}
fun get(element: JsonElement, orElse: JsonElement): JsonElement {
return find(element) ?: orElse
}
companion object {
val EMPTY = JsonPath(ImmutableList.of())
// https://datatracker.ietf.org/doc/html/rfc6901
@JvmStatic
fun pointer(path: String): JsonPath {
if (path == "")
return EMPTY
val split = path.split('/')
if (split.first() != "") {
throw IllegalArgumentException("Invalid JSON pointer: $path")
}
val pieces = ImmutableList.Builder<Piece>()
for (i in 1 until split.size) {
pieces.add(Piece(split[i].replace("~1", "/").replace("~0", "~")))
}
return JsonPath(pieces.build())
}
/**
* JSON query path doesn't have a standard,
* Starbound goes with JavaScript-like query path
*
* But this parser is different, original parser will throw
* an exception when you put non-numbers into `[brackets]`,
* like this:
*
* `someValue.a[bb]`
*
* This allows to effectively escape dots (`.`), because
* when reading array index, everything is read up until closing bracket.
*
* Another change is - empty keys
* (sequences like `..a. .key.value` -> `"" -> "" -> "a" -> " " -> "key" -> "value"`)
* are allowed
*/
@JvmStatic
fun query(path: String): JsonPath {
val pieces = ArrayList<Piece>()
val current = StringBuilder()
var readingIndex = false
for (char in path) {
if (readingIndex) {
if (char == ']') {
val finish = current.toString()
val finishInt = finish.toIntOrNull()
pieces.add(Piece(finish, if (finishInt != null) Hint.ARRAY else null))
current.clear()
readingIndex = false
} else {
current.append(char)
}
} else {
if (char == '.') {
pieces.add(Piece(current.toString()))
current.clear()
} else if (char == '[') {
readingIndex = true
} else {
current.append(char)
}
}
}
if (readingIndex) {
throw IllegalArgumentException("Expected closing ] in $path")
}
if (current.isNotEmpty()) {
pieces.add(Piece(current.toString()))
}
for ((i, piece) in pieces.withIndex()) {
if (piece.hint == null && i != pieces.size - 1) {
pieces[i] = Piece(piece.value, hint = Hint.OBJECT)
}
}
return JsonPath(ImmutableList.copyOf(pieces))
}
}
}

View File

@ -8,15 +8,15 @@ import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.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
}

View File

@ -0,0 +1,23 @@
package ru.dbotthepony.kstarbound.json
import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.gson.consumeNull
// instead of `.nullSafe()`, so tracebacks don't get polluted with anonymous class names
class NullSafeTypeAdapter<T>(private val parent: TypeAdapter<T>) : TypeAdapter<T>() {
override fun write(out: JsonWriter, value: T?) {
if (value == null)
out.nullValue()
else
parent.write(out, value)
}
override fun read(`in`: JsonReader): T? {
if (`in`.consumeNull())
return null
return parent.read(`in`)
}
}

View File

@ -1,9 +1,5 @@
package ru.dbotthepony.kstarbound.json.builder
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
}
}

View File

@ -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, *>>()

View File

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

View File

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

View File

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

View File

@ -0,0 +1,343 @@
package ru.dbotthepony.kstarbound.network.syncher
import ru.dbotthepony.kommons.io.StreamCodec
import ru.dbotthepony.kommons.io.readVarInt
import ru.dbotthepony.kommons.io.writeVarInt
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.collect.RandomListIterator
import ru.dbotthepony.kstarbound.collect.RandomSubList
import java.io.DataInputStream
import java.io.DataOutputStream
// original engine does not have "networked list", so it is always networked
// the dumb way on legacy protocol
class NetworkedList<E>(
val codec: StreamCodec<E>,
val legacyCodec: StreamCodec<E> = codec,
private val maxBacklogSize: Int = 100,
private val elementsFactory: (Int) -> MutableList<E> = ::ArrayList
) : NetworkedElement(), MutableList<E> {
private val backlog = ArrayDeque<Pair<Long, Entry<E>>>()
private val queue = ArrayDeque<Pair<Double, Entry<E>>>()
private val elements = elementsFactory(10)
private enum class Type {
ADD, REMOVE, CLEAR;
}
private data class Entry<E>(val type: Type, val index: Int, val value: KOptional<E>) {
constructor(index: Int) : this(Type.REMOVE, index, KOptional())
constructor(index: Int, value: E) : this(Type.REMOVE, index, KOptional(value))
fun apply(list: MutableList<E>) {
when (type) {
Type.ADD -> list.add(index, value.value)
Type.REMOVE -> list.removeAt(index)
Type.CLEAR -> list.clear()
}
}
}
private val clearEntry = Entry<E>(Type.CLEAR, 0, KOptional())
private var isInterpolating = false
private var currentTime = 0.0
private var isRemote = false
private fun purgeBacklog() {
while (backlog.size >= maxBacklogSize) {
backlog.removeFirst()
}
}
private fun latestState(): List<E> {
if (queue.isEmpty()) {
return elements
} else {
val copy = elementsFactory(elements.size)
for (v in elements)
copy.add(v)
for ((_, e) in queue)
e.apply(copy)
return copy
}
}
override fun readInitial(data: DataInputStream, isLegacy: Boolean) {
isRemote = true
backlog.clear()
backlog.add(currentVersion() to clearEntry)
queue.clear()
elements.clear()
val count = data.readVarInt()
if (isLegacy) {
for (i in 0 until count) {
val read = legacyCodec.read(data)
elements.add(read)
backlog.add(currentVersion() to Entry(elements.size - 1, read))
}
} else {
for (i in 0 until count) {
val read = codec.read(data)
elements.add(read)
backlog.add(currentVersion() to Entry(elements.size - 1, read))
}
}
purgeBacklog()
}
override fun writeInitial(data: DataOutputStream, isLegacy: Boolean) {
val latest = latestState()
data.writeVarInt(latest.size)
if (isLegacy) {
latest.forEach { legacyCodec.write(data, it) }
} else {
latest.forEach { codec.write(data, it) }
}
}
override fun readDelta(data: DataInputStream, interpolationDelay: Double, isLegacy: Boolean) {
isRemote = true
if (isLegacy) {
readInitial(data, true)
} else {
if (data.readBoolean()) {
var action = data.readUnsignedByte()
while (action != 0) {
val entry = when (Type.entries[action - 1]) {
Type.ADD -> Entry(data.readVarInt(), codec.read(data))
Type.REMOVE -> Entry(data.readVarInt())
Type.CLEAR -> clearEntry
}
if (isInterpolating) {
val actualTime = interpolationDelay + currentTime
if (queue.isNotEmpty() && queue.last().first >= actualTime) {
queue.forEach { it.second.apply(elements) }
queue.clear()
}
if (interpolationDelay > 0.0)
queue.add(actualTime to entry)
else
entry.apply(elements)
} else {
entry.apply(elements)
}
backlog.add(currentVersion() to entry)
action = data.readUnsignedByte()
}
purgeBacklog()
} else {
readInitial(data, false)
}
}
}
override fun writeDelta(data: DataOutputStream, remoteVersion: Long, isLegacy: Boolean) {
if (isLegacy) {
writeInitial(data, true)
} else if (backlog.isNotEmpty() && remoteVersion < backlog.first().first) {
data.writeBoolean(false)
writeInitial(data, false)
} else {
data.writeBoolean(true)
for ((version, entry) in backlog) {
if (version >= remoteVersion) {
data.writeByte(entry.type.ordinal + 1)
when (entry.type) {
Type.ADD -> { data.writeVarInt(entry.index); codec.write(data, entry.value.value) }
Type.REMOVE -> { data.writeVarInt(entry.index) }
Type.CLEAR -> {}
}
}
}
data.writeByte(0)
}
}
override fun readBlankDelta(interpolationDelay: Double) {}
override fun enableInterpolation(extrapolation: Double) {
isInterpolating = true
}
override fun disableInterpolation() {
isInterpolating = false
queue.forEach { it.second.apply(elements) }
queue.clear()
}
override fun tickInterpolation(delta: Double) {
currentTime += delta
while (queue.isNotEmpty() && queue.first().first <= currentTime) {
queue.removeFirst().second.apply(elements)
}
}
override fun hasChangedSince(version: Long): Boolean {
return backlog.isNotEmpty() && backlog.first().first >= version
}
override val size: Int
get() = elements.size
override fun contains(element: E): Boolean {
return elements.contains(element)
}
override fun containsAll(elements: Collection<E>): Boolean {
return this.elements.containsAll(elements)
}
override fun get(index: Int): E {
return elements[index]
}
override fun indexOf(element: E): Int {
return elements.indexOf(element)
}
override fun isEmpty(): Boolean {
return elements.isEmpty()
}
override fun iterator(): MutableIterator<E> {
return listIterator()
}
override fun lastIndexOf(element: E): Int {
return elements.lastIndexOf(element)
}
override fun add(element: E): Boolean {
add(size, element)
return true
}
override fun add(index: Int, element: E) {
check(!isRemote) { "List is not owned by this side" }
elements.add(index, element)
backlog.add(currentVersion() to Entry(index, element))
purgeBacklog()
}
override fun addAll(index: Int, elements: Collection<E>): Boolean {
var newIndex = index
elements.forEach { add(newIndex++, it) }
return true
}
override fun addAll(elements: Collection<E>): Boolean {
return addAll(size, elements)
}
override fun clear() {
check(!isRemote) { "List is not owned by this side" }
backlog.clear()
backlog.add(currentVersion() to clearEntry)
elements.clear()
}
override fun listIterator(): MutableListIterator<E> {
return listIterator(0)
}
override fun listIterator(index: Int): MutableListIterator<E> {
return RandomListIterator(this, index)
}
override fun remove(element: E): Boolean {
check(!isRemote) { "List is not owned by this side" }
val indexOf = elements.indexOf(element)
if (indexOf == -1)
return false
removeAt(indexOf)
return true
}
override fun removeAll(elements: Collection<E>): Boolean {
check(!isRemote) { "List is not owned by this side" }
var any = false
elements.forEach { any = remove(it) || any }
return any
}
override fun removeAt(index: Int): E {
check(!isRemote) { "List is not owned by this side" }
val element = elements.removeAt(index)
backlog.add(currentVersion() to Entry(index))
purgeBacklog()
return element
}
override fun retainAll(elements: Collection<E>): Boolean {
check(!isRemote) { "List is not owned by this side" }
val itr = iterator()
var modified = false
for (v in itr) {
if (v !in elements) {
itr.remove()
modified = true
}
}
return modified
}
override fun set(index: Int, element: E): E {
check(!isRemote) { "List is not owned by this side" }
val old = elements.set(index, element)
backlog.add(currentVersion() to Entry(index, element))
purgeBacklog()
return old
}
override fun subList(fromIndex: Int, toIndex: Int): MutableList<E> {
return RandomSubList(this, fromIndex, toIndex)
}
override fun equals(other: Any?): Boolean {
if (other === this) return true
if (other !is List<*>) return false
val e1: ListIterator<E> = listIterator()
val e2 = other.listIterator()
while (e1.hasNext() && e2.hasNext()) {
val o1 = e1.next()
val o2 = e2.next()
if (!(if (o1 == null) o2 == null else o1 == o2)) return false
}
return !(e1.hasNext() || e2.hasNext())
}
override fun hashCode(): Int {
var hashCode = 1
for (e in this) hashCode = 31 * hashCode + e.hashCode()
return hashCode
}
override fun toString(): String {
return "NetworkedList[${joinToString()}]"
}
}

View File

@ -5,8 +5,10 @@ import ru.dbotthepony.kommons.io.StreamCodec
import ru.dbotthepony.kommons.io.readVarInt
import ru.dbotthepony.kommons.io.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)
}

View File

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

View File

@ -1,18 +0,0 @@
package ru.dbotthepony.kstarbound.player
import com.google.gson.JsonObject
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
@JsonFactory
data class QuestDescriptor(
val questId: String,
val templateId: String = questId,
val seed: Long = makeSeed(),
val parameters: JsonObject = JsonObject()
) {
companion object {
fun makeSeed(): Long {
return System.nanoTime().rotateLeft(27).xor(System.currentTimeMillis())
}
}
}

View File

@ -3,7 +3,6 @@ package ru.dbotthepony.kstarbound.player
import com.google.gson.JsonArray
import com.google.gson.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 {

View File

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

View File

@ -18,7 +18,7 @@ import ru.dbotthepony.kstarbound.world.api.AbstractCell
import ru.dbotthepony.kstarbound.world.api.ImmutableCell
import ru.dbotthepony.kstarbound.world.api.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

View File

@ -5,17 +5,22 @@ import it.unimi.dsi.fastutil.ints.IntArraySet
import it.unimi.dsi.fastutil.longs.Long2ObjectFunction
import it.unimi.dsi.fastutil.longs.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

View File

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

View File

@ -21,6 +21,7 @@ import ru.dbotthepony.kstarbound.defs.world.CelestialParameters
import ru.dbotthepony.kstarbound.defs.world.CelestialPlanet
import ru.dbotthepony.kstarbound.defs.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(

View File

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

View File

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

View File

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

View File

@ -5,15 +5,16 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
import it.unimi.dsi.fastutil.ints.IntArraySet
import it.unimi.dsi.fastutil.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()

View File

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

View File

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

View File

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

View File

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

View File

@ -1,59 +0,0 @@
package ru.dbotthepony.kstarbound.world.entities
import it.unimi.dsi.fastutil.objects.Object2ObjectAVLTreeMap
import ru.dbotthepony.kstarbound.defs.animation.AnimatedPartsDefinition
class AnimatedParts {
private class StateType(config: AnimatedPartsDefinition.StateType) {
var enabled = config.enabled
var activeStateDirty = true
val priority = config.priority
val stateTypeProperties = config.properties
val default: String
// sorted by key
val states = Object2ObjectAVLTreeMap<String, AnimatedPartsDefinition.StateType.State>()
init {
config.states.forEach { (t, u) -> states[t] = u }
if (states.isNotEmpty() && config.default.isBlank())
default = states.firstKey()
else
default = config.default
}
}
private class Part(config: AnimatedPartsDefinition.Part) {
val partProperties = config.properties
var activePartDirty = true
val partStates = config.partStates
}
// sorted by priority
private val stateTypes = LinkedHashMap<String, StateType>()
// sorted by key
private val parts = Object2ObjectAVLTreeMap<String, Part>()
constructor() {
}
constructor(config: AnimatedPartsDefinition) {
for ((k, v) in config.stateTypes.entries.sortedWith { o1, o2 -> o2.value.priority.compareTo(o1.value.priority) }) {
stateTypes[k] = StateType(v)
}
for ((k, v) in config.parts) {
parts[k] = Part(v)
}
}
fun parts(): Collection<String> {
return parts.keys
}
fun stateTypes(): Collection<String> {
return stateTypes.keys
}
}

View File

@ -1,30 +1,27 @@
package ru.dbotthepony.kstarbound.world.entities
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 {

View File

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

View File

@ -1,33 +0,0 @@
package ru.dbotthepony.kstarbound.world.entities
import com.google.gson.JsonObject
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.SpatialIndex
import ru.dbotthepony.kstarbound.world.World
/**
* (Hopefully) Static world entities (Plants, Objects, etc), which reside on cell grid
*/
abstract class TileEntity(path: String) : AbstractEntity(path) {
var tilePosition = Vector2i()
set(value) {
if (isSpawned) {
field = world.geometry.wrap(value)
// spatialEntry?.fixture?.move()
} else {
field = value
}
}
override val position: Vector2d
get() = tilePosition.toDoubleVector()
override fun onJoinWorld(world: World<*, *>) {
tilePosition = tilePosition
}
override fun onRemove(world: World<*, *>, isDeath: Boolean) {
}
}

View File

@ -1,205 +0,0 @@
package ru.dbotthepony.kstarbound.world.entities
import com.google.common.collect.ImmutableMap
import com.google.gson.JsonObject
import com.google.gson.TypeAdapter
import com.google.gson.reflect.TypeToken
import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.client.render.LayeredRenderer
import ru.dbotthepony.kstarbound.client.world.ClientWorld
import ru.dbotthepony.kstarbound.defs.Drawable
import ru.dbotthepony.kstarbound.defs.image.SpriteReference
import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition
import ru.dbotthepony.kstarbound.defs.`object`.ObjectOrientation
import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kstarbound.defs.EntityType
import ru.dbotthepony.kstarbound.util.asStringOrNull
import ru.dbotthepony.kstarbound.world.Side
import ru.dbotthepony.kstarbound.world.LightCalculator
import ru.dbotthepony.kstarbound.world.PIXELS_IN_STARBOUND_UNITf
import ru.dbotthepony.kstarbound.world.api.TileColor
import java.io.DataOutputStream
import java.util.HashMap
open class WorldObject(
val prototype: Registry.Entry<ObjectDefinition>,
) : TileEntity(prototype.file?.computeDirectory() ?: "/") {
fun deserialize(data: JsonObject) {
direction = data.get("direction", directions) { Side.LEFT }
orientationIndex = data.get("orientationIndex", -1)
interactive = data.get("interactive", false)
uniqueId = data["uniqueId"]?.asStringOrNull
for ((k, v) in data.get("parameters") { JsonObject() }.entrySet()) {
properties[k] = v.deepCopy()
}
}
override val type: EntityType
get() = EntityType.OBJECT
override fun writeNetwork(stream: DataOutputStream, isLegacy: Boolean) {
TODO("Not yet implemented")
}
fun serialize(): JsonObject {
val into = JsonObject()
into["name"] = prototype.key
into["tilePosition"] = vectors.toJsonTree(tilePosition)
into["direction"] = directions.toJsonTree(direction)
into["orientationIndex"] = orientationIndex
into["interactive"] = interactive
if (uniqueId != null) {
into["uniqueId"] = uniqueId!!
}
into["parameters"] = properties.deepCopy()
return into
}
//
// internal runtime properties
//
inline val clientWorld get() = world as ClientWorld
inline val serverWorld get() = world as ServerWorld
inline val orientations get() = prototype.value.orientations
protected val renderParamLocations = HashMap<String, () -> String?>()
private var frame = 0
set(value) {
if (field != value) {
field = value
drawablesCache.invalidate()
}
}
private var frameTimer = 0.0
val flickerPeriod = prototype.value.flickerPeriod?.copy()
//
// top level properties
//
var uniqueId: String? = null
var interactive = false
var direction = Side.LEFT
var orientationIndex = -1
set(value) {
if (field != value) {
field = value
invalidate()
}
}
//
// json driven properties
//
var color: TileColor by Property(TileColor.DEFAULT)
var animationParts: ImmutableMap<String, SpriteReference> by Property()
var imagePosition: Vector2i by Property(Vector2i.ZERO)
var animationPosition: Vector2i by Property(Vector2i.ZERO)
init {
renderParamLocations["color"] = { color.lowercase }
renderParamLocations["frame"] = { frame.toString() }
}
private val drawablesCache = LazyData {
orientation?.drawables?.map { it.with(::getRenderParam) } ?: listOf()
}
val drawables: List<Drawable> by drawablesCache
fun getRenderParam(key: String): String? {
return renderParamLocations[key]?.invoke() ?: "default"
}
override fun invalidate(name: String) {
super.invalidate(name)
if (name in renderParamLocations) {
drawablesCache.invalidate()
}
}
override fun invalidate() {
super.invalidate()
}
override fun tickShared() {
super.tickShared()
flickerPeriod?.update(Starbound.TIMESTEP, world.random)
}
override fun tickRemote() {
val orientation = orientation
if (orientation != null) {
frameTimer = (frameTimer + Starbound.TIMESTEP) % orientation.animationCycle
frame = (frameTimer / orientation.animationCycle * orientation.frames).toInt()
}
}
val orientation: ObjectOrientation? get() {
return orientations.getOrNull(orientationIndex)
}
override fun defs(): Collection<JsonObject> {
val get = orientation
return if (get == null) listOf(prototype.jsonObject) else listOf(get.json, prototype.jsonObject)
}
val lightColors: ImmutableMap<String, RGBAColor> by LazyData(listOf("lightColor", "lightColors")) {
dataValue("lightColor")?.let { ImmutableMap.of("default", colors0.fromJsonTree(it)) }
?: dataValue("lightColors")?.let { colors1.fromJsonTree(it) }
?: ImmutableMap.of()
}
override fun addLights(lightCalculator: LightCalculator, xOffset: Int, yOffset: Int) {
var color = lightColors[color.lowercase]
if (color != null) {
if (flickerPeriod != null) {
val sample = flickerPeriod.sinValue().toFloat()
color *= sample
}
lightCalculator.addPointLight(tilePosition.x - xOffset, tilePosition.y - yOffset, color)
}
}
override fun render(client: StarboundClient, layers: LayeredRenderer) {
val layer = layers.getLayer(orientation?.renderLayer ?: return)
drawables.forEach {
val (x, y) = imagePosition
it.render(client, layer, position.x.toFloat() + x / PIXELS_IN_STARBOUND_UNITf, position.y.toFloat() + y / PIXELS_IN_STARBOUND_UNITf)
}
}
companion object {
private val colors1 by lazy { Starbound.gson.getAdapter(TypeToken.getParameterized(ImmutableMap::class.java, String::class.java, RGBAColor::class.java)) as TypeAdapter<ImmutableMap<String, RGBAColor>> }
private val colors0 by lazy { Starbound.gson.getAdapter(RGBAColor::class.java) }
private val strings by lazy { Starbound.gson.getAdapter(String::class.java) }
private val directions by lazy { Starbound.gson.getAdapter(Side::class.java) }
private val vectors by lazy { Starbound.gson.getAdapter(Vector2i::class.java) }
fun fromJson(content: JsonObject): WorldObject {
val prototype = Registries.worldObjects[content["name"]?.asString ?: throw IllegalArgumentException("Missing object name")] ?: throw IllegalArgumentException("No such object defined for '${content["name"]}'")
val pos = content.get("tilePosition", vectors) { throw IllegalArgumentException("No tilePosition was present in saved data") }
val result = WorldObject(prototype)
result.tilePosition = pos
result.deserialize(content)
return result
}
}
}

View File

@ -1,9 +1,11 @@
package ru.dbotthepony.kstarbound.world.entities.player
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")
}
}

View File

@ -0,0 +1,23 @@
package ru.dbotthepony.kstarbound.world.entities.tile
import ru.dbotthepony.kommons.util.getValue
import ru.dbotthepony.kommons.util.setValue
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition
import ru.dbotthepony.kstarbound.math.Interpolator
import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean
import ru.dbotthepony.kstarbound.network.syncher.networkedBytes
import ru.dbotthepony.kstarbound.network.syncher.networkedFloat
import ru.dbotthepony.kstarbound.network.syncher.networkedSignedInt
class ContainerObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(config) {
var opened by networkedSignedInt().also { networkGroup.upstream.add(it) }
var isCrafting by networkedBoolean().also { networkGroup.upstream.add(it) }
var craftingProgress by networkedFloat().also { networkGroup.upstream.add(it); it.interpolator = Interpolator.Linear }
// i have no words.
// this field embeds ENTIRE net state of 'ItemBag',
// and each time container is updated, its contents are networked fully
// each. damn. time.
// placeholder data, container size 40, read 0 items
var itemsNetState by networkedBytes(byteArrayOf(40, 0)).also { networkGroup.upstream.add(it) }
}

View File

@ -0,0 +1,10 @@
package ru.dbotthepony.kstarbound.world.entities.tile
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition
class LoungeableObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(config) {
init {
isInteractive = true
}
}

View File

@ -0,0 +1,69 @@
package ru.dbotthepony.kstarbound.world.entities.tile
import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.defs.tile.TileDamage
import ru.dbotthepony.kstarbound.network.syncher.networkedSignedInt
import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
/**
* (Hopefully) Static world entities (Plants, Objects, etc), which reside on cell grid
*/
abstract class TileEntity(path: String) : AbstractEntity(path) {
protected val xTilePositionNet = networkedSignedInt()
protected val yTilePositionNet = networkedSignedInt()
init {
xTilePositionNet.addListener(::updateSpatialIndex)
yTilePositionNet.addListener(::updateSpatialIndex)
}
abstract val metaBoundingBox: AABB
private fun updateSpatialIndex() {
val spatialEntry = spatialEntry ?: return
spatialEntry.fixture.move(metaBoundingBox + position)
}
var xTilePosition: Int
get() = xTilePositionNet.get()
set(value) {
if (isSpawned) {
xTilePositionNet.accept(world.geometry.x.cell(value))
} else {
xTilePositionNet.accept(value)
}
}
var yTilePosition: Int
get() = yTilePositionNet.get()
set(value) {
if (isSpawned) {
yTilePositionNet.accept(world.geometry.x.cell(value))
} else {
yTilePositionNet.accept(value)
}
}
var tilePosition: Vector2i
get() = Vector2i(xTilePosition, yTilePosition)
set(value) {
xTilePosition = value.x
yTilePosition = value.y
}
override val position: Vector2d
get() = Vector2d(xTilePosition.toDouble(), yTilePosition.toDouble())
abstract val occupySpaces: Set<Vector2i>
abstract fun damage(damageSpaces: List<Vector2i>, source: Vector2d, damage: TileDamage): Boolean
override fun onJoinWorld(world: World<*, *>) {
updateSpatialIndex()
}
override fun onRemove(world: World<*, *>, isDeath: Boolean) {
}
}

View File

@ -0,0 +1,375 @@
package ru.dbotthepony.kstarbound.world.entities.tile
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import com.google.gson.TypeAdapter
import com.google.gson.reflect.TypeToken
import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.client.render.LayeredRenderer
import ru.dbotthepony.kstarbound.defs.Drawable
import ru.dbotthepony.kstarbound.defs.image.SpriteReference
import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition
import ru.dbotthepony.kstarbound.defs.`object`.ObjectOrientation
import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kommons.io.RGBACodec
import ru.dbotthepony.kommons.io.StreamCodec
import ru.dbotthepony.kommons.io.Vector2iCodec
import ru.dbotthepony.kommons.io.map
import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.util.getValue
import ru.dbotthepony.kommons.util.setValue
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.client.world.ClientWorld
import ru.dbotthepony.kstarbound.defs.DamageSource
import ru.dbotthepony.kstarbound.defs.EntityType
import ru.dbotthepony.kstarbound.defs.`object`.ObjectType
import ru.dbotthepony.kstarbound.defs.quest.QuestArcDescriptor
import ru.dbotthepony.kstarbound.defs.tile.TileDamage
import ru.dbotthepony.kstarbound.json.JsonPath
import ru.dbotthepony.kstarbound.json.mergeJson
import ru.dbotthepony.kstarbound.json.stream
import ru.dbotthepony.kstarbound.network.syncher.InternedStringCodec
import ru.dbotthepony.kstarbound.network.syncher.JsonElementCodec
import ru.dbotthepony.kstarbound.network.syncher.NetworkedList
import ru.dbotthepony.kstarbound.network.syncher.NetworkedMap
import ru.dbotthepony.kstarbound.network.syncher.UnsignedShortCodec
import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean
import ru.dbotthepony.kstarbound.network.syncher.networkedData
import ru.dbotthepony.kstarbound.network.syncher.networkedEnum
import ru.dbotthepony.kstarbound.network.syncher.networkedEventCounter
import ru.dbotthepony.kstarbound.network.syncher.networkedFloat
import ru.dbotthepony.kstarbound.network.syncher.networkedJsonElement
import ru.dbotthepony.kstarbound.network.syncher.networkedPointer
import ru.dbotthepony.kstarbound.network.syncher.networkedString
import ru.dbotthepony.kstarbound.json.writeJsonElement
import ru.dbotthepony.kstarbound.util.asStringOrNull
import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.LightCalculator
import ru.dbotthepony.kstarbound.world.PIXELS_IN_STARBOUND_UNITf
import ru.dbotthepony.kstarbound.world.TileHealth
import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.api.TileColor
import ru.dbotthepony.kstarbound.world.entities.Animator
import ru.dbotthepony.kstarbound.world.entities.wire.WireConnection
import java.io.DataOutputStream
import java.util.HashMap
open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntity(config.file?.computeDirectory() ?: "/") {
open fun deserialize(data: JsonObject) {
direction = data.get("direction", directions) { Direction.LEFT }
orientationIndex = data.get("orientationIndex", -1).toLong()
isInteractive = data.get("interactive", false)
tilePosition = data.get("tilePosition", vectors)
uniqueID.accept(KOptional.ofNullable(data["uniqueId"]?.asStringOrNull))
for ((k, v) in data.get("parameters") { JsonObject() }.entrySet()) {
parameters[k] = v
}
}
open fun serialize(): JsonObject {
val into = JsonObject()
into["name"] = config.key
into["tilePosition"] = vectors.toJsonTree(tilePosition)
into["direction"] = directions.toJsonTree(direction)
into["orientationIndex"] = orientationIndex
into["interactive"] = isInteractive
uniqueID.get().ifPresent {
into["uniqueId"] = it
}
into["parameters"] = JsonObject().also {
for ((k, v) in parameters) {
it[k] = v.deepCopy()
}
}
return into
}
override val metaBoundingBox: AABB by LazyData {
orientation?.metaBoundBox ?: orientation?.let { AABB(it.boundingBox.mins.toDoubleVector() + Vector2d.NEGATIVE_XY, it.boundingBox.maxs.toDoubleVector() + Vector2d(2.0, 2.0)) } ?: AABB.ZERO
}
val parameters = NetworkedMap(InternedStringCodec, JsonElementCodec).also {
networkGroup.upstream.add(it)
it.addListener(Runnable {
invalidate()
})
}
val orientation: ObjectOrientation? get() {
return config.value.orientations.getOrNull(orientationIndex.toInt())
}
protected val mergedJson = LazyData {
val orientation = orientation
if (orientation == null) {
mergeJson(config.jsonObject.deepCopy(), parameters)
} else {
mergeJson(mergeJson(config.jsonObject.deepCopy(), orientation.json), parameters)
}
}
final override fun lookupProperty(path: JsonPath, orElse: () -> JsonElement): JsonElement {
return path.get(mergedJson.value, orElse)
}
init {
networkGroup.upstream.add(uniqueID)
}
var isInteractive by networkedBoolean().also { networkGroup.upstream.add(it) }
var materialSpaces = NetworkedList(materialSpacesCodec, materialSpacesCodecLegacy).also { networkGroup.upstream.add(it) }
init {
networkGroup.upstream.add(xTilePositionNet)
networkGroup.upstream.add(yTilePositionNet)
}
var direction by networkedEnum(Direction.LEFT).also { networkGroup.upstream.add(it) }
var health by networkedFloat().also { networkGroup.upstream.add(it) }
var orientationIndex by networkedPointer().also {
networkGroup.upstream.add(it)
it.addListener(Runnable { invalidate() })
}
private val networkedRenderKeys = NetworkedMap(InternedStringCodec, InternedStringCodec).also { networkGroup.upstream.add(it) }
private val localRenderKeys = HashMap<String, String>()
var soundEffectEnabled by networkedBoolean(true).also { networkGroup.upstream.add(it) }
var lightSourceColor by networkedData(RGBAColor.TRANSPARENT_BLACK, RGBACodec).also { networkGroup.upstream.add(it) }
val newChatMessageEvent = networkedEventCounter().also { networkGroup.upstream.add(it) }
val chatMessage by networkedString().also { networkGroup.upstream.add(it) }
val chatPortrait by networkedString().also { networkGroup.upstream.add(it) }
val chatConfig by networkedJsonElement().also { networkGroup.upstream.add(it) }
inner class WireNode(val position: Vector2i) {
var state by networkedBoolean().also { networkGroup.upstream.add(it) }
val connections = NetworkedList(WireConnection.CODEC).also { networkGroup.upstream.add(it) }
}
val inputNodes: ImmutableList<WireNode> = lookupProperty(JsonPath("inputNodes")) { JsonArray() }
.asJsonArray
.stream()
.map { WireNode(vectors.fromJsonTree(it)) }
.collect(ImmutableList.toImmutableList())
val outputNodes: ImmutableList<WireNode> = lookupProperty(JsonPath("outputNodes")) { JsonArray() }
.asJsonArray
.stream()
.map { WireNode(vectors.fromJsonTree(it)) }
.collect(ImmutableList.toImmutableList())
val offeredQuests = NetworkedList(QuestArcDescriptor.CODEC, QuestArcDescriptor.LEGACY_CODEC).also { networkGroup.upstream.add(it) }
val turnInQuests = NetworkedList(InternedStringCodec).also { networkGroup.upstream.add(it) }
val damageSources = NetworkedList(DamageSource.CODEC, DamageSource.LEGACY_CODEC).also { networkGroup.upstream.add(it) }
// don't interpolate scripted animation parameters
val scriptedAnimationParameters = NetworkedMap(InternedStringCodec, JsonElementCodec).also { networkGroup.upstream.add(it, false) }
// Why is this a thing when we have 'health' field??????? Hello chucklefish again?
val tileHealth = TileHealth.TileEntity().also { networkGroup.upstream.add(it.networkGroup) }
val animator: Animator
init {
if (config.value.animation?.value != null) {
animator = Animator(config.value.animation!!.value!!).also { networkGroup.upstream.add(it.networkGroup) }
} else {
animator = Animator().also { networkGroup.upstream.add(it.networkGroup) }
}
}
val unbreakable by LazyData {
lookupProperty(JsonPath("unbreakable")) { JsonPrimitive(false) }.asBoolean
}
override val type: EntityType
get() = EntityType.OBJECT
override fun setProperty0(key: JsonPath, value: JsonElement) {
TODO("Not yet implemented")
}
override fun writeNetwork(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeBinaryString(config.key)
stream.writeJsonElement(JsonObject().also {
for ((k, v) in parameters) {
it[k] = v.deepCopy()
}
})
}
private var frameTimer = 0.0
private var frame = 0
set(value) {
if (field != value) {
field = value
drawablesCache.invalidate()
}
}
val flickerPeriod = config.value.flickerPeriod?.copy()
//
// json driven properties
//
var color: TileColor by Property(JsonPath("color"), TileColor.DEFAULT)
var animationParts: ImmutableMap<String, SpriteReference> by Property(JsonPath("animationParts"))
var imagePosition: Vector2i by Property(JsonPath("imagePosition"), Vector2i.ZERO)
var animationPosition: Vector2i by Property(JsonPath("animationPosition"), Vector2i.ZERO)
private val drawablesCache = LazyData {
orientation?.drawables?.map { it.with(::getRenderParam) } ?: listOf()
}
init {
networkedRenderKeys.addListener(Runnable { drawablesCache.invalidate() })
}
val drawables: List<Drawable> by drawablesCache
override val occupySpaces get() = orientation?.occupySpaces ?: setOf()
fun getRenderParam(key: String): String? {
return localRenderKeys[key] ?: networkedRenderKeys[key] ?: "default"
}
protected fun setImageKey(key: String, value: String) {
val old = localRenderKeys.put(key, value)
if (old != value) {
drawablesCache.invalidate()
}
if (!isRemote && networkedRenderKeys[key] != value) {
networkedRenderKeys[key] = value
}
}
override fun invalidate() {
super.invalidate()
drawablesCache.invalidate()
}
override fun tick() {
super.tick()
flickerPeriod?.update(Starbound.TIMESTEP, world.random)
if (!isRemote) {
tileHealth.tick(config.value.damageConfig)
animator.tick()
val orientation = orientation
if (orientation != null) {
frameTimer = (frameTimer + Starbound.TIMESTEP) % orientation.animationCycle
val oldFrame = frame
frame = (frameTimer / orientation.animationCycle * orientation.frames).toInt().coerceIn(0, orientation.frames)
if (oldFrame != frame) {
setImageKey("frame", frame.toString())
}
}
}
}
override fun onJoinWorld(world: World<*, *>) {
super.onJoinWorld(world)
setImageKey("color", lookupProperty(JsonPath("color")) { JsonPrimitive("default") }.asString)
}
override fun damage(damageSpaces: List<Vector2i>, source: Vector2d, damage: TileDamage): Boolean {
if (unbreakable)
return false
tileHealth.damage(config.value.damageConfig, source, damage)
return tileHealth.isDead
}
val lightColors: ImmutableMap<String, RGBAColor> by LazyData {
val lightColor = lookupProperty(lightColorPath)
if (!lightColor.isJsonNull) {
return@LazyData ImmutableMap.of("default", colors0.fromJsonTree(lightColor))
}
val lightColors = lookupProperty(lightColorsPath)
if (!lightColors.isJsonNull) {
return@LazyData colors1.fromJsonTree(lightColors)
}
ImmutableMap.of()
}
override fun addLights(lightCalculator: LightCalculator, xOffset: Int, yOffset: Int) {
var color = lightColors[color.lowercase]
if (color != null) {
if (flickerPeriod != null) {
val sample = flickerPeriod.sinValue().toFloat()
color *= sample
}
lightCalculator.addPointLight(tilePosition.x - xOffset, tilePosition.y - yOffset, color)
}
}
override fun render(client: StarboundClient, layers: LayeredRenderer) {
val layer = layers.getLayer(orientation?.renderLayer ?: return)
drawables.forEach {
val (x, y) = imagePosition
it.render(client, layer, position.x.toFloat() + x / PIXELS_IN_STARBOUND_UNITf, position.y.toFloat() + y / PIXELS_IN_STARBOUND_UNITf)
}
}
companion object {
private val lightColorPath = JsonPath("lightColor")
private val lightColorsPath = JsonPath("lightColors")
private val materialSpacesCodec = StreamCodec.Pair(Vector2iCodec, InternedStringCodec.map({ Registries.tiles.ref(this) }, { entry?.key ?: key.left.orElse("") }))
private val materialSpacesCodecLegacy = StreamCodec.Pair(Vector2iCodec, UnsignedShortCodec.map({ Registries.tiles.ref(this) }, { entry?.id ?: 65534 }))
private val colors1 by lazy { Starbound.gson.getAdapter(TypeToken.getParameterized(ImmutableMap::class.java, String::class.java, RGBAColor::class.java)) as TypeAdapter<ImmutableMap<String, RGBAColor>> }
private val colors0 by lazy { Starbound.gson.getAdapter(RGBAColor::class.java) }
private val strings by lazy { Starbound.gson.getAdapter(String::class.java) }
private val directions by lazy { Starbound.gson.getAdapter(Direction::class.java) }
private val vectors by lazy { Starbound.gson.getAdapter(Vector2i::class.java) }
fun fromJson(content: JsonObject): WorldObject {
val prototype = Registries.worldObjects[content["name"]?.asString ?: throw IllegalArgumentException("Missing object name")] ?: throw IllegalArgumentException("No such object defined for '${content["name"]}'")
val result = when (prototype.value.objectType) {
ObjectType.OBJECT -> WorldObject(prototype)
ObjectType.LOUNGEABLE -> LoungeableObject(prototype)
ObjectType.CONTAINER -> ContainerObject(prototype)
ObjectType.FARMABLE -> TODO("ObjectType.FARMABLE")
ObjectType.TELEPORTER -> TODO("ObjectType.TELEPORTER")
ObjectType.PHYSICS -> TODO("ObjectType.PHYSICS")
}
result.deserialize(content)
return result
}
}
}

View File

@ -0,0 +1,23 @@
package ru.dbotthepony.kstarbound.world.entities.wire
import ru.dbotthepony.kommons.io.StreamCodec
import ru.dbotthepony.kommons.io.readVector2i
import ru.dbotthepony.kommons.io.writeStruct2i
import ru.dbotthepony.kommons.io.writeVarLong
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.network.syncher.SizeTCodec
import java.io.DataInputStream
import java.io.DataOutputStream
data class WireConnection(val entityLocation: Vector2i, val index: Int = -1) {
constructor(stream: DataInputStream) : this(stream.readVector2i(), SizeTCodec.read(stream).toInt())
fun write(stream: DataOutputStream) {
stream.writeStruct2i((entityLocation))
SizeTCodec.write(stream, index.toLong())
}
companion object {
val CODEC = StreamCodec.Impl(::WireConnection, { a, b -> b.write(a) })
}
}

View File

@ -0,0 +1,47 @@
package ru.dbotthepony.kstarbound.test
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import ru.dbotthepony.kstarbound.collect.RandomSubList
object CollectionTests {
@Test
@DisplayName("Sublists test")
fun subLists() {
val list = ArrayList<Int>()
val list2 = ArrayList<Int>()
for (i in 0 .. 1000) {
list.add(i)
list2.add(i)
}
val sub0 = list.subList(40, 80)
val sub1 = RandomSubList(list2, 40, 80)
assertEquals(list, list2)
assertEquals(40, sub0.size)
assertEquals(40, sub1.size)
assertEquals(sub0, sub1)
assertEquals(45, sub0.removeAt(5))
assertEquals(45, sub1.removeAt(5))
assertEquals(list, list2)
assertEquals(sub0, sub1)
assertEquals(46, sub0.removeAt(5))
assertEquals(46, sub1.removeAt(5))
assertEquals(list, list2)
assertEquals(sub0, sub1)
assertEquals(38, sub0.size)
assertEquals(38, sub1.size)
assertEquals(list, list2)
}
}