diff --git a/src/main/kotlin/xyz/atrius/waystones/manager/AdvancementManager.kt b/src/main/kotlin/xyz/atrius/waystones/manager/AdvancementManager.kt index 1fbe26b..6a26831 100644 --- a/src/main/kotlin/xyz/atrius/waystones/manager/AdvancementManager.kt +++ b/src/main/kotlin/xyz/atrius/waystones/manager/AdvancementManager.kt @@ -62,8 +62,12 @@ class AdvancementManager( loadAdvancements() } - fun awardAdvancement(player: Player, adv: AdvancementProvider) = + fun awardAdvancement(player: Player, adv: AdvancementProvider) { + if (!enableAdvancements.value()) { + return + } awardAdvancement(player, plugin.server.getAdvancement(adv.namespacedKey())) + } fun awardAdvancement(player: Player, adv: SpigotAdvancement?) { if (adv == null || !enableAdvancements.value()) { diff --git a/src/test/kotlin/xyz/atrius/waystones/event/DestroyEventTest.kt b/src/test/kotlin/xyz/atrius/waystones/event/DestroyEventTest.kt new file mode 100644 index 0000000..4b5755e --- /dev/null +++ b/src/test/kotlin/xyz/atrius/waystones/event/DestroyEventTest.kt @@ -0,0 +1,127 @@ +package xyz.atrius.waystones.event + +import io.kotest.assertions.nondeterministic.eventually +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import net.kyori.adventure.text.Component +import org.bukkit.Material +import org.bukkit.ExplosionResult +import org.bukkit.entity.TNTPrimed +import org.bukkit.event.block.BlockBreakEvent +import org.bukkit.event.block.BlockExplodeEvent +import org.bukkit.event.entity.EntityExplodeEvent +import org.bukkit.inventory.ItemStack +import xyz.atrius.waystones.repository.WaystoneInfoRepository +import xyz.atrius.waystones.test.ServerFunSpec +import java.util.concurrent.TimeUnit +import kotlin.time.Duration.Companion.seconds + +class DestroyEventTest : ServerFunSpec({ + + test("Breaking a waystone removes it from the database") { + val world = createWorld("world") + val player = createPlayer() + val waystone = placeWaystone(world, 0, 64, 0) + + val nameTag = ItemStack(Material.NAME_TAG) + nameTag.editMeta { it.displayName(Component.text("Test Waystone")) } + player.inventory.setItem(0, nameTag) + player.inventory.heldItemSlot = 0 + simulateRightClick(player, waystone, nameTag) + + val repository = get() + eventually(1.seconds) { + repository.getWaystone(waystone.location).get(5, TimeUnit.SECONDS) shouldNotBe null + } + + val breakEvent = BlockBreakEvent(waystone, player) + server.pluginManager.callEvent(breakEvent) + + eventually(1.seconds) { + repository.getWaystone(waystone.location).get(5, TimeUnit.SECONDS) shouldBe null + } + } + + test("Breaking a non-waystone block does not affect the database") { + val world = createWorld("world") + val player = createPlayer() + val stone = placeBlock(world, 0, 64, 0, Material.STONE) + + val breakEvent = BlockBreakEvent(stone, player) + server.pluginManager.callEvent(breakEvent) + + breakEvent.isCancelled shouldBe false + } + + test("Breaking a lodestone without a database entry does not crash") { + val world = createWorld("world") + val player = createPlayer() + val waystone = placeWaystone(world, 0, 64, 0) + + val breakEvent = BlockBreakEvent(waystone, player) + server.pluginManager.callEvent(breakEvent) + + breakEvent.isCancelled shouldBe false + } + + test("Destroying a waystone via block explosion removes it from the database") { + val world = createWorld("world") + val waystone = placeWaystone(world, 0, 64, 0) + + val nameTag = ItemStack(Material.NAME_TAG) + nameTag.editMeta { it.displayName(Component.text("Explosive Waystone")) } + val player = createPlayer() + player.inventory.setItem(0, nameTag) + player.inventory.heldItemSlot = 0 + simulateRightClick(player, waystone, nameTag) + + val repository = get() + eventually(1.seconds) { + repository.getWaystone(waystone.location).get(5, TimeUnit.SECONDS) shouldNotBe null + } + + val explodeEvent = BlockExplodeEvent( + waystone, + waystone.state, + mutableListOf(waystone), + 4f, + ExplosionResult.DESTROY, + ) + server.pluginManager.callEvent(explodeEvent) + + eventually(1.seconds) { + repository.getWaystone(waystone.location).get(5, TimeUnit.SECONDS) shouldBe null + } + } + + test("Destroying a waystone via entity explosion removes it from the database") { + val world = createWorld("world") + val waystone = placeWaystone(world, 0, 64, 0) + + val nameTag = ItemStack(Material.NAME_TAG) + nameTag.editMeta { it.displayName(Component.text("TNT Waystone")) } + val player = createPlayer() + player.inventory.setItem(0, nameTag) + player.inventory.heldItemSlot = 0 + simulateRightClick(player, waystone, nameTag) + + val repository = get() + eventually(1.seconds) { + repository.getWaystone(waystone.location).get(5, TimeUnit.SECONDS) shouldNotBe null + } + + val tnt = world.spawn(waystone.location, TNTPrimed::class.java) + val explodeEvent = EntityExplodeEvent( + tnt, + waystone.location, + mutableListOf(waystone), + 4f, + ExplosionResult.DESTROY, + ) + server.pluginManager.callEvent(explodeEvent) + + eventually(1.seconds) { + repository.getWaystone(waystone.location).get(5, TimeUnit.SECONDS) shouldBe null + } + } +}) diff --git a/src/test/kotlin/xyz/atrius/waystones/event/LinkEventTest.kt b/src/test/kotlin/xyz/atrius/waystones/event/LinkEventTest.kt new file mode 100644 index 0000000..4f69e54 --- /dev/null +++ b/src/test/kotlin/xyz/atrius/waystones/event/LinkEventTest.kt @@ -0,0 +1,88 @@ +package xyz.atrius.waystones.event + +import io.kotest.assertions.nondeterministic.eventually +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import org.bukkit.Material +import org.bukkit.inventory.ItemStack +import org.bukkit.inventory.meta.CompassMeta +import org.bukkit.persistence.PersistentDataType +import xyz.atrius.waystones.repository.WaystoneInfoRepository +import xyz.atrius.waystones.test.ServerFunSpec +import xyz.atrius.waystones.utility.toKey +import java.util.concurrent.TimeUnit +import kotlin.time.Duration.Companion.seconds + +class LinkEventTest : ServerFunSpec({ + + test("Linking a compass to a waystone creates a database entry") { + val world = createWorld("world") + val player = createPlayer() + val waystone = placeWaystone(world, 100, 64, 200) + + val compass = ItemStack(Material.COMPASS) + val meta = compass.itemMeta as CompassMeta + meta.persistentDataContainer.set("is_warp_key".toKey(), PersistentDataType.INTEGER, 1) + compass.itemMeta = meta + player.inventory.setItem(0, compass) + player.inventory.heldItemSlot = 0 + + simulateRightClick(player, waystone, compass) + + val repository = get() + eventually(1.seconds) { + repository.getWaystone(waystone.location).get(5, TimeUnit.SECONDS) shouldNotBe null + } + } + + test("Linking a compass to a non-waystone block does nothing") { + val world = createWorld("world") + val player = createPlayer() + val stone = placeBlock(world, 0, 64, 0, Material.STONE) + + val compass = ItemStack(Material.COMPASS) + player.inventory.setItem(0, compass) + player.inventory.heldItemSlot = 0 + + simulateRightClick(player, stone, compass) + + val repository = get() + eventually(1.seconds) { + repository.getWaystone(stone.location).get(5, TimeUnit.SECONDS) shouldBe null + } + } + + test("Linking a non-compass item to a waystone does nothing") { + val world = createWorld("world") + val player = createPlayer() + val waystone = placeWaystone(world, 0, 64, 0) + + val stick = ItemStack(Material.STICK) + player.inventory.setItem(0, stick) + player.inventory.heldItemSlot = 0 + + simulateRightClick(player, waystone, stick) + + val repository = get() + eventually(1.seconds) { + repository.getWaystone(waystone.location).get(5, TimeUnit.SECONDS) shouldBe null + } + } + + test("Linking a compass without warp key marker does nothing") { + val world = createWorld("world") + val player = createPlayer() + val waystone = placeWaystone(world, 0, 64, 0) + + val compass = ItemStack(Material.COMPASS) + player.inventory.setItem(0, compass) + player.inventory.heldItemSlot = 0 + + simulateRightClick(player, waystone, compass) + + val repository = get() + eventually(1.seconds) { + repository.getWaystone(waystone.location).get(5, TimeUnit.SECONDS) shouldBe null + } + } +}) diff --git a/src/test/kotlin/xyz/atrius/waystones/event/NameEventTest.kt b/src/test/kotlin/xyz/atrius/waystones/event/NameEventTest.kt new file mode 100644 index 0000000..fa037a0 --- /dev/null +++ b/src/test/kotlin/xyz/atrius/waystones/event/NameEventTest.kt @@ -0,0 +1,132 @@ +package xyz.atrius.waystones.event + +import io.kotest.assertions.nondeterministic.eventually +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import net.kyori.adventure.text.Component +import org.bukkit.Material +import org.bukkit.inventory.ItemStack +import org.bukkit.inventory.meta.ItemMeta +import xyz.atrius.waystones.repository.WaystoneInfoRepository +import xyz.atrius.waystones.test.ServerFunSpec +import java.util.concurrent.TimeUnit +import kotlin.time.Duration.Companion.seconds + +class NameEventTest : ServerFunSpec({ + + test("Naming a waystone with a name tag persists to the database") { + val world = createWorld("world") + val player = createPlayer() + val waystone = placeWaystone(world, 0, 64, 0) + + val nameTag = ItemStack(Material.NAME_TAG) + nameTag.editMeta { meta: ItemMeta -> + meta.displayName(Component.text("My Waystone")) + } + player.inventory.setItem(0, nameTag) + player.inventory.heldItemSlot = 0 + + simulateRightClick(player, waystone, nameTag) + + val repository = get() + eventually(1.seconds) { + repository.getWaystone(waystone.location).get(5, TimeUnit.SECONDS)?.name shouldBe "My Waystone" + } + } + + test("Renaming a waystone updates the database entry") { + val world = createWorld("world") + val player = createPlayer() + val waystone = placeWaystone(world, 0, 64, 0) + + val nameTag1 = ItemStack(Material.NAME_TAG) + nameTag1.editMeta { it.displayName(Component.text("First Name")) } + player.inventory.setItem(0, nameTag1) + player.inventory.heldItemSlot = 0 + simulateRightClick(player, waystone, nameTag1) + + val nameTag2 = ItemStack(Material.NAME_TAG) + nameTag2.editMeta { it.displayName(Component.text("Second Name")) } + player.inventory.setItem(0, nameTag2) + player.inventory.heldItemSlot = 0 + simulateRightClick(player, waystone, nameTag2) + + val repository = get() + eventually(1.seconds) { + repository.getWaystone(waystone.location).get(5, TimeUnit.SECONDS)?.name shouldBe "Second Name" + } + } + + test("Using a non-name-tag item does not name the waystone") { + val world = createWorld("world") + val player = createPlayer() + val waystone = placeWaystone(world, 0, 64, 0) + + val stick = ItemStack(Material.STICK) + player.inventory.setItem(0, stick) + player.inventory.heldItemSlot = 0 + simulateRightClick(player, waystone, stick) + + val repository = get() + eventually(1.seconds) { + repository.getWaystone(waystone.location).get(5, TimeUnit.SECONDS)?.name shouldBe null + } + } + + test("Naming a waystone with a name tag without display name does nothing") { + val world = createWorld("world") + val player = createPlayer() + val waystone = placeWaystone(world, 0, 64, 0) + + val nameTag = ItemStack(Material.NAME_TAG) + player.inventory.setItem(0, nameTag) + player.inventory.heldItemSlot = 0 + simulateRightClick(player, waystone, nameTag) + + val repository = get() + eventually(1.seconds) { + repository.getWaystone(waystone.location).get(5, TimeUnit.SECONDS)?.name shouldBe null + } + } + + test("Naming a non-lodestone block does nothing") { + val world = createWorld("world") + val player = createPlayer() + val stone = placeBlock(world, 0, 64, 0, Material.STONE) + + val nameTag = ItemStack(Material.NAME_TAG) + nameTag.editMeta { it.displayName(Component.text("My Stone")) } + player.inventory.setItem(0, nameTag) + player.inventory.heldItemSlot = 0 + simulateRightClick(player, stone, nameTag) + + val repository = get() + val info = repository.getWaystone(stone.location).get(5, TimeUnit.SECONDS) + info?.name shouldBe null + } + + test("Naming a waystone with the same name is ignored") { + val world = createWorld("world") + val player = createPlayer() + val waystone = placeWaystone(world, 0, 64, 0) + + val nameTag1 = ItemStack(Material.NAME_TAG) + nameTag1.editMeta { it.displayName(Component.text("Same Name")) } + player.inventory.setItem(0, nameTag1) + player.inventory.heldItemSlot = 0 + simulateRightClick(player, waystone, nameTag1) + + val repository = get() + eventually(1.seconds) { + repository.getWaystone(waystone.location).get(5, TimeUnit.SECONDS)?.name shouldBe "Same Name" + } + + val nameTag2 = ItemStack(Material.NAME_TAG) + nameTag2.editMeta { it.displayName(Component.text("Same Name")) } + player.inventory.setItem(0, nameTag2) + player.inventory.heldItemSlot = 0 + simulateRightClick(player, waystone, nameTag2) + + repository.getWaystone(waystone.location).get(5, TimeUnit.SECONDS)?.name shouldBe "Same Name" + } +}) diff --git a/src/test/kotlin/xyz/atrius/waystones/test/ServerFunSpec.kt b/src/test/kotlin/xyz/atrius/waystones/test/ServerFunSpec.kt index 071b7ed..2b4b161 100644 --- a/src/test/kotlin/xyz/atrius/waystones/test/ServerFunSpec.kt +++ b/src/test/kotlin/xyz/atrius/waystones/test/ServerFunSpec.kt @@ -12,6 +12,7 @@ import org.bukkit.entity.Player import org.bukkit.event.block.Action import org.bukkit.event.player.PlayerInteractEvent import org.bukkit.inventory.ItemStack +import org.koin.core.context.stopKoin import org.mockbukkit.mockbukkit.MockBukkit import org.mockbukkit.mockbukkit.ServerMock import xyz.atrius.waystones.Waystones @@ -51,14 +52,21 @@ abstract class ServerFunSpec private constructor() : FunSpec() { extensions( object : SpecExtension { override suspend fun intercept(spec: Spec, execute: suspend (Spec) -> Unit) { - _server = MockBukkit.mock() - _plugin = MockBukkit.load(Waystones::class.java) + _server = MockBukkit.mock(TestServerMock()) + + // Load plugin with custom config + val configStream = javaClass.getResourceAsStream("/test-config.yml") + ?: throw IllegalStateException("test-config.yml not found") + _plugin = MockBukkit.loadWithConfig(Waystones::class.java, configStream.use { + org.bukkit.configuration.file.YamlConfiguration.loadConfiguration(java.io.InputStreamReader(it)) + }) _koin = _plugin!!.getKoinApp().koin try { execute(spec) } finally { MockBukkit.unmock() + stopKoin() } } } diff --git a/src/test/kotlin/xyz/atrius/waystones/test/TestServerMock.kt b/src/test/kotlin/xyz/atrius/waystones/test/TestServerMock.kt new file mode 100644 index 0000000..e6b7566 --- /dev/null +++ b/src/test/kotlin/xyz/atrius/waystones/test/TestServerMock.kt @@ -0,0 +1,12 @@ +package xyz.atrius.waystones.test + +import org.mockbukkit.mockbukkit.ServerMock + +/** + * Custom ServerMock that disables the AsyncCatcher by always reporting + * that we're on the main thread. This is necessary because Kotest runs + * tests in coroutines which may switch threads between suspensions. + */ +class TestServerMock : ServerMock() { + override fun isPrimaryThread(): Boolean = true +} diff --git a/src/test/resources/test-config.yml b/src/test/resources/test-config.yml new file mode 100644 index 0000000..7c8c479 --- /dev/null +++ b/src/test/resources/test-config.yml @@ -0,0 +1,38 @@ +fallback-locale: en +wait-time: 3.0 +damage-stops-warping: true +limit-distance: true +base-distance: 100 +max-boost: 150 +max-warp-size: 50 +jump-worlds: true +default-world-ratio: 1.0 +warp-animations: true +single-use: false +require-power: INTER_DIMENSION +power-cost: 1 +enable-portal-sickness: true +portal-sickness-chance: 5.0 +portal-sickness-warping: DAMAGE_ON_TELEPORT +portal-sickness-damage: 5.0 +relinkable-keys: true +enable-key-items: true +enable-advancements: false +show-compass-coordinates: true +key-recipe: + - AIR + - IRON_INGOT + - AIR + - IRON_INGOT + - REDSTONE_BLOCK + - IRON_INGOT + - AIR + - IRON_INGOT + - AIR +database: + type: sqlite + host: "localhost" + port: 3306 + username: "root" + password: "password" + database-name: "waystones"