Skip to content

Latest commit

 

History

History
884 lines (740 loc) · 32.7 KB

AC-3-1.md

File metadata and controls

884 lines (740 loc) · 32.7 KB

AC-3-1 CuteCoin Part 1

行动背景

Minecraft 原版没有提供合适的经济系统,虽然用金锭代替货币很合适,但不利于携带与交易。此外,插件想要检测玩家的钱财也很困难,这就催生了经济系统插件。

笔者不是很喜欢「金币 - 点券」这样的结构,我们希望服务器使用多种货币,并且可由服主管理。那么我们就来做一个这样的插件。

行动规划

!> CuteCoin 的许可协议
CuteCoin 不适用首页的许可,它遵循 GNU 通用公共许可证(第三版),请仔细阅读该许可证。该许可证也包含在其代码仓库中。
GPLv3

行动名称:CuteCoin

行动代号:AC-3

行动类别:作战

涉及章节:

  • AC-3-1
  • AC-3-2

难度:~~(小)~~末影龙

我们希望我们的插件有这些功能:

  • 自定义货币
  • 使用数据库和文件存储数据
  • 可以进行货币兑换及转账

暂时就这些功能,但是我们要将这个插件做得可靠。

开始行动

这次我们使用 Maven 进行开发。

不知道怎么回事,笔者的 IDEA 在管理多个 Maven 项目时很混乱,构建耗时也很长,因此这次我们重新创建一个新的项目吧。

单击菜单中的「File」、「Close Project」关闭这个我们一直在其中工作的项目,回到 IDEA 首页。

单击「New Project」,在弹出的窗口左侧选择「Maven」,单击「Next」。

为新项目命名为「CuteCoin」,「GroupId」改成你的包名(我就直接用 rarityeg 了)。

单击「Finish」完成。

?> 到底怎么回事
Maven 的项目管理方法和 IDEA 不一样,Maven 中的「项目」相当于 IDEA 中的「模块」,然而由于我们之前的项目中混合使用了传统项目和 Maven 项目,Maven 和 IDEA 被搞糊涂了。
按照标准,实际上一个 Maven 项目就对应 IDEA 中的一个项目,这样做也能提升 Build 的速度。

现在你的 pom.xml 应该像这样:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>这里是你自己命名的!</groupId>
    <artifactId>CuteCoin</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>

</project>

现在我们需要添加三个依赖,分别是 JDBC,Paper API 和 RarityCommons(好不容易写好了,为什么不用呢)。

pom.xml 中加入(建议直接加在 </project> 上面):

<repositories>
    <!-- Apache Maven 中央仓库是内置的 -->
    <repository>
        <id>papermc</id>
        <url>https://papermc.io/repo/repository/maven-public/</url>
    </repository>
    <!-- Paper Maven 仓库 -->
    <repository>
        <id>plugin-diary-code</id>
        <url>https://raw.fastgit.org/Andy-K-Sparklight/PluginDiaryCode/master/repo/</url>
    </repository>
    <!-- 我的 Maven 仓库 -->
</repositories>
<dependencies>
    <dependency>
        <groupId>com.destroystokyo.paper</groupId>
        <artifactId>paper-api</artifactId>
        <version>1.16.5-R0.1-SNAPSHOT</version>
        <scope>provided</scope>
        <!-- Paper -->
    </dependency>
    <dependency>
        <groupId>rarityeg</groupId>
        <artifactId>commons</artifactId>
        <version>1.0-SNAPSHOT</version>
        <!-- RarityCommons -->
        <!-- 这是注释,不必添加! -->
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>[8.0.23,)</version>
        <!-- JDBC -->
    </dependency>
</dependencies>

这里我们包括了 Maven 中央仓库(Maven 内置)、Paper 仓库和我的 Maven 仓库。我的仓库是通过 FastGit 分发的,在此感谢他们的奉献!

