From 7e4eb8c381ef98222a537d93827b9e55dd66ecfb Mon Sep 17 00:00:00 2001
From: vcwdfca <2168409010@qq.com>
Date: Wed, 14 Oct 2026 22:44:40 +0800
Subject: [PATCH 1/9] use mdg
---
.github/workflows/build.yml | 35 +-
.github/workflows/release.yml | 44 +-
.github/workflows/runtime-test.yml | 18 +
.gitignore | 2 -
build.gradle | 446 ++++++++++++------
gradle.properties | 86 +++-
gradle/scripts/dependencies.gradle | 48 +-
gradle/scripts/extra.gradle | 336 ++++++++++++-
gradle/scripts/helpers.gradle | 96 ++++
gradle/scripts/publishing.gradle | 157 +++++-
gradle/wrapper/gradle-wrapper.properties | 6 +-
settings.gradle | 27 +-
....mod.mixin.json => ae2gtaddon.mixins.json} | 5 +-
src/main/resources/mcmod.info | 9 +-
14 files changed, 1043 insertions(+), 272 deletions(-)
create mode 100644 .github/workflows/runtime-test.yml
create mode 100644 gradle/scripts/helpers.gradle
rename src/main/resources/{ae2gtaddon.mod.mixin.json => ae2gtaddon.mixins.json} (80%)
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/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/build.gradle b/build.gradle
index cde26c5..47def7c 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,123 @@ 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'))
+ 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:cleanmix:0.7.2'
}
- 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 +211,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 +265,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 +299,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 +385,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..6123d58 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.16
+
# 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..33f7974 100644
--- a/gradle/scripts/dependencies.gradle
+++ b/gradle/scripts/dependencies.gradle
@@ -1,44 +1,70 @@
+apply from: 'gradle/scripts/helpers.gradle'
+
repositories {
- // Other repositories described by default:
- // CleanroomMC: https://maven.cleanroommc.com
flatDir {
- dirs('lib')
+ dirs('lib') // /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 'org.jetbrains:annotations:26.0.2'
compileOnly "com.cleanroommc:sponge-mixin:0.20.13+mixin.0.8.7"
- if (enable_lwjglx.toBoolean()) {
+ 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.31.2")
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..dfae4c0 100644
--- a/gradle/scripts/extra.gradle
+++ b/gradle/scripts/extra.gradle
@@ -1,5 +1,333 @@
-// You may write any gradle buildscript component in this file
-// This file is automatically applied after build.gradle + dependencies.gradle is ran
+// You may write any Gradle buildscript component in this file.
+// This file is automatically applied after build.gradle + dependencies.gradle.
-// Helper methods (assertProperty, assertSubProperties, setDefaultProperty) are
-// defined directly in build.gradle's script scope and exported via ext.
+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
+)
+runtimeTestProductionMods.attributes {
+ attribute(mappingsAttribute, 'srg')
+ attribute(
+ Usage.USAGE_ATTRIBUTE,
+ objects.named(Usage, 'java-runtime')
+ )
+}
+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') {
+ inputs.files(runtimeTestProductionMods)
+ inputs.property('runtimeRemapSchema', 'moddevgradle-mcp-runtime-classpath-v1')
+ outputs.file(runtimeMcpRemapMarker)
+ doLast {
+ def files = runtimeTestProductionMods.files.findAll {
+ it.name.endsWith('.jar') && !it.name.endsWith('-sources.jar')
+ }
+ runtimeMcpRemapMarker.get().asFile.with {
+ parentFile.mkdirs()
+ text = files.collect { it.name }.sort().join('\n') + '\n'
+ }
+ logger.lifecycle(
+ "[RuntimeTest] ModDevGradle supplied ${files.size()} runtime mod variant(s)"
+ )
+ }
+}
+
+tasks.register('stageRuntimeTestMods', Sync) {
+ dependsOn productionJarTask
+ dependsOn runtimeMcpRemapTask
+ from(productionJarTask.flatMap { it.archiveFile })
+ from(runtimeTestProductionMods) {
+ include '*.jar'
+ exclude '*-sources.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}"
+ }
+}
+
+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..fc43e87 100644
--- a/gradle/scripts/publishing.gradle
+++ b/gradle/scripts/publishing.gradle
@@ -1,25 +1,158 @@
-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 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
+ 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/resources/ae2gtaddon.mod.mixin.json b/src/main/resources/ae2gtaddon.mixins.json
similarity index 80%
rename from src/main/resources/ae2gtaddon.mod.mixin.json
rename to src/main/resources/ae2gtaddon.mixins.json
index 219eb8c..7474004 100644
--- a/src/main/resources/ae2gtaddon.mod.mixin.json
+++ b/src/main/resources/ae2gtaddon.mixins.json
@@ -2,13 +2,16 @@
"required": true,
"package": "com.formlessDragon.ae2gtaddon.mixin.mod",
"plugin": "com.formlessDragon.ae2gtaddon.ModMixinConfigPlugin",
- "compatibilityLevel": "JAVA_8",
+ "compatibilityLevel": "JAVA_25",
"target": "@env(MOD)",
+ "refmap": "ae2gtaddon.refmap.json",
"client": [
"hei.MixinPatternEncodingRecipeTransferHandler",
+ "ae2.MixinAEGuiHandlerClient",
"ae2.MixinProcessingEncodingPanel"
],
"mixins": [
+ "ae2.MixinAEGuiHandler",
"ae2.MixinContainerPatternEncodingTerm",
"ae2.MixinEncodingModePanel",
"ae2.MixinPatternProviderP2PTunnelPart",
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}"
+}]
From 2a4e5f0d05639b7340f99f8f8772ba4fa83f65ce Mon Sep 17 00:00:00 2001
From: vcwdfca <2168409010@qq.com>
Date: Wed, 14 Oct 2026 22:45:13 +0800
Subject: [PATCH 2/9] add extended encoding term
---
.github/workflows/issue-labels.yml | 36 +
README.md | 28 +-
gradle.properties | 2 +-
gradle/scripts/dependencies.gradle | 4 +-
.../ExtendedProviderSelectionBridge.java | 184 ++
.../formlessDragon/ae2gtaddon/AEGTAddon.java | 18 +-
.../ae2gtaddon/AEGTAddonConfig.java | 12 +
...nfigPlugin.java => MixinConfigPlugin.java} | 26 +-
.../client/gui/ExtendedEncodingModePanel.java | 60 +
.../gui/ExtendedProcessingEncodingPanel.java | 347 +++
.../gui/GuiExtendedPatternEncodingTerm.java | 327 +++
.../gui/GuiExtendedPatternItemRenamer.java | 95 +
.../handler/HeiPatternAutoNamingHandler.java | 50 +
.../handler/HeiPatternInputFilterHandler.java | 52 +
.../ContainerExtendedEncodingTerm.java | 2411 +++++++++++++++++
.../ExtendedEncodingTermGuiHandler.java | 80 +
.../ae2gtaddon/ids/ItemIds.java | 2 +-
.../ae2gtaddon/{Init => init}/Items.java | 29 +-
.../GregTechCircuitPatternDetails.java | 180 +-
.../integration/hei/AE2GTAddonHeiPlugin.java | 30 +
.../hei/AutoPatternItemNaming.java | 18 +
...dPatternEncodingRecipeTransferHandler.java | 428 +++
.../HeiAssemblyLineRecipeTransferHandler.java | 41 +
.../hei/HeiPatternItemAutoNamer.java | 118 +
.../PatternEncodingPanelContainerAccess.java | 2 +-
.../ProcessingPatternCircuitImportMode.java | 4 +-
.../mixin/ae2/MixinAEProcessingPattern.java | 22 +
.../MixinContainerPatternEncodingTerm.java | 37 +
.../{mod => }/ae2/MixinEncodingModePanel.java | 6 +-
.../ae2/MixinPatternProviderLogic.java | 11 +-
.../MixinPatternProviderP2PTunnelPart.java | 2 +-
.../ae2/MixinProcessingEncodingPanel.java | 175 ++
.../ae2/MixinPseudoPatternDetails.java | 2 +-
.../MixinContainerPatternEncodingTerm.java | 23 -
.../mod/ae2/MixinProcessingEncodingPanel.java | 110 -
...nPatternEncodingRecipeTransferHandler.java | 88 -
.../parts/ExtendedEncodingDraft.java | 116 +
.../parts/ExtendedEncodingLogic.java | 183 ++
.../parts/ExtendedEncodingMode.java | 52 +
.../parts/ExtendedEncodingTermPart.java | 119 +
.../ae2gtaddon/proxy/ClientProxy.java | 16 +
.../ae2gtaddon/proxy/CommonProxy.java | 2 +-
.../ae2gtaddon/util/AEKeyMatcher.java | 11 +
.../ae2gtaddon/util/Matcher.java | 31 +
.../formlessDragon/ae2gtaddon/util/Util.java | 29 +
src/main/resources/ae2gtaddon.mixins.json | 8 +-
...on_extended_pattern_encoding_terminal.json | 165 ++
.../ae2gtaddon_extended_processing.json | 146 +
.../assets/ae2gtaddon/lang/en_us.lang | 11 +-
.../assets/ae2gtaddon/lang/zh_cn.lang | 11 +-
.../item/extended_encoding_terminal.json | 9 +
.../guis/extended_encoding_terminal.png | Bin 0 -> 3285 bytes
.../ae2gtaddon/textures/guis/multi-in.png | Bin 0 -> 1031 bytes
.../ae2gtaddon/textures/guis/multi-out.png | Bin 0 -> 1015 bytes
.../GregTechCircuitExternalTargetTest.java | 97 -
.../GregTechCircuitPatternDetailsTest.java | 669 -----
56 files changed, 5566 insertions(+), 1169 deletions(-)
create mode 100644 .github/workflows/issue-labels.yml
create mode 100644 src/main/java/ae2/container/me/patternencode/ExtendedProviderSelectionBridge.java
create mode 100644 src/main/java/com/formlessDragon/ae2gtaddon/AEGTAddonConfig.java
rename src/main/java/com/formlessDragon/ae2gtaddon/{ModMixinConfigPlugin.java => MixinConfigPlugin.java} (55%)
create mode 100644 src/main/java/com/formlessDragon/ae2gtaddon/client/gui/ExtendedEncodingModePanel.java
create mode 100644 src/main/java/com/formlessDragon/ae2gtaddon/client/gui/ExtendedProcessingEncodingPanel.java
create mode 100644 src/main/java/com/formlessDragon/ae2gtaddon/client/gui/GuiExtendedPatternEncodingTerm.java
create mode 100644 src/main/java/com/formlessDragon/ae2gtaddon/client/gui/GuiExtendedPatternItemRenamer.java
create mode 100644 src/main/java/com/formlessDragon/ae2gtaddon/client/handler/HeiPatternAutoNamingHandler.java
create mode 100644 src/main/java/com/formlessDragon/ae2gtaddon/client/handler/HeiPatternInputFilterHandler.java
create mode 100644 src/main/java/com/formlessDragon/ae2gtaddon/container/ContainerExtendedEncodingTerm.java
create mode 100644 src/main/java/com/formlessDragon/ae2gtaddon/container/ExtendedEncodingTermGuiHandler.java
rename src/main/java/com/formlessDragon/ae2gtaddon/{Init => init}/Items.java (59%)
create mode 100644 src/main/java/com/formlessDragon/ae2gtaddon/integration/hei/AE2GTAddonHeiPlugin.java
create mode 100644 src/main/java/com/formlessDragon/ae2gtaddon/integration/hei/AutoPatternItemNaming.java
create mode 100644 src/main/java/com/formlessDragon/ae2gtaddon/integration/hei/ExtendedPatternEncodingRecipeTransferHandler.java
create mode 100644 src/main/java/com/formlessDragon/ae2gtaddon/integration/hei/HeiAssemblyLineRecipeTransferHandler.java
create mode 100644 src/main/java/com/formlessDragon/ae2gtaddon/integration/hei/HeiPatternItemAutoNamer.java
create mode 100644 src/main/java/com/formlessDragon/ae2gtaddon/mixin/ae2/MixinAEProcessingPattern.java
create mode 100644 src/main/java/com/formlessDragon/ae2gtaddon/mixin/ae2/MixinContainerPatternEncodingTerm.java
rename src/main/java/com/formlessDragon/ae2gtaddon/mixin/{mod => }/ae2/MixinEncodingModePanel.java (71%)
rename src/main/java/com/formlessDragon/ae2gtaddon/mixin/{mod => }/ae2/MixinPatternProviderLogic.java (97%)
rename src/main/java/com/formlessDragon/ae2gtaddon/mixin/{mod => }/ae2/MixinPatternProviderP2PTunnelPart.java (98%)
create mode 100644 src/main/java/com/formlessDragon/ae2gtaddon/mixin/ae2/MixinProcessingEncodingPanel.java
rename src/main/java/com/formlessDragon/ae2gtaddon/mixin/{mod => }/ae2/MixinPseudoPatternDetails.java (96%)
delete mode 100644 src/main/java/com/formlessDragon/ae2gtaddon/mixin/mod/ae2/MixinContainerPatternEncodingTerm.java
delete mode 100644 src/main/java/com/formlessDragon/ae2gtaddon/mixin/mod/ae2/MixinProcessingEncodingPanel.java
delete mode 100644 src/main/java/com/formlessDragon/ae2gtaddon/mixin/mod/hei/MixinPatternEncodingRecipeTransferHandler.java
create mode 100644 src/main/java/com/formlessDragon/ae2gtaddon/parts/ExtendedEncodingDraft.java
create mode 100644 src/main/java/com/formlessDragon/ae2gtaddon/parts/ExtendedEncodingLogic.java
create mode 100644 src/main/java/com/formlessDragon/ae2gtaddon/parts/ExtendedEncodingMode.java
create mode 100644 src/main/java/com/formlessDragon/ae2gtaddon/parts/ExtendedEncodingTermPart.java
create mode 100644 src/main/java/com/formlessDragon/ae2gtaddon/util/AEKeyMatcher.java
create mode 100644 src/main/java/com/formlessDragon/ae2gtaddon/util/Matcher.java
create mode 100644 src/main/java/com/formlessDragon/ae2gtaddon/util/Util.java
create mode 100644 src/main/resources/assets/ae2/screens/terminals/ae2gtaddon_extended_pattern_encoding_terminal.json
create mode 100644 src/main/resources/assets/ae2/screens/terminals/encoding/ae2gtaddon_extended_processing.json
create mode 100644 src/main/resources/assets/ae2gtaddon/models/item/extended_encoding_terminal.json
create mode 100644 src/main/resources/assets/ae2gtaddon/textures/guis/extended_encoding_terminal.png
create mode 100644 src/main/resources/assets/ae2gtaddon/textures/guis/multi-in.png
create mode 100644 src/main/resources/assets/ae2gtaddon/textures/guis/multi-out.png
delete mode 100644 src/test/java/ae2/helpers/patternprovider/GregTechCircuitExternalTargetTest.java
delete mode 100644 src/test/java/com/formlessDragon/ae2gtaddon/integration/gregtech/GregTechCircuitPatternDetailsTest.java
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/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 [](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/gradle.properties b/gradle.properties
index 6123d58..3dffed0 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -56,7 +56,7 @@ mod_icon_item =
mod_background_path =
# Dependencies
-ae2_version = 1.0.16
+ae2_version = 1.0.17
# Run Configurations
minecraft_username = Developer
diff --git a/gradle/scripts/dependencies.gradle b/gradle/scripts/dependencies.gradle
index 33f7974..f303fea 100644
--- a/gradle/scripts/dependencies.gradle
+++ b/gradle/scripts/dependencies.gradle
@@ -2,7 +2,7 @@ apply from: 'gradle/scripts/helpers.gradle'
repositories {
flatDir {
- dirs('lib') // /lib
+ dirs('lib', 'lib/local') // /lib
}
maven {
name = 'CleanroomMC'
@@ -60,7 +60,7 @@ dependencies {
}
compileOnly("curse.maven:mouse-tweaks-unofficial-461660:5876158")
- modImplementation("mezz:jei:4.31.2")
+ modImplementation("mezz:jei:4.35.1")
modImplementation("codechicken:CodeChickenLib:3.3.5")
modImplementation("curse.maven:gregtech-ce-unofficial-557242:5519022")
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 extends GuiTextField> 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 extends GuiTextField> 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..e28804e
--- /dev/null
+++ b/src/main/java/com/formlessDragon/ae2gtaddon/container/ContainerExtendedEncodingTerm.java
@@ -0,0 +1,2411 @@
+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 extends PatternContainer> 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..232fef5
--- /dev/null
+++ b/src/main/java/com/formlessDragon/ae2gtaddon/integration/hei/ExtendedPatternEncodingRecipeTransferHandler.java
@@ -0,0 +1,428 @@
+package com.formlessDragon.ae2gtaddon.integration.hei;
+
+import ae2.api.client.PatternImportPriorityContext;
+import ae2.api.crafting.PatternDetailsHelper;
+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
+ || stack.amount() > PatternDetailsHelper.MAX_PROCESSING_PATTERN_AMOUNT)) {
+ 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..1458735
--- /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