Skip to content

搜索与多渠道输入框架

概述

这页专门讲 HNCore 提供的两套偏“业务能力型”的开发框架:

  1. 通用搜索框架 - 支持任意类型的对象搜索、评分、高亮
  2. 多渠道输入会话框架 - 支持聊天、铁砧、告示牌三种输入方式(告示牌输入在 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");

二、多渠道输入会话框架

适用场景

场景推荐渠道原因
输入标题/名称ANVILGUI 内输入,体验流畅
输入价格/数量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

八、获取帮助

如果遇到问题:

  1. 查看相关文档
  2. 参考工具箱的实现(HNCoreToolboxService
  3. 查看 HNMail 的实现(铁砧输入示例)
  4. 联系 HNCore 开发团队

祝开发顺利! 🚀

HN 系列插件文档