物品搜索服务

基本信息

属性 值
服务端基类 com.gtnh.findit.service.itemfinder.ItemFindService
客户端类 com.gtnh.findit.service.itemfinder.ClientItemFindService(继承 ItemFindService)
主类注册 FindIt.preInit 中 event.getSide() == Side.CLIENT ? new ClientItemFindService() : new ItemFindService()
触发入口 搜索附近库存物品 热键
插件系统 FindIt.pluginsList: List<IStackFilterProvider>(共 8 个默认 provider)
网络通道 FindItNetwork.CHANNEL
消息流 FindItemRequest(C→S)→ BlockFoundResponse + ItemFoundResponse(S→C)

功能

FindIt 的服务端物品搜索实现:在 ±FindItConfig.SEARCH_RADIUS 立方体内扫描所有 TileEntity,逐个 TileEntity 调用 FindItemRequest.isTileSatisfies(player, tileEntity),后者遍历 FindIt.pluginsList 中每个 IStackFilterProvider 的 getFilter(player, tileEntity) 返回 IStackFilter,再 filter.matches(request)。任一 provider 匹配即视为命中。

关键类层次

IStackFilter 体系

com.gtnh.findit.IStackFilter 是 @FunctionalInterface:

boolean matches(FindItemRequest request);

三种内置实现:

类 用途 关键方法
IStackFilter.InventoryStackFilter IInventory 逐槽位检查 for slot<10:request.isStackSatisfies(player, slotItem) 任一为 true 即命中
IStackFilter.FluidStackFilter List<FluidStack> 流体匹配 for fluid in fluids:request.isFluidSatisfies(fluid) 任一为 true 即命中
IStackFilter.AnyMultiItemFilter 多过滤器 OR 组合 for filter in filters:filter.matches(request) 任一为 true 即命中

IStackFilterProvider 插件接口

IStackFilter getFilter(EntityPlayer player, TileEntity tileEntity);
IStackFilter getFilter(EntityPlayer player, ItemStack stack);

返回 null 表示该 provider 对当前目标不参与匹配。返回 IStackFilter 后由 IStackFilterProvider.matches(request) 在 FindItemRequest.isTileSatisfies / isStackSatisfies 中遍历插件列表时调用。

FindItemRequest(C→S)

字段 类型 序列化
targetStack ItemStack ProtoUtils.writeItemStack/readItemStack(PacketBuffer.writeItemStackToBuffer)
targetFluidStack FluidStack 不序列化;fromBytes 时通过 StackInfo.getFluid(targetStack) 重新计算(NEI 的"流体罐"映射)
highlightingEmptyItemStacks boolean buf.writeBoolean(...)
searchInGTPipes boolean buf.writeBoolean(...)
searchConduits boolean buf.writeBoolean(...)

isTileSatisfies 算法:

  1. 若 FindIt.isGregTechLoaded() && !searchInGTPipes && tileEntity instanceof BaseMetaPipeEntity → 返回 false(GT 管道被禁用);
  2. 若 FindIt.isEnderIOLoaded() && !searchConduits && tileEntity instanceof TileConduitBundle → 返回 false(EnderIO 导管被禁用);
  3. 遍历 FindIt.pluginsList,调用 provider.getFilter(player, tileEntity),非 null 且 filter.matches(this) 即返回 true。

isStackSatisfies(player, stack) 遍历 pluginsList,调用 provider.getFilter(player, stack),非 null 且 filter.matches(this) 即返回 true。当 !highlightingEmptyItemStacks && stack.stackSize == 0 时直接返回 false。

isFluidSatisfies(fluid):fluid == null 或 !highlightingEmptyItemStacks && amount == 0 返回 false;fluid.getFluid() == WATER && amount == 0 返回 false;targetFluidStack != null && targetFluidStack.isFluidEqual(fluid) 返回 true。

ItemFoundResponse(S→C)