单击右上角的「Load Maven Changes」(或按下 Ctrl + Shift + O),重新载入 Maven 仓库,这样 Paper API,JDBC 和 RarityCommons 即被下载到你的计算机上。

Maven 的好处就在这里,即使你没有看 6-3,没有编写 RarityCommons,也可以使用它,只需要一条链接就可以了。

!> 注意
RarityCommons 是自由软件,刚刚你的操作已经将它作为了你的依赖,按照 RarityCommons 的许可证(GPL - 3.0)规定,你的项目(即 CuteCoin)必须以相同的许可证发行!
再次提醒!要么不发行,要么使用相同的许可证!
如果你不想使用 RarityCommons,请将它从您的 pom.xml 中移除,您便不再受 GPL - 3.0 的限制。
在附录中我们将讲到有关许可证的更多内容。

这样我们就准备好了开发环境。

右键 java(在 src/main 下),创建新的包和类,并继承 JavaPluginR(RarityCommons 中的):

package 你的包名;

import rarityeg.commons.JavaPluginR;
// 别导入错了!如果你不用 RarityCommons,这里可以使用 Bukkit 的 JavaPlugin

public class CuteCoin extends JavaPluginR {
}

配置文件

这次我们要做一个服主可以自定义货币的配置文件,因此我们将 CuteCoin 的配置文件设计成这样:

!> 当心
Maven 项目的结构有所不同,config.ymlplugin.yml 等非 Java 内容需要放在 src/resources 下才能被正确打包!

main-coin: cute-coin # 主要货币
coins:
  cute-coin: # 货币英文名,只允许小写字母,数字和减号!
    name: "可爱币" # 显示名称,以 & 代替 § 来输入彩色符号
    description: "❤ So Cute~" # 介绍
    value: 1 # 价值,这决定货币转换和财产评估时的数据
    exchange:
      in: true # 允许兑换为这种货币
      out: true # 允许将这种货币兑换为其它货币
      tax: 0 # 兑换为其它货币时不收税
    transfer:
      allow: true # 允许转账
      tax: 0 # 转账时不收费
  # 另一个货币的示例
  crystal:
    name: "水晶币"
    description: "如此纯净的水晶,让我的心也变得如一汪清水……"
    value: 20 # 1 水晶币转换为 20 可爱币
    max: 100 # 最多允许持有 100 个水晶币,主要货币不允许设置最大值!
    exchange:
      in: true # 不允许兑换为这种货币
      out: false # 允许将这种货币兑换为其它货币
      tax: 0.5 # 兑换为其它货币时,收取 50% 手续费
    transfer:
      allow: false # 不允许转账
      tax: 0 # 不允许转账时,此项无需配置
  # 继续添加您的货币...
messages:
  ok: "&a操作成功完成"
  cancelled: "&c操作已经取消"
  not-enough: "&c您没有足够的 &6{1.name} 来完成本操作!"
  confirm: "&d&l要继续吗?&b(&ay&b/&cn&b)"
  overflow-hint: "&e进行此操作后,您的 &6{1.name}&e 将超出 &6${overflow}\n&e超出部分将被转换为 &6${2.count} ${2.name}"
  exchange:
    # ${xxx} 代表 xxx 变量
    # 1 要兑出的货币
    # 2 要兑入的货币
    # count 数量,name 名字,tax.rate 税率,tax.result 合计后消耗
    # tax.rate 已经包括百分号
    # 使用 & 代替 § 渲染颜色
    to: "&e您正在将 &6${1.count} ${1.name}&e 转换为 &6${2.count} ${2.name}\n&e这将收取手续费 &6${tax.rate}&e,合 &6${tax.result} ${1.name}&e"
    no-out: "&c抱歉,&6${1.name}&c 目前不允许被兑出"
    no-in: "&c抱歉,&6${1.name}&c 目前不允许被兑入"
  transfer:
    to: "&e您正在将 &6${1.count} ${1.name}&e 转账给 ${player.name}\n&e这将收取手续费 &6${tax.rate}&e,合 &6${tax.result} ${1.name}&e"
    from: "&e您收到来自 ${player.name} 转账的 ${1.count} ${1.name}"
    no-transfer: "&c抱歉,&6${1.name}&c 目前不允许转账"
  sprintf: "&6${1.name}  ${1.count}/${1.max}\n&7${1.description}"
  # 玩家请求查询货币时的响应

