第 14 章:占位符扩展
第 14 章:占位符扩展
学习 PlaceholderAPI 集成,让你的插件数据可被其他插件引用。
14.1 PlaceholderAPI 概述
PlaceholderAPI (PAPI) 是 Bukkit 服务器上最流行的占位符框架。它允许插件注册自定义变量(占位符),其他插件可以通过统一的格式引用这些数据。
占位符格式
%扩展名_参数%
示例:
%player_name%— 玩家名称%player_health%— 玩家生命值%server_online%— 在线人数%myplugin_balance%— 你的插件的余额占位符
常用内置扩展
| 扩展 | 前缀 | 常用占位符 |
|---|---|---|
| Player | %player_ | name, health, level, gamemode |
| Server | %server_ | online, max_players, tps |
| Vault | %vault_eco_balance% | 经济余额 |
| Statistic | %statistic_ | 玩家统计 |
14.2 添加 PlaceholderAPI 依赖
Maven 依赖
<repository>
<id>placeholderapi</id>
<url>https://repo.extendedclip.net/content/repositories/placeholderapi/</url>
</repository>
<dependency>
<groupId>me.clip</groupId>
<artifactId>placeholderapi</artifactId>
<version>2.11.6</version>
<scope>provided</scope>
</dependency>
plugin.yml 声明
softdepend:
- PlaceholderAPI
14.3 注册自定义占位符
实现 Expansion
package com.example.myplugin.placeholders;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class MyPluginExpansion extends PlaceholderExpansion {
private final MyPlugin plugin;
public MyPluginExpansion(MyPlugin plugin) {
this.plugin = plugin;
}
/**
* 占位符标识符(前缀)
* 返回 "myplugin" → 占位符格式: %myplugin_xxx%
*/
@Override
public @NotNull String getIdentifier() {
return "myplugin";
}
/**
* 作者
*/
@Override
public @NotNull String getAuthor() {
return plugin.getDescription().getAuthors().toString();
}
/**
* 版本
*/
@Override
public @NotNull String getVersion() {
return plugin.getDescription().getVersion();
}
/**
* 是否持久化(插件禁用后是否保留)
*/
@Override
public boolean persist() {
return true;
}
/**
* 是否可以注册
*/
@Override
public boolean canRegister() {
return true;
}
/**
* 处理占位符请求
* @param player 玩家(可能为 null,表示服务器级占位符)
* @param params 占位符参数(去掉前缀后的部分)
* @return 占位符值
*/
@Override
public @Nullable String onRequest(OfflinePlayer player,
@NotNull String params) {
// 处理无玩家的服务器级占位符
if (player == null) {
return handleServerPlaceholder(params);
}
// 处理玩家级占位符
return handlePlayerPlaceholder(player, params);
}
/**
* 处理玩家级占位符
*/
private String handlePlayerPlaceholder(OfflinePlayer player, String params) {
// %myplugin_balance%
if (params.equalsIgnoreCase("balance")) {
double balance = EconomyManager.getBalance(player.getUniqueId());
return String.format("%.2f", balance);
}
// %myplugin_level%
if (params.equalsIgnoreCase("level")) {
int level = DataManager.getLevel(player.getUniqueId());
return String.valueOf(level);
}
// %myplugin_playtime%
if (params.equalsIgnoreCase("playtime")) {
long playTime = DataManager.getPlayTime(player.getUniqueId());
return formatTime(playTime);
}
// %myplugin_rank%
if (params.equalsIgnoreCase("rank")) {
return DataManager.getRank(player.getUniqueId());
}
// %myplugin_top_balance_1%
// 处理带参数的占位符
if (params.startsWith("top_balance_")) {
String rankStr = params.substring("top_balance_".length());
try {
int rank = Integer.parseInt(rankStr);
return DataManager.getTopBalance(rank);
} catch (NumberFormatException e) {
return "N/A";
}
}
return null; // 未知占位符
}
/**
* 处理服务器级占位符
*/
private String handleServerPlaceholder(String params) {
// %myplugin_total_players%
if (params.equalsIgnoreCase("total_players")) {
return String.valueOf(DataManager.getTotalPlayers());
}
// %myplugin_total_balance%
if (params.equalsIgnoreCase("total_balance")) {
return String.format("%.2f", DataManager.getTotalBalance());
}
return null;
}
private String formatTime(long ticks) {
long seconds = ticks / 20;
long hours = seconds / 3600;
long minutes = (seconds % 3600) / 60;
return hours + "h " + minutes + "m";
}
}
注册 Expansion
// 在 onEnable() 中
@Override
public void onEnable() {
// 检查 PlaceholderAPI 是否存在
if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) {
new MyPluginExpansion(this).register();
getLogger().info("PlaceholderAPI 扩展已注册!");
}
}
14.4 使用其他插件的占位符
读取占位符值
import me.clip.placeholderapi.PlaceholderAPI;
public class PlaceholderHelper {
/**
* 替换字符串中的占位符
*/
public static String setPlaceholders(Player player, String text) {
return PlaceholderAPI.setPlaceholders(player, text);
}
/**
* 替换字符串中的括号占位符
*/
public static String setBracketPlaceholders(Player player, String text) {
return PlaceholderAPI.setBracketPlaceholders(player, text);
}
/**
* 检查占位符是否存在
*/
public static boolean isRegistered(String identifier) {
return PlaceholderAPI.isRegistered(identifier);
}
/**
* 获取占位符的值
*/
public static String getPlaceholder(Player player, String placeholder) {
return PlaceholderAPI.setPlaceholders(player, "%" + placeholder + "%");
}
}
使用示例
// 在侧边栏中使用其他插件的占位符
String line = "%player_name% - %vault_eco_balance%";
String parsed = PlaceholderAPI.setPlaceholders(player, line);
// 结果: "Steve - 1000.00"
14.5 动态占位符缓存
对于计算成本较高的占位符,应该使用缓存。
public class CachedExpansion extends PlaceholderExpansion {
private final Map<UUID, Map<String, CachedValue>> cache = new HashMap<>();
private static final long CACHE_TTL = 5000; // 5 秒缓存
@Override
public String onRequest(OfflinePlayer player, String params) {
if (player == null || !player.isOnline()) return null;
UUID uuid = player.getUniqueId();
// 检查缓存
Map<String, CachedValue> playerCache = cache.computeIfAbsent(
uuid, k -> new HashMap<>());
CachedValue cached = playerCache.get(params);
if (cached != null && !cached.isExpired()) {
return cached.value();
}
// 计算新值
String value = computeValue(player, params);
// 更新缓存
playerCache.put(params, new CachedValue(value, System.currentTimeMillis()));
return value;
}
private String computeValue(OfflinePlayer player, String params) {
// 实际计算逻辑
return "computed_value";
}
private record CachedValue(String value, long timestamp) {
boolean isExpired() {
return System.currentTimeMillis() - timestamp > CACHE_TTL;
}
}
}
14.6 多语言占位符支持
@Override
public String onRequest(OfflinePlayer player, String params) {
// %myplugin_balance_formatted%
// %myplugin_balance_short%
String[] parts = params.split("_");
if (parts.length == 0) return null;
String type = parts[0];
if ("balance".equals(type)) {
double balance = EconomyManager.getBalance(player.getUniqueId());
if (parts.length > 1) {
return switch (parts[1]) {
case "formatted" -> String.format("§6$%,.2f", balance);
case "short" -> formatShort(balance);
case "integer" -> String.valueOf((int) balance);
default -> String.valueOf(balance);
};
}
return String.valueOf(balance);
}
return null;
}
private String formatShort(double amount) {
if (amount >= 1_000_000_000) return String.format("%.1fB", amount / 1_000_000_000);
if (amount >= 1_000_000) return String.format("%.1fM", amount / 1_000_000);
if (amount >= 1_000) return String.format("%.1fK", amount / 1_000);
return String.format("%.2f", amount);
}
14.7 业务场景:聊天格式插件
public class ChatFormatter implements Listener {
@EventHandler(priority = EventPriority.HIGH)
public void onChat(AsyncChatEvent event) {
Player player = event.getPlayer();
// 聊天格式模板(从配置读取)
String format = config.getString("chat-format",
"%myplugin_prefix% &7%player_name%&8: &f%message%");
// 替换自定义占位符
format = format.replace("%myplugin_prefix%", getPrefix(player));
// 使用 PlaceholderAPI 替换其他占位符
if (isPAPIEnabled()) {
format = PlaceholderAPI.setPlaceholders(player, format);
}
// 设置渲染器
event.renderer((source, name, msg, viewer) -> {
String rendered = format
.replace("%player_name%", source.getName())
.replace("%message%", PlainTextComponentSerializer.plainText()
.serialize(msg));
return LegacyComponentSerializer.legacySection().deserialize(rendered);
});
}
}
14.8 占位符测试
测试命令
public class PlaceholderTestCommand implements CommandExecutor {
@Override
public boolean onCommand(CommandSender sender, Command command,
String label, String[] args) {
if (!(sender instanceof Player player)) {
sender.sendMessage("§c只能由玩家执行!");
return true;
}
if (args.length == 0) {
sender.sendMessage("§c用法: /papi <占位符>");
return true;
}
String placeholder = args[0];
// 确保格式正确
if (!placeholder.startsWith("%")) placeholder = "%" + placeholder;
if (!placeholder.endsWith("%")) placeholder = placeholder + "%";
// 解析占位符
String result = PlaceholderAPI.setPlaceholders(player, placeholder);
player.sendMessage("§a占位符: §e" + placeholder);
player.sendMessage("§a结果: §f" + result);
return true;
}
}
14.9 PlaceholderAPI 内置占位符列表
常用内置扩展
| 扩展名 | 安装方式 | 占位符 |
|---|---|---|
player | 内置 | %player_name%, %player_uuid% |
server | 内置 | %server_online%, %server_max_players% |
vault | 需安装 | %vault_eco_balance% |
statistic | 需安装 | %statistic_deaths% |
time | 需安装 | %time_current% |
rel_ | 需安装 | 关系占位符(队友、好友等) |
14.10 常见问题排查
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 占位符返回原文 | Expansion 未注册 | 检查 register() 调用 |
| 占位符返回 null | onRequest 返回了 null | 未知占位符返回空字符串 |
| 性能问题 | 占位符计算成本高 | 使用缓存 |
| PlaceholderAPI 未安装 | 依赖缺失 | 用 softdepend + 运行时检查 |
14.11 扩展阅读
14.12 本章小结
| 要点 | 内容 |
|---|---|
| PlaceholderAPI | 统一的占位符框架,插件间数据共享 |
| 注册 Expansion | 继承 PlaceholderExpansion,实现 onRequest |
| 使用占位符 | PlaceholderAPI.setPlaceholders() |
| 缓存 | 计算成本高的占位符应缓存结果 |
| 软依赖 | 用 softdepend + 运行时检查 |
下一章: 第 15 章:Docker 环境 — 学习使用 Docker 搭建开发和测试环境。