Note: Before editing this article, please ask in the discussion section for a second opinion – personal opinions and views should be kept out of wiki articles. Thank you!
Contents
Before you start(top)
For this example, we will be using MongoDB, but this tutorial should be followable without having a lot of knowledge about MongoDB. If you do wish to read about MongoDB you can read the "Using MongoDB" wiki page. We will also be creating a command, of which there is an explanation here.
What is asynchronous(top)
Every program has a starting point, from which the execution of the program happens basically line-by-line, which means every task will run after the previous task is finished. Because this is in a lot of cases, not the most optimal situation there is a way of starting a new task, that starts doing it thing, while the main program continues as well.
Those tasks are called Threads by the system and most programming languages that allow you to make use of them. Making use of multiple threads is especially useful if for instance one task is designed to wait for input, and you don't want the entire program to stop until a player sends a chat message. Threads are also smartly distributed by the operating system to the cores of the processor, which means that if you have multiple cores you can even get a performance increase from utilizing threads, as Minecraft's main loop runs on a single core.
Asynchronous is the opposite of synchronous, and all there is to it is that when you execute a task in a new or separate thread from where you are starting the task from, you're calling it asynchronously, while if you're starting a task and waiting for it to be finished in the same thread as where you started it from, you have called it synchronously. Neither is bad or good on it's own, the usages of both depend on the situation.
Creating a /playerinfo command(top)
For this tutorial, we will assume that a valid connection is made to the database and that a collection exists with some player data in it. The command will take one argument which will be the player name or UUID. An example of a player document in the database:
Code (Text):
{
_id: ObjectId("564bff868ab04da7798b4569"),
username: "Wouto1997",
lookupUsername: "wouto1997",
uuid: "a44c33ce480e486f9f782d1f52db037b",
money: 17500,
flying: true,
friends: [],
rank: "DEVELOPER",
lastSeen: ISODate("2015-11-18T11:33:10.852Z"),
registered: ISODate("2014-08-14T21:15:10.152Z")
}
_id: ObjectId("564bff868ab04da7798b4569"),
username: "Wouto1997",
lookupUsername: "wouto1997",
uuid: "a44c33ce480e486f9f782d1f52db037b",
money: 17500,
flying: true,
friends: [],
rank: "DEVELOPER",
lastSeen: ISODate("2015-11-18T11:33:10.852Z"),
registered: ISODate("2014-08-14T21:15:10.152Z")
}
Code (Java):
public class CommandPlayerInfo implements CommandExecutor {
@Override
public boolean onCommand(CommandSender cs, Command cmd, String label, String[] args) {
if (args.length < 1) {
cs.sendMessage("Usage:");
cs.sendMessage(" /playerinfo <name>");
cs.sendMessage(" /playerinfo <uuid>");
return true;
}
String param = args[0];
String key = null;
if (param.length() <= 16 && param.length() >= 3) {
key = "lookupUsername";
} else {
param = param.replaceAll("-", "");
if (param.length() != 32) {
cs.sendMessage("Invalid username or uuid");
return true;
}
key = "uuid";
}
param = param.toLowerCase();
DBCollection playerCollection = DatabaseHelper.getPlayerDatabase();
DBObject result = playerCollection.findOne(new BasicDBObject(key, param));
if (result == null) {
cs.sendMessage("The specified player could not be found");
return true;
}
cs.sendMessage( "Information about " + ((String) result.get("username")) );
cs.sendMessage( "UUID: " + ((String) result.get("uuid")) );
cs.sendMessage( "money: " + ((Integer) result.get("money")) );
cs.sendMessage( "Fly: " + ((Boolean) result.get("fly")).toString() );
cs.sendMessage( "Friends: " + ((BasicDBList) result.get("friends")).size() );
cs.sendMessage( "rank: " + ((String) result.get("rank")) );
cs.sendMessage( "Last online: " + ((Date) result.get("lastSeen")).toLocaleString() );
cs.sendMessage( "Registered: " + ((Date) result.get("registered")).toLocaleString() );
return true;
}
}
@Override
public boolean onCommand(CommandSender cs, Command cmd, String label, String[] args) {
if (args.length < 1) {
cs.sendMessage("Usage:");
cs.sendMessage(" /playerinfo <name>");
cs.sendMessage(" /playerinfo <uuid>");
return true;
}
String param = args[0];
String key = null;
if (param.length() <= 16 && param.length() >= 3) {
key = "lookupUsername";
} else {
param = param.replaceAll("-", "");
if (param.length() != 32) {
cs.sendMessage("Invalid username or uuid");
return true;
}
key = "uuid";
}
param = param.toLowerCase();
DBCollection playerCollection = DatabaseHelper.getPlayerDatabase();
DBObject result = playerCollection.findOne(new BasicDBObject(key, param));
if (result == null) {
cs.sendMessage("The specified player could not be found");
return true;
}
cs.sendMessage( "Information about " + ((String) result.get("username")) );
cs.sendMessage( "UUID: " + ((String) result.get("uuid")) );
cs.sendMessage( "money: " + ((Integer) result.get("money")) );
cs.sendMessage( "Fly: " + ((Boolean) result.get("fly")).toString() );
cs.sendMessage( "Friends: " + ((BasicDBList) result.get("friends")).size() );
cs.sendMessage( "rank: " + ((String) result.get("rank")) );
cs.sendMessage( "Last online: " + ((Date) result.get("lastSeen")).toLocaleString() );
cs.sendMessage( "Registered: " + ((Date) result.get("registered")).toLocaleString() );
return true;
}
}
Using bukkit's asynchronous functions(top)
The first thing you'll need to learn about is the Bukkit Scheduler, which you can access with Bukkit.getScheduler() from anywhere. There are 3 different types of tasks you can add to the scheduler, and each of those types has a synchronous and an asynchronous variant.
Important to note is that a synchronous task in this context is ran on a Minecraft tick, and not in the thread (usually) where you're calling it from. It is advised to not access most of the Bukkit API while not inside the Minecraft-tick thread, but on the other hand, it's also smart to execute database and other heavy tasks outside of the main thread.
We will be using runTask and runTaskAsynchronously only for this example, as we do not wish to execute a command later or repeatedly. After having parsed all the arguments the first thing we want to do is "escape" the command thread and start a new thread separated from Minecraft's threads.
Code (Java):
Bukkit.getScheduler().runTaskAsynchronously(plugin, new Runnable() {
@Override
public void run() {
}
});
@Override
public void run() {
}
});
Code (Java):
@Override
public boolean onCommand(final CommandSender cs, Command cmd, String label, String[] args) {
if (args.length < 1) {
cs.sendMessage("Usage:");
cs.sendMessage(" /playerinfo <name>");
cs.sendMessage(" /playerinfo <uuid>");
return true;
}
String param = args[0];
String key = null;
if (param.length() <= 16 && param.length() >= 3) {
key = "lookupUsername";
} else {
param = param.replaceAll("-", "");
if (param.length() != 32) {
cs.sendMessage("Invalid username or uuid");
return true;
}
key = "uuid";
}
final String fparam = param.toLowerCase();
final String fkey = key;
Bukkit.getScheduler().runTaskAsynchronously(plugin, new Runnable() {
@Override
public void run() {
DBCollection playerCollection = DatabaseHelper.getPlayerDatabase();
DBObject result = playerCollection.findOne(new BasicDBObject(fkey, fparam));
if (result == null) {
fcs.sendMessage("The specified player could not be found");
return;
}
fcs.sendMessage("Information about " + ((String) result.get("username")));
fcs.sendMessage("UUID: " + ((String) result.get("uuid")));
fcs.sendMessage("money: " + ((Integer) result.get("money")));
fcs.sendMessage("Fly: " + ((Boolean) result.get("fly")).toString());
fcs.sendMessage("Friends: " + ((BasicDBList) result.get("friends")).size());
fcs.sendMessage("rank: " + ((String) result.get("rank")));
fcs.sendMessage("Last online: " + ((Date) result.get("lastSeen")).toLocaleString());
fcs.sendMessage("Registered: " + ((Date) result.get("registered")).toLocaleString());
}
});
return true;
}
public boolean onCommand(final CommandSender cs, Command cmd, String label, String[] args) {
if (args.length < 1) {
cs.sendMessage("Usage:");
cs.sendMessage(" /playerinfo <name>");
cs.sendMessage(" /playerinfo <uuid>");
return true;
}
String param = args[0];
String key = null;
if (param.length() <= 16 && param.length() >= 3) {
key = "lookupUsername";
} else {
param = param.replaceAll("-", "");
if (param.length() != 32) {
cs.sendMessage("Invalid username or uuid");
return true;
}
key = "uuid";
}
final String fparam = param.toLowerCase();
final String fkey = key;
Bukkit.getScheduler().runTaskAsynchronously(plugin, new Runnable() {
@Override
public void run() {
DBCollection playerCollection = DatabaseHelper.getPlayerDatabase();
DBObject result = playerCollection.findOne(new BasicDBObject(fkey, fparam));
if (result == null) {
fcs.sendMessage("The specified player could not be found");
return;
}
fcs.sendMessage("Information about " + ((String) result.get("username")));
fcs.sendMessage("UUID: " + ((String) result.get("uuid")));
fcs.sendMessage("money: " + ((Integer) result.get("money")));
fcs.sendMessage("Fly: " + ((Boolean) result.get("fly")).toString());
fcs.sendMessage("Friends: " + ((BasicDBList) result.get("friends")).size());
fcs.sendMessage("rank: " + ((String) result.get("rank")));
fcs.sendMessage("Last online: " + ((Date) result.get("lastSeen")).toLocaleString());
fcs.sendMessage("Registered: " + ((Date) result.get("registered")).toLocaleString());
}
});
return true;
}
However now that we've already got one task in a command it may get messy to create another one inside of that, so that brings me to the next step, which is a very nice way to make this process way more beautiful.
Callbacks(top)
Creating callbacks is a very elegant way of hiding one or more tasks in the background and returning to where you were in the code you were writing only when the other tasks are done. The Runnable class used by the scheduler in the earlier snippets are basically callbacks as well, they just call back very quickly as all they have to wait for is the Minecraft tick to occur.
To create our own callback all we have to do is create an interface. I named mine FindOneCallback, and I gave it one function that can be called when the query is done. The callback class looks like this:
Code (Java):
public interface FindOneCallback {
public void onQueryDone(DBObject result);
}
public void onQueryDone(DBObject result);
}
Code (Java):
public static void findPlayerAsync(final DBObject query, final FindOneCallback callback) {
// Run outside of the tick loop
Bukkit.getScheduler().runTaskAsynchronously(plugin, new Runnable() {
@Override
public void run() {
DBCollection playerCollection = DatabaseHelper.getPlayerDatabase();
final DBObject result = playerCollection.findOne(query);
// go back to the tick loop
Bukkit.getScheduler().runTask(plugin, new Runnable() {
@Override
public void run() {
// call the callback with the result
callback.onQueryDone(result);
}
});
}
});
}
// Run outside of the tick loop
Bukkit.getScheduler().runTaskAsynchronously(plugin, new Runnable() {
@Override
public void run() {
DBCollection playerCollection = DatabaseHelper.getPlayerDatabase();
final DBObject result = playerCollection.findOne(query);
// go back to the tick loop
Bukkit.getScheduler().runTask(plugin, new Runnable() {
@Override
public void run() {
// call the callback with the result
callback.onQueryDone(result);
}
});
}
});
}
Reorganising the command(top)
Now all we have to do is patch our command, and you'll notice how much cleaner the command can become by utilizing this, as well as improving performance quite a bit.Code (Java):
@Override
public boolean onCommand(final CommandSender cs, Command cmd, String label, String[] args) {
if (args.length < 1) {
cs.sendMessage("Usage:");
cs.sendMessage(" /playerinfo <name>");
cs.sendMessage(" /playerinfo <uuid>");
return true;
}
String param = args[0];
String key = null;
if (param.length() <= 16 && param.length() >= 3) {
key = "lookupUsername";
} else {
param = param.replaceAll("-", "");
if (param.length() != 32) {
cs.sendMessage("Invalid username or uuid");
return true;
}
key = "uuid";
}
BasicDBObject query = new BasicDBObject(key, param);
DatabaseHelper.findPlayerAsync(query, new FindOneCallback() {
@Override
public void onQueryDone(DBObject result) {
if (result == null) {
cs.sendMessage("The specified player could not be found");
return;
}
cs.sendMessage("Information about " + ((String) result.get("username")));
cs.sendMessage("UUID: " + ((String) result.get("uuid")));
cs.sendMessage("money: " + ((Integer) result.get("money")));
cs.sendMessage("Fly: " + ((Boolean) result.get("fly")).toString());
cs.sendMessage("Friends: " + ((BasicDBList) result.get("friends")).size());
cs.sendMessage("rank: " + ((String) result.get("rank")));
cs.sendMessage("Last online: " + ((Date) result.get("lastSeen")).toLocaleString());
cs.sendMessage("Registered: " + ((Date) result.get("registered")).toLocaleString());
}
});
return true;
}
public boolean onCommand(final CommandSender cs, Command cmd, String label, String[] args) {
if (args.length < 1) {
cs.sendMessage("Usage:");
cs.sendMessage(" /playerinfo <name>");
cs.sendMessage(" /playerinfo <uuid>");
return true;
}
String param = args[0];
String key = null;
if (param.length() <= 16 && param.length() >= 3) {
key = "lookupUsername";
} else {
param = param.replaceAll("-", "");
if (param.length() != 32) {
cs.sendMessage("Invalid username or uuid");
return true;
}
key = "uuid";
}
BasicDBObject query = new BasicDBObject(key, param);
DatabaseHelper.findPlayerAsync(query, new FindOneCallback() {
@Override
public void onQueryDone(DBObject result) {
if (result == null) {
cs.sendMessage("The specified player could not be found");
return;
}
cs.sendMessage("Information about " + ((String) result.get("username")));
cs.sendMessage("UUID: " + ((String) result.get("uuid")));
cs.sendMessage("money: " + ((Integer) result.get("money")));
cs.sendMessage("Fly: " + ((Boolean) result.get("fly")).toString());
cs.sendMessage("Friends: " + ((BasicDBList) result.get("friends")).size());
cs.sendMessage("rank: " + ((String) result.get("rank")));
cs.sendMessage("Last online: " + ((Date) result.get("lastSeen")).toLocaleString());
cs.sendMessage("Registered: " + ((Date) result.get("registered")).toLocaleString());
}
});
return true;
}