物品搜索服务
基本信息
| 属性 | 值 |
|---|---|
| 服务端基类 | 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 算法:
- 若
FindIt.isGregTechLoaded() && !searchInGTPipes && tileEntity instanceof BaseMetaPipeEntity→ 返回 false(GT 管道被禁用); - 若
FindIt.isEnderIOLoaded() && !searchConduits && tileEntity instanceof TileConduitBundle→ 返回 false(EnderIO 导管被禁用); - 遍历
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 构造器
- 若
FindIt.isExtraUtilitiesLoaded()为 false:codechicken.nei.api.API.addHashBind("gui.findit.find_item", Keyboard.KEY_T)注册热键; - 实例化
SlotHighlighter、EntityHighlighter; GuiContainerManager.addDrawHandler(slotHighlighter)与addInputHandler(new ItemFindInputHandler());MinecraftForge.EVENT_BUS.register(new NEIEventListener())处理 ExtraUtilities 的 NEI ping 抢占;MinecraftForge.EVENT_BUS.register(new WorldRenderListener())渲染地面 EntityItem 描边;FMLCommonHandler.instance().bus().register(new TickListener())在ClientTickEvent.Phase.END阶段重算 GUI 槽位高亮集合。
handleResponse(player, response)
slotHighlighter.highlightSlots(null, new HashSet<>(), 0xFFFF8726)重置旧高亮;this.foundItem = response.getFoundStack() != null ? new FindItemRequest(response.getFoundStack()) : null;this.expirationTime = System.currentTimeMillis() + FindItConfig.ITEM_HIGHLIGHTING_DURATION * 1000L;- 若
foundItem == null || !FindItConfig.SEARCH_ITEMS_ON_GROUND提前返回(不做地面搜索); - 构造
AxisAlignedBB.expand(SEARCH_RADIUS, ..., ...)用player.worldObj.getEntitiesWithinAABB(EntityItem.class, searchBox)取附近地面物品; - 对每个
EntityItem调foundItem.isStackSatisfies(player, itemEntity.getEntityItem()),命中 ID 加入foundItemEntities列表(上限MAX_RESPONSE_SIZE); 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 自家盖板有效。
相关条目
- 搜索附近库存物品 热键 - 触发本服务的客户端入口
- FindItConfig - 所有可调开关
- 搜索冷却服务 - 服务端每玩家 tick 冷却
- 方块搜索服务 - 配对服务,按 Block+Meta 匹配