mysql:
  use: false # false 以使用文件,true 以使用 MySQL
  host: localhost # 数据库主机
  db-name: cutecoin # 数据库名
  username: root # 用户名
  password: 123456 # 密码
  port: 3306 # 端口
  save-threshold: 30 # 数据库写入缓存时间,0 以禁用(仅在关闭时保存)

这里我模仿了 JavaScript 中的插值语句,稍后我们将实现这项功能。

货币类

首先我们创建一个 Coin 类描述货币。这很简单……

不过先等等,万一将来有人要对我们的插件进行开发,我们最好不要直接提供我们当前的类(因为其他开发者可能会创建自己的货币类型),我们先创建抽象的 Coin 类,再用其它的类来继承它。

package rarityeg.cutecoin;

import javax.annotation.Nonnull;

public abstract class Coin {
    public abstract int getValue();

    public abstract int getMax();

    public int valueOf(int count) {
        return count * getValue();
    }

    public int countOf(int value) {
        return value / getValue();
    }

    @Nonnull
    public abstract String getName();

    @Nonnull
    public abstract String getDescription();

    public abstract boolean isAllowOut();

    public abstract boolean isAllowIn();

    public abstract boolean isAllowTransfer();

    public abstract double getExchangeTax();

    public abstract double getTransferTax();

    public int exchangeTo(@Nonnull Coin target, int count, double tax) {
        return (int) ((count * getValue() * (1 - tax)) / target.getValue());
    }
}

abstract 用于标识「这个方法还没有实现,用不了哦」。

我们还需要一个类来管理所有存在的币种,这很简单:

package rarityeg.cutecoin;

import javax.annotation.Nullable;
import java.security.InvalidParameterException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public final class CoinManager {
    private static final Map<String, Coin> allCoins = new ConcurrentHashMap<>();
    private static Coin mainCoin;

    @Nonnull
    public static Coin byName(String name) {
        Coin c = allCoins.get(name);
        if (c == null) {
            throw new InvalidParameterException("Coin not found!");
        }
        return c;
    }

    public static void registerCoin(Coin coin) {
        if (allCoins.containsKey(coin.getName())) {
            throw new InvalidParameterException("Coin of same name already exists!");
        } else {
            allCoins.put(coin.getName(), coin);
        }
    }
    public static Set<String> getCoins() {
        return allCoins.keySet();
    }

    public static void setMainCoin(Coin coin) {
        if (mainCoin == null) {
            mainCoin = coin;

        } else {
            throw new IllegalStateException("Main coin has already been set!");
        }
    }

    public static Coin getMainCoin() {
        return mainCoin;
    }
}

接下来我们来实现 Coin 类。由于我们的货币是基于配置文件创建的,因此我们称之为 ConfigCoin。除了实现 Coin 的方法以外,我们还需要为它创建从配置文件读取的功能,实际上实现这个也很简单:

package rarityeg.cutecoin;

import org.bukkit.configuration.Configuration;

import javax.annotation.Nonnull;
import java.security.InvalidParameterException;

public class ConfigCoin extends Coin {
    private final int VALUE;
    private final String NAME;
    private final int MAX;
    private final String DESCRIPTION;
    private final boolean ALLOW_OUT;
    private final boolean ALLOW_IN;
    private final boolean ALLOW_TRANSFER;
    private final double EXCHANGE_TAX;
    private final double TRANSFER_TAX;


