Structure Layout Optimizer

Structure Layout Optimizer

尝试优化拼图结构和NBT片段的生成

结构

尽可能优化通用 Jigsaw 结构生成。本质上,这个模组的目标是完全不改变结构的外观。它只是尝试让结构生成得更快、更高效。仅此而已。

以下是所进行优化的更技术性细节。如果你在整合包中运行此模组时发现任何冲突或问题,请报告!

  • 用 BoxOctree 替换 VertexShape 未优化的调用,使各个部件只检查附近部件是否相交,而不是检查整个结构。

默认情况下,原版使用 VoxelShape 来存储它在组装结构布局时所使用的各个部件的边界。当要添加新部件时,它需要查看该部件是否能放入布局中而不与其他任何部件相交。

问题在于 VoxelShape 并不适合此用途,因为在检查一个 VoxelShape 是否与另一个 VoxelShape 相交时,会比较所有的顶点。在拥有大量部件的 Jigsaw 结构中,随着添加到布局中的有效部件越来越多,这种相交检查会大幅变慢。在为递归 Jigsaw 结构生成大量部件时,可能会导致卡顿峰值。

这里的优化用一个内部持有 BoxOctree 实现的虚拟 VoxelShape 替换普通的 VoxelShape,以便将 BoxOctree 传递到需要的地方。这个 BoxOctree 在检查相交时,只会检查与传入边界框附近的部件。因此,由于大多数部件距离太远、对检查无关紧要,它们会被忽略,从而防止相交检查时间失控增长。

  • 检查一个 Jigsaw Block 是否被完全阻挡,以得知何时跳过检查子刚性部件。

在原版中,父部件中的每一个 Jigsaw Block 都会检查子部件中的每一个 Jigsaw Block,以得知何时尝试连接这两个部件。问题在于,如果父部件的 Jigsaw Block 朝向结构边界边缘,或者朝向另一个独立的部件,那么这个 Jigsaw Block 就永远没有空间生成刚性结构部件。因此,我们可以告诉游戏跳过检查这个刚性子部件,转而检查下一个部件。这让我们免于进行大量昂贵的 Jigsaw Block 匹配检查。尤其是在拥有巨量 Jigsaw Block 的结构中。

  • (26.2 及以下)用稍微更优化的版本替换 Jigsaw 目标/朝向匹配。

将获取方块属性的数量减少了一半。原版对每个 JigsawBlock 调用两次 getValue,以从同一个属性获取顶部和正面值,而实际上只需获取一次就能得到它需要的所有值。还稍微提高了从 Jigsaw Block 的 NBT 中获取 joints、targets 和 name 字符串值的速度。同时简化了 joint 数据检查,使其不需要通过 byName(性能不佳)转换为 Enum,并重新排序检查以减少经常需要运行的逻辑量。

这将有助于那些拥有极大量 Jigsaw Block 的 Jigsaw 结构,因为每个 Jigsaw Block 都会针对其他结构部件中的所有 Jigsaw Block 运行这段匹配代码。

  • 使任何没有 finalizeProcessing StructureProcessor 的巨型结构 NBT 现在加载得快得多。

当一个 NBT 结构部件要在区块中生成时,整个 NBT 部件会被加载到内存中,然后对所有的位置多次迭代,以便 StructureProcessor 应用,随后又忽略当前正在生成的区块之外的所有位置。这不太高效(实际上非常浪费),并会导致巨型 NBT 文件加载时间很长。

此模组的优化方式是提前进行边界检查,在将数据传递给 StructureProcessor 之前剔除所有我们不需要的位置。然而,任何重写 finalizeProcessing 方法的 StructureProcessor 都可能需要所有 NBT 位置才能正常工作,因此对于带有这类 StructureProcessor 的部件,此优化会被禁用。这类结构非常少。在原版中,只有 Trail Ruins 不会获得此优化,因为它使用了重写 finalizeProcessing 方法的 Capped StructureProcessor。

  • (1.21.1+)用稍微更快的版本替换 SinglePoolElement 中的 Jigsaw Block 列表打乱和优先级排序逻辑。

主要好处是通过使用一种新方法从 NBT 中获取 selection_priority 数据稍微更快,该方法只获取一次条目而不是两次。原版会获取两次,先检查数据类型,然后再返回值。有点奇怪而且浪费。

