Skip to content

Latest commit

 

History

History
528 lines (440 loc) · 19.1 KB

AC-3-2.md

File metadata and controls

528 lines (440 loc) · 19.1 KB

AC-3-2 CuteCoin Part 2

未完成的工作

首先,我们完成上一节中没做的一项工作,在主类的 onEnable 方法中:

AbstractDataManager.getDataManagerInstance().load();

我们初始化了一下数据管理器。

plugin.yml

现在我们来编写 plugin.yml 以及其中的命令。设计好的命令有:转账、查询、兑换。记得,plugin.yml 放在 resources 下。

main: rarityeg.cutecoin.CuteCoin
name: CuteCoin
version: 1.0
api-version: 1.16
database: true
commands:
  exchange:
    aliases:
      - "EX"
  transfer:
    aliases:
      - "trans"
  my-coins:
    aliases:
      - "query"
      - "mycoins"
      - "coins"

文本处理器

在继续编写命令处理器之前,我们要将从配置中读出的数据「渲染」成最终的颜色,为此,我专门创建了一个类:

package rarityeg.cutecoin;

import org.bukkit.configuration.ConfigurationSection;

import java.util.Objects;

public final class InsertedString {
    private int index = 1;
    private String content;

    public InsertedString(String s) {
        content = Objects.requireNonNullElse(s, "");
    }

    public void applyCoin(Coin c, int count) {
        ConfigurationSection coinData = CuteCoin.getInstance().getConfig().getConfigurationSection("coins." + c.getName());
        content = content
                .replace(generateReplaceHolder("name"), c.getName())
                .replace(generateReplaceHolder("max"), "" + c.getMax())
                .replace(generateReplaceHolder("description"), c.getDescription())
                .replace(generateReplaceHolder("count"), "" + count);

    }

    public void next() {
        ++index;
    }

    public void applyExchangeTax(Coin c, int count) {
        content = content
                .replace("${tax.rate}", c.getExchangeTax() * 100 + "%")
                .replace("${tax.result}", count * c.getExchangeTax() + "");
    }

    public void applyTransfer(Coin c, String playerName, int count) {
        content = content
                .replace("${player.name}", playerName)
                .replace("${tax.rate}", c.getTransferTax() * 100 + "%")
                .replace("${tax.result}", count * c.getTransferTax() + "");
    }

    private String generateReplaceHolder(String item) {
        return "${" + index + "." + item + "}";
    }

    @Override
    public String toString() {
        return ChatColor.translateAlternateColorCodes('&', content);
    }

    public void clear() {
        index = 1;
        content = "";
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof InsertedString)) {
            return false;
        }
        return content.equals(((InsertedString) o).content);
    }

    @Override
    public int hashCode() {
        return content.hashCode();
    }
    
    public static String getAndTranslate(String key) {
        return ChatColor.translateAlternateColorCodes('&',
                Objects.requireNonNullElse(
                        CuteCoin
                                .getInstance()
                                .getConfig()
                                .getString("messages." + key, "")
                        , ""));
    }
}

hashCodeequals 是不必要的,只是方便起见。

这里的各个方法的作用就是「装配」不同的文字进去。另外我们也部署了翻译颜色文本的方法。

下面我们来实现命令处理器……哦不,在那之前,我们也需要想上次一样的 RuntimeDataManager

package rarityeg.cutecoin;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;

public final class RuntimeDataManager {
    private static final Map<UUID, List<Supplier<Void>>> acceptingActions = new ConcurrentHashMap<>();

    public static boolean isPending(UUID id) {
        return acceptingActions.containsKey(id);
    }

    public static void addPending(UUID id, Supplier<Void> onAccept, Supplier<Void> onReject) {
        acceptingActions.put(id, Arrays.asList(onAccept, onReject));
    }

    public static void removePending(UUID id) {
        acceptingActions.remove(id);
    }
    public static void executePending(UUID id, boolean isAccepted) {
        if (acceptingActions.containsKey(id)) {
            if (isAccepted) {
                acceptingActions.get(id).get(0).get();
            } else {
                acceptingActions.get(id).get(1).get();
            }
        }
    }
}

这里记录了哪些玩家正在被程序提问,等待回答 y/n

Supplier 代表一个「生成器」,也就是一个方法,这里不用 Method 是因为那个是给反射用的,Supplier 更合适。<Void> 表示「没有返回值」。另外这里存储的两个 Supplier 分别用于「确认」和「取消」。