仅 ItemStack foundStack,ProtoUtils.writeItemStack/readItemStack 序列化。

BlockFoundResponse(复用)

与服务端方块搜索共用,序列化 List<ChunkPosition>(详见 方块搜索服务)。

默认 provider 清单

顺序 Provider 触发条件 TileEntity 侧 ItemStack 侧
1 MinecraftProvider 始终 IInventory 槽位 + IFluidTank.getFluid() + IFluidHandler.getTankInfo(ForgeDirection.UNKNOWN) 流体 StackInfo.getFluid(stack) 转 FluidStack + StackInfo.equalItemAndNBT(request.getStackToFind(), stack, true)
2 ForestryStackFilterProvider Loader.isModLoaded("Forestry") null ItemBackpack → ItemInventoryBackpack(player, backpack.getBackpackSize(), stack) → InventoryStackFilter
3 AdventureBackpackProvider Loader.isModLoaded("adventurebackpack") IInventoryBackpack 槽位 + getTanksArray() 流体 ItemAdventureBackpack → InventoryBackpack(stack)
4 ProjectRedExplorationProvider `Loader.isModLoaded("ProjRed Exploration")` null
5 DraconicEvolutionProvider Loader.isModLoaded("DraconicEvolution") TilePlacedItem.getStack() 直接栈匹配 null
6 BackpackProvider Loader.isModLoaded("Backpack") null de.eydamos.backpack.item.ItemBackpack → ItemBackpack.getInventory(stack, player)
7 ThaumcraftProvider Loader.isModLoaded("Thaumcraft") IAspectContainer.getAspects() → AspectStackFilter IEssentiaContainerItem.getAspects(stack) → AspectStackFilter;额外兼容 aspectrecipeindex 与 thaumcraftneiplugin 的 ItemAspect
8 GregTechCoverProvider Loader.isModLoaded("gregtech") && !Loader.isModLoaded("gregapi") ICoverable.hasCoverAtSide + getCoverItemAtSide 比对 GT 盖板 null
9 GregTechCoverChestProvider 同上 ICoverable + CoverChest.getItems() 逐槽位 null

ThaumcraftProvider 内部 AspectStackFilter.matches 行为(IS_SIDE 推荐穷举):

来源物品 比较方式
IEssentiaContainerItem getAspects(stack) 返回的每个 Aspect 任一在 filterAspects 中 getAmount > 0 即命中
aspectrecipeindex 的 ItemAspect ItemAspect.getAspect(stack) 与 filterAspects 比对
thaumcraftneiplugin 的 ItemAspect ItemAspect.getAspects(stack) 取首项 Aspect 与 filterAspects 比对
其他 返回 false

GregTechCoverProvider.matches:

for side in VALID_DIRECTIONS:
    if !coverable.hasCoverAtSide(side): continue
    coverStack = coverable.getCoverItemAtSide(side)
    if GTUtility.areStacksEqual(stackToFind, coverStack, true): return true
return false

注:CoverRegistry.isCover(stackToFind) 先做类型检查,非 GT 盖板直接返回 false。

GregTechCoverChestProvider.matches:

for side in VALID_DIRECTIONS:
    if !coverable.hasCoverAtSide(side): continue
    cover = coverable.getCoverAtSide(side)
    if coverChestMatches(player, request, cover): return true
return false

coverChestMatches 仅在 cover 是 CoverChest 时进入:遍历 IItemHandler 的每个槽位,request.isStackSatisfies(player, slotItem) 任一为 true 即命中。

客户端实现

ClientItemFindService 构造器

  1. 若 FindIt.isExtraUtilitiesLoaded() 为 false:codechicken.nei.api.API.addHashBind("gui.findit.find_item", Keyboard.KEY_T) 注册热键;
  2. 实例化 SlotHighlighter、EntityHighlighter;
  3. GuiContainerManager.addDrawHandler(slotHighlighter) 与 addInputHandler(new ItemFindInputHandler());
  4. MinecraftForge.EVENT_BUS.register(new NEIEventListener()) 处理 ExtraUtilities 的 NEI ping 抢占;
  5. MinecraftForge.EVENT_BUS.register(new WorldRenderListener()) 渲染地面 EntityItem 描边;
  6. FMLCommonHandler.instance().bus().register(new TickListener()) 在 ClientTickEvent.Phase.END 阶段重算 GUI 槽位高亮集合。

