diff --git a/game/build.gradle b/game/build.gradle index b9646593..b4441d4a 100644 --- a/game/build.gradle +++ b/game/build.gradle @@ -24,6 +24,12 @@ sourceSets { } } +repositories { + maven { + url { 'https://dl.bintray.com/kotlin/kotlinx/' } + } +} + dependencies { compile project(':cache') compile project(':net') @@ -32,6 +38,8 @@ dependencies { compile group: 'io.github.lukehutch', name: 'fast-classpath-scanner', version: '2.0.21' compile group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-jre8', version: "$kotlinVersion" compile group: 'org.jetbrains.kotlin', name: 'kotlin-compiler-embeddable', version: "$kotlinVersion" + compile group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-jdk8', version: '0.16' + compile group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-core', version: '0.16' testCompile group: 'org.assertj', name: 'assertj-core', version: '3.8.0' } diff --git a/game/src/main/java/org/apollo/game/plugin/KotlinPluginCompilerStub.java b/game/src/main/java/org/apollo/game/plugin/KotlinPluginCompilerStub.java deleted file mode 100644 index 1912d407..00000000 --- a/game/src/main/java/org/apollo/game/plugin/KotlinPluginCompilerStub.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.apollo.game.plugin; - -import org.apollo.game.plugin.kotlin.KotlinPluginCompiler; - -public class KotlinPluginCompilerStub { - public static void main(String[] argv) { - KotlinPluginCompiler.main(argv); - } -} diff --git a/game/src/main/java/org/apollo/game/plugin/KotlinPluginEnvironment.java b/game/src/main/java/org/apollo/game/plugin/KotlinPluginEnvironment.java index dba14469..853326aa 100644 --- a/game/src/main/java/org/apollo/game/plugin/KotlinPluginEnvironment.java +++ b/game/src/main/java/org/apollo/game/plugin/KotlinPluginEnvironment.java @@ -19,7 +19,6 @@ import java.util.stream.Collectors; import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner; import io.github.lukehutch.fastclasspathscanner.scanner.ScanResult; import org.apollo.game.model.World; -import org.apollo.game.plugin.kotlin.KotlinPluginCompiler; import org.apollo.game.plugin.kotlin.KotlinPluginScript; import org.jetbrains.annotations.NotNull; import org.jetbrains.kotlin.cli.common.messages.CompilerMessageLocation; 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 00000000..abf85dc8 --- /dev/null +++ b/game/src/main/kotlin/org/apollo/game/action/AsyncAction.kt @@ -0,0 +1,26 @@ +package org.apollo.game.action + +import org.apollo.game.model.entity.Mob + +abstract class AsyncAction : Action, AsyncActionTrait { + override val runner: AsyncActionRunner + + constructor(delay: Int, immediate: Boolean, mob: T) : super(delay, immediate, mob) { + this.runner = AsyncActionRunner({ this }, { executeActionAsync() }) + } + + abstract suspend fun executeActionAsync() + + override fun execute() { + if (!runner.started()) { + runner.start() + } + + runner.pulse() + } + + override fun stop() { + super.stop() + runner.stop() + } +} \ No newline at end of file diff --git a/game/src/main/kotlin/org/apollo/game/action/AsyncActionRunner.kt b/game/src/main/kotlin/org/apollo/game/action/AsyncActionRunner.kt new file mode 100644 index 00000000..366af4e2 --- /dev/null +++ b/game/src/main/kotlin/org/apollo/game/action/AsyncActionRunner.kt @@ -0,0 +1,56 @@ +package org.apollo.game.action + +import kotlinx.coroutines.experimental.* +import kotlinx.coroutines.experimental.channels.Channel +import kotlinx.coroutines.experimental.selects.select +import org.apollo.game.model.entity.Mob +import java.util.function.Supplier + +class AsyncActionRunner(val actionSupplier: () -> Action<*>, val callback: suspend () -> Unit) { + var job: Job? = null + var pulseChannel = Channel(1) + var unsentPulses = 0 + + fun pulse() { + if (pulseChannel.offer(unsentPulses + 1)) { + unsentPulses = 0 + } else { + unsentPulses++ + } + } + + fun start() { + if (job != null) { + return + } + + val action = actionSupplier.invoke() + + job = launch(CommonPool) { + select { + pulseChannel.onReceive { + callback() + action.stop() + } + } + } + } + + fun started(): Boolean { + return job != null + } + + fun stop() { + job?.cancel() + pulseChannel.close() + } + + suspend fun wait(pulses: Int = 1) { + var remainingPulses = pulses + + while (remainingPulses > 0) { + val numPulses = pulseChannel.receive() + remainingPulses -= numPulses + } + } +} \ 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 00000000..f398c5f9 --- /dev/null +++ b/game/src/main/kotlin/org/apollo/game/action/AsyncActionTrait.kt @@ -0,0 +1,9 @@ +package org.apollo.game.action + +interface AsyncActionTrait { + val runner: AsyncActionRunner + + suspend fun wait(pulses: Int = 1) { + runner.wait(pulses) + } +} \ 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 00000000..e947f8e4 --- /dev/null +++ b/game/src/main/kotlin/org/apollo/game/action/AsyncDistancedAction.kt @@ -0,0 +1,32 @@ +package org.apollo.game.action + +import org.apollo.game.model.Position +import org.apollo.game.model.entity.Mob + +abstract class AsyncDistancedAction : DistancedAction, AsyncActionTrait { + + override val runner: AsyncActionRunner + + constructor(delay: Int, immediate: Boolean, mob: T, position: Position, distance: Int) : + super(delay, immediate, mob, position, distance) { + + this.runner = AsyncActionRunner({ this }, { executeActionAsync() }) + } + + abstract suspend fun executeActionAsync() + + override fun stop() { + super.stop() + runner.stop() + } + + override fun executeAction() { + if (!runner.started()) { + runner.start() + } + + runner.pulse() + } + +} + diff --git a/game/src/main/kotlin/org/apollo/game/plugin/kotlin/KotlinPluginCompiler.kt b/game/src/main/kotlin/org/apollo/game/plugin/kotlin/KotlinPluginCompiler.kt deleted file mode 100644 index 85840729..00000000 --- a/game/src/main/kotlin/org/apollo/game/plugin/kotlin/KotlinPluginCompiler.kt +++ /dev/null @@ -1,159 +0,0 @@ -package org.apollo.game.plugin.kotlin - -import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys -import org.jetbrains.kotlin.cli.common.messages.* -import org.jetbrains.kotlin.cli.jvm.compiler.* -import org.jetbrains.kotlin.cli.jvm.config.JvmClasspathRoot -import org.jetbrains.kotlin.codegen.CompilationException -import org.jetbrains.kotlin.com.intellij.openapi.util.Disposer -import org.jetbrains.kotlin.config.* -import org.jetbrains.kotlin.script.KotlinScriptDefinitionFromAnnotatedTemplate -import java.io.File -import java.lang.management.ManagementFactory -import java.net.URISyntaxException -import java.net.URLClassLoader -import java.nio.file.* -import java.nio.file.StandardOpenOption.* -import java.util.* - -class KotlinMessageCollector : MessageCollector { - - override fun clear() { - } - - override fun report(severity: CompilerMessageSeverity, message: String, location: CompilerMessageLocation) { - if (severity.isError) { - println("${location.path}:${location.line}-${location.column}: $message") - println(">>> ${location.lineContent}") - } - } - - override fun hasErrors(): Boolean { - return false - } - -} - -data class KotlinCompilerResult(val fqName: String, val outputPath: Path) - -class KotlinPluginCompiler(val classpath: List, val messageCollector: MessageCollector) { - - companion object { - - fun currentClasspath(): List { - val classLoader = Thread.currentThread().contextClassLoader as? URLClassLoader ?: - throw RuntimeException("Unable to resolve classpath for current ClassLoader") - - val classpathUrls = classLoader.urLs - val classpath = ArrayList() - - for (classpathUrl in classpathUrls) { - try { - classpath.add(File(classpathUrl.toURI())) - } catch (e: URISyntaxException) { - throw RuntimeException("URL returned by ClassLoader is invalid") - } - - } - - return classpath - } - - @JvmStatic - fun main(args: Array) { - if (args.size < 2) throw RuntimeException("Usage: script1.kts script2.kts ...") - - val outputDir = Paths.get(args[0]) - val inputScripts = args.slice(1..args.size - 1).map { Paths.get(it) } - val classpath = mutableListOf() - - val runtimeBean = ManagementFactory.getRuntimeMXBean() - if (!runtimeBean.isBootClassPathSupported) { - println("Warning! Boot class path is not supported, must be supplied on the command line") - } else { - val bootClasspath = runtimeBean.bootClassPath - classpath.addAll(bootClasspath.split(File.pathSeparatorChar).map { File(it) }) - } - - /** - * Our current classpath should contain all compile time dependencies for the plugin as well as Apollo's - * own sources. We can also achieve this via Gradle but doing it at runtime prevents Gradle from thinking - * the build has been modified after evaluation. - */ - classpath.addAll(currentClasspath()) - - val compiler = KotlinPluginCompiler(classpath, MessageCollector.NONE) - val compiledScriptClasses = mutableListOf() - - try { - try { - Files.createDirectory(outputDir) - } catch (e: FileAlreadyExistsException) { - // do nothing... - } - - inputScripts.forEach { - compiledScriptClasses.add(compiler.compile(it, outputDir).fqName) - } - } catch (t: Throwable) { - t.printStackTrace() - System.exit(1) - } - } - } - - private fun createCompilerConfiguration(inputPath: Path): CompilerConfiguration { - val configuration = CompilerConfiguration() - val scriptDefinition = KotlinScriptDefinitionFromAnnotatedTemplate(KotlinPluginScript::class) - - configuration.add(JVMConfigurationKeys.SCRIPT_DEFINITIONS, scriptDefinition) - configuration.put(JVMConfigurationKeys.CONTENT_ROOTS, classpath.map { JvmClasspathRoot(it) }) - configuration.put(JVMConfigurationKeys.RETAIN_OUTPUT_IN_MEMORY, true) - configuration.put(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, KotlinMessageCollector()) - configuration.put(CommonConfigurationKeys.MODULE_NAME, inputPath.toString()) - configuration.addKotlinSourceRoot(inputPath.toAbsolutePath().toString()) - - return configuration - } - - @Throws(KotlinPluginCompilerException::class) - fun compile(inputPath: Path, outputPath: Path): KotlinCompilerResult { - val rootDisposable = Disposer.newDisposable() - val configuration = createCompilerConfiguration(inputPath) - - val configFiles = EnvironmentConfigFiles.JVM_CONFIG_FILES - val environment = KotlinCoreEnvironment.createForProduction(rootDisposable, configuration, configFiles) - - try { - val generationState = KotlinToJVMBytecodeCompiler.analyzeAndGenerate(environment) - if (generationState == null) { - throw KotlinPluginCompilerException("Failed to generate bytecode for kotlin script") - } - - val sourceFiles = environment.getSourceFiles() - val script = sourceFiles[0].script ?: throw KotlinPluginCompilerException("Main script file isnt a script") - - val scriptFilePath = script.fqName.asString().replace('.', '/') + ".class" - val scriptFileClass = generationState.factory.get(scriptFilePath) - - if (scriptFileClass == null) { - throw KotlinPluginCompilerException("Unable to find compiled plugin class file $scriptFilePath") - } - - generationState.factory.asList().forEach { - Files.write(outputPath.resolve(it.relativePath), it.asByteArray(), CREATE, WRITE, TRUNCATE_EXISTING) - } - - return KotlinCompilerResult(script.fqName.asString(), outputPath.resolve(scriptFileClass.relativePath)) - } catch (e: CompilationException) { - throw KotlinPluginCompilerException("Compilation failed", e) - } finally { - Disposer.dispose(rootDisposable) - } - } - -} - -class KotlinPluginCompilerException(message: String, cause: Throwable? = null) : Exception(message, cause) { - -} diff --git a/game/src/pluginTesting/kotlin/org/apollo/game/plugin/testing/KotlinPluginTestHelpers.kt b/game/src/pluginTesting/kotlin/org/apollo/game/plugin/testing/KotlinPluginTestHelpers.kt index b6a26f66..9dd4898a 100644 --- a/game/src/pluginTesting/kotlin/org/apollo/game/plugin/testing/KotlinPluginTestHelpers.kt +++ b/game/src/pluginTesting/kotlin/org/apollo/game/plugin/testing/KotlinPluginTestHelpers.kt @@ -38,6 +38,16 @@ abstract class KotlinPluginTestHelpers { do { action.pulse() + + /** + * Introducing an artificial delay is necessary to prevent the timeout being exceeded before + * an asynchronous [Action] really starts. When a job is submitted to a new coroutine context + * there may be a delay before it is actually executed. + * + * This delay is typically sub-millisecond and is only incurred with startup. Since game actions + * have larger delays of their own this isn't a problem in practice. + */ + Thread.sleep(50L) } while (action.isRunning && pulses++ < timeout) Assert.assertFalse("Exceeded timeout waiting for action completion", pulses > timeout) diff --git a/game/src/plugins/dummy/src/dummy.plugin.kts b/game/src/plugins/dummy/src/dummy.plugin.kts index eed2880d..ac7e9079 100644 --- a/game/src/plugins/dummy/src/dummy.plugin.kts +++ b/game/src/plugins/dummy/src/dummy.plugin.kts @@ -1,9 +1,12 @@ +import kotlinx.coroutines.experimental.* +import kotlinx.coroutines.experimental.channels.Channel +import kotlinx.coroutines.experimental.selects.select +import org.apollo.game.action.AsyncDistancedAction import org.apollo.game.action.DistancedAction 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.Player -import org.apollo.game.model.entity.Skill +import org.apollo.game.model.entity.* import org.apollo.net.message.Message /** @@ -15,7 +18,7 @@ on { ObjectActionMessage::class } .where { option == 2 && id in DUMMY_IDS } .then { DummyAction.start(this, it, position) } -class DummyAction(val player: Player, position: Position) : DistancedAction(0, true, player, position, DISTANCE) { +class DummyAction(val player: Player, position: Position) : AsyncDistancedAction(0, true, player, position, DISTANCE) { companion object { @@ -49,25 +52,19 @@ class DummyAction(val player: Player, position: Position) : DistancedAction= LEVEL_THRESHOLD) { - player.sendMessage("There is nothing more you can learn from hitting a dummy.") - } else { - skills.addExperience(Skill.ATTACK, EXP_PER_HIT) - } + val skills = player.skillSet - stop() + if (skills.getSkill(Skill.ATTACK).maximumLevel >= LEVEL_THRESHOLD) { + player.sendMessage("There is nothing more you can learn from hitting a dummy.") } else { - mob.sendMessage("You hit the dummy.") - mob.turnTo(this.position) - mob.playAnimation(PUNCH_ANIMATION) - - started = true + skills.addExperience(Skill.ATTACK, EXP_PER_HIT) } }