Loungeable objects, some cleanup to entities in general

This commit is contained in:
DBotThePony 2024-04-14 20:59:01 +07:00
parent 403aac63de
commit 77aa05d9f3
Signed by: DBot
GPG Key ID: DCC23B5715498507
19 changed files with 228 additions and 344 deletions

View File

@ -1,157 +0,0 @@
package ru.dbotthepony.kstarbound.defs
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 ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.util.AssetPathStack
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
/**
* Base class for instances which can be mutated by altering underlying JSON
*/
abstract class JsonDriven(val path: String) {
private val delegates = ArrayList<Property<*>>()
private val lazies = ArrayList<LazyData<*>>()
/**
* [JsonObject]s which define behavior of properties
*/
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() }
}
inner class LazyData<T>(private val initializer: () -> T) : Lazy<T> {
init {
lazies.add(this)
}
private var _value: Any? = mark
override val value: T get() {
var value = _value
if (value !== mark) {
return value as T
}
value = initializer.invoke()
_value = value
return value
}
override fun isInitialized(): Boolean {
return _value !== mark
}
fun invalidate() {
_value = mark
}
}
inner class Property<T>(
val name: JsonPath,
val default: Either<Supplier<T>, JsonElement>? = null,
private var adapter: TypeAdapter<T>? = null,
) : 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)
}
private var value: Supplier<T> by Delegates.notNull()
private fun compute(): T {
val value = lookupProperty(name)
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(this@JsonDriven.path) {
return adapter!!.fromJsonTree(default.right())
}
}
} else {
AssetPathStack(this@JsonDriven.path) {
return adapter!!.fromJsonTree(value)
}
}
}
fun invalidate() {
value = Supplier {
val new = compute()
this.value = Supplier { new }
new
}
}
init {
invalidate()
}
override fun get(): T {
return value.get()
}
override fun accept(t: T) {
AssetPathStack(this@JsonDriven.path) {
setProperty0(name, adapter!!.toJsonTree(t))
}
value = Supplier { t }
}
@OptIn(ExperimentalStdlibApi::class)
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
if (adapter == null) {
adapter = Starbound.gson.getAdapter(TypeToken.get(property.returnType.javaType)) as TypeAdapter<T>
}
return value.get()
}
@OptIn(ExperimentalStdlibApi::class)
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
if (adapter == null) {
adapter = Starbound.gson.getAdapter(TypeToken.get(property.returnType.javaType)) as TypeAdapter<T>
}
return accept(value)
}
}
companion object {
private val mark = Any()
}
}

View File

