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>) {
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"))

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

View file

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

View file

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

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

View file

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

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
*/
@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!!`.
*/

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.
*/
@OptIn(ExperimentalUnsignedTypes::class)
fun ByteBuf.writeVoxelLocation(value: VoxelLocation): ByteBuf {
writeLong(
value.x.toLong() and 0x3FFFFFF shl 38 or

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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,34 +38,20 @@ 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
override var selectedHotbarSlot by container.ifChanged(
selectedHotbarSlot,
{ clampArgument("selectedHotbarSlot", 0..8, it) }) {
session.send(SelectedHotbarSlotPacket(it))
}
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 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 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 {
this.selectedHotbarSlot = selectedHotbarSlot
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) }
chunks.forEach { session.send(ChunkLightDataPacket(it.key, it.getLightData(this))) }
chunks.forEach { session.send(ChunkDataPacket(it.key, it.getData(this))) }