    public ConfigCoin(String id) {
        Configuration config = CuteCoin.getInstance().getConfig();
        String coinPath = "coins." + id;
        if (config.get(coinPath) == null) {
            throw new InvalidParameterException("Coin of this id not found.");
        }
        coinPath += ".";
        String name = config.getString(coinPath + "name");
        String description = config.getString(coinPath + "description");
        if (name == null || description == null) {
            throw new InvalidParameterException("Coin data missing!");
        }
        int value = config.getInt(coinPath + "value");
        int max = config.getInt(coinPath + "max");
        boolean allowIn = config.getBoolean(coinPath + "exchange.in");
        boolean allowOut = config.getBoolean(coinPath + "exchange.out");
        double exchangeTax = config.getDouble(coinPath + "exchange.tax");
        boolean allowTransfer = config.getBoolean(coinPath + "transfer.allow");
        double transferTax = config.getDouble(coinPath + "transfer.tax");
        NAME = name;
        DESCRIPTION = description;
        VALUE = value;
        MAX = max;
        ALLOW_IN = allowIn;
        ALLOW_OUT = allowOut;
        EXCHANGE_TAX = exchangeTax;
        TRANSFER_TAX = transferTax;
        ALLOW_TRANSFER = allowTransfer;
    }

    @Override
    public int getValue() {
        return VALUE;
    }

    @Override
    public int getMax() {
        return MAX;
    }

    @Override
    @Nonnull
    public String getName() {
        return NAME;
    }

    @Override
    @Nonnull
    public String getDescription() {
        return DESCRIPTION;
    }

    @Override
    public boolean isAllowOut() {
        return ALLOW_OUT;
    }

    @Override
    public boolean isAllowIn() {
        return ALLOW_IN;
    }

    @Override
    public boolean isAllowTransfer() {
        return ALLOW_TRANSFER;
    }

    @Override
    public double getExchangeTax() {
        return EXCHANGE_TAX;
    }

    @Override
    public double getTransferTax() {
        return TRANSFER_TAX;
    }
}

接下来我们需要在插件加载时注册所有的货币,修改主类:

public static boolean isDBAvailable = false;
@Override
public void onEnable() {
    try {
        saveDefaultConfig();
    	// 保存配置
    	Set<String> allCoins = Objects.requireNonNullElseGet(getConfig().getConfigurationSection("coins"),
            	() -> {
                throw new InvalidParameterException("You shouldn't have removed the 'coins' section!");
            }).getKeys(false);
    // requireNonNullElseGet 是一个比较高级的用法,查看得到的值是否是 null,如果是,就执行后面的方法,这里我们选择直接抛出异常。
    	if (allCoins.size() == 0) {
        	throw new InvalidParameterException("At least one coin should be added!");
        	// 不设置货币想啥呢
    	}
    	String mainCoin = getConfig().getString("main-coin");
    	if (mainCoin == null) {
        	throw new InvalidParameterException("You must specify the main coin!");
        	// 不设置主货币想啥呢
    	}
    	if (getConfig().getString("coins." + mainCoin + ".name") == null) {
        	throw new InvalidParameterException("The main coin does not exist!");
        	// 阁下何不乘风起,扶摇直上九万里?
    	}
    	if (getConfig().getInt("coins." + mainCoin + ".max") != 0) {
        	throw new InvalidParameterException("Main coin could not have a limit!");
        	// 主货币不允许有上限
    	}
    	for (String key : allCoins) {
        	Coin c = new ConfigCoin(key);
        	CoinManager.registerCoin(c);
        	// 注册各个 Coin
    	}
    	CoinManager.setMainCoin(new ConfigCoin(mainCoin));
    	// 设置主货币
    	isDBAvailable = getConfig().getBoolean("mysql.use");
    } catch (InvalidParameterException e) {
        e.printStackTrace();
        Bukkit.getPluginManager().disablePlugin(this);
        // 出现问题,拒绝工作
    }
}

就这么简单。我们还顺便把数据库的启用信息读入了程序中。