@ -493,7 +493,7 @@ class DungeonWorld(val parent: ServerWorld, val random: RandomGenerator, val mar
val orientation = obj!!.config.value.findValidOrientation(parent, obj.tilePosition, direction) val orientation = obj!!.config.value.findValidOrientation(parent, obj.tilePosition, direction)
if (orientation != -1) { if (orientation != -1) {
obj.setOrientation(orientation) obj.orientationIndex = orientation.toLong()
obj.joinWorld(parent) obj.joinWorld(parent)
} else { } else {
LOGGER.error("Tried to place object ${obj.config.key} at ${obj.tilePosition}, but it can't be placed there!") LOGGER.error("Tried to place object ${obj.config.key} at ${obj.tilePosition}, but it can't be placed there!")

View File

@ -0,0 +1,17 @@
package ru.dbotthepony.kstarbound.defs.`object`
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
// int32_t
enum class LoungeControl(override val jsonName: String) : IStringSerializable {
LEFT("Left"),
RIGHT("Right"),
DOWN("Down"),
UP("Up"),
JUMP("Jump"),
PRIMARY_FIRE("PrimaryFire"),
ALT_FIRE("AltFire"),
SPECIAL1("Special1"),
SPECIAL2("Special2"),
SPECIAL3("Special3");
}

View File

@ -0,0 +1,11 @@
package ru.dbotthepony.kstarbound.defs.`object`
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
// int32_t
enum class LoungeOrientation(override val jsonName: String) : IStringSerializable {
NONE("none"),
SIT("sit"),
LAY("lay"),
STAND("stand");
}

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.kstarbound.lua.bindings package ru.dbotthepony.kstarbound.lua.bindings
import com.google.gson.JsonNull
import com.google.gson.JsonPrimitive import com.google.gson.JsonPrimitive
import org.classdump.luna.ByteString import org.classdump.luna.ByteString
import org.classdump.luna.Table import org.classdump.luna.Table
@ -30,7 +31,7 @@ fun provideWorldObjectBindings(self: WorldObject, lua: LuaEnvironment) {
config["getParameter"] = luaFunction { name: ByteString, default: Any? -> config["getParameter"] = luaFunction { name: ByteString, default: Any? ->
val path = JsonPath.query(name.decode()) val path = JsonPath.query(name.decode())
val find = self.lookupProperty(path) val find = self.lookupProperty(path) { JsonNull.INSTANCE }
if (find.isJsonNull) { if (find.isJsonNull) {
returnBuffer.setTo(default) returnBuffer.setTo(default)
@ -206,7 +207,7 @@ fun provideWorldObjectBindings(self: WorldObject, lua: LuaEnvironment) {
} }
table["setMaterialSpaces"] = luaFunction { spaces: Table -> table["setMaterialSpaces"] = luaFunction { spaces: Table ->
self.networkedMaterialSpaces.clear() self.declaredMaterialSpaces.clear()
for ((i, pair) in spaces) { for ((i, pair) in spaces) {
pair as Table pair as Table
@ -214,10 +215,8 @@ fun provideWorldObjectBindings(self: WorldObject, lua: LuaEnvironment) {
val position = toVector2i(indexNoYield(pair, 1L) ?: throw NullPointerException("invalid space at $i")) val position = toVector2i(indexNoYield(pair, 1L) ?: throw NullPointerException("invalid space at $i"))
val material = indexNoYield(pair, 2L) as? ByteString ?: throw NullPointerException("invalid space at $i") val material = indexNoYield(pair, 2L) as? ByteString ?: throw NullPointerException("invalid space at $i")
self.networkedMaterialSpaces.add(position to Registries.tiles.ref(material.decode())) self.declaredMaterialSpaces.add(position to Registries.tiles.ref(material.decode()))
} }
self.markSpacesDirty()
} }
table["setDamageSources"] = luaFunction { sources: Table? -> table["setDamageSources"] = luaFunction { sources: Table? ->

View File

@ -0,0 +1,16 @@
package ru.dbotthepony.kstarbound.util
class ManualLazy<T>(private val initializer: () -> T) : Lazy<T> {
private var parent: Lazy<T> = lazy(initializer)
override val value: T
get() = parent.value
override fun isInitialized(): Boolean {
return parent.isInitialized()
}
fun invalidate() {
parent = lazy(initializer)
}
}

View File

@ -2,8 +2,11 @@ package ru.dbotthepony.kstarbound.util
import com.google.gson.JsonElement import com.google.gson.JsonElement
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
import java.util.* import java.util.*
import java.util.stream.Stream import java.util.stream.Stream
import kotlin.NoSuchElementException
import kotlin.collections.Collection
fun String.sbIntern(): String { fun String.sbIntern(): String {
return Starbound.STRINGS.intern(this) return Starbound.STRINGS.intern(this)
@ -65,3 +68,7 @@ fun <C : Comparable<C>, T : Any> Stream<Pair<C, T>>.binnedChoice(value: C): Opti
.filter { it.first <= value } .filter { it.first <= value }
.findFirst().map { it.second } .findFirst().map { it.second }
} }
fun <E : IStringSerializable> Collection<E>.valueOf(value: String): E {
return firstOrNull { it.match(value) } ?: throw NoSuchElementException("$value is not a valid element")
}

View File

@ -4,12 +4,12 @@ import ru.dbotthepony.kommons.io.StreamCodec
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
enum class Direction(val normal: Vector2d, override val jsonName: String, val luaValue: Long) : IStringSerializable { enum class Direction(val normal: Vector2d, override val jsonName: String, val luaValue: Long, val isRight: Boolean, val isLeft: Boolean) : IStringSerializable {
LEFT(Vector2d.NEGATIVE_X, "left", -1L) { LEFT(Vector2d.NEGATIVE_X, "left", -1L, false, true) {
override val opposite: Direction override val opposite: Direction
get() = RIGHT get() = RIGHT
}, },
RIGHT(Vector2d.POSITIVE_X, "right", 1L) { RIGHT(Vector2d.POSITIVE_X, "right", 1L, true, false) {
override val opposite: Direction override val opposite: Direction
get() = LEFT get() = LEFT
}; };

View File

@ -14,8 +14,6 @@ import ru.dbotthepony.kstarbound.defs.EntityDamageTeam
import ru.dbotthepony.kstarbound.defs.EntityType import ru.dbotthepony.kstarbound.defs.EntityType
import ru.dbotthepony.kstarbound.defs.InteractAction import ru.dbotthepony.kstarbound.defs.InteractAction
import ru.dbotthepony.kstarbound.defs.InteractRequest import ru.dbotthepony.kstarbound.defs.InteractRequest
import ru.dbotthepony.kstarbound.defs.JsonDriven
import ru.dbotthepony.kstarbound.defs.`object`.DamageTeam
import ru.dbotthepony.kstarbound.network.syncher.InternedStringCodec import ru.dbotthepony.kstarbound.network.syncher.InternedStringCodec
import ru.dbotthepony.kstarbound.network.syncher.MasterElement import ru.dbotthepony.kstarbound.network.syncher.MasterElement
import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup
@ -28,7 +26,7 @@ import ru.dbotthepony.kstarbound.world.World
import java.io.DataOutputStream import java.io.DataOutputStream
import java.util.function.Consumer import java.util.function.Consumer
abstract class AbstractEntity(path: String) : JsonDriven(path), Comparable<AbstractEntity> { abstract class AbstractEntity : Comparable<AbstractEntity> {
abstract val position: Vector2d abstract val position: Vector2d
var entityID: Int = 0 var entityID: Int = 0

View File

@ -3,7 +3,7 @@ package ru.dbotthepony.kstarbound.world.entities
/** /**
* Monsters, NPCs, Players * Monsters, NPCs, Players
*/ */
abstract class ActorEntity(path: String) : DynamicEntity(path) { abstract class ActorEntity() : DynamicEntity() {
final override val movement: ActorMovementController = ActorMovementController() final override val movement: ActorMovementController = ActorMovementController()
abstract val statusController: StatusController abstract val statusController: StatusController
} }

View File

@ -192,7 +192,6 @@ class Animator() {
private var activeStateChanged = false private var activeStateChanged = false
private var frameChanged = false private var frameChanged = false
private var firstTime = false
var activeState: AnimatedPartsDefinition.StateType.State? = null var activeState: AnimatedPartsDefinition.StateType.State? = null
set(value) { set(value) {
@ -226,14 +225,10 @@ class Animator() {
fun set(state: String, alwaysStart: Boolean): Boolean { fun set(state: String, alwaysStart: Boolean): Boolean {
val getState = states[state] ?: return false val getState = states[state] ?: return false
// allow to reset active state if calling for first time if (activeState?.name != getState.name || alwaysStart) {
// without this client and server will disagree what state is active
// if there default state present
if (activeState != getState || alwaysStart || firstTime) {
activeState = getState activeState = getState
timer = 0.0 timer = 0.0
startedEvent.trigger() startedEvent.trigger()
firstTime = false
return true return true
} }
@ -304,8 +299,6 @@ class Animator() {
} }
fun finishAnimations() { fun finishAnimations() {
firstTime = true
while (true) { while (true) {
val active = activeState ?: break val active = activeState ?: break

View File

@ -11,7 +11,7 @@ import ru.dbotthepony.kstarbound.world.World
/** /**
* Entities with dynamics (Player, Drops, Projectiles, NPCs, etc) * Entities with dynamics (Player, Drops, Projectiles, NPCs, etc)
*/ */
abstract class DynamicEntity(path: String) : AbstractEntity(path) { abstract class DynamicEntity() : AbstractEntity() {
private var forceChunkRepos = false private var forceChunkRepos = false
override var position override var position

View File

@ -16,7 +16,7 @@ import java.io.DataInputStream
/** /**
* Players and NPCs * Players and NPCs
*/ */
abstract class HumanoidActorEntity(path: String) : ActorEntity(path) { abstract class HumanoidActorEntity() : ActorEntity() {
abstract val aimPosition: Vector2d abstract val aimPosition: Vector2d
val effects = EffectEmitter(this) val effects = EffectEmitter(this)

View File

@ -26,7 +26,7 @@ import java.io.DataOutputStream
import java.util.function.Predicate import java.util.function.Predicate
import kotlin.math.min import kotlin.math.min
class ItemDropEntity() : DynamicEntity("/") { class ItemDropEntity() : DynamicEntity() {
// int32_t, but networked as proper enum // int32_t, but networked as proper enum
// костыль (именно DEAD состояние), но требуют оригинальные клиенты // костыль (именно DEAD состояние), но требуют оригинальные клиенты
// мда. // мда.
@ -93,14 +93,6 @@ class ItemDropEntity() : DynamicEntity("/") {
state = State.INTANGIBLE state = State.INTANGIBLE
} }
override fun lookupProperty(path: JsonPath, orElse: () -> JsonElement): JsonElement {
TODO("Not yet implemented")
}
override fun setProperty0(key: JsonPath, value: JsonElement) {
TODO("Not yet implemented")
}
override val type: EntityType override val type: EntityType
get() = EntityType.ITEM_DROP get() = EntityType.ITEM_DROP

View File

@ -32,7 +32,7 @@ import java.io.DataOutputStream
import java.util.UUID import java.util.UUID
import kotlin.properties.Delegates import kotlin.properties.Delegates
class PlayerEntity() : HumanoidActorEntity("/") { class PlayerEntity() : HumanoidActorEntity() {
enum class State(override val jsonName: String) : IStringSerializable { enum class State(override val jsonName: String) : IStringSerializable {
IDLE("Idle"), IDLE("Idle"),
WALK("Walk"), WALK("Walk"),
@ -115,12 +115,4 @@ class PlayerEntity() : HumanoidActorEntity("/") {
var uuid: UUID by Delegates.notNull() var uuid: UUID by Delegates.notNull()
private set private set
override fun lookupProperty(path: JsonPath, orElse: () -> JsonElement): JsonElement {
TODO("Not yet implemented")
}
override fun setProperty0(key: JsonPath, value: JsonElement) {
TODO("Not yet implemented")
}
} }

View File

@ -1,10 +1,107 @@
package ru.dbotthepony.kstarbound.world.entities.tile package ru.dbotthepony.kstarbound.world.entities.tile
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableSet
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import com.google.gson.reflect.TypeToken
import it.unimi.dsi.fastutil.objects.ObjectArraySet
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.InteractAction
import ru.dbotthepony.kstarbound.defs.InteractRequest
import ru.dbotthepony.kstarbound.defs.actor.StatModifier
import ru.dbotthepony.kstarbound.defs.`object`.LoungeOrientation
import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition
import ru.dbotthepony.kstarbound.util.coalesceNull
import ru.dbotthepony.kstarbound.util.valueOf
import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.PIXELS_IN_STARBOUND_UNIT
import java.lang.Math.toRadians
class LoungeableObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(config) { class LoungeableObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(config) {
init { init {
isInteractive = true isInteractive = true
} }
private val sitPositions = ArrayList<Vector2d>()
private var sitFlipDirection = false
private var sitOrientation = LoungeOrientation.NONE
private var sitAngle = 0.0
private var sitCoverImage = ""
private var sitFlipImages = false
private val sitStatusEffects = ObjectArraySet<Either<StatModifier, String>>()
private val sitEffectEmitters = ObjectArraySet<String>()
private var sitEmote: String? = null
private var sitDance: String? = null
private var sitArmorCosmeticOverrides: JsonObject = JsonObject()
private var sitCursorOverride: String? = null
private fun updateSitParams() {
orientation ?: return
sitPositions.clear()
val sitPosition = lookupProperty("sitPosition")
val sitPositions = lookupProperty("sitPositions")
if (!sitPosition.isJsonNull) {
this.sitPositions.add(vectors.fromJsonTree(sitPosition) / PIXELS_IN_STARBOUND_UNIT)
} else if (!sitPositions.isJsonNull) {
vectorsList.fromJsonTree(sitPositions).forEach { this.sitPositions.add(it / PIXELS_IN_STARBOUND_UNIT) }
}
sitFlipDirection = lookupProperty("sitFlipDirection") { JsonPrimitive(false) }.asBoolean
sitOrientation = LoungeOrientation.entries.valueOf(lookupProperty("sitOrientation") { JsonPrimitive("sit") }.asString)
sitAngle = toRadians(lookupProperty("sitAngle") { JsonPrimitive(0.0) }.asDouble)
sitCoverImage = lookupProperty("sitCoverImage") { JsonPrimitive("") }.asString
sitFlipImages = lookupProperty("flipImages") { JsonPrimitive(false) }.asBoolean
sitStatusEffects.clear()
sitStatusEffects.addAll(statusEffects.fromJsonTree(lookupProperty("sitStatusEffects") { JsonArray() }))
sitEffectEmitters.clear()
lookupProperty("sitEffectEmitters") { JsonArray() }.asJsonArray.forEach { sitEffectEmitters.add(it.asString) }
sitEmote = lookupProperty("sitEmote").coalesceNull?.asString
sitDance = lookupProperty("sitDance").coalesceNull?.asString
sitArmorCosmeticOverrides = lookupProperty("sitArmorCosmeticOverrides") { JsonObject() } as JsonObject
sitCursorOverride = lookupProperty("sitCursorOverride").coalesceNull?.asString
}
override fun parametersUpdated() {
super.parametersUpdated()
updateSitParams()
}
override fun orientationUpdated() {
super.orientationUpdated()
updateSitParams()
}
override fun interact(request: InteractRequest): InteractAction {
val upstream = super.interact(request)
if (upstream.type == InteractAction.Type.NONE && sitPositions.isNotEmpty()) {
val interactOffset = if (direction.isRight) position - request.targetPos else request.targetPos - position
var index = 0
for (i in 1 until sitPositions.size) {
if ((sitPositions[i] + interactOffset).length < (sitPositions[index] + interactOffset).length) {
index = i
}
}
return InteractAction(InteractAction.Type.SIT_DOWN, entityID, JsonPrimitive(index))
}
return upstream
}
companion object {
private val vectors by lazy { Starbound.gson.getAdapter(Vector2d::class.java) }
private val vectorsList by lazy { Starbound.gson.getAdapter(object : TypeToken<ImmutableList<Vector2d>>() {}) }
private val statusEffects by lazy { Starbound.gson.getAdapter(object : TypeToken<ImmutableSet<Either<StatModifier, String>>>() {}) }
}
} }

View File

@ -22,7 +22,7 @@ import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
/** /**
* (Hopefully) Static world entities (Plants, Objects, etc), which reside on cell grid * (Hopefully) Static world entities (Plants, Objects, etc), which reside on cell grid
*/ */
abstract class TileEntity(path: String) : AbstractEntity(path) { abstract class TileEntity : AbstractEntity() {
protected val xTilePositionNet = networkedSignedInt() protected val xTilePositionNet = networkedSignedInt()
protected val yTilePositionNet = networkedSignedInt() protected val yTilePositionNet = networkedSignedInt()
@ -93,7 +93,7 @@ abstract class TileEntity(path: String) : AbstractEntity(path) {
private fun updateSpatialPosition() { private fun updateSpatialPosition() {
val spatialEntry = spatialEntry ?: return val spatialEntry = spatialEntry ?: return
spatialEntry.fixture.move(metaBoundingBox + position) spatialEntry.fixture.move(metaBoundingBox)
} }
protected open fun onPositionUpdated() { protected open fun onPositionUpdated() {

View File

@ -21,7 +21,6 @@ import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.client.StarboundClient import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.client.render.LayeredRenderer import ru.dbotthepony.kstarbound.client.render.LayeredRenderer
import ru.dbotthepony.kstarbound.defs.Drawable 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`.ObjectDefinition
import ru.dbotthepony.kstarbound.defs.`object`.ObjectOrientation import ru.dbotthepony.kstarbound.defs.`object`.ObjectOrientation
import ru.dbotthepony.kommons.gson.get import ru.dbotthepony.kommons.gson.get
@ -75,6 +74,7 @@ import ru.dbotthepony.kstarbound.lua.set
import ru.dbotthepony.kstarbound.lua.tableOf import ru.dbotthepony.kstarbound.lua.tableOf
import ru.dbotthepony.kstarbound.lua.toJson import ru.dbotthepony.kstarbound.lua.toJson
import ru.dbotthepony.kstarbound.lua.toJsonFromLua import ru.dbotthepony.kstarbound.lua.toJsonFromLua
import ru.dbotthepony.kstarbound.util.ManualLazy
import ru.dbotthepony.kstarbound.util.asStringOrNull import ru.dbotthepony.kstarbound.util.asStringOrNull
import ru.dbotthepony.kstarbound.world.Direction import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.LightCalculator import ru.dbotthepony.kstarbound.world.LightCalculator
@ -87,7 +87,7 @@ import ru.dbotthepony.kstarbound.world.entities.wire.WireConnection
import java.io.DataOutputStream import java.io.DataOutputStream
import java.util.HashMap import java.util.HashMap
open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntity(config.file?.computeDirectory() ?: "/") { open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntity() {
open fun deserialize(data: JsonObject) { open fun deserialize(data: JsonObject) {
direction = data.get("direction", directions) { Direction.LEFT } direction = data.get("direction", directions) { Direction.LEFT }
orientationIndex = data.get("orientationIndex", -1).toLong() orientationIndex = data.get("orientationIndex", -1).toLong()
@ -134,31 +134,43 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
return into return into
} }
override val metaBoundingBox: AABB by LazyData { protected val orientationLazies = ArrayList<ManualLazy<*>>()
orientation?.metaBoundBox ?: orientation?.let { AABB(it.boundingBox.mins.toDoubleVector() + Vector2d.NEGATIVE_XY, it.boundingBox.maxs.toDoubleVector() + Vector2d(2.0, 2.0)) } ?: AABB.ZERO protected val parametersLazies = ArrayList<ManualLazy<*>>()
} protected val spacesLazies = ArrayList<ManualLazy<*>>()
override val metaBoundingBox: AABB by ManualLazy {
(orientation?.metaBoundBox ?: orientation?.let { AABB(it.boundingBox.mins.toDoubleVector() + Vector2d.NEGATIVE_XY, it.boundingBox.maxs.toDoubleVector() + Vector2d(2.0, 2.0)) } ?: AABB.ZERO) + position
}.also { orientationLazies.add(it); spacesLazies.add(it) }
val parameters = NetworkedMap(InternedStringCodec, JsonElementCodec).also { val parameters = NetworkedMap(InternedStringCodec, JsonElementCodec).also {
networkGroup.upstream.add(it) networkGroup.upstream.add(it)
it.addListener { invalidate() } it.addListener { parametersUpdated() }
} }
val orientation: ObjectOrientation? get() { val orientation: ObjectOrientation? get() {
return config.value.orientations.getOrNull(orientationIndex.toInt()) return config.value.orientations.getOrNull(orientationIndex.toInt())
} }
protected val mergedJson = LazyData { protected val mergedJson = ManualLazy {
val orientation = orientation val orientation = orientation
if (orientation == null) { if (orientation == null) {
mergeJson(config.jsonObject.deepCopy(), parameters) mergeJson(config.jsonObject.deepCopy(), parameters) as JsonObject
} else { } else {
mergeJson(mergeJson(config.jsonObject.deepCopy(), orientation.json), parameters) mergeJson(mergeJson(config.jsonObject.deepCopy(), orientation.json), parameters) as JsonObject
} }
}.also { orientationLazies.add(it); parametersLazies.add(it) }
fun lookupProperty(path: JsonPath, orElse: () -> JsonElement): JsonElement {
return path.get(mergedJson.value, orElse)
} }
final override fun lookupProperty(path: JsonPath, orElse: () -> JsonElement): JsonElement { fun lookupProperty(path: String, orElse: () -> JsonElement): JsonElement {
return path.get(mergedJson.value, orElse) return mergedJson.value[path] ?: orElse.invoke()
}
fun lookupProperty(path: String): JsonElement {
return mergedJson.value[path] ?: JsonNull.INSTANCE
} }
init { init {
@ -166,36 +178,32 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
} }
var isInteractive by networkedBoolean().also { networkGroup.upstream.add(it) } var isInteractive by networkedBoolean().also { networkGroup.upstream.add(it) }
val networkedMaterialSpaces = NetworkedList(materialSpacesCodec, materialSpacesCodecLegacy).also { networkGroup.upstream.add(it) } val declaredMaterialSpaces = NetworkedList(materialSpacesCodec, materialSpacesCodecLegacy).also { networkGroup.upstream.add(it) }
private val materialSpaces0 = LazyData { private val materialSpaces0 = ManualLazy {
networkedMaterialSpaces.map { (it.first + tilePosition) to it.second } declaredMaterialSpaces.map { world.geometry.wrap(it.first + tilePosition) to it.second }
} }.also { orientationLazies.add(it); spacesLazies.add(it) }
override val materialSpaces by materialSpaces0 override val materialSpaces by materialSpaces0
private val occupySpaces0 = LazyData { override val occupySpaces: ImmutableSet<Vector2i> by ManualLazy {
(orientation?.occupySpaces ?: setOf()).stream().map { world.geometry.wrap(it + tilePosition) }.collect(ImmutableSet.toImmutableSet()) (orientation?.occupySpaces ?: setOf()).stream().map { world.geometry.wrap(it + tilePosition) }.collect(ImmutableSet.toImmutableSet())
} }.also { orientationLazies.add(it); spacesLazies.add(it) }
override val occupySpaces: ImmutableSet<Vector2i> by occupySpaces0
override val roots: Set<Vector2i> override val roots: Set<Vector2i>
get() = setOf() get() = setOf()
private val anchorPositions0 = LazyData { val anchorPositions: ImmutableSet<Vector2i> by ManualLazy {
immutableSet { immutableSet {
orientation?.anchors?.forEach { accept(it.position + tilePosition) } orientation?.anchors?.forEach { accept(it.position + tilePosition) }
} }
} }.also { orientationLazies.add(it); spacesLazies.add(it) }
val anchorPositions: ImmutableSet<Vector2i> by anchorPositions0
init { init {
networkGroup.upstream.add(xTilePositionNet) networkGroup.upstream.add(xTilePositionNet)
networkGroup.upstream.add(yTilePositionNet) networkGroup.upstream.add(yTilePositionNet)
networkedMaterialSpaces.addListener { declaredMaterialSpaces.addListener {
materialSpaces0.invalidate() materialSpaces0.invalidate()
markSpacesDirty() markSpacesDirty()
} }
@ -204,9 +212,9 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
var direction by networkedEnum(Direction.LEFT).also { networkGroup.upstream.add(it) } var direction by networkedEnum(Direction.LEFT).also { networkGroup.upstream.add(it) }
var health by networkedFloat(config.value.health).also { networkGroup.upstream.add(it) } var health by networkedFloat(config.value.health).also { networkGroup.upstream.add(it) }
private var orientationIndex by networkedPointer(-1L).also { var orientationIndex by networkedPointer(-1L).also {
networkGroup.upstream.add(it) networkGroup.upstream.add(it)
it.addListener(Runnable { invalidate() }) it.addListener(Runnable { orientationUpdated() })
} }
private val networkedRenderKeys = NetworkedMap(InternedStringCodec, InternedStringCodec).also { networkGroup.upstream.add(it) } private val networkedRenderKeys = NetworkedMap(InternedStringCodec, InternedStringCodec).also { networkGroup.upstream.add(it) }
@ -230,13 +238,13 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
} }
} }
val inputNodes: ImmutableList<WireNode> = lookupProperty(JsonPath("inputNodes")) { JsonArray() } val inputNodes: ImmutableList<WireNode> = lookupProperty("inputNodes") { JsonArray() }
.asJsonArray .asJsonArray
.stream() .stream()
.map { WireNode(vectors.fromJsonTree(it), true) } .map { WireNode(vectors.fromJsonTree(it), true) }
.collect(ImmutableList.toImmutableList()) .collect(ImmutableList.toImmutableList())
val outputNodes: ImmutableList<WireNode> = lookupProperty(JsonPath("outputNodes")) { JsonArray() } val outputNodes: ImmutableList<WireNode> = lookupProperty("outputNodes") { JsonArray() }
.asJsonArray .asJsonArray
.stream() .stream()
.map { WireNode(vectors.fromJsonTree(it), false) } .map { WireNode(vectors.fromJsonTree(it), false) }
@ -275,17 +283,13 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
lua.globals["storage"] = lua.newTable() lua.globals["storage"] = lua.newTable()
} }
val unbreakable by LazyData { val unbreakable by ManualLazy {
lookupProperty(JsonPath("unbreakable")) { JsonPrimitive(false) }.asBoolean lookupProperty("unbreakable") { JsonPrimitive(false) }.asBoolean
} }.also { parametersLazies.add(it) }
override val type: EntityType override val type: EntityType
get() = EntityType.OBJECT get() = EntityType.OBJECT
override fun setProperty0(key: JsonPath, value: JsonElement) {
TODO("Not yet implemented")
}
override fun writeNetwork(stream: DataOutputStream, isLegacy: Boolean) { override fun writeNetwork(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeBinaryString(config.key) stream.writeBinaryString(config.key)
stream.writeJsonElement(JsonObject().also { stream.writeJsonElement(JsonObject().also {
@ -296,25 +300,10 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
} }
private var frameTimer = 0.0 private var frameTimer = 0.0
private var frame = 0 private var frame = 0
set(value) {
if (field != value) {
field = value
drawablesCache.invalidate()
}
}
val flickerPeriod = config.value.flickerPeriod?.copy() val flickerPeriod = config.value.flickerPeriod?.copy()
//
// json driven properties
//
var color: TileColor by Property(JsonPath("color"), TileColor.DEFAULT)
var imagePosition: Vector2i by Property(JsonPath("imagePosition"), Vector2i.ZERO)
var animationPosition: Vector2i by Property(JsonPath("animationPosition"), Vector2i.ZERO)
// ???????? // ????????
val volumeBoundingBox: AABB get() { val volumeBoundingBox: AABB get() {
val orientation = orientation val orientation = orientation
@ -326,25 +315,14 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
} }
} }
private val drawablesCache = LazyData { protected open fun parametersUpdated() {
orientation?.drawables?.map { it.with(::getRenderParam) } ?: listOf() parametersLazies.forEach { it.invalidate() }
} }
init { protected open fun orientationUpdated() {
networkedRenderKeys.addListener { drawablesCache.invalidate() } parametersUpdated()
} spacesLazies.forEach { it.invalidate() }
orientationLazies.forEach { it.invalidate() }
val drawables: List<Drawable> by drawablesCache
private fun updateOrientation() {
setOrientation(config.value.findValidOrientation(world, tilePosition, direction))
}
fun setOrientation(index: Int) {
if (orientationIndex.toInt() == index)
return
orientationIndex = index.toLong()
val orientation = orientation val orientation = orientation
@ -353,19 +331,16 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
direction = orientation.directionAffinity direction = orientation.directionAffinity
} }
networkedMaterialSpaces.clear() declaredMaterialSpaces.clear()
networkedMaterialSpaces.addAll(orientation.materialSpaces) declaredMaterialSpaces.addAll(orientation.materialSpaces)
} else { } else {
networkedMaterialSpaces.clear() declaredMaterialSpaces.clear()
} }
} }
override fun onPositionUpdated() { override fun onPositionUpdated() {
super.onPositionUpdated() super.onPositionUpdated()
spacesLazies.forEach { it.invalidate() }
occupySpaces0.invalidate()
anchorPositions0.invalidate()
materialSpaces0.invalidate()
if (isInWorld && world.isServer) { if (isInWorld && world.isServer) {
updateMaterialSpaces(listOf()) // remove old spaces after moving before updating orientation updateMaterialSpaces(listOf()) // remove old spaces after moving before updating orientation
@ -378,9 +353,8 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
} }
} }
override fun markSpacesDirty() { private fun updateOrientation() {
super.markSpacesDirty() orientationIndex = config.value.findValidOrientation(world, tilePosition, direction).toLong()
materialSpaces0.invalidate()
} }
override fun onJoinWorld(world: World<*, *>) { override fun onJoinWorld(world: World<*, *>) {
@ -395,16 +369,16 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
direction = orientation.directionAffinity direction = orientation.directionAffinity
} }
networkedMaterialSpaces.clear() declaredMaterialSpaces.clear()
networkedMaterialSpaces.addAll(orientation.materialSpaces) declaredMaterialSpaces.addAll(orientation.materialSpaces)
} }
if (isRemote) { if (isRemote) {
} else { } else {
setImageKey("color", lookupProperty(JsonPath("color")) { JsonPrimitive("default") }.asString) setImageKey("color", lookupProperty("color") { JsonPrimitive("default") }.asString)
for ((k, v) in lookupProperty(JsonPath("animationParts")) { JsonObject() }.asJsonObject.entrySet()) { for ((k, v) in lookupProperty("animationParts") { JsonObject() }.asJsonObject.entrySet()) {
animator.setPartTag(k, "partImage", v.asString) animator.setPartTag(k, "partImage", v.asString)
} }
@ -423,7 +397,7 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
// Don't animate the initial state when first spawned IF you're dumb, which by default // Don't animate the initial state when first spawned IF you're dumb, which by default
// you would be, and don't know how to use transition and static states properly. Someday // you would be, and don't know how to use transition and static states properly. Someday
// I'll be brave and delete shit garbage entirely and we'll see what breaks. // I'll be brave and delete shit garbage entirely and we'll see what breaks.
if (lookupProperty(JsonPath("forceFinishAnimationsInInit")) { JsonPrimitive(true) }.asBoolean) { if (lookupProperty("forceFinishAnimationsInInit") { JsonPrimitive(true) }.asBoolean) {
animator.finishAnimations() animator.finishAnimations()
} }
} }
@ -435,22 +409,18 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
protected fun setImageKey(key: String, value: String) { protected fun setImageKey(key: String, value: String) {
val old = localRenderKeys.put(key, value) val old = localRenderKeys.put(key, value)
if (old != value) {
drawablesCache.invalidate()
}
if (!isRemote && networkedRenderKeys[key] != value) { if (!isRemote && networkedRenderKeys[key] != value) {
networkedRenderKeys[key] = value networkedRenderKeys[key] = value
} }
} }
private val interactAction by LazyData { private val interactAction by ManualLazy {
lookupProperty(JsonPath("interactAction")) { JsonNull.INSTANCE } lookupProperty("interactAction") { JsonNull.INSTANCE }
} }.also { parametersLazies.add(it) }
private val interactData by LazyData { private val interactData by ManualLazy {
lookupProperty(JsonPath("interactData")) { JsonNull.INSTANCE } lookupProperty("interactData") { JsonNull.INSTANCE }
} }.also { parametersLazies.add(it) }
override fun interact(request: InteractRequest): InteractAction { override fun interact(request: InteractRequest): InteractAction {
val diff = world.geometry.diff(request.sourcePos, position) val diff = world.geometry.diff(request.sourcePos, position)
@ -480,11 +450,6 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
return super.interact(request) return super.interact(request)
} }
override fun invalidate() {
super.invalidate()
drawablesCache.invalidate()
}
fun callBreak(smash: Boolean = false) { fun callBreak(smash: Boolean = false) {
} }
@ -551,44 +516,6 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
return tileHealth.isDead 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 { companion object {
private val lightColorPath = JsonPath("lightColor") private val lightColorPath = JsonPath("lightColor")
private val lightColorsPath = JsonPath("lightColors") private val lightColorsPath = JsonPath("lightColors")

View File

@ -49,15 +49,7 @@ object WorldTests {
val geometry = WorldGeometry(Vector2i(3000, 2000), true, false) val geometry = WorldGeometry(Vector2i(3000, 2000), true, false)
val index = EntityIndex(geometry) val index = EntityIndex(geometry)
val entry = index.Entry(object : AbstractEntity("/") { val entry = index.Entry(object : AbstractEntity() {
override fun lookupProperty(path: JsonPath, orElse: () -> JsonElement): JsonElement {
TODO("Not yet implemented")
}
override fun setProperty0(key: JsonPath, value: JsonElement) {
TODO("Not yet implemented")
}
override val position: Vector2d override val position: Vector2d
get() = TODO("Not yet implemented") get() = TODO("Not yet implemented")
override val type: EntityType override val type: EntityType