RuneScape 2 Development > Server Tutorials

Groovy Scripting for Beginners

Pages: (1/3) >>>

Pure_:

Groovy Scripting

In recent years the use of scripting languages in RSPS projects has blown up. The scene has typically prefered Jython or JRuby, but recently Groovy has also been explored.
The use of scripting in our projects hasn't typically been to create plugins though, but rather to migrate code away from the core of a server, i.e. modules. I will explore both avenues with this 'tutorial'.
You may notice that some of my naming/code is influenced by RuneSource, this is by design.

Requirements

* JDK 1.6
* Some prior java knowledge (as long as you know the basics about OOP you should be fine - i.e. classes, packaging, dependencies)
* An IDE (try Netbeans, Eclipse or IntelliJ IDEA)
* An RSPS (with source)
* Download and add the Groovy[/url] binaries to your list of dependencies
Core Entities

* Plugin - a base class which plugins will inherit
* PluginHandler - the plugin handler (for standard processing, loading, unloading, etc)
* PluginBridge - a bridge between the server and invokable plugins (for non-standard processing)
Part I: Server Setup
The first things we will need to do is prepare our project for the implementation of scripting. Firstly you should add some folders to your server's root. 

My setup looks like the following: 
root 
|--- plugins/ 
    |--- bindings/ 
    |--- scripts/ 

Now we will add a file which will determine which scripts to load when our server starts, so create a file called "plugins.ini" under the root folder. The format of this file is simple, each line will contain a script's location relative to the plugin's base directory (which we will define in the PluginHandler later).

A sample for adding two plugins to the file would be:

--- Code: Text ---scripts/SamplePluginbindings/CommandHandler

Before you continue, make sure you add the Groovy class library to your project/its classpath/etc if you haven't done so already.


Next we need to add the plugin initialisation logic and on-tick processing. Firstly locate where your server initialises modules, i.e. trace it from your main function (a good place to start may be a class called Server). Then add our plugin loading code there.


