diff --git a/.editorconfig b/.editorconfig
index 69f4250be..3a188f337 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -8,6 +8,16 @@ insert_final_newline = false
indent_style=tab
tab_width=4
+[*.{kt, kts}]
+indent_style=space
+tab_width=4
+continuation_indent_size=4
+
+[*.gradle]
+indent_style=space
+tab_width=4
+continuation_indent_size=4
+
[*.rb]
indent_style=space
-indent_size=2
\ No newline at end of file
+indent_size=2
diff --git a/.gitignore b/.gitignore
index 1b37aca00..4f11942ef 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,8 +9,10 @@
*.class
*.iml
-/data/rsa.pem
-/data/savedGames
+/game/data/rsa.pem
+/game/data/savedGames
/lib/
*/target/
*/build/
+**/build/
+**/out/
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 9bcf99945..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,3 +0,0 @@
-language: java
-jdk:
- - oraclejdk8
diff --git a/Jenkinsfile b/Jenkinsfile
new file mode 100644
index 000000000..7fb4e676b
--- /dev/null
+++ b/Jenkinsfile
@@ -0,0 +1,14 @@
+node {
+ stage 'Stage Checkout'
+ checkout scm
+
+ stage 'Stage Build'
+ gradle 'clean assemble'
+
+ stage 'Stage Test'
+ gradle 'check'
+}
+
+def gradle(command) {
+ sh "${tool name: 'gradle', type: 'hudson.plugins.gradle.GradleInstallation'}/bin/gradle ${command}"
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index 208f7ba20..fca367c15 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@ Apollo is a high-performance, modular RuneScape emulator with a collection of ut
### Developer information
-Most discussion related to the development of Apollo happens on our [Slack team](https://join.slack.com/t/apollo-rsps/shared_invite/enQtMjQ0NTYwNzkwMjExLTI5NGVmOWZjZGRkYzY4NjM1MjgxNjYyYmEyZWQxMzcxZTA5NDM1MGJkNmRkMjc2ZDQ2NjUwMjAzOGI1NjY1Zjk). If you have a problem and can't get in touch with anyone, create a GitHub issue. If making a pull request, please make sure all tests are still passing after making your changes, and that your code style is consistent with the rest of Apollo.
+Most discussion related to the development of Apollo happens on our [Discord](https://discord.gg/Fuft67P). If you have a problem and can't get in touch with anyone, create a GitHub issue. If making a pull request, please make sure all tests are still passing after making your changes, and that your code style is consistent with the rest of Apollo.
### Getting started
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
new file mode 100644
index 000000000..2167045f3
--- /dev/null
+++ b/azure-pipelines.yml
@@ -0,0 +1,47 @@
+pool:
+ vmImage: 'ubuntu-latest'
+
+variables:
+ GRADLE_USER_HOME: $(Pipeline.Workspace)/.gradle
+
+steps:
+ - task: CacheBeta@0
+ inputs:
+ key: $(Agent.OS)
+ path: $(GRADLE_USER_HOME)
+ displayName: "Gradle: setup build cache"
+
+ - task: SonarCloudPrepare@1
+ inputs:
+ SonarCloud: 'apollo-rsps-sonarcloud'
+ organization: 'apollo-rsps'
+ scannerMode: 'Other'
+ displayName: "SonarCloud: prepare analysis"
+
+ - task: Gradle@2
+ displayName: "Gradle: build"
+ inputs:
+ workingDirectory: ''
+ gradleWrapperFile: 'gradlew'
+ gradleOptions: '-Xmx3072m -Dorg.gradle.parallel=true -Dorg.gradle.caching=true -Dsonar.host.url=https://sonarcloud.io'
+ javaHomeOption: 'JDKVersion'
+ jdkVersionOption: '1.8'
+ jdkArchitectureOption: 'x64'
+ publishJUnitResults: true
+ testResultsFiles: '**/TEST-*.xml'
+ tasks: 'check jacocoTestReport sonarqube'
+
+ - script: |
+ ./gradlew --stop
+ displayName: "Gradle: stop daemon"
+
+ - task: SonarCloudPublish@1
+ inputs:
+ pollingTimeoutSec: '300'
+ displayName: "SonarCloud: publish quality gate"
+
+ - script: |
+ bash <(curl -s https://codecov.io/bash) -t "${CODECOV_TOKEN}"
+ env:
+ CODECOV_TOKEN: $(CODECOV_TOKEN)
+ displayName: "Codecov: publish coverage"
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index bdaa13e15..c1756fcf0 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,51 +1,34 @@
+plugins {
+ id 'org.jetbrains.kotlin.jvm' version '1.3.41' apply(false)
+ id 'org.jetbrains.intellij' version '0.4.9' apply(false)
+ id 'org.jmailen.kotlinter' version '1.26.0' apply(false)
+ id 'org.sonarqube' version '2.7.1'
+ id "io.gitlab.arturbosch.detekt" version '1.0.0-RC16'
+}
+
allprojects {
group = 'apollo'
version = '0.0.1'
- apply plugin: 'java'
-}
-
-subprojects {
- sourceCompatibility = 1.8
- targetCompatibility = 1.8
repositories {
mavenLocal()
maven { url "https://repo.maven.apache.org/maven2" }
+ maven { url "https://dl.bintray.com/kotlin/kotlinx/" }
}
+}
- dependencies {
- compile group: 'org.apache.commons', name: 'commons-compress', version: '1.10'
- compile group: 'org.jruby', name: 'jruby-complete', version: '9.0.5.0'
- compile group: 'com.google.guava', name: 'guava', version: '19.0'
- compile group: 'io.netty', name: 'netty-all', version: '4.0.34.Final'
- compile group: 'com.lambdaworks', name: 'scrypt', version: '1.4.0'
- compile group: 'com.mchange', name: 'c3p0', version: '0.9.5.2'
- compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.54'
- testCompile group: 'junit', name: 'junit', version: '4.12'
- testCompile group: 'org.powermock', name: 'powermock-module-junit4', version: '1.6.4'
- testCompile group: 'org.powermock', name: 'powermock-api-mockito', version: '1.6.4'
- }
+apply from: 'gradle/properties.gradle'
+apply from: 'gradle/code-quality.gradle'
+apply from: 'gradle/testing.gradle'
+apply from: 'gradle/wrapper.gradle'
- sourceSets {
- main {
- java {
- srcDirs = ['src/main']
- }
- }
+gradle.projectsEvaluated {
+ task check {
+ def deps = []
+ deps += getTasksByName("check", true).findAll { it.project != rootProject }
+ deps += "detekt"
+ deps += jacocoReport
- test {
- java {
- srcDirs = ['src/test']
- }
- }
+ dependsOn(deps)
}
-}
-
-task(run, dependsOn: classes, type: JavaExec) {
- def gameSubproject = project(':game')
- def gameClasspath = gameSubproject.sourceSets.main.runtimeClasspath
-
- main = 'org.apollo.Server'
- classpath = gameClasspath
- jvmArgs = ['-Xmx1750M']
-}
+}
\ No newline at end of file
diff --git a/cache/build.gradle b/cache/build.gradle
index 47511913d..af79e12dd 100644
--- a/cache/build.gradle
+++ b/cache/build.gradle
@@ -1,5 +1,14 @@
+apply plugin: 'java-library'
+
description = 'Apollo Cache'
dependencies {
- compile project(':util')
+ implementation project(':util')
+ implementation group: 'com.google.guava', name: 'guava', version: guavaVersion
+
+ test.useJUnitPlatform()
+ testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: junitJupiterVersion
+ testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: junitJupiterVersion
+ testImplementation group: 'org.junit.vintage', name: 'junit-vintage-engine', version: junitVintageVersion
+ testImplementation group: 'org.junit.platform', name: 'junit-platform-launcher', version: junitPlatformVersion
}
diff --git a/cache/src/main/org/apollo/cache/FileDescriptor.java b/cache/src/main/java/org/apollo/cache/FileDescriptor.java
similarity index 100%
rename from cache/src/main/org/apollo/cache/FileDescriptor.java
rename to cache/src/main/java/org/apollo/cache/FileDescriptor.java
diff --git a/cache/src/main/org/apollo/cache/FileSystemConstants.java b/cache/src/main/java/org/apollo/cache/FileSystemConstants.java
similarity index 100%
rename from cache/src/main/org/apollo/cache/FileSystemConstants.java
rename to cache/src/main/java/org/apollo/cache/FileSystemConstants.java
diff --git a/cache/src/main/org/apollo/cache/Index.java b/cache/src/main/java/org/apollo/cache/Index.java
similarity index 100%
rename from cache/src/main/org/apollo/cache/Index.java
rename to cache/src/main/java/org/apollo/cache/Index.java
diff --git a/cache/src/main/org/apollo/cache/IndexedFileSystem.java b/cache/src/main/java/org/apollo/cache/IndexedFileSystem.java
similarity index 100%
rename from cache/src/main/org/apollo/cache/IndexedFileSystem.java
rename to cache/src/main/java/org/apollo/cache/IndexedFileSystem.java
diff --git a/cache/src/main/org/apollo/cache/archive/Archive.java b/cache/src/main/java/org/apollo/cache/archive/Archive.java
similarity index 100%
rename from cache/src/main/org/apollo/cache/archive/Archive.java
rename to cache/src/main/java/org/apollo/cache/archive/Archive.java
diff --git a/cache/src/main/org/apollo/cache/archive/ArchiveEntry.java b/cache/src/main/java/org/apollo/cache/archive/ArchiveEntry.java
similarity index 100%
rename from cache/src/main/org/apollo/cache/archive/ArchiveEntry.java
rename to cache/src/main/java/org/apollo/cache/archive/ArchiveEntry.java
diff --git a/cache/src/main/org/apollo/cache/archive/package-info.java b/cache/src/main/java/org/apollo/cache/archive/package-info.java
similarity index 100%
rename from cache/src/main/org/apollo/cache/archive/package-info.java
rename to cache/src/main/java/org/apollo/cache/archive/package-info.java
diff --git a/cache/src/main/org/apollo/cache/decoder/ItemDefinitionDecoder.java b/cache/src/main/java/org/apollo/cache/decoder/ItemDefinitionDecoder.java
similarity index 100%
rename from cache/src/main/org/apollo/cache/decoder/ItemDefinitionDecoder.java
rename to cache/src/main/java/org/apollo/cache/decoder/ItemDefinitionDecoder.java
diff --git a/cache/src/main/org/apollo/cache/decoder/NpcDefinitionDecoder.java b/cache/src/main/java/org/apollo/cache/decoder/NpcDefinitionDecoder.java
similarity index 100%
rename from cache/src/main/org/apollo/cache/decoder/NpcDefinitionDecoder.java
rename to cache/src/main/java/org/apollo/cache/decoder/NpcDefinitionDecoder.java
diff --git a/cache/src/main/org/apollo/cache/decoder/ObjectDefinitionDecoder.java b/cache/src/main/java/org/apollo/cache/decoder/ObjectDefinitionDecoder.java
similarity index 100%
rename from cache/src/main/org/apollo/cache/decoder/ObjectDefinitionDecoder.java
rename to cache/src/main/java/org/apollo/cache/decoder/ObjectDefinitionDecoder.java
diff --git a/cache/src/main/org/apollo/cache/decoder/package-info.java b/cache/src/main/java/org/apollo/cache/decoder/package-info.java
similarity index 100%
rename from cache/src/main/org/apollo/cache/decoder/package-info.java
rename to cache/src/main/java/org/apollo/cache/decoder/package-info.java
diff --git a/cache/src/main/org/apollo/cache/def/EquipmentDefinition.java b/cache/src/main/java/org/apollo/cache/def/EquipmentDefinition.java
similarity index 100%
rename from cache/src/main/org/apollo/cache/def/EquipmentDefinition.java
rename to cache/src/main/java/org/apollo/cache/def/EquipmentDefinition.java
diff --git a/cache/src/main/org/apollo/cache/def/ItemDefinition.java b/cache/src/main/java/org/apollo/cache/def/ItemDefinition.java
similarity index 100%
rename from cache/src/main/org/apollo/cache/def/ItemDefinition.java
rename to cache/src/main/java/org/apollo/cache/def/ItemDefinition.java
diff --git a/cache/src/main/org/apollo/cache/def/NpcDefinition.java b/cache/src/main/java/org/apollo/cache/def/NpcDefinition.java
similarity index 100%
rename from cache/src/main/org/apollo/cache/def/NpcDefinition.java
rename to cache/src/main/java/org/apollo/cache/def/NpcDefinition.java
diff --git a/cache/src/main/org/apollo/cache/def/ObjectDefinition.java b/cache/src/main/java/org/apollo/cache/def/ObjectDefinition.java
similarity index 100%
rename from cache/src/main/org/apollo/cache/def/ObjectDefinition.java
rename to cache/src/main/java/org/apollo/cache/def/ObjectDefinition.java
diff --git a/cache/src/main/org/apollo/cache/def/package-info.java b/cache/src/main/java/org/apollo/cache/def/package-info.java
similarity index 100%
rename from cache/src/main/org/apollo/cache/def/package-info.java
rename to cache/src/main/java/org/apollo/cache/def/package-info.java
diff --git a/cache/src/main/org/apollo/cache/map/MapConstants.java b/cache/src/main/java/org/apollo/cache/map/MapConstants.java
similarity index 100%
rename from cache/src/main/org/apollo/cache/map/MapConstants.java
rename to cache/src/main/java/org/apollo/cache/map/MapConstants.java
diff --git a/cache/src/main/org/apollo/cache/map/MapFile.java b/cache/src/main/java/org/apollo/cache/map/MapFile.java
similarity index 100%
rename from cache/src/main/org/apollo/cache/map/MapFile.java
rename to cache/src/main/java/org/apollo/cache/map/MapFile.java
diff --git a/cache/src/main/org/apollo/cache/map/MapFileDecoder.java b/cache/src/main/java/org/apollo/cache/map/MapFileDecoder.java
similarity index 100%
rename from cache/src/main/org/apollo/cache/map/MapFileDecoder.java
rename to cache/src/main/java/org/apollo/cache/map/MapFileDecoder.java
diff --git a/cache/src/main/org/apollo/cache/map/MapIndex.java b/cache/src/main/java/org/apollo/cache/map/MapIndex.java
similarity index 100%
rename from cache/src/main/org/apollo/cache/map/MapIndex.java
rename to cache/src/main/java/org/apollo/cache/map/MapIndex.java
diff --git a/cache/src/main/org/apollo/cache/map/MapIndexDecoder.java b/cache/src/main/java/org/apollo/cache/map/MapIndexDecoder.java
similarity index 100%
rename from cache/src/main/org/apollo/cache/map/MapIndexDecoder.java
rename to cache/src/main/java/org/apollo/cache/map/MapIndexDecoder.java
diff --git a/cache/src/main/org/apollo/cache/map/MapObject.java b/cache/src/main/java/org/apollo/cache/map/MapObject.java
similarity index 100%
rename from cache/src/main/org/apollo/cache/map/MapObject.java
rename to cache/src/main/java/org/apollo/cache/map/MapObject.java
diff --git a/cache/src/main/org/apollo/cache/map/MapObjectsDecoder.java b/cache/src/main/java/org/apollo/cache/map/MapObjectsDecoder.java
similarity index 100%
rename from cache/src/main/org/apollo/cache/map/MapObjectsDecoder.java
rename to cache/src/main/java/org/apollo/cache/map/MapObjectsDecoder.java
diff --git a/cache/src/main/org/apollo/cache/map/MapPlane.java b/cache/src/main/java/org/apollo/cache/map/MapPlane.java
similarity index 100%
rename from cache/src/main/org/apollo/cache/map/MapPlane.java
rename to cache/src/main/java/org/apollo/cache/map/MapPlane.java
diff --git a/cache/src/main/org/apollo/cache/map/Tile.java b/cache/src/main/java/org/apollo/cache/map/Tile.java
similarity index 100%
rename from cache/src/main/org/apollo/cache/map/Tile.java
rename to cache/src/main/java/org/apollo/cache/map/Tile.java
diff --git a/cache/src/main/org/apollo/cache/map/TileUtils.java b/cache/src/main/java/org/apollo/cache/map/TileUtils.java
similarity index 100%
rename from cache/src/main/org/apollo/cache/map/TileUtils.java
rename to cache/src/main/java/org/apollo/cache/map/TileUtils.java
diff --git a/cache/src/main/org/apollo/cache/package-info.java b/cache/src/main/java/org/apollo/cache/package-info.java
similarity index 100%
rename from cache/src/main/org/apollo/cache/package-info.java
rename to cache/src/main/java/org/apollo/cache/package-info.java
diff --git a/cache/src/main/org/apollo/cache/tools/EquipmentUpdater.java b/cache/src/main/java/org/apollo/cache/tools/EquipmentUpdater.java
similarity index 100%
rename from cache/src/main/org/apollo/cache/tools/EquipmentUpdater.java
rename to cache/src/main/java/org/apollo/cache/tools/EquipmentUpdater.java
diff --git a/cache/src/main/org/apollo/cache/tools/package-info.java b/cache/src/main/java/org/apollo/cache/tools/package-info.java
similarity index 100%
rename from cache/src/main/org/apollo/cache/tools/package-info.java
rename to cache/src/main/java/org/apollo/cache/tools/package-info.java
diff --git a/game/build.gradle b/game/build.gradle
index a96d17a99..1a451eb99 100644
--- a/game/build.gradle
+++ b/game/build.gradle
@@ -1,7 +1,45 @@
+apply plugin: 'application'
+apply plugin: 'org.jetbrains.kotlin.jvm'
+apply from: "$rootDir/gradle/kotlin.gradle"
+
description = 'Apollo Game'
+mainClassName = 'org.apollo.Server'
dependencies {
compile project(':cache')
compile project(':net')
compile project(':util')
+
+ compile group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-jdk8'
+ compile group: 'org.jetbrains.kotlin', name: 'kotlin-scripting-common'
+ compile group: 'org.jetbrains.kotlin', name: 'kotlin-script-runtime'
+ compile group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-jdk8', version: kotlinxCoroutinesVersion
+ compile group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-core', version: kotlinxCoroutinesVersion
+
+ implementation group: 'com.google.guava', name: 'guava', version: guavaVersion
+ implementation group: 'io.github.classgraph', name: 'classgraph', version: classGraphVersion
+ implementation group: 'com.lambdaworks', name: 'scrypt', version: scryptVersion
+
+ test.useJUnitPlatform()
+ testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: junitJupiterVersion
+ testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: junitJupiterVersion
+ testImplementation group: 'org.junit.vintage', name: 'junit-vintage-engine', version: junitVintageVersion
+ testImplementation group: 'org.junit.platform', name: 'junit-platform-launcher', version: junitPlatformVersion
+
+ testImplementation group: 'junit', name: 'junit', version: junitVersion
+ testImplementation group: 'org.powermock', name: 'powermock-module-junit4', version: powermockVersion
+ testImplementation group: 'org.powermock', name: 'powermock-api-mockito2', version: powermockVersion
+ testImplementation group: 'org.assertj', name: 'assertj-core', version: assertjVersion
+
+ project(":game:plugin").subprojects { pluginProject ->
+ if (pluginProject.buildFile.exists()) {
+ runtimeClasspath pluginProject
+ }
+ }
+}
+
+applicationDistribution.from("$rootDir/data") {
+ include '*.dat'
+ include '*.xml'
+ into "data/"
}
diff --git a/data/equipment-317.dat b/game/data/equipment-317.dat
similarity index 100%
rename from data/equipment-317.dat
rename to game/data/equipment-317.dat
diff --git a/data/equipment-377.dat b/game/data/equipment-377.dat
similarity index 100%
rename from data/equipment-377.dat
rename to game/data/equipment-377.dat
diff --git a/data/fs/.gitignore b/game/data/fs/.gitignore
similarity index 100%
rename from data/fs/.gitignore
rename to game/data/fs/.gitignore
diff --git a/data/login.xml b/game/data/login.xml
similarity index 100%
rename from data/login.xml
rename to game/data/login.xml
diff --git a/data/messages.xml b/game/data/messages.xml
similarity index 95%
rename from data/messages.xml
rename to game/data/messages.xml
index 910c4a15a..f4a5bec2e 100644
--- a/data/messages.xml
+++ b/game/data/messages.xml
@@ -71,6 +71,12 @@
org.apollo.game.message.handler.ItemVerificationHandler
+
+ org.apollo.game.message.impl.MagicOnMobMessage
+
+ org.apollo.game.message.handler.MagicOnMobVerificationHandler
+
+
org.apollo.game.message.impl.NpcActionMessage
diff --git a/data/net.xml b/game/data/net.xml
similarity index 100%
rename from data/net.xml
rename to game/data/net.xml
diff --git a/data/plugins/.rubocop.yml b/game/data/plugins/.rubocop.yml
similarity index 100%
rename from data/plugins/.rubocop.yml
rename to game/data/plugins/.rubocop.yml
diff --git a/data/plugins/areas/actions.rb b/game/data/plugins/areas/actions.rb
similarity index 100%
rename from data/plugins/areas/actions.rb
rename to game/data/plugins/areas/actions.rb
diff --git a/data/plugins/areas/areas.rb b/game/data/plugins/areas/areas.rb
similarity index 100%
rename from data/plugins/areas/areas.rb
rename to game/data/plugins/areas/areas.rb
diff --git a/data/plugins/areas/plugin.xml b/game/data/plugins/areas/plugin.xml
similarity index 100%
rename from data/plugins/areas/plugin.xml
rename to game/data/plugins/areas/plugin.xml
diff --git a/data/plugins/bank/bank.rb b/game/data/plugins/bank/bank.rb
similarity index 100%
rename from data/plugins/bank/bank.rb
rename to game/data/plugins/bank/bank.rb
diff --git a/data/plugins/bank/plugin.xml b/game/data/plugins/bank/plugin.xml
similarity index 100%
rename from data/plugins/bank/plugin.xml
rename to game/data/plugins/bank/plugin.xml
diff --git a/data/plugins/bootstrap.rb b/game/data/plugins/bootstrap.rb
similarity index 100%
rename from data/plugins/bootstrap.rb
rename to game/data/plugins/bootstrap.rb
diff --git a/data/plugins/chat/privacy/plugin.xml b/game/data/plugins/chat/privacy/plugin.xml
similarity index 100%
rename from data/plugins/chat/privacy/plugin.xml
rename to game/data/plugins/chat/privacy/plugin.xml
diff --git a/data/plugins/chat/privacy/privacy.rb b/game/data/plugins/chat/privacy/privacy.rb
similarity index 100%
rename from data/plugins/chat/privacy/privacy.rb
rename to game/data/plugins/chat/privacy/privacy.rb
diff --git a/data/plugins/chat/private-messaging/friend.rb b/game/data/plugins/chat/private-messaging/friend.rb
similarity index 100%
rename from data/plugins/chat/private-messaging/friend.rb
rename to game/data/plugins/chat/private-messaging/friend.rb
diff --git a/data/plugins/chat/private-messaging/ignore.rb b/game/data/plugins/chat/private-messaging/ignore.rb
similarity index 100%
rename from data/plugins/chat/private-messaging/ignore.rb
rename to game/data/plugins/chat/private-messaging/ignore.rb
diff --git a/data/plugins/chat/private-messaging/messaging.rb b/game/data/plugins/chat/private-messaging/messaging.rb
similarity index 100%
rename from data/plugins/chat/private-messaging/messaging.rb
rename to game/data/plugins/chat/private-messaging/messaging.rb
diff --git a/data/plugins/chat/private-messaging/plugin.xml b/game/data/plugins/chat/private-messaging/plugin.xml
similarity index 100%
rename from data/plugins/chat/private-messaging/plugin.xml
rename to game/data/plugins/chat/private-messaging/plugin.xml
diff --git a/data/plugins/cmd/animate/animate.rb b/game/data/plugins/cmd/animate/animate.rb
similarity index 100%
rename from data/plugins/cmd/animate/animate.rb
rename to game/data/plugins/cmd/animate/animate.rb
diff --git a/data/plugins/cmd/animate/plugin.xml b/game/data/plugins/cmd/animate/plugin.xml
similarity index 100%
rename from data/plugins/cmd/animate/plugin.xml
rename to game/data/plugins/cmd/animate/plugin.xml
diff --git a/data/plugins/cmd/bank/bank.rb b/game/data/plugins/cmd/bank/bank.rb
similarity index 100%
rename from data/plugins/cmd/bank/bank.rb
rename to game/data/plugins/cmd/bank/bank.rb
diff --git a/data/plugins/cmd/bank/plugin.xml b/game/data/plugins/cmd/bank/plugin.xml
similarity index 100%
rename from data/plugins/cmd/bank/plugin.xml
rename to game/data/plugins/cmd/bank/plugin.xml
diff --git a/data/plugins/cmd/item/item.rb b/game/data/plugins/cmd/item/item.rb
similarity index 100%
rename from data/plugins/cmd/item/item.rb
rename to game/data/plugins/cmd/item/item.rb
diff --git a/data/plugins/cmd/item/plugin.xml b/game/data/plugins/cmd/item/plugin.xml
similarity index 100%
rename from data/plugins/cmd/item/plugin.xml
rename to game/data/plugins/cmd/item/plugin.xml
diff --git a/data/plugins/cmd/lookup/lookup.rb b/game/data/plugins/cmd/lookup/lookup.rb
similarity index 100%
rename from data/plugins/cmd/lookup/lookup.rb
rename to game/data/plugins/cmd/lookup/lookup.rb
diff --git a/data/plugins/cmd/lookup/plugin.xml b/game/data/plugins/cmd/lookup/plugin.xml
similarity index 100%
rename from data/plugins/cmd/lookup/plugin.xml
rename to game/data/plugins/cmd/lookup/plugin.xml
diff --git a/data/plugins/cmd/messaging/broadcast.rb b/game/data/plugins/cmd/messaging/broadcast.rb
similarity index 100%
rename from data/plugins/cmd/messaging/broadcast.rb
rename to game/data/plugins/cmd/messaging/broadcast.rb
diff --git a/data/plugins/cmd/messaging/plugin.xml b/game/data/plugins/cmd/messaging/plugin.xml
similarity index 100%
rename from data/plugins/cmd/messaging/plugin.xml
rename to game/data/plugins/cmd/messaging/plugin.xml
diff --git a/data/plugins/cmd/npc/plugin.xml b/game/data/plugins/cmd/npc/plugin.xml
similarity index 100%
rename from data/plugins/cmd/npc/plugin.xml
rename to game/data/plugins/cmd/npc/plugin.xml
diff --git a/data/plugins/cmd/npc/spawn.rb b/game/data/plugins/cmd/npc/spawn.rb
similarity index 100%
rename from data/plugins/cmd/npc/spawn.rb
rename to game/data/plugins/cmd/npc/spawn.rb
diff --git a/data/plugins/cmd/punishment/plugin.xml b/game/data/plugins/cmd/punishment/plugin.xml
similarity index 100%
rename from data/plugins/cmd/punishment/plugin.xml
rename to game/data/plugins/cmd/punishment/plugin.xml
diff --git a/data/plugins/cmd/punishment/punish.rb b/game/data/plugins/cmd/punishment/punish.rb
similarity index 100%
rename from data/plugins/cmd/punishment/punish.rb
rename to game/data/plugins/cmd/punishment/punish.rb
diff --git a/data/plugins/cmd/skill/plugin.xml b/game/data/plugins/cmd/skill/plugin.xml
similarity index 100%
rename from data/plugins/cmd/skill/plugin.xml
rename to game/data/plugins/cmd/skill/plugin.xml
diff --git a/data/plugins/cmd/skill/skill.rb b/game/data/plugins/cmd/skill/skill.rb
similarity index 100%
rename from data/plugins/cmd/skill/skill.rb
rename to game/data/plugins/cmd/skill/skill.rb
diff --git a/data/plugins/cmd/teleport/plugin.xml b/game/data/plugins/cmd/teleport/plugin.xml
similarity index 100%
rename from data/plugins/cmd/teleport/plugin.xml
rename to game/data/plugins/cmd/teleport/plugin.xml
diff --git a/data/plugins/cmd/teleport/teleport.rb b/game/data/plugins/cmd/teleport/teleport.rb
similarity index 100%
rename from data/plugins/cmd/teleport/teleport.rb
rename to game/data/plugins/cmd/teleport/teleport.rb
diff --git a/data/plugins/combat/plugin.xml b/game/data/plugins/combat/plugin.xml
similarity index 100%
rename from data/plugins/combat/plugin.xml
rename to game/data/plugins/combat/plugin.xml
diff --git a/data/plugins/combat/wilderness.rb b/game/data/plugins/combat/wilderness.rb
similarity index 100%
rename from data/plugins/combat/wilderness.rb
rename to game/data/plugins/combat/wilderness.rb
diff --git a/data/plugins/consumables/consumable.rb b/game/data/plugins/consumables/consumable.rb
similarity index 100%
rename from data/plugins/consumables/consumable.rb
rename to game/data/plugins/consumables/consumable.rb
diff --git a/data/plugins/consumables/drink.rb b/game/data/plugins/consumables/drink.rb
similarity index 100%
rename from data/plugins/consumables/drink.rb
rename to game/data/plugins/consumables/drink.rb
diff --git a/data/plugins/consumables/food.rb b/game/data/plugins/consumables/food.rb
similarity index 100%
rename from data/plugins/consumables/food.rb
rename to game/data/plugins/consumables/food.rb
diff --git a/data/plugins/consumables/plugin.xml b/game/data/plugins/consumables/plugin.xml
similarity index 100%
rename from data/plugins/consumables/plugin.xml
rename to game/data/plugins/consumables/plugin.xml
diff --git a/data/plugins/consumables/potions.rb b/game/data/plugins/consumables/potions.rb
similarity index 100%
rename from data/plugins/consumables/potions.rb
rename to game/data/plugins/consumables/potions.rb
diff --git a/data/plugins/dialogue/dialogue.rb b/game/data/plugins/dialogue/dialogue.rb
similarity index 100%
rename from data/plugins/dialogue/dialogue.rb
rename to game/data/plugins/dialogue/dialogue.rb
diff --git a/data/plugins/dialogue/emotes.rb b/game/data/plugins/dialogue/emotes.rb
similarity index 100%
rename from data/plugins/dialogue/emotes.rb
rename to game/data/plugins/dialogue/emotes.rb
diff --git a/data/plugins/dialogue/plugin.xml b/game/data/plugins/dialogue/plugin.xml
similarity index 100%
rename from data/plugins/dialogue/plugin.xml
rename to game/data/plugins/dialogue/plugin.xml
diff --git a/data/plugins/dummy/dummy.rb b/game/data/plugins/dummy/dummy.rb
similarity index 100%
rename from data/plugins/dummy/dummy.rb
rename to game/data/plugins/dummy/dummy.rb
diff --git a/data/plugins/dummy/plugin.xml b/game/data/plugins/dummy/plugin.xml
similarity index 100%
rename from data/plugins/dummy/plugin.xml
rename to game/data/plugins/dummy/plugin.xml
diff --git a/data/plugins/emote-tab/emote_tab.rb b/game/data/plugins/emote-tab/emote_tab.rb
similarity index 100%
rename from data/plugins/emote-tab/emote_tab.rb
rename to game/data/plugins/emote-tab/emote_tab.rb
diff --git a/data/plugins/emote-tab/plugin.xml b/game/data/plugins/emote-tab/plugin.xml
similarity index 100%
rename from data/plugins/emote-tab/plugin.xml
rename to game/data/plugins/emote-tab/plugin.xml
diff --git a/data/plugins/entity/attributes/attributes.rb b/game/data/plugins/entity/attributes/attributes.rb
similarity index 100%
rename from data/plugins/entity/attributes/attributes.rb
rename to game/data/plugins/entity/attributes/attributes.rb
diff --git a/data/plugins/entity/attributes/plugin.xml b/game/data/plugins/entity/attributes/plugin.xml
similarity index 100%
rename from data/plugins/entity/attributes/plugin.xml
rename to game/data/plugins/entity/attributes/plugin.xml
diff --git a/data/plugins/entity/mob/extension/extension.rb b/game/data/plugins/entity/mob/extension/extension.rb
similarity index 100%
rename from data/plugins/entity/mob/extension/extension.rb
rename to game/data/plugins/entity/mob/extension/extension.rb
diff --git a/data/plugins/entity/mob/extension/plugin.xml b/game/data/plugins/entity/mob/extension/plugin.xml
similarity index 100%
rename from data/plugins/entity/mob/extension/plugin.xml
rename to game/data/plugins/entity/mob/extension/plugin.xml
diff --git a/data/plugins/entity/mob/following/following.rb b/game/data/plugins/entity/mob/following/following.rb
similarity index 100%
rename from data/plugins/entity/mob/following/following.rb
rename to game/data/plugins/entity/mob/following/following.rb
diff --git a/data/plugins/entity/mob/following/plugin.xml b/game/data/plugins/entity/mob/following/plugin.xml
similarity index 100%
rename from data/plugins/entity/mob/following/plugin.xml
rename to game/data/plugins/entity/mob/following/plugin.xml
diff --git a/data/plugins/entity/mob/walk-to/plugin.xml b/game/data/plugins/entity/mob/walk-to/plugin.xml
similarity index 100%
rename from data/plugins/entity/mob/walk-to/plugin.xml
rename to game/data/plugins/entity/mob/walk-to/plugin.xml
diff --git a/data/plugins/entity/mob/walk-to/walk_to.rb b/game/data/plugins/entity/mob/walk-to/walk_to.rb
similarity index 100%
rename from data/plugins/entity/mob/walk-to/walk_to.rb
rename to game/data/plugins/entity/mob/walk-to/walk_to.rb
diff --git a/data/plugins/entity/spawning/npc-spawn.rb b/game/data/plugins/entity/spawning/npc-spawn.rb
similarity index 100%
rename from data/plugins/entity/spawning/npc-spawn.rb
rename to game/data/plugins/entity/spawning/npc-spawn.rb
diff --git a/data/plugins/entity/spawning/plugin.xml b/game/data/plugins/entity/spawning/plugin.xml
similarity index 100%
rename from data/plugins/entity/spawning/plugin.xml
rename to game/data/plugins/entity/spawning/plugin.xml
diff --git a/data/plugins/location/al-kharid/npcs.rb b/game/data/plugins/location/al-kharid/npcs.rb
similarity index 100%
rename from data/plugins/location/al-kharid/npcs.rb
rename to game/data/plugins/location/al-kharid/npcs.rb
diff --git a/data/plugins/location/al-kharid/plugin.xml b/game/data/plugins/location/al-kharid/plugin.xml
similarity index 100%
rename from data/plugins/location/al-kharid/plugin.xml
rename to game/data/plugins/location/al-kharid/plugin.xml
diff --git a/data/plugins/location/edgeville/npcs.rb b/game/data/plugins/location/edgeville/npcs.rb
similarity index 100%
rename from data/plugins/location/edgeville/npcs.rb
rename to game/data/plugins/location/edgeville/npcs.rb
diff --git a/data/plugins/location/edgeville/plugin.xml b/game/data/plugins/location/edgeville/plugin.xml
similarity index 100%
rename from data/plugins/location/edgeville/plugin.xml
rename to game/data/plugins/location/edgeville/plugin.xml
diff --git a/data/plugins/location/falador/npcs.rb b/game/data/plugins/location/falador/npcs.rb
similarity index 100%
rename from data/plugins/location/falador/npcs.rb
rename to game/data/plugins/location/falador/npcs.rb
diff --git a/data/plugins/location/falador/plugin.xml b/game/data/plugins/location/falador/plugin.xml
similarity index 100%
rename from data/plugins/location/falador/plugin.xml
rename to game/data/plugins/location/falador/plugin.xml
diff --git a/data/plugins/location/lumbridge/npcs.rb b/game/data/plugins/location/lumbridge/npcs.rb
similarity index 100%
rename from data/plugins/location/lumbridge/npcs.rb
rename to game/data/plugins/location/lumbridge/npcs.rb
diff --git a/data/plugins/location/lumbridge/plugin.xml b/game/data/plugins/location/lumbridge/plugin.xml
similarity index 100%
rename from data/plugins/location/lumbridge/plugin.xml
rename to game/data/plugins/location/lumbridge/plugin.xml
diff --git a/data/plugins/location/tutorial-island/guide.rb b/game/data/plugins/location/tutorial-island/guide.rb
similarity index 100%
rename from data/plugins/location/tutorial-island/guide.rb
rename to game/data/plugins/location/tutorial-island/guide.rb
diff --git a/data/plugins/location/tutorial-island/instructions.rb b/game/data/plugins/location/tutorial-island/instructions.rb
similarity index 100%
rename from data/plugins/location/tutorial-island/instructions.rb
rename to game/data/plugins/location/tutorial-island/instructions.rb
diff --git a/data/plugins/location/tutorial-island/npcs.rb b/game/data/plugins/location/tutorial-island/npcs.rb
similarity index 100%
rename from data/plugins/location/tutorial-island/npcs.rb
rename to game/data/plugins/location/tutorial-island/npcs.rb
diff --git a/data/plugins/location/tutorial-island/plugin.xml b/game/data/plugins/location/tutorial-island/plugin.xml
similarity index 100%
rename from data/plugins/location/tutorial-island/plugin.xml
rename to game/data/plugins/location/tutorial-island/plugin.xml
diff --git a/data/plugins/location/tutorial-island/stages.rb b/game/data/plugins/location/tutorial-island/stages.rb
similarity index 100%
rename from data/plugins/location/tutorial-island/stages.rb
rename to game/data/plugins/location/tutorial-island/stages.rb
diff --git a/data/plugins/location/tutorial-island/survival.rb b/game/data/plugins/location/tutorial-island/survival.rb
similarity index 100%
rename from data/plugins/location/tutorial-island/survival.rb
rename to game/data/plugins/location/tutorial-island/survival.rb
diff --git a/data/plugins/location/tutorial-island/utils.rb b/game/data/plugins/location/tutorial-island/utils.rb
similarity index 100%
rename from data/plugins/location/tutorial-island/utils.rb
rename to game/data/plugins/location/tutorial-island/utils.rb
diff --git a/data/plugins/location/varrock/npcs.rb b/game/data/plugins/location/varrock/npcs.rb
similarity index 100%
rename from data/plugins/location/varrock/npcs.rb
rename to game/data/plugins/location/varrock/npcs.rb
diff --git a/data/plugins/location/varrock/plugin.xml b/game/data/plugins/location/varrock/plugin.xml
similarity index 100%
rename from data/plugins/location/varrock/plugin.xml
rename to game/data/plugins/location/varrock/plugin.xml
diff --git a/data/plugins/location/varrock/shops.rb b/game/data/plugins/location/varrock/shops.rb
similarity index 100%
rename from data/plugins/location/varrock/shops.rb
rename to game/data/plugins/location/varrock/shops.rb
diff --git a/data/plugins/logout/logout.rb b/game/data/plugins/logout/logout.rb
similarity index 100%
rename from data/plugins/logout/logout.rb
rename to game/data/plugins/logout/logout.rb
diff --git a/data/plugins/logout/plugin.xml b/game/data/plugins/logout/plugin.xml
similarity index 100%
rename from data/plugins/logout/plugin.xml
rename to game/data/plugins/logout/plugin.xml
diff --git a/data/plugins/navigation/door/constants.rb b/game/data/plugins/navigation/door/constants.rb
similarity index 100%
rename from data/plugins/navigation/door/constants.rb
rename to game/data/plugins/navigation/door/constants.rb
diff --git a/data/plugins/navigation/door/door.rb b/game/data/plugins/navigation/door/door.rb
similarity index 100%
rename from data/plugins/navigation/door/door.rb
rename to game/data/plugins/navigation/door/door.rb
diff --git a/data/plugins/navigation/door/plugin.xml b/game/data/plugins/navigation/door/plugin.xml
similarity index 100%
rename from data/plugins/navigation/door/plugin.xml
rename to game/data/plugins/navigation/door/plugin.xml
diff --git a/data/plugins/navigation/door/util.rb b/game/data/plugins/navigation/door/util.rb
similarity index 100%
rename from data/plugins/navigation/door/util.rb
rename to game/data/plugins/navigation/door/util.rb
diff --git a/data/plugins/player-action/action.rb b/game/data/plugins/player-action/action.rb
similarity index 100%
rename from data/plugins/player-action/action.rb
rename to game/data/plugins/player-action/action.rb
diff --git a/data/plugins/player-action/login.rb b/game/data/plugins/player-action/login.rb
similarity index 100%
rename from data/plugins/player-action/login.rb
rename to game/data/plugins/player-action/login.rb
diff --git a/data/plugins/player-action/plugin.xml b/game/data/plugins/player-action/plugin.xml
similarity index 100%
rename from data/plugins/player-action/plugin.xml
rename to game/data/plugins/player-action/plugin.xml
diff --git a/data/plugins/quest/plugin.xml b/game/data/plugins/quest/plugin.xml
similarity index 100%
rename from data/plugins/quest/plugin.xml
rename to game/data/plugins/quest/plugin.xml
diff --git a/data/plugins/quest/repository.rb b/game/data/plugins/quest/repository.rb
similarity index 100%
rename from data/plugins/quest/repository.rb
rename to game/data/plugins/quest/repository.rb
diff --git a/data/plugins/run/plugin.xml b/game/data/plugins/run/plugin.xml
similarity index 100%
rename from data/plugins/run/plugin.xml
rename to game/data/plugins/run/plugin.xml
diff --git a/data/plugins/run/run.rb b/game/data/plugins/run/run.rb
similarity index 100%
rename from data/plugins/run/run.rb
rename to game/data/plugins/run/run.rb
diff --git a/data/plugins/shops/currency.rb b/game/data/plugins/shops/currency.rb
similarity index 100%
rename from data/plugins/shops/currency.rb
rename to game/data/plugins/shops/currency.rb
diff --git a/data/plugins/shops/plugin.xml b/game/data/plugins/shops/plugin.xml
similarity index 100%
rename from data/plugins/shops/plugin.xml
rename to game/data/plugins/shops/plugin.xml
diff --git a/data/plugins/shops/shop.rb b/game/data/plugins/shops/shop.rb
similarity index 100%
rename from data/plugins/shops/shop.rb
rename to game/data/plugins/shops/shop.rb
diff --git a/data/plugins/shops/shop_item.rb b/game/data/plugins/shops/shop_item.rb
similarity index 100%
rename from data/plugins/shops/shop_item.rb
rename to game/data/plugins/shops/shop_item.rb
diff --git a/data/plugins/shops/shops.rb b/game/data/plugins/shops/shops.rb
similarity index 100%
rename from data/plugins/shops/shops.rb
rename to game/data/plugins/shops/shops.rb
diff --git a/data/plugins/skill/fishing/fish.rb b/game/data/plugins/skill/fishing/fish.rb
similarity index 100%
rename from data/plugins/skill/fishing/fish.rb
rename to game/data/plugins/skill/fishing/fish.rb
diff --git a/data/plugins/skill/fishing/fishing.rb b/game/data/plugins/skill/fishing/fishing.rb
similarity index 100%
rename from data/plugins/skill/fishing/fishing.rb
rename to game/data/plugins/skill/fishing/fishing.rb
diff --git a/data/plugins/skill/fishing/plugin.xml b/game/data/plugins/skill/fishing/plugin.xml
similarity index 100%
rename from data/plugins/skill/fishing/plugin.xml
rename to game/data/plugins/skill/fishing/plugin.xml
diff --git a/data/plugins/skill/fishing/spot.rb b/game/data/plugins/skill/fishing/spot.rb
similarity index 100%
rename from data/plugins/skill/fishing/spot.rb
rename to game/data/plugins/skill/fishing/spot.rb
diff --git a/data/plugins/skill/fishing/tool.rb b/game/data/plugins/skill/fishing/tool.rb
similarity index 100%
rename from data/plugins/skill/fishing/tool.rb
rename to game/data/plugins/skill/fishing/tool.rb
diff --git a/data/plugins/skill/herblore/herb.rb b/game/data/plugins/skill/herblore/herb.rb
similarity index 100%
rename from data/plugins/skill/herblore/herb.rb
rename to game/data/plugins/skill/herblore/herb.rb
diff --git a/data/plugins/skill/herblore/herblore.rb b/game/data/plugins/skill/herblore/herblore.rb
similarity index 100%
rename from data/plugins/skill/herblore/herblore.rb
rename to game/data/plugins/skill/herblore/herblore.rb
diff --git a/data/plugins/skill/herblore/ingredient.rb b/game/data/plugins/skill/herblore/ingredient.rb
similarity index 100%
rename from data/plugins/skill/herblore/ingredient.rb
rename to game/data/plugins/skill/herblore/ingredient.rb
diff --git a/data/plugins/skill/herblore/plugin.xml b/game/data/plugins/skill/herblore/plugin.xml
similarity index 100%
rename from data/plugins/skill/herblore/plugin.xml
rename to game/data/plugins/skill/herblore/plugin.xml
diff --git a/data/plugins/skill/herblore/potion.rb b/game/data/plugins/skill/herblore/potion.rb
similarity index 100%
rename from data/plugins/skill/herblore/potion.rb
rename to game/data/plugins/skill/herblore/potion.rb
diff --git a/data/plugins/skill/magic/alchemy.rb b/game/data/plugins/skill/magic/alchemy.rb
similarity index 100%
rename from data/plugins/skill/magic/alchemy.rb
rename to game/data/plugins/skill/magic/alchemy.rb
diff --git a/data/plugins/skill/magic/convert.rb b/game/data/plugins/skill/magic/convert.rb
similarity index 100%
rename from data/plugins/skill/magic/convert.rb
rename to game/data/plugins/skill/magic/convert.rb
diff --git a/data/plugins/skill/magic/element.rb b/game/data/plugins/skill/magic/element.rb
similarity index 100%
rename from data/plugins/skill/magic/element.rb
rename to game/data/plugins/skill/magic/element.rb
diff --git a/data/plugins/skill/magic/enchant.rb b/game/data/plugins/skill/magic/enchant.rb
similarity index 100%
rename from data/plugins/skill/magic/enchant.rb
rename to game/data/plugins/skill/magic/enchant.rb
diff --git a/data/plugins/skill/magic/magic.rb b/game/data/plugins/skill/magic/magic.rb
similarity index 100%
rename from data/plugins/skill/magic/magic.rb
rename to game/data/plugins/skill/magic/magic.rb
diff --git a/data/plugins/skill/magic/plugin.xml b/game/data/plugins/skill/magic/plugin.xml
similarity index 100%
rename from data/plugins/skill/magic/plugin.xml
rename to game/data/plugins/skill/magic/plugin.xml
diff --git a/data/plugins/skill/magic/teleport.rb b/game/data/plugins/skill/magic/teleport.rb
similarity index 100%
rename from data/plugins/skill/magic/teleport.rb
rename to game/data/plugins/skill/magic/teleport.rb
diff --git a/data/plugins/skill/mining/gem.rb b/game/data/plugins/skill/mining/gem.rb
similarity index 100%
rename from data/plugins/skill/mining/gem.rb
rename to game/data/plugins/skill/mining/gem.rb
diff --git a/data/plugins/skill/mining/mining.rb b/game/data/plugins/skill/mining/mining.rb
similarity index 100%
rename from data/plugins/skill/mining/mining.rb
rename to game/data/plugins/skill/mining/mining.rb
diff --git a/data/plugins/skill/mining/ore.rb b/game/data/plugins/skill/mining/ore.rb
similarity index 100%
rename from data/plugins/skill/mining/ore.rb
rename to game/data/plugins/skill/mining/ore.rb
diff --git a/data/plugins/skill/mining/pickaxe.rb b/game/data/plugins/skill/mining/pickaxe.rb
similarity index 100%
rename from data/plugins/skill/mining/pickaxe.rb
rename to game/data/plugins/skill/mining/pickaxe.rb
diff --git a/data/plugins/skill/mining/plugin.xml b/game/data/plugins/skill/mining/plugin.xml
similarity index 100%
rename from data/plugins/skill/mining/plugin.xml
rename to game/data/plugins/skill/mining/plugin.xml
diff --git a/data/plugins/skill/mining/respawn.rb b/game/data/plugins/skill/mining/respawn.rb
similarity index 100%
rename from data/plugins/skill/mining/respawn.rb
rename to game/data/plugins/skill/mining/respawn.rb
diff --git a/data/plugins/skill/prayer/bury.rb b/game/data/plugins/skill/prayer/bury.rb
similarity index 100%
rename from data/plugins/skill/prayer/bury.rb
rename to game/data/plugins/skill/prayer/bury.rb
diff --git a/data/plugins/skill/prayer/plugin.xml b/game/data/plugins/skill/prayer/plugin.xml
similarity index 100%
rename from data/plugins/skill/prayer/plugin.xml
rename to game/data/plugins/skill/prayer/plugin.xml
diff --git a/data/plugins/skill/prayer/prayers.rb b/game/data/plugins/skill/prayer/prayers.rb
similarity index 100%
rename from data/plugins/skill/prayer/prayers.rb
rename to game/data/plugins/skill/prayer/prayers.rb
diff --git a/data/plugins/skill/runecraft/altar.rb b/game/data/plugins/skill/runecraft/altar.rb
similarity index 100%
rename from data/plugins/skill/runecraft/altar.rb
rename to game/data/plugins/skill/runecraft/altar.rb
diff --git a/data/plugins/skill/runecraft/plugin.xml b/game/data/plugins/skill/runecraft/plugin.xml
similarity index 100%
rename from data/plugins/skill/runecraft/plugin.xml
rename to game/data/plugins/skill/runecraft/plugin.xml
diff --git a/data/plugins/skill/runecraft/rune.rb b/game/data/plugins/skill/runecraft/rune.rb
similarity index 100%
rename from data/plugins/skill/runecraft/rune.rb
rename to game/data/plugins/skill/runecraft/rune.rb
diff --git a/data/plugins/skill/runecraft/runecraft.rb b/game/data/plugins/skill/runecraft/runecraft.rb
similarity index 100%
rename from data/plugins/skill/runecraft/runecraft.rb
rename to game/data/plugins/skill/runecraft/runecraft.rb
diff --git a/data/plugins/skill/runecraft/talisman.rb b/game/data/plugins/skill/runecraft/talisman.rb
similarity index 100%
rename from data/plugins/skill/runecraft/talisman.rb
rename to game/data/plugins/skill/runecraft/talisman.rb
diff --git a/data/plugins/skill/runecraft/tiara.rb b/game/data/plugins/skill/runecraft/tiara.rb
similarity index 100%
rename from data/plugins/skill/runecraft/tiara.rb
rename to game/data/plugins/skill/runecraft/tiara.rb
diff --git a/data/plugins/util/command.rb b/game/data/plugins/util/command.rb
similarity index 100%
rename from data/plugins/util/command.rb
rename to game/data/plugins/util/command.rb
diff --git a/data/plugins/util/name_lookup.rb b/game/data/plugins/util/name_lookup.rb
similarity index 100%
rename from data/plugins/util/name_lookup.rb
rename to game/data/plugins/util/name_lookup.rb
diff --git a/data/plugins/util/plugin.xml b/game/data/plugins/util/plugin.xml
similarity index 100%
rename from data/plugins/util/plugin.xml
rename to game/data/plugins/util/plugin.xml
diff --git a/data/synchronizer.xml b/game/data/synchronizer.xml
similarity index 100%
rename from data/synchronizer.xml
rename to game/data/synchronizer.xml
diff --git a/game/plugin-detekt-rules/build.gradle b/game/plugin-detekt-rules/build.gradle
new file mode 100644
index 000000000..2a459bb66
--- /dev/null
+++ b/game/plugin-detekt-rules/build.gradle
@@ -0,0 +1,16 @@
+apply plugin: 'java-library'
+apply plugin: 'org.jetbrains.kotlin.jvm'
+apply from: "$rootDir/gradle/kotlin.gradle"
+
+dependencies {
+ api group: 'io.gitlab.arturbosch.detekt', name: 'detekt-api', version: detektVersion
+ api group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-jdk8'
+
+ test.useJUnitPlatform()
+ testImplementation("org.junit.jupiter:junit-jupiter-api:${junitJupiterVersion}")
+ testImplementation("org.junit.jupiter:junit-jupiter-params:${junitJupiterVersion}")
+ testImplementation("org.junit.jupiter:junit-jupiter-engine:${junitJupiterVersion}")
+ testImplementation("org.junit.platform:junit-platform-launcher:${junitPlatformVersion}")
+
+ testImplementation group: 'io.gitlab.arturbosch.detekt', name: 'detekt-test', version: detektVersion
+}
diff --git a/game/plugin-detekt-rules/src/main/kotlin/org/apollo/game/plugin/detekt/ApolloPluginRuleSetProvider.kt b/game/plugin-detekt-rules/src/main/kotlin/org/apollo/game/plugin/detekt/ApolloPluginRuleSetProvider.kt
new file mode 100644
index 000000000..c084d54b9
--- /dev/null
+++ b/game/plugin-detekt-rules/src/main/kotlin/org/apollo/game/plugin/detekt/ApolloPluginRuleSetProvider.kt
@@ -0,0 +1,16 @@
+package org.apollo.game.plugin.detekt
+
+import io.gitlab.arturbosch.detekt.api.Config
+import io.gitlab.arturbosch.detekt.api.RuleSet
+import io.gitlab.arturbosch.detekt.api.RuleSetProvider
+import org.apollo.game.plugin.detekt.rules.DeclarationInScriptRule
+
+class ApolloPluginRuleSetProvider : RuleSetProvider {
+ override val ruleSetId = "apollo-plugin"
+
+ override fun instance(config: Config): RuleSet {
+ return RuleSet(ruleSetId, listOf(
+ DeclarationInScriptRule()
+ ))
+ }
+}
\ No newline at end of file
diff --git a/game/plugin-detekt-rules/src/main/kotlin/org/apollo/game/plugin/detekt/rules/DeclarationInScriptRule.kt b/game/plugin-detekt-rules/src/main/kotlin/org/apollo/game/plugin/detekt/rules/DeclarationInScriptRule.kt
new file mode 100644
index 000000000..055b5fd1d
--- /dev/null
+++ b/game/plugin-detekt-rules/src/main/kotlin/org/apollo/game/plugin/detekt/rules/DeclarationInScriptRule.kt
@@ -0,0 +1,31 @@
+package org.apollo.game.plugin.detekt.rules
+
+import io.gitlab.arturbosch.detekt.api.*
+import org.jetbrains.kotlin.psi.KtClass
+import org.jetbrains.kotlin.psi.KtFile
+import org.jetbrains.kotlin.psi.KtObjectDeclaration
+
+class DeclarationInScriptRule : Rule() {
+ override val issue = Issue(
+ "DeclarationInScript",
+ Severity.CodeSmell,
+ "This rule reports a plugin file containing class or object declarations.",
+ Debt.FIVE_MINS
+ )
+
+ override fun visit(root: KtFile) {
+ super.visit(root)
+
+ val script = root.script ?: return
+ val declarations = script.declarations.filter { it is KtClass || it is KtObjectDeclaration }
+
+ declarations
+ .forEach {
+ report(CodeSmell(
+ issue,
+ Entity.from(it),
+ message = "Declaration of ${it.name} should live in a top-level file, not a script"
+ ))
+ }
+ }
+}
\ No newline at end of file
diff --git a/game/plugin-detekt-rules/src/main/resources/META-INF/services/io.gitlab.arturbosch.detekt.api.RuleSetProvider b/game/plugin-detekt-rules/src/main/resources/META-INF/services/io.gitlab.arturbosch.detekt.api.RuleSetProvider
new file mode 100644
index 000000000..3eb44c259
--- /dev/null
+++ b/game/plugin-detekt-rules/src/main/resources/META-INF/services/io.gitlab.arturbosch.detekt.api.RuleSetProvider
@@ -0,0 +1 @@
+org.apollo.game.plugin.detekt.ApolloPluginRuleSetProvider
\ No newline at end of file
diff --git a/game/plugin-detekt-rules/src/test/kotlin/org/apollo/game/plugin/detekt/rules/DeclarationInScriptRuleTest.kt b/game/plugin-detekt-rules/src/test/kotlin/org/apollo/game/plugin/detekt/rules/DeclarationInScriptRuleTest.kt
new file mode 100644
index 000000000..f6ca6daeb
--- /dev/null
+++ b/game/plugin-detekt-rules/src/test/kotlin/org/apollo/game/plugin/detekt/rules/DeclarationInScriptRuleTest.kt
@@ -0,0 +1,19 @@
+package org.apollo.game.plugin.detekt.rules
+
+import io.gitlab.arturbosch.detekt.test.lint
+import java.nio.file.Paths
+import org.junit.jupiter.api.Assertions.*
+import org.junit.jupiter.api.Test
+
+internal class DeclarationInScriptRuleTest {
+ val rule = DeclarationInScriptRule()
+
+ @Test
+ fun `Finds warning in script file`() {
+ val srcPath = Paths.get(this.javaClass.getResource("/testData/example.kts").toURI())
+ val findings = rule.lint(srcPath)
+
+ assertEquals(1, findings.size)
+ assertEquals("Declaration of ExampleDeclaration should live in a top-level file, not a script", findings[0].message)
+ }
+}
\ No newline at end of file
diff --git a/game/plugin-detekt-rules/src/test/resources/testData/example.kts b/game/plugin-detekt-rules/src/test/resources/testData/example.kts
new file mode 100644
index 000000000..0fb1729b7
--- /dev/null
+++ b/game/plugin-detekt-rules/src/test/resources/testData/example.kts
@@ -0,0 +1,3 @@
+class ExampleDeclaration {
+
+}
\ No newline at end of file
diff --git a/game/plugin-testing/build.gradle b/game/plugin-testing/build.gradle
new file mode 100644
index 000000000..0f2dc14d1
--- /dev/null
+++ b/game/plugin-testing/build.gradle
@@ -0,0 +1,28 @@
+apply plugin: 'java-library'
+apply plugin: 'org.jetbrains.kotlin.jvm'
+apply from: "$rootDir/gradle/kotlin.gradle"
+
+dependencies {
+ test.useJUnitPlatform()
+
+ api project(':game')
+ api project(':net')
+
+ // JUnit Jupiter API and TestEngine implementation
+ api("org.junit.jupiter:junit-jupiter-api:${junitJupiterVersion}")
+ api("org.junit.jupiter:junit-jupiter-params:${junitJupiterVersion}")
+ implementation("org.junit.jupiter:junit-jupiter-engine:${junitJupiterVersion}")
+ implementation("org.junit.platform:junit-platform-launcher:${junitPlatformVersion}")
+
+ api group: 'io.mockk', name: 'mockk', version: mockkVersion
+ api group: 'org.assertj', name: 'assertj-core', version: assertjVersion
+ api group: 'com.willowtreeapps.assertk', name: 'assertk', version: assertkVersion
+
+ implementation group: 'org.powermock', name: 'powermock-module-junit4', version: powermockVersion
+}
+
+tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+}
\ No newline at end of file
diff --git a/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/assertions/StringAssertions.kt b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/assertions/StringAssertions.kt
new file mode 100644
index 000000000..c32e88419
--- /dev/null
+++ b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/assertions/StringAssertions.kt
@@ -0,0 +1,7 @@
+package org.apollo.game.plugin.testing.assertions
+
+import io.mockk.MockKMatcherScope
+
+fun MockKMatcherScope.contains(search: String) = match { it.contains(search) }
+fun MockKMatcherScope.startsWith(search: String) = match { it.startsWith(search) }
+fun MockKMatcherScope.endsWith(search: String) = match { it.endsWith(search) }
\ No newline at end of file
diff --git a/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/assertions/actionAsserts.kt b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/assertions/actionAsserts.kt
new file mode 100644
index 000000000..f514a0d5f
--- /dev/null
+++ b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/assertions/actionAsserts.kt
@@ -0,0 +1,20 @@
+package org.apollo.game.plugin.testing.assertions
+
+import io.mockk.MockKVerificationScope
+import io.mockk.verify
+import org.apollo.game.plugin.testing.junit.api.ActionCaptureCallbackRegistration
+
+/**
+ * Verify some expectations on a [mock] after a delayed event (specified by [DelayMode]).
+ */
+fun verifyAfter(registration: ActionCaptureCallbackRegistration, description: String? = null, verifier: MockKVerificationScope.() -> Unit) {
+ after(registration, description) { verify(verifyBlock = verifier) }
+}
+
+/**
+ * Run a [callback] after a given delay, specified by [DelayMode].
+ */
+fun after(registration: ActionCaptureCallbackRegistration, description: String? = null, callback: () -> Unit) {
+ registration.function = callback
+ registration.description = description
+}
\ No newline at end of file
diff --git a/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/fakes/FakePluginContextFactory.kt b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/fakes/FakePluginContextFactory.kt
new file mode 100644
index 000000000..701900478
--- /dev/null
+++ b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/fakes/FakePluginContextFactory.kt
@@ -0,0 +1,25 @@
+package org.apollo.game.plugin.testing.fakes
+
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.slot
+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
+
+object FakePluginContextFactory {
+ fun create(messageHandlers: MessageHandlerChainSet): PluginContext {
+ val ctx = mockk()
+ val typeCapture = slot>()
+ val handlerCapture = slot>()
+
+ every {
+ ctx.addMessageHandler(capture(typeCapture), capture(handlerCapture))
+ } answers {
+ messageHandlers.putHandler(typeCapture.captured, handlerCapture.captured)
+ }
+
+ return ctx
+ }
+}
\ No newline at end of file
diff --git a/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/ApolloTestState.kt b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/ApolloTestState.kt
new file mode 100644
index 000000000..cae992930
--- /dev/null
+++ b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/ApolloTestState.kt
@@ -0,0 +1,72 @@
+package org.apollo.game.plugin.testing.junit
+
+import io.mockk.every
+import io.mockk.slot
+import io.mockk.spyk
+import kotlin.reflect.KClass
+import org.apollo.game.action.Action
+import org.apollo.game.message.handler.MessageHandlerChainSet
+import org.apollo.game.model.World
+import org.apollo.game.model.entity.Player
+import org.apollo.game.plugin.testing.junit.api.ActionCapture
+import org.apollo.game.plugin.testing.junit.mocking.StubPrototype
+import org.apollo.game.plugin.testing.junit.stubs.PlayerStubInfo
+import org.apollo.net.message.Message
+import org.apollo.util.security.PlayerCredentials
+
+data class ApolloTestState(val handlers: MessageHandlerChainSet, val world: World) {
+ val players = mutableListOf()
+ var actionCapture: ActionCapture? = null
+
+ fun createActionCapture(type: KClass>): ActionCapture {
+ if (actionCapture != null) {
+ throw IllegalStateException("Cannot specify more than one ActionCapture")
+ }
+
+ actionCapture = ActionCapture(type)
+ return actionCapture!!
+ }
+
+ fun createStub(proto: StubPrototype): T {
+ val annotations = proto.annotations
+
+ return when (proto.type) {
+ Player::class -> createPlayer(PlayerStubInfo.create(annotations)) as T
+ World::class -> world as T
+ ActionCapture::class -> createActionCapture(Action::class) as T
+ else -> throw IllegalArgumentException("Can't stub ${proto.type.qualifiedName}")
+ }
+ }
+
+ fun createPlayer(info: PlayerStubInfo): Player {
+ val credentials = PlayerCredentials(info.name, "test", 1, 1, "0.0.0.0")
+ val region = world.regionRepository.fromPosition(info.position)
+
+ val player = spyk(Player(world, credentials, info.position))
+
+ world.register(player)
+ region.addEntity(player)
+ players.add(player)
+
+ val actionSlot = slot>()
+ val messageSlot = slot()
+
+ every { player.send(capture(messageSlot)) } answers { handlers.notify(player, messageSlot.captured) }
+ every { player.startAction(capture(actionSlot)) } answers {
+ actionCapture?.capture(actionSlot.captured)
+ true
+ }
+
+ return player
+ }
+
+ fun reset() {
+ actionCapture = null
+ players.forEach {
+ it.stopAction()
+ world.unregister(it)
+ }
+
+ players.clear()
+ }
+}
\ No newline at end of file
diff --git a/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/ApolloTestingExtension.kt b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/ApolloTestingExtension.kt
new file mode 100644
index 000000000..1a6f5af2f
--- /dev/null
+++ b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/ApolloTestingExtension.kt
@@ -0,0 +1,188 @@
+package org.apollo.game.plugin.testing.junit
+
+import io.mockk.every
+import io.mockk.slot
+import io.mockk.spyk
+import io.mockk.staticMockk
+import kotlin.reflect.KCallable
+import kotlin.reflect.KMutableProperty
+import kotlin.reflect.full.companionObject
+import kotlin.reflect.full.createType
+import kotlin.reflect.full.declaredMemberFunctions
+import kotlin.reflect.full.declaredMemberProperties
+import kotlin.reflect.jvm.isAccessible
+import kotlin.reflect.jvm.jvmErasure
+import org.apollo.cache.def.ItemDefinition
+import org.apollo.cache.def.NpcDefinition
+import org.apollo.cache.def.ObjectDefinition
+import org.apollo.game.message.handler.MessageHandlerChainSet
+import org.apollo.game.model.World
+import org.apollo.game.model.entity.Npc
+import org.apollo.game.model.entity.Player
+import org.apollo.game.model.entity.obj.GameObject
+import org.apollo.game.plugin.KotlinPluginEnvironment
+import org.apollo.game.plugin.testing.fakes.FakePluginContextFactory
+import org.apollo.game.plugin.testing.junit.api.ActionCapture
+import org.apollo.game.plugin.testing.junit.api.annotations.ItemDefinitions
+import org.apollo.game.plugin.testing.junit.api.annotations.NpcDefinitions
+import org.apollo.game.plugin.testing.junit.api.annotations.ObjectDefinitions
+import org.apollo.game.plugin.testing.junit.mocking.StubPrototype
+import org.junit.jupiter.api.extension.*
+
+class ApolloTestingExtension :
+ AfterTestExecutionCallback,
+ BeforeAllCallback,
+ AfterAllCallback,
+ BeforeEachCallback,
+ AfterEachCallback,
+ ParameterResolver {
+
+ private val namespace = ExtensionContext.Namespace.create("apollo")
+
+ private fun cleanup(context: ExtensionContext) {
+ val store = context.getStore(namespace)
+ val state = store[ApolloTestState::class] as ApolloTestState
+
+ try {
+ state.actionCapture?.runAction()
+ } finally {
+ state.reset()
+ }
+ }
+
+ override fun afterAll(context: ExtensionContext) {
+ val store = context.getStore(namespace)
+ store.remove(ApolloTestState::class)
+ }
+
+ override fun afterEach(context: ExtensionContext) = cleanup(context)
+
+ override fun afterTestExecution(context: ExtensionContext) = cleanup(context)
+
+ override fun beforeAll(context: ExtensionContext) {
+ val stubHandlers = MessageHandlerChainSet()
+ val stubWorld = spyk(World())
+
+ context.testClass // This _must_ come before plugin environment initialisation
+ .map { it.kotlin.companionObject }
+ .ifPresent { companion ->
+ val companionInstance = companion.objectInstance!!
+ val callables: List> = companion.declaredMemberFunctions + companion.declaredMemberProperties
+
+ createTestDefinitions(
+ callables, companionInstance, ItemDefinition::getId, ItemDefinition::lookup,
+ ItemDefinition::getDefinitions, ItemDefinition::count
+ )
+
+ createTestDefinitions(
+ callables, companionInstance, NpcDefinition::getId, NpcDefinition::lookup,
+ NpcDefinition::getDefinitions, NpcDefinition::count
+ )
+
+ createTestDefinitions(
+ callables, companionInstance, ObjectDefinition::getId, ObjectDefinition::lookup,
+ ObjectDefinition::getDefinitions, ObjectDefinition::count
+ )
+ }
+
+ KotlinPluginEnvironment(stubWorld).apply {
+ setContext(FakePluginContextFactory.create(stubHandlers))
+ load(emptyList())
+ }
+
+ val store = context.getStore(namespace)
+ val state = ApolloTestState(stubHandlers, stubWorld)
+
+ store.put(ApolloTestState::class, state)
+ }
+
+ override fun beforeEach(context: ExtensionContext) {
+ val testClassInstance = context.requiredTestInstance
+ val testClassProps = context.requiredTestClass.kotlin.declaredMemberProperties
+
+ val store = context.getStore(namespace)
+ val state = store.get(ApolloTestState::class) as ApolloTestState
+
+ val propertyStubSites = testClassProps.asSequence()
+ .mapNotNull { it as? KMutableProperty<*> }
+ .filter { supportedTestDoubleTypes.contains(it.returnType) }
+
+ propertyStubSites.forEach { property ->
+ property.setter.call(
+ testClassInstance,
+ state.createStub(StubPrototype(property.returnType.jvmErasure, property.annotations))
+ )
+ }
+ }
+
+ override fun supportsParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Boolean {
+ val param = parameterContext.parameter
+ val paramType = param.type.kotlin
+
+ return supportedTestDoubleTypes.contains(paramType.createType())
+ }
+
+ override fun resolveParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Any {
+ val param = parameterContext.parameter
+ val paramType = param.type.kotlin
+ val testStore = extensionContext.getStore(namespace)
+ val testState = testStore.get(ApolloTestState::class) as ApolloTestState
+
+ return testState.createStub(StubPrototype(paramType, param.annotations.toList()))
+ }
+
+ /**
+ * Mocks the definition class of type [D] for any function with the attached annotation [A].
+ *
+ * @param testClassMethods All of the methods in the class being tested.
+ * @param idMapper The map function that returns an id given a definition [D].
+ * @param lookup The lookup function that returns an instance of [D] given a definition id.
+ */
+ private inline fun createTestDefinitions(
+ testClassMethods: Collection>,
+ companionObjectInstance: Any?,
+ crossinline idMapper: (D) -> Int,
+ crossinline lookup: (Int) -> D?,
+ crossinline getAll: () -> Array,
+ crossinline count: () -> Int
+ ) {
+ val testDefinitions = findTestDefinitions(testClassMethods, companionObjectInstance)
+ .associateBy(idMapper)
+
+ if (testDefinitions.isNotEmpty()) {
+ val idSlot = slot()
+ staticMockk().mock()
+
+ every { lookup(capture(idSlot)) } answers { testDefinitions[idSlot.captured] }
+ every { getAll() } answers { testDefinitions.values.sortedBy(idMapper).toTypedArray() }
+ every { count() } answers { _ -> testDefinitions.maxBy { (id, _) -> id }?.key?.let { it + 1 } ?: 0 }
+ }
+ }
+
+ companion object {
+ internal val supportedTestDoubleTypes = setOf(
+ Player::class.createType(),
+ Npc::class.createType(),
+ GameObject::class.createType(),
+ World::class.createType(),
+ ActionCapture::class.createType()
+ )
+
+ inline fun findTestDefinitions(
+ callables: Collection>,
+ companionObjectInstance: Any?
+ ): List {
+ return callables
+ .filter { method -> method.annotations.any { it is A } }
+ .flatMap { method ->
+ @Suppress("UNCHECKED_CAST")
+ method as? KCallable> ?: throw RuntimeException("${method.name} is annotated with " +
+ "${A::class.simpleName} but does not return Collection<${D::class.simpleName}>."
+ )
+
+ method.isAccessible = true // lets us call methods in private companion objects
+ method.call(companionObjectInstance)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/api/ActionCapture.kt b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/api/ActionCapture.kt
new file mode 100644
index 000000000..8ebf8fa35
--- /dev/null
+++ b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/api/ActionCapture.kt
@@ -0,0 +1,93 @@
+package org.apollo.game.plugin.testing.junit.api
+
+import kotlin.reflect.KClass
+import kotlin.reflect.full.isSuperclassOf
+import org.apollo.game.action.Action
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertTrue
+
+class ActionCapture(val type: KClass>) {
+ private var action: Action<*>? = null
+ private val callbacks = mutableListOf()
+ private var lastTicks: Int = 0
+
+ fun capture(captured: Action<*>) {
+ assertTrue(type.isSuperclassOf(captured::class)) {
+ "${captured::class.simpleName} is not an instance of ${type.simpleName}"
+ }
+
+ this.action = captured
+ }
+
+ private fun callback(delay: ActionCaptureDelay): ActionCaptureCallbackRegistration {
+ val registration = ActionCaptureCallbackRegistration()
+ val callback = ActionCaptureCallback(delay, registration)
+
+ callbacks.add(callback)
+ return registration
+ }
+
+ fun runAction(timeout: Int = 50) {
+ action?.let {
+ var pulses = 0
+
+ do {
+ it.pulse()
+ pulses++
+
+ val tickCallbacks = callbacks.filter { it.delay == ActionCaptureDelay.Ticks(pulses) }
+ tickCallbacks.forEach { it.invoke() }
+
+ callbacks.removeAll(tickCallbacks)
+ } while (it.isRunning && pulses < timeout)
+
+ val completionCallbacks = callbacks.filter { it.delay == ActionCaptureDelay.Completed }
+ completionCallbacks.forEach { it.invoke() }
+
+ callbacks.removeAll(completionCallbacks)
+ }
+
+ assertEquals(0, callbacks.size, {
+ "untriggered callbacks:\n" + callbacks
+ .map {
+ val delayDescription = when (it.delay) {
+ is ActionCaptureDelay.Ticks -> "${it.delay.count} ticks"
+ is ActionCaptureDelay.Completed -> "action completion"
+ }
+
+ "$delayDescription (${it.callbackRegistration.description ?: ""})"
+ }
+ .joinToString("\n")
+ .prependIndent(" ")
+ })
+ }
+
+ /**
+ * Create a callback registration that triggers after exactly [count] ticks.
+ */
+ fun exactTicks(count: Int) = callback(ActionCaptureDelay.Ticks(count))
+
+ /**
+ * Create a callback registration that triggers after [count] ticks. This method is cumulative,
+ * and will take into account previous calls to [ticks] when creating new callbacks.
+ *
+ * To run a callback after an exact number of ticks use [exactTicks].
+ */
+ fun ticks(count: Int): ActionCaptureCallbackRegistration {
+ lastTicks += count
+
+ return exactTicks(lastTicks)
+ }
+
+ /**
+ * Create a callback registration that triggers when an [Action] completes.
+ */
+ fun complete() = callback(ActionCaptureDelay.Completed)
+
+ /**
+ * Check if this capture has a pending [Action] to run.
+ */
+ fun isPending(): Boolean {
+ return action?.isRunning ?: false
+ }
+}
\ No newline at end of file
diff --git a/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/api/ActionCaptureCallback.kt b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/api/ActionCaptureCallback.kt
new file mode 100644
index 000000000..7f9f9b78c
--- /dev/null
+++ b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/api/ActionCaptureCallback.kt
@@ -0,0 +1,7 @@
+package org.apollo.game.plugin.testing.junit.api
+
+data class ActionCaptureCallback(val delay: ActionCaptureDelay, val callbackRegistration: ActionCaptureCallbackRegistration) {
+ fun invoke() {
+ callbackRegistration.function?.invoke()
+ }
+}
\ No newline at end of file
diff --git a/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/api/ActionCaptureCallbackRegistration.kt b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/api/ActionCaptureCallbackRegistration.kt
new file mode 100644
index 000000000..15d940f5f
--- /dev/null
+++ b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/api/ActionCaptureCallbackRegistration.kt
@@ -0,0 +1,5 @@
+package org.apollo.game.plugin.testing.junit.api
+
+typealias Function = () -> Unit
+
+class ActionCaptureCallbackRegistration(var function: Function? = null, var description: String? = null)
\ No newline at end of file
diff --git a/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/api/ActionCaptureDelay.kt b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/api/ActionCaptureDelay.kt
new file mode 100644
index 000000000..f24ff8b68
--- /dev/null
+++ b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/api/ActionCaptureDelay.kt
@@ -0,0 +1,6 @@
+package org.apollo.game.plugin.testing.junit.api
+
+sealed class ActionCaptureDelay {
+ data class Ticks(val count: Int) : ActionCaptureDelay()
+ object Completed : ActionCaptureDelay()
+}
diff --git a/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/api/annotations/DefinitionAnnotations.kt b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/api/annotations/DefinitionAnnotations.kt
new file mode 100644
index 000000000..b3d6c8d64
--- /dev/null
+++ b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/api/annotations/DefinitionAnnotations.kt
@@ -0,0 +1,37 @@
+package org.apollo.game.plugin.testing.junit.api.annotations
+
+/**
+ * Specifies that the the ItemDefinitions returned by the annotated function should be inserted into the definition
+ * table.
+ *
+ * The annotated function **must**:
+ * - Be inside a **companion object** inside an apollo test class (a regular object will not work).
+ * - Return a `Collection`.
+ */
+@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY)
+@Retention(AnnotationRetention.RUNTIME)
+annotation class ItemDefinitions
+
+/**
+ * Specifies that the the NpcDefinitions returned by the annotated function should be inserted into the definition
+ * table.
+ *
+ * The annotated function **must**:
+ * - Be inside a **companion object** inside an apollo test class (a regular object will not work).
+ * - Return a `Collection`.
+ */
+@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY)
+@Retention(AnnotationRetention.RUNTIME)
+annotation class NpcDefinitions
+
+/**
+ * Specifies that the the ObjectDefinitions returned by the annotated function should be inserted into the definition
+ * table.
+ *
+ * The annotated function **must**:
+ * - Be inside a **companion object** inside an apollo test class (a regular object will not work).
+ * - Return a `Collection`.
+ */
+@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY)
+@Retention(AnnotationRetention.RUNTIME)
+annotation class ObjectDefinitions
\ No newline at end of file
diff --git a/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/api/annotations/stubAnnotations.kt b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/api/annotations/stubAnnotations.kt
new file mode 100644
index 000000000..2acf1636b
--- /dev/null
+++ b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/api/annotations/stubAnnotations.kt
@@ -0,0 +1,9 @@
+package org.apollo.game.plugin.testing.junit.api.annotations
+
+annotation class Id(val value: Int)
+annotation class Pos(val x: Int, val y: Int, val height: Int = 0)
+annotation class Name(val value: String)
+
+@Target(AnnotationTarget.FIELD)
+@Retention(AnnotationRetention.RUNTIME)
+annotation class TestMock
\ No newline at end of file
diff --git a/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/api/interactions/player.kt b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/api/interactions/player.kt
new file mode 100644
index 000000000..6fffee19a
--- /dev/null
+++ b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/api/interactions/player.kt
@@ -0,0 +1,42 @@
+package org.apollo.game.plugin.testing.junit.api.interactions
+
+import org.apollo.game.message.impl.ItemOptionMessage
+import org.apollo.game.message.impl.NpcActionMessage
+import org.apollo.game.message.impl.ObjectActionMessage
+import org.apollo.game.message.impl.PlayerActionMessage
+import org.apollo.game.model.Direction
+import org.apollo.game.model.Position
+import org.apollo.game.model.entity.Entity
+import org.apollo.game.model.entity.Npc
+import org.apollo.game.model.entity.Player
+import org.apollo.game.model.entity.obj.GameObject
+
+/**
+ * Send an [ItemOptionMessage] for the given [id], [option], [slot], and [interfaceId], simulating a
+ * player interacting with an item.
+ */
+fun Player.interactWithItem(id: Int, option: Int, slot: Int? = null, interfaceId: Int? = null) {
+ send(ItemOptionMessage(option, interfaceId ?: -1, id, slot ?: inventory.slotOf(id)))
+}
+
+/**
+ * Spawn a new object (defaulting to in-front of the player) and immediately interact with it.
+ */
+fun Player.interactWithObject(id: Int, option: Int, at: Position? = null) {
+ val obj = world.spawnObject(id, at ?: position.step(1, Direction.NORTH))
+ interactWith(obj, option)
+}
+
+/**
+ * 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 -> send(ObjectActionMessage(option, entity.id, entity.position))
+ is Npc -> send(NpcActionMessage(option, entity.index))
+ is Player -> send(PlayerActionMessage(option, entity.index))
+ }
+}
\ No newline at end of file
diff --git a/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/api/interactions/world.kt b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/api/interactions/world.kt
new file mode 100644
index 000000000..8dfd2cbcd
--- /dev/null
+++ b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/api/interactions/world.kt
@@ -0,0 +1,34 @@
+package org.apollo.game.plugin.testing.junit.api.interactions
+
+import org.apollo.cache.def.NpcDefinition
+import org.apollo.game.model.Position
+import org.apollo.game.model.World
+import org.apollo.game.model.entity.Npc
+import org.apollo.game.model.entity.obj.GameObject
+import org.apollo.game.model.entity.obj.StaticGameObject
+
+/**
+ * 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
+}
+
+/**
+ * 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
+}
\ No newline at end of file
diff --git a/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/mocking/StubPrototype.kt b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/mocking/StubPrototype.kt
new file mode 100644
index 000000000..924b2d5b5
--- /dev/null
+++ b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/mocking/StubPrototype.kt
@@ -0,0 +1,5 @@
+package org.apollo.game.plugin.testing.junit.mocking
+
+import kotlin.reflect.KClass
+
+data class StubPrototype(val type: KClass, val annotations: Collection)
diff --git a/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/params/DefinitionProviders.kt b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/params/DefinitionProviders.kt
new file mode 100644
index 000000000..1daced5d4
--- /dev/null
+++ b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/params/DefinitionProviders.kt
@@ -0,0 +1,97 @@
+package org.apollo.game.plugin.testing.junit.params
+
+import java.util.stream.Stream
+import kotlin.reflect.KCallable
+import kotlin.reflect.full.companionObject
+import kotlin.reflect.full.declaredMemberFunctions
+import kotlin.reflect.full.declaredMemberProperties
+import org.apollo.cache.def.ItemDefinition
+import org.apollo.cache.def.NpcDefinition
+import org.apollo.cache.def.ObjectDefinition
+import org.apollo.game.plugin.testing.junit.ApolloTestingExtension.Companion.findTestDefinitions
+import org.apollo.game.plugin.testing.junit.api.annotations.ItemDefinitions
+import org.apollo.game.plugin.testing.junit.api.annotations.NpcDefinitions
+import org.apollo.game.plugin.testing.junit.api.annotations.ObjectDefinitions
+import org.junit.jupiter.api.extension.ExtensionContext
+import org.junit.jupiter.params.provider.Arguments
+import org.junit.jupiter.params.provider.ArgumentsProvider
+import org.junit.jupiter.params.support.AnnotationConsumer
+
+/**
+ * An [ArgumentsProvider] for a definition of type `D`.
+ */
+abstract class DefinitionsProvider(
+ private val definitionProvider: (methods: Collection>, companionObjectInstance: Any) -> List
+) : ArgumentsProvider {
+
+ protected lateinit var sourceNames: Set
+
+ override fun provideArguments(context: ExtensionContext): Stream {
+ val companion = context.requiredTestClass.kotlin.companionObject
+ ?: throw RuntimeException("${context.requiredTestMethod.name} is annotated with a DefinitionsProvider," +
+ " but does not contain a companion object to search for Definitions in."
+ )
+
+ val companionInstance = companion.objectInstance!! // safe
+ val callables: List> = companion.declaredMemberFunctions + companion.declaredMemberProperties
+
+ val filtered = if (sourceNames.isEmpty()) {
+ callables
+ } else {
+ callables.filter { it.name in sourceNames }
+ }
+
+ return definitionProvider(filtered, companionInstance).map { Arguments.of(it) }.stream()
+ }
+}
+
+// These providers are separate because of a JUnit bug in its use of ArgumentsSource and AnnotationConsumer -
+// the reflection code that invokes the AnnotationConsumer searches for an accept() method that takes an
+// Annotation parameter, prohibiting usage of the actual `Annotation` type as the parameter - meaning
+// DefinitionsProvider cannot abstract over different annotation implementations (i.e. over ItemDefinitionSource,
+// NpcDefinitionSource, and ObjectDefinitionSource).
+
+/**
+ * An [ArgumentsProvider] for [ItemDefinition]s.
+ *
+ * Test authors should not need to utilise this class, and should instead annotate their function with
+ * [@ItemDefinitionSource][ItemDefinitionSource].
+ */
+object ItemDefinitionsProvider : DefinitionsProvider(
+ { methods, companion -> findTestDefinitions(methods, companion) }
+), AnnotationConsumer {
+
+ override fun accept(source: ItemDefinitionSource) {
+ sourceNames = source.sourceNames.toHashSet()
+ }
+}
+
+/**
+ * An [ArgumentsProvider] for [NpcDefinition]s.
+ *
+ * Test authors should not need to utilise this class, and should instead annotate their function with
+ * [@NpcDefinitionSource][NpcDefinitionSource].
+ */
+object NpcDefinitionsProvider : DefinitionsProvider(
+ { methods, companion -> findTestDefinitions(methods, companion) }
+), AnnotationConsumer {
+
+ override fun accept(source: NpcDefinitionSource) {
+ sourceNames = source.sourceNames.toHashSet()
+ }
+}
+
+/**
+ * An [ArgumentsProvider] for [ObjectDefinition]s.
+ *
+ * Test authors should not need to utilise this class, and should instead annotate their function with
+ * [@ObjectDefinitionSource][ObjectDefinitionSource].
+ */
+object ObjectDefinitionsProvider : DefinitionsProvider(
+ { methods, companion -> findTestDefinitions(methods, companion) }
+), AnnotationConsumer {
+
+ override fun accept(source: ObjectDefinitionSource) {
+ sourceNames = source.sourceNames.toHashSet()
+ }
+}
diff --git a/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/params/DefinitionSource.kt b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/params/DefinitionSource.kt
new file mode 100644
index 000000000..c97fc9ca1
--- /dev/null
+++ b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/params/DefinitionSource.kt
@@ -0,0 +1,54 @@
+package org.apollo.game.plugin.testing.junit.params
+
+import org.apollo.cache.def.ItemDefinition
+import org.apollo.cache.def.NpcDefinition
+import org.apollo.cache.def.ObjectDefinition
+import org.junit.jupiter.params.provider.ArgumentsSource
+
+/**
+ * `@ItemDefinitionSource` is an [ArgumentsSource] for [ItemDefinition]s.
+ *
+ * @param sourceNames The names of the properties or functions annotated with `@ItemDefinitions` to use as sources of
+ * [ItemDefinition]s for the test with this annotation. Every property/function must return
+ * `Collection`. If no [sourceNames] are provided, every property and function annotated with
+ * `@ItemDefinitions` (in this class's companion object) will be used.
+ *
+ * @see org.junit.jupiter.params.provider.ArgumentsSource
+ * @see org.junit.jupiter.params.ParameterizedTest
+ */
+@Target(AnnotationTarget.FUNCTION)
+@Retention(AnnotationRetention.RUNTIME)
+@ArgumentsSource(ItemDefinitionsProvider::class)
+annotation class ItemDefinitionSource(vararg val sourceNames: String)
+
+/**
+ * `@NpcDefinitionSource` is an [ArgumentsSource] for [NpcDefinition]s.
+ *
+ * @param sourceNames The names of the properties or functions annotated with `@NpcDefinitions` to use as sources of
+ * [NpcDefinition]s for the test with this annotation. Every property/function must return
+ * `Collection`. If no [sourceNames] are provided, every property and function annotated with
+ * `@NpcDefinitions` (in this class's companion object) will be used.
+ *
+ * @see org.junit.jupiter.params.provider.ArgumentsSource
+ * @see org.junit.jupiter.params.ParameterizedTest
+ */
+@Target(AnnotationTarget.FUNCTION)
+@Retention(AnnotationRetention.RUNTIME)
+@ArgumentsSource(NpcDefinitionsProvider::class)
+annotation class NpcDefinitionSource(vararg val sourceNames: String)
+
+/**
+ * `@ObjectDefinitionSource` is an [ArgumentsSource] for [ObjectDefinition]s.
+ *
+ * @param sourceNames The names of the properties or functions annotated with `@ObjectDefinitions` to use as sources of
+ * [ObjectDefinition]s for the test with this annotation. Every property/function must return
+ * `Collection`. If no [sourceNames] are provided, every property and function annotated with
+ * `@ObjectDefinitions` (in this class's companion object) will be used.
+ *
+ * @see org.junit.jupiter.params.provider.ArgumentsSource
+ * @see org.junit.jupiter.params.ParameterizedTest
+ */
+@Target(AnnotationTarget.FUNCTION)
+@Retention(AnnotationRetention.RUNTIME)
+@ArgumentsSource(ObjectDefinitionsProvider::class)
+annotation class ObjectDefinitionSource(vararg val sourceNames: String)
diff --git a/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/stubs/GameObjectStubInfo.kt b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/stubs/GameObjectStubInfo.kt
new file mode 100644
index 000000000..48c842f89
--- /dev/null
+++ b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/stubs/GameObjectStubInfo.kt
@@ -0,0 +1 @@
+package org.apollo.game.plugin.testing.junit.stubs
diff --git a/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/stubs/NpcStubInfo.kt b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/stubs/NpcStubInfo.kt
new file mode 100644
index 000000000..48c842f89
--- /dev/null
+++ b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/stubs/NpcStubInfo.kt
@@ -0,0 +1 @@
+package org.apollo.game.plugin.testing.junit.stubs
diff --git a/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/stubs/PlayerStubInfo.kt b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/stubs/PlayerStubInfo.kt
new file mode 100644
index 000000000..cd192c068
--- /dev/null
+++ b/game/plugin-testing/src/main/kotlin/org/apollo/game/plugin/testing/junit/stubs/PlayerStubInfo.kt
@@ -0,0 +1,25 @@
+package org.apollo.game.plugin.testing.junit.stubs
+
+import org.apollo.game.model.Position
+import org.apollo.game.plugin.testing.junit.api.annotations.Name
+import org.apollo.game.plugin.testing.junit.api.annotations.Pos
+
+class PlayerStubInfo {
+ companion object {
+ fun create(annotations: Collection): PlayerStubInfo {
+ val info = PlayerStubInfo()
+
+ annotations.forEach {
+ when (it) {
+ is Name -> info.name = it.value
+ is Pos -> info.position = Position(it.x, it.y, it.height)
+ }
+ }
+
+ return info
+ }
+ }
+
+ var position = Position(3222, 3222)
+ var name = "test"
+}
diff --git a/game/plugin/api/build.gradle b/game/plugin/api/build.gradle
new file mode 100644
index 000000000..c1c8d477f
--- /dev/null
+++ b/game/plugin/api/build.gradle
@@ -0,0 +1,16 @@
+apply plugin: 'org.jetbrains.kotlin.jvm'
+apply plugin: 'java'
+description = 'Helpers and API for common plugin usecases'
+
+test {
+ useJUnitPlatform()
+}
+
+dependencies {
+ implementation project(':game')
+ implementation project(':cache')
+ implementation project(':net')
+ implementation project(':util')
+
+ testImplementation project(':game:plugin-testing')
+}
diff --git a/game/plugin/api/src/org/apollo/game/plugin/api/Definitions.kt b/game/plugin/api/src/org/apollo/game/plugin/api/Definitions.kt
new file mode 100644
index 000000000..f0873b998
--- /dev/null
+++ b/game/plugin/api/src/org/apollo/game/plugin/api/Definitions.kt
@@ -0,0 +1,104 @@
+package org.apollo.game.plugin.api
+
+import java.lang.IllegalArgumentException
+import org.apollo.cache.def.ItemDefinition
+import org.apollo.cache.def.NpcDefinition
+import org.apollo.cache.def.ObjectDefinition
+
+/**
+ * Provides plugins with access to item, npc, and object definitions
+ */
+object Definitions {
+
+ /**
+ * Returns the [ItemDefinition] with the specified [id]. Callers of this function must perform bounds checking on
+ * the [id] prior to invoking this method (i.e. verify that `id >= 0 && id < ItemDefinition.count()`).
+ *
+ * @throws IndexOutOfBoundsException If the id is out of bounds.
+ */
+ fun item(id: Int): ItemDefinition {
+ return ItemDefinition.lookup(id)
+ }
+
+ /**
+ * Returns the [ItemDefinition] with the specified name, performing case-insensitive matching. If multiple items
+ * share the same name, the item with the lowest id is returned.
+ *
+ * The name may be suffixed with an explicit item id (as a way to disambiguate in the above case), by ending the
+ * name with `_id`, e.g. `monks_robe_42`. If an explicit id is attached, it must be bounds checked (in the same
+ * manner as [item(id: Int)][item]).
+ */
+ fun item(name: String): ItemDefinition? {
+ return findEntity(ItemDefinition::getDefinitions, ItemDefinition::getName, name)
+ }
+
+ /**
+ * Returns the [ObjectDefinition] with the specified [id]. Callers of this function must perform bounds checking on
+ * the [id] prior to invoking this method (i.e. verify that `id >= 0 && id < ObjectDefinition.count()`).
+ *
+ * @throws IndexOutOfBoundsException If the id is out of bounds.
+ */
+ fun obj(id: Int): ObjectDefinition {
+ return ObjectDefinition.lookup(id)
+ }
+
+ /**
+ * Returns the [ObjectDefinition] with the specified name, performing case-insensitive matching. If multiple objects
+ * share the same name, the object with the lowest id is returned.
+ *
+ * The name may be suffixed with an explicit object id (as a way to disambiguate in the above case), by ending the
+ * name with `_id`, e.g. `man_2`. If an explicit id is attached, it must be bounds checked (in the same
+ * manner as [object(id: Int)][object]).
+ */
+ fun obj(name: String): ObjectDefinition? {
+ return findEntity(ObjectDefinition::getDefinitions, ObjectDefinition::getName, name)
+ }
+
+ /**
+ * Returns the [NpcDefinition] with the specified [id]. Callers of this function must perform bounds checking on
+ * the [id] prior to invoking this method (i.e. verify that `id >= 0 && id < NpcDefinition.count()`).
+ *
+ * @throws IndexOutOfBoundsException If the id is out of bounds.
+ */
+ fun npc(id: Int): NpcDefinition {
+ return NpcDefinition.lookup(id)
+ }
+
+ /**
+ * Returns the [NpcDefinition] with the specified name, performing case-insensitive matching. If multiple npcs
+ * share the same name, the npc with the lowest id is returned.
+ *
+ * The name may be suffixed with an explicit npc id (as a way to disambiguate in the above case), by ending the
+ * name with `_id`, e.g. `man_2`. If an explicit id is attached, it must be bounds checked (in the same
+ * manner as [npc(id: Int)][npc]).
+ */
+ fun npc(name: String): NpcDefinition? {
+ return findEntity(NpcDefinition::getDefinitions, NpcDefinition::getName, name)
+ }
+
+ /**
+ * The [Regex] used to match 'names' that have an id attached to the end.
+ */
+ private val ID_REGEX = Regex(".+_[0-9]+$")
+
+ private fun findEntity(
+ definitionsProvider: () -> Array,
+ nameSupplier: T.() -> String,
+ name: String
+ ): T? {
+ val definitions = definitionsProvider()
+
+ if (ID_REGEX matches name) {
+ val id = name.substring(name.lastIndexOf('_') + 1, name.length).toIntOrNull()
+
+ if (id == null || id >= definitions.size) {
+ throw IllegalArgumentException("Error while searching for definition: invalid id suffix in $name.")
+ }
+
+ return definitions[id]
+ }
+
+ val normalizedName = name.replace('_', ' ')
+ return definitions.firstOrNull { it.nameSupplier().equals(normalizedName, ignoreCase = true) }
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/api/src/org/apollo/game/plugin/api/Player.kt b/game/plugin/api/src/org/apollo/game/plugin/api/Player.kt
new file mode 100644
index 000000000..a98a54481
--- /dev/null
+++ b/game/plugin/api/src/org/apollo/game/plugin/api/Player.kt
@@ -0,0 +1,89 @@
+package org.apollo.game.plugin.api
+
+import org.apollo.game.model.entity.Player
+import org.apollo.game.model.entity.Skill
+import org.apollo.game.model.entity.SkillSet
+
+val Player.attack: SkillProxy get() = SkillProxy(skillSet, Skill.ATTACK)
+val Player.defence: SkillProxy get() = SkillProxy(skillSet, Skill.DEFENCE)
+val Player.strength: SkillProxy get() = SkillProxy(skillSet, Skill.STRENGTH)
+val Player.hitpoints: SkillProxy get() = SkillProxy(skillSet, Skill.HITPOINTS)
+val Player.ranged: SkillProxy get() = SkillProxy(skillSet, Skill.RANGED)
+val Player.prayer: SkillProxy get() = SkillProxy(skillSet, Skill.PRAYER)
+val Player.magic: SkillProxy get() = SkillProxy(skillSet, Skill.MAGIC)
+val Player.cooking: SkillProxy get() = SkillProxy(skillSet, Skill.COOKING)
+val Player.woodcutting: SkillProxy get() = SkillProxy(skillSet, Skill.WOODCUTTING)
+val Player.fletching: SkillProxy get() = SkillProxy(skillSet, Skill.FLETCHING)
+val Player.fishing: SkillProxy get() = SkillProxy(skillSet, Skill.FISHING)
+val Player.firemaking: SkillProxy get() = SkillProxy(skillSet, Skill.FIREMAKING)
+val Player.crafting: SkillProxy get() = SkillProxy(skillSet, Skill.CRAFTING)
+val Player.smithing: SkillProxy get() = SkillProxy(skillSet, Skill.SMITHING)
+val Player.mining: SkillProxy get() = SkillProxy(skillSet, Skill.MINING)
+val Player.herblore: SkillProxy get() = SkillProxy(skillSet, Skill.HERBLORE)
+val Player.agility: SkillProxy get() = SkillProxy(skillSet, Skill.AGILITY)
+val Player.thieving: SkillProxy get() = SkillProxy(skillSet, Skill.THIEVING)
+val Player.slayer: SkillProxy get() = SkillProxy(skillSet, Skill.SLAYER)
+val Player.farming: SkillProxy get() = SkillProxy(skillSet, Skill.FARMING)
+val Player.runecraft: SkillProxy get() = SkillProxy(skillSet, Skill.RUNECRAFT)
+
+/**
+ * A proxy class to allow
+ */
+class SkillProxy(private val skills: SkillSet, private val skill: Int) {
+
+ /**
+ * The maximum level of this skill.
+ */
+ val maximum: Int
+ get() = skills.getMaximumLevel(skill)
+
+ /**
+ * The current level of this skill.
+ */
+ val current: Int
+ get() = skills.getCurrentLevel(skill)
+
+ /**
+ * The amount of experience in this skill a player has.
+ */
+ var experience: Double
+ get() = skills.getExperience(skill)
+ set(value) {
+ skills.setExperience(skill, value)
+ }
+
+ /**
+ * Boosts the current level of this skill by [amount], if possible.
+ */
+ fun boost(amount: Int) {
+ require(amount >= 1) { "Can only boost skills by positive values." }
+
+ val new = if (current - maximum > amount) {
+ current
+ } else {
+ Math.min(current + amount, maximum + amount)
+ }
+
+ skills.setCurrentLevel(skill, new)
+ }
+
+ /**
+ * Drains the current level of this skill by [amount], if possible.
+ */
+ fun drain(amount: Int) {
+ require(amount >= 1) { "Can only drain skills by positive values." }
+
+ val new = Math.max(current - amount, 0)
+ skills.setCurrentLevel(skill, new)
+ }
+
+ /**
+ * Restores the current level of this skill by [amount], if possible.
+ */
+ fun restore(amount: Int) {
+ require(amount >= 1) { "Can only restore skills by positive values." }
+
+ val new = Math.min(current + amount, maximum)
+ skills.setCurrentLevel(skill, new)
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/api/src/org/apollo/game/plugin/api/Position.kt b/game/plugin/api/src/org/apollo/game/plugin/api/Position.kt
new file mode 100644
index 000000000..35f901427
--- /dev/null
+++ b/game/plugin/api/src/org/apollo/game/plugin/api/Position.kt
@@ -0,0 +1,12 @@
+package org.apollo.game.plugin.api
+
+import org.apollo.game.model.Position
+
+/**
+ * Support destructuring a Position into its components.
+ */
+object Position {
+ operator fun Position.component1(): Int = x
+ operator fun Position.component2(): Int = y
+ operator fun Position.component3(): Int = height
+}
\ No newline at end of file
diff --git a/game/plugin/api/src/org/apollo/game/plugin/api/Random.kt b/game/plugin/api/src/org/apollo/game/plugin/api/Random.kt
new file mode 100644
index 000000000..a8d538264
--- /dev/null
+++ b/game/plugin/api/src/org/apollo/game/plugin/api/Random.kt
@@ -0,0 +1,9 @@
+package org.apollo.game.plugin.api
+
+import java.util.*
+
+val RAND = Random()
+
+fun rand(bounds: Int): Int {
+ return RAND.nextInt(bounds)
+}
\ No newline at end of file
diff --git a/game/plugin/api/src/org/apollo/game/plugin/api/World.kt b/game/plugin/api/src/org/apollo/game/plugin/api/World.kt
new file mode 100644
index 000000000..6f981e487
--- /dev/null
+++ b/game/plugin/api/src/org/apollo/game/plugin/api/World.kt
@@ -0,0 +1,103 @@
+package org.apollo.game.plugin.api
+
+import org.apollo.game.model.Position
+import org.apollo.game.model.World
+import org.apollo.game.model.area.Region
+import org.apollo.game.model.entity.Entity
+import org.apollo.game.model.entity.EntityType
+import org.apollo.game.model.entity.EntityType.DYNAMIC_OBJECT
+import org.apollo.game.model.entity.EntityType.STATIC_OBJECT
+import org.apollo.game.model.entity.obj.DynamicGameObject
+import org.apollo.game.model.entity.obj.GameObject
+import org.apollo.game.scheduling.ScheduledTask
+
+/**
+ * Finds all of the [Entities][Entity] with the specified [EntityTypes][EntityType] at the specified [position], that
+ * match the provided [predicate].
+ *
+ * ```
+ * const val GOLD_COINS = 995
+ * ...
+ *
+ * val allCoins: Sequence = region.find(position, EntityType.GROUND_ITEM) { item -> item.id == GOLD_COINS }
+ * ```
+ */
+fun Region.find(position: Position, vararg types: EntityType, predicate: (T) -> Boolean): Sequence {
+ return getEntities(position, *types).asSequence().filter(predicate)
+}
+
+/**
+ * Finds the first [GameObject]s with the specified [id] at the specified [position].
+ *
+ * Note that the iteration order of entities in a [Region] is not defined - this function should not be used if there
+ * may be more than [GameObject] with the specified [id] (see [Region.findObjects]).
+ */
+fun Region.findObject(position: Position, id: Int): GameObject? {
+ return find(position, DYNAMIC_OBJECT, STATIC_OBJECT) { it.id == id }
+ .firstOrNull()
+}
+
+/**
+ * Finds **all** [GameObject]s with the specified [id] at the specified [position].
+ */
+fun Region.findObjects(position: Position, id: Int): Sequence {
+ return find(position, DYNAMIC_OBJECT, STATIC_OBJECT) { it.id == id }
+}
+
+/**
+ * Finds the first [GameObject]s with the specified [id] at the specified [position].
+ *
+ * Note that the iteration order of entities in a [Region] is not defined - this function should not be used if there
+ * may be more than [GameObject] with the specified [id] (see [World.findObjects]).
+ */
+fun World.findObject(position: Position, id: Int): GameObject? {
+ return regionRepository.fromPosition(position).findObject(position, id)
+}
+
+/**
+ * Finds **all** [GameObject]s with the specified [id] at the specified [position].
+ */
+fun World.findObjects(position: Position, id: Int): Sequence {
+ return regionRepository.fromPosition(position).findObjects(position, id)
+}
+
+/**
+ * Removes the specified [GameObject] from the world, replacing it with [replacement] object for [delay] **pulses**.
+ */
+fun World.replaceObject(obj: GameObject, replacement: Int, delay: Int) {
+ val replacementObj = DynamicGameObject.createPublic(this, replacement, obj.position, obj.type, obj.orientation)
+
+ schedule(ExpireObjectTask(this, obj, replacementObj, delay))
+}
+
+/**
+ * A [ScheduledTask] that temporarily replaces the [existing] [GameObject] with the [replacement] [GameObject] for the
+ * specified [duration].
+ *
+ * @param existing The [GameObject] that already exists and should be replaced.
+ * @param replacement The [GameObject] to replace the [existing] object with.
+ * @param duration The time, in **pulses**, for the [replacement] object to exist in the game world.
+ */
+private class ExpireObjectTask(
+ private val world: World,
+ private val existing: GameObject,
+ private val replacement: GameObject,
+ private val duration: Int
+) : ScheduledTask(0, true) {
+
+ private var respawning: Boolean = false
+
+ override fun execute() {
+ val region = world.regionRepository.fromPosition(existing.position)
+
+ if (!respawning) {
+ world.spawn(replacement)
+ respawning = true
+ setDelay(duration)
+ } else {
+ region.removeEntity(replacement)
+ world.spawn(existing)
+ stop()
+ }
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/api/test/org/apollo/game/plugin/api/DefinitionsTests.kt b/game/plugin/api/test/org/apollo/game/plugin/api/DefinitionsTests.kt
new file mode 100644
index 000000000..89c662aee
--- /dev/null
+++ b/game/plugin/api/test/org/apollo/game/plugin/api/DefinitionsTests.kt
@@ -0,0 +1,92 @@
+package org.apollo.game.plugin.api
+
+import org.apollo.cache.def.ItemDefinition
+import org.apollo.cache.def.NpcDefinition
+import org.apollo.cache.def.ObjectDefinition
+import org.apollo.game.plugin.testing.junit.ApolloTestingExtension
+import org.apollo.game.plugin.testing.junit.api.annotations.ItemDefinitions
+import org.apollo.game.plugin.testing.junit.api.annotations.NpcDefinitions
+import org.apollo.game.plugin.testing.junit.api.annotations.ObjectDefinitions
+import org.apollo.game.plugin.testing.junit.params.ItemDefinitionSource
+import org.apollo.game.plugin.testing.junit.params.NpcDefinitionSource
+import org.apollo.game.plugin.testing.junit.params.ObjectDefinitionSource
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.junit.jupiter.params.ParameterizedTest
+
+@ExtendWith(ApolloTestingExtension::class)
+class DefinitionsTests {
+
+ @Test
+ fun `can find an ItemDefinition directly using its id`() {
+ val searched = Definitions.item(0)
+ assertEquals(items.first().id, searched.id)
+ }
+
+ @Test
+ fun `can find an ItemDefinition using its name`() {
+ val searched = Definitions.item("item_two")
+ assertEquals(items[2].id, searched?.id)
+ }
+
+ @ParameterizedTest
+ @ItemDefinitionSource
+ fun `can find ItemDefinitions directly using id suffixing`(item: ItemDefinition) {
+ val searched = Definitions.item("${item.name}_${item.id}")
+ assertEquals(item.id, searched?.id)
+ }
+
+ @Test
+ fun `can find an NpcDefinition directly using its id`() {
+ val searched = Definitions.npc(0)
+ assertEquals(npcs.first().id, searched.id)
+ }
+
+ @Test
+ fun `can find an NpcDefinition using its name`() {
+ val searched = Definitions.npc("npc_two")
+ assertEquals(items[2].id, searched?.id)
+ }
+
+ @ParameterizedTest
+ @NpcDefinitionSource
+ fun `can find NpcDefinitions directly using id suffixing`(npc: NpcDefinition) {
+ val searched = Definitions.npc("${npc.name}_${npc.id}")
+ assertEquals(npc.id, searched?.id)
+ }
+
+ @Test
+ fun `can find an ObjectDefinition directly using its id`() {
+ val searched = Definitions.obj(0)
+ assertEquals(objs.first().id, searched.id)
+ }
+
+ @Test
+ fun `can find an ObjectDefinition using its name`() {
+ val searched = Definitions.obj("obj_two")
+ assertEquals(items[2].id, searched?.id)
+ }
+
+ @ParameterizedTest
+ @ObjectDefinitionSource
+ fun `can find ObjectDefinitions directly using id suffixing`(obj: ObjectDefinition) {
+ val searched = Definitions.obj("${obj.name}_${obj.id}")
+ assertEquals(obj.id, searched?.id)
+ }
+
+ private companion object {
+
+ @ItemDefinitions
+ val items = listOf("item zero", "item one", "item two", "item duplicate name", "item duplicate name")
+ .mapIndexed { id, name -> ItemDefinition(id).also { it.name = name } }
+
+ @NpcDefinitions
+ val npcs = listOf("npc zero", "npc one", "npc two", "npc duplicate name", "npc duplicate name")
+ .mapIndexed { id, name -> NpcDefinition(id).also { it.name = name } }
+
+ @ObjectDefinitions
+ val objs = listOf("obj zero", "obj one", "obj two", "obj duplicate name", "obj duplicate name")
+ .mapIndexed { id, name -> ObjectDefinition(id).also { it.name = name } }
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/api/test/org/apollo/game/plugin/api/PlayerTests.kt b/game/plugin/api/test/org/apollo/game/plugin/api/PlayerTests.kt
new file mode 100644
index 000000000..753f16639
--- /dev/null
+++ b/game/plugin/api/test/org/apollo/game/plugin/api/PlayerTests.kt
@@ -0,0 +1,122 @@
+package org.apollo.game.plugin.api
+
+import org.apollo.game.model.entity.Player
+import org.apollo.game.model.entity.Skill
+import org.apollo.game.plugin.testing.junit.ApolloTestingExtension
+import org.apollo.game.plugin.testing.junit.api.annotations.TestMock
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+
+@ExtendWith(ApolloTestingExtension::class)
+class PlayerTests {
+
+ @TestMock
+ lateinit var player: Player
+
+ @BeforeEach
+ fun setHitpointsLevel() {
+ player.skillSet.setSkill(Skill.HITPOINTS, Skill(1_154.0, 10, 10))
+ }
+
+ @Test
+ fun `can boost skill above maximum level`() {
+ player.apply {
+ hitpoints.boost(5)
+
+ assertEquals(15, hitpoints.current)
+ assertEquals(10, hitpoints.maximum)
+ }
+ }
+
+ @Test
+ fun `boosts to the same skill do not accumulate`() {
+ player.apply {
+ hitpoints.boost(5)
+ hitpoints.boost(4)
+
+ assertEquals(15, hitpoints.current)
+ assertEquals(10, hitpoints.maximum)
+ }
+ }
+
+ @Test
+ fun `greater boosts can override earlier boosts`() {
+ player.apply {
+ hitpoints.boost(5)
+ hitpoints.boost(7)
+
+ assertEquals(17, hitpoints.current)
+ assertEquals(10, hitpoints.maximum)
+ }
+ }
+
+ @Test
+ fun `can drain skills`() {
+ player.apply {
+ hitpoints.drain(5)
+
+ assertEquals(5, hitpoints.current)
+ assertEquals(10, hitpoints.maximum)
+ }
+ }
+
+ @Test
+ fun `repeated drains on the same skill accumulate`() {
+ player.apply {
+ hitpoints.drain(4)
+ hitpoints.drain(5)
+
+ assertEquals(1, hitpoints.current)
+ assertEquals(10, hitpoints.maximum)
+ }
+ }
+
+ @Test
+ fun `cannot drain skills below zero`() {
+ player.apply {
+ hitpoints.drain(99)
+
+ assertEquals(0, hitpoints.current)
+ assertEquals(10, hitpoints.maximum)
+ }
+ }
+
+ @Test
+ fun `can restore previously-drained skills`() {
+ player.skillSet.setCurrentLevel(Skill.HITPOINTS, 1)
+
+ player.apply {
+ hitpoints.restore(5)
+
+ assertEquals(6, hitpoints.current)
+ assertEquals(10, hitpoints.maximum)
+ }
+ }
+
+ @Test
+ fun `repeated restores on the same skill accumulate`() {
+ player.skillSet.setCurrentLevel(Skill.HITPOINTS, 1)
+
+ player.apply {
+ hitpoints.restore(3)
+ hitpoints.restore(4)
+
+ assertEquals(8, hitpoints.current)
+ assertEquals(10, hitpoints.maximum)
+ }
+ }
+
+ @Test
+ fun `cannot restore skills above their maximum level`() {
+ player.skillSet.setCurrentLevel(Skill.HITPOINTS, 1)
+
+ player.apply {
+ hitpoints.restore(99)
+
+ assertEquals(10, hitpoints.current)
+ assertEquals(10, hitpoints.maximum)
+ }
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/api/test/org/apollo/game/plugin/api/PositionTests.kt b/game/plugin/api/test/org/apollo/game/plugin/api/PositionTests.kt
new file mode 100644
index 000000000..7d9644bf3
--- /dev/null
+++ b/game/plugin/api/test/org/apollo/game/plugin/api/PositionTests.kt
@@ -0,0 +1,25 @@
+package org.apollo.game.plugin.api
+
+import org.apollo.game.model.Position
+import org.apollo.game.plugin.api.Position.component1
+import org.apollo.game.plugin.api.Position.component2
+import org.apollo.game.plugin.api.Position.component3
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
+
+class PositionTests {
+
+ @Test
+ fun `Positions are destructured in the correct order`() {
+ val x = 10
+ val y = 20
+ val z = 1
+
+ val position = Position(x, y, z)
+ val (x2, y2, z2) = position
+
+ assertEquals(x, x2) { "x coordinate mismatch in Position destructuring." }
+ assertEquals(y, y2) { "y coordinate mismatch in Position destructuring." }
+ assertEquals(z, z2) { "z coordinate mismatch in Position destructuring." }
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/areas/build.gradle b/game/plugin/areas/build.gradle
new file mode 100644
index 000000000..22344beaa
--- /dev/null
+++ b/game/plugin/areas/build.gradle
@@ -0,0 +1,12 @@
+apply plugin: 'kotlin'
+
+description = 'Enables plugins to listen on mobs entering, moving inside of, or leaving a rectangular area.'
+
+dependencies {
+ implementation project(':game')
+ implementation project(':cache')
+ implementation project(':net')
+ implementation project(':util')
+ implementation project(':game:plugin:api')
+ testImplementation project(':game:plugin-testing')
+}
diff --git a/game/plugin/areas/src/Area.kt b/game/plugin/areas/src/Area.kt
new file mode 100644
index 000000000..e594a170d
--- /dev/null
+++ b/game/plugin/areas/src/Area.kt
@@ -0,0 +1,25 @@
+package org.apollo.game.plugins.area
+
+import org.apollo.game.model.Position
+import org.apollo.game.plugin.api.Position.component1
+import org.apollo.game.plugin.api.Position.component2
+import org.apollo.game.plugin.api.Position.component3
+
+/**
+ * An area in the game world.
+ */
+interface Area {
+
+ /**
+ * Returns whether or not the specified [Position] is inside this [Area].
+ */
+ operator fun contains(position: Position): Boolean
+}
+
+internal class RectangularArea(private val x: IntRange, private val y: IntRange, private val height: Int) : Area {
+
+ override operator fun contains(position: Position): Boolean {
+ val (x, y, z) = position
+ return x in this.x && y in this.y && z == height
+ }
+}
diff --git a/game/plugin/areas/src/AreaAction.kt b/game/plugin/areas/src/AreaAction.kt
new file mode 100644
index 000000000..85d8141a5
--- /dev/null
+++ b/game/plugin/areas/src/AreaAction.kt
@@ -0,0 +1,51 @@
+package org.apollo.game.plugins.area
+
+import org.apollo.game.model.Position
+import org.apollo.game.model.entity.Player
+
+/**
+ * A set of actions to execute when a player enters, moves inside, or exits a specific area of the world.
+ */
+internal class AreaAction(val entrance: AreaListener, val inside: AreaListener, val exit: AreaListener)
+
+/**
+ * A function that is invoked when a player enters, moves inside of, or exits an [Area].
+ */
+typealias AreaListener = Player.(Position) -> Unit
+
+/**
+ * Registers an [AreaAction] for the specified [Area] using the builder.
+ */
+fun action(name: String, area: Area, builder: AreaActionBuilder.() -> Unit) {
+ actions += AreaActionBuilder(name, area).apply(builder).build()
+}
+
+/**
+ * Registers an [AreaAction] for the specified [Area] using the builder.
+ *
+ * @param predicate The predicate that determines whether or not the given [Position] is inside the [Area].
+ */
+fun action(name: String, predicate: (Position) -> Boolean, builder: AreaActionBuilder.() -> Unit) {
+ val area = object : Area {
+ override fun contains(position: Position): Boolean = predicate(position)
+ }
+
+ action(name, area, builder)
+}
+
+/**
+ * Registers an [AreaAction] for the specified [Area] using the builder.
+ *
+ * @param x The `x` coordinate range, both ends inclusive.
+ * @param y The `y` coordinate range, both ends inclusive.
+ */
+fun action(name: String, x: IntRange, y: IntRange, height: Int = 0, builder: AreaActionBuilder.() -> Unit) {
+ val area = RectangularArea(x, y, height)
+
+ action(name, area, builder)
+}
+
+/**
+ * The [Set] of ([Area], [AreaAction]) [Pair]s.
+ */
+internal val actions = mutableSetOf>()
diff --git a/game/plugin/areas/src/AreaActionBuilder.kt b/game/plugin/areas/src/AreaActionBuilder.kt
new file mode 100644
index 000000000..7da8aeada
--- /dev/null
+++ b/game/plugin/areas/src/AreaActionBuilder.kt
@@ -0,0 +1,41 @@
+package org.apollo.game.plugins.area
+
+/**
+ * A builder for ([Area], [AreaAction]) [Pair]s.
+ */
+class AreaActionBuilder internal constructor(val name: String, val area: Area) {
+
+ private var entrance: AreaListener = { }
+
+ private var inside: AreaListener = { }
+
+ private var exit: AreaListener = { }
+
+ /**
+ * Places the contents of this builder into an ([Area], [AreaAction]) [Pair].
+ */
+ internal fun build(): Pair {
+ return Pair(area, AreaAction(entrance, inside, exit))
+ }
+
+ /**
+ * The [listener] to execute when a player enters the associated [Area].
+ */
+ fun entrance(listener: AreaListener) {
+ this.entrance = listener
+ }
+
+ /**
+ * The [listener] to execute when a player moves around inside the associated [Area].
+ */
+ fun inside(listener: AreaListener) {
+ this.inside = listener
+ }
+
+ /**
+ * The [listener] to execute when a player moves exits the associated [Area].
+ */
+ fun exit(listener: AreaListener) {
+ this.exit = listener
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/areas/src/Areas.plugin.kts b/game/plugin/areas/src/Areas.plugin.kts
new file mode 100644
index 000000000..e029b1999
--- /dev/null
+++ b/game/plugin/areas/src/Areas.plugin.kts
@@ -0,0 +1,23 @@
+
+import org.apollo.game.model.entity.Player
+import org.apollo.game.model.event.impl.MobPositionUpdateEvent
+import org.apollo.game.plugins.area.actions
+
+/**
+ * Intercepts the [MobPositionUpdateEvent] and invokes area actions if necessary.
+ */
+on_event { MobPositionUpdateEvent::class }
+ .where { mob is Player }
+ .then {
+ for ((area, action) in actions) {
+ if (mob.position in area) {
+ if (next in area) {
+ action.inside(mob as Player, next)
+ } else {
+ action.exit(mob as Player, next)
+ }
+ } else if (next in area) {
+ action.entrance(mob as Player, next)
+ }
+ }
+ }
diff --git a/game/plugin/areas/test/AreaActionTests.kt b/game/plugin/areas/test/AreaActionTests.kt
new file mode 100644
index 000000000..7c577efd9
--- /dev/null
+++ b/game/plugin/areas/test/AreaActionTests.kt
@@ -0,0 +1,58 @@
+import org.apollo.game.model.Position
+import org.apollo.game.model.entity.Player
+import org.apollo.game.plugin.testing.junit.ApolloTestingExtension
+import org.apollo.game.plugin.testing.junit.api.annotations.TestMock
+import org.apollo.game.plugins.area.action
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+
+@ExtendWith(ApolloTestingExtension::class)
+class AreaActionTests {
+
+ @TestMock
+ lateinit var player: Player
+
+ @Test
+ fun `entrance action is triggered when a player enters the area`() {
+ var triggered = false
+ val position = Position(3222, 3222)
+
+ action("entrance_test_action", predicate = { it == player.position }) {
+ triggered = true
+ }
+
+ player.position = position
+
+ assertTrue(triggered) { "entrance_test_action was not triggered." }
+ }
+
+ @Test
+ fun `inside action is triggered when a player moves inside an area`() {
+ player.position = Position(3222, 3222)
+ var triggered = false
+
+ action("inside_test_action", x = 3220..3224, y = 3220..3224) {
+ triggered = true
+ }
+
+ player.position = Position(3223, 3222)
+
+ assertTrue(triggered) { "inside_test_action was not triggered." }
+ }
+
+ @Test
+ fun `exit action is triggered when a player exits the area`() {
+ player.position = Position(3222, 3222)
+
+ var triggered = false
+
+ action("exit_test_action", predicate = { it == player.position }) {
+ triggered = true
+ }
+
+ player.position = Position(3221, 3221)
+
+ assertTrue(triggered) { "exit_test_action was not triggered." }
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/bank/build.gradle b/game/plugin/bank/build.gradle
new file mode 100644
index 000000000..3a9351392
--- /dev/null
+++ b/game/plugin/bank/build.gradle
@@ -0,0 +1,10 @@
+apply plugin: 'kotlin'
+
+dependencies {
+ implementation project(':game')
+ implementation project(':cache')
+ implementation project(':net')
+ implementation project(':util')
+
+ testImplementation project(':game:plugin-testing')
+}
diff --git a/game/plugin/bank/src/bank.plugin.kts b/game/plugin/bank/src/bank.plugin.kts
new file mode 100644
index 000000000..0aef5ac18
--- /dev/null
+++ b/game/plugin/bank/src/bank.plugin.kts
@@ -0,0 +1,73 @@
+import org.apollo.game.action.DistancedAction
+import org.apollo.game.message.impl.NpcActionMessage
+import org.apollo.game.message.impl.ObjectActionMessage
+import org.apollo.game.model.Position
+import org.apollo.game.model.entity.Npc
+import org.apollo.game.model.entity.Player
+import org.apollo.game.model.inter.bank.BankUtils
+import org.apollo.net.message.Message
+
+val BANK_BOOTH_ID = 2213
+
+/**
+ * Hook into the [ObjectActionMessage] and listen for when a bank booth's second action ("Open Bank") is selected.
+ */
+on { ObjectActionMessage::class }
+ .where { option == 2 && id == BANK_BOOTH_ID }
+ .then { BankAction.start(this, it, position) }
+
+/**
+ * Hook into the [NpcActionMessage] and listen for when a banker's second action ("Open Bank") is selected.
+ */
+on { NpcActionMessage::class }
+ .where { option == 2 }
+ .then {
+ val npc = it.world.npcRepository[index]
+
+ if (npc.id in BANKER_NPCS) {
+ BankAction.start(this, it, npc.position)
+ }
+ }
+
+/**
+ * The ids of all banker [Npcs][Npc].
+ */
+val BANKER_NPCS = setOf(166, 494, 495, 496, 497, 498, 499, 1036, 1360, 1702, 2163, 2164, 2354, 2355, 2568, 2569, 2570)
+
+/**
+ * A [DistancedAction] that opens a [Player]'s bank when they get close enough to a booth or banker.
+ *
+ * @property position The [Position] of the booth/[Npc].
+ */
+class BankAction(player: Player, position: Position) : DistancedAction(0, true, player, position, DISTANCE) {
+
+ companion object {
+
+ /**
+ * The distance threshold that must be reached before the bank interface is opened.
+ */
+ const val DISTANCE = 1
+
+ /**
+ * Starts a [BankAction] for the specified [Player], terminating the [Message] that triggered.
+ */
+ fun start(message: Message, player: Player, position: Position) {
+ player.startAction(BankAction(player, position))
+ message.terminate()
+ }
+ }
+
+ override fun executeAction() {
+ mob.turnTo(position)
+ BankUtils.openBank(mob)
+ stop()
+ }
+
+ override fun equals(other: Any?): Boolean {
+ return other is BankAction && position == other.position
+ }
+
+ override fun hashCode(): Int {
+ return position.hashCode()
+ }
+}
diff --git a/game/plugin/bank/test/OpenBankTest.kt b/game/plugin/bank/test/OpenBankTest.kt
new file mode 100644
index 000000000..578325e08
--- /dev/null
+++ b/game/plugin/bank/test/OpenBankTest.kt
@@ -0,0 +1,52 @@
+
+import org.apollo.game.model.Position
+import org.apollo.game.model.World
+import org.apollo.game.model.entity.Player
+import org.apollo.game.plugin.testing.assertions.verifyAfter
+import org.apollo.game.plugin.testing.junit.ApolloTestingExtension
+import org.apollo.game.plugin.testing.junit.api.ActionCapture
+import org.apollo.game.plugin.testing.junit.api.annotations.TestMock
+import org.apollo.game.plugin.testing.junit.api.interactions.interactWith
+import org.apollo.game.plugin.testing.junit.api.interactions.spawnNpc
+import org.apollo.game.plugin.testing.junit.api.interactions.spawnObject
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+
+@ExtendWith(ApolloTestingExtension::class)
+class OpenBankTest {
+
+ companion object {
+ const val BANK_BOOTH_ID = 2213
+ const val BANK_TELLER_ID = 166
+
+ val BANK_POSITION = Position(3200, 3200, 0)
+ }
+
+ @TestMock
+ lateinit var action: ActionCapture
+
+ @TestMock
+ lateinit var player: Player
+
+ @TestMock
+ lateinit var world: World
+
+ @Test
+ fun `Interacting with a bank teller should open the players bank`() {
+ 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
+ player.interactWith(bankTeller, option = 2)
+
+ verifyAfter(action.complete()) { player.openBank() }
+ }
+
+ @Test
+ fun `Interacting with a bank booth object should open the players bank`() {
+ val bankBooth = world.spawnObject(BANK_BOOTH_ID, BANK_POSITION)
+
+ player.interactWith(bankBooth, option = 2)
+
+ verifyAfter(action.complete()) { player.openBank() }
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/build.gradle b/game/plugin/build.gradle
new file mode 100644
index 000000000..24ba2cc84
--- /dev/null
+++ b/game/plugin/build.gradle
@@ -0,0 +1,33 @@
+gradle.projectsEvaluated {
+ configure(subprojects.findAll { it.buildFile.exists() }) { subproj ->
+ apply from: "$rootDir/gradle/kotlin.gradle"
+
+ sourceSets {
+ main {
+ kotlin {
+ srcDirs += "src"
+ }
+ }
+
+ test {
+ kotlin {
+ srcDirs += "test"
+ }
+ }
+ }
+
+ test {
+ useJUnitPlatform()
+ }
+
+ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+ }
+
+ dependencies {
+ implementation group: 'com.google.guava', name: 'guava', version: guavaVersion
+ }
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/chat/private-messaging/build.gradle b/game/plugin/chat/private-messaging/build.gradle
new file mode 100644
index 000000000..3a9351392
--- /dev/null
+++ b/game/plugin/chat/private-messaging/build.gradle
@@ -0,0 +1,10 @@
+apply plugin: 'kotlin'
+
+dependencies {
+ implementation project(':game')
+ implementation project(':cache')
+ implementation project(':net')
+ implementation project(':util')
+
+ testImplementation project(':game:plugin-testing')
+}
diff --git a/game/plugin/chat/private-messaging/src/friends.plugin.kts b/game/plugin/chat/private-messaging/src/friends.plugin.kts
new file mode 100644
index 000000000..6ac357d72
--- /dev/null
+++ b/game/plugin/chat/private-messaging/src/friends.plugin.kts
@@ -0,0 +1,21 @@
+import org.apollo.game.message.impl.AddFriendMessage
+import org.apollo.game.message.impl.SendFriendMessage
+import org.apollo.game.model.entity.setting.PrivacyState
+
+on { AddFriendMessage::class }
+ .then {
+ it.addFriend(username)
+
+ val friend = it.world.getPlayer(username)
+
+ 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/plugin/chat/private-messaging/src/ignores.plugin.kts b/game/plugin/chat/private-messaging/src/ignores.plugin.kts
new file mode 100644
index 000000000..47b0f29fe
--- /dev/null
+++ b/game/plugin/chat/private-messaging/src/ignores.plugin.kts
@@ -0,0 +1,8 @@
+import org.apollo.game.message.impl.AddIgnoreMessage
+import org.apollo.game.message.impl.RemoveIgnoreMessage
+
+on { AddIgnoreMessage::class }
+ .then { it.addIgnore(username) }
+
+on { RemoveIgnoreMessage::class }
+ .then { it.removeIgnore(username) }
\ No newline at end of file
diff --git a/game/plugin/chat/private-messaging/src/messaging.plugin.kts b/game/plugin/chat/private-messaging/src/messaging.plugin.kts
new file mode 100644
index 000000000..d9fd21e92
--- /dev/null
+++ b/game/plugin/chat/private-messaging/src/messaging.plugin.kts
@@ -0,0 +1,25 @@
+import org.apollo.game.message.impl.ForwardPrivateChatMessage
+import org.apollo.game.message.impl.PrivateChatMessage
+import org.apollo.game.model.entity.Player
+import org.apollo.game.model.entity.setting.PrivacyState.OFF
+import org.apollo.game.model.entity.setting.PrivacyState.ON
+
+on { PrivateChatMessage::class }
+ .then {
+ val friend = it.world.getPlayer(username)
+
+ if (interactionPermitted(it, friend)) {
+ friend.send(ForwardPrivateChatMessage(it.username, it.privilegeLevel, compressedMessage))
+ }
+ }
+
+fun interactionPermitted(player: Player, friend: Player?): Boolean {
+ val username = player.username
+ val privacy = friend?.friendPrivacy
+
+ if (friend == null || friend.hasIgnored(username)) {
+ return false
+ } else {
+ return if (friend.friendsWith(username)) privacy != OFF else privacy == ON
+ }
+}
diff --git a/game/plugin/cmd/build.gradle b/game/plugin/cmd/build.gradle
new file mode 100644
index 000000000..4d96c9f57
--- /dev/null
+++ b/game/plugin/cmd/build.gradle
@@ -0,0 +1,10 @@
+apply plugin: 'kotlin'
+
+dependencies {
+ implementation project(':game')
+ implementation project(':cache')
+ implementation project(':net')
+ implementation project(':util')
+ implementation project(':game:plugin:api')
+ testImplementation project(':game:plugin-testing')
+}
diff --git a/game/plugin/cmd/src/animate-cmd.plugin.kts b/game/plugin/cmd/src/animate-cmd.plugin.kts
new file mode 100644
index 000000000..20993a93c
--- /dev/null
+++ b/game/plugin/cmd/src/animate-cmd.plugin.kts
@@ -0,0 +1,15 @@
+import org.apollo.game.model.Animation
+import org.apollo.game.model.entity.setting.PrivilegeLevel
+
+on_command("animate", PrivilegeLevel.MODERATOR)
+ .then { player ->
+ arguments.firstOrNull()
+ ?.let(String::toIntOrNull)
+ ?.let(::Animation)
+ ?.let {
+ player.playAnimation(it)
+ return@then
+ }
+
+ player.sendMessage("Invalid syntax - ::animate [animation-id]")
+ }
\ No newline at end of file
diff --git a/game/plugin/cmd/src/bank-cmd.plugin.kts b/game/plugin/cmd/src/bank-cmd.plugin.kts
new file mode 100644
index 000000000..6ded33ebc
--- /dev/null
+++ b/game/plugin/cmd/src/bank-cmd.plugin.kts
@@ -0,0 +1,5 @@
+import org.apollo.game.model.entity.setting.PrivilegeLevel
+
+// Opens the player's bank if they are an administrator.
+on_command("bank", PrivilegeLevel.ADMINISTRATOR)
+ .then { player -> player.openBank() }
\ No newline at end of file
diff --git a/game/plugin/cmd/src/item-cmd.plugin.kts b/game/plugin/cmd/src/item-cmd.plugin.kts
new file mode 100644
index 000000000..f6c27ab85
--- /dev/null
+++ b/game/plugin/cmd/src/item-cmd.plugin.kts
@@ -0,0 +1,42 @@
+import com.google.common.primitives.Ints
+import org.apollo.cache.def.ItemDefinition
+import org.apollo.game.model.entity.setting.PrivilegeLevel
+
+on_command("item", PrivilegeLevel.ADMINISTRATOR)
+ .then { player ->
+ if (arguments.size !in 1..2) {
+ player.sendMessage("Invalid syntax - ::item [id] [amount]")
+ return@then
+ }
+
+ val id = Ints.tryParse(arguments[0])
+ if (id == null) {
+ player.sendMessage("Invalid syntax - ::item [id] [amount]")
+ return@then
+ }
+
+ val amount = if (arguments.size == 2) {
+ val amt = Ints.tryParse(arguments[1])
+
+ if (amt == null) {
+ player.sendMessage("Invalid syntax - ::item [id] [amount]")
+ return@then
+ }
+
+ amt
+ } else {
+ 1
+ }
+
+ if (id < 0 || id >= ItemDefinition.count()) {
+ player.sendMessage("The item id you specified is out of bounds!")
+ return@then
+ }
+
+ if (amount < 0) {
+ player.sendMessage("The amount you specified is out of bounds!")
+ return@then
+ }
+
+ player.inventory.add(id, amount)
+ }
\ No newline at end of file
diff --git a/game/plugin/cmd/src/lookup.plugin.kts b/game/plugin/cmd/src/lookup.plugin.kts
new file mode 100644
index 000000000..80d5178c0
--- /dev/null
+++ b/game/plugin/cmd/src/lookup.plugin.kts
@@ -0,0 +1,52 @@
+import org.apollo.game.model.entity.setting.PrivilegeLevel
+import org.apollo.game.plugin.api.Definitions
+
+on_command("iteminfo", PrivilegeLevel.ADMINISTRATOR)
+ .then { player ->
+ arguments.firstOrNull()
+ ?.let(String::toIntOrNull)
+ ?.let(Definitions::item)
+ ?.apply {
+ val members = if (isMembersOnly) "members" else "not members"
+
+ player.sendMessage("Item $id is called $name and is $members only.")
+ player.sendMessage("Its description is `$description`.")
+
+ return@then
+ }
+
+ player.sendMessage("Invalid syntax - ::iteminfo [item id]")
+ }
+
+on_command("npcinfo", PrivilegeLevel.ADMINISTRATOR)
+ .then { player ->
+ arguments.firstOrNull()
+ ?.let(String::toIntOrNull)
+ ?.let(Definitions::npc)
+ ?.apply {
+ val combat = if (hasCombatLevel()) "has a combat level of $combatLevel" else
+ "does not have a combat level"
+
+ player.sendMessage("Npc $id is called $name and $combat.")
+ player.sendMessage("Its description is `$description`.")
+
+ return@then
+ }
+
+ player.sendMessage("Invalid syntax - ::npcinfo [npc id]")
+ }
+
+on_command("objectinfo", PrivilegeLevel.ADMINISTRATOR)
+ .then { player ->
+ arguments.firstOrNull()
+ ?.let(String::toIntOrNull)
+ ?.let(Definitions::obj)
+ ?.apply {
+ player.sendMessage("Object ${arguments[0]} is called $name (width=$width, length=$length).")
+ player.sendMessage("Its description is `$description`.")
+
+ return@then
+ }
+
+ player.sendMessage("Invalid syntax - ::objectinfo [object id]")
+ }
\ No newline at end of file
diff --git a/game/plugin/cmd/src/messaging-cmd.plugin.kts b/game/plugin/cmd/src/messaging-cmd.plugin.kts
new file mode 100644
index 000000000..304c14685
--- /dev/null
+++ b/game/plugin/cmd/src/messaging-cmd.plugin.kts
@@ -0,0 +1,11 @@
+import org.apollo.game.model.entity.setting.PrivilegeLevel
+
+on_command("broadcast", PrivilegeLevel.ADMINISTRATOR)
+ .then { player ->
+ val message = arguments.joinToString(" ")
+ val broadcast = "[Broadcast] ${player.username.capitalize()}: $message"
+
+ player.world.playerRepository.forEach { other ->
+ other.sendMessage(broadcast)
+ }
+ }
diff --git a/game/plugin/cmd/src/punish-cmd.plugin.kts b/game/plugin/cmd/src/punish-cmd.plugin.kts
new file mode 100644
index 000000000..3df799c47
--- /dev/null
+++ b/game/plugin/cmd/src/punish-cmd.plugin.kts
@@ -0,0 +1,59 @@
+import org.apollo.game.model.entity.Player
+import org.apollo.game.model.entity.setting.PrivilegeLevel
+
+/**
+ * Adds a command to mute a player. Admins cannot be muted.
+ */
+on_command("mute", PrivilegeLevel.MODERATOR)
+ .then { player ->
+ val name = arguments.joinToString(" ")
+ val targetPlayer = player.world.getPlayer(name)
+
+ if (validate(player, targetPlayer)) {
+ targetPlayer.isMuted = true
+ player.sendMessage("You have just unmuted ${targetPlayer.username}.")
+ }
+ }
+
+/**
+ * Adds a command to unmute a player.
+ */
+on_command("unmute", PrivilegeLevel.MODERATOR)
+ .then { player ->
+ val name = arguments.joinToString(" ")
+ val targetPlayer = player.world.getPlayer(name)
+
+ if (validate(player, targetPlayer)) {
+ targetPlayer.isMuted = false
+ player.sendMessage("You have just unmuted ${targetPlayer.username}.")
+ }
+ }
+
+/**
+ * Adds a command to ban a player. Admins cannot be banned.
+ */
+on_command("ban", PrivilegeLevel.ADMINISTRATOR)
+ .then { player ->
+ val name = arguments.joinToString(" ")
+ val targetPlayer = player.world.getPlayer(name)
+
+ if (validate(player, targetPlayer)) {
+ targetPlayer.ban()
+ targetPlayer.logout() // TODO force logout
+ player.sendMessage("You have just banned ${targetPlayer.username}.")
+ }
+ }
+
+/**
+ * Ensures the player isn't null, and that they aren't an Administrator.
+ */
+fun validate(player: Player, targetPlayer: Player?): Boolean {
+ if (targetPlayer == null) {
+ player.sendMessage("That player does not exist.")
+ return false
+ } else if (targetPlayer.privilegeLevel == PrivilegeLevel.ADMINISTRATOR) {
+ player.sendMessage("You cannot perform this action on Administrators.")
+ return false
+ }
+ return true
+}
\ No newline at end of file
diff --git a/game/plugin/cmd/src/skill-cmd.plugin.kts b/game/plugin/cmd/src/skill-cmd.plugin.kts
new file mode 100644
index 000000000..b95ed0181
--- /dev/null
+++ b/game/plugin/cmd/src/skill-cmd.plugin.kts
@@ -0,0 +1,85 @@
+import com.google.common.primitives.Doubles
+import com.google.common.primitives.Ints
+import org.apollo.game.model.entity.Skill
+import org.apollo.game.model.entity.SkillSet
+import org.apollo.game.model.entity.setting.PrivilegeLevel
+
+/**
+ * Maximises the player's skill set.
+ */
+on_command("max", PrivilegeLevel.ADMINISTRATOR)
+ .then { player ->
+ val skills = player.skillSet
+
+ for (skill in 0 until skills.size()) {
+ skills.addExperience(skill, SkillSet.MAXIMUM_EXP)
+ }
+ }
+
+/**
+ * Levels the specified skill to the specified level, optionally updating the current level as well.
+ */
+on_command("level", PrivilegeLevel.ADMINISTRATOR)
+ .then { player ->
+ val invalidSyntax = "Invalid syntax - ::level [skill-id] [level] "
+ if (arguments.size !in 2..3) {
+ player.sendMessage(invalidSyntax)
+ return@then
+ }
+
+ val skillId = Ints.tryParse(arguments[0])
+ if (skillId == null) {
+ player.sendMessage(invalidSyntax)
+ return@then
+ }
+ val level = Ints.tryParse(arguments[1])
+ if (level == null) {
+ player.sendMessage(invalidSyntax)
+ return@then
+ }
+
+ if (skillId !in 0..20 || level !in 1..99) {
+ player.sendMessage(invalidSyntax)
+ return@then
+ }
+
+ val experience = SkillSet.getExperienceForLevel(level).toDouble()
+ var current = level
+
+ if (arguments.size == 3 && arguments[2] == "old") {
+ val skill = player.skillSet.getSkill(skillId)
+ current = skill.currentLevel
+ }
+
+ player.skillSet.setSkill(skillId, Skill(experience, current, level))
+ }
+
+/**
+ * Adds the specified amount of experience to the specified skill.
+ */
+on_command("xp", PrivilegeLevel.ADMINISTRATOR)
+ .then { player ->
+ val invalidSyntax = "Invalid syntax - ::xp [skill-id] [experience]"
+ if (arguments.size != 2) {
+ player.sendMessage(invalidSyntax)
+ return@then
+ }
+
+ val skillId = Ints.tryParse(arguments[0])
+ if (skillId == null) {
+ player.sendMessage(invalidSyntax)
+ return@then
+ }
+ val experience = Doubles.tryParse(arguments[1])
+ if (experience == null) {
+ player.sendMessage(invalidSyntax)
+ return@then
+ }
+
+ if (skillId !in 0..20 || experience <= 0) {
+ player.sendMessage("Invalid syntax - ::xp [skill-id] [experience]")
+ return@then
+ }
+
+ player.skillSet.addExperience(skillId, experience)
+ }
\ No newline at end of file
diff --git a/game/plugin/cmd/src/spawn-cmd.plugin.kts b/game/plugin/cmd/src/spawn-cmd.plugin.kts
new file mode 100644
index 000000000..4626503d2
--- /dev/null
+++ b/game/plugin/cmd/src/spawn-cmd.plugin.kts
@@ -0,0 +1,121 @@
+import com.google.common.primitives.Ints
+import org.apollo.game.model.Position
+import org.apollo.game.model.entity.Npc
+import org.apollo.game.model.entity.setting.PrivilegeLevel
+
+/**
+ * An array of npcs that cannot be spawned.
+ */
+val blacklist: IntArray = intArrayOf()
+
+/**
+ * Spawns a non-blacklisted npc in the specified position, or the player's position if both 'x' and
+ * 'y' are not supplied.
+ */
+on_command("spawn", PrivilegeLevel.ADMINISTRATOR)
+ .then { player ->
+ val invalidSyntax = "Invalid syntax - ::spawn [npc id] [optional-x] [optional-y] [optional-z]"
+ if (arguments.size !in intArrayOf(1, 3, 4)) {
+ player.sendMessage(invalidSyntax)
+ return@then
+ }
+
+ val id = Ints.tryParse(arguments[0])
+ if (id == null) {
+ player.sendMessage(invalidSyntax)
+ return@then
+ }
+
+ if (id in blacklist) {
+ player.sendMessage("Sorry, npc $id is blacklisted!")
+ return@then
+ }
+
+ val position: Position?
+ if (arguments.size == 1) {
+ position = player.position
+ } else {
+ val x = Ints.tryParse(arguments[1])
+ val y = Ints.tryParse(arguments[2])
+
+ if (x == null || y == null) {
+ player.sendMessage(invalidSyntax)
+ return@then
+ }
+
+ val height = if (arguments.size == 4) {
+ val h = Ints.tryParse(arguments[3])
+ if (h == null) {
+ player.sendMessage(invalidSyntax)
+ return@then
+ }
+
+ h
+ } else {
+ player.position.height
+ }
+
+ position = Position(x, y, height)
+ }
+
+ player.world.register(Npc(player.world, id, position))
+ }
+
+/**
+ * Mass spawns npcs around the player.
+ */
+on_command("mass", PrivilegeLevel.ADMINISTRATOR)
+ .then { player ->
+ val invalidSyntax = "Invalid syntax - ::mass [npc id] [range (1-5)]"
+ if (arguments.size != 2) {
+ player.sendMessage(invalidSyntax)
+ return@then
+ }
+
+ val id = Ints.tryParse(arguments[0])
+ if (id == null) {
+ player.sendMessage(invalidSyntax)
+ return@then
+ }
+
+ val range = Ints.tryParse(arguments[1])
+ if (range == null) {
+ player.sendMessage(invalidSyntax)
+ return@then
+ }
+
+ if (id < 0 || range !in 1..5) {
+ player.sendMessage(invalidSyntax)
+ return@then
+ }
+
+ if (id in blacklist) {
+ player.sendMessage("Sorry, npc $id is blacklisted!")
+ return@then
+ }
+
+ val centerPosition = player.position
+
+ val minX = centerPosition.x - range
+ val minY = centerPosition.y - range
+ val maxX = centerPosition.x + range
+ val maxY = centerPosition.y + range
+ val z = centerPosition.height
+
+ for (x in minX..maxX) {
+ for (y in minY..maxY) {
+ player.world.register(Npc(player.world, id, Position(x, y, z)))
+ }
+ }
+
+ player.sendMessage("Mass spawning npcs with id $id.")
+ }
+
+/**
+ * Unregisters all npcs from the world npc repository.
+ */
+on_command("clearnpcs", PrivilegeLevel.ADMINISTRATOR)
+ .then { player ->
+ player.world.npcRepository.forEach { npc -> player.world.unregister(npc) }
+ player.sendMessage("Unregistered all npcs from the world.")
+ }
\ No newline at end of file
diff --git a/game/plugin/cmd/src/teleport-cmd.plugin.kts b/game/plugin/cmd/src/teleport-cmd.plugin.kts
new file mode 100644
index 000000000..2ff9ad5db
--- /dev/null
+++ b/game/plugin/cmd/src/teleport-cmd.plugin.kts
@@ -0,0 +1,154 @@
+
+import com.google.common.primitives.Ints
+import org.apollo.game.model.Position
+import org.apollo.game.model.entity.Player
+import org.apollo.game.model.entity.setting.PrivilegeLevel
+import org.apollo.game.plugin.api.Position.component1
+import org.apollo.game.plugin.api.Position.component2
+import org.apollo.game.plugin.api.Position.component3
+
+/**
+ * Sends a player's position.
+ */
+on_command("pos", PrivilegeLevel.MODERATOR)
+ .then { player ->
+ val target: Player
+ val name: String
+
+ if (arguments.size >= 1) {
+ name = arguments.joinToString(" ")
+ if (player.world.isPlayerOnline(name)) {
+ target = player.world.getPlayer(name)
+ } else {
+ player.sendMessage("$name is offline.")
+ return@then
+ }
+ } else {
+ target = player
+ }
+
+ val (x, y, z) = target.position
+ val region = target.position.regionCoordinates
+
+ player.sendMessage("${target.username} is located at ($x, $y, $z) in region (${region.x}, ${region.y}).")
+ }
+
+/**
+ * Teleports the player to the specified position.
+ */
+on_command("tele", PrivilegeLevel.ADMINISTRATOR)
+ .then { player ->
+ val invalidSyntax = "Invalid syntax - ::tele [x] [y] [optional-z] or ::tele [place name]"
+
+ if (arguments.size == 1) {
+ val query = arguments[0]
+ val results = TELEPORT_DESTINATIONS.filter { (name) -> name.startsWith(query) }
+
+ if (results.isEmpty()) {
+ player.sendMessage("No destinations matching '$query'.")
+ player.sendMessage(invalidSyntax)
+ return@then
+ } else if (results.size > 1) {
+ player.sendMessage("Ambiguous query '$query' (could be $results). Please disambiguate.")
+ return@then
+ }
+
+ val (name, dest) = results[0]
+ player.sendMessage("Teleporting to $name.")
+ player.teleport(dest)
+
+ return@then
+ }
+
+ if (arguments.size !in 2..3) {
+ player.sendMessage(invalidSyntax)
+ return@then
+ }
+
+ val x = Ints.tryParse(arguments[0])
+ if (x == null) {
+ player.sendMessage(invalidSyntax)
+ return@then
+ }
+
+ val y = Ints.tryParse(arguments[1])
+ if (y == null) {
+ player.sendMessage(invalidSyntax)
+ return@then
+ }
+
+ var z = player.position.height
+ if (arguments.size == 3) {
+ val plane = Ints.tryParse(arguments[2])
+ if (plane == null) {
+ player.sendMessage(invalidSyntax)
+ return@then
+ }
+ z = plane
+ }
+
+ if (z in 0..4) {
+ player.teleport(Position(x, y, z))
+ }
+ }
+
+/**
+ * Teleports the player to another player.
+ */
+on_command("teleto", PrivilegeLevel.ADMINISTRATOR)
+ .then { player ->
+ val invalidSyntax = "Invalid syntax - ::teleto [player name]"
+
+ if (arguments.size == 1) {
+ val playerName = arguments[0]
+ try {
+ val foundPlayer = player.world.getPlayer(playerName)
+
+ if (foundPlayer == null) {
+ player.sendMessage("Player $playerName is currently offline or does not exist.")
+ return@then
+ }
+
+ player.teleport(foundPlayer.position)
+ player.sendMessage("You have teleported to player $playerName.")
+ } catch (_: Exception) {
+ // Invalid player name syntax
+ player.sendMessage(invalidSyntax)
+ }
+ } else {
+ player.sendMessage(invalidSyntax)
+ }
+ }
+
+internal val TELEPORT_DESTINATIONS = listOf(
+ "alkharid" to Position(3292, 3171),
+ "ardougne" to Position(2662, 3304),
+ "barrows" to Position(3565, 3314),
+ "brimhaven" to Position(2802, 3179),
+ "burthorpe" to Position(2898, 3545),
+ "camelot" to Position(2757, 3478),
+ "canifis" to Position(3493, 3489),
+ "cw" to Position(2442, 3090),
+ "draynor" to Position(3082, 3249),
+ "duelarena" to Position(3370, 3267),
+ "edgeville" to Position(3087, 3504),
+ "entrana" to Position(2827, 3343),
+ "falador" to Position(2965, 3379),
+ "ge" to Position(3164, 3476),
+ "kbd" to Position(2273, 4680),
+ "keldagrim" to Position(2845, 10210),
+ "kq" to Position(3507, 9494),
+ "lumbridge" to Position(3222, 3219),
+ "lunar" to Position(2113, 3915),
+ "misc" to Position(2515, 3866),
+ "neit" to Position(2332, 3804),
+ "pc" to Position(2658, 2660),
+ "rellekka" to Position(2660, 3657),
+ "shilo" to Position(2852, 2955),
+ "taverley" to Position(2895, 3443),
+ "tutorial" to Position(3094, 3107),
+ "tzhaar" to Position(2480, 5175),
+ "varrock" to Position(3212, 3423),
+ "yanille" to Position(2605, 3096),
+ "zanaris" to Position(2452, 4473)
+)
\ No newline at end of file
diff --git a/game/plugin/cmd/test/AnimateCommandTests.kt b/game/plugin/cmd/test/AnimateCommandTests.kt
new file mode 100644
index 000000000..5a49dd6e1
--- /dev/null
+++ b/game/plugin/cmd/test/AnimateCommandTests.kt
@@ -0,0 +1,39 @@
+import io.mockk.verify
+import org.apollo.game.command.Command
+import org.apollo.game.model.Animation
+import org.apollo.game.model.World
+import org.apollo.game.model.entity.Player
+import org.apollo.game.model.entity.setting.PrivilegeLevel
+import org.apollo.game.plugin.testing.assertions.contains
+import org.apollo.game.plugin.testing.junit.ApolloTestingExtension
+import org.apollo.game.plugin.testing.junit.api.annotations.TestMock
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+
+@ExtendWith(ApolloTestingExtension::class)
+class AnimateCommandTests {
+
+ @TestMock
+ lateinit var world: World
+
+ @TestMock
+ lateinit var player: Player
+
+ @Test
+ fun `Plays the animation provided as input`() {
+ player.privilegeLevel = PrivilegeLevel.MODERATOR
+ world.commandDispatcher.dispatch(player, Command("animate", arrayOf("1")))
+
+ verify { player.playAnimation(Animation(1)) }
+ }
+
+ @Test
+ fun `Help message sent on invalid syntax`() {
+ player.privilegeLevel = PrivilegeLevel.ADMINISTRATOR
+ world.commandDispatcher.dispatch(player, Command("animate", arrayOf("")))
+
+ verify {
+ player.sendMessage(contains("Invalid syntax"))
+ }
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/cmd/test/BankCommandTests.kt b/game/plugin/cmd/test/BankCommandTests.kt
new file mode 100644
index 000000000..4ef96051c
--- /dev/null
+++ b/game/plugin/cmd/test/BankCommandTests.kt
@@ -0,0 +1,27 @@
+import io.mockk.verify
+import org.apollo.game.command.Command
+import org.apollo.game.model.World
+import org.apollo.game.model.entity.Player
+import org.apollo.game.model.entity.setting.PrivilegeLevel
+import org.apollo.game.plugin.testing.junit.ApolloTestingExtension
+import org.apollo.game.plugin.testing.junit.api.annotations.TestMock
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+
+@ExtendWith(ApolloTestingExtension::class)
+class BankCommandTests {
+
+ @TestMock
+ lateinit var world: World
+
+ @TestMock
+ lateinit var player: Player
+
+ @Test
+ fun `Opens bank when used`() {
+ player.privilegeLevel = PrivilegeLevel.ADMINISTRATOR
+ world.commandDispatcher.dispatch(player, Command("bank", emptyArray()))
+
+ verify { player.openBank() }
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/cmd/test/BroadcastCommandTests.kt b/game/plugin/cmd/test/BroadcastCommandTests.kt
new file mode 100644
index 000000000..d3ba8b16f
--- /dev/null
+++ b/game/plugin/cmd/test/BroadcastCommandTests.kt
@@ -0,0 +1,29 @@
+import io.mockk.verify
+import org.apollo.game.command.Command
+import org.apollo.game.model.World
+import org.apollo.game.model.entity.Player
+import org.apollo.game.model.entity.setting.PrivilegeLevel
+import org.apollo.game.plugin.testing.junit.ApolloTestingExtension
+import org.apollo.game.plugin.testing.junit.api.annotations.TestMock
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+
+@ExtendWith(ApolloTestingExtension::class)
+class BroadcastCommandTests {
+
+ @TestMock
+ lateinit var world: World
+
+ @TestMock
+ lateinit var player: Player
+
+ @Test
+ fun `Shows basic information on an item`() {
+ player.privilegeLevel = PrivilegeLevel.ADMINISTRATOR
+ world.commandDispatcher.dispatch(player, Command("broadcast", arrayOf("msg")))
+
+ verify {
+ player.sendMessage("[Broadcast] Test: msg")
+ }
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/cmd/test/ItemCommandTests.kt b/game/plugin/cmd/test/ItemCommandTests.kt
new file mode 100644
index 000000000..c9884105a
--- /dev/null
+++ b/game/plugin/cmd/test/ItemCommandTests.kt
@@ -0,0 +1,57 @@
+import io.mockk.verify
+import org.apollo.cache.def.ItemDefinition
+import org.apollo.game.command.Command
+import org.apollo.game.model.World
+import org.apollo.game.model.entity.Player
+import org.apollo.game.model.entity.setting.PrivilegeLevel
+import org.apollo.game.plugin.testing.assertions.contains
+import org.apollo.game.plugin.testing.junit.ApolloTestingExtension
+import org.apollo.game.plugin.testing.junit.api.annotations.ItemDefinitions
+import org.apollo.game.plugin.testing.junit.api.annotations.TestMock
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.ValueSource
+
+@ExtendWith(ApolloTestingExtension::class)
+class ItemCommandTests {
+
+ @TestMock
+ lateinit var world: World
+
+ @TestMock
+ lateinit var player: Player
+
+ @Test
+ fun `Defaults to an amount of 1`() {
+ player.privilegeLevel = PrivilegeLevel.ADMINISTRATOR
+ world.commandDispatcher.dispatch(player, Command("item", arrayOf("1")))
+
+ assertEquals(1, player.inventory.getAmount(1))
+ }
+
+ @Test
+ fun `Adds item of specified amount to inventory`() {
+ player.privilegeLevel = PrivilegeLevel.ADMINISTRATOR
+ world.commandDispatcher.dispatch(player, Command("item", arrayOf("1", "10")))
+
+ assertEquals(10, player.inventory.getAmount(1))
+ }
+
+ @ParameterizedTest(name = "::item {0}")
+ @ValueSource(strings = ["", "1 ", " 1"])
+ fun `Help message sent on invalid syntax`(args: String) {
+ player.privilegeLevel = PrivilegeLevel.ADMINISTRATOR
+ world.commandDispatcher.dispatch(player, Command("item", args.split(" ").toTypedArray()))
+
+ verify {
+ player.sendMessage(contains("Invalid syntax"))
+ }
+ }
+
+ companion object {
+ @ItemDefinitions
+ val items = listOf(ItemDefinition(1))
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/cmd/test/LookupCommandTests.kt b/game/plugin/cmd/test/LookupCommandTests.kt
new file mode 100644
index 000000000..ada7ebeeb
--- /dev/null
+++ b/game/plugin/cmd/test/LookupCommandTests.kt
@@ -0,0 +1,96 @@
+import io.mockk.verify
+import org.apollo.cache.def.ItemDefinition
+import org.apollo.cache.def.NpcDefinition
+import org.apollo.cache.def.ObjectDefinition
+import org.apollo.game.command.Command
+import org.apollo.game.model.World
+import org.apollo.game.model.entity.Player
+import org.apollo.game.model.entity.setting.PrivilegeLevel
+import org.apollo.game.plugin.testing.assertions.contains
+import org.apollo.game.plugin.testing.junit.ApolloTestingExtension
+import org.apollo.game.plugin.testing.junit.api.annotations.ItemDefinitions
+import org.apollo.game.plugin.testing.junit.api.annotations.NpcDefinitions
+import org.apollo.game.plugin.testing.junit.api.annotations.ObjectDefinitions
+import org.apollo.game.plugin.testing.junit.api.annotations.TestMock
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.ValueSource
+
+@ExtendWith(ApolloTestingExtension::class)
+class LookupCommandTests {
+
+ @TestMock
+ lateinit var world: World
+
+ @TestMock
+ lateinit var player: Player
+
+ @Test
+ fun `Shows basic information on an item`() {
+ player.privilegeLevel = PrivilegeLevel.ADMINISTRATOR
+ world.commandDispatcher.dispatch(player, Command("iteminfo", arrayOf("1")))
+
+ verify {
+ player.sendMessage("Item 1 is called and is not members only.")
+ player.sendMessage("Its description is ``.")
+ }
+ }
+
+ @Test
+ fun `Shows basic information on an npc`() {
+ player.privilegeLevel = PrivilegeLevel.ADMINISTRATOR
+ world.commandDispatcher.dispatch(player, Command("npcinfo", arrayOf("1")))
+
+ verify {
+ player.sendMessage("Npc 1 is called and has a combat level of 126.")
+ player.sendMessage("Its description is ``.")
+ }
+ }
+
+ @Test
+ fun `Shows basic information on an object`() {
+ player.privilegeLevel = PrivilegeLevel.ADMINISTRATOR
+ world.commandDispatcher.dispatch(player, Command("objectinfo", arrayOf("1")))
+
+ verify {
+ player.sendMessage("Object 1 is called (width=1, length=1).")
+ player.sendMessage("Its description is ``.")
+ }
+ }
+
+ @ParameterizedTest(name = "::{0} ")
+ @ValueSource(strings = ["npcinfo", "iteminfo", "objectinfo"])
+ fun `Help message sent on invalid syntax`(command: String) {
+ player.privilegeLevel = PrivilegeLevel.ADMINISTRATOR
+ world.commandDispatcher.dispatch(player, Command(command, arrayOf("")))
+
+ verify {
+ player.sendMessage(contains("Invalid syntax"))
+ }
+ }
+
+ companion object {
+ @ItemDefinitions
+ val items = listOf(ItemDefinition(1).apply {
+ name = ""
+ description = ""
+ isMembersOnly = false
+ })
+
+ @NpcDefinitions
+ val npcs = listOf(NpcDefinition(1).apply {
+ name = ""
+ combatLevel = 126
+ description = ""
+ })
+
+ @ObjectDefinitions
+ val objects = listOf(ObjectDefinition(1).apply {
+ name = ""
+ description = ""
+ width = 1
+ length = 1
+ })
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/cmd/test/PunishCommandTests.kt b/game/plugin/cmd/test/PunishCommandTests.kt
new file mode 100644
index 000000000..6f3c48801
--- /dev/null
+++ b/game/plugin/cmd/test/PunishCommandTests.kt
@@ -0,0 +1,97 @@
+import io.mockk.verify
+import org.apollo.game.command.Command
+import org.apollo.game.model.World
+import org.apollo.game.model.entity.Player
+import org.apollo.game.model.entity.setting.PrivilegeLevel
+import org.apollo.game.plugin.testing.junit.ApolloTestingExtension
+import org.apollo.game.plugin.testing.junit.api.annotations.Name
+import org.apollo.game.plugin.testing.junit.api.annotations.TestMock
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+
+@ExtendWith(ApolloTestingExtension::class)
+class PunishCommandTests {
+
+ @TestMock
+ lateinit var world: World
+
+ @TestMock
+ lateinit var admin: Player
+
+ @TestMock
+ @Name("player_two")
+ lateinit var player: Player
+
+ @BeforeEach
+ fun setup() {
+ admin.privilegeLevel = PrivilegeLevel.ADMINISTRATOR
+ }
+
+ @Test
+ fun `Staff can mute players`() {
+ world.commandDispatcher.dispatch(admin, Command("mute", arrayOf("player_two")))
+
+ verify {
+ player.setMuted(true)
+ }
+ }
+
+ @Test
+ fun `Staff can unmute players`() {
+ player.isMuted = true
+ world.commandDispatcher.dispatch(admin, Command("unmute", arrayOf("player_two")))
+
+ verify {
+ player.setMuted(false)
+ }
+ }
+
+ @Test
+ fun `Staff cant mute admins`() {
+ player.privilegeLevel = PrivilegeLevel.ADMINISTRATOR
+ world.commandDispatcher.dispatch(admin, Command("unmute", arrayOf("player_two")))
+
+ verify {
+ admin.sendMessage("You cannot perform this action on Administrators.")
+ }
+ }
+
+ @Test
+ fun `Cant mute players that arent online`() {
+ world.commandDispatcher.dispatch(admin, Command("mute", arrayOf("player555")))
+
+ verify {
+ admin.sendMessage("That player does not exist.")
+ }
+ }
+
+ @Test
+ fun `Staff can ban players`() {
+ world.commandDispatcher.dispatch(admin, Command("ban", arrayOf("player_two")))
+
+ verify {
+ player.ban()
+ player.logout()
+ }
+ }
+
+ @Test
+ fun `Staff cant ban admins`() {
+ player.privilegeLevel = PrivilegeLevel.ADMINISTRATOR
+ world.commandDispatcher.dispatch(admin, Command("ban", arrayOf("player_two")))
+
+ verify {
+ admin.sendMessage("You cannot perform this action on Administrators.")
+ }
+ }
+
+ @Test
+ fun `Cant ban players that arent online`() {
+ world.commandDispatcher.dispatch(admin, Command("ban", arrayOf("player555")))
+
+ verify {
+ admin.sendMessage("That player does not exist.")
+ }
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/cmd/test/SkillCommandTests.kt b/game/plugin/cmd/test/SkillCommandTests.kt
new file mode 100644
index 000000000..4a243cb73
--- /dev/null
+++ b/game/plugin/cmd/test/SkillCommandTests.kt
@@ -0,0 +1,75 @@
+import io.mockk.verify
+import org.apollo.game.command.Command
+import org.apollo.game.model.World
+import org.apollo.game.model.entity.Player
+import org.apollo.game.model.entity.Skill
+import org.apollo.game.model.entity.setting.PrivilegeLevel
+import org.apollo.game.plugin.testing.assertions.contains
+import org.apollo.game.plugin.testing.junit.ApolloTestingExtension
+import org.apollo.game.plugin.testing.junit.api.annotations.TestMock
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.ValueSource
+
+@ExtendWith(ApolloTestingExtension::class)
+class SkillCommandTests {
+
+ @TestMock
+ lateinit var world: World
+
+ @TestMock
+ lateinit var player: Player
+
+ @BeforeEach
+ fun setup() {
+ player.privilegeLevel = PrivilegeLevel.ADMINISTRATOR
+ }
+
+ @Test
+ fun `Max stats to 99`() {
+ world.commandDispatcher.dispatch(player, Command("max", emptyArray()))
+
+ for (stat in 0 until Skill.RUNECRAFT) {
+ assertEquals(99, player.skillSet.getCurrentLevel(stat))
+ }
+ }
+
+ @Test
+ fun `Set skill to given level`() {
+ world.commandDispatcher.dispatch(player, Command("level", arrayOf("1", "99")))
+
+ assertEquals(99, player.skillSet.getCurrentLevel(1))
+ }
+
+ @Test
+ fun `Set skill to given experience`() {
+ world.commandDispatcher.dispatch(player, Command("xp", arrayOf("1", "50")))
+
+ assertEquals(50.0, player.skillSet.getExperience(1))
+ }
+
+ @ParameterizedTest(name = "::{0}")
+ @ValueSource(strings = [
+ "level 50 100",
+ "level 15 100",
+ "level 100",
+ "level 15 ",
+ "level",
+ "xp",
+ "xp 50 ",
+ "xp 50"
+ ])
+ fun `Help message shown on invalid syntax`(command: String) {
+ val args = command.split(" ").toMutableList()
+ val cmd = args.removeAt(0)
+
+ world.commandDispatcher.dispatch(player, Command(cmd, args.toTypedArray()))
+
+ verify {
+ player.sendMessage(contains("Invalid syntax"))
+ }
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/cmd/test/SpawnCommandTests.kt b/game/plugin/cmd/test/SpawnCommandTests.kt
new file mode 100644
index 000000000..8dfde978e
--- /dev/null
+++ b/game/plugin/cmd/test/SpawnCommandTests.kt
@@ -0,0 +1,76 @@
+import io.mockk.verify
+import org.apollo.cache.def.NpcDefinition
+import org.apollo.game.command.Command
+import org.apollo.game.model.Position
+import org.apollo.game.model.World
+import org.apollo.game.model.entity.Npc
+import org.apollo.game.model.entity.Player
+import org.apollo.game.model.entity.setting.PrivilegeLevel
+import org.apollo.game.plugin.testing.assertions.contains
+import org.apollo.game.plugin.testing.junit.ApolloTestingExtension
+import org.apollo.game.plugin.testing.junit.api.annotations.NpcDefinitions
+import org.apollo.game.plugin.testing.junit.api.annotations.TestMock
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.ValueSource
+
+@ExtendWith(ApolloTestingExtension::class)
+class SpawnCommandTests {
+
+ @TestMock
+ lateinit var world: World
+
+ @TestMock
+ lateinit var player: Player
+
+ @BeforeEach
+ fun setup() {
+ player.privilegeLevel = PrivilegeLevel.ADMINISTRATOR
+ }
+
+ @Test
+ fun `Spawns NPC at players position by default`() {
+ player.position = Position(3, 3, 0)
+ world.commandDispatcher.dispatch(player, Command("spawn", arrayOf("1")))
+
+ verify {
+ world.register(match {
+ it.id == 1 && it.position == Position(3, 3, 0)
+ })
+ }
+ }
+
+ @Test
+ fun `Spawn NPC at given position`() {
+ world.commandDispatcher.dispatch(player, Command("spawn", arrayOf("1", "5", "5")))
+
+ verify {
+ world.register(match {
+ it.id == 1 && it.position == Position(5, 5)
+ })
+ }
+ }
+
+ @ParameterizedTest(name = "::spawn {0}")
+ @ValueSource(strings = [
+ "",
+ "1 2",
+ "1 2 ",
+ "1 2 3 "
+ ])
+ fun `Help message on invalid syntax`(args: String) {
+ player.privilegeLevel = PrivilegeLevel.ADMINISTRATOR
+ world.commandDispatcher.dispatch(player, Command("spawn", args.split(" ").toTypedArray()))
+
+ verify {
+ player.sendMessage(contains("Invalid syntax"))
+ }
+ }
+
+ companion object {
+ @NpcDefinitions
+ val npcs = listOf(NpcDefinition(1))
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/cmd/test/TeleportCommandTests.kt b/game/plugin/cmd/test/TeleportCommandTests.kt
new file mode 100644
index 000000000..2fffdf342
--- /dev/null
+++ b/game/plugin/cmd/test/TeleportCommandTests.kt
@@ -0,0 +1,94 @@
+import io.mockk.verify
+import org.apollo.game.command.Command
+import org.apollo.game.model.Position
+import org.apollo.game.model.World
+import org.apollo.game.model.entity.Player
+import org.apollo.game.model.entity.setting.PrivilegeLevel
+import org.apollo.game.plugin.testing.assertions.contains
+import org.apollo.game.plugin.testing.junit.ApolloTestingExtension
+import org.apollo.game.plugin.testing.junit.api.annotations.Name
+import org.apollo.game.plugin.testing.junit.api.annotations.TestMock
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.ValueSource
+
+@ExtendWith(ApolloTestingExtension::class)
+class TeleportCommandTests {
+
+ @TestMock
+ lateinit var world: World
+
+ @TestMock
+ lateinit var player: Player
+
+ @TestMock
+ @Name("player_two")
+ lateinit var player_two: Player
+
+ @Test
+ fun `Teleport to given coordinates`() {
+ player.privilegeLevel = PrivilegeLevel.ADMINISTRATOR
+ world.commandDispatcher.dispatch(player, Command("tele", arrayOf("1", "2", "0")))
+
+ assertEquals(Position(1, 2, 0), player.position)
+ }
+
+ @Test
+ fun `Teleport to given coordinates on players plane when no plane given`() {
+ player.privilegeLevel = PrivilegeLevel.ADMINISTRATOR
+ player.position = Position(1, 1, 1)
+ world.commandDispatcher.dispatch(player, Command("tele", arrayOf("1", "2")))
+
+ assertEquals(Position(1, 2, 1), player.position)
+ }
+
+ @Test
+ fun `Shows current position information`() {
+ player.privilegeLevel = PrivilegeLevel.ADMINISTRATOR
+ player.position = Position(1, 2, 3)
+ world.commandDispatcher.dispatch(player, Command("pos", emptyArray()))
+
+ verify {
+ player.sendMessage(contains("1, 2, 3"))
+ }
+ }
+
+ @Test
+ fun `Shows another players current position information`() {
+ player.privilegeLevel = PrivilegeLevel.ADMINISTRATOR
+ player_two.position = Position(1, 2, 3)
+ world.commandDispatcher.dispatch(player, Command("pos", arrayOf("player_two")))
+
+ verify {
+ player.sendMessage(contains("1, 2, 3"))
+ }
+ }
+
+ @Test
+ fun `Shows no position information for a nonexistent player`() {
+ player.privilegeLevel = PrivilegeLevel.ADMINISTRATOR
+ world.commandDispatcher.dispatch(player, Command("pos", arrayOf("player999")))
+
+ verify {
+ player.sendMessage(contains("offline"))
+ }
+ }
+
+ @ParameterizedTest(name = "::tele {0}")
+ @ValueSource(strings = [
+ "1 2 ",
+ "1 2",
+ "1",
+ "1 2 3 4"
+ ])
+ fun `Help message sent on invalid syntax`(args: String) {
+ player.privilegeLevel = PrivilegeLevel.ADMINISTRATOR
+ world.commandDispatcher.dispatch(player, Command("tele", args.split(" ").toTypedArray()))
+
+ verify {
+ player.sendMessage(contains("Invalid syntax"))
+ }
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/cmd/test/TeleportToPlayerCommandTests.kt b/game/plugin/cmd/test/TeleportToPlayerCommandTests.kt
new file mode 100644
index 000000000..d70c8e4d3
--- /dev/null
+++ b/game/plugin/cmd/test/TeleportToPlayerCommandTests.kt
@@ -0,0 +1,52 @@
+import io.mockk.verify
+import org.apollo.game.command.Command
+import org.apollo.game.model.Position
+import org.apollo.game.model.World
+import org.apollo.game.model.entity.Player
+import org.apollo.game.model.entity.setting.PrivilegeLevel
+import org.apollo.game.plugin.testing.assertions.contains
+import org.apollo.game.plugin.testing.junit.ApolloTestingExtension
+import org.apollo.game.plugin.testing.junit.api.annotations.Name
+import org.apollo.game.plugin.testing.junit.api.annotations.TestMock
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.ValueSource
+
+@ExtendWith(ApolloTestingExtension::class)
+class TeleportToPlayerCommandTests {
+
+ @TestMock
+ lateinit var world: World
+
+ @TestMock
+ lateinit var player: Player
+
+ @TestMock
+ @Name("player_two")
+ lateinit var secondPlayer: Player
+
+ @Test
+ fun `Teleport to given player`() {
+ player.privilegeLevel = PrivilegeLevel.ADMINISTRATOR
+ player.position = Position(300, 300)
+ secondPlayer.position = Position(1, 2)
+ world.commandDispatcher.dispatch(player, Command("teleto", arrayOf("player_two")))
+
+ assertEquals(secondPlayer.position, player.position)
+ }
+
+ @ParameterizedTest(name = "::teleto {0}")
+ @ValueSource(strings = [
+ ""
+ ])
+ fun `Help message sent on invalid syntax`(args: String) {
+ player.privilegeLevel = PrivilegeLevel.ADMINISTRATOR
+ world.commandDispatcher.dispatch(player, Command("teleto", args.split(" ").toTypedArray()))
+
+ verify {
+ player.sendMessage(contains("Invalid syntax"))
+ }
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/consumables/build.gradle b/game/plugin/consumables/build.gradle
new file mode 100644
index 000000000..3a9351392
--- /dev/null
+++ b/game/plugin/consumables/build.gradle
@@ -0,0 +1,10 @@
+apply plugin: 'kotlin'
+
+dependencies {
+ implementation project(':game')
+ implementation project(':cache')
+ implementation project(':net')
+ implementation project(':util')
+
+ testImplementation project(':game:plugin-testing')
+}
diff --git a/game/plugin/consumables/src/consumables.kt b/game/plugin/consumables/src/consumables.kt
new file mode 100644
index 000000000..f8bc5ef0e
--- /dev/null
+++ b/game/plugin/consumables/src/consumables.kt
@@ -0,0 +1,80 @@
+package org.apollo.plugin.consumables
+
+import org.apollo.game.model.entity.Player
+import org.apollo.game.model.entity.Skill
+
+/**
+ * An item that can be consumed to restore or buff stats.
+ */
+abstract class Consumable(val name: String, val id: Int, val sound: Int, val delay: Int, val replacement: Int?) {
+
+ abstract fun addEffect(player: Player)
+
+ fun consume(player: Player, slot: Int) {
+ addEffect(player)
+ player.inventory.reset(slot)
+
+ if (replacement != null) {
+ player.inventory.add(replacement)
+ }
+ }
+}
+
+private val consumables = mutableMapOf()
+
+fun isConsumable(itemId: Int) = consumables.containsKey(itemId)
+fun lookupConsumable(itemId: Int): Consumable = consumables.get(itemId)!!
+fun consumable(consumable: Consumable) = consumables.put(consumable.id, consumable)
+
+enum class FoodOrDrinkType(val action: String) {
+ FOOD("eat"), DRINK("drink")
+}
+
+class FoodOrDrink : Consumable {
+
+ companion object {
+ const val EAT_FOOD_SOUND = 317
+ }
+
+ val restoration: Int
+ val type: FoodOrDrinkType
+
+ constructor(
+ name: String,
+ id: Int,
+ delay: Int,
+ type: FoodOrDrinkType,
+ restoration: Int,
+ replacement: Int? = null
+ ) : super(name, id, EAT_FOOD_SOUND, delay, replacement) {
+ this.type = type
+ this.restoration = restoration
+ }
+
+ override fun addEffect(player: Player) {
+ val hitpoints = player.skillSet.getSkill(Skill.HITPOINTS)
+ val hitpointsLevel = hitpoints.currentLevel
+ val newHitpointsLevel = Math.min(hitpointsLevel + restoration, hitpoints.maximumLevel)
+
+ player.sendMessage("You ${type.action} the $name.")
+ if (newHitpointsLevel > hitpointsLevel) {
+ player.sendMessage("It heals some health.")
+ }
+
+ player.skillSet.setCurrentLevel(Skill.HITPOINTS, newHitpointsLevel)
+ }
+}
+
+/**
+ * Define a new type of [Consumable] food.
+ */
+fun food(name: String, id: Int, restoration: Int, replacement: Int? = null, delay: Int = 3) {
+ consumable(FoodOrDrink(name, id, delay, FoodOrDrinkType.FOOD, restoration, replacement))
+}
+
+/**
+ * Define a new type of [Consumable] drink.
+ */
+fun drink(name: String, id: Int, restoration: Int, replacement: Int? = null, delay: Int = 3) {
+ consumable(FoodOrDrink(name, id, delay, FoodOrDrinkType.DRINK, restoration, replacement))
+}
\ No newline at end of file
diff --git a/game/plugin/consumables/src/consumables.plugin.kts b/game/plugin/consumables/src/consumables.plugin.kts
new file mode 100644
index 000000000..1711d43eb
--- /dev/null
+++ b/game/plugin/consumables/src/consumables.plugin.kts
@@ -0,0 +1,37 @@
+import org.apollo.game.action.ActionBlock
+import org.apollo.game.action.AsyncAction
+import org.apollo.game.message.impl.ItemOptionMessage
+import org.apollo.game.model.Animation
+import org.apollo.game.model.entity.Player
+import org.apollo.net.message.Message
+import org.apollo.plugin.consumables.*
+
+on { ItemOptionMessage::class }
+ .where { option == 1 && isConsumable(id) }
+ .then {
+ ConsumeAction.start(this, it, lookupConsumable(id), slot)
+ }
+
+class ConsumeAction(val consumable: Consumable, player: Player, val slot: Int) :
+ AsyncAction(CONSUME_STARTUP_DELAY, true, player) {
+
+ companion object {
+ const val CONSUME_ANIMATION_ID = 829
+ const val CONSUME_STARTUP_DELAY = 2
+
+ /**
+ * Starts a [ConsumeAction] for the specified [Player], terminating the [Message] that triggered it.
+ */
+ fun start(message: Message, player: Player, consumable: Consumable, slot: Int) {
+ player.startAction(ConsumeAction(consumable, player, slot))
+ message.terminate()
+ }
+ }
+
+ override fun action(): ActionBlock = {
+ consumable.consume(mob, slot)
+ mob.playAnimation(Animation(CONSUME_ANIMATION_ID))
+ wait(consumable.delay)
+ stop()
+ }
+}
diff --git a/game/plugin/consumables/src/drinks.plugin.kts b/game/plugin/consumables/src/drinks.plugin.kts
new file mode 100644
index 000000000..a96a8a3f8
--- /dev/null
+++ b/game/plugin/consumables/src/drinks.plugin.kts
@@ -0,0 +1,24 @@
+import org.apollo.plugin.consumables.drink
+
+// # Wine
+drink(name = "jug_of_wine", id = 1993, restoration = 11)
+
+// # Hot Drinks
+drink(name = "nettle_tea", id = 4239, restoration = 3)
+drink(name = "nettle_tea", id = 4240, restoration = 3)
+
+// # Gnome Cocktails
+drink(name = "fruit_blast", id = 2034, restoration = 9)
+drink(name = "fruit_blast", id = 2084, restoration = 9)
+drink(name = "pineapple_punch", id = 2036, restoration = 9)
+drink(name = "pineapple_punch", id = 2048, restoration = 9)
+drink(name = "wizard_blizzard", id = 2040, restoration = 5) // -4 attack, +5 strength also
+drink(name = "wizard_blizzard", id = 2054, restoration = 5) // -4 attack, +5 strength also
+drink(name = "short_green_guy", id = 2038, restoration = 5) // -4 attack, +5 strength also
+drink(name = "short_green_guy", id = 2080, restoration = 5) // -4 attack, +5 strength also
+drink(name = "drunk_dragon", id = 2032, restoration = 5) // -4 attack, +6 strength also
+drink(name = "drunk_dragon", id = 2092, restoration = 5) // -4 attack, +6 strength also
+drink(name = "chocolate_saturday", id = 2030, restoration = 7) // -4 attack, +6 strength also
+drink(name = "chocolate_saturday", id = 2074, restoration = 7) // -4 attack, +6 strength also
+drink(name = "blurberry_special", id = 2028, restoration = 7) // -4 attack, +6 strength also
+drink(name = "blurberry_special", id = 2064, restoration = 7) // -4 attack, +6 strength also
diff --git a/game/plugin/consumables/src/foods.plugin.kts b/game/plugin/consumables/src/foods.plugin.kts
new file mode 100644
index 000000000..029818900
--- /dev/null
+++ b/game/plugin/consumables/src/foods.plugin.kts
@@ -0,0 +1,159 @@
+import org.apollo.plugin.consumables.food
+
+food(name = "anchovies", id = 319, restoration = 1)
+food(name = "crab_meat", id = 7521, restoration = 2, replacement = 7523)
+food(name = "crab_meat", id = 7523, restoration = 2, replacement = 7524)
+food(name = "crab_meat", id = 7524, restoration = 2, replacement = 7525)
+food(name = "crab_meat", id = 7525, restoration = 2, replacement = 7526)
+food(name = "crab_meat", id = 7526, restoration = 2)
+food(name = "shrimp", id = 315, restoration = 3)
+food(name = "sardine", id = 325, restoration = 3)
+food(name = "cooked_meat", id = 2142, restoration = 3)
+food(name = "cooked_chicken", id = 2140, restoration = 3)
+food(name = "ugthanki_meat", id = 1861, restoration = 3)
+food(name = "karambwanji", id = 3151, restoration = 3)
+food(name = "cooked_rabbit", id = 3228, restoration = 5)
+food(name = "herring", id = 347, restoration = 6)
+food(name = "trout", id = 333, restoration = 7)
+food(name = "cod", id = 339, restoration = 7)
+food(name = "mackeral", id = 355, restoration = 7)
+food(name = "roast_rabbit", id = 7223, restoration = 7)
+food(name = "pike", id = 351, restoration = 8)
+food(name = "lean_snail_meat", id = 3371, restoration = 8)
+food(name = "salmon", id = 329, restoration = 9)
+food(name = "tuna", id = 361, restoration = 10)
+food(name = "lobster", id = 379, restoration = 12)
+food(name = "bass", id = 365, restoration = 13)
+food(name = "swordfish", id = 373, restoration = 14)
+food(name = "cooked_jubbly", id = 7568, restoration = 15)
+food(name = "monkfish", id = 7946, restoration = 16)
+food(name = "cooked_karambwan", id = 3144, restoration = 18, delay = 0)
+food(name = "shark", id = 385, restoration = 20)
+food(name = "sea_turtle", id = 397, restoration = 21)
+food(name = "manta_ray", id = 391, restoration = 22)
+
+// # Breads/Wraps
+food(name = "bread", id = 2309, restoration = 5)
+food(name = "oomlie_wrap", id = 2343, restoration = 14)
+food(name = "ugthanki_kebab", id = 1883, restoration = 19)
+
+// # Fruits
+food(name = "banana", id = 1963, restoration = 2)
+food(name = "sliced_banana", id = 3162, restoration = 2)
+food(name = "lemon", id = 2102, restoration = 2)
+food(name = "lemon_chunks", id = 2104, restoration = 2)
+food(name = "lemon_slices", id = 2106, restoration = 2)
+food(name = "lime", id = 2120, restoration = 2)
+food(name = "lime_chunks", id = 2122, restoration = 2)
+food(name = "lime_slices", id = 2124, restoration = 2)
+food(name = "strawberry", id = 5504, restoration = 5)
+food(name = "papaya_fruit", id = 5972, restoration = 8)
+food(name = "pineapple_chunks", id = 2116, restoration = 2)
+food(name = "pineapple_ring", id = 2118, restoration = 2)
+food(name = "orange", id = 2108, restoration = 2)
+food(name = "orange_rings", id = 2110, restoration = 2)
+food(name = "orange_slices", id = 2112, restoration = 2)
+
+// # Pies
+// # TODO: pie special effects (e.g. fish pie raises fishing level)
+food(name = "redberry_pie", id = 2325, restoration = 5, replacement = 2333, delay = 1)
+food(name = "redberry_pie", id = 2333, restoration = 5, delay = 1)
+
+food(name = "meat_pie", id = 2327, restoration = 6, replacement = 2331, delay = 1)
+food(name = "meat_pie", id = 2331, restoration = 6, delay = 1)
+
+food(name = "apple_pie", id = 2323, restoration = 7, replacement = 2335, delay = 1)
+food(name = "apple_pie", id = 2335, restoration = 7, delay = 1)
+
+food(name = "fish_pie", id = 7188, restoration = 6, replacement = 7190, delay = 1)
+food(name = "fish_pie", id = 7190, restoration = 6, delay = 1)
+
+food(name = "admiral_pie", id = 7198, restoration = 8, replacement = 7200, delay = 1)
+food(name = "admiral_pie", id = 7200, restoration = 8, delay = 1)
+
+food(name = "wild_pie", id = 7208, restoration = 11, replacement = 7210, delay = 1)
+food(name = "wild_pie", id = 7210, restoration = 11, delay = 1)
+
+food(name = "summer_pie", id = 7218, restoration = 11, replacement = 7220, delay = 1)
+food(name = "summer_pie", id = 7220, restoration = 11, delay = 1)
+
+// # Stews
+food(name = "stew", id = 2003, restoration = 11)
+food(name = "banana_stew", id = 4016, restoration = 11)
+food(name = "curry", id = 2011, restoration = 19)
+
+// # Pizzas
+food(name = "plain_pizza", id = 2289, restoration = 7, replacement = 2291)
+food(name = "plain_pizza", id = 2291, restoration = 7)
+
+food(name = "meat_pizza", id = 2293, restoration = 8, replacement = 2295)
+food(name = "meat_pizza", id = 2295, restoration = 8)
+
+food(name = "anchovy_pizza", id = 2297, restoration = 9, replacement = 2299)
+food(name = "anchovy_pizza", id = 2299, restoration = 9)
+
+food(name = "pineapple_pizza", id = 2301, restoration = 11, replacement = 2303)
+food(name = "pineapple_pizza", id = 2303, restoration = 11)
+
+// # Cakes
+food(name = "fishcake", id = 7530, restoration = 11)
+
+food(name = "cake", id = 1891, restoration = 4, replacement = 1893)
+food(name = "cake", id = 1893, restoration = 4, replacement = 1895)
+food(name = "cake", id = 1895, restoration = 4)
+
+food(name = "chocolate_cake", id = 1897, restoration = 5, replacement = 1899)
+food(name = "chocolate_cake", id = 1899, restoration = 5, replacement = 1901)
+food(name = "chocolate_cake", id = 1901, restoration = 5)
+
+// # Vegetables
+food(name = "potato", id = 1942, restoration = 1)
+food(name = "spinach_roll", id = 1969, restoration = 2)
+food(name = "baked_potato", id = 6701, restoration = 4)
+food(name = "sweetcorn", id = 5988, restoration = 10)
+food(name = "sweetcorn_bowl", id = 7088, restoration = 13)
+food(name = "potato_with_butter", id = 6703, restoration = 14)
+food(name = "chili_potato", id = 7054, restoration = 14)
+food(name = "potato_with_cheese", id = 6705, restoration = 16)
+food(name = "egg_potato", id = 7056, restoration = 16)
+food(name = "mushroom_potato", id = 7058, restoration = 20)
+food(name = "tuna_potato", id = 7060, restoration = 22)
+
+// # Dairy
+food(name = "cheese", id = 1985, restoration = 2)
+food(name = "pot_of_cream", id = 2130, restoration = 1)
+
+// # Gnome Food
+food(name = "toads_legs", id = 2152, restoration = 3)
+
+// # Gnome Bowls
+food(name = "worm_hole", id = 2191, restoration = 12)
+food(name = "worm_hole", id = 2233, restoration = 12)
+food(name = "vegetable_ball", id = 2195, restoration = 12)
+food(name = "vegetable_ball", id = 2235, restoration = 12)
+food(name = "tangled_toads_legs", id = 2187, restoration = 15)
+food(name = "tangled_toads_legs", id = 2231, restoration = 15)
+food(name = "chocolate_bomb", id = 2185, restoration = 15)
+food(name = "chocolate_bomb", id = 2229, restoration = 15)
+
+// # Gnome Crunchies
+food(name = "toad_crunchies", id = 2217, restoration = 7)
+food(name = "toad_crunchies", id = 2243, restoration = 7)
+food(name = "spicy_crunchies", id = 2213, restoration = 7)
+food(name = "spicy_crunchies", id = 2241, restoration = 7)
+food(name = "worm_crunchies", id = 2205, restoration = 8)
+food(name = "worm_crunchies", id = 2237, restoration = 8)
+food(name = "chocchip_crunchies", id = 2209, restoration = 7)
+food(name = "chocchip_crunchies", id = 2239, restoration = 7)
+
+// # Gnome Battas
+food(name = "fruit_batta", id = 2225, restoration = 11)
+food(name = "fruit_batta", id = 2277, restoration = 11)
+food(name = "toad_batta", id = 2221, restoration = 11)
+food(name = "toad_batta", id = 2255, restoration = 11)
+food(name = "worm_batta", id = 2219, restoration = 11)
+food(name = "worm_batta", id = 2253, restoration = 11)
+food(name = "vegetable_batta", id = 2227, restoration = 11)
+food(name = "vegetable_batta", id = 2281, restoration = 11)
+food(name = "cheese_tom_batta", id = 2223, restoration = 11)
+food(name = "cheese_tom_batta", id = 2259, restoration = 11)
diff --git a/game/plugin/consumables/test/FoodOrDrinkTests.kt b/game/plugin/consumables/test/FoodOrDrinkTests.kt
new file mode 100644
index 000000000..59aad8da8
--- /dev/null
+++ b/game/plugin/consumables/test/FoodOrDrinkTests.kt
@@ -0,0 +1,93 @@
+package org.apollo.plugin.consumables
+
+import io.mockk.verify
+import org.apollo.game.model.entity.Player
+import org.apollo.game.model.entity.Skill
+import org.apollo.game.plugin.testing.assertions.after
+import org.apollo.game.plugin.testing.assertions.contains
+import org.apollo.game.plugin.testing.assertions.verifyAfter
+import org.apollo.game.plugin.testing.junit.ApolloTestingExtension
+import org.apollo.game.plugin.testing.junit.api.ActionCapture
+import org.apollo.game.plugin.testing.junit.api.annotations.TestMock
+import org.apollo.game.plugin.testing.junit.api.interactions.interactWithItem
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.TestInstance
+import org.junit.jupiter.api.extension.ExtendWith
+
+@ExtendWith(ApolloTestingExtension::class)
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+class FoodOrDrinkTests {
+
+ companion object {
+ const val TEST_FOOD_NAME = "test_food"
+ const val TEST_FOOD_ID = 2000
+ const val TEST_FOOD_RESTORATION = 5
+
+ const val TEST_DRINK_NAME = "test_drink"
+ const val TEST_DRINK_ID = 2001
+ const val TEST_DRINK_RESTORATION = 5
+
+ const val HP_LEVEL = 5
+ const val MAX_HP_LEVEL = 10
+ }
+
+ @TestMock
+ lateinit var player: Player
+
+ @TestMock
+ lateinit var action: ActionCapture
+
+ @BeforeEach
+ fun setup() {
+ val skills = player.skillSet
+ skills.setCurrentLevel(Skill.HITPOINTS, HP_LEVEL)
+ skills.setMaximumLevel(Skill.HITPOINTS, MAX_HP_LEVEL)
+
+ food("test_food", TEST_FOOD_ID, TEST_FOOD_RESTORATION)
+ drink("test_drink", TEST_DRINK_ID, TEST_DRINK_RESTORATION)
+ }
+
+ @Test
+ fun `Consuming food or drink should restore the players hitpoints`() {
+ val expectedHpLevel = TEST_FOOD_RESTORATION + HP_LEVEL
+
+ player.interactWithItem(TEST_FOOD_ID, option = 1, slot = 1)
+
+ after(action.complete()) {
+ assertEquals(expectedHpLevel, player.skillSet.getCurrentLevel(Skill.HITPOINTS))
+ }
+ }
+
+ @Test
+ fun `A message should be sent notifying the player if the item restored hitpoints`() {
+ player.interactWithItem(TEST_FOOD_ID, option = 1, slot = 1)
+
+ verifyAfter(action.complete()) { player.sendMessage("It heals some health.") }
+ }
+
+ @Test
+ fun `A message should not be sent to the player if the item did not restore hitpoints`() {
+ player.skillSet.setCurrentLevel(Skill.HITPOINTS, MAX_HP_LEVEL)
+ player.interactWithItem(TEST_FOOD_ID, option = 1, slot = 1)
+
+ after(action.complete()) {
+ verify(exactly = 0) { player.sendMessage(contains("it heals some")) }
+ }
+ }
+
+ @Test
+ fun `A message should be sent saying the player has drank an item when consuming a drink`() {
+ player.interactWithItem(TEST_DRINK_ID, option = 1, slot = 1)
+
+ verifyAfter(action.complete()) { player.sendMessage("You drink the $TEST_DRINK_NAME.") }
+ }
+
+ @Test
+ fun `A message should be sent saying the player has eaten an item when consuming food`() {
+ player.interactWithItem(TEST_FOOD_ID, option = 1, slot = 1)
+
+ verifyAfter(action.complete()) { player.sendMessage("You eat the $TEST_FOOD_NAME.") }
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/dummy/build.gradle b/game/plugin/dummy/build.gradle
new file mode 100644
index 000000000..3a9351392
--- /dev/null
+++ b/game/plugin/dummy/build.gradle
@@ -0,0 +1,10 @@
+apply plugin: 'kotlin'
+
+dependencies {
+ implementation project(':game')
+ implementation project(':cache')
+ implementation project(':net')
+ implementation project(':util')
+
+ testImplementation project(':game:plugin-testing')
+}
diff --git a/game/plugin/dummy/src/dummy.plugin.kts b/game/plugin/dummy/src/dummy.plugin.kts
new file mode 100644
index 000000000..75dee6f19
--- /dev/null
+++ b/game/plugin/dummy/src/dummy.plugin.kts
@@ -0,0 +1,65 @@
+import org.apollo.game.action.ActionBlock
+import org.apollo.game.action.AsyncDistancedAction
+import org.apollo.game.message.impl.ObjectActionMessage
+import org.apollo.game.model.Animation
+import org.apollo.game.model.Position
+import org.apollo.game.model.entity.*
+import org.apollo.net.message.Message
+
+/**
+ * A list of [ObjectDefinition] identifiers which are training dummies.
+ */
+val DUMMY_IDS = setOf(823)
+
+on { ObjectActionMessage::class }
+ .where { option == 2 && id in DUMMY_IDS }
+ .then { DummyAction.start(this, it, position) }
+
+class DummyAction(val player: Player, position: Position) : AsyncDistancedAction(0, true, player, position, DISTANCE) {
+
+ companion object {
+
+ /**
+ * The maximum level a player can be before the dummy stops giving XP.
+ */
+ const val LEVEL_THRESHOLD = 8
+
+ /**
+ * The number of experience points per hit.
+ */
+ const val EXP_PER_HIT = 5.0
+
+ /**
+ * The minimum distance a player can be from the dummy.
+ */
+ const val DISTANCE = 1
+
+ /**
+ * The [Animation] played when a player hits a dummy.
+ */
+ val PUNCH_ANIMATION = Animation(422)
+
+ /**
+ * Starts a [DummyAction] for the specified [Player], terminating the [Message] that triggered it.
+ */
+ fun start(message: Message, player: Player, position: Position) {
+ player.startAction(DummyAction(player, position))
+ message.terminate()
+ }
+ }
+
+ override fun action(): ActionBlock = {
+ mob.sendMessage("You hit the dummy.")
+ mob.turnTo(position)
+ mob.playAnimation(PUNCH_ANIMATION)
+ wait()
+
+ val skills = player.skillSet
+
+ if (skills.getSkill(Skill.ATTACK).maximumLevel >= LEVEL_THRESHOLD) {
+ player.sendMessage("There is nothing more you can learn from hitting a dummy.")
+ } else {
+ skills.addExperience(Skill.ATTACK, EXP_PER_HIT)
+ }
+ }
+}
diff --git a/game/plugin/dummy/test/TrainingDummyTest.kt b/game/plugin/dummy/test/TrainingDummyTest.kt
new file mode 100644
index 000000000..ccc22be2b
--- /dev/null
+++ b/game/plugin/dummy/test/TrainingDummyTest.kt
@@ -0,0 +1,63 @@
+
+import io.mockk.verify
+import org.apollo.game.model.Position
+import org.apollo.game.model.World
+import org.apollo.game.model.entity.Player
+import org.apollo.game.model.entity.Skill
+import org.apollo.game.plugin.testing.assertions.after
+import org.apollo.game.plugin.testing.assertions.contains
+import org.apollo.game.plugin.testing.junit.ApolloTestingExtension
+import org.apollo.game.plugin.testing.junit.api.ActionCapture
+import org.apollo.game.plugin.testing.junit.api.annotations.TestMock
+import org.apollo.game.plugin.testing.junit.api.interactions.interactWith
+import org.apollo.game.plugin.testing.junit.api.interactions.spawnObject
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+
+@ExtendWith(ApolloTestingExtension::class)
+class TrainingDummyTest {
+
+ companion object {
+ const val DUMMY_ID = 823
+ val DUMMY_POSITION = Position(3200, 3230, 0)
+ }
+
+ @TestMock
+ lateinit var action: ActionCapture
+
+ @TestMock
+ lateinit var player: Player
+
+ @TestMock
+ lateinit var world: World
+
+ @Test
+ fun `Hitting the training dummy should give the player attack experience`() {
+ val dummy = world.spawnObject(DUMMY_ID, DUMMY_POSITION)
+ val skills = player.skillSet
+ val beforeExp = skills.getExperience(Skill.ATTACK)
+
+ player.interactWith(dummy, option = 2)
+
+ after(action.complete()) {
+ assertTrue(skills.getExperience(Skill.ATTACK) > beforeExp)
+ }
+ }
+
+ @Test
+ fun `The player should stop getting attack experience from the training dummy at level 8`() {
+ val dummy = world.spawnObject(DUMMY_ID, DUMMY_POSITION)
+ val skills = player.skillSet
+ skills.setMaximumLevel(Skill.ATTACK, 8)
+ val beforeExp = skills.getExperience(Skill.ATTACK)
+
+ player.interactWith(dummy, option = 2)
+
+ after(action.complete()) {
+ verify { player.sendMessage(contains("nothing more you can learn")) }
+ assertEquals(beforeExp, skills.getExperience(Skill.ATTACK))
+ }
+ }
+}
diff --git a/game/plugin/emote-tab/build.gradle b/game/plugin/emote-tab/build.gradle
new file mode 100644
index 000000000..3a9351392
--- /dev/null
+++ b/game/plugin/emote-tab/build.gradle
@@ -0,0 +1,10 @@
+apply plugin: 'kotlin'
+
+dependencies {
+ implementation project(':game')
+ implementation project(':cache')
+ implementation project(':net')
+ implementation project(':util')
+
+ testImplementation project(':game:plugin-testing')
+}
diff --git a/game/plugin/emote-tab/src/Emote.kt b/game/plugin/emote-tab/src/Emote.kt
new file mode 100644
index 000000000..2b61f88ba
--- /dev/null
+++ b/game/plugin/emote-tab/src/Emote.kt
@@ -0,0 +1,40 @@
+import org.apollo.game.model.Animation
+
+enum class Emote(val button: Int, animation: Int) {
+ ANGRY_EMOTE(button = 165, animation = 859),
+ BECKON_EMOTE(button = 167, animation = 864),
+ BLOW_KISS_EMOTE(button = 11_100, animation = 1368),
+ BOW_EMOTE(button = 164, animation = 858),
+ CHEER_EMOTE(button = 171, animation = 862),
+ CLAP_EMOTE(button = 172, animation = 865),
+ CLIMB_ROPE_EMOTE(button = 6503, animation = 1130),
+ CRY_EMOTE(button = 161, animation = 860),
+ DANCE_EMOTE(button = 166, animation = 866),
+ GLASS_BOX_EMOTE(button = 667, animation = 1131),
+ GLASS_WALL_EMOTE(button = 666, animation = 1128),
+ GOBLIN_BOW_EMOTE(button = 13_383, animation = 2127),
+ GOBLIN_DANCE_EMOTE(button = 13_384, animation = 2128),
+ HEAD_BANG_EMOTE(button = 13_365, animation = 2108),
+ JIG_EMOTE(button = 13_363, animation = 2106),
+ JOY_JUMP_EMOTE(button = 13_366, animation = 2109),
+ LAUGH_EMOTE(button = 170, animation = 861),
+ LEAN_EMOTE(button = 6_506, animation = 1129),
+ NO_EMOTE(button = 169, animation = 856),
+ PANIC_EMOTE(button = 3_362, animation = 2105),
+ RASPBERRY_EMOTE(button = 13_367, animation = 2110),
+ SALUTE_EMOTE(button = 13_369, animation = 2112),
+ SHRUG_EMOTE(button = 13_370, animation = 2113),
+ SPIN_EMOTE(button = 13_364, animation = 2107),
+ THINKING_EMOTE(button = 162, animation = 857),
+ WAVE_EMOTE(button = 163, animation = 863),
+ YAWN_EMOTE(button = 13_368, animation = 2111),
+ YES_EMOTE(button = 168, animation = 855);
+
+ val animation = Animation(animation)
+
+ companion object {
+ internal val MAP = Emote.values().associateBy { it.button }
+
+ fun fromButton(button: Int): Emote? = MAP[button]
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/emote-tab/src/EmoteTab.plugin.kts b/game/plugin/emote-tab/src/EmoteTab.plugin.kts
new file mode 100644
index 000000000..0f546e818
--- /dev/null
+++ b/game/plugin/emote-tab/src/EmoteTab.plugin.kts
@@ -0,0 +1,7 @@
+import org.apollo.game.message.impl.ButtonMessage
+
+on { ButtonMessage::class }
+ .where { widgetId in Emote.MAP }
+ .then { player ->
+ player.playAnimation(Emote.fromButton(widgetId)!!.animation)
+ }
\ No newline at end of file
diff --git a/game/plugin/entity/actions/build.gradle b/game/plugin/entity/actions/build.gradle
new file mode 100644
index 000000000..3a9351392
--- /dev/null
+++ b/game/plugin/entity/actions/build.gradle
@@ -0,0 +1,10 @@
+apply plugin: 'kotlin'
+
+dependencies {
+ implementation project(':game')
+ implementation project(':cache')
+ implementation project(':net')
+ implementation project(':util')
+
+ testImplementation project(':game:plugin-testing')
+}
diff --git a/game/plugin/entity/actions/src/PlayerActionEvent.kt b/game/plugin/entity/actions/src/PlayerActionEvent.kt
new file mode 100644
index 000000000..3cca682a5
--- /dev/null
+++ b/game/plugin/entity/actions/src/PlayerActionEvent.kt
@@ -0,0 +1,6 @@
+package org.apollo.game.plugin.entity.actions
+
+import org.apollo.game.model.entity.Player
+import org.apollo.game.model.event.PlayerEvent
+
+class PlayerActionEvent(player: Player, val target: Player, val action: PlayerActionType) : PlayerEvent(player)
\ No newline at end of file
diff --git a/game/plugin/entity/actions/src/PlayerActionType.kt b/game/plugin/entity/actions/src/PlayerActionType.kt
new file mode 100644
index 000000000..049349157
--- /dev/null
+++ b/game/plugin/entity/actions/src/PlayerActionType.kt
@@ -0,0 +1,8 @@
+package org.apollo.game.plugin.entity.actions
+
+enum class PlayerActionType(val displayName: String, val slot: Int, val primary: Boolean = true) {
+ ATTACK("Attack", 2),
+ CHALLENGE("Challenge", 2),
+ FOLLOW("Follow", 4),
+ TRADE("Trade with", 5)
+}
\ No newline at end of file
diff --git a/game/plugin/entity/actions/src/PlayerActions.plugin.kts b/game/plugin/entity/actions/src/PlayerActions.plugin.kts
new file mode 100644
index 000000000..675a8a53e
--- /dev/null
+++ b/game/plugin/entity/actions/src/PlayerActions.plugin.kts
@@ -0,0 +1,20 @@
+package org.apollo.game.plugin.entity.actions
+
+import org.apollo.game.message.impl.PlayerActionMessage
+import org.apollo.game.model.event.impl.LoginEvent
+
+on { PlayerActionMessage::class }
+ .then {
+ val action = it.actionAt(option)
+ if (action != null) {
+ it.world.submit(PlayerActionEvent(it, it.world.playerRepository[index], action))
+ }
+
+ terminate()
+ }
+
+on_player_event { LoginEvent::class }
+ .then {
+ it.enableAction(PlayerActionType.FOLLOW)
+ it.enableAction(PlayerActionType.TRADE)
+ }
\ No newline at end of file
diff --git a/game/plugin/entity/actions/src/playerAction.kt b/game/plugin/entity/actions/src/playerAction.kt
new file mode 100644
index 000000000..d08b77974
--- /dev/null
+++ b/game/plugin/entity/actions/src/playerAction.kt
@@ -0,0 +1,28 @@
+package org.apollo.game.plugin.entity.actions
+
+import java.util.*
+import org.apollo.game.message.impl.SetPlayerActionMessage
+import org.apollo.game.model.entity.Player
+
+fun Player.enableAction(action: PlayerActionType) {
+ send(SetPlayerActionMessage(action.displayName, action.slot, action.primary))
+ actions += action
+}
+
+fun Player.disableAction(action: PlayerActionType) {
+ send(SetPlayerActionMessage("null", action.slot, action.primary))
+ actions -= action
+}
+
+fun Player.actionEnabled(action: PlayerActionType): Boolean {
+ return action in actions
+}
+
+fun Player.actionAt(slot: Int): PlayerActionType? {
+ return actions.find { it.slot == slot }
+}
+
+private val playerActionsMap = mutableMapOf>()
+
+private val Player.actions: EnumSet
+ get() = playerActionsMap.computeIfAbsent(this) { EnumSet.noneOf(PlayerActionType::class.java) }
diff --git a/game/plugin/entity/actions/test/PlayerActionTests.kt b/game/plugin/entity/actions/test/PlayerActionTests.kt
new file mode 100644
index 000000000..153345d0b
--- /dev/null
+++ b/game/plugin/entity/actions/test/PlayerActionTests.kt
@@ -0,0 +1,33 @@
+package org.apollo.game.plugin.entity.actions
+
+import io.mockk.verify
+import org.apollo.game.message.impl.SetPlayerActionMessage
+import org.apollo.game.model.entity.Player
+import org.apollo.game.plugin.testing.junit.ApolloTestingExtension
+import org.apollo.game.plugin.testing.junit.api.annotations.TestMock
+import org.junit.jupiter.api.Assertions.assertFalse
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.extension.ExtendWith
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.EnumSource
+
+@ExtendWith(ApolloTestingExtension::class)
+class PlayerActionTests {
+
+ @TestMock
+ lateinit var player: Player
+
+ @ParameterizedTest
+ @EnumSource(PlayerActionType::class)
+ fun `enabling and disabling PlayerActions sends SetPlayerActionMessages`(type: PlayerActionType) {
+ player.enableAction(type)
+
+ verify { player.send(eq(SetPlayerActionMessage(type.displayName, type.slot, type.primary))) }
+ assertTrue(player.actionEnabled(type)) { "Action $type should have been enabled, but was not." }
+
+ player.disableAction(type)
+
+ verify { player.send(eq(SetPlayerActionMessage("null", type.slot, type.primary))) }
+ assertFalse(player.actionEnabled(type)) { "Action $type should not have been enabled, but was." }
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/entity/following/build.gradle b/game/plugin/entity/following/build.gradle
new file mode 100644
index 000000000..4aca75d50
--- /dev/null
+++ b/game/plugin/entity/following/build.gradle
@@ -0,0 +1,11 @@
+apply plugin: 'kotlin'
+
+dependencies {
+ implementation project(':game')
+ implementation project(':cache')
+ implementation project(':net')
+ implementation project(':util')
+ implementation project(':game:plugin:entity:pathing')
+ implementation project(':game:plugin:entity:actions')
+ testImplementation project(':game:plugin-testing')
+}
diff --git a/game/plugin/entity/following/src/org/apollo/plugin/entity/following/FollowAction.kt b/game/plugin/entity/following/src/org/apollo/plugin/entity/following/FollowAction.kt
new file mode 100644
index 000000000..517c0af96
--- /dev/null
+++ b/game/plugin/entity/following/src/org/apollo/plugin/entity/following/FollowAction.kt
@@ -0,0 +1,49 @@
+package org.apollo.plugin.entity.following
+
+import org.apollo.game.action.Action
+import org.apollo.game.model.Direction
+import org.apollo.game.model.Position
+import org.apollo.game.model.entity.Player
+import org.apollo.net.message.Message
+import org.apollo.plugin.entity.pathing.walkBehind
+import org.apollo.plugin.entity.pathing.walkTo
+
+class FollowAction(player: Player, private val target: Player) : Action(0, true, player) {
+ var lastPosition: Position? = null
+
+ companion object {
+ fun start(player: Player, target: Player, message: Message? = null) {
+ player.startAction(FollowAction(player, target))
+ message?.terminate()
+ }
+ }
+
+ override fun execute() {
+ if (!target.isActive) {
+ stop()
+ return
+ }
+
+ mob.interactingMob = target
+
+ if (target.position == lastPosition) {
+ return
+ }
+
+ val distance = mob.position.getDistance(target.position)
+ if (distance >= 15) {
+ stop()
+ return
+ }
+
+ if (mob.position == target.position) {
+ val directions = Direction.NESW
+ val directionOffset = (Math.random() * directions.size).toInt()
+
+ mob.walkTo(target.position.step(1, directions[directionOffset]))
+ } else {
+ mob.walkBehind(target)
+ lastPosition = target.position
+ }
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/entity/following/src/org/apollo/plugin/entity/following/Following.plugin.kts b/game/plugin/entity/following/src/org/apollo/plugin/entity/following/Following.plugin.kts
new file mode 100644
index 000000000..98916343f
--- /dev/null
+++ b/game/plugin/entity/following/src/org/apollo/plugin/entity/following/Following.plugin.kts
@@ -0,0 +1,11 @@
+package org.apollo.plugin.entity.following
+
+import org.apollo.game.plugin.entity.actions.PlayerActionEvent
+import org.apollo.game.plugin.entity.actions.PlayerActionType
+
+on_player_event { PlayerActionEvent::class }
+ .where { action == PlayerActionType.FOLLOW }
+ .then { player ->
+ FollowAction.start(player, target)
+ terminate()
+ }
\ No newline at end of file
diff --git a/game/plugin/entity/pathing/build.gradle b/game/plugin/entity/pathing/build.gradle
new file mode 100644
index 000000000..3a9351392
--- /dev/null
+++ b/game/plugin/entity/pathing/build.gradle
@@ -0,0 +1,10 @@
+apply plugin: 'kotlin'
+
+dependencies {
+ implementation project(':game')
+ implementation project(':cache')
+ implementation project(':net')
+ implementation project(':util')
+
+ testImplementation project(':game:plugin-testing')
+}
diff --git a/game/plugin/entity/pathing/src/org/apollo/plugin/entity/pathing/Pathing.kt b/game/plugin/entity/pathing/src/org/apollo/plugin/entity/pathing/Pathing.kt
new file mode 100644
index 000000000..963bb48ad
--- /dev/null
+++ b/game/plugin/entity/pathing/src/org/apollo/plugin/entity/pathing/Pathing.kt
@@ -0,0 +1,66 @@
+package org.apollo.plugin.entity.pathing
+
+import org.apollo.game.model.Direction
+import org.apollo.game.model.Position
+import org.apollo.game.model.entity.Entity
+import org.apollo.game.model.entity.Mob
+import org.apollo.game.model.entity.Npc
+import org.apollo.game.model.entity.Player
+import org.apollo.game.model.entity.obj.GameObject
+import org.apollo.game.model.entity.path.SimplePathfindingAlgorithm
+
+/**
+ * Adds a path from this [Mob] to the [target] [Entity], with the final position determined by the [facing] [Direction].
+ */
+fun Mob.walkTo(target: Entity, facing: Direction? = null) {
+ val (width, length) = bounds(this)
+ val (targetWidth, targetLength) = bounds(target)
+
+ val direction = facing ?: Direction.between(position, target.position)
+ val dx = direction.deltaX()
+ val dy = direction.deltaY()
+
+ val targetX = if (dx <= 0) target.position.x else target.position.x + targetWidth - 1
+ val targetY = if (dy <= 0) target.position.y else target.position.y + targetLength - 1
+ val offsetX = if (dx < 0) -width else if (dx > 0) 1 else 0
+ val offsetY = if (dy < 0) -length else if (dy > 0) 1 else 0
+
+ walkTo(Position(targetX + offsetX, targetY + offsetY, position.height))
+}
+
+/**
+ * Adds a path for this [Mob] to the target [Mob]s last position.
+ */
+fun Mob.walkBehind(target: Mob) {
+ walkTo(target, target.lastDirection.opposite())
+}
+
+/**
+ * Adds a path from this [Mob] to the [target] [Position], ending the path as soon as [positionPredicate] returns
+ * `false` (if provided).
+ */
+fun Mob.walkTo(target: Position, positionPredicate: ((Position) -> Boolean)? = null) {
+ if (position == target) {
+ return
+ }
+
+ val pathfinder = SimplePathfindingAlgorithm(world.collisionManager)
+ val path = pathfinder.find(position, target)
+
+ if (positionPredicate == null) {
+ path.forEach(walkingQueue::addStep)
+ } else {
+ for (step in path) {
+ if (!positionPredicate(step)) {
+ return
+ }
+
+ walkingQueue.addStep(step)
+ }
+ }
+}
+
+/**
+ * Returns the bounding size of the specified [Entity], in [x-size, y-size] format.
+ */
+private fun bounds(target: Entity): Pair = Pair(target.width, target.length)
\ No newline at end of file
diff --git a/game/plugin/entity/spawn/build.gradle b/game/plugin/entity/spawn/build.gradle
new file mode 100644
index 000000000..4d96c9f57
--- /dev/null
+++ b/game/plugin/entity/spawn/build.gradle
@@ -0,0 +1,10 @@
+apply plugin: 'kotlin'
+
+dependencies {
+ implementation project(':game')
+ implementation project(':cache')
+ implementation project(':net')
+ implementation project(':util')
+ implementation project(':game:plugin:api')
+ testImplementation project(':game:plugin-testing')
+}
diff --git a/game/plugin/entity/spawn/src/org/apollo/game/plugin/entity/spawn/Spawn.kt b/game/plugin/entity/spawn/src/org/apollo/game/plugin/entity/spawn/Spawn.kt
new file mode 100644
index 000000000..3e7732691
--- /dev/null
+++ b/game/plugin/entity/spawn/src/org/apollo/game/plugin/entity/spawn/Spawn.kt
@@ -0,0 +1,27 @@
+package org.apollo.game.plugin.entity.spawn
+
+import org.apollo.game.model.Animation
+import org.apollo.game.model.Direction
+import org.apollo.game.model.Graphic
+import org.apollo.game.model.Position
+
+fun spawnNpc(name: String, x: Int, y: Int, z: Int = 0, id: Int? = null, facing: Direction = Direction.NORTH) {
+ Spawns.list += Spawn(id, name, Position(x, y, z), facing)
+}
+
+fun spawnNpc(name: String, position: Position, id: Int? = null, facing: Direction = Direction.NORTH) {
+ Spawns.list += Spawn(id, name, position, facing)
+}
+
+internal data class Spawn(
+ val id: Int?,
+ val name: String,
+ val position: Position,
+ val facing: Direction,
+ val spawnAnimation: Animation? = null,
+ val spawnGraphic: Graphic? = null
+)
+
+internal object Spawns {
+ val list = mutableListOf()
+}
diff --git a/game/plugin/entity/spawn/src/org/apollo/game/plugin/entity/spawn/Spawn.plugin.kts b/game/plugin/entity/spawn/src/org/apollo/game/plugin/entity/spawn/Spawn.plugin.kts
new file mode 100644
index 000000000..7d9fb4ce8
--- /dev/null
+++ b/game/plugin/entity/spawn/src/org/apollo/game/plugin/entity/spawn/Spawn.plugin.kts
@@ -0,0 +1,20 @@
+package org.apollo.game.plugin.entity.spawn
+
+import org.apollo.game.model.entity.Npc
+import org.apollo.game.plugin.api.Definitions
+
+start { world ->
+ for ((id, name, position, facing, animation, graphic) in Spawns.list) {
+ val definition = requireNotNull(id?.let(Definitions::npc) ?: Definitions.npc(name)) {
+ "Could not find an Npc named $name to spawn."
+ }
+
+ val npc = Npc(world, definition.id, position).apply {
+ turnTo(position.step(1, facing))
+ animation?.let(::playAnimation)
+ graphic?.let(::playGraphic)
+ }
+
+ world.register(npc)
+ }
+}
diff --git a/game/plugin/entity/spawn/test/org/apollo/game/plugin/entity/spawn/SpawnTests.kt b/game/plugin/entity/spawn/test/org/apollo/game/plugin/entity/spawn/SpawnTests.kt
new file mode 100644
index 000000000..7290a7892
--- /dev/null
+++ b/game/plugin/entity/spawn/test/org/apollo/game/plugin/entity/spawn/SpawnTests.kt
@@ -0,0 +1,142 @@
+package org.apollo.game.plugin.entity.spawn
+
+import org.apollo.cache.def.NpcDefinition
+import org.apollo.game.model.*
+import org.apollo.game.plugin.testing.junit.ApolloTestingExtension
+import org.apollo.game.plugin.testing.junit.api.annotations.NpcDefinitions
+import org.apollo.game.plugin.testing.junit.api.annotations.TestMock
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Disabled
+import org.junit.jupiter.api.extension.ExtendWith
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.Arguments
+import org.junit.jupiter.params.provider.MethodSource
+
+@ExtendWith(ApolloTestingExtension::class)
+class SpawnTests {
+
+ @TestMock
+ lateinit var world: World
+
+ @BeforeEach
+ fun addNewNpcs() {
+ var previousSize: Int
+
+ do {
+ previousSize = world.npcRepository.size()
+ world.pulse()
+ } while (previousSize != world.npcRepository.size())
+ }
+
+ @MethodSource("npcs spawned by id")
+ @ParameterizedTest(name = "spawning {1} at {2} (id = {0})")
+ fun `spawned npcs are in the correct position`(id: Int, name: String, spawn: Position) {
+ val npc = world.npcRepository.find { it.id == id }
+
+ assertEquals(spawn, npc?.position) { "Failed to find npc $name with id $id." }
+ }
+
+ @MethodSource("npcs spawned by id with directions")
+ @ParameterizedTest(name = "spawning {1} with direction {3} (id = {0})")
+ fun `spawned npcs are facing the correct direction`(id: Int, name: String, spawn: Position, direction: Direction) {
+ val npc = requireNotNull(world.npcRepository.find { it.id == id }) { "Failed to find npc $name with id $id." }
+ val facing = spawn.step(1, direction)
+
+ assertEquals(facing, npc.facingPosition)
+ }
+
+ @Disabled("Currently no way to test if the animation was played")
+ @MethodSource("npcs spawned by id with animations")
+ @ParameterizedTest(name = "spawning {1} with animation {3} (id = {0})")
+ fun `spawned npcs are playing the correct animation`(id: Int, name: String, spawn: Position, animation: Animation) {
+ val npc = requireNotNull(world.npcRepository.find { it.id == id }) { "Failed to find npc $name with id $id." }
+
+ TODO("How to verify that npc.playAnimation was called with $animation.")
+ }
+
+ @Disabled("Currently no way to test if the graphic was played")
+ @MethodSource("npcs spawned by id with graphics")
+ @ParameterizedTest(name = "spawning {1} with graphic {3} (id = {0})")
+ fun `spawned npcs are playing the correct graphic`(id: Int, name: String, spawn: Position, graphic: Graphic) {
+ val npc = requireNotNull(world.npcRepository.find { it.id == id }) { "Failed to find npc $name with id $id." }
+
+ TODO("How to verify that npc.playGraphic was called with $graphic.")
+ }
+
+ @MethodSource("npcs spawned by name")
+ @ParameterizedTest(name = "spawning {0}")
+ fun `spawns are looked up by name if the id is unspecified`(name: String) {
+ val npc = world.npcRepository.find { it.definition.name === name }!!
+ val expectedId = name.substringAfterLast("_").toInt()
+
+ assertEquals(expectedId, npc.id)
+ }
+
+ companion object {
+
+ // This test class has multiple (hidden) order dependencies because of the nature of the spawn
+ // plugin, where npcs are inserted into the world immediately after world initialisation.
+ //
+ // All npcs that should be spawned by the test must be passed to `spawnNpc` _before_ the test world
+ // is created by the ApolloTestingExtension - which means they must be done inside the initialisation
+ // block of this companion object.
+ //
+ // When npcs are created, however, they look up their NpcDefinition - so all of the definitions must
+ // be created (via `@NpcDefinitions`) before the initialisation block is executed.
+ //
+ // The world must also be pulsed after the spawn plugin executes, so that npcs are registered (i.e. moved
+ // out of the queue).
+
+ @JvmStatic
+ fun `npcs spawned by id`(): List {
+ return npcs.filterNot { it.id == null }
+ .map { (id, name, position) -> Arguments.of(id, name, position) }
+ }
+
+ @JvmStatic
+ fun `npcs spawned by id with directions`(): List {
+ return npcs.filterNot { it.id == null }
+ .map { (id, name, position, direction) -> Arguments.of(id, name, position, direction) }
+ }
+
+ @JvmStatic
+ fun `npcs spawned by id with animations`(): List {
+ return npcs.filterNot { it.id == null }
+ .map { (id, name, position, _, animation) -> Arguments.of(id, name, position, animation) }
+ }
+
+ @JvmStatic
+ fun `npcs spawned by id with graphics`(): List {
+ return npcs.filterNot { it.id == null }
+ .map { (id, name, position, _, _, graphic) -> Arguments.of(id, name, position, graphic) }
+ }
+
+ @JvmStatic
+ fun `npcs spawned by name`(): List {
+ return npcs.filter { it.id == null }
+ .map { (_, name) -> Arguments.of(name) }
+ }
+
+ private val npcs = listOf(
+ Spawn(0, "hans", Position(1000, 1000, 0), facing = Direction.NORTH, spawnAnimation = Animation(10, 100)),
+ Spawn(1, "man", Position(1000, 1000, 0), facing = Direction.NORTH, spawnGraphic = Graphic(154, 0, 100)),
+ Spawn(null, "man_2", Position(1000, 1000, 2), facing = Direction.NORTH),
+ Spawn(null, "man_3", Position(1000, 1000, 3), facing = Direction.SOUTH),
+ Spawn(6, "fakename123", Position(1500, 1500, 3), facing = Direction.EAST, spawnAnimation = Animation(112)),
+ Spawn(12, "fakename123", Position(1500, 1500, 3), facing = Direction.WEST, spawnGraphic = Graphic(964))
+ )
+
+ @NpcDefinitions
+ val definitions = npcs.map { (id, name) ->
+ val definitionId = id ?: name.substringAfterLast("_").toInt()
+ NpcDefinition(definitionId).also { it.name = name }
+ }
+
+ init { // Must come after NpcDef initialisation, before mocked World initialisation
+ for ((id, name, position, direction) in npcs) {
+ spawnNpc(name, position.x, position.y, position.height, id, direction)
+ }
+ }
+ }
+}
diff --git a/game/plugin/locations/al-kharid/build.gradle b/game/plugin/locations/al-kharid/build.gradle
new file mode 100644
index 000000000..7eed7d878
--- /dev/null
+++ b/game/plugin/locations/al-kharid/build.gradle
@@ -0,0 +1,13 @@
+apply plugin: 'kotlin'
+
+
+
+dependencies {
+ implementation project(':game')
+ implementation project(':cache')
+ implementation project(':net')
+ implementation project(':util')
+ implementation project(':game:plugin:entity:spawn')
+ implementation project(':game:plugin:shops')
+ testImplementation project(':game:plugin-testing')
+}
diff --git a/game/plugin/locations/al-kharid/src/npcs.plugin.kts b/game/plugin/locations/al-kharid/src/npcs.plugin.kts
new file mode 100644
index 000000000..79b74471c
--- /dev/null
+++ b/game/plugin/locations/al-kharid/src/npcs.plugin.kts
@@ -0,0 +1,114 @@
+package org.apollo.plugin.locations.alKharid
+
+import org.apollo.game.model.Direction
+import org.apollo.game.plugin.entity.spawn.spawnNpc
+
+// Generic npcs
+
+spawnNpc("man", x = 3276, y = 3186)
+spawnNpc("man", x = 3282, y = 3197)
+
+spawnNpc("man", id = 3, x = 3301, y = 3200)
+spawnNpc("man", id = 3, x = 3300, y = 3208)
+
+spawnNpc("man", id = 2, x = 3297, y = 3196)
+
+spawnNpc("man", id = 16, x = 3294, y = 3204)
+
+spawnNpc("spider", x = 3319, y = 3145)
+spawnNpc("spider", x = 3319, y = 3140)
+spawnNpc("spider", x = 3323, y = 3138)
+
+spawnNpc("scorpion", x = 3282, y = 3149)
+
+// Camels
+
+spawnNpc("cam_the_camel", x = 3295, y = 3232)
+spawnNpc("elly_the_camel", x = 3312, y = 3210)
+spawnNpc("camel", x = 3285, y = 3198)
+spawnNpc("ollie_the_camel", x = 3291, y = 3209)
+spawnNpc("al_the_camel", x = 3275, y = 3162)
+
+// Quest npc
+
+spawnNpc("osman", x = 3286, y = 3180, facing = Direction.EAST)
+
+spawnNpc("hassan", x = 3302, y = 3163)
+
+spawnNpc("father_reen", x = 3272, y = 3158)
+
+spawnNpc("man", id = 663, x = 3297, y = 3287)
+
+// Boarder guards
+
+spawnNpc("border_guard", x = 3268, y = 3226)
+spawnNpc("border_guard", x = 3267, y = 3226)
+spawnNpc("border_guard", x = 3268, y = 3229, facing = Direction.SOUTH)
+spawnNpc("border_guard", x = 3267, y = 3229, facing = Direction.SOUTH)
+
+// Palace guards
+
+spawnNpc("Al-Kharid warrior", x = 3285, y = 3174)
+spawnNpc("Al-Kharid warrior", x = 3283, y = 3168)
+spawnNpc("Al-Kharid warrior", x = 3285, y = 3169)
+spawnNpc("Al-Kharid warrior", x = 3290, y = 3162)
+spawnNpc("Al-Kharid warrior", x = 3295, y = 3170)
+spawnNpc("Al-Kharid warrior", x = 3300, y = 3175)
+spawnNpc("Al-Kharid warrior", x = 3300, y = 3171)
+spawnNpc("Al-Kharid warrior", x = 3301, y = 3168)
+
+// Shanty pass
+
+spawnNpc("shantay_guard", x = 3301, y = 3120)
+spawnNpc("shantay_guard", x = 3302, y = 3126)
+spawnNpc("shantay_guard", x = 3306, y = 3126)
+spawnNpc("shantay_guard", id = 838, x = 3303, y = 3118)
+
+// Mine
+
+spawnNpc("scorpion", x = 3296, y = 3294)
+spawnNpc("scorpion", x = 3298, y = 3280)
+spawnNpc("scorpion", x = 3299, y = 3299)
+spawnNpc("scorpion", x = 3299, y = 3309)
+spawnNpc("scorpion", x = 3300, y = 3287)
+spawnNpc("scorpion", x = 3300, y = 3315)
+spawnNpc("scorpion", x = 3301, y = 3305)
+spawnNpc("scorpion", x = 3301, y = 3312)
+
+// Functional npcs
+
+spawnNpc("gnome_pilot", x = 3279, y = 3213)
+
+spawnNpc("banker", id = 496, x = 3267, y = 3164, facing = Direction.EAST)
+spawnNpc("banker", id = 496, x = 3267, y = 3167, facing = Direction.EAST)
+spawnNpc("banker", id = 496, x = 3267, y = 3169, facing = Direction.EAST)
+
+spawnNpc("banker", id = 497, x = 3267, y = 3166, facing = Direction.EAST)
+spawnNpc("banker", id = 497, x = 3267, y = 3168, facing = Direction.EAST)
+
+spawnNpc("gem_trader", x = 3287, y = 3210)
+
+spawnNpc("zeke", x = 3289, y = 3189)
+
+spawnNpc("shantay", x = 3304, y = 3124)
+
+spawnNpc("rug_merchant", id = 2296, x = 3311, y = 3109, facing = Direction.WEST)
+
+spawnNpc("ranael", x = 3315, y = 3163)
+
+spawnNpc("shop_keeper", id = 524, x = 3315, y = 3178)
+spawnNpc("shop_assistant", id = 525, x = 3315, y = 3180, facing = Direction.WEST)
+
+spawnNpc("louie_legs", x = 3316, y = 3175, facing = Direction.WEST)
+
+spawnNpc("ellis", x = 3274, y = 3192)
+
+spawnNpc("dommik", x = 3321, y = 3193)
+
+spawnNpc("tool_leprechaun", x = 3319, y = 3204)
+
+spawnNpc("ali_morrisane", x = 3304, y = 3211, facing = Direction.EAST)
+
+spawnNpc("silk_trader", x = 3300, y = 3203)
+
+spawnNpc("karim", x = 3273, y = 3180)
\ No newline at end of file
diff --git a/game/plugin/locations/al-kharid/src/shops.plugin.kts b/game/plugin/locations/al-kharid/src/shops.plugin.kts
new file mode 100644
index 000000000..2d51292bc
--- /dev/null
+++ b/game/plugin/locations/al-kharid/src/shops.plugin.kts
@@ -0,0 +1,146 @@
+package org.apollo.plugin.locations.alKharid
+
+import org.apollo.game.plugin.shops.builder.shop
+
+shop("Al-Kharid General Store") {
+ operated by "Shop keeper"(524) and "Shop assistant"(525)
+ buys any items
+
+ sell(5) of "Pot"
+ sell(2) of "Jug"
+ sell(2) of "Shears"
+ sell(3) of "Bucket"
+ sell(2) of "Bowl"
+ sell(2) of "Cake tin"
+ sell(2) of "Tinderbox"
+ sell(2) of "Chisel"
+ sell(5) of "Hammer"
+ sell(5) of "Newcomer map"
+}
+
+/**
+ * TODO add a way to "unlock" items, as more of Ali Morrisane's items are unlocked by completing certain parts of
+ * the Rogue Trader minigame progressively.
+ *
+ * TODO this shop can be accessed only through dialogue, so support for that should be added.
+ */
+/*shop("Ali's Discount Wares") {
+ operated by "Ali Morrisane"
+
+ sell(3) of "Pot"
+ sell(2) of "Jug"
+ sell(10) of { "Waterskin"(1825) }
+ sell(3) of "Desert shirt"
+ sell(2) of "Desert boots"
+ sell(19) of "Bucket"
+ sell(11) of "Fake beard"
+ sell(12) of "Karidian headpiece"
+ sell(50) of "Papyrus"
+ sell(5) of "Knife"
+ sell(11) of "Tinderbox"
+ sell(23) of "Bronze pickaxe"
+ sell(15) of "Raw chicken"
+}*/
+
+shop("Dommik's Crafting Store") {
+ operated by "Dommik"
+
+ sell(2) of "Chisel"
+ category("mould") {
+ sell(10) of "Ring"
+ sell(2) of "Necklace"
+ sell(10) of "Amulet"
+ }
+ sell(3) of "Needle"
+ sell(100) of "Thread"
+ category("mould") {
+ sell(3) of "Holy"
+ sell(10) of "Sickle"
+ sell(10) of "Tiara"
+ }
+}
+
+shop("Gem Trader") {
+ operated by "Gem trader"
+
+ category("uncut", affix = prefix) {
+ sell(1) of {
+ -"Sapphire"
+ -"Emerald"
+ }
+ sell(0) of {
+ -"Ruby"
+ -"Diamond"
+ }
+ }
+
+ sell(1) of {
+ -"Sapphire"
+ -"Emerald"
+ }
+ sell(0) of {
+ -"Ruby"
+ -"Diamond"
+ }
+}
+
+shop("Louie's Armoured Legs Bazaar") {
+ operated by "Louie Legs"
+
+ category("platelegs", depluralise = false) {
+ sell(5) of "Bronze"
+ sell(3) of "Iron"
+ sell(2) of "Steel"
+ sell(1) of "Black"
+ sell(1) of "Mithril"
+ sell(1) of "Adamant"
+ }
+}
+
+shop("Ranael's Super Skirt Store") {
+ operated by "Ranael"
+
+ category("plateskirt") {
+ sell(5) of "Bronze"
+ sell(3) of "Iron"
+ sell(2) of "Steel"
+ sell(1) of "Black"
+ sell(1) of "Mithril"
+ sell(1) of "Adamant"
+ }
+}
+
+shop("Shantay Pass Shop") {
+ operated by "Shantay"
+
+ sell(100) of { "Waterskin"(1823) }
+ sell(100) of { "Waterskin"(1831) }
+ sell(10) of "Jug of water"
+ sell(10) of "Bowl of water"
+ sell(10) of "Bucket of water"
+ sell(10) of "Knife"
+ category("desert", affix = prefix) {
+ sell(10) of "shirt"
+ sell(10) of "robe"
+ sell(10) of "boots"
+ }
+ sell(10) of "Bronze bar"
+ sell(500) of "Feather"
+ sell(10) of "Hammer"
+ sell(0) of "Bucket"
+ sell(0) of "Bowl"
+ sell(0) of "Jug"
+ sell(500) of "Shantay pass"
+ sell(20) of "Rope"
+}
+
+shop("Zeke's Superior Scimitars") {
+ operated by "Zeke"
+
+ category("scimitar") {
+ sell(5) of "Bronze"
+ sell(3) of "Iron"
+ sell(2) of "Steel"
+ sell(1) of "Mithril"
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/locations/edgeville/build.gradle b/game/plugin/locations/edgeville/build.gradle
new file mode 100644
index 000000000..7eed7d878
--- /dev/null
+++ b/game/plugin/locations/edgeville/build.gradle
@@ -0,0 +1,13 @@
+apply plugin: 'kotlin'
+
+
+
+dependencies {
+ implementation project(':game')
+ implementation project(':cache')
+ implementation project(':net')
+ implementation project(':util')
+ implementation project(':game:plugin:entity:spawn')
+ implementation project(':game:plugin:shops')
+ testImplementation project(':game:plugin-testing')
+}
diff --git a/game/plugin/locations/edgeville/src/npcs.plugin.kts b/game/plugin/locations/edgeville/src/npcs.plugin.kts
new file mode 100644
index 000000000..d89e84926
--- /dev/null
+++ b/game/plugin/locations/edgeville/src/npcs.plugin.kts
@@ -0,0 +1,53 @@
+package org.apollo.plugin.locations.edgeville
+
+import org.apollo.game.model.Direction
+import org.apollo.game.plugin.entity.spawn.spawnNpc
+
+// Generic npcs
+
+spawnNpc("man", x = 3095, y = 3508)
+spawnNpc("man", x = 3095, y = 3511)
+spawnNpc("man", x = 3098, y = 3509)
+spawnNpc("man", id = 2, x = 3093, y = 3511)
+spawnNpc("man", id = 3, x = 3097, y = 3508)
+spawnNpc("man", id = 3, x = 3092, y = 3508)
+spawnNpc("man", id = 3, x = 3097, y = 3512)
+
+spawnNpc("guard", x = 3086, y = 3516)
+spawnNpc("guard", x = 3094, y = 3518)
+spawnNpc("guard", x = 3108, y = 3514)
+spawnNpc("guard", x = 3110, y = 3514)
+spawnNpc("guard", x = 3113, y = 3514)
+spawnNpc("guard", x = 3113, y = 3516)
+
+spawnNpc("sheep", id = 43, x = 3050, y = 3516)
+spawnNpc("sheep", id = 43, x = 3051, y = 3514)
+spawnNpc("sheep", id = 43, x = 3056, y = 3517)
+spawnNpc("ram", id = 3673, x = 3048, y = 3515)
+
+spawnNpc("monk", x = 3044, y = 3491)
+spawnNpc("monk", x = 3045, y = 3483)
+spawnNpc("monk", x = 3045, y = 3497)
+spawnNpc("monk", x = 3050, y = 3490)
+spawnNpc("monk", x = 3054, y = 3490)
+spawnNpc("monk", x = 3058, y = 3497)
+
+// Functional npcs
+
+spawnNpc("richard", x = 3098, y = 3516)
+spawnNpc("doris", x = 3079, y = 3491)
+spawnNpc("brother_jered", x = 3045, y = 3488)
+spawnNpc("brother_althric", x = 3054, y = 3504)
+
+spawnNpc("abbot_langley", x = 3059, y = 3484)
+spawnNpc("oziach", x = 3067, y = 3518, facing = Direction.EAST)
+
+spawnNpc("shop_keeper", id = 528, x = 3079, y = 3509)
+spawnNpc("shop_assistant", id = 529, x = 3082, y = 3513)
+
+spawnNpc("banker", x = 3096, y = 3489, facing = Direction.WEST)
+spawnNpc("banker", x = 3096, y = 3491, facing = Direction.WEST)
+spawnNpc("banker", x = 3096, y = 3492)
+spawnNpc("banker", x = 3098, y = 3492)
+
+spawnNpc("mage_of_zamorak", x = 3106, y = 3560)
\ No newline at end of file
diff --git a/game/plugin/locations/edgeville/src/shops.plugin.kts b/game/plugin/locations/edgeville/src/shops.plugin.kts
new file mode 100644
index 000000000..0a5facec5
--- /dev/null
+++ b/game/plugin/locations/edgeville/src/shops.plugin.kts
@@ -0,0 +1,31 @@
+package org.apollo.plugin.locations.edgeville
+
+import org.apollo.game.plugin.shops.builder.shop
+
+shop("Edgeville General Store") {
+ operated by "Shop keeper"(528) and "Shop assistant"(529)
+ buys any items
+
+ sell(5) of "Pot"
+ sell(2) of "Jug"
+ sell(2) of "Shears"
+ sell(3) of "Bucket"
+ sell(2) of "Bowl"
+ sell(2) of "Cake tin"
+ sell(2) of "Tinderbox"
+ sell(2) of "Chisel"
+ sell(5) of "Hammer"
+ sell(5) of "Newcomer map"
+}
+
+/**
+ * TODO make a way to have requirements to open shops. Players have to have finished Dragon Slayer to access
+ * "Oziach's Armour"
+ */
+shop("Oziach's Armour") {
+ operated by "Oziach"
+
+ sell(2) of "Rune platebody"
+ sell(2) of "Green d'hide body"
+ sell(35) of "Anti-dragon shield"
+}
\ No newline at end of file
diff --git a/game/plugin/locations/falador/build.gradle b/game/plugin/locations/falador/build.gradle
new file mode 100644
index 000000000..7eed7d878
--- /dev/null
+++ b/game/plugin/locations/falador/build.gradle
@@ -0,0 +1,13 @@
+apply plugin: 'kotlin'
+
+
+
+dependencies {
+ implementation project(':game')
+ implementation project(':cache')
+ implementation project(':net')
+ implementation project(':util')
+ implementation project(':game:plugin:entity:spawn')
+ implementation project(':game:plugin:shops')
+ testImplementation project(':game:plugin-testing')
+}
diff --git a/game/plugin/locations/falador/src/npcs.plugin.kts b/game/plugin/locations/falador/src/npcs.plugin.kts
new file mode 100644
index 000000000..9136f9c09
--- /dev/null
+++ b/game/plugin/locations/falador/src/npcs.plugin.kts
@@ -0,0 +1,171 @@
+package org.apollo.plugin.locations.falador
+
+import org.apollo.game.plugin.entity.spawn.spawnNpc
+
+// Generic npcs
+
+spawnNpc("chicken", x = 2965, y = 3345)
+
+spawnNpc("duck", x = 2988, y = 3383)
+spawnNpc("duck", x = 2992, y = 3383)
+spawnNpc("duck", x = 2993, y = 3385)
+
+spawnNpc("drunken_man", x = 2957, y = 3368, z = 1)
+
+spawnNpc("dwarf", id = 118, x = 3023, y = 3334)
+spawnNpc("dwarf", id = 118, x = 3027, y = 3341)
+spawnNpc("dwarf", id = 118, x = 3012, y = 3341)
+spawnNpc("dwarf", id = 118, x = 3017, y = 3346, z = 1)
+spawnNpc("dwarf", id = 118, x = 3011, y = 3341, z = 1)
+
+spawnNpc("dwarf", id = 121, x = 3027, y = 3341)
+
+spawnNpc("dwarf", id = 382, x = 3017, y = 3340)
+
+spawnNpc("dwarf", id = 3294, x = 3022, y = 3338)
+spawnNpc("dwarf", id = 3295, x = 3021, y = 3341)
+
+spawnNpc("gardener", x = 2998, y = 3385)
+spawnNpc("gardener", x = 3019, y = 3369)
+spawnNpc("gardener", id = 3234, x = 3016, y = 3386)
+
+spawnNpc("guard", x = 2965, y = 3394)
+spawnNpc("guard", x = 2964, y = 3396)
+spawnNpc("guard", x = 2966, y = 3397)
+spawnNpc("guard", x = 2964, y = 3384)
+spawnNpc("guard", x = 2963, y = 3380)
+spawnNpc("guard", x = 3006, y = 3325)
+spawnNpc("guard", x = 3008, y = 3320)
+spawnNpc("guard", x = 3006, y = 3322)
+spawnNpc("guard", x = 3038, y = 3356)
+
+spawnNpc("guard", id = 10, x = 2942, y = 3375)
+spawnNpc("guard", id = 10, x = 3040, y = 3352)
+
+spawnNpc("guard", id = 3230, x = 2967, y = 3395)
+spawnNpc("guard", id = 3230, x = 2966, y = 3392)
+spawnNpc("guard", id = 3230, x = 2963, y = 3376)
+spawnNpc("guard", id = 3230, x = 2954, y = 3382)
+spawnNpc("guard", id = 3230, x = 2950, y = 3377)
+spawnNpc("guard", id = 3230, x = 2968, y = 3381)
+
+spawnNpc("guard", id = 3231, x = 3033, y = 3389, z = 1)
+spawnNpc("guard", id = 3231, x = 3041, y = 3388, z = 1)
+spawnNpc("guard", id = 3231, x = 3048, y = 3389, z = 1)
+spawnNpc("guard", id = 3231, x = 3056, y = 3389, z = 1)
+spawnNpc("guard", id = 3231, x = 3062, y = 3386, z = 1)
+spawnNpc("guard", id = 3231, x = 3058, y = 3329, z = 1)
+spawnNpc("guard", id = 3231, x = 3050, y = 3329, z = 1)
+spawnNpc("guard", id = 3231, x = 3038, y = 3329, z = 1)
+spawnNpc("guard", id = 3231, x = 3029, y = 3329, z = 1)
+
+spawnNpc("swan", x = 2960, y = 3359)
+spawnNpc("swan", x = 2963, y = 3360)
+spawnNpc("swan", x = 2968, y = 3359)
+spawnNpc("swan", x = 2971, y = 3360)
+spawnNpc("swan", x = 2976, y = 3358)
+spawnNpc("swan", x = 2989, y = 3384)
+
+spawnNpc("man", id = 3223, x = 2991, y = 3365)
+spawnNpc("man", id = 3225, x = 3037, y = 3345, z = 1)
+
+spawnNpc("white_knight", x = 2983, y = 3343)
+spawnNpc("white_knight", x = 2981, y = 3334)
+spawnNpc("white_knight", x = 2988, y = 3335)
+spawnNpc("white_knight", x = 2996, y = 3342)
+spawnNpc("white_knight", x = 2960, y = 3340)
+spawnNpc("white_knight", x = 2962, y = 3336)
+spawnNpc("white_knight", x = 2974, y = 3342)
+spawnNpc("white_knight", x = 2972, y = 3345)
+spawnNpc("white_knight", x = 2977, y = 3348)
+
+spawnNpc("white_knight", id = 3348, x = 2971, y = 3340)
+spawnNpc("white_knight", id = 3348, x = 2978, y = 3350)
+
+spawnNpc("white_knight", x = 2964, y = 3330, z = 1)
+spawnNpc("white_knight", x = 2968, y = 3334, z = 1)
+spawnNpc("white_knight", x = 2969, y = 3339, z = 1)
+spawnNpc("white_knight", x = 2978, y = 3332, z = 1)
+spawnNpc("white_knight", x = 2958, y = 3340, z = 1)
+spawnNpc("white_knight", x = 2960, y = 3343, z = 1)
+
+spawnNpc("white_knight", id = 3348, x = 2987, y = 3334, z = 1)
+spawnNpc("white_knight", id = 3348, x = 2983, y = 3336, z = 1)
+spawnNpc("white_knight", id = 3348, x = 2987, y = 3334, z = 1)
+spawnNpc("white_knight", id = 3348, x = 2979, y = 3348, z = 1)
+spawnNpc("white_knight", id = 3348, x = 2964, y = 3337, z = 1)
+
+spawnNpc("white_knight", id = 3349, x = 2989, y = 3344, z = 1)
+
+spawnNpc("white_knight", x = 2985, y = 3342, z = 2)
+
+spawnNpc("white_knight", id = 3348, x = 2979, y = 3348, z = 2)
+spawnNpc("white_knight", id = 3348, x = 2974, y = 3329, z = 2)
+spawnNpc("white_knight", id = 3348, x = 2982, y = 3341, z = 2)
+
+spawnNpc("white_knight", id = 3349, x = 2990, y = 3341, z = 2)
+spawnNpc("white_knight", id = 3349, x = 2971, y = 3330, z = 2)
+spawnNpc("white_knight", id = 3349, x = 2965, y = 3350, z = 2)
+spawnNpc("white_knight", id = 3349, x = 2965, y = 3329, z = 2)
+
+spawnNpc("white_knight", id = 3350, x = 2961, y = 3347, z = 2)
+
+spawnNpc("white_knight", id = 3349, x = 2962, y = 3339, z = 3)
+
+spawnNpc("white_knight", id = 3350, x = 2960, y = 3336, z = 3)
+spawnNpc("white_knight", id = 3350, x = 2984, y = 3349, z = 3)
+
+spawnNpc("woman", id = 3226, x = 2991, y = 3384)
+
+// Functional npcs
+
+spawnNpc("apprentice_workman", id = 3235, x = 2971, y = 3369, z = 1)
+
+spawnNpc("banker", id = 495, x = 2945, y = 3366)
+spawnNpc("banker", id = 495, x = 2946, y = 3366)
+spawnNpc("banker", id = 495, x = 2947, y = 3366)
+spawnNpc("banker", id = 495, x = 2948, y = 3366)
+
+spawnNpc("banker", x = 2949, y = 3366)
+
+spawnNpc("banker", x = 3015, y = 3353)
+spawnNpc("banker", x = 3014, y = 3353)
+spawnNpc("banker", x = 3013, y = 3353)
+spawnNpc("banker", x = 3012, y = 3353)
+spawnNpc("banker", x = 3011, y = 3353)
+spawnNpc("banker", x = 3010, y = 3353)
+
+spawnNpc("cassie", x = 2976, y = 3383)
+
+spawnNpc("emily", x = 2954, y = 3372)
+
+spawnNpc("flynn", x = 2950, y = 3387)
+
+spawnNpc("hairdresser", x = 2944, y = 3380)
+
+spawnNpc("herquin", x = 2945, y = 3335)
+
+spawnNpc("heskel", x = 3007, y = 3374)
+
+spawnNpc("kaylee", x = 2957, y = 3372)
+
+spawnNpc("tina", x = 2955, y = 3371, z = 1)
+
+spawnNpc("tool_leprechaun", x = 3005, y = 3370)
+
+spawnNpc("squire", x = 2977, y = 3343)
+
+spawnNpc("sir_tiffy_cashien", x = 2997, y = 3373)
+
+spawnNpc("sir_amik_varze", x = 2960, y = 3336, z = 2)
+
+spawnNpc("sir_vyvin", x = 2983, y = 3335, z = 2)
+
+spawnNpc("shop_keeper", id = 524, x = 2955, y = 3389)
+spawnNpc("shop_assistant", id = 525, x = 2957, y = 3387)
+
+spawnNpc("wayne", x = 2972, y = 3312)
+
+spawnNpc("workman", id = 3236, x = 2975, y = 3369, z = 1)
+
+spawnNpc("wyson_the_gardener", x = 3028, y = 3381)
\ No newline at end of file
diff --git a/game/plugin/locations/falador/src/shops.plugin.kts b/game/plugin/locations/falador/src/shops.plugin.kts
new file mode 100644
index 000000000..5b7446fa9
--- /dev/null
+++ b/game/plugin/locations/falador/src/shops.plugin.kts
@@ -0,0 +1,76 @@
+package org.apollo.plugin.locations.falador
+
+import org.apollo.game.plugin.shops.builder.shop
+
+shop("Falador General Store") {
+ operated by "Shop keeper"(524) and "Shop assistant"( 525)
+ buys any items
+
+ sell(5) of "Pot"
+ sell(2) of "Jug"
+ sell(2) of "Shears"
+ sell(3) of "Bucket"
+ sell(2) of "Bowl"
+ sell(2) of "Cake tin"
+ sell(2) of "Tinderbox"
+ sell(2) of "Chisel"
+ sell(5) of "Hammer"
+ sell(5) of "Newcomer map"
+}
+
+shop("Cassie's Shield Shop") {
+ operated by "Cassie"
+
+ sell(5) of "Wooden shield"
+ sell(3) of "Bronze sq shield"
+ sell(3) of "Bronze kiteshield"
+ sell(2) of "Iron sq shield"
+ sell(0) of "Iron kiteshield"
+ sell(0) of "Steel sq shield"
+ sell(0) of "Steel kiteshield"
+ sell(0) of "Mithril sq shield"
+}
+
+shop("Flynn's Mace Market") {
+ operated by "Flynn"
+
+ category("mace") {
+ sell(5) of "Bronze"
+ sell(4) of "Iron"
+ sell(3) of "Mithril"
+ sell(2) of "Adamant"
+ }
+}
+
+shop("Herquin's Gems") {
+ operated by "Herquin"
+
+ category("uncut", affix = prefix) {
+ sell(1) of "Sapphire"
+ sell(0) of {
+ -"Emerald"
+ -"Ruby"
+ -"Diamond"
+ }
+ }
+
+ sell(1) of "Sapphire"
+ sell(0) of {
+ -"Emerald"
+ -"Ruby"
+ -"Diamond"
+ }
+}
+
+shop("Wayne's Chains - Chainmail Specialist") {
+ operated by "Wayne"
+
+ category("chainbody") {
+ sell(3) of "Bronze"
+ sell(2) of "Iron"
+ sell(1) of "Steel"
+ sell(1) of "Black"
+ sell(1) of "Mithril"
+ sell(1) of "Adamant"
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/locations/lumbridge/build.gradle b/game/plugin/locations/lumbridge/build.gradle
new file mode 100644
index 000000000..7eed7d878
--- /dev/null
+++ b/game/plugin/locations/lumbridge/build.gradle
@@ -0,0 +1,13 @@
+apply plugin: 'kotlin'
+
+
+
+dependencies {
+ implementation project(':game')
+ implementation project(':cache')
+ implementation project(':net')
+ implementation project(':util')
+ implementation project(':game:plugin:entity:spawn')
+ implementation project(':game:plugin:shops')
+ testImplementation project(':game:plugin-testing')
+}
diff --git a/game/plugin/locations/lumbridge/src/npcs.plugin.kts b/game/plugin/locations/lumbridge/src/npcs.plugin.kts
new file mode 100644
index 000000000..03094618d
--- /dev/null
+++ b/game/plugin/locations/lumbridge/src/npcs.plugin.kts
@@ -0,0 +1,15 @@
+package org.apollo.plugin.locations.lumbridge
+
+import org.apollo.game.plugin.entity.spawn.spawnNpc
+
+spawnNpc("woman", id = 4, x = 3232, y = 3207)
+spawnNpc("man", id = 1, x = 3231, y = 3237)
+spawnNpc("man", id = 2, x = 3224, y = 3240)
+spawnNpc("woman", id = 5, x = 3229, y = 2329)
+
+spawnNpc("hans", x = 3221, y = 3221)
+spawnNpc("father aereck", x = 3243, y = 3210)
+spawnNpc("bob", x = 3231, y = 3203)
+spawnNpc("shop keeper", x = 3212, y = 3247)
+spawnNpc("shop assistant", x = 3211, y = 3245)
+spawnNpc("lumbridge guide", x = 323, y = 3229)
diff --git a/game/plugin/locations/lumbridge/src/shops.plugin.kts b/game/plugin/locations/lumbridge/src/shops.plugin.kts
new file mode 100644
index 000000000..942bb62d3
--- /dev/null
+++ b/game/plugin/locations/lumbridge/src/shops.plugin.kts
@@ -0,0 +1,42 @@
+package org.apollo.plugin.locations.lumbridge
+
+import org.apollo.game.plugin.shops.builder.shop
+
+shop("Lumbridge General Store") {
+ operated by "Shop keeper" and "Shop assistant"
+ buys any items
+
+ sell(5) of "Pot"
+ sell(2) of "Jug"
+ sell(2) of "Shears"
+ sell(3) of "Bucket"
+ sell(2) of "Bowl"
+ sell(2) of "Cake tin"
+ sell(2) of "Tinderbox"
+ sell(2) of "Chisel"
+ sell(5) of "Hammer"
+ sell(5) of "Newcomer map"
+}
+
+shop("Bob's Brilliant Axes") {
+ operated by "Bob"
+
+ category("pickaxe") {
+ sell(5) of "Bronze"
+ }
+
+ category("axe") {
+ sell(10) of "Bronze"
+ sell(5) of "Iron"
+ sell(3) of "Steel"
+ }
+
+ category("battleaxe") {
+ sell(5) of "Iron"
+ sell(2) of "Steel"
+ sell(1) of "Mithril"
+ }
+}
+
+// TODO find out how to make objects be able to open stores for the Culinaromancer's Chest. Also links to TODO in
+// Al-Kharid's shops plugin for "unlockable" items.
\ No newline at end of file
diff --git a/game/plugin/locations/tutorial-island/build.gradle b/game/plugin/locations/tutorial-island/build.gradle
new file mode 100644
index 000000000..24e7bbaed
--- /dev/null
+++ b/game/plugin/locations/tutorial-island/build.gradle
@@ -0,0 +1,10 @@
+apply plugin: 'kotlin'
+
+dependencies {
+ implementation project(':game')
+ implementation project(':cache')
+ implementation project(':net')
+ implementation project(':util')
+ implementation project(':game:plugin:entity:spawn')
+ testImplementation project(':game:plugin-testing')
+}
diff --git a/game/plugin/locations/tutorial-island/src/npcs.plugin.kts b/game/plugin/locations/tutorial-island/src/npcs.plugin.kts
new file mode 100644
index 000000000..5ec51c09b
--- /dev/null
+++ b/game/plugin/locations/tutorial-island/src/npcs.plugin.kts
@@ -0,0 +1,44 @@
+package org.apollo.plugin.locations.tutorialIsland
+
+import org.apollo.game.model.Direction
+import org.apollo.game.plugin.entity.spawn.spawnNpc
+
+// Functional npcs
+
+// 'Above-ground' npcs
+
+spawnNpc("master_chef", x = 3076, y = 3085)
+spawnNpc("quest_guide", x = 3086, y = 3122)
+spawnNpc("financial_advisor", x = 3127, y = 3124, facing = Direction.WEST)
+spawnNpc("brother_brace", x = 3124, y = 3107, facing = Direction.EAST)
+spawnNpc("magic_instructor", x = 3140, y = 3085)
+
+// 'Below-ground' npcs
+// Note: They aren't actually on a different plane, they're just in a different location that
+// pretends to be underground.
+
+spawnNpc("mining_instructor", x = 3081, y = 9504)
+spawnNpc("combat_instructor", x = 3104, y = 9506)
+
+// Non-humanoid npcs
+
+spawnNpc("fishing_spot", id = 316, x = 3102, y = 3093)
+
+spawnNpc("chicken", x = 3140, y = 3095)
+spawnNpc("chicken", x = 3140, y = 3093)
+spawnNpc("chicken", x = 3138, y = 3092)
+spawnNpc("chicken", x = 3137, y = 3094)
+spawnNpc("chicken", x = 3138, y = 3095)
+
+// 'Below-ground' npcs
+// Note: They aren't actually on a different plane, they're just in a different location that
+// pretends to be underground.
+
+spawnNpc("giant_rat", id = 87, x = 3105, y = 9514)
+spawnNpc("giant_rat", id = 87, x = 3105, y = 9517)
+spawnNpc("giant_rat", id = 87, x = 3106, y = 9514)
+spawnNpc("giant_rat", id = 87, x = 3104, y = 9514)
+spawnNpc("giant_rat", id = 87, x = 3105, y = 9519)
+spawnNpc("giant_rat", id = 87, x = 3109, y = 9516)
+spawnNpc("giant_rat", id = 87, x = 3108, y = 9520)
+spawnNpc("giant_rat", id = 87, x = 3102, y = 9517)
\ No newline at end of file
diff --git a/game/plugin/locations/varrock/build.gradle b/game/plugin/locations/varrock/build.gradle
new file mode 100644
index 000000000..6a06d7334
--- /dev/null
+++ b/game/plugin/locations/varrock/build.gradle
@@ -0,0 +1,11 @@
+apply plugin: 'kotlin'
+
+dependencies {
+ implementation project(':game')
+ implementation project(':cache')
+ implementation project(':net')
+ implementation project(':util')
+ implementation project(':game:plugin:entity:spawn')
+ implementation project(':game:plugin:shops')
+ testImplementation project(':game:plugin-testing')
+}
diff --git a/game/plugin/locations/varrock/src/npcs.plugin.kts b/game/plugin/locations/varrock/src/npcs.plugin.kts
new file mode 100644
index 000000000..83d5fb9ec
--- /dev/null
+++ b/game/plugin/locations/varrock/src/npcs.plugin.kts
@@ -0,0 +1,268 @@
+package org.apollo.plugin.locations.varrock
+
+import org.apollo.game.model.Direction
+import org.apollo.game.plugin.entity.spawn.spawnNpc
+
+spawnNpc("barbarian_woman", x = 3222, y = 3399)
+
+spawnNpc("bear", id = 106, x = 3289, y = 3351)
+
+spawnNpc("black_knight", x = 3238, y = 3514)
+spawnNpc("black_knight", x = 3227, y = 3518)
+spawnNpc("black_knight", x = 3279, y = 3502)
+
+spawnNpc("dark_wizard", id = 174, x = 3230, y = 3366)
+
+spawnNpc("dark_wizard", id = 174, x = 3228, y = 3368)
+spawnNpc("dark_wizard", id = 174, x = 3225, y = 3367)
+spawnNpc("dark_wizard", id = 174, x = 3226, y = 3365)
+spawnNpc("dark_wizard", id = 174, x = 3226, y = 3372)
+spawnNpc("dark_wizard", id = 174, x = 3231, y = 3371)
+
+spawnNpc("dark_wizard", id = 172, x = 3229, y = 3372)
+spawnNpc("dark_wizard", id = 172, x = 3224, y = 3370)
+spawnNpc("dark_wizard", id = 172, x = 3228, y = 3366)
+spawnNpc("dark_wizard", id = 172, x = 3232, y = 3368)
+spawnNpc("dark_wizard", id = 172, x = 3226, y = 3369)
+
+spawnNpc("giant_rat", id = 87, x = 3292, y = 3375)
+spawnNpc("giant_rat", id = 87, x = 3265, y = 3384)
+spawnNpc("giant_rat", id = 87, x = 3267, y = 3381)
+
+spawnNpc("guard", id = 368, x = 3263, y = 3407, facing = Direction.SOUTH)
+
+spawnNpc("jeremy_clerksin", x = 3253, y = 3477)
+spawnNpc("martina_scorsby", x = 3256, y = 3481)
+
+spawnNpc("man", x = 3281, y = 3500)
+spawnNpc("man", x = 3193, y = 3394)
+spawnNpc("man", x = 3159, y = 3429)
+spawnNpc("man", x = 3245, y = 3394)
+spawnNpc("man", x = 3283, y = 3492, z = 1)
+
+spawnNpc("man", id = 2, x = 3283, y = 3492, z = 1)
+spawnNpc("man", id = 2, x = 3263, y = 3400)
+
+spawnNpc("man", id = 3, x = 3227, y = 3395, z = 1)
+spawnNpc("man", id = 3, x = 3231, y = 3399, z = 1)
+
+spawnNpc("mugger", x = 3251, y = 3390)
+spawnNpc("mugger", x = 3177, y = 3363)
+
+spawnNpc("tramp", id = 2792, x = 3177, y = 3363)
+
+spawnNpc("woman", x = 3221, y = 3396)
+
+spawnNpc("woman", id = 5, x = 3279, y = 3497)
+spawnNpc("woman", id = 25, x = 3278, y = 3492)
+
+spawnNpc("thief", x = 3285, y = 3500)
+spawnNpc("thief", x = 3234, y = 3389)
+spawnNpc("thief", x = 3188, y = 3383)
+spawnNpc("thief", x = 3184, y = 3390)
+spawnNpc("thief", x = 3188, y = 3394)
+
+spawnNpc("unicorn", x = 3286, y = 3342)
+spawnNpc("unicorn", x = 3279, y = 3345)
+
+// North Guards
+
+spawnNpc("guard", x = 3244, y = 3500)
+spawnNpc("guard", x = 3247, y = 3503)
+
+// East Guards
+
+spawnNpc("guard", x = 3271, y = 3431)
+spawnNpc("guard", x = 3270, y = 3425)
+spawnNpc("guard", x = 3274, y = 3421)
+spawnNpc("guard", x = 3274, y = 3427)
+
+// South Guards
+
+spawnNpc("guard", x = 3210, y = 3382)
+spawnNpc("guard", x = 3212, y = 3380)
+spawnNpc("guard", x = 3207, y = 3376)
+
+// West Guards
+
+spawnNpc("guard", x = 3174, y = 3427)
+spawnNpc("guard", x = 3176, y = 3430)
+spawnNpc("guard", x = 3176, y = 3427)
+spawnNpc("guard", x = 3180, y = 3399)
+spawnNpc("guard", x = 3175, y = 3415, z = 1)
+spawnNpc("guard", x = 3174, y = 3403, z = 1)
+
+// Varrock Palace
+
+spawnNpc("guard", x = 3210, y = 3461)
+spawnNpc("guard", x = 3211, y = 3465)
+spawnNpc("guard", x = 3214, y = 3462)
+spawnNpc("guard", x = 3216, y = 3464)
+spawnNpc("guard", x = 3220, y = 3461)
+spawnNpc("guard", x = 3206, y = 3461)
+spawnNpc("guard", x = 3204, y = 3495)
+
+spawnNpc("guard", x = 3204, y = 3495, z = 1)
+spawnNpc("guard", x = 3205, y = 3492, z = 1)
+spawnNpc("guard", x = 3203, y = 3492, z = 1)
+spawnNpc("guard", x = 3205, y = 3497, z = 1)
+
+spawnNpc("guard", x = 3221, y = 3471, z = 2)
+spawnNpc("guard", x = 3214, y = 3474, z = 2)
+spawnNpc("guard", x = 3215, y = 3471, z = 2)
+spawnNpc("guard", x = 3211, y = 3471, z = 2)
+spawnNpc("guard", x = 3209, y = 3473, z = 2)
+spawnNpc("guard", x = 3212, y = 3475, z = 2)
+spawnNpc("guard", x = 3207, y = 3477, z = 2)
+spawnNpc("guard", x = 3203, y = 3476, z = 2)
+spawnNpc("guard", x = 3205, y = 3479, z = 2)
+spawnNpc("guard", x = 3203, y = 3483, z = 2)
+spawnNpc("guard", x = 3221, y = 3485, z = 2)
+
+spawnNpc("monk_of_zamorak", id = 189, x = 3213, y = 3476)
+
+spawnNpc("warrior_woman", x = 3203, y = 3490)
+spawnNpc("warrior_woman", x = 3205, y = 3493)
+
+// Varrock/Lumbridge Pen
+
+spawnNpc("swan", x = 3261, y = 3354)
+spawnNpc("swan", x = 3260, y = 3356)
+
+spawnNpc("ram", id = 3673, x = 3238, y = 3346)
+spawnNpc("ram", id = 3673, x = 3248, y = 3352)
+spawnNpc("ram", id = 3673, x = 3260, y = 3348)
+
+spawnNpc("sheep", id = 42, x = 3263, y = 3347)
+spawnNpc("sheep", id = 42, x = 3268, y = 3350)
+spawnNpc("sheep", id = 42, x = 3252, y = 3352)
+spawnNpc("sheep", id = 42, x = 3243, y = 3344)
+spawnNpc("sheep", id = 42, x = 3235, y = 3347)
+
+spawnNpc("sheep", id = 3579, x = 3234, y = 3344)
+spawnNpc("sheep", id = 3579, x = 3241, y = 3347)
+spawnNpc("sheep", id = 3579, x = 3257, y = 3350)
+
+// Champions Guild
+
+spawnNpc("chicken", x = 3195, y = 3359)
+spawnNpc("chicken", x = 3198, y = 3356)
+spawnNpc("chicken", x = 3195, y = 3355)
+
+spawnNpc("chicken", id = 1017, x = 3196, y = 3353)
+spawnNpc("chicken", id = 1017, x = 3197, y = 3356)
+
+spawnNpc("evil_chicken", x = 3198, y = 3359)
+
+// Function Npc
+
+spawnNpc("apothecary", x = 3196, y = 3403)
+
+spawnNpc("captain_rovin", x = 3204, y = 3496, z = 2)
+
+spawnNpc("curator", x = 3256, y = 3447)
+
+spawnNpc("dimintheis", x = 3280, y = 3403)
+
+spawnNpc("dr_harlow", x = 3224, y = 3398)
+
+spawnNpc("ellamaria", x = 3228, y = 3475)
+
+spawnNpc("father_lawrence", x = 3253, y = 3484)
+
+spawnNpc("guidors_wife", id = 342, x = 3280, y = 3382)
+
+spawnNpc("guidor", x = 3284, y = 3381, facing = Direction.SOUTH)
+
+spawnNpc("guild_master", x = 3189, y = 3360)
+
+spawnNpc("gypsy", x = 3203, y = 3423)
+
+spawnNpc("hooknosed_jack", x = 3268, y = 3400)
+
+spawnNpc("jonny_the_beard", x = 3223, y = 3395)
+
+spawnNpc("johnathon", x = 3278, y = 3503, z = 1)
+
+spawnNpc("katrine", x = 3185, y = 3386)
+
+spawnNpc("king_roald", x = 3223, y = 3473)
+
+spawnNpc("master_farmer", x = 3243, y = 3349)
+
+spawnNpc("pox", x = 3267, y = 3399)
+
+spawnNpc("reldo", x = 3210, y = 3492)
+
+spawnNpc("romeo", x = 3211, y = 3423)
+
+spawnNpc("shilop", x = 3211, y = 3435)
+
+spawnNpc("sir_prysin", x = 3204, y = 3472)
+
+spawnNpc("tarquin", x = 3203, y = 3344, facing = Direction.SOUTH)
+
+spawnNpc("tool_leprechaun", x = 3182, y = 3355)
+
+spawnNpc("tool_leprechaun", x = 3229, y = 3455)
+
+spawnNpc("tramp", id = 641, x = 3207, y = 3392)
+
+spawnNpc("wilough", x = 3222, y = 3437)
+
+// Shop Npc
+
+spawnNpc("aubury", x = 3253, y = 3401)
+
+spawnNpc("baraek", x = 3217, y = 3434)
+
+spawnNpc("bartender", x = 3226, y = 3400)
+
+spawnNpc("bartender", id = 1921, x = 3277, y = 3487)
+
+spawnNpc("fancy_dress_shop_owner", x = 3281, y = 3398)
+
+spawnNpc("horvik", x = 3229, y = 3438)
+
+spawnNpc("lowe", x = 3233, y = 3421)
+
+spawnNpc("scavvo", x = 3192, y = 3353, z = 1)
+
+spawnNpc("shop_keeper", id = 551, x = 3206, y = 3399)
+spawnNpc("shop_assistant", id = 552, x = 3207, y = 3396)
+
+spawnNpc("shop_keeper", id = 522, x = 3216, y = 3414)
+spawnNpc("shop_assistant", id = 523, x = 3216, y = 3417)
+
+spawnNpc("tea_seller", x = 3271, y = 3411)
+
+spawnNpc("thessalia", x = 3206, y = 3417)
+
+spawnNpc("zaff", x = 3203, y = 3434)
+
+// Juliet House
+
+spawnNpc("draul_leptoc", x = 3228, y = 3475)
+spawnNpc("juliet", x = 3159, y = 3425, z = 1)
+spawnNpc("phillipa", x = 3160, y = 3429, z = 1)
+
+// Gertrude House
+
+spawnNpc("gertrude", x = 3153, y = 3413)
+spawnNpc("kanel", x = 3155, y = 3405, facing = Direction.EAST)
+spawnNpc("philop", x = 3150, y = 3405, facing = Direction.SOUTH)
+
+// Small Bank
+
+spawnNpc("banker", id = 495, x = 3252, y = 3418)
+spawnNpc("banker", id = 494, x = 3252, y = 3418)
+spawnNpc("banker", id = 494, x = 3252, y = 3418)
+spawnNpc("banker", id = 494, x = 3252, y = 3418)
+
+// Big Bank
+
+spawnNpc("banker", id = 494, x = 3187, y = 3436, facing = Direction.WEST)
+spawnNpc("banker", id = 494, x = 3187, y = 3440, facing = Direction.WEST)
+spawnNpc("banker", id = 494, x = 3187, y = 3444, facing = Direction.WEST)
+spawnNpc("banker", id = 495, x = 3187, y = 3438, facing = Direction.WEST)
+spawnNpc("banker", id = 495, x = 3187, y = 3442, facing = Direction.WEST)
\ No newline at end of file
diff --git a/game/plugin/locations/varrock/src/shops.plugin.kts b/game/plugin/locations/varrock/src/shops.plugin.kts
new file mode 100644
index 000000000..cc3d14881
--- /dev/null
+++ b/game/plugin/locations/varrock/src/shops.plugin.kts
@@ -0,0 +1,176 @@
+package org.apollo.plugin.locations.varrock
+
+import org.apollo.game.plugin.shops.builder.shop
+
+shop("Aubury's Rune Shop.") {
+ operated by "Aubury"
+
+ category("runes") {
+ sell(5000) of {
+ -"Earth"
+ -"Water"
+ -"Fire"
+ -"Air"
+ -"Mind"
+ -"Body"
+ }
+
+ sell(250) of {
+ -"Chaos"
+ -"Death"
+ }
+ }
+}
+
+shop("Lowe's Archery Emporium.") {
+ operated by "Lowe"
+
+ category("arrows") {
+ sell(2000) of "Bronze"
+ sell(1500) of "Iron"
+ sell(1000) of "Steel"
+ sell(800) of "Mithril"
+ sell(600) of "Adamant"
+ }
+
+ category("normal weapons", affix = nothing) {
+ sell(4) of "Shortbow"
+ sell(4) of "Longbow"
+ sell(2) of "Crossbow"
+ }
+
+ category("shortbows") {
+ sell(3) of "Oak"
+ sell(2) of "Willow"
+ sell(1) of "Maple"
+ }
+
+ category("longbows") {
+ sell(3) of "Oak"
+ sell(2) of "Willow"
+ sell(1) of "Maple"
+ }
+}
+
+shop("Horvik's Armour Shop.") {
+ operated by "Horvik"
+
+ category("chainbody") {
+ sell(5) of "Bronze"
+ sell(3) of "Iron"
+ sell(3) of "Steel"
+ sell(1) of "Mithril"
+ }
+
+ category("platebody") {
+ sell(3) of "Bronze"
+
+ sell(1) of {
+ -"Iron"
+ -"Steel"
+ -"Black"
+ -"Mithril"
+ }
+ }
+
+ sell(1) of {
+ -"Iron platelegs"
+ -"Studded body"
+ -"Studded chaps"
+ }
+}
+
+shop("Thessalia's Fine Clothes.") {
+ operated by "Thessalia"
+
+ category("apron") {
+ sell(3) of "White"
+ sell(1) of "Brown"
+ }
+
+ category("leather", affix = prefix) {
+ sell(12) of "Body"
+ sell(10) of "Gloves"
+ sell(10) of "Boots"
+ }
+
+ category("skirt") {
+ sell(5) of "Pink"
+ sell(3) of "Black"
+ sell(2) of "Blue"
+ }
+
+ sell(4) of "Cape"
+ sell(5) of "Silk"
+
+ sell(3) of {
+ -"Priest gown"(426)
+ -"Priest gown"(428)
+ }
+}
+
+shop("Varrock General Store.") {
+ operated by "Shopkeeper"(522) and "Shop assistant"(523)
+ buys any items
+
+ sell(5) of "Pot"
+ sell(2) of "Jug"
+ sell(2) of "Shears"
+ sell(3) of "Bucket"
+ sell(2) of "Bowl"
+ sell(2) of "Cake tin"
+ sell(2) of "Tinderbox"
+ sell(2) of "Chisel"
+ sell(5) of "Hammer"
+ sell(5) of "Newcomer map"
+}
+
+shop("Varrock Swordshop.") {
+ operated by "Shopkeeper"(551) and "Shop assistant"(552)
+
+ category("swords") {
+ sell(5) of "Bronze"
+ sell(4) of "Iron"
+ sell(4) of "Steel"
+ sell(3) of "Black"
+ sell(3) of "Mithril"
+ sell(2) of "Adamant"
+ }
+
+ category("longswords") {
+ sell(4) of "Bronze"
+ sell(3) of "Iron"
+ sell(3) of "Steel"
+ sell(2) of "Black"
+ sell(2) of "Mithril"
+ sell(1) of "Adamant"
+ }
+
+ category("daggers") {
+ sell(10) of "Bronze"
+ sell(6) of "Iron"
+ sell(5) of "Steel"
+ sell(4) of "Black"
+ sell(3) of "Mithril"
+ sell(2) of "Adamant"
+ }
+}
+
+shop("Zaff's Superior Staffs!") {
+ operated by "Zaff"
+
+ category("staves", affix = nothing) {
+ sell(5) of {
+ -"Battlestaff"
+ -"Staff"
+ -"Magic staff"
+ }
+
+ sell(2) of {
+ -"Staff of air"
+ -"Staff of water"
+ -"Staff of earth"
+ -"Staff of fire"
+ }
+ }
+}
diff --git a/game/plugin/logout/build.gradle b/game/plugin/logout/build.gradle
new file mode 100644
index 000000000..3a9351392
--- /dev/null
+++ b/game/plugin/logout/build.gradle
@@ -0,0 +1,10 @@
+apply plugin: 'kotlin'
+
+dependencies {
+ implementation project(':game')
+ implementation project(':cache')
+ implementation project(':net')
+ implementation project(':util')
+
+ testImplementation project(':game:plugin-testing')
+}
diff --git a/game/plugin/logout/src/logout.plugin.kts b/game/plugin/logout/src/logout.plugin.kts
new file mode 100644
index 000000000..0243b091f
--- /dev/null
+++ b/game/plugin/logout/src/logout.plugin.kts
@@ -0,0 +1,5 @@
+val LOGOUT_BUTTON_ID = 2458
+
+on_button(LOGOUT_BUTTON_ID)
+ .where { widgetId == LOGOUT_BUTTON_ID }
+ .then { it.logout() }
\ No newline at end of file
diff --git a/game/plugin/logout/test/LogoutTests.kt b/game/plugin/logout/test/LogoutTests.kt
new file mode 100644
index 000000000..9a8cb9c66
--- /dev/null
+++ b/game/plugin/logout/test/LogoutTests.kt
@@ -0,0 +1,25 @@
+import io.mockk.verify
+import org.apollo.game.message.impl.ButtonMessage
+import org.apollo.game.model.entity.Player
+import org.apollo.game.plugin.testing.junit.ApolloTestingExtension
+import org.apollo.game.plugin.testing.junit.api.annotations.TestMock
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+
+@ExtendWith(ApolloTestingExtension::class)
+class LogoutTests {
+
+ companion object {
+ const val LOGOUT_BUTTON_ID = 2458
+ }
+
+ @TestMock
+ lateinit var player: Player
+
+ @Test
+ fun `The player should be logged out when they click the logout button`() {
+ player.send(ButtonMessage(LOGOUT_BUTTON_ID))
+
+ verify { player.logout() }
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/navigation/door/build.gradle b/game/plugin/navigation/door/build.gradle
new file mode 100644
index 000000000..4d96c9f57
--- /dev/null
+++ b/game/plugin/navigation/door/build.gradle
@@ -0,0 +1,10 @@
+apply plugin: 'kotlin'
+
+dependencies {
+ implementation project(':game')
+ implementation project(':cache')
+ implementation project(':net')
+ implementation project(':util')
+ implementation project(':game:plugin:api')
+ testImplementation project(':game:plugin-testing')
+}
diff --git a/game/plugin/navigation/door/src/door.kt b/game/plugin/navigation/door/src/door.kt
new file mode 100644
index 000000000..59ee13256
--- /dev/null
+++ b/game/plugin/navigation/door/src/door.kt
@@ -0,0 +1,157 @@
+package org.apollo.plugin.navigation.door
+
+import java.util.*
+import org.apollo.game.action.DistancedAction
+import org.apollo.game.model.Direction
+import org.apollo.game.model.Position
+import org.apollo.game.model.World
+import org.apollo.game.model.entity.Player
+import org.apollo.game.model.entity.obj.DynamicGameObject
+import org.apollo.game.model.entity.obj.GameObject
+import org.apollo.game.model.event.PlayerEvent
+import org.apollo.game.plugin.api.findObject
+import org.apollo.net.message.Message
+
+enum class DoorType {
+ LEFT, RIGHT, NOT_SUPPORTED
+}
+
+class Door(private val gameObject: GameObject) {
+
+ companion object {
+
+ val LEFT_HINGE_ORIENTATION: HashMap = hashMapOf(
+ Direction.NORTH to Direction.WEST,
+ Direction.SOUTH to Direction.EAST,
+ Direction.WEST to Direction.SOUTH,
+ Direction.EAST to Direction.NORTH
+ )
+
+ val RIGHT_HINGE_ORIENTATION: HashMap = hashMapOf(
+ Direction.NORTH to Direction.EAST,
+ Direction.SOUTH to Direction.WEST,
+ Direction.WEST to Direction.NORTH,
+ Direction.EAST to Direction.SOUTH
+ )
+
+ val toggledDoors = hashMapOf()
+
+ val LEFT_HINGED = setOf(1516, 1536, 1533)
+
+ val RIGHT_HINGED = setOf(1519, 1530, 4465, 4467, 3014, 3017, 3018, 3019)
+
+ /**
+ * Find a given door in the world
+ * @param world The [World] the door lives in
+ * @param position The [Position] of the door
+ * @param objectId The [GameObject] id of the door
+ */
+ fun find(world: World, position: Position, objectId: Int): Door? {
+ return world.findObject(position, objectId)?.let(::Door)
+ }
+ }
+
+ /**
+ * Returns the supported doors by the system
+ * See [DoorType]
+ */
+ fun supported(): Boolean {
+ return type() !== DoorType.NOT_SUPPORTED
+ }
+
+ /**
+ * Computes the given door type by which id exists in
+ * the supported left and right hinged doors
+ */
+ fun type(): DoorType {
+ return when {
+ gameObject.id in LEFT_HINGED -> DoorType.LEFT
+ gameObject.id in RIGHT_HINGED -> DoorType.RIGHT
+ else -> DoorType.NOT_SUPPORTED
+ }
+ }
+
+ /**
+ * Toggles a given [GameObject] orientation and position
+ * Stores the door state in toggleDoors class variable
+ */
+ fun toggle() {
+ val world = gameObject.world
+ val regionRepository = world.regionRepository
+
+ regionRepository.fromPosition(gameObject.position).removeEntity(gameObject)
+
+ val originalDoor = toggledDoors[gameObject]
+
+ if (originalDoor == null) {
+ val position = movePosition()
+ val orientation: Int = translateDirection()?.toOrientationInteger() ?: gameObject.orientation
+
+ val toggledDoor = DynamicGameObject.createPublic(world, gameObject.id, position, gameObject.type,
+ orientation)
+
+ regionRepository.fromPosition(position).addEntity(toggledDoor)
+ toggledDoors[toggledDoor] = gameObject
+ } else {
+ toggledDoors.remove(gameObject)
+ regionRepository.fromPosition(originalDoor.position).addEntity(originalDoor)
+ }
+ }
+
+ /**
+ * Calculates the position to move the door based on orientation
+ */
+ private fun movePosition(): Position {
+ return gameObject.position.step(1, Direction.WNES[gameObject.orientation])
+ }
+
+ /**
+ * Calculates the orientation of the door based on
+ * if it is right or left hinged door
+ */
+ private fun translateDirection(): Direction? {
+ val direction = Direction.WNES[gameObject.orientation]
+
+ return when (type()) {
+ DoorType.LEFT -> LEFT_HINGE_ORIENTATION[direction]
+ DoorType.RIGHT -> RIGHT_HINGE_ORIENTATION[direction]
+ DoorType.NOT_SUPPORTED -> null
+ }
+ }
+}
+
+class OpenDoorAction(private val player: Player, private val door: Door, position: Position) : DistancedAction(
+ 0, true, player, position, DISTANCE) {
+
+ companion object {
+
+ /**
+ * The distance threshold that must be reached before the door is opened.
+ */
+ const val DISTANCE = 1
+
+ /**
+ * Starts a [OpenDoorAction] for the specified [Player], terminating the [Message] that triggered.
+ */
+ fun start(message: Message, player: Player, door: Door, position: Position) {
+ player.startAction(OpenDoorAction(player, door, position))
+ message.terminate()
+ }
+ }
+
+ override fun executeAction() {
+ if (player.world.submit(OpenDoorEvent(player))) {
+ player.turnTo(position)
+ door.toggle()
+ }
+ stop()
+ }
+
+ override fun equals(other: Any?): Boolean {
+ return other is OpenDoorAction && position == other.position && player == other.player
+ }
+
+ override fun hashCode(): Int = Objects.hash(position, player)
+}
+
+class OpenDoorEvent(player: Player) : PlayerEvent(player)
diff --git a/game/plugin/navigation/door/src/door.plugin.kts b/game/plugin/navigation/door/src/door.plugin.kts
new file mode 100644
index 000000000..d91ceee9a
--- /dev/null
+++ b/game/plugin/navigation/door/src/door.plugin.kts
@@ -0,0 +1,16 @@
+
+import org.apollo.game.message.impl.ObjectActionMessage
+import org.apollo.plugin.navigation.door.Door
+import org.apollo.plugin.navigation.door.OpenDoorAction
+
+/**
+ * Hook into the [ObjectActionMessage] and listens for a supported door [GameObject]
+ */
+on { ObjectActionMessage::class }
+ .where { option == 1 }
+ .then {
+ val door = Door.find(it.world, position, id) ?: return@then
+ if (door.supported()) {
+ OpenDoorAction.start(this, it, door, position)
+ }
+ }
\ No newline at end of file
diff --git a/game/plugin/run/build.gradle b/game/plugin/run/build.gradle
new file mode 100644
index 000000000..3a9351392
--- /dev/null
+++ b/game/plugin/run/build.gradle
@@ -0,0 +1,10 @@
+apply plugin: 'kotlin'
+
+dependencies {
+ implementation project(':game')
+ implementation project(':cache')
+ implementation project(':net')
+ implementation project(':util')
+
+ testImplementation project(':game:plugin-testing')
+}
diff --git a/game/plugin/run/src/run.plugin.kts b/game/plugin/run/src/run.plugin.kts
new file mode 100644
index 000000000..fa65f0495
--- /dev/null
+++ b/game/plugin/run/src/run.plugin.kts
@@ -0,0 +1,10 @@
+import org.apollo.game.message.impl.ButtonMessage
+
+val WALK_BUTTON_ID = 152
+val RUN_BUTTON_ID = 153
+
+on { ButtonMessage::class }
+ .where { widgetId == WALK_BUTTON_ID || widgetId == RUN_BUTTON_ID }
+ .then {
+ it.toggleRunning()
+ }
\ No newline at end of file
diff --git a/game/plugin/shops/build.gradle b/game/plugin/shops/build.gradle
new file mode 100644
index 000000000..4d96c9f57
--- /dev/null
+++ b/game/plugin/shops/build.gradle
@@ -0,0 +1,10 @@
+apply plugin: 'kotlin'
+
+dependencies {
+ implementation project(':game')
+ implementation project(':cache')
+ implementation project(':net')
+ implementation project(':util')
+ implementation project(':game:plugin:api')
+ testImplementation project(':game:plugin-testing')
+}
diff --git a/game/plugin/shops/src/org/apollo/game/plugin/shops/Currency.kt b/game/plugin/shops/src/org/apollo/game/plugin/shops/Currency.kt
new file mode 100644
index 000000000..6548f4d30
--- /dev/null
+++ b/game/plugin/shops/src/org/apollo/game/plugin/shops/Currency.kt
@@ -0,0 +1,26 @@
+package org.apollo.game.plugin.shops
+
+import org.apollo.game.plugin.api.Definitions
+
+/**
+ * A [Shop]'s method of payment.
+ *
+ * @param id The item id of the currency.
+ * @param plural Whether or not the name of this currency is plural.
+ */
+data class Currency(val id: Int, val plural: Boolean = false) {
+
+ val name = requireNotNull(Definitions.item(id).name?.toLowerCase()) { "Currencies must have a name." }
+
+ fun name(amount: Int): String {
+ return when {
+ amount == 1 && plural -> name.removeSuffix("s")
+ else -> name
+ }
+ }
+
+ companion object {
+ val COINS = Currency(995, plural = true)
+ }
+
+}
\ No newline at end of file
diff --git a/game/plugin/shops/src/org/apollo/game/plugin/shops/OpenShopAction.kt b/game/plugin/shops/src/org/apollo/game/plugin/shops/OpenShopAction.kt
new file mode 100644
index 000000000..a87a31256
--- /dev/null
+++ b/game/plugin/shops/src/org/apollo/game/plugin/shops/OpenShopAction.kt
@@ -0,0 +1,69 @@
+package org.apollo.game.plugin.shops
+
+import org.apollo.game.action.DistancedAction
+import org.apollo.game.message.handler.ItemVerificationHandler
+import org.apollo.game.message.impl.SetWidgetTextMessage
+import org.apollo.game.model.entity.Mob
+import org.apollo.game.model.entity.Player
+import org.apollo.game.model.inter.InterfaceListener
+import org.apollo.game.model.inv.Inventory
+import org.apollo.game.model.inv.SynchronizationInventoryListener
+
+/**
+ * A [DistancedAction] that opens a [Shop].
+ */
+class OpenShopAction(
+ player: Player,
+ private val shop: Shop,
+ private val operator: Mob
+) : DistancedAction(0, true, player, operator.position, 1) { // TODO this needs to follow the NPC if they move
+
+ override fun executeAction() {
+ mob.interactingMob = operator
+
+ val closeListener = addInventoryListeners(mob, shop.inventory)
+ mob.send(SetWidgetTextMessage(ShopInterfaces.SHOP_NAME, shop.name))
+
+ mob.interfaceSet.openWindowWithSidebar(closeListener, ShopInterfaces.SHOP_WINDOW,
+ ShopInterfaces.INVENTORY_SIDEBAR)
+ stop()
+ }
+
+ /**
+ * Adds [SynchronizationInventoryListener]s to the [Player] and [Shop] [Inventories][Inventory], returning an
+ * [InterfaceListener] that removes them when the interface is closed.
+ */
+ private fun addInventoryListeners(player: Player, shop: Inventory): InterfaceListener {
+ val invListener = SynchronizationInventoryListener(player, ShopInterfaces.INVENTORY_CONTAINER)
+ val shopListener = SynchronizationInventoryListener(player, ShopInterfaces.SHOP_CONTAINER)
+
+ player.inventory.addListener(invListener)
+ player.inventory.forceRefresh()
+
+ shop.addListener(shopListener)
+ shop.forceRefresh()
+
+ return InterfaceListener {
+ mob.interfaceSet.close()
+ mob.resetInteractingMob()
+
+ mob.inventory.removeListener(invListener)
+ shop.removeListener(shopListener)
+ }
+ }
+}
+
+/**
+ * An [InventorySupplier] that returns a [Player]'s [Inventory] if they are browsing a shop.
+ */
+object PlayerInventorySupplier : ItemVerificationHandler.InventorySupplier {
+
+ override fun getInventory(player: Player): Inventory? {
+ return if (Interfaces.SHOP_WINDOW in player.interfaceSet) {
+ player.inventory
+ } else {
+ null
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/game/plugin/shops/src/org/apollo/game/plugin/shops/Shop.kt b/game/plugin/shops/src/org/apollo/game/plugin/shops/Shop.kt
new file mode 100644
index 000000000..08f9014c3
--- /dev/null
+++ b/game/plugin/shops/src/org/apollo/game/plugin/shops/Shop.kt
@@ -0,0 +1,312 @@
+package org.apollo.game.plugin.shops
+
+import org.apollo.cache.def.ItemDefinition
+import org.apollo.game.model.Item
+import org.apollo.game.model.entity.Player
+import org.apollo.game.model.inv.Inventory
+import org.apollo.game.model.inv.Inventory.StackMode.STACK_ALWAYS
+import org.apollo.game.plugin.shops.Shop.Companion.ExchangeType.BUYING
+import org.apollo.game.plugin.shops.Shop.Companion.ExchangeType.SELLING
+import org.apollo.game.plugin.shops.Shop.PurchasePolicy.ANY
+import org.apollo.game.plugin.shops.Shop.PurchasePolicy.NOTHING
+import org.apollo.game.plugin.shops.Shop.PurchasePolicy.OWNED
+
+/**
+ * Contains shop-related interface ids.
+ */
+object Interfaces {
+
+ /**
+ * The container interface id for the player's inventory.
+ */
+ const val INVENTORY_CONTAINER = 3823
+
+ /**
+ * The sidebar id for the inventory, when a Shop window is open.
+ */
+ const val INVENTORY_SIDEBAR = 3822
+
+ /**
+ * The shop window interface id.
+ */
+ const val SHOP_WINDOW = 3824
+
+ /**
+ * The container interface id for the shop's inventory.
+ */
+ const val SHOP_CONTAINER = 3900
+
+ /**
+ * The id of the text widget that displays a shop's name.
+ */
+ const val SHOP_NAME = 3901
+}
+
+/**
+ * The [Map] from npc ids to [Shop]s.
+ */
+val SHOPS = mutableMapOf()
+
+/**
+ * An in-game shop, operated by one or more npcs.
+ *
+ * @param name The name of the shop.
+ * @param action The id of the NpcActionMessage sent (by the client) when a player opens this shop.
+ * @param sells The [Map] from item id to amount sold.
+ * @param operators The [List] of Npc ids that can open this shop.
+ * @param currency The [Currency] used when making exchanges with this [Shop].
+ * @param purchases This [Shop]'s attitude towards purchasing items from players.
+ */
+class Shop(
+ val name: String,
+ val action: Int,
+ private val sells: Map,
+ val operators: List,
+ private val currency: Currency = Currency.COINS,
+ private val purchases: PurchasePolicy = OWNED
+) {
+
+ /**
+ * The [Inventory] containing this [Shop]'s current items.
+ */
+ val inventory = Inventory(CAPACITY, STACK_ALWAYS)
+
+ init {
+ sells.forEach { (id, amount) -> inventory.add(id, amount) }
+ }
+
+ /**
+ * Restocks this [Shop], adding and removing items as necessary to move the stock closer to its initial state.
+ */
+ fun restock() {
+ for (item in inventory.items.filterNotNull()) {
+ val id = item.id
+
+ if (!sells(id) || item.amount > sells[id]!!) {
+ inventory.remove(id)
+ } else if (item.amount < sells[id]!!) {
+ inventory.add(id)
+ }
+ }
+ }
+
+ /**
+ * Sells an item to a [Player].
+ */
+ fun sell(player: Player, slot: Int, option: Int) {
+ val item = inventory.get(slot)
+ val id = item.id
+ val itemCost = value(id, SELLING)
+
+ if (option == VALUATION_OPTION) {
+ val itemId = ItemDefinition.lookup(id).name
+ player.sendMessage("$itemId: currently costs $itemCost ${currency.name(itemCost)}.")
+ return
+ }
+
+ var buying = amount(option)
+ var unavailable = false
+
+ val amount = item.amount
+ if (buying > amount) {
+ buying = amount
+ unavailable = true
+ }
+
+ val stackable = item.definition.isStackable
+ val slotsRequired = when {
+ stackable && player.inventory.contains(id) -> 0
+ !stackable -> buying
+ else -> 1
+ }
+
+ val freeSlots = player.inventory.freeSlots()
+ var full = false
+
+ if (slotsRequired > freeSlots) {
+ buying = freeSlots
+ full = true
+ }
+
+ val totalCost = buying * itemCost
+ val totalCurrency = player.inventory.getAmount(currency.id)
+ var unaffordable = false
+
+ if (totalCost > totalCurrency) {
+ buying = totalCurrency / itemCost
+ unaffordable = true
+ }
+
+ if (buying > 0) {
+ player.inventory.remove(currency.id, totalCost)
+ val remaining = player.inventory.add(id, buying)
+
+ if (remaining > 0) {
+ player.inventory.add(currency.id, remaining * itemCost)
+ }
+
+ if (buying >= amount && sells(id)) {
+ // If the item is from the shop's main stock, set its amount to zero so it can be restocked over time.
+ inventory.set(slot, Item(id, 0))
+ } else {
+ inventory.remove(id, buying - remaining)
+ }
+ }
+
+ val message = when {
+ unaffordable -> "You don't have enough ${currency.name}."
+ full -> "You don't have enough inventory space."
+ unavailable -> "The shop has run out of stock."
+ else -> return
+ }
+
+ player.sendMessage(message)
+ }
+
+ /**
+ * Purchases the item from the specified [Player].
+ */
+ fun buy(seller: Player, slot: Int, option: Int) {
+ val player = seller.inventory
+ val id = player.get(slot).id
+
+ if (!verifyPurchase(seller, id)) {
+ return
+ }
+
+ val value = value(id, BUYING)
+ if (option == VALUATION_OPTION) {
+ seller.sendMessage("${ItemDefinition.lookup(id).name}: shop will buy for $value ${currency.name(value)}.")
+ return
+ }
+
+ val amount = Math.min(player.getAmount(id), amount(option))
+
+ player.remove(id, amount)
+ inventory.add(id, amount)
+
+ if (value != 0) {
+ player.add(currency.id, value * amount)
+ }
+ }
+
+ /**
+ * Returns the value of the item with the specified id.
+ *
+ * @param method The [ExchangeType].
+ */
+ private fun value(item: Int, method: ExchangeType): Int {
+ val value = ItemDefinition.lookup(item).value
+
+ return when (method) {
+ BUYING -> when (purchases) {
+ NOTHING -> throw UnsupportedOperationException("Cannot get sell value in shop that doesn't buy.")
+ OWNED -> (value * 0.6).toInt()
+ ANY -> (value * 0.4).toInt()
+ }
+ SELLING -> when (purchases) {
+ ANY -> Math.ceil(value * 0.8).toInt()
+ else -> value
+ }
+ }
+ }
+
+ /**
+ * Verifies that the [Player] can actually sell an item with the given id to this [Shop].
+ *
+ * @param id The id of the [Item] to sell.
+ */
+ private fun verifyPurchase(player: Player, id: Int): Boolean {
+ val item = ItemDefinition.lookup(id)
+
+ if (!purchases(id) || item.isMembersOnly && !player.isMembers || item.value == 0) {
+ player.sendMessage("You can't sell this item to this shop.")
+ return false
+ } else if (inventory.freeSlots() == 0 && !inventory.contains(id)) {
+ player.sendMessage("The shop is currently full at the moment.")
+ return false
+ }
+
+ return true
+ }
+
+ /**
+ * Returns whether or not this [Shop] will purchase an item with the given id.
+ *
+ * @param id The id of the [Item] purchase buy.
+ */
+ private fun purchases(id: Int): Boolean {
+ return id != currency.id && when (purchases) {
+ NOTHING -> false
+ OWNED -> sells.containsKey(id)
+ ANY -> true
+ }
+ }
+
+ /**
+ * Returns whether or not this [Shop] sells the item with the given id.
+ *
+ * @param id The id of the [Item] to sell.
+ */
+ private fun sells(id: Int): Boolean = sells.containsKey(id)
+
+ /**
+ * The [Shop]s policy regarding purchasing items from players.
+ */
+ enum class PurchasePolicy {
+
+ /**
+ * Never purchase anything from players.
+ */
+ NOTHING,
+
+ /**
+ * Only purchase items that this Shop sells by default.
+ */
+ OWNED,
+
+ /**
+ * Purchase any tradeable items.
+ */
+ ANY
+ }
+
+ companion object {
+
+ /**
+ * The amount of pulses between shop inventory restocking.
+ */
+ const val RESTOCK_INTERVAL = 100
+
+ /**
+ * The capacity of a [Shop].
+ */
+ private const val CAPACITY = 30
+
+ /**
+ * The type of exchange occurring between the [Player] and [Shop].
+ */
+ private enum class ExchangeType { BUYING, SELLING }
+
+ /**
+ * The option id for item valuation.
+ */
+ private const val VALUATION_OPTION = 1
+
+ /**
+ * Returns the amount that a player tried to buy or sell.
+ *
+ * @param option The id of the option the player selected.
+ */
+ private fun amount(option: Int): Int {
+ return when (option) {
+ 2 -> 1
+ 3 -> 5
+ 4 -> 10
+ else -> throw IllegalArgumentException("Option must be 1-4")
+ }
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/game/plugin/shops/src/org/apollo/game/plugin/shops/ShopInterfaces.kt b/game/plugin/shops/src/org/apollo/game/plugin/shops/ShopInterfaces.kt
new file mode 100644
index 000000000..4a678bd8f
--- /dev/null
+++ b/game/plugin/shops/src/org/apollo/game/plugin/shops/ShopInterfaces.kt
@@ -0,0 +1,33 @@
+package org.apollo.game.plugin.shops
+
+/**
+ * Contains shop-related interface ids.
+ */
+internal object ShopInterfaces {
+
+ /**
+ * The container interface id for the player's inventory.
+ */
+ const val INVENTORY_CONTAINER = 3823
+
+ /**
+ * The sidebar id for the inventory, when a Shop window is open.
+ */
+ const val INVENTORY_SIDEBAR = 3822
+
+ /**
+ * The shop window interface id.
+ */
+ const val SHOP_WINDOW = 3824
+
+ /**
+ * The container interface id for the shop's inventory.
+ */
+ const val SHOP_CONTAINER = 3900
+
+ /**
+ * The id of the text widget that displays a shop's name.
+ */
+ const val SHOP_NAME = 3901
+
+}
\ No newline at end of file
diff --git a/game/plugin/shops/src/org/apollo/game/plugin/shops/Shops.plugin.kts b/game/plugin/shops/src/org/apollo/game/plugin/shops/Shops.plugin.kts
new file mode 100644
index 000000000..4d3be298d
--- /dev/null
+++ b/game/plugin/shops/src/org/apollo/game/plugin/shops/Shops.plugin.kts
@@ -0,0 +1,47 @@
+package org.apollo.game.plugin.shops
+
+import org.apollo.game.message.handler.ItemVerificationHandler
+import org.apollo.game.message.impl.ItemActionMessage
+import org.apollo.game.message.impl.NpcActionMessage
+import org.apollo.game.model.entity.Mob
+import org.apollo.game.scheduling.ScheduledTask
+
+fun Mob.shop(): Shop? = SHOPS[definition.id]
+
+start { world ->
+ ItemVerificationHandler.addInventory(ShopInterfaces.SHOP_CONTAINER) { it.interactingMob?.shop()?.inventory }
+ ItemVerificationHandler.addInventory(ShopInterfaces.INVENTORY_CONTAINER, PlayerInventorySupplier)
+
+ world.schedule(object : ScheduledTask(Shop.RESTOCK_INTERVAL, false) {
+ override fun execute() = SHOPS.values.distinct().forEach(Shop::restock)
+ })
+}
+
+on { NpcActionMessage::class }
+ .then { player ->
+ val npc = player.world.npcRepository.get(index)
+ val shop = npc.shop() ?: return@then
+
+ if (shop.action == option) {
+ player.startAction(OpenShopAction(player, shop, npc))
+ terminate()
+ }
+ }
+
+
+on { ItemActionMessage::class }
+ .where { interfaceId == ShopInterfaces.SHOP_CONTAINER || interfaceId == ShopInterfaces.INVENTORY_CONTAINER }
+ .then { player ->
+ if (ShopInterfaces.SHOP_WINDOW !in player.interfaceSet) {
+ return@then
+ }
+
+ val shop = player.interactingMob?.shop() ?: return@then
+ when (interfaceId) {
+ ShopInterfaces.INVENTORY_CONTAINER -> shop.buy(player, slot, option)
+ ShopInterfaces.SHOP_CONTAINER -> shop.sell(player, slot, option)
+ else -> error("Supposedly unreacheable case.")
+ }
+
+ terminate()
+ }
diff --git a/game/plugin/shops/src/org/apollo/game/plugin/shops/builder/ActionBuilder.kt b/game/plugin/shops/src/org/apollo/game/plugin/shops/builder/ActionBuilder.kt
new file mode 100644
index 000000000..bfd251a11
--- /dev/null
+++ b/game/plugin/shops/src/org/apollo/game/plugin/shops/builder/ActionBuilder.kt
@@ -0,0 +1,62 @@
+package org.apollo.game.plugin.shops.builder
+
+import org.apollo.cache.def.NpcDefinition
+
+/**
+ * A builder to provide the action id used to open the shop.
+ */
+@ShopDslMarker
+class ActionBuilder {
+
+ private var action: String = "Trade"
+
+ private var actionId: Int? = null
+
+ /**
+ * Sets the name or id of the action used to open the shop interface with an npc. Defaults to "Trade".
+ *
+ * If specifying an id it must account for hidden npc menu actions (if any exist) - if "Open Shop" is the first
+ * action displayed when the npc is right-clicked, it does not necessarily mean that the action id is `1`.
+ *
+ * @param action The `name` (as a [String]) or `id` (as an `Int`) of the npc's action menu, to open the shop.
+ * @throws IllegalArgumentException If `action` is not a [String] or [Int].
+ */ // TODO this is dumb, replace it
+ override fun equals(@Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE") action: Any?): Boolean {
+ if (action is String) {
+ this.action = action
+ return true
+ } else if (action is Int) {
+ actionId = action
+ return true
+ }
+
+ throw IllegalArgumentException("The Npc option must be provided as a String (the option name) or an Int (the option index)\"")
+ }
+
+ /**
+ * Returns the open shop action slot.
+ *
+ * @throws IllegalArgumentException If the action id or name is invalid.
+ */
+ internal fun slot(npc: NpcDefinition): Int {
+ actionId?.let { action ->
+ require(npc.hasInteraction(action - 1)) {
+ "Npc ${npc.name} does not have an an action $action." // action - 1 because ActionMessages are 1-based
+ }
+
+ return action
+ }
+
+ val index = npc.interactions.indexOf(action)
+ require(index != -1) { "Npc ${npc.name} does not have an an action $action." }
+
+ return index + 1 // ActionMessages are 1-based
+ }
+
+ /**
+ * Throws [UnsupportedOperationException].
+ */
+ override fun hashCode(): Int = throw UnsupportedOperationException("ActionBuilder is a utility class for a DSL " +
+ "and improperly implements equals() - it should not be used anywhere outside of the DSL.")
+
+}
\ No newline at end of file
diff --git a/game/plugin/shops/src/org/apollo/game/plugin/shops/builder/CategoryBuilder.kt b/game/plugin/shops/src/org/apollo/game/plugin/shops/builder/CategoryBuilder.kt
new file mode 100644
index 000000000..5f10c470c
--- /dev/null
+++ b/game/plugin/shops/src/org/apollo/game/plugin/shops/builder/CategoryBuilder.kt
@@ -0,0 +1,59 @@
+package org.apollo.game.plugin.shops.builder
+
+/**
+ * A builder for a category - a collection of sold items that share a common prefix or suffix.
+ *
+ * ```
+ * category("mould") {
+ * sell(10) of "Ring"
+ * sell(2) of "Necklace"
+ * sell(10) of "Amulet"
+ * }
+ * ```
+ */
+@ShopDslMarker
+class CategoryBuilder {
+
+ /**
+ * The items that this shop sells, as a pair of item name to amount sold.
+ */
+ private val items = mutableListOf>()
+
+ /**
+ * Creates a [SellBuilder] with the specified [amount].
+ */
+ fun sell(amount: Int): SellBuilder = SellBuilder(amount, items)
+
+ /**
+ * Builds this category into a list of sold items, represented as a pair of item name to amount sold.
+ */
+ fun build(): List> = items
+
+ /**
+ * The method of joining the item and category name.
+ */
+ sealed class Affix(private val joiner: (item: String, category: String) -> String) {
+
+ /**
+ * Appends the category after the item name (with a space between).
+ */
+ object Suffix : Affix({ item, affix -> "$item $affix" })
+
+ /**
+ * Prepends the category before the item name (with a space between).
+ */
+ object Prefix : Affix({ item, affix -> "$affix $item" })
+
+ /**
+ * Does not join the category at all (i.e. only returns the item name).
+ */
+ object None : Affix({ item, _ -> item })
+
+ /**
+ * Joins the item and category name in the expected manner.
+ */
+ fun join(item: String, category: String): String = joiner(item, category)
+
+ }
+
+}
\ No newline at end of file
diff --git a/game/plugin/shops/src/org/apollo/game/plugin/shops/builder/CurrencyBuilder.kt b/game/plugin/shops/src/org/apollo/game/plugin/shops/builder/CurrencyBuilder.kt
new file mode 100644
index 000000000..f4d812245
--- /dev/null
+++ b/game/plugin/shops/src/org/apollo/game/plugin/shops/builder/CurrencyBuilder.kt
@@ -0,0 +1,25 @@
+package org.apollo.game.plugin.shops.builder
+
+import org.apollo.game.plugin.shops.Currency
+
+/**
+ * A builder to provide the currency used by the [Shop].
+ */
+@ShopDslMarker
+class CurrencyBuilder {
+
+ private var currency = Currency.COINS
+
+ /**
+ * Overloads the `in` operator on [Currency] to achieve e.g. `trades in tokkul`.
+ *
+ * This function violates the contract for the `in` operator and is only to be used inside the Shops DSL.
+ */
+ operator fun Currency.contains(builder: CurrencyBuilder): Boolean {
+ builder.currency = this
+ return true
+ }
+
+ fun build(): Currency = currency
+
+}
\ No newline at end of file
diff --git a/game/plugin/shops/src/org/apollo/game/plugin/shops/builder/OperatorBuilder.kt b/game/plugin/shops/src/org/apollo/game/plugin/shops/builder/OperatorBuilder.kt
new file mode 100644
index 000000000..7585cbbba
--- /dev/null
+++ b/game/plugin/shops/src/org/apollo/game/plugin/shops/builder/OperatorBuilder.kt
@@ -0,0 +1,82 @@
+package org.apollo.game.plugin.shops.builder
+
+import org.apollo.game.plugin.api.Definitions
+
+/**
+ * A builder to provide the list of shop operators - the npcs that can be interacted with to access the shop.
+ *
+ * ```
+ * shop("General Store.") {
+ * operated by "Shopkeeper"(522) and "Shop assistant"(523) and "Shop assistant"(524)
+ * ...
+ * }
+ * ```
+ */
+@ShopDslMarker
+class OperatorBuilder internal constructor(private val shopName: String) {
+
+ /**
+ * The [List] of shop operator ids.
+ */
+ private val operators = mutableListOf()
+
+ /**
+ * Adds a shop operator, using the specified [name] to resolve the npc id.
+ */
+ infix fun by(name: String): OperatorBuilder {
+ val npc = requireNotNull(Definitions.npc(name)) {
+ "Failed to resolve npc named `$name` when building shop $shopName."
+ }
+
+ operators += npc.id
+ return this
+ }
+
+ /**
+ * Adds a shop operator, using the specified [name] to resolve the npc id.
+ *
+ * An alias for [by].
+ */
+ infix fun and(name: String): OperatorBuilder = by(name)
+
+ /**
+ * Adds a shop operator, using the specified [name] to resolve the npc id.
+ *
+ * An alias for [by].
+ */
+ operator fun plus(name: String): OperatorBuilder = and(name)
+
+ /**
+ * Adds a shop operator with the specified npc id. Intended to be used with the overloaded String invokation
+ * operator, solely to disambiguate between npcs with the same name (e.g. `"Shopkeeper"(500) vs
+ * `"Shopkeeper"(501)`). Use [by(String)][by] if the npc name is unambiguous.
+ */
+ infix fun by(pair: Pair): OperatorBuilder {
+ operators += pair.second
+ return this
+ }
+
+ /**
+ * Adds a shop operator with the specified npc id. Intended to be used with the overloaded String invokation
+ * operator, solely to disambiguate between npcs with the same name (e.g. `"Shopkeeper"(500) vs
+ * `"Shopkeeper"(501)`). Use [by(String)][by] if the npc name is unambiguous.
+ *
+ * An alias for [by(Pair)][by].
+ */
+ infix fun and(pair: Pair): OperatorBuilder = by(pair)
+
+ /**
+ * Adds a shop operator with the specified npc id. Intended to be used with the overloaded String invokation
+ * operator, solely to disambiguate between npcs with the same name (e.g. `"Shopkeeper"(500) vs
+ * `"Shopkeeper"(501)`). Use [by(String)][by] if the npc name is unambiguous.
+ *
+ * An alias for [by(Pair)][by].
+ */
+ operator fun plus(pair: Pair): OperatorBuilder = by(pair)
+
+ /**
+ * Builds this [OperatorBuilder] into a [List] of operator npc ids.
+ */
+ fun build(): List = operators
+
+}
\ No newline at end of file
diff --git a/game/plugin/shops/src/org/apollo/game/plugin/shops/builder/PurchasesBuilder.kt b/game/plugin/shops/src/org/apollo/game/plugin/shops/builder/PurchasesBuilder.kt
new file mode 100644
index 000000000..9adfa8deb
--- /dev/null
+++ b/game/plugin/shops/src/org/apollo/game/plugin/shops/builder/PurchasesBuilder.kt
@@ -0,0 +1,29 @@
+package org.apollo.game.plugin.shops.builder
+
+import org.apollo.game.plugin.shops.Shop
+
+/**
+ * A builder to provide the [Shop.PurchasePolicy].
+ */
+@ShopDslMarker
+class PurchasesBuilder {
+
+ private var policy = Shop.PurchasePolicy.OWNED
+
+ /**
+ * Instructs the shop to purchase no items, regardless of whether or not it sells it.
+ */
+ infix fun no(@Suppress("UNUSED_PARAMETER") items: Unit) {
+ policy = Shop.PurchasePolicy.NOTHING
+ }
+
+ /**
+ * Instructs the shop to purchase any tradeable item.
+ */
+ infix fun any(@Suppress("UNUSED_PARAMETER") items: Unit) {
+ policy = Shop.PurchasePolicy.ANY
+ }
+
+ fun build(): Shop.PurchasePolicy = policy
+
+}
\ No newline at end of file
diff --git a/game/plugin/shops/src/org/apollo/game/plugin/shops/builder/SellBuilder.kt b/game/plugin/shops/src/org/apollo/game/plugin/shops/builder/SellBuilder.kt
new file mode 100644
index 000000000..fe9550564
--- /dev/null
+++ b/game/plugin/shops/src/org/apollo/game/plugin/shops/builder/SellBuilder.kt
@@ -0,0 +1,43 @@
+package org.apollo.game.plugin.shops.builder
+
+/**
+ * A builder to provide the items to sell.
+ *
+ * @param amount The amount to sell (of each item).
+ * @param items The [MutableList] to insert the given items into.
+ */
+@ShopDslMarker
+class SellBuilder(val amount: Int, val items: MutableList>) {
+
+ infix fun of(lambda: SellBuilder.() -> Unit) = lambda(this)
+
+ /**
+ * Provides an item with the specified name.
+ *
+ * @name The item name. Must be unambiguous.
+ */
+ infix fun of(name: String) {
+ items += Pair(name, amount)
+ }
+
+ /**
+ * Overloads unary minus on Strings so that item names can be listed.
+ */
+ operator fun String.unaryMinus() {
+ of(this)
+ }
+
+ /**
+ * Overloads the unary minus on Pairs so that name+id pairs can be listed. Only intended to be used with the
+ * overloaded String invokation operator.
+ */ // ShopBuilder uses the lookup plugin, which can operate on _ids tacked on the end
+ operator fun Pair.unaryMinus() {
+ items += Pair("${first}_$second", amount)
+ }
+
+ /**
+ * Overloads function invokation on Strings to map `"ambiguous_npc_name"(id)` to a [Pair].
+ */
+ operator fun String.invoke(id: Int): Pair = Pair(this, id)
+
+}
\ No newline at end of file
diff --git a/game/plugin/shops/src/org/apollo/game/plugin/shops/builder/ShopBuilder.kt b/game/plugin/shops/src/org/apollo/game/plugin/shops/builder/ShopBuilder.kt
new file mode 100644
index 000000000..00b6b3b94
--- /dev/null
+++ b/game/plugin/shops/src/org/apollo/game/plugin/shops/builder/ShopBuilder.kt
@@ -0,0 +1,117 @@
+package org.apollo.game.plugin.shops.builder
+
+import org.apollo.cache.def.NpcDefinition
+import org.apollo.game.plugin.api.Definitions
+import org.apollo.game.plugin.shops.Currency
+import org.apollo.game.plugin.shops.SHOPS
+import org.apollo.game.plugin.shops.Shop
+import org.apollo.game.plugin.shops.builder.CategoryBuilder.Affix
+
+/**
+ * Creates a [Shop].
+ *
+ * @param name The name of the shop.
+ */
+fun shop(name: String, builder: ShopBuilder.() -> Unit) {
+ val shop = ShopBuilder(name).apply(builder).build()
+
+ shop.operators.associateByTo(SHOPS, { it }, { shop })
+}
+
+/**
+ * A builder for a [Shop].
+ */
+@ShopDslMarker
+class ShopBuilder(val name: String) {
+
+ /**
+ * The id on the operator npc's action menu used to open the shop.
+ */
+ val action = ActionBuilder()
+
+ /**
+ * The type of [Currency] the [Shop] makes exchanges with.
+ */
+ var trades = CurrencyBuilder()
+
+ /**
+ * The [OperatorBuilder] used to collate the [Shop]'s operators.
+ */
+ val operated = OperatorBuilder(name)
+
+ /**
+ * The [Shop]'s policy towards purchasing items from players.
+ */
+ var buys = PurchasesBuilder()
+
+ /**
+ * Redundant variable used in the purchases dsl, to complete the [PurchasesBuilder] (e.g. `buys no items`).
+ */
+ val items = Unit
+
+ /**
+ * Used in the category dsl. Places the category name before the item name (inserting a space between the names).
+ */
+ val prefix = Affix.Prefix
+
+ /**
+ * Used in the category dsl. Prevents the category name from being joined to the item name in any way.
+ */
+ val nothing = Affix.None
+
+ /**
+ * The [List] of items sold by the shop, as (name, amount) [Pair]s.
+ */
+ private val sold = mutableListOf>()
+
+ /**
+ * Overloads function invokation on strings to map `"ambiguous_npc_name"(id)` to a [Pair].
+ */
+ operator fun String.invoke(id: Int): Pair = Pair(this, id)
+
+ /**
+ * Adds a sequence of items to this Shop, grouped together (in the DSL) for convenience. Items will be displayed
+ * in the same order they are provided.
+ *
+ * @param name The name of the category.
+ * @param affix The method of affixation between the item and category name (see [Affix]).
+ * @param depluralise Whether or not the category name should have the "s".
+ * @param builder The builder that adds items to the category.
+ */
+ fun category(
+ name: String,
+ affix: Affix = Affix.Suffix,
+ depluralise: Boolean = true, // TODO search for both with and without plural
+ builder: CategoryBuilder.() -> Unit
+ ) {
+ val items = CategoryBuilder().apply(builder).build()
+
+ val category = when {
+ depluralise -> name.removeSuffix("s")
+ else -> name
+ }
+
+ sold += items.map { (name, amount) -> Pair(affix.join(name, category), amount) }
+ }
+
+ /**
+ * Creates a [SellBuilder] with the specified [amount].
+ */
+ fun sell(amount: Int): SellBuilder = SellBuilder(amount, sold)
+
+ /**
+ * Converts this builder into a [Shop].
+ */
+ internal fun build(): Shop {
+ val operators = operated.build()
+ val npc = NpcDefinition.lookup(operators.first())
+
+ val items = sold.associateBy(
+ { requireNotNull(Definitions.item(it.first)?.id) { "Failed to find item ${it.first} in shop $name." } },
+ { it.second }
+ )
+
+ return Shop(name, action.slot(npc), items, operators, trades.build(), buys.build())
+ }
+
+}
diff --git a/game/plugin/shops/src/org/apollo/game/plugin/shops/builder/ShopDslMarker.kt b/game/plugin/shops/src/org/apollo/game/plugin/shops/builder/ShopDslMarker.kt
new file mode 100644
index 000000000..3187a23bb
--- /dev/null
+++ b/game/plugin/shops/src/org/apollo/game/plugin/shops/builder/ShopDslMarker.kt
@@ -0,0 +1,7 @@
+package org.apollo.game.plugin.shops.builder
+
+/**
+ * A [DslMarker] for the shop DSL.
+ */
+@DslMarker
+internal annotation class ShopDslMarker
\ No newline at end of file
diff --git a/game/plugin/shops/test/org/apollo/game/plugin/shops/CurrencyTests.kt b/game/plugin/shops/test/org/apollo/game/plugin/shops/CurrencyTests.kt
new file mode 100644
index 000000000..de897d0c2
--- /dev/null
+++ b/game/plugin/shops/test/org/apollo/game/plugin/shops/CurrencyTests.kt
@@ -0,0 +1,27 @@
+package org.apollo.game.plugin.shops
+
+import org.apollo.cache.def.ItemDefinition
+import org.apollo.game.plugin.testing.junit.ApolloTestingExtension
+import org.apollo.game.plugin.testing.junit.api.annotations.ItemDefinitions
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
+import org.junit.jupiter.api.extension.ExtendWith
+
+@ExtendWith(ApolloTestingExtension::class)
+class CurrencyTests {
+
+ @Test
+ fun `items used as currencies must have names in their definitions`() {
+ assertThrows("Should not be able to create a Currency with an item missing a name") {
+ Currency(id = ITEM_MISSING_NAME)
+ }
+ }
+
+ private companion object {
+ private const val ITEM_MISSING_NAME = 0
+
+ @ItemDefinitions
+ private val unnamed = listOf(ItemDefinition(ITEM_MISSING_NAME))
+ }
+
+}
\ No newline at end of file
diff --git a/game/plugin/shops/test/org/apollo/game/plugin/shops/ShopActionTests.kt b/game/plugin/shops/test/org/apollo/game/plugin/shops/ShopActionTests.kt
new file mode 100644
index 000000000..643fcd5ff
--- /dev/null
+++ b/game/plugin/shops/test/org/apollo/game/plugin/shops/ShopActionTests.kt
@@ -0,0 +1,21 @@
+package org.apollo.game.plugin.shops
+
+import org.apollo.game.model.entity.Player
+import org.apollo.game.plugin.testing.junit.ApolloTestingExtension
+import org.apollo.game.plugin.testing.junit.api.ActionCapture
+import org.apollo.game.plugin.testing.junit.api.annotations.TestMock
+import org.junit.jupiter.api.extension.ExtendWith
+
+@ExtendWith(ApolloTestingExtension::class)
+class ShopActionTests {
+
+ @TestMock
+ lateinit var player: Player
+
+ @TestMock
+ lateinit var action: ActionCapture
+
+
+
+
+}
\ No newline at end of file
diff --git a/game/plugin/skills/fishing/build.gradle b/game/plugin/skills/fishing/build.gradle
new file mode 100644
index 000000000..4314d6fd4
--- /dev/null
+++ b/game/plugin/skills/fishing/build.gradle
@@ -0,0 +1,13 @@
+apply plugin: 'kotlin'
+
+
+
+dependencies {
+ implementation project(':game')
+ implementation project(':cache')
+ implementation project(':net')
+ implementation project(':util')
+ implementation project(':game:plugin:api')
+ implementation project(':game:plugin:entity:spawn')
+ testImplementation project(':game:plugin-testing')
+}
diff --git a/game/plugin/skills/fishing/src/org/apollo/game/plugin/skills/fishing/Fish.kt b/game/plugin/skills/fishing/src/org/apollo/game/plugin/skills/fishing/Fish.kt
new file mode 100644
index 000000000..45f76b826
--- /dev/null
+++ b/game/plugin/skills/fishing/src/org/apollo/game/plugin/skills/fishing/Fish.kt
@@ -0,0 +1,30 @@
+package org.apollo.game.plugin.skills.fishing
+
+import org.apollo.game.plugin.api.Definitions
+
+/**
+ * A fish that can be gathered using the fishing skill.
+ */
+enum class Fish(val id: Int, val level: Int, val experience: Double, catchSuffix: String? = null) {
+ SHRIMPS(id = 317, level = 1, experience = 10.0, catchSuffix = "some shrimp."),
+ SARDINE(id = 327, level = 5, experience = 20.0),
+ MACKEREL(id = 353, level = 16, experience = 20.0),
+ HERRING(id = 345, level = 10, experience = 30.0),
+ ANCHOVIES(id = 321, level = 15, experience = 40.0, catchSuffix = "some anchovies."),
+ TROUT(id = 335, level = 20, experience = 50.0),
+ COD(id = 341, level = 23, experience = 45.0),
+ PIKE(id = 349, level = 25, experience = 60.0),
+ SALMON(id = 331, level = 30, experience = 70.0),
+ TUNA(id = 359, level = 35, experience = 80.0),
+ LOBSTER(id = 377, level = 40, experience = 90.0),
+ BASS(id = 363, level = 46, experience = 100.0),
+ SWORDFISH(id = 371, level = 50, experience = 100.0),
+ SHARK(id = 383, level = 76, experience = 110.0, catchSuffix = "a shark!");
+
+ /**
+ * The name of this fish, formatted so it can be inserted into a message.
+ */
+ val catchMessage by lazy { "You catch ${catchSuffix ?: "a $catchName."}" }
+
+ private val catchName by lazy { Definitions.item(id).name.toLowerCase().removePrefix("raw ") }
+}
diff --git a/game/plugin/skills/fishing/src/org/apollo/game/plugin/skills/fishing/Fishing.plugin.kts b/game/plugin/skills/fishing/src/org/apollo/game/plugin/skills/fishing/Fishing.plugin.kts
new file mode 100644
index 000000000..597058bbe
--- /dev/null
+++ b/game/plugin/skills/fishing/src/org/apollo/game/plugin/skills/fishing/Fishing.plugin.kts
@@ -0,0 +1,20 @@
+package org.apollo.game.plugin.skills.fishing
+
+import org.apollo.game.message.impl.NpcActionMessage
+
+// TODO: moving fishing spots, seaweed and caskets, evil bob
+
+/**
+ * Intercepts the [NpcActionMessage] and starts a [FishingAction] if the npc
+ */
+on { NpcActionMessage::class }
+ .where { option == 1 || option == 3 }
+ .then { player ->
+ val entity = player.world.npcRepository[index]
+ val option = FishingSpot.lookup(entity.id)?.option(option) ?: return@then
+
+ val target = FishingTarget(entity.position, option)
+ player.startAction(FishingAction(player, target))
+
+ terminate()
+ }
diff --git a/game/plugin/skills/fishing/src/org/apollo/game/plugin/skills/fishing/FishingAction.kt b/game/plugin/skills/fishing/src/org/apollo/game/plugin/skills/fishing/FishingAction.kt
new file mode 100644
index 000000000..9eacf024e
--- /dev/null
+++ b/game/plugin/skills/fishing/src/org/apollo/game/plugin/skills/fishing/FishingAction.kt
@@ -0,0 +1,86 @@
+package org.apollo.game.plugin.skills.fishing
+
+import java.util.Objects
+import org.apollo.game.action.ActionBlock
+import org.apollo.game.action.AsyncDistancedAction
+import org.apollo.game.model.entity.Player
+import org.apollo.game.plugin.api.fishing
+
+class FishingAction(
+ player: Player,
+ private val target: FishingTarget
+) : AsyncDistancedAction(0, true, player, target.position, SPOT_DISTANCE) {
+
+ /**
+ * The [FishingTool] used for the fishing spot.
+ */
+ private val tool = target.option.tool
+
+ override fun action(): ActionBlock = {
+ if (!target.verify(mob)) {
+ stop()
+ }
+
+ mob.turnTo(position)
+ mob.sendMessage(tool.message)
+
+ while (isRunning) {
+ mob.playAnimation(tool.animation)
+ wait(FISHING_DELAY)
+
+ val level = mob.fishing.current
+ val fish = target.option.sample(level)
+
+ if (target.isSuccessful(mob, fish.level)) {
+ if (tool.bait != -1) {
+ mob.inventory.remove(tool.bait)
+ }
+
+ mob.inventory.add(fish.id)
+ mob.sendMessage(fish.catchMessage)
+ mob.fishing.experience += fish.experience
+
+ if (mob.inventory.freeSlots() == 0) {
+ mob.inventory.forceCapacityExceeded()
+
+ mob.stopAnimation()
+ stop()
+ } else if (!hasBait(mob, tool.bait)) {
+ mob.sendMessage("You need more ${tool.baitName} to fish at this spot.")
+
+ mob.stopAnimation()
+ stop()
+ }
+ }
+ }
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (other is FishingAction) {
+ return position == other.position && target == other.target && mob == other.mob
+ }
+
+ return false
+ }
+
+ override fun hashCode(): Int = Objects.hash(target, position, mob)
+
+ internal companion object {
+ private const val SPOT_DISTANCE = 1
+ private const val FISHING_DELAY = 4
+
+ /**
+ * Returns whether or not the [Player] has (or does not need) bait.
+ */
+ internal fun hasBait(player: Player, bait: Int): Boolean {
+ return bait == -1 || bait in player.inventory
+ }
+
+ /**
+ * Returns whether or not the player has the required tool to fish at the spot.
+ */
+ internal fun hasTool(player: Player, tool: FishingTool): Boolean {
+ return tool.id in player.equipment || tool.id in player.inventory
+ }
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/skills/fishing/src/org/apollo/game/plugin/skills/fishing/FishingSpot.kt b/game/plugin/skills/fishing/src/org/apollo/game/plugin/skills/fishing/FishingSpot.kt
new file mode 100644
index 000000000..380dd1a07
--- /dev/null
+++ b/game/plugin/skills/fishing/src/org/apollo/game/plugin/skills/fishing/FishingSpot.kt
@@ -0,0 +1,120 @@
+package org.apollo.game.plugin.skills.fishing
+
+import org.apollo.game.plugin.api.rand
+import org.apollo.game.plugin.skills.fishing.Fish.*
+
+/**
+ * A spot that can be fished from.
+ */
+enum class FishingSpot(val npc: Int, private val first: Option, private val second: Option) {
+
+ ROD(
+ npc = 309,
+ first = Option.of(tool = FishingTool.FLY_FISHING_ROD, primary = TROUT, secondary = SALMON),
+ second = Option.of(tool = FishingTool.FISHING_ROD, primary = PIKE)
+ ),
+
+ CAGE_HARPOON(
+ npc = 312,
+ first = Option.of(tool = FishingTool.LOBSTER_CAGE, primary = LOBSTER),
+ second = Option.of(tool = FishingTool.HARPOON, primary = TUNA, secondary = SWORDFISH)
+ ),
+
+ NET_HARPOON(
+ npc = 313,
+ first = Option.of(tool = FishingTool.BIG_NET, primary = MACKEREL, secondary = COD),
+ second = Option.of(tool = FishingTool.HARPOON, primary = BASS, secondary = SHARK)
+ ),
+
+ NET_ROD(
+ npc = 316,
+ first = Option.of(tool = FishingTool.SMALL_NET, primary = SHRIMPS, secondary = ANCHOVIES),
+ second = Option.of(tool = FishingTool.FISHING_ROD, primary = SARDINE, secondary = HERRING)
+ );
+
+ /**
+ * Returns the [FishingSpot.Option] associated with the specified action id.
+ */
+ fun option(action: Int): Option {
+ return when (action) {
+ 1 -> first
+ 3 -> second
+ else -> throw UnsupportedOperationException("Unexpected fishing spot option $action.")
+ }
+ }
+
+ /**
+ * An option at a [FishingSpot] (e.g. either "rod fishing" or "net fishing").
+ */
+ sealed class Option {
+
+ /**
+ * The tool used to obtain fish
+ */
+ abstract val tool: FishingTool
+
+ /**
+ * The minimum level required to obtain fish.
+ */
+ abstract val level: Int
+
+ /**
+ * Samples a [Fish], randomly (with weighting) returning one (that can be fished by the player).
+ *
+ * @param level The fishing level of the player.
+ */
+ abstract fun sample(level: Int): Fish
+
+ /**
+ * A [FishingSpot] [Option] that can only provide a single type of fish.
+ */
+ private data class Single(override val tool: FishingTool, val primary: Fish) : Option() {
+ override val level = primary.level
+
+ override fun sample(level: Int): Fish = primary
+ }
+
+ /**
+ * A [FishingSpot] [Option] that can provide a two different types of fish.
+ */
+ private data class Pair(override val tool: FishingTool, val primary: Fish, val secondary: Fish) : Option() {
+ override val level = Math.min(primary.level, secondary.level)
+
+ override fun sample(level: Int): Fish {
+ return if (level < secondary.level || rand(100) < WEIGHTING) {
+ primary
+ } else {
+ secondary
+ }
+ }
+
+ private companion object {
+ /**
+ * The weighting factor that causes the lower-level fish to be returned more frequently.
+ */
+ private const val WEIGHTING = 70
+ }
+ }
+
+ companion object {
+
+ fun of(tool: FishingTool, primary: Fish): Option = Single(tool, primary)
+
+ fun of(tool: FishingTool, primary: Fish, secondary: Fish): Option {
+ return when {
+ primary.level < secondary.level -> Pair(tool, primary, secondary)
+ else -> Pair(tool, secondary, primary)
+ }
+ }
+ }
+ }
+
+ companion object {
+ private val FISHING_SPOTS = FishingSpot.values().associateBy(FishingSpot::npc)
+
+ /**
+ * Returns the [FishingSpot] with the specified [id], or `null` if the spot does not exist.
+ */
+ fun lookup(id: Int): FishingSpot? = FISHING_SPOTS[id]
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/skills/fishing/src/org/apollo/game/plugin/skills/fishing/FishingTarget.kt b/game/plugin/skills/fishing/src/org/apollo/game/plugin/skills/fishing/FishingTarget.kt
new file mode 100644
index 000000000..bdcb5713a
--- /dev/null
+++ b/game/plugin/skills/fishing/src/org/apollo/game/plugin/skills/fishing/FishingTarget.kt
@@ -0,0 +1,38 @@
+package org.apollo.game.plugin.skills.fishing
+
+import org.apollo.game.model.Position
+import org.apollo.game.model.entity.Player
+import org.apollo.game.plugin.api.fishing
+import org.apollo.game.plugin.api.rand
+import org.apollo.game.plugin.skills.fishing.FishingAction.Companion.hasBait
+import org.apollo.game.plugin.skills.fishing.FishingAction.Companion.hasTool
+
+data class FishingTarget(val position: Position, val option: FishingSpot.Option) {
+
+ /**
+ * Returns whether or not the catch was successful.
+ * TODO: We need to identify the correct algorithm for this
+ */
+ fun isSuccessful(player: Player, req: Int): Boolean {
+ return minOf(player.fishing.current - req + 5, 40) > rand(100)
+ }
+
+ /**
+ * Verifies that the [Player] can gather fish from their chosen [FishingSpot.Option].
+ */
+ fun verify(player: Player): Boolean {
+ val current = player.fishing.current
+ val required = option.level
+ val tool = option.tool
+
+ when {
+ current < required -> player.sendMessage("You need a fishing level of $required to fish at this spot.")
+ hasTool(player, tool) -> player.sendMessage("You need a ${tool.formattedName} to fish at this spot.")
+ hasBait(player, tool.bait) -> player.sendMessage("You need some ${tool.baitName} to fish at this spot.")
+ player.inventory.freeSlots() == 0 -> player.inventory.forceCapacityExceeded()
+ else -> return true
+ }
+
+ return false
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/skills/fishing/src/org/apollo/game/plugin/skills/fishing/FishingTool.kt b/game/plugin/skills/fishing/src/org/apollo/game/plugin/skills/fishing/FishingTool.kt
new file mode 100644
index 000000000..e49122142
--- /dev/null
+++ b/game/plugin/skills/fishing/src/org/apollo/game/plugin/skills/fishing/FishingTool.kt
@@ -0,0 +1,32 @@
+package org.apollo.game.plugin.skills.fishing
+
+import org.apollo.game.model.Animation
+import org.apollo.game.plugin.api.Definitions
+
+/**
+ * A tool used to gather [Fish] from a [FishingSpot].
+ */
+enum class FishingTool(
+ val message: String,
+ val id: Int,
+ animation: Int,
+ val bait: Int = -1,
+ val baitName: String? = null
+) {
+ LOBSTER_CAGE("You attempt to catch a lobster...", id = 301, animation = 619),
+ SMALL_NET("You cast out your net...", id = 303, animation = 620),
+ BIG_NET("You cast out your net...", id = 305, animation = 620),
+ HARPOON("You start harpooning fish...", id = 311, animation = 618),
+ FISHING_ROD("You attempt to catch a fish...", id = 307, animation = 622, bait = 313, baitName = "feathers"),
+ FLY_FISHING_ROD("You attempt to catch a fish...", id = 309, animation = 622, bait = 314, baitName = "fishing bait");
+
+ /**
+ * The [Animation] played when fishing with this tool.
+ */
+ val animation: Animation = Animation(animation)
+
+ /**
+ * The name of this tool, formatted so it can be inserted into a message.
+ */
+ val formattedName by lazy { Definitions.item(id).name.toLowerCase() }
+}
\ No newline at end of file
diff --git a/game/plugin/skills/fishing/src/org/apollo/game/plugin/skills/fishing/Spots.plugin.kts b/game/plugin/skills/fishing/src/org/apollo/game/plugin/skills/fishing/Spots.plugin.kts
new file mode 100644
index 000000000..3d162459a
--- /dev/null
+++ b/game/plugin/skills/fishing/src/org/apollo/game/plugin/skills/fishing/Spots.plugin.kts
@@ -0,0 +1,180 @@
+package org.apollo.game.plugin.skills.fishing
+import org.apollo.game.model.Position
+import org.apollo.game.plugin.entity.spawn.spawnNpc
+import org.apollo.game.plugin.skills.fishing.FishingSpot.CAGE_HARPOON
+import org.apollo.game.plugin.skills.fishing.FishingSpot.NET_HARPOON
+import org.apollo.game.plugin.skills.fishing.FishingSpot.NET_ROD
+import org.apollo.game.plugin.skills.fishing.FishingSpot.ROD
+
+// Al-Kharid
+NET_ROD at Position(3267, 3148)
+NET_ROD at Position(3268, 3147)
+NET_ROD at Position(3277, 3139)
+CAGE_HARPOON at Position(3350, 3817)
+CAGE_HARPOON at Position(3347, 3814)
+CAGE_HARPOON at Position(3363, 3816)
+CAGE_HARPOON at Position(3368, 3811)
+
+// Ardougne
+ROD at Position(2561, 3374)
+ROD at Position(2562, 3374)
+ROD at Position(2568, 3365)
+
+// Bandit camp
+NET_ROD at Position(3047, 3703)
+NET_ROD at Position(3045, 3702)
+
+// Baxtorian falls
+ROD at Position(2527, 3412)
+ROD at Position(2530, 3412)
+ROD at Position(2533, 3410)
+
+// Burgh de Rott
+NET_HARPOON at Position(3497, 3175)
+NET_HARPOON at Position(3496, 3178)
+NET_HARPOON at Position(3499, 3178)
+NET_HARPOON at Position(3489, 3184)
+NET_HARPOON at Position(3496, 3176)
+NET_HARPOON at Position(3486, 3184)
+NET_HARPOON at Position(3479, 3189)
+NET_HARPOON at Position(3476, 3191)
+NET_HARPOON at Position(3472, 3196)
+NET_HARPOON at Position(3496, 3180)
+NET_HARPOON at Position(3512, 3178)
+NET_HARPOON at Position(3515, 3180)
+NET_HARPOON at Position(3518, 3177)
+NET_HARPOON at Position(3528, 3172)
+NET_HARPOON at Position(3531, 3169)
+NET_HARPOON at Position(3531, 3172)
+NET_HARPOON at Position(3531, 3167)
+
+// Camelot
+ROD at Position(2726, 3524)
+ROD at Position(2727, 3524)
+
+// Castle wars
+ROD at Position(2461, 3151)
+ROD at Position(2461, 3150)
+ROD at Position(2462, 3145)
+ROD at Position(2472, 3156)
+
+// Catherby 1
+NET_ROD at Position(2838, 3431)
+CAGE_HARPOON at Position(2837, 3431)
+CAGE_HARPOON at Position(2836, 3431)
+NET_ROD at Position(2846, 3429)
+NET_ROD at Position(2844, 3429)
+CAGE_HARPOON at Position(2845, 3429)
+NET_HARPOON at Position(2853, 3423)
+NET_HARPOON at Position(2855, 3423)
+NET_HARPOON at Position(2859, 3426)
+
+// Draynor village
+NET_ROD at Position(3085, 3230)
+NET_ROD at Position(3085, 3231)
+NET_ROD at Position(3086, 3227)
+
+// Elf camp
+ROD at Position(2210, 3243)
+ROD at Position(2216, 3236)
+ROD at Position(2222, 3241)
+
+// Entrana
+NET_ROD at Position(2843, 3359)
+NET_ROD at Position(2842, 3359)
+NET_ROD at Position(2847, 3361)
+NET_ROD at Position(2848, 3361)
+NET_ROD at Position(2840, 3356)
+NET_ROD at Position(2845, 3356)
+NET_ROD at Position(2875, 3342)
+NET_ROD at Position(2876, 3342)
+NET_ROD at Position(2877, 3342)
+
+// Fishing guild
+CAGE_HARPOON at Position(2612, 3411)
+CAGE_HARPOON at Position(2607, 3410)
+NET_HARPOON at Position(2612, 3414)
+NET_HARPOON at Position(2612, 3415)
+NET_HARPOON at Position(2609, 3416)
+CAGE_HARPOON at Position(2604, 3417)
+NET_HARPOON at Position(2605, 3416)
+NET_HARPOON at Position(2602, 3411)
+NET_HARPOON at Position(2602, 3412)
+CAGE_HARPOON at Position(2602, 3414)
+NET_HARPOON at Position(2603, 3417)
+NET_HARPOON at Position(2599, 3419)
+NET_HARPOON at Position(2601, 3422)
+NET_HARPOON at Position(2605, 3421)
+CAGE_HARPOON at Position(2602, 3426)
+NET_HARPOON at Position(2604, 3426)
+CAGE_HARPOON at Position(2605, 3425)
+
+// Fishing platform
+NET_ROD at Position(2791, 3279)
+NET_ROD at Position(2795, 3279)
+NET_ROD at Position(2790, 3273)
+
+// Grand Tree
+ROD at Position(2393, 3419)
+ROD at Position(2391, 3421)
+ROD at Position(2389, 3423)
+ROD at Position(2388, 3423)
+ROD at Position(2385, 3422)
+ROD at Position(2384, 3419)
+ROD at Position(2383, 3417)
+
+// Karamja
+NET_ROD at Position(2921, 3178)
+CAGE_HARPOON at Position(2923, 3179)
+CAGE_HARPOON at Position(2923, 3180)
+NET_ROD at Position(2924, 3181)
+NET_ROD at Position(2926, 3180)
+CAGE_HARPOON at Position(2926, 3179)
+
+// Lumbridge
+ROD at Position(3239, 3244)
+NET_ROD at Position(3238, 3252)
+
+// Miscellenia
+CAGE_HARPOON at Position(2580, 3851)
+CAGE_HARPOON at Position(2581, 3851)
+CAGE_HARPOON at Position(2582, 3851)
+CAGE_HARPOON at Position(2583, 3852)
+CAGE_HARPOON at Position(2583, 3853)
+
+// Rellekka
+NET_ROD at Position(2633, 3691)
+NET_ROD at Position(2633, 3689)
+CAGE_HARPOON at Position(2639, 3698)
+CAGE_HARPOON at Position(2639, 3697)
+CAGE_HARPOON at Position(2639, 3695)
+NET_HARPOON at Position(2642, 3694)
+NET_HARPOON at Position(2642, 3697)
+NET_HARPOON at Position(2644, 3709)
+
+// Rimmington
+NET_ROD at Position(2990, 3169)
+NET_ROD at Position(2986, 3176)
+
+// Shilo Village
+ROD at Position(2855, 2974)
+ROD at Position(2865, 2972)
+ROD at Position(2860, 2972)
+ROD at Position(2835, 2974)
+ROD at Position(2859, 2976)
+
+// Tirannwn
+ROD at Position(2266, 3253)
+ROD at Position(2265, 3258)
+ROD at Position(2264, 3258)
+
+// Tutorial island
+NET_ROD at Position(3101, 3092)
+NET_ROD at Position(3103, 3092)
+
+/**
+ * Registers the [FishingSpot] at the specified position.
+ */
+infix fun FishingSpot.at(position: Position) {
+ spawnNpc("", position, id = npc)
+}
\ No newline at end of file
diff --git a/game/plugin/skills/fishing/test/org/apollo/game/plugin/skills/fishing/FishingActionTests.kt b/game/plugin/skills/fishing/test/org/apollo/game/plugin/skills/fishing/FishingActionTests.kt
new file mode 100644
index 000000000..42fd6cbb6
--- /dev/null
+++ b/game/plugin/skills/fishing/test/org/apollo/game/plugin/skills/fishing/FishingActionTests.kt
@@ -0,0 +1,92 @@
+package org.apollo.game.plugin.skills.fishing
+
+import io.mockk.every
+import io.mockk.spyk
+import io.mockk.verify
+import org.apollo.cache.def.ItemDefinition
+import org.apollo.cache.def.NpcDefinition
+import org.apollo.game.model.World
+import org.apollo.game.model.entity.Player
+import org.apollo.game.model.entity.Skill
+import org.apollo.game.plugin.testing.assertions.after
+import org.apollo.game.plugin.testing.assertions.contains
+import org.apollo.game.plugin.testing.assertions.startsWith
+import org.apollo.game.plugin.testing.assertions.verifyAfter
+import org.apollo.game.plugin.testing.junit.ApolloTestingExtension
+import org.apollo.game.plugin.testing.junit.api.ActionCapture
+import org.apollo.game.plugin.testing.junit.api.annotations.ItemDefinitions
+import org.apollo.game.plugin.testing.junit.api.annotations.NpcDefinitions
+import org.apollo.game.plugin.testing.junit.api.annotations.TestMock
+import org.apollo.game.plugin.testing.junit.api.interactions.spawnNpc
+import org.apollo.game.plugin.testing.junit.api.interactions.spawnObject
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+
+@ExtendWith(ApolloTestingExtension::class)
+class FishingActionTests {
+
+ @TestMock
+ lateinit var world: World
+
+ @TestMock
+ lateinit var player: Player
+
+ @TestMock
+ lateinit var action: ActionCapture
+
+ @Test
+ fun `Attempting to fish at a spot we don't have the skill to should send the player a message`() {
+ val obj = world.spawnObject(1, player.position)
+
+ val option = spyk(FishingSpot.CAGE_HARPOON.option(1))
+ val target = FishingTarget(obj.position, option)
+
+ player.startAction(FishingAction(player, target))
+
+ every { option.level } returns Int.MAX_VALUE
+
+ verifyAfter(action.complete()) {
+ player.sendMessage(contains("need a fishing level of ${Int.MAX_VALUE}"))
+ }
+ }
+
+ @Test
+ fun `Fishing at a spot we have the skill to should eventually reward fish and experience`() {
+ val option = spyk(FishingSpot.CAGE_HARPOON.option(1))
+ val obj = world.spawnNpc(FishingSpot.CAGE_HARPOON.npc, player.position)
+
+ val target = spyk(FishingTarget(obj.position, option))
+ every { target.isSuccessful(player, any()) } returns true
+ every { target.verify(player) } returns true
+
+ player.skillSet.setCurrentLevel(Skill.FISHING, option.level)
+ player.startAction(FishingAction(player, target))
+
+ verifyAfter(action.ticks(1)) {
+ player.sendMessage(startsWith("You attempt to catch a lobster"))
+ }
+
+ after(action.ticks(4)) {
+ verify { player.sendMessage(startsWith("You catch a .")) }
+
+ assertTrue(player.inventory.contains(Fish.LOBSTER.id))
+ assertEquals(player.skillSet.getExperience(Skill.FISHING), Fish.LOBSTER.experience)
+ }
+ }
+
+ private companion object {
+ @ItemDefinitions
+ private val fish = Fish.values()
+ .map { ItemDefinition(it.id).apply { name = "" } }
+
+ @ItemDefinitions
+ private val tools = FishingTool.values()
+ .map { ItemDefinition(it.id).apply { name = "" } }
+
+ @NpcDefinitions
+ private val spots = FishingSpot.values()
+ .map { NpcDefinition(it.npc).apply { name = "" } }
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/skills/herblore/build.gradle b/game/plugin/skills/herblore/build.gradle
new file mode 100644
index 000000000..4d96c9f57
--- /dev/null
+++ b/game/plugin/skills/herblore/build.gradle
@@ -0,0 +1,10 @@
+apply plugin: 'kotlin'
+
+dependencies {
+ implementation project(':game')
+ implementation project(':cache')
+ implementation project(':net')
+ implementation project(':util')
+ implementation project(':game:plugin:api')
+ testImplementation project(':game:plugin-testing')
+}
diff --git a/game/plugin/skills/herblore/src/CrushIngredientAction.kt b/game/plugin/skills/herblore/src/CrushIngredientAction.kt
new file mode 100644
index 000000000..6c519431b
--- /dev/null
+++ b/game/plugin/skills/herblore/src/CrushIngredientAction.kt
@@ -0,0 +1,39 @@
+import java.util.Objects
+import org.apollo.game.action.ActionBlock
+import org.apollo.game.action.AsyncAction
+import org.apollo.game.model.Animation
+import org.apollo.game.model.entity.Player
+
+class CrushIngredientAction(
+ player: Player,
+ private val ingredient: CrushableIngredient
+) : AsyncAction(0, true, player) {
+
+ override fun action(): ActionBlock = {
+ mob.playAnimation(GRINDING_ANIM)
+ wait(pulses = 1)
+
+ val inventory = mob.inventory
+ if (inventory.remove(ingredient.uncrushed)) {
+ inventory.add(ingredient.id)
+
+ mob.sendMessage("You carefully grind the ${ingredient.uncrushedName} to dust.")
+ }
+
+ stop()
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as CrushIngredientAction
+ return mob == other.mob && ingredient == other.ingredient
+ }
+
+ override fun hashCode(): Int = Objects.hash(mob, ingredient)
+
+ private companion object {
+ private val GRINDING_ANIM = Animation(364)
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/skills/herblore/src/Herb.kt b/game/plugin/skills/herblore/src/Herb.kt
new file mode 100644
index 000000000..5b921f204
--- /dev/null
+++ b/game/plugin/skills/herblore/src/Herb.kt
@@ -0,0 +1,34 @@
+import org.apollo.game.plugin.api.Definitions
+
+enum class Herb(
+ val identified: Int,
+ val unidentified: Int,
+ val level: Int,
+ val experience: Double
+) {
+ GUAM_LEAF(identified = 249, unidentified = 199, level = 1, experience = 2.5),
+ MARRENTILL(identified = 251, unidentified = 201, level = 5, experience = 3.8),
+ TARROMIN(identified = 253, unidentified = 203, level = 11, experience = 5.0),
+ HARRALANDER(identified = 255, unidentified = 205, level = 20, experience = 6.3),
+ RANARR(identified = 257, unidentified = 207, level = 25, experience = 7.5),
+ TOADFLAX(identified = 2998, unidentified = 2998, level = 30, experience = 8.0),
+ IRIT_LEAF(identified = 259, unidentified = 209, level = 40, experience = 8.8),
+ AVANTOE(identified = 261, unidentified = 211, level = 48, experience = 10.0),
+ KWUARM(identified = 263, unidentified = 213, level = 54, experience = 11.3),
+ SNAPDRAGON(identified = 3000, unidentified = 3051, level = 59, experience = 11.8),
+ CADANTINE(identified = 265, unidentified = 215, level = 65, experience = 12.5),
+ LANTADYME(identified = 2481, unidentified = 2485, level = 67, experience = 13.1),
+ DWARF_WEED(identified = 267, unidentified = 217, level = 70, experience = 13.8),
+ TORSTOL(identified = 269, unidentified = 219, level = 75, experience = 15.0);
+
+ val identifiedName by lazy { Definitions.item(identified)!!.name }
+
+ companion object {
+ private val identified = Herb.values().map(Herb::identified).toHashSet()
+ private val herbs = Herb.values().associateBy(Herb::unidentified)
+
+ operator fun get(id: Int): Herb? = herbs[id]
+ internal fun Int.isUnidentified(): Boolean = this in herbs
+ internal fun Int.isIdentified(): Boolean = this in identified
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/skills/herblore/src/Herblore.plugin.kts b/game/plugin/skills/herblore/src/Herblore.plugin.kts
new file mode 100644
index 000000000..f575a6532
--- /dev/null
+++ b/game/plugin/skills/herblore/src/Herblore.plugin.kts
@@ -0,0 +1,48 @@
+
+import CrushableIngredient.Companion.isCrushable
+import CrushableIngredient.Companion.isGrindingTool
+import Herb.Companion.isIdentified
+import Herb.Companion.isUnidentified
+import Ingredient.Companion.isIngredient
+import UnfinishedPotion.Companion.isUnfinished
+import org.apollo.game.message.impl.ItemOnItemMessage
+import org.apollo.game.message.impl.ItemOptionMessage
+
+on { ItemOptionMessage::class }
+ .where { option == IdentifyHerbAction.IDENTIFY_OPTION && id.isUnidentified() }
+ .then { player ->
+ val unidentified = Herb[id]!!
+
+ player.startAction(IdentifyHerbAction(player, slot, unidentified))
+ terminate()
+ }
+
+on { ItemOnItemMessage::class }
+ .where { (id.isGrindingTool() && targetId.isCrushable() || id.isCrushable() && targetId.isGrindingTool()) }
+ .then { player ->
+ val crushableId = if (id.isCrushable()) id else targetId
+ val raw = CrushableIngredient[crushableId]!!
+
+ player.startAction(CrushIngredientAction(player, raw))
+ terminate()
+ }
+
+on { ItemOnItemMessage::class }
+ .where { id == VIAL_OF_WATER && targetId.isIdentified() || id.isIdentified() && targetId == VIAL_OF_WATER }
+ .then { player ->
+ val herbId = if (id.isIdentified()) id else targetId
+ val unfinished = UnfinishedPotion[herbId]!!
+
+ player.startAction(MakeUnfinishedPotionAction(player, unfinished))
+ terminate()
+ }
+
+on { ItemOnItemMessage::class }
+ .where { id.isUnfinished() && targetId.isIngredient() || id.isIngredient() && targetId.isUnfinished() }
+ .then { player ->
+ val key = if (id.isUnfinished()) Pair(id, targetId) else Pair(targetId, id)
+ val finished = FinishedPotion[key]!!
+
+ player.startAction(MakeFinishedPotionAction(player, finished))
+ terminate()
+ }
\ No newline at end of file
diff --git a/game/plugin/skills/herblore/src/IdentifyHerbAction.kt b/game/plugin/skills/herblore/src/IdentifyHerbAction.kt
new file mode 100644
index 000000000..bd9bd4b75
--- /dev/null
+++ b/game/plugin/skills/herblore/src/IdentifyHerbAction.kt
@@ -0,0 +1,49 @@
+
+import java.util.Objects
+import org.apollo.game.action.ActionBlock
+import org.apollo.game.action.AsyncAction
+import org.apollo.game.model.entity.Player
+import org.apollo.game.plugin.api.herblore
+import org.apollo.util.LanguageUtil
+
+class IdentifyHerbAction(
+ player: Player,
+ private val slot: Int,
+ private val herb: Herb
+) : AsyncAction(0, true, player) {
+
+ override fun action(): ActionBlock = {
+ if (mob.herblore.current < herb.level) {
+ mob.sendMessage("You need a Herblore level of ${herb.level} to clean this herb.")
+ stop()
+ }
+
+ val inventory = mob.inventory
+
+ if (inventory.removeSlot(slot, 1) > 0) {
+ inventory.add(herb.identified)
+ mob.herblore.experience += herb.experience
+
+ val name = herb.identifiedName
+ val article = LanguageUtil.getIndefiniteArticle(name)
+
+ mob.sendMessage("You identify the herb as $article $name.")
+ }
+
+ stop()
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as IdentifyHerbAction
+ return mob == other.mob && herb == other.herb && slot == other.slot
+ }
+
+ override fun hashCode(): Int = Objects.hash(mob, herb, slot)
+
+ companion object {
+ const val IDENTIFY_OPTION = 1
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/skills/herblore/src/Ingredient.kt b/game/plugin/skills/herblore/src/Ingredient.kt
new file mode 100644
index 000000000..475436945
--- /dev/null
+++ b/game/plugin/skills/herblore/src/Ingredient.kt
@@ -0,0 +1,60 @@
+import CrushableIngredient.Companion.isCrushable
+import NormalIngredient.Companion.isNormalIngredient
+import org.apollo.game.plugin.api.Definitions
+
+/**
+ * A secondary ingredient in a potion.
+ */
+interface Ingredient {
+ val id: Int
+
+ companion object {
+ internal fun Int.isIngredient(): Boolean = isNormalIngredient() || isCrushable()
+ }
+}
+
+enum class CrushableIngredient(val uncrushed: Int, override val id: Int) : Ingredient {
+ UNICORN_HORN(uncrushed = 237, id = 235),
+ DRAGON_SCALE(uncrushed = 243, id = 241),
+ CHOCOLATE_BAR(uncrushed = 1973, id = 1975),
+ BIRDS_NEST(uncrushed = 5075, id = 6693);
+
+ val uncrushedName by lazy { Definitions.item(uncrushed)!!.name }
+
+ companion object {
+ private const val PESTLE_AND_MORTAR = 233
+ private const val KNIFE = 5605
+
+ private val ingredients = CrushableIngredient.values().associateBy(CrushableIngredient::uncrushed)
+ operator fun get(id: Int): CrushableIngredient? = ingredients[id]
+
+ internal fun Int.isCrushable(): Boolean = this in ingredients
+ internal fun Int.isGrindingTool(): Boolean = this == KNIFE || this == PESTLE_AND_MORTAR
+ }
+}
+
+enum class NormalIngredient(override val id: Int) : Ingredient {
+ EYE_NEWT(id = 221),
+ RED_SPIDERS_EGGS(id = 223),
+ LIMPWURT_ROOT(id = 225),
+ SNAPE_GRASS(id = 231),
+ WHITE_BERRIES(id = 239),
+ WINE_ZAMORAK(id = 245),
+ JANGERBERRIES(id = 247),
+ TOADS_LEGS(id = 2152),
+ MORT_MYRE_FUNGI(id = 2970),
+ POTATO_CACTUS(id = 3138),
+ PHOENIX_FEATHER(id = 4621),
+ FROG_SPAWN(id = 5004),
+ PAPAYA_FRUIT(id = 5972),
+ POISON_IVY_BERRIES(id = 6018),
+ YEW_ROOTS(id = 6049),
+ MAGIC_ROOTS(id = 6051);
+
+ companion object {
+ private val ingredients = NormalIngredient.values().associateBy(NormalIngredient::id)
+ operator fun get(id: Int): NormalIngredient? = ingredients[id]
+
+ internal fun Int.isNormalIngredient(): Boolean = this in ingredients
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/skills/herblore/src/MakePotionAction.kt b/game/plugin/skills/herblore/src/MakePotionAction.kt
new file mode 100644
index 000000000..19f5ef94c
--- /dev/null
+++ b/game/plugin/skills/herblore/src/MakePotionAction.kt
@@ -0,0 +1,74 @@
+import java.util.*
+import org.apollo.game.action.ActionBlock
+import org.apollo.game.action.AsyncAction
+import org.apollo.game.model.Animation
+import org.apollo.game.model.entity.Player
+import org.apollo.game.model.entity.Skill
+import org.apollo.game.plugin.api.herblore
+
+abstract class MakePotionAction(
+ player: Player,
+ private val potion: Potion
+) : AsyncAction(1, true, player) {
+
+ override fun action(): ActionBlock = {
+ val level = mob.herblore.current
+
+ if (level < potion.level) {
+ mob.sendMessage("You need a Herblore level of ${potion.level} to make this.")
+ stop()
+ }
+
+ val inventory = mob.inventory
+
+ if (inventory.containsAll(*ingredients)) {
+ ingredients.forEach { inventory.remove(it) }
+ inventory.add(potion.id)
+
+ mob.playAnimation(MIXING_ANIMATION)
+ mob.sendMessage(message)
+ reward()
+ }
+ }
+
+ abstract val ingredients: IntArray
+ abstract val message: String
+
+ open fun reward() {}
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as MakePotionAction
+ return mob == other.mob && potion == other.potion
+ }
+
+ override fun hashCode(): Int = Objects.hash(mob, potion)
+
+ private companion object {
+ private val MIXING_ANIMATION = Animation(363)
+ }
+}
+
+class MakeFinishedPotionAction(
+ player: Player,
+ private val potion: FinishedPotion
+) : MakePotionAction(player, potion) {
+
+ override val ingredients = intArrayOf(potion.unfinished.id, potion.ingredient)
+ override val message by lazy { "You mix the ${potion.ingredientName} into your potion." }
+
+ override fun reward() {
+ mob.skillSet.addExperience(Skill.HERBLORE, potion.experience)
+ }
+}
+
+class MakeUnfinishedPotionAction(
+ player: Player,
+ private val potion: UnfinishedPotion
+) : MakePotionAction(player, potion) {
+
+ override val ingredients = intArrayOf(VIAL_OF_WATER, potion.herb)
+ override val message by lazy { "You put the ${potion.herbName} into the vial of water." }
+}
\ No newline at end of file
diff --git a/game/plugin/skills/herblore/src/Potion.kt b/game/plugin/skills/herblore/src/Potion.kt
new file mode 100644
index 000000000..ce9053ed5
--- /dev/null
+++ b/game/plugin/skills/herblore/src/Potion.kt
@@ -0,0 +1,78 @@
+import CrushableIngredient.CHOCOLATE_BAR
+import CrushableIngredient.DRAGON_SCALE
+import CrushableIngredient.UNICORN_HORN
+import NormalIngredient.*
+import UnfinishedPotion.*
+import org.apollo.game.plugin.api.Definitions
+
+const val VIAL_OF_WATER = 227
+
+interface Potion {
+ val id: Int
+ val level: Int
+}
+
+enum class UnfinishedPotion(override val id: Int, herb: Herb, override val level: Int) : Potion {
+ GUAM(id = 91, herb = Herb.GUAM_LEAF, level = 1),
+ MARRENTILL(id = 93, herb = Herb.MARRENTILL, level = 5),
+ TARROMIN(id = 95, herb = Herb.TARROMIN, level = 12),
+ HARRALANDER(id = 97, herb = Herb.HARRALANDER, level = 22),
+ RANARR(id = 99, herb = Herb.RANARR, level = 30),
+ TOADFLAX(id = 3002, herb = Herb.TOADFLAX, level = 34),
+ IRIT(id = 101, herb = Herb.IRIT_LEAF, level = 45),
+ AVANTOE(id = 103, herb = Herb.AVANTOE, level = 50),
+ KWUARM(id = 105, herb = Herb.KWUARM, level = 55),
+ SNAPDRAGON(id = 3004, herb = Herb.SNAPDRAGON, level = 63),
+ CADANTINE(id = 107, herb = Herb.CADANTINE, level = 66),
+ LANTADYME(id = 2483, herb = Herb.LANTADYME, level = 69),
+ DWARF_WEED(id = 109, herb = Herb.DWARF_WEED, level = 72),
+ TORSTOL(id = 111, herb = Herb.TORSTOL, level = 78);
+
+ val herb = herb.identified
+ val herbName: String = Definitions.item(herb.identified)!!.name
+
+ companion object {
+ private val ids = values().map(UnfinishedPotion::id).toHashSet()
+ private val potions = values().associateBy(UnfinishedPotion::herb)
+
+ operator fun get(id: Int) = potions[id]
+ internal fun Int.isUnfinished(): Boolean = this in ids
+ }
+}
+
+enum class FinishedPotion(
+ override val id: Int,
+ val unfinished: UnfinishedPotion,
+ ingredient: Ingredient,
+ override val level: Int,
+ val experience: Double
+) : Potion {
+ ATTACK(id = 121, unfinished = GUAM, ingredient = EYE_NEWT, level = 1, experience = 25.0),
+ ANTIPOISON(id = 175, unfinished = MARRENTILL, ingredient = UNICORN_HORN, level = 5, experience = 37.5),
+ STRENGTH(id = 115, unfinished = TARROMIN, ingredient = LIMPWURT_ROOT, level = 12, experience = 50.0),
+ RESTORE(id = 127, unfinished = HARRALANDER, ingredient = RED_SPIDERS_EGGS, level = 18, experience = 62.5),
+ ENERGY(id = 3010, unfinished = HARRALANDER, ingredient = CHOCOLATE_BAR, level = 26, experience = 67.5),
+ DEFENCE(id = 133, unfinished = RANARR, ingredient = WHITE_BERRIES, level = 30, experience = 75.0),
+ AGILITY(id = 3034, unfinished = TOADFLAX, ingredient = TOADS_LEGS, level = 34, experience = 80.0),
+ PRAYER(id = 139, unfinished = RANARR, ingredient = SNAPE_GRASS, level = 38, experience = 87.5),
+ SUPER_ATTACK(id = 145, unfinished = IRIT, ingredient = EYE_NEWT, level = 45, experience = 100.0),
+ SUPER_ANTIPOISON(id = 181, unfinished = IRIT, ingredient = UNICORN_HORN, level = 48, experience = 106.3),
+ FISHING(id = 151, unfinished = AVANTOE, ingredient = SNAPE_GRASS, level = 50, experience = 112.5),
+ SUPER_ENERGY(id = 3018, unfinished = AVANTOE, ingredient = MORT_MYRE_FUNGI, level = 52, experience = 117.5),
+ SUPER_STRENGTH(id = 157, unfinished = KWUARM, ingredient = LIMPWURT_ROOT, level = 55, experience = 125.0),
+ WEAPON_POISON(id = 187, unfinished = KWUARM, ingredient = DRAGON_SCALE, level = 60, experience = 137.5),
+ SUPER_RESTORE(id = 3026, unfinished = SNAPDRAGON, ingredient = RED_SPIDERS_EGGS, level = 63, experience = 142.5),
+ SUPER_DEFENCE(id = 163, unfinished = CADANTINE, ingredient = WHITE_BERRIES, level = 66, experience = 150.0),
+ ANTIFIRE(id = 2428, unfinished = LANTADYME, ingredient = DRAGON_SCALE, level = 69, experience = 157.5),
+ RANGING(id = 169, unfinished = DWARF_WEED, ingredient = WINE_ZAMORAK, level = 72, experience = 162.5),
+ MAGIC(id = 3042, unfinished = LANTADYME, ingredient = POTATO_CACTUS, level = 76, experience = 172.5),
+ ZAMORAK_BREW(id = 189, unfinished = TORSTOL, ingredient = JANGERBERRIES, level = 78, experience = 175.0);
+
+ val ingredientName = Definitions.item(ingredient.id)!!.name.toLowerCase()
+ val ingredient = ingredient.id
+
+ companion object {
+ private val potions = FinishedPotion.values().associateBy { Pair(it.unfinished.id, it.ingredient) }
+ operator fun get(key: Pair) = potions[key]
+ }
+}
diff --git a/game/plugin/skills/herblore/test/CrushIngredientActionTests.kt b/game/plugin/skills/herblore/test/CrushIngredientActionTests.kt
new file mode 100644
index 000000000..a81154a6d
--- /dev/null
+++ b/game/plugin/skills/herblore/test/CrushIngredientActionTests.kt
@@ -0,0 +1,63 @@
+import org.apollo.cache.def.ItemDefinition
+import org.apollo.game.model.entity.Player
+import org.apollo.game.plugin.testing.assertions.after
+import org.apollo.game.plugin.testing.assertions.startsWith
+import org.apollo.game.plugin.testing.assertions.verifyAfter
+import org.apollo.game.plugin.testing.junit.ApolloTestingExtension
+import org.apollo.game.plugin.testing.junit.api.ActionCapture
+import org.apollo.game.plugin.testing.junit.api.annotations.ItemDefinitions
+import org.apollo.game.plugin.testing.junit.api.annotations.TestMock
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+
+@ExtendWith(ApolloTestingExtension::class)
+internal class CrushIngredientActionTests {
+
+ @TestMock
+ lateinit var player: Player
+
+ @TestMock
+ lateinit var action: ActionCapture
+
+ private val ingredient = CrushableIngredient.BIRDS_NEST
+
+ @BeforeEach
+ internal fun startAction() {
+ player.inventory.add(ingredient.uncrushed)
+ player.startAction(CrushIngredientAction(player, ingredient))
+ }
+
+ @Test
+ internal fun `Preparing an uncrushed ingredient rewards a new ingredient after 2 ticks`() {
+ after(action.ticks(2), "ingredient removed and new ingredient added") {
+ assertEquals(0, player.inventory.getAmount(ingredient.uncrushed))
+ assertEquals(1, player.inventory.getAmount(ingredient.id))
+ }
+ }
+
+ @Test
+ internal fun `Preparing an uncrushed ingredient should send a message to the player after 2 ticks`() {
+ verifyAfter(action.ticks(2), "notification message sent to the player") {
+ player.sendMessage(startsWith("You carefully grind the to dust"))
+ }
+ }
+
+ @Test
+ internal fun `Preparing an uncrushed ingredient should play an animation on the first tick`() {
+ verifyAfter(action.ticks(1), "grinding animation played") {
+ player.playAnimation(match { it.id == 364 })
+ }
+ }
+
+ private companion object {
+ @ItemDefinitions
+ private val ingredients = CrushableIngredient.values()
+ .map { ItemDefinition(it.uncrushed).apply { name = "" } }
+
+ @ItemDefinitions
+ private val prepared = CrushableIngredient.values()
+ .map { ItemDefinition(it.id).apply { name = "" } }
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/skills/herblore/test/IdentifyHerbActionTests.kt b/game/plugin/skills/herblore/test/IdentifyHerbActionTests.kt
new file mode 100644
index 000000000..b5273c184
--- /dev/null
+++ b/game/plugin/skills/herblore/test/IdentifyHerbActionTests.kt
@@ -0,0 +1,66 @@
+import org.apollo.cache.def.ItemDefinition
+import org.apollo.game.model.Item
+import org.apollo.game.model.entity.Player
+import org.apollo.game.model.entity.Skill
+import org.apollo.game.plugin.testing.assertions.after
+import org.apollo.game.plugin.testing.assertions.startsWith
+import org.apollo.game.plugin.testing.assertions.verifyAfter
+import org.apollo.game.plugin.testing.junit.ApolloTestingExtension
+import org.apollo.game.plugin.testing.junit.api.ActionCapture
+import org.apollo.game.plugin.testing.junit.api.annotations.ItemDefinitions
+import org.apollo.game.plugin.testing.junit.api.annotations.TestMock
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+
+@ExtendWith(ApolloTestingExtension::class)
+internal class IdentifyHerbActionTests {
+
+ @TestMock
+ lateinit var player: Player
+
+ @TestMock
+ lateinit var action: ActionCapture
+
+ private val herb = Herb.GUAM_LEAF
+
+ @BeforeEach
+ internal fun startAction() {
+ player.inventory.set(0, Item(herb.unidentified))
+ player.startAction(IdentifyHerbAction(player, 0, herb))
+ }
+
+ @Test
+ internal fun `Identifying a herb should send a message if the player doesnt have the required level`() {
+ player.skillSet.setCurrentLevel(Skill.HERBLORE, 0)
+
+ verifyAfter(action.complete(), "level requirement message sent to player") {
+ player.sendMessage(startsWith("You need a Herblore level of"))
+ }
+ }
+
+ @Test
+ internal fun `Identifying a herb should remove the undentified herb`() {
+ after(action.complete()) {
+ assertEquals(0, player.inventory.getAmount(herb.unidentified))
+ }
+ }
+
+ @Test
+ internal fun `Identifying a herb should add the identified herb to the players inventory`() {
+ after(action.complete()) {
+ assertEquals(1, player.inventory.getAmount(herb.identified))
+ }
+ }
+
+ private companion object {
+ @ItemDefinitions
+ val identifiedHerbs = Herb.values()
+ .map { ItemDefinition(it.identified).apply { name = "" } }
+
+ @ItemDefinitions
+ val unidentifiedHerbs = Herb.values()
+ .map { ItemDefinition(it.unidentified).apply { name = "" } }
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/skills/mining/build.gradle b/game/plugin/skills/mining/build.gradle
new file mode 100644
index 000000000..4d96c9f57
--- /dev/null
+++ b/game/plugin/skills/mining/build.gradle
@@ -0,0 +1,10 @@
+apply plugin: 'kotlin'
+
+dependencies {
+ implementation project(':game')
+ implementation project(':cache')
+ implementation project(':net')
+ implementation project(':util')
+ implementation project(':game:plugin:api')
+ testImplementation project(':game:plugin-testing')
+}
diff --git a/game/plugin/skills/mining/src/ExpiredProspectingAction.kt b/game/plugin/skills/mining/src/ExpiredProspectingAction.kt
new file mode 100644
index 000000000..41f3229e6
--- /dev/null
+++ b/game/plugin/skills/mining/src/ExpiredProspectingAction.kt
@@ -0,0 +1,41 @@
+import java.util.*
+import org.apollo.game.action.DistancedAction
+import org.apollo.game.message.impl.ObjectActionMessage
+import org.apollo.game.model.Position
+import org.apollo.game.model.entity.Player
+
+class ExpiredProspectingAction(
+ mob: Player,
+ position: Position
+) : DistancedAction(DELAY, true, mob, position, ORE_SIZE) {
+
+ companion object {
+ private const val DELAY = 0
+ private const val ORE_SIZE = 1
+
+ /**
+ * Starts a [ExpiredProspectingAction] for the specified [Player], terminating the [Message] that triggered it.
+ */
+ fun start(message: ObjectActionMessage, player: Player) {
+ val action = ExpiredProspectingAction(player, message.position)
+ player.startAction(action)
+
+ message.terminate()
+ }
+ }
+
+ override fun executeAction() {
+ mob.sendMessage("There is currently no ore available in this rock.")
+ stop()
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as ExpiredProspectingAction
+ return mob == other.mob && position == other.position
+ }
+
+ override fun hashCode(): Int = Objects.hash(mob, position)
+}
\ No newline at end of file
diff --git a/game/plugin/skills/mining/src/Gem.kt b/game/plugin/skills/mining/src/Gem.kt
new file mode 100644
index 000000000..2252ea4e4
--- /dev/null
+++ b/game/plugin/skills/mining/src/Gem.kt
@@ -0,0 +1,13 @@
+package org.apollo.game.plugin.skills.mining
+
+enum class Gem(val id: Int) { // TODO add gem drop chances
+ UNCUT_SAPPHIRE(1623),
+ UNCUT_EMERALD(1605),
+ UNCUT_RUBY(1619),
+ UNCUT_DIAMOND(1617);
+
+ companion object {
+ private val GEMS = Gem.values().associateBy({ it.id }, { it })
+ operator fun get(id: Int): Gem? = GEMS[id]
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/skills/mining/src/Mining.plugin.kts b/game/plugin/skills/mining/src/Mining.plugin.kts
new file mode 100644
index 000000000..8b3f61d1e
--- /dev/null
+++ b/game/plugin/skills/mining/src/Mining.plugin.kts
@@ -0,0 +1,27 @@
+import org.apollo.game.message.impl.ObjectActionMessage
+import org.apollo.game.plugin.skills.mining.Ore
+
+on { ObjectActionMessage::class }
+ .where { option == Actions.MINING }
+ .then { player ->
+ Ore.fromRock(id)?.let { ore ->
+ MiningAction.start(this, player, ore)
+ }
+ }
+
+on { ObjectActionMessage::class }
+ .where { option == Actions.PROSPECTING }
+ .then { player ->
+ val ore = Ore.fromRock(id)
+
+ if (ore != null) {
+ ProspectingAction.start(this, player, ore)
+ } else if (Ore.fromExpiredRock(id) != null) {
+ ExpiredProspectingAction.start(this, player)
+ }
+ }
+
+private object Actions {
+ const val MINING = 1
+ const val PROSPECTING = 2
+}
\ No newline at end of file
diff --git a/game/plugin/skills/mining/src/MiningAction.kt b/game/plugin/skills/mining/src/MiningAction.kt
new file mode 100644
index 000000000..0f159b388
--- /dev/null
+++ b/game/plugin/skills/mining/src/MiningAction.kt
@@ -0,0 +1,83 @@
+import java.util.*
+import org.apollo.game.action.ActionBlock
+import org.apollo.game.action.AsyncDistancedAction
+import org.apollo.game.message.impl.ObjectActionMessage
+import org.apollo.game.model.entity.Player
+import org.apollo.game.plugin.api.*
+import org.apollo.game.plugin.skills.mining.Ore
+import org.apollo.game.plugin.skills.mining.Pickaxe
+import org.apollo.net.message.Message
+
+class MiningAction(
+ player: Player,
+ private val tool: Pickaxe,
+ private val target: MiningTarget
+) : AsyncDistancedAction(PULSES, true, player, target.position, ORE_SIZE) {
+
+ companion object {
+ private const val PULSES = 0
+ private const val ORE_SIZE = 1
+
+ /**
+ * Starts a [MiningAction] for the specified [Player], terminating the [Message] that triggered it.
+ */
+ fun start(message: ObjectActionMessage, player: Player, ore: Ore) {
+ val pickaxe = Pickaxe.bestFor(player)
+
+ if (pickaxe == null) {
+ player.sendMessage("You do not have a pickaxe for which you have the level to use.")
+ } else {
+ val target = MiningTarget(message.id, message.position, ore)
+ val action = MiningAction(player, pickaxe, target)
+
+ player.startAction(action)
+ }
+
+ message.terminate()
+ }
+ }
+
+ override fun action(): ActionBlock = {
+ mob.turnTo(position)
+
+ if (!target.skillRequirementsMet(mob)) {
+ mob.sendMessage("You do not have the required level to mine this rock.")
+ stop()
+ }
+
+ mob.sendMessage("You swing your pick at the rock.")
+ mob.playAnimation(tool.animation)
+
+ wait(tool.pulses)
+
+ if (!target.isValid(mob.world)) {
+ stop()
+ }
+
+ val successChance = rand(100)
+
+ if (target.isSuccessful(mob, successChance)) {
+ if (mob.inventory.freeSlots() == 0) {
+ mob.inventory.forceCapacityExceeded()
+ stop()
+ }
+
+ if (target.reward(mob)) {
+ mob.sendMessage("You manage to mine some ${target.oreName()}")
+ target.deplete(mob.world)
+
+ stop()
+ }
+ }
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as MiningAction
+ return mob == other.mob && target == other.target
+ }
+
+ override fun hashCode(): Int = Objects.hash(mob, target)
+}
diff --git a/game/plugin/skills/mining/src/MiningTarget.kt b/game/plugin/skills/mining/src/MiningTarget.kt
new file mode 100644
index 000000000..5bbe1b4a1
--- /dev/null
+++ b/game/plugin/skills/mining/src/MiningTarget.kt
@@ -0,0 +1,58 @@
+import org.apollo.game.model.Position
+import org.apollo.game.model.World
+import org.apollo.game.model.entity.Player
+import org.apollo.game.plugin.api.Definitions
+import org.apollo.game.plugin.api.findObject
+import org.apollo.game.plugin.api.mining
+import org.apollo.game.plugin.api.replaceObject
+import org.apollo.game.plugin.skills.mining.Ore
+
+data class MiningTarget(val objectId: Int, val position: Position, val ore: Ore) {
+
+ /**
+ * Deplete this mining resource from the [World], and schedule it to be respawned
+ * in a number of ticks specified by the [Ore].
+ */
+ fun deplete(world: World) {
+ val obj = world.findObject(position, objectId)!!
+
+ world.replaceObject(obj, ore.objects[objectId]!!, ore.respawn)
+ }
+
+ /**
+ * Check if the [Player] was successful in mining this ore with a random success [chance] value between 0 and 100.
+ */
+ fun isSuccessful(mob: Player, chance: Int): Boolean {
+ val percent = (ore.chance * mob.mining.current + ore.chanceOffset) * 100
+ return chance < percent
+ }
+
+ /**
+ * Check if this target is still valid in the [World] (i.e. has not been [deplete]d).
+ */
+ fun isValid(world: World) = world.findObject(position, objectId) != null
+
+ /**
+ * Get the normalized name of the [Ore] represented by this target.
+ */
+ fun oreName() = Definitions.item(ore.id).name.toLowerCase()
+
+ /**
+ * Reward a [player] with experience and ore if they have the inventory capacity to take a new ore.
+ */
+ fun reward(player: Player): Boolean {
+ val hasInventorySpace = player.inventory.add(ore.id)
+
+ if (hasInventorySpace) {
+ player.mining.experience += ore.exp
+ }
+
+ return hasInventorySpace
+ }
+
+ /**
+ * Check if the [mob] has met the skill requirements to mine te [Ore] represented by
+ * this [MiningTarget].
+ */
+ fun skillRequirementsMet(mob: Player) = mob.mining.current < ore.level
+}
\ No newline at end of file
diff --git a/game/plugin/skills/mining/src/Ore.kt b/game/plugin/skills/mining/src/Ore.kt
new file mode 100644
index 000000000..1f4a94ff9
--- /dev/null
+++ b/game/plugin/skills/mining/src/Ore.kt
@@ -0,0 +1,153 @@
+package org.apollo.game.plugin.skills.mining
+
+/*
+Thanks to Mikey` for helping
+to find some of the item/object IDs, minimum levels and experiences.
+Thanks to Clifton for helping
+to find some of the expired object IDs.
+ */
+/**
+ * Chance values thanks to: http://runescape.wikia.com/wiki/Talk:Mining#Mining_success_rate_formula
+ * Respawn times and xp thanks to: http://oldschoolrunescape.wikia.com/wiki/
+ */
+enum class Ore(
+ val objects: Map,
+ val id: Int,
+ val level: Int,
+ val exp: Double,
+ val respawn: Int,
+ val chance: Double,
+ val chanceOffset: Double = 0.0
+) {
+ CLAY(CLAY_OBJECTS, id = 434, level = 1, exp = 5.0, respawn = 1, chance = 0.0085, chanceOffset = 0.45),
+ COPPER(COPPER_OBJECTS, id = 436, level = 1, exp = 17.5, respawn = 4, chance = 0.0085, chanceOffset = 0.45),
+ TIN(TIN_OBJECTS, id = 438, level = 1, exp = 17.5, respawn = 4, chance = 0.0085, chanceOffset = 0.45),
+ IRON(IRON_OBJECTS, id = 440, level = 15, exp = 35.0, respawn = 9, chance = 0.0085, chanceOffset = 0.45),
+ COAL(COAL_OBJECTS, id = 453, level = 30, exp = 50.0, respawn = 50, chance = 0.004),
+ GOLD(GOLD_OBJECTS, id = 444, level = 40, exp = 65.0, respawn = 100, chance = 0.003),
+ SILVER(SILVER_OBJECTS, id = 442, level = 20, exp = 40.0, respawn = 100, chance = 0.0085),
+ MITHRIL(MITHRIL_OBJECTS, id = 447, level = 55, exp = 80.0, respawn = 200, chance = 0.002),
+ ADAMANT(ADAMANT_OBJECTS, id = 449, level = 70, exp = 95.0, respawn = 800, chance = 0.001),
+ RUNITE(RUNITE_OBJECTS, id = 451, level = 85, exp = 125.0, respawn = 1_200, chance = 0.0008);
+
+ companion object {
+ private val ORE_ROCKS = Ore.values().flatMap { ore -> ore.objects.map { Pair(it.key, ore) } }.toMap()
+ private val EXPIRED_ORE = Ore.values().flatMap { ore -> ore.objects.map { Pair(it.value, ore) } }.toMap()
+
+ fun fromRock(id: Int): Ore? = ORE_ROCKS[id]
+ fun fromExpiredRock(id: Int): Ore? = EXPIRED_ORE[id]
+ }
+}
+
+// Maps from regular rock id to expired rock id.
+
+val CLAY_OBJECTS = mapOf(
+ 2108 to 450,
+ 2109 to 451,
+ 14904 to 14896,
+ 14905 to 14897
+)
+
+val COPPER_OBJECTS = mapOf(
+ 11960 to 11555,
+ 11961 to 11556,
+ 11962 to 11557,
+ 11936 to 11552,
+ 11937 to 11553,
+ 11938 to 11554,
+ 2090 to 450,
+ 2091 to 451,
+ 14906 to 14898,
+ 14907 to 14899,
+ 14856 to 14832,
+ 14857 to 14833,
+ 14858 to 14834
+)
+
+val TIN_OBJECTS = mapOf(
+ 11597 to 11555,
+ 11958 to 11556,
+ 11959 to 11557,
+ 11933 to 11552,
+ 11934 to 11553,
+ 11935 to 11554,
+ 2094 to 450,
+ 2095 to 451,
+ 14092 to 14894,
+ 14903 to 14895
+)
+
+val IRON_OBJECTS = mapOf(
+ 11954 to 11555,
+ 11955 to 11556,
+ 11956 to 11557,
+ 2092 to 450,
+ 2093 to 451,
+ 14900 to 14892,
+ 14901 to 14893,
+ 14913 to 14915,
+ 14914 to 14916
+)
+
+val COAL_OBJECTS = mapOf(
+ 11963 to 11555,
+ 11964 to 11556,
+ 11965 to 11557,
+ 11930 to 11552,
+ 11931 to 11553,
+ 11932 to 11554,
+ 2096 to 450,
+ 2097 to 451,
+ 14850 to 14832,
+ 14851 to 14833,
+ 14852 to 14834
+)
+
+val SILVER_OBJECTS = mapOf(
+ 11948 to 11555,
+ 11949 to 11556,
+ 11950 to 11557,
+ 2100 to 450,
+ 2101 to 451
+)
+
+val GOLD_OBJECTS = mapOf(
+ 11951 to 11555,
+ 11952 to 11556,
+ 11953 to 11557,
+ 2098 to 450,
+ 2099 to 451
+)
+
+val MITHRIL_OBJECTS = mapOf(
+ 11945 to 11555,
+ 11946 to 11556,
+ 11947 to 11557,
+ 11942 to 11552,
+ 11943 to 11553,
+ 11944 to 11554,
+ 2102 to 450,
+ 2103 to 451,
+ 14853 to 14832,
+ 14854 to 14833,
+ 14855 to 14834
+)
+
+val ADAMANT_OBJECTS = mapOf(
+ 11939 to 11552,
+ 11940 to 11553,
+ 11941 to 11554,
+ 2104 to 450,
+ 2105 to 451,
+ 14862 to 14832,
+ 14863 to 14833,
+ 14864 to 14834
+)
+
+val RUNITE_OBJECTS = mapOf(
+ 2106 to 450,
+ 2107 to 451,
+ 14859 to 14832,
+ 14860 to 14833,
+ 14861 to 14834
+)
\ No newline at end of file
diff --git a/game/plugin/skills/mining/src/Pickaxe.kt b/game/plugin/skills/mining/src/Pickaxe.kt
new file mode 100644
index 000000000..45071f0c9
--- /dev/null
+++ b/game/plugin/skills/mining/src/Pickaxe.kt
@@ -0,0 +1,27 @@
+package org.apollo.game.plugin.skills.mining
+
+import org.apollo.game.model.Animation
+import org.apollo.game.model.entity.Player
+import org.apollo.game.plugin.api.mining
+
+enum class Pickaxe(val id: Int, val level: Int, animation: Int, val pulses: Int) {
+ BRONZE(id = 1265, level = 1, animation = 625, pulses = 8),
+ ITRON(id = 1267, level = 1, animation = 626, pulses = 7),
+ STEEL(id = 1269, level = 6, animation = 627, pulses = 6),
+ MITHRIL(id = 1273, level = 21, animation = 629, pulses = 5),
+ ADAMANT(id = 1271, level = 31, animation = 628, pulses = 4),
+ RUNE(id = 1275, level = 41, animation = 624, pulses = 3);
+
+ val animation = Animation(animation)
+
+ companion object {
+ private val PICKAXES = Pickaxe.values().sortedByDescending { it.level }
+
+ fun bestFor(player: Player): Pickaxe? {
+ return PICKAXES.asSequence()
+ .filter { it.level <= player.mining.current }
+ .filter { player.equipment.contains(it.id) || player.inventory.contains(it.id) }
+ .firstOrNull()
+ }
+ }
+}
diff --git a/game/plugin/skills/mining/src/ProspectingAction.kt b/game/plugin/skills/mining/src/ProspectingAction.kt
new file mode 100644
index 000000000..10bf83a11
--- /dev/null
+++ b/game/plugin/skills/mining/src/ProspectingAction.kt
@@ -0,0 +1,53 @@
+import java.util.Objects
+import org.apollo.game.action.ActionBlock
+import org.apollo.game.action.AsyncDistancedAction
+import org.apollo.game.message.impl.ObjectActionMessage
+import org.apollo.game.model.Position
+import org.apollo.game.model.entity.Player
+import org.apollo.game.plugin.api.Definitions
+import org.apollo.game.plugin.skills.mining.Ore
+import org.apollo.net.message.Message
+
+class ProspectingAction(
+ player: Player,
+ position: Position,
+ private val ore: Ore
+) : AsyncDistancedAction(DELAY, true, player, position, ORE_SIZE) {
+
+ companion object {
+ private const val DELAY = 3
+ private const val ORE_SIZE = 1
+
+ /**
+ * Starts a [MiningAction] for the specified [Player], terminating the [Message] that triggered it.
+ */
+ fun start(message: ObjectActionMessage, player: Player, ore: Ore) {
+ val action = ProspectingAction(player, message.position, ore)
+ player.startAction(action)
+
+ message.terminate()
+ }
+ }
+
+ override fun action(): ActionBlock = {
+ mob.sendMessage("You examine the rock for ores...")
+ mob.turnTo(position)
+
+ wait()
+
+ val oreName = Definitions.item(ore.id)?.name?.toLowerCase()
+ mob.sendMessage("This rock contains $oreName.")
+
+ stop()
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as ProspectingAction
+ return mob == other.mob && position == other.position && ore == other.ore
+ }
+
+ override fun hashCode(): Int = Objects.hash(mob, position, ore)
+}
diff --git a/game/plugin/skills/mining/test/MiningActionTests.kt b/game/plugin/skills/mining/test/MiningActionTests.kt
new file mode 100644
index 000000000..59e9a6bec
--- /dev/null
+++ b/game/plugin/skills/mining/test/MiningActionTests.kt
@@ -0,0 +1,90 @@
+
+import io.mockk.every
+import io.mockk.spyk
+import io.mockk.staticMockk
+import io.mockk.verify
+import org.apollo.cache.def.ItemDefinition
+import org.apollo.game.model.World
+import org.apollo.game.model.entity.Player
+import org.apollo.game.model.entity.Skill
+import org.apollo.game.plugin.api.replaceObject
+import org.apollo.game.plugin.skills.mining.Ore
+import org.apollo.game.plugin.skills.mining.Pickaxe
+import org.apollo.game.plugin.skills.mining.TIN_OBJECTS
+import org.apollo.game.plugin.testing.assertions.after
+import org.apollo.game.plugin.testing.assertions.contains
+import org.apollo.game.plugin.testing.assertions.verifyAfter
+import org.apollo.game.plugin.testing.junit.ApolloTestingExtension
+import org.apollo.game.plugin.testing.junit.api.ActionCapture
+import org.apollo.game.plugin.testing.junit.api.annotations.ItemDefinitions
+import org.apollo.game.plugin.testing.junit.api.annotations.TestMock
+import org.apollo.game.plugin.testing.junit.api.interactions.spawnObject
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+
+@ExtendWith(ApolloTestingExtension::class)
+class MiningActionTests {
+
+ @TestMock
+ lateinit var world: World
+
+ @TestMock
+ lateinit var player: Player
+
+ @TestMock
+ lateinit var action: ActionCapture
+
+ @Test
+ fun `Attempting to mine a rock we don't have the skill to should send the player a message`() {
+ val obj = world.spawnObject(1, player.position)
+ val target = spyk(MiningTarget(obj.id, obj.position, Ore.TIN))
+
+ every { target.skillRequirementsMet(player) } returns false
+
+ player.startAction(MiningAction(player, Pickaxe.BRONZE, target))
+
+ verifyAfter(action.complete()) {
+ player.sendMessage(contains("do not have the required level"))
+ }
+ }
+
+ @Test
+ fun `Mining a rock we have the skill to mine should eventually reward ore and experience`() {
+ val (tinId, expiredTinId) = TIN_OBJ_IDS
+ val obj = world.spawnObject(tinId, player.position)
+ val target = spyk(MiningTarget(obj.id, obj.position, Ore.TIN))
+ staticMockk("org.apollo.game.plugin.api.WorldKt").mock()
+
+ every { target.skillRequirementsMet(player) } returns true
+ every { target.isSuccessful(player, any()) } returns true
+ every { world.replaceObject(obj, any(), any()) } answers { }
+
+ player.skillSet.setCurrentLevel(Skill.MINING, Ore.TIN.level)
+ player.startAction(MiningAction(player, Pickaxe.BRONZE, target))
+
+ verifyAfter(action.ticks(1)) {
+ player.sendMessage(contains("You swing your pick"))
+ }
+
+ after(action.complete()) {
+ verify { player.sendMessage("You manage to mine some ") }
+ verify { world.replaceObject(obj, expiredTinId, Ore.TIN.respawn) }
+
+ assertTrue(player.inventory.contains(Ore.TIN.id))
+ assertEquals(player.skillSet.getExperience(Skill.MINING), Ore.TIN.exp)
+ }
+ }
+
+ private companion object {
+ private val TIN_OBJ_IDS = TIN_OBJECTS.entries.first()
+
+ @ItemDefinitions
+ fun ores() = Ore.values()
+ .map { ItemDefinition(it.id).apply { name = "" } }
+
+ @ItemDefinitions
+ fun pickaxes() = listOf(ItemDefinition(Pickaxe.BRONZE.id))
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/skills/mining/test/PickaxeTests.kt b/game/plugin/skills/mining/test/PickaxeTests.kt
new file mode 100644
index 000000000..70112ec14
--- /dev/null
+++ b/game/plugin/skills/mining/test/PickaxeTests.kt
@@ -0,0 +1,73 @@
+import org.apollo.cache.def.ItemDefinition
+import org.apollo.game.model.entity.Player
+import org.apollo.game.model.entity.Skill
+import org.apollo.game.plugin.skills.mining.Pickaxe
+import org.apollo.game.plugin.testing.junit.ApolloTestingExtension
+import org.apollo.game.plugin.testing.junit.api.annotations.ItemDefinitions
+import org.apollo.game.plugin.testing.junit.api.annotations.TestMock
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.extension.ExtendWith
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.EnumSource
+
+@ExtendWith(ApolloTestingExtension::class)
+class PickaxeTests {
+
+ @TestMock
+ lateinit var player: Player
+
+ @ParameterizedTest
+ @EnumSource(Pickaxe::class)
+ fun `No pickaxe is chosen if none are available`(pickaxe: Pickaxe) {
+ player.skillSet.setCurrentLevel(Skill.MINING, pickaxe.level)
+
+ assertEquals(null, Pickaxe.bestFor(player))
+ }
+
+ @ParameterizedTest
+ @EnumSource(Pickaxe::class)
+ fun `The highest level pickaxe is chosen when available`(pickaxe: Pickaxe) {
+ player.skillSet.setCurrentLevel(Skill.MINING, pickaxe.level)
+ player.inventory.add(pickaxe.id)
+
+ assertEquals(pickaxe, Pickaxe.bestFor(player))
+ }
+
+ @ParameterizedTest
+ @EnumSource(Pickaxe::class)
+ fun `Only pickaxes the player has are chosen`(pickaxe: Pickaxe) {
+ player.skillSet.setCurrentLevel(Skill.MINING, pickaxe.level)
+ player.inventory.add(Pickaxe.BRONZE.id)
+
+ assertEquals(Pickaxe.BRONZE, Pickaxe.bestFor(player))
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = Pickaxe::class)
+ fun `Pickaxes can be chosen from equipment as well as inventory`(pickaxe: Pickaxe) {
+ player.skillSet.setCurrentLevel(Skill.MINING, pickaxe.level)
+ player.inventory.add(pickaxe.id)
+
+ assertEquals(pickaxe, Pickaxe.bestFor(player))
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = Pickaxe::class)
+ fun `Pickaxes with a level requirement higher than the player's are ignored`(pickaxe: Pickaxe) {
+ player.skillSet.setCurrentLevel(Skill.MINING, pickaxe.level)
+ player.inventory.add(pickaxe.id)
+
+ Pickaxe.values()
+ .filter { it.level > pickaxe.level }
+ .forEach { player.inventory.add(it.id) }
+
+ assertEquals(pickaxe, Pickaxe.bestFor(player))
+ }
+
+ private companion object {
+ @ItemDefinitions
+ fun pickaxes() = Pickaxe.values().map {
+ ItemDefinition(it.id).apply { isStackable = false }
+ }
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/skills/mining/test/ProspectingTests.kt b/game/plugin/skills/mining/test/ProspectingTests.kt
new file mode 100644
index 000000000..27d11c712
--- /dev/null
+++ b/game/plugin/skills/mining/test/ProspectingTests.kt
@@ -0,0 +1,48 @@
+
+import org.apollo.cache.def.ItemDefinition
+import org.apollo.game.model.entity.Player
+import org.apollo.game.plugin.skills.mining.Ore
+import org.apollo.game.plugin.testing.assertions.contains
+import org.apollo.game.plugin.testing.assertions.verifyAfter
+import org.apollo.game.plugin.testing.junit.ApolloTestingExtension
+import org.apollo.game.plugin.testing.junit.api.ActionCapture
+import org.apollo.game.plugin.testing.junit.api.annotations.ItemDefinitions
+import org.apollo.game.plugin.testing.junit.api.annotations.TestMock
+import org.apollo.game.plugin.testing.junit.api.interactions.interactWithObject
+import org.junit.jupiter.api.extension.ExtendWith
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.ArgumentsSource
+
+@ExtendWith(ApolloTestingExtension::class)
+class ProspectingTests {
+
+ @TestMock
+ lateinit var player: Player
+
+ @TestMock
+ lateinit var action: ActionCapture
+
+ @ParameterizedTest
+ @ArgumentsSource(MiningTestDataProvider::class)
+ fun `Prospecting a rock should reveal the type of ore it contains`(data: MiningTestData) {
+ player.interactWithObject(data.rockId, 2)
+
+ verifyAfter(action.ticks(1)) { player.sendMessage(contains("examine the rock")) }
+ verifyAfter(action.complete()) { player.sendMessage(contains("This rock contains ")) }
+ }
+
+ @ParameterizedTest
+ @ArgumentsSource(MiningTestDataProvider::class)
+ fun `Prospecting an expired rock should reveal it contains no ore`(data: MiningTestData) {
+ player.interactWithObject(data.expiredRockId, 2)
+
+ verifyAfter(action.complete()) { player.sendMessage(contains("no ore available in this rock")) }
+ }
+
+ private companion object {
+ @ItemDefinitions
+ fun ores() = Ore.values().map {
+ ItemDefinition(it.id).also { it.name = "" }
+ }
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/skills/mining/test/TestData.kt b/game/plugin/skills/mining/test/TestData.kt
new file mode 100644
index 000000000..aa798d24a
--- /dev/null
+++ b/game/plugin/skills/mining/test/TestData.kt
@@ -0,0 +1,17 @@
+import java.util.stream.Stream
+import org.apollo.game.plugin.skills.mining.Ore
+import org.junit.jupiter.api.extension.ExtensionContext
+import org.junit.jupiter.params.provider.Arguments
+import org.junit.jupiter.params.provider.ArgumentsProvider
+
+data class MiningTestData(val rockId: Int, val expiredRockId: Int, val ore: Ore)
+
+fun miningTestData(): Collection = Ore.values()
+ .flatMap { ore -> ore.objects.map { MiningTestData(it.key, it.value, ore) } }
+ .toList()
+
+class MiningTestDataProvider : ArgumentsProvider {
+ override fun provideArguments(context: ExtensionContext?): Stream {
+ return miningTestData().map { Arguments { arrayOf(it) } }.stream()
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/skills/prayer/build.gradle b/game/plugin/skills/prayer/build.gradle
new file mode 100644
index 000000000..4d96c9f57
--- /dev/null
+++ b/game/plugin/skills/prayer/build.gradle
@@ -0,0 +1,10 @@
+apply plugin: 'kotlin'
+
+dependencies {
+ implementation project(':game')
+ implementation project(':cache')
+ implementation project(':net')
+ implementation project(':util')
+ implementation project(':game:plugin:api')
+ testImplementation project(':game:plugin-testing')
+}
diff --git a/game/plugin/skills/prayer/src/Bone.kt b/game/plugin/skills/prayer/src/Bone.kt
new file mode 100644
index 000000000..9f3c73c13
--- /dev/null
+++ b/game/plugin/skills/prayer/src/Bone.kt
@@ -0,0 +1,28 @@
+enum class Bone(val id: Int, val xp: Double) {
+ REGULAR_BONES(id = 526, xp = 5.0),
+ BURNT_BONES(id = 528, xp = 5.0),
+ BAT_BONES(id = 530, xp = 4.0),
+ BIG_BONES(id = 532, xp = 45.0),
+ BABY_DRAGON_BONES(id = 534, xp = 30.0),
+ DRAGON_BONES(id = 536, xp = 72.0),
+ WOLF_BONES(id = 2859, xp = 14.0),
+ SHAIKAHAN_BONES(id = 3123, xp = 25.0),
+ JOGRE_BONES(id = 3125, xp = 15.0),
+ BURNT_ZOGRE_BONES(id = 3127, xp = 25.0),
+ MONKEY_BONES_SMALL_0(id = 3179, xp = 14.0),
+ MONKEY_BONES_MEDIUM(id = 3180, xp = 14.0),
+ MONKEY_BONES_LARGE_0(id = 3181, xp = 14.0),
+ MONKEY_BONES_LARGE_1(id = 3182, xp = 14.0),
+ MONKEY_BONES_SMALL_1(id = 3183, xp = 14.0),
+ SHAKING_BONES(id = 3187, xp = 14.0),
+ FAYRG_BONES(id = 4830, xp = 84.0),
+ RAURG_BONES(id = 4832, xp = 96.0),
+ OURG_BONES(id = 4834, xp = 140.0);
+
+ companion object {
+ private val BONES = Bone.values().associateBy(Bone::id)
+
+ operator fun get(id: Int) = BONES[id]
+ internal fun Int.isBone(): Boolean = this in BONES
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/skills/prayer/src/BuryBoneAction.kt b/game/plugin/skills/prayer/src/BuryBoneAction.kt
new file mode 100644
index 000000000..3b6720caf
--- /dev/null
+++ b/game/plugin/skills/prayer/src/BuryBoneAction.kt
@@ -0,0 +1,31 @@
+import org.apollo.game.action.ActionBlock
+import org.apollo.game.action.AsyncAction
+import org.apollo.game.model.Animation
+import org.apollo.game.model.entity.Player
+import org.apollo.game.plugin.api.prayer
+
+class BuryBoneAction(
+ player: Player,
+ private val slot: Int,
+ private val bone: Bone
+) : AsyncAction(0, true, player) {
+
+ override fun action(): ActionBlock = {
+ if (mob.inventory.removeSlot(slot, 1) > 0) {
+ mob.sendMessage("You dig a hole in the ground...")
+ mob.playAnimation(BURY_BONE_ANIMATION)
+
+ wait(pulses = 1)
+
+ mob.sendMessage("You bury the bones.")
+ mob.prayer.experience += bone.xp
+ }
+
+ stop()
+ }
+
+ companion object {
+ public val BURY_BONE_ANIMATION = Animation(827)
+ internal const val BURY_OPTION = 1
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/skills/prayer/src/Prayer.kt b/game/plugin/skills/prayer/src/Prayer.kt
new file mode 100644
index 000000000..3f281cc05
--- /dev/null
+++ b/game/plugin/skills/prayer/src/Prayer.kt
@@ -0,0 +1,44 @@
+import com.google.common.collect.MultimapBuilder
+import com.google.common.collect.SetMultimap
+import org.apollo.game.message.impl.ConfigMessage
+import org.apollo.game.model.entity.Player
+
+enum class Prayer(val button: Int, val level: Int, val setting: Int, val drain: Double) {
+ THICK_SKIN(button = 5609, level = 1, setting = 83, drain = 0.01),
+ BURST_OF_STRENGTH(button = 5610, level = 4, setting = 84, drain = 0.01),
+ CLARITY_OF_THOUGHT(button = 5611, level = 7, setting = 85, drain = 0.01),
+ ROCK_SKIN(button = 5612, level = 10, setting = 86, drain = 0.04),
+ SUPERHUMAN_STRENGTH(button = 5613, level = 13, setting = 87, drain = 0.04),
+ IMPROVED_REFLEXES(button = 5614, level = 16, setting = 88, drain = 0.04),
+ RAPID_RESTORE(button = 5615, level = 19, setting = 89, drain = 0.01),
+ RAPID_HEAL(button = 5615, level = 22, setting = 90, drain = 0.01),
+ PROTECT_ITEM(button = 5617, level = 25, setting = 91, drain = 0.01),
+ STEEL_SKIN(button = 5618, level = 28, setting = 92, drain = 0.1),
+ ULTIMATE_STRENGTH(button = 5619, level = 31, setting = 93, drain = 0.1),
+ INCREDIBLE_REFLEXES(button = 5620, level = 34, setting = 94, drain = 0.1),
+ PROTECT_FROM_MAGIC(button = 5621, level = 37, setting = 95, drain = 0.15),
+ PROTECT_FROM_MISSILES(button = 5622, level = 40, setting = 96, drain = 0.15),
+ PROTECT_FROM_MELEE(button = 5623, level = 43, setting = 97, drain = 0.15),
+ RETRIBUTION(button = 683, level = 46, setting = 98, drain = 0.15),
+ REDEMPTION(button = 684, level = 49, setting = 99, drain = 0.15),
+ SMITE(button = 685, level = 52, setting = 100, drain = 0.2);
+
+ companion object {
+ private val prayers = Prayer.values().associateBy(Prayer::button)
+
+ fun forButton(button: Int) = prayers[button]
+ internal fun Int.isPrayerButton(): Boolean = this in prayers
+ }
+}
+
+val Player.currentPrayers: Set
+ get() = playerPrayers[this]
+
+fun Player.updatePrayer(prayer: Prayer) {
+ val value = if (currentPrayers.contains(prayer)) 1 else 0
+ send(ConfigMessage(prayer.setting, value))
+}
+
+internal val playerPrayers: SetMultimap = MultimapBuilder.hashKeys()
+ .enumSetValues(Prayer::class.java)
+ .build()
\ No newline at end of file
diff --git a/game/plugin/skills/prayer/src/Prayer.plugin.kts b/game/plugin/skills/prayer/src/Prayer.plugin.kts
new file mode 100644
index 000000000..2338d8332
--- /dev/null
+++ b/game/plugin/skills/prayer/src/Prayer.plugin.kts
@@ -0,0 +1,37 @@
+import Bone.Companion.isBone
+import Prayer.Companion.isPrayerButton
+import org.apollo.game.message.impl.ButtonMessage
+import org.apollo.game.message.impl.ItemOptionMessage
+import org.apollo.game.model.event.impl.LogoutEvent
+import org.apollo.game.plugin.api.prayer
+
+// Clear the player's prayer(s) on logout
+on_player_event { LogoutEvent::class }
+ .then {
+ playerPrayers.removeAll(it)
+ }
+
+on { ButtonMessage::class }
+ .where { widgetId.isPrayerButton() }
+ .then { player ->
+ val prayer = Prayer.forButton(widgetId)!!
+ val level = prayer.level
+
+ if (level > player.prayer.current) {
+ player.sendMessage("You need a prayer level of $level to use this prayer.")
+ terminate()
+ return@then
+ }
+
+ player.updatePrayer(prayer)
+ terminate()
+ }
+
+on { ItemOptionMessage::class }
+ .where { option == BuryBoneAction.BURY_OPTION && id.isBone() }
+ .then { player ->
+ val bone = Bone[id]!!
+
+ player.startAction(BuryBoneAction(player, slot, bone))
+ terminate()
+ }
diff --git a/game/plugin/skills/prayer/test/BuryBoneTests.kt b/game/plugin/skills/prayer/test/BuryBoneTests.kt
new file mode 100644
index 000000000..23d57f684
--- /dev/null
+++ b/game/plugin/skills/prayer/test/BuryBoneTests.kt
@@ -0,0 +1,73 @@
+
+import BuryBoneAction.Companion.BURY_BONE_ANIMATION
+import io.mockk.verify
+import org.apollo.cache.def.ItemDefinition
+import org.apollo.game.model.entity.Player
+import org.apollo.game.plugin.api.prayer
+import org.apollo.game.plugin.testing.assertions.after
+import org.apollo.game.plugin.testing.assertions.startsWith
+import org.apollo.game.plugin.testing.assertions.verifyAfter
+import org.apollo.game.plugin.testing.junit.ApolloTestingExtension
+import org.apollo.game.plugin.testing.junit.api.ActionCapture
+import org.apollo.game.plugin.testing.junit.api.annotations.ItemDefinitions
+import org.apollo.game.plugin.testing.junit.api.annotations.TestMock
+import org.apollo.game.plugin.testing.junit.api.interactions.interactWithItem
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.extension.ExtendWith
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.EnumSource
+
+@ExtendWith(ApolloTestingExtension::class)
+class BuryBoneTests {
+
+ @TestMock
+ lateinit var player: Player
+
+ @TestMock
+ lateinit var action: ActionCapture
+
+ @ParameterizedTest
+ @EnumSource(value = Bone::class)
+ fun `Burying a bone should send a message`(bone: Bone) {
+ player.inventory.add(bone.id)
+ player.interactWithItem(bone.id, option = 1)
+
+ verifyAfter(action.ticks(1), "message is sent") {
+ player.sendMessage(startsWith("You dig a hole"))
+ }
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = Bone::class)
+ fun `Burying a bone should play an animation`(bone: Bone) {
+ player.inventory.add(bone.id)
+ player.interactWithItem(bone.id, option = 1)
+
+ verifyAfter(action.ticks(1), "animation is played") {
+ player.playAnimation(eq(BURY_BONE_ANIMATION))
+ }
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = Bone::class)
+ fun `Burying a bone should give the player experience`(bone: Bone) {
+ player.inventory.add(bone.id)
+ player.interactWithItem(bone.id, option = 1)
+
+ action.ticks(1)
+
+ after(action.complete(), "experience is granted after bone burial") {
+ verify { player.sendMessage(startsWith("You bury the bones")) }
+
+ assertEquals(bone.xp, player.prayer.experience)
+ assertEquals(player.inventory.getAmount(bone.id), 0)
+ }
+ }
+
+ private companion object {
+ @ItemDefinitions
+ fun bones(): Collection {
+ return Bone.values().map { ItemDefinition(it.id) }
+ }
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/skills/runecrafting/build.gradle b/game/plugin/skills/runecrafting/build.gradle
new file mode 100644
index 000000000..4d96c9f57
--- /dev/null
+++ b/game/plugin/skills/runecrafting/build.gradle
@@ -0,0 +1,10 @@
+apply plugin: 'kotlin'
+
+dependencies {
+ implementation project(':game')
+ implementation project(':cache')
+ implementation project(':net')
+ implementation project(':util')
+ implementation project(':game:plugin:api')
+ testImplementation project(':game:plugin-testing')
+}
diff --git a/game/plugin/skills/runecrafting/src/Altar.kt b/game/plugin/skills/runecrafting/src/Altar.kt
new file mode 100644
index 000000000..f336b2b1d
--- /dev/null
+++ b/game/plugin/skills/runecrafting/src/Altar.kt
@@ -0,0 +1,25 @@
+package org.apollo.game.plugin.skill.runecrafting
+
+import org.apollo.game.model.Position
+
+enum class Altar(val entranceId: Int, val craftingId: Int, val portalId: Int, val entrance: Position, val exit: Position, val center: Position) {
+ AIR_ALTAR(2452, 2478, 2465, Position(2841, 4829), Position(2983, 3292), Position(2844, 4834)),
+ MIND_ALTAR(2453, 2479, 2466, Position(2793, 4828), Position(2980, 3514), Position(2786, 4841)),
+ WATER_ALTAR(2454, 2480, 2467, Position(2726, 4832), Position(3187, 3166), Position(2716, 4836)),
+ EARTH_ALTAR(2455, 2481, 2468, Position(2655, 4830), Position(3304, 3474), Position(2658, 4841)),
+ FIRE_ALTAR(2456, 2482, 2469, Position(2574, 4849), Position(3311, 3256), Position(2585, 4838)),
+ BODY_ALTAR(2457, 2483, 2470, Position(2524, 4825), Position(3051, 3445), Position(2525, 4832)),
+ COSMIC_ALTAR(2458, 2484, 2471, Position(2142, 4813), Position(2408, 4379), Position(2142, 4833)),
+ LAW_ALTAR(2459, 2485, 2472, Position(2464, 4818), Position(2858, 3379), Position(2464, 4832)),
+ NATURE_ALTAR(2460, 2486, 2473, Position(2400, 4835), Position(2867, 3019), Position(2400, 4841)),
+ CHAOS_ALTAR(2461, 2487, 2474, Position(2268, 4842), Position(3058, 3591), Position(2271, 4842)),
+ DEATH_ALTAR(2462, 2488, 2475, Position(2208, 4830), Position(3222, 3222), Position(2205, 4836));
+
+ companion object {
+ private val ALTARS = Altar.values()
+
+ fun findByEntranceId(id: Int): Altar? = ALTARS.find { Altar -> Altar.entranceId == id }
+ fun findByPortalId(id: Int): Altar? = ALTARS.find { Altar -> Altar.portalId == id }
+ fun findByCraftingId(id: Int): Altar? = ALTARS.find { Altar -> Altar.craftingId == id }
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/skills/runecrafting/src/CreateTiaraAction.kt b/game/plugin/skills/runecrafting/src/CreateTiaraAction.kt
new file mode 100644
index 000000000..f6bcc43fe
--- /dev/null
+++ b/game/plugin/skills/runecrafting/src/CreateTiaraAction.kt
@@ -0,0 +1,25 @@
+package org.apollo.game.plugin.skill.runecrafting
+
+import org.apollo.game.action.DistancedAction
+import org.apollo.game.model.Position
+import org.apollo.game.model.entity.Player
+import org.apollo.game.plugin.api.runecraft
+
+class CreateTiaraAction(val player: Player, val position: Position, val tiara: Tiara, val altar: Altar) : DistancedAction(0, true, player, position, 2) {
+ override fun executeAction() {
+ if (tiara.altar != altar) {
+ player.sendMessage("You can't use that talisman on this altar.")
+ stop()
+ return
+ }
+
+ if (player.inventory.contains(blankTiaraId)) {
+ player.inventory.remove(blankTiaraId)
+ player.inventory.add(tiara.id)
+ player.runecraft.experience += tiara.xp
+ player.playAnimation(runecraftingAnimation)
+ player.playGraphic(runecraftingGraphic)
+ stop()
+ }
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/skills/runecrafting/src/Rune.kt b/game/plugin/skills/runecrafting/src/Rune.kt
new file mode 100644
index 000000000..5717442b5
--- /dev/null
+++ b/game/plugin/skills/runecrafting/src/Rune.kt
@@ -0,0 +1,65 @@
+package org.apollo.game.plugin.skill.runecrafting
+
+import org.apollo.game.plugin.skill.runecrafting.Altar.*
+
+interface Rune {
+ /**
+ * The item id of the rune.
+ */
+ val id: Int
+
+ /**
+ * The [Altar] this rune must be crafted at.
+ */
+ val altar: Altar
+
+ /**
+ * The runecrafting level required to craft runes of this type.
+ */
+ val level: Int
+
+ /**
+ * The amount of experience rewarded from crafting a single rune of this type.
+ */
+ val xp: Double
+
+ /**
+ * Get the multiplier that is applied to the number of runes the player crafts for their runecrafting level.
+ *
+ * [playerLevel] - The players current runecrafting level.
+ */
+ fun getBonusMultiplier(playerLevel: Int): Double
+}
+
+enum class DefaultRune(
+ override val id: Int,
+ override val altar: Altar,
+ override val level: Int,
+ override val xp: Double
+) : Rune {
+ AIR_RUNE(556, AIR_ALTAR, 1, 5.0),
+ MIND_RUNE(558, MIND_ALTAR, 1, 5.5),
+ WATER_RUNE(555, WATER_ALTAR, 5, 6.0),
+ EARTH_RUNE(557, EARTH_ALTAR, 9, 6.5),
+ FIRE_RUNE(554, FIRE_ALTAR, 14, 7.0),
+ BODY_RUNE(559, BODY_ALTAR, 20, 7.5),
+ COSMIC_RUNE(564, COSMIC_ALTAR, 27, 8.0),
+ CHAOS_RUNE(562, CHAOS_ALTAR, 35, 8.5),
+ NATURE_RUNE(561, NATURE_ALTAR, 44, 9.0),
+ LAW_RUNE(563, LAW_ALTAR, 54, 9.5),
+ DEATH_RUNE(560, DEATH_ALTAR, 65, 10.0);
+
+ override fun getBonusMultiplier(playerLevel: Int): Double = when (this) {
+ DefaultRune.AIR_RUNE -> (Math.floor((playerLevel / 11.0)) + 1)
+ DefaultRune.MIND_RUNE -> (Math.floor((playerLevel / 14.0)) + 1)
+ DefaultRune.WATER_RUNE -> (Math.floor((playerLevel / 19.0)) + 1)
+ DefaultRune.EARTH_RUNE -> (Math.floor((playerLevel / 26.0)) + 1)
+ DefaultRune.FIRE_RUNE -> (Math.floor((playerLevel / 35.0)) + 1)
+ DefaultRune.BODY_RUNE -> (Math.floor((playerLevel / 46.0)) + 1)
+ DefaultRune.COSMIC_RUNE -> (Math.floor((playerLevel / 59.0)) + 1)
+ DefaultRune.CHAOS_RUNE -> (Math.floor((playerLevel / 74.0)) + 1)
+ DefaultRune.NATURE_RUNE -> (Math.floor((playerLevel / 91.0)) + 1)
+ DefaultRune.LAW_RUNE -> 1.0
+ DefaultRune.DEATH_RUNE -> 1.0
+ }
+}
diff --git a/game/plugin/skills/runecrafting/src/Runecrafting.plugin.kts b/game/plugin/skills/runecrafting/src/Runecrafting.plugin.kts
new file mode 100644
index 000000000..88f983f77
--- /dev/null
+++ b/game/plugin/skills/runecrafting/src/Runecrafting.plugin.kts
@@ -0,0 +1,93 @@
+package org.apollo.game.plugin.skill.runecrafting
+
+import org.apollo.game.message.impl.*
+import org.apollo.game.model.entity.EquipmentConstants
+import org.apollo.game.model.event.impl.LoginEvent
+
+private val changeAltarObjectConfigId = 491
+
+internal val RUNES = mutableListOf()
+
+fun List.findById(id: Int): Rune? {
+ return find { rune -> rune.id == id }
+}
+
+start {
+ RUNES.addAll(DefaultRune.values())
+}
+
+on_player_event { LoginEvent::class }
+ .then {
+ val equippedHat = player.equipment.get(EquipmentConstants.HAT)
+ val equippedTiaraConfig = equippedHat?.let { item -> Tiara.findById(item.id)?.configId } ?: 0
+ val configValue = 1 shl equippedTiaraConfig
+
+ player.send(ConfigMessage(changeAltarObjectConfigId, configValue))
+ }
+
+on { ObjectActionMessage::class }
+ .where { option == 2 }
+ .then {
+ val tiara = Tiara.findByAltarId(id) ?: return@then
+ val hat = it.equipment.get(EquipmentConstants.HAT) ?: return@then
+
+ if (hat.id == tiara.id && tiara.altar.entranceId == id) {
+ it.startAction(TeleportToAltarAction(it, position, 2, tiara.altar.entrance))
+ terminate()
+ }
+ }
+
+on { ItemActionMessage::class }
+ .where { option == 1 }
+ .then { player ->
+ Tiara.findById(id)?.let {
+ player.send(ConfigMessage(changeAltarObjectConfigId, 0))
+ terminate()
+ }
+ }
+
+on { ItemOnObjectMessage::class }
+ .then {
+ val tiara = Tiara.findByTalismanId(id) ?: return@then
+ val altar = Altar.findByCraftingId(objectId) ?: return@then
+
+ it.startAction(CreateTiaraAction(it, position, tiara, altar))
+ terminate()
+ }
+
+on { ItemOptionMessage::class }
+ .where { option == 4 }
+ .then {
+ val talisman = Talisman.findById(id) ?: return@then
+
+ talisman.sendProximityMessageTo(it)
+ terminate()
+ }
+
+on { ItemOnObjectMessage::class }
+ .then {
+ val talisman = Talisman.findById(id) ?: return@then
+ val altar = Altar.findByEntranceId(objectId) ?: return@then
+
+ it.startAction(TeleportToAltarAction(it, position, 2, altar.entrance))
+ terminate()
+ }
+
+on { ObjectActionMessage::class }
+ .where { option == 1 }
+ .then {
+ val altar = Altar.findByPortalId(id) ?: return@then
+
+ it.startAction(TeleportToAltarAction(it, altar.entrance, 1, altar.exit))
+ terminate()
+ }
+
+on { ObjectActionMessage::class }
+ .where { option == 1 }
+ .then {
+ val rune = RUNES.findById(id) ?: return@then
+ val craftingAltar = Altar.findByCraftingId(id) ?: return@then
+
+ it.startAction(RunecraftingAction(it, rune, craftingAltar))
+ terminate()
+ }
\ No newline at end of file
diff --git a/game/plugin/skills/runecrafting/src/RunecraftingAction.kt b/game/plugin/skills/runecrafting/src/RunecraftingAction.kt
new file mode 100644
index 000000000..ec8c04381
--- /dev/null
+++ b/game/plugin/skills/runecrafting/src/RunecraftingAction.kt
@@ -0,0 +1,39 @@
+package org.apollo.game.plugin.skill.runecrafting
+
+import org.apollo.game.action.ActionBlock
+import org.apollo.game.action.AsyncDistancedAction
+import org.apollo.game.model.entity.Player
+import org.apollo.game.plugin.api.Definitions
+import org.apollo.game.plugin.api.runecraft
+import org.apollo.util.LanguageUtil
+
+class RunecraftingAction(val player: Player, val rune: Rune, altar: Altar) : AsyncDistancedAction(0, true, player, altar.center, 3) {
+ override fun action(): ActionBlock = {
+ if (player.runecraft.current < rune.level) {
+ player.sendMessage("You need a runecrafting level of ${rune.level} to craft this rune.")
+ stop()
+ }
+
+ if (!player.inventory.contains(runeEssenceId)) {
+ player.sendMessage("You need rune essence to craft runes.")
+ stop()
+ }
+
+ player.turnTo(position)
+ player.playAnimation(runecraftingAnimation)
+ player.playGraphic(runecraftingGraphic)
+
+ wait(1)
+
+ val name = Definitions.item(rune.id).name
+ val nameArticle = LanguageUtil.getIndefiniteArticle(name)
+ val essenceAmount = player.inventory.removeAll(runeEssenceId)
+ val runeAmount = essenceAmount * rune.getBonusMultiplier(player.runecraft.current)
+ val runesDescription = if (runeAmount > 1) "some ${name}s" else "$nameArticle $name"
+
+ player.sendMessage("You craft the rune essence into $runesDescription")
+ player.inventory.add(rune.id, runeAmount.toInt())
+ player.runecraft.experience += rune.xp * essenceAmount
+ stop()
+ }
+}
diff --git a/game/plugin/skills/runecrafting/src/Talisman.kt b/game/plugin/skills/runecrafting/src/Talisman.kt
new file mode 100644
index 000000000..bd4cc542e
--- /dev/null
+++ b/game/plugin/skills/runecrafting/src/Talisman.kt
@@ -0,0 +1,36 @@
+package org.apollo.game.plugin.skill.runecrafting
+
+import org.apollo.game.model.Position
+import org.apollo.game.model.entity.Player
+
+enum class Talisman(val id: Int, val altar: Position) {
+ AIR_TALISMAN(1438, Position(2985, 3292)),
+ EARTH_TALISMAN(1440, Position(3306, 3474)),
+ FIRE_TALISMAN(1442, Position(3313, 3255)),
+ WATER_TALISMAN(1444, Position(3185, 3165)),
+ BODY_TALISMAN(1446, Position(3053, 3445)),
+ MIND_TALISMAN(1448, Position(2982, 3514)),
+ CHAOS_TALISMAN(1452, Position(3059, 3590)),
+ COSMIC_TALISMAN(1454, Position(2408, 4377)),
+ DEATH_TALISMAN(1456, Position(0, 0)),
+ LAW_TALISMAN(1458, Position(2858, 3381)),
+ NATURE_TALISMAN(1462, Position(2869, 3019));
+
+ companion object {
+ private val TALISMANS = Talisman.values()
+
+ fun findById(id: Int): Talisman? = TALISMANS.find { talisman -> talisman.id == id }
+ }
+
+ fun sendProximityMessageTo(player: Player) {
+ if (altar.isWithinDistance(player.position, 10)) {
+ player.sendMessage("Your talisman glows brightly.")
+ return
+ }
+
+ var direction = if (player.position.y > altar.y) "North" else "South"
+ direction += if (player.position.x > altar.x) "-East" else "-West"
+
+ player.sendMessage("The talisman pulls toward the $direction")
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/skills/runecrafting/src/TeleportToAltarAction.kt b/game/plugin/skills/runecrafting/src/TeleportToAltarAction.kt
new file mode 100644
index 000000000..ec6ff9a8c
--- /dev/null
+++ b/game/plugin/skills/runecrafting/src/TeleportToAltarAction.kt
@@ -0,0 +1,12 @@
+package org.apollo.game.plugin.skill.runecrafting
+
+import org.apollo.game.action.DistancedAction
+import org.apollo.game.model.Position
+import org.apollo.game.model.entity.Player
+
+class TeleportToAltarAction(val player: Player, val start: Position, val distance: Int, val end: Position) : DistancedAction(0, true, player, start, distance) {
+ override fun executeAction() {
+ player.teleport(end)
+ stop()
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/skills/runecrafting/src/Tiara.kt b/game/plugin/skills/runecrafting/src/Tiara.kt
new file mode 100644
index 000000000..d9e002c0e
--- /dev/null
+++ b/game/plugin/skills/runecrafting/src/Tiara.kt
@@ -0,0 +1,26 @@
+package org.apollo.game.plugin.skill.runecrafting
+
+import org.apollo.game.plugin.skill.runecrafting.Altar.*
+import org.apollo.game.plugin.skill.runecrafting.Talisman.*
+
+enum class Tiara(val id: Int, val altar: Altar, val talisman: Talisman, val configId: Int, val xp: Double) {
+ AIR_TIARA(5527, AIR_ALTAR, AIR_TALISMAN, 0, 25.0),
+ MIND_TIARA(5529, MIND_ALTAR, MIND_TALISMAN, 1, 27.5),
+ WATER_TIARA(5531, WATER_ALTAR, WATER_TALISMAN, 2, 30.0),
+ BODY_TIARA(5533, BODY_ALTAR, BODY_TALISMAN, 5, 37.5),
+ EARTH_TIARA(5535, EARTH_ALTAR, EARTH_TALISMAN, 3, 32.5),
+ FIRE_TIARA(5537, FIRE_ALTAR, FIRE_TALISMAN, 4, 35.0),
+ COSMIC_TIARA(5539, COSMIC_ALTAR, COSMIC_TALISMAN, 6, 40.0),
+ NATURE_TIARA(5541, NATURE_ALTAR, NATURE_TALISMAN, 8, 45.0),
+ CHAOS_TIARA(5543, CHAOS_ALTAR, CHAOS_TALISMAN, 9, 42.5),
+ LAW_TIARA(5545, LAW_ALTAR, LAW_TALISMAN, 7, 47.5),
+ DEATH_TIARA(5548, DEATH_ALTAR, DEATH_TALISMAN, 10, 50.0);
+
+ companion object {
+ private val TIARAS = Tiara.values()
+
+ fun findById(id: Int): Tiara? = TIARAS.find { tiara -> tiara.id == id }
+ fun findByAltarId(id: Int): Tiara? = TIARAS.find { tiara -> tiara.altar.entranceId == id }
+ fun findByTalismanId(id: Int): Tiara? = TIARAS.find { tiara -> tiara.talisman.id == id }
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/skills/runecrafting/src/constants.kt b/game/plugin/skills/runecrafting/src/constants.kt
new file mode 100644
index 000000000..73f77cf52
--- /dev/null
+++ b/game/plugin/skills/runecrafting/src/constants.kt
@@ -0,0 +1,9 @@
+package org.apollo.game.plugin.skill.runecrafting
+
+import org.apollo.game.model.Animation
+import org.apollo.game.model.Graphic
+
+const val blankTiaraId = 5525
+val runecraftingAnimation = Animation(791)
+val runecraftingGraphic = Graphic(186, 0, 100)
+const val runeEssenceId = 1436
\ No newline at end of file
diff --git a/game/plugin/skills/runecrafting/test/CreateTiaraActionTests.kt b/game/plugin/skills/runecrafting/test/CreateTiaraActionTests.kt
new file mode 100644
index 000000000..98af45f22
--- /dev/null
+++ b/game/plugin/skills/runecrafting/test/CreateTiaraActionTests.kt
@@ -0,0 +1,68 @@
+package org.apollo.game.plugin.skill.runecrafting
+
+import org.apollo.cache.def.ItemDefinition
+import org.apollo.game.model.World
+import org.apollo.game.model.entity.Player
+import org.apollo.game.model.entity.Skill
+import org.apollo.game.plugin.testing.assertions.after
+import org.apollo.game.plugin.testing.assertions.verifyAfter
+import org.apollo.game.plugin.testing.junit.ApolloTestingExtension
+import org.apollo.game.plugin.testing.junit.api.ActionCapture
+import org.apollo.game.plugin.testing.junit.api.annotations.ItemDefinitions
+import org.apollo.game.plugin.testing.junit.api.annotations.TestMock
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+
+@ExtendWith(ApolloTestingExtension::class)
+class CreateTiaraActionTests {
+
+ @TestMock
+ lateinit var world: World
+
+ @TestMock
+ lateinit var player: Player
+
+ @TestMock
+ lateinit var action: ActionCapture
+
+ @Test
+ fun `A tiara should be rewarded after action completion`() {
+ player.inventory.add(blankTiaraId)
+ player.startAction(CreateTiaraAction(player, player.position, Tiara.AIR_TIARA, Altar.AIR_ALTAR))
+
+ after(action.complete(), "tiara added to inventory") {
+ assertEquals(1, player.inventory.getAmount(Tiara.AIR_TIARA.id))
+ }
+ }
+
+ @Test
+ fun `Tiaras can only be enchanted on compatible altars`() {
+ player.inventory.add(blankTiaraId)
+ player.startAction(CreateTiaraAction(player, player.position, Tiara.AIR_TIARA, Altar.BODY_ALTAR))
+
+ verifyAfter(action.complete(), "error message sent") {
+ player.sendMessage("You can't use that talisman on this altar.")
+ }
+ }
+
+ @Test
+ fun `Experience is rewarded for enchanting tiaras`() {
+ player.inventory.add(blankTiaraId)
+ player.skillSet.setExperience(Skill.RUNECRAFT, 0.0)
+ player.startAction(CreateTiaraAction(player, player.position, Tiara.AIR_TIARA, Altar.AIR_ALTAR))
+
+ after(action.complete(), "experience gained") {
+ assertEquals(Tiara.AIR_TIARA.xp, player.skillSet.getExperience(Skill.RUNECRAFT))
+ }
+ }
+
+ companion object {
+ @ItemDefinitions
+ private val tiaras = Tiara.values()
+ .map { ItemDefinition(it.id).apply { name = "" } }
+
+ @ItemDefinitions
+ private val blankTiara = listOf(ItemDefinition(blankTiaraId))
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/skills/runecrafting/test/RunecraftingActionTests.kt b/game/plugin/skills/runecrafting/test/RunecraftingActionTests.kt
new file mode 100644
index 000000000..9751696bc
--- /dev/null
+++ b/game/plugin/skills/runecrafting/test/RunecraftingActionTests.kt
@@ -0,0 +1,115 @@
+package org.apollo.game.plugin.skill.runecrafting
+
+import io.mockk.verify
+import org.apollo.cache.def.ItemDefinition
+import org.apollo.game.model.entity.Player
+import org.apollo.game.model.entity.Skill
+import org.apollo.game.plugin.testing.assertions.after
+import org.apollo.game.plugin.testing.junit.ApolloTestingExtension
+import org.apollo.game.plugin.testing.junit.api.ActionCapture
+import org.apollo.game.plugin.testing.junit.api.annotations.ItemDefinitions
+import org.apollo.game.plugin.testing.junit.api.annotations.TestMock
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+
+@ExtendWith(ApolloTestingExtension::class)
+class RunecraftingActionTests {
+
+ @TestMock
+ lateinit var player: Player
+
+ @TestMock
+ lateinit var action: ActionCapture
+
+ @BeforeEach
+ fun setupPlayer() {
+ player.position = TEST_ALTAR.center
+ player.inventory.add(runeEssenceId, 25)
+ }
+
+ @Test
+ fun `Bonus runes are rewarded depending on the multiplier returned by the rune type`() {
+ player.startAction(RunecraftingAction(player, `rune with 1xp and bonus multiplier of 2`, TEST_ALTAR))
+
+ after(action.complete()) {
+ assertEquals(50, player.inventory.getAmount(1))
+ }
+ }
+
+ @Test
+ fun `Experience does not stack with bonus multiplier`() {
+ player.skillSet.setExperience(Skill.RUNECRAFT, 0.0)
+ player.startAction(RunecraftingAction(player, `rune with 1xp and bonus multiplier of 2`, TEST_ALTAR))
+
+ after(action.complete()) {
+ assertEquals(25.0, player.skillSet.getExperience(Skill.RUNECRAFT))
+ }
+ }
+
+ @Test
+ fun `Experience is rewarded for each rune essence used`() {
+ player.skillSet.setExperience(Skill.RUNECRAFT, 0.0)
+ player.startAction(RunecraftingAction(player, `rune with 1xp and bonus multiplier of 1`, TEST_ALTAR))
+
+ after(action.complete()) {
+ assertEquals(25.0, player.skillSet.getExperience(Skill.RUNECRAFT))
+ }
+ }
+
+ @Test
+ fun `Cannot create runes that are too high of a level`() {
+ player.skillSet.setCurrentLevel(Skill.RUNECRAFT, 1)
+ player.startAction(RunecraftingAction(player, `rune with required level of 99`, TEST_ALTAR))
+
+ after(action.complete()) {
+ verify { player.sendMessage("You need a runecrafting level of 99 to craft this rune.") }
+
+ assertEquals(25, player.inventory.getAmount(runeEssenceId))
+ assertEquals(0, player.inventory.getAmount(1))
+ }
+ }
+
+ companion object {
+ val TEST_ALTAR = Altar.AIR_ALTAR
+
+ val `rune with required level of 99` = object : Rune {
+ override val id = 1
+ override val altar: Altar = TEST_ALTAR
+ override val level = 99
+ override val xp = 1.0
+
+ override fun getBonusMultiplier(playerLevel: Int) = 1.0
+ }
+
+ val `rune with 1xp and bonus multiplier of 1` = object : Rune {
+ override val id = 1
+ override val altar: Altar = TEST_ALTAR
+ override val level = 1
+ override val xp = 1.0
+
+ override fun getBonusMultiplier(playerLevel: Int) = 1.0
+ }
+
+ val `rune with 1xp and bonus multiplier of 2` = object : Rune {
+ override val id = 1
+ override val altar: Altar = TEST_ALTAR
+ override val level = 1
+ override val xp = 1.0
+
+ override fun getBonusMultiplier(playerLevel: Int) = 2.0
+ }
+
+ @ItemDefinitions
+ private val runeEssence = listOf(ItemDefinition(runeEssenceId).apply {
+ isStackable = true
+ })
+
+ @ItemDefinitions
+ private val runes = listOf(ItemDefinition(1).apply {
+ name = ""
+ isStackable = true
+ })
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/skills/woodcutting/build.gradle b/game/plugin/skills/woodcutting/build.gradle
new file mode 100644
index 000000000..4d96c9f57
--- /dev/null
+++ b/game/plugin/skills/woodcutting/build.gradle
@@ -0,0 +1,10 @@
+apply plugin: 'kotlin'
+
+dependencies {
+ implementation project(':game')
+ implementation project(':cache')
+ implementation project(':net')
+ implementation project(':util')
+ implementation project(':game:plugin:api')
+ testImplementation project(':game:plugin-testing')
+}
diff --git a/game/plugin/skills/woodcutting/src/Axe.kt b/game/plugin/skills/woodcutting/src/Axe.kt
new file mode 100644
index 000000000..ab51ec0da
--- /dev/null
+++ b/game/plugin/skills/woodcutting/src/Axe.kt
@@ -0,0 +1,28 @@
+package org.apollo.game.plugin.skills.woodcutting
+
+import org.apollo.game.model.Animation
+import org.apollo.game.model.entity.Player
+import org.apollo.game.plugin.api.woodcutting
+
+enum class Axe(val id: Int, val level: Int, animation: Int, val pulses: Int) {
+ BRONZE(id = 1351, level = 1, animation = 879, pulses = 8),
+ IRON(id = 1349, level = 1, animation = 877, pulses = 7),
+ STEEL(id = 1353, level = 6, animation = 875, pulses = 6),
+ BLACK(id = 1361, level = 11, animation = 873, pulses = 6),
+ MITHRIL(id = 1355, level = 21, animation = 871, pulses = 5),
+ ADAMANT(id = 1357, level = 31, animation = 869, pulses = 4),
+ RUNE(id = 1359, level = 41, animation = 867, pulses = 3);
+
+ val animation = Animation(animation)
+
+ companion object {
+ private val AXES = Axe.values().sortedByDescending { it.level }
+
+ fun bestFor(player: Player): Axe? {
+ return AXES.asSequence()
+ .filter { it.level <= player.woodcutting.current }
+ .filter { player.equipment.contains(it.id) || player.inventory.contains(it.id) }
+ .firstOrNull()
+ }
+ }
+}
diff --git a/game/plugin/skills/woodcutting/src/Tree.kt b/game/plugin/skills/woodcutting/src/Tree.kt
new file mode 100644
index 000000000..51a274a0e
--- /dev/null
+++ b/game/plugin/skills/woodcutting/src/Tree.kt
@@ -0,0 +1,45 @@
+package org.apollo.game.plugin.skills.woodcutting
+
+/*
+ * Values thanks to: http://oldschoolrunescape.wikia.com/wiki/Woodcutting
+ * https://twitter.com/JagexKieren/status/713409506273787904
+ */
+
+enum class Tree(
+ val objects: Set,
+ val id: Int,
+ val stump: Int,
+ val level: Int,
+ val exp: Double,
+ val chance: Double
+) {
+ NORMAL(NORMAL_OBJECTS, id = 1511, stump = 1342, level = 1, exp = 25.0, chance = 100.0),
+ ACHEY(ACHEY_OBJECTS, id = 2862, stump = 3371, level = 1, exp = 25.0, chance = 100.0),
+ OAK(OAK_OBJECTS, id = 1521, stump = 1342, level = 15, exp = 37.5, chance = 12.5),
+ WILLOW(WILLOW_OBJECTS, id = 1519, stump = 1342, level = 30, exp = 67.5, chance = 12.5),
+ TEAK(TEAK_OBJECTS, id = 6333, stump = 1342, level = 35, exp = 85.0, chance = 12.5),
+ MAPLE(MAPLE_OBJECTS, id = 1517, stump = 1342, level = 45, exp = 100.0, chance = 12.5),
+ MAHOGANY(MAHOGANY_OBJECTS, id = 6332, stump = 1342, level = 50, exp = 125.0, chance = 12.5),
+ YEW(YEW_OBJECTS, id = 1515, stump = 1342, level = 60, exp = 175.0, chance = 12.5),
+ MAGIC(MAGIC_OBJECTS, id = 1513, stump = 1324, level = 75, exp = 250.0, chance = 12.5);
+
+ companion object {
+ private val TREES = Tree.values().flatMap { tree -> tree.objects.map { Pair(it, tree) } }.toMap()
+ fun lookup(id: Int): Tree? = TREES[id]
+ }
+}
+
+private val NORMAL_OBJECTS = hashSetOf(
+ 1276, 1277, 1278, 1279, 1280, 1282, 1283, 1284, 1285, 1285, 1286, 1289, 1290, 1291, 1315,
+ 1316, 1318, 1330, 1331, 1332, 1365, 1383, 1384, 2409, 3033, 3034, 3035, 3036, 3881, 3882,
+ 3883, 5902, 5903, 5904, 10041
+)
+
+private val ACHEY_OBJECTS = hashSetOf(2023)
+private val OAK_OBJECTS = hashSetOf(1281, 3037)
+private val WILLOW_OBJECTS = hashSetOf(5551, 5552, 5553)
+private val TEAK_OBJECTS = hashSetOf(9036)
+private val MAPLE_OBJECTS = hashSetOf(1307, 4674)
+private val MAHOGANY_OBJECTS = hashSetOf(9034)
+private val YEW_OBJECTS = hashSetOf(1309)
+private val MAGIC_OBJECTS = hashSetOf(1292, 1306)
\ No newline at end of file
diff --git a/game/plugin/skills/woodcutting/src/Woodcutting.plugin.kts b/game/plugin/skills/woodcutting/src/Woodcutting.plugin.kts
new file mode 100644
index 000000000..f0d962395
--- /dev/null
+++ b/game/plugin/skills/woodcutting/src/Woodcutting.plugin.kts
@@ -0,0 +1,104 @@
+
+import java.util.concurrent.TimeUnit
+import org.apollo.game.GameConstants
+import org.apollo.game.action.ActionBlock
+import org.apollo.game.action.AsyncDistancedAction
+import org.apollo.game.message.impl.ObjectActionMessage
+import org.apollo.game.model.Position
+import org.apollo.game.model.World
+import org.apollo.game.model.entity.Player
+import org.apollo.game.model.entity.obj.GameObject
+import org.apollo.game.plugin.api.*
+import org.apollo.game.plugin.skills.woodcutting.Axe
+import org.apollo.game.plugin.skills.woodcutting.Tree
+
+// TODO Accurate chopping rates, e.g. https://twitter.com/JagexKieren/status/713403124464107520
+
+on { ObjectActionMessage::class }
+ .where { option == 1 }
+ .then { player ->
+ Tree.lookup(id)?.let { WoodcuttingAction.start(this, player, it) }
+ }
+
+class WoodcuttingTarget(private val objectId: Int, val position: Position, val tree: Tree) {
+
+ /**
+ * Get the tree object in the world
+ */
+ fun getObject(world: World): GameObject? {
+ return world.findObject(position, objectId)
+ }
+
+ /**
+ * Returns whether or not the tree was cut down.
+ */
+ fun isCutDown(): Boolean = rand(100) <= tree.chance * 100
+}
+
+class WoodcuttingAction(
+ player: Player,
+ private val tool: Axe,
+ private val target: WoodcuttingTarget
+) : AsyncDistancedAction(DELAY, true, player, target.position, TREE_SIZE) {
+
+ companion object {
+ private const val DELAY = 0
+ private const val TREE_SIZE = 2
+ private const val MINIMUM_RESPAWN_TIME = 30L // In seconds
+
+ /**
+ * Starts a [WoodcuttingAction] for the specified [Player], terminating the [ObjectActionMessage] that triggered
+ * it.
+ */
+ fun start(message: ObjectActionMessage, player: Player, wood: Tree) {
+ val axe = Axe.bestFor(player)
+ if (axe != null) {
+ if (player.inventory.freeSlots() == 0) {
+ player.inventory.forceCapacityExceeded()
+ return
+ }
+
+ val action = WoodcuttingAction(player, axe, WoodcuttingTarget(message.id, message.position, wood))
+ player.startAction(action)
+ } else {
+ player.sendMessage("You do not have an axe for which you have the level to use.")
+ }
+
+ message.terminate()
+ }
+ }
+
+ override fun action(): ActionBlock = {
+ mob.turnTo(position)
+
+ val level = mob.woodcutting.current
+ if (level < target.tree.level) {
+ mob.sendMessage("You do not have the required level to cut down this tree.")
+ stop()
+ }
+
+ while (isRunning) {
+ mob.sendMessage("You swing your axe at the tree.")
+ mob.playAnimation(tool.animation)
+
+ wait(tool.pulses)
+
+ // Check that the object exists in the world
+ val obj = target.getObject(mob.world) ?: stop()
+
+ if (mob.inventory.add(target.tree.id)) {
+ val logName = Definitions.item(target.tree.id).name.toLowerCase()
+ mob.sendMessage("You managed to cut some $logName.")
+ mob.woodcutting.experience += target.tree.exp
+ }
+
+ if (target.isCutDown()) {
+ // respawn time: http://runescape.wikia.com/wiki/Trees
+ val respawn = TimeUnit.SECONDS.toMillis(MINIMUM_RESPAWN_TIME + rand(150)) / GameConstants.PULSE_DELAY
+
+ mob.world.replaceObject(obj, target.tree.stump, respawn.toInt())
+ stop()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/skills/woodcutting/test/AxeTests.kt b/game/plugin/skills/woodcutting/test/AxeTests.kt
new file mode 100644
index 000000000..4f271f489
--- /dev/null
+++ b/game/plugin/skills/woodcutting/test/AxeTests.kt
@@ -0,0 +1,73 @@
+import org.apollo.cache.def.ItemDefinition
+import org.apollo.game.model.entity.Player
+import org.apollo.game.model.entity.Skill
+import org.apollo.game.plugin.skills.woodcutting.Axe
+import org.apollo.game.plugin.testing.junit.ApolloTestingExtension
+import org.apollo.game.plugin.testing.junit.api.annotations.ItemDefinitions
+import org.apollo.game.plugin.testing.junit.api.annotations.TestMock
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.extension.ExtendWith
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.EnumSource
+
+@ExtendWith(ApolloTestingExtension::class)
+class AxeTests {
+
+ @TestMock
+ lateinit var player: Player
+
+ @ParameterizedTest
+ @EnumSource(Axe::class)
+ fun `No axe is chosen if none are available`(axe: Axe) {
+ player.skillSet.setCurrentLevel(Skill.WOODCUTTING, axe.level)
+
+ assertEquals(null, Axe.bestFor(player))
+ }
+
+ @ParameterizedTest
+ @EnumSource(Axe::class)
+ fun `The highest level axe is chosen when available`(axe: Axe) {
+ player.skillSet.setCurrentLevel(Skill.WOODCUTTING, axe.level)
+ player.inventory.add(axe.id)
+
+ assertEquals(axe, Axe.bestFor(player))
+ }
+
+ @ParameterizedTest
+ @EnumSource(Axe::class)
+ fun `Only axes the player has are chosen`(axe: Axe) {
+ player.skillSet.setCurrentLevel(Skill.WOODCUTTING, axe.level)
+ player.inventory.add(Axe.BRONZE.id)
+
+ assertEquals(Axe.BRONZE, Axe.bestFor(player))
+ }
+
+ @ParameterizedTest
+ @EnumSource(Axe::class)
+ fun `Axes can be chosen from equipment as well as inventory`(axe: Axe) {
+ player.skillSet.setCurrentLevel(Skill.WOODCUTTING, axe.level)
+ player.inventory.add(axe.id)
+
+ assertEquals(axe, Axe.bestFor(player))
+ }
+
+ @ParameterizedTest
+ @EnumSource(Axe::class)
+ fun `Axes with a level requirement higher than the player's are ignored`(axe: Axe) {
+ player.skillSet.setCurrentLevel(Skill.WOODCUTTING, axe.level)
+ player.inventory.add(axe.id)
+
+ Axe.values()
+ .filter { it.level > axe.level }
+ .forEach { player.inventory.add(it.id) }
+
+ assertEquals(axe, Axe.bestFor(player))
+ }
+
+ private companion object {
+ @ItemDefinitions
+ fun axes() = Axe.values().map {
+ ItemDefinition(it.id).apply { isStackable = false }
+ }
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/skills/woodcutting/test/TestData.kt b/game/plugin/skills/woodcutting/test/TestData.kt
new file mode 100644
index 000000000..80c82e76f
--- /dev/null
+++ b/game/plugin/skills/woodcutting/test/TestData.kt
@@ -0,0 +1,17 @@
+import java.util.stream.Stream
+import org.apollo.game.plugin.skills.woodcutting.Tree
+import org.junit.jupiter.api.extension.ExtensionContext
+import org.junit.jupiter.params.provider.Arguments
+import org.junit.jupiter.params.provider.ArgumentsProvider
+
+data class WoodcuttingTestData(val treeId: Int, val stumpId: Int, val tree: Tree)
+
+fun woodcuttingTestData(): Collection = Tree.values()
+ .flatMap { tree -> tree.objects.map { WoodcuttingTestData(it, tree.stump, tree) } }
+ .toList()
+
+class WoodcuttingTestDataProvider : ArgumentsProvider {
+ override fun provideArguments(context: ExtensionContext?): Stream {
+ return woodcuttingTestData().map { Arguments { arrayOf(it) } }.stream()
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/skills/woodcutting/test/WoodcuttingTests.kt b/game/plugin/skills/woodcutting/test/WoodcuttingTests.kt
new file mode 100644
index 000000000..f98ab24f4
--- /dev/null
+++ b/game/plugin/skills/woodcutting/test/WoodcuttingTests.kt
@@ -0,0 +1,90 @@
+
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import java.util.Random
+import org.apollo.cache.def.ItemDefinition
+import org.apollo.game.model.entity.Player
+import org.apollo.game.model.entity.Skill
+import org.apollo.game.plugin.skills.woodcutting.Axe
+import org.apollo.game.plugin.testing.assertions.after
+import org.apollo.game.plugin.testing.assertions.contains
+import org.apollo.game.plugin.testing.assertions.verifyAfter
+import org.apollo.game.plugin.testing.junit.ApolloTestingExtension
+import org.apollo.game.plugin.testing.junit.api.ActionCapture
+import org.apollo.game.plugin.testing.junit.api.annotations.ItemDefinitions
+import org.apollo.game.plugin.testing.junit.api.annotations.TestMock
+import org.apollo.game.plugin.testing.junit.api.interactions.interactWithObject
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assumptions.assumeTrue
+import org.junit.jupiter.api.Disabled
+import org.junit.jupiter.api.extension.ExtendWith
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.ArgumentsSource
+
+@ExtendWith(ApolloTestingExtension::class)
+class WoodcuttingTests {
+
+ @TestMock
+ lateinit var action: ActionCapture
+
+ @TestMock
+ lateinit var player: Player
+
+ @ParameterizedTest
+ @ArgumentsSource(WoodcuttingTestDataProvider::class)
+ fun `Attempting to cut a tree when the player has no axe should send a message`(data: WoodcuttingTestData) {
+ player.interactWithObject(data.treeId, 1)
+
+ verify { player.sendMessage(contains("do not have an axe")) }
+ }
+
+ @ParameterizedTest
+ @ArgumentsSource(WoodcuttingTestDataProvider::class)
+ fun `Attempting to cut a tree when the player is too low levelled should send a message`(data: WoodcuttingTestData) {
+ assumeTrue(data.tree.level > 1, "Normal trees are covered by axe requirements")
+
+ player.inventory.add(Axe.BRONZE.id)
+ player.skillSet.setCurrentLevel(Skill.WOODCUTTING, data.tree.level - 1)
+
+ player.interactWithObject(data.treeId, 1)
+
+ verifyAfter(action.complete()) { player.sendMessage(contains("do not have the required level")) }
+ }
+
+ @Disabled("Mocking constructors is not supported in mockk. Update WoodcuttingAction to pass a chance value")
+ @ParameterizedTest
+ @ArgumentsSource(WoodcuttingTestDataProvider::class)
+ fun `Cutting a tree we have the skill to cut should eventually reward logs and experience`(
+ data: WoodcuttingTestData
+ ) {
+ // Mock RNG instances used by mining internally to determine success
+ // @todo - improve this so we don't have to mock Random
+ val rng = mockk()
+ every { rng.nextInt(100) } answers { 0 }
+
+ player.inventory.add(Axe.BRONZE.id)
+ player.skillSet.setCurrentLevel(Skill.WOODCUTTING, data.tree.level)
+
+ player.interactWithObject(data.treeId, 1)
+
+ verifyAfter(action.ticks(1)) { player.sendMessage(contains("You swing your axe")) }
+
+ after(action.ticks(Axe.BRONZE.pulses)) {
+ // @todo - cummulative ticks() calls?
+ verify { player.sendMessage("You manage to cut some ") }
+ assertEquals(data.tree.exp, player.skillSet.getExperience(Skill.WOODCUTTING))
+ assertEquals(1, player.inventory.getAmount(data.tree.id))
+ }
+ }
+
+ private companion object {
+ @ItemDefinitions
+ fun logs() = woodcuttingTestData().map {
+ ItemDefinition(it.tree.id).also { it.name = "" }
+ }
+
+ @ItemDefinitions
+ fun tools() = listOf(ItemDefinition(Axe.BRONZE.id))
+ }
+}
\ No newline at end of file
diff --git a/game/src/main/org/apollo/Server.java b/game/src/main/java/org/apollo/Server.java
similarity index 100%
rename from game/src/main/org/apollo/Server.java
rename to game/src/main/java/org/apollo/Server.java
diff --git a/game/src/main/org/apollo/ServerContext.java b/game/src/main/java/org/apollo/ServerContext.java
similarity index 100%
rename from game/src/main/org/apollo/ServerContext.java
rename to game/src/main/java/org/apollo/ServerContext.java
diff --git a/game/src/main/org/apollo/Service.java b/game/src/main/java/org/apollo/Service.java
similarity index 100%
rename from game/src/main/org/apollo/Service.java
rename to game/src/main/java/org/apollo/Service.java
diff --git a/game/src/main/org/apollo/ServiceManager.java b/game/src/main/java/org/apollo/ServiceManager.java
similarity index 100%
rename from game/src/main/org/apollo/ServiceManager.java
rename to game/src/main/java/org/apollo/ServiceManager.java
diff --git a/game/src/main/org/apollo/game/GameConstants.java b/game/src/main/java/org/apollo/game/GameConstants.java
similarity index 100%
rename from game/src/main/org/apollo/game/GameConstants.java
rename to game/src/main/java/org/apollo/game/GameConstants.java
diff --git a/game/src/main/org/apollo/game/GamePulseHandler.java b/game/src/main/java/org/apollo/game/GamePulseHandler.java
similarity index 100%
rename from game/src/main/org/apollo/game/GamePulseHandler.java
rename to game/src/main/java/org/apollo/game/GamePulseHandler.java
diff --git a/game/src/main/org/apollo/game/action/Action.java b/game/src/main/java/org/apollo/game/action/Action.java
similarity index 100%
rename from game/src/main/org/apollo/game/action/Action.java
rename to game/src/main/java/org/apollo/game/action/Action.java
diff --git a/game/src/main/org/apollo/game/action/DistancedAction.java b/game/src/main/java/org/apollo/game/action/DistancedAction.java
similarity index 100%
rename from game/src/main/org/apollo/game/action/DistancedAction.java
rename to game/src/main/java/org/apollo/game/action/DistancedAction.java
diff --git a/game/src/main/org/apollo/game/action/package-info.java b/game/src/main/java/org/apollo/game/action/package-info.java
similarity index 100%
rename from game/src/main/org/apollo/game/action/package-info.java
rename to game/src/main/java/org/apollo/game/action/package-info.java
diff --git a/game/src/main/org/apollo/game/command/Command.java b/game/src/main/java/org/apollo/game/command/Command.java
similarity index 100%
rename from game/src/main/org/apollo/game/command/Command.java
rename to game/src/main/java/org/apollo/game/command/Command.java
diff --git a/game/src/main/org/apollo/game/command/CommandDispatcher.java b/game/src/main/java/org/apollo/game/command/CommandDispatcher.java
similarity index 100%
rename from game/src/main/org/apollo/game/command/CommandDispatcher.java
rename to game/src/main/java/org/apollo/game/command/CommandDispatcher.java
diff --git a/game/src/main/org/apollo/game/command/CommandListener.java b/game/src/main/java/org/apollo/game/command/CommandListener.java
similarity index 100%
rename from game/src/main/org/apollo/game/command/CommandListener.java
rename to game/src/main/java/org/apollo/game/command/CommandListener.java
diff --git a/game/src/main/org/apollo/game/command/CreditsCommandListener.java b/game/src/main/java/org/apollo/game/command/CreditsCommandListener.java
similarity index 100%
rename from game/src/main/org/apollo/game/command/CreditsCommandListener.java
rename to game/src/main/java/org/apollo/game/command/CreditsCommandListener.java
diff --git a/game/src/main/org/apollo/game/command/package-info.java b/game/src/main/java/org/apollo/game/command/package-info.java
similarity index 100%
rename from game/src/main/org/apollo/game/command/package-info.java
rename to game/src/main/java/org/apollo/game/command/package-info.java
diff --git a/game/src/main/org/apollo/game/fs/decoder/SynchronousDecoder.java b/game/src/main/java/org/apollo/game/fs/decoder/SynchronousDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/fs/decoder/SynchronousDecoder.java
rename to game/src/main/java/org/apollo/game/fs/decoder/SynchronousDecoder.java
diff --git a/game/src/main/org/apollo/game/fs/decoder/SynchronousDecoderException.java b/game/src/main/java/org/apollo/game/fs/decoder/SynchronousDecoderException.java
similarity index 100%
rename from game/src/main/org/apollo/game/fs/decoder/SynchronousDecoderException.java
rename to game/src/main/java/org/apollo/game/fs/decoder/SynchronousDecoderException.java
diff --git a/game/src/main/org/apollo/game/fs/decoder/WorldMapDecoder.java b/game/src/main/java/org/apollo/game/fs/decoder/WorldMapDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/fs/decoder/WorldMapDecoder.java
rename to game/src/main/java/org/apollo/game/fs/decoder/WorldMapDecoder.java
diff --git a/game/src/main/org/apollo/game/fs/decoder/WorldObjectsDecoder.java b/game/src/main/java/org/apollo/game/fs/decoder/WorldObjectsDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/fs/decoder/WorldObjectsDecoder.java
rename to game/src/main/java/org/apollo/game/fs/decoder/WorldObjectsDecoder.java
diff --git a/game/src/main/org/apollo/game/fs/decoder/package-info.java b/game/src/main/java/org/apollo/game/fs/decoder/package-info.java
similarity index 100%
rename from game/src/main/org/apollo/game/fs/decoder/package-info.java
rename to game/src/main/java/org/apollo/game/fs/decoder/package-info.java
diff --git a/game/src/main/org/apollo/game/io/EquipmentDefinitionParser.java b/game/src/main/java/org/apollo/game/io/EquipmentDefinitionParser.java
similarity index 100%
rename from game/src/main/org/apollo/game/io/EquipmentDefinitionParser.java
rename to game/src/main/java/org/apollo/game/io/EquipmentDefinitionParser.java
diff --git a/game/src/main/org/apollo/game/io/MessageHandlerChainSetParser.java b/game/src/main/java/org/apollo/game/io/MessageHandlerChainSetParser.java
similarity index 100%
rename from game/src/main/org/apollo/game/io/MessageHandlerChainSetParser.java
rename to game/src/main/java/org/apollo/game/io/MessageHandlerChainSetParser.java
diff --git a/game/src/main/org/apollo/game/io/PluginMetaDataParser.java b/game/src/main/java/org/apollo/game/io/PluginMetaDataParser.java
similarity index 100%
rename from game/src/main/org/apollo/game/io/PluginMetaDataParser.java
rename to game/src/main/java/org/apollo/game/io/PluginMetaDataParser.java
diff --git a/game/src/main/org/apollo/game/io/package-info.java b/game/src/main/java/org/apollo/game/io/package-info.java
similarity index 100%
rename from game/src/main/org/apollo/game/io/package-info.java
rename to game/src/main/java/org/apollo/game/io/package-info.java
diff --git a/game/src/main/org/apollo/game/io/player/BinaryPlayerSerializer.java b/game/src/main/java/org/apollo/game/io/player/BinaryPlayerSerializer.java
similarity index 100%
rename from game/src/main/org/apollo/game/io/player/BinaryPlayerSerializer.java
rename to game/src/main/java/org/apollo/game/io/player/BinaryPlayerSerializer.java
diff --git a/game/src/main/org/apollo/game/io/player/DummyPlayerSerializer.java b/game/src/main/java/org/apollo/game/io/player/DummyPlayerSerializer.java
similarity index 100%
rename from game/src/main/org/apollo/game/io/player/DummyPlayerSerializer.java
rename to game/src/main/java/org/apollo/game/io/player/DummyPlayerSerializer.java
diff --git a/game/src/main/org/apollo/game/io/player/JdbcPlayerSerializer.java b/game/src/main/java/org/apollo/game/io/player/JdbcPlayerSerializer.java
similarity index 100%
rename from game/src/main/org/apollo/game/io/player/JdbcPlayerSerializer.java
rename to game/src/main/java/org/apollo/game/io/player/JdbcPlayerSerializer.java
diff --git a/game/src/main/org/apollo/game/io/player/PlayerLoaderResponse.java b/game/src/main/java/org/apollo/game/io/player/PlayerLoaderResponse.java
similarity index 100%
rename from game/src/main/org/apollo/game/io/player/PlayerLoaderResponse.java
rename to game/src/main/java/org/apollo/game/io/player/PlayerLoaderResponse.java
diff --git a/game/src/main/org/apollo/game/io/player/PlayerSerializer.java b/game/src/main/java/org/apollo/game/io/player/PlayerSerializer.java
similarity index 100%
rename from game/src/main/org/apollo/game/io/player/PlayerSerializer.java
rename to game/src/main/java/org/apollo/game/io/player/PlayerSerializer.java
diff --git a/game/src/main/org/apollo/game/io/player/package-info.java b/game/src/main/java/org/apollo/game/io/player/package-info.java
similarity index 100%
rename from game/src/main/org/apollo/game/io/player/package-info.java
rename to game/src/main/java/org/apollo/game/io/player/package-info.java
diff --git a/game/src/main/org/apollo/game/login/PlayerLoaderWorker.java b/game/src/main/java/org/apollo/game/login/PlayerLoaderWorker.java
similarity index 100%
rename from game/src/main/org/apollo/game/login/PlayerLoaderWorker.java
rename to game/src/main/java/org/apollo/game/login/PlayerLoaderWorker.java
diff --git a/game/src/main/org/apollo/game/login/PlayerSaverWorker.java b/game/src/main/java/org/apollo/game/login/PlayerSaverWorker.java
similarity index 100%
rename from game/src/main/org/apollo/game/login/PlayerSaverWorker.java
rename to game/src/main/java/org/apollo/game/login/PlayerSaverWorker.java
diff --git a/game/src/main/org/apollo/game/login/package-info.java b/game/src/main/java/org/apollo/game/login/package-info.java
similarity index 100%
rename from game/src/main/org/apollo/game/login/package-info.java
rename to game/src/main/java/org/apollo/game/login/package-info.java
diff --git a/game/src/main/org/apollo/game/message/handler/BankButtonMessageHandler.java b/game/src/main/java/org/apollo/game/message/handler/BankButtonMessageHandler.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/handler/BankButtonMessageHandler.java
rename to game/src/main/java/org/apollo/game/message/handler/BankButtonMessageHandler.java
diff --git a/game/src/main/org/apollo/game/message/handler/BankMessageHandler.java b/game/src/main/java/org/apollo/game/message/handler/BankMessageHandler.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/handler/BankMessageHandler.java
rename to game/src/main/java/org/apollo/game/message/handler/BankMessageHandler.java
diff --git a/game/src/main/org/apollo/game/message/handler/ClosedInterfaceMessageHandler.java b/game/src/main/java/org/apollo/game/message/handler/ClosedInterfaceMessageHandler.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/handler/ClosedInterfaceMessageHandler.java
rename to game/src/main/java/org/apollo/game/message/handler/ClosedInterfaceMessageHandler.java
diff --git a/game/src/main/org/apollo/game/message/handler/CommandMessageHandler.java b/game/src/main/java/org/apollo/game/message/handler/CommandMessageHandler.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/handler/CommandMessageHandler.java
rename to game/src/main/java/org/apollo/game/message/handler/CommandMessageHandler.java
diff --git a/game/src/main/org/apollo/game/message/handler/DialogueButtonHandler.java b/game/src/main/java/org/apollo/game/message/handler/DialogueButtonHandler.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/handler/DialogueButtonHandler.java
rename to game/src/main/java/org/apollo/game/message/handler/DialogueButtonHandler.java
diff --git a/game/src/main/org/apollo/game/message/handler/DialogueContinueMessageHandler.java b/game/src/main/java/org/apollo/game/message/handler/DialogueContinueMessageHandler.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/handler/DialogueContinueMessageHandler.java
rename to game/src/main/java/org/apollo/game/message/handler/DialogueContinueMessageHandler.java
diff --git a/game/src/main/org/apollo/game/message/handler/EnteredAmountMessageHandler.java b/game/src/main/java/org/apollo/game/message/handler/EnteredAmountMessageHandler.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/handler/EnteredAmountMessageHandler.java
rename to game/src/main/java/org/apollo/game/message/handler/EnteredAmountMessageHandler.java
diff --git a/game/src/main/org/apollo/game/message/handler/EquipItemHandler.java b/game/src/main/java/org/apollo/game/message/handler/EquipItemHandler.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/handler/EquipItemHandler.java
rename to game/src/main/java/org/apollo/game/message/handler/EquipItemHandler.java
diff --git a/game/src/main/org/apollo/game/message/handler/ItemOnItemVerificationHandler.java b/game/src/main/java/org/apollo/game/message/handler/ItemOnItemVerificationHandler.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/handler/ItemOnItemVerificationHandler.java
rename to game/src/main/java/org/apollo/game/message/handler/ItemOnItemVerificationHandler.java
diff --git a/game/src/main/org/apollo/game/message/handler/ItemOnObjectVerificationHandler.java b/game/src/main/java/org/apollo/game/message/handler/ItemOnObjectVerificationHandler.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/handler/ItemOnObjectVerificationHandler.java
rename to game/src/main/java/org/apollo/game/message/handler/ItemOnObjectVerificationHandler.java
diff --git a/game/src/main/org/apollo/game/message/handler/ItemVerificationHandler.java b/game/src/main/java/org/apollo/game/message/handler/ItemVerificationHandler.java
similarity index 97%
rename from game/src/main/org/apollo/game/message/handler/ItemVerificationHandler.java
rename to game/src/main/java/org/apollo/game/message/handler/ItemVerificationHandler.java
index 5f61d8043..a6e96c9c2 100644
--- a/game/src/main/org/apollo/game/message/handler/ItemVerificationHandler.java
+++ b/game/src/main/java/org/apollo/game/message/handler/ItemVerificationHandler.java
@@ -31,7 +31,7 @@ public interface InventorySupplier {
* Gets the appropriate {@link Inventory}.
*
* @param player The {@link Player} who prompted the verification call.
- * @return The inventory. Must not be {@code null}.
+ * @return The inventory, or {@code null} to immediately fail verification.
*/
Inventory getInventory(Player player);
diff --git a/game/src/main/java/org/apollo/game/message/handler/MagicOnMobVerificationHandler.java b/game/src/main/java/org/apollo/game/message/handler/MagicOnMobVerificationHandler.java
new file mode 100644
index 000000000..7df2ba757
--- /dev/null
+++ b/game/src/main/java/org/apollo/game/message/handler/MagicOnMobVerificationHandler.java
@@ -0,0 +1,51 @@
+package org.apollo.game.message.handler;
+
+import org.apollo.game.message.impl.MagicOnMobMessage;
+import org.apollo.game.model.World;
+import org.apollo.game.model.entity.EntityType;
+import org.apollo.game.model.entity.Mob;
+import org.apollo.game.model.entity.MobRepository;
+import org.apollo.game.model.entity.Player;
+
+/**
+ * A verification {@link MessageHandler} for the {@link MagicOnMobMessage}.
+ *
+ * @author Tom
+ */
+public final class MagicOnMobVerificationHandler extends MessageHandler{
+
+ /**
+ * Creates the MessageListener.
+ *
+ * @param world The {@link World} the {@link MagicOnMobMessage} occurred in.
+ */
+ public MagicOnMobVerificationHandler(World world) {
+ super(world);
+ }
+
+ @Override
+ public void handle(Player player, MagicOnMobMessage message) {
+ int index = message.getIndex();
+ MobRepository extends Mob> repository;
+
+ if (message.getType() == EntityType.NPC) {
+ repository = world.getNpcRepository();
+ } else if (message.getType() == EntityType.PLAYER) {
+ repository = world.getPlayerRepository();
+ } else {
+ throw new IllegalStateException("Invalid mob type for message: " + message.toString());
+ }
+
+ if (index < 0 || index >= repository.capacity()) {
+ message.terminate();
+ return;
+ }
+
+ Mob mob = repository.get(index);
+
+ if (mob == null || !player.getPosition().isWithinDistance(mob.getPosition(), player.getViewingDistance() + 1)) {
+ // +1 in case it was decremented after the player clicked the action.
+ message.terminate();
+ }
+ }
+}
diff --git a/game/src/main/org/apollo/game/message/handler/MessageHandler.java b/game/src/main/java/org/apollo/game/message/handler/MessageHandler.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/handler/MessageHandler.java
rename to game/src/main/java/org/apollo/game/message/handler/MessageHandler.java
diff --git a/game/src/main/org/apollo/game/message/handler/MessageHandlerChain.java b/game/src/main/java/org/apollo/game/message/handler/MessageHandlerChain.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/handler/MessageHandlerChain.java
rename to game/src/main/java/org/apollo/game/message/handler/MessageHandlerChain.java
diff --git a/game/src/main/org/apollo/game/message/handler/MessageHandlerChainSet.java b/game/src/main/java/org/apollo/game/message/handler/MessageHandlerChainSet.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/handler/MessageHandlerChainSet.java
rename to game/src/main/java/org/apollo/game/message/handler/MessageHandlerChainSet.java
diff --git a/game/src/main/org/apollo/game/message/handler/NpcActionVerificationHandler.java b/game/src/main/java/org/apollo/game/message/handler/NpcActionVerificationHandler.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/handler/NpcActionVerificationHandler.java
rename to game/src/main/java/org/apollo/game/message/handler/NpcActionVerificationHandler.java
diff --git a/game/src/main/org/apollo/game/message/handler/ObjectActionVerificationHandler.java b/game/src/main/java/org/apollo/game/message/handler/ObjectActionVerificationHandler.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/handler/ObjectActionVerificationHandler.java
rename to game/src/main/java/org/apollo/game/message/handler/ObjectActionVerificationHandler.java
diff --git a/game/src/main/org/apollo/game/message/handler/PlayerActionVerificationHandler.java b/game/src/main/java/org/apollo/game/message/handler/PlayerActionVerificationHandler.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/handler/PlayerActionVerificationHandler.java
rename to game/src/main/java/org/apollo/game/message/handler/PlayerActionVerificationHandler.java
diff --git a/game/src/main/org/apollo/game/message/handler/PlayerDesignMessageHandler.java b/game/src/main/java/org/apollo/game/message/handler/PlayerDesignMessageHandler.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/handler/PlayerDesignMessageHandler.java
rename to game/src/main/java/org/apollo/game/message/handler/PlayerDesignMessageHandler.java
diff --git a/game/src/main/org/apollo/game/message/handler/PlayerDesignVerificationHandler.java b/game/src/main/java/org/apollo/game/message/handler/PlayerDesignVerificationHandler.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/handler/PlayerDesignVerificationHandler.java
rename to game/src/main/java/org/apollo/game/message/handler/PlayerDesignVerificationHandler.java
diff --git a/game/src/main/org/apollo/game/message/handler/PublicChatMessageHandler.java b/game/src/main/java/org/apollo/game/message/handler/PublicChatMessageHandler.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/handler/PublicChatMessageHandler.java
rename to game/src/main/java/org/apollo/game/message/handler/PublicChatMessageHandler.java
diff --git a/game/src/main/org/apollo/game/message/handler/PublicChatVerificationHandler.java b/game/src/main/java/org/apollo/game/message/handler/PublicChatVerificationHandler.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/handler/PublicChatVerificationHandler.java
rename to game/src/main/java/org/apollo/game/message/handler/PublicChatVerificationHandler.java
diff --git a/game/src/main/org/apollo/game/message/handler/RemoveEquippedItemHandler.java b/game/src/main/java/org/apollo/game/message/handler/RemoveEquippedItemHandler.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/handler/RemoveEquippedItemHandler.java
rename to game/src/main/java/org/apollo/game/message/handler/RemoveEquippedItemHandler.java
diff --git a/game/src/main/org/apollo/game/message/handler/SwitchItemMessageHandler.java b/game/src/main/java/org/apollo/game/message/handler/SwitchItemMessageHandler.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/handler/SwitchItemMessageHandler.java
rename to game/src/main/java/org/apollo/game/message/handler/SwitchItemMessageHandler.java
diff --git a/game/src/main/org/apollo/game/message/handler/WalkMessageHandler.java b/game/src/main/java/org/apollo/game/message/handler/WalkMessageHandler.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/handler/WalkMessageHandler.java
rename to game/src/main/java/org/apollo/game/message/handler/WalkMessageHandler.java
diff --git a/game/src/main/org/apollo/game/message/handler/package-info.java b/game/src/main/java/org/apollo/game/message/handler/package-info.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/handler/package-info.java
rename to game/src/main/java/org/apollo/game/message/handler/package-info.java
diff --git a/game/src/main/org/apollo/game/message/impl/AddFriendMessage.java b/game/src/main/java/org/apollo/game/message/impl/AddFriendMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/AddFriendMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/AddFriendMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/AddIgnoreMessage.java b/game/src/main/java/org/apollo/game/message/impl/AddIgnoreMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/AddIgnoreMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/AddIgnoreMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/ArrowKeyMessage.java b/game/src/main/java/org/apollo/game/message/impl/ArrowKeyMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/ArrowKeyMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/ArrowKeyMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/ButtonMessage.java b/game/src/main/java/org/apollo/game/message/impl/ButtonMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/ButtonMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/ButtonMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/ChatMessage.java b/game/src/main/java/org/apollo/game/message/impl/ChatMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/ChatMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/ChatMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/ClearRegionMessage.java b/game/src/main/java/org/apollo/game/message/impl/ClearRegionMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/ClearRegionMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/ClearRegionMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/CloseInterfaceMessage.java b/game/src/main/java/org/apollo/game/message/impl/CloseInterfaceMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/CloseInterfaceMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/CloseInterfaceMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/ClosedInterfaceMessage.java b/game/src/main/java/org/apollo/game/message/impl/ClosedInterfaceMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/ClosedInterfaceMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/ClosedInterfaceMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/CommandMessage.java b/game/src/main/java/org/apollo/game/message/impl/CommandMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/CommandMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/CommandMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/ConfigMessage.java b/game/src/main/java/org/apollo/game/message/impl/ConfigMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/ConfigMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/ConfigMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/DialogueContinueMessage.java b/game/src/main/java/org/apollo/game/message/impl/DialogueContinueMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/DialogueContinueMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/DialogueContinueMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/DisplayCrossbonesMessage.java b/game/src/main/java/org/apollo/game/message/impl/DisplayCrossbonesMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/DisplayCrossbonesMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/DisplayCrossbonesMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/DisplayTabInterfaceMessage.java b/game/src/main/java/org/apollo/game/message/impl/DisplayTabInterfaceMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/DisplayTabInterfaceMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/DisplayTabInterfaceMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/EnterAmountMessage.java b/game/src/main/java/org/apollo/game/message/impl/EnterAmountMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/EnterAmountMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/EnterAmountMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/EnteredAmountMessage.java b/game/src/main/java/org/apollo/game/message/impl/EnteredAmountMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/EnteredAmountMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/EnteredAmountMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/FlaggedMouseEventMessage.java b/game/src/main/java/org/apollo/game/message/impl/FlaggedMouseEventMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/FlaggedMouseEventMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/FlaggedMouseEventMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/FlashTabInterfaceMessage.java b/game/src/main/java/org/apollo/game/message/impl/FlashTabInterfaceMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/FlashTabInterfaceMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/FlashTabInterfaceMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/FlashingTabClickedMessage.java b/game/src/main/java/org/apollo/game/message/impl/FlashingTabClickedMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/FlashingTabClickedMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/FlashingTabClickedMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/FocusUpdateMessage.java b/game/src/main/java/org/apollo/game/message/impl/FocusUpdateMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/FocusUpdateMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/FocusUpdateMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/ForwardPrivateChatMessage.java b/game/src/main/java/org/apollo/game/message/impl/ForwardPrivateChatMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/ForwardPrivateChatMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/ForwardPrivateChatMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/FriendServerStatusMessage.java b/game/src/main/java/org/apollo/game/message/impl/FriendServerStatusMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/FriendServerStatusMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/FriendServerStatusMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/GroupedRegionUpdateMessage.java b/game/src/main/java/org/apollo/game/message/impl/GroupedRegionUpdateMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/GroupedRegionUpdateMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/GroupedRegionUpdateMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/HintIconMessage.java b/game/src/main/java/org/apollo/game/message/impl/HintIconMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/HintIconMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/HintIconMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/IdAssignmentMessage.java b/game/src/main/java/org/apollo/game/message/impl/IdAssignmentMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/IdAssignmentMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/IdAssignmentMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/IgnoreListMessage.java b/game/src/main/java/org/apollo/game/message/impl/IgnoreListMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/IgnoreListMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/IgnoreListMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/InventoryItemMessage.java b/game/src/main/java/org/apollo/game/message/impl/InventoryItemMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/InventoryItemMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/InventoryItemMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/ItemActionMessage.java b/game/src/main/java/org/apollo/game/message/impl/ItemActionMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/ItemActionMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/ItemActionMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/ItemOnItemMessage.java b/game/src/main/java/org/apollo/game/message/impl/ItemOnItemMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/ItemOnItemMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/ItemOnItemMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/ItemOnNpcMessage.java b/game/src/main/java/org/apollo/game/message/impl/ItemOnNpcMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/ItemOnNpcMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/ItemOnNpcMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/ItemOnObjectMessage.java b/game/src/main/java/org/apollo/game/message/impl/ItemOnObjectMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/ItemOnObjectMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/ItemOnObjectMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/ItemOptionMessage.java b/game/src/main/java/org/apollo/game/message/impl/ItemOptionMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/ItemOptionMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/ItemOptionMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/KeepAliveMessage.java b/game/src/main/java/org/apollo/game/message/impl/KeepAliveMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/KeepAliveMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/KeepAliveMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/LogoutMessage.java b/game/src/main/java/org/apollo/game/message/impl/LogoutMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/LogoutMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/LogoutMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/MagicOnItemMessage.java b/game/src/main/java/org/apollo/game/message/impl/MagicOnItemMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/MagicOnItemMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/MagicOnItemMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/MagicOnMobMessage.java b/game/src/main/java/org/apollo/game/message/impl/MagicOnMobMessage.java
similarity index 84%
rename from game/src/main/org/apollo/game/message/impl/MagicOnMobMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/MagicOnMobMessage.java
index f408f9b3f..fabdc16b8 100644
--- a/game/src/main/org/apollo/game/message/impl/MagicOnMobMessage.java
+++ b/game/src/main/java/org/apollo/game/message/impl/MagicOnMobMessage.java
@@ -1,5 +1,6 @@
package org.apollo.game.message.impl;
+import com.google.common.base.MoreObjects;
import org.apollo.game.model.entity.EntityType;
import org.apollo.net.message.Message;
@@ -65,4 +66,8 @@ public int getSpellId() {
return spellId;
}
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this).add("type", getType()).add("index", getIndex()).add("spellId", getSpellId()).toString();
+ }
}
\ No newline at end of file
diff --git a/game/src/main/org/apollo/game/message/impl/MagicOnNpcMessage.java b/game/src/main/java/org/apollo/game/message/impl/MagicOnNpcMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/MagicOnNpcMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/MagicOnNpcMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/MagicOnPlayerMessage.java b/game/src/main/java/org/apollo/game/message/impl/MagicOnPlayerMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/MagicOnPlayerMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/MagicOnPlayerMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/MobAnimationResetMessage.java b/game/src/main/java/org/apollo/game/message/impl/MobAnimationResetMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/MobAnimationResetMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/MobAnimationResetMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/MobHintIconMessage.java b/game/src/main/java/org/apollo/game/message/impl/MobHintIconMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/MobHintIconMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/MobHintIconMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/MouseClickedMessage.java b/game/src/main/java/org/apollo/game/message/impl/MouseClickedMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/MouseClickedMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/MouseClickedMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/NpcActionMessage.java b/game/src/main/java/org/apollo/game/message/impl/NpcActionMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/NpcActionMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/NpcActionMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/NpcSynchronizationMessage.java b/game/src/main/java/org/apollo/game/message/impl/NpcSynchronizationMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/NpcSynchronizationMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/NpcSynchronizationMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/ObjectActionMessage.java b/game/src/main/java/org/apollo/game/message/impl/ObjectActionMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/ObjectActionMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/ObjectActionMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/OpenDialogueInterfaceMessage.java b/game/src/main/java/org/apollo/game/message/impl/OpenDialogueInterfaceMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/OpenDialogueInterfaceMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/OpenDialogueInterfaceMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/OpenDialogueOverlayMessage.java b/game/src/main/java/org/apollo/game/message/impl/OpenDialogueOverlayMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/OpenDialogueOverlayMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/OpenDialogueOverlayMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/OpenInterfaceMessage.java b/game/src/main/java/org/apollo/game/message/impl/OpenInterfaceMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/OpenInterfaceMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/OpenInterfaceMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/OpenInterfaceSidebarMessage.java b/game/src/main/java/org/apollo/game/message/impl/OpenInterfaceSidebarMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/OpenInterfaceSidebarMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/OpenInterfaceSidebarMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/OpenOverlayMessage.java b/game/src/main/java/org/apollo/game/message/impl/OpenOverlayMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/OpenOverlayMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/OpenOverlayMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/OpenSidebarMessage.java b/game/src/main/java/org/apollo/game/message/impl/OpenSidebarMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/OpenSidebarMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/OpenSidebarMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/PlayerActionMessage.java b/game/src/main/java/org/apollo/game/message/impl/PlayerActionMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/PlayerActionMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/PlayerActionMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/PlayerDesignMessage.java b/game/src/main/java/org/apollo/game/message/impl/PlayerDesignMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/PlayerDesignMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/PlayerDesignMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/PlayerSynchronizationMessage.java b/game/src/main/java/org/apollo/game/message/impl/PlayerSynchronizationMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/PlayerSynchronizationMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/PlayerSynchronizationMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/PositionHintIconMessage.java b/game/src/main/java/org/apollo/game/message/impl/PositionHintIconMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/PositionHintIconMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/PositionHintIconMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/PrivacyOptionMessage.java b/game/src/main/java/org/apollo/game/message/impl/PrivacyOptionMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/PrivacyOptionMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/PrivacyOptionMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/PrivateChatMessage.java b/game/src/main/java/org/apollo/game/message/impl/PrivateChatMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/PrivateChatMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/PrivateChatMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/PublicChatMessage.java b/game/src/main/java/org/apollo/game/message/impl/PublicChatMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/PublicChatMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/PublicChatMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/RegionChangeMessage.java b/game/src/main/java/org/apollo/game/message/impl/RegionChangeMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/RegionChangeMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/RegionChangeMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/RegionUpdateMessage.java b/game/src/main/java/org/apollo/game/message/impl/RegionUpdateMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/RegionUpdateMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/RegionUpdateMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/RemoveFriendMessage.java b/game/src/main/java/org/apollo/game/message/impl/RemoveFriendMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/RemoveFriendMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/RemoveFriendMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/RemoveIgnoreMessage.java b/game/src/main/java/org/apollo/game/message/impl/RemoveIgnoreMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/RemoveIgnoreMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/RemoveIgnoreMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/RemoveObjectMessage.java b/game/src/main/java/org/apollo/game/message/impl/RemoveObjectMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/RemoveObjectMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/RemoveObjectMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/RemoveTileItemMessage.java b/game/src/main/java/org/apollo/game/message/impl/RemoveTileItemMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/RemoveTileItemMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/RemoveTileItemMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/ReportAbuseMessage.java b/game/src/main/java/org/apollo/game/message/impl/ReportAbuseMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/ReportAbuseMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/ReportAbuseMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/SendFriendMessage.java b/game/src/main/java/org/apollo/game/message/impl/SendFriendMessage.java
similarity index 81%
rename from game/src/main/org/apollo/game/message/impl/SendFriendMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/SendFriendMessage.java
index 2900ab247..b86353c1a 100644
--- a/game/src/main/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 int getWorld() {
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/org/apollo/game/message/impl/SendObjectMessage.java b/game/src/main/java/org/apollo/game/message/impl/SendObjectMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/SendObjectMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/SendObjectMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/SendProjectileMessage.java b/game/src/main/java/org/apollo/game/message/impl/SendProjectileMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/SendProjectileMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/SendProjectileMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/SendPublicTileItemMessage.java b/game/src/main/java/org/apollo/game/message/impl/SendPublicTileItemMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/SendPublicTileItemMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/SendPublicTileItemMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/SendTileItemMessage.java b/game/src/main/java/org/apollo/game/message/impl/SendTileItemMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/SendTileItemMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/SendTileItemMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/ServerChatMessage.java b/game/src/main/java/org/apollo/game/message/impl/ServerChatMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/ServerChatMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/ServerChatMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/SetPlayerActionMessage.java b/game/src/main/java/org/apollo/game/message/impl/SetPlayerActionMessage.java
similarity index 76%
rename from game/src/main/org/apollo/game/message/impl/SetPlayerActionMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/SetPlayerActionMessage.java
index 4ebae8c32..4db7ceefc 100644
--- a/game/src/main/org/apollo/game/message/impl/SetPlayerActionMessage.java
+++ b/game/src/main/java/org/apollo/game/message/impl/SetPlayerActionMessage.java
@@ -2,6 +2,8 @@
import org.apollo.net.message.Message;
+import java.util.Objects;
+
/**
* A {@link Message} sent by the client to add an action to the menu when a player right-clicks another.
*
@@ -37,8 +39,8 @@ public SetPlayerActionMessage(String text, int slot) {
/**
* Creates the set player action message.
*
- * @param text The action text.
- * @param slot The menu slot.
+ * @param text The action text.
+ * @param slot The menu slot.
* @param primaryInteraction Whether or not the action is the primary action.
*/
public SetPlayerActionMessage(String text, int slot, boolean primaryInteraction) {
@@ -75,4 +77,20 @@ public boolean isPrimaryAction() {
return primaryAction;
}
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof SetPlayerActionMessage) {
+ SetPlayerActionMessage other = (SetPlayerActionMessage) o;
+ return slot == other.slot && primaryAction == other.primaryAction && Objects.equals(text, other.text);
+
+ }
+
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(text, slot, primaryAction);
+ }
+
}
\ No newline at end of file
diff --git a/game/src/main/org/apollo/game/message/impl/SetUpdatedRegionMessage.java b/game/src/main/java/org/apollo/game/message/impl/SetUpdatedRegionMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/SetUpdatedRegionMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/SetUpdatedRegionMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/SetWidgetItemModelMessage.java b/game/src/main/java/org/apollo/game/message/impl/SetWidgetItemModelMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/SetWidgetItemModelMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/SetWidgetItemModelMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/SetWidgetModelAnimationMessage.java b/game/src/main/java/org/apollo/game/message/impl/SetWidgetModelAnimationMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/SetWidgetModelAnimationMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/SetWidgetModelAnimationMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/SetWidgetModelMessage.java b/game/src/main/java/org/apollo/game/message/impl/SetWidgetModelMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/SetWidgetModelMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/SetWidgetModelMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/SetWidgetNpcModelMessage.java b/game/src/main/java/org/apollo/game/message/impl/SetWidgetNpcModelMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/SetWidgetNpcModelMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/SetWidgetNpcModelMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/SetWidgetPlayerModelMessage.java b/game/src/main/java/org/apollo/game/message/impl/SetWidgetPlayerModelMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/SetWidgetPlayerModelMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/SetWidgetPlayerModelMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/SetWidgetTextMessage.java b/game/src/main/java/org/apollo/game/message/impl/SetWidgetTextMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/SetWidgetTextMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/SetWidgetTextMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/SetWidgetVisibilityMessage.java b/game/src/main/java/org/apollo/game/message/impl/SetWidgetVisibilityMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/SetWidgetVisibilityMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/SetWidgetVisibilityMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/SpamPacketMessage.java b/game/src/main/java/org/apollo/game/message/impl/SpamPacketMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/SpamPacketMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/SpamPacketMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/SwitchItemMessage.java b/game/src/main/java/org/apollo/game/message/impl/SwitchItemMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/SwitchItemMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/SwitchItemMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/SwitchTabInterfaceMessage.java b/game/src/main/java/org/apollo/game/message/impl/SwitchTabInterfaceMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/SwitchTabInterfaceMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/SwitchTabInterfaceMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/TakeTileItemMessage.java b/game/src/main/java/org/apollo/game/message/impl/TakeTileItemMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/TakeTileItemMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/TakeTileItemMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/UpdateItemsMessage.java b/game/src/main/java/org/apollo/game/message/impl/UpdateItemsMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/UpdateItemsMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/UpdateItemsMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/UpdateRunEnergyMessage.java b/game/src/main/java/org/apollo/game/message/impl/UpdateRunEnergyMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/UpdateRunEnergyMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/UpdateRunEnergyMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/UpdateSkillMessage.java b/game/src/main/java/org/apollo/game/message/impl/UpdateSkillMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/UpdateSkillMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/UpdateSkillMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/UpdateSlottedItemsMessage.java b/game/src/main/java/org/apollo/game/message/impl/UpdateSlottedItemsMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/UpdateSlottedItemsMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/UpdateSlottedItemsMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/UpdateTileItemMessage.java b/game/src/main/java/org/apollo/game/message/impl/UpdateTileItemMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/UpdateTileItemMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/UpdateTileItemMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/UpdateWeightMessage.java b/game/src/main/java/org/apollo/game/message/impl/UpdateWeightMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/UpdateWeightMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/UpdateWeightMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/WalkMessage.java b/game/src/main/java/org/apollo/game/message/impl/WalkMessage.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/WalkMessage.java
rename to game/src/main/java/org/apollo/game/message/impl/WalkMessage.java
diff --git a/game/src/main/org/apollo/game/message/impl/package-info.java b/game/src/main/java/org/apollo/game/message/impl/package-info.java
similarity index 100%
rename from game/src/main/org/apollo/game/message/impl/package-info.java
rename to game/src/main/java/org/apollo/game/message/impl/package-info.java
diff --git a/game/src/main/org/apollo/game/model/Animation.java b/game/src/main/java/org/apollo/game/model/Animation.java
similarity index 61%
rename from game/src/main/org/apollo/game/model/Animation.java
rename to game/src/main/java/org/apollo/game/model/Animation.java
index 57df88a9b..0fe52750b 100644
--- a/game/src/main/org/apollo/game/model/Animation.java
+++ b/game/src/main/java/org/apollo/game/model/Animation.java
@@ -1,5 +1,8 @@
package org.apollo.game.model;
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Objects;
+
/**
* Represents an animation.
*
@@ -60,4 +63,28 @@ public int getId() {
return id;
}
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Animation animation = (Animation) o;
+ return delay == animation.delay && id == animation.id;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(delay, id);
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("delay", delay)
+ .add("id", id)
+ .toString();
+ }
}
\ No newline at end of file
diff --git a/game/src/main/org/apollo/game/model/Appearance.java b/game/src/main/java/org/apollo/game/model/Appearance.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/Appearance.java
rename to game/src/main/java/org/apollo/game/model/Appearance.java
diff --git a/game/src/main/org/apollo/game/model/Direction.java b/game/src/main/java/org/apollo/game/model/Direction.java
similarity index 98%
rename from game/src/main/org/apollo/game/model/Direction.java
rename to game/src/main/java/org/apollo/game/model/Direction.java
index dc03c5585..d1a13ffbd 100644
--- a/game/src/main/org/apollo/game/model/Direction.java
+++ b/game/src/main/java/org/apollo/game/model/Direction.java
@@ -85,7 +85,7 @@ public static Direction between(Position current, Position next) {
int deltaX = next.getX() - current.getX();
int deltaY = next.getY() - current.getY();
- return fromDeltas(deltaX, deltaY);
+ return fromDeltas(Integer.signum(deltaX), Integer.signum(deltaY));
}
/**
diff --git a/game/src/main/org/apollo/game/model/Graphic.java b/game/src/main/java/org/apollo/game/model/Graphic.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/Graphic.java
rename to game/src/main/java/org/apollo/game/model/Graphic.java
diff --git a/game/src/main/org/apollo/game/model/Item.java b/game/src/main/java/org/apollo/game/model/Item.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/Item.java
rename to game/src/main/java/org/apollo/game/model/Item.java
diff --git a/game/src/main/org/apollo/game/model/Position.java b/game/src/main/java/org/apollo/game/model/Position.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/Position.java
rename to game/src/main/java/org/apollo/game/model/Position.java
diff --git a/game/src/main/org/apollo/game/model/World.java b/game/src/main/java/org/apollo/game/model/World.java
similarity index 98%
rename from game/src/main/org/apollo/game/model/World.java
rename to game/src/main/java/org/apollo/game/model/World.java
index 991a41fc6..5d5adb0f8 100644
--- a/game/src/main/org/apollo/game/model/World.java
+++ b/game/src/main/java/org/apollo/game/model/World.java
@@ -299,7 +299,7 @@ public void register(Player player) {
playerRepository.add(player);
players.put(NameUtil.encodeBase37(username), player);
- logger.info("Registered player: " + player + " [count=" + playerRepository.size() + "]");
+ logger.finest("Registered player: " + player + " [count=" + playerRepository.size() + "]");
}
/**
@@ -359,7 +359,7 @@ public void unregister(final Player player) {
region.removeEntity(player);
playerRepository.remove(player);
- logger.info("Unregistered player: " + player + " [count=" + playerRepository.size() + "]");
+ logger.finest("Unregistered player: " + player + " [count=" + playerRepository.size() + "]");
}
/**
diff --git a/game/src/main/org/apollo/game/model/WorldConstants.java b/game/src/main/java/org/apollo/game/model/WorldConstants.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/WorldConstants.java
rename to game/src/main/java/org/apollo/game/model/WorldConstants.java
diff --git a/game/src/main/org/apollo/game/model/area/EntityUpdateType.java b/game/src/main/java/org/apollo/game/model/area/EntityUpdateType.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/area/EntityUpdateType.java
rename to game/src/main/java/org/apollo/game/model/area/EntityUpdateType.java
diff --git a/game/src/main/org/apollo/game/model/area/Region.java b/game/src/main/java/org/apollo/game/model/area/Region.java
similarity index 99%
rename from game/src/main/org/apollo/game/model/area/Region.java
rename to game/src/main/java/org/apollo/game/model/area/Region.java
index 7eafb952c..df7a9a170 100644
--- a/game/src/main/org/apollo/game/model/area/Region.java
+++ b/game/src/main/java/org/apollo/game/model/area/Region.java
@@ -413,7 +413,9 @@ private void record(T entity, EntityUpdateT
} else { // TODO should this really be possible?
removedObjects.get(height).remove(inverse);
}
- } else if (update == EntityUpdateType.REMOVE && !type.isTransient()) {
+ }
+
+ if (update == EntityUpdateType.REMOVE && !type.isTransient()) {
updates.remove(inverse);
}
diff --git a/game/src/main/org/apollo/game/model/area/RegionCoordinates.java b/game/src/main/java/org/apollo/game/model/area/RegionCoordinates.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/area/RegionCoordinates.java
rename to game/src/main/java/org/apollo/game/model/area/RegionCoordinates.java
diff --git a/game/src/main/org/apollo/game/model/area/RegionListener.java b/game/src/main/java/org/apollo/game/model/area/RegionListener.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/area/RegionListener.java
rename to game/src/main/java/org/apollo/game/model/area/RegionListener.java
diff --git a/game/src/main/org/apollo/game/model/area/RegionRepository.java b/game/src/main/java/org/apollo/game/model/area/RegionRepository.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/area/RegionRepository.java
rename to game/src/main/java/org/apollo/game/model/area/RegionRepository.java
diff --git a/game/src/main/org/apollo/game/model/area/collision/CollisionFlag.java b/game/src/main/java/org/apollo/game/model/area/collision/CollisionFlag.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/area/collision/CollisionFlag.java
rename to game/src/main/java/org/apollo/game/model/area/collision/CollisionFlag.java
diff --git a/game/src/main/org/apollo/game/model/area/collision/CollisionManager.java b/game/src/main/java/org/apollo/game/model/area/collision/CollisionManager.java
similarity index 84%
rename from game/src/main/org/apollo/game/model/area/collision/CollisionManager.java
rename to game/src/main/java/org/apollo/game/model/area/collision/CollisionManager.java
index 4b54b36ac..d8a428ff7 100644
--- a/game/src/main/org/apollo/game/model/area/collision/CollisionManager.java
+++ b/game/src/main/java/org/apollo/game/model/area/collision/CollisionManager.java
@@ -1,19 +1,21 @@
package org.apollo.game.model.area.collision;
import com.google.common.base.Preconditions;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Multimap;
import org.apollo.game.model.Direction;
import org.apollo.game.model.Position;
import org.apollo.game.model.area.Region;
+import org.apollo.game.model.area.RegionCoordinates;
import org.apollo.game.model.area.RegionRepository;
import org.apollo.game.model.area.collision.CollisionUpdate.DirectionFlag;
import org.apollo.game.model.entity.EntityType;
import org.apollo.game.model.entity.obj.GameObject;
import java.util.Collection;
-import java.util.Comparator;
+import java.util.HashSet;
import java.util.Map;
-import java.util.SortedSet;
-import java.util.TreeSet;
+import java.util.Set;
import static org.apollo.game.model.entity.EntityType.DYNAMIC_OBJECT;
import static org.apollo.game.model.entity.EntityType.STATIC_OBJECT;
@@ -25,20 +27,14 @@
public final class CollisionManager {
/**
- * A comparator that sorts {@link Position}s by their X coordinate, then Y, then height.
+ * A {@code HashMultimap} of region coordinates mapped to positions where the tile is completely blocked.
*/
- private static final Comparator POSITION_COMPARATOR =
- Comparator.comparingInt(Position::getX).thenComparingInt(Position::getY).thenComparingInt(Position::getHeight);
+ private final Multimap blocked = HashMultimap.create();
/**
- * A {@code SortedSet} of positions where the tile is part of a bridged structure.
+ * A {@code HashSet} of positions where the tile is part of a bridged structure.
*/
- private final SortedSet bridges = new TreeSet<>(POSITION_COMPARATOR);
-
- /**
- * A {@code SortedSet} of positions where the tile is completely blocked.
- */
- private final SortedSet blocked = new TreeSet<>(POSITION_COMPARATOR);
+ private final Set bridges = new HashSet<>();
/**
* The {@link RegionRepository} used to lookup {@link CollisionMatrix} objects.
@@ -69,31 +65,33 @@ public void build(boolean rebuilding) {
}
}
- CollisionUpdate.Builder builder = new CollisionUpdate.Builder();
- builder.type(CollisionUpdateType.ADDING);
+ regions.getRegions().forEach(region -> {
+ CollisionUpdate.Builder builder = new CollisionUpdate.Builder();
+ builder.type(CollisionUpdateType.ADDING);
- for (Position tile : blocked) {
- int x = tile.getX(), y = tile.getY();
- int height = tile.getHeight();
+ blocked.get(region.getCoordinates()).forEach(tile -> {
+ int x = tile.getX(), y = tile.getY();
+ int height = tile.getHeight();
- if (bridges.contains(new Position(x, y, 1))) {
- height--;
- }
+ if (bridges.contains(new Position(x, y, 1))) {
+ height--;
+ }
- if (height >= 0) {
- builder.tile(new Position(x, y, height), false, Direction.NESW);
- }
- }
+ if (height >= 0) {
+ builder.tile(new Position(x, y, height), false, Direction.NESW);
+ }
+ });
- apply(builder.build());
+ apply(builder.build());
- for (Region region : regions.getRegions()) {
CollisionUpdate.Builder objects = new CollisionUpdate.Builder();
objects.type(CollisionUpdateType.ADDING);
- region.getEntities(STATIC_OBJECT, DYNAMIC_OBJECT).forEach(entity -> objects.object((GameObject) entity));
+ region.getEntities(STATIC_OBJECT, DYNAMIC_OBJECT)
+ .forEach(entity -> objects.object((GameObject) entity));
+
apply(objects.build());
- }
+ });
}
/**
@@ -255,7 +253,7 @@ private void flag(CollisionUpdateType type, CollisionMatrix matrix, int localX,
* @param position The {@link Position} of the tile.
*/
public void block(Position position) {
- blocked.add(position);
+ blocked.put(position.getRegionCoordinates(), position);
}
/**
diff --git a/game/src/main/org/apollo/game/model/area/collision/CollisionMatrix.java b/game/src/main/java/org/apollo/game/model/area/collision/CollisionMatrix.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/area/collision/CollisionMatrix.java
rename to game/src/main/java/org/apollo/game/model/area/collision/CollisionMatrix.java
diff --git a/game/src/main/org/apollo/game/model/area/collision/CollisionUpdate.java b/game/src/main/java/org/apollo/game/model/area/collision/CollisionUpdate.java
similarity index 95%
rename from game/src/main/org/apollo/game/model/area/collision/CollisionUpdate.java
rename to game/src/main/java/org/apollo/game/model/area/collision/CollisionUpdate.java
index cd8fb73ab..1c4047842 100644
--- a/game/src/main/org/apollo/game/model/area/collision/CollisionUpdate.java
+++ b/game/src/main/java/org/apollo/game/model/area/collision/CollisionUpdate.java
@@ -195,23 +195,16 @@ public void object(GameObject object) {
}
int x = position.getX(), y = position.getY(), height = position.getHeight();
- int width = definition.getWidth(), length = definition.getLength();
boolean impenetrable = definition.isImpenetrable();
int orientation = object.getOrientation();
- // north / south for walls, north east / south west for corners
- if (orientation == 1 || orientation == 3) {
- width = definition.getLength();
- length = definition.getWidth();
- }
-
if (type == FLOOR_DECORATION.getValue()) {
if (definition.isInteractive() && definition.isSolid()) {
tile(new Position(x, y, height), impenetrable, Direction.NESW);
}
} else if (type >= DIAGONAL_WALL.getValue() && type < FLOOR_DECORATION.getValue()) {
- for (int dx = 0; dx < width; dx++) {
- for (int dy = 0; dy < length; dy++) {
+ for (int dx = 0; dx < object.getWidth(); dx++) {
+ for (int dy = 0; dy < object.getLength(); dy++) {
tile(new Position(x + dx, y + dy, height), impenetrable, Direction.NESW);
}
}
diff --git a/game/src/main/org/apollo/game/model/area/collision/CollisionUpdateListener.java b/game/src/main/java/org/apollo/game/model/area/collision/CollisionUpdateListener.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/area/collision/CollisionUpdateListener.java
rename to game/src/main/java/org/apollo/game/model/area/collision/CollisionUpdateListener.java
diff --git a/game/src/main/org/apollo/game/model/area/collision/CollisionUpdateType.java b/game/src/main/java/org/apollo/game/model/area/collision/CollisionUpdateType.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/area/collision/CollisionUpdateType.java
rename to game/src/main/java/org/apollo/game/model/area/collision/CollisionUpdateType.java
diff --git a/game/src/main/org/apollo/game/model/area/collision/package-info.java b/game/src/main/java/org/apollo/game/model/area/collision/package-info.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/area/collision/package-info.java
rename to game/src/main/java/org/apollo/game/model/area/collision/package-info.java
diff --git a/game/src/main/org/apollo/game/model/area/package-info.java b/game/src/main/java/org/apollo/game/model/area/package-info.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/area/package-info.java
rename to game/src/main/java/org/apollo/game/model/area/package-info.java
diff --git a/game/src/main/org/apollo/game/model/area/update/GroupableEntity.java b/game/src/main/java/org/apollo/game/model/area/update/GroupableEntity.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/area/update/GroupableEntity.java
rename to game/src/main/java/org/apollo/game/model/area/update/GroupableEntity.java
diff --git a/game/src/main/org/apollo/game/model/area/update/ItemUpdateOperation.java b/game/src/main/java/org/apollo/game/model/area/update/ItemUpdateOperation.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/area/update/ItemUpdateOperation.java
rename to game/src/main/java/org/apollo/game/model/area/update/ItemUpdateOperation.java
diff --git a/game/src/main/org/apollo/game/model/area/update/ObjectUpdateOperation.java b/game/src/main/java/org/apollo/game/model/area/update/ObjectUpdateOperation.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/area/update/ObjectUpdateOperation.java
rename to game/src/main/java/org/apollo/game/model/area/update/ObjectUpdateOperation.java
diff --git a/game/src/main/org/apollo/game/model/area/update/ProjectileUpdateOperation.java b/game/src/main/java/org/apollo/game/model/area/update/ProjectileUpdateOperation.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/area/update/ProjectileUpdateOperation.java
rename to game/src/main/java/org/apollo/game/model/area/update/ProjectileUpdateOperation.java
diff --git a/game/src/main/org/apollo/game/model/area/update/UpdateOperation.java b/game/src/main/java/org/apollo/game/model/area/update/UpdateOperation.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/area/update/UpdateOperation.java
rename to game/src/main/java/org/apollo/game/model/area/update/UpdateOperation.java
diff --git a/game/src/main/org/apollo/game/model/area/update/package-info.java b/game/src/main/java/org/apollo/game/model/area/update/package-info.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/area/update/package-info.java
rename to game/src/main/java/org/apollo/game/model/area/update/package-info.java
diff --git a/game/src/main/org/apollo/game/model/entity/Entity.java b/game/src/main/java/org/apollo/game/model/entity/Entity.java
similarity index 69%
rename from game/src/main/org/apollo/game/model/entity/Entity.java
rename to game/src/main/java/org/apollo/game/model/entity/Entity.java
index 97e35349f..3f5a955a9 100644
--- a/game/src/main/org/apollo/game/model/entity/Entity.java
+++ b/game/src/main/java/org/apollo/game/model/entity/Entity.java
@@ -20,6 +20,11 @@ public abstract class Entity {
*/
protected final World world;
+ /**
+ * The EntityBounds for this Entity.
+ */
+ private EntityBounds bounds;
+
/**
* Creates the Entity.
*
@@ -34,6 +39,20 @@ public Entity(World world, Position position) {
@Override
public abstract boolean equals(Object obj);
+ /**
+ * Gets the {@link EntityBounds} for this Entity.
+ *
+ * @return The EntityBounds.
+ */
+ public EntityBounds getBounds() {
+
+ if(bounds == null) {
+ bounds = new EntityBounds(this);
+ }
+
+ return bounds;
+ }
+
/**
* Gets the {@link Position} of this Entity.
*
@@ -59,6 +78,20 @@ public World getWorld() {
*/
public abstract EntityType getEntityType();
+ /**
+ * Gets the length of this Entity.
+ *
+ * @return The length.
+ */
+ public abstract int getLength();
+
+ /**
+ * Gets the width of this Entity.
+ *
+ * @return The width.
+ */
+ public abstract int getWidth();
+
@Override
public abstract int hashCode();
diff --git a/game/src/main/java/org/apollo/game/model/entity/EntityBounds.java b/game/src/main/java/org/apollo/game/model/entity/EntityBounds.java
new file mode 100644
index 000000000..3d4c3a9ae
--- /dev/null
+++ b/game/src/main/java/org/apollo/game/model/entity/EntityBounds.java
@@ -0,0 +1,48 @@
+package org.apollo.game.model.entity;
+
+import org.apollo.game.model.Position;
+
+/**
+ * The bounds of an {@link Entity}.
+ *
+ * @author Steve Soltys
+ */
+public class EntityBounds {
+
+ /**
+ * The {@link Entity}.
+ */
+ private final Entity entity;
+
+ /**
+ * Creates an EntityBounds.
+ *
+ * @param entity The entity.
+ */
+ EntityBounds(Entity entity) {
+ this.entity = entity;
+ }
+
+ /**
+ * Checks whether the given position is within the Entity's bounds.
+ *
+ * @param position The position.
+ * @return A flag indicating whether or not the position exists within the Entity's bounds.
+ */
+ public boolean contains(Position position) {
+ int positionX = position.getX();
+ int positionY = position.getY();
+ int positionHeight = position.getHeight();
+
+ int entityX = entity.getPosition().getX();
+ int entityY = entity.getPosition().getY();
+ int entityHeight = entity.getPosition().getHeight();
+
+ int width = entity.getWidth();
+ int length = entity.getLength();
+
+ return positionX >= entityX && positionX < entityX + width &&
+ positionY >= entityY && positionY < entityY + length &&
+ positionHeight == entityHeight;
+ }
+}
diff --git a/game/src/main/org/apollo/game/model/entity/EntityType.java b/game/src/main/java/org/apollo/game/model/entity/EntityType.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/entity/EntityType.java
rename to game/src/main/java/org/apollo/game/model/entity/EntityType.java
diff --git a/game/src/main/org/apollo/game/model/entity/EquipmentConstants.java b/game/src/main/java/org/apollo/game/model/entity/EquipmentConstants.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/entity/EquipmentConstants.java
rename to game/src/main/java/org/apollo/game/model/entity/EquipmentConstants.java
diff --git a/game/src/main/org/apollo/game/model/entity/GroundItem.java b/game/src/main/java/org/apollo/game/model/entity/GroundItem.java
similarity index 96%
rename from game/src/main/org/apollo/game/model/entity/GroundItem.java
rename to game/src/main/java/org/apollo/game/model/entity/GroundItem.java
index a81c612c1..a338d4149 100644
--- a/game/src/main/org/apollo/game/model/entity/GroundItem.java
+++ b/game/src/main/java/org/apollo/game/model/entity/GroundItem.java
@@ -80,6 +80,16 @@ public EntityType getEntityType() {
return EntityType.GROUND_ITEM;
}
+ @Override
+ public int getLength() {
+ return 1;
+ }
+
+ @Override
+ public int getWidth() {
+ return 1;
+ }
+
/**
* Gets the {@link Item} displayed on the ground.
*
diff --git a/game/src/main/org/apollo/game/model/entity/Mob.java b/game/src/main/java/org/apollo/game/model/entity/Mob.java
similarity index 96%
rename from game/src/main/org/apollo/game/model/entity/Mob.java
rename to game/src/main/java/org/apollo/game/model/entity/Mob.java
index 82cdba964..ec09bb9cb 100644
--- a/game/src/main/org/apollo/game/model/entity/Mob.java
+++ b/game/src/main/java/org/apollo/game/model/entity/Mob.java
@@ -235,6 +235,15 @@ public final Direction getFirstDirection() {
return firstDirection;
}
+ /**
+ * Gets the current action, if any, of this mob.
+ *
+ * @return The action.
+ */
+ public final Action> getAction() {
+ return action;
+ }
+
/**
* Gets the index of this mob.
*
@@ -327,6 +336,25 @@ public final WalkingQueue getWalkingQueue() {
return walkingQueue;
}
+ @Override
+ public int getLength() {
+ return definition.map(NpcDefinition::getSize).orElse(1);
+ }
+
+ @Override
+ public int getWidth() {
+ return definition.map(NpcDefinition::getSize).orElse(1);
+ }
+
+ /**
+ * Check whether this mob has a current active {@link Action}.
+ *
+ * @return {@code true} if this mob has a non-null {@link Action}.
+ */
+ public final boolean hasAction() {
+ return action != null;
+ }
+
/**
* Returns whether or not this mob has an {@link NpcDefinition}.
*
diff --git a/game/src/main/org/apollo/game/model/entity/MobRepository.java b/game/src/main/java/org/apollo/game/model/entity/MobRepository.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/entity/MobRepository.java
rename to game/src/main/java/org/apollo/game/model/entity/MobRepository.java
diff --git a/game/src/main/org/apollo/game/model/entity/Npc.java b/game/src/main/java/org/apollo/game/model/entity/Npc.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/entity/Npc.java
rename to game/src/main/java/org/apollo/game/model/entity/Npc.java
diff --git a/game/src/main/org/apollo/game/model/entity/Player.java b/game/src/main/java/org/apollo/game/model/entity/Player.java
similarity index 98%
rename from game/src/main/org/apollo/game/model/entity/Player.java
rename to game/src/main/java/org/apollo/game/model/entity/Player.java
index 2a323b109..76c06db95 100644
--- a/game/src/main/org/apollo/game/model/entity/Player.java
+++ b/game/src/main/java/org/apollo/game/model/entity/Player.java
@@ -28,6 +28,7 @@
import org.apollo.game.model.entity.attr.AttributeMap;
import org.apollo.game.model.entity.attr.AttributePersistence;
import org.apollo.game.model.entity.attr.NumericalAttribute;
+import org.apollo.game.model.entity.attr.BooleanAttribute;
import org.apollo.game.model.entity.obj.DynamicGameObject;
import org.apollo.game.model.entity.setting.MembershipStatus;
import org.apollo.game.model.entity.setting.PrivacyState;
@@ -943,6 +944,22 @@ public void setWithdrawingNotes(boolean withdrawingNotes) {
this.withdrawingNotes = withdrawingNotes;
}
+ /**
+ * Ban the player.
+ */
+ public void ban() {
+ attributes.set("banned", new BooleanAttribute(true));
+ }
+
+ /**
+ * Sets the mute status of a player.
+ *
+ * @param muted Whether the player is muted.
+ */
+ public void setMuted(boolean muted) {
+ attributes.set("muted", new BooleanAttribute(muted));
+ }
+
@Override
public void shout(String message, boolean chatOnly) {
blockSet.add(SynchronizationBlock.createForceChatBlock(chatOnly ? message : '~' + message));
diff --git a/game/src/main/org/apollo/game/model/entity/Projectile.java b/game/src/main/java/org/apollo/game/model/entity/Projectile.java
similarity index 99%
rename from game/src/main/org/apollo/game/model/entity/Projectile.java
rename to game/src/main/java/org/apollo/game/model/entity/Projectile.java
index db5459d89..a05f218b1 100644
--- a/game/src/main/org/apollo/game/model/entity/Projectile.java
+++ b/game/src/main/java/org/apollo/game/model/entity/Projectile.java
@@ -414,6 +414,16 @@ public EntityType getEntityType() {
return EntityType.PROJECTILE;
}
+ @Override
+ public int getLength() {
+ return 1;
+ }
+
+ @Override
+ public int getWidth() {
+ return 1;
+ }
+
@Override
public int hashCode() {
return Objects.hashCode(position, destination, delay, lifetime, target, startHeight,
diff --git a/game/src/main/org/apollo/game/model/entity/Skill.java b/game/src/main/java/org/apollo/game/model/entity/Skill.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/entity/Skill.java
rename to game/src/main/java/org/apollo/game/model/entity/Skill.java
diff --git a/game/src/main/org/apollo/game/model/entity/SkillSet.java b/game/src/main/java/org/apollo/game/model/entity/SkillSet.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/entity/SkillSet.java
rename to game/src/main/java/org/apollo/game/model/entity/SkillSet.java
diff --git a/game/src/main/org/apollo/game/model/entity/WalkingQueue.java b/game/src/main/java/org/apollo/game/model/entity/WalkingQueue.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/entity/WalkingQueue.java
rename to game/src/main/java/org/apollo/game/model/entity/WalkingQueue.java
diff --git a/game/src/main/org/apollo/game/model/entity/attr/Attribute.java b/game/src/main/java/org/apollo/game/model/entity/attr/Attribute.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/entity/attr/Attribute.java
rename to game/src/main/java/org/apollo/game/model/entity/attr/Attribute.java
diff --git a/game/src/main/org/apollo/game/model/entity/attr/AttributeDefinition.java b/game/src/main/java/org/apollo/game/model/entity/attr/AttributeDefinition.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/entity/attr/AttributeDefinition.java
rename to game/src/main/java/org/apollo/game/model/entity/attr/AttributeDefinition.java
diff --git a/game/src/main/org/apollo/game/model/entity/attr/AttributeMap.java b/game/src/main/java/org/apollo/game/model/entity/attr/AttributeMap.java
similarity index 96%
rename from game/src/main/org/apollo/game/model/entity/attr/AttributeMap.java
rename to game/src/main/java/org/apollo/game/model/entity/attr/AttributeMap.java
index 0ee12779d..c521e7b79 100644
--- a/game/src/main/org/apollo/game/model/entity/attr/AttributeMap.java
+++ b/game/src/main/java/org/apollo/game/model/entity/attr/AttributeMap.java
@@ -3,7 +3,6 @@
import java.util.HashMap;
import java.util.Map;
-import org.jruby.RubySymbol;
import com.google.common.base.Preconditions;
@@ -118,8 +117,6 @@ private Attribute> createAttribute(T value, AttributeType type) {
return new NumericalAttribute((Double) value);
case STRING:
return new StringAttribute((String) value);
- case SYMBOL:
- return new StringAttribute(((RubySymbol) value).asJavaString(), true);
case BOOLEAN:
return new BooleanAttribute((Boolean) value);
}
diff --git a/game/src/main/org/apollo/game/model/entity/attr/AttributePersistence.java b/game/src/main/java/org/apollo/game/model/entity/attr/AttributePersistence.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/entity/attr/AttributePersistence.java
rename to game/src/main/java/org/apollo/game/model/entity/attr/AttributePersistence.java
diff --git a/game/src/main/org/apollo/game/model/entity/attr/AttributeType.java b/game/src/main/java/org/apollo/game/model/entity/attr/AttributeType.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/entity/attr/AttributeType.java
rename to game/src/main/java/org/apollo/game/model/entity/attr/AttributeType.java
diff --git a/game/src/main/org/apollo/game/model/entity/attr/BooleanAttribute.java b/game/src/main/java/org/apollo/game/model/entity/attr/BooleanAttribute.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/entity/attr/BooleanAttribute.java
rename to game/src/main/java/org/apollo/game/model/entity/attr/BooleanAttribute.java
diff --git a/game/src/main/org/apollo/game/model/entity/attr/NumericalAttribute.java b/game/src/main/java/org/apollo/game/model/entity/attr/NumericalAttribute.java
similarity index 84%
rename from game/src/main/org/apollo/game/model/entity/attr/NumericalAttribute.java
rename to game/src/main/java/org/apollo/game/model/entity/attr/NumericalAttribute.java
index 0aa287006..48b53f469 100644
--- a/game/src/main/org/apollo/game/model/entity/attr/NumericalAttribute.java
+++ b/game/src/main/java/org/apollo/game/model/entity/attr/NumericalAttribute.java
@@ -30,13 +30,13 @@ public NumericalAttribute(Number value) {
@Override
public byte[] encode() {
- long encoded = type == AttributeType.DOUBLE ? Double.doubleToLongBits((double) value) : (long) value;
+ long encoded = type == AttributeType.DOUBLE ? Double.doubleToLongBits(value.doubleValue()) : value.longValue();
return Longs.toByteArray(encoded);
}
@Override
public String toString() {
- return type == AttributeType.DOUBLE ? Double.toString((double) value) : Long.toString((long) value);
+ return type == AttributeType.DOUBLE ? Double.toString(value.doubleValue()) : Long.toString(value.longValue());
}
}
\ No newline at end of file
diff --git a/game/src/main/org/apollo/game/model/entity/attr/StringAttribute.java b/game/src/main/java/org/apollo/game/model/entity/attr/StringAttribute.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/entity/attr/StringAttribute.java
rename to game/src/main/java/org/apollo/game/model/entity/attr/StringAttribute.java
diff --git a/game/src/main/org/apollo/game/model/entity/attr/package-info.java b/game/src/main/java/org/apollo/game/model/entity/attr/package-info.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/entity/attr/package-info.java
rename to game/src/main/java/org/apollo/game/model/entity/attr/package-info.java
diff --git a/game/src/main/org/apollo/game/model/entity/obj/DynamicGameObject.java b/game/src/main/java/org/apollo/game/model/entity/obj/DynamicGameObject.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/entity/obj/DynamicGameObject.java
rename to game/src/main/java/org/apollo/game/model/entity/obj/DynamicGameObject.java
diff --git a/game/src/main/org/apollo/game/model/entity/obj/GameObject.java b/game/src/main/java/org/apollo/game/model/entity/obj/GameObject.java
similarity index 72%
rename from game/src/main/org/apollo/game/model/entity/obj/GameObject.java
rename to game/src/main/java/org/apollo/game/model/entity/obj/GameObject.java
index cdb0f749d..1ceb76897 100644
--- a/game/src/main/org/apollo/game/model/entity/obj/GameObject.java
+++ b/game/src/main/java/org/apollo/game/model/entity/obj/GameObject.java
@@ -2,6 +2,7 @@
import org.apollo.cache.def.ObjectDefinition;
import org.apollo.game.model.Position;
+import org.apollo.game.model.Direction;
import org.apollo.game.model.World;
import org.apollo.game.model.area.EntityUpdateType;
import org.apollo.game.model.area.Region;
@@ -12,6 +13,9 @@
import com.google.common.base.MoreObjects;
+import static org.apollo.game.model.entity.obj.ObjectType.RECTANGULAR_CORNER;
+import static org.apollo.game.model.entity.obj.ObjectType.TRIANGULAR_CORNER;
+
/**
* Represents an object in the game world.
*
@@ -85,6 +89,34 @@ public int getType() {
return packed >> 2 & 0x3F;
}
+ @Override
+ public int getLength() {
+ return isRotated() ? getDefinition().getWidth() : getDefinition().getLength();
+ }
+
+ @Override
+ public int getWidth() {
+ return isRotated() ? getDefinition().getLength() : getDefinition().getWidth();
+ }
+
+ /**
+ * Returns whether or not this GameObject's orientation is rotated {@link Direction#WEST} or {@link Direction#EAST}.
+ *
+ * @return {@code true} iff this GameObject's orientation is rotated.
+ */
+ public boolean isRotated() {
+ int orientation = getOrientation();
+ int type = getType();
+ Direction direction = Direction.WNES[orientation];
+
+ if (type == TRIANGULAR_CORNER.getValue() || type == RECTANGULAR_CORNER.getValue()) {
+ direction = Direction.WNES_DIAGONAL[orientation];
+ }
+
+ return direction == Direction.NORTH || direction == Direction.SOUTH
+ || direction == Direction.NORTH_WEST || direction == Direction.SOUTH_EAST;
+ }
+
@Override
public int hashCode() {
return packed;
@@ -109,5 +141,4 @@ public ObjectUpdateOperation toUpdateOperation(Region region, EntityUpdateType o
* @return {@code true} if the Player can see this GameObject, {@code false} if not.
*/
public abstract boolean viewableBy(Player player, World world);
-
}
\ No newline at end of file
diff --git a/game/src/main/org/apollo/game/model/entity/obj/ObjectGroup.java b/game/src/main/java/org/apollo/game/model/entity/obj/ObjectGroup.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/entity/obj/ObjectGroup.java
rename to game/src/main/java/org/apollo/game/model/entity/obj/ObjectGroup.java
diff --git a/game/src/main/org/apollo/game/model/entity/obj/ObjectType.java b/game/src/main/java/org/apollo/game/model/entity/obj/ObjectType.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/entity/obj/ObjectType.java
rename to game/src/main/java/org/apollo/game/model/entity/obj/ObjectType.java
diff --git a/game/src/main/org/apollo/game/model/entity/obj/StaticGameObject.java b/game/src/main/java/org/apollo/game/model/entity/obj/StaticGameObject.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/entity/obj/StaticGameObject.java
rename to game/src/main/java/org/apollo/game/model/entity/obj/StaticGameObject.java
diff --git a/game/src/main/org/apollo/game/model/entity/obj/package-info.java b/game/src/main/java/org/apollo/game/model/entity/obj/package-info.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/entity/obj/package-info.java
rename to game/src/main/java/org/apollo/game/model/entity/obj/package-info.java
diff --git a/game/src/main/org/apollo/game/model/entity/package-info.java b/game/src/main/java/org/apollo/game/model/entity/package-info.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/entity/package-info.java
rename to game/src/main/java/org/apollo/game/model/entity/package-info.java
diff --git a/game/src/main/org/apollo/game/model/entity/path/AStarPathfindingAlgorithm.java b/game/src/main/java/org/apollo/game/model/entity/path/AStarPathfindingAlgorithm.java
similarity index 96%
rename from game/src/main/org/apollo/game/model/entity/path/AStarPathfindingAlgorithm.java
rename to game/src/main/java/org/apollo/game/model/entity/path/AStarPathfindingAlgorithm.java
index 0f2f1bc7a..2b8482f52 100644
--- a/game/src/main/org/apollo/game/model/entity/path/AStarPathfindingAlgorithm.java
+++ b/game/src/main/java/org/apollo/game/model/entity/path/AStarPathfindingAlgorithm.java
@@ -11,8 +11,6 @@
import org.apollo.game.model.Direction;
import org.apollo.game.model.Position;
-import org.apollo.game.model.World;
-import org.apollo.game.model.area.RegionRepository;
import org.apollo.game.model.area.collision.CollisionManager;
/**
@@ -76,7 +74,7 @@ public Deque find(Position origin, Position target) {
continue;
}
- Position adjacent = new Position(nextX, nextY);
+ Position adjacent = new Position(nextX, nextY, position.getHeight());
Direction direction = Direction.between(adjacent, position);
if (traversable(adjacent, direction)) {
Node node = nodes.computeIfAbsent(adjacent, Node::new);
diff --git a/game/src/main/org/apollo/game/model/entity/path/ChebyshevHeuristic.java b/game/src/main/java/org/apollo/game/model/entity/path/ChebyshevHeuristic.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/entity/path/ChebyshevHeuristic.java
rename to game/src/main/java/org/apollo/game/model/entity/path/ChebyshevHeuristic.java
diff --git a/game/src/main/org/apollo/game/model/entity/path/EuclideanHeuristic.java b/game/src/main/java/org/apollo/game/model/entity/path/EuclideanHeuristic.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/entity/path/EuclideanHeuristic.java
rename to game/src/main/java/org/apollo/game/model/entity/path/EuclideanHeuristic.java
diff --git a/game/src/main/org/apollo/game/model/entity/path/Heuristic.java b/game/src/main/java/org/apollo/game/model/entity/path/Heuristic.java
similarity index 88%
rename from game/src/main/org/apollo/game/model/entity/path/Heuristic.java
rename to game/src/main/java/org/apollo/game/model/entity/path/Heuristic.java
index 468c65bde..45390aafe 100644
--- a/game/src/main/org/apollo/game/model/entity/path/Heuristic.java
+++ b/game/src/main/java/org/apollo/game/model/entity/path/Heuristic.java
@@ -7,7 +7,7 @@
*
* @author Major
*/
-abstract class Heuristic {
+public abstract class Heuristic {
/**
* Estimates the value for this heuristic.
diff --git a/game/src/main/org/apollo/game/model/entity/path/ManhattanHeuristic.java b/game/src/main/java/org/apollo/game/model/entity/path/ManhattanHeuristic.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/entity/path/ManhattanHeuristic.java
rename to game/src/main/java/org/apollo/game/model/entity/path/ManhattanHeuristic.java
diff --git a/game/src/main/org/apollo/game/model/entity/path/Node.java b/game/src/main/java/org/apollo/game/model/entity/path/Node.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/entity/path/Node.java
rename to game/src/main/java/org/apollo/game/model/entity/path/Node.java
diff --git a/game/src/main/org/apollo/game/model/entity/path/PathfindingAlgorithm.java b/game/src/main/java/org/apollo/game/model/entity/path/PathfindingAlgorithm.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/entity/path/PathfindingAlgorithm.java
rename to game/src/main/java/org/apollo/game/model/entity/path/PathfindingAlgorithm.java
diff --git a/game/src/main/org/apollo/game/model/entity/path/SimplePathfindingAlgorithm.java b/game/src/main/java/org/apollo/game/model/entity/path/SimplePathfindingAlgorithm.java
similarity index 97%
rename from game/src/main/org/apollo/game/model/entity/path/SimplePathfindingAlgorithm.java
rename to game/src/main/java/org/apollo/game/model/entity/path/SimplePathfindingAlgorithm.java
index 421315f83..1affd1278 100644
--- a/game/src/main/org/apollo/game/model/entity/path/SimplePathfindingAlgorithm.java
+++ b/game/src/main/java/org/apollo/game/model/entity/path/SimplePathfindingAlgorithm.java
@@ -91,7 +91,7 @@ private Deque addHorizontal(Position start, Position target, Deque 0 ? Direction.SOUTH : Direction.NORTH)) {
+ if (!last.equals(target) && dy != 0 && traversable(last, boundaries, dy > 0 ? Direction.SOUTH : Direction.NORTH)) {
return addVertical(last, target, positions);
}
@@ -143,4 +143,4 @@ && traversable(last, boundaries, dx > 0 ? Direction.WEST : Direction.EAST)) {
return positions;
}
-}
\ No newline at end of file
+}
diff --git a/game/src/main/org/apollo/game/model/entity/path/package-info.java b/game/src/main/java/org/apollo/game/model/entity/path/package-info.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/entity/path/package-info.java
rename to game/src/main/java/org/apollo/game/model/entity/path/package-info.java
diff --git a/game/src/main/org/apollo/game/model/entity/setting/Gender.java b/game/src/main/java/org/apollo/game/model/entity/setting/Gender.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/entity/setting/Gender.java
rename to game/src/main/java/org/apollo/game/model/entity/setting/Gender.java
diff --git a/game/src/main/org/apollo/game/model/entity/setting/MembershipStatus.java b/game/src/main/java/org/apollo/game/model/entity/setting/MembershipStatus.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/entity/setting/MembershipStatus.java
rename to game/src/main/java/org/apollo/game/model/entity/setting/MembershipStatus.java
diff --git a/game/src/main/org/apollo/game/model/entity/setting/PrivacyState.java b/game/src/main/java/org/apollo/game/model/entity/setting/PrivacyState.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/entity/setting/PrivacyState.java
rename to game/src/main/java/org/apollo/game/model/entity/setting/PrivacyState.java
diff --git a/game/src/main/org/apollo/game/model/entity/setting/PrivilegeLevel.java b/game/src/main/java/org/apollo/game/model/entity/setting/PrivilegeLevel.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/entity/setting/PrivilegeLevel.java
rename to game/src/main/java/org/apollo/game/model/entity/setting/PrivilegeLevel.java
diff --git a/game/src/main/org/apollo/game/model/entity/setting/ScreenBrightness.java b/game/src/main/java/org/apollo/game/model/entity/setting/ScreenBrightness.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/entity/setting/ScreenBrightness.java
rename to game/src/main/java/org/apollo/game/model/entity/setting/ScreenBrightness.java
diff --git a/game/src/main/org/apollo/game/model/entity/setting/ServerStatus.java b/game/src/main/java/org/apollo/game/model/entity/setting/ServerStatus.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/entity/setting/ServerStatus.java
rename to game/src/main/java/org/apollo/game/model/entity/setting/ServerStatus.java
diff --git a/game/src/main/org/apollo/game/model/entity/setting/package-info.java b/game/src/main/java/org/apollo/game/model/entity/setting/package-info.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/entity/setting/package-info.java
rename to game/src/main/java/org/apollo/game/model/entity/setting/package-info.java
diff --git a/game/src/main/org/apollo/game/model/event/Event.java b/game/src/main/java/org/apollo/game/model/event/Event.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/event/Event.java
rename to game/src/main/java/org/apollo/game/model/event/Event.java
diff --git a/game/src/main/org/apollo/game/model/event/EventListener.java b/game/src/main/java/org/apollo/game/model/event/EventListener.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/event/EventListener.java
rename to game/src/main/java/org/apollo/game/model/event/EventListener.java
diff --git a/game/src/main/org/apollo/game/model/event/EventListenerChain.java b/game/src/main/java/org/apollo/game/model/event/EventListenerChain.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/event/EventListenerChain.java
rename to game/src/main/java/org/apollo/game/model/event/EventListenerChain.java
diff --git a/game/src/main/org/apollo/game/model/event/EventListenerChainSet.java b/game/src/main/java/org/apollo/game/model/event/EventListenerChainSet.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/event/EventListenerChainSet.java
rename to game/src/main/java/org/apollo/game/model/event/EventListenerChainSet.java
diff --git a/game/src/main/org/apollo/game/model/event/PlayerEvent.java b/game/src/main/java/org/apollo/game/model/event/PlayerEvent.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/event/PlayerEvent.java
rename to game/src/main/java/org/apollo/game/model/event/PlayerEvent.java
diff --git a/game/src/main/org/apollo/game/model/event/ProxyEvent.java b/game/src/main/java/org/apollo/game/model/event/ProxyEvent.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/event/ProxyEvent.java
rename to game/src/main/java/org/apollo/game/model/event/ProxyEvent.java
diff --git a/game/src/main/org/apollo/game/model/event/ProxyEventListener.java b/game/src/main/java/org/apollo/game/model/event/ProxyEventListener.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/event/ProxyEventListener.java
rename to game/src/main/java/org/apollo/game/model/event/ProxyEventListener.java
diff --git a/game/src/main/org/apollo/game/model/event/impl/CloseInterfacesEvent.java b/game/src/main/java/org/apollo/game/model/event/impl/CloseInterfacesEvent.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/event/impl/CloseInterfacesEvent.java
rename to game/src/main/java/org/apollo/game/model/event/impl/CloseInterfacesEvent.java
diff --git a/game/src/main/org/apollo/game/model/event/impl/LoginEvent.java b/game/src/main/java/org/apollo/game/model/event/impl/LoginEvent.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/event/impl/LoginEvent.java
rename to game/src/main/java/org/apollo/game/model/event/impl/LoginEvent.java
diff --git a/game/src/main/org/apollo/game/model/event/impl/LogoutEvent.java b/game/src/main/java/org/apollo/game/model/event/impl/LogoutEvent.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/event/impl/LogoutEvent.java
rename to game/src/main/java/org/apollo/game/model/event/impl/LogoutEvent.java
diff --git a/game/src/main/org/apollo/game/model/event/impl/MobPositionUpdateEvent.java b/game/src/main/java/org/apollo/game/model/event/impl/MobPositionUpdateEvent.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/event/impl/MobPositionUpdateEvent.java
rename to game/src/main/java/org/apollo/game/model/event/impl/MobPositionUpdateEvent.java
diff --git a/game/src/main/org/apollo/game/model/event/impl/package-info.java b/game/src/main/java/org/apollo/game/model/event/impl/package-info.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/event/impl/package-info.java
rename to game/src/main/java/org/apollo/game/model/event/impl/package-info.java
diff --git a/game/src/main/org/apollo/game/model/event/package-info.java b/game/src/main/java/org/apollo/game/model/event/package-info.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/event/package-info.java
rename to game/src/main/java/org/apollo/game/model/event/package-info.java
diff --git a/game/src/main/org/apollo/game/model/inter/EnterAmountListener.java b/game/src/main/java/org/apollo/game/model/inter/EnterAmountListener.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/inter/EnterAmountListener.java
rename to game/src/main/java/org/apollo/game/model/inter/EnterAmountListener.java
diff --git a/game/src/main/org/apollo/game/model/inter/InterfaceConstants.java b/game/src/main/java/org/apollo/game/model/inter/InterfaceConstants.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/inter/InterfaceConstants.java
rename to game/src/main/java/org/apollo/game/model/inter/InterfaceConstants.java
diff --git a/game/src/main/org/apollo/game/model/inter/InterfaceListener.java b/game/src/main/java/org/apollo/game/model/inter/InterfaceListener.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/inter/InterfaceListener.java
rename to game/src/main/java/org/apollo/game/model/inter/InterfaceListener.java
diff --git a/game/src/main/org/apollo/game/model/inter/InterfaceSet.java b/game/src/main/java/org/apollo/game/model/inter/InterfaceSet.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/inter/InterfaceSet.java
rename to game/src/main/java/org/apollo/game/model/inter/InterfaceSet.java
diff --git a/game/src/main/org/apollo/game/model/inter/InterfaceType.java b/game/src/main/java/org/apollo/game/model/inter/InterfaceType.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/inter/InterfaceType.java
rename to game/src/main/java/org/apollo/game/model/inter/InterfaceType.java
diff --git a/game/src/main/org/apollo/game/model/inter/bank/BankConstants.java b/game/src/main/java/org/apollo/game/model/inter/bank/BankConstants.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/inter/bank/BankConstants.java
rename to game/src/main/java/org/apollo/game/model/inter/bank/BankConstants.java
diff --git a/game/src/main/org/apollo/game/model/inter/bank/BankDepositEnterAmountListener.java b/game/src/main/java/org/apollo/game/model/inter/bank/BankDepositEnterAmountListener.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/inter/bank/BankDepositEnterAmountListener.java
rename to game/src/main/java/org/apollo/game/model/inter/bank/BankDepositEnterAmountListener.java
diff --git a/game/src/main/org/apollo/game/model/inter/bank/BankInterfaceListener.java b/game/src/main/java/org/apollo/game/model/inter/bank/BankInterfaceListener.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/inter/bank/BankInterfaceListener.java
rename to game/src/main/java/org/apollo/game/model/inter/bank/BankInterfaceListener.java
diff --git a/game/src/main/org/apollo/game/model/inter/bank/BankUtils.java b/game/src/main/java/org/apollo/game/model/inter/bank/BankUtils.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/inter/bank/BankUtils.java
rename to game/src/main/java/org/apollo/game/model/inter/bank/BankUtils.java
diff --git a/game/src/main/org/apollo/game/model/inter/bank/BankWithdrawEnterAmountListener.java b/game/src/main/java/org/apollo/game/model/inter/bank/BankWithdrawEnterAmountListener.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/inter/bank/BankWithdrawEnterAmountListener.java
rename to game/src/main/java/org/apollo/game/model/inter/bank/BankWithdrawEnterAmountListener.java
diff --git a/game/src/main/org/apollo/game/model/inter/bank/package-info.java b/game/src/main/java/org/apollo/game/model/inter/bank/package-info.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/inter/bank/package-info.java
rename to game/src/main/java/org/apollo/game/model/inter/bank/package-info.java
diff --git a/game/src/main/org/apollo/game/model/inter/dialogue/DialogueAdapter.java b/game/src/main/java/org/apollo/game/model/inter/dialogue/DialogueAdapter.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/inter/dialogue/DialogueAdapter.java
rename to game/src/main/java/org/apollo/game/model/inter/dialogue/DialogueAdapter.java
diff --git a/game/src/main/org/apollo/game/model/inter/dialogue/DialogueListener.java b/game/src/main/java/org/apollo/game/model/inter/dialogue/DialogueListener.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/inter/dialogue/DialogueListener.java
rename to game/src/main/java/org/apollo/game/model/inter/dialogue/DialogueListener.java
diff --git a/game/src/main/org/apollo/game/model/inter/dialogue/package-info.java b/game/src/main/java/org/apollo/game/model/inter/dialogue/package-info.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/inter/dialogue/package-info.java
rename to game/src/main/java/org/apollo/game/model/inter/dialogue/package-info.java
diff --git a/game/src/main/org/apollo/game/model/inter/package-info.java b/game/src/main/java/org/apollo/game/model/inter/package-info.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/inter/package-info.java
rename to game/src/main/java/org/apollo/game/model/inter/package-info.java
diff --git a/game/src/main/org/apollo/game/model/inv/AppearanceInventoryListener.java b/game/src/main/java/org/apollo/game/model/inv/AppearanceInventoryListener.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/inv/AppearanceInventoryListener.java
rename to game/src/main/java/org/apollo/game/model/inv/AppearanceInventoryListener.java
diff --git a/game/src/main/org/apollo/game/model/inv/FullInventoryListener.java b/game/src/main/java/org/apollo/game/model/inv/FullInventoryListener.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/inv/FullInventoryListener.java
rename to game/src/main/java/org/apollo/game/model/inv/FullInventoryListener.java
diff --git a/game/src/main/org/apollo/game/model/inv/Inventory.java b/game/src/main/java/org/apollo/game/model/inv/Inventory.java
similarity index 98%
rename from game/src/main/org/apollo/game/model/inv/Inventory.java
rename to game/src/main/java/org/apollo/game/model/inv/Inventory.java
index c4ff61769..ae8f081e5 100644
--- a/game/src/main/org/apollo/game/model/inv/Inventory.java
+++ b/game/src/main/java/org/apollo/game/model/inv/Inventory.java
@@ -1,15 +1,14 @@
package org.apollo.game.model.inv;
+import com.google.common.base.Preconditions;
+import org.apollo.cache.def.ItemDefinition;
+import org.apollo.game.model.Item;
+
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
-import org.apollo.cache.def.ItemDefinition;
-import org.apollo.game.model.Item;
-
-import com.google.common.base.Preconditions;
-
/**
* Represents an inventory - a collection of {@link Item}s.
*
@@ -518,6 +517,15 @@ public int remove(Item item) {
return remove(item.getId(), item.getAmount());
}
+ /**
+ * Remove all items with the given {@code id} and return the number of
+ * items removed.
+ *
+ * @param id The id of items to remove.
+ * @return The amount that was removed.
+ */
+ public int removeAll(int id) { return remove(id, getAmount(id)); }
+
/**
* Removes all the listeners.
*/
diff --git a/game/src/main/org/apollo/game/model/inv/InventoryAdapter.java b/game/src/main/java/org/apollo/game/model/inv/InventoryAdapter.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/inv/InventoryAdapter.java
rename to game/src/main/java/org/apollo/game/model/inv/InventoryAdapter.java
diff --git a/game/src/main/org/apollo/game/model/inv/InventoryConstants.java b/game/src/main/java/org/apollo/game/model/inv/InventoryConstants.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/inv/InventoryConstants.java
rename to game/src/main/java/org/apollo/game/model/inv/InventoryConstants.java
diff --git a/game/src/main/org/apollo/game/model/inv/InventoryListener.java b/game/src/main/java/org/apollo/game/model/inv/InventoryListener.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/inv/InventoryListener.java
rename to game/src/main/java/org/apollo/game/model/inv/InventoryListener.java
diff --git a/game/src/main/org/apollo/game/model/inv/SlottedItem.java b/game/src/main/java/org/apollo/game/model/inv/SlottedItem.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/inv/SlottedItem.java
rename to game/src/main/java/org/apollo/game/model/inv/SlottedItem.java
diff --git a/game/src/main/org/apollo/game/model/inv/SynchronizationInventoryListener.java b/game/src/main/java/org/apollo/game/model/inv/SynchronizationInventoryListener.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/inv/SynchronizationInventoryListener.java
rename to game/src/main/java/org/apollo/game/model/inv/SynchronizationInventoryListener.java
diff --git a/game/src/main/org/apollo/game/model/inv/package-info.java b/game/src/main/java/org/apollo/game/model/inv/package-info.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/inv/package-info.java
rename to game/src/main/java/org/apollo/game/model/inv/package-info.java
diff --git a/game/src/main/org/apollo/game/model/package-info.java b/game/src/main/java/org/apollo/game/model/package-info.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/package-info.java
rename to game/src/main/java/org/apollo/game/model/package-info.java
diff --git a/game/src/main/org/apollo/game/model/skill/LevelUpSkillListener.java b/game/src/main/java/org/apollo/game/model/skill/LevelUpSkillListener.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/skill/LevelUpSkillListener.java
rename to game/src/main/java/org/apollo/game/model/skill/LevelUpSkillListener.java
diff --git a/game/src/main/org/apollo/game/model/skill/SkillAdapter.java b/game/src/main/java/org/apollo/game/model/skill/SkillAdapter.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/skill/SkillAdapter.java
rename to game/src/main/java/org/apollo/game/model/skill/SkillAdapter.java
diff --git a/game/src/main/org/apollo/game/model/skill/SkillListener.java b/game/src/main/java/org/apollo/game/model/skill/SkillListener.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/skill/SkillListener.java
rename to game/src/main/java/org/apollo/game/model/skill/SkillListener.java
diff --git a/game/src/main/org/apollo/game/model/skill/SynchronizationSkillListener.java b/game/src/main/java/org/apollo/game/model/skill/SynchronizationSkillListener.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/skill/SynchronizationSkillListener.java
rename to game/src/main/java/org/apollo/game/model/skill/SynchronizationSkillListener.java
diff --git a/game/src/main/org/apollo/game/model/skill/package-info.java b/game/src/main/java/org/apollo/game/model/skill/package-info.java
similarity index 100%
rename from game/src/main/org/apollo/game/model/skill/package-info.java
rename to game/src/main/java/org/apollo/game/model/skill/package-info.java
diff --git a/game/src/main/org/apollo/game/package-info.java b/game/src/main/java/org/apollo/game/package-info.java
similarity index 100%
rename from game/src/main/org/apollo/game/package-info.java
rename to game/src/main/java/org/apollo/game/package-info.java
diff --git a/game/src/main/org/apollo/game/plugin/DependencyException.java b/game/src/main/java/org/apollo/game/plugin/DependencyException.java
similarity index 100%
rename from game/src/main/org/apollo/game/plugin/DependencyException.java
rename to game/src/main/java/org/apollo/game/plugin/DependencyException.java
diff --git a/game/src/main/java/org/apollo/game/plugin/KotlinPluginEnvironment.java b/game/src/main/java/org/apollo/game/plugin/KotlinPluginEnvironment.java
new file mode 100644
index 000000000..7e8d61fa7
--- /dev/null
+++ b/game/src/main/java/org/apollo/game/plugin/KotlinPluginEnvironment.java
@@ -0,0 +1,66 @@
+package org.apollo.game.plugin;
+
+import java.lang.reflect.Constructor;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.logging.Logger;
+
+import io.github.classgraph.ClassGraph;
+import io.github.classgraph.ClassInfo;
+import io.github.classgraph.ClassInfoList;
+import io.github.classgraph.ScanResult;
+import org.apollo.game.model.World;
+import org.apollo.game.plugin.kotlin.KotlinPluginScript;
+
+public class KotlinPluginEnvironment implements PluginEnvironment {
+
+ private static final Logger logger = Logger.getLogger(KotlinPluginEnvironment.class.getName());
+ private static final String PLUGIN_SUFFIX = "_plugin";
+
+ private final World world;
+ private PluginContext context;
+
+ public KotlinPluginEnvironment(World world) {
+ this.world = world;
+ }
+
+ @Override
+ public void load(Collection plugins) {
+ List pluginScripts = new ArrayList<>();
+
+ ClassGraph classGraph = new ClassGraph().enableAllInfo();
+
+ try (ScanResult scanResult = classGraph.scan()) {
+ ClassInfoList pluginClassList = scanResult
+ .getSubclasses(KotlinPluginScript.class.getName())
+ .directOnly();
+
+ for (ClassInfo pluginClassInfo : pluginClassList) {
+ Class scriptClass = pluginClassInfo.loadClass(KotlinPluginScript.class);
+ Constructor scriptConstructor = scriptClass.getConstructor(World.class,
+ PluginContext.class);
+
+ pluginScripts.add(scriptConstructor.newInstance(world, context));
+ logger.info(String.format("Loaded plugin: %s", pluginDescriptor(scriptClass)));
+ }
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+
+ pluginScripts.forEach(script -> script.doStart(world));
+ }
+
+ @Override
+ public void setContext(PluginContext context) {
+ this.context = context;
+ }
+
+ private static String pluginDescriptor(Class extends KotlinPluginScript> clazz) {
+ String className = clazz.getSimpleName();
+ String name = className.substring(0, className.length() - PLUGIN_SUFFIX.length());
+ Package pkg = clazz.getPackage();
+
+ return pkg == null ? name : name + " from " + pkg.getName();
+ }
+}
diff --git a/game/src/main/org/apollo/game/plugin/PluginContext.java b/game/src/main/java/org/apollo/game/plugin/PluginContext.java
similarity index 100%
rename from game/src/main/org/apollo/game/plugin/PluginContext.java
rename to game/src/main/java/org/apollo/game/plugin/PluginContext.java
diff --git a/game/src/main/org/apollo/game/plugin/PluginEnvironment.java b/game/src/main/java/org/apollo/game/plugin/PluginEnvironment.java
similarity index 62%
rename from game/src/main/org/apollo/game/plugin/PluginEnvironment.java
rename to game/src/main/java/org/apollo/game/plugin/PluginEnvironment.java
index f049f9748..505d8473e 100644
--- a/game/src/main/org/apollo/game/plugin/PluginEnvironment.java
+++ b/game/src/main/java/org/apollo/game/plugin/PluginEnvironment.java
@@ -1,6 +1,8 @@
package org.apollo.game.plugin;
import java.io.InputStream;
+import java.util.Collection;
+import java.util.Set;
/**
* Represents some sort of environment that plugins could be executed in, e.g. {@code javax.script} or Jython.
@@ -10,12 +12,11 @@
public interface PluginEnvironment {
/**
- * Parses the input stream.
+ * Load all of the plugins defined in the given {@link Set} of {@link PluginMetaData}.
*
- * @param is The input stream.
- * @param name The name of the file.
+ * @param plugins The plugins to be loaded.
*/
- public void parse(InputStream is, String name);
+ void load(Collection plugins);
/**
* Sets the context for this environment.
diff --git a/game/src/main/org/apollo/game/plugin/PluginManager.java b/game/src/main/java/org/apollo/game/plugin/PluginManager.java
similarity index 68%
rename from game/src/main/org/apollo/game/plugin/PluginManager.java
rename to game/src/main/java/org/apollo/game/plugin/PluginManager.java
index acf980d0c..5471cb697 100644
--- a/game/src/main/org/apollo/game/plugin/PluginManager.java
+++ b/game/src/main/java/org/apollo/game/plugin/PluginManager.java
@@ -4,14 +4,7 @@
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-import java.util.SortedSet;
-import java.util.TreeSet;
+import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -72,7 +65,7 @@ private static Map createMap(Collection
* @throws SAXException If a SAX error occurs.
*/
private Collection findPlugins() throws IOException, SAXException {
- return findPlugins(new File("./data/plugins"));
+ return findPlugins(new File("./game/data/plugins"));
}
/**
@@ -143,49 +136,13 @@ private void initAuthors() {
* @throws DependencyException If a dependency could not be resolved.
*/
public void start() throws IOException, SAXException, DependencyException {
- Map plugins = createMap(findPlugins());
- Set started = new HashSet<>();
+ //@todo - load metadata and respective plugins
+ Map plugins = new HashMap<>();
- PluginEnvironment env = new RubyPluginEnvironment(world); // TODO isolate plugins if possible in the future!
+ PluginEnvironment env = new KotlinPluginEnvironment(world); // TODO isolate plugins if possible in the future!
env.setContext(context);
- for (PluginMetaData plugin : plugins.values()) {
- start(env, plugin, plugins, started);
- }
- }
-
- /**
- * Starts a specific plugin.
- *
- * @param env The environment.
- * @param plugin The plugin.
- * @param plugins The plugin map.
- * @param started A set of started plugins.
- * @throws DependencyException If a dependency error occurs.
- * @throws IOException If an I/O error occurs.
- */
- private void start(PluginEnvironment env, PluginMetaData plugin, Map plugins, Set started) throws DependencyException, IOException {
- // TODO check for cyclic dependencies! this way just won't cut it, we need an exception
- if (started.contains(plugin)) {
- return;
- }
- started.add(plugin);
-
- for (String dependencyId : plugin.getDependencies()) {
- PluginMetaData dependency = plugins.get(dependencyId);
- if (dependency == null) {
- throw new DependencyException("Unresolved dependency: " + dependencyId + ".");
- }
- start(env, dependency, plugins, started);
- }
-
- String[] scripts = plugin.getScripts();
-
- for (String script : scripts) {
- File scriptFile = new File(plugin.getBase(), script);
- InputStream is = new FileInputStream(scriptFile);
- env.parse(is, scriptFile.getAbsolutePath());
- }
+ env.load(plugins.values());
}
}
\ No newline at end of file
diff --git a/game/src/main/org/apollo/game/plugin/PluginMetaData.java b/game/src/main/java/org/apollo/game/plugin/PluginMetaData.java
similarity index 100%
rename from game/src/main/org/apollo/game/plugin/PluginMetaData.java
rename to game/src/main/java/org/apollo/game/plugin/PluginMetaData.java
diff --git a/game/src/main/org/apollo/game/plugin/package-info.java b/game/src/main/java/org/apollo/game/plugin/package-info.java
similarity index 100%
rename from game/src/main/org/apollo/game/plugin/package-info.java
rename to game/src/main/java/org/apollo/game/plugin/package-info.java
diff --git a/game/src/main/org/apollo/game/release/r317/AddFriendMessageDecoder.java b/game/src/main/java/org/apollo/game/release/r317/AddFriendMessageDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/AddFriendMessageDecoder.java
rename to game/src/main/java/org/apollo/game/release/r317/AddFriendMessageDecoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/AddGlobalTileItemMessageEncoder.java b/game/src/main/java/org/apollo/game/release/r317/AddGlobalTileItemMessageEncoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/AddGlobalTileItemMessageEncoder.java
rename to game/src/main/java/org/apollo/game/release/r317/AddGlobalTileItemMessageEncoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/AddIgnoreMessageDecoder.java b/game/src/main/java/org/apollo/game/release/r317/AddIgnoreMessageDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/AddIgnoreMessageDecoder.java
rename to game/src/main/java/org/apollo/game/release/r317/AddIgnoreMessageDecoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/AddTileItemMessageEncoder.java b/game/src/main/java/org/apollo/game/release/r317/AddTileItemMessageEncoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/AddTileItemMessageEncoder.java
rename to game/src/main/java/org/apollo/game/release/r317/AddTileItemMessageEncoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/ArrowKeyMessageDecoder.java b/game/src/main/java/org/apollo/game/release/r317/ArrowKeyMessageDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/ArrowKeyMessageDecoder.java
rename to game/src/main/java/org/apollo/game/release/r317/ArrowKeyMessageDecoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/ButtonMessageDecoder.java b/game/src/main/java/org/apollo/game/release/r317/ButtonMessageDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/ButtonMessageDecoder.java
rename to game/src/main/java/org/apollo/game/release/r317/ButtonMessageDecoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/ClearRegionMessageEncoder.java b/game/src/main/java/org/apollo/game/release/r317/ClearRegionMessageEncoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/ClearRegionMessageEncoder.java
rename to game/src/main/java/org/apollo/game/release/r317/ClearRegionMessageEncoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/CloseInterfaceMessageEncoder.java b/game/src/main/java/org/apollo/game/release/r317/CloseInterfaceMessageEncoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/CloseInterfaceMessageEncoder.java
rename to game/src/main/java/org/apollo/game/release/r317/CloseInterfaceMessageEncoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/ClosedInterfaceMessageDecoder.java b/game/src/main/java/org/apollo/game/release/r317/ClosedInterfaceMessageDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/ClosedInterfaceMessageDecoder.java
rename to game/src/main/java/org/apollo/game/release/r317/ClosedInterfaceMessageDecoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/CommandMessageDecoder.java b/game/src/main/java/org/apollo/game/release/r317/CommandMessageDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/CommandMessageDecoder.java
rename to game/src/main/java/org/apollo/game/release/r317/CommandMessageDecoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/ConfigMessageEncoder.java b/game/src/main/java/org/apollo/game/release/r317/ConfigMessageEncoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/ConfigMessageEncoder.java
rename to game/src/main/java/org/apollo/game/release/r317/ConfigMessageEncoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/DialogueContinueMessageDecoder.java b/game/src/main/java/org/apollo/game/release/r317/DialogueContinueMessageDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/DialogueContinueMessageDecoder.java
rename to game/src/main/java/org/apollo/game/release/r317/DialogueContinueMessageDecoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/DisplayCrossbonesMessageEncoder.java b/game/src/main/java/org/apollo/game/release/r317/DisplayCrossbonesMessageEncoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/DisplayCrossbonesMessageEncoder.java
rename to game/src/main/java/org/apollo/game/release/r317/DisplayCrossbonesMessageEncoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/DisplayTabInterfaceMessageEncoder.java b/game/src/main/java/org/apollo/game/release/r317/DisplayTabInterfaceMessageEncoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/DisplayTabInterfaceMessageEncoder.java
rename to game/src/main/java/org/apollo/game/release/r317/DisplayTabInterfaceMessageEncoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/EnterAmountMessageEncoder.java b/game/src/main/java/org/apollo/game/release/r317/EnterAmountMessageEncoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/EnterAmountMessageEncoder.java
rename to game/src/main/java/org/apollo/game/release/r317/EnterAmountMessageEncoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/EnteredAmountMessageDecoder.java b/game/src/main/java/org/apollo/game/release/r317/EnteredAmountMessageDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/EnteredAmountMessageDecoder.java
rename to game/src/main/java/org/apollo/game/release/r317/EnteredAmountMessageDecoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/FifthItemActionMessageDecoder.java b/game/src/main/java/org/apollo/game/release/r317/FifthItemActionMessageDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/FifthItemActionMessageDecoder.java
rename to game/src/main/java/org/apollo/game/release/r317/FifthItemActionMessageDecoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/FifthItemOptionMessageDecoder.java b/game/src/main/java/org/apollo/game/release/r317/FifthItemOptionMessageDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/FifthItemOptionMessageDecoder.java
rename to game/src/main/java/org/apollo/game/release/r317/FifthItemOptionMessageDecoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/FifthNpcActionMessageDecoder.java b/game/src/main/java/org/apollo/game/release/r317/FifthNpcActionMessageDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/FifthNpcActionMessageDecoder.java
rename to game/src/main/java/org/apollo/game/release/r317/FifthNpcActionMessageDecoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/FifthPlayerActionMessageDecoder.java b/game/src/main/java/org/apollo/game/release/r317/FifthPlayerActionMessageDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/FifthPlayerActionMessageDecoder.java
rename to game/src/main/java/org/apollo/game/release/r317/FifthPlayerActionMessageDecoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/FirstItemActionMessageDecoder.java b/game/src/main/java/org/apollo/game/release/r317/FirstItemActionMessageDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/FirstItemActionMessageDecoder.java
rename to game/src/main/java/org/apollo/game/release/r317/FirstItemActionMessageDecoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/FirstItemOptionMessageDecoder.java b/game/src/main/java/org/apollo/game/release/r317/FirstItemOptionMessageDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/FirstItemOptionMessageDecoder.java
rename to game/src/main/java/org/apollo/game/release/r317/FirstItemOptionMessageDecoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/FirstNpcActionMessageDecoder.java b/game/src/main/java/org/apollo/game/release/r317/FirstNpcActionMessageDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/FirstNpcActionMessageDecoder.java
rename to game/src/main/java/org/apollo/game/release/r317/FirstNpcActionMessageDecoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/FirstObjectActionMessageDecoder.java b/game/src/main/java/org/apollo/game/release/r317/FirstObjectActionMessageDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/FirstObjectActionMessageDecoder.java
rename to game/src/main/java/org/apollo/game/release/r317/FirstObjectActionMessageDecoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/FirstPlayerActionMessageDecoder.java b/game/src/main/java/org/apollo/game/release/r317/FirstPlayerActionMessageDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/FirstPlayerActionMessageDecoder.java
rename to game/src/main/java/org/apollo/game/release/r317/FirstPlayerActionMessageDecoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/FlaggedMouseEventMessageDecoder.java b/game/src/main/java/org/apollo/game/release/r317/FlaggedMouseEventMessageDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/FlaggedMouseEventMessageDecoder.java
rename to game/src/main/java/org/apollo/game/release/r317/FlaggedMouseEventMessageDecoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/FlashTabInterfaceMessageEncoder.java b/game/src/main/java/org/apollo/game/release/r317/FlashTabInterfaceMessageEncoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/FlashTabInterfaceMessageEncoder.java
rename to game/src/main/java/org/apollo/game/release/r317/FlashTabInterfaceMessageEncoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/FlashingTabClickedMessageDecoder.java b/game/src/main/java/org/apollo/game/release/r317/FlashingTabClickedMessageDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/FlashingTabClickedMessageDecoder.java
rename to game/src/main/java/org/apollo/game/release/r317/FlashingTabClickedMessageDecoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/FocusUpdateMessageDecoder.java b/game/src/main/java/org/apollo/game/release/r317/FocusUpdateMessageDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/FocusUpdateMessageDecoder.java
rename to game/src/main/java/org/apollo/game/release/r317/FocusUpdateMessageDecoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/ForwardPrivateChatMessageEncoder.java b/game/src/main/java/org/apollo/game/release/r317/ForwardPrivateChatMessageEncoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/ForwardPrivateChatMessageEncoder.java
rename to game/src/main/java/org/apollo/game/release/r317/ForwardPrivateChatMessageEncoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/FourthItemActionMessageDecoder.java b/game/src/main/java/org/apollo/game/release/r317/FourthItemActionMessageDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/FourthItemActionMessageDecoder.java
rename to game/src/main/java/org/apollo/game/release/r317/FourthItemActionMessageDecoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/FourthItemOptionMessageDecoder.java b/game/src/main/java/org/apollo/game/release/r317/FourthItemOptionMessageDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/FourthItemOptionMessageDecoder.java
rename to game/src/main/java/org/apollo/game/release/r317/FourthItemOptionMessageDecoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/FourthNpcActionMessageDecoder.java b/game/src/main/java/org/apollo/game/release/r317/FourthNpcActionMessageDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/FourthNpcActionMessageDecoder.java
rename to game/src/main/java/org/apollo/game/release/r317/FourthNpcActionMessageDecoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/FourthPlayerActionMessageDecoder.java b/game/src/main/java/org/apollo/game/release/r317/FourthPlayerActionMessageDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/FourthPlayerActionMessageDecoder.java
rename to game/src/main/java/org/apollo/game/release/r317/FourthPlayerActionMessageDecoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/FriendServerStatusMessageEncoder.java b/game/src/main/java/org/apollo/game/release/r317/FriendServerStatusMessageEncoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/FriendServerStatusMessageEncoder.java
rename to game/src/main/java/org/apollo/game/release/r317/FriendServerStatusMessageEncoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/GroupedRegionUpdateMessageEncoder.java b/game/src/main/java/org/apollo/game/release/r317/GroupedRegionUpdateMessageEncoder.java
similarity index 97%
rename from game/src/main/org/apollo/game/release/r317/GroupedRegionUpdateMessageEncoder.java
rename to game/src/main/java/org/apollo/game/release/r317/GroupedRegionUpdateMessageEncoder.java
index 80fa508fd..cd6703b77 100644
--- a/game/src/main/org/apollo/game/release/r317/GroupedRegionUpdateMessageEncoder.java
+++ b/game/src/main/java/org/apollo/game/release/r317/GroupedRegionUpdateMessageEncoder.java
@@ -47,7 +47,7 @@ public GamePacket encode(GroupedRegionUpdateMessage message) {
GamePacket packet = encoder.encode(update);
builder.put(DataType.BYTE, packet.getOpcode());
- builder.putBytes(packet.getPayload());
+ builder.putBytes(packet.content());
}
return builder.toGamePacket();
diff --git a/game/src/main/org/apollo/game/release/r317/IdAssignmentMessageEncoder.java b/game/src/main/java/org/apollo/game/release/r317/IdAssignmentMessageEncoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/IdAssignmentMessageEncoder.java
rename to game/src/main/java/org/apollo/game/release/r317/IdAssignmentMessageEncoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/IgnoreListMessageEncoder.java b/game/src/main/java/org/apollo/game/release/r317/IgnoreListMessageEncoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/IgnoreListMessageEncoder.java
rename to game/src/main/java/org/apollo/game/release/r317/IgnoreListMessageEncoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/ItemOnItemMessageDecoder.java b/game/src/main/java/org/apollo/game/release/r317/ItemOnItemMessageDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/ItemOnItemMessageDecoder.java
rename to game/src/main/java/org/apollo/game/release/r317/ItemOnItemMessageDecoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/ItemOnNpcMessageDecoder.java b/game/src/main/java/org/apollo/game/release/r317/ItemOnNpcMessageDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/ItemOnNpcMessageDecoder.java
rename to game/src/main/java/org/apollo/game/release/r317/ItemOnNpcMessageDecoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/ItemOnObjectMessageDecoder.java b/game/src/main/java/org/apollo/game/release/r317/ItemOnObjectMessageDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/ItemOnObjectMessageDecoder.java
rename to game/src/main/java/org/apollo/game/release/r317/ItemOnObjectMessageDecoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/KeepAliveMessageDecoder.java b/game/src/main/java/org/apollo/game/release/r317/KeepAliveMessageDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/KeepAliveMessageDecoder.java
rename to game/src/main/java/org/apollo/game/release/r317/KeepAliveMessageDecoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/LogoutMessageEncoder.java b/game/src/main/java/org/apollo/game/release/r317/LogoutMessageEncoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/LogoutMessageEncoder.java
rename to game/src/main/java/org/apollo/game/release/r317/LogoutMessageEncoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/MagicOnItemMessageDecoder.java b/game/src/main/java/org/apollo/game/release/r317/MagicOnItemMessageDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/MagicOnItemMessageDecoder.java
rename to game/src/main/java/org/apollo/game/release/r317/MagicOnItemMessageDecoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/MagicOnNpcMessageDecoder.java b/game/src/main/java/org/apollo/game/release/r317/MagicOnNpcMessageDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/MagicOnNpcMessageDecoder.java
rename to game/src/main/java/org/apollo/game/release/r317/MagicOnNpcMessageDecoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/MagicOnPlayerMessageDecoder.java b/game/src/main/java/org/apollo/game/release/r317/MagicOnPlayerMessageDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/MagicOnPlayerMessageDecoder.java
rename to game/src/main/java/org/apollo/game/release/r317/MagicOnPlayerMessageDecoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/MobAnimationResetMessageEncoder.java b/game/src/main/java/org/apollo/game/release/r317/MobAnimationResetMessageEncoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/MobAnimationResetMessageEncoder.java
rename to game/src/main/java/org/apollo/game/release/r317/MobAnimationResetMessageEncoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/MobHintIconMessageEncoder.java b/game/src/main/java/org/apollo/game/release/r317/MobHintIconMessageEncoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/MobHintIconMessageEncoder.java
rename to game/src/main/java/org/apollo/game/release/r317/MobHintIconMessageEncoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/MouseClickedMessageDecoder.java b/game/src/main/java/org/apollo/game/release/r317/MouseClickedMessageDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/MouseClickedMessageDecoder.java
rename to game/src/main/java/org/apollo/game/release/r317/MouseClickedMessageDecoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/NpcSynchronizationMessageEncoder.java b/game/src/main/java/org/apollo/game/release/r317/NpcSynchronizationMessageEncoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/NpcSynchronizationMessageEncoder.java
rename to game/src/main/java/org/apollo/game/release/r317/NpcSynchronizationMessageEncoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/OpenDialogueInterfaceMessageEncoder.java b/game/src/main/java/org/apollo/game/release/r317/OpenDialogueInterfaceMessageEncoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/OpenDialogueInterfaceMessageEncoder.java
rename to game/src/main/java/org/apollo/game/release/r317/OpenDialogueInterfaceMessageEncoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/OpenDialogueOverlayMessageEncoder.java b/game/src/main/java/org/apollo/game/release/r317/OpenDialogueOverlayMessageEncoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/OpenDialogueOverlayMessageEncoder.java
rename to game/src/main/java/org/apollo/game/release/r317/OpenDialogueOverlayMessageEncoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/OpenInterfaceMessageEncoder.java b/game/src/main/java/org/apollo/game/release/r317/OpenInterfaceMessageEncoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/OpenInterfaceMessageEncoder.java
rename to game/src/main/java/org/apollo/game/release/r317/OpenInterfaceMessageEncoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/OpenInterfaceSidebarMessageEncoder.java b/game/src/main/java/org/apollo/game/release/r317/OpenInterfaceSidebarMessageEncoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/OpenInterfaceSidebarMessageEncoder.java
rename to game/src/main/java/org/apollo/game/release/r317/OpenInterfaceSidebarMessageEncoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/OpenOverlayMessageEncoder.java b/game/src/main/java/org/apollo/game/release/r317/OpenOverlayMessageEncoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/OpenOverlayMessageEncoder.java
rename to game/src/main/java/org/apollo/game/release/r317/OpenOverlayMessageEncoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/OpenSidebarMessageEncoder.java b/game/src/main/java/org/apollo/game/release/r317/OpenSidebarMessageEncoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/OpenSidebarMessageEncoder.java
rename to game/src/main/java/org/apollo/game/release/r317/OpenSidebarMessageEncoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/PlayerDesignMessageDecoder.java b/game/src/main/java/org/apollo/game/release/r317/PlayerDesignMessageDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/PlayerDesignMessageDecoder.java
rename to game/src/main/java/org/apollo/game/release/r317/PlayerDesignMessageDecoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/PlayerSynchronizationMessageEncoder.java b/game/src/main/java/org/apollo/game/release/r317/PlayerSynchronizationMessageEncoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/PlayerSynchronizationMessageEncoder.java
rename to game/src/main/java/org/apollo/game/release/r317/PlayerSynchronizationMessageEncoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/PositionHintIconMessageEncoder.java b/game/src/main/java/org/apollo/game/release/r317/PositionHintIconMessageEncoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/PositionHintIconMessageEncoder.java
rename to game/src/main/java/org/apollo/game/release/r317/PositionHintIconMessageEncoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/PrivacyOptionMessageDecoder.java b/game/src/main/java/org/apollo/game/release/r317/PrivacyOptionMessageDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/PrivacyOptionMessageDecoder.java
rename to game/src/main/java/org/apollo/game/release/r317/PrivacyOptionMessageDecoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/PrivacyOptionMessageEncoder.java b/game/src/main/java/org/apollo/game/release/r317/PrivacyOptionMessageEncoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/PrivacyOptionMessageEncoder.java
rename to game/src/main/java/org/apollo/game/release/r317/PrivacyOptionMessageEncoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/PrivateChatMessageDecoder.java b/game/src/main/java/org/apollo/game/release/r317/PrivateChatMessageDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/PrivateChatMessageDecoder.java
rename to game/src/main/java/org/apollo/game/release/r317/PrivateChatMessageDecoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/PublicChatMessageDecoder.java b/game/src/main/java/org/apollo/game/release/r317/PublicChatMessageDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/PublicChatMessageDecoder.java
rename to game/src/main/java/org/apollo/game/release/r317/PublicChatMessageDecoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/RegionChangeMessageEncoder.java b/game/src/main/java/org/apollo/game/release/r317/RegionChangeMessageEncoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/RegionChangeMessageEncoder.java
rename to game/src/main/java/org/apollo/game/release/r317/RegionChangeMessageEncoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/Release317.java b/game/src/main/java/org/apollo/game/release/r317/Release317.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/Release317.java
rename to game/src/main/java/org/apollo/game/release/r317/Release317.java
diff --git a/game/src/main/org/apollo/game/release/r317/RemoveFriendMessageDecoder.java b/game/src/main/java/org/apollo/game/release/r317/RemoveFriendMessageDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/RemoveFriendMessageDecoder.java
rename to game/src/main/java/org/apollo/game/release/r317/RemoveFriendMessageDecoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/RemoveIgnoreMessageDecoder.java b/game/src/main/java/org/apollo/game/release/r317/RemoveIgnoreMessageDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/RemoveIgnoreMessageDecoder.java
rename to game/src/main/java/org/apollo/game/release/r317/RemoveIgnoreMessageDecoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/RemoveObjectMessageEncoder.java b/game/src/main/java/org/apollo/game/release/r317/RemoveObjectMessageEncoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/RemoveObjectMessageEncoder.java
rename to game/src/main/java/org/apollo/game/release/r317/RemoveObjectMessageEncoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/RemoveTileItemMessageEncoder.java b/game/src/main/java/org/apollo/game/release/r317/RemoveTileItemMessageEncoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/RemoveTileItemMessageEncoder.java
rename to game/src/main/java/org/apollo/game/release/r317/RemoveTileItemMessageEncoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/ReportAbuseMessageDecoder.java b/game/src/main/java/org/apollo/game/release/r317/ReportAbuseMessageDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/ReportAbuseMessageDecoder.java
rename to game/src/main/java/org/apollo/game/release/r317/ReportAbuseMessageDecoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/SecondItemActionMessageDecoder.java b/game/src/main/java/org/apollo/game/release/r317/SecondItemActionMessageDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/SecondItemActionMessageDecoder.java
rename to game/src/main/java/org/apollo/game/release/r317/SecondItemActionMessageDecoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/SecondItemOptionMessageDecoder.java b/game/src/main/java/org/apollo/game/release/r317/SecondItemOptionMessageDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/SecondItemOptionMessageDecoder.java
rename to game/src/main/java/org/apollo/game/release/r317/SecondItemOptionMessageDecoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/SecondNpcActionMessageDecoder.java b/game/src/main/java/org/apollo/game/release/r317/SecondNpcActionMessageDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/SecondNpcActionMessageDecoder.java
rename to game/src/main/java/org/apollo/game/release/r317/SecondNpcActionMessageDecoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/SecondObjectActionMessageDecoder.java b/game/src/main/java/org/apollo/game/release/r317/SecondObjectActionMessageDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/SecondObjectActionMessageDecoder.java
rename to game/src/main/java/org/apollo/game/release/r317/SecondObjectActionMessageDecoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/SecondPlayerActionMessageDecoder.java b/game/src/main/java/org/apollo/game/release/r317/SecondPlayerActionMessageDecoder.java
similarity index 100%
rename from game/src/main/org/apollo/game/release/r317/SecondPlayerActionMessageDecoder.java
rename to game/src/main/java/org/apollo/game/release/r317/SecondPlayerActionMessageDecoder.java
diff --git a/game/src/main/org/apollo/game/release/r317/SendFriendMessageEncoder.java b/game/src/main/java/org/apollo/game/release/r317/SendFriendMessageEncoder.java
similarity index 92%
rename from game/src/main/org/apollo/game/release/r317/SendFriendMessageEncoder.java
rename to game/src/main/java/org/apollo/game/release/r317/SendFriendMessageEncoder.java
index 15660fcbd..932dca8ef 100644
--- a/game/src/main/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 attribute = channel.attr(ApolloHandler.SESSION_KEY);
- Session session = attribute.get();
+ Channel channel = ctx.channel();
+ Attribute attribute = channel.attr(ApolloHandler.SESSION_KEY);
+ Session session = attribute.get();
- if (message instanceof HttpRequest || message instanceof JagGrabRequest) {
- session = new UpdateSession(channel, serverContext);
- }
+ if (message instanceof HttpRequest || message instanceof JagGrabRequest) {
+ session = new UpdateSession(channel, serverContext);
+ }
- if (session != null) {
- session.messageReceived(message);
- return;
- }
+ if (session != null) {
+ session.messageReceived(message);
+ return;
+ }
- // TODO: Perhaps let HandshakeMessage implement Message to remove this explicit check
- if (message instanceof HandshakeMessage) {
- HandshakeMessage handshakeMessage = (HandshakeMessage) message;
+ // TODO: Perhaps let HandshakeMessage implement Message to remove this explicit check
+ if (message instanceof HandshakeMessage) {
+ HandshakeMessage handshakeMessage = (HandshakeMessage) message;
- switch (handshakeMessage.getServiceId()) {
- case HandshakeConstants.SERVICE_GAME:
- attribute.set(new LoginSession(channel, serverContext));
- break;
+ switch (handshakeMessage.getServiceId()) {
+ case HandshakeConstants.SERVICE_GAME:
+ attribute.set(new LoginSession(channel, serverContext));
+ break;
- case HandshakeConstants.SERVICE_UPDATE:
- attribute.set(new UpdateSession(channel, serverContext));
- break;
- }
+ case HandshakeConstants.SERVICE_UPDATE:
+ attribute.set(new UpdateSession(channel, serverContext));
+ break;
}
-
- } finally {
- ReferenceCountUtil.release(message);
}
}
diff --git a/game/src/main/org/apollo/game/session/GameSession.java b/game/src/main/java/org/apollo/game/session/GameSession.java
similarity index 100%
rename from game/src/main/org/apollo/game/session/GameSession.java
rename to game/src/main/java/org/apollo/game/session/GameSession.java
diff --git a/game/src/main/org/apollo/game/session/LoginSession.java b/game/src/main/java/org/apollo/game/session/LoginSession.java
similarity index 100%
rename from game/src/main/org/apollo/game/session/LoginSession.java
rename to game/src/main/java/org/apollo/game/session/LoginSession.java
diff --git a/game/src/main/org/apollo/game/session/Session.java b/game/src/main/java/org/apollo/game/session/Session.java
similarity index 100%
rename from game/src/main/org/apollo/game/session/Session.java
rename to game/src/main/java/org/apollo/game/session/Session.java
diff --git a/game/src/main/org/apollo/game/session/UpdateSession.java b/game/src/main/java/org/apollo/game/session/UpdateSession.java
similarity index 100%
rename from game/src/main/org/apollo/game/session/UpdateSession.java
rename to game/src/main/java/org/apollo/game/session/UpdateSession.java
diff --git a/game/src/main/org/apollo/game/session/package-info.java b/game/src/main/java/org/apollo/game/session/package-info.java
similarity index 100%
rename from game/src/main/org/apollo/game/session/package-info.java
rename to game/src/main/java/org/apollo/game/session/package-info.java
diff --git a/game/src/main/org/apollo/game/sync/ClientSynchronizer.java b/game/src/main/java/org/apollo/game/sync/ClientSynchronizer.java
similarity index 100%
rename from game/src/main/org/apollo/game/sync/ClientSynchronizer.java
rename to game/src/main/java/org/apollo/game/sync/ClientSynchronizer.java
diff --git a/game/src/main/org/apollo/game/sync/ParallelClientSynchronizer.java b/game/src/main/java/org/apollo/game/sync/ParallelClientSynchronizer.java
similarity index 100%
rename from game/src/main/org/apollo/game/sync/ParallelClientSynchronizer.java
rename to game/src/main/java/org/apollo/game/sync/ParallelClientSynchronizer.java
diff --git a/game/src/main/org/apollo/game/sync/SequentialClientSynchronizer.java b/game/src/main/java/org/apollo/game/sync/SequentialClientSynchronizer.java
similarity index 100%
rename from game/src/main/org/apollo/game/sync/SequentialClientSynchronizer.java
rename to game/src/main/java/org/apollo/game/sync/SequentialClientSynchronizer.java
diff --git a/game/src/main/org/apollo/game/sync/block/AnimationBlock.java b/game/src/main/java/org/apollo/game/sync/block/AnimationBlock.java
similarity index 100%
rename from game/src/main/org/apollo/game/sync/block/AnimationBlock.java
rename to game/src/main/java/org/apollo/game/sync/block/AnimationBlock.java
diff --git a/game/src/main/org/apollo/game/sync/block/AppearanceBlock.java b/game/src/main/java/org/apollo/game/sync/block/AppearanceBlock.java
similarity index 100%
rename from game/src/main/org/apollo/game/sync/block/AppearanceBlock.java
rename to game/src/main/java/org/apollo/game/sync/block/AppearanceBlock.java
diff --git a/game/src/main/org/apollo/game/sync/block/ChatBlock.java b/game/src/main/java/org/apollo/game/sync/block/ChatBlock.java
similarity index 100%
rename from game/src/main/org/apollo/game/sync/block/ChatBlock.java
rename to game/src/main/java/org/apollo/game/sync/block/ChatBlock.java
diff --git a/game/src/main/org/apollo/game/sync/block/ForceChatBlock.java b/game/src/main/java/org/apollo/game/sync/block/ForceChatBlock.java
similarity index 100%
rename from game/src/main/org/apollo/game/sync/block/ForceChatBlock.java
rename to game/src/main/java/org/apollo/game/sync/block/ForceChatBlock.java
diff --git a/game/src/main/org/apollo/game/sync/block/ForceMovementBlock.java b/game/src/main/java/org/apollo/game/sync/block/ForceMovementBlock.java
similarity index 100%
rename from game/src/main/org/apollo/game/sync/block/ForceMovementBlock.java
rename to game/src/main/java/org/apollo/game/sync/block/ForceMovementBlock.java
diff --git a/game/src/main/org/apollo/game/sync/block/GraphicBlock.java b/game/src/main/java/org/apollo/game/sync/block/GraphicBlock.java
similarity index 100%
rename from game/src/main/org/apollo/game/sync/block/GraphicBlock.java
rename to game/src/main/java/org/apollo/game/sync/block/GraphicBlock.java
diff --git a/game/src/main/org/apollo/game/sync/block/HitUpdateBlock.java b/game/src/main/java/org/apollo/game/sync/block/HitUpdateBlock.java
similarity index 100%
rename from game/src/main/org/apollo/game/sync/block/HitUpdateBlock.java
rename to game/src/main/java/org/apollo/game/sync/block/HitUpdateBlock.java
diff --git a/game/src/main/org/apollo/game/sync/block/InteractingMobBlock.java b/game/src/main/java/org/apollo/game/sync/block/InteractingMobBlock.java
similarity index 100%
rename from game/src/main/org/apollo/game/sync/block/InteractingMobBlock.java
rename to game/src/main/java/org/apollo/game/sync/block/InteractingMobBlock.java
diff --git a/game/src/main/org/apollo/game/sync/block/SecondaryHitUpdateBlock.java b/game/src/main/java/org/apollo/game/sync/block/SecondaryHitUpdateBlock.java
similarity index 100%
rename from game/src/main/org/apollo/game/sync/block/SecondaryHitUpdateBlock.java
rename to game/src/main/java/org/apollo/game/sync/block/SecondaryHitUpdateBlock.java
diff --git a/game/src/main/org/apollo/game/sync/block/SynchronizationBlock.java b/game/src/main/java/org/apollo/game/sync/block/SynchronizationBlock.java
similarity index 100%
rename from game/src/main/org/apollo/game/sync/block/SynchronizationBlock.java
rename to game/src/main/java/org/apollo/game/sync/block/SynchronizationBlock.java
diff --git a/game/src/main/org/apollo/game/sync/block/SynchronizationBlockSet.java b/game/src/main/java/org/apollo/game/sync/block/SynchronizationBlockSet.java
similarity index 100%
rename from game/src/main/org/apollo/game/sync/block/SynchronizationBlockSet.java
rename to game/src/main/java/org/apollo/game/sync/block/SynchronizationBlockSet.java
diff --git a/game/src/main/org/apollo/game/sync/block/TransformBlock.java b/game/src/main/java/org/apollo/game/sync/block/TransformBlock.java
similarity index 100%
rename from game/src/main/org/apollo/game/sync/block/TransformBlock.java
rename to game/src/main/java/org/apollo/game/sync/block/TransformBlock.java
diff --git a/game/src/main/org/apollo/game/sync/block/TurnToPositionBlock.java b/game/src/main/java/org/apollo/game/sync/block/TurnToPositionBlock.java
similarity index 100%
rename from game/src/main/org/apollo/game/sync/block/TurnToPositionBlock.java
rename to game/src/main/java/org/apollo/game/sync/block/TurnToPositionBlock.java
diff --git a/game/src/main/org/apollo/game/sync/block/package-info.java b/game/src/main/java/org/apollo/game/sync/block/package-info.java
similarity index 100%
rename from game/src/main/org/apollo/game/sync/block/package-info.java
rename to game/src/main/java/org/apollo/game/sync/block/package-info.java
diff --git a/game/src/main/org/apollo/game/sync/package-info.java b/game/src/main/java/org/apollo/game/sync/package-info.java
similarity index 100%
rename from game/src/main/org/apollo/game/sync/package-info.java
rename to game/src/main/java/org/apollo/game/sync/package-info.java
diff --git a/game/src/main/org/apollo/game/sync/seg/AddNpcSegment.java b/game/src/main/java/org/apollo/game/sync/seg/AddNpcSegment.java
similarity index 100%
rename from game/src/main/org/apollo/game/sync/seg/AddNpcSegment.java
rename to game/src/main/java/org/apollo/game/sync/seg/AddNpcSegment.java
diff --git a/game/src/main/org/apollo/game/sync/seg/AddPlayerSegment.java b/game/src/main/java/org/apollo/game/sync/seg/AddPlayerSegment.java
similarity index 100%
rename from game/src/main/org/apollo/game/sync/seg/AddPlayerSegment.java
rename to game/src/main/java/org/apollo/game/sync/seg/AddPlayerSegment.java
diff --git a/game/src/main/org/apollo/game/sync/seg/MovementSegment.java b/game/src/main/java/org/apollo/game/sync/seg/MovementSegment.java
similarity index 100%
rename from game/src/main/org/apollo/game/sync/seg/MovementSegment.java
rename to game/src/main/java/org/apollo/game/sync/seg/MovementSegment.java
diff --git a/game/src/main/org/apollo/game/sync/seg/RemoveMobSegment.java b/game/src/main/java/org/apollo/game/sync/seg/RemoveMobSegment.java
similarity index 100%
rename from game/src/main/org/apollo/game/sync/seg/RemoveMobSegment.java
rename to game/src/main/java/org/apollo/game/sync/seg/RemoveMobSegment.java
diff --git a/game/src/main/org/apollo/game/sync/seg/SegmentType.java b/game/src/main/java/org/apollo/game/sync/seg/SegmentType.java
similarity index 100%
rename from game/src/main/org/apollo/game/sync/seg/SegmentType.java
rename to game/src/main/java/org/apollo/game/sync/seg/SegmentType.java
diff --git a/game/src/main/org/apollo/game/sync/seg/SynchronizationSegment.java b/game/src/main/java/org/apollo/game/sync/seg/SynchronizationSegment.java
similarity index 100%
rename from game/src/main/org/apollo/game/sync/seg/SynchronizationSegment.java
rename to game/src/main/java/org/apollo/game/sync/seg/SynchronizationSegment.java
diff --git a/game/src/main/org/apollo/game/sync/seg/TeleportSegment.java b/game/src/main/java/org/apollo/game/sync/seg/TeleportSegment.java
similarity index 100%
rename from game/src/main/org/apollo/game/sync/seg/TeleportSegment.java
rename to game/src/main/java/org/apollo/game/sync/seg/TeleportSegment.java
diff --git a/game/src/main/org/apollo/game/sync/seg/package-info.java b/game/src/main/java/org/apollo/game/sync/seg/package-info.java
similarity index 100%
rename from game/src/main/org/apollo/game/sync/seg/package-info.java
rename to game/src/main/java/org/apollo/game/sync/seg/package-info.java
diff --git a/game/src/main/org/apollo/game/sync/task/NpcSynchronizationTask.java b/game/src/main/java/org/apollo/game/sync/task/NpcSynchronizationTask.java
similarity index 100%
rename from game/src/main/org/apollo/game/sync/task/NpcSynchronizationTask.java
rename to game/src/main/java/org/apollo/game/sync/task/NpcSynchronizationTask.java
diff --git a/game/src/main/org/apollo/game/sync/task/PhasedSynchronizationTask.java b/game/src/main/java/org/apollo/game/sync/task/PhasedSynchronizationTask.java
similarity index 100%
rename from game/src/main/org/apollo/game/sync/task/PhasedSynchronizationTask.java
rename to game/src/main/java/org/apollo/game/sync/task/PhasedSynchronizationTask.java
diff --git a/game/src/main/org/apollo/game/sync/task/PlayerSynchronizationTask.java b/game/src/main/java/org/apollo/game/sync/task/PlayerSynchronizationTask.java
similarity index 100%
rename from game/src/main/org/apollo/game/sync/task/PlayerSynchronizationTask.java
rename to game/src/main/java/org/apollo/game/sync/task/PlayerSynchronizationTask.java
diff --git a/game/src/main/org/apollo/game/sync/task/PostNpcSynchronizationTask.java b/game/src/main/java/org/apollo/game/sync/task/PostNpcSynchronizationTask.java
similarity index 100%
rename from game/src/main/org/apollo/game/sync/task/PostNpcSynchronizationTask.java
rename to game/src/main/java/org/apollo/game/sync/task/PostNpcSynchronizationTask.java
diff --git a/game/src/main/org/apollo/game/sync/task/PostPlayerSynchronizationTask.java b/game/src/main/java/org/apollo/game/sync/task/PostPlayerSynchronizationTask.java
similarity index 100%
rename from game/src/main/org/apollo/game/sync/task/PostPlayerSynchronizationTask.java
rename to game/src/main/java/org/apollo/game/sync/task/PostPlayerSynchronizationTask.java
diff --git a/game/src/main/org/apollo/game/sync/task/PreNpcSynchronizationTask.java b/game/src/main/java/org/apollo/game/sync/task/PreNpcSynchronizationTask.java
similarity index 100%
rename from game/src/main/org/apollo/game/sync/task/PreNpcSynchronizationTask.java
rename to game/src/main/java/org/apollo/game/sync/task/PreNpcSynchronizationTask.java
diff --git a/game/src/main/org/apollo/game/sync/task/PrePlayerSynchronizationTask.java b/game/src/main/java/org/apollo/game/sync/task/PrePlayerSynchronizationTask.java
similarity index 100%
rename from game/src/main/org/apollo/game/sync/task/PrePlayerSynchronizationTask.java
rename to game/src/main/java/org/apollo/game/sync/task/PrePlayerSynchronizationTask.java
diff --git a/game/src/main/org/apollo/game/sync/task/SynchronizationTask.java b/game/src/main/java/org/apollo/game/sync/task/SynchronizationTask.java
similarity index 100%
rename from game/src/main/org/apollo/game/sync/task/SynchronizationTask.java
rename to game/src/main/java/org/apollo/game/sync/task/SynchronizationTask.java
diff --git a/game/src/main/org/apollo/game/sync/task/package-info.java b/game/src/main/java/org/apollo/game/sync/task/package-info.java
similarity index 100%
rename from game/src/main/org/apollo/game/sync/task/package-info.java
rename to game/src/main/java/org/apollo/game/sync/task/package-info.java
diff --git a/game/src/main/org/apollo/package-info.java b/game/src/main/java/org/apollo/package-info.java
similarity index 100%
rename from game/src/main/org/apollo/package-info.java
rename to game/src/main/java/org/apollo/package-info.java
diff --git a/game/src/main/kotlin/org/apollo/game/action/ActionCoroutine.kt b/game/src/main/kotlin/org/apollo/game/action/ActionCoroutine.kt
new file mode 100644
index 000000000..9f1bd9fd9
--- /dev/null
+++ b/game/src/main/kotlin/org/apollo/game/action/ActionCoroutine.kt
@@ -0,0 +1,137 @@
+package org.apollo.game.action
+
+import java.util.concurrent.CancellationException
+import java.util.concurrent.atomic.AtomicInteger
+import java.util.concurrent.atomic.AtomicReference
+import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED
+import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlin.coroutines.*
+
+typealias ActionPredicate = () -> Boolean
+typealias ActionBlock = suspend ActionCoroutine.() -> Unit
+
+interface ActionCoroutineCondition {
+ /**
+ * Called once every tick to check if `Continuation` associated with this condition should be resumed.
+ */
+ fun resume(): Boolean
+}
+
+/**
+ * A continuation condition that waits on a given number of pulses.
+ */
+class AwaitPulses(pulses: Int) : ActionCoroutineCondition {
+ val remainingPulses: AtomicInteger = AtomicInteger(pulses)
+
+ override fun resume(): Boolean {
+ return remainingPulses.decrementAndGet() <= 0
+ }
+}
+
+/**
+ * A continuation condition that waits until a predicate is fufilled.
+ */
+class AwaitPredicate(val predicate: ActionPredicate) : ActionCoroutineCondition {
+ override fun resume(): Boolean {
+ return predicate.invoke()
+ }
+}
+
+/**
+ * A suspend point in an `ActionCoroutine` that has its `continuation` resumed whenever the `condition` evaluates
+ * to `true`.
+ */
+data class ActionCoroutineStep(val condition: ActionCoroutineCondition, internal val continuation: Continuation)
+
+@RestrictsSuspension
+class ActionCoroutine : Continuation {
+ companion object {
+ /**
+ * Create a new `ActionCoroutine` and immediately execute the given `block`, returning a continuation that
+ * can be resumed.
+ */
+ fun start(block: ActionBlock): ActionCoroutine {
+ val coroutine = ActionCoroutine()
+ val continuation = block.createCoroutine(coroutine, coroutine)
+
+ coroutine.resumeContinuation(continuation)
+
+ return coroutine
+ }
+ }
+
+ override val context: CoroutineContext = EmptyCoroutineContext
+ override fun resumeWith(result: Result) {
+ if (result.isFailure) {
+ throw result.exceptionOrNull()!!
+ }
+ }
+
+ private fun resumeContinuation(continuation: Continuation, allowCancellation: Boolean = true) {
+ try {
+ continuation.resume(Unit)
+ } catch (ex: CancellationException) {
+ if (!allowCancellation) {
+ throw ex
+ }
+ }
+ }
+
+ /**
+ * The next `step` in this `ActionCoroutine` saved as a resume point.
+ */
+ private var next = AtomicReference()
+
+ /**
+ * Check if this continuation has no more steps to execute.
+ */
+ fun stopped(): Boolean {
+ return next.get() == null
+ }
+
+ /**
+ * Update this continuation and check if the condition for the next step to be resumed is satisfied.
+ */
+ fun pulse() {
+ val nextStep = next.getAndSet(null) ?: return
+
+ val condition = nextStep.condition
+ val continuation = nextStep.continuation
+
+ if (condition.resume()) {
+ resumeContinuation(continuation)
+ } else {
+ next.compareAndSet(null, nextStep)
+ }
+ }
+
+ private suspend fun awaitCondition(condition: ActionCoroutineCondition) {
+ return suspendCoroutineUninterceptedOrReturn { cont ->
+ next.compareAndSet(null, ActionCoroutineStep(condition, cont))
+ COROUTINE_SUSPENDED
+ }
+ }
+
+ /**
+ * Stop execution of this continuation.
+ */
+ suspend fun stop(): Nothing {
+ suspendCancellableCoroutine { cont ->
+ next.set(null)
+ cont.cancel()
+ }
+
+ error("Tried to resume execution a coroutine that should have been cancelled.")
+ }
+
+ /**
+ * Wait `pulses` game updates before resuming this continuation.
+ */
+ suspend fun wait(pulses: Int = 1) = awaitCondition(AwaitPulses(pulses))
+
+ /**
+ * Wait until the `predicate` returns `true` before resuming this continuation.
+ */
+ suspend fun wait(predicate: ActionPredicate) = awaitCondition(AwaitPredicate(predicate))
+}
\ No newline at end of file
diff --git a/game/src/main/kotlin/org/apollo/game/action/AsyncAction.kt b/game/src/main/kotlin/org/apollo/game/action/AsyncAction.kt
new file mode 100644
index 000000000..e8df401d2
--- /dev/null
+++ b/game/src/main/kotlin/org/apollo/game/action/AsyncAction.kt
@@ -0,0 +1,16 @@
+package org.apollo.game.action
+
+import org.apollo.game.model.entity.Mob
+
+abstract class AsyncAction : Action, AsyncActionTrait {
+
+ override var continuation: ActionCoroutine? = null
+
+ constructor(delay: Int, immediate: Boolean, mob: T) : super(delay, immediate, mob)
+
+ override fun execute() {
+ if (update()) {
+ stop()
+ }
+ }
+}
\ No newline at end of file
diff --git a/game/src/main/kotlin/org/apollo/game/action/AsyncActionTrait.kt b/game/src/main/kotlin/org/apollo/game/action/AsyncActionTrait.kt
new file mode 100644
index 000000000..2f0e20d0c
--- /dev/null
+++ b/game/src/main/kotlin/org/apollo/game/action/AsyncActionTrait.kt
@@ -0,0 +1,29 @@
+package org.apollo.game.action
+
+interface AsyncActionTrait {
+ /**
+ * The continuation that this `Action` is executing. May be `null` if this action hasn't started yet.
+ */
+ abstract var continuation: ActionCoroutine?
+
+ /**
+ * Update this action, initializing the continuation if not already initialized.
+ *
+ * @return `true` if this `Action` has completed execution.
+ */
+ fun update(): Boolean {
+ val continuation = this.continuation
+ if (continuation == null) {
+ this.continuation = ActionCoroutine.start(action())
+ return false
+ }
+
+ continuation.pulse()
+ return continuation.stopped()
+ }
+
+ /**
+ * Create a new `ActionBlock` to execute.
+ */
+ fun action(): ActionBlock
+}
\ No newline at end of file
diff --git a/game/src/main/kotlin/org/apollo/game/action/AsyncDistancedAction.kt b/game/src/main/kotlin/org/apollo/game/action/AsyncDistancedAction.kt
new file mode 100644
index 000000000..ce550eb34
--- /dev/null
+++ b/game/src/main/kotlin/org/apollo/game/action/AsyncDistancedAction.kt
@@ -0,0 +1,21 @@
+package org.apollo.game.action
+
+import org.apollo.game.model.Position
+import org.apollo.game.model.entity.Mob
+
+/**
+ * A `DistancedAction` that uses `ActionCoroutine`s to run asynchronously.
+ */
+abstract class AsyncDistancedAction : DistancedAction, AsyncActionTrait {
+
+ override var continuation: ActionCoroutine? = null
+
+ constructor(delay: Int, immediate: Boolean, mob: T, position: Position, distance: Int) :
+ super(delay, immediate, mob, position, distance)
+
+ override fun executeAction() {
+ if (update()) {
+ stop()
+ }
+ }
+}
diff --git a/game/src/main/kotlin/org/apollo/game/plugin/kotlin/KotlinCommandHandler.kt b/game/src/main/kotlin/org/apollo/game/plugin/kotlin/KotlinCommandHandler.kt
new file mode 100644
index 000000000..4509eaa2f
--- /dev/null
+++ b/game/src/main/kotlin/org/apollo/game/plugin/kotlin/KotlinCommandHandler.kt
@@ -0,0 +1,41 @@
+package org.apollo.game.plugin.kotlin
+
+import org.apollo.game.command.Command
+import org.apollo.game.command.CommandListener
+import org.apollo.game.model.World
+import org.apollo.game.model.entity.Player
+import org.apollo.game.model.entity.setting.PrivilegeLevel
+
+/**
+ * A handler for [Command]s.
+ */
+class KotlinCommandHandler(
+ val world: World,
+ val command: String,
+ privileges: PrivilegeLevel
+) : CommandListener(privileges) {
+
+ var callback: Command.(Player) -> Unit = {}
+ var predicate: Command.() -> Boolean = { true }
+
+ override fun execute(player: Player, command: Command) {
+ if (command.predicate()) {
+ command.callback(player)
+ }
+ }
+
+ fun register() {
+ world.commandDispatcher.register(command, this)
+ }
+
+ fun where(predicate: Command.() -> Boolean): KotlinCommandHandler {
+ this.predicate = predicate
+ return this
+ }
+
+ fun then(callback: Command.(Player) -> Unit) {
+ this.callback = callback
+ this.register()
+ }
+
+}
\ No newline at end of file
diff --git a/game/src/main/kotlin/org/apollo/game/plugin/kotlin/KotlinMessageHandler.kt b/game/src/main/kotlin/org/apollo/game/plugin/kotlin/KotlinMessageHandler.kt
new file mode 100644
index 000000000..799db9dc9
--- /dev/null
+++ b/game/src/main/kotlin/org/apollo/game/plugin/kotlin/KotlinMessageHandler.kt
@@ -0,0 +1,22 @@
+package org.apollo.game.plugin.kotlin
+
+import org.apollo.game.message.handler.MessageHandler
+import org.apollo.game.model.World
+import org.apollo.game.model.entity.Player
+import org.apollo.game.plugin.PluginContext
+import org.apollo.net.message.Message
+import kotlin.reflect.KClass
+
+/**
+ * A handler for [Message]s.
+ */
+@Deprecated("To be removed")
+class OldKotlinMessageHandler(val world: World, val context: PluginContext, val type: KClass) :
+ KotlinPlayerHandlerProxyTrait, MessageHandler(world) {
+
+ override var callback: T.(Player) -> Unit = {}
+ override var predicate: T.() -> Boolean = { true }
+
+ override fun handle(player: Player, message: T) = handleProxy(player, message)
+ override fun register() = context.addMessageHandler(type.java, this)
+}
\ No newline at end of file
diff --git a/game/src/main/kotlin/org/apollo/game/plugin/kotlin/KotlinPlayerHandlerProxyTrait.kt b/game/src/main/kotlin/org/apollo/game/plugin/kotlin/KotlinPlayerHandlerProxyTrait.kt
new file mode 100644
index 000000000..0de3a0f07
--- /dev/null
+++ b/game/src/main/kotlin/org/apollo/game/plugin/kotlin/KotlinPlayerHandlerProxyTrait.kt
@@ -0,0 +1,31 @@
+package org.apollo.game.plugin.kotlin
+
+import org.apollo.game.model.entity.Player
+
+/**
+ * A proxy interface for any handler that operates on [Player]s.
+ */
+@Deprecated("To be removed")
+interface KotlinPlayerHandlerProxyTrait {
+
+ var callback: S.(Player) -> Unit
+ var predicate: S.() -> Boolean
+
+ fun register()
+
+ fun where(predicate: S.() -> Boolean): KotlinPlayerHandlerProxyTrait {
+ this.predicate = predicate
+ return this
+ }
+
+ fun then(callback: S.(Player) -> Unit) {
+ this.callback = callback
+ this.register()
+ }
+
+ fun handleProxy(player: Player, subject: S) {
+ if (subject.predicate()) {
+ subject.callback(player)
+ }
+ }
+}
diff --git a/game/src/main/kotlin/org/apollo/game/plugin/kotlin/KotlinPluginScript.kt b/game/src/main/kotlin/org/apollo/game/plugin/kotlin/KotlinPluginScript.kt
new file mode 100644
index 000000000..37bdfdfca
--- /dev/null
+++ b/game/src/main/kotlin/org/apollo/game/plugin/kotlin/KotlinPluginScript.kt
@@ -0,0 +1,112 @@
+package org.apollo.game.plugin.kotlin
+
+import org.apollo.game.command.CommandListener
+import org.apollo.game.message.handler.MessageHandler
+import org.apollo.game.message.impl.ButtonMessage
+import org.apollo.game.model.World
+import org.apollo.game.model.entity.setting.PrivilegeLevel
+import org.apollo.game.model.event.Event
+import org.apollo.game.model.event.EventListener
+import org.apollo.game.model.event.PlayerEvent
+import org.apollo.game.plugin.PluginContext
+import org.apollo.net.message.Message
+import kotlin.reflect.KClass
+import kotlin.script.experimental.annotations.KotlinScript
+
+@KotlinScript("Apollo Plugin Script", fileExtension = "plugin.kts")
+abstract class KotlinPluginScript(var world: World, val context: PluginContext) {
+
+ private var startListener: (World) -> Unit = { _ -> }
+
+ private var stopListener: (World) -> Unit = { _ -> }
+
+ fun on(
+ listenable: Listenable,
+ callback: C.() -> Unit
+ ) {
+ registerListener(listenable, null, callback)
+ }
+
+ internal fun registerListener(
+ listenable: Listenable,
+ predicateContext: I?,
+ callback: C.() -> Unit
+ ) {
+ // Smart-casting/type-inference is completely broken in this function in intelliJ, so assign to otherwise
+ // pointless `l` values for now.
+
+ return when (listenable) {
+ is MessageListenable -> {
+ @Suppress("UNCHECKED_CAST")
+ val l = listenable as MessageListenable
+
+ val handler = l.createHandler(world, predicateContext, callback)
+ context.addMessageHandler(l.type.java, handler)
+ }
+ is EventListenable -> {
+ @Suppress("UNCHECKED_CAST")
+ val l = listenable as EventListenable
+
+ val handler = l.createHandler(world, predicateContext, callback)
+ world.listenFor(l.type.java, handler)
+ }
+ }
+ }
+
+ /**
+ * Create a [CommandListener] for the given [command] name, which only players with a [PrivilegeLevel]
+ * of [privileges] and above can use.
+ */
+ fun on_command(command: String, privileges: PrivilegeLevel): KotlinCommandHandler { // TODO what to do with this?
+ return KotlinCommandHandler(world, command, privileges)
+ }
+
+ /**
+ * Creates a [MessageHandler].
+ */
+ @Deprecated("Use new on(Type) listener")
+ fun on(type: () -> KClass): OldKotlinMessageHandler {
+ return OldKotlinMessageHandler(world, context, type())
+ }
+
+ /**
+ * Create an [EventListener] for a [PlayerEvent].
+ */
+ @Deprecated("Use new on(Type) listener")
+ fun on_player_event(type: () -> KClass): OldKotlinPlayerEventHandler {
+ return OldKotlinPlayerEventHandler(world, type())
+ }
+
+ /**
+ * Create an [EventListener] for an [Event].
+ */
+ @Deprecated("Use new on(Type) listener")
+ fun on_event(type: () -> KClass): OldKotlinEventHandler {
+ return OldKotlinEventHandler(world, type())
+ }
+
+ /**
+ * Create a [ButtonMessage] [MessageHandler] for the given [id].
+ */
+ @Deprecated("Use new on(Type) listener")
+ fun on_button(id: Int): KotlinPlayerHandlerProxyTrait {
+ return on { ButtonMessage::class }.where { widgetId == id }
+ }
+
+ fun start(callback: (World) -> Unit) {
+ startListener = callback
+ }
+
+ fun stop(callback: (World) -> Unit) {
+ stopListener = callback
+ }
+
+ fun doStart(world: World) {
+ startListener(world)
+ }
+
+ fun doStop(world: World) {
+ stopListener(world)
+ }
+
+}
\ No newline at end of file
diff --git a/game/src/main/kotlin/org/apollo/game/plugin/kotlin/Listenable.kt b/game/src/main/kotlin/org/apollo/game/plugin/kotlin/Listenable.kt
new file mode 100644
index 000000000..9a89fa641
--- /dev/null
+++ b/game/src/main/kotlin/org/apollo/game/plugin/kotlin/Listenable.kt
@@ -0,0 +1,27 @@
+package org.apollo.game.plugin.kotlin
+
+import org.apollo.game.message.handler.MessageHandler
+import org.apollo.game.model.World
+import org.apollo.game.model.event.Event
+import org.apollo.game.model.event.EventListener
+import org.apollo.net.message.Message
+import kotlin.reflect.KClass
+
+/**
+ * A game occurrence that can be listened to.
+ */
+sealed class Listenable {
+ abstract val type: KClass
+}
+
+abstract class EventListenable : Listenable() {
+
+ abstract fun createHandler(world: World, predicateContext: P?, callback: C.() -> Unit): EventListener
+
+}
+
+abstract class MessageListenable : Listenable() {
+
+ abstract fun createHandler(world: World, predicateContext: P?, callback: C.() -> Unit): MessageHandler
+
+}
diff --git a/game/src/main/kotlin/org/apollo/game/plugin/kotlin/ListenableContext.kt b/game/src/main/kotlin/org/apollo/game/plugin/kotlin/ListenableContext.kt
new file mode 100644
index 000000000..760644a4f
--- /dev/null
+++ b/game/src/main/kotlin/org/apollo/game/plugin/kotlin/ListenableContext.kt
@@ -0,0 +1,17 @@
+package org.apollo.game.plugin.kotlin
+
+import org.apollo.game.model.entity.Player
+
+/**
+ * Contextual information for a [Listenable].
+ */
+interface ListenableContext
+
+/**
+ * Contextual information for a [Listenable] involving a specific [Player].
+ */
+interface PlayerContext : ListenableContext {
+ val player: Player
+}
+
+interface PredicateContext
\ No newline at end of file
diff --git a/game/src/main/kotlin/org/apollo/game/plugin/kotlin/OldKotlinEventHandler.kt b/game/src/main/kotlin/org/apollo/game/plugin/kotlin/OldKotlinEventHandler.kt
new file mode 100644
index 000000000..7269b96d1
--- /dev/null
+++ b/game/src/main/kotlin/org/apollo/game/plugin/kotlin/OldKotlinEventHandler.kt
@@ -0,0 +1,34 @@
+package org.apollo.game.plugin.kotlin
+
+import org.apollo.game.model.World
+import org.apollo.game.model.event.Event
+import org.apollo.game.model.event.EventListener
+import kotlin.reflect.KClass
+
+/**
+ * A handler for [Event]s.
+ */
+@Deprecated("To be removed")
+class OldKotlinEventHandler(val world: World, val type: KClass) : EventListener {
+
+ private var callback: S.() -> Unit = {}
+ private var predicate: S.() -> Boolean = { true }
+
+ fun where(predicate: S.() -> Boolean): OldKotlinEventHandler {
+ this.predicate = predicate
+ return this
+ }
+
+ fun then(callback: S.() -> Unit) {
+ this.callback = callback
+ this.register()
+ }
+
+ override fun handle(event: S) {
+ if (event.predicate()) {
+ event.callback()
+ }
+ }
+
+ fun register() = world.listenFor(type.java, this)
+}
\ No newline at end of file
diff --git a/game/src/main/kotlin/org/apollo/game/plugin/kotlin/OldKotlinPlayerEventHandler.kt b/game/src/main/kotlin/org/apollo/game/plugin/kotlin/OldKotlinPlayerEventHandler.kt
new file mode 100644
index 000000000..8a127037b
--- /dev/null
+++ b/game/src/main/kotlin/org/apollo/game/plugin/kotlin/OldKotlinPlayerEventHandler.kt
@@ -0,0 +1,21 @@
+package org.apollo.game.plugin.kotlin
+
+import org.apollo.game.model.World
+import org.apollo.game.model.entity.Player
+import org.apollo.game.model.event.EventListener
+import org.apollo.game.model.event.PlayerEvent
+import kotlin.reflect.KClass
+
+/**
+ * A handler for [PlayerEvent]s.
+ */
+@Deprecated("To be removed")
+class OldKotlinPlayerEventHandler(val world: World, val type: KClass) :
+ KotlinPlayerHandlerProxyTrait, EventListener {
+
+ override var callback: T.(Player) -> Unit = {}
+ override var predicate: T.() -> Boolean = { true }
+
+ override fun handle(event: T) = handleProxy(event.player, event)
+ override fun register() = world.listenFor(type.java, this)
+}
\ No newline at end of file
diff --git a/game/src/main/kotlin/org/apollo/game/plugin/kotlin/message/ButtonClick.kt b/game/src/main/kotlin/org/apollo/game/plugin/kotlin/message/ButtonClick.kt
new file mode 100644
index 000000000..1b62dbf1f
--- /dev/null
+++ b/game/src/main/kotlin/org/apollo/game/plugin/kotlin/message/ButtonClick.kt
@@ -0,0 +1,56 @@
+package org.apollo.game.plugin.kotlin.message
+
+import org.apollo.game.message.handler.MessageHandler
+import org.apollo.game.message.impl.ButtonMessage
+import org.apollo.game.model.World
+import org.apollo.game.model.entity.Player
+import org.apollo.game.plugin.kotlin.KotlinPluginScript
+import org.apollo.game.plugin.kotlin.MessageListenable
+import org.apollo.game.plugin.kotlin.PlayerContext
+import org.apollo.game.plugin.kotlin.PredicateContext
+
+/**
+ * Registers a listener for [ButtonMessage]s that occur on the given [button] id.
+ *
+ * ```
+ * on(ButtonClick, button = 416) {
+ * player.sendMessage("You click the button.")
+ * }
+ * ```
+ */
+fun KotlinPluginScript.on(
+ listenable: ButtonClick.Companion,
+ button: Int,
+ callback: ButtonClick.() -> Unit
+) {
+ registerListener(listenable, ButtonPredicateContext(button), callback)
+}
+
+class ButtonClick(override val player: Player, val button: Int) : PlayerContext {
+
+ companion object : MessageListenable() {
+
+ override val type = ButtonMessage::class
+
+ override fun createHandler(
+ world: World,
+ predicateContext: ButtonPredicateContext?,
+ callback: ButtonClick.() -> Unit
+ ): MessageHandler {
+ return object : MessageHandler