Writing a MCServer plugin
This article will explain how to write a basic plugin. It details basic requirements for a plugin, explains how to register a hook and bind a command, and gives plugin standards details.
Let us begin. In order to begin development, we must firstly obtain a compiled copy of MCServer, and make sure that the Core plugin is within the Plugins folder, and activated. Core handles much of the MCServer end-user experience and gameplay will be very bland without it.
Creating the basic template
Plugins are written in Lua. Therefore, create a new Lua file. You can create as many files as you wish, with any filename - MCServer bungs them all together at runtime, however, let us create a file called main.lua for now. Format it like so:
PLUGIN = nil function Initialize(Plugin) Plugin:SetName("NewPlugin") Plugin:SetVersion(1) -- Hooks PLUGIN = Plugin -- NOTE: only needed if you want OnDisable() to use GetName() or something like that -- Command Bindings LOG("Initialised " .. Plugin:GetName() .. " v." .. Plugin:GetVersion()) return true end function OnDisable() LOG(PLUGIN:GetName() .. " is shutting down...") end
Now for an explanation of the basics.
- function Initialize is called on plugin startup. It is the place where the plugin is set up.
- Plugin:SetName sets the name of the plugin.
- Plugin:SetVersion sets the revision number of the plugin. This must be an integer.
- LOG logs to console a message, in this case, it prints that the plugin was initialised.
- The PLUGIN variable just stores this plugin's object, so GetName() can be called in OnDisable (as no Plugin parameter is passed there, contrary to Initialize). This global variable is only needed if you want to know the plugin details (name, etc.) when shutting down.
- function OnDisable is called when the plugin is disabled, commonly when the server is shutting down. Perform cleanup and logging here.
Registering hooks
Hooks are things that MCServer calls when an internal event occurs. For example, a hook is fired when a player places a block, moves, logs on, eats, and many other things. For a full list, see the API documentation.
A hook can be either informative or overridable. In any case, returning false will not trigger a response, but returning true will cancel the hook and prevent it from being propagated further to other plugins. An overridable hook simply means that there is visible behaviour to a hook's cancellation, such as a chest being prevented from being opened. There are some exceptions to this where only changing the value the hook passes has an effect, and not the actual return value, an example being the HOOK_KILLING hook. See the API docs for details.
To register a hook, insert the following code template into the "-- Hooks" area in the previous code example.
cPluginManager.AddHook(cPluginManager.HOOK_NAME_HERE, FunctionNameToBeCalled)
What does this code do?
- cPluginManager.AddHook registers the hook. The hook name is the second parameter. See the previous API documentation link for a list of all hooks.
So in total, this is a working representation of what we have so far covered.
function Initialize(Plugin) Plugin:SetName("DerpyPlugin") Plugin:SetVersion(1) cPluginManager.AddHook(cPluginManager.HOOK_PLAYER_MOVING, OnPlayerMoving) LOG("Initialised " .. Plugin:GetName() .. " v." .. Plugin:GetVersion()) return true end function OnPlayerMoving(Player) -- See API docs for parameters of all hooks return true -- Prohibit player movement, see docs for whether a hook is cancellable end
So, that code stops the player from moving. Not particularly helpful, but yes :P. Note that ALL documentation is available on the main API docs page, so if ever in doubt, go there.
Binding a command
Format
So now we know how to hook into MCServer, how do we bind a command, such as /explode, for a player to type? That is more complicated. We firstly add this template to the "-- Command bindings" section of the initial example:
-- ADD THIS IF COMMAND DOES NOT REQUIRE A PARAMETER (/explode) cPluginManager.BindCommand("/commandname", "permissionnode", FunctionToCall, " - Description of command") -- ADD THIS IF COMMAND DOES REQUIRE A PARAMETER (/explode Notch) cPluginManager.BindCommand("/commandname", "permissionnode", FunctionToCall, " ~ Description of command and parameter(s)")
What does it do, and why are there two?
- PluginManager:BindCommand binds a command. It takes the command name (with a slash), the permission a player needs to execute the command, the function to call when the command is executed, and a description of the command.
So why are there two? Standards. A plugin that accepts a parameter MUST use a format for the description of " ~ Description of command and parms" whereas a command that doesn't accept parameters MUST use " - Description of command" instead. Be sure to put a space before the tildes or dashes. Additionally, try to keep the description brief and on one line on the client.
Parameters
What parameters are in the function MCServer calls when the command is executed? A 'Split' array and a 'Player' object.
The Split Array
The Split array is an array of all text submitted to the server, including the actual command. MCServer automatically splits the text into the array,
so plugin authors do not need to worry about that. An example of a Split array passed for the command, "/derp zubby explode" would be:
   /derp (Split[1])
   zubby (Split[2])
   explode (Split[3])
   The total amount of parameters passed were: 3 (#Split)
The Player Object and sending them messages
The Player object is basically a pointer to the player that has executed the command. You can do things with them, but most common is sending a message. Again, see the API documentation for fuller details. But, you ask, how do we send a message to the client?
There are dedicated functions used for sending a player formatted messages. By format, I refer to coloured prefixes/coloured text (depending on configuration) that clearly categorise what type of message a player is being sent. For example, an informational message has a yellow coloured [INFO] prefix, and a warning message has a rose coloured [WARNING] prefix. A few of the most used functions are listed here, but see the API docs for more details. Look in the cRoot, cWorld, and cPlayer sections for functions that broadcast to the entire server, the whole world, and a single player, respectively.
-- Format: §yellow[INFO] §white%text% (yellow [INFO], white text following it) -- Use: Informational message, such as instructions for usage of a command Player:SendMessageInfo("Usage: /explode [player]") -- Format: §green[INFO] §white%text% (green [INFO] etc.) -- Use: Success message, like when a command executes successfully Player:SendMessageSuccess("Notch was blown up!") -- Format: §rose[INFO] §white%text% (rose coloured [INFO] etc.) -- Use: Failure message, like when a command was entered correctly but failed to run, such as when the destination player wasn't found in a /tp command Player:SendMessageFailure("Player Salted was not found")
Those are the basics. If you want to output text to the player for a reason other than the three listed above, and you want to colour the text, simply concatenate "cChatColor.*colorhere*" with your desired text, concatenate being "..". See the API docs for more details of all colours, as well as details on logging to console with LOG("Text").
Final example and conclusion
So, a working example that checks the validity of a command, and blows up a player, and also refuses pickup collection to players with >100ms ping.
function Initialize(Plugin) Plugin:SetName("DerpyPluginThatBlowsPeopleUp") Plugin:SetVersion(9001) cPluginManager.BindCommand("/explode", "derpyplugin.explode", Explode, " ~ Explode a player"); cPluginManager:AddHook(cPluginManager.HOOK_COLLECTING_PICKUP, OnCollectingPickup) LOG("Initialised " .. Plugin:GetName() .. " v." .. Plugin:GetVersion()) return true end function Explode(Split, Player) if (#Split ~= 2) then -- There was more or less than one argument (excluding the "/explode" bit) -- Send the proper usage to the player and exit SendMessage(Player, "Usage: /explode [playername]") return true end -- Create a callback ExplodePlayer with parameter Explodee, which MCS calls for every player on the server local HasExploded = false local ExplodePlayer = function(Explodee) -- If the player we are currently at is the one we specified as the parameter if (Explodee:GetName() == Split[2]) then -- Create an explosion at the same position as they are; see API docs for further details of this function Player:GetWorld():DoExplosionAt(Explodee:GetPosX(), Explodee:GetPosY(), Explodee:GetPosZ(), false, esPlugin) SendMessageSuccess(Player, Split[2] .. " was successfully exploded") HasExploded = true; return true -- Signalize to MCS that we do not need to call this callback for any more players end end -- Tell MCS to loop through all players and call the callback above with the Player object it has found cRoot:Get():FindAndDoWithPlayer(Split[2], ExplodePlayer) if not(HasExploded) then -- We have not broken out so far, therefore, the player must not exist, send failure SendMessageFailure(Player, Split[2] .. " was not found") end return true end function OnCollectingPickup(Player, Pickup) -- Again, see the API docs for parameters of all hooks. In this case, it is a Player and Pickup object if (Player:GetClientHandle():GetPing() > 100) then -- Get ping of player, in milliseconds return true -- Discriminate against high latency - you don't get drops :D else return false -- You do get the drops! Yay~ end end
Make sure to read the comments for a description of what everything does. Also be sure to return true for all command handlers, unless you want MCS to print out an "Unknown command" message when the command gets executed :P. Make sure to follow standards - use CoreMessaging.lua functions for messaging, dashes for no parameter commands and tildes for vice versa, and finally, the API documentation is your friend!
Happy coding ;)