三个 get 看上去很迷惑,实际上它们分别是 MapListSupplierget 方法。最后一个 get 执行 Supplier 中的方法。

命令处理器

下面是命令处理器的代码,当心长度~

package rarityeg.cutecoin;

import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.security.InvalidParameterException;
import java.util.*;

@SuppressWarnings("deprecation")
public class CommandHandler implements TabExecutor {

    @Override
    public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
        if (!(sender instanceof Player)) {
            return false;
        }
        if (command.getName().equals("my-coins")) {
            processQueryCommand((Player) sender);
            return true;
        }
        if (command.getName().equals("exchange")) {
            if (args.length < 3) {
                return false;
            }
            // 检查参数
            try {
                CoinManager.byName(args[0]);
                CoinManager.byName(args[1]);
                int count = Integer.parseInt(args[2]);
                if (count == 0) {
                    return false;
                }
                if (count < 0) {
                    count = -count;
                }
                startExchangeProcess((Player) sender, args[0], args[1], count);
            } catch (NumberFormatException | InvalidParameterException e) {
                return false;
            }
        }

        if (command.getName().equals("transfer")) {
            if (args.length < 3) {
                return false;
            }
            try {
                CoinManager.byName(args[2]);
                int count = Integer.parseInt(args[1]);
                // Bukkit 异步调用会出问题
                new BukkitRunnable() {
                    @Override
                    public void run() {
                        Player p = Bukkit.getPlayer(args[0]);
                        if (p == null) {
                            return;
                        }
                        startTransferProcess((Player) sender, p, args[2], count);
                    }
                }.runTask(CuteCoin.getInstance());
            } catch (InvalidParameterException | NumberFormatException e) {
                return false;
            }

        }
        return false;
    }

    @Override
    public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) {
        if (command.getName().equals("exchange")) {
            if (args.length <= 2) {
                return Arrays.asList(CoinManager.getCoins().toArray(new String[0]));
                // Set 转为 List
            }
            if (args.length == 3) {
                return Collections.singletonList("<数量>");
            }
        }
        if (command.getName().equals("transfer")) {
            if (args.length == 2) {
                return Collections.singletonList("<数量>");
            }
            if (args.length == 3) {
                return Arrays.asList(CoinManager.getCoins().toArray(new String[0]));
            }
        }
        return null;
    }


    private void startTransferProcess(Player from, Player to, String coinName, int count) {
        AbstractDataManager dataManager = AbstractDataManager.getDataManagerInstance();
        UUID fromId = from.getUniqueId();
        UUID toId = to.getUniqueId();
        Coin c = CoinManager.byName(coinName);
        if (!CoinManager.byName(coinName).isAllowTransfer()) {
            InsertedString ist = new InsertedString("transfer.no-transfer");
            ist.applyCoin(c, count);
            ist.applyTransfer(c, to.getDisplayName(), count);
            from.sendMessage(ist.toString());
            return;
        }
        int fromOriginal = dataManager.getPlayerCoin(fromId, coinName);
        if (count > fromOriginal) {
            InsertedString ist = new InsertedString("not-enough");
            ist.applyCoin(CoinManager.byName(coinName), count);
            from.sendMessage(ist.toString());
            return;
        }
        int toOriginal = dataManager.getPlayerCoin(toId, coinName);
        InsertedString confirmIst = new InsertedString("transfer.to");
        confirmIst.applyTransfer(c, to.getDisplayName(), count);
        confirmIst.applyCoin(c, count);
        count = (int) (count * (1 - c.getTransferTax()));
        int fromFinal = fromOriginal - count;
        boolean overflowBit = false;
        int toMainCoinDelta = 0;
        int toDelta = count;
        if (c.getMax() != 0 && toOriginal + count > c.getMax()) {
            overflowBit = true;
            toDelta = c.getMax() - toOriginal;
            toMainCoinDelta = c.exchangeTo(CoinManager.getMainCoin(), toOriginal + count - c.getMax(), 0);
        }
        from.sendMessage(confirmIst.toString());
        from.sendMessage(new InsertedString("confirm").toString());
        boolean finalOverflowBit = overflowBit;
        InsertedString finalFromIst = new InsertedString("ok");
        finalFromIst.applyCoin(c, count);
        finalFromIst.applyTransfer(c, to.getDisplayName(), count);
        InsertedString finalToIst = new InsertedString("transfer.from");
        finalToIst.applyCoin(c, count);
        finalToIst.applyTransfer(c, from.getDisplayName(), count);
        InsertedString istOnCancel = new InsertedString("cancelled");
        istOnCancel.applyCoin(c, count);
        istOnCancel.applyTransfer(c, to.getDisplayName(), count);
        int finalToDelta = toDelta;
        int finalToMainCoinDelta = toMainCoinDelta;
        RuntimeDataManager.addPending(
                fromId,
                () -> {
                    dataManager.setPlayerCoin(from.getUniqueId(), coinName, fromFinal);
                    dataManager.setPlayerCoin(to.getUniqueId(), coinName, dataManager.getPlayerCoin(to.getUniqueId(), coinName) + finalToDelta);
                    if (finalOverflowBit) {
                        dataManager.setPlayerCoin(to.getUniqueId(), CoinManager.getMainCoin().getName(), dataManager.getPlayerCoin(to.getUniqueId(), CoinManager.getMainCoin().getName()) + finalToMainCoinDelta);
                    }
                    from.sendMessage(finalFromIst.toString());
                    to.sendMessage(finalToIst.toString());
                    return null;
                },
                () -> {
                    from.sendMessage(istOnCancel.toString());
                    return null;
                }
        );
    }

    private void processQueryCommand(Player p) {
        
        AbstractDataManager dataManager = AbstractDataManager.getDataManagerInstance();
        
        Map<String, Integer> map = dataManager.getPlayerCoins(p.getUniqueId());
        System.out.println(map.entrySet().toString());
        for (Map.Entry<String, Integer> e : map.entrySet()) {
            InsertedString ist = new InsertedString("sprintf");
            ist.applyCoin(CoinManager.byName(e.getKey()), e.getValue());
            p.sendMessage(ist.toString());
        }
    }

    private void startExchangeProcess(Player p, String fromCoinName, String toCoinName, int count) {
        AbstractDataManager dataManager = AbstractDataManager.getDataManagerInstance();
        UUID id = p.getUniqueId();
        int fromCoinOriginal = dataManager.getPlayerCoin(id, fromCoinName);

        if (count > fromCoinOriginal) {
            InsertedString ist = new InsertedString("not-enough");
            ist.applyCoin(CoinManager.byName(fromCoinName), count);
            p.sendMessage(ist.toString());
            return;
        }


        Coin fromCoin = CoinManager.byName(fromCoinName);
        Coin toCoin = CoinManager.byName(toCoinName);
        if (!fromCoin.isAllowOut()) {
            InsertedString ist = new InsertedString("exchange.no-out");
            ist.applyCoin(fromCoin, count);
            p.sendMessage(ist.toString());
            return;
        }

        if (!toCoin.isAllowIn()) {
            InsertedString ist = new InsertedString("exchange.no-in");
            ist.applyCoin(toCoin, count);
            p.sendMessage(ist.toString());
            return;
        }

        int toCoinOriginal = dataManager.getPlayerCoin(id, toCoinName);
        int toCoinCount = fromCoin.exchangeTo(toCoin, count, fromCoin.getExchangeTax());
        InsertedString hintExchange = new InsertedString("exchange.to");
        hintExchange.applyCoin(fromCoin, count);
        hintExchange.applyExchangeTax(fromCoin, count);
        hintExchange.next();
        hintExchange.applyCoin(toCoin, toCoinCount);
        p.sendMessage(hintExchange.toString());
        int finalFromCoinDelta = -count;
        int finalToCoinDelta = toCoinCount;
        int finalMainCoinDelta = 0;
        if (toCoin.getMax() != 0 && toCoinCount + toCoinOriginal > toCoin.getMax()) {
            InsertedString ist = new InsertedString("overflow-hint");
            int overflow = toCoinCount + toCoinOriginal - toCoin.getMax();
            ist.applyCoin(toCoin, overflow);
            ist.next();
            int overflowCount = toCoin.exchangeTo(CoinManager.getMainCoin(), overflow, 0);
            ist.applyCoin(CoinManager.getMainCoin(), overflowCount);
            p.sendMessage(ist.toString());
            finalToCoinDelta = toCoin.getMax() - toCoinOriginal;
            finalMainCoinDelta = overflowCount;
        }
        p.sendMessage(new InsertedString("confirm").toString());
        InsertedString finalIst = new InsertedString("ok");
        finalIst.applyCoin(fromCoin, count);
        finalIst.applyExchangeTax(fromCoin, count);
        finalIst.next();
        finalIst.applyCoin(toCoin, finalToCoinDelta);
        InsertedString finalIstOnCancel = new InsertedString("cancelled");
        finalIstOnCancel.applyCoin(fromCoin, count);
        finalIstOnCancel.applyExchangeTax(fromCoin, count);
        finalIstOnCancel.next();
        finalIstOnCancel.applyCoin(toCoin, finalToCoinDelta);
        int finalMainCoinDelta1 = finalMainCoinDelta;
        int finalToCoinDelta1 = finalToCoinDelta;
        RuntimeDataManager.addPending(
                id,
                () -> {
                    dataManager.setPlayerCoin(id, CoinManager.getMainCoin().getName(), dataManager.getPlayerCoin(id, CoinManager.getMainCoin().getName()) + finalMainCoinDelta1);
                    dataManager.setPlayerCoin(id, fromCoinName, dataManager.getPlayerCoin(id, fromCoinName) + finalFromCoinDelta);
                    dataManager.setPlayerCoin(id, toCoinName, dataManager.getPlayerCoin(id, toCoinName) + finalToCoinDelta1);
                    p.sendMessage(finalIst.toString());
                    return null;
                },
                () -> {
                    p.sendMessage(finalIstOnCancel.toString());
                    return null;
                }

        );
    }
}

