Skip to content

Latest commit

 

History

History
930 lines (770 loc) · 24.6 KB

6-3.md

File metadata and controls

930 lines (770 loc) · 24.6 KB

6-3 为更好的自己

这一节我们会编写一个类库(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 通用公共许可证(第三版),请仔细阅读该许可证。该许可证也包含在其代码仓库中。
GPLv3

添加 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

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 就好啦!没必要自己去写啦!

ItemBuilder

我们编写这样一个类来帮助我们构造 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();

SyncBukkit

我们设计这个类用于执行 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);
    }


}

到这里,我们就创建好了 JavaPluginRSyncBukkitItemBuilder,提供了一些方便的方法。

Reflector

这个类用于完成 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;
    }
    
}

这里仅完成了基础工作,其它工作就交由插件开发者完成吧(笑)。

CommandValidator

编写这个类用来分析命令的参数等信息,检查命令是否符合要求。

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;
            }
        }
    }

}

原理很简单,就是判断对应的参数是否符合。

再谈 JavaPluginR

最后我们来改进这个类,将文件的读取变得简单些,加上了两个方法而已。

@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 就可以用了,虽然我们只在里面放了一些非常非常基础的内容,但它们确实可以方便插件的开发。