summaryrefslogtreecommitdiff
path: root/src/main/kotlin/io/dico/parcels2/listener/ParcelListeners.kt
blob: e87dd68318a5f2600515b8bb59e07c69a40de9ff (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
package io.dico.parcels2.listener

import gnu.trove.TLongCollection
import gnu.trove.set.hash.TLongHashSet
import io.dico.dicore.Formatting
import io.dico.dicore.ListenerMarker
import io.dico.dicore.RegistratorListener
import io.dico.parcels2.*
import io.dico.parcels2.storage.Storage
import io.dico.parcels2.util.ext.*
import io.dico.parcels2.util.math.*
import org.bukkit.Location
import org.bukkit.Material
import org.bukkit.Material.*
import org.bukkit.World
import org.bukkit.block.Biome
import org.bukkit.block.Block
import org.bukkit.block.data.Directional
import org.bukkit.block.data.type.Bed
import org.bukkit.entity.*
import org.bukkit.entity.minecart.ExplosiveMinecart
import org.bukkit.event.EventPriority
import org.bukkit.event.EventPriority.NORMAL
import org.bukkit.event.block.*
import org.bukkit.event.entity.*
import org.bukkit.event.hanging.HangingBreakByEntityEvent
import org.bukkit.event.hanging.HangingBreakEvent
import org.bukkit.event.hanging.HangingPlaceEvent
import org.bukkit.event.inventory.InventoryInteractEvent
import org.bukkit.event.player.*
import org.bukkit.event.vehicle.VehicleMoveEvent
import org.bukkit.event.weather.WeatherChangeEvent
import org.bukkit.event.world.ChunkLoadEvent
import org.bukkit.event.world.StructureGrowEvent
import org.bukkit.inventory.InventoryHolder
import java.util.EnumSet

class ParcelListeners(
    val parcelProvider: ParcelProvider,
    val entityTracker: ParcelEntityTracker,
    val storage: Storage
) {
    private fun canBuildOnArea(user: Player, area: Parcel?) =
        if (area == null) user.hasPermBuildAnywhere else area.canBuild(user)

    private fun canInteract(user: Player, area: Parcel?, interactClass: String) =
        canBuildOnArea(user, area) || (area != null && area.interactableConfig(interactClass))

    /**
     * Get the world and parcel that the block resides in
     * the parcel is nullable, and often named area because that means path.
     * returns null if not in a registered parcel world - should always return in that case to not affect other worlds.
     */
    private fun getWorldAndArea(block: Block): Pair<ParcelWorld, Parcel?>? {
        val world = parcelProvider.getWorld(block.world) ?: return null
        return world to world.getParcelAt(block)
    }


    /*
     * Prevents players from entering plots they are banned from
     */
    @field:ListenerMarker(priority = NORMAL)
    val onPlayerMoveEvent = RegistratorListener<PlayerMoveEvent> l@{ event ->
        val user = event.player
        if (user.hasPermBanBypass) return@l
        val toLoc = event.to
        val parcel = parcelProvider.getParcelAt(toLoc) ?: return@l

        if (!parcel.canEnterFast(user)) {
            val region = parcel.world.blockManager.getRegion(parcel.id)
            val dimension = region.getFirstUncontainedDimensionOf(Vec3i(event.from))

            if (dimension == null) {
                user.teleport(parcel.homeLocation)
                user.sendParcelMessage(nopermit = true, message = "You are banned from this parcel")

            } else {
                val speed = getPlayerSpeed(user)
                val from = Vec3d(event.from)
                val to = Vec3d(toLoc).with(dimension, from[dimension])

                var newTo = to
                dimension.otherDimensions.forEach {
                    val delta = to[it] - from[it]
                    newTo = newTo.add(it, delta * 100 * if (it == Dimension.Y) 0.5 else speed)
                }

                event.to = Location(
                    toLoc.world,
                    newTo.x, newTo.y.clampMin(0.0).clampMax(255.0), newTo.z,
                    toLoc.yaw, toLoc.pitch
                )
            }
        }
    }

    /*
     * Prevents players from breaking blocks outside of their parcels
     * Prevents containers from dropping their contents when broken, if configured
     */
    @field:ListenerMarker(priority = NORMAL)
    val onBlockBreakEvent = RegistratorListener<BlockBreakEvent> l@{ event ->
        val (world, area) = getWorldAndArea(event.block) ?: return@l
        if (!canBuildOnArea(event.player, area)) {
            event.isCancelled = true; return@l
        }

        if (!world.options.dropEntityItems) {
            val state = event.block.state
            if (state is InventoryHolder) {
                state.inventory.clear()
                state.update()
            }
        }
    }

    /*
     * Prevents players from placing blocks outside of their parcels
     */
    @field:ListenerMarker(priority = NORMAL)
    val onBlockPlaceEvent = RegistratorListener<BlockPlaceEvent> l@{ event ->
        val (_, area) = getWorldAndArea(event.block) ?: return@l
        if (!canBuildOnArea(event.player, area)) {
            event.isCancelled = true
        }

        area?.updateOwnerSign()
    }

    /*
     * Control pistons
     */
    @field:ListenerMarker(priority = NORMAL)
    val onBlockPistonExtendEvent = RegistratorListener<BlockPistonExtendEvent> l@{ event ->
        checkPistonMovement(event, event.blocks)
    }

    @field:ListenerMarker(priority = NORMAL)
    val onBlockPistonRetractEvent = RegistratorListener<BlockPistonRetractEvent> l@{ event ->
        checkPistonMovement(event, event.blocks)
    }

    // Doing some unnecessary optimizations here..
    //@formatter:off
    private inline fun Column(x: Int, z: Int): Long = x.toLong() or (z.toLong().shl(32))

    private inline val Long.columnX get() = and(0xFFFF_FFFFL).toInt()
    private inline val Long.columnZ get() = ushr(32).and(0xFFFF_FFFFL).toInt()
    private inline fun TLongCollection.troveForEach(block: (Long) -> Unit) = iterator().let { while (it.hasNext()) block(it.next()) }
    //@formatter:on
    private fun checkPistonMovement(event: BlockPistonEvent, blocks: List<Block>) {
        val world = parcelProvider.getWorld(event.block.world) ?: return
        val direction = event.direction
        val columns = TLongHashSet(blocks.size * 2)

        blocks.forEach {
            columns.add(Column(it.x, it.z))
            it.getRelative(direction).let { columns.add(Column(it.x, it.z)) }
        }

        columns.troveForEach {
            val area = world.getParcelAt(it.columnX, it.columnZ)
            if (area == null || area.hasBlockVisitors) {
                event.isCancelled = true
                return
            }
        }
    }

    /*
     * Prevents explosions if enabled by the configs for that world
     */
    @field:ListenerMarker(priority = NORMAL)
    val onExplosionPrimeEvent = RegistratorListener<ExplosionPrimeEvent> l@{ event ->
        val (world, area) = getWorldAndArea(event.entity.location.block) ?: return@l
        if (area != null && area.hasBlockVisitors) {
            event.radius = 0F; event.isCancelled = true
        } else if (world.options.disableExplosions) {
            event.radius = 0F
        }
    }

    /*
     * Prevents creepers and tnt minecarts from exploding if explosions are disabled
     */
    @field:ListenerMarker(priority = NORMAL)
    val onEntityExplodeEvent = RegistratorListener<EntityExplodeEvent> l@{ event ->
        entityTracker.untrack(event.entity)
        val world = parcelProvider.getWorld(event.entity.world) ?: return@l
        if (world.options.disableExplosions || world.getParcelAt(event.entity).let { it != null && it.hasBlockVisitors }) {
            event.isCancelled = true
        }
    }

    /*
     * Prevents liquids from flowing out of plots
     */
    @field:ListenerMarker(priority = NORMAL)
    val onBlockFromToEvent = RegistratorListener<BlockFromToEvent> l@{ event ->
        val (_, area) = getWorldAndArea(event.toBlock) ?: return@l
        if (area == null || area.hasBlockVisitors) event.isCancelled = true
    }

    private val bedTypes = EnumSet.copyOf(getMaterialsWithWoolColorPrefix("BED").toList())
    /*
     * Prevents players from placing liquids, using flint and steel, changing redstone components,
     * using inputs (unless allowed by the plot),
     * and using items disabled in the configuration for that world.
     * Prevents player from using beds in HELL or SKY biomes if explosions are disabled.
     */
    @Suppress("NON_EXHAUSTIVE_WHEN")
    @field:ListenerMarker(priority = NORMAL, ignoreCancelled = false)
    val onPlayerInteractEvent = RegistratorListener<PlayerInteractEvent> l@{ event ->
        val user = event.player
        val world = parcelProvider.getWorld(user.world) ?: return@l
        val clickedBlock = event.clickedBlock
        val parcel = clickedBlock?.let { world.getParcelAt(it) }

        if (!user.hasPermBuildAnywhere && parcel != null && !parcel.canEnter(user)) {
            user.sendParcelMessage(nopermit = true, message = "You cannot interact with parcels you're banned from")
            event.isCancelled = true; return@l
        }

        when (event.action) {
            Action.RIGHT_CLICK_BLOCK -> run {
                if (event.isCancelled) return@l
                val type = clickedBlock.type

                val interactableClass = Interactables[type]
                if (interactableClass != null && !parcel.effectiveInteractableConfig.isInteractable(type) && (parcel == null || !parcel.canBuild(user))) {
                    user.sendParcelMessage(nopermit = true, message = "You cannot interact with ${interactableClass.name} here")
                    event.isCancelled = true
                    return@l
                }

                if (bedTypes.contains(type)) {
                    val bed = clickedBlock.blockData as Bed
                    val head = if (bed.part == Bed.Part.FOOT) clickedBlock.getRelative(bed.facing) else clickedBlock
                    when (head.biome) {
                        Biome.NETHER, Biome.THE_END -> {
                            if (world.options.disableExplosions) {
                                user.sendParcelMessage(nopermit = true, message = "You cannot use this bed because it would explode")
                                event.isCancelled = true; return@l
                            }
                        }
                    }

                    if (!canBuildOnArea(user, parcel)) {
                        user.sendParcelMessage(nopermit = true, message = "You may not sleep here")
                        event.isCancelled = true; return@l
                    }
                }

                onPlayerRightClick(event, world, parcel)

                if (!event.isCancelled && parcel == null) {
                    world.blockManager.getParcelForInfoBlockInteraction(Vec3i(clickedBlock), type, event.blockFace)
                        ?.apply { user.sendMessage(Formatting.GREEN + infoString) }
                }
            }

            Action.RIGHT_CLICK_AIR -> onPlayerRightClick(event, world, parcel)
            Action.PHYSICAL -> if (!event.isCancelled && !canBuildOnArea(user, parcel)) {
                if (clickedBlock.type == Material.TURTLE_EGG) {
                    event.isCancelled = true; return@l
                }

                if (!(parcel != null && parcel.interactableConfig("pressure_plates"))) {
                    user.sendParcelMessage(nopermit = true, message = "You cannot use inputs in this parcel")
                    event.isCancelled = true; return@l
                }
            }
        }
    }

    // private val blockPlaceInteractItems = EnumSet.of(LAVA_BUCKET, WATER_BUCKET, BUCKET, FLINT_AND_STEEL)

    @Suppress("NON_EXHAUSTIVE_WHEN")
    private fun onPlayerRightClick(event: PlayerInteractEvent, world: ParcelWorld, parcel: Parcel?) {
        if (event.hasItem()) {
            val item = event.item.type
            if (world.options.blockedItems.contains(item)) {
                event.player.sendParcelMessage(nopermit = true, message = "You cannot use this item because it is disabled in this world")
                event.isCancelled = true; return
            }

            when (item) {
                LAVA_BUCKET, WATER_BUCKET, BUCKET, FLINT_AND_STEEL -> {
                    val block = event.clickedBlock.getRelative(event.blockFace)
                    val otherParcel = world.getParcelAt(block)
                    if (!canBuildOnArea(event.player, otherParcel)) {
                        event.isCancelled = true
                    }
                }
            }
        }
    }

    /*
     * Prevents players from breeding mobs, entering or opening boats/minecarts,
     * rotating item frames, doing stuff with leashes, and putting stuff on armor stands.
     */
    @Suppress("NON_EXHAUSTIVE_WHEN")
    @field:ListenerMarker(priority = NORMAL)
    val onPlayerInteractEntityEvent = RegistratorListener<PlayerInteractEntityEvent> l@{ event ->
        val (_, area) = getWorldAndArea(event.rightClicked.location.block) ?: return@l
        if (canBuildOnArea(event.player, area)) return@l
        when (event.rightClicked.type) {
            EntityType.BOAT,
            EntityType.MINECART,
            EntityType.MINECART_CHEST,
            EntityType.MINECART_COMMAND,
            EntityType.MINECART_FURNACE,
            EntityType.MINECART_HOPPER,
            EntityType.MINECART_MOB_SPAWNER,
            EntityType.MINECART_TNT,

            EntityType.ARMOR_STAND,
            EntityType.PAINTING,
            EntityType.ITEM_FRAME,
            EntityType.LEASH_HITCH,

            EntityType.CHICKEN,
            EntityType.COW,
            EntityType.HORSE,
            EntityType.SHEEP,
            EntityType.VILLAGER,
            EntityType.WOLF -> event.isCancelled = true
        }
    }

    /*
     * Prevents endermen from griefing.
     * Prevents sand blocks from exiting the parcel in which they became an entity.
     */
    @field:ListenerMarker(priority = NORMAL)
    val onEntityChangeBlockEvent = RegistratorListener<EntityChangeBlockEvent> l@{ event ->
        val (_, area) = getWorldAndArea(event.block) ?: return@l
        if (event.entity.type == EntityType.ENDERMAN || area == null || area.hasBlockVisitors) {
            event.isCancelled = true; return@l
        }

        if (event.entity.type == EntityType.FALLING_BLOCK) {
            // a sand block started falling. Track it and delete it if it gets out of this parcel.
            entityTracker.track(event.entity, area)
        }
    }

    /*
     * Prevents portals from being created if set so in the configs for that world
     */
    @field:ListenerMarker(priority = NORMAL)
    val onEntityCreatePortalEvent = RegistratorListener<EntityCreatePortalEvent> l@{ event ->
        val world = parcelProvider.getWorld(event.entity.world) ?: return@l
        if (world.options.blockPortalCreation) event.isCancelled = true
    }

    /*
     * Prevents players from dropping items
     */
    @field:ListenerMarker(priority = NORMAL)
    val onPlayerDropItemEvent = RegistratorListener<PlayerDropItemEvent> l@{ event ->
        val (_, area) = getWorldAndArea(event.itemDrop.location.block) ?: return@l
        if (!canInteract(event.player, area, "containers")) event.isCancelled = true
    }

    /*
     * Prevents players from picking up items
     */
    @field:ListenerMarker(priority = NORMAL)
    val onEntityPickupItemEvent = RegistratorListener<EntityPickupItemEvent> l@{ event ->
        val user = event.entity as? Player ?: return@l
        val (_, area) = getWorldAndArea(event.item.location.block) ?: return@l
        if (!canInteract(user, area, "containers")) event.isCancelled = true
    }

    /*
     * Prevents players from editing inventories
     */
    @field:ListenerMarker(priority = NORMAL, events = ["inventory.InventoryClickEvent", "inventory.InventoryDragEvent"])
    val onInventoryClickEvent = RegistratorListener<InventoryInteractEvent> l@{ event ->
        val user = event.whoClicked as? Player ?: return@l
        if ((event.inventory ?: return@l).holder === user) return@l // inventory null: hotbar
        val (_, area) = getWorldAndArea(event.inventory.location.block) ?: return@l
        if (!canInteract(user, area, "containers")) {
            event.isCancelled = true
        }
    }

    /*
     * Cancels weather changes and sets the weather to sunny if requested by the config for that world.
     */
    @field:ListenerMarker(priority = NORMAL)
    val onWeatherChangeEvent = RegistratorListener<WeatherChangeEvent> l@{ event ->
        val world = parcelProvider.getWorld(event.world) ?: return@l
        if (world.options.noWeather && event.toWeatherState()) {
            event.isCancelled = true
        }
    }

    private fun resetWeather(world: World) {
        world.setStorm(false)
        world.isThundering = false
        world.weatherDuration = Int.MAX_VALUE
    }

// TODO: BlockFormEvent, BlockSpreadEvent, BlockFadeEvent, Fireworks

    /*
     * Prevents natural blocks forming
     */
    @ListenerMarker(priority = NORMAL)
    val onBlockFormEvent = RegistratorListener<BlockFormEvent> l@{ event ->
        val block = event.block
        val (world, area) = getWorldAndArea(block) ?: return@l

        // prevent any generation whatsoever on paths
        if (area == null) {
            event.isCancelled = true; return@l
        }

        val hasEntity = event is EntityBlockFormEvent
        val player = (event as? EntityBlockFormEvent)?.entity as? Player

        val cancel: Boolean = when (event.newState.type) {

            // prevent ice generation from Frost Walkers enchantment
            FROSTED_ICE -> player != null && !area.canBuild(player)

            // prevent snow generation from weather
            SNOW -> !hasEntity && world.options.preventWeatherBlockChanges

            else -> false
        }

        if (cancel) {
            event.isCancelled = true
        }
    }

    /*
     * Prevents mobs (living entities) from spawning if that is disabled for that world in the config.
     */
    @field:ListenerMarker(priority = NORMAL)
    val onEntitySpawnEvent = RegistratorListener<EntitySpawnEvent> l@{ event ->
        val world = parcelProvider.getWorld(event.entity.world) ?: return@l
        if (event.entity is Mob && world.options.blockMobSpawning) {
            event.isCancelled = true
        } else if (world.getParcelAt(event.entity).let { it != null && it.hasBlockVisitors }) {
            event.isCancelled = true
        }
    }



    /*
     * Prevents minecarts/boats from moving outside a plot
     */
    @field:ListenerMarker(priority = NORMAL)
    val onVehicleMoveEvent = RegistratorListener<VehicleMoveEvent> l@{ event ->
        val (_, area) = getWorldAndArea(event.to.block) ?: return@l
        if (area == null) {
            event.vehicle.passengers.forEach {
                if (it.type == EntityType.PLAYER) {
                    (it as Player).sendParcelMessage(except = true, message = "Your ride ends here")
                } else it.remove()
            }
            event.vehicle.eject()
            event.vehicle.remove()
        } else if (area.hasBlockVisitors) {
            event.to.subtract(event.to).add(event.from)
        }
    }

    /*
     * Prevents players from removing items from item frames
     * Prevents TNT Minecarts and creepers from destroying entities (This event is called BEFORE EntityExplodeEvent GG)
     * Actually doesn't prevent this because the entities are destroyed anyway, even though the code works?
     */
    @field:ListenerMarker(priority = NORMAL)
    val onEntityDamageByEntityEvent = RegistratorListener<EntityDamageByEntityEvent> l@{ event ->
        val world = parcelProvider.getWorld(event.entity.world) ?: return@l
        if (world.options.disableExplosions && (event.damager is ExplosiveMinecart || event.damager is Creeper)) {
            event.isCancelled = true; return@l
        }

        val user = event.damager as? Player
            ?: (event.damager as? Projectile)?.let { it.shooter as? Player }
            ?: return@l

        if (!canBuildOnArea(user, world.getParcelAt(event.entity))) {
            event.isCancelled = true
        }
    }

    @field:ListenerMarker(priority = NORMAL)
    val onHangingBreakEvent = RegistratorListener<HangingBreakEvent> l@{ event ->
        val world = parcelProvider.getWorld(event.entity.world) ?: return@l
        if (event.cause == HangingBreakEvent.RemoveCause.EXPLOSION && world.options.disableExplosions) {
            event.isCancelled = true; return@l
        }

        if (world.getParcelAt(event.entity).let { it != null && it.hasBlockVisitors }) {
            event.isCancelled = true
        }
    }

    /*
     * Prevents players from deleting paintings and item frames
     * This appears to take care of shooting with a bow, throwing snowballs or throwing ender pearls.
     */
    @field:ListenerMarker(priority = NORMAL)
    val onHangingBreakByEntityEvent = RegistratorListener<HangingBreakByEntityEvent> l@{ event ->
        val world = parcelProvider.getWorld(event.entity.world) ?: return@l
        val user = event.remover as? Player ?: return@l
        if (!canBuildOnArea(user, world.getParcelAt(event.entity))) {
            event.isCancelled = true
        }
    }

    /*
     * Prevents players from placing paintings and item frames
     */
    @field:ListenerMarker(priority = NORMAL)
    val onHangingPlaceEvent = RegistratorListener<HangingPlaceEvent> l@{ event ->
        val world = parcelProvider.getWorld(event.entity.world) ?: return@l
        val block = event.block.getRelative(event.blockFace)
        if (!canBuildOnArea(event.player, world.getParcelAt(block))) {
            event.isCancelled = true
        }
    }

    /*
     * Prevents stuff from growing outside of plots
     */
    @field:ListenerMarker(priority = NORMAL)
    val onStructureGrowEvent = RegistratorListener<StructureGrowEvent> l@{ event ->
        val (world, area) = getWorldAndArea(event.location.block) ?: return@l
        if (area == null) {
            event.isCancelled = true; return@l
        }

        if (!event.player.hasPermBuildAnywhere && !area.canBuild(event.player)) {
            event.isCancelled = true; return@l
        }

        event.blocks.removeIf { world.getParcelAt(it.block) !== area }
    }

    @field:ListenerMarker(priority = NORMAL)
    val onBlockGrowEvent = RegistratorListener<BlockGrowEvent> l@{ event ->
        val (world, area) = getWorldAndArea(event.block) ?: return@l
        if (area == null) {
            event.isCancelled = true
        }
    }

    /*
     * Prevents dispensers/droppers from dispensing out of parcels
     */
    @field:ListenerMarker(priority = NORMAL)
    val onBlockDispenseEvent = RegistratorListener<BlockDispenseEvent> l@{ event ->
        val block = event.block
        if (!block.type.let { it == DISPENSER || it == DROPPER }) return@l
        val world = parcelProvider.getWorld(block.world) ?: return@l
        val data = block.blockData as Directional
        val targetBlock = block.getRelative(data.facing)
        if (world.getParcelAt(targetBlock) == null) {
            event.isCancelled = true
        }
    }

    /*
     * Track spawned items, making sure they don't leave the parcel.
     */
    @field:ListenerMarker(priority = NORMAL)
    val onItemSpawnEvent = RegistratorListener<ItemSpawnEvent> l@{ event ->
        val (_, area) = getWorldAndArea(event.location.block) ?: return@l
        if (area == null) event.isCancelled = true
        else entityTracker.track(event.entity, area)
    }

    /*
     * Prevents endermen and endermite from teleporting outside their parcel
     */
    @field:ListenerMarker(priority = NORMAL)
    val onEntityTeleportEvent = RegistratorListener<EntityTeleportEvent> l@{ event ->
        val (world, area) = getWorldAndArea(event.from.block) ?: return@l
        if (area !== world.getParcelAt(event.to)) {
            event.isCancelled = true
        }
    }

    /*
     * Prevents projectiles from flying out of parcels
     * Prevents players from firing projectiles if they cannot build
     */
    @field:ListenerMarker(priority = NORMAL)
    val onProjectileLaunchEvent = RegistratorListener<ProjectileLaunchEvent> l@{ event ->
        val (_, area) = getWorldAndArea(event.entity.location.block) ?: return@l
        if (area == null || (event.entity.shooter as? Player)?.let { !canBuildOnArea(it, area) } == true) {
            event.isCancelled = true
        } else {
            entityTracker.track(event.entity, area)
        }
    }

    /*
     * Prevents entities from dropping items upon death, if configured that way
     */
    @field:ListenerMarker(priority = NORMAL)
    val onEntityDeathEvent = RegistratorListener<EntityDeathEvent> l@{ event ->
        entityTracker.untrack(event.entity)
        val world = parcelProvider.getWorld(event.entity.world) ?: return@l
        if (!world.options.dropEntityItems) {
            event.drops.clear()
            event.droppedExp = 0
        }
    }

    /*
     * Assigns players their default game mode upon entering the world
     */
    @field:ListenerMarker(priority = NORMAL)
    val onPlayerChangedWorldEvent = RegistratorListener<PlayerChangedWorldEvent> l@{ event ->
        val world = parcelProvider.getWorld(event.player.world) ?: return@l
        if (world.options.gameMode != null && !event.player.hasPermGamemodeBypass) {
            event.player.gameMode = world.options.gameMode
        }
    }

    /**
     * Updates owner signs of parcels that get loaded if it is marked outdated
     */
    @ListenerMarker(priority = EventPriority.NORMAL)
    val onChunkLoadEvent = RegistratorListener<ChunkLoadEvent> l@{ event ->
        val world = parcelProvider.getWorld(event.chunk.world) ?: return@l
        val parcels = world.blockManager.getParcelsWithOwnerBlockIn(event.chunk)
        if (parcels.isEmpty()) return@l

        parcels.forEach { id ->
            val parcel = world.getParcelById(id)?.takeIf { it.isOwnerSignOutdated } ?: return@forEach
            world.blockManager.updateParcelInfo(parcel.id, parcel.owner)
            parcel.isOwnerSignOutdated = false
        }

    }

    @ListenerMarker
    val onPlayerJoinEvent = RegistratorListener<PlayerJoinEvent> l@{ event ->
        storage.updatePlayerName(event.player.uuid, event.player.name)
    }

    /**
     * Attempts to prevent redstone contraptions from breaking while they are being swapped
     * Might remove if it causes lag
     */
    @ListenerMarker
    val onBlockRedstoneEvent = RegistratorListener<BlockRedstoneEvent> l@{ event ->
        val (_, area) = getWorldAndArea(event.block) ?: return@l
        if (area == null || area.hasBlockVisitors) {
            event.newCurrent = event.oldCurrent
        }
    }


    private fun getPlayerSpeed(player: Player): Double =
        if (player.isFlying) {
            player.flySpeed * if (player.isSprinting) 21.6 else 10.92
        } else {
            player.walkSpeed * when {
                player.isSprinting -> 5.612
                player.isSneaking -> 1.31
                else -> 4.317
            } / 1.5 //?
        } / 20.0

}