diff --git a/.github/workflows/modern-build.yml b/.github/workflows/modern-build.yml new file mode 100644 index 0000000..1c096dc --- /dev/null +++ b/.github/workflows/modern-build.yml @@ -0,0 +1,57 @@ +name: Modern Builds + +on: + push: + pull_request: + workflow_dispatch: + +jobs: + forge-1-20-1: + name: Forge 1.20.1 + runs-on: ubuntu-latest + defaults: + run: + working-directory: versions/forge-1.20.1 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Build + run: | + sed -i 's/\r$//' gradlew + chmod +x gradlew + ./gradlew test build --no-daemon + + neoforge-1-21-1: + name: NeoForge 1.21.1 + runs-on: ubuntu-latest + defaults: + run: + working-directory: versions/neoforge-1.21.1 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '21' + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Build + run: | + sed -i 's/\r$//' gradlew + chmod +x gradlew + ./gradlew test build --no-daemon diff --git a/README.md b/README.md index 22a0ec1..4699ef3 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,17 @@ AE2 Smart Pattern System is a smart pattern extension for AE2 and AE2UEL. -It adds wildcard recipe expansion, item or fluid or gas marker support, recipe search, filter lists, and mod-based exclusion controls across multiple Minecraft version tracks. +It adds wildcard recipe expansion, item or fluid or gas marker support, recipe search, filter lists, and mod-based filtering across multiple Minecraft version tracks. + +## Modern Pattern Filters + +The Forge 1.20.1 and NeoForge 1.21.1 tracks support independent mod filters for +each pattern input and output: + +- Each input and output can define its own mod whitelist and blacklist. +- Legacy `Excluded` data is migrated to the blacklist. +- An explicitly empty whitelist rejects all candidates; it does not mean + "allow everything". ## Version Tracks diff --git a/versions/forge-1.20.1/README.md b/versions/forge-1.20.1/README.md index db51d84..b68cdcd 100644 --- a/versions/forge-1.20.1/README.md +++ b/versions/forge-1.20.1/README.md @@ -1,77 +1,60 @@ -# Forge 1.20.1 路线 / Forge 1.20.1 Track +# AE2 Smart Pattern System - Forge 1.20.1 -当前状态:可编译、可打开基础编辑器 GUI、可保存基础样板数据,且已接入 AE2 官方 API 解码器。 -Current status: compilable, basic editor GUI opens, pattern data persists, and AE2 official API decoder is integrated. +This track targets Minecraft 1.20.1 on Forge and keeps the runtime namespace +`sampleintegration` for compatibility with the existing assets and metadata. -## 已完成 / Done +## Current Status -- [x] 模组引导与元数据。 / Mod bootstrap and metadata. -- [x] 物品与方块注册。 / Item and block registration. -- [x] 方块实体(18 槽)迁移。 / Block entity migration (18 slots). -- [x] 菜单与界面迁移(基础版)。 / Menu and screen migration (basic). -- [x] 样板 NBT 回写(打开读入、关闭写回)。 / Pattern NBT round-trip (load on open, save on close). -- [x] 基础 Tooltip 与本地化。 / Basic tooltip and localization. -- [x] 基础资源(model/blockstate/loot/recipes)。 / Basic assets (model/blockstate/loot/recipes). -- [x] AE2 软依赖接入(15.4.10)与解码器注册。 / AE2 soft integration (15.4.10) and decoder registration. -- [ ] AE2 自动下单执行链深度迁移。 / Deep migration of AE2 crafting execution chain. -- [ ] 黑白名单模式与流体/气体标记迁移。 / Filter mode and fluid/gas marker migration. +- The mod bootstrap, item and block registration, pattern editor menu, basic + editor screen, pattern NBT persistence, tooltip, and localization are + implemented. +- The Forge 1.20.1 build includes the shared core model and its JUnit 5 tests. +- AE2 official API decoding is integrated as a soft dependency. +- Fluid and gas marker data, item markers, wildcard expansion, and recipe-level + filter entries are supported. +- AE2 crafting execution-chain migration is still outside this track. -## 构建命令 / Build Commands +## Mod Filters -推荐使用本地脚本(已固定 Java 17 路径): -Use the local script (Java 17 path is pinned): +Each encoded pattern stores independent input and output mod filter rules. +Every side has its own mode: -```bat -cd migration\forge-1.20.1 -build-dev.bat build --console=plain -``` +- `WHITELIST`: only listed mod namespaces are allowed. +- `BLACKLIST`: listed mod namespaces are blocked. -启动客户端: -Run client: +The mod filter screen provides separate input and output mode buttons in the +sidebar. Left-click toggles the input list and right-click toggles the output +list. Row markers, tooltips, and blocked counts reflect the effective rule, +including whitelist behavior. -```bat -cd migration\forge-1.20.1 -build-dev.bat runClient --console=plain -``` +Canonical pattern keys are: -Safe dev run (temporarily disables incompatible AE2/GuideME/JEI jars in `run/mods` and restores them after run): +- `TechStartInputModFilterMode` +- `TechStartOutputModFilterMode` +- `TechStartInputModFilterIds` +- `TechStartOutputModFilterIds` -```bat -cd migration\forge-1.20.1 -run-dev-safe.bat runClient --no-daemon -``` +Older `ExcludedInputModIds` and `ExcludedOutputModIds` lists are read as +blacklists when canonical IDs are absent. Saving always writes canonical mode +data, removes legacy keys, and limits each side to 512 normalized IDs of at +most 64 characters. -```bat -cd migration\forge-1.20.1 -run-dev-safe.bat runServer --no-daemon -``` +Recipe-level filter entries remain a second, independent filter layer after +mod namespace filtering. -鏃ュ父寮€鍙戞祴璇曢粯璁や笉鍔犺浇 AE2 杩愯鏃讹紙閬垮厤宸叉柟 AE2 + Forge dev Mixin 鍐茬獊锛夈€? -Default dev run does not load AE2 runtime jars to avoid AE2 + Forge-dev mixin conflicts. +## Build -濡傞渶鍚敤 AE2 杩愯鏃讹紝鍙樉寮忎紶鍏ュ弬鏁帮細 -To enable AE2 runtime explicitly: +Run from this directory: ```bat -cd migration\forge-1.20.1 -build-dev.bat -PenableAe2Runtime=true runClient --console=plain +gradlew.bat test build --no-daemon --console=plain ``` -## Progress Note - -- Added JEI ghost ingredient support for `PatternEditorScreen` (drag ingredient into pattern slots). -- Added server sync packet for marker placement to keep client/server slots consistent. -- Kept `sampleintegration` as the runtime-compatible `modid` and resource namespace while migrating the codebase and branding to AE2 Smart Pattern System. -- Added runtime filter application in the AE2SPS pattern decode path (whitelist/blacklist now affects decoded inputs/outputs). -- Added item-marker decode cleanup so legacy `TechStartItemMarker` tags are stripped before creating AE2 item keys. -- Added offhand/main inventory sync reinforcement after saving pattern NBT in editor menu. -- Added `run-dev-safe.bat` to automate temporary runtime-jar disable/restore for dev launch stability. +For a development client: -## AE2 对齐说明 / AE2 Alignment Notes +```bat +build-dev.bat runClient --console=plain +``` -- 依赖源使用 AE2 官方文档给出的坐标体系与仓库。 - Dependency coordinates/repository follow AE2 official documentation. -- 当前接入版本:`appeng:appliedenergistics2-forge:15.4.10`(软依赖)。 - Current integrated version: `appeng:appliedenergistics2-forge:15.4.10` (soft dependency). -- 已实现 `IPatternDetailsDecoder` + `IPatternDetails`,让 Pattern Provider 可识别 AE2SPS 样板。 - Implemented `IPatternDetailsDecoder` + `IPatternDetails` so Pattern Provider can decode AE2SPS patterns. +The optional AE2 runtime is controlled by the existing +`enableAe2Runtime` Gradle property. diff --git a/versions/forge-1.20.1/build.gradle b/versions/forge-1.20.1/build.gradle index 1819397..4537d0a 100644 --- a/versions/forge-1.20.1/build.gradle +++ b/versions/forge-1.20.1/build.gradle @@ -76,27 +76,39 @@ minecraft { sourceSets.main.resources { srcDir 'src/generated/resources' } sourceSets.main.java { srcDir '../shared/common-core/src/main/java' } +sourceSets.test.java { srcDir '../shared/common-core/src/test/java' } repositories { mavenCentral() maven { url = 'https://maven.minecraftforge.net' } + maven { url = 'https://maven.blamejared.com' } maven { name = 'Modmaven' url = 'https://modmaven.dev/' content { includeGroup 'appeng' + includeGroup 'mekanism' + } + } + maven { + name = 'Modrinth' + url = 'https://api.modrinth.com/maven' + content { + includeGroup 'maven.modrinth' } } } dependencies { minecraft "net.minecraftforge:forge:${minecraft_version}-${forge_version}" + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' // AE2 1.20.1 official Forge artifact (soft integration). compileOnly fg.deobf("appeng:appliedenergistics2-forge:${ae2_version}:api") compileOnly fg.deobf("appeng:appliedenergistics2-forge:${ae2_version}") - compileOnly files('D:/桌面/我的世界/GTL/versions/1.20.1-Forge_47.4.10/mods/[通用机械] Mekanism-1.20.1-10.4.16.80.jar') - compileOnly files('D:/桌面/我的世界/GTL/versions/1.20.1-Forge_47.4.10/mods/[应用能源:通用机械附属] Applied-Mekanistics-1.4.3.jar') + compileOnly fg.deobf("mekanism:Mekanism:${minecraft_version}-${mekanism_version}:api") + compileOnly fg.deobf("maven.modrinth:applied-mekanistics:${applied_mekanistics_version}") compileOnly "org.spongepowered:mixin:0.8.5" annotationProcessor "org.spongepowered:mixin:0.8.5:processor" // Keep AE2 runtime optional in dev runs; enable with -PenableAe2Runtime=true. @@ -111,12 +123,10 @@ dependencies { } } - // Optional JEI integration: compile against a local JEI jar when available. - def jeiLocalJar = file('run/mods/jei-1.20.1-forge-15.20.0.129.jar') - def jeiDisabledJar = file('run/mods/_disabled_by_run_dev_safe/jei-1.20.1-forge-15.20.0.129.jar') - def jeiCompileJar = jeiLocalJar.exists() ? jeiLocalJar : (jeiDisabledJar.exists() ? jeiDisabledJar : null) - if (jeiCompileJar != null) { - compileOnly files(jeiCompileJar) + // Optional JEI integration uses the public developer Maven artifact. + compileOnly fg.deobf("mezz.jei:jei-1.20.1-forge:${jei_version}") + if ((findProperty('enableJeiRuntime') ?: 'false').toString().toBoolean()) { + runtimeOnly fg.deobf("mezz.jei:jei-1.20.1-forge:${jei_version}") } } @@ -137,6 +147,10 @@ tasks.named('processResources', ProcessResources).configure { } } +tasks.withType(Test).configureEach { + useJUnitPlatform() +} + tasks.named('jar', Jar).configure { manifest { attributes(commonManifestAttributes()) diff --git a/versions/forge-1.20.1/gradle.properties b/versions/forge-1.20.1/gradle.properties index d8eb2c0..90cf73a 100644 --- a/versions/forge-1.20.1/gradle.properties +++ b/versions/forge-1.20.1/gradle.properties @@ -1,7 +1,5 @@ org.gradle.jvmargs=-Xmx3G -Dfile.encoding=UTF-8 org.gradle.daemon=false -org.gradle.java.home=C:/Users/PC/AppData/Roaming/.hmcl/java/windows-x86_64/mojang-java-runtime-beta -org.gradle.java.installations.paths=C:/Users/PC/AppData/Roaming/.hmcl/java/windows-x86_64/mojang-java-runtime-beta mod_id=sampleintegration mod_name=AE2SPS Smart Pattern System @@ -13,4 +11,6 @@ mod_description=Smart pattern extension for AE2SPS on AE2. Supports wildcard rec minecraft_version=1.20.1 forge_version=47.4.10 ae2_version=15.4.10 - +mekanism_version=10.4.16.80 +applied_mekanistics_version=1.4.3 +jei_version=15.20.0.129 diff --git a/versions/forge-1.20.1/src/main/java/com/ae2smartpatternsystem/client/ModFilterScreen.java b/versions/forge-1.20.1/src/main/java/com/ae2smartpatternsystem/client/ModFilterScreen.java index 3028f12..a082ac5 100644 --- a/versions/forge-1.20.1/src/main/java/com/ae2smartpatternsystem/client/ModFilterScreen.java +++ b/versions/forge-1.20.1/src/main/java/com/ae2smartpatternsystem/client/ModFilterScreen.java @@ -1,6 +1,8 @@ package com.ae2smartpatternsystem.client; import com.ae2smartpatternsystem.TechStartForge; +import com.ae2smartpatternsystem.core.model.FilterMode; +import com.ae2smartpatternsystem.core.model.ModFilterRule; import com.ae2smartpatternsystem.menu.PatternEditorMenu; import com.ae2smartpatternsystem.network.SetPatternModFiltersPacket; import com.ae2smartpatternsystem.network.TechStartNetwork; @@ -46,6 +48,8 @@ public class ModFilterScreen extends Screen { private static final int MODE_BUTTON_Y = 48; private static final int SEARCH_BUTTON_Y = 72; private static final int MOD_FILTER_BUTTON_Y = 96; + private static final int INPUT_MODE_BUTTON_Y = 120; + private static final int OUTPUT_MODE_BUTTON_Y = 144; private static final int GUI_WIDTH = SIDE_BUTTON_X + SIDE_BUTTON_WIDTH; private static final int GUI_HEIGHT = PANEL_HEIGHT; private static final int LIST_X = 16; @@ -62,10 +66,14 @@ public class ModFilterScreen extends Screen { private final PatternEditorMenu menu; private final List allEntries = new ArrayList<>(); private final List filteredEntries = new ArrayList<>(); - private final LinkedHashSet excludedInputMods = new LinkedHashSet<>(); - private final LinkedHashSet excludedOutputMods = new LinkedHashSet<>(); + private final LinkedHashSet inputModIds = new LinkedHashSet<>(); + private final LinkedHashSet outputModIds = new LinkedHashSet<>(); + private FilterMode inputMode = FilterMode.BLACKLIST; + private FilterMode outputMode = FilterMode.BLACKLIST; private EditBox searchField; + private InvisibleButton inputModeButton; + private InvisibleButton outputModeButton; private int leftPos; private int topPos; private int listScroll = 0; @@ -83,10 +91,12 @@ protected void init() { this.leftPos = (this.width - GUI_WIDTH) / 2; this.topPos = (this.height - GUI_HEIGHT) / 2; - this.excludedInputMods.clear(); - this.excludedInputMods.addAll(this.menu.getExcludedInputModIdsSnapshot()); - this.excludedOutputMods.clear(); - this.excludedOutputMods.addAll(this.menu.getExcludedOutputModIdsSnapshot()); + this.inputModIds.clear(); + this.inputModIds.addAll(this.menu.getInputModFilterIdsSnapshot()); + this.outputModIds.clear(); + this.outputModIds.addAll(this.menu.getOutputModFilterIdsSnapshot()); + this.inputMode = this.menu.getInputModFilterMode(); + this.outputMode = this.menu.getOutputModFilterMode(); this.searchField = new EditBox(this.font, this.leftPos + 38, this.topPos + 25, 136, 12, Component.empty()); this.searchField.setCanLoseFocus(false); @@ -104,12 +114,14 @@ protected void init() { addSideButton(MODE_BUTTON_Y, MODE_BUTTON_U, MODE_BUTTON_V, "mode", Component.translatable("gui.ae2sps.toggle_filter_mode"), ignored -> toggleFilterMode()); addSideButton(SEARCH_BUTTON_Y, SEARCH_BUTTON_U, SEARCH_BUTTON_V, "search", Component.translatable("gui.ae2sps.open_search"), ignored -> openSearch()); addSideButton(MOD_FILTER_BUTTON_Y, MOD_FILTER_BUTTON_U, MOD_FILTER_BUTTON_V, "mod", Component.translatable("gui.ae2sps.open_mod_filter"), ignored -> focusSearch()); + this.inputModeButton = addSideButton(INPUT_MODE_BUTTON_Y, MODE_BUTTON_U, MODE_BUTTON_V, "input_mode", modeHint(true), ignored -> toggleModFilterMode(true)); + this.outputModeButton = addSideButton(OUTPUT_MODE_BUTTON_Y, MODE_BUTTON_U, MODE_BUTTON_V, "output_mode", modeHint(false), ignored -> toggleModFilterMode(false)); reloadEntries(); applyFilter(); } - private void addSideButton(int y, int u, int v, String id, Component hint, InvisibleButton.OnPress onPress) { + private InvisibleButton addSideButton(int y, int u, int v, String id, Component hint, InvisibleButton.OnPress onPress) { InvisibleButton button = this.addRenderableWidget(new InvisibleButton( this.leftPos + SIDE_BUTTON_X, this.topPos + y, @@ -120,6 +132,7 @@ private void addSideButton(int y, int u, int v, String id, Component hint, Invis onPress )); button.setSprite(u, v, v, ATLAS_WIDTH, ATLAS_HEIGHT); + return button; } private void toggleFilterMode() { @@ -132,6 +145,49 @@ private void toggleFilterMode() { } } + private void toggleModFilterMode(boolean input) { + FilterMode next; + if (input) { + next = opposite(this.inputMode); + this.inputMode = next; + this.menu.setInputModFilterMode(next); + } else { + next = opposite(this.outputMode); + this.outputMode = next; + this.menu.setOutputModFilterMode(next); + } + if (this.minecraft != null && this.minecraft.gameMode != null) { + this.minecraft.gameMode.handleInventoryButtonClick( + this.menu.containerId, + input + ? PatternEditorMenu.INPUT_MOD_FILTER_MODE_BUTTON + : PatternEditorMenu.OUTPUT_MOD_FILTER_MODE_BUTTON); + } + syncModFilters(); + } + + private FilterMode opposite(FilterMode mode) { + return mode == FilterMode.WHITELIST ? FilterMode.BLACKLIST : FilterMode.WHITELIST; + } + + private Component modeHint(boolean input) { + return Component.translatable( + input ? "gui.ae2sps.mod_filter.input_mode" : "gui.ae2sps.mod_filter.output_mode", + modeText(input ? this.inputMode : this.outputMode)); + } + + private Component modeText(FilterMode mode) { + return Component.translatable(mode == FilterMode.WHITELIST + ? "gui.ae2sps.mode.whitelist" + : "gui.ae2sps.mode.blacklist"); + } + + private Component modeShort(FilterMode mode) { + return Component.translatable(mode == FilterMode.WHITELIST + ? "gui.ae2sps.mod_filter.mode_short.whitelist" + : "gui.ae2sps.mod_filter.mode_short.blacklist"); + } + private void openSearch() { if (this.minecraft != null) { this.minecraft.setScreen(new PatternSearchScreen(this.parent, this.menu)); @@ -220,13 +276,23 @@ public void render(@NotNull GuiGraphics guiGraphics, int mouseX, int mouseY, flo guiGraphics.drawString(this.font, this.title, this.leftPos + 16, this.topPos + 16, 0x404040, false); Component stat = Component.translatable( "gui.ae2sps.mod_filter.stats", - this.excludedInputMods.size(), - this.excludedOutputMods.size(), + modeShort(this.inputMode), + this.inputModIds.size(), + modeShort(this.outputMode), + this.outputModIds.size(), this.allEntries.size()); guiGraphics.drawString(this.font, stat, this.leftPos + 16, this.topPos + 45, 0x666666, false); drawEntryRows(guiGraphics, mouseX, mouseY); drawScrollBar(guiGraphics); + if (this.inputModeButton != null) { + this.inputModeButton.setHint(modeHint(true)); + } + if (this.outputModeButton != null) { + this.outputModeButton.setHint(modeHint(false)); + } super.render(guiGraphics, mouseX, mouseY, partialTick); + guiGraphics.drawString(this.font, "I", this.leftPos + SIDE_BUTTON_X + 4, this.topPos + INPUT_MODE_BUTTON_Y + 8, 0xFFFFFF, true); + guiGraphics.drawString(this.font, "O", this.leftPos + SIDE_BUTTON_X + 4, this.topPos + OUTPUT_MODE_BUTTON_Y + 8, 0xFFFFFF, true); ModEntry hovered = getEntryAt(mouseX, mouseY); if (hovered != null) { @@ -234,12 +300,12 @@ public void render(@NotNull GuiGraphics guiGraphics, int mouseX, int mouseY, flo tooltip.add(Component.literal(hovered.modId() + " - " + hovered.name())); tooltip.add(Component.translatable("gui.ae2sps.mod_filter.tooltip.left")); tooltip.add(Component.translatable("gui.ae2sps.mod_filter.tooltip.right")); - Component inputState = Component.translatable(this.excludedInputMods.contains(hovered.modId()) - ? "gui.ae2sps.mod_filter.state.excluded" - : "gui.ae2sps.mod_filter.state.allowed"); - Component outputState = Component.translatable(this.excludedOutputMods.contains(hovered.modId()) - ? "gui.ae2sps.mod_filter.state.excluded" - : "gui.ae2sps.mod_filter.state.allowed"); + Component inputState = Component.translatable(inputRule().allows(hovered.modId()) + ? "gui.ae2sps.mod_filter.state.allowed" + : "gui.ae2sps.mod_filter.state.excluded"); + Component outputState = Component.translatable(outputRule().allows(hovered.modId()) + ? "gui.ae2sps.mod_filter.state.allowed" + : "gui.ae2sps.mod_filter.state.excluded"); tooltip.add(Component.translatable("gui.ae2sps.mod_filter.tooltip.state", inputState, outputState)); guiGraphics.renderTooltip(this.font, tooltip.stream().map(Component::getVisualOrderText).toList(), mouseX, mouseY); return; @@ -287,24 +353,25 @@ private void applyFilter() { } private void toggleInput(String modId) { - if (!this.excludedInputMods.add(modId)) { - this.excludedInputMods.remove(modId); + if (!this.inputModIds.add(modId)) { + this.inputModIds.remove(modId); } - syncExcludedMods(); + syncModFilters(); } private void toggleOutput(String modId) { - if (!this.excludedOutputMods.add(modId)) { - this.excludedOutputMods.remove(modId); + if (!this.outputModIds.add(modId)) { + this.outputModIds.remove(modId); } - syncExcludedMods(); + syncModFilters(); } - private void syncExcludedMods() { - String[] inputValues = this.excludedInputMods.toArray(new String[0]); - String[] outputValues = this.excludedOutputMods.toArray(new String[0]); - this.menu.applyExcludedModFilters(inputValues, outputValues); - TechStartNetwork.CHANNEL.sendToServer(new SetPatternModFiltersPacket(inputValues, outputValues)); + private void syncModFilters() { + String[] inputValues = this.inputModIds.toArray(new String[0]); + String[] outputValues = this.outputModIds.toArray(new String[0]); + this.menu.applyModFilters(this.inputMode, inputValues, this.outputMode, outputValues); + TechStartNetwork.CHANNEL.sendToServer(new SetPatternModFiltersPacket( + this.inputMode, inputValues, this.outputMode, outputValues)); } private void drawEntryRows(GuiGraphics guiGraphics, int mouseX, int mouseY) { @@ -319,25 +386,44 @@ private void drawEntryRows(GuiGraphics guiGraphics, int mouseX, int mouseY) { int rowY = top + row * ROW_HEIGHT; boolean hovered = mouseX >= left && mouseX < left + LIST_WIDTH && mouseY >= rowY && mouseY < rowY + ROW_HEIGHT; - boolean inputExcluded = this.excludedInputMods.contains(entry.modId()); - boolean outputExcluded = this.excludedOutputMods.contains(entry.modId()); - if (inputExcluded && outputExcluded) { + boolean inputBlocked = !inputRule().allows(entry.modId()); + boolean outputBlocked = !outputRule().allows(entry.modId()); + if (inputBlocked && outputBlocked) { guiGraphics.fill(left, rowY, left + LIST_WIDTH, rowY + ROW_HEIGHT, 0x446633CC); - } else if (inputExcluded) { + } else if (inputBlocked) { guiGraphics.fill(left, rowY, left + LIST_WIDTH, rowY + ROW_HEIGHT, 0x44CC3333); - } else if (outputExcluded) { + } else if (outputBlocked) { guiGraphics.fill(left, rowY, left + LIST_WIDTH, rowY + ROW_HEIGHT, 0x443366CC); } if (hovered) { guiGraphics.fill(left, rowY, left + LIST_WIDTH, rowY + ROW_HEIGHT, 0x33FFFFFF); } - String marker = (inputExcluded ? "I" : "-") + "/" + (outputExcluded ? "O" : "-"); + String marker = (inputBlocked ? "I-" : "I+") + "/" + (outputBlocked ? "O-" : "O+"); String line = this.font.plainSubstrByWidth(marker + " " + entry.modId() + " - " + entry.name(), LIST_WIDTH - 4); guiGraphics.drawString(this.font, line, left + 2, rowY + 4, 0x404040, false); } } + private ModFilterRule inputRule() { + return ModFilterRule.of(this.inputMode, this.inputModIds); + } + + private ModFilterRule outputRule() { + return ModFilterRule.of(this.outputMode, this.outputModIds); + } + + private int countBlocked(boolean input) { + ModFilterRule rule = input ? inputRule() : outputRule(); + int blocked = 0; + for (ModEntry entry : this.allEntries) { + if (!rule.allows(entry.modId())) { + blocked++; + } + } + return blocked; + } + private void drawScrollBar(GuiGraphics guiGraphics) { int barLeft = getScrollBarLeft(); int barTop = getListTop(); diff --git a/versions/forge-1.20.1/src/main/java/com/ae2smartpatternsystem/content/PatternIntegrationsItem.java b/versions/forge-1.20.1/src/main/java/com/ae2smartpatternsystem/content/PatternIntegrationsItem.java index daca177..9729b62 100644 --- a/versions/forge-1.20.1/src/main/java/com/ae2smartpatternsystem/content/PatternIntegrationsItem.java +++ b/versions/forge-1.20.1/src/main/java/com/ae2smartpatternsystem/content/PatternIntegrationsItem.java @@ -1,6 +1,9 @@ package com.ae2smartpatternsystem.content; import com.ae2smartpatternsystem.core.PatternDefinitionBridge; +import com.ae2smartpatternsystem.core.codec.PatternNbtKeys; +import com.ae2smartpatternsystem.core.model.FilterMode; +import com.ae2smartpatternsystem.core.model.ModFilterRule; import com.ae2smartpatternsystem.core.model.PatternDefinition; import com.ae2smartpatternsystem.integration.mekanism.MekanismGasHelper; import net.minecraft.ChatFormatting; @@ -33,8 +36,8 @@ public class PatternIntegrationsItem extends Item { private static final String TAG_INPUTS = "TechStartInputs"; private static final String TAG_OUTPUTS = "TechStartOutputs"; private static final String TAG_ENCODED_ITEM = "EncodedItem"; - private static final String TAG_FILTER_MODE = "TechStartFilterMode"; - private static final String TAG_FILTER_MODE_LEGACY = "FilterMode"; + private static final String TAG_FILTER_MODE = PatternNbtKeys.TAG_FILTER_MODE; + private static final String TAG_FILTER_MODE_LEGACY = PatternNbtKeys.TAG_FILTER_MODE_LEGACY; private static final String TAG_INPUT_FLUIDS = "InputFluids"; private static final String TAG_INPUT_FLUID_AMOUNTS = "InputFluidAmounts"; private static final String TAG_OUTPUT_FLUIDS = "OutputFluids"; @@ -44,8 +47,6 @@ public class PatternIntegrationsItem extends Item { private static final String TAG_INPUT_GAS_AMOUNTS = "InputGasAmounts"; private static final String TAG_OUTPUT_GASES = "OutputGases"; private static final String TAG_OUTPUT_GAS_AMOUNTS = "OutputGasAmounts"; - private static final String TAG_EXCLUDED_INPUT_MOD_IDS = "ExcludedInputModIds"; - private static final String TAG_EXCLUDED_OUTPUT_MOD_IDS = "ExcludedOutputModIds"; private static final String TAG_GAS_MARKER = "TechStartGasMarker"; private static final String TAG_GAS_NAME = "TechStartGasName"; private static final String TAG_GAS_AMOUNT = "TechStartGasAmount"; @@ -55,7 +56,6 @@ public class PatternIntegrationsItem extends Item { private static final String TAG_STACK = "Stack"; private static final int FILTER_MODE_WHITELIST = 0; private static final int FILTER_MODE_BLACKLIST = 1; - public PatternIntegrationsItem(Properties properties) { super(properties); } @@ -101,11 +101,25 @@ public void appendHoverText(ItemStack stack, @Nullable Level level, List inputLines = buildEntryLines(tag, TAG_INPUTS, true); @@ -148,8 +162,12 @@ private void clearPatternData(ItemStack stack) { tag.remove(TAG_INPUT_GAS_AMOUNTS); tag.remove(TAG_OUTPUT_GASES); tag.remove(TAG_OUTPUT_GAS_AMOUNTS); - tag.remove(TAG_EXCLUDED_INPUT_MOD_IDS); - tag.remove(TAG_EXCLUDED_OUTPUT_MOD_IDS); + tag.remove(PatternNbtKeys.TAG_INPUT_MOD_FILTER_MODE); + tag.remove(PatternNbtKeys.TAG_OUTPUT_MOD_FILTER_MODE); + tag.remove(PatternNbtKeys.TAG_INPUT_MOD_FILTER_IDS); + tag.remove(PatternNbtKeys.TAG_OUTPUT_MOD_FILTER_IDS); + tag.remove(PatternNbtKeys.TAG_EXCLUDED_INPUT_MOD_IDS); + tag.remove(PatternNbtKeys.TAG_EXCLUDED_OUTPUT_MOD_IDS); if (tag.isEmpty()) { stack.setTag(null); } @@ -165,6 +183,53 @@ private int readFilterMode(@Nullable CompoundTag tag) { return FILTER_MODE_BLACKLIST; } + private FilterMode readModFilterMode(@Nullable CompoundTag tag, boolean input) { + if (tag == null) { + return FilterMode.BLACKLIST; + } + String modeKey = input + ? PatternNbtKeys.TAG_INPUT_MOD_FILTER_MODE + : PatternNbtKeys.TAG_OUTPUT_MOD_FILTER_MODE; + String idsKey = input + ? PatternNbtKeys.TAG_INPUT_MOD_FILTER_IDS + : PatternNbtKeys.TAG_OUTPUT_MOD_FILTER_IDS; + String legacyKey = input + ? PatternNbtKeys.TAG_EXCLUDED_INPUT_MOD_IDS + : PatternNbtKeys.TAG_EXCLUDED_OUTPUT_MOD_IDS; + if (tag.contains(modeKey, Tag.TAG_INT)) { + return FilterMode.fromSerializedValue(tag.getInt(modeKey)); + } + if (tag.contains(idsKey, Tag.TAG_LIST) || tag.contains(legacyKey, Tag.TAG_LIST)) { + return FilterMode.BLACKLIST; + } + return FilterMode.BLACKLIST; + } + + private List readModFilterIds(@Nullable CompoundTag tag, boolean input) { + if (tag == null) { + return List.of(); + } + String idsKey = input + ? PatternNbtKeys.TAG_INPUT_MOD_FILTER_IDS + : PatternNbtKeys.TAG_OUTPUT_MOD_FILTER_IDS; + String legacyKey = input + ? PatternNbtKeys.TAG_EXCLUDED_INPUT_MOD_IDS + : PatternNbtKeys.TAG_EXCLUDED_OUTPUT_MOD_IDS; + String sourceKey = tag.contains(idsKey, Tag.TAG_LIST) ? idsKey : legacyKey; + if (!tag.contains(sourceKey, Tag.TAG_LIST)) { + return List.of(); + } + ListTag raw = tag.getList(sourceKey, Tag.TAG_STRING); + List ids = new ArrayList<>(Math.min(raw.size(), 512)); + for (int i = 0; i < raw.size() && ids.size() < 512; i++) { + String id = ModFilterRule.normalizeModId(raw.getString(i)); + if (!id.isBlank() && id.length() <= 64 && !ids.contains(id)) { + ids.add(id); + } + } + return ids; + } + private List buildEntryLines(CompoundTag tag, String listKey, boolean input) { if (!tag.contains(listKey, Tag.TAG_LIST)) { return List.of(); diff --git a/versions/forge-1.20.1/src/main/java/com/ae2smartpatternsystem/integration/ae2/TechStartPatternExpansion.java b/versions/forge-1.20.1/src/main/java/com/ae2smartpatternsystem/integration/ae2/TechStartPatternExpansion.java index 7a91cbb..d3a209e 100644 --- a/versions/forge-1.20.1/src/main/java/com/ae2smartpatternsystem/integration/ae2/TechStartPatternExpansion.java +++ b/versions/forge-1.20.1/src/main/java/com/ae2smartpatternsystem/integration/ae2/TechStartPatternExpansion.java @@ -3,9 +3,11 @@ import appeng.api.crafting.IPatternDetails; import appeng.api.stacks.AEItemKey; import com.ae2smartpatternsystem.core.PatternDefinitionBridge; +import com.ae2smartpatternsystem.core.codec.PatternNbtKeys; import com.ae2smartpatternsystem.core.model.EntryKind; import com.ae2smartpatternsystem.core.model.FilterEntry; import com.ae2smartpatternsystem.core.model.FilterMode; +import com.ae2smartpatternsystem.core.model.ModFilterRule; import com.ae2smartpatternsystem.core.model.PatternDefinition; import com.ae2smartpatternsystem.core.model.PatternEntry; import com.ae2smartpatternsystem.core.model.WildcardRecipe; @@ -40,8 +42,6 @@ public final class TechStartPatternExpansion { private static final String TAG_FILTER_MODE = "TechStartFilterMode"; private static final String TAG_FILTER_ENTRIES = "FilterEntries"; private static final String TAG_FILTER_MODE_LEGACY = "FilterMode"; - private static final String TAG_EXCLUDED_INPUT_MOD_IDS = "ExcludedInputModIds"; - private static final String TAG_EXCLUDED_OUTPUT_MOD_IDS = "ExcludedOutputModIds"; private static final String TAG_VIRTUAL_INPUT_STACKS = "VirtualInputStacks"; private static final String TAG_VIRTUAL_OUTPUT_STACKS = "VirtualOutputStacks"; private static final String TAG_VIRTUAL_DISPLAY_NAME = "VirtualDisplayName"; @@ -412,21 +412,37 @@ private static boolean isStackAllowedByModFilter(CompoundTag tag, ItemStack stac return true; } ResourceLocation key = BuiltInRegistries.ITEM.getKey(stack.getItem()); - if (key == null || key.getNamespace().isBlank()) { - return true; - } - ListTag excluded = tag.getList(input ? TAG_EXCLUDED_INPUT_MOD_IDS : TAG_EXCLUDED_OUTPUT_MOD_IDS, Tag.TAG_STRING); - if (excluded.isEmpty()) { - return true; - } - String namespace = key.getNamespace().toLowerCase(java.util.Locale.ROOT); - for (int i = 0; i < excluded.size(); i++) { - String blocked = excluded.getString(i); - if (blocked != null && namespace.equals(blocked.trim().toLowerCase(java.util.Locale.ROOT))) { - return false; + String namespace = key == null ? "" : key.getNamespace(); + return readModFilterRule(tag, input).allows(namespace); + } + + private static ModFilterRule readModFilterRule(CompoundTag tag, boolean input) { + String modeKey = input + ? PatternNbtKeys.TAG_INPUT_MOD_FILTER_MODE + : PatternNbtKeys.TAG_OUTPUT_MOD_FILTER_MODE; + String idsKey = input + ? PatternNbtKeys.TAG_INPUT_MOD_FILTER_IDS + : PatternNbtKeys.TAG_OUTPUT_MOD_FILTER_IDS; + String legacyKey = input + ? PatternNbtKeys.TAG_EXCLUDED_INPUT_MOD_IDS + : PatternNbtKeys.TAG_EXCLUDED_OUTPUT_MOD_IDS; + + boolean canonicalIdsPresent = tag.contains(idsKey, Tag.TAG_LIST); + String sourceKey = canonicalIdsPresent ? idsKey : legacyKey; + ListTag ids = tag.getList(sourceKey, Tag.TAG_STRING); + List normalized = new ArrayList<>(Math.min(ids.size(), 512)); + for (int i = 0; i < ids.size() && normalized.size() < 512; i++) { + String id = ModFilterRule.normalizeModId(ids.getString(i)); + if (id.length() <= 64 && !id.isBlank()) { + normalized.add(id); } } - return true; + Integer serializedMode = tag.contains(modeKey, Tag.TAG_INT) ? tag.getInt(modeKey) : null; + return ModFilterRule.fromStoredData( + serializedMode, + canonicalIdsPresent, + canonicalIdsPresent ? normalized : List.of(), + canonicalIdsPresent ? List.of() : normalized); } private static String getRegistryNamespace(ItemStack stack) { @@ -556,4 +572,3 @@ private List resolve(String key) { private record ScoredOutputCandidate(ItemStack stack, int score, int order) { } } - diff --git a/versions/forge-1.20.1/src/main/java/com/ae2smartpatternsystem/menu/PatternEditorMenu.java b/versions/forge-1.20.1/src/main/java/com/ae2smartpatternsystem/menu/PatternEditorMenu.java index d625dbc..741416f 100644 --- a/versions/forge-1.20.1/src/main/java/com/ae2smartpatternsystem/menu/PatternEditorMenu.java +++ b/versions/forge-1.20.1/src/main/java/com/ae2smartpatternsystem/menu/PatternEditorMenu.java @@ -1,6 +1,9 @@ package com.ae2smartpatternsystem.menu; import com.ae2smartpatternsystem.config.TechStartConfig; +import com.ae2smartpatternsystem.core.codec.PatternNbtKeys; +import com.ae2smartpatternsystem.core.model.FilterMode; +import com.ae2smartpatternsystem.core.model.ModFilterRule; import com.ae2smartpatternsystem.integration.ae2.TechStartPatternExpansion; import com.ae2smartpatternsystem.integration.mekanism.MekanismGasHelper; import com.ae2smartpatternsystem.registry.TechStartItems; @@ -43,6 +46,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; +import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -74,8 +78,6 @@ public class PatternEditorMenu extends AbstractContainerMenu { private static final String TAG_INPUT_GAS_AMOUNTS = "InputGasAmounts"; private static final String TAG_OUTPUT_GASES = "OutputGases"; private static final String TAG_OUTPUT_GAS_AMOUNTS = "OutputGasAmounts"; - private static final String TAG_EXCLUDED_INPUT_MOD_IDS = "ExcludedInputModIds"; - private static final String TAG_EXCLUDED_OUTPUT_MOD_IDS = "ExcludedOutputModIds"; private static final String TAG_INPUT_ORES = "InputOreNames"; private static final String TAG_OUTPUT_ORES = "OutputOreNames"; private static final String TAG_INPUT_COUNTS = "InputCounts"; @@ -94,16 +96,20 @@ public class PatternEditorMenu extends AbstractContainerMenu { public static final int FILTER_MODE_WHITELIST = 0; public static final int FILTER_MODE_BLACKLIST = 1; + public static final int INPUT_MOD_FILTER_MODE_BUTTON = 2; + public static final int OUTPUT_MOD_FILTER_MODE_BUTTON = 3; + private static final int MAX_MOD_FILTER_IDS = 512; + private static final int MAX_MOD_FILTER_ID_LENGTH = 64; private final ItemStackHandler itemHandler; private final ContainerLevelAccess access; private final Player player; private final @Nullable InteractionHand boundPatternHand; private final ItemStack boundPatternStack; - private final ContainerData data = new SimpleContainerData(1); + private final ContainerData data = new SimpleContainerData(3); private final LinkedHashSet filterEntries = new LinkedHashSet<>(); - private final LinkedHashSet excludedInputModIds = new LinkedHashSet<>(); - private final LinkedHashSet excludedOutputModIds = new LinkedHashSet<>(); + private final LinkedHashSet inputModFilterIds = new LinkedHashSet<>(); + private final LinkedHashSet outputModFilterIds = new LinkedHashSet<>(); public static class PatternSlot extends SlotItemHandler { private boolean active = true; @@ -135,6 +141,8 @@ public PatternEditorMenu(int containerId, Inventory playerInventory, Interaction this.boundPatternStack = locateBoundPatternStack(hand); setFilterMode(FILTER_MODE_BLACKLIST); + setInputModFilterMode(FilterMode.BLACKLIST); + setOutputModFilterMode(FilterMode.BLACKLIST); this.addDataSlots(this.data); for (int row = 0; row < 3; row++) { @@ -205,6 +213,16 @@ public boolean clickMenuButton(@NotNull Player player, int id) { saveToPatternItem(); return true; } + if (id == INPUT_MOD_FILTER_MODE_BUTTON) { + setInputModFilterMode(opposite(getInputModFilterMode())); + saveToPatternItem(); + return true; + } + if (id == OUTPUT_MOD_FILTER_MODE_BUTTON) { + setOutputModFilterMode(opposite(getOutputModFilterMode())); + saveToPatternItem(); + return true; + } if (id >= BUTTON_FILTER_ENTRY_BASE) { int entryIndex = id - BUTTON_FILTER_ENTRY_BASE; toggleFilterEntryByIndex(entryIndex); @@ -226,12 +244,28 @@ public List getFilterEntriesSnapshot() { return List.copyOf(this.filterEntries); } - public List getExcludedInputModIdsSnapshot() { - return List.copyOf(this.excludedInputModIds); + public List getInputModFilterIdsSnapshot() { + return List.copyOf(this.inputModFilterIds); + } + + public List getOutputModFilterIdsSnapshot() { + return List.copyOf(this.outputModFilterIds); + } + + public FilterMode getInputModFilterMode() { + return FilterMode.fromSerializedValue(this.data.get(1)); } - public List getExcludedOutputModIdsSnapshot() { - return List.copyOf(this.excludedOutputModIds); + public FilterMode getOutputModFilterMode() { + return FilterMode.fromSerializedValue(this.data.get(2)); + } + + public ModFilterRule getInputModFilterRuleSnapshot() { + return ModFilterRule.of(getInputModFilterMode(), this.inputModFilterIds); + } + + public ModFilterRule getOutputModFilterRuleSnapshot() { + return ModFilterRule.of(getOutputModFilterMode(), this.outputModFilterIds); } public ItemStack getPatternStackSnapshot() { @@ -256,16 +290,23 @@ public void applyPatternSlotAmountFromClient(int slotId, int amount, Player play onPatternSlotsMutated(player); } - public void applyExcludedModFilters(String[] inputValues, String[] outputValues) { - this.excludedInputModIds.clear(); - this.excludedInputModIds.addAll(normalizeModIds(inputValues)); - this.excludedOutputModIds.clear(); - this.excludedOutputModIds.addAll(normalizeModIds(outputValues)); + public void applyModFilters(FilterMode inputMode, String[] inputValues, FilterMode outputMode, String[] outputValues) { + setInputModFilterMode(inputMode); + setOutputModFilterMode(outputMode); + this.inputModFilterIds.clear(); + this.inputModFilterIds.addAll(normalizeModIds(inputValues)); + this.outputModFilterIds.clear(); + this.outputModFilterIds.addAll(normalizeModIds(outputValues)); saveToPatternItem(); } - public void applyExcludedModFiltersFromClient(String[] inputValues, String[] outputValues, Player actor) { - applyExcludedModFilters(inputValues, outputValues); + public void applyModFiltersFromClient( + FilterMode inputMode, + String[] inputValues, + FilterMode outputMode, + String[] outputValues, + Player actor) { + applyModFilters(inputMode, inputValues, outputMode, outputValues); if (!actor.level().isClientSide) { this.broadcastChanges(); } @@ -337,6 +378,18 @@ private void setFilterMode(int mode) { this.data.set(0, normalizeFilterMode(mode)); } + public void setInputModFilterMode(FilterMode mode) { + this.data.set(1, Objects.requireNonNull(mode, "mode").serializedValue()); + } + + public void setOutputModFilterMode(FilterMode mode) { + this.data.set(2, Objects.requireNonNull(mode, "mode").serializedValue()); + } + + private FilterMode opposite(FilterMode mode) { + return mode == FilterMode.WHITELIST ? FilterMode.BLACKLIST : FilterMode.WHITELIST; + } + private int normalizeFilterMode(int mode) { return mode == FILTER_MODE_WHITELIST ? FILTER_MODE_WHITELIST : FILTER_MODE_BLACKLIST; } @@ -353,9 +406,11 @@ private int readFilterMode(CompoundTag tag) { private void loadFilterStateFromPatternItem() { this.filterEntries.clear(); - this.excludedInputModIds.clear(); - this.excludedOutputModIds.clear(); + this.inputModFilterIds.clear(); + this.outputModFilterIds.clear(); setFilterMode(FILTER_MODE_BLACKLIST); + setInputModFilterMode(FilterMode.BLACKLIST); + setOutputModFilterMode(FilterMode.BLACKLIST); ItemStack patternStack = resolvePatternStack(); if (patternStack.isEmpty()) { @@ -367,8 +422,8 @@ private void loadFilterStateFromPatternItem() { } setFilterMode(readFilterMode(tag)); readFilterEntries(tag); - readExcludedModIds(tag, true); - readExcludedModIds(tag, false); + readModFilterState(tag, true); + readModFilterState(tag, false); } private ItemStack locateBoundPatternStack() { @@ -527,8 +582,8 @@ private void saveToPatternItem() { CompoundTag tag = patternStack.getOrCreateTag(); tag.putInt(TAG_FILTER_MODE, getFilterMode()); tag.putBoolean(TAG_ENCODED, encoded); - writeExcludedModIds(tag, true); - writeExcludedModIds(tag, false); + writeModFilterState(tag, true); + writeModFilterState(tag, false); if (encoded) { tag.put(TAG_INPUTS, inputs); @@ -579,16 +634,49 @@ private void readFilterEntries(CompoundTag tag) { } } - private void readExcludedModIds(CompoundTag tag, boolean input) { - String key = input ? TAG_EXCLUDED_INPUT_MOD_IDS : TAG_EXCLUDED_OUTPUT_MOD_IDS; - if (!tag.contains(key, Tag.TAG_LIST)) { + private void readModFilterState(CompoundTag tag, boolean input) { + String modeKey = input + ? PatternNbtKeys.TAG_INPUT_MOD_FILTER_MODE + : PatternNbtKeys.TAG_OUTPUT_MOD_FILTER_MODE; + String idsKey = input + ? PatternNbtKeys.TAG_INPUT_MOD_FILTER_IDS + : PatternNbtKeys.TAG_OUTPUT_MOD_FILTER_IDS; + String legacyKey = input + ? PatternNbtKeys.TAG_EXCLUDED_INPUT_MOD_IDS + : PatternNbtKeys.TAG_EXCLUDED_OUTPUT_MOD_IDS; + boolean hasCanonicalMode = tag.contains(modeKey, Tag.TAG_INT); + boolean hasCanonicalIds = tag.contains(idsKey, Tag.TAG_LIST); + LinkedHashSet target = input ? this.inputModFilterIds : this.outputModFilterIds; + if (hasCanonicalMode || hasCanonicalIds) { + FilterMode mode = hasCanonicalMode + ? FilterMode.fromSerializedValue(tag.getInt(modeKey)) + : FilterMode.BLACKLIST; + if (input) { + setInputModFilterMode(mode); + } else { + setOutputModFilterMode(mode); + } + } else if (tag.contains(legacyKey, Tag.TAG_LIST)) { + if (input) { + setInputModFilterMode(FilterMode.BLACKLIST); + } else { + setOutputModFilterMode(FilterMode.BLACKLIST); + } + } else { return; } - ListTag list = tag.getList(key, Tag.TAG_STRING); - LinkedHashSet target = input ? this.excludedInputModIds : this.excludedOutputModIds; + + String sourceKey = hasCanonicalIds ? idsKey : legacyKey; + if (!tag.contains(sourceKey, Tag.TAG_LIST)) { + return; + } + ListTag list = tag.getList(sourceKey, Tag.TAG_STRING); for (int i = 0; i < list.size(); i++) { + if (target.size() >= MAX_MOD_FILTER_IDS) { + break; + } String value = normalizeModId(list.getString(i)); - if (!value.isBlank()) { + if (value.length() <= MAX_MOD_FILTER_ID_LENGTH && !value.isBlank()) { target.add(value); } } @@ -613,24 +701,31 @@ private void writeFilterEntries(CompoundTag tag) { } } - private void writeExcludedModIds(CompoundTag tag, boolean input) { - LinkedHashSet source = input ? this.excludedInputModIds : this.excludedOutputModIds; - String key = input ? TAG_EXCLUDED_INPUT_MOD_IDS : TAG_EXCLUDED_OUTPUT_MOD_IDS; + private void writeModFilterState(CompoundTag tag, boolean input) { + LinkedHashSet source = input ? this.inputModFilterIds : this.outputModFilterIds; + String modeKey = input + ? PatternNbtKeys.TAG_INPUT_MOD_FILTER_MODE + : PatternNbtKeys.TAG_OUTPUT_MOD_FILTER_MODE; + String idsKey = input + ? PatternNbtKeys.TAG_INPUT_MOD_FILTER_IDS + : PatternNbtKeys.TAG_OUTPUT_MOD_FILTER_IDS; + String legacyKey = input + ? PatternNbtKeys.TAG_EXCLUDED_INPUT_MOD_IDS + : PatternNbtKeys.TAG_EXCLUDED_OUTPUT_MOD_IDS; + tag.putInt(modeKey, input ? getInputModFilterMode().serializedValue() : getOutputModFilterMode().serializedValue()); + tag.remove(legacyKey); if (source.isEmpty()) { - tag.remove(key); + tag.put(idsKey, new ListTag()); return; } ListTag list = new ListTag(); for (String modId : source) { - if (modId == null || modId.isBlank()) { - continue; - } list.add(StringTag.valueOf(modId)); } if (list.isEmpty()) { - tag.remove(key); + tag.remove(idsKey); } else { - tag.put(key, list); + tag.put(idsKey, list); } } @@ -733,7 +828,10 @@ private static List normalizeModIds(String[] values) { } for (String value : values) { String modId = normalizeModId(value); - if (!modId.isBlank()) { + if (normalized.size() >= MAX_MOD_FILTER_IDS) { + break; + } + if (modId.length() <= MAX_MOD_FILTER_ID_LENGTH && !modId.isBlank()) { normalized.add(modId); } } @@ -741,7 +839,7 @@ private static List normalizeModIds(String[] values) { } private static String normalizeModId(String modId) { - return modId == null ? "" : modId.trim().toLowerCase(Locale.ROOT); + return ModFilterRule.normalizeModId(modId); } private void applyPatternSlotCount(int slotId, int count) { @@ -1391,5 +1489,3 @@ private void onPatternSlotsMutated(Player player) { this.broadcastChanges(); } } - - diff --git a/versions/forge-1.20.1/src/main/java/com/ae2smartpatternsystem/network/SetPatternModFiltersPacket.java b/versions/forge-1.20.1/src/main/java/com/ae2smartpatternsystem/network/SetPatternModFiltersPacket.java index e0f4dad..9f00f47 100644 --- a/versions/forge-1.20.1/src/main/java/com/ae2smartpatternsystem/network/SetPatternModFiltersPacket.java +++ b/versions/forge-1.20.1/src/main/java/com/ae2smartpatternsystem/network/SetPatternModFiltersPacket.java @@ -1,6 +1,7 @@ package com.ae2smartpatternsystem.network; import com.ae2smartpatternsystem.menu.PatternEditorMenu; +import com.ae2smartpatternsystem.core.model.FilterMode; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.server.level.ServerPlayer; import net.minecraftforge.network.NetworkEvent; @@ -9,25 +10,44 @@ import java.util.List; import java.util.function.Supplier; -public record SetPatternModFiltersPacket(String[] excludedInputModIds, String[] excludedOutputModIds) { +public record SetPatternModFiltersPacket( + FilterMode inputMode, + String[] inputIds, + FilterMode outputMode, + String[] outputIds) { + private static final int MAX_IDS_PER_SIDE = 512; + private static final int MAX_ID_LENGTH = 64; + public static void encode(SetPatternModFiltersPacket packet, FriendlyByteBuf buf) { - writeStringArray(buf, packet.excludedInputModIds); - writeStringArray(buf, packet.excludedOutputModIds); + validate(packet); + buf.writeEnum(packet.inputMode); + writeStringArray(buf, packet.inputIds); + buf.writeEnum(packet.outputMode); + writeStringArray(buf, packet.outputIds); } public static SetPatternModFiltersPacket decode(FriendlyByteBuf buf) { - return new SetPatternModFiltersPacket(readStringArray(buf), readStringArray(buf)); + return new SetPatternModFiltersPacket( + buf.readEnum(FilterMode.class), + readStringArray(buf), + buf.readEnum(FilterMode.class), + readStringArray(buf)); } public static void handle(SetPatternModFiltersPacket packet, Supplier contextSupplier) { NetworkEvent.Context context = contextSupplier.get(); context.enqueueWork(() -> { ServerPlayer sender = context.getSender(); - if (sender == null) { + if (sender == null || !isValid(packet)) { return; } if (sender.containerMenu instanceof PatternEditorMenu menu) { - menu.applyExcludedModFiltersFromClient(packet.excludedInputModIds, packet.excludedOutputModIds, sender); + menu.applyModFiltersFromClient( + packet.inputMode, + packet.inputIds, + packet.outputMode, + packet.outputIds, + sender); } }); context.setPacketHandled(true); @@ -36,20 +56,53 @@ public static void handle(SetPatternModFiltersPacket packet, Supplier MAX_IDS_PER_SIDE) { + throw new IllegalArgumentException("Too many mod filter ids: " + size); + } List values = new ArrayList<>(size); for (int i = 0; i < size; i++) { - String value = buf.readUtf(); + String value = buf.readUtf(MAX_ID_LENGTH); if (!value.isBlank()) { values.add(value); } } return values.toArray(new String[0]); } + + static void validate(SetPatternModFiltersPacket packet) { + if (!isValid(packet)) { + throw new IllegalArgumentException("Invalid pattern mod filter payload"); + } + } + + static boolean isValid(SetPatternModFiltersPacket packet) { + return packet != null + && packet.inputMode != null + && packet.outputMode != null + && isValidStringArray(packet.inputIds) + && isValidStringArray(packet.outputIds); + } + + private static boolean isValidStringArray(String[] values) { + if (values == null) { + return true; + } + if (values.length > MAX_IDS_PER_SIDE) { + return false; + } + for (String value : values) { + if (value != null && value.length() > MAX_ID_LENGTH) { + return false; + } + } + return true; + } } diff --git a/versions/forge-1.20.1/src/main/java/com/ae2smartpatternsystem/network/TechStartNetwork.java b/versions/forge-1.20.1/src/main/java/com/ae2smartpatternsystem/network/TechStartNetwork.java index c736716..3eb7b32 100644 --- a/versions/forge-1.20.1/src/main/java/com/ae2smartpatternsystem/network/TechStartNetwork.java +++ b/versions/forge-1.20.1/src/main/java/com/ae2smartpatternsystem/network/TechStartNetwork.java @@ -6,7 +6,7 @@ import net.minecraftforge.network.simple.SimpleChannel; public final class TechStartNetwork { - private static final String PROTOCOL = "2"; + private static final String PROTOCOL = "3"; public static final SimpleChannel CHANNEL = NetworkRegistry.newSimpleChannel( ResourceLocation.fromNamespaceAndPath(TechStartForge.MODID, "network"), () -> PROTOCOL, diff --git a/versions/forge-1.20.1/src/main/resources/assets/sampleintegration/lang/en_us.json b/versions/forge-1.20.1/src/main/resources/assets/sampleintegration/lang/en_us.json index 55783c3..cc7d7f4 100644 --- a/versions/forge-1.20.1/src/main/resources/assets/sampleintegration/lang/en_us.json +++ b/versions/forge-1.20.1/src/main/resources/assets/sampleintegration/lang/en_us.json @@ -17,14 +17,18 @@ "gui.ae2sps.toggle_filter_mode": "Toggle whitelist/blacklist mode", "gui.ae2sps.open_mod_filter": "Open mod filter", "gui.ae2sps.mod_filter.title": "Mod Filter", - "gui.ae2sps.mod_filter.stats": "Input blocked: %s | Output blocked: %s | Total mods: %s", + "gui.ae2sps.mod_filter.stats": "I %s:%s | O %s:%s | Total:%s", "gui.ae2sps.mod_filter.back": "Back", "gui.ae2sps.mod_filter.clear": "Clear", - "gui.ae2sps.mod_filter.tooltip.left": "Left click: toggle input exclusion", - "gui.ae2sps.mod_filter.tooltip.right": "Right click: toggle output exclusion", + "gui.ae2sps.mod_filter.input_mode": "Input mod filter: %s", + "gui.ae2sps.mod_filter.output_mode": "Output mod filter: %s", + "gui.ae2sps.mod_filter.mode_short.whitelist": "W", + "gui.ae2sps.mod_filter.mode_short.blacklist": "B", + "gui.ae2sps.mod_filter.tooltip.left": "Left click: toggle input mod list", + "gui.ae2sps.mod_filter.tooltip.right": "Right click: toggle output mod list", "gui.ae2sps.mod_filter.tooltip.state": "State - Input: %s, Output: %s", "gui.ae2sps.mod_filter.state.allowed": "Allowed", - "gui.ae2sps.mod_filter.state.excluded": "Excluded", + "gui.ae2sps.mod_filter.state.excluded": "Blocked", "gui.ae2sps.open_search": "Open search", "gui.ae2sps.search.title": "Pattern Search", "gui.ae2sps.search.matches": "Matches: %s / %s", @@ -42,6 +46,8 @@ "tooltip.ae2sps.filter_mode": "Filter mode: %s", "tooltip.ae2sps.filter_mode_whitelist": "Whitelist", "tooltip.ae2sps.filter_mode_blacklist": "Blacklist", + "tooltip.ae2sps.input_mod_filter": "Input mod filter: %s (%s listed)", + "tooltip.ae2sps.output_mod_filter": "Output mod filter: %s (%s listed)", "tooltip.ae2sps.bool_yes": "Yes", "tooltip.ae2sps.bool_no": "No", "tooltip.ae2sps.input_count": "Input entries: %s", diff --git a/versions/forge-1.20.1/src/main/resources/assets/sampleintegration/lang/zh_cn.json b/versions/forge-1.20.1/src/main/resources/assets/sampleintegration/lang/zh_cn.json index e50dfc1..28dc3d9 100644 --- a/versions/forge-1.20.1/src/main/resources/assets/sampleintegration/lang/zh_cn.json +++ b/versions/forge-1.20.1/src/main/resources/assets/sampleintegration/lang/zh_cn.json @@ -17,11 +17,15 @@ "gui.ae2sps.toggle_filter_mode": "\u5207\u6362\u9ed1\u767d\u540d\u5355\u6a21\u5f0f", "gui.ae2sps.open_mod_filter": "\u6253\u5f00\u6a21\u7ec4\u7b5b\u9009", "gui.ae2sps.mod_filter.title": "\u6a21\u7ec4\u7b5b\u9009", - "gui.ae2sps.mod_filter.stats": "\u8f93\u5165\u5c4f\u853d\uff1a%s | \u8f93\u51fa\u5c4f\u853d\uff1a%s | \u6a21\u7ec4\u603b\u6570\uff1a%s", + "gui.ae2sps.mod_filter.stats": "\u5165 %s:%s | \u51fa %s:%s | \u603b:%s", "gui.ae2sps.mod_filter.back": "\u8fd4\u56de", "gui.ae2sps.mod_filter.clear": "\u6e05\u7a7a", - "gui.ae2sps.mod_filter.tooltip.left": "\u5de6\u952e\uff1a\u5207\u6362\u8f93\u5165\u5c4f\u853d", - "gui.ae2sps.mod_filter.tooltip.right": "\u53f3\u952e\uff1a\u5207\u6362\u8f93\u51fa\u5c4f\u853d", + "gui.ae2sps.mod_filter.input_mode": "\u8f93\u5165\u6a21\u7ec4\u7b5b\u9009\uff1a%s", + "gui.ae2sps.mod_filter.output_mode": "\u8f93\u51fa\u6a21\u7ec4\u7b5b\u9009\uff1a%s", + "gui.ae2sps.mod_filter.mode_short.whitelist": "\u767d", + "gui.ae2sps.mod_filter.mode_short.blacklist": "\u9ed1", + "gui.ae2sps.mod_filter.tooltip.left": "\u5de6\u952e\uff1a\u5207\u6362\u8f93\u5165\u6a21\u7ec4\u540d\u5355", + "gui.ae2sps.mod_filter.tooltip.right": "\u53f3\u952e\uff1a\u5207\u6362\u8f93\u51fa\u6a21\u7ec4\u540d\u5355", "gui.ae2sps.mod_filter.tooltip.state": "\u72b6\u6001 - \u8f93\u5165\uff1a%s\uff0c\u8f93\u51fa\uff1a%s", "gui.ae2sps.mod_filter.state.allowed": "\u5141\u8bb8", "gui.ae2sps.mod_filter.state.excluded": "\u5c4f\u853d", @@ -42,6 +46,8 @@ "tooltip.ae2sps.filter_mode": "\u8fc7\u6ee4\u6a21\u5f0f\uff1a%s", "tooltip.ae2sps.filter_mode_whitelist": "\u767d\u540d\u5355", "tooltip.ae2sps.filter_mode_blacklist": "\u9ed1\u540d\u5355", + "tooltip.ae2sps.input_mod_filter": "\u8f93\u5165\u6a21\u7ec4\u7b5b\u9009\uff1a%s\uff08\u5df2\u5217\u51fa %s \u9879\uff09", + "tooltip.ae2sps.output_mod_filter": "\u8f93\u51fa\u6a21\u7ec4\u7b5b\u9009\uff1a%s\uff08\u5df2\u5217\u51fa %s \u9879\uff09", "tooltip.ae2sps.bool_yes": "\u662f", "tooltip.ae2sps.bool_no": "\u5426", "tooltip.ae2sps.input_count": "\u8f93\u5165\u6761\u76ee\uff1a%s", diff --git a/versions/forge-1.20.1/src/test/java/com/ae2smartpatternsystem/network/SetPatternModFiltersPacketTest.java b/versions/forge-1.20.1/src/test/java/com/ae2smartpatternsystem/network/SetPatternModFiltersPacketTest.java new file mode 100644 index 0000000..0c4d46c --- /dev/null +++ b/versions/forge-1.20.1/src/test/java/com/ae2smartpatternsystem/network/SetPatternModFiltersPacketTest.java @@ -0,0 +1,58 @@ +package com.ae2smartpatternsystem.network; + +import com.ae2smartpatternsystem.core.model.FilterMode; +import io.netty.buffer.Unpooled; +import net.minecraft.network.FriendlyByteBuf; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class SetPatternModFiltersPacketTest { + @Test + void codecRoundTripPreservesIndependentModesAndIds() { + SetPatternModFiltersPacket source = new SetPatternModFiltersPacket( + FilterMode.WHITELIST, + new String[]{"create", "ae2"}, + FilterMode.BLACKLIST, + new String[]{"minecraft"}); + FriendlyByteBuf buffer = new FriendlyByteBuf(Unpooled.buffer()); + + SetPatternModFiltersPacket.encode(source, buffer); + SetPatternModFiltersPacket decoded = SetPatternModFiltersPacket.decode(buffer); + + assertEquals(source.inputMode(), decoded.inputMode()); + assertArrayEquals(source.inputIds(), decoded.inputIds()); + assertEquals(source.outputMode(), decoded.outputMode()); + assertArrayEquals(source.outputIds(), decoded.outputIds()); + } + + @Test + void codecRejectsOversizedListsAndIdsInsteadOfTruncating() { + String[] tooMany = new String[513]; + Arrays.fill(tooMany, "create"); + SetPatternModFiltersPacket tooManyPacket = new SetPatternModFiltersPacket( + FilterMode.BLACKLIST, tooMany, FilterMode.BLACKLIST, new String[0]); + SetPatternModFiltersPacket tooLongPacket = new SetPatternModFiltersPacket( + FilterMode.BLACKLIST, new String[]{"a".repeat(65)}, FilterMode.BLACKLIST, new String[0]); + + assertThrows(IllegalArgumentException.class, () -> + SetPatternModFiltersPacket.encode(tooManyPacket, new FriendlyByteBuf(Unpooled.buffer()))); + assertThrows(IllegalArgumentException.class, () -> + SetPatternModFiltersPacket.encode(tooLongPacket, new FriendlyByteBuf(Unpooled.buffer()))); + } + + @Test + void serverValidationRejectsNullModesAndOversizedValues() { + assertFalse(SetPatternModFiltersPacket.isValid(new SetPatternModFiltersPacket( + null, new String[0], FilterMode.BLACKLIST, new String[0]))); + assertFalse(SetPatternModFiltersPacket.isValid(new SetPatternModFiltersPacket( + FilterMode.BLACKLIST, new String[513], FilterMode.BLACKLIST, new String[0]))); + assertFalse(SetPatternModFiltersPacket.isValid(new SetPatternModFiltersPacket( + FilterMode.BLACKLIST, new String[]{"a".repeat(65)}, FilterMode.BLACKLIST, new String[0]))); + } +} diff --git a/versions/neoforge-1.21.1/README.md b/versions/neoforge-1.21.1/README.md index 03927f8..1968f2c 100644 --- a/versions/neoforge-1.21.1/README.md +++ b/versions/neoforge-1.21.1/README.md @@ -1,27 +1,40 @@ +# AE2SPS Smart Pattern System - NeoForge 1.21.1 -Installation information -======= +This version targets Minecraft 1.21.1 on NeoForge 21.1.x and integrates with +Applied Energistics 2. -This template repository can be directly cloned to get you started with a new -mod. Simply create a new repository cloned from this one, by following the -instructions at [github](https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-repository-from-a-template). +## Mod filters -Once you have your clone, simply open the repository in the IDE of your choice. The usual recommendation for an IDE is either IntelliJ IDEA or Eclipse. +Each smart pattern stores independent input and output mod rules: -> **Note**: For Eclipse, use tasks in `Launch Group` instead of ones founds in `Java Application`. A preparation task must run before launching the game. NeoGradle uses launch groups to do these subsequently. +- `Whitelist`: only listed mod IDs are allowed. +- `Blacklist`: listed mod IDs are blocked and unlisted IDs are allowed. +- Left-click a mod row to edit the input set. +- Right-click a mod row to edit the output set. +- The `I` and `O` mode buttons switch the corresponding rule. -If at any point you are missing libraries in your IDE, or you've run into problems you can -run `gradlew --refresh-dependencies` to refresh the local cache. `gradlew clean` to reset everything -{this does not affect your code} and then start the process again. +Mod IDs are normalized to lowercase namespace IDs. The server accepts at most +512 IDs per side and 64 characters per ID. -Mapping Names: -============ -By default, the MDK is configured to use the official mapping names from Mojang for methods and fields -in the Minecraft codebase. These names are covered by a specific license. All modders should be aware of this -license. For the latest license text, refer to the mapping file itself, or the reference copy here: -https://github.com/NeoForged/NeoForm/blob/main/Mojang.md +The canonical NBT keys are: -Additional Resources: -========== -Community Documentation: https://docs.neoforged.net/ -NeoForged Discord: https://discord.neoforged.net/ +- `TechStartInputModFilterMode` +- `TechStartOutputModFilterMode` +- `TechStartInputModFilterIds` +- `TechStartOutputModFilterIds` + +Patterns using `ExcludedInputModIds` or `ExcludedOutputModIds` are migrated as +blacklists when loaded. Saving writes the canonical keys and removes the legacy +keys. + +The recipe `FilterEntries` list remains a separate second-stage recipe filter. + +## Development + +Run from this directory: + +```text +gradlew.bat test build --no-daemon --console=plain +``` + +The build includes the shared common-core main sources and its JUnit 5 tests. diff --git a/versions/neoforge-1.21.1/build.gradle b/versions/neoforge-1.21.1/build.gradle index 526730a..d342a6c 100644 --- a/versions/neoforge-1.21.1/build.gradle +++ b/versions/neoforge-1.21.1/build.gradle @@ -30,6 +30,14 @@ repositories { url = 'https://modmaven.dev/' content { includeGroup 'appeng' + includeGroup 'mekanism' + } + } + maven { + name = 'Modrinth' + url = 'https://api.modrinth.com/maven' + content { + includeGroup 'maven.modrinth' } } } @@ -106,6 +114,7 @@ runs { // Include resources generated by data generators. sourceSets.main.resources { srcDir 'src/generated/resources' } sourceSets.main.java { srcDir '../shared/common-core/src/main/java' } +sourceSets.test.java { srcDir '../shared/common-core/src/test/java' } // Sets up a dependency configuration called 'localRuntime'. // This configuration should be used instead of 'runtimeOnly' to declare @@ -119,8 +128,8 @@ dependencies { implementation "net.neoforged:neoforge:${neo_version}" compileOnly "appeng:appliedenergistics2:${ae2_version}:api" compileOnly "appeng:appliedenergistics2:${ae2_version}" - compileOnly fileTree(dir: 'D:/\u684c\u9762/\u6211\u7684\u4e16\u754c/GTL/versions/1.21.1-NeoForge_21.1.219/mods', include: ['*Mekanism-1.21.1-*.jar']) - compileOnly files('D:/\u684c\u9762/\u6211\u7684\u4e16\u754c/GTL/versions/1.21.1-NeoForge_21.1.219/mods/[\u5e94\u7528\u80fd\u6e90\uff1a\u901a\u7528\u673a\u68b0\u9644\u5c5e] Applied-Mekanistics-1.6.3.jar') + compileOnly "mekanism:Mekanism:${minecraft_version}-${mekanism_version}:api" + compileOnly "maven.modrinth:applied-mekanistics:${applied_mekanistics_version}" if ((findProperty('enableAe2Runtime') ?: 'false').toString().toBoolean()) { localRuntime "appeng:appliedenergistics2:${ae2_version}" } @@ -128,10 +137,12 @@ dependencies { if ((findProperty('enableJeiRuntime') ?: 'false').toString().toBoolean()) { localRuntime "mezz.jei:jei-1.21.1-neoforge:19.27.0.340" } - compileOnly "dev.emi:emi-neoforge:1.1.22+1.21.1" + compileOnly "maven.modrinth:emi:${emi_version}" if ((findProperty('enableEmiRuntime') ?: 'false').toString().toBoolean()) { - localRuntime "dev.emi:emi-neoforge:1.1.22+1.21.1" + localRuntime "maven.modrinth:emi:${emi_version}" } + testImplementation platform('org.junit:junit-bom:5.11.0') + testImplementation 'org.junit.jupiter:junit-jupiter' } // This block of code expands all declared replace properties in the specified resource targets. @@ -177,6 +188,10 @@ tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8' // Use the UTF-8 charset for Java compilation } +tasks.withType(Test).configureEach { + useJUnitPlatform() +} + tasks.named('jar', Jar).configure { manifest { attributes(commonManifestAttributes()) diff --git a/versions/neoforge-1.21.1/gradle.properties b/versions/neoforge-1.21.1/gradle.properties index 0ce4314..f542fb3 100644 --- a/versions/neoforge-1.21.1/gradle.properties +++ b/versions/neoforge-1.21.1/gradle.properties @@ -4,8 +4,6 @@ org.gradle.parallel=true org.gradle.caching=true org.gradle.configuration-cache=false org.gradle.java.installations.auto-download=true -org.gradle.java.home=C:/Users/PC/AppData/Roaming/.hmcl/java/windows-x86_64/mojang-java-runtime-delta -org.gradle.java.installations.paths=C:/Users/PC/AppData/Roaming/.hmcl/java/windows-x86_64/mojang-java-runtime-delta neogradle.subsystems.parchment.minecraftVersion=1.21.1 neogradle.subsystems.parchment.mappingsVersion=2024.11.17 @@ -15,6 +13,9 @@ minecraft_version_range=[1.21.1] neo_version=21.1.219 neo_version_range=[21.1.0,) ae2_version=19.2.0-beta +mekanism_version=10.7.14.79 +applied_mekanistics_version=1.6.3 +emi_version=1.1.22+1.21.1+neoforge loader_version_range=[4,) mod_id=sampleintegration diff --git a/versions/neoforge-1.21.1/src/main/java/com/ae2smartpatternsystem/client/ModFilterScreen.java b/versions/neoforge-1.21.1/src/main/java/com/ae2smartpatternsystem/client/ModFilterScreen.java index 31c015f..7ebf915 100644 --- a/versions/neoforge-1.21.1/src/main/java/com/ae2smartpatternsystem/client/ModFilterScreen.java +++ b/versions/neoforge-1.21.1/src/main/java/com/ae2smartpatternsystem/client/ModFilterScreen.java @@ -3,6 +3,7 @@ import com.ae2smartpatternsystem.menu.PatternEditorMenu; import com.ae2smartpatternsystem.network.SetPatternModFiltersPayload; import com.ae2smartpatternsystem.network.TechStartNetwork; +import com.ae2smartpatternsystem.core.model.ModFilterRule; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.EditBox; import net.minecraft.client.gui.screens.Screen; @@ -44,6 +45,8 @@ public class ModFilterScreen extends Screen { private static final int MODE_BUTTON_Y = 48; private static final int SEARCH_BUTTON_Y = 72; private static final int MOD_FILTER_BUTTON_Y = 96; + private static final int INPUT_MODE_BUTTON_Y = 120; + private static final int OUTPUT_MODE_BUTTON_Y = 144; private static final int GUI_WIDTH = SIDE_BUTTON_X + SIDE_BUTTON_WIDTH; private static final int GUI_HEIGHT = PANEL_HEIGHT; private static final int LIST_X = 16; @@ -59,10 +62,12 @@ public class ModFilterScreen extends Screen { private final PatternEditorMenu menu; private final List allEntries = new ArrayList<>(); private final List filteredEntries = new ArrayList<>(); - private final LinkedHashSet excludedInputMods = new LinkedHashSet<>(); - private final LinkedHashSet excludedOutputMods = new LinkedHashSet<>(); + private final LinkedHashSet inputModIds = new LinkedHashSet<>(); + private final LinkedHashSet outputModIds = new LinkedHashSet<>(); private EditBox searchField; + private InvisibleButton inputModeButton; + private InvisibleButton outputModeButton; private int leftPos; private int topPos; private int listScroll = 0; @@ -80,10 +85,10 @@ protected void init() { this.leftPos = (this.width - GUI_WIDTH) / 2; this.topPos = (this.height - GUI_HEIGHT) / 2; - this.excludedInputMods.clear(); - this.excludedInputMods.addAll(this.menu.getExcludedInputModIdsSnapshot()); - this.excludedOutputMods.clear(); - this.excludedOutputMods.addAll(this.menu.getExcludedOutputModIdsSnapshot()); + this.inputModIds.clear(); + this.inputModIds.addAll(this.menu.getInputModFilterIdsSnapshot()); + this.outputModIds.clear(); + this.outputModIds.addAll(this.menu.getOutputModFilterIdsSnapshot()); this.searchField = new EditBox(this.font, this.leftPos + 38, this.topPos + 25, 136, 12, Component.empty()); this.searchField.setCanLoseFocus(false); @@ -101,12 +106,18 @@ protected void init() { addSideButton(MODE_BUTTON_Y, MODE_BUTTON_U, MODE_BUTTON_V, "mode", Component.translatable("gui.ae2sps.toggle_filter_mode"), ignored -> toggleFilterMode()); addSideButton(SEARCH_BUTTON_Y, SEARCH_BUTTON_U, SEARCH_BUTTON_V, "search", Component.translatable("gui.ae2sps.open_search"), ignored -> openSearch()); addSideButton(MOD_FILTER_BUTTON_Y, MOD_FILTER_BUTTON_U, MOD_FILTER_BUTTON_V, "mod", Component.translatable("gui.ae2sps.open_mod_filter"), ignored -> focusSearch()); + this.inputModeButton = addSideButton( + INPUT_MODE_BUTTON_Y, MODE_BUTTON_U, MODE_BUTTON_V, "input_mode", + modeHint(true), ignored -> toggleModFilterMode(true)); + this.outputModeButton = addSideButton( + OUTPUT_MODE_BUTTON_Y, MODE_BUTTON_U, MODE_BUTTON_V, "output_mode", + modeHint(false), ignored -> toggleModFilterMode(false)); reloadEntries(); applyFilter(); } - private void addSideButton(int y, int u, int v, String id, Component hint, InvisibleButton.OnPress onPress) { + private InvisibleButton addSideButton(int y, int u, int v, String id, Component hint, InvisibleButton.OnPress onPress) { InvisibleButton button = this.addRenderableWidget(new InvisibleButton( this.leftPos + SIDE_BUTTON_X, this.topPos + y, @@ -117,6 +128,7 @@ private void addSideButton(int y, int u, int v, String id, Component hint, Invis onPress )); button.setSprite(u, v, v, ATLAS_WIDTH, ATLAS_HEIGHT); + return button; } private void toggleFilterMode() { @@ -129,6 +141,29 @@ private void toggleFilterMode() { } } + private void toggleModFilterMode(boolean input) { + if (this.minecraft != null && this.minecraft.gameMode != null) { + this.minecraft.gameMode.handleInventoryButtonClick( + this.menu.containerId, + input ? PatternEditorMenu.BUTTON_INPUT_MOD_FILTER_MODE : PatternEditorMenu.BUTTON_OUTPUT_MOD_FILTER_MODE); + } + } + + private Component modeShort(int mode) { + return Component.translatable(mode == PatternEditorMenu.FILTER_MODE_WHITELIST + ? "gui.ae2sps.mod_filter.mode_short.whitelist" + : "gui.ae2sps.mod_filter.mode_short.blacklist"); + } + + private Component modeHint(boolean input) { + return Component.translatable( + input ? "gui.ae2sps.mod_filter.input_mode" : "gui.ae2sps.mod_filter.output_mode", + Component.translatable((input ? this.menu.getInputModFilterMode() : this.menu.getOutputModFilterMode()) + == PatternEditorMenu.FILTER_MODE_WHITELIST + ? "gui.ae2sps.mode.whitelist" + : "gui.ae2sps.mode.blacklist")); + } + private void openSearch() { if (this.minecraft != null) { this.minecraft.setScreen(new PatternSearchScreen(this.parent, this.menu)); @@ -216,16 +251,28 @@ public void render(@NotNull GuiGraphics guiGraphics, int mouseX, int mouseY, flo guiGraphics.blit(TEXTURE, this.leftPos, this.topPos, 0, 0, PANEL_WIDTH, PANEL_HEIGHT, ATLAS_WIDTH, ATLAS_HEIGHT); guiGraphics.drawString(this.font, this.title, this.leftPos + 16, this.topPos + 16, 0x404040, false); + ModFilterRule inputRule = this.menu.getInputModFilterRule(); + ModFilterRule outputRule = this.menu.getOutputModFilterRule(); Component stat = Component.translatable( "gui.ae2sps.mod_filter.stats", - this.excludedInputMods.size(), - this.excludedOutputMods.size(), + modeShort(this.menu.getInputModFilterMode()), + this.inputModIds.size(), + modeShort(this.menu.getOutputModFilterMode()), + this.outputModIds.size(), this.allEntries.size()); guiGraphics.drawString(this.font, stat, this.leftPos + 16, this.topPos + 45, 0x666666, false); drawEntryRows(guiGraphics, mouseX, mouseY); drawScrollBar(guiGraphics); + if (this.inputModeButton != null) { + this.inputModeButton.setHint(modeHint(true)); + } + if (this.outputModeButton != null) { + this.outputModeButton.setHint(modeHint(false)); + } super.render(guiGraphics, mouseX, mouseY, partialTick); + guiGraphics.drawString(this.font, "I", this.leftPos + SIDE_BUTTON_X + 8, this.topPos + INPUT_MODE_BUTTON_Y + 8, 0x404040, false); + guiGraphics.drawString(this.font, "O", this.leftPos + SIDE_BUTTON_X + 8, this.topPos + OUTPUT_MODE_BUTTON_Y + 8, 0x404040, false); ModEntry hovered = getEntryAt(mouseX, mouseY); if (hovered != null) { @@ -233,12 +280,12 @@ public void render(@NotNull GuiGraphics guiGraphics, int mouseX, int mouseY, flo tooltip.add(Component.literal(hovered.modId() + " - " + hovered.name())); tooltip.add(Component.translatable("gui.ae2sps.mod_filter.tooltip.left")); tooltip.add(Component.translatable("gui.ae2sps.mod_filter.tooltip.right")); - Component inputState = Component.translatable(this.excludedInputMods.contains(hovered.modId()) - ? "gui.ae2sps.mod_filter.state.excluded" - : "gui.ae2sps.mod_filter.state.allowed"); - Component outputState = Component.translatable(this.excludedOutputMods.contains(hovered.modId()) - ? "gui.ae2sps.mod_filter.state.excluded" - : "gui.ae2sps.mod_filter.state.allowed"); + Component inputState = Component.translatable(inputRule.allows(hovered.modId()) + ? "gui.ae2sps.mod_filter.state.allowed" + : "gui.ae2sps.mod_filter.state.excluded"); + Component outputState = Component.translatable(outputRule.allows(hovered.modId()) + ? "gui.ae2sps.mod_filter.state.allowed" + : "gui.ae2sps.mod_filter.state.excluded"); tooltip.add(Component.translatable("gui.ae2sps.mod_filter.tooltip.state", inputState, outputState)); guiGraphics.renderTooltip(this.font, tooltip.stream().map(Component::getVisualOrderText).toList(), mouseX, mouseY); return; @@ -283,24 +330,26 @@ private void applyFilter() { } private void toggleInput(String modId) { - if (!this.excludedInputMods.add(modId)) { - this.excludedInputMods.remove(modId); + if (!this.inputModIds.add(modId)) { + this.inputModIds.remove(modId); } - syncExcludedMods(); + syncModFilters(); } private void toggleOutput(String modId) { - if (!this.excludedOutputMods.add(modId)) { - this.excludedOutputMods.remove(modId); + if (!this.outputModIds.add(modId)) { + this.outputModIds.remove(modId); } - syncExcludedMods(); + syncModFilters(); } - private void syncExcludedMods() { - List inputValues = new ArrayList<>(this.excludedInputMods); - List outputValues = new ArrayList<>(this.excludedOutputMods); - this.menu.applyExcludedModFilters(inputValues.toArray(new String[0]), outputValues.toArray(new String[0])); - TechStartNetwork.sendToServer(new SetPatternModFiltersPayload(inputValues, outputValues)); + private void syncModFilters() { + List inputValues = new ArrayList<>(this.inputModIds); + List outputValues = new ArrayList<>(this.outputModIds); + int inputMode = this.menu.getInputModFilterMode(); + int outputMode = this.menu.getOutputModFilterMode(); + this.menu.applyModFilters(inputMode, inputValues, outputMode, outputValues); + TechStartNetwork.sendToServer(new SetPatternModFiltersPayload(inputMode, inputValues, outputMode, outputValues)); } @@ -316,25 +365,35 @@ private void drawEntryRows(GuiGraphics guiGraphics, int mouseX, int mouseY) { int rowY = top + row * ROW_HEIGHT; boolean hovered = mouseX >= left && mouseX < left + LIST_WIDTH && mouseY >= rowY && mouseY < rowY + ROW_HEIGHT; - boolean inputExcluded = this.excludedInputMods.contains(entry.modId()); - boolean outputExcluded = this.excludedOutputMods.contains(entry.modId()); - if (inputExcluded && outputExcluded) { + boolean inputAllowed = this.menu.getInputModFilterRule().allows(entry.modId()); + boolean outputAllowed = this.menu.getOutputModFilterRule().allows(entry.modId()); + if (!inputAllowed && !outputAllowed) { guiGraphics.fill(left, rowY, left + LIST_WIDTH, rowY + ROW_HEIGHT, 0x446633CC); - } else if (inputExcluded) { + } else if (!inputAllowed) { guiGraphics.fill(left, rowY, left + LIST_WIDTH, rowY + ROW_HEIGHT, 0x44CC3333); - } else if (outputExcluded) { + } else if (!outputAllowed) { guiGraphics.fill(left, rowY, left + LIST_WIDTH, rowY + ROW_HEIGHT, 0x443366CC); } if (hovered) { guiGraphics.fill(left, rowY, left + LIST_WIDTH, rowY + ROW_HEIGHT, 0x33FFFFFF); } - String marker = (inputExcluded ? "I" : "-") + "/" + (outputExcluded ? "O" : "-"); + String marker = "I" + (inputAllowed ? "+" : "-") + "/O" + (outputAllowed ? "+" : "-"); String line = this.font.plainSubstrByWidth(marker + " " + entry.modId() + " - " + entry.name(), LIST_WIDTH - 4); guiGraphics.drawString(this.font, line, left + 2, rowY + 4, 0x404040, false); } } + private int countDisallowed(ModFilterRule rule) { + int blocked = 0; + for (ModEntry entry : this.allEntries) { + if (!rule.allows(entry.modId())) { + blocked++; + } + } + return blocked; + } + private void drawScrollBar(GuiGraphics guiGraphics) { int barLeft = getScrollBarLeft(); int barTop = getListTop(); @@ -425,7 +484,7 @@ private void closeToParent() { } private static String normalizeModId(String modId) { - return modId == null ? "" : modId.trim().toLowerCase(Locale.ROOT); + return ModFilterRule.normalizeModId(modId); } private record ModEntry(String modId, String name) { diff --git a/versions/neoforge-1.21.1/src/main/java/com/ae2smartpatternsystem/content/PatternIntegrationsItem.java b/versions/neoforge-1.21.1/src/main/java/com/ae2smartpatternsystem/content/PatternIntegrationsItem.java index edbd5df..0bcffa1 100644 --- a/versions/neoforge-1.21.1/src/main/java/com/ae2smartpatternsystem/content/PatternIntegrationsItem.java +++ b/versions/neoforge-1.21.1/src/main/java/com/ae2smartpatternsystem/content/PatternIntegrationsItem.java @@ -1,6 +1,9 @@ package com.ae2smartpatternsystem.content; import com.ae2smartpatternsystem.integration.mekanism.MekanismGasHelper; +import com.ae2smartpatternsystem.core.codec.PatternNbtKeys; +import com.ae2smartpatternsystem.core.model.FilterMode; +import com.ae2smartpatternsystem.core.model.ModFilterRule; import net.minecraft.ChatFormatting; import net.minecraft.core.HolderLookup; import net.minecraft.core.component.DataComponents; @@ -25,6 +28,7 @@ import com.ae2smartpatternsystem.menu.PatternEditorMenu; import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; public class PatternIntegrationsItem extends Item { @@ -43,8 +47,6 @@ public class PatternIntegrationsItem extends Item { private static final String TAG_INPUT_GAS_AMOUNTS = "InputGasAmounts"; private static final String TAG_OUTPUT_GASES = "OutputGases"; private static final String TAG_OUTPUT_GAS_AMOUNTS = "OutputGasAmounts"; - private static final String TAG_EXCLUDED_INPUT_MOD_IDS = "ExcludedInputModIds"; - private static final String TAG_EXCLUDED_OUTPUT_MOD_IDS = "ExcludedOutputModIds"; private static final String TAG_FLUID_MARKER = "TechStartFluidMarker"; private static final String TAG_FLUID_NAME = "TechStartFluidName"; private static final String TAG_FLUID_AMOUNT = "TechStartFluidAmount"; @@ -103,6 +105,11 @@ public void appendHoverText(ItemStack stack, TooltipContext context, List inputLines = buildEntryLines(tag, TAG_INPUTS, true, registries); @@ -137,8 +144,12 @@ private void clearPatternData(ItemStack stack) { tag.remove(TAG_INPUT_GAS_AMOUNTS); tag.remove(TAG_OUTPUT_GASES); tag.remove(TAG_OUTPUT_GAS_AMOUNTS); - tag.remove(TAG_EXCLUDED_INPUT_MOD_IDS); - tag.remove(TAG_EXCLUDED_OUTPUT_MOD_IDS); + tag.remove(PatternNbtKeys.TAG_INPUT_MOD_FILTER_MODE); + tag.remove(PatternNbtKeys.TAG_OUTPUT_MOD_FILTER_MODE); + tag.remove(PatternNbtKeys.TAG_INPUT_MOD_FILTER_IDS); + tag.remove(PatternNbtKeys.TAG_OUTPUT_MOD_FILTER_IDS); + tag.remove(PatternNbtKeys.TAG_EXCLUDED_INPUT_MOD_IDS); + tag.remove(PatternNbtKeys.TAG_EXCLUDED_OUTPUT_MOD_IDS); CustomData.set(DataComponents.CUSTOM_DATA, stack, tag); } @@ -152,6 +163,42 @@ private int readFilterMode(CompoundTag tag) { return FILTER_MODE_BLACKLIST; } + private Component formatModFilterTooltip(String key, ModFilterRule rule) { + Component mode = Component.translatable(rule.mode() == FilterMode.WHITELIST + ? "tooltip.ae2sps.filter_mode_whitelist" + : "tooltip.ae2sps.filter_mode_blacklist"); + return Component.translatable(key, mode, rule.modIds().size()) + .withStyle(rule.mode() == FilterMode.WHITELIST ? ChatFormatting.AQUA : ChatFormatting.GOLD); + } + + private ModFilterRule readModFilterRule(CompoundTag tag, boolean input) { + String modeKey = input ? PatternNbtKeys.TAG_INPUT_MOD_FILTER_MODE : PatternNbtKeys.TAG_OUTPUT_MOD_FILTER_MODE; + String idsKey = input ? PatternNbtKeys.TAG_INPUT_MOD_FILTER_IDS : PatternNbtKeys.TAG_OUTPUT_MOD_FILTER_IDS; + String legacyKey = input ? PatternNbtKeys.TAG_EXCLUDED_INPUT_MOD_IDS : PatternNbtKeys.TAG_EXCLUDED_OUTPUT_MOD_IDS; + boolean canonicalIdsPresent = tag.contains(idsKey, Tag.TAG_LIST); + Integer serializedMode = tag.contains(modeKey, Tag.TAG_INT) ? tag.getInt(modeKey) : null; + return ModFilterRule.fromStoredData( + serializedMode, + canonicalIdsPresent, + readModFilterIds(tag, idsKey), + readModFilterIds(tag, legacyKey)); + } + + private List readModFilterIds(CompoundTag tag, String key) { + if (!tag.contains(key, Tag.TAG_LIST)) { + return List.of(); + } + ListTag ids = tag.getList(key, Tag.TAG_STRING); + LinkedHashSet values = new LinkedHashSet<>(); + for (int i = 0; i < ids.size() && values.size() < PatternEditorMenu.MAX_MOD_FILTER_IDS; i++) { + String normalized = ModFilterRule.normalizeModId(ids.getString(i)); + if (!normalized.isBlank() && normalized.length() <= PatternEditorMenu.MAX_MOD_FILTER_ID_LENGTH) { + values.add(normalized); + } + } + return new ArrayList<>(values); + } + private List buildEntryLines(CompoundTag tag, String listKey, boolean input, HolderLookup.Provider registries) { if (!tag.contains(listKey, Tag.TAG_LIST)) { return List.of(); diff --git a/versions/neoforge-1.21.1/src/main/java/com/ae2smartpatternsystem/integration/ae2/TechStartPatternExpansion.java b/versions/neoforge-1.21.1/src/main/java/com/ae2smartpatternsystem/integration/ae2/TechStartPatternExpansion.java index bf4b13f..ab4a5e2 100644 --- a/versions/neoforge-1.21.1/src/main/java/com/ae2smartpatternsystem/integration/ae2/TechStartPatternExpansion.java +++ b/versions/neoforge-1.21.1/src/main/java/com/ae2smartpatternsystem/integration/ae2/TechStartPatternExpansion.java @@ -3,7 +3,10 @@ import appeng.api.crafting.IPatternDetails; import appeng.api.stacks.AEItemKey; import com.ae2smartpatternsystem.core.PatternDefinitionBridge; +import com.ae2smartpatternsystem.core.codec.PatternNbtKeys; import com.ae2smartpatternsystem.core.model.EntryKind; +import com.ae2smartpatternsystem.core.model.FilterMode; +import com.ae2smartpatternsystem.core.model.ModFilterRule; import com.ae2smartpatternsystem.core.model.PatternDefinition; import com.ae2smartpatternsystem.core.model.PatternEntry; import com.ae2smartpatternsystem.core.model.WildcardRecipe; @@ -39,14 +42,14 @@ public final class TechStartPatternExpansion { private static final String TAG_FILTER_MODE = "TechStartFilterMode"; private static final String TAG_FILTER_MODE_LEGACY = "FilterMode"; private static final String TAG_FILTER_ENTRIES = "FilterEntries"; - private static final String TAG_EXCLUDED_INPUT_MOD_IDS = "ExcludedInputModIds"; - private static final String TAG_EXCLUDED_OUTPUT_MOD_IDS = "ExcludedOutputModIds"; private static final String TAG_VIRTUAL_INPUT_STACKS = "VirtualInputStacks"; private static final String TAG_VIRTUAL_OUTPUT_STACKS = "VirtualOutputStacks"; private static final String TAG_VIRTUAL_DISPLAY_NAME = "VirtualDisplayName"; private static final String TAG_VIRTUAL_FILTER_ENTRY_ID = "VirtualFilterEntryId"; private static final String TAG_ITEM_MARKER = "TechStartItemMarker"; private static final String TAG_ITEM_AMOUNT = "TechStartItemAmount"; + private static final int MAX_MOD_FILTER_IDS = 512; + private static final int MAX_MOD_FILTER_ID_LENGTH = 64; private static final WildcardRecipeResolver WILDCARD_RESOLVER = new WildcardRecipeResolver(WildcardRuleConfig.defaults()); private static final Map TAG_PREFIXES = Map.ofEntries( Map.entry("ingots", "ingot"), @@ -414,21 +417,36 @@ private static boolean isStackAllowedByModFilter(CompoundTag tag, ItemStack stac return true; } ResourceLocation key = BuiltInRegistries.ITEM.getKey(stack.getItem()); - if (key == null || key.getNamespace().isBlank()) { - return true; - } - ListTag excluded = tag.getList(input ? TAG_EXCLUDED_INPUT_MOD_IDS : TAG_EXCLUDED_OUTPUT_MOD_IDS, Tag.TAG_STRING); - if (excluded.isEmpty()) { - return true; + String namespace = key == null ? "" : key.getNamespace(); + return readModFilterRule(tag, input).allows(namespace); + } + + private static ModFilterRule readModFilterRule(CompoundTag tag, boolean input) { + String modeKey = input ? PatternNbtKeys.TAG_INPUT_MOD_FILTER_MODE : PatternNbtKeys.TAG_OUTPUT_MOD_FILTER_MODE; + String idsKey = input ? PatternNbtKeys.TAG_INPUT_MOD_FILTER_IDS : PatternNbtKeys.TAG_OUTPUT_MOD_FILTER_IDS; + String legacyKey = input ? PatternNbtKeys.TAG_EXCLUDED_INPUT_MOD_IDS : PatternNbtKeys.TAG_EXCLUDED_OUTPUT_MOD_IDS; + boolean canonicalIdsPresent = tag.contains(idsKey, Tag.TAG_LIST); + Integer serializedMode = tag.contains(modeKey, Tag.TAG_INT) ? tag.getInt(modeKey) : null; + return ModFilterRule.fromStoredData( + serializedMode, + canonicalIdsPresent, + readModFilterIds(tag, idsKey), + readModFilterIds(tag, legacyKey)); + } + + private static List readModFilterIds(CompoundTag tag, String key) { + if (!tag.contains(key, Tag.TAG_LIST)) { + return List.of(); } - String namespace = key.getNamespace().toLowerCase(java.util.Locale.ROOT); - for (int i = 0; i < excluded.size(); i++) { - String blocked = excluded.getString(i); - if (blocked != null && namespace.equals(blocked.trim().toLowerCase(java.util.Locale.ROOT))) { - return false; + ListTag ids = tag.getList(key, Tag.TAG_STRING); + Set values = new LinkedHashSet<>(); + for (int i = 0; i < ids.size() && values.size() < MAX_MOD_FILTER_IDS; i++) { + String normalized = ModFilterRule.normalizeModId(ids.getString(i)); + if (!normalized.isBlank() && normalized.length() <= MAX_MOD_FILTER_ID_LENGTH) { + values.add(normalized); } } - return true; + return new ArrayList<>(values); } private static String getRegistryNamespace(ItemStack stack) { diff --git a/versions/neoforge-1.21.1/src/main/java/com/ae2smartpatternsystem/menu/PatternEditorMenu.java b/versions/neoforge-1.21.1/src/main/java/com/ae2smartpatternsystem/menu/PatternEditorMenu.java index 857bcd6..0ff416b 100644 --- a/versions/neoforge-1.21.1/src/main/java/com/ae2smartpatternsystem/menu/PatternEditorMenu.java +++ b/versions/neoforge-1.21.1/src/main/java/com/ae2smartpatternsystem/menu/PatternEditorMenu.java @@ -2,6 +2,9 @@ import com.ae2smartpatternsystem.config.TechStartConfig; import com.ae2smartpatternsystem.TechStartNeoForge; +import com.ae2smartpatternsystem.core.codec.PatternNbtKeys; +import com.ae2smartpatternsystem.core.model.FilterMode; +import com.ae2smartpatternsystem.core.model.ModFilterRule; import com.ae2smartpatternsystem.integration.ae2.TechStartPatternExpansion; import com.ae2smartpatternsystem.integration.mekanism.MekanismGasHelper; import com.ae2smartpatternsystem.registry.TechStartItems; @@ -44,6 +47,7 @@ import org.jetbrains.annotations.Nullable; import java.util.ArrayList; +import java.util.Collection; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; @@ -73,8 +77,6 @@ public class PatternEditorMenu extends AbstractContainerMenu { private static final String TAG_INPUT_GAS_AMOUNTS = "InputGasAmounts"; private static final String TAG_OUTPUT_GASES = "OutputGases"; private static final String TAG_OUTPUT_GAS_AMOUNTS = "OutputGasAmounts"; - private static final String TAG_EXCLUDED_INPUT_MOD_IDS = "ExcludedInputModIds"; - private static final String TAG_EXCLUDED_OUTPUT_MOD_IDS = "ExcludedOutputModIds"; private static final String TAG_ITEM_MARKER = "TechStartItemMarker"; private static final String TAG_ITEM_AMOUNT = "TechStartItemAmount"; private static final String TAG_FILTER_MODE = "TechStartFilterMode"; @@ -90,6 +92,10 @@ public class PatternEditorMenu extends AbstractContainerMenu { private static final int OUTPUT_SLOTS = 9; private static final int TOTAL_PATTERN_SLOTS = INPUT_SLOTS + OUTPUT_SLOTS; private static final int BUTTON_FILTER_ENTRY_BASE = 1000; + public static final int BUTTON_INPUT_MOD_FILTER_MODE = 2000; + public static final int BUTTON_OUTPUT_MOD_FILTER_MODE = 2001; + public static final int MAX_MOD_FILTER_IDS = 512; + public static final int MAX_MOD_FILTER_ID_LENGTH = 64; public static final int FILTER_MODE_WHITELIST = 0; public static final int FILTER_MODE_BLACKLIST = 1; @@ -99,10 +105,10 @@ public class PatternEditorMenu extends AbstractContainerMenu { private final Player player; private final @Nullable InteractionHand boundPatternHand; private final ItemStack boundPatternStack; - private final ContainerData data = new SimpleContainerData(1); + private final ContainerData data = new SimpleContainerData(3); private final LinkedHashSet filterEntries = new LinkedHashSet<>(); - private final LinkedHashSet excludedInputModIds = new LinkedHashSet<>(); - private final LinkedHashSet excludedOutputModIds = new LinkedHashSet<>(); + private final LinkedHashSet inputModFilterIds = new LinkedHashSet<>(); + private final LinkedHashSet outputModFilterIds = new LinkedHashSet<>(); public static class PatternSlot extends SlotItemHandler { private boolean active = true; @@ -133,6 +139,8 @@ public PatternEditorMenu(int containerId, Inventory playerInventory, Interaction this.boundPatternHand = hand; this.boundPatternStack = locateBoundPatternStack(hand); this.data.set(0, FILTER_MODE_BLACKLIST); + this.data.set(1, FILTER_MODE_BLACKLIST); + this.data.set(2, FILTER_MODE_BLACKLIST); this.addDataSlots(this.data); for (int row = 0; row < 3; row++) { @@ -221,6 +229,20 @@ public boolean clickMenuButton(@NotNull Player player, int id) { saveToPatternItem(); return true; } + if (id == BUTTON_INPUT_MOD_FILTER_MODE) { + setInputModFilterMode(getInputModFilterMode() == FILTER_MODE_WHITELIST + ? FILTER_MODE_BLACKLIST + : FILTER_MODE_WHITELIST); + saveToPatternItem(); + return true; + } + if (id == BUTTON_OUTPUT_MOD_FILTER_MODE) { + setOutputModFilterMode(getOutputModFilterMode() == FILTER_MODE_WHITELIST + ? FILTER_MODE_BLACKLIST + : FILTER_MODE_WHITELIST); + saveToPatternItem(); + return true; + } if (id >= BUTTON_FILTER_ENTRY_BASE) { int entryIndex = id - BUTTON_FILTER_ENTRY_BASE; toggleFilterEntryByIndex(entryIndex); @@ -255,12 +277,28 @@ public List getFilterEntriesSnapshot() { return List.copyOf(this.filterEntries); } - public List getExcludedInputModIdsSnapshot() { - return List.copyOf(this.excludedInputModIds); + public List getInputModFilterIdsSnapshot() { + return List.copyOf(this.inputModFilterIds); } - public List getExcludedOutputModIdsSnapshot() { - return List.copyOf(this.excludedOutputModIds); + public List getOutputModFilterIdsSnapshot() { + return List.copyOf(this.outputModFilterIds); + } + + public int getInputModFilterMode() { + return normalizeFilterMode(this.data.get(1)); + } + + public int getOutputModFilterMode() { + return normalizeFilterMode(this.data.get(2)); + } + + public ModFilterRule getInputModFilterRule() { + return ModFilterRule.of(FilterMode.fromSerializedValue(getInputModFilterMode()), this.inputModFilterIds); + } + + public ModFilterRule getOutputModFilterRule() { + return ModFilterRule.of(FilterMode.fromSerializedValue(getOutputModFilterMode()), this.outputModFilterIds); } public ItemStack getPatternStackSnapshot() { @@ -288,16 +326,21 @@ public void applyPatternSlotAmountFromClient(int slotId, int amount, Player play onPatternSlotsMutated(player); } - public void applyExcludedModFilters(String[] inputValues, String[] outputValues) { - this.excludedInputModIds.clear(); - this.excludedInputModIds.addAll(normalizeModIds(inputValues)); - this.excludedOutputModIds.clear(); - this.excludedOutputModIds.addAll(normalizeModIds(outputValues)); + public void applyModFilters(int inputMode, Collection inputValues, int outputMode, Collection outputValues) { + setInputModFilterMode(inputMode); + setOutputModFilterMode(outputMode); + this.inputModFilterIds.clear(); + this.inputModFilterIds.addAll(normalizeModIds(inputValues)); + this.outputModFilterIds.clear(); + this.outputModFilterIds.addAll(normalizeModIds(outputValues)); saveToPatternItem(); } - public void applyExcludedModFiltersFromClient(String[] inputValues, String[] outputValues, Player actor) { - applyExcludedModFilters(inputValues, outputValues); + public void applyModFiltersFromClient(int inputMode, Collection inputValues, int outputMode, Collection outputValues, Player actor) { + if (!isValidFilterMode(inputMode) || !isValidFilterMode(outputMode)) { + return; + } + applyModFilters(inputMode, inputValues, outputMode, outputValues); if (!actor.level().isClientSide) { this.broadcastChanges(); if (actor instanceof ServerPlayer serverPlayer) { @@ -420,9 +463,11 @@ private void loadFromPatternItem() { private void loadFilterStateFromPatternItem() { this.filterEntries.clear(); - this.excludedInputModIds.clear(); - this.excludedOutputModIds.clear(); + this.inputModFilterIds.clear(); + this.outputModFilterIds.clear(); setFilterMode(FILTER_MODE_BLACKLIST); + setInputModFilterMode(FILTER_MODE_BLACKLIST); + setOutputModFilterMode(FILTER_MODE_BLACKLIST); ItemStack patternStack = resolvePatternStack(); if (patternStack.isEmpty()) { return; @@ -433,9 +478,13 @@ private void loadFilterStateFromPatternItem() { return; } setFilterMode(readFilterMode(tag)); + ModFilterRule inputRule = readModFilterRule(tag, true); + ModFilterRule outputRule = readModFilterRule(tag, false); + setInputModFilterMode(inputRule.mode().serializedValue()); + setOutputModFilterMode(outputRule.mode().serializedValue()); + this.inputModFilterIds.addAll(inputRule.modIds()); + this.outputModFilterIds.addAll(outputRule.modIds()); readFilterEntries(tag); - readExcludedModIds(tag, true); - readExcludedModIds(tag, false); } private void readSlotList(ListTag listTag, int baseSlot, int maxSlots) { @@ -486,8 +535,8 @@ private void saveToPatternItem() { CompoundTag tag = patternStack.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY).copyTag(); tag.putInt(TAG_FILTER_MODE, getFilterMode()); tag.putBoolean(TAG_ENCODED, encoded); - writeExcludedModIds(tag, true); - writeExcludedModIds(tag, false); + writeModFilterRule(tag, true); + writeModFilterRule(tag, false); if (encoded) { tag.put(TAG_INPUTS, inputs); tag.put(TAG_OUTPUTS, outputs); @@ -534,10 +583,22 @@ private void setFilterMode(int mode) { this.data.set(0, normalizeFilterMode(mode)); } + private void setInputModFilterMode(int mode) { + this.data.set(1, normalizeFilterMode(mode)); + } + + private void setOutputModFilterMode(int mode) { + this.data.set(2, normalizeFilterMode(mode)); + } + private int normalizeFilterMode(int mode) { return mode == FILTER_MODE_WHITELIST ? FILTER_MODE_WHITELIST : FILTER_MODE_BLACKLIST; } + private boolean isValidFilterMode(int mode) { + return mode == FILTER_MODE_WHITELIST || mode == FILTER_MODE_BLACKLIST; + } + private int readFilterMode(CompoundTag tag) { if (tag.contains(TAG_FILTER_MODE, Tag.TAG_INT)) { return normalizeFilterMode(tag.getInt(TAG_FILTER_MODE)); @@ -561,19 +622,32 @@ private void readFilterEntries(CompoundTag tag) { } } - private void readExcludedModIds(CompoundTag tag, boolean input) { - String key = input ? TAG_EXCLUDED_INPUT_MOD_IDS : TAG_EXCLUDED_OUTPUT_MOD_IDS; + private ModFilterRule readModFilterRule(CompoundTag tag, boolean input) { + String modeKey = input ? PatternNbtKeys.TAG_INPUT_MOD_FILTER_MODE : PatternNbtKeys.TAG_OUTPUT_MOD_FILTER_MODE; + String idsKey = input ? PatternNbtKeys.TAG_INPUT_MOD_FILTER_IDS : PatternNbtKeys.TAG_OUTPUT_MOD_FILTER_IDS; + String legacyKey = input ? PatternNbtKeys.TAG_EXCLUDED_INPUT_MOD_IDS : PatternNbtKeys.TAG_EXCLUDED_OUTPUT_MOD_IDS; + boolean canonicalIdsPresent = tag.contains(idsKey, Tag.TAG_LIST); + Integer serializedMode = tag.contains(modeKey, Tag.TAG_INT) ? tag.getInt(modeKey) : null; + return ModFilterRule.fromStoredData( + serializedMode, + canonicalIdsPresent, + readModFilterIds(tag, idsKey), + readModFilterIds(tag, legacyKey)); + } + + private List readModFilterIds(CompoundTag tag, String key) { if (!tag.contains(key, Tag.TAG_LIST)) { - return; + return List.of(); } ListTag list = tag.getList(key, Tag.TAG_STRING); - LinkedHashSet target = input ? this.excludedInputModIds : this.excludedOutputModIds; - for (int index = 0; index < list.size(); index++) { - String value = normalizeModId(list.getString(index)); - if (!value.isBlank()) { - target.add(value); + List values = new ArrayList<>(Math.min(MAX_MOD_FILTER_IDS, list.size())); + for (int index = 0; index < list.size() && values.size() < MAX_MOD_FILTER_IDS; index++) { + String value = list.getString(index); + if (value != null && value.length() <= MAX_MOD_FILTER_ID_LENGTH) { + values.add(value); } } + return normalizeModIds(values); } private void writeFilterEntries(CompoundTag tag) { @@ -594,24 +668,20 @@ private void writeFilterEntries(CompoundTag tag) { } } - private void writeExcludedModIds(CompoundTag tag, boolean input) { - LinkedHashSet source = input ? this.excludedInputModIds : this.excludedOutputModIds; - String key = input ? TAG_EXCLUDED_INPUT_MOD_IDS : TAG_EXCLUDED_OUTPUT_MOD_IDS; - if (source.isEmpty()) { - tag.remove(key); - return; - } + private void writeModFilterRule(CompoundTag tag, boolean input) { + LinkedHashSet source = input ? this.inputModFilterIds : this.outputModFilterIds; + String modeKey = input ? PatternNbtKeys.TAG_INPUT_MOD_FILTER_MODE : PatternNbtKeys.TAG_OUTPUT_MOD_FILTER_MODE; + String idsKey = input ? PatternNbtKeys.TAG_INPUT_MOD_FILTER_IDS : PatternNbtKeys.TAG_OUTPUT_MOD_FILTER_IDS; + String legacyKey = input ? PatternNbtKeys.TAG_EXCLUDED_INPUT_MOD_IDS : PatternNbtKeys.TAG_EXCLUDED_OUTPUT_MOD_IDS; + tag.putInt(modeKey, input ? getInputModFilterMode() : getOutputModFilterMode()); ListTag list = new ListTag(); for (String modId : source) { if (modId != null && !modId.isBlank()) { list.add(StringTag.valueOf(modId)); } } - if (list.isEmpty()) { - tag.remove(key); - } else { - tag.put(key, list); - } + tag.put(idsKey, list); + tag.remove(legacyKey); } private void toggleFilterEntryByIndex(int entryIndex) { @@ -1001,24 +1071,23 @@ private static String buildStackDescriptor(ItemStack stack) { return id + "|" + stack.getCount() + "|" + stack.getComponentsPatch(); } - private static List normalizeModIds(String[] values) { + private static List normalizeModIds(Collection values) { LinkedHashSet normalized = new LinkedHashSet<>(); if (values == null) { return List.of(); } for (String value : values) { - String modId = normalizeModId(value); - if (!modId.isBlank()) { + if (normalized.size() >= MAX_MOD_FILTER_IDS) { + break; + } + String modId = ModFilterRule.normalizeModId(value); + if (!modId.isBlank() && modId.length() <= MAX_MOD_FILTER_ID_LENGTH) { normalized.add(modId); } } return new ArrayList<>(normalized); } - private static String normalizeModId(String modId) { - return modId == null ? "" : modId.trim().toLowerCase(Locale.ROOT); - } - private boolean isItemMarkerStack(ItemStack stack) { if (stack.isEmpty()) { return false; diff --git a/versions/neoforge-1.21.1/src/main/java/com/ae2smartpatternsystem/network/SetPatternModFiltersPayload.java b/versions/neoforge-1.21.1/src/main/java/com/ae2smartpatternsystem/network/SetPatternModFiltersPayload.java index 6ffe0a3..9cbf3dd 100644 --- a/versions/neoforge-1.21.1/src/main/java/com/ae2smartpatternsystem/network/SetPatternModFiltersPayload.java +++ b/versions/neoforge-1.21.1/src/main/java/com/ae2smartpatternsystem/network/SetPatternModFiltersPayload.java @@ -12,19 +12,29 @@ import java.util.ArrayList; import java.util.List; -public record SetPatternModFiltersPayload(List excludedInputModIds, List excludedOutputModIds) implements CustomPacketPayload { +public record SetPatternModFiltersPayload( + int inputMode, + List inputIds, + int outputMode, + List outputIds) implements CustomPacketPayload { public static final Type TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(TechStartNeoForge.MODID, "set_pattern_mod_filters")); public static final StreamCodec STREAM_CODEC = new StreamCodec<>() { @Override public SetPatternModFiltersPayload decode(RegistryFriendlyByteBuf buf) { - return new SetPatternModFiltersPayload(readStringList(buf), readStringList(buf)); + return new SetPatternModFiltersPayload( + readMode(buf, "input"), + readStringList(buf), + readMode(buf, "output"), + readStringList(buf)); } @Override public void encode(RegistryFriendlyByteBuf buf, SetPatternModFiltersPayload payload) { - writeStringList(buf, payload.excludedInputModIds); - writeStringList(buf, payload.excludedOutputModIds); + writeMode(buf, payload.inputMode, "input"); + writeStringList(buf, payload.inputIds); + writeMode(buf, payload.outputMode, "output"); + writeStringList(buf, payload.outputIds); } }; @@ -37,10 +47,15 @@ public static void handle(SetPatternModFiltersPayload payload, IPayloadContext c if (!(context.player() instanceof ServerPlayer serverPlayer)) { return; } + if (!isValid(payload)) { + return; + } if (serverPlayer.containerMenu instanceof PatternEditorMenu menu) { - menu.applyExcludedModFiltersFromClient( - payload.excludedInputModIds.toArray(new String[0]), - payload.excludedOutputModIds.toArray(new String[0]), + menu.applyModFiltersFromClient( + payload.inputMode, + payload.inputIds, + payload.outputMode, + payload.outputIds, serverPlayer ); } @@ -48,21 +63,73 @@ public static void handle(SetPatternModFiltersPayload payload, IPayloadContext c private static void writeStringList(RegistryFriendlyByteBuf buf, List values) { List source = values == null ? List.of() : values; + if (source.size() > PatternEditorMenu.MAX_MOD_FILTER_IDS) { + throw new IllegalArgumentException("Too many mod filter IDs: " + source.size()); + } buf.writeVarInt(source.size()); for (String value : source) { - buf.writeUtf(value == null ? "" : value); + if (value != null && value.length() > PatternEditorMenu.MAX_MOD_FILTER_ID_LENGTH) { + throw new IllegalArgumentException("Mod filter ID is too long"); + } + buf.writeUtf(value == null ? "" : value, PatternEditorMenu.MAX_MOD_FILTER_ID_LENGTH); } } private static List readStringList(RegistryFriendlyByteBuf buf) { - int size = Math.max(0, buf.readVarInt()); + int size = buf.readVarInt(); + if (size < 0 || size > PatternEditorMenu.MAX_MOD_FILTER_IDS) { + throw new IllegalArgumentException("Invalid mod filter ID count: " + size); + } List values = new ArrayList<>(size); for (int i = 0; i < size; i++) { - String value = buf.readUtf(); + String value = buf.readUtf(PatternEditorMenu.MAX_MOD_FILTER_ID_LENGTH); if (!value.isBlank()) { values.add(value); } } return values; } + + private static int readMode(RegistryFriendlyByteBuf buf, String side) { + int mode = buf.readVarInt(); + if (!isValidMode(mode)) { + throw new IllegalArgumentException("Invalid " + side + " mod filter mode: " + mode); + } + return mode; + } + + private static void writeMode(RegistryFriendlyByteBuf buf, int mode, String side) { + if (!isValidMode(mode)) { + throw new IllegalArgumentException("Invalid " + side + " mod filter mode: " + mode); + } + buf.writeVarInt(mode); + } + + private static boolean isValidMode(int mode) { + return mode == PatternEditorMenu.FILTER_MODE_WHITELIST + || mode == PatternEditorMenu.FILTER_MODE_BLACKLIST; + } + + static boolean isValid(SetPatternModFiltersPayload payload) { + return payload != null + && isValidMode(payload.inputMode) + && isValidMode(payload.outputMode) + && isValidStringList(payload.inputIds) + && isValidStringList(payload.outputIds); + } + + private static boolean isValidStringList(List values) { + if (values == null) { + return true; + } + if (values.size() > PatternEditorMenu.MAX_MOD_FILTER_IDS) { + return false; + } + for (String value : values) { + if (value != null && value.length() > PatternEditorMenu.MAX_MOD_FILTER_ID_LENGTH) { + return false; + } + } + return true; + } } diff --git a/versions/neoforge-1.21.1/src/main/java/com/ae2smartpatternsystem/network/TechStartNetwork.java b/versions/neoforge-1.21.1/src/main/java/com/ae2smartpatternsystem/network/TechStartNetwork.java index 16893b4..38fca43 100644 --- a/versions/neoforge-1.21.1/src/main/java/com/ae2smartpatternsystem/network/TechStartNetwork.java +++ b/versions/neoforge-1.21.1/src/main/java/com/ae2smartpatternsystem/network/TechStartNetwork.java @@ -6,7 +6,7 @@ import net.minecraft.network.protocol.common.custom.CustomPacketPayload; public final class TechStartNetwork { - private static final String PROTOCOL_VERSION = "1"; + private static final String PROTOCOL_VERSION = "2"; private TechStartNetwork() { } diff --git a/versions/neoforge-1.21.1/src/main/resources/assets/sampleintegration/lang/en_us.json b/versions/neoforge-1.21.1/src/main/resources/assets/sampleintegration/lang/en_us.json index 48d491f..b51db3e 100644 --- a/versions/neoforge-1.21.1/src/main/resources/assets/sampleintegration/lang/en_us.json +++ b/versions/neoforge-1.21.1/src/main/resources/assets/sampleintegration/lang/en_us.json @@ -17,14 +17,20 @@ "gui.ae2sps.toggle_filter_mode": "Toggle whitelist/blacklist mode", "gui.ae2sps.open_mod_filter": "Open mod filter", "gui.ae2sps.mod_filter.title": "Mod Filter", - "gui.ae2sps.mod_filter.stats": "Input blocked: %s | Output blocked: %s | Total mods: %s", + "gui.ae2sps.mod_filter.stats": "I %s:%s | O %s:%s | Total:%s", "gui.ae2sps.mod_filter.back": "Back", "gui.ae2sps.mod_filter.clear": "Clear", - "gui.ae2sps.mod_filter.tooltip.left": "Left click: toggle input exclusion", - "gui.ae2sps.mod_filter.tooltip.right": "Right click: toggle output exclusion", + "gui.ae2sps.mod_filter.input_mode": "Input mod filter: %s", + "gui.ae2sps.mod_filter.output_mode": "Output mod filter: %s", + "gui.ae2sps.mod_filter.mode_short.whitelist": "W", + "gui.ae2sps.mod_filter.mode_short.blacklist": "B", + "gui.ae2sps.mod_filter.tooltip.left": "Left click: toggle input membership", + "gui.ae2sps.mod_filter.tooltip.right": "Right click: toggle output membership", "gui.ae2sps.mod_filter.tooltip.state": "State - Input: %s, Output: %s", "gui.ae2sps.mod_filter.state.allowed": "Allowed", - "gui.ae2sps.mod_filter.state.excluded": "Excluded", + "gui.ae2sps.mod_filter.state.excluded": "Blocked", + "gui.ae2sps.mod_filter.toggle_input_mode": "Toggle input whitelist/blacklist mode", + "gui.ae2sps.mod_filter.toggle_output_mode": "Toggle output whitelist/blacklist mode", "gui.ae2sps.open_search": "Open search", "gui.ae2sps.search.title": "Pattern Search", "gui.ae2sps.search.matches": "Matches: %s / %s", @@ -42,6 +48,8 @@ "tooltip.ae2sps.filter_mode": "Filter mode: %s", "tooltip.ae2sps.filter_mode_whitelist": "Whitelist", "tooltip.ae2sps.filter_mode_blacklist": "Blacklist", + "tooltip.ae2sps.input_mod_filter": "Input mods: %s (%s IDs)", + "tooltip.ae2sps.output_mod_filter": "Output mods: %s (%s IDs)", "tooltip.ae2sps.bool_yes": "Yes", "tooltip.ae2sps.bool_no": "No", "tooltip.ae2sps.input_count": "Input entries: %s", diff --git a/versions/neoforge-1.21.1/src/main/resources/assets/sampleintegration/lang/zh_cn.json b/versions/neoforge-1.21.1/src/main/resources/assets/sampleintegration/lang/zh_cn.json index 41a0a2e..856d631 100644 --- a/versions/neoforge-1.21.1/src/main/resources/assets/sampleintegration/lang/zh_cn.json +++ b/versions/neoforge-1.21.1/src/main/resources/assets/sampleintegration/lang/zh_cn.json @@ -17,14 +17,20 @@ "gui.ae2sps.toggle_filter_mode": "\u5207\u6362\u9ed1\u767d\u540d\u5355\u6a21\u5f0f", "gui.ae2sps.open_mod_filter": "\u6253\u5f00\u6a21\u7ec4\u7b5b\u9009", "gui.ae2sps.mod_filter.title": "\u6a21\u7ec4\u7b5b\u9009", - "gui.ae2sps.mod_filter.stats": "\u8f93\u5165\u5c4f\u853d\uff1a%s | \u8f93\u51fa\u5c4f\u853d\uff1a%s | \u6a21\u7ec4\u603b\u6570\uff1a%s", + "gui.ae2sps.mod_filter.stats": "\u5165 %s:%s | \u51fa %s:%s | \u603b:%s", "gui.ae2sps.mod_filter.back": "\u8fd4\u56de", "gui.ae2sps.mod_filter.clear": "\u6e05\u7a7a", - "gui.ae2sps.mod_filter.tooltip.left": "\u5de6\u952e\uff1a\u5207\u6362\u8f93\u5165\u5c4f\u853d", - "gui.ae2sps.mod_filter.tooltip.right": "\u53f3\u952e\uff1a\u5207\u6362\u8f93\u51fa\u5c4f\u853d", + "gui.ae2sps.mod_filter.input_mode": "\u8f93\u5165\u6a21\u7ec4\u7b5b\u9009\uff1a%s", + "gui.ae2sps.mod_filter.output_mode": "\u8f93\u51fa\u6a21\u7ec4\u7b5b\u9009\uff1a%s", + "gui.ae2sps.mod_filter.mode_short.whitelist": "\u767d", + "gui.ae2sps.mod_filter.mode_short.blacklist": "\u9ed1", + "gui.ae2sps.mod_filter.tooltip.left": "\u5de6\u952e\uff1a\u5207\u6362\u8f93\u5165\u96c6\u5408\u6210\u5458", + "gui.ae2sps.mod_filter.tooltip.right": "\u53f3\u952e\uff1a\u5207\u6362\u8f93\u51fa\u96c6\u5408\u6210\u5458", "gui.ae2sps.mod_filter.tooltip.state": "\u72b6\u6001 - \u8f93\u5165\uff1a%s\uff0c\u8f93\u51fa\uff1a%s", "gui.ae2sps.mod_filter.state.allowed": "\u5141\u8bb8", "gui.ae2sps.mod_filter.state.excluded": "\u5c4f\u853d", + "gui.ae2sps.mod_filter.toggle_input_mode": "\u5207\u6362\u8f93\u5165\u767d/\u9ed1\u540d\u5355\u6a21\u5f0f", + "gui.ae2sps.mod_filter.toggle_output_mode": "\u5207\u6362\u8f93\u51fa\u767d/\u9ed1\u540d\u5355\u6a21\u5f0f", "gui.ae2sps.open_search": "\u6253\u5f00\u641c\u7d22", "gui.ae2sps.search.title": "\u6837\u677f\u641c\u7d22", "gui.ae2sps.search.matches": "\u5339\u914d\uff1a%s / %s", @@ -42,6 +48,8 @@ "tooltip.ae2sps.filter_mode": "\u8fc7\u6ee4\u6a21\u5f0f\uff1a%s", "tooltip.ae2sps.filter_mode_whitelist": "\u767d\u540d\u5355", "tooltip.ae2sps.filter_mode_blacklist": "\u9ed1\u540d\u5355", + "tooltip.ae2sps.input_mod_filter": "\u8f93\u5165\u6a21\u7ec4\uff1a%s\uff08%s \u4e2a ID\uff09", + "tooltip.ae2sps.output_mod_filter": "\u8f93\u51fa\u6a21\u7ec4\uff1a%s\uff08%s \u4e2a ID\uff09", "tooltip.ae2sps.bool_yes": "\u662f", "tooltip.ae2sps.bool_no": "\u5426", "tooltip.ae2sps.input_count": "\u8f93\u5165\u6761\u76ee\uff1a%s", diff --git a/versions/neoforge-1.21.1/src/test/java/com/ae2smartpatternsystem/network/SetPatternModFiltersPayloadTest.java b/versions/neoforge-1.21.1/src/test/java/com/ae2smartpatternsystem/network/SetPatternModFiltersPayloadTest.java new file mode 100644 index 0000000..46f4232 --- /dev/null +++ b/versions/neoforge-1.21.1/src/test/java/com/ae2smartpatternsystem/network/SetPatternModFiltersPayloadTest.java @@ -0,0 +1,56 @@ +package com.ae2smartpatternsystem.network; + +import com.ae2smartpatternsystem.menu.PatternEditorMenu; +import io.netty.buffer.Unpooled; +import net.minecraft.core.RegistryAccess; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.neoforged.neoforge.network.connection.ConnectionType; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class SetPatternModFiltersPayloadTest { + @Test + void codecRoundTripPreservesIndependentModesAndIds() { + SetPatternModFiltersPayload source = new SetPatternModFiltersPayload( + PatternEditorMenu.FILTER_MODE_WHITELIST, + List.of("create", "ae2"), + PatternEditorMenu.FILTER_MODE_BLACKLIST, + List.of("minecraft")); + RegistryFriendlyByteBuf buffer = new RegistryFriendlyByteBuf( + Unpooled.buffer(), RegistryAccess.EMPTY, ConnectionType.OTHER); + + SetPatternModFiltersPayload.STREAM_CODEC.encode(buffer, source); + SetPatternModFiltersPayload decoded = SetPatternModFiltersPayload.STREAM_CODEC.decode(buffer); + + assertEquals(source, decoded); + } + + @Test + void codecRejectsInvalidModeAndOversizedValues() { + SetPatternModFiltersPayload invalidMode = new SetPatternModFiltersPayload( + 99, List.of(), PatternEditorMenu.FILTER_MODE_BLACKLIST, List.of()); + SetPatternModFiltersPayload tooMany = new SetPatternModFiltersPayload( + PatternEditorMenu.FILTER_MODE_BLACKLIST, + java.util.stream.IntStream.range(0, 513).mapToObj(index -> "mod" + index).toList(), + PatternEditorMenu.FILTER_MODE_BLACKLIST, + List.of()); + SetPatternModFiltersPayload tooLong = new SetPatternModFiltersPayload( + PatternEditorMenu.FILTER_MODE_BLACKLIST, + List.of("a".repeat(65)), + PatternEditorMenu.FILTER_MODE_BLACKLIST, + List.of()); + + assertFalse(SetPatternModFiltersPayload.isValid(invalidMode)); + assertFalse(SetPatternModFiltersPayload.isValid(tooMany)); + assertFalse(SetPatternModFiltersPayload.isValid(tooLong)); + assertThrows(IllegalArgumentException.class, () -> + SetPatternModFiltersPayload.STREAM_CODEC.encode( + new RegistryFriendlyByteBuf(Unpooled.buffer(), RegistryAccess.EMPTY, ConnectionType.OTHER), + invalidMode)); + } +} diff --git a/versions/shared/common-core/src/main/java/com/ae2smartpatternsystem/core/codec/PatternNbtKeys.java b/versions/shared/common-core/src/main/java/com/ae2smartpatternsystem/core/codec/PatternNbtKeys.java index 3a9bdf5..50b0bbf 100644 --- a/versions/shared/common-core/src/main/java/com/ae2smartpatternsystem/core/codec/PatternNbtKeys.java +++ b/versions/shared/common-core/src/main/java/com/ae2smartpatternsystem/core/codec/PatternNbtKeys.java @@ -36,6 +36,13 @@ public final class PatternNbtKeys { public static final String TAG_FILTER_MODE_LEGACY = "FilterMode"; public static final String TAG_FILTER_ENTRIES = "FilterEntries"; + public static final String TAG_INPUT_MOD_FILTER_MODE = "TechStartInputModFilterMode"; + public static final String TAG_OUTPUT_MOD_FILTER_MODE = "TechStartOutputModFilterMode"; + public static final String TAG_INPUT_MOD_FILTER_IDS = "TechStartInputModFilterIds"; + public static final String TAG_OUTPUT_MOD_FILTER_IDS = "TechStartOutputModFilterIds"; + public static final String TAG_EXCLUDED_INPUT_MOD_IDS = "ExcludedInputModIds"; + public static final String TAG_EXCLUDED_OUTPUT_MOD_IDS = "ExcludedOutputModIds"; + private PatternNbtKeys() { } -} \ No newline at end of file +} diff --git a/versions/shared/common-core/src/main/java/com/ae2smartpatternsystem/core/model/ModFilterRule.java b/versions/shared/common-core/src/main/java/com/ae2smartpatternsystem/core/model/ModFilterRule.java new file mode 100644 index 0000000..059d4a8 --- /dev/null +++ b/versions/shared/common-core/src/main/java/com/ae2smartpatternsystem/core/model/ModFilterRule.java @@ -0,0 +1,80 @@ +package com.ae2smartpatternsystem.core.model; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Set; +import java.util.regex.Pattern; + +public final class ModFilterRule { + private static final Pattern MOD_ID_PATTERN = Pattern.compile("[a-z0-9_.-]+"); + + private final FilterMode mode; + private final List modIds; + + private ModFilterRule(FilterMode mode, Collection modIds) { + this.mode = Objects.requireNonNull(mode, "mode"); + + Set normalizedIds = new LinkedHashSet<>(); + if (modIds != null) { + for (String modId : modIds) { + String normalized = normalizeModId(modId); + if (!normalized.isEmpty()) { + normalizedIds.add(normalized); + } + } + } + this.modIds = Collections.unmodifiableList(new ArrayList<>(normalizedIds)); + } + + public static ModFilterRule of(FilterMode mode, Collection modIds) { + return new ModFilterRule(mode, modIds); + } + + public static ModFilterRule blacklist(Collection modIds) { + return of(FilterMode.BLACKLIST, modIds); + } + + public static ModFilterRule fromStoredData( + Integer serializedMode, + boolean canonicalIdsPresent, + Collection canonicalIds, + Collection legacyExcludedIds) { + FilterMode mode = serializedMode == null + ? FilterMode.BLACKLIST + : FilterMode.fromSerializedValue(serializedMode); + Collection ids = canonicalIdsPresent ? canonicalIds : legacyExcludedIds; + return of(mode, ids); + } + + public FilterMode mode() { + return mode; + } + + public List modIds() { + return modIds; + } + + public boolean allows(String namespace) { + String normalized = normalizeModId(namespace); + if (normalized.isEmpty()) { + return mode == FilterMode.BLACKLIST; + } + + boolean listed = modIds.contains(normalized); + return mode == FilterMode.BLACKLIST ? !listed : listed; + } + + public static String normalizeModId(String modId) { + if (modId == null) { + return ""; + } + + String normalized = modId.trim().toLowerCase(Locale.ROOT); + return MOD_ID_PATTERN.matcher(normalized).matches() ? normalized : ""; + } +} diff --git a/versions/shared/common-core/src/test/java/com/ae2smartpatternsystem/core/model/ModFilterRuleTest.java b/versions/shared/common-core/src/test/java/com/ae2smartpatternsystem/core/model/ModFilterRuleTest.java new file mode 100644 index 0000000..e08805b --- /dev/null +++ b/versions/shared/common-core/src/test/java/com/ae2smartpatternsystem/core/model/ModFilterRuleTest.java @@ -0,0 +1,167 @@ +package com.ae2smartpatternsystem.core.model; + +import com.ae2smartpatternsystem.core.codec.PatternNbtKeys; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ModFilterRuleTest { + @Test + void normalizesValidIdsAndPreservesFirstOccurrenceOrder() { + ModFilterRule rule = ModFilterRule.blacklist(List.of( + " Create ", + "create", + "AE2", + "foo_bar", + "foo-bar", + "foo.bar", + "foo.bar" + )); + + assertEquals(List.of("create", "ae2", "foo_bar", "foo-bar", "foo.bar"), rule.modIds()); + assertEquals(FilterMode.BLACKLIST, rule.mode()); + assertEquals("some_mod", ModFilterRule.normalizeModId(" Some_Mod ")); + } + + @Test + void ignoresNullBlankAndIllegalIds() { + ModFilterRule rule = ModFilterRule.blacklist(Arrays.asList( + null, + "", + " ", + "valid.mod", + "bad id", + "bad/id", + "bad:id", + "bad~id", + "valid.mod!" + )); + + assertEquals(List.of("valid.mod"), rule.modIds()); + assertEquals("", ModFilterRule.normalizeModId(null)); + assertEquals("", ModFilterRule.normalizeModId(" ")); + assertEquals("", ModFilterRule.normalizeModId("bad id")); + assertEquals("", ModFilterRule.normalizeModId("UPPER/CASE")); + } + + @Test + void blacklistAllowsUnlistedNamespacesAndRejectsListedNamespaces() { + ModFilterRule rule = ModFilterRule.blacklist(List.of(" create ")); + + assertFalse(rule.allows("CREATE")); + assertTrue(rule.allows("minecraft")); + assertTrue(rule.allows(null)); + assertTrue(rule.allows("bad id")); + } + + @Test + void whitelistAllowsListedNamespacesAndRejectsEverythingElse() { + ModFilterRule rule = ModFilterRule.of(FilterMode.WHITELIST, List.of(" create ")); + + assertTrue(rule.allows("CREATE")); + assertFalse(rule.allows("minecraft")); + assertFalse(rule.allows(null)); + assertFalse(rule.allows("bad id")); + } + + @Test + void emptyListsHaveModeSpecificDefaults() { + ModFilterRule blacklist = ModFilterRule.blacklist(List.of()); + ModFilterRule whitelist = ModFilterRule.of(FilterMode.WHITELIST, List.of()); + + assertTrue(blacklist.allows("minecraft")); + assertTrue(blacklist.allows("")); + assertFalse(whitelist.allows("minecraft")); + assertFalse(whitelist.allows("")); + } + + @Test + void nullCollectionsAreTreatedAsEmpty() { + assertTrue(ModFilterRule.blacklist(null).modIds().isEmpty()); + assertFalse(ModFilterRule.of(FilterMode.WHITELIST, null).allows("minecraft")); + } + + @Test + void returnedIdsAreImmutableAndInputChangesDoNotAffectRule() { + List source = new ArrayList<>(List.of("create")); + ModFilterRule rule = ModFilterRule.blacklist(source); + source.add("minecraft"); + + assertEquals(List.of("create"), rule.modIds()); + assertThrows(UnsupportedOperationException.class, () -> rule.modIds().add("minecraft")); + } + + @Test + void inputAndOutputRulesCanBeCombinedIndependently() { + ModFilterRule inputRule = ModFilterRule.blacklist(List.of("create")); + ModFilterRule outputRule = ModFilterRule.of(FilterMode.WHITELIST, List.of("ae2")); + + assertFalse(inputRule.allows("create")); + assertTrue(inputRule.allows("ae2")); + assertTrue(outputRule.allows("ae2")); + assertFalse(outputRule.allows("create")); + assertFalse(outputRule.allows("bad id")); + } + + @Test + void migratesLegacyExcludedIdsWithoutChangingBlacklistBehavior() { + ModFilterRule rule = ModFilterRule.fromStoredData( + null, + false, + List.of(), + List.of(" Create ", "AE2")); + + assertEquals(FilterMode.BLACKLIST, rule.mode()); + assertEquals(List.of("create", "ae2"), rule.modIds()); + assertFalse(rule.allows("create")); + assertTrue(rule.allows("minecraft")); + } + + @Test + void canonicalModeCanReuseLegacyIdsDuringPartialMigration() { + ModFilterRule rule = ModFilterRule.fromStoredData( + FilterMode.WHITELIST.serializedValue(), + false, + List.of(), + List.of(" Create ")); + + assertEquals(FilterMode.WHITELIST, rule.mode()); + assertTrue(rule.allows("create")); + assertFalse(rule.allows("minecraft")); + } + + @Test + void canonicalIdsTakePrecedenceEvenWhenTheyAreEmpty() { + ModFilterRule rule = ModFilterRule.fromStoredData( + FilterMode.WHITELIST.serializedValue(), + true, + List.of(), + List.of("create")); + + assertEquals(FilterMode.WHITELIST, rule.mode()); + assertTrue(rule.modIds().isEmpty()); + assertFalse(rule.allows("create")); + } + + @Test + void exposesCanonicalAndLegacyNbtKeys() { + assertEquals("TechStartInputModFilterMode", PatternNbtKeys.TAG_INPUT_MOD_FILTER_MODE); + assertEquals("TechStartOutputModFilterMode", PatternNbtKeys.TAG_OUTPUT_MOD_FILTER_MODE); + assertEquals("TechStartInputModFilterIds", PatternNbtKeys.TAG_INPUT_MOD_FILTER_IDS); + assertEquals("TechStartOutputModFilterIds", PatternNbtKeys.TAG_OUTPUT_MOD_FILTER_IDS); + assertEquals("ExcludedInputModIds", PatternNbtKeys.TAG_EXCLUDED_INPUT_MOD_IDS); + assertEquals("ExcludedOutputModIds", PatternNbtKeys.TAG_EXCLUDED_OUTPUT_MOD_IDS); + } + + @Test + void rejectsNullMode() { + assertThrows(NullPointerException.class, () -> ModFilterRule.of(null, List.of("create"))); + } +}