diff --git a/nova-api/src/main/java/xyz/xenondevs/nova/api/protection/ProtectionIntegration.java b/nova-api/src/main/java/xyz/xenondevs/nova/api/protection/ProtectionIntegration.java index 993632258d5..e1c0de1a10e 100644 --- a/nova-api/src/main/java/xyz/xenondevs/nova/api/protection/ProtectionIntegration.java +++ b/nova-api/src/main/java/xyz/xenondevs/nova/api/protection/ProtectionIntegration.java @@ -159,7 +159,7 @@ default boolean canHurtEntity(@NotNull TileEntity tileEntity, @NotNull Entity en enum ExecutionMode { /** - * The thread will not be changed in order to call the methods + * The method can be called from any thread */ NONE, @@ -170,7 +170,10 @@ enum ExecutionMode { /** * The methods are never called from the server thread + * + * @deprecated Use {@link #NONE} instead */ + @Deprecated ASYNC } diff --git a/nova-hooks/nova-hook-worldguard/src/main/kotlin/xyz/xenondevs/nova/hook/impl/worldguard/BetterBukkitOfflinePlayer.kt b/nova-hooks/nova-hook-worldguard/src/main/kotlin/xyz/xenondevs/nova/hook/impl/worldguard/BetterBukkitOfflinePlayer.kt index b3299299a6b..75559fa946c 100644 --- a/nova-hooks/nova-hook-worldguard/src/main/kotlin/xyz/xenondevs/nova/hook/impl/worldguard/BetterBukkitOfflinePlayer.kt +++ b/nova-hooks/nova-hook-worldguard/src/main/kotlin/xyz/xenondevs/nova/hook/impl/worldguard/BetterBukkitOfflinePlayer.kt @@ -44,7 +44,7 @@ internal class BetterBukkitOfflinePlayer( } override fun hasPermission(perm: String): Boolean { - return (player.isOp && platform.globalStateManager[world].opPermissions) || PermissionManager.hasPermission(bukkitWorld, player, perm) + return (player.isOp && platform.globalStateManager[world].opPermissions) || PermissionManager.hasPermission(bukkitWorld, player, perm).get() } override fun getWorld(): World { diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/integration/permission/PermissionManager.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/integration/permission/PermissionManager.kt index 0a16b007f06..8ed6f02fd7a 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/integration/permission/PermissionManager.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/integration/permission/PermissionManager.kt @@ -8,11 +8,11 @@ import org.bukkit.OfflinePlayer import org.bukkit.World import org.bukkit.entity.Player import xyz.xenondevs.nova.LOGGER -import xyz.xenondevs.nova.integration.HooksLoader import xyz.xenondevs.nova.initialize.DisableFun import xyz.xenondevs.nova.initialize.InitFun -import xyz.xenondevs.nova.initialize.InternalInitStage import xyz.xenondevs.nova.initialize.InternalInit +import xyz.xenondevs.nova.initialize.InternalInitStage +import xyz.xenondevs.nova.integration.HooksLoader import xyz.xenondevs.nova.util.MINECRAFT_SERVER import java.time.Duration import java.util.* @@ -43,10 +43,11 @@ object PermissionManager { 10, 10, 0, TimeUnit.MILLISECONDS, LinkedBlockingQueue(), - ThreadFactoryBuilder().setNameFormat("Nova Protection Worker - %s").build() + ThreadFactoryBuilder().setNameFormat("Nova Permission Worker - %s").build() ) offlinePermissionCache = Caffeine.newBuilder() + .executor(executor) .expireAfterAccess(Duration.ofMinutes(30)) .refreshAfterWrite(Duration.ofMinutes(1)) .build { hasOfflinePermission(it.world, it.player, it.permission) } @@ -60,56 +61,22 @@ object PermissionManager { executor.shutdown() } - /** - * Checks whether the player under the given [UUID][player] has the given [permission] in the given [world]. - * - * This method will use [Player.hasPermission] if the player is online and otherwise use access the offline - * permission cache. If the permission is not cached yet, this method will return false and initiate a cache load. - */ - fun hasPermission(world: World, player: UUID, permission: String): Boolean = - hasPermission(world, Bukkit.getOfflinePlayer(player), permission) - - /** - * Checks whether the given [player] has the given [permission] in the given [world]. - * - * This method will use [Player.hasPermission] if the player is online and otherwise use access the offline - * permission cache. If the permission is not cached yet, this method will return false and initiate a cache load. - */ - fun hasPermission(world: World, player: OfflinePlayer, permission: String): Boolean { - // online-player permissions are cached by the permissions plugin - if (player.isOnline) - return player.player!!.hasPermission(permission) - - val args = PermissionArgs(world, player, permission) - - // don't initiate cache loads, as that would cause lag spikes due to database access from the main thread - val cachedResult = offlinePermissionCache.getIfPresent(args) - if (cachedResult == null) { - // load cache async - offlinePermissionCache.refresh(args) - // return false as we don't know the result yet - return false - } - - return cachedResult - } - /** * Checks whether the player under the given [UUID][player] has the given [permission] in the given [world]. * * This method will use [Player.hasPermission] if the player is online and otherwise use access the offline permission cache, - * which accessed the registered [PermissionIntegrations][PermissionIntegration] asynchronously. + * which accesses the registered [PermissionIntegrations][PermissionIntegration] asynchronously. */ - fun hasPermissionAsync(world: World, player: UUID, permission: String): CompletableFuture = - hasPermissionAsync(world, Bukkit.getOfflinePlayer(player), permission) + fun hasPermission(world: World, player: UUID, permission: String): CompletableFuture = + hasPermission(world, Bukkit.getOfflinePlayer(player), permission) /** * Checks whether the given [player] has the given [permission] in the given [world]. * * This method will use [Player.hasPermission] if the player is online and otherwise use access the offline permission cache, - * which accessed the registered [PermissionIntegrations][PermissionIntegration] asynchronously. + * which accesses the registered [PermissionIntegrations][PermissionIntegration] asynchronously. */ - fun hasPermissionAsync(world: World, player: OfflinePlayer, permission: String): CompletableFuture { + fun hasPermission(world: World, player: OfflinePlayer, permission: String): CompletableFuture { // online-player permissions are cached by the permissions plugin if (player.isOnline) return CompletableFuture.completedFuture(player.player!!.hasPermission(permission)) diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/integration/protection/ProtectionManager.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/integration/protection/ProtectionManager.kt index 860f36b9bbc..717bc8018b3 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/integration/protection/ProtectionManager.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/integration/protection/ProtectionManager.kt @@ -8,6 +8,7 @@ import org.bukkit.Bukkit import org.bukkit.Location import org.bukkit.OfflinePlayer import org.bukkit.entity.Entity +import org.bukkit.entity.Player import org.bukkit.inventory.ItemStack import xyz.xenondevs.nova.NOVA_PLUGIN import xyz.xenondevs.nova.api.ApiTileEntityWrapper @@ -20,8 +21,9 @@ import xyz.xenondevs.nova.initialize.InternalInitStage import xyz.xenondevs.nova.integration.HooksLoader import xyz.xenondevs.nova.tileentity.TileEntity import xyz.xenondevs.nova.util.concurrent.CombinedBooleanFuture -import xyz.xenondevs.nova.util.concurrent.completeServerThread +import xyz.xenondevs.nova.util.concurrent.isServerThread import xyz.xenondevs.nova.util.isBetweenXZ +import xyz.xenondevs.nova.util.runTask import java.time.Duration import java.util.concurrent.CompletableFuture import java.util.concurrent.ExecutorService @@ -53,12 +55,15 @@ private data class CanUseItemTileArgs(override val tileEntity: TileEntity, val i private data class CanInteractWithEntityUserArgs(override val player: OfflinePlayer, val entity: Entity, val item: ItemStack?) : ProtectionArgs { override val location: Location = entity.location } + private data class CanInteractWithEntityTileArgs(override val tileEntity: TileEntity, val entity: Entity, val item: ItemStack?) : ProtectionArgsTileEntity { override val location: Location = entity.location } + private data class CanHurtEntityUserArgs(override val player: OfflinePlayer, val entity: Entity, val item: ItemStack?) : ProtectionArgs { override val location: Location = entity.location } + private data class CanHurtEntityTileArgs(override val tileEntity: TileEntity, val entity: Entity, val item: ItemStack?) : ProtectionArgsTileEntity { override val location: Location = entity.location } @@ -66,7 +71,7 @@ private data class CanHurtEntityTileArgs(override val tileEntity: TileEntity, va /** * Handles protection checks using registered [ProtectionIntegrations][ProtectionIntegration]. - * + * * Protection checks are cached for 60s after the last access. * If all protection integrations [can be called asynchronously][ExecutionMode], commonly used protection checks will be refreshed * every 30s. @@ -92,16 +97,13 @@ object ProtectionManager { @InitFun private fun init() { - // executor = ThreadPoolExecutor( 10, 10, 0, TimeUnit.MILLISECONDS, LinkedBlockingQueue(), ThreadFactoryBuilder().setNameFormat("Nova Protection Worker - %s").build() ) - // - // val cacheBuilder = Caffeine.newBuilder() .executor(executor) .expireAfterAccess(Duration.ofSeconds(60)) @@ -122,7 +124,6 @@ object ProtectionManager { cacheCanInteractWithEntityTile = cacheBuilder.build { checkProtection(it) { canInteractWithEntity(it.apiTileEntity, it.entity, it.item) } } cacheCanHurtEntityUser = cacheBuilder.build { checkProtection(it) { canHurtEntity(it.player, it.entity, it.item) } } cacheCanHurtEntityTile = cacheBuilder.build { checkProtection(it) { canHurtEntity(it.apiTileEntity, it.entity, it.item) } } - // } @DisableFun @@ -131,7 +132,7 @@ object ProtectionManager { } /** - * Checks if the [tileEntity] can place that [item] at that [location] + * Checks if the [tileEntity] can place that [item] at that [location]. */ suspend fun canPlace(tileEntity: TileEntity, item: ItemStack, location: Location): Boolean { if (tileEntity.owner == null) return true @@ -139,13 +140,19 @@ object ProtectionManager { } /** - * Checks if the [player] can place that [item] at that [location] + * Checks if the [player] can place that [item] at that [location]. */ suspend fun canPlace(player: OfflinePlayer, item: ItemStack, location: Location): Boolean = cacheCanPlaceUser.get(CanPlaceUserArgs(player, item.clone(), location.clone())).await() /** - * Checks if that [tileEntity] can break a block at that [location] using that [item] + * Checks if the [player] can place that [item] at that [location]. + */ + fun canPlace(player: Player, item: ItemStack, location: Location): Boolean = + cacheCanPlaceUser.get(CanPlaceUserArgs(player, item.clone(), location.clone())).get() + + /** + * Checks if that [tileEntity] can break a block at that [location] using that [item]. */ suspend fun canBreak(tileEntity: TileEntity, item: ItemStack?, location: Location): Boolean { if (tileEntity.owner == null) return true @@ -153,13 +160,19 @@ object ProtectionManager { } /** - * Checks if that [player] can break a block at that [location] using that [item] + * Checks if that [player] can break a block at that [location] using that [item]. */ suspend fun canBreak(player: OfflinePlayer, item: ItemStack?, location: Location): Boolean = cacheCanBreakUser.get(CanBreakUserArgs(player, item?.clone(), location.clone())).await() /** - * Checks if the [tileEntity] can interact with a block at that [location] using that [item] + * Checks if that [player] can break a block at that [location] using that [item]. + */ + fun canBreak(player: Player, item: ItemStack?, location: Location): Boolean = + cacheCanBreakUser.get(CanBreakUserArgs(player, item?.clone(), location.clone())).get() + + /** + * Checks if the [tileEntity] can interact with a block at that [location] using that [item]. */ suspend fun canUseBlock(tileEntity: TileEntity, item: ItemStack?, location: Location): Boolean { if (tileEntity.owner == null) return true @@ -167,13 +180,19 @@ object ProtectionManager { } /** - * Checks if the [player] can interact with a block at that [location] using that [item] + * Checks if the [player] can interact with a block at that [location] using that [item]. */ suspend fun canUseBlock(player: OfflinePlayer, item: ItemStack?, location: Location): Boolean = cacheCanUseBlockUser.get(CanUseBlockUserArgs(player, item?.clone(), location.clone())).await() /** - * Checks if the [tileEntity] can use that [item] at that [location] + * Checks if the [player] can interact with a block at that [location] using that [item]. + */ + fun canUseBlock(player: Player, item: ItemStack?, location: Location): Boolean = + cacheCanUseBlockUser.get(CanUseBlockUserArgs(player, item?.clone(), location.clone())).get() + + /** + * Checks if the [tileEntity] can use that [item] at that [location]. */ suspend fun canUseItem(tileEntity: TileEntity, item: ItemStack, location: Location): Boolean { if (tileEntity.owner == null) return true @@ -181,13 +200,19 @@ object ProtectionManager { } /** - * Checks if the [player] can use that [item] at that [location] + * Checks if the [player] can use that [item] at that [location]. */ suspend fun canUseItem(player: OfflinePlayer, item: ItemStack, location: Location): Boolean = cacheCanUseItemUser.get(CanUseItemUserArgs(player, item.clone(), location.clone())).await() /** - * Checks if the [tileEntity] can interact with the [entity] wile holding that [item] + * Checks if the [player] can use that [item] at that [location]. + */ + fun canUseItem(player: Player, item: ItemStack, location: Location): Boolean = + cacheCanUseItemUser.get(CanUseItemUserArgs(player, item.clone(), location.clone())).get() + + /** + * Checks if the [tileEntity] can interact with the [entity] wile holding that [item]. */ suspend fun canInteractWithEntity(tileEntity: TileEntity, entity: Entity, item: ItemStack?): Boolean { if (tileEntity.owner == null) return true @@ -195,13 +220,19 @@ object ProtectionManager { } /** - * Checks if the [player] can interact with the [entity] while holding that [item] + * Checks if the [player] can interact with the [entity] while holding that [item]. */ suspend fun canInteractWithEntity(player: OfflinePlayer, entity: Entity, item: ItemStack?): Boolean = cacheCanInteractWithEntityUser.get(CanInteractWithEntityUserArgs(player, entity, item?.clone())).await() /** - * Checks if the [tileEntity] can hurt the [entity] with this [item] + * Checks if the [player] can interact with the [entity] while holding that [item]. + */ + fun canInteractWithEntity(player: Player, entity: Entity, item: ItemStack?): Boolean = + cacheCanInteractWithEntityUser.get(CanInteractWithEntityUserArgs(player, entity, item?.clone())).get() + + /** + * Checks if the [tileEntity] can hurt the [entity] with this [item]. */ suspend fun canHurtEntity(tileEntity: TileEntity, entity: Entity, item: ItemStack?): Boolean { if (tileEntity.owner == null) return true @@ -209,11 +240,17 @@ object ProtectionManager { } /** - * Checks if the [player] can hurt the [entity] with this [item] + * Checks if the [player] can hurt the [entity] with this [item]. */ suspend fun canHurtEntity(player: OfflinePlayer, entity: Entity, item: ItemStack?): Boolean = cacheCanHurtEntityUser.get(CanHurtEntityUserArgs(player, entity, item?.clone())).await() + /** + * Checks if the [player] can hurt the [entity] with this [item]. + */ + fun canHurtEntity(player: Player, entity: Entity, item: ItemStack?): Boolean = + cacheCanHurtEntityUser.get(CanHurtEntityUserArgs(player, entity, item?.clone())).get() + private fun checkProtection( args: ProtectionArgs, check: ProtectionIntegration.() -> Boolean @@ -222,8 +259,25 @@ object ProtectionManager { return CompletableFuture.completedFuture(false) if (integrations.isNotEmpty()) { - val futures = checkIntegrations(check) - futures += CompletableFuture.completedFuture(!isVanillaProtected(args.player, args.location)) + val player = args.player + if (!isVanillaProtected(player, args.location)) + return CompletableFuture.completedFuture(false) + + val futures = ArrayList>() + for (integration in integrations) { + // assumes that queries for online players can be performed on the main thread + if (!player.isOnline && isServerThread && integration.executionMode != ExecutionMode.SERVER) { + // player offline, in main thread, async calls allowed -> check protection async + futures += CompletableFuture.supplyAsync({ integration.check() }, executor) + } else if (!isServerThread && integration.executionMode == ExecutionMode.SERVER) { + // not in main thread, no async calls allowed -> check protection on main thread + futures += CompletableFuture().apply { runTask { complete(integration.check()) } } + } else { + // check protection in current thread (online player, already async, or no async calls allowed) + futures += CompletableFuture.completedFuture(integration.check()) + } + } + return CombinedBooleanFuture(futures) } else { return CompletableFuture.completedFuture(!isVanillaProtected(args.player, args.location)) @@ -242,14 +296,5 @@ object ProtectionManager { && location.isBetweenXZ(spawnMin, spawnMax) } - private fun checkIntegrations(check: ProtectionIntegration.() -> Boolean): MutableList> = - integrations.mapTo(ArrayList()) { - when (it.executionMode) { - ExecutionMode.NONE -> CompletableFuture.completedFuture(it.check()) - ExecutionMode.ASYNC -> CompletableFuture().apply { completeAsync({ it.check() }, executor) } - ExecutionMode.SERVER -> CompletableFuture().apply { completeServerThread { it.check() } } - } - } - } diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/item/logic/ItemListener.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/item/logic/ItemListener.kt index 2f03566a99e..18917331841 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/item/logic/ItemListener.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/item/logic/ItemListener.kt @@ -1,6 +1,5 @@ package xyz.xenondevs.nova.item.logic -import kotlinx.coroutines.runBlocking import net.minecraft.network.protocol.game.ServerboundPlayerActionPacket import org.bukkit.entity.Player import org.bukkit.event.EventHandler @@ -51,7 +50,7 @@ internal object ItemListener : Listener, PacketListener { val item = event.item val location = event.clickedBlock?.location ?: player.location - if (item == null || runBlocking { !ProtectionManager.canUseItem(player, item, location) }) // TODO + if (item == null || !ProtectionManager.canUseItem(player, item, location)) return item.novaItem?.handleInteract(event.player, item, event.action, wrappedEvent) diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/util/FakeOnlinePlayer.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/util/FakeOnlinePlayer.kt index 8c60a59e35f..6abb2f23e4a 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/util/FakeOnlinePlayer.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/util/FakeOnlinePlayer.kt @@ -32,11 +32,11 @@ abstract class FakeOnlinePlayer( ) : Player, OfflinePlayer by offlinePlayer { override fun hasPermission(name: String): Boolean { - return PermissionManager.hasPermission(world, uniqueId, name) + return PermissionManager.hasPermission(world, uniqueId, name).get() } override fun hasPermission(perm: Permission): Boolean { - return PermissionManager.hasPermission(world, uniqueId, perm.name) + return PermissionManager.hasPermission(world, uniqueId, perm.name).get() } override fun isPermissionSet(name: String): Boolean { diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/world/block/hitbox/HitboxManager.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/world/block/hitbox/HitboxManager.kt index 06d22b0def8..03b74ae361d 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/world/block/hitbox/HitboxManager.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/world/block/hitbox/HitboxManager.kt @@ -1,9 +1,5 @@ package xyz.xenondevs.nova.world.block.hitbox -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import net.minecraft.world.level.BlockGetter import net.minecraft.world.level.ClipContext import net.minecraft.world.phys.HitResult @@ -24,9 +20,7 @@ import xyz.xenondevs.nmsutils.network.event.clientbound.ServerboundInteractPacke import xyz.xenondevs.nmsutils.network.event.registerPacketListener import xyz.xenondevs.nova.integration.protection.ProtectionManager import xyz.xenondevs.nova.player.WrappedPlayerInteractEvent -import xyz.xenondevs.nova.util.BukkitDispatcher import xyz.xenondevs.nova.util.bukkitEquipmentSlot -import xyz.xenondevs.nova.util.concurrent.runIfTrueOnSimilarThread import xyz.xenondevs.nova.util.registerEvents import xyz.xenondevs.nova.util.runTask import xyz.xenondevs.nova.util.serverLevel @@ -216,12 +210,8 @@ internal object HitboxManager : Listener, PacketListener { val relHitLoc = Vector3f(hitLoc.x - center.x, hitLoc.y - center.y, hitLoc.z - center.z) // check protection integrations - CoroutineScope(Dispatchers.Default).launch { - if (ProtectionManager.canUseBlock(player, event.item, hitLoc.toLocation(player.world))) { - withContext(BukkitDispatcher) { - handlers.forEach { it.invoke(player, event.hand!!, relHitLoc) } - } - } + if (ProtectionManager.canUseBlock(player, event.item, hitLoc.toLocation(player.world))) { + handlers.forEach { it.invoke(player, event.hand!!, relHitLoc) } } return@traverseBlocks Unit // hitbox hit, don't continue ray diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/world/block/logic/interact/BlockInteracting.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/world/block/logic/interact/BlockInteracting.kt index 38b43eb8191..9677b04834d 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/world/block/logic/interact/BlockInteracting.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/world/block/logic/interact/BlockInteracting.kt @@ -1,6 +1,5 @@ package xyz.xenondevs.nova.world.block.logic.interact -import kotlinx.coroutines.runBlocking import org.bukkit.Material import org.bukkit.block.Block import org.bukkit.entity.EntityType @@ -21,7 +20,6 @@ import xyz.xenondevs.nova.data.context.Context import xyz.xenondevs.nova.data.context.intention.ContextIntentions import xyz.xenondevs.nova.data.context.intention.ContextIntentions.BlockBreak import xyz.xenondevs.nova.data.context.param.ContextParamTypes -import xyz.xenondevs.nova.world.format.WorldDataManager import xyz.xenondevs.nova.initialize.InitFun import xyz.xenondevs.nova.initialize.InternalInit import xyz.xenondevs.nova.initialize.InternalInitStage @@ -29,6 +27,7 @@ import xyz.xenondevs.nova.integration.protection.ProtectionManager import xyz.xenondevs.nova.player.WrappedPlayerInteractEvent import xyz.xenondevs.nova.util.BlockUtils import xyz.xenondevs.nova.util.registerEvents +import xyz.xenondevs.nova.world.format.WorldDataManager import xyz.xenondevs.nova.world.pos @InternalInit( @@ -53,7 +52,7 @@ internal object BlockInteracting : Listener { val pos = event.clickedBlock!!.pos val blockState = WorldDataManager.getBlockState(pos) - if (blockState != null && runBlocking { ProtectionManager.canUseBlock(player, event.item, pos.location) }) { // TODO + if (blockState != null && ProtectionManager.canUseBlock(player, event.item, pos.location)) { val block = blockState.block val ctx = Context.intention(ContextIntentions.BlockInteract) diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/world/block/logic/place/BlockPlacing.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/world/block/logic/place/BlockPlacing.kt index e7e5ba2a1b3..305568e05e2 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/world/block/logic/place/BlockPlacing.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/world/block/logic/place/BlockPlacing.kt @@ -147,7 +147,7 @@ internal object BlockPlacing : Listener { } } - private suspend fun placeVanillaBlock(event: PlayerInteractEvent) { + private fun placeVanillaBlock(event: PlayerInteractEvent) { val player = event.player val handItem = event.item!! val placedOn = event.clickedBlock!!.pos