Replace DataStorage with a better concept
This commit is contained in:
parent
55806e3aba
commit
e1651806ef
20 changed files with 118 additions and 202 deletions
|
@ -70,8 +70,8 @@ class EntitiesGenerator(
|
|||
|
||||
private fun generateEntityStubs(types: List<JsonAny>) {
|
||||
for (entity in types) {
|
||||
val name =
|
||||
CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, entity.get("name").toString())!! + "Entity"
|
||||
val name = CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, entity.get("name").toString())!! +
|
||||
"Entity"
|
||||
|
||||
val filePathRelativeToSourceRoot = "./${ENTITY_PACKAGE.replace(".", "/")}/$name.kt"
|
||||
if (sourcesDir.resolve(filePathRelativeToSourceRoot).exists()) continue
|
||||
|
@ -84,16 +84,6 @@ class EntitiesGenerator(
|
|||
.initializer("Type")
|
||||
.build()
|
||||
)
|
||||
.addProperty(
|
||||
PropertySpec.builder(
|
||||
"dataStorage",
|
||||
ClassName("$BASE_PACKAGE.data", "DataStorage").parameterizedBy(ClassName(ENTITY_PACKAGE, name)),
|
||||
KModifier.OVERRIDE
|
||||
)
|
||||
.addAnnotation(AnnotationSpec.builder(Suppress::class).addMember("\"LeakingThis\"").build())
|
||||
.initializer("%T(this)", ClassName("$BASE_PACKAGE.data", "DataStorage"))
|
||||
.build()
|
||||
)
|
||||
.addType(
|
||||
TypeSpec.companionObjectBuilder("Type")
|
||||
.superclass(ClassName(ENTITY_PACKAGE, name + "Type"))
|
||||
|
|
|
@ -1,61 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020-2021 Moritz Ruth and Uranos contributors
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file
|
||||
*/
|
||||
|
||||
package space.uranos.data
|
||||
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
class DataStorage<ContextT : Any>(val context: ContextT) {
|
||||
private val map = HashMap<DataStorageKey<ContextT, *>, Entry<*>>()
|
||||
|
||||
inner class Entry<V>(private val key: DataStorageKey<ContextT, V>, value: V) {
|
||||
private var oldValueRef = WeakReference(value)
|
||||
private var changed = true
|
||||
|
||||
var value: V = value
|
||||
set(value) {
|
||||
val oldValue = oldValueRef.get()
|
||||
if (oldValue == value) return
|
||||
field = value
|
||||
changed = true
|
||||
}
|
||||
|
||||
suspend fun tick(): DataStorageCombinableAction<ContextT, *>? {
|
||||
var action = key.tick(context, value, changed)
|
||||
|
||||
if (changed) action = key.tickIfChanged(context, value)
|
||||
|
||||
oldValueRef = WeakReference(value)
|
||||
changed = false
|
||||
|
||||
return action
|
||||
}
|
||||
}
|
||||
|
||||
fun <V> set(key: DataStorageKey<ContextT, V>, value: V) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val entry = map[key] as Entry<V>?
|
||||
|
||||
if (entry == null) map[key] = Entry(key, value)
|
||||
else entry.value = value
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <V> get(key: DataStorageKey<ContextT, V>) = (map[key] as Entry<V>?)!!.value
|
||||
|
||||
suspend fun tick() {
|
||||
val actions = map.values.mapNotNull { it.tick() }
|
||||
|
||||
actions.groupBy { it.key }.forEach { (key, values) ->
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
key as DataStorageCombinableActionKey<ContextT, Any>
|
||||
|
||||
key.tick(context, values)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST", "NOTHING_TO_INLINE")
|
||||
inline fun <T : Any> cast() = this as DataStorage<T>
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020-2021 Moritz Ruth and Uranos contributors
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file
|
||||
*/
|
||||
|
||||
package space.uranos.data
|
||||
|
||||
data class DataStorageCombinableAction<ContextT, V>(val key: DataStorageCombinableActionKey<ContextT, V>, val value: V)
|
||||
|
||||
abstract class DataStorageCombinableActionKey<ContextT, V> {
|
||||
open suspend fun tick(context: ContextT, values: List<V>) {}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020-2021 Moritz Ruth and Uranos contributors
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file
|
||||
*/
|
||||
|
||||
package space.uranos.data
|
||||
|
||||
abstract class DataStorageKey<ContextT, V>(val name: String) {
|
||||
open suspend fun tick(context: ContextT, value: V, changed: Boolean): DataStorageCombinableAction<ContextT, *>? =
|
||||
null
|
||||
|
||||
open suspend fun tickIfChanged(context: ContextT, value: V): DataStorageCombinableAction<ContextT, *>? = null
|
||||
|
||||
override fun toString(): String = "DataStorageKey:$name"
|
||||
}
|
||||
|
||||
fun <ContextT : Any, V> createDataStorageKey(
|
||||
name: String,
|
||||
tick: suspend (context: ContextT, value: V, changed: Boolean) -> DataStorageCombinableAction<ContextT, *>? = { _, _, _ -> null },
|
||||
tickIfChanged: suspend (context: ContextT, value: V) -> DataStorageCombinableAction<ContextT, *>? = { _, _ -> null }
|
||||
) =
|
||||
object : DataStorageKey<ContextT, V>(name) {
|
||||
override suspend fun tick(context: ContextT, value: V, changed: Boolean) = tick(context, value, changed)
|
||||
override suspend fun tickIfChanged(context: ContextT, value: V) = tickIfChanged(context, value)
|
||||
}
|
|
@ -3,24 +3,14 @@
|
|||
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file
|
||||
*/
|
||||
|
||||
@file:Suppress("LeakingThis")
|
||||
|
||||
package space.uranos.entity
|
||||
|
||||
import space.uranos.Position
|
||||
import space.uranos.Vector
|
||||
import space.uranos.data.DataStorage
|
||||
|
||||
open class CowEntity(position: Position, override var headPitch: Float) : LivingEntity() {
|
||||
final override val type: EntityType = Type
|
||||
override var velocity: Vector = Vector.ZERO
|
||||
open class CowEntity(position: Position, override var headPitch: Float) : LivingEntity(position) {
|
||||
final override val type: EntityType = Type
|
||||
override var velocity: Vector = Vector.ZERO
|
||||
|
||||
@Suppress("LeakingThis")
|
||||
override val dataStorage: DataStorage<CowEntity> = DataStorage(this)
|
||||
|
||||
init {
|
||||
this.position = position
|
||||
}
|
||||
|
||||
companion object Type : CowEntityType()
|
||||
companion object Type : CowEntityType()
|
||||
}
|
||||
|
|
|
@ -8,11 +8,25 @@ package space.uranos.entity
|
|||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import space.uranos.Uranos
|
||||
import space.uranos.data.DataStorage
|
||||
import space.uranos.util.TickSynchronizationContainer
|
||||
import space.uranos.world.World
|
||||
import java.util.*
|
||||
|
||||
abstract class Entity internal constructor() {
|
||||
protected val container = TickSynchronizationContainer()
|
||||
|
||||
@Deprecated(
|
||||
"This function should only be called by the server.",
|
||||
ReplaceWith(""),
|
||||
DeprecationLevel.ERROR
|
||||
)
|
||||
suspend fun tick() {
|
||||
onTick()
|
||||
container.tick()
|
||||
}
|
||||
|
||||
open fun onTick() {}
|
||||
|
||||
/**
|
||||
* The UUID of this entity.
|
||||
*
|
||||
|
@ -24,7 +38,6 @@ abstract class Entity internal constructor() {
|
|||
open val uuid: UUID = UUID.randomUUID()
|
||||
|
||||
abstract val type: EntityType
|
||||
abstract val dataStorage: DataStorage<out Entity>
|
||||
|
||||
private val worldMutex = Mutex()
|
||||
var world: World? = null; protected set
|
||||
|
@ -51,5 +64,6 @@ abstract class Entity internal constructor() {
|
|||
* An integer unique to this entity which will not be persisted, for example when the entity is serialized.
|
||||
*/
|
||||
@Suppress("LeakingThis")
|
||||
val uid: Int = Uranos.registerEntity(this)
|
||||
@Deprecated("This is an internal value that you usually should not use.", ReplaceWith("uuid"))
|
||||
val numericID: Int = Uranos.registerEntity(this)
|
||||
}
|
||||
|
|
|
@ -6,13 +6,9 @@
|
|||
package space.uranos.entity
|
||||
|
||||
import space.uranos.Position
|
||||
import space.uranos.data.DataStorage
|
||||
|
||||
open class ItemEntity(override var position: Position) : ObjectEntity() {
|
||||
final override val type: EntityType = Type
|
||||
|
||||
@Suppress("LeakingThis")
|
||||
public override val dataStorage: DataStorage<ItemEntity> = DataStorage(this)
|
||||
|
||||
companion object Type : ItemEntityType()
|
||||
}
|
||||
|
|
|
@ -3,28 +3,16 @@
|
|||
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file
|
||||
*/
|
||||
|
||||
@file:Suppress("LeakingThis")
|
||||
|
||||
package space.uranos.entity
|
||||
|
||||
import space.uranos.Position
|
||||
import space.uranos.Vector
|
||||
import space.uranos.data.DataStorage
|
||||
import space.uranos.data.createDataStorageKey
|
||||
|
||||
abstract class LivingEntity : Entity(), Mobile {
|
||||
abstract var headPitch: Float // TODO: This should probably be headYaw, but wiki.vg says headPitch
|
||||
abstract override var velocity: Vector
|
||||
abstract override val dataStorage: DataStorage<out LivingEntity>
|
||||
abstract class LivingEntity(position: Position) : Entity(), Mobile {
|
||||
abstract var headPitch: Float // TODO: This should probably be headYaw, but wiki.vg says headPitch. And it is only used in the SpawnLivingEntity packet
|
||||
abstract override var velocity: Vector // TODO: Move the entity every tick
|
||||
|
||||
override var position: Position
|
||||
get() = dataStorage.cast<LivingEntity>().get(DataStorageKeys.position)
|
||||
set(value) = dataStorage.cast<LivingEntity>().set(DataStorageKeys.position, value)
|
||||
|
||||
object DataStorageKeys {
|
||||
val position = createDataStorageKey("position") { entity: LivingEntity, value: Position ->
|
||||
// TODO: Send the position to players
|
||||
null
|
||||
}
|
||||
override var position: Position by container.ifChanged(position) { value ->
|
||||
// TODO: Broadcast to players
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ package space.uranos.entity
|
|||
|
||||
import space.uranos.CardinalDirection
|
||||
import space.uranos.PaintingMotive
|
||||
import space.uranos.data.DataStorage
|
||||
import space.uranos.world.VoxelLocation
|
||||
|
||||
class PaintingEntity(
|
||||
|
@ -17,8 +16,5 @@ class PaintingEntity(
|
|||
) : Entity() {
|
||||
override val type: EntityType = Type
|
||||
|
||||
@Suppress("LeakingThis")
|
||||
override val dataStorage: DataStorage<PaintingEntity> = DataStorage(this)
|
||||
|
||||
companion object Type : PaintingEntityType()
|
||||
}
|
||||
|
|
|
@ -3,13 +3,10 @@
|
|||
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file
|
||||
*/
|
||||
|
||||
@file:Suppress("LeakingThis")
|
||||
|
||||
package space.uranos.entity
|
||||
|
||||
import space.uranos.Position
|
||||
import space.uranos.Vector
|
||||
import space.uranos.data.DataStorage
|
||||
import space.uranos.player.Player
|
||||
import space.uranos.world.World
|
||||
|
||||
|
@ -22,16 +19,10 @@ open class PlayerEntity(
|
|||
*/
|
||||
open val player: Player? = null,
|
||||
override var headPitch: Float = 0f
|
||||
) : LivingEntity() {
|
||||
) : LivingEntity(position) {
|
||||
final override val type: EntityType = Type
|
||||
override var velocity: Vector = Vector.ZERO
|
||||
|
||||
override val dataStorage: DataStorage<PlayerEntity> = DataStorage(this)
|
||||
|
||||
init {
|
||||
this.position = position
|
||||
}
|
||||
|
||||
/**
|
||||
* Because [world] is never `null` for player entities, you can use this property instead of writing `world!!`.
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright 2020-2021 Moritz Ruth and Uranos contributors
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file
|
||||
*/
|
||||
|
||||
package space.uranos.util
|
||||
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.lang.ref.WeakReference
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
// If it is too resource-intensive to create a new object for every property, one instance could be used per TickSynchronizationContainer
|
||||
class TickSynchronizationContainer {
|
||||
private val delegates = mutableSetOf<Delegate<*>>()
|
||||
|
||||
suspend fun tick() {
|
||||
coroutineScope {
|
||||
delegates.forEach { launch { it.tick() } }
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> ifChanged(
|
||||
initialValue: T,
|
||||
onChange: ((value: T) -> Unit)? = null,
|
||||
onTick: suspend (value: T) -> Unit
|
||||
): Delegate<T> =
|
||||
Delegate(initialValue, onChange, { value, changed -> if (changed) onTick(value) }).also { delegates.add(it) }
|
||||
|
||||
operator fun <T> invoke(
|
||||
initialValue: T,
|
||||
onChange: ((value: T) -> Unit)? = null,
|
||||
onTick: suspend (value: T, changed: Boolean) -> Unit
|
||||
): Delegate<T> = Delegate(initialValue, onChange, onTick).also { delegates.add(it) }
|
||||
|
||||
inner class Delegate<T>(
|
||||
initialValue: T,
|
||||
private val onChange: ((value: T) -> Unit)?,
|
||||
private val onTick: suspend (value: T, changed: Boolean) -> Unit
|
||||
) {
|
||||
operator fun getValue(thisRef: Any?, property: KProperty<*>): T = value
|
||||
|
||||
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
|
||||
val oldValue = oldValueRef.get()
|
||||
if (oldValue == value) return
|
||||
onChange?.invoke(value)
|
||||
this.value = value
|
||||
changed = true
|
||||
}
|
||||
|
||||
private var oldValueRef = WeakReference(initialValue)
|
||||
private var changed = true
|
||||
|
||||
var value: T = initialValue
|
||||
|
||||
suspend fun tick() {
|
||||
onTick(value, changed)
|
||||
|
||||
oldValueRef = WeakReference(value)
|
||||
changed = false
|
||||
}
|
||||
}
|
||||
}
|
|
@ -147,7 +147,6 @@ object MinecraftProtocolDataTypes {
|
|||
/**
|
||||
* Writes a [VoxelLocation] encoded as long.
|
||||
*/
|
||||
@OptIn(ExperimentalUnsignedTypes::class)
|
||||
fun ByteBuf.writeVoxelLocation(value: VoxelLocation): ByteBuf {
|
||||
writeLong(
|
||||
value.x.toLong() and 0x3FFFFFF shl 38 or
|
||||
|
|
|
@ -63,7 +63,7 @@ object JoinGamePacketCodec : OutgoingPacketCodec<JoinGamePacket>(0x24, JoinGameP
|
|||
|
||||
"minecraft:worldgen/biome" {
|
||||
"type" set "minecraft:worldgen/biome"
|
||||
"value" set Uranos.biomeRegistry.items.values.mapIndexed { index, biome ->
|
||||
"value" set Uranos.biomeRegistry.items.values.map { biome ->
|
||||
buildNBT {
|
||||
"name" set biome.id
|
||||
"id" set biome.numericID!!
|
||||
|
|
|
@ -29,7 +29,8 @@ object SpawnLivingEntityPacketCodec : OutgoingPacketCodec<SpawnLivingEntityPacke
|
|||
}
|
||||
|
||||
fun getPacketFromEntity(entity: LivingEntity) = SpawnLivingEntityPacket(
|
||||
entity.uid,
|
||||
@Suppress("DEPRECATION")
|
||||
entity.numericID,
|
||||
entity.uuid,
|
||||
entity.type,
|
||||
entity.position,
|
||||
|
|
|
@ -30,7 +30,8 @@ object SpawnObjectEntityPacketCodec : OutgoingPacketCodec<SpawnObjectEntityPacke
|
|||
}
|
||||
|
||||
fun getPacketFromEntity(entity: ObjectEntity) = SpawnObjectEntityPacket(
|
||||
entity.uid,
|
||||
@Suppress("DEPRECATION")
|
||||
entity.numericID,
|
||||
entity.uuid,
|
||||
entity.type,
|
||||
entity.position,
|
||||
|
|
|
@ -31,7 +31,8 @@ object SpawnPaintingPacketCodec : OutgoingPacketCodec<SpawnPaintingPacket>(0x03,
|
|||
}
|
||||
|
||||
fun getPacketFromEntity(entity: PaintingEntity) = SpawnPaintingPacket(
|
||||
entity.uid,
|
||||
@Suppress("DEPRECATION")
|
||||
entity.numericID,
|
||||
entity.uuid,
|
||||
entity.motive,
|
||||
getCenterLocation(entity.topLeftLocation, entity.motive),
|
||||
|
|
|
@ -11,10 +11,8 @@ import space.uranos.net.MinecraftProtocolDataTypes.writeVarInt
|
|||
import space.uranos.net.packet.OutgoingPacketCodec
|
||||
import space.uranos.tag.Tag
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.toJavaDuration
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
object TagsPacketCodec :
|
||||
OutgoingPacketCodec<TagsPacket>(0x5B, TagsPacket::class, CacheOptions(3, Duration.INFINITE.toJavaDuration())) {
|
||||
private val ORDER = listOf(
|
||||
|
|
|
@ -124,9 +124,14 @@ class UranosServer internal constructor() : Server() {
|
|||
|
||||
private fun startTicking() {
|
||||
scheduler.executeRepeating(1, 0) {
|
||||
players.forEach { it.dataStorage.tick() }
|
||||
entities.forEach { it.dataStorage.tick() }
|
||||
sessions.forEach { it.tick() }
|
||||
players.forEach { it.container.tick() }
|
||||
|
||||
entities.forEach {
|
||||
@Suppress("DEPRECATION_ERROR")
|
||||
it.tick()
|
||||
}
|
||||
|
||||
sessions.forEach { it.packetsAdapter.tick() }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -79,10 +79,6 @@ class UranosSession(val channel: io.netty.channel.Channel, val server: UranosSer
|
|||
scope.launch { packetsAdapter.sendNextTick(packet) }
|
||||
}
|
||||
|
||||
suspend fun tick() {
|
||||
packetsAdapter.tick()
|
||||
}
|
||||
|
||||
override suspend fun sendPluginMessage(channel: String, data: ByteBuf) {
|
||||
if (this.currentProtocol != PlayProtocol) throw IllegalStateException("The session is not using the PLAY protocol")
|
||||
send(OutgoingPluginMessagePacket(channel, data))
|
||||
|
|
|
@ -7,14 +7,13 @@ package space.uranos.player
|
|||
|
||||
import space.uranos.Position
|
||||
import space.uranos.chat.TextComponent
|
||||
import space.uranos.data.DataStorage
|
||||
import space.uranos.data.createDataStorageKey
|
||||
import space.uranos.entity.PlayerEntity
|
||||
import space.uranos.net.UranosSession
|
||||
import space.uranos.net.packet.play.ChunkDataPacket
|
||||
import space.uranos.net.packet.play.ChunkLightDataPacket
|
||||
import space.uranos.net.packet.play.PlayerInfoPacket
|
||||
import space.uranos.net.packet.play.SelectedHotbarSlotPacket
|
||||
import space.uranos.util.TickSynchronizationContainer
|
||||
import space.uranos.util.clampArgument
|
||||
import space.uranos.world.Chunk
|
||||
import space.uranos.world.VoxelLocation
|
||||
|
@ -39,33 +38,19 @@ class UranosPlayer(
|
|||
override var compassTarget: VoxelLocation,
|
||||
selectedHotbarSlot: Int
|
||||
) : Player {
|
||||
val dataStorage = DataStorage(this)
|
||||
val container = TickSynchronizationContainer()
|
||||
|
||||
object DataStorageKeys {
|
||||
val selectedHotbarSlot = createDataStorageKey("selectedHotbarSlot") { player: UranosPlayer, value: Int ->
|
||||
player.session.send(SelectedHotbarSlotPacket(value))
|
||||
null
|
||||
}
|
||||
|
||||
val playerListName = createDataStorageKey("playerListName") { player: UranosPlayer, value: TextComponent? ->
|
||||
player.session.server.players.forEach {
|
||||
it.session.sendNextTick(PlayerInfoPacket(PlayerInfoPacket.Action.UpdateDisplayName(mapOf(player.uuid to value))))
|
||||
}
|
||||
|
||||
null
|
||||
}
|
||||
override var selectedHotbarSlot by container.ifChanged(
|
||||
selectedHotbarSlot,
|
||||
{ clampArgument("selectedHotbarSlot", 0..8, it) }) {
|
||||
session.send(SelectedHotbarSlotPacket(it))
|
||||
}
|
||||
|
||||
override var selectedHotbarSlot
|
||||
get() = dataStorage.get(DataStorageKeys.selectedHotbarSlot)
|
||||
set(value) {
|
||||
clampArgument("selectedHotbarSlot", 0..8, value)
|
||||
dataStorage.set(DataStorageKeys.selectedHotbarSlot, value)
|
||||
override var playerListName by container.ifChanged<TextComponent?>(TextComponent of name) { value ->
|
||||
session.server.players.forEach {
|
||||
it.session.sendNextTick(PlayerInfoPacket(PlayerInfoPacket.Action.UpdateDisplayName(mapOf(uuid to value))))
|
||||
}
|
||||
|
||||
override var playerListName
|
||||
get() = dataStorage.get(DataStorageKeys.playerListName)
|
||||
set(value) = dataStorage.set(DataStorageKeys.playerListName, value)
|
||||
}
|
||||
|
||||
init {
|
||||
this.selectedHotbarSlot = selectedHotbarSlot
|
||||
|
@ -99,7 +84,7 @@ class UranosPlayer(
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun sendChunksAndLight() {
|
||||
private suspend fun sendChunksAndLight() {
|
||||
val chunks = currentlyViewedChunks.sortedBy { abs(it.key.x) + abs(it.key.z) }
|
||||
chunks.forEach { session.send(ChunkLightDataPacket(it.key, it.getLightData(this))) }
|
||||
chunks.forEach { session.send(ChunkDataPacket(it.key, it.getData(this))) }
|
||||
|
|
Reference in a new issue