Skip to content

开发接口

LyMySQLCore 为 Bukkit/Paper 插件提供基于 MySQL 的玩家数据加载、保存和跨服务器数据锁机制。接入插件可以通过监听事件,或注册 MySQLSupplier,在玩家数据加载完成、退出保存和周期保存时处理自己的数据。

LyMySQLCore 只有在插件已安装且成功连接 MySQL 数据库后,玩家数据锁、加载、保存和相关事件才会生效。

功能概览

  • 玩家进入服务器后,先尝试获取 MySQL 数据锁,再异步加载玩家数据。
  • 数据加载期间限制玩家移动、交互、聊天、背包操作和主动攻击。
  • 数据加载完成后触发 MySQLSafePlayerLoadEvent,随后解除加载限制。
  • 玩家退出或被踢出时执行保存并释放当前服务器持有的数据锁。
  • 服务器执行 stoprestart 时保存在线玩家数据,并等待当前服务器的玩家锁释放。
  • 通过周期保存任务调用 Supplier 的 cycleSaveData
  • 使用 MySQL 锁表避免同一玩家在多个服务器同时操作数据。
  • 自动记录玩家名称与 UUID,使用 lymysqlcore_playerlog 表保存玩家记录。
  • 自动创建 lymysqlcore_lock 表保存玩家数据锁。

依赖

接入 LyMySQLCore 的插件需要在服务器中安装 LyMySQLCore,并在插件成功连接 MySQL 后使用相关 API。

插件的 plugin.yml 可以声明硬依赖:

yaml
depend:
  - LyMySQLCore

如果插件只在 LyMySQLCore 存在时启用相关功能,可以声明软依赖:

yaml
softdepend:
  - LyMySQLCore

LyMySQLCore 自身的 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字符串mc2MySQL 数据库名称。
mysql.username字符串mc2MySQL 用户名。
mysql.password字符串mc1234MySQL 密码。
mysql.port整数3306MySQL 端口。
mysql.ip字符串127.0.0.1MySQL 地址。
mysql.link字符串useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8JDBC 连接参数。
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) {
        // 在这里保存玩家数据
    }
}

该事件会在以下流程中使用:

  • 玩家正常退出。
  • 玩家被踢出服务器。
  • 服务器执行 stoprestart 并保存在线玩家时。

保存完成后,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 的 loadDatasaveDatacycleSaveData 会按照注册优先级执行。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 优先级

可用优先级及执行顺序如下:

优先级数值执行顺序
LOWEST0最早执行
LOW1较早执行
NORMAL2默认顺序
HIGH3较晚执行
HIGHEST4更晚执行
MONITOR5最后执行

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)判断玩家是否处于 CONNECTINGLOADINGFAILED 状态。
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 的方法按什么顺序执行?

按照 LOWESTLOWNORMALHIGHHIGHESTMONITOR 的顺序执行。同一优先级下,项目没有提供额外的排序保证。

为什么玩家加载时不能移动或操作?

LyMySQLCore 会在获取数据锁和完成数据加载前限制玩家操作,避免玩家在旧数据尚未读取完成时修改游戏状态。数据加载完成并触发 MySQLSafePlayerLoadEvent 后,限制会解除。

为什么玩家被提示数据加载失败?

常见原因包括:

  • 其它服务器仍持有该玩家的有效数据锁。
  • MySQL 连接异常。
  • Supplier 的 loadData 抛出异常。
  • 获取玩家数据锁的尝试次数达到 100 次。

应先检查 LyMySQLCore 控制台日志和数据库连接状态,再检查 Supplier 的加载逻辑。

是否需要手动创建数据表?

不需要。LyMySQLCore 启动时会尝试自动创建 lymysqlcore_locklymysqlcore_playerlog 表。前提是数据库连接成功,并且数据库用户具有创建表和写入数据的权限。