diff --git a/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/adapter/vanilla/BuiltinNodeResolver.kt b/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/adapter/vanilla/BuiltinNodeResolver.kt index 00296ea..ec0506c 100644 --- a/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/adapter/vanilla/BuiltinNodeResolver.kt +++ b/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/adapter/vanilla/BuiltinNodeResolver.kt @@ -11,9 +11,24 @@ import net.minecraft.tags.TagKey import net.minecraft.world.item.ItemStack import net.minecraftforge.fluids.FluidStack import net.minecraftforge.registries.ForgeRegistries +import java.util.concurrent.ConcurrentHashMap @Suppress("unused") object BuiltinNodeResolver { + private val itemNodesByMod = ConcurrentHashMap>() + private val fluidNodesByMod = ConcurrentHashMap>() + @Volatile + private var itemNodeCacheSize = -1 + + @Volatile + private var fluidNodeCacheSize = -1 + + @Volatile + private var allItemNodesCache: List = emptyList() + + @Volatile + private var allFluidNodesCache: List = emptyList() + fun itemNode(stack: ItemStack): NodeKey? { if (stack.isEmpty) { return null @@ -61,20 +76,29 @@ object BuiltinNodeResolver { } .orElse(emptyList()) - fun allItemNodes(): List = - BuiltInRegistries.ITEM - .map { item -> MatterNodes.item(BuiltInRegistries.ITEM.getKey(item).toLite()) } + fun allItemNodes(): List { + refreshItemCacheIfNeeded() + return allItemNodesCache + } - fun allFluidNodes(): List = - BuiltInRegistries.FLUID - .filter { fluid -> fluid != net.minecraft.world.level.material.Fluids.EMPTY } - .map { fluid -> MatterNodes.fluid(BuiltInRegistries.FLUID.getKey(fluid).toLite()) } + fun allFluidNodes(): List { + refreshFluidCacheIfNeeded() + return allFluidNodesCache + } - fun itemNodesInMod(modId: String): List = - allItemNodes().filter { it.id.namespace == modId } + fun itemNodesInMod(modId: String): List { + refreshItemCacheIfNeeded() + return itemNodesByMod.computeIfAbsent(modId) { id -> + allItemNodesCache.filter { it.id.namespace == id } + } + } - fun fluidNodesInMod(modId: String): List = - allFluidNodes().filter { it.id.namespace == modId } + fun fluidNodesInMod(modId: String): List { + refreshFluidCacheIfNeeded() + return fluidNodesByMod.computeIfAbsent(modId) { id -> + allFluidNodesCache.filter { it.id.namespace == id } + } + } fun itemAmount(stack: ItemStack): NodeAmount? = itemNode(stack)?.let { NodeAmount(it, stack.count.coerceAtLeast(1).toLong()) } @@ -82,6 +106,34 @@ object BuiltinNodeResolver { fun fluidAmount(stack: FluidStack): NodeAmount? = fluidNode(stack)?.takeIf { stack.amount > 0 }?.let { NodeAmount(it, stack.amount.toLong()) } + @Synchronized + private fun refreshItemCacheIfNeeded() { + val size = BuiltInRegistries.ITEM.size() + if (size != itemNodeCacheSize) { + itemNodeCacheSize = size + allItemNodesCache = + BuiltInRegistries.ITEM.map { item -> + MatterNodes.item(BuiltInRegistries.ITEM.getKey(item).toLite()) + } + itemNodesByMod.clear() + } + } + + @Synchronized + private fun refreshFluidCacheIfNeeded() { + val size = BuiltInRegistries.FLUID.size() + if (size != fluidNodeCacheSize) { + fluidNodeCacheSize = size + allFluidNodesCache = + BuiltInRegistries.FLUID + .filter { fluid -> fluid != net.minecraft.world.level.material.Fluids.EMPTY } + .map { fluid -> + MatterNodes.fluid(BuiltInRegistries.FLUID.getKey(fluid).toLite()) + } + fluidNodesByMod.clear() + } + } + private fun ResourceLocation.toLite(): LiteResourceLocation = LiteResourceLocation.of(namespace, path) diff --git a/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/client/MatterIndexScreen.kt b/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/client/MatterIndexScreen.kt index a27fcd1..f7126fa 100644 --- a/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/client/MatterIndexScreen.kt +++ b/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/client/MatterIndexScreen.kt @@ -27,6 +27,8 @@ class MatterIndexScreen( private val focusedItemId: String? = null, ) : Screen(Component.literal("Replication Index")) { private val entries: List = loadEntries() + private val entryRecords: List = entries.map { it.record } + private val entriesById: Map = entries.associateBy { it.record.itemId } private val matterTypes: List = entries .flatMap { it.record.matterValues.keys } @@ -487,18 +489,16 @@ class MatterIndexScreen( private fun visibleEntries(): List { val query = MatterIndexFiltering.parseQuery(searchBox.value, replicatableOnly, disintegratableOnly) - val ordered = MatterIndexFiltering.filterAndSort(entries.map { it.record }, query, sort, ::displayAmount) - val byId = entries.associateBy { it.record.itemId } - return ordered.mapNotNull { byId[it.itemId] } + val ordered = MatterIndexFiltering.filterAndSort(entryRecords, query, sort, ::displayAmount) + return ordered.mapNotNull { entriesById[it.itemId] } } private fun selectedEntry(): MatterIndexEntry? = - entries.firstOrNull { it.record.itemId == selectedItemId } + selectedItemId?.let { entriesById[it] } private fun equivalentEntries(): List { - val records = MatterIndexFiltering.equivalentRecords(entries.map { it.record }, selectedEntry()?.record, ::displayAmount) - val byId = entries.associateBy { it.record.itemId } - return records.mapNotNull { byId[it.itemId] } + val records = MatterIndexFiltering.equivalentRecords(entryRecords, selectedEntry()?.record, ::displayAmount) + return records.mapNotNull { entriesById[it.itemId] } } private fun columns(layout: Layout): Columns { diff --git a/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/adapter/vanilla/BuiltinNodeResolver.kt b/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/adapter/vanilla/BuiltinNodeResolver.kt index 5fad1cf..d7ff00c 100644 --- a/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/adapter/vanilla/BuiltinNodeResolver.kt +++ b/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/adapter/vanilla/BuiltinNodeResolver.kt @@ -10,8 +10,23 @@ import net.minecraft.resources.ResourceLocation import net.minecraft.tags.TagKey import net.minecraft.world.item.ItemStack import net.neoforged.neoforge.fluids.FluidStack +import java.util.concurrent.ConcurrentHashMap object BuiltinNodeResolver { + private val itemNodesByMod = ConcurrentHashMap>() + private val fluidNodesByMod = ConcurrentHashMap>() + @Volatile + private var itemNodeCacheSize = -1 + + @Volatile + private var fluidNodeCacheSize = -1 + + @Volatile + private var allItemNodesCache: List = emptyList() + + @Volatile + private var allFluidNodesCache: List = emptyList() + fun itemNode(stack: ItemStack): NodeKey? { if (stack.isEmpty) { return null @@ -59,20 +74,29 @@ object BuiltinNodeResolver { } .orElse(emptyList()) - fun allItemNodes(): List = - BuiltInRegistries.ITEM - .map { item -> MatterNodes.item(BuiltInRegistries.ITEM.getKey(item).toLite()) } + fun allItemNodes(): List { + refreshItemCacheIfNeeded() + return allItemNodesCache + } - fun allFluidNodes(): List = - BuiltInRegistries.FLUID - .filter { fluid -> fluid != net.minecraft.world.level.material.Fluids.EMPTY } - .map { fluid -> MatterNodes.fluid(BuiltInRegistries.FLUID.getKey(fluid).toLite()) } + fun allFluidNodes(): List { + refreshFluidCacheIfNeeded() + return allFluidNodesCache + } - fun itemNodesInMod(modId: String): List = - allItemNodes().filter { it.id.namespace == modId } + fun itemNodesInMod(modId: String): List { + refreshItemCacheIfNeeded() + return itemNodesByMod.computeIfAbsent(modId) { id -> + allItemNodesCache.filter { it.id.namespace == id } + } + } - fun fluidNodesInMod(modId: String): List = - allFluidNodes().filter { it.id.namespace == modId } + fun fluidNodesInMod(modId: String): List { + refreshFluidCacheIfNeeded() + return fluidNodesByMod.computeIfAbsent(modId) { id -> + allFluidNodesCache.filter { it.id.namespace == id } + } + } fun itemAmount(stack: ItemStack): NodeAmount? = itemNode(stack)?.let { NodeAmount(it, stack.count.coerceAtLeast(1).toLong()) } @@ -80,6 +104,34 @@ object BuiltinNodeResolver { fun fluidAmount(stack: FluidStack): NodeAmount? = fluidNode(stack)?.takeIf { stack.amount > 0 }?.let { NodeAmount(it, stack.amount.toLong()) } + @Synchronized + private fun refreshItemCacheIfNeeded() { + val size = BuiltInRegistries.ITEM.size() + if (size != itemNodeCacheSize) { + itemNodeCacheSize = size + allItemNodesCache = + BuiltInRegistries.ITEM.map { item -> + MatterNodes.item(BuiltInRegistries.ITEM.getKey(item).toLite()) + } + itemNodesByMod.clear() + } + } + + @Synchronized + private fun refreshFluidCacheIfNeeded() { + val size = BuiltInRegistries.FLUID.size() + if (size != fluidNodeCacheSize) { + fluidNodeCacheSize = size + allFluidNodesCache = + BuiltInRegistries.FLUID + .filter { fluid -> fluid != net.minecraft.world.level.material.Fluids.EMPTY } + .map { fluid -> + MatterNodes.fluid(BuiltInRegistries.FLUID.getKey(fluid).toLite()) + } + fluidNodesByMod.clear() + } + } + private fun ResourceLocation.toLite(): LiteResourceLocation = LiteResourceLocation.of(namespace, path) diff --git a/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/client/MatterIndexScreen.kt b/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/client/MatterIndexScreen.kt index ce41e8d..97a5921 100644 --- a/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/client/MatterIndexScreen.kt +++ b/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/client/MatterIndexScreen.kt @@ -27,6 +27,8 @@ class MatterIndexScreen( private val focusedItemId: String? = null, ) : Screen(Component.literal("Replication Index")) { private val entries: List = loadEntries() + private val entryRecords: List = entries.map { it.record } + private val entriesById: Map = entries.associateBy { it.record.itemId } private val matterTypes: List = entries .flatMap { it.record.matterValues.keys } @@ -89,6 +91,10 @@ class MatterIndexScreen( ) } + override fun tick() { + searchBox.tick() + } + override fun render(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) { hoveredStack = ItemStack.EMPTY hoveredAmountTooltip = emptyList() @@ -566,19 +572,16 @@ class MatterIndexScreen( private fun visibleEntries(): List { val query = MatterIndexFiltering.parseQuery(searchBox.value, replicatableOnly, disintegratableOnly) - val ordered = MatterIndexFiltering.filterAndSort(entries.map { it.record }, query, sort, ::displayAmount) - val byId = entries.associateBy { it.record.itemId } - return ordered.mapNotNull { byId[it.itemId] } + val ordered = MatterIndexFiltering.filterAndSort(entryRecords, query, sort, ::displayAmount) + return ordered.mapNotNull { entriesById[it.itemId] } } private fun selectedEntry(): MatterIndexEntry? = - entries.firstOrNull { it.record.itemId == selectedItemId } + selectedItemId?.let { entriesById[it] } private fun equivalentEntries(): List { - val records = - MatterIndexFiltering.equivalentRecords(entries.map { it.record }, selectedEntry()?.record, ::displayAmount) - val byId = entries.associateBy { it.record.itemId } - return records.mapNotNull { byId[it.itemId] } + val records = MatterIndexFiltering.equivalentRecords(entryRecords, selectedEntry()?.record, ::displayAmount) + return records.mapNotNull { entriesById[it.itemId] } } private fun columns(layout: Layout): Columns { diff --git a/common/src/main/kotlin/com/p_nsk/replicated_integration/api/selector/MatterSelectorMaterializer.kt b/common/src/main/kotlin/com/p_nsk/replicated_integration/api/selector/MatterSelectorMaterializer.kt index be56c14..96f34c2 100644 --- a/common/src/main/kotlin/com/p_nsk/replicated_integration/api/selector/MatterSelectorMaterializer.kt +++ b/common/src/main/kotlin/com/p_nsk/replicated_integration/api/selector/MatterSelectorMaterializer.kt @@ -29,7 +29,12 @@ object MatterSelectorMaterializer { ): Map { val values = linkedMapOf() val expansion = MatterRuleExpansion(expandTag, expandMod, allNodes, rawValue) - for (rule in rules.sortedWith(compareBy { it.source.priority })) { + val orderedRules = + rules.withIndex().sortedWith( + compareBy> { it.value.source.priority } + .thenBy { it.index }, + ) + for ((_, rule) in orderedRules) { val targets = rule.selector.expand(expansion) for (target in targets) { values[target] = rule.value diff --git a/common/src/main/kotlin/com/p_nsk/replicated_integration/api/selector/MatterSelectorStorageCodec.kt b/common/src/main/kotlin/com/p_nsk/replicated_integration/api/selector/MatterSelectorStorageCodec.kt index 0fd7fbc..4bb88c4 100644 --- a/common/src/main/kotlin/com/p_nsk/replicated_integration/api/selector/MatterSelectorStorageCodec.kt +++ b/common/src/main/kotlin/com/p_nsk/replicated_integration/api/selector/MatterSelectorStorageCodec.kt @@ -92,7 +92,9 @@ object MatterSelectorStorageCodec { val root = JsonObject() val entries = JsonArray() for (rule in rules) { - val selector = rule.selector.staticKey ?: continue + val selector = + rule.selector.staticKey + ?: throw IllegalArgumentException("Cannot save non-static selector: ${rule.selector}") val value = rule.value val json = JsonObject() json.addProperty("selector", selector.kind.name.lowercase()) diff --git a/common/src/main/kotlin/com/p_nsk/replicated_integration/api/selector/MutableMatterSelectors.kt b/common/src/main/kotlin/com/p_nsk/replicated_integration/api/selector/MutableMatterSelectors.kt index bbef69d..36b9d55 100644 --- a/common/src/main/kotlin/com/p_nsk/replicated_integration/api/selector/MutableMatterSelectors.kt +++ b/common/src/main/kotlin/com/p_nsk/replicated_integration/api/selector/MutableMatterSelectors.kt @@ -41,7 +41,12 @@ class MutableMatterSelectors { fun snapshot(): Map { val values = linkedMapOf() - for (rule in rules.sortedWith(compareBy { it.source.priority })) { + val orderedRules = + rules.withIndex().sortedWith( + compareBy> { it.value.source.priority } + .thenBy { it.index }, + ) + for ((_, rule) in orderedRules) { val key = rule.selector.staticKey ?: continue values[key] = rule.value } diff --git a/common/src/test/kotlin/MatterSelectorOrderingTest.kt b/common/src/test/kotlin/MatterSelectorOrderingTest.kt new file mode 100644 index 0000000..f14e6a2 --- /dev/null +++ b/common/src/test/kotlin/MatterSelectorOrderingTest.kt @@ -0,0 +1,81 @@ +package com.p_nsk.replicated_integration.api + +import com.p_nsk.replicated_integration.api.model.ExplicitMatterSource +import com.p_nsk.replicated_integration.api.model.ExplicitMatterValue +import com.p_nsk.replicated_integration.api.model.LiteMatterCompound +import com.p_nsk.replicated_integration.api.model.LiteResourceLocation +import com.p_nsk.replicated_integration.api.node.NodeKey +import com.p_nsk.replicated_integration.api.selector.AnyMatterRuleSelector +import com.p_nsk.replicated_integration.api.selector.MatterRule +import com.p_nsk.replicated_integration.api.selector.MatterSelectorKey +import com.p_nsk.replicated_integration.api.selector.MatterSelectorKind +import com.p_nsk.replicated_integration.api.selector.MatterSelectorMaterializer +import com.p_nsk.replicated_integration.api.selector.MatterSelectorStorageCodec +import com.p_nsk.replicated_integration.api.selector.MutableMatterSelectors +import com.p_nsk.replicated_integration.api.selector.asRuleSelector +import java.nio.file.Files +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class MatterSelectorOrderingTest { + @Test + fun materializerKeepsInsertionOrderForSamePriority() { + val type = LiteResourceLocation.of("replicated_integration", "item") + val id = LiteResourceLocation.of("minecraft", "stone") + val selector = MatterSelectorKey(MatterSelectorKind.NODE, type, id).asRuleSelector() + val firstValue = + ExplicitMatterValue.Set( + LiteMatterCompound(mapOf(LiteResourceLocation.of("replication", "earth") to 1.0)), + ExplicitMatterSource.CONFIG, + ) + val secondValue = + ExplicitMatterValue.Set( + LiteMatterCompound(mapOf(LiteResourceLocation.of("replication", "earth") to 2.0)), + ExplicitMatterSource.CONFIG, + ) + val rules = listOf(MatterRule(selector, firstValue), MatterRule(selector, secondValue)) + + val materialized = + MatterSelectorMaterializer.materialize( + rules, + expandTag = { _, _ -> emptyList() }, + ) + + assertEquals(secondValue, materialized[NodeKey(type, id)]) + } + + @Test + fun snapshotKeepsInsertionOrderForSamePriority() { + val type = LiteResourceLocation.of("replicated_integration", "item") + val id = LiteResourceLocation.of("minecraft", "stone") + val key = MatterSelectorKey(MatterSelectorKind.NODE, type, id) + val selectors = MutableMatterSelectors() + val firstCompound = LiteMatterCompound(mapOf(LiteResourceLocation.of("replication", "earth") to 1.0)) + val secondCompound = LiteMatterCompound(mapOf(LiteResourceLocation.of("replication", "earth") to 2.0)) + + selectors.put(key, firstCompound, ExplicitMatterSource.CONFIG) + selectors.put(key, secondCompound, ExplicitMatterSource.CONFIG) + + val snapshot = selectors.snapshot() + + assertEquals(ExplicitMatterValue.Set(secondCompound, ExplicitMatterSource.CONFIG), snapshot[key]) + } + + @Test + fun saveRulesRejectsNonStaticSelectors() { + val type = LiteResourceLocation.of("replicated_integration", "item") + val id = LiteResourceLocation.of("minecraft", "stone") + val staticSelector = MatterSelectorKey(MatterSelectorKind.NODE, type, id).asRuleSelector() + val rule = + MatterRule( + AnyMatterRuleSelector(listOf(staticSelector)), + ExplicitMatterValue.Deny(ExplicitMatterSource.CONFIG), + ) + val path = Files.createTempDirectory("repint").resolve("rules.json") + + assertFailsWith { + MatterSelectorStorageCodec.saveRules(path, listOf(rule)) + } + } +} diff --git a/docs/1.20.1-1.21.1-spec-differences.md b/docs/1.20.1-1.21.1-spec-differences.md new file mode 100644 index 0000000..dde939f --- /dev/null +++ b/docs/1.20.1-1.21.1-spec-differences.md @@ -0,0 +1,33 @@ +# 1.20.1 (Forge) と 1.21.1 (NeoForge) の仕様差分まとめ + +v0.2.0 リリース前の品質保証向けに、1.20.1 と 1.21.1 の実装差分を整理したメモです。 + +## ローダー / プラットフォーム + +- イベントバス: 1.20.1 は `net.minecraftforge.*` 系の Forge イベント、1.21.1 は `net.neoforged.*` 系の NeoForge イベント。 +- Reload hook: `AddReloadListenerEvent` は 1.21.1 で `net.neoforged.neoforge.event.AddReloadListenerEvent` に移動。 +- Fluid API: `FluidStack` が `net.minecraftforge.fluids.FluidStack` から `net.neoforged.neoforge.fluids.FluidStack` に変更。 +- Mod 検索: `ModList` が `net.minecraftforge.fml.ModList` から `net.neoforged.fml.ModList` に変更。 + +## Replication 連携差分 + +- `ReplicationCalculation.DEFAULT_MATTER_RECIPE` が 1.21.1 では `RecipeHolder` を保持。 +- `ReplicationCalculation.DEFAULT_MATTER_COMPOUND` のキーが文字列 ID から `Item` に変更。 +- `MatterCompound.serializeNBT` が 1.21.1 では `RegistryAccess` を要求。 +- Mixin でフックするメソッドが `calculateRecipes()` → `calculateRecipes(RegistryAccess)` に変更。 +- 1.21.1 ではサーバ起動前に `calculateRecipes(RegistryAccess)` が走る場合があるため、サーバ起動後に再計算するフックが必要。 +- `MatterValueRecipe` の JSON は 1.21.1 で `amount` フィールドを使用(独自 node-value JSON は従来通り `value`)。 +- Datapack のレシピパスが `recipes/...` から `data//recipe/...` に変更。 +- 共有タグが `forge:` から `c:` に変更。 + +## Mekanism 連携差分 + +- `mekanism.api.recipes.chemical.*` の複数クラスが 1.21.1 では `mekanism.api.recipes.*` 配下へ移動。 +- 圧縮レシピ判定: 1.20.1 は `ItemStackGasToItemStackRecipe` を使うが、1.21.1 では公開されないため `MekanismRecipeTypes.TYPE_COMPRESSING` を使用。 +- 化学溶解の出力: 1.20.1 は JEI 出力ラッパーの `chemicalStack` を参照、1.21.1 は `outputDefinition` で `ChemicalStack` を直接参照。 +- Rotary 関連は 1.21.1 API 名称に合わせ、fluid/chemical ベースの命名へ統一。 + +## 追加データ(1.21.1 側に持ち込み済み) + +- `data/replicated_integration/recipe/matter_values/c/tags/gems/fluorite.json` +- `data/replicated_integration/replicated_integration/matter_node_values/minecraft/fluids/water.json`