物品完成追踪

event/ItemTrackHandler.java。这是 TaskNH 唯一的自动完成机制:任务的 trackItem 出现在某个队员的主物品栏里,任务就自动完成。配置项见 服务端配置,任务字段见 任务数据模型。

注册位置

ItemTrackHandler 同时注册在 FML 总线和 Forge 总线上,源码注释说明原因:EntityJoinWorldEvent 属于 Forge 总线,而 ServerTickEvent 属于 FML 总线。

CommonProxy.init():

FMLCommonHandler.instance().bus().register(itemTrackHandler);
MinecraftForge.EVENT_BUS.register(itemTrackHandler);

触发方式:监听器 + 延迟到下一 tick 检查

设计上不在物品变化时立刻扫描,而是先标记再统一检查(类注释):

  1. InventoryListener(私有静态内部类,实现 ICrafting)挂在 player.inventoryContainer 上,作为容器的 crafting 监听器。
  2. 容器内容变化时回调 sendContainerAndContentsToPlayer 或 sendSlotContents,把玩家加进 pending 集合。
  3. 下一个服务端 tick(TickEvent.ServerTickEvent,phase == START)统一 checkPlayer()。

因此物品栏静止时不做任何扫描。

sendSlotContents 只认主物品栏:if (slot >= 9 && slot <= 44) schedule(player);——跳过合成格与护甲槽。

监听器的挂载时机在 onEntityJoinWorld(EntityJoinWorldEvent):登录、重生、换维度都会走这里。源码注释解释了一个坑:换维度会复用同一个玩家对象和容器,监听器可能已经挂过了,因此记住已挂载的容器(listener.attachedTo)并比对,而不是重复 addCraftingToCrafters;且 Container.removeCraftingFromCrafters 是客户端专用方法,所以改用「记住容器」的方式。挂载后立刻 schedule(player) 做一次检查——源码注释:物品可能是在离线期间或另一个维度里拿到的。

玩家退出时 ItemTrackHandler.forget(player) 移除监听器与待检查项,由 PlayerLogoutHandler 调用。

匹配规则

countItem(ItemStack[] mainInventory, ItemStack trackItem, String ore) 是公开静态方法,HUD 在客户端也调它,源码注释:这样 HUD 显示的数量与服务端检查的数量是同一个数。

它只遍历 player.inventory.mainInventory——不含护甲、不含 Curios 之类模组的额外栏位。

两种匹配方式,由 ore 是否为空串决定:

  • ore 非空 → 走 hasOre(),遍历 OreDictionary.getOreIDs(stack),只要有一个 ID 的名字等于 ore 就算命中。
    • 源码注释说明为什么比较名字而不是 ID:OreDictionary.getOreID 会把它不认识的任何名字注册进去,所以对一个没人加过的标签去查询,反而会凭空造出一个空标签。
  • ore 为空 → 走 matches(),要求 candidate.getItem() == tracked.getItem() 且 candidate.getItemDamage() == tracked.getItemDamage() 且 candidate.getUnlocalizedName().equals(trackedName)。

matches() 为什么比名字:一个物品承载多种不同东西时,模组会覆写 unlocalized name,于是 CropsNH 不同植物的种子会分开计数,而它们各自携带的生长值被忽略;尚未被任何玩家分析过的种子没有植物名,因此在有人扫描之前,它不会满足「指定某一种植物」的任务。源码注释明确写了这个行为。

trackedName 在循环外读一次(trackItem.getUnlocalizedName()),源码注释:读名字要付一次种子物品的注册表查询开销。

判定与单向性

对每个未 DONE 的任务(if (task.status == TaskStatus.DONE) continue;):

清单项:

  • 跳过 checked 为真或 trackItem 为空的项——源码注释:已勾选的项不再被查看,因此每项只触发一次,之后花掉物品也保持勾选
  • 条件是 countItem(...) < item.trackItemCount 则跳过
  • 命中则置 item.checked = true,并在 announceAutoComplete 开启时给该玩家发 tasknh.chat.auto_check

任务本体:

  • 完成条件为 trackItem != null && countItem(mainInventory, trackItem, "") >= Math.max(1, trackItemCount)(任务级永远用空 ore,即精确匹配)
  • 或 anyChecked && task.shouldCompleteOnChecklist()——即本轮有勾选,且任务勾上了「清单全勾自动完成」

完成是单向的:类注释明确写「Completion is one-way: spending the item afterwards does not reopen the task or uncheck the item.」任务一旦 DONE 就被 continue 跳过,勾选项也不再复查。

Math.max(1, trackItemCount) 的地板值:与 HudRenderer 用的地板值一致,源码注释「Same floor as the server check, which treats a count below one as one.」

完成后的动作

  • task.status = TaskStatus.DONE; 然后 data.moveToEnd(...)——已完成任务在 DONE 分组内排到末尾
  • data.updateTask(...)
  • 若完成且 announceAutoComplete:遍历所有在线玩家,凡是 team.getMembers() 含其 UUID 的都发 tasknh.chat.auto_complete(消息里带触发者用户名)
  • 本轮有任何改动(完成或有勾选)时,最后统一发一次 SyncAllTasksPacket 给全队

总开关

onServerTick 里 if (!TaskNHConfig.itemTrackingEnabled) return;——该配置关闭时,pending 集合已在前面被清空,检查整个跳过。因此关闭后重新打开不会补做之前的检查。