So you want to write plugins in Scala? Great! Or not so great? Scala runtime classes are not on the class/module path by default. Server administrators
could add them using the -classpath commandline option, but in reality most bukkit servers run in managed environments by minecraft-specialized server hosts. The standard workaround for this problem is to include the classes in your own plugin and relocate them using a shading plugin in your build process. While this
does work, it means that Scala library classes are no longer shared between plugins, making them incompatible because the scala classes will be loaded by different classloaders. Since Spigot 1.16.5 there is the option to include the dependency in the plugin.yml, but this approach has the same problems. And there is the problem of unnecessary ram and disc usage too. As of Scala 2.12.6, the standard library has a size of 3.5 MB. The reflection library is another 5 MB. Using both libraries in multiple plugins results unnecessarily large plugins sizes, while really those Scala classes should only be loaded once. Introducing…
ScalaLoader
ScalaLoader uses a custom PluginLoader that loads the Scala runtime classes for you! You can use this Giter8 template to get started quickly!
Pros
Cons
Caveats
I know what I'm doing, just give me what I need!
Javadocs
Available on GitHub Pages.
Example plugin
Depending on a ScalaPlugin from a JavaPlugin
plugin.yml:
Java code:
Commands & Permissions
Since this plugin is meant for other developers, the commands and permissions are probably not very useful to end-users. But for completeness here they are:
Usage statistics
ScalaLoader collects some anonymous metrics and sends that to bStats.org. In addition to the standard charts, you can see here which Scala versions are most in use. Opting out can be done by editing the <server_root>/plugins/bStats/config.yml file.
Chat & Support
GitHub issues is the place for bug reports and feature requests, but if you just want to chat about the project, or want to know more about how to use it, you can reach me via Discord: https://discord.gg/pb2cBBKdwq
ScalaLoader
ScalaLoader uses a custom PluginLoader that loads the Scala runtime classes for you! You can use this Giter8 template to get started quickly!
Pros
- Write idiomatic Scala!
- Able to use a true singleton for your plugin's main class, through Scala's object!
- Supports multiple Scala versions at the same time! ScalaLoader uses classloader magic to make that work.
- Supports custom scala version by adding/changing URLs in the config file.
- Annotation-based detection of the plugin's main class - no need to write a plugin.yml. If you wish to use a plugin.yml still, you can, however I always found it a pain.
- A boilerplate-free event api! No need to implement #getHandlerList(), #getHandlers(), #isCancelled() and #setCancelled(boolean)!
- A boilerplate-free ConfigurationSerializable api! No need to implement #serialize() and #deserialize(Map<String,Object>)! (Scala collection support is a work in progress)
Cons
- Scala library classes are only accessible to ScalaPlugins (You can still write them in Java though).
- ScalaLoader uses a lot of reflection/injection hacks to make ScalaPlugins accessible to JavaPlugins. This may not work correctly if there is a SecurityManager in place.
Caveats
- ScalaPlugin jars go in the <server_root>/plugins/ScalaLoader/scalaplugins/ directory. I made this choice so that ScalaLoader doesn't try to load JavaPlugins that are loaded already.
- By default ScalaLoader downloads the scala libraries from over the network the first time. I made this choice to provide the best possible user experience for server admins. If you're very security-focused you might want to provide your own jars by changing the URLs to "file://some/location.jar". The scala classes aren't actually loaded until there's a plugin that needs them, so you can run ScalaLoader once without ScalaPlugins to generate the config.
I know what I'm doing, just give me what I need!
Spoiler: Dependency information
Maven:
<repositories>
<repository>
<id>jannyboy11-minecraft-repo </id>
<url>https://repo.repsy.io/mvn/jannyboy11/minecraft/ </url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>com.janboerman.scalaloader </groupId>
<artifactId>ScalaLoader </artifactId>
<version>0.18.15-SNAPSHOT </version>
<scope>provided </scope>
</dependency>
</dependencies>
SBT:
resolvers + = "jannyboy11-minecraft-repo" at "https://repo.repsy.io/mvn/jannyboy11/minecraft"
libraryDependencies + = "com.janboerman.scalaloader" % "ScalaLoader" % "0.18.15-SNAPSHOT" % "provided"
Code (XML):
<repositories>
<repository>
<id>jannyboy11-minecraft-repo </id>
<url>https://repo.repsy.io/mvn/jannyboy11/minecraft/ </url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>com.janboerman.scalaloader </groupId>
<artifactId>ScalaLoader </artifactId>
<version>0.18.15-SNAPSHOT </version>
<scope>provided </scope>
</dependency>
</dependencies>
Code (Scala):
resolvers + = "jannyboy11-minecraft-repo" at "https://repo.repsy.io/mvn/jannyboy11/minecraft"
libraryDependencies + = "com.janboerman.scalaloader" % "ScalaLoader" % "0.18.15-SNAPSHOT" % "provided"
Javadocs
Available on GitHub Pages.
Example plugin
Code (Scala):
package xyz.
janboerman.
scalaloader.
example.
scala
import org. bukkit. ChatColor
import org. bukkit. command. {CommandSender, Command }
import org. bukkit. event. {EventHandler, Listener }
import org. bukkit. event. player. PlayerJoinEvent
import org. bukkit. permissions. PermissionDefault
import xyz. janboerman. scalaloader. plugin. ScalaPluginDescription. {Command => SPCommand, Permission => SPPermission }
import xyz. janboerman. scalaloader. plugin. {ScalaPlugin, ScalaPluginDescription }
import xyz. janboerman. scalaloader. plugin. description. {Scala, ScalaVersion, Api, ApiVersion }
@Scala (version = ScalaVersion. v2_13_10 )
@Api (ApiVersion. v1_19 )
object ExamplePlugin
extends ScalaPlugin ( new ScalaPluginDescription ( "ScalaExample", "0.1-SNAPSHOT" )
. commands ( new SPCommand ( "foo" )
. permission ( "scalaexample.foo" ) )
. permissions ( new SPPermission ( "scalaexample.foo" )
. permissionDefault (PermissionDefault. TRUE ) ) )
with Listener {
getLogger ( ). info ( "ScalaExample - I am constructed!" )
override def onLoad ( ) : Unit = {
getLogger. info ( "ScalaExample - I am loaded!" )
}
override def onEnable ( ) : Unit = {
getLogger. info ( "ScalaExample - I am enabled!" )
getServer. getPluginManager. registerEvents ( this, this )
}
override def onDisable ( ) : Unit = {
getLogger. info ( "ScalaExample - I am disabled!" )
}
@EventHandler
def onJoin (event : PlayerJoinEvent ) : Unit = {
event. setJoinMessage (s "${ChatColor.GREEN}Howdy ${event.getPlayer.getName}!" )
}
override def onCommand (sender : CommandSender, command : Command, label : String, args : Array [String ] ) : Boolean = {
sender. sendMessage ( "Executed foo command!" )
true
}
def getInt ( ) = 42
}
import org. bukkit. ChatColor
import org. bukkit. command. {CommandSender, Command }
import org. bukkit. event. {EventHandler, Listener }
import org. bukkit. event. player. PlayerJoinEvent
import org. bukkit. permissions. PermissionDefault
import xyz. janboerman. scalaloader. plugin. ScalaPluginDescription. {Command => SPCommand, Permission => SPPermission }
import xyz. janboerman. scalaloader. plugin. {ScalaPlugin, ScalaPluginDescription }
import xyz. janboerman. scalaloader. plugin. description. {Scala, ScalaVersion, Api, ApiVersion }
@Scala (version = ScalaVersion. v2_13_10 )
@Api (ApiVersion. v1_19 )
object ExamplePlugin
extends ScalaPlugin ( new ScalaPluginDescription ( "ScalaExample", "0.1-SNAPSHOT" )
. commands ( new SPCommand ( "foo" )
. permission ( "scalaexample.foo" ) )
. permissions ( new SPPermission ( "scalaexample.foo" )
. permissionDefault (PermissionDefault. TRUE ) ) )
with Listener {
getLogger ( ). info ( "ScalaExample - I am constructed!" )
override def onLoad ( ) : Unit = {
getLogger. info ( "ScalaExample - I am loaded!" )
}
override def onEnable ( ) : Unit = {
getLogger. info ( "ScalaExample - I am enabled!" )
getServer. getPluginManager. registerEvents ( this, this )
}
override def onDisable ( ) : Unit = {
getLogger. info ( "ScalaExample - I am disabled!" )
}
@EventHandler
def onJoin (event : PlayerJoinEvent ) : Unit = {
event. setJoinMessage (s "${ChatColor.GREEN}Howdy ${event.getPlayer.getName}!" )
}
override def onCommand (sender : CommandSender, command : Command, label : String, args : Array [String ] ) : Boolean = {
sender. sendMessage ( "Executed foo command!" )
true
}
def getInt ( ) = 42
}
plugin.yml:
Code (YAML):
name
: DummyPlugin
version : 1.0
main : xyz.janboerman.dummy.dummyplugin.DummyPlugin
depend : [ScalaLoader ]
softdepend : [ScalaExample ] #A hard dependency will not work! Your plugin will not load!
version : 1.0
main : xyz.janboerman.dummy.dummyplugin.DummyPlugin
depend : [ScalaLoader ]
softdepend : [ScalaExample ] #A hard dependency will not work! Your plugin will not load!
Code (Java):
package
xyz.janboerman.dummy.dummyplugin
;
import org.bukkit.plugin.java.JavaPlugin ;
import xyz.janboerman.scalaloader.compat.IScalaPluginLoader ;
import xyz. janboerman. scalaloader. example. scala. ExamplePlugin$ ;
public final class DummyPlugin extends JavaPlugin {
@Override
public void onEnable ( ) {
//get the plugin instance
ExamplePlugin$ plugin = (ExamplePlugin$ ) getServer ( ). getPluginManager ( ). getPlugin ( "ScalaExample" ) ;
//make sure all classes from the scala plugin can be accessed
IScalaPluginLoader. openUpToJavaPlugin (plugin, this ) ;
//do whatever you want afterwards!
getLogger ( ). info ( "We got " + plugin. getInt ( ) + " from Scala!" ) ;
}
}
import org.bukkit.plugin.java.JavaPlugin ;
import xyz.janboerman.scalaloader.compat.IScalaPluginLoader ;
import xyz. janboerman. scalaloader. example. scala. ExamplePlugin$ ;
public final class DummyPlugin extends JavaPlugin {
@Override
public void onEnable ( ) {
//get the plugin instance
ExamplePlugin$ plugin = (ExamplePlugin$ ) getServer ( ). getPluginManager ( ). getPlugin ( "ScalaExample" ) ;
//make sure all classes from the scala plugin can be accessed
IScalaPluginLoader. openUpToJavaPlugin (plugin, this ) ;
//do whatever you want afterwards!
getLogger ( ). info ( "We got " + plugin. getInt ( ) + " from Scala!" ) ;
}
}
Commands & Permissions
Since this plugin is meant for other developers, the commands and permissions are probably not very useful to end-users. But for completeness here they are:
Spoiler: Commands & Permissions
- /resetScalaUrls <scala version>: Resets the URLs in the config for a version of Scala. Useful if you overwrote some of them. If 'all' is provided as a scala version then all URLs will be reset. Permission: scalaloader.resetscalaurls
- /listScalaPlugins: Outputs the names of all scala plugins. Binary-compatible plugins are shown on the same line. Permission: scalaloader.listscalaplugins
- /dumpClass <plugin> <class file> <format>?: Outpus the bytecode of a class to the console in a textual representation. This command is mostly useful for myself, but if you fancy diving into Java bytecode then more power to you! Permission: scalaloader.dumpclass
- /setDebug <class name>: Marks or unmarks a class as a class that should be dumped to the console by the classloader when it is loaded. A list is saved in the debug.yml file. Permission: scalaloader.setdebug
- /classMembers <class name>: Dumps fields and methods visible to ScalaLoader to the command sender. Permission: scalaloader.classmembers
Usage statistics
ScalaLoader collects some anonymous metrics and sends that to bStats.org. In addition to the standard charts, you can see here which Scala versions are most in use. Opting out can be done by editing the <server_root>/plugins/bStats/config.yml file.
Chat & Support
GitHub issues is the place for bug reports and feature requests, but if you just want to chat about the project, or want to know more about how to use it, you can reach me via Discord: https://discord.gg/pb2cBBKdwq
Quick facts
- Edition: Minecraft Java
- File type: .jar
- Minecraft versions listed: 1.8, 1.12, 1.15
- How to install: Install the matching mod loader (Forge, Fabric or NeoForge) for your Minecraft version. → Download the .jar. → Put it in the .minecraft/mods folder and launch that loader profile.
- Where to get it: Opens on Spigot — not every file is mirrored on our own servers.
Install steps are the general flow for this file type — How to install Minecraft Java mods & modpacks walks through it step by step.
ScalaLoader is a free Minecraft Java mod. Compatible with Minecraft 1.8, 1.12, 1.15, 1.16 and newer. Downloaded 3,549 times (via Spigot). Download it and open it directly in the game.