另外,由于我们一会还要保存数据到文件中,所以现在在 resources 下创建文件 data.yml,并将相应代码写在 onEnable 中:

saveResource("data.yml", false);

数据管理器

和 HarmonyAuth SMART 中一样,我们需要一个数据管理器接口,并且使用两个类实现它。不过,这里我们使用一个抽象类 AbstractDataManager 来做这一点。

package rarityeg.cutecoin;

import java.util.Map;
import java.util.UUID;

public abstract class AbstractDataManager {
    public abstract int getPlayerCoin(UUID id, String coinName);

    public abstract void setPlayerCoin(UUID id, String coinName, int count);

    public abstract Map<String, Integer> getPlayerCoins(UUID id);

    public abstract void save();

    public abstract void load();

    public synchronized static AbstractDataManager getDataManagerInstance() {
        if (CuteCoin.isDBAvailable) {
            return new DBDataManager();
        } else {
            return new FileDataManager();
        }
    }
}

静态方法 getDataManagerInstance 通过当前状态创建并返回合适的数据处理器。

接下来我们就来创建 FileDataManagerDBDataManager

首先是较简单的基于文件的实现:

package rarityeg.cutecoin;

import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;

import javax.annotation.Nonnull;
import javax.annotation.ParametersAreNonnullByDefault;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

public class FileDataManager extends AbstractDataManager {
    private static FileConfiguration data;
    private static final Map<UUID, Map<String, Integer>> CACHE = new ConcurrentHashMap<>();
    // 缓存区

    @Override
    @ParametersAreNonnullByDefault
    public int getPlayerCoin(UUID id, String coinName) {
        if (CACHE.containsKey(id)) {
            return CACHE.get(id).get(coinName);
        }
        return data.getInt(id.toString() + "." + coinName);
    }

    @Override
    @ParametersAreNonnullByDefault
    public void setPlayerCoin(UUID id, String coinName, int count) {
        synchronized (FileDataManager.class) {
            // 以 FileDataManager 类作为「锁」,进行同步
            data.set(id.toString() + "." + coinName, count);
        }
        // 如果对应的数据已经被修改,就重新缓存
        if (CACHE.containsKey(id)) {
            CACHE.get(id).put(coinName, count);
        }
    }

    @Override
    @Nonnull
    @ParametersAreNonnullByDefault
    public Map<String, Integer> getPlayerCoins(UUID id) {
        ConfigurationSection coins = data.getConfigurationSection(id.toString());
        if (coins == null) {
            // 这名玩家不存在
            return new ConcurrentHashMap<>();
        }
        Map<String, Integer> cacheMap;
        if (CACHE.containsKey(id)) {
            return CACHE.get(id);
        }
        cacheMap = new ConcurrentHashMap<>();
        for (String coin : coins.getKeys(false)) {
            cacheMap.put(coin, coins.getInt(coin));
        }
        CACHE.put(id, cacheMap);
        // 存入缓存
        return cacheMap;
    }

    @Override
    public void save() {
        // 保存文件
        File f = new File(CuteCoin.getInstance().getDataFolder(), "data.yml");
        try {
            data.save(f);
        } catch (IOException e) {
            CuteCoin.getInstance().getLogger().warning("Failed to save data.");
            e.printStackTrace();
        }
    }

    @Override
    public void load() {
        File f = new File(CuteCoin.getInstance().getDataFolder(), "data.yml");
        data = YamlConfiguration.loadConfiguration(f);
    }
}

这里的缓存需要稍微说明一下。

每当我们调用 getPlayerCoins 进行读取数据时,缓存就被触发,如果已存在就直接从缓存中读取,没有就通过 data 进行读取。

事实上,每次读取(尤其是那些更多的 getPlayerCoin 单点读取)都应该将数据存入缓存,但 data 本来就位于内存中,只是因为 ConcurrentHashMap 的速度更快我们才使用它作为缓存的。另外,ConcurrentHashMap 是线程安全的。

synchronized 这里是个新用法:

