这一节我们会编写一个类库(Class Library),用来简化插件开发的代码。
怎么简化呢?我们举个例子,有时候我们只需要设置物品的名称,这时候就得:
ItemMeta im = item.getItemMeta();
im.setDisplayName("SomeItem");
item.setItemMeta(im);
我们可以把它封装成一个方法:
public void setDisplayName(String name);
以后就只需要一行代码了。
我们首先来学习一下建造模式,这是一个概念。
建造模式通常有两个特点:
- 就地修改
- 返回修改后的对象
非建造模式的代码写起来像这样:
a.doThis();
a.doThat();
a.xxxx();
建造模式的代码写起来像这样:
a.doThis().doThat().xxxx();
这可以减少代码的行数,还能使得程序简洁。
下面我们就来试试吧!
既然我们学习了 Maven,我们自然要使用 Maven 啦!
在 IDEA 的「Project Structure」中添加新模块,选择 Maven,可随意命名,我就叫它「RarityCommons」。
!> RarityCommons 的许可协议
RarityCommons 不适用首页的许可,它遵循 GNU 通用公共许可证(第三版),请仔细阅读该许可证。该许可证也包含在其代码仓库中。
添加 Paper 作为依赖,在 pom.xml
中添加:
<repositories>
<repository>
<id>papermc</id>
<url>https://papermc.io/repo/repository/maven-public/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>com.destroystokyo.paper</groupId>
<artifactId>paper-api</artifactId>
<version>1.16.5-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
</dependencies>
单击右上角的「同步」,稍等一会,导入即完成。
在 java
下创建包,随意命名(只要你能够记住),我就命名为 rarityeg.commons
。
由于类库并不是一个插件,因此也不需要主类,不需要 plugin.yml
,我们可以直接开始编写工具类。
JavaPlugin
中的 instance
问题实在是令人不爽,我们干脆直接把它解决了。
创建类 JavaPluginR
,类名实际上无所谓,但为了自己好用,我就把修改过的类后面加上一个 R。
package rarityeg.commons;
import org.bukkit.plugin.java.JavaPlugin;
import javax.annotation.Nonnull;
public class JavaPluginR extends JavaPlugin {
private static JavaPluginR instance; // 小戏法
/**
* Gets the instance of this plugin.
*
* @return A instance of this plugin.
* @throws IllegalStateException When this plugin hasn't been loaded.
*/
@Nonnull
public static JavaPluginR getInstance() {
if (instance == null) { // null 就抛出错误
throw new IllegalStateException("This plugin hasn't been loaded yet!");
}
return instance; // 不是 null 就返回
}
public JavaPluginR() {
super(); // 父类构造方法,Bukkit 使用
instance = this; // 小戏法
}
}
代码原理很简单,但看上去「正式」了很多。
首先是我们将 instance
设为了 private
,这样可以防止其它类将它修改。
然后我们编写 getInstance
方法来获取 instance
。如果是 null
就抛出异常(实际上一般不可能)。
最后我们编写了构造方法,先调用 JavaPlugin
的构造方法,再将 instance
设为自己。
代码中出现了 @NonNull
,这表示「返回值不是 null
」,这是为了方便自己,同时避免了 NullPointerException
。
那剩下要解释的就只有这一部分了:
/**
* Gets the instance of this plugin.
*
* @return A instance of this plugin.
* @throws IllegalStateException When this plugin hasn't been loaded.
*/
这是 JavaDocs 的语法。由于我们的类库最终可能要被别人使用,因此相关的文档要写在其中。这里的文档最终可以被 JavaDoc 工具转换为网页,或者,即使不作为网页,IDEA 也会合理显示 JavaDocs。写好 JavaDocs 很重要!
当你在编写时,你只需要输入 /**
,并按回车,IDEA 就会为你自动编写好左边每一行的 *
,并且当你换行时也会自动添加 *
。
@returns
表示返回值,后面跟的就是介绍。
@throws
表示可能抛出的异常,先写异常类型再(空一格)写什么时候抛出异常。
没有前缀的就是该方法的介绍。
这样,以后我们编写插件,只需要 extends JavaPluginR
,再 getInstance
就好啦!没必要自己去写啦!
我们编写这样一个类来帮助我们构造 ItemStack
,符合建造模式。
package rarityeg.commons;
import net.md_5.bungee.api.chat.BaseComponent;
import org.bukkit.Bukkit;
import org.bukkit.attribute.Attribute;
import org.bukkit.attribute.AttributeModifier;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import javax.annotation.Nonnull;
import javax.annotation.ParametersAreNonnullByDefault;
import javax.annotation.ParametersAreNullableByDefault;
import java.util.*;
public class ItemBuilder {
@Nonnull
private final ItemStack stack;
@Nonnull
private ItemMeta meta;
@ParametersAreNonnullByDefault
public ItemBuilder(ItemStack originStack) {
stack = originStack;
meta = Objects.requireNonNullElse(originStack.getItemMeta(), Bukkit.getItemFactory().getItemMeta(stack.getType()));
}
@ParametersAreNonnullByDefault
public ItemBuilder(ItemStack originStack, ItemMeta originMeta) {
stack = originStack;
meta = originMeta;
}
/**
* Return the ItemStack.
*
* @return The modified ItemStack.
*/
@Nonnull
public ItemStack toItemStack() {
stack.setItemMeta(meta);
return stack;
}
@Nonnull
@ParametersAreNullableByDefault
public ItemBuilder setDisplayName(String name) {
if (name == null) {
return this;
}
meta.setDisplayName(name);
return this;
}
@Nonnull
@ParametersAreNullableByDefault
public ItemBuilder setDisplayName(BaseComponent... name) {
if (name == null) {
return this;
}
meta.setDisplayNameComponent(name);
return this;
}
@Nonnull
@ParametersAreNullableByDefault
public ItemBuilder setLore(String... lore) {
if (lore == null) {
return this;
}
meta.setLore(Arrays.asList(lore));
return this;
}
@Nonnull
@ParametersAreNullableByDefault
public ItemBuilder setLore(BaseComponent[]... lore) {
if (lore == null) {
return this;
}
meta.setLoreComponents(Arrays.asList(lore));
return this;
}
/**
* Insert something after the lore.
*
* @param lore What to prepend.
* @return The builder.
*/
@Nonnull
@ParametersAreNullableByDefault
public ItemBuilder appendLore(String... lore) {
return insertLoreAt(-1, lore);
}
/**
* Insert something after the lore.
*
* @param lore What to prepend.
* @return The builder.
*/
@Nonnull
@ParametersAreNullableByDefault
public ItemBuilder appendLore(BaseComponent[]... lore) {
return insertLoreAt(-1, lore);
}
/**
* Insert something to the very start of the lore.
*
* @param lore What to prepend.
* @return The builder.
*/
@Nonnull
@ParametersAreNullableByDefault
public ItemBuilder prependLore(String... lore) {
return insertLoreAt(0, lore);
}
/**
* Insert something to the very start of the lore.
*
* @param lore What to prepend.
* @return The builder.
*/
@Nonnull
@ParametersAreNullableByDefault
public ItemBuilder prependLore(BaseComponent[]... lore) {
return insertLoreAt(0, lore);
}
/**
* Insert a series of things into the Lore at a specified position.
*
* @param position Where to insert, negative to count from back.
* @param lore What to insert.
* @return The builder.
*/
@Nonnull
@ParametersAreNullableByDefault
public ItemBuilder insertLoreAt(int position, String... lore) {
if (lore == null) {
return this;
}
List<String> rawLore = meta.getLore();
// If null then create
if (rawLore == null || rawLore.size() == 0) {
meta.setLore(Arrays.asList(lore));
return this;
}
// Reverse position
if (position < 0) {
position = rawLore.size() + position;
}
// If writeable
if (rawLore instanceof ArrayList || rawLore instanceof Vector || rawLore instanceof LinkedList) {
rawLore.addAll(position, Arrays.asList(lore));
meta.setLore(rawLore);
return this;
}
// Change to Arraylist then add
List<String> properLore = Arrays.asList(rawLore.toArray(new String[0]));
properLore.addAll(position, Arrays.asList(lore));
meta.setLore(properLore);
return this;
}
/**
* Insert a series of things into the Lore at a specified position.
*
* @param position Where to insert, negative to count from back.
* @param lore What to insert.
* @return The builder.
*/
@Nonnull
@ParametersAreNullableByDefault
public ItemBuilder insertLoreAt(int position, BaseComponent[]... lore) {
if (lore == null) {
return this;
}
List<BaseComponent[]> rawLore = meta.getLoreComponents();
if (rawLore == null || rawLore.size() == 0) {
meta.setLoreComponents(Arrays.asList(lore));
return this;
}
if (position < 0) {
position = rawLore.size() + position;
}
if (rawLore instanceof ArrayList || rawLore instanceof Vector || rawLore instanceof LinkedList) {
rawLore.addAll(position, Arrays.asList(lore));
meta.setLoreComponents(rawLore);
return this;
}
List<BaseComponent[]> properLore = Arrays.asList(rawLore.toArray(new BaseComponent[0][0]));
properLore.addAll(position, Arrays.asList(lore));
meta.setLoreComponents(properLore);
return this;
}
@Nonnull
@ParametersAreNullableByDefault
public ItemBuilder setLoreByLines(String lore) {
if (lore == null) {
return this;
}
return setLore(lore.split("\\r?\\n"));
}
@Nonnull
public ItemBuilder dropChanges() {
meta = stack.getItemMeta();
return this;
}
@Nonnull
public ItemBuilder setAmount(int amount) {
stack.setAmount(amount);
return this;
}
@Override
@Nonnull
public ItemBuilder clone() {
return new ItemBuilder(stack.clone(), meta.clone());
}
/**
* To check if the two ItemBuilders will produce same results.
*
* @param builder The object to compare.
* @return If the stacks are same.
*/
@Override
@ParametersAreNonnullByDefault
public boolean equals(Object builder) {
if (!(builder instanceof ItemBuilder)) {
return false;
}
if (this == builder) {
return true;
}
ItemStack spare = ((ItemBuilder) builder).clone().toItemStack();
ItemStack self = clone().toItemStack();
return spare.equals(self);
}
@Override
public int hashCode() {
return clone().toItemStack().hashCode();
}
@Nonnull
@ParametersAreNullableByDefault
public ItemBuilder addEnchants(int level, Enchantment... enchantment) {
if (enchantment == null) {
return this;
}
for (Enchantment e : enchantment) {
if (e != null) {
meta.addEnchant(e, level, true);
}
}
return this;
}
@Nonnull
@ParametersAreNullableByDefault
public ItemBuilder removeEnchants(Enchantment... enchantment) {
if (enchantment == null) {
return this;
}
for (Enchantment e : enchantment) {
if (e != null) {
meta.removeEnchant(e);
}
}
return this;
}
@Nonnull
public ItemBuilder setUnbreakable(boolean unbreakable) {
meta.setUnbreakable(unbreakable);
return this;
}
/**
* Set an attribute modifier, null to remove.
*
* @param attribute The attribute.
* @param am The modifier.
* @return The builder.
*/
@Nonnull
@ParametersAreNullableByDefault
public ItemBuilder setAttributeModifier(Attribute attribute, AttributeModifier am) {
if (attribute == null) {
return this;
}
if (am == null) {
meta.removeAttributeModifier(attribute);
return this;
}
meta.addAttributeModifier(attribute, am);
return this;
}
}
这里我们写了一些有用的方法。
现在如果要创建一个物品并进行设置就很简单啦:
ItemBuilder ib = new ItemBuilder(new ItemStack(Material.TORCH));
ib.setDisplayName("大火球").setAmount(64).appendLore("提供光亮").prependLore("一个大大的火球").toItemStack();
我们设计这个类用于执行 Bukkit
对象中的方法,由于 Bukkit
的方法不能在异步方法中被使用,我们设计这个类来完成同步。由于线程设计原因,那些 getXXX
方法没法使用,我们就只设计那些操作性的方法吧。
package rarityeg.commons;
import net.md_5.bungee.api.chat.BaseComponent;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.NamespacedKey;
import org.bukkit.command.CommandSender;
import org.bukkit.inventory.Recipe;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitRunnable;
import javax.annotation.Nonnull;
import javax.annotation.ParametersAreNonnullByDefault;
import javax.annotation.ParametersAreNullableByDefault;
/**
* Call Bukkit methods asynchronously with BukkitRunnable, not supported for getters.
*
* @see Bukkit
*/
public class SyncBukkit {
@Nonnull
private final JavaPlugin pluginInstance;
@ParametersAreNonnullByDefault
public SyncBukkit(JavaPlugin plugin) {
pluginInstance = plugin;
}
@ParametersAreNullableByDefault
public void dispatchCommand(CommandSender cs, String cmd) {
if (cs == null || cmd == null) {
return;
}
new BukkitRunnable() {
@Override
public void run() {
Bukkit.dispatchCommand(cs, cmd);
}
}.runTask(pluginInstance);
}
public void setMaxPlayers(int count) {
new BukkitRunnable() {
@Override
public void run() {
Bukkit.setMaxPlayers(count);
}
}.runTask(pluginInstance);
}
public void setWhiteList(boolean useWhiteList) {
new BukkitRunnable() {
@Override
public void run() {
Bukkit.setWhitelist(useWhiteList);
}
}.runTask(pluginInstance);
}
public void reloadWhiteList() {
new BukkitRunnable() {
@Override
public void run() {
Bukkit.reloadWhitelist();
}
}.runTask(pluginInstance);
}
@ParametersAreNullableByDefault
public void broadcast(String... msg) {
if (msg == null) {
return;
}
new BukkitRunnable() {
@Override
public void run() {
for (String s : msg) {
if (s != null) {
Bukkit.broadcastMessage(s);
}
}
}
}.runTask(pluginInstance);
}
@ParametersAreNullableByDefault
public void broadcast(BaseComponent... components) {
if (components == null) {
return;
}
new BukkitRunnable() {
@Override
public void run() {
Bukkit.broadcast(components);
}
}.runTask(pluginInstance);
}
public void reload() {
new BukkitRunnable() {
@Override
public void run() {
Bukkit.reload();
}
}.runTask(pluginInstance);
}
public void reloadData() {
new BukkitRunnable() {
@Override
public void run() {
Bukkit.reloadData();
}
}.runTask(pluginInstance);
}
@ParametersAreNullableByDefault
public void addRecipe(Recipe... recipes) {
if (recipes == null) {
return;
}
new BukkitRunnable() {
@Override
public void run() {
for (Recipe r : recipes) {
Bukkit.addRecipe(r);
}
}
}.runTask(pluginInstance);
}
public void clearRecipes() {
new BukkitRunnable() {
@Override
public void run() {
Bukkit.clearRecipes();
}
}.runTask(pluginInstance);
}
public void resetRecipes() {
new BukkitRunnable() {
@Override
public void run() {
Bukkit.resetRecipes();
}
}.runTask(pluginInstance);
}
@ParametersAreNullableByDefault
public void removeRecipe(NamespacedKey... name) {
if (name == null) {
return;
}
new BukkitRunnable() {
@Override
public void run() {
for (NamespacedKey n : name) {
if (n != null) {
Bukkit.removeRecipe(n);
}
}
}
}.runTask(pluginInstance);
}
public void setSpawnRadius(int radius) {
new BukkitRunnable() {
@Override
public void run() {
Bukkit.setSpawnRadius(radius);
}
}.runTask(pluginInstance);
}
public void shutdown() {
new BukkitRunnable() {
@Override
public void run() {
Bukkit.shutdown();
}
}.runTask(pluginInstance);
}
public void setDefaultGameMode(GameMode mode) {
new BukkitRunnable() {
@Override
public void run() {
Bukkit.setDefaultGameMode(mode);
}
}.runTask(pluginInstance);
}
public void reloadPermissions() {
new BukkitRunnable() {
@Override
public void run() {
Bukkit.reloadPermissions();
}
}.runTask(pluginInstance);
}
public void reloadCommandAliases() {
new BukkitRunnable() {
@Override
public void run() {
Bukkit.reloadCommandAliases();
}
}.runTask(pluginInstance);
}
}
另外我们还修改了 JavaPluginR
,将 SyncBukkit
作为了它的一个对象。
package rarityeg.commons;
import org.bukkit.plugin.java.JavaPlugin;
import javax.annotation.Nonnull;
public class JavaPluginR extends JavaPlugin {
private static JavaPluginR instance;
@Nonnull
protected final SyncBukkit syncBukkit;
/**
* Gets the instance of this plugin.
*
* @return A instance of this plugin.
* @throws IllegalStateException When this plugin hasn't been loaded.
*/
@Nonnull
public JavaPluginR getInstance() {
if (instance == null) {
throw new IllegalStateException("This plugin hasn't been loaded yet!");
}
return instance;
}
/**
* Return a synced Bukkit instance so that you can call them in asynchronous methods.
*
* @return The synced Bukkit.
* @see SyncBukkit
*/
@Nonnull
public SyncBukkit getSyncBukkit() {
return syncBukkit;
}
public JavaPluginR() {
super();
instance = this;
syncBukkit = new SyncBukkit(this);
}
}
到这里,我们就创建好了 JavaPluginR
、SyncBukkit
和 ItemBuilder
,提供了一些方便的方法。
这个类用于完成 NMS 反射的基础工作。
package rarityeg.commons;
import org.bukkit.Bukkit;
public class Reflector {
private static String VERSION = "";
private static String NMS_PACKAGE = "";
private static String OBC_PACKAGE = "";
static {
String version = Bukkit.getMinecraftVersion();
String v1 = version.split("\\.")[0];
String v2 = version.split("\\.")[1];
String nmsBaseHead = "net.minecraft.server.";
for (int i = 1; i <= 20; i++) {
try {
Class.forName(nmsBaseHead + "v" + v1 + "_" + v2 + "_R" + i + ".ItemStack");
VERSION = "v" + v1 + "_" + v2 + "_R" + i;
NMS_PACKAGE = nmsBaseHead + VERSION;
OBC_PACKAGE = "org.bukkit.craftbukkit." + VERSION;
} catch (ClassNotFoundException ignored) {
}
}
}
public static String getNMSPackage() {
return NMS_PACKAGE;
}
public static String getOBCPackage() {
return OBC_PACKAGE;
}
public static String getVersion() {
return VERSION;
}
}
这里仅完成了基础工作,其它工作就交由插件开发者完成吧(笑)。
编写这个类用来分析命令的参数等信息,检查命令是否符合要求。
package rarityeg.commons;
public class CommandValidator {
public enum ArgType {
STRING,
NUMBER,
BOOLEAN
}
/**
* Validate arguments using type assertion.
*
* @param args Arguments of this command.
* @param types ArgType to compare.
* @return If the arguments matches those types.
* @see ArgType
*/
public static boolean validate(String[] args, ArgType... types) {
if (args.length < types.length) {
return false;
}
for (int i = 0; i <= types.length; i++) {
if (types[i] == ArgType.STRING) {
continue;
}
if (types[i] == ArgType.NUMBER) {
if (!isNumber(args[i])) {
return false;
}
continue;
}
if (types[i] == ArgType.BOOLEAN) {
if (!isBoolean(args[i])) {
return false;
}
}
}
return true;
}
/**
* Check if the string is a boolean, like "y...", "n...", "true", "false", "t", "f"
*
* @param s The string to check.
* @return If the string is a boolean.
*/
protected static boolean isBoolean(String s) {
String sLow = s.toLowerCase();
return sLow.startsWith("y") || sLow.startsWith("n") || sLow.equals("true") || sLow.equals("false") || sLow.equals("t") || sLow.equals("f");
}
/**
* Check if the string contains a number.
*
* @param s The string to check.
* @return If the string contains a number.
*/
protected static boolean isNumber(String s) {
try {
Integer.parseInt(s);
return true;
} catch (NumberFormatException e) {
try {
Double.parseDouble(s);
return true;
} catch (NumberFormatException e2) {
return false;
}
}
}
}
原理很简单,就是判断对应的参数是否符合。
最后我们来改进这个类,将文件的读取变得简单些,加上了两个方法而已。
@ParametersAreNonnullByDefault
public String readFile(File f) {
try {
StringBuilder builder = new StringBuilder();
BufferedReader reader = new BufferedReader(new FileReader(f));
String temp;
while ((temp = reader.readLine()) != null) {
builder.append(temp);
}
reader.close();
return builder.toString();
} catch (IOException e) {
e.printStackTrace();
return "";
}
}
@ParametersAreNonnullByDefault
public void writeFile(File f, Serializable data) {
String dataS = data.toString();
try {
BufferedWriter writer = new BufferedWriter(new FileWriter(f));
writer.write(dataS);
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
现在 RarityCommons 就可以用了,虽然我们只在里面放了一些非常非常基础的内容,但它们确实可以方便插件的开发。