还尝试了一种新的优先级排序系统,但只对那些实际使用 selection_priority 的少数结构有帮助,例如 Trial Chambers。总体而言,这可能是最弱的优化,但对于拥有极其荒谬数量的 Jigsaw Block 的 Jigsaw 结构,它确实有很大帮助。因此值得保留此优化。

可能会破坏与原版种子在 Jigsaw Block 运行顺序方面的等价性,但我目前还未找到相关证据。

  • 跳过我们已经检查过、并且已知无法在当前地点生成的 SinglePoolElement 的运行逻辑。

这个优化源于这样一个事实:原版 StructureTemplatePool 持有一个包含所有 SinglePoolElement 的列表,并根据其权重值复制元素。因此,如果在 Template Pool 中为一座房子指定权重 100,那么这座房子会被放入该列表 100 次!而在生成布局时,游戏会复制这个列表,将其打乱,然后遍历该列表,尝试生成它找到的第一个适合的部件。这意味着,即使之前已经发现某个重复条目在该地点不适合,它也会重新运行检查逻辑。我所做的优化是跳过我们已经检查过的部件逻辑,并继续处理列表中的下一个部件。这为那些在 Template Pool 中使用大量高权重值的结构带来了不错的性能提升。

此外,你可以开启 deduplicateShuffledTemplatePoolElementList 配置选项,从 Template Pool 中的高权重元素获得更多性能。问题在于,这个配置的代价是会改变结构的布局。布局仍然有效且良好。只是会与关闭该配置选项时不同。基本上,它会专门为了获得额外性能提升而破坏结构布局的种子等价性。

*需要说明的是,种子等价性是指每次你使用种子 777,并且在某个地点有一个带有两座铁匠房的村庄,那么每次你使用同一个种子时,那个村庄都会保持在那个地点,并带有两座铁匠房。开启这个优化配置并使用相同种子时,那个村庄仍然会在相同地点。但它这次可能只有 1 座铁匠房。并且每次你使用相同种子且开启此配置时,该村庄都会继续生成 1 座铁匠房。布局是种子稳定的,但不再与关闭配置时相同。

  • (1.21.4 与 v1.1.0+):将 StructureTemplate$Palette 的 StructureBlockInfo 对象列表替换为 StructureBlockInfo 对象的 palette,以减少在未生成该缓存 StructureTemplate 时的内存占用。

此优化是基于 contaria 为 1.16.1 制作的 Glacier 模组修改而来。特别感谢他们允许我使用他们的代码作为此优化的基础!

本质上,在原版中,每当加载一个结构 nbt 文件时,它会被转换为一个 StructureTemplate 对象并缓存。埋藏在该对象中的是一个包含该 nbt 文件中所有位置的列表,并配对有每个位置的方块和 nbt 标签。这是 StructureTemplate 对象最大的内存占用来源。这个模组会在底层将这个列表替换为一个特殊 palette 数据结构对象,它假装自己是一个列表。该 palette 的内存大小显著小于列表!然后在世界生成查询时,它会再次构造该列表,作为 WeakReference,使世界生成保持快速并正常工作。当世界生成完成后,这个列表会被垃圾回收以再次释放内存,使每个 StructureTemplate 只在世界生成时使用其所需的完整内存量,同时在长期缓存中保持较小的形态。缺点是,这个列表不能也不应该通过 add、remove、clear 调用进行修改,如果这样做会抛出异常。幸运的是,我认为没有任何模组会直接在 StructureTemplate 上修改这个列表,但如果有,请告诉我。

ModernFix 确实有一个 mixin,会使 StructureTemplate 对象本身变为 SoftReference,并在游戏内存占满时将它们从内存中完全移除。这是一个有效的解决方案,旨在阻止长期游玩中加载结构 nbt 到永不清除的缓存所导致的内存泄漏。其缺点是,如果该部件已从内存中清除,则必须再次直接从文件加载。这个 mixin 会与我模组的优化一起正常工作。可能会发生的情况是,我的模组使 StructureTemplate 在内存中更小,从而允许 ModernFix 在内存中保留更多 StructureTemplate,而不会将它们完全从内存中移除。理论上,这应该允许长期获得更多缓存命中。