-
Notifications
You must be signed in to change notification settings - Fork 156
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Migrate KTS Plugins to KT for Compatibility and Future K2 Support #552
Comments
Proposal for replacementThis was written for PokeForce but was eventually scrapped in favor of a Java environment. The following code simply provides a "similar" but alternative way to construct plugins. This will require a bit more involvement to introduce to 2011Scape, and some more insight into its core. package org.pokeforce.game.plugin
import org.pokeforce.game.plugin.impl.Interaction
import org.pokeforce.model.World
import org.pokeforce.model.entity.Player
/**
* @author Alycia <https://github.com/alycii>
*/
abstract class Plugin(protected val world: World) {
private val interactions = mutableListOf<Interaction>()
abstract fun buildPlugin()
fun registerInteraction(interaction: Interaction) {
interactions.add(interaction)
}
fun handleEvent(event: GameEvent, player: Player): Boolean {
val interaction = interactions.firstOrNull { it.event.id == event.id }
interaction?.action?.invoke(player, event)
return interaction != null
}
} package org.pokeforce.game.plugin
import org.pokeforce.model.timer.TimerKey
/**
* @author Alycia <https://github.com/alycii>
*/
sealed class GameEvent {
abstract val id: String
data class TileInteraction(val mapId: Int, val x: Int, val y: Int, override val id: String = "TileInteraction${mapId}_${x}_$y") : GameEvent()
data class BagInteraction(val item: Int, override val id: String = "BagInteraction${item}") : GameEvent()
data class LoginPlugin(override val id: String = "login") : GameEvent()
data class TimerTrigger(val key: TimerKey, override val id: String = "timer_$key") : GameEvent()
data class NodeInteraction(val nodeType: String, override val id: String = "NodeInteraction_${nodeType}") : GameEvent()
} package org.pokeforce.plugin.api
import org.pokeforce.game.plugin.GameEvent
import org.pokeforce.game.plugin.Plugin
import org.pokeforce.game.plugin.impl.Interaction
import org.pokeforce.model.entity.Player
import org.pokeforce.model.timer.TimerKey
/**
* @author Alycia <https://github.com/alycii>
*/
object PluginExt {
fun Plugin.onTileInteraction(name: String = "", mapId: Int, x: Int, y: Int, action: Player.(GameEvent.TileInteraction) -> Unit) {
registerInteraction(Interaction(GameEvent.TileInteraction(mapId, x, y)) { player, event ->
if (event is GameEvent.TileInteraction) {
player.action(event)
}
})
}
fun Plugin.onBagInteraction(name: String = "", item: Int, action: Player.(GameEvent.BagInteraction) -> Unit) {
registerInteraction(Interaction(GameEvent.BagInteraction(item)) { player, event ->
if (event is GameEvent.BagInteraction) {
player.action(event)
}
})
}
fun Plugin.onNodeInteraction(name: String = "", nodeType: String, action: Player.(GameEvent.NodeInteraction) -> Unit) {
registerInteraction(Interaction(GameEvent.NodeInteraction(nodeType)) { player, event ->
if (event is GameEvent.NodeInteraction) {
player.action(event)
}
})
}
fun Plugin.onLogin(name: String = "", action: Player.(GameEvent.LoginPlugin) -> Unit) {
registerInteraction(Interaction(GameEvent.LoginPlugin()) { player, event ->
if (event is GameEvent.LoginPlugin) {
player.action(event)
}
})
}
fun Plugin.onTimer(name: String = "", key: TimerKey, action: Player.(GameEvent.TimerTrigger) -> Unit) {
registerInteraction(Interaction(GameEvent.TimerTrigger(key)) { player, event ->
if (event is GameEvent.TimerTrigger) {
player.action(event)
}
})
}
} package org.pokeforce.plugin.skills.foraging
import com.badlogic.gdx.math.MathUtils
import org.pokeforce.game.plugin.Plugin
import org.pokeforce.message.impl.Emote
import org.pokeforce.model.Location
import org.pokeforce.model.World
import org.pokeforce.model.entity.Player
import org.pokeforce.model.map.objects.ForageNodeObject
import org.pokeforce.model.skill.Skills
import org.pokeforce.plugin.api.PluginExt.onBagInteraction
/**
* @author Alycia <https://github.com/alycii>
*/
class ForagingAction(world: World) : Plugin(world) {
companion object {
private const val TOOL_USE_DELAY = 150
private val RESPAWN_DELAY = 25_000..100_000
}
override fun buildPlugin() {
onBagInteraction("trowel", item = 914) {
val targetLocation = location.getFacingCoordinates(lastFacingDirection)
val node = getNode(targetLocation)
if (node == null) {
emote(Emote.CANT_DIG)
return@onBagInteraction
}
val pickable = Pickables.findByNode(node.nodeType)
if (pickable == null) {
emote(Emote.CANT_DIG)
return@onBagInteraction
}
if (!canPick(this, pickable)) {
emote(Emote.CANT_DIG)
return@onBagInteraction
}
queue {
emote(Emote.DIG)
wait(TOOL_USE_DELAY)
node.animate(1)
giveAndNotify(pickable.reward, node.location)
wait(5)
node.invisible = true
addXp(Skills.FORAGING.ordinal, pickable.experience)
occupiedPickableLocations.remove(targetLocation)
world.queue {
wait(MathUtils.random(25_000, 100_000))
refillNode(this@onBagInteraction)
}
}
}
}
private fun canPick(player: Player, pickable: Pickables): Boolean {
if (player.skillSet[Skills.FORAGING.ordinal].currentLevel < pickable.requiredLevel) {
player.sendMessage("You need level ${pickable.requiredLevel} Foraging to forage this item.")
return false
}
return true
}
private fun refillNode(player: Player) {
val availableNodes = ForageNodeObject.forageNodes.filter {
val newNodeLocation = Location(it.x, it.y, 0)
!player.occupiedPickableLocations.contains(newNodeLocation)
}
if (availableNodes.isNotEmpty()) {
val newNode = ForageNodeObject.forageNodes.random()
val newNodeLocation = Location(newNode.x, newNode.y, 0)
player.occupiedPickableLocations.add(newNodeLocation)
player.buildResourceNode(newNode.name, x = newNode.x, y = newNode.y, map = 0, definitionId = newNode.definitionId)
println("New node has spawned ${newNode.name}: ${newNode.x}, ${newNode.y}")
}
}
} private fun handleNodeInteraction(client: Client): Boolean {
val targetLocation = client.location.getFacingCoordinates(client.lastFacingDirection)
val node = client.getNode(targetLocation) ?: return false
return client.dispatchEvent(GameEvent.NodeInteraction(node.nodeType))
} /**
* Dispatches an event from this player using the world's plugin manager.
*/
fun dispatchEvent(event: GameEvent): Boolean {
return world.pluginManager.dispatchEvent(event, this)
} val pluginManager = PluginManager()
/**
* Loads all plugins using reflection to automatically discover plugin classes in a specified package.
* This function instantiates each plugin and registers it to the PluginManager for event handling.
*/
fun loadPlugins(world: World) {
val reflections = Reflections(
ConfigurationBuilder().setUrls(ClasspathHelper.forPackage("org.pokeforce.plugin")).setScanners(SubTypesScanner()).filterInputsBy(FilterBuilder().includePackage("org.pokeforce.plugin"))
)
val pluginClasses = reflections.getSubTypesOf(Plugin::class.java)
pluginClasses.forEach { pluginClass ->
val pluginInstance = pluginClass.getDeclaredConstructor(World::class.java).newInstance(world)
pluginManager.registerPlugin(pluginInstance)
}
} package org.pokeforce.game.plugin
import org.pokeforce.model.entity.Player
/**
* Handles the registration and event dispatching for plugins.
*/
class PluginManager {
val plugins = mutableListOf<Plugin>()
fun registerPlugin(plugin: Plugin) {
plugins.add(plugin)
plugin.buildPlugin()
}
fun dispatchEvent(event: GameEvent, player: Player): Boolean {
plugins.forEach { plugin ->
if (plugin.handleEvent(event, player)) {
return true
}
}
return false
}
} Please let me know if I'm missing anything from this code dump. |
Hmm. Very interesting @alycii . I personally prefer the rsmod style of a separate map per different plugin/interaction type. This simplifies it to a single lookup and you can get the safety of easily having debug out for binding duplicate plugins/overriding behavior. Storing in a list like that which gets iterated it looks like there could be issues with the order that plugins get registered. For example potentially causing issues if a repackage occurs and then the scanner registers them in a different order (assuming you have the same ids bound across 2 different plugins). I do like the "buildPlugin" method. It is nice to move the binding of the handlers to that rather than the init{} block like you would get with a direct conversion of the existing rsmod kts. If the binding of the handlers gets desynched from the scanning/instantiation then it would also be easier to multithread safely. As the project size increases that might help. On my rsmod fork i basically just deleted this class And then the kotlinscript references from any of the gradle files. |
Just as an update on this: https://blog.jetbrains.com/kotlin/2024/11/state-of-kotlin-scripting-2024/ Probably will be in the cards to get off kts in the near future. When i did it on my src, it took maybe an hour to migrate the kts, but id done it via hand. A macro or something setup could be nice. |
Yep, because of that announcement many people in this scene are switching away from KTS. It's likely the best move going forward. |
Added a screenshot from R-S on the topic for archive purposes. |
We are proposing to migrate our KTS (Kotlin Script) plugins to regular Kotlin (KT) in order to ensure better compatibility with the latest versions of IntelliJ IDEA and to make it easier to add K2 support in the future. Below is the reasoning behind this proposed migration.
Context and Rationale:
IDE Support:
K2 Compatibility:
Boilerplate Reduction vs. Fragility:
TestPlugin: Plugin
, this benefit does not justify the instability of using KTS, especially as it relates to newer Kotlin versions.Dependency Injection (DI) Issues:
KTS is Heavily "Magic" Dependent:
Neglected Maintenance:
Limited Usage Scenarios:
The text was updated successfully, but these errors were encountered: