-
Notifications
You must be signed in to change notification settings - Fork 2
PocketMine Plugin Development FAQ
This page aims to talk about questions frequently asked on the Plugin Development forum. The content is mainly summarized from high-quality posts on the forums, and linked as references if copied.
Visit this page with a bitlink: https://bit.ly/PmDev
These are pure PHP problems. See this StackOverflow page — it is very detailed.
While it makes sense in English to say "if the ID is 1, 2, 3 or 4", it's incorrect to write it in PHP as if($id == [1, 2, 3, 4])
nor if($id == 1 or 2 or 3 or 4)
. See this StackOverflow question for details and this for more solutions.
Check if your class files are placed and written according to PSR-0 (check here).
As of API 3.0.0-ALPHA4 and before, you have to first rename the original command into something else using Command::setLabel
, then remove it from the command map using Command::unregister
. Then your own command class can be registered to the command map directly. ref from @iksaku and @shoghicp
For example, to override the /help command:
$map = $server->getCommandMap();
$old = $map->getCommand("help");
$old->setLabel("help_disabled");
$old->unregister($map);
$new = new MyHelpCommand($this);
$map->register($this->getName(), $new);
$map->register($this->getName(), $new, "?");
In API 2.0.0 to API 3.0.0-ALPHA4, you have to do this for automatic command completion: The actual command completion part can be done using the "enum_values" option.
// Class that extends pocketmine\command\Command
public function generateCustomCommandData(Player $player) {
$commandData = parent::generateCustomCommandData($player);
$commandData["overloads"]["default"]["input"]["parameters"] = [
0 => [
"type" => "stringenum",
"name" => "parameter",
"optional" => false,
"enum_values" => ["parameter1", "parameter2"]
],
1 => [
"type" => "rawtext",
"name" => "value",
"optional" => true
],
2 => [
"type" => "string",
"name" => "name",
"optional" => true
]
];
return $commandData;
}
For more information, refer to https://gist.github.com/NiclasOlofsson/db712fd9e3c9cb0777cd9381cb48915a ref
You registered an event handler for an event that does not have a handler list. To fix this issue, find the parent class of the event that has a handler list, and handle that event class instead. Use instanceof
in your event handler to filter events.
Take EntityDamageByChildEntityEvent
as an example. Look at the class file — there is no public static $handlerList;
, so you cannot make event handlers for it. Instead, look at the class declaration to find out its superclass (EntityDamageByEntityEvent
), and then the superclass of the superclass, vice versa, until you find a superclass with the line public static $handlerList = null;
. Make your event handler handle this event class, and check if you are receiving EntityDamageByChildEntityEvent
in the handler:
public function handleDamage(EntityDamageEvent $event){
if($event instanceof EntityDamageByChildEntityEvent){
// handle $event here
}
}
Use the ServerScheduler::cancelTask()
method. Do not use Task::cancel()
— it only triggers internal callbacks of the task, but does not remove it from the scheduler.
Regarding the task ID, which is required when using ServerScheduler::cancelTask()
. This can be obtained by Task::getTaskId()
. If you cancel the task from itself, you can use $serverScheduler->cancelTask($this->getTaskId())
. Otherwise, you have to retain a reference to the Task instance, or retrieve the task ID when scheduling the task. ref
For example:
class MyTask extends PluginTask{
public function onRun(int $ticks){
if(needToCancelTask()){
$this->getOwner()->getServer()->getScheduler()->cancelTask($this->getTaskId());
}
}
}
Or, store the task ID when scheduling the task:
$this->id = $server->getScheduler()->scheduleRepeatingTask($myTask)->getTaskId();
// some time later...
$server->getScheduler()->cancelTask($this->id);
You don't do it with an event. You do it by comparing the system time.
This post is not yet cleaned up:
TimerScheduler & Tasks are for doing an action after a period of time, not having a state within a period of time. (The player will be unfrozen 10 seconds later vs The player cannot move for 10 seconds)
What is the difference? For the former, "be unfrozen" is the action, while for the latter, "be immobile" is the state.
How is a state different from an action? For an action, you do something actively. For example, "kill the player" is an action, but "the player is dead" is a state, a description, a condition.
In programming, the things you put in a function are always actions. You change a value, you call a function, you stop the code from executing. How does a condition affect execution? A condition is queried upon when something is executing, and it does not actively executes. Your condition is "cannot move". What executions will query this condition? Of course, you only need to know whether the player can move when the player tries to move. This is, as you said, cancel an event. You handle a PlayerMoveEvent and cancel it if the condition applies.
To be most convenient, you can change a value to make a player frozen/unfrozen, but this does not exist in PocketMine (there is Player->blocked, but it has side effects and it isn't very reliable). Therefore, you have to make PocketMine ask you whether the player can move every time the player tries to move. How do you yourself know whether the player can move? You ask yourself whether it is within the 10 seconds you asked for.
But the time that code executes changes - how do you know if it is that relative 10 seconds? Therefore, we make the relative time absolute. For example, if I tell you that I typed this sentence in the past minute, you don't know when I typed this sentence. So how do you find it out? You look at the time this post is posted, and minus one miniute, to know what I meant by "in the past minute". Without the post timestamp from the forums? I have to tell you the exact (absolute) date and time, that is, 2016-11-16 17:38:05 UTC+8.
Same goes to your code. You write down that "It is now 2016-11-16 17:38:05 UTC+8, and for the next 10 seconds the player can't move". Since time only goes forwards, I can make it even more straightforward: "The player can't move before 2016-11-16 17:38:15 UTC+8".
An absolute time can be retrieved using the time() or microtime() function. So, you only store the timestamp for the "unfreeze" time, then in the PlayerMoveEvent handler, you check if the current time is less than the "unfreeze" time. You don't need to check if it is less than the "freeze" time, because time only goes forwards.
You don't need to use scheduler tasks at all.
What is different between scheduleDelayedTask
, scheduleRepeatingTask
, scheduleDelayedRepeatingTask
and scheduleAsyncTask
?
Do not mix them up. There is something called Task
, where PluginTask
is a type of Task
; and there is something called AsyncTask
, which is entirely irrelevant with Task
.
A Task
can be scheduled to run after some time, which is scheduleDelayedTask
; or to run repeatedly at given intervals, starting immediately, which is scheduleRepeatingTask
; or to start running repeatedly after some time, which is scheduleDelayedRepeatingTask
. The code in Task
is executed on the main thread.
Threading is like executing code in another machine. Executing code in threads generally won't slow down the server unless you try to mine bitcoins. Each thread executes differently. We call the thread that the server does most of the work the "main thread". If the "main thread" is blocked (stops executing because it has to do something busy), it cannot respond to players, so it looks like the server hangs, or if it is only blocked for short periods, the server is laggy (decrease in TPS). In simple words, threading is executing in background.
An AsyncTask
is executed in other threads, but an AsyncTask
is not an independent thread. PocketMine internally has some threads called AsyncWorker
. Each AsyncWorker
is an independent thread, which you can think as another CPU. When you scheduleAsyncTask
, PocketMine adds your new AsyncTask
to a queue of AsyncTasks called the AsyncPool
. An AsyncWorker
will take the oldest AsyncTask
from the AsyncPool
and execute it, and then takes and execute the next one from the AsyncPool
. (Imagine there are a few water pipes removing water from a water pool — the water at the bottom gets removed first) They are still executed on the same machine, possibly on another core (system-dependent, I guess). Using an AsyncTask will not give you infinite CPU, but may facilitate better utilization of all the CPU available.
You should only use AsyncTask
to run one-time operations (e.g. usually takes less than 15 seconds to execute). If you are using it to run repeating stuff, such as starting another server (e.g. an HTTP server like Volt or an IRC client like the IRCClient plugin), start a thread instead. Otherwise, you will keep one of the workers busy (the water pipe is stuck), and the server will get one less AsyncWorker available. ref
In this analogy, a customer = a player, the waiter = main thread, food = some data from the Internet that takes a long time to download, the cook = the code that downloads data
- A customer enters a restaurant and orders some food.
- A player logins and asks the server to download some data from the Internet.
- The waiter takes the order.
- The server receives the command to download data.
- The waiter writes a note to the cook to prepare the food.
- The server starts an AsyncTask to download the data. While downloading data, the thread can't do anything else.
- The waiter continues to serve other customers.
- The server still serves the players, keeping them online, maintaining game mechanics, keep the server ticking, etc.
- If the waiter does not ask the cook to prepare the food, but does the cooking himself, (assuming there is only one waiter) the customers will be left unattended, and they will be angry and leave the restaurant.
- If the server downloads data on the main thread instead of in an AsyncTask, the players cannot see any response from the server to what they do, and may eventually timeout.
- The waiter occasionally looks if the cook has finished preparing the food. (Looking at the table does not really stop him from serving customers, because it's just a glimpse and takes very little time)
- The server checks if an AsyncTask is completed every tick.
- The restaurant has a partition between the kitchen and the dining area, so the waiter and the cook cannot really talk directly.
- Threads in PHP cannot communicate easily. They are often based on message stacks that may only be queried in long intervals.
- The cook wants to say "the beef is ready, now preparing the noodles". He writes it on a note, and the waiter reads the note to the customer.
- The AsyncTask announces that "50% downloaded" (using
AsyncTask::publishProgress
). The main thread checks for the progress every tick, and accordingly sends them to the player. - The main thread code is implemented in
AsyncTask::onProgressUpdate
, but they are actually run on the main thread.
- If the food is ready, it is served to the player.
- If the AsyncTask has a result, i.e. the data are downloaded, the data will be processed and the server accordingly reacts to the player.
- The main thread code is implemented in
AsyncTask::onCompletion
, but they are actually run on the main thread.
ref: an ancient post from @PEMapModder that can no longer be found. @SOF3 recited it.
You can't call API methods on a separate thread. This is the undeniable fact; this is by definition of "API methods of PocketMine" and "threads". The main body (onRun
) of an AsyncTask is run entirely on a worker thread, and it is incorrect, if not impossible to call API methods from AsyncTask::onRun()
. In addition to system limitations, most API operations you want to run in background actually have a bottleneck in network I/O with players, which cannot be prevented by threading anyway.
You may request a delayed call of API methods on the main thread from an AsyncTask using publishProgress()
+ onProgressUpdate()
, or using onCompletion()
, but you still cannot call API methods on other threads, but just on the main thread. You can pass scalar/serializable data between threads, and you can request something to be done upon an object on the main thread through a complicated series of scheduling, but you cannot do them directly on other threads. You may break down objects and reconstruct them in another thread, but they will not be the same object, i.e. if you somehow pass the Player object to an AsyncTask (which is actually impossible because it has a Server reference), it will be a clone, and API methods won't work as they usually do.
In other threads you may, however, call static API methods, such as Utils::getURL()
, or even construct classes in the PocketMine API; but this is very dangerous, because if you aren't 100% sure what you are doing, you will end up trying to modify some data ineffectively without errors. PHP builtin functions are not API methods, but they are safe to use as long as they don't reference any storage in the main thread. For example, fopen
returns a resource, which is actually an integer pointing to a handle stored in the PHP process; this resource ID will mean something probably entirely different if passed to another thread. You can still use resources in other threads, but make sure you don't try to reference resources in the main thread.
Therefore, trying to call an API method on other threads will fail silently. It is not possible, because you aren't calling API methods from the server at all; you are calling them probably on a clone of the server, or a clone of other objects, etc., which does not affect anything on the main thread. ref
There are two typical uses:
- No, this is not how you call setBlock() in a large loop in world edit. Remember that you cannot execute PocketMine core API stuff in AsyncTask or any other threads. (to be explained below)
- One example is when you want to, say, delete a very big directory (e.g. the .git/ directory of a Git repo with many commits and branches like PocketMine, or the world folder of a map with many generated chunks). That is not related to PocketMine core, but it is time-consuming to run ("lagging the server") (I would consider anything taking more than half a tick, i.e. 25 ms "lagging the server"). You wouldn't expect it to take too much time (like a whole hour) to run, and you may execute it quite frequently.
- For example, you may be copying/deleting a SkyWars map 5 times every 10 minutes). This is when you want to use AsyncTask. AsyncTask wouldn't magically use less CPU. But it executes in another thread, which is like in another process (or is it really in a subprocess/another process? I'm not sure about how pthreads works internally). So instead of spending the server's core tick time to do stuff that you don't need to get done on the pulse, do it in another thread.
- E.g. you can teleport the player to that world after the copying AsyncTask has completed; you don't need to do that immediately, and maybe your copying task spends 100 milliseconds to run, the player doesn't notice it but your server will lag behind for 2 ticks if you had not used an AsyncTask).
- No, I didn't say waiting for a player to do something. You can simply use the event API or the scheduler API to do that instead of suspending an async worker.
- This refers to thread-blocking function calls, such as curl_exec() (hence
Utils::getURL()
andUtils::postURL()
). HTTP requests (cURL execution) would lag your server, because it spends most of the time waiting for a response from another server. - Another example is MySQL queries. You aren't doing anything constructive in the majority time of a query function call. You are simply waiting for MySQL server to read your query, execute it and return a result.
- Why spend all the main thread performance on waiting for a response?
- It would of course be most convenient if you can set your queries/requests to be non-blocking and use a scheduler task to synchronously check if the server has respond. But in cases where you can't, we are simply putting the query aside and get a worker (another thread) stuck instead of getting the main thread stuck. Do you want the waiters look sick or the cooks to look sick, when your clients can see your waiters but not your cooks?
- It doesn't save CPU. It just redistributes the CPU use.
Do not pass anything to the AsyncTask (parent) constructor unless you are going to use them in onCompletion() or onProgressUpdate(). If you do so, you must call fetchLocal() in onCompletion() to remove the data.
AsyncTask is not a PluginTask. You do not need to pass arguments to it.
JSON is a subset of YAML. Any valid JSON data can be parsed as YAML. So unless you want to store the data in a compact way (which is pointless for small files, since the size of each file is always multiples of 4 KB), YAML seems to always be a better option compared to JSON, unless your data will be parsed by a program that only knows JSON but not YAML. (After all, JSON has a long history but YAML is kind of new, but most languages already have YAML libraries)
If you have may data for many small units, e.g. a few numbers per player, you won't want to store all the data in the same YAML file, because it's slow. You have to load all the data into memory whenever you do I/O, which may cause memory failure.
Therefore, to save data for each player, each account should have its own file. However, it creates a large number of files, which many users may dislike. Also, it is inconvenient to evaluate statistics based on all data, e.g. mean, sum, maximum, etc. To do so with YAML, you have to either:
- Load the data of all units every time. But if you have 100000 players registered, you are in trouble. You have to open a file stream for each unit, read the contents, store them in a list, and close stream. This will lead to very poor performance, especially on personal computers, whose harddisks don't expect you to read so many files at a time. Therefore, this is not a good idea.
- Store the values. If you want a sum, store the sum somewhere and update it every time a datum is added/removed/updated. If you want an average or standard deviation, store the count too and also update it every time data are changed. If you want top 10, a bit trouble. If you want the median, basically you must recalculate all the data every time, unless you otherwise have an index of all numbers somewhere, which isn't per-player storage anymore. ref
Oh the other hand, SQL has these advantages:
- All the data are stored in the same file, but the data are accessed using
fseek
, which prevents loading unnecessary data. SQL is optimized for various queries, and data are indexed upon request, so it is more efficient. - Most SQL implementations (e.g. MySQL) have good concurrency support, so multiple servers can access data together without conflict, if you know how to use it. Nevertheless, user-side setup might be slightly inconvenient.
Of course, the best solution is to let the users pick what database to use and bear their own consequences! See SimpleAuth for how to support switching between multiple database types.
The Minecraft coordinate system complies with a right-handed Cartesian coordinate system, i.e. x cross y = z
, as in this figure:
In the Minecraft system, Y (the index finger) points upwards (negative Y points downwards). X points eastwards (negative X goes westwards), and Z points southwards (negative Z goes northwards).
The sun and moon come out from the east since Pocket Edition 1.1 ref. Clouds float westward, too ref.
Rotation is measured by yaw (a.k.a. azimuth) and pitch. The yaw is the clockwise rotation from the south, i.e.:
Yaw (Degrees), mod 360 | Yaw (Radians), mod 2π | Direction | One step from (x = 0, z = 0) in this direction |
---|---|---|---|
0 | 0 | South | (0, 1) |
90 | 0.5π | West | (-1, 0) |
180 | π | North | (0, -1) |
270 | 1.5π | East | (1, 0) |
Pitch is the downward rotation. Starting at 0 at the horizontal, it increases to 90° (0.5π) when facing the ground, and decreases to -90° (-0.5π) when facing the sky.
A few cautions:
- The yaw and pitch are represented in degrees in most places in PocketMine, including
Entity::$yaw
andEntity::$pitch
. - The yaw is not guaranteed to be within the range
[0, 360)
. Be sure to mod 360 before comparing it. - PHP's behaviour with
$negative_number % $positive_number
orfmod($negative_number, $positive_number)
: a negative number is returned. But the yaw can be negative. Be sure to conditionally add 360 to the yaw after% 360
if it is negative. - PHP's
%
operator casts floats to ints. This may affect your results slightly. Usefmod
instead; but it might still produce negative results.
Here is an alternative to %
/fmod
that always produces zero/positive float results:
function positive_fmod(float $a, float $b) : float{
return $a - floor($a / $b) * $b;
}
Conversion of yaw+pitch to a unit vector src
$y = -sin(deg2rad($this->pitch));
$xz = cos(deg2rad($this->pitch));
$x = -$xz * sin(deg2rad($this->yaw));
$z = $xz * cos(deg2rad($this->yaw));
return (new Vector3($x $y, $z))->normalize(); // TODO: verify if normalize() call is needed
Evaluate yaw and pitch looking from $pos1 to $pos2 ref
$diff = $pos2->subtract($pos1)->normalize();
$pitch = asin(-$diff->y);
$yaw = acos($diff->z / cos($pitch));
symbol | meaning |
---|---|
ref | Content is copied from the linked forum thread |
src | Content is based on evidence from source code |
Virions is a framework that deals with shading of library (often abbreviated to "lib") code among PocketMine-MP plugins.
Composer is a commonly used PHP library manager. However, it has myriad shortages that would become fatal when comes to plugin development.
While code-shading allows different versions or even editions of the code to simultaneously exist in the same runtime, it is not supported by Composer. For instance, InvMenu 3 and 4 had both been popular before the elimination of the former.
And the Virions framework is the saviour to the problems.
Without breaking Poggit rule C1b or replacing the namespace in all files of a library, infecting (or injecting) its Virion is your only choice. But if you have not yet ready to release your plugin, loading Virions would be an easier way during development time.
To begin an infection, you must first find the antigen of a Virion. Finding its Poggit-CI page is also a way out because the URL is supposed to contain the antigen.
For example, sof3/await-generator/await-generator
is the antigen of poggit.pmmp.io/ci/SOF3/await-generator/await-geneator.
Libraries that are made for PocketMine-MP often build their Virion on Poggit-CI (the "Dev" section of Poggit).
The following image demonstrates how you can find the Poggit-CI page of a Virion when it uses Poggit-CI to build PHAR:
A PHAR file of the Virion can also be downloaded from its Poggit-CI page.
This not only requires you to have PHP installed, but also have the Virion PHAR downloaded and have your plugin already packed to PHAR.
Path/To/php -dphar.readonly=0 <Path/To/Virion>.phar <Path/To/Plugin>.phar
(php
would be php.exe
for Windows users.)
When you are building a plugin on Poggit-CI, it will create a .poggit.yml file in the repository of the plugin. You may set Virions to be infected inside it:
projects:
<Plugin>:
path: <Relative/Path/To/Plugin/Yml>
libs:
- src: <Virion Antigen> # First Virion.
version: <Version in SemVer>
- src: ... # Second.
version: ...
- ... # Third...
(To know more about SemVer, please refer to https://nodesource.com/blog/semver-tilde-and-caret/)
- Download DEVirion.
- Create directory
virions
(beware of thes
) at your server directory. - Download the PHAR file of a Virion.
- Put it in the
virions
directory. (Notvirion
orplugins
!) - Expect a warning that looks like "Virions should be bundled into plugins, not redistributed separately! Do NOT use DEVirion on production servers!!".
The warning reminds you to not use this method unless during development time.
In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to http://unlicense.org
-
FAQ
-
PHP
- 🌐 Syntax errors
- [[ClassNotFoundException: Class
a class in your plugin
not found|PocketMine Plugin Development FAQ#classnotfoundexception class a class in your plugin not found]]
- Command API
- Event API
-
Scheduler API & Threading
- Cancelling tasks
- [[Difference between
schedule***Task
|PocketMine Plugin Development FAQ#what is different between scheduledelayedtask, schedulerepeatingtask, scheduledelayedrepeatingtask and scheduleasynctask]] - What is threading? Does it make the server faster?
- AsyncTask: An analogy of a restaurant
- What can be done in AsyncTasks
- Data Saving
- The Virions framework
-
PHP