-
Notifications
You must be signed in to change notification settings - Fork 23
Developing a Shine plugin
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.
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.
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.
It shows the structure of a shared plugin and basic usage of a datatable.
Details on the datatables system can be found here.
How a plugin is loaded depends on which state(s) it is using.
- Load plugin's Lua file.
- Check to make sure the plugin registered itself with
Shine:RegisterExtension()
. - If it's defined
Plugin.HasConfig = true
, runPlugin:LoadConfig()
. - Run
Plugin:Initialise()
, if this returns true, the plugin is assumed to have loaded successfully.
- Load the
shared.lua
file in the plugin's folder. - Check to make sure the plugin registered itself.
- On the server, load
server.lua
if it exists, with the global valuePlugin
set to the table registered in theshared.lua
file. On the client, loadclient.lua
if it exists, with the global valuePlugin
set to the table registered in theshared.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:
- If either side has defined
Plugin.HasConfig = true
, then runPlugin:LoadConfig()
. - Run
Plugin:Initialise()
, if this returns true, the plugin is assumed to have loaded successfully.
- Load the client.lua file in the plugin's folder.
- Wait to be told to enable. A client side command can be used to enable the plugin.
- Enabling works as before, running
Plugin:LoadConfig()
ifPlugin.HasConfig = true
and then runningPlugin:Initialise()
.
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.
local 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.DefaultState = false -- Should the plugin be enabled when it is first added to the config?
Plugin.NS2Only = false -- Set to true to disable the plugin in NS2: Combat if you want to use the same code for both games in a mod.
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.
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"
}
}
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.
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, 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 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
self.Enabled = true
return true
end
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
Finally, at the very bottom of the file after defining everything, you need to register the extension.
-- Registers the plugin "example" with the Plugin table.
Shine:RegisterExtension( "example", Plugin )
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.
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.
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.
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, you need to pass a third table argument to Shine:RegisterExtension()
as follows:
Shine:RegisterExtension( "myawesomeplugin", Plugin, {
Base = "baseplugin"
} )
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:
Shine:RegisterExtension( "myawesomeplugin", Plugin, {
Base = "baseplugin",
-- This plugin will not use the ClientConnect or ClientDisconnect functions from the parent plugin.
BlacklistKeys = {
ClientConnect = true,
ClientDisconnect = true
}
} )
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:
Shine:RegisterExtension( "myawesomeplugin", Plugin, {
Base = "baseplugin",
-- This plugin will ONLY use the ClientConnect and ClientDisconnect functions from the parent plugin.
WhitelistKeys = {
ClientConnect = true,
ClientDisconnect = true
}
} )