diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/Registered.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/Registered.kt index f034b7484..84879c5f0 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/Registered.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/Registered.kt @@ -4,14 +4,8 @@ import world.gregs.voidps.engine.entity.character.Character import world.gregs.voidps.engine.entity.character.npc.NPC import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.item.floor.FloorItem -import world.gregs.voidps.engine.event.Event -import world.gregs.voidps.engine.event.Priority -import world.gregs.voidps.engine.event.on -import world.gregs.voidps.engine.event.onWorld -import world.gregs.voidps.engine.event.onFloorItem -import world.gregs.voidps.engine.event.onCharacter -import world.gregs.voidps.engine.event.onNPC -import world.gregs.voidps.engine.event.wildcardEquals +import world.gregs.voidps.engine.entity.obj.GameObject +import world.gregs.voidps.engine.event.* object Registered : Event @@ -39,6 +33,10 @@ fun floorItemSpawn(block: suspend Registered.(FloorItem) -> Unit) { onFloorItem(block = block) } +fun objectSpawn(block: suspend Registered.(GameObject) -> Unit) { + onObject(block = block) +} + fun worldSpawn(block: suspend () -> Unit) { onWorld { block.invoke() diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/Unregistered.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/Unregistered.kt index ba0ce093d..937b24ed8 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/Unregistered.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/Unregistered.kt @@ -4,12 +4,8 @@ import world.gregs.voidps.engine.entity.character.Character import world.gregs.voidps.engine.entity.character.npc.NPC import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.item.floor.FloorItem -import world.gregs.voidps.engine.event.Event -import world.gregs.voidps.engine.event.Priority -import world.gregs.voidps.engine.event.on -import world.gregs.voidps.engine.event.onFloorItem -import world.gregs.voidps.engine.event.onCharacter -import world.gregs.voidps.engine.event.onNPC +import world.gregs.voidps.engine.entity.obj.GameObject +import world.gregs.voidps.engine.event.* object Unregistered : Event @@ -28,3 +24,7 @@ fun characterDespawn(block: suspend Unregistered.(Character) -> Unit) { fun floorItemDespawn(block: suspend Unregistered.(FloorItem) -> Unit) { onFloorItem(block = block) } + +fun objectDespawn(block: suspend Unregistered.(GameObject) -> Unit) { + onObject(block = block) +} diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/obj/GameObject.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/obj/GameObject.kt index 383cd3305..c8fabf115 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/obj/GameObject.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/obj/GameObject.kt @@ -1,10 +1,12 @@ package world.gregs.voidps.engine.entity.obj +import org.koin.mp.KoinPlatformTools import world.gregs.voidps.cache.definition.data.ObjectDefinition import world.gregs.voidps.engine.client.update.batch.ZoneBatchUpdates import world.gregs.voidps.engine.data.definition.AnimationDefinitions import world.gregs.voidps.engine.data.definition.ObjectDefinitions import world.gregs.voidps.engine.entity.Entity +import world.gregs.voidps.engine.event.EventDispatcher import world.gregs.voidps.engine.get import world.gregs.voidps.network.encode.zone.ObjectAnimation import world.gregs.voidps.type.Distance @@ -14,7 +16,7 @@ import world.gregs.voidps.type.Tile * Interactive Object */ @JvmInline -value class GameObject(internal val packed: Long) : Entity { +value class GameObject(internal val packed: Long) : Entity, EventDispatcher { constructor(id: Int, x: Int, y: Int, level: Int, shape: Int, rotation: Int) : this(pack(id, x, y, level, shape, rotation)) @@ -53,7 +55,11 @@ value class GameObject(internal val packed: Long) : Entity { ) override fun toString(): String { - return "GameObject(id=$id, intId=$intId, tile=$tile, shape=$shape, rotation=$rotation)" + return if (KoinPlatformTools.defaultContext().getOrNull() == null) { + "GameObject(intId=$intId, tile=$tile, shape=$shape, rotation=$rotation)" + } else { + "GameObject(id=$id, intId=$intId, tile=$tile, shape=$shape, rotation=$rotation)" + } } companion object { diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/obj/GameObjects.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/obj/GameObjects.kt index 8d9dcf1e2..8f04e3e6c 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/obj/GameObjects.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/obj/GameObjects.kt @@ -5,6 +5,8 @@ import world.gregs.voidps.cache.definition.data.ObjectDefinition import world.gregs.voidps.engine.client.ui.chat.toInt import world.gregs.voidps.engine.client.update.batch.ZoneBatchUpdates import world.gregs.voidps.engine.data.definition.ObjectDefinitions +import world.gregs.voidps.engine.entity.Registered +import world.gregs.voidps.engine.entity.Unregistered import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.get import world.gregs.voidps.engine.map.collision.GameObjectCollision @@ -19,6 +21,7 @@ import world.gregs.voidps.type.Zone * "original" objects refer to those [set] on game load from the cache or map file * "temporary" objects are [add]ed or [remove]ed with a reset timer * "permanent" objects are [add]ed or [remove]ed without a reset timer (but don't persist after server restart) + * Note: [Registered] and [Unregistered] events are only emitted for temporary and permanent objects, original objects that are added or removed do not emit events. * @param storeUnused store non-interactive and objects without configs for debugging and content dev (uses ~240MB more ram). */ class GameObjects( @@ -63,15 +66,17 @@ class GameObjects( } size++ } else { - // Remove original (if exists) map.add(obj, REPLACED) - if (original > 0 && !replaced(original)) { - val originalObj = GameObject(id(original), obj.x, obj.y, obj.level, shape(original), rotation(original)) - batches.add(obj.tile.zone, ObjectRemoval(obj.tile.id, originalObj.shape, originalObj.rotation)) - if (collision) { - collisions.modify(originalObj, add = false) + if (replaced(original)) { + // Remove replacements (if exists) + val current = replacements[obj.index] + if (current != null) { + val currentObj = remove(current, obj, collision) + currentObj.emit(Unregistered) } - size-- + } else if (original > 0) { + // Remove original (if exists) + remove(original, obj, collision) } // Add replacement @@ -81,9 +86,20 @@ class GameObjects( collisions.modify(obj, add = true) } size++ + obj.emit(Registered) } } + private fun remove(objectValue: Int, obj: GameObject, collision: Boolean): GameObject { + val gameObject = GameObject(id(objectValue), obj.x, obj.y, obj.level, shape(objectValue), rotation(objectValue)) + batches.add(obj.tile.zone, ObjectRemoval(obj.tile.id, gameObject.shape, gameObject.rotation)) + if (collision) { + collisions.modify(gameObject, add = false) + } + size-- + return gameObject + } + /** * Sets the original placement of a game object */ @@ -130,6 +146,7 @@ class GameObjects( collisions.modify(obj, add = false) } size-- + obj.emit(Unregistered) // Re-add original (if exists) map.remove(obj, REPLACED) if (original > 1) { @@ -154,7 +171,15 @@ class GameObjects( /** * Replaces [original] object with [id], optionally reverting after [ticks] */ - fun replace(original: GameObject, id: String, tile: Tile = original.tile, shape: Int = original.shape, rotation: Int = original.rotation, ticks: Int = NEVER, collision: Boolean = true): GameObject { + fun replace( + original: GameObject, + id: String, + tile: Tile = original.tile, + shape: Int = original.shape, + rotation: Int = original.rotation, + ticks: Int = NEVER, + collision: Boolean = true + ): GameObject { val replacement = GameObject(definitions.get(id).id, tile, shape, rotation) replace(original, replacement, ticks, collision) return replacement @@ -334,12 +359,14 @@ class GameObjects( private const val REPLACED = 0x1 private fun empty(value: Int) = value == -1 || value == 0 + /** * Value represents an objects id, shape and rotation plus and extra bit for whether the object has been [REPLACED] or removed. */ internal fun value(replaced: Boolean, id: Int, shape: Int, rotation: Int): Int { return replaced.toInt() or (rotation shl 1) + (shape shl 3) + (id shl 8) } + private fun id(value: Int): Int = value shr 8 and 0x1ffff private fun shape(value: Int): Int = value shr 3 and 0x1f private fun rotation(value: Int): Int = value shr 1 and 0x3 diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/event/EventStore.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/event/EventStore.kt index 030bcc58c..64679dfd9 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/event/EventStore.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/event/EventStore.kt @@ -8,6 +8,7 @@ import world.gregs.voidps.engine.entity.character.Character import world.gregs.voidps.engine.entity.character.npc.NPC import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.item.floor.FloorItem +import world.gregs.voidps.engine.entity.obj.GameObject import kotlin.coroutines.CoroutineContext import kotlin.reflect.KClass @@ -88,6 +89,11 @@ class EventStore : CoroutineScope { } var events = EventStore() private set + + fun setEvents(eventStore: EventStore) { + this.events = eventStore + } + private val parents = Object2ObjectOpenHashMap(mapOf( Character::class to listOf(Player::class.simpleName, NPC::class.simpleName) )) @@ -115,4 +121,7 @@ inline fun onFloorItem(noinline condition: E.(FloorItem) -> addEvent(condition, priority, block) inline fun onWorld(noinline condition: E.(World) -> Boolean = { true }, priority: Priority = Priority.MEDIUM, noinline block: suspend E.(World) -> Unit) = + addEvent(condition, priority, block) + +inline fun onObject(noinline condition: E.(GameObject) -> Boolean = { true }, priority: Priority = Priority.MEDIUM, noinline block: suspend E.(GameObject) -> Unit) = addEvent(condition, priority, block) \ No newline at end of file diff --git a/engine/src/test/kotlin/world/gregs/voidps/engine/entity/obj/GameObjectsTest.kt b/engine/src/test/kotlin/world/gregs/voidps/engine/entity/obj/GameObjectsTest.kt index 6b832b752..084ffd803 100644 --- a/engine/src/test/kotlin/world/gregs/voidps/engine/entity/obj/GameObjectsTest.kt +++ b/engine/src/test/kotlin/world/gregs/voidps/engine/entity/obj/GameObjectsTest.kt @@ -1,13 +1,14 @@ package world.gregs.voidps.engine.entity.obj -import io.mockk.every -import io.mockk.mockk -import io.mockk.verify +import io.mockk.* import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import world.gregs.voidps.cache.definition.data.ObjectDefinition import world.gregs.voidps.engine.client.update.batch.ZoneBatchUpdates import world.gregs.voidps.engine.data.definition.ObjectDefinitions +import world.gregs.voidps.engine.entity.Registered +import world.gregs.voidps.engine.entity.Unregistered +import world.gregs.voidps.engine.event.EventStore import world.gregs.voidps.type.Tile import world.gregs.voidps.network.encode.zone.ObjectAddition import world.gregs.voidps.network.encode.zone.ObjectRemoval @@ -20,6 +21,7 @@ class GameObjectsTest { private lateinit var objects: GameObjects private lateinit var updates: ZoneBatchUpdates + private lateinit var events: EventStore @BeforeEach fun setup() { @@ -29,6 +31,8 @@ class GameObjectsTest { every { definitions.get("test2") } returns ObjectDefinition(456) updates = mockk(relaxed = true) objects = GameObjects(mockk(relaxed = true), updates, definitions, storeUnused = true) + events = spyk(EventStore()) + EventStore.setEvents(events) } @Test @@ -42,6 +46,9 @@ class GameObjectsTest { assertNull(objects.getLayer(Tile(10, 10, 1), ObjectLayer.GROUND)) objects.clear() assertNull(objects.getLayer(obj.tile, ObjectLayer.GROUND)) + verify(exactly = 0) { + events.emit(obj, any()) + } } @Test @@ -57,6 +64,9 @@ class GameObjectsTest { verify { updates.add(obj.tile.zone, ObjectRemoval(tile = obj.tile.id, type = ObjectShape.CENTRE_PIECE_STRAIGHT, rotation = 1)) } + verify(exactly = 0) { + events.emit(obj, any()) + } } @Test @@ -70,8 +80,10 @@ class GameObjectsTest { objects.reset(obj.tile.zone) assertNull(objects.getLayer(obj.tile, ObjectLayer.GROUND)) assertFalse(objects.contains(obj)) - verify { + verifyOrder { updates.add(obj.tile.zone, ObjectAddition(tile = obj.tile.id, id = 1234, type = ObjectShape.CENTRE_PIECE_STRAIGHT, rotation = 1)) + events.emit(obj, Registered) + events.emit(obj, Unregistered) } } @@ -93,10 +105,13 @@ class GameObjectsTest { assertNull(objects.getLayer(obj.tile, ObjectLayer.GROUND)) assertFalse(objects.contains(obj)) assertFalse(objects.contains(override)) - verify { + verifyOrder { updates.add(obj.tile.zone, ObjectAddition(tile = obj.tile.id, id = 1234, type = ObjectShape.CENTRE_PIECE_STRAIGHT, rotation = 1)) + events.emit(obj, Registered) updates.add(obj.tile.zone, ObjectAddition(tile = obj.tile.id, id = 4321, type = ObjectShape.CENTRE_PIECE_STRAIGHT, rotation = 0)) + events.emit(override, Registered) updates.add(obj.tile.zone, ObjectRemoval(tile = obj.tile.id, type = ObjectShape.CENTRE_PIECE_STRAIGHT, rotation = 0)) + events.emit(override, Unregistered) } } @@ -112,10 +127,12 @@ class GameObjectsTest { objects.remove(obj) assertEquals(original, objects.getLayer(obj.tile, ObjectLayer.GROUND)) - verify { + verifyOrder { updates.add(obj.tile.zone, ObjectRemoval(tile = obj.tile.id, type = ObjectShape.CENTRE_PIECE_STRAIGHT, rotation = 1)) updates.add(obj.tile.zone, ObjectAddition(tile = obj.tile.id, id = 1234, type = ObjectShape.CENTRE_PIECE_STRAIGHT, rotation = 0)) + events.emit(obj, Registered) updates.add(obj.tile.zone, ObjectRemoval(tile = obj.tile.id, type = ObjectShape.CENTRE_PIECE_STRAIGHT, rotation = 0)) + events.emit(obj, Unregistered) updates.add(obj.tile.zone, ObjectAddition(tile = obj.tile.id, id = 123, type = ObjectShape.CENTRE_PIECE_STRAIGHT, rotation = 1)) } } @@ -136,12 +153,19 @@ class GameObjectsTest { objects.remove(override) assertEquals(original, objects.getLayer(obj.tile, ObjectLayer.GROUND)) - verify { + verifyOrder { + // Add 1234 updates.add(obj.tile.zone, ObjectRemoval(tile = obj.tile.id, type = ObjectShape.CENTRE_PIECE_STRAIGHT, rotation = 1)) updates.add(obj.tile.zone, ObjectAddition(tile = obj.tile.id, id = 1234, type = ObjectShape.CENTRE_PIECE_STRAIGHT, rotation = 0)) + events.emit(obj, Registered) + // Add 4321 updates.add(obj.tile.zone, ObjectRemoval(tile = obj.tile.id, type = ObjectShape.CENTRE_PIECE_STRAIGHT, rotation = 0)) + events.emit(obj, Unregistered) updates.add(obj.tile.zone, ObjectAddition(tile = obj.tile.id, id = 4321, type = ObjectShape.CENTRE_PIECE_STRAIGHT, rotation = 0)) + events.emit(override, Registered) + // Remove 4321 updates.add(obj.tile.zone, ObjectRemoval(tile = obj.tile.id, type = ObjectShape.CENTRE_PIECE_STRAIGHT, rotation = 0)) + events.emit(override, Unregistered) updates.add(obj.tile.zone, ObjectAddition(tile = obj.tile.id, id = 123, type = ObjectShape.CENTRE_PIECE_STRAIGHT, rotation = 1)) } } @@ -154,6 +178,10 @@ class GameObjectsTest { objects.timers.run() } assertFalse(objects.contains(obj)) + verifyOrder { + events.emit(obj, Registered) + events.emit(obj, Unregistered) + } } @Test @@ -166,6 +194,11 @@ class GameObjectsTest { objects.timers.run() } assertTrue(objects.contains(obj)) + verifyOrder { + events.emit(obj, Registered) + events.emit(obj, Unregistered) + events.emit(obj, Registered) + } } @Test @@ -180,6 +213,12 @@ class GameObjectsTest { } assertTrue(objects.contains(obj)) assertFalse(objects.contains(replacement)) + verifyOrder { + events.emit(obj, Registered) + events.emit(obj, Unregistered) + events.emit(replacement, Registered) + events.emit(obj, Registered) + } } @Test @@ -194,5 +233,9 @@ class GameObjectsTest { } assertTrue(objects.contains(original)) assertFalse(objects.contains(replacement)) + verifyOrder { + events.emit(replacement, Registered) + events.emit(replacement, Unregistered) + } } } \ No newline at end of file