Spigot Plugin Development Tutorial
1 First Plugin
Create New Project
new project > minecraft > spigot plugin
Build Settings > GroupId: com.bowen-craft.plugin
ArtifactId: PluginName
Compile & Run
Run: right upper ▶️ button
2 Event Listeners & Javadocs
Event Listeners
To have event listeners in the class:
public final class EventListeners extends JavaPlugin { }
public final class EventListeners extends JavaPlugin implements Listener { }
In class:
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event)
Remember to register (Usually register in onEnable class):
getSever().getPluginManager().registerEvents(listener, plugin);
- 在同一个class中只需要register一次
Javadocs:
https://hub.spigotmc.org/javadocs/bukkit/
3 External Classes Event Listeners & Cancellable Events
Create External Classes Event Listeners
New package under java folder: com.bowen-craft.plugin1 > New… > Package > listeners (renameable) ↓
New class: listeners > new class > name
In the class: public class NameListener implements Listener {}
In main class: add
getServer().getPluginManager().registerEvents(new NameListener(), this);
Cancel Event
e.setCancelled(true);
4 Chat Color
Chat Color
ChatColor.YELLOW
5 Commands Part 1
In class:
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (command.getName().equalsIgnoreCase("command")) {
if (sender instanceof Player) {
}
}
return true;
}
In plugin.yml
, add:
Commands:
die:
description: An easy way to kill a player
usage: /<command>
plugin.yml
usage: https://www.spigotmc.org/wiki/plugin-yml/
6 Commands Part 2 - External class commands
1 Create new package commands
2 Create new class CommandName
under commands package
3 In new class:
public class GodCommand implements CommandExecutor {
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if(sender instanceof Player) {
}
return true;
}
}
4 In main class:
@Override
public void onEnable() {
getCommand("name").setExecutor(new ClassName());
}
5 In plugin.yml
:
commands:
name:
description: Become invincible
6.5 Permissions
if (player.hasPermission("commandsparts2.feed")) {
}
7 Configuration
1 Under resources
folder, create a new file named config.yml
2 In main class, add:
@Override
public void onEnable() {
getConfig().options().copyDefaults();
saveDefaultConfig();
}
3 In config.yml - Keep yaml structure:
Food: 'Potato'
FoodList:
- 'Pizza'
- 'Organge'
- 'Grape'
4 To get content in the config.yml:
getConfig().getString("Food");
getConfig().getStringList("FoodList").get(2);
Here it will return Potato
and Orange
.
5 To set content in the config.yml:
getConfig().set("Food", "Shrimp");
In external class to use the config
In the external class:
Plugin plugin = MainClassName.getPlugin(MainClassName.class);
To get config content:
plugin.getConfig().getString("Name");
7.5 To Passing an Plugin Instance
1 - Static getter method to access plugin instance
2 - Constructor injection (using a constructor)
1 Static Method
1 In main class:
private static MainClassName plugin;
2 Set plugin in onEnable()
method:
plugin = this;
3 Create a getter method in main class:
public static MainClassName getPlugin() {
return plugin;
}
Use Plugin
in external class
MainClassName.getPlugin()
MainClassName.getPlugin().getConfig();
2 Constructor Injection Way
In external class:
private final MainClassName plugin;
public ExternalClassName(MainClassName plugin) {
this.plugin = plugin;
}
In main
class onEnable
method:
for listeners:
getServer().getPluginManager().registerEvents(new ExternalClassName(this), this);
for commands:
getCommand("setspawn").setExecutor(new CommandClassName(this));
8 Command Arguments
Arguments:
/kill
Args[] - [“player”, “time”, “place”]
9 Use Name to Target Player
Player target = Bukkit.getServer().getPlayerExact(playerName);
if(target == null) {
p.sendMessage("This player is not online or doesn't exist.")
} else {
}
Get display name - p.getDisplayName()
9.? Change Compile Directory
Default compile directory: Under target folder
When using maven
as plugin manager, you can find pom.xml
,
Add content between
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.3.1</version>
<configuration>
<outputDirectory>/my/path</outputDirectory>
</configuration>
</plugin>
and change /my/path
into the plugins folder directory.
9.5 Spawn Plugin
Save location
In command class:
Location location = player.getLocation();
// method 1
plugin.getConfig().set("spawn.x", location.getX());
plugin.getConfig().set("spawn.y", location.getY());
plugin.getConfig().set("spawn.z", location.getZ());
// method 2 (when key is same)
plugin.getConfig().set("spawn", location);
plugin.saveConfig();
In config.yml:
spawn:
x: 69
y: 69
z: 69
Load Location
Location location = plugin.getConfig().getLocation("spawn");
if (location != null) {
player.teleport(location);
}
10 Inventories and Items
Inventory and Item
Inventory inv = Bukkit.createInventary(player, size, title); //size = 9, 18, 27, 36, 45, 54
ItemStack item = new ItemStack(Material.DIAMOND_HOE, amount); // set item
ItemMeta itemMeta = item.getItemMeta(); // get ItemMeta of the item
itemMeta.setDisplayName(ChatColor.GREEN + "Click Me"); // Modify itemMeta property
item.setItemMeta(itemMeta); // Set itemmeta back to item;
inv.setItem(index, item); // set item into inventory
p.openInventory(inv); // open inventory for player
Add Item to inventory
ItemStack item = new ItemStack(Material.DIAMOND_HOE, 1);
inv.addItem(item);
Modify Lore
ItemStack item = new ItemStack(Material.DIAMOND_HOE, 1);
ItemMeta itemMeta = item.getItemMeta();
itemMeta.setDisplayName(ChatColor.GREEN + "Click Me");
ArrayList<String> lore = new ArrayList<>();
lore.add("Lore line 1");
lore.add("Lore line 2");
flowerMeta.setLore(lore);
item.setItemMeta(itemMeta);
Inventory Item Click & Cancel click
In a new listener class:
public void onMenuClick(InventoryClickEvent e) {
if(e.getView().getTitle().equalsIgnoreCase("MenuName")){
if(e.getCurrentItem() == null){
return;
}
if(e.getCurrentItem().getItemMeta().getDisplayName().equalsIgnoreCase("Name")){
System.out.println("Message!");
}
e.setCancelled(true);
}
}
10.5 Cooldown
Create a hashmap, store players’ UUID and their cooldown time.
In the class:
private final HashMap<UUID, Long> cooldown;
public ClassName() {
this.cooldown = new HashMap<>();
}
In the method:
if (!cooldown.containsKey(player.getUniqueId()) || System.currentTimeMillis() - cooldown.get(player.getUniqueId()) > 5000) {
cooldown.put(player.getUniqueId(), System.currentTimeMillis());
player.sendMessage("Command executed!");
}else{
player.sendMessage("You can't use the command again for another " + (5000 - (System.currentTimeMillis() - cooldown.get(player.getUniqueId()))) + " milliseconds!");
}
11 Fly Plugin
Use ArrayList to store player
Create arraylist
private ArrayList<player> list_of_flying_players = new ArrayList<>();
Check
if (list_of_flying_players.contains(player))
Add / remove
list_of_flying_players.remove(player);
list_of_flying_players.add(player);
Hook messages in yaml file into plugin
In config.yml
:
off-message: '&8[&2Illuminati&eFly&8] &3You can no longer fly.'
on-message: '&8[&2Illuminati&eFly&8] &bYou can fly now. Be free!'
In main class onEnable()
method:
getConfig().options().copyDefaults();
saveDefaultConfig();
In external class build a constructor:
private MainClassName plugin;
// generate constructor
public Externalclassname(MainClassName plugin) {
this.plugin = plugin;
}
Get config message
plugin.getConfig().getString("on-message");
Translate Color in string into color code
ChatColor.translateAlternateColorCodes('&', message);
12 Teleport Bow
Create an utility
folder to save utils
In utility > BowUtils
,
public class BowUtils {
public static ItemStack createTeleportBow() {
ItemStack bow = new ItemStack(Material.BOW, 1);
return bow;
}
}
In other class - ItemStack bow = BowUtils.createTeleportBow();
When use config in the static itemstack
1 remove the static
2 In other class using
private final MainClassName plugin;
private final BowUtils bowUtils;
public GiveCommand(TeleportBow plugin) {
this.plugin = plugin;
this.bowUtils = new BowUtils(plugin);
}
13 Entities
Spawn entities
player.getWorld().spawnEntity(location, TYPE);
14 Armor Stands
ArmorStand armorstand = (ArmorStand) player.getWorld().spawnEntity(plauer.getLocation(), EntityType.ARMOR_STAND);
armorstand.setHelmet(new ItemStack(Materia.JUNGLE_PLANKS));
armorstand.setHeadPose(new EulerAngle(Math.toRadians(90), 0, 0));
15 GUI
To cancel click for just gui (before 1.14.4)
Player p = (player) e.getWhoClicked();
if (e.getClickedInventory().getTitle().equalsIgnoreCase(ChatColor.AQUA + "CustomGUI")) {
e.setCancelled(true);
player.closeInventory();
}
To cancel click for inv and the gui (after 1.14.4)
Player p = (player) e.getWhoClicked();
if(e.getView().getTitle().equalsIgnoreCase("MenuName")){
e.setCancelled(true);
if (e.getCurrentItem() == null) {
return; // to prevent null (when nothing in slot)
}
}
21 Holograms
player.getWorld().spawnEntity(plauer.getLocation().add(0, 0, 0), EntityType.ARMOR_STAND);
22 Custom Config Files
Create an files
folder, create an new class in the folder:
private static File file;
private static FileConfiguration customFile;
public static FileConfiguration get() {
return customFile;
}
// finds or generates the custom config file
public static void setup() {
file = new File(Bukkit.getServer().getPluginManager().getPlugin("MainClassName").getDataFolder(), "filename.yml");
if (!file.exists()){
try{
file.createNewFile();
} catch(IOException e){}
}
customFile = YamlConfiguration.loadConfiguration(file);
}
public static void save() {
try{
customFile.save(file);
} catch(IOException e){
System.out.println("Couldn't save file");
}
}
public static void reload(){
customFile = YamlConfiguration.loadConfiguration(file);
}
In main file:
public void onEnable() {
// Setup config
getConfig().options().copyDefaults();
saveDefaultConfig();
CustomConfig.setup();
// CustomConfig.get().addDefault("Data", "value");
// setup default value in the file
CustomConfig.get().options().copyDefaults(true);
CustomConfig.save();
}
If you need a custom config file.yml, you need also to generate a regular config.yml.
In resources
folder, create config.yml & filename.yml.
23 Signs
Set block on player’s place:
player.getWorld().getBlockAt(player.getLocation()).setType(Material.SIGN);
In @EventHandler
:
e.getBlock().setType(Material.DIAMOND_BLOCK);
24 Ban GUI Plugin
Player p = (Player) sender;
ArrayList<Player> list = new ArrayList<>(p.getServer().getOnlinePlayers());
for (int i=0; i<list.size(); i++){
ItemStack playerHead = new ItemStack(Material.PLAYER_HEAD, 1);
ItemMeta meta = playerHead.getItemMeta();
playerHead.setItemMeta(meta);
bangui.addItem(playerHead);
}
25 APIs
In File>Project Structure>Library
, click +
and select jar
, then click apply
.
In plugin.yml
, add line
depend: [ActionBarAPI]
Remeber to register dependency in the pom.xml
26 RandomTP
Implement random:
Random random = new Random();
int x = random.nextInt(1000);
Bad blocks list:
public static HashSet<Material> bad_blocks = new HashSet<>();
static {
bad_blocks.add(Material.GRASS_BLOCK);
}
return !(bad_blocks.contains(block.getType() || block.getType().isSolid()));
Highest Blocks:
y = randomLocation.getWorld().getHighestBlockYAt(randomLocation);
28 MangoDB
https://www.youtube.com/watch?v=tUs1CY7SmV8&list=PLfu_Bpi_zcDNEKmR82hnbv9UxQ16nUBF7&index=38
29 Teleport Plugin
Use try…catch
try{
player.teleport(target.getLocation());
} catch (NullPointerException e){
player.sendMessage(ChatColor.RED + "Player does not exist.");
}
Loop every single player in the server
for (Player p : Bukkit.getServer().getOnlinePlayers())
Check server online player amount
if (Bukkit.getServer().getOnlinePlayers().size() == 1)
30 Title Packets
https://www.youtube.com/watch?v=TRgu3h8CQZc&list=PLfu_Bpi_zcDNEKmR82hnbv9UxQ16nUBF7&index=40
32 Custom Events
Event - BukkitWiki https://bukkit.fandom.com/wiki/Event_API_Reference#Creating_Custom_Events
Create an new event
In custom event class:
private static final HandlerList handlers = new HandlerList();
// add codes
public HandlerList getHandlers() {
return handlers;
}
public static HandlerList getHandlerList() {
return handlers;
}
Example
Create custom event:
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
public final class CustomEvent extends Event {
private static final HandlerList handlers = new HandlerList();
private String message;
public CustomEvent(String example) {
message = example;
}
public String getMessage() {
return message;
}
public HandlerList getHandlers() {
return handlers;
}
public static HandlerList getHandlerList() {
return handlers;
}
}
call the custom event:
// Create the event here
CustomEvent event = new CustomEvent("Sample Message");
// Call the event
Bukkit.getServer().getPluginManager().callEvent(event);
// Now you do the event
Bukkit.getServer().broadcastMessage(event.getMessage());
for listener class:
public final class CustomListener implements Listener {
@EventHandler
public void normalLogin(CustomEvent event) {
// Some code here
}
}
register the listener:
Bukkit.getServer().getPluginManager().registerEvents(new GameListeners(), this);
make the event cancellable:
public final class CustomEvent extends Event implements Cancellable {
private static final HandlerList handlers = new HandlerList();
private String message;
private boolean cancelled;
public CustomEvent(String example) {
message = example;
}
public String getMessage() {
return message;
}
public boolean isCancelled() {
return cancelled;
}
public void setCancelled(boolean cancel) {
cancelled = cancel;
}
public HandlerList getHandlers() {
return handlers;
}
public static HandlerList getHandlerList() {
return handlers;
}
}
cancel event:
// Create the event here
CustomEvent event = new CustomEvent("Sample Message");
// Call the event
Bukkit.getServer().getPluginManager().callEvent(event);
// Check if the event is not cancelled
if (!event.isCancelled()) {
// Now you do the event
Bukkit.getServer().broadcastMessage(event.getMessage());
}
33 Silk Touch Spawners
to convert block state:
BlockStateMeta meta = (BlockStateMeta) spawner_to_give.getItemMeta();
34 Tasks and Schedulers
Runnable / Schedule
Bukkit循环”BukkitRunnable” https://zhuanlan.zhihu.com/p/344548127
Scheduler Programming https://bukkit.fandom.com/wiki/Scheduler_Programming
Runnable task
Create an task
Create an tasks
package(folder), then create an new class:
public class ExampleTask extends BukkitRunnable {
private final JavaPlugin plugin;
public ExampleTask(JavaPlugin plugin) {
this.plugin = plugin;
}
@Override
public void run() {
// What you want to schedule goes here
plugin.getServer().broadcastMessage("Welcome to Bukkit! Remember to read the documentation!");
}
}
In main
class:
BukkitTask cooltask = new TaskName(this).runTaskLater(this, 20L); // delay
BukkitTask cooltask = new TaskName(this).runTaskTimer(this, 0L, 100L); // repeat
Cancel a task
this.cancel();
Anonymous BukkitRunnable Example:
new BukkitRunnable() {
@Override
public void run() {
// What you want to schedule goes here
plugin.getServer().broadcastMessage(
"Welcome to Bukkit! Remember to read the documentation!");
}
}.runTaskLater(this.plugin, 20);
Async Example:
BukkitTask task = new ExampleTask(this.plugin).runTaskAsynchronously(this.plugin);
Lambda:
Runnable r = new Runnable() {
@Override
public void run() {
System.out.print("Run method");
}
};
Runnable r = ()-> System.out.print("Run method");
Schedule
Delay
public final class ExamplePlugin extends JavaPlugin {
public void onEnable() {
BukkitScheduler scheduler = getServer().getScheduler();
scheduler.scheduleSyncDelayedTask(this, new Runnable() {
@Override
public void run() {
// Do something
}
}, 20L);
}
}
Repeat
public final class ExamplePlugin extends JavaPlugin {
public void onEnable() {
BukkitScheduler scheduler = getServer().getScheduler();
scheduler.scheduleSyncRepeatingTask(this, new Runnable() {
@Override
public void run() {
// Do something
}
}, 0L, 20L);
}
}
Use Async Lambda
BukkitScheduler scheduler = getServer().getScheduler();
scheduler.runTaskAsynchronously(this, () -> {
// Do something in an async task off the main thread
// Very useful for database operations or web requests
});
Tips for thread safety
The Bukkit API, with the exception of the scheduler package, is not thread safe nor guaranteed to be thread safe.
- Asynchronous tasks should never access any API in Bukkit.
- Do not access or modify shared collections from your asynchronous tasks. Normal collections are not thread-safe. This also applies to objects which are not thread safe.
- An asynchronous task can schedule a synchronous task.
- A synchronous task can schedule an asynchronous task.
- If you want to schedule something at a fixed time, by calculating how many ticks until that point in time, you should use an asynchronous task. If you don’t, lag will increase the delay.