From 8a07b27ba8f69b76ebaa4253355fb7a003cf2ee9 Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Mon, 5 Oct 2026 12:26:48 +0100 Subject: [PATCH] Port Base Metals to Minecraft 1.14.4 Keep the existing materials, recipes and save names while updating configuration screens, fluids, village trades and loot for Forge 28. Use OreSpawn 4.1.0.114041, preserve the tested world upgrades and fix Creative crossbow firing without bolts. Split the build scripts and refresh the release documentation. --- .gitattributes | 9 +- .github/workflows/ci.yml | 34 +- .github/workflows/codeql-analysis.yml | 4 +- .github/workflows/validate-gradle-build.yml | 4 +- CHANGELOG.txt | 25 + LICENSE-ART.txt | 22 +- README.md | 38 +- build.gradle | 1062 +---------------- docs/COMPATIBILITY.md | 6 +- docs/MIGRATION.md | 31 +- docs/VERSIONS.md | 10 +- docs/WORLDGEN.md | 6 +- gradle.properties | 26 +- gradle/README.md | 29 + gradle/ide/eclipse.gradle | 243 ++++ gradle/ide/intellij.gradle | 46 + gradle/release/artifacts.gradle | 338 ++++++ gradle/verification/development.gradle | 153 +++ gradle/verification/generated-data.gradle | 95 ++ gradle/verification/packaged-forge.gradle | 316 +++++ .../fml/client/gui/ModSelection.java | 20 + .../testmod/BaseMetalsClientProbe.java | 137 ++- .../resources/META-INF/mods.toml | 10 +- .../loot_tables/blocks/adamantine_anvil.json | 48 + .../loot_tables/blocks/adamantine_bars.json | 21 + .../loot_tables/blocks/adamantine_block.json | 21 + .../loot_tables/blocks/adamantine_button.json | 21 + .../loot_tables/blocks/adamantine_door.json | 28 + .../loot_tables/blocks/adamantine_lever.json | 21 + .../loot_tables/blocks/adamantine_ore.json | 21 + .../loot_tables/blocks/adamantine_plate.json | 21 + .../blocks/adamantine_pressure_plate.json | 21 + .../loot_tables/blocks/adamantine_slab.json | 35 + .../loot_tables/blocks/adamantine_stairs.json | 21 + .../blocks/adamantine_trapdoor.json | 21 + .../loot_tables/blocks/adamantine_wall.json | 21 + .../loot_tables/blocks/antimony_bars.json | 21 + .../loot_tables/blocks/antimony_block.json | 21 + .../loot_tables/blocks/antimony_button.json | 21 + .../loot_tables/blocks/antimony_door.json | 28 + .../loot_tables/blocks/antimony_lever.json | 21 + .../loot_tables/blocks/antimony_ore.json | 21 + .../loot_tables/blocks/antimony_plate.json | 21 + .../blocks/antimony_pressure_plate.json | 21 + .../loot_tables/blocks/antimony_slab.json | 35 + .../loot_tables/blocks/antimony_stairs.json | 21 + .../loot_tables/blocks/antimony_trapdoor.json | 21 + .../loot_tables/blocks/antimony_wall.json | 21 + .../loot_tables/blocks/aquarium_bars.json | 21 + .../loot_tables/blocks/aquarium_block.json | 21 + .../loot_tables/blocks/aquarium_button.json | 21 + .../loot_tables/blocks/aquarium_door.json | 28 + .../loot_tables/blocks/aquarium_lever.json | 21 + .../loot_tables/blocks/aquarium_plate.json | 21 + .../blocks/aquarium_pressure_plate.json | 21 + .../loot_tables/blocks/aquarium_slab.json | 35 + .../loot_tables/blocks/aquarium_stairs.json | 21 + .../loot_tables/blocks/aquarium_trapdoor.json | 21 + .../loot_tables/blocks/aquarium_wall.json | 21 + .../loot_tables/blocks/bismuth_bars.json | 21 + .../loot_tables/blocks/bismuth_block.json | 21 + .../loot_tables/blocks/bismuth_button.json | 21 + .../loot_tables/blocks/bismuth_door.json | 28 + .../loot_tables/blocks/bismuth_lever.json | 21 + .../loot_tables/blocks/bismuth_ore.json | 21 + .../loot_tables/blocks/bismuth_plate.json | 21 + .../blocks/bismuth_pressure_plate.json | 21 + .../loot_tables/blocks/bismuth_slab.json | 35 + .../loot_tables/blocks/bismuth_stairs.json | 21 + .../loot_tables/blocks/bismuth_trapdoor.json | 21 + .../loot_tables/blocks/bismuth_wall.json | 21 + .../loot_tables/blocks/brass_bars.json | 21 + .../loot_tables/blocks/brass_block.json | 21 + .../loot_tables/blocks/brass_button.json | 21 + .../loot_tables/blocks/brass_door.json | 28 + .../loot_tables/blocks/brass_lever.json | 21 + .../loot_tables/blocks/brass_plate.json | 21 + .../blocks/brass_pressure_plate.json | 21 + .../loot_tables/blocks/brass_slab.json | 35 + .../loot_tables/blocks/brass_stairs.json | 21 + .../loot_tables/blocks/brass_trapdoor.json | 21 + .../loot_tables/blocks/brass_wall.json | 21 + .../loot_tables/blocks/bronze_bars.json | 21 + .../loot_tables/blocks/bronze_block.json | 21 + .../loot_tables/blocks/bronze_button.json | 21 + .../loot_tables/blocks/bronze_door.json | 28 + .../loot_tables/blocks/bronze_lever.json | 21 + .../loot_tables/blocks/bronze_plate.json | 21 + .../blocks/bronze_pressure_plate.json | 21 + .../loot_tables/blocks/bronze_slab.json | 35 + .../loot_tables/blocks/bronze_stairs.json | 21 + .../loot_tables/blocks/bronze_trapdoor.json | 21 + .../loot_tables/blocks/bronze_wall.json | 21 + .../loot_tables/blocks/charcoal_block.json | 21 + .../loot_tables/blocks/coldiron_bars.json | 21 + .../loot_tables/blocks/coldiron_block.json | 21 + .../loot_tables/blocks/coldiron_button.json | 21 + .../loot_tables/blocks/coldiron_door.json | 28 + .../loot_tables/blocks/coldiron_lever.json | 21 + .../loot_tables/blocks/coldiron_ore.json | 21 + .../loot_tables/blocks/coldiron_plate.json | 21 + .../blocks/coldiron_pressure_plate.json | 21 + .../loot_tables/blocks/coldiron_slab.json | 35 + .../loot_tables/blocks/coldiron_stairs.json | 21 + .../loot_tables/blocks/coldiron_trapdoor.json | 21 + .../loot_tables/blocks/coldiron_wall.json | 21 + .../loot_tables/blocks/copper_bars.json | 21 + .../loot_tables/blocks/copper_block.json | 21 + .../loot_tables/blocks/copper_button.json | 21 + .../loot_tables/blocks/copper_door.json | 28 + .../loot_tables/blocks/copper_lever.json | 21 + .../loot_tables/blocks/copper_ore.json | 21 + .../loot_tables/blocks/copper_plate.json | 21 + .../blocks/copper_pressure_plate.json | 21 + .../loot_tables/blocks/copper_slab.json | 35 + .../loot_tables/blocks/copper_stairs.json | 21 + .../loot_tables/blocks/copper_trapdoor.json | 21 + .../loot_tables/blocks/copper_wall.json | 21 + .../loot_tables/blocks/cupronickel_bars.json | 21 + .../loot_tables/blocks/cupronickel_block.json | 21 + .../blocks/cupronickel_button.json | 21 + .../loot_tables/blocks/cupronickel_door.json | 28 + .../loot_tables/blocks/cupronickel_lever.json | 21 + .../loot_tables/blocks/cupronickel_plate.json | 21 + .../blocks/cupronickel_pressure_plate.json | 21 + .../loot_tables/blocks/cupronickel_slab.json | 35 + .../blocks/cupronickel_stairs.json | 21 + .../blocks/cupronickel_trapdoor.json | 21 + .../loot_tables/blocks/cupronickel_wall.json | 21 + .../loot_tables/blocks/diamond_bars.json | 21 + .../loot_tables/blocks/diamond_button.json | 21 + .../loot_tables/blocks/diamond_door.json | 28 + .../loot_tables/blocks/diamond_lever.json | 21 + .../blocks/diamond_pressure_plate.json | 21 + .../loot_tables/blocks/diamond_slab.json | 35 + .../loot_tables/blocks/diamond_stairs.json | 21 + .../loot_tables/blocks/diamond_trapdoor.json | 21 + .../loot_tables/blocks/diamond_wall.json | 21 + .../blocks/double_adamantine_slab.json | 26 + .../blocks/double_antimony_slab.json | 26 + .../blocks/double_aquarium_slab.json | 26 + .../blocks/double_bismuth_slab.json | 26 + .../loot_tables/blocks/double_brass_slab.json | 26 + .../blocks/double_bronze_slab.json | 26 + .../blocks/double_coldiron_slab.json | 26 + .../blocks/double_copper_slab.json | 26 + .../blocks/double_cupronickel_slab.json | 26 + .../blocks/double_diamond_slab.json | 26 + .../blocks/double_electrum_slab.json | 26 + .../blocks/double_emerald_slab.json | 26 + .../loot_tables/blocks/double_gold_slab.json | 26 + .../loot_tables/blocks/double_invar_slab.json | 26 + .../loot_tables/blocks/double_iron_slab.json | 26 + .../loot_tables/blocks/double_lead_slab.json | 26 + .../blocks/double_mithril_slab.json | 26 + .../blocks/double_nickel_slab.json | 26 + .../blocks/double_obsidian_slab.json | 26 + .../blocks/double_pewter_slab.json | 26 + .../blocks/double_platinum_slab.json | 26 + .../blocks/double_silver_slab.json | 26 + .../blocks/double_starsteel_slab.json | 26 + .../loot_tables/blocks/double_steel_slab.json | 26 + .../loot_tables/blocks/double_tin_slab.json | 26 + .../loot_tables/blocks/double_zinc_slab.json | 26 + .../loot_tables/blocks/electrum_bars.json | 21 + .../loot_tables/blocks/electrum_block.json | 21 + .../loot_tables/blocks/electrum_button.json | 21 + .../loot_tables/blocks/electrum_door.json | 28 + .../loot_tables/blocks/electrum_lever.json | 21 + .../loot_tables/blocks/electrum_plate.json | 21 + .../blocks/electrum_pressure_plate.json | 21 + .../loot_tables/blocks/electrum_slab.json | 35 + .../loot_tables/blocks/electrum_stairs.json | 21 + .../loot_tables/blocks/electrum_trapdoor.json | 21 + .../loot_tables/blocks/electrum_wall.json | 21 + .../loot_tables/blocks/emerald_bars.json | 21 + .../loot_tables/blocks/emerald_button.json | 21 + .../loot_tables/blocks/emerald_door.json | 28 + .../loot_tables/blocks/emerald_lever.json | 21 + .../blocks/emerald_pressure_plate.json | 21 + .../loot_tables/blocks/emerald_slab.json | 35 + .../loot_tables/blocks/emerald_stairs.json | 21 + .../loot_tables/blocks/emerald_trapdoor.json | 21 + .../loot_tables/blocks/emerald_wall.json | 21 + .../loot_tables/blocks/gold_bars.json | 21 + .../loot_tables/blocks/gold_button.json | 21 + .../loot_tables/blocks/gold_door.json | 28 + .../loot_tables/blocks/gold_lever.json | 21 + .../loot_tables/blocks/gold_plate.json | 21 + .../loot_tables/blocks/gold_slab.json | 35 + .../loot_tables/blocks/gold_stairs.json | 21 + .../loot_tables/blocks/gold_trapdoor.json | 21 + .../loot_tables/blocks/gold_wall.json | 21 + .../loot_tables/blocks/human_detector.json | 21 + .../loot_tables/blocks/invar_bars.json | 21 + .../loot_tables/blocks/invar_block.json | 21 + .../loot_tables/blocks/invar_button.json | 21 + .../loot_tables/blocks/invar_door.json | 28 + .../loot_tables/blocks/invar_lever.json | 21 + .../loot_tables/blocks/invar_plate.json | 21 + .../blocks/invar_pressure_plate.json | 21 + .../loot_tables/blocks/invar_slab.json | 35 + .../loot_tables/blocks/invar_stairs.json | 21 + .../loot_tables/blocks/invar_trapdoor.json | 21 + .../loot_tables/blocks/invar_wall.json | 21 + .../loot_tables/blocks/iron_button.json | 21 + .../loot_tables/blocks/iron_lever.json | 21 + .../loot_tables/blocks/iron_plate.json | 21 + .../loot_tables/blocks/iron_slab.json | 35 + .../loot_tables/blocks/iron_stairs.json | 21 + .../loot_tables/blocks/iron_wall.json | 21 + .../loot_tables/blocks/lead_bars.json | 21 + .../loot_tables/blocks/lead_block.json | 21 + .../loot_tables/blocks/lead_button.json | 21 + .../loot_tables/blocks/lead_door.json | 28 + .../loot_tables/blocks/lead_lever.json | 21 + .../loot_tables/blocks/lead_ore.json | 21 + .../loot_tables/blocks/lead_plate.json | 21 + .../blocks/lead_pressure_plate.json | 21 + .../loot_tables/blocks/lead_slab.json | 35 + .../loot_tables/blocks/lead_stairs.json | 21 + .../loot_tables/blocks/lead_trapdoor.json | 21 + .../loot_tables/blocks/lead_wall.json | 21 + .../loot_tables/blocks/mercury_ore.json | 21 + .../loot_tables/blocks/mithril_bars.json | 21 + .../loot_tables/blocks/mithril_block.json | 21 + .../loot_tables/blocks/mithril_button.json | 21 + .../loot_tables/blocks/mithril_door.json | 28 + .../loot_tables/blocks/mithril_lever.json | 21 + .../loot_tables/blocks/mithril_plate.json | 21 + .../blocks/mithril_pressure_plate.json | 21 + .../loot_tables/blocks/mithril_slab.json | 35 + .../loot_tables/blocks/mithril_stairs.json | 21 + .../loot_tables/blocks/mithril_trapdoor.json | 21 + .../loot_tables/blocks/mithril_wall.json | 21 + .../loot_tables/blocks/nickel_bars.json | 21 + .../loot_tables/blocks/nickel_block.json | 21 + .../loot_tables/blocks/nickel_button.json | 21 + .../loot_tables/blocks/nickel_door.json | 28 + .../loot_tables/blocks/nickel_lever.json | 21 + .../loot_tables/blocks/nickel_ore.json | 21 + .../loot_tables/blocks/nickel_plate.json | 21 + .../blocks/nickel_pressure_plate.json | 21 + .../loot_tables/blocks/nickel_slab.json | 35 + .../loot_tables/blocks/nickel_stairs.json | 21 + .../loot_tables/blocks/nickel_trapdoor.json | 21 + .../loot_tables/blocks/nickel_wall.json | 21 + .../loot_tables/blocks/obsidian_bars.json | 21 + .../loot_tables/blocks/obsidian_button.json | 21 + .../loot_tables/blocks/obsidian_door.json | 28 + .../loot_tables/blocks/obsidian_lever.json | 21 + .../blocks/obsidian_pressure_plate.json | 21 + .../loot_tables/blocks/obsidian_slab.json | 35 + .../loot_tables/blocks/obsidian_stairs.json | 21 + .../loot_tables/blocks/obsidian_trapdoor.json | 21 + .../loot_tables/blocks/obsidian_wall.json | 21 + .../loot_tables/blocks/pewter_bars.json | 21 + .../loot_tables/blocks/pewter_block.json | 21 + .../loot_tables/blocks/pewter_button.json | 21 + .../loot_tables/blocks/pewter_door.json | 28 + .../loot_tables/blocks/pewter_lever.json | 21 + .../loot_tables/blocks/pewter_plate.json | 21 + .../blocks/pewter_pressure_plate.json | 21 + .../loot_tables/blocks/pewter_slab.json | 35 + .../loot_tables/blocks/pewter_stairs.json | 21 + .../loot_tables/blocks/pewter_trapdoor.json | 21 + .../loot_tables/blocks/pewter_wall.json | 21 + .../loot_tables/blocks/platinum_bars.json | 21 + .../loot_tables/blocks/platinum_block.json | 21 + .../loot_tables/blocks/platinum_button.json | 21 + .../loot_tables/blocks/platinum_door.json | 28 + .../loot_tables/blocks/platinum_lever.json | 21 + .../loot_tables/blocks/platinum_ore.json | 21 + .../loot_tables/blocks/platinum_plate.json | 21 + .../blocks/platinum_pressure_plate.json | 21 + .../loot_tables/blocks/platinum_slab.json | 35 + .../loot_tables/blocks/platinum_stairs.json | 21 + .../loot_tables/blocks/platinum_trapdoor.json | 21 + .../loot_tables/blocks/platinum_wall.json | 21 + .../loot_tables/blocks/quartz_bars.json | 21 + .../loot_tables/blocks/quartz_button.json | 21 + .../loot_tables/blocks/quartz_door.json | 28 + .../loot_tables/blocks/quartz_lever.json | 21 + .../blocks/quartz_pressure_plate.json | 21 + .../loot_tables/blocks/quartz_trapdoor.json | 21 + .../loot_tables/blocks/quartz_wall.json | 21 + .../loot_tables/blocks/silver_bars.json | 21 + .../loot_tables/blocks/silver_block.json | 21 + .../loot_tables/blocks/silver_button.json | 21 + .../loot_tables/blocks/silver_door.json | 28 + .../loot_tables/blocks/silver_lever.json | 21 + .../loot_tables/blocks/silver_ore.json | 21 + .../loot_tables/blocks/silver_plate.json | 21 + .../blocks/silver_pressure_plate.json | 21 + .../loot_tables/blocks/silver_slab.json | 35 + .../loot_tables/blocks/silver_stairs.json | 21 + .../loot_tables/blocks/silver_trapdoor.json | 21 + .../loot_tables/blocks/silver_wall.json | 21 + .../loot_tables/blocks/starsteel_bars.json | 21 + .../loot_tables/blocks/starsteel_block.json | 21 + .../loot_tables/blocks/starsteel_button.json | 21 + .../loot_tables/blocks/starsteel_door.json | 28 + .../loot_tables/blocks/starsteel_lever.json | 21 + .../loot_tables/blocks/starsteel_ore.json | 21 + .../loot_tables/blocks/starsteel_plate.json | 21 + .../blocks/starsteel_pressure_plate.json | 21 + .../loot_tables/blocks/starsteel_slab.json | 35 + .../loot_tables/blocks/starsteel_stairs.json | 21 + .../blocks/starsteel_trapdoor.json | 21 + .../loot_tables/blocks/starsteel_wall.json | 21 + .../loot_tables/blocks/steel_anvil.json | 48 + .../loot_tables/blocks/steel_bars.json | 21 + .../loot_tables/blocks/steel_block.json | 21 + .../loot_tables/blocks/steel_button.json | 21 + .../loot_tables/blocks/steel_door.json | 28 + .../loot_tables/blocks/steel_lever.json | 21 + .../loot_tables/blocks/steel_plate.json | 21 + .../blocks/steel_pressure_plate.json | 21 + .../loot_tables/blocks/steel_slab.json | 35 + .../loot_tables/blocks/steel_stairs.json | 21 + .../loot_tables/blocks/steel_trapdoor.json | 21 + .../loot_tables/blocks/steel_wall.json | 21 + .../loot_tables/blocks/stone_anvil.json | 48 + .../loot_tables/blocks/tin_bars.json | 21 + .../loot_tables/blocks/tin_block.json | 21 + .../loot_tables/blocks/tin_button.json | 21 + .../loot_tables/blocks/tin_door.json | 28 + .../loot_tables/blocks/tin_lever.json | 21 + .../loot_tables/blocks/tin_ore.json | 21 + .../loot_tables/blocks/tin_plate.json | 21 + .../blocks/tin_pressure_plate.json | 21 + .../loot_tables/blocks/tin_slab.json | 35 + .../loot_tables/blocks/tin_stairs.json | 21 + .../loot_tables/blocks/tin_trapdoor.json | 21 + .../loot_tables/blocks/tin_wall.json | 21 + .../loot_tables/blocks/zinc_bars.json | 21 + .../loot_tables/blocks/zinc_block.json | 21 + .../loot_tables/blocks/zinc_button.json | 21 + .../loot_tables/blocks/zinc_door.json | 28 + .../loot_tables/blocks/zinc_lever.json | 21 + .../loot_tables/blocks/zinc_ore.json | 21 + .../loot_tables/blocks/zinc_plate.json | 21 + .../blocks/zinc_pressure_plate.json | 21 + .../loot_tables/blocks/zinc_slab.json | 35 + .../loot_tables/blocks/zinc_stairs.json | 21 + .../loot_tables/blocks/zinc_trapdoor.json | 21 + .../loot_tables/blocks/zinc_wall.json | 21 + .../data/basemetals/registry_manifest.json | 2 +- .../testmod/BaseMetalsRuntimeProbe.java | 253 ++-- .../basemetals/testmod/ContentModeChecks.java | 64 +- .../testmod/GameplayRegressionChecks.java | 240 +++- .../basemetals/testmod/Native114Checks.java | 124 ++ .../testmod/OrePlacementChecks.java | 86 ++ .../resources/META-INF/mods.toml | 10 +- src/integrationTest/resources/pack.mcmeta | 2 +- .../upgrade/advancement_progress_113.json | 24 + .../zone/moddev/mc/basemetals/BaseMetals.java | 1 - .../mc/basemetals/BaseMetalsAdvancements.java | 32 +- .../mc/basemetals/BaseMetalsEvents.java | 120 +- .../moddev/mc/basemetals/MissingMappings.java | 27 +- .../zone/moddev/mc/basemetals/ModTabs.java | 4 +- .../client/BaseMetalsConfigScreen.java | 84 +- .../client/ClientMoltenMetalRenderer.java | 54 - .../mc/basemetals/client/ClientSetup.java | 5 +- .../client/MaterialProjectileRenderer.java | 8 +- .../client/ModsListConfigButton.java | 51 - .../basemetals/content/BaseMetalAmmoItem.java | 10 +- .../content/BaseMetalAnvilBlock.java | 71 +- .../content/BaseMetalBucketItem.java | 6 +- .../content/CompatibilityDoubleSlabBlock.java | 10 +- .../basemetals/content/CrackhammerItem.java | 56 +- .../mc/basemetals/content/FluidContent.java | 8 +- .../content/HumanDetectorBlock.java | 10 +- .../mc/basemetals/content/MaterialArmor.java | 8 +- .../mc/basemetals/content/MaterialItems.java | 122 +- .../mc/basemetals/content/ModContent.java | 133 ++- .../mc/basemetals/content/MoltenFluid.java | 40 +- .../basemetals/content/MoltenMetalBlock.java | 18 +- .../mc/basemetals/content/PlateBlock.java | 21 +- .../mc/basemetals/content/ScytheItem.java | 18 +- .../basemetals/entity/MaterialProjectile.java | 23 +- .../mc/basemetals/entity/ModEntities.java | 5 +- .../loot/ContentModeLootCondition.java | 12 +- .../moddev/mc/basemetals/loot/ModLoot.java | 13 +- .../material/MaterialDefinition.java | 4 +- .../migration/LegacyWorldDataHook.java | 284 +++-- .../network/ContentModeNetwork.java | 76 +- .../recipe/ContentCraftingRecipe.java | 46 +- .../mc/basemetals/recipe/CrushingRecipe.java | 37 +- .../recipe/LegacySmeltingRecipe.java | 24 +- .../basemetals/recipe/PlateRepairRecipe.java | 28 +- .../mc/basemetals/trade/BaseMetalsTrades.java | 89 +- src/main/resources/META-INF/coremods.json | 2 +- src/main/resources/META-INF/mods.toml | 10 +- .../coremods/basemetals_113_compatibility.js | 199 --- .../coremods/basemetals_114_compatibility.js | 113 ++ .../mc/basemetals/MechanicsContractTest.java | 14 + .../data/ResourceIntegrityTest.java | 11 +- .../material/MaterialCatalogueTest.java | 14 +- .../migration/LegacyWorldSourceTest.java | 132 +- ...resources.js => generate_114_resources.js} | 52 +- tools/test_content_modes.js | 16 +- tools/validation/compare_world_terrain.py | 108 ++ tools/validation/legacy-capture/README.md | 26 +- tools/validation/legacy-capture/build.gradle | 33 +- .../mcmoddev/validation/LegacyCapture.java | 20 +- tools/validation/test_legacy_worlds.js | 28 +- 407 files changed, 10897 insertions(+), 2469 deletions(-) create mode 100644 gradle/README.md create mode 100644 gradle/ide/eclipse.gradle create mode 100644 gradle/ide/intellij.gradle create mode 100644 gradle/release/artifacts.gradle create mode 100644 gradle/verification/development.gradle create mode 100644 gradle/verification/generated-data.gradle create mode 100644 gradle/verification/packaged-forge.gradle create mode 100644 src/clientIntegrationTest/java/net/minecraftforge/fml/client/gui/ModSelection.java create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/adamantine_anvil.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/adamantine_bars.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/adamantine_block.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/adamantine_button.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/adamantine_door.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/adamantine_lever.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/adamantine_ore.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/adamantine_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/adamantine_pressure_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/adamantine_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/adamantine_stairs.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/adamantine_trapdoor.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/adamantine_wall.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/antimony_bars.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/antimony_block.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/antimony_button.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/antimony_door.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/antimony_lever.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/antimony_ore.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/antimony_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/antimony_pressure_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/antimony_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/antimony_stairs.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/antimony_trapdoor.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/antimony_wall.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/aquarium_bars.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/aquarium_block.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/aquarium_button.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/aquarium_door.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/aquarium_lever.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/aquarium_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/aquarium_pressure_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/aquarium_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/aquarium_stairs.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/aquarium_trapdoor.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/aquarium_wall.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/bismuth_bars.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/bismuth_block.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/bismuth_button.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/bismuth_door.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/bismuth_lever.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/bismuth_ore.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/bismuth_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/bismuth_pressure_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/bismuth_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/bismuth_stairs.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/bismuth_trapdoor.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/bismuth_wall.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/brass_bars.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/brass_block.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/brass_button.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/brass_door.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/brass_lever.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/brass_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/brass_pressure_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/brass_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/brass_stairs.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/brass_trapdoor.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/brass_wall.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/bronze_bars.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/bronze_block.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/bronze_button.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/bronze_door.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/bronze_lever.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/bronze_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/bronze_pressure_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/bronze_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/bronze_stairs.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/bronze_trapdoor.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/bronze_wall.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/charcoal_block.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/coldiron_bars.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/coldiron_block.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/coldiron_button.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/coldiron_door.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/coldiron_lever.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/coldiron_ore.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/coldiron_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/coldiron_pressure_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/coldiron_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/coldiron_stairs.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/coldiron_trapdoor.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/coldiron_wall.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/copper_bars.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/copper_block.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/copper_button.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/copper_door.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/copper_lever.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/copper_ore.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/copper_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/copper_pressure_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/copper_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/copper_stairs.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/copper_trapdoor.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/copper_wall.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/cupronickel_bars.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/cupronickel_block.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/cupronickel_button.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/cupronickel_door.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/cupronickel_lever.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/cupronickel_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/cupronickel_pressure_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/cupronickel_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/cupronickel_stairs.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/cupronickel_trapdoor.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/cupronickel_wall.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/diamond_bars.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/diamond_button.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/diamond_door.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/diamond_lever.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/diamond_pressure_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/diamond_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/diamond_stairs.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/diamond_trapdoor.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/diamond_wall.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/double_adamantine_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/double_antimony_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/double_aquarium_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/double_bismuth_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/double_brass_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/double_bronze_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/double_coldiron_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/double_copper_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/double_cupronickel_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/double_diamond_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/double_electrum_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/double_emerald_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/double_gold_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/double_invar_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/double_iron_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/double_lead_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/double_mithril_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/double_nickel_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/double_obsidian_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/double_pewter_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/double_platinum_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/double_silver_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/double_starsteel_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/double_steel_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/double_tin_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/double_zinc_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/electrum_bars.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/electrum_block.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/electrum_button.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/electrum_door.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/electrum_lever.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/electrum_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/electrum_pressure_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/electrum_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/electrum_stairs.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/electrum_trapdoor.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/electrum_wall.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/emerald_bars.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/emerald_button.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/emerald_door.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/emerald_lever.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/emerald_pressure_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/emerald_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/emerald_stairs.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/emerald_trapdoor.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/emerald_wall.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/gold_bars.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/gold_button.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/gold_door.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/gold_lever.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/gold_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/gold_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/gold_stairs.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/gold_trapdoor.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/gold_wall.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/human_detector.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/invar_bars.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/invar_block.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/invar_button.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/invar_door.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/invar_lever.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/invar_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/invar_pressure_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/invar_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/invar_stairs.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/invar_trapdoor.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/invar_wall.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/iron_button.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/iron_lever.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/iron_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/iron_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/iron_stairs.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/iron_wall.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/lead_bars.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/lead_block.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/lead_button.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/lead_door.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/lead_lever.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/lead_ore.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/lead_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/lead_pressure_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/lead_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/lead_stairs.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/lead_trapdoor.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/lead_wall.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/mercury_ore.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/mithril_bars.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/mithril_block.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/mithril_button.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/mithril_door.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/mithril_lever.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/mithril_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/mithril_pressure_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/mithril_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/mithril_stairs.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/mithril_trapdoor.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/mithril_wall.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/nickel_bars.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/nickel_block.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/nickel_button.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/nickel_door.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/nickel_lever.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/nickel_ore.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/nickel_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/nickel_pressure_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/nickel_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/nickel_stairs.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/nickel_trapdoor.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/nickel_wall.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/obsidian_bars.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/obsidian_button.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/obsidian_door.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/obsidian_lever.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/obsidian_pressure_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/obsidian_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/obsidian_stairs.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/obsidian_trapdoor.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/obsidian_wall.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/pewter_bars.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/pewter_block.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/pewter_button.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/pewter_door.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/pewter_lever.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/pewter_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/pewter_pressure_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/pewter_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/pewter_stairs.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/pewter_trapdoor.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/pewter_wall.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/platinum_bars.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/platinum_block.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/platinum_button.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/platinum_door.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/platinum_lever.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/platinum_ore.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/platinum_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/platinum_pressure_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/platinum_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/platinum_stairs.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/platinum_trapdoor.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/platinum_wall.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/quartz_bars.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/quartz_button.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/quartz_door.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/quartz_lever.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/quartz_pressure_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/quartz_trapdoor.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/quartz_wall.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/silver_bars.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/silver_block.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/silver_button.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/silver_door.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/silver_lever.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/silver_ore.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/silver_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/silver_pressure_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/silver_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/silver_stairs.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/silver_trapdoor.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/silver_wall.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/starsteel_bars.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/starsteel_block.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/starsteel_button.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/starsteel_door.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/starsteel_lever.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/starsteel_ore.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/starsteel_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/starsteel_pressure_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/starsteel_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/starsteel_stairs.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/starsteel_trapdoor.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/starsteel_wall.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/steel_anvil.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/steel_bars.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/steel_block.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/steel_button.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/steel_door.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/steel_lever.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/steel_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/steel_pressure_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/steel_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/steel_stairs.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/steel_trapdoor.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/steel_wall.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/stone_anvil.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/tin_bars.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/tin_block.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/tin_button.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/tin_door.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/tin_lever.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/tin_ore.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/tin_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/tin_pressure_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/tin_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/tin_stairs.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/tin_trapdoor.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/tin_wall.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/zinc_bars.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/zinc_block.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/zinc_button.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/zinc_door.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/zinc_lever.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/zinc_ore.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/zinc_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/zinc_pressure_plate.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/zinc_slab.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/zinc_stairs.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/zinc_trapdoor.json create mode 100644 src/generated/resources/data/basemetals/loot_tables/blocks/zinc_wall.json create mode 100644 src/integrationTest/java/zone/moddev/mc/basemetals/testmod/Native114Checks.java create mode 100644 src/integrationTest/java/zone/moddev/mc/basemetals/testmod/OrePlacementChecks.java create mode 100644 src/integrationTest/resources/upgrade/advancement_progress_113.json delete mode 100644 src/main/java/zone/moddev/mc/basemetals/client/ClientMoltenMetalRenderer.java delete mode 100644 src/main/java/zone/moddev/mc/basemetals/client/ModsListConfigButton.java delete mode 100644 src/main/resources/coremods/basemetals_113_compatibility.js create mode 100644 src/main/resources/coremods/basemetals_114_compatibility.js rename tools/{generate_113_resources.js => generate_114_resources.js} (90%) create mode 100644 tools/validation/compare_world_terrain.py diff --git a/.gitattributes b/.gitattributes index f6833076..9a857ea2 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,9 @@ -# Disable autocrlf on generated files, they always generate with LF -# Add any extra files or paths here to make git stop saying they -# are changed when only line endings change. +# Keep source, scripts and generated text portable between Windows and Linux. +* text=auto eol=lf +*.png binary +*.jar binary +*.zip binary +*.nbt binary src/generated/**/.cache/cache text eol=lf src/generated/**/*.json text eol=lf src/main/resources/assets/basemetals/models/block/*_ore.json text eol=lf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 714ba476..361dcb9d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,19 +1,19 @@ -name: Base Metals 1.13.2 CI +name: Base Metals 1.14.4 CI on: push: branches: - - master-1.13.2 + - master-1.14.4 - 'feature/**' pull_request: branches: - - master-1.13.2 + - master-1.14.4 permissions: contents: read concurrency: - group: basemetals-1.13-${{ github.workflow }}-${{ github.ref }} + group: basemetals-1.14-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: @@ -133,7 +133,7 @@ jobs: clean verifyGeneratedOreModels check build javadoc verifyReleaseArtifacts writeReleaseChecksums verifyEclipseProductionClasspath -PorespawnVerificationRepository="${{ steps.orespawn.outputs.repository }}" - --no-daemon --stacktrace --max-workers=2 + --no-daemon --no-build-cache --stacktrace --max-workers=2 -Dorg.gradle.java.installations.paths="$JAVA_HOME,$JAVA_HOME_8_X64,$JAVA_HOME_25_X64" -Dorg.gradle.java.installations.auto-detect=false -Dorg.gradle.java.installations.auto-download=false @@ -141,9 +141,9 @@ jobs: ./gradlew "${gradle_args[@]}" first="$RUNNER_TEMP/basemetals-first" mkdir -p "$first" - cp build/libs/BaseMetals-3.0.1.113021.jar "$first/" - cp build/libs/BaseMetals-3.0.1.113021-sources.jar "$first/" - cp build/libs/BaseMetals-3.0.1.113021-javadoc.jar "$first/" + cp build/libs/BaseMetals-3.0.1.114041.jar "$first/" + cp build/libs/BaseMetals-3.0.1.114041-sources.jar "$first/" + cp build/libs/BaseMetals-3.0.1.114041-javadoc.jar "$first/" cp build/release/SHA256SUMS "$first/" - name: Rebuild and prove byte-identical archives @@ -153,15 +153,15 @@ jobs: gradle_args=( clean assemble verifyReleaseArtifacts writeReleaseChecksums -PorespawnVerificationRepository="${{ steps.orespawn.outputs.repository }}" - --no-daemon --stacktrace --max-workers=2 + --no-daemon --no-build-cache --stacktrace --max-workers=2 -Dorg.gradle.java.installations.paths="$JAVA_HOME,$JAVA_HOME_8_X64,$JAVA_HOME_25_X64" -Dorg.gradle.java.installations.auto-detect=false -Dorg.gradle.java.installations.auto-download=false ) ./gradlew "${gradle_args[@]}" - cmp "$RUNNER_TEMP/basemetals-first/BaseMetals-3.0.1.113021.jar" build/libs/BaseMetals-3.0.1.113021.jar - cmp "$RUNNER_TEMP/basemetals-first/BaseMetals-3.0.1.113021-sources.jar" build/libs/BaseMetals-3.0.1.113021-sources.jar - cmp "$RUNNER_TEMP/basemetals-first/BaseMetals-3.0.1.113021-javadoc.jar" build/libs/BaseMetals-3.0.1.113021-javadoc.jar + cmp "$RUNNER_TEMP/basemetals-first/BaseMetals-3.0.1.114041.jar" build/libs/BaseMetals-3.0.1.114041.jar + cmp "$RUNNER_TEMP/basemetals-first/BaseMetals-3.0.1.114041-sources.jar" build/libs/BaseMetals-3.0.1.114041-sources.jar + cmp "$RUNNER_TEMP/basemetals-first/BaseMetals-3.0.1.114041-javadoc.jar" build/libs/BaseMetals-3.0.1.114041-javadoc.jar cmp "$RUNNER_TEMP/basemetals-first/SHA256SUMS" build/release/SHA256SUMS - name: Run exact-OreSpawn dedicated-server probe @@ -181,13 +181,13 @@ jobs: - name: Upload audited release candidate uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: BaseMetals-1.13.2-${{ github.sha }} + name: BaseMetals-1.14.4-${{ github.sha }} if-no-files-found: error retention-days: 30 path: | - build/libs/BaseMetals-3.0.1.113021.jar - build/libs/BaseMetals-3.0.1.113021-sources.jar - build/libs/BaseMetals-3.0.1.113021-javadoc.jar + build/libs/BaseMetals-3.0.1.114041.jar + build/libs/BaseMetals-3.0.1.114041-sources.jar + build/libs/BaseMetals-3.0.1.114041-javadoc.jar build/release/SHA256SUMS CHANGELOG.txt @@ -195,7 +195,7 @@ jobs: if: failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: BaseMetals-1.13.2-diagnostics-${{ github.sha }} + name: BaseMetals-1.14.4-diagnostics-${{ github.sha }} if-no-files-found: ignore retention-days: 14 path: | diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index da038548..c65022fe 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -3,11 +3,11 @@ name: CodeQL on: push: branches: - - master-1.13.2 + - master-1.14.4 - 'feature/**' pull_request: branches: - - master-1.13.2 + - master-1.14.4 schedule: - cron: '37 7 * * 4' diff --git a/.github/workflows/validate-gradle-build.yml b/.github/workflows/validate-gradle-build.yml index d4d55ba3..c471b098 100644 --- a/.github/workflows/validate-gradle-build.yml +++ b/.github/workflows/validate-gradle-build.yml @@ -3,11 +3,11 @@ name: Validate Gradle Wrapper on: push: branches: - - master-1.13.2 + - master-1.14.4 - 'feature/**' pull_request: branches: - - master-1.13.2 + - master-1.14.4 permissions: contents: read diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 2cfc395a..30584064 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,28 @@ +Base Metals 3.0.1.114041 + +- Bring the finished 1.13.2 port to Minecraft 1.14.4 and Forge 28.2.26. +- Keep existing blocks, items, recipes, save identities, High/Low Fantasy + settings and material-based recipe discovery. +- Fix a crossbow crash when firing without bolts in Creative mode. Crossbows + use an iron bolt for the free shot instead of Minecraft's arrow fallback. +- Use native 1.14 configuration widgets and Forge's Mods-list Config button. + OreSpawn's cog opens the same screen. +- Use Forge's fluid textures and colours without the old renderer patch. +- Move smith trades to Armorer, Toolsmith and Weaponsmith, with normal + restocking and profession experience. Add the old blacksmith loot to each + profession's village chest. +- Generate block loot tables for 1.14, including doors, slabs and worn anvils. +- Keep legacy-world migration and custom-anvil handling, while removing the + obsolete Forge 1.13 leaves patch. +- Let 1.13 saves load without Forge stopping on the retired vanilla villager + registry. Villagers and their existing trades remain for Minecraft to convert. +- Preserve the old vanilla sign and dye registry names when upgrading a world. +- Keep Kiri's ore overlays, vanilla host textures and automatic model updates. +- Build against the published OreSpawn 4.1.0.114041 release. The eleven ore + rules and their defaults are unchanged. +- Split the longer build checks into focused scripts and retain ForgeGradle 7, + reproducible archives, checked dependencies and isolated test launches. + Base Metals 3.0.1.113021 - Add the configuration screen to Forge's Mods list and OreSpawn's cog, with diff --git a/LICENSE-ART.txt b/LICENSE-ART.txt index f069dff1..a7172e70 100644 --- a/LICENSE-ART.txt +++ b/LICENSE-ART.txt @@ -5,7 +5,7 @@ Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public L By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. -Section 1 – Definitions. +Section 1 – Definitions. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. @@ -22,7 +22,7 @@ Section 1 Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. -Section 2 – Scope. +Section 2 – Scope. License grant. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: @@ -32,8 +32,8 @@ Section 2 Term. The term of this Public License is specified in Section 6(a). Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material. Downstream recipients. - Offer from the Licensor – Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. - Additional offer from the Licensor – Adapted Material. Every recipient of Adapted Material from You automatically receives an offer from the Licensor to exercise the Licensed Rights in the Adapted Material under the conditions of the Adapter’s License You apply. + Offer from the Licensor – Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. + Additional offer from the Licensor – Adapted Material. Every recipient of Adapted Material from You automatically receives an offer from the Licensor to exercise the Licensed Rights in the Adapted Material under the conditions of the Adapter’s License You apply. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). @@ -42,7 +42,7 @@ Section 2 Patent and trademark rights are not licensed under this Public License. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties, including when the Licensed Material is used other than for NonCommercial purposes. -Section 3 – License Conditions. +Section 3 – License Conditions. Your exercise of the Licensed Rights is expressly made subject to the following conditions. @@ -62,11 +62,11 @@ Your exercise of the Licensed Rights is expressly made subject to the following ShareAlike. In addition to the conditions in Section 3(a), if You Share Adapted Material You produce, the following conditions also apply. - The Adapter’s License You apply must be a Creative Commons license with the same License Elements, this version or later, or a BY-NC-SA Compatible License. + The Adapter’s License You apply must be a Creative Commons license with the same License Elements, this version or later, or a BY-NC-SA Compatible License. You must include the text of, or the URI or hyperlink to, the Adapter's License You apply. You may satisfy this condition in any reasonable manner based on the medium, means, and context in which You Share Adapted Material. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, Adapted Material that restrict exercise of the rights granted under the Adapter's License You apply. -Section 4 – Sui Generis Database Rights. +Section 4 – Sui Generis Database Rights. Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: @@ -76,14 +76,14 @@ Where the Licensed Rights include Sui Generis Database Rights that apply to Your For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. -Section 5 – Disclaimer of Warranties and Limitation of Liability. +Section 5 – Disclaimer of Warranties and Limitation of Liability. Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You. To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. -Section 6 – Term and Termination. +Section 6 – Term and Termination. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. @@ -94,12 +94,12 @@ Section 6 For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. -Section 7 – Other Terms and Conditions. +Section 7 – Other Terms and Conditions. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. -Section 8 – Interpretation. +Section 8 – Interpretation. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. diff --git a/README.md b/README.md index 748f93eb..eb9a2ae6 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![Discord](https://img.shields.io/badge/Discord-MMD-green.svg?style=flat&logo=Discord)](https://discord.moddev.zone) [![CurseForge downloads](https://cf.way2muchnoise.eu/full_base-metals_downloads.svg)](https://www.curseforge.com/minecraft/mc-mods/base-metals) [![Supported Minecraft versions](https://cf.way2muchnoise.eu/versions/Minecraft_base-metals_all.svg)](https://www.curseforge.com/minecraft/mc-mods/base-metals) -[![Build, test, and audit](https://github.com/MinecraftModDevelopmentMods/BaseMetals/actions/workflows/ci.yml/badge.svg?branch=master-1.13.2)](https://github.com/MinecraftModDevelopmentMods/BaseMetals/actions/workflows/ci.yml?query=branch%3Amaster-1.13.2) +[![Build, test, and audit](https://github.com/MinecraftModDevelopmentMods/BaseMetals/actions/workflows/ci.yml/badge.svg?branch=master-1.14.4)](https://github.com/MinecraftModDevelopmentMods/BaseMetals/actions/workflows/ci.yml?query=branch%3Amaster-1.14.4) # Base Metals @@ -12,20 +12,25 @@ shields, bows, crossbows, arrows, and bolts. The mod also includes Crack Hammers, scythes, metal anvils, the human detector, villager trades, advancements, and extra loot to find in chests. -This is the Minecraft 1.13.2 version, `3.0.1.113021`, adapted from the 1.18.2 -port. It keeps the historical `basemetals` block and item IDs, but no longer +This is the Minecraft 1.14.4 version, `3.0.1.114041`, continued from the finished +1.13.2 port. It keeps the historical `basemetals` block and item IDs, but no longer needs MMDLib or Additional Loot Tables. OreSpawn places the ores, and Base Metals supplies the metals and their recipes. +Ores use vanilla stone, netherrack and end-stone textures beneath Kiri's ore +overlays, so resource packs can change their surroundings without making the +ores look out of place. Smith trades and village chest loot use the new +Armorer, Weaponsmith and Toolsmith professions introduced in Minecraft 1.14. + ## Requirements -- Minecraft `1.13.2` -- Forge `25.0.223` +- Minecraft `1.14.4` +- Forge `28.2.26` - Java 8 -- OreSpawn `[4.1.0.113021,5.0.0)` on both client and server +- OreSpawn `[4.1.0.114041,5.0.0)` on both client and server -Put `BaseMetals-3.0.1.113021.jar` and OreSpawn `4.1.0.113021` (or a later -compatible OreSpawn 4 build for 1.13.2) in the `mods` directory. MMDLib and +Put `BaseMetals-3.0.1.114041.jar` and OreSpawn `4.1.0.114041` (or a later +compatible OreSpawn 4 build for 1.14.4) in the `mods` directory. MMDLib and Additional Loot Tables are not dependencies. Mineralogy is optional. ## Materials and gameplay @@ -59,6 +64,8 @@ Notable mechanics include: - Base Metals bows fire material arrows. Its legacy-style crossbows are draw-and-release weapons which fire material bolts. Both the launcher and ammunition materials contribute to damage; Power is added afterward. + Crossbows do not use arrows. In Creative mode, firing without bolts uses a + free iron bolt. Arrows and bolts share the vanilla arrow projectile texture. - Configurable armour and melee effects give Adamantine, Aquarium, Cold Iron, Lead, Mithril, and Starsteel their distinctive behaviour. Held Starsteel equipment repairs by one durability every 10 seconds; its armour does @@ -115,13 +122,13 @@ items in containers are protected too. Always perform the first upgrade on a copy of the world and keep the untouched original. Read [Migration](docs/MIGRATION.md) before opening an old save. A -world already upgraded to Minecraft 1.18 must not be opened in this older -version. +world already upgraded to Minecraft 1.15 or later must not be opened in this +older version. Worlds from the Base Metals 1.13.2 port can be upgraded normally. ## Compatibility Forge item, block, and fluid tags are Base Metals' public compatibility API. -The 1.13.2 line deliberately contains no version-specific Mekanism, Thermal, +The 1.14.4 line deliberately contains no version-specific Mekanism, Thermal, Tinkers' Construct, Ender IO, IC2, or Thaumcraft plugin code. Compatible mods can consume the common tags without linking to Base Metals internals. See [Compatibility](docs/COMPATIBILITY.md) and [Supported versions](docs/VERSIONS.md). @@ -134,6 +141,9 @@ the mod compiles and runs on Java 8. Development builds accept newer Java 8 updates; release auditing requires Temurin `8.0.502+7` for reproducibility. Node.js is also needed to generate and check the resource files. +The build scripts are divided by purpose; [Build scripts](gradle/README.md) +explains where to find the IDE, resource-generation and release checks. + For a normal development build and client launch, use: ```text @@ -164,7 +174,7 @@ Generate and verify Eclipse launches with: ``` Release candidates can also be exercised as packaged mods in prepared official -Forge 25 client and server runtimes with `packagedRuntimeIntegrationTest`; pass +Forge 28 client and server runtimes with `packagedRuntimeIntegrationTest`; pass their directories through the `packagedForgeClientRuntime` and `packagedForgeServerRuntime` Gradle properties. These probes load the exact release JAR and OreSpawn dependency, create real worlds, validate client models @@ -180,12 +190,12 @@ builds, CodeQL, wrapper validation, and Eclipse classpath isolation. Test probes and historical fixtures are excluded from the published JAR. Development and CI use the published -[OreSpawn 4.1.0.113021 release](https://www.curseforge.com/minecraft/mc-mods/mmd-orespawn/files/9035885) +[OreSpawn 4.1.0.114041 release](https://www.curseforge.com/minecraft/mc-mods/mmd-orespawn/files/9041431) from CurseMaven. The build checks its exact file ID and SHA-256 before release auditing and packaged tests; no local OreSpawn build is needed. Release artifacts use Maven coordinate -`zone.moddev.mc.basemetals:BaseMetals:3.0.1.113021`. +`zone.moddev.mc.basemetals:BaseMetals:3.0.1.114041`. Report defects through the [Base Metals issue tracker](https://github.com/MinecraftModDevelopmentMods/BaseMetals/issues). diff --git a/build.gradle b/build.gradle index 3da9c907..f16d5bc9 100644 --- a/build.gradle +++ b/build.gradle @@ -3,6 +3,7 @@ import groovy.xml.XmlSlurper import java.security.MessageDigest import java.util.jar.Manifest import java.util.zip.ZipFile +import org.apache.tools.ant.filters.FixCrLfFilter plugins { id 'java' @@ -70,6 +71,12 @@ tasks.withType(Javadoc).configureEach { tasks.withType(AbstractArchiveTask).configureEach { preserveFileTimestamps = false reproducibleFileOrder = true + filesMatching(['**/*.java', '**/*.js', '**/*.json', '**/*.toml', '**/*.mcmeta', + '**/*.md', '**/*.txt', '**/*.html', '**/*.css', '**/*.properties', + '**/element-list', '**/package-list']) { + filter(FixCrLfFilter, eol: FixCrLfFilter.CrLf.newInstance('lf'), + eof: FixCrLfFilter.AddAsisRemove.newInstance('asis')) + } } sourceSets { @@ -105,7 +112,7 @@ def mainModClasses = "${mod_id}%%${forgeMainOutputPath}${File.pathSeparator}${mo def forgeProbeOutputPath = integrationTestModClassesDirectory.get().asFile.absolutePath def probeModClasses = mainModClasses + "${File.pathSeparator}basemetalsprobe%%${forgeProbeOutputPath}" + "${File.pathSeparator}basemetalsprobe%%${forgeProbeOutputPath}" -def forge25RunTargets = [client: 'fmluserdevclient', server: 'fmluserdevserver', +def forge28RunTargets = [client: 'fmluserdevclient', server: 'fmluserdevserver', data: 'fmluserdevdata', runtimeProbeServer: 'fmluserdevserver', legacyUpgradeServer: 'fmluserdevserver'] minecraft { @@ -118,7 +125,7 @@ minecraft { systemProperty 'forge.logging.markers', 'REGISTRIES' systemProperty 'forge.logging.console.level', 'info' mods { basemetals { source sourceSets.main } } - environment 'target', forge25RunTargets[name] + environment 'target', forge28RunTargets[name] environment 'MCP_MAPPINGS', "${mapping_channel}_${mapping_version}" environment 'MCP_VERSION', mcp_version environment 'FORGE_VERSION', forge_version @@ -164,7 +171,7 @@ def prepareIntegrationTestModClasses = tasks.register('prepareIntegrationTestMod } tasks.withType(JavaExec).configureEach { JavaExec runTask -> - def runName = forge25RunTargets.keySet().find { runTask.name == 'run' + it.capitalize() } + def runName = forge28RunTargets.keySet().find { runTask.name == 'run' + it.capitalize() } if (runName != null) { runTask.dependsOn prepareForgeRunModClasses if (runTask.name in ['runRuntimeProbeServer', 'runLegacyUpgradeServer']) { @@ -180,152 +187,6 @@ tasks.withType(JavaExec).configureEach { JavaExec runTask -> } } -def integrationTestRunDirectory = layout.buildDirectory.dir('integration-test-run') -def prepareIntegrationTestRun = tasks.register('prepareIntegrationTestRun') { - group = 'verification' - doLast { - File runDirectory = integrationTestRunDirectory.get().asFile.canonicalFile - File buildDirectory = layout.buildDirectory.get().asFile.canonicalFile - if (runDirectory == projectDir.canonicalFile - || !runDirectory.toPath().startsWith(buildDirectory.toPath()) - || runDirectory.name != 'integration-test-run') { - throw new GradleException("Unsafe integration-test run directory: ${runDirectory}") - } - delete runDirectory - if (!runDirectory.mkdirs()) throw new GradleException("Could not create ${runDirectory}") - new File(runDirectory, 'eula.txt').setText('eula=true\n', 'UTF-8') - new File(runDirectory, 'server.properties').setText('''\ -level-name=basemetals-runtime-probe -online-mode=false -spawn-protection=0 -server-port=0 -view-distance=4 -''', 'UTF-8') - if (!new File(runDirectory, 'mods').mkdirs()) { - throw new GradleException("Could not create integration-test mods directory") - } - } -} -def verifyIntegrationTestRun = tasks.register('verifyIntegrationTestRun') { - group = 'verification' - doLast { - File log = new File(integrationTestRunDirectory.get().asFile, 'logs/latest.log') - if (!log.isFile()) throw new GradleException("Runtime-probe log is missing: ${log}") - String text = log.getText('UTF-8') - if (!text.contains('BASEMETALS_RUNTIME_PROBE PASS')) { - throw new GradleException('Base Metals runtime probe did not report PASS') - } - List failures = ['BASEMETALS_RUNTIME_PROBE FAIL', 'Failed to load correctly', - 'This crash report has been saved to', - 'Encountered an unexpected exception'].findAll { text.contains(it) } - if (!failures.isEmpty()) throw new GradleException("Runtime-probe failures: ${failures}") - } -} -tasks.matching { it.name == 'runRuntimeProbeServer' }.configureEach { - dependsOn prepareIntegrationTestRun - finalizedBy verifyIntegrationTestRun -} - -def legacyWorldFixture = providers.gradleProperty('legacyWorldFixture') -def legacyWorldRegions = providers.gradleProperty('legacyWorldRegions') -def legacyUpgradeRunDirectory = layout.buildDirectory.dir('legacy-upgrade-run') -def resolveLegacyWorldFixture = { - if (!legacyWorldFixture.isPresent()) { - throw new GradleException('Pass -PlegacyWorldFixture=') - } - File candidate = file(legacyWorldFixture.get()).canonicalFile - File world = new File(candidate, 'level.dat').isFile() ? candidate - : new File(new File(candidate, 'world'), 'level.dat').isFile() - ? new File(candidate, 'world').canonicalFile : null - if (world == null) { - throw new GradleException("legacyWorldFixture has no level.dat: ${candidate}") - } - world -} -def prepareLegacyUpgradeRun = tasks.register('prepareLegacyUpgradeRun') { - group = 'verification' - dependsOn prepareIntegrationTestModClasses - doLast { - File sourceWorld = resolveLegacyWorldFixture() - File runDirectory = legacyUpgradeRunDirectory.get().asFile.canonicalFile - File buildDirectory = layout.buildDirectory.get().asFile.canonicalFile - if (runDirectory == projectDir.canonicalFile - || !runDirectory.toPath().startsWith(buildDirectory.toPath()) - || runDirectory.name != 'legacy-upgrade-run') { - throw new GradleException("Unsafe legacy-upgrade run directory: ${runDirectory}") - } - if (sourceWorld.toPath().startsWith(buildDirectory.toPath())) { - throw new GradleException("Legacy source must be outside disposable build output: ${sourceWorld}") - } - delete runDirectory - if (!runDirectory.mkdirs()) throw new GradleException("Could not create ${runDirectory}") - Set includedRegions = legacyWorldRegions.isPresent() - ? legacyWorldRegions.get().split(',').collect { it.trim() }.findAll { !it.isEmpty() }.toSet() - : Collections.emptySet() - copy { - from sourceWorld - into new File(runDirectory, 'world') - if (!includedRegions.isEmpty()) { - eachFile { details -> - String relative = details.relativePath.pathString.replace('\\', '/') - if ((relative.startsWith('region/') || relative.contains('/region/')) - && details.name ==~ /r\.-?\d+\.-?\d+\.mc[ar]/ - && !includedRegions.contains(details.name)) { - details.exclude() - } - } - } - } - new File(runDirectory, 'eula.txt').setText('eula=true\n', 'UTF-8') - new File(runDirectory, 'server.properties').setText('''\ -level-name=world -online-mode=false -spawn-protection=0 -server-port=0 -view-distance=4 -max-tick-time=-1 -''', 'UTF-8') - if (!new File(runDirectory, 'mods').mkdirs()) { - throw new GradleException('Could not create legacy-upgrade mods directory') - } - logger.lifecycle('Copied legacy fixture {} to disposable {}{}', sourceWorld, runDirectory, - includedRegions.isEmpty() ? '' : " using regions ${includedRegions.sort()}") - } -} -def verifyLegacyUpgradeRun = tasks.register('verifyLegacyUpgradeRun') { - group = 'verification' - doLast { - File runDirectory = legacyUpgradeRunDirectory.get().asFile - File log = new File(runDirectory, 'logs/latest.log') - if (!log.isFile()) throw new GradleException("Legacy-upgrade log is missing: ${log}") - String text = log.getText('UTF-8') - if (!text.contains('BASEMETALS_LEGACY_UPGRADE_PROBE PASS')) { - throw new GradleException('Legacy-world runtime probe did not report PASS') - } - File fixtureManifest = new File(runDirectory, 'world/legacy_registry_manifest_runtime.json') - if (fixtureManifest.isFile()) { - boolean from110 = new File(runDirectory, - 'world/BASEMETALS_1_10_FIXTURE_COMPLETE.txt').isFile() - String marker = from110 ? 'BASEMETALS_1_10_TO_1_13_UPGRADE PASS' - : 'BASEMETALS_1_12_TO_1_13_UPGRADE PASS' - if (!text.contains(marker)) { - throw new GradleException("Complete ${from110 ? '1.10' : '1.12'} fixture verification did not report PASS") - } - } - if (!text.contains('legacy Base Metals states') || !text.contains('protected')) { - throw new GradleException('Legacy Base Metals registry/chunk preparation was not observed') - } - List failures = ['BASEMETALS_RUNTIME_PROBE FAIL', 'BASEMETALS_LEGACY_UPGRADE_PROBE FAIL', - 'Failed to load correctly', 'This crash report has been saved to', - 'Encountered an unexpected exception'].findAll { text.contains(it) } - if (!failures.isEmpty()) throw new GradleException("Legacy-upgrade failures: ${failures}") - } -} -tasks.matching { it.name == 'runLegacyUpgradeServer' }.configureEach { - dependsOn prepareLegacyUpgradeRun - finalizedBy verifyLegacyUpgradeRun -} - def orespawnModule = "mmd-orespawn-${project.orespawn_curse_project_id}" def orespawnVerificationRepository = project.findProperty('orespawnVerificationRepository') def orespawnCoordinate = "curse.maven:${orespawnModule}:${orespawn_curse_file_id}" @@ -409,895 +270,20 @@ tasks.named('assemble') { dependsOn tasks.named('javadocJar') } -def mainReleaseName = "BaseMetals-${project.version}.jar" -def sourcesReleaseName = "BaseMetals-${project.version}-sources.jar" -def javadocReleaseName = "BaseMetals-${project.version}-javadoc.jar" -def expectedReleaseNames = [mainReleaseName, sourcesReleaseName, javadocReleaseName] -def preparedReleaseDir = project.findProperty('preparedReleaseDir') -def mavenUploadUrl = System.getenv('MAVEN_UPLOAD_URL') ?: 'https://invalid.invalid/missing-maven-upload-url' -publishing { - publications { - mavenJava(MavenPublication) { - groupId = project.group - artifactId = expectedMavenArtifact - version = project.version - if (preparedReleaseDir != null) { - artifact new File(preparedReleaseDir.toString(), mainReleaseName) - artifact(new File(preparedReleaseDir.toString(), sourcesReleaseName)) { classifier = 'sources' } - artifact(new File(preparedReleaseDir.toString(), javadocReleaseName)) { classifier = 'javadoc' } - } else { - artifact releaseJar - artifact tasks.named('sourcesJar') - artifact tasks.named('javadocJar') - } - pom.withXml { - def root = asNode() - root.appendNode('name', 'Base Metals') - root.appendNode('description', - 'Historically inspired and fantasy metals, equipment, processing, and OreSpawn provider data.') - root.appendNode('url', 'https://github.com/MinecraftModDevelopmentMods/BaseMetals') - def license = root.appendNode('licenses').appendNode('license') - license.appendNode('name', 'GNU Lesser General Public License, Version 2.1') - license.appendNode('url', 'https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt') - } - } - } - repositories { - maven { - name = 'release' - url = uri(mavenUploadUrl) - credentials { - username = System.getenv('MAVEN_UPLOAD_USERNAME') ?: '' - password = System.getenv('MAVEN_UPLOAD_PASSWORD') ?: '' - } - } - } -} - -def sha256 = { File artifact -> - MessageDigest.getInstance('SHA-256').digest(artifact.bytes).encodeHex().toString().toUpperCase(Locale.ROOT) -} -def configuredJavaToolchainVersion = project.java_toolchain_version.toString() -def configuredOreSpawnIdentity = [version: project.orespawn_version.toString(), - curseProjectId: project.orespawn_curse_project_id.toString(), - curseFileId: project.orespawn_curse_file_id.toString(), sha256: project.orespawn_sha256.toString()] - -tasks.register('verifyJava8Toolchain') { - group = 'verification' - dependsOn tasks.named('compileJava') - doLast { - def metadata = tasks.named('compileJava', JavaCompile).get().javaCompiler.get().metadata - if (metadata.languageVersion.asInt() != 8) { - throw new GradleException("Minecraft 1.13.2 requires a Java 8 JDK, found " + - "${metadata.vendor} ${metadata.javaRuntimeVersion} at ${metadata.installationPath}") - } - logger.lifecycle("Java 8 development toolchain: ${metadata.vendor} ${metadata.javaRuntimeVersion}") - } -} - -tasks.register('verifyReleaseJava8Toolchain') { - group = 'verification' - description = 'Requires the pinned compiler for reproducible release artifacts.' - dependsOn tasks.named('verifyJava8Toolchain') - doLast { - def metadata = tasks.named('compileJava', JavaCompile).get().javaCompiler.get().metadata - if (configuredJavaToolchainVersion != '8.0.502+7' - || metadata.javaRuntimeVersion != '1.8.0_502-b07' - || !metadata.vendor.toString().toLowerCase(Locale.ROOT).contains('temurin')) { - throw new GradleException("Release auditing requires Eclipse Temurin ${configuredJavaToolchainVersion}; " + - "ordinary build and runClient tasks accept other Java 8 updates. Found " + - "${metadata.vendor} ${metadata.javaRuntimeVersion} at ${metadata.installationPath}") - } - } -} - -tasks.register('verifyDevelopmentLaunches') { - group = 'verification' - description = 'Checks that development runs use the ForgeGradle launcher.' - doLast { - ['runClient', 'runServer', 'runData', 'runRuntimeProbeServer', 'runLegacyUpgradeServer'].each { name -> - def run = tasks.named(name, JavaExec).get() - if (run.mainClass.get() != 'net.minecraftforge.launcher.Main') { - throw new GradleException("${name} bypasses Slime Launcher: ${run.mainClass.get()}") - } - } - } -} - -tasks.register('verifyMavenCoordinates') { - group = 'verification' - dependsOn tasks.named('generatePomFileForMavenJavaPublication') - doLast { - File pomFile = file("${buildDir}/publications/mavenJava/pom-default.xml") - def pom = new XmlSlurper(false, false).parse(pomFile) - def actual = [pom.groupId.text(), pom.artifactId.text(), pom.version.text()] - def expected = [expectedMavenGroup, expectedMavenArtifact, project.version.toString()] - if (project.group.toString() != expectedMavenGroup || actual != expected) { - throw new GradleException("Expected Maven coordinate ${expected.join(':')}, found ${actual.join(':')}") - } - } -} - -tasks.register('verifyReleaseConfiguration') { - group = 'verification' - dependsOn tasks.named('verifyJava8Toolchain') - doLast { - def actual = [modVersion: project.mod_version.toString(), modGroup: project.mod_group.toString(), - minecraftVersion: project.minecraft_version.toString(), forgeVersion: project.forge_version.toString(), - forgeInstallerSha256: project.forge_installer_sha256.toString(), - mappingChannel: project.mapping_channel.toString(), mappingVersion: project.mapping_version.toString(), - mcpVersion: project.mcp_version.toString(), loaderName: project.loader_name.toString(), - loaderCode: project.loader_code.toString(), javaVersion: project.java_version.toString(), - gradleJavaVersion: project.gradle_java_version.toString(), - curseForgeProjectId: project.curseforge_project_id.toString()] - def expected = [modVersion: '3.0.1.113021', modGroup: expectedMavenGroup, - minecraftVersion: '1.13.2', forgeVersion: '25.0.223', - forgeInstallerSha256: 'D46CB326E7DC32695084090C8169BB57F32F1C23B801DDB35A336F252E381191', - mappingChannel: 'snapshot', mappingVersion: '20180921-1.13', - mcpVersion: '20190213.203750', loaderName: 'forge', loaderCode: '1', - javaVersion: '8', gradleJavaVersion: '17', curseForgeProjectId: '240967'] - if (actual != expected) throw new GradleException("Unexpected Base Metals 1.13.2 identity: ${actual}") - } -} - -tasks.register('verifyReleaseDependencies') { - group = 'verification' - doLast { - def expected = [version: '4.1.0.113021', curseProjectId: '245586', curseFileId: '9035885', - sha256: '66C9CC5F8BE8F08F3DD5FA26EBEA79B52972B2FB86CB4DADBEEBC29AC01D3435'] - if (configuredOreSpawnIdentity != expected) { - throw new GradleException("Unexpected OreSpawn identity: ${configuredOreSpawnIdentity}") - } - File artifact = configurations.orespawnRelease.singleFile - if (artifact == null || !artifact.isFile()) throw new GradleException('Released OreSpawn did not resolve') - String actual = sha256(artifact) - if (actual != configuredOreSpawnIdentity.sha256) { - throw new GradleException("OreSpawn SHA-256 mismatch: ${actual}") - } - new ZipFile(artifact).withCloseable { zip -> - def metadataEntry = zip.getEntry('META-INF/mods.toml') - if (metadataEntry == null) throw new GradleException('OreSpawn mod metadata is missing') - String metadata = zip.getInputStream(metadataEntry).getText('UTF-8') - if (!metadata.contains('modId="orespawn"') - || !metadata.contains('version="4.1.0.113021"') - || !metadata.contains('versionRange="[1.13.2]"') - || zip.getEntry('zone/moddev/mc/orespawn/api/OreSpawnApi.class') == null) { - throw new GradleException('Expected the published Minecraft 1.13.2 OreSpawn 4.1 release and public API') - } - } - } -} - -def forbiddenReleasePath = { String entryName -> - String name = entryName.replace('\\', '/').toLowerCase(Locale.ROOT) - name.startsWith('com/mcmoddev/') || name.startsWith('ci-fixtures/') || name.startsWith('reference/') - || name.startsWith('agent-notes/') || name.contains('/agent-notes/') - || name.endsWith('/ore_textures.md') - || name.startsWith('src/integrationtest/') || name.startsWith('src/clientintegrationtest/') - || name.startsWith('src/gametest/') - || name.startsWith('src/modelgenerator/') || name.endsWith('/bmblockstateprovider.class') - || name.startsWith('src/integratedsmoke/') || name.startsWith('src/packagedclientsmoke/') - || name.startsWith('src/upgradesmoke/') || name.startsWith('src/packagedserversmoke/') - || name.contains('minecraft-mavenizer') || name.contains('/smoke/') - || name.contains('/gametest/') || name.endsWith('/oregenerator.class') - || name.endsWith('/worldgenerator.class') -} - -tasks.register('verifyReleaseArtifacts') { - group = 'verification' - dependsOn tasks.named('verifyReleaseJava8Toolchain') - dependsOn tasks.named('verifyReleaseConfiguration') - dependsOn tasks.named('verifyReleaseDependencies') - dependsOn tasks.named('verifyMavenCoordinates') - dependsOn tasks.named('assemble') - doLast { - File libs = file("${buildDir}/libs") - def actualArtifacts = fileTree(libs) { include 'BaseMetals-*.jar' }.files.collect { it.name }.sort() - if (actualArtifacts != expectedReleaseNames.sort()) { - throw new GradleException("Expected exactly ${expectedReleaseNames.sort()}, found ${actualArtifacts}") - } - expectedReleaseNames.each { artifactName -> - new ZipFile(new File(libs, artifactName)).withCloseable { zip -> - def leaked = zip.entries().toList().find { forbiddenReleasePath(it.name) } - if (leaked != null) throw new GradleException("${artifactName} contains forbidden ${leaked.name}") - } - } - File main = new File(libs, mainReleaseName) - new ZipFile(main).withCloseable { zip -> - Set names = zip.entries().toList().collect { it.name } as Set - ['META-INF/mods.toml', 'META-INF/MANIFEST.MF', 'META-INF/coremods.json', - 'coremods/basemetals_113_compatibility.js', 'pack.mcmeta', - 'data/basemetals/orespawn/provider.json', 'data/basemetals/recipes/stone_crackhammer.json', - 'data/minecraft/tags/blocks/anvil.json', 'assets/basemetals/lang/en_us.json', - 'META-INF/basemetals/docs/VERSIONS.md'].each { required -> - if (!names.contains(required)) throw new GradleException("Release jar is missing ${required}") - } - Manifest manifest = new Manifest(zip.getInputStream(zip.getEntry('META-INF/MANIFEST.MF'))) - if (manifest.mainAttributes.getValue('Maven-Artifact') != expectedMavenCoordinate - || manifest.mainAttributes.getValue('Build-Target') != - 'minecraft-1.13.2-forge-25.0.223-java-8') { - throw new GradleException('Release manifest contains the wrong identity') - } - String modsToml = zip.getInputStream(zip.getEntry('META-INF/mods.toml')).getText('UTF-8') - if (modsToml.contains('${') || !modsToml.contains('version="3.0.1.113021"') - || !modsToml.contains('versionRange="[4.1.0.113021,5.0.0)"')) { - throw new GradleException('Release mods.toml contains unresolved or incompatible metadata') - } - def provider = new JsonSlurper().parse(zip.getInputStream( - zip.getEntry('data/basemetals/orespawn/provider.json'))) - if (provider.schema_version != 3 || provider.provider_revision != 1 - || provider.provider_modid != 'basemetals' || provider.ores.size() != 11 - || provider.ores['basemetals:ore/copper'].enabled != true) { - throw new GradleException('OreSpawn provider contract changed') - } - def locales = names.findAll { it ==~ /assets\/basemetals\/lang\/.+\.json/ } - if (locales.size() != 18) { - throw new GradleException("Release jar must contain all 18 language files, found ${locales.size()}") - } - def classEntry = zip.entries().toList().find { it.name.endsWith('.class') } - byte[] header = zip.getInputStream(classEntry).withCloseable { it.readNBytes(8) } - int major = ((header[6] & 0xff) << 8) | (header[7] & 0xff) - if (major != 52) throw new GradleException("Release classes use bytecode major ${major}, not Java 8") - } - } -} - -tasks.register('writeReleaseChecksums') { - group = 'verification' - dependsOn tasks.named('verifyReleaseArtifacts') - doLast { - File output = file("${buildDir}/release/SHA256SUMS") - output.parentFile.mkdirs() - output.setText(expectedReleaseNames.sort().collect { name -> - "${sha256(file("${buildDir}/libs/${name}")).toLowerCase(Locale.ROOT)} ${name}" - }.join('\n') + '\n', 'UTF-8') - } -} - -tasks.register('verifyPreparedReleaseArtifacts') { - group = 'verification' - doLast { - if (preparedReleaseDir == null) throw new GradleException('preparedReleaseDir is required') - File prepared = file(preparedReleaseDir.toString()) - def expectedBundle = (expectedReleaseNames + ['SHA256SUMS', 'CHANGELOG.txt']).sort() - def actualBundle = prepared.listFiles().findAll { it.isFile() }.collect { it.name }.sort() - if (actualBundle != expectedBundle) { - throw new GradleException("Expected prepared bundle ${expectedBundle}, found ${actualBundle}") - } - Map declared = [:] - new File(prepared, 'SHA256SUMS').readLines('UTF-8').findAll { !it.trim().isEmpty() }.each { line -> - def match = line =~ /^([0-9a-fA-F]{64}) (.+)$/ - if (!match.matches()) throw new GradleException("Invalid prepared checksum line: ${line}") - declared[match[0][2]] = match[0][1].toUpperCase(Locale.ROOT) - } - if (declared.keySet().sort() != expectedReleaseNames.sort()) { - throw new GradleException('Prepared checksums do not declare exactly the three release jars') - } - declared.each { name, expectedHash -> - if (sha256(new File(prepared, name)) != expectedHash) { - throw new GradleException("Prepared checksum mismatch for ${name}") - } - } - } -} - -tasks.register('validateMavenReleaseCredentials') { - group = 'publishing' - doLast { - if (!System.getenv('MAVEN_UPLOAD_URL') || !System.getenv('MAVEN_UPLOAD_USERNAME') - || !System.getenv('MAVEN_UPLOAD_PASSWORD')) { - throw new GradleException('MAVEN_UPLOAD_URL, MAVEN_UPLOAD_USERNAME, and MAVEN_UPLOAD_PASSWORD are required') - } - if (System.getenv('MAVEN_UPLOAD_URL').startsWith('file:')) { - throw new GradleException('Maven release publication must use a remote repository') - } - } -} -tasks.withType(PublishToMavenRepository).configureEach { - dependsOn tasks.named('validateMavenReleaseCredentials') - dependsOn tasks.named('verifyMavenCoordinates') - dependsOn(preparedReleaseDir == null - ? tasks.named('verifyReleaseArtifacts') : tasks.named('verifyPreparedReleaseArtifacts')) -} - -def verifyGeneratedData = tasks.register('verifyGeneratedData', Exec) { - group = 'verification' - description = 'Regenerates and proves that the committed Minecraft 1.13.2 resource projection is current.' - commandLine 'node', 'tools/generate_113_resources.js' - def snapshot = { - Map result = new TreeMap<>() - [file('src/generated/resources'), - file('src/main/resources/data/basemetals/orespawn/provider.json')].each { File root -> - if (root.isFile()) { - result[project.relativePath(root).replace('\\', '/')] = sha256(root) - } else if (root.isDirectory()) { - fileTree(root) { exclude '.cache/**' }.files.findAll { it.isFile() }.each { File generated -> - result[project.relativePath(generated).replace('\\', '/')] = sha256(generated) - } - } - } - result - } - doFirst { ext.beforeGeneration = snapshot() } - doLast { - Map afterGeneration = snapshot() - if (ext.beforeGeneration != afterGeneration) { - Set beforePaths = ext.beforeGeneration.keySet() - Set afterPaths = afterGeneration.keySet() - List added = (afterPaths - beforePaths).toList().sort() - List removed = (beforePaths - afterPaths).toList().sort() - List changed = (beforePaths.intersect(afterPaths)).findAll { - ext.beforeGeneration[it] != afterGeneration[it] - }.toList().sort() - throw new GradleException('Generated resources were stale. Commit the generator output. ' + - "Added=${added.take(20)}, removed=${removed.take(20)}, changed=${changed.take(20)}") - } - } -} -def oreModelsDirectory = file('src/main/resources/assets/basemetals/models/block') -def oreModelSnapshot = { File directory -> - fileTree(directory) { include '*_ore.json' }.files.collectEntries { - [(it.name): sha256(it)] - } -} -def generateOreModels = tasks.register('generateOreModels', JavaExec) { - group = 'data generation' - description = 'Uses vanilla host textures when an ore overlay is present, otherwise keeps the legacy model.' - dependsOn tasks.named('modelGeneratorClasses') - classpath = sourceSets.modelGenerator.runtimeClasspath - mainClass = 'zone.moddev.mc.basemetals.data.BMBlockStateProvider' - args file('src/main/resources').absolutePath -} -tasks.named('processResources') { dependsOn generateOreModels } -def oreModelVerificationDirectory = layout.buildDirectory.dir('ore-model-verification') -def verifyGeneratedOreModels = tasks.register('verifyGeneratedOreModels', JavaExec) { - group = 'verification' - description = 'Checks the saved ore models without rewriting them.' - dependsOn tasks.named('modelGeneratorClasses') - classpath = sourceSets.modelGenerator.runtimeClasspath - mainClass = 'zone.moddev.mc.basemetals.data.BMBlockStateProvider' - args file('src/main/resources').absolutePath, '--output', oreModelVerificationDirectory.get().asFile.absolutePath - - doFirst { - File output = oreModelVerificationDirectory.get().asFile.canonicalFile - File buildDirectory = layout.buildDirectory.get().asFile.canonicalFile - if (!output.toPath().startsWith(buildDirectory.toPath()) || output.name != 'ore-model-verification') { - throw new GradleException("Unsafe ore-model verification directory: ${output}") - } - delete output - } - doLast { - File expectedModels = new File(oreModelVerificationDirectory.get().asFile, 'assets/basemetals/models/block') - if (oreModelSnapshot(oreModelsDirectory) != oreModelSnapshot(expectedModels)) { - throw new GradleException('Ore models are stale. Build the project and commit the updated JSON files.') - } - } -} -// Release checks must see stale models before the normal build updates them. -generateOreModels.configure { mustRunAfter verifyGeneratedOreModels } -tasks.named('verifyReleaseArtifacts') { dependsOn verifyGeneratedOreModels } -tasks.named('check') { - dependsOn tasks.named('verifyDevelopmentLaunches') - dependsOn verifyGeneratedData - dependsOn tasks.named('verifyReleaseConfiguration') - dependsOn tasks.named('verifyReleaseDependencies') - dependsOn tasks.named('verifyMavenCoordinates') -} - -eclipse { - project { name = eclipseWorkspaceProjectName } - classpath { - downloadSources = true - downloadJavadoc = true - file.whenMerged { classpath -> - classpath.entries.findAll { entry -> - String path = entry.path?.replace('\\', '/') ?: '' - path.startsWith('src/test/') || path.startsWith('src/integrationTest/') - || path.startsWith('src/modelGenerator/') || path.endsWith('/modelGenerator') - }.each { entry -> entry.entryAttributes['test'] = 'true' } - } - } - synchronizationTasks generateOreModels, 'isolateEclipseProductionRuns' -} -idea { - module { - downloadSources = true - downloadJavadoc = true - } -} - -def configureEclipseBuildship = tasks.register('configureEclipseBuildship') { - group = 'ide' - doLast { - File preferencesFile = file('.settings/org.eclipse.buildship.core.prefs') - Properties preferences = new Properties() - ['eclipse.preferences.version': '1', - 'connection.gradle.distribution': 'GRADLE_DISTRIBUTION(WRAPPER)', - 'connection.gradle.user.home': gradle.gradleUserHomeDir.canonicalPath, - 'connection.project.dir': '', 'gradle.user.home': gradle.gradleUserHomeDir.canonicalPath, - 'java.home': new File(System.getProperty('java.home')).canonicalPath, - 'override.workspace.settings': 'true'].each { key, value -> preferences.setProperty(key, value) } - preferencesFile.parentFile.mkdirs() - preferencesFile.withOutputStream { - preferences.store(it, 'Generated by Base Metals Buildship configuration.') - } - } -} - -def eclipseLaunchPathBoundaries = [ - [option: '--cache', suffix: ' --metadata'], - [option: '--metadata', suffix: ' --to-srg'], - [option: '--to-srg', suffix: ' --to-obf'], - [option: '--to-obf', suffix: ' --'], - [option: '--output', suffix: ' --existing'], - [option: '--existing', suffix: '"/>'] +// Longer checks share only these artifact helpers and build paths. +ext.baseMetalsBuild = [ + expectedMavenGroup: expectedMavenGroup, + expectedMavenArtifact: expectedMavenArtifact, + expectedMavenCoordinate: expectedMavenCoordinate, + eclipseWorkspaceProjectName: eclipseWorkspaceProjectName, + prepareIntegrationTestModClasses: prepareIntegrationTestModClasses, + releaseJar: releaseJar ] -def quoteEclipseLaunchPathArguments = { String contents -> - eclipseLaunchPathBoundaries.each { Map boundary -> - String option = boundary.option - String suffix = boundary.suffix - java.util.regex.Pattern pattern = java.util.regex.Pattern.compile( - java.util.regex.Pattern.quote(option + ' ') + '(.+?)' + - java.util.regex.Pattern.quote(suffix)) - java.util.regex.Matcher matcher = pattern.matcher(contents) - if (matcher.find()) { - String value = matcher.group(1) - if (!(value.startsWith('"') && value.endsWith('"'))) { - String replacement = option + ' "' + value + '"' + suffix - contents = matcher.replaceFirst(java.util.regex.Matcher.quoteReplacement(replacement)) - } - } - } - contents -} - -def syncForge25EclipseLaunches = tasks.register('syncForge25EclipseLaunches') { - group = 'ide' - dependsOn tasks.named('processResources') - doLast { - String mainClass = 'net.minecraftforge.launcher.Main' - String mainOutput = new File(projectDir, 'bin/main').absolutePath - String ordinaryModClasses = "${mod_id}%%${mainOutput};${mod_id}%%${mainOutput}" - def environmentEntries = { String launchTarget -> - " \r\n" + - " \r\n" + - " \r\n" + - " \r\n" + - " \r\n" + - " \r\n" - } - File processedResources = layout.buildDirectory.dir('resources/main').get().asFile - copy { from processedResources; into mainOutput } - [Client: 'fmluserdevclient', Server: 'fmluserdevserver', Data: 'fmluserdevdata'].each { - String runName, String launchTarget -> - File launch = file("run${runName}.launch") - if (!launch.isFile()) throw new GradleException("Missing generated Eclipse launch: ${launch}") - String text = launch.getText('UTF-8') - if (text.contains('')) { - text = text.replace('', - '\r\n' + - environmentEntries(launchTarget) + - " \r\n" + - '') - } - text = text.replace('', - "") - text = text.replaceFirst(//, - java.util.regex.Matcher.quoteReplacement( - "")) - if (!text.contains('key="target"')) { - text = text.replace(' ', - ' \r\n' + - '') - } - launch.setText(text.replace('\r\n', '\n'), 'UTF-8') - } - fileTree(projectDir) { include '*.launch' }.files.each { File launch -> - String text = quoteEclipseLaunchPathArguments(launch.getText('UTF-8')) - launch.setText(text.replace('\r\n', '\n'), 'UTF-8') - } - } -} -tasks.matching { it.name == 'genEclipseRuns' }.configureEach { finalizedBy syncForge25EclipseLaunches } -tasks.matching { it.name == 'eclipse' }.configureEach { finalizedBy syncForge25EclipseLaunches } - -tasks.register('isolateEclipseProductionRuns') { - group = 'ide' - dependsOn tasks.named('genEclipseRuns') - dependsOn syncForge25EclipseLaunches - dependsOn configureEclipseBuildship -} - -tasks.register('verifyEclipseProductionClasspath') { - group = 'verification' - dependsOn tasks.named('eclipseClasspath') - dependsOn tasks.named('isolateEclipseProductionRuns') - doLast { - Set synchronizationTasks = eclipse.synchronizationTasks.getDependencies(delegate).collect { - it.name - } as Set - if (!synchronizationTasks.contains('generateOreModels')) { - throw new GradleException('Eclipse Gradle refresh must regenerate ore models') - } - - File prefs = file('.settings/org.eclipse.buildship.core.prefs') - Properties buildshipPreferences = new Properties() - prefs.withInputStream { buildshipPreferences.load(it) } - String expectedGradleHome = gradle.gradleUserHomeDir.canonicalPath - if (buildshipPreferences.getProperty('connection.gradle.user.home') != expectedGradleHome - || buildshipPreferences.getProperty('gradle.user.home') != expectedGradleHome - || buildshipPreferences.getProperty('override.workspace.settings') != 'true') { - throw new GradleException("Eclipse Buildship must use Gradle home ${expectedGradleHome}") - } - File expectedJavaHome = new File(System.getProperty('java.home')).canonicalFile - File configuredJavaHome = new File(buildshipPreferences.getProperty('java.home', '')).canonicalFile - if (configuredJavaHome != expectedJavaHome) { - throw new GradleException("Eclipse Buildship must use Gradle Java home ${expectedJavaHome}") - } - File classpath = file('.classpath') - if (!classpath.isFile() || !classpath.getText('UTF-8').contains('path="src/generated/resources"')) { - throw new GradleException('Eclipse does not expose generated production resources') - } - String mainOutput = new File(projectDir, 'bin/main').absolutePath - String expectedModClasses = "${mod_id}%%${mainOutput};${mod_id}%%${mainOutput}" - def verifyQuotedPaths = { File launch, List> boundaries -> - String contents = launch.getText('UTF-8') - boundaries.each { Map boundary -> - String option = boundary.option - String suffix = boundary.suffix - java.util.regex.Pattern quotedPath = java.util.regex.Pattern.compile( - java.util.regex.Pattern.quote(option + ' "') + '(.+?)' + - java.util.regex.Pattern.quote('"' + suffix)) - java.util.regex.Matcher matcher = quotedPath.matcher(contents) - if (!matcher.find()) { - throw new GradleException("${launch.name} does not quote ${option} path") - } - File target = new File(matcher.group(1)) - if (!target.exists()) { - throw new GradleException("${launch.name} ${option} path does not exist: ${target}") - } - } - } - ['runClient.launch', 'runServer.launch', 'runData.launch'].each { name -> - File launch = file(name) - if (!launch.isFile()) throw new GradleException("Missing ${name}") - String contents = launch.getText('UTF-8') - List forbidden = ['src/test', 'src/integrationTest', 'src/modelGenerator', 'junit-', 'opentest4j-', - 'basemetalsprobe', 'C:\\Users\\John'] - List leaked = forbidden.findAll { contents.contains(it) } - if (!leaked.isEmpty() || !contents.contains('ATTR_EXCLUDE_TEST_CODE') - || !contents.contains("PROJECT_ATTR\" value=\"${eclipseWorkspaceProjectName}\"") - || !contents.contains("MOD_CLASSES\" value=\"${expectedModClasses}\"") - || !contents.contains('MAIN_TYPE" value="net.minecraftforge.launcher.Main"') - || !contents.contains('--main net.minecraftforge.userdev.LaunchTesting')) { - throw new GradleException("${name} is not an isolated Forge 25 launch: ${leaked}") - } - } - List allLaunches = fileTree(projectDir) { include '*.launch' }.files.sort { it.name } - allLaunches.each { File launch -> - verifyQuotedPaths(launch, eclipseLaunchPathBoundaries.take(4)) - } - allLaunches.findAll { it.name.endsWith('Data.launch') }.each { File launch -> - verifyQuotedPaths(launch, eclipseLaunchPathBoundaries.drop(4)) - } - String clientLaunch = file('runClient.launch').getText('UTF-8') - def launcherMatch = clientLaunch =~ /externalArchive="([^&]+[\\\/]slime-launcher-[^&]+\.jar)"/ - if (!launcherMatch.find()) { - throw new GradleException('runClient.launch does not reference the ForgeGradle slime launcher') - } - File launcherJar = file(launcherMatch.group(1).replace('&', '&')) - if (!launcherJar.isFile() || !new ZipFile(launcherJar).withCloseable { - it.getEntry('net/minecraftforge/launcher/Main.class') != null - }) { - throw new GradleException("Invalid ForgeGradle slime launcher archive: ${launcherJar}") - } - ['META-INF/mods.toml', 'data/basemetals/orespawn/provider.json', - 'data/basemetals/recipes/stone_crackhammer.json'].each { relative -> - if (!new File(projectDir, "bin/main/${relative}").isFile()) { - throw new GradleException("Eclipse output is missing ${relative}") - } - } - [file('src/main/resources/META-INF/mods.toml'), - file('bin/main/META-INF/mods.toml')].each { File metadata -> - String contents = metadata.getText('UTF-8') - if (contents.contains('${') || !contents.contains('modId="basemetals"') - || !contents.contains("version=\"${project.version}\"") - || !contents.contains('[[dependencies.basemetals]]')) { - throw new GradleException("Eclipse mod metadata is unresolved or has the wrong identity: ${metadata}") - } - } - [file('src/main/resources/pack.mcmeta'), file('bin/main/pack.mcmeta')].each { File metadata -> - if (metadata.getText('UTF-8').contains('${')) { - throw new GradleException("Eclipse pack metadata is unresolved: ${metadata}") - } - } - } -} - -def packagedServerProbeDeobfJar = tasks.register('packagedServerProbeDeobfJar', Jar) { - dependsOn tasks.named(sourceSets.integrationTest.classesTaskName) - archiveFileName = 'basemetalsprobe-deobf.jar' - destinationDirectory = layout.buildDirectory.dir('packaged-probes') - from sourceSets.integrationTest.output -} -def packagedServerProbeJar = renamer.classes(packagedServerProbeDeobfJar) { - map.from minecraft.dependency.toSrgFile - output = layout.buildDirectory.file('packaged-probes/basemetalsprobe.jar') -} - -def clientIntegrationClasses = layout.buildDirectory.dir('client-integration-fixture/classes') -def compileClientIntegrationTestMod = tasks.register('compileClientIntegrationTestMod', JavaCompile) { - dependsOn tasks.named('classes') - source fileTree('src/clientIntegrationTest/java') - classpath = files(sourceSets.main.output, sourceSets.main.compileClasspath) - destinationDirectory = clientIntegrationClasses - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - options.encoding = 'UTF-8' -} -def clientIntegrationTestModJar = tasks.register('clientIntegrationTestModJar', Jar) { - dependsOn compileClientIntegrationTestMod - archiveFileName = 'basemetalsclientprobe-deobf.jar' - destinationDirectory = layout.buildDirectory.dir('client-integration-fixture') - from clientIntegrationClasses - from 'src/clientIntegrationTest/resources' -} -def oreOverlayClientFixtures = layout.buildDirectory.dir('ore-overlay-client-fixtures') -def generateOreOverlayClientFixtures = tasks.register('generateOreOverlayClientFixtures', JavaExec) { - dependsOn tasks.named('modelGeneratorClasses') - classpath = sourceSets.modelGenerator.runtimeClasspath - mainClass = 'zone.moddev.mc.basemetals.data.BMBlockStateProvider' - args file('src/main/resources').absolutePath, '--client-fixtures', oreOverlayClientFixtures.get().asFile.absolutePath -} -def packagedClientProbeJar = renamer.classes(clientIntegrationTestModJar) { - map.from minecraft.dependency.toSrgFile - output = layout.buildDirectory.file('client-integration-fixture/basemetalsclientprobe.jar') -} - -def packagedForgeServerRuntime = providers.gradleProperty('packagedForgeServerRuntime') -def packagedForgeClientRuntime = providers.gradleProperty('packagedForgeClientRuntime') -def requireRuntimeDirectory = { Provider configuredPath, String propertyName -> - if (!configuredPath.isPresent()) { - throw new GradleException("Pass -P${propertyName}=") - } - File runtime = file(configuredPath.get()).canonicalFile - if (!runtime.isDirectory()) { - throw new GradleException("${propertyName} does not name a directory: ${runtime}") - } - runtime -} -def prepareBuildOwnedRunDirectory = { File candidate, String expectedName -> - File runDirectory = candidate.canonicalFile - File buildDirectory = layout.buildDirectory.get().asFile.canonicalFile - if (!runDirectory.toPath().startsWith(buildDirectory.toPath()) || runDirectory.name != expectedName) { - throw new GradleException("Unsafe packaged-runtime directory: ${runDirectory}") - } - delete runDirectory - if (!runDirectory.mkdirs()) throw new GradleException("Could not create ${runDirectory}") - runDirectory -} -def releasedOreSpawnJar = { - File artifact = configurations.orespawnRelease.singleFile - if (artifact == null || !artifact.isFile() || sha256(artifact) != configuredOreSpawnIdentity.sha256) { - throw new GradleException('Exact released OreSpawn artifact is unavailable for packaged testing') - } - artifact -} -def assertPackagedRuntimePassed = { File runDirectory, String marker, String label -> - File log = new File(runDirectory, 'logs/latest.log') - if (!log.isFile()) throw new GradleException("${label} log is missing: ${log}") - String contents = log.getText('UTF-8') - if (!contents.contains(marker)) throw new GradleException("${label} did not report ${marker}") - List failures = ['Failed to load correctly', 'This crash report has been saved to', - 'Encountered an unexpected exception', 'BASEMETALS_RUNTIME_PROBE FAIL', - 'BASEMETALS_CLIENT_PROBE FAIL'] - .findAll { contents.contains(it) } - if (!failures.isEmpty()) throw new GradleException("${label} failures: ${failures}") - File crashReports = new File(runDirectory, 'crash-reports') - if (crashReports.isDirectory() && !fileTree(crashReports).files.isEmpty()) { - throw new GradleException("${label} created crash reports in ${crashReports}") - } -} - -def packagedServerRunDirectory = layout.buildDirectory.dir('packaged-server-run') -def preparePackagedServerIntegration = tasks.register('preparePackagedServerIntegration') { - dependsOn releaseJar - dependsOn packagedServerProbeJar - dependsOn tasks.named('verifyReleaseDependencies') - doLast { - File runDirectory = prepareBuildOwnedRunDirectory( - packagedServerRunDirectory.get().asFile, 'packaged-server-run') - copy { - from releaseJar - from packagedServerProbeJar - from releasedOreSpawnJar() - into new File(runDirectory, 'mods') - } - new File(runDirectory, 'server.properties').setText('''\ -level-name=basemetals-packaged-server-smoke -online-mode=false -spawn-protection=0 -server-port=0 -view-distance=4 -max-tick-time=-1 -''', 'UTF-8') - new File(runDirectory, 'eula.txt').setText('eula=true\n', 'UTF-8') - } -} -def packagedServerProcess = tasks.register('packagedServerProcess', Exec) { - group = 'verification' - dependsOn preparePackagedServerIntegration - doFirst { - File runtime = requireRuntimeDirectory(packagedForgeServerRuntime, 'packagedForgeServerRuntime') - File launcher = new File(runtime, "forge-${minecraft_version}-${forge_version}.jar") - File vanillaServer = new File(runtime, "minecraft_server.${minecraft_version}.jar") - File libraries = new File(runtime, 'libraries') - [launcher, vanillaServer, libraries].each { - if (!it.exists()) throw new GradleException("Incomplete official server runtime: ${it}") - } - workingDir packagedServerRunDirectory.get().asFile - commandLine javaToolchains.launcherFor { - languageVersion = JavaLanguageVersion.of(8) - vendor = JvmVendorSpec.ADOPTIUM - }.get().executablePath.asFile.absolutePath, - '-Xms512m', '-Xmx2g', '-jar', launcher.absolutePath, 'nogui' - } -} -tasks.register('packagedServerIntegrationTest') { - group = 'verification' - description = 'Runs the exact release jars in an official Forge 25 dedicated-server runtime.' - dependsOn packagedServerProcess - doLast { - assertPackagedRuntimePassed(packagedServerRunDirectory.get().asFile, - 'BASEMETALS_RUNTIME_PROBE PASS', 'packaged dedicated server') - } -} - -def packagedClientRunDirectory = layout.buildDirectory.dir('packaged-client-run') -def preparePackagedClientIntegration = tasks.register('preparePackagedClientIntegration') { - dependsOn releaseJar - dependsOn packagedClientProbeJar - dependsOn tasks.named('verifyReleaseDependencies') - dependsOn generateOreOverlayClientFixtures - doLast { - File runDirectory = prepareBuildOwnedRunDirectory( - packagedClientRunDirectory.get().asFile, 'packaged-client-run') - copy { - from releaseJar - from packagedClientProbeJar - from releasedOreSpawnJar() - into new File(runDirectory, 'mods') - } - File overlayPack = new File(runDirectory, 'resourcepacks/basemetals-ore-overlay-fixture') - copy { from oreOverlayClientFixtures; into overlayPack } - new File(overlayPack, 'pack.mcmeta').setText( - '{"pack":{"pack_format":4,"description":"Base Metals ore overlay test fixture"}}\n', 'UTF-8') - new File(runDirectory, 'options.txt').setText( - 'fullscreen:false\nlang:en_us\nresourcePacks:["file/basemetals-ore-overlay-fixture"]\n', 'UTF-8') - } -} -def packagedClientProcess = tasks.register('packagedClientProcess', Exec) { - group = 'verification' - dependsOn preparePackagedClientIntegration - doFirst { - File runtime = requireRuntimeDirectory(packagedForgeClientRuntime, 'packagedForgeClientRuntime') - File forgeJsonFile = new File(runtime, - "versions/${minecraft_version}-forge-${forge_version}/${minecraft_version}-forge-${forge_version}.json") - File baseJsonFile = new File(runtime, "versions/${minecraft_version}/${minecraft_version}.json") - File baseJar = new File(runtime, "versions/${minecraft_version}/${minecraft_version}.jar") - File libraries = new File(runtime, 'libraries') - File assets = new File(runtime, 'assets-local') - File natives = new File(runtime, 'natives') - [forgeJsonFile, baseJsonFile, baseJar, libraries, assets, natives].each { - if (!it.exists()) throw new GradleException("Incomplete official client runtime: ${it}") - } - - Map forgeJson = (Map) new JsonSlurper().parse(forgeJsonFile) - Map baseJson = (Map) new JsonSlurper().parse(baseJsonFile) - Map classpathByModule = new LinkedHashMap<>() - [baseJson, forgeJson].each { Map metadata -> - ((List) metadata.libraries).each { Map library -> - List rules = (List) library.rules - boolean allowed = rules == null || rules.isEmpty() - if (rules != null) { - rules.each { Map rule -> - Map os = (Map) rule.os - boolean matches = os == null || (os.name == 'windows' - && (os.arch == null || os.arch == System.getProperty('os.arch'))) - if (matches) allowed = rule.action == 'allow' - } - } - if (!allowed) return - String relative = (String) ((Map) ((Map) library.downloads).artifact).path - File artifact = new File(runtime, "libraries/${relative}") - if (!artifact.isFile()) throw new GradleException("Missing official client library: ${artifact}") - List coordinates = ((String) library.name).split(':') as List - String module = coordinates.size() >= 2 - ? "${coordinates[0]}:${coordinates[1]}" : (String) library.name - classpathByModule.put(module, artifact) - } - } - List classpathFiles = new ArrayList<>(classpathByModule.values()) - classpathFiles.add(baseJar) - List gameArguments = [] - gameArguments.addAll((List) ((Map) forgeJson.arguments).game) - gameArguments.addAll([ - '--username', 'BaseMetalsValidation', - '--version', "${minecraft_version}-forge-${forge_version}", - '--gameDir', packagedClientRunDirectory.get().asFile.absolutePath, - '--assetsDir', assets.absolutePath, - '--assetIndex', '1.13.1', - '--uuid', '00000000-0000-0000-0000-000000000001', - '--accessToken', 'validation-token', - '--userType', 'legacy', - '--versionType', 'release', - '--width', '854', '--height', '480' - ]) - workingDir packagedClientRunDirectory.get().asFile - commandLine javaToolchains.launcherFor { - languageVersion = JavaLanguageVersion.of(8) - vendor = JvmVendorSpec.ADOPTIUM - }.get().executablePath.asFile.absolutePath, - '-Xms512m', '-Xmx2g', '-Dbasemetalsclientprobe.enabled=true', - "-Djava.library.path=${natives.absolutePath}", - '-Dminecraft.launcher.brand=basemetals-validation', - '-Dminecraft.launcher.version=1', - '-cp', classpathFiles.collect { it.absolutePath }.join(File.pathSeparator), - (String) forgeJson.mainClass - args gameArguments - } -} -tasks.register('packagedClientIntegrationTest') { - group = 'verification' - description = 'Loads and renders the exact release jars in an official Forge 25 client runtime.' - dependsOn packagedClientProcess - doLast { - File runDirectory = packagedClientRunDirectory.get().asFile - File marker = new File(runDirectory, 'client-smoke-pass.properties') - if (!marker.isFile()) throw new GradleException('Packaged client completion marker is missing') - Properties values = new Properties() - marker.withInputStream { values.load(it) } - if (values.getProperty('content_verified') != 'true' - || values.getProperty('integrated_world_rendered') != 'true') { - throw new GradleException("Packaged client probe failed: ${values}") - } - assertPackagedRuntimePassed(runDirectory, 'BASEMETALS_CLIENT_PROBE PASS', 'packaged client') - } -} - -tasks.register('packagedRuntimeIntegrationTest') { - group = 'verification' - description = 'Runs the exact Base Metals and OreSpawn jars in official Forge 25 server and client runtimes.' - dependsOn tasks.named('packagedServerIntegrationTest') - dependsOn tasks.named('packagedClientIntegrationTest') -} - -tasks.register('contentModeIntegrationTest', Exec) { - group = 'verification' - description = 'Tests packaged content policies and client/server mode matching with disposable profiles.' - dependsOn releaseJar, packagedServerProbeJar, packagedClientProbeJar - dependsOn tasks.named('verifyReleaseDependencies') - doFirst { - File root = prepareBuildOwnedRunDirectory(file("${buildDir}/content-mode-tests"), 'content-mode-tests') - File fixture = new File(root, 'fixture') - copy { - from releaseJar, packagedServerProbeJar, packagedClientProbeJar, releasedOreSpawnJar() - into fixture - } - File java = javaToolchains.launcherFor { - languageVersion = JavaLanguageVersion.of(8) - vendor = JvmVendorSpec.ADOPTIUM - }.get().executablePath.asFile - commandLine 'node', file('tools/test_content_modes.js').absolutePath, java.absolutePath, - requireRuntimeDirectory(packagedForgeServerRuntime, 'packagedForgeServerRuntime').absolutePath, - requireRuntimeDirectory(packagedForgeClientRuntime, 'packagedForgeClientRuntime').absolutePath, - fixture.absolutePath, root.absolutePath - if (providers.gradleProperty('contentModeCycleOnly').isPresent()) args '--cycle-only' - } -} +apply from: 'gradle/release/artifacts.gradle' +apply from: 'gradle/verification/development.gradle' +apply from: 'gradle/verification/generated-data.gradle' +apply from: 'gradle/ide/eclipse.gradle' +apply from: 'gradle/ide/intellij.gradle' +apply from: 'gradle/verification/packaged-forge.gradle' diff --git a/docs/COMPATIBILITY.md b/docs/COMPATIBILITY.md index b08cf602..07137128 100644 --- a/docs/COMPATIBILITY.md +++ b/docs/COMPATIBILITY.md @@ -8,21 +8,21 @@ ores, crystals, shards, clumps, dirty dusts, and molten fluids. Adamantite, Adamantium, Adamant, Quicksilver, and steel-sprocket names are forwarding tags. Copper-consuming recipes accept `forge:ingots/copper`, except Base Metals' own compacting and decompacting recipes, whose output must remain -the Base Metals item. On Minecraft 1.13.2 the copper ore, ingot, and storage +the Base Metals item. On Minecraft 1.14.4 the copper ore, ingot, and storage tags contain Base Metals entries; other mods can add their equivalents through normal tag merging. This version has no mod-specific plugins for Mekanism, Thermal Expansion, Tinkers' Construct, Ender IO, IC2, Thaumcraft, Dense Ores, VeinMiner, or Constructs Armory. Their version-specific 1.18 data and APIs are not valid on Forge -25. Generic processors and tools can interoperate through Forge tags. +28. Generic processors and tools can interoperate through Forge tags. Mineralogy is optional. When installed, OreSpawn can place the same ores in Mineralogy's rocks. Base Metals does not need Mineralogy to run. ## Changes from the 1.12 release -The native 1.13 port retains the corrected fuel values, door and trapdoor +The native port retains the corrected fuel values, door and trapdoor recipes, material fishing-rod durability, rail yields, ranged damage and projectile pickup, armour effects, recipe-book unlocks, plate repairs and the 18 gameplay achievements. High/Low Fantasy and both configuration entry points diff --git a/docs/MIGRATION.md b/docs/MIGRATION.md index 0d284173..cd2f155e 100644 --- a/docs/MIGRATION.md +++ b/docs/MIGRATION.md @@ -1,4 +1,4 @@ -# Migrating from Base Metals 1.10 or 1.12 +# Migrating from earlier Base Metals versions Minecraft 1.13 introduced the flattening, which replaced numeric block and item identifiers with names and palette-based block states. Third-party numeric @@ -6,7 +6,7 @@ blocks cannot be reconstructed by Mojang's vanilla fixer alone, so Base Metals installs a narrowly scoped migration hook before Forge reads an old world. Always upgrade a copy and retain the original 1.10/1.12 world. Install Base -Metals `3.0.1.113021`, OreSpawn `4.1.0.113021`, and any other 1.13.2 mods needed +Metals `3.0.1.114041`, OreSpawn `4.1.0.114041`, and any other 1.14.4 mods needed by that world. Do not carry MMDLib, Additional Loot Tables, the OreSpawn 3 plugin, or Base Metals' old fallback generator into the new instance. @@ -37,8 +37,8 @@ nugget, and the MMDLib Vanilla Bits block/item/fluid names. The migration supports the original Cyano Base Metals 2.4 line and the later MMD Base Metals releases for Minecraft 1.10.2 and 1.12.2. It is not a downgrade -path: do not open a world already saved by Minecraft 1.14 or newer, including -the Base Metals 1.18.2 port, in Minecraft 1.13.2. +path: do not open a world already saved by Minecraft 1.15 or newer, including +the Base Metals 1.18.2 port, in Minecraft 1.14.4. ## Mod settings @@ -54,11 +54,30 @@ limit what you can make instead. See [Content modes and configuration](CONTENT_M ## OreSpawn configuration -The Base Metals provider uses the historical 1.13.2 defaults documented in +The Base Metals provider uses the historical 1.14.4 defaults documented in [WORLDGEN.md](WORLDGEN.md), including enabled copper. OreSpawn configuration migration remains OreSpawn's responsibility. Keep a copy of the old OreSpawn configuration alongside the untouched source world when validating an upgrade. The generated manifests at `data/basemetals/registry_manifest_1_12.json` and `data/basemetals/registry_manifest.json` document the historical registry -baseline and its 1.13.2 projection. +baseline and its 1.14.4 projection. + +## Moving from Minecraft 1.13.2 + +Use a copy of your 1.13.2 world, with the 1.14.4 releases of Base Metals and +OreSpawn. Keep `basemetals-common.toml` and your OreSpawn settings: this port +uses the same configuration keys, registered names and ore-rule IDs. Existing +items, recipe-book progress and terrain should remain unchanged. Once Minecraft +has saved the copy in 1.14.4, do not reopen it in the older game. + +Forge 1.13 saved a profession registry that no longer exists in 1.14. Base +Metals removes that obsolete index when it contains only vanilla professions; +Minecraft still converts the villagers and their saved trades normally. If +another mod added professions to that index, Forge keeps its warning so you +can check that mod's migration support. The original `level.dat` is backed up +before Base Metals changes it. + +The old vanilla sign and dye names are mapped to oak signs and the matching +1.14 dyes. Other missing registry entries still need the relevant mod's +migration support; Base Metals does not silently discard them. diff --git a/docs/VERSIONS.md b/docs/VERSIONS.md index fc0ab343..b58de736 100644 --- a/docs/VERSIONS.md +++ b/docs/VERSIONS.md @@ -1,14 +1,14 @@ # Supported versions -Base Metals `3.0.1.113021` targets Minecraft `1.13.2`, Forge `25.0.223`, and +Base Metals `3.0.1.114041` targets Minecraft `1.14.4`, Forge `28.2.26`, and Java 8. The Gradle build itself runs on Java `17.0.1+12` and uses ForgeGradle `7.0.34` with Gradle `9.6.1`; those build-time runtimes do not change the Java 8 requirement of the released mod. Builds and tests use the published OreSpawn -`4.1.0.113021` release (CurseForge project `245586`, file `9035885`, SHA-256 -`66C9CC5F8BE8F08F3DD5FA26EBEA79B52972B2FB86CB4DADBEEBC29AC01D3435`). -At runtime, Base Metals requires OreSpawn `[4.1.0.113021,5.0.0)` for Minecraft 1.13.2. +`4.1.0.114041` release (CurseForge project `245586`, file `9041431`, SHA-256 +`7691C1AE7C6AC890A2CC785F3EEF54A1255D61E4CFCF5661D67CB1609CFC41C6`). +At runtime, Base Metals requires OreSpawn `[4.1.0.114041,5.0.0)` for Minecraft 1.14.4. The configuration cog uses OreSpawn's public 4.1 client API. Builds download this file through CurseMaven. CI verifies the download and @@ -16,4 +16,4 @@ stages it in a temporary Maven repository. The schema-3 provider supports the OreSpawn versions in the runtime range above. Release artifacts use Maven coordinate -`zone.moddev.mc.basemetals:BaseMetals:3.0.1.113021`. +`zone.moddev.mc.basemetals:BaseMetals:3.0.1.114041`. diff --git a/docs/WORLDGEN.md b/docs/WORLDGEN.md index b5fbbc91..74b0c6b8 100644 --- a/docs/WORLDGEN.md +++ b/docs/WORLDGEN.md @@ -1,8 +1,8 @@ # OreSpawn world generation -Base Metals requires OreSpawn `[4.1.0.113021,5.0.0)` and packages provider +Base Metals requires OreSpawn `[4.1.0.114041,5.0.0)` and packages provider schema 3, revision 1 at `data/basemetals/orespawn/provider.json`. Builds and -tests use the published OreSpawn `4.1.0.113021` release. +tests use the published OreSpawn `4.1.0.114041` release. The provider declares ores only: it has no rocks, geomes, biome rules, terrain dimensions, formations, fluid deposits, vanilla suppression, or strata @@ -24,7 +24,7 @@ and `retrogen:false`. | Nickel | all except Nether/End | 32..95 | uniform | 1 | same | | Platinum | all except Nether/End | 1..31 | uniform | 0.125 | same | -Minecraft 1.13.2 has no vanilla copper ore, so Base Metals copper generates +Minecraft 1.14.4 has no vanilla copper ore, so Base Metals copper generates by default. Antimony and Bismuth ores are available in creative mode and old saves, but do not generate unless you add rules for them in OreSpawn. diff --git a/gradle.properties b/gradle.properties index a872d516..f3fff463 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,10 +7,10 @@ org.gradle.parallel=false net.minecraftforge.gradle.merge-source-sets=false # Minecraft, Forge and mappings must describe the same game version. -minecraft_version=1.13.2 -minecraft_version_range=[1.13.2] -forge_version=25.0.223 -forge_installer_sha256=D46CB326E7DC32695084090C8169BB57F32F1C23B801DDB35A336F252E381191 +minecraft_version=1.14.4 +minecraft_version_range=[1.14.4] +forge_version=28.2.26 +forge_installer_sha256=E68A0E2D589B2954C5D109A04B80747AC8C061ACABE074DB1607F9C2FD7D2CE1 # Target and Java versions used by release checks and publishing. loader_name=forge @@ -21,25 +21,25 @@ gradle_java_version=17 curseforge_project_id=240967 # Pin the OreSpawn release we build and test against. -orespawn_version=4.1.0.113021 +orespawn_version=4.1.0.114041 orespawn_curse_project_id=245586 -orespawn_curse_file_id=9035885 -orespawn_sha256=66C9CC5F8BE8F08F3DD5FA26EBEA79B52972B2FB86CB4DADBEEBC29AC01D3435 +orespawn_curse_file_id=9041431 +orespawn_sha256=7691C1AE7C6AC890A2CC785F3EEF54A1255D61E4CFCF5661D67CB1609CFC41C6 # Versions accepted when players load the mod. -forge_version_range=[25,) -loader_version_range=[25,) +forge_version_range=[28,) +loader_version_range=[28,) -# MCP mappings for Forge 25. +# MCP mappings for Forge 28. mapping_channel=snapshot -mapping_version=20180921-1.13 -mcp_version=20190213.203750 +mapping_version=20190719-1.14.3 +mcp_version=20190829.143755 # Mod identity. Keep mod_id unchanged so existing saves still load. mod_id=basemetals mod_name=Base Metals mod_license=LGPL-2.1 -mod_version=3.0.1.113021 +mod_version=3.0.1.114041 # Maven coordinates and credits. mod_group=zone.moddev.mc.basemetals diff --git a/gradle/README.md b/gradle/README.md new file mode 100644 index 00000000..3c23c090 --- /dev/null +++ b/gradle/README.md @@ -0,0 +1,29 @@ +# Build scripts + +`build.gradle` holds the Minecraft target, toolchains, dependencies and resource +setup. Longer checks live in the scripts below, following OreSpawn's layout. +They share a small `baseMetalsBuild` map of artifact helpers and build paths. + +| Script | Purpose | +| --- | --- | +| `release/artifacts.gradle` | Release identity, dependency checks, jar audits, checksums and Maven publication. | +| `verification/development.gradle` | Disposable development-server and legacy-upgrade profiles. | +| `verification/generated-data.gradle` | Catalogue resources and Kiri's ore model generation. | +| `verification/packaged-forge.gradle` | Finished-jar client, server and content-mode checks. | +| `ide/eclipse.gradle` | Buildship settings, launches and production-classpath isolation. | +| `ide/intellij.gradle` | Client and server shortcuts using IntelliJ's Gradle runner. | + +Use Java 17 to run Gradle. ForgeGradle uses Java 25 for its Mavenizer; Minecraft +and the mod run on Java 8. Development accepts newer Java 8 updates, while +release auditing requires the pinned compiler in `gradle.properties`. + +Run `check build` for the normal build, then `verifyReleaseArtifacts` and +`writeReleaseChecksums` when preparing a release candidate. Packaged checks +also need `packagedForgeServerRuntime` and `packagedForgeClientRuntime`, pointing +to official Forge installations. They copy runtime files into disposable build +directories and never open your normal saved worlds. + +After changing launch settings, run `genEclipseRuns eclipse +verifyEclipseProductionClasspath`. IntelliJ users can use `genIntellijRuns` or +the Gradle `runClient` task. Tests and probes stay out of normal launches and +public jars. diff --git a/gradle/ide/eclipse.gradle b/gradle/ide/eclipse.gradle new file mode 100644 index 00000000..6c386c51 --- /dev/null +++ b/gradle/ide/eclipse.gradle @@ -0,0 +1,243 @@ +import groovy.json.JsonSlurper +import groovy.xml.XmlSlurper +import java.security.MessageDigest +import java.util.jar.Manifest +import java.util.zip.ZipFile + +def eclipseWorkspaceProjectName = project.ext.baseMetalsBuild.eclipseWorkspaceProjectName +def generateOreModels = project.ext.baseMetalsBuild.generateOreModels + +eclipse { + project { name = eclipseWorkspaceProjectName } + classpath { + downloadSources = true + downloadJavadoc = true + file.whenMerged { classpath -> + classpath.entries.findAll { entry -> + String path = entry.path?.replace('\\', '/') ?: '' + path.startsWith('src/test/') || path.startsWith('src/integrationTest/') + || path.startsWith('src/modelGenerator/') || path.endsWith('/modelGenerator') + }.each { entry -> entry.entryAttributes['test'] = 'true' } + } + } + synchronizationTasks generateOreModels, 'isolateEclipseProductionRuns' +} +idea { + module { + downloadSources = true + downloadJavadoc = true + } +} + +def configureEclipseBuildship = tasks.register('configureEclipseBuildship') { + group = 'ide' + doLast { + File preferencesFile = file('.settings/org.eclipse.buildship.core.prefs') + Properties preferences = new Properties() + ['eclipse.preferences.version': '1', + 'connection.gradle.distribution': 'GRADLE_DISTRIBUTION(WRAPPER)', + 'connection.gradle.user.home': gradle.gradleUserHomeDir.canonicalPath, + 'connection.project.dir': '', 'gradle.user.home': gradle.gradleUserHomeDir.canonicalPath, + 'java.home': new File(System.getProperty('java.home')).canonicalPath, + 'override.workspace.settings': 'true'].each { key, value -> preferences.setProperty(key, value) } + preferencesFile.parentFile.mkdirs() + preferencesFile.withOutputStream { + preferences.store(it, 'Generated by Base Metals Buildship configuration.') + } + } +} + +def eclipseLaunchPathBoundaries = [ + [option: '--cache', suffix: ' --metadata'], + [option: '--metadata', suffix: ' --to-srg'], + [option: '--to-srg', suffix: ' --to-obf'], + [option: '--to-obf', suffix: ' --'], + [option: '--output', suffix: ' --existing'], + [option: '--existing', suffix: '"/>'] +] + +def quoteEclipseLaunchPathArguments = { String contents -> + eclipseLaunchPathBoundaries.each { Map boundary -> + String option = boundary.option + String suffix = boundary.suffix + java.util.regex.Pattern pattern = java.util.regex.Pattern.compile( + java.util.regex.Pattern.quote(option + ' ') + '(.+?)' + + java.util.regex.Pattern.quote(suffix)) + java.util.regex.Matcher matcher = pattern.matcher(contents) + if (matcher.find()) { + String value = matcher.group(1) + if (!(value.startsWith('"') && value.endsWith('"'))) { + String replacement = option + ' "' + value + '"' + suffix + contents = matcher.replaceFirst(java.util.regex.Matcher.quoteReplacement(replacement)) + } + } + } + contents +} + +def syncForge28EclipseLaunches = tasks.register('syncForge28EclipseLaunches') { + group = 'ide' + dependsOn tasks.named('processResources') + doLast { + String mainClass = 'net.minecraftforge.launcher.Main' + String mainOutput = new File(projectDir, 'bin/main').absolutePath + String ordinaryModClasses = "${mod_id}%%${mainOutput}${File.pathSeparator}${mod_id}%%${mainOutput}" + def environmentEntries = { String launchTarget -> + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + } + File processedResources = layout.buildDirectory.dir('resources/main').get().asFile + copy { from processedResources; into mainOutput } + [Client: 'fmluserdevclient', Server: 'fmluserdevserver', Data: 'fmluserdevdata'].each { + String runName, String launchTarget -> + File launch = file("run${runName}.launch") + if (!launch.isFile()) throw new GradleException("Missing generated Eclipse launch: ${launch}") + String text = launch.getText('UTF-8') + if (text.contains('')) { + text = text.replace('', + '\r\n' + + environmentEntries(launchTarget) + + " \r\n" + + '') + } + text = text.replace('', + "") + text = text.replaceFirst(//, + java.util.regex.Matcher.quoteReplacement( + "")) + if (!text.contains('key="target"')) { + text = text.replace(' ', + ' \r\n' + + '') + } + launch.setText(text.replace('\r\n', '\n'), 'UTF-8') + } + fileTree(projectDir) { include '*.launch' }.files.each { File launch -> + String text = quoteEclipseLaunchPathArguments(launch.getText('UTF-8')) + launch.setText(text.replace('\r\n', '\n'), 'UTF-8') + } + } +} +tasks.matching { it.name == 'genEclipseRuns' }.configureEach { finalizedBy syncForge28EclipseLaunches } +tasks.matching { it.name == 'eclipse' }.configureEach { finalizedBy syncForge28EclipseLaunches } + +tasks.register('isolateEclipseProductionRuns') { + group = 'ide' + dependsOn tasks.named('genEclipseRuns') + dependsOn syncForge28EclipseLaunches + dependsOn configureEclipseBuildship +} + +tasks.register('verifyEclipseProductionClasspath') { + group = 'verification' + dependsOn tasks.named('eclipseClasspath') + dependsOn tasks.named('isolateEclipseProductionRuns') + doLast { + Set synchronizationTasks = eclipse.synchronizationTasks.getDependencies(delegate).collect { + it.name + } as Set + if (!synchronizationTasks.contains('generateOreModels')) { + throw new GradleException('Eclipse Gradle refresh must regenerate ore models') + } + + File prefs = file('.settings/org.eclipse.buildship.core.prefs') + Properties buildshipPreferences = new Properties() + prefs.withInputStream { buildshipPreferences.load(it) } + String expectedGradleHome = gradle.gradleUserHomeDir.canonicalPath + if (buildshipPreferences.getProperty('connection.gradle.user.home') != expectedGradleHome + || buildshipPreferences.getProperty('gradle.user.home') != expectedGradleHome + || buildshipPreferences.getProperty('override.workspace.settings') != 'true') { + throw new GradleException("Eclipse Buildship must use Gradle home ${expectedGradleHome}") + } + File expectedJavaHome = new File(System.getProperty('java.home')).canonicalFile + File configuredJavaHome = new File(buildshipPreferences.getProperty('java.home', '')).canonicalFile + if (configuredJavaHome != expectedJavaHome) { + throw new GradleException("Eclipse Buildship must use Gradle Java home ${expectedJavaHome}") + } + File classpath = file('.classpath') + if (!classpath.isFile() || !classpath.getText('UTF-8').contains('path="src/generated/resources"')) { + throw new GradleException('Eclipse does not expose generated production resources') + } + String mainOutput = new File(projectDir, 'bin/main').absolutePath + String expectedModClasses = "${mod_id}%%${mainOutput}${File.pathSeparator}${mod_id}%%${mainOutput}" + def verifyQuotedPaths = { File launch, List> boundaries -> + String contents = launch.getText('UTF-8') + boundaries.each { Map boundary -> + String option = boundary.option + String suffix = boundary.suffix + java.util.regex.Pattern quotedPath = java.util.regex.Pattern.compile( + java.util.regex.Pattern.quote(option + ' "') + '(.+?)' + + java.util.regex.Pattern.quote('"' + suffix)) + java.util.regex.Matcher matcher = quotedPath.matcher(contents) + if (!matcher.find()) { + throw new GradleException("${launch.name} does not quote ${option} path") + } + File target = new File(matcher.group(1)) + if (!target.exists()) { + throw new GradleException("${launch.name} ${option} path does not exist: ${target}") + } + } + } + ['runClient.launch', 'runServer.launch', 'runData.launch'].each { name -> + File launch = file(name) + if (!launch.isFile()) throw new GradleException("Missing ${name}") + String contents = launch.getText('UTF-8') + List forbidden = ['src/test', 'src/integrationTest', 'src/modelGenerator', 'junit-', 'opentest4j-', + 'basemetalsprobe', 'C:\\Users\\John'] + List leaked = forbidden.findAll { contents.contains(it) } + if (!leaked.isEmpty() || !contents.contains('ATTR_EXCLUDE_TEST_CODE') + || !contents.contains("PROJECT_ATTR\" value=\"${eclipseWorkspaceProjectName}\"") + || !contents.contains("MOD_CLASSES\" value=\"${expectedModClasses}\"") + || !contents.contains('MAIN_TYPE" value="net.minecraftforge.launcher.Main"') + || !contents.contains('--main net.minecraftforge.userdev.LaunchTesting')) { + throw new GradleException("${name} is not an isolated Forge 28 launch: ${leaked}") + } + } + List allLaunches = fileTree(projectDir) { include '*.launch' }.files.sort { it.name } + allLaunches.each { File launch -> + verifyQuotedPaths(launch, eclipseLaunchPathBoundaries.take(4)) + } + allLaunches.findAll { it.name.endsWith('Data.launch') }.each { File launch -> + verifyQuotedPaths(launch, eclipseLaunchPathBoundaries.drop(4)) + } + String clientLaunch = file('runClient.launch').getText('UTF-8') + def launcherMatch = clientLaunch =~ /externalArchive="([^&]+[\\\/]slime-launcher-[^&]+\.jar)"/ + if (!launcherMatch.find()) { + throw new GradleException('runClient.launch does not reference the ForgeGradle slime launcher') + } + File launcherJar = file(launcherMatch.group(1).replace('&', '&')) + if (!launcherJar.isFile() || !new ZipFile(launcherJar).withCloseable { + it.getEntry('net/minecraftforge/launcher/Main.class') != null + }) { + throw new GradleException("Invalid ForgeGradle slime launcher archive: ${launcherJar}") + } + ['META-INF/mods.toml', 'data/basemetals/orespawn/provider.json', + 'data/basemetals/recipes/stone_crackhammer.json'].each { relative -> + if (!new File(projectDir, "bin/main/${relative}").isFile()) { + throw new GradleException("Eclipse output is missing ${relative}") + } + } + [file('src/main/resources/META-INF/mods.toml'), + file('bin/main/META-INF/mods.toml')].each { File metadata -> + String contents = metadata.getText('UTF-8') + if (contents.contains('${') || !contents.contains('modId="basemetals"') + || !contents.contains("version=\"${project.version}\"") + || !contents.contains('[[dependencies.basemetals]]')) { + throw new GradleException("Eclipse mod metadata is unresolved or has the wrong identity: ${metadata}") + } + } + [file('src/main/resources/pack.mcmeta'), file('bin/main/pack.mcmeta')].each { File metadata -> + if (metadata.getText('UTF-8').contains('${')) { + throw new GradleException("Eclipse pack metadata is unresolved: ${metadata}") + } + } + } +} diff --git a/gradle/ide/intellij.gradle b/gradle/ide/intellij.gradle new file mode 100644 index 00000000..b3d0a1b9 --- /dev/null +++ b/gradle/ide/intellij.gradle @@ -0,0 +1,46 @@ +import groovy.xml.MarkupBuilder + +// Use Gradle launches so Forge prepares the assets, natives and game classpath. +def genIntellijRuns = tasks.register('genIntellijRuns') { + group = 'ide' + description = 'Adds client and server launches to IntelliJ using the Gradle wrapper.' + doLast { + ['Client': 'runClient', 'Server': 'runServer'].each { label, taskName -> + File configurationFile = file(".idea/runConfigurations/BaseMetals_${label}.xml") + configurationFile.parentFile.mkdirs() + StringWriter text = new StringWriter() + def xml = new MarkupBuilder(text) + xml.component(name: 'ProjectRunConfigurationManager') { + configuration(default: 'false', name: "Base Metals ${label}", + type: 'GradleRunConfiguration', factoryName: 'Gradle') { + ExternalSystemSettings { + option(name: 'executionName', value: "Base Metals ${label}") + option(name: 'externalProjectPath', value: '$PROJECT_DIR$') + option(name: 'externalSystemIdString', value: 'GRADLE') + option(name: 'scriptParameters', value: '--console=plain') + option(name: 'taskNames') { + list { option(value: taskName) } + } + option(name: 'vmOptions', value: '') + } + method(v: '2') + } + } + configurationFile.setText(text.toString() + '\n', 'UTF-8') + } + } +} + +tasks.register('verifyIntellijProductionRuns') { + group = 'verification' + dependsOn genIntellijRuns + doLast { + ['Client': 'runClient', 'Server': 'runServer'].each { label, taskName -> + String xml = file(".idea/runConfigurations/BaseMetals_${label}.xml").getText('UTF-8') + if (!xml.contains('GradleRunConfiguration') || !xml.contains(taskName) + || xml.contains('probe') || xml.contains('integrationTest')) { + throw new GradleException("IntelliJ ${label} launch is not production-only") + } + } + } +} diff --git a/gradle/release/artifacts.gradle b/gradle/release/artifacts.gradle new file mode 100644 index 00000000..1af220d3 --- /dev/null +++ b/gradle/release/artifacts.gradle @@ -0,0 +1,338 @@ +import groovy.json.JsonSlurper +import groovy.xml.XmlSlurper +import java.security.MessageDigest +import java.util.jar.Manifest +import java.util.zip.ZipFile + +def expectedMavenGroup = project.ext.baseMetalsBuild.expectedMavenGroup +def expectedMavenArtifact = project.ext.baseMetalsBuild.expectedMavenArtifact +def expectedMavenCoordinate = project.ext.baseMetalsBuild.expectedMavenCoordinate +def releaseJar = project.ext.baseMetalsBuild.releaseJar + +def mainReleaseName = "BaseMetals-${project.version}.jar" +def sourcesReleaseName = "BaseMetals-${project.version}-sources.jar" +def javadocReleaseName = "BaseMetals-${project.version}-javadoc.jar" +def expectedReleaseNames = [mainReleaseName, sourcesReleaseName, javadocReleaseName] +def preparedReleaseDir = project.findProperty('preparedReleaseDir') +def mavenUploadUrl = System.getenv('MAVEN_UPLOAD_URL') ?: 'https://invalid.invalid/missing-maven-upload-url' + +publishing { + publications { + mavenJava(MavenPublication) { + groupId = project.group + artifactId = expectedMavenArtifact + version = project.version + if (preparedReleaseDir != null) { + artifact new File(preparedReleaseDir.toString(), mainReleaseName) + artifact(new File(preparedReleaseDir.toString(), sourcesReleaseName)) { classifier = 'sources' } + artifact(new File(preparedReleaseDir.toString(), javadocReleaseName)) { classifier = 'javadoc' } + } else { + artifact releaseJar + artifact tasks.named('sourcesJar') + artifact tasks.named('javadocJar') + } + pom.withXml { + def root = asNode() + root.appendNode('name', 'Base Metals') + root.appendNode('description', + 'Historically inspired and fantasy metals, equipment, processing, and OreSpawn provider data.') + root.appendNode('url', 'https://github.com/MinecraftModDevelopmentMods/BaseMetals') + def license = root.appendNode('licenses').appendNode('license') + license.appendNode('name', 'GNU Lesser General Public License, Version 2.1') + license.appendNode('url', 'https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt') + } + } + } + repositories { + maven { + name = 'release' + url = uri(mavenUploadUrl) + credentials { + username = System.getenv('MAVEN_UPLOAD_USERNAME') ?: '' + password = System.getenv('MAVEN_UPLOAD_PASSWORD') ?: '' + } + } + } +} + +def sha256 = { File artifact -> + MessageDigest.getInstance('SHA-256').digest(artifact.bytes).encodeHex().toString().toUpperCase(Locale.ROOT) +} +def configuredJavaToolchainVersion = project.java_toolchain_version.toString() +def configuredOreSpawnIdentity = [version: project.orespawn_version.toString(), + curseProjectId: project.orespawn_curse_project_id.toString(), + curseFileId: project.orespawn_curse_file_id.toString(), sha256: project.orespawn_sha256.toString()] + +tasks.register('verifyJava8Toolchain') { + group = 'verification' + dependsOn tasks.named('compileJava') + doLast { + def metadata = tasks.named('compileJava', JavaCompile).get().javaCompiler.get().metadata + if (metadata.languageVersion.asInt() != 8) { + throw new GradleException("Minecraft 1.14.4 requires a Java 8 JDK, found " + + "${metadata.vendor} ${metadata.javaRuntimeVersion} at ${metadata.installationPath}") + } + logger.lifecycle("Java 8 development toolchain: ${metadata.vendor} ${metadata.javaRuntimeVersion}") + } +} + +tasks.register('verifyReleaseJava8Toolchain') { + group = 'verification' + description = 'Requires the pinned compiler for reproducible release artifacts.' + dependsOn tasks.named('verifyJava8Toolchain') + doLast { + def metadata = tasks.named('compileJava', JavaCompile).get().javaCompiler.get().metadata + if (configuredJavaToolchainVersion != '8.0.502+7' + || metadata.javaRuntimeVersion != '1.8.0_502-b07' + || !metadata.vendor.toString().toLowerCase(Locale.ROOT).contains('temurin')) { + throw new GradleException("Release auditing requires Eclipse Temurin ${configuredJavaToolchainVersion}; " + + "ordinary build and runClient tasks accept other Java 8 updates. Found " + + "${metadata.vendor} ${metadata.javaRuntimeVersion} at ${metadata.installationPath}") + } + } +} + +tasks.register('verifyDevelopmentLaunches') { + group = 'verification' + description = 'Checks that development runs use the ForgeGradle launcher.' + doLast { + ['runClient', 'runServer', 'runData', 'runRuntimeProbeServer', 'runLegacyUpgradeServer'].each { name -> + def run = tasks.named(name, JavaExec).get() + if (run.mainClass.get() != 'net.minecraftforge.launcher.Main') { + throw new GradleException("${name} bypasses Slime Launcher: ${run.mainClass.get()}") + } + } + } +} + +tasks.register('verifyMavenCoordinates') { + group = 'verification' + dependsOn tasks.named('generatePomFileForMavenJavaPublication') + doLast { + File pomFile = file("${buildDir}/publications/mavenJava/pom-default.xml") + def pom = new XmlSlurper(false, false).parse(pomFile) + def actual = [pom.groupId.text(), pom.artifactId.text(), pom.version.text()] + def expected = [expectedMavenGroup, expectedMavenArtifact, project.version.toString()] + if (project.group.toString() != expectedMavenGroup || actual != expected) { + throw new GradleException("Expected Maven coordinate ${expected.join(':')}, found ${actual.join(':')}") + } + } +} + +tasks.register('verifyReleaseConfiguration') { + group = 'verification' + dependsOn tasks.named('verifyJava8Toolchain') + doLast { + def actual = [modVersion: project.mod_version.toString(), modGroup: project.mod_group.toString(), + minecraftVersion: project.minecraft_version.toString(), forgeVersion: project.forge_version.toString(), + forgeInstallerSha256: project.forge_installer_sha256.toString(), + mappingChannel: project.mapping_channel.toString(), mappingVersion: project.mapping_version.toString(), + mcpVersion: project.mcp_version.toString(), loaderName: project.loader_name.toString(), + loaderCode: project.loader_code.toString(), javaVersion: project.java_version.toString(), + gradleJavaVersion: project.gradle_java_version.toString(), + curseForgeProjectId: project.curseforge_project_id.toString()] + def expected = [modVersion: '3.0.1.114041', modGroup: expectedMavenGroup, + minecraftVersion: '1.14.4', forgeVersion: '28.2.26', + forgeInstallerSha256: 'E68A0E2D589B2954C5D109A04B80747AC8C061ACABE074DB1607F9C2FD7D2CE1', + mappingChannel: 'snapshot', mappingVersion: '20190719-1.14.3', + mcpVersion: '20190829.143755', loaderName: 'forge', loaderCode: '1', + javaVersion: '8', gradleJavaVersion: '17', curseForgeProjectId: '240967'] + if (actual != expected) throw new GradleException("Unexpected Base Metals 1.14.4 identity: ${actual}") + } +} + +tasks.register('verifyReleaseDependencies') { + group = 'verification' + doLast { + def expected = [version: '4.1.0.114041', curseProjectId: '245586', curseFileId: '9041431', + sha256: '7691C1AE7C6AC890A2CC785F3EEF54A1255D61E4CFCF5661D67CB1609CFC41C6'] + if (configuredOreSpawnIdentity != expected) { + throw new GradleException("Unexpected OreSpawn identity: ${configuredOreSpawnIdentity}") + } + File artifact = configurations.orespawnRelease.singleFile + if (artifact == null || !artifact.isFile()) throw new GradleException('Released OreSpawn did not resolve') + String actual = sha256(artifact) + if (actual != configuredOreSpawnIdentity.sha256) { + throw new GradleException("OreSpawn SHA-256 mismatch: ${actual}") + } + new ZipFile(artifact).withCloseable { zip -> + def metadataEntry = zip.getEntry('META-INF/mods.toml') + if (metadataEntry == null) throw new GradleException('OreSpawn mod metadata is missing') + String metadata = zip.getInputStream(metadataEntry).getText('UTF-8') + if (!metadata.contains('modId="orespawn"') + || !metadata.contains('version="4.1.0.114041"') + || !metadata.contains('versionRange="[1.14.4]"') + || zip.getEntry('zone/moddev/mc/orespawn/api/OreSpawnApi.class') == null) { + throw new GradleException('Expected the published Minecraft 1.14.4 OreSpawn 4.1 release and public API') + } + } + } +} + +def forbiddenReleasePath = { String entryName -> + String name = entryName.replace('\\', '/').toLowerCase(Locale.ROOT) + def parts = name.split('/') + if (parts.any { it in ['agent.md', 'agents.md', 'claude.md', 'gemini.md', 'cursor.md', 'junie.md', + 'copilot-instructions.md', '.agent', '.agents', '.codex', '.claude', '.gemini', + 'validation.md', 'development.md', 'registry_audit.md'] + || it.startsWith('agent-') || it.startsWith('codex-') }) return true + if (name.contains('/testmod/') || name.contains('basemetalsprobe') + || name.contains('basemetalsclientprobe')) return true + if (name.endsWith('.class') && (name.startsWith('net/') || name.startsWith('cpw/') + || name.startsWith('zone/moddev/mc/orespawn/') + || name.startsWith('zone/moddev/mc/mineralogy/'))) return true + name.startsWith('com/mcmoddev/') || name.startsWith('ci-fixtures/') || name.startsWith('reference/') + || name.startsWith('agent-notes/') || name.contains('/agent-notes/') + || name.endsWith('/ore_textures.md') + || name.startsWith('src/integrationtest/') || name.startsWith('src/clientintegrationtest/') + || name.startsWith('src/gametest/') + || name.startsWith('src/modelgenerator/') || name.endsWith('/bmblockstateprovider.class') + || name.startsWith('src/integratedsmoke/') || name.startsWith('src/packagedclientsmoke/') + || name.startsWith('src/upgradesmoke/') || name.startsWith('src/packagedserversmoke/') + || name.contains('minecraft-mavenizer') || name.contains('/smoke/') + || name.contains('/gametest/') || name.endsWith('/oregenerator.class') + || name.endsWith('/worldgenerator.class') +} + +tasks.register('verifyReleaseArtifacts') { + group = 'verification' + dependsOn tasks.named('verifyReleaseJava8Toolchain') + dependsOn tasks.named('verifyReleaseConfiguration') + dependsOn tasks.named('verifyReleaseDependencies') + dependsOn tasks.named('verifyMavenCoordinates') + dependsOn tasks.named('assemble') + doLast { + File libs = file("${buildDir}/libs") + def actualArtifacts = fileTree(libs) { include 'BaseMetals-*.jar' }.files.collect { it.name }.sort() + if (actualArtifacts != expectedReleaseNames.sort()) { + throw new GradleException("Expected exactly ${expectedReleaseNames.sort()}, found ${actualArtifacts}") + } + expectedReleaseNames.each { artifactName -> + new ZipFile(new File(libs, artifactName)).withCloseable { zip -> + def leaked = zip.entries().toList().find { forbiddenReleasePath(it.name) } + if (leaked != null) throw new GradleException("${artifactName} contains forbidden ${leaked.name}") + zip.entries().toList().findAll { !it.directory }.each { entry -> + if (entry.name.endsWith('.class')) { + byte[] header = zip.getInputStream(entry).withCloseable { it.readNBytes(8) } + int major = ((header[6] & 0xff) << 8) | (header[7] & 0xff) + if (major != 52) { + throw new GradleException("${entry.name} uses bytecode major ${major}, not Java 8") + } + } + if (entry.name ==~ /.*\.(class|java|json|md|toml|mcmeta|html)/) { + String contents = zip.getInputStream(entry).withCloseable { it.getText('UTF-8') } + if (contents =~ /(?i)[a-z]:[\\\/](Users|BaseMetals|MinecraftMineralogy)[\\\/]/) { + throw new GradleException("${artifactName}:${entry.name} contains a local workspace path") + } + } + } + def manifestEntry = zip.getEntry('META-INF/MANIFEST.MF') + if (manifestEntry != null) { + Manifest manifest = new Manifest(zip.getInputStream(manifestEntry)) + def volatileKey = manifest.mainAttributes.keySet().find { + it.toString() ==~ /(?i).*(timestamp|build-time|built-at|build-date).*/ + } + if (volatileKey != null) { + throw new GradleException("${artifactName} has volatile manifest field ${volatileKey}") + } + } + } + } + File main = new File(libs, mainReleaseName) + new ZipFile(main).withCloseable { zip -> + Set names = zip.entries().toList().collect { it.name } as Set + ['META-INF/mods.toml', 'META-INF/MANIFEST.MF', 'META-INF/coremods.json', + 'coremods/basemetals_114_compatibility.js', 'pack.mcmeta', + 'data/basemetals/orespawn/provider.json', 'data/basemetals/recipes/stone_crackhammer.json', + 'data/minecraft/tags/blocks/anvil.json', 'assets/basemetals/lang/en_us.json', + 'META-INF/basemetals/docs/VERSIONS.md'].each { required -> + if (!names.contains(required)) throw new GradleException("Release jar is missing ${required}") + } + Manifest manifest = new Manifest(zip.getInputStream(zip.getEntry('META-INF/MANIFEST.MF'))) + if (manifest.mainAttributes.getValue('Maven-Artifact') != expectedMavenCoordinate + || manifest.mainAttributes.getValue('Build-Target') != + 'minecraft-1.14.4-forge-28.2.26-java-8') { + throw new GradleException('Release manifest contains the wrong identity') + } + String modsToml = zip.getInputStream(zip.getEntry('META-INF/mods.toml')).getText('UTF-8') + if (modsToml.contains('${') || !modsToml.contains('version="3.0.1.114041"') + || !modsToml.contains('versionRange="[4.1.0.114041,5.0.0)"')) { + throw new GradleException('Release mods.toml contains unresolved or incompatible metadata') + } + def provider = new JsonSlurper().parse(zip.getInputStream( + zip.getEntry('data/basemetals/orespawn/provider.json'))) + if (provider.schema_version != 3 || provider.provider_revision != 1 + || provider.provider_modid != 'basemetals' || provider.ores.size() != 11 + || provider.ores['basemetals:ore/copper'].enabled != true) { + throw new GradleException('OreSpawn provider contract changed') + } + def locales = names.findAll { it ==~ /assets\/basemetals\/lang\/.+\.json/ } + if (locales.size() != 18) { + throw new GradleException("Release jar must contain all 18 language files, found ${locales.size()}") + } + } + } +} + +tasks.register('writeReleaseChecksums') { + group = 'verification' + dependsOn tasks.named('verifyReleaseArtifacts') + doLast { + File output = file("${buildDir}/release/SHA256SUMS") + output.parentFile.mkdirs() + output.setText(expectedReleaseNames.sort().collect { name -> + "${sha256(file("${buildDir}/libs/${name}")).toLowerCase(Locale.ROOT)} ${name}" + }.join('\n') + '\n', 'UTF-8') + } +} + +tasks.register('verifyPreparedReleaseArtifacts') { + group = 'verification' + doLast { + if (preparedReleaseDir == null) throw new GradleException('preparedReleaseDir is required') + File prepared = file(preparedReleaseDir.toString()) + def expectedBundle = (expectedReleaseNames + ['SHA256SUMS', 'CHANGELOG.txt']).sort() + def actualBundle = prepared.listFiles().findAll { it.isFile() }.collect { it.name }.sort() + if (actualBundle != expectedBundle) { + throw new GradleException("Expected prepared bundle ${expectedBundle}, found ${actualBundle}") + } + Map declared = [:] + new File(prepared, 'SHA256SUMS').readLines('UTF-8').findAll { !it.trim().isEmpty() }.each { line -> + def match = line =~ /^([0-9a-fA-F]{64}) (.+)$/ + if (!match.matches()) throw new GradleException("Invalid prepared checksum line: ${line}") + declared[match[0][2]] = match[0][1].toUpperCase(Locale.ROOT) + } + if (declared.keySet().sort() != expectedReleaseNames.sort()) { + throw new GradleException('Prepared checksums do not declare exactly the three release jars') + } + declared.each { name, expectedHash -> + if (sha256(new File(prepared, name)) != expectedHash) { + throw new GradleException("Prepared checksum mismatch for ${name}") + } + } + } +} + +tasks.register('validateMavenReleaseCredentials') { + group = 'publishing' + doLast { + if (!System.getenv('MAVEN_UPLOAD_URL') || !System.getenv('MAVEN_UPLOAD_USERNAME') + || !System.getenv('MAVEN_UPLOAD_PASSWORD')) { + throw new GradleException('MAVEN_UPLOAD_URL, MAVEN_UPLOAD_USERNAME, and MAVEN_UPLOAD_PASSWORD are required') + } + if (System.getenv('MAVEN_UPLOAD_URL').startsWith('file:')) { + throw new GradleException('Maven release publication must use a remote repository') + } + } +} +tasks.withType(PublishToMavenRepository).configureEach { + dependsOn tasks.named('validateMavenReleaseCredentials') + dependsOn tasks.named('verifyMavenCoordinates') + dependsOn(preparedReleaseDir == null + ? tasks.named('verifyReleaseArtifacts') : tasks.named('verifyPreparedReleaseArtifacts')) +} + +project.ext.baseMetalsBuild.putAll([ + sha256: sha256, + configuredOreSpawnIdentity: configuredOreSpawnIdentity +]) diff --git a/gradle/verification/development.gradle b/gradle/verification/development.gradle new file mode 100644 index 00000000..51097cdf --- /dev/null +++ b/gradle/verification/development.gradle @@ -0,0 +1,153 @@ +import groovy.json.JsonSlurper +import groovy.xml.XmlSlurper +import java.security.MessageDigest +import java.util.jar.Manifest +import java.util.zip.ZipFile + +def prepareIntegrationTestModClasses = project.ext.baseMetalsBuild.prepareIntegrationTestModClasses + +def integrationTestRunDirectory = layout.buildDirectory.dir('integration-test-run') +def prepareIntegrationTestRun = tasks.register('prepareIntegrationTestRun') { + group = 'verification' + doLast { + File runDirectory = integrationTestRunDirectory.get().asFile.canonicalFile + File buildDirectory = layout.buildDirectory.get().asFile.canonicalFile + if (runDirectory == projectDir.canonicalFile + || !runDirectory.toPath().startsWith(buildDirectory.toPath()) + || runDirectory.name != 'integration-test-run') { + throw new GradleException("Unsafe integration-test run directory: ${runDirectory}") + } + delete runDirectory + if (!runDirectory.mkdirs()) throw new GradleException("Could not create ${runDirectory}") + new File(runDirectory, 'eula.txt').setText('eula=true\n', 'UTF-8') + new File(runDirectory, 'server.properties').setText('''\ +level-name=basemetals-runtime-probe +online-mode=false +spawn-protection=0 +server-port=0 +view-distance=4 +''', 'UTF-8') + if (!new File(runDirectory, 'mods').mkdirs()) { + throw new GradleException("Could not create integration-test mods directory") + } + } +} +def verifyIntegrationTestRun = tasks.register('verifyIntegrationTestRun') { + group = 'verification' + doLast { + File log = new File(integrationTestRunDirectory.get().asFile, 'logs/latest.log') + if (!log.isFile()) throw new GradleException("Runtime-probe log is missing: ${log}") + String text = log.getText('UTF-8') + if (!text.contains('BASEMETALS_RUNTIME_PROBE PASS')) { + throw new GradleException('Base Metals runtime probe did not report PASS') + } + List failures = ['BASEMETALS_RUNTIME_PROBE FAIL', 'Failed to load correctly', + 'This crash report has been saved to', + 'Encountered an unexpected exception'].findAll { text.contains(it) } + if (!failures.isEmpty()) throw new GradleException("Runtime-probe failures: ${failures}") + } +} +tasks.matching { it.name == 'runRuntimeProbeServer' }.configureEach { + dependsOn prepareIntegrationTestRun + finalizedBy verifyIntegrationTestRun +} + +def legacyWorldFixture = providers.gradleProperty('legacyWorldFixture') +def legacyWorldRegions = providers.gradleProperty('legacyWorldRegions') +def legacyUpgradeRunDirectory = layout.buildDirectory.dir('legacy-upgrade-run') +def resolveLegacyWorldFixture = { + if (!legacyWorldFixture.isPresent()) { + throw new GradleException('Pass -PlegacyWorldFixture=') + } + File candidate = file(legacyWorldFixture.get()).canonicalFile + File world = new File(candidate, 'level.dat').isFile() ? candidate + : new File(new File(candidate, 'world'), 'level.dat').isFile() + ? new File(candidate, 'world').canonicalFile : null + if (world == null) { + throw new GradleException("legacyWorldFixture has no level.dat: ${candidate}") + } + world +} +def prepareLegacyUpgradeRun = tasks.register('prepareLegacyUpgradeRun') { + group = 'verification' + dependsOn prepareIntegrationTestModClasses + doLast { + File sourceWorld = resolveLegacyWorldFixture() + File runDirectory = legacyUpgradeRunDirectory.get().asFile.canonicalFile + File buildDirectory = layout.buildDirectory.get().asFile.canonicalFile + if (runDirectory == projectDir.canonicalFile + || !runDirectory.toPath().startsWith(buildDirectory.toPath()) + || runDirectory.name != 'legacy-upgrade-run') { + throw new GradleException("Unsafe legacy-upgrade run directory: ${runDirectory}") + } + if (sourceWorld.toPath().startsWith(buildDirectory.toPath())) { + throw new GradleException("Legacy source must be outside disposable build output: ${sourceWorld}") + } + delete runDirectory + if (!runDirectory.mkdirs()) throw new GradleException("Could not create ${runDirectory}") + Set includedRegions = legacyWorldRegions.isPresent() + ? legacyWorldRegions.get().split(',').collect { it.trim() }.findAll { !it.isEmpty() }.toSet() + : Collections.emptySet() + copy { + from sourceWorld + into new File(runDirectory, 'world') + if (!includedRegions.isEmpty()) { + eachFile { details -> + String relative = details.relativePath.pathString.replace('\\', '/') + if ((relative.startsWith('region/') || relative.contains('/region/')) + && details.name ==~ /r\.-?\d+\.-?\d+\.mc[ar]/ + && !includedRegions.contains(details.name)) { + details.exclude() + } + } + } + } + new File(runDirectory, 'eula.txt').setText('eula=true\n', 'UTF-8') + new File(runDirectory, 'server.properties').setText('''\ +level-name=world +online-mode=false +spawn-protection=0 +server-port=0 +view-distance=4 +max-tick-time=-1 +''', 'UTF-8') + if (!new File(runDirectory, 'mods').mkdirs()) { + throw new GradleException('Could not create legacy-upgrade mods directory') + } + logger.lifecycle('Copied legacy fixture {} to disposable {}{}', sourceWorld, runDirectory, + includedRegions.isEmpty() ? '' : " using regions ${includedRegions.sort()}") + } +} +def verifyLegacyUpgradeRun = tasks.register('verifyLegacyUpgradeRun') { + group = 'verification' + doLast { + File runDirectory = legacyUpgradeRunDirectory.get().asFile + File log = new File(runDirectory, 'logs/latest.log') + if (!log.isFile()) throw new GradleException("Legacy-upgrade log is missing: ${log}") + String text = log.getText('UTF-8') + if (!text.contains('BASEMETALS_LEGACY_UPGRADE_PROBE PASS')) { + throw new GradleException('Legacy-world runtime probe did not report PASS') + } + File fixtureManifest = new File(runDirectory, 'world/legacy_registry_manifest_runtime.json') + if (fixtureManifest.isFile()) { + boolean from110 = new File(runDirectory, + 'world/BASEMETALS_1_10_FIXTURE_COMPLETE.txt').isFile() + String marker = from110 ? 'BASEMETALS_1_10_TO_1_14_UPGRADE PASS' + : 'BASEMETALS_1_12_TO_1_14_UPGRADE PASS' + if (!text.contains(marker)) { + throw new GradleException("Complete ${from110 ? '1.10' : '1.12'} fixture verification did not report PASS") + } + } + if (!text.contains('legacy Base Metals states') || !text.contains('protected')) { + throw new GradleException('Legacy Base Metals registry/chunk preparation was not observed') + } + List failures = ['BASEMETALS_RUNTIME_PROBE FAIL', 'BASEMETALS_LEGACY_UPGRADE_PROBE FAIL', + 'Failed to load correctly', 'This crash report has been saved to', + 'Encountered an unexpected exception'].findAll { text.contains(it) } + if (!failures.isEmpty()) throw new GradleException("Legacy-upgrade failures: ${failures}") + } +} +tasks.matching { it.name == 'runLegacyUpgradeServer' }.configureEach { + dependsOn prepareLegacyUpgradeRun + finalizedBy verifyLegacyUpgradeRun +} diff --git a/gradle/verification/generated-data.gradle b/gradle/verification/generated-data.gradle new file mode 100644 index 00000000..4d37279f --- /dev/null +++ b/gradle/verification/generated-data.gradle @@ -0,0 +1,95 @@ +import groovy.json.JsonSlurper +import groovy.xml.XmlSlurper +import java.security.MessageDigest +import java.util.jar.Manifest +import java.util.zip.ZipFile + +def sha256 = project.ext.baseMetalsBuild.sha256 + +def verifyGeneratedData = tasks.register('verifyGeneratedData', Exec) { + group = 'verification' + description = 'Regenerates and proves that the committed Minecraft 1.14.4 resource projection is current.' + commandLine 'node', 'tools/generate_114_resources.js' + def snapshot = { + Map result = new TreeMap<>() + [file('src/generated/resources'), + file('src/main/resources/data/basemetals/orespawn/provider.json')].each { File root -> + if (root.isFile()) { + result[project.relativePath(root).replace('\\', '/')] = sha256(root) + } else if (root.isDirectory()) { + fileTree(root) { exclude '.cache/**' }.files.findAll { it.isFile() }.each { File generated -> + result[project.relativePath(generated).replace('\\', '/')] = sha256(generated) + } + } + } + result + } + doFirst { ext.beforeGeneration = snapshot() } + doLast { + Map afterGeneration = snapshot() + if (ext.beforeGeneration != afterGeneration) { + Set beforePaths = ext.beforeGeneration.keySet() + Set afterPaths = afterGeneration.keySet() + List added = (afterPaths - beforePaths).toList().sort() + List removed = (beforePaths - afterPaths).toList().sort() + List changed = (beforePaths.intersect(afterPaths)).findAll { + ext.beforeGeneration[it] != afterGeneration[it] + }.toList().sort() + throw new GradleException('Generated resources were stale. Commit the generator output. ' + + "Added=${added.take(20)}, removed=${removed.take(20)}, changed=${changed.take(20)}") + } + } +} +def oreModelsDirectory = file('src/main/resources/assets/basemetals/models/block') +def oreModelSnapshot = { File directory -> + fileTree(directory) { include '*_ore.json' }.files.collectEntries { + [(it.name): sha256(it)] + } +} +def generateOreModels = tasks.register('generateOreModels', JavaExec) { + group = 'data generation' + description = 'Uses vanilla host textures when an ore overlay is present, otherwise keeps the legacy model.' + dependsOn tasks.named('modelGeneratorClasses') + classpath = sourceSets.modelGenerator.runtimeClasspath + mainClass = 'zone.moddev.mc.basemetals.data.BMBlockStateProvider' + args file('src/main/resources').absolutePath +} +tasks.named('processResources') { dependsOn generateOreModels } +def oreModelVerificationDirectory = layout.buildDirectory.dir('ore-model-verification') +def verifyGeneratedOreModels = tasks.register('verifyGeneratedOreModels', JavaExec) { + group = 'verification' + description = 'Checks the saved ore models without rewriting them.' + dependsOn tasks.named('modelGeneratorClasses') + classpath = sourceSets.modelGenerator.runtimeClasspath + mainClass = 'zone.moddev.mc.basemetals.data.BMBlockStateProvider' + args file('src/main/resources').absolutePath, '--output', oreModelVerificationDirectory.get().asFile.absolutePath + + doFirst { + File output = oreModelVerificationDirectory.get().asFile.canonicalFile + File buildDirectory = layout.buildDirectory.get().asFile.canonicalFile + if (!output.toPath().startsWith(buildDirectory.toPath()) || output.name != 'ore-model-verification') { + throw new GradleException("Unsafe ore-model verification directory: ${output}") + } + delete output + } + doLast { + File expectedModels = new File(oreModelVerificationDirectory.get().asFile, 'assets/basemetals/models/block') + if (oreModelSnapshot(oreModelsDirectory) != oreModelSnapshot(expectedModels)) { + throw new GradleException('Ore models are stale. Build the project and commit the updated JSON files.') + } + } +} +// Release checks must see stale models before the normal build updates them. +generateOreModels.configure { mustRunAfter verifyGeneratedOreModels } +tasks.named('verifyReleaseArtifacts') { dependsOn verifyGeneratedOreModels } +tasks.named('check') { + dependsOn tasks.named('verifyDevelopmentLaunches') + dependsOn verifyGeneratedData + dependsOn tasks.named('verifyReleaseConfiguration') + dependsOn tasks.named('verifyReleaseDependencies') + dependsOn tasks.named('verifyMavenCoordinates') +} + +project.ext.baseMetalsBuild.putAll([ + generateOreModels: generateOreModels +]) diff --git a/gradle/verification/packaged-forge.gradle b/gradle/verification/packaged-forge.gradle new file mode 100644 index 00000000..d7871f95 --- /dev/null +++ b/gradle/verification/packaged-forge.gradle @@ -0,0 +1,316 @@ +import groovy.json.JsonSlurper +import groovy.xml.XmlSlurper +import java.security.MessageDigest +import java.util.jar.Manifest +import java.util.zip.ZipFile + +def releaseJar = project.ext.baseMetalsBuild.releaseJar +def sha256 = project.ext.baseMetalsBuild.sha256 +def configuredOreSpawnIdentity = project.ext.baseMetalsBuild.configuredOreSpawnIdentity + +def packagedServerProbeDeobfJar = tasks.register('packagedServerProbeDeobfJar', Jar) { + dependsOn tasks.named(sourceSets.integrationTest.classesTaskName) + archiveFileName = 'basemetalsprobe-deobf.jar' + destinationDirectory = layout.buildDirectory.dir('packaged-probes') + from sourceSets.integrationTest.output +} +def packagedServerProbeJar = renamer.classes(packagedServerProbeDeobfJar) { + map.from minecraft.dependency.toSrgFile + output = layout.buildDirectory.file('packaged-probes/basemetalsprobe.jar') +} + +def clientIntegrationClasses = layout.buildDirectory.dir('client-integration-fixture/classes') +def compileClientIntegrationTestMod = tasks.register('compileClientIntegrationTestMod', JavaCompile) { + dependsOn tasks.named('classes') + source fileTree('src/clientIntegrationTest/java') + classpath = files(sourceSets.main.output, sourceSets.main.compileClasspath) + destinationDirectory = clientIntegrationClasses + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + options.encoding = 'UTF-8' +} +def clientIntegrationTestModJar = tasks.register('clientIntegrationTestModJar', Jar) { + dependsOn compileClientIntegrationTestMod + archiveFileName = 'basemetalsclientprobe-deobf.jar' + destinationDirectory = layout.buildDirectory.dir('client-integration-fixture') + from clientIntegrationClasses + from 'src/clientIntegrationTest/resources' +} +def oreOverlayClientFixtures = layout.buildDirectory.dir('ore-overlay-client-fixtures') +def generateOreOverlayClientFixtures = tasks.register('generateOreOverlayClientFixtures', JavaExec) { + dependsOn tasks.named('modelGeneratorClasses') + classpath = sourceSets.modelGenerator.runtimeClasspath + mainClass = 'zone.moddev.mc.basemetals.data.BMBlockStateProvider' + args file('src/main/resources').absolutePath, '--client-fixtures', oreOverlayClientFixtures.get().asFile.absolutePath +} +def packagedClientProbeJar = renamer.classes(clientIntegrationTestModJar) { + map.from minecraft.dependency.toSrgFile + output = layout.buildDirectory.file('client-integration-fixture/basemetalsclientprobe.jar') +} + +def packagedForgeServerRuntime = providers.gradleProperty('packagedForgeServerRuntime') +def packagedForgeClientRuntime = providers.gradleProperty('packagedForgeClientRuntime') +def requireRuntimeDirectory = { Provider configuredPath, String propertyName -> + if (!configuredPath.isPresent()) { + throw new GradleException("Pass -P${propertyName}=") + } + File runtime = file(configuredPath.get()).canonicalFile + if (!runtime.isDirectory()) { + throw new GradleException("${propertyName} does not name a directory: ${runtime}") + } + runtime +} +def prepareBuildOwnedRunDirectory = { File candidate, String expectedName -> + File runDirectory = candidate.canonicalFile + File buildDirectory = layout.buildDirectory.get().asFile.canonicalFile + if (!runDirectory.toPath().startsWith(buildDirectory.toPath()) || runDirectory.name != expectedName) { + throw new GradleException("Unsafe packaged-runtime directory: ${runDirectory}") + } + delete runDirectory + if (!runDirectory.mkdirs()) throw new GradleException("Could not create ${runDirectory}") + runDirectory +} +def releasedOreSpawnJar = { + File artifact = configurations.orespawnRelease.singleFile + if (artifact == null || !artifact.isFile() || sha256(artifact) != configuredOreSpawnIdentity.sha256) { + throw new GradleException('Exact released OreSpawn artifact is unavailable for packaged testing') + } + artifact +} +def assertPackagedRuntimePassed = { File runDirectory, String marker, String label -> + File log = new File(runDirectory, 'logs/latest.log') + if (!log.isFile()) throw new GradleException("${label} log is missing: ${log}") + String contents = log.getText('UTF-8') + if (!contents.contains(marker)) throw new GradleException("${label} did not report ${marker}") + List failures = ['Failed to load correctly', 'This crash report has been saved to', + 'Encountered an unexpected exception', 'BASEMETALS_RUNTIME_PROBE FAIL', + 'BASEMETALS_CLIENT_PROBE FAIL'] + .findAll { contents.contains(it) } + if (!failures.isEmpty()) throw new GradleException("${label} failures: ${failures}") + File crashReports = new File(runDirectory, 'crash-reports') + if (crashReports.isDirectory() && !fileTree(crashReports).files.isEmpty()) { + throw new GradleException("${label} created crash reports in ${crashReports}") + } +} + +def packagedServerRunDirectory = layout.buildDirectory.dir('packaged-server-run') +def preparePackagedServerIntegration = tasks.register('preparePackagedServerIntegration') { + dependsOn releaseJar + dependsOn packagedServerProbeJar + dependsOn tasks.named('verifyReleaseDependencies') + doLast { + File runDirectory = prepareBuildOwnedRunDirectory( + packagedServerRunDirectory.get().asFile, 'packaged-server-run') + copy { + from releaseJar + from packagedServerProbeJar + from releasedOreSpawnJar() + into new File(runDirectory, 'mods') + } + new File(runDirectory, 'server.properties').setText('''\ +level-name=basemetals-packaged-server-smoke +online-mode=false +spawn-protection=0 +server-port=0 +view-distance=4 +max-tick-time=-1 +''', 'UTF-8') + new File(runDirectory, 'eula.txt').setText('eula=true\n', 'UTF-8') + } +} +def packagedServerProcess = tasks.register('packagedServerProcess', Exec) { + group = 'verification' + timeout = java.time.Duration.ofMinutes(5) + dependsOn preparePackagedServerIntegration + doFirst { + File runtime = requireRuntimeDirectory(packagedForgeServerRuntime, 'packagedForgeServerRuntime') + File launcher = new File(runtime, "forge-${minecraft_version}-${forge_version}.jar") + File vanillaServer = new File(runtime, "minecraft_server.${minecraft_version}.jar") + File libraries = new File(runtime, 'libraries') + [launcher, vanillaServer, libraries].each { + if (!it.exists()) throw new GradleException("Incomplete official server runtime: ${it}") + } + workingDir packagedServerRunDirectory.get().asFile + commandLine javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(8) + vendor = JvmVendorSpec.ADOPTIUM + }.get().executablePath.asFile.absolutePath, + '-Xms512m', '-Xmx2g', '-Dbasemetalsprobe.sampleOres=true', '-jar', launcher.absolutePath, 'nogui' + } +} +tasks.register('packagedServerIntegrationTest') { + group = 'verification' + description = 'Runs the exact release jars in an official Forge 28 dedicated-server runtime.' + dependsOn packagedServerProcess + doLast { + assertPackagedRuntimePassed(packagedServerRunDirectory.get().asFile, + 'BASEMETALS_RUNTIME_PROBE PASS', 'packaged dedicated server') + } +} + +def packagedServerReloadProcess = tasks.register('packagedServerReloadProcess', Exec) { + group = 'verification' + timeout = java.time.Duration.ofMinutes(5) + dependsOn tasks.named('packagedServerIntegrationTest') + doFirst { + workingDir packagedServerRunDirectory.get().asFile + commandLine packagedServerProcess.get().commandLine + } +} + +tasks.register('packagedServerReloadTest') { + group = 'verification' + description = 'Reopens the packaged server world without resetting its config or save.' + dependsOn packagedServerReloadProcess + doLast { + assertPackagedRuntimePassed(packagedServerRunDirectory.get().asFile, + 'BASEMETALS_RUNTIME_PROBE PASS', 'reloaded packaged server') + } +} + +def packagedClientRunDirectory = layout.buildDirectory.dir('packaged-client-run') +def preparePackagedClientIntegration = tasks.register('preparePackagedClientIntegration') { + dependsOn releaseJar + dependsOn packagedClientProbeJar + dependsOn tasks.named('verifyReleaseDependencies') + dependsOn generateOreOverlayClientFixtures + doLast { + File runDirectory = prepareBuildOwnedRunDirectory( + packagedClientRunDirectory.get().asFile, 'packaged-client-run') + copy { + from releaseJar + from packagedClientProbeJar + from releasedOreSpawnJar() + into new File(runDirectory, 'mods') + } + File overlayPack = new File(runDirectory, 'resourcepacks/basemetals-ore-overlay-fixture') + copy { from oreOverlayClientFixtures; into overlayPack } + new File(overlayPack, 'pack.mcmeta').setText( + '{"pack":{"pack_format":4,"description":"Base Metals ore overlay test fixture"}}\n', 'UTF-8') + new File(runDirectory, 'options.txt').setText( + 'fullscreen:false\nlang:en_us\nresourcePacks:["file/basemetals-ore-overlay-fixture"]\n', 'UTF-8') + } +} +def packagedClientProcess = tasks.register('packagedClientProcess', Exec) { + group = 'verification' + timeout = java.time.Duration.ofMinutes(3) + dependsOn preparePackagedClientIntegration + doFirst { + File runtime = requireRuntimeDirectory(packagedForgeClientRuntime, 'packagedForgeClientRuntime') + File forgeJsonFile = new File(runtime, + "versions/${minecraft_version}-forge-${forge_version}/${minecraft_version}-forge-${forge_version}.json") + File baseJsonFile = new File(runtime, "versions/${minecraft_version}/${minecraft_version}.json") + File baseJar = new File(runtime, "versions/${minecraft_version}/${minecraft_version}.jar") + File libraries = new File(runtime, 'libraries') + File assets = new File(runtime, 'assets-local') + if (!assets.isDirectory()) assets = new File(runtime, 'assets') + File natives = new File(runtime, 'natives') + File versionNatives = new File(natives, "forge-${forge_version}") + if (versionNatives.isDirectory()) natives = versionNatives + [forgeJsonFile, baseJsonFile, baseJar, libraries, assets, natives].each { + if (!it.exists()) throw new GradleException("Incomplete official client runtime: ${it}") + } + + Map forgeJson = (Map) new JsonSlurper().parse(forgeJsonFile) + Map baseJson = (Map) new JsonSlurper().parse(baseJsonFile) + Map classpathByModule = new LinkedHashMap<>() + [baseJson, forgeJson].each { Map metadata -> + ((List) metadata.libraries).each { Map library -> + List rules = (List) library.rules + boolean allowed = rules == null || rules.isEmpty() + if (rules != null) { + rules.each { Map rule -> + Map os = (Map) rule.os + boolean matches = os == null || (os.name == 'windows' + && (os.arch == null || os.arch == System.getProperty('os.arch'))) + if (matches) allowed = rule.action == 'allow' + } + } + if (!allowed) return + String relative = (String) ((Map) ((Map) library.downloads).artifact).path + File artifact = new File(runtime, "libraries/${relative}") + if (!artifact.isFile()) throw new GradleException("Missing official client library: ${artifact}") + List coordinates = ((String) library.name).split(':') as List + String module = coordinates.size() >= 2 + ? "${coordinates[0]}:${coordinates[1]}" : (String) library.name + classpathByModule.put(module, artifact) + } + } + List classpathFiles = new ArrayList<>(classpathByModule.values()) + classpathFiles.add(baseJar) + List gameArguments = [] + gameArguments.addAll((List) ((Map) forgeJson.arguments).game) + gameArguments.addAll([ + '--username', 'BaseMetalsValidation', + '--version', "${minecraft_version}-forge-${forge_version}", + '--gameDir', packagedClientRunDirectory.get().asFile.absolutePath, + '--assetsDir', assets.absolutePath, + '--assetIndex', baseJson.assetIndex.id, + '--uuid', '00000000-0000-0000-0000-000000000001', + '--accessToken', 'validation-token', + '--userType', 'legacy', + '--versionType', 'release', + '--width', '854', '--height', '480' + ]) + workingDir packagedClientRunDirectory.get().asFile + commandLine javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(8) + vendor = JvmVendorSpec.ADOPTIUM + }.get().executablePath.asFile.absolutePath, + '-Xms512m', '-Xmx2g', '-Dbasemetalsclientprobe.enabled=true', + "-Djava.library.path=${natives.absolutePath}", + '-Dminecraft.launcher.brand=basemetals-validation', + '-Dminecraft.launcher.version=1', + '-cp', classpathFiles.collect { it.absolutePath }.join(File.pathSeparator), + (String) forgeJson.mainClass + args gameArguments + } +} +tasks.register('packagedClientIntegrationTest') { + group = 'verification' + description = 'Loads and renders the exact release jars in an official Forge 28 client runtime.' + dependsOn packagedClientProcess + doLast { + File runDirectory = packagedClientRunDirectory.get().asFile + File marker = new File(runDirectory, 'client-smoke-pass.properties') + if (!marker.isFile()) throw new GradleException('Packaged client completion marker is missing') + Properties values = new Properties() + marker.withInputStream { values.load(it) } + if (values.getProperty('content_verified') != 'true' + || values.getProperty('integrated_world_rendered') != 'true') { + throw new GradleException("Packaged client probe failed: ${values}") + } + assertPackagedRuntimePassed(runDirectory, 'BASEMETALS_CLIENT_PROBE PASS', 'packaged client') + } +} + +tasks.register('packagedRuntimeIntegrationTest') { + group = 'verification' + description = 'Runs the exact Base Metals and OreSpawn jars in official Forge 28 server and client runtimes.' + dependsOn tasks.named('packagedServerReloadTest') + dependsOn tasks.named('packagedClientIntegrationTest') +} + +tasks.register('contentModeIntegrationTest', Exec) { + group = 'verification' + description = 'Tests packaged content policies and client/server mode matching with disposable profiles.' + dependsOn releaseJar, packagedServerProbeJar, packagedClientProbeJar + dependsOn tasks.named('verifyReleaseDependencies') + doFirst { + File root = prepareBuildOwnedRunDirectory(file("${buildDir}/content-mode-tests"), 'content-mode-tests') + File fixture = new File(root, 'fixture') + copy { + from releaseJar, packagedServerProbeJar, packagedClientProbeJar, releasedOreSpawnJar() + into fixture + } + File java = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(8) + vendor = JvmVendorSpec.ADOPTIUM + }.get().executablePath.asFile + commandLine 'node', file('tools/test_content_modes.js').absolutePath, java.absolutePath, + requireRuntimeDirectory(packagedForgeServerRuntime, 'packagedForgeServerRuntime').absolutePath, + requireRuntimeDirectory(packagedForgeClientRuntime, 'packagedForgeClientRuntime').absolutePath, + fixture.absolutePath, root.absolutePath + if (providers.gradleProperty('contentModeCycleOnly').isPresent()) args '--cycle-only' + } +} diff --git a/src/clientIntegrationTest/java/net/minecraftforge/fml/client/gui/ModSelection.java b/src/clientIntegrationTest/java/net/minecraftforge/fml/client/gui/ModSelection.java new file mode 100644 index 00000000..76b606dd --- /dev/null +++ b/src/clientIntegrationTest/java/net/minecraftforge/fml/client/gui/ModSelection.java @@ -0,0 +1,20 @@ +package net.minecraftforge.fml.client.gui; + +/** Test access to Forge's package-private Mods-list entries. */ +public final class ModSelection { + private ModSelection() {} + + public static void select(GuiModList screen, String modId) { + GuiSlotModList list = (GuiSlotModList) screen.children().stream() + .filter(child -> child instanceof GuiSlotModList).findFirst().get(); + + for (GuiSlotModList.ModEntry entry : list.children()) { + if (entry.getInfo().getModId().equals(modId)) { + screen.setSelected(entry); + return; + } + } + + throw new IllegalStateException("Missing mod " + modId); + } +} diff --git a/src/clientIntegrationTest/java/zone/moddev/mc/basemetals/testmod/BaseMetalsClientProbe.java b/src/clientIntegrationTest/java/zone/moddev/mc/basemetals/testmod/BaseMetalsClientProbe.java index 27e0e13f..ae613ee3 100644 --- a/src/clientIntegrationTest/java/zone/moddev/mc/basemetals/testmod/BaseMetalsClientProbe.java +++ b/src/clientIntegrationTest/java/zone/moddev/mc/basemetals/testmod/BaseMetalsClientProbe.java @@ -19,13 +19,16 @@ import net.minecraft.block.Block; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiMainMenu; +import net.minecraft.client.gui.screen.MainMenuScreen; import net.minecraft.client.renderer.model.BakedQuad; import net.minecraft.client.renderer.model.IBakedModel; import net.minecraft.item.ItemStack; +import net.minecraft.item.BowItem; +import net.minecraft.item.Items; import net.minecraft.resources.IResource; import net.minecraft.util.BlockRenderLayer; -import net.minecraft.util.EnumFacing; +import net.minecraft.util.Direction; +import net.minecraft.util.Hand; import net.minecraft.util.ResourceLocation; import net.minecraft.world.GameType; import net.minecraft.world.WorldSettings; @@ -37,7 +40,7 @@ import net.minecraftforge.fml.ModList; import net.minecraftforge.fml.ExtensionPoint; import net.minecraftforge.fml.common.Mod; -import net.minecraftforge.fml.common.gameevent.TickEvent; +import net.minecraftforge.event.TickEvent; import net.minecraftforge.fml.client.gui.GuiModList; import net.minecraftforge.registries.ForgeRegistries; @@ -47,9 +50,9 @@ import zone.moddev.mc.basemetals.client.BaseMetalsConfigScreen; import zone.moddev.mc.basemetals.config.BaseMetalsConfig; import zone.moddev.mc.basemetals.config.ContentMode; -import net.minecraft.client.gui.GuiButton; -import net.minecraft.client.gui.GuiScreen; -import net.minecraft.client.gui.GuiYesNo; +import net.minecraft.client.gui.widget.button.Button; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.ConfirmScreen; import net.minecraft.client.resources.I18n; /** Checks models and renders a disposable world in the packaged Forge client. */ @@ -67,6 +70,7 @@ public final class BaseMetalsClientProbe { public BaseMetalsClientProbe() { instance = this; + LOGGER.info("Client probe loaded; enabled={}", Boolean.getBoolean("basemetalsclientprobe.enabled")); } @SubscribeEvent @@ -87,13 +91,15 @@ public static void onClientTick(TickEvent.ClientTickEvent event) { private void tick() { Minecraft minecraft = Minecraft.getInstance(); + if (stateTicks == 0) LOGGER.info("Client probe state={} screen={}", state, + minecraft.currentScreen == null ? "none" : minecraft.currentScreen.getClass().getName()); if (++stateTicks > 3600) fail(minecraft, "Timed out in client probe state " + state); try { - if (state == 0 && minecraft.currentScreen instanceof GuiMainMenu) { + if (state == 0 && minecraft.currentScreen instanceof MainMenuScreen) { if (Boolean.getBoolean("basemetalsclientprobe.login")) { require(BaseMetalsConfig.activeMode().serializedName().equals( System.getProperty("basemetalsclientprobe.expectedMode")), "startup mode differs from profile"); - minecraft.displayGuiScreen(new net.minecraft.client.gui.GuiConnecting( + minecraft.displayGuiScreen(new net.minecraft.client.gui.screen.ConnectingScreen( minecraft.currentScreen, minecraft, "127.0.0.1", Integer.getInteger("basemetalsclientprobe.port", 25565))); nextState(3); @@ -105,6 +111,7 @@ private void tick() { nextState(1); } else if (state == 1 && minecraft.world != null && minecraft.player != null && renderedFrames >= 8 && stateTicks >= 100) { + validateCreativeCrossbows(minecraft); writeMarker(); LOGGER.info("BASEMETALS_CLIENT_PROBE PASS frames={}", Integer.valueOf(renderedFrames)); minecraft.shutdown(); @@ -117,12 +124,46 @@ private void tick() { } } + private static void validateCreativeCrossbows(Minecraft minecraft) { + require(minecraft.world.isRemote, "crossbow probe must run on the client"); + require(minecraft.player.abilities.isCreativeMode, "crossbow probe needs a Creative player"); + int shots = 0; + + for (String name : ModContent.itemsById().keySet()) { + if (!name.endsWith("_crossbow")) continue; + String boltName = name.substring(0, name.length() - "_crossbow".length()) + "_bolt"; + ItemStack[] ammunition = {ItemStack.EMPTY, new ItemStack(Items.ARROW), + new ItemStack(ModContent.item("adamantine_arrow").get()), + new ItemStack(ModContent.item(boltName).get())}; + + for (ItemStack supply : ammunition) { + minecraft.player.inventory.clear(); + ItemStack launcher = new ItemStack(ModContent.item(name).get()); + ItemStack original = supply.copy(); + minecraft.player.setHeldItem(Hand.MAIN_HAND, launcher); + minecraft.player.setHeldItem(Hand.OFF_HAND, supply); + BowItem crossbow = (BowItem) launcher.getItem(); + + crossbow.onPlayerStoppedUsing(launcher, minecraft.world, minecraft.player, + crossbow.getUseDuration(launcher) - 20); + + require(ItemStack.areItemStacksEqual(original, supply), + "Creative crossbow consumed client ammunition: " + name); + require(launcher.getDamage() == 0, "Creative crossbow wore out on the client: " + name); + shots++; + } + } + + minecraft.player.inventory.clear(); + require(shots == 108, "all 27 crossbows need four client ammunition cases"); + LOGGER.info("BASEMETALS_CLIENT_CROSSBOWS PASS shots={}", Integer.valueOf(shots)); + } + private void checkLogin(Minecraft minecraft) throws IOException { boolean rejected = Boolean.getBoolean("basemetalsclientprobe.expectReject"); if (minecraft.world != null && minecraft.player != null && stateTicks > 60) { require(!rejected, "mismatched modes entered gameplay"); - net.minecraft.item.crafting.IRecipe bow = minecraft.world.getRecipeManager().getRecipe( - new ResourceLocation("basemetals", "tin_bow")); + net.minecraft.item.crafting.IRecipe bow = minecraft.world.getRecipeManager().getRecipe(new ResourceLocation("basemetals", "tin_bow")).orElse(null); require(bow != null, "recipe IDs changed during mode synchronization"); require(bow.isDynamic() == (BaseMetalsConfig.activeMode() == ContentMode.LOW_FANTASY), "client recipe policy did not match the server"); @@ -133,18 +174,18 @@ private void checkLogin(Minecraft minecraft) throws IOException { for (String name : new String[] {"steel_bow", "adamantine_bow", "adamantine_crossbow", "adamantine_gear", "adamantine_arrow", "adamantine_rod", "adamantine_pickaxe"}) { net.minecraft.item.crafting.IRecipe recipe = minecraft.world.getRecipeManager() - .getRecipe(new ResourceLocation("basemetals", name)); + .getRecipe(new ResourceLocation("basemetals", name)).orElse(null); require(minecraft.player.getRecipeBook().isUnlocked(recipe) != recipe.isDynamic(), "material discovery did not synchronize to the client: " + name); } require(!minecraft.player.getRecipeBook().isUnlocked(minecraft.world.getRecipeManager() - .getRecipe(new ResourceLocation("basemetals", "gold_bow"))), + .getRecipe(new ResourceLocation("basemetals", "gold_bow")).orElse(null)), "mode change unlocked an unearned recipe"); } require(minecraft.player.inventory.getStackInSlot(0).getItem() == ModContent.item("tin_bow").get(), "existing restricted item did not synchronize"); finishLogin(minecraft, "connected"); - } else if (minecraft.currentScreen instanceof net.minecraft.client.gui.GuiDisconnected) { + } else if (minecraft.currentScreen instanceof net.minecraft.client.gui.screen.DisconnectedScreen) { StringBuilder message = new StringBuilder(); try { for (java.lang.reflect.Field field : minecraft.currentScreen.getClass().getDeclaredFields()) { @@ -206,7 +247,7 @@ private static void validateClientContent(Minecraft minecraft) throws IOExceptio } private static void validateConfigScreen(Minecraft minecraft) { - GuiScreen parent = minecraft.currentScreen; + Screen parent = minecraft.currentScreen; boolean originalEffects = BaseMetalsConfig.SPECIAL_EFFECTS.get(); String originalMode = BaseMetalsConfig.CONTENT_MODE.get(); ContentMode activeMode = BaseMetalsConfig.activeMode(); @@ -218,15 +259,15 @@ private static void validateConfigScreen(Minecraft minecraft) { if ("basemetals".equals(ModList.get().getMods().get(index).getModId())) baseMetalsIndex = index; } require(baseMetalsIndex >= 0, "Base Metals is missing from the Mods list"); - modsScreen.selectModIndex(baseMetalsIndex); + selectMod(modsScreen, "basemetals"); ForgeHooksClient.drawScreen(modsScreen, 0, 0, 0.0F); - GuiButton configButton = button(modsScreen, 20); - require(configButton.enabled, "Base Metals Config button is disabled in the Mods list"); + Button configButton = button(modsScreen, 20); + require(configButton.active, "Base Metals Config button is disabled in the Mods list"); require(modsScreen.mouseClicked(configButton.x + 1, configButton.y + 1, 0), "Mods-list Config button did not accept a mouse click"); - GuiScreen forgeScreen = minecraft.currentScreen; + Screen forgeScreen = minecraft.currentScreen; require(forgeScreen instanceof BaseMetalsConfigScreen, "Mods-list button opened the wrong screen"); - require(forgeScreen.getChildren().size() == 9, "config screen must expose five settings and four actions"); + require(forgeScreen.children().size() == 9, "config screen must expose five settings and four actions"); click(forgeScreen, 20); click(forgeScreen, 10); click(forgeScreen, 1); @@ -235,22 +276,22 @@ private static void validateConfigScreen(Minecraft minecraft) { && originalEffects == BaseMetalsConfig.SPECIAL_EFFECTS.get(), "Cancel changed the loaded config"); ForgeHooksClient.drawScreen(modsScreen, 0, 0, 0.0F); - require(button(modsScreen, 20).enabled, "Config button was disabled on returning from the settings"); - modsScreen.selectModIndex(baseMetalsIndex); + require(button(modsScreen, 20).active, "Config button was disabled on returning from the settings"); + modsScreen.setSelected(null); ForgeHooksClient.drawScreen(modsScreen, 0, 0, 0.0F); - require(!button(modsScreen, 20).enabled, "Config button stayed enabled with no mod selected"); + require(!button(modsScreen, 20).active, "Config button stayed enabled with no mod selected"); int forgeIndex = -1; for (int index = 0; index < ModList.get().getMods().size(); index++) { if ("forge".equals(ModList.get().getMods().get(index).getModId())) forgeIndex = index; } require(forgeIndex >= 0, "Forge is missing from the Mods list"); - modsScreen.selectModIndex(forgeIndex); + selectMod(modsScreen, "forge"); ForgeHooksClient.drawScreen(modsScreen, 0, 0, 0.0F); - require(!button(modsScreen, 20).enabled, "Base Metals enabled the Config button for Forge"); + require(!button(modsScreen, 20).active, "Base Metals enabled the Config button for Forge"); click(modsScreen, 6); require(minecraft.currentScreen == parent, "Mods-list Done did not return to the main menu"); - GuiScreen oreSpawnScreen = WorldSettingsExtensionRegistry.extensions().stream() + Screen oreSpawnScreen = WorldSettingsExtensionRegistry.extensions().stream() .filter(extension -> "basemetals:configuration".equals(extension.id().toString())) .findFirst().get().createScreen(parent); require(oreSpawnScreen instanceof BaseMetalsConfigScreen, "OreSpawn factory uses the wrong screen"); @@ -258,19 +299,19 @@ private static void validateConfigScreen(Minecraft minecraft) { click(oreSpawnScreen, 10); click(oreSpawnScreen, 20); click(oreSpawnScreen, 2); - require(button(oreSpawnScreen, 20).displayString.contains(I18n.format(ContentMode.HIGH_FANTASY.translationKey())), + require(button(oreSpawnScreen, 20).getMessage().contains(I18n.format(ContentMode.HIGH_FANTASY.translationKey())), "Defaults did not restore High Fantasy"); click(oreSpawnScreen, 3); - require(button(oreSpawnScreen, 10).displayString.endsWith(I18n.format(originalEffects ? "options.on" : "options.off")), + require(button(oreSpawnScreen, 10).getMessage().endsWith(I18n.format(originalEffects ? "options.on" : "options.off")), "Undo did not restore the original boolean"); click(oreSpawnScreen, 20); click(oreSpawnScreen, 0); - require(minecraft.currentScreen instanceof GuiYesNo, "Mode change has no restart confirmation"); - oreSpawnScreen.confirmResult(false, 0); + require(minecraft.currentScreen instanceof ConfirmScreen, "Mode change has no restart confirmation"); + confirmation(minecraft, false); require(minecraft.currentScreen == oreSpawnScreen, "Declining confirmation lost pending edits"); require(BaseMetalsConfig.CONTENT_MODE.get().equals(originalMode), "Declining confirmation saved the mode"); click(oreSpawnScreen, 0); - oreSpawnScreen.confirmResult(true, 0); + confirmation(minecraft, true); require(minecraft.currentScreen == parent, "Done did not return to the OreSpawn parent"); require(!BaseMetalsConfig.CONTENT_MODE.get().equals(originalMode), "Done did not save the string property"); require(BaseMetalsConfig.activeMode() == activeMode, "GUI changes altered the startup-latched mode"); @@ -283,14 +324,34 @@ private static void validateConfigScreen(Minecraft minecraft) { LOGGER.info("BASEMETALS_CONFIG_GUI_PROBE PASS entries=5 factories=2 mods_button=true cancel=true undo=true defaults=true save=true"); } - private static GuiButton button(GuiScreen screen, int id) { - return screen.getChildren().stream().filter(child -> child instanceof GuiButton) - .map(child -> (GuiButton) child).filter(button -> button.id == id).findFirst() + private static void selectMod(GuiModList screen, String modId) { + net.minecraftforge.fml.client.gui.ModSelection.select(screen, modId); + } + + private static void confirmation(Minecraft minecraft, boolean accepted) { + List