主题
搜索与多渠道输入框架
概述
这页专门讲 HNCore 提供的两套偏“业务能力型”的开发框架:
- 通用搜索框架 - 支持任意类型的对象搜索、评分、高亮
- 多渠道输入会话框架 - 支持聊天、铁砧、告示牌三种输入方式(告示牌输入在 2.3.0 完整实现)
版本要求
- HNCore: 2.2.0 或更高版本(推荐 2.3.0+)
- 依赖方式: Maven/Gradle 依赖
hncore-api
gradle
dependencies {
compileOnly("com.github.hnplugins:hncore-api:x.y.z")
}其中 x.y.z 应替换为你当前部署的 HNCore 对应版本。
一、通用搜索框架
适用场景
- ✅ 仓库:搜索物品、分类、标签
- ✅ 商店:搜索商品、店铺
- ✅ 拍卖:搜索拍卖品、卖家
- ✅ 邮件:搜索邮件、收件人
- ✅ 任何需要搜索功能的场景
快速开始
步骤 1:实现 Searchable 接口
java
import com.github.hnplugins.hncore.search.Searchable;
public class WarehouseItemSearchable implements Searchable<WarehouseItem> {
@Override
public String getId(WarehouseItem obj) {
return obj.getItemId(); // 主标识符(如 "DIAMOND_SWORD")
}
@Override
public String getDisplayName(WarehouseItem obj) {
return obj.getDisplayName(); // 显示名称(如 "钻石剑")
}
@Override
public List<String> getDetails(WarehouseItem obj) {
// 详细信息(用于搜索匹配)
List<String> details = new ArrayList<>();
details.add(obj.getCategory()); // 分类
details.addAll(obj.getTags()); // 标签
if (obj.getDescription() != null) {
details.add(obj.getDescription()); // 描述
}
return details;
}
}步骤 2:配置搜索别名(可选)
创建 warehouse-search-aliases.yml:
yaml
groups:
# 中英文互查
- ["钻石", "diamond", "宝石"]
- ["剑", "sword", "武器"]
- ["药水", "potion", "药品"]
- ["食物", "food", "吃的"]
# 拼音支持
- ["钻石", "zuanshi", "zs"]
- ["剑", "jian", "j"]步骤 3:创建搜索引擎工厂
java
import com.github.hnplugins.hncore.search.*;
import org.bukkit.configuration.file.FileConfiguration;
public class WarehouseSearchEngineFactory {
public static SearchEngine<WarehouseItem> create(FileConfiguration config, Logger logger) {
// 加载别名
SearchAliasManager aliasManager = SearchAliasManager.load(
config,
WarehouseSearchEngineFactory::loadFallbackAliases,
logger::info
);
// 创建搜索引擎
return new SearchEngine<>(
new WarehouseItemSearchable(),
new DefaultSearchScorer<>(),
aliasManager
);
}
private static void loadFallbackAliases(Map<String, List<String>> aliases) {
// 内置默认别名(当配置文件为空时使用)
SearchAliasManager.registerAliases(aliases, "钻石", "diamond", "宝石");
SearchAliasManager.registerAliases(aliases, "剑", "sword", "武器");
SearchAliasManager.registerAliases(aliases, "药水", "potion", "药品");
// ... 更多别名
}
}步骤 4:使用搜索引擎
java
public class WarehouseService {
private final SearchEngine<WarehouseItem> searchEngine;
private final SearchHighlighter highlighter;
public WarehouseService(Plugin plugin) {
FileConfiguration config = loadConfig("warehouse-search-aliases.yml");
this.searchEngine = WarehouseSearchEngineFactory.create(config, plugin.getLogger());
this.highlighter = new SearchHighlighter("§e"); // 黄色高亮
}
public void openSearchGui(Player player, String keyword) {
// 1. 创建搜索上下文
SearchContext context = searchEngine.createContext(keyword);
// 2. 搜索物品
List<WarehouseItem> allItems = getAllWarehouseItems(player);
List<WarehouseItem> results = searchEngine.search(allItems, context);
// 3. 构建 GUI
GuiBuilder gui = GuiBuilder.create(plugin, "§3仓库搜索", 54);
for (int i = 0; i < results.size() && i < 45; i++) {
WarehouseItem item = results.get(i);
// 4. 高亮显示
String name = highlighter.highlight("§b" + item.getDisplayName(), context);
String id = highlighter.highlight("§7ID: §f" + item.getItemId(), context);
gui.item(i, ItemBuilder.of(item.getIcon())
.name(name)
.lore(id, "§7数量: §f" + item.getAmount())
.build(),
viewer -> takeItem(viewer, item));
}
gui.open(player);
}
}高级用法
自定义评分策略
java
// 创建自定义权重
DefaultSearchScorer.ScoreWeights customWeights = new DefaultSearchScorer.ScoreWeights(
2000, 1600, 1000, // ID: 完全匹配, 前缀匹配, 包含匹配
1900, 1500, 900, // 显示名称
1400, 1100, 600 // 详细信息
);
SearchEngine<MyObject> engine = new SearchEngine<>(
searchable,
new DefaultSearchScorer<>(customWeights, DefaultSearchScorer.ScoreWeights.defaultAlias()),
aliasManager
);自定义高亮颜色
java
SearchHighlighter highlighter = new SearchHighlighter("§e"); // 黄色高亮
// 自动提取恢复颜色
String highlighted = highlighter.highlight("§b钻石剑", context);
// 结果: §b§e钻石§b剑
// 手动指定恢复颜色
String lore = highlighter.highlight("§7描述: §f锋利的钻石剑", context, "§f");二、多渠道输入会话框架
适用场景
| 场景 | 推荐渠道 | 原因 |
|---|---|---|
| 输入标题/名称 | ANVIL | GUI 内输入,体验流畅 |
| 输入价格/数量 | ANVIL | 适合短数字输入 |
| 输入长文本 | CHAT | 无长度限制 |
| 输入多行短文本 | SIGN | 支持 4 行,GUI 内输入 |
快速开始
1. 聊天输入(基础)
java
import com.github.hnplugins.hncore.input.*;
public class MailService {
private final ChatInputSessionManager sessionManager;
public MailService(Plugin plugin) {
this.sessionManager = new ChatInputSessionManager(plugin);
plugin.getServer().getPluginManager().registerEvents(sessionManager, plugin);
}
public void promptMailContent(Player player, MailDraft draft) {
SimpleChatInputSession<MailDraft> session = SimpleChatInputSession
.builder(player.getUniqueId(), draft)
.onInput((p, content) -> {
if (content.isBlank()) {
p.sendMessage("§c内容不能为空");
openComposeMenu(p, draft);
return;
}
draft.setContent(content);
p.sendMessage("§a内容已设置");
openComposeMenu(p, draft);
})
.onCancel(p -> {
p.sendMessage("§7已取消");
openComposeMenu(p, draft);
})
.timeout(60000) // 60 秒超时
.build();
sessionManager.beginSession(player, session);
// 显示提示
player.closeInventory();
player.sendMessage("§a请输入邮件内容:");
player.sendMessage("§7输入 cancel 取消");
}
}2. 铁砧输入(推荐)
java
import com.github.hnplugins.hncore.input.*;
public class ShopService {
private final MultiChannelInputSessionManager sessionManager;
public ShopService(Plugin plugin) {
this.sessionManager = new MultiChannelInputSessionManager(plugin);
plugin.getServer().getPluginManager().registerEvents(sessionManager, plugin);
}
public void promptItemPrice(Player player, ShopItem item) {
AnvilInputSession<ShopItem> session = AnvilInputSession
.builder(player.getUniqueId(), item)
.initialText(String.valueOf(item.getPrice())) // 预填充当前价格
.promptMessage("§a请输入商品价格:")
.onInput((p, input) -> {
try {
double price = Double.parseDouble(input);
if (price <= 0) {
p.sendMessage("§c价格必须大于 0");
openEditMenu(p, item);
return;
}
item.setPrice(price);
p.sendMessage("§a价格已更新为 " + price);
openEditMenu(p, item);
} catch (NumberFormatException e) {
p.sendMessage("§c请输入有效的数字");
openEditMenu(p, item);
}
})
.onCancel(p -> {
p.sendMessage("§7已取消");
openEditMenu(p, item);
})
.timeout(60000)
.build();
// 开始会话(会自动打开铁砧界面)
sessionManager.beginSession(player, session);
}
}3. 告示牌输入(多行)
java
import com.github.hnplugins.hncore.input.*;
import java.util.List;
public class MailService {
private final MultiChannelInputSessionManager sessionManager;
public MailService(Plugin plugin) {
this.sessionManager = new MultiChannelInputSessionManager(plugin);
plugin.getServer().getPluginManager().registerEvents(sessionManager, plugin);
}
public void promptMailContent(Player player, MailDraft draft) {
SignInputSession<MailDraft> session = SignInputSession.<MailDraft>builder(player.getUniqueId(), draft)
.initialLines(List.of("邮件标题", "邮件内容", "", "")) // 预填充 4 行
.promptMessage("§a请在告示牌上输入邮件内容:")
.onInput((p, content) -> {
// content 是所有行合并后的文本(用空格分隔)
if (content.isBlank()) {
p.sendMessage("§c内容不能为空");
openComposeMenu(p, draft);
return;
}
draft.setContent(content);
p.sendMessage("§a内容已设置");
openComposeMenu(p, draft);
})
.onCancel(p -> {
p.sendMessage("§7已取消");
openComposeMenu(p, draft);
})
.timeout(60000)
.build();
// 开始会话(会自动在玩家脚下放置临时告示牌并打开编辑界面)
sessionManager.beginSession(player, session);
}
}4. 带验证的输入
java
public void promptWarehouseName(Player player) {
ValidatedChatInputSession<Void> session = ValidatedChatInputSession
.builder(player.getUniqueId(), null)
.validator(InputValidators.allOf(
InputValidators.NOT_BLANK,
InputValidators.length(1, 16)
))
.onSuccess((p, name) -> {
if (warehouseExists(name)) {
p.sendMessage("§c仓库名称已存在");
openWarehouseList(p);
return;
}
createWarehouse(p, name);
p.sendMessage("§a仓库创建成功:" + name);
openWarehouseList(p);
})
.onFailure((p, input) -> {
p.sendMessage("§c仓库名称必须是 1-16 个字符");
})
.onCancel(p -> openWarehouseList(p))
.maxRetries(3) // 最多重试 3 次
.timeout(60000)
.build();
sessionManager.beginSession(player, session);
player.closeInventory();
player.sendMessage("§a请输入仓库名称:");
}内置验证器
java
import com.github.hnplugins.hncore.input.InputValidators;
// 非空验证
InputValidators.NOT_BLANK
// 数字验证
InputValidators.INTEGER
InputValidators.POSITIVE_INTEGER
InputValidators.DOUBLE
InputValidators.POSITIVE_DOUBLE
// 范围验证
InputValidators.intRange(1, 100) // 1-100 之间的整数
InputValidators.doubleRange(0.1, 999.9) // 0.1-999.9 之间的浮点数
// 长度验证
InputValidators.length(1, 16) // 1-16 个字符
// 正则验证
InputValidators.regex("^[a-zA-Z0-9_]+$")
// 枚举验证
InputValidators.oneOf("yes", "no", "是", "否")
// 组合验证
InputValidators.allOf(
InputValidators.NOT_BLANK,
InputValidators.length(3, 16),
InputValidators.regex("^[a-zA-Z0-9_]+$")
)三、完整示例
示例 1:仓库搜索功能
java
public class HNWarehouseSearchIntegration {
private final HNWarehouse plugin;
private final SearchEngine<WarehouseItem> searchEngine;
private final SearchHighlighter highlighter;
public HNWarehouseSearchIntegration(HNWarehouse plugin) {
this.plugin = plugin;
// 初始化搜索引擎
FileConfiguration config = plugin.getConfigManager().load("warehouse-search-aliases.yml");
this.searchEngine = WarehouseSearchEngineFactory.create(config, plugin.getLogger());
this.highlighter = new SearchHighlighter("§e");
}
public void openSearchGui(Player player, String keyword) {
SearchContext context = searchEngine.createContext(keyword);
List<WarehouseItem> allItems = plugin.getWarehouseService().getPlayerItems(player);
List<WarehouseItem> results = searchEngine.search(allItems, context);
int totalPages = (int) Math.ceil((double) results.size() / 45);
GuiBuilder gui = GuiBuilder.create(plugin, "§3仓库搜索 §8[1/" + totalPages + "]", 54);
// 显示搜索结果
for (int i = 0; i < Math.min(results.size(), 45); i++) {
WarehouseItem item = results.get(i);
String name = highlighter.highlight("§b" + item.getDisplayName(), context);
String id = highlighter.highlight("§7ID: §f" + item.getItemId(), context);
gui.item(i, ItemBuilder.of(item.getIcon())
.name(name)
.lore(id, "§7数量: §f" + item.getAmount())
.build(),
viewer -> takeItem(viewer, item));
}
// 返回按钮
gui.item(49, ItemBuilder.of(Material.BARRIER)
.name("§c返回")
.build(),
viewer -> openMainMenu(viewer));
gui.open(player);
}
}示例 2:商店价格设置
java
public class HNShopPriceEditor {
private final HNShop plugin;
private final MultiChannelInputSessionManager sessionManager;
public HNShopPriceEditor(HNShop plugin) {
this.plugin = plugin;
this.sessionManager = new MultiChannelInputSessionManager(plugin);
plugin.getServer().getPluginManager().registerEvents(sessionManager, plugin);
}
public void promptPrice(Player player, ShopItem item) {
AnvilInputSession<ShopItem> session = AnvilInputSession
.builder(player.getUniqueId(), item)
.initialText(String.valueOf(item.getPrice()))
.promptMessage("§a请输入商品价格:")
.onInput((p, input) -> {
try {
double price = Double.parseDouble(input);
if (price <= 0 || price > 1000000) {
p.sendMessage("§c价格必须在 0.01 - 1000000 之间");
openEditMenu(p, item);
return;
}
item.setPrice(price);
plugin.getShopService().saveItem(item);
p.sendMessage("§a价格已更新为 §f" + price);
openEditMenu(p, item);
} catch (NumberFormatException e) {
p.sendMessage("§c请输入有效的数字");
openEditMenu(p, item);
}
})
.onCancel(p -> openEditMenu(p, item))
.timeout(60000)
.build();
sessionManager.beginSession(player, session);
}
}示例 3:拍卖竞拍
java
public class HNAuctionBidding {
private final HNAuction plugin;
private final MultiChannelInputSessionManager sessionManager;
public HNAuctionBidding(HNAuction plugin) {
this.plugin = plugin;
this.sessionManager = new MultiChannelInputSessionManager(plugin);
plugin.getServer().getPluginManager().registerEvents(sessionManager, plugin);
}
public void promptBidPrice(Player player, Auction auction) {
double minBid = auction.getCurrentPrice() + auction.getMinIncrement();
AnvilInputSession<Auction> session = AnvilInputSession
.builder(player.getUniqueId(), auction)
.initialText(String.valueOf(minBid))
.promptMessage("§a请输入竞拍价格(最低 " + minBid + "):")
.onInput((p, input) -> {
try {
double price = Double.parseDouble(input);
// 验证价格
if (price < minBid) {
p.sendMessage("§c出价必须至少为 " + minBid);
openAuctionDetail(p, auction);
return;
}
// 验证余额
if (plugin.getEconomy().getBalance(p) < price) {
p.sendMessage("§c余额不足");
openAuctionDetail(p, auction);
return;
}
// 出价
auction.placeBid(p, price);
p.sendMessage("§a出价成功:" + price);
openAuctionDetail(p, auction);
} catch (NumberFormatException e) {
p.sendMessage("§c请输入有效的数字");
openAuctionDetail(p, auction);
}
})
.onCancel(p -> openAuctionDetail(p, auction))
.timeout(60000)
.build();
sessionManager.beginSession(player, session);
}
}四、最佳实践
1. 搜索框架
✅ 推荐做法:
- 缓存搜索引擎实例(单例)
- 使用配置文件管理别名
- 提供内置默认别名作为回退
- 限制搜索结果数量(分页)
❌ 避免:
- 每次搜索都创建新的搜索引擎
- 在主线程执行大数据集搜索(使用异步)
- 忘记高亮显示搜索词
2. 输入会话框架
✅ 推荐做法:
- 使用
MultiChannelInputSessionManager支持多种输入方式 - 铁砧输入用于短文本(标题、价格、名称)
- 聊天输入用于长文本(内容、描述)
- 设置合理的超时时间(30-60 秒)
- 提供清晰的提示消息
- 使用验证器确保输入有效
❌ 避免:
- 重复创建会话管理器
- 忘记注册事件监听器
- 铁砧输入用于长文本(有长度限制)
- 没有提供取消机制
3. 性能优化
java
// ✅ 异步搜索大数据集
TaskUtil.runAsync(() -> {
SearchContext context = searchEngine.createContext(keyword);
List<Item> results = searchEngine.search(allItems, context);
TaskUtil.runSync(() -> {
openSearchGui(player, results, context);
});
});
// ✅ 缓存搜索引擎
private static SearchEngine<Item> searchEngine;
// ✅ 限制结果数量
List<Item> results = searchEngine.search(allItems, context);
List<Item> limited = results.subList(0, Math.min(results.size(), 100));五、迁移指南
从自定义搜索迁移
旧代码:
java
List<Item> results = new ArrayList<>();
for (Item item : allItems) {
if (item.getName().contains(keyword)) {
results.add(item);
}
}新代码:
java
SearchContext context = searchEngine.createContext(keyword);
List<Item> results = searchEngine.search(allItems, context);从自定义输入迁移
旧代码:
java
@EventHandler
public void onChat(AsyncChatEvent event) {
if (pendingInputs.containsKey(event.getPlayer().getUniqueId())) {
event.setCancelled(true);
String input = PlainTextComponentSerializer.plainText().serialize(event.message());
// 处理输入...
}
}新代码:
java
SimpleChatInputSession session = SimpleChatInputSession
.builder(player.getUniqueId(), context)
.onInput((p, input) -> { /* 处理输入 */ })
.build();
sessionManager.beginSession(player, session);六、常见问题
Q: 搜索框架支持模糊匹配吗?
A: 是的,默认支持三种匹配:
- 完全匹配(exact)- 最高分
- 前缀匹配(prefix)- 中等分
- 包含匹配(contains)- 较低分
Q: 如何支持拼音搜索?
A: 在别名配置中添加拼音映射:
yaml
groups:
- ["钻石", "diamond", "zuanshi", "zs"]Q: 铁砧输入有长度限制吗?
A: 是的,约 35 个字符。超过此长度请使用聊天输入。
Q: 告示牌输入如何实现?
A: 当前为占位实现,需要下游插件自行完善。参考 SignInputSession 的注释。
Q: 如何检查玩家是否有活跃会话?
A: 使用 sessionManager.hasActiveSession(playerId)
Q: 会话超时后会发生什么?
A: 会自动调用 cancel() 回调并清理会话。
七、相关文档
- 搜索框架详细指南:
SEARCH_FRAMEWORK_GUIDE.md - 聊天输入会话指南:
CHAT_INPUT_SESSION_GUIDE.md - 多渠道输入指南:
MULTI_CHANNEL_INPUT_GUIDE.md - API 2.2.0 总结:
API_2.2.0_SUMMARY.md
八、获取帮助
如果遇到问题:
- 查看相关文档
- 参考工具箱的实现(
HNCoreToolboxService) - 查看 HNMail 的实现(铁砧输入示例)
- 联系 HNCore 开发团队
祝开发顺利! 🚀