synchronized (锁) {
    // ...
}

这里的锁表示「在这个级别上,只允许同时一个线程进行操作」,如果传入 this 就表示当前对象的实例一次只能有一个线程执行该部分,传入 FileDataManager.class 就表示整个类的所有实例一次只能有一个线程执行该部分。由于我们将来将创建很多的 FileDataManager 实例,因此我们需要把整个类都锁上。

下面我们来看数据库的实现:

package rarityeg.cutecoin;

import org.bukkit.Bukkit;
import org.bukkit.configuration.Configuration;

import javax.annotation.Nonnull;
import javax.annotation.ParametersAreNonnullByDefault;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.security.InvalidParameterException;
import java.sql.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

public class DBDataManager extends AbstractDataManager {
    private static final int SAVE_THRESHOLD = CuteCoin.getInstance().getConfig().getInt("mysql.save-threshold");
    private static int setsSinceLastSave = 0;
    private static final Map<UUID, Map<String, Integer>> CACHE = new ConcurrentHashMap<>();
    private static final Set<UUID> DIRTY_LIST = Collections.synchronizedSet(new HashSet<>());
    private static String dbUrl;
    private static String user;
    private static String password;

    @Override
    @ParametersAreNonnullByDefault
    public int getPlayerCoin(UUID id, String coinName) {
        if (CACHE.containsKey(id)) {
            Map<String, Integer> subMap = CACHE.get(id);
            if (subMap.containsKey(coinName)) {
                return subMap.get(coinName);
            }
        } else {
            CACHE.put(id, new ConcurrentHashMap<>());
        }
        ifNotExistThenInsert(id);
        Coin coin = CoinManager.byName(coinName);
        try {
            Connection conn = DriverManager.getConnection(dbUrl, user, password);
            PreparedStatement ps;
            if (CoinManager.getMainCoin().getName().equals(coinName)) {
                ps = conn.prepareStatement("SELECT main_coin_value FROM cute_coin_data WHERE uuid=?", ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY);
                ps.setString(1, id.toString());
                ResultSet rs = ps.executeQuery();
                rs.first();
                int value = rs.getInt(1);
                rs.close();
                ps.close();
                conn.close();

                int count = coin.countOf(value);
                CACHE.get(id).put(coinName, count);
                return count;
            }
            return Objects.requireNonNullElse(getMap(id).get(coinName), 0);
        } catch (SQLException e) {
            putError(e);
        }
        return 0;
    }

    @Override
    @ParametersAreNonnullByDefault
    public void setPlayerCoin(UUID id, String coinName, int count) {
        if (CACHE.containsKey(id)) {
            CACHE.get(id).put(coinName, count);

        } else {
            Map<String, Integer> t = new ConcurrentHashMap<>();
            t.put(coinName, count);
            CACHE.put(id, t);
        }

        DIRTY_LIST.add(id);
        ++setsSinceLastSave;
        if (SAVE_THRESHOLD > 0 && setsSinceLastSave >= SAVE_THRESHOLD) {
            save();
            setsSinceLastSave = 0;
            DIRTY_LIST.clear();
        }
    }

    @Override
    @Nonnull
    @ParametersAreNonnullByDefault
    public Map<String, Integer> getPlayerCoins(UUID id) {
        return getMap(id);
    }

