diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..4ffbd90 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,22 @@ +# This is the universal Text Editor Configuration +# for all GTNewHorizons projects +# See: https://editorconfig.org/ + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{bat,ini}] +end_of_line = crlf + +[*.{dtd,json,info,mcmeta,md,sh,svg,xml,xsd,xsl,yaml,yml}] +indent_size = 2 + +[*.lang] +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..fd2792b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,44 @@ +* text eol=lf + +*.[jJ][aA][rR] binary + +*.[pP][nN][gG] binary +*.[jJ][pP][gG] binary +*.[jJ][pP][eE][gG] binary +*.[gG][iI][fF] binary +*.[tT][iI][fF] binary +*.[tT][iI][fF][fF] binary +*.[iI][cC][oO] binary +*.[sS][vV][gG] text +*.[eE][pP][sS] binary +*.[xX][cC][fF] binary + +*.[kK][aA][rR] binary +*.[mM]4[aA] binary +*.[mM][iI][dD] binary +*.[mM][iI][dD][iI] binary +*.[mM][pP]3 binary +*.[oO][gG][gG] binary +*.[rR][aA] binary + +*.7[zZ] binary +*.[gG][zZ] binary +*.[tT][aA][rR] binary +*.[tT][gG][zZ] binary +*.[zZ][iI][pP] binary + +*.[tT][cC][nN] binary +*.[sS][oO] binary +*.[dD][lL][lL] binary +*.[dD][yY][lL][iI][bB] binary +*.[pP][sS][dD] binary +*.[tT][tT][fF] binary +*.[oO][tT][fF] binary + +*.[pP][aA][tT][cC][hH] -text + +*.[bB][aA][tT] text eol=crlf +*.[cC][mM][dD] text eol=crlf +*.[pP][sS]1 text eol=crlf + +*[aA][uU][tT][oO][gG][eE][nN][eE][rR][aA][tT][eE][dD]* binary diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml new file mode 100644 index 0000000..3ee2f68 --- /dev/null +++ b/.github/workflows/build-and-test.yml @@ -0,0 +1,13 @@ + +name: Build and test + +on: + pull_request: + branches: [ master, main ] + push: + branches: [ master, main ] + +jobs: + build-and-test: + uses: GTNewHorizons/GTNH-Actions-Workflows/.github/workflows/build-and-test.yml@master + secrets: inherit diff --git a/.github/workflows/release-tags.yml b/.github/workflows/release-tags.yml new file mode 100644 index 0000000..e4c0be6 --- /dev/null +++ b/.github/workflows/release-tags.yml @@ -0,0 +1,14 @@ + +name: Release tagged build + +on: + push: + tags: [ '*' ] + +permissions: + contents: write + +jobs: + release-tags: + uses: GTNewHorizons/GTNH-Actions-Workflows/.github/workflows/release-tags.yml@master + secrets: inherit diff --git a/.gitignore b/.gitignore index 1fc89bc..5e80e0a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,21 +1,38 @@ -# ForgeGradle -/.gradle -/build -/run - -# Eclipse +.gradle +.settings +/.idea/ +/.vscode/ +/run/ +/build/ +/eclipse/ .classpath .project -/.settings -/bin - -# IntelliJ -/.idea +/bin/ +/config/ +/crash-reports/ +/logs/ +options.txt +/saves/ +usernamecache.json +banned-ips.json +banned-players.json +eula.txt +ops.json +server.properties +servers.dat +usercache.json +whitelist.json +/out/ *.iml *.ipr *.iws -/out - -# Mac Stuff -.DS_Store -*.command \ No newline at end of file +src/main/resources/mixins.*([!.]).json +*.bat +*.DS_Store +!gradlew.bat +.factorypath +addon.local.gradle +addon.local.gradle.kts +addon.late.local.gradle +addon.late.local.gradle.kts +layout.json diff --git a/LICENSE.txt b/LICENSE similarity index 100% rename from LICENSE.txt rename to LICENSE diff --git a/build.gradle b/build.gradle index 6dfd798..621bc62 100644 --- a/build.gradle +++ b/build.gradle @@ -1,63 +1,1624 @@ +//version: 1696265388 +/* + DO NOT CHANGE THIS FILE! + Also, you may replace this file at any time if there is an update available. + Please check https://github.com/GTNewHorizons/ExampleMod1.7.10/blob/master/build.gradle for updates. + */ + + +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +import com.gtnewhorizons.retrofuturagradle.ObfuscationAttribute +import com.gtnewhorizons.retrofuturagradle.mcp.ReobfuscatedJar +import com.gtnewhorizons.retrofuturagradle.minecraft.RunMinecraftTask +import com.gtnewhorizons.retrofuturagradle.util.Distribution +import com.matthewprenger.cursegradle.CurseArtifact +import com.matthewprenger.cursegradle.CurseRelation +import com.modrinth.minotaur.dependencies.ModDependency +import com.modrinth.minotaur.dependencies.VersionDependency +import org.gradle.internal.logging.text.StyledTextOutput.Style +import org.gradle.internal.logging.text.StyledTextOutputFactory +import org.gradle.internal.xml.XmlTransformer +import org.jetbrains.gradle.ext.Application +import org.jetbrains.gradle.ext.Gradle + +import javax.inject.Inject +import java.nio.file.Files +import java.nio.file.Paths +import java.util.concurrent.TimeUnit + buildscript { repositories { mavenCentral() + + maven { + name 'forge' + url 'https://maven.minecraftforge.net' + } maven { - name = "forge" - url = "http://files.minecraftforge.net/maven" + // GTNH RetroFuturaGradle and ASM Fork + name "GTNH Maven" + url "http://jenkins.usrv.eu:8081/nexus/content/groups/public/" + allowInsecureProtocol = true } maven { - name = "sonatype" - url = "https://oss.sonatype.org/content/repositories/snapshots/" + name 'sonatype' + url 'https://oss.sonatype.org/content/repositories/snapshots/' } + maven { + name 'Scala CI dependencies' + url 'https://repo1.maven.org/maven2/' + } + + mavenLocal() } +} +plugins { + id 'java-library' + id "org.jetbrains.gradle.plugin.idea-ext" version "1.1.7" + id 'eclipse' + id 'scala' + id 'maven-publish' + id 'org.jetbrains.kotlin.jvm' version '1.8.0' apply false + id 'org.jetbrains.kotlin.kapt' version '1.8.0' apply false + id 'com.google.devtools.ksp' version '1.8.0-1.0.9' apply false + id 'org.ajoberstar.grgit' version '4.1.1' // 4.1.1 is the last jvm8 supporting version, unused, available for addon.gradle + id 'com.github.johnrengelman.shadow' version '8.1.1' apply false + id 'com.palantir.git-version' version '3.0.0' apply false + id 'de.undercouch.download' version '5.4.0' + id 'com.github.gmazzo.buildconfig' version '3.1.0' apply false // Unused, available for addon.gradle + id 'com.diffplug.spotless' version '6.13.0' apply false // 6.13.0 is the last jvm8 supporting version + id 'com.modrinth.minotaur' version '2.+' apply false + id 'com.matthewprenger.cursegradle' version '1.4.0' apply false + id 'com.gtnewhorizons.retrofuturagradle' version '1.3.24' +} + +print("You might want to check out './gradlew :faq' if your build fails.\n") + +boolean settingsupdated = verifySettingsGradle() +settingsupdated = verifyGitAttributes() || settingsupdated +if (settingsupdated) + throw new GradleException("Settings has been updated, please re-run task.") + +// In submodules, .git is a file pointing to the real git dir +if (project.file('.git/HEAD').isFile() || project.file('.git').isFile()) { + apply plugin: 'com.palantir.git-version' +} + +def out = services.get(StyledTextOutputFactory).create('an-output') + +def projectJavaVersion = JavaLanguageVersion.of(8) + +boolean disableSpotless = project.hasProperty("disableSpotless") ? project.disableSpotless.toBoolean() : false +boolean disableCheckstyle = project.hasProperty("disableCheckstyle") ? project.disableCheckstyle.toBoolean() : false + +final String CHECKSTYLE_CONFIG = """ + + + + + + + + + + + +""" + +checkPropertyExists("modName") +checkPropertyExists("modId") +checkPropertyExists("modGroup") +checkPropertyExists("autoUpdateBuildScript") +checkPropertyExists("minecraftVersion") +checkPropertyExists("forgeVersion") +checkPropertyExists("replaceGradleTokenInFile") +checkPropertyExists("gradleTokenVersion") +checkPropertyExists("apiPackage") +checkPropertyExists("accessTransformersFile") +checkPropertyExists("usesMixins") +checkPropertyExists("mixinPlugin") +checkPropertyExists("mixinsPackage") +checkPropertyExists("coreModClass") +checkPropertyExists("containsMixinsAndOrCoreModOnly") +checkPropertyExists("usesShadowedDependencies") +checkPropertyExists("developmentEnvironmentUserName") + +propertyDefaultIfUnset("generateGradleTokenClass", "") +propertyDefaultIfUnset("includeWellKnownRepositories", true) +propertyDefaultIfUnset("noPublishedSources", false) +propertyDefaultIfUnset("usesMixinDebug", project.usesMixins) +propertyDefaultIfUnset("forceEnableMixins", false) +propertyDefaultIfUnset("channel", "stable") +propertyDefaultIfUnset("mappingsVersion", "12") +propertyDefaultIfUnset("usesMavenPublishing", true) +propertyDefaultIfUnset("mavenPublishUrl", "http://jenkins.usrv.eu:8081/nexus/content/repositories/releases") +propertyDefaultIfUnset("modrinthProjectId", "") +propertyDefaultIfUnset("modrinthRelations", "") +propertyDefaultIfUnset("curseForgeProjectId", "") +propertyDefaultIfUnset("curseForgeRelations", "") +propertyDefaultIfUnset("minimizeShadowedDependencies", true) +propertyDefaultIfUnset("relocateShadowedDependencies", true) +// Deprecated properties (kept for backwards compat) +propertyDefaultIfUnset("gradleTokenModId", "") +propertyDefaultIfUnset("gradleTokenModName", "") +propertyDefaultIfUnset("gradleTokenGroupName", "") + +propertyDefaultIfUnset("enableModernJavaSyntax", false) // On by default for new projects only +propertyDefaultIfUnset("enableGenericInjection", false) // On by default for new projects only + +// this is meant to be set using the user wide property file. by default we do nothing. +propertyDefaultIfUnset("ideaOverrideBuildType", "") // Can be nothing, "gradle" or "idea" + +project.extensions.add(com.diffplug.blowdryer.Blowdryer, "Blowdryer", com.diffplug.blowdryer.Blowdryer) // Make blowdryer available in "apply from:" scripts +if (!disableSpotless) { + apply plugin: 'com.diffplug.spotless' + apply from: Blowdryer.file('spotless.gradle') +} + +if (!disableCheckstyle) { + apply plugin: 'checkstyle' + tasks.named("checkstylePatchedMc") { enabled = false } + tasks.named("checkstyleMcLauncher") { enabled = false } + tasks.named("checkstyleIdeVirtualMain") { enabled = false } + tasks.named("checkstyleInjectedTags") { enabled = false } + checkstyle { + config = resources.text.fromString(CHECKSTYLE_CONFIG) + } +} + +String javaSourceDir = "src/main/java/" +String scalaSourceDir = "src/main/scala/" +String kotlinSourceDir = "src/main/kotlin/" + +if (usesShadowedDependencies.toBoolean()) { + apply plugin: "com.github.johnrengelman.shadow" +} + +java { + toolchain { + if (enableModernJavaSyntax.toBoolean()) { + languageVersion.set(JavaLanguageVersion.of(17)) + } else { + languageVersion.set(projectJavaVersion) + } + vendor.set(JvmVendorSpec.AZUL) + } + if (!noPublishedSources) { + withSourcesJar() + } +} + +tasks.withType(JavaCompile).configureEach { + options.encoding = "UTF-8" +} + +tasks.withType(ScalaCompile).configureEach { + options.encoding = "UTF-8" +} + +pluginManager.withPlugin('org.jetbrains.kotlin.jvm') { + // If Kotlin is enabled in the project + kotlin { + jvmToolchain(8) + } + // Kotlin hacks our source sets, so we hack Kotlin's tasks + def disabledKotlinTaskList = [ + "kaptGenerateStubsMcLauncherKotlin", + "kaptGenerateStubsPatchedMcKotlin", + "kaptGenerateStubsInjectedTagsKotlin", + "compileMcLauncherKotlin", + "compilePatchedMcKotlin", + "compileInjectedTagsKotlin", + "kaptMcLauncherKotlin", + "kaptPatchedMcKotlin", + "kaptInjectedTagsKotlin", + "kspMcLauncherKotlin", + "kspPatchedMcKotlin", + "kspInjectedTagsKotlin", + ] + tasks.configureEach { task -> + if (task.name in disabledKotlinTaskList) { + task.enabled = false + } + } +} + +configurations { + create("runtimeOnlyNonPublishable") { + description = "Runtime only dependencies that are not published alongside the jar" + canBeConsumed = false + canBeResolved = false + } + + create("devOnlyNonPublishable") { + description = "Runtime and compiletime dependencies that are not published alongside the jar (compileOnly + runtimeOnlyNonPublishable)" + canBeConsumed = false + canBeResolved = false + } + compileOnly.extendsFrom(devOnlyNonPublishable) + runtimeOnlyNonPublishable.extendsFrom(devOnlyNonPublishable) +} + +if (enableModernJavaSyntax.toBoolean()) { + repositories { + mavenCentral { + mavenContent { + includeGroup("me.eigenraven.java8unsupported") + } + } + } + dependencies { - classpath 'net.minecraftforge.gradle:ForgeGradle:1.2-SNAPSHOT' + annotationProcessor 'com.github.bsideup.jabel:jabel-javac-plugin:1.0.0' + // workaround for https://github.com/bsideup/jabel/issues/174 + annotationProcessor 'net.java.dev.jna:jna-platform:5.13.0' + compileOnly('com.github.bsideup.jabel:jabel-javac-plugin:1.0.0') { + transitive = false // We only care about the 1 annotation class + } + // Allow using jdk.unsupported classes like sun.misc.Unsafe in the compiled code, working around JDK-8206937. + patchedMinecraft('me.eigenraven.java8unsupported:java-8-unsupported-shim:1.0.0') + } + + tasks.withType(JavaCompile).configureEach { + if (it.name in ["compileMcLauncherJava", "compilePatchedMcJava"]) { + return + } + sourceCompatibility = 17 // for the IDE support + options.release.set(8) + + javaCompiler.set(javaToolchains.compilerFor { + languageVersion.set(JavaLanguageVersion.of(17)) + vendor.set(JvmVendorSpec.AZUL) + }) + } +} + +eclipse { + classpath { + downloadSources = true + downloadJavadoc = true + } +} + +final String modGroupPath = modGroup.toString().replace('.' as char, '/' as char) +final String apiPackagePath = apiPackage.toString().replace('.' as char, '/' as char) + +String targetPackageJava = javaSourceDir + modGroupPath +String targetPackageScala = scalaSourceDir + modGroupPath +String targetPackageKotlin = kotlinSourceDir + modGroupPath +if (!(getFile(targetPackageJava).exists() || getFile(targetPackageScala).exists() || getFile(targetPackageKotlin).exists())) { + throw new GradleException("Could not resolve \"modGroup\"! Could not find " + targetPackageJava + " or " + targetPackageScala + " or " + targetPackageKotlin) +} + +if (apiPackage) { + targetPackageJava = javaSourceDir + modGroupPath + "/" + apiPackagePath + targetPackageScala = scalaSourceDir + modGroupPath + "/" + apiPackagePath + targetPackageKotlin = kotlinSourceDir + modGroupPath + "/" + apiPackagePath + if (!(getFile(targetPackageJava).exists() || getFile(targetPackageScala).exists() || getFile(targetPackageKotlin).exists())) { + throw new GradleException("Could not resolve \"apiPackage\"! Could not find " + targetPackageJava + " or " + targetPackageScala + " or " + targetPackageKotlin) + } +} + +if (accessTransformersFile) { + for (atFile in accessTransformersFile.split(",")) { + String targetFile = "src/main/resources/META-INF/" + atFile.trim() + if (!getFile(targetFile).exists()) { + throw new GradleException("Could not resolve \"accessTransformersFile\"! Could not find " + targetFile) + } + tasks.deobfuscateMergedJarToSrg.accessTransformerFiles.from(targetFile) + tasks.srgifyBinpatchedJar.accessTransformerFiles.from(targetFile) + } +} else { + boolean atsFound = false + for (File at : sourceSets.getByName("main").resources.files) { + if (at.name.toLowerCase().endsWith("_at.cfg")) { + atsFound = true + tasks.deobfuscateMergedJarToSrg.accessTransformerFiles.from(at) + tasks.srgifyBinpatchedJar.accessTransformerFiles.from(at) + } + } + for (File at : sourceSets.getByName("api").resources.files) { + if (at.name.toLowerCase().endsWith("_at.cfg")) { + atsFound = true + tasks.deobfuscateMergedJarToSrg.accessTransformerFiles.from(at) + tasks.srgifyBinpatchedJar.accessTransformerFiles.from(at) + } + } + if (atsFound) { + logger.warn("Found and added access transformers in the resources folder, please configure gradle.properties to explicitly mention them by name") } } -apply plugin: 'forge' +if (usesMixins.toBoolean()) { + if (mixinsPackage.isEmpty()) { + throw new GradleException("\"usesMixins\" requires \"mixinsPackage\" to be set!") + } + final String mixinPackagePath = mixinsPackage.toString().replaceAll("\\.", "/") + final String mixinPluginPath = mixinPlugin.toString().replaceAll("\\.", "/") + + targetPackageJava = javaSourceDir + modGroupPath + "/" + mixinPackagePath + targetPackageScala = scalaSourceDir + modGroupPath + "/" + mixinPackagePath + targetPackageKotlin = kotlinSourceDir + modGroupPath + "/" + mixinPackagePath + if (!(getFile(targetPackageJava).exists() || getFile(targetPackageScala).exists() || getFile(targetPackageKotlin).exists())) { + throw new GradleException("Could not resolve \"mixinsPackage\"! Could not find " + targetPackageJava + " or " + targetPackageScala + " or " + targetPackageKotlin) + } + + if (!mixinPlugin.isEmpty()) { + String targetFileJava = javaSourceDir + modGroupPath + "/" + mixinPluginPath + ".java" + String targetFileScala = scalaSourceDir + modGroupPath + "/" + mixinPluginPath + ".scala" + String targetFileScalaJava = scalaSourceDir + modGroupPath + "/" + mixinPluginPath + ".java" + String targetFileKotlin = kotlinSourceDir + modGroupPath + "/" + mixinPluginPath + ".kt" + if (!(getFile(targetFileJava).exists() || getFile(targetFileScala).exists() || getFile(targetFileScalaJava).exists() || getFile(targetFileKotlin).exists())) { + throw new GradleException("Could not resolve \"mixinPlugin\"! Could not find " + targetFileJava + " or " + targetFileScala + " or " + targetFileScalaJava + " or " + targetFileKotlin) + } + } +} + +if (coreModClass) { + final String coreModPath = coreModClass.toString().replaceAll("\\.", "/") + String targetFileJava = javaSourceDir + modGroupPath + "/" + coreModPath + ".java" + String targetFileScala = scalaSourceDir + modGroupPath + "/" + coreModPath + ".scala" + String targetFileScalaJava = scalaSourceDir + modGroupPath + "/" + coreModPath + ".java" + String targetFileKotlin = kotlinSourceDir + modGroupPath + "/" + coreModPath + ".kt" + if (!(getFile(targetFileJava).exists() || getFile(targetFileScala).exists() || getFile(targetFileScalaJava).exists() || getFile(targetFileKotlin).exists())) { + throw new GradleException("Could not resolve \"coreModClass\"! Could not find " + targetFileJava + " or " + targetFileScala + " or " + targetFileScalaJava + " or " + targetFileKotlin) + } +} + +configurations.configureEach { + resolutionStrategy.cacheChangingModulesFor(0, TimeUnit.SECONDS) + + // Make sure GregTech build won't time out + System.setProperty("org.gradle.internal.http.connectionTimeout", 120000 as String) + System.setProperty("org.gradle.internal.http.socketTimeout", 120000 as String) +} + +// Fix Jenkins' Git: chmod a file should not be detected as a change and append a '.dirty' to the version +try { + 'git config core.fileMode false'.execute() +} +catch (Exception ignored) { + out.style(Style.Failure).println("git isn't installed at all") +} + +// Pulls version first from the VERSION env and then git tag +String identifiedVersion +String versionOverride = System.getenv("VERSION") ?: null +try { + // Produce a version based on the tag, or for branches something like 0.2.2-configurable-maven-and-extras.38+43090270b6-dirty + if (versionOverride == null) { + def gitDetails = versionDetails() + def isDirty = gitVersion().endsWith(".dirty") // No public API for this, isCleanTag has a different meaning + String branchName = gitDetails.branchName ?: (System.getenv('GIT_BRANCH') ?: 'git') + if (branchName.startsWith('origin/')) { + branchName = branchName.minus('origin/') + } + branchName = branchName.replaceAll("[^a-zA-Z0-9-]+", "-") // sanitize branch names for semver + identifiedVersion = gitDetails.lastTag ?: '${gitDetails.gitHash}' + if (gitDetails.commitDistance > 0) { + identifiedVersion += "-${branchName}.${gitDetails.commitDistance}+${gitDetails.gitHash}" + if (isDirty) { + identifiedVersion += "-dirty" + } + } else if (isDirty) { + identifiedVersion += "-${branchName}+${gitDetails.gitHash}-dirty" + } + } else { + identifiedVersion = versionOverride + } +} +catch (Exception ignored) { + out.style(Style.Failure).text( + 'This mod must be version controlled by Git AND the repository must provide at least one tag,\n' + + 'or the VERSION override must be set! ').style(Style.SuccessHeader).text('(Do NOT download from GitHub using the ZIP option, instead\n' + + 'clone the repository, see ').style(Style.Info).text('https://gtnh.miraheze.org/wiki/Development').style(Style.SuccessHeader).println(' for details.)' + ) + versionOverride = 'NO-GIT-TAG-SET' + identifiedVersion = versionOverride +} +version = identifiedVersion +ext { + modVersion = identifiedVersion +} + +if (identifiedVersion == versionOverride) { + out.style(Style.Failure).text('Override version to ').style(Style.Identifier).text(modVersion).style(Style.Failure).println('!\7') +} + +group = "com.github.GTNewHorizons" +if (project.hasProperty("customArchiveBaseName") && customArchiveBaseName) { + base { + archivesName = customArchiveBaseName + } +} else { + base { + archivesName = modId + } +} -version = "1.0" // Change this when BeefCore begins being built by itself -group= "erogenousbeef" // http://maven.apache.org/guides/mini/guide-naming-conventions.html -archivesBaseName = "BeefCore" minecraft { - version = "1.7.10-10.13.0.1180" - runDir = "run" + if (replaceGradleTokenInFile) { + for (f in replaceGradleTokenInFile.split(',')) { + tagReplacementFiles.add f + } + } + if (gradleTokenModId) { + injectedTags.put gradleTokenModId, modId + } + if (gradleTokenModName) { + injectedTags.put gradleTokenModName, modName + } + if (gradleTokenVersion) { + injectedTags.put gradleTokenVersion, modVersion + } + if (gradleTokenGroupName) { + injectedTags.put gradleTokenGroupName, modGroup + } + if (enableGenericInjection.toBoolean()) { + injectMissingGenerics.set(true) + } + + username = developmentEnvironmentUserName.toString() + + lwjgl3Version = "3.3.2" + + // Enable assertions in the current mod + extraRunJvmArguments.add("-ea:${modGroup}") + + if (usesMixins.toBoolean() || forceEnableMixins.toBoolean()) { + if (usesMixinDebug.toBoolean()) { + extraRunJvmArguments.addAll([ + "-Dmixin.debug.countInjections=true", + "-Dmixin.debug.verbose=true", + "-Dmixin.debug.export=true" + ]) + } + } + + // Blowdryer is present in some old mod builds, do not propagate it further as a dependency + // IC2 has no reobf jars in its Maven + groupsToExcludeFromAutoReobfMapping.addAll(["com.diffplug", "com.diffplug.durian", "net.industrial-craft"]) +} + +if (generateGradleTokenClass) { + tasks.injectTags.outputClassName.set(generateGradleTokenClass) +} + +// Custom reobf auto-mappings +configurations.configureEach { + dependencies.configureEach { dep -> + if (dep instanceof org.gradle.api.artifacts.ExternalModuleDependency) { + if (dep.group == "net.industrial-craft" && dep.name == "industrialcraft-2") { + // https://www.curseforge.com/minecraft/mc-mods/industrial-craft/files/2353971 + project.dependencies.reobfJarConfiguration("curse.maven:ic2-242638:2353971") + } + } + } + def obfuscationAttr = it.attributes.getAttribute(ObfuscationAttribute.OBFUSCATION_ATTRIBUTE) + if (obfuscationAttr != null && obfuscationAttr.name == ObfuscationAttribute.SRG) { + resolutionStrategy.eachDependency { DependencyResolveDetails details -> + // Remap CoFH core cursemaven dev jar to the obfuscated version for runObfClient/Server + if (details.requested.group == 'curse.maven' && details.requested.name.endsWith('-69162') && details.requested.version == '2388751') { + details.useVersion '2388750' + details.because 'Pick obfuscated jar' + } + } + } +} + +// Ensure tests have access to minecraft classes +sourceSets { + test { + java { + compileClasspath += sourceSets.patchedMc.output + sourceSets.mcLauncher.output + runtimeClasspath += sourceSets.patchedMc.output + sourceSets.mcLauncher.output + } + } +} + +if (file('addon.gradle.kts').exists()) { + apply from: 'addon.gradle.kts' +} else if (file('addon.gradle').exists()) { + apply from: 'addon.gradle' +} + +// File for local tweaks not commited to Git +if (file('addon.local.gradle.kts').exists()) { + apply from: 'addon.local.gradle.kts' +} else if (file('addon.local.gradle').exists()) { + apply from: 'addon.local.gradle' +} + +// Allow unsafe repos but warn +repositories.configureEach { repo -> + if (repo instanceof org.gradle.api.artifacts.repositories.UrlArtifactRepository) { + if (repo.getUrl() != null && repo.getUrl().getScheme() == "http" && !repo.allowInsecureProtocol) { + logger.warn("Deprecated: Allowing insecure connections for repo '${repo.name}' - add 'allowInsecureProtocol = true'") + repo.allowInsecureProtocol = true + } + } +} + +if (file('repositories.gradle.kts').exists()) { + apply from: 'repositories.gradle.kts' +} else if (file('repositories.gradle').exists()) { + apply from: 'repositories.gradle' +} else { + logger.error("Neither repositories.gradle.kts nor repositories.gradle was found, make sure you extracted the full ExampleMod template.") + throw new RuntimeException("Missing repositories.gradle[.kts]") +} + +configurations { + runtimeClasspath.extendsFrom(runtimeOnlyNonPublishable) + testRuntimeClasspath.extendsFrom(runtimeOnlyNonPublishable) + for (config in [compileClasspath, runtimeClasspath, testCompileClasspath, testRuntimeClasspath]) { + if (usesShadowedDependencies.toBoolean()) { + config.extendsFrom(shadowImplementation) + // TODO: remove Compile after all uses are refactored to Implementation + config.extendsFrom(shadeCompile) + config.extendsFrom(shadowCompile) + } + } + // A "bag-of-dependencies"-style configuration for backwards compatibility, gets put in "api" + create("compile") { + description = "Deprecated: use api or implementation instead, gets put in api" + canBeConsumed = false + canBeResolved = false + visible = false + } + create("testCompile") { + description = "Deprecated: use testImplementation instead" + canBeConsumed = false + canBeResolved = false + visible = false + } + api.extendsFrom(compile) + testImplementation.extendsFrom(testCompile) +} + +afterEvaluate { + if (!configurations.compile.allDependencies.empty || !configurations.testCompile.allDependencies.empty) { + logger.warn("This project uses deprecated `compile` dependencies, please migrate to using `api` and `implementation`") + logger.warn("For more details, see https://github.com/GTNewHorizons/ExampleMod1.7.10/blob/master/dependencies.gradle") + } +} + +repositories { + maven { + name 'Overmind forge repo mirror' + url 'https://gregtech.overminddl1.com/' + } + maven { + name = "GTNH Maven" + url = "http://jenkins.usrv.eu:8081/nexus/content/groups/public/" + allowInsecureProtocol = true + } + maven { + name 'sonatype' + url 'https://oss.sonatype.org/content/repositories/snapshots/' + content { + includeGroup "org.lwjgl" + } + } + if (includeWellKnownRepositories.toBoolean()) { + exclusiveContent { + forRepository { + maven { + name "CurseMaven" + url "https://cursemaven.com" + } + } + filter { + includeGroup "curse.maven" + } + } + exclusiveContent { + forRepository { + maven { + name = "Modrinth" + url = "https://api.modrinth.com/maven" + } + } + filter { + includeGroup "maven.modrinth" + } + } + maven { + name = "ic2" + url = getURL("https://maven.ic2.player.to/", "https://maven2.ic2.player.to/") + content { + includeGroup "net.industrial-craft" + } + metadataSources { + mavenPom() + artifact() + } + } + maven { + name "MMD Maven" + url "https://maven.mcmoddev.com/" + } + } +} + +def mixinProviderGroup = "io.github.legacymoddingmc" +def mixinProviderModule = "unimixins" +def mixinProviderVersion = "0.1.7.1" +def mixinProviderSpecNoClassifer = "${mixinProviderGroup}:${mixinProviderModule}:${mixinProviderVersion}" +def mixinProviderSpec = "${mixinProviderSpecNoClassifer}:dev" +ext.mixinProviderSpec = mixinProviderSpec + +def mixingConfigRefMap = 'mixins.' + modId + '.refmap.json' + +dependencies { + if (usesMixins.toBoolean()) { + annotationProcessor('org.ow2.asm:asm-debug-all:5.0.3') + annotationProcessor('com.google.guava:guava:24.1.1-jre') + annotationProcessor('com.google.code.gson:gson:2.8.6') + annotationProcessor(mixinProviderSpec) + if (usesMixinDebug.toBoolean()) { + runtimeOnlyNonPublishable('org.jetbrains:intellij-fernflower:1.2.1.16') + } + } + if (usesMixins.toBoolean()) { + implementation(modUtils.enableMixins(mixinProviderSpec, mixingConfigRefMap)) + } else if (forceEnableMixins.toBoolean()) { + runtimeOnlyNonPublishable(mixinProviderSpec) + } +} + +pluginManager.withPlugin('org.jetbrains.kotlin.kapt') { + if (usesMixins.toBoolean()) { + dependencies { + kapt(mixinProviderSpec) + } + } +} + +// Replace old mixin mods with unimixins +// https://docs.gradle.org/8.0.2/userguide/resolution_rules.html#sec:substitution_with_classifier +configurations.all { + resolutionStrategy.dependencySubstitution { + substitute module('com.gtnewhorizon:gtnhmixins') using module(mixinProviderSpecNoClassifer) withClassifier("dev") because("Unimixins replaces other mixin mods") + substitute module('com.github.GTNewHorizons:Mixingasm') using module(mixinProviderSpecNoClassifer) withClassifier("dev") because("Unimixins replaces other mixin mods") + substitute module('com.github.GTNewHorizons:SpongePoweredMixin') using module(mixinProviderSpecNoClassifer) withClassifier("dev") because("Unimixins replaces other mixin mods") + substitute module('com.github.GTNewHorizons:SpongeMixins') using module(mixinProviderSpecNoClassifer) withClassifier("dev") because("Unimixins replaces other mixin mods") + substitute module('io.github.legacymoddingmc:unimixins') using module(mixinProviderSpecNoClassifer) withClassifier("dev") because("Our previous unimixins upload was missing the dev classifier") + } } dependencies { - // you may put jars on which you depend on in ./libs - // or you may define them like so.. - //compile "some.group:artifact:version:classifier" - //compile "some.group:artifact:version" - - // real examples - //compile 'com.mod-buildcraft:buildcraft:6.0.8:dev' // adds buildcraft to the dev env - //compile 'com.googlecode.efficient-java-matrix-library:ejml:0.24' // adds ejml to the dev env + constraints { + def minGtnhLibVersion = "0.0.13" + implementation("com.github.GTNewHorizons:GTNHLib:${minGtnhLibVersion}") { + because("fixes duplicate mod errors in java 17 configurations using old gtnhlib") + } + runtimeOnly("com.github.GTNewHorizons:GTNHLib:${minGtnhLibVersion}") { + because("fixes duplicate mod errors in java 17 configurations using old gtnhlib") + } + devOnlyNonPublishable("com.github.GTNewHorizons:GTNHLib:${minGtnhLibVersion}") { + because("fixes duplicate mod errors in java 17 configurations using old gtnhlib") + } + runtimeOnlyNonPublishable("com.github.GTNewHorizons:GTNHLib:${minGtnhLibVersion}") { + because("fixes duplicate mod errors in java 17 configurations using old gtnhlib") + } + } +} - // for more info... - // http://www.gradle.org/docs/current/userguide/artifact_dependencies_tutorial.html - // http://www.gradle.org/docs/current/userguide/dependency_management.html +if (file('dependencies.gradle.kts').exists()) { + apply from: 'dependencies.gradle.kts' +} else if (file('dependencies.gradle').exists()) { + apply from: 'dependencies.gradle' +} else { + logger.error("Neither dependencies.gradle.kts nor dependencies.gradle was found, make sure you extracted the full ExampleMod template.") + throw new RuntimeException("Missing dependencies.gradle[.kts]") +} +tasks.register('generateAssets') { + group = "GTNH Buildscript" + description = "Generates a mixin config file at /src/main/resources/mixins.modid.json if needed" + onlyIf { usesMixins.toBoolean() } + doLast { + def mixinConfigFile = getFile("/src/main/resources/mixins." + modId + ".json") + if (!mixinConfigFile.exists()) { + def mixinPluginLine = "" + if (!mixinPlugin.isEmpty()) { + // We might not have a mixin plugin if we're using early/late mixins + mixinPluginLine += """\n "plugin": "${modGroup}.${mixinPlugin}", """ + } + + mixinConfigFile.text = """{ + "required": true, + "minVersion": "0.8.5-GTNH", + "package": "${modGroup}.${mixinsPackage}",${mixinPluginLine} + "refmap": "${mixingConfigRefMap}", + "target": "@env(DEFAULT)", + "compatibilityLevel": "JAVA_8", + "mixins": [], + "client": [], + "server": [] +} +""" + } + } +} + +if (usesMixins.toBoolean()) { + tasks.named("processResources").configure { + dependsOn("generateAssets") + } + + tasks.named("compileJava", JavaCompile).configure { + options.compilerArgs += [ + // Elan: from what I understand they are just some linter configs so you get some warning on how to properly code + "-XDenableSunApiLintControl", + "-XDignore.symbol.file" + ] + } } -processResources -{ +tasks.named("processResources", ProcessResources).configure { // this will ensure that this task is redone when the versions change. inputs.property "version", project.version - inputs.property "mcversion", project.minecraft.version + inputs.property "mcversion", project.minecraft.mcVersion + exclude("spotless.gradle") - // replace stuff in mcmod.info, nothing else - from(sourceSets.main.resources.srcDirs) { - include 'mcmod.info' - - // replace version and mcversion - expand 'version':project.version, 'mcversion':project.minecraft.version + // replace stuff in mcmod.info, nothing else. replaces ${key} with value in text + filesMatching("mcmod.info") { + expand "minecraftVersion": project.minecraft.mcVersion, + "modVersion": modVersion, + "modId": modId, + "modName": modName + } + + if (usesMixins.toBoolean()) { + dependsOn("compileJava", "compileScala") + } +} + +ext.java17Toolchain = (JavaToolchainSpec spec) -> { + spec.languageVersion.set(JavaLanguageVersion.of(17)) + spec.vendor.set(JvmVendorSpec.matching("jetbrains")) +} + +ext.java17DependenciesCfg = configurations.create("java17Dependencies") { + extendsFrom(configurations.getByName("runtimeClasspath")) // Ensure consistent transitive dependency resolution + canBeConsumed = false +} +ext.java17PatchDependenciesCfg = configurations.create("java17PatchDependencies") { + canBeConsumed = false +} + +dependencies { + def lwjgl3ifyVersion = '1.5.0' + if (modId != 'lwjgl3ify') { + java17Dependencies("com.github.GTNewHorizons:lwjgl3ify:${lwjgl3ifyVersion}") + } + if (modId != 'hodgepodge') { + java17Dependencies('com.github.GTNewHorizons:Hodgepodge:2.3.7') } - - // copy everything else, thats not the mcmod.info + + java17PatchDependencies("com.github.GTNewHorizons:lwjgl3ify:${lwjgl3ifyVersion}:forgePatches") {transitive = false} +} + +ext.java17JvmArgs = [ + // Java 9+ support + "--illegal-access=warn", + "-Djava.security.manager=allow", + "-Dfile.encoding=UTF-8", + "--add-opens", "java.base/jdk.internal.loader=ALL-UNNAMED", + "--add-opens", "java.base/java.net=ALL-UNNAMED", + "--add-opens", "java.base/java.nio=ALL-UNNAMED", + "--add-opens", "java.base/java.io=ALL-UNNAMED", + "--add-opens", "java.base/java.lang=ALL-UNNAMED", + "--add-opens", "java.base/java.lang.reflect=ALL-UNNAMED", + "--add-opens", "java.base/java.text=ALL-UNNAMED", + "--add-opens", "java.base/java.util=ALL-UNNAMED", + "--add-opens", "java.base/jdk.internal.reflect=ALL-UNNAMED", + "--add-opens", "java.base/sun.nio.ch=ALL-UNNAMED", + "--add-opens", "jdk.naming.dns/com.sun.jndi.dns=ALL-UNNAMED,java.naming", + "--add-opens", "java.desktop/sun.awt.image=ALL-UNNAMED", + "--add-modules", "jdk.dynalink", + "--add-opens", "jdk.dynalink/jdk.dynalink.beans=ALL-UNNAMED", + "--add-modules", "java.sql.rowset", + "--add-opens", "java.sql.rowset/javax.sql.rowset.serial=ALL-UNNAMED" +] + +ext.hotswapJvmArgs = [ + // DCEVM advanced hot reload + "-XX:+AllowEnhancedClassRedefinition", + "-XX:HotswapAgent=fatjar" +] + +ext.setupHotswapAgentTask = tasks.register("setupHotswapAgent") { + group = "GTNH Buildscript" + description = "Installs a recent version of HotSwapAgent into the Java 17 JetBrains runtime directory" + def hsaUrl = 'https://github.com/HotswapProjects/HotswapAgent/releases/download/1.4.2-SNAPSHOT/hotswap-agent-1.4.2-SNAPSHOT.jar' + def targetFolderProvider = javaToolchains.launcherFor(java17Toolchain).map {it.metadata.installationPath.dir("lib/hotswap")} + def targetFilename = "hotswap-agent.jar" + onlyIf { + !targetFolderProvider.get().file(targetFilename).asFile.exists() + } + doLast { + def targetFolder = targetFolderProvider.get() + targetFolder.asFile.mkdirs() + download.run { + src hsaUrl + dest targetFolder.file(targetFilename).asFile + overwrite false + tempAndMove true + } + } +} + +public abstract class RunHotswappableMinecraftTask extends RunMinecraftTask { + // IntelliJ doesn't seem to allow commandline arguments so we also support an env variable + private boolean enableHotswap = Boolean.valueOf(System.getenv("HOTSWAP")); + + @Input + public boolean getEnableHotswap() { return enableHotswap } + @Option(option = "hotswap", description = "Enables HotSwapAgent for enhanced class reloading under a debugger") + public boolean setEnableHotswap(boolean enable) { enableHotswap = enable } + + @Inject + public RunHotswappableMinecraftTask(Distribution side, String superTask, org.gradle.api.invocation.Gradle gradle) { + super(side, gradle) + + this.lwjglVersion = 3 + this.javaLauncher = project.javaToolchains.launcherFor(project.java17Toolchain) + this.extraJvmArgs.addAll(project.java17JvmArgs) + this.extraJvmArgs.addAll(project.provider(() -> enableHotswap ? project.hotswapJvmArgs : [])) + + this.classpath(project.java17PatchDependenciesCfg) + if (side == Distribution.CLIENT) { + this.classpath(project.minecraftTasks.lwjgl3Configuration) + } + // Use a raw provider instead of map to not create a dependency on the task + this.classpath(project.provider(() -> project.tasks.named(superTask, RunMinecraftTask).get().classpath)) + this.classpath.filter { file -> + !file.path.contains("2.9.4-nightly-20150209") // Remove lwjgl2 + } + this.classpath(project.java17DependenciesCfg) + } + + public void setup(Project project) { + super.setup(project) + if (project.usesMixins.toBoolean()) { + this.extraJvmArgs.addAll(project.provider(() -> { + def mixinCfg = project.configurations.detachedConfiguration(project.dependencies.create(project.mixinProviderSpec)) + mixinCfg.canBeConsumed = false + mixinCfg.transitive = false + enableHotswap ? ["-javaagent:" + mixinCfg.singleFile.absolutePath] : [] + })) + } + } +} + +def runClient17Task = tasks.register("runClient17", RunHotswappableMinecraftTask, Distribution.CLIENT, "runClient") +runClient17Task.configure { + setup(project) + group = "Modded Minecraft" + description = "Runs the modded client using Java 17, lwjgl3ify and Hodgepodge" + dependsOn(setupHotswapAgentTask, mcpTasks.launcherSources.classesTaskName, minecraftTasks.taskDownloadVanillaAssets, mcpTasks.taskPackagePatchedMc, 'jar') + mainClass = "GradleStart" + username = minecraft.username + userUUID = minecraft.userUUID +} + +def runServer17Task = tasks.register("runServer17", RunHotswappableMinecraftTask, Distribution.DEDICATED_SERVER, "runServer") +runServer17Task.configure { + setup(project) + group = "Modded Minecraft" + description = "Runs the modded server using Java 17, lwjgl3ify and Hodgepodge" + dependsOn(setupHotswapAgentTask, mcpTasks.launcherSources.classesTaskName, minecraftTasks.taskDownloadVanillaAssets, mcpTasks.taskPackagePatchedMc, 'jar') + mainClass = "GradleStartServer" + extraArgs.add("nogui") +} + +def getManifestAttributes() { + def manifestAttributes = [:] + if (!containsMixinsAndOrCoreModOnly.toBoolean() && (usesMixins.toBoolean() || coreModClass)) { + manifestAttributes += ["FMLCorePluginContainsFMLMod": true] + } + + if (accessTransformersFile) { + manifestAttributes += ["FMLAT": accessTransformersFile.toString()] + } + + if (coreModClass) { + manifestAttributes += ["FMLCorePlugin": modGroup + "." + coreModClass] + } + + if (usesMixins.toBoolean()) { + manifestAttributes += [ + "TweakClass" : "org.spongepowered.asm.launch.MixinTweaker", + "MixinConfigs" : "mixins." + modId + ".json", + "ForceLoadAsMod": !containsMixinsAndOrCoreModOnly.toBoolean() + ] + } + return manifestAttributes +} + +tasks.named("jar", Jar).configure { + manifest { + attributes(getManifestAttributes()) + } +} + +if (usesShadowedDependencies.toBoolean()) { + tasks.named("shadowJar", ShadowJar).configure { + manifest { + attributes(getManifestAttributes()) + } + + if (minimizeShadowedDependencies.toBoolean()) { + minimize() // This will only allow shading for actually used classes + } + configurations = [ + project.configurations.shadowImplementation, + project.configurations.shadowCompile, + project.configurations.shadeCompile + ] + archiveClassifier.set('dev') + if (relocateShadowedDependencies.toBoolean()) { + relocationPrefix = modGroup + ".shadow" + enableRelocation = true + } + } + configurations.runtimeElements.outgoing.artifacts.clear() + configurations.apiElements.outgoing.artifacts.clear() + configurations.runtimeElements.outgoing.artifact(tasks.named("shadowJar", ShadowJar)) + configurations.apiElements.outgoing.artifact(tasks.named("shadowJar", ShadowJar)) + tasks.named("jar", Jar) { + enabled = false + finalizedBy(tasks.shadowJar) + } + tasks.named("reobfJar", ReobfuscatedJar) { + inputJar.set(tasks.named("shadowJar", ShadowJar).flatMap({it.archiveFile})) + } + AdhocComponentWithVariants javaComponent = (AdhocComponentWithVariants) project.components.findByName("java") + javaComponent.withVariantsFromConfiguration(configurations.shadowRuntimeElements) { + skip() + } + for (runTask in ["runClient", "runServer", "runClient17", "runServer17"]) { + tasks.named(runTask).configure { + dependsOn("shadowJar") + } + } +} +ext.publishableDevJar = usesShadowedDependencies.toBoolean() ? tasks.shadowJar : tasks.jar +ext.publishableObfJar = tasks.reobfJar + +tasks.register('apiJar', Jar) { + from(sourceSets.main.allSource) { + include modGroupPath + "/" + apiPackagePath + '/**' + } + + from(sourceSets.main.output) { + include modGroupPath + "/" + apiPackagePath + '/**' + } + from(sourceSets.main.resources.srcDirs) { - exclude 'mcmod.info' + include("LICENSE") + } + + getArchiveClassifier().set('api') +} + +artifacts { + if (!noPublishedSources) { + archives tasks.named("sourcesJar") + } + if (apiPackage) { + archives tasks.named("apiJar") + } +} + +idea { + module { + downloadJavadoc = true + downloadSources = true + inheritOutputDirs = true + } + project { + settings { + if (ideaOverrideBuildType != "") { + delegateActions { + if ("gradle".equalsIgnoreCase(ideaOverrideBuildType)) { + delegateBuildRunToGradle = true + testRunner = org.jetbrains.gradle.ext.ActionDelegationConfig.TestRunner.GRADLE + } else if ("idea".equalsIgnoreCase(ideaOverrideBuildType)) { + delegateBuildRunToGradle = false + testRunner = org.jetbrains.gradle.ext.ActionDelegationConfig.TestRunner.PLATFORM + } else { + throw GradleScriptException('Accepted value for ideaOverrideBuildType is one of gradle or idea.') + } + } + } + runConfigurations { + "0. Build and Test"(Gradle) { + taskNames = ["build"] + } + "1. Run Client"(Gradle) { + taskNames = ["runClient"] + } + "2. Run Server"(Gradle) { + taskNames = ["runServer"] + } + "1a. Run Client (Java 17)"(Gradle) { + taskNames = ["runClient17"] + } + "2a. Run Server (Java 17)"(Gradle) { + taskNames = ["runServer17"] + } + "1b. Run Client (Java 17, Hotswap)"(Gradle) { + taskNames = ["runClient17"] + envs = ["HOTSWAP": "true"] + } + "2b. Run Server (Java 17, Hotswap)"(Gradle) { + taskNames = ["runServer17"] + envs = ["HOTSWAP": "true"] + } + "3. Run Obfuscated Client"(Gradle) { + taskNames = ["runObfClient"] + } + "4. Run Obfuscated Server"(Gradle) { + taskNames = ["runObfServer"] + } + if (!disableSpotless) { + "5. Apply spotless"(Gradle) { + taskNames = ["spotlessApply"] + } + } + def coreModArgs = "" + if (coreModClass) { + coreModArgs = ' "-Dfml.coreMods.load=' + modGroup + '.' + coreModClass + '"' + } + "Run Client (IJ Native)"(Application) { + mainClass = "GradleStart" + moduleName = project.name + ".ideVirtualMain" + afterEvaluate { + workingDirectory = tasks.runClient.workingDir.absolutePath + programParameters = tasks.runClient.calculateArgs(project).collect { '"' + it + '"' }.join(' ') + jvmArgs = tasks.runClient.calculateJvmArgs(project).collect { '"' + it + '"' }.join(' ') + + ' ' + tasks.runClient.systemProperties.collect { '"-D' + it.key + '=' + it.value.toString() + '"' }.join(' ') + + coreModArgs + } + } + "Run Server (IJ Native)"(Application) { + mainClass = "GradleStartServer" + moduleName = project.name + ".ideVirtualMain" + afterEvaluate { + workingDirectory = tasks.runServer.workingDir.absolutePath + programParameters = tasks.runServer.calculateArgs(project).collect { '"' + it + '"' }.join(' ') + jvmArgs = tasks.runServer.calculateJvmArgs(project).collect { '"' + it + '"' }.join(' ') + + ' ' + tasks.runServer.systemProperties.collect { '"-D' + it.key + '=' + it.value.toString() + '"' }.join(' ') + + coreModArgs + } + } + } + compiler.javac { + afterEvaluate { + javacAdditionalOptions = "-encoding utf8" + moduleJavacAdditionalOptions = [ + (project.name + ".main"): tasks.compileJava.options.compilerArgs.collect { '"' + it + '"' }.join(' ') + ] + } + } + withIDEADir { File ideaDir -> + if (!ideaDir.path.contains(".idea")) { + // If an .ipr file exists, the project root directory is passed here instead of the .idea subdirectory + ideaDir = new File(ideaDir, ".idea") + } + if (ideaDir.isDirectory()) { + def miscFile = new File(ideaDir, "misc.xml") + if (miscFile.isFile()) { + boolean dirty = false + def miscTransformer = new XmlTransformer() + miscTransformer.addAction { root -> + Node rootNode = root.asNode() + def rootManager = rootNode + .component.find { it.@name == 'ProjectRootManager' } + if (!rootManager) { + rootManager = rootNode.appendNode('component', ['name': 'ProjectRootManager', 'version': '2']) + dirty = true + } + def output = rootManager.output + if (!output) { + output = rootManager.appendNode('output') + dirty = true + } + if (!output.@url) { + // Only modify the output url if it doesn't yet have one, or if the existing one is blank somehow. + // This is a sensible default for most setups + output.@url = 'file://$PROJECT_DIR$/build/ideaBuild' + dirty = true + } + } + def result = miscTransformer.transform(miscFile.text) + if (dirty) { + miscFile.write(result) + } + } else { + miscFile.text = """ + + + + + +""" + } + } + } + } + } +} + +tasks.named("processIdeaSettings").configure { + dependsOn("injectTags") +} + +tasks.named("ideVirtualMainClasses").configure { + // Make IntelliJ "Build project" build the mod jars + dependsOn("jar", "reobfJar") + if (!disableSpotless) { + dependsOn("spotlessCheck") + } +} + +// workaround variable hiding in pom processing +def projectConfigs = project.configurations + +publishing { + publications { + create("maven", MavenPublication) { + from components.java + + if (apiPackage) { + artifact apiJar + } + + groupId = System.getenv("ARTIFACT_GROUP_ID") ?: project.group + artifactId = System.getenv("ARTIFACT_ID") ?: project.name + // Using the identified version, not project.version as it has the prepended 1.7.10 + version = System.getenv("RELEASE_VERSION") ?: identifiedVersion + } + } + + repositories { + if (usesMavenPublishing.toBoolean()) { + maven { + url = mavenPublishUrl + allowInsecureProtocol = mavenPublishUrl.startsWith("http://") // Mostly for the GTNH maven + credentials { + username = System.getenv("MAVEN_USER") ?: "NONE" + password = System.getenv("MAVEN_PASSWORD") ?: "NONE" + } + } + } + } +} + +if (modrinthProjectId.size() != 0 && System.getenv("MODRINTH_TOKEN") != null) { + apply plugin: 'com.modrinth.minotaur' + + File changelogFile = new File(System.getenv("CHANGELOG_FILE") ?: "CHANGELOG.md") + + modrinth { + token = System.getenv("MODRINTH_TOKEN") + projectId = modrinthProjectId + versionNumber = identifiedVersion + versionType = identifiedVersion.endsWith("-pre") ? "beta" : "release" + changelog = changelogFile.exists() ? changelogFile.getText("UTF-8") : "" + uploadFile = publishableObfJar + additionalFiles = getSecondaryArtifacts() + gameVersions = [minecraftVersion] + loaders = ["forge"] + debugMode = false + } + + if (modrinthRelations.size() != 0) { + String[] deps = modrinthRelations.split(";") + deps.each { dep -> + if (dep.size() == 0) { + return + } + String[] parts = dep.split(":") + String[] qual = parts[0].split("-") + addModrinthDep(qual[0], qual[1], parts[1]) + } + } + if (usesMixins.toBoolean()) { + addModrinthDep("required", "project", "unimixins") + } + tasks.modrinth.dependsOn(build) + tasks.publish.dependsOn(tasks.modrinth) +} + +if (curseForgeProjectId.size() != 0 && System.getenv("CURSEFORGE_TOKEN") != null) { + apply plugin: 'com.matthewprenger.cursegradle' + + File changelogFile = new File(System.getenv("CHANGELOG_FILE") ?: "CHANGELOG.md") + + curseforge { + apiKey = System.getenv("CURSEFORGE_TOKEN") + project { + id = curseForgeProjectId + if (changelogFile.exists()) { + changelogType = "markdown" + changelog = changelogFile + } + releaseType = identifiedVersion.endsWith("-pre") ? "beta" : "release" + addGameVersion minecraftVersion + addGameVersion "Forge" + mainArtifact publishableObfJar + for (artifact in getSecondaryArtifacts()) addArtifact artifact + } + + options { + javaIntegration = false + forgeGradleIntegration = false + debug = false + } + } + + if (curseForgeRelations.size() != 0) { + String[] deps = curseForgeRelations.split(";") + deps.each { dep -> + if (dep.size() == 0) { + return + } + String[] parts = dep.split(":") + addCurseForgeRelation(parts[0], parts[1]) + } + } + if (usesMixins.toBoolean()) { + addCurseForgeRelation("requiredDependency", "unimixins") + } + tasks.curseforge.dependsOn(build) + tasks.publish.dependsOn(tasks.curseforge) +} + +def addModrinthDep(String scope, String type, String name) { + com.modrinth.minotaur.dependencies.Dependency dep; + if (!(scope in ["required", "optional", "incompatible", "embedded"])) { + throw new Exception("Invalid modrinth dependency scope: " + scope) + } + switch (type) { + case "project": + dep = new ModDependency(name, scope) + break + case "version": + dep = new VersionDependency(name, scope) + break + default: + throw new Exception("Invalid modrinth dependency type: " + type) } + project.modrinth.dependencies.add(dep) +} + +def addCurseForgeRelation(String type, String name) { + if (!(type in ["requiredDependency", "embeddedLibrary", "optionalDependency", "tool", "incompatible"])) { + throw new Exception("Invalid CurseForge relation type: " + type) + } + CurseArtifact artifact = project.curseforge.curseProjects[0].mainArtifact + CurseRelation rel = (artifact.curseRelations ?: (artifact.curseRelations = new CurseRelation())) + rel."$type"(name) +} + +// Updating + +def buildscriptGradleVersion = "8.2.1" + +tasks.named('wrapper', Wrapper).configure { + gradleVersion = buildscriptGradleVersion +} + +tasks.register('updateBuildScript') { + group = 'GTNH Buildscript' + description = 'Updates the build script to the latest version' + + if (gradle.gradleVersion != buildscriptGradleVersion && !Boolean.getBoolean('DISABLE_BUILDSCRIPT_GRADLE_UPDATE')) { + dependsOn('wrapper') + } + + doLast { + if (performBuildScriptUpdate()) return + + print("Build script already up-to-date!") + } +} + +if (!project.getGradle().startParameter.isOffline() && !Boolean.getBoolean('DISABLE_BUILDSCRIPT_UPDATE_CHECK') && isNewBuildScriptVersionAvailable()) { + if (autoUpdateBuildScript.toBoolean()) { + performBuildScriptUpdate() + } else { + out.style(Style.SuccessHeader).println("Build script update available! Run 'gradle updateBuildScript'") + if (gradle.gradleVersion != buildscriptGradleVersion) { + out.style(Style.SuccessHeader).println("updateBuildScript can update gradle from ${gradle.gradleVersion} to ${buildscriptGradleVersion}\n") + } + } +} + +// If you want to add more cases to this task, implement them as arguments if total amount to print gets too large +tasks.register('faq') { + group = 'GTNH Buildscript' + description = 'Prints frequently asked questions about building a project' + + doLast { + print("If your build fails to fetch dependencies, run './gradlew updateDependencies'. " + + "Or you can manually check if the versions are still on the distributing sites - " + + "the links can be found in repositories.gradle and build.gradle:repositories, " + + "but not build.gradle:buildscript.repositories - those ones are for gradle plugin metadata.\n\n" + + "If your build fails to recognize the syntax of new Java versions, enable Jabel in your " + + "gradle.properties. See how it's done in GTNH ExampleMod/gradle.properties. " + + "However, keep in mind that Jabel enables only syntax features, but not APIs that were introduced in " + + "Java 9 or later.") + } +} + +static URL availableBuildScriptUrl() { + new URL("https://raw.githubusercontent.com/GTNewHorizons/ExampleMod1.7.10/master/build.gradle") +} + +static URL exampleSettingsGradleUrl() { + new URL("https://raw.githubusercontent.com/GTNewHorizons/ExampleMod1.7.10/master/settings.gradle.example") +} + +static URL exampleGitAttributesUrl() { + new URL("https://raw.githubusercontent.com/GTNewHorizons/ExampleMod1.7.10/master/.gitattributes") +} + + +boolean verifyGitAttributes() { + def gitattributesFile = getFile(".gitattributes") + if (!gitattributesFile.exists()) { + println("Downloading default .gitattributes") + exampleGitAttributesUrl().withInputStream { i -> gitattributesFile.withOutputStream { it << i } } + exec { + workingDir '.' + commandLine 'git', 'add', '--renormalize', '.' + } + return true + } + return false +} + +boolean verifySettingsGradle() { + def settingsFile = getFile("settings.gradle") + if (!settingsFile.exists()) { + println("Downloading default settings.gradle") + exampleSettingsGradleUrl().withInputStream { i -> settingsFile.withOutputStream { it << i } } + return true + } + return false +} + +boolean performBuildScriptUpdate() { + if (isNewBuildScriptVersionAvailable()) { + def buildscriptFile = getFile("build.gradle") + availableBuildScriptUrl().withInputStream { i -> buildscriptFile.withOutputStream { it << i } } + def out = services.get(StyledTextOutputFactory).create('buildscript-update-output') + out.style(Style.Success).print("Build script updated. Please REIMPORT the project or RESTART your IDE!") + boolean settingsupdated = verifySettingsGradle() + settingsupdated = verifyGitAttributes() || settingsupdated + if (settingsupdated) + throw new GradleException("Settings has been updated, please re-run task.") + return true + } + return false +} + +boolean isNewBuildScriptVersionAvailable() { + Map parameters = ["connectTimeout": 2000, "readTimeout": 2000] + + String currentBuildScript = getFile("build.gradle").getText() + String currentBuildScriptHash = getVersionHash(currentBuildScript) + String availableBuildScriptHash + try { + String availableBuildScript = availableBuildScriptUrl().newInputStream(parameters).getText() + availableBuildScriptHash = getVersionHash(availableBuildScript) + } catch (IOException e) { + logger.warn("Could not check for buildscript update availability: {}", e.message) + return false + } + + boolean isUpToDate = currentBuildScriptHash.empty || availableBuildScriptHash.empty || currentBuildScriptHash == availableBuildScriptHash + return !isUpToDate +} + +static String getVersionHash(String buildScriptContent) { + String versionLine = buildScriptContent.find("^//version: [a-z0-9]*") + if (versionLine != null) { + return versionLine.split(": ").last() + } + return "" +} + +// Parameter Deobfuscation + +tasks.register('deobfParams') { + group = 'GTNH Buildscript' + description = 'Rename all obfuscated parameter names inherited from Minecraft classes' + doLast { // TODO + + String mcpDir = "$project.gradle.gradleUserHomeDir/caches/minecraft/de/oceanlabs/mcp/mcp_$channel/$mappingsVersion" + String mcpZIP = "$mcpDir/mcp_$channel-$mappingsVersion-${minecraftVersion}.zip" + String paramsCSV = "$mcpDir/params.csv" + + download.run { + src "https://maven.minecraftforge.net/de/oceanlabs/mcp/mcp_$channel/$mappingsVersion-$minecraftVersion/mcp_$channel-$mappingsVersion-${minecraftVersion}.zip" + dest mcpZIP + overwrite false + } + + if (!file(paramsCSV).exists()) { + println("Extracting MCP archive ...") + copy { + from(zipTree(mcpZIP)) + into(mcpDir) + } + } + + println("Parsing params.csv ...") + Map params = new HashMap<>() + Files.lines(Paths.get(paramsCSV)).forEach { line -> + String[] cells = line.split(",") + if (cells.length > 2 && cells[0].matches("p_i?\\d+_\\d+_")) { + params.put(cells[0], cells[1]) + } + } + + out.style(Style.Success).println("Modified ${replaceParams(file("$projectDir/src/main/java"), params)} files!") + out.style(Style.Failure).println("Don't forget to verify that the code still works as before!\n It could be broken due to duplicate variables existing now\n or parameters taking priority over other variables.") + } +} + +static int replaceParams(File file, Map params) { + int fileCount = 0 + + if (file.isDirectory()) { + for (File f : file.listFiles()) { + fileCount += replaceParams(f, params) + } + return fileCount + } + println("Visiting ${file.getName()} ...") + try { + String content = new String(Files.readAllBytes(file.toPath())) + int hash = content.hashCode() + params.forEach { key, value -> + content = content.replaceAll(key, value) + } + if (hash != content.hashCode()) { + Files.write(file.toPath(), content.getBytes("UTF-8")) + return 1 + } + } catch (Exception e) { + e.printStackTrace() + } + return 0 +} + +// Dependency Deobfuscation (Deprecated, use the new RFG API documented in dependencies.gradle) + +def deobf(String sourceURL) { + try { + URL url = new URL(sourceURL) + String fileName = url.getFile() + + //get rid of directories: + int lastSlash = fileName.lastIndexOf("/") + if (lastSlash > 0) { + fileName = fileName.substring(lastSlash + 1) + } + //get rid of extension: + if (fileName.endsWith(".jar") || fileName.endsWith(".litemod")) { + fileName = fileName.substring(0, fileName.lastIndexOf(".")) + } + + String hostName = url.getHost() + if (hostName.startsWith("www.")) { + hostName = hostName.substring(4) + } + List parts = Arrays.asList(hostName.split("\\.")) + Collections.reverse(parts) + hostName = String.join(".", parts) + + return deobf(sourceURL, "$hostName/$fileName") + } catch (Exception ignored) { + return deobf(sourceURL, "deobf/${sourceURL.hashCode()}") + } +} + +def deobfMaven(String repoURL, String mavenDep) { + if (!repoURL.endsWith("/")) { + repoURL += "/" + } + String[] parts = mavenDep.split(":") + parts[0] = parts[0].replace('.', '/') + def jarURL = repoURL + parts[0] + "/" + parts[1] + "/" + parts[2] + "/" + parts[1] + "-" + parts[2] + ".jar" + return deobf(jarURL) +} + +def deobfCurse(String curseDep) { + return dependencies.rfg.deobf("curse.maven:$curseDep") +} + +// The method above is to be preferred. Use this method if the filename is not at the end of the URL. +def deobf(String sourceURL, String rawFileName) { + String bon2Version = "2.5.1" + String fileName = URLDecoder.decode(rawFileName, "UTF-8") + String cacheDir = "$project.gradle.gradleUserHomeDir/caches" + String obfFile = "$cacheDir/modules-2/files-2.1/${fileName}.jar" + + download.run { + src sourceURL + dest obfFile + quiet true + overwrite false + } + return dependencies.rfg.deobf(files(obfFile)) +} +// Helper methods + +def checkPropertyExists(String propertyName) { + if (!project.hasProperty(propertyName)) { + throw new GradleException("This project requires a property \"" + propertyName + "\"! Please add it your \"gradle.properties\". You can find all properties and their description here: https://github.com/GTNewHorizons/ExampleMod1.7.10/blob/main/gradle.properties") + } +} + +def propertyDefaultIfUnset(String propertyName, defaultValue) { + if (!project.hasProperty(propertyName) || project.property(propertyName) == "") { + project.ext.setProperty(propertyName, defaultValue) + } +} + +def getFile(String relativePath) { + return new File(projectDir, relativePath) +} + +def getSecondaryArtifacts() { + // Because noPublishedSources from the beginning of the script is somehow not visible here... + boolean noPublishedSources = project.hasProperty("noPublishedSources") ? project.noPublishedSources.toBoolean() : false + def secondaryArtifacts = [publishableDevJar] + if (!noPublishedSources) secondaryArtifacts += [sourcesJar] + if (apiPackage) secondaryArtifacts += [apiJar] + return secondaryArtifacts +} + +def getURL(String main, String fallback) { + return pingURL(main, 10000) ? main : fallback +} + +// credit: https://stackoverflow.com/a/3584332 +def pingURL(String url, int timeout) { + url = url.replaceFirst("^https", "http") // Otherwise an exception may be thrown on invalid SSL certificates. + try { + HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection() + connection.setConnectTimeout(timeout) + connection.setReadTimeout(timeout) + connection.setRequestMethod("HEAD") + int responseCode = connection.getResponseCode() + return 200 <= responseCode && responseCode <= 399 + } catch (IOException ignored) { + return false + } +} + +// For easier scripting of things that require variables defined earlier in the buildscript +if (file('addon.late.gradle.kts').exists()) { + apply from: 'addon.late.gradle.kts' +} else if (file('addon.late.gradle').exists()) { + apply from: 'addon.late.gradle' +} + +// File for local tweaks not commited to Git +if (file('addon.late.local.gradle.kts').exists()) { + apply from: 'addon.late.local.gradle.kts' +} else if (file('addon.late.local.gradle').exists()) { + apply from: 'addon.late.local.gradle' } diff --git a/build.gradle.old b/build.gradle.old new file mode 100644 index 0000000..6dfd798 --- /dev/null +++ b/build.gradle.old @@ -0,0 +1,63 @@ +buildscript { + repositories { + mavenCentral() + maven { + name = "forge" + url = "http://files.minecraftforge.net/maven" + } + maven { + name = "sonatype" + url = "https://oss.sonatype.org/content/repositories/snapshots/" + } + } + dependencies { + classpath 'net.minecraftforge.gradle:ForgeGradle:1.2-SNAPSHOT' + } +} + +apply plugin: 'forge' + +version = "1.0" // Change this when BeefCore begins being built by itself +group= "erogenousbeef" // http://maven.apache.org/guides/mini/guide-naming-conventions.html +archivesBaseName = "BeefCore" + +minecraft { + version = "1.7.10-10.13.0.1180" + runDir = "run" +} + +dependencies { + // you may put jars on which you depend on in ./libs + // or you may define them like so.. + //compile "some.group:artifact:version:classifier" + //compile "some.group:artifact:version" + + // real examples + //compile 'com.mod-buildcraft:buildcraft:6.0.8:dev' // adds buildcraft to the dev env + //compile 'com.googlecode.efficient-java-matrix-library:ejml:0.24' // adds ejml to the dev env + + // for more info... + // http://www.gradle.org/docs/current/userguide/artifact_dependencies_tutorial.html + // http://www.gradle.org/docs/current/userguide/dependency_management.html + +} + +processResources +{ + // this will ensure that this task is redone when the versions change. + inputs.property "version", project.version + inputs.property "mcversion", project.minecraft.version + + // replace stuff in mcmod.info, nothing else + from(sourceSets.main.resources.srcDirs) { + include 'mcmod.info' + + // replace version and mcversion + expand 'version':project.version, 'mcversion':project.minecraft.version + } + + // copy everything else, thats not the mcmod.info + from(sourceSets.main.resources.srcDirs) { + exclude 'mcmod.info' + } +} diff --git a/dependencies.gradle b/dependencies.gradle new file mode 100644 index 0000000..4fab30b --- /dev/null +++ b/dependencies.gradle @@ -0,0 +1,36 @@ +/* + * Add your dependencies here. Supported configurations: + * - api("group:name:version:classifier"): if you use the types from this dependency in the public API of this mod + * Available at runtime and compiletime for mods depending on this mod + * - implementation("g:n:v:c"): if you need this for internal implementation details of the mod, but none of it is visible via the public API + * Available at runtime but not compiletime for mods depending on this mod + * - compileOnly("g:n:v:c"): if the mod you're building doesn't need this dependency during runtime at all, e.g. for optional mods + * Not available at all for mods depending on this mod, only visible at compiletime for this mod + * - compileOnlyApi("g:n:v:c"): like compileOnly, but also visible at compiletime for mods depending on this mod + * Available at compiletime but not runtime for mods depending on this mod + * - runtimeOnlyNonPublishable("g:n:v:c"): if you want to include a mod in this mod's runClient/runServer runs, but not publish it as a dependency + * Not available at all for mods depending on this mod, only visible at runtime for this mod + * - devOnlyNonPublishable("g:n:v:c"): a combination of runtimeOnlyNonPublishable and compileOnly for dependencies present at both compiletime and runtime, + * but not published as Maven dependencies - useful for RFG-deobfuscated dependencies or local testing + * - runtimeOnly("g:n:v:c"): if you don't need this at compile time, but want it to be present at runtime + * Available at runtime for mods depending on this mod + * - annotationProcessor("g:n:v:c"): mostly for java compiler plugins, if you know you need this, use it, otherwise don't worry + * - testCONFIG("g:n:v:c") - replace CONFIG by one of the above (except api), same as above but for the test sources instead of main + * + * - shadowImplementation("g:n:v:c"): effectively the same as API, but the dependency is included in your jar under a renamed package name + * Requires you to enable usesShadowedDependencies in gradle.properties + * + * - compile("g:n:v:c"): deprecated, replace with "api" (works like the old "compile") or "implementation" (can be more efficient) + * + * You can exclude transitive dependencies (dependencies of the chosen dependency) by appending { transitive = false } if needed, + * but use this sparingly as it can break using your mod as another mod's dependency if you're not careful. + * + * To depend on obfuscated jars you can use `devOnlyNonPublishable(rfg.deobf("dep:spec:1.2.3"))` to fetch an obfuscated jar from maven, + * or `devOnlyNonPublishable(rfg.deobf(project.files("libs/my-mod-jar.jar")))` to use a file. + * + * Gradle names for some of the configuration can be misleading, compileOnlyApi and runtimeOnly both get published as dependencies in Maven, but compileOnly does not. + * The buildscript adds runtimeOnlyNonPublishable to also have a runtime dependency that's not published. + * + * For more details, see https://docs.gradle.org/8.0.1/userguide/java_library_plugin.html#sec:java_library_configurations_graph + */ +dependencies {} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..4cd0ff9 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,152 @@ +modName = Beef Core + +# This is a case-sensitive string to identify your mod. Convention is to use lower case. +modId = beefcore + +modGroup = erogenousbeef.core + +# WHY is there no version field? +# The build script relies on git to provide a version via tags. It is super easy and will enable you to always know the +# code base or your binary. Check out this tutorial: https://blog.mattclemente.com/2017/10/13/versioning-with-git-tags/ + +# Will update your build.gradle automatically whenever an update is available +autoUpdateBuildScript = false + +minecraftVersion = 1.7.10 +forgeVersion = 10.13.4.1614 + +# Specify a MCP channel and mappings version for dependency deobfuscation and the deobfParams task. +channel = stable +mappingsVersion = 12 + +# Define other MCP mappings for dependency deobfuscation +remoteMappings = https://raw.githubusercontent.com/MinecraftForge/FML/1.7.10/conf/ + +# Select a username for testing your mod with breakpoints. You may leave this empty for a random username each time you +# restart Minecraft in development. Choose this dependent on your mod: +# Do you need consistent player progressing (for example Thaumcraft)? -> Select a name +# Do you need to test how your custom blocks interacts with a player that is not the owner? -> leave name empty +developmentEnvironmentUserName = Developer + +# Enables using modern java syntax (up to version 17) via Jabel, while still targeting JVM 8. +# See https://github.com/bsideup/jabel for details on how this works. +enableModernJavaSyntax = true + +# Enables injecting missing generics into the decompiled source code for a better coding experience +# Turns most publicly visible List, Map, etc. into proper List, Map types +enableGenericInjection = false + +# Generate a class with String fields for the mod id, name, version and group name named with the fields below +generateGradleTokenClass = erogenousbeef.core.Tags +gradleTokenModId = MODID +gradleTokenModName = MODNAME +gradleTokenVersion = VERSION +gradleTokenGroupName = GROUPNAME +# [DEPRECATED] +# Multiple source files can be defined here by providing a comma-seperated list: Class1.java,Class2.java,Class3.java +# public static final String VERSION = "GRADLETOKEN_VERSION"; +# The string's content will be replaced with your mod's version when compiled. You should use this to specify your mod's +# version in @Mod([...], version = VERSION, [...]) +# Leave these properties empty to skip individual token replacements +replaceGradleTokenInFile = + +# In case your mod provides an API for other mods to implement you may declare its package here. Otherwise, you can +# leave this property empty. +# Example value: apiPackage = api + modGroup = com.myname.mymodid -> com.myname.mymodid.api +apiPackage = + +# Specify the configuration file for Forge's access transformers here. It must be placed into /src/main/resources/META-INF/ +# There can be multiple files in a comma-separated list. +# Example value: mymodid_at.cfg,nei_at.cfg +accessTransformersFile = + +# Provides setup for Mixins if enabled. If you don't know what mixins are: Keep it disabled! +usesMixins = false +# Adds some debug arguments like verbose output and export +usesMixinDebug = false +# Specify the location of your implementation of IMixinConfigPlugin. Leave it empty otherwise. +mixinPlugin = +# Specify the package that contains all of your Mixins. You may only place Mixins in this package or the build will fail! +mixinsPackage = +# Specify the core mod entry class if you use a core mod. This class must implement IFMLLoadingPlugin! +# This parameter is for legacy compatibility only +# Example value: coreModClass = asm.FMLPlugin + modGroup = com.myname.mymodid -> com.myname.mymodid.asm.FMLPlugin +coreModClass = +# If your project is only a consolidation of mixins or a core mod and does NOT contain a 'normal' mod ( = some class +# that is annotated with @Mod) you want this to be true. When in doubt: leave it on false! +containsMixinsAndOrCoreModOnly = false + +# Enables Mixins even if this mod doesn't use them, useful if one of the dependencies uses mixins. +forceEnableMixins = false + +# If enabled, you may use 'shadowCompile' for dependencies. They will be integrated in your jar. It is your +# responsibility check the licence and request permission for distribution, if required. +usesShadowedDependencies = false +# If disabled, won't remove unused classes from shaded dependencies. Some libraries use reflection to access +# their own classes, making the minimization unreliable. +minimizeShadowedDependencies = true +# If disabled, won't rename the shadowed classes. +relocateShadowedDependencies = true + +# Adds the GTNH maven, CurseMaven, IC2/Player maven, and some more well-known 1.7.10 repositories +includeWellKnownRepositories = true + +# Change these to your Maven coordinates if you want to publish to a custom Maven repository instead of the default GTNH Maven. +# Authenticate with the MAVEN_USERNAME and MAVEN_PASSWORD environment variables. +# If you need a more complex setup disable maven publishing here and add a publishing repository to addon.gradle. +usesMavenPublishing = true +# mavenPublishUrl = http://jenkins.usrv.eu:8081/nexus/content/repositories/releases + +# Publishing to modrinth requires you to set the MODRINTH_TOKEN environment variable to your current modrinth API token. + +# The project's ID on Modrinth. Can be either the slug or the ID. +# Leave this empty if you don't want to publish on Modrinth. +modrinthProjectId = + +# The project's relations on Modrinth. You can use this to refer to other projects on Modrinth. +# Syntax: scope1-type1:name1;scope2-type2:name2;... +# Where scope can be one of [required, optional, incompatible, embedded], +# type can be one of [project, version], +# and the name is the Modrinth project or version slug/id of the other mod. +# Example: required-project:fplib;optional-project:gasstation;incompatible-project:gregtech +# Note: GTNH Mixins is automatically set as a required dependency if usesMixins = true +modrinthRelations = + + +# Publishing to CurseForge requires you to set the CURSEFORGE_TOKEN environment variable to one of your CurseForge API tokens. + +# The project's numeric ID on CurseForge. You can find this in the About Project box. +# Leave this empty if you don't want to publish on CurseForge. +curseForgeProjectId = + +# The project's relations on CurseForge. You can use this to refer to other projects on CurseForge. +# Syntax: type1:name1;type2:name2;... +# Where type can be one of [requiredDependency, embeddedLibrary, optionalDependency, tool, incompatible], +# and the name is the CurseForge project slug of the other mod. +# Example: requiredDependency:railcraft;embeddedLibrary:cofhlib;incompatible:buildcraft +# Note: GTNH Mixins is automatically set as a required dependency if usesMixins = true +curseForgeRelations = + + +# Optional parameter to customize the produced artifacts. Use this to preserver artifact naming when migrating older +# projects. New projects should not use this parameter. +# customArchiveBaseName = + +# Optional parameter to prevent the source code from being published +# noPublishedSources = + +# Uncomment this to disable spotless checks +# This should only be uncommented to keep it easier to sync with upstream/other forks. +# That is, if there is no other active fork/upstream, NEVER change this. +# disableSpotless = true + +# Uncomment this to disable checkstyle checks (currently wildcard import check). +# disableCheckstyle = true + +# Override the IDEA build type. Valid value is "" (leave blank, do not override), "idea" (force use native IDEA build), "gradle" +# (force use delegated build). +# This is meant to be set in $HOME/.gradle/gradle.properties. +# e.g. add "systemProp.org.gradle.project.ideaOverrideBuildType=idea" will override the build type to be always native build. +# WARNING: If you do use this option, it will overwrite whatever you have in your existing projects. This might not be what you want! +# Usually there is no need to uncomment this here as other developers do not necessarily use the same build type as you. +# ideaOverrideBuildType = idea diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index b761216..033e24c 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 678d9d8..9f4197d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ -#Wed Jul 02 15:54:47 CDT 2014 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.0-bin.zip diff --git a/gradlew b/gradlew index 91a7e26..fcb6fca 100755 --- a/gradlew +++ b/gradlew @@ -1,79 +1,126 @@ -#!/usr/bin/env bash +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum -warn ( ) { +warn () { echo "$*" -} +} >&2 -die ( ) { +die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac -# For Cygwin, ensure paths are in UNIX format before anything is touched. -if $cygwin ; then - [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` -fi - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >&- -APP_HOME="`pwd -P`" -cd "$SAVED" >&- - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -82,83 +129,120 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=$((i+1)) + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") -} -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 8a0b282..93e3f59 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,4 +1,20 @@ -@if "%DEBUG%" == "" @echo off +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -8,20 +24,24 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -35,7 +55,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -45,44 +65,26 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windowz variants - -if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ - :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/jitpack.yml b/jitpack.yml new file mode 100644 index 0000000..09bbb51 --- /dev/null +++ b/jitpack.yml @@ -0,0 +1,2 @@ +before_install: + - ./gradlew setupCIWorkspace \ No newline at end of file diff --git a/repositories.gradle b/repositories.gradle new file mode 100644 index 0000000..c884390 --- /dev/null +++ b/repositories.gradle @@ -0,0 +1,5 @@ +// Add any additional repositories for your dependencies here + +repositories { + +} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..e334397 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,28 @@ + +pluginManagement { + repositories { + maven { + // RetroFuturaGradle + name "GTNH Maven" + url "http://jenkins.usrv.eu:8081/nexus/content/groups/public/" + allowInsecureProtocol = true + mavenContent { + includeGroup("com.gtnewhorizons.retrofuturagradle") + } + } + gradlePluginPortal() + mavenCentral() + mavenLocal() + } +} + +plugins { + id 'com.diffplug.blowdryerSetup' version '1.6.0' + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.4.0' // Provides java toolchains +} + +blowdryerSetup { + repoSubfolder 'gtnhShared' + github('GTNewHorizons/ExampleMod1.7.10', 'tag', '0.2.2') + //devLocal '.' // Use this when testing config updates locally +} diff --git a/src/main/java/erogenousbeef/core/common/BeefCoreLog.java b/src/main/java/erogenousbeef/core/common/BeefCoreLog.java index 936484a..69c5cd4 100644 --- a/src/main/java/erogenousbeef/core/common/BeefCoreLog.java +++ b/src/main/java/erogenousbeef/core/common/BeefCoreLog.java @@ -5,41 +5,34 @@ import cpw.mods.fml.common.FMLLog; public class BeefCoreLog { - - private static final String CHANNEL = "BeefCore"; - public static void log(Level level, String format, Object... data) - { - FMLLog.log(level, format, data); + private static final String CHANNEL = "BeefCore"; + + public static void log(Level level, String format, Object... data) { + FMLLog.log(level, format, data); } - public static void fatal(String format, Object... data) - { + public static void fatal(String format, Object... data) { log(Level.FATAL, format, data); } - public static void error(String format, Object... data) - { + public static void error(String format, Object... data) { log(Level.ERROR, format, data); } - public static void warning(String format, Object... data) - { + public static void warning(String format, Object... data) { log(Level.WARN, format, data); } - public static void info(String format, Object... data) - { + public static void info(String format, Object... data) { log(Level.INFO, format, data); } - public static void debug(String format, Object... data) - { + public static void debug(String format, Object... data) { log(Level.DEBUG, format, data); } - public static void trace(String format, Object... data) - { + public static void trace(String format, Object... data) { log(Level.TRACE, format, data); } } diff --git a/src/main/java/erogenousbeef/core/common/CoordTriplet.java b/src/main/java/erogenousbeef/core/common/CoordTriplet.java index 6da5a7e..cce4413 100644 --- a/src/main/java/erogenousbeef/core/common/CoordTriplet.java +++ b/src/main/java/erogenousbeef/core/common/CoordTriplet.java @@ -7,122 +7,157 @@ * Simple wrapper class for XYZ coordinates. */ public class CoordTriplet implements Comparable { - public int x, y, z; - - public CoordTriplet(int x, int y, int z) { - this.x = x; - this.y = y; - this.z = z; - } - - public int getChunkX() { return x >> 4; } - public int getChunkZ() { return z >> 4; } - public long getChunkXZHash() { return ChunkCoordIntPair.chunkXZ2Int(x >> 4, z >> 4); } - - @Override - public boolean equals(Object other) { - if(other == null) - { return false; } - else if(other instanceof CoordTriplet) { - CoordTriplet otherTriplet = (CoordTriplet)other; - return this.x == otherTriplet.x && this.y == otherTriplet.y && this.z == otherTriplet.z; - } - else { - return false; - } - } - - public void translate(ForgeDirection dir) { - this.x += dir.offsetX; - this.y += dir.offsetY; - this.z += dir.offsetZ; - } - - public boolean equals(int x, int y, int z) { - return this.x == x && this.y == y && this.z == z; - } - - // Suggested implementation from NetBeans 7.1 - public int hashCode() { - int hash = 7; - hash = 71 * hash + this.x; - hash = 71 * hash + this.y; - hash = 71 * hash + this.z; - return hash; - } - - public CoordTriplet copy() { - return new CoordTriplet(x, y, z); - } - - public void copy(CoordTriplet other) { - this.x = other.x; - this.y = other.y; - this.z = other.z; - } - - public CoordTriplet[] getNeighbors() { - return new CoordTriplet[]{ - new CoordTriplet(x + 1, y, z), - new CoordTriplet(x - 1, y, z), - new CoordTriplet(x, y + 1, z), - new CoordTriplet(x, y - 1, z), - new CoordTriplet(x, y, z + 1), - new CoordTriplet(x, y, z - 1) - }; - } - - ///// IComparable - - @Override - public int compareTo(Object o) { - if(o instanceof CoordTriplet) { - CoordTriplet other = (CoordTriplet)o; - if(this.x < other.x) { return -1; } - else if(this.x > other.x) { return 1; } - else if(this.y < other.y) { return -1; } - else if(this.y > other.y) { return 1; } - else if(this.z < other.z) { return -1; } - else if(this.z > other.z) { return 1; } - else { return 0; } - } - return 0; - } - - ///// Really confusing code that should be cleaned up - - public ForgeDirection getDirectionFromSourceCoords(int x, int y, int z) { - if(this.x < x) { return ForgeDirection.WEST; } - else if(this.x > x) { return ForgeDirection.EAST; } - else if(this.y < y) { return ForgeDirection.DOWN; } - else if(this.y > y) { return ForgeDirection.UP; } - else if(this.z < z) { return ForgeDirection.SOUTH; } - else if(this.z > z) { return ForgeDirection.NORTH; } - else { return ForgeDirection.UNKNOWN; } - } - - public ForgeDirection getOppositeDirectionFromSourceCoords(int x, int y, int z) { - if(this.x < x) { return ForgeDirection.EAST; } - else if(this.x > x) { return ForgeDirection.WEST; } - else if(this.y < y) { return ForgeDirection.UP; } - else if(this.y > y) { return ForgeDirection.DOWN; } - else if(this.z < z) { return ForgeDirection.NORTH; } - else if(this.z > z) { return ForgeDirection.SOUTH; } - else { return ForgeDirection.UNKNOWN; } - } - - @Override - public String toString() { - return String.format("(%d, %d, %d)", this.x, this.y, this.z); - } - - public int compareTo(int xCoord, int yCoord, int zCoord) { - if(this.x < xCoord) { return -1; } - else if(this.x > xCoord) { return 1; } - else if(this.y < yCoord) { return -1; } - else if(this.y > yCoord) { return 1; } - else if(this.z < zCoord) { return -1; } - else if(this.z > zCoord) { return 1; } - else { return 0; } - } + + public int x, y, z; + + public CoordTriplet(int x, int y, int z) { + this.x = x; + this.y = y; + this.z = z; + } + + public int getChunkX() { + return x >> 4; + } + + public int getChunkZ() { + return z >> 4; + } + + public long getChunkXZHash() { + return ChunkCoordIntPair.chunkXZ2Int(x >> 4, z >> 4); + } + + @Override + public boolean equals(Object other) { + if (other == null) { + return false; + } else if (other instanceof CoordTriplet) { + CoordTriplet otherTriplet = (CoordTriplet) other; + return this.x == otherTriplet.x && this.y == otherTriplet.y && this.z == otherTriplet.z; + } else { + return false; + } + } + + public void translate(ForgeDirection dir) { + this.x += dir.offsetX; + this.y += dir.offsetY; + this.z += dir.offsetZ; + } + + public boolean equals(int x, int y, int z) { + return this.x == x && this.y == y && this.z == z; + } + + // Suggested implementation from NetBeans 7.1 + public int hashCode() { + int hash = 7; + hash = 71 * hash + this.x; + hash = 71 * hash + this.y; + hash = 71 * hash + this.z; + return hash; + } + + public CoordTriplet copy() { + return new CoordTriplet(x, y, z); + } + + public void copy(CoordTriplet other) { + this.x = other.x; + this.y = other.y; + this.z = other.z; + } + + public CoordTriplet[] getNeighbors() { + return new CoordTriplet[] { new CoordTriplet(x + 1, y, z), new CoordTriplet(x - 1, y, z), + new CoordTriplet(x, y + 1, z), new CoordTriplet(x, y - 1, z), new CoordTriplet(x, y, z + 1), + new CoordTriplet(x, y, z - 1) }; + } + + ///// IComparable + + @Override + public int compareTo(Object o) { + if (o instanceof CoordTriplet) { + CoordTriplet other = (CoordTriplet) o; + if (this.x < other.x) { + return -1; + } else if (this.x > other.x) { + return 1; + } else if (this.y < other.y) { + return -1; + } else if (this.y > other.y) { + return 1; + } else if (this.z < other.z) { + return -1; + } else if (this.z > other.z) { + return 1; + } else { + return 0; + } + } + return 0; + } + + ///// Really confusing code that should be cleaned up + + public ForgeDirection getDirectionFromSourceCoords(int x, int y, int z) { + if (this.x < x) { + return ForgeDirection.WEST; + } else if (this.x > x) { + return ForgeDirection.EAST; + } else if (this.y < y) { + return ForgeDirection.DOWN; + } else if (this.y > y) { + return ForgeDirection.UP; + } else if (this.z < z) { + return ForgeDirection.SOUTH; + } else if (this.z > z) { + return ForgeDirection.NORTH; + } else { + return ForgeDirection.UNKNOWN; + } + } + + public ForgeDirection getOppositeDirectionFromSourceCoords(int x, int y, int z) { + if (this.x < x) { + return ForgeDirection.EAST; + } else if (this.x > x) { + return ForgeDirection.WEST; + } else if (this.y < y) { + return ForgeDirection.UP; + } else if (this.y > y) { + return ForgeDirection.DOWN; + } else if (this.z < z) { + return ForgeDirection.NORTH; + } else if (this.z > z) { + return ForgeDirection.SOUTH; + } else { + return ForgeDirection.UNKNOWN; + } + } + + @Override + public String toString() { + return String.format("(%d, %d, %d)", this.x, this.y, this.z); + } + + public int compareTo(int xCoord, int yCoord, int zCoord) { + if (this.x < xCoord) { + return -1; + } else if (this.x > xCoord) { + return 1; + } else if (this.y < yCoord) { + return -1; + } else if (this.y > yCoord) { + return 1; + } else if (this.z < zCoord) { + return -1; + } else if (this.z > zCoord) { + return 1; + } else { + return 0; + } + } } diff --git a/src/main/java/erogenousbeef/core/multiblock/BlockMultiblockBase.java b/src/main/java/erogenousbeef/core/multiblock/BlockMultiblockBase.java index c501859..8b3010f 100644 --- a/src/main/java/erogenousbeef/core/multiblock/BlockMultiblockBase.java +++ b/src/main/java/erogenousbeef/core/multiblock/BlockMultiblockBase.java @@ -9,7 +9,7 @@ */ public abstract class BlockMultiblockBase extends BlockContainer { - protected BlockMultiblockBase(Material material) { - super(material); - } + protected BlockMultiblockBase(Material material) { + super(material); + } } diff --git a/src/main/java/erogenousbeef/core/multiblock/IMultiblockPart.java b/src/main/java/erogenousbeef/core/multiblock/IMultiblockPart.java index 3f74f4c..14b00c6 100644 --- a/src/main/java/erogenousbeef/core/multiblock/IMultiblockPart.java +++ b/src/main/java/erogenousbeef/core/multiblock/IMultiblockPart.java @@ -4,6 +4,7 @@ import net.minecraft.nbt.NBTTagCompound; import net.minecraft.tileentity.TileEntity; + import erogenousbeef.core.common.CoordTriplet; /** @@ -15,177 +16,190 @@ * {@link erogenousbeef.core.multiblock.MultiblockTileEntityBase} */ public abstract class IMultiblockPart extends TileEntity { - public static final int INVALID_DISTANCE = Integer.MAX_VALUE; - - /** - * @return True if this block is connected to a multiblock controller. False otherwise. - */ - public abstract boolean isConnected(); - - /** - * @return The attached multiblock controller for this tile entity. - */ - public abstract MultiblockControllerBase getMultiblockController(); - - /** - * Returns the location of this tile entity in the world, in CoordTriplet form. - * @return A CoordTriplet with its x,y,z members set to the location of this tile entity in the world. - */ - public abstract CoordTriplet getWorldLocation(); - - // Multiblock connection-logic callbacks - - /** - * Called after this block has been attached to a new multiblock controller. - * @param newController The new multiblock controller to which this tile entity is attached. - */ - public abstract void onAttached(MultiblockControllerBase newController); - - /** - * Called after this block has been detached from a multiblock controller. - * @param multiblockController The multiblock controller that no longer controls this tile entity. - */ - public abstract void onDetached(MultiblockControllerBase multiblockController); - - /** - * Called when this block is being orphaned. Use this to copy game-data values that - * should persist despite a machine being broken. - * This should NOT mark the part as disconnected. onDetached will be called immediately afterwards. - * @see #onDetached(MultiblockControllerBase) - * @param oldController The controller which is orphaning this block. - * @param oldControllerSize The number of connected blocks in the controller prior to shedding orphans. - * @param newControllerSize The number of connected blocks in the controller after shedding orphans. - */ - public abstract void onOrphaned(MultiblockControllerBase oldController, int oldControllerSize, int newControllerSize); - - // Multiblock fuse/split helper methods. Here there be dragons. - /** - * Factory method. Creates a new multiblock controller and returns it. - * Does not attach this tile entity to it. - * Override this in your game code! - * @return A new Multiblock Controller, derived from MultiblockControllerBase. - */ - public abstract MultiblockControllerBase createNewMultiblock(); - - /** - * Retrieve the type of multiblock controller which governs this part. - * Used to ensure that incompatible multiblocks are not merged. - * @return The class/type of the multiblock controller which governs this type of part. - */ - public abstract Class getMultiblockControllerType(); - - /** - * Called when this block is moved from its current controller into a new controller. - * A special case of attach/detach, done here for efficiency to avoid triggering - * lots of recalculation logic. - * @param newController The new controller into which this tile entity is being merged. - */ - public abstract void onAssimilated(MultiblockControllerBase newController); - - // Multiblock connection data access. - // You generally shouldn't toy with these! - // They're for use by Multiblock Controllers. - - /** - * Set that this block has been visited by your validation algorithms. - */ - public abstract void setVisited(); - - /** - * Set that this block has not been visited by your validation algorithms; - */ - public abstract void setUnvisited(); - - /** - * @return True if this block has been visited by your validation algorithms since the last reset. - */ - public abstract boolean isVisited(); - - /** - * Called when this block becomes the designated block for saving data and - * transmitting data across the wire. - */ - public abstract void becomeMultiblockSaveDelegate(); - - /** - * Called when this block is no longer the designated block for saving data - * and transmitting data across the wire. - */ - public abstract void forfeitMultiblockSaveDelegate(); - - /** - * Is this block the designated save/load & network delegate? - */ - public abstract boolean isMultiblockSaveDelegate(); - - /** - * Returns an array containing references to neighboring IMultiblockPart tile entities. - * Primarily a utility method. Only works after tileentity construction, so it cannot be used in - * MultiblockControllerBase::attachBlock. - * - * This method is chunk-safe on the server; it will not query for parts in chunks that are unloaded. - * Note that no method is chunk-safe on the client, because ChunkProviderClient is stupid. - * @return An array of references to neighboring IMultiblockPart tile entities. - */ - public abstract IMultiblockPart[] getNeighboringParts(); - - // Multiblock business-logic callbacks - implement these! - /** - * Called when a machine is fully assembled from the disassembled state, meaning - * it was broken by a player/entity action, not by chunk unloads. - * Note that, for non-square machines, the min/max coordinates may not actually be part - * of the machine! They form an outer bounding box for the whole machine itself. - * @param multiblockControllerBase The controller to which this part is being assembled. - */ - public abstract void onMachineAssembled(MultiblockControllerBase multiblockControllerBase); - - /** - * Called when the machine is broken for game reasons, e.g. a player removed a block - * or an explosion occurred. - */ - public abstract void onMachineBroken(); - - /** - * Called when the user activates the machine. This is not called by default, but is included - * as most machines have this game-logical concept. - */ - public abstract void onMachineActivated(); - - /** - * Called when the user deactivates the machine. This is not called by default, but is included - * as most machines have this game-logical concept. - */ - public abstract void onMachineDeactivated(); - - // Block events - /** - * Called when this part should check its neighbors. - * This method MUST NOT cause additional chunks to load. - * ALWAYS check to see if a chunk is loaded before querying for its tile entity - * This part should inform the controller that it is attaching at this time. - * @return A Set of multiblock controllers to which this object would like to attach. It should have attached to one of the controllers in this list. Return null if there are no compatible controllers nearby. - */ - public abstract Set attachToNeighbors(); - - /** - * Assert that this part is detached. If not, log a warning and set the part's controller to null. - * Do NOT fire the full disconnection logic. - */ - public abstract void assertDetached(); - - /** - * @return True if a part has multiblock game-data saved inside it. - */ - public abstract boolean hasMultiblockSaveData(); - - /** - * @return The part's saved multiblock game-data in NBT format, or null if there isn't any. - */ - public abstract NBTTagCompound getMultiblockSaveData(); - - /** - * Called after a block is added and the controller has incorporated the part's saved - * multiblock game-data into itself. Generally, you should clear the saved data here. - */ - public abstract void onMultiblockDataAssimilated(); + + public static final int INVALID_DISTANCE = Integer.MAX_VALUE; + + /** + * @return True if this block is connected to a multiblock controller. False otherwise. + */ + public abstract boolean isConnected(); + + /** + * @return The attached multiblock controller for this tile entity. + */ + public abstract MultiblockControllerBase getMultiblockController(); + + /** + * Returns the location of this tile entity in the world, in CoordTriplet form. + * + * @return A CoordTriplet with its x,y,z members set to the location of this tile entity in the world. + */ + public abstract CoordTriplet getWorldLocation(); + + // Multiblock connection-logic callbacks + + /** + * Called after this block has been attached to a new multiblock controller. + * + * @param newController The new multiblock controller to which this tile entity is attached. + */ + public abstract void onAttached(MultiblockControllerBase newController); + + /** + * Called after this block has been detached from a multiblock controller. + * + * @param multiblockController The multiblock controller that no longer controls this tile entity. + */ + public abstract void onDetached(MultiblockControllerBase multiblockController); + + /** + * Called when this block is being orphaned. Use this to copy game-data values that + * should persist despite a machine being broken. + * This should NOT mark the part as disconnected. onDetached will be called immediately afterwards. + * + * @see #onDetached(MultiblockControllerBase) + * @param oldController The controller which is orphaning this block. + * @param oldControllerSize The number of connected blocks in the controller prior to shedding orphans. + * @param newControllerSize The number of connected blocks in the controller after shedding orphans. + */ + public abstract void onOrphaned(MultiblockControllerBase oldController, int oldControllerSize, + int newControllerSize); + + // Multiblock fuse/split helper methods. Here there be dragons. + /** + * Factory method. Creates a new multiblock controller and returns it. + * Does not attach this tile entity to it. + * Override this in your game code! + * + * @return A new Multiblock Controller, derived from MultiblockControllerBase. + */ + public abstract MultiblockControllerBase createNewMultiblock(); + + /** + * Retrieve the type of multiblock controller which governs this part. + * Used to ensure that incompatible multiblocks are not merged. + * + * @return The class/type of the multiblock controller which governs this type of part. + */ + public abstract Class getMultiblockControllerType(); + + /** + * Called when this block is moved from its current controller into a new controller. + * A special case of attach/detach, done here for efficiency to avoid triggering + * lots of recalculation logic. + * + * @param newController The new controller into which this tile entity is being merged. + */ + public abstract void onAssimilated(MultiblockControllerBase newController); + + // Multiblock connection data access. + // You generally shouldn't toy with these! + // They're for use by Multiblock Controllers. + + /** + * Set that this block has been visited by your validation algorithms. + */ + public abstract void setVisited(); + + /** + * Set that this block has not been visited by your validation algorithms; + */ + public abstract void setUnvisited(); + + /** + * @return True if this block has been visited by your validation algorithms since the last reset. + */ + public abstract boolean isVisited(); + + /** + * Called when this block becomes the designated block for saving data and + * transmitting data across the wire. + */ + public abstract void becomeMultiblockSaveDelegate(); + + /** + * Called when this block is no longer the designated block for saving data + * and transmitting data across the wire. + */ + public abstract void forfeitMultiblockSaveDelegate(); + + /** + * Is this block the designated save/load & network delegate? + */ + public abstract boolean isMultiblockSaveDelegate(); + + /** + * Returns an array containing references to neighboring IMultiblockPart tile entities. + * Primarily a utility method. Only works after tileentity construction, so it cannot be used in + * MultiblockControllerBase::attachBlock. + * + * This method is chunk-safe on the server; it will not query for parts in chunks that are unloaded. + * Note that no method is chunk-safe on the client, because ChunkProviderClient is stupid. + * + * @return An array of references to neighboring IMultiblockPart tile entities. + */ + public abstract IMultiblockPart[] getNeighboringParts(); + + // Multiblock business-logic callbacks - implement these! + /** + * Called when a machine is fully assembled from the disassembled state, meaning + * it was broken by a player/entity action, not by chunk unloads. + * Note that, for non-square machines, the min/max coordinates may not actually be part + * of the machine! They form an outer bounding box for the whole machine itself. + * + * @param multiblockControllerBase The controller to which this part is being assembled. + */ + public abstract void onMachineAssembled(MultiblockControllerBase multiblockControllerBase); + + /** + * Called when the machine is broken for game reasons, e.g. a player removed a block + * or an explosion occurred. + */ + public abstract void onMachineBroken(); + + /** + * Called when the user activates the machine. This is not called by default, but is included + * as most machines have this game-logical concept. + */ + public abstract void onMachineActivated(); + + /** + * Called when the user deactivates the machine. This is not called by default, but is included + * as most machines have this game-logical concept. + */ + public abstract void onMachineDeactivated(); + + // Block events + /** + * Called when this part should check its neighbors. + * This method MUST NOT cause additional chunks to load. + * ALWAYS check to see if a chunk is loaded before querying for its tile entity + * This part should inform the controller that it is attaching at this time. + * + * @return A Set of multiblock controllers to which this object would like to attach. It should have attached to one + * of the controllers in this list. Return null if there are no compatible controllers nearby. + */ + public abstract Set attachToNeighbors(); + + /** + * Assert that this part is detached. If not, log a warning and set the part's controller to null. + * Do NOT fire the full disconnection logic. + */ + public abstract void assertDetached(); + + /** + * @return True if a part has multiblock game-data saved inside it. + */ + public abstract boolean hasMultiblockSaveData(); + + /** + * @return The part's saved multiblock game-data in NBT format, or null if there isn't any. + */ + public abstract NBTTagCompound getMultiblockSaveData(); + + /** + * Called after a block is added and the controller has incorporated the part's saved + * multiblock game-data into itself. Generally, you should clear the saved data here. + */ + public abstract void onMultiblockDataAssimilated(); } diff --git a/src/main/java/erogenousbeef/core/multiblock/MultiblockClientTickHandler.java b/src/main/java/erogenousbeef/core/multiblock/MultiblockClientTickHandler.java index 0ae08c2..2cf504d 100644 --- a/src/main/java/erogenousbeef/core/multiblock/MultiblockClientTickHandler.java +++ b/src/main/java/erogenousbeef/core/multiblock/MultiblockClientTickHandler.java @@ -1,6 +1,7 @@ package erogenousbeef.core.multiblock; import net.minecraft.client.Minecraft; + import cpw.mods.fml.common.eventhandler.SubscribeEvent; import cpw.mods.fml.common.gameevent.TickEvent; @@ -8,7 +9,7 @@ public class MultiblockClientTickHandler { @SubscribeEvent public void onClientTick(TickEvent.ClientTickEvent event) { - if(event.phase == TickEvent.Phase.START) { + if (event.phase == TickEvent.Phase.START) { MultiblockRegistry.tickStart(Minecraft.getMinecraft().theWorld); } } diff --git a/src/main/java/erogenousbeef/core/multiblock/MultiblockControllerBase.java b/src/main/java/erogenousbeef/core/multiblock/MultiblockControllerBase.java index c8e2204..9d4a5cf 100644 --- a/src/main/java/erogenousbeef/core/multiblock/MultiblockControllerBase.java +++ b/src/main/java/erogenousbeef/core/multiblock/MultiblockControllerBase.java @@ -9,6 +9,7 @@ import net.minecraft.world.World; import net.minecraft.world.chunk.Chunk; import net.minecraft.world.chunk.IChunkProvider; + import erogenousbeef.core.common.BeefCoreLog; import erogenousbeef.core.common.CoordTriplet; @@ -19,887 +20,1018 @@ * Subordinate TileEntities implement the IMultiblockPart class and, generally, should not have an update() loop. */ public abstract class MultiblockControllerBase { - public static final short DIMENSION_UNBOUNDED = -1; - - // Multiblock stuff - do not mess with - protected World worldObj; - - // Disassembled -> Assembled; Assembled -> Disassembled OR Paused; Paused -> Assembled - protected enum AssemblyState { Disassembled, Assembled, Paused }; - protected AssemblyState assemblyState; - - protected HashSet connectedParts; - - /** This is a deterministically-picked coordinate that identifies this - * multiblock uniquely in its dimension. - * Currently, this is the coord with the lowest X, Y and Z coordinates, in that order of evaluation. - * i.e. If something has a lower X but higher Y/Z coordinates, it will still be the reference. - * If something has the same X but a lower Y coordinate, it will be the reference. Etc. - */ - private CoordTriplet referenceCoord; - - /** - * Minimum bounding box coordinate. Blocks do not necessarily exist at this coord if your machine - * is not a cube/rectangular prism. - */ - private CoordTriplet minimumCoord; - - /** - * Maximum bounding box coordinate. Blocks do not necessarily exist at this coord if your machine - * is not a cube/rectangular prism. - */ - private CoordTriplet maximumCoord; - - /** - * Set to true whenever a part is removed from this controller. - */ - private boolean shouldCheckForDisconnections; - - /** - * Set whenever we validate the multiblock - */ - private MultiblockValidationException lastValidationException; - - protected boolean debugMode; - - protected MultiblockControllerBase(World world) { - // Multiblock stuff - worldObj = world; - connectedParts = new HashSet(); - - referenceCoord = null; - assemblyState = AssemblyState.Disassembled; - - minimumCoord = null; - maximumCoord = null; - - shouldCheckForDisconnections = true; - lastValidationException = null; - - debugMode = false; - } - - public void setDebugMode(boolean active) { - debugMode = active; - } - - public boolean isDebugMode() { return debugMode; } - - /** - * Call when a block with cached save-delegate data is added to the multiblock. - * The part will be notified that the data has been used after this call completes. - * @param part The NBT tag containing this controller's data. - */ - public abstract void onAttachedPartWithMultiblockData(IMultiblockPart part, NBTTagCompound data); - - /** - * Check if a block is being tracked by this machine. - * @param blockCoord Coordinate to check. - * @return True if the tile entity at blockCoord is being tracked by this machine, false otherwise. - */ - public boolean hasBlock(CoordTriplet blockCoord) { - return connectedParts.contains(blockCoord); - } - - /** - * Attach a new part to this machine. - * @param part The part to add. - */ - public void attachBlock(IMultiblockPart part) { - IMultiblockPart candidate; - CoordTriplet coord = part.getWorldLocation(); - - if(!connectedParts.add(part)) { - BeefCoreLog.warning("[%s] Controller %s is double-adding part %d @ %s. This is unusual. If you encounter odd behavior, please tear down the machine and rebuild it.", (worldObj.isRemote?"CLIENT":"SERVER"), hashCode(), part.hashCode(), coord); - } - - part.onAttached(this); - this.onBlockAdded(part); - - if(part.hasMultiblockSaveData()) { - NBTTagCompound savedData = part.getMultiblockSaveData(); - onAttachedPartWithMultiblockData(part, savedData); - part.onMultiblockDataAssimilated(); - } - - if(this.referenceCoord == null) { - referenceCoord = coord; - part.becomeMultiblockSaveDelegate(); - } - else if(coord.compareTo(referenceCoord) < 0) { - TileEntity te = this.worldObj.getTileEntity(referenceCoord.x, referenceCoord.y, referenceCoord.z); - ((IMultiblockPart)te).forfeitMultiblockSaveDelegate(); - - referenceCoord = coord; - part.becomeMultiblockSaveDelegate(); - } - else { - part.forfeitMultiblockSaveDelegate(); - } - - if(minimumCoord != null) { - if(part.xCoord < minimumCoord.x) { minimumCoord.x = part.xCoord; } - if(part.yCoord < minimumCoord.y) { minimumCoord.y = part.yCoord; } - if(part.zCoord < minimumCoord.z) { minimumCoord.z = part.zCoord; } - } - - if(maximumCoord != null) { - if(part.xCoord > maximumCoord.x) { maximumCoord.x = part.xCoord; } - if(part.yCoord > maximumCoord.y) { maximumCoord.y = part.yCoord; } - if(part.zCoord > maximumCoord.z) { maximumCoord.z = part.zCoord; } - } - - MultiblockRegistry.addDirtyController(worldObj, this); - } - - /** - * Called when a new part is added to the machine. Good time to register things into lists. - * @param newPart The part being added. - */ - protected abstract void onBlockAdded(IMultiblockPart newPart); - - /** - * Called when a part is removed from the machine. Good time to clean up lists. - * @param oldPart The part being removed. - */ - protected abstract void onBlockRemoved(IMultiblockPart oldPart); - - /** - * Called when a machine is assembled from a disassembled state. - */ - protected abstract void onMachineAssembled(); - - /** - * Called when a machine is restored to the assembled state from a paused state. - */ - protected abstract void onMachineRestored(); - - /** - * Called when a machine is paused from an assembled state - * This generally only happens due to chunk-loads and other "system" events. - */ - protected abstract void onMachinePaused(); - - /** - * Called when a machine is disassembled from an assembled state. - * This happens due to user or in-game actions (e.g. explosions) - */ - protected abstract void onMachineDisassembled(); - - /** - * Callback whenever a part is removed (or will very shortly be removed) from a controller. - * Do housekeeping/callbacks, also nulls min/max coords. - * @param part The part being removed. - */ - private void onDetachBlock(IMultiblockPart part) { - // Strip out this part - part.onDetached(this); - this.onBlockRemoved(part); - part.forfeitMultiblockSaveDelegate(); - - minimumCoord = maximumCoord = null; - - if(referenceCoord != null && referenceCoord.equals(part.xCoord, part.yCoord, part.zCoord)) { - referenceCoord = null; - } - - shouldCheckForDisconnections = true; - } - - /** - * Call to detach a block from this machine. Generally, this should be called - * when the tile entity is being released, e.g. on block destruction. - * @param part The part to detach from this machine. - * @param chunkUnloading Is this entity detaching due to the chunk unloading? If true, the multiblock will be paused instead of broken. - */ - public void detachBlock(IMultiblockPart part, boolean chunkUnloading) { - if(chunkUnloading && this.assemblyState == AssemblyState.Assembled) { - this.assemblyState = AssemblyState.Paused; - this.onMachinePaused(); - } - - // Strip out this part - onDetachBlock(part); - if(!connectedParts.remove(part)) { - BeefCoreLog.warning("[%s] Double-removing part (%d) @ %d, %d, %d, this is unexpected and may cause problems. If you encounter anomalies, please tear down the reactor and rebuild it.", worldObj.isRemote?"CLIENT":"SERVER", part.hashCode(), part.xCoord, part.yCoord, part.zCoord); - } - - if(connectedParts.isEmpty()) { - // Destroy/unregister - MultiblockRegistry.addDeadController(this.worldObj, this); - return; - } - - MultiblockRegistry.addDirtyController(this.worldObj, this); - - // Find new save delegate if we need to. - if(referenceCoord == null) { - selectNewReferenceCoord(); - } - } - - /** - * Helper method so we don't check for a whole machine until we have enough blocks - * to actually assemble it. This isn't as simple as xmax*ymax*zmax for non-cubic machines - * or for machines with hollow/complex interiors. - * @return The minimum number of blocks connected to the machine for it to be assembled. - */ - protected abstract int getMinimumNumberOfBlocksForAssembledMachine(); - - /** - * Returns the maximum X dimension size of the machine, or -1 (DIMENSION_UNBOUNDED) to disable - * dimension checking in X. (This is not recommended.) - * @return The maximum X dimension size of the machine, or -1 - */ - protected abstract int getMaximumXSize(); - - /** - * Returns the maximum Z dimension size of the machine, or -1 (DIMENSION_UNBOUNDED) to disable - * dimension checking in X. (This is not recommended.) - * @return The maximum Z dimension size of the machine, or -1 - */ - protected abstract int getMaximumZSize(); - - /** - * Returns the maximum Y dimension size of the machine, or -1 (DIMENSION_UNBOUNDED) to disable - * dimension checking in X. (This is not recommended.) - * @return The maximum Y dimension size of the machine, or -1 - */ - protected abstract int getMaximumYSize(); - - /** - * Returns the minimum X dimension size of the machine. Must be at least 1, because nothing else makes sense. - * @return The minimum X dimension size of the machine - */ - protected int getMinimumXSize() { return 1; } - - /** - * Returns the minimum Y dimension size of the machine. Must be at least 1, because nothing else makes sense. - * @return The minimum Y dimension size of the machine - */ - protected int getMinimumYSize() { return 1; } - - /** - * Returns the minimum Z dimension size of the machine. Must be at least 1, because nothing else makes sense. - * @return The minimum Z dimension size of the machine - */ - protected int getMinimumZSize() { return 1; } - - - /** - * @return An exception representing the last error encountered when trying to assemble this - * multiblock, or null if there is no error. - */ - public MultiblockValidationException getLastValidationException() { return lastValidationException; } - - /** - * Checks if a machine is whole. If not, throws an exception with the reason why. - */ - protected abstract void isMachineWhole() throws MultiblockValidationException; - - /** - * Check if the machine is whole or not. - * If the machine was not whole, but now is, assemble the machine. - * If the machine was whole, but no longer is, disassemble the machine. - * @return - */ - public void checkIfMachineIsWhole() { - AssemblyState oldState = this.assemblyState; - boolean isWhole; - lastValidationException = null; - try { - isMachineWhole(); - isWhole = true; - } catch (MultiblockValidationException e) { - lastValidationException = e; - isWhole = false; - } - - if(isWhole) { - // This will alter assembly state - assembleMachine(oldState); - } - else if(oldState == AssemblyState.Assembled) { - // This will alter assembly state - disassembleMachine(); - } - // Else Paused, do nothing - } - - /** - * Called when a machine becomes "whole" and should begin - * functioning as a game-logically finished machine. - * Calls onMachineAssembled on all attached parts. - */ - private void assembleMachine(AssemblyState oldState) { - for(IMultiblockPart part : connectedParts) { - part.onMachineAssembled(this); - } - - this.assemblyState = AssemblyState.Assembled; - if(oldState == assemblyState.Paused) { - onMachineRestored(); - } - else { - onMachineAssembled(); - } - } - - /** - * Called when the machine needs to be disassembled. - * It is not longer "whole" and should not be functional, usually - * as a result of a block being removed. - * Calls onMachineBroken on all attached parts. - */ - private void disassembleMachine() { - for(IMultiblockPart part : connectedParts) { - part.onMachineBroken(); - } - - this.assemblyState = AssemblyState.Disassembled; - onMachineDisassembled(); - } - - /** - * Assimilate another controller into this controller. - * Acquire all of the other controller's blocks and attach them - * to this one. - * - * @param other The controller to merge into this one. - */ - public void assimilate(MultiblockControllerBase other) { - CoordTriplet otherReferenceCoord = other.getReferenceCoord(); - if(otherReferenceCoord != null && getReferenceCoord().compareTo(otherReferenceCoord) >= 0) { - throw new IllegalArgumentException("The controller with the lowest minimum-coord value must consume the one with the higher coords"); - } - - TileEntity te; - Set partsToAcquire = new HashSet(other.connectedParts); - - // releases all blocks and references gently so they can be incorporated into another multiblock - other._onAssimilated(this); - - for(IMultiblockPart acquiredPart : partsToAcquire) { - // By definition, none of these can be the minimum block. - if(acquiredPart.isInvalid()) { continue; } - - connectedParts.add(acquiredPart); - acquiredPart.onAssimilated(this); - this.onBlockAdded(acquiredPart); - } - - this.onAssimilate(other); - other.onAssimilated(this); - } - - /** - * Called when this machine is consumed by another controller. - * Essentially, forcibly tear down this object. - * @param otherController The controller consuming this controller. - */ - private void _onAssimilated(MultiblockControllerBase otherController) { - if(referenceCoord != null) { - if(worldObj.getChunkProvider().chunkExists(referenceCoord.getChunkX(), referenceCoord.getChunkZ())) { - TileEntity te = this.worldObj.getTileEntity(referenceCoord.x, referenceCoord.y, referenceCoord.z); - if(te instanceof IMultiblockPart) { - ((IMultiblockPart)te).forfeitMultiblockSaveDelegate(); - } - } - this.referenceCoord = null; - } - - connectedParts.clear(); - } - - /** - * Callback. Called after this controller assimilates all the blocks - * from another controller. - * Use this to absorb that controller's game data. - * @param assimilated The controller whose uniqueness was added to our own. - */ - protected abstract void onAssimilate(MultiblockControllerBase assimilated); - - /** - * Callback. Called after this controller is assimilated into another controller. - * All blocks have been stripped out of this object and handed over to the - * other controller. - * This is intended primarily for cleanup. - * @param assimilator The controller which has assimilated this controller. - */ - protected abstract void onAssimilated(MultiblockControllerBase assimilator); - - /** - * Driver for the update loop. If the machine is assembled, runs - * the game logic update method. - * @see erogenousbeef.core.multiblock.MultiblockControllerBase#update() //TODO Fix this Javadoc - */ - public final void updateMultiblockEntity() { - if(connectedParts.isEmpty()) { - // This shouldn't happen, but just in case... - MultiblockRegistry.addDeadController(this.worldObj, this); - return; - } - - if(this.assemblyState != AssemblyState.Assembled) { - // Not assembled - don't run game logic - return; - } - - if(worldObj.isRemote) { - updateClient(); - } - else if(updateServer()) { - // If this returns true, the server has changed its internal data. - // If our chunks are loaded (they should be), we must mark our chunks as dirty. - if(minimumCoord != null && maximumCoord != null && - this.worldObj.checkChunksExist(minimumCoord.x, minimumCoord.y, minimumCoord.z, - maximumCoord.x, maximumCoord.y, maximumCoord.z)) { - int minChunkX = minimumCoord.x >> 4; - int minChunkZ = minimumCoord.z >> 4; - int maxChunkX = maximumCoord.x >> 4; - int maxChunkZ = maximumCoord.z >> 4; - - for(int x = minChunkX; x <= maxChunkX; x++) { - for(int z = minChunkZ; z <= maxChunkZ; z++) { - // Ensure that we save our data, even if the our save delegate is in has no TEs. - Chunk chunkToSave = this.worldObj.getChunkFromChunkCoords(x, z); - chunkToSave.setChunkModified(); - } - } - } - } - // Else: Server, but no need to save data. - } - - /** - * The server-side update loop! Use this similarly to a TileEntity's update loop. - * You do not need to call your superclass' update() if you're directly - * derived from MultiblockControllerBase. This is a callback. - * Note that this will only be called when the machine is assembled. - * @return True if the multiblock should save data, i.e. its internal game state has changed. False otherwise. - */ - protected abstract boolean updateServer(); - - /** - * Client-side update loop. Generally, this shouldn't do anything, but if you want - * to do some interpolation or something, do it here. - */ - protected abstract void updateClient(); - - // Validation helpers - /** - * The "frame" consists of the outer edges of the machine, plus the corners. - * - * @param world World object for the world in which this controller is located. - * @param x X coordinate of the block being tested - * @param y Y coordinate of the block being tested - * @param z Z coordinate of the block being tested - * @throws MultiblockValidationException if the tested block is not allowed on the machine's frame - */ - protected void isBlockGoodForFrame(World world, int x, int y, int z) throws MultiblockValidationException { - throw new MultiblockValidationException(String.format("%d, %d, %d - Block is not valid for use in the machine's interior", x, y, z)); - } - - /** - * The top consists of the top face, minus the edges. - * @param world World object for the world in which this controller is located. - * @param x X coordinate of the block being tested - * @param y Y coordinate of the block being tested - * @param z Z coordinate of the block being tested - * @throws MultiblockValidationException if the tested block is not allowed on the machine's top face - */ - protected void isBlockGoodForTop(World world, int x, int y, int z) throws MultiblockValidationException { - throw new MultiblockValidationException(String.format("%d, %d, %d - Block is not valid for use in the machine's interior", x, y, z)); - } - - /** - * The bottom consists of the bottom face, minus the edges. - * @param world World object for the world in which this controller is located. - * @param x X coordinate of the block being tested - * @param y Y coordinate of the block being tested - * @param z Z coordinate of the block being tested - * @throws MultiblockValidationException if the tested block is not allowed on the machine's bottom face - */ - protected void isBlockGoodForBottom(World world, int x, int y, int z) throws MultiblockValidationException { - throw new MultiblockValidationException(String.format("%d, %d, %d - Block is not valid for use in the machine's interior", x, y, z)); - } - - /** - * The sides consists of the N/E/S/W-facing faces, minus the edges. - * @param world World object for the world in which this controller is located. - * @param x X coordinate of the block being tested - * @param y Y coordinate of the block being tested - * @param z Z coordinate of the block being tested - * @throws MultiblockValidationException if the tested block is not allowed on the machine's side faces - */ - protected void isBlockGoodForSides(World world, int x, int y, int z) throws MultiblockValidationException { - throw new MultiblockValidationException(String.format("%d, %d, %d - Block is not valid for use in the machine's interior", x, y, z)); - } - - /** - * The interior is any block that does not touch blocks outside the machine. - * @param world World object for the world in which this controller is located. - * @param x X coordinate of the block being tested - * @param y Y coordinate of the block being tested - * @param z Z coordinate of the block being tested - * @throws MultiblockValidationException if the tested block is not allowed in the machine's interior - */ - protected void isBlockGoodForInterior(World world, int x, int y, int z) throws MultiblockValidationException { - throw new MultiblockValidationException(String.format("%d, %d, %d - Block is not valid for use in the machine's interior", x, y, z)); - } - - /** - * @return The reference coordinate, the block with the lowest x, y, z coordinates, evaluated in that order. - */ - public CoordTriplet getReferenceCoord() { - if(referenceCoord == null) { selectNewReferenceCoord(); } - return referenceCoord; - } - - /** - * @return The number of blocks connected to this controller. - */ - public int getNumConnectedBlocks() { return connectedParts.size(); } - - public abstract void writeToNBT(NBTTagCompound data); - public abstract void readFromNBT(NBTTagCompound data); - - /** - * Force this multiblock to recalculate its minimum and maximum coordinates - * from the list of connected parts. - */ - public void recalculateMinMaxCoords() { - minimumCoord = new CoordTriplet(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE); - maximumCoord = new CoordTriplet(Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE); - - for(IMultiblockPart part : connectedParts) { - if(part.xCoord < minimumCoord.x) { minimumCoord.x = part.xCoord; } - if(part.xCoord > maximumCoord.x) { maximumCoord.x = part.xCoord; } - if(part.yCoord < minimumCoord.y) { minimumCoord.y = part.yCoord; } - if(part.yCoord > maximumCoord.y) { maximumCoord.y = part.yCoord; } - if(part.zCoord < minimumCoord.z) { minimumCoord.z = part.zCoord; } - if(part.zCoord > maximumCoord.z) { maximumCoord.z = part.zCoord; } - } - } - - /** - * @return The minimum bounding-box coordinate containing this machine's blocks. - */ - public CoordTriplet getMinimumCoord() { - if(minimumCoord == null) { recalculateMinMaxCoords(); } - return minimumCoord.copy(); - } - - /** - * @return The maximum bounding-box coordinate containing this machine's blocks. - */ - public CoordTriplet getMaximumCoord() { - if(maximumCoord == null) { recalculateMinMaxCoords(); } - return maximumCoord.copy(); - } - - /** - * Called when the save delegate's tile entity is being asked for its description packet - * @param data A fresh compound tag to write your multiblock data into - */ - public abstract void formatDescriptionPacket(NBTTagCompound data); - - /** - * Called when the save delegate's tile entity receiving a description packet - * @param data A compound tag containing multiblock data to import - */ - public abstract void decodeDescriptionPacket(NBTTagCompound data); - - /** - * @return True if this controller has no associated blocks, false otherwise - */ - public boolean isEmpty() { - return connectedParts.isEmpty(); - } - - /** - * Tests whether this multiblock should consume the other multiblock - * and become the new multiblock master when the two multiblocks - * are adjacent. Assumes both multiblocks are the same type. - * @param otherController The other multiblock controller. - * @return True if this multiblock should consume the other, false otherwise. - */ - public boolean shouldConsume(MultiblockControllerBase otherController) { - if(!otherController.getClass().equals(getClass())) { - throw new IllegalArgumentException("Attempting to merge two multiblocks with different master classes - this should never happen!"); - } - - if(otherController == this) { return false; } // Don't be silly, don't eat yourself. - - int res = _shouldConsume(otherController); - if(res < 0) { return true; } - else if(res > 0) { return false; } - else { - // Strip dead parts from both and retry - BeefCoreLog.warning("[%s] Encountered two controllers with the same reference coordinate. Auditing connected parts and retrying.", worldObj.isRemote?"CLIENT":"SERVER"); - auditParts(); - otherController.auditParts(); - - res = _shouldConsume(otherController); - if(res < 0) { return true; } - else if(res > 0) { return false; } - else { - BeefCoreLog.error("My Controller (%d): size (%d), parts: %s", hashCode(), connectedParts.size(), getPartsListString()); - BeefCoreLog.error("Other Controller (%d): size (%d), coords: %s", otherController.hashCode(), otherController.connectedParts.size(), otherController.getPartsListString()); - throw new IllegalArgumentException("[" + (worldObj.isRemote?"CLIENT":"SERVER") + "] Two controllers with the same reference coord that somehow both have valid parts - this should never happen!"); - } - - } - } - - private int _shouldConsume(MultiblockControllerBase otherController) { - CoordTriplet myCoord = getReferenceCoord(); - CoordTriplet theirCoord = otherController.getReferenceCoord(); - - // Always consume other controllers if their reference coordinate is null - this means they're empty and can be assimilated on the cheap - if(theirCoord == null) { return -1; } - else { return myCoord.compareTo(theirCoord); } - } - - private String getPartsListString() { - StringBuilder sb = new StringBuilder(); - boolean first = true; - for(IMultiblockPart part : connectedParts) { - if(!first) { - sb.append(", "); - } - sb.append(String.format("(%d: %d, %d, %d)", part.hashCode(), part.xCoord, part.yCoord, part.zCoord)); - first = false; - } - - return sb.toString(); - } - - /** - * Checks all of the parts in the controller. If any are dead or do not exist in the world, they are removed. - */ - private void auditParts() { - HashSet deadParts = new HashSet(); - for(IMultiblockPart part : connectedParts) { - if(part.isInvalid() || worldObj.getTileEntity(part.xCoord, part.yCoord, part.zCoord) != part) { - onDetachBlock(part); - deadParts.add(part); - } - } - - connectedParts.removeAll(deadParts); - BeefCoreLog.warning("[%s] Controller found %d dead parts during an audit, %d parts remain attached", worldObj.isRemote?"CLIENT":"SERVER", deadParts.size(), connectedParts.size()); - } - - /** - * Called when this machine may need to check for blocks that are no - * longer physically connected to the reference coordinate. - * @return - */ - public Set checkForDisconnections() { - if(!this.shouldCheckForDisconnections) { - return null; - } - - if(this.isEmpty()) { - MultiblockRegistry.addDeadController(worldObj, this); - return null; - } - - TileEntity te; - IChunkProvider chunkProvider = worldObj.getChunkProvider(); - - // Invalidate our reference coord, we'll recalculate it shortly - referenceCoord = null; - - // Reset visitations and find the minimum coordinate - Set deadParts = new HashSet(); - CoordTriplet c; - IMultiblockPart referencePart = null; - - int originalSize = connectedParts.size(); - - for(IMultiblockPart part : connectedParts) { - // This happens during chunk unload. - if(!chunkProvider.chunkExists(part.xCoord >> 4, part.zCoord >> 4) || part.isInvalid()) { - deadParts.add(part); - onDetachBlock(part); - continue; - } - - if(worldObj.getTileEntity(part.xCoord, part.yCoord, part.zCoord) != part) { - deadParts.add(part); - onDetachBlock(part); - continue; - } - - part.setUnvisited(); - part.forfeitMultiblockSaveDelegate(); - - c = part.getWorldLocation(); - if(referenceCoord == null) { - referenceCoord = c; - referencePart = part; - } - else if(c.compareTo(referenceCoord) < 0) { - referenceCoord = c; - referencePart = part; - } - } - - connectedParts.removeAll(deadParts); - deadParts.clear(); - - if(referencePart == null || isEmpty()) { - // There are no valid parts remaining. The entire multiblock was unloaded during a chunk unload. Halt. - shouldCheckForDisconnections = false; - MultiblockRegistry.addDeadController(worldObj, this); - return null; - } - else { - referencePart.becomeMultiblockSaveDelegate(); - } - - // Now visit all connected parts, breadth-first, starting from reference coord's part - IMultiblockPart part; - LinkedList partsToCheck = new LinkedList(); - IMultiblockPart[] nearbyParts = null; - int visitedParts = 0; - - partsToCheck.add(referencePart); - - while(!partsToCheck.isEmpty()) { - part = partsToCheck.removeFirst(); - part.setVisited(); - visitedParts++; - - nearbyParts = part.getNeighboringParts(); // Chunk-safe on server, but not on client - for(IMultiblockPart nearbyPart : nearbyParts) { - // Ignore different machines - if(nearbyPart.getMultiblockController() != this) { - continue; - } - - if(!nearbyPart.isVisited()) { - nearbyPart.setVisited(); - partsToCheck.add(nearbyPart); - } - } - } - - // Finally, remove all parts that remain disconnected. - Set removedParts = new HashSet(); - for(IMultiblockPart orphanCandidate : connectedParts) { - if (!orphanCandidate.isVisited()) { - deadParts.add(orphanCandidate); - orphanCandidate.onOrphaned(this, originalSize, visitedParts); - onDetachBlock(orphanCandidate); - removedParts.add(orphanCandidate); - } - } - - // Trim any blocks that were invalid, or were removed. - connectedParts.removeAll(deadParts); - - // Cleanup. Not necessary, really. - deadParts.clear(); - - // Juuuust in case. - if(referenceCoord == null) { - selectNewReferenceCoord(); - } - - // We've run the checks from here on out. - shouldCheckForDisconnections = false; - - return removedParts; - } - - /** - * Detach all parts. Return a set of all parts which still - * have a valid tile entity. Chunk-safe. - * @return A set of all parts which still have a valid tile entity. - */ - public Set detachAllBlocks() { - if(worldObj == null) { return new HashSet(); } - - IChunkProvider chunkProvider = worldObj.getChunkProvider(); - for(IMultiblockPart part : connectedParts) { - if(chunkProvider.chunkExists(part.xCoord >> 4, part.zCoord >> 4)) { - onDetachBlock(part); - } - } - - Set detachedParts = connectedParts; - connectedParts = new HashSet(); - return detachedParts; - } - - /** - * @return True if this multiblock machine is considered assembled and ready to go. - */ - public boolean isAssembled() { - return this.assemblyState == AssemblyState.Assembled; - } - - private void selectNewReferenceCoord() { - IChunkProvider chunkProvider = worldObj.getChunkProvider(); - TileEntity theChosenOne = null; - referenceCoord = null; - - for(IMultiblockPart part : connectedParts) { - if(part.isInvalid() || !chunkProvider.chunkExists(part.xCoord >> 4, part.zCoord >> 4)) { - // Chunk is unloading, skip this coord to prevent chunk thrashing - continue; - } - - if(referenceCoord == null || referenceCoord.compareTo(part.xCoord, part.yCoord, part.zCoord) > 0) { - referenceCoord = part.getWorldLocation(); - theChosenOne = part; - } - } - - if(theChosenOne != null) { - ((IMultiblockPart)theChosenOne).becomeMultiblockSaveDelegate(); - } - } - - /** - * Marks the reference coord dirty & updateable. - * - * On the server, this will mark the for a data-update, so that - * nearby clients will receive an updated description packet from the server - * after a short time. The block's chunk will also be marked dirty and the - * block's chunk will be saved to disk the next time chunks are saved. - * - * On the client, this will mark the block for a rendering update. - */ - protected void markReferenceCoordForUpdate() { - CoordTriplet rc = getReferenceCoord(); - if(worldObj != null && rc != null) { - worldObj.markBlockForUpdate(rc.x, rc.y, rc.z); - } - } - - /** - * Marks the reference coord dirty. - * - * On the server, this marks the reference coord's chunk as dirty; the block (and chunk) - * will be saved to disk the next time chunks are saved. This does NOT mark it dirty for - * a description-packet update. - * - * On the client, does nothing. - * @see MultiblockControllerBase#markReferenceCoordForUpdate() - */ - protected void markReferenceCoordDirty() { - if(worldObj == null || worldObj.isRemote) { return; } - - CoordTriplet referenceCoord = getReferenceCoord(); - if(referenceCoord == null) { return; } - - TileEntity saveTe = worldObj.getTileEntity(referenceCoord.x, referenceCoord.y, referenceCoord.z); - worldObj.markTileEntityChunkModified(referenceCoord.x, referenceCoord.y, referenceCoord.z, saveTe); - } - - -} \ No newline at end of file + + public static final short DIMENSION_UNBOUNDED = -1; + + // Multiblock stuff - do not mess with + protected World worldObj; + + // Disassembled -> Assembled; Assembled -> Disassembled OR Paused; Paused -> Assembled + protected enum AssemblyState { + Disassembled, + Assembled, + Paused + }; + + protected AssemblyState assemblyState; + + protected HashSet connectedParts; + + /** + * This is a deterministically-picked coordinate that identifies this + * multiblock uniquely in its dimension. + * Currently, this is the coord with the lowest X, Y and Z coordinates, in that order of evaluation. + * i.e. If something has a lower X but higher Y/Z coordinates, it will still be the reference. + * If something has the same X but a lower Y coordinate, it will be the reference. Etc. + */ + private CoordTriplet referenceCoord; + + /** + * Minimum bounding box coordinate. Blocks do not necessarily exist at this coord if your machine + * is not a cube/rectangular prism. + */ + private CoordTriplet minimumCoord; + + /** + * Maximum bounding box coordinate. Blocks do not necessarily exist at this coord if your machine + * is not a cube/rectangular prism. + */ + private CoordTriplet maximumCoord; + + /** + * Set to true whenever a part is removed from this controller. + */ + private boolean shouldCheckForDisconnections; + + /** + * Set whenever we validate the multiblock + */ + private MultiblockValidationException lastValidationException; + + protected boolean debugMode; + + protected MultiblockControllerBase(World world) { + // Multiblock stuff + worldObj = world; + connectedParts = new HashSet(); + + referenceCoord = null; + assemblyState = AssemblyState.Disassembled; + + minimumCoord = null; + maximumCoord = null; + + shouldCheckForDisconnections = true; + lastValidationException = null; + + debugMode = false; + } + + public void setDebugMode(boolean active) { + debugMode = active; + } + + public boolean isDebugMode() { + return debugMode; + } + + /** + * Call when a block with cached save-delegate data is added to the multiblock. + * The part will be notified that the data has been used after this call completes. + * + * @param part The NBT tag containing this controller's data. + */ + public abstract void onAttachedPartWithMultiblockData(IMultiblockPart part, NBTTagCompound data); + + /** + * Check if a block is being tracked by this machine. + * + * @param blockCoord Coordinate to check. + * @return True if the tile entity at blockCoord is being tracked by this machine, false otherwise. + */ + public boolean hasBlock(CoordTriplet blockCoord) { + return connectedParts.contains(blockCoord); + } + + /** + * Attach a new part to this machine. + * + * @param part The part to add. + */ + public void attachBlock(IMultiblockPart part) { + IMultiblockPart candidate; + CoordTriplet coord = part.getWorldLocation(); + + if (!connectedParts.add(part)) { + BeefCoreLog.warning( + "[%s] Controller %s is double-adding part %d @ %s. This is unusual. If you encounter odd behavior, please tear down the machine and rebuild it.", + (worldObj.isRemote ? "CLIENT" : "SERVER"), + hashCode(), + part.hashCode(), + coord); + } + + part.onAttached(this); + this.onBlockAdded(part); + + if (part.hasMultiblockSaveData()) { + NBTTagCompound savedData = part.getMultiblockSaveData(); + onAttachedPartWithMultiblockData(part, savedData); + part.onMultiblockDataAssimilated(); + } + + if (this.referenceCoord == null) { + referenceCoord = coord; + part.becomeMultiblockSaveDelegate(); + } else if (coord.compareTo(referenceCoord) < 0) { + TileEntity te = this.worldObj.getTileEntity(referenceCoord.x, referenceCoord.y, referenceCoord.z); + ((IMultiblockPart) te).forfeitMultiblockSaveDelegate(); + + referenceCoord = coord; + part.becomeMultiblockSaveDelegate(); + } else { + part.forfeitMultiblockSaveDelegate(); + } + + if (minimumCoord != null) { + if (part.xCoord < minimumCoord.x) { + minimumCoord.x = part.xCoord; + } + if (part.yCoord < minimumCoord.y) { + minimumCoord.y = part.yCoord; + } + if (part.zCoord < minimumCoord.z) { + minimumCoord.z = part.zCoord; + } + } + + if (maximumCoord != null) { + if (part.xCoord > maximumCoord.x) { + maximumCoord.x = part.xCoord; + } + if (part.yCoord > maximumCoord.y) { + maximumCoord.y = part.yCoord; + } + if (part.zCoord > maximumCoord.z) { + maximumCoord.z = part.zCoord; + } + } + + MultiblockRegistry.addDirtyController(worldObj, this); + } + + /** + * Called when a new part is added to the machine. Good time to register things into lists. + * + * @param newPart The part being added. + */ + protected abstract void onBlockAdded(IMultiblockPart newPart); + + /** + * Called when a part is removed from the machine. Good time to clean up lists. + * + * @param oldPart The part being removed. + */ + protected abstract void onBlockRemoved(IMultiblockPart oldPart); + + /** + * Called when a machine is assembled from a disassembled state. + */ + protected abstract void onMachineAssembled(); + + /** + * Called when a machine is restored to the assembled state from a paused state. + */ + protected abstract void onMachineRestored(); + + /** + * Called when a machine is paused from an assembled state + * This generally only happens due to chunk-loads and other "system" events. + */ + protected abstract void onMachinePaused(); + + /** + * Called when a machine is disassembled from an assembled state. + * This happens due to user or in-game actions (e.g. explosions) + */ + protected abstract void onMachineDisassembled(); + + /** + * Callback whenever a part is removed (or will very shortly be removed) from a controller. + * Do housekeeping/callbacks, also nulls min/max coords. + * + * @param part The part being removed. + */ + private void onDetachBlock(IMultiblockPart part) { + // Strip out this part + part.onDetached(this); + this.onBlockRemoved(part); + part.forfeitMultiblockSaveDelegate(); + + minimumCoord = maximumCoord = null; + + if (referenceCoord != null && referenceCoord.equals(part.xCoord, part.yCoord, part.zCoord)) { + referenceCoord = null; + } + + shouldCheckForDisconnections = true; + } + + /** + * Call to detach a block from this machine. Generally, this should be called + * when the tile entity is being released, e.g. on block destruction. + * + * @param part The part to detach from this machine. + * @param chunkUnloading Is this entity detaching due to the chunk unloading? If true, the multiblock will be paused + * instead of broken. + */ + public void detachBlock(IMultiblockPart part, boolean chunkUnloading) { + if (chunkUnloading && this.assemblyState == AssemblyState.Assembled) { + this.assemblyState = AssemblyState.Paused; + this.onMachinePaused(); + } + + // Strip out this part + onDetachBlock(part); + if (!connectedParts.remove(part)) { + BeefCoreLog.warning( + "[%s] Double-removing part (%d) @ %d, %d, %d, this is unexpected and may cause problems. If you encounter anomalies, please tear down the reactor and rebuild it.", + worldObj.isRemote ? "CLIENT" : "SERVER", + part.hashCode(), + part.xCoord, + part.yCoord, + part.zCoord); + } + + if (connectedParts.isEmpty()) { + // Destroy/unregister + MultiblockRegistry.addDeadController(this.worldObj, this); + return; + } + + MultiblockRegistry.addDirtyController(this.worldObj, this); + + // Find new save delegate if we need to. + if (referenceCoord == null) { + selectNewReferenceCoord(); + } + } + + /** + * Helper method so we don't check for a whole machine until we have enough blocks + * to actually assemble it. This isn't as simple as xmax*ymax*zmax for non-cubic machines + * or for machines with hollow/complex interiors. + * + * @return The minimum number of blocks connected to the machine for it to be assembled. + */ + protected abstract int getMinimumNumberOfBlocksForAssembledMachine(); + + /** + * Returns the maximum X dimension size of the machine, or -1 (DIMENSION_UNBOUNDED) to disable + * dimension checking in X. (This is not recommended.) + * + * @return The maximum X dimension size of the machine, or -1 + */ + protected abstract int getMaximumXSize(); + + /** + * Returns the maximum Z dimension size of the machine, or -1 (DIMENSION_UNBOUNDED) to disable + * dimension checking in X. (This is not recommended.) + * + * @return The maximum Z dimension size of the machine, or -1 + */ + protected abstract int getMaximumZSize(); + + /** + * Returns the maximum Y dimension size of the machine, or -1 (DIMENSION_UNBOUNDED) to disable + * dimension checking in X. (This is not recommended.) + * + * @return The maximum Y dimension size of the machine, or -1 + */ + protected abstract int getMaximumYSize(); + + /** + * Returns the minimum X dimension size of the machine. Must be at least 1, because nothing else makes sense. + * + * @return The minimum X dimension size of the machine + */ + protected int getMinimumXSize() { + return 1; + } + + /** + * Returns the minimum Y dimension size of the machine. Must be at least 1, because nothing else makes sense. + * + * @return The minimum Y dimension size of the machine + */ + protected int getMinimumYSize() { + return 1; + } + + /** + * Returns the minimum Z dimension size of the machine. Must be at least 1, because nothing else makes sense. + * + * @return The minimum Z dimension size of the machine + */ + protected int getMinimumZSize() { + return 1; + } + + /** + * @return An exception representing the last error encountered when trying to assemble this + * multiblock, or null if there is no error. + */ + public MultiblockValidationException getLastValidationException() { + return lastValidationException; + } + + /** + * Checks if a machine is whole. If not, throws an exception with the reason why. + */ + protected abstract void isMachineWhole() throws MultiblockValidationException; + + /** + * Check if the machine is whole or not. + * If the machine was not whole, but now is, assemble the machine. + * If the machine was whole, but no longer is, disassemble the machine. + * + * @return + */ + public void checkIfMachineIsWhole() { + AssemblyState oldState = this.assemblyState; + boolean isWhole; + lastValidationException = null; + try { + isMachineWhole(); + isWhole = true; + } catch (MultiblockValidationException e) { + lastValidationException = e; + isWhole = false; + } + + if (isWhole) { + // This will alter assembly state + assembleMachine(oldState); + } else if (oldState == AssemblyState.Assembled) { + // This will alter assembly state + disassembleMachine(); + } + // Else Paused, do nothing + } + + /** + * Called when a machine becomes "whole" and should begin + * functioning as a game-logically finished machine. + * Calls onMachineAssembled on all attached parts. + */ + private void assembleMachine(AssemblyState oldState) { + for (IMultiblockPart part : connectedParts) { + part.onMachineAssembled(this); + } + + this.assemblyState = AssemblyState.Assembled; + if (oldState == assemblyState.Paused) { + onMachineRestored(); + } else { + onMachineAssembled(); + } + } + + /** + * Called when the machine needs to be disassembled. + * It is not longer "whole" and should not be functional, usually + * as a result of a block being removed. + * Calls onMachineBroken on all attached parts. + */ + private void disassembleMachine() { + for (IMultiblockPart part : connectedParts) { + part.onMachineBroken(); + } + + this.assemblyState = AssemblyState.Disassembled; + onMachineDisassembled(); + } + + /** + * Assimilate another controller into this controller. + * Acquire all of the other controller's blocks and attach them + * to this one. + * + * @param other The controller to merge into this one. + */ + public void assimilate(MultiblockControllerBase other) { + CoordTriplet otherReferenceCoord = other.getReferenceCoord(); + if (otherReferenceCoord != null && getReferenceCoord().compareTo(otherReferenceCoord) >= 0) { + throw new IllegalArgumentException( + "The controller with the lowest minimum-coord value must consume the one with the higher coords"); + } + + TileEntity te; + Set partsToAcquire = new HashSet(other.connectedParts); + + // releases all blocks and references gently so they can be incorporated into another multiblock + other._onAssimilated(this); + + for (IMultiblockPart acquiredPart : partsToAcquire) { + // By definition, none of these can be the minimum block. + if (acquiredPart.isInvalid()) { + continue; + } + + connectedParts.add(acquiredPart); + acquiredPart.onAssimilated(this); + this.onBlockAdded(acquiredPart); + } + + this.onAssimilate(other); + other.onAssimilated(this); + } + + /** + * Called when this machine is consumed by another controller. + * Essentially, forcibly tear down this object. + * + * @param otherController The controller consuming this controller. + */ + private void _onAssimilated(MultiblockControllerBase otherController) { + if (referenceCoord != null) { + if (worldObj.getChunkProvider() + .chunkExists(referenceCoord.getChunkX(), referenceCoord.getChunkZ())) { + TileEntity te = this.worldObj.getTileEntity(referenceCoord.x, referenceCoord.y, referenceCoord.z); + if (te instanceof IMultiblockPart) { + ((IMultiblockPart) te).forfeitMultiblockSaveDelegate(); + } + } + this.referenceCoord = null; + } + + connectedParts.clear(); + } + + /** + * Callback. Called after this controller assimilates all the blocks + * from another controller. + * Use this to absorb that controller's game data. + * + * @param assimilated The controller whose uniqueness was added to our own. + */ + protected abstract void onAssimilate(MultiblockControllerBase assimilated); + + /** + * Callback. Called after this controller is assimilated into another controller. + * All blocks have been stripped out of this object and handed over to the + * other controller. + * This is intended primarily for cleanup. + * + * @param assimilator The controller which has assimilated this controller. + */ + protected abstract void onAssimilated(MultiblockControllerBase assimilator); + + /** + * Driver for the update loop. If the machine is assembled, runs + * the game logic update method. + * + * @see erogenousbeef.core.multiblock.MultiblockControllerBase#update() //TODO Fix this Javadoc + */ + public final void updateMultiblockEntity() { + if (connectedParts.isEmpty()) { + // This shouldn't happen, but just in case... + MultiblockRegistry.addDeadController(this.worldObj, this); + return; + } + + if (this.assemblyState != AssemblyState.Assembled) { + // Not assembled - don't run game logic + return; + } + + if (worldObj.isRemote) { + updateClient(); + } else if (updateServer()) { + // If this returns true, the server has changed its internal data. + // If our chunks are loaded (they should be), we must mark our chunks as dirty. + if (minimumCoord != null && maximumCoord != null + && this.worldObj.checkChunksExist( + minimumCoord.x, + minimumCoord.y, + minimumCoord.z, + maximumCoord.x, + maximumCoord.y, + maximumCoord.z)) { + int minChunkX = minimumCoord.x >> 4; + int minChunkZ = minimumCoord.z >> 4; + int maxChunkX = maximumCoord.x >> 4; + int maxChunkZ = maximumCoord.z >> 4; + + for (int x = minChunkX; x <= maxChunkX; x++) { + for (int z = minChunkZ; z <= maxChunkZ; z++) { + // Ensure that we save our data, even if the our save delegate is in has no TEs. + Chunk chunkToSave = this.worldObj.getChunkFromChunkCoords(x, z); + chunkToSave.setChunkModified(); + } + } + } + } + // Else: Server, but no need to save data. + } + + /** + * The server-side update loop! Use this similarly to a TileEntity's update loop. + * You do not need to call your superclass' update() if you're directly + * derived from MultiblockControllerBase. This is a callback. + * Note that this will only be called when the machine is assembled. + * + * @return True if the multiblock should save data, i.e. its internal game state has changed. False otherwise. + */ + protected abstract boolean updateServer(); + + /** + * Client-side update loop. Generally, this shouldn't do anything, but if you want + * to do some interpolation or something, do it here. + */ + protected abstract void updateClient(); + + // Validation helpers + /** + * The "frame" consists of the outer edges of the machine, plus the corners. + * + * @param world World object for the world in which this controller is located. + * @param x X coordinate of the block being tested + * @param y Y coordinate of the block being tested + * @param z Z coordinate of the block being tested + * @throws MultiblockValidationException if the tested block is not allowed on the machine's frame + */ + protected void isBlockGoodForFrame(World world, int x, int y, int z) throws MultiblockValidationException { + throw new MultiblockValidationException( + String.format("%d, %d, %d - Block is not valid for use in the machine's interior", x, y, z)); + } + + /** + * The top consists of the top face, minus the edges. + * + * @param world World object for the world in which this controller is located. + * @param x X coordinate of the block being tested + * @param y Y coordinate of the block being tested + * @param z Z coordinate of the block being tested + * @throws MultiblockValidationException if the tested block is not allowed on the machine's top face + */ + protected void isBlockGoodForTop(World world, int x, int y, int z) throws MultiblockValidationException { + throw new MultiblockValidationException( + String.format("%d, %d, %d - Block is not valid for use in the machine's interior", x, y, z)); + } + + /** + * The bottom consists of the bottom face, minus the edges. + * + * @param world World object for the world in which this controller is located. + * @param x X coordinate of the block being tested + * @param y Y coordinate of the block being tested + * @param z Z coordinate of the block being tested + * @throws MultiblockValidationException if the tested block is not allowed on the machine's bottom face + */ + protected void isBlockGoodForBottom(World world, int x, int y, int z) throws MultiblockValidationException { + throw new MultiblockValidationException( + String.format("%d, %d, %d - Block is not valid for use in the machine's interior", x, y, z)); + } + + /** + * The sides consists of the N/E/S/W-facing faces, minus the edges. + * + * @param world World object for the world in which this controller is located. + * @param x X coordinate of the block being tested + * @param y Y coordinate of the block being tested + * @param z Z coordinate of the block being tested + * @throws MultiblockValidationException if the tested block is not allowed on the machine's side faces + */ + protected void isBlockGoodForSides(World world, int x, int y, int z) throws MultiblockValidationException { + throw new MultiblockValidationException( + String.format("%d, %d, %d - Block is not valid for use in the machine's interior", x, y, z)); + } + + /** + * The interior is any block that does not touch blocks outside the machine. + * + * @param world World object for the world in which this controller is located. + * @param x X coordinate of the block being tested + * @param y Y coordinate of the block being tested + * @param z Z coordinate of the block being tested + * @throws MultiblockValidationException if the tested block is not allowed in the machine's interior + */ + protected void isBlockGoodForInterior(World world, int x, int y, int z) throws MultiblockValidationException { + throw new MultiblockValidationException( + String.format("%d, %d, %d - Block is not valid for use in the machine's interior", x, y, z)); + } + + /** + * @return The reference coordinate, the block with the lowest x, y, z coordinates, evaluated in that order. + */ + public CoordTriplet getReferenceCoord() { + if (referenceCoord == null) { + selectNewReferenceCoord(); + } + return referenceCoord; + } + + /** + * @return The number of blocks connected to this controller. + */ + public int getNumConnectedBlocks() { + return connectedParts.size(); + } + + public abstract void writeToNBT(NBTTagCompound data); + + public abstract void readFromNBT(NBTTagCompound data); + + /** + * Force this multiblock to recalculate its minimum and maximum coordinates + * from the list of connected parts. + */ + public void recalculateMinMaxCoords() { + minimumCoord = new CoordTriplet(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE); + maximumCoord = new CoordTriplet(Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE); + + for (IMultiblockPart part : connectedParts) { + if (part.xCoord < minimumCoord.x) { + minimumCoord.x = part.xCoord; + } + if (part.xCoord > maximumCoord.x) { + maximumCoord.x = part.xCoord; + } + if (part.yCoord < minimumCoord.y) { + minimumCoord.y = part.yCoord; + } + if (part.yCoord > maximumCoord.y) { + maximumCoord.y = part.yCoord; + } + if (part.zCoord < minimumCoord.z) { + minimumCoord.z = part.zCoord; + } + if (part.zCoord > maximumCoord.z) { + maximumCoord.z = part.zCoord; + } + } + } + + /** + * @return The minimum bounding-box coordinate containing this machine's blocks. + */ + public CoordTriplet getMinimumCoord() { + if (minimumCoord == null) { + recalculateMinMaxCoords(); + } + return minimumCoord.copy(); + } + + /** + * @return The maximum bounding-box coordinate containing this machine's blocks. + */ + public CoordTriplet getMaximumCoord() { + if (maximumCoord == null) { + recalculateMinMaxCoords(); + } + return maximumCoord.copy(); + } + + /** + * Called when the save delegate's tile entity is being asked for its description packet + * + * @param data A fresh compound tag to write your multiblock data into + */ + public abstract void formatDescriptionPacket(NBTTagCompound data); + + /** + * Called when the save delegate's tile entity receiving a description packet + * + * @param data A compound tag containing multiblock data to import + */ + public abstract void decodeDescriptionPacket(NBTTagCompound data); + + /** + * @return True if this controller has no associated blocks, false otherwise + */ + public boolean isEmpty() { + return connectedParts.isEmpty(); + } + + /** + * Tests whether this multiblock should consume the other multiblock + * and become the new multiblock master when the two multiblocks + * are adjacent. Assumes both multiblocks are the same type. + * + * @param otherController The other multiblock controller. + * @return True if this multiblock should consume the other, false otherwise. + */ + public boolean shouldConsume(MultiblockControllerBase otherController) { + if (!otherController.getClass() + .equals(getClass())) { + throw new IllegalArgumentException( + "Attempting to merge two multiblocks with different master classes - this should never happen!"); + } + + if (otherController == this) { + return false; + } // Don't be silly, don't eat yourself. + + int res = _shouldConsume(otherController); + if (res < 0) { + return true; + } else if (res > 0) { + return false; + } else { + // Strip dead parts from both and retry + BeefCoreLog.warning( + "[%s] Encountered two controllers with the same reference coordinate. Auditing connected parts and retrying.", + worldObj.isRemote ? "CLIENT" : "SERVER"); + auditParts(); + otherController.auditParts(); + + res = _shouldConsume(otherController); + if (res < 0) { + return true; + } else if (res > 0) { + return false; + } else { + BeefCoreLog.error( + "My Controller (%d): size (%d), parts: %s", + hashCode(), + connectedParts.size(), + getPartsListString()); + BeefCoreLog.error( + "Other Controller (%d): size (%d), coords: %s", + otherController.hashCode(), + otherController.connectedParts.size(), + otherController.getPartsListString()); + throw new IllegalArgumentException( + "[" + (worldObj.isRemote ? "CLIENT" : "SERVER") + + "] Two controllers with the same reference coord that somehow both have valid parts - this should never happen!"); + } + + } + } + + private int _shouldConsume(MultiblockControllerBase otherController) { + CoordTriplet myCoord = getReferenceCoord(); + CoordTriplet theirCoord = otherController.getReferenceCoord(); + + // Always consume other controllers if their reference coordinate is null - this means they're empty and can be + // assimilated on the cheap + if (theirCoord == null) { + return -1; + } else { + return myCoord.compareTo(theirCoord); + } + } + + private String getPartsListString() { + StringBuilder sb = new StringBuilder(); + boolean first = true; + for (IMultiblockPart part : connectedParts) { + if (!first) { + sb.append(", "); + } + sb.append(String.format("(%d: %d, %d, %d)", part.hashCode(), part.xCoord, part.yCoord, part.zCoord)); + first = false; + } + + return sb.toString(); + } + + /** + * Checks all of the parts in the controller. If any are dead or do not exist in the world, they are removed. + */ + private void auditParts() { + HashSet deadParts = new HashSet(); + for (IMultiblockPart part : connectedParts) { + if (part.isInvalid() || worldObj.getTileEntity(part.xCoord, part.yCoord, part.zCoord) != part) { + onDetachBlock(part); + deadParts.add(part); + } + } + + connectedParts.removeAll(deadParts); + BeefCoreLog.warning( + "[%s] Controller found %d dead parts during an audit, %d parts remain attached", + worldObj.isRemote ? "CLIENT" : "SERVER", + deadParts.size(), + connectedParts.size()); + } + + /** + * Called when this machine may need to check for blocks that are no + * longer physically connected to the reference coordinate. + * + * @return + */ + public Set checkForDisconnections() { + if (!this.shouldCheckForDisconnections) { + return null; + } + + if (this.isEmpty()) { + MultiblockRegistry.addDeadController(worldObj, this); + return null; + } + + TileEntity te; + IChunkProvider chunkProvider = worldObj.getChunkProvider(); + + // Invalidate our reference coord, we'll recalculate it shortly + referenceCoord = null; + + // Reset visitations and find the minimum coordinate + Set deadParts = new HashSet(); + CoordTriplet c; + IMultiblockPart referencePart = null; + + int originalSize = connectedParts.size(); + + for (IMultiblockPart part : connectedParts) { + // This happens during chunk unload. + if (!chunkProvider.chunkExists(part.xCoord >> 4, part.zCoord >> 4) || part.isInvalid()) { + deadParts.add(part); + onDetachBlock(part); + continue; + } + + if (worldObj.getTileEntity(part.xCoord, part.yCoord, part.zCoord) != part) { + deadParts.add(part); + onDetachBlock(part); + continue; + } + + part.setUnvisited(); + part.forfeitMultiblockSaveDelegate(); + + c = part.getWorldLocation(); + if (referenceCoord == null) { + referenceCoord = c; + referencePart = part; + } else if (c.compareTo(referenceCoord) < 0) { + referenceCoord = c; + referencePart = part; + } + } + + connectedParts.removeAll(deadParts); + deadParts.clear(); + + if (referencePart == null || isEmpty()) { + // There are no valid parts remaining. The entire multiblock was unloaded during a chunk unload. Halt. + shouldCheckForDisconnections = false; + MultiblockRegistry.addDeadController(worldObj, this); + return null; + } else { + referencePart.becomeMultiblockSaveDelegate(); + } + + // Now visit all connected parts, breadth-first, starting from reference coord's part + IMultiblockPart part; + LinkedList partsToCheck = new LinkedList(); + IMultiblockPart[] nearbyParts = null; + int visitedParts = 0; + + partsToCheck.add(referencePart); + + while (!partsToCheck.isEmpty()) { + part = partsToCheck.removeFirst(); + part.setVisited(); + visitedParts++; + + nearbyParts = part.getNeighboringParts(); // Chunk-safe on server, but not on client + for (IMultiblockPart nearbyPart : nearbyParts) { + // Ignore different machines + if (nearbyPart.getMultiblockController() != this) { + continue; + } + + if (!nearbyPart.isVisited()) { + nearbyPart.setVisited(); + partsToCheck.add(nearbyPart); + } + } + } + + // Finally, remove all parts that remain disconnected. + Set removedParts = new HashSet(); + for (IMultiblockPart orphanCandidate : connectedParts) { + if (!orphanCandidate.isVisited()) { + deadParts.add(orphanCandidate); + orphanCandidate.onOrphaned(this, originalSize, visitedParts); + onDetachBlock(orphanCandidate); + removedParts.add(orphanCandidate); + } + } + + // Trim any blocks that were invalid, or were removed. + connectedParts.removeAll(deadParts); + + // Cleanup. Not necessary, really. + deadParts.clear(); + + // Juuuust in case. + if (referenceCoord == null) { + selectNewReferenceCoord(); + } + + // We've run the checks from here on out. + shouldCheckForDisconnections = false; + + return removedParts; + } + + /** + * Detach all parts. Return a set of all parts which still + * have a valid tile entity. Chunk-safe. + * + * @return A set of all parts which still have a valid tile entity. + */ + public Set detachAllBlocks() { + if (worldObj == null) { + return new HashSet(); + } + + IChunkProvider chunkProvider = worldObj.getChunkProvider(); + for (IMultiblockPart part : connectedParts) { + if (chunkProvider.chunkExists(part.xCoord >> 4, part.zCoord >> 4)) { + onDetachBlock(part); + } + } + + Set detachedParts = connectedParts; + connectedParts = new HashSet(); + return detachedParts; + } + + /** + * @return True if this multiblock machine is considered assembled and ready to go. + */ + public boolean isAssembled() { + return this.assemblyState == AssemblyState.Assembled; + } + + private void selectNewReferenceCoord() { + IChunkProvider chunkProvider = worldObj.getChunkProvider(); + TileEntity theChosenOne = null; + referenceCoord = null; + + for (IMultiblockPart part : connectedParts) { + if (part.isInvalid() || !chunkProvider.chunkExists(part.xCoord >> 4, part.zCoord >> 4)) { + // Chunk is unloading, skip this coord to prevent chunk thrashing + continue; + } + + if (referenceCoord == null || referenceCoord.compareTo(part.xCoord, part.yCoord, part.zCoord) > 0) { + referenceCoord = part.getWorldLocation(); + theChosenOne = part; + } + } + + if (theChosenOne != null) { + ((IMultiblockPart) theChosenOne).becomeMultiblockSaveDelegate(); + } + } + + /** + * Marks the reference coord dirty & updateable. + * + * On the server, this will mark the for a data-update, so that + * nearby clients will receive an updated description packet from the server + * after a short time. The block's chunk will also be marked dirty and the + * block's chunk will be saved to disk the next time chunks are saved. + * + * On the client, this will mark the block for a rendering update. + */ + protected void markReferenceCoordForUpdate() { + CoordTriplet rc = getReferenceCoord(); + if (worldObj != null && rc != null) { + worldObj.markBlockForUpdate(rc.x, rc.y, rc.z); + } + } + + /** + * Marks the reference coord dirty. + * + * On the server, this marks the reference coord's chunk as dirty; the block (and chunk) + * will be saved to disk the next time chunks are saved. This does NOT mark it dirty for + * a description-packet update. + * + * On the client, does nothing. + * + * @see MultiblockControllerBase#markReferenceCoordForUpdate() + */ + protected void markReferenceCoordDirty() { + if (worldObj == null || worldObj.isRemote) { + return; + } + + CoordTriplet referenceCoord = getReferenceCoord(); + if (referenceCoord == null) { + return; + } + + TileEntity saveTe = worldObj.getTileEntity(referenceCoord.x, referenceCoord.y, referenceCoord.z); + worldObj.markTileEntityChunkModified(referenceCoord.x, referenceCoord.y, referenceCoord.z, saveTe); + } + +} diff --git a/src/main/java/erogenousbeef/core/multiblock/MultiblockEventHandler.java b/src/main/java/erogenousbeef/core/multiblock/MultiblockEventHandler.java index 57e230f..3b8733b 100644 --- a/src/main/java/erogenousbeef/core/multiblock/MultiblockEventHandler.java +++ b/src/main/java/erogenousbeef/core/multiblock/MultiblockEventHandler.java @@ -4,6 +4,7 @@ import net.minecraft.world.chunk.Chunk; import net.minecraftforge.event.world.ChunkEvent; import net.minecraftforge.event.world.WorldEvent; + import cpw.mods.fml.common.eventhandler.EventPriority; import cpw.mods.fml.common.eventhandler.SubscribeEvent; @@ -14,16 +15,17 @@ * process any blocks that are in chunks which are still loading. */ public class MultiblockEventHandler { - @SubscribeEvent(priority = EventPriority.NORMAL) - public void onChunkLoad(ChunkEvent.Load loadEvent) { - Chunk chunk = loadEvent.getChunk(); - World world = loadEvent.world; - MultiblockRegistry.onChunkLoaded(world, chunk.xPosition, chunk.zPosition); - } - // Cleanup, for nice memory usageness - @SubscribeEvent(priority = EventPriority.NORMAL) - public void onWorldUnload(WorldEvent.Unload unloadWorldEvent) { - MultiblockRegistry.onWorldUnloaded(unloadWorldEvent.world); - } + @SubscribeEvent(priority = EventPriority.NORMAL) + public void onChunkLoad(ChunkEvent.Load loadEvent) { + Chunk chunk = loadEvent.getChunk(); + World world = loadEvent.world; + MultiblockRegistry.onChunkLoaded(world, chunk.xPosition, chunk.zPosition); + } + + // Cleanup, for nice memory usageness + @SubscribeEvent(priority = EventPriority.NORMAL) + public void onWorldUnload(WorldEvent.Unload unloadWorldEvent) { + MultiblockRegistry.onWorldUnloaded(unloadWorldEvent.world); + } } diff --git a/src/main/java/erogenousbeef/core/multiblock/MultiblockRegistry.java b/src/main/java/erogenousbeef/core/multiblock/MultiblockRegistry.java index 9dd6ed1..7ce4a04 100644 --- a/src/main/java/erogenousbeef/core/multiblock/MultiblockRegistry.java +++ b/src/main/java/erogenousbeef/core/multiblock/MultiblockRegistry.java @@ -4,128 +4,143 @@ import java.util.Set; import net.minecraft.world.World; + import erogenousbeef.core.common.BeefCoreLog; /** * This is a very static singleton registry class which directs incoming events to sub-objects, which * actually manage each individual world's multiblocks. + * * @author Erogenous Beef */ public class MultiblockRegistry { - // World > WorldRegistry map - private static HashMap registries = new HashMap(); - - /** - * Called before Tile Entities are ticked in the world. Do bookkeeping here. - * @param world The world being ticked - */ - public static void tickStart(World world) { - if(registries.containsKey(world)) { - MultiblockWorldRegistry registry = registries.get(world); - registry.processMultiblockChanges(); - registry.tickStart(); - } - } - - /** - * Called when the world has finished loading a chunk. - * @param world The world which has finished loading a chunk - * @param chunkX The X coordinate of the chunk - * @param chunkZ The Z coordinate of the chunk - */ - public static void onChunkLoaded(World world, int chunkX, int chunkZ) { - if(registries.containsKey(world)) { - registries.get(world).onChunkLoaded(chunkX, chunkZ); - } - } - - /** - * Register a new part in the system. The part has been created either through user action or via a chunk loading. - * @param world The world into which this part is loading. - * @param part The part being loaded. - */ - public static void onPartAdded(World world, IMultiblockPart part) { - MultiblockWorldRegistry registry = getOrCreateRegistry(world); - registry.onPartAdded(part); - } - - /** - * Call to remove a part from world lists. - * @param world The world from which a multiblock part is being removed. - * @param part The part being removed. - */ - public static void onPartRemovedFromWorld(World world, IMultiblockPart part) { - if(registries.containsKey(world)) { - registries.get(world).onPartRemovedFromWorld(part); - } - - } - - - /** - * Called whenever a world is unloaded. Unload the relevant registry, if we have one. - * @param world The world being unloaded. - */ - public static void onWorldUnloaded(World world) { - if(registries.containsKey(world)) { - registries.get(world).onWorldUnloaded(); - registries.remove(world); - } - } - - /** - * Call to mark a controller as dirty. Dirty means that parts have - * been added or removed this tick. - * @param world The world containing the multiblock - * @param controller The dirty controller - */ - public static void addDirtyController(World world, - MultiblockControllerBase controller) { - if(registries.containsKey(world)) { - registries.get(world).addDirtyController(controller); - } - else { - throw new IllegalArgumentException("Adding a dirty controller to a world that has no registered controllers!"); - } - } - - /** - * Call to mark a controller as dead. It should only be marked as dead - * when it has no connected parts. It will be removed after the next world tick. - * @param world The world formerly containing the multiblock - * @param controller The dead controller - */ - public static void addDeadController(World world, MultiblockControllerBase controller) { - if(registries.containsKey(world)) { - registries.get(world).addDeadController(controller); - } - else { - BeefCoreLog.warning("Controller %d in world %s marked as dead, but that world is not tracked! Controller is being ignored.", controller.hashCode(), world); - } - } - - /** - * @param world The world whose controllers you wish to retrieve. - * @return An unmodifiable set of controllers active in the given world, or null if there are none. - */ - public static Set getControllersFromWorld(World world) { - if(registries.containsKey(world)) { - return registries.get(world).getControllers(); - } - return null; - } - - /// *** PRIVATE HELPERS *** /// - - private static MultiblockWorldRegistry getOrCreateRegistry(World world) { - if(registries.containsKey(world)) { - return registries.get(world); - } - else { - MultiblockWorldRegistry newRegistry = new MultiblockWorldRegistry(world); - registries.put(world, newRegistry); - return newRegistry; - } - } + + // World > WorldRegistry map + private static HashMap registries = new HashMap(); + + /** + * Called before Tile Entities are ticked in the world. Do bookkeeping here. + * + * @param world The world being ticked + */ + public static void tickStart(World world) { + if (registries.containsKey(world)) { + MultiblockWorldRegistry registry = registries.get(world); + registry.processMultiblockChanges(); + registry.tickStart(); + } + } + + /** + * Called when the world has finished loading a chunk. + * + * @param world The world which has finished loading a chunk + * @param chunkX The X coordinate of the chunk + * @param chunkZ The Z coordinate of the chunk + */ + public static void onChunkLoaded(World world, int chunkX, int chunkZ) { + if (registries.containsKey(world)) { + registries.get(world) + .onChunkLoaded(chunkX, chunkZ); + } + } + + /** + * Register a new part in the system. The part has been created either through user action or via a chunk loading. + * + * @param world The world into which this part is loading. + * @param part The part being loaded. + */ + public static void onPartAdded(World world, IMultiblockPart part) { + MultiblockWorldRegistry registry = getOrCreateRegistry(world); + registry.onPartAdded(part); + } + + /** + * Call to remove a part from world lists. + * + * @param world The world from which a multiblock part is being removed. + * @param part The part being removed. + */ + public static void onPartRemovedFromWorld(World world, IMultiblockPart part) { + if (registries.containsKey(world)) { + registries.get(world) + .onPartRemovedFromWorld(part); + } + + } + + /** + * Called whenever a world is unloaded. Unload the relevant registry, if we have one. + * + * @param world The world being unloaded. + */ + public static void onWorldUnloaded(World world) { + if (registries.containsKey(world)) { + registries.get(world) + .onWorldUnloaded(); + registries.remove(world); + } + } + + /** + * Call to mark a controller as dirty. Dirty means that parts have + * been added or removed this tick. + * + * @param world The world containing the multiblock + * @param controller The dirty controller + */ + public static void addDirtyController(World world, MultiblockControllerBase controller) { + if (registries.containsKey(world)) { + registries.get(world) + .addDirtyController(controller); + } else { + throw new IllegalArgumentException( + "Adding a dirty controller to a world that has no registered controllers!"); + } + } + + /** + * Call to mark a controller as dead. It should only be marked as dead + * when it has no connected parts. It will be removed after the next world tick. + * + * @param world The world formerly containing the multiblock + * @param controller The dead controller + */ + public static void addDeadController(World world, MultiblockControllerBase controller) { + if (registries.containsKey(world)) { + registries.get(world) + .addDeadController(controller); + } else { + BeefCoreLog.warning( + "Controller %d in world %s marked as dead, but that world is not tracked! Controller is being ignored.", + controller.hashCode(), + world); + } + } + + /** + * @param world The world whose controllers you wish to retrieve. + * @return An unmodifiable set of controllers active in the given world, or null if there are none. + */ + public static Set getControllersFromWorld(World world) { + if (registries.containsKey(world)) { + return registries.get(world) + .getControllers(); + } + return null; + } + + /// *** PRIVATE HELPERS *** /// + + private static MultiblockWorldRegistry getOrCreateRegistry(World world) { + if (registries.containsKey(world)) { + return registries.get(world); + } else { + MultiblockWorldRegistry newRegistry = new MultiblockWorldRegistry(world); + registries.put(world, newRegistry); + return newRegistry; + } + } } diff --git a/src/main/java/erogenousbeef/core/multiblock/MultiblockServerTickHandler.java b/src/main/java/erogenousbeef/core/multiblock/MultiblockServerTickHandler.java index d5ee44a..2206615 100644 --- a/src/main/java/erogenousbeef/core/multiblock/MultiblockServerTickHandler.java +++ b/src/main/java/erogenousbeef/core/multiblock/MultiblockServerTickHandler.java @@ -16,7 +16,7 @@ public class MultiblockServerTickHandler { @SubscribeEvent public void onWorldTick(TickEvent.WorldTickEvent event) { - if(event.phase == TickEvent.Phase.START) { + if (event.phase == TickEvent.Phase.START) { MultiblockRegistry.tickStart(event.world); } } diff --git a/src/main/java/erogenousbeef/core/multiblock/MultiblockTileEntityBase.java b/src/main/java/erogenousbeef/core/multiblock/MultiblockTileEntityBase.java index 4310c6e..dc28e98 100644 --- a/src/main/java/erogenousbeef/core/multiblock/MultiblockTileEntityBase.java +++ b/src/main/java/erogenousbeef/core/multiblock/MultiblockTileEntityBase.java @@ -11,6 +11,7 @@ import net.minecraft.network.play.server.S35PacketUpdateTileEntity; import net.minecraft.tileentity.TileEntity; import net.minecraft.world.chunk.IChunkProvider; + import erogenousbeef.core.common.BeefCoreLog; import erogenousbeef.core.common.CoordTriplet; @@ -19,339 +20,351 @@ * should derive from this and implement their game logic in certain abstract methods. */ public abstract class MultiblockTileEntityBase extends IMultiblockPart { - private MultiblockControllerBase controller; - private boolean visited; - - private boolean saveMultiblockData; - private NBTTagCompound cachedMultiblockData; - private boolean paused; - - public MultiblockTileEntityBase() { - super(); - controller = null; - visited = false; - saveMultiblockData = false; - paused = false; - cachedMultiblockData = null; - } - - ///// Multiblock Connection Base Logic - @Override - public Set attachToNeighbors() { - Set controllers = null; - MultiblockControllerBase bestController = null; - - // Look for a compatible controller in our neighboring parts. - IMultiblockPart[] partsToCheck = getNeighboringParts(); - for(IMultiblockPart neighborPart : partsToCheck) { - if(neighborPart.isConnected()) { - MultiblockControllerBase candidate = neighborPart.getMultiblockController(); - if(!candidate.getClass().equals(this.getMultiblockControllerType())) { - // Skip multiblocks with incompatible types - continue; - } - - if(controllers == null) { - controllers = new HashSet(); - bestController = candidate; - } - else if(!controllers.contains(candidate) && candidate.shouldConsume(bestController)) { - bestController = candidate; - } - - controllers.add(candidate); - } - } - - // If we've located a valid neighboring controller, attach to it. - if(bestController != null) { - // attachBlock will call onAttached, which will set the controller. - this.controller = bestController; - bestController.attachBlock(this); - } - - return controllers; - } - - @Override - public void assertDetached() { - if(this.controller != null) { - BeefCoreLog.info("[assert] Part @ (%d, %d, %d) should be detached already, but detected that it was not. This is not a fatal error, and will be repaired, but is unusual.", xCoord, yCoord, zCoord); - this.controller = null; - } - } - - ///// Overrides from base TileEntity methods - - @Override - public void readFromNBT(NBTTagCompound data) { - super.readFromNBT(data); - - // We can't directly initialize a multiblock controller yet, so we cache the data here until - // we receive a validate() call, which creates the controller and hands off the cached data. - if(data.hasKey("multiblockData")) { - this.cachedMultiblockData = data.getCompoundTag("multiblockData"); - } - } - - @Override - public void writeToNBT(NBTTagCompound data) { - super.writeToNBT(data); - - if(isMultiblockSaveDelegate() && isConnected()) { - NBTTagCompound multiblockData = new NBTTagCompound(); - this.controller.writeToNBT(multiblockData); - data.setTag("multiblockData", multiblockData); - } - } - - /** - * Generally, TileEntities that are part of a multiblock should not subscribe to updates - * from the main game loop. Instead, you should have lists of TileEntities which need to - * be notified during an update() in your Controller and perform callbacks from there. - * @see net.minecraft.tileentity.TileEntity#canUpdate() - */ - @Override - public boolean canUpdate() { return false; } - - /** - * Called when a block is removed by game actions, such as a player breaking the block - * or the block being changed into another block. - * @see net.minecraft.tileentity.TileEntity#invalidate() - */ - @Override - public void invalidate() { - super.invalidate(); - detachSelf(false); - } - - /** - * Called from Minecraft's tile entity loop, after all tile entities have been ticked, - * as the chunk in which this tile entity is contained is unloading. - * Happens before the Forge TickEnd event. - * @see net.minecraft.tileentity.TileEntity#onChunkUnload() - */ - @Override - public void onChunkUnload() { - super.onChunkUnload(); - detachSelf(true); - } - - /** - * This is called when a block is being marked as valid by the chunk, but has not yet fully - * been placed into the world's TileEntity cache. this.worldObj, xCoord, yCoord and zCoord have - * been initialized, but any attempts to read data about the world can cause infinite loops - - * if you call getTileEntity on this TileEntity's coordinate from within validate(), you will - * blow your call stack. - * - * TL;DR: Here there be dragons. - * @see net.minecraft.tileentity.TileEntity#validate() - */ - @Override - public void validate() { - super.validate(); - MultiblockRegistry.onPartAdded(this.worldObj, this); - } - - // Network Communication - @Override - public Packet getDescriptionPacket() { - NBTTagCompound packetData = new NBTTagCompound(); - encodeDescriptionPacket(packetData); - return new S35PacketUpdateTileEntity(xCoord, yCoord, zCoord, 0, packetData); - } - - @Override - public void onDataPacket(NetworkManager network, S35PacketUpdateTileEntity packet) { - decodeDescriptionPacket(packet.func_148857_g()); - } - - ///// Things to override in most implementations (IMultiblockPart) - /** - * Override this to easily modify the description packet's data without having - * to worry about sending the packet itself. - * Decode this data in decodeDescriptionPacket. - * @param packetData An NBT compound tag into which you should write your custom description data. - * @see erogenousbeef.core.multiblock.MultiblockTileEntityBase#decodeDescriptionPacket(NBTTagCompound) - */ - protected void encodeDescriptionPacket(NBTTagCompound packetData) { - if(this.isMultiblockSaveDelegate() && isConnected()) { - NBTTagCompound tag = new NBTTagCompound(); - getMultiblockController().formatDescriptionPacket(tag); - packetData.setTag("multiblockData", tag); - } - } - - /** - * Override this to easily read in data from a TileEntity's description packet. - * Encoded in encodeDescriptionPacket. - * @param packetData The NBT data from the tile entity's description packet. - * @see erogenousbeef.core.multiblock.MultiblockTileEntityBase#encodeDescriptionPacket(NBTTagCompound) - */ - protected void decodeDescriptionPacket(NBTTagCompound packetData) { - if(packetData.hasKey("multiblockData")) { - NBTTagCompound tag = packetData.getCompoundTag("multiblockData"); - if(isConnected()) { - getMultiblockController().decodeDescriptionPacket(tag); - } - else { - // This part hasn't been added to a machine yet, so cache the data. - this.cachedMultiblockData = tag; - } - } - } - - @Override - public boolean hasMultiblockSaveData() { - return this.cachedMultiblockData != null; - } - - @Override - public NBTTagCompound getMultiblockSaveData() { - return this.cachedMultiblockData; - } - - @Override - public void onMultiblockDataAssimilated() { - this.cachedMultiblockData = null; - } - - ///// Game logic callbacks (IMultiblockPart) - - @Override - public abstract void onMachineAssembled(MultiblockControllerBase multiblockControllerBase); - - @Override - public abstract void onMachineBroken(); - - @Override - public abstract void onMachineActivated(); - - @Override - public abstract void onMachineDeactivated(); - - ///// Miscellaneous multiblock-assembly callbacks and support methods (IMultiblockPart) - - @Override - public boolean isConnected() { - return (controller != null); - } - - @Override - public MultiblockControllerBase getMultiblockController() { - return controller; - } - - @Override - public CoordTriplet getWorldLocation() { - return new CoordTriplet(this.xCoord, this.yCoord, this.zCoord); - } - - @Override - public void becomeMultiblockSaveDelegate() { - this.saveMultiblockData = true; - } - - @Override - public void forfeitMultiblockSaveDelegate() { - this.saveMultiblockData = false; - } - - @Override - public boolean isMultiblockSaveDelegate() { return this.saveMultiblockData; } - - @Override - public void setUnvisited() { - this.visited = false; - } - - @Override - public void setVisited() { - this.visited = true; - } - - @Override - public boolean isVisited() { - return this.visited; - } - - @Override - public void onAssimilated(MultiblockControllerBase newController) { - assert(this.controller != newController); - this.controller = newController; - } - - @Override - public void onAttached(MultiblockControllerBase newController) { - this.controller = newController; - } - - @Override - public void onDetached(MultiblockControllerBase oldController) { - this.controller = null; - } - - @Override - public abstract MultiblockControllerBase createNewMultiblock(); - - @Override - public IMultiblockPart[] getNeighboringParts() { - CoordTriplet[] neighbors = new CoordTriplet[] { - new CoordTriplet(this.xCoord-1, this.yCoord, this.zCoord), - new CoordTriplet(this.xCoord, this.yCoord-1, this.zCoord), - new CoordTriplet(this.xCoord, this.yCoord, this.zCoord-1), - new CoordTriplet(this.xCoord, this.yCoord, this.zCoord+1), - new CoordTriplet(this.xCoord, this.yCoord+1, this.zCoord), - new CoordTriplet(this.xCoord+1, this.yCoord, this.zCoord) - }; - - TileEntity te; - List neighborParts = new ArrayList(); - IChunkProvider chunkProvider = worldObj.getChunkProvider(); - for(CoordTriplet neighbor : neighbors) { - if(!chunkProvider.chunkExists(neighbor.getChunkX(), neighbor.getChunkZ())) { - // Chunk not loaded, skip it. - continue; - } - - te = this.worldObj.getTileEntity(neighbor.x, neighbor.y, neighbor.z); - if(te instanceof IMultiblockPart) { - neighborParts.add((IMultiblockPart)te); - } - } - IMultiblockPart[] tmp = new IMultiblockPart[neighborParts.size()]; - return neighborParts.toArray(tmp); - } - - @Override - public void onOrphaned(MultiblockControllerBase controller, int oldSize, int newSize) { - this.markDirty(); - worldObj.markTileEntityChunkModified(xCoord, yCoord, zCoord, this); - } - - //// Helper functions for notifying neighboring blocks - protected void notifyNeighborsOfBlockChange() { - worldObj.notifyBlocksOfNeighborChange(xCoord, yCoord, zCoord, getBlockType()); - } - - protected void notifyNeighborsOfTileChange() { - worldObj.func_147453_f(xCoord, yCoord, zCoord, getBlockType()); - } - - ///// Private/Protected Logic Helpers - /* - * Detaches this block from its controller. Calls detachBlock() and clears the controller member. - */ - protected void detachSelf(boolean chunkUnloading) { - if(this.controller != null) { - // Clean part out of controller - this.controller.detachBlock(this, chunkUnloading); - - // The above should call onDetached, but, just in case... - this.controller = null; - } - - // Clean part out of lists in the registry - MultiblockRegistry.onPartRemovedFromWorld(worldObj, this); - } + + private MultiblockControllerBase controller; + private boolean visited; + + private boolean saveMultiblockData; + private NBTTagCompound cachedMultiblockData; + private boolean paused; + + public MultiblockTileEntityBase() { + super(); + controller = null; + visited = false; + saveMultiblockData = false; + paused = false; + cachedMultiblockData = null; + } + + ///// Multiblock Connection Base Logic + @Override + public Set attachToNeighbors() { + Set controllers = null; + MultiblockControllerBase bestController = null; + + // Look for a compatible controller in our neighboring parts. + IMultiblockPart[] partsToCheck = getNeighboringParts(); + for (IMultiblockPart neighborPart : partsToCheck) { + if (neighborPart.isConnected()) { + MultiblockControllerBase candidate = neighborPart.getMultiblockController(); + if (!candidate.getClass() + .equals(this.getMultiblockControllerType())) { + // Skip multiblocks with incompatible types + continue; + } + + if (controllers == null) { + controllers = new HashSet(); + bestController = candidate; + } else if (!controllers.contains(candidate) && candidate.shouldConsume(bestController)) { + bestController = candidate; + } + + controllers.add(candidate); + } + } + + // If we've located a valid neighboring controller, attach to it. + if (bestController != null) { + // attachBlock will call onAttached, which will set the controller. + this.controller = bestController; + bestController.attachBlock(this); + } + + return controllers; + } + + @Override + public void assertDetached() { + if (this.controller != null) { + BeefCoreLog.info( + "[assert] Part @ (%d, %d, %d) should be detached already, but detected that it was not. This is not a fatal error, and will be repaired, but is unusual.", + xCoord, + yCoord, + zCoord); + this.controller = null; + } + } + + ///// Overrides from base TileEntity methods + + @Override + public void readFromNBT(NBTTagCompound data) { + super.readFromNBT(data); + + // We can't directly initialize a multiblock controller yet, so we cache the data here until + // we receive a validate() call, which creates the controller and hands off the cached data. + if (data.hasKey("multiblockData")) { + this.cachedMultiblockData = data.getCompoundTag("multiblockData"); + } + } + + @Override + public void writeToNBT(NBTTagCompound data) { + super.writeToNBT(data); + + if (isMultiblockSaveDelegate() && isConnected()) { + NBTTagCompound multiblockData = new NBTTagCompound(); + this.controller.writeToNBT(multiblockData); + data.setTag("multiblockData", multiblockData); + } + } + + /** + * Generally, TileEntities that are part of a multiblock should not subscribe to updates + * from the main game loop. Instead, you should have lists of TileEntities which need to + * be notified during an update() in your Controller and perform callbacks from there. + * + * @see net.minecraft.tileentity.TileEntity#canUpdate() + */ + @Override + public boolean canUpdate() { + return false; + } + + /** + * Called when a block is removed by game actions, such as a player breaking the block + * or the block being changed into another block. + * + * @see net.minecraft.tileentity.TileEntity#invalidate() + */ + @Override + public void invalidate() { + super.invalidate(); + detachSelf(false); + } + + /** + * Called from Minecraft's tile entity loop, after all tile entities have been ticked, + * as the chunk in which this tile entity is contained is unloading. + * Happens before the Forge TickEnd event. + * + * @see net.minecraft.tileentity.TileEntity#onChunkUnload() + */ + @Override + public void onChunkUnload() { + super.onChunkUnload(); + detachSelf(true); + } + + /** + * This is called when a block is being marked as valid by the chunk, but has not yet fully + * been placed into the world's TileEntity cache. this.worldObj, xCoord, yCoord and zCoord have + * been initialized, but any attempts to read data about the world can cause infinite loops - + * if you call getTileEntity on this TileEntity's coordinate from within validate(), you will + * blow your call stack. + * + * TL;DR: Here there be dragons. + * + * @see net.minecraft.tileentity.TileEntity#validate() + */ + @Override + public void validate() { + super.validate(); + MultiblockRegistry.onPartAdded(this.worldObj, this); + } + + // Network Communication + @Override + public Packet getDescriptionPacket() { + NBTTagCompound packetData = new NBTTagCompound(); + encodeDescriptionPacket(packetData); + return new S35PacketUpdateTileEntity(xCoord, yCoord, zCoord, 0, packetData); + } + + @Override + public void onDataPacket(NetworkManager network, S35PacketUpdateTileEntity packet) { + decodeDescriptionPacket(packet.func_148857_g()); + } + + ///// Things to override in most implementations (IMultiblockPart) + /** + * Override this to easily modify the description packet's data without having + * to worry about sending the packet itself. + * Decode this data in decodeDescriptionPacket. + * + * @param packetData An NBT compound tag into which you should write your custom description data. + * @see erogenousbeef.core.multiblock.MultiblockTileEntityBase#decodeDescriptionPacket(NBTTagCompound) + */ + protected void encodeDescriptionPacket(NBTTagCompound packetData) { + if (this.isMultiblockSaveDelegate() && isConnected()) { + NBTTagCompound tag = new NBTTagCompound(); + getMultiblockController().formatDescriptionPacket(tag); + packetData.setTag("multiblockData", tag); + } + } + + /** + * Override this to easily read in data from a TileEntity's description packet. + * Encoded in encodeDescriptionPacket. + * + * @param packetData The NBT data from the tile entity's description packet. + * @see erogenousbeef.core.multiblock.MultiblockTileEntityBase#encodeDescriptionPacket(NBTTagCompound) + */ + protected void decodeDescriptionPacket(NBTTagCompound packetData) { + if (packetData.hasKey("multiblockData")) { + NBTTagCompound tag = packetData.getCompoundTag("multiblockData"); + if (isConnected()) { + getMultiblockController().decodeDescriptionPacket(tag); + } else { + // This part hasn't been added to a machine yet, so cache the data. + this.cachedMultiblockData = tag; + } + } + } + + @Override + public boolean hasMultiblockSaveData() { + return this.cachedMultiblockData != null; + } + + @Override + public NBTTagCompound getMultiblockSaveData() { + return this.cachedMultiblockData; + } + + @Override + public void onMultiblockDataAssimilated() { + this.cachedMultiblockData = null; + } + + ///// Game logic callbacks (IMultiblockPart) + + @Override + public abstract void onMachineAssembled(MultiblockControllerBase multiblockControllerBase); + + @Override + public abstract void onMachineBroken(); + + @Override + public abstract void onMachineActivated(); + + @Override + public abstract void onMachineDeactivated(); + + ///// Miscellaneous multiblock-assembly callbacks and support methods (IMultiblockPart) + + @Override + public boolean isConnected() { + return (controller != null); + } + + @Override + public MultiblockControllerBase getMultiblockController() { + return controller; + } + + @Override + public CoordTriplet getWorldLocation() { + return new CoordTriplet(this.xCoord, this.yCoord, this.zCoord); + } + + @Override + public void becomeMultiblockSaveDelegate() { + this.saveMultiblockData = true; + } + + @Override + public void forfeitMultiblockSaveDelegate() { + this.saveMultiblockData = false; + } + + @Override + public boolean isMultiblockSaveDelegate() { + return this.saveMultiblockData; + } + + @Override + public void setUnvisited() { + this.visited = false; + } + + @Override + public void setVisited() { + this.visited = true; + } + + @Override + public boolean isVisited() { + return this.visited; + } + + @Override + public void onAssimilated(MultiblockControllerBase newController) { + assert (this.controller != newController); + this.controller = newController; + } + + @Override + public void onAttached(MultiblockControllerBase newController) { + this.controller = newController; + } + + @Override + public void onDetached(MultiblockControllerBase oldController) { + this.controller = null; + } + + @Override + public abstract MultiblockControllerBase createNewMultiblock(); + + @Override + public IMultiblockPart[] getNeighboringParts() { + CoordTriplet[] neighbors = new CoordTriplet[] { new CoordTriplet(this.xCoord - 1, this.yCoord, this.zCoord), + new CoordTriplet(this.xCoord, this.yCoord - 1, this.zCoord), + new CoordTriplet(this.xCoord, this.yCoord, this.zCoord - 1), + new CoordTriplet(this.xCoord, this.yCoord, this.zCoord + 1), + new CoordTriplet(this.xCoord, this.yCoord + 1, this.zCoord), + new CoordTriplet(this.xCoord + 1, this.yCoord, this.zCoord) }; + + TileEntity te; + List neighborParts = new ArrayList(); + IChunkProvider chunkProvider = worldObj.getChunkProvider(); + for (CoordTriplet neighbor : neighbors) { + if (!chunkProvider.chunkExists(neighbor.getChunkX(), neighbor.getChunkZ())) { + // Chunk not loaded, skip it. + continue; + } + + te = this.worldObj.getTileEntity(neighbor.x, neighbor.y, neighbor.z); + if (te instanceof IMultiblockPart) { + neighborParts.add((IMultiblockPart) te); + } + } + IMultiblockPart[] tmp = new IMultiblockPart[neighborParts.size()]; + return neighborParts.toArray(tmp); + } + + @Override + public void onOrphaned(MultiblockControllerBase controller, int oldSize, int newSize) { + this.markDirty(); + worldObj.markTileEntityChunkModified(xCoord, yCoord, zCoord, this); + } + + //// Helper functions for notifying neighboring blocks + protected void notifyNeighborsOfBlockChange() { + worldObj.notifyBlocksOfNeighborChange(xCoord, yCoord, zCoord, getBlockType()); + } + + protected void notifyNeighborsOfTileChange() { + worldObj.func_147453_f(xCoord, yCoord, zCoord, getBlockType()); + } + + ///// Private/Protected Logic Helpers + /* + * Detaches this block from its controller. Calls detachBlock() and clears the controller member. + */ + protected void detachSelf(boolean chunkUnloading) { + if (this.controller != null) { + // Clean part out of controller + this.controller.detachBlock(this, chunkUnloading); + + // The above should call onDetached, but, just in case... + this.controller = null; + } + + // Clean part out of lists in the registry + MultiblockRegistry.onPartRemovedFromWorld(worldObj, this); + } } diff --git a/src/main/java/erogenousbeef/core/multiblock/MultiblockValidationException.java b/src/main/java/erogenousbeef/core/multiblock/MultiblockValidationException.java index 4403274..8c383bf 100644 --- a/src/main/java/erogenousbeef/core/multiblock/MultiblockValidationException.java +++ b/src/main/java/erogenousbeef/core/multiblock/MultiblockValidationException.java @@ -3,11 +3,12 @@ /** * An exception thrown when trying to validate a multiblock. Requires a string describing why the multiblock * could not assemble. + * * @author Erogenous Beef */ public class MultiblockValidationException extends Exception { - public MultiblockValidationException(String reason) { - super(reason); - } + public MultiblockValidationException(String reason) { + super(reason); + } } diff --git a/src/main/java/erogenousbeef/core/multiblock/MultiblockWorldRegistry.java b/src/main/java/erogenousbeef/core/multiblock/MultiblockWorldRegistry.java index 1ddcd8d..26d215f 100644 --- a/src/main/java/erogenousbeef/core/multiblock/MultiblockWorldRegistry.java +++ b/src/main/java/erogenousbeef/core/multiblock/MultiblockWorldRegistry.java @@ -11,6 +11,7 @@ import net.minecraft.world.ChunkCoordIntPair; import net.minecraft.world.World; import net.minecraft.world.chunk.IChunkProvider; + import erogenousbeef.core.common.BeefCoreLog; import erogenousbeef.core.common.CoordTriplet; @@ -23,395 +24,403 @@ */ public class MultiblockWorldRegistry { - private World worldObj; - - private Set controllers; // Active controllers - private Set dirtyControllers; // Controllers whose parts lists have changed - private Set deadControllers; // Controllers which are empty - - // A list of orphan parts - parts which currently have no master, but should seek one this tick - // Indexed by the hashed chunk coordinate - // This can be added-to asynchronously via chunk loads! - private Set orphanedParts; - - // A list of parts which have been detached during internal operations - private Set detachedParts; - - // A list of parts whose chunks have not yet finished loading - // They will be added to the orphan list when they are finished loading. - // Indexed by the hashed chunk coordinate - // This can be added-to asynchronously via chunk loads! - private HashMap> partsAwaitingChunkLoad; - - // Mutexes to protect lists which may be changed due to asynchronous events, such as chunk loads - private Object partsAwaitingChunkLoadMutex; - private Object orphanedPartsMutex; - - public MultiblockWorldRegistry(World world) { - worldObj = world; - - controllers = new HashSet(); - deadControllers = new HashSet(); - dirtyControllers = new HashSet(); - - detachedParts = new HashSet(); - orphanedParts = new HashSet(); - - partsAwaitingChunkLoad = new HashMap>(); - partsAwaitingChunkLoadMutex = new Object(); - orphanedPartsMutex = new Object(); - } - - /** - * Called before Tile Entities are ticked in the world. Run game logic. - */ - public void tickStart() { - if(controllers.size() > 0) { - for(MultiblockControllerBase controller : controllers) { - if(controller.worldObj == worldObj && controller.worldObj.isRemote == worldObj.isRemote) { - if(controller.isEmpty()) { - // This happens on the server when the user breaks the last block. It's fine. - // Mark 'er dead and move on. - deadControllers.add(controller); - } - else { - // Run the game logic for this world - controller.updateMultiblockEntity(); - } - } - } - } - } - - /** - * Called prior to processing multiblock controllers. Do bookkeeping. - */ - public void processMultiblockChanges() { - IChunkProvider chunkProvider = worldObj.getChunkProvider(); - CoordTriplet coord; - - // Merge pools - sets of adjacent machines which should be merged later on in processing - List> mergePools = null; - if(orphanedParts.size() > 0) { - Set orphansToProcess = null; - - // Keep the synchronized block small. We can't iterate over orphanedParts directly - // because the client does not know which chunks are actually loaded, so attachToNeighbors() - // is not chunk-safe on the client, because Minecraft is stupid. - // It's possible to polyfill this, but the polyfill is too slow for comfort. - synchronized(orphanedPartsMutex) { - if(orphanedParts.size() > 0) { - orphansToProcess = orphanedParts; - orphanedParts = new HashSet(); - } - } - - if(orphansToProcess != null && orphansToProcess.size() > 0) { - Set compatibleControllers; - - // Process orphaned blocks - // These are blocks that exist in a valid chunk and require a controller - for(IMultiblockPart orphan : orphansToProcess) { - coord = orphan.getWorldLocation(); - if(!chunkProvider.chunkExists(coord.getChunkX(), coord.getChunkZ())) { - continue; - } - - // This can occur on slow machines. - if(orphan.isInvalid()) { continue; } - - if(worldObj.getTileEntity(coord.x, coord.y, coord.z) != orphan) { - // This block has been replaced by another. - continue; - } - - // THIS IS THE ONLY PLACE WHERE PARTS ATTACH TO MACHINES - // Try to attach to a neighbor's master controller - compatibleControllers = orphan.attachToNeighbors(); - if(compatibleControllers == null) { - // FOREVER ALONE! Create and register a new controller. - // THIS IS THE ONLY PLACE WHERE NEW CONTROLLERS ARE CREATED. - MultiblockControllerBase newController = orphan.createNewMultiblock(); - newController.attachBlock(orphan); - this.controllers.add(newController); - } - else if(compatibleControllers.size() > 1) { - if(mergePools == null) { mergePools = new ArrayList>(); } - - // THIS IS THE ONLY PLACE WHERE MERGES ARE DETECTED - // Multiple compatible controllers indicates an impending merge. - // Locate the appropriate merge pool(s) - boolean hasAddedToPool = false; - List> candidatePools = new ArrayList>(); - for(Set candidatePool : mergePools) { - if(!Collections.disjoint(candidatePool, compatibleControllers)) { - // They share at least one element, so that means they will all touch after the merge - candidatePools.add(candidatePool); - } - } - - if(candidatePools.size() <= 0) { - // No pools nearby, create a new merge pool - mergePools.add(compatibleControllers); - } - else if(candidatePools.size() == 1) { - // Only one pool nearby, simply add to that one - candidatePools.get(0).addAll(compatibleControllers); - } - else { - // Multiple pools- merge into one, then add the compatible controllers - Set masterPool = candidatePools.get(0); - Set consumedPool; - for(int i = 1; i < candidatePools.size(); i++) { - consumedPool = candidatePools.get(i); - masterPool.addAll(consumedPool); - mergePools.remove(consumedPool); - } - masterPool.addAll(compatibleControllers); - } - } - } - } - } - - if(mergePools != null && mergePools.size() > 0) { - // Process merges - any machines that have been marked for merge should be merged - // into the "master" machine. - // To do this, we combine lists of machines that are touching one another and therefore - // should voltron the fuck up. - for(Set mergePool : mergePools) { - // Search for the new master machine, which will take over all the blocks contained in the other machines - MultiblockControllerBase newMaster = null; - for(MultiblockControllerBase controller : mergePool) { - if(newMaster == null || controller.shouldConsume(newMaster)) { - newMaster = controller; - } - } - - if(newMaster == null) { - BeefCoreLog.fatal("Multiblock system checked a merge pool of size %d, found no master candidates. This should never happen.", mergePool.size()); - } - else { - // Merge all the other machines into the master machine, then unregister them - addDirtyController(newMaster); - for(MultiblockControllerBase controller : mergePool) { - if(controller != newMaster) { - newMaster.assimilate(controller); - addDeadController(controller); - addDirtyController(newMaster); - } - } - } - } - } - - // Process splits and assembly - // Any controllers which have had parts removed must be checked to see if some parts are no longer - // physically connected to their master. - if(dirtyControllers.size() > 0) { - Set newlyDetachedParts = null; - for(MultiblockControllerBase controller : dirtyControllers) { - // Tell the machine to check if any parts are disconnected. - // It should return a set of parts which are no longer connected. - // POSTCONDITION: The controller must have informed those parts that - // they are no longer connected to this machine. - newlyDetachedParts = controller.checkForDisconnections(); - - if(!controller.isEmpty()) { - controller.recalculateMinMaxCoords(); - controller.checkIfMachineIsWhole(); - } - else { - addDeadController(controller); - } - - if(newlyDetachedParts != null && newlyDetachedParts.size() > 0) { - // Controller has shed some parts - add them to the detached list for delayed processing - detachedParts.addAll(newlyDetachedParts); - } - } - - dirtyControllers.clear(); - } - - // Unregister dead controllers - if(deadControllers.size() > 0) { - for(MultiblockControllerBase controller : deadControllers) { - // Go through any controllers which have marked themselves as potentially dead. - // Validate that they are empty/dead, then unregister them. - if(!controller.isEmpty()) { - BeefCoreLog.fatal("Found a non-empty controller. Forcing it to shed its blocks and die. This should never happen!"); - detachedParts.addAll(controller.detachAllBlocks()); - } - - // THIS IS THE ONLY PLACE WHERE CONTROLLERS ARE UNREGISTERED. - this.controllers.remove(controller); - } - - deadControllers.clear(); - } - - // Process detached blocks - // Any blocks which have been detached this tick should be moved to the orphaned - // list, and will be checked next tick to see if their chunk is still loaded. - for(IMultiblockPart part : detachedParts) { - // Ensure parts know they're detached - part.assertDetached(); - } - - addAllOrphanedPartsThreadsafe(detachedParts); - detachedParts.clear(); - } - - /** - * Called when a multiblock part is added to the world, either via chunk-load or user action. - * If its chunk is loaded, it will be processed during the next tick. - * If the chunk is not loaded, it will be added to a list of objects waiting for a chunkload. - * @param part The part which is being added to this world. - */ - public void onPartAdded(IMultiblockPart part) { - CoordTriplet worldLocation = part.getWorldLocation(); - - if(!worldObj.getChunkProvider().chunkExists(worldLocation.getChunkX(), worldLocation.getChunkZ())) { - // Part goes into the waiting-for-chunk-load list - Set partSet; - long chunkHash = worldLocation.getChunkXZHash(); - synchronized(partsAwaitingChunkLoadMutex) { - if(!partsAwaitingChunkLoad.containsKey(chunkHash)) { - partSet = new HashSet(); - partsAwaitingChunkLoad.put(chunkHash, partSet); - } - else { - partSet = partsAwaitingChunkLoad.get(chunkHash); - } - - partSet.add(part); - } - } - else { - // Part goes into the orphan queue, to be checked this tick - addOrphanedPartThreadsafe(part); - } - } - - /** - * Called when a part is removed from the world, via user action or via chunk unloads. - * This part is removed from any lists in which it may be, and its machine is marked for recalculation. - * @param part The part which is being removed. - */ - public void onPartRemovedFromWorld(IMultiblockPart part) { - CoordTriplet coord = part.getWorldLocation(); - if(coord != null) { - long hash = coord.getChunkXZHash(); - - if(partsAwaitingChunkLoad.containsKey(hash)) { - synchronized(partsAwaitingChunkLoadMutex) { - if(partsAwaitingChunkLoad.containsKey(hash)) { - partsAwaitingChunkLoad.get(hash).remove(part); - if(partsAwaitingChunkLoad.get(hash).size() <= 0) { - partsAwaitingChunkLoad.remove(hash); - } - } - } - } - } - - detachedParts.remove(part); - if(orphanedParts.contains(part)) { - synchronized(orphanedPartsMutex) { - orphanedParts.remove(part); - } - } - - part.assertDetached(); - } - - /** - * Called when the world which this World Registry represents is fully unloaded from the system. - * Does some housekeeping just to be nice. - */ - public void onWorldUnloaded() { - controllers.clear(); - deadControllers.clear(); - dirtyControllers.clear(); - - detachedParts.clear(); - - synchronized(partsAwaitingChunkLoadMutex) { - partsAwaitingChunkLoad.clear(); - } - - synchronized(orphanedPartsMutex) { - orphanedParts.clear(); - } - - worldObj = null; - } - - /** - * Called when a chunk has finished loading. Adds all of the parts which are awaiting - * load to the list of parts which are orphans and therefore will be added to machines - * after the next world tick. - * - * @param chunkX Chunk X coordinate (world coordate >> 4) of the chunk that was loaded - * @param chunkZ Chunk Z coordinate (world coordate >> 4) of the chunk that was loaded - */ - public void onChunkLoaded(int chunkX, int chunkZ) { - long chunkHash = ChunkCoordIntPair.chunkXZ2Int(chunkX, chunkZ); - if(partsAwaitingChunkLoad.containsKey(chunkHash)) { - synchronized(partsAwaitingChunkLoadMutex) { - if(partsAwaitingChunkLoad.containsKey(chunkHash)) { - addAllOrphanedPartsThreadsafe(partsAwaitingChunkLoad.get(chunkHash)); - partsAwaitingChunkLoad.remove(chunkHash); - } - } - } - } - - /** - * Registers a controller as dead. It will be cleaned up at the end of the next world tick. - * Note that a controller must shed all of its blocks before being marked as dead, or the system - * will complain at you. - * - * @param deadController The controller which is dead. - */ - public void addDeadController(MultiblockControllerBase deadController) { - this.deadControllers.add(deadController); - } - - /** - * Registers a controller as dirty - its list of attached blocks has changed, and it - * must be re-checked for assembly and, possibly, for orphans. - * - * @param dirtyController The dirty controller. - */ - public void addDirtyController(MultiblockControllerBase dirtyController) { - this.dirtyControllers.add(dirtyController); - } - - /** - * Use this only if you know what you're doing. You should rarely need to iterate - * over all controllers in a world! - * - * @return An (unmodifiable) set of controllers which are active in this world. - */ - public Set getControllers() { - return Collections.unmodifiableSet(controllers); - } - - /* *** PRIVATE HELPERS *** */ - - private void addOrphanedPartThreadsafe(IMultiblockPart part) { - synchronized(orphanedPartsMutex) { - orphanedParts.add(part); - } - } - - private void addAllOrphanedPartsThreadsafe(Collection parts) { - synchronized(orphanedPartsMutex) { - orphanedParts.addAll(parts); - } - } - - private String clientOrServer() { return worldObj.isRemote ? "CLIENT" : "SERVER"; } + private World worldObj; + + private Set controllers; // Active controllers + private Set dirtyControllers; // Controllers whose parts lists have changed + private Set deadControllers; // Controllers which are empty + + // A list of orphan parts - parts which currently have no master, but should seek one this tick + // Indexed by the hashed chunk coordinate + // This can be added-to asynchronously via chunk loads! + private Set orphanedParts; + + // A list of parts which have been detached during internal operations + private Set detachedParts; + + // A list of parts whose chunks have not yet finished loading + // They will be added to the orphan list when they are finished loading. + // Indexed by the hashed chunk coordinate + // This can be added-to asynchronously via chunk loads! + private HashMap> partsAwaitingChunkLoad; + + // Mutexes to protect lists which may be changed due to asynchronous events, such as chunk loads + private Object partsAwaitingChunkLoadMutex; + private Object orphanedPartsMutex; + + public MultiblockWorldRegistry(World world) { + worldObj = world; + + controllers = new HashSet(); + deadControllers = new HashSet(); + dirtyControllers = new HashSet(); + + detachedParts = new HashSet(); + orphanedParts = new HashSet(); + + partsAwaitingChunkLoad = new HashMap>(); + partsAwaitingChunkLoadMutex = new Object(); + orphanedPartsMutex = new Object(); + } + + /** + * Called before Tile Entities are ticked in the world. Run game logic. + */ + public void tickStart() { + if (controllers.size() > 0) { + for (MultiblockControllerBase controller : controllers) { + if (controller.worldObj == worldObj && controller.worldObj.isRemote == worldObj.isRemote) { + if (controller.isEmpty()) { + // This happens on the server when the user breaks the last block. It's fine. + // Mark 'er dead and move on. + deadControllers.add(controller); + } else { + // Run the game logic for this world + controller.updateMultiblockEntity(); + } + } + } + } + } + + /** + * Called prior to processing multiblock controllers. Do bookkeeping. + */ + public void processMultiblockChanges() { + IChunkProvider chunkProvider = worldObj.getChunkProvider(); + CoordTriplet coord; + + // Merge pools - sets of adjacent machines which should be merged later on in processing + List> mergePools = null; + if (orphanedParts.size() > 0) { + Set orphansToProcess = null; + + // Keep the synchronized block small. We can't iterate over orphanedParts directly + // because the client does not know which chunks are actually loaded, so attachToNeighbors() + // is not chunk-safe on the client, because Minecraft is stupid. + // It's possible to polyfill this, but the polyfill is too slow for comfort. + synchronized (orphanedPartsMutex) { + if (orphanedParts.size() > 0) { + orphansToProcess = orphanedParts; + orphanedParts = new HashSet(); + } + } + + if (orphansToProcess != null && orphansToProcess.size() > 0) { + Set compatibleControllers; + + // Process orphaned blocks + // These are blocks that exist in a valid chunk and require a controller + for (IMultiblockPart orphan : orphansToProcess) { + coord = orphan.getWorldLocation(); + if (!chunkProvider.chunkExists(coord.getChunkX(), coord.getChunkZ())) { + continue; + } + + // This can occur on slow machines. + if (orphan.isInvalid()) { + continue; + } + + if (worldObj.getTileEntity(coord.x, coord.y, coord.z) != orphan) { + // This block has been replaced by another. + continue; + } + + // THIS IS THE ONLY PLACE WHERE PARTS ATTACH TO MACHINES + // Try to attach to a neighbor's master controller + compatibleControllers = orphan.attachToNeighbors(); + if (compatibleControllers == null) { + // FOREVER ALONE! Create and register a new controller. + // THIS IS THE ONLY PLACE WHERE NEW CONTROLLERS ARE CREATED. + MultiblockControllerBase newController = orphan.createNewMultiblock(); + newController.attachBlock(orphan); + this.controllers.add(newController); + } else if (compatibleControllers.size() > 1) { + if (mergePools == null) { + mergePools = new ArrayList>(); + } + + // THIS IS THE ONLY PLACE WHERE MERGES ARE DETECTED + // Multiple compatible controllers indicates an impending merge. + // Locate the appropriate merge pool(s) + boolean hasAddedToPool = false; + List> candidatePools = new ArrayList>(); + for (Set candidatePool : mergePools) { + if (!Collections.disjoint(candidatePool, compatibleControllers)) { + // They share at least one element, so that means they will all touch after the merge + candidatePools.add(candidatePool); + } + } + + if (candidatePools.size() <= 0) { + // No pools nearby, create a new merge pool + mergePools.add(compatibleControllers); + } else if (candidatePools.size() == 1) { + // Only one pool nearby, simply add to that one + candidatePools.get(0) + .addAll(compatibleControllers); + } else { + // Multiple pools- merge into one, then add the compatible controllers + Set masterPool = candidatePools.get(0); + Set consumedPool; + for (int i = 1; i < candidatePools.size(); i++) { + consumedPool = candidatePools.get(i); + masterPool.addAll(consumedPool); + mergePools.remove(consumedPool); + } + masterPool.addAll(compatibleControllers); + } + } + } + } + } + + if (mergePools != null && mergePools.size() > 0) { + // Process merges - any machines that have been marked for merge should be merged + // into the "master" machine. + // To do this, we combine lists of machines that are touching one another and therefore + // should voltron the fuck up. + for (Set mergePool : mergePools) { + // Search for the new master machine, which will take over all the blocks contained in the other + // machines + MultiblockControllerBase newMaster = null; + for (MultiblockControllerBase controller : mergePool) { + if (newMaster == null || controller.shouldConsume(newMaster)) { + newMaster = controller; + } + } + + if (newMaster == null) { + BeefCoreLog.fatal( + "Multiblock system checked a merge pool of size %d, found no master candidates. This should never happen.", + mergePool.size()); + } else { + // Merge all the other machines into the master machine, then unregister them + addDirtyController(newMaster); + for (MultiblockControllerBase controller : mergePool) { + if (controller != newMaster) { + newMaster.assimilate(controller); + addDeadController(controller); + addDirtyController(newMaster); + } + } + } + } + } + + // Process splits and assembly + // Any controllers which have had parts removed must be checked to see if some parts are no longer + // physically connected to their master. + if (dirtyControllers.size() > 0) { + Set newlyDetachedParts = null; + for (MultiblockControllerBase controller : dirtyControllers) { + // Tell the machine to check if any parts are disconnected. + // It should return a set of parts which are no longer connected. + // POSTCONDITION: The controller must have informed those parts that + // they are no longer connected to this machine. + newlyDetachedParts = controller.checkForDisconnections(); + + if (!controller.isEmpty()) { + controller.recalculateMinMaxCoords(); + controller.checkIfMachineIsWhole(); + } else { + addDeadController(controller); + } + + if (newlyDetachedParts != null && newlyDetachedParts.size() > 0) { + // Controller has shed some parts - add them to the detached list for delayed processing + detachedParts.addAll(newlyDetachedParts); + } + } + + dirtyControllers.clear(); + } + + // Unregister dead controllers + if (deadControllers.size() > 0) { + for (MultiblockControllerBase controller : deadControllers) { + // Go through any controllers which have marked themselves as potentially dead. + // Validate that they are empty/dead, then unregister them. + if (!controller.isEmpty()) { + BeefCoreLog.fatal( + "Found a non-empty controller. Forcing it to shed its blocks and die. This should never happen!"); + detachedParts.addAll(controller.detachAllBlocks()); + } + + // THIS IS THE ONLY PLACE WHERE CONTROLLERS ARE UNREGISTERED. + this.controllers.remove(controller); + } + + deadControllers.clear(); + } + + // Process detached blocks + // Any blocks which have been detached this tick should be moved to the orphaned + // list, and will be checked next tick to see if their chunk is still loaded. + for (IMultiblockPart part : detachedParts) { + // Ensure parts know they're detached + part.assertDetached(); + } + + addAllOrphanedPartsThreadsafe(detachedParts); + detachedParts.clear(); + } + + /** + * Called when a multiblock part is added to the world, either via chunk-load or user action. + * If its chunk is loaded, it will be processed during the next tick. + * If the chunk is not loaded, it will be added to a list of objects waiting for a chunkload. + * + * @param part The part which is being added to this world. + */ + public void onPartAdded(IMultiblockPart part) { + CoordTriplet worldLocation = part.getWorldLocation(); + + if (!worldObj.getChunkProvider() + .chunkExists(worldLocation.getChunkX(), worldLocation.getChunkZ())) { + // Part goes into the waiting-for-chunk-load list + Set partSet; + long chunkHash = worldLocation.getChunkXZHash(); + synchronized (partsAwaitingChunkLoadMutex) { + if (!partsAwaitingChunkLoad.containsKey(chunkHash)) { + partSet = new HashSet(); + partsAwaitingChunkLoad.put(chunkHash, partSet); + } else { + partSet = partsAwaitingChunkLoad.get(chunkHash); + } + + partSet.add(part); + } + } else { + // Part goes into the orphan queue, to be checked this tick + addOrphanedPartThreadsafe(part); + } + } + + /** + * Called when a part is removed from the world, via user action or via chunk unloads. + * This part is removed from any lists in which it may be, and its machine is marked for recalculation. + * + * @param part The part which is being removed. + */ + public void onPartRemovedFromWorld(IMultiblockPart part) { + CoordTriplet coord = part.getWorldLocation(); + if (coord != null) { + long hash = coord.getChunkXZHash(); + + if (partsAwaitingChunkLoad.containsKey(hash)) { + synchronized (partsAwaitingChunkLoadMutex) { + if (partsAwaitingChunkLoad.containsKey(hash)) { + partsAwaitingChunkLoad.get(hash) + .remove(part); + if (partsAwaitingChunkLoad.get(hash) + .size() <= 0) { + partsAwaitingChunkLoad.remove(hash); + } + } + } + } + } + + detachedParts.remove(part); + if (orphanedParts.contains(part)) { + synchronized (orphanedPartsMutex) { + orphanedParts.remove(part); + } + } + + part.assertDetached(); + } + + /** + * Called when the world which this World Registry represents is fully unloaded from the system. + * Does some housekeeping just to be nice. + */ + public void onWorldUnloaded() { + controllers.clear(); + deadControllers.clear(); + dirtyControllers.clear(); + + detachedParts.clear(); + + synchronized (partsAwaitingChunkLoadMutex) { + partsAwaitingChunkLoad.clear(); + } + + synchronized (orphanedPartsMutex) { + orphanedParts.clear(); + } + + worldObj = null; + } + + /** + * Called when a chunk has finished loading. Adds all of the parts which are awaiting + * load to the list of parts which are orphans and therefore will be added to machines + * after the next world tick. + * + * @param chunkX Chunk X coordinate (world coordate >> 4) of the chunk that was loaded + * @param chunkZ Chunk Z coordinate (world coordate >> 4) of the chunk that was loaded + */ + public void onChunkLoaded(int chunkX, int chunkZ) { + long chunkHash = ChunkCoordIntPair.chunkXZ2Int(chunkX, chunkZ); + if (partsAwaitingChunkLoad.containsKey(chunkHash)) { + synchronized (partsAwaitingChunkLoadMutex) { + if (partsAwaitingChunkLoad.containsKey(chunkHash)) { + addAllOrphanedPartsThreadsafe(partsAwaitingChunkLoad.get(chunkHash)); + partsAwaitingChunkLoad.remove(chunkHash); + } + } + } + } + + /** + * Registers a controller as dead. It will be cleaned up at the end of the next world tick. + * Note that a controller must shed all of its blocks before being marked as dead, or the system + * will complain at you. + * + * @param deadController The controller which is dead. + */ + public void addDeadController(MultiblockControllerBase deadController) { + this.deadControllers.add(deadController); + } + + /** + * Registers a controller as dirty - its list of attached blocks has changed, and it + * must be re-checked for assembly and, possibly, for orphans. + * + * @param dirtyController The dirty controller. + */ + public void addDirtyController(MultiblockControllerBase dirtyController) { + this.dirtyControllers.add(dirtyController); + } + + /** + * Use this only if you know what you're doing. You should rarely need to iterate + * over all controllers in a world! + * + * @return An (unmodifiable) set of controllers which are active in this world. + */ + public Set getControllers() { + return Collections.unmodifiableSet(controllers); + } + + /* *** PRIVATE HELPERS *** */ + + private void addOrphanedPartThreadsafe(IMultiblockPart part) { + synchronized (orphanedPartsMutex) { + orphanedParts.add(part); + } + } + + private void addAllOrphanedPartsThreadsafe(Collection parts) { + synchronized (orphanedPartsMutex) { + orphanedParts.addAll(parts); + } + } + + private String clientOrServer() { + return worldObj.isRemote ? "CLIENT" : "SERVER"; + } } diff --git a/src/main/java/erogenousbeef/core/multiblock/rectangular/PartPosition.java b/src/main/java/erogenousbeef/core/multiblock/rectangular/PartPosition.java index deb8648..deea431 100644 --- a/src/main/java/erogenousbeef/core/multiblock/rectangular/PartPosition.java +++ b/src/main/java/erogenousbeef/core/multiblock/rectangular/PartPosition.java @@ -1,28 +1,29 @@ package erogenousbeef.core.multiblock.rectangular; public enum PartPosition { - Unknown, - Interior, - FrameCorner, - Frame, - TopFace, - BottomFace, - NorthFace, - SouthFace, - EastFace, - WestFace; - - public boolean isFace(PartPosition position) { - switch(position) { - case TopFace: - case BottomFace: - case NorthFace: - case SouthFace: - case EastFace: - case WestFace: - return true; - default: - return false; - } - } + + Unknown, + Interior, + FrameCorner, + Frame, + TopFace, + BottomFace, + NorthFace, + SouthFace, + EastFace, + WestFace; + + public boolean isFace(PartPosition position) { + switch (position) { + case TopFace: + case BottomFace: + case NorthFace: + case SouthFace: + case EastFace: + case WestFace: + return true; + default: + return false; + } + } } diff --git a/src/main/java/erogenousbeef/core/multiblock/rectangular/RectangularMultiblockControllerBase.java b/src/main/java/erogenousbeef/core/multiblock/rectangular/RectangularMultiblockControllerBase.java index 8f242e0..b94409c 100644 --- a/src/main/java/erogenousbeef/core/multiblock/rectangular/RectangularMultiblockControllerBase.java +++ b/src/main/java/erogenousbeef/core/multiblock/rectangular/RectangularMultiblockControllerBase.java @@ -2,129 +2,154 @@ import net.minecraft.tileentity.TileEntity; import net.minecraft.world.World; + import erogenousbeef.core.common.CoordTriplet; import erogenousbeef.core.multiblock.MultiblockControllerBase; import erogenousbeef.core.multiblock.MultiblockValidationException; -public abstract class RectangularMultiblockControllerBase extends - MultiblockControllerBase { - - protected RectangularMultiblockControllerBase(World world) { - super(world); - } - - /** - * @return True if the machine is "whole" and should be assembled. False otherwise. - */ - protected void isMachineWhole() throws MultiblockValidationException { - if(connectedParts.size() < getMinimumNumberOfBlocksForAssembledMachine()) { - throw new MultiblockValidationException("Machine is too small."); - } - - CoordTriplet maximumCoord = getMaximumCoord(); - CoordTriplet minimumCoord = getMinimumCoord(); - - // Quickly check for exceeded dimensions - int deltaX = maximumCoord.x - minimumCoord.x + 1; - int deltaY = maximumCoord.y - minimumCoord.y + 1; - int deltaZ = maximumCoord.z - minimumCoord.z + 1; - - int maxX = getMaximumXSize(); - int maxY = getMaximumYSize(); - int maxZ = getMaximumZSize(); - int minX = getMinimumXSize(); - int minY = getMinimumYSize(); - int minZ = getMinimumZSize(); - - if(maxX > 0 && deltaX > maxX) { throw new MultiblockValidationException(String.format("Machine is too large, it may be at most %d blocks in the X dimension", maxX)); } - if(maxY > 0 && deltaY > maxY) { throw new MultiblockValidationException(String.format("Machine is too large, it may be at most %d blocks in the Y dimension", maxY)); } - if(maxZ > 0 && deltaZ > maxZ) { throw new MultiblockValidationException(String.format("Machine is too large, it may be at most %d blocks in the Z dimension", maxZ)); } - if(deltaX < minX) { throw new MultiblockValidationException(String.format("Machine is too small, it must be at least %d blocks in the X dimension", minX)); } - if(deltaY < minY) { throw new MultiblockValidationException(String.format("Machine is too small, it must be at least %d blocks in the Y dimension", minY)); } - if(deltaZ < minZ) { throw new MultiblockValidationException(String.format("Machine is too small, it must be at least %d blocks in the Z dimension", minZ)); } - - // Now we run a simple check on each block within that volume. - // Any block deviating = NO DEAL SIR - TileEntity te; - RectangularMultiblockTileEntityBase part; - Class myClass = this.getClass(); - - for(int x = minimumCoord.x; x <= maximumCoord.x; x++) { - for(int y = minimumCoord.y; y <= maximumCoord.y; y++) { - for(int z = minimumCoord.z; z <= maximumCoord.z; z++) { - // Okay, figure out what sort of block this should be. - - te = this.worldObj.getTileEntity(x, y, z); - if(te instanceof RectangularMultiblockTileEntityBase) { - part = (RectangularMultiblockTileEntityBase)te; - - // Ensure this part should actually be allowed within a cube of this controller's type - if(!myClass.equals(part.getMultiblockControllerType())) - { - throw new MultiblockValidationException(String.format("Part @ %d, %d, %d is incompatible with machines of type %s", x, y, z, myClass.getSimpleName())); - } - } - else { - // This is permitted so that we can incorporate certain non-multiblock parts inside interiors - part = null; - } - - // Validate block type against both part-level and material-level validators. - int extremes = 0; - if(x == minimumCoord.x) { extremes++; } - if(y == minimumCoord.y) { extremes++; } - if(z == minimumCoord.z) { extremes++; } - - if(x == maximumCoord.x) { extremes++; } - if(y == maximumCoord.y) { extremes++; } - if(z == maximumCoord.z) { extremes++; } - - if(extremes >= 2) { - if(part != null) { - part.isGoodForFrame(); - } - else { - isBlockGoodForFrame(this.worldObj, x, y, z); - } - } - else if(extremes == 1) { - if(y == maximumCoord.y) { - if(part != null) { - part.isGoodForTop(); - } - else { - isBlockGoodForTop(this.worldObj, x, y, z); - } - } - else if(y == minimumCoord.y) { - if(part != null) { - part.isGoodForBottom(); - } - else { - isBlockGoodForBottom(this.worldObj, x, y, z); - } - } - else { - // Side - if(part != null) { - part.isGoodForSides(); - } - else { - isBlockGoodForSides(this.worldObj, x, y, z); - } - } - } - else { - if(part != null) { - part.isGoodForInterior(); - } - else { - isBlockGoodForInterior(this.worldObj, x, y, z); - } - } - } - } - } - } - +public abstract class RectangularMultiblockControllerBase extends MultiblockControllerBase { + + protected RectangularMultiblockControllerBase(World world) { + super(world); + } + + /** + * @return True if the machine is "whole" and should be assembled. False otherwise. + */ + protected void isMachineWhole() throws MultiblockValidationException { + if (connectedParts.size() < getMinimumNumberOfBlocksForAssembledMachine()) { + throw new MultiblockValidationException("Machine is too small."); + } + + CoordTriplet maximumCoord = getMaximumCoord(); + CoordTriplet minimumCoord = getMinimumCoord(); + + // Quickly check for exceeded dimensions + int deltaX = maximumCoord.x - minimumCoord.x + 1; + int deltaY = maximumCoord.y - minimumCoord.y + 1; + int deltaZ = maximumCoord.z - minimumCoord.z + 1; + + int maxX = getMaximumXSize(); + int maxY = getMaximumYSize(); + int maxZ = getMaximumZSize(); + int minX = getMinimumXSize(); + int minY = getMinimumYSize(); + int minZ = getMinimumZSize(); + + if (maxX > 0 && deltaX > maxX) { + throw new MultiblockValidationException( + String.format("Machine is too large, it may be at most %d blocks in the X dimension", maxX)); + } + if (maxY > 0 && deltaY > maxY) { + throw new MultiblockValidationException( + String.format("Machine is too large, it may be at most %d blocks in the Y dimension", maxY)); + } + if (maxZ > 0 && deltaZ > maxZ) { + throw new MultiblockValidationException( + String.format("Machine is too large, it may be at most %d blocks in the Z dimension", maxZ)); + } + if (deltaX < minX) { + throw new MultiblockValidationException( + String.format("Machine is too small, it must be at least %d blocks in the X dimension", minX)); + } + if (deltaY < minY) { + throw new MultiblockValidationException( + String.format("Machine is too small, it must be at least %d blocks in the Y dimension", minY)); + } + if (deltaZ < minZ) { + throw new MultiblockValidationException( + String.format("Machine is too small, it must be at least %d blocks in the Z dimension", minZ)); + } + + // Now we run a simple check on each block within that volume. + // Any block deviating = NO DEAL SIR + TileEntity te; + RectangularMultiblockTileEntityBase part; + Class myClass = this.getClass(); + + for (int x = minimumCoord.x; x <= maximumCoord.x; x++) { + for (int y = minimumCoord.y; y <= maximumCoord.y; y++) { + for (int z = minimumCoord.z; z <= maximumCoord.z; z++) { + // Okay, figure out what sort of block this should be. + + te = this.worldObj.getTileEntity(x, y, z); + if (te instanceof RectangularMultiblockTileEntityBase) { + part = (RectangularMultiblockTileEntityBase) te; + + // Ensure this part should actually be allowed within a cube of this controller's type + if (!myClass.equals(part.getMultiblockControllerType())) { + throw new MultiblockValidationException( + String.format( + "Part @ %d, %d, %d is incompatible with machines of type %s", + x, + y, + z, + myClass.getSimpleName())); + } + } else { + // This is permitted so that we can incorporate certain non-multiblock parts inside interiors + part = null; + } + + // Validate block type against both part-level and material-level validators. + int extremes = 0; + if (x == minimumCoord.x) { + extremes++; + } + if (y == minimumCoord.y) { + extremes++; + } + if (z == minimumCoord.z) { + extremes++; + } + + if (x == maximumCoord.x) { + extremes++; + } + if (y == maximumCoord.y) { + extremes++; + } + if (z == maximumCoord.z) { + extremes++; + } + + if (extremes >= 2) { + if (part != null) { + part.isGoodForFrame(); + } else { + isBlockGoodForFrame(this.worldObj, x, y, z); + } + } else if (extremes == 1) { + if (y == maximumCoord.y) { + if (part != null) { + part.isGoodForTop(); + } else { + isBlockGoodForTop(this.worldObj, x, y, z); + } + } else if (y == minimumCoord.y) { + if (part != null) { + part.isGoodForBottom(); + } else { + isBlockGoodForBottom(this.worldObj, x, y, z); + } + } else { + // Side + if (part != null) { + part.isGoodForSides(); + } else { + isBlockGoodForSides(this.worldObj, x, y, z); + } + } + } else { + if (part != null) { + part.isGoodForInterior(); + } else { + isBlockGoodForInterior(this.worldObj, x, y, z); + } + } + } + } + } + } + } diff --git a/src/main/java/erogenousbeef/core/multiblock/rectangular/RectangularMultiblockTileEntityBase.java b/src/main/java/erogenousbeef/core/multiblock/rectangular/RectangularMultiblockTileEntityBase.java index 8353179..6f05cfa 100644 --- a/src/main/java/erogenousbeef/core/multiblock/rectangular/RectangularMultiblockTileEntityBase.java +++ b/src/main/java/erogenousbeef/core/multiblock/rectangular/RectangularMultiblockTileEntityBase.java @@ -1,106 +1,109 @@ package erogenousbeef.core.multiblock.rectangular; import net.minecraftforge.common.util.ForgeDirection; + import erogenousbeef.core.common.CoordTriplet; import erogenousbeef.core.multiblock.MultiblockControllerBase; import erogenousbeef.core.multiblock.MultiblockTileEntityBase; import erogenousbeef.core.multiblock.MultiblockValidationException; -public abstract class RectangularMultiblockTileEntityBase extends - MultiblockTileEntityBase { - - PartPosition position; - ForgeDirection outwards; - - public RectangularMultiblockTileEntityBase() { - super(); - - position = PartPosition.Unknown; - outwards = ForgeDirection.UNKNOWN; - } - - // Positional Data - public ForgeDirection getOutwardsDir() { - return outwards; - } - - public PartPosition getPartPosition() { - return position; - } - - // Handlers from MultiblockTileEntityBase - @Override - public void onAttached(MultiblockControllerBase newController) { - super.onAttached(newController); - recalculateOutwardsDirection(newController.getMinimumCoord(), newController.getMaximumCoord()); - } - - - @Override - public void onMachineAssembled(MultiblockControllerBase controller) { - CoordTriplet maxCoord = controller.getMaximumCoord(); - CoordTriplet minCoord = controller.getMinimumCoord(); - - // Discover where I am on the reactor - recalculateOutwardsDirection(minCoord, maxCoord); - } - - @Override - public void onMachineBroken() { - position = PartPosition.Unknown; - outwards = ForgeDirection.UNKNOWN; - } - - // Positional helpers - public void recalculateOutwardsDirection(CoordTriplet minCoord, CoordTriplet maxCoord) { - outwards = ForgeDirection.UNKNOWN; - position = PartPosition.Unknown; - - int facesMatching = 0; - if(maxCoord.x == this.xCoord || minCoord.x == this.xCoord) { facesMatching++; } - if(maxCoord.y == this.yCoord || minCoord.y == this.yCoord) { facesMatching++; } - if(maxCoord.z == this.zCoord || minCoord.z == this.zCoord) { facesMatching++; } - - if(facesMatching <= 0) { position = PartPosition.Interior; } - else if(facesMatching >= 3) { position = PartPosition.FrameCorner; } - else if(facesMatching == 2) { position = PartPosition.Frame; } - else { - // 1 face matches - if(maxCoord.x == this.xCoord) { - position = PartPosition.EastFace; - outwards = ForgeDirection.EAST; - } - else if(minCoord.x == this.xCoord) { - position = PartPosition.WestFace; - outwards = ForgeDirection.WEST; - } - else if(maxCoord.z == this.zCoord) { - position = PartPosition.SouthFace; - outwards = ForgeDirection.SOUTH; - } - else if(minCoord.z == this.zCoord) { - position = PartPosition.NorthFace; - outwards = ForgeDirection.NORTH; - } - else if(maxCoord.y == this.yCoord) { - position = PartPosition.TopFace; - outwards = ForgeDirection.UP; - } - else { - position = PartPosition.BottomFace; - outwards = ForgeDirection.DOWN; - } - } - } - - ///// Validation Helpers (IMultiblockPart) - public abstract void isGoodForFrame() throws MultiblockValidationException; - - public abstract void isGoodForSides() throws MultiblockValidationException; - - public abstract void isGoodForTop() throws MultiblockValidationException; - - public abstract void isGoodForBottom() throws MultiblockValidationException; - - public abstract void isGoodForInterior() throws MultiblockValidationException; +public abstract class RectangularMultiblockTileEntityBase extends MultiblockTileEntityBase { + + PartPosition position; + ForgeDirection outwards; + + public RectangularMultiblockTileEntityBase() { + super(); + + position = PartPosition.Unknown; + outwards = ForgeDirection.UNKNOWN; + } + + // Positional Data + public ForgeDirection getOutwardsDir() { + return outwards; + } + + public PartPosition getPartPosition() { + return position; + } + + // Handlers from MultiblockTileEntityBase + @Override + public void onAttached(MultiblockControllerBase newController) { + super.onAttached(newController); + recalculateOutwardsDirection(newController.getMinimumCoord(), newController.getMaximumCoord()); + } + + @Override + public void onMachineAssembled(MultiblockControllerBase controller) { + CoordTriplet maxCoord = controller.getMaximumCoord(); + CoordTriplet minCoord = controller.getMinimumCoord(); + + // Discover where I am on the reactor + recalculateOutwardsDirection(minCoord, maxCoord); + } + + @Override + public void onMachineBroken() { + position = PartPosition.Unknown; + outwards = ForgeDirection.UNKNOWN; + } + + // Positional helpers + public void recalculateOutwardsDirection(CoordTriplet minCoord, CoordTriplet maxCoord) { + outwards = ForgeDirection.UNKNOWN; + position = PartPosition.Unknown; + + int facesMatching = 0; + if (maxCoord.x == this.xCoord || minCoord.x == this.xCoord) { + facesMatching++; + } + if (maxCoord.y == this.yCoord || minCoord.y == this.yCoord) { + facesMatching++; + } + if (maxCoord.z == this.zCoord || minCoord.z == this.zCoord) { + facesMatching++; + } + + if (facesMatching <= 0) { + position = PartPosition.Interior; + } else if (facesMatching >= 3) { + position = PartPosition.FrameCorner; + } else if (facesMatching == 2) { + position = PartPosition.Frame; + } else { + // 1 face matches + if (maxCoord.x == this.xCoord) { + position = PartPosition.EastFace; + outwards = ForgeDirection.EAST; + } else if (minCoord.x == this.xCoord) { + position = PartPosition.WestFace; + outwards = ForgeDirection.WEST; + } else if (maxCoord.z == this.zCoord) { + position = PartPosition.SouthFace; + outwards = ForgeDirection.SOUTH; + } else if (minCoord.z == this.zCoord) { + position = PartPosition.NorthFace; + outwards = ForgeDirection.NORTH; + } else if (maxCoord.y == this.yCoord) { + position = PartPosition.TopFace; + outwards = ForgeDirection.UP; + } else { + position = PartPosition.BottomFace; + outwards = ForgeDirection.DOWN; + } + } + } + + ///// Validation Helpers (IMultiblockPart) + public abstract void isGoodForFrame() throws MultiblockValidationException; + + public abstract void isGoodForSides() throws MultiblockValidationException; + + public abstract void isGoodForTop() throws MultiblockValidationException; + + public abstract void isGoodForBottom() throws MultiblockValidationException; + + public abstract void isGoodForInterior() throws MultiblockValidationException; }