diff --git a/1.20.1-forge/build.gradle.kts b/1.20.1-forge/build.gradle.kts index 998ec69..bb6e36d 100644 --- a/1.20.1-forge/build.gradle.kts +++ b/1.20.1-forge/build.gradle.kts @@ -20,6 +20,9 @@ dependencies { modImplementation(libs.titanium.mc1201) modImplementation(libs.ae2.mc1201) modImplementation(libs.aae.mc1201) + modImplementation(libs.kubejs.mc1201) + modImplementation(libs.architectury.mc1201) + modImplementation(libs.rhino.mc1201) // aaeについてくる modCompileOnly(libs.ae2addonlib.mc1201) // aaeに不可欠 @@ -30,6 +33,7 @@ dependencies { modRuntimeOnly(libs.emi.mc1201) modRuntimeOnly(libs.jade.mc1201) modRuntimeOnly(libs.configured.mc1201) + modRuntimeOnly(libs.probejs.mc1201) // ae2 dependency modRuntimeOnly(libs.guideme.mc1201) diff --git a/1.20.1-forge/src/generated/resources/assets/replicated_integration/lang/en_us.json b/1.20.1-forge/src/generated/resources/assets/replicated_integration/lang/en_us.json index 92cbe4a..cebb84b 100644 --- a/1.20.1-forge/src/generated/resources/assets/replicated_integration/lang/en_us.json +++ b/1.20.1-forge/src/generated/resources/assets/replicated_integration/lang/en_us.json @@ -1,4 +1,6 @@ { + "key.categories.replicated_integration": "Replicated Integration", + "key.replicated_integration.open_replication_index": "Open Replication Index", "replicated_integration.chat.notice.config_path": "Open %s to adjust optional integration behavior.", "replicated_integration.chat.notice.configurable_integrations": "You can fine-tune these integrations in the compatibility config:", "replicated_integration.chat.notice.supported_integrations": "has tuned support for these installed mods:", diff --git a/1.20.1-forge/src/generated/resources/assets/replicated_integration/lang/ja_jp.json b/1.20.1-forge/src/generated/resources/assets/replicated_integration/lang/ja_jp.json index f308816..aeb1d2c 100644 --- a/1.20.1-forge/src/generated/resources/assets/replicated_integration/lang/ja_jp.json +++ b/1.20.1-forge/src/generated/resources/assets/replicated_integration/lang/ja_jp.json @@ -1,4 +1,6 @@ { + "key.categories.replicated_integration": "Replicated Integration", + "key.replicated_integration.open_replication_index": "Replication Index を開く", "replicated_integration.chat.notice.config_path": "任意の連携動作は %s から変更できます。", "replicated_integration.chat.notice.configurable_integrations": "さらに、以下の統合は互換性コンフィグで調整できます:", "replicated_integration.chat.notice.supported_integrations": "は、導入済みの以下 mod と連携できます:", diff --git a/1.20.1-forge/src/generated/resources/probejs.documents.txt b/1.20.1-forge/src/generated/resources/probejs.documents.txt new file mode 100644 index 0000000..3421512 --- /dev/null +++ b/1.20.1-forge/src/generated/resources/probejs.documents.txt @@ -0,0 +1 @@ +probejs/replicated_integration.json diff --git a/1.20.1-forge/src/generated/resources/probejs/replicated_integration.json b/1.20.1-forge/src/generated/resources/probejs/replicated_integration.json new file mode 100644 index 0000000..75bc5a2 --- /dev/null +++ b/1.20.1-forge/src/generated/resources/probejs/replicated_integration.json @@ -0,0 +1,614 @@ +{ + "classes": [ + { + "type": "document:class", + "className": "com.p_nsk.replicated_integration.kubejs.ForgeRepInt", + "methods": [ + { + "type": "document:method", + "abstract": false, + "name": "nodeMod", + "params": [ + { + "type": "property:param", + "name": "type", + "paramType": { + "type": "type:class", + "name": "java.lang.String" + }, + "varArg": false + }, + { + "type": "property:param", + "name": "modId", + "paramType": { + "type": "type:class", + "name": "java.lang.String" + }, + "varArg": false + } + ], + "properties": [ + { + "type": "property:modify", + "index": 1, + "newType": { + "type": "type:primitive", + "name": "Special.Mod" + } + } + ], + "returns": { + "type": "type:class", + "name": "com.p_nsk.replicated_integration.api.selector.MatterRuleSelector" + }, + "static": true + }, + { + "type": "document:method", + "abstract": false, + "name": "item", + "params": [ + { + "type": "property:param", + "name": "id", + "paramType": { + "type": "type:class", + "name": "java.lang.String" + }, + "varArg": false + } + ], + "properties": [ + { + "type": "property:modify", + "index": 0, + "newType": { + "type": "type:primitive", + "name": "Special.Item" + } + } + ], + "returns": { + "type": "type:class", + "name": "com.p_nsk.replicated_integration.api.selector.MatterRuleSelector" + }, + "static": true + }, + { + "type": "document:method", + "abstract": false, + "name": "itemTag", + "params": [ + { + "type": "property:param", + "name": "tag", + "paramType": { + "type": "type:class", + "name": "java.lang.String" + }, + "varArg": false + } + ], + "properties": [ + { + "type": "property:modify", + "index": 0, + "newType": { + "type": "type:primitive", + "name": "Special.ItemTag" + } + } + ], + "returns": { + "type": "type:class", + "name": "com.p_nsk.replicated_integration.api.selector.MatterRuleSelector" + }, + "static": true + }, + { + "type": "document:method", + "abstract": false, + "name": "itemMod", + "params": [ + { + "type": "property:param", + "name": "modId", + "paramType": { + "type": "type:class", + "name": "java.lang.String" + }, + "varArg": false + } + ], + "properties": [ + { + "type": "property:modify", + "index": 0, + "newType": { + "type": "type:primitive", + "name": "Special.Mod" + } + } + ], + "returns": { + "type": "type:class", + "name": "com.p_nsk.replicated_integration.api.selector.MatterRuleSelector" + }, + "static": true + }, + { + "type": "document:method", + "abstract": false, + "name": "fluid", + "params": [ + { + "type": "property:param", + "name": "id", + "paramType": { + "type": "type:class", + "name": "java.lang.String" + }, + "varArg": false + } + ], + "properties": [ + { + "type": "property:modify", + "index": 0, + "newType": { + "type": "type:primitive", + "name": "Special.Fluid" + } + } + ], + "returns": { + "type": "type:class", + "name": "com.p_nsk.replicated_integration.api.selector.MatterRuleSelector" + }, + "static": true + }, + { + "type": "document:method", + "abstract": false, + "name": "fluidTag", + "params": [ + { + "type": "property:param", + "name": "tag", + "paramType": { + "type": "type:class", + "name": "java.lang.String" + }, + "varArg": false + } + ], + "properties": [ + { + "type": "property:modify", + "index": 0, + "newType": { + "type": "type:primitive", + "name": "Special.FluidTag" + } + } + ], + "returns": { + "type": "type:class", + "name": "com.p_nsk.replicated_integration.api.selector.MatterRuleSelector" + }, + "static": true + }, + { + "type": "document:method", + "abstract": false, + "name": "fluidMod", + "params": [ + { + "type": "property:param", + "name": "modId", + "paramType": { + "type": "type:class", + "name": "java.lang.String" + }, + "varArg": false + } + ], + "properties": [ + { + "type": "property:modify", + "index": 0, + "newType": { + "type": "type:primitive", + "name": "Special.Mod" + } + } + ], + "returns": { + "type": "type:class", + "name": "com.p_nsk.replicated_integration.api.selector.MatterRuleSelector" + }, + "static": true + } + ] + }, + { + "type": "document:class", + "className": "com.p_nsk.replicated_integration.kubejs.ForgeRepIntMek", + "methods": [ + { + "type": "document:method", + "abstract": false, + "name": "gas", + "params": [ + { + "type": "property:param", + "name": "id", + "paramType": { + "type": "type:class", + "name": "java.lang.String" + }, + "varArg": false + } + ], + "properties": [ + { + "type": "property:modify", + "index": 0, + "newType": { + "type": "type:primitive", + "name": "Special.Gas" + } + } + ], + "returns": { + "type": "type:class", + "name": "com.p_nsk.replicated_integration.api.selector.MatterRuleSelector" + }, + "static": true + }, + { + "type": "document:method", + "abstract": false, + "name": "gasTag", + "params": [ + { + "type": "property:param", + "name": "tag", + "paramType": { + "type": "type:class", + "name": "java.lang.String" + }, + "varArg": false + } + ], + "properties": [ + { + "type": "property:modify", + "index": 0, + "newType": { + "type": "type:primitive", + "name": "Special.GasTag" + } + } + ], + "returns": { + "type": "type:class", + "name": "com.p_nsk.replicated_integration.api.selector.MatterRuleSelector" + }, + "static": true + }, + { + "type": "document:method", + "abstract": false, + "name": "gasMod", + "params": [ + { + "type": "property:param", + "name": "modId", + "paramType": { + "type": "type:class", + "name": "java.lang.String" + }, + "varArg": false + } + ], + "properties": [ + { + "type": "property:modify", + "index": 0, + "newType": { + "type": "type:primitive", + "name": "Special.Mod" + } + } + ], + "returns": { + "type": "type:class", + "name": "com.p_nsk.replicated_integration.api.selector.MatterRuleSelector" + }, + "static": true + }, + { + "type": "document:method", + "abstract": false, + "name": "infuseType", + "params": [ + { + "type": "property:param", + "name": "id", + "paramType": { + "type": "type:class", + "name": "java.lang.String" + }, + "varArg": false + } + ], + "properties": [ + { + "type": "property:modify", + "index": 0, + "newType": { + "type": "type:primitive", + "name": "Special.InfuseType" + } + } + ], + "returns": { + "type": "type:class", + "name": "com.p_nsk.replicated_integration.api.selector.MatterRuleSelector" + }, + "static": true + }, + { + "type": "document:method", + "abstract": false, + "name": "infuseTypeTag", + "params": [ + { + "type": "property:param", + "name": "tag", + "paramType": { + "type": "type:class", + "name": "java.lang.String" + }, + "varArg": false + } + ], + "properties": [ + { + "type": "property:modify", + "index": 0, + "newType": { + "type": "type:primitive", + "name": "Special.InfuseTypeTag" + } + } + ], + "returns": { + "type": "type:class", + "name": "com.p_nsk.replicated_integration.api.selector.MatterRuleSelector" + }, + "static": true + }, + { + "type": "document:method", + "abstract": false, + "name": "infuseTypeMod", + "params": [ + { + "type": "property:param", + "name": "modId", + "paramType": { + "type": "type:class", + "name": "java.lang.String" + }, + "varArg": false + } + ], + "properties": [ + { + "type": "property:modify", + "index": 0, + "newType": { + "type": "type:primitive", + "name": "Special.Mod" + } + } + ], + "returns": { + "type": "type:class", + "name": "com.p_nsk.replicated_integration.api.selector.MatterRuleSelector" + }, + "static": true + }, + { + "type": "document:method", + "abstract": false, + "name": "pigment", + "params": [ + { + "type": "property:param", + "name": "id", + "paramType": { + "type": "type:class", + "name": "java.lang.String" + }, + "varArg": false + } + ], + "properties": [ + { + "type": "property:modify", + "index": 0, + "newType": { + "type": "type:primitive", + "name": "Special.Pigment" + } + } + ], + "returns": { + "type": "type:class", + "name": "com.p_nsk.replicated_integration.api.selector.MatterRuleSelector" + }, + "static": true + }, + { + "type": "document:method", + "abstract": false, + "name": "pigmentTag", + "params": [ + { + "type": "property:param", + "name": "tag", + "paramType": { + "type": "type:class", + "name": "java.lang.String" + }, + "varArg": false + } + ], + "properties": [ + { + "type": "property:modify", + "index": 0, + "newType": { + "type": "type:primitive", + "name": "Special.PigmentTag" + } + } + ], + "returns": { + "type": "type:class", + "name": "com.p_nsk.replicated_integration.api.selector.MatterRuleSelector" + }, + "static": true + }, + { + "type": "document:method", + "abstract": false, + "name": "pigmentMod", + "params": [ + { + "type": "property:param", + "name": "modId", + "paramType": { + "type": "type:class", + "name": "java.lang.String" + }, + "varArg": false + } + ], + "properties": [ + { + "type": "property:modify", + "index": 0, + "newType": { + "type": "type:primitive", + "name": "Special.Mod" + } + } + ], + "returns": { + "type": "type:class", + "name": "com.p_nsk.replicated_integration.api.selector.MatterRuleSelector" + }, + "static": true + }, + { + "type": "document:method", + "abstract": false, + "name": "slurry", + "params": [ + { + "type": "property:param", + "name": "id", + "paramType": { + "type": "type:class", + "name": "java.lang.String" + }, + "varArg": false + } + ], + "properties": [ + { + "type": "property:modify", + "index": 0, + "newType": { + "type": "type:primitive", + "name": "Special.Slurry" + } + } + ], + "returns": { + "type": "type:class", + "name": "com.p_nsk.replicated_integration.api.selector.MatterRuleSelector" + }, + "static": true + }, + { + "type": "document:method", + "abstract": false, + "name": "slurryTag", + "params": [ + { + "type": "property:param", + "name": "tag", + "paramType": { + "type": "type:class", + "name": "java.lang.String" + }, + "varArg": false + } + ], + "properties": [ + { + "type": "property:modify", + "index": 0, + "newType": { + "type": "type:primitive", + "name": "Special.SlurryTag" + } + } + ], + "returns": { + "type": "type:class", + "name": "com.p_nsk.replicated_integration.api.selector.MatterRuleSelector" + }, + "static": true + }, + { + "type": "document:method", + "abstract": false, + "name": "slurryMod", + "params": [ + { + "type": "property:param", + "name": "modId", + "paramType": { + "type": "type:class", + "name": "java.lang.String" + }, + "varArg": false + } + ], + "properties": [ + { + "type": "property:modify", + "index": 0, + "newType": { + "type": "type:primitive", + "name": "Special.Mod" + } + } + ], + "returns": { + "type": "type:class", + "name": "com.p_nsk.replicated_integration.api.selector.MatterRuleSelector" + }, + "static": true + } + ] + } + ] +} \ No newline at end of file diff --git a/1.20.1-forge/src/main/java/com/p_nsk/replicated_integration/mixin/ScreenAccessor.java b/1.20.1-forge/src/main/java/com/p_nsk/replicated_integration/mixin/ScreenAccessor.java new file mode 100644 index 0000000..c149e77 --- /dev/null +++ b/1.20.1-forge/src/main/java/com/p_nsk/replicated_integration/mixin/ScreenAccessor.java @@ -0,0 +1,14 @@ +package com.p_nsk.replicated_integration.mixin; + +import net.minecraft.client.gui.components.Renderable; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.narration.NarratableEntry; +import net.minecraft.client.gui.screens.Screen; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(Screen.class) +public interface ScreenAccessor { + @Invoker("addRenderableWidget") + T replicatedIntegration$addRenderableWidget(T widget); +} diff --git a/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/ModMain.kt b/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/ModMain.kt index 9cee8e6..55632e6 100644 --- a/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/ModMain.kt +++ b/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/ModMain.kt @@ -5,6 +5,7 @@ package com.p_nsk.replicated_integration import com.p_nsk.replicated_integration.command.MatterNodeDebugCommand import com.p_nsk.replicated_integration.command.MatterCommand import com.p_nsk.replicated_integration.config.ForgeCompatibilityConfig +import com.p_nsk.replicated_integration.config.ForgeUiConfig import com.p_nsk.replicated_integration.core.ForgeBuiltinReplicationAddons import com.p_nsk.replicated_integration.data.ReplicatedIntegrationDataGen import com.p_nsk.replicated_integration.data.ReplicationReloadHooks @@ -43,5 +44,10 @@ object ModMain { ForgeCompatibilityConfig.spec, "replicated_integration-compatibility.toml", ) + ModLoadingContext.get().registerConfig( + ModConfig.Type.CLIENT, + ForgeUiConfig.spec, + "replicated_integration-ui.toml", + ) } } diff --git a/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/adapter/mekanism/MekanismNodeResolver.kt b/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/adapter/mekanism/MekanismNodeResolver.kt index da0cb69..b729b7d 100644 --- a/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/adapter/mekanism/MekanismNodeResolver.kt +++ b/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/adapter/mekanism/MekanismNodeResolver.kt @@ -4,12 +4,14 @@ import com.p_nsk.replicated_integration.api.model.LiteResourceLocation import com.p_nsk.replicated_integration.api.model.NodeAmount import com.p_nsk.replicated_integration.api.node.NodeKey import com.p_nsk.replicated_integration.api.node.MatterNodes +import mekanism.api.MekanismAPI import mekanism.api.chemical.ChemicalStack import mekanism.api.chemical.gas.GasStack import mekanism.api.chemical.infuse.InfusionStack import mekanism.api.chemical.pigment.PigmentStack import mekanism.api.chemical.slurry.SlurryStack import net.minecraft.resources.ResourceLocation +import net.minecraftforge.registries.IForgeRegistry object MekanismNodeResolver { fun chemicalNode(stack: ChemicalStack<*>): NodeKey? { @@ -30,6 +32,53 @@ object MekanismNodeResolver { fun chemicalAmount(stack: ChemicalStack<*>): NodeAmount? = chemicalNode(stack)?.takeIf { stack.amount > 0L }?.let { NodeAmount(it, stack.amount) } + fun gasNodesInTag(id: ResourceLocation): List = + nodesInTag(MekanismAPI.gasRegistry(), id, MatterNodes::gas) + + fun gasNodesInMod(modId: String): List = + nodesInMod(MekanismAPI.gasRegistry(), modId, MatterNodes::gas) + + fun infuseTypeNodesInTag(id: ResourceLocation): List = + nodesInTag(MekanismAPI.infuseTypeRegistry(), id, MatterNodes::infuseType) + + fun infuseTypeNodesInMod(modId: String): List = + nodesInMod(MekanismAPI.infuseTypeRegistry(), modId, MatterNodes::infuseType) + + fun pigmentNodesInTag(id: ResourceLocation): List = + nodesInTag(MekanismAPI.pigmentRegistry(), id, MatterNodes::pigment) + + fun pigmentNodesInMod(modId: String): List = + nodesInMod(MekanismAPI.pigmentRegistry(), modId, MatterNodes::pigment) + + fun slurryNodesInTag(id: ResourceLocation): List = + nodesInTag(MekanismAPI.slurryRegistry(), id, MatterNodes::slurry) + + fun slurryNodesInMod(modId: String): List = + nodesInMod(MekanismAPI.slurryRegistry(), modId, MatterNodes::slurry) + + private fun nodesInTag( + registry: IForgeRegistry, + id: ResourceLocation, + nodeOf: (LiteResourceLocation) -> NodeKey, + ): List { + val tags = registry.tags() ?: return emptyList() + return tags.getTag(tags.createTagKey(id)).mapNotNull { value -> + registry.getKey(value)?.toLite()?.let(nodeOf) + } + } + + private fun nodesInMod( + registry: IForgeRegistry, + modId: String, + nodeOf: (LiteResourceLocation) -> NodeKey, + ): List = + registry.mapNotNull { value -> + registry.getKey(value) + ?.takeIf { it.namespace == modId } + ?.toLite() + ?.let(nodeOf) + } + private fun ResourceLocation.toLite(): LiteResourceLocation = LiteResourceLocation.of(namespace, path) } diff --git a/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/adapter/mekanism/ReplicationMekanismAddon.kt b/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/adapter/mekanism/ReplicationMekanismAddon.kt index 64cee45..d5a6007 100644 --- a/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/adapter/mekanism/ReplicationMekanismAddon.kt +++ b/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/adapter/mekanism/ReplicationMekanismAddon.kt @@ -51,6 +51,12 @@ object ReplicationMekanismAddon : ForgeReplicationAddon { suggestions = MekanismMatterCommandInputs.infuseTypeSuggestions(), validate = MekanismMatterCommandInputs.infuseTypeValidator(), ) + + tag( + literal = "infuse_type_tag", + suggestions = MekanismMatterCommandInputs.infuseTypeTagSuggestions(), + validate = MekanismMatterCommandInputs.infuseTypeTagValidator(), + ) } node(MekanismMatterNodes.PIGMENT, "Pigment") { @@ -59,6 +65,12 @@ object ReplicationMekanismAddon : ForgeReplicationAddon { suggestions = MekanismMatterCommandInputs.pigmentSuggestions(), validate = MekanismMatterCommandInputs.pigmentValidator(), ) + + tag( + literal = "pigment_tag", + suggestions = MekanismMatterCommandInputs.pigmentTagSuggestions(), + validate = MekanismMatterCommandInputs.pigmentTagValidator(), + ) } node(MekanismMatterNodes.SLURRY, "Slurry") { @@ -67,6 +79,12 @@ object ReplicationMekanismAddon : ForgeReplicationAddon { suggestions = MekanismMatterCommandInputs.slurrySuggestions(), validate = MekanismMatterCommandInputs.slurryValidator(), ) + + tag( + literal = "slurry_tag", + suggestions = MekanismMatterCommandInputs.slurryTagSuggestions(), + validate = MekanismMatterCommandInputs.slurryTagValidator(), + ) } } 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 9c64a8b..00296ea 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 @@ -2,16 +2,17 @@ package com.p_nsk.replicated_integration.adapter.vanilla import com.p_nsk.replicated_integration.api.model.LiteResourceLocation import com.p_nsk.replicated_integration.api.model.NodeAmount -import com.p_nsk.replicated_integration.api.node.NodeKey import com.p_nsk.replicated_integration.api.node.MatterNodes -import net.minecraft.core.registries.Registries +import com.p_nsk.replicated_integration.api.node.NodeKey import net.minecraft.core.registries.BuiltInRegistries +import net.minecraft.core.registries.Registries import net.minecraft.resources.ResourceLocation import net.minecraft.tags.TagKey import net.minecraft.world.item.ItemStack import net.minecraftforge.fluids.FluidStack import net.minecraftforge.registries.ForgeRegistries +@Suppress("unused") object BuiltinNodeResolver { fun itemNode(stack: ItemStack): NodeKey? { if (stack.isEmpty) { @@ -24,6 +25,9 @@ object BuiltinNodeResolver { fun itemNode(id: ResourceLocation): NodeKey = MatterNodes.item(id.toLite()) + fun item(id: LiteResourceLocation) = + BuiltInRegistries.ITEM.get(id.toMcResourceLocation()) + fun fluidNode(stack: FluidStack): NodeKey? { if (stack.isEmpty) { return null @@ -37,6 +41,9 @@ object BuiltinNodeResolver { if (fluid == net.minecraft.world.level.material.Fluids.EMPTY) null else MatterNodes.fluid(id.toLite()) } + fun fluid(id: LiteResourceLocation) = + ForgeRegistries.FLUIDS.getValue(id.toMcResourceLocation()) + fun itemNodesInTag(id: ResourceLocation): List = BuiltInRegistries.ITEM.getTag(TagKey.create(Registries.ITEM, id)) .map { holders -> @@ -54,6 +61,21 @@ object BuiltinNodeResolver { } .orElse(emptyList()) + fun allItemNodes(): List = + BuiltInRegistries.ITEM + .map { item -> MatterNodes.item(BuiltInRegistries.ITEM.getKey(item).toLite()) } + + 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 itemNodesInMod(modId: String): List = + allItemNodes().filter { it.id.namespace == modId } + + fun fluidNodesInMod(modId: String): List = + allFluidNodes().filter { it.id.namespace == modId } + fun itemAmount(stack: ItemStack): NodeAmount? = itemNode(stack)?.let { NodeAmount(it, stack.count.coerceAtLeast(1).toLong()) } @@ -62,4 +84,7 @@ object BuiltinNodeResolver { private fun ResourceLocation.toLite(): LiteResourceLocation = LiteResourceLocation.of(namespace, path) + + private fun LiteResourceLocation.toMcResourceLocation(): ResourceLocation = + ResourceLocation.fromNamespaceAndPath(namespace, path) } diff --git a/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/adapter/vanilla/ReplicationVanillaAddon.kt b/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/adapter/vanilla/ReplicationVanillaAddon.kt index 5fe6642..6d70ddd 100644 --- a/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/adapter/vanilla/ReplicationVanillaAddon.kt +++ b/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/adapter/vanilla/ReplicationVanillaAddon.kt @@ -41,6 +41,9 @@ object ReplicationVanillaAddon : ForgeReplicationAddon { override fun registerMatterNodes(registry: ForgeMatterNodeRegistry) = with(registry) { node(MatterNodes.ITEM, "Item") { + enumerate(BuiltinNodeResolver::allItemNodes) + rawValue(BuiltinNodeResolver::item) + value( literal = "item", suggestions = VanillaMatterCommandInputs.itemSuggestions(), @@ -55,6 +58,9 @@ object ReplicationVanillaAddon : ForgeReplicationAddon { } node(MatterNodes.FLUID, "Fluid") { + enumerate(BuiltinNodeResolver::allFluidNodes) + rawValue(BuiltinNodeResolver::fluid) + value( literal = "fluid", suggestions = VanillaMatterCommandInputs.fluidSuggestions(), diff --git a/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/client/MatterIndexClientEvents.kt b/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/client/MatterIndexClientEvents.kt new file mode 100644 index 0000000..344d7c9 --- /dev/null +++ b/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/client/MatterIndexClientEvents.kt @@ -0,0 +1,188 @@ +package com.p_nsk.replicated_integration.client + +import com.mojang.blaze3d.platform.InputConstants +import com.p_nsk.replicated_integration.Constants +import com.p_nsk.replicated_integration.mixin.ScreenAccessor +import net.minecraft.ChatFormatting +import net.minecraft.client.KeyMapping +import net.minecraft.client.Minecraft +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.gui.components.AbstractButton +import net.minecraft.client.gui.narration.NarrationElementOutput +import net.minecraft.client.gui.screens.Screen +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen +import net.minecraft.network.chat.Component +import net.minecraft.world.item.ItemStack +import net.minecraftforge.api.distmarker.Dist +import net.minecraftforge.client.event.RegisterKeyMappingsEvent +import net.minecraftforge.client.event.ScreenEvent +import net.minecraftforge.client.settings.KeyConflictContext +import net.minecraftforge.event.TickEvent +import net.minecraftforge.event.entity.player.ItemTooltipEvent +import net.minecraftforge.eventbus.api.SubscribeEvent +import net.minecraftforge.fml.common.Mod +import kotlin.math.max +import kotlin.math.min + +@Mod.EventBusSubscriber(modid = Constants.MOD_ID, value = [Dist.CLIENT], bus = Mod.EventBusSubscriber.Bus.FORGE) +object MatterIndexClientEvents { + private const val HOLD_TICKS = 10 + + private var hoveredStack: ItemStack = ItemStack.EMPTY + private var trackingStack: ItemStack = ItemStack.EMPTY + private var holdTicks: Int = 0 + + @JvmStatic + @SubscribeEvent + fun onScreenInit(event: ScreenEvent.Init.Post) { + val screen = event.screen + if (screen !is AbstractContainerScreen<*>) return + (screen as ScreenAccessor).`replicatedIntegration$addRenderableWidget`(MatterIndexSidebarButton(screen)) + } + + @JvmStatic + @SubscribeEvent + fun onItemTooltip(event: ItemTooltipEvent) { + val minecraft = Minecraft.getInstance() + val screen = minecraft.screen ?: return + if (screen is MatterIndexScreen || event.entity == null) return + updateHovered(event.itemStack) + if (trackingStack !== event.itemStack && !trackingStack.`is`(event.itemStack.item)) return + val progress = holdTicks.toFloat() / HOLD_TICKS.toFloat() + val line = + if (progress <= 0f) { + holdHint() + } else { + progressBar(progress, holdHint()) + } + if (event.toolTip.size < 2) { + event.toolTip.add(line) + } else { + event.toolTip.add(1, line) + } + } + + @JvmStatic + @SubscribeEvent + fun onClientTick(event: TickEvent.ClientTickEvent) { + if (event.phase != TickEvent.Phase.END) return + val minecraft = Minecraft.getInstance() + val screen = minecraft.screen + if (screen == null || screen is MatterIndexScreen || trackingStack.isEmpty) { + resetHold() + return + } + + val holding = !hoveredStack.isEmpty && MatterIndexKeyMappings.isOpenIndexDown(minecraft.window.window) + holdTicks = + if (holding) { + min(HOLD_TICKS, holdTicks + 1) + } else { + max(0, holdTicks - 1) + } + + if (holdTicks >= HOLD_TICKS) { + val itemId = MatterIndexScreen.itemId(trackingStack) + resetHold() + minecraft.setScreen(MatterIndexScreen(screen, itemId)) + return + } + + hoveredStack = ItemStack.EMPTY + } + + private fun updateHovered(stack: ItemStack) { + hoveredStack = ItemStack.EMPTY + if (stack.isEmpty) return + if (!MatterIndexScreen.hasMatter(MatterIndexScreen.itemId(stack))) return + if (trackingStack.isEmpty || !trackingStack.`is`(stack.item)) { + holdTicks = 0 + } + trackingStack = stack + hoveredStack = stack + } + + private fun resetHold() { + hoveredStack = ItemStack.EMPTY + trackingStack = ItemStack.EMPTY + holdTicks = 0 + } + + private fun progressBar(progress: Float, hint: Component): Component { + val total = max(26, Minecraft.getInstance().font.width(hint) / Minecraft.getInstance().font.width("|")) + val easedProgress = easeIn(progress.coerceIn(0f, 1f)) + val current = (easedProgress * total).toInt() + return Component.literal("|".repeat(current)) + .withStyle(ChatFormatting.GRAY) + .append(Component.literal("|".repeat(total - current)).withStyle(ChatFormatting.DARK_GRAY)) + } + + private fun easeIn(progress: Float): Float = + progress * progress + + private fun holdHint(): Component = + Component.literal("Hold ") + .withStyle(ChatFormatting.DARK_GRAY) + .append(Component.literal("[").withStyle(ChatFormatting.GRAY)) + .append(MatterIndexKeyMappings.openIndex.translatedKeyMessage.copy().withStyle(ChatFormatting.GRAY)) + .append(Component.literal("]").withStyle(ChatFormatting.GRAY)) + .append(Component.literal(" to open Replication Index").withStyle(ChatFormatting.DARK_GRAY)) + + private class MatterIndexSidebarButton( + private val owner: Screen, + ) : AbstractButton(2, 2, 22, 22, Component.literal("Replication Index")) { + override fun onPress() { + Minecraft.getInstance().setScreen(MatterIndexScreen(owner)) + } + + override fun renderWidget(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) { + guiGraphics.fill(x, y, x + width, y + height, 0xCC071010.toInt()) + guiGraphics.fill(x, y, x + width, y + 1, 0xFF41514D.toInt()) + guiGraphics.fill(x, y + height - 1, x + width, y + height, 0xFF41514D.toInt()) + guiGraphics.fill(x, y, x + 1, y + height, 0xFF41514D.toInt()) + guiGraphics.fill(x + width - 1, y, x + width, y + height, 0xFF41514D.toInt()) + if (isHoveredOrFocused) { + guiGraphics.fill(x + 1, y + 1, x + width - 1, y + height - 1, 0x5536D84A) + guiGraphics.renderTooltip(Minecraft.getInstance().font, message, mouseX, mouseY) + } + drawRiMark(guiGraphics, x + 4, y + 4) + } + + override fun updateWidgetNarration(output: NarrationElementOutput) { + defaultButtonNarrationText(output) + } + + private fun drawRiMark(guiGraphics: GuiGraphics, x: Int, y: Int) { + guiGraphics.fill(x + 5, y, x + 9, y + 4, 0xFF42E84F.toInt()) + guiGraphics.fill(x, y + 5, x + 4, y + 9, 0xFF42E84F.toInt()) + guiGraphics.fill(x + 10, y + 5, x + 14, y + 9, 0xFF42E84F.toInt()) + guiGraphics.fill(x + 5, y + 10, x + 9, y + 14, 0xFF42E84F.toInt()) + } + } +} + +object MatterIndexKeyMappings { + val openIndex: KeyMapping = + KeyMapping( + "key.replicated_integration.open_replication_index", + KeyConflictContext.GUI, + InputConstants.Type.KEYSYM, + InputConstants.KEY_W, + "key.categories.replicated_integration", + ) + + fun isOpenIndexDown(window: Long): Boolean { + if (openIndex.isUnbound) return false + val key = openIndex.key + return InputConstants.isKeyDown(window, key.value) + } +} + +@Mod.EventBusSubscriber(modid = Constants.MOD_ID, value = [Dist.CLIENT], bus = Mod.EventBusSubscriber.Bus.MOD) +object MatterIndexClientModEvents { + @JvmStatic + @SubscribeEvent + fun onRegisterKeyMappings(event: RegisterKeyMappingsEvent) { + event.register(MatterIndexKeyMappings.openIndex) + } +} 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 new file mode 100644 index 0000000..f1b7450 --- /dev/null +++ b/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/client/MatterIndexScreen.kt @@ -0,0 +1,923 @@ +package com.p_nsk.replicated_integration.client + +import com.buuz135.replication.Replication +import com.buuz135.replication.calculation.MatterCompound +import com.buuz135.replication.calculation.client.ClientReplicationCalculation +import com.buuz135.replication.util.ReplicationTags +import com.p_nsk.replicated_integration.api.client.* +import com.p_nsk.replicated_integration.config.ForgeUiConfig +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.gui.components.Button +import net.minecraft.client.gui.components.EditBox +import net.minecraft.client.gui.screens.Screen +import net.minecraft.core.registries.BuiltInRegistries +import net.minecraft.network.chat.Component +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.item.ItemStack +import org.lwjgl.glfw.GLFW +import kotlin.math.ceil +import kotlin.math.max +import kotlin.math.min + +class MatterIndexScreen( + private val parent: Screen?, + private val focusedItemId: String? = null, +) : Screen(Component.literal("Replication Index")) { + private val entries: List = loadEntries() + private val matterTypes: List = + entries + .flatMap { it.record.matterValues.keys } + .distinct() + .filterNot { it == "empty" } + .sorted() + + private var selectedItemId: String? = focusedItemId + private var sort = MatterIndexSort() + private var indexScroll = 0 + private var equivalentScroll = 0 + private var matterOnly = true + private var replicatableOnly = false + private var disintegratableOnly = false + private var displayScalePercent = MatterIndexDisplayScale.DEFAULT_PERCENT + private var hoveredStack: ItemStack = ItemStack.EMPTY + private var hoveredAmountTooltip: List = emptyList() + private var draggingScrollbar: DraggingScrollbar? = null + + private lateinit var searchBox: EditBox + private lateinit var matterOnlyButton: Button + private lateinit var replicatableButton: Button + private lateinit var disintegratableButton: Button + + override fun init() { + val layout = layout() + searchBox = EditBox(font, layout.index.x + 16, layout.index.y + 35, min(260, layout.index.w / 3), 18, Component.literal("search")) + searchBox.setMaxLength(80) + searchBox.setTextColor(0xE8F3ED) + searchBox.value = "" + addWidget(searchBox) + + matterOnlyButton = addRenderableWidget( + Button.builder(toggleLabel("M", matterOnly)) { + matterOnly = !matterOnly + matterOnlyButton.message = toggleLabel("M", matterOnly) + indexScroll = 0 + }.bounds(searchBox.x + searchBox.width + 8, searchBox.y, 34, 18).build(), + ) + replicatableButton = addRenderableWidget( + Button.builder(toggleLabel("R", replicatableOnly)) { + replicatableOnly = !replicatableOnly + replicatableButton.message = toggleLabel("R", replicatableOnly) + indexScroll = 0 + }.bounds(matterOnlyButton.x + matterOnlyButton.width + 4, searchBox.y, 34, 18).build(), + ) + disintegratableButton = addRenderableWidget( + Button.builder(toggleLabel("D", disintegratableOnly)) { + disintegratableOnly = !disintegratableOnly + disintegratableButton.message = toggleLabel("D", disintegratableOnly) + indexScroll = 0 + }.bounds(replicatableButton.x + replicatableButton.width + 4, searchBox.y, 34, 18).build(), + ) + } + + override fun tick() { + searchBox.tick() + } + + override fun render(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) { + hoveredStack = ItemStack.EMPTY + hoveredAmountTooltip = emptyList() + val layout = layout() + guiGraphics.fill(0, 0, width, height, 0xAA050A0A.toInt()) + renderPanel(guiGraphics, layout.index) + renderPanel(guiGraphics, layout.focus) + renderTitle(guiGraphics, layout) + searchBox.render(guiGraphics, mouseX, mouseY, partialTick) + renderControls(guiGraphics, mouseX, mouseY, partialTick) + + val visible = visibleEntries() + if (selectedItemId == null || entries.none { it.record.itemId == selectedItemId }) { + selectedItemId = visible.firstOrNull()?.record?.itemId ?: entries.firstOrNull()?.record?.itemId + } + + renderIndex(guiGraphics, layout, visible, mouseX, mouseY) + renderFocus(guiGraphics, layout, mouseX, mouseY) + + if (!hoveredStack.isEmpty) { + guiGraphics.renderTooltip(font, hoveredStack, mouseX, mouseY) + } else if (hoveredAmountTooltip.isNotEmpty()) { + guiGraphics.renderTooltip(font, hoveredAmountTooltip.map { it.visualOrderText }, mouseX, mouseY) + } + } + + override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { + if (button == 0 && clickDisplayScaleControl(layout(), mouseX.toInt(), mouseY.toInt())) return true + if (super.mouseClicked(mouseX, mouseY, button)) return true + if (button != 0) return false + + val layout = layout() + val visible = visibleEntries() + if (startScrollbarDrag(layout, visible, mouseX.toInt(), mouseY.toInt())) return true + if (clickHeader(layout, mouseX.toInt(), mouseY.toInt())) return true + + val rowArea = indexRows(layout) + if (mouseX.toInt() in rowArea.x until rowArea.x + rowArea.w && mouseY.toInt() in rowArea.y until rowArea.y + rowArea.h) { + val index = indexScroll + (mouseY.toInt() - rowArea.y) / ROW_H + visible.getOrNull(index)?.let { + selectedItemId = it.record.itemId + equivalentScroll = 0 + return true + } + } + + equivalentAt(layout, mouseX.toInt(), mouseY.toInt())?.let { + selectedItemId = it.record.itemId + equivalentScroll = 0 + return true + } + + return false + } + + override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, dragX: Double, dragY: Double): Boolean { + if (button == 0 && dragScrollbar(mouseY.toInt())) return true + return super.mouseDragged(mouseX, mouseY, button, dragX, dragY) + } + + override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean { + if (button == 0 && draggingScrollbar != null) { + draggingScrollbar = null + return true + } + return super.mouseReleased(mouseX, mouseY, button) + } + + override fun mouseScrolled(mouseX: Double, mouseY: Double, delta: Double): Boolean { + val layout = layout() + if (contains(indexRows(layout), mouseX.toInt(), mouseY.toInt())) { + val visibleRows = max(1, indexRows(layout).h / ROW_H) + val maxScroll = max(0, visibleEntries().size - visibleRows) + indexScroll = (indexScroll - (delta * 3).toInt()).coerceIn(0, maxScroll) + return true + } + if (contains(equivalentRect(layout), mouseX.toInt(), mouseY.toInt())) { + val columns = equivalentColumns(layout) + val rows = max(1, equivalentRect(layout).h / SLOT_STEP) + val maxScroll = max(0, (equivalentEntries().size + columns - 1) / columns - rows) + equivalentScroll = (equivalentScroll - delta.toInt()).coerceIn(0, maxScroll) + return true + } + return super.mouseScrolled(mouseX, mouseY, delta) + } + + override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + if (keyCode == GLFW.GLFW_KEY_UP) { + return moveSelection(-1) + } + if (keyCode == GLFW.GLFW_KEY_DOWN) { + return moveSelection(1) + } + if (searchBox.keyPressed(keyCode, scanCode, modifiers)) { + indexScroll = 0 + return true + } + return super.keyPressed(keyCode, scanCode, modifiers) + } + + override fun charTyped(codePoint: Char, modifiers: Int): Boolean { + if (searchBox.charTyped(codePoint, modifiers)) { + indexScroll = 0 + return true + } + return super.charTyped(codePoint, modifiers) + } + + override fun onClose() { + minecraft?.setScreen(parent) + } + + override fun isPauseScreen(): Boolean = false + + private fun renderTitle(guiGraphics: GuiGraphics, layout: Layout) { + guiGraphics.drawString(font, "Replicated Integration", layout.index.x + 28, layout.index.y + 14, 0xDDEFE7, true) + drawRiMark(guiGraphics, layout.index.x + 6, layout.index.y + 11) + } + + private fun renderControls(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) { + matterOnlyButton.render(guiGraphics, mouseX, mouseY, partialTick) + replicatableButton.render(guiGraphics, mouseX, mouseY, partialTick) + disintegratableButton.render(guiGraphics, mouseX, mouseY, partialTick) + renderDisplayScaleControl(guiGraphics, layout(), mouseX, mouseY) + } + + private fun renderIndex(guiGraphics: GuiGraphics, layout: Layout, visible: List, mouseX: Int, mouseY: Int) { + val rows = indexRows(layout) + val columns = columns(layout) + renderHeader(guiGraphics, columns, rows.y - HEADER_H) + + val visibleRows = max(1, rows.h / ROW_H) + indexScroll = indexScroll.coerceIn(0, max(0, visible.size - visibleRows)) + val page = visible.drop(indexScroll).take(visibleRows) + + for ((rowIndex, entry) in page.withIndex()) { + val y = rows.y + rowIndex * ROW_H + val selected = entry.record.itemId == selectedItemId + if (selected) { + guiGraphics.fill(rows.x, y, rows.x + rows.w, y + ROW_H, 0x7736D84A) + guiGraphics.fill(rows.x, y, rows.x + 4, y + ROW_H, 0xFF44E35A.toInt()) + } else if (rowIndex % 2 == 0) { + guiGraphics.fill(rows.x, y, rows.x + rows.w, y + ROW_H, 0x33000000) + } + drawHorizontalLine(guiGraphics, rows.x, rows.x + rows.w, y + ROW_H, 0x335E716D) + guiGraphics.renderItem(entry.stack, columns.item.x + 8, y + 5) + if (mouseX in columns.item.x + 8 until columns.item.x + 24 && mouseY in y + 5 until y + 21) { + hoveredStack = entry.stack + } + drawItemName(guiGraphics, entry.record.displayName, columns.item.x + 30, y + 9, columns.item.w - 34, 0xE6ECE8) + drawTableTotalAmount(guiGraphics, formatAmount(entry.record.total), columns.total.x + columns.total.w - TOTAL_VALUE_RIGHT_PADDING, y + 9, 0xE6ECE8) + if (contains(Rect(columns.total.x, y, columns.total.w, ROW_H), mouseX, mouseY)) { + setAmountTooltip("Total", entry.record.total) + } + for ((matterId, rect) in columns.matter) { + val amount = entry.record.matterValues[matterId] ?: 0.0 + drawTableMatterAmount(guiGraphics, formatAmount(amount), rect.x + rect.w / 2, y + 9, 0xE6ECE8) + if (contains(Rect(rect.x, y, rect.w, ROW_H), mouseX, mouseY)) { + setAmountTooltip(matterId, amount) + } + } + } + + drawScrollbar(guiGraphics, rows.x + rows.w - 5, rows.y, rows.h, visible.size, visibleRows, indexScroll) + drawScaled(guiGraphics, "${visible.size} items", layout.index.x + 16, layout.index.y + layout.index.h - 18, 0xAAB8B2, false) + } + + private fun renderHeader(guiGraphics: GuiGraphics, columns: Columns, y: Int) { + val h = HEADER_H + guiGraphics.fill(columns.item.x, y, columns.endX, y + h, 0x44000000) + drawCell(guiGraphics, columns.item, y, h) + drawCell(guiGraphics, columns.total, y, h) + drawScaled(guiGraphics, headerText("Item", MatterIndexSort.ITEM_COLUMN), columns.item.x + 10, y + 10, 0xBFD0CA, false) + drawCentered(guiGraphics, headerText("Total", MatterIndexSort.TOTAL_COLUMN), columns.total.x + columns.total.w / 2, y + 10, 0xBFD0CA) + for ((matterId, rect) in columns.matter) { + drawCell(guiGraphics, rect, y, h) + renderMatterIcon(guiGraphics, matterId, rect.x + rect.w / 2 - 8, y + 7) + if (sort.column == MatterIndexSort.matterColumn(matterId)) { + drawScaled(guiGraphics, sortArrow(), rect.x + rect.w - scaledWidth(sortArrow()) - 3, y + 10, 0x7CF47B, false) + } + } + } + + private fun renderFocus(guiGraphics: GuiGraphics, layout: Layout, mouseX: Int, mouseY: Int) { + val selected = selectedEntry() + drawScaled(guiGraphics, "Selected Item", layout.focus.x + 16, layout.focus.y + 14, 0xDDEFE7, true) + if (selected == null) { + drawScaled(guiGraphics, "No matter data", layout.focus.x + 16, layout.focus.y + 42, 0xAAB8B2, false) + return + } + + val slotX = layout.focus.x + 18 + val slotY = layout.focus.y + 42 + drawSlot(guiGraphics, slotX, slotY, 54, true) + guiGraphics.renderItem(selected.stack, slotX + 19, slotY + 19) + if (mouseX in slotX until slotX + 54 && mouseY in slotY until slotY + 54) { + hoveredStack = selected.stack + } + drawTrimmed(guiGraphics, selected.record.displayName, slotX + 70, slotY + 4, layout.focus.w - 100, 0xE6ECE8) + drawTrimmed(guiGraphics, selected.record.itemId, slotX + 70, slotY + 18, layout.focus.w - 100, 0x94A29C) + + val focusMatter = focusMatterLayout(layout, slotX, slotY) + for ((index, matterId) in matterTypes.withIndex()) { + val column = index % focusMatter.columns + val row = index / focusMatter.columns + val x = focusMatter.x + column * focusMatter.cellW + val y = focusMatter.y + row * FOCUS_MATTER_ROW_H + val amount = selected.record.matterValues[matterId] ?: 0.0 + drawFocusMatterAmount(guiGraphics, matterId, formatAmount(amount), x, y, focusMatter.cellW, focusMatterAmountColor(amount)) + if (mouseX in x until x + focusMatter.cellW && mouseY in y until y + FOCUS_MATTER_ROW_H) { + setAmountTooltip(matterId, amount) + } + } + + val badgeY = max(slotY + 82, focusMatter.y + focusMatter.rows * FOCUS_MATTER_ROW_H + 6) + drawBadge(guiGraphics, slotX, badgeY, "Replicatable", selected.record.replicatable) + drawBadge(guiGraphics, slotX + scaledPixels(104), badgeY, "Disintegratable", selected.record.disintegratable) + drawDivider(guiGraphics, layout.focus.x + 12, badgeY + 32, layout.focus.w - 24) + + val eq = equivalentRect(layout) + drawScaled(guiGraphics, "Matter Equivalent", eq.x, eq.y - 15, 0xDDEFE7, false) + renderEquivalentGrid(guiGraphics, layout, mouseX, mouseY) + } + + private fun renderEquivalentGrid(guiGraphics: GuiGraphics, layout: Layout, mouseX: Int, mouseY: Int) { + val rect = equivalentRect(layout) + guiGraphics.fill(rect.x, rect.y, rect.x + rect.w, rect.y + rect.h, 0x66000000) + val columns = equivalentColumns(layout) + val rows = max(1, rect.h / SLOT_STEP) + val equivalent = equivalentEntries() + equivalentScroll = equivalentScroll.coerceIn(0, max(0, (equivalent.size + columns - 1) / columns - rows)) + val first = equivalentScroll * columns + for (index in 0 until min(equivalent.size - first, columns * rows)) { + val entry = equivalent[first + index] + val x = rect.x + 8 + (index % columns) * SLOT_STEP + val y = rect.y + 8 + (index / columns) * SLOT_STEP + val selected = entry.record.itemId == selectedItemId + drawSlot(guiGraphics, x, y, SLOT_SIZE, selected) + guiGraphics.renderItem(entry.stack, x + 5, y + 5) + if (mouseX in x until x + SLOT_SIZE && mouseY in y until y + SLOT_SIZE) { + hoveredStack = entry.stack + } + } + drawScrollbar(guiGraphics, rect.x + rect.w - 5, rect.y, rect.h, (equivalent.size + columns - 1) / columns, rows, equivalentScroll) + } + + private fun clickHeader(layout: Layout, mouseX: Int, mouseY: Int): Boolean { + val rows = indexRows(layout) + val y = rows.y - HEADER_H + if (mouseY !in y until y + HEADER_H) return false + val columns = columns(layout) + val column = + when { + contains(columns.item, mouseX, mouseY) -> MatterIndexSort.ITEM_COLUMN + contains(columns.total, mouseX, mouseY) -> MatterIndexSort.TOTAL_COLUMN + else -> columns.matter.firstNotNullOfOrNull { (matterId, rect) -> + if (contains(rect, mouseX, mouseY)) MatterIndexSort.matterColumn(matterId) else null + } + } ?: return false + sort = + if (sort.column == column) { + sort.copy(direction = if (sort.direction == MatterIndexSortDirection.ASC) MatterIndexSortDirection.DESC else MatterIndexSortDirection.ASC) + } else { + MatterIndexSort(column, MatterIndexSortDirection.DESC) + } + indexScroll = 0 + return true + } + + private fun equivalentAt(layout: Layout, mouseX: Int, mouseY: Int): MatterIndexEntry? { + val rect = equivalentRect(layout) + if (!contains(rect, mouseX, mouseY)) return null + val columns = equivalentColumns(layout) + val localX = mouseX - rect.x - 8 + val localY = mouseY - rect.y - 8 + if (localX < 0 || localY < 0) return null + val column = localX / SLOT_STEP + val row = localY / SLOT_STEP + if (column !in 0 until columns) return null + if (localX % SLOT_STEP >= SLOT_SIZE || localY % SLOT_STEP >= SLOT_SIZE) return null + return equivalentEntries().getOrNull((equivalentScroll + row) * columns + column) + } + + private fun startScrollbarDrag(layout: Layout, visible: List, mouseX: Int, mouseY: Int): Boolean { + val bar = scrollbarAt(layout, visible, mouseX, mouseY) ?: return false + val thumb = scrollbarThumb(bar) ?: return false + val thumbOffset = + if (contains(thumb.inflate(SCROLLBAR_HIT_PAD), mouseX, mouseY)) { + mouseY - thumb.y + } else { + thumb.h / 2 + } + draggingScrollbar = DraggingScrollbar(bar.target, thumbOffset) + setScrollbarFromThumbTop(bar, mouseY - thumbOffset) + return true + } + + private fun dragScrollbar(mouseY: Int): Boolean { + val dragging = draggingScrollbar ?: return false + val bar = scrollbarForTarget(layout(), visibleEntries(), dragging.target) ?: return false + setScrollbarFromThumbTop(bar, mouseY - dragging.thumbOffset) + return true + } + + private fun scrollbarAt(layout: Layout, visible: List, mouseX: Int, mouseY: Int): Scrollbar? = + listOfNotNull( + scrollbarForTarget(layout, visible, ScrollbarTarget.INDEX), + scrollbarForTarget(layout, visible, ScrollbarTarget.EQUIVALENT), + ).firstOrNull { contains(scrollbarHitRect(it), mouseX, mouseY) && it.total > it.visible } + + private fun scrollbarForTarget(layout: Layout, visible: List, target: ScrollbarTarget): Scrollbar? = + when (target) { + ScrollbarTarget.INDEX -> { + val rows = indexRows(layout) + Scrollbar( + target = target, + x = rows.x + rows.w - 5, + y = rows.y, + h = rows.h, + total = visible.size, + visible = max(1, rows.h / ROW_H), + scroll = indexScroll, + ) + } + ScrollbarTarget.EQUIVALENT -> { + val rect = equivalentRect(layout) + val columns = equivalentColumns(layout) + Scrollbar( + target = target, + x = rect.x + rect.w - 5, + y = rect.y, + h = rect.h, + total = (equivalentEntries().size + columns - 1) / columns, + visible = max(1, rect.h / SLOT_STEP), + scroll = equivalentScroll, + ) + } + } + + private fun scrollbarHitRect(bar: Scrollbar): Rect = + Rect(bar.x - SCROLLBAR_HIT_PAD, bar.y, SCROLLBAR_W + SCROLLBAR_HIT_PAD * 2, bar.h) + + private fun scrollbarThumb(bar: Scrollbar): Rect? { + if (bar.total <= bar.visible) return null + val thumbH = max(16, bar.h * bar.visible / bar.total) + val thumbY = bar.y + ((bar.h - thumbH) * bar.scroll / max(1, bar.total - bar.visible)) + return Rect(bar.x, thumbY, SCROLLBAR_W, thumbH) + } + + private fun setScrollbarFromThumbTop(bar: Scrollbar, thumbTop: Int) { + val thumb = scrollbarThumb(bar) ?: return + val maxScroll = max(0, bar.total - bar.visible) + val track = max(1, bar.h - thumb.h) + val local = (thumbTop - bar.y).coerceIn(0, track) + val scroll = ((local * maxScroll.toFloat()) / track.toFloat()).toInt().coerceIn(0, maxScroll) + when (bar.target) { + ScrollbarTarget.INDEX -> indexScroll = scroll + ScrollbarTarget.EQUIVALENT -> equivalentScroll = scroll + } + } + + private fun moveSelection(delta: Int): Boolean { + val visible = visibleEntries() + if (visible.isEmpty()) return false + val current = visible.indexOfFirst { it.record.itemId == selectedItemId } + val target = + if (current < 0) { + if (delta < 0) visible.lastIndex else 0 + } else { + (current + delta).coerceIn(0, visible.lastIndex) + } + selectedItemId = visible[target].record.itemId + equivalentScroll = 0 + keepSelectionVisible(target, visible.size) + return true + } + + private fun keepSelectionVisible(index: Int, total: Int) { + val visibleRows = max(1, indexRows(layout()).h / ROW_H) + indexScroll = + when { + index < indexScroll -> index + index >= indexScroll + visibleRows -> index - visibleRows + 1 + else -> indexScroll + }.coerceIn(0, max(0, total - visibleRows)) + } + + private fun visibleEntries(): List { + val query = MatterIndexFiltering.parseQuery(searchBox.value, matterOnly, replicatableOnly, disintegratableOnly) + val ordered = MatterIndexFiltering.filterAndSort(entries.map { it.record }, query, sort) + val byId = entries.associateBy { it.record.itemId } + return ordered.mapNotNull { byId[it.itemId] } + } + + private fun selectedEntry(): MatterIndexEntry? = + entries.firstOrNull { it.record.itemId == selectedItemId } + + private fun equivalentEntries(): List { + val records = MatterIndexFiltering.equivalentRecords(entries.map { it.record }, selectedEntry()?.record) + val byId = entries.associateBy { it.record.itemId } + return records.mapNotNull { byId[it.itemId] } + } + + private fun columns(layout: Layout): Columns { + val rows = indexRows(layout) + val itemW = max(scaledPixels(126), (rows.w * ITEM_COLUMN_WIDTH_RATIO).toInt()) + val totalW = scaledPixels(52) + val matterW = max(tableMatterColumnMinWidth(), (rows.w - itemW - totalW) / max(1, matterTypes.size)) + val matter = matterTypes.mapIndexed { index, id -> + id to Rect(rows.x + itemW + totalW + index * matterW, rows.y - HEADER_H, matterW, HEADER_H) + }.filter { it.second.x + it.second.w <= rows.x + rows.w } + return Columns( + item = Rect(rows.x, rows.y - HEADER_H, itemW, HEADER_H), + total = Rect(rows.x + itemW, rows.y - HEADER_H, totalW, HEADER_H), + matter = matter, + endX = rows.x + rows.w, + ) + } + + private fun layout(): Layout { + val margin = 24 + val gap = 6 + val panelH = height - margin * 2 + val totalW = width - margin * 2 - gap + val defaultIndexW = (totalW * 0.60).toInt() + val maxIndexW = max(0, totalW - FOCUS_MIN_W) + val indexW = min(maxIndexW, max(defaultIndexW, requiredIndexPanelWidth())) + val focusW = totalW - indexW + return Layout( + index = Rect(margin, margin, indexW, panelH), + focus = Rect(margin + indexW + gap, margin, focusW, panelH), + ) + } + + private fun requiredIndexPanelWidth(): Int { + val matterCount = max(1, matterTypes.size) + val denominator = 1.0 - ITEM_COLUMN_WIDTH_RATIO + val fixedWidth = scaledPixels(52) + matterCount * tableMatterColumnMinWidth() + val rowsW = max(scaledPixels(126) + fixedWidth, ceil(fixedWidth / denominator).toInt()) + return rowsW + INDEX_HORIZONTAL_PADDING + } + + private fun indexRows(layout: Layout): Rect = + Rect(layout.index.x + 14, layout.index.y + 88, layout.index.w - 28, layout.index.h - 124) + + private fun equivalentRect(layout: Layout): Rect { + val y = focusEquivalentY(layout) + return Rect(layout.focus.x + 16, y, layout.focus.w - 32, max(0, layout.focus.y + layout.focus.h - y - 20)) + } + + private fun equivalentColumns(layout: Layout): Int = + max(2, (equivalentRect(layout).w - 12) / SLOT_STEP) + + private fun focusEquivalentY(layout: Layout): Int { + val focusMatter = focusMatterLayout(layout, layout.focus.x + 18, layout.focus.y + 42) + val badgeY = max(layout.focus.y + 124, focusMatter.y + focusMatter.rows * FOCUS_MATTER_ROW_H + 6) + return badgeY + 60 + } + + private fun focusMatterLayout(layout: Layout, slotX: Int, slotY: Int): FocusMatterLayout { + val x = slotX + 70 + val availableW = max(FOCUS_MATTER_MIN_CELL_W, layout.focus.x + layout.focus.w - 8 - x) + val columns = min(FOCUS_MATTER_COLUMNS, matterTypes.size.coerceAtLeast(1)) + val rows = (matterTypes.size + columns - 1) / columns + return FocusMatterLayout( + x = x, + y = slotY + 36, + cellW = availableW / columns, + columns = columns, + rows = rows, + ) + } + + private fun renderPanel(guiGraphics: GuiGraphics, rect: Rect) { + guiGraphics.fill(rect.x, rect.y, rect.x + rect.w, rect.y + rect.h, 0xCC071010.toInt()) + drawBorder(guiGraphics, rect, 0xFF41514D.toInt()) + drawBorder(guiGraphics, rect.inflate(-2), 0x6625C34A) + } + + private fun drawCell(guiGraphics: GuiGraphics, rect: Rect, y: Int, h: Int) { + drawVerticalLine(guiGraphics, rect.x, y, y + h, 0x445E716D) + drawVerticalLine(guiGraphics, rect.x + rect.w, y, y + h, 0x445E716D) + } + + private fun drawSlot(guiGraphics: GuiGraphics, x: Int, y: Int, size: Int, selected: Boolean) { + val edge = if (selected) 0xFF58E65B.toInt() else 0xFF59625F.toInt() + guiGraphics.fill(x, y, x + size, y + size, edge) + guiGraphics.fill(x + 1, y + 1, x + size - 1, y + size - 1, 0xFF131918.toInt()) + guiGraphics.fill(x + 2, y + 2, x + size - 2, y + size - 2, 0xFF202826.toInt()) + guiGraphics.fill(x + 3, y + 3, x + size - 3, y + size - 3, 0xFF0D1211.toInt()) + } + + private fun drawBadge(guiGraphics: GuiGraphics, x: Int, y: Int, text: String, active: Boolean) { + val w = scaledPixels(100) + val h = scaledPixels(22) + val fill = if (active) 0x5517E517 else 0x55E03232 + val border = if (active) 0xAA36D84A.toInt() else 0xAAC74949.toInt() + val color = if (active) 0x70F06E else 0xF07171 + val label = if (active) text else "Not $text" + guiGraphics.fill(x, y, x + w, y + h, fill) + drawBorder(guiGraphics, Rect(x, y, w, h), border) + drawCentered(guiGraphics, label, x + w / 2, y + max(1, (h - scaledPixels(font.lineHeight)) / 2), color) + } + + private fun renderMatterIcon(guiGraphics: GuiGraphics, matterId: String, x: Int, y: Int) { + guiGraphics.blit(ResourceLocation.fromNamespaceAndPath(Replication.MOD_ID, "textures/gui/mattertypes/${matterId.substringAfter(':')}.png"), x, y, 0f, 0f, 16, 16, 16, 16) + } + + private fun renderMatterIcon(guiGraphics: GuiGraphics, matterId: String, x: Int, y: Int, size: Int) { + val scale = size / 16f + guiGraphics.pose().pushPose() + guiGraphics.pose().translate(x.toFloat(), y.toFloat(), 0f) + guiGraphics.pose().scale(scale, scale, 1f) + renderMatterIcon(guiGraphics, matterId, 0, 0) + guiGraphics.pose().popPose() + } + + private fun renderMatterIcon(guiGraphics: GuiGraphics, matterId: String, x: Int, y: Int, size: Int, muted: Boolean) { + if (!muted) { + renderMatterIcon(guiGraphics, matterId, x, y, size) + return + } + guiGraphics.setColor(0.56f, 0.63f, 0.60f, 0.70f) + renderMatterIcon(guiGraphics, matterId, x, y, size) + guiGraphics.setColor(1f, 1f, 1f, 1f) + } + + private fun drawRiMark(guiGraphics: GuiGraphics, x: Int, y: Int) { + guiGraphics.fill(x + 5, y, x + 9, y + 4, 0xFF42E84F.toInt()) + guiGraphics.fill(x, y + 5, x + 4, y + 9, 0xFF42E84F.toInt()) + guiGraphics.fill(x + 10, y + 5, x + 14, y + 9, 0xFF42E84F.toInt()) + guiGraphics.fill(x + 5, y + 10, x + 9, y + 14, 0xFF42E84F.toInt()) + } + + private fun drawScrollbar(guiGraphics: GuiGraphics, x: Int, y: Int, h: Int, total: Int, visible: Int, scroll: Int) { + val bar = Scrollbar(ScrollbarTarget.INDEX, x, y, h, total, visible, scroll) + val thumb = scrollbarThumb(bar) ?: return + guiGraphics.fill(x, y, x + SCROLLBAR_W, y + h, 0xAA050606.toInt()) + guiGraphics.fill(thumb.x, thumb.y, thumb.x + thumb.w, thumb.y + thumb.h, 0xFF9EAAA5.toInt()) + } + + private fun drawDivider(guiGraphics: GuiGraphics, x: Int, y: Int, w: Int) = + guiGraphics.fill(x, y, x + w, y + 1, 0x6641514D) + + private fun drawBorder(guiGraphics: GuiGraphics, rect: Rect, color: Int) { + drawHorizontalLine(guiGraphics, rect.x, rect.x + rect.w, rect.y, color) + drawHorizontalLine(guiGraphics, rect.x, rect.x + rect.w, rect.y + rect.h, color) + drawVerticalLine(guiGraphics, rect.x, rect.y, rect.y + rect.h, color) + drawVerticalLine(guiGraphics, rect.x + rect.w, rect.y, rect.y + rect.h, color) + } + + private fun drawHorizontalLine(guiGraphics: GuiGraphics, x1: Int, x2: Int, y: Int, color: Int) = + guiGraphics.fill(x1, y, x2, y + 1, color) + + private fun drawVerticalLine(guiGraphics: GuiGraphics, x: Int, y1: Int, y2: Int, color: Int) = + guiGraphics.fill(x, y1, x + 1, y2, color) + + private fun drawItemName(guiGraphics: GuiGraphics, text: String, x: Int, y: Int, maxWidth: Int, color: Int) = + drawTrimmed(guiGraphics, text, x, y, maxWidth, color, itemNameScale()) + + private fun drawTableMatterAmount(guiGraphics: GuiGraphics, text: String, centerX: Int, y: Int, color: Int) { + val scale = tableMatterAmountScale() + drawScaled(guiGraphics, text, centerX - scaledWidth(text, scale) / 2, y, color, false, scale) + } + + private fun drawTableTotalAmount(guiGraphics: GuiGraphics, text: String, rightX: Int, y: Int, color: Int) { + val scale = tableMatterAmountScale() + drawScaled(guiGraphics, text, rightX - scaledWidth(text, scale), y, color, false, scale) + } + + private fun drawFocusMatterAmount(guiGraphics: GuiGraphics, matterId: String, text: String, x: Int, y: Int, cellW: Int, color: Int) { + val textMaxW = cellW - FOCUS_MATTER_ICON_SIZE - FOCUS_MATTER_VALUE_GAP + if (textMaxW <= 0) return + val scale = min(focusMatterAmountScale(), textMaxW.toFloat() / max(1, font.width(text))).coerceAtLeast(0.35f) + val textH = ceil(font.lineHeight * scale).toInt() + val pairX = x + val iconY = y + (FOCUS_MATTER_ROW_H - FOCUS_MATTER_ICON_SIZE) / 2 + val textX = pairX + FOCUS_MATTER_ICON_SIZE + FOCUS_MATTER_VALUE_GAP + val textY = y + (FOCUS_MATTER_ROW_H - textH) / 2 + renderMatterIcon(guiGraphics, matterId, pairX, iconY, FOCUS_MATTER_ICON_SIZE, color == FOCUS_MATTER_ZERO_COLOR) + drawScaled(guiGraphics, text, textX, textY, color, false, scale) + } + + private fun focusMatterAmountColor(amount: Double): Int = + if (amount == 0.0) FOCUS_MATTER_ZERO_COLOR else 0xE6ECE8 + + private fun drawTrimmed(guiGraphics: GuiGraphics, text: String, x: Int, y: Int, maxWidth: Int, color: Int) = + drawTrimmed(guiGraphics, text, x, y, maxWidth, color, displayScale()) + + private fun drawTrimmed(guiGraphics: GuiGraphics, text: String, x: Int, y: Int, maxWidth: Int, color: Int, scale: Float) { + if (maxWidth <= 0) return + val logicalMaxWidth = max(1, (maxWidth / scale).toInt()) + val ellipsis = "..." + val rendered = + if (font.width(text) <= logicalMaxWidth) { + text + } else if (logicalMaxWidth <= font.width(ellipsis)) { + font.plainSubstrByWidth(text, logicalMaxWidth) + } else { + "${font.plainSubstrByWidth(text, logicalMaxWidth - font.width(ellipsis))}$ellipsis" + } + drawScaled(guiGraphics, rendered, x, y, color, false, scale) + } + + private fun drawRight(guiGraphics: GuiGraphics, text: String, rightX: Int, y: Int, color: Int) = + drawScaled(guiGraphics, text, rightX - scaledWidth(text), y, color, false) + + private fun drawCentered(guiGraphics: GuiGraphics, text: String, centerX: Int, y: Int, color: Int) = + drawScaled(guiGraphics, text, centerX - scaledWidth(text) / 2, y, color, false) + + private fun drawCentered(guiGraphics: GuiGraphics, text: String, centerX: Int, y: Int, color: Int, scale: Float) = + drawScaled(guiGraphics, text, centerX - scaledWidth(text, scale) / 2, y, color, false, scale) + + private fun drawScaled(guiGraphics: GuiGraphics, text: String, x: Int, y: Int, color: Int, shadow: Boolean) = + drawScaled(guiGraphics, text, x, y, color, shadow, displayScale()) + + private fun drawScaled(guiGraphics: GuiGraphics, text: String, x: Int, y: Int, color: Int, shadow: Boolean, scale: Float) { + if (scale == 1f) { + guiGraphics.drawString(font, text, x, y, color, shadow) + return + } + guiGraphics.pose().pushPose() + guiGraphics.pose().translate(x.toFloat(), y.toFloat(), 0f) + guiGraphics.pose().scale(scale, scale, 1f) + guiGraphics.drawString(font, text, 0, 0, color, shadow) + guiGraphics.pose().popPose() + } + + private fun scaledWidth(text: String): Int = + scaledWidth(text, displayScale()) + + private fun scaledWidth(text: String, scale: Float): Int = + ceil(font.width(text) * scale).toInt() + + private fun scaledPixels(value: Int): Int = + MatterIndexDisplayScale.scalePixels(value, displayScalePercent) + + private fun displayScale(): Float = + MatterIndexDisplayScale.toScale(displayScalePercent) + + private fun itemNameScale(): Float = + displayScale() * ITEM_NAME_SCALE + + private fun tableMatterAmountScale(): Float = + displayScale() * AMOUNT_SCALE + + private fun focusMatterAmountScale(): Float = + displayScale() * FOCUS_MATTER_AMOUNT_SCALE + + private fun tableMatterColumnMinWidth(): Int = + scaledWidth(MATTER_AMOUNT_WIDTH_SAMPLE, tableMatterAmountScale()) + MATTER_AMOUNT_HORIZONTAL_PADDING + + private fun adjustDisplayScale(steps: Int) { + displayScalePercent = MatterIndexDisplayScale.adjust(displayScalePercent, steps) + } + + private fun renderDisplayScaleControl(guiGraphics: GuiGraphics, layout: Layout, mouseX: Int, mouseY: Int) { + val control = displayScaleControl(layout) + drawScaleButton(guiGraphics, control.minus, "-", displayScalePercent > MatterIndexDisplayScale.MIN_PERCENT, contains(control.minus, mouseX, mouseY)) + drawScaleButton(guiGraphics, control.plus, "+", displayScalePercent < MatterIndexDisplayScale.MAX_PERCENT, contains(control.plus, mouseX, mouseY)) + val label = MatterIndexDisplayScale.label(displayScalePercent) + guiGraphics.drawString(font, label, control.label.x + (control.label.w - font.width(label)) / 2, control.label.y + 4, 0xBFD0CA, false) + } + + private fun drawScaleButton(guiGraphics: GuiGraphics, rect: Rect, text: String, enabled: Boolean, hovered: Boolean) { + val fill = + when { + !enabled -> 0x33151918 + hovered -> 0x6636D84A + else -> 0x44151918 + } + val color = if (enabled) 0xDDEFE7 else 0x66706F6A + guiGraphics.fill(rect.x, rect.y, rect.x + rect.w, rect.y + rect.h, fill) + drawBorder(guiGraphics, rect, if (enabled) 0x8841514D.toInt() else 0x4441514D) + guiGraphics.drawString(font, text, rect.x + (rect.w - font.width(text)) / 2, rect.y + 4, color, false) + } + + private fun clickDisplayScaleControl(layout: Layout, mouseX: Int, mouseY: Int): Boolean { + val control = displayScaleControl(layout) + if (contains(control.minus, mouseX, mouseY)) { + adjustDisplayScale(-1) + return true + } + if (contains(control.plus, mouseX, mouseY)) { + adjustDisplayScale(1) + return true + } + return false + } + + private fun displayScaleControl(layout: Layout): DisplayScaleControl { + val x = max(layout.index.x + 16, layout.index.x + layout.index.w - 82) + val y = layout.index.y + 11 + return DisplayScaleControl( + minus = Rect(x, y, 16, 16), + label = Rect(x + 18, y, 44, 16), + plus = Rect(x + 64, y, 16, 16), + ) + } + + private fun headerText(text: String, column: String): String = + if (sort.column == column) "$text ${sortArrow()}" else text + + private fun sortArrow(): String = + if (sort.direction == MatterIndexSortDirection.ASC) "^" else "v" + + private fun toggleLabel(text: String, active: Boolean): Component = + Component.literal(if (active) "[$text]" else text) + + private fun formatAmount(amount: Double): String = + MatterAmountFormatter.compact(amount, ForgeUiConfig.matterAmountNotation()) + + private fun setAmountTooltip(label: String, amount: Double) { + hoveredAmountTooltip = + listOf( + Component.literal(label), + Component.literal(MatterAmountFormatter.exact(amount)), + ) + } + + private fun contains(rect: Rect, x: Int, y: Int): Boolean = + x in rect.x until rect.x + rect.w && y in rect.y until rect.y + rect.h + + companion object { + private const val HEADER_H = 30 + private const val ROW_H = 26 + private const val FOCUS_MIN_W = 220 + private const val INDEX_HORIZONTAL_PADDING = 28 + private const val ITEM_COLUMN_WIDTH_RATIO = 0.22 + private const val ITEM_NAME_SCALE = 0.90f + private const val AMOUNT_SCALE = 0.80f + private const val MATTER_AMOUNT_WIDTH_SAMPLE = "88.8Qa" + private const val MATTER_AMOUNT_HORIZONTAL_PADDING = 8 + private const val TOTAL_VALUE_RIGHT_PADDING = 8 + private const val FOCUS_MATTER_COLUMNS = 4 + private const val FOCUS_MATTER_MIN_CELL_W = 26 + private const val FOCUS_MATTER_ROW_H = 18 + private const val FOCUS_MATTER_ICON_SIZE = 8 + private const val FOCUS_MATTER_VALUE_GAP = 3 + private const val FOCUS_MATTER_AMOUNT_SCALE = 0.50f + private const val FOCUS_MATTER_ZERO_COLOR = 0x8EA09A + private const val SCROLLBAR_W = 4 + private const val SCROLLBAR_HIT_PAD = 3 + private const val SLOT_SIZE = 28 + private const val SLOT_STEP = 34 + + fun hasMatter(itemId: String): Boolean = + ClientReplicationCalculation.DEFAULT_MATTER_COMPOUND.containsKey(itemId) + + fun itemId(stack: ItemStack): String = + BuiltInRegistries.ITEM.getKey(stack.item).toString() + + private fun loadEntries(): List = + ClientReplicationCalculation.DEFAULT_MATTER_COMPOUND.entries.mapNotNull { (id, compound) -> + val item = BuiltInRegistries.ITEM.get(ResourceLocation.parse(id)) + val stack = item.defaultInstance + if (stack.isEmpty) return@mapNotNull null + val tags = stack.tags.map { "#${it.location()}" }.toList().joinToString(" ") + MatterIndexEntry( + record = MatterIndexRecord( + itemId = id, + displayName = stack.hoverName.string, + matterValues = compound.toMatterMap(), + replicatable = !stack.`is`(ReplicationTags.CANT_BE_SCANNED), + disintegratable = !stack.`is`(ReplicationTags.CANT_BE_DISINTEGRATED), + extraSearchText = tags, + ), + stack = stack, + ) + }.sortedBy { it.record.displayName.lowercase() } + + private fun MatterCompound.toMatterMap(): Map = + values.values.associate { value -> + value.matter.name to value.amount + } + } + + private data class MatterIndexEntry( + val record: MatterIndexRecord, + val stack: ItemStack, + ) + + private data class Layout( + val index: Rect, + val focus: Rect, + ) + + private data class Columns( + val item: Rect, + val total: Rect, + val matter: List>, + val endX: Int, + ) + + private data class DisplayScaleControl( + val minus: Rect, + val label: Rect, + val plus: Rect, + ) + + private data class FocusMatterLayout( + val x: Int, + val y: Int, + val cellW: Int, + val columns: Int, + val rows: Int, + ) + + private enum class ScrollbarTarget { + INDEX, + EQUIVALENT, + } + + private data class DraggingScrollbar( + val target: ScrollbarTarget, + val thumbOffset: Int, + ) + + private data class Scrollbar( + val target: ScrollbarTarget, + val x: Int, + val y: Int, + val h: Int, + val total: Int, + val visible: Int, + val scroll: Int, + ) + + private data class Rect( + val x: Int, + val y: Int, + val w: Int, + val h: Int, + ) { + fun inflate(amount: Int): Rect = + Rect(x - amount, y - amount, w + amount * 2, h + amount * 2) + } +} diff --git a/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/command/MatterCommandTree.kt b/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/command/MatterCommandTree.kt index 0a11ffc..bb01de6 100644 --- a/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/command/MatterCommandTree.kt +++ b/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/command/MatterCommandTree.kt @@ -116,6 +116,12 @@ object MatterCommandTree { context, MatterCommandParsing.targetSelectorKey(context, target), ) + + MatterSelectorKind.MOD -> + MatterCommandActions.getSelector( + context, + MatterCommandParsing.targetSelectorKey(context, target), + ) } private fun targetSetSelector(target: ForgeMatterCommand): ArgumentBuilder = diff --git a/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/config/ForgeUiConfig.kt b/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/config/ForgeUiConfig.kt new file mode 100644 index 0000000..5ad6d6e --- /dev/null +++ b/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/config/ForgeUiConfig.kt @@ -0,0 +1,25 @@ +package com.p_nsk.replicated_integration.config + +import com.p_nsk.replicated_integration.api.client.MatterAmountNotation +import net.minecraftforge.common.ForgeConfigSpec + +object ForgeUiConfig { + private const val KEY_MATTER_AMOUNT_NOTATION = "matterAmountNotation" + + val spec: ForgeConfigSpec + private val matterAmountNotationValue: ForgeConfigSpec.EnumValue + + init { + val builder = ForgeConfigSpec.Builder() + builder.push("matterIndex") + matterAmountNotationValue = + builder + .comment("Notation used for compact matter amounts. SI uses K/M/G/T; SHORT_SCALE uses K/M/B/T.") + .translation("replicated_integration.config.ui.matter_amount_notation") + .defineEnum(KEY_MATTER_AMOUNT_NOTATION, MatterAmountNotation.SHORT_SCALE) + builder.pop() + spec = builder.build() + } + + fun matterAmountNotation(): MatterAmountNotation = matterAmountNotationValue.get() +} diff --git a/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/core/Aliases.kt b/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/core/Aliases.kt index 77d7bd1..0eccacf 100644 --- a/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/core/Aliases.kt +++ b/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/core/Aliases.kt @@ -22,6 +22,8 @@ fun ForgeMatterNodeRegistry.node( id = id, displayName = displayName, formatter = formatter, + enumerateNodes = builder.buildEnumerator(), + rawValue = builder.buildRawValue(), commands = builder.buildCommands(), ) ) diff --git a/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/core/ForgeMatterNodeBuilder.kt b/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/core/ForgeMatterNodeBuilder.kt index f27126d..1f64055 100644 --- a/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/core/ForgeMatterNodeBuilder.kt +++ b/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/core/ForgeMatterNodeBuilder.kt @@ -3,6 +3,7 @@ package com.p_nsk.replicated_integration.core import com.mojang.brigadier.suggestion.SuggestionProvider import com.p_nsk.replicated_integration.api.model.LiteResourceLocation import com.p_nsk.replicated_integration.api.node.MatterNodeCommands +import com.p_nsk.replicated_integration.api.node.NodeKey import com.p_nsk.replicated_integration.api.selector.MatterSelectorKind import net.minecraft.commands.CommandSourceStack import net.minecraft.resources.ResourceLocation @@ -13,6 +14,8 @@ class ForgeMatterNodeBuilder( ) { private var valueCommand: ForgeMatterCommand? = null private var tagCommand: ForgeMatterCommand? = null + private var enumerateNodes: () -> Iterable = { emptyList() } + private var rawValue: (LiteResourceLocation) -> Any? = { null } fun value( literal: String, @@ -50,10 +53,21 @@ class ForgeMatterNodeBuilder( ) } + fun enumerate(nodes: () -> Iterable) { + enumerateNodes = nodes + } + + fun rawValue(value: (LiteResourceLocation) -> Any?) { + rawValue = value + } + fun buildCommands(): MatterNodeCommands = MatterNodeCommands( value = valueCommand, tag = tagCommand, ) -} + fun buildEnumerator(): () -> Iterable = enumerateNodes + + fun buildRawValue(): (LiteResourceLocation) -> Any? = rawValue +} diff --git a/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/core/ForgeReplicationCalculationService.kt b/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/core/ForgeReplicationCalculationService.kt index 6d83df7..33258a0 100644 --- a/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/core/ForgeReplicationCalculationService.kt +++ b/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/core/ForgeReplicationCalculationService.kt @@ -6,6 +6,7 @@ import com.buuz135.replication.calculation.MatterValue import com.buuz135.replication.recipe.MatterValueRecipe import com.p_nsk.replicated_integration.Constants import com.p_nsk.replicated_integration.adapter.vanilla.BuiltinNodeResolver +import com.p_nsk.replicated_integration.adapter.mekanism.MekanismNodeResolver import com.p_nsk.replicated_integration.api.addon.ReplicationAddonLoadSafetyContract import com.p_nsk.replicated_integration.api.addon.ReplicationAddonRegistry import com.p_nsk.replicated_integration.api.graph.ConversionGraphBuilder @@ -17,9 +18,11 @@ import com.p_nsk.replicated_integration.api.node.MutableMatterDefaults import com.p_nsk.replicated_integration.api.node.NodeKey import com.p_nsk.replicated_integration.api.selector.MatterSelectorMaterializer import com.p_nsk.replicated_integration.api.selector.MutableMatterSelectors +import com.p_nsk.replicated_integration.api.selector.formatSelector import com.p_nsk.replicated_integration.data.ForgeMatterConfigOverrides import com.p_nsk.replicated_integration.data.ForgeMatterRuntimeOverrides import com.p_nsk.replicated_integration.debug.MatterNodeDebugCache +import com.p_nsk.replicated_integration.kubejs.ForgeKubeJSMatterRules import com.p_nsk.replicated_integration.network.ReplicationCalculationSyncChannel import com.p_nsk.replicated_integration.network.ReplicationCalculationSyncPacket import net.minecraft.nbt.CompoundTag @@ -40,6 +43,7 @@ object ForgeReplicationCalculationService { @Volatile private var latestSyncHash: Int = 0 + private val warnedKubeJSConfigOverlaps = mutableSetOf() private val addons: ReplicationAddonRegistry by lazy { ReplicationAddonRegistry(ForgeReplicationAddonCollector.addons()) } @@ -61,11 +65,24 @@ object ForgeReplicationCalculationService { addon.collectDefaults(context, defaults) addon.collectSelectors(context, selectors) } - selectors.putAll(ForgeMatterConfigOverrides.snapshot()) - selectors.putAll(ForgeMatterRuntimeOverrides.snapshot(server)) + val configRules = ForgeMatterConfigOverrides.snapshotRules() + val kubeJSRules = ForgeKubeJSMatterRules.snapshot() + warnKubeJSConfigOverlap(configRules, kubeJSRules) + selectors.putRules(configRules) + selectors.putRules(kubeJSRules) + selectors.putRules(ForgeMatterRuntimeOverrides.snapshotRules(server)) val selectorSnapshot = selectors.snapshot() - defaults.putAll(MatterSelectorMaterializer.materialize(selectorSnapshot, ::expandSelectorTag)) + val selectorRules = selectors.snapshotRules() + defaults.putAll( + MatterSelectorMaterializer.materialize( + selectorRules, + ::expandSelectorTag, + ::expandSelectorMod, + ::allMatterNodes, + ::rawValue, + ), + ) val explicitSnapshot = defaults.snapshot() for (addon in activeAddons) { addon.collectConversions(context, builder) @@ -191,8 +208,47 @@ object ForgeReplicationCalculationService { return when (type) { MatterNodes.ITEM -> BuiltinNodeResolver.itemNodesInTag(resourceId) MatterNodes.FLUID -> BuiltinNodeResolver.fluidNodesInTag(resourceId) + MatterNodes.GAS -> MekanismNodeResolver.gasNodesInTag(resourceId) + MatterNodes.INFUSE_TYPE -> MekanismNodeResolver.infuseTypeNodesInTag(resourceId) + MatterNodes.PIGMENT -> MekanismNodeResolver.pigmentNodesInTag(resourceId) + MatterNodes.SLURRY -> MekanismNodeResolver.slurryNodesInTag(resourceId) + else -> emptyList() + } + } + + private fun expandSelectorMod(type: LiteResourceLocation, modId: String): Iterable = + when (type) { + MatterNodes.ITEM -> BuiltinNodeResolver.itemNodesInMod(modId) + MatterNodes.FLUID -> BuiltinNodeResolver.fluidNodesInMod(modId) + MatterNodes.GAS -> MekanismNodeResolver.gasNodesInMod(modId) + MatterNodes.INFUSE_TYPE -> MekanismNodeResolver.infuseTypeNodesInMod(modId) + MatterNodes.PIGMENT -> MekanismNodeResolver.pigmentNodesInMod(modId) + MatterNodes.SLURRY -> MekanismNodeResolver.slurryNodesInMod(modId) else -> emptyList() } + + private fun allMatterNodes(): Iterable = + matterNodeRegistry().all().flatMap { it.enumerateNodes() } + + private fun rawValue(node: NodeKey): Any? = + matterNodeRegistry().get(node.type)?.rawValue?.invoke(node.id) + + private fun warnKubeJSConfigOverlap( + configRules: List, + kubeJSRules: List, + ) { + val configKeys = configRules.mapNotNullTo(linkedSetOf()) { it.selector.staticKey } + val kubeKeys = kubeJSRules.mapNotNullTo(linkedSetOf()) { it.selector.staticKey } + for (key in configKeys.intersect(kubeKeys)) { + val warningKey = key.toString() + if (!warnedKubeJSConfigOverlaps.add(warningKey)) { + continue + } + Constants.LOGGER.warn( + "Matter selector {} is defined by both config and KubeJS; KubeJS has higher source priority", + matterNodeRegistry().formatSelector(key), + ) + } } private fun LiteMatterCompound.toMatterCompound(): MatterCompound? { diff --git a/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/data/ForgeMatterConfigOverrides.kt b/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/data/ForgeMatterConfigOverrides.kt index 29b221f..550ff8e 100644 --- a/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/data/ForgeMatterConfigOverrides.kt +++ b/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/data/ForgeMatterConfigOverrides.kt @@ -2,18 +2,26 @@ package com.p_nsk.replicated_integration.data 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.selector.MatterRule import com.p_nsk.replicated_integration.api.selector.MatterSelectorKey import com.p_nsk.replicated_integration.api.selector.MatterSelectorStorageCodec +import com.p_nsk.replicated_integration.api.selector.asRuleSelector import net.minecraft.server.MinecraftServer import net.minecraftforge.fml.loading.FMLPaths import java.nio.file.Path object ForgeMatterConfigOverrides { private var loadedFrom: Path? = null - private var values: Map = emptyMap() + private var values: List = emptyList() @Synchronized fun snapshot(): Map { + loadIfNeeded() + return values.toStaticMap() + } + + @Synchronized + fun snapshotRules(): List { loadIfNeeded() return values } @@ -25,7 +33,7 @@ object ForgeMatterConfigOverrides { if (runtime.isEmpty()) { return 0 } - values = values.toMutableMap().also { it.putAll(runtime.mapValues { (_, value) -> value.asConfigValue() }) } + values += runtime.map { (selector, value) -> MatterRule(selector.asRuleSelector(), value.asConfigValue()) } save() ForgeMatterRuntimeOverrides.clear(server) return runtime.size @@ -37,12 +45,12 @@ object ForgeMatterConfigOverrides { return } loadedFrom = path - values = MatterSelectorStorageCodec.load(path, ExplicitMatterSource.CONFIG) + values = MatterSelectorStorageCodec.loadRules(path, ExplicitMatterSource.CONFIG) } private fun save() { val path = filePath() - MatterSelectorStorageCodec.save(path, values) + MatterSelectorStorageCodec.saveRules(path, values) } private fun filePath(): Path = @@ -53,4 +61,13 @@ object ForgeMatterConfigOverrides { is ExplicitMatterValue.Deny -> ExplicitMatterValue.Deny(ExplicitMatterSource.CONFIG) is ExplicitMatterValue.Set -> ExplicitMatterValue.Set(compound, ExplicitMatterSource.CONFIG) } + + private fun List.toStaticMap(): Map { + val map = linkedMapOf() + for (rule in this.sortedWith(compareBy { it.source.priority })) { + val key = rule.selector.staticKey ?: continue + map[key] = rule.value + } + return map + } } diff --git a/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/data/ForgeMatterRuntimeOverrides.kt b/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/data/ForgeMatterRuntimeOverrides.kt index aafeeb3..a822c96 100644 --- a/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/data/ForgeMatterRuntimeOverrides.kt +++ b/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/data/ForgeMatterRuntimeOverrides.kt @@ -3,8 +3,10 @@ package com.p_nsk.replicated_integration.data 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.selector.MatterRule import com.p_nsk.replicated_integration.api.selector.MatterSelectorKey import com.p_nsk.replicated_integration.api.selector.MatterSelectorStorageCodec +import com.p_nsk.replicated_integration.api.selector.asRuleSelector import net.minecraft.server.MinecraftServer import net.minecraft.world.level.storage.LevelResource import java.nio.file.Path @@ -19,6 +21,12 @@ object ForgeMatterRuntimeOverrides { return values } + @Synchronized + fun snapshotRules(server: MinecraftServer): List { + loadIfNeeded(server) + return values.map { (selector, value) -> MatterRule(selector.asRuleSelector(), value) } + } + @Synchronized fun set( server: MinecraftServer, diff --git a/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/data/ProbeJSDocumentProvider.kt b/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/data/ProbeJSDocumentProvider.kt new file mode 100644 index 0000000..38fe508 --- /dev/null +++ b/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/data/ProbeJSDocumentProvider.kt @@ -0,0 +1,147 @@ +package com.p_nsk.replicated_integration.data + +import com.google.common.hash.Hashing +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import net.minecraft.data.CachedOutput +import net.minecraft.data.DataProvider +import net.minecraft.data.PackOutput +import java.nio.charset.StandardCharsets +import java.nio.file.Path +import java.util.concurrent.CompletableFuture + +class ProbeJSDocumentProvider( + output: PackOutput, +) : DataProvider { + private val root = output.outputFolder + + override fun run(cache: CachedOutput): CompletableFuture<*> { + val documentsPath = root.resolve("probejs.documents.txt") + val documentJsonPath = root.resolve("probejs/replicated_integration.json") + return CompletableFuture.allOf( + writeText(cache, documentsPath, "probejs/replicated_integration.json\n"), + DataProvider.saveStable(cache, documentJson(), documentJsonPath), + ) + } + + override fun getName(): String = + "Replicated Integration ProbeJS Documents" + + private fun documentJson(): JsonObject = + jsonObject( + "classes" to jsonArray( + classDocument( + "com.p_nsk.replicated_integration.kubejs.ForgeRepInt", + method("nodeMod", listOf("type" to "java.lang.String", "modId" to "java.lang.String"), 1, "Special.Mod"), + method("item", "id", "Special.Item"), + method("itemTag", "tag", "Special.ItemTag"), + method("itemMod", "modId", "Special.Mod"), + method("fluid", "id", "Special.Fluid"), + method("fluidTag", "tag", "Special.FluidTag"), + method("fluidMod", "modId", "Special.Mod"), + ), + classDocument( + "com.p_nsk.replicated_integration.kubejs.ForgeRepIntMek", + method("gas", "id", "Special.Gas"), + method("gasTag", "tag", "Special.GasTag"), + method("gasMod", "modId", "Special.Mod"), + method("infuseType", "id", "Special.InfuseType"), + method("infuseTypeTag", "tag", "Special.InfuseTypeTag"), + method("infuseTypeMod", "modId", "Special.Mod"), + method("pigment", "id", "Special.Pigment"), + method("pigmentTag", "tag", "Special.PigmentTag"), + method("pigmentMod", "modId", "Special.Mod"), + method("slurry", "id", "Special.Slurry"), + method("slurryTag", "tag", "Special.SlurryTag"), + method("slurryMod", "modId", "Special.Mod"), + ), + ), + ) + + private fun classDocument( + className: String, + vararg methods: JsonObject, + ): JsonObject = + jsonObject( + "type" to "document:class", + "className" to className, + "methods" to jsonArray(*methods), + ) + + private fun method( + name: String, + paramName: String, + specialType: String, + ): JsonObject = + method(name, listOf(paramName to "java.lang.String"), 0, specialType) + + private fun method( + name: String, + params: List>, + overrideIndex: Int, + specialType: String, + ): JsonObject = + jsonObject( + "type" to "document:method", + "name" to name, + "static" to true, + "abstract" to false, + "params" to jsonArray( + *params.map { (paramName, paramType) -> + jsonObject( + "type" to "property:param", + "name" to paramName, + "paramType" to classType(paramType), + "varArg" to false, + ) + }.toTypedArray(), + ), + "returns" to classType("com.p_nsk.replicated_integration.api.selector.MatterRuleSelector"), + "properties" to jsonArray( + jsonObject( + "type" to "property:modify", + "index" to overrideIndex, + "newType" to jsonObject( + "type" to "type:primitive", + "name" to specialType, + ), + ), + ), + ) + + private fun classType(name: String): JsonObject = + jsonObject( + "type" to "type:class", + "name" to name, + ) + + private fun jsonObject(vararg entries: Pair): JsonObject = + JsonObject().apply { + for ((key, value) in entries) { + when (value) { + null -> add(key, null) + is Boolean -> addProperty(key, value) + is Number -> addProperty(key, value) + is String -> addProperty(key, value) + is JsonArray -> add(key, value) + is JsonObject -> add(key, value) + else -> error("Unsupported JSON value: $value") + } + } + } + + private fun jsonArray(vararg values: JsonObject): JsonArray = + JsonArray().apply { + values.forEach(::add) + } + + private fun writeText( + cache: CachedOutput, + path: Path, + text: String, + ): CompletableFuture<*> = + CompletableFuture.runAsync { + val bytes = text.toByteArray(StandardCharsets.UTF_8) + cache.writeIfNeeded(path, bytes, Hashing.sha1().hashBytes(bytes)) + } +} diff --git a/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/data/ReplicatedIntegrationDataGen.kt b/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/data/ReplicatedIntegrationDataGen.kt index 186611a..4823de0 100644 --- a/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/data/ReplicatedIntegrationDataGen.kt +++ b/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/data/ReplicatedIntegrationDataGen.kt @@ -13,6 +13,10 @@ object ReplicatedIntegrationDataGen { true, ReplicatedIntegrationLanguageProvider(event.generator.packOutput, "ja_jp"), ) + event.generator.addProvider( + true, + ProbeJSDocumentProvider(event.generator.packOutput), + ) } if (event.includeServer()) { event.generator.addProvider( diff --git a/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/data/ReplicatedIntegrationLanguageProvider.kt b/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/data/ReplicatedIntegrationLanguageProvider.kt index 9c9828e..1cd650d 100644 --- a/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/data/ReplicatedIntegrationLanguageProvider.kt +++ b/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/data/ReplicatedIntegrationLanguageProvider.kt @@ -18,6 +18,8 @@ class ReplicatedIntegrationLanguageProvider( private fun addEnglishTranslations() { add("replicated_integration.config.enable_nuclear_recipe", "Enable Mekanism nuclear recipe support") add("replicated_integration.config.notice.compatibility_summary_shown", "Compatibility summary notice shown") + add("key.categories.replicated_integration", "Replicated Integration") + add("key.replicated_integration.open_replication_index", "Open Replication Index") add("replicated_integration.chat.notice.title", "Replicated Integration") add( "replicated_integration.chat.notice.supported_integrations", @@ -36,6 +38,8 @@ class ReplicatedIntegrationLanguageProvider( private fun addJapaneseTranslations() { add("replicated_integration.config.enable_nuclear_recipe", "Mekanism の核分裂レシピ対応を有効化") add("replicated_integration.config.notice.compatibility_summary_shown", "互換性サマリー通知を表示済み") + add("key.categories.replicated_integration", "Replicated Integration") + add("key.replicated_integration.open_replication_index", "Replication Index を開く") add("replicated_integration.chat.notice.title", "Replicated Integration") add( "replicated_integration.chat.notice.supported_integrations", diff --git a/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/kubejs/ForgeKubeJSMatterRules.kt b/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/kubejs/ForgeKubeJSMatterRules.kt new file mode 100644 index 0000000..5580ad9 --- /dev/null +++ b/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/kubejs/ForgeKubeJSMatterRules.kt @@ -0,0 +1,13 @@ +package com.p_nsk.replicated_integration.kubejs + +import com.p_nsk.replicated_integration.api.selector.MatterRule +import net.minecraftforge.fml.ModList + +object ForgeKubeJSMatterRules { + fun snapshot(): List = + if (ModList.get().isLoaded("kubejs")) { + ForgeRepIntKubeJSEvents.collectRules() + } else { + emptyList() + } +} diff --git a/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/kubejs/ForgeRepInt.kt b/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/kubejs/ForgeRepInt.kt new file mode 100644 index 0000000..2a88c48 --- /dev/null +++ b/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/kubejs/ForgeRepInt.kt @@ -0,0 +1,30 @@ +package com.p_nsk.replicated_integration.kubejs + +import com.p_nsk.replicated_integration.api.kubejs.RepInt +import com.p_nsk.replicated_integration.api.node.MatterNodes +import com.p_nsk.replicated_integration.api.selector.MatterRuleSelector +import com.p_nsk.replicated_integration.api.selector.NodeView +import net.minecraft.world.item.Item +import java.util.function.Predicate +@Suppress("unused") +object ForgeRepInt { + @JvmStatic fun node(type: String, id: String): MatterRuleSelector = RepInt.node(type, id) + @JvmStatic fun nodeTag(type: String, tag: String): MatterRuleSelector = RepInt.nodeTag(type, tag) + @JvmStatic fun nodeMod(type: String, modId: String): MatterRuleSelector = RepInt.nodeMod(type, modId) + @JvmStatic fun item(id: String): MatterRuleSelector = RepInt.item(id) + @JvmStatic fun itemTag(tag: String): MatterRuleSelector = RepInt.itemTag(tag) + @JvmStatic fun itemMod(modId: String): MatterRuleSelector = RepInt.itemMod(modId) + @JvmStatic fun fluid(id: String): MatterRuleSelector = RepInt.fluid(id) + @JvmStatic fun fluidTag(tag: String): MatterRuleSelector = RepInt.fluidTag(tag) + @JvmStatic fun fluidMod(modId: String): MatterRuleSelector = RepInt.fluidMod(modId) + @JvmStatic fun anyOf(vararg selectors: Any): MatterRuleSelector = RepInt.anyOf(*selectors) + @JvmStatic fun allOf(vararg selectors: Any): MatterRuleSelector = RepInt.allOf(*selectors) + @JvmStatic fun not(selector: Any): MatterRuleSelector = RepInt.not(selector) + @JvmStatic fun whereNode(name: String, predicate: Predicate): MatterRuleSelector = RepInt.whereNode(name, predicate) + + @JvmStatic + fun whereItem(name: String, predicate: Predicate): MatterRuleSelector = + RepInt.whereNode(name) { view -> + view.key.type == MatterNodes.ITEM && view.getRaw()?.let(predicate::test) == true + } +} diff --git a/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/kubejs/ForgeRepIntKubeJSPlugin.kt b/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/kubejs/ForgeRepIntKubeJSPlugin.kt new file mode 100644 index 0000000..18fd487 --- /dev/null +++ b/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/kubejs/ForgeRepIntKubeJSPlugin.kt @@ -0,0 +1,47 @@ +package com.p_nsk.replicated_integration.kubejs + +import com.p_nsk.replicated_integration.api.kubejs.RepIntMatterEvent +import com.p_nsk.replicated_integration.api.selector.MatterRule +import dev.latvian.mods.kubejs.KubeJSPlugin +import dev.latvian.mods.kubejs.event.EventGroup +import dev.latvian.mods.kubejs.event.EventGroupWrapper +import dev.latvian.mods.kubejs.event.EventJS +import dev.latvian.mods.kubejs.script.BindingsEvent +import dev.latvian.mods.kubejs.script.ScriptType +import dev.latvian.mods.kubejs.util.ClassFilter + +class ForgeRepIntKubeJSPlugin : KubeJSPlugin() { + override fun registerClasses(type: ScriptType?, filter: ClassFilter) { + filter.allow("com.p_nsk.replicated_integration") + } + + override fun registerEvents() { + ForgeRepIntKubeJSEvents.GROUP.register() + } + + override fun registerBindings(event: BindingsEvent) { + event.add("RepInt", ForgeRepInt::class.java) + } +} + +object ForgeRepIntKubeJSEvents { + val GROUP: EventGroup = EventGroup.of("RepIntEvents") + val MATTER = GROUP.server("matter") { ForgeRepIntMatterEvent::class.java } + + fun collectRules(): List { + val event = ForgeRepIntMatterEvent() + MATTER.post(event) + return event.rules + } +} + +class ForgeRepIntMatterEvent : EventJS() { + private val delegate = RepIntMatterEvent() + + val rules: List + get() = delegate.rules + + fun deny(selector: Any) = delegate.deny(selector) + + fun set(selector: Any, matter: Map) = delegate.set(selector, matter) +} diff --git a/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/kubejs/ForgeRepIntMek.kt b/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/kubejs/ForgeRepIntMek.kt new file mode 100644 index 0000000..582e23f --- /dev/null +++ b/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/kubejs/ForgeRepIntMek.kt @@ -0,0 +1,23 @@ +package com.p_nsk.replicated_integration.kubejs + +import com.p_nsk.replicated_integration.api.kubejs.RepInt +import com.p_nsk.replicated_integration.api.node.MatterNodes +import com.p_nsk.replicated_integration.api.selector.MatterRuleSelector + +object ForgeRepIntMek { + @JvmStatic fun gas(id: String): MatterRuleSelector = RepInt.node(MatterNodes.GAS.toString(), id) + @JvmStatic fun gasTag(tag: String): MatterRuleSelector = RepInt.nodeTag(MatterNodes.GAS.toString(), tag) + @JvmStatic fun gasMod(modId: String): MatterRuleSelector = RepInt.nodeMod(MatterNodes.GAS.toString(), modId) + + @JvmStatic fun infuseType(id: String): MatterRuleSelector = RepInt.node(MatterNodes.INFUSE_TYPE.toString(), id) + @JvmStatic fun infuseTypeTag(tag: String): MatterRuleSelector = RepInt.nodeTag(MatterNodes.INFUSE_TYPE.toString(), tag) + @JvmStatic fun infuseTypeMod(modId: String): MatterRuleSelector = RepInt.nodeMod(MatterNodes.INFUSE_TYPE.toString(), modId) + + @JvmStatic fun pigment(id: String): MatterRuleSelector = RepInt.node(MatterNodes.PIGMENT.toString(), id) + @JvmStatic fun pigmentTag(tag: String): MatterRuleSelector = RepInt.nodeTag(MatterNodes.PIGMENT.toString(), tag) + @JvmStatic fun pigmentMod(modId: String): MatterRuleSelector = RepInt.nodeMod(MatterNodes.PIGMENT.toString(), modId) + + @JvmStatic fun slurry(id: String): MatterRuleSelector = RepInt.node(MatterNodes.SLURRY.toString(), id) + @JvmStatic fun slurryTag(tag: String): MatterRuleSelector = RepInt.nodeTag(MatterNodes.SLURRY.toString(), tag) + @JvmStatic fun slurryMod(modId: String): MatterRuleSelector = RepInt.nodeMod(MatterNodes.SLURRY.toString(), modId) +} diff --git a/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/kubejs/ForgeRepIntMekKubeJSPlugin.kt b/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/kubejs/ForgeRepIntMekKubeJSPlugin.kt new file mode 100644 index 0000000..11b5fe9 --- /dev/null +++ b/1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/kubejs/ForgeRepIntMekKubeJSPlugin.kt @@ -0,0 +1,10 @@ +package com.p_nsk.replicated_integration.kubejs + +import dev.latvian.mods.kubejs.KubeJSPlugin +import dev.latvian.mods.kubejs.script.BindingsEvent + +class ForgeRepIntMekKubeJSPlugin : KubeJSPlugin() { + override fun registerBindings(event: BindingsEvent) { + event.add("RepIntMek", ForgeRepIntMek::class.java) + } +} diff --git a/1.20.1-forge/src/main/resources/kubejs.plugins.txt b/1.20.1-forge/src/main/resources/kubejs.plugins.txt new file mode 100644 index 0000000..7117a9b --- /dev/null +++ b/1.20.1-forge/src/main/resources/kubejs.plugins.txt @@ -0,0 +1,2 @@ +com.p_nsk.replicated_integration.kubejs.ForgeRepIntKubeJSPlugin +com.p_nsk.replicated_integration.kubejs.ForgeRepIntMekKubeJSPlugin mekanism diff --git a/1.20.1-forge/src/main/resources/replicated_integration.mixins.json b/1.20.1-forge/src/main/resources/replicated_integration.mixins.json index 60cf888..bcd606c 100644 --- a/1.20.1-forge/src/main/resources/replicated_integration.mixins.json +++ b/1.20.1-forge/src/main/resources/replicated_integration.mixins.json @@ -12,6 +12,7 @@ "ReplicationCalculationMixin" ], "client": [ + "ScreenAccessor" ], "server": [], "injectors": { diff --git a/1.21.1-neo/build.gradle.kts b/1.21.1-neo/build.gradle.kts index 90bcb44..eb2175b 100644 --- a/1.21.1-neo/build.gradle.kts +++ b/1.21.1-neo/build.gradle.kts @@ -11,16 +11,21 @@ dependencies { implementation(libs.kff.mc1211) implementation(libs.titanium.mc1211) { + // JEIがついてくるけど古過ぎて役に立たないので除外 exclude(group = "mezz.jei") } implementation(libs.replication.mc1211) implementation(libs.ae2.mc1211) implementation(libs.aae.mc1211) implementation(libs.draconic.evolution.mc1211) + implementation(libs.kubejs.mc1211) + implementation(libs.architectury.mc1211) + implementation(libs.rhino.mc1211) // depend (optional) compileOnly(mekclassifier("api")) compileOnly(libs.ae2addonlib.mc1211) + compileOnly(libs.probejs.mc1211) // for develop runtimeOnly(libs.mekanism.mc1211) @@ -39,6 +44,7 @@ dependencies { runtimeOnly(libs.jade.mc1211) runtimeOnly(libs.guideme.mc1211) runtimeOnly(libs.configured.mc1211) + runtimeOnly(libs.probejs.mc1211) } diff --git a/1.21.1-neo/src/generated/resources/assets/replicated_integration/lang/en_us.json b/1.21.1-neo/src/generated/resources/assets/replicated_integration/lang/en_us.json index 92cbe4a..7e74cb2 100644 --- a/1.21.1-neo/src/generated/resources/assets/replicated_integration/lang/en_us.json +++ b/1.21.1-neo/src/generated/resources/assets/replicated_integration/lang/en_us.json @@ -4,5 +4,6 @@ "replicated_integration.chat.notice.supported_integrations": "has tuned support for these installed mods:", "replicated_integration.chat.notice.title": "Replicated Integration", "replicated_integration.config.enable_nuclear_recipe": "Enable Mekanism nuclear recipe support", + "replicated_integration.config.ui.matter_amount_notation": "Matter amount notation", "replicated_integration.config.notice.compatibility_summary_shown": "Compatibility summary notice shown" } \ No newline at end of file diff --git a/1.21.1-neo/src/generated/resources/assets/replicated_integration/lang/ja_jp.json b/1.21.1-neo/src/generated/resources/assets/replicated_integration/lang/ja_jp.json index f308816..4d03627 100644 --- a/1.21.1-neo/src/generated/resources/assets/replicated_integration/lang/ja_jp.json +++ b/1.21.1-neo/src/generated/resources/assets/replicated_integration/lang/ja_jp.json @@ -4,5 +4,6 @@ "replicated_integration.chat.notice.supported_integrations": "は、導入済みの以下 mod と連携できます:", "replicated_integration.chat.notice.title": "Replicated Integration", "replicated_integration.config.enable_nuclear_recipe": "Mekanism の核分裂レシピ対応を有効化", + "replicated_integration.config.ui.matter_amount_notation": "Matter 量の表記", "replicated_integration.config.notice.compatibility_summary_shown": "互換性サマリー通知を表示済み" } \ No newline at end of file diff --git a/1.21.1-neo/src/main/java/com/p_nsk/replicated_integration/mixin/neo/ScreenAccessor.java b/1.21.1-neo/src/main/java/com/p_nsk/replicated_integration/mixin/neo/ScreenAccessor.java new file mode 100644 index 0000000..793ad57 --- /dev/null +++ b/1.21.1-neo/src/main/java/com/p_nsk/replicated_integration/mixin/neo/ScreenAccessor.java @@ -0,0 +1,14 @@ +package com.p_nsk.replicated_integration.mixin.neo; + +import net.minecraft.client.gui.components.Renderable; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.narration.NarratableEntry; +import net.minecraft.client.gui.screens.Screen; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(Screen.class) +public interface ScreenAccessor { + @Invoker("addRenderableWidget") + T replicatedIntegration$addRenderableWidget(T widget); +} diff --git a/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/ModMain.kt b/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/ModMain.kt index fed55f2..01f77b1 100644 --- a/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/ModMain.kt +++ b/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/ModMain.kt @@ -2,12 +2,17 @@ package com.p_nsk.replicated_integration import com.p_nsk.replicated_integration.command.MatterCommand import com.p_nsk.replicated_integration.command.MatterNodeDebugCommand +import com.p_nsk.replicated_integration.client.MatterIndexClientEvents +import com.p_nsk.replicated_integration.client.MatterIndexKeyMappings import com.p_nsk.replicated_integration.config.NeoCompatibilityConfig +import com.p_nsk.replicated_integration.config.NeoUiConfig import com.p_nsk.replicated_integration.core.NeoBuiltinReplicationAddons import com.p_nsk.replicated_integration.data.ReplicationReloadHooks import com.p_nsk.replicated_integration.data.NeoReplicatedIntegrationDataGen import com.p_nsk.replicated_integration.data.ReplicationServerLifecycleHooks import com.p_nsk.replicated_integration.network.NeoReplicationCalculationSyncChannel +import net.neoforged.api.distmarker.Dist +import net.neoforged.fml.loading.FMLEnvironment import net.neoforged.fml.ModLoadingContext import net.neoforged.neoforge.common.NeoForge import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent @@ -24,6 +29,9 @@ object ModMain { NeoForge.EVENT_BUS.register(MatterNodeDebugCommand) NeoForge.EVENT_BUS.register(ReplicationReloadHooks) NeoForge.EVENT_BUS.register(ReplicationServerLifecycleHooks) + if (FMLEnvironment.dist == Dist.CLIENT) { + NeoForge.EVENT_BUS.register(MatterIndexClientEvents) + } } private fun registerConfigs() { @@ -32,12 +40,20 @@ object ModMain { NeoCompatibilityConfig.spec, "replicated_integration-compatibility.toml", ) + ModLoadingContext.get().activeContainer.registerConfig( + ModConfig.Type.CLIENT, + NeoUiConfig.spec, + "replicated_integration-ui.toml", + ) } private fun registerNetwork() { val eventBus = requireNotNull(ModLoadingContext.get().activeContainer.eventBus) eventBus.addListener(::onRegisterPayloadHandlers) eventBus.addListener(NeoReplicatedIntegrationDataGen::gatherData) + if (FMLEnvironment.dist == Dist.CLIENT) { + eventBus.addListener(MatterIndexKeyMappings::register) + } } private fun onRegisterPayloadHandlers(event: RegisterPayloadHandlersEvent) { diff --git a/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/adapter/mekanism/MekanismNodeResolver.kt b/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/adapter/mekanism/MekanismNodeResolver.kt index ec36b17..bbf1c85 100644 --- a/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/adapter/mekanism/MekanismNodeResolver.kt +++ b/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/adapter/mekanism/MekanismNodeResolver.kt @@ -33,6 +33,24 @@ object MekanismNodeResolver { } .orElse(emptyList()) + @Suppress("DEPRECATION") + fun allChemicalNodes(): List = + MekanismAPI.CHEMICAL_REGISTRY + .mapNotNull { chemical -> + val chemicalId = MekanismAPI.CHEMICAL_REGISTRY.getKey(chemical) + if (chemicalId == MekanismAPI.EMPTY_CHEMICAL_KEY) null else MatterNodes.chemical(chemicalId.toLite()) + } + + fun chemicalNodesInMod(modId: String): List = + allChemicalNodes().filter { it.id.namespace == modId } + + @Suppress("DEPRECATION") + fun chemical(id: LiteResourceLocation) = + MekanismAPI.CHEMICAL_REGISTRY.get(id.toMcResourceLocation()) + private fun ResourceLocation.toLite(): LiteResourceLocation = LiteResourceLocation.of(namespace, path) + + private fun LiteResourceLocation.toMcResourceLocation(): ResourceLocation = + ResourceLocation.fromNamespaceAndPath(namespace, path) } diff --git a/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/adapter/mekanism/ReplicationMekanismAddon.kt b/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/adapter/mekanism/ReplicationMekanismAddon.kt index b62ff5e..4bacfed 100644 --- a/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/adapter/mekanism/ReplicationMekanismAddon.kt +++ b/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/adapter/mekanism/ReplicationMekanismAddon.kt @@ -31,6 +31,9 @@ object ReplicationMekanismAddon : NeoReplicationAddon { override fun registerMatterNodes(registry: NeoMatterNodeRegistry) = with(registry) { node(MatterNodes.CHEMICAL, "Chemical") { + enumerate(MekanismNodeResolver::allChemicalNodes) + rawValue(MekanismNodeResolver::chemical) + value( literal = "chemical", suggestions = MekanismMatterCommandInputs.chemicalSuggestions(), 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 01f388b..5fad1cf 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 @@ -23,6 +23,9 @@ object BuiltinNodeResolver { fun itemNode(id: ResourceLocation): NodeKey = MatterNodes.item(id.toLite()) + fun item(id: LiteResourceLocation) = + BuiltInRegistries.ITEM.get(id.toMcResourceLocation()) + fun fluidNode(stack: FluidStack): NodeKey? { if (stack.isEmpty) { return null @@ -36,6 +39,9 @@ object BuiltinNodeResolver { if (fluid == net.minecraft.world.level.material.Fluids.EMPTY) null else MatterNodes.fluid(id.toLite()) } + fun fluid(id: LiteResourceLocation) = + BuiltInRegistries.FLUID.get(id.toMcResourceLocation()) + fun itemNodesInTag(id: ResourceLocation): List = BuiltInRegistries.ITEM.getTag(TagKey.create(Registries.ITEM, id)) .map { holders -> @@ -53,6 +59,21 @@ object BuiltinNodeResolver { } .orElse(emptyList()) + fun allItemNodes(): List = + BuiltInRegistries.ITEM + .map { item -> MatterNodes.item(BuiltInRegistries.ITEM.getKey(item).toLite()) } + + 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 itemNodesInMod(modId: String): List = + allItemNodes().filter { it.id.namespace == modId } + + fun fluidNodesInMod(modId: String): List = + allFluidNodes().filter { it.id.namespace == modId } + fun itemAmount(stack: ItemStack): NodeAmount? = itemNode(stack)?.let { NodeAmount(it, stack.count.coerceAtLeast(1).toLong()) } @@ -61,4 +82,7 @@ object BuiltinNodeResolver { private fun ResourceLocation.toLite(): LiteResourceLocation = LiteResourceLocation.of(namespace, path) + + private fun LiteResourceLocation.toMcResourceLocation(): ResourceLocation = + ResourceLocation.fromNamespaceAndPath(namespace, path) } diff --git a/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/adapter/vanilla/ReplicationVanillaAddon.kt b/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/adapter/vanilla/ReplicationVanillaAddon.kt index 6fe4e9b..55aa95d 100644 --- a/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/adapter/vanilla/ReplicationVanillaAddon.kt +++ b/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/adapter/vanilla/ReplicationVanillaAddon.kt @@ -3,6 +3,7 @@ package com.p_nsk.replicated_integration.adapter.vanilla import com.buuz135.replication.ReplicationRegistry import com.buuz135.replication.calculation.MatterValue import com.buuz135.replication.recipe.MatterValueRecipe +import com.buuz135.replication.util.ReplicationTags import com.p_nsk.replicated_integration.api.addon.ReplicationAddonEnvironment import com.p_nsk.replicated_integration.api.addon.ReplicationAddonLoadSafetyContract import com.p_nsk.replicated_integration.api.graph.IConversionSink @@ -38,6 +39,9 @@ object ReplicationVanillaAddon : NeoReplicationAddon { override fun registerMatterNodes(registry: NeoMatterNodeRegistry) = with(registry) { node(MatterNodes.ITEM, "Item") { + enumerate(BuiltinNodeResolver::allItemNodes) + rawValue(BuiltinNodeResolver::item) + value( literal = "item", suggestions = VanillaMatterCommandInputs.itemSuggestions(), @@ -52,6 +56,9 @@ object ReplicationVanillaAddon : NeoReplicationAddon { } node(MatterNodes.FLUID, "Fluid") { + enumerate(BuiltinNodeResolver::allFluidNodes) + rawValue(BuiltinNodeResolver::fluid) + value( literal = "fluid", suggestions = VanillaMatterCommandInputs.fluidSuggestions(), @@ -247,6 +254,12 @@ object ReplicationVanillaAddon : NeoReplicationAddon { return@mapNotNull null } val item = itemById(consume.node.id) ?: return@mapNotNull null + // Passive Replication compatibility: this opts an input item out of + // the vanilla crafting-remainder subtraction only. It does not change + // the value of the item itself. + if (ItemStack(item).`is`(ReplicationTags.DONT_CHECK_FOR_CRAFTING_RESULT)) { + return@mapNotNull null + } val remainder = item.craftingRemainingItem ?: return@mapNotNull null val remainderNode = BuiltinNodeResolver.itemNode(ItemStack(remainder)) ?: return@mapNotNull null NodeAmount(remainderNode, consume.amount) diff --git a/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/client/MatterIndexClientEvents.kt b/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/client/MatterIndexClientEvents.kt new file mode 100644 index 0000000..c6feceb --- /dev/null +++ b/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/client/MatterIndexClientEvents.kt @@ -0,0 +1,175 @@ +package com.p_nsk.replicated_integration.client + +import com.mojang.blaze3d.platform.InputConstants +import com.p_nsk.replicated_integration.mixin.neo.ScreenAccessor +import net.minecraft.ChatFormatting +import net.minecraft.client.KeyMapping +import net.minecraft.client.Minecraft +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.gui.components.AbstractButton +import net.minecraft.client.gui.narration.NarrationElementOutput +import net.minecraft.client.gui.screens.Screen +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen +import net.minecraft.network.chat.Component +import net.minecraft.world.item.ItemStack +import net.neoforged.bus.api.SubscribeEvent +import net.neoforged.neoforge.client.event.ClientTickEvent +import net.neoforged.neoforge.client.event.RegisterKeyMappingsEvent +import net.neoforged.neoforge.client.event.ScreenEvent +import net.neoforged.neoforge.client.settings.KeyConflictContext +import net.neoforged.neoforge.event.entity.player.ItemTooltipEvent +import kotlin.math.max +import kotlin.math.min + +object MatterIndexClientEvents { + private const val HOLD_TICKS = 10 + + private var hoveredStack: ItemStack = ItemStack.EMPTY + private var trackingStack: ItemStack = ItemStack.EMPTY + private var holdTicks: Int = 0 + + @SubscribeEvent + fun onScreenInit(event: ScreenEvent.Init.Post) { + val screen = event.screen + if (screen !is AbstractContainerScreen<*>) return + (screen as ScreenAccessor).`replicatedIntegration$addRenderableWidget`(MatterIndexSidebarButton(screen)) + } + + @SubscribeEvent + fun onItemTooltip(event: ItemTooltipEvent) { + val minecraft = Minecraft.getInstance() + val screen = minecraft.screen ?: return + if (screen is MatterIndexScreen || event.entity == null) return + updateHovered(event.itemStack) + if (trackingStack !== event.itemStack && !trackingStack.`is`(event.itemStack.item)) return + val progress = holdTicks.toFloat() / HOLD_TICKS.toFloat() + val line = + if (progress <= 0f) { + holdHint() + } else { + progressBar(progress, holdHint()) + } + if (event.toolTip.size < 2) { + event.toolTip.add(line) + } else { + event.toolTip.add(1, line) + } + } + + @SubscribeEvent + fun onClientTick(event: ClientTickEvent.Post) { + val minecraft = Minecraft.getInstance() + val screen = minecraft.screen + if (screen == null || screen is MatterIndexScreen || trackingStack.isEmpty) { + resetHold() + return + } + + val holding = !hoveredStack.isEmpty && MatterIndexKeyMappings.isOpenIndexDown(minecraft.window.window) + holdTicks = + if (holding) { + min(HOLD_TICKS, holdTicks + 1) + } else { + max(0, holdTicks - 1) + } + + if (holdTicks >= HOLD_TICKS) { + val itemId = MatterIndexScreen.itemId(trackingStack) + resetHold() + minecraft.setScreen(MatterIndexScreen(screen, itemId)) + return + } + + hoveredStack = ItemStack.EMPTY + } + + private fun updateHovered(stack: ItemStack) { + hoveredStack = ItemStack.EMPTY + if (stack.isEmpty) return + if (!MatterIndexScreen.hasMatter(MatterIndexScreen.itemId(stack))) return + if (trackingStack.isEmpty || !trackingStack.`is`(stack.item)) { + holdTicks = 0 + } + trackingStack = stack + hoveredStack = stack + } + + private fun resetHold() { + hoveredStack = ItemStack.EMPTY + trackingStack = ItemStack.EMPTY + holdTicks = 0 + } + + private fun progressBar(progress: Float, hint: Component): Component { + val total = max(26, Minecraft.getInstance().font.width(hint) / Minecraft.getInstance().font.width("|")) + val easedProgress = easeIn(progress.coerceIn(0f, 1f)) + val current = (easedProgress * total).toInt() + return Component.literal("|".repeat(current)) + .withStyle(ChatFormatting.GRAY) + .append(Component.literal("|".repeat(total - current)).withStyle(ChatFormatting.DARK_GRAY)) + } + + private fun easeIn(progress: Float): Float = + progress * progress + + private fun holdHint(): Component = + Component.literal("Hold ") + .withStyle(ChatFormatting.DARK_GRAY) + .append(Component.literal("[").withStyle(ChatFormatting.GRAY)) + .append(MatterIndexKeyMappings.openIndex.translatedKeyMessage.copy().withStyle(ChatFormatting.GRAY)) + .append(Component.literal("]").withStyle(ChatFormatting.GRAY)) + .append(Component.literal(" to open Replication Index").withStyle(ChatFormatting.DARK_GRAY)) + + private class MatterIndexSidebarButton( + private val owner: Screen, + ) : AbstractButton(2, 2, 22, 22, Component.literal("Replication Index")) { + override fun onPress() { + Minecraft.getInstance().setScreen(MatterIndexScreen(owner)) + } + + override fun renderWidget(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) { + guiGraphics.fill(x, y, x + width, y + height, 0xCC071010.toInt()) + guiGraphics.fill(x, y, x + width, y + 1, 0xFF41514D.toInt()) + guiGraphics.fill(x, y + height - 1, x + width, y + height, 0xFF41514D.toInt()) + guiGraphics.fill(x, y, x + 1, y + height, 0xFF41514D.toInt()) + guiGraphics.fill(x + width - 1, y, x + width, y + height, 0xFF41514D.toInt()) + if (isHoveredOrFocused) { + guiGraphics.fill(x + 1, y + 1, x + width - 1, y + height - 1, 0x5536D84A) + guiGraphics.renderTooltip(Minecraft.getInstance().font, message, mouseX, mouseY) + } + drawRiMark(guiGraphics, x + 4, y + 4) + } + + override fun updateWidgetNarration(output: NarrationElementOutput) { + defaultButtonNarrationText(output) + } + + private fun drawRiMark(guiGraphics: GuiGraphics, x: Int, y: Int) { + guiGraphics.fill(x + 5, y, x + 9, y + 4, 0xFF42E84F.toInt()) + guiGraphics.fill(x, y + 5, x + 4, y + 9, 0xFF42E84F.toInt()) + guiGraphics.fill(x + 10, y + 5, x + 14, y + 9, 0xFF42E84F.toInt()) + guiGraphics.fill(x + 5, y + 10, x + 9, y + 14, 0xFF42E84F.toInt()) + } + } +} + +object MatterIndexKeyMappings { + val openIndex: KeyMapping = + KeyMapping( + "key.replicated_integration.open_replication_index", + KeyConflictContext.GUI, + InputConstants.Type.KEYSYM, + InputConstants.KEY_W, + "key.categories.replicated_integration", + ) + + fun isOpenIndexDown(window: Long): Boolean { + if (openIndex.isUnbound) return false + val key = openIndex.key + return InputConstants.isKeyDown(window, key.value) + } + + fun register(event: RegisterKeyMappingsEvent) { + event.register(openIndex) + } +} 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 new file mode 100644 index 0000000..f4ad391 --- /dev/null +++ b/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/client/MatterIndexScreen.kt @@ -0,0 +1,920 @@ +package com.p_nsk.replicated_integration.client + +import com.buuz135.replication.Replication +import com.buuz135.replication.calculation.MatterCompound +import com.buuz135.replication.calculation.client.ClientReplicationCalculation +import com.buuz135.replication.util.ReplicationTags +import com.p_nsk.replicated_integration.api.client.* +import com.p_nsk.replicated_integration.config.NeoUiConfig +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.gui.components.Button +import net.minecraft.client.gui.components.EditBox +import net.minecraft.client.gui.screens.Screen +import net.minecraft.core.registries.BuiltInRegistries +import net.minecraft.network.chat.Component +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.item.ItemStack +import org.lwjgl.glfw.GLFW +import kotlin.math.ceil +import kotlin.math.max +import kotlin.math.min + +class MatterIndexScreen( + private val parent: Screen?, + private val focusedItemId: String? = null, +) : Screen(Component.literal("Replication Index")) { + private val entries: List = loadEntries() + private val matterTypes: List = + entries + .flatMap { it.record.matterValues.keys } + .distinct() + .filterNot { it == "empty" } + .sorted() + + private var selectedItemId: String? = focusedItemId + private var sort = MatterIndexSort() + private var indexScroll = 0 + private var equivalentScroll = 0 + private var matterOnly = true + private var replicatableOnly = false + private var disintegratableOnly = false + private var displayScalePercent = MatterIndexDisplayScale.DEFAULT_PERCENT + private var hoveredStack: ItemStack = ItemStack.EMPTY + private var hoveredAmountTooltip: List = emptyList() + private var draggingScrollbar: DraggingScrollbar? = null + + private lateinit var searchBox: EditBox + private lateinit var matterOnlyButton: Button + private lateinit var replicatableButton: Button + private lateinit var disintegratableButton: Button + + override fun init() { + val layout = layout() + searchBox = EditBox(font, layout.index.x + 16, layout.index.y + 35, min(260, layout.index.w / 3), 18, Component.literal("search")) + searchBox.setMaxLength(80) + searchBox.setTextColor(0xE8F3ED) + searchBox.setValue("") + addWidget(searchBox) + + matterOnlyButton = addRenderableWidget( + Button.builder(toggleLabel("M", matterOnly)) { + matterOnly = !matterOnly + matterOnlyButton.message = toggleLabel("M", matterOnly) + indexScroll = 0 + }.bounds(searchBox.x + searchBox.width + 8, searchBox.y, 34, 18).build(), + ) + replicatableButton = addRenderableWidget( + Button.builder(toggleLabel("R", replicatableOnly)) { + replicatableOnly = !replicatableOnly + replicatableButton.message = toggleLabel("R", replicatableOnly) + indexScroll = 0 + }.bounds(matterOnlyButton.x + matterOnlyButton.width + 4, searchBox.y, 34, 18).build(), + ) + disintegratableButton = addRenderableWidget( + Button.builder(toggleLabel("D", disintegratableOnly)) { + disintegratableOnly = !disintegratableOnly + disintegratableButton.message = toggleLabel("D", disintegratableOnly) + indexScroll = 0 + }.bounds(replicatableButton.x + replicatableButton.width + 4, searchBox.y, 34, 18).build(), + ) + } + + override fun render(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) { + hoveredStack = ItemStack.EMPTY + hoveredAmountTooltip = emptyList() + val layout = layout() + guiGraphics.fill(0, 0, width, height, 0xAA050A0A.toInt()) + renderPanel(guiGraphics, layout.index) + renderPanel(guiGraphics, layout.focus) + renderTitle(guiGraphics, layout) + searchBox.render(guiGraphics, mouseX, mouseY, partialTick) + renderControls(guiGraphics, mouseX, mouseY, partialTick) + + val visible = visibleEntries() + if (selectedItemId == null || entries.none { it.record.itemId == selectedItemId }) { + selectedItemId = visible.firstOrNull()?.record?.itemId ?: entries.firstOrNull()?.record?.itemId + } + + renderIndex(guiGraphics, layout, visible, mouseX, mouseY) + renderFocus(guiGraphics, layout, mouseX, mouseY) + + if (!hoveredStack.isEmpty) { + guiGraphics.renderTooltip(font, hoveredStack, mouseX, mouseY) + } else if (hoveredAmountTooltip.isNotEmpty()) { + guiGraphics.renderTooltip(font, hoveredAmountTooltip.map { it.visualOrderText }, mouseX, mouseY) + } + } + + override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { + if (button == 0 && clickDisplayScaleControl(layout(), mouseX.toInt(), mouseY.toInt())) return true + if (super.mouseClicked(mouseX, mouseY, button)) return true + if (button != 0) return false + + val layout = layout() + val visible = visibleEntries() + if (startScrollbarDrag(layout, visible, mouseX.toInt(), mouseY.toInt())) return true + if (clickHeader(layout, mouseX.toInt(), mouseY.toInt())) return true + + val rowArea = indexRows(layout) + if (mouseX.toInt() in rowArea.x until rowArea.x + rowArea.w && mouseY.toInt() in rowArea.y until rowArea.y + rowArea.h) { + val index = indexScroll + (mouseY.toInt() - rowArea.y) / ROW_H + visible.getOrNull(index)?.let { + selectedItemId = it.record.itemId + equivalentScroll = 0 + return true + } + } + + equivalentAt(layout, mouseX.toInt(), mouseY.toInt())?.let { + selectedItemId = it.record.itemId + equivalentScroll = 0 + return true + } + + return false + } + + override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, dragX: Double, dragY: Double): Boolean { + if (button == 0 && dragScrollbar(mouseY.toInt())) return true + return super.mouseDragged(mouseX, mouseY, button, dragX, dragY) + } + + override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean { + if (button == 0 && draggingScrollbar != null) { + draggingScrollbar = null + return true + } + return super.mouseReleased(mouseX, mouseY, button) + } + + override fun mouseScrolled(mouseX: Double, mouseY: Double, scrollX: Double, scrollY: Double): Boolean { + val layout = layout() + if (contains(indexRows(layout), mouseX.toInt(), mouseY.toInt())) { + val visibleRows = max(1, indexRows(layout).h / ROW_H) + val maxScroll = max(0, visibleEntries().size - visibleRows) + indexScroll = (indexScroll - (scrollY * 3).toInt()).coerceIn(0, maxScroll) + return true + } + if (contains(equivalentRect(layout), mouseX.toInt(), mouseY.toInt())) { + val columns = equivalentColumns(layout) + val rows = max(1, equivalentRect(layout).h / SLOT_STEP) + val maxScroll = max(0, (equivalentEntries().size + columns - 1) / columns - rows) + equivalentScroll = (equivalentScroll - scrollY.toInt()).coerceIn(0, maxScroll) + return true + } + return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY) + } + + override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + if (keyCode == GLFW.GLFW_KEY_UP) { + return moveSelection(-1) + } + if (keyCode == GLFW.GLFW_KEY_DOWN) { + return moveSelection(1) + } + if (searchBox.keyPressed(keyCode, scanCode, modifiers)) { + indexScroll = 0 + return true + } + return super.keyPressed(keyCode, scanCode, modifiers) + } + + override fun charTyped(codePoint: Char, modifiers: Int): Boolean { + if (searchBox.charTyped(codePoint, modifiers)) { + indexScroll = 0 + return true + } + return super.charTyped(codePoint, modifiers) + } + + override fun onClose() { + minecraft?.setScreen(parent) + } + + override fun isPauseScreen(): Boolean = false + + private fun renderTitle(guiGraphics: GuiGraphics, layout: Layout) { + guiGraphics.drawString(font, "Replicated Integration", layout.index.x + 28, layout.index.y + 14, 0xDDEFE7, true) + drawRiMark(guiGraphics, layout.index.x + 6, layout.index.y + 11) + } + + private fun renderControls(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) { + matterOnlyButton.render(guiGraphics, mouseX, mouseY, partialTick) + replicatableButton.render(guiGraphics, mouseX, mouseY, partialTick) + disintegratableButton.render(guiGraphics, mouseX, mouseY, partialTick) + renderDisplayScaleControl(guiGraphics, layout(), mouseX, mouseY) + } + + private fun renderIndex(guiGraphics: GuiGraphics, layout: Layout, visible: List, mouseX: Int, mouseY: Int) { + val rows = indexRows(layout) + val columns = columns(layout) + renderHeader(guiGraphics, columns, rows.y - HEADER_H) + + val visibleRows = max(1, rows.h / ROW_H) + indexScroll = indexScroll.coerceIn(0, max(0, visible.size - visibleRows)) + val page = visible.drop(indexScroll).take(visibleRows) + + for ((rowIndex, entry) in page.withIndex()) { + val y = rows.y + rowIndex * ROW_H + val selected = entry.record.itemId == selectedItemId + if (selected) { + guiGraphics.fill(rows.x, y, rows.x + rows.w, y + ROW_H, 0x7736D84A) + guiGraphics.fill(rows.x, y, rows.x + 4, y + ROW_H, 0xFF44E35A.toInt()) + } else if (rowIndex % 2 == 0) { + guiGraphics.fill(rows.x, y, rows.x + rows.w, y + ROW_H, 0x33000000) + } + drawHorizontalLine(guiGraphics, rows.x, rows.x + rows.w, y + ROW_H, 0x335E716D) + guiGraphics.renderItem(entry.stack, columns.item.x + 8, y + 5) + if (mouseX in columns.item.x + 8 until columns.item.x + 24 && mouseY in y + 5 until y + 21) { + hoveredStack = entry.stack + } + drawItemName(guiGraphics, entry.record.displayName, columns.item.x + 30, y + 9, columns.item.w - 34, 0xE6ECE8) + drawTableTotalAmount(guiGraphics, formatAmount(entry.record.total), columns.total.x + columns.total.w - TOTAL_VALUE_RIGHT_PADDING, y + 9, 0xE6ECE8) + if (contains(Rect(columns.total.x, y, columns.total.w, ROW_H), mouseX, mouseY)) { + setAmountTooltip("Total", entry.record.total) + } + for ((matterId, rect) in columns.matter) { + val amount = entry.record.matterValues[matterId] ?: 0.0 + drawTableMatterAmount(guiGraphics, formatAmount(amount), rect.x + rect.w / 2, y + 9, 0xE6ECE8) + if (contains(Rect(rect.x, y, rect.w, ROW_H), mouseX, mouseY)) { + setAmountTooltip(matterId, amount) + } + } + } + + drawScrollbar(guiGraphics, rows.x + rows.w - 5, rows.y, rows.h, visible.size, visibleRows, indexScroll) + drawScaled(guiGraphics, "${visible.size} items", layout.index.x + 16, layout.index.y + layout.index.h - 18, 0xAAB8B2, false) + } + + private fun renderHeader(guiGraphics: GuiGraphics, columns: Columns, y: Int) { + val h = HEADER_H + guiGraphics.fill(columns.item.x, y, columns.endX, y + h, 0x44000000) + drawCell(guiGraphics, columns.item, y, h) + drawCell(guiGraphics, columns.total, y, h) + drawScaled(guiGraphics, headerText("Item", MatterIndexSort.ITEM_COLUMN), columns.item.x + 10, y + 10, 0xBFD0CA, false) + drawCentered(guiGraphics, headerText("Total", MatterIndexSort.TOTAL_COLUMN), columns.total.x + columns.total.w / 2, y + 10, 0xBFD0CA) + for ((matterId, rect) in columns.matter) { + drawCell(guiGraphics, rect, y, h) + renderMatterIcon(guiGraphics, matterId, rect.x + rect.w / 2 - 8, y + 7) + if (sort.column == MatterIndexSort.matterColumn(matterId)) { + drawScaled(guiGraphics, sortArrow(), rect.x + rect.w - scaledWidth(sortArrow()) - 3, y + 10, 0x7CF47B, false) + } + } + } + + private fun renderFocus(guiGraphics: GuiGraphics, layout: Layout, mouseX: Int, mouseY: Int) { + val selected = selectedEntry() + drawScaled(guiGraphics, "Selected Item", layout.focus.x + 16, layout.focus.y + 14, 0xDDEFE7, true) + if (selected == null) { + drawScaled(guiGraphics, "No matter data", layout.focus.x + 16, layout.focus.y + 42, 0xAAB8B2, false) + return + } + + val slotX = layout.focus.x + 18 + val slotY = layout.focus.y + 42 + drawSlot(guiGraphics, slotX, slotY, 54, true) + guiGraphics.renderItem(selected.stack, slotX + 19, slotY + 19) + if (mouseX in slotX until slotX + 54 && mouseY in slotY until slotY + 54) { + hoveredStack = selected.stack + } + + drawTrimmed(guiGraphics, selected.record.displayName, slotX + 70, slotY + 4, layout.focus.w - 100, 0xE6ECE8) + drawTrimmed(guiGraphics, selected.record.itemId, slotX + 70, slotY + 18, layout.focus.w - 100, 0x94A29C) + + val focusMatter = focusMatterLayout(layout, slotX, slotY) + for ((index, matterId) in matterTypes.withIndex()) { + val column = index % focusMatter.columns + val row = index / focusMatter.columns + val x = focusMatter.x + column * focusMatter.cellW + val y = focusMatter.y + row * FOCUS_MATTER_ROW_H + val amount = selected.record.matterValues[matterId] ?: 0.0 + drawFocusMatterAmount(guiGraphics, matterId, formatAmount(amount), x, y, focusMatter.cellW, focusMatterAmountColor(amount)) + if (mouseX in x until x + focusMatter.cellW && mouseY in y until y + FOCUS_MATTER_ROW_H) { + setAmountTooltip(matterId, amount) + } + } + + val badgeY = max(slotY + 82, focusMatter.y + focusMatter.rows * FOCUS_MATTER_ROW_H + 6) + drawBadge(guiGraphics, slotX, badgeY, "Replicatable", selected.record.replicatable) + drawBadge(guiGraphics, slotX + scaledPixels(104), badgeY, "Disintegratable", selected.record.disintegratable) + drawDivider(guiGraphics, layout.focus.x + 12, badgeY + 32, layout.focus.w - 24) + + val eq = equivalentRect(layout) + drawScaled(guiGraphics, "Matter Equivalent", eq.x, eq.y - 15, 0xDDEFE7, false) + renderEquivalentGrid(guiGraphics, layout, mouseX, mouseY) + } + + private fun renderEquivalentGrid(guiGraphics: GuiGraphics, layout: Layout, mouseX: Int, mouseY: Int) { + val rect = equivalentRect(layout) + guiGraphics.fill(rect.x, rect.y, rect.x + rect.w, rect.y + rect.h, 0x66000000) + val columns = equivalentColumns(layout) + val rows = max(1, rect.h / SLOT_STEP) + val equivalent = equivalentEntries() + equivalentScroll = equivalentScroll.coerceIn(0, max(0, (equivalent.size + columns - 1) / columns - rows)) + val first = equivalentScroll * columns + for (index in 0 until min(equivalent.size - first, columns * rows)) { + val entry = equivalent[first + index] + val x = rect.x + 8 + (index % columns) * SLOT_STEP + val y = rect.y + 8 + (index / columns) * SLOT_STEP + val selected = entry.record.itemId == selectedItemId + drawSlot(guiGraphics, x, y, SLOT_SIZE, selected) + guiGraphics.renderItem(entry.stack, x + 5, y + 5) + if (mouseX in x until x + SLOT_SIZE && mouseY in y until y + SLOT_SIZE) { + hoveredStack = entry.stack + } + } + drawScrollbar(guiGraphics, rect.x + rect.w - 5, rect.y, rect.h, (equivalent.size + columns - 1) / columns, rows, equivalentScroll) + } + + private fun clickHeader(layout: Layout, mouseX: Int, mouseY: Int): Boolean { + val rows = indexRows(layout) + val y = rows.y - HEADER_H + if (mouseY !in y until y + HEADER_H) return false + val columns = columns(layout) + val column = + when { + contains(columns.item, mouseX, mouseY) -> MatterIndexSort.ITEM_COLUMN + contains(columns.total, mouseX, mouseY) -> MatterIndexSort.TOTAL_COLUMN + else -> columns.matter.firstNotNullOfOrNull { (matterId, rect) -> + if (contains(rect, mouseX, mouseY)) MatterIndexSort.matterColumn(matterId) else null + } + } ?: return false + sort = + if (sort.column == column) { + sort.copy(direction = if (sort.direction == MatterIndexSortDirection.ASC) MatterIndexSortDirection.DESC else MatterIndexSortDirection.ASC) + } else { + MatterIndexSort(column, MatterIndexSortDirection.DESC) + } + indexScroll = 0 + return true + } + + private fun equivalentAt(layout: Layout, mouseX: Int, mouseY: Int): MatterIndexEntry? { + val rect = equivalentRect(layout) + if (!contains(rect, mouseX, mouseY)) return null + val columns = equivalentColumns(layout) + val localX = mouseX - rect.x - 8 + val localY = mouseY - rect.y - 8 + if (localX < 0 || localY < 0) return null + val column = localX / SLOT_STEP + val row = localY / SLOT_STEP + if (column !in 0 until columns) return null + if (localX % SLOT_STEP >= SLOT_SIZE || localY % SLOT_STEP >= SLOT_SIZE) return null + return equivalentEntries().getOrNull((equivalentScroll + row) * columns + column) + } + + private fun startScrollbarDrag(layout: Layout, visible: List, mouseX: Int, mouseY: Int): Boolean { + val bar = scrollbarAt(layout, visible, mouseX, mouseY) ?: return false + val thumb = scrollbarThumb(bar) ?: return false + val thumbOffset = + if (contains(thumb.inflate(SCROLLBAR_HIT_PAD), mouseX, mouseY)) { + mouseY - thumb.y + } else { + thumb.h / 2 + } + draggingScrollbar = DraggingScrollbar(bar.target, thumbOffset) + setScrollbarFromThumbTop(bar, mouseY - thumbOffset) + return true + } + + private fun dragScrollbar(mouseY: Int): Boolean { + val dragging = draggingScrollbar ?: return false + val bar = scrollbarForTarget(layout(), visibleEntries(), dragging.target) ?: return false + setScrollbarFromThumbTop(bar, mouseY - dragging.thumbOffset) + return true + } + + private fun scrollbarAt(layout: Layout, visible: List, mouseX: Int, mouseY: Int): Scrollbar? = + listOfNotNull( + scrollbarForTarget(layout, visible, ScrollbarTarget.INDEX), + scrollbarForTarget(layout, visible, ScrollbarTarget.EQUIVALENT), + ).firstOrNull { contains(scrollbarHitRect(it), mouseX, mouseY) && it.total > it.visible } + + private fun scrollbarForTarget(layout: Layout, visible: List, target: ScrollbarTarget): Scrollbar? = + when (target) { + ScrollbarTarget.INDEX -> { + val rows = indexRows(layout) + Scrollbar( + target = target, + x = rows.x + rows.w - 5, + y = rows.y, + h = rows.h, + total = visible.size, + visible = max(1, rows.h / ROW_H), + scroll = indexScroll, + ) + } + ScrollbarTarget.EQUIVALENT -> { + val rect = equivalentRect(layout) + val columns = equivalentColumns(layout) + Scrollbar( + target = target, + x = rect.x + rect.w - 5, + y = rect.y, + h = rect.h, + total = (equivalentEntries().size + columns - 1) / columns, + visible = max(1, rect.h / SLOT_STEP), + scroll = equivalentScroll, + ) + } + } + + private fun scrollbarHitRect(bar: Scrollbar): Rect = + Rect(bar.x - SCROLLBAR_HIT_PAD, bar.y, SCROLLBAR_W + SCROLLBAR_HIT_PAD * 2, bar.h) + + private fun scrollbarThumb(bar: Scrollbar): Rect? { + if (bar.total <= bar.visible) return null + val thumbH = max(16, bar.h * bar.visible / bar.total) + val thumbY = bar.y + ((bar.h - thumbH) * bar.scroll / max(1, bar.total - bar.visible)) + return Rect(bar.x, thumbY, SCROLLBAR_W, thumbH) + } + + private fun setScrollbarFromThumbTop(bar: Scrollbar, thumbTop: Int) { + val thumb = scrollbarThumb(bar) ?: return + val maxScroll = max(0, bar.total - bar.visible) + val track = max(1, bar.h - thumb.h) + val local = (thumbTop - bar.y).coerceIn(0, track) + val scroll = ((local * maxScroll.toFloat()) / track.toFloat()).toInt().coerceIn(0, maxScroll) + when (bar.target) { + ScrollbarTarget.INDEX -> indexScroll = scroll + ScrollbarTarget.EQUIVALENT -> equivalentScroll = scroll + } + } + + private fun moveSelection(delta: Int): Boolean { + val visible = visibleEntries() + if (visible.isEmpty()) return false + val current = visible.indexOfFirst { it.record.itemId == selectedItemId } + val target = + if (current < 0) { + if (delta < 0) visible.lastIndex else 0 + } else { + (current + delta).coerceIn(0, visible.lastIndex) + } + selectedItemId = visible[target].record.itemId + equivalentScroll = 0 + keepSelectionVisible(target, visible.size) + return true + } + + private fun keepSelectionVisible(index: Int, total: Int) { + val visibleRows = max(1, indexRows(layout()).h / ROW_H) + indexScroll = + when { + index < indexScroll -> index + index >= indexScroll + visibleRows -> index - visibleRows + 1 + else -> indexScroll + }.coerceIn(0, max(0, total - visibleRows)) + } + + private fun visibleEntries(): List { + val query = MatterIndexFiltering.parseQuery(searchBox.value, matterOnly, replicatableOnly, disintegratableOnly) + val ordered = MatterIndexFiltering.filterAndSort(entries.map { it.record }, query, sort) + val byId = entries.associateBy { it.record.itemId } + return ordered.mapNotNull { byId[it.itemId] } + } + + private fun selectedEntry(): MatterIndexEntry? = + entries.firstOrNull { it.record.itemId == selectedItemId } + + private fun equivalentEntries(): List { + val records = MatterIndexFiltering.equivalentRecords(entries.map { it.record }, selectedEntry()?.record) + val byId = entries.associateBy { it.record.itemId } + return records.mapNotNull { byId[it.itemId] } + } + + private fun columns(layout: Layout): Columns { + val rows = indexRows(layout) + val itemW = max(scaledPixels(126), (rows.w * ITEM_COLUMN_WIDTH_RATIO).toInt()) + val totalW = scaledPixels(52) + val matterW = max(tableMatterColumnMinWidth(), (rows.w - itemW - totalW) / max(1, matterTypes.size)) + val matter = matterTypes.mapIndexed { index, id -> + id to Rect(rows.x + itemW + totalW + index * matterW, rows.y - HEADER_H, matterW, HEADER_H) + }.filter { it.second.x + it.second.w <= rows.x + rows.w } + return Columns( + item = Rect(rows.x, rows.y - HEADER_H, itemW, HEADER_H), + total = Rect(rows.x + itemW, rows.y - HEADER_H, totalW, HEADER_H), + matter = matter, + endX = rows.x + rows.w, + ) + } + + private fun layout(): Layout { + val margin = 24 + val gap = 6 + val panelH = height - margin * 2 + val totalW = width - margin * 2 - gap + val defaultIndexW = (totalW * 0.60).toInt() + val maxIndexW = max(0, totalW - FOCUS_MIN_W) + val indexW = min(maxIndexW, max(defaultIndexW, requiredIndexPanelWidth())) + val focusW = totalW - indexW + return Layout( + index = Rect(margin, margin, indexW, panelH), + focus = Rect(margin + indexW + gap, margin, focusW, panelH), + ) + } + + private fun requiredIndexPanelWidth(): Int { + val matterCount = max(1, matterTypes.size) + val denominator = 1.0 - ITEM_COLUMN_WIDTH_RATIO + val fixedWidth = scaledPixels(52) + matterCount * tableMatterColumnMinWidth() + val rowsW = max(scaledPixels(126) + fixedWidth, ceil(fixedWidth / denominator).toInt()) + return rowsW + INDEX_HORIZONTAL_PADDING + } + + private fun indexRows(layout: Layout): Rect = + Rect(layout.index.x + 14, layout.index.y + 88, layout.index.w - 28, layout.index.h - 124) + + private fun equivalentRect(layout: Layout): Rect { + val y = focusEquivalentY(layout) + return Rect(layout.focus.x + 16, y, layout.focus.w - 32, max(0, layout.focus.y + layout.focus.h - y - 20)) + } + + private fun equivalentColumns(layout: Layout): Int = + max(2, (equivalentRect(layout).w - 12) / SLOT_STEP) + + private fun focusEquivalentY(layout: Layout): Int { + val focusMatter = focusMatterLayout(layout, layout.focus.x + 18, layout.focus.y + 42) + val badgeY = max(layout.focus.y + 124, focusMatter.y + focusMatter.rows * FOCUS_MATTER_ROW_H + 6) + return badgeY + 60 + } + + private fun focusMatterLayout(layout: Layout, slotX: Int, slotY: Int): FocusMatterLayout { + val x = slotX + 70 + val availableW = max(FOCUS_MATTER_MIN_CELL_W, layout.focus.x + layout.focus.w - 8 - x) + val columns = min(FOCUS_MATTER_COLUMNS, matterTypes.size.coerceAtLeast(1)) + val rows = (matterTypes.size + columns - 1) / columns + return FocusMatterLayout( + x = x, + y = slotY + 36, + cellW = availableW / columns, + columns = columns, + rows = rows, + ) + } + + private fun renderPanel(guiGraphics: GuiGraphics, rect: Rect) { + guiGraphics.fill(rect.x, rect.y, rect.x + rect.w, rect.y + rect.h, 0xCC071010.toInt()) + drawBorder(guiGraphics, rect, 0xFF41514D.toInt()) + drawBorder(guiGraphics, rect.inflate(-2), 0x6625C34A) + } + + private fun drawCell(guiGraphics: GuiGraphics, rect: Rect, y: Int, h: Int) { + drawVerticalLine(guiGraphics, rect.x, y, y + h, 0x445E716D) + drawVerticalLine(guiGraphics, rect.x + rect.w, y, y + h, 0x445E716D) + } + + private fun drawSlot(guiGraphics: GuiGraphics, x: Int, y: Int, size: Int, selected: Boolean) { + val edge = if (selected) 0xFF58E65B.toInt() else 0xFF59625F.toInt() + guiGraphics.fill(x, y, x + size, y + size, edge) + guiGraphics.fill(x + 1, y + 1, x + size - 1, y + size - 1, 0xFF131918.toInt()) + guiGraphics.fill(x + 2, y + 2, x + size - 2, y + size - 2, 0xFF202826.toInt()) + guiGraphics.fill(x + 3, y + 3, x + size - 3, y + size - 3, 0xFF0D1211.toInt()) + } + + private fun drawBadge(guiGraphics: GuiGraphics, x: Int, y: Int, text: String, active: Boolean) { + val w = scaledPixels(100) + val h = scaledPixels(22) + val fill = if (active) 0x5517E517 else 0x55E03232 + val border = if (active) 0xAA36D84A.toInt() else 0xAAC74949.toInt() + val color = if (active) 0x70F06E else 0xF07171 + val label = if (active) text else "Not $text" + guiGraphics.fill(x, y, x + w, y + h, fill) + drawBorder(guiGraphics, Rect(x, y, w, h), border) + drawCentered(guiGraphics, label, x + w / 2, y + max(1, (h - scaledPixels(font.lineHeight)) / 2), color) + } + + private fun renderMatterIcon(guiGraphics: GuiGraphics, matterId: String, x: Int, y: Int) { + guiGraphics.blit(ResourceLocation.fromNamespaceAndPath(Replication.MOD_ID, "textures/gui/mattertypes/${matterId.substringAfter(':')}.png"), x, y, 0f, 0f, 16, 16, 16, 16) + } + + private fun renderMatterIcon(guiGraphics: GuiGraphics, matterId: String, x: Int, y: Int, size: Int) { + val scale = size / 16f + guiGraphics.pose().pushPose() + guiGraphics.pose().translate(x.toFloat(), y.toFloat(), 0f) + guiGraphics.pose().scale(scale, scale, 1f) + renderMatterIcon(guiGraphics, matterId, 0, 0) + guiGraphics.pose().popPose() + } + + private fun renderMatterIcon(guiGraphics: GuiGraphics, matterId: String, x: Int, y: Int, size: Int, muted: Boolean) { + if (!muted) { + renderMatterIcon(guiGraphics, matterId, x, y, size) + return + } + guiGraphics.setColor(0.56f, 0.63f, 0.60f, 0.70f) + renderMatterIcon(guiGraphics, matterId, x, y, size) + guiGraphics.setColor(1f, 1f, 1f, 1f) + } + + private fun drawRiMark(guiGraphics: GuiGraphics, x: Int, y: Int) { + guiGraphics.fill(x + 5, y, x + 9, y + 4, 0xFF42E84F.toInt()) + guiGraphics.fill(x, y + 5, x + 4, y + 9, 0xFF42E84F.toInt()) + guiGraphics.fill(x + 10, y + 5, x + 14, y + 9, 0xFF42E84F.toInt()) + guiGraphics.fill(x + 5, y + 10, x + 9, y + 14, 0xFF42E84F.toInt()) + } + + private fun drawScrollbar(guiGraphics: GuiGraphics, x: Int, y: Int, h: Int, total: Int, visible: Int, scroll: Int) { + val bar = Scrollbar(ScrollbarTarget.INDEX, x, y, h, total, visible, scroll) + val thumb = scrollbarThumb(bar) ?: return + guiGraphics.fill(x, y, x + SCROLLBAR_W, y + h, 0xAA050606.toInt()) + guiGraphics.fill(thumb.x, thumb.y, thumb.x + thumb.w, thumb.y + thumb.h, 0xFF9EAAA5.toInt()) + } + + private fun drawDivider(guiGraphics: GuiGraphics, x: Int, y: Int, w: Int) = + guiGraphics.fill(x, y, x + w, y + 1, 0x6641514D) + + private fun drawBorder(guiGraphics: GuiGraphics, rect: Rect, color: Int) { + drawHorizontalLine(guiGraphics, rect.x, rect.x + rect.w, rect.y, color) + drawHorizontalLine(guiGraphics, rect.x, rect.x + rect.w, rect.y + rect.h, color) + drawVerticalLine(guiGraphics, rect.x, rect.y, rect.y + rect.h, color) + drawVerticalLine(guiGraphics, rect.x + rect.w, rect.y, rect.y + rect.h, color) + } + + private fun drawHorizontalLine(guiGraphics: GuiGraphics, x1: Int, x2: Int, y: Int, color: Int) = + guiGraphics.fill(x1, y, x2, y + 1, color) + + private fun drawVerticalLine(guiGraphics: GuiGraphics, x: Int, y1: Int, y2: Int, color: Int) = + guiGraphics.fill(x, y1, x + 1, y2, color) + + private fun drawItemName(guiGraphics: GuiGraphics, text: String, x: Int, y: Int, maxWidth: Int, color: Int) = + drawTrimmed(guiGraphics, text, x, y, maxWidth, color, itemNameScale()) + + private fun drawTableMatterAmount(guiGraphics: GuiGraphics, text: String, centerX: Int, y: Int, color: Int) { + val scale = tableMatterAmountScale() + drawScaled(guiGraphics, text, centerX - scaledWidth(text, scale) / 2, y, color, false, scale) + } + + private fun drawTableTotalAmount(guiGraphics: GuiGraphics, text: String, rightX: Int, y: Int, color: Int) { + val scale = tableMatterAmountScale() + drawScaled(guiGraphics, text, rightX - scaledWidth(text, scale), y, color, false, scale) + } + + private fun drawFocusMatterAmount(guiGraphics: GuiGraphics, matterId: String, text: String, x: Int, y: Int, cellW: Int, color: Int) { + val textMaxW = cellW - FOCUS_MATTER_ICON_SIZE - FOCUS_MATTER_VALUE_GAP + if (textMaxW <= 0) return + val scale = min(focusMatterAmountScale(), textMaxW.toFloat() / max(1, font.width(text))).coerceAtLeast(0.35f) + val textH = ceil(font.lineHeight * scale).toInt() + val pairX = x + val iconY = y + (FOCUS_MATTER_ROW_H - FOCUS_MATTER_ICON_SIZE) / 2 + val textX = pairX + FOCUS_MATTER_ICON_SIZE + FOCUS_MATTER_VALUE_GAP + val textY = y + (FOCUS_MATTER_ROW_H - textH) / 2 + renderMatterIcon(guiGraphics, matterId, pairX, iconY, FOCUS_MATTER_ICON_SIZE, color == FOCUS_MATTER_ZERO_COLOR) + drawScaled(guiGraphics, text, textX, textY, color, false, scale) + } + + private fun focusMatterAmountColor(amount: Double): Int = + if (amount == 0.0) FOCUS_MATTER_ZERO_COLOR else 0xE6ECE8 + + private fun drawTrimmed(guiGraphics: GuiGraphics, text: String, x: Int, y: Int, maxWidth: Int, color: Int) = + drawTrimmed(guiGraphics, text, x, y, maxWidth, color, displayScale()) + + private fun drawTrimmed(guiGraphics: GuiGraphics, text: String, x: Int, y: Int, maxWidth: Int, color: Int, scale: Float) { + if (maxWidth <= 0) return + val logicalMaxWidth = max(1, (maxWidth / scale).toInt()) + val ellipsis = "..." + val rendered = + if (font.width(text) <= logicalMaxWidth) { + text + } else if (logicalMaxWidth <= font.width(ellipsis)) { + font.plainSubstrByWidth(text, logicalMaxWidth) + } else { + "${font.plainSubstrByWidth(text, logicalMaxWidth - font.width(ellipsis))}$ellipsis" + } + drawScaled(guiGraphics, rendered, x, y, color, false, scale) + } + + private fun drawRight(guiGraphics: GuiGraphics, text: String, rightX: Int, y: Int, color: Int) = + drawScaled(guiGraphics, text, rightX - scaledWidth(text), y, color, false) + + private fun drawCentered(guiGraphics: GuiGraphics, text: String, centerX: Int, y: Int, color: Int) = + drawScaled(guiGraphics, text, centerX - scaledWidth(text) / 2, y, color, false) + + private fun drawCentered(guiGraphics: GuiGraphics, text: String, centerX: Int, y: Int, color: Int, scale: Float) = + drawScaled(guiGraphics, text, centerX - scaledWidth(text, scale) / 2, y, color, false, scale) + + private fun drawScaled(guiGraphics: GuiGraphics, text: String, x: Int, y: Int, color: Int, shadow: Boolean) = + drawScaled(guiGraphics, text, x, y, color, shadow, displayScale()) + + private fun drawScaled(guiGraphics: GuiGraphics, text: String, x: Int, y: Int, color: Int, shadow: Boolean, scale: Float) { + if (scale == 1f) { + guiGraphics.drawString(font, text, x, y, color, shadow) + return + } + guiGraphics.pose().pushPose() + guiGraphics.pose().translate(x.toFloat(), y.toFloat(), 0f) + guiGraphics.pose().scale(scale, scale, 1f) + guiGraphics.drawString(font, text, 0, 0, color, shadow) + guiGraphics.pose().popPose() + } + + private fun scaledWidth(text: String): Int = + scaledWidth(text, displayScale()) + + private fun scaledWidth(text: String, scale: Float): Int = + ceil(font.width(text) * scale).toInt() + + private fun scaledPixels(value: Int): Int = + MatterIndexDisplayScale.scalePixels(value, displayScalePercent) + + private fun displayScale(): Float = + MatterIndexDisplayScale.toScale(displayScalePercent) + + private fun itemNameScale(): Float = + displayScale() * ITEM_NAME_SCALE + + private fun tableMatterAmountScale(): Float = + displayScale() * AMOUNT_SCALE + + private fun focusMatterAmountScale(): Float = + displayScale() * FOCUS_MATTER_AMOUNT_SCALE + + private fun tableMatterColumnMinWidth(): Int = + scaledWidth(MATTER_AMOUNT_WIDTH_SAMPLE, tableMatterAmountScale()) + MATTER_AMOUNT_HORIZONTAL_PADDING + + private fun adjustDisplayScale(steps: Int) { + displayScalePercent = MatterIndexDisplayScale.adjust(displayScalePercent, steps) + } + + private fun renderDisplayScaleControl(guiGraphics: GuiGraphics, layout: Layout, mouseX: Int, mouseY: Int) { + val control = displayScaleControl(layout) + drawScaleButton(guiGraphics, control.minus, "-", displayScalePercent > MatterIndexDisplayScale.MIN_PERCENT, contains(control.minus, mouseX, mouseY)) + drawScaleButton(guiGraphics, control.plus, "+", displayScalePercent < MatterIndexDisplayScale.MAX_PERCENT, contains(control.plus, mouseX, mouseY)) + val label = MatterIndexDisplayScale.label(displayScalePercent) + guiGraphics.drawString(font, label, control.label.x + (control.label.w - font.width(label)) / 2, control.label.y + 4, 0xBFD0CA, false) + } + + private fun drawScaleButton(guiGraphics: GuiGraphics, rect: Rect, text: String, enabled: Boolean, hovered: Boolean) { + val fill = + when { + !enabled -> 0x33151918 + hovered -> 0x6636D84A + else -> 0x44151918 + } + val color = if (enabled) 0xDDEFE7 else 0x66706F6A + guiGraphics.fill(rect.x, rect.y, rect.x + rect.w, rect.y + rect.h, fill) + drawBorder(guiGraphics, rect, if (enabled) 0x8841514D.toInt() else 0x4441514D) + guiGraphics.drawString(font, text, rect.x + (rect.w - font.width(text)) / 2, rect.y + 4, color, false) + } + + private fun clickDisplayScaleControl(layout: Layout, mouseX: Int, mouseY: Int): Boolean { + val control = displayScaleControl(layout) + if (contains(control.minus, mouseX, mouseY)) { + adjustDisplayScale(-1) + return true + } + if (contains(control.plus, mouseX, mouseY)) { + adjustDisplayScale(1) + return true + } + return false + } + + private fun displayScaleControl(layout: Layout): DisplayScaleControl { + val x = max(layout.index.x + 16, layout.index.x + layout.index.w - 82) + val y = layout.index.y + 11 + return DisplayScaleControl( + minus = Rect(x, y, 16, 16), + label = Rect(x + 18, y, 44, 16), + plus = Rect(x + 64, y, 16, 16), + ) + } + + private fun headerText(text: String, column: String): String = + if (sort.column == column) "$text ${sortArrow()}" else text + + private fun sortArrow(): String = + if (sort.direction == MatterIndexSortDirection.ASC) "^" else "v" + + private fun toggleLabel(text: String, active: Boolean): Component = + Component.literal(if (active) "[$text]" else text) + + private fun formatAmount(amount: Double): String = + MatterAmountFormatter.compact(amount, NeoUiConfig.matterAmountNotation()) + + private fun setAmountTooltip(label: String, amount: Double) { + hoveredAmountTooltip = + listOf( + Component.literal(label), + Component.literal(MatterAmountFormatter.exact(amount)), + ) + } + + private fun contains(rect: Rect, x: Int, y: Int): Boolean = + x in rect.x until rect.x + rect.w && y in rect.y until rect.y + rect.h + + companion object { + private const val HEADER_H = 30 + private const val ROW_H = 26 + private const val FOCUS_MIN_W = 220 + private const val INDEX_HORIZONTAL_PADDING = 28 + private const val ITEM_COLUMN_WIDTH_RATIO = 0.22 + private const val ITEM_NAME_SCALE = 0.90f + private const val AMOUNT_SCALE = 0.80f + private const val MATTER_AMOUNT_WIDTH_SAMPLE = "88.8Qa" + private const val MATTER_AMOUNT_HORIZONTAL_PADDING = 8 + private const val TOTAL_VALUE_RIGHT_PADDING = 8 + private const val FOCUS_MATTER_COLUMNS = 4 + private const val FOCUS_MATTER_MIN_CELL_W = 26 + private const val FOCUS_MATTER_ROW_H = 18 + private const val FOCUS_MATTER_ICON_SIZE = 8 + private const val FOCUS_MATTER_VALUE_GAP = 3 + private const val FOCUS_MATTER_AMOUNT_SCALE = 0.50f + private const val FOCUS_MATTER_ZERO_COLOR = 0x8EA09A + private const val SCROLLBAR_W = 4 + private const val SCROLLBAR_HIT_PAD = 3 + private const val SLOT_SIZE = 28 + private const val SLOT_STEP = 34 + + fun hasMatter(itemId: String): Boolean = + ClientReplicationCalculation.DEFAULT_MATTER_COMPOUND.containsKey(itemId) + + fun itemId(stack: ItemStack): String = + BuiltInRegistries.ITEM.getKey(stack.item).toString() + + private fun loadEntries(): List = + ClientReplicationCalculation.DEFAULT_MATTER_COMPOUND.entries.mapNotNull { (id, compound) -> + val item = BuiltInRegistries.ITEM.get(ResourceLocation.parse(id)) + val stack = item.defaultInstance + if (stack.isEmpty) return@mapNotNull null + val tags = stack.tags.map { "#${it.location()}" }.toList().joinToString(" ") + MatterIndexEntry( + record = MatterIndexRecord( + itemId = id, + displayName = stack.hoverName.string, + matterValues = compound.toMatterMap(), + replicatable = !stack.`is`(ReplicationTags.CANT_BE_SCANNED), + disintegratable = !stack.`is`(ReplicationTags.CANT_BE_DISINTEGRATED), + extraSearchText = tags, + ), + stack = stack, + ) + }.sortedBy { it.record.displayName.lowercase() } + + private fun MatterCompound.toMatterMap(): Map = + values.values.associate { value -> + value.matter.name to value.amount + } + } + + private data class MatterIndexEntry( + val record: MatterIndexRecord, + val stack: ItemStack, + ) + + private data class Layout( + val index: Rect, + val focus: Rect, + ) + + private data class Columns( + val item: Rect, + val total: Rect, + val matter: List>, + val endX: Int, + ) + + private data class DisplayScaleControl( + val minus: Rect, + val label: Rect, + val plus: Rect, + ) + + private data class FocusMatterLayout( + val x: Int, + val y: Int, + val cellW: Int, + val columns: Int, + val rows: Int, + ) + + private enum class ScrollbarTarget { + INDEX, + EQUIVALENT, + } + + private data class DraggingScrollbar( + val target: ScrollbarTarget, + val thumbOffset: Int, + ) + + private data class Scrollbar( + val target: ScrollbarTarget, + val x: Int, + val y: Int, + val h: Int, + val total: Int, + val visible: Int, + val scroll: Int, + ) + + private data class Rect( + val x: Int, + val y: Int, + val w: Int, + val h: Int, + ) { + fun inflate(amount: Int): Rect = + Rect(x - amount, y - amount, w + amount * 2, h + amount * 2) + } +} diff --git a/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/command/MatterCommandTree.kt b/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/command/MatterCommandTree.kt index 9cb3768..7ea18bf 100644 --- a/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/command/MatterCommandTree.kt +++ b/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/command/MatterCommandTree.kt @@ -115,6 +115,12 @@ object MatterCommandTree { context, MatterCommandParsing.targetSelectorKey(context, target), ) + + MatterSelectorKind.MOD -> + MatterCommandActions.getSelector( + context, + MatterCommandParsing.targetSelectorKey(context, target), + ) } private fun targetSetSelector(target: NeoMatterCommand): ArgumentBuilder = diff --git a/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/config/NeoUiConfig.kt b/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/config/NeoUiConfig.kt new file mode 100644 index 0000000..8533e41 --- /dev/null +++ b/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/config/NeoUiConfig.kt @@ -0,0 +1,25 @@ +package com.p_nsk.replicated_integration.config + +import com.p_nsk.replicated_integration.api.client.MatterAmountNotation +import net.neoforged.neoforge.common.ModConfigSpec + +object NeoUiConfig { + private const val KEY_MATTER_AMOUNT_NOTATION = "matterAmountNotation" + + val spec: ModConfigSpec + private val matterAmountNotationValue: ModConfigSpec.EnumValue + + init { + val builder = ModConfigSpec.Builder() + builder.push("matterIndex") + matterAmountNotationValue = + builder + .comment("Notation used for compact matter amounts. SI uses K/M/G/T; SHORT_SCALE uses K/M/B/T.") + .translation("replicated_integration.config.ui.matter_amount_notation") + .defineEnum(KEY_MATTER_AMOUNT_NOTATION, MatterAmountNotation.SHORT_SCALE) + builder.pop() + spec = builder.build() + } + + fun matterAmountNotation(): MatterAmountNotation = matterAmountNotationValue.get() +} diff --git a/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/core/Aliases.kt b/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/core/Aliases.kt index ad61216..c207b44 100644 --- a/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/core/Aliases.kt +++ b/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/core/Aliases.kt @@ -22,6 +22,8 @@ fun NeoMatterNodeRegistry.node( id = id, displayName = displayName, formatter = formatter, + enumerateNodes = builder.buildEnumerator(), + rawValue = builder.buildRawValue(), commands = builder.buildCommands(), ) ) diff --git a/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/core/NeoMatterNodeBuilder.kt b/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/core/NeoMatterNodeBuilder.kt index f9a1500..4a8b941 100644 --- a/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/core/NeoMatterNodeBuilder.kt +++ b/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/core/NeoMatterNodeBuilder.kt @@ -3,6 +3,7 @@ package com.p_nsk.replicated_integration.core import com.mojang.brigadier.suggestion.SuggestionProvider import com.p_nsk.replicated_integration.api.model.LiteResourceLocation import com.p_nsk.replicated_integration.api.node.MatterNodeCommands +import com.p_nsk.replicated_integration.api.node.NodeKey import com.p_nsk.replicated_integration.api.selector.MatterSelectorKind import net.minecraft.commands.CommandSourceStack import net.minecraft.resources.ResourceLocation @@ -13,6 +14,8 @@ class NeoMatterNodeBuilder( ) { private var valueCommand: NeoMatterCommand? = null private var tagCommand: NeoMatterCommand? = null + private var enumerateNodes: () -> Iterable = { emptyList() } + private var rawValue: (LiteResourceLocation) -> Any? = { null } fun value( literal: String, @@ -50,9 +53,21 @@ class NeoMatterNodeBuilder( ) } + fun enumerate(nodes: () -> Iterable) { + enumerateNodes = nodes + } + + fun rawValue(value: (LiteResourceLocation) -> Any?) { + rawValue = value + } + fun buildCommands(): MatterNodeCommands = MatterNodeCommands( value = valueCommand, tag = tagCommand, ) + + fun buildEnumerator(): () -> Iterable = enumerateNodes + + fun buildRawValue(): (LiteResourceLocation) -> Any? = rawValue } diff --git a/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/core/NeoRecipeConversionSupport.kt b/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/core/NeoRecipeConversionSupport.kt index 93c917c..c86e74c 100644 --- a/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/core/NeoRecipeConversionSupport.kt +++ b/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/core/NeoRecipeConversionSupport.kt @@ -1,14 +1,17 @@ package com.p_nsk.replicated_integration.core +import com.buuz135.replication.util.ReplicationTags import com.p_nsk.replicated_integration.api.graph.IConversionSink import com.p_nsk.replicated_integration.api.model.InputNodes import com.p_nsk.replicated_integration.api.model.LiteResourceLocation import com.p_nsk.replicated_integration.api.model.NodeAmount import com.p_nsk.replicated_integration.api.model.MatterConversion +import com.p_nsk.replicated_integration.api.node.MatterNodes import com.p_nsk.replicated_integration.api.node.NodeKey import net.minecraft.core.registries.BuiltInRegistries import net.minecraft.resources.ResourceLocation import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.Items import net.minecraft.world.item.crafting.Ingredient object NeoRecipeConversionSupport { @@ -26,6 +29,12 @@ object NeoRecipeConversionSupport { if (consumes.isEmpty()) { return } + // Passive Replication compatibility: this tag only blocks recipe-derived + // calculations. Explicit matter values and repint deny/set overrides stay + // in the higher-level default/selector pipeline. + if (produces.node.isSkipCalculationTaggedItem()) { + return + } collector.add( MatterConversion( id = id.toLite(), @@ -134,6 +143,15 @@ object NeoRecipeConversionSupport { ingredientInputNodesCache.clear() } + private fun NodeKey.isSkipCalculationTaggedItem(): Boolean { + if (type != MatterNodes.ITEM) { + return false + } + val itemId = ResourceLocation.fromNamespaceAndPath(id.namespace, id.path) + val item = BuiltInRegistries.ITEM.get(itemId).takeUnless { it == Items.AIR } ?: return false + return ItemStack(item).`is`(ReplicationTags.SKIP_CALCULATION) + } + fun ResourceLocation.toLite(): LiteResourceLocation = LiteResourceLocation.of(namespace, path) } diff --git a/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/core/NeoReplicationCalculationService.kt b/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/core/NeoReplicationCalculationService.kt index 4937639..ed3c7d1 100644 --- a/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/core/NeoReplicationCalculationService.kt +++ b/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/core/NeoReplicationCalculationService.kt @@ -17,11 +17,13 @@ import com.p_nsk.replicated_integration.api.node.MatterNodes import com.p_nsk.replicated_integration.api.node.MutableMatterDefaults import com.p_nsk.replicated_integration.api.selector.MutableMatterSelectors import com.p_nsk.replicated_integration.api.selector.MatterSelectorMaterializer +import com.p_nsk.replicated_integration.api.selector.formatSelector import com.p_nsk.replicated_integration.api.addon.ReplicationAddonRegistry import com.p_nsk.replicated_integration.api.graph.SimpleConversionSolver import com.p_nsk.replicated_integration.debug.MatterNodeDebugCache import com.p_nsk.replicated_integration.data.NeoMatterConfigOverrides import com.p_nsk.replicated_integration.data.NeoMatterRuntimeOverrides +import com.p_nsk.replicated_integration.kubejs.NeoKubeJSMatterRules import com.p_nsk.replicated_integration.network.NeoReplicationCalculationSyncPayload import net.minecraft.core.registries.BuiltInRegistries import net.minecraft.nbt.CompoundTag @@ -46,6 +48,7 @@ object NeoReplicationCalculationService { @Volatile private var latestSyncId: Long = 0L + private val warnedKubeJSConfigOverlaps = mutableSetOf() private val addons: ReplicationAddonRegistry by lazy { ReplicationAddonRegistry(NeoReplicationAddonCollector.addons()) @@ -68,11 +71,24 @@ object NeoReplicationCalculationService { addon.collectDefaults(context, defaults) addon.collectSelectors(context, selectors) } - selectors.putAll(NeoMatterConfigOverrides.snapshot()) - selectors.putAll(NeoMatterRuntimeOverrides.snapshot(server)) + val configRules = NeoMatterConfigOverrides.snapshotRules() + val kubeJSRules = NeoKubeJSMatterRules.snapshot() + warnKubeJSConfigOverlap(configRules, kubeJSRules) + selectors.putRules(configRules) + selectors.putRules(kubeJSRules) + selectors.putRules(NeoMatterRuntimeOverrides.snapshotRules(server)) val selectorSnapshot = selectors.snapshot() - defaults.putAll(MatterSelectorMaterializer.materialize(selectorSnapshot, ::expandSelectorTag)) + val selectorRules = selectors.snapshotRules() + defaults.putAll( + MatterSelectorMaterializer.materialize( + selectorRules, + ::expandSelectorTag, + ::expandSelectorMod, + ::allMatterNodes, + ::rawValue, + ), + ) val explicitSnapshot = defaults.snapshot() for (addon in activeAddons) { addon.collectConversions(context, builder) @@ -183,6 +199,38 @@ object NeoReplicationCalculationService { } } + private fun expandSelectorMod(type: LiteResourceLocation, modId: String): Iterable = + when (type) { + MatterNodes.ITEM -> BuiltinNodeResolver.itemNodesInMod(modId) + MatterNodes.FLUID -> BuiltinNodeResolver.fluidNodesInMod(modId) + MatterNodes.CHEMICAL -> MekanismNodeResolver.chemicalNodesInMod(modId) + else -> emptyList() + } + + private fun allMatterNodes(): Iterable = + matterNodeRegistry().all().flatMap { it.enumerateNodes() } + + private fun rawValue(node: NodeKey): Any? = + matterNodeRegistry().get(node.type)?.rawValue?.invoke(node.id) + + private fun warnKubeJSConfigOverlap( + configRules: List, + kubeJSRules: List, + ) { + val configKeys = configRules.mapNotNullTo(linkedSetOf()) { it.selector.staticKey } + val kubeKeys = kubeJSRules.mapNotNullTo(linkedSetOf()) { it.selector.staticKey } + for (key in configKeys.intersect(kubeKeys)) { + val warningKey = key.toString() + if (!warnedKubeJSConfigOverlaps.add(warningKey)) { + continue + } + Constants.LOGGER.warn( + "Matter selector {} is defined by both config and KubeJS; KubeJS has higher source priority", + matterNodeRegistry().formatSelector(key), + ) + } + } + fun syncLatestToPlayer(player: ServerPlayer) { if (latestSyncTag.isEmpty) { return diff --git a/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/data/NeoMatterConfigOverrides.kt b/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/data/NeoMatterConfigOverrides.kt index ab31c81..d5727fc 100644 --- a/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/data/NeoMatterConfigOverrides.kt +++ b/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/data/NeoMatterConfigOverrides.kt @@ -2,18 +2,26 @@ package com.p_nsk.replicated_integration.data 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.selector.MatterRule import com.p_nsk.replicated_integration.api.selector.MatterSelectorKey import com.p_nsk.replicated_integration.api.selector.MatterSelectorStorageCodec +import com.p_nsk.replicated_integration.api.selector.asRuleSelector import net.minecraft.server.MinecraftServer import net.neoforged.fml.loading.FMLPaths import java.nio.file.Path object NeoMatterConfigOverrides { private var loadedFrom: Path? = null - private var values: Map = emptyMap() + private var values: List = emptyList() @Synchronized fun snapshot(): Map { + loadIfNeeded() + return values.toStaticMap() + } + + @Synchronized + fun snapshotRules(): List { loadIfNeeded() return values } @@ -25,7 +33,7 @@ object NeoMatterConfigOverrides { if (runtime.isEmpty()) { return 0 } - values = values.toMutableMap().also { it.putAll(runtime.mapValues { (_, value) -> value.asConfigValue() }) } + values += runtime.map { (selector, value) -> MatterRule(selector.asRuleSelector(), value.asConfigValue()) } save() NeoMatterRuntimeOverrides.clear(server) return runtime.size @@ -37,12 +45,12 @@ object NeoMatterConfigOverrides { return } loadedFrom = path - values = MatterSelectorStorageCodec.load(path, ExplicitMatterSource.CONFIG) + values = MatterSelectorStorageCodec.loadRules(path, ExplicitMatterSource.CONFIG) } private fun save() { val path = filePath() - MatterSelectorStorageCodec.save(path, values) + MatterSelectorStorageCodec.saveRules(path, values) } private fun filePath(): Path = @@ -53,4 +61,13 @@ object NeoMatterConfigOverrides { is ExplicitMatterValue.Deny -> ExplicitMatterValue.Deny(ExplicitMatterSource.CONFIG) is ExplicitMatterValue.Set -> ExplicitMatterValue.Set(compound, ExplicitMatterSource.CONFIG) } + + private fun List.toStaticMap(): Map { + val map = linkedMapOf() + for (rule in this.sortedWith(compareBy { it.source.priority })) { + val key = rule.selector.staticKey ?: continue + map[key] = rule.value + } + return map + } } diff --git a/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/data/NeoMatterRuntimeOverrides.kt b/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/data/NeoMatterRuntimeOverrides.kt index 317d1eb..099bb60 100644 --- a/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/data/NeoMatterRuntimeOverrides.kt +++ b/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/data/NeoMatterRuntimeOverrides.kt @@ -3,8 +3,10 @@ package com.p_nsk.replicated_integration.data 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.selector.MatterRule import com.p_nsk.replicated_integration.api.selector.MatterSelectorKey import com.p_nsk.replicated_integration.api.selector.MatterSelectorStorageCodec +import com.p_nsk.replicated_integration.api.selector.asRuleSelector import net.minecraft.server.MinecraftServer import net.minecraft.world.level.storage.LevelResource import java.nio.file.Path @@ -19,6 +21,12 @@ object NeoMatterRuntimeOverrides { return values } + @Synchronized + fun snapshotRules(server: MinecraftServer): List { + loadIfNeeded(server) + return values.map { (selector, value) -> MatterRule(selector.asRuleSelector(), value) } + } + @Synchronized fun set( server: MinecraftServer, diff --git a/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/data/NeoReplicatedIntegrationLanguageProvider.kt b/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/data/NeoReplicatedIntegrationLanguageProvider.kt index bb2925f..15fe43b 100644 --- a/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/data/NeoReplicatedIntegrationLanguageProvider.kt +++ b/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/data/NeoReplicatedIntegrationLanguageProvider.kt @@ -17,7 +17,10 @@ class NeoReplicatedIntegrationLanguageProvider( private fun addEnglishTranslations() { add("replicated_integration.config.enable_nuclear_recipe", "Enable Mekanism nuclear recipe support") + add("replicated_integration.config.ui.matter_amount_notation", "Matter amount notation") add("replicated_integration.config.notice.compatibility_summary_shown", "Compatibility summary notice shown") + add("key.categories.replicated_integration", "Replicated Integration") + add("key.replicated_integration.open_replication_index", "Open Replication Index") add("replicated_integration.chat.notice.title", "Replicated Integration") add( "replicated_integration.chat.notice.supported_integrations", @@ -35,7 +38,10 @@ class NeoReplicatedIntegrationLanguageProvider( private fun addJapaneseTranslations() { add("replicated_integration.config.enable_nuclear_recipe", "Mekanism の核分裂レシピ対応を有効化") + add("replicated_integration.config.ui.matter_amount_notation", "Matter 量の表記") add("replicated_integration.config.notice.compatibility_summary_shown", "互換性サマリー通知を表示済み") + add("key.categories.replicated_integration", "Replicated Integration") + add("key.replicated_integration.open_replication_index", "Replication Index を開く") add("replicated_integration.chat.notice.title", "Replicated Integration") add( "replicated_integration.chat.notice.supported_integrations", diff --git a/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/kubejs/NeoKubeJSMatterRules.kt b/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/kubejs/NeoKubeJSMatterRules.kt new file mode 100644 index 0000000..4750826 --- /dev/null +++ b/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/kubejs/NeoKubeJSMatterRules.kt @@ -0,0 +1,13 @@ +package com.p_nsk.replicated_integration.kubejs + +import com.p_nsk.replicated_integration.api.selector.MatterRule +import net.neoforged.fml.ModList + +object NeoKubeJSMatterRules { + fun snapshot(): List = + if (ModList.get().isLoaded("kubejs")) { + NeoRepIntKubeJSEvents.collectRules() + } else { + emptyList() + } +} diff --git a/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/kubejs/NeoRepInt.kt b/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/kubejs/NeoRepInt.kt new file mode 100644 index 0000000..0e26e51 --- /dev/null +++ b/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/kubejs/NeoRepInt.kt @@ -0,0 +1,30 @@ +package com.p_nsk.replicated_integration.kubejs + +import com.p_nsk.replicated_integration.api.kubejs.RepInt +import com.p_nsk.replicated_integration.api.node.MatterNodes +import com.p_nsk.replicated_integration.api.selector.MatterRuleSelector +import com.p_nsk.replicated_integration.api.selector.NodeView +import net.minecraft.world.item.Item +import java.util.function.Predicate +@Suppress("unused") +object NeoRepInt { + @JvmStatic fun node(type: String, id: String): MatterRuleSelector = RepInt.node(type, id) + @JvmStatic fun nodeTag(type: String, tag: String): MatterRuleSelector = RepInt.nodeTag(type, tag) + @JvmStatic fun nodeMod(type: String, modId: String): MatterRuleSelector = RepInt.nodeMod(type, modId) + @JvmStatic fun item(id: String): MatterRuleSelector = RepInt.item(id) + @JvmStatic fun itemTag(tag: String): MatterRuleSelector = RepInt.itemTag(tag) + @JvmStatic fun itemMod(modId: String): MatterRuleSelector = RepInt.itemMod(modId) + @JvmStatic fun fluid(id: String): MatterRuleSelector = RepInt.fluid(id) + @JvmStatic fun fluidTag(tag: String): MatterRuleSelector = RepInt.fluidTag(tag) + @JvmStatic fun fluidMod(modId: String): MatterRuleSelector = RepInt.fluidMod(modId) + @JvmStatic fun anyOf(vararg selectors: Any): MatterRuleSelector = RepInt.anyOf(*selectors) + @JvmStatic fun allOf(vararg selectors: Any): MatterRuleSelector = RepInt.allOf(*selectors) + @JvmStatic fun not(selector: Any): MatterRuleSelector = RepInt.not(selector) + @JvmStatic fun whereNode(name: String, predicate: Predicate): MatterRuleSelector = RepInt.whereNode(name, predicate) + + @JvmStatic + fun whereItem(name: String, predicate: Predicate): MatterRuleSelector = + RepInt.whereNode(name) { view -> + view.key.type == MatterNodes.ITEM && view.getRaw()?.let(predicate::test) == true + } +} diff --git a/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/kubejs/NeoRepIntKubeJSPlugin.kt b/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/kubejs/NeoRepIntKubeJSPlugin.kt new file mode 100644 index 0000000..8f58470 --- /dev/null +++ b/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/kubejs/NeoRepIntKubeJSPlugin.kt @@ -0,0 +1,46 @@ +package com.p_nsk.replicated_integration.kubejs + +import com.p_nsk.replicated_integration.api.kubejs.RepIntMatterEvent +import com.p_nsk.replicated_integration.api.selector.MatterRule +import dev.latvian.mods.kubejs.event.EventGroup +import dev.latvian.mods.kubejs.event.EventGroupRegistry +import dev.latvian.mods.kubejs.event.KubeEvent +import dev.latvian.mods.kubejs.plugin.ClassFilter +import dev.latvian.mods.kubejs.plugin.KubeJSPlugin +import dev.latvian.mods.kubejs.script.BindingRegistry + +class NeoRepIntKubeJSPlugin : KubeJSPlugin { + override fun registerClasses(filter: ClassFilter) { + filter.allow("com.p_nsk.replicated_integration") + } + + override fun registerEvents(registry: EventGroupRegistry) { + registry.register(NeoRepIntKubeJSEvents.GROUP) + } + + override fun registerBindings(registry: BindingRegistry) { + registry.add("RepInt", NeoRepInt::class.java) + } +} + +object NeoRepIntKubeJSEvents { + val GROUP: EventGroup = EventGroup.of("RepIntEvents") + val MATTER = GROUP.server("matter") { NeoRepIntMatterEvent::class.java } + + fun collectRules(): List { + val event = NeoRepIntMatterEvent() + MATTER.post(event) + return event.rules + } +} +@Suppress("unused") +class NeoRepIntMatterEvent : KubeEvent { + private val delegate = RepIntMatterEvent() + + val rules: List + get() = delegate.rules + + fun deny(selector: Any) = delegate.deny(selector) + + fun set(selector: Any, matter: Map) = delegate.set(selector, matter) +} diff --git a/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/kubejs/NeoRepIntMek.kt b/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/kubejs/NeoRepIntMek.kt new file mode 100644 index 0000000..4372211 --- /dev/null +++ b/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/kubejs/NeoRepIntMek.kt @@ -0,0 +1,11 @@ +package com.p_nsk.replicated_integration.kubejs + +import com.p_nsk.replicated_integration.api.kubejs.RepInt +import com.p_nsk.replicated_integration.api.node.MatterNodes +import com.p_nsk.replicated_integration.api.selector.MatterRuleSelector + +object NeoRepIntMek { + @JvmStatic fun chemical(id: String): MatterRuleSelector = RepInt.node(MatterNodes.CHEMICAL.toString(), id) + @JvmStatic fun chemicalTag(tag: String): MatterRuleSelector = RepInt.nodeTag(MatterNodes.CHEMICAL.toString(), tag) + @JvmStatic fun chemicalMod(modId: String): MatterRuleSelector = RepInt.nodeMod(MatterNodes.CHEMICAL.toString(), modId) +} diff --git a/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/kubejs/NeoRepIntMekKubeJSPlugin.kt b/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/kubejs/NeoRepIntMekKubeJSPlugin.kt new file mode 100644 index 0000000..74e8613 --- /dev/null +++ b/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/kubejs/NeoRepIntMekKubeJSPlugin.kt @@ -0,0 +1,10 @@ +package com.p_nsk.replicated_integration.kubejs + +import dev.latvian.mods.kubejs.plugin.KubeJSPlugin +import dev.latvian.mods.kubejs.script.BindingRegistry +@Suppress("unused") +class NeoRepIntMekKubeJSPlugin : KubeJSPlugin { + override fun registerBindings(registry: BindingRegistry) { + registry.add("RepIntMek", NeoRepIntMek::class.java) + } +} diff --git a/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/kubejs/NeoRepIntProbeJSPlugin.kt b/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/kubejs/NeoRepIntProbeJSPlugin.kt new file mode 100644 index 0000000..f8f0d85 --- /dev/null +++ b/1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/kubejs/NeoRepIntProbeJSPlugin.kt @@ -0,0 +1,61 @@ +package com.p_nsk.replicated_integration.kubejs + +import moe.wolfgirl.probejs.plugin.ProbeJSPlugin +import moe.wolfgirl.probejs.plugin.builtins.alias.RegistryTypes +import moe.wolfgirl.probejs.plugin.builtins.alias.SpecialTypes +import moe.wolfgirl.probejs.typescript.Documents +import moe.wolfgirl.probejs.typescript.document.base.Type +import moe.wolfgirl.probejs.typescript.document.members.MethodDecl +import moe.wolfgirl.probejs.typescript.document.members.ParamDecl + +class NeoRepIntProbeJSPlugin : ProbeJSPlugin() { + override fun transformClass(document: Documents.ClassDocument) { + val className = document.classInfo().clazz().name + if (className !in PROBE_CLASS_NAMES) return + + document.methodDocs() + .map { it.second } + .forEach { method -> + PARAM_OVERRIDES[className]?.get(method.name)?.let { paramOverride -> + replaceParam(method, paramOverride.index, paramOverride.type) + } + } + } + + private fun replaceParam(method: MethodDecl, index: Int, type: Type) { + if (index !in method.params.indices) return + method.params = method.params.mapIndexed { paramIndex, param -> + if (paramIndex == index) { + ParamDecl(param.name, type, param.isRest, param.optional) + } else { + param + } + } + } + + companion object { + private data class ParamOverride(val index: Int, val type: Type) + + private const val REP_INT_CLASS_NAME = "com.p_nsk.replicated_integration.kubejs.NeoRepInt" + private const val REP_INT_MEK_CLASS_NAME = "com.p_nsk.replicated_integration.kubejs.NeoRepIntMek" + + private val PROBE_CLASS_NAMES = setOf(REP_INT_CLASS_NAME, REP_INT_MEK_CLASS_NAME) + + private val PARAM_OVERRIDES: Map> = mapOf( + REP_INT_CLASS_NAME to mapOf( + "nodeMod" to ParamOverride(1, SpecialTypes.MOD_ID), + "item" to ParamOverride(0, RegistryTypes.`object`("Item")), + "itemTag" to ParamOverride(0, RegistryTypes.tag("Item")), + "itemMod" to ParamOverride(0, SpecialTypes.MOD_ID), + "fluid" to ParamOverride(0, RegistryTypes.`object`("Fluid")), + "fluidTag" to ParamOverride(0, RegistryTypes.tag("Fluid")), + "fluidMod" to ParamOverride(0, SpecialTypes.MOD_ID), + ), + REP_INT_MEK_CLASS_NAME to mapOf( + "chemical" to ParamOverride(0, RegistryTypes.`object`("MekanismChemical")), + "chemicalTag" to ParamOverride(0, RegistryTypes.tag("MekanismChemical")), + "chemicalMod" to ParamOverride(0, SpecialTypes.MOD_ID), + ), + ) + } +} diff --git a/1.21.1-neo/src/main/resources/kubejs.plugins.txt b/1.21.1-neo/src/main/resources/kubejs.plugins.txt new file mode 100644 index 0000000..571da96 --- /dev/null +++ b/1.21.1-neo/src/main/resources/kubejs.plugins.txt @@ -0,0 +1,3 @@ +com.p_nsk.replicated_integration.kubejs.NeoRepIntKubeJSPlugin +com.p_nsk.replicated_integration.kubejs.NeoRepIntProbeJSPlugin probejs +com.p_nsk.replicated_integration.kubejs.NeoRepIntMekKubeJSPlugin mekanism diff --git a/1.21.1-neo/src/main/resources/replicated_integration.mixins.json b/1.21.1-neo/src/main/resources/replicated_integration.mixins.json index afce7a8..9c3dc95 100644 --- a/1.21.1-neo/src/main/resources/replicated_integration.mixins.json +++ b/1.21.1-neo/src/main/resources/replicated_integration.mixins.json @@ -8,6 +8,9 @@ "MatterValueRecipeMixin", "ReplicationCalculationMixin" ], + "client": [ + "ScreenAccessor" + ], "injectors": { "defaultRequire": 1 }, diff --git a/build.gradle.kts b/build.gradle.kts index 517ed8a..f5aeb71 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -77,6 +77,14 @@ subprojects { url = uri("https://thedarkcolour.github.io/KotlinForForge/") } + maven { + name = "Architectury" + url = uri("https://maven.architectury.dev/") + content { + includeGroup("dev.architectury") + } + } + maven { name = "covers1624" url = uri("https://maven.covers1624.net/") @@ -86,6 +94,14 @@ subprojects { } } + maven { + name = "JitPack" + url = uri("https://jitpack.io") + content { + includeGroup("com.github.rtyley") + } + } + maven { name = "Curse Maven" url = uri("https://cursemaven.com") @@ -93,6 +109,15 @@ subprojects { includeGroup("curse.maven") } } + + maven { + name = "Latvian" + url = uri("https://maven.latvian.dev/releases") + content { + includeGroup ("dev.latvian.mods") + includeGroup ("dev.latvian.apps") + } + } } tasks.withType().configureEach { diff --git a/buildSrc/src/main/kotlin/legacyforge-mod-conventions.gradle.kts b/buildSrc/src/main/kotlin/legacyforge-mod-conventions.gradle.kts index ebd047a..fa1f4fd 100644 --- a/buildSrc/src/main/kotlin/legacyforge-mod-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/legacyforge-mod-conventions.gradle.kts @@ -76,12 +76,14 @@ legacyForge { create("server") { server() + gameDirectory = project.file("run-server") programArgument("--nogui") systemProperty("forge.enabledGameTestNamespaces", modId) } create("gameTestServer") { type = "gameTestServer" + gameDirectory = project.file("run-server") systemProperty("forge.enabledGameTestNamespaces", modId) } diff --git a/common/src/main/kotlin/com/p_nsk/replicated_integration/api/client/MatterAmountFormatter.kt b/common/src/main/kotlin/com/p_nsk/replicated_integration/api/client/MatterAmountFormatter.kt new file mode 100644 index 0000000..ac67954 --- /dev/null +++ b/common/src/main/kotlin/com/p_nsk/replicated_integration/api/client/MatterAmountFormatter.kt @@ -0,0 +1,70 @@ +package com.p_nsk.replicated_integration.api.client + +import java.text.DecimalFormat +import java.text.DecimalFormatSymbols +import java.util.Locale +import kotlin.math.abs +import kotlin.math.floor +import kotlin.math.log10 +import kotlin.math.pow + +enum class MatterAmountNotation { + SI, + SHORT_SCALE, +} + +object MatterAmountFormatter { + private val exactFormat = DecimalFormat("#,##0.##", DecimalFormatSymbols(Locale.ROOT)) + private val siSuffixes = listOf("", "K", "M", "G", "T", "P", "E") + private val shortScaleSuffixes = listOf("", "K", "M", "B", "T", "Qa", "Qi") + + fun compact( + amount: Double, + notation: MatterAmountNotation, + ): String { + if (!amount.isFinite()) return amount.toString() + if (amount == 0.0) return "0" + val suffixes = if (notation == MatterAmountNotation.SI) siSuffixes else shortScaleSuffixes + val absAmount = abs(amount) + var group = floor(log10(absAmount) / 3.0).toInt() + if (group > suffixes.lastIndex) return scientific(amount) + group = group.coerceAtLeast(0) + if (group == 0) return exact(amount) + + var scaled = amount / 1000.0.pow(group) + if (abs(scaled) >= 999.5 && group < suffixes.lastIndex) { + group += 1 + scaled = amount / 1000.0.pow(group) + } + return "${formatThreeSignificantDigits(scaled)}${suffixes[group]}" + } + + fun exact(amount: Double): String = + if (amount.isFinite()) exactFormat.format(amount) else amount.toString() + + fun parseNotation(value: String): MatterAmountNotation = + MatterAmountNotation.entries.firstOrNull { it.name.equals(value, ignoreCase = true) } + ?: MatterAmountNotation.SHORT_SCALE + + private fun formatThreeSignificantDigits(value: Double): String { + val absValue = abs(value) + val decimals = + when { + absValue >= 100.0 -> 0 + absValue >= 10.0 -> 1 + else -> 2 + } + val formatted = "%.${decimals}f".format(Locale.ROOT, value) + return if ('.' in formatted) { + formatted.trimEnd('0').trimEnd('.') + } else { + formatted + } + } + + private fun scientific(value: Double): String { + val exponent = floor(log10(abs(value))).toInt() + val mantissa = value / 10.0.pow(exponent) + return "${formatThreeSignificantDigits(mantissa)}e$exponent" + } +} diff --git a/common/src/main/kotlin/com/p_nsk/replicated_integration/api/client/MatterIndexDisplayScale.kt b/common/src/main/kotlin/com/p_nsk/replicated_integration/api/client/MatterIndexDisplayScale.kt new file mode 100644 index 0000000..1b53eea --- /dev/null +++ b/common/src/main/kotlin/com/p_nsk/replicated_integration/api/client/MatterIndexDisplayScale.kt @@ -0,0 +1,20 @@ +package com.p_nsk.replicated_integration.api.client + +object MatterIndexDisplayScale { + const val MIN_PERCENT = 70 + const val DEFAULT_PERCENT = 100 + const val MAX_PERCENT = 130 + const val STEP_PERCENT = 10 + + fun adjust(currentPercent: Int, steps: Int): Int = + (currentPercent + steps * STEP_PERCENT).coerceIn(MIN_PERCENT, MAX_PERCENT) + + fun toScale(percent: Int): Float = + percent.coerceIn(MIN_PERCENT, MAX_PERCENT) / 100f + + fun scalePixels(value: Int, percent: Int): Int = + (value * toScale(percent)).toInt().coerceAtLeast(1) + + fun label(percent: Int): String = + "${percent.coerceIn(MIN_PERCENT, MAX_PERCENT)}%" +} diff --git a/common/src/main/kotlin/com/p_nsk/replicated_integration/api/client/MatterIndexModels.kt b/common/src/main/kotlin/com/p_nsk/replicated_integration/api/client/MatterIndexModels.kt new file mode 100644 index 0000000..a7491c1 --- /dev/null +++ b/common/src/main/kotlin/com/p_nsk/replicated_integration/api/client/MatterIndexModels.kt @@ -0,0 +1,147 @@ +package com.p_nsk.replicated_integration.api.client + +data class MatterIndexRecord( + val itemId: String, + val displayName: String, + val matterValues: Map, + val replicatable: Boolean = true, + val disintegratable: Boolean = true, + val extraSearchText: String = "", +) { + val namespace: String = itemId.substringBefore(':', "") + val total: Double = matterValues.values.sum() + val signature: String = + matterValues + .filterValues { it > 0.0 } + .entries + .sortedBy { it.key } + .joinToString("|") { (matter, amount) -> "$matter=${amount.formatSignatureAmount()}" } + + fun hasMatter(matterId: String): Boolean = + (matterValues[matterId] ?: 0.0) > 0.0 +} + +data class MatterIndexQuery( + val searchText: String = "", + val matterOnly: Boolean = true, + val replicatableOnly: Boolean = false, + val disintegratableOnly: Boolean = false, + val namespaceFilter: String? = null, + val matterFilter: String? = null, +) + +enum class MatterIndexSortDirection { + ASC, + DESC, +} + +data class MatterIndexSort( + val column: String = TOTAL_COLUMN, + val direction: MatterIndexSortDirection = MatterIndexSortDirection.DESC, +) { + companion object { + const val ITEM_COLUMN = "item" + const val TOTAL_COLUMN = "total" + const val MATTER_PREFIX = "matter:" + + fun matterColumn(matterId: String): String = "$MATTER_PREFIX$matterId" + } +} + +object MatterIndexFiltering { + fun parseQuery( + raw: String, + matterOnly: Boolean, + replicatableOnly: Boolean, + disintegratableOnly: Boolean, + ): MatterIndexQuery { + var namespaceFilter: String? = null + var matterFilter: String? = null + val textTerms = mutableListOf() + + for (token in raw.split(Regex("\\s+")).filter { it.isNotBlank() }) { + when { + token.startsWith("namespace:", ignoreCase = true) -> + namespaceFilter = token.substringAfter(':').ifBlank { null } + token.startsWith("mod:", ignoreCase = true) -> + namespaceFilter = token.substringAfter(':').ifBlank { null } + token.startsWith("matter:", ignoreCase = true) -> + matterFilter = token.substringAfter(':').ifBlank { null } + else -> textTerms += token + } + } + + return MatterIndexQuery( + searchText = textTerms.joinToString(" "), + matterOnly = matterOnly, + replicatableOnly = replicatableOnly, + disintegratableOnly = disintegratableOnly, + namespaceFilter = namespaceFilter, + matterFilter = matterFilter, + ) + } + + fun filterAndSort( + records: List, + query: MatterIndexQuery, + sort: MatterIndexSort, + ): List { + val normalizedSearch = query.searchText.trim().lowercase() + val namespace = query.namespaceFilter?.lowercase() + val matter = query.matterFilter?.lowercase() + + val filtered = + records.filter { record -> + if (query.matterOnly && record.total <= 0.0) return@filter false + if (query.replicatableOnly && !record.replicatable) return@filter false + if (query.disintegratableOnly && !record.disintegratable) return@filter false + if (namespace != null && record.namespace.lowercase() != namespace) return@filter false + if (matter != null && record.matterValues.none { (id, amount) -> id.lowercase().contains(matter) && amount > 0.0 }) { + return@filter false + } + if (normalizedSearch.isBlank()) return@filter true + record.displayName.lowercase().contains(normalizedSearch) || + record.itemId.lowercase().contains(normalizedSearch) || + record.namespace.lowercase().contains(normalizedSearch) || + record.extraSearchText.lowercase().contains(normalizedSearch) + } + + val comparator = + when { + sort.column == MatterIndexSort.ITEM_COLUMN -> + compareBy { it.displayName.lowercase() }.thenBy { it.itemId } + sort.column == MatterIndexSort.TOTAL_COLUMN -> + compareBy { it.total }.thenBy { it.displayName.lowercase() } + sort.column.startsWith(MatterIndexSort.MATTER_PREFIX) -> { + val matterId = sort.column.removePrefix(MatterIndexSort.MATTER_PREFIX) + compareBy { it.matterValues[matterId] ?: 0.0 }.thenBy { it.displayName.lowercase() } + } + else -> + compareBy { it.total }.thenBy { it.displayName.lowercase() } + } + + return if (sort.direction == MatterIndexSortDirection.ASC) { + filtered.sortedWith(comparator) + } else { + filtered.sortedWith(comparator.reversed()) + } + } + + fun equivalentRecords( + records: List, + selected: MatterIndexRecord?, + ): List { + val signature = selected?.signature + if (signature.isNullOrBlank()) return emptyList() + return records + .filter { it.signature == signature } + .sortedBy { it.displayName.lowercase() } + } +} + +private fun Double.formatSignatureAmount(): String = + if (this % 1.0 == 0.0) { + toLong().toString() + } else { + toString() + } diff --git a/common/src/main/kotlin/com/p_nsk/replicated_integration/api/kubejs/RepInt.kt b/common/src/main/kotlin/com/p_nsk/replicated_integration/api/kubejs/RepInt.kt new file mode 100644 index 0000000..d4cefd8 --- /dev/null +++ b/common/src/main/kotlin/com/p_nsk/replicated_integration/api/kubejs/RepInt.kt @@ -0,0 +1,113 @@ +package com.p_nsk.replicated_integration.api.kubejs + +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.MatterNodes +import com.p_nsk.replicated_integration.api.selector.AllMatterRuleSelector +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.MatterRuleSelector +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.NodeView +import com.p_nsk.replicated_integration.api.selector.NotMatterRuleSelector +import com.p_nsk.replicated_integration.api.selector.PredicateMatterRuleSelector +import com.p_nsk.replicated_integration.api.selector.asRuleSelector +import java.util.function.Predicate + +object RepInt { + @JvmStatic fun node(type: String, id: String): MatterRuleSelector = + static(MatterSelectorKind.NODE, type, id) + + @JvmStatic fun nodeTag(type: String, tag: String): MatterRuleSelector = + static(MatterSelectorKind.TAG, type, tag) + + @JvmStatic fun nodeMod(type: String, modId: String): MatterRuleSelector = + MatterSelectorKey(MatterSelectorKind.MOD, type.toLite(), LiteResourceLocation.of("mod", modId)).asRuleSelector() + + @JvmStatic fun item(id: String): MatterRuleSelector = + node(MatterNodes.ITEM.toString(), id) + + @JvmStatic fun itemTag(tag: String): MatterRuleSelector = + nodeTag(MatterNodes.ITEM.toString(), tag) + + @JvmStatic fun itemMod(modId: String): MatterRuleSelector = + nodeMod(MatterNodes.ITEM.toString(), modId) + + @JvmStatic fun fluid(id: String): MatterRuleSelector = + node(MatterNodes.FLUID.toString(), id) + + @JvmStatic fun fluidTag(tag: String): MatterRuleSelector = + nodeTag(MatterNodes.FLUID.toString(), tag) + + @JvmStatic fun fluidMod(modId: String): MatterRuleSelector = + nodeMod(MatterNodes.FLUID.toString(), modId) + + @JvmStatic fun anyOf(vararg selectors: Any): MatterRuleSelector = + AnyMatterRuleSelector(selectors.map(::selector)) + + @JvmStatic fun allOf(vararg selectors: Any): MatterRuleSelector = + AllMatterRuleSelector(selectors.map(::selector)) + + @JvmStatic fun not(selector: Any): MatterRuleSelector = + NotMatterRuleSelector(selector(selector)) + + @JvmStatic fun whereNode(name: String, predicate: Predicate): MatterRuleSelector = + PredicateMatterRuleSelector(name) { predicate.test(it) } + + internal fun selector(value: Any): MatterRuleSelector = + when (value) { + is MatterRuleSelector -> value + is MatterSelectorKey -> value.asRuleSelector() + is String -> item(value) + else -> throw IllegalArgumentException("Unsupported RepInt selector: $value") + } + + private fun static(kind: MatterSelectorKind, type: String, id: String): MatterRuleSelector = + MatterSelectorKey(kind, type.toLite(), id.toLite()).asRuleSelector() + + private fun String.toLite(): LiteResourceLocation { + val parts = split(':', limit = 2) + require(parts.size == 2) { "Invalid resource location: $this" } + return LiteResourceLocation.of(parts[0], parts[1]) + } +} + +open class RepIntMatterEvent { + private val mutableRules = mutableListOf() + + val rules: List + get() = mutableRules.toList() + + fun deny(selector: Any) { + mutableRules += MatterRule(RepInt.selector(selector), ExplicitMatterValue.Deny(ExplicitMatterSource.KUBEJS)) + } + + fun set(selector: Any, matter: Map) { + mutableRules += MatterRule( + RepInt.selector(selector), + ExplicitMatterValue.Set(parseMatter(matter), ExplicitMatterSource.KUBEJS), + ) + } + + private fun parseMatter(value: Map): LiteMatterCompound { + val values = linkedMapOf() + for ((key, amount) in value) { + val id = key.toMatterId() + if (amount > 0.0) { + values[id] = amount + } + } + return LiteMatterCompound(values) + } + + private fun String.toMatterId(): LiteResourceLocation = + if (contains(':')) { + val parts = split(':', limit = 2) + LiteResourceLocation.of(parts[0], parts[1]) + } else { + LiteResourceLocation.of("replication", this) + } +} diff --git a/common/src/main/kotlin/com/p_nsk/replicated_integration/api/model/ExplicitMatterValue.kt b/common/src/main/kotlin/com/p_nsk/replicated_integration/api/model/ExplicitMatterValue.kt index e2054e6..c8f887d 100644 --- a/common/src/main/kotlin/com/p_nsk/replicated_integration/api/model/ExplicitMatterValue.kt +++ b/common/src/main/kotlin/com/p_nsk/replicated_integration/api/model/ExplicitMatterValue.kt @@ -7,7 +7,8 @@ enum class ExplicitMatterSource( NODE_VALUE(0, "replicated_integration node value"), DATAPACK(1, "replication matter_values"), CONFIG(2, "config override"), - RUNTIME(3, "runtime override"), + KUBEJS(3, "KubeJS override"), + RUNTIME(4, "runtime override"), } sealed interface ExplicitMatterValue { diff --git a/common/src/main/kotlin/com/p_nsk/replicated_integration/api/node/MatterNodeDef.kt b/common/src/main/kotlin/com/p_nsk/replicated_integration/api/node/MatterNodeDef.kt index 4687114..22239aa 100644 --- a/common/src/main/kotlin/com/p_nsk/replicated_integration/api/node/MatterNodeDef.kt +++ b/common/src/main/kotlin/com/p_nsk/replicated_integration/api/node/MatterNodeDef.kt @@ -7,6 +7,8 @@ data class MatterNodeDef( val id: LiteResourceLocation, val displayName: String, val formatter: (LiteResourceLocation) -> String = LiteResourceLocation::toString, + val enumerateNodes: () -> Iterable = { emptyList() }, + val rawValue: (LiteResourceLocation) -> Any? = { null }, val commands: MatterNodeCommands = MatterNodeCommands(), ) diff --git a/common/src/main/kotlin/com/p_nsk/replicated_integration/api/node/MatterNodeRegistry.kt b/common/src/main/kotlin/com/p_nsk/replicated_integration/api/node/MatterNodeRegistry.kt index 81e6f89..dd4ef58 100644 --- a/common/src/main/kotlin/com/p_nsk/replicated_integration/api/node/MatterNodeRegistry.kt +++ b/common/src/main/kotlin/com/p_nsk/replicated_integration/api/node/MatterNodeRegistry.kt @@ -6,6 +6,8 @@ import com.p_nsk.replicated_integration.api.model.LiteResourceLocation class MatterNodeRegistry { private val nodes = linkedMapOf>() private val commandsByLiteral = linkedMapOf() + private val emptyEnumerator: () -> Iterable = { emptyList() } + private val emptyRawValue: (LiteResourceLocation) -> Any? = { null } fun register(node: MatterNodeDef) { val previous = nodes[node.id] @@ -39,5 +41,9 @@ class MatterNodeRegistry { commandsByLiteral.values.toList() private fun MatterNodeDef.withoutCommands(): MatterNodeDef = - copy(commands = MatterNodeCommands()) + copy( + commands = MatterNodeCommands(), + enumerateNodes = emptyEnumerator, + rawValue = emptyRawValue, + ) } diff --git a/common/src/main/kotlin/com/p_nsk/replicated_integration/api/selector/MatterRule.kt b/common/src/main/kotlin/com/p_nsk/replicated_integration/api/selector/MatterRule.kt new file mode 100644 index 0000000..520effb --- /dev/null +++ b/common/src/main/kotlin/com/p_nsk/replicated_integration/api/selector/MatterRule.kt @@ -0,0 +1,105 @@ +package com.p_nsk.replicated_integration.api.selector + +import com.p_nsk.replicated_integration.api.model.ExplicitMatterValue +import com.p_nsk.replicated_integration.api.model.LiteResourceLocation +import com.p_nsk.replicated_integration.api.node.NodeKey +import java.util.function.Supplier + +data class MatterRule( + val selector: MatterRuleSelector, + val value: ExplicitMatterValue, +) { + val source = value.source +} + +class MatterRuleExpansion( + val expandTag: (type: LiteResourceLocation, id: LiteResourceLocation) -> Iterable, + val expandMod: (type: LiteResourceLocation, modId: String) -> Iterable, + val allNodes: () -> Iterable, + val rawValue: (NodeKey) -> Any?, +) + +sealed interface MatterRuleSelector { + val staticKey: MatterSelectorKey? + + fun expand(expansion: MatterRuleExpansion): Set +} + +data class StaticMatterRuleSelector( + val key: MatterSelectorKey, +) : MatterRuleSelector { + override val staticKey: MatterSelectorKey = key + + override fun expand(expansion: MatterRuleExpansion): Set = + when (key.kind) { + MatterSelectorKind.NODE -> linkedSetOf(NodeKey(key.type, key.id)) + MatterSelectorKind.TAG -> expansion.expandTag(key.type, key.id).toCollection(linkedSetOf()) + MatterSelectorKind.MOD -> expansion.expandMod(key.type, key.id.path).toCollection(linkedSetOf()) + } +} + +data class AnyMatterRuleSelector( + val children: List, +) : MatterRuleSelector { + override val staticKey: MatterSelectorKey? = null + + override fun expand(expansion: MatterRuleExpansion): Set = + children.flatMapTo(linkedSetOf()) { it.expand(expansion) } +} + +data class AllMatterRuleSelector( + val children: List, +) : MatterRuleSelector { + override val staticKey: MatterSelectorKey? = null + + override fun expand(expansion: MatterRuleExpansion): Set { + if (children.isEmpty()) { + return emptySet() + } + val result = children.first().expand(expansion).toMutableSet() + for (child in children.drop(1)) { + result.retainAll(child.expand(expansion)) + } + return result + } +} + +data class NotMatterRuleSelector( + val child: MatterRuleSelector, +) : MatterRuleSelector { + override val staticKey: MatterSelectorKey? = null + + override fun expand(expansion: MatterRuleExpansion): Set { + val excluded = child.expand(expansion) + return expansion.allNodes().filterTo(linkedSetOf()) { it !in excluded } + } +} + +class PredicateMatterRuleSelector( + val name: String, + private val predicate: (NodeView) -> Boolean, +) : MatterRuleSelector { + override val staticKey: MatterSelectorKey? = null + + override fun expand(expansion: MatterRuleExpansion): Set = + expansion.allNodes().filterTo(linkedSetOf()) { node -> + predicate(NodeView(node, Supplier { expansion.rawValue(node) })) + } + + override fun toString(): String = "whereNode($name)" +} + +class NodeView( + val key: NodeKey, + private val raw: Supplier = Supplier { null }, +) { + val type: String = key.type.toString() + val id: String = key.id.toString() + val modId: String = key.id.namespace + + @Suppress("UNCHECKED_CAST") + fun getRaw(): T? = raw.get() as? T +} + +fun MatterSelectorKey.asRuleSelector(): MatterRuleSelector = + StaticMatterRuleSelector(this) diff --git a/common/src/main/kotlin/com/p_nsk/replicated_integration/api/selector/MatterSelectorFormatter.kt b/common/src/main/kotlin/com/p_nsk/replicated_integration/api/selector/MatterSelectorFormatter.kt index cc089ae..b1f857d 100644 --- a/common/src/main/kotlin/com/p_nsk/replicated_integration/api/selector/MatterSelectorFormatter.kt +++ b/common/src/main/kotlin/com/p_nsk/replicated_integration/api/selector/MatterSelectorFormatter.kt @@ -12,4 +12,7 @@ fun MatterNodeRegistry<*>.formatSelector(selector: MatterSelectorKey): String = MatterSelectorKind.TAG -> "#${selector.id} [${formatType(selector.type)} tag]" + + MatterSelectorKind.MOD -> + "${selector.id.path} [${formatType(selector.type)} mod]" } diff --git a/common/src/main/kotlin/com/p_nsk/replicated_integration/api/selector/MatterSelectorKind.kt b/common/src/main/kotlin/com/p_nsk/replicated_integration/api/selector/MatterSelectorKind.kt index 7b658ce..abc325c 100644 --- a/common/src/main/kotlin/com/p_nsk/replicated_integration/api/selector/MatterSelectorKind.kt +++ b/common/src/main/kotlin/com/p_nsk/replicated_integration/api/selector/MatterSelectorKind.kt @@ -3,4 +3,5 @@ package com.p_nsk.replicated_integration.api.selector enum class MatterSelectorKind { NODE, TAG, + MOD, } 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 2310135..be56c14 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 @@ -8,51 +8,33 @@ object MatterSelectorMaterializer { fun materialize( selectors: Map, expandTag: (type: LiteResourceLocation, id: LiteResourceLocation) -> Iterable, + expandMod: (type: LiteResourceLocation, modId: String) -> Iterable = { _, _ -> emptyList() }, + allNodes: () -> Iterable = { emptyList() }, + rawValue: (NodeKey) -> Any? = { null }, + ): Map = + materialize( + selectors.entries.map { (selector, value) -> MatterRule(selector.asRuleSelector(), value) }, + expandTag, + expandMod, + allNodes, + rawValue, + ) + + fun materialize( + rules: List, + expandTag: (type: LiteResourceLocation, id: LiteResourceLocation) -> Iterable, + expandMod: (type: LiteResourceLocation, modId: String) -> Iterable = { _, _ -> emptyList() }, + allNodes: () -> Iterable = { emptyList() }, + rawValue: (NodeKey) -> Any? = { null }, ): Map { - val values = linkedMapOf() - for ((selector, explicitValue) in selectors.entries.sortedBy { it.key }) { - val targets = - when (selector.kind) { - MatterSelectorKind.NODE -> listOf(NodeKey(selector.type, selector.id)) - MatterSelectorKind.TAG -> expandTag(selector.type, selector.id).toList() - } - val specificity = - when (selector.kind) { - MatterSelectorKind.NODE -> 1 - MatterSelectorKind.TAG -> 0 - } + val values = linkedMapOf() + val expansion = MatterRuleExpansion(expandTag, expandMod, allNodes, rawValue) + for (rule in rules.sortedWith(compareBy { it.source.priority })) { + val targets = rule.selector.expand(expansion) for (target in targets) { - val current = values[target] - if (current == null || shouldReplace(current, explicitValue, specificity)) { - values[target] = AppliedValue(explicitValue, specificity) - } + values[target] = rule.value } } - return values.mapValues { it.value.explicitValue } + return values } - - private fun shouldReplace( - current: AppliedValue, - candidate: ExplicitMatterValue, - specificity: Int, - ): Boolean { - if (candidate.source.priority != current.explicitValue.source.priority) { - return candidate.source.priority > current.explicitValue.source.priority - } - if (specificity != current.specificity) { - return specificity > current.specificity - } - return when { - current.explicitValue is ExplicitMatterValue.Deny -> false - candidate is ExplicitMatterValue.Deny -> true - current.explicitValue is ExplicitMatterValue.Set && candidate is ExplicitMatterValue.Set -> - candidate.compound.isBetterThan(current.explicitValue.compound) - else -> false - } - } - - private data class AppliedValue( - val explicitValue: ExplicitMatterValue, - val specificity: Int, - ) } 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 30d2511..0fd7fbc 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 @@ -19,24 +19,43 @@ object MatterSelectorStorageCodec { path: Path, source: ExplicitMatterSource, ): Map { + val values = linkedMapOf() + for (rule in loadRules(path, source)) { + val key = rule.selector.staticKey ?: continue + values[key] = rule.value + } + return values + } + + fun loadRules( + path: Path, + source: ExplicitMatterSource, + ): List { if (!Files.exists(path)) { - return emptyMap() + return emptyList() } val root = JsonParser.parseString(Files.readString(path)).asJsonObject val entries = root.getAsJsonArray("entries") ?: JsonArray() - val parsed = linkedMapOf() + val parsed = mutableListOf() for (element in entries) { val json = element.asJsonObject + val kind = + when (json["selector"]?.asString ?: "node") { + "node" -> MatterSelectorKind.NODE + "tag" -> MatterSelectorKind.TAG + "mod" -> MatterSelectorKind.MOD + else -> throw IllegalArgumentException("Unknown selector kind: ${json["selector"]}") + } val selector = MatterSelectorKey( - kind = - when (json["selector"]?.asString ?: "node") { - "node" -> MatterSelectorKind.NODE - "tag" -> MatterSelectorKind.TAG - else -> throw IllegalArgumentException("Unknown selector kind: ${json["selector"]}") - }, + kind = kind, type = json["type"].asString.toLite(), - id = json["id"].asString.toLite(), + id = + if (kind == MatterSelectorKind.MOD) { + LiteResourceLocation.of("mod", json["id"].asString) + } else { + json["id"].asString.toLite() + }, ) val value = if (json["deny"]?.asBoolean == true) { @@ -50,7 +69,7 @@ object MatterSelectorStorageCodec { } ExplicitMatterValue.Set(LiteMatterCompound(compound), source) } - parsed[selector] = value + parsed += MatterRule(selector.asRuleSelector(), value) } return parsed } @@ -58,15 +77,30 @@ object MatterSelectorStorageCodec { fun save( path: Path, values: Map, + ) = saveRules( + path, + values.entries.sortedBy { it.key }.map { (selector, value) -> + MatterRule(selector.asRuleSelector(), value) + }, + ) + + fun saveRules( + path: Path, + rules: List, ) { Files.createDirectories(path.parent) val root = JsonObject() val entries = JsonArray() - for ((selector, value) in values.entries.sortedBy { it.key }) { + for (rule in rules) { + val selector = rule.selector.staticKey ?: continue + val value = rule.value val json = JsonObject() json.addProperty("selector", selector.kind.name.lowercase()) json.addProperty("type", selector.type.toString()) - json.addProperty("id", selector.id.toString()) + json.addProperty( + "id", + if (selector.kind == MatterSelectorKind.MOD) selector.id.path else selector.id.toString(), + ) when (value) { is ExplicitMatterValue.Deny -> json.addProperty("deny", true) is ExplicitMatterValue.Set -> { 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 91b420d..bbef69d 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 @@ -5,47 +5,46 @@ import com.p_nsk.replicated_integration.api.model.ExplicitMatterValue import com.p_nsk.replicated_integration.api.model.LiteMatterCompound class MutableMatterSelectors { - private val values = linkedMapOf() + private val rules = mutableListOf() fun put(selector: MatterSelectorKey, compound: LiteMatterCompound, source: ExplicitMatterSource) { - val current = values[selector] - val candidate = ExplicitMatterValue.Set(compound, source) - if (current == null || shouldReplace(current, candidate)) { - values[selector] = candidate - } + put(selector.asRuleSelector(), compound, source) + } + + fun put(selector: MatterRuleSelector, compound: LiteMatterCompound, source: ExplicitMatterSource) { + rules += MatterRule(selector, ExplicitMatterValue.Set(compound, source)) } fun deny(selector: MatterSelectorKey, source: ExplicitMatterSource) { - val current = values[selector] - val candidate = ExplicitMatterValue.Deny(source) - if (current == null || shouldReplace(current, candidate)) { - values[selector] = candidate - } + deny(selector.asRuleSelector(), source) + } + + fun deny(selector: MatterRuleSelector, source: ExplicitMatterSource) { + rules += MatterRule(selector, ExplicitMatterValue.Deny(source)) } fun putAll(other: Map) { other.forEach { (selector, value) -> - when (value) { - is ExplicitMatterValue.Deny -> deny(selector, value.source) - is ExplicitMatterValue.Set -> put(selector, value.compound, value.source) - } + putRule(MatterRule(selector.asRuleSelector(), value)) } } - fun snapshot(): Map = values.toMap() + fun putRule(rule: MatterRule) { + rules += rule + } - private fun shouldReplace( - current: ExplicitMatterValue, - candidate: ExplicitMatterValue, - ): Boolean { - if (candidate.source.priority != current.source.priority) { - return candidate.source.priority > current.source.priority - } - return when { - current is ExplicitMatterValue.Deny -> false - candidate is ExplicitMatterValue.Deny -> true - current is ExplicitMatterValue.Set && candidate is ExplicitMatterValue.Set -> candidate.compound.isBetterThan(current.compound) - else -> false + fun putRules(other: Iterable) { + rules += other + } + + fun snapshotRules(): List = rules.toList() + + fun snapshot(): Map { + val values = linkedMapOf() + for (rule in rules.sortedWith(compareBy { it.source.priority })) { + val key = rule.selector.staticKey ?: continue + values[key] = rule.value } + return values } } diff --git a/common/src/test/kotlin/MatterAmountFormatterTest.kt b/common/src/test/kotlin/MatterAmountFormatterTest.kt new file mode 100644 index 0000000..c3ab066 --- /dev/null +++ b/common/src/test/kotlin/MatterAmountFormatterTest.kt @@ -0,0 +1,31 @@ +package com.p_nsk.replicated_integration.api + +import com.p_nsk.replicated_integration.api.client.MatterAmountFormatter +import com.p_nsk.replicated_integration.api.client.MatterAmountNotation +import kotlin.test.Test +import kotlin.test.assertEquals + +class MatterAmountFormatterTest { + @Test + fun compactUsesShortScaleSuffixes() { + assertEquals("300K", MatterAmountFormatter.compact(300_000.0, MatterAmountNotation.SHORT_SCALE)) + assertEquals("20M", MatterAmountFormatter.compact(20_000_000.0, MatterAmountNotation.SHORT_SCALE)) + assertEquals("10B", MatterAmountFormatter.compact(10_000_000_000.0, MatterAmountNotation.SHORT_SCALE)) + } + + @Test + fun compactUsesSiSuffixes() { + assertEquals("10G", MatterAmountFormatter.compact(10_000_000_000.0, MatterAmountNotation.SI)) + } + + @Test + fun exactKeepsGroupedValue() { + assertEquals("1,234,567.89", MatterAmountFormatter.exact(1_234_567.89)) + } + + @Test + fun compactFallsBackToScientificWhenSuffixesRunOut() { + assertEquals("1e21", MatterAmountFormatter.compact(1.0e21, MatterAmountNotation.SHORT_SCALE)) + assertEquals("1e21", MatterAmountFormatter.compact(1.0e21, MatterAmountNotation.SI)) + } +} diff --git a/common/src/test/kotlin/MatterIndexDisplayScaleTest.kt b/common/src/test/kotlin/MatterIndexDisplayScaleTest.kt new file mode 100644 index 0000000..532c3c1 --- /dev/null +++ b/common/src/test/kotlin/MatterIndexDisplayScaleTest.kt @@ -0,0 +1,25 @@ +package com.p_nsk.replicated_integration.api + +import com.p_nsk.replicated_integration.api.client.MatterIndexDisplayScale +import kotlin.test.Test +import kotlin.test.assertEquals + +class MatterIndexDisplayScaleTest { + @Test + fun adjustsInTenPercentSteps() { + assertEquals(90, MatterIndexDisplayScale.adjust(100, -1)) + assertEquals(110, MatterIndexDisplayScale.adjust(100, 1)) + } + + @Test + fun clampsToSupportedRange() { + assertEquals(70, MatterIndexDisplayScale.adjust(70, -1)) + assertEquals(130, MatterIndexDisplayScale.adjust(130, 1)) + } + + @Test + fun scalesPixelConstantsLocally() { + assertEquals(37, MatterIndexDisplayScale.scalePixels(42, 90)) + assertEquals(46, MatterIndexDisplayScale.scalePixels(42, 110)) + } +} diff --git a/common/src/test/kotlin/SimpleConversionSolverTest.kt b/common/src/test/kotlin/SimpleConversionSolverTest.kt index 085dd15..9673260 100644 --- a/common/src/test/kotlin/SimpleConversionSolverTest.kt +++ b/common/src/test/kotlin/SimpleConversionSolverTest.kt @@ -12,6 +12,7 @@ import com.p_nsk.replicated_integration.api.node.MatterNodes 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.asRuleSelector import kotlin.test.Test import kotlin.test.assertEquals @@ -189,4 +190,38 @@ class SimpleConversionSolverTest { assertEquals(20.0, (materialized[oakLog] as ExplicitMatterValue.Set).compound.values[earth]) } + + @Test + fun selectorMaterializerUsesLastRuleWithinSameSourcePriority() { + val stick = MatterNodes.item(LiteResourceLocation.of("minecraft", "stick")) + val earth = LiteResourceLocation.of("replication", "earth") + + val materialized = + MatterSelectorMaterializer.materialize( + rules = + listOf( + com.p_nsk.replicated_integration.api.selector.MatterRule( + MatterSelectorKey( + MatterSelectorKind.MOD, + MatterNodes.ITEM, + LiteResourceLocation.of("mod", "minecraft"), + ).asRuleSelector(), + ExplicitMatterValue.Deny(ExplicitMatterSource.CONFIG), + ), + com.p_nsk.replicated_integration.api.selector.MatterRule( + MatterSelectorKey( + MatterSelectorKind.NODE, + MatterNodes.ITEM, + LiteResourceLocation.of("minecraft", "stick"), + ).asRuleSelector(), + ExplicitMatterValue.Set(LiteMatterCompound.single(earth, 4.0), ExplicitMatterSource.CONFIG), + ), + ), + expandTag = { _, _ -> emptyList() }, + expandMod = { _, _ -> listOf(stick) }, + allNodes = { listOf(stick) }, + ) + + assertEquals(4.0, (materialized[stick] as ExplicitMatterValue.Set).compound.values[earth]) + } } diff --git a/docs/pr4-unresolved-review-items.md b/docs/pr4-unresolved-review-items.md new file mode 100644 index 0000000..7f9e8fa --- /dev/null +++ b/docs/pr4-unresolved-review-items.md @@ -0,0 +1,12 @@ +# PR #4 unresolved review items (parked) + +The following review threads are intentionally left unresolved for now. + +- `common/src/main/kotlin/com/p_nsk/replicated_integration/api/selector/MatterSelectorStorageCodec.kt` (`saveRules()` static selector handling) +- `common/src/main/kotlin/com/p_nsk/replicated_integration/api/selector/MatterSelectorMaterializer.kt` (explicit tie-break for equal priority rules) +- `common/src/main/kotlin/com/p_nsk/replicated_integration/api/selector/MutableMatterSelectors.kt` (explicit tie-break for equal priority rules in `snapshot()`) +- `1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/client/MatterIndexScreen.kt` (`searchBox.tick()` in NeoForge `tick()`) +- `1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/adapter/vanilla/BuiltinNodeResolver.kt` (cache/index for mod-scoped node lookups) +- `1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/adapter/vanilla/BuiltinNodeResolver.kt` (cache/index for mod-scoped node lookups) +- `1.21.1-neo/src/main/kotlin/com/p_nsk/replicated_integration/client/MatterIndexScreen.kt` (avoid rebuilding record/index maps in `visibleEntries()`) +- `1.20.1-forge/src/main/kotlin/com/p_nsk/replicated_integration/client/MatterIndexScreen.kt` (avoid rebuilding record/index maps in `visibleEntries()` / `equivalentEntries()`) diff --git a/gradle.properties b/gradle.properties index 2c18508..37a87e4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,7 +9,7 @@ org.gradle.configuration-cache=true modId=replicated_integration modName=Replicated Integration modLicense=MIT -modVersion=0.1.1 +modVersion=0.2.0 modGroupId=com.p_nsk modAuthors=p_nsk modDescription=Optional compatibility addon for Replication across Forge and NeoForge. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c0ceb63..077d811 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -51,6 +51,16 @@ curios-mc1211 = "6528893" configured-mc1201 = "5180900" configured-mc1211 = "7276577" +kubejs-mc1201 = "2001.6.4-build.120" +kubejs-mc1211 = "2101.7.2-build.365" +architectury-mc1201 = "9.2.14" +architectury-mc1211 = "13.0.8" +rhino-mc1201 = "2001.2.3-build.10" +rhino-mc1211 = "2101.2.7-build.82" +# probejsLegacyはバグがあって使えないので一旦封印 +probejsLegacy-mc1201 = "N/A" # 8087580 # 7615033 +probejs-mc1211 = "8122173" +probejs-mc1201 = "5227399" [libraries] slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j-api" } @@ -101,5 +111,16 @@ curios-mc1211 = { module = "curse.maven:curios-309927", version.ref = "curios-mc configured-mc1201 = { module = "curse.maven:configured-457570", version.ref = "configured-mc1201" } configured-mc1211 = { module = "curse.maven:configured-457570", version.ref = "configured-mc1211" } +kubejs-mc1201 = { module = "dev.latvian.mods:kubejs-forge", version.ref = "kubejs-mc1201" } +kubejs-mc1211 = { module = "dev.latvian.mods:kubejs-neoforge", version.ref = "kubejs-mc1211" } +architectury-mc1201 = { module = "dev.architectury:architectury-forge", version.ref = "architectury-mc1201" } +architectury-mc1211 = { module = "dev.architectury:architectury-neoforge", version.ref = "architectury-mc1211" } +rhino-mc1201 = { module = "dev.latvian.mods:rhino-forge", version.ref = "rhino-mc1201" } +rhino-mc1211 = { module = "dev.latvian.mods:rhino", version.ref = "rhino-mc1211" } + +probejsLegacy-mc1201 = { module = "curse.maven:probejs-legacy-956446", version.ref = "probejsLegacy-mc1201" } +probejs-mc1201 = { module = "curse.maven:probejs-585406", version.ref = "probejs-mc1201" } +probejs-mc1211 = { module = "curse.maven:probejs-585406", version.ref = "probejs-mc1211" } + [plugins] kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }