Advertisement
  1. This site uses cookies. By continuing to use this site, you are agreeing to our use of cookies. Learn More.

Inspiration How to start learning writing plugins

Comments in 'Resources' started by PEMapModder, Feb 8, 2014.

  1. PEMapModder
    Offline

    PEMapModder Notable Member Plugin Developer

    Joined:
    Oct 9, 2013
    Posts:
    7,325
    Plugins:
    11
    Minecraft User:
    PEMapModder
    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.
    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.
    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=YourPluginName
    version=version
    author=your name
    class=PluginClass
    apiversion=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=PluginName
    version=version
    author=your name
    class=MyFirstPlugin
    apiversion=7
    */
    class MyFirstPlugin implements Plugin{
        public function 
    __construct(ServerAPI $api$server=false){
        }
        public function 
    init(){
        }
        public function 
    __destruct(){
        }
    }
    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.
    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"]->and $a["y"] == $data["target"]->and $a["z"] == $data["target"]->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"]->== $a["x"]and $data["target"]->=== $a["y"]and $data["target"]->== $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"]->and $value["y"] == $data["target"]->and $value["z"] == $data["target"]->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"]->== $d["x"]and $data["target"]->== $d["y"]and $data["target"]->== $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(0100 $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"]->== $d["x"]and $data["target"]->== $d["y"]and $data["target"]->== $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.
    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 :D

    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 :D

    REM I am in a hurry so I will continue this if the paragraph looks unfinished.
    CONTINUED AT SECOND POST
    Last edited: Apr 2, 2014
  2. PEMapModder
    Offline

    PEMapModder Notable Member Plugin Developer

    Joined:
    Oct 9, 2013
    Posts:
    7,325
    Plugins:
    11
    Minecraft User:
    PEMapModder
    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:
    <?php

    namespace pemapmodder\tutorial\plugin0;

    use 
    pocketmine\Server// namespace pocketmine; class Server{} // well actually we don't need this line here lol
    use pocketmine\utils\TextFormat as Colors// my favour. Call TextFormat Colors xD
    use 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.
    Last edited: May 19, 2014
    PocketKiller, Jon and Tuff like this.
  3. PEMapModder
    Offline

    PEMapModder Notable Member Plugin Developer

    Joined:
    Oct 9, 2013
    Posts:
    7,325
    Plugins:
    11
    Minecraft User:
    PEMapModder
    Registering commands
    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 $issuerCommand $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->2$player->z));
        
    $issuer->sendMessage("Teleported you above ".$player->getDisplayName().".");
        return 
    true;
      }
    }
    Last edited: May 19, 2014
  4. PEMapModder
    Offline

    PEMapModder Notable Member Plugin Developer

    Joined:
    Oct 9, 2013
    Posts:
    7,325
    Plugins:
    11
    Minecraft User:
    PEMapModder
    No one reading?
  5. Falk
    Offline

    Falk Staff Member Global Moderator

    Joined:
    Sep 2, 2013
    Posts:
    1,710
    Plugins:
    22
    Minecraft User:
    Falkirknh
    This is pretty amazing, great work!
  6. PEMapModder
    Offline

    PEMapModder Notable Member Plugin Developer

    Joined:
    Oct 9, 2013
    Posts:
    7,325
    Plugins:
    11
    Minecraft User:
    PEMapModder
    Starting a section about debugging.
    DeathRaven359, Falk and xktiverz like this.
  7. GlaciercreepsMC
    Offline

    GlaciercreepsMC Active Member Plugin Developer

    Joined:
    Jan 21, 2014
    Posts:
    91
    Plugins:
    1
    Minecraft User:
    GlaciercreepsMC
    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.
    DeathRaven359 and Legoboy0215 like this.
  8. kgdwhsk
    Offline

    kgdwhsk New Member Plugin Developer

    Joined:
    Aug 23, 2013
    Posts:
    13
    Plugins:
    1
    This is very good!
    Legoboy0215, Snake1999 and xktiverz like this.
  9. Falk
    Offline

    Falk Staff Member Global Moderator

    Joined:
    Sep 2, 2013
    Posts:
    1,710
    Plugins:
    22
    Minecraft User:
    Falkirknh
    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";
    Last edited: Mar 28, 2014
    Legoboy0215 and PEMapModder like this.
  10. PEMapModder
    Offline

    PEMapModder Notable Member Plugin Developer

    Joined:
    Oct 9, 2013
    Posts:
    7,325
    Plugins:
    11
    Minecraft User:
    PEMapModder
    Can sticky this?
  11. Falk
    Offline

    Falk Staff Member Global Moderator

    Joined:
    Sep 2, 2013
    Posts:
    1,710
    Plugins:
    22
    Minecraft User:
    Falkirknh
    Done :)
    PEMapModder likes this.
  12. PEMapModder
    Offline

    PEMapModder Notable Member Plugin Developer

    Joined:
    Oct 9, 2013
    Posts:
    7,325
    Plugins:
    11
    Minecraft User:
    PEMapModder
    Thanks
  13. PEMapModder
    Offline

    PEMapModder Notable Member Plugin Developer

    Joined:
    Oct 9, 2013
    Posts:
    7,325
    Plugins:
    11
    Minecraft User:
    PEMapModder
    Simple tutorial for learning from the sourcee code without much knowledge.
  14. kgdwhsk
    Offline

    kgdwhsk New Member Plugin Developer

    Joined:
    Aug 23, 2013
    Posts:
    13
    Plugins:
    1
    Have you mucker Event does not work,because a plugin.
  15. wies
    Offline

    wies Notable Member

    Joined:
    Aug 23, 2013
    Posts:
    392
    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.
  16. PEMapModder
    Offline

    PEMapModder Notable Member Plugin Developer

    Joined:
    Oct 9, 2013
    Posts:
    7,325
    Plugins:
    11
    Minecraft User:
    PEMapModder
    The false thing won't prevent default in some events
  17. PEMapModder
    Offline

    PEMapModder Notable Member Plugin Developer

    Joined:
    Oct 9, 2013
    Posts:
    7,325
    Plugins:
    11
    Minecraft User:
    PEMapModder
    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. :( :oops:
  18. PEMapModder
    Offline

    PEMapModder Notable Member Plugin Developer

    Joined:
    Oct 9, 2013
    Posts:
    7,325
    Plugins:
    11
    Minecraft User:
    PEMapModder
    What do you mean?
  19. PEMapModder
    Offline

    PEMapModder Notable Member Plugin Developer

    Joined:
    Oct 9, 2013
    Posts:
    7,325
    Plugins:
    11
    Minecraft User:
    PEMapModder
    Added something about testing plugins.
  20. PEMapModder
    Offline

    PEMapModder Notable Member Plugin Developer

    Joined:
    Oct 9, 2013
    Posts:
    7,325
    Plugins:
    11
    Minecraft User:
    PEMapModder
    Need to rewrite at least 50% for the Alpha_1.4 update!

Share This Page

Advertisement