Skip to content

Developing a Shine plugin

Person8880 edited this page Nov 12, 2013 · 23 revisions

Overview

One of my design goals with Shine was to make it easy to extend. To this end, I'll document how to make a plugin with Shine.

Before you start

If you want to add new plugins to Shine, you need to make your own Steam Workshop mod, with the folder:

lua/shine/extensions

In this folder, place all the plugins you want to add. Run your mod alongside the main Shine mod and your plugins will be loaded.

Shared and client side plugins

New to revision 85, Shine now supports the loading of plugin files on the client as well as the server. For current server side plugins, no change is necessary. However, plugins that need a client side part need to be created differently.

Server side only:

  • extensions/pluginname.lua

Shared/client side:

  • extensions/pluginname/client.lua - This is only loaded on the client.
  • extensions/pluginname/shared.lua - This is loaded on both the server and the client.
  • extensions/pluginname/server.lua - This is only loaded on the server.

You do not need all three of those files, but at least one of shared.lua and client.lua must be present to load on the client.

A basic example plugin is available here:

It shows the structure of a shared plugin and basic usage of a datatable.

Details on the datatables system can be found here: https://github.com/Person8880/Shine/wiki/Datatables

Plugin loading

How a plugin is loaded depends on which state(s) it is using.

Server side only plugin loading process goes like this:

  1. Load plugin's Lua file.
  2. Check to make sure the plugin registered itself with Shine:RegisterExtension().
  3. If it's defined HasConfig = true, run Plugin:LoadConfig().
  4. Run Plugin:Initialise(), if this returns true, the plugin is assumed to have loaded successfully.

Shared plugins, (i.e plugins with a shared.lua file) will load differently:

  1. Load the shared.lua file in the plugin's folder.
  2. Check to make sure the plugin registered itself.
  3. On the server, load server.lua if it exists, with the global value 'Plugin' set to the table registered in the shared.lua file. On the client, load client.lua if it exists, with the global value 'Plugin' set to the table registered in the shared.lua file.

The load process then stops here, and waits for the server to confirm that the plugin is set to enabled in the server's config. If it is, then the last two steps of the load process are:

  1. If either side has defined Plugin.HasConfig = true, then run Plugin:LoadConfig().
  2. Run Plugin:Initialise(), if this returns true, the plugin is assumed to have loaded successfully.

Client only plugins (i.e plugins with only a client.lua file) will again load differently.

  1. Load the client.lua file in the plugin's folder.
  2. Wait to be told to enable. A client side command can be used to enable the plugin.
  3. Enabling works as before, running Plugin:LoadConfig() if Plugin.HasConfig = true and returning Plugin:Initialise().

Important variables

At the top of the plugin's file, you need to make the plugin's table. For shared plugins, you should create the plugin table in the shared.lua file. It will then get passed to the other two files.

Following this, you can define the plugin's version server side for when people use sh_listplugins. This is optional, it will default to "1.0".

Next you need to decide if the plugin is going to have a config file or not. If it will, you need to define Plugin.HasConfig = true. Configs work on both the server and the client side, so you can have config files for each. If you want separate files for each side, then define the config names in the server.lua and client.lua files instead of in shared.lua.

local Plugin = {}
Plugin.Version = "1.0"

Plugin.HasConfig = true --Does this plugin have a config file?
Plugin.ConfigName = "Example.json" --What's the name of the file?
Plugin.DefaultConfig = { --What's the default config setup?
    DoYouLikeCake = true
}
Plugin.CheckConfig = true --Should we check for missing/unused entries when loading?

Plugin.DefaultState = false --Should the plugin be enabled when it is first added to the config?

You can also load configs from the web or some other method, you will need to create your own Plugin:LoadConfig() function which will override the default. This function is called before Initialise and should not depend on anything other than the file it is loading from.

Inter-Plugin conflicts

If you know your plugin will interfere with another plugin, then you can tell Shine to unload either your plugin or the other plugin. The tournament mode plugin uses this to unload the pregame, mapvote, afkkick and readyroom plugins automatically when enabled.

To define conflicts:

Plugin.Conflicts = {
    --Which plugins should we force to be disabled if they're enabled and we are?
    DisableThem = {
         "mapvote"
    },
    --Which plugins should force us to be disabled if they're enabled and we are?
    DisableUs = {
         "tournamentmode"
    }
}

Required functions

Once you have the variables above set, you need to define a few mandatory functions.

Plugin:Initialise()

In this you should set up the plugin and clear out any values you may have. Once you have set up, set your plugin to enabled and return true. If you have a condition to meet in order for the plugin to be enabled, then you can return false, ErrorString if the condition isn't met. Shine will then print this error in the load process.

Initialise is called after loading the plugin's config, so you can freely access the config from it. Both server and client side plugins need to have this function, and it behaves in the same way on both sides.

function Plugin:Initialise()
    --Example of how to assert a condition when loading a plugin.
    if not Shine.Config.EnableLogging then return false, "logging must be enabled." end 

    self.Count = 0
    self.Enabled = true

    return true
end

Plugin:Cleanup()

This function is called when the plugin is unloaded. By default, it removes any commands bound to the plugin with Plugin:BindCommand(). If you have more to cleanup, then you will want to define the function and call the base class cleanup to clean up your bound commands, i.e

function Plugin:Cleanup()
    --Cleanup your extra stuff like timers, data etc.

    self.BaseClass.Cleanup( self )
end

Shine:RegisterExtension()

Finally, at the very bottom of the file after defining everything, you need to register the extension. If you are making a shared plugin with a shared.lua file, you must register the plugin in the shared.lua file. It will then pass the table you've registered here to the client.lua and server.lua files as the global 'Plugin' value.

Shine:RegisterExtension( "example", Plugin )

With these functions defined, the rest is up to you.

Hooking with plugins

Although you can use Shine.Hook.Add() to hook, there's a much nicer way to hook with plugins. Simply take the Shine hook name you want to hook into, and make a method with the same name on the plugin's table.

function Plugin:Think( DeltaTime )
    self.Count = self.Count + 1
end

This will automatically be hooked to Shine's 'Think' hook, which is NS2's 'UpdateServer' hook on the server, and the 'UpdateClient' hook on the client. When the plugin is defined as enabled, this hook is ran. When the plugin is defined as disabled, it is not. There's no need to remove or add hooks in Initialise or Cleanup, it's all handled for you.

Plugin hooks take priority over hooks registered with Shine.Hook.Add(), apart from certain key hooks.

Adding console/chat commands

Console and chat commands are a breeze to add. The way I like to do it is create them in a Plugin:CreateCommands() function, but feel free to make them where you want.

function Plugin:CreateCommands()
    local function PrintCount( Client )
        Shine:AdminPrint( Client, tostring( self.Count ) )
    end
    local Command = self:BindCommand( "sh_printcount", "printcount", PrintCount )
    Command:Help( "Prints the current count." ) 
end

Commands created using Plugin:BindCommand() will be removed for you on plugin shutdown providing you didn't define Plugin:Cleanup() or you call the base class cleanup in it (see the Plugin:Cleanup() section above).

I cover the console/chat command system in greater detail here.

Clone this wiki locally