首先,我们完成上一节中没做的一项工作,在主类的 onEnable
方法中:
AbstractDataManager.getDataManagerInstance().load();
我们初始化了一下数据管理器。
现在我们来编写 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, "")
, ""));
}
}
hashCode
和 equals
是不必要的,只是方便起见。
这里的各个方法的作用就是「装配」不同的文字进去。另外我们也部署了翻译颜色文本的方法。
下面我们来实现命令处理器……哦不,在那之前,我们也需要想上次一样的 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
看上去很迷惑,实际上它们分别是 Map
、List
和 Supplier
的 get
方法。最后一个 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;
}
);
}
}
代码很长,可能也不好读,不急,你可以慢慢看。
InsertedString
和 AbstractDataManager
帮了我们很多忙,不然想一想这里的代码……天哪。
()->{}
用于代表一个函数(函数也是对象!),我们将两个函数传递给了 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」……
准备好迎接最后的挑战了吗?