diff --git a/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/PylonCore.kt b/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/PylonCore.kt index 906e000a3..e0f080ba2 100644 --- a/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/PylonCore.kt +++ b/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/PylonCore.kt @@ -9,6 +9,7 @@ import com.github.shynixn.mccoroutine.bukkit.ticks import io.github.pylonmc.pylon.core.addon.PylonAddon import io.github.pylonmc.pylon.core.block.* import io.github.pylonmc.pylon.core.block.base.* +import io.github.pylonmc.pylon.core.block.base.PylonFallingBlock.PylonFallingBlockEntity import io.github.pylonmc.pylon.core.command.ROOT_COMMAND import io.github.pylonmc.pylon.core.command.ROOT_COMMAND_PY_ALIAS import io.github.pylonmc.pylon.core.config.Config @@ -55,6 +56,7 @@ import org.bukkit.Material import org.bukkit.NamespacedKey import org.bukkit.configuration.file.YamlConfiguration import org.bukkit.entity.Display +import org.bukkit.entity.FallingBlock import org.bukkit.entity.ItemDisplay import org.bukkit.permissions.Permission import org.bukkit.permissions.PermissionDefault @@ -202,6 +204,8 @@ object PylonCore : JavaPlugin(), PylonAddon { PylonEntity.register(FluidIntersectionDisplay.KEY) PylonEntity.register(FluidPipeDisplay.KEY) + PylonEntity.register(PylonFallingBlock.KEY) + PylonBlock.register(FluidSectionMarker.KEY, Material.STRUCTURE_VOID) PylonBlock.register(FluidIntersectionMarker.KEY, Material.STRUCTURE_VOID) diff --git a/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/block/BlockListener.kt b/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/block/BlockListener.kt index 2ef627927..c8877a568 100644 --- a/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/block/BlockListener.kt +++ b/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/block/BlockListener.kt @@ -14,6 +14,7 @@ import io.github.pylonmc.pylon.core.item.PylonItem import io.github.pylonmc.pylon.core.item.research.Research.Companion.canUse import io.github.pylonmc.pylon.core.util.damageItem import io.github.pylonmc.pylon.core.util.isFakeEvent +import io.github.pylonmc.pylon.core.util.position.position import io.papermc.paper.datacomponent.DataComponentTypes import io.papermc.paper.event.block.* import io.papermc.paper.event.entity.EntityCompostItemEvent @@ -22,7 +23,7 @@ import org.bukkit.GameMode import org.bukkit.Material import org.bukkit.block.Container import org.bukkit.block.Hopper -import org.bukkit.entity.minecart.HopperMinecart +import org.bukkit.entity.FallingBlock import org.bukkit.event.Event import org.bukkit.event.EventHandler import org.bukkit.event.EventPriority @@ -31,13 +32,11 @@ import org.bukkit.event.block.* import org.bukkit.event.block.BellRingEvent import org.bukkit.event.enchantment.EnchantItemEvent import org.bukkit.event.enchantment.PrepareItemEnchantEvent +import org.bukkit.event.entity.EntityChangeBlockEvent +import org.bukkit.event.entity.EntityDropItemEvent import org.bukkit.event.entity.EntityExplodeEvent -import org.bukkit.event.inventory.BrewingStandFuelEvent -import org.bukkit.event.inventory.FurnaceBurnEvent -import org.bukkit.event.inventory.FurnaceExtractEvent -import org.bukkit.event.inventory.InventoryMoveItemEvent -import org.bukkit.event.inventory.InventoryOpenEvent -import org.bukkit.event.inventory.InventoryPickupItemEvent +import org.bukkit.event.entity.EntityRemoveEvent +import org.bukkit.event.inventory.* import org.bukkit.event.player.PlayerBucketEmptyEvent import org.bukkit.event.player.PlayerInteractEvent import org.bukkit.event.player.PlayerTakeLecternBookEvent @@ -95,6 +94,68 @@ internal object BlockListener : Listener { } } + private val fallMap = HashMap>(); + + @EventHandler(ignoreCancelled = true) + private fun entityBlockChange(event: EntityChangeBlockEvent) { + val entity = event.entity + + if (entity !is FallingBlock) return + + val block = event.block + if (!entity.isInWorld) { + val pylonBlock = BlockStorage.get(block) ?: return + val pylonFallingBlock = pylonBlock as? PylonFallingBlock + if (pylonFallingBlock == null) { + event.isCancelled = true + return + } + + val blockPdc = PylonBlock.serialize(pylonBlock, block.chunk.persistentDataContainer.adapterContext) + val fallingEntity = PylonFallingBlock.PylonFallingBlockEntity(pylonBlock.schema, blockPdc, block.position, entity) + pylonFallingBlock.onFallStart(event, fallingEntity) + if (!event.isCancelled) { + BlockStorage.deleteBlock(block.position) + EntityStorage.add(fallingEntity) + // save this here as the entity storage is going to nuke it if the item drops + fallMap[entity.uniqueId] = Pair(pylonFallingBlock, fallingEntity) + } + } else { + val pylonEntity = EntityStorage.get(entity) as? PylonFallingBlock.PylonFallingBlockEntity ?: return + val pylonBlock = BlockStorage.loadBlock(block.position, pylonEntity.blockSchema, pylonEntity.blockData) as PylonFallingBlock + + pylonBlock.onFallStop(event, pylonEntity) + } + } + + @EventHandler + private fun entityDespawn(event: EntityRemoveEvent) { + // DESPAWN = Fell and created block ; OUT_OF_WORLD = Fell and dropped item + if (event.cause != EntityRemoveEvent.Cause.DESPAWN) return + val entity = event.entity + if (entity !is FallingBlock) return + fallMap.remove(entity.uniqueId) + } + + @EventHandler(ignoreCancelled = true) + private fun fallingBlockDrop(event: EntityDropItemEvent) { + val entity = event.entity + + if (entity !is FallingBlock) return + + val (pylonFallingBlock, pylonFallingEntity) = fallMap[entity.uniqueId] ?: return + fallMap.remove(entity.uniqueId) + + val relativeItem = pylonFallingBlock.onItemDrop(event, pylonFallingEntity) + if (event.isCancelled) return + if (relativeItem == null) { + event.isCancelled = true + return + } + + event.itemDrop.itemStack = relativeItem + } + @EventHandler(ignoreCancelled = true) private fun blockRemove(event: BlockBreakEvent) { if (BlockStorage.isPylonBlock(event.block)) { diff --git a/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/block/BlockStorage.kt b/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/block/BlockStorage.kt index 106eafe8d..67848717d 100644 --- a/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/block/BlockStorage.kt +++ b/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/block/BlockStorage.kt @@ -27,6 +27,8 @@ import org.bukkit.event.Listener import org.bukkit.event.world.ChunkLoadEvent import org.bukkit.event.world.ChunkUnloadEvent import org.bukkit.inventory.ItemStack +import org.bukkit.persistence.PersistentDataContainer +import org.bukkit.persistence.PersistentDataType import java.util.Collections import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.locks.ReentrantReadWriteLock @@ -61,7 +63,7 @@ import kotlin.random.Random */ object BlockStorage : Listener { - private val pylonBlocksKey = pylonKey("blocks") + val pylonBlocksKey = pylonKey("blocks") // Access to blocks, blocksByChunk, blocksById fields must be synchronized // to prevent them briefly going out of sync @@ -262,6 +264,47 @@ object BlockStorage : Listener { return block } + /** + * Manually loads Pylon block data at a location from a persistent data container that contains the serialised block. Only call on the main thread. + * + * @return The block that was loaded, or null if the block loading was cancelled + * + * @throws IllegalArgumentException if the chunk of the given [blockPosition] is not + * loaded, the block already contains a Pylon block + */ + @JvmStatic + fun loadBlock( + blockPosition: BlockPosition, + schema: PylonBlockSchema, + pdcData: PersistentDataContainer + ): PylonBlock? { + val context = BlockCreateContext.ManualLoading(blockPosition.block) + val block = blockPosition.block + pdcData.set(PylonBlock.pylonBlockPositionKey, PersistentDataType.LONG, blockPosition.asLong) + + require(block.chunk.isLoaded) { "You can only place Pylon blocks in loaded chunks" } + require(!isPylonBlock(block)) { "You cannot place a new Pylon block in place of an existing Pylon blocks" } + + if (!PrePylonBlockPlaceEvent(block, schema, context).callEvent()) return null + if (context.shouldSetType) { + block.type = schema.material + } + + val pyBlock = PylonBlock.deserialize(block.world, pdcData)!! + + lockBlockWrite { + check(blockPosition.chunk in blocksByChunk) { "Chunk '${blockPosition.chunk}' must be loaded" } + blocks[blockPosition] = pyBlock + blocksByKey.getOrPut(schema.key, ::mutableListOf).add(pyBlock) + blocksByChunk[blockPosition.chunk]!!.add(pyBlock) + } + + BlockTextureEngine.insert(pyBlock) + PylonBlockPlaceEvent(block, pyBlock, context).callEvent() + + return pyBlock + } + /** * Creates a new Pylon block. Only call on the main thread. * diff --git a/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/block/PylonBlock.kt b/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/block/PylonBlock.kt index 8677ad4a7..138e2587e 100644 --- a/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/block/PylonBlock.kt +++ b/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/block/PylonBlock.kt @@ -315,8 +315,11 @@ open class PylonBlock internal constructor(val block: Block) { @JvmStatic val pylonBlockTextureEntityKey = pylonKey("pylon_block_texture_entity") - private val pylonBlockKeyKey = pylonKey("pylon_block_key") - private val pylonBlockPositionKey = pylonKey("position") + @JvmStatic + val pylonBlockKeyKey = pylonKey("pylon_block_key") + + @JvmStatic + val pylonBlockPositionKey = pylonKey("position") @get:JvmStatic val Block.pylonBlock: PylonBlock? diff --git a/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/block/base/PylonEntityHolderBlock.kt b/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/block/base/PylonEntityHolderBlock.kt index 965b11bd4..f7b6031cf 100644 --- a/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/block/base/PylonEntityHolderBlock.kt +++ b/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/block/base/PylonEntityHolderBlock.kt @@ -108,9 +108,12 @@ interface PylonEntityHolderBlock { @ApiStatus.Internal companion object : Listener { - private val entityKey = pylonKey("entity_holder_entity_uuids") - private val blockKey = pylonKey("entity_holder_block") - private val entityType = PylonSerializers.MAP.mapTypeFrom(PylonSerializers.STRING, PylonSerializers.UUID) + @JvmStatic + val entityKey = pylonKey("entity_holder_entity_uuids") + @JvmStatic + val blockKey = pylonKey("entity_holder_block") + @JvmStatic + val entityType = PylonSerializers.MAP.mapTypeFrom(PylonSerializers.STRING, PylonSerializers.UUID) @JvmSynthetic internal val holders = IdentityHashMap>() diff --git a/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/block/base/PylonFallingBlock.kt b/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/block/base/PylonFallingBlock.kt new file mode 100644 index 000000000..30fa6114f --- /dev/null +++ b/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/block/base/PylonFallingBlock.kt @@ -0,0 +1,90 @@ +package io.github.pylonmc.pylon.core.block.base + +import io.github.pylonmc.pylon.core.PylonCore +import io.github.pylonmc.pylon.core.block.BlockStorage +import io.github.pylonmc.pylon.core.block.PylonBlock +import io.github.pylonmc.pylon.core.block.PylonBlockSchema +import io.github.pylonmc.pylon.core.datatypes.NamespacedKeyPersistentDataType +import io.github.pylonmc.pylon.core.datatypes.PylonSerializers +import io.github.pylonmc.pylon.core.entity.PylonEntity +import io.github.pylonmc.pylon.core.registry.PylonRegistry +import io.github.pylonmc.pylon.core.util.position.BlockPosition +import org.bukkit.NamespacedKey +import org.bukkit.entity.FallingBlock +import org.bukkit.event.entity.EntityChangeBlockEvent +import org.bukkit.event.entity.EntityDropItemEvent +import org.bukkit.persistence.PersistentDataContainer + +/** + * Interface meant to be used for all pylon blocks affected by gravity, like sand, gravel etc. + * + * If you implement this interface, and the block can't fall, then the methods will never be called. + * + * Beware of how you modify the passed data in the entity, because of the order of serialization and deserialization + * some modification need to be applied directly to the PDC stored in the entity, or they will be lost. + * + * Also at some steps, the entity or the block might not exist yet in their relative storage, + * handle this interface with caution as it needs to handle state using internals most of the time. + * + * As a suggestion, don't make important blocks with lots of data affected by gravity, + * or it might become a nightmare to apply the correct changes. + */ +interface PylonFallingBlock { + /** + * When calling this, the entity doesn't exist yet in [io.github.pylonmc.pylon.core.entity.EntityStorage] + * Called after serialization + */ + fun onFallStart(event: EntityChangeBlockEvent, spawnedEntity: PylonFallingBlockEntity) + + /** + * Called after deserialization + * Cancelling the event at this step does nothing, and the entity is about to be removed + */ + fun onFallStop(event: EntityChangeBlockEvent, entity: PylonFallingBlockEntity) + + /** + * When called the block doesn't exist in the world and in [BlockStorage] + */ + fun onItemDrop(event: EntityDropItemEvent, entity: PylonFallingBlockEntity) = PylonRegistry.ITEMS[(this as PylonBlock).key]?.getItemStack() + + class PylonFallingBlockEntity : PylonEntity { + val fallStartPosition: BlockPosition + val blockSchema: PylonBlockSchema + val blockData: PersistentDataContainer + + constructor(blockSchema: PylonBlockSchema, blockData: PersistentDataContainer, fallingStart: BlockPosition, entity: FallingBlock) : super(KEY, entity) { + this.blockSchema = blockSchema + this.blockData = blockData + this.fallStartPosition = fallingStart + } + + constructor(entity: FallingBlock) : super(entity) { + val pdc = entity.persistentDataContainer + + val fallingBlockType = pdc.get(FALLING_BLOCK_TYPE, NamespacedKeyPersistentDataType)!! + this.blockSchema = PylonRegistry.BLOCKS[fallingBlockType]!! + this.blockData = pdc.get(FALLING_BLOCK_DATA, PylonSerializers.TAG_CONTAINER)!! + this.fallStartPosition = pdc.get(FALLING_BLOCK_START, PylonSerializers.BLOCK_POSITION)!! + } + + override fun write(pdc: PersistentDataContainer) { + pdc.set(FALLING_BLOCK_TYPE, NamespacedKeyPersistentDataType, blockSchema.key) + pdc.set(FALLING_BLOCK_DATA, PylonSerializers.TAG_CONTAINER, blockData) + pdc.set(FALLING_BLOCK_START, PylonSerializers.BLOCK_POSITION, fallStartPosition) + } + } + + companion object { + @JvmField + val KEY = NamespacedKey(PylonCore, "falling_pylon_block") + + @JvmField + val FALLING_BLOCK_DATA = NamespacedKey(PylonCore, "falling_pylon_block_data") + + @JvmField + val FALLING_BLOCK_TYPE = NamespacedKey(PylonCore, "falling_pylon_block_type") + + @JvmField + val FALLING_BLOCK_START = NamespacedKey(PylonCore, "falling_pylon_block_start") + } +} diff --git a/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/block/context/BlockCreateContext.kt b/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/block/context/BlockCreateContext.kt index 94c5713e8..a32dd7947 100644 --- a/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/block/context/BlockCreateContext.kt +++ b/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/block/context/BlockCreateContext.kt @@ -81,4 +81,11 @@ interface BlockCreateContext { override val item: ItemStack? = null, override val shouldSetType: Boolean = true ) : BlockCreateContext + + @JvmRecord + data class ManualLoading @JvmOverloads constructor( + override val block: Block, + override val item: ItemStack? = null, + override val shouldSetType: Boolean = true + ) : BlockCreateContext } \ No newline at end of file