diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a193371e..e2eb71ed8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -178,9 +178,9 @@ jobs: if-no-files-found: error retention-days: 30 path: | - build/libs/BaseMetals-2.6.1.112021.jar - build/libs/BaseMetals-2.6.1.112021-sources.jar - build/libs/BaseMetals-2.6.1.112021-javadoc.jar + build/libs/BaseMetals-2.6.2.112021.jar + build/libs/BaseMetals-2.6.2.112021-sources.jar + build/libs/BaseMetals-2.6.2.112021-javadoc.jar build/release/SHA256SUMS CHANGELOG.txt diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 8d3b23bbc..1e036f6ea 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,15 @@ +Base Metals 2.6.2.112021 — Minecraft 1.12.2 + +- Updated to OreSpawn 4.1.6.112021. Install it on both the client and server. + Saved ore rules carry over when you move a world from 1.10.2 or 1.11.2, + and importing an old OreSpawn 3 config no longer replaces an existing + OreSpawn override. +- Fixed shallow molten metal so it burns on contact, just like deeper molten + metal. Mercury still behaves as before. +- The server now checks your Fantasy Level before you join and tells you + what to change if it doesn't match. Old configs without that setting + still use High Fantasy. + Base Metals 2.6.1.112021 — Minecraft 1.12.2 - Fixed recovery of mercury blocks, carbon powder, iron nuggets and metal door diff --git a/README.md b/README.md index 968a8ddfc..bc89a612e 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,11 @@ Base Metals adds metals, alloys, equipment, building blocks, and the crackhammer Crush ores into powder, smelt the powder into ingots, or combine it into alloy blends. The metals also work with supported processing and tool-building mods. -This branch builds **2.6.1.112021** for Minecraft 1.12.2 and Forge. This update -fixes missing blocks and items when upgrading older Base Metals worlds. It -also keeps the gameplay fixes and optional Low Fantasy mode from 2.6.0. See the -[changelog](CHANGELOG.txt) for the release changes. +This branch builds **2.6.2.112021** for Minecraft 1.12.2 and Forge. I've +updated OreSpawn support so saved ore settings carry over when you move an +older world. This update also fixes shallow molten metal not burning on +contact and gives players a clear message if their Fantasy Level doesn't +match the server's. See the [changelog](CHANGELOG.txt) for the details. ## Installation @@ -24,7 +25,8 @@ Install these mods on both the client and server: - [MMDLib](https://www.curseforge.com/minecraft/mc-mods/mmdlib), tested with `1.0.0-rc2.36`. - [MMD OreSpawn](https://www.curseforge.com/minecraft/mc-mods/mmd-orespawn) - `4.1.0.112021` or newer in the 4.x line. + [`4.1.6.112021`](https://www.curseforge.com/minecraft/mc-mods/mmd-orespawn/files/9527784) + or newer in the 4.x line. - [Base Metals](https://www.curseforge.com/minecraft/mc-mods/base-metals). Put the jars in the instance's `mods` folder. Minecraft 1.12.2 runs on Java 8. @@ -44,6 +46,8 @@ loss instead. Test a complete copy of the world first. Containers and entities supplied by other mods still need their own compatible versions and upgrade support. +When moving a world from 1.10.2 or 1.11.2, copy the whole world folder, +including `serverconfig`, to keep its saved OreSpawn rules. ## Metals and alloys @@ -100,8 +104,9 @@ screen. Changes require a Minecraft restart. It does not remove existing items or change ore generation. An old configuration without `contentMode` still uses High Fantasy. The mode -applies to the whole Minecraft instance or server, not to one world. Clients -and servers must use the same mode. +applies to the whole Minecraft instance or server, not to one world. Use +the same Base Metals build and Fantasy Level on the client and server. If +the settings don't match, you'll see what to change before you join. OreSpawn is the only Base Metals ore generator. Edit ore rules in OreSpawn, not through MMDLib's old `using_orespawn` or `fallback_orespawn` switches. @@ -143,7 +148,7 @@ The [build-script guide](gradle/README.md) explains the supporting scripts. The [release guide](docs/releasing.md) covers publication from the MMD Actions tab. Release artifacts use the Maven coordinates -`zone.moddev.mc.basemetals:BaseMetals:2.6.1.112021`. Java packages remain under +`zone.moddev.mc.basemetals:BaseMetals:2.6.2.112021`. Java packages remain under `com.mcmoddev.basemetals`; this release does not move saved registry names. Report bugs through the [issue tracker](https://github.com/MinecraftModDevelopmentMods/BaseMetals/issues). diff --git a/docs/configuration.md b/docs/configuration.md index fc0932e88..67d60e622 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -48,9 +48,9 @@ enabled. Recipe-book unlocks operate independently of this setting. An older `BaseMetals.cfg` without `contentMode` is treated exactly like an explicit `high_fantasy` configuration. Unrecognized values are corrected to High -Fantasy. The mode is chosen when Minecraft starts. A client using a different -mode from the server is disconnected with an explanatory -Base Metals message before gameplay. +Fantasy. The mode is chosen when Minecraft starts. Use the same Base Metals +build and Fantasy Level on the server and client. If the settings differ, +Base Metals explains the mismatch before the player joins the world. The mode belongs to the Minecraft instance or dedicated server, not to an individual save. After a restart, the selected mode applies to every world @@ -64,6 +64,6 @@ and saved-world profiles are not rewritten when the mode changes. ## OreSpawn dependency Development, CI, and release qualification use the published OreSpawn -`4.1.0.112021` CurseForge artifact. Base Metals pins its exact CurseForge file +`4.1.6.112021` CurseForge artifact. Base Metals pins its exact CurseForge file ID and SHA-256 and verifies the mod metadata and required client API classes before accepting the dependency. diff --git a/docs/world-generation.md b/docs/world-generation.md index 24d59154a..8fc4cd179 100644 --- a/docs/world-generation.md +++ b/docs/world-generation.md @@ -1,6 +1,6 @@ # Base Metals world generation -Base Metals 2.6 requires OreSpawn 4.1.0.112021 or newer in the 4.x line. +This build requires OreSpawn 4.1.6.112021 or newer in the 4.x line. OreSpawn is the only Base Metals ore generator. Base Metals does not register a fallback generator. @@ -48,7 +48,11 @@ OreSpawn 4 migrates `config/orespawn3/basemetals.json` into the same stable rule IDs used by the native provider. Customized frequencies, heights and disabled rules therefore remain associated with their Base Metals rules. Previously generated `config/basemetals-orespawn.json` overrides also remain -valid and authoritative. +valid and take precedence over a first-time OreSpawn 3 import. + +When moving a world from 1.10.2 or 1.11.2, copy the whole world folder, +including its `serverconfig` folder. OreSpawn keeps the world's saved ore +settings. Moving the world does not, by itself, regenerate existing chunks. The legacy MMDLib settings named `using_orespawn` and `fallback_orespawn` are not Base Metals OreSpawn 4 controls. Use the provider override or the per-world diff --git a/gradle.properties b/gradle.properties index 1a8a97dce..fc77db254 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ org.gradle.parallel=false net.minecraftforge.gradle.merge-source-sets=false mod_group=zone.moddev.mc.basemetals -mod_version=2.6.1.112021 +mod_version=2.6.2.112021 # Generic release-dispatcher metadata. minecraft_version=1.12.2 @@ -31,9 +31,9 @@ mmdlib_version=1.0.0-rc2.36 mmdlib_sha256=FE2229E6755A5FD306C33CC22DCCF055378D371339133E4912C5DC9213E360AF orespawn4_curse_project_id=245586 -orespawn4_curse_file_id=9008120 -orespawn4_version=4.1.0.112021 -orespawn4_sha256=B3018214BD1A612309A78DF02C552C0948A8FCD3CF314F25D40071E8B4605670 +orespawn4_curse_file_id=9527784 +orespawn4_version=4.1.6.112021 +orespawn4_sha256=DA6B243317E3365371C397CC9A895D108312F3437FB1797FBB5E8241525EA75F # Rejection and migration fixture only; Base Metals no longer supports OS3. orespawn3_curse_project_id=245586 diff --git a/gradle/release/artifacts.gradle b/gradle/release/artifacts.gradle index e74e89fc7..52fd2bdcd 100644 --- a/gradle/release/artifacts.gradle +++ b/gradle/release/artifacts.gradle @@ -139,6 +139,7 @@ tasks.register('verifyReleaseArtifacts') { 'com/mcmoddev/basemetals/items/ItemBaseMetalsBow.class', // Returns the tool-material name expected by Ancient Warfare 2. 'com/mcmoddev/basemetals/items/ItemBaseMetalsHoe.class', + 'com/mcmoddev/basemetals/block/BlockBaseMetalsMoltenFluid.class', 'com/mcmoddev/basemetals/items/MaterialRangedDamage.class', // Replaces MMDLib's reference to a nonexistent bolt texture. 'com/mcmoddev/basemetals/client/renderer/RenderBaseMetalsBolt.class', @@ -158,6 +159,7 @@ tasks.register('verifyReleaseArtifacts') { 'com/mcmoddev/basemetals/network/ClientContentModeConnectionHandler.class', 'com/mcmoddev/basemetals/network/ContentModeMessage.class', 'com/mcmoddev/basemetals/network/ContentModeNetwork.class', + 'com/mcmoddev/basemetals/network/ContentModeNetwork$RejectModList.class', 'com/mcmoddev/basemetals/network/ContentModeNetwork$ServerHandler.class', 'com/mcmoddev/basemetals/recipe/ContentAcquisitionFilters.class', 'com/mcmoddev/basemetals/recipe/PlateRepairFactory.class', diff --git a/gradle/verification/packaged-forge-dependencies.gradle b/gradle/verification/packaged-forge-dependencies.gradle index beb2c4f25..b9546eb7f 100644 --- a/gradle/verification/packaged-forge-dependencies.gradle +++ b/gradle/verification/packaged-forge-dependencies.gradle @@ -9,9 +9,15 @@ def originalArtifact = packagedForge.originalArtifact def runtimeLogFailures = packagedForge.runtimeLogFailures def stagePackagedForgeBase = packagedForge.stagePackagedForgeBase -// Check that OreSpawn is required and saved worldgen overrides are preserved. +// Importing an older OreSpawn file must not replace an existing override. File existingOverrideRun = file("${buildDir}/packaged-forge-existing-override-run") File existingOverrideMarker = new File(existingOverrideRun, 'basemetals-runtime-probe.properties') +File existingOverrideFile = new File(existingOverrideRun, 'config/basemetals-orespawn.json') +File existingOverrideLegacyFile = new File(existingOverrideRun, 'config/orespawn3/basemetals.json') +def legacyFixture = file('src/test/resources/orespawn3/basemetals-custom.json') +String ordinaryDimensions = 'orespawn:all_except_nether_end' +BigDecimal overrideNickelFrequency = 6.25 +String expectedOverrideText def prepareExistingOverride = tasks.register('preparePackagedForgeExistingOverrideRuntime') { group = 'verification' dependsOn releaseJar @@ -34,9 +40,16 @@ def prepareExistingOverride = tasks.register('preparePackagedForgeExistingOverri def explicitOverride = new JsonSlurper().parse( file('docs/examples/basemetals-orespawn-disabled.json')) explicitOverride.ores.each { id, rule -> rule.enabled = true } - new File(config, 'basemetals-orespawn.json').setText( - groovy.json.JsonOutput.prettyPrint(groovy.json.JsonOutput.toJson(explicitOverride)) - + System.lineSeparator(), 'UTF-8') + def nickel = explicitOverride.ores['basemetals:legacy/nickel_ore'] + nickel.dimension_selectors[ordinaryDimensions].frequency = overrideNickelFrequency + expectedOverrideText = groovy.json.JsonOutput.prettyPrint( + groovy.json.JsonOutput.toJson(explicitOverride)) + System.lineSeparator() + existingOverrideFile.setText(expectedOverrideText, 'UTF-8') + copy { + from legacyFixture + into existingOverrideLegacyFile.parentFile + rename { 'basemetals.json' } + } } } @@ -69,26 +82,33 @@ def previousExistingOverride = prepareExistingOverride if (!existingOverrideMarker.isFile()) { throw new GradleException("Existing OreSpawn 4 override ${phase} probe failed") } - File override = new File(existingOverrideRun, 'config/basemetals-orespawn.json') File worldProfile = new File(existingOverrideRun, 'world/serverconfig/orespawn-worldgen.json') - if (!override.isFile() || !worldProfile.isFile()) { - throw new GradleException('Existing OreSpawn 4 override or world snapshot is missing') + File report = new File(existingOverrideRun, 'config/orespawn-os3-migration-report.json') + if (!existingOverrideFile.isFile() || !worldProfile.isFile() || !report.isFile()) { + throw new GradleException('The OreSpawn override, migration report or world snapshot is missing') } - def overrideJson = new JsonSlurper().parse(override) - def profile = new JsonSlurper().parse(worldProfile) - def overrideRules = overrideJson.ores.findAll { id, rule -> - id.startsWith('basemetals:legacy/') + if (existingOverrideFile.getText('UTF-8') != expectedOverrideText) { + throw new GradleException("OreSpawn 3 import rewrote the native override on ${phase}") } + if (!existingOverrideLegacyFile.isFile() || !Arrays.equals(existingOverrideLegacyFile.bytes, + legacyFixture.bytes)) { + throw new GradleException("OreSpawn 3 import changed the original legacy file on ${phase}") + } + def profile = new JsonSlurper().parse(worldProfile) def snapshotRules = profile.ores.findAll { id, rule -> id.startsWith('basemetals:legacy/') } - if (overrideRules.size() != 11 || snapshotRules.size() != 11 + if (snapshotRules.size() != 11 || !snapshotRules.every { id, rule -> rule.enabled && rule.source_provider == 'basemetals' }) { throw new GradleException('Existing OreSpawn 4 override did not retain eleven stable rules') } + def nickel = snapshotRules['basemetals:legacy/nickel_ore'] + if (nickel.dimension_selectors[ordinaryDimensions]?.frequency != overrideNickelFrequency) { + throw new GradleException("OreSpawn 3 import displaced the native nickel frequency on ${phase}") + } List failures = runtimeLogFailures(existingOverrideRun) if (!failures.isEmpty()) { throw new GradleException("Existing OreSpawn 4 override ${phase} logged runtime failures:\n" @@ -101,7 +121,7 @@ def previousExistingOverride = prepareExistingOverride tasks.register('packagedForgeExistingOverrideRuntimeTest') { group = 'verification' - description = 'Loads an existing translated OreSpawn 4 override and reloads its world snapshot.' + description = 'Checks native OreSpawn 4 override precedence on first OreSpawn 3 import and reload.' dependsOn previousExistingOverride } @@ -148,7 +168,7 @@ tasks.register('packagedForgeMissingOreSpawnTest', Exec) { if (!output.contains('MissingModsException') || !output.contains('basemetals') || !output.contains('orespawn') - || !output.contains('4.1.0.112021')) { + || !output.contains(project.orespawn4_version)) { throw new GradleException('Missing OreSpawn 4 was not rejected by the Base Metals version contract') } } @@ -199,7 +219,7 @@ tasks.register('packagedForgeOs3RejectionTest', Exec) { if (!output.contains('MissingModsException') || !output.contains('basemetals') || !output.contains('orespawn') - || !output.contains('4.1.0.112021')) { + || !output.contains(project.orespawn4_version)) { throw new GradleException('OreSpawn 3 was not rejected by the Base Metals version contract') } } diff --git a/gradle/verification/support.gradle b/gradle/verification/support.gradle index 48caef782..87a4fe7b2 100644 --- a/gradle/verification/support.gradle +++ b/gradle/verification/support.gradle @@ -82,7 +82,7 @@ tasks.register('verifyReleaseConfiguration') { description = 'Validates the complete Base Metals 1.12 release identity and dependency contract.' dependsOn tasks.named('verifyReleaseToolchains') doLast { - if (project.mod_version != '2.6.1.112021' + if (project.mod_version != '2.6.2.112021' || project.minecraft_version != '1.12.2' || project.forge_version != '14.23.5.2859' || project.mappings_channel != 'stable' @@ -120,19 +120,19 @@ tasks.register('verifyReleaseConfiguration') { throw new GradleException('Unexpected CurseForge publication contract') } if (dependencySpecs.mmdlib.version != '1.0.0-rc2.36' - || dependencySpecs.orespawn4.version != '4.1.0.112021' + || dependencySpecs.orespawn4.version != '4.1.6.112021' || dependencySpecs.orespawn3.version != '3.3.1.179') { throw new GradleException('Unexpected MMDLib, OreSpawn 4, or OreSpawn 3 fixture version') } if (project.orespawn4_curse_project_id != '245586' - || project.orespawn4_curse_file_id != '9008120' - || project.orespawn4_sha256 != 'B3018214BD1A612309A78DF02C552C0948A8FCD3CF314F25D40071E8B4605670') { + || project.orespawn4_curse_file_id != '9527784' + || project.orespawn4_sha256 != 'DA6B243317E3365371C397CC9A895D108312F3437FB1797FBB5E8241525EA75F') { throw new GradleException('Unexpected published OreSpawn 4.1 CurseForge artifact identity') } String source = file('src/main/java/com/mcmoddev/basemetals/BaseMetals.java').getText('UTF-8') - if (!source.contains('VERSION = "2.6.1.112021"') + if (!source.contains('VERSION = "2.6.2.112021"') || !source.contains('certificateFingerprint = ""') - || !source.contains('required-after:orespawn@[4.1.0.112021,5.0.0)')) { + || !source.contains("required-after:orespawn@[${project.orespawn4_version},5.0.0)")) { throw new GradleException('Base Metals source version or unsigned fingerprint contract drifted') } } diff --git a/src/main/java/com/mcmoddev/basemetals/BaseMetals.java b/src/main/java/com/mcmoddev/basemetals/BaseMetals.java index 847fc96aa..f9f15724e 100644 --- a/src/main/java/com/mcmoddev/basemetals/BaseMetals.java +++ b/src/main/java/com/mcmoddev/basemetals/BaseMetals.java @@ -28,7 +28,7 @@ modid = BaseMetals.MODID, name = BaseMetals.NAME, version = BaseMetals.VERSION, - dependencies = "required-after:forge@[14.23.5.2859,);required-after:mmdlib;required-after:orespawn@[4.1.0.112021,5.0.0);after:tconstruct;after:conarm;after:thaumcraft;after:ic2;before:buildingbricks", + dependencies = "required-after:forge@[14.23.5.2859,);required-after:mmdlib;required-after:orespawn@[4.1.6.112021,5.0.0);after:tconstruct;after:conarm;after:thaumcraft;after:ic2;before:buildingbricks", acceptedMinecraftVersions = "[1.12,1.12.2]", guiFactory = "com.mcmoddev.basemetals.client.config.BaseMetalsGuiFactory", certificateFingerprint = "", @@ -45,7 +45,7 @@ public final class BaseMetals { protected static final String NAME = "Base Metals"; /** Release version in the shared MMD major.minor.patch.target format. */ - protected static final String VERSION = "2.6.1.112021"; + protected static final String VERSION = "2.6.2.112021"; protected static final String UPDATEJSON = SharedStrings.UPDATE_JSON_URL + "BaseMetals/master/update.json"; diff --git a/src/main/java/com/mcmoddev/basemetals/block/BlockBaseMetalsMoltenFluid.java b/src/main/java/com/mcmoddev/basemetals/block/BlockBaseMetalsMoltenFluid.java new file mode 100644 index 000000000..733eb8714 --- /dev/null +++ b/src/main/java/com/mcmoddev/basemetals/block/BlockBaseMetalsMoltenFluid.java @@ -0,0 +1,36 @@ +package com.mcmoddev.basemetals.block; + +import com.mcmoddev.lib.block.BlockMoltenFluid; +import net.minecraft.block.state.IBlockState; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.util.DamageSource; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import net.minecraftforge.fluids.Fluid; + +/** Makes shallow molten metal burn on contact. */ +public final class BlockBaseMetalsMoltenFluid extends BlockMoltenFluid { + public BlockBaseMetalsMoltenFluid(final Fluid fluid) { + super(fluid); + } + + @Override + public void onEntityCollision(final World world, final BlockPos pos, final IBlockState state, + final Entity entity) { + super.onEntityCollision(world, pos, state, entity); + if (world.isRemote || !(entity instanceof EntityLivingBase) || entity.isImmuneToFire()) { + return; + } + + final IBlockState above = world.getBlockState(pos.up()); + final double surface = pos.getY() + getFluidHeightForRender(world, pos, above); + if (entity.getEntityBoundingBox().minY >= surface || entity.isInLava()) { + return; + } + + // Minecraft already handles deeper lava contact. + entity.attackEntityFrom(DamageSource.LAVA, 4.0F); + entity.setFire(15); + } +} diff --git a/src/main/java/com/mcmoddev/basemetals/init/Fluids.java b/src/main/java/com/mcmoddev/basemetals/init/Fluids.java index a9acdc352..06ff5f61e 100644 --- a/src/main/java/com/mcmoddev/basemetals/init/Fluids.java +++ b/src/main/java/com/mcmoddev/basemetals/init/Fluids.java @@ -4,12 +4,15 @@ import java.util.List; import com.mcmoddev.basemetals.BaseMetals; +import com.mcmoddev.basemetals.block.BlockBaseMetalsMoltenFluid; import com.mcmoddev.basemetals.data.MaterialNames; import com.mcmoddev.lib.data.SharedStrings; import com.mcmoddev.lib.events.MMDLibRegisterFluids; import com.mcmoddev.lib.init.Materials; +import com.mcmoddev.lib.material.MMDMaterial; import com.mcmoddev.lib.util.Config.Options; +import net.minecraftforge.fluids.FluidRegistry; import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; @@ -34,14 +37,25 @@ public static void registerEvent(MMDLibRegisterFluids event) { MaterialNames.TIN, MaterialNames.ZINC); materials.stream().filter(Materials::hasMaterial).filter(Options::isFluidEnabled) - .forEach(materialName -> { - addFluid(materialName, 2000, 10000, 769, 10); - addFluidBlock(materialName); - }); + .forEach(Fluids::registerMoltenFluid); if (Materials.hasMaterial(MaterialNames.MERCURY) && Options.isFluidEnabled(MaterialNames.MERCURY)) { addFluid(MaterialNames.MERCURY, 13594, 2000, 769, 0); addFluidBlock(MaterialNames.MERCURY); } } + + /** Registers a molten fluid for Base Metals or its Vanilla Bits. */ + public static void registerMoltenFluid(final String materialName) { + final MMDMaterial material = Materials.getMaterialByName(materialName); + final boolean useDefaultBlock = material.getFluid() == null && !material.hasCustomFluid() + && !FluidRegistry.isFluidRegistered(materialName); + addFluid(materialName, 2000, 10000, 769, 10); + + if (useDefaultBlock && material.getFluidBlock() == null) { + material.setFluidBlockGetter(name -> new BlockBaseMetalsMoltenFluid(FluidRegistry.getFluid(name))); + } + + addFluidBlock(materialName); + } } diff --git a/src/main/java/com/mcmoddev/basemetals/network/ContentModeNetwork.java b/src/main/java/com/mcmoddev/basemetals/network/ContentModeNetwork.java index 3d36ebe6a..ee5938528 100644 --- a/src/main/java/com/mcmoddev/basemetals/network/ContentModeNetwork.java +++ b/src/main/java/com/mcmoddev/basemetals/network/ContentModeNetwork.java @@ -1,10 +1,21 @@ package com.mcmoddev.basemetals.network; +import java.util.Set; + import com.mcmoddev.basemetals.BaseMetals; import com.mcmoddev.basemetals.content.ContentMode; import com.mcmoddev.basemetals.util.BMeConfig; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.network.NetworkManager; +import net.minecraft.network.PacketBuffer; +import net.minecraft.network.play.client.CPacketCustomPayload; +import net.minecraft.network.play.server.SPacketDisconnect; import net.minecraft.util.text.TextComponentString; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import net.minecraftforge.fml.common.network.FMLNetworkEvent.CustomPacketRegistrationEvent; import net.minecraftforge.fml.common.network.NetworkRegistry; import net.minecraftforge.fml.common.network.simpleimpl.IMessage; import net.minecraftforge.fml.common.network.simpleimpl.IMessageHandler; @@ -14,6 +25,11 @@ /** Rejects connections when the client and server use different content modes. */ public final class ContentModeNetwork { + private static final String HIGH_CHANNEL = "bm_mode_high"; + private static final String LOW_CHANNEL = "bm_mode_low"; + private static final String FORGE_HANDSHAKE = "FML|HS"; + private static final int FORGE_MOD_LIST = 2; + private static final String REJECTION_HANDLER = "basemetals:content_mode"; private static final SimpleNetworkWrapper CHANNEL = NetworkRegistry.INSTANCE.newSimpleChannel("bm_content_mode"); private static boolean initialized; @@ -22,6 +38,9 @@ private ContentModeNetwork() { public static synchronized void init() { if (!initialized) { + NetworkRegistry.INSTANCE.newEventDrivenChannel( + BMeConfig.getActiveContentMode() == ContentMode.LOW_FANTASY ? LOW_CHANNEL : HIGH_CHANNEL); + MinecraftForge.EVENT_BUS.register(new ContentModeNetwork()); CHANNEL.registerMessage(ServerHandler.class, ContentModeMessage.class, 0, Side.SERVER); initialized = true; } @@ -35,6 +54,72 @@ public static boolean modesMatch(final ContentMode client, final ContentMode ser return client == server; } + @SubscribeEvent + public void checkLoginChannels(final CustomPacketRegistrationEvent event) { + final Set channels = event.getRegistrations(); + if (!event.getSide().isServer() || !"REGISTER".equals(event.getOperation()) + || !channels.contains(FORGE_HANDSHAKE)) { + return; + } + + final ContentMode server = BMeConfig.getActiveContentMode(); + final boolean high = channels.contains(HIGH_CHANNEL); + final boolean low = channels.contains(LOW_CHANNEL); + final ContentMode client = low ? ContentMode.LOW_FANTASY : ContentMode.HIGH_FANTASY; + final String reason; + + if (high == low) { + reason = "Base Metals could not check the client's Fantasy Level. Use the same Base Metals build " + + "on the server and client."; + } else if (!modesMatch(client, server)) { + reason = mismatchMessage(client.serializedName(), server); + } else { + return; + } + + final NetworkManager manager = event.getManager(); + if (manager.channel().pipeline().get(REJECTION_HANDLER) == null) { + manager.channel().pipeline().addBefore("fml:packet_handler", REJECTION_HANDLER, + new RejectModList(manager, reason)); + } + } + + private static String mismatchMessage(final String client, final ContentMode server) { + return "Base Metals content mode mismatch: server uses '" + server.serializedName() + + "' but client uses '" + client + + "'. Set General/contentMode to the same value and restart Minecraft."; + } + + private static final class RejectModList extends ChannelInboundHandlerAdapter { + private final NetworkManager manager; + private final TextComponentString reason; + + RejectModList(final NetworkManager manager, final String reason) { + this.manager = manager; + this.reason = new TextComponentString(reason); + } + + @Override + public void channelRead(final ChannelHandlerContext context, final Object message) { + if (message instanceof CPacketCustomPayload) { + final CPacketCustomPayload packet = (CPacketCustomPayload) message; + final PacketBuffer data = packet.getBufferData(); + + // Closing at registration interrupts Forge's login writes. Wait for the client's mod list. + if (FORGE_HANDSHAKE.equals(packet.getChannelName()) && data.isReadable() + && data.getUnsignedByte(data.readerIndex()) == FORGE_MOD_LIST) { + data.release(); + context.channel().config().setAutoRead(false); + BaseMetals.logger.warn(reason.getUnformattedText()); + manager.sendPacket(new SPacketDisconnect(reason), future -> manager.closeChannel(reason)); + return; + } + } + + context.fireChannelRead(message); + } + } + public static final class ServerHandler implements IMessageHandler { @Override public IMessage onMessage(final ContentModeMessage message, final MessageContext context) { @@ -47,11 +132,9 @@ public IMessage onMessage(final ContentModeMessage message, final MessageContext final String clientName = message.hasValidMode() ? message.mode().serializedName() : "invalid"; - final String reason = "Base Metals content mode mismatch: server uses '" - + serverMode.serializedName() + "' but client uses '" + clientName - + "'. Set General/contentMode to the same value and restart Minecraft."; + final String reason = mismatchMessage(clientName, serverMode); - BaseMetals.logger.warn("Rejecting {}: {}", player.getName(), reason); + BaseMetals.logger.warn("Rejecting %s: %s", player.getName(), reason); player.connection.disconnect(new TextComponentString(reason)); } }); diff --git a/src/main/java/com/mcmoddev/basemetals/vanillabits/VanillaFluids.java b/src/main/java/com/mcmoddev/basemetals/vanillabits/VanillaFluids.java index 0f24ca3bb..6b4153b52 100755 --- a/src/main/java/com/mcmoddev/basemetals/vanillabits/VanillaFluids.java +++ b/src/main/java/com/mcmoddev/basemetals/vanillabits/VanillaFluids.java @@ -24,9 +24,7 @@ public static void registerFluids(MMDLibRegisterFluids ev) { MaterialNames.WOOD, MaterialNames.REDSTONE, MaterialNames.STONE, MaterialNames.REDSTONE).stream() .filter(Options::isMaterialEnabled) - .filter(Options::isFluidEnabled).forEach(materialName -> { - addFluid(materialName, 2000, 10000, 769, 10); - addFluidBlock(materialName); - }); + .filter(Options::isFluidEnabled) + .forEach(com.mcmoddev.basemetals.init.Fluids::registerMoltenFluid); } } diff --git a/src/test/java/com/mcmoddev/basemetals/test/BaseMetalsClientConfigRuntimeProbe.java b/src/test/java/com/mcmoddev/basemetals/test/BaseMetalsClientConfigRuntimeProbe.java index 9a19ab36f..2fceb2344 100644 --- a/src/test/java/com/mcmoddev/basemetals/test/BaseMetalsClientConfigRuntimeProbe.java +++ b/src/test/java/com/mcmoddev/basemetals/test/BaseMetalsClientConfigRuntimeProbe.java @@ -3,8 +3,11 @@ import com.mcmoddev.basemetals.client.config.BaseMetalsConfigScreen; import com.mcmoddev.basemetals.client.config.BaseMetalsGuiFactory; import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiDisconnected; import net.minecraft.client.gui.GuiMainMenu; import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.multiplayer.GuiConnecting; +import net.minecraft.util.text.ITextComponent; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.fml.client.FMLClientHandler; import net.minecraftforge.fml.client.GuiModList; @@ -22,6 +25,9 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.List; import java.util.Properties; import java.util.stream.Collectors; @@ -32,15 +38,22 @@ name = "Base Metals Client Configuration Probe", version = "1", clientSideOnly = true, - dependencies = "required-after:basemetals;required-after:orespawn@[4.1.0.112021,5.0.0)", + dependencies = "required-after:basemetals;required-after:orespawn@[4.1.6.112021,5.0.0)", acceptedMinecraftVersions = "[1.12.2]") public final class BaseMetalsClientConfigRuntimeProbe { public static final String MODID = "basemetals_client_config_probe"; private final Properties result = new Properties(); private boolean checkedModsButton; + private boolean connecting; + private int connectedTicks; @EventHandler public void init(final FMLInitializationEvent event) throws IOException { + if (Integer.getInteger("basemetals.connectionProbe.port", 0) != 0) { + MinecraftForge.EVENT_BUS.register(this); + return; + } + final List extensions = WorldSettingsExtensionRegistry.extensions() .stream() .filter(extension -> "basemetals".equals(extension.id().getNamespace())) @@ -82,11 +95,17 @@ public void init(final FMLInitializationEvent event) throws IOException { } @SubscribeEvent - public void checkModsButton(final TickEvent.ClientTickEvent event) throws IOException { + public void checkModsButton(final TickEvent.ClientTickEvent event) throws IOException, IllegalAccessException { if (checkedModsButton || event.phase != TickEvent.Phase.END) { return; } + final int port = Integer.getInteger("basemetals.connectionProbe.port", 0); + if (port != 0) { + checkConnection(port); + return; + } + // Forge creates GUI factories after mod initialization, before the first client tick. final IModGuiFactory factory = FMLClientHandler.instance().getGuiFactoryFor( Loader.instance().getIndexedModList().get("basemetals")); @@ -134,6 +153,40 @@ public void checkModsButton(final TickEvent.ClientTickEvent event) throws IOExce } } + private void checkConnection(final int port) throws IOException, IllegalAccessException { + final Minecraft client = Minecraft.getMinecraft(); + if (!connecting && client.currentScreen instanceof GuiMainMenu) { + connecting = true; + client.displayGuiScreen(new GuiConnecting(client.currentScreen, client, "127.0.0.1", port)); + return; + } + + if (connecting && client.currentScreen instanceof GuiDisconnected) { + for (final Field field : GuiDisconnected.class.getDeclaredFields()) { + if (field.getType() == ITextComponent.class) { + field.setAccessible(true); + final ITextComponent message = (ITextComponent) field.get(client.currentScreen); + finishConnectionCheck(client, message.getUnformattedText()); + return; + } + } + + throw new AssertionError("The disconnect screen has no message"); + } + + if (!Boolean.getBoolean("basemetals.connectionProbe.expectDisconnect") + && client.world != null && client.player != null && ++connectedTicks >= 10) { + finishConnectionCheck(client, "joined"); + } + } + + private void finishConnectionCheck(final Minecraft client, final String message) throws IOException { + final File marker = new File(System.getProperty("basemetals.connectionProbe.marker")); + Files.write(marker.toPath(), message.getBytes(StandardCharsets.UTF_8)); + checkedModsButton = true; + client.shutdown(); + } + private static void require(final boolean condition, final String message) { if (!condition) { throw new IllegalStateException(message); diff --git a/src/test/java/com/mcmoddev/basemetals/test/BaseMetalsRuntimeProbe.java b/src/test/java/com/mcmoddev/basemetals/test/BaseMetalsRuntimeProbe.java index ad470cb6e..ae98584e7 100644 --- a/src/test/java/com/mcmoddev/basemetals/test/BaseMetalsRuntimeProbe.java +++ b/src/test/java/com/mcmoddev/basemetals/test/BaseMetalsRuntimeProbe.java @@ -29,8 +29,10 @@ import net.minecraft.advancements.Advancement; import net.minecraft.block.Block; +import net.minecraft.block.state.IBlockState; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; +import net.minecraft.entity.MoverType; import net.minecraft.entity.monster.EntityIronGolem; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; @@ -137,6 +139,8 @@ public void serverStarted(final FMLServerStartedEvent event) throws IOException FMLCommonHandler.instance().getMinecraftServerInstance()); final int armorEffectChecks = verifyArmorEffects( FMLCommonHandler.instance().getMinecraftServerInstance()); + final int moltenContactChecks = verifyMoltenContact( + FMLCommonHandler.instance().getMinecraftServerInstance().getWorld(0)); final WorldgenCounts worldgen = expectedCoFHWorld ? verifyCoFHWorldgen(FMLCommonHandler.instance().getMinecraftServerInstance()) : new WorldgenCounts(0, 0); @@ -145,7 +149,7 @@ public void serverStarted(final FMLServerStartedEvent event) throws IOException final String mmdLibVersion = version("mmdlib"); final String oreSpawnVersion = version("orespawn"); - require("2.6.1.112021".equals(baseMetalsVersion), + require("2.6.2.112021".equals(baseMetalsVersion), "Unexpected Base Metals version " + baseMetalsVersion); // MMDLib rc2.36 still reports "rc2" in its mod metadata. require("1.0.0-rc2".equals(mmdLibVersion), @@ -179,6 +183,7 @@ public void serverStarted(final FMLServerStartedEvent event) throws IOException result.setProperty("rangedItemsChecked", Integer.toString(rangedItemsChecked)); result.setProperty("rangedDamageChecks", Integer.toString(rangedDamageChecks)); result.setProperty("armorEffectChecks", Integer.toString(armorEffectChecks)); + result.setProperty("moltenContactChecks", Integer.toString(moltenContactChecks)); result.setProperty("registered", "true"); marker.getParentFile().mkdirs(); @@ -189,6 +194,65 @@ public void serverStarted(final FMLServerStartedEvent event) throws IOException FMLCommonHandler.instance().getMinecraftServerInstance().initiateShutdown(); } + private static int verifyMoltenContact(final WorldServer world) { + final BlockPos pos = new BlockPos(8, 150, 12); + final IBlockState original = world.getBlockState(pos); + final Block copper = Block.REGISTRY.getObject(new ResourceLocation("basemetals", "copper")); + final Block mercury = Block.REGISTRY.getObject(new ResourceLocation("basemetals", "mercury")); + requireRegistered(copper, "basemetals:copper"); + requireRegistered(mercury, "basemetals:mercury"); + + try { + world.setBlockState(pos, copper.getDefaultState(), 2); + checkFluidContact(world, pos, "deep molten copper", 0.25, false, false, 4.0F); + checkFluidContact(world, pos, "above molten copper", 0.90, false, false, 0.0F); + checkFluidContact(world, pos, "shallow molten copper", 0.70, false, false, 4.0F); + checkFluidContact(world, pos, "fire resistance", 0.70, true, false, 0.0F); + checkFluidContact(world, pos, "creative mode", 0.70, false, true, 0.0F); + + world.setBlockState(pos, copper.getStateFromMeta(1), 2); + checkFluidContact(world, pos, "above flowing copper", 0.80, false, false, 0.0F); + checkFluidContact(world, pos, "shallow flowing copper", 0.70, false, false, 4.0F); + + world.setBlockState(pos, Blocks.WATER.getDefaultState(), 2); + checkFluidContact(world, pos, "water", 0.25, false, false, 0.0F); + world.setBlockState(pos, mercury.getDefaultState(), 2); + checkFluidContact(world, pos, "liquid mercury", 0.25, false, false, 0.0F); + } finally { + world.setBlockState(pos, original, 2); + } + + return 9; + } + + private static void checkFluidContact(final WorldServer world, final BlockPos pos, final String label, + final double height, final boolean resistant, final boolean creative, final float expectedDamage) { + final EntityPlayer player = new EntityPlayer(world, new GameProfile(UUID.randomUUID(), "FluidCheck")) { + @Override + public boolean isSpectator() { + return false; + } + + @Override + public boolean isCreative() { + return creative; + } + }; + player.capabilities.disableDamage = creative; + if (resistant) { + player.addPotionEffect(new PotionEffect(MobEffects.FIRE_RESISTANCE, 200)); + } + + player.setPosition(pos.getX() - 0.5, pos.getY() + height, pos.getZ() + 0.5); + player.move(MoverType.SELF, 1.0, 0.0, 0.0); + player.onEntityUpdate(); + + final float damage = player.getMaxHealth() - player.getHealth(); + require(damage == expectedDamage && (expectedDamage == 0.0F || player.isBurning()), + label + " expected " + expectedDamage + " damage, got " + damage + + " (burning: " + player.isBurning() + ")"); + } + private static WorldgenCounts verifyCoFHWorldgen(final MinecraftServer server) { final WorldServer world = server.getWorld(0); final BlockPos spawn = world.getSpawnPoint();