代码很长,可能也不好读,不急,你可以慢慢看。

InsertedStringAbstractDataManager 帮了我们很多忙,不然想一想这里的代码……天哪。

()->{} 用于代表一个函数(函数也是对象!),我们将两个函数传递给了 RuntimeDataManager,马上我们就会执行它们。

事件监听器

这次要监听的事件很少,只有一个,代码也很简单:

package rarityeg.cutecoin;

import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.AsyncPlayerChatEvent;

public class EventListener implements Listener {
    @EventHandler
    public void onPlayerChat(AsyncPlayerChatEvent e) {
        if (RuntimeDataManager.isPending(e.getPlayer().getUniqueId())) {
            String msg = e.getMessage();
            if (msg.toLowerCase().startsWith("y")) {
                e.setCancelled(true);
                RuntimeDataManager.executePending(e.getPlayer().getUniqueId(), true);
                return;
            }
            if (msg.toLowerCase().startsWith("n")) {
                e.setCancelled(true);
                RuntimeDataManager.executePending(e.getPlayer().getUniqueId(), false);
               
            }

        }
    }
}

测试用方法

你注意到了吗?目前,似乎没有可以用来给玩家添加货币的方法。

仅仅是为了测试,我们在玩家聊天时给玩家指定数量的货币。修改 EventListener

