Files
2009scape/Server/src/main/core/plugin/ClassScanner.kt
T
Ceikry 661390e66f Implemented timer subsystem to eventually replace pulses
Authentic subsystem supports saving/loading arbitrary data and resuming timer countdowns
Lots of documented CAPI functions for working with these new timers
Converted poison to new timers
Converted disease to new timers
Converted farming to new timers (CropGrowth and Compost)
Farming now syncs with 5 minute intervals on realtime clocks (authentic)
Converted seedling growth to new timers
Converted shooting star mining bonus to new timers
Converted entity freezing to new timers
Converted incubation to new timers
Incubation now supports using both incubators (so 2 eggs at once)
Converted miasmic states to the new timers
Converted god spell charged state to new timers
Converted teleblock to new timers
Converted skulled state to new timers
Converted Enhanced Excalibur special attack effect to new timers
Converted passive stat restoration to timers
Converted multicannon firing and decay to a timer
2023-07-29 04:51:06 +00:00

222 lines
9.0 KiB
Kotlin

package core.plugin
import core.api.*
import core.game.activity.ActivityManager
import core.game.activity.ActivityPlugin
import core.game.node.entity.Entity
import core.game.node.entity.player.info.login.LoginConfiguration
import core.game.node.entity.player.link.quest.Quest
import core.game.node.entity.player.link.quest.QuestRepository
import core.game.world.map.Location
import core.game.world.map.zone.MapZone
import core.game.world.map.zone.ZoneBuilder
import io.github.classgraph.ClassGraph
import io.github.classgraph.ClassInfo
import io.github.classgraph.ScanResult
import core.game.system.timer.*
import core.game.bots.PlayerScripts
import core.game.interaction.InteractionListener
import core.game.interaction.InterfaceListener
import core.game.node.entity.npc.NPCBehavior
import core.game.node.entity.player.info.login.PlayerSaveParser
import core.game.node.entity.player.info.login.PlayerSaver
import core.tools.SystemLogger
import core.tools.SystemLogger.logStartup
import core.game.world.GameWorld
import core.tools.Log
import java.util.function.Consumer
/**
* A class used to reflectively scan the classpath and load classes
* @author Ceikry
*/
object ClassScanner {
var disabledPlugins = HashMap<String, Boolean>()
/**
* The amount of content interfaces loaded.
*/
var amountLoaded = 0
private set
var numPlugins = 0
private set
/**
* The currently loaded plugin names.
*/
private var loadedPlugins: MutableList<String>? = ArrayList()
/**
* The last loaded plugin.
*/
private val lastLoaded: String? = null
lateinit var scanResults: ScanResult
@JvmStatic fun scanClasspath() {
scanResults = ClassGraph().enableClassInfo().enableAnnotationInfo().scan()
}
/**
* Scan the classpath for reflection-loaded content classes such as listeners, "plugins", etc
*/
@JvmStatic
fun loadPureInterfaces() {
try {
loadContentInterfacesFrom(scanResults)
logStartup("Loaded $amountLoaded content interfaces.")
} catch (t: Throwable) {
log(this::class.java, Log.ERR, "Error initializing Plugins -> " + t.localizedMessage + " for file -> " + lastLoaded)
t.printStackTrace()
} catch (e: Exception) {
log(this::class.java, Log.ERR, "Error initializing Plugins -> " + e.localizedMessage + " for file -> " + lastLoaded)
e.printStackTrace()
}
}
fun loadTimers () {
scanResults.getSubclasses ("core.game.system.timer.RSTimer").filter { !it.isAbstract }.forEach {
try {
val clazz = it.loadClass().newInstance() as RSTimer
TimerRegistry.registerTimer (clazz)
} catch (e: Exception) {
log(this::class.java, Log.ERR, "Error registering timer instance: ${it.simpleName}")
e.printStackTrace()
}
}
}
private fun loadContentInterfacesFrom(scanResults: ScanResult) {
scanResults.getClassesImplementing("core.api.ContentInterface").filter { !it.isAbstract }.forEach {
try {
val clazz = it.loadClass().newInstance()
if(clazz is LoginListener) GameWorld.loginListeners.add(clazz)
if(clazz is LogoutListener) GameWorld.logoutListeners.add(clazz)
if(clazz is TickListener) GameWorld.tickListeners.add(clazz)
if(clazz is StartupListener) GameWorld.startupListeners.add(clazz)
if(clazz is ShutdownListener) GameWorld.shutdownListeners.add(clazz)
if(clazz is PersistWorld) GameWorld.worldPersists.add(clazz)
if(clazz is InteractionListener) clazz.defineListeners().also { clazz.defineDestinationOverrides() }
if(clazz is InterfaceListener) clazz.defineInterfaceListeners()
if(clazz is Commands) clazz.defineCommands()
if(clazz is NPCBehavior) NPCBehavior.register(clazz.ids, clazz)
if(clazz is PersistPlayer) {
PlayerSaver.contentHooks.add(clazz)
PlayerSaveParser.contentHooks.add(clazz)
}
if(clazz is MapArea)
{
val zone = object : MapZone(clazz.javaClass.simpleName + "MapArea", true, *clazz.getRestrictions()){
override fun enter(e: Entity?): Boolean {
clazz.areaEnter(e ?: return super.enter(null))
return super.enter(e)
}
override fun leave(e: Entity?, logout: Boolean): Boolean {
clazz.areaLeave(e ?: return super.leave(null, logout), logout)
return super.leave(e, logout)
}
override fun move(e: Entity?, from: Location?, to: Location?): Boolean {
if(e != null && from != null && to != null) clazz.entityStep(e, to, from)
return super.move(e, from, to)
}
}
for(border in clazz.defineAreaBorders()) zone.register(border)
ZoneBuilder.configure(zone)
log(this::class.java, Log.FINE, "Configured zone: ${clazz.javaClass.simpleName + "MapArea"}")
MapArea.zoneMaps[clazz.javaClass.simpleName + "MapArea"] = zone
}
amountLoaded++
} catch (e: Exception) {
log(this::class.java, Log.ERR, "Error loading content: ${it.simpleName}, ${e.localizedMessage}")
e.printStackTrace()
}
}
}
@JvmStatic
fun loadSideEffectfulPlugins() {
loadedPlugins!!.clear()
loadPluginsFrom(scanResults)
logStartup("We still have $numPlugins legacy plugins being loaded.")
}
private fun loadPluginsFrom(scanResults: ScanResult) {
scanResults.getClassesWithAnnotation("core.plugin.Initializable").forEach(Consumer { p: ClassInfo ->
try {
val clazz = p.loadClass().getDeclaredConstructor().newInstance()
if (clazz is Plugin<*>) {
definePlugin(clazz)
}
} catch (t: Throwable) {
log(this::class.java, Log.ERR, "Failed to load plugin ${p.name}.");
if (t is NoSuchMethodException && p.superclass.simpleName == core.game.dialogue.DialoguePlugin::class.simpleName) {
log(this::class.java, Log.ERR,
"Make sure the constructor signature matches " +
"`${p.simpleName}(player: Player? = null) : DialoguePlugin(player)'."
)
}
t.printStackTrace()
}
})
scanResults.getClassesWithAnnotation("core.game.bots.PlayerCompatible").forEach { res ->
val description = res.getAnnotationInfo("core.game.bots.ScriptDescription").parameterValues[0].value as Array<String>
val identifier = res.getAnnotationInfo("core.game.bots.ScriptIdentifier").parameterValues[0].value.toString()
val name = res.getAnnotationInfo("core.game.bots.ScriptName").parameterValues[0].value.toString()
PlayerScripts.identifierMap[identifier] = PlayerScripts.PlayerScript(identifier, description, name, res.loadClass())
}
}
/**
* Defines a list of plugins.
* @param plugins the plugins.
*/
@JvmStatic
fun definePlugins(vararg plugins: Plugin<*>) {
val pluginsLength = plugins.size
for (i in 0 until pluginsLength) {
val p = plugins[i]
definePlugin(p)
}
}
/**
* Defines the plugin.
* @param plugin The plugin.
*/
@JvmStatic
fun definePlugin(plugin: Plugin<*>) {
try {
var manifest = plugin.javaClass.getAnnotation(PluginManifest::class.java)
if (manifest == null) {
manifest = plugin.javaClass.superclass.getAnnotation(PluginManifest::class.java)
} else {
if (disabledPlugins[manifest.name] != null) {
return
}
}
if (manifest == null || manifest.type == PluginType.ACTION) {
plugin.newInstance(null)
} else {
when (manifest.type) {
PluginType.DIALOGUE -> (plugin as core.game.dialogue.DialoguePlugin).init()
PluginType.ACTIVITY -> ActivityManager.register(plugin as ActivityPlugin)
PluginType.LOGIN -> LoginConfiguration.getLoginPlugins().add(plugin as Plugin<Any?>)
PluginType.QUEST -> {
plugin.newInstance(null)
QuestRepository.register(plugin as Quest)
}
else -> log(this::class.java, Log.WARN, "Unknown Manifest: " + manifest.type)
}
}
numPlugins++
} catch (e: Throwable) {
e.printStackTrace()
}
}
}