diff --git a/Server/src/main/java/core/game/node/entity/player/link/quest/Quest.java b/Server/src/main/java/core/game/node/entity/player/link/quest/Quest.java index b56fae868..0cdbe77bb 100644 --- a/Server/src/main/java/core/game/node/entity/player/link/quest/Quest.java +++ b/Server/src/main/java/core/game/node/entity/player/link/quest/Quest.java @@ -117,6 +117,9 @@ public abstract class Quest implements Plugin { * @param player The player. */ public void finish(Player player) { + if(player.getQuestRepository().isComplete(name)) { + throw new IllegalStateException("Tried to complete quest " + name + " twice, which is not allowed!"); + } for (int i = 0; i < 18; i++) { if (i == 9 || i == 3 || i == 6) { continue; diff --git a/Server/src/main/kotlin/rs09/game/content/global/shops/Shop.kt b/Server/src/main/kotlin/rs09/game/content/global/shops/Shop.kt index 8556aa9ff..32d6402a9 100644 --- a/Server/src/main/kotlin/rs09/game/content/global/shops/Shop.kt +++ b/Server/src/main/kotlin/rs09/game/content/global/shops/Shop.kt @@ -39,8 +39,8 @@ class Shop(val title: String, val stock: Array, val general: Boolean = { val stockInstances = HashMap() val playerStock = if (general) generalPlayerStock else Container(40, ContainerType.SHOP) - private val needsUpdate = HashMap() - private val restockRates = HashMap() + val needsUpdate = HashMap() + val restockRates = HashMap() init { if(!getServerConfig().getBoolean(Shops.personalizedShops, false)) @@ -93,7 +93,7 @@ class Shop(val title: String, val stock: Array, val general: Boolean = setAttribute(player, "shop-main", main) } - private fun getContainer(player: Player) : Container + public fun getContainer(player: Player) : Container { val container = if(getServerConfig().getBoolean(Shops.personalizedShops, false)) stockInstances[player.username.hashCode()] ?: generateStockContainer().also { stockInstances[player.username.hashCode()] = it } @@ -131,6 +131,7 @@ class Shop(val title: String, val stock: Array, val general: Boolean = stockInstances.filter { needsUpdate[it.key] == true }.forEach{ (player,cont) -> for(i in 0 until cont.capacity()) { + if(cont[i] == null) continue if(stock.size < i + 1) break if(GameWorld.ticks % stock[i].restockRate != 0) continue @@ -236,29 +237,29 @@ class Shop(val title: String, val stock: Array, val general: Boolean = return max(price, 1) } - fun buy(player: Player, slot: Int, amount: Int) + fun buy(player: Player, slot: Int, amount: Int) : TransactionStatus { - if(amount !in 1..Integer.MAX_VALUE) return + if(amount !in 1..Integer.MAX_VALUE) return TransactionStatus.Failure("Invalid amount: $amount") val isMainStock = getAttribute(player, "shop-main", false) if(!isMainStock && player.ironmanManager.isIronman) { sendDialogue(player, "As an ironman, you cannot buy from player stock in shops.") - return + return TransactionStatus.Failure("Ironman buying from player stock") } - val cont = if (isMainStock) getAttribute(player, "shop-cont", null) ?: return else playerStock + val cont = if (isMainStock) getAttribute(player, "shop-cont", null) ?: return TransactionStatus.Failure("Invalid shop-cont attr") else playerStock val inStock = cont[slot] val item = Item(inStock.id, amount) if(inStock.amount < amount) item.amount = inStock.amount - if(inStock.amount > stock[slot].amount && !getServerConfig().getBoolean(Shops.personalizedShops, false)) + if(inStock.amount > stock[slot].amount && !getServerConfig().getBoolean(Shops.personalizedShops, false) && player.ironmanManager.isIronman) { sendDialogue(player, "As an ironman, you cannot buy overstocked items from shops.") - return + return TransactionStatus.Failure("Ironman overstock purchase") } val cost = getBuyPrice(player, slot) - if(cost.id == -1) sendMessage(player, "This shop cannot sell that item.").also { return } + if(cost.id == -1) sendMessage(player, "This shop cannot sell that item.").also { return TransactionStatus.Failure("Shop cannot sell this item")} if(currency == Items.COINS_995){ var amt = item.amount @@ -276,7 +277,7 @@ class Shop(val title: String, val stock: Array, val general: Boolean = if(!hasSpaceFor(player, item)) { addItem(player, cost.id, cost.amount) sendMessage(player, "You don't have enough inventory space to buy that many.") - return + return TransactionStatus.Failure("Not enough inventory space") } if(!isMainStock && cont[slot].amount - item.amount == 0) @@ -303,20 +304,22 @@ class Shop(val title: String, val stock: Array, val general: Boolean = { sendMessage(player, "You don't have enough ${cost.name.toLowerCase()} to buy that many.") } + + return TransactionStatus.Success() } - fun sell(player: Player, slot: Int, amount: Int) + fun sell(player: Player, slot: Int, amount: Int) : TransactionStatus { - if(amount !in 1..Integer.MAX_VALUE) return + if(amount !in 1..Integer.MAX_VALUE) return TransactionStatus.Failure("Invalid amount: $amount") val playerInventory = player.inventory[slot] if(playerInventory.id in intArrayOf(Items.COINS_995, Items.TOKKUL_6529, Items.ARCHERY_TICKET_1464)) { sendMessage(player, "You can't sell currency to a shop.") - return + return TransactionStatus.Failure("Tried to sell currency - ${playerInventory.id}") } val item = Item(playerInventory.id, amount) val (container,profit) = getSellPrice(player, slot) - if(profit.amount == -1) sendMessage(player, "This item can't be sold to this shop.").also { return } + if(profit.amount == -1) sendMessage(player, "This item can't be sold to this shop.").also { return TransactionStatus.Failure("Can't sell this item to this shop - ${playerInventory.id}, general: $general, price: $profit") } if(amount > player.inventory.getAmount(item.id)) item.amount = player.inventory.getAmount(item.id) @@ -336,7 +339,7 @@ class Shop(val title: String, val stock: Array, val general: Boolean = if(!hasSpaceFor(player, profit)){ sendMessage(player, "You don't have enough space to do that.") addItem(player, item.id, item.amount) - return + return TransactionStatus.Failure("Did not have enough inventory space") } if(container == playerStock && getAttribute(player, "shop-main", false)){ showTab(player, false) @@ -358,6 +361,7 @@ class Shop(val title: String, val stock: Array, val general: Boolean = needsUpdate[ServerConstants.SERVER_NAME.hashCode()] = true } } + return TransactionStatus.Success() } fun getStockSlot(itemId: Int): Pair @@ -382,6 +386,7 @@ class Shop(val title: String, val stock: Array, val general: Boolean = } } + if(shopSlot == -1) isPlayerStock = true return Pair(isPlayerStock, shopSlot) } @@ -390,4 +395,9 @@ class Shop(val title: String, val stock: Array, val general: Boolean = val generalPlayerStock = Container(40, ContainerType.SHOP) val listenerInstances = HashMap() } + + sealed class TransactionStatus { + class Success : TransactionStatus() + class Failure(val reason: String) : TransactionStatus() + } } \ No newline at end of file diff --git a/Server/src/test/kotlin/APITests.kt b/Server/src/test/kotlin/APITests.kt index 3a1b98a72..66bd618ee 100644 --- a/Server/src/test/kotlin/APITests.kt +++ b/Server/src/test/kotlin/APITests.kt @@ -13,8 +13,8 @@ import rs09.game.node.entity.skill.slayer.SlayerManager import rs09.game.system.SystemLogger object APITests { - val testPlayer = Player(PlayerDetails("test", "testing")) - val testPlayer2 = Player(PlayerDetails("test2", "testing")) + val testPlayer = TestUtils.getMockPlayer("test") + val testPlayer2 = TestUtils.getMockPlayer("test2") @Test fun testIfaceSettings(){ var builder = IfaceSettingsBuilder() diff --git a/Server/src/test/kotlin/QuestTests.kt b/Server/src/test/kotlin/QuestTests.kt new file mode 100644 index 000000000..7487edf84 --- /dev/null +++ b/Server/src/test/kotlin/QuestTests.kt @@ -0,0 +1,56 @@ +import core.game.node.entity.player.link.quest.Quest +import core.game.node.entity.player.link.quest.QuestRepository +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +class QuestTests { + val testPlayer = TestUtils.getMockPlayer("test") + class TestQuest : Quest("Test Quest", 0, 0, 1, 1, 0, 1, 2) { + override fun newInstance(`object`: Any?): Quest { + return this + } + } + val testQuest = TestQuest() + + @Test fun getIndexShouldNotThrowException() { + Assertions.assertDoesNotThrow { + testQuest.index + } + } + + @Test fun registerShouldMakeQuestImmediatelyAvailable() { + QuestRepository.register(testQuest) + Assertions.assertNotNull(QuestRepository.getQuests()[testQuest.name]) + } + + @Test fun registerShouldMakeQuestImmediatelyAvailableToInstances() { + QuestRepository.register(testQuest) + val instance = QuestRepository(testPlayer) + Assertions.assertNotNull(instance.getQuest(testQuest.name)) + } + + @Test fun getStageOnUnstartedQuestShouldNotThrowException() { + QuestRepository.register(testQuest) + val instance = QuestRepository(testPlayer) + Assertions.assertDoesNotThrow { + instance.getStage(testQuest) + } + } + + @Test fun setStageOnUnstartedQuestShouldNotThrowException() { + QuestRepository.register(testQuest) + val instance = QuestRepository(testPlayer) + Assertions.assertDoesNotThrow { + instance.setStage(testQuest, 10) + } + } + + @Test fun completeQuestShouldThrowExceptionIfAlreadyComplete() { + Assertions.assertThrows(IllegalStateException::class.java, { + QuestRepository.register(testQuest) + val repo = QuestRepository(testPlayer) + repo.getQuest("Test Quest").finish(testPlayer) + repo.getQuest("Test Quest").finish(testPlayer) + }, "Quest completed twice without throwing an exception or threw wrong exception!") + } +} \ No newline at end of file diff --git a/Server/src/test/kotlin/ShopTests.kt b/Server/src/test/kotlin/ShopTests.kt new file mode 100644 index 000000000..97909fdba --- /dev/null +++ b/Server/src/test/kotlin/ShopTests.kt @@ -0,0 +1,117 @@ +import core.game.node.entity.player.link.IronmanMode +import core.game.node.item.Item +import org.junit.Assert +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import rs09.game.content.global.shops.Shop + +class ShopTests { + val testPlayer = TestUtils.getMockPlayer("test") + val testIronman = TestUtils.getMockPlayer("test2", IronmanMode.STANDARD) + val nonGeneral = TestUtils.getMockShop("Not General", false, Item(4151, 1)) + val general = TestUtils.getMockShop("General", true, Item(4151, 1)) + + @Test fun shouldSellItemToStore() { + testPlayer.inventory.add(Item(4151, 1)) + testPlayer.setAttribute("shop-cont", general.getContainer(testPlayer)) + val status = general.sell(testPlayer, 0, 1) + Assertions.assertEquals(true, status is Shop.TransactionStatus.Success, "Transaction failed: ${if(status is Shop.TransactionStatus.Failure) status.reason else ""}") + } + + @Test fun shouldNotSellUnstockedItemToStandardStore() { + testPlayer.inventory.add(Item(1, 1)) + testPlayer.setAttribute("shop-cont", nonGeneral.getContainer(testPlayer)) + val status = nonGeneral.sell(testPlayer, 0, 1) + Assertions.assertEquals(true, status is Shop.TransactionStatus.Failure) + } + + @Test fun shouldSellUnstockedItemToGeneralStore() { + testPlayer.inventory.add(Item(1, 1)) + testPlayer.setAttribute("shop-cont", general.getContainer(testPlayer)) + val status = general.sell(testPlayer, 0, 1) + Assertions.assertEquals(true, status is Shop.TransactionStatus.Success, "Transaction failure: ${if(status is Shop.TransactionStatus.Failure) status.reason else ""}") + } + + @Test fun shouldSellUnstockedItemToGeneralStoreAsIronman() { + testIronman.inventory.add(Item(1, 1)) + testIronman.setAttribute("shop-cont", general.getContainer(testPlayer)) + val status = general.sell(testIronman, 0, 1) + Assertions.assertEquals(true, status is Shop.TransactionStatus.Success, "Transaction failure: ${if(status is Shop.TransactionStatus.Failure) status.reason else ""}") + } + + @Test fun shouldSellStackOfUnstockedItemsToPlayerStock() { + testPlayer.inventory.add(Item(1, 20)) + testPlayer.setAttribute("shop-cont", general.getContainer(testPlayer)) + val status = general.sell(testPlayer, 0, 20) + Assertions.assertEquals(true, status is Shop.TransactionStatus.Success, "Transaction failure: ${if(status is Shop.TransactionStatus.Failure) status.reason else ""}") + } + + @Test fun shouldPutSoldUnstockedItemsInPlayerStock() { + testPlayer.inventory.add(Item(2,1)) + testPlayer.setAttribute("shop-cont", general.getContainer(testPlayer)) + val status = general.sell(testPlayer, 0, 1) + Assertions.assertEquals(true, status is Shop.TransactionStatus.Success, "Transaction failure: ${if(status is Shop.TransactionStatus.Failure) status.reason else ""}") + Assertions.assertEquals(1, general.playerStock.getAmount(2)) + Assertions.assertEquals(0, general.getContainer(testPlayer).getAmount(2)) + } + + @Test fun shouldAllowStandardPlayerToBuy() { + testPlayer.inventory.add(Item(995, 100000)) + testPlayer.setAttribute("shop-cont", general.getContainer(testPlayer)) + testPlayer.setAttribute("shop-main", true) + val status = general.buy(testPlayer, 0, 1) + Assertions.assertEquals(true, status is Shop.TransactionStatus.Success, "Transaction failure: ${if(status is Shop.TransactionStatus.Failure) status.reason else ""}") + } + + @Test fun shouldAllowStandardPlayerToBuyOverstock() { + testPlayer.inventory.add(Item(995, 100000)) + testPlayer.setAttribute("shop-cont", general.getContainer(testPlayer)) + testPlayer.setAttribute("shop-main", true) + general.getContainer(testPlayer).add(Item(4151, 100)) + val status = general.buy(testPlayer, 0, 1) + Assertions.assertEquals(true, status is Shop.TransactionStatus.Success, "Transaction failure: ${if(status is Shop.TransactionStatus.Failure) status.reason else ""}") + } + + @Test fun shouldAllowStandardPlayerToBuyPlayerStock() { + testPlayer.inventory.add(Item(995, 100000)) + testPlayer.setAttribute("shop-cont", general.getContainer(testPlayer)) + testPlayer.setAttribute("shop-main", false) + general.playerStock.add(Item(4151, 100)) + val status = general.buy(testPlayer, 0, 1) + Assertions.assertEquals(true, status is Shop.TransactionStatus.Success, "Transaction failure: ${if(status is Shop.TransactionStatus.Failure) status.reason else ""}") + } + + @Test fun shouldNotAllowIronmanToBuyOverstock() { + testIronman.inventory.add(Item(995, 100000)) + testIronman.setAttribute("shop-cont", general.getContainer(testIronman)) + testIronman.setAttribute("shop-main", true) + general.getContainer(testIronman).add(Item(4151, 100)) + val status = general.buy(testIronman, 0, 1) + Assertions.assertEquals(true, status is Shop.TransactionStatus.Failure) + } + + @Test fun shouldNotAllowIronmanToBuyPlayerStock() { + testIronman.inventory.add(Item(995, 100000)) + testIronman.setAttribute("shop-cont", general.playerStock) + testIronman.setAttribute("shop-main", false) + general.playerStock.add(Item(4151, 1)) + val status = general.buy(testIronman, 0, 1) + Assertions.assertEquals(true, status is Shop.TransactionStatus.Failure) + } + + @Test fun openShopShouldNotThrowException() { + Assertions.assertDoesNotThrow { + general.openFor(testPlayer) + } + } + + @Test fun shouldNotThrowExceptionWhenRestockingStockWithNullSlot() { + Assertions.assertDoesNotThrow { + general.getContainer(testPlayer).add(Item(1, 100)) + general.getContainer(testPlayer).add(Item(2, 100)) + general.getContainer(testPlayer).replace(null, 0) //replace item in slot 0 with null + for ((k,_) in general.stockInstances) general.needsUpdate[k] = true + general.restock() + } + } +} \ No newline at end of file diff --git a/Server/src/test/kotlin/TestUtils.kt b/Server/src/test/kotlin/TestUtils.kt new file mode 100644 index 000000000..b80c57069 --- /dev/null +++ b/Server/src/test/kotlin/TestUtils.kt @@ -0,0 +1,20 @@ +import core.game.node.entity.player.Player +import core.game.node.entity.player.info.PlayerDetails +import core.game.node.entity.player.link.IronmanMode +import core.game.node.item.Item +import rs09.game.ai.ArtificialSession +import rs09.game.content.global.shops.Shop +import rs09.game.content.global.shops.ShopItem + +object TestUtils { + fun getMockPlayer(name: String, ironman: IronmanMode = IronmanMode.NONE): Player { + val p = Player(PlayerDetails(name, name)) + p.details.session = ArtificialSession.getSingleton() + p.ironmanManager.mode = ironman + return p + } + + fun getMockShop(name: String, general: Boolean, vararg stock: Item) : Shop { + return Shop(name, stock.map { ShopItem(it.id, it.amount, 100) }.toTypedArray(), general) + } +} \ No newline at end of file