if (RuntimeDataManager.isPending(e.getPlayer().getUniqueId())) {
    // ...
} else {
    String coinName = e.getMessage().split(" ")[0];
    int count = Integer.parseInt(e.getMessage().split(" ")[1]);
    AbstractDataManager.getDataManagerInstance().setPlayerCoin(e.getPlayer().getUniqueId(), coinName, count);
}

注册到主类

只剩最后一点了:注册各个处理器,并在插件关闭时保存数据。

onEnable 方法中:

TabExecutor te = new CommandHandler();
Objects.requireNonNull(Bukkit.getPluginCommand("my-coins")).setTabCompleter(te);
Objects.requireNonNull(Bukkit.getPluginCommand("my-coins")).setExecutor(te);
Objects.requireNonNull(Bukkit.getPluginCommand("exchange")).setTabCompleter(te);
Objects.requireNonNull(Bukkit.getPluginCommand("exchange")).setExecutor(te);
Objects.requireNonNull(Bukkit.getPluginCommand("transfer")).setTabCompleter(te);
Objects.requireNonNull(Bukkit.getPluginCommand("transfer")).setExecutor(te);
Bukkit.getPluginManager().registerEvents(new EventListener(), this);

onDisable 方法中:

AbstractDataManager.getDataManagerInstance().save();

最终检查

都到这个时候了,应该没什么好说的了。

IDEA 如果给你划出了错误,记得改正。

编译构建

Maven 项目的构建和经典项目的构建一样。在「Project Structure」、「Artifacts」中添加即可。

要包含到你的项目中的内容如下:

  • (双击)'CuteCoin' compile output
  • (右键,「Extract Into Output Root」)Extracted 'commons-1.0-SNAPSHOT.jar/'
  • (右键,「Extract Into Output Root」)Extraced 'mysql-connector-java-8.0.23.jar/'

勾选「Include in project build」,单击「Apply」、「OK」,单击右上角的「Build project」……

准备好迎接最后的挑战了吗?