    @Override
    public void save() {
        try {
            Connection conn = DriverManager.getConnection(dbUrl, user, password);
            PreparedStatement ps = conn.prepareStatement("INSERT INTO cute_coin_data (uuid, main_coin_value, map) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE uuid=?, main_coin_value=?, map=?");
            for (UUID k : DIRTY_LIST) {
                String uuid = k.toString();
                ps.setString(1, uuid);
                ps.setString(4, uuid);
                Map<String, Integer> map = getMap(k);
                if (map.containsKey(CoinManager.getMainCoin().getName())) {
                    int i = CoinManager.getMainCoin().valueOf(map.get(CoinManager.getMainCoin().getName()));
                    ps.setInt(2, i);
                    ps.setInt(5, i);
                } else {
                    ps.setInt(2, 0);
                    ps.setInt(5, 0);
                }
                Map<String, Integer> valueMap = new HashMap<>();
                for (String s : map.keySet()) {
                    valueMap.put(s, CoinManager.byName(s).valueOf(map.get(s)));
                }
                Blob mapBlob = conn.createBlob();
                ObjectOutputStream outputStream = new ObjectOutputStream(mapBlob.setBinaryStream(1));
                outputStream.writeObject(valueMap);
                outputStream.close();
                ps.setBlob(3, mapBlob);
                ps.setBlob(6, mapBlob);
                ps.execute();
                ps.close();
                conn.close();
            }
        } catch (SQLException e) {
            putError(e);
        } catch (IOException e) {
            putError(e);
        }
    }

    @Override
    public void load() {
        Configuration cfg = CuteCoin.getInstance().getConfig();
        String host = cfg.getString("mysql.host");
        String port = cfg.getString("mysql.port");
        String dbName = cfg.getString("mysql.db-name");
        user = cfg.getString("mysql.username");
        password = cfg.getString("mysql.password");
        if (host == null || port == null || dbName == null) {
            Bukkit.getPluginManager().disablePlugin(CuteCoin.getInstance());
            throw new InvalidParameterException("You must configure MySQL settings properly!");
        }
        dbUrl = "jdbc:mysql://" + host + ":" + port + "/" + dbName + "?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC";
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
            Connection conn = DriverManager.getConnection(dbUrl, user, password);
            PreparedStatement ps = conn.prepareStatement("CREATE TABLE IF NOT EXISTS cute_coin_data(uuid VARCHAR(36) PRIMARY KEY NOT NULL , main_coin_value BIGINT NOT NULL, map BLOB)");
            ps.execute();
            ps.close();
            conn.close();
            CuteCoin.getInstance().getLogger().info("Successfully connected to database server.");
        } catch (ClassNotFoundException e) {
            CuteCoin.getInstance().getLogger().warning("Failed to load driver, please reinstall this plugin.");
            Bukkit.getPluginManager().disablePlugin(CuteCoin.getInstance());
        } catch (SQLException e) {
            putError(e);
        }
    }
    private static boolean trySaveBit = true;
    private void putError(SQLException e) {
        CuteCoin.getInstance().getLogger().warning("Failed to operate database, will use file instead.");
        CuteCoin.isDBAvailable = false;
        if (trySaveBit) {
            trySaveBit = false;
            save();
        }
        e.printStackTrace();
    }

    private void putError(IOException e) {
        CuteCoin.getInstance().getLogger().warning("Failed to read from binary, data might have been corrupted, now reinitializing.");
        e.printStackTrace();
    }

    private void putError(ClassNotFoundException e) {
        CuteCoin.getInstance().getLogger().warning("Failed to read from binary, data might have been corrupted, now reinitializing.");
        e.printStackTrace();
    }

    private Map<String, Integer> getMap(UUID id) {
        if (CACHE.containsKey(id)) {
            return CACHE.get(id);
        } else {
            CACHE.put(id, new ConcurrentHashMap<>());
        }
        ifNotExistThenInsert(id);
        try {
            Connection conn = DriverManager.getConnection(dbUrl, user, password);
            PreparedStatement ps;
            ps = conn.prepareStatement("SELECT map FROM cute_coin_data WHERE uuid=?", ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY);
            ps.setString(1, id.toString());
            ResultSet rs = ps.executeQuery();
            rs.first();
            Blob rawMap = rs.getBlob(1);
            ObjectInputStream mapIn = new ObjectInputStream(rawMap.getBinaryStream());
            Object obj = mapIn.readObject();
            mapIn.close();
            Map<String, Integer> bufferMap = new ConcurrentHashMap<>();
            int overflow = 0;
            // Start reading map
            if (obj instanceof Map<?, ?>) {
                for (Object k : ((Map<?, ?>) obj).entrySet()) {
                    Object v = ((Map<?, ?>) obj).get(k);
                    if (k instanceof String && v instanceof Integer) {
                        try {
                            Coin c = CoinManager.byName((String) k);
                            bufferMap.put((String) k, c.countOf((int) v));
                        } catch (InvalidParameterException e) {
                            // No such coin
                            overflow += (int) v;
                        }
                    }
                }
            }
            // End reading map
            Coin mainCoin = CoinManager.getMainCoin();
            if (overflow != 0) {
                if (bufferMap.containsKey(mainCoin.getName())) {
                    bufferMap.put(mainCoin.getName(), bufferMap.get(mainCoin.getName()) + mainCoin.countOf(overflow));
                } else {
                    bufferMap.put(mainCoin.getName(), mainCoin.countOf(overflow));
                }

                DIRTY_LIST.add(id);
                
                ++setsSinceLastSave;
            }
            CACHE.put(id, bufferMap);
            return bufferMap;
        } catch (SQLException e) {
            putError(e);
        } catch (IOException e) {
            putError(e);
        } catch (ClassNotFoundException e) {
            putError(e);
        }
        return new ConcurrentHashMap<>();
    }

    private void ifNotExistThenInsert(UUID id) {
        try {
            Connection conn = DriverManager.getConnection(dbUrl, user, password);
            PreparedStatement ps = conn.prepareStatement("SELECT COUNT(uuid) FROM cute_coin_data WHERE uuid=?", ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY);
            ps.setString(1, id.toString());
            ResultSet rs = ps.executeQuery();
            rs.first();
            if (rs.getInt(1) == 0) {
                rs.close();
                ps.close();
                Map<String, Integer> emptyMap = new ConcurrentHashMap<>();
                for (String k : CoinManager.getCoins()) {
                    emptyMap.put(k, 0);
                }
                Blob b = conn.createBlob();
                ObjectOutputStream outputStream = new ObjectOutputStream(b.setBinaryStream(1));
                outputStream.writeObject(emptyMap);
                outputStream.close();
                PreparedStatement ps2 = conn.prepareStatement("INSERT INTO cute_coin_data (uuid, main_coin_value, map) VALUES (?, ?, ?)");
                ps2.setString(1, id.toString());
                ps2.setInt(2, 0);
                ps2.setBlob(3, b);
                ps2.execute();
                ps2.close();
                conn.close();
            }
        } catch (SQLException e) {
            putError(e);
        } catch (IOException e) {
            putError(e);
        }
    }
}

