diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0073f21..a188903 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,35 +1,16 @@ name: Build mod jar on: - [workflow_dispatch, push] + workflow_dispatch: + push: + pull_request: permissions: - contents: read + contents: write jobs: build: - - if: "! contains(toJSON(github.event.commits.*.message), '[ci skip]')" - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v6 - - name: Set up JDK - uses: actions/setup-java@v5 - with: - java-version: '25' - distribution: 'temurin' - - uses: gradle/actions/setup-gradle@v6 - with: - gradle-version: 9.5.1 - name: Set up Gradle - - name: Add permission - run: chmod +x ./gradlew - - name: Execute Gradle build - run: ./gradlew build - - - name: Upload a Build Artifact - uses: actions/upload-artifact@v7 - with: - path: build/libs + uses: FormlessDragon/.github/.github/workflows/build-mdg.yml@main + secrets: inherit + permissions: + contents: write diff --git a/.github/workflows/issue-labels.yml b/.github/workflows/issue-labels.yml new file mode 100644 index 0000000..5c835f3 --- /dev/null +++ b/.github/workflows/issue-labels.yml @@ -0,0 +1,36 @@ +name: Issue labels + +on: + issues: + types: + - closed + +permissions: + issues: write + +jobs: + complete-closed-issue: + if: contains(github.event.issue.labels.*.name, '处理中') + runs-on: ubuntu-latest + + steps: + - name: Move closed issue to completed + uses: actions/github-script@v8 + with: + script: | + const { owner, repo } = context.repo; + const issue_number = context.issue.number; + + await github.rest.issues.removeLabel({ + owner, + repo, + issue_number, + name: '处理中', + }); + + await github.rest.issues.addLabels({ + owner, + repo, + issue_number, + labels: ['已完成'], + }); \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2e96516..5c253a4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,41 +1,17 @@ -name: Releases +name: Release tagged build on: push: - tags: - - "v*" + tags: ['v*'] + +permissions: + contents: write + actions: write jobs: - build: - runs-on: ubuntu-latest + release: + uses: FormlessDragon/.github/.github/workflows/release-mdg.yml@main + secrets: inherit permissions: contents: write - env: - RELEASE_TAG: ${{ github.ref_name }} - steps: - - uses: actions/checkout@v6 - - name: Set up JDK - uses: actions/setup-java@v5 - with: - java-version: '25' - distribution: 'temurin' - - uses: gradle/actions/setup-gradle@v6 - with: - gradle-version: 9.5.1 - name: Set up Gradle - - name: Add permission - run: chmod +x ./gradlew - - name: Set release version - run: | - RELEASE_VERSION="${RELEASE_TAG#v}" - echo "RELEASE_VERSION=${RELEASE_VERSION}" >> "$GITHUB_ENV" - echo "Release tag: ${RELEASE_TAG}" - echo "Release version: ${RELEASE_VERSION}" - - name: Execute Gradle build - run: ./gradlew build -Pmod_version="${RELEASE_VERSION}" - - - uses: ncipollo/release-action@v1.21.0 - with: - artifacts: "build/libs/*.jar" - generateReleaseNotes: true - allowUpdates: true + actions: write diff --git a/.github/workflows/runtime-test.yml b/.github/workflows/runtime-test.yml new file mode 100644 index 0000000..15e724f --- /dev/null +++ b/.github/workflows/runtime-test.yml @@ -0,0 +1,18 @@ +name: Runtime Test + +on: + workflow_dispatch: + workflow_call: + pull_request: + push: + branches: [main] + merge_group: + +permissions: + contents: read + actions: write + +jobs: + runtime-test: + uses: FormlessDragon/.github/.github/workflows/runtime-test-mdg.yml@main + secrets: inherit diff --git a/.gitignore b/.gitignore index ebfa0fa..fd90a32 100644 --- a/.gitignore +++ b/.gitignore @@ -24,5 +24,3 @@ gradle-app.setting # IDEA config .idea - -AGENTS.md diff --git a/README.md b/README.md index a79ca72..d122aa7 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,28 @@ # AE2-GTIntegration -An addon for Applied Energistics 2 Supergiant and gtceu. +An addon for **Applied Energistics 2 Supergiant** and **GregTech CEu**. -# Features +## Features -- Add a button to enable to ignore integration circuits in JEI recipe. -- Add Circuit Card inspired by [NAE2](https://github.com/AE2-UEL/NAE2/blob/2fe8dede709b6917d880f41085cd5ece9042fa7e/src/main/java/co/neeve/nae2/mixin/upgrades/gregcircuit/MixinDualityInterface.java#L52) \ No newline at end of file +- **Ignore Integration Circuits in JEI Recipe Transfer** + Adds a button to ignore integration circuits when transferring recipes from JEI. + +- **Auto-Rename Duplicate Items in JEI Recipe Transfer** + Adds a button to automatically rename identical items when transferring recipes from JEI. + +- **Circuit Card** + Inspired by [NAE2](https://github.com/AE2-UEL/NAE2/blob/2fe8dede709b6917d880f41085cd5ece9042fa7e/src/main/java/co/neeve/nae2/mixin/upgrades/gregcircuit/MixinDualityInterface.java#L52). + +- **Extended Encoding Term** + Inspired by [Applied Energistics 2 Unofficial](https://github.com/GTNewHorizons/Applied-Energistics-2-Unofficial). + Its assets are modified from [Modernity GTNH UI](https://github.com/ModernityGTNH/Modernity-GTNH-UI) and are licensed under [![License: CC BY-NC-SA 3.0](https://img.shields.io/badge/License-CC%20BY--NC--SA%203.0-yellow.svg?style=flat-square)](https://creativecommons.org/licenses/by-nc-sa/3.0/). + +## Credits + +- [NAE2](https://github.com/AE2-UEL/NAE2) — inspiration for the Circuit Card. +- [Applied Energistics 2 Unofficial](https://github.com/GTNewHorizons/Applied-Energistics-2-Unofficial) — inspiration for the Extended Encoding Term. +- [Modernity GTNH UI](https://github.com/ModernityGTNH/Modernity-GTNH-UI) — source of the modified assets used by the Extended Encoding Term, licensed under CC BY-NC-SA 3.0. + +## License + +The assets modified from [Modernity GTNH UI](https://github.com/ModernityGTNH/Modernity-GTNH-UI) are licensed under [CC BY-NC-SA 3.0](https://creativecommons.org/licenses/by-nc-sa/3.0/). \ No newline at end of file diff --git a/build.gradle b/build.gradle index cde26c5..a5cd301 100644 --- a/build.gradle +++ b/build.gradle @@ -1,35 +1,84 @@ +// It is advised to not edit this file directly, in order to update script easily. +// Instead, edit gradle/scripts/extra.gradle to customize the build. +import groovy.json.JsonOutput +import org.jetbrains.gradle.ext.Gradle + plugins { id 'java' id 'java-library' id 'maven-publish' - id 'com.gradleup.shadow' version '9.4.1' id 'org.jetbrains.gradle.plugin.idea-ext' version '1.4.1' - id 'xyz.wagyourtail.unimined' version '1.4.23-kappa' - id 'net.kyori.blossom' version '2.2.0' + id 'net.neoforged.moddev.mcpforge' version '2.0.185-legacy' + id 'com.diffplug.spotless' version '8.10.4' apply false + id 'com.gradleup.shadow' version '9.6.1' apply false } -import org.jetbrains.gradle.ext.Gradle +apply from: 'gradle/scripts/helpers.gradle' + +private String gitOutput(List args, String fallback) { + try { + def process = new ProcessBuilder(['git'] + args) + .directory(rootProject.projectDir) + .redirectErrorStream(true) + .start() + def stdout = process.inputStream.getText('UTF-8').trim() + if (process.waitFor() == 0 && !stdout.isEmpty()) { + return stdout + } + } catch (Exception ignored) { + // Source archives and offline builds may not have Git available. + } + return fallback +} + +private String defaultModVersion() { + def previousTag = gitOutput(['describe', '--tags', '--abbrev=0', '--exclude=dev'], '0.0.0') + if (previousTag.startsWith('v')) { + previousTag = previousTag.substring(1) + } + def shortHash = gitOutput(['rev-parse', '--short', 'HEAD'], 'unknown') + return "${previousTag}+${shortHash}" +} + +if (!gradle.startParameter.projectProperties.containsKey('mod_version')) { + project.setProperty('mod_version', defaultModVersion()) +} -ext { - //noinspection GroovyAssignabilityCheck - access_transformer_locations = "${mod_id}_at.cfg" +if (propertyBool('enable_shadow')) { + apply plugin: 'com.gradleup.shadow' } -version = mod_version -group = root_package +// Early Assertions +assertProperty 'mod_version' +assertProperty 'root_package' +assertProperty 'mod_id' +assertProperty 'mod_name' + +assertSubProperties 'use_access_transformer', 'access_transformer_locations' +assertSubProperties 'use_mixins', 'mixin_configs' +assertSubProperties 'is_coremod', 'coremod_includes_mod', 'coremod_plugin_class_name' + +setDefaultProperty 'generate_sources_jar', true, false +setDefaultProperty 'generate_javadocs_jar', true, false +setDefaultProperty 'enable_shadow', false, false +setDefaultProperty 'minecraft_username', true, 'Developer' +setDefaultProperty 'extra_jvm_args', false, '' + +version = propertyString('mod_version') +group = propertyString('root_package') base { - archivesName = mod_id + archivesName = propertyString('mod_id') } java { toolchain { languageVersion = JavaLanguageVersion.of(25) } - if (generate_sources_jar.toBoolean()) { + if (propertyBool('generate_sources_jar')) { withSourcesJar() } - if (generate_javadocs_jar.toBoolean()) { + if (propertyBool('generate_javadocs_jar')) { withJavadocJar() } } @@ -37,112 +86,126 @@ java { configurations { contain implementation.extendsFrom(contain) - modCompileOnly - compileOnly.extendsFrom(modCompileOnly) - testCompileOnly.extendsFrom(compileOnly) - modRuntimeOnly - runtimeOnly.extendsFrom(modRuntimeOnly) - testRuntimeOnly.extendsFrom(runtimeOnly) -} - -def remapTaskName = enable_shadow.toBoolean() ? "remapShadowJar" : "remapJar" -def gtceuMappingsJar = layout.projectDirectory.file(".gradle/unimined/local/mappings/srg2mcp.jar") -def gtceuMappingsSrg = layout.projectDirectory.file(".gradle/unimined/local/mappings/srg2mcp.tsrg") -def gtceuMappingsCsvDir = layout.buildDirectory.dir("gtceu/mcp-csv") -def prepareGtceuDevMappings = tasks.register("prepareGtceuDevMappings", Copy) { - from({ zipTree(gtceuMappingsJar.asFile) }) { - include "fields.csv", "methods.csv" + if (propertyBool('enable_shadow')) { + shaded + implementation.extendsFrom(shaded) } - into(gtceuMappingsCsvDir) } -tasks.matching { it.name == 'runClient' || it.name == 'runServer' }.configureEach { - dependsOn(prepareGtceuDevMappings) +boolean useMixins = propertyBool('use_mixins') +def mixinConfigs = propertyStringList('mixin_configs', ',') + +String reobfTaskName = 'reobfJar' + +if (propertyBool('enable_shadow')) { + def reobfShadowJar = obfuscation.reobfuscate(tasks.named('shadowJar'), sourceSets.main) + reobfTaskName = reobfShadowJar.name + tasks.named('assemble').configure { + dependsOn reobfShadowJar + } } //noinspection GroovyAssignabilityCheck -unimined.minecraft { - version "1.12.2" +mcpForge { + if (propertyBool('use_access_transformer')) { + accessTransformers.from(propertyStringList('access_transformer_locations').collect { + "src/main/resources/${it}" + }) + } - mappings { - mcp("stable", "39-1.12") + enable { + cleanroomVersion = '0.6.13-alpha' + mcpMappings = 'de.oceanlabs.mcp:mcp_stable:39-1.12@zip' + obfuscateJar = !propertyBool('enable_shadow') } - cleanroom { - if (use_access_transformer.toBoolean()) { - accessTransformer "${rootProject.projectDir}/src/main/resources/$access_transformer_locations" - } - loader "0.5.14-alpha" - runs.all { - systemProperty("crl.dev.mixin", "${mod_id}.mod.mixin.json") - systemProperty("net.minecraftforge.gradle.GradleStart.srg.notch-srg", gtceuMappingsSrg.asFile.absolutePath) - systemProperty("net.minecraftforge.gradle.GradleStart.csvDir", gtceuMappingsCsvDir.get().asFile.absolutePath) - args += ['--username', minecraft_username] - def extraArgs = extra_jvm_args + runs { + configureEach { + programArguments.addAll('--username', propertyString('minecraft_username')) + systemProperty('net.minecraftforge.gradle.GradleStart.srg.srg-mcp', + layout.buildDirectory.file('moddev/artifacts/intermediateToNamed.srg').get().asFile.absolutePath) + def extraArgs = propertyString('extra_jvm_args') if (extraArgs != null && !extraArgs.trim().isEmpty()) { - jvmArgs += extraArgs.split { "\\s+" }.toList() + extraArgs.trim().split(/\s+/).each { jvmArgument(it) } } - if (enable_foundation_debug.toBoolean()) { + if (propertyBool('enable_foundation_debug')) { systemProperty("foundation.dump", "true") systemProperty("foundation.verbose", "true") } - if (is_coremod.toBoolean()) { - systemProperty("fml.coreMods.load", coremod_plugin_class_name) + if (propertyBool('is_coremod')) { + systemProperty("fml.coreMods.load", propertyString('coremod_plugin_class_name')) } - return + } + client { + client() + disableIdeRun() + } + server { + server() + disableIdeRun() + systemProperty 'fml.queryResult', 'confirm' } } - defaultRemapJar = false - - String jarTaskName = enable_shadow.toBoolean() ? "shadowJar" : "jar" - - remap(tasks.named(jarTaskName).get()) { - mixinRemap { - enableBaseMixin() - enableMixinExtra() + mods { + create(propertyString('mod_id')) { + sourceSet(sourceSets.main) } } +} - mods { - remap(configurations.modCompileOnly) - remap(configurations.modRuntimeOnly) +def mixinRefmap = null +if (useMixins) { + mixin { + mixinConfigs.each { config(it) } + String refmap = propertyString('mixin_refmap') + if (!refmap.isBlank()) { + mixinRefmap = add(refmap) + } } } +// Do not configure your deps here! +// Check gradle/scripts/dependencies.gradle for that. dependencies { - if (use_asset_mover.toBoolean()) { - implementation "com.cleanroommc:assetmover:${asset_mover_version}" + if (useMixins) { + annotationProcessor 'com.cleanroommc:mixinextras-common:0.5.5' + annotationProcessor 'com.cleanroommc:cleanmix:0.7.4' } - if (enable_junit_testing.toBoolean()) { - testImplementation 'org.junit.jupiter:junit-jupiter:6.0.3' + if (propertyBool('enable_junit_testing')) { + testImplementation 'org.junit.jupiter:junit-jupiter:6.1.3' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } + if (propertyBool('use_asset_mover')) { + implementation "com.cleanroommc:assetmover:${propertyString('asset_mover_version')}" + } } apply from: 'gradle/scripts/dependencies.gradle' def generatedTagsSourceDir = layout.buildDirectory.dir('generated/sources/tags/java') -def tagsPackage = "${root_package}.ae2gtaddon" def generateTagsSource = tasks.register('generateTagsSource') { -def outputFile = generatedTagsSourceDir.map { it.file("${tagsPackage.replace('.', '/')}/Tags.java") } + def tagsPackage = "${propertyString('root_package')}.${propertyString('mod_id')}" + def outputFile = generatedTagsSourceDir.map { + it.file("${tagsPackage.replace('.', '/')}/Tags.java") + } - inputs.property('mod_id', mod_id) - inputs.property('mod_name', mod_name) - inputs.property('mod_version', mod_version) + inputs.property('root_package', propertyString('root_package')) + inputs.property('mod_id', propertyString('mod_id')) + inputs.property('mod_name', propertyString('mod_name')) + inputs.property('mod_version', propertyString('mod_version')) outputs.file(outputFile) doLast { def file = outputFile.get().asFile - delete generatedTagsSourceDir file.parentFile.mkdirs() file.text = """package ${tagsPackage}; public final class Tags { - public static final String MOD_ID = "${mod_id}"; - public static final String MOD_NAME = "${mod_name}"; - public static final String VERSION = "${mod_version}"; + public static final String MOD_ID = "${propertyString('mod_id')}"; + public static final String MOD_NAME = "${propertyString('mod_name')}"; + public static final String VERSION = "${propertyString('mod_version')}"; private Tags() { } @@ -151,70 +214,51 @@ public final class Tags { } } -processResources { - rename '(.+_at.cfg)', 'META-INF/$1' - def resourceProps = [ - mod_id : mod_id, - mod_name : mod_name, - mod_version : mod_version, - mod_description : mod_description, - mod_authors : mod_authors.split(',').findAll { !it.isBlank() }.collect { it.strip() }.join('", "'), - mod_credits : mod_credits, - mod_url : mod_url, - mod_update_json : mod_update_json, - mod_logo_path : mod_logo_path, - ] - inputs.properties(resourceProps) - filesMatching(['mcmod.info', 'pack.mcmeta']) { - expand(resourceProps) - } -} - sourceSets { main { java { srcDir generatedTagsSourceDir } } + test { - compileClasspath += sourceSets.main.compileClasspath - runtimeClasspath += sourceSets.main.runtimeClasspath + compileClasspath += sourceSets.main.output + sourceSets.main.compileClasspath + runtimeClasspath += sourceSets.main.output + sourceSets.main.runtimeClasspath } } -idea { - module { - inheritOutputDirs = true - } - project { - settings { - runConfigurations { - '1. Build'(Gradle) { - taskNames = ["build"] - } - '2. Run Client'(Gradle) { - taskNames = ["runClient"] - } - '3. Run Server'(Gradle) { - taskNames = ["runServer"] - } - } - compiler.javac { - afterEvaluate { - javacAdditionalOptions = '-encoding utf8' - moduleJavacAdditionalOptions = [ - (project.name + '.main'): tasks.compileJava.options.compilerArgs.collect { "\"${it}\"" }.join(' ') - ] - } - } - } - } +configurations.named('testCompileOnly').configure { + extendsFrom(configurations.compileOnly) +} +configurations.named('testRuntimeOnly').configure { + extendsFrom(configurations.runtimeOnly) } -if (!enable_shadow.toBoolean()) { - shadowJar.enabled = false +processResources { + rename '(.+_at.cfg)', 'META-INF/$1' + def resourceProps = [ + mod_id : propertyString('mod_id'), + mod_name : propertyString('mod_name'), + mod_version : propertyString('mod_version'), + mod_description : propertyString('mod_description'), + mod_authors : propertyStringList('mod_authors', ',').collect { it.strip() }.join('", "'), + mod_credits : propertyString('mod_credits'), + mod_license : propertyString('mod_license'), + mod_url : propertyString('mod_url'), + mod_issue_tracker_url: propertyString('mod_issue_tracker_url'), + mod_update_json : propertyString('mod_update_json'), + mod_icon_item : propertyString('mod_icon_item'), + mod_logo_path : propertyString('mod_logo_path'), + mod_icon_path : propertyString('mod_icon_path'), + mod_background_path : propertyString('mod_background_path') + ] + inputs.properties(resourceProps) + filesMatching(['mcmod.info', 'pack.mcmeta']) { + expand(resourceProps) + } } + compileJava { dependsOn(generateTagsSource) sourceCompatibility = targetCompatibility = JavaVersion.VERSION_25 @@ -224,8 +268,32 @@ tasks.named('sourcesJar').configure { dependsOn(generateTagsSource) } +def modManifestAttributes = { + def attribute_map = [:] + attribute_map['ModType'] = "CRL" + if (configurations.contain.size() > 0) { + attribute_map['ContainedDeps'] = configurations.contain.collect { it.name }.join(' ') + attribute_map['NonModDeps'] = true + } + if (propertyBool('is_coremod')) { + attribute_map['FMLCorePlugin'] = propertyString('coremod_plugin_class_name') + if (propertyBool('coremod_includes_mod')) { + attribute_map['FMLCorePluginContainsFMLMod'] = true + } + } + if (propertyBool('use_access_transformer')) { + attribute_map['FMLAT'] = propertyString('access_transformer_locations') + } + return attribute_map +} + jar { - archiveClassifier = 'dev' + if (propertyBool('enable_shadow')) { + archiveClassifier = 'plain' + } else { + archiveClassifier = 'dev' + destinationDirectory = layout.buildDirectory.dir('libs') + } duplicatesStrategy = DuplicatesStrategy.EXCLUDE if (configurations.contain.size() > 0) { into('/') { @@ -234,47 +302,84 @@ jar { } doFirst { manifest { - def attribute_map = [:] - attribute_map['ModType'] = "CRL" - attribute_map['MixinConfigs'] = "${mod_id}.mod.mixin.json" - if (configurations.contain.size() > 0) { - attribute_map['ContainedDeps'] = configurations.contain.collect { it.name }.join(' ') - attribute_map['NonModDeps'] = true - } - if (is_coremod.toBoolean()) { - attribute_map['FMLCorePlugin'] = coremod_plugin_class_name - if (coremod_includes_mod.toBoolean()) { - attribute_map['FMLCorePluginContainsFMLMod'] = true - } + attributes(modManifestAttributes()) + } + } +} + +if (propertyBool('enable_shadow')) { + // Shade deps to the dev jar, so the normal dev jar will contain deps + shadowJar { + archiveClassifier = 'dev' + destinationDirectory = layout.buildDirectory.dir('libs') + //noinspection GroovyAssignabilityCheck,GroovyAccessibility + configurations = [ project.configurations.shaded ] + // ShadowJar has its own `configurations` (the shaded ones), so the project's have to be qualified + if (project.configurations.contain.size() > 0) { + into('/') { + from project.configurations.contain } - if (use_access_transformer.toBoolean()) { - attribute_map['FMLAT'] = access_transformer_locations + } + if (mixinRefmap != null) { + // The shaded jar replaces `jar`, and reobfuscation remaps this jar, so the refmap has to be here + from(mixinRefmap) + } + doFirst { + manifest { + attributes(modManifestAttributes()) } - attributes(attribute_map) } } - finalizedBy(tasks.named(remapTaskName).get()) + // Unshaded dev jar get 'plain' as name + tasks.named("jar").configure { + finalizedBy shadowJar + } } - -shadowJar { - //noinspection GroovyAssignabilityCheck,GroovyAccessibility - configurations = [project.configurations.shadow] - archiveClassifier = "shadow" +tasks.named(reobfTaskName).configure { + // Convention from jar/shadowJar + archiveClassifier = '' } - -tasks.named(remapTaskName).configure { - doFirst { - logging.captureStandardOutput LogLevel.INFO +idea { + module { + inheritOutputDirs = true } - doLast { - logging.captureStandardOutput LogLevel.QUIET + project { + settings { + runConfigurations { + "0. Create Artifacts"(Gradle) { + taskNames = ["createMinecraftArtifacts"] + } + "1. Run Client"(Gradle) { + taskNames = ["runClient"] + } + "2. Run Server"(Gradle) { + taskNames = ["runServer"] + } + "3. Build Mod"(Gradle) { + taskNames = ["build"] + } + } + compiler.javac { + afterEvaluate { + javacAdditionalOptions = "-encoding utf8" + moduleJavacAdditionalOptions = [ + (project.name + ".main"): tasks.compileJava.options.compilerArgs.collect { '"' + it + '"' }.join(' ') + ] + } + } + } + } +} + +plugins.withId('com.diffplug.spotless') { + project.idea.project.settings.runConfigurations.create('4. Apply Spotless', Gradle) { + taskNames = ['spotlessApply'] } } compileTestJava { - dependsOn(generateTagsSource) sourceCompatibility = targetCompatibility = JavaVersion.VERSION_25 } @@ -283,15 +388,51 @@ test { javaLauncher.set(javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(25) }) - if (show_testing_output.toBoolean()) { + if (propertyBool('show_testing_output')) { testLogging { showStandardStreams = true } } } +tasks.register('generateMixinJson') { + group = 'build setup' + description = 'Generates missing Mixin configuration JSON files.' + onlyIf { + useMixins + } + doLast { + def missingConfigs = mixinConfigs.findAll { configName -> + !file("src/main/resources/${configName}").exists() + } + if (missingConfigs.isEmpty()) { + logger.lifecycle('No missing Mixin JSON files.') + return + } + missingConfigs.each { configName -> + def configFile = file("src/main/resources/${configName}") + configFile.parentFile.mkdirs() + def config = [ + 'package': '', + 'refmap': propertyString('mixin_refmap'), + 'required': true, + 'target': '@env(DEFAULT)', + 'minVersion': '0.8.7', + 'compatibilityLevel': 'JAVA_25', + 'mixins': [], + 'server': [], + 'client': [], + 'setSourceFile': true + ] + configFile.setText(JsonOutput.prettyPrint(JsonOutput.toJson(config)) + '\n', 'UTF-8') + logger.lifecycle("Generated Mixin JSON: ${configFile}") + } + } +} + tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8' + options.compilerArgs += ['-Xlint:deprecation', '-Xlint:unchecked'] } apply from: 'gradle/scripts/publishing.gradle' diff --git a/gradle.properties b/gradle.properties index 4a7bd81..3dffed0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,9 +7,12 @@ generate_javadocs_jar = false # Testing enable_junit_testing = true +# Show test standard output and error in the console show_testing_output = false # Shadow +# This will rename the 'dev' jar to 'plain' and replace 'dev' with a shaded jar +# Deps will be shaded into the final obf jar with no suffix too enable_shadow = false # Set this to true if you want to use old LWJGL2 methods. @@ -20,6 +23,7 @@ enable_lwjglx = true # Mod Information # HIGHLY RECOMMEND complying with SemVer for mod_version: https://semver.org/ mod_version = 1.0.0 +# Package used as group in maven publishing and as the project's `group` root_package = com.formlessDragon mod_id = ae2gtaddon mod_name = AE2-GTIntegration @@ -27,37 +31,93 @@ mod_name = AE2-GTIntegration # Mod Metadata (Optional) mod_description = An addon for Applied Energistics 2 Supergiant and gtceu mod_url = https://github.com/FormlessDragon/AE2-GTIntegration +# A url where the mod's update JSON can be fetched +# Wiki: https://docs.minecraftforge.net/en/1.12.x/gettingstarted/autoupdate/#forge-update-checker +# Also see: https://forge.curseupdate.com + https://docs.modrinth.com/api/operations/forgeupdates mod_update_json = -# Delimit authors with commas +# Delimit multiple authors with commas. Eg: Author1,Author2 mod_authors = FormlessDragon mod_credits = +# Path to a png file. Eg: assets/${mod_id}/logo.png mod_logo_path = +# Cleanroom Extra Metadata (Optional) +# License of the mod, an SPDX identifier is preferred. Eg: MIT +mod_license = LGPL-3.0 +mod_issue_tracker_url = https://github.com/FormlessDragon/AE2-GTIntegration/issues +# Path to a square png file +mod_icon_path = +# An item to be rendered as the mod icon in Catalogue mod list, +# Formatted as modid:item_name(:meta) +# Final icon will be mod_icon_path if path is not empty. +mod_icon_item = +# Path to a png file +# Aspect-preserving cover cropping to fill the details panel, then faded out toward the bottom +mod_background_path = + +# Dependencies +ae2_version = 1.0.17 + # Run Configurations -# If multiple arguments/tweak classes are stated, use spaces as the delimiter minecraft_username = Developer +# Delimit multiple arguments with spaces extra_jvm_args = +# Print extra foundation (Cleanroom's patching framework) diagnostics enable_foundation_debug = false -# Maven Publishing (Provide secret: MAVEN_USER, MAVEN_PASS) +# Maven Publishing +# Credentials are looked up by repository name, which is `maven_name` with spaces removed, plus +# `Username` / `Password`. The default below therefore expects `ModNameUsername` and `ModNamePassword`. +# They are only required when a publishing task actually runs. publish_to_maven = false -# Good for debugging artifacts before uploading to remote maven -# GitHub actions won't run if this is true, test this by running the task `publishToMavenLocal` -publish_to_local_maven = false +publish_to_jitpack = false maven_name = ${mod_name} maven_url = +maven_group = ${root_package} +maven_artifact_id = ${mod_id} +maven_developer_id = +maven_developer_name = +maven_license_name = ${mod_license} +maven_license_url = +maven_scm_url = ${mod_url} +jitpack_group = +jitpack_artifact_id = +jitpack_version = +# Good for debugging artifacts before uploading to remote maven: run `./gradlew publishToMavenLocal` + +# CurseForge publishing (set the project id in CI or on the command line) +curseforge_project_id = +curseforge_release_type = +curseforge_game_versions = 1.12.2,Forge,Java 25,Client,Server +curseforge_relations_required = +curseforge_relations_optional = +curseforge_relations_embedded = +curseforge_relations_incompatible = +curseforge_relations_tool = +curseforge_debug_mode = false -# If any properties changes below this line, refresh gradle again to ensure everything is working correctly. +# If any properties change below this line, refresh gradle again to ensure everything is working correctly. # ---------------------------------------------------------------------------------------------------------------------- # Access Transformers # A way to change visibility of Minecraft's classes, methods and fields -# An example access transformer file is given in the path: `src/main/resources/example_at.cfg` -# AT files should be in the root of src/main/resources with the filename formatted as: `mod_id_at.cfg` -# Use the property `access_transformer_locations` to state custom AT files if you aren't using the default `mod_id_at.cfg` location -# If multiple locations are stated, use spaces as the delimiter -# WARNING: Use MCP name in AT file. Unimined will remap it to srg name when building. +# AT files belong in the root of src/main/resources, and every filename must end with `_at.cfg` (eg: ${mod_id}_at.cfg) +# Delimit multiple AT files with commas +# WARNING: Use SRG name in AT file (eg: `field_130070_K`). ModDevGradle does not remap it. use_access_transformer = false +access_transformer_locations = ${mod_id}_at.cfg + +# Mixins +# Powerful tool to do runtime modification of classes +# Only use mixins once you understand the underlying structure +use_mixins = true +# Delimit configuration filenames with commas +# You may run `./gradlew generateMixinJson` to generate missing ones +mixin_configs = ${mod_id}.mixins.json +# A refmap is a JSON that denotes mapping conversions, this JSON is generated automatically, with the name ${mod_id}.refmap.json +# Change this property to use a different name, only one name is accepted. +# NOTE: This does not apply to JSON files that already exist. +mixin_refmap = ${mod_id}.refmap.json # Coremods # The most powerful way to change java classes at runtime, it is however very primitive with little documentation. @@ -73,5 +133,3 @@ coremod_plugin_class_name = # Documentation: https://github.com/CleanroomMC/AssetMover use_asset_mover = false asset_mover_version = 2.5 - -ae2_version = 1.0.4 \ No newline at end of file diff --git a/gradle/scripts/dependencies.gradle b/gradle/scripts/dependencies.gradle index 2eac6f2..83803e8 100644 --- a/gradle/scripts/dependencies.gradle +++ b/gradle/scripts/dependencies.gradle @@ -1,44 +1,71 @@ +apply from: 'gradle/scripts/helpers.gradle' + repositories { - // Other repositories described by default: - // CleanroomMC: https://maven.cleanroommc.com flatDir { - dirs('lib') + dirs('lib', 'lib/local') // /lib + } + maven { + name = 'CleanroomMC' + url = 'https://maven.cleanroommc.com' + content { + includeGroup 'mezz' + includeGroup 'codechicken' + } } maven { name = 'CurseMaven' url = 'https://cursemaven.com' + content { + includeGroup 'curse.maven' + } } maven { name = 'CleanroomCurseMaven' url = 'https://curse.cleanroommc.com' + content { + includeGroup 'curse.maven' + } } maven { name = 'Modrinth' url = 'https://api.modrinth.com/maven' + content { + includeGroup 'maven.modrinth' + } } - mavenCentral() maven { name = 'chickenbones' url = 'https://chickenbones.net/maven/' } maven { - name 'BlameJared Maven' - url 'https://maven.blamejared.com' + name = 'BlameJared' + url = 'https://maven.blamejared.com' } + maven { + name = 'jitpack' + url = 'https://jitpack.io' + content { + includeGroup 'com.github.FormlessDragon' + } + } + mavenCentral() mavenLocal() // Must be last for caching to work } dependencies { - compileOnly "com.cleanroommc:sponge-mixin:0.20.13+mixin.0.8.7" - if (enable_lwjglx.toBoolean()) { + compileOnly 'org.jetbrains:annotations:26.0.2' + compileOnly "com.cleanroommc:cleanmix:0.7.4" + compileOnly "com.cleanroommc:mixinextras-common:0.5.5" + if (propertyBool('enable_lwjglx')) { compileOnly "com.cleanroommc:lwjglx:1.0.0" } - modImplementation("mezz:jei:4.31.2") + compileOnly("curse.maven:mouse-tweaks-unofficial-461660:5876158") + modImplementation("mezz:jei:4.35.1") modImplementation("codechicken:CodeChickenLib:3.3.5") modImplementation("curse.maven:gregtech-ce-unofficial-557242:5519022") - compileOnly("io.github.formlessdragon:AppliedEnergistics2-Supergiant:${ae2_version}:dev") - modRuntimeOnly("io.github.formlessdragon:AppliedEnergistics2-Supergiant:${ae2_version}:dev") - modRuntimeOnly(":appcompat:0.0.0+6e7bceb:dev") + modImplementation("com.github.FormlessDragon:Applied-Energistics-2-Supergiant:v${propertyString('ae2_version')}:dev") + + //modRuntimeOnly(":appcompat:0.0.0+6e7bceb:dev") } diff --git a/gradle/scripts/extra.gradle b/gradle/scripts/extra.gradle index 106911e..44ed74d 100644 --- a/gradle/scripts/extra.gradle +++ b/gradle/scripts/extra.gradle @@ -1,5 +1,592 @@ -// You may write any gradle buildscript component in this file -// This file is automatically applied after build.gradle + dependencies.gradle is ran +import java.util.zip.ZipFile -// Helper methods (assertProperty, assertSubProperties, setDefaultProperty) are -// defined directly in build.gradle's script scope and exported via ext. +// You may write any Gradle buildscript component in this file. +// This file is automatically applied after build.gradle + dependencies.gradle. + +def minecraftVersion = '1.12.2' +def cleanroomVersion = '0.6.13-alpha' + +// The runtime-test workflow intentionally keeps its original staging and +// installation flow. These tasks provide the same contract for ModDevGradle. +apply plugin: 'com.diffplug.spotless' + +def cleanroomInstaller = configurations.maybeCreate('cleanroomInstaller') +cleanroomInstaller.canBeConsumed = false +cleanroomInstaller.canBeResolved = true +dependencies.add(cleanroomInstaller.name, "com.cleanroommc:cleanroom:${cleanroomVersion}:installer") { + transitive = false +} + +def productionJarTask = tasks.named(propertyBool('enable_shadow') ? 'reobfShadowJar' : 'reobfJar') +def runtimeTestProductionMods = configurations.maybeCreate('runtimeTestProductionMods') +runtimeTestProductionMods.canBeConsumed = false +runtimeTestProductionMods.canBeResolved = true +runtimeTestProductionMods.extendsFrom( + configurations.getByName('modImplementation'), + configurations.getByName('modRuntimeOnly'), + configurations.getByName('modApi') +) +def mappingsAttribute = Attribute.of( + 'com.gtnewhorizons.retrofuturagradle.obfuscation', + String +) +def runtimeProductionAttributes = { + attribute(mappingsAttribute, 'srg') + attribute( + Usage.USAGE_ATTRIBUTE, + objects.named(Usage, 'java-runtime') + ) +} +runtimeTestProductionMods.attributes(runtimeProductionAttributes) + +def runtimeTestReobfMods = configurations.maybeCreate('runtimeTestReobfMods') +runtimeTestReobfMods.canBeConsumed = false +runtimeTestReobfMods.canBeResolved = true +runtimeTestReobfMods.attributes(runtimeProductionAttributes) + +def runtimeTestNamedModJars = configurations.maybeCreate('runtimeTestNamedModJars') +runtimeTestNamedModJars.canBeConsumed = false +runtimeTestNamedModJars.canBeResolved = true + +runtimeTestProductionMods.allDependencies + .withType(ExternalModuleDependency) + .each { dependency -> + if (!dependency.artifacts.any { it.classifier == 'dev' }) { + return + } + dependencies.add( + runtimeTestReobfMods.name, + "${dependency.group}:${dependency.name}:${dependency.version}" + ) { + transitive = false + } + dependencies.add( + runtimeTestNamedModJars.name, + "${dependency.group}:${dependency.name}:${dependency.version}:dev@jar" + ) { + transitive = false + } + } + +def namedRuntimeTestJarNames = providers.provider { + runtimeTestNamedModJars.files.collect { it.name } as Set +} +def isNamedRuntimeTestJar = { String fileName -> + namedRuntimeTestJarNames.get().contains(fileName) +} + +def remappedRuntimeTestModsDir = + layout.buildDirectory.dir('runtime-test/remapped-mods') +def isNamedMinecraftJar = { File jar -> + def srgNames = new HashSet() + def mcRefs = new HashSet() + def zf = new ZipFile(jar) + try { + zf.entries().each { entry -> + if (!entry.name.endsWith('.class')) { + return + } + def text = new String( + zf.getInputStream(entry).withCloseable { it.getBytes() }, + 'ISO-8859-1' + ) + def srgMatcher = text =~ /(?:func|field)_\d+_[A-Za-z0-9_]+/ + srgMatcher.each { srgNames << it } + def mcMatcher = text =~ /net\/minecraft\/[a-zA-Z0-9_\/]+/ + mcMatcher.each { mcRefs << it } + } + } finally { + zf.close() + } + if (mcRefs.isEmpty()) { + return false + } + return srgNames.size() * 2 < mcRefs.size() +} +def namedRuntimeTestJars = { + def substituted = namedRuntimeTestJarNames.get() + runtimeTestProductionMods.files.findAll { + it.name.endsWith('.jar') && + !it.name.endsWith('-sources.jar') && + !substituted.contains(it.name) && + isNamedMinecraftJar(it) + } +} + +def namedRuntimeTestRemapTasks = [] +def cleanRemappedRuntimeTestMods = tasks.register('runtimeTestCleanRemappedMods') { + def outputDir = remappedRuntimeTestModsDir.get().asFile + def expectedNames = providers.provider { + namedRuntimeTestJars().collect { it.name } as Set + } + inputs.property('expectedNames', expectedNames.map { it.sort().join(',') }) + doLast { + if (!outputDir.isDirectory()) { + return + } + def keep = expectedNames.get() + outputDir.listFiles()?.each { stale -> + if (!keep.contains(stale.name)) { + stale.delete() + } + } + } +} + +def rewriteAccessTransformer = { File mappingFile, String text -> + def fields = [:] + def methods = [:] + mappingFile.eachLine('UTF-8') { String line -> + if (line.startsWith('FD: ')) { + def parts = line.substring(4).split(' ') + if (parts.length >= 2) { + fields[parts[0]] = parts[1] + } + } else if (line.startsWith('MD: ')) { + def parts = line.substring(4).split(' ') + if (parts.length >= 4) { + methods[parts[0] + ' ' + parts[1]] = parts[2] + ' ' + parts[3] + } + } + } + def out = new StringBuilder() + text.split('\r?\n', -1).each { String raw -> + def matcher = raw =~ /^(\s*\S+\s+)(\S+)(\s+)([^#]*?)(\s*#.*)?$/ + if (!matcher.matches()) { + out.append(raw).append('\n') + return + } + def prefix = matcher.group(1) + def dotted = matcher.group(2) + def gap = matcher.group(3) + def member = matcher.group(4)?.trim() ?: '' + def comment = matcher.group(5) ?: '' + def owner = dotted.replace('.', '/') + def renamed = member + def signature = member =~ /^([A-Za-z0-9_$]+)(\(.*)$/ + if (signature.matches()) { + def key = owner + '/' + signature.group(1) + ' ' + signature.group(2) + if (methods.containsKey(key)) { + renamed = methods[key].split(' ')[0].tokenize('/').last() + + signature.group(2) + } + } else if (member && !member.contains('(')) { + def key = owner + '/' + member + if (fields.containsKey(key)) { + renamed = fields[key].tokenize('/').last() + } + } + out.append(prefix).append(dotted).append(gap).append(renamed) + .append(comment).append('\n') + } + out.toString() +} +def rewriteAccessTransformersInJar = { File mappingFile, File jar -> + def entries = [:] + def order = [] + def source = new java.util.zip.ZipFile(jar) + try { + source.entries().each { entry -> + if (entry.directory) { + return + } + order << entry.name + if (entry.name.startsWith('META-INF/') && + entry.name.endsWith('_at.cfg')) { + entries[entry.name] = rewriteAccessTransformer( + mappingFile, + new String( + source.getInputStream(entry) + .withCloseable { it.getBytes() }, + 'UTF-8' + ) + ) + } else { + entries[entry.name] = source.getInputStream(entry) + .withCloseable { it.getBytes() } + } + } + } finally { + source.close() + } + if (entries.keySet().every { !it.startsWith('META-INF/') || + !it.endsWith('_at.cfg') }) { + return false + } + def target = new java.util.zip.ZipOutputStream( + new java.io.FileOutputStream(jar) + ) + try { + order.each { name -> + def bytes = entries[name] + def entry = new java.util.zip.ZipEntry(name) + entry.time = 0L + target.putNextEntry(entry) + target.write(bytes as byte[]) + target.closeEntry() + } + } finally { + target.close() + } + return true +} + +afterEvaluate { + def remapJarType = tasks.getByName('reobfJar').class + namedRuntimeTestJars().each { jar -> + def remapTask = tasks.register( + 'runtimeTestRemap' + jar.name.replaceAll('[^A-Za-z0-9]', '_'), + remapJarType + ) { task -> + task.dependsOn cleanRemappedRuntimeTestMods + task.input = layout.file(providers.provider { jar }) + task.archiveFileName.set(jar.name) + task.destinationDirectory.set(remappedRuntimeTestModsDir) + task.libraries.from(sourceSets.main.compileClasspath) + obfuscation.configureNamedToSrgOperation(task.remapOperation) + task.doLast { + rewriteAccessTransformersInJar( + obfuscation.namedToSrgMappings.get().asFile, + task.archiveFile.get().asFile + ) + } + } + namedRuntimeTestRemapTasks << remapTask + } +} + +def remapNamedRuntimeTestMods = tasks.register('runtimeTestRemapNamedMods') { + dependsOn cleanRemappedRuntimeTestMods + dependsOn namedRuntimeTestRemapTasks + outputs.dir(remappedRuntimeTestModsDir) + doLast { + logger.lifecycle( + "[RuntimeTest] Remapped ${namedRuntimeTestRemapTasks.size()} named " + + 'jar(s) to SRG' + ) + } +} +def runtimeTestModsDir = providers.gradleProperty('runtimeTestModsDir') + .map { file(it) } + .orElse(layout.buildDirectory.dir('runtime-test/instance/mods')) +def runtimeTestMinecraftDir = providers.gradleProperty('runtimeTestMinecraftDir') + .map { file(it) } + .orElse(layout.buildDirectory.dir('runtime-test/minecraft')) +def runtimeTestSupportDir = layout.buildDirectory.dir('runtime-test/support') +def runtimeTestClientModsDir = layout.buildDirectory.dir('runtime-test/client-mods') +def runtimeMcpRemapMarker = layout.buildDirectory.file( + 'runtime-test/runtime-mcp-remap.marker' +) + +def runtimeMcpRemapTask = tasks.register('runtimeTestRemapMcp') { + dependsOn remapNamedRuntimeTestMods + inputs.files(runtimeTestProductionMods) + inputs.files(runtimeTestNamedModJars) + inputs.files(runtimeTestReobfMods) + inputs.dir(remappedRuntimeTestModsDir) + inputs.property('runtimeRemapSchema', 'moddevgradle-runtime-production-variants-v3') + outputs.file(runtimeMcpRemapMarker) + doLast { + def dropped = namedRuntimeTestJars() + def substituted = namedRuntimeTestJarNames.get() + def files = runtimeTestProductionMods.files.findAll { + it.name.endsWith('.jar') && !it.name.endsWith('-sources.jar') + }.findAll { !dropped.any { d -> d.name == it.name } } + .findAll { !substituted.contains(it.name) } + def reobfFiles = runtimeTestReobfMods.files.findAll { + it.name.endsWith('.jar') && !it.name.endsWith('-sources.jar') + } + def remappedFiles = remappedRuntimeTestModsDir.get().asFile.listFiles() + ?.findAll { it.name.endsWith('.jar') } ?: [] + runtimeMcpRemapMarker.get().asFile.with { + parentFile.mkdirs() + text = (files + reobfFiles + remappedFiles) + .collect { it.name }.sort().join('\n') + '\n' + } + logger.lifecycle( + "[RuntimeTest] Production variants: ${files.size()}, " + + "reobfuscated substitutions: ${reobfFiles.size()}, " + + "remapped: ${remappedFiles.size()}" + ) + } +} + +tasks.register('stageRuntimeTestMods', Sync) { + dependsOn productionJarTask + dependsOn runtimeMcpRemapTask + from(productionJarTask.flatMap { it.archiveFile }) + from(runtimeTestProductionMods) { + include '*.jar' + exclude '*-sources.jar' + exclude { isNamedRuntimeTestJar(it.name) } + exclude { + it.file.name.endsWith('.jar') && isNamedMinecraftJar(it.file) + } + } + from(runtimeTestReobfMods) { + include '*.jar' + exclude '*-sources.jar' + } + from(remappedRuntimeTestModsDir) { + include '*.jar' + } + into(runtimeTestModsDir) + include '*.jar' + duplicatesStrategy = DuplicatesStrategy.FAIL +} + +tasks.register('stageRuntimeTestClientMods', Sync) { + dependsOn 'stageRuntimeTestMods' + from(runtimeTestSupportDir) { + include '*.jar' + } + into(runtimeTestClientModsDir) + include '*.jar' + duplicatesStrategy = DuplicatesStrategy.FAIL +} + +tasks.register('installRuntimeTestClient', JavaExec) { + dependsOn cleanroomInstaller + dependsOn 'stageRuntimeTestClientMods' + classpath = cleanroomInstaller + mainClass = 'net.minecraftforge.installer.SimpleInstaller' + def installDir = runtimeTestMinecraftDir.get().asFile + def profile = new File( + installDir, + "versions/${minecraftVersion}-Cleanroom-${cleanroomVersion}/" + + "${minecraftVersion}-Cleanroom-${cleanroomVersion}.json" + ) + def cleanroomJar = new File( + installDir, + "libraries/com/cleanroommc/cleanroom/${cleanroomVersion}/" + + "cleanroom-${cleanroomVersion}.jar" + ) + def marker = layout.buildDirectory.file('runtime-test/cleanroom-client-installed.marker').get().asFile + onlyIf { + !marker.exists() + || marker.text.trim() != "${minecraftVersion}-Cleanroom-${cleanroomVersion}" + || !profile.isFile() + || !cleanroomJar.isFile() + } + doFirst { + installDir.mkdirs() + def launcherProfiles = new File(installDir, 'launcher_profiles.json') + if (!launcherProfiles.exists()) { + launcherProfiles.text = '{"profiles":{},"selectedProfile":""}' + } + args '--install-client', installDir.absolutePath + } + doLast { + marker.parentFile.mkdirs() + marker.text = "${minecraftVersion}-Cleanroom-${cleanroomVersion}\n" + } +} + +tasks.register('printRuntimeTestPaths') { + doLast { + println "runtimeTestModsDir=${runtimeTestModsDir.get().asFile.absolutePath}" + println "runtimeTestMinecraftDir=${runtimeTestMinecraftDir.get().asFile.absolutePath}" + println "runtimeTestProductionMods=${runtimeTestProductionMods.files*.absolutePath}" + println "runtimeTestReobfMods=${runtimeTestReobfMods.files*.absolutePath}" + println "runtimeTestNamedModJars=${runtimeTestNamedModJars.files*.absolutePath}" + } +} + +spotless { + java { + target 'src/main/java/**/*.java' + leadingTabsToSpaces() + endWithNewline() + removeUnusedImports() + } +} + +// --------------------------------------------------------------------------- +// AE2-GTIntegration additions +// --------------------------------------------------------------------------- + +// Cleanroom loads dev-time mixin configs from this system property. +// `mixin_configs` is a comma-delimited list; mirror it here. +def mixinConfigList = propertyBool('use_mixins') + ? propertyStringList('mixin_configs', ',').join(',') + : '' + +if (!mixinConfigList.isEmpty()) { + mcpForge { + runs { + configureEach { + systemProperty 'crl.dev.mixin', mixinConfigList + } + } + } + + // Forge/Cleanroom discovers production mixin configs through the jar manifest. + // The template's manifest helper does not emit this attribute. + tasks.named('jar').configure { + doFirst { + manifest { + attributes('MixinConfigs': mixinConfigList) + } + } + } +} + +// --------------------------------------------------------------------------- +// GregTech CE: Unofficial dev-time mapping bridge +// --------------------------------------------------------------------------- +// GTCEU's ASM coremod builds its SRG->MCP dictionaries at class-load time from +// two ForgeGradle-2 system properties whenever the game runs un-obfuscated: +// +// net.minecraftforge.gradle.GradleStart.srg.notch-srg : an .srg file +// net.minecraftforge.gradle.GradleStart.csvDir : a directory holding +// fields.csv + methods.csv +// +// gregtech.asm.util.ObfMapping$MCPRemapper#getConfFiles() existence-checks the +// first and actually parses the two CSVs; if any of the three paths is missing +// it throws RuntimeException("Failed to grab mappings from GradleStart args."). +// ObfMapping's static initializer runs that path only while `obfuscated` is +// false, i.e. in dev runs -- production jars are unaffected. +// +// Unimined used to inject these through its `cleanroom` extension. ModDevGradle +// ships an equivalent bridge (ForgeGradleMcpCache), but McpForgeModDevPlugin +// registers it only when CompatibilityProfile#forgeGradle2CacheEnabled() is +// true, and the Cleanroom 1.12.2 platform reports false. `populateForgeGradleMcpCache` +// is therefore never created and the properties stay unset, so GTCEU fails on +// every class transform with an ExceptionInInitializerError storm. +// +// Rebuild the wiring from ModDevGradle's own published artifacts. Do not point +// these at Unimined cache paths; those exist only on developer machines. +def createMcArtifacts = tasks.named('createMinecraftArtifacts') +def mcAdditionalResults = createMcArtifacts.flatMap { it.additionalResults } +def gtceuNotchSrg = mcAdditionalResults.map { it['notchToIntermediaryMapping'] } +def gtceuCsvZip = mcAdditionalResults.map { it['csvMapping'] } +def gtceuCsvDir = layout.buildDirectory.dir('gtceu/mcp-csv') + +// The CSV artifact is a zip (fields.csv/methods.csv/params.csv), but GTCEU +// wants a directory, so unpack the two dictionaries it reads. +def extractGtceuMcpCsv = tasks.register('extractGtceuMcpCsv') { + group = 'ae2gtaddon' + description = "Unpacks the MCP name dictionaries GTCEU's dev-time remapper reads." + def csvZipProvider = gtceuCsvZip + def outputDir = gtceuCsvDir + inputs.file(csvZipProvider) + outputs.dir(outputDir) + doLast { + File target = outputDir.get().asFile + target.mkdirs() + new java.util.zip.ZipFile(csvZipProvider.get()).withCloseable { archive -> + ['fields.csv', 'methods.csv'].each { String entryName -> + def entry = archive.getEntry(entryName) + if (entry == null) { + throw new GradleException("MCP CSVs are missing ${entryName}") + } + new File(target, entryName).withOutputStream { out -> + archive.getInputStream(entry).transferTo(out) + } + } + } + } +} + +def scriptProviders = providers +mcpForge { + runs { + configureEach { + systemProperties.putAll(scriptProviders.provider { + [ + 'net.minecraftforge.gradle.GradleStart.srg.notch-srg': gtceuNotchSrg.get().absolutePath, + 'net.minecraftforge.gradle.GradleStart.csvDir': gtceuCsvDir.get().asFile.absolutePath + ] + }) + } + } +} + +tasks.matching { it.name == 'runClient' || it.name == 'runServer' }.configureEach { + dependsOn(extractGtceuMcpCsv) +} + +def curseforgeProjectId = findProperty('curseforge_project_id') +if (curseforgeProjectId != null && !curseforgeProjectId.toString().isBlank()) { + curseforgeProjectId = curseforgeProjectId.toString().trim() + if (!curseforgeProjectId.matches('\\d+')) { + throw new GradleException( + "Property curseforge_project_id must be a numeric CurseForge project id: ${curseforgeProjectId}" + ) + } + def curseforgeReleaseType = findProperty('curseforge_release_type') + if (curseforgeReleaseType == null || curseforgeReleaseType.toString().isBlank()) { + def detectedVersion = propertyString('mod_version').toLowerCase() + if (detectedVersion.contains('alpha')) { + curseforgeReleaseType = 'alpha' + } else if (detectedVersion.contains('beta') || detectedVersion.contains('pre') || detectedVersion.contains('rc')) { + curseforgeReleaseType = 'beta' + } else { + curseforgeReleaseType = 'release' + } + } else { + curseforgeReleaseType = curseforgeReleaseType.toString().toLowerCase().trim() + } + if (!['alpha', 'beta', 'release'].contains(curseforgeReleaseType)) { + throw new GradleException( + "Property curseforge_release_type must be one of alpha, beta, release: ${curseforgeReleaseType}" + ) + } + + setDefaultProperty('curseforge_game_versions', false, '1.12.2,Forge,Java 25,Client,Server') + def curseforgeGameVersions = propertyStringList('curseforge_game_versions', ',') + .collect { it.trim() } + .findAll { !it.isBlank() } + if (curseforgeGameVersions.isEmpty()) { + throw new GradleException('Property curseforge_game_versions must define at least one CurseForge game version tag!') + } + + def curseforgeRelationTypes = [ + 'curseforge_relations_required': 'requiredDependency', + 'curseforge_relations_optional': 'optionalDependency', + 'curseforge_relations_embedded': 'embeddedLibrary', + 'curseforge_relations_incompatible': 'incompatible', + 'curseforge_relations_tool': 'tool' + ] + def curseforgeRelations = [:] + curseforgeRelationTypes.each { propertyKey, relationType -> + setDefaultProperty(propertyKey, false, '') + def slugs = propertyStringList(propertyKey) + if (!slugs.isEmpty()) { + curseforgeRelations.put(relationType, slugs) + } + } + + def curseforgeChangelogFile = null + def curseforgeChangelogProperty = findProperty('curseforge_changelog_file') + if (curseforgeChangelogProperty != null && !curseforgeChangelogProperty.toString().isBlank()) { + curseforgeChangelogFile = file(curseforgeChangelogProperty.toString()) + } else if (file('CHANGELOG.md').isFile()) { + curseforgeChangelogFile = file('CHANGELOG.md') + } + setDefaultProperty('curseforge_debug_mode', false, 'false') + + tasks.register('publishCurseForge', net.darkhax.curseforgegradle.TaskPublishCurseForge) { + dependsOn tasks.named('build') + group = 'publishing' + description = "Publishes the production mod jar to CurseForge project ${curseforgeProjectId}." + apiToken = providers.environmentVariable('CURSEFORGE_TOKEN') + debugMode = propertyBool('curseforge_debug_mode') + def mainFile = upload(curseforgeProjectId, productionJarTask.get()) + mainFile.releaseType = curseforgeReleaseType + mainFile.changelogType = 'markdown' + if (curseforgeChangelogFile != null) { + mainFile.changelog = curseforgeChangelogFile + } + curseforgeGameVersions.each { mainFile.addGameVersion(it) } + curseforgeRelations.each { relationType, slugs -> + mainFile.addRelations(relationType, slugs as String[]) + } + } + + def curseforgeAddon = file('addon.gradle') + if (curseforgeAddon.isFile()) { + apply from: curseforgeAddon + logger.lifecycle("Applied CurseForge addon script: ${curseforgeAddon}") + } +} diff --git a/gradle/scripts/helpers.gradle b/gradle/scripts/helpers.gradle new file mode 100644 index 0000000..fd0e96f --- /dev/null +++ b/gradle/scripts/helpers.gradle @@ -0,0 +1,96 @@ +import groovy.text.SimpleTemplateEngine +import org.codehaus.groovy.runtime.MethodClosure + +ext.propertyString = this.&propertyString as MethodClosure +ext.propertyBool = this.&propertyBool as MethodClosure +ext.propertyStringList = this.&propertyStringList as MethodClosure +ext.interpolate = this.&interpolate as MethodClosure +ext.assertProperty = this.&assertProperty as MethodClosure +ext.assertSubProperties = this.&assertSubProperties as MethodClosure +ext.setDefaultProperty = this.&setDefaultProperty as MethodClosure +ext.assertEnvironmentVariable = this.&assertEnvironmentVariable as MethodClosure + +String propertyString(String key) { + return $property(key).toString() +} + +boolean propertyBool(String key) { + return propertyString(key).toBoolean() +} + +Collection propertyStringList(String key) { + return propertyStringList(key, ' ') +} + +Collection propertyStringList(String key, String delimit) { + return propertyString(key).split(delimit).findAll { !it.isBlank() } +} + +private Object $property(String key) { + def value = project.findProperty(key) + if (value instanceof String) { + return interpolate(value) + } + return value +} + +String interpolate(String value) { + if (value.startsWith('${{') && value.endsWith('}}')) { + value = value.substring(3, value.length() - 2) + Binding newBinding = new Binding(this.binding.getVariables()) + newBinding.setProperty('it', this) + return new GroovyShell(this.getClass().getClassLoader(), newBinding).evaluate(value) + } + if (value.contains('${')) { + return new SimpleTemplateEngine().createTemplate(value).make(project.properties).toString() + } + return value +} + +void assertProperty(String propertyName) { + def property = property(propertyName) + if (property == null) { + throw new GradleException("Property ${propertyName} is not defined!") + } + if (property.isEmpty()) { + throw new GradleException("Property ${propertyName} is empty!") + } +} + +void assertSubProperties(String propertyName, String... subPropertyNames) { + assertProperty(propertyName) + if (propertyBool(propertyName)) { + for (String subPropertyName : subPropertyNames) { + assertProperty(subPropertyName) + } + } +} + +void setDefaultProperty(String propertyName, boolean warn, defaultValue) { + def property = property(propertyName) + def exists = true + if (property == null) { + exists = false + if (warn) { + project.logger.log(LogLevel.WARN, "Property ${propertyName} is not defined!") + } + } else if (property.isEmpty()) { + exists = false + if (warn) { + project.logger.log(LogLevel.WARN, "Property ${propertyName} is empty!") + } + } + if (!exists) { + project.setProperty(propertyName, defaultValue.toString()) + } +} + +void assertEnvironmentVariable(String propertyName) { + def property = System.getenv(propertyName) + if (property == null) { + throw new GradleException("System Environment Variable $propertyName is not defined!") + } + if (property.isEmpty()) { + throw new GradleException("Property $propertyName is empty!") + } +} diff --git a/gradle/scripts/publishing.gradle b/gradle/scripts/publishing.gradle index 94a8e45..4154cd0 100644 --- a/gradle/scripts/publishing.gradle +++ b/gradle/scripts/publishing.gradle @@ -1,25 +1,169 @@ -if (publish_to_maven.toBoolean()) { +import groovy.util.Node +import groovy.xml.XmlSlurper + +apply from: 'gradle/scripts/helpers.gradle' + +private static boolean isPomNodeNamed(Object node, String localName) { + if (!(node instanceof Node)) { + return false + } + def nodeName = node.name().toString() + return nodeName == localName + || nodeName.endsWith("}" + localName) + || nodeName.endsWith(":" + localName) +} + +private static void removePomChildren(Node pomNode, String localName) { + pomNode.children().removeAll { child -> + isPomNodeNamed(child, localName) + } +} + +setDefaultProperty('publish_to_maven', true, false) +setDefaultProperty('publish_to_jitpack', false, false) +setDefaultProperty('maven_group', false, propertyString('root_package')) +setDefaultProperty('maven_artifact_id', false, propertyString('mod_id')) +setDefaultProperty('maven_developer_id', false, '') +setDefaultProperty('maven_developer_name', false, '') +setDefaultProperty('maven_license_name', false, propertyString('mod_license')) +setDefaultProperty('maven_license_url', false, '') +setDefaultProperty('maven_scm_url', false, propertyString('mod_url')) +setDefaultProperty('jitpack_group', false, '') +setDefaultProperty('jitpack_artifact_id', false, '') +setDefaultProperty('jitpack_version', false, '') + +def publishMainJarTask = propertyBool('enable_shadow') ? 'reobfShadowJar' : 'reobfJar' + +def configureMavenJavaPublication = { MavenPublication publication, String groupId, String artifactId, String publicationVersion -> + def scmUrl = propertyString('maven_scm_url') + def scmConnectionUrl = scmUrl.endsWith('.git') ? scmUrl : "${scmUrl}.git" + + publication.from components.java + // MDG replaces the Java component's development variant with the remapped jar. + // Publish both explicitly, and exclude the Shadow plugin's optional variants. + publication.setArtifacts([tasks.named(publishMainJarTask), tasks.named('jar')]) + if (propertyBool('generate_sources_jar')) { + publication.artifact(tasks.named('sourcesJar')) + } + if (propertyBool('generate_javadocs_jar')) { + publication.artifact(tasks.named('javadocJar')) + } + publication.setGroupId(groupId) + publication.setArtifactId(artifactId) + publication.setVersion(publicationVersion) + + publication.pom { + name = propertyString('mod_name') + description = propertyString('mod_description') + url = propertyString('mod_url') + licenses { + license { + name = propertyString('maven_license_name') + url = propertyString('maven_license_url') + } + } + developers { + developer { + id = propertyString('maven_developer_id') + name = propertyString('maven_developer_name') + } + } + scm { + url = scmUrl + connection = "scm:git:${scmConnectionUrl}" + developerConnection = "scm:git:${scmConnectionUrl}" + } + } + publication.pom.withXml { + removePomChildren(it.asNode(), 'dependencies') + } +} + +tasks.withType(GenerateModuleMetadata).configureEach { + enabled = false +} + +if (propertyBool('publish_to_maven')) { + assertProperty('maven_name') + assertProperty('maven_url') + assertProperty('maven_group') + assertProperty('maven_artifact_id') + assertProperty('maven_developer_id') + assertProperty('maven_developer_name') + assertProperty('maven_license_name') + assertProperty('maven_license_url') + assertProperty('maven_scm_url') + publishing { repositories { maven { - name = maven_name.replaceAll("\\s", "") - url = maven_url + name propertyString('maven_name').replaceAll("\\s", "") + url propertyString('maven_url') credentials(PasswordCredentials) } } publications { mavenJava(MavenPublication) { - from components.java // Publish with standard artifacts - setGroupId(root_package)// Publish with root package as maven group - setArtifactId(mod_id) // Publish artifacts with mod id as the artifact id + configureMavenJavaPublication(it, propertyString('maven_group'), propertyString('maven_artifact_id'), project.version.toString()) + } + } + } + + def verifyMavenPomHasNoDependencies = tasks.register('verifyMavenPomHasNoDependencies') { + def generatedPom = layout.buildDirectory.file('publications/mavenJava/pom-default.xml') + dependsOn tasks.named('generatePomFileForMavenJavaPublication') + inputs.file(generatedPom) + doLast { + def pom = new XmlSlurper(false, false).parse(generatedPom.get().asFile) + def dependenciesNode = pom.dependencies + if (dependenciesNode.size() > 0) { + def dependencies = dependenciesNode.dependency.collect { dependency -> + "${dependency.groupId.text()}:${dependency.artifactId.text()}:${dependency.version.text()}" + }.join(', ') + throw new GradleException("Maven publication POM must not contain dependencies section: ${dependencies}") + } + } + } + tasks.matching { task -> + task.name == 'publishMavenJavaPublicationToMavenLocal' + || task.name.startsWith('publishMavenJavaPublicationTo') + }.configureEach { + dependsOn verifyMavenPomHasNoDependencies + } +} + +if (propertyBool('publish_to_jitpack')) { + assertProperty('jitpack_group') + assertProperty('jitpack_artifact_id') + assertProperty('jitpack_version') + assertProperty('maven_scm_url') + + publishing { + repositories { + maven { + name = 'jitpack' + url = layout.buildDirectory.dir('jitpack-repository') + } + } + publications { + mavenJava(MavenPublication) { + configureMavenJavaPublication(it, propertyString('jitpack_group'), propertyString('jitpack_artifact_id'), propertyString('jitpack_version')) + } + } + } - // Custom artifact: - // If you want to publish a different artifact to the one outputted when building normally - // Create a different gradle task (Jar task), in extra.gradle - // Remove the 'from components.java' line above - // Add this line (change the task name): - // artifacts task_name + def verifyJitpackMavenPomHasNoDependencies = tasks.register('verifyJitpackMavenPomHasNoDependencies') { + def generatedPom = layout.buildDirectory.file('publications/mavenJava/pom-default.xml') + dependsOn tasks.named('generatePomFileForMavenJavaPublication') + inputs.file(generatedPom) + doLast { + def pom = new XmlSlurper(false, false).parse(generatedPom.get().asFile) + if (pom.dependencies.size() > 0) { + throw new GradleException('JitPack Maven publication POM must not contain a dependencies section.') } } } + tasks.named('publishMavenJavaPublicationToJitpackRepository').configure { + dependsOn verifyJitpackMavenPomHasNoDependencies + } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 5dd3c01..6e9b360 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,11 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.1-bin.zip networkTimeout=10000 +retries=0 +retryBackOffMs=500 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists + +org.gradle.configuration-cache=true diff --git a/settings.gradle b/settings.gradle index 0191728..9a230bf 100644 --- a/settings.gradle +++ b/settings.gradle @@ -9,20 +9,27 @@ pluginManagement { maven { url = 'https://maven.minecraftforge.net/' } - maven { - url = 'https://maven.fabricmc.net/' - } - maven { - url = 'https://maven.wagyourtail.xyz/releases' - } - maven { - url = 'https://maven.arcseekers.com/releases' + } + resolutionStrategy { + eachPlugin { + if (requested.id.namespace == 'net.neoforged.moddev') { + useModule("io.github.ruixuqi:moddev-gradle:${requested.version}") + } } + } +} - maven { - url = 'https://maven.wagyourtail.xyz/snapshots' +buildscript { + repositories { + gradlePluginPortal() { + content { + excludeGroup('org.apache.logging.log4j') + } } } + dependencies { + classpath 'net.darkhax.curseforgegradle:CurseForgeGradle:1.3.33' + } } plugins { diff --git a/src/main/java/ae2/container/me/patternencode/ExtendedProviderSelectionBridge.java b/src/main/java/ae2/container/me/patternencode/ExtendedProviderSelectionBridge.java new file mode 100644 index 0000000..92edea9 --- /dev/null +++ b/src/main/java/ae2/container/me/patternencode/ExtendedProviderSelectionBridge.java @@ -0,0 +1,184 @@ +package ae2.container.me.patternencode; + +import ae2.api.networking.IGrid; +import ae2.container.me.patternencode.PatternProviderUploadService.ProcessingPatternUploadPreparation; +import ae2.container.me.patternencode.PatternProviderUploadService.ProcessingPatternUploadResult; +import ae2.container.me.patternencode.ProviderSelectionSession.ProviderEntryAction; +import ae2.container.me.patternencode.ProviderSelectionSession.ProviderMappingAction; +import ae2.container.me.patternencode.ProviderSelectionSession.ProviderSelectionOverlayOpenRequest; +import ae2.core.worlddata.PatternProviderMappingData; +import ae2.me.service.ActivePatternProviderDirectory.ProviderKey; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.ItemStack; +import net.minecraft.world.World; +import org.jetbrains.annotations.Nullable; + +public final class ExtendedProviderSelectionBridge { + private final ProviderSelectionSession session; + + private ExtendedProviderSelectionBridge(Host host) { + this.session = new ProviderSelectionSession(new ProviderSelectionSession.Host() { + @Override + public boolean isLinkConnected() { + return host.isLinkConnected(); + } + + @Override + public @Nullable IGrid getProviderSelectionGrid() { + return host.getProviderSelectionGrid(); + } + + @Override + public @Nullable IGrid requireProviderSelectionGrid() { + return host.requireProviderSelectionGrid(); + } + + @Override + public PatternProviderMappingData getProviderMappingData() { + return host.getProviderMappingData(); + } + + @Override + public EntityPlayer getPlayer() { + return host.getPlayer(); + } + + @Override + public World getWorld() { + return host.getWorld(); + } + + @Override + public int getWindowId() { + return host.getWindowId(); + } + + @Override + public ItemStack getEncodedPattern() { + return host.getEncodedPattern(); + } + + @Override + public void setEncodedPattern(ItemStack encodedPattern) { + host.setEncodedPattern(encodedPattern); + } + + @Override + public ProcessingPatternUploadResult uploadProcessingPattern(ItemStack encodedPattern, IGrid grid, + ProviderKey target) { + return host.uploadProcessingPattern(encodedPattern, grid, target); + } + + @Override + public ProcessingPatternUploadPreparation prepareProcessingPatternUpload(ItemStack encodedPattern, + IGrid grid, + ProviderKey target) { + return host.prepareProcessingPatternUpload(encodedPattern, grid, target); + } + + @Override + public void sendProviderDirectoryPage(ProviderDirectoryPage page) { + host.sendProviderDirectoryPage(page); + } + + @Override + public void sendProviderMappingPage(ProviderMappingPage page) { + host.sendProviderMappingPage(page); + } + + @Override + public void syncProviderDirectoryRevision(long revision) { + host.syncProviderDirectoryRevision(revision); + } + + @Override + public void syncProviderSelectionOverlay(ProviderSelectionOverlayOpenRequest request) { + host.syncProviderSelectionOverlay(request.nonce(), request.searchText(), request.mappingText()); + } + }); + } + + public static ExtendedProviderSelectionBridge create(Host host) { + return new ExtendedProviderSelectionBridge(host); + } + + public interface Host { + boolean isLinkConnected(); + + @Nullable + IGrid getProviderSelectionGrid(); + + @Nullable + IGrid requireProviderSelectionGrid(); + + PatternProviderMappingData getProviderMappingData(); + + EntityPlayer getPlayer(); + + World getWorld(); + + int getWindowId(); + + ItemStack getEncodedPattern(); + + void setEncodedPattern(ItemStack encodedPattern); + + ProcessingPatternUploadResult uploadProcessingPattern(ItemStack encodedPattern, IGrid grid, + ProviderKey target); + + ProcessingPatternUploadPreparation prepareProcessingPatternUpload(ItemStack encodedPattern, IGrid grid, + ProviderKey target); + + void sendProviderDirectoryPage(ProviderDirectoryPage page); + + void sendProviderMappingPage(ProviderMappingPage page); + + void syncProviderDirectoryRevision(long revision); + + void syncProviderSelectionOverlay(long nonce, String searchText, String mappingText); + } + + public void onBroadcastChanges() { + this.session.onBroadcastChanges(); + } + + public long getDirectoryRevision() { + return this.session.getDirectoryRevision(); + } + + public void open(@Nullable String searchText, @Nullable String mappingText) { + this.session.open(searchText, mappingText); + } + + public void close() { + this.session.close(); + } + + public void requestProviderDirectoryPage(@Nullable ProviderDirectoryPageRequest request) { + this.session.requestProviderDirectoryPage(request); + } + + public void requestProviderMappingPage(@Nullable ProviderMappingPageRequest request) { + this.session.requestProviderMappingPage(request); + } + + public void uploadProcessingPatternToProvider(@Nullable ProviderEntryAction action) { + this.session.uploadProcessingPatternToProvider(action); + } + + public void bindProviderMapping(@Nullable ProviderMappingAction action) { + this.session.bindProviderMapping(action); + } + + public void bindAndUploadProcessingPatternToProvider(@Nullable ProviderMappingAction action) { + this.session.bindAndUploadProcessingPatternToProvider(action); + } + + public void unbindProviderMapping(@Nullable ProviderMappingAction action) { + this.session.unbindProviderMapping(action); + } + + public void rebuildMappingsFromActiveProviders() { + this.session.rebuildMappingsFromActiveProviders(); + } +} diff --git a/src/main/java/com/formlessDragon/ae2gtaddon/AEGTAddon.java b/src/main/java/com/formlessDragon/ae2gtaddon/AEGTAddon.java index 85b42d6..272ed3e 100644 --- a/src/main/java/com/formlessDragon/ae2gtaddon/AEGTAddon.java +++ b/src/main/java/com/formlessDragon/ae2gtaddon/AEGTAddon.java @@ -1,18 +1,34 @@ package com.formlessDragon.ae2gtaddon; +import com.formlessDragon.ae2gtaddon.container.ExtendedEncodingTermGuiHandler; import com.formlessDragon.ae2gtaddon.proxy.IProxy; +import net.minecraftforge.common.config.Config; +import net.minecraftforge.common.config.ConfigManager; import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.common.SidedProxy; import net.minecraftforge.fml.common.event.FMLInitializationEvent; +import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; +import net.minecraftforge.fml.common.network.NetworkRegistry; -@Mod(modid = Tags.MOD_ID, name = Tags.MOD_NAME, version = Tags.VERSION) +@Mod(modid = Tags.MOD_ID, name = Tags.MOD_NAME, version = Tags.VERSION, dependencies = "after:ae2") public class AEGTAddon { + public static AEGTAddon instance; @SidedProxy(modId = Tags.MOD_ID, clientSide = "com.formlessDragon.ae2gtaddon.proxy.ClientProxy", serverSide = "com.formlessDragon.ae2gtaddon.proxy.CommonProxy") public static IProxy proxy; + public AEGTAddon() { + instance = this; + } + + @Mod.EventHandler + public void preInit(FMLPreInitializationEvent event) { + ConfigManager.sync(Tags.MOD_ID, Config.Type.INSTANCE); + } + @Mod.EventHandler public void init(FMLInitializationEvent event) { + NetworkRegistry.INSTANCE.registerGuiHandler(this, new ExtendedEncodingTermGuiHandler()); proxy.init(); } diff --git a/src/main/java/com/formlessDragon/ae2gtaddon/AEGTAddonConfig.java b/src/main/java/com/formlessDragon/ae2gtaddon/AEGTAddonConfig.java new file mode 100644 index 0000000..1f69a3c --- /dev/null +++ b/src/main/java/com/formlessDragon/ae2gtaddon/AEGTAddonConfig.java @@ -0,0 +1,12 @@ +package com.formlessDragon.ae2gtaddon; + +import net.minecraftforge.common.config.Config; + +@Config(modid = Tags.MOD_ID, name = Tags.MOD_NAME) +public final class AEGTAddonConfig { + + private AEGTAddonConfig() {} + + @Config.Comment("Keep item name when transferring recipe from jei") + public static boolean autoNamingKeepsItemName = false; +} diff --git a/src/main/java/com/formlessDragon/ae2gtaddon/ModMixinConfigPlugin.java b/src/main/java/com/formlessDragon/ae2gtaddon/MixinConfigPlugin.java similarity index 55% rename from src/main/java/com/formlessDragon/ae2gtaddon/ModMixinConfigPlugin.java rename to src/main/java/com/formlessDragon/ae2gtaddon/MixinConfigPlugin.java index c1fe97d..7f6fdf0 100644 --- a/src/main/java/com/formlessDragon/ae2gtaddon/ModMixinConfigPlugin.java +++ b/src/main/java/com/formlessDragon/ae2gtaddon/MixinConfigPlugin.java @@ -8,7 +8,7 @@ import java.util.List; import java.util.Set; -public class ModMixinConfigPlugin implements IMixinConfigPlugin { +public class MixinConfigPlugin implements IMixinConfigPlugin { @Override public void onLoad(String s) { @@ -19,21 +19,19 @@ public String getRefMapperConfig() { return null; } - /** - * An example of mod mixin - * The {@link org.spongepowered.asm.mixin.MixinEnvironment.Phase#MOD} allow the mixins being processed after modlist building - * Which allow calling {@link Loader#isModLoaded(String)} - * @param targetClassName Not important unless you are writing multi-target mixin - * @param mixinClassName The full mixin class name. Filtering with group name is the easiest solution here. - * @return If the mixin should apply - */ @Override public boolean shouldApplyMixin(String targetClassName, String mixinClassName) { - return switch (mixinClassName.split("\\.")[5]) { - case "hei" -> Loader.isModLoaded("jei"); - case "ae2" -> Loader.isModLoaded("ae2"); - default -> true; - }; + String[] packageParts = mixinClassName.split("\\."); + if (packageParts.length > 5) { + String integration = packageParts[5]; + if ("hei".equals(integration)) { + return Loader.isModLoaded("jei"); + } + if ("ae2".equals(integration)) { + return Loader.isModLoaded("ae2"); + } + } + return true; } @Override diff --git a/src/main/java/com/formlessDragon/ae2gtaddon/client/gui/ExtendedEncodingModePanel.java b/src/main/java/com/formlessDragon/ae2gtaddon/client/gui/ExtendedEncodingModePanel.java new file mode 100644 index 0000000..8adc4d4 --- /dev/null +++ b/src/main/java/com/formlessDragon/ae2gtaddon/client/gui/ExtendedEncodingModePanel.java @@ -0,0 +1,60 @@ +package com.formlessDragon.ae2gtaddon.client.gui; + +import ae2.client.Point; +import ae2.client.gui.ICompositeWidget; +import ae2.client.gui.Icon; +import ae2.client.gui.WidgetContainer; +import com.formlessDragon.ae2gtaddon.container.ContainerExtendedEncodingTerm; +import net.minecraft.util.text.ITextComponent; + +import java.awt.Rectangle; + +abstract class ExtendedEncodingModePanel implements ICompositeWidget { + protected final GuiExtendedPatternEncodingTerm screen; + protected final ContainerExtendedEncodingTerm container; + protected final WidgetContainer widgets; + protected Point position = Point.ZERO; + protected int width; + protected int height; + protected boolean visible; + + ExtendedEncodingModePanel(GuiExtendedPatternEncodingTerm screen, WidgetContainer widgets) { + this.screen = screen; + this.container = screen.getContainer(); + this.widgets = widgets; + } + + @Override + public boolean isVisible() { + return this.visible; + } + + public void setVisible(boolean visible) { + this.visible = visible; + } + + @Override + public void setPosition(Point position) { + this.position = position; + } + + @Override + public void setSize(int width, int height) { + this.width = width; + this.height = height; + } + + @Override + public Rectangle getBounds() { + return new Rectangle(this.position.x(), this.position.y(), this.width, this.height); + } + + @Override + public boolean hitTest(Point mousePos) { + return false; + } + + abstract Icon getIcon(); + + public abstract ITextComponent getTabTooltip(); +} diff --git a/src/main/java/com/formlessDragon/ae2gtaddon/client/gui/ExtendedProcessingEncodingPanel.java b/src/main/java/com/formlessDragon/ae2gtaddon/client/gui/ExtendedProcessingEncodingPanel.java new file mode 100644 index 0000000..e994dce --- /dev/null +++ b/src/main/java/com/formlessDragon/ae2gtaddon/client/gui/ExtendedProcessingEncodingPanel.java @@ -0,0 +1,347 @@ +package com.formlessDragon.ae2gtaddon.client.gui; + +import ae2.api.config.ActionItems; +import ae2.client.Point; +import ae2.client.gui.Icon; +import ae2.client.gui.Tooltip; +import ae2.client.gui.WidgetContainer; +import ae2.client.gui.style.Blitter; +import ae2.client.gui.widgets.ActionButton; +import ae2.client.gui.widgets.Scrollbar; +import ae2.client.gui.widgets.SmallTextTooltipButton; +import ae2.client.gui.widgets.ToggleButton; +import ae2.container.SlotSemantics; +import ae2.core.localization.GuiText; +import ae2.core.localization.Tooltips; +import ae2.integration.Integrations; +import ae2.parts.encoding.ProcessingPatternAmountHelper; +import com.formlessDragon.ae2gtaddon.parts.ExtendedEncodingMode; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.text.ITextComponent; +import net.minecraft.util.text.Style; +import net.minecraft.util.text.TextComponentTranslation; +import net.minecraft.util.text.TextFormatting; + +import java.awt.Rectangle; +import java.util.List; + +final class ExtendedProcessingEncodingPanel extends ExtendedEncodingModePanel { + private static final int PANEL_WIDTH = 131; + private static final int PANEL_HEIGHT = 82; + private static final int VISIBLE_ROWS = 4; + private static final int SLOT_SPACING = 18; + private static final int INPUT_SLOT_X = 11; + private static final int SLOT_Y = 7; + private static final int MULTI_IN_OUTPUT_SLOT_X = 108; + private static final int MULTI_OUT_OUTPUT_SLOT_X = 55; + private static final float HALF_SIZE_ICON_SCALE = 0.5F; + private static final Rectangle MOUSE_WHEEL_AREA = new Rectangle(-1, 1, PANEL_WIDTH, PANEL_HEIGHT); + + private final ExtendedEncodingMode mode; + private final Blitter background; + private final ActionButton clearBtn; + private final ActionButton cycleOutputBtn; + private final ActionButton clearSecondaryOutputsBtn; + private final Scrollbar scrollbar; + private final SmallTextTooltipButton[] amountButtons; + private final ToggleButton ignoreCircuitButton; + private final ToggleButton mergeHeiInputsBtn; + private final ToggleButton autoNameButton; + + ExtendedProcessingEncodingPanel(GuiExtendedPatternEncodingTerm screen, WidgetContainer widgets, + ExtendedEncodingMode mode) { + super(screen, widgets); + this.mode = mode; + this.background = Blitter.texture(new ResourceLocation( + "ae2gtaddon", "textures/guis/" + mode.name().toLowerCase(java.util.Locale.ROOT).replace('_', '-') + ".png"), + PANEL_WIDTH, PANEL_HEIGHT).src(0, 0, PANEL_WIDTH, PANEL_HEIGHT); + this.clearBtn = new ActionButton(ActionItems.S_CLOSE, this.container::clear); + this.clearBtn.setHalfSize(true); + this.clearBtn.setDisableBackground(true); + widgets.add(widgetId("processingClearPattern"), this.clearBtn); + + this.cycleOutputBtn = new ActionButton(ActionItems.S_CYCLE_PROCESSING_OUTPUT, + this.container::cycleProcessingOutput); + this.cycleOutputBtn.setHalfSize(true); + this.cycleOutputBtn.setDisableBackground(true); + widgets.add(widgetId("processingCycleOutput"), this.cycleOutputBtn); + + this.clearSecondaryOutputsBtn = new ActionButton(ActionItems.S_CLEAR_PROCESSING_SECONDARY_OUTPUTS, + this.container::clearProcessingSecondaryOutputs); + this.clearSecondaryOutputsBtn.setHalfSize(true); + this.clearSecondaryOutputsBtn.setDisableBackground(true); + widgets.add(widgetId("processingClearSecondaryOutputs"), this.clearSecondaryOutputsBtn); + + this.scrollbar = widgets.addScrollBar(widgetId("processingPatternModeScrollbar"), Scrollbar.SMALL); + this.scrollbar.setRange(0, getScrollableRows(), VISIBLE_ROWS); + this.scrollbar.setVisible(false); + this.scrollbar.setCaptureMouseWheel(false); + + this.amountButtons = new SmallTextTooltipButton[]{ + amountButton("/2", ProcessingPatternAmountHelper.Operation.DIVIDE_2, GuiText.ProcessingPatternDivideDescription), + amountButton("x2", ProcessingPatternAmountHelper.Operation.MULTIPLY_2, + GuiText.ProcessingPatternMultiplyDescription), + amountButton("/3", ProcessingPatternAmountHelper.Operation.DIVIDE_3, GuiText.ProcessingPatternDivideDescription), + amountButton("x3", ProcessingPatternAmountHelper.Operation.MULTIPLY_3, + GuiText.ProcessingPatternMultiplyDescription), + amountButton("/5", ProcessingPatternAmountHelper.Operation.DIVIDE_5, GuiText.ProcessingPatternDivideDescription), + amountButton("x5", ProcessingPatternAmountHelper.Operation.MULTIPLY_5, + GuiText.ProcessingPatternMultiplyDescription) + }; + for (int index = 0; index < this.amountButtons.length; index++) { + widgets.add(widgetId("processingAmountButton" + index), this.amountButtons[index]); + } + + if (Integrations.hei().isEnabled()) { + this.ignoreCircuitButton = new ToggleButton( + Icon.S_SUBSTITUTION_DISABLED, + Icon.S_SUBSTITUTION_ENABLED, + this.container::setIgnoreCircuit); + this.ignoreCircuitButton.setHalfSize(true); + this.ignoreCircuitButton.setDisableBackground(true); + this.ignoreCircuitButton.setTooltipOn(List.of( + new TextComponentTranslation("gui.tooltips.ae2gtaddon.IgnoreCircuitHeiImport"), + new TextComponentTranslation("gui.tooltips.ae2gtaddon.IgnoreCircuitHeiImportOn"))); + this.ignoreCircuitButton.setTooltipOff(List.of( + new TextComponentTranslation("gui.tooltips.ae2gtaddon.IgnoreCircuitHeiImport"), + new TextComponentTranslation("gui.tooltips.ae2gtaddon.IgnoreCircuitHeiImportOff"))); + widgets.add(widgetId("processingIgnoreCircuit"), this.ignoreCircuitButton); + + this.mergeHeiInputsBtn = new ToggleButton( + Icon.S_MERGE_ENABLED, + Icon.S_MERGE_DISABLED, + this.container::setMergeHeiProcessingInputs + ); + this.mergeHeiInputsBtn.setHalfSize(true); + this.mergeHeiInputsBtn.setDisableBackground(true); + this.mergeHeiInputsBtn.setTooltipOn(List.of( + Tooltips.HeiProcessingMerge.text(), + Tooltips.HeiProcessingMergeDescEnabled.text())); + this.mergeHeiInputsBtn.setTooltipOff(List.of( + Tooltips.HeiProcessingMerge.text(), + Tooltips.HeiProcessingMergeDescDisabled.text())); + widgets.add(widgetId("processingMergeHeiInputs"), this.mergeHeiInputsBtn); + + this.autoNameButton = new ToggleButton( + Icon.PATTERN_AUTO_FILL_ON, + Icon.PATTERN_AUTO_FILL_OFF, + this::setAutoNamingItems + ); + this.autoNameButton.setHalfSize(true); + this.autoNameButton.setDisableBackground(true); + this.autoNameButton.setIconScale(HALF_SIZE_ICON_SCALE); + this.autoNameButton.setTooltipOn(List.of( + new TextComponentTranslation("gui.tooltips.ae2gtaddon.AutoNameHeiImport"), + new TextComponentTranslation("gui.tooltips.ae2gtaddon.AutoNameHeiImportOn"))); + this.autoNameButton.setTooltipOff(List.of( + new TextComponentTranslation("gui.tooltips.ae2gtaddon.AutoNameHeiImport"), + new TextComponentTranslation("gui.tooltips.ae2gtaddon.AutoNameHeiImportOff"))); + widgets.add(widgetId("processingAutoNaming"), this.autoNameButton); + } else { + this.ignoreCircuitButton = null; + this.mergeHeiInputsBtn = null; + this.autoNameButton = null; + } + } + + private void setAutoNamingItems(boolean autoName) { + this.container.setAutoNamingItems(autoName); + if (autoName) { + // Auto naming keeps every occurrence in its own slot, so use the existing HEI merge switch to match. + this.container.setMergeHeiProcessingInputs(false); + } + } + + @Override + public void updateBeforeRender() { + if (!this.visible) { + return; + } + this.screen.repositionSlots(SlotSemantics.PROCESSING_INPUTS); + this.screen.repositionSlots(SlotSemantics.PROCESSING_OUTPUTS); + repositionProcessingSlots(); + + this.cycleOutputBtn.setVisibility(this.visible && this.container.canCycleProcessingOutputs()); + this.clearSecondaryOutputsBtn.setVisibility(this.visible && this.container.canCycleProcessingOutputs()); + if (this.ignoreCircuitButton != null) { + this.ignoreCircuitButton.setState(this.container.ignoreCircuit()); + } + if (this.mergeHeiInputsBtn != null) { + this.mergeHeiInputsBtn.setState(this.container.isMergeHeiProcessingInputs()); + } + if (this.autoNameButton != null) { + this.autoNameButton.setState(this.container.isAutoNamingItems()); + } + } + + private void repositionProcessingSlots() { + var inputSlots = this.container.getProcessingInputSlots(); + var outputSlots = this.container.getProcessingOutputSlots(); + if (inputSlots.length == 0 || outputSlots.length == 0) { + return; + } + + int scroll = this.scrollbar.getCurrentScroll(); + Point inputOrigin = this.position.move(INPUT_SLOT_X, SLOT_Y); + Point outputOrigin = this.position.move( + this.mode == ExtendedEncodingMode.MULTI_IN ? MULTI_IN_OUTPUT_SLOT_X : MULTI_OUT_OUTPUT_SLOT_X, + SLOT_Y); + repositionSlots(inputSlots, inputOrigin, this.mode.getInputSlots(), columnsForInputs(), scroll); + repositionSlots(outputSlots, outputOrigin, this.mode.getOutputSlots(), columnsForOutputs(), scroll); + } + + private int getScrollableRows() { + int inputColumns = columnsForInputs(); + int outputColumns = columnsForOutputs(); + int inputRows = (this.mode.getInputSlots() + inputColumns - 1) / inputColumns; + int outputRows = (this.mode.getOutputSlots() + outputColumns - 1) / outputColumns; + return Math.max(0, Math.max(inputRows, outputRows) - VISIBLE_ROWS); + } + + private static void repositionSlots(ae2.container.slot.FakeSlot[] slots, Point origin, int activeSlots, + int columns, int scroll) { + for (int i = 0; i < slots.length; i++) { + int row = i / columns; + int effectiveRow = row - scroll; + slots[i].xPos = origin.x() + (i % columns) * SLOT_SPACING; + slots[i].yPos = origin.y() + effectiveRow * SLOT_SPACING; + slots[i].setActive(i < activeSlots && effectiveRow >= 0 && effectiveRow < VISIBLE_ROWS); + } + } + + private int columnsForInputs() { + return this.mode == ExtendedEncodingMode.MULTI_IN ? 4 : 1; + } + + private int columnsForOutputs() { + return this.mode == ExtendedEncodingMode.MULTI_IN ? 1 : 4; + } + + @Override + public void drawBackgroundLayer(Rectangle bounds, Point mouse) { + this.background.dest( + bounds.x + this.position.x() - 1, + bounds.y + this.position.y() + 1, + PANEL_WIDTH, + PANEL_HEIGHT).blit(); + } + + @Override + Icon getIcon() { + return Icon.TAB_PROCESSING; + } + + @Override + public ITextComponent getTabTooltip() { + return new TextComponentTranslation(this.mode == ExtendedEncodingMode.MULTI_IN + ? "gui.ae2gtaddon.MultiIn" + : "gui.ae2gtaddon.MultiOut"); + } + + @Override + public void setVisible(boolean visible) { + super.setVisible(visible); + this.scrollbar.setVisible(visible); + this.clearBtn.setVisibility(visible); + this.cycleOutputBtn.setVisibility(visible && this.container.canCycleProcessingOutputs()); + this.clearSecondaryOutputsBtn.setVisibility(visible && this.container.canCycleProcessingOutputs()); + if (this.ignoreCircuitButton != null) { + this.ignoreCircuitButton.setVisibility(visible); + } + if (this.mergeHeiInputsBtn != null) { + this.mergeHeiInputsBtn.setVisibility(visible); + } + if (this.autoNameButton != null) { + this.autoNameButton.setVisibility(visible); + } + for (SmallTextTooltipButton button : this.amountButtons) { + button.visible = visible; + button.enabled = visible; + } + } + + @Override + public boolean onMouseWheel(Point mousePos, double delta) { + if (!getMouseWheelArea().contains(mousePos.x(), mousePos.y())) { + return false; + } + return this.scrollbar.onMouseWheel(mousePos, delta); + } + + @Override + public boolean wantsAllMouseWheelEvents() { + return true; + } + + private Rectangle getMouseWheelArea() { + return new Rectangle( + this.position.x() + MOUSE_WHEEL_AREA.x, + this.position.y() + MOUSE_WHEEL_AREA.y, + MOUSE_WHEEL_AREA.width, + MOUSE_WHEEL_AREA.height); + } + + @Override + public boolean wantsAllMouseDownEvents() { + return true; + } + + @Override + public Tooltip getTooltip(int mouseX, int mouseY) { + if (this.ignoreCircuitButton != null + && this.ignoreCircuitButton.visible + && this.ignoreCircuitButton.isMouseOver()) { + return new Tooltip(this.ignoreCircuitButton.getTooltipMessage()); + } + return getEmptyOutputTooltip(mouseX, mouseY); + } + + private Tooltip getEmptyOutputTooltip(int mouseX, int mouseY) { + int columns = columnsForOutputs(); + int scroll = this.scrollbar.getCurrentScroll(); + var outputSlots = this.container.getProcessingOutputSlots(); + for (int slotIndex = 0; slotIndex < this.mode.getOutputSlots(); slotIndex++) { + int effectiveRow = slotIndex / columns - scroll; + if (effectiveRow < 0 || effectiveRow >= VISIBLE_ROWS) { + continue; + } + + var slot = outputSlots[slotIndex]; + if (slot.getStack().isEmpty() + && new Rectangle(slot.xPos, slot.yPos, SLOT_SPACING, SLOT_SPACING).contains(mouseX, mouseY)) { + boolean primary = slotIndex == 0; + String tooltipKey = primary + ? "gui.ae2.PatternEncoding.primary_processing_result_tooltip" + : "gui.ae2.PatternEncoding.secondary_processing_result_tooltip"; + String hintKey = primary + ? "gui.ae2.PatternEncoding.primary_processing_result_hint" + : "gui.ae2.PatternEncoding.secondary_processing_result_hint"; + return new Tooltip(List.of( + new TextComponentTranslation(tooltipKey), + new TextComponentTranslation(hintKey) + .setStyle(new Style().setColor(TextFormatting.DARK_GRAY)))); + } + } + return null; + } + + private SmallTextTooltipButton amountButton(String label, ProcessingPatternAmountHelper.Operation operation, + GuiText tooltip) { + return new SmallTextTooltipButton(8, 8, label, + () -> List.of(tooltip.text(amountForOperation(operation))), + () -> this.container.modifyProcessingPatternAmounts(operation), + SmallTextTooltipButton.BackgroundStyle.TOOLBAR); + } + + private static int amountForOperation(ProcessingPatternAmountHelper.Operation operation) { + return switch (operation) { + case MULTIPLY_2, DIVIDE_2 -> 2; + case MULTIPLY_3, DIVIDE_3 -> 3; + case MULTIPLY_5, DIVIDE_5 -> 5; + }; + } + + private String widgetId(String baseId) { + return (this.mode == ExtendedEncodingMode.MULTI_IN ? "multiIn" : "multiOut") + baseId; + } +} diff --git a/src/main/java/com/formlessDragon/ae2gtaddon/client/gui/GuiExtendedPatternEncodingTerm.java b/src/main/java/com/formlessDragon/ae2gtaddon/client/gui/GuiExtendedPatternEncodingTerm.java new file mode 100644 index 0000000..6144973 --- /dev/null +++ b/src/main/java/com/formlessDragon/ae2gtaddon/client/gui/GuiExtendedPatternEncodingTerm.java @@ -0,0 +1,327 @@ +package com.formlessDragon.ae2gtaddon.client.gui; + +import ae2.api.behaviors.ContainerItemStrategies; +import ae2.api.behaviors.EmptyingAction; +import ae2.api.config.ActionItems; +import ae2.api.config.YesNo; +import ae2.api.stacks.AEItemKey; +import ae2.api.stacks.AmountFormat; +import ae2.api.stacks.GenericStack; +import ae2.client.Point; +import ae2.client.gui.Icon; +import ae2.client.gui.me.common.GuiMEStorage; +import ae2.client.gui.me.items.GuiSetProcessingPatternAmount; +import ae2.client.gui.me.patternaccess.ProviderSelectionOverlay; +import ae2.client.gui.style.GuiStyle; +import ae2.client.gui.widgets.ActionButton; +import ae2.client.gui.widgets.IconButton; +import ae2.client.gui.widgets.PatternModifierPanelWidget; +import ae2.client.gui.widgets.TabButton; +import ae2.container.slot.AppEngSlot; +import ae2.core.AEConfig; +import ae2.core.definitions.AEItems; +import ae2.core.localization.ButtonToolTips; +import ae2.core.localization.Tooltips; +import ae2.core.network.InitNetwork; +import ae2.core.network.clientbound.IProviderSelectionPageReceiver; +import ae2.core.network.serverbound.InventoryActionPacket; +import ae2.helpers.InventoryAction; +import ae2.helpers.patternmodifier.PatternModifierToolboxLayout; +import ae2.text.TextComponentItemStack; +import com.formlessDragon.ae2gtaddon.container.ContainerExtendedEncodingTerm; +import com.formlessDragon.ae2gtaddon.parts.ExtendedEncodingMode; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.minecraft.entity.player.InventoryPlayer; +import net.minecraft.client.gui.GuiTextField; +import net.minecraft.inventory.ClickType; +import net.minecraft.inventory.Slot; +import net.minecraft.item.ItemStack; +import net.minecraft.util.text.ITextComponent; +import net.minecraft.util.text.TextComponentString; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.input.Keyboard; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +public class GuiExtendedPatternEncodingTerm extends GuiMEStorage + implements IProviderSelectionPageReceiver { + private static final ExtendedEncodingMode[] ENCODING_MODES = ExtendedEncodingMode.values(); + private static final String PROVIDER_SELECTION_OVERLAY_WIDGET = ProviderSelectionOverlay.WIDGET_ID; + private final Map modePanels = + new EnumMap<>(ExtendedEncodingMode.class); + private final Map modeTabButtons = + new EnumMap<>(ExtendedEncodingMode.class); + private final PatternModifierPanelWidget patternModifierPanel; + private final IconButton uploadPatternButton; + private final ProviderSelectionOverlay providerSelectionOverlay; + private int providerSelectionOverlayRequestNonce; + + public GuiExtendedPatternEncodingTerm(ContainerExtendedEncodingTerm container, + InventoryPlayer playerInventory, + @Nullable ITextComponent title, + GuiStyle style) { + super(container, playerInventory, resolveTitle(container, title), style); + this.providerSelectionOverlay = new ProviderSelectionOverlay<>(this); + addMode(ExtendedEncodingMode.MULTI_IN, new ExtendedProcessingEncodingPanel(this, widgets, + ExtendedEncodingMode.MULTI_IN), 0); + addMode(ExtendedEncodingMode.MULTI_OUT, new ExtendedProcessingEncodingPanel(this, widgets, + ExtendedEncodingMode.MULTI_OUT), 1); + this.uploadPatternButton = new IconButton(this::uploadPattern) { + { + setHalfSize(true); + setIconScale(0.5F); + setVisibility(false); + } + + @Override + protected Icon getIcon() { + return Icon.PATTERN_UPLOAD; + } + + @Override + @NotNull + public List getTooltipMessage() { + return List.of( + ButtonToolTips.PatternUpload.text(), + ButtonToolTips.PatternUploadProcessingHint.text(), + ButtonToolTips.PatternUploadProcessingShiftHint.text()); + } + }; + widgets.add("uploadPattern", this.uploadPatternButton); + widgets.add("encodePattern", new ActionButton(ActionItems.ENCODE, + () -> container.encode(isShiftDown()))); + this.patternModifierPanel = new PatternModifierPanelWidget(this, new EncodingTerminalPanelHost()); + this.patternModifierPanel.addButtons(); + widgets.add(PROVIDER_SELECTION_OVERLAY_WIDGET, this.providerSelectionOverlay); + addToLeftToolbar(this.patternModifierPanel.getToolbarButton()); + } + + private static ITextComponent resolveTitle(ContainerExtendedEncodingTerm container, + @Nullable ITextComponent title) { + if (title != null) { + return title; + } + if (container.getGuiTitle() != null) { + return container.getGuiTitle(); + } + return new TextComponentString(""); + } + + @Override + public void initGui() { + super.initGui(); + this.container.setClearOnClose(AEConfig.instance().isClearGridOnClose()); + } + + private void addMode(ExtendedEncodingMode mode, ExtendedEncodingModePanel panel, int index) { + TabButton tabButton = new TabButton(panel.getIcon(), panel.getTabTooltip(), + () -> this.container.setExtendedMode(mode)); + tabButton.setStyle(TabButton.Style.HORIZONTAL); + widgets.add("modePanel" + index, panel); + widgets.add("modeTabButton" + index, tabButton); + this.modePanels.put(mode, panel); + this.modeTabButtons.put(mode, tabButton); + } + + @Override + protected void updateBeforeRender() { + super.updateBeforeRender(); + syncProviderSelectionOverlayOpenRequest(); + for (var mode : ENCODING_MODES) { + boolean selected = this.container.getExtendedMode() == mode; + var tabButton = this.modeTabButtons.get(mode); + var panel = this.modePanels.get(mode); + if (tabButton != null) { + tabButton.setSelected(selected); + } + if (panel != null) { + panel.setVisible(selected); + } + } + this.uploadPatternButton.setVisibility(true); + this.patternModifierPanel.update(); + } + + private void syncProviderSelectionOverlayOpenRequest() { + int requestNonce = this.container.getProviderSelectionOverlayRequestNonce(); + if (requestNonce <= this.providerSelectionOverlayRequestNonce) { + return; + } + + this.providerSelectionOverlayRequestNonce = requestNonce; + if (requestNonce == 0) { + return; + } + + this.providerSelectionOverlay.open( + this.container.getProviderSelectionOverlaySearchText(), + this.container.getProviderSelectionOverlayMappingText()); + } + + @Override + public void drawBG(int offsetX, int offsetY, int mouseX, int mouseY, float partialTicks) { + super.drawBG(offsetX, offsetY, mouseX, mouseY, partialTicks); + this.patternModifierPanel.drawBackground(); + } + + @Override + @Nullable + protected String getSlotAmountText(Slot slot, AppEngSlot appEngSlot, ItemStack rawStack, ItemStack displayStack) { + if (this.container.isBlankPatternSlot(slot) + && this.container.getAutoFillPatterns() == YesNo.YES + && AEItems.BLANK_PATTERN.is(rawStack)) { + long total = this.container.getSyncedNetworkBlankPatternCount() + rawStack.getCount(); + if (total <= 1) { + return null; + } + + AEItemKey blankPatternKey = AEItemKey.of(AEItems.BLANK_PATTERN.stack(1)); + if (blankPatternKey == null) { + return Long.toString(total); + } + return blankPatternKey.formatAmount(total, AmountFormat.SLOT); + } + + return super.getSlotAmountText(slot, appEngSlot, rawStack, displayStack); + } + + @Override + protected void mouseClicked(int mouseX, int mouseY, int mouseButton) throws IOException { + if (handleSelectionPopupMouseClicked(mouseX, mouseY)) { + return; + } + + Point mousePos = new Point(mouseX - this.guiLeft, mouseY - this.guiTop); + if (this.widgets.blocksMouseInteraction(mousePos)) { + this.widgets.onMouseDown(mousePos, mouseButton); + return; + } + + if (mouseButton == 2) { + Slot slot = findSlot(mouseX, mouseY); + if (isAltDown() && slot != null && this.container.isProcessingPatternItemSlot(slot)) { + switchToScreen(new GuiExtendedPatternItemRenamer(this, slot, + TextComponentItemStack.of(this.container.getHost().getMainContainerIcon()))); + return; + } + if (slot != null && this.container.canModifyAmountForSlot(slot)) { + GenericStack currentStack = GenericStack.fromItemStack(slot.getStack()); + if (currentStack != null) { + switchToScreen(new GuiSetProcessingPatternAmount(this, currentStack, + newStack -> InitNetwork.sendToServer(new InventoryActionPacket( + this.container.windowId, + InventoryAction.SET_FILTER, + slot.slotNumber, + GenericStack.wrapInItemStack(newStack))), + TextComponentItemStack.of(this.container.getHost().getMainContainerIcon()))); + return; + } + } + } + + super.mouseClicked(mouseX, mouseY, mouseButton); + } + + private void uploadPattern() { + this.container.uploadPattern(isShiftDown()); + } + + private static boolean isAltDown() { + return Keyboard.isKeyDown(Keyboard.KEY_LMENU) || Keyboard.isKeyDown(Keyboard.KEY_RMENU); + } + + private static boolean isShiftDown() { + return Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) || Keyboard.isKeyDown(Keyboard.KEY_RSHIFT); + } + + @Override + protected EmptyingAction getEmptyingAction(@Nullable Slot slot, ItemStack carried) { + if (this.container.isProcessingPatternSlot(slot)) { + EmptyingAction emptyingAction = ContainerItemStrategies.getEmptyingAction(carried); + if (emptyingAction != null) { + return emptyingAction; + } + } + + return super.getEmptyingAction(slot, carried); + } + + @Override + protected void handleMouseClick(@Nullable Slot slot, int slotId, int mouseButton, ClickType clickType) { + if (mouseButton == 2 && this.container.canModifyAmountForSlot(slot)) { + return; + } + + super.handleMouseClick(slot, slotId, mouseButton, clickType); + } + + @Override + protected void renderHoveredToolTip(int mouseX, int mouseY) { + Slot slot = getSlotUnderMouse(); + if (this.playerInventory.getItemStack().isEmpty() && this.container.canModifyAmountForSlot(slot)) { + assert slot != null; + var itemTooltip = new ObjectArrayList<>(getItemToolTip(slot.getStack())); + GenericStack unwrapped = GenericStack.fromItemStack(slot.getStack()); + if (unwrapped != null) { + itemTooltip.add(Tooltips.getAmountTooltipLocal(ButtonToolTips.Amount, unwrapped)); + } + itemTooltip.add(Tooltips.getSetAmountTooltipLocal()); + if (this.container.isProcessingPatternItemSlot(slot)) { + itemTooltip.add(Tooltips.getRenameTooltipLocal()); + } + drawItemTooltipWithImages(mouseX, mouseY, slot.getStack(), itemTooltip); + return; + } + + super.renderHoveredToolTip(mouseX, mouseY); + } + + private final class EncodingTerminalPanelHost implements PatternModifierPanelWidget.PanelHost { + @Override + public boolean isPatternModifierPanelAvailable() { + return container.isPatternModifierPanelAvailable(); + } + + @Override + public ae2.client.Point getPatternModifierPanelOffset() { + return new ae2.client.Point( + PatternModifierToolboxLayout.PANEL_LEFT_OFFSET - 4, + PatternModifierToolboxLayout.PANEL_TOP_OFFSET - 50); + } + + @Override + public void updatePatternModifierPanelVisibleSlots(boolean visible) { + container.updatePatternModifierPanelVisibleSlots(visible); + } + + @Override + public void clearPatternModifierPanel() { + container.getPatternModifierPanel().clearPatterns(); + } + + @Override + public void modifyPatternModifierPanelAmounts(int factor, boolean divide) { + container.getPatternModifierPanel().modifyAmounts(factor, divide); + } + } + + @Override + public ProviderSelectionOverlay getProviderSelectionOverlay() { + return this.providerSelectionOverlay; + } + + @Override + public Collection getTextFields() { + Collection textFields = new ArrayList<>(super.getTextFields()); + if (this.providerSelectionOverlay.isVisible()) { + textFields.addAll(this.providerSelectionOverlay.getTextFields()); + } + return textFields; + } +} diff --git a/src/main/java/com/formlessDragon/ae2gtaddon/client/gui/GuiExtendedPatternItemRenamer.java b/src/main/java/com/formlessDragon/ae2gtaddon/client/gui/GuiExtendedPatternItemRenamer.java new file mode 100644 index 0000000..17f1fec --- /dev/null +++ b/src/main/java/com/formlessDragon/ae2gtaddon/client/gui/GuiExtendedPatternItemRenamer.java @@ -0,0 +1,95 @@ +package com.formlessDragon.ae2gtaddon.client.gui; + +import ae2.client.gui.AEBaseGui; +import ae2.client.gui.Icon; +import ae2.client.gui.style.GuiStyleManager; +import ae2.client.gui.widgets.AETextField; +import ae2.client.gui.widgets.ITextFieldGui; +import ae2.client.gui.widgets.TabButton; +import com.formlessDragon.ae2gtaddon.container.ContainerExtendedEncodingTerm; +import it.unimi.dsi.fastutil.objects.ObjectLists; +import net.minecraft.client.gui.GuiTextField; +import net.minecraft.inventory.Slot; +import net.minecraft.item.ItemStack; +import net.minecraft.util.text.ITextComponent; + +import java.io.IOException; +import java.util.Collection; + +public class GuiExtendedPatternItemRenamer extends AEBaseGui + implements ITextFieldGui { + private final GuiExtendedPatternEncodingTerm parent; + private final int slotNumber; + private final AETextField name; + private boolean submitted; + + public GuiExtendedPatternItemRenamer(GuiExtendedPatternEncodingTerm parent, Slot slot, + ITextComponent parentIconTooltip) { + super(parent.getContainer(), parent.getContainer().getPlayerInventory(), + GuiStyleManager.loadStyleDoc("/screens/renamer.json")); + this.parent = parent; + this.slotNumber = slot.slotNumber; + this.name = widgets.addTextField("name"); + this.name.setMaxStringLength(32); + this.name.setResponder(text -> container.renameProcessingPatternItem(this.slotNumber, text)); + widgets.add("back", new TabButton(Icon.BACK, parentIconTooltip, this::returnToParent)); + + ItemStack stack = slot.getStack(); + this.name.setText(stack.hasDisplayName() ? stack.getDisplayName() : ""); + } + + @Override + public void initGui() { + super.initGui(); + setInitialFocus(this.name); + } + + @Override + protected void keyTyped(char typedChar, int keyCode) throws IOException { + if (keyCode == 1 || keyCode == 28 || keyCode == 156 + || this.mc.gameSettings.keyBindInventory.isActiveAndMatches(keyCode)) { + submitName(); + returnToParent(); + return; + } + super.keyTyped(typedChar, keyCode); + } + + @Override + protected void mouseClicked(int mouseX, int mouseY, int mouseButton) throws IOException { + if (handleSelectionPopupMouseClicked(mouseX, mouseY)) { + return; + } + + if (mouseButton == 1 && this.name.isMouseOver(mouseX, mouseY)) { + this.name.setTextFromClient(""); + this.name.setFocused(true); + return; + } + super.mouseClicked(mouseX, mouseY, mouseButton); + } + + @Override + public void onGuiClosed() { + submitName(); + super.onGuiClosed(); + } + + private void submitName() { + if (this.submitted) { + return; + } + this.submitted = true; + container.renameProcessingPatternItem(this.slotNumber, this.name.getText()); + } + + private void returnToParent() { + switchToScreen(this.parent); + this.parent.returnFromSubScreen(this); + } + + @Override + public Collection getTextFields() { + return ObjectLists.singleton(this.name); + } +} diff --git a/src/main/java/com/formlessDragon/ae2gtaddon/client/handler/HeiPatternAutoNamingHandler.java b/src/main/java/com/formlessDragon/ae2gtaddon/client/handler/HeiPatternAutoNamingHandler.java new file mode 100644 index 0000000..2bd5a23 --- /dev/null +++ b/src/main/java/com/formlessDragon/ae2gtaddon/client/handler/HeiPatternAutoNamingHandler.java @@ -0,0 +1,50 @@ +package com.formlessDragon.ae2gtaddon.client.handler; + +import ae2.api.client.PatternRecipeTransferEvent; +import ae2.api.client.PatternRecipeTransferSlot; +import ae2.api.stacks.GenericStack; +import ae2.container.me.patternencode.ContainerPatternEncodingTerm; +import ae2.parts.encoding.EncodingMode; +import com.formlessDragon.ae2gtaddon.integration.hei.AutoPatternItemNaming; +import com.formlessDragon.ae2gtaddon.integration.hei.HeiPatternItemAutoNamer; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; + +import java.util.List; + +public final class HeiPatternAutoNamingHandler { + + @SubscribeEvent + public void onPatternRecipeTransfer(PatternRecipeTransferEvent event) { + if (event.getMode() != EncodingMode.PROCESSING) { + return; + } + + ContainerPatternEncodingTerm container = event.getContainer(); + if (!(container instanceof AutoPatternItemNaming naming)) { + return; + } + if (!HeiPatternItemAutoNamer.isEnabled(naming.ae2gtaddon$autoNamePatternItems(), + event.getRecipeCategoryUid())) { + return; + } + + if (container.isMergeHeiProcessingInputs()) { + container.setMergeHeiProcessingInputs(false); + } + + List inputSlots = event.getInputSlots(); + List planned = new ObjectArrayList<>(inputSlots.size()); + for (PatternRecipeTransferSlot slot : inputSlots) { + planned.add(slot.getStack()); + } + + List renamed = HeiPatternItemAutoNamer.autoNameRepeatedItems(planned); + for (int index = 0; index < renamed.size(); index++) { + GenericStack replacement = renamed.get(index); + if (replacement != null && replacement != planned.get(index)) { + inputSlots.get(index).setStack(replacement); + } + } + } +} diff --git a/src/main/java/com/formlessDragon/ae2gtaddon/client/handler/HeiPatternInputFilterHandler.java b/src/main/java/com/formlessDragon/ae2gtaddon/client/handler/HeiPatternInputFilterHandler.java new file mode 100644 index 0000000..3a09146 --- /dev/null +++ b/src/main/java/com/formlessDragon/ae2gtaddon/client/handler/HeiPatternInputFilterHandler.java @@ -0,0 +1,52 @@ +package com.formlessDragon.ae2gtaddon.client.handler; + +import ae2.api.client.PatternRecipeTransferEvent; +import ae2.api.client.PatternRecipeTransferSlot; +import ae2.api.stacks.AEKey; +import ae2.api.stacks.GenericStack; +import ae2.container.me.patternencode.ContainerPatternEncodingTerm; +import ae2.parts.encoding.EncodingMode; +import com.formlessDragon.ae2gtaddon.integration.hei.ProcessingPatternCircuitImportMode; +import com.formlessDragon.ae2gtaddon.util.Matcher; +import com.formlessDragon.ae2gtaddon.util.Util; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; + +import java.util.List; +import java.util.function.Predicate; + +public final class HeiPatternInputFilterHandler { + + @SubscribeEvent + public void onPatternRecipeTransfer(PatternRecipeTransferEvent event) { + if (event.getMode() != EncodingMode.PROCESSING) { + return; + } + + ContainerPatternEncodingTerm container = event.getContainer(); + boolean ignoreCircuit = container instanceof ProcessingPatternCircuitImportMode importMode + && importMode.ae2gtaddon$ignoreCircuit(); + boolean assemblyLine = Util.isAssemblyLineRecipe(event.getRecipeCategoryUid()); + if (!ignoreCircuit && !assemblyLine) { + return; + } + + List inputSlots = event.getInputSlots(); + for (PatternRecipeTransferSlot slot : inputSlots) { + if (ignoreCircuit) { + dropExcludedInput(slot, Matcher.Circuit); + } + if (assemblyLine) { + dropExcludedInput(slot, Matcher.DataItem); + } + } + } + + static void dropExcludedInput(PatternRecipeTransferSlot slot, Predicate isExcluded) { + GenericStack stack = slot.getStack(); + if (stack != null && stack.what() != null && isExcluded.test(stack.what())) { + slot.clear(); + return; + } + slot.getCandidates().removeIf(key -> key != null && isExcluded.test(key)); + } +} diff --git a/src/main/java/com/formlessDragon/ae2gtaddon/container/ContainerExtendedEncodingTerm.java b/src/main/java/com/formlessDragon/ae2gtaddon/container/ContainerExtendedEncodingTerm.java new file mode 100644 index 0000000..c838a9d --- /dev/null +++ b/src/main/java/com/formlessDragon/ae2gtaddon/container/ContainerExtendedEncodingTerm.java @@ -0,0 +1,2408 @@ +package com.formlessDragon.ae2gtaddon.container; + +import ae2.api.config.Settings; +import ae2.api.config.YesNo; +import ae2.api.crafting.IAssemblerPattern; +import ae2.api.crafting.IPatternDetails; +import ae2.api.crafting.PatternDetailsHelper; +import ae2.api.inventories.InternalInventory; +import ae2.api.networking.IGrid; +import ae2.api.networking.IGridNode; +import ae2.api.stacks.AEItemKey; +import ae2.api.stacks.AEKey; +import ae2.api.stacks.GenericStack; +import ae2.api.storage.ILinkStatus; +import ae2.api.storage.StorageHelper; +import ae2.container.GuiIds; +import ae2.container.SlotSemantics; +import ae2.container.crafting.DeferredRecipeLookup; +import ae2.container.crafting.RecipeSelection; +import ae2.container.guisync.GuiSync; +import ae2.container.implementations.PatternModifierPanel; +import ae2.container.me.common.ContainerMEStorage; +import ae2.container.me.patternencode.PatternProviderUploadService.ProcessingPatternUploadPreparation; +import ae2.container.me.patternencode.PatternProviderUploadService.ProcessingPatternUploadResult; +import ae2.container.slot.FakeSlot; +import ae2.container.slot.PatternTermSlot; +import ae2.container.slot.RestrictedInputSlot; +import ae2.container.slot.SlotBackgroundIcon; +import ae2.core.AELog; +import ae2.core.definitions.AEItems; +import ae2.core.localization.PlayerMessages; +import ae2.core.network.NetworkPacketHelper; +import ae2.core.network.clientbound.ProviderDirectoryPagePacket; +import ae2.core.network.clientbound.ProviderMappingPagePacket; +import ae2.core.worlddata.PatternProviderMappingData; +import ae2.crafting.CraftingEventSimulation; +import ae2.crafting.pattern.AECraftingPattern; +import ae2.crafting.pattern.AEProcessingPattern; +import ae2.helpers.IPatternTerminalGuiHost; +import ae2.helpers.patternprovider.PatternContainer; +import ae2.me.service.ActivePatternProviderDirectory.ProviderKey; +import ae2.parts.encoding.EncodingMode; +import ae2.parts.encoding.PatternEncodingLogic; +import ae2.parts.encoding.ProcessingPatternAmountHelper; +import ae2.util.ConfigGuiInventory; +import ae2.util.ConfigInventory; +import ae2.util.inv.FilteredInternalInventory; +import ae2.util.inv.filter.IAEItemFilter; +import it.unimi.dsi.fastutil.ints.IntArraySet; +import it.unimi.dsi.fastutil.ints.IntSet; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import it.unimi.dsi.fastutil.shorts.ShortSet; +import net.minecraft.entity.item.EntityItem; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.InventoryPlayer; +import net.minecraft.inventory.Container; +import net.minecraft.inventory.InventoryCrafting; +import net.minecraft.inventory.Slot; +import net.minecraft.item.ItemStack; +import net.minecraft.item.crafting.CraftingManager; +import net.minecraft.item.crafting.IRecipe; +import net.minecraft.nbt.JsonToNBT; +import net.minecraft.nbt.NBTException; +import net.minecraft.util.ResourceLocation; +import net.minecraft.world.World; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +import ae2.container.me.patternencode.ExtendedProviderSelectionBridge; +import ae2.container.me.patternencode.ProviderPageLimits; +import com.formlessDragon.ae2gtaddon.parts.ExtendedEncodingLogic; +import com.formlessDragon.ae2gtaddon.parts.ExtendedEncodingMode; +import ae2.container.SlotSemantic; +import ae2.container.slot.AppEngSlot; +import ae2.helpers.InventoryAction; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.inventory.ClickType; +import net.minecraft.util.text.TextComponentTranslation; +import ae2.container.me.patternencode.IProviderSelectionEndpoint; +import ae2.container.me.patternencode.PatternProviderUploadService; +import ae2.container.me.patternencode.ProviderDirectoryPage; +import ae2.container.me.patternencode.ProviderDirectoryPageRequest; +import ae2.container.me.patternencode.ProviderMappingPage; +import ae2.container.me.patternencode.ProviderMappingPageRequest; +import ae2.container.me.patternencode.ProviderSelectionSession; + +public class ContainerExtendedEncodingTerm extends ContainerMEStorage + implements PatternModifierPanel.Host, IProviderSelectionEndpoint { + private static final int CRAFTING_GRID_WIDTH = 3; + private static final int CRAFTING_GRID_HEIGHT = 3; + private static final int CRAFTING_GRID_SLOTS = CRAFTING_GRID_WIDTH * CRAFTING_GRID_HEIGHT; + private static final short SYNC_CRAFTING_OUTPUT = 78; + private static final short SYNC_RECIPE_REVISION = 79; + private static final short SYNC_RECIPE_CONFLICT = 80; + private static final short SYNC_SELECTED_RECIPE = 81; + + private static final String ACTION_SET_MODE = "setMode"; + private static final String ACTION_ENCODE = "encode"; + private static final String ACTION_CLEAR = "clear"; + private static final String ACTION_SET_CLEAR_ON_CLOSE = "setClearOnClose"; + private static final String ACTION_SET_SUBSTITUTION = "setSubstitution"; + private static final String ACTION_SET_FLUID_SUBSTITUTION = "setFluidSubstitution"; + private static final String ACTION_SET_HEI_PROCESSING_MERGE = "setHeiProcessingMerge"; + private static final String ACTION_CYCLE_PROCESSING_OUTPUT = "cycleProcessingOutput"; + private static final String ACTION_CLEAR_PROCESSING_SECONDARY_OUTPUTS = "clearProcessingSecondaryOutputs"; + private static final String ACTION_PROCESSING_MULTIPLY_2 = "processingMultiply2"; + private static final String ACTION_PROCESSING_MULTIPLY_3 = "processingMultiply3"; + private static final String ACTION_PROCESSING_MULTIPLY_5 = "processingMultiply5"; + private static final String ACTION_PROCESSING_DIVIDE_2 = "processingDivide2"; + private static final String ACTION_PROCESSING_DIVIDE_3 = "processingDivide3"; + private static final String ACTION_PROCESSING_DIVIDE_5 = "processingDivide5"; + private static final String ACTION_RENAME_PROCESSING_PATTERN_ITEM = "renameProcessingPatternItem"; + private static final String ACTION_SET_HEI_PROCESSING_RECIPE = "setHeiProcessingRecipe"; + private static final String ACTION_SET_HEI_CRAFTING_RECIPE = "setHeiCraftingRecipe"; + private static final String ACTION_SELECT_RECIPE = "selectRecipe"; + private static final String ACTION_UPLOAD_PATTERN = "uploadPattern"; + private static final String ACTION_SET_PATTERN_MODIFIER_PANEL_VISIBLE = "setPatternModifierPanelVisible"; + private static final String ACTION_UPLOAD_PROCESSING_PATTERN_TO_PROVIDER = "uploadProcessingPatternToProvider"; + private static final String ACTION_BIND_PROVIDER_MAPPING = "bindProviderMapping"; + private static final String ACTION_BIND_AND_UPLOAD_PROVIDER_MAPPING = "bindAndUploadProviderMapping"; + private static final String ACTION_UNBIND_PROVIDER_MAPPING = "unbindProviderMapping"; + private static final String ACTION_REQUEST_PROVIDER_DIRECTORY_PAGE = "requestProviderDirectoryPage"; + private static final String ACTION_REQUEST_PROVIDER_MAPPING_PAGE = "requestProviderMappingPage"; + private static final String ACTION_REBUILD_MAPPINGS_FROM_ACTIVE_PROVIDERS = "rebuildMappingsFromActiveProviders"; + private static final int MAX_RENAME_PROCESSING_PATTERN_ITEM_PAYLOAD_LENGTH = 512; + private static final int MAX_SET_HEI_PROCESSING_RECIPE_PAYLOAD_LENGTH = 16384; + static final int MAX_PROVIDER_MAPPING_ACTION_PAYLOAD_LENGTH = MAX_SET_HEI_PROCESSING_RECIPE_PAYLOAD_LENGTH; + // Fixed JSON syntax, field names and the longest decimal forms in a valid focused request. + private static final int MAX_PROVIDER_DIRECTORY_PAGE_REQUEST_FIXED_JSON_LENGTH = 158; + private static final int MAX_PROVIDER_DIRECTORY_PAGE_REQUEST_PAYLOAD_LENGTH = + MAX_PROVIDER_DIRECTORY_PAGE_REQUEST_FIXED_JSON_LENGTH + + ProviderPageLimits.MAX_QUERY_UTF16_LENGTH * 6; + private static final int MAX_PROVIDER_MAPPING_PAGE_REQUEST_PAYLOAD_LENGTH = 128; + private static final int MAX_CUSTOM_NAME_LENGTH = 32; + private static final int MAX_HEI_PROCESSING_RECIPE_INPUT_SLOTS = AEProcessingPattern.MAX_INPUT_SLOTS; + private static final int MAX_HEI_PROCESSING_RECIPE_CANDIDATES_PER_SLOT = 256; + private static final int MAX_HEI_PROCESSING_RECIPE_TOTAL_CANDIDATES = + MAX_HEI_PROCESSING_RECIPE_INPUT_SLOTS * MAX_HEI_PROCESSING_RECIPE_CANDIDATES_PER_SLOT; + private static final int MAX_HEI_PROCESSING_RECIPE_KEY_TAG_LENGTH = 4096; + + private static final Container DUMMY_CONTAINER = new Container() { + @Override + public boolean canInteractWith(@NotNull EntityPlayer playerIn) { + return false; + } + }; + public final IntSet slotsSupportingFluidSubstitution = new IntArraySet(); + private final PatternEncodingLogic encodingLogic; + private final FakeSlot[] craftingGridSlots = new FakeSlot[CRAFTING_GRID_SLOTS]; + private final FakeSlot[] processingInputSlots = new FakeSlot[ExtendedEncodingLogic.PROCESSING_INPUT_SLOTS]; + private final FakeSlot[] processingOutputSlots = new FakeSlot[ExtendedEncodingLogic.PROCESSING_OUTPUT_SLOTS]; + private final PatternTermSlot craftOutputSlot; + private final RestrictedInputSlot blankPatternSlot; + private final RestrictedInputSlot encodedPatternSlot; + private final ConfigInventory encodedInputsInv; + private final ConfigInventory encodedOutputsInv; + private final PatternModifierPanel patternModifierPanel; + private final ExtendedProviderSelectionBridge providerSelectionSession; + private boolean patternModifierPanelVisible; + private static final String ACTION_SET_EXTENDED_MODE = "setExtendedMode"; + private static final String ACTION_PREPARE_PROCESSING_RECIPE = "prepareExtendedProcessingRecipe"; + @GuiSync(98) + public ExtendedEncodingMode extendedMode = ExtendedEncodingMode.MULTI_IN; + @GuiSync(99) + public int extendedModeRevision; + private ExtendedEncodingMode clientExtendedMode; + private boolean ignoreCircuitInHeiProcessingPattern; + private boolean autoNamePatternItemsInHeiImport; + @GuiSync(97) + public EncodingMode mode; + @GuiSync(96) + public boolean substitute; + @GuiSync(95) + public boolean substituteFluids; + @GuiSync(94) + public YesNo autoFillPatterns = YesNo.NO; + @GuiSync(93) + public boolean patternModifierPanelAvailable; + @GuiSync(92) + public long networkBlankPatternCount; + @GuiSync(82) + public boolean mergeHeiProcessingInputs; + @GuiSync(86) + private long providerDirectoryRevision; + @GuiSync(85) + private int providerSelectionOverlayRequestNonce; + @GuiSync(84) + private String providerSelectionOverlaySearchText = ""; + @GuiSync(83) + private String providerSelectionOverlayMappingText = ""; + @Nullable + private IRecipe currentRecipe; + private List recipeCandidates = List.of(); + private final DeferredRecipeLookup deferredRecipeLookup = new DeferredRecipeLookup(); + private boolean recipeCandidatesInitialized; + @Nullable + private ItemStack[] lastCraftingIngredients; + private boolean craftingCandidatesValid; + private boolean fluidSubstitutionSupportKnown; + @Nullable + private InventoryCrafting craftingInventory; + @Nullable + private ResourceLocation lastResolvedRecipeId; + @GuiSync(SYNC_RECIPE_CONFLICT) + private boolean recipeConflict; + @GuiSync(SYNC_RECIPE_REVISION) + private long recipeCandidatesRevision; + @GuiSync(SYNC_SELECTED_RECIPE) + @Nullable + private ResourceLocation selectedRecipeId; + @Nullable + private HeiProcessingRecipeSnapshot heiProcessingRecipeSnapshot; + private boolean changingEncodedPatternSlotInternally; + private boolean clearOnClose; + @GuiSync(SYNC_CRAFTING_OUTPUT) + @Nullable + private GenericStack syncedCraftingOutput; + + public ContainerExtendedEncodingTerm(InventoryPlayer ip, IPatternTerminalGuiHost host) { + this(GuiIds.GuiKey.PATTERN_ENCODING_TERMINAL, ip, host, true); + } + + public ContainerExtendedEncodingTerm(GuiIds.GuiKey guiKey, InventoryPlayer ip, IPatternTerminalGuiHost host, + boolean bindInventory) { + this(guiKey, ip, host, bindInventory, true); + } + + protected ContainerExtendedEncodingTerm(GuiIds.GuiKey guiKey, InventoryPlayer ip, IPatternTerminalGuiHost host, + boolean bindInventory, boolean registerProviderSelectionActions) { + super(guiKey, ip, host, bindInventory); + this.encodingLogic = host.getLogic(); + this.encodedInputsInv = this.encodingLogic.getEncodedInputInv(); + this.encodedOutputsInv = this.encodingLogic.getEncodedOutputInv(); + this.mode = this.encodingLogic.getMode(); + this.substitute = this.encodingLogic.isSubstitution(); + this.substituteFluids = this.encodingLogic.isFluidSubstitution(); + this.mergeHeiProcessingInputs = this.encodingLogic.isMergeHeiProcessingInputs(); + + ConfigGuiInventory encodedInputs = this.encodedInputsInv.createGuiWrapper(); + ConfigGuiInventory encodedOutputs = this.encodedOutputsInv.createGuiWrapper(); + + for (int i = 0; i < this.craftingGridSlots.length; i++) { + FakeSlot slot = new FakeSlot(encodedInputs, i, 0, 0); + slot.setHideAmount(true); + this.addSlot(this.craftingGridSlots[i] = slot, SlotSemantics.CRAFTING_GRID); + } + this.addSlot(this.craftOutputSlot = new PatternTermSlot(), SlotSemantics.CRAFTING_RESULT); + + for (int i = 0; i < this.processingInputSlots.length; i++) { + this.addSlot(this.processingInputSlots[i] = new FakeSlot(encodedInputs, i, 0, 0), + SlotSemantics.PROCESSING_INPUTS); + } + for (int i = 0; i < this.processingOutputSlots.length; i++) { + this.addSlot(this.processingOutputSlots[i] = new FakeSlot(encodedOutputs, i, 0, 0), + SlotSemantics.PROCESSING_OUTPUTS); + } + this.processingOutputSlots[0].setBackgroundIcon(SlotBackgroundIcon.PRIMARY_OUTPUT); + + this.addSlot(this.blankPatternSlot = new RestrictedInputSlot(RestrictedInputSlot.PlacableItemType.BLANK_PATTERN, + this.encodingLogic.getBlankPatternInv(), 0), SlotSemantics.BLANK_PATTERN); + this.addSlot(this.encodedPatternSlot = new RestrictedInputSlot(RestrictedInputSlot.PlacableItemType.ENCODED_PATTERN, + this.encodingLogic.getEncodedPatternInv(), 0), SlotSemantics.ENCODED_PATTERN); + this.encodedPatternSlot.setStackLimit(1); + this.providerSelectionSession = ExtendedProviderSelectionBridge.create( + new ExtendedProviderSelectionBridge.Host() { + @Override + public boolean isLinkConnected() { + return ContainerExtendedEncodingTerm.this.getHost().getLinkStatus().connected(); + } + + @Override + public IGrid getProviderSelectionGrid() { + return ContainerExtendedEncodingTerm.this.getProviderSelectionGrid(); + } + + @Override + public IGrid requireProviderSelectionGrid() { + return ContainerExtendedEncodingTerm.this.requireProviderSelectionGrid(); + } + + @Override + public PatternProviderMappingData getProviderMappingData() { + return ContainerExtendedEncodingTerm.this.getPatternProviderMappingData(); + } + + @Override + public EntityPlayer getPlayer() { + return ContainerExtendedEncodingTerm.this.getPlayer(); + } + + @Override + public World getWorld() { + return ContainerExtendedEncodingTerm.this.getPlayer().world; + } + + @Override + public int getWindowId() { + return ContainerExtendedEncodingTerm.this.windowId; + } + + @Override + public ItemStack getEncodedPattern() { + return ContainerExtendedEncodingTerm.this.encodedPatternSlot.getStack().copy(); + } + + @Override + public void setEncodedPattern(ItemStack encodedPattern) { + ContainerExtendedEncodingTerm.this.putEncodedPatternStackFromContainer(encodedPattern); + } + + @Override + public ProcessingPatternUploadResult uploadProcessingPattern(ItemStack encodedPattern, IGrid grid, + ProviderKey target) { + return PatternProviderUploadService.tryUploadProcessingPatternToProvider( + ContainerExtendedEncodingTerm.this.getPlayer(), + (IPatternTerminalGuiHost) ContainerExtendedEncodingTerm.this.getHost(), grid, target, encodedPattern); + } + + @Override + public ProcessingPatternUploadPreparation prepareProcessingPatternUpload(ItemStack encodedPattern, + IGrid grid, + ProviderKey target) { + return PatternProviderUploadService.prepareProcessingPatternUpload( + ContainerExtendedEncodingTerm.this.getPlayer(), + (IPatternTerminalGuiHost) ContainerExtendedEncodingTerm.this.getHost(), grid, target, encodedPattern); + } + + @Override + public void sendProviderDirectoryPage(ProviderDirectoryPage page) { + ContainerExtendedEncodingTerm.this.sendProviderDirectoryPage(page); + } + + @Override + public void sendProviderMappingPage(ProviderMappingPage page) { + ContainerExtendedEncodingTerm.this.sendProviderMappingPage(page); + } + + @Override + public void syncProviderDirectoryRevision(long revision) { + ContainerExtendedEncodingTerm.this.providerDirectoryRevision = revision; + } + + @Override + public void syncProviderSelectionOverlay(long nonce, String searchText, String mappingText) { + ContainerExtendedEncodingTerm.this.providerSelectionOverlayRequestNonce = (int) nonce; + ContainerExtendedEncodingTerm.this.providerSelectionOverlaySearchText = searchText; + ContainerExtendedEncodingTerm.this.providerSelectionOverlayMappingText = mappingText; + } + }); + + registerClientAction(ACTION_ENCODE, Boolean.class, this::encode); + registerClientAction(ACTION_CLEAR, this::clear); + registerClientAction(ACTION_SET_CLEAR_ON_CLOSE, Boolean.class, this::setClearOnClose); + registerClientAction(ACTION_SET_MODE, EncodingMode.class, this::changeMode); + registerClientAction(ACTION_SET_SUBSTITUTION, Boolean.class, this::changeSubstitution); + registerClientAction(ACTION_SET_FLUID_SUBSTITUTION, Boolean.class, this::changeFluidSubstitution); + registerClientAction(ACTION_SET_HEI_PROCESSING_MERGE, Boolean.class, this::changeMergeHeiProcessingInputs); + registerClientAction(ACTION_CYCLE_PROCESSING_OUTPUT, this::cycleProcessingOutput); + registerClientAction(ACTION_CLEAR_PROCESSING_SECONDARY_OUTPUTS, this::clearProcessingSecondaryOutputs); + registerClientAction(ACTION_PROCESSING_MULTIPLY_2, + () -> modifyProcessingPatternAmounts(ProcessingPatternAmountHelper.Operation.MULTIPLY_2)); + registerClientAction(ACTION_PROCESSING_MULTIPLY_3, + () -> modifyProcessingPatternAmounts(ProcessingPatternAmountHelper.Operation.MULTIPLY_3)); + registerClientAction(ACTION_PROCESSING_MULTIPLY_5, + () -> modifyProcessingPatternAmounts(ProcessingPatternAmountHelper.Operation.MULTIPLY_5)); + registerClientAction(ACTION_PROCESSING_DIVIDE_2, + () -> modifyProcessingPatternAmounts(ProcessingPatternAmountHelper.Operation.DIVIDE_2)); + registerClientAction(ACTION_PROCESSING_DIVIDE_3, + () -> modifyProcessingPatternAmounts(ProcessingPatternAmountHelper.Operation.DIVIDE_3)); + registerClientAction(ACTION_PROCESSING_DIVIDE_5, + () -> modifyProcessingPatternAmounts(ProcessingPatternAmountHelper.Operation.DIVIDE_5)); + registerClientAction(ACTION_RENAME_PROCESSING_PATTERN_ITEM, RenamePatternItemRequest.class, + MAX_RENAME_PROCESSING_PATTERN_ITEM_PAYLOAD_LENGTH, + this::renameProcessingPatternItem); + registerClientAction(ACTION_SET_HEI_PROCESSING_RECIPE, HeiProcessingRecipeRequest.class, + MAX_SET_HEI_PROCESSING_RECIPE_PAYLOAD_LENGTH, + this::setHeiProcessingRecipe); + registerClientAction(ACTION_SET_HEI_CRAFTING_RECIPE, String.class, 256, + this::setHeiCraftingRecipeFromClient); + registerClientAction(ACTION_SELECT_RECIPE, String.class, 256, this::selectRecipeFromClient); + registerClientAction(ACTION_UPLOAD_PATTERN, Boolean.class, this::uploadPattern); + registerClientAction(ACTION_SET_PATTERN_MODIFIER_PANEL_VISIBLE, Boolean.class, + this::setPatternModifierPanelVisibleFromClient); + if (registerProviderSelectionActions) { + registerProviderSelectionActions(); + } + this.patternModifierPanel = new PatternModifierPanel(this); + this.patternModifierPanelAvailable = this.patternModifierPanel.isAvailable(); + + this.extendedMode = extendedLogic().getExtendedMode(); + registerClientAction(ACTION_SET_EXTENDED_MODE, ExtendedEncodingMode.class, this::changeExtendedMode); + registerClientAction(ACTION_PREPARE_PROCESSING_RECIPE, ExtendedEncodingMode.class, + this::prepareProcessingRecipe); + + updateSlotVisibility(); + updateExtendedSlots(); + restoreSelectedRecipeFromEncodedPattern(); + if (isServerSide() && this.mode == EncodingMode.CRAFTING) { + markCraftingRecipeDirty(); + } + + tryAutoFillBlankPatterns(); + } + + private void registerProviderSelectionActions() { + registerClientAction(ACTION_UPLOAD_PROCESSING_PATTERN_TO_PROVIDER, + ProviderSelectionSession.ProviderEntryAction.class, this::handleProviderEntryUpload); + registerClientAction(ACTION_BIND_PROVIDER_MAPPING, ProviderSelectionSession.ProviderMappingAction.class, + MAX_PROVIDER_MAPPING_ACTION_PAYLOAD_LENGTH, this::handleProviderMappingBind); + registerClientAction(ACTION_BIND_AND_UPLOAD_PROVIDER_MAPPING, ProviderSelectionSession.ProviderMappingAction.class, + MAX_PROVIDER_MAPPING_ACTION_PAYLOAD_LENGTH, this::handleProviderMappingBindAndUpload); + registerClientAction(ACTION_UNBIND_PROVIDER_MAPPING, ProviderSelectionSession.ProviderMappingAction.class, + MAX_PROVIDER_MAPPING_ACTION_PAYLOAD_LENGTH, this::handleProviderMappingUnbind); + registerClientAction(ACTION_REQUEST_PROVIDER_DIRECTORY_PAGE, ProviderDirectoryPageRequest.class, + MAX_PROVIDER_DIRECTORY_PAGE_REQUEST_PAYLOAD_LENGTH, this::handleProviderDirectoryPageRequest); + registerClientAction(ACTION_REQUEST_PROVIDER_MAPPING_PAGE, ProviderMappingPageRequest.class, + MAX_PROVIDER_MAPPING_PAGE_REQUEST_PAYLOAD_LENGTH, this::handleProviderMappingPageRequest); + registerClientAction(ACTION_REBUILD_MAPPINGS_FROM_ACTIVE_PROVIDERS, + this::handleRebuildMappingsFromActiveProviders); + } + + private void handleProviderEntryUpload(ProviderSelectionSession.ProviderEntryAction action) { + if (isServerSide()) { + this.providerSelectionSession.uploadProcessingPatternToProvider(action); + } + } + + private void handleProviderMappingBind(ProviderSelectionSession.ProviderMappingAction action) { + if (isServerSide()) { + this.providerSelectionSession.bindProviderMapping(action); + } + } + + private void handleProviderMappingBindAndUpload(ProviderSelectionSession.ProviderMappingAction action) { + if (isServerSide()) { + this.providerSelectionSession.bindAndUploadProcessingPatternToProvider(action); + } + } + + private void handleProviderMappingUnbind(ProviderSelectionSession.ProviderMappingAction action) { + if (isServerSide()) { + this.providerSelectionSession.unbindProviderMapping(action); + } + } + + private void handleProviderDirectoryPageRequest(ProviderDirectoryPageRequest request) { + if (isServerSide()) { + this.providerSelectionSession.requestProviderDirectoryPage(request); + } + } + + private void handleProviderMappingPageRequest(ProviderMappingPageRequest request) { + if (isServerSide()) { + this.providerSelectionSession.requestProviderMappingPage(request); + } + } + + private void handleRebuildMappingsFromActiveProviders() { + if (isServerSide()) { + this.providerSelectionSession.rebuildMappingsFromActiveProviders(); + } + } + + private static String actionForProcessingAmountOperation(ProcessingPatternAmountHelper.Operation operation) { + return switch (operation) { + case MULTIPLY_2 -> ACTION_PROCESSING_MULTIPLY_2; + case MULTIPLY_3 -> ACTION_PROCESSING_MULTIPLY_3; + case MULTIPLY_5 -> ACTION_PROCESSING_MULTIPLY_5; + case DIVIDE_2 -> ACTION_PROCESSING_DIVIDE_2; + case DIVIDE_3 -> ACTION_PROCESSING_DIVIDE_3; + case DIVIDE_5 -> ACTION_PROCESSING_DIVIDE_5; + }; + } + + private static void collectProcessingStacks(ConfigInventory inventory, List stacks) { + for (int slot = 0; slot < inventory.size(); slot++) { + stacks.add(inventory.getStack(slot)); + } + } + + private static void applyProcessingAmountOperation(ConfigInventory inventory, + ProcessingPatternAmountHelper.Operation operation) { + for (int slot = 0; slot < inventory.size(); slot++) { + inventory.setStack(slot, ProcessingPatternAmountHelper.apply(inventory.getStack(slot), operation)); + } + } + + private static ItemStack[] copyCraftingIngredients(ItemStack[] ingredients) { + ItemStack[] copy = new ItemStack[ingredients.length]; + for (int i = 0; i < ingredients.length; i++) { + copy[i] = ingredients[i].copy(); + } + return copy; + } + + @Override + public void broadcastChanges() { + if (isServerSide()) { + this.extendedMode = extendedLogic().getExtendedMode(); + updateExtendedSlots(); + resolveDeferredCraftingRecipe(); + this.providerSelectionSession.onBroadcastChanges(); + } + super.broadcastChanges(); + if (isServerSide()) { + this.mode = this.encodingLogic.getMode(); + this.substitute = this.encodingLogic.isSubstitution(); + this.substituteFluids = this.encodingLogic.isFluidSubstitution(); + this.mergeHeiProcessingInputs = this.encodingLogic.isMergeHeiProcessingInputs(); + this.autoFillPatterns = getHost().getConfigManager().getSetting(Settings.PATTERN_AUTO_FILL); + this.patternModifierPanelAvailable = this.patternModifierPanel.isAvailable(); + this.networkBlankPatternCount = this.autoFillPatterns == YesNo.YES ? computeNetworkBlankPatternCount() : 0; + } + } + + @Override + public void onServerDataSync(ShortSet updatedFields) { + super.onServerDataSync(updatedFields); + if (updatedFields.contains((short) 98) || updatedFields.contains((short) 99)) { + this.clientExtendedMode = this.extendedMode; + } + updateSlotVisibility(); + updateExtendedSlots(); + if (updatedFields.contains(SYNC_CRAFTING_OUTPUT) || updatedFields.contains(SYNC_RECIPE_REVISION) + || updatedFields.contains(SYNC_RECIPE_CONFLICT) || updatedFields.contains(SYNC_SELECTED_RECIPE) + || updatedFields.contains((short) 97)) { + updateClientCraftingRecipe(); + } + } + + private void updateClientCraftingRecipe() { + if (this.mode != EncodingMode.CRAFTING) { + clearClientCraftingRecipeState(); + return; + } + + ResourceLocation recipeId = this.selectedRecipeId; + if (recipeId == null) { + clearClientCraftingRecipeState(); + return; + } + + IRecipe recipe = CraftingManager.REGISTRY.getObject(recipeId); + if (recipe == null || !recipeId.equals(recipe.getRegistryName())) { + AELog.error("Server synchronized unknown crafting recipe %s", recipeId); + clearClientCraftingRecipeState(); + return; + } + + this.currentRecipe = recipe; + this.lastResolvedRecipeId = recipeId; + this.craftingCandidatesValid = false; + this.craftingInventory = null; + this.lastCraftingIngredients = null; + this.recipeCandidates = List.of(); + this.recipeCandidatesInitialized = false; + this.craftOutputSlot.setResultItem(toCraftingOutput(this.syncedCraftingOutput)); + } + + private void clearClientCraftingRecipeState() { + this.currentRecipe = null; + this.craftingCandidatesValid = false; + this.craftingInventory = null; + this.recipeCandidates = List.of(); + this.recipeCandidatesInitialized = false; + this.lastCraftingIngredients = null; + this.lastResolvedRecipeId = null; + this.craftOutputSlot.setResultItem(ItemStack.EMPTY); + this.slotsSupportingFluidSubstitution.clear(); + this.fluidSubstitutionSupportKnown = true; + } + + private ItemStack toCraftingOutput(@Nullable GenericStack output) { + if (output == null) { + return ItemStack.EMPTY; + } + if (!(output.what() instanceof AEItemKey itemKey) + || output.amount() <= 0 || output.amount() > Integer.MAX_VALUE) { + AELog.error("Server synchronized invalid crafting output %s", output); + return ItemStack.EMPTY; + } + return itemKey.toStack((int) output.amount()); + } + + @Override + public void onSlotChange(Slot slot) { + if (slot == this.encodedPatternSlot) { + if (this.changingEncodedPatternSlotInternally) { + return; + } + restoreSelectedRecipeFromEncodedPattern(); + markCraftingRecipeDirty(); + } else if (isCraftingGridSlot(slot)) { + // Keep the last successful recipe as the common preference for the deferred lookup. + // It is discarded only when it no longer matches the final matrix. + markCraftingRecipeDirty(); + } + } + + private void restoreSelectedRecipeFromEncodedPattern() { + ItemStack encodedPattern = this.encodedPatternSlot.getStack(); + if (!PatternDetailsHelper.isEncodedPattern(encodedPattern)) { + this.selectedRecipeId = null; + return; + } + + try { + IPatternDetails details = PatternDetailsHelper.decodePattern(encodedPattern, this.getPlayer().world); + this.selectedRecipeId = details instanceof AECraftingPattern craftingPattern + ? craftingPattern.getRecipeId() + : null; + } catch (RuntimeException e) { + this.selectedRecipeId = null; + AELog.warn(e, "Failed to restore the selected crafting recipe from an encoded pattern"); + } + } + + private void updateSlotVisibility() { + boolean crafting = this.mode == EncodingMode.CRAFTING; + boolean processing = this.mode == EncodingMode.PROCESSING; + for (FakeSlot slot : this.craftingGridSlots) { + slot.setActive(crafting); + } + this.craftOutputSlot.setActive(crafting); + for (FakeSlot slot : this.processingInputSlots) { + slot.setActive(processing); + } + for (FakeSlot slot : this.processingOutputSlots) { + slot.setActive(processing); + } + this.patternModifierPanel.updateSlotState(this.patternModifierPanelVisible && this.patternModifierPanelAvailable); + } + + private boolean isCraftingGridSlot(@Nullable Slot slot) { + if (slot == null) { + return false; + } + for (FakeSlot craftingGridSlot : this.craftingGridSlots) { + if (craftingGridSlot == slot) { + return true; + } + } + return false; + } + + private void resolveCraftingRecipe() { + this.deferredRecipeLookup.clear(); + if (this.mode != EncodingMode.CRAFTING) { + clearCraftingRecipeState(); + return; + } + + ItemStack[] ingredients = new ItemStack[CRAFTING_GRID_SLOTS]; + boolean invalidIngredient = false; + boolean hasIngredient = false; + for (int i = 0; i < ingredients.length; i++) { + ingredients[i] = getEncodedCraftingIngredient(i); + if (ingredients[i] == null) { + invalidIngredient = true; + break; + } + hasIngredient |= !ingredients[i].isEmpty(); + } + + if (invalidIngredient || !hasIngredient) { + clearCraftingRecipeState(); + return; + } + + boolean ingredientsChanged = !areCraftingIngredientsEqual(ingredients); + boolean selectedRecipeChanged = !Objects.equals(this.lastResolvedRecipeId, this.selectedRecipeId); + if (!this.craftingCandidatesValid || ingredientsChanged || selectedRecipeChanged) { + this.craftingInventory = new InventoryCrafting(DUMMY_CONTAINER, CRAFTING_GRID_WIDTH, + CRAFTING_GRID_HEIGHT); + for (int i = 0; i < ingredients.length; i++) { + this.craftingInventory.setInventorySlotContents(i, ingredients[i]); + } + this.recipeCandidates = List.of(); + this.recipeCandidatesInitialized = false; + this.lastCraftingIngredients = copyCraftingIngredients(ingredients); + this.craftingCandidatesValid = true; + this.recipeConflict = false; + this.recipeCandidatesRevision++; + RecipeSelection.Selection selection = RecipeSelection.findFirstCandidateAndConflict(this.craftingInventory, + this.getPlayer().world, this.selectedRecipeId); + RecipeSelection.Candidate selected = simulateCandidateOutput(selection.candidate()); + IRecipe previousRecipe = this.currentRecipe; + this.currentRecipe = selected == null ? null : selected.recipe(); + this.selectedRecipeId = selected == null ? null : selected.id(); + this.lastResolvedRecipeId = this.selectedRecipeId; + this.recipeConflict = selection.conflict(); + setCraftingOutput(selected == null ? ItemStack.EMPTY : selected.output()); + if (ingredientsChanged || this.currentRecipe != previousRecipe) { + this.slotsSupportingFluidSubstitution.clear(); + this.fluidSubstitutionSupportKnown = false; + } + } + + if (this.currentRecipe == null) { + setCraftingOutput(ItemStack.EMPTY); + } + + } + + private void markCraftingRecipeDirty() { + if (!isServerSide()) { + return; + } + + if (this.deferredRecipeLookup.markDirty(this.getPlayer().world.getTotalWorldTime())) { + invalidateCraftingCandidates(); + } + } + + private void resolveDeferredCraftingRecipe() { + if (this.deferredRecipeLookup.isDue(this.getPlayer().world.getTotalWorldTime())) { + resolveCraftingRecipe(); + } + } + + private void clearCraftingRecipeState() { + this.currentRecipe = null; + this.recipeCandidates = List.of(); + this.recipeCandidatesInitialized = false; + this.lastCraftingIngredients = null; + invalidateCraftingCandidates(); + this.craftingInventory = null; + this.lastResolvedRecipeId = null; + this.selectedRecipeId = null; + this.deferredRecipeLookup.clear(); + setCraftingOutput(ItemStack.EMPTY); + this.slotsSupportingFluidSubstitution.clear(); + this.fluidSubstitutionSupportKnown = true; + } + + private void invalidateCraftingCandidates() { + this.craftingCandidatesValid = false; + this.recipeConflict = false; + this.recipeCandidatesRevision++; + } + + private void setCraftingOutput(ItemStack output) { + ItemStack copy = output.copy(); + this.craftOutputSlot.setResultItem(copy); + if (isServerSide()) { + this.syncedCraftingOutput = GenericStack.fromItemStack(copy); + } + } + + private boolean areCraftingIngredientsEqual(ItemStack[] ingredients) { + if (this.lastCraftingIngredients == null || this.lastCraftingIngredients.length != ingredients.length) { + return false; + } + for (int i = 0; i < ingredients.length; i++) { + if (!ItemStack.areItemStacksEqual(this.lastCraftingIngredients[i], ingredients[i])) { + return false; + } + } + return true; + } + + private void checkFluidSubstitutionSupport() { + this.slotsSupportingFluidSubstitution.clear(); + if (this.currentRecipe == null) { + return; + } + + ItemStack encodedPattern = encodeCraftingPattern(); + if (encodedPattern == null) { + return; + } + + IPatternDetails decodedPattern = PatternDetailsHelper.decodePattern(encodedPattern, this.getPlayer().world); + if (decodedPattern instanceof AECraftingPattern craftingPattern) { + for (int i = 0; i < craftingPattern.getSparseInputs().size(); i++) { + if (craftingPattern.getValidFluid(i) != null) { + this.slotsSupportingFluidSubstitution.add(i); + } + } + } + } + + public void ensureFluidSubstitutionSupport() { + if (!this.fluidSubstitutionSupportKnown) { + this.fluidSubstitutionSupportKnown = true; + checkFluidSubstitutionSupport(); + } + } + + private void ensureRecipeCandidates() { + if (this.recipeCandidatesInitialized || this.mode != EncodingMode.CRAFTING) { + return; + } + + ItemStack[] ingredients = new ItemStack[CRAFTING_GRID_SLOTS]; + boolean hasIngredient = false; + for (int i = 0; i < ingredients.length; i++) { + ingredients[i] = getEncodedCraftingIngredient(i); + if (ingredients[i] == null) { + this.recipeCandidatesInitialized = true; + return; + } + hasIngredient |= !ingredients[i].isEmpty(); + } + if (!hasIngredient) { + this.recipeCandidatesInitialized = true; + return; + } + + InventoryCrafting craftingInventory = this.craftingInventory; + if (craftingInventory == null || !areCraftingIngredientsEqual(ingredients)) { + craftingInventory = new InventoryCrafting(DUMMY_CONTAINER, CRAFTING_GRID_WIDTH, CRAFTING_GRID_HEIGHT); + this.craftingInventory = craftingInventory; + } + for (int i = 0; i < ingredients.length; i++) { + craftingInventory.setInventorySlotContents(i, ingredients[i]); + } + this.recipeCandidates = simulateCandidateOutputs( + RecipeSelection.findCandidates(craftingInventory, this.getPlayer().world), craftingInventory, + this.getPlayer().world); + this.recipeCandidatesInitialized = true; + } + + /** + * Runs the crafting-event simulation over a candidate's output so the displayed preview matches what gets + * baked into the encoded pattern. Returns null when the simulation fails, which marks the candidate as not + * encodable. + */ + @Nullable + private RecipeSelection.Candidate simulateCandidateOutput(@Nullable RecipeSelection.Candidate candidate) { + if (candidate == null) { + return null; + } + try { + var output = CraftingEventSimulation.processCraftingResult(candidate.output(), this.craftingInventory, + this.getPlayer().world); + return new RecipeSelection.Candidate(candidate.id(), candidate.recipe(), output); + } catch (RuntimeException e) { + return null; + } + } + + private static List simulateCandidateOutputs(List candidates, + InventoryCrafting input, World world) { + var processed = new ObjectArrayList(candidates.size()); + for (var candidate : candidates) { + try { + processed.add(new RecipeSelection.Candidate(candidate.id(), candidate.recipe(), + CraftingEventSimulation.processCraftingResult(candidate.output(), input, world))); + } catch (RuntimeException ignored) { + // The simulation failed for this candidate, so it cannot be encoded. + } + } + return processed; + } + + public boolean hasRecipeConflict() { + return this.mode == EncodingMode.CRAFTING && this.recipeConflict; + } + + public long getRecipeCandidatesRevision() { + return this.recipeCandidatesRevision; + } + + public void encode() { + encode(false); + } + + public void encode(boolean preferModifierAndInventory) { + if (isClientSide()) { + sendClientAction(ACTION_ENCODE, preferModifierAndInventory); + return; + } + + if (!extendedLogic().isDraftValid()) { + getPlayer().sendStatusMessage( + new TextComponentTranslation("gui.ae2gtaddon.InvalidDraft"), false); + return; + } + + tryAutoFillBlankPatterns(); + ItemStack encodedPattern = encodePattern(); + if (encodedPattern == null) { + if (this.mode == EncodingMode.CRAFTING && this.deferredRecipeLookup.isDirty()) { + return; + } + clearPattern(); + return; + } + + ItemStack encodedOutput = this.encodedPatternSlot.getStack(); + if (!encodedOutput.isEmpty() + && !PatternDetailsHelper.isEncodedPattern(encodedOutput) + && !AEItems.BLANK_PATTERN.is(encodedOutput)) { + return; + } + + boolean usedOutputPattern = !encodedOutput.isEmpty(); + if (encodedOutput.isEmpty()) { + if (!consumeBlankPatternForEncoding()) { + return; + } + } + + if (preferModifierAndInventory && insertEncodedPatternIntoModifierOrInventory(encodedPattern.copy())) { + if (usedOutputPattern) { + putEncodedPatternStackFromContainer(ItemStack.EMPTY); + } + return; + } + + insertEncodedPatternIntoOutputSlot(encodedPattern.copy()); + } + + private static boolean isAcceptedByContainer(PatternContainer container, @Nullable IPatternDetails details) { + return details != null && (details instanceof IAssemblerPattern) == container.isAssemblerPatternContainer(); + } + + private boolean insertEncodedPatternIntoModifierOrInventory(ItemStack encodedPattern) { + ItemStack remaining = this.patternModifierPanel.insertPattern(encodedPattern, false); + if (remaining.isEmpty()) { + return true; + } + + ItemStack inventoryRemainder = remaining.copy(); + return this.getPlayerInventory().addItemStackToInventory(inventoryRemainder); + } + + private void insertEncodedPatternIntoOutputSlot(ItemStack encodedPattern) { + putEncodedPatternStackFromContainer(encodedPattern); + } + + public void uploadPattern(boolean shiftDown) { + if (isClientSide()) { + sendClientAction(ACTION_UPLOAD_PATTERN, shiftDown); + return; + } + + ILinkStatus linkStatus = getHost().getLinkStatus(); + if (!linkStatus.connected()) { + if (linkStatus.statusDescription() != null) { + getPlayer().sendStatusMessage(linkStatus.statusDescription(), false); + } + return; + } + + if (this.mode == EncodingMode.PROCESSING && shiftDown) { + openProcessingPatternProviderSelection("", getValidHeiProcessingRecipeTypeUid()); + return; + } + + ItemStack encodedPattern = this.encodedPatternSlot.getStack(); + if (!PatternDetailsHelper.isEncodedPattern(encodedPattern)) { + getPlayer().sendStatusMessage(PlayerMessages.PatternUploadNoEncodedPattern.text(), false); + return; + } + + IPatternDetails details = PatternDetailsHelper.decodePattern(encodedPattern, getPlayer().world); + IGrid grid = getProviderSelectionGrid(); + if (this.mode == EncodingMode.PROCESSING) { + if (!(details instanceof AEProcessingPattern processingPattern)) { + getPlayer().sendStatusMessage(PlayerMessages.PatternUploadProcessingOnly.text(), false); + return; + } + + uploadProcessingPattern(encodedPattern, processingPattern, grid); + return; + } + + if (!(details instanceof IAssemblerPattern)) { + getPlayer().sendStatusMessage(PlayerMessages.PatternUploadAssemblerOnly.text(), false); + return; + } + + if (grid == null) { + getPlayer().sendStatusMessage(PlayerMessages.PatternUploadNoTarget.text(), false); + return; + } + + uploadAssemblerPattern(encodedPattern, grid, shiftDown); + } + + private void uploadAssemblerPattern(ItemStack encodedPattern, IGrid grid, boolean shiftDown) { + AEItemKey patternKey = AEItemKey.of(encodedPattern); + if (patternKey == null) { + getPlayer().sendStatusMessage(PlayerMessages.PatternUploadNoEncodedPattern.text(), false); + return; + } + + if (!shiftDown && grid.getCraftingService().isKnownPattern(patternKey)) { + handleDuplicateUploadFailure(PlayerMessages.PatternUploadDuplicateInNetwork); + return; + } + + List candidates = collectAssemblerPatternContainers(grid); + boolean duplicateInContainer = false; + boolean nonDuplicateCandidateFound = false; + for (PatternContainer container : candidates) { + if (container.containsPattern(patternKey)) { + duplicateInContainer = true; + continue; + } + + nonDuplicateCandidateFound = true; + if (movePatternToFirstAvailableSlot(container, encodedPattern.copy())) { + putEncodedPatternStackFromContainer(ItemStack.EMPTY); + return; + } + } + + if (duplicateInContainer && !nonDuplicateCandidateFound) { + handleDuplicateUploadFailure(PlayerMessages.PatternUploadDuplicateInContainer); + return; + } + + getPlayer().sendStatusMessage(PlayerMessages.PatternUploadNoTarget.text(), false); + } + + private void uploadProcessingPattern(ItemStack encodedPattern, AEProcessingPattern processingPattern, + @Nullable IGrid grid) { + uploadProcessingPattern(encodedPattern, grid, processingPattern.getRecipeTypeUid(), + processingPattern.getRecipeType(), new ProcessingPatternUploadActions() { + @Override + public List findProcessingPatternUploadTargets(IGrid grid, String recipeTypeUid) { + return PatternProviderUploadService.findProcessingPatternUploadTargets( + getPatternProviderMappingData(), grid, recipeTypeUid); + } + + @Override + public ProcessingPatternUploadResult uploadProcessingPatternToProvider(ItemStack encodedPattern, + IGrid grid, + PatternContainer uploadTarget) { + return ContainerExtendedEncodingTerm.this.uploadProcessingPatternToProvider(encodedPattern, grid, + uploadTarget); + } + + @Override + public void openProcessingPatternProviderSelection(String initialSearchText, String initialMappingText) { + ContainerExtendedEncodingTerm.this.openProcessingPatternProviderSelection(initialSearchText, + initialMappingText); + } + + @Override + public void sendNoProviderTargetMessage() { + getPlayer().sendStatusMessage(PlayerMessages.PatternUploadNoProviderTarget.text(), false); + } + }); + } + + static void uploadProcessingPattern(ItemStack encodedPattern, @Nullable IGrid grid, + @Nullable String recipeTypeUid, @Nullable String recipeTypeTitle, + ProcessingPatternUploadActions actions) { + Objects.requireNonNull(actions, "actions"); + PatternProviderUploadService.UploadPlan uploadPlan = + PatternProviderUploadService.getImmediateProcessingPatternUploadPlan(recipeTypeUid, recipeTypeTitle); + if (uploadPlan.openProviderSelection()) { + actions.openProcessingPatternProviderSelection(uploadPlan.initialSearchText(), + uploadPlan.initialMappingText()); + return; + } + + String automaticRecipeTypeUid = uploadPlan.recipeTypeUid(); + + if (grid == null) { + PatternProviderUploadService.UploadPlan fallbackPlan = + PatternProviderUploadService.getProcessingPatternFallbackPlan(recipeTypeUid, recipeTypeTitle, true); + actions.openProcessingPatternProviderSelection(fallbackPlan.initialSearchText(), + fallbackPlan.initialMappingText()); + return; + } + + List uploadTargets = actions.findProcessingPatternUploadTargets(grid, + automaticRecipeTypeUid); + if (uploadTargets.isEmpty()) { + handleNoProcessingPatternAutomaticTarget(grid, automaticRecipeTypeUid, recipeTypeTitle, actions); + return; + } + + uploadProcessingPatternToProviders(encodedPattern, grid, uploadTargets, automaticRecipeTypeUid, + recipeTypeTitle, actions); + } + + protected void openProcessingPatternProviderSelection(@Nullable String initialSearchText, + @Nullable String initialMappingText) { + openProviderSelection(initialSearchText, initialMappingText); + } + + private static void handleNoProcessingPatternAutomaticTarget(@Nullable IGrid grid, String recipeTypeUid, + @Nullable String recipeTypeTitle, + ProcessingPatternUploadActions actions) { + PatternProviderUploadService.UploadPlan fallbackPlan = + PatternProviderUploadService.getProcessingPatternUploadTargetPlan(0, recipeTypeUid, recipeTypeTitle, + grid != null && PatternProviderUploadService.hasAvailableProvider(grid)); + if (!fallbackPlan.openProviderSelection()) { + actions.sendNoProviderTargetMessage(); + return; + } + actions.openProcessingPatternProviderSelection(fallbackPlan.initialSearchText(), + fallbackPlan.initialMappingText()); + } + + private static void uploadProcessingPatternToProviders(ItemStack encodedPattern, IGrid grid, + List uploadTargets, + String recipeTypeUid, + @Nullable String recipeTypeTitle, + ProcessingPatternUploadActions actions) { + PatternProviderUploadService.UploadPlan uploadPlan = + PatternProviderUploadService.getProcessingPatternUploadTargetPlan(uploadTargets.size(), recipeTypeUid, + recipeTypeTitle, PatternProviderUploadService.hasAvailableProvider(grid)); + if (uploadPlan.openProviderSelection()) { + actions.openProcessingPatternProviderSelection(uploadPlan.initialSearchText(), + uploadPlan.initialMappingText()); + return; + } + + if (uploadTargets.size() != 1) { + actions.sendNoProviderTargetMessage(); + return; + } + + ProcessingPatternUploadResult result = actions.uploadProcessingPatternToProvider(encodedPattern, grid, + uploadTargets.getFirst()); + if (result == ProcessingPatternUploadResult.NO_PROVIDER_TARGET + && PatternProviderUploadService.hasAvailableProvider(grid)) { + PatternProviderUploadService.UploadPlan fallbackPlan = + PatternProviderUploadService.getProcessingPatternFallbackPlan(recipeTypeUid, recipeTypeTitle, true); + actions.openProcessingPatternProviderSelection(fallbackPlan.initialSearchText(), + fallbackPlan.initialMappingText()); + } + } + + protected ProcessingPatternUploadResult uploadProcessingPatternToProvider(ItemStack encodedPattern, IGrid grid, + PatternContainer uploadTarget) { + return PatternProviderUploadService.tryUploadProcessingPatternToProvider(getPlayer(), + (IPatternTerminalGuiHost) getHost(), grid, uploadTarget, encodedPattern); + } + + @Override + public void uploadProcessingPatternToProvider(long directoryRevision, long providerEntryId) { + ProviderSelectionSession.ProviderEntryAction action = + new ProviderSelectionSession.ProviderEntryAction(directoryRevision, providerEntryId); + if (isClientSide()) { + sendClientAction(ACTION_UPLOAD_PROCESSING_PATTERN_TO_PROVIDER, action); + return; + } + this.providerSelectionSession.uploadProcessingPatternToProvider(action); + } + + @Override + public void bindProviderMapping(long directoryRevision, long providerEntryId, String mappingText) { + ProviderSelectionSession.ProviderMappingAction action = + new ProviderSelectionSession.ProviderMappingAction(directoryRevision, providerEntryId, mappingText); + if (isClientSide()) { + sendClientAction(ACTION_BIND_PROVIDER_MAPPING, action); + return; + } + this.providerSelectionSession.bindProviderMapping(action); + } + + @Override + public void bindAndUploadProcessingPatternToProvider(long directoryRevision, long providerEntryId, + String recipeTypeUid) { + ProviderSelectionSession.ProviderMappingAction action = + new ProviderSelectionSession.ProviderMappingAction(directoryRevision, providerEntryId, recipeTypeUid); + if (isClientSide()) { + sendClientAction(ACTION_BIND_AND_UPLOAD_PROVIDER_MAPPING, action); + return; + } + this.providerSelectionSession.bindAndUploadProcessingPatternToProvider(action); + } + + @Override + public void unbindProviderMapping(long directoryRevision, long providerEntryId) { + unbindProviderMapping(directoryRevision, providerEntryId, null); + } + + @Override + public void unbindProviderMapping(long directoryRevision, long providerEntryId, @Nullable String recipeType) { + ProviderSelectionSession.ProviderMappingAction action = + new ProviderSelectionSession.ProviderMappingAction(directoryRevision, providerEntryId, recipeType); + if (isClientSide()) { + sendClientAction(ACTION_UNBIND_PROVIDER_MAPPING, action); + return; + } + this.providerSelectionSession.unbindProviderMapping(action); + } + + @Override + public void rebuildMappingsFromActiveProviders() { + if (isClientSide()) { + sendClientAction(ACTION_REBUILD_MAPPINGS_FROM_ACTIVE_PROVIDERS); + return; + } + this.providerSelectionSession.rebuildMappingsFromActiveProviders(); + } + + @Override + public void requestProviderDirectoryPage(long nonce, String query, int page, + @Nullable ProviderDirectoryPageRequest.Focus focus) { + ProviderDirectoryPageRequest request = new ProviderDirectoryPageRequest(nonce, query, page, focus); + if (isClientSide()) { + sendClientAction(ACTION_REQUEST_PROVIDER_DIRECTORY_PAGE, request); + return; + } + this.providerSelectionSession.requestProviderDirectoryPage(request); + } + + @Override + public void requestProviderMappingPage(long nonce, long directoryRevision, long providerEntryId, int page) { + ProviderMappingPageRequest request = + new ProviderMappingPageRequest(nonce, directoryRevision, providerEntryId, page); + if (isClientSide()) { + sendClientAction(ACTION_REQUEST_PROVIDER_MAPPING_PAGE, request); + return; + } + this.providerSelectionSession.requestProviderMappingPage(request); + } + + private void openProviderSelection(@Nullable String initialSearchText, @Nullable String initialMappingText) { + if (isClientSide()) { + return; + } + + this.providerSelectionSession.open(initialSearchText, initialMappingText); + } + + @Nullable + protected IGrid getProviderSelectionGrid() { + if (!getHost().getLinkStatus().connected()) { + return null; + } + IGridNode node = getGridNode(); + if (node == null || !node.isActive()) { + return null; + } + + return node.grid(); + } + + @Nullable + private IGrid requireProviderSelectionGrid() { + ILinkStatus linkStatus = getHost().getLinkStatus(); + if (!linkStatus.connected()) { + getPlayer().sendStatusMessage(linkStatus.statusDescription() != null + ? linkStatus.statusDescription() + : PlayerMessages.PatternUploadNoProviderTarget.text(), false); + return null; + } + IGrid grid = getProviderSelectionGrid(); + if (grid == null) { + getPlayer().sendStatusMessage(PlayerMessages.PatternUploadNoProviderTarget.text(), false); + } + return grid; + } + + protected PatternProviderMappingData getPatternProviderMappingData() { + return PatternProviderMappingData.get(getPlayer().world); + } + + protected void sendProviderDirectoryPage(ProviderDirectoryPage page) { + sendPacketToClient(new ProviderDirectoryPagePacket(page)); + } + + protected void sendProviderMappingPage(ProviderMappingPage page) { + sendPacketToClient(new ProviderMappingPagePacket(page)); + } + + private List collectAssemblerPatternContainers(IGrid grid) { + List containers = new ArrayList<>(); + for (Class machineClass : grid.getMachineClasses()) { + if (!PatternContainer.class.isAssignableFrom(machineClass)) { + continue; + } + + Class containerClass = machineClass.asSubclass(PatternContainer.class); + for (PatternContainer container : grid.getActiveMachines(containerClass)) { + if (container.isVisibleInTerminal() && container.isAssemblerPatternContainer()) { + containers.add(container); + } + } + } + containers.sort(Comparator.comparingLong(PatternContainer::getTerminalSortOrder)); + return containers; + } + + private boolean movePatternToFirstAvailableSlot(PatternContainer container, ItemStack encodedPattern) { + InternalInventory inventory = container.getTerminalPatternInventory(); + for (int slot = 0; slot < inventory.size(); slot++) { + if (!inventory.getStackInSlot(slot).isEmpty()) { + continue; + } + + InternalInventory targetSlot = new FilteredInternalInventory(inventory.getSlotInv(slot), + new PatternSlotFilter(container, getPlayer().world)); + ItemStack remainder = targetSlot.addItems(encodedPattern.copy()); + if (remainder.isEmpty()) { + return true; + } + } + return false; + } + + private void handleDuplicateUploadFailure(PlayerMessages message) { + ItemStack encodedPattern = this.encodedPatternSlot.getStack(); + if (encodedPattern.isEmpty()) { + getPlayer().sendStatusMessage(message.text(), false); + return; + } + + ItemStack blankPattern = AEItems.BLANK_PATTERN.stack(encodedPattern.getCount()); + putEncodedPatternStackFromContainer(ItemStack.EMPTY); + blankPattern = insertBlankPatternIntoBlankSlot(blankPattern); + blankPattern = insertBlankPatternIntoPlayerInventory(blankPattern); + blankPattern = insertBlankPatternIntoNetwork(blankPattern); + if (!blankPattern.isEmpty()) { + dropBlankPattern(blankPattern); + } + getPlayer().sendStatusMessage(message.text(), false); + } + + private ItemStack insertBlankPatternIntoBlankSlot(ItemStack blankPattern) { + return this.blankPatternSlot.getSlotInv().addItems(blankPattern); + } + + private ItemStack insertBlankPatternIntoPlayerInventory(ItemStack blankPattern) { + if (blankPattern.isEmpty()) { + return ItemStack.EMPTY; + } + + ItemStack remaining = blankPattern.copy(); + if (getPlayerInventory().addItemStackToInventory(remaining)) { + return ItemStack.EMPTY; + } + return remaining; + } + + private ItemStack insertBlankPatternIntoNetwork(ItemStack blankPattern) { + if (blankPattern.isEmpty()) { + return ItemStack.EMPTY; + } + + AEItemKey blankPatternKey = AEItemKey.of(blankPattern); + if (blankPatternKey == null) { + return blankPattern; + } + + long inserted = StorageHelper.poweredInsert(this.energySource, this.storage, blankPatternKey, + blankPattern.getCount(), getActionSource()); + if (inserted <= 0) { + return blankPattern; + } + + ItemStack remaining = blankPattern.copy(); + remaining.shrink((int) inserted); + return remaining; + } + + private void dropBlankPattern(ItemStack blankPattern) { + if (blankPattern.isEmpty() || getPlayer().world == null) { + return; + } + + EntityItem drop = getPlayer().dropItem(blankPattern, false); + if (drop != null) { + drop.setNoPickupDelay(); + } + } + + private boolean consumeBlankPatternForEncoding() { + ItemStack blankPattern = this.blankPatternSlot.getStack().copy(); + if (AEItems.BLANK_PATTERN.is(blankPattern)) { + blankPattern.shrink(1); + this.blankPatternSlot.putStack(blankPattern); + return true; + } + return this.patternModifierPanel.consumeBlankPattern(); + } + + private void tryAutoFillBlankPatterns() { + if (!shouldAutoFillBlankPatterns()) { + return; + } + + ItemStack currentStack = this.blankPatternSlot.getStack(); + if (!currentStack.isEmpty() && !AEItems.BLANK_PATTERN.is(currentStack)) { + return; + } + + ItemStack blankPatternPrototype = AEItems.BLANK_PATTERN.stack(1); + int maxStackSize = currentStack.isEmpty() ? blankPatternPrototype.getMaxStackSize() + : currentStack.getMaxStackSize(); + int missing = maxStackSize - currentStack.getCount(); + if (missing <= 0) { + return; + } + + AEItemKey blankPatternKey = AEItemKey.of(blankPatternPrototype); + if (blankPatternKey == null) { + return; + } + + long extracted = StorageHelper.poweredExtraction(this.energySource, this.storage, blankPatternKey, missing, + getActionSource()); + if (extracted <= 0) { + return; + } + + if (currentStack.isEmpty()) { + this.blankPatternSlot.putStack(AEItems.BLANK_PATTERN.stack((int) extracted)); + return; + } + + ItemStack updatedStack = currentStack.copy(); + updatedStack.grow((int) extracted); + this.blankPatternSlot.putStack(updatedStack); + } + + private boolean shouldAutoFillBlankPatterns() { + return isServerSide() + && getHost().getConfigManager().getSetting(Settings.PATTERN_AUTO_FILL) == YesNo.YES; + } + + private void clearPattern() { + ItemStack encodedPattern = this.encodedPatternSlot.getStack(); + if (PatternDetailsHelper.isEncodedPattern(encodedPattern)) { + putEncodedPatternStackFromContainer(AEItems.BLANK_PATTERN.stack(encodedPattern.getCount())); + } + } + + private void putEncodedPatternStackFromContainer(ItemStack stack) { + Objects.requireNonNull(stack, "stack"); + if (this.changingEncodedPatternSlotInternally) { + throw new IllegalStateException("Nested encoded pattern slot mutation"); + } + this.changingEncodedPatternSlotInternally = true; + try { + this.encodedPatternSlot.putStack(stack); + } finally { + this.changingEncodedPatternSlotInternally = false; + } + } + + @Nullable + private ItemStack encodePattern() { + return switch (this.mode) { + case CRAFTING -> encodeCraftingPattern(); + case PROCESSING -> encodeProcessingPattern(); + }; + } + + @Nullable + private ItemStack encodeCraftingPattern() { + ItemStack[] ingredients = new ItemStack[CRAFTING_GRID_SLOTS]; + boolean valid = false; + for (int i = 0; i < ingredients.length; i++) { + ingredients[i] = getEncodedCraftingIngredient(i); + if (ingredients[i] == null) { + return null; + } + if (!ingredients[i].isEmpty()) { + valid = true; + } + } + if (!valid) { + return null; + } + + if (this.deferredRecipeLookup.isDirty()) { + return null; + } + ItemStack result = this.craftOutputSlot.getStack(); + if (this.currentRecipe == null || result.isEmpty()) { + return null; + } + + return PatternDetailsHelper.encodeCraftingPattern(this.currentRecipe, ingredients, result, isSubstitute(), + isSubstituteFluids()); + } + + @Nullable + private ItemStack encodeProcessingPattern() { + GenericStack[] inputs = new GenericStack[this.encodedInputsInv.size()]; + boolean valid = false; + for (int slot = 0; slot < this.encodedInputsInv.size(); slot++) { + inputs[slot] = this.encodedInputsInv.getStack(slot); + if (inputs[slot] != null) { + valid = true; + } + } + if (!valid) { + return null; + } + + GenericStack[] outputs = new GenericStack[this.encodedOutputsInv.size()]; + for (int slot = 0; slot < this.encodedOutputsInv.size(); slot++) { + outputs[slot] = this.encodedOutputsInv.getStack(slot); + } + if (outputs[0] == null) { + return null; + } + + String recipeTypeUid = getValidHeiProcessingRecipeTypeUid(); + return PatternDetailsHelper.encodeProcessingPattern(Arrays.asList(inputs), Arrays.asList(outputs), + recipeTypeUid); + } + + @Nullable + private String getValidHeiProcessingRecipeTypeUid() { + if (this.mode != EncodingMode.PROCESSING || this.heiProcessingRecipeSnapshot == null) { + return null; + } + + List> candidatesBySlot = this.heiProcessingRecipeSnapshot.inputCandidatesBySlot; + int nonEmptyInputs = 0; + for (int slot = 0; slot < this.encodedInputsInv.size(); slot++) { + AEKey currentKey = this.encodedInputsInv.getKey(slot); + if (currentKey == null) { + continue; + } + + if (slot >= candidatesBySlot.size() || !candidatesBySlot.get(slot).contains(currentKey)) { + return null; + } + nonEmptyInputs++; + } + + return nonEmptyInputs == candidatesBySlot.size() ? this.heiProcessingRecipeSnapshot.recipeTypeUid : null; + } + + @Nullable + private ItemStack getEncodedCraftingIngredient(int slot) { + AEKey what = this.encodedInputsInv.getKey(slot); + if (what == null) { + return ItemStack.EMPTY; + } + if (what instanceof AEItemKey itemKey) { + return itemKey.toStack(1); + } + return null; + } + + @Nullable + private HeiProcessingRecipeSnapshot parseHeiProcessingRecipeSnapshot(@Nullable HeiProcessingRecipeRequest request) { + if (request == null || request.recipeTypeUid == null || request.recipeTypeUid.isEmpty() + || request.inputCandidateKeyTags == null || request.inputCandidateKeyTags.isEmpty()) { + return null; + } + if (request.inputCandidateKeyTags.size() > MAX_HEI_PROCESSING_RECIPE_INPUT_SLOTS) { + return null; + } + + List> candidatesBySlot = new ArrayList<>(request.inputCandidateKeyTags.size()); + int totalCandidates = 0; + for (List candidateTags : request.inputCandidateKeyTags) { + if (candidateTags == null || candidateTags.isEmpty()) { + return null; + } + if (candidateTags.size() > MAX_HEI_PROCESSING_RECIPE_CANDIDATES_PER_SLOT) { + return null; + } + totalCandidates += candidateTags.size(); + if (totalCandidates > MAX_HEI_PROCESSING_RECIPE_TOTAL_CANDIDATES) { + return null; + } + + List candidates = new ArrayList<>(candidateTags.size()); + for (String candidateTag : candidateTags) { + AEKey key = parseKey(candidateTag); + if (key != null && !candidates.contains(key)) { + candidates.add(key); + } + } + if (candidates.isEmpty()) { + return null; + } + candidatesBySlot.add(candidates); + } + + return new HeiProcessingRecipeSnapshot(request.recipeTypeUid, candidatesBySlot); + } + + public void clear() { + if (isClientSide()) { + sendClientAction(ACTION_CLEAR); + return; + } + + this.encodedInputsInv.clear(); + this.encodedOutputsInv.clear(); + clearCraftingRecipeState(); + this.broadcastChanges(); + } + + public void setClearOnClose(boolean clearOnClose) { + if (isClientSide()) { + sendClientAction(ACTION_SET_CLEAR_ON_CLOSE, clearOnClose); + return; + } + + this.clearOnClose = clearOnClose; + } + + public EncodingMode getMode() { + return this.mode; + } + + public List getRecipeCandidates() { + ensureRecipeCandidates(); + return this.recipeCandidates; + } + + @Nullable + public ResourceLocation getSelectedRecipeId() { + return this.selectedRecipeId; + } + + public void selectRecipe(ResourceLocation recipeId) { + if (isClientSide()) { + sendClientAction(ACTION_SELECT_RECIPE, recipeId.toString()); + return; + } + applyRecipeSelection(recipeId); + } + + private void selectRecipeFromClient(String recipeId) { + try { + if (this.deferredRecipeLookup.isDirty()) { + return; + } + applyRecipeSelection(new ResourceLocation(recipeId)); + } catch (RuntimeException e) { + AELog.warn(e, "Ignoring invalid crafting recipe selection '%s'", recipeId); + } + } + + private void applyRecipeSelection(ResourceLocation recipeId) { + ensureRecipeCandidates(); + RecipeSelection.Candidate selected = RecipeSelection.select(this.recipeCandidates, recipeId); + if (selected == null || !selected.id().equals(recipeId)) { + return; + } + IRecipe previousRecipe = this.currentRecipe; + this.selectedRecipeId = selected.id(); + this.currentRecipe = selected.recipe(); + this.lastResolvedRecipeId = selected.id(); + setCraftingOutput(selected.output()); + if (this.currentRecipe != previousRecipe) { + this.slotsSupportingFluidSubstitution.clear(); + this.fluidSubstitutionSupportKnown = false; + } + } + + public void setMode(EncodingMode mode) { + if (mode != EncodingMode.PROCESSING) { + throw new IllegalArgumentException("Extended terminal only supports processing patterns"); + } + if (this.mode == mode) { + return; + } + if (isClientSide()) { + sendClientAction(ACTION_SET_MODE, mode); + } + changeMode(mode); + } + + private void changeMode(EncodingMode mode) { + this.mode = mode; + this.currentRecipe = null; + this.fluidSubstitutionSupportKnown = false; + if (isServerSide()) { + markCraftingRecipeDirty(); + } + updateSlotVisibility(); + if (isServerSide()) { + this.encodingLogic.setMode(mode); + if (mode == EncodingMode.CRAFTING) { + markCraftingRecipeDirty(); + } else { + clearCraftingRecipeState(); + } + } + } + + public boolean isSubstitute() { + return this.substitute; + } + + public void setSubstitute(boolean substitute) { + if (isClientSide()) { + sendClientAction(ACTION_SET_SUBSTITUTION, substitute); + } + changeSubstitution(substitute); + } + + private void changeSubstitution(boolean substitute) { + this.substitute = substitute; + if (isServerSide()) { + this.encodingLogic.setSubstitution(substitute); + } + this.currentRecipe = null; + this.fluidSubstitutionSupportKnown = false; + if (isServerSide()) { + markCraftingRecipeDirty(); + } + } + + public boolean isSubstituteFluids() { + return this.substituteFluids; + } + + public void setSubstituteFluids(boolean substituteFluids) { + if (isClientSide()) { + sendClientAction(ACTION_SET_FLUID_SUBSTITUTION, substituteFluids); + } + changeFluidSubstitution(substituteFluids); + } + + public YesNo getAutoFillPatterns() { + return this.autoFillPatterns; + } + + public boolean isBlankPatternSlot(@Nullable Slot slot) { + return slot == this.blankPatternSlot; + } + + public long getSyncedNetworkBlankPatternCount() { + return this.networkBlankPatternCount; + } + + @Override + public long getProviderDirectoryRevision() { + return this.providerDirectoryRevision; + } + + @Override + public int getProviderSelectionOverlayRequestNonce() { + return this.providerSelectionOverlayRequestNonce; + } + + @Override + public String getProviderSelectionOverlaySearchText() { + return this.providerSelectionOverlaySearchText; + } + + @Override + public String getProviderSelectionOverlayMappingText() { + return this.providerSelectionOverlayMappingText; + } + + private record PatternSlotFilter(PatternContainer container, World level) implements IAEItemFilter { + + @Override + public boolean allowInsert(InternalInventory inv, int slot, ItemStack stack) { + return !stack.isEmpty() + && isAcceptedByContainer(this.container, PatternDetailsHelper.decodePattern(stack, this.level)); + } + } + + private long computeNetworkBlankPatternCount() { + AEItemKey blankPatternKey = AEItemKey.of(AEItems.BLANK_PATTERN.stack(1)); + if (blankPatternKey == null) { + return 0; + } + return getPreviousAvailableStacks().get(blankPatternKey); + } + + public boolean isPatternModifierPanelAvailable() { + return this.patternModifierPanelAvailable; + } + + public PatternModifierPanel getPatternModifierPanel() { + return this.patternModifierPanel; + } + + @Override + public void registerPatternModifierPanelAction(String action, Runnable runnable) { + registerClientAction(action, runnable); + } + + @Override + public void sendPatternModifierPanelAction(String action) { + sendClientAction(action); + } + + @Override + public void lockPatternModifierPlayerInventorySlot(int slot) { + lockPlayerInventorySlot(slot); + } + + public void updatePatternModifierPanelVisibleSlots(boolean visible) { + if (isClientSide() && this.patternModifierPanelVisible != visible) { + sendClientAction(ACTION_SET_PATTERN_MODIFIER_PANEL_VISIBLE, visible); + } + applyPatternModifierPanelVisible(visible); + } + + private void setPatternModifierPanelVisibleFromClient(boolean visible) { + applyPatternModifierPanelVisible(visible); + } + + private void applyPatternModifierPanelVisible(boolean visible) { + this.patternModifierPanelVisible = visible; + this.patternModifierPanel.updateSlotState(visible && this.patternModifierPanelAvailable); + } + + private void changeFluidSubstitution(boolean substituteFluids) { + this.substituteFluids = substituteFluids; + if (isServerSide()) { + this.encodingLogic.setFluidSubstitution(substituteFluids); + } + this.currentRecipe = null; + this.fluidSubstitutionSupportKnown = false; + } + + @Override + protected int transferStackToContainer(ItemStack input) { + int initialCount = input.getCount(); + + if (this.blankPatternSlot.isItemValid(input)) { + input = this.blankPatternSlot.getSlotInv().addItems(input); + if (input.isEmpty()) { + return initialCount; + } + } + + if (this.encodedPatternSlot.isItemValid(input)) { + input = this.encodedPatternSlot.getSlotInv().addItems(input); + if (input.isEmpty()) { + return initialCount; + } + } + + int transferred = initialCount - input.getCount(); + return transferred + super.transferStackToContainer(input); + } + + public FakeSlot[] getCraftingGridSlots() { + return this.craftingGridSlots; + } + + public void setHeiProcessingRecipe(String recipeTypeUid, List> inputCandidateKeyTags) { + if (isClientSide()) { + if (recipeTypeUid == null || recipeTypeUid.isEmpty()) { + return; + } + if (!fitsHeiProcessingRecipePayload(recipeTypeUid, inputCandidateKeyTags)) { + AELog.warn("HEI processing recipe type %s was not sent because the candidate payload is too large", + recipeTypeUid); + return; + } + sendClientAction(ACTION_SET_HEI_PROCESSING_RECIPE, + new HeiProcessingRecipeRequest(recipeTypeUid, inputCandidateKeyTags)); + } + } + + private static boolean fitsHeiProcessingRecipePayload(String recipeTypeUid, + @Nullable List> inputCandidateKeyTags) { + long estimate = 128L + 2L * recipeTypeUid.length(); + if (inputCandidateKeyTags != null) { + for (List candidates : inputCandidateKeyTags) { + if (candidates == null) { + continue; + } + for (String candidate : candidates) { + if (candidate != null) { + estimate += 4L + 2L * candidate.length(); + } + } + } + } + return estimate <= MAX_SET_HEI_PROCESSING_RECIPE_PAYLOAD_LENGTH; + } + + public void setHeiCraftingRecipe(@Nullable ResourceLocation recipeId) { + if (isClientSide()) { + sendClientAction(ACTION_SET_HEI_CRAFTING_RECIPE, recipeId == null ? "" : recipeId.toString()); + } + } + + private void setHeiCraftingRecipeFromClient(String serializedRecipeId) { + if (!isServerSide()) { + return; + } + if (serializedRecipeId == null || serializedRecipeId.isEmpty()) { + markCraftingRecipeDirty(); + return; + } + + ResourceLocation recipeId; + try { + recipeId = new ResourceLocation(serializedRecipeId); + } catch (RuntimeException e) { + AELog.warn(e, "HEI supplied a malformed crafting recipe id '%s'", serializedRecipeId); + markCraftingRecipeDirty(); + return; + } + + if (!applyHeiCraftingRecipe(recipeId)) { + AELog.warn("HEI crafting recipe %s is not registered or does not match the imported ingredients; falling back to recipe lookup", + recipeId); + markCraftingRecipeDirty(); + } + } + + private boolean applyHeiCraftingRecipe(ResourceLocation recipeId) { + if (this.mode != EncodingMode.CRAFTING) { + return false; + } + + ItemStack[] ingredients = new ItemStack[CRAFTING_GRID_SLOTS]; + boolean hasIngredient = false; + for (int i = 0; i < ingredients.length; i++) { + ingredients[i] = getEncodedCraftingIngredient(i); + if (ingredients[i] == null) { + return false; + } + hasIngredient |= !ingredients[i].isEmpty(); + } + if (!hasIngredient) { + return false; + } + + InventoryCrafting input = new InventoryCrafting(DUMMY_CONTAINER, CRAFTING_GRID_WIDTH, CRAFTING_GRID_HEIGHT); + for (int i = 0; i < ingredients.length; i++) { + input.setInventorySlotContents(i, ingredients[i]); + } + + RecipeSelection.Candidate selected; + try { + selected = RecipeSelection.findCandidateById(input, this.getPlayer().world, recipeId); + } catch (RuntimeException | LinkageError e) { + AELog.warn(e, "HEI crafting recipe %s failed while validating imported ingredients", recipeId); + return false; + } + if (selected == null) { + return false; + } + + IRecipe previousRecipe = this.currentRecipe; + this.craftingInventory = input; + this.lastCraftingIngredients = copyCraftingIngredients(ingredients); + this.craftingCandidatesValid = true; + this.recipeCandidates = List.of(selected); + this.recipeCandidatesInitialized = true; + this.currentRecipe = selected.recipe(); + this.selectedRecipeId = selected.id(); + this.lastResolvedRecipeId = selected.id(); + this.recipeConflict = false; + this.recipeCandidatesRevision++; + setCraftingOutput(selected.output()); + this.deferredRecipeLookup.clear(); + if (this.currentRecipe != previousRecipe) { + this.slotsSupportingFluidSubstitution.clear(); + this.fluidSubstitutionSupportKnown = false; + } + return true; + } + + public boolean isMergeHeiProcessingInputs() { + return this.mergeHeiProcessingInputs; + } + + public void setMergeHeiProcessingInputs(boolean merge) { + if (isClientSide()) { + sendClientAction(ACTION_SET_HEI_PROCESSING_MERGE, merge); + } + changeMergeHeiProcessingInputs(merge); + } + + private void changeMergeHeiProcessingInputs(boolean merge) { + this.mergeHeiProcessingInputs = merge; + if (isServerSide()) { + this.encodingLogic.setMergeHeiProcessingInputs(merge); + } + } + + private void setHeiProcessingRecipe(HeiProcessingRecipeRequest request) { + if (isClientSide()) { + return; + } + + this.heiProcessingRecipeSnapshot = parseHeiProcessingRecipeSnapshot(request); + } + + @Nullable + private static AEKey parseKey(@Nullable String serializedKey) { + if (serializedKey == null || serializedKey.isEmpty() + || serializedKey.length() > MAX_HEI_PROCESSING_RECIPE_KEY_TAG_LENGTH) { + return null; + } + + try { + return AEKey.fromTagGeneric(JsonToNBT.getTagFromJson(serializedKey)); + } catch (NBTException | RuntimeException e) { + NetworkPacketHelper.warnMalformedPacket(e, "hei-processing-recipe-key", + "Ignoring malformed HEI processing recipe key"); + return null; + } + } + + @Override + public void onContainerClosed(EntityPlayer player) { + if (isServerSide()) { + this.providerSelectionSession.close(); + } + if (isServerSide() && this.clearOnClose) { + clear(); + } + super.onContainerClosed(player); + } + + public void renameProcessingPatternItem(int slotNumber, String name) { + if (isClientSide()) { + sendClientAction(ACTION_RENAME_PROCESSING_PATTERN_ITEM, new RenamePatternItemRequest(slotNumber, name)); + } + } + + private void renameProcessingPatternItem(RenamePatternItemRequest request) { + if (isClientSide() || request == null) { + return; + } + + Slot slot = getSlotByNumber(request.slotNumber); + if (slot == null) { + return; + } + if (!isProcessingPatternItemSlot(slot)) { + return; + } + + GenericStack stack = getProcessingStack(slot); + if (stack == null || !(stack.what() instanceof AEItemKey itemKey)) { + return; + } + + ItemStack renamed = itemKey.toStack((int) Math.min(stack.amount(), Integer.MAX_VALUE)); + if (request.name != null && request.name.length() > MAX_CUSTOM_NAME_LENGTH) { + return; + } + if (request.name == null || request.name.isEmpty()) { + renamed.clearCustomName(); + } else { + renamed.setStackDisplayName(request.name); + } + setProcessingStack(slot, GenericStack.fromItemStack(renamed)); + broadcastChanges(); + } + + public boolean isProcessingPatternItemSlot(@Nullable Slot slot) { + if (slot == null || this.mode != EncodingMode.PROCESSING) { + return false; + } + + GenericStack stack = getProcessingStack(slot); + return stack != null && stack.what() instanceof AEItemKey; + } + + public FakeSlot[] getProcessingInputSlots() { + return this.processingInputSlots; + } + + public FakeSlot[] getProcessingOutputSlots() { + return this.processingOutputSlots; + } + + private ExtendedEncodingLogic extendedLogic() { + return (ExtendedEncodingLogic) this.encodingLogic; + } + + public ExtendedEncodingMode getExtendedMode() { + return this.clientExtendedMode != null ? this.clientExtendedMode : this.extendedMode; + } + + public void setExtendedMode(ExtendedEncodingMode mode) { + Objects.requireNonNull(mode, "mode"); + if (isClientSide()) { + sendClientAction(ACTION_SET_EXTENDED_MODE, mode); + this.clientExtendedMode = mode; + updateExtendedSlots(); + } else { + changeExtendedMode(mode); + } + } + + private void changeExtendedMode(ExtendedEncodingMode mode) { + this.extendedModeRevision++; + if (!extendedLogic().canSwitchTo(mode)) { + getPlayer().sendStatusMessage( + new TextComponentTranslation("gui.ae2gtaddon.ModeSwitchWouldLoseData"), false); + updateExtendedSlots(); + return; + } + extendedLogic().setExtendedMode(mode); + this.extendedMode = mode; + updateExtendedSlots(); + } + + public void prepareProcessingRecipe(ExtendedEncodingMode mode) { + Objects.requireNonNull(mode, "mode"); + if (isClientSide()) { + sendClientAction(ACTION_PREPARE_PROCESSING_RECIPE, mode); + this.clientExtendedMode = mode; + updateExtendedSlots(); + } else { + clear(); + changeExtendedMode(mode); + } + } + + private void updateExtendedSlots() { + ExtendedEncodingMode mode = getExtendedMode(); + boolean processing = this.mode == EncodingMode.PROCESSING; + for (int index = 0; index < this.processingInputSlots.length; index++) { + this.processingInputSlots[index].setActive(processing && index < mode.getInputSlots()); + } + for (int index = 0; index < this.processingOutputSlots.length; index++) { + this.processingOutputSlots[index].setActive(processing && index < mode.getOutputSlots()); + } + } + + public FakeSlot[] getActiveProcessingInputSlots() { + return Arrays.copyOf(this.processingInputSlots, getExtendedMode().getInputSlots()); + } + + public FakeSlot[] getActiveProcessingOutputSlots() { + return Arrays.copyOf(this.processingOutputSlots, getExtendedMode().getOutputSlots()); + } + + private boolean isActiveLayoutSlot(@Nullable Slot slot) { + if (slot == null) { + return false; + } + int inputLimit = Math.min(getExtendedMode().getInputSlots(), this.processingInputSlots.length); + for (int index = 0; index < inputLimit; index++) { + if (this.processingInputSlots[index] == slot) { + return true; + } + } + int outputLimit = Math.min(getExtendedMode().getOutputSlots(), this.processingOutputSlots.length); + for (int index = 0; index < outputLimit; index++) { + if (this.processingOutputSlots[index] == slot) { + return true; + } + } + return false; + } + + private boolean isOutsideLayout(int slotIndex) { + if (slotIndex < 0 || slotIndex >= this.inventorySlots.size()) { + return false; + } + Slot slot = this.inventorySlots.get(slotIndex); + return (getSlots(SlotSemantics.PROCESSING_INPUTS).contains(slot) + || getSlots(SlotSemantics.PROCESSING_OUTPUTS).contains(slot)) && !isActiveLayoutSlot(slot); + } + + @Override + public Slot addSlot(Slot slot, SlotSemantic semantic) { + if (semantic == SlotSemantics.CRAFTING_GRID || semantic == SlotSemantics.CRAFTING_RESULT) { + ae2gtaddon$deactivateSlot(slot); + return slot; + } + return super.addSlot(slot, semantic); + } + + private static void ae2gtaddon$deactivateSlot(Slot slot) { + if (slot instanceof AppEngSlot appEngSlot) { + appEngSlot.setActive(false); + } else if (slot instanceof PatternTermSlot patternTermSlot) { + patternTermSlot.setActive(false); + } + } + + @Override + public boolean isValidForSlot(Slot slot, ItemStack stack) { + if (getSlots(SlotSemantics.ENCODED_PATTERN).contains(slot)) { + return extendedLogic().canLoadPattern(stack); + } + return super.isValidForSlot(slot, stack); + } + + @Override + public void setFilter(int slotIndex, ItemStack stack, boolean preferEmptying) { + if (isOutsideLayout(slotIndex)) { + return; + } + super.setFilter(slotIndex, stack, preferEmptying); + } + + @Override + public void doAction(EntityPlayerMP player, InventoryAction action, int slotIndex, long id) { + if (isOutsideLayout(slotIndex)) { + return; + } + super.doAction(player, action, slotIndex, id); + } + + @Override + public ItemStack slotClick(int slotIndex, int dragType, ClickType clickType, EntityPlayer player) { + return isOutsideLayout(slotIndex) ? ItemStack.EMPTY + : super.slotClick(slotIndex, dragType, clickType, player); + } + + public boolean ignoreCircuit() { + return this.ignoreCircuitInHeiProcessingPattern; + } + + public void setIgnoreCircuit(boolean ignoreCircuit) { + this.ignoreCircuitInHeiProcessingPattern = ignoreCircuit; + } + + public boolean isAutoNamingItems() { + return this.autoNamePatternItemsInHeiImport; + } + + public void setAutoNamingItems(boolean autoName) { + this.autoNamePatternItemsInHeiImport = autoName; + } + + public void cycleProcessingOutput() { + if (isClientSide()) { + sendClientAction(ACTION_CYCLE_PROCESSING_OUTPUT); + return; + } + if (this.mode != EncodingMode.PROCESSING) { + return; + } + + ItemStack[] newOutputs = new ItemStack[this.processingOutputSlots.length]; + Arrays.fill(newOutputs, ItemStack.EMPTY); + for (int i = 0; i < this.processingOutputSlots.length; i++) { + ItemStack current = this.processingOutputSlots[i].getStack(); + if (current.isEmpty()) { + continue; + } + for (int j = 1; j < this.processingOutputSlots.length; j++) { + ItemStack next = this.processingOutputSlots[(i + j) % this.processingOutputSlots.length].getStack(); + if (!next.isEmpty()) { + newOutputs[i] = next.copy(); + break; + } + } + } + + for (int i = 0; i < newOutputs.length; i++) { + this.processingOutputSlots[i].putStack(newOutputs[i]); + } + } + + public void clearProcessingSecondaryOutputs() { + if (isClientSide()) { + sendClientAction(ACTION_CLEAR_PROCESSING_SECONDARY_OUTPUTS); + return; + } + if (this.mode != EncodingMode.PROCESSING) { + return; + } + + for (int slot = 1; slot < this.encodedOutputsInv.size(); slot++) { + this.encodedOutputsInv.setStack(slot, null); + } + broadcastChanges(); + } + + public void modifyProcessingPatternAmounts(ProcessingPatternAmountHelper.Operation operation) { + if (isClientSide()) { + sendClientAction(actionForProcessingAmountOperation(operation)); + return; + } + if (this.mode != EncodingMode.PROCESSING) { + return; + } + + List stacks = new ArrayList<>(this.encodedInputsInv.size() + this.encodedOutputsInv.size()); + collectProcessingStacks(this.encodedInputsInv, stacks); + collectProcessingStacks(this.encodedOutputsInv, stacks); + if (!ProcessingPatternAmountHelper.canApply(stacks, operation)) { + return; + } + + applyProcessingAmountOperation(this.encodedInputsInv, operation); + applyProcessingAmountOperation(this.encodedOutputsInv, operation); + broadcastChanges(); + } + + public boolean canCycleProcessingOutputs() { + if (this.mode != EncodingMode.PROCESSING) { + return false; + } + long nonEmpty = 0; + for (FakeSlot slot : this.processingOutputSlots) { + if (!slot.getStack().isEmpty()) { + nonEmpty++; + if (nonEmpty > 1) { + return true; + } + } + } + return false; + } + + public boolean canModifyAmountForSlot(@Nullable Slot slot) { + if (!isProcessingPatternSlot(slot)) { + return false; + } + assert slot != null; + return slot.getHasStack(); + } + + public boolean isProcessingPatternSlot(@Nullable Slot slot) { + if (slot == null || this.mode != EncodingMode.PROCESSING) { + return false; + } + + for (FakeSlot processingInputSlot : this.processingInputSlots) { + if (processingInputSlot == slot) { + return true; + } + } + for (FakeSlot processingOutputSlot : this.processingOutputSlots) { + if (processingOutputSlot == slot) { + return true; + } + } + return false; + } + + @Nullable + private GenericStack getProcessingStack(Slot slot) { + for (int i = 0; i < this.processingInputSlots.length; i++) { + if (this.processingInputSlots[i] == slot) { + return this.encodedInputsInv.getStack(i); + } + } + for (int i = 0; i < this.processingOutputSlots.length; i++) { + if (this.processingOutputSlots[i] == slot) { + return this.encodedOutputsInv.getStack(i); + } + } + return null; + } + + private void setProcessingStack(Slot slot, @Nullable GenericStack stack) { + for (int i = 0; i < this.processingInputSlots.length; i++) { + if (this.processingInputSlots[i] == slot) { + this.encodedInputsInv.setStack(i, stack); + return; + } + } + for (int i = 0; i < this.processingOutputSlots.length; i++) { + if (this.processingOutputSlots[i] == slot) { + this.encodedOutputsInv.setStack(i, stack); + return; + } + } + } + + private Slot getSlotByNumber(int slotNumber) { + for (Slot slot : this.inventorySlots) { + if (slot.slotNumber == slotNumber) { + return slot; + } + } + return null; + } + + @SuppressWarnings("ClassCanBeRecord") + private static final class RenamePatternItemRequest { + private final int slotNumber; + private final String name; + + private RenamePatternItemRequest(int slotNumber, String name) { + this.slotNumber = slotNumber; + this.name = name; + } + } + + @SuppressWarnings("ClassCanBeRecord") + private static final class HeiProcessingRecipeRequest { + private final String recipeTypeUid; + private final List> inputCandidateKeyTags; + + private HeiProcessingRecipeRequest(String recipeTypeUid, List> inputCandidateKeyTags) { + this.recipeTypeUid = recipeTypeUid; + this.inputCandidateKeyTags = inputCandidateKeyTags; + } + } + + interface ProcessingPatternUploadActions { + List findProcessingPatternUploadTargets(IGrid grid, String recipeTypeUid); + + ProcessingPatternUploadResult uploadProcessingPatternToProvider(ItemStack encodedPattern, IGrid grid, + PatternContainer uploadTarget); + + void openProcessingPatternProviderSelection(String initialSearchText, String initialMappingText); + + void sendNoProviderTargetMessage(); + } + + private record HeiProcessingRecipeSnapshot(String recipeTypeUid, List> inputCandidatesBySlot) { + } +} diff --git a/src/main/java/com/formlessDragon/ae2gtaddon/container/ExtendedEncodingTermGuiHandler.java b/src/main/java/com/formlessDragon/ae2gtaddon/container/ExtendedEncodingTermGuiHandler.java new file mode 100644 index 0000000..e32b55e --- /dev/null +++ b/src/main/java/com/formlessDragon/ae2gtaddon/container/ExtendedEncodingTermGuiHandler.java @@ -0,0 +1,80 @@ +package com.formlessDragon.ae2gtaddon.container; + +import ae2.client.gui.style.GuiStyleManager; +import ae2.core.gui.PatternContainerGuiReturnContext; +import ae2.core.gui.locator.PartLocator; +import com.formlessDragon.ae2gtaddon.client.gui.GuiExtendedPatternEncodingTerm; +import com.formlessDragon.ae2gtaddon.parts.ExtendedEncodingTermPart; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.util.EnumFacing; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.text.ITextComponent; +import net.minecraft.util.text.TextComponentString; +import net.minecraft.world.World; +import net.minecraftforge.fml.common.network.IGuiHandler; +import org.jetbrains.annotations.Nullable; + +public class ExtendedEncodingTermGuiHandler implements IGuiHandler { + public static final int GUI_ID = 0; + private static final int RETURNED_FROM_SUBSCREEN_FLAG = 1 << 30; + private static final String STYLE = + "/screens/terminals/ae2gtaddon_extended_pattern_encoding_terminal.json"; + + public static int guiId(boolean returnedFromSubScreen) { + return returnedFromSubScreen ? GUI_ID | RETURNED_FROM_SUBSCREEN_FLAG : GUI_ID; + } + + @Override + public @Nullable Object getServerGuiElement(int id, EntityPlayer player, World world, int x, int y, int z) { + return createContainer(id, player, world, x, y, z); + } + + @Override + public @Nullable Object getClientGuiElement(int id, EntityPlayer player, World world, int x, int y, int z) { + ContainerExtendedEncodingTerm container = createContainer(id, player, world, x, y, z); + if (container == null) { + return null; + } + return new GuiExtendedPatternEncodingTerm(container, player.inventory, container.getGuiTitle(), + GuiStyleManager.loadStyleDoc(STYLE)); + } + + private static @Nullable ContainerExtendedEncodingTerm createContainer(int id, EntityPlayer player, World world, + int x, int y, int z) { + if (player == null || world == null) { + return null; + } + PartLocator locator = partLocator(x, y, z); + if (locator == null) { + return null; + } + ExtendedEncodingTermPart part = locator.locate(player, ExtendedEncodingTermPart.class); + if (part == null) { + return null; + } + ContainerExtendedEncodingTerm container = new ContainerExtendedEncodingTerm(player.inventory, part); + container.setLocator(locator); + container.setReturnedFromSubScreen((id & RETURNED_FROM_SUBSCREEN_FLAG) != 0); + container.setGuiTitle(defaultGuiTitle(container.getTarget())); + return PatternContainerGuiReturnContext.initializeContainer(container); + } + + private static @Nullable PartLocator partLocator(int x, int y, int z) { + int side = y >> 8; + if (side == EnumFacing.VALUES.length) { + return new PartLocator(new BlockPos(x, y & 255, z), null); + } + if (side < 0 || side > EnumFacing.VALUES.length) { + return null; + } + return new PartLocator(new BlockPos(x, y & 255, z), EnumFacing.VALUES[side]); + } + + private static @Nullable ITextComponent defaultGuiTitle(Object host) { + if (host instanceof ExtendedEncodingTermPart part && part.hasCustomName()) { + String customName = part.getCustomName(); + return customName == null || customName.isEmpty() ? null : new TextComponentString(customName); + } + return null; + } +} diff --git a/src/main/java/com/formlessDragon/ae2gtaddon/ids/ItemIds.java b/src/main/java/com/formlessDragon/ae2gtaddon/ids/ItemIds.java index fc7da63..b6f6f28 100644 --- a/src/main/java/com/formlessDragon/ae2gtaddon/ids/ItemIds.java +++ b/src/main/java/com/formlessDragon/ae2gtaddon/ids/ItemIds.java @@ -6,8 +6,8 @@ public final class ItemIds { public static final ResourceLocation CIRCUIT_CARD = id("circuit_card"); + public static final ResourceLocation EXTENDED_ENCODING_TERM = id("extended_encoding_terminal"); - @SuppressWarnings("SameParameterValue") private static ResourceLocation id(String id) { return new ResourceLocation(Tags.MOD_ID, id); } diff --git a/src/main/java/com/formlessDragon/ae2gtaddon/Init/Items.java b/src/main/java/com/formlessDragon/ae2gtaddon/init/Items.java similarity index 59% rename from src/main/java/com/formlessDragon/ae2gtaddon/Init/Items.java rename to src/main/java/com/formlessDragon/ae2gtaddon/init/Items.java index 2f74957..2fb2385 100644 --- a/src/main/java/com/formlessDragon/ae2gtaddon/Init/Items.java +++ b/src/main/java/com/formlessDragon/ae2gtaddon/init/Items.java @@ -1,13 +1,20 @@ -package com.formlessDragon.ae2gtaddon.Init; +package com.formlessDragon.ae2gtaddon.init; +import ae2.api.parts.IPart; +import ae2.api.parts.IPartItem; +import ae2.api.parts.PartModels; import ae2.api.upgrades.Upgrades; import ae2.core.definitions.ItemDefinition; +import ae2.items.parts.PartItem; +import ae2.items.parts.PartModelsHelper; import com.formlessDragon.ae2gtaddon.Tags; import com.formlessDragon.ae2gtaddon.ids.ItemIds; +import com.formlessDragon.ae2gtaddon.parts.ExtendedEncodingTermPart; import net.minecraft.client.renderer.block.model.ModelResourceLocation; import net.minecraft.creativetab.CreativeTabs; import net.minecraft.item.Item; import net.minecraft.item.ItemStack; +import net.minecraft.util.ResourceLocation; import net.minecraftforge.client.event.ModelRegistryEvent; import net.minecraftforge.client.model.ModelLoader; import net.minecraftforge.event.RegistryEvent; @@ -15,6 +22,9 @@ import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import net.minecraftforge.fml.relauncher.Side; import net.minecraftforge.fml.relauncher.SideOnly; +import org.jetbrains.annotations.NotNull; + +import java.util.function.Function; @Mod.EventBusSubscriber(modid = Tags.MOD_ID) public final class Items { @@ -22,16 +32,20 @@ public final class Items { public static final CreativeTabs TABS = new CreativeTabs(Tags.MOD_ID) { @Override + @NotNull public ItemStack createIcon() { return CIRCUIT_CARD.stack(1); } }; - public static final ItemDefinition CIRCUIT_CARD = new ItemDefinition<>(ItemIds.CIRCUIT_CARD, - Upgrades.createUpgradeCardItem(), TABS); + public static final ItemDefinition CIRCUIT_CARD = new ItemDefinition<>( + ItemIds.CIRCUIT_CARD, Upgrades.createUpgradeCardItem(), TABS); + public static final ItemDefinition> EXTENDED_ENCODING_TERM = createPart( + ItemIds.EXTENDED_ENCODING_TERM, ExtendedEncodingTermPart.class, ExtendedEncodingTermPart::new); private static final ItemDefinition[] ITEMS = { - CIRCUIT_CARD + CIRCUIT_CARD, + EXTENDED_ENCODING_TERM }; private Items() { @@ -55,4 +69,11 @@ public static void registerModels(ModelRegistryEvent event) { } } } + + @SuppressWarnings("SameParameterValue") + private static ItemDefinition> createPart(ResourceLocation id, Class partClass, + Function, T> factory) { + PartModels.registerModels(PartModelsHelper.createModels(partClass)); + return new ItemDefinition<>(id, new PartItem<>(partClass, factory), TABS); + } } diff --git a/src/main/java/com/formlessDragon/ae2gtaddon/integration/gregtech/GregTechCircuitPatternDetails.java b/src/main/java/com/formlessDragon/ae2gtaddon/integration/gregtech/GregTechCircuitPatternDetails.java index d7e523d..81c7852 100644 --- a/src/main/java/com/formlessDragon/ae2gtaddon/integration/gregtech/GregTechCircuitPatternDetails.java +++ b/src/main/java/com/formlessDragon/ae2gtaddon/integration/gregtech/GregTechCircuitPatternDetails.java @@ -13,12 +13,12 @@ import ae2.api.stacks.KeyCounter; import ae2.helpers.patternprovider.PatternProviderTarget; import ae2.helpers.patternprovider.PseudoPatternDetails; +import com.formlessDragon.ae2gtaddon.util.Matcher; +import com.formlessDragon.ae2gtaddon.util.Util; import gregtech.api.capability.IGhostSlotConfigurable; import gregtech.api.metatileentity.MetaTileEntityHolder; import gregtech.api.metatileentity.interfaces.IGregTechTileEntity; -import gregtech.api.recipes.ingredients.IntCircuitIngredient; import net.minecraft.client.util.ITooltipFlag; -import net.minecraft.item.ItemStack; import net.minecraft.tileentity.TileEntity; import net.minecraft.util.EnumFacing; import net.minecraft.world.World; @@ -38,34 +38,26 @@ * while the real item must not be inserted into the machine input inventory.

*/ public final class GregTechCircuitPatternDetails implements IPatternDetails { - public static final CircuitMatcher GREGTECH_CIRCUIT_MATCHER = new GregTechCircuitMatcher(); - public static final CircuitConfigExtractor GREGTECH_CIRCUIT_CONFIG_EXTRACTOR = new GregTechCircuitConfigExtractor(); private static final ThreadLocal ACTIVE_CIRCUIT_CONFIGURATION = new ThreadLocal<>(); private final IPatternDetails delegate; - private final CircuitMatcher circuitMatcher; private final IInput[] originalInputs; private final IInput[] filteredInputs; private final int[] filteredInputOriginalIndexes; - private GregTechCircuitPatternDetails(IPatternDetails delegate, CircuitMatcher circuitMatcher) { + private GregTechCircuitPatternDetails(IPatternDetails delegate) { this.delegate = Objects.requireNonNull(delegate, "delegate"); - this.circuitMatcher = Objects.requireNonNull(circuitMatcher, "circuitMatcher"); this.originalInputs = delegate.getInputs().clone(); - InputFilterResult filtered = filterInputs(this.originalInputs, circuitMatcher); + InputFilterResult filtered = filterInputs(this.originalInputs); this.filteredInputs = filtered.inputs(); this.filteredInputOriginalIndexes = filtered.originalIndexes(); } public static IPatternDetails filterIntegratedCircuits(IPatternDetails patternDetails) { - return filterIntegratedCircuits(patternDetails, GREGTECH_CIRCUIT_MATCHER); - } - - public static IPatternDetails filterIntegratedCircuits(IPatternDetails patternDetails, CircuitMatcher circuitMatcher) { if (patternDetails instanceof GregTechCircuitPatternDetails) { return patternDetails; } - return new GregTechCircuitPatternDetails(patternDetails, circuitMatcher); + return new GregTechCircuitPatternDetails(patternDetails); } public static IPatternDetails unwrap(IPatternDetails patternDetails) { @@ -81,7 +73,7 @@ public static IPatternDetails unwrapPseudo(IPatternDetails patternDetails) { if (unwrappedDelegate == circuitPatternDetails.delegate) { return patternDetails; } - return new GregTechCircuitPatternDetails(unwrappedDelegate, circuitPatternDetails.circuitMatcher); + return new GregTechCircuitPatternDetails(unwrappedDelegate); } return PseudoPatternDetails.unwrap(patternDetails); } @@ -139,18 +131,10 @@ private static IInput[] getCircuitSearchInputs(IPatternDetails patternDetails) { } public static OptionalInt findCircuitConfiguration(IPatternDetails patternDetails) { - return findCircuitConfiguration(patternDetails, GREGTECH_CIRCUIT_CONFIG_EXTRACTOR); - } - - public static OptionalInt findCircuitConfiguration( - IPatternDetails patternDetails, - CircuitConfigExtractor circuitConfigExtractor - ) { Objects.requireNonNull(patternDetails, "patternDetails"); - Objects.requireNonNull(circuitConfigExtractor, "circuitConfigExtractor"); for (IInput input : getCircuitSearchInputs(patternDetails)) { for (GenericStack possibleInput : input.possibleInputs()) { - OptionalInt configuration = circuitConfigExtractor.getCircuitConfiguration(possibleInput.what()); + OptionalInt configuration = Util.getCircuitConfiguration(possibleInput.what()); if (configuration.isPresent()) { return configuration; } @@ -159,14 +143,13 @@ public static OptionalInt findCircuitConfiguration( return OptionalInt.empty(); } - public static KeyCounter[] filterInputHolder(KeyCounter[] inputHolder, CircuitMatcher circuitMatcher) { + public static KeyCounter[] filterInputHolder(KeyCounter[] inputHolder) { Objects.requireNonNull(inputHolder, "inputHolder"); - Objects.requireNonNull(circuitMatcher, "circuitMatcher"); KeyCounter[] filtered = new KeyCounter[inputHolder.length]; for (int i = 0; i < inputHolder.length; i++) { filtered[i] = new KeyCounter(); for (var input : inputHolder[i]) { - if (!circuitMatcher.isCircuit(input.getKey())) { + if (!Matcher.Circuit.test(input.getKey())) { filtered[i].add(input.getKey(), input.getLongValue()); } } @@ -178,40 +161,20 @@ public static ICraftingMachine wrapCraftingMachine( ICraftingMachine machine, IGhostSlotConfigurable ghostTarget, int circuitConfiguration - ) { - return wrapCraftingMachine(machine, ghostTarget, circuitConfiguration, GREGTECH_CIRCUIT_MATCHER); - } - - public static ICraftingMachine wrapCraftingMachine( - ICraftingMachine machine, - IGhostSlotConfigurable ghostTarget, - int circuitConfiguration, - CircuitMatcher circuitMatcher ) { Objects.requireNonNull(machine, "machine"); Objects.requireNonNull(ghostTarget, "ghostTarget"); - Objects.requireNonNull(circuitMatcher, "circuitMatcher"); - return new CircuitConfiguredCraftingMachine(machine, ghostTarget, circuitConfiguration, circuitMatcher); + return new CircuitConfiguredCraftingMachine(machine, ghostTarget, circuitConfiguration); } public static IPatternProviderBatchTarget wrapBatchTarget( IPatternProviderBatchTarget batchTarget, IGhostSlotConfigurable ghostTarget, int circuitConfiguration - ) { - return wrapBatchTarget(batchTarget, ghostTarget, circuitConfiguration, GREGTECH_CIRCUIT_MATCHER); - } - - public static IPatternProviderBatchTarget wrapBatchTarget( - IPatternProviderBatchTarget batchTarget, - IGhostSlotConfigurable ghostTarget, - int circuitConfiguration, - CircuitMatcher circuitMatcher ) { Objects.requireNonNull(batchTarget, "batchTarget"); Objects.requireNonNull(ghostTarget, "ghostTarget"); - Objects.requireNonNull(circuitMatcher, "circuitMatcher"); - return new CircuitConfiguredBatchTarget(batchTarget, ghostTarget, circuitConfiguration, circuitMatcher); + return new CircuitConfiguredBatchTarget(batchTarget, ghostTarget, circuitConfiguration); } public static PatternProviderTarget wrapExternalTarget( @@ -224,10 +187,15 @@ public static PatternProviderTarget wrapExternalTarget( return new CircuitConfiguredExternalTarget(target, ghostTarget, circuitConfiguration); } + /** + * Locates the ghost-circuit-configurable target behind a crafting-machine or batch-target role. AE2 no longer + * forces {@code ICraftingMachine} to implement {@code IPatternProviderBatchTarget}, so callers may hold either + * role (or the tile entity alone) for the same machine; only {@code instanceof} checks are performed here. + */ @Nullable public static IGhostSlotConfigurable findGhostCircuitTarget( @Nullable TileEntity tileEntity, - @Nullable ICraftingMachine machine + @Nullable Object machine ) { if (machine instanceof IGhostSlotConfigurable configurable && configurable.hasGhostCircuitInventory()) { return configurable; @@ -240,24 +208,6 @@ public static IGhostSlotConfigurable findGhostCircuitTarget( return null; } - private static boolean isGregTechIntegratedCircuit(AEKey key) { - if (!(key instanceof AEItemKey itemKey)) { - return false; - } - return IntCircuitIngredient.isIntegratedCircuit(itemKey.toStack()); - } - - private static OptionalInt getGregTechCircuitConfiguration(AEKey key) { - if (!(key instanceof AEItemKey itemKey)) { - return OptionalInt.empty(); - } - ItemStack stack = itemKey.toStack(); - if (!IntCircuitIngredient.isIntegratedCircuit(stack)) { - return OptionalInt.empty(); - } - return OptionalInt.of(IntCircuitIngredient.getCircuitConfiguration(stack)); - } - @Override public AEItemKey getDefinition() { return this.delegate.getDefinition(); @@ -283,7 +233,7 @@ public void pushInputsToExternalInventory(KeyCounter[] inputHolder, PatternInput Objects.requireNonNull(inputHolder, "inputHolder"); Objects.requireNonNull(inputSink, "inputSink"); this.delegate.pushInputsToExternalInventory(toOriginalInputHolder(inputHolder), (key, amount) -> { - if (!this.circuitMatcher.isCircuit(key)) { + if (!Matcher.Circuit.test(key)) { inputSink.pushInput(key, amount); } }); @@ -297,13 +247,12 @@ public PatternDetailsTooltip getTooltip(World level, ITooltipFlag flags) { @Override public boolean equals(Object obj) { return obj instanceof GregTechCircuitPatternDetails other - && this.delegate.equals(other.delegate) - && this.circuitMatcher.equals(other.circuitMatcher); + && this.delegate.equals(other.delegate); } @Override public int hashCode() { - return 31 * this.delegate.hashCode() + this.circuitMatcher.hashCode(); + return 31 * this.delegate.hashCode(); } private KeyCounter[] toOriginalInputHolder(KeyCounter[] inputHolder) { @@ -323,7 +272,7 @@ private KeyCounter[] expandFilteredInputHolder(KeyCounter[] inputHolder) { boolean[] hasRealInputSlot = new boolean[this.originalInputs.length]; for (int filteredSlot = 0; filteredSlot < inputHolder.length; filteredSlot++) { int originalSlot = this.filteredInputOriginalIndexes[filteredSlot]; - expanded[originalSlot] = filterInputCounter(inputHolder[filteredSlot], this.circuitMatcher); + expanded[originalSlot] = filterInputCounter(inputHolder[filteredSlot]); hasRealInputSlot[originalSlot] = true; } synthesizeCircuitInputs(expanded, hasRealInputSlot, getInputPushMultiplier(inputHolder, this.filteredInputs)); @@ -334,8 +283,8 @@ private KeyCounter[] toOriginalInputHolder(KeyCounter[] inputHolder, IInput[] in KeyCounter[] result = createEmptyInputHolder(inputs.length); boolean[] hasRealInputSlot = new boolean[inputs.length]; for (int slot = 0; slot < inputs.length; slot++) { - result[slot] = filterInputCounter(inputHolder[slot], this.circuitMatcher); - hasRealInputSlot[slot] = hasNonCircuitInput(inputs[slot], this.circuitMatcher); + result[slot] = filterInputCounter(inputHolder[slot]); + hasRealInputSlot[slot] = hasNonCircuitInput(inputs[slot]); } synthesizeCircuitInputs(result, hasRealInputSlot, getInputPushMultiplier(inputHolder, inputs)); return result; @@ -349,10 +298,10 @@ private static KeyCounter[] createEmptyInputHolder(int length) { return result; } - private static KeyCounter filterInputCounter(KeyCounter inputCounter, CircuitMatcher circuitMatcher) { + private static KeyCounter filterInputCounter(KeyCounter inputCounter) { KeyCounter filtered = new KeyCounter(); for (var input : inputCounter) { - if (!circuitMatcher.isCircuit(input.getKey())) { + if (!Matcher.Circuit.test(input.getKey())) { filtered.add(input.getKey(), input.getLongValue()); } } @@ -368,7 +317,7 @@ private void synthesizeCircuitInputs( if (hasRealInputSlot[slot]) { continue; } - GenericStack circuitTemplate = findCircuitTemplate(this.originalInputs[slot], this.circuitMatcher); + GenericStack circuitTemplate = findCircuitTemplate(this.originalInputs[slot]); long amount = Math.multiplyExact( Math.multiplyExact(circuitTemplate.amount(), this.originalInputs[slot].getMultiplier()), patternPushMultiplier); @@ -398,35 +347,35 @@ private static long getInputPushMultiplier(KeyCounter[] inputHolder, IInput[] in return multiplier == Long.MAX_VALUE ? 1 : multiplier; } - private static boolean hasNonCircuitInput(IInput input, CircuitMatcher circuitMatcher) { + private static boolean hasNonCircuitInput(IInput input) { for (GenericStack possibleInput : input.possibleInputs()) { - if (!circuitMatcher.isCircuit(possibleInput.what())) { + if (!Matcher.Circuit.test(possibleInput.what())) { return true; } } return false; } - private static GenericStack findCircuitTemplate(IInput input, CircuitMatcher circuitMatcher) { + private static GenericStack findCircuitTemplate(IInput input) { for (GenericStack possibleInput : input.possibleInputs()) { - if (circuitMatcher.isCircuit(possibleInput.what())) { + if (Matcher.Circuit.test(possibleInput.what())) { return possibleInput; } } throw new IllegalStateException("A filtered-out input slot did not contain a circuit template."); } - private static InputFilterResult filterInputs(IInput[] inputs, CircuitMatcher circuitMatcher) { + private static InputFilterResult filterInputs(IInput[] inputs) { List result = new ArrayList<>(inputs.length); List originalIndexes = new ArrayList<>(inputs.length); for (int slot = 0; slot < inputs.length; slot++) { IInput input = inputs[slot]; GenericStack[] possibleInputs = input.possibleInputs(); GenericStack[] filteredPossibleInputs = Arrays.stream(possibleInputs) - .filter(possibleInput -> !circuitMatcher.isCircuit(possibleInput.what())) + .filter(possibleInput -> !Matcher.Circuit.test(possibleInput.what())) .toArray(GenericStack[]::new); if (filteredPossibleInputs.length > 0) { - result.add(new FilteredInput(input, circuitMatcher, filteredPossibleInputs)); + result.add(new FilteredInput(input, filteredPossibleInputs)); originalIndexes.add(slot); } } @@ -437,36 +386,6 @@ private static InputFilterResult filterInputs(IInput[] inputs, CircuitMatcher ci return new InputFilterResult(result.toArray(IInput[]::new), indexes); } - /** - * Identifies AE keys that represent GregTech integrated circuits. - */ - public interface CircuitMatcher { - boolean isCircuit(AEKey key); - } - - /** - * Extracts the configured circuit number from AE keys that represent GregTech integrated circuits. - */ - public interface CircuitConfigExtractor { - OptionalInt getCircuitConfiguration(AEKey key); - } - - private record GregTechCircuitMatcher() implements CircuitMatcher { - - @Override - public boolean isCircuit(AEKey key) { - return isGregTechIntegratedCircuit(key); - } - } - - private record GregTechCircuitConfigExtractor() implements CircuitConfigExtractor { - - @Override - public OptionalInt getCircuitConfiguration(AEKey key) { - return getGregTechCircuitConfiguration(key); - } - } - private record InputFilterResult(IInput[] inputs, int[] originalIndexes) { private InputFilterResult { @@ -487,13 +406,11 @@ public int[] originalIndexes() { private record FilteredInput( IInput delegate, - CircuitMatcher circuitMatcher, GenericStack[] possibleInputs ) implements IInput { private FilteredInput { Objects.requireNonNull(delegate, "delegate"); - Objects.requireNonNull(circuitMatcher, "circuitMatcher"); possibleInputs = possibleInputs.clone(); } @@ -509,12 +426,12 @@ public long getMultiplier() { @Override public boolean isValid(AEKey input, World level) { - return !this.circuitMatcher.isCircuit(input) && this.delegate.isValid(input, level); + return !Matcher.Circuit.test(input) && this.delegate.isValid(input, level); } @Override public @Nullable AEKey getRemainingKey(AEKey template) { - if (this.circuitMatcher.isCircuit(template)) { + if (Matcher.Circuit.test(template)) { return null; } return this.delegate.getRemainingKey(template); @@ -524,14 +441,12 @@ public boolean isValid(AEKey input, World level) { private record CircuitConfiguredCraftingMachine( ICraftingMachine delegate, IGhostSlotConfigurable ghostTarget, - int circuitConfiguration, - CircuitMatcher circuitMatcher - ) implements ICraftingMachine { + int circuitConfiguration + ) implements ICraftingMachine, IPatternProviderBatchTarget { private CircuitConfiguredCraftingMachine { Objects.requireNonNull(delegate, "delegate"); Objects.requireNonNull(ghostTarget, "ghostTarget"); - Objects.requireNonNull(circuitMatcher, "circuitMatcher"); } @Override @@ -548,8 +463,8 @@ public boolean pushPattern( ) { this.ghostTarget.setGhostCircuitConfig(this.circuitConfiguration); return this.delegate.pushPattern( - filterIntegratedCircuits(patternDetails, this.circuitMatcher), - filterInputHolder(inputs, this.circuitMatcher), + filterIntegratedCircuits(patternDetails), + filterInputHolder(inputs), multiplier, ejectionDirection); } @@ -566,25 +481,26 @@ public int getMaxPatternPushMultiplier( int maxMultiplier, EnumFacing ejectionDirection ) { + if (!(this.delegate instanceof IPatternProviderBatchTarget batchTarget)) { + return 0; + } return wrapBatchTarget( - this.delegate, + batchTarget, this.ghostTarget, - this.circuitConfiguration, - this.circuitMatcher).getMaxPatternPushMultiplier(patternDetails, inputs, maxMultiplier, ejectionDirection); + this.circuitConfiguration + ).getMaxPatternPushMultiplier(patternDetails, inputs, maxMultiplier, ejectionDirection); } } private record CircuitConfiguredBatchTarget( IPatternProviderBatchTarget delegate, IGhostSlotConfigurable ghostTarget, - int circuitConfiguration, - CircuitMatcher circuitMatcher + int circuitConfiguration ) implements IPatternProviderBatchTarget { private CircuitConfiguredBatchTarget { Objects.requireNonNull(delegate, "delegate"); Objects.requireNonNull(ghostTarget, "ghostTarget"); - Objects.requireNonNull(circuitMatcher, "circuitMatcher"); } @Override @@ -596,8 +512,8 @@ public int getMaxPatternPushMultiplier( ) { this.ghostTarget.setGhostCircuitConfig(this.circuitConfiguration); return this.delegate.getMaxPatternPushMultiplier( - filterIntegratedCircuits(patternDetails, this.circuitMatcher), - filterInputHolder(inputs, this.circuitMatcher), + filterIntegratedCircuits(patternDetails), + filterInputHolder(inputs), maxMultiplier, ejectionDirection); } diff --git a/src/main/java/com/formlessDragon/ae2gtaddon/integration/hei/AE2GTAddonHeiPlugin.java b/src/main/java/com/formlessDragon/ae2gtaddon/integration/hei/AE2GTAddonHeiPlugin.java new file mode 100644 index 0000000..9a7bf02 --- /dev/null +++ b/src/main/java/com/formlessDragon/ae2gtaddon/integration/hei/AE2GTAddonHeiPlugin.java @@ -0,0 +1,30 @@ +package com.formlessDragon.ae2gtaddon.integration.hei; + +import ae2.container.me.patternencode.ContainerPEATerm; +import ae2.container.me.patternencode.ContainerPatternEncodingTerm; +import com.formlessDragon.ae2gtaddon.util.Util; +import mezz.jei.api.IModPlugin; +import mezz.jei.api.IModRegistry; +import mezz.jei.api.JEIPlugin; + +import javax.annotation.ParametersAreNonnullByDefault; + +@JEIPlugin +@ParametersAreNonnullByDefault +public class AE2GTAddonHeiPlugin implements IModPlugin { + + @Override + public void register(IModRegistry registry) { + registry.getRecipeTransferRegistry().addUniversalRecipeTransferHandler( + new ExtendedPatternEncodingRecipeTransferHandler()); + + // JEI prefers an exact recipe category handler over the universal handler AE2 registers for the standard + // pattern encoding terminals, which lets assembly line auto naming run before AE2 plans the transfer. + registry.getRecipeTransferRegistry().addRecipeTransferHandler( + new HeiAssemblyLineRecipeTransferHandler<>(ContainerPatternEncodingTerm.class), + Util.ASSEMBLY_LINE); + registry.getRecipeTransferRegistry().addRecipeTransferHandler( + new HeiAssemblyLineRecipeTransferHandler<>(ContainerPEATerm.class), + Util.ASSEMBLY_LINE); + } +} diff --git a/src/main/java/com/formlessDragon/ae2gtaddon/integration/hei/AutoPatternItemNaming.java b/src/main/java/com/formlessDragon/ae2gtaddon/integration/hei/AutoPatternItemNaming.java new file mode 100644 index 0000000..68d1d43 --- /dev/null +++ b/src/main/java/com/formlessDragon/ae2gtaddon/integration/hei/AutoPatternItemNaming.java @@ -0,0 +1,18 @@ +package com.formlessDragon.ae2gtaddon.integration.hei; + +/** + * Client-only HEI import preference of a pattern encoding terminal container. HEI writes the processing pattern slots + * before sending fake slot updates to the server, so the preference never has to leave the client container. + */ +public interface AutoPatternItemNaming { + + /** + * @return whether repeated input items are split into {@code name #1}, {@code name #2} and so on. + */ + boolean ae2gtaddon$autoNamePatternItems(); + + /** + * @param autoName whether repeated input items should be split into separate named pattern slots. + */ + void ae2gtaddon$setAutoNamePatternItems(boolean autoName); +} diff --git a/src/main/java/com/formlessDragon/ae2gtaddon/integration/hei/ExtendedPatternEncodingRecipeTransferHandler.java b/src/main/java/com/formlessDragon/ae2gtaddon/integration/hei/ExtendedPatternEncodingRecipeTransferHandler.java new file mode 100644 index 0000000..f92dc1e --- /dev/null +++ b/src/main/java/com/formlessDragon/ae2gtaddon/integration/hei/ExtendedPatternEncodingRecipeTransferHandler.java @@ -0,0 +1,426 @@ +package com.formlessDragon.ae2gtaddon.integration.hei; + +import ae2.api.client.PatternImportPriorityContext; +import ae2.api.stacks.AEKey; +import ae2.api.stacks.GenericStack; +import ae2.client.patternimport.PatternImportPrioritySelector; +import ae2.container.me.common.GridInventoryEntry; +import ae2.container.me.common.IClientRepo; +import ae2.container.me.patternencode.ContainerPatternEncodingTerm; +import ae2.container.slot.FakeSlot; +import ae2.core.network.InitNetwork; +import ae2.core.network.serverbound.InventoryActionPacket; +import ae2.helpers.InventoryAction; +import ae2.integration.modules.hei.GenericIngredientHelper; +import ae2.integration.modules.hei.HeiBookmarkHelper; +import com.formlessDragon.ae2gtaddon.container.ContainerExtendedEncodingTerm; +import com.formlessDragon.ae2gtaddon.parts.ExtendedEncodingMode; +import com.formlessDragon.ae2gtaddon.util.Matcher; +import com.formlessDragon.ae2gtaddon.util.Util; +import com.google.common.math.LongMath; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import mezz.jei.api.gui.IRecipeLayout; +import mezz.jei.api.recipe.IRecipeCategory; +import mezz.jei.api.recipe.VanillaRecipeCategoryUid; +import mezz.jei.api.recipe.transfer.IRecipeTransferError; +import mezz.jei.api.recipe.transfer.IRecipeTransferHandler; +import mezz.jei.gui.TooltipRenderer; +import net.minecraft.client.Minecraft; +import net.minecraft.util.text.TextComponentTranslation; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.function.Predicate; + +public class ExtendedPatternEncodingRecipeTransferHandler + implements IRecipeTransferHandler { + private static final int CRAFTING_GRID_SIZE = 9; + + private static RecipePlan planProcessingRecipe(ContainerExtendedEncodingTerm container, + IRecipeLayout recipeLayout, + String recipeCategoryUid, + boolean autoName) { + PatternImportPriorityContext context = ExtendedPatternImportPriorityContext.create(container, + HeiBookmarkHelper.getBookmarkedStacks()); + + List> inputs = getGenericInputs(recipeLayout); + if (Util.isAssemblyLineRecipe(recipeCategoryUid)) { + inputs = filterIngredients(inputs, Matcher.DataItem); + } + if (container.ignoreCircuit()) { + inputs = filterIngredients(inputs, Matcher.Circuit); + } + + RecipeInputs plannedInputs = planInputs(inputs, context, autoName, + container.isMergeHeiProcessingInputs()); + List outputs = planOutputs(getGenericOutputs(recipeLayout), context); + ExtendedEncodingMode mode = ExtendedEncodingMode.select(plannedInputs.stacks(), outputs, + container.getExtendedMode()); + validateAmounts(plannedInputs.stacks()); + validateAmounts(outputs); + return new RecipePlan(mode, plannedInputs, outputs); + } + + private static List> getGenericInputs(IRecipeLayout recipeLayout) { + return GenericIngredientHelper.getIngredients(recipeLayout, true, false, CRAFTING_GRID_SIZE); + } + + private static List> getGenericOutputs(IRecipeLayout recipeLayout) { + return GenericIngredientHelper.getIngredients(recipeLayout, false, false, CRAFTING_GRID_SIZE); + } + + private static List planOutputs(List> possibleInputsBySlot, + PatternImportPriorityContext context) { + List encodedInputs = new ObjectArrayList<>(); + for (List genericIngredient : possibleInputsBySlot) { + if (!genericIngredient.isEmpty()) { + GenericStack selected = PatternImportPrioritySelector.selectIngredient(genericIngredient, context, + false); + if (isValidStack(selected)) { + addOrMerge(encodedInputs, selected); + } + } + } + + return encodedInputs; + } + + private static RecipeInputs planInputs(List> possibleInputsBySlot, + PatternImportPriorityContext context, + boolean autoName, + boolean mergeInputs) { + List selectedInputs = new ObjectArrayList<>(); + List> selectedIngredients = new ObjectArrayList<>(); + for (List genericIngredient : possibleInputsBySlot) { + if (!genericIngredient.isEmpty()) { + GenericStack selected = PatternImportPrioritySelector.selectIngredient(genericIngredient, context, + false); + if (isValidStack(selected)) { + selectedInputs.add(selected); + selectedIngredients.add(genericIngredient); + } + } + } + + if (autoName) { + selectedInputs = HeiPatternItemAutoNamer.autoNameRepeatedItems(selectedInputs); + } + + List encodedInputs = new ObjectArrayList<>(); + List> candidatesByEncodedSlot = new ObjectArrayList<>(); + for (int index = 0; index < selectedInputs.size(); index++) { + GenericStack selected = selectedInputs.get(index); + List genericIngredient = selectedIngredients.get(index); + if (mergeInputs) { + addOrMergeInput(encodedInputs, candidatesByEncodedSlot, selected, genericIngredient); + } else { + addInput(encodedInputs, candidatesByEncodedSlot, selected, genericIngredient); + } + } + + List> candidateKeyTags = new ObjectArrayList<>(candidatesByEncodedSlot.size()); + for (List candidates : candidatesByEncodedSlot) { + List tags = new ObjectArrayList<>(candidates.size()); + for (AEKey candidate : candidates) { + tags.add(candidate.toTagGeneric().toString()); + } + candidateKeyTags.add(tags); + } + return new RecipeInputs(encodedInputs, candidateKeyTags); + } + + private static void validateAmounts(List stacks) { + if (stacks.isEmpty() || stacks.stream().anyMatch(stack -> stack.amount() <= 0)) { + throw new IllegalArgumentException("Recipe contains empty or oversized ingredients"); + } + } + + private static void writeSlots(ContainerExtendedEncodingTerm container, FakeSlot[] slots, + List stacks) { + if (stacks.size() > slots.length) { + throw new IllegalArgumentException("Recipe exceeds the selected layout"); + } + for (int index = 0; index < slots.length; index++) { + setFilter(container, slots[index], index < stacks.size() + ? GenericStack.wrapInItemStack(stacks.get(index)) : ItemStack.EMPTY); + } + } + + private record RecipeInputs(List stacks, List> candidates) { + } + + private record RecipePlan(ExtendedEncodingMode mode, RecipeInputs inputs, List outputs) { + } + + private static void addOrMergeInput(List stacks, List> candidatesByEncodedSlot, + GenericStack newStack, List possibleInputs) { + List newCandidates = getCandidateKeys(possibleInputs); + if (!isValidStack(newStack)) { + return; + } + if (!newCandidates.contains(newStack.what())) { + newCandidates.add(newStack.what()); + } + for (int i = 0; i < stacks.size(); i++) { + GenericStack existingStack = stacks.get(i); + if (isValidStack(existingStack) && Objects.equals(existingStack.what(), newStack.what())) { + long newAmount = LongMath.saturatedAdd(existingStack.amount(), newStack.amount()); + stacks.set(i, new GenericStack(newStack.what(), newAmount)); + candidatesByEncodedSlot.set(i, intersectCandidates(candidatesByEncodedSlot.get(i), newCandidates)); + + long overflow = newStack.amount() - (newAmount - existingStack.amount()); + if (overflow > 0) { + stacks.add(new GenericStack(newStack.what(), overflow)); + candidatesByEncodedSlot.add(new ObjectArrayList<>(newCandidates)); + } + return; + } + } + + stacks.add(newStack); + candidatesByEncodedSlot.add(new ObjectArrayList<>(newCandidates)); + } + + private static void addInput(List stacks, List> candidatesByEncodedSlot, + GenericStack newStack, List possibleInputs) { + List candidates = getCandidateKeys(possibleInputs); + if (!isValidStack(newStack)) { + return; + } + if (!candidates.contains(newStack.what())) { + candidates.add(newStack.what()); + } + stacks.add(newStack); + candidatesByEncodedSlot.add(new ObjectArrayList<>(candidates)); + } + + private static List getCandidateKeys(List possibleInputs) { + List candidates = new ObjectArrayList<>(); + for (GenericStack possibleInput : possibleInputs) { + if (isValidStack(possibleInput) && !candidates.contains(possibleInput.what())) { + candidates.add(possibleInput.what()); + } + } + return candidates; + } + + private static List intersectCandidates(List first, List second) { + List result = new ObjectArrayList<>(); + for (AEKey key : first) { + if (second.contains(key)) { + result.add(key); + } + } + return result; + } + + private static void addOrMerge(List stacks, GenericStack newStack) { + if (!isValidStack(newStack)) { + return; + } + for (int i = 0; i < stacks.size(); i++) { + GenericStack existingStack = stacks.get(i); + if (isValidStack(existingStack) && Objects.equals(existingStack.what(), newStack.what())) { + long newAmount = LongMath.saturatedAdd(existingStack.amount(), newStack.amount()); + stacks.set(i, new GenericStack(newStack.what(), newAmount)); + + long overflow = newStack.amount() - (newAmount - existingStack.amount()); + if (overflow > 0) { + stacks.add(new GenericStack(newStack.what(), overflow)); + } + return; + } + } + + stacks.add(newStack); + } + + private static void setFilter(ContainerExtendedEncodingTerm container, FakeSlot slot, ItemStack stack) { + if (slot == null) { + return; + } + InitNetwork.sendToServer(new InventoryActionPacket( + container.windowId, + InventoryAction.SET_FILTER, + slot.slotNumber, + stack)); + } + + @Override + @NotNull + public Class getContainerClass() { + return ContainerExtendedEncodingTerm.class; + } + + private static String getRecipeCategoryUid(IRecipeLayout recipeLayout) { + IRecipeCategory category = recipeLayout.getRecipeCategory(); + return category.getUid(); + } + + static List> filterIngredients( + List> inputs, + Predicate isExcluded + ) { + List> filteredInputs = new ObjectArrayList<>(inputs.size()); + for (List candidates : inputs) { + List filteredCandidates = new ObjectArrayList<>(candidates.size()); + for (GenericStack candidate : candidates) { + if (isValidStack(candidate) && !isExcluded.test(candidate.what())) { + filteredCandidates.add(candidate); + } + } + filteredInputs.add(filteredCandidates); + } + return filteredInputs; + } + + private static boolean isValidStack(@Nullable GenericStack stack) { + return stack != null && stack.what() != null && stack.amount() > 0; + } + + @Override + public IRecipeTransferError transferRecipe(@NotNull ContainerExtendedEncodingTerm container, + @NotNull IRecipeLayout recipeLayout, + @NotNull EntityPlayer player, + boolean maxTransfer, + boolean doTransfer) { + String recipeCategoryUid = getRecipeCategoryUid(recipeLayout); + if (VanillaRecipeCategoryUid.INFORMATION.equals(recipeCategoryUid) + || VanillaRecipeCategoryUid.FUEL.equals(recipeCategoryUid)) { + return null; + } + + boolean assemblyLine = Util.isAssemblyLineRecipe(recipeCategoryUid); + boolean autoName = HeiPatternItemAutoNamer.isEnabled(container.isAutoNamingItems(), recipeCategoryUid); + RecipePlan plan; + try { + plan = planProcessingRecipe(container, recipeLayout, recipeCategoryUid, autoName); + } catch (IllegalArgumentException exception) { + return new CapacityError(); + } + if (doTransfer) { + if (assemblyLine) { + // Assembly lines always use auto naming; the panel reflects the new state on the next frame. + container.setAutoNamingItems(true); + // Repeated assembly line inputs have to stay in separate slots, so make the existing switch match. + container.setMergeHeiProcessingInputs(false); + } + container.prepareProcessingRecipe(plan.mode()); + writeSlots(container, container.getActiveProcessingInputSlots(), plan.inputs().stacks()); + writeSlots(container, container.getActiveProcessingOutputSlots(), plan.outputs()); + container.setHeiProcessingRecipe(recipeCategoryUid, plan.inputs().candidates()); + } + return null; + } + + private static final class CapacityError implements IRecipeTransferError { + @Override + @NotNull + public Type getType() { + return Type.USER_FACING; + } + + @Override + public void showError(@NotNull Minecraft minecraft, int mouseX, int mouseY, @NotNull IRecipeLayout recipeLayout, + int recipeX, int recipeY) { + TooltipRenderer.drawHoveringText(minecraft, + new TextComponentTranslation("gui.ae2gtaddon.RecipeDoesNotFit").getFormattedText(), mouseX, mouseY); + } + } + + private static final class ExtendedPatternImportPriorityContext implements PatternImportPriorityContext { + @Nullable + private final IClientRepo clientRepo; + private final List bookmarkedStacks; + private final List bookmarkedKeys; + private final List repoEntries; + + private ExtendedPatternImportPriorityContext(ContainerExtendedEncodingTerm container, + List bookmarkedStacks) { + Objects.requireNonNull(container, "container"); + this.clientRepo = container.getClientRepo(); + int bookmarkedCount = bookmarkedStacks != null ? bookmarkedStacks.size() : 0; + this.bookmarkedStacks = new ObjectArrayList<>(bookmarkedCount); + this.bookmarkedKeys = new ObjectArrayList<>(bookmarkedCount); + if (bookmarkedStacks != null) { + for (GenericStack bookmarkedStack : bookmarkedStacks) { + if (bookmarkedStack != null) { + this.bookmarkedStacks.add(bookmarkedStack); + this.bookmarkedKeys.add(bookmarkedStack.what()); + } + } + } + this.repoEntries = new ObjectArrayList<>(); + if (this.clientRepo != null) { + this.repoEntries.addAll(this.clientRepo.getAllEntries()); + } + } + + private static PatternImportPriorityContext create(ContainerExtendedEncodingTerm container, + List bookmarkedStacks) { + return new ExtendedPatternImportPriorityContext(container, bookmarkedStacks); + } + + @Override + public ContainerPatternEncodingTerm getContainer() { + return null; + } + + @Override + @Nullable + public IClientRepo getClientRepo() { + return this.clientRepo; + } + + @Override + public List getBookmarkedStacks() { + return Collections.unmodifiableList(this.bookmarkedStacks); + } + + @Override + @Nullable + public GridInventoryEntry getClientRepoEntry(GenericStack candidate) { + if (candidate == null || candidate.what() == null) { + return null; + } + GridInventoryEntry best = null; + for (GridInventoryEntry entry : this.repoEntries) { + if (entry != null && Objects.equals(entry.what(), candidate.what())) { + best = best == null ? entry : mergeEntries(best, entry); + } + } + return best; + } + + @Override + public boolean isBookmarked(GenericStack candidate) { + return candidate != null && this.bookmarkedKeys.contains(candidate.what()); + } + + @Override + public boolean isCraftable(GenericStack candidate) { + GridInventoryEntry entry = getClientRepoEntry(candidate); + return entry != null && entry.craftable(); + } + + @Override + public boolean isStored(GenericStack candidate) { + GridInventoryEntry entry = getClientRepoEntry(candidate); + return entry != null && entry.storedAmount() > 0; + } + + private static GridInventoryEntry mergeEntries(GridInventoryEntry left, GridInventoryEntry right) { + return new GridInventoryEntry( + left.serial(), + left.what(), + Math.max(left.storedAmount(), right.storedAmount()), + Math.max(left.requestableAmount(), right.requestableAmount()), + left.craftable() || right.craftable()); + } + } +} diff --git a/src/main/java/com/formlessDragon/ae2gtaddon/integration/hei/HeiAssemblyLineRecipeTransferHandler.java b/src/main/java/com/formlessDragon/ae2gtaddon/integration/hei/HeiAssemblyLineRecipeTransferHandler.java new file mode 100644 index 0000000..5c35771 --- /dev/null +++ b/src/main/java/com/formlessDragon/ae2gtaddon/integration/hei/HeiAssemblyLineRecipeTransferHandler.java @@ -0,0 +1,41 @@ +package com.formlessDragon.ae2gtaddon.integration.hei; + +import ae2.container.me.patternencode.ContainerPatternEncodingTerm; +import ae2.integration.modules.hei.PatternEncodingRecipeTransferHandler; +import mezz.jei.api.gui.IRecipeLayout; +import mezz.jei.api.recipe.transfer.IRecipeTransferError; +import mezz.jei.api.recipe.transfer.IRecipeTransferHandler; +import net.minecraft.entity.player.EntityPlayer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class HeiAssemblyLineRecipeTransferHandler + implements IRecipeTransferHandler { + + private final Class containerClass; + private final PatternEncodingRecipeTransferHandler delegate; + + public HeiAssemblyLineRecipeTransferHandler(Class containerClass) { + this.containerClass = containerClass; + this.delegate = new PatternEncodingRecipeTransferHandler<>(containerClass); + } + + @Override + @NotNull + public Class getContainerClass() { + return this.containerClass; + } + + @Override + @Nullable + public IRecipeTransferError transferRecipe(@NotNull C container, @NotNull IRecipeLayout recipeLayout, + @NotNull EntityPlayer player, boolean maxTransfer, + boolean doTransfer) { + if (doTransfer && container instanceof AutoPatternItemNaming naming) { + naming.ae2gtaddon$setAutoNamePatternItems(true); + // Repeated assembly line inputs have to stay in separate slots, so use the existing merge switch to match. + container.setMergeHeiProcessingInputs(false); + } + return this.delegate.transferRecipe(container, recipeLayout, player, maxTransfer, doTransfer); + } +} diff --git a/src/main/java/com/formlessDragon/ae2gtaddon/integration/hei/HeiPatternItemAutoNamer.java b/src/main/java/com/formlessDragon/ae2gtaddon/integration/hei/HeiPatternItemAutoNamer.java new file mode 100644 index 0000000..743c58b --- /dev/null +++ b/src/main/java/com/formlessDragon/ae2gtaddon/integration/hei/HeiPatternItemAutoNamer.java @@ -0,0 +1,118 @@ +package com.formlessDragon.ae2gtaddon.integration.hei; + +import ae2.api.stacks.AEItemKey; +import ae2.api.stacks.AEKey; +import ae2.api.stacks.GenericStack; +import com.formlessDragon.ae2gtaddon.AEGTAddonConfig; +import com.formlessDragon.ae2gtaddon.util.Util; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.minecraft.item.ItemStack; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public final class HeiPatternItemAutoNamer { + + public static final int MAX_CUSTOM_NAME_LENGTH = 32; + + private HeiPatternItemAutoNamer() { + } + + public static boolean isEnabled(boolean toggleOn, @Nullable String recipeCategoryUid) { + return toggleOn || Util.isAssemblyLineRecipe(recipeCategoryUid); + } + + public static List autoNameRepeatedItems(List orderedInputs) { + Map occurrences = new LinkedHashMap<>(); + for (GenericStack stack : orderedInputs) { + Object group = groupOf(stack); + if (group != null) { + occurrences.merge(group, 1, Integer::sum); + } + } + + Map nextIndexByGroup = new HashMap<>(); + List renamed = new ObjectArrayList<>(orderedInputs.size()); + for (GenericStack stack : orderedInputs) { + Object group = groupOf(stack); + Integer occurrencesInGroup = group == null ? null : occurrences.get(group); + if (occurrencesInGroup == null || occurrencesInGroup < 2) { + renamed.add(stack); + continue; + } + + int index = nextIndexByGroup.merge(group, 0, (pre, _) -> pre + 1); + String name = buildName(ItemKeyRenameStrategy.INSTANCE.baseName(stack.what()), index, AEGTAddonConfig.autoNamingKeepsItemName); + AEKey replacement = name == null ? null : ItemKeyRenameStrategy.INSTANCE.rename(stack.what(), name); + renamed.add(replacement == null ? stack : new GenericStack(replacement, stack.amount())); + } + return renamed; + } + + @Nullable + static String buildName(@Nullable String baseName, int index, boolean keepItemName) { + if (!keepItemName) { + return "#" + index; + } + + if (baseName == null || baseName.isEmpty()) { + return null; + } + + String suffix = " #" + index; + int prefixLength = Math.min(baseName.length(), MAX_CUSTOM_NAME_LENGTH - suffix.length()); + return baseName.substring(0, prefixLength) + suffix; + } + + @Nullable + private static Object groupOf(@Nullable GenericStack stack) { + if (stack == null || stack.what() == null) { + return null; + } + return ItemKeyRenameStrategy.INSTANCE.groupKey(stack.what()); + } + + private enum ItemKeyRenameStrategy { + INSTANCE; + + public Object groupKey(AEKey key) { + ItemStack stack = stripCustomName(key); + return stack == null ? null : AEItemKey.of(stack); + } + + public String baseName(AEKey key) { + ItemStack stack = stripCustomName(key); + return stack == null ? null : stack.getDisplayName(); + } + + public AEKey rename(AEKey key, String name) { + ItemStack stack = toPlainStack(key); + if (stack == null) { + return null; + } + stack.setStackDisplayName(name); + return AEItemKey.of(stack); + } + + @Nullable + private static ItemStack stripCustomName(AEKey key) { + ItemStack stack = toPlainStack(key); + if (stack != null) { + stack.clearCustomName(); + } + return stack; + } + + @Nullable + private static ItemStack toPlainStack(AEKey key) { + if (!(key instanceof AEItemKey itemKey)) { + return null; + } + ItemStack stack = itemKey.toStack(1); + return stack.isEmpty() ? null : stack; + } + } +} diff --git a/src/main/java/com/formlessDragon/ae2gtaddon/integration/hei/PatternEncodingPanelContainerAccess.java b/src/main/java/com/formlessDragon/ae2gtaddon/integration/hei/PatternEncodingPanelContainerAccess.java index 3001ff7..6f10299 100644 --- a/src/main/java/com/formlessDragon/ae2gtaddon/integration/hei/PatternEncodingPanelContainerAccess.java +++ b/src/main/java/com/formlessDragon/ae2gtaddon/integration/hei/PatternEncodingPanelContainerAccess.java @@ -1,6 +1,6 @@ package com.formlessDragon.ae2gtaddon.integration.hei; -import ae2.container.me.items.ContainerPatternEncodingTerm; +import ae2.container.me.patternencode.ContainerPatternEncodingTerm; /** * Exposes the owning pattern encoding terminal container from AE2's encoding mode panels. diff --git a/src/main/java/com/formlessDragon/ae2gtaddon/integration/hei/ProcessingPatternCircuitImportMode.java b/src/main/java/com/formlessDragon/ae2gtaddon/integration/hei/ProcessingPatternCircuitImportMode.java index 1ceb1ae..536c43a 100644 --- a/src/main/java/com/formlessDragon/ae2gtaddon/integration/hei/ProcessingPatternCircuitImportMode.java +++ b/src/main/java/com/formlessDragon/ae2gtaddon/integration/hei/ProcessingPatternCircuitImportMode.java @@ -13,7 +13,7 @@ public interface ProcessingPatternCircuitImportMode { * * @return {@code true} when integrated circuits should not be written into processing input slots. */ - boolean ae2gtaddon$shouldIgnoreCircuitInHeiProcessingPattern(); + boolean ae2gtaddon$ignoreCircuit(); /** * Updates whether HEI processing pattern imports should skip GregTech integrated circuits. @@ -21,5 +21,5 @@ public interface ProcessingPatternCircuitImportMode { * @param ignoreCircuit {@code true} to skip integrated circuits, {@code false} to keep the original HEI import * behavior. */ - void ae2gtaddon$setIgnoreCircuitInHeiProcessingPattern(boolean ignoreCircuit); + void ae2gtaddon$setIgnoreCircuit(boolean ignoreCircuit); } diff --git a/src/main/java/com/formlessDragon/ae2gtaddon/mixin/ae2/MixinAEProcessingPattern.java b/src/main/java/com/formlessDragon/ae2gtaddon/mixin/ae2/MixinAEProcessingPattern.java new file mode 100644 index 0000000..90b3833 --- /dev/null +++ b/src/main/java/com/formlessDragon/ae2gtaddon/mixin/ae2/MixinAEProcessingPattern.java @@ -0,0 +1,22 @@ +package com.formlessDragon.ae2gtaddon.mixin.ae2; + +import ae2.crafting.pattern.AEProcessingPattern; +import com.formlessDragon.ae2gtaddon.parts.ExtendedEncodingLogic; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyArg; + +@Mixin(value = AEProcessingPattern.class, remap = false) +public class MixinAEProcessingPattern { + @ModifyArg( + method = "(Lae2/api/stacks/AEItemKey;)V", + at = @At( + value = "INVOKE", + target = "Lae2/crafting/pattern/AEProcessingPattern;validateSparseStacks(Ljava/util/List;ILjava/lang/String;)V", + ordinal = 1), + index = 1, + require = 1) + private static int ae2gtaddon$allowThirtyTwoOutputs(int maxOutputSlots) { + return ExtendedEncodingLogic.PROCESSING_OUTPUT_SLOTS; + } +} diff --git a/src/main/java/com/formlessDragon/ae2gtaddon/mixin/ae2/MixinContainerPatternEncodingTerm.java b/src/main/java/com/formlessDragon/ae2gtaddon/mixin/ae2/MixinContainerPatternEncodingTerm.java new file mode 100644 index 0000000..96beab7 --- /dev/null +++ b/src/main/java/com/formlessDragon/ae2gtaddon/mixin/ae2/MixinContainerPatternEncodingTerm.java @@ -0,0 +1,37 @@ +package com.formlessDragon.ae2gtaddon.mixin.ae2; + +import ae2.container.me.patternencode.ContainerPatternEncodingTerm; +import com.formlessDragon.ae2gtaddon.integration.hei.AutoPatternItemNaming; +import com.formlessDragon.ae2gtaddon.integration.hei.ProcessingPatternCircuitImportMode; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +@Mixin(value = ContainerPatternEncodingTerm.class, remap = false) +public abstract class MixinContainerPatternEncodingTerm + implements ProcessingPatternCircuitImportMode, AutoPatternItemNaming { + @Unique + private boolean ae2gtaddon$ignoreCircuit; + + @Unique + private boolean ae2gtaddon$autoNamePatternItems; + + @Override + public boolean ae2gtaddon$ignoreCircuit() { + return this.ae2gtaddon$ignoreCircuit; + } + + @Override + public void ae2gtaddon$setIgnoreCircuit(boolean ignoreCircuit) { + this.ae2gtaddon$ignoreCircuit = ignoreCircuit; + } + + @Override + public boolean ae2gtaddon$autoNamePatternItems() { + return this.ae2gtaddon$autoNamePatternItems; + } + + @Override + public void ae2gtaddon$setAutoNamePatternItems(boolean autoName) { + this.ae2gtaddon$autoNamePatternItems = autoName; + } +} diff --git a/src/main/java/com/formlessDragon/ae2gtaddon/mixin/mod/ae2/MixinEncodingModePanel.java b/src/main/java/com/formlessDragon/ae2gtaddon/mixin/ae2/MixinEncodingModePanel.java similarity index 71% rename from src/main/java/com/formlessDragon/ae2gtaddon/mixin/mod/ae2/MixinEncodingModePanel.java rename to src/main/java/com/formlessDragon/ae2gtaddon/mixin/ae2/MixinEncodingModePanel.java index e542585..3ad95c2 100644 --- a/src/main/java/com/formlessDragon/ae2gtaddon/mixin/mod/ae2/MixinEncodingModePanel.java +++ b/src/main/java/com/formlessDragon/ae2gtaddon/mixin/ae2/MixinEncodingModePanel.java @@ -1,12 +1,12 @@ -package com.formlessDragon.ae2gtaddon.mixin.mod.ae2; +package com.formlessDragon.ae2gtaddon.mixin.ae2; -import ae2.container.me.items.ContainerPatternEncodingTerm; +import ae2.container.me.patternencode.ContainerPatternEncodingTerm; import com.formlessDragon.ae2gtaddon.integration.hei.PatternEncodingPanelContainerAccess; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; -@Mixin(targets = "ae2.client.gui.me.items.EncodingModePanel", remap = false) +@Mixin(targets = "ae2.client.gui.me.patternencode.EncodingModePanel", remap = false) public class MixinEncodingModePanel implements PatternEncodingPanelContainerAccess { @Shadow diff --git a/src/main/java/com/formlessDragon/ae2gtaddon/mixin/mod/ae2/MixinPatternProviderLogic.java b/src/main/java/com/formlessDragon/ae2gtaddon/mixin/ae2/MixinPatternProviderLogic.java similarity index 97% rename from src/main/java/com/formlessDragon/ae2gtaddon/mixin/mod/ae2/MixinPatternProviderLogic.java rename to src/main/java/com/formlessDragon/ae2gtaddon/mixin/ae2/MixinPatternProviderLogic.java index ecd2e31..8bf1d14 100644 --- a/src/main/java/com/formlessDragon/ae2gtaddon/mixin/mod/ae2/MixinPatternProviderLogic.java +++ b/src/main/java/com/formlessDragon/ae2gtaddon/mixin/ae2/MixinPatternProviderLogic.java @@ -1,4 +1,4 @@ -package com.formlessDragon.ae2gtaddon.mixin.mod.ae2; +package com.formlessDragon.ae2gtaddon.mixin.ae2; import ae2.api.AECapabilities; import ae2.api.crafting.IPatternDetails; @@ -12,7 +12,7 @@ import ae2.helpers.patternprovider.PatternProviderTarget; import ae2.parts.p2p.PatternProviderP2PTunnelPart; import ae2.util.inv.InternalInventoryHost; -import com.formlessDragon.ae2gtaddon.Init.Items; +import com.formlessDragon.ae2gtaddon.init.Items; import com.formlessDragon.ae2gtaddon.integration.gregtech.GregTechCircuitPatternDetails; import gregtech.api.capability.IGhostSlotConfigurable; import it.unimi.dsi.fastutil.objects.ObjectList; @@ -265,14 +265,14 @@ private PatternProviderTarget findAdapter(EnumFacing side) { method = "collectPushTargets", at = @At( value = "INVOKE", - target = "Lae2/helpers/patternprovider/PatternProviderLogic;getBatchTarget(Lnet/minecraft/world/World;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/util/EnumFacing;Lae2/api/implementations/blockentities/ICraftingMachine;)Lae2/api/implementations/blockentities/IPatternProviderBatchTarget;" + target = "Lae2/helpers/patternprovider/PatternProviderLogic;getBatchTarget(Lnet/minecraft/world/World;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/util/EnumFacing;Lae2/api/implementations/blockentities/IPatternProviderBatchTarget;)Lae2/api/implementations/blockentities/IPatternProviderBatchTarget;" ) ) private IPatternProviderBatchTarget ae2gtaddon$collectBatchTarget( World level, BlockPos pos, EnumFacing side, - ICraftingMachine fallback + IPatternProviderBatchTarget fallback ) { TileEntity tileEntity = level.getTileEntity(pos); IPatternProviderBatchTarget batchTarget = fallback; @@ -318,9 +318,6 @@ private PatternProviderTarget findAdapter(EnumFacing side) { TileEntity hostTileEntity = this.host.getTileEntity(); World level = hostTileEntity.getWorld(); - if (level == null) { - return target; - } BlockPos targetPos = hostTileEntity.getPos().offset(direction); TileEntity targetTileEntity = level.getTileEntity(targetPos); diff --git a/src/main/java/com/formlessDragon/ae2gtaddon/mixin/mod/ae2/MixinPatternProviderP2PTunnelPart.java b/src/main/java/com/formlessDragon/ae2gtaddon/mixin/ae2/MixinPatternProviderP2PTunnelPart.java similarity index 98% rename from src/main/java/com/formlessDragon/ae2gtaddon/mixin/mod/ae2/MixinPatternProviderP2PTunnelPart.java rename to src/main/java/com/formlessDragon/ae2gtaddon/mixin/ae2/MixinPatternProviderP2PTunnelPart.java index f75bb17..bfaed08 100644 --- a/src/main/java/com/formlessDragon/ae2gtaddon/mixin/mod/ae2/MixinPatternProviderP2PTunnelPart.java +++ b/src/main/java/com/formlessDragon/ae2gtaddon/mixin/ae2/MixinPatternProviderP2PTunnelPart.java @@ -1,4 +1,4 @@ -package com.formlessDragon.ae2gtaddon.mixin.mod.ae2; +package com.formlessDragon.ae2gtaddon.mixin.ae2; import ae2.api.AECapabilities; import ae2.api.networking.security.IActionSource; diff --git a/src/main/java/com/formlessDragon/ae2gtaddon/mixin/ae2/MixinProcessingEncodingPanel.java b/src/main/java/com/formlessDragon/ae2gtaddon/mixin/ae2/MixinProcessingEncodingPanel.java new file mode 100644 index 0000000..4362b7a --- /dev/null +++ b/src/main/java/com/formlessDragon/ae2gtaddon/mixin/ae2/MixinProcessingEncodingPanel.java @@ -0,0 +1,175 @@ +package com.formlessDragon.ae2gtaddon.mixin.ae2; + +import ae2.client.gui.AEBaseGui; +import ae2.client.gui.Icon; +import ae2.client.gui.Tooltip; +import ae2.client.gui.WidgetContainer; +import ae2.client.gui.me.patternencode.ProcessingEncodingPanel; +import ae2.client.gui.widgets.ActionButton; +import ae2.client.gui.widgets.Scrollbar; +import ae2.client.gui.widgets.ToggleButton; +import ae2.container.me.patternencode.ContainerPatternEncodingTerm; +import ae2.integration.Integrations; +import com.formlessDragon.ae2gtaddon.integration.hei.AutoPatternItemNaming; +import com.formlessDragon.ae2gtaddon.integration.hei.PatternEncodingPanelContainerAccess; +import com.formlessDragon.ae2gtaddon.integration.hei.ProcessingPatternCircuitImportMode; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.util.text.TextComponentTranslation; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.awt.*; +import java.util.List; +import java.util.function.Consumer; + +@Mixin(value = ProcessingEncodingPanel.class, remap = false) +public abstract class MixinProcessingEncodingPanel { + @Unique + private static final int AE2GTADDON_BUTTON_SPACING = 10; + + @Unique + private static final float AE2GTADDON_HALF_SIZE_ICON_SCALE = 0.5F; + + @Shadow + @Final + private ActionButton clearBtn; + + @Unique + private ToggleButton ae2gtaddon$ignoreCircuitButton; + + @Unique + private ToggleButton ae2gtaddon$autoNameButton; + + @Unique + private ProcessingPatternCircuitImportMode ae2gtaddon$importMode; + + @Unique + private AutoPatternItemNaming ae2gtaddon$namingMode; + + @Unique + private ContainerPatternEncodingTerm ae2gtaddon$container; + + @Redirect( + method = "(Lae2/client/gui/AEBaseGui;Lae2/client/gui/WidgetContainer;)V", + at = @At( + value = "INVOKE", + target = "Lae2/client/gui/widgets/Scrollbar;setRange(III)V"), + require = 1) + private void ae2gtaddon$includeAllOutputSlots( + Scrollbar scrollbar, int min, int max, int pageSize + ) { + int outputSlots = ((PatternEncodingPanelContainerAccess) this) + .ae2gtaddon$getContainer() + .getProcessingOutputSlots().length; + scrollbar.setRange(min, Math.max(max, outputSlots - pageSize), pageSize); + } + + @Inject(method = "", at = @At("TAIL")) + private void ae2gtaddon$addHeiImportButtons( + AEBaseGui screen, WidgetContainer widgets, CallbackInfo ci + ) { + this.ae2gtaddon$container = ((PatternEncodingPanelContainerAccess) this).ae2gtaddon$getContainer(); + this.ae2gtaddon$importMode = (ProcessingPatternCircuitImportMode) this.ae2gtaddon$container; + this.ae2gtaddon$namingMode = (AutoPatternItemNaming) this.ae2gtaddon$container; + + this.ae2gtaddon$ignoreCircuitButton = new ToggleButton( + Icon.S_SUBSTITUTION_DISABLED, + Icon.S_SUBSTITUTION_ENABLED, + this::ae2gtaddon$setIgnoreCircuit); + this.ae2gtaddon$ignoreCircuitButton.setHalfSize(true); + this.ae2gtaddon$ignoreCircuitButton.setDisableBackground(true); + this.ae2gtaddon$ignoreCircuitButton.setTooltipOn(List.of( + new TextComponentTranslation("gui.tooltips.ae2gtaddon.IgnoreCircuitHeiImport"), + new TextComponentTranslation("gui.tooltips.ae2gtaddon.IgnoreCircuitHeiImportOn"))); + this.ae2gtaddon$ignoreCircuitButton.setTooltipOff(List.of( + new TextComponentTranslation("gui.tooltips.ae2gtaddon.IgnoreCircuitHeiImport"), + new TextComponentTranslation("gui.tooltips.ae2gtaddon.IgnoreCircuitHeiImportOff"))); + + this.ae2gtaddon$autoNameButton = new ToggleButton( + Icon.PATTERN_AUTO_FILL_ON, + Icon.PATTERN_AUTO_FILL_OFF, + this::ae2gtaddon$setAutoNameItems); + this.ae2gtaddon$autoNameButton.setHalfSize(true); + this.ae2gtaddon$autoNameButton.setDisableBackground(true); + this.ae2gtaddon$autoNameButton.setIconScale(AE2GTADDON_HALF_SIZE_ICON_SCALE); + this.ae2gtaddon$autoNameButton.setTooltipOn(List.of( + new TextComponentTranslation("gui.tooltips.ae2gtaddon.AutoNameHeiImport"), + new TextComponentTranslation("gui.tooltips.ae2gtaddon.AutoNameHeiImportOn"))); + this.ae2gtaddon$autoNameButton.setTooltipOff(List.of( + new TextComponentTranslation("gui.tooltips.ae2gtaddon.AutoNameHeiImport"), + new TextComponentTranslation("gui.tooltips.ae2gtaddon.AutoNameHeiImportOff"))); + } + + @Inject(method = "updateBeforeRender", at = @At("TAIL")) + private void ae2gtaddon$updateHeiImportButtons(CallbackInfo ci) { + this.ae2gtaddon$ignoreCircuitButton.setState(this.ae2gtaddon$shouldIgnoreCircuit()); + this.ae2gtaddon$ignoreCircuitButton.x = this.clearBtn.x; + this.ae2gtaddon$ignoreCircuitButton.y = this.clearBtn.y + AE2GTADDON_BUTTON_SPACING; + + this.ae2gtaddon$autoNameButton.setState(this.ae2gtaddon$namingMode.ae2gtaddon$autoNamePatternItems()); + this.ae2gtaddon$autoNameButton.x = this.clearBtn.x; + this.ae2gtaddon$autoNameButton.y = this.clearBtn.y + 3 * AE2GTADDON_BUTTON_SPACING; + } + + @Inject(method = "setVisible", at = @At("TAIL")) + private void ae2gtaddon$setHeiImportButtonsVisible(boolean visible, CallbackInfo ci) { + this.ae2gtaddon$ignoreCircuitButton.setVisibility(visible); + this.ae2gtaddon$autoNameButton.setVisibility(visible); + } + + @Unique + private boolean ae2gtaddon$shouldIgnoreCircuit() { + return this.ae2gtaddon$importMode.ae2gtaddon$ignoreCircuit(); + } + + @Unique + private void ae2gtaddon$setIgnoreCircuit(boolean ignoreCircuit) { + this.ae2gtaddon$importMode.ae2gtaddon$setIgnoreCircuit(ignoreCircuit); + } + + @Unique + private void ae2gtaddon$setAutoNameItems(boolean autoName) { + this.ae2gtaddon$namingMode.ae2gtaddon$setAutoNamePatternItems(autoName); + if (autoName) { + // Auto naming keeps every occurrence in its own slot, so use the existing HEI merge switch to match. + this.ae2gtaddon$container.setMergeHeiProcessingInputs(false); + } + } + + @Unique + @SuppressWarnings("all") + public void populateScreen(Consumer addWidget, Rectangle bounds, AEBaseGui baseGui) { + if(Integrations.hei().isEnabled()) { + addWidget.accept(this.ae2gtaddon$ignoreCircuitButton); + addWidget.accept(this.ae2gtaddon$autoNameButton); + } + } + + @Unique + @SuppressWarnings("all") + public boolean wantsAllMouseDownEvents() { + return true; + } + + @Unique + @SuppressWarnings("all") + public Tooltip getTooltip(int mouseX, int mouseY) { + if(!Integrations.hei().isEnabled()) { + return null; + } + + if(this.ae2gtaddon$autoNameButton.visible && this.ae2gtaddon$autoNameButton.isMouseOver()) { + return new Tooltip(ae2gtaddon$autoNameButton.getTooltipMessage()); + } + if(this.ae2gtaddon$ignoreCircuitButton.visible && this.ae2gtaddon$ignoreCircuitButton.isMouseOver()) { + return new Tooltip(ae2gtaddon$ignoreCircuitButton.getTooltipMessage()); + } + return null; + } +} diff --git a/src/main/java/com/formlessDragon/ae2gtaddon/mixin/mod/ae2/MixinPseudoPatternDetails.java b/src/main/java/com/formlessDragon/ae2gtaddon/mixin/ae2/MixinPseudoPatternDetails.java similarity index 96% rename from src/main/java/com/formlessDragon/ae2gtaddon/mixin/mod/ae2/MixinPseudoPatternDetails.java rename to src/main/java/com/formlessDragon/ae2gtaddon/mixin/ae2/MixinPseudoPatternDetails.java index d93b531..73c4946 100644 --- a/src/main/java/com/formlessDragon/ae2gtaddon/mixin/mod/ae2/MixinPseudoPatternDetails.java +++ b/src/main/java/com/formlessDragon/ae2gtaddon/mixin/ae2/MixinPseudoPatternDetails.java @@ -1,4 +1,4 @@ -package com.formlessDragon.ae2gtaddon.mixin.mod.ae2; +package com.formlessDragon.ae2gtaddon.mixin.ae2; import ae2.api.crafting.IPatternDetails; import ae2.helpers.patternprovider.PseudoPatternDetails; diff --git a/src/main/java/com/formlessDragon/ae2gtaddon/mixin/mod/ae2/MixinContainerPatternEncodingTerm.java b/src/main/java/com/formlessDragon/ae2gtaddon/mixin/mod/ae2/MixinContainerPatternEncodingTerm.java deleted file mode 100644 index 187d1d2..0000000 --- a/src/main/java/com/formlessDragon/ae2gtaddon/mixin/mod/ae2/MixinContainerPatternEncodingTerm.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.formlessDragon.ae2gtaddon.mixin.mod.ae2; - -import ae2.container.me.items.ContainerPatternEncodingTerm; -import com.formlessDragon.ae2gtaddon.integration.hei.ProcessingPatternCircuitImportMode; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Unique; - -@Mixin(value = ContainerPatternEncodingTerm.class, remap = false) -public class MixinContainerPatternEncodingTerm implements ProcessingPatternCircuitImportMode { - - @Unique - private boolean ae2gtaddon$ignoreCircuitInHeiProcessingPattern; - - @Override - public boolean ae2gtaddon$shouldIgnoreCircuitInHeiProcessingPattern() { - return this.ae2gtaddon$ignoreCircuitInHeiProcessingPattern; - } - - @Override - public void ae2gtaddon$setIgnoreCircuitInHeiProcessingPattern(boolean ignoreCircuit) { - this.ae2gtaddon$ignoreCircuitInHeiProcessingPattern = ignoreCircuit; - } -} diff --git a/src/main/java/com/formlessDragon/ae2gtaddon/mixin/mod/ae2/MixinProcessingEncodingPanel.java b/src/main/java/com/formlessDragon/ae2gtaddon/mixin/mod/ae2/MixinProcessingEncodingPanel.java deleted file mode 100644 index ab2a64a..0000000 --- a/src/main/java/com/formlessDragon/ae2gtaddon/mixin/mod/ae2/MixinProcessingEncodingPanel.java +++ /dev/null @@ -1,110 +0,0 @@ -package com.formlessDragon.ae2gtaddon.mixin.mod.ae2; - -import ae2.client.gui.AEBaseGui; -import ae2.client.gui.Icon; -import ae2.client.gui.Tooltip; -import ae2.client.gui.WidgetContainer; -import ae2.client.gui.me.items.GuiPatternEncodingTerm; -import ae2.client.gui.me.items.ProcessingEncodingPanel; -import ae2.client.gui.widgets.ActionButton; -import ae2.client.gui.widgets.ToggleButton; -import com.formlessDragon.ae2gtaddon.integration.hei.PatternEncodingPanelContainerAccess; -import com.formlessDragon.ae2gtaddon.integration.hei.ProcessingPatternCircuitImportMode; -import net.minecraft.client.gui.GuiButton; -import net.minecraft.util.text.TextComponentTranslation; -import org.spongepowered.asm.mixin.Final; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.Unique; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -import java.awt.*; -import java.util.List; -import java.util.function.Consumer; - -@Mixin(value = ProcessingEncodingPanel.class, remap = false) -public abstract class MixinProcessingEncodingPanel { - @Unique - private static final int AE2GTADDON_BUTTON_SPACING = 10; - - @Shadow - @Final - private ActionButton clearBtn; - - @Unique - private ToggleButton ae2gtaddon$ignoreCircuitButton; - - @Unique - private ProcessingPatternCircuitImportMode ae2gtaddon$importMode; - - @Inject(method = "", at = @At("TAIL")) - private void ae2gtaddon$addIgnoreCircuitButton( - GuiPatternEncodingTerm screen, - WidgetContainer widgets, - CallbackInfo ci - ) { - this.ae2gtaddon$importMode = (ProcessingPatternCircuitImportMode) - ((PatternEncodingPanelContainerAccess) this).ae2gtaddon$getContainer(); - this.ae2gtaddon$ignoreCircuitButton = new ToggleButton( - Icon.S_SUBSTITUTION_DISABLED, - Icon.S_SUBSTITUTION_ENABLED, - this::ae2gtaddon$setIgnoreCircuit); - this.ae2gtaddon$ignoreCircuitButton.setHalfSize(true); - this.ae2gtaddon$ignoreCircuitButton.setDisableBackground(true); - this.ae2gtaddon$ignoreCircuitButton.setTooltipOn(List.of( - new TextComponentTranslation("gui.tooltips.ae2gtaddon.IgnoreCircuitHeiImport"), - new TextComponentTranslation("gui.tooltips.ae2gtaddon.IgnoreCircuitHeiImportOn"))); - this.ae2gtaddon$ignoreCircuitButton.setTooltipOff(List.of( - new TextComponentTranslation("gui.tooltips.ae2gtaddon.IgnoreCircuitHeiImport"), - new TextComponentTranslation("gui.tooltips.ae2gtaddon.IgnoreCircuitHeiImportOff"))); - } - - @Inject(method = "updateBeforeRender", at = @At("TAIL")) - private void ae2gtaddon$updateIgnoreCircuitButton(CallbackInfo ci) { - this.ae2gtaddon$ignoreCircuitButton.setState(this.ae2gtaddon$shouldIgnoreCircuit()); - this.ae2gtaddon$ignoreCircuitButton.x = this.clearBtn.x; - this.ae2gtaddon$ignoreCircuitButton.y = this.clearBtn.y + AE2GTADDON_BUTTON_SPACING; - } - - @Inject(method = "setVisible", at = @At("TAIL")) - private void ae2gtaddon$setIgnoreCircuitButtonVisible(boolean visible, CallbackInfo ci) { - this.ae2gtaddon$ignoreCircuitButton.setVisibility(visible); - } - - @Unique - private boolean ae2gtaddon$shouldIgnoreCircuit() { - return this.ae2gtaddon$importMode.ae2gtaddon$shouldIgnoreCircuitInHeiProcessingPattern(); - } - - @Unique - private void ae2gtaddon$setIgnoreCircuit(boolean ignoreCircuit) { - this.ae2gtaddon$importMode.ae2gtaddon$setIgnoreCircuitInHeiProcessingPattern(ignoreCircuit); - } - - @Unique - @SuppressWarnings("all") - public void populateScreen(Consumer addWidget, Rectangle bounds, AEBaseGui baseGui) { - addWidget.accept(this.ae2gtaddon$ignoreCircuitButton); - } - - @Unique - @SuppressWarnings("all") - public boolean wantsAllMouseDownEvents() { - return true; - } - - @Unique - @SuppressWarnings("all") - public Tooltip getTooltip(int mouseX, int mouseY) { - if(!this.ae2gtaddon$ignoreCircuitButton.visible) { - return null; - } - - if(this.ae2gtaddon$ignoreCircuitButton.isMouseOver()) { - return new Tooltip(ae2gtaddon$ignoreCircuitButton.getTooltipMessage()); - } - return null; - } -} diff --git a/src/main/java/com/formlessDragon/ae2gtaddon/mixin/mod/hei/MixinPatternEncodingRecipeTransferHandler.java b/src/main/java/com/formlessDragon/ae2gtaddon/mixin/mod/hei/MixinPatternEncodingRecipeTransferHandler.java deleted file mode 100644 index 361c998..0000000 --- a/src/main/java/com/formlessDragon/ae2gtaddon/mixin/mod/hei/MixinPatternEncodingRecipeTransferHandler.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.formlessDragon.ae2gtaddon.mixin.mod.hei; - -import ae2.api.stacks.GenericStack; -import ae2.container.me.items.ContainerPatternEncodingTerm; -import ae2.integration.modules.hei.GenericIngredientHelper; -import ae2.integration.modules.hei.PatternEncodingRecipeTransferHandler; -import com.formlessDragon.ae2gtaddon.integration.gregtech.GregTechCircuitPatternDetails; -import com.formlessDragon.ae2gtaddon.integration.hei.ProcessingPatternCircuitImportMode; -import it.unimi.dsi.fastutil.objects.ObjectArrayList; -import mezz.jei.api.gui.IRecipeLayout; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Unique; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.Redirect; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -import java.util.List; - -@Mixin(value = PatternEncodingRecipeTransferHandler.class, remap = false) -public class MixinPatternEncodingRecipeTransferHandler { - @Unique - private static final int AE2GTADDON_CRAFTING_GRID_SIZE = 9; - - @Unique - private static boolean ae2gtaddon$ignoreCircuitForCurrentImport; - - @Inject(method = "encodeProcessingRecipe", at = @At("HEAD")) - private static void ae2gtaddon$captureImportMode( - ContainerPatternEncodingTerm container, - IRecipeLayout recipeLayout, - CallbackInfo ci - ) { - ae2gtaddon$ignoreCircuitForCurrentImport = ((ProcessingPatternCircuitImportMode) container) - .ae2gtaddon$shouldIgnoreCircuitInHeiProcessingPattern(); - } - - @Inject(method = "encodeProcessingRecipe", at = @At("RETURN")) - private static void ae2gtaddon$clearImportMode( - ContainerPatternEncodingTerm container, - IRecipeLayout recipeLayout, - CallbackInfo ci - ) { - ae2gtaddon$ignoreCircuitForCurrentImport = false; - } - - @Redirect( - method = "encodeProcessingRecipe", - at = @At( - value = "INVOKE", - target = "Lae2/integration/modules/hei/PatternEncodingRecipeTransferHandler;getGenericInputs(Lmezz/jei/api/gui/IRecipeLayout;)Ljava/util/List;" - ) - ) - private static List> ae2gtaddon$getGenericInputs(IRecipeLayout recipeLayout) { - List> inputs = GenericIngredientHelper.getIngredients( - recipeLayout, - true, - false, - AE2GTADDON_CRAFTING_GRID_SIZE); - return ae2gtaddon$shouldIgnoreCircuitInProcessingImport() - ? ae2gtaddon$filterIntegratedCircuits(inputs) - : inputs; - } - - @Unique - private static boolean ae2gtaddon$shouldIgnoreCircuitInProcessingImport() { - return ae2gtaddon$ignoreCircuitForCurrentImport; - } - - @Unique - private static List> ae2gtaddon$filterIntegratedCircuits( - List> inputs - ) { - List> filteredInputs = new ObjectArrayList<>(inputs.size()); - for (List candidates : inputs) { - List filteredCandidates = new ObjectArrayList<>(candidates.size()); - for (GenericStack candidate : candidates) { - if (candidate != null - && candidate.what() != null - && !GregTechCircuitPatternDetails.GREGTECH_CIRCUIT_MATCHER.isCircuit(candidate.what())) { - filteredCandidates.add(candidate); - } - } - filteredInputs.add(filteredCandidates); - } - return filteredInputs; - } -} diff --git a/src/main/java/com/formlessDragon/ae2gtaddon/parts/ExtendedEncodingDraft.java b/src/main/java/com/formlessDragon/ae2gtaddon/parts/ExtendedEncodingDraft.java new file mode 100644 index 0000000..a641d05 --- /dev/null +++ b/src/main/java/com/formlessDragon/ae2gtaddon/parts/ExtendedEncodingDraft.java @@ -0,0 +1,114 @@ +package com.formlessDragon.ae2gtaddon.parts; + +import ae2.api.stacks.GenericStack; +import ae2.core.AELog; +import ae2.util.ConfigInventory; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public final class ExtendedEncodingDraft { + private final ConfigInventory inputs; + private final ConfigInventory outputs; + private final Runnable save; + private ExtendedEncodingMode mode = ExtendedEncodingMode.MULTI_IN; + + public ExtendedEncodingDraft(Runnable save) { + this.save = Objects.requireNonNull(save, "save"); + this.inputs = ConfigInventory.configStacks(32).allowOverstacking(true).changeListener(save).build(); + this.outputs = ConfigInventory.configStacks(32).allowOverstacking(true).changeListener(save).build(); + } + + public ConfigInventory inputs() { return this.inputs; } + public ConfigInventory outputs() { return this.outputs; } + public ExtendedEncodingMode mode() { return this.mode; } + + public boolean canSwitchTo(ExtendedEncodingMode target) { + return Objects.requireNonNull(target, "target").fits(stacks(this.inputs), stacks(this.outputs)); + } + + public void setMode(ExtendedEncodingMode target) { + if (!canSwitchTo(target)) { + throw new IllegalArgumentException("Switching processing layout would hide occupied slots"); + } + this.mode = target; + this.save.run(); + } + + public void load(List inputs, List outputs) { + ExtendedEncodingMode target = ExtendedEncodingMode.select(inputs, outputs, this.mode); + validate(inputs, target.getInputSlots()); + validate(outputs, target.getOutputSlots()); + this.mode = target; + fill(this.inputs, inputs); + fill(this.outputs, outputs); + this.save.run(); + } + + public void restoreMode(String serialized) { + ExtendedEncodingMode preferred = ExtendedEncodingMode.MULTI_IN; + if (!serialized.isEmpty()) { + try { + preferred = ExtendedEncodingMode.valueOf(serialized); + } catch (IllegalArgumentException exception) { + AELog.warn(exception, "Invalid extended terminal layout %s; recovering from saved slots", serialized); + } + } + this.mode = preferred; + try { + this.mode = ExtendedEncodingMode.select(stacks(this.inputs), stacks(this.outputs), preferred); + } catch (IllegalArgumentException exception) { + AELog.warn(exception, "Saved terminal draft exceeds both layouts; preserving contents for correction"); + } + } + + public Snapshot snapshot() { + List inputs = stacks(this.inputs); + List outputs = stacks(this.outputs); + validate(inputs, this.mode.getInputSlots()); + validate(outputs, this.mode.getOutputSlots()); + return new Snapshot(trim(inputs), trim(outputs)); + } + + private static List trim(List stacks) { + return java.util.Collections.unmodifiableList(new ArrayList<>( + stacks.subList(0, ExtendedEncodingMode.occupiedLength(stacks)))); + } + + private static List stacks(ConfigInventory inventory) { + List stacks = new ArrayList<>(inventory.size()); + for (int index = 0; index < inventory.size(); index++) { + stacks.add(inventory.getStack(index)); + } + return stacks; + } + + private static void validate(List stacks, int capacity) { + if (ExtendedEncodingMode.occupiedLength(stacks) > capacity) { + throw new IllegalArgumentException("Processing draft exceeds layout capacity"); + } + for (GenericStack stack : stacks) { + if (stack != null && (stack.amount() <= 0)) { + throw new IllegalArgumentException("Processing ingredient amount is out of range"); + } + } + } + + private static void fill(ConfigInventory inventory, List stacks) { + inventory.beginBatch(); + try { + for (int index = 0; index < inventory.size(); index++) { + inventory.setStack(index, index < stacks.size() ? stacks.get(index) : null); + } + } finally { + inventory.endBatch(); + } + } + + public record Snapshot(List inputs, List outputs) { + public boolean isEncodable() { + return !this.inputs.isEmpty() && !this.outputs.isEmpty() && this.outputs.getFirst() != null; + } + } +} diff --git a/src/main/java/com/formlessDragon/ae2gtaddon/parts/ExtendedEncodingLogic.java b/src/main/java/com/formlessDragon/ae2gtaddon/parts/ExtendedEncodingLogic.java new file mode 100644 index 0000000..e1ae627 --- /dev/null +++ b/src/main/java/com/formlessDragon/ae2gtaddon/parts/ExtendedEncodingLogic.java @@ -0,0 +1,183 @@ +package com.formlessDragon.ae2gtaddon.parts; + +import ae2.api.crafting.PatternDetailsHelper; +import ae2.api.inventories.InternalInventory; +import ae2.api.stacks.GenericStack; +import ae2.core.AELog; +import ae2.core.definitions.AEItems; +import ae2.crafting.pattern.AEProcessingPattern; +import ae2.helpers.IPatternTerminalLogicHost; +import ae2.parts.encoding.EncodingMode; +import ae2.parts.encoding.PatternEncodingLogic; +import ae2.util.ConfigInventory; +import ae2.util.inv.AppEngInternalInventory; +import ae2.util.inv.filter.AEItemDefinitionFilter; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Objects; + +@SuppressWarnings("unused") +public class ExtendedEncodingLogic extends PatternEncodingLogic { + public static final int PROCESSING_INPUT_SLOTS = 32; + public static final int PROCESSING_OUTPUT_SLOTS = 32; + + private final IPatternTerminalLogicHost host; + private final AppEngInternalInventory blankPatternInv = new AppEngInternalInventory(this, 1); + private final AppEngInternalInventory encodedPatternInv = new AppEngInternalInventory(this, 1); + private final ExtendedEncodingDraft draft = new ExtendedEncodingDraft(this::saveChanges); + private boolean loading; + + public ExtendedEncodingLogic(IPatternTerminalLogicHost host) { + super(host); + this.host = Objects.requireNonNull(host, "host"); + this.blankPatternInv.setFilter(new AEItemDefinitionFilter(AEItems.BLANK_PATTERN)); + this.encodedPatternInv.setMaxStackSize(0, 1); + } + + @Override + public void onChangeInventory(AppEngInternalInventory inventory, int slot) { + if (!this.loading && inventory == this.encodedPatternInv) { + loadEncodedPattern(this.encodedPatternInv.getStackInSlot(0)); + } + saveChanges(); + } + + @Override + public void saveChangedInventory(AppEngInternalInventory inventory) { + saveChanges(); + } + + @Override + public boolean isClientSide() { + return this.host.getLevel() == null || this.host.getLevel().isRemote; + } + + @Override + public void saveChanges() { + if (!this.loading) { + this.host.markForSave(); + } + } + + private void loadEncodedPattern(ItemStack pattern) { + if (pattern.isEmpty() || AEItems.BLANK_PATTERN.is(pattern)) { + return; + } + try { + var details = PatternDetailsHelper.decodePattern(pattern, this.host.getLevel()); + if (details instanceof AEProcessingPattern processing) { + loadProcessingPattern(processing); + } else { + AELog.warn("Extended terminal ignored unsupported pattern %s", pattern); + } + } catch (IllegalArgumentException exception) { + AELog.warn(exception, "Extended terminal rejected a processing pattern that exceeds its capacity"); + } + } + + public boolean canLoadPattern(ItemStack stack) { + var details = PatternDetailsHelper.decodePattern(stack, this.host.getLevel()); + if (!(details instanceof AEProcessingPattern pattern)) { + return false; + } + return ExtendedEncodingMode.MULTI_IN.fits(pattern.getSparseInputs(), pattern.getSparseOutputs()) + || ExtendedEncodingMode.MULTI_OUT.fits(pattern.getSparseInputs(), pattern.getSparseOutputs()); + } + + public void loadProcessingPattern(AEProcessingPattern pattern) { + loadProcessingPattern(pattern.getSparseInputs(), pattern.getSparseOutputs()); + } + + public void loadProcessingPattern(List inputs, List outputs) { + this.draft.load(inputs, outputs); + } + + public boolean canSwitchTo(ExtendedEncodingMode mode) { + return this.draft.canSwitchTo(mode); + } + + public ExtendedEncodingMode getExtendedMode() { + return this.draft.mode(); + } + + public void setExtendedMode(ExtendedEncodingMode mode) { + this.draft.setMode(mode); + } + + public boolean isDraftValid() { + try { + this.draft.snapshot(); + return true; + } catch (IllegalArgumentException exception) { + AELog.warn(exception, "Refusing to encode an invalid extended terminal draft"); + return false; + } + } + + @Nullable + public ItemStack encodeProcessingPattern(@Nullable String recipeTypeUid) { + var snapshot = this.draft.snapshot(); + return snapshot.isEncodable() + ? PatternDetailsHelper.encodeProcessingPattern(snapshot.inputs(), snapshot.outputs(), recipeTypeUid) + : null; + } + + @Override + public EncodingMode getMode() { + return EncodingMode.PROCESSING; + } + + @Override + public void setMode(EncodingMode mode) { + if (Objects.requireNonNull(mode, "mode") != EncodingMode.PROCESSING) { + AELog.warn("Rejected crafting mode for an extended processing terminal"); + } + } + + @Override + public ConfigInventory getEncodedInputInv() { + return this.draft.inputs(); + } + + @Override + public ConfigInventory getEncodedOutputInv() { + return this.draft.outputs(); + } + + @Override + public InternalInventory getBlankPatternInv() { + return this.blankPatternInv; + } + + @Override + public InternalInventory getEncodedPatternInv() { + return this.encodedPatternInv; + } + + @Override + public void readFromNBT(NBTTagCompound data) { + this.loading = true; + try { + this.blankPatternInv.readFromNBT(data, "blankPattern"); + this.encodedPatternInv.readFromNBT(data, "encodedPattern"); + this.draft.inputs().readFromChildTag(data, "encodedInputs"); + this.draft.outputs().readFromChildTag(data, "encodedOutputs"); + this.draft.restoreMode(data.getString("extendedMode")); + } finally { + this.loading = false; + } + } + + @Override + public void writeToNBT(NBTTagCompound data) { + data.setString("mode", EncodingMode.PROCESSING.name()); + data.setString("extendedMode", this.draft.mode().name()); + this.blankPatternInv.writeToNBT(data, "blankPattern"); + this.encodedPatternInv.writeToNBT(data, "encodedPattern"); + this.draft.inputs().writeToChildTag(data, "encodedInputs"); + this.draft.outputs().writeToChildTag(data, "encodedOutputs"); + } +} diff --git a/src/main/java/com/formlessDragon/ae2gtaddon/parts/ExtendedEncodingMode.java b/src/main/java/com/formlessDragon/ae2gtaddon/parts/ExtendedEncodingMode.java new file mode 100644 index 0000000..b6ef79e --- /dev/null +++ b/src/main/java/com/formlessDragon/ae2gtaddon/parts/ExtendedEncodingMode.java @@ -0,0 +1,52 @@ +package com.formlessDragon.ae2gtaddon.parts; + +import java.util.List; +import java.util.Objects; + +public enum ExtendedEncodingMode { + MULTI_IN(32, 8), + MULTI_OUT(8, 32); + + private final int inputSlots; + private final int outputSlots; + + ExtendedEncodingMode(int inputSlots, int outputSlots) { + this.inputSlots = inputSlots; + this.outputSlots = outputSlots; + } + + public int getInputSlots() { + return this.inputSlots; + } + + public int getOutputSlots() { + return this.outputSlots; + } + + public boolean fits(List inputs, List outputs) { + return occupiedLength(inputs) <= this.inputSlots && occupiedLength(outputs) <= this.outputSlots; + } + + public static ExtendedEncodingMode select(List inputs, List outputs, ExtendedEncodingMode preferred) { + Objects.requireNonNull(preferred, "preferred"); + if (preferred.fits(inputs, outputs)) { + return preferred; + } + for (ExtendedEncodingMode mode : values()) { + if (mode.fits(inputs, outputs)) { + return mode; + } + } + throw new IllegalArgumentException("Processing recipe exceeds both 32 -> 8 and 8 -> 32 layouts"); + } + + public static int occupiedLength(List stacks) { + Objects.requireNonNull(stacks, "stacks"); + for (int index = stacks.size() - 1; index >= 0; index--) { + if (stacks.get(index) != null) { + return index + 1; + } + } + return 0; + } +} diff --git a/src/main/java/com/formlessDragon/ae2gtaddon/parts/ExtendedEncodingTermPart.java b/src/main/java/com/formlessDragon/ae2gtaddon/parts/ExtendedEncodingTermPart.java new file mode 100644 index 0000000..9f50657 --- /dev/null +++ b/src/main/java/com/formlessDragon/ae2gtaddon/parts/ExtendedEncodingTermPart.java @@ -0,0 +1,119 @@ +package com.formlessDragon.ae2gtaddon.parts; + +import ae2.api.config.Settings; +import ae2.api.config.YesNo; +import ae2.api.parts.IPartItem; +import ae2.api.parts.IPartModel; +import ae2.api.util.IConfigManagerBuilder; +import ae2.core.AppEng; +import ae2.container.ISubGui; +import ae2.helpers.IPatternTerminalGuiHost; +import ae2.helpers.IPatternTerminalLogicHost; +import ae2.items.parts.PartModels; +import ae2.parts.PartModel; +import ae2.parts.reporting.AbstractTerminalPart; +import com.formlessDragon.ae2gtaddon.AEGTAddon; +import com.formlessDragon.ae2gtaddon.container.ExtendedEncodingTermGuiHandler; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.EnumFacing; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Vec3d; + +import java.util.List; + +public class ExtendedEncodingTermPart extends AbstractTerminalPart + implements IPatternTerminalLogicHost, IPatternTerminalGuiHost { + @PartModels + public static final ResourceLocation MODEL_OFF = AppEng.makeId("part/pattern_encoding_terminal_off"); + @PartModels + public static final ResourceLocation MODEL_ON = AppEng.makeId("part/pattern_encoding_terminal_on"); + + public static final IPartModel MODELS_OFF = new PartModel(MODEL_BASE, MODEL_OFF, MODEL_STATUS_OFF); + public static final IPartModel MODELS_ON = new PartModel(MODEL_BASE, MODEL_ON, MODEL_STATUS_ON); + public static final IPartModel MODELS_HAS_CHANNEL = new PartModel(MODEL_BASE, MODEL_ON, MODEL_STATUS_HAS_CHANNEL); + + private final ExtendedEncodingLogic logic = new ExtendedEncodingLogic(this); + + public ExtendedEncodingTermPart(IPartItem partItem) { + super(partItem); + } + + @Override + protected void registerSettings(IConfigManagerBuilder builder) { + super.registerSettings(builder); + builder.registerSetting(Settings.PATTERN_AUTO_FILL, YesNo.NO); + } + + @Override + public void addAdditionalDrops(List drops, boolean wrenched) { + super.addAdditionalDrops(drops, wrenched); + for (var stack : this.logic.getBlankPatternInv()) { + if (!stack.isEmpty()) { + drops.add(stack); + } + } + for (var stack : this.logic.getEncodedPatternInv()) { + if (!stack.isEmpty()) { + drops.add(stack); + } + } + } + + @Override + public void clearContent() { + super.clearContent(); + this.logic.getBlankPatternInv().clear(); + this.logic.getEncodedPatternInv().clear(); + } + + @Override + public void readFromNBT(NBTTagCompound data) { + super.readFromNBT(data); + this.logic.readFromNBT(data); + } + + @Override + public void writeToNBT(NBTTagCompound data) { + super.writeToNBT(data); + this.logic.writeToNBT(data); + } + + @Override + public boolean onUseWithoutItem(EntityPlayer player, Vec3d pos) { + if (!player.world.isRemote) { + openEncodingTermGui(player, false); + } + return true; + } + + @Override + public void returnToMainContainer(EntityPlayer player, ISubGui subGui) { + openEncodingTermGui(player, true); + } + + private void openEncodingTermGui(EntityPlayer player, boolean returnedFromSubScreen) { + EnumFacing side = getSide(); + int encodedSide = side == null ? EnumFacing.VALUES.length : side.ordinal(); + BlockPos pos = getHost().getLocation().getPos(); + player.openGui(AEGTAddon.instance, ExtendedEncodingTermGuiHandler.guiId(returnedFromSubScreen), + player.world, pos.getX(), (encodedSide << 8) | (pos.getY() & 255), pos.getZ()); + } + + @Override + public IPartModel getStaticModels() { + return this.selectModel(MODELS_OFF, MODELS_ON, MODELS_HAS_CHANNEL); + } + + @Override + public ExtendedEncodingLogic getLogic() { + return this.logic; + } + + @Override + public void markForSave() { + getHost().markForSave(); + } +} diff --git a/src/main/java/com/formlessDragon/ae2gtaddon/proxy/ClientProxy.java b/src/main/java/com/formlessDragon/ae2gtaddon/proxy/ClientProxy.java index 60af6a4..d055eb5 100644 --- a/src/main/java/com/formlessDragon/ae2gtaddon/proxy/ClientProxy.java +++ b/src/main/java/com/formlessDragon/ae2gtaddon/proxy/ClientProxy.java @@ -1,5 +1,21 @@ package com.formlessDragon.ae2gtaddon.proxy; +import ae2.api.util.AEColor; +import ae2.client.render.StaticItemColor; +import com.formlessDragon.ae2gtaddon.init.Items; +import com.formlessDragon.ae2gtaddon.client.handler.HeiPatternAutoNamingHandler; +import com.formlessDragon.ae2gtaddon.client.handler.HeiPatternInputFilterHandler; +import net.minecraft.client.Minecraft; +import net.minecraftforge.common.MinecraftForge; + @SuppressWarnings("unused") public class ClientProxy extends CommonProxy { + @Override + public void init() { + super.init(); + MinecraftForge.EVENT_BUS.register(new HeiPatternAutoNamingHandler()); + MinecraftForge.EVENT_BUS.register(new HeiPatternInputFilterHandler()); + Minecraft.getMinecraft().getItemColors().registerItemColorHandler( + new StaticItemColor(AEColor.TRANSPARENT), Items.EXTENDED_ENCODING_TERM.item()); + } } diff --git a/src/main/java/com/formlessDragon/ae2gtaddon/proxy/CommonProxy.java b/src/main/java/com/formlessDragon/ae2gtaddon/proxy/CommonProxy.java index d0291cc..9a26dc6 100644 --- a/src/main/java/com/formlessDragon/ae2gtaddon/proxy/CommonProxy.java +++ b/src/main/java/com/formlessDragon/ae2gtaddon/proxy/CommonProxy.java @@ -4,7 +4,7 @@ import ae2.core.definitions.AEBlocks; import ae2.core.definitions.AEParts; import ae2.core.localization.GuiText; -import com.formlessDragon.ae2gtaddon.Init.Items; +import com.formlessDragon.ae2gtaddon.init.Items; public class CommonProxy implements IProxy { diff --git a/src/main/java/com/formlessDragon/ae2gtaddon/util/AEKeyMatcher.java b/src/main/java/com/formlessDragon/ae2gtaddon/util/AEKeyMatcher.java new file mode 100644 index 0000000..a5ac0d3 --- /dev/null +++ b/src/main/java/com/formlessDragon/ae2gtaddon/util/AEKeyMatcher.java @@ -0,0 +1,11 @@ +package com.formlessDragon.ae2gtaddon.util; + +import ae2.api.stacks.AEKey; + +import java.util.function.Predicate; + +public interface AEKeyMatcher extends Predicate { + + @Override + boolean test(AEKey aeKey); +} diff --git a/src/main/java/com/formlessDragon/ae2gtaddon/util/Matcher.java b/src/main/java/com/formlessDragon/ae2gtaddon/util/Matcher.java new file mode 100644 index 0000000..21c7bf2 --- /dev/null +++ b/src/main/java/com/formlessDragon/ae2gtaddon/util/Matcher.java @@ -0,0 +1,31 @@ +package com.formlessDragon.ae2gtaddon.util; + +import ae2.api.stacks.AEItemKey; +import ae2.api.stacks.AEKey; +import gregtech.api.recipes.ingredients.IntCircuitIngredient; +import gregtech.api.util.AssemblyLineManager; +import net.minecraft.item.ItemStack; + +public enum Matcher implements AEKeyMatcher { + DataItem { + @Override + public boolean test(AEKey what) { + if (!(what instanceof AEItemKey key)) { + return false; + } + + ItemStack stack = key.toStack(); + return !stack.isEmpty() && AssemblyLineManager.isStackDataItem(stack, true); + } + }, + Circuit { + @Override + public boolean test(AEKey what) { + if (!(what instanceof AEItemKey key)) { + return false; + } + + return IntCircuitIngredient.isIntegratedCircuit(key.toStack()); + } + } +} diff --git a/src/main/java/com/formlessDragon/ae2gtaddon/util/Util.java b/src/main/java/com/formlessDragon/ae2gtaddon/util/Util.java new file mode 100644 index 0000000..3413e26 --- /dev/null +++ b/src/main/java/com/formlessDragon/ae2gtaddon/util/Util.java @@ -0,0 +1,29 @@ +package com.formlessDragon.ae2gtaddon.util; + +import ae2.api.stacks.AEItemKey; +import ae2.api.stacks.AEKey; +import gregtech.api.recipes.ingredients.IntCircuitIngredient; +import net.minecraft.item.ItemStack; +import org.jetbrains.annotations.Nullable; + +import java.util.OptionalInt; + +public final class Util { + + public static final String ASSEMBLY_LINE = "gregtech:assembly_line"; + + public static OptionalInt getCircuitConfiguration(AEKey what) { + if (!(what instanceof AEItemKey itemKey)) { + return OptionalInt.empty(); + } + ItemStack stack = itemKey.toStack(); + if (!IntCircuitIngredient.isIntegratedCircuit(stack)) { + return OptionalInt.empty(); + } + return OptionalInt.of(IntCircuitIngredient.getCircuitConfiguration(stack)); + } + + public static boolean isAssemblyLineRecipe(@Nullable String recipeCategoryUid) { + return ASSEMBLY_LINE.equals(recipeCategoryUid); + } +} diff --git a/src/main/resources/ae2gtaddon.mod.mixin.json b/src/main/resources/ae2gtaddon.mixins.json similarity index 62% rename from src/main/resources/ae2gtaddon.mod.mixin.json rename to src/main/resources/ae2gtaddon.mixins.json index 219eb8c..ce3becb 100644 --- a/src/main/resources/ae2gtaddon.mod.mixin.json +++ b/src/main/resources/ae2gtaddon.mixins.json @@ -1,11 +1,11 @@ { "required": true, - "package": "com.formlessDragon.ae2gtaddon.mixin.mod", - "plugin": "com.formlessDragon.ae2gtaddon.ModMixinConfigPlugin", - "compatibilityLevel": "JAVA_8", + "package": "com.formlessDragon.ae2gtaddon.mixin", + "plugin": "com.formlessDragon.ae2gtaddon.MixinConfigPlugin", + "compatibilityLevel": "JAVA_25", "target": "@env(MOD)", + "refmap": "ae2gtaddon.refmap.json", "client": [ - "hei.MixinPatternEncodingRecipeTransferHandler", "ae2.MixinProcessingEncodingPanel" ], "mixins": [ @@ -13,6 +13,7 @@ "ae2.MixinEncodingModePanel", "ae2.MixinPatternProviderP2PTunnelPart", "ae2.MixinPatternProviderLogic", + "ae2.MixinAEProcessingPattern", "ae2.MixinPseudoPatternDetails" ], "server": [], diff --git a/src/main/resources/assets/ae2/screens/terminals/ae2gtaddon_extended_pattern_encoding_terminal.json b/src/main/resources/assets/ae2/screens/terminals/ae2gtaddon_extended_pattern_encoding_terminal.json new file mode 100644 index 0000000..7fc66ea --- /dev/null +++ b/src/main/resources/assets/ae2/screens/terminals/ae2gtaddon_extended_pattern_encoding_terminal.json @@ -0,0 +1,165 @@ +{ + "$schema": "../schema.json", + "includes": [ + "terminal.json", + "encoding/ae2gtaddon_extended_processing.json" + ], + "slots": { + "BLANK_PATTERN": { + "left": 152, + "bottom": 163 + }, + "ENCODED_PATTERN": { + "left": 152, + "bottom": 120 + } + }, + "text": { + "dialog_title": { + "text": { + "translate": "gui.ae2.Terminal" + }, + "position": { + "left": 8, + "top": 6 + } + } + }, + "terminalStyle": { + "header": { + "texture": "ae2gtaddon:textures/guis/extended_encoding_terminal.png", + "srcRect": [ + 0, + 0, + 195, + 17 + ] + }, + "firstRow": { + "texture": "ae2gtaddon:textures/guis/extended_encoding_terminal.png", + "srcRect": [ + 0, + 17, + 195, + 18 + ] + }, + "row": { + "texture": "ae2gtaddon:textures/guis/extended_encoding_terminal.png", + "srcRect": [ + 0, + 35, + 195, + 18 + ] + }, + "lastRow": { + "texture": "ae2gtaddon:textures/guis/extended_encoding_terminal.png", + "srcRect": [ + 0, + 53, + 195, + 18 + ] + }, + "bottom": { + "texture": "ae2gtaddon:textures/guis/extended_encoding_terminal.png", + "srcRect": [ + 0, + 71, + 195, + 180 + ] + } + }, + "widgets": { + "modeTabButton0": { + "left": 173, + "bottom": 174, + "width": 22, + "height": 22 + }, + "modeTabButton1": { + "left": 173, + "bottom": 153, + "width": 22, + "height": 22 + }, + "modePanel0": { + "left": 9, + "bottom": 172, + "width": 131, + "height": 82 + }, + "modePanel1": { + "left": 9, + "bottom": 172, + "width": 131, + "height": 82 + }, + "encodePattern": { + "left": 152, + "bottom": 145 + }, + "uploadPattern": { + "left": 141, + "bottom": 140, + "width": 8, + "height": 8 + }, + "providerSelectionOverlay": { + "left": 0, + "top": 0, + "width": 0, + "height": 0 + }, + "patternModifierPanel": { + "left": -126, + "top": 59, + "width": 86, + "height": 180 + }, + "patternModifierPanelAmount0": { + "left": -63, + "top": 67, + "width": 18, + "height": 20 + }, + "patternModifierPanelAmount1": { + "left": -63, + "top": 89, + "width": 18, + "height": 20 + }, + "patternModifierPanelAmount2": { + "left": -63, + "top": 111, + "width": 18, + "height": 20 + }, + "patternModifierPanelAmount3": { + "left": -63, + "top": 133, + "width": 18, + "height": 20 + }, + "patternModifierPanelAmount4": { + "left": -63, + "top": 155, + "width": 18, + "height": 20 + }, + "patternModifierPanelAmount5": { + "left": -63, + "top": 177, + "width": 18, + "height": 20 + }, + "patternModifierPanelClear": { + "left": -63, + "top": 199, + "width": 18, + "height": 20 + } + } +} diff --git a/src/main/resources/assets/ae2/screens/terminals/encoding/ae2gtaddon_extended_processing.json b/src/main/resources/assets/ae2/screens/terminals/encoding/ae2gtaddon_extended_processing.json new file mode 100644 index 0000000..37bf3e6 --- /dev/null +++ b/src/main/resources/assets/ae2/screens/terminals/encoding/ae2gtaddon_extended_processing.json @@ -0,0 +1,146 @@ +{ + "$schema": "../../schema.json", + "slots": { + "PROCESSING_INPUTS": { + "left": 21, + "bottom": 158 + }, + "PROCESSING_OUTPUTS": { + "left": 21, + "bottom": 158, + "grid": "VERTICAL" + } + }, + "widgets": { + "multiInprocessingPatternModeScrollbar": { + "left": 11, + "bottom": 165, + "height": 70 + }, + "multiInprocessingCycleOutput": { + "left": 106, + "bottom": 166 + }, + "multiInprocessingClearSecondaryOutputs": { + "left": 106, + "bottom": 156 + }, + "multiInprocessingClearPattern": { + "left": 93, + "bottom": 166 + }, + "multiInprocessingIgnoreCircuit": { + "left": 93, + "bottom": 156 + }, + "multiInprocessingMergeHeiInputs": { + "left": 93, + "bottom": 146 + }, + "multiInprocessingAutoNaming": { + "left": 106, + "bottom": 146 + }, + "multiInprocessingAmountButton0": { + "left": 93, + "bottom": 122, + "width": 8, + "height": 8 + }, + "multiInprocessingAmountButton1": { + "left": 106, + "bottom": 122, + "width": 8, + "height": 8 + }, + "multiInprocessingAmountButton2": { + "left": 93, + "bottom": 112, + "width": 8, + "height": 8 + }, + "multiInprocessingAmountButton3": { + "left": 106, + "bottom": 112, + "width": 8, + "height": 8 + }, + "multiInprocessingAmountButton4": { + "left": 93, + "bottom": 102, + "width": 8, + "height": 8 + }, + "multiInprocessingAmountButton5": { + "left": 106, + "bottom": 102, + "width": 8, + "height": 8 + }, + "multiOutprocessingPatternModeScrollbar": { + "left": 11, + "bottom": 165, + "height": 70 + }, + "multiOutprocessingCycleOutput": { + "left": 52, + "bottom": 166 + }, + "multiOutprocessingClearSecondaryOutputs": { + "left": 52, + "bottom": 156 + }, + "multiOutprocessingClearPattern": { + "left": 39, + "bottom": 166 + }, + "multiOutprocessingIgnoreCircuit": { + "left": 39, + "bottom": 156 + }, + "multiOutprocessingMergeHeiInputs": { + "left": 39, + "bottom": 146 + }, + "multiOutprocessingAutoNaming": { + "left": 52, + "bottom": 146 + }, + "multiOutprocessingAmountButton0": { + "left": 39, + "bottom": 122, + "width": 8, + "height": 8 + }, + "multiOutprocessingAmountButton1": { + "left": 52, + "bottom": 122, + "width": 8, + "height": 8 + }, + "multiOutprocessingAmountButton2": { + "left": 39, + "bottom": 112, + "width": 8, + "height": 8 + }, + "multiOutprocessingAmountButton3": { + "left": 52, + "bottom": 112, + "width": 8, + "height": 8 + }, + "multiOutprocessingAmountButton4": { + "left": 39, + "bottom": 102, + "width": 8, + "height": 8 + }, + "multiOutprocessingAmountButton5": { + "left": 52, + "bottom": 102, + "width": 8, + "height": 8 + } + } +} diff --git a/src/main/resources/assets/ae2gtaddon/lang/en_us.lang b/src/main/resources/assets/ae2gtaddon/lang/en_us.lang index 3ecd294..5b345c3 100644 --- a/src/main/resources/assets/ae2gtaddon/lang/en_us.lang +++ b/src/main/resources/assets/ae2gtaddon/lang/en_us.lang @@ -1,10 +1,19 @@ # Item item.ae2gtaddon.circuit_card.name=Circuit Card +item.ae2gtaddon.extended_encoding_terminal.name=ME Extended Pattern Encoding Terminal # Tooltips gui.tooltips.ae2gtaddon.IgnoreCircuitHeiImport=HEI Circuit Import gui.tooltips.ae2gtaddon.IgnoreCircuitHeiImportOff=Keep integrated circuits when importing processing patterns. gui.tooltips.ae2gtaddon.IgnoreCircuitHeiImportOn=Ignore integrated circuits when importing processing patterns. +gui.tooltips.ae2gtaddon.AutoNameHeiImport=HEI Auto Naming +gui.tooltips.ae2gtaddon.AutoNameHeiImportOff=Import processing inputs exactly as HEI provides them. +gui.tooltips.ae2gtaddon.AutoNameHeiImportOn=Name repeated items as "name #1", "name #2" and keep them in separate slots. Requires HEI input merging to be off. +gui.ae2gtaddon.MultiIn=Multi-In (32 inputs -> 8 outputs) +gui.ae2gtaddon.MultiOut=Multi-Out (8 inputs -> 32 outputs) +gui.ae2gtaddon.ModeSwitchWouldLoseData=Clear the excess input/output slots before changing layout. Your current recipe has been preserved. +gui.ae2gtaddon.RecipeDoesNotFit=This recipe needs a 32-input / 8-output or 8-input / 32-output layout, with valid processing amounts. Nothing was imported. +gui.ae2gtaddon.InvalidDraft=The recipe contains invalid amounts or slots outside the current layout. Your draft and pattern have been preserved. # Others -itemGroup.ae2gtaddon=AE2-GTIntergration \ No newline at end of file +itemGroup.ae2gtaddon=AE2-GTIntergration diff --git a/src/main/resources/assets/ae2gtaddon/lang/zh_cn.lang b/src/main/resources/assets/ae2gtaddon/lang/zh_cn.lang index 224dbcc..0abfb8c 100644 --- a/src/main/resources/assets/ae2gtaddon/lang/zh_cn.lang +++ b/src/main/resources/assets/ae2gtaddon/lang/zh_cn.lang @@ -1,10 +1,19 @@ # Item item.ae2gtaddon.circuit_card.name=电路卡 +item.ae2gtaddon.extended_encoding_terminal.name=ME 扩展样板编码终端 # Tooltips gui.tooltips.ae2gtaddon.IgnoreCircuitHeiImport=HEI 电路导入 gui.tooltips.ae2gtaddon.IgnoreCircuitHeiImportOff=导入处理样板时保留集成电路。 gui.tooltips.ae2gtaddon.IgnoreCircuitHeiImportOn=导入处理样板时忽略集成电路。 +gui.tooltips.ae2gtaddon.AutoNameHeiImport=HEI 自动命名 +gui.tooltips.ae2gtaddon.AutoNameHeiImportOff=导入处理样板时按 HEI 提供的内容写入,不重命名。 +gui.tooltips.ae2gtaddon.AutoNameHeiImportOn=导入处理样板时把重复物品命名为“名称 #1”“名称 #2”,并保留在独立槽位中;需要关闭 HEI 输入合并。 +gui.ae2gtaddon.MultiIn=多输入(32 输入 -> 8 输出) +gui.ae2gtaddon.MultiOut=多输出(8 输入 -> 32 输出) +gui.ae2gtaddon.ModeSwitchWouldLoseData=请先清空超出目标布局容量的输入或输出槽。当前配方已保留。 +gui.ae2gtaddon.RecipeDoesNotFit=配方必须符合 32 输入 / 8 输出或 8 输入 / 32 输出容量,且数量合法。本次未导入任何内容。 +gui.ae2gtaddon.InvalidDraft=配方数量不合法或存在超出当前布局的槽位。草稿和样板均已保留。 # Others -itemGroup.ae2gtaddon=AE2-GT集成 \ No newline at end of file +itemGroup.ae2gtaddon=AE2-GT集成 diff --git a/src/main/resources/assets/ae2gtaddon/models/item/extended_encoding_terminal.json b/src/main/resources/assets/ae2gtaddon/models/item/extended_encoding_terminal.json new file mode 100644 index 0000000..efb3bed --- /dev/null +++ b/src/main/resources/assets/ae2gtaddon/models/item/extended_encoding_terminal.json @@ -0,0 +1,9 @@ +{ + "parent": "ae2:item/display_base", + "textures": { + "front": "ae2:item/part/pattern_encoding_terminal", + "front_bright": "ae2:part/pattern_encoding_terminal_bright", + "front_medium": "ae2:part/pattern_encoding_terminal_medium", + "front_dark": "ae2:part/pattern_encoding_terminal_dark" + } +} diff --git a/src/main/resources/assets/ae2gtaddon/textures/guis/extended_encoding_terminal.png b/src/main/resources/assets/ae2gtaddon/textures/guis/extended_encoding_terminal.png new file mode 100644 index 0000000..b8541f3 Binary files /dev/null and b/src/main/resources/assets/ae2gtaddon/textures/guis/extended_encoding_terminal.png differ diff --git a/src/main/resources/assets/ae2gtaddon/textures/guis/multi-in.png b/src/main/resources/assets/ae2gtaddon/textures/guis/multi-in.png new file mode 100644 index 0000000..85cd72a Binary files /dev/null and b/src/main/resources/assets/ae2gtaddon/textures/guis/multi-in.png differ diff --git a/src/main/resources/assets/ae2gtaddon/textures/guis/multi-out.png b/src/main/resources/assets/ae2gtaddon/textures/guis/multi-out.png new file mode 100644 index 0000000..bbfd9c3 Binary files /dev/null and b/src/main/resources/assets/ae2gtaddon/textures/guis/multi-out.png differ diff --git a/src/main/resources/mcmod.info b/src/main/resources/mcmod.info index a3ec904..c4631bd 100644 --- a/src/main/resources/mcmod.info +++ b/src/main/resources/mcmod.info @@ -6,7 +6,12 @@ "description": "${mod_description}", "authorList": ["${mod_authors}"], "credits": "${mod_credits}", + "license": "${mod_license}", "url": "${mod_url}", + "issueTrackerUrl": "${mod_issue_tracker_url}", "updateJSON": "${mod_update_json}", - "logoFile": "${mod_logo_path}" -}] \ No newline at end of file + "iconItem": "${mod_icon_item}", + "logoFile": "${mod_logo_path}", + "iconFile": "${mod_icon_path}", + "backgroundFile": "${mod_background_path}" +}] diff --git a/src/test/java/ae2/helpers/patternprovider/GregTechCircuitExternalTargetTest.java b/src/test/java/ae2/helpers/patternprovider/GregTechCircuitExternalTargetTest.java deleted file mode 100644 index db6ac37..0000000 --- a/src/test/java/ae2/helpers/patternprovider/GregTechCircuitExternalTargetTest.java +++ /dev/null @@ -1,97 +0,0 @@ -package ae2.helpers.patternprovider; - -import ae2.api.config.Actionable; -import ae2.api.config.PatternProviderInsertionMode; -import ae2.api.stacks.AEKey; -import com.formlessDragon.ae2gtaddon.integration.gregtech.GregTechCircuitPatternDetails; -import gregtech.api.capability.IGhostSlotConfigurable; -import org.junit.jupiter.api.Test; - -import java.util.Set; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -class GregTechCircuitExternalTargetTest { - private static final int CIRCUIT_CONFIGURATION = 12; - - @Test - void configuredExternalTargetSetsGhostCircuitBeforeModulatedInsert() { - RecordingPatternProviderTarget delegate = new RecordingPatternProviderTarget(); - RecordingGhostCircuitTarget ghostTarget = new RecordingGhostCircuitTarget(); - PatternProviderTarget wrapped = GregTechCircuitPatternDetails.wrapExternalTarget( - delegate, - ghostTarget, - CIRCUIT_CONFIGURATION); - - long inserted = wrapped.insert(null, 3, Actionable.MODULATE, PatternProviderInsertionMode.DEFAULT); - - assertEquals(3, inserted); - assertEquals(CIRCUIT_CONFIGURATION, ghostTarget.lastConfig); - assertEquals(1, delegate.modulatedInsertCalls); - } - - @Test - void configuredExternalTargetDoesNotSetGhostCircuitForSimulatedInsert() { - RecordingPatternProviderTarget delegate = new RecordingPatternProviderTarget(); - RecordingGhostCircuitTarget ghostTarget = new RecordingGhostCircuitTarget(); - PatternProviderTarget wrapped = GregTechCircuitPatternDetails.wrapExternalTarget( - delegate, - ghostTarget, - CIRCUIT_CONFIGURATION); - - long inserted = wrapped.insert(null, 3, Actionable.SIMULATE, PatternProviderInsertionMode.DEFAULT); - - assertEquals(3, inserted); - assertEquals(-1, ghostTarget.lastConfig); - assertEquals(1, delegate.simulatedInsertCalls); - } - - private static final class RecordingPatternProviderTarget implements PatternProviderTarget { - private int simulatedInsertCalls; - private int modulatedInsertCalls; - - @Override - public long insert(AEKey what, long amount, Actionable type) { - return this.insert(what, amount, type, PatternProviderInsertionMode.DEFAULT); - } - - @Override - public long insert(AEKey what, long amount, Actionable type, PatternProviderInsertionMode insertionMode) { - if (type == Actionable.MODULATE) { - this.modulatedInsertCalls++; - } else { - this.simulatedInsertCalls++; - } - return amount; - } - - @Override - public boolean containsPatternInput(Set patternInputs) { - return false; - } - - @Override - public boolean containsAnyStack() { - return false; - } - - @Override - public boolean hasEmptySlots() { - return true; - } - } - - private static final class RecordingGhostCircuitTarget implements IGhostSlotConfigurable { - private int lastConfig = -1; - - @Override - public boolean hasGhostCircuitInventory() { - return true; - } - - @Override - public void setGhostCircuitConfig(int config) { - this.lastConfig = config; - } - } -} diff --git a/src/test/java/com/formlessDragon/ae2gtaddon/integration/gregtech/GregTechCircuitPatternDetailsTest.java b/src/test/java/com/formlessDragon/ae2gtaddon/integration/gregtech/GregTechCircuitPatternDetailsTest.java deleted file mode 100644 index 1c73537..0000000 --- a/src/test/java/com/formlessDragon/ae2gtaddon/integration/gregtech/GregTechCircuitPatternDetailsTest.java +++ /dev/null @@ -1,669 +0,0 @@ -package com.formlessDragon.ae2gtaddon.integration.gregtech; - -import ae2.api.crafting.IPatternDetails; -import ae2.api.implementations.blockentities.ICraftingMachine; -import ae2.api.implementations.blockentities.IPatternProviderBatchTarget; -import ae2.api.implementations.blockentities.PatternContainerGroup; -import ae2.api.stacks.AEItemKey; -import ae2.api.stacks.AEKey; -import ae2.api.stacks.AEKeyType; -import ae2.api.stacks.GenericStack; -import ae2.api.stacks.KeyCounter; -import gregtech.api.capability.IGhostSlotConfigurable; -import net.minecraft.nbt.NBTBase; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.network.PacketBuffer; -import net.minecraft.util.EnumFacing; -import net.minecraft.util.ResourceLocation; -import net.minecraft.util.text.ITextComponent; -import net.minecraft.util.text.TextComponentString; -import net.minecraft.world.World; -import org.jetbrains.annotations.Nullable; -import org.junit.jupiter.api.Test; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.OptionalInt; -import java.util.Set; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertTrue; - -class GregTechCircuitPatternDetailsTest { - private static final TestKey IRON = new TestKey("iron"); - private static final TestKey GOLD = new TestKey("gold"); - private static final TestKey REDSTONE = new TestKey("redstone"); - private static final TestKey CIRCUIT_4 = new TestKey("circuit-4"); - private static final TestKey CIRCUIT_7 = new TestKey("circuit-7"); - private static final TestKey CIRCUIT_12 = new TestKey("circuit-12"); - private static final TestKey CIRCUIT_21 = new TestKey("circuit-21"); - private static final TestCircuitStrategy CIRCUITS = new TestCircuitStrategy(); - - @Test - void findsFirstCircuitConfigurationWithInjectedExtractor() { - TestPatternDetails patternDetails = new TestPatternDetails( - new TestInput(IRON), - new TestInput(CIRCUIT_7)); - - OptionalInt configuration = GregTechCircuitPatternDetails.findCircuitConfiguration(patternDetails, CIRCUITS); - - assertTrue(configuration.isPresent()); - assertEquals(7, configuration.getAsInt()); - } - - @Test - void wrappedInputsExcludeCircuitChoices() { - TestPatternDetails patternDetails = new TestPatternDetails( - new TestInput(IRON, CIRCUIT_4), - new TestInput(GOLD)); - - IPatternDetails wrapped = GregTechCircuitPatternDetails.filterIntegratedCircuits(patternDetails, CIRCUITS); - - IPatternDetails.IInput[] inputs = wrapped.getInputs(); - assertEquals(2, inputs.length); - assertStacks(List.of(IRON), inputs[0].possibleInputs()); - assertStacks(List.of(GOLD), inputs[1].possibleInputs()); - } - - @Test - void wrappedInputsDropCircuitOnlyIngredients() { - TestPatternDetails patternDetails = new TestPatternDetails( - new TestInput(CIRCUIT_12), - new TestInput(REDSTONE)); - - IPatternDetails wrapped = GregTechCircuitPatternDetails.filterIntegratedCircuits(patternDetails, CIRCUITS); - - IPatternDetails.IInput[] inputs = wrapped.getInputs(); - assertEquals(1, inputs.length); - assertStacks(List.of(REDSTONE), inputs[0].possibleInputs()); - } - - @Test - void plannerInputViewDropsCircuitOnlyIngredients() { - TestPatternDetails patternDetails = new TestPatternDetails( - new TestInput(CIRCUIT_12), - new TestInput(REDSTONE)); - - IPatternDetails wrapped = GregTechCircuitPatternDetails.filterIntegratedCircuits(patternDetails, CIRCUITS); - - IPatternDetails.IInput[] plannerInputs = wrapped.getInputs(); - assertEquals(1, plannerInputs.length); - assertStacks(List.of(REDSTONE), plannerInputs[0].possibleInputs()); - } - - @Test - void wrappedPatternStillExposesOriginalCircuitConfiguration() { - TestPatternDetails patternDetails = new TestPatternDetails( - new TestInput(CIRCUIT_12), - new TestInput(REDSTONE)); - IPatternDetails wrapped = GregTechCircuitPatternDetails.filterIntegratedCircuits(patternDetails, CIRCUITS); - - OptionalInt configuration = GregTechCircuitPatternDetails.findCircuitConfiguration(wrapped, CIRCUITS); - - assertTrue(configuration.isPresent()); - assertEquals(12, configuration.getAsInt()); - } - - @Test - void plannerInputCountIgnoresFilteredCircuitInputs() { - TestPatternDetails patternDetails = new TestPatternDetails( - new TestInput(3, CIRCUIT_12), - new TestInput(2, REDSTONE)); - IPatternDetails wrapped = GregTechCircuitPatternDetails.filterIntegratedCircuits(patternDetails, CIRCUITS); - - assertEquals(0, countPlannerInputs(wrapped, CIRCUIT_12)); - assertEquals(2, countPlannerInputs(wrapped, REDSTONE)); - } - - @Test - void plannerPatternNetIgnoresFilteredCircuitInputs() { - TestPatternDetails patternDetails = new TestPatternDetails( - List.of(new GenericStack(GOLD, 4)), - new TestInput(3, CIRCUIT_12), - new TestInput(2, REDSTONE)); - IPatternDetails wrapped = GregTechCircuitPatternDetails.filterIntegratedCircuits(patternDetails, CIRCUITS); - - KeyCounter net = new KeyCounter(); - Set inputKeys = new HashSet<>(); - accumulatePlannerPatternNet(net, inputKeys, wrapped, 2); - - assertEquals(8, net.get(GOLD)); - assertEquals(-4, net.get(REDSTONE)); - assertEquals(0, net.get(CIRCUIT_12)); - assertFalse(inputKeys.contains(CIRCUIT_12)); - assertTrue(inputKeys.contains(REDSTONE)); - } - - @Test - void unwrapReturnsOriginalPatternForFilteredCircuitPattern() { - TestPatternDetails patternDetails = new TestPatternDetails( - new TestInput(CIRCUIT_12), - new TestInput(REDSTONE)); - IPatternDetails wrapped = GregTechCircuitPatternDetails.filterIntegratedCircuits(patternDetails, CIRCUITS); - - assertSame(patternDetails, GregTechCircuitPatternDetails.unwrap(wrapped)); - } - - @Test - void unwrapPseudoKeepsCircuitWrapperWhenPatternIsNotPseudo() { - TestPatternDetails patternDetails = new TestPatternDetails( - new TestInput(CIRCUIT_12), - new TestInput(REDSTONE)); - IPatternDetails wrapped = GregTechCircuitPatternDetails.filterIntegratedCircuits(patternDetails, CIRCUITS); - - assertSame(wrapped, GregTechCircuitPatternDetails.unwrapPseudo(wrapped)); - } - - @Test - void filteringAlreadyFilteredPatternKeepsSameWrapper() { - TestPatternDetails patternDetails = new TestPatternDetails( - new TestInput(CIRCUIT_12), - new TestInput(REDSTONE)); - IPatternDetails wrapped = GregTechCircuitPatternDetails.filterIntegratedCircuits(patternDetails, CIRCUITS); - - assertSame(wrapped, GregTechCircuitPatternDetails.filterIntegratedCircuits(wrapped, CIRCUITS)); - } - - @Test - void repeatedFilteringOfSamePatternCanHitPlannerPatternMaps() { - TestPatternDetails patternDetails = new TestPatternDetails( - new TestInput(CIRCUIT_12), - new TestInput(REDSTONE)); - IPatternDetails firstWrapped = GregTechCircuitPatternDetails.filterIntegratedCircuits(patternDetails, CIRCUITS); - IPatternDetails secondWrapped = GregTechCircuitPatternDetails.filterIntegratedCircuits(patternDetails, CIRCUITS); - - Map providersByPattern = new HashMap<>(); - providersByPattern.put(firstWrapped, "provider"); - - assertEquals(firstWrapped, secondWrapped); - assertEquals(firstWrapped.hashCode(), secondWrapped.hashCode()); - assertEquals("provider", providersByPattern.get(secondWrapped)); - } - - @Test - void unwrapKeepsInnerWrapperForDoubleWrappedPattern() { - TestPatternDetails patternDetails = new TestPatternDetails( - new TestInput(CIRCUIT_12), - new TestInput(REDSTONE)); - TestPatternDetailsWrapper innerWrapper = new TestPatternDetailsWrapper(patternDetails); - IPatternDetails wrapped = GregTechCircuitPatternDetails.filterIntegratedCircuits(innerWrapper, CIRCUITS); - - assertSame(innerWrapper, GregTechCircuitPatternDetails.unwrap(wrapped)); - } - - @Test - void registeredProviderPatternMatchesFilteredCircuitWrapper() { - TestPatternDetails patternDetails = new TestPatternDetails( - new TestInput(CIRCUIT_12), - new TestInput(REDSTONE)); - IPatternDetails wrapped = GregTechCircuitPatternDetails.filterIntegratedCircuits(patternDetails, CIRCUITS); - - assertTrue(GregTechCircuitPatternDetails.containsPattern(List.of(patternDetails), wrapped)); - } - - @Test - void pushInputsToExternalInventorySkipsCircuitKeysAndKeepsDelegateAmounts() { - TestPatternDetails patternDetails = new TestPatternDetails( - new TestInput(CIRCUIT_21), - new TestInput(IRON)); - IPatternDetails wrapped = GregTechCircuitPatternDetails.filterIntegratedCircuits(patternDetails, CIRCUITS); - - KeyCounter firstInput = new KeyCounter(); - firstInput.add(CIRCUIT_21, 1); - KeyCounter secondInput = new KeyCounter(); - secondInput.add(IRON, 3); - - List pushed = new ArrayList<>(); - wrapped.pushInputsToExternalInventory(new KeyCounter[] { firstInput, secondInput }, - (key, amount) -> pushed.add(new GenericStack(key, amount))); - - assertEquals(1, pushed.size()); - assertSame(IRON, pushed.getFirst().what()); - assertEquals(3, pushed.getFirst().amount()); - } - - @Test - void pushInputsToExternalInventoryAcceptsFilteredInputHolderAfterDroppingCircuitOnlyInput() { - TestPatternDetails patternDetails = new IndexingExternalPushPatternDetails( - new TestInput(CIRCUIT_21), - new TestInput(IRON)); - IPatternDetails wrapped = GregTechCircuitPatternDetails.filterIntegratedCircuits(patternDetails, CIRCUITS); - - KeyCounter materialInput = new KeyCounter(); - materialInput.add(IRON, 3); - - List pushed = new ArrayList<>(); - wrapped.pushInputsToExternalInventory(new KeyCounter[] { materialInput }, - (key, amount) -> pushed.add(new GenericStack(key, amount))); - - assertEquals(1, pushed.size()); - assertSame(IRON, pushed.getFirst().what()); - assertEquals(3, pushed.getFirst().amount()); - } - - @Test - void filteredInputRejectsCircuitValidityAndDelegatesNormalValidity() { - TestInput delegateInput = new TestInput(IRON, CIRCUIT_4); - IPatternDetails wrapped = GregTechCircuitPatternDetails.filterIntegratedCircuits( - new TestPatternDetails(delegateInput), - CIRCUITS); - IPatternDetails.IInput filteredInput = wrapped.getInputs()[0]; - - assertTrue(filteredInput.isValid(IRON, null)); - assertFalse(filteredInput.isValid(CIRCUIT_4, null)); - assertSame(REDSTONE, filteredInput.getRemainingKey(IRON)); - assertEquals(1, delegateInput.validityChecks); - assertEquals(1, delegateInput.remainingKeyChecks); - } - - @Test - void noCircuitPatternHasNoConfigurationAndKeepsInputs() { - TestPatternDetails patternDetails = new TestPatternDetails( - new TestInput(IRON), - new TestInput(GOLD)); - - IPatternDetails wrapped = GregTechCircuitPatternDetails.filterIntegratedCircuits(patternDetails, CIRCUITS); - - assertTrue(GregTechCircuitPatternDetails.findCircuitConfiguration(patternDetails, CIRCUITS).isEmpty()); - assertEquals(2, wrapped.getInputs().length); - } - - @Test - void filterInputHolderRemovesCircuitEntriesWithoutMutatingOriginalCounters() { - KeyCounter firstInput = new KeyCounter(); - firstInput.add(CIRCUIT_4, 1); - firstInput.add(IRON, 2); - KeyCounter secondInput = new KeyCounter(); - secondInput.add(CIRCUIT_7, 1); - - KeyCounter[] filtered = GregTechCircuitPatternDetails.filterInputHolder( - new KeyCounter[] { firstInput, secondInput }, - CIRCUITS); - - assertEquals(2, filtered.length); - assertEquals(0, filtered[0].get(CIRCUIT_4)); - assertEquals(2, filtered[0].get(IRON)); - assertTrue(filtered[1].isEmpty()); - assertEquals(1, firstInput.get(CIRCUIT_4)); - assertEquals(1, secondInput.get(CIRCUIT_7)); - } - - @Test - void wrappedCraftingMachineSetsGhostCircuitAndFiltersMachineInputs() { - TestPatternDetails patternDetails = new TestPatternDetails( - new TestInput(CIRCUIT_4), - new TestInput(IRON)); - KeyCounter circuitSlot = new KeyCounter(); - circuitSlot.add(CIRCUIT_4, 1); - KeyCounter materialSlot = new KeyCounter(); - materialSlot.add(IRON, 3); - KeyCounter[] inputHolder = new KeyCounter[] { circuitSlot, materialSlot }; - RecordingCraftingMachine machine = new RecordingCraftingMachine(); - RecordingGhostCircuitTarget ghostTarget = new RecordingGhostCircuitTarget(); - - ICraftingMachine wrapped = GregTechCircuitPatternDetails.wrapCraftingMachine( - machine, - ghostTarget, - 4, - CIRCUITS); - - assertEquals(9, wrapped.getMaxPatternPushMultiplier(patternDetails, inputHolder, 9, EnumFacing.NORTH)); - assertEquals(4, ghostTarget.lastConfig); - assertEquals(0, machine.lastBatchInputs[0].get(CIRCUIT_4)); - assertEquals(3, machine.lastBatchInputs[1].get(IRON)); - assertInstanceOf(GregTechCircuitPatternDetails.class, machine.lastBatchPattern); - - assertTrue(wrapped.pushPattern(patternDetails, inputHolder, 2, EnumFacing.NORTH)); - assertEquals(4, ghostTarget.lastConfig); - assertEquals(0, machine.lastPushInputs[0].get(CIRCUIT_4)); - assertEquals(3, machine.lastPushInputs[1].get(IRON)); - assertInstanceOf(GregTechCircuitPatternDetails.class, machine.lastPushPattern); - assertEquals(1, circuitSlot.get(CIRCUIT_4)); - } - - @Test - void wrappedBatchTargetSetsGhostCircuitAndFiltersBatchInputs() { - TestPatternDetails patternDetails = new TestPatternDetails( - new TestInput(CIRCUIT_7), - new TestInput(IRON)); - KeyCounter circuitSlot = new KeyCounter(); - circuitSlot.add(CIRCUIT_7, 1); - KeyCounter materialSlot = new KeyCounter(); - materialSlot.add(IRON, 5); - RecordingBatchTarget batchTarget = new RecordingBatchTarget(); - RecordingGhostCircuitTarget ghostTarget = new RecordingGhostCircuitTarget(); - - IPatternProviderBatchTarget wrapped = GregTechCircuitPatternDetails.wrapBatchTarget( - batchTarget, - ghostTarget, - 7, - CIRCUITS); - - assertEquals(4, wrapped.getMaxPatternPushMultiplier( - patternDetails, - new KeyCounter[] { circuitSlot, materialSlot }, - 4, - EnumFacing.SOUTH)); - assertEquals(7, ghostTarget.lastConfig); - assertEquals(0, batchTarget.lastBatchInputs[0].get(CIRCUIT_7)); - assertEquals(5, batchTarget.lastBatchInputs[1].get(IRON)); - assertInstanceOf(GregTechCircuitPatternDetails.class, batchTarget.lastBatchPattern); - assertEquals(1, circuitSlot.get(CIRCUIT_7)); - } - - private static void assertStacks(List expected, GenericStack[] actual) { - assertEquals(expected.size(), actual.length); - for (int i = 0; i < expected.size(); i++) { - assertSame(expected.get(i), actual[i].what()); - } - } - - private static long countPlannerInputs(IPatternDetails patternDetails, TestKey what) { - long total = 0; - for (IPatternDetails.IInput input : patternDetails.getInputs()) { - for (GenericStack possibleInput : input.possibleInputs()) { - if (what.matches(possibleInput)) { - total += possibleInput.amount() * input.getMultiplier(); - break; - } - } - } - return total; - } - - @SuppressWarnings("SameParameterValue") - private static void accumulatePlannerPatternNet( - KeyCounter netByKey, - Set inputKeys, - IPatternDetails patternDetails, - long times - ) { - for (GenericStack output : patternDetails.getOutputs()) { - netByKey.add(output.what(), output.amount() * times); - } - for (IPatternDetails.IInput input : patternDetails.getInputs()) { - GenericStack[] possibleInputs = input.possibleInputs(); - if (possibleInputs.length == 0) { - continue; - } - GenericStack primaryInput = possibleInputs[0]; - netByKey.add(primaryInput.what(), -primaryInput.amount() * input.getMultiplier() * times); - inputKeys.add(primaryInput.what()); - } - } - - private static class TestPatternDetails implements IPatternDetails { - private final IPatternDetails.IInput[] inputs; - private final List outputs; - - private TestPatternDetails(IPatternDetails.IInput... inputs) { - this(List.of(), inputs); - } - - private TestPatternDetails(List outputs, IPatternDetails.IInput... inputs) { - this.inputs = inputs.clone(); - this.outputs = List.copyOf(outputs); - } - - @Override - public AEItemKey getDefinition() { - throw new UnsupportedOperationException("Definition is not part of this logic test"); - } - - @Override - public IPatternDetails.IInput[] getInputs() { - return this.inputs.clone(); - } - - @Override - public List getOutputs() { - return this.outputs; - } - } - - private record TestPatternDetailsWrapper(IPatternDetails delegate) implements IPatternDetails { - - @Override - public AEItemKey getDefinition() { - return this.delegate.getDefinition(); - } - - @Override - public IInput[] getInputs() { - return this.delegate.getInputs(); - } - - @Override - public List getOutputs() { - return this.delegate.getOutputs(); - } - } - - private static final class IndexingExternalPushPatternDetails extends TestPatternDetails { - - private IndexingExternalPushPatternDetails(IPatternDetails.IInput... inputs) { - super(inputs); - } - - @Override - public void pushInputsToExternalInventory(KeyCounter[] inputHolder, PatternInputSink inputSink) { - IPatternDetails.IInput[] inputs = this.getInputs(); - for (int i = 0; i < inputs.length; i++) { - for (var input : inputHolder[i]) { - inputSink.pushInput(input.getKey(), input.getLongValue()); - } - } - } - } - - private static final class TestInput implements IPatternDetails.IInput { - private final GenericStack[] possibleInputs; - private int validityChecks; - private int remainingKeyChecks; - - private TestInput(TestKey... possibleInputs) { - this(1, possibleInputs); - } - - private TestInput(long multiplier, TestKey... possibleInputs) { - this.possibleInputs = new GenericStack[possibleInputs.length]; - for (int i = 0; i < possibleInputs.length; i++) { - this.possibleInputs[i] = new GenericStack(possibleInputs[i], 1); - } - this.multiplier = multiplier; - } - - private final long multiplier; - - @Override - public GenericStack[] possibleInputs() { - return this.possibleInputs.clone(); - } - - @Override - public long getMultiplier() { - return this.multiplier; - } - - @Override - public boolean isValid(AEKey input, World level) { - this.validityChecks++; - for (GenericStack possibleInput : this.possibleInputs) { - if (possibleInput.what().equals(input)) { - return true; - } - } - return false; - } - - @Override - public AEKey getRemainingKey(AEKey template) { - this.remainingKeyChecks++; - return REDSTONE; - } - } - - private record TestCircuitStrategy() - implements GregTechCircuitPatternDetails.CircuitMatcher, GregTechCircuitPatternDetails.CircuitConfigExtractor { - - @Override - public boolean isCircuit(AEKey key) { - return getCircuitConfiguration(key).isPresent(); - } - - @Override - public OptionalInt getCircuitConfiguration(AEKey key) { - if (key instanceof TestKey testKey && testKey.name().startsWith("circuit-")) { - return OptionalInt.of(Integer.parseInt(testKey.name().substring("circuit-".length()))); - } - return OptionalInt.empty(); - } - } - - private static final class RecordingGhostCircuitTarget implements IGhostSlotConfigurable { - private int lastConfig = -1; - - @Override - public boolean hasGhostCircuitInventory() { - return true; - } - - @Override - public void setGhostCircuitConfig(int config) { - this.lastConfig = config; - } - } - - private static final class RecordingCraftingMachine implements ICraftingMachine { - private IPatternDetails lastBatchPattern; - private KeyCounter[] lastBatchInputs; - private IPatternDetails lastPushPattern; - private KeyCounter[] lastPushInputs; - - @Override - public PatternContainerGroup getCraftingMachineInfo() { - return PatternContainerGroup.nothing(); - } - - @Override - public boolean pushPattern( - IPatternDetails patternDetails, - KeyCounter[] inputs, - int multiplier, - EnumFacing ejectionDirection - ) { - this.lastPushPattern = patternDetails; - this.lastPushInputs = inputs; - return true; - } - - @Override - public boolean acceptsPlans() { - return true; - } - - @Override - public int getMaxPatternPushMultiplier( - IPatternDetails patternDetails, - KeyCounter[] inputs, - int maxMultiplier, - EnumFacing ejectionDirection - ) { - this.lastBatchPattern = patternDetails; - this.lastBatchInputs = inputs; - return maxMultiplier; - } - } - - private static final class RecordingBatchTarget implements IPatternProviderBatchTarget { - private IPatternDetails lastBatchPattern; - private KeyCounter[] lastBatchInputs; - - @Override - public int getMaxPatternPushMultiplier( - IPatternDetails patternDetails, - KeyCounter[] inputs, - int maxMultiplier, - EnumFacing ejectionDirection - ) { - this.lastBatchPattern = patternDetails; - this.lastBatchInputs = inputs; - return maxMultiplier; - } - } - - private static final class TestKey extends AEKey { - private final String name; - - private TestKey(String name) { - this.name = name; - } - - private String name() { - return this.name; - } - - @Override - public AEKeyType getType() { - return AEKeyType.items(); - } - - @Override - public AEKey dropSecondary() { - return this; - } - - @Override - public NBTTagCompound toTag() { - NBTTagCompound tag = new NBTTagCompound(); - tag.setString("name", this.name); - return tag; - } - - @Override - public Object getPrimaryKey() { - return this.name; - } - - @Override - public ResourceLocation getId() { - return new ResourceLocation("ae2gtaddon_test", this.name); - } - - @Override - public void writeToPacket(PacketBuffer data) { - throw new UnsupportedOperationException("Packet serialization is not part of this logic test"); - } - - @Override - public @Nullable Object getReadOnlyStack() { - return this.name; - } - - @Override - protected ITextComponent computeDisplayName() { - return new TextComponentString(this.name); - } - - @Override - public boolean isTagged(String tag) { - return false; - } - - @Override - public @Nullable NBTBase get(String componentId) { - return null; - } - - @Override - public boolean hasComponents() { - return false; - } - } -}