开发者 API
其他插件可以通过 StarMailAPI 把物品、金币、点券、指令奖励寄进玩家的邮箱。玩家背包满了、不在线、在别的子服都没关系,奖励会在邮箱里等他。 同系列的 StarCrate 星月造物就是用它把抽奖奖励寄给玩家的;StarAuction 星月市集则用它(加上 幂等寄送)寄出买到的物品、卖出的收入与退回的东西。
所有类都在 dev.starplugins.mail.api 包里:
| 类 | 用途 |
|---|---|
StarMailAPI | 入口:寄信、查询数量、打开邮箱 |
MailDraft | 一封正在写的信(建造者写法) |
MailSendEvent | 邮件存入前触发,可以修改或取消 |
MailClaimEvent | 玩家领取附件后触发 |
引入
把 StarMail 的 jar 加进编译依赖(只在 编译时使用,不要打包进你的插件),例如 Gradle:
dependencies {
compileOnly files('libs/StarMail.jar') // 资源中心下载的星月邮箱 jar
}
plugin.yml 加上软依赖,确保星月邮箱先载入(没装也能正常启动):
softdepend: [StarMail]
快速开始
import dev.starplugins.mail.api.StarMailAPI;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
if (StarMailAPI.available()) {
StarMailAPI.send(player, StarMailAPI.draft()
.sender("<gold>签到系统")
.title("每日签到奖励")
.content("感谢你今天也来签到!", "明天继续加油~")
.item(new ItemStack(Material.DIAMOND, 3))
.money(100)
.source("MySignIn"));
}
星月邮箱没安装、被停用、或授权还没验证通过时,available() 返回 false,这时调用 send / sendOnline / sendAll 会抛出 IllegalStateException。
StarMailAPI
| 方法 | 返回 | 说明 |
|---|---|---|
available() | boolean | 现在能不能寄信 |
draft() | MailDraft | 建立一封空白的信,等于 new MailDraft() |
send(UUID receiver, String receiverName, MailDraft draft) | CompletableFuture<List<Long>> | 寄给一个玩家(在线、离线都可以)。返回新邮件的编号;物品超过 mail.max-items 时拆成好几封,所以可能有好几个编号;被 MailSendEvent 取消时是空列表 |
send(OfflinePlayer receiver, MailDraft draft) | 同上 | 同上,直接传玩家 |
sendOnline(MailDraft draft) | CompletableFuture<Integer> | 寄给这个子服上所有在线玩家,每人一封。返回收到的人数 |
sendAll(MailDraft draft, boolean onlyExisting, String permission) | CompletableFuture<Long> | 全服邮件,返回全服邮件编号(被取消时是 -1)。见下方 |
unread(UUID uuid) | int | 在线玩家的未读数(读取缓存,不查数据库);不知道时 -1 |
unclaimed(UUID uuid) | int | 在线玩家附件待领取的邮件数;不知道时 -1 |
open(Player player) | boolean | 帮玩家打开邮箱(装了云界面时打开 云界面邮箱) |
寄信的细节
- API 寄出的信类型是「系统」,期限默认
mail.system-expire-days(30 天),寄件人没写时用default-sender。 - 寄信是异步的:方法马上返回,数据库写入在后台完成。
CompletableFuture的回调在后台线程执行,要碰 Bukkit 的东西(发消息、改背包)请切回主线程(Folia 用对应的调度器)。 - 收件人在线时,会马上收到新邮件提示;在别的子服时,几秒内收到。
StarMailAPI.send(uuid, name, draft).thenAccept(ids -> {
if (ids.isEmpty()) getLogger().info("寄信被其他插件取消了");
else getLogger().info("已寄出邮件 #" + ids.get(0));
}).exceptionally(e -> {
getLogger().warning("寄信失败:" + e);
return null;
});
全服邮件 sendAll
// 所有玩家(含之后第一次进服的新玩家)
StarMailAPI.sendAll(draft, false, "");
// 只给现在已经进过服的玩家
StarMailAPI.sendAll(draft, true, "");
// 只给有 group.vip 权限的玩家
StarMailAPI.sendAll(draft, false, "group.vip");
| 参数 | 说明 |
|---|---|
onlyExisting | true = 只发给发送前就进过服的玩家;false = 之后的新玩家也收到 |
permission | 只发给有这个权限的玩家;"" 或 null = 所有人。权限在玩家在线时检查:现在没有权限的玩家不会收到,之后拿到权限、全服邮件还在发放期间上线时就会收到 |
- 每个玩家只会收到一封;跨服时任何子服都会发放。
- 期限从发出那一刻开始算,到期后不再发给新上线的玩家。想一直发,
expireDays(0)。 - 一封全服邮件只能有
mail.max-items组物品,超过的部分会被丢弃(控制台警告),不会拆成多封。
MailDraft
所有设定方法都返回 MailDraft 本身,可以一直串下去。文字都支持 & 颜色、&#RRGGBB 与 MiniMessage。
| 方法 | 说明 |
|---|---|
sender(String name) | 寄件人名称。不写 = default-sender |
senderUuid(UUID uuid) | 寄件人的 UUID(一般不需要) |
title(String title) | 标题 |
content(String... lines) / content(Collection<String> lines) | 正文,会取代之前设的正文;字符串里的 \n 也会分行 |
item(ItemStack item) | 加一个物品(会复制一份,空气与数量 0 的会被忽略) |
items(Collection<ItemStack> list) | 加好几个物品 |
clearItems() | 清空物品 |
money(double amount) | 金币(负数当 0)。领取的子服需要 Vault |
points(int amount) | 点券(负数当 0)。领取的子服需要 PlayerPoints |
command(String command) / commands(Collection<String> list) | 领取时执行的指令,见下方 |
note(String line) / notes(Collection<String> list) | 附件说明,显示在附件列表(例如「VIP 7 天」) |
icon(String material) / icon(Material material) | 附件待领取时在邮箱里显示的图标;空 = 默认箱子 |
source(String source) | 来源标记(例如你的插件名称),会传到 MailClaimEvent,方便你认出自己寄的信 |
expireDays(double days) | 几天后过期;0 = 永不过期 |
expireMillis(long ms) | 同上,用毫秒;0 = 永不过期,-1 = 默认 |
key(String key) | (1.1.0+)幂等寄送的唯一键:同一个 key 只会存一次邮件,最长 128 个字符,见 幂等寄送 |
copy() | 复制一份 |
hasAttachments() | 有没有物品、金币、点券或指令 |
对应的读取方法:sender()、senderUuid()、title()、content()、items()、money()、points()、commands()、notes()、icon()、source()、expireMillis()、key()(没设时是空字符串)。
领取指令
draft.command("lp user {player} parent addtemp vip 7d") // 控制台执行
.command("[player] warp vip") // 玩家自己执行
.command("[message] <green>VIP 已开通!") // 给玩家发一条消息
.note("VIP 会员 7 天"); // 让玩家在附件列表看到奖励名称
- 变量:
{player}/%player%= 玩家名,{uuid}= 玩家 UUID。 - 没有前缀(或
[console])= 控制台执行;[player]= 玩家执行;[message]= 发消息。开头的/会自动去掉。 - 指令在玩家领取附件时、在领取的那个子服执行。
幂等寄送:同一个 key 只寄一次
从 1.1.0 起,MailDraft 可以带一个 key。带 key 寄出的信,同一个 key 在数据库里最多只存一次:
- 第一次寄出:照常存入邮件,并记下这个 key 与邮件编号(两者写在同一个数据库交易里,要么都存进去,要么都没有)。
- 之后再用同一个 key 寄(不论内容是否相同):不会再存任何邮件,直接返回第一次的邮件编号;收件人也不会再收到新邮件提示。
- 两个子服同时用同一个 key 寄:数据库主键只让一边成功,另一边拿到的是成功那一边的邮件编号。
这让「寄出去了没有」不确定的情况可以放心重试:等待回应超时、寄出后还没记录结果就当机、好几个子服都可能负责寄同一笔……只要每次都带同一个 key,玩家最后只会收到一份。
long rewardId = 1234; // 你自己数据库里这笔奖励的编号
StarMailAPI.send(uuid, name, StarMailAPI.draft()
.title("活动奖励")
.item(new ItemStack(Material.DIAMOND, 5))
.source("MyEvent")
.key("myevent:reward:" + rewardId))
.thenAccept(ids -> {
// 第一次或重试都会拿到同一组邮件编号:这时才把奖励标记为「已寄出」
if (!ids.isEmpty()) markSent(rewardId, ids.get(0));
});
// 超时、失败、开服时发现还没标记:用同一个 key 再寄一次就好,不会重复
| 注意事项 | 说明 |
|---|---|
| key 要唯一且固定 | 建议写成 插件名:用途:编号,例如 myplugin:reward:1234;超过 128 个字符的部分会被截掉 |
先检查 available() | 星月邮箱没就绪时照样会抛出 IllegalStateException,之后再重试即可 |
| 被事件取消 | MailSendEvent 仍然会触发;被取消时返回空列表、不会记下 key,之后可以再寄 |
| 物品拆成多封 | 物品超过 mail.max-items 拆成好几封时,整组邮件共用这个 key,重寄时返回同一组编号 |
| 跨服 | key 存在星月邮箱的数据库里:子服要共用同一个 MySQL(同样的 table-prefix)才能跨服防重复,见 跨服设置 |
| 保留多 久 | key 记录在 mail_key 表,与邮件记录一起保留 mail.log-keep-days 天(默认 90 天,0 = 永久),过期后清除;同一个 key 隔这么久再寄会被当成新邮件 |
| 不带 key | 没设 key(或设成空字符串)时和以前完全一样,每次寄送都是新邮件 |
key() 是 1.1.0 新增的方法。想兼容旧版时,可以先用反射检查 MailDraft.class.getMethod("key", String.class) 是否存在;StarAuction 星月市集就是这样在开服时检查星月邮箱版本的。
一个完整的例子:抽奖奖励寄到邮箱
public void giveRewards(Player player, List<ItemStack> prizes, String crateName) {
if (!StarMailAPI.available()) {
// 没有星月邮箱:直接给,放不下的掉在地上
player.getInventory().addItem(prizes.toArray(new ItemStack[0]))
.values().forEach(i -> player.getWorld().dropItemNaturally(player.getLocation(), i));
return;
}
MailDraft draft = StarMailAPI.draft()
.sender("<gradient:#F7971E:#FFD200>" + crateName + "</gradient>")
.title("开箱奖励")
.content("恭喜获得以下奖励:", "请在 7 天内领取。")
.items(prizes)
.icon(prizes.isEmpty() ? Material.CHEST : prizes.get(0).getType())
.source("MyCrates")
.expireDays(7);
StarMailAPI.send(player, draft);
}
事件
MailSendEvent
邮件存入数据库之前触发。可以修改内容,或取消。
| 方法 | 说明 |
|---|---|
target() | PLAYER(寄给一个玩家)、ONLINE(本服全部在线)、ALL(全服邮件) |
receiver() / receiverName() | 收件人;ONLINE、ALL 时为 null |
type() | SYSTEM(API)、ADMIN(管理员 / 指令)、PLAYER(玩家互寄)、BROADCAST(全服) |
draft() | 这封信,可以直接修改(标题、物品…) |
isCancelled() / setCancelled(boolean) | 取消后不会寄出 |
- 可能在非主线程触发(
isAsynchronous()为true),监听器里不要直接操作世界与玩家。 - 全服邮件只在发出时触发一次(
target()为ALL),之后复制到每个玩家邮箱时不会再触发。 - 取消玩家互寄(
PLAYER)时,寄件人的附件、邮费与金币会退回。 - 「背包已满 · 物品退回」与过期退信是插件自己保管物品用的,不会触发这个事件,所以不会被其他插件取消或修改。
@EventHandler
public void onSend(MailSendEvent e) {
if (e.type().equals("PLAYER") && e.draft().money() > 100000) {
e.setCancelled(true); // 不允许玩家一次寄太多金币
}
}
MailClaimEvent
玩家领取附件之后触发(每封邮件一次),在玩家所在的线程。只用来通知,不能取消。
| 方法 | 说明 |
|---|---|
getPlayer() | 领取的玩家 |
mailId() | 邮件编号 |
title() | 标题 |
source() | 寄信时 MailDraft.source(...) 设的来源 |
items() | 领到的物品(复制品) |
money() / points() | 领到的金币 / 点券 |
commands() | 执行的指令 |
@EventHandler
public void onClaim(MailClaimEvent e) {
if ("MyCrates".equals(e.source())) {
e.getPlayer().sendMessage("你领取了开箱奖励 #" + e.mailId());
}
}