handleResponse(player, response)

  1. slotHighlighter.highlightSlots(null, new HashSet<>(), 0xFFFF8726) 重置旧高亮;
  2. this.foundItem = response.getFoundStack() != null ? new FindItemRequest(response.getFoundStack()) : null;
  3. this.expirationTime = System.currentTimeMillis() + FindItConfig.ITEM_HIGHLIGHTING_DURATION * 1000L;
  4. 若 foundItem == null || !FindItConfig.SEARCH_ITEMS_ON_GROUND 提前返回(不做地面搜索);
  5. 构造 AxisAlignedBB.expand(SEARCH_RADIUS, ..., ...) 用 player.worldObj.getEntitiesWithinAABB(EntityItem.class, searchBox) 取附近地面物品;
  6. 对每个 EntityItem 调 foundItem.isStackSatisfies(player, itemEntity.getEntityItem()),命中 ID 加入 foundItemEntities 列表(上限 MAX_RESPONSE_SIZE);
  7. itemEntitiesHighlighter.highlightEntities(foundItemEntities, expirationTime)。

TickListener.onClientPostTick

  • 仅在 event.phase == TickEvent.Phase.END 处理;
  • Minecraft.getMinecraft().theWorld == null 直接返回;
  • 当前 currentScreen 不是 GuiContainer 直接返回;
  • System.currentTimeMillis() > expirationTime 时把 foundItem 置 null(防止无限高亮);
  • 遍历 gui.inventorySlots.inventorySlots,跳过 slot.inventory instanceof InventoryPlayer(玩家背包不标),命中槽位加到 highlightedSlots 集合(上限 256);
  • slotHighlighter.highlightSlots(gui, highlightedSlots, getItemHighlightingColor())。

ItemFindInputHandler

  • getKeyBindId() 返回 FindIt.isExtraUtilitiesLoaded() ? "gui.xu_ping" : "gui.findit.find_item";
  • findStack(stack):FindItNetwork.CHANNEL.sendToServer(new FindItemRequest(stack)) 并返回 true(截断事件传递给 NEI)。

NEIEventListener

当 FindIt.isExtraUtilitiesLoaded() 为 true 时,在 NEIConfigsLoadedEvent 中把 com.rwtema.extrautils.nei.ping.NEIPing 从 GuiContainerManager.inputHandlers 移除,避免 ExtraUtilities 的 ping 与 findit 同时响应 T 键。

数值

数值 默认 字段
搜索半径 16 FindItConfig.SEARCH_RADIUS
服务端冷却 10 tick FindItConfig.SEARCH_COOLDOWN
最大响应数 20(服务端 TileEntity + 客户端 EntityItem 共用) FindItConfig.MAX_RESPONSE_SIZE
物品高亮时长 10 秒 FindItConfig.ITEM_HIGHLIGHTING_DURATION
物品高亮颜色 0xFFFF8726(橙红色) FindItConfig.ITEM_HIGHLIGHTING_COLOR
单 tick GUI 高亮槽位上限 256 TickListener.onClientPostTick 硬编码
是否搜索地面 EntityItem true FindItConfig.SEARCH_ITEMS_ON_GROUND
是否搜索 GT 管道 false FindItConfig.SEARCH_IN_GT_PIPES
是否搜索 EnderIO 导管 false FindItConfig.SEARCH_IN_ENDERIO_CONDUITS
是否忽略 stackSize==0 true FindItConfig.ITEM_HIGHLIGHTING_EMPTY_ITEMSTACKS

