Minecraft 原版没有提供合适的经济系统,虽然用金锭代替货币很合适,但不利于携带与交易。此外,插件想要检测玩家的钱财也很困难,这就催生了经济系统插件。
笔者不是很喜欢「金币 - 点券」这样的结构,我们希望服务器使用多种货币,并且可由服主管理。那么我们就来做一个这样的插件。
!> CuteCoin 的许可协议
CuteCoin 不适用首页的许可,它遵循 GNU 通用公共许可证(第三版),请仔细阅读该许可证。该许可证也包含在其代码仓库中。
行动名称: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.yml
、plugin.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
通过当前状态创建并返回合适的数据处理器。
接下来我们就来创建 FileDataManager
和 DBDataManager
。
首先是较简单的基于文件的实现:
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
。通过 createBlob
、writeObject
等方法,我们实现了保存数据。
这一节太长了,我本来想在一节里面写完的,但是到现在已经有 4655 词了,剩余的内容我们还是放到下一节吧。