diff --git a/game/build.gradle b/game/build.gradle index 7c5c8f0f..b9646593 100644 --- a/game/build.gradle +++ b/game/build.gradle @@ -33,9 +33,7 @@ dependencies { compile group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-jre8', version: "$kotlinVersion" compile group: 'org.jetbrains.kotlin', name: 'kotlin-compiler-embeddable', version: "$kotlinVersion" - // https://mvnrepository.com/artifact/org.hamcrest/hamcrest-all - testCompile group: 'org.hamcrest', name: 'hamcrest-all', version: '1.3' - testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlinVersion" + testCompile group: 'org.assertj', name: 'assertj-core', version: '3.8.0' } task run(type: JavaExec, dependsOn: classes) { diff --git a/game/src/main/java/org/apollo/game/message/impl/SendFriendMessage.java b/game/src/main/java/org/apollo/game/message/impl/SendFriendMessage.java index 2900ab24..b86353c1 100644 --- a/game/src/main/java/org/apollo/game/message/impl/SendFriendMessage.java +++ b/game/src/main/java/org/apollo/game/message/impl/SendFriendMessage.java @@ -27,7 +27,7 @@ public final class SendFriendMessage extends Message { */ public SendFriendMessage(String username, int world) { this.username = username; - this.world = world == 0 ? 0 : world + 9; + this.world = world; } /** @@ -48,4 +48,12 @@ public final class SendFriendMessage extends Message { return world; } + /** + * Gets the encoded world id to be sent to the client. + * + * @return The encoded world id. + */ + public int getEncodedWorld() { + return world == 0 ? 0 : world + 9; + } } \ No newline at end of file diff --git a/game/src/main/java/org/apollo/game/release/r317/SendFriendMessageEncoder.java b/game/src/main/java/org/apollo/game/release/r317/SendFriendMessageEncoder.java index 15660fcb..932dca8e 100644 --- a/game/src/main/java/org/apollo/game/release/r317/SendFriendMessageEncoder.java +++ b/game/src/main/java/org/apollo/game/release/r317/SendFriendMessageEncoder.java @@ -18,7 +18,7 @@ public final class SendFriendMessageEncoder extends MessageEncoder()) + + player = world.spawnPlayer("testPlayer") + } + +} diff --git a/game/src/pluginTesting/kotlin/org/apollo/game/plugin/testing/KotlinPluginTestHelpers.kt b/game/src/pluginTesting/kotlin/org/apollo/game/plugin/testing/KotlinPluginTestHelpers.kt new file mode 100644 index 00000000..b6a26f66 --- /dev/null +++ b/game/src/pluginTesting/kotlin/org/apollo/game/plugin/testing/KotlinPluginTestHelpers.kt @@ -0,0 +1,111 @@ +package org.apollo.game.plugin.testing + +import org.apollo.cache.def.ItemDefinition +import org.apollo.cache.def.NpcDefinition +import org.apollo.game.action.Action +import org.apollo.game.message.handler.MessageHandlerChainSet +import org.apollo.game.message.impl.* +import org.apollo.game.model.* +import org.apollo.game.model.entity.* +import org.apollo.game.model.entity.obj.GameObject +import org.apollo.game.model.entity.obj.StaticGameObject +import org.apollo.net.message.Message +import org.apollo.util.security.PlayerCredentials +import org.junit.Assert +import org.mockito.* +import org.powermock.api.mockito.PowerMockito + +/** + * A base class containing a set of helper methods to be used within plugin tests. + */ +abstract class KotlinPluginTestHelpers { + abstract var world: World + abstract var player: Player + abstract var messageHandlers: MessageHandlerChainSet + + /** + * Waits for an [Action] to complete within a specified number of pulses, and with an optional predicate + * to test the [Action] against. + */ + fun Player.waitForActionCompletion(predicate: (Action) -> Boolean = { _ -> true }, timeout: Int = 15) { + val actionCaptor: ArgumentCaptor> = ArgumentCaptor.forClass(Action::class.java) + Mockito.verify(this).startAction(actionCaptor.capture()) + + val action: Action = actionCaptor.value as Action + Assert.assertTrue("Found wrong action type", predicate.invoke(action)) + + var pulses = 0 + + do { + action.pulse() + } while (action.isRunning && pulses++ < timeout) + + Assert.assertFalse("Exceeded timeout waiting for action completion", pulses > timeout) + } + + /** + * Spawns a new NPC with the minimum set of dependencies required to function correctly in the world. + */ + fun World.spawnNpc(id: Int, position: Position): Npc { + val definition = NpcDefinition(id) + val npc = Npc(this, position, definition, arrayOfNulls(4)) + val region = regionRepository.fromPosition(position) + val npcs = npcRepository + + npcs.add(npc) + region.addEntity(npc) + + return npc + } + + /** + * Spawn a new player stub in the world, with a dummy game session. + */ + fun World.spawnPlayer(username: String, position: Position = Position(3200, 3200, 0)): Player { + val credentials = PlayerCredentials(username, "test", 1, 1, "0.0.0.0") + val region = regionRepository.fromPosition(position) + + val player = PowerMockito.spy(Player(this, credentials, position)) + register(player) + region.addEntity(player) + + PowerMockito.doNothing().`when`(player).send(Matchers.any()) + + return player + } + + /** + * Spawn a new static game object into the world with the given id and position. + */ + fun World.spawnObject(id: Int, position: Position): GameObject { + val obj = StaticGameObject(this, id, position, 0, 0) + + spawn(obj) + + return obj + } + + /** + * Fake a client [Message] originating from a player and send it to the relevant + * message handlers. + */ + fun Player.notify(message: Message) { + messageHandlers.notify(this, message) + } + + /** + * Move the player within interaction distance to the given [Entity] and fake an action + * message. + */ + fun Player.interactWith(entity: Entity, option: Int = 1) { + position = entity.position.step(1, Direction.NORTH) + + when (entity) { + is GameObject -> notify(ObjectActionMessage(option, entity.id, entity.position)) + is Npc -> notify(NpcActionMessage(option, entity.index)) + is Player -> notify(PlayerActionMessage(option, entity.index)) + } + } + +} + diff --git a/game/src/pluginTesting/kotlin/org/apollo/game/plugin/testing/fakes/FakePluginContextFactory.kt b/game/src/pluginTesting/kotlin/org/apollo/game/plugin/testing/fakes/FakePluginContextFactory.kt new file mode 100644 index 00000000..aac82329 --- /dev/null +++ b/game/src/pluginTesting/kotlin/org/apollo/game/plugin/testing/fakes/FakePluginContextFactory.kt @@ -0,0 +1,21 @@ +package org.apollo.game.plugin.testing.fakes + +import org.apollo.game.message.handler.MessageHandler +import org.apollo.game.message.handler.MessageHandlerChainSet +import org.apollo.game.plugin.PluginContext +import org.apollo.net.message.Message +import org.mockito.invocation.InvocationOnMock +import org.mockito.stubbing.Answer +import org.powermock.api.mockito.PowerMockito + +object FakePluginContextFactory { + fun create(messageHandlers: MessageHandlerChainSet): PluginContext { + val answer = Answer { invocation: InvocationOnMock -> + messageHandlers.putHandler( + invocation.arguments[0] as Class, + invocation.arguments[1] as MessageHandler<*>) + } + + return PowerMockito.mock(PluginContext::class.java, answer) + } +} \ No newline at end of file diff --git a/game/src/pluginTesting/kotlin/org/apollo/game/plugin/testing/mockito/KotlinArgMatcher.kt b/game/src/pluginTesting/kotlin/org/apollo/game/plugin/testing/mockito/KotlinArgMatcher.kt new file mode 100644 index 00000000..74814626 --- /dev/null +++ b/game/src/pluginTesting/kotlin/org/apollo/game/plugin/testing/mockito/KotlinArgMatcher.kt @@ -0,0 +1,25 @@ +package org.apollo.game.plugin.testing.mockito + +import org.mockito.ArgumentMatcher +import java.lang.AssertionError +import java.util.function.Consumer + +class KotlinArgMatcher(val consumer: Consumer) : ArgumentMatcher() { + private var error: String? = null + + override fun matches(argument: Any?): Boolean { + try { + consumer.accept(argument as T) + return true + } catch (err: AssertionError) { + error = err.message + println(error) + return false + } + } + + override fun toString(): String { + return error ?: "" + } +} + diff --git a/game/src/pluginTesting/kotlin/org/apollo/game/plugin/testing/mockito/KotlinMockitoExtensions.kt b/game/src/pluginTesting/kotlin/org/apollo/game/plugin/testing/mockito/KotlinMockitoExtensions.kt new file mode 100644 index 00000000..0bfad491 --- /dev/null +++ b/game/src/pluginTesting/kotlin/org/apollo/game/plugin/testing/mockito/KotlinMockitoExtensions.kt @@ -0,0 +1,14 @@ +package org.apollo.game.plugin.testing.mockito + +import org.mockito.Mockito +import java.util.function.Consumer + + +object KotlinMockitoExtensions { + inline fun matches(crossinline callback: T.() -> Unit): T { + val consumer = Consumer { it.callback() } + val matcher = KotlinArgMatcher(consumer) + + return Mockito.argThat(matcher) + } +} diff --git a/game/src/pluginTesting/kotlin/org/apollo/game/plugins/testing/KotlinPluginTest.kt b/game/src/pluginTesting/kotlin/org/apollo/game/plugins/testing/KotlinPluginTest.kt deleted file mode 100644 index 827d8804..00000000 --- a/game/src/pluginTesting/kotlin/org/apollo/game/plugins/testing/KotlinPluginTest.kt +++ /dev/null @@ -1,113 +0,0 @@ -package org.apollo.game.plugins.testing - -import org.apollo.cache.def.NpcDefinition -import org.apollo.game.action.Action -import org.apollo.game.message.handler.MessageHandler -import org.apollo.game.message.handler.MessageHandlerChainSet -import org.apollo.game.message.impl.* -import org.apollo.game.model.* -import org.apollo.game.model.entity.* -import org.apollo.game.model.entity.obj.GameObject -import org.apollo.game.model.entity.obj.StaticGameObject -import org.apollo.game.plugin.* -import org.apollo.net.message.Message -import org.apollo.util.security.PlayerCredentials -import org.junit.Assert -import org.junit.Before -import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Matchers.any -import org.mockito.Mockito -import org.mockito.invocation.InvocationOnMock -import org.mockito.stubbing.Answer -import org.powermock.api.mockito.PowerMockito -import org.powermock.core.classloader.annotations.PrepareForTest -import org.powermock.modules.junit4.PowerMockRunner -import java.util.* - -@RunWith(PowerMockRunner::class) -@PrepareForTest(World::class, PluginContext::class, Player::class) -abstract class KotlinPluginTest { - lateinit var world: World - lateinit var player: Player - lateinit var messageHandlers: MessageHandlerChainSet - - @Before - fun setup() { - messageHandlers = MessageHandlerChainSet() - world = PowerMockito.spy(World()) - - val answer = Answer { invocation: InvocationOnMock -> - messageHandlers.putHandler( - invocation.arguments[0] as Class, - invocation.arguments[1] as MessageHandler<*>) - } - - val pluginContext = PowerMockito.mock(PluginContext::class.java, answer) - val pluginEnvironment = KotlinPluginEnvironment(world) - - pluginEnvironment.setContext(pluginContext) - pluginEnvironment.load(ArrayList()) - - val credentials = PlayerCredentials("test", "test", 1, 1, "0.0.0.0") - val position = Position(3200, 3200, 0) - val region = world.regionRepository.fromPosition(position) - - player = PowerMockito.spy(Player(world, credentials, position)) - world.register(player) - region.addEntity(player) - - PowerMockito.doNothing().`when`(player).send(any()) - } - - fun interactWith(entity: Entity, option: Int = 1) { - player.position = entity.position.step(1, Direction.NORTH) - - when (entity) { - is GameObject -> notify(ObjectActionMessage(option, entity.id, entity.position)) - is Npc -> notify(NpcActionMessage(option, entity.index)) - is Player -> notify(PlayerActionMessage(option, entity.index)) - } - } - - fun spawnNpc(id: Int, position: Position): Npc { - val definition = NpcDefinition(id) - val npc = Npc(world, position, definition, arrayOfNulls(4)) - val region = world.regionRepository.fromPosition(position) - val npcs = world.npcRepository - - npcs.add(npc) - region.addEntity(npc) - - return npc - } - - fun spawnObject(id: Int, position: Position): GameObject { - val obj = StaticGameObject(world, id, position, 0, 0) - - world.spawn(obj) - - return obj - } - - fun notify(message: Message) { - messageHandlers.notify(player, message) - } - - fun waitForActionCompletion(predicate: (Action) -> Boolean = { _ -> true }, timeout: Int = 15) { - val actionCaptor: ArgumentCaptor> = ArgumentCaptor.forClass(Action::class.java) - Mockito.verify(player).startAction(actionCaptor.capture()) - - val action: Action = actionCaptor.value as Action - Assert.assertTrue("Found wrong action type", predicate.invoke(action)) - - var pulses = 0 - - do { - action.pulse() - } while (action.isRunning && pulses++ < timeout) - - Assert.assertFalse("Exceeded timeout waiting for action completion", pulses > timeout) - } - -} diff --git a/game/src/plugins/bank/test/OpenBankTest.kt b/game/src/plugins/bank/test/OpenBankTest.kt index 8bede6be..d7b4ed1c 100644 --- a/game/src/plugins/bank/test/OpenBankTest.kt +++ b/game/src/plugins/bank/test/OpenBankTest.kt @@ -1,5 +1,5 @@ import org.apollo.game.model.Position -import org.apollo.game.plugins.testing.KotlinPluginTest +import org.apollo.game.plugin.testing.KotlinPluginTest import org.junit.Test import org.mockito.Mockito.verify @@ -14,21 +14,21 @@ class OpenBankTest() : KotlinPluginTest() { @Test fun `Interacting with a bank teller should open the players bank`() { - val bankTeller = spawnNpc(BANK_TELLER_ID, BANK_POSITION) + val bankTeller = world.spawnNpc(BANK_TELLER_ID, BANK_POSITION) // @todo - these option numbers only match by coincidence, we should be looking up the correct ones - interactWith(bankTeller, option = 2) - waitForActionCompletion() + player.interactWith(bankTeller, option = 2) + player.waitForActionCompletion() verify(player).openBank() } @Test fun `Interacting with a bank booth object should open the players bank`() { - val bankBooth = spawnObject(BANK_BOOTH_ID, BANK_POSITION) + val bankBooth = world.spawnObject(BANK_BOOTH_ID, BANK_POSITION) - interactWith(bankBooth, option = 2) - waitForActionCompletion() + player.interactWith(bankBooth, option = 2) + player.waitForActionCompletion() verify(player).openBank() } diff --git a/game/src/plugins/chat/private-messaging/src/friends.plugin.kts b/game/src/plugins/chat/private-messaging/src/friends.plugin.kts index ea7ced88..6ac357d7 100644 --- a/game/src/plugins/chat/private-messaging/src/friends.plugin.kts +++ b/game/src/plugins/chat/private-messaging/src/friends.plugin.kts @@ -3,21 +3,19 @@ import org.apollo.game.message.impl.SendFriendMessage import org.apollo.game.model.entity.setting.PrivacyState on { AddFriendMessage::class } - .then { - it.addFriend(username) + .then { + it.addFriend(username) - val playerUsername = it.username - val friend = it.world.getPlayer(username) + val friend = it.world.getPlayer(username) - if (friend == null) { - it.send(SendFriendMessage(username, 0)) - } else if (friend.friendsWith(playerUsername) || friend.friendPrivacy == PrivacyState.ON) { - if (it.friendPrivacy != PrivacyState.OFF) { - friend.send(SendFriendMessage(playerUsername, it.worldId)) - } - - if (friend.friendPrivacy != PrivacyState.OFF) { - it.send(SendFriendMessage(username, friend.worldId)) - } - } + if (friend == null || friend.friendPrivacy == PrivacyState.OFF) { + it.send(SendFriendMessage(username, 0)) + return@then + } else { + it.send(SendFriendMessage(username, friend.worldId)) } + + if (friend.friendsWith(it.username) && it.friendPrivacy != PrivacyState.OFF) { + friend.send(SendFriendMessage(it.username, it.worldId)) + } + } diff --git a/game/src/plugins/dummy/test/TrainingDummyTest.kt b/game/src/plugins/dummy/test/TrainingDummyTest.kt index 0fc2da24..2e99e313 100644 --- a/game/src/plugins/dummy/test/TrainingDummyTest.kt +++ b/game/src/plugins/dummy/test/TrainingDummyTest.kt @@ -1,10 +1,10 @@ import org.apollo.game.model.Position import org.apollo.game.model.entity.Skill -import org.apollo.game.plugins.testing.KotlinPluginTest -import org.hamcrest.Matchers.* -import org.junit.Assert.* +import org.apollo.game.plugin.testing.* +import org.assertj.core.api.Assertions.assertThat import org.junit.Test -import org.mockito.Mockito.* +import org.mockito.Mockito.contains +import org.mockito.Mockito.verify class TrainingDummyTest : KotlinPluginTest() { @@ -14,30 +14,30 @@ class TrainingDummyTest : KotlinPluginTest() { } @Test fun `Hitting the training dummy should give the player attack experience`() { - val dummy = spawnObject(DUMMY_ID, DUMMY_POSITION) + val dummy = world.spawnObject(DUMMY_ID, DUMMY_POSITION) val skills = player.skillSet val beforeExp = skills.getExperience(Skill.ATTACK) - interactWith(dummy, option = 2) - waitForActionCompletion() + player.interactWith(dummy, option = 2) + player.waitForActionCompletion() val afterExp = skills.getExperience(Skill.ATTACK) - assertThat(afterExp, greaterThan(beforeExp)) + assertThat(afterExp).isGreaterThan(beforeExp) } @Test fun `The player should stop getting attack experience from the training dummy at level 8`() { - val dummy = spawnObject(DUMMY_ID, DUMMY_POSITION) + val dummy = world.spawnObject(DUMMY_ID, DUMMY_POSITION) val skills = player.skillSet skills.setMaximumLevel(Skill.ATTACK, 8) val beforeExp = skills.getExperience(Skill.ATTACK) - interactWith(dummy, option = 2) - waitForActionCompletion() + player.interactWith(dummy, option = 2) + player.waitForActionCompletion() val afterExp = skills.getExperience(Skill.ATTACK) verify(player).sendMessage(contains("nothing more you can learn")) - assertThat(afterExp, equalTo(beforeExp)) + assertThat(afterExp).isEqualTo(beforeExp) } -} \ No newline at end of file +} diff --git a/game/src/plugins/stub/stub.kt b/game/src/plugins/stub/stub.kt deleted file mode 100644 index f82da254..00000000 --- a/game/src/plugins/stub/stub.kt +++ /dev/null @@ -1,29 +0,0 @@ -/** - * NOTE: This file is a stub, intended only for use within an IDE. It should be updated - * each time [org.apollo.game.plugin.kotlin.KotlinPluginScript] has a new method added to it. - * - * Until IntelliJ IDEA starts to support ScriptTemplateDefinitions this is - * required to resolve references within plugin code. - */ - -import org.apollo.game.model.World -import org.apollo.game.plugin.PluginContext -import org.apollo.game.plugin.kotlin.* -import org.apollo.net.message.Message -import kotlin.reflect.KClass - -fun on(type: () -> KClass): KotlinMessageHandler { - null!! -} - -fun on_command(command: String, privileges: PrivilegeLevel): KotlinCommandHandler { - null!! -} - -fun start(callback: (World) -> Unit) { - -} - -fun stop(callback: (World) -> Unit) { - -} diff --git a/game/src/plugins/util/lookup/test/LookupTests.kt b/game/src/plugins/util/lookup/test/LookupTests.kt index 545cbc41..fc4c18f9 100644 --- a/game/src/plugins/util/lookup/test/LookupTests.kt +++ b/game/src/plugins/util/lookup/test/LookupTests.kt @@ -1,14 +1,15 @@ import org.apollo.cache.def.ItemDefinition +import org.apollo.game.plugin.testing.* +import org.assertj.core.api.Assertions.assertThat import org.junit.Test -import kotlin.test.assertEquals -class LookupTests { +class LookupTests : KotlinPluginTest() { @Test fun itemLookup() { val testItem = ItemDefinition(0) testItem.name = "sword" ItemDefinition.init(arrayOf(testItem)) - assertEquals(testItem, lookup_item("sword")) + assertThat(lookup_item("sword")).isEqualTo(testItem); } } \ No newline at end of file