物品完成追踪
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 检查
设计上不在物品变化时立刻扫描,而是先标记再统一检查(类注释):
InventoryListener(私有静态内部类,实现ICrafting)挂在player.inventoryContainer上,作为容器的 crafting 监听器。- 容器内容变化时回调
sendContainerAndContentsToPlayer或sendSlotContents,把玩家加进pending集合。 - 下一个服务端 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会把它不认识的任何名字注册进去,所以对一个没人加过的标签去查询,反而会凭空造出一个空标签。
- 源码注释说明为什么比较名字而不是 ID:
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 集合已在前面被清空,检查整个跳过。因此关闭后重新打开不会补做之前的检查。