Skip to content

Developing a Shine plugin

Person8880 edited this page Apr 6, 2024 · 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

Shine supports the loading of plugin files on the client as well as the server.

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.

Prediction VM

In addition to loading on the client, plugins can add logic into the prediction VM. To do this, create a predict.lua file in the plugin folder, e.g.

extensions/pluginname/predict.lua

Only plugins that define a predict.lua file will be loaded within the prediction VM. Note the following limitations:

  • Timers and the Think event are not available, as no update loop exists in the prediction VM. Use the OnProcessMove hook if you need to perform think-like logic (or to directly influence the predicted move command).
  • Network messages cannot be received by the prediction VM (despite the game confusingly having a Predict.HookNetworkMessage function). Thus, only datatable values can be used by plugins in the prediction VM.
  • Any configuration used by the plugin should be shared with the client VM. The prediction VM cannot save configuration values.

Caution

Generally, most plugins will have no need to run in the prediction VM and thus should not create a predict.lua file. The main use of running plugins in the prediction VM is to override game code that runs within it already (e.g. player move command processing). If you define a predict.lua file, shared.lua will also be executed within the prediction VM.

Plugin loading

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

Server side only

  1. Load plugin's Lua file.
  2. Check to make sure the plugin registered itself with Shine:RegisterExtension() OR returned a plugin object.
  3. If it's defined Plugin.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)

  1. Load the shared.lua file in the plugin's folder.
  2. Check to make sure the plugin registered itself OR returned a plugin object.
  3. On the server, load server.lua if it exists, with the table registered in the shared.lua file and the plugin's name passed in as arguments (accessed with ...). On the client, load client.lua if it exists, again with the plugin table and name passed in as arguments.

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)

  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 then running 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. This version is displayed by the sh_listplugins command, and is also used to perform config migrations. 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.

-- Start with a plugin object. The file is invoked with the plugin's name, meaning you don't have to hardcode it.
-- Files in Lua are really just functions, and their arguments are accessed as a var-arg (hence the '...')
local Plugin = Shine.Plugin( ... )

Plugin.Version = "1.0" -- The plugin's (config) version

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.CheckConfigTypes = true -- Should we check the types of values in the config to make sure they match our default's types?
Plugin.CheckConfigRecursively = false -- Should we check sub-table's values for missing/unused entries too?

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

If you wish to use a different config loading method than the default, 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 else in the plugin.

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 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"
    }
}

Important

For shared plugins, define these conflicts in shared.lua to ensure they are identical on the server and the client/prediction VM. Otherwise, the plugin may be disabled inconsistently between client and server, causing clients to be disconnected.

Entry Points

Once you have the variables above set, you can define a few entry point functions. You do not have to define these functions. If you have nothing to initialise or cleanup in your plugin, you can leave them out and the base plugin's versions will be run for you.

Plugin:Initialise()

This function is called when the plugin's config is loaded and Shine wants to enable the plugin.

In this you should set up the plugin and prepare any fields you may need. Once you have set up everything, return true to indicate loading succeeded. 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 and abort loading the plugin.

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

    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() and any timers created using the plugin timer functions.

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 data etc.
    self.Count = nil
    self.BaseClass.Cleanup( self )
end

Registering your plugin

Returning the plugin object

When the plugin table is created using Shine.Plugin( ... ), it is already set up with the plugin's name and can simply be returned at the bottom of the file. The plugin loader will then register it for you without you needing to call Shine:RegisterExtension() in the plugin file.

local Plugin = Shine.Plugin( ... )

-- Add all your plugin methods/fields...

-- Return the plugin table to be registered automatically.
-- As Lua files are just functions, this value is returned to the plugin loader.
return Plugin

This approach has the advantage of never having to refer to the plugin's name in the code. It will be entirely determined by the file/folder path, which is also what the loader uses to determine what files to load for a given plugin name.

Important

For a shared plugin, do this only in the shared.lua file. When server.lua, client.lua and predict.lua are invoked, they will be passed the plugin table from shared.lua as an argument:

-- As shared.lua already registered the plugin, it's injected in the other files.
local Plugin = ...
-- Thus fields and methods should be set on the injected plugin table.
function Plugin:Initialise()
    -- Realm-specific initialisation logic...
    return true
end

Manually calling Shine:RegisterExtension

Older versions of Shine required manually registering plugins at the very bottom of the plugin file (shared.lua for shared plugins):

-- Registers the plugin "example" with the Plugin table.
Shine:RegisterExtension( "example", Plugin )

This is still supported, but should be considered deprecated. Using Shine.Plugin() and returning the table is the recommended approach for new plugins.

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 unload 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.

Plugin inheritance

Plugins can inherit values from another plugin, allowing for easier development of plugins that do similar things. Plugins can even inherit in a chain, there's nothing stopping you from inheriting from a plugin that also inherits.

To inherit from another plugin, either:

  • Return a second table of options from your main plugin file (shared.lua if a shared plugin, otherwise the appropriate side's file):
local Options = {
    Base = "baseplugin"
}

return Plugin, Options
  • Pass the options table as the 3rd argument to Shine:RegisterExtension():
Shine:RegisterExtension( "myawesomeplugin", Plugin, {
    Base = "baseplugin"
} )

Blacklisting keys

Sometimes you might want to ignore some specific functions/values from the parent plugin, for instance, a hook that the parent has but the child plugin doesn't want. To do this, add a BlacklistKeys field to the table as follows:

local Options = {
    Base = "baseplugin",
    -- This plugin will not use the ClientConnect or ClientDisconnect functions from the parent plugin.
    BlacklistKeys = {
        ClientConnect = true,
        ClientDisconnect = true
    }
} )

return Plugin, Options

Whitelisting keys

Alternatively, you can set a specific whitelist of keys so that the plugin only inherits those values and no others. To do this, add a WhitelistKeys field to the table as follows:

local Options = {
    Base = "baseplugin",
    -- This plugin will ONLY use the ClientConnect and ClientDisconnect functions from the parent plugin.
    WhitelistKeys = {
        ClientConnect = true,
        ClientDisconnect = true
    }
} )

return Plugin, Options
Clone this wiki locally