渲染与工具类

SlotHighlighter

com.gtnh.findit.fx.SlotHighlighter implements IContainerDrawHandler:

  • 贴图:assets/findit/textures/gui/slot_highlight.png,UV (0,0)–(18/32, 18/32);
  • 颜色来自 FindItConfig.ITEM_HIGHLIGHTING_COLOR(ARGB 解析失败回退到 0xFFFF8726);
  • 仅在 renderSlotUnderlay 钩子里画,slot 在 slots 集合内才画;
  • GL11.glDisable(GL11.GL_LIGHTING)、GL11.glDisable(GL11.GL_ALPHA_TEST)、GL11.glShadeModel(GL11.GL_SMOOTH)、GL11.glEnable(GL11.GL_BLEND)、OpenGlHelper.glBlendFunc(770, 771, 1, 0)。

EntityHighlighter

com.gtnh.findit.fx.EntityHighlighter 在 RenderWorldLastEvent 渲染地面 EntityItem:

  • 颜色 GL11.glColor4f(0.75f, 0.0f, 1.0f, 1.0f) 紫色 RGB=192,0,255;
  • 线宽 GL11.glLineWidth(3),GL11.glDisable(GL11.GL_DEPTH_TEST)、GL11.glDisable(GL11.GL_TEXTURE_2D);
  • 闪烁:((time / 500) & 1) == 0 时不画;
  • EntityItem 时 scale=1.2f,并附加 hoverStart 的自旋(age / 20.0F)+ shouldBob() 上下浮动(MathHelper.sin(age / 10.0F + hoverStart) * 0.1F + 0.1F);
  • FxHelper.renderEntityOutline(entity, scale, event.partialTicks) 画 AABB 描边。

FxHelper

com.gtnh.findit.fx.FxHelper:

  • renderBlockOutline(pos):画 AABB [(pos.x, pos.y, pos.z) – (pos.x+1, pos.y+1, pos.z+1)];
  • renderBoxOutline(box):画 box 的 AABB;
  • renderEntityOutline(entity, scale, partialTicks):根据 lastTickPosX/Y/Z 与 posX/Y/Z 插值 + width/2/height 算 renderOutline(x-w, y, z-w, x+w, y+h, z+w),scale != 1f 时等比例缩放并下移;
  • renderOutline(x0,y0,z0,x1,y1,z1) 用 GL11.glBegin(GL11.GL_LINE_STRIP) + 16 个顶点画完整 12 边线框。

第三方 mod 集成

详细 provider 行为见 默认 provider 清单 章节。除表中所列 9 个 provider 外,ClientItemFindService 还与以下 mod 集成:

  • NotEnoughItems:注册 GUI.inputHandlers、API.addHashBind、绘制 IContainerDrawHandler、NEIConfigsLoadedEvent 钩子移除 ExtraUtilities NEI ping。
  • ExtraUtilities:仅客户端,修改 ItemFindInputHandler.getKeyBindId() 并移除 NEIPing 输入处理器(不增加 provider)。

限制

  • 服务端单次 WorldUtils.getTileEntitiesAround 同步遍历所有 TileEntity,SEARCH_RADIUS 增大性能随之恶化。
  • 命中数量受 MAX_RESPONSE_SIZE 截断,先到先得(遍历顺序与 chunk.chunkTileEntityMap.values() 一致);无距离排序。
  • ItemFindRequest.fromBytes 用 StackInfo.getFluid(targetStack) 重新计算 targetFluidStack,依赖 NEI 的"流体罐"映射,未安装/不识别则 targetFluidStack=null,isFluidSatisfies 永远返回 false。
  • AspectStackFilter 只支持"任意 Aspect 命中",不支持多 Aspect 交集/子集语义。
  • GregTechCoverProvider 要求目标 stack 是 CoverRegistry.isCover(stackToFind) 否则直接返回 false;只对 GT 自家盖板有效。

相关条目