Spoiler: Introduction For most new plugin developers, you may not have a rich knowledge of PHP. If you believe you know PHP excellently, study the source code starting from /src/API/ServerAPI.php and if you understand it you are the god of plugin development. But for most of the other people, you may be new to programming. This tutorial is for those who believe that they do not have much experience but have a fast and creative brain. If you believe you have none of above, you would hardly get to, even understand how a code works. Now let's get into the topic. Today I would like to share some experience on how to learn plugins. I may be using the code of other plugin developers with full credit, but if you mind it, give me a yell and I will remove it. Preparing the development environment. Spoiler: Preparing the development environment. Before anything, you must prepare your working environment. Believe me, you will find it much better to use a coloured syntax editor (like NotePad++) than the notepad from microsoft windows or other plain text editor. At least, these apps and programs can help you to prevent, like, forgetting to close your quotes "". Initializing the plugin. Spoiler: Pre-Alpha_1.4 There can be any files in the plugins directory, but to get it being loaded, you must let PocketMine-MP know it is a plugin. The syntax of a PHP file is like this: PHP: <?php And as a pocketmine plugin, you must create a plugin class. A class is something like this: PHP: class Class{} And a plugin class is a class that implements the Plugin interface. You can find the contents of the Plugin interface at https://github.com/PocketMine/PocketMine-MP/blob/master/src/API/PluginAPI.php#L222 In PHP syntax, you make a class implement an interface by adding "implements Plugin" after the class name. And when you implement the interface, your class must also contain the functions in the interface. So, our plugin class should be like PHP: class MyPlugin implements Plugin{ public function __construct(ServerAPI $api, $server=false){ } public function init(){ } public function __destruct(){ }} However, in PHP, a single file can contain multiple classes. So how to let the server know which class is the main plugin? This is the solution: PHP: /*__PocketMine Plugin__name=YourPluginNameversion=versionauthor=your nameclass=PluginClassapiversion=11,12*/ The comments are self-explaining, but I should still explain them one by one. When a plugin is loaded, the console displays this message: 13:04:01 [INFO] Loading plugin "YourPluginName" version by your name So obvioiusly, the name, version and author is the information about the plugin. Note that in a function called configPath() in PluginAPI, it will create a folder in the plugins/ directory after the plugin name, so make sure the plugin name is a valid folder name. (and do not contain characters like ?!\/#) As for the name and class, they are some unique identifiers for the server. They should be unique and, for the name, make sure no other plugins use it, and for the class, look at https://github.com/PocketMine/PocketMine-MP/tree/master/src and make sure no files have this class name. (For example, if you make a plugin about signs, don't make the class name SignPostBlock because there is a class called SignPostBlock in https://github.com/PocketMine/PocketMine-MP/tree/master/src/material/block/attachable/SignPost.php ) And you are strongly advised to make the class name end with "Plugin" for later management. As for the API version, the current stable version is 11 and latest development version is 12. You should test your plugin for both of them if you plan to release it. So this is what every plugin should have (and whatever you do do not delete them): PHP: <?php/*__PocketMine Plugin__name=PluginNameversion=versionauthor=your nameclass=MyFirstPluginapiversion=7*/class MyFirstPlugin implements Plugin{ public function __construct(ServerAPI $api, $server=false){ } public function init(){ } public function __destruct(){ }} Spoiler: The new API as of Alpha_1.4, documentation created as of commit a38d3006168] [B]The plugin structure[/B] In the new PluginLoader, plugins must be in folders. This is the outline of it: [code] [cd plugins] [cd MyPlugin] [dir -s] Directory index of C:\PocketMine-MP\plugins\MyPlugin: plugin.yml src/ [DIR] Directory index of C:\PocketMine-MP\plugins\MyPlugin\src: MyPlugin/ [DIR] Directory index of C:\PocketMine-MP\plugins\MyPlugin\src\MyPlugin: MainPlugin.php [/code] [/spoiler] [B]Creating communication with the server[/B]. [spoiler="Old API event handling Now that your plugin is created, it does nothing at all. Before reading the following, close your eyes and think for a while: how would you expect the server to communicate with you? What have you thought? If you like put it in the comments below and I might make an API for that. But anyway, the way most plugins work is: Code: ===The things behind the console=== When the server starts... 1. The server loads plugins. 2. The plugins are initialized. 3. The plugins tell the server that they are going to handle certain events and tell the server how to communicate them. After a player taps a block... 1. The player's client internally evaluates the action. 2. A packet is sent from the player's client to the server about which block the player taps. 3. The server receives the packet. 4. The server (internally) broadcasts an event that a player touches a block along with an array of data about the player and the block and the item the player is holding and etc. 5. The main server thread calls the plugins by the functions they were told when the server started. Why I am putting this is that you must not only know how to write a plugin, but also understand it. When I was still new to PHP, I didn't develop anything, but I made posts on this forun about theories, like, I know very well about the limitations of PocketMine (cannot create items, change world height, etc.), but I worked out how to make (fake) infinite worlds in my mind. So although I can't write it out, I told the theory to other people and they all proved that it is possible. Now, let us see how we communicate with the server in code. First, we have to get the main server object. We get it with the static function PHP: ServerAPI::request(); Store it in a variable. You are advised to call these functions at the function init(). So, PHP: public function init(){ $server=ServerAPI::request();} Now you must first prepare a listener to an event. For example, when a player touches a block, you plan to tell the player about the coordinates of the block. So, PHP: public function blockTouchListener($data,$event){} And you must tell the server to call this function when players touch blocks. We make it like this: PHP: public function init(){ $server=ServerAPI::request(); // It is Server::getInstance() in Alpha_1.4!!!! $server->addHandler("player.block.touch", array($this,"blockTouchListener"); // It is changed in Alpha_1.4!!!!} The array [$this, "blockTouchListener"] is called a "callable". It is obvious so I am not going to explain how it works. (If you insist, go to php.net/manuel/en/type-callablle.php or something like that) Learning from other people. Now, how can I know what is in $data in the parameters? What I usually do is to read the code by others. Spoiler: onebone's plugin as example, old API Here I include some code by @onebone, it is the Economys Airport plugin. PHP: #This code is by @onebone. All credits go to him. I am only using a downloaded version, possibly not the latest version, of the plugin. This copy would be deleted ASAP if @onebone requests it to be removed.case "player.block.touch":$player = $this->api->player->get($data["player"]);switch ($data["type"]){case "break": if ($data["target"]->getID() == 323 or $data["target"]->getID() == 63 or $data["target"]->getID() == 68){ if ($this->arrival == null){ break; } foreach($this->arrival as $a){ if ($a["x"] == $data["target"]->x and $a["y"] == $data["target"]->y and $a["z"] == $data["target"]->z and $a["level"] == $data["target"]->level->getName()){ if ($this->api->ban->isOp($data["player"]->username) === false){ $output .= "You don't have permission to destroy airport"; $player->sendChat($output); $this->api->ban->kick("kick", array($player->username, "tried to destroy airport"), "EconomyAirport", false); return false; } foreach($this->arrival as $key => $a){ if ($data["target"]->x == $a["x"]and $data["target"]->y === $a["y"]and $data["target"]->z == $a["z"]and $a["level"] == $data["target"]->level->getName()){ unset($this->arrival[$key]); break; } } } } if ($this->departure == null){ break; } foreach($this->departure as $key => $value){ if ($value["x"] == $data["target"]->x and $value["y"] == $data["target"]->y and $value["z"] == $data["target"]->z and $value["level"]){ if ($this->api->ban->isOp($data["player"]->username) == false){ $output .= "You don't have permission to destroy airport"; return false; } unset($this->departure[$key]); } } } break;}if ($this->departure == null or $this->arrival == null){ break;}foreach($this->departure as $d){ if ($data["target"]->x == $d["x"]and $data["target"]->y == $d["y"]and $data["target"]->z == $d["z"]and $d["level"] == $data["target"]->level->getName()){ foreach($this->arrival as $v){ if ($v["name"] == $d["arrival"]){ if ($this->config->get("enable-accident") and $this->config->get("accident-percent") > 0){ $r = rand(0, 100 - $this->config->get("accident-percent")); } if (isset($r)){ if ($r == 1){} } $user = $player->username; $can = $this->api->economy->useMoney($player, $d["cost"]); if ($can !== false){ $this->api->player->tppos($user, $v["x"], $v["y"], $v["z"]); $player->sendChat("Thank you for flying with ".$this->config->get("Plane-name").". We arrived at ".$v["name"]." airport."); } else { $player->sendChat("You don't have money to ride plane."); } break 2; } } }}break; This is the code Economys Airport use to handle the event "player.block.touch". To see how he uses the data, I use Ctrl+F to search the world $data. So far the result is, there are three items in the array of $data used, which are $data["type"], $data["player"] and $data["target"]. And we would see that "type" represents whether the action is breaking or not ("break" or not "break"), $data["player"] is the object of the player, and $data["target"] is the block touched. On the line PHP: if ($data["target"]->x == $d["x"]and $data["target"]->y == $d["y"]and $data["target"]->z == $d["z"]and $d["level"] == $data["target"]->level->getName()){ , we can see that we can get the coords of the target by $data["target"]->x, $data["target"]->y and $data["target"]->z. And we can also see that we send a chat to the player by $player->sendChat("msg"). As for the syntax of PHP, we can see that we link pieces of a string by the character '.'. So, back to our plugin, we can make: PHP: public function blockChangedListener($data, $event){ if($event=="player.block.touch"){ $data["player"]->sendChat("You tapped the block at X:".$data["target"]->x.", Y:".$data["target"]->y.", Z:".$data["target"]->z); }} Debugging a plugin. Spoiler: Debugging There are multiple types of bugs in a plugin. Let's first talk about runtime crashes. Runtime rashes, although most fatal, are most convenient to debug since there are crash dumps. However, due to the length, most are too lazy to read them, or just don't understand. But let me tell you. The point of a crash dump is just at the beginning. Crash dump sample: Code: Error: array ( 'type' => 1, 'message' => 'Call to a member function getName() on a non-object', 'file' => 'C:\\PocketMineCNR\\plugins\\CNR.php', 'line' => 31, ) Code: [22] blah [23] blah [24] blah [25] blah [26] blah If you read these lines, obviously the bug is on line 31. But occasionally the line that crashes is in fact one or two lines in front of or behind the dumped line. This is an unsolved issue. However, the "message" item is the ultimate fix to your bug. The most frequently seen error messages are the infamous "Trying to get property of non-object" and "Call to member function methodName() on a non-object". This error is the same as java.lang.NullPointerException in Java. A common cause is Player not found when $this->api->player->get($name) is caused. When the player is not found, false is returned. However, plugin developers sometimes forget to check if the returned value is an instance of Player and calls to its field (like entity, username and inventory) or methods (like sendChat() and addItem()) directly. Since false is not an object, (primitive type) the property cannot be resolved and the server crashes. Another cause of this crash is the commandHandler function. Developers sometimes forget the case that console can issue command too, and in fact, the $issuer is a string "console" or "rcon" if it is not a player. So, as in my signature, the best way to return a message in a commandHandler is return "message" directly. The most annoymous error is "Undefined offset" in a field. For this kind of error, you have to read the code all over and track when an item in the field array is set, and in what occasions it can be unset. The best way to solve is to echo a debug message to console when it is set, and, if the problem item is an object, try implementing __unset(), __isset() and __destruct() in the object to track it. Learning from the source code without the plenty of coding experience. As you do not have a lot of coding or PHP knowledge (as targeted of this topic's readers), it is quite impossible (or at least very difficult) for you to understand the code by reading the full pocketmine source. But by reading this section, you can know how to learn (or research about functions) by reading the src. First, you must have the concept of a "class". A class is a type of an object (like stored in a variable). For example, in the callback of commands, the third parameter is $issuer (as commonly used), and it is an object of the class Player. So you can count "one Player, two Players"... And when you call the function $issuer->sendChat(), it is a function in the Player class. The source of the Player class is available at https://github.com/PocketMine/PocketMine-MP/tree/master/src/Player.php and you can use the awesome "Search" button on your device to search "function sendChat(". This skill gets very useful if you sometimes know there is such function but forget where it is. (Like, if you want to heal a player, you might have forgotten whether the function is in the player, or what is the sequence of the arguments. Then you search in Player.php and you will find that you should heal by $player->entity->heal($halfhearts, $reason="generic") This concept of classes is hard to explain but you will get to understand it through experience. In fact it is OK if you don't understand. Now, as @Glitchmaster_PE suggests in his tutorial, you should store the ServerAPI parameter ($api) on construct into a field (private $api, $this->api). So, in fact this is an instance of ServerAPI (https://github.com/PocketMine/PocketMine-MP/tree/master/src/API/ServerAPI.php), and, if you look at lines 35-78, you will find that $this->api->player is an instance of PlayerAPI, $this->api->chat instance of ChatAPI, and so on. So, if you want to know more functions, you can just look at them and guess and test. It is not necessary to understand the method body, just the function names are OK. If you are looking for events $data explain, you can use the same method. There is a powerful sesarch box on GitHub that searches through all the files. (Thanks to @Falk for this method a lot) Now, you want to prevent all item drops in a level called "PvP". So how do you find the information about parameters? First search "item.drop" (find this event name from the Documentation wiki). Don't look for addHandler. Look for dhandle("item.drop" or handle("item.drop" because they supply the data. So in the second result you see it on the 133rd line of EntityAPI.php (as of the current commit 5f3829692d6c62fd1b1728d80aa73021c11d54c4) you see the data is $data. Then look upwards and you see the contents in $data. Try using them. If it works, you win! Also I will be making a documentation on events at https://github.com/pemapmodder/PMPluginTutrl.git, but don't ask me when I will finish it because the answer is IDK Studying the API Update The best way to get your knowledge update with the API, obviously, is to "Watch" the PocketMine-MP repo on GitHub and read every line. But the point is, if you are reading this, it is most possibly difficult for you to understand each line of the code, so here are some shortcut methods. I will use the Alpha_1.4 update as an example. In the new API, if you use the "diff" or "compare" function in Git/GitHub, you would find some major changes, including the deletion of some APIs and the new event structure. T.B.C... Testing the Plugin in a Convenient Method It is obvious, especially for Android users and especially those with slow devices, that it is so much trouble to test a plugin. True, in a totally vanilla server and MCPE copy (except your own plugin: hope it doesn't take long to initialize), it takes about 30 seconds to go in the server. And including the closing-and-restarting of the server, it takes maybe a whole minute to test a plugin. For this, here I introduce a system for on-console-updating-plugins: 1. Make your plugin possible to be stopped on-console. e.g. PHP: public function eventHandler($data, $event){ if($this->hasNewUpdate)return;} In every function add this at the first line. The $this-hasNewUpdate field can be easily changed by making a simple console-only command. 2. Load your plugin. Here is an example code: PHP: public function cmdHandler($cmd, $arg, $issuer){ if($issuer!="console")return "Please run this command on-console."; if($cmd==="load-plugin"){ ServerAPI::request()->api->plugin->load(FILE_PATH."plugins/".implode(" ", $arg)); return "Finished loading."; }} This uses the PluginAPI to load the plugin manuelly. What you have to do is to type on console "load-plugin This is the file_name-of the plugin.php". Convenient? Of course, you need to get this system working first REM I am in a hurry so I will continue this if the paragraph looks unfinished. CONTINUED AT SECOND POST
Part 2 Initialization of code in new API Now, in the new API, with the (namespace+)class name specified at plugin.yml (here as example: pemapmodder\tutorial\plugin0\MainPlugin; So, if you didn't know namespaces, just look through these: PHP: <?phpnamespace pemapmodder\tutorial\plugin0;use pocketmine\Server; // namespace pocketmine; class Server{} // well actually we don't need this line here loluse pocketmine\utils\TextFormat as Colors; // my favour. Call TextFormat Colors xDuse pocketmine\plugin\PluginBase as Base; // my favour to call it like this.class MainPlugin extends Base{ // Base (PluginBase) is an abstract class implementing the Plugin interface. public function onLoad(){ console(Colors::GREEN."[INFO] ".get_class()." is loaded!"); } public function onEnable(){ console(Colors::AQUA."[DEBUG] ".get_class()."::onEnable() is called!"); }} Test it and look at the time loaded. The post below is a tutorial of the new API with a different style. Its target is the ones who already know the old API coding.
Registering commands Spoiler: cmds Commands, as in 1.0.0, can be registered easily by including it in plugin.yml Example: Old API: PHP: $this->api->console->register("cmd-a", "<usage|arg0> [arg1] description blah blah blah", array($this, "onCommand")); New API: Code: # this is plugin.yml name: ExamplePlugin # ... commands: cmd-a: description: description blah blah blah usage: "/cmd-a <usage|arg0> [arg1]" permission: egplugin.cmd.cmd-a permissions: egplugin.fmd.cmd-a: description: allow using command /cmd-a default: true Then, you'll have to put a command handler at the plugin. It must be in function onCommand(CommandSender $issuer, Command $cmd, $label, array $args). Example: PHP: use pocketmine\command\Command;use pocketmine\command\CommandSender;class MainClass extends PluginBase{...public function onCommand(CommandSender $issuer, Command $cmd, $label, array $args){ switch($cmd->getName()){ // just like what we did before, remember? case "cmd-a": if($issuer instanceof ConsoleCommandSender){ // if it is sent from console; this includes RCon, a subclass of ConsoleCommandSender $issuer->sendMessage("Please run this command in-game."); // use this; if you use console(), it doesn't work with RCon. If you use return, sorry, you can no longer return strings to be sent in the new API. return true; // return true always unless you want the usage message be shown: return false } if(!isset($args[0])){ return false; // show the usage } if(!(($player = $this->getServer()->get player($args[0])) instanceof Player)){ $issuer->sendMessage("Player not found."); return true; } $issuer->teleport(new Vector3($player->x, $player->y + 2, $player->z)); $issuer->sendMessage("Teleported you above ".$player->getDisplayName()."."); return true; }}
Thanks for this tutorial. Although, I still have to learn more php to further understand, at least I learned the basics of events/handlers and some basic stuff on $data.
I have been meaning to fix those If I did a lot of plugins would need updated and that means they would get pushed back up to the top. For anyone who is wondering how to easily make a command in game only just add the following to the beginning of your command function: PHP: if(!($issuer instanceof Player) ) return "This is an in game command";
Great work! Suggestion: add that the addHandler has an optional third parameter that is the priority, and the handler that has the highest priority will be executed first. When a handler is called and returns false or true it won't call the handlers in the other plugins, when the returned value is different from false of true then it will continue with calling the handlers until someone returns false or true.
Anyway it just came to me in the previous week that I had been using the wrong thing in $priority. I thought the priority was ascend, and I found from SimpleAuth that it is descending priority.