虽然代码很长,但原理是比较清晰的。

数据表的格式我们设置得很简单,三列分别是 UUID、主货币的价值、存储各个货币对应价值的 Map 的二进制格式。之所以记录价值而不是个数,是因为服主可能会修改配置文件,为防止「通货膨胀一夜暴富」或者「财产查封」这样的情况,即使货币的信息已经不存在(无法转换为个数),我们也可以将其转换为主货币以气死服主

SAVE_THRESHOLD 控制每多少次设置后进行一次保存。

CACHE 是主要缓存空间,每次从数据库读取时都要将数据保存到缓存中。

DIRTY_LIST 记录哪些数据被修改了,只保存真正需要保存的部分,加快了保存的速度。虽然它叫做 DIRTY_LIST,但它实际上是一个 Set,不能保存重复的值,重复加入其中的元素将被忽略,这正是我们需要的。

BLOB 是 MySQL 的一个新数据类型,可以存储二进制数据,通过 ObjectOutputStream 可以将 Map 以二进制形式写入数据库中,反之,通过 Object InputStream 可以读取 Map。通过 createBlobwriteObject 等方法,我们实现了保存数据。


这一节太长了,我本来想在一节里面写完的,但是到现在已经有 4655 词了,剩余的内容我们还是放到下一节吧。