--- Code: Java ---void Init() {    // ...    ItemDefs.load();    Map.cacheRegions();    PluginHandler.loadPlugins("./plugins.ini"); // our initialisation function    // ...}
Finally we must add code to have the plugin tick function called from the server's processing logic (a good place to start may be a class called PlayerHandler). You should be able to add this anywhere in there but pick somewhere sensible, i.e. at the end.


--- Code: Java ---void Tick() {  // ...  PlayerHandler.process()  NpcHandler.process();  PluginHandler.tick();  // ...}
Part II: Writing the PluginHandler and Plugin classes
Plugin
We will now continue with writing a base class for plugins. Implementations of this class will need to be able to handle the loading and unloading of the plugin, have a general processing function called tick (for scripts) and an instance of the Groovy object of the script. This all leads to something like the following:


--- Code: Java ---import groovy.lang.GroovyObject; /** * Plugin base class. */public abstract class Plugin {        /**         * An instance of this script as a Groovy object.         */        private GroovyObject instance;         /**         * Called every time the server performs a tick.         * @throws Exception If the plugin throws any form of exception         */        public abstract void tick() throws Exception;         /**         * Called when the plugin is enabled.         * @param pluginName The name of this plugin from the plugins list file         * @throws Exception If the plugin throws any form of exception         */        public abstract void onEnable(String pluginName) throws Exception;         /**         * Called when the plugin is disabled.         * @throws Exception If the plugin is disabled         */        public abstract void onDisable() throws Exception;         /**         * Get the groovy object instance of the plugin.         * @return Groovy instance of plugin instance         */        public GroovyObject getInstance() {                return instance;        }         /**         * Set the groovy object instance of this plugin         * @param instance Groovy instance of plugin instance         */        public void setInstance(GroovyObject instance) {                if (this.instance != null) {                        throw new IllegalStateException("Plugin already loaded.");                }                this.instance = instance;        }}
PluginHandler
Now we will need to assemble the core of our system, the plugin handler. We need to declare the location of the plugins, alternatively you could pass this through the class constructor. We also need a function to load the script instances and initialise them using Groovy.


--- Code: Java ---import groovy.lang.GroovyClassLoader;import groovy.lang.GroovyObject; import java.io.BufferedReader;import java.io.File;import java.io.FileReader;import java.util.HashMap;import java.util.Map; /** * The plugin handler, used for managing plugins and their execution. */public final class PluginHandler {         /**         * The base directory of all plugins.         */        private static final String PLUGIN_DIRECTORY = "./plugins/";         /**         * The Groovy class loader.         */        private static final GroovyClassLoader classLoader = new GroovyClassLoader();                /**         * A collection of all registered plugins.         */        private static HashMap<String, Plugin> plugins = new HashMap<String, Plugin>();         /**         * Processes on-tick execution for all registered plugins.         */        public static void tick() throws Exception {                synchronized (plugins) {                        for (Plugin plugin : plugins.values()) { // looping through all registered plugins                                plugin.tick(); // calling the tick function                        }                }        }         /**         * Invokes a method from the given plugin.         * @param pluginName plugin name         * @param method     method name         * @param args       arguments         */        public static void invokeMethod(String pluginName, String method, Object... args) {                // Attempting to fetch the plugin                Plugin plugin = plugins.get(pluginName);                 if (plugin == null) {                        return;                }                 // Invoking the method                plugin.getInstance().invokeMethod(method, args);        }         /**         * Loads all plugins.         *         * @param pluginsFile a text file containing a list of plugins to load         * @throws Exception         */        public static void loadPlugins(String pluginsFile) throws Exception {                File file = new File(pluginsFile); // loading the plugins file                BufferedReader reader = new BufferedReader(new FileReader(file)); // preparing a reader, so that we can traverse it                String pluginName; // the current plugins name                 while ((pluginName = reader.readLine()) != null) { // each line we read has the pluginName set to it                        if (pluginName.trim().length() == 0) // skipping empty lines                                continue;                         // Loading the plugin instance                        Class cls = classLoader.parseClass(new File(PLUGIN_DIRECTORY + pluginName + ".groovy")); // parsing the current class                        GroovyObject obj = (GroovyObject) cls.newInstance(); // initialising a new groovy instance                        Plugin plugin = (Plugin) obj; // casting the object to a plugin, as per our interface                        plugin.setInstance(obj); // setting the instance of this plugin to the one we created earlier so that we do more than just call the plugin base functionality                        register(pluginName.replace('/', '.'), plugin); // we clean up the plugin name from containing slashes to dots to separate namespaces, this gives it a java feel when invoking                }        }         /**         * Registers a plugin and calls the plugin's onEnable method.         * @param name   The plugin name         * @param plugin The plugin to register         */        public static void register(String name, Plugin plugin) {                try {                        plugin.onEnable(name); // performing on load tasks                         synchronized (plugins) {                                plugins.put(name, plugin); // adding the plugin to the plugins list                        }                } catch (Exception ex) {                        ex.printStackTrace();                }        }         /**         * Unregisters a plugin and calls the plugin's onDisable method.         * @param plugin The plugin to unregister         */        public static void unregister(Plugin plugin) {                for (Map.Entry<String, Plugin> entry : plugins.entrySet()) {                        if (entry.getValue().equals(plugin)) {                                unregister(entry.getKey()); // unregistering all instances of the plugin, regardless of key                        }                }        }         /**         * Unregisters a plugin and calls the plugin's onDisable method.         * @param name The plugin name to unregister         */        public static void unregister(String name) {                try {                        plugins.get(name).onDisable(); // performing on unload tasks                         synchronized (plugins) {                                plugins.remove(name); // removing the plugin from the plugins list                        }                } catch (Exception ex) {                        ex.printStackTrace();                }        }}
Part III: PluginBridge
The purpose of the plugin bridge is to allow us to do more than just call functions defined in the Plugin base class. It must literally as a bridge between our server's core and bindings which need to be accessed directly from it.


--- Code: Java ---import ... /** * A bridge between invokable plugins and the core of the server. */public final class PluginBridge {         /**         * A list of currently registered bindings.         */        private static HashMap<String, String> bindings = new HashMap<String, String>();                /**         * A key for the command handler binding (to be used with the bindings map), so that we can easily invoke it from the core.         */        public static final String COMMAND_HANDLER_BINDING_KEY = "bindings.packets.CommandHandler"; // slashes replaced with dots as we did before         /**         * Registers a binding.         *         * @param binding binding internal name         * @param pluginName plugin name         */        public static void registerBinding(String binding, String pluginName) { // this will be called from the onEnable function of binding scripts                bindings.put(binding, pluginName); // putting the binding mapping, binding_name -> plugin_name        }         /**         * An implementation of the handle command function.         * @returns execution could occur or not         */        public static boolean handleCommand(Player player, String keyword, String[] args) {                if (!bindings.containsKey(COMMAND_HANDLER_BINDING_KEY)) { // checking if the key is binded to a script                        return false;                }                PluginHandler.invokeMethod(bindings.get(COMMAND_HANDLER_BINDING_KEY), "handle", player, keyword, args); // calling it with our arguments                return true;        }}
Part IV: Plugin Examples
Now we can create some plugins. We had split our plugins directory into two directories, bindings and scripts. I will give examples of each. The point of a binding is to extend the core functionality of the server directly whereas plugins themselves should typically perform other tasks, i.e. http server serving of data.

Binding: Command Handler (plugins/bindings/CommandHandler.groovy)
You will need to make sure the PluginBridge definition for this binding is called from the server's core upon receiving a command packet, for it to work obviously.


--- Code: Groovy ---import ... class CommandHandler extends Plugin { // our class must extend Plugin         // this is the entry-point into this plugin, we will call this from the plugin bridge whenever necessary        void handle(Player player, String keyword, String[] args) {                PlayerAttributes attributes = player.getAttributes();                 if (keyword.equals("master")) {                        for (int i = 0; i < attributes.getSkills().length; i++) {                                attributes.getSkills()[i] = 99;                                attributes.getExperience()[i] = 200000000;                        }                        player.sendSkills();                }                 if (keyword.equals("pickup")) {                        int id = Integer.parseInt(args[0]);                        int amount = 1;                        if (args.length > 1) {                                amount = Integer.parseInt(args[1]);                        }                        attributes.addInventoryItem(id, amount, this);                        player.sendInventory();                }                 if (keyword.equals("tele")) {                        int x = Integer.parseInt(args[0]);                        int y = Integer.parseInt(args[1]);                        player.teleport(new Position(x, y, player.getPosition().getZ()));                }                 // ...        }         @Override        void tick() throws Exception {                // not necessary, this is a binding        }         @Override        void onEnable(String pluginName) throws Exception {                // when the script is enabled we must bind it to the plugin bridge, so that it is defined and can be called from the core                PluginBridge.registerBinding(PluginBridge.COMMAND_HANDLER_BINDING_KEY, pluginName);        }         @Override        void onDisable() throws Exception {                // not necessary, since this script isnt particularly intensive        }}
Script: Sample Plugin (plugins/scripts/SamplePlugin.groovy)
A suitable purpose of a script may be to respond to HTTP API requests or perform backups. I don't have a ready example for this so just imagine something useful is below :-)


--- Code: Groovy ---import ... class SamplePlugin extends Plugin {         @Override        void tick() throws Exception {                // Code to execute on tick        }         @Override        void onEnable(String pluginName) throws Exception {                // Code to execute when plugin is enabled        }         @Override        void onDisable() throws Exception {                // Code to execute when plugin is disabled        }}
Possible improvements

* Delete tick functionality if unused.
* Rename the loading/binding to be more pretty.
* Multi-threaded loading of scripts.
Final words
You might find that the tick function is useless for your purpose, I kept it due to how I found the server I was working on (i.e. it had such functionality). Most people will want to only use the binding capabilities of these so called plugins.

The logic found in this tutorial is applicable to any other scripting language implementation and feel free to discuss design improvements below alongside any errors you find in my post.

justaguy:

This is a well-written tutorial, good job!

wavemaker:

Thanks for posting this Pure_. I feel like a simple to implement, scripting system is essential with server content creation these days. It would be interesting to see if someone creates a DSL using Groovy.

sini:

Like I told you on IRC I'd just slightly tweak it and remove the tick method then have another module for periodic tasks.

FoHammer:


--- Quote from: sini on June 07, 2015, 10:56:44 PM ---Like I told you on IRC I'd just slightly tweak it and remove the tick method then have another module for periodic tasks.

--- End quote ---


Pages: (1/3) >>>

Go to full version