diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dba2530..2b84ae8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,9 +59,9 @@ jobs: if-no-files-found: error retention-days: 30 path: | - build/libs/SkysGrassSlabs-1.1.0.110021.jar - build/libs/SkysGrassSlabs-1.1.0.110021-sources.jar - build/libs/SkysGrassSlabs-1.1.0.110021-javadoc.jar + build/libs/SkysGrassSlabs-1.1.1.110021.jar + build/libs/SkysGrassSlabs-1.1.1.110021-sources.jar + build/libs/SkysGrassSlabs-1.1.1.110021-javadoc.jar build/release/SHA256SUMS CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dfb00b..083ce73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 1.1.1.110021 - Minecraft 1.10.2 + +- Added a Config screen to the Mods list, with help for each setting. Changes + take effect after restarting Minecraft. +- Improved smoothing where nearby decoration has already placed grass or + flowers. Trees, crops and occupied blocks are left alone; saved terrain stays + as it is. +- Kept exposed ground grassy beneath steps and corners when using Building + Pieces. Full-cover slabs keep their existing dirt support. +- Replaced repeated warnings about intentional legacy slab names with one + clear recovery notice. Saved IDs and recovery behaviour are unchanged. +- Fixed the mod information and Config button after Eclipse rebuilds. + ## 1.1.0.110021 - Minecraft 1.10.2 - Added optional shared grass rules for Building Pieces and safe generation diff --git a/README.md b/README.md index 8d4c53c..aef034c 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,8 @@ covers the other recipes, plant support and drops. ## Smoother terrain By default, grass slabs soften suitable one-block slopes in new Overworld -chunks. Existing terrain stays as it is. You can turn generation off in +chunks. Existing terrain stays as it is. To turn generation off, select the mod +in **Mods → Config**, save your choice and restart Minecraft. You can also edit `config/skysgrassslabs.cfg`; see [configuration](docs/CONFIGURATION.md). The mod works on its own. If you install diff --git a/build.gradle b/build.gradle index 5c04908..f3cc7b3 100644 --- a/build.gradle +++ b/build.gradle @@ -223,6 +223,8 @@ dependencies { testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.10.2' } +apply from: 'gradle/runtime-log-checks.gradle' + def integrationTestClasses = layout.buildDirectory.dir('integration-test-mod/classes') def compileIntegrationTestMod = tasks.register('compileIntegrationTestMod', JavaCompile) { group = 'verification' @@ -268,11 +270,16 @@ def prepareIntegrationTest = tasks.register('prepareIntegrationTest') { into mods } new File(runDirectory, 'eula.txt').setText('eula=true\n', 'UTF-8') + ['banned-players.json', 'banned-ips.json', 'ops.json', 'whitelist.json'].each { name -> + new File(runDirectory, name).setText('[]\n', 'UTF-8') + } new File(runDirectory, 'server.properties').setText('''\ level-name=world level-seed=skys-grass-slabs-integration level-type=default online-mode=false +server-ip=127.0.0.1 +server-port=0 generate-structures=false spawn-animals=false spawn-monsters=false @@ -301,6 +308,14 @@ def createIntegrationProcess = { String phase, Object dependency -> if (values.getProperty("${phase.toLowerCase(Locale.ROOT)}_complete") != 'true') { throw new GradleException("${phase} integration did not complete: ${marker}") } + File log = new File(integrationRunDirectory.get().asFile, 'logs/latest.log') + File archived = layout.buildDirectory.file("integration-logs/${phase.toLowerCase(Locale.ROOT)}.log").get().asFile + archived.parentFile.mkdirs() + archived.bytes = log.bytes + project.ext.auditRuntimeLog(archived, [ + kind: 'development', side: 'server', + directory: integrationRunDirectory.get().asFile + ]) } } } @@ -497,6 +512,7 @@ tasks.register('writeReleaseChecksums') { group = 'verification' dependsOn tasks.named('verifyReleaseArtifacts') def output = layout.buildDirectory.file('release/SHA256SUMS') + inputs.files(releaseFiles.map { names -> names.collect { layout.buildDirectory.file("libs/${it}") } }) outputs.file(output) doLast { File libs = layout.buildDirectory.dir('libs').get().asFile @@ -610,8 +626,27 @@ idea { module { downloadSources = true; downloadJavadoc = true } } eclipse { - classpath { downloadSources = true; downloadJavadoc = true } - synchronizationTasks 'genEclipseRuns' + classpath { + downloadSources = true + downloadJavadoc = true + file.whenMerged { classpath -> + // Eclipse copies these files on every build. Use Gradle's expanded metadata, never the template. + def resources = classpath.entries.findAll { + it instanceof org.gradle.plugins.ide.eclipse.model.SourceFolder && + it.path in ['src/main/resources', 'build/resources/main'] + } + // Keep one entry when Gradle merges an existing Eclipse classpath. + resources.drop(1).each { classpath.entries.remove(it) } + resources.take(1).each { entry -> + entry.path = 'build/resources/main' + } + } + } + synchronizationTasks 'processResources', 'genEclipseRuns' +} + +tasks.named('eclipseClasspath') { + dependsOn tasks.named('processResources') } def prepareEclipseResources = tasks.register('prepareEclipseResources') { @@ -691,6 +726,12 @@ tasks.register('verifyEclipseProductionClasspath') { throw new GradleException('Eclipse Buildship uses the wrong Gradle home') } File metadata = file('bin/main/mcmod.info') + def eclipseClasspath = new groovy.xml.XmlSlurper().parse(file('.classpath')) + def resourceFolders = eclipseClasspath.classpathentry.findAll { it.@kind == 'src' }*.@path*.text() + if (resourceFolders.contains('src/main/resources') || + resourceFolders.count { it == 'build/resources/main' } != 1) { + throw new GradleException('Eclipse must copy processed resources, not overwrite them with templates') + } def parsed = new JsonSlurper().parseText(metadata.getText('UTF-8')) if (parsed[0].version != project.mod_version || parsed[0].mcversion != project.minecraft_version) { throw new GradleException('Eclipse output contains incorrect mcmod.info') diff --git a/docs/BUILDING.md b/docs/BUILDING.md new file mode 100644 index 0000000..af1b7a7 --- /dev/null +++ b/docs/BUILDING.md @@ -0,0 +1,42 @@ +# Building and checking this branch + +Use the Gradle wrapper with Java 17 and set `SGS_JAVA8_HOME` to the Java 8 +installation used for Minecraft. Run these commands from the checkout: + +```text +gradlew clean check build javadoc genEclipseRuns verifyEclipseProductionClasspath verifyReleaseArtifacts writeReleaseChecksums +``` + +The checks include unit tests and disposable fresh/reload servers. Test mods +are not included in the main jar or ordinary Eclipse launches. + +## Runtime logs + +Each integration server log is saved and checked before the next process can +overwrite it. An unexpected warning, error, crash, missing recovery notice or +remaining legacy-prefix warning fails the check. + +To check another captured verification log: + +```text +gradlew verifyRuntimeLog -PruntimeLog= -PruntimeDirectory= -PruntimeKind=packaged -PruntimeSide=server +``` + +Use `development` only for a mapped development launch and `client` for a +client log. Offline-server warnings are accepted only when that test folder +is configured for loopback networking on an automatically allocated port. +Use `-PrecoveryMessage=false` when the original legacy mod is installed. + +The development allowances cover specific old-Forge launcher and class-scanner +diagnostics, with their expected file names and paired messages. They do not +permit arbitrary corrupt jars or missing classes. Packaged runs do not inherit +those allowances. Unrelated warnings remain visible in the game logs. + +For the combined 1.10 development checks, `-PruntimeLocalMineralogy=true` allows +the unsigned local Mineralogy build's fingerprint warning only after verifying +its exact jar checksum. It does not allow fingerprint warnings from other jars, +and it does not hide the warning in the game. + +The 1.12 branch also checks cross-version fixtures against reviewed diagnostics +tied to their archive checksums. Those exceptions cover the test-only generator +mod and exact vanilla numeric-ID changes, not ordinary worlds. diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 13c4886..54be2c9 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -1,9 +1,12 @@ # Configuration -Forge creates `config/skysgrassslabs.cfg` after the mod is started for the -first time. Stop the game or server before editing it, then restart to apply -changes. On a dedicated server, the server's configuration controls world -generation and world upgrades. +Open **Mods**, select **Sky's Grass Slabs** and choose **Config** to change +world generation and the legacy replacement option. Save with **Done**, then +restart Minecraft to apply your choices. Settings are not changed mid-world. + +You can also edit `config/skysgrassslabs.cfg` while the game or server is +stopped. On a dedicated server, edit the server's file and restart it; the +client's Config screen does not change a remote server. ## World smoothing diff --git a/docs/INTEGRATION.md b/docs/INTEGRATION.md index aa5b619..d93e383 100644 --- a/docs/INTEGRATION.md +++ b/docs/INTEGRATION.md @@ -1,5 +1,11 @@ # Optional content integration +`coversSupport(state, world, pos)` reports whether a piece covers the whole +horizontal footprint. This method is available from 1.1.1. Support repair leaves +grass beneath narrower steps and corners intact, and that exposed soil can +regrow. Full-cover slabs and turf +retain their previous support rules. + Grass Slabs remains standalone. Terrain Smoother 0.1 uses Grass Slabs 1.1's version 1 API in `zone.moddev.mc.skysgrassslabs.api.GrassSlabsApi`. diff --git a/docs/RELEASE-1.1.1.110021.md b/docs/RELEASE-1.1.1.110021.md new file mode 100644 index 0000000..bb59b0f --- /dev/null +++ b/docs/RELEASE-1.1.1.110021.md @@ -0,0 +1,20 @@ +# Sky's Grass Slabs 1.1.1 for Minecraft 1.10.2 + +You can now change the world-generation and legacy replacement settings from +**Mods → Sky's Grass Slabs → Config**. Each setting has help text. Save your +changes and restart Minecraft to apply them. + +Grass slab smoothing now handles small plants that neighbouring decoration has +already placed on a slope. Trees, crops and occupied blocks stay protected, +and existing terrain is unchanged. + +With Building Pieces installed, exposed grass beneath steps and corners stays +grassy. Full-cover slabs keep their usual dirt support. Grass Slabs still works +on its own, with the same blocks, recipes, configuration defaults and saved +migration progress. + +Legacy recovery now explains its intentional registrations with one startup +notice. The historical IDs and recovery rules have not changed. + +Requires Minecraft 1.10.2, Forge 12.18.3.2511 and Java 8. Back up your world before +updating mods. diff --git a/gradle.properties b/gradle.properties index cff9d61..8c995b7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -23,7 +23,7 @@ curseforge_project_id=1677588 mod_id=skysgrassslabs mod_name=Sky's Grass Slabs mod_license=LGPL-2.1-only -mod_version=1.1.0.110021 +mod_version=1.1.1.110021 mod_group=zone.moddev.mc.skysgrassslabs mod_authors=SkyBlade1978 mod_description=Adds dirt, grass and path slabs, turf, natural grass spreading and smoother Overworld terrain. diff --git a/gradle/runtime-log-checks.gradle b/gradle/runtime-log-checks.gradle new file mode 100644 index 0000000..47de32a --- /dev/null +++ b/gradle/runtime-log-checks.gradle @@ -0,0 +1,342 @@ +// Audit whole runtime logs, not just the final pass marker. +def recoveryNotice = 'Legacy slab recovery enabled for buildingbricks:grass_slab, buildingbricks:dirt_slab and buildingbrickscompatvanilla:grass_slab. Saved blocks and items will be converted as they load.' +def scanEntries = [ + 'slime-launcher-0.2.2.jar': 'META-INF/versions/11/net/minecraftforge/launcher/shadow/util/download/DownloadUtilsImpl.class' +] +if (project.minecraft_version == '1.12.2') { + ['asm', 'asm-analysis', 'asm-tree', 'asm-util'].each { + scanEntries[it + '-6.2.jar'] = 'module-info.class' + } + scanEntries['slf4j-api-1.8.0-beta4.jar'] = 'META-INF/versions/9/module-info.class' +} + +ext.auditRuntimeLogText = { String text, Map options -> + if (!(options.kind in ['development', 'packaged']) || !(options.side in ['client', 'server'])) { + throw new GradleException('Runtime kind and side must be explicit') + } + boolean development = options.kind == 'development' + boolean client = options.side == 'client' + List records = [] + def header = ~/^\[[^\]]+\] \[([^\]]+)\/(TRACE|DEBUG|INFO|WARN|ERROR|FATAL)\] \[([^\]]+)\]: (.*)$/ + text.readLines().each { line -> + def match = line =~ header + if (match.matches()) { + records.add([level: match[0][2], logger: match[0][3], + message: match[0][4], body: '']) + } else { + def shortHeader = line =~ /^\[[^\]]+\] \[[^\]]+\/(WARN|ERROR|FATAL)\]: (.*)$/ + def status = line =~ /^(?:\d{4}-\d\d-\d\d [0-9:,.]+ (?:[A-Za-z0-9 #._-]+ )?)?(WARN|ERROR|FATAL) (.*)$/ + if (shortHeader.matches()) { + records.add([level: shortHeader[0][1], logger: 'Unlabelled', + message: shortHeader[0][2], body: '']) + } else if (status.matches()) { + records.add([level: status[0][1], logger: 'Log4jStatus', + message: status[0][2], body: '']) + } else if (!records.isEmpty()) { + records.last().body += line + '\n' + } + } + } + if (records.isEmpty()) { + throw new GradleException('Runtime log contains no recognised log records') + } + + Set knownDiagnostics = [] as Set + if (development) { + // Old Forge cannot scan these exact Java 9/11 launcher descriptors. + // Require the paired class-reader failure and entry, not "corrupt zip" generally. + for (int index = 0; index + 2 < records.size(); index++) { + def failure = records[index] + def entry = records[index + 1] + def warning = records[index + 2] + if (failure.logger == 'FML' && failure.level == 'ERROR' && + failure.message == 'Unable to read a class file correctly' && + failure.body.contains('org.objectweb.asm.ClassReader') && + failure.body.contains('java.lang.IllegalArgumentException')) { + scanEntries.each { jar, classEntry -> + String prefix = 'There was a problem reading the entry ' + classEntry + ' in the jar ' + if (entry.logger == 'FML' && entry.level == 'ERROR' && + entry.message.startsWith(prefix) && + entry.message.replace('\\', '/').endsWith('/' + jar + ' - probably a corrupt zip') && + warning.logger == 'FML' && warning.level == 'WARN' && + warning.message == 'Zip file ' + jar + ' failed to read properly, it will be ignored') { + knownDiagnostics.addAll([index, index + 1, index + 2]) + } + } + } + + // Forge 1.12 expects Maven's flat layout, not Gradle's hash directory. + if (project.minecraft_version == '1.12.2' && failure.logger == 'FML' && + failure.level == 'ERROR' && + failure.message == 'Apache Maven library folder was not in the format expected. Using default libraries directory.') { + String full = entry.message.replace('\\', '/') + def expectedJar = full =~ /^Full: (.+\/org\.apache\.maven\/maven-artifact\/3\.5\.3\/)[a-f0-9]{40}\/maven-artifact-3\.5\.3\.jar$/ + if (expectedJar.matches() && entry.logger == 'FML' && entry.level == 'ERROR' && + warning.logger == 'FML' && warning.level == 'ERROR' && + warning.message.replace('\\', '/') == 'Trimmed: ' + expectedJar[0][1].toLowerCase(Locale.ROOT)) { + knownDiagnostics.addAll([index, index + 1, index + 2]) + } + } + } + } + + List offlineMessages = [ + '**** SERVER IS RUNNING IN OFFLINE/INSECURE MODE!', + 'The server will make no attempt to authenticate usernames. Beware.', + 'While this makes the game possible to play without internet access, it also opens up the ability for hackers to connect with any username they choose.', + 'To change this, set "online-mode" to "true" in the server.properties file.' + ] + List unexpected = [] + int reviewed = 0 + records.eachWithIndex { record, index -> + if (!(record.level in ['WARN', 'ERROR', 'FATAL'])) { + return + } + boolean allowed = knownDiagnostics.contains(index) + if (record.logger == 'FML' && record.body.trim().isEmpty()) { + allowed |= record.level == 'ERROR' && + record.message == 'FML appears to be missing any signature data. This is not a good thing' && + (development || project.minecraft_version == '1.10.2') + allowed |= development && record.level == 'ERROR' && + record.message == 'The binary patch set is missing. Either you are in a development environment, or things are not going to work!' + allowed |= project.minecraft_version == '1.12.2' && record.level == 'WARN' && + record.message == 'Missing English translation for FML: assets/fml/lang/en_us.lang' + } + allowed |= options.offlineTestServer && !client && record.level == 'WARN' && + record.logger == 'net.minecraft.server.dedicated.DedicatedServer' && + offlineMessages.contains(record.message) && record.body.trim().isEmpty() + allowed |= client && record.level == 'WARN' && + record.logger == 'net.minecraft.client.audio.SoundManager' && + record.message == 'Author: Paul Lamb, www.paulscode.com' && record.body.trim().isEmpty() + allowed |= development && record.level == 'WARN' && record.logger == 'Log4jStatus' && + record.message == 'Unable to instantiate org.fusesource.jansi.WindowsAnsiOutputStream' + // This unsigned local Mineralogy build has a retained upstream fingerprint. + // Its warning stays in the log and is accepted only after checking the jar. + allowed |= options.verifiedLocalMineralogy && record.level == 'WARN' && + record.logger == 'mineralogy' && record.message == 'Invalid fingerprint detected!' && + record.body.trim().isEmpty() + allowed |= !development && record.body.trim().isEmpty() && (options.reviewedFixtureMessages ?: []).any { + it.level == record.level && it.logger == record.logger && it.message == record.message + } + if (!allowed) { + unexpected.add(record.level + ' [' + record.logger + '] ' + record.message) + } else { + reviewed++ + } + } + if (!unexpected.isEmpty()) { + throw new GradleException('Unexpected runtime diagnostics:\n' + unexpected.join('\n')) + } + if (text =~ /(?i)dangerous alternative prefix|Encountered an unexpected exception|OutOfMemoryError|AssertionError|Exception in thread|MissingModsException|cascading worldgen lag|IllegalAccessError|NoClassDefFoundError|NoSuchMethodError|VerifyError|UnsatisfiedLinkError|StackOverflowError|ExceptionInInitializerError|ClassCastException/) { + throw new GradleException('Runtime log contains an unsuppressed registry warning, crash or world-generation failure') + } + int notices = records.count { + it.level == 'INFO' && it.logger == 'skysgrassslabs' && it.message == recoveryNotice + } + int expectedNotices = options.recoveryMessage == false ? 0 : 1 + if (notices != expectedNotices) { + throw new GradleException('Expected ' + expectedNotices + ' legacy recovery notices, found ' + notices) + } + if (options.marker && !text.contains(options.marker.toString())) { + throw new GradleException('Runtime log is missing its completion marker: ' + options.marker) + } + [records: records.size(), reviewedDiagnostics: reviewed] +} + +def fixturePolicies = file('gradle/runtime-fixture-diagnostics.json').isFile() ? + new groovy.json.JsonSlurper().parse(file('gradle/runtime-fixture-diagnostics.json')).fixtures : [:] + +ext.verifyRuntimeFixture = { File archive -> + def policy = fixturePolicies[archive.name] + if (policy == null || file(policy.path).canonicalFile != archive.canonicalFile) { + throw new GradleException('No reviewed log policy for this upgrade fixture: ' + archive.name) + } + String actual = java.security.MessageDigest.getInstance('SHA-256').digest(archive.bytes) + .encodeHex().toString().toUpperCase(Locale.ROOT) + if (actual != policy.sha256) { + throw new GradleException('Upgrade fixture does not match its reviewed log policy: ' + archive.name) + } + [policy.blocks, policy.sounds].each { entries -> + entries.each { name, ids -> + if (!name.startsWith('minecraft:') || ids.size() != 2 || ids.any { it < 0 }) { + throw new GradleException('Fixture allowances must name exact vanilla numeric reassignments') + } + } + } + policy +} + +ext.writeRuntimeFixtureContext = { File directory, File archive -> + def policy = project.ext.verifyRuntimeFixture(archive) + new File(directory, 'verification-fixture.json').setText( + groovy.json.JsonOutput.toJson([fixture: archive.name, sha256: policy.sha256]) + '\n', 'UTF-8') +} + +def reviewedFixtureMessages = { File directory -> + File contextFile = new File(directory, 'verification-fixture.json') + if (!contextFile.isFile()) { + return [] + } + def context = new groovy.json.JsonSlurper().parse(contextFile) + def policy = fixturePolicies[context.fixture] + if (policy == null || policy.sha256 != context.sha256) { + throw new GradleException('Unrecognised upgrade-fixture verification context') + } + project.ext.verifyRuntimeFixture(file(policy.path)) + List messages = [[level: 'ERROR', logger: 'FML.ModTracker', + message: 'This world was saved with mod ' + policy.generator + + ' which appears to be missing, things may not work well']] + messages.addAll(policy.extraDiagnostics ?: []) + [Block: policy.blocks, SoundEvent: policy.sounds].each { type, entries -> + entries.each { name, ids -> + messages.add([level: 'WARN', logger: 'FML', + message: 'Registry ' + type + ': Object did not get ID it asked for. Name: ' + + name + ' Expected: ' + ids[0] + ' Got: ' + ids[1]]) + } + } + messages +} + +ext.auditRuntimeLog = { File log, Map options = [:] -> + if (!log.isFile()) { + throw new GradleException('Runtime log is missing: ' + log) + } + File directory = options.directory ? file(options.directory) : log.parentFile.parentFile + Properties settings = new Properties() + File serverProperties = new File(directory, 'server.properties') + if (serverProperties.isFile()) { + serverProperties.withInputStream { settings.load(it) } + } + Map verified = new LinkedHashMap(options) + verified.reviewedFixtureMessages = reviewedFixtureMessages(directory) + verified.verifiedLocalMineralogy = false + if (providers.gradleProperty('runtimeLocalMineralogy').getOrElse('false').toBoolean()) { + File mineralogy = new File(directory, 'mods/Mineralogy-6.0.2.110021.jar') + String expectedHash = '48841ABD9517127D1F0D17960AAD86FEFB251D340B006872ACECADD01A4B43F9' + if (project.minecraft_version != '1.10.2' || !mineralogy.isFile() || + java.security.MessageDigest.getInstance('SHA-256').digest(mineralogy.bytes) + .encodeHex().toString().toUpperCase(Locale.ROOT) != expectedHash) { + throw new GradleException('The reviewed local Mineralogy jar is missing or has changed') + } + verified.verifiedLocalMineralogy = true + } + verified.offlineTestServer = settings.getProperty('online-mode') == 'false' && + settings.getProperty('server-ip') == '127.0.0.1' && settings.getProperty('server-port') == '0' + def result = project.ext.auditRuntimeLogText(log.getText('UTF-8'), verified) + logger.lifecycle('Runtime log audit passed: ' + log.name + ' (' + result.records + + ' records; ' + result.reviewedDiagnostics + ' reviewed diagnostics)') +} + +tasks.register('verifyRuntimeLog') { + group = 'verification' + description = 'Checks a complete development or packaged runtime log for unexpected warnings and errors.' + doLast { + if (!providers.gradleProperty('runtimeLog').isPresent()) { + throw new GradleException('Supply -PruntimeLog=, -PruntimeKind=development|packaged and -PruntimeSide=client|server') + } + project.ext.auditRuntimeLog(file(providers.gradleProperty('runtimeLog').get()), [ + kind: providers.gradleProperty('runtimeKind').getOrElse('packaged'), + side: providers.gradleProperty('runtimeSide').getOrElse('server'), + recoveryMessage: providers.gradleProperty('recoveryMessage').getOrElse('true').toBoolean(), + directory: providers.gradleProperty('runtimeDirectory').orNull, + marker: providers.gradleProperty('runtimeMarker').orNull + ]) + } +} + +tasks.register('verifyRuntimeLogPolicy') { + group = 'verification' + description = 'Tests strict warning/error classification and the recovery startup notice.' + doLast { + String notice = '[00:00:00] [main/INFO] [skysgrassslabs]: ' + recoveryNotice + '\n' + Map options = [kind: 'packaged', side: 'server'] + project.ext.auditRuntimeLogText(notice, options) + fixturePolicies.each { name, policy -> + project.ext.verifyRuntimeFixture(file(policy.path)) + String originalHash = policy.sha256 + try { + policy.sha256 = '0' * 64 + try { + project.ext.verifyRuntimeFixture(file(policy.path)) + throw new IllegalStateException('A changed fixture hash was accepted') + } catch (GradleException expected) { + // A reviewed warning list must never authorise another input world. + } + } finally { + policy.sha256 = originalHash + } + } + int rejected = 0 + List badLogs = [ + '', + '[00:00:00] [main/INFO] [FML]: Ordinary startup without a recovery notice\n', + notice + '[00:00:01] [main/WARN] [FML]: An unexpected warning\n', + notice + '[00:00:01] [main/ERROR] [FML]: An unexpected error\n', + notice + '[00:00:01] [main/FATAL] [FML]: A fatal error\n', + notice + '[00:00:01] [main/WARN] [FML]: Dangerous alternative prefix buildingbricks for grass_slab\n', + notice + '[00:00:01] [main/WARN] [FML]: Zip file unknown.jar failed to read properly, it will be ignored\n', + notice + '[00:00:01] [main/ERROR] [FML]: Unable to read a class file correctly\n', + notice + '[00:00:01] [main/ERROR] [FML]: The binary patch set is missing. Either you are in a development environment, or things are not going to work!\n', + notice + '[00:00:01] [main/WARN] [FML]: Author: Paul Lamb, www.paulscode.com\n', + notice + '2026-10-23 00:00:01,000 ERROR Logging failed\n', + notice + '2026-10-23 00:00:01,000 main WARN Unexpected status warning\n', + notice + '[00:00:01] [main/WARN]: Unlabelled warning\n', + notice + '[00:00:01] [main/WARN] [mineralogy]: Invalid fingerprint detected!\n', + notice + notice + ] + badLogs.each { text -> + try { + project.ext.auditRuntimeLogText(text, options) + throw new IllegalStateException('An unexpected log was accepted') + } catch (GradleException expected) { + rejected++ + } + } + String scan = '[00:00:01] [main/ERROR] [FML]: Unable to read a class file correctly\n' + + 'java.lang.IllegalArgumentException\n at org.objectweb.asm.ClassReader.read(Unknown Source)\n' + + '[00:00:01] [main/ERROR] [FML]: There was a problem reading the entry ' + + scanEntries['slime-launcher-0.2.2.jar'] + ' in the jar /cache/slime-launcher-0.2.2.jar - probably a corrupt zip\n' + + '[00:00:01] [main/WARN] [FML]: Zip file slime-launcher-0.2.2.jar failed to read properly, it will be ignored\n' + Map development = options + [kind: 'development'] + project.ext.auditRuntimeLogText(notice + + '[00:00:01] [main/WARN] [mineralogy]: Invalid fingerprint detected!\n', + options + [verifiedLocalMineralogy: true]) + project.ext.auditRuntimeLogText(notice + scan, development) + [ + scan.replace('DownloadUtilsImpl.class', 'AnUnrecognisedClass.class'), + scan.replace('slime-launcher-0.2.2.jar', 'unknown-library.jar'), + scan + 'java.lang.IllegalAccessError: A real launch failure\n' + ].each { altered -> + try { + project.ext.auditRuntimeLogText(notice + altered, development) + throw new IllegalStateException('An unrecognised scanner failure was accepted') + } catch (GradleException expected) { + rejected++ + } + } + String offline = '[00:00:01] [Server thread/WARN] [net.minecraft.server.dedicated.DedicatedServer]: ' + + '**** SERVER IS RUNNING IN OFFLINE/INSECURE MODE!\n' + project.ext.auditRuntimeLogText(notice + offline, options + [offlineTestServer: true]) + try { + project.ext.auditRuntimeLogText(notice + offline, options) + throw new IllegalStateException('Offline warnings were accepted without a disposable server configuration') + } catch (GradleException expected) { + rejected++ + } + Map fixture = options + [reviewedFixtureMessages: [ + [level: 'WARN', logger: 'FML', message: 'Reviewed fixture diagnostic']]] + String fixtureWarning = '[00:00:01] [main/WARN] [FML]: Reviewed fixture diagnostic\n' + project.ext.auditRuntimeLogText(notice + fixtureWarning, fixture) + try { + project.ext.auditRuntimeLogText(notice + fixtureWarning + + 'java.io.IOException: Unexpected fixture failure\n', fixture) + throw new IllegalStateException('A reviewed fixture message concealed an exception') + } catch (GradleException expected) { + rejected++ + } + logger.lifecycle('Runtime log policy passed: ' + rejected + ' unexpected-log cases rejected') + } +} +tasks.named('check') { + dependsOn 'verifyRuntimeLogPolicy' +} diff --git a/src/integrationTest/java/zone/moddev/mc/skysgrassslabs/integrationtest/FoliageChecks.java b/src/integrationTest/java/zone/moddev/mc/skysgrassslabs/integrationtest/FoliageChecks.java new file mode 100644 index 0000000..601be65 --- /dev/null +++ b/src/integrationTest/java/zone/moddev/mc/skysgrassslabs/integrationtest/FoliageChecks.java @@ -0,0 +1,89 @@ +package zone.moddev.mc.skysgrassslabs.integrationtest; + +import java.util.Random; +import net.minecraft.block.BlockDoublePlant; +import net.minecraft.block.BlockTallGrass; +import net.minecraft.block.state.IBlockState; +import net.minecraft.init.Blocks; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.WorldServer; +import net.minecraftforge.event.terraingen.DecorateBiomeEvent; +import zone.moddev.mc.skysgrassslabs.init.ModBlocks; +import zone.moddev.mc.skysgrassslabs.world.GrassSlabSmoothingHandler; + +/** Covers plants present before this owner's decoration, not a second generation pass. */ +final class FoliageChecks { + static int run(WorldServer world) { + BlockPos floor = new BlockPos(3528, 210, 3528); + BlockPos origin = new BlockPos(floor.getX() & ~15, 0, floor.getZ() & ~15); + GrassSlabSmoothingHandler handler = new GrassSlabSmoothingHandler(); + int checks = 0; + + for (IBlockState plant : new IBlockState[] { Blocks.TALLGRASS.getDefaultState(), + Blocks.TALLGRASS.getDefaultState().withProperty(BlockTallGrass.TYPE, BlockTallGrass.EnumType.FERN), + Blocks.YELLOW_FLOWER.getDefaultState(), Blocks.RED_FLOWER.getDefaultState(), + Blocks.DEADBUSH.getDefaultState() }) { + for (int arrangement = 0; arrangement < 3; arrangement++) { + prepare(world, floor); + if (arrangement != 1) { + world.setBlockState(floor.up(), plant, 2); + } + if (arrangement != 0) { + world.setBlockState(floor.east().up(2), plant, 2); + } + handler.beforeDecoration(new DecorateBiomeEvent.Pre(world, new Random(1), origin)); + require(world.getBlockState(floor.up()).getBlock() == ModBlocks.GRASS_SLAB + && world.getBlockState(floor).getBlock() == Blocks.DIRT, + "plants on either side must not hide a valid slope"); + checks++; + } + } + + prepare(world, floor); + world.setBlockState(floor.up(), Blocks.DOUBLE_PLANT.getDefaultState() + .withProperty(BlockDoublePlant.HALF, BlockDoublePlant.EnumBlockHalf.LOWER), 2); + world.setBlockState(floor.up(2), Blocks.DOUBLE_PLANT.getDefaultState() + .withProperty(BlockDoublePlant.HALF, BlockDoublePlant.EnumBlockHalf.UPPER), 2); + handler.beforeDecoration(new DecorateBiomeEvent.Pre(world, new Random(1), origin)); + require(world.getBlockState(floor.up()).getBlock() == ModBlocks.GRASS_SLAB + && world.isAirBlock(floor.up(2)), "tall plants leave no orphaned upper half"); + checks++; + + for (IBlockState obstacle : new IBlockState[] { Blocks.LEAVES.getDefaultState(), Blocks.LOG.getDefaultState(), + Blocks.WHEAT.getDefaultState(), Blocks.SAPLING.getDefaultState(), Blocks.VINE.getDefaultState(), + Blocks.CHEST.getDefaultState(), Blocks.WATER.getDefaultState() }) { + prepare(world, floor); + world.setBlockState(floor.up(), obstacle, 2); + handler.beforeDecoration(new DecorateBiomeEvent.Pre(world, new Random(1), origin)); + require(world.getBlockState(floor.up()).equals(obstacle) + && world.getBlockState(floor).getBlock() == Blocks.GRASS, + "trees, crops, vines, fluids and block entities remain intact"); + checks++; + } + + System.out.println("GRASS_SLAB_FOLIAGE_PASS checks=" + checks); + return checks; + } + + private static void prepare(WorldServer world, BlockPos floor) { + for (int offsetZ = -2; offsetZ <= 2; offsetZ++) { + for (int offsetX = -2; offsetX <= 2; offsetX++) { + BlockPos support = floor.add(offsetX, 0, offsetZ); + for (int height = 1; height <= 6; height++) { + world.setBlockState(support.up(height), Blocks.AIR.getDefaultState(), 2); + } + world.setBlockState(support, Blocks.GRASS.getDefaultState(), 2); + } + } + world.setBlockState(floor.east().up(), Blocks.GRASS.getDefaultState(), 2); + } + + private static void require(boolean condition, String message) { + if (!condition) { + throw new IllegalStateException(message); + } + } + + private FoliageChecks() { + } +} diff --git a/src/integrationTest/java/zone/moddev/mc/skysgrassslabs/integrationtest/IntegrationTestMod.java b/src/integrationTest/java/zone/moddev/mc/skysgrassslabs/integrationtest/IntegrationTestMod.java index 915f120..820537a 100644 --- a/src/integrationTest/java/zone/moddev/mc/skysgrassslabs/integrationtest/IntegrationTestMod.java +++ b/src/integrationTest/java/zone/moddev/mc/skysgrassslabs/integrationtest/IntegrationTestMod.java @@ -1508,7 +1508,7 @@ private static void verifyRecipes(World world) { } private static int verifyWorldgen(WorldServer world) { - int checks = 0; + int checks = FoliageChecks.run(world); int chunkX = (world.getSpawnPoint().getX() >> 4) + 4; int chunkZ = (world.getSpawnPoint().getZ() >> 4) + 4; @@ -1555,8 +1555,8 @@ && metadata(ModBlocks.GRASS_SLAB, world.getBlockState(lower.up())) == 1 prepareGrassSurface(world, replaceable.east().up()); world.setBlockState(replaceable.up(), Blocks.TALLGRASS.getDefaultState(), 3); handler.beforeDecoration(new DecorateBiomeEvent.Pre(world, new Random(5), chunkStart)); - require(world.getBlockState(replaceable.up()).getBlock() == Blocks.TALLGRASS, - "Smoothing overwrote replaceable provider decoration"); + require(world.getBlockState(replaceable.up()).getBlock() == ModBlocks.GRASS_SLAB, + "Grass placed by a neighbouring decorator blocked a valid transition"); checks++; BlockPos wet = new BlockPos((chunkX << 4) + 12, 192, (chunkZ << 4) + 12); diff --git a/src/main/java/zone/moddev/mc/skysgrassslabs/SkysGrassSlabs.java b/src/main/java/zone/moddev/mc/skysgrassslabs/SkysGrassSlabs.java index 1ed9aba..ed5566e 100644 --- a/src/main/java/zone/moddev/mc/skysgrassslabs/SkysGrassSlabs.java +++ b/src/main/java/zone/moddev/mc/skysgrassslabs/SkysGrassSlabs.java @@ -24,11 +24,12 @@ modid = SkysGrassSlabs.MOD_ID, name = SkysGrassSlabs.NAME, version = SkysGrassSlabs.VERSION, - dependencies = "before:buildingbricks") + dependencies = "before:buildingbricks", + guiFactory = "zone.moddev.mc.skysgrassslabs.client.GrassSlabsGuiFactory") public final class SkysGrassSlabs { public static final String MOD_ID = "skysgrassslabs"; public static final String NAME = "Sky's Grass Slabs"; - public static final String VERSION = "1.1.0.110021"; + public static final String VERSION = "1.1.1.110021"; @SidedProxy( clientSide = "zone.moddev.mc.skysgrassslabs.proxy.ClientProxy", diff --git a/src/main/java/zone/moddev/mc/skysgrassslabs/api/GrassSlabsApi.java b/src/main/java/zone/moddev/mc/skysgrassslabs/api/GrassSlabsApi.java index b4c59b5..f6604db 100644 --- a/src/main/java/zone/moddev/mc/skysgrassslabs/api/GrassSlabsApi.java +++ b/src/main/java/zone/moddev/mc/skysgrassslabs/api/GrassSlabsApi.java @@ -6,6 +6,8 @@ import net.minecraft.block.state.IBlockState; import net.minecraft.init.Blocks; import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.AxisAlignedBB; +import net.minecraft.world.IBlockAccess; import net.minecraft.world.World; import zone.moddev.mc.skysgrassslabs.block.GrassSpread; import zone.moddev.mc.skysgrassslabs.config.SkysGrassSlabsConfig; @@ -175,12 +177,13 @@ public static void tickGrass(World world, BlockPos pos, IBlockState state, Rando world.setBlockState(pos, dirt, 3); } } else if (isSpreadingGrassForm(state)) { - GrassSpread.spreadFrom(world, pos, random, pos.down()); + GrassSpread.spreadFrom(world, pos, random, coversSupport(state, world, pos) ? pos.down() : null); } } public static void repairSupport(World world, BlockPos pos) { - if (world.isRemote || !world.isBlockLoaded(pos.down())) { + if (world.isRemote || !world.isBlockLoaded(pos.down()) + || !coversSupport(world.getBlockState(pos), world, pos)) { return; } @@ -193,6 +196,12 @@ public static void repairSupport(World world, BlockPos pos) { } } + /** Steps and corners leave some ground exposed; only a full footprint hides all of its grass. */ + public static boolean coversSupport(IBlockState piece, IBlockAccess world, BlockPos pos) { + AxisAlignedBB bounds = piece.getBoundingBox(world, pos); + return bounds != null && bounds.minX == 0 && bounds.maxX == 1 && bounds.minZ == 0 && bounds.maxZ == 1; + } + /** Ignore derived visual properties while retaining the material and saved orientation. */ private static IBlockState canonical(IBlockState state) { return state.getBlock().getStateFromMeta(state.getBlock().getMetaFromState(state)); diff --git a/src/main/java/zone/moddev/mc/skysgrassslabs/block/GrassSpread.java b/src/main/java/zone/moddev/mc/skysgrassslabs/block/GrassSpread.java index 0ac50a2..ed9d1ca 100644 --- a/src/main/java/zone/moddev/mc/skysgrassslabs/block/GrassSpread.java +++ b/src/main/java/zone/moddev/mc/skysgrassslabs/block/GrassSpread.java @@ -116,7 +116,10 @@ private static boolean targetIsViable(World world, BlockPos target) { BlockPos above = target.up(); IBlockState cover = world.getBlockState(above); - if (cover.getBlock() == ModBlocks.TURF || GrassSlabsApi.isGrassForm(cover)) { + // Keep covered-piece rejection; only full ground can regrow beside a narrower grass piece. + if (cover.getBlock() == ModBlocks.TURF + || GrassSlabsApi.isGrassForm(cover) + && (!state.isFullCube() || GrassSlabsApi.coversSupport(cover, world, above))) { return false; } diff --git a/src/main/java/zone/moddev/mc/skysgrassslabs/client/GrassSlabsConfigScreen.java b/src/main/java/zone/moddev/mc/skysgrassslabs/client/GrassSlabsConfigScreen.java new file mode 100644 index 0000000..5b2feaa --- /dev/null +++ b/src/main/java/zone/moddev/mc/skysgrassslabs/client/GrassSlabsConfigScreen.java @@ -0,0 +1,23 @@ +package zone.moddev.mc.skysgrassslabs.client; + +import java.util.ArrayList; +import java.util.List; +import net.minecraft.client.gui.GuiScreen; +import net.minecraftforge.common.config.ConfigElement; +import net.minecraftforge.fml.client.config.GuiConfig; +import net.minecraftforge.fml.client.config.IConfigElement; +import zone.moddev.mc.skysgrassslabs.config.SkysGrassSlabsConfig; + +/** These generation and migration choices take effect after restarting Minecraft. */ +public final class GrassSlabsConfigScreen extends GuiConfig { + public GrassSlabsConfigScreen(GuiScreen parent) { + super(parent, elements(), "skysgrassslabs", false, true, "Sky's Grass Slabs"); + } + + private static List elements() { + List elements = new ArrayList<>(); + elements.addAll(new ConfigElement(SkysGrassSlabsConfig.configuration().getCategory("worldgen")).getChildElements()); + elements.addAll(new ConfigElement(SkysGrassSlabsConfig.configuration().getCategory("compat")).getChildElements()); + return elements; + } +} diff --git a/src/main/java/zone/moddev/mc/skysgrassslabs/client/GrassSlabsGuiFactory.java b/src/main/java/zone/moddev/mc/skysgrassslabs/client/GrassSlabsGuiFactory.java new file mode 100644 index 0000000..01d4de2 --- /dev/null +++ b/src/main/java/zone/moddev/mc/skysgrassslabs/client/GrassSlabsGuiFactory.java @@ -0,0 +1,42 @@ +package zone.moddev.mc.skysgrassslabs.client; + +import java.util.Collections; +import java.util.Set; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiScreen; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.fml.client.IModGuiFactory; +import net.minecraftforge.fml.client.event.ConfigChangedEvent; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import zone.moddev.mc.skysgrassslabs.config.SkysGrassSlabsConfig; + +/** Connects the Mods list's Config button to Forge's standard settings screen. */ +public final class GrassSlabsGuiFactory implements IModGuiFactory { + @Override + public void initialize(Minecraft minecraftInstance) { + MinecraftForge.EVENT_BUS.register(this); + } + + @Override + public Class mainConfigGuiClass() { + return GrassSlabsConfigScreen.class; + } + + @Override + public Set runtimeGuiCategories() { + return Collections.emptySet(); + } + + @Override + public RuntimeOptionGuiHandler getHandlerFor(RuntimeOptionCategoryElement element) { + return null; + } + + @SubscribeEvent + public void configChanged(ConfigChangedEvent.OnConfigChangedEvent event) { + if ("skysgrassslabs".equals(event.getModID())) { + // Save for the next restart; do not rerun generation or compatibility arbitration. + SkysGrassSlabsConfig.saveChanges(); + } + } +} diff --git a/src/main/java/zone/moddev/mc/skysgrassslabs/compat/BuildingBricksCompat.java b/src/main/java/zone/moddev/mc/skysgrassslabs/compat/BuildingBricksCompat.java index 8106766..16cc7ca 100644 --- a/src/main/java/zone/moddev/mc/skysgrassslabs/compat/BuildingBricksCompat.java +++ b/src/main/java/zone/moddev/mc/skysgrassslabs/compat/BuildingBricksCompat.java @@ -121,13 +121,16 @@ private static void registerLegacyAliases() { registerLegacyAlias(HISTORICAL_GRASS_SLAB_ID, true); legacyAliasesRegistered = true; resolveBlocks(); + SkysGrassSlabs.logger.info("Legacy slab recovery enabled for {}, {} and {}. " + + "Saved blocks and items will be converted as they load.", + GRASS_SLAB_ID, DIRT_SLAB_ID, HISTORICAL_GRASS_SLAB_ID); } private static void registerLegacyAlias(ResourceLocation registryId, boolean grass) { LegacySlabAliasBlock block = new LegacySlabAliasBlock(grass); - block.setRegistryName(registryId) - .setUnlocalizedName(SkysGrassSlabs.MOD_ID + ".legacy_" + registryId.getResourcePath()); + LegacyRegistryWarnings.setName(block, registryId); + block.setUnlocalizedName(SkysGrassSlabs.MOD_ID + ".legacy_" + registryId.getResourcePath()); ItemBlock item = new ItemBlock(block) { @Override @@ -137,7 +140,7 @@ public int getMetadata(int damage) { }; item.setHasSubtypes(true); - item.setRegistryName(registryId); + LegacyRegistryWarnings.setName(item, registryId); GameRegistry.register(block); GameRegistry.register(item); } diff --git a/src/main/java/zone/moddev/mc/skysgrassslabs/compat/LegacyRegistryWarnings.java b/src/main/java/zone/moddev/mc/skysgrassslabs/compat/LegacyRegistryWarnings.java new file mode 100644 index 0000000..b6e8a97 --- /dev/null +++ b/src/main/java/zone/moddev/mc/skysgrassslabs/compat/LegacyRegistryWarnings.java @@ -0,0 +1,153 @@ +package zone.moddev.mc.skysgrassslabs.compat; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import net.minecraft.util.ResourceLocation; +import net.minecraftforge.fml.common.registry.IForgeRegistryEntry; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.message.Message; +import org.apache.logging.log4j.core.Filter; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.filter.AbstractFilter; +import org.apache.logging.log4j.core.filter.CompositeFilter; + +/** Keeps intentional recovery names readable without hiding other registry problems. */ +final class LegacyRegistryWarnings { + private static final Set RECOVERY_IDS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( + "buildingbricks:grass_slab", + "buildingbricks:dirt_slab", + "buildingbrickscompatvanilla:grass_slab"))); + private static final ThreadLocal CURRENT_NAME = new ThreadLocal<>(); + private static final Filter FILTER = new RegistrationFilter(); + + private LegacyRegistryWarnings() { + } + + static void setName(IForgeRegistryEntry entry, ResourceLocation name) { + String identity = name.toString(); + if (!RECOVERY_IDS.contains(identity)) { + throw new IllegalArgumentException("Not a supported legacy recovery name: " + identity); + } + + installFilter(); + // The installed filter is neutral everywhere except this thread's naming call. + String previous = CURRENT_NAME.get(); + CURRENT_NAME.set(identity); + try { + entry.setRegistryName(name); + } finally { + if (previous == null) { + CURRENT_NAME.remove(); + } else { + CURRENT_NAME.set(previous); + } + } + } + + private static synchronized void installFilter() { + // Forge's logger can belong to a different context from the mod's logger. + org.apache.logging.log4j.Logger logger = net.minecraftforge.fml.common.FMLLog.getLogger(); + if (logger == null) { + logger = LogManager.getLogger("FML"); + } + if (!(logger instanceof org.apache.logging.log4j.core.Logger)) { + // An unfamiliar logging backend keeps Forge's original warning. + return; + } + + org.apache.logging.log4j.core.Logger forgeLogger = (org.apache.logging.log4j.core.Logger) logger; + Configuration configuration = forgeLogger.getContext().getConfiguration(); + // Configuration filters run before a message reaches an asynchronous queue. + if (!containsFilter(configuration.getFilter())) { + configuration.addFilter(FILTER); + } + } + + private static boolean containsFilter(Filter filter) { + if (filter == FILTER) { + return true; + } + if (filter instanceof CompositeFilter) { + for (Filter child : (CompositeFilter) filter) { + if (containsFilter(child)) { + return true; + } + } + } + return false; + } + + static boolean hideForCurrentRegistration(String logger, Level level, String message, + Throwable thrown, StackTraceElement[] stack) { + return matches(CURRENT_NAME.get(), logger, level, message, thrown, stack); + } + + static boolean matches(String identity, String logger, Level level, String message, + Throwable thrown, StackTraceElement[] stack) { + if (!RECOVERY_IDS.contains(identity) || !"FML".equals(logger) || !Level.WARN.equals(level) + || message == null || thrown != null || stack == null) { + return false; + } + + boolean namingEntry = false; + boolean checkingPrefix = false; + for (StackTraceElement frame : stack) { + namingEntry |= "net.minecraftforge.fml.common.registry.IForgeRegistryEntry$Impl".equals(frame.getClassName()) + && "setRegistryName".equals(frame.getMethodName()); + checkingPrefix |= "net.minecraftforge.fml.common.FMLLog".equals(frame.getClassName()) + && "bigWarning".equals(frame.getMethodName()); + } + if (!namingEntry || !checkingPrefix) { + return false; + } + + String[] parts = identity.split(":", 2); + String expected = "* Dangerous alternative prefix `" + parts[0] + "` for name `" + parts[1] + + "`, expected `skysgrassslabs` invalid registry invocation/invalid name?"; + if (expected.equals(message)) { + return true; + } + + // Forge 1.10 prints the banner and six call-site rows as separate events. + return "****************************************".equals(message) + || message.matches("\\* at [A-Za-z0-9_.$]+\\([^\\r\\n]*\\)(?:\\.\\.\\.)?"); + } + + private static final class RegistrationFilter extends AbstractFilter { + @Override + public Result filter(org.apache.logging.log4j.core.Logger logger, Level level, + Marker marker, Message message, Throwable thrown) { + if (CURRENT_NAME.get() == null || !Level.WARN.equals(level) + || !"FML".equals(logger.getName()) || thrown != null || message == null + || message.getThrowable() != null) { + return Result.NEUTRAL; + } + return hideForCurrentRegistration(logger.getName(), level, message.getFormattedMessage(), null, + Thread.currentThread().getStackTrace()) ? Result.DENY : Result.NEUTRAL; + } + + @Override + public Result filter(org.apache.logging.log4j.core.Logger logger, Level level, + Marker marker, String message, Object... parameters) { + if (CURRENT_NAME.get() == null || !Level.WARN.equals(level) || !"FML".equals(logger.getName())) { + return Result.NEUTRAL; + } + return filter(logger, level, marker, logger.getMessageFactory().newMessage(message, parameters), null); + } + + @Override + public Result filter(org.apache.logging.log4j.core.Logger logger, Level level, + Marker marker, Object message, Throwable thrown) { + if (CURRENT_NAME.get() == null || !Level.WARN.equals(level) + || !"FML".equals(logger.getName()) || thrown != null) { + return Result.NEUTRAL; + } + return filter(logger, level, marker, + logger.getMessageFactory().newMessage(message), thrown); + } + } +} diff --git a/src/main/java/zone/moddev/mc/skysgrassslabs/config/SkysGrassSlabsConfig.java b/src/main/java/zone/moddev/mc/skysgrassslabs/config/SkysGrassSlabsConfig.java index 216879d..676bb8d 100644 --- a/src/main/java/zone/moddev/mc/skysgrassslabs/config/SkysGrassSlabsConfig.java +++ b/src/main/java/zone/moddev/mc/skysgrassslabs/config/SkysGrassSlabsConfig.java @@ -10,6 +10,7 @@ public final class SkysGrassSlabsConfig { public static final String WORLDGEN_CATEGORY = "worldgen"; public static final String GENERATE_GRASS_SLABS = "generateGrassSlabs"; + private static Configuration configuration; private static boolean forceReplaceBuildingBricksSlabs; private static boolean generateGrassSlabs = true; private static boolean compatibilitySuppressed; @@ -17,7 +18,8 @@ public final class SkysGrassSlabsConfig { public static void load(File file) { compatibilitySuppressed = false; - Configuration config = new Configuration(file); + configuration = new Configuration(file); + Configuration config = configuration; config.load(); forceReplaceBuildingBricksSlabs = config.getBoolean(FORCE_REPLACE_BUILDINGBRICKS_SLABS, COMPAT_CATEGORY, false, @@ -26,6 +28,14 @@ public static void load(File file) { generateGrassSlabs = config.getBoolean(GENERATE_GRASS_SLABS, WORLDGEN_CATEGORY, true, "Place grass slabs on suitable one-block slopes in newly generated Overworld chunks."); + config.setCategoryRequiresMcRestart(WORLDGEN_CATEGORY, true); + config.setCategoryRequiresMcRestart(COMPAT_CATEGORY, true); + // Keep the comments created above: calling the generic getter again clears Forge's help text. + config.getCategory(WORLDGEN_CATEGORY).get(GENERATE_GRASS_SLABS) + .setLanguageKey("config.skysgrassslabs.generateGrassSlabs"); + config.getCategory(COMPAT_CATEGORY).get(FORCE_REPLACE_BUILDINGBRICKS_SLABS) + .setLanguageKey("config.skysgrassslabs.forceReplaceBuildingBricksSlabs"); + if (config.hasChanged()) { config.save(); } @@ -33,6 +43,21 @@ public static void load(File file) { GrassSlabsApi.configurationLoaded(); } + /** The config screen edits pending settings, not the active generation policy. */ + public static Configuration configuration() { + if (configuration == null) { + throw new IllegalStateException("Grass Slabs configuration has not loaded"); + } + + return configuration; + } + + public static void saveChanges() { + if (configuration().hasChanged()) { + configuration.save(); + } + } + public static boolean generateGrassSlabs() { return generateGrassSlabs; } diff --git a/src/main/java/zone/moddev/mc/skysgrassslabs/world/GrassSlabSmoothingHandler.java b/src/main/java/zone/moddev/mc/skysgrassslabs/world/GrassSlabSmoothingHandler.java index 0660a7f..9f28482 100644 --- a/src/main/java/zone/moddev/mc/skysgrassslabs/world/GrassSlabSmoothingHandler.java +++ b/src/main/java/zone/moddev/mc/skysgrassslabs/world/GrassSlabSmoothingHandler.java @@ -17,12 +17,7 @@ /** Plans a chunk's grass caps before writing any, without loading neighbouring chunks. */ public final class GrassSlabSmoothingHandler { public static final String FEATURE_ID = "skysgrassslabs:grass_slab_smoothing"; - private static final ThreadLocal DECISIONS = new ThreadLocal() { - @Override - protected boolean[] initialValue() { - return new boolean[256]; - } - }; + private static final ThreadLocal DECISIONS = ThreadLocal.withInitial(Decisions::new); @SubscribeEvent( priority = EventPriority.LOWEST) @@ -40,11 +35,10 @@ public void beforeDecoration(DecorateBiomeEvent.Pre event) { return; } - boolean[] decisions = DECISIONS.get(); + Decisions decisions = DECISIONS.get(); - for (int index = 0; index < decisions.length; ++index) { - decisions[index] = false; - } + java.util.Arrays.fill(decisions.targetHeights, -1); + java.util.Arrays.fill(decisions.originalTargets, null); int startX = chunkPos.getXStart(); int startZ = chunkPos.getZStart(); @@ -53,9 +47,9 @@ public void beforeDecoration(DecorateBiomeEvent.Pre event) { for (int localX = 0; localX < 16; ++localX) { int worldX = startX + localX; int worldZ = startZ + localZ; - int surfaceY = owner.getHeightValue(localX, localZ) - 1; + int surfaceY = SmoothingFoliage.surfaceHeight(owner, localX, localZ, startX, startZ); - if (surfaceY < 0) { + if (surfaceY < 0 || surfaceY >= 255) { continue; } @@ -64,7 +58,7 @@ public void beforeDecoration(DecorateBiomeEvent.Pre event) { IBlockState lower = owner.getBlockState(surface); IBlockState targetState = owner.getBlockState(target); boolean lowerGrass = lower.getBlock() == Blocks.GRASS; - boolean clear = targetState.getBlock().isAir(targetState, world, target); + boolean clear = SmoothingFoliage.canReplace(world, owner, target); boolean dry = targetState.getMaterial() != Material.WATER && targetState.getMaterial() != Material.LAVA; boolean supported = lower.isSideSolid(world, surface, EnumFacing.UP); boolean hasBlockEntity = owner.getTileEntity(target, Chunk.EnumCreateEntityType.CHECK) != null; @@ -74,22 +68,34 @@ public void beforeDecoration(DecorateBiomeEvent.Pre event) { } if (hasHigherGrassNeighbour(world, chunkPos, localX, localZ, surfaceY)) { - decisions[(localZ << 4) | localX] = true; + int index = (localZ << 4) | localX; + + decisions.targetHeights[index] = surfaceY + 1; + decisions.originalTargets[index] = targetState; } } } - for (int index = 0; index < decisions.length; ++index) { - if (!decisions[index]) { + for (int index = 0; index < decisions.targetHeights.length; ++index) { + if (decisions.targetHeights[index] < 0) { continue; } int localX = index & 15; int localZ = index >>> 4; - int surfaceY = owner.getHeightValue(localX, localZ) - 1; - BlockPos target = new BlockPos(startX + localX, surfaceY + 1, startZ + localZ); + // Retain the snapshot height: earlier writes must not move a later target. + BlockPos target = new BlockPos(startX + localX, decisions.targetHeights[index], startZ + localZ); + + if (!owner.getBlockState(target).equals(decisions.originalTargets[index]) + || !SmoothingFoliage.canReplace(world, owner, target)) { + continue; + } if (owner.setBlockState(target, ModBlocks.GRASS_SLAB.getDefaultState()) != null) { + if (decisions.originalTargets[index].getBlock() == Blocks.DOUBLE_PLANT) { + owner.setBlockState(target.up(), Blocks.AIR.getDefaultState()); + } + BlockPos support = target.down(); if (owner.getBlockState(support).getBlock() == Blocks.GRASS) { @@ -129,7 +135,7 @@ private static boolean isHigherGrass(World world, int chunkX, int chunkZ, int lo return false; } - int neighbourSurfaceY = chunk.getHeightValue(localX, localZ) - 1; + int neighbourSurfaceY = SmoothingFoliage.surfaceHeight(chunk, localX, localZ, chunkX << 4, chunkZ << 4); BlockPos neighbour = new BlockPos((chunkX << 4) + localX, neighbourSurfaceY, (chunkZ << 4) + localZ); boolean neighbourGrass = neighbourSurfaceY >= 0 && chunk.getBlockState(neighbour).getBlock() == Blocks.GRASS; @@ -137,6 +143,11 @@ private static boolean isHigherGrass(World world, int chunkX, int chunkZ, int lo false); } + private static final class Decisions { + final int[] targetHeights = new int[256]; + final IBlockState[] originalTargets = new IBlockState[256]; + } + private static Chunk loadedChunk(World world, int chunkX, int chunkZ) { if (!(world.getChunkProvider() instanceof ChunkProviderServer)) { return null; diff --git a/src/main/java/zone/moddev/mc/skysgrassslabs/world/SmoothingFoliage.java b/src/main/java/zone/moddev/mc/skysgrassslabs/world/SmoothingFoliage.java new file mode 100644 index 0000000..f798669 --- /dev/null +++ b/src/main/java/zone/moddev/mc/skysgrassslabs/world/SmoothingFoliage.java @@ -0,0 +1,60 @@ +package zone.moddev.mc.skysgrassslabs.world; + +import net.minecraft.block.BlockDoublePlant; +import net.minecraft.block.state.IBlockState; +import net.minecraft.init.Blocks; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import net.minecraft.world.chunk.Chunk; + +/** Only small natural plants may give way to a transition in a fresh chunk. */ +final class SmoothingFoliage { + static boolean isPlant(IBlockState state) { + return state.getBlock() == Blocks.TALLGRASS || state.getBlock() == Blocks.YELLOW_FLOWER + || state.getBlock() == Blocks.RED_FLOWER || state.getBlock() == Blocks.DEADBUSH + || state.getBlock() == Blocks.DOUBLE_PLANT; + } + + static boolean canReplace(World world, Chunk chunk, BlockPos target) { + IBlockState state = chunk.getBlockState(target); + + if (state.getBlock().hasTileEntity(state) + || chunk.getTileEntity(target, Chunk.EnumCreateEntityType.CHECK) != null) { + return false; + } + if (state.getBlock().isAir(state, world, target)) { + return true; + } + if (!isPlant(state)) { + return false; + } + if (state.getBlock() != Blocks.DOUBLE_PLANT) { + return true; + } + if (target.getY() >= 255 + || state.getValue(BlockDoublePlant.HALF) != BlockDoublePlant.EnumBlockHalf.LOWER) { + return false; + } + + IBlockState upper = chunk.getBlockState(target.up()); + + return upper.getBlock() == Blocks.DOUBLE_PLANT + && upper.getValue(BlockDoublePlant.HALF) == BlockDoublePlant.EnumBlockHalf.UPPER + && chunk.getTileEntity(target.up(), Chunk.EnumCreateEntityType.CHECK) == null; + } + + static int surfaceHeight(Chunk chunk, int localX, int localZ, int startX, int startZ) { + int surfaceY = chunk.getHeightValue(localX, localZ) - 1; + + // A decorator may already have reached here from a neighbouring chunk. + while (surfaceY >= 0 && isPlant(chunk.getBlockState( + new BlockPos(startX + localX, surfaceY, startZ + localZ)))) { + surfaceY--; + } + + return surfaceY; + } + + private SmoothingFoliage() { + } +} diff --git a/src/main/resources/assets/skysgrassslabs/lang/de_AT.lang b/src/main/resources/assets/skysgrassslabs/lang/de_AT.lang index 830b439..e45c6b3 100644 --- a/src/main/resources/assets/skysgrassslabs/lang/de_AT.lang +++ b/src/main/resources/assets/skysgrassslabs/lang/de_AT.lang @@ -2,3 +2,7 @@ tile.skysgrassslabs.dirt_slab.name=Eanstufn tile.skysgrassslabs.grass_slab.name=Grosstufn tile.skysgrassslabs.path_slab.name=Steigstufn tile.skysgrassslabs.turf.name=Grassodn +config.skysgrassslabs.generateGrassSlabs=Grasstufen erzeugen +config.skysgrassslabs.forceReplaceBuildingBricksSlabs=Alte Stufen ersetzen +config.skysgrassslabs.generateGrassSlabs.tooltip=Fügt Grasplatten an neuen Hängen der Oberwelt hinzu. Mit Terrain Smoother steuert dies nur Gras. Bestehende Chunks bleiben unverändert. Minecraft neu starten. +config.skysgrassslabs.forceReplaceBuildingBricksSlabs.tooltip=Ersetzt unterstützte Gras- und Erdplatten von BuildingBricks. Zuerst die Welt sichern: Ersetzungen lassen sich nicht rückgängig machen. Minecraft neu starten. diff --git a/src/main/resources/assets/skysgrassslabs/lang/de_AU.lang b/src/main/resources/assets/skysgrassslabs/lang/de_AU.lang index df80ecc..49d613d 100644 --- a/src/main/resources/assets/skysgrassslabs/lang/de_AU.lang +++ b/src/main/resources/assets/skysgrassslabs/lang/de_AU.lang @@ -2,3 +2,7 @@ tile.skysgrassslabs.dirt_slab.name=Erdstufe tile.skysgrassslabs.grass_slab.name=Grasblockstufe tile.skysgrassslabs.path_slab.name=Trampelpfadstufe tile.skysgrassslabs.turf.name=Grassode +config.skysgrassslabs.generateGrassSlabs=Grasstufen erzeugen +config.skysgrassslabs.forceReplaceBuildingBricksSlabs=Alte Stufen ersetzen +config.skysgrassslabs.generateGrassSlabs.tooltip=Fügt Grasplatten an neuen Hängen der Oberwelt hinzu. Mit Terrain Smoother steuert dies nur Gras. Bestehende Chunks bleiben unverändert. Minecraft neu starten. +config.skysgrassslabs.forceReplaceBuildingBricksSlabs.tooltip=Ersetzt unterstützte Gras- und Erdplatten von BuildingBricks. Zuerst die Welt sichern: Ersetzungen lassen sich nicht rückgängig machen. Minecraft neu starten. diff --git a/src/main/resources/assets/skysgrassslabs/lang/de_DE.lang b/src/main/resources/assets/skysgrassslabs/lang/de_DE.lang index df80ecc..49d613d 100644 --- a/src/main/resources/assets/skysgrassslabs/lang/de_DE.lang +++ b/src/main/resources/assets/skysgrassslabs/lang/de_DE.lang @@ -2,3 +2,7 @@ tile.skysgrassslabs.dirt_slab.name=Erdstufe tile.skysgrassslabs.grass_slab.name=Grasblockstufe tile.skysgrassslabs.path_slab.name=Trampelpfadstufe tile.skysgrassslabs.turf.name=Grassode +config.skysgrassslabs.generateGrassSlabs=Grasstufen erzeugen +config.skysgrassslabs.forceReplaceBuildingBricksSlabs=Alte Stufen ersetzen +config.skysgrassslabs.generateGrassSlabs.tooltip=Fügt Grasplatten an neuen Hängen der Oberwelt hinzu. Mit Terrain Smoother steuert dies nur Gras. Bestehende Chunks bleiben unverändert. Minecraft neu starten. +config.skysgrassslabs.forceReplaceBuildingBricksSlabs.tooltip=Ersetzt unterstützte Gras- und Erdplatten von BuildingBricks. Zuerst die Welt sichern: Ersetzungen lassen sich nicht rückgängig machen. Minecraft neu starten. diff --git a/src/main/resources/assets/skysgrassslabs/lang/en_CA.lang b/src/main/resources/assets/skysgrassslabs/lang/en_CA.lang index e83efd3..b72fa7b 100644 --- a/src/main/resources/assets/skysgrassslabs/lang/en_CA.lang +++ b/src/main/resources/assets/skysgrassslabs/lang/en_CA.lang @@ -2,3 +2,7 @@ tile.skysgrassslabs.dirt_slab.name=Dirt Slab tile.skysgrassslabs.grass_slab.name=Grass Slab tile.skysgrassslabs.path_slab.name=Grass Path Slab tile.skysgrassslabs.turf.name=Turf +config.skysgrassslabs.generateGrassSlabs=Generate grass slabs +config.skysgrassslabs.forceReplaceBuildingBricksSlabs=Replace legacy slabs +config.skysgrassslabs.generateGrassSlabs.tooltip=Add grass slabs to new Overworld slopes. With Terrain Smoother, this controls grass only. Existing chunks stay unchanged. Restart Minecraft. +config.skysgrassslabs.forceReplaceBuildingBricksSlabs.tooltip=Replace supported BuildingBricks grass and dirt slabs. Back up the world first: replacements cannot be undone. Restart Minecraft. diff --git a/src/main/resources/assets/skysgrassslabs/lang/en_EN.lang b/src/main/resources/assets/skysgrassslabs/lang/en_EN.lang index e83efd3..b72fa7b 100644 --- a/src/main/resources/assets/skysgrassslabs/lang/en_EN.lang +++ b/src/main/resources/assets/skysgrassslabs/lang/en_EN.lang @@ -2,3 +2,7 @@ tile.skysgrassslabs.dirt_slab.name=Dirt Slab tile.skysgrassslabs.grass_slab.name=Grass Slab tile.skysgrassslabs.path_slab.name=Grass Path Slab tile.skysgrassslabs.turf.name=Turf +config.skysgrassslabs.generateGrassSlabs=Generate grass slabs +config.skysgrassslabs.forceReplaceBuildingBricksSlabs=Replace legacy slabs +config.skysgrassslabs.generateGrassSlabs.tooltip=Add grass slabs to new Overworld slopes. With Terrain Smoother, this controls grass only. Existing chunks stay unchanged. Restart Minecraft. +config.skysgrassslabs.forceReplaceBuildingBricksSlabs.tooltip=Replace supported BuildingBricks grass and dirt slabs. Back up the world first: replacements cannot be undone. Restart Minecraft. diff --git a/src/main/resources/assets/skysgrassslabs/lang/en_GB.lang b/src/main/resources/assets/skysgrassslabs/lang/en_GB.lang index e83efd3..b72fa7b 100644 --- a/src/main/resources/assets/skysgrassslabs/lang/en_GB.lang +++ b/src/main/resources/assets/skysgrassslabs/lang/en_GB.lang @@ -2,3 +2,7 @@ tile.skysgrassslabs.dirt_slab.name=Dirt Slab tile.skysgrassslabs.grass_slab.name=Grass Slab tile.skysgrassslabs.path_slab.name=Grass Path Slab tile.skysgrassslabs.turf.name=Turf +config.skysgrassslabs.generateGrassSlabs=Generate grass slabs +config.skysgrassslabs.forceReplaceBuildingBricksSlabs=Replace legacy slabs +config.skysgrassslabs.generateGrassSlabs.tooltip=Add grass slabs to new Overworld slopes. With Terrain Smoother, this controls grass only. Existing chunks stay unchanged. Restart Minecraft. +config.skysgrassslabs.forceReplaceBuildingBricksSlabs.tooltip=Replace supported BuildingBricks grass and dirt slabs. Back up the world first: replacements cannot be undone. Restart Minecraft. diff --git a/src/main/resources/assets/skysgrassslabs/lang/en_PT.lang b/src/main/resources/assets/skysgrassslabs/lang/en_PT.lang index e83efd3..b72fa7b 100644 --- a/src/main/resources/assets/skysgrassslabs/lang/en_PT.lang +++ b/src/main/resources/assets/skysgrassslabs/lang/en_PT.lang @@ -2,3 +2,7 @@ tile.skysgrassslabs.dirt_slab.name=Dirt Slab tile.skysgrassslabs.grass_slab.name=Grass Slab tile.skysgrassslabs.path_slab.name=Grass Path Slab tile.skysgrassslabs.turf.name=Turf +config.skysgrassslabs.generateGrassSlabs=Generate grass slabs +config.skysgrassslabs.forceReplaceBuildingBricksSlabs=Replace legacy slabs +config.skysgrassslabs.generateGrassSlabs.tooltip=Add grass slabs to new Overworld slopes. With Terrain Smoother, this controls grass only. Existing chunks stay unchanged. Restart Minecraft. +config.skysgrassslabs.forceReplaceBuildingBricksSlabs.tooltip=Replace supported BuildingBricks grass and dirt slabs. Back up the world first: replacements cannot be undone. Restart Minecraft. diff --git a/src/main/resources/assets/skysgrassslabs/lang/en_US.lang b/src/main/resources/assets/skysgrassslabs/lang/en_US.lang index e83efd3..b72fa7b 100644 --- a/src/main/resources/assets/skysgrassslabs/lang/en_US.lang +++ b/src/main/resources/assets/skysgrassslabs/lang/en_US.lang @@ -2,3 +2,7 @@ tile.skysgrassslabs.dirt_slab.name=Dirt Slab tile.skysgrassslabs.grass_slab.name=Grass Slab tile.skysgrassslabs.path_slab.name=Grass Path Slab tile.skysgrassslabs.turf.name=Turf +config.skysgrassslabs.generateGrassSlabs=Generate grass slabs +config.skysgrassslabs.forceReplaceBuildingBricksSlabs=Replace legacy slabs +config.skysgrassslabs.generateGrassSlabs.tooltip=Add grass slabs to new Overworld slopes. With Terrain Smoother, this controls grass only. Existing chunks stay unchanged. Restart Minecraft. +config.skysgrassslabs.forceReplaceBuildingBricksSlabs.tooltip=Replace supported BuildingBricks grass and dirt slabs. Back up the world first: replacements cannot be undone. Restart Minecraft. diff --git a/src/main/resources/assets/skysgrassslabs/lang/es_ES.lang b/src/main/resources/assets/skysgrassslabs/lang/es_ES.lang index 5071e06..b4f6e8b 100644 --- a/src/main/resources/assets/skysgrassslabs/lang/es_ES.lang +++ b/src/main/resources/assets/skysgrassslabs/lang/es_ES.lang @@ -2,3 +2,7 @@ tile.skysgrassslabs.dirt_slab.name=Losa de tierra tile.skysgrassslabs.grass_slab.name=Losa de césped tile.skysgrassslabs.path_slab.name=Losa de camino de hierba tile.skysgrassslabs.turf.name=Tepe de césped +config.skysgrassslabs.generateGrassSlabs=Generar losas de césped +config.skysgrassslabs.forceReplaceBuildingBricksSlabs=Reemplazar losas antiguas +config.skysgrassslabs.generateGrassSlabs.tooltip=Añade losas de hierba a pendientes nuevas del mundo normal. Con Terrain Smoother, solo controla la hierba. Los chunks existentes no cambian. Reinicia Minecraft. +config.skysgrassslabs.forceReplaceBuildingBricksSlabs.tooltip=Sustituye las losas de hierba y tierra compatibles de BuildingBricks. Haz una copia del mundo primero: los cambios no se pueden deshacer. Reinicia Minecraft. diff --git a/src/main/resources/assets/skysgrassslabs/lang/es_MX.lang b/src/main/resources/assets/skysgrassslabs/lang/es_MX.lang index c371835..034ad05 100644 --- a/src/main/resources/assets/skysgrassslabs/lang/es_MX.lang +++ b/src/main/resources/assets/skysgrassslabs/lang/es_MX.lang @@ -2,3 +2,7 @@ tile.skysgrassslabs.dirt_slab.name=Losa de tierra tile.skysgrassslabs.grass_slab.name=Losa de pasto tile.skysgrassslabs.path_slab.name=Losa de sendero de pasto tile.skysgrassslabs.turf.name=Tapete de pasto +config.skysgrassslabs.generateGrassSlabs=Generar losas de césped +config.skysgrassslabs.forceReplaceBuildingBricksSlabs=Reemplazar losas antiguas +config.skysgrassslabs.generateGrassSlabs.tooltip=Añade losas de hierba a pendientes nuevas del mundo normal. Con Terrain Smoother, solo controla la hierba. Los chunks existentes no cambian. Reinicia Minecraft. +config.skysgrassslabs.forceReplaceBuildingBricksSlabs.tooltip=Sustituye las losas de hierba y tierra compatibles de BuildingBricks. Haz una copia del mundo primero: los cambios no se pueden deshacer. Reinicia Minecraft. diff --git a/src/main/resources/assets/skysgrassslabs/lang/fr_CA.lang b/src/main/resources/assets/skysgrassslabs/lang/fr_CA.lang index bef1dba..11a5a75 100644 --- a/src/main/resources/assets/skysgrassslabs/lang/fr_CA.lang +++ b/src/main/resources/assets/skysgrassslabs/lang/fr_CA.lang @@ -2,3 +2,7 @@ tile.skysgrassslabs.dirt_slab.name=Dalle de terre tile.skysgrassslabs.grass_slab.name=Dalle de gazon tile.skysgrassslabs.path_slab.name=Dalle de sentier de gazon tile.skysgrassslabs.turf.name=Plaque de gazon +config.skysgrassslabs.generateGrassSlabs=Générer des dalles d'herbe +config.skysgrassslabs.forceReplaceBuildingBricksSlabs=Remplacer les anciennes dalles +config.skysgrassslabs.generateGrassSlabs.tooltip=Ajoute des dalles d'herbe aux nouvelles pentes du monde normal. Avec Terrain Smoother, cette option ne contrôle que l'herbe. Les chunks existants ne changent pas. Redémarrez Minecraft. +config.skysgrassslabs.forceReplaceBuildingBricksSlabs.tooltip=Remplace les dalles d'herbe et de terre compatibles de BuildingBricks. Sauvegardez le monde d'abord : les remplacements sont irréversibles. Redémarrez Minecraft. diff --git a/src/main/resources/assets/skysgrassslabs/lang/fr_FR.lang b/src/main/resources/assets/skysgrassslabs/lang/fr_FR.lang index 668594a..3a17769 100644 --- a/src/main/resources/assets/skysgrassslabs/lang/fr_FR.lang +++ b/src/main/resources/assets/skysgrassslabs/lang/fr_FR.lang @@ -2,3 +2,7 @@ tile.skysgrassslabs.dirt_slab.name=Dalle de terre tile.skysgrassslabs.grass_slab.name=Dalle d'herbe tile.skysgrassslabs.path_slab.name=Dalle de chemin d'herbe tile.skysgrassslabs.turf.name=Plaque de gazon +config.skysgrassslabs.generateGrassSlabs=Générer des dalles d'herbe +config.skysgrassslabs.forceReplaceBuildingBricksSlabs=Remplacer les anciennes dalles +config.skysgrassslabs.generateGrassSlabs.tooltip=Ajoute des dalles d'herbe aux nouvelles pentes du monde normal. Avec Terrain Smoother, cette option ne contrôle que l'herbe. Les chunks existants ne changent pas. Redémarrez Minecraft. +config.skysgrassslabs.forceReplaceBuildingBricksSlabs.tooltip=Remplace les dalles d'herbe et de terre compatibles de BuildingBricks. Sauvegardez le monde d'abord : les remplacements sont irréversibles. Redémarrez Minecraft. diff --git a/src/main/resources/assets/skysgrassslabs/lang/ja_JP.lang b/src/main/resources/assets/skysgrassslabs/lang/ja_JP.lang index 3cbc383..7011e6a 100644 --- a/src/main/resources/assets/skysgrassslabs/lang/ja_JP.lang +++ b/src/main/resources/assets/skysgrassslabs/lang/ja_JP.lang @@ -2,3 +2,7 @@ tile.skysgrassslabs.dirt_slab.name=土のハーフブロック tile.skysgrassslabs.grass_slab.name=草ブロックのハーフブロック tile.skysgrassslabs.path_slab.name=草の道のハーフブロック tile.skysgrassslabs.turf.name=芝生 +config.skysgrassslabs.generateGrassSlabs=草のハーフブロックを生成 +config.skysgrassslabs.forceReplaceBuildingBricksSlabs=旧MODのハーフブロックを置換 +config.skysgrassslabs.generateGrassSlabs.tooltip=新しく生成されるオーバーワールドの斜面に草のハーフブロックを置きます。Terrain Smoother導入時は草だけを設定します。既存のチャンクは変わりません。Minecraftの再起動が必要です。 +config.skysgrassslabs.forceReplaceBuildingBricksSlabs.tooltip=対応するBuildingBricksの草と土のハーフブロックを置き換えます。元に戻せないため、先にワールドをバックアップしてください。Minecraftの再起動が必要です。 diff --git a/src/main/resources/assets/skysgrassslabs/lang/ko_KR.lang b/src/main/resources/assets/skysgrassslabs/lang/ko_KR.lang index 136d120..ab39adc 100644 --- a/src/main/resources/assets/skysgrassslabs/lang/ko_KR.lang +++ b/src/main/resources/assets/skysgrassslabs/lang/ko_KR.lang @@ -2,3 +2,7 @@ tile.skysgrassslabs.dirt_slab.name=흙 반 블록 tile.skysgrassslabs.grass_slab.name=잔디 블록 반 블록 tile.skysgrassslabs.path_slab.name=잔디 길 반 블록 tile.skysgrassslabs.turf.name=잔디 +config.skysgrassslabs.generateGrassSlabs=잔디 반 블록 생성 +config.skysgrassslabs.forceReplaceBuildingBricksSlabs=이전 모드 반 블록 교체 +config.skysgrassslabs.generateGrassSlabs.tooltip=새로 생성되는 오버월드 경사면에 잔디 반 블록을 놓습니다. Terrain Smoother가 있으면 잔디에만 적용됩니다. 기존 청크는 바뀌지 않습니다. Minecraft를 다시 시작해야 합니다. +config.skysgrassslabs.forceReplaceBuildingBricksSlabs.tooltip=지원되는 BuildingBricks 잔디와 흙 반 블록을 교체합니다. 되돌릴 수 없으므로 먼저 월드를 백업하세요. Minecraft를 다시 시작해야 합니다. diff --git a/src/main/resources/assets/skysgrassslabs/lang/pt_BR.lang b/src/main/resources/assets/skysgrassslabs/lang/pt_BR.lang index 98e2bf8..645a2fc 100644 --- a/src/main/resources/assets/skysgrassslabs/lang/pt_BR.lang +++ b/src/main/resources/assets/skysgrassslabs/lang/pt_BR.lang @@ -2,3 +2,7 @@ tile.skysgrassslabs.dirt_slab.name=Laje de Terra tile.skysgrassslabs.grass_slab.name=Laje de Bloco de Grama tile.skysgrassslabs.path_slab.name=Laje de Caminho de Grama tile.skysgrassslabs.turf.name=Placa de Grama +config.skysgrassslabs.generateGrassSlabs=Gerar lajes de relva +config.skysgrassslabs.forceReplaceBuildingBricksSlabs=Substituir lajes antigas +config.skysgrassslabs.generateGrassSlabs.tooltip=Adiciona lajes de grama às novas encostas do mundo normal. Com Terrain Smoother, controla apenas a grama. Os chunks existentes não mudam. Reinicie o Minecraft. +config.skysgrassslabs.forceReplaceBuildingBricksSlabs.tooltip=Substitui as lajes compatíveis de grama e terra do BuildingBricks. Faça um backup do mundo antes: as substituições não podem ser desfeitas. Reinicie o Minecraft. diff --git a/src/main/resources/assets/skysgrassslabs/lang/pt_PT.lang b/src/main/resources/assets/skysgrassslabs/lang/pt_PT.lang index 3729a8b..c4ea2a4 100644 --- a/src/main/resources/assets/skysgrassslabs/lang/pt_PT.lang +++ b/src/main/resources/assets/skysgrassslabs/lang/pt_PT.lang @@ -2,3 +2,7 @@ tile.skysgrassslabs.dirt_slab.name=Degrau de Terra tile.skysgrassslabs.grass_slab.name=Degrau de Bloco de Relva tile.skysgrassslabs.path_slab.name=Degrau de Caminho de Relva tile.skysgrassslabs.turf.name=Placa de Relva +config.skysgrassslabs.generateGrassSlabs=Gerar lajes de relva +config.skysgrassslabs.forceReplaceBuildingBricksSlabs=Substituir lajes antigas +config.skysgrassslabs.generateGrassSlabs.tooltip=Acrescenta degraus de relva às novas encostas do mundo normal. Com Terrain Smoother, controla apenas a relva. Os chunks existentes não mudam. Reinicie o Minecraft. +config.skysgrassslabs.forceReplaceBuildingBricksSlabs.tooltip=Substitui os degraus compatíveis de relva e terra do BuildingBricks. Faça primeiro uma cópia do mundo: as substituições não podem ser desfeitas. Reinicie o Minecraft. diff --git a/src/main/resources/assets/skysgrassslabs/lang/ru_RU.lang b/src/main/resources/assets/skysgrassslabs/lang/ru_RU.lang index ec5357d..ef57b7b 100644 --- a/src/main/resources/assets/skysgrassslabs/lang/ru_RU.lang +++ b/src/main/resources/assets/skysgrassslabs/lang/ru_RU.lang @@ -2,3 +2,7 @@ tile.skysgrassslabs.dirt_slab.name=Земляная плита tile.skysgrassslabs.grass_slab.name=Дёрновая плита tile.skysgrassslabs.path_slab.name=Плита тропы tile.skysgrassslabs.turf.name=Дёрн +config.skysgrassslabs.generateGrassSlabs=Генерировать дёрновые плиты +config.skysgrassslabs.forceReplaceBuildingBricksSlabs=Заменять старые плиты +config.skysgrassslabs.generateGrassSlabs.tooltip=Добавляет дёрновые плиты на новые склоны обычного мира. С Terrain Smoother настройка управляет только травой. Существующие чанки не меняются. Перезапустите Minecraft. +config.skysgrassslabs.forceReplaceBuildingBricksSlabs.tooltip=Заменяет поддерживаемые дёрновые и земляные плиты BuildingBricks. Сначала сохраните копию мира: замену нельзя отменить. Перезапустите Minecraft. diff --git a/src/main/resources/assets/skysgrassslabs/lang/zh_CN.lang b/src/main/resources/assets/skysgrassslabs/lang/zh_CN.lang index 5d604b1..cda44b6 100644 --- a/src/main/resources/assets/skysgrassslabs/lang/zh_CN.lang +++ b/src/main/resources/assets/skysgrassslabs/lang/zh_CN.lang @@ -2,3 +2,7 @@ tile.skysgrassslabs.dirt_slab.name=泥土台阶 tile.skysgrassslabs.grass_slab.name=草方块台阶 tile.skysgrassslabs.path_slab.name=草径台阶 tile.skysgrassslabs.turf.name=草皮 +config.skysgrassslabs.generateGrassSlabs=生成草方块台阶 +config.skysgrassslabs.forceReplaceBuildingBricksSlabs=替换旧版台阶 +config.skysgrassslabs.generateGrassSlabs.tooltip=在新生成的主世界斜坡上放置草方块台阶。安装Terrain Smoother后,此选项只控制草地。已有区块不会改变。需要重启Minecraft。 +config.skysgrassslabs.forceReplaceBuildingBricksSlabs.tooltip=替换受支持的BuildingBricks草方块和泥土台阶。替换无法撤销,请先备份世界。需要重启Minecraft。 diff --git a/src/test/java/zone/moddev/mc/skysgrassslabs/BuildArtifactContractTest.java b/src/test/java/zone/moddev/mc/skysgrassslabs/BuildArtifactContractTest.java new file mode 100644 index 0000000..43718cb --- /dev/null +++ b/src/test/java/zone/moddev/mc/skysgrassslabs/BuildArtifactContractTest.java @@ -0,0 +1,17 @@ +package zone.moddev.mc.skysgrassslabs; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class BuildArtifactContractTest { + @Test + void checksumTaskDeclaresItsArtifactInputs() throws Exception { + String build = new String(Files.readAllBytes(Paths.get("build.gradle")), StandardCharsets.UTF_8); + String checksumTask = build.substring(build.indexOf("tasks.register('writeReleaseChecksums')")); + assertTrue(checksumTask.contains("inputs.files(releaseFiles.map"), + "Incremental builds must regenerate checksums when a release jar changes"); + } +} diff --git a/src/test/java/zone/moddev/mc/skysgrassslabs/LocalizationContractTest.java b/src/test/java/zone/moddev/mc/skysgrassslabs/LocalizationContractTest.java index 5202e8f..a30e185 100644 --- a/src/test/java/zone/moddev/mc/skysgrassslabs/LocalizationContractTest.java +++ b/src/test/java/zone/moddev/mc/skysgrassslabs/LocalizationContractTest.java @@ -26,7 +26,10 @@ class LocalizationContractTest { private static final File LANG_DIR = new File("src/main/resources/assets/skysgrassslabs/lang"); private static final List KEYS = Arrays.asList("tile.skysgrassslabs.dirt_slab.name", "tile.skysgrassslabs.grass_slab.name", "tile.skysgrassslabs.path_slab.name", - "tile.skysgrassslabs.turf.name"); + "tile.skysgrassslabs.turf.name", "config.skysgrassslabs.generateGrassSlabs", + "config.skysgrassslabs.forceReplaceBuildingBricksSlabs", + "config.skysgrassslabs.generateGrassSlabs.tooltip", + "config.skysgrassslabs.forceReplaceBuildingBricksSlabs.tooltip"); private static final List LOCALES = Arrays.asList("de_AT", "de_AU", "de_DE", "en_CA", "en_EN", "en_GB", "en_PT", "en_US", "es_ES", "es_MX", "fr_CA", "fr_FR", "ja_JP", "ko_KR", "pt_BR", "pt_PT", "ru_RU", "zh_CN"); @@ -48,7 +51,7 @@ void allEighteenLocalesHaveExactOrderedKeyParity() throws Exception { Map translations = read(locale); assertEquals(KEYS, new ArrayList(translations.keySet()), locale); - assertEquals(4, translations.size(), locale); + assertEquals(8, translations.size(), locale); } } @@ -103,7 +106,7 @@ void translationsAndRegionalDifferencesAreLocked() throws Exception { } private static void assertValues(String locale, String... values) throws Exception { - assertEquals(Arrays.asList(values), new ArrayList(read(locale).values()), locale); + assertEquals(Arrays.asList(values), new ArrayList(read(locale).values()).subList(0, 4), locale); } private static Map read(String locale) throws Exception { diff --git a/src/test/java/zone/moddev/mc/skysgrassslabs/ProjectContractTest.java b/src/test/java/zone/moddev/mc/skysgrassslabs/ProjectContractTest.java index 38cd9f2..40d7fde 100644 --- a/src/test/java/zone/moddev/mc/skysgrassslabs/ProjectContractTest.java +++ b/src/test/java/zone/moddev/mc/skysgrassslabs/ProjectContractTest.java @@ -41,7 +41,7 @@ void releaseIdentityAndToolchainArePinned() throws Exception { assertTrue(properties.contains("minecraft_version=1.10.2")); assertTrue(properties.contains("forge_version=12.18.3.2511")); assertTrue(properties.contains("mapping_version=29-1.10.2")); - assertTrue(properties.contains("mod_version=1.1.0.110021")); + assertTrue(properties.contains("mod_version=1.1.1.110021")); assertTrue(properties.contains("curseforge_project_id=1677588")); assertTrue(properties.contains("java_toolchain_version=8.0.502+7")); } @@ -96,7 +96,7 @@ void releaseTagWorkflowPointsAtTheManualDispatcherInputs() throws Exception { assertFalse(workflow.contains("release_ref:")); assertFalse(workflow.contains("curseforge_channel:")); assertFalse(workflow.contains("confirm_version:")); - assertTrue(new File("docs/RELEASE-1.1.0.110021.md").isFile()); + assertTrue(new File("docs/RELEASE-1.1.1.110021.md").isFile()); } @Test diff --git a/src/test/java/zone/moddev/mc/skysgrassslabs/block/GrassFormsContractTest.java b/src/test/java/zone/moddev/mc/skysgrassslabs/block/GrassFormsContractTest.java index 3fc345b..e52c608 100644 --- a/src/test/java/zone/moddev/mc/skysgrassslabs/block/GrassFormsContractTest.java +++ b/src/test/java/zone/moddev/mc/skysgrassslabs/block/GrassFormsContractTest.java @@ -1,8 +1,13 @@ package zone.moddev.mc.skysgrassslabs.block; import net.minecraft.block.BlockGrass; +import net.minecraft.block.Block; import net.minecraft.block.BlockSlab; +import net.minecraft.block.material.Material; import net.minecraft.block.state.IBlockState; +import net.minecraft.util.math.AxisAlignedBB; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.IBlockAccess; import org.junit.jupiter.api.*; import zone.moddev.mc.skysgrassslabs.MinecraftTestBootstrap; import zone.moddev.mc.skysgrassslabs.api.GrassSlabsApi; @@ -44,4 +49,30 @@ void duplicateOrInvalidMappingsAreRejected() { assertThrows(IllegalArgumentException.class, () -> GrassSlabsApi .registerGrassForm(ModBlocks.DIRT_SLAB.getDefaultState(), ModBlocks.DIRT_SLAB.getDefaultState())); } + + @Test + void onlyACompleteFootprintCoversTheGrassBelow() { + for (int metadata = 0; metadata < 2; metadata++) { + assertTrue(GrassSlabsApi.coversSupport(ModBlocks.GRASS_SLAB.getStateFromMeta(metadata), + null, BlockPos.ORIGIN)); + } + assertTrue(GrassSlabsApi.coversSupport(ModBlocks.TURF.getDefaultState(), + null, BlockPos.ORIGIN)); + + for (AxisAlignedBB bounds : new AxisAlignedBB[] { + new AxisAlignedBB(0, 0, 0, 1, .5, .5), + new AxisAlignedBB(.5, 0, 0, 1, .5, 1), + new AxisAlignedBB(0, 0, .5, 1, .5, 1), + new AxisAlignedBB(0, 0, 0, .5, .5, 1), + new AxisAlignedBB(0, 0, 0, .5, .5, .5) }) { + Block partial = new Block(Material.GRASS) { + @Override + public AxisAlignedBB getBoundingBox(IBlockState state, IBlockAccess world, BlockPos pos) { + return bounds; + } + }; + assertFalse(GrassSlabsApi.coversSupport(partial.getDefaultState(), + null, BlockPos.ORIGIN)); + } + } } diff --git a/src/test/java/zone/moddev/mc/skysgrassslabs/compat/LegacyRegistryWarningsTest.java b/src/test/java/zone/moddev/mc/skysgrassslabs/compat/LegacyRegistryWarningsTest.java new file mode 100644 index 0000000..11f3f22 --- /dev/null +++ b/src/test/java/zone/moddev/mc/skysgrassslabs/compat/LegacyRegistryWarningsTest.java @@ -0,0 +1,189 @@ +package zone.moddev.mc.skysgrassslabs.compat; + +import java.util.concurrent.atomic.AtomicBoolean; +import net.minecraft.util.ResourceLocation; +import net.minecraftforge.fml.common.registry.IForgeRegistryEntry; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class LegacyRegistryWarningsTest { + private static final String GRASS = "buildingbricks:grass_slab"; + private static final StackTraceElement[] FORGE_ORIGIN = { + new StackTraceElement("net.minecraftforge.fml.common.FMLLog", "bigWarning", "Forge.java", 82), + new StackTraceElement("net.minecraftforge.fml.common.registry.IForgeRegistryEntry$Impl", "setRegistryName", "IForgeRegistryEntry.java", 90) + }; + + @Test + void realLoggingKeepsUnrelatedWarningsErrorsAndOutOfScopeMessages() { + org.apache.logging.log4j.Logger apiLogger = net.minecraftforge.fml.common.FMLLog.getLogger(); + if (apiLogger == null) { + apiLogger = LogManager.getLogger("FML"); + } + org.apache.logging.log4j.core.Logger logger = (org.apache.logging.log4j.core.Logger) apiLogger; + java.util.List captured = new java.util.ArrayList<>(); + AbstractAppender appender = new AbstractAppender("LegacyRegistryWarningTest", null, null) { + @Override + public void append(LogEvent event) { + captured.add(event.getMessage().getFormattedMessage()); + } + }; + Level previousLevel = logger.getLevel(); + appender.start(); + logger.addAppender(appender); + logger.setLevel(Level.ALL); + try { + Entry entry = new Entry(() -> { + logger.warn("Unexpected registration warning"); + logger.error("Actual registration error"); + logger.warn(message(GRASS), new IllegalStateException("A real exception")); + }); + LegacyRegistryWarnings.setName(entry, new ResourceLocation(GRASS)); + logger.warn(message(GRASS)); + assertEquals(4, captured.size()); + assertEquals("Unexpected registration warning", captured.get(0)); + assertEquals("Actual registration error", captured.get(1)); + assertEquals(message(GRASS), captured.get(2)); + assertEquals(message(GRASS), captured.get(3)); + } finally { + logger.removeAppender(appender); + logger.setLevel(previousLevel); + appender.stop(); + } + } + + @Test + void onlyTheThreeExactRecoveryNamesQualify() { + for (String identity : new String[] { + GRASS, "buildingbricks:dirt_slab", "buildingbrickscompatvanilla:grass_slab" }) { + assertTrue(matches(identity, message(identity)), identity); + } + assertFalse(matches("buildingbricks:stone_slab", message("buildingbricks:stone_slab"))); + assertFalse(matches("othermod:grass_slab", message("othermod:grass_slab"))); + assertFalse(matches(null, message(GRASS))); + } + + @Test + void anotherNameOrRegisteringModIsNotHidden() { + assertFalse(matches(GRASS, message("buildingbricks:dirt_slab"))); + assertFalse(matches(GRASS, message(GRASS).replace("skysgrassslabs", "anothermod"))); + assertFalse(matches(GRASS, message(GRASS) + " Additional registry error")); + assertFalse(matches(GRASS, "A different warning")); + } + + @Test + void otherLevelsLoggersExceptionsAndCallSitesStayVisible() { + assertFalse(LegacyRegistryWarnings.matches(GRASS, "anothermod", Level.WARN, + message(GRASS), null, FORGE_ORIGIN)); + for (Level level : new Level[] { Level.INFO, Level.ERROR, Level.FATAL }) { + assertFalse(LegacyRegistryWarnings.matches(GRASS, "FML", level, + message(GRASS), null, FORGE_ORIGIN)); + } + assertFalse(LegacyRegistryWarnings.matches(GRASS, "FML", Level.WARN, + message(GRASS), new IllegalStateException("real failure"), FORGE_ORIGIN)); + assertFalse(LegacyRegistryWarnings.matches(GRASS, "FML", Level.WARN, + message(GRASS), null, new StackTraceElement[0])); + assertFalse(LegacyRegistryWarnings.matches(GRASS, "FML", Level.WARN, + message(GRASS), null, new StackTraceElement[] { FORGE_ORIGIN[1] })); + } + + @Test + void theScopeEndsAfterSuccessOrFailure() { + Entry successful = new Entry(() -> assertTrue(active(GRASS))); + LegacyRegistryWarnings.setName(successful, new ResourceLocation(GRASS)); + assertEquals(GRASS, successful.getRegistryName().toString()); + assertFalse(active(GRASS)); + + Entry failing = new Entry(() -> { + assertTrue(active(GRASS)); + throw new IllegalStateException("registration failed"); + }); + assertThrows(IllegalStateException.class, + () -> LegacyRegistryWarnings.setName(failing, new ResourceLocation(GRASS))); + assertFalse(active(GRASS)); + } + + @Test + void nestedNamesRestoreTheOuterScopeAndOtherThreadsAreUnaffected() { + String dirt = "buildingbricks:dirt_slab"; + Entry outer = new Entry(() -> { + LegacyRegistryWarnings.setName(new Entry(() -> assertTrue(active(dirt))), + new ResourceLocation(dirt)); + assertTrue(active(GRASS)); + AtomicBoolean hiddenElsewhere = new AtomicBoolean(true); + Thread worker = new Thread(() -> hiddenElsewhere.set(active(GRASS))); + worker.start(); + try { + worker.join(); + } catch (InterruptedException failure) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(failure); + } + assertFalse(hiddenElsewhere.get()); + }); + LegacyRegistryWarnings.setName(outer, new ResourceLocation(GRASS)); + assertFalse(active(GRASS)); + } + + @Test + void unknownNamesNeverEnterTheFilterScope() { + Entry entry = new Entry(() -> fail("Unknown registration must not be invoked")); + assertThrows(IllegalArgumentException.class, + () -> LegacyRegistryWarnings.setName(entry, new ResourceLocation("buildingbricks:stone_slab"))); + assertNull(entry.getRegistryName()); + } + + @Test + void diagnosticRowsAreOnlyHiddenAtTheKnownForgeCallSite() { + assertTrue(matches(GRASS, "****************************************")); + assertTrue(matches(GRASS, + "* at net.minecraftforge.fml.common.registry.IForgeRegistryEntry$Impl.setRegistryName(Forge.java:82)")); + assertFalse(matches(GRASS, "* A real unrelated warning")); + assertFalse(LegacyRegistryWarnings.matches(GRASS, "FML", Level.WARN, + "****************************************", null, new StackTraceElement[0])); + } + + private static boolean matches(String identity, String text) { + return LegacyRegistryWarnings.matches(identity, "FML", Level.WARN, text, null, FORGE_ORIGIN); + } + + private static boolean active(String identity) { + return LegacyRegistryWarnings.hideForCurrentRegistration( + "FML", Level.WARN, message(identity), null, FORGE_ORIGIN); + } + + private static String message(String identity) { + String[] parts = identity.split(":", 2); + return "* Dangerous alternative prefix `" + parts[0] + "` for name `" + parts[1] + + "`, expected `skysgrassslabs` invalid registry invocation/invalid name?"; + } + + private static final class Entry implements IForgeRegistryEntry { + private final Runnable duringRegistration; + private ResourceLocation name; + + private Entry(Runnable duringRegistration) { + this.duringRegistration = duringRegistration; + } + + @Override + public Entry setRegistryName(ResourceLocation name) { + duringRegistration.run(); + this.name = name; + return this; + } + + @Override + public ResourceLocation getRegistryName() { + return name; + } + + @Override + public Class getRegistryType() { + return Entry.class; + } + } +} diff --git a/src/test/java/zone/moddev/mc/skysgrassslabs/config/ConfigScreenContractTest.java b/src/test/java/zone/moddev/mc/skysgrassslabs/config/ConfigScreenContractTest.java new file mode 100644 index 0000000..ae288f1 --- /dev/null +++ b/src/test/java/zone/moddev/mc/skysgrassslabs/config/ConfigScreenContractTest.java @@ -0,0 +1,82 @@ +package zone.moddev.mc.skysgrassslabs.config; + +import java.io.File; +import java.nio.file.Path; +import net.minecraftforge.common.config.Configuration; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.relauncher.FMLInjectionData; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import zone.moddev.mc.skysgrassslabs.SkysGrassSlabs; +import static org.junit.jupiter.api.Assertions.*; + +class ConfigScreenContractTest { + @TempDir + Path temporaryDirectory; + + @BeforeEach + void provideMinecraftHome() throws Exception { + java.lang.reflect.Field home = FMLInjectionData.class.getDeclaredField("minecraftHome"); + home.setAccessible(true); + home.set(null, temporaryDirectory.toFile()); + } + + @Test + void configButtonHasAClientFactory() { + assertEquals("zone.moddev.mc.skysgrassslabs.client.GrassSlabsGuiFactory", + SkysGrassSlabs.class.getAnnotation(Mod.class).guiFactory()); + } + + @Test + void loadingKeepsTheHelpTextOnEveryProperty() { + SkysGrassSlabsConfig.load(temporaryDirectory.resolve("help.cfg").toFile()); + Configuration pending = SkysGrassSlabsConfig.configuration(); + assertFalse(pending.getCategory("worldgen").get("generateGrassSlabs").getComment().trim().isEmpty()); + assertFalse(pending.getCategory("compat").get("forceReplaceBuildingBricksSlabs").getComment().trim().isEmpty()); + } + + @Test + void allLanguagesExplainEachConfigEntry() throws Exception { + try (java.util.stream.Stream languages = java.nio.file.Files.list( + java.nio.file.Paths.get("src/main/resources/assets/skysgrassslabs/lang"))) { + for (Path language : languages.collect(java.util.stream.Collectors.toList())) { + java.util.Properties translations = new java.util.Properties(); + try (java.io.Reader reader = java.nio.file.Files.newBufferedReader(language, + java.nio.charset.StandardCharsets.UTF_8)) { + translations.load(reader); + } + assertFalse(translations.getProperty("config.skysgrassslabs.generateGrassSlabs.tooltip", "").trim().isEmpty(), + language + " needs help for generateGrassSlabs"); + assertFalse(translations.getProperty("config.skysgrassslabs.forceReplaceBuildingBricksSlabs.tooltip", "").trim().isEmpty(), + language + " needs help for forceReplaceBuildingBricksSlabs"); + } + } + } + + @Test + void savingChoicesDoesNotChangeTheRunningSession() { + File file = temporaryDirectory.resolve("settings.cfg").toFile(); + SkysGrassSlabsConfig.load(file); + Configuration pending = SkysGrassSlabsConfig.configuration(); + + assertTrue(pending.getCategory("worldgen").requiresMcRestart()); + assertTrue(pending.getCategory("compat").requiresMcRestart()); + pending.get("worldgen", "generateGrassSlabs", true).set(false); + pending.get("compat", "forceReplaceBuildingBricksSlabs", false).set(true); + SkysGrassSlabsConfig.saveChanges(); + + // Saving the GUI must not re-arbitrate generation or turn on migration mid-world. + assertTrue(SkysGrassSlabsConfig.generateGrassSlabs()); + assertFalse(SkysGrassSlabsConfig.forceReplaceBuildingBricksSlabs()); + Configuration written = new Configuration(file); + written.load(); + assertFalse(written.get("worldgen", "generateGrassSlabs", true).getBoolean()); + assertTrue(written.get("compat", "forceReplaceBuildingBricksSlabs", false).getBoolean()); + + SkysGrassSlabsConfig.load(file); + assertFalse(SkysGrassSlabsConfig.generateGrassSlabs()); + assertTrue(SkysGrassSlabsConfig.forceReplaceBuildingBricksSlabs()); + } + +}