Archived
1
0
Fork 0

Replace DataStorage with a better concept

This commit is contained in:
Moritz Ruth 2021-01-09 16:20:05 +01:00
parent 55806e3aba
commit e1651806ef
No known key found for this signature in database
GPG key ID: AFD57E23E753841B
20 changed files with 118 additions and 202 deletions

View file

@ -70,8 +70,8 @@ class EntitiesGenerator(
private fun generateEntityStubs(types: List<JsonAny>) { private fun generateEntityStubs(types: List<JsonAny>) {
for (entity in types) { for (entity in types) {
val name = val name = CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, entity.get("name").toString())!! +
CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, entity.get("name").toString())!! + "Entity" "Entity"
val filePathRelativeToSourceRoot = "./${ENTITY_PACKAGE.replace(".", "/")}/$name.kt" val filePathRelativeToSourceRoot = "./${ENTITY_PACKAGE.replace(".", "/")}/$name.kt"
if (sourcesDir.resolve(filePathRelativeToSourceRoot).exists()) continue if (sourcesDir.resolve(filePathRelativeToSourceRoot).exists()) continue
@ -84,16 +84,6 @@ class EntitiesGenerator(
.initializer("Type") .initializer("Type")
.build() .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( .addType(
TypeSpec.companionObjectBuilder("Type") TypeSpec.companionObjectBuilder("Type")
.superclass(ClassName(ENTITY_PACKAGE, name + "Type")) .superclass(ClassName(ENTITY_PACKAGE, name + "Type"))

View file

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

View file

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

View file

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

View file

@ -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 * 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 package space.uranos.entity
import space.uranos.Position import space.uranos.Position
import space.uranos.Vector import space.uranos.Vector
import space.uranos.data.DataStorage
open class CowEntity(position: Position, override var headPitch: Float) : LivingEntity() { open class CowEntity(position: Position, override var headPitch: Float) : LivingEntity(position) {
final override val type: EntityType = Type final override val type: EntityType = Type
override var velocity: Vector = Vector.ZERO 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()
} }

View file

@ -8,11 +8,25 @@ package space.uranos.entity
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import space.uranos.Uranos import space.uranos.Uranos
import space.uranos.data.DataStorage import space.uranos.util.TickSynchronizationContainer
import space.uranos.world.World import space.uranos.world.World
import java.util.* import java.util.*
abstract class Entity internal constructor() { 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. * The UUID of this entity.
* *
@ -24,7 +38,6 @@ abstract class Entity internal constructor() {
open val uuid: UUID = UUID.randomUUID() open val uuid: UUID = UUID.randomUUID()
abstract val type: EntityType abstract val type: EntityType
abstract val dataStorage: DataStorage<out Entity>
private val worldMutex = Mutex() private val worldMutex = Mutex()
var world: World? = null; protected set 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. * An integer unique to this entity which will not be persisted, for example when the entity is serialized.
*/ */
@Suppress("LeakingThis") @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)
} }

View file

@ -6,13 +6,9 @@
package space.uranos.entity package space.uranos.entity
import space.uranos.Position import space.uranos.Position
import space.uranos.data.DataStorage
open class ItemEntity(override var position: Position) : ObjectEntity() { open class ItemEntity(override var position: Position) : ObjectEntity() {
final override val type: EntityType = Type final override val type: EntityType = Type
@Suppress("LeakingThis")
public override val dataStorage: DataStorage<ItemEntity> = DataStorage(this)
companion object Type : ItemEntityType() companion object Type : ItemEntityType()
} }

View file

@ -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 * 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 package space.uranos.entity
import space.uranos.Position import space.uranos.Position
import space.uranos.Vector import space.uranos.Vector
import space.uranos.data.DataStorage
import space.uranos.data.createDataStorageKey
abstract class LivingEntity : Entity(), Mobile { abstract class LivingEntity(position: Position) : Entity(), Mobile {
abstract var headPitch: Float // TODO: This should probably be headYaw, but wiki.vg says headPitch 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 abstract override var velocity: Vector // TODO: Move the entity every tick
abstract override val dataStorage: DataStorage<out LivingEntity>
override var position: Position override var position: Position by container.ifChanged(position) { value ->
get() = dataStorage.cast<LivingEntity>().get(DataStorageKeys.position) // TODO: Broadcast to players
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
}
} }
} }

View file

@ -7,7 +7,6 @@ package space.uranos.entity
import space.uranos.CardinalDirection import space.uranos.CardinalDirection
import space.uranos.PaintingMotive import space.uranos.PaintingMotive
import space.uranos.data.DataStorage
import space.uranos.world.VoxelLocation import space.uranos.world.VoxelLocation
class PaintingEntity( class PaintingEntity(
@ -17,8 +16,5 @@ class PaintingEntity(
) : Entity() { ) : Entity() {
override val type: EntityType = Type override val type: EntityType = Type
@Suppress("LeakingThis")
override val dataStorage: DataStorage<PaintingEntity> = DataStorage(this)
companion object Type : PaintingEntityType() companion object Type : PaintingEntityType()
} }

View file

@ -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 * 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 package space.uranos.entity
import space.uranos.Position import space.uranos.Position
import space.uranos.Vector import space.uranos.Vector
import space.uranos.data.DataStorage
import space.uranos.player.Player import space.uranos.player.Player
import space.uranos.world.World import space.uranos.world.World
@ -22,16 +19,10 @@ open class PlayerEntity(
*/ */
open val player: Player? = null, open val player: Player? = null,
override var headPitch: Float = 0f override var headPitch: Float = 0f
) : LivingEntity() { ) : LivingEntity(position) {
final override val type: EntityType = Type final override val type: EntityType = Type
override var velocity: Vector = Vector.ZERO 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!!`. * Because [world] is never `null` for player entities, you can use this property instead of writing `world!!`.
*/ */

View file

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

View file

@ -147,7 +147,6 @@ object MinecraftProtocolDataTypes {
/** /**
* Writes a [VoxelLocation] encoded as long. * Writes a [VoxelLocation] encoded as long.
*/ */
@OptIn(ExperimentalUnsignedTypes::class)
fun ByteBuf.writeVoxelLocation(value: VoxelLocation): ByteBuf { fun ByteBuf.writeVoxelLocation(value: VoxelLocation): ByteBuf {
writeLong( writeLong(
value.x.toLong() and 0x3FFFFFF shl 38 or value.x.toLong() and 0x3FFFFFF shl 38 or

View file

@ -63,7 +63,7 @@ object JoinGamePacketCodec : OutgoingPacketCodec<JoinGamePacket>(0x24, JoinGameP
"minecraft:worldgen/biome" { "minecraft:worldgen/biome" {
"type" set "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 { buildNBT {
"name" set biome.id "name" set biome.id
"id" set biome.numericID!! "id" set biome.numericID!!

View file

@ -29,7 +29,8 @@ object SpawnLivingEntityPacketCodec : OutgoingPacketCodec<SpawnLivingEntityPacke
} }
fun getPacketFromEntity(entity: LivingEntity) = SpawnLivingEntityPacket( fun getPacketFromEntity(entity: LivingEntity) = SpawnLivingEntityPacket(
entity.uid, @Suppress("DEPRECATION")
entity.numericID,
entity.uuid, entity.uuid,
entity.type, entity.type,
entity.position, entity.position,

View file

@ -30,7 +30,8 @@ object SpawnObjectEntityPacketCodec : OutgoingPacketCodec<SpawnObjectEntityPacke
} }
fun getPacketFromEntity(entity: ObjectEntity) = SpawnObjectEntityPacket( fun getPacketFromEntity(entity: ObjectEntity) = SpawnObjectEntityPacket(
entity.uid, @Suppress("DEPRECATION")
entity.numericID,
entity.uuid, entity.uuid,
entity.type, entity.type,
entity.position, entity.position,

View file

@ -31,7 +31,8 @@ object SpawnPaintingPacketCodec : OutgoingPacketCodec<SpawnPaintingPacket>(0x03,
} }
fun getPacketFromEntity(entity: PaintingEntity) = SpawnPaintingPacket( fun getPacketFromEntity(entity: PaintingEntity) = SpawnPaintingPacket(
entity.uid, @Suppress("DEPRECATION")
entity.numericID,
entity.uuid, entity.uuid,
entity.motive, entity.motive,
getCenterLocation(entity.topLeftLocation, entity.motive), getCenterLocation(entity.topLeftLocation, entity.motive),

View file

@ -11,10 +11,8 @@ import space.uranos.net.MinecraftProtocolDataTypes.writeVarInt
import space.uranos.net.packet.OutgoingPacketCodec import space.uranos.net.packet.OutgoingPacketCodec
import space.uranos.tag.Tag import space.uranos.tag.Tag
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.ExperimentalTime
import kotlin.time.toJavaDuration import kotlin.time.toJavaDuration
@OptIn(ExperimentalTime::class)
object TagsPacketCodec : object TagsPacketCodec :
OutgoingPacketCodec<TagsPacket>(0x5B, TagsPacket::class, CacheOptions(3, Duration.INFINITE.toJavaDuration())) { OutgoingPacketCodec<TagsPacket>(0x5B, TagsPacket::class, CacheOptions(3, Duration.INFINITE.toJavaDuration())) {
private val ORDER = listOf( private val ORDER = listOf(

View file

@ -124,9 +124,14 @@ class UranosServer internal constructor() : Server() {
private fun startTicking() { private fun startTicking() {
scheduler.executeRepeating(1, 0) { scheduler.executeRepeating(1, 0) {
players.forEach { it.dataStorage.tick() } players.forEach { it.container.tick() }
entities.forEach { it.dataStorage.tick() }
sessions.forEach { it.tick() } entities.forEach {
@Suppress("DEPRECATION_ERROR")
it.tick()
}
sessions.forEach { it.packetsAdapter.tick() }
} }
} }

View file

@ -79,10 +79,6 @@ class UranosSession(val channel: io.netty.channel.Channel, val server: UranosSer
scope.launch { packetsAdapter.sendNextTick(packet) } scope.launch { packetsAdapter.sendNextTick(packet) }
} }
suspend fun tick() {
packetsAdapter.tick()
}
override suspend fun sendPluginMessage(channel: String, data: ByteBuf) { override suspend fun sendPluginMessage(channel: String, data: ByteBuf) {
if (this.currentProtocol != PlayProtocol) throw IllegalStateException("The session is not using the PLAY protocol") if (this.currentProtocol != PlayProtocol) throw IllegalStateException("The session is not using the PLAY protocol")
send(OutgoingPluginMessagePacket(channel, data)) send(OutgoingPluginMessagePacket(channel, data))

View file

@ -7,14 +7,13 @@ package space.uranos.player
import space.uranos.Position import space.uranos.Position
import space.uranos.chat.TextComponent import space.uranos.chat.TextComponent
import space.uranos.data.DataStorage
import space.uranos.data.createDataStorageKey
import space.uranos.entity.PlayerEntity import space.uranos.entity.PlayerEntity
import space.uranos.net.UranosSession import space.uranos.net.UranosSession
import space.uranos.net.packet.play.ChunkDataPacket import space.uranos.net.packet.play.ChunkDataPacket
import space.uranos.net.packet.play.ChunkLightDataPacket import space.uranos.net.packet.play.ChunkLightDataPacket
import space.uranos.net.packet.play.PlayerInfoPacket import space.uranos.net.packet.play.PlayerInfoPacket
import space.uranos.net.packet.play.SelectedHotbarSlotPacket import space.uranos.net.packet.play.SelectedHotbarSlotPacket
import space.uranos.util.TickSynchronizationContainer
import space.uranos.util.clampArgument import space.uranos.util.clampArgument
import space.uranos.world.Chunk import space.uranos.world.Chunk
import space.uranos.world.VoxelLocation import space.uranos.world.VoxelLocation
@ -39,34 +38,20 @@ class UranosPlayer(
override var compassTarget: VoxelLocation, override var compassTarget: VoxelLocation,
selectedHotbarSlot: Int selectedHotbarSlot: Int
) : Player { ) : Player {
val dataStorage = DataStorage(this) val container = TickSynchronizationContainer()
object DataStorageKeys { override var selectedHotbarSlot by container.ifChanged(
val selectedHotbarSlot = createDataStorageKey("selectedHotbarSlot") { player: UranosPlayer, value: Int -> selectedHotbarSlot,
player.session.send(SelectedHotbarSlotPacket(value)) { clampArgument("selectedHotbarSlot", 0..8, it) }) {
null session.send(SelectedHotbarSlotPacket(it))
} }
val playerListName = createDataStorageKey("playerListName") { player: UranosPlayer, value: TextComponent? -> override var playerListName by container.ifChanged<TextComponent?>(TextComponent of name) { value ->
player.session.server.players.forEach { session.server.players.forEach {
it.session.sendNextTick(PlayerInfoPacket(PlayerInfoPacket.Action.UpdateDisplayName(mapOf(player.uuid to value)))) it.session.sendNextTick(PlayerInfoPacket(PlayerInfoPacket.Action.UpdateDisplayName(mapOf(uuid to value))))
}
null
} }
} }
override var selectedHotbarSlot
get() = dataStorage.get(DataStorageKeys.selectedHotbarSlot)
set(value) {
clampArgument("selectedHotbarSlot", 0..8, value)
dataStorage.set(DataStorageKeys.selectedHotbarSlot, value)
}
override var playerListName
get() = dataStorage.get(DataStorageKeys.playerListName)
set(value) = dataStorage.set(DataStorageKeys.playerListName, value)
init { init {
this.selectedHotbarSlot = selectedHotbarSlot this.selectedHotbarSlot = selectedHotbarSlot
this.playerListName = null this.playerListName = null
@ -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) } 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(ChunkLightDataPacket(it.key, it.getLightData(this))) }
chunks.forEach { session.send(ChunkDataPacket(it.key, it.getData(this))) } chunks.forEach { session.send(ChunkDataPacket(it.key, it.getData(this))) }