元素配方索引机制
基本信息
| 属性 | 值 |
|---|---|
| 类型 | 特殊机制(NEI 查询索引) |
| 核心类 | ItemsContainingAspectHandler、TemplateThaumHandler.CachedThaumRecipe |
| 进度状态类 | com.gtnewhorizons.aspectrecipeindex.client.ThaumcraftHooks |
| 触发时机 | 惰性查询时(无预建索引、无加载期缓存表) |
结论先行:它没有维护一张"配方索引表"
尽管模组名叫 “Aspect Recipe Index”,源码里不存在任何长期存活的 Map<Aspect, List<Recipe>> 之类的结构。实际机制是每次查询即时线性扫描 + 惰性分页:
- 正向(元素 → 配方):
ItemsContainingAspectHandler遍历玩家自己的扫描记录算出含该元素的物品。 - 反向(元素 → 配方/用途):其余 6 个 handler 每次都把
ThaumcraftApi.getCraftingRecipes()全量遍历一遍,按配方类型和元素集合匹配。 - 元素物品的"命中判定"发生在
CachedThaumRecipe.contains/containsWithNBT里,走的是内存中的AspectList,不是索引表。
下面分层说明。
第 1 层:数据源是玩家的扫描记录
ItemsContainingAspectHandler.findContainingItemStacks(Aspect) 的唯一数据源:
Thaumcraft.proxy.getScannedObjects().get(Util.getUsername())— 玩家研究存档里的已扫描对象列表,List<String>。- 每个字符串是一个物品 hash 缓存键,首字符是前缀:
@与#两种。源码注释举例@12921929129。 - 若列表为
null(尚未初始化)直接返回空。
这意味着结果按玩家不同:别人的扫描记录不会被索引进来。服务器无法预生成一份全局索引,因为内容依赖玩家进度。
第 2 层:逐条 hash → ItemStack → AspectList
findContainingItemStacks 的转换过程:
itemStackCache.substring(1)去掉前缀字符。Integer.parseInt转成 hash;解析失败(NumberFormatException)静默跳过。GuiResearchRecipe.getFromCache(hash)反查ItemStack;null跳过。ThaumcraftCraftingManager.getObjectTags(stack)取物品自身元素。- 再过
ThaumcraftCraftingManager.getBonusTags(stack, tags)叠加罐装 / 瓶装等容器附带的 essentia。 stackSize = tags.getAmount(aspect);小于等于 0 则跳过(不排除该物品,只是不显示这一格)。stacks.sort(...)按stackSize降序排列——所以 UI 顶部总是"该元素含量最高"的物品。
复杂度上界是"该玩家扫描过的物品数",每次查询都重算一次。
第 3 层:36 格分页 + 链表
AspectCachedRecipe 是内部类常量 STACKS_COUNT = 36 的分页单元:
- 构造函数接收
start偏移量,切片区间为[start, start + 36)(getItemsInInterval)。 - 用一个
next字段串成单向链表:initStackList在start + 36 < list.size()时惰性创建下一页。 - 每一页都是独立的
CachedRecipe,getResult()返回同一个元素物品(放在OUTPUT_X, 5),所以 NEI 的"配方翻页"直接生效。 - 网格布局 9 列 × 4 行:
x = 起始X + 1 + (i % 9) * 18,y = 起始Y + 1 + (i / 9) * 18(源码注释标明这是"像原版那样留一点上边距")。 - 背景板
aspectrecipeindex:textures/gui/itemstack_background.png,尺寸 163×74;OUTPUT_X = 75,所以STACKS_OVERLAY_START_X落在 1 附近,START_Y = 116 - 74 = 42。 - 每一页都挂同一个前置研究
ResearchCategories.getResearch("ASPECTS"),且恒定标记为已完成(true)。
第 4 层:元素可见性过滤
在生成任何分页之前先过 Util.shouldShowAspect(aspect):
- 叠加列表加载时,对
Aspect.aspects.values()里每个元素逐一过滤,未发现的直接continue。 - 单个元素物品查询时,先
ItemAspect.getAspect(ingredient)再过滤,且结果为空时不生成任何页面。
判定式:showUndiscoveredAspectRecipes || hasDiscoveredAspect(当前用户名, aspect)。
第 5 层:加载进度与自动刷新
Thaumcraft 在客户端有一个后台 MappingThread 在重建物品 hash 映射。ARI 用一个 LATE mixin 挂进去,把进度暴露成一个静态三元组 ThaumcraftHooks:
| 字段 | 含义 | 写入方 |
|---|---|---|
totalToLoad |
待加载条目数 = MappingThread.idMappings.size() |
mixin 在 run() 开头 |
itemsLoaded |
已迭代条数 | mixin 在 Iterator.next() 处自增 |
allDataLoaded |
是否跑完 | mixin 在 run() 结尾置 true |
ThaumcraftHooks 里标 @ApiStatus.Internal 的三个 setter 是给 mixin 用的,不是对外 API;对外只有两个 getter 加一个 isDataLoaded()。
刷新策略在 ItemsContainingAspectHandler.onUpdate():
isDataLoaded()为真 → 直接返回,完全不做事。- 未完成时,每 200 tick(10 秒)只重算第一个缓存配方(
arecipes.get(0))的元素列表,然后initStackList重建它自己及后续全部分页。 - 界面上
drawBackground绘制Still loading... (已加载/总数),取自 lang 键aspectrecipeindex.items_containing_aspect.still_load。
所以在 MappingThread 跑完之前,界面上看到的内容是边加载边变的,且只刷新第一页的来源列表。
第 6 层:其余 handler 的"伪索引"
另外 6 个 recipe handler 没有分页结构,它们是纯线性扫描:
| Handler | 扫描对象 | 元素匹配方式 |
|---|---|---|
| Alchemy | ThaumcraftApi.getCraftingRecipes() 里所有 CrucibleRecipe |
recipe.catalystMatches(ingredient) 或 recipe.aspects 与 Util.getEssentiaFromItem(ingredient) 交集 |
| Infusion | 所有 InfusionRecipe(先经 TC4RecipeLib 转换) |
中心物品匹配 / 外围 RecipeIngredient 匹配 / essentia 交集 |
| Shaped Arcane | 所有 ShapedArcaneRecipe |
匿名子类覆写 isValid() 加 contains(ingredients, ingredient) 与研究判定 |
| Shapeless Arcane | 所有 ShapelessArcaneRecipe |
同上,改用 containsWithNBT |
| Wand | 杖柄 × 杖帽笛卡尔积,或 GTNH-TC-Wands 的 wrapper 列表 | OreDictionary.itemMatches 匹配组件;元素物品走"原始元素"分支 |
| Aspect Combination | Aspect.getCompoundAspects() |
ArrayUtils.contains(components, aspect) |
共用的两个技巧(在 TemplateThaumHandler.CachedThaumRecipe 上):
contains/containsWithNBT对ItemAspect走特判:直接查内存里的aspects.aspects.containsKey(aspect),不走普通物品匹配。这就是"拿元素物品当搜索词"能命中的原因。setIngredientPermutation对ItemAspect直接return——元素物品永远不会被当成真实材料摆进合成格。
相关条目
- 配置项 -
showUndiscoveredAspectRecipes/showLockedRecipes在这里的门控点 - Items Containing Aspect Handler - 本页描述的索引消费者
- Mixin 与 LATE 注入 - 进度三元组的数据来源
- Aspect 物品 - 索引的查询键