开发接口
LyMySQLCore 为 Bukkit/Paper 插件提供基于 MySQL 的玩家数据加载、保存和跨服务器数据锁机制。接入插件可以通过监听事件,或注册 MySQLSupplier,在玩家数据加载完成、退出保存和周期保存时处理自己的数据。
LyMySQLCore 只有在插件已安装且成功连接 MySQL 数据库后,玩家数据锁、加载、保存和相关事件才会生效。
功能概览
- 玩家进入服务器后,先尝试获取 MySQL 数据锁,再异步加载玩家数据。
- 数据加载期间限制玩家移动、交互、聊天、背包操作和主动攻击。
- 数据加载完成后触发
MySQLSafePlayerLoadEvent,随后解除加载限制。 - 玩家退出或被踢出时执行保存并释放当前服务器持有的数据锁。
- 服务器执行
stop或restart时保存在线玩家数据,并等待当前服务器的玩家锁释放。 - 通过周期保存任务调用 Supplier 的
cycleSaveData。 - 使用 MySQL 锁表避免同一玩家在多个服务器同时操作数据。
- 自动记录玩家名称与 UUID,使用
lymysqlcore_playerlog表保存玩家记录。 - 自动创建
lymysqlcore_lock表保存玩家数据锁。
依赖
接入 LyMySQLCore 的插件需要在服务器中安装 LyMySQLCore,并在插件成功连接 MySQL 后使用相关 API。
插件的 plugin.yml 可以声明硬依赖:
yaml
depend:
- LyMySQLCore如果插件只在 LyMySQLCore 存在时启用相关功能,可以声明软依赖:
yaml
softdepend:
- LyMySQLCoreLyMySQLCore 自身的 plugin.yml 未声明其它插件依赖。
配置相关
LyMySQLCore 默认从 plugins/LyMySQLCore/config.yml 读取配置。数据库连接配置位于 mysql 节点下:
yaml
mysql:
databasename: mc2
username: mc2
password: mc1234
port: 3306
ip: 127.0.0.1
link: 'useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8'
join-load-delay: 20| 配置键 | 类型 | 默认值 | 说明 |
|---|---|---|---|
mysql.databasename | 字符串 | mc2 | MySQL 数据库名称。 |
mysql.username | 字符串 | mc2 | MySQL 用户名。 |
mysql.password | 字符串 | mc1234 | MySQL 密码。 |
mysql.port | 整数 | 3306 | MySQL 端口。 |
mysql.ip | 字符串 | 127.0.0.1 | MySQL 地址。 |
mysql.link | 字符串 | useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8 | JDBC 连接参数。 |
join-load-delay | 长整数 | 20 | 玩家异步数据加载完成后,触发加载事件前等待的 tick 数。 |
连接成功后,插件会创建以下数据表:
| 表名 | 用途 |
|---|---|
lymysqlcore_lock | 保存玩家当前持有的数据锁、服务器标识和锁更新时间。 |
lymysqlcore_playerlog | 保存玩家名称与 UUID 的对应记录。 |
监听玩家数据加载
MySQLSafePlayerLoadEvent 表示玩家数据已经由已注册的 Supplier 加载完成,并且玩家即将解除加载限制。普通业务插件应在此事件中读取已经准备好的玩家数据,并开放依赖这些数据的功能。
java
import Ly.sqlcore.safer.event.MySQLSafePlayerLoadEvent;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
public class DataListener implements Listener {
@EventHandler
public void onMySQLLoad(MySQLSafePlayerLoadEvent event) {
// 玩家数据已经加载完成,在这里应用数据并开放插件功能
}
}事件类继承 Bukkit 的 PlayerEvent,可以通过 event.getPlayer() 获取玩家对象。
java
@EventHandler
public void onMySQLLoad(MySQLSafePlayerLoadEvent event) {
Player player = event.getPlayer();
// 使用 player 处理加载完成后的逻辑
}玩家进入服务器后,LyMySQLCore 会先建立本地数据处理状态并尝试获取 MySQL 锁。只有获得锁并完成异步数据加载后,才会触发该事件。不要在此事件之前开放依赖玩家数据库数据的操作。
监听玩家数据保存
MySQLSafePlayerSaveEvent 表示已注册 Supplier 的 saveData 已执行,事件会在保存流程中触发。可以在此事件中保存业务插件自身的数据。
java
import Ly.sqlcore.safer.event.MySQLSafePlayerSaveEvent;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
public class DataListener implements Listener {
@EventHandler
public void onMySQLSave(MySQLSafePlayerSaveEvent event) {
// 在这里保存玩家数据
}
}该事件会在以下流程中使用:
- 玩家正常退出。
- 玩家被踢出服务器。
- 服务器执行
stop或restart并保存在线玩家时。
保存完成后,LyMySQLCore 会释放该玩家在当前服务器上的锁,并清理玩家状态。
监听周期保存
MySQLSafePlayerCycleSaveEvent 表示一次周期保存流程已处理到该玩家。周期保存由 LyMySQLCore 的定时任务触发,并调用已注册 Supplier 的 cycleSaveData。
java
import Ly.sqlcore.safer.event.MySQLSafePlayerCycleSaveEvent;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
public class DataListener implements Listener {
@EventHandler
public void onMySQLCycleSave(MySQLSafePlayerCycleSaveEvent event) {
// 在这里执行周期保存后的逻辑
}
}周期保存任务每 20 秒检查一次在线玩家。每个 Supplier 会根据自身上次处理时间和随机分散逻辑参与周期保存,避免所有 Supplier 在同一时刻集中执行。处于加载状态的玩家不会进入周期保存玩家列表。
注册 Supplier
需要接入 LyMySQLCore 数据处理顺序时,可以实现 MySQLSupplier 并注册到 MySQLSupplierManager。
java
import Ly.sqlcore.safer.MySQLSupplier;
import Ly.sqlcore.safer.MySQLSupplierManager;
import org.bukkit.OfflinePlayer;
public class DemoSupplier implements MySQLSupplier {
@Override
public String getName() {
return "DemoPlugin";
}
@Override
public void loadData(OfflinePlayer player) {
// 读取并准备玩家数据
}
@Override
public void saveData(OfflinePlayer player) {
// 保存玩家数据
}
@Override
public void cycleSaveData(OfflinePlayer player) {
// 周期保存玩家数据
}
public static void register() {
MySQLSupplierManager.registerSupplier(new DemoSupplier());
}
}MySQLSupplier 方法
| 方法 | 说明 |
|---|---|
getName() | 返回 Supplier 的唯一名称。 |
loadData(OfflinePlayer player) | 玩家首次进入并获得数据锁后调用,用于加载玩家数据。 |
saveData(OfflinePlayer player) | 玩家退出、被踢出或服务器关闭保存时调用。 |
cycleSaveData(OfflinePlayer player) | 周期保存时调用。 |
Supplier 的 loadData、saveData 和 cycleSaveData 会按照注册优先级执行。Supplier 方法抛出异常时,当前数据处理流程会失败;玩家加载异常会被记录、解锁并踢出服务器。
Supplier 注册与删除
注册默认优先级
java
MySQLSupplierManager.registerSupplier(new DemoSupplier());未指定优先级时使用 MySQLSupplierPriority.NORMAL。
注册指定优先级
java
MySQLSupplierManager.registerSupplier(
new DemoSupplier(),
MySQLSupplierPriority.HIGH
);同名 Supplier 只能注册一次。重复注册相同名称时会抛出运行时异常。
删除 Supplier
可以通过名称或 Supplier 实例删除已注册的 Supplier:
java
MySQLSupplierManager.deleteSupplier("DemoPlugin");
MySQLSupplierManager.deleteSupplier(demoSupplier);获取已注册 Supplier
java
Set<MySQLSupplierWrapper> suppliers = MySQLSupplierManager.getSuppliers();MySQLSupplierWrapper 为包内类型,普通接入插件通常不需要直接操作返回集合。Supplier 的注册、删除和执行集合使用并发集合实现。
Supplier 优先级
可用优先级及执行顺序如下:
| 优先级 | 数值 | 执行顺序 |
|---|---|---|
LOWEST | 0 | 最早执行 |
LOW | 1 | 较早执行 |
NORMAL | 2 | 默认顺序 |
HIGH | 3 | 较晚执行 |
HIGHEST | 4 | 更晚执行 |
MONITOR | 5 | 最后执行 |
LyMySQLCore 遍历 MySQLSupplierPriority.values(),因此数值较小的优先级先执行。
玩家数据处理状态
PlayerService 用于记录玩家当前的数据处理状态。状态枚举为 PlayerPhase:
| 状态 | 说明 |
|---|---|
CONNECTING | 玩家刚进入服务器,正在等待获得数据锁。 |
LOADING | 玩家已经获得数据锁,正在异步加载数据。 |
LOADED | 玩家数据加载完成,可以正常参与游戏。 |
LEAVING | 玩家离服保存流程已经开始,用于避免重复保存。 |
FAILED | 玩家数据加载失败,等待清理状态并踢出。 |
可以通过以下方法查询状态:
java
import Ly.sqlcore.safer.PlayerPhase;
import Ly.sqlcore.safer.PlayerService;
PlayerPhase phase = PlayerService.getPhase(player.getName());
boolean loading = PlayerService.isLoading(player.getName());
boolean connecting = PlayerService.isConnecting(player.getName());
boolean leaving = PlayerService.isLeave(player.getName());| 方法 | 说明 |
|---|---|
PlayerService.getPlayerData(String name) | 获取玩家当前的 PlayerData,不存在时返回 null。 |
PlayerService.getPhase(String name) | 获取玩家当前的 PlayerPhase,不存在时返回 null。 |
PlayerService.isLoading(String name) | 判断玩家是否处于 CONNECTING、LOADING 或 FAILED 状态。 |
PlayerService.isConnecting(String name) | 判断玩家是否处于 CONNECTING 状态。 |
PlayerService.isLeave(String name) | 判断玩家是否处于 LEAVING 状态。 |
PlayerService 的状态主要由 LyMySQLCore 内部维护。业务插件通常应优先监听加载、保存和周期保存事件,而不是直接切换玩家状态。
玩家加载期间的限制
玩家进入服务器后,在数据处理状态未完成前,LyMySQLCore 会暂时限制以下操作:
- 移动,并尝试将玩家保持在进入服务器时的位置。
- 物品交互。
- 丢弃物品。
- 打开或点击背包。
- 聊天。
- 作为攻击者主动攻击实体。
加载期间会施加失明效果。数据加载成功后移除该效果;加载失败时会释放锁并踢出玩家。
数据锁机制
LyMySQLCore 使用 lymysqlcore_lock 表记录玩家锁。玩家进入服务器后,插件会检查该玩家是否仍被其它服务器持有有效锁:
- 没有其它服务器的有效锁时,当前服务器获取玩家锁并开始加载数据。
- 当前服务器已经持有锁时,不会重复开始加载。
- 其它服务器持有有效锁时,当前服务器继续等待。
- 连续尝试获取锁达到 100 次后,玩家会被标记为加载失败并踢出服务器。
- 当前服务器保存完成后,将玩家锁标记为释放状态。
- 插件启动时会释放当前服务器遗留的锁状态。
- 插件关闭时等待当前服务器仍然有效的玩家锁释放,最多等待 10 次,每次间隔 1 秒。
插件会定时更新已加载玩家的锁时间。处于加载状态或已经不在线的玩家不会被更新。
线程注意事项
Supplier 的玩家数据加载和保存流程可能在异步任务中执行。数据库查询和写入可以放在异步流程中,但涉及 Bukkit 玩家、背包、实体、世界或方块的操作应切回主线程执行。
加载完成事件、保存事件和周期保存事件由 Bukkit 事件系统触发。业务插件应根据自身逻辑确认 Bukkit API 的线程要求,不要在异步数据库回调中直接修改游戏对象。
FAQ
为什么监听不到加载事件?
请确认:
- 服务器已安装 LyMySQLCore。
plugins/LyMySQLCore/config.yml中的 MySQL 地址、端口、数据库名、用户名和密码正确。- LyMySQLCore 已成功连接数据库并完成初始化。
- 你的插件已经注册事件监听器。
- 玩家确实完成了数据锁获取和异步加载流程。
数据库连接失败或初始化失败时,LyMySQLCore 会禁用自身,相关玩家数据功能不会生效。
应该在什么时候开放插件功能?
应在 MySQLSafePlayerLoadEvent 触发后开放依赖数据库玩家数据的功能。在该事件之前,玩家可能仍处于等待锁或异步加载阶段。
保存逻辑应该放在哪里?
可以将保存逻辑放在 MySQLSafePlayerSaveEvent 中。需要周期保存的逻辑实现 MySQLSupplier.cycleSaveData,或监听 MySQLSafePlayerCycleSaveEvent。
Supplier 名称可以重复吗?
不可以。MySQLSupplierManager 根据 getName() 判断重复名称。重复注册会抛出运行时异常。
Supplier 的方法按什么顺序执行?
按照 LOWEST、LOW、NORMAL、HIGH、HIGHEST、MONITOR 的顺序执行。同一优先级下,项目没有提供额外的排序保证。
为什么玩家加载时不能移动或操作?
LyMySQLCore 会在获取数据锁和完成数据加载前限制玩家操作,避免玩家在旧数据尚未读取完成时修改游戏状态。数据加载完成并触发 MySQLSafePlayerLoadEvent 后,限制会解除。
为什么玩家被提示数据加载失败?
常见原因包括:
- 其它服务器仍持有该玩家的有效数据锁。
- MySQL 连接异常。
- Supplier 的
loadData抛出异常。 - 获取玩家数据锁的尝试次数达到 100 次。
应先检查 LyMySQLCore 控制台日志和数据库连接状态,再检查 Supplier 的加载逻辑。
是否需要手动创建数据表?
不需要。LyMySQLCore 启动时会尝试自动创建 lymysqlcore_lock 和 lymysqlcore_playerlog 表。前提是数据库连接成功,并且数据库用户具有创建表和写入数据的权限。