diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 9cce54e..c0b1fa8 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -5,6 +5,7 @@ Update the Base Metals and OreSpawn dependencies and move the Java packages to z Use OreSpawn 4 for ore generation, with new depths and deposit shapes for the seven minerals. Existing chunks are not regenerated. Fix material and fluid switches, charcoal ownership, disabled-content recipes and achievements, and client models and molten textures. Add restart-required material settings to Forge Mods and OreSpawn Mods, plus automated build, compatibility and world-generation checks. +Use KiriCattus's ore overlays for all seven ores. Generate the models during the build, with support for full ore textures as well. Known issues: - OreSpawn 4.1.2 can add a second lithium rule when an OS1-imported world is moved without its original global import file. diff --git a/README.md b/README.md index 6460ee9..ba2d37f 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ materials, ingots, blends and molten forms. Charcoal uses the material supplied by Base Metals. Silicon is made from sand and coal or charcoal dust; it does not generate as ore. +The seven ores use KiriCattus's overlays on Minecraft's stone texture. + This branch targets Minecraft 1.10.2 and Forge 12.18.3.2511 or later. It requires Base Metals 2.5.1.110021 and OreSpawn 4.1.2.110021, or compatible versions in the declared ranges. Mineralogy is optional. Tinkers' Construct adds melting and diff --git a/build.gradle b/build.gradle index 1738268..4a32d3e 100644 --- a/build.gradle +++ b/build.gradle @@ -169,6 +169,7 @@ tasks.named('jar', Jar) { } ext.baseMineralsBuild = [developmentOutput: developmentOutput].asImmutable() +apply from: 'gradle/ore-models.gradle' apply from: 'gradle/release/artifacts.gradle' apply from: 'gradle/release/publishing.gradle' apply from: 'gradle/verification/runtime.gradle' diff --git a/docs/BUILDING.md b/docs/BUILDING.md index 6cec5d0..c47c7be 100644 --- a/docs/BUILDING.md +++ b/docs/BUILDING.md @@ -44,6 +44,19 @@ ForgeGradle's `source-sets-not-merged-for-eclipse` diagnostic refers to its own automatic merge. This project deliberately merges production output itself; the generated profiles, Buildship model and Java 8 Eclipse compiler are audited. +## Ore models + +`generateOreModels` writes the ore block models to `build/generated/ore-models`. +The normal build includes them in the game and sources jars. Blockstates and +inventory models use the existing model names. + +The seven ores use KiriCattus's transparent PNGs in +`src/main/resources/assets/baseminerals/textures/blocks/ore_overlays/`. The build +layers each PNG over Minecraft stone for the block and inventory models. +An ore with only a full texture in `textures/blocks/` uses that texture on a +single cube. To switch it to an overlay, put a PNG with the same name in +`ore_overlays/` and rebuild. + ## Verification `test` checks native declarations, configuration latching and namespace compatibility. diff --git a/gradle/ore-models.gradle b/gradle/ore-models.gradle new file mode 100644 index 0000000..a1c36a8 --- /dev/null +++ b/gradle/ore-models.gradle @@ -0,0 +1,77 @@ +import groovy.json.JsonOutput + +// Forge 1.10 predates Minecraft's data generator. Generate models during the build. +def assets = layout.projectDirectory.dir('src/main/resources/assets/baseminerals') +def textures = assets.dir('textures/blocks') +def solidTextures = fileTree(textures) { include '*_ore.png' } +def overlayTextures = fileTree(textures.dir('ore_overlays')) { include '*_ore.png' } +def oreStates = fileTree(assets.dir('blockstates')) { + include '*_ore.json' + // Historical placeholders with no registered ore blocks. + exclude 'charcoal_ore.json', 'silicon_ore.json' +} +def generated = layout.buildDirectory.dir('generated/ore-models') + +def generateOreModels = tasks.register('generateOreModels') { + group = 'build' + description = 'Generates ore models from the available overlay or solid textures.' + inputs.files(oreStates, solidTextures, overlayTextures).withPathSensitivity(PathSensitivity.RELATIVE) + outputs.dir(generated) + + doLast { + def names = oreStates.files.collect { it.name - '.json' }.sort() + def overlays = overlayTextures.files.collect { it.name - '.png' }.toSet() + def solid = solidTextures.files.collect { it.name - '.png' }.toSet() + def extra = overlays - names + def missing = names.findAll { !overlays.contains(it) && !solid.contains(it) } + if (extra || missing) { + throw new GradleException("Invalid ore textures. Missing textures: ${missing}. Unmatched overlays: ${extra}.") + } + + def directory = generated.get().dir('assets/baseminerals/models/block').asFile + directory.mkdirs() + def writeModel = { String name, Map model -> + new File(directory, "${name}.json").setText(JsonOutput.prettyPrint(JsonOutput.toJson(model)) + '\n', 'UTF-8') + } + def cube = { Number from, Number to, String texture -> + def faces = ['down', 'up', 'north', 'south', 'west', 'east'].collectEntries { face -> + [(face): [texture: texture, cullface: face, uv: [0, 0, 16, 16]]] + } + [from: [from, from, from], to: [to, to, to], faces: faces] + } + writeModel('ore_overlay', [ + parent: 'minecraft:block/block', + elements: [cube(0, 16, '#base'), cube(-0.05, 16.05, '#overlay')] + ]) + names.each { name -> + if (overlays.contains(name)) { + writeModel(name, [ + parent: 'baseminerals:block/ore_overlay', + textures: [particle: 'minecraft:blocks/stone', base: 'minecraft:blocks/stone', + overlay: "baseminerals:blocks/ore_overlays/${name}"] + ]) + } else { + writeModel(name, [ + parent: 'block/cube_all', + textures: [all: "baseminerals:blocks/${name}"] + ]) + } + } + directory.listFiles().findAll { + it.isFile() && it.name.endsWith('_ore.json') && !names.contains(it.name - '.json') + }.each { + if (!it.delete()) throw new GradleException("Could not remove stale generated ore model ${it.name}") + } + } +} + +tasks.named('processResources', ProcessResources) { + dependsOn generateOreModels + from generated + duplicatesStrategy = DuplicatesStrategy.FAIL +} + +tasks.named('sourcesJar', Jar) { + dependsOn generateOreModels + from generated +} diff --git a/gradle/verification/runtime.gradle b/gradle/verification/runtime.gradle index 679f049..cd41522 100644 --- a/gradle/verification/runtime.gradle +++ b/gradle/verification/runtime.gradle @@ -49,7 +49,7 @@ tasks.named('test', Test) { tasks.register('verifyProjectResources', Exec) { group = 'verification' - dependsOn tasks.named('test') + dependsOn tasks.named('test'), tasks.named('processResources') doFirst { commandLine providers.gradleProperty('pythonExecutable').getOrElse('python3'), 'tools/validation/legacy110/audit_resources.py', '--mirror', diff --git a/src/main/java/zone/moddev/mc/baseminerals/block/OverlayOreBlock.java b/src/main/java/zone/moddev/mc/baseminerals/block/OverlayOreBlock.java new file mode 100644 index 0000000..11827ed --- /dev/null +++ b/src/main/java/zone/moddev/mc/baseminerals/block/OverlayOreBlock.java @@ -0,0 +1,20 @@ +package zone.moddev.mc.baseminerals.block; + +import com.mcmoddev.lib.block.BlockMMDOre; +import com.mcmoddev.lib.material.MMDMaterial; +import net.minecraft.util.BlockRenderLayer; +import net.minecraftforge.fml.relauncher.Side; +import net.minecraftforge.fml.relauncher.SideOnly; + +/** Uses the shared ore behaviour with alpha testing for overlay models. */ +public final class OverlayOreBlock extends BlockMMDOre { + public OverlayOreBlock(MMDMaterial material) { + super(material); + } + + @Override + @SideOnly(Side.CLIENT) + public BlockRenderLayer getBlockLayer() { + return BlockRenderLayer.CUTOUT_MIPPED; + } +} diff --git a/src/main/java/zone/moddev/mc/baseminerals/init/Blocks.java b/src/main/java/zone/moddev/mc/baseminerals/init/Blocks.java index d9651a1..f2dddf8 100644 --- a/src/main/java/zone/moddev/mc/baseminerals/init/Blocks.java +++ b/src/main/java/zone/moddev/mc/baseminerals/init/Blocks.java @@ -1,15 +1,16 @@ package zone.moddev.mc.baseminerals.init; -import zone.moddev.mc.baseminerals.init.ItemGroups; - import java.util.Arrays; import java.util.List; +import zone.moddev.mc.baseminerals.block.OverlayOreBlock; import zone.moddev.mc.baseminerals.data.MaterialNames; import com.mcmoddev.lib.data.Names; import com.mcmoddev.lib.data.SharedStrings; import com.mcmoddev.lib.init.Materials; import com.mcmoddev.lib.material.MMDMaterial; +import com.mcmoddev.lib.util.Oredicts; +import net.minecraft.block.Block; /** * This class initializes all blocks in Base Minerals. @@ -40,7 +41,7 @@ public static void init() { final MMDMaterial material = Materials.getMaterialByName(materialName); create(Names.BLOCK, material); - create(Names.ORE, material); + createOre(material); }); /* if (zone.moddev.mc.baseminerals.util.Config.isEnabled(MaterialNames.SILICON) && Materials.hasMaterial(MaterialNames.SILICON)) { @@ -48,4 +49,14 @@ public static void init() { } */ } + + private static void createOre(MMDMaterial material) { + if (!material.hasOre() || material.hasBlock(Names.ORE) || !isNameEnabled(Names.ORE)) { + return; + } + final Block ore = addBlock(new OverlayOreBlock(material), Names.ORE, material, + ItemGroups.getTab(SharedStrings.TAB_BLOCKS)); + material.addNewBlock(Names.ORE.toString(), ore); + Oredicts.registerOre("ore" + material.getCapitalizedName(), ore); + } } diff --git a/src/main/resources/assets/baseminerals/models/block/lithium_ore.json b/src/main/resources/assets/baseminerals/models/block/lithium_ore.json deleted file mode 100644 index acdbf7f..0000000 --- a/src/main/resources/assets/baseminerals/models/block/lithium_ore.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "parent": "block/cube_all", - "textures": { - "all": "baseminerals:blocks/lithium_ore" - } -} diff --git a/src/main/resources/assets/baseminerals/models/block/niter_ore.json b/src/main/resources/assets/baseminerals/models/block/niter_ore.json deleted file mode 100644 index 5699b39..0000000 --- a/src/main/resources/assets/baseminerals/models/block/niter_ore.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "parent": "block/cube_all", - "textures": { - "all": "baseminerals:blocks/niter_ore" - } -} diff --git a/src/main/resources/assets/baseminerals/models/block/phosphorus_ore.json b/src/main/resources/assets/baseminerals/models/block/phosphorus_ore.json deleted file mode 100644 index 485fa4f..0000000 --- a/src/main/resources/assets/baseminerals/models/block/phosphorus_ore.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "parent": "block/cube_all", - "textures": { - "all": "baseminerals:blocks/phosphorus_ore" - } -} diff --git a/src/main/resources/assets/baseminerals/models/block/potash_ore.json b/src/main/resources/assets/baseminerals/models/block/potash_ore.json deleted file mode 100644 index 6e00183..0000000 --- a/src/main/resources/assets/baseminerals/models/block/potash_ore.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "parent": "block/cube_all", - "textures": { - "all": "baseminerals:blocks/potash_ore" - } -} diff --git a/src/main/resources/assets/baseminerals/models/block/salt_ore.json b/src/main/resources/assets/baseminerals/models/block/salt_ore.json deleted file mode 100644 index 046de71..0000000 --- a/src/main/resources/assets/baseminerals/models/block/salt_ore.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "parent": "block/cube_all", - "textures": { - "all": "baseminerals:blocks/salt_ore" - } -} diff --git a/src/main/resources/assets/baseminerals/models/block/saltpeter_ore.json b/src/main/resources/assets/baseminerals/models/block/saltpeter_ore.json deleted file mode 100644 index 2669156..0000000 --- a/src/main/resources/assets/baseminerals/models/block/saltpeter_ore.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "parent": "block/cube_all", - "textures": { - "all": "baseminerals:blocks/saltpeter_ore" - } -} diff --git a/src/main/resources/assets/baseminerals/models/block/sulfur_ore.json b/src/main/resources/assets/baseminerals/models/block/sulfur_ore.json deleted file mode 100644 index f342e04..0000000 --- a/src/main/resources/assets/baseminerals/models/block/sulfur_ore.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "parent": "block/cube_all", - "textures": { - "all": "baseminerals:blocks/sulfur_ore" - } -} diff --git a/tools/validation/legacy110/audit_resources.py b/tools/validation/legacy110/audit_resources.py index e52c12b..c48c4f8 100644 --- a/tools/validation/legacy110/audit_resources.py +++ b/tools/validation/legacy110/audit_resources.py @@ -9,7 +9,8 @@ def main(): parser=argparse.ArgumentParser();parser.add_argument('--mirror',type=Path);args=parser.parse_args() count=0 - for path in (ROOT/'src/main/resources').rglob('*'): + resources=ROOT/'build/resources/main' + for path in resources.rglob('*'): if path.is_file() and (path.suffix in ('.json','.mcmeta') or path.name=='mcmod.info'): json.loads(path.read_text(encoding='utf-8-sig'));count+=1 specs=json.loads((ROOT/'gradle/dependencies.json').read_text()) @@ -21,7 +22,29 @@ def main(): Draft202012Validator(schema,registry=registry).validate(provider) assert len(provider['ores'])==7 assert all(not provider.get(section) for section in ['rocks','terrain_dimensions','geomes','fluid_deposits','biome_palettes']) - assets=ROOT/'src/main/resources/assets/baseminerals' + assets=resources/'assets/baseminerals' + source_textures=ROOT/'src/main/resources/assets/baseminerals/textures/blocks' + for ore in provider['ores'].values(): + name=ore['block'].split(':',1)[1] + state=json.loads((assets/'blockstates'/f'{name}.json').read_text()) + item=json.loads((assets/'models/item'/f'{name}.json').read_text()) + model=json.loads((assets/'models/block'/f'{name}.json').read_text()) + assert state['variants']['normal']['model']==f'baseminerals:{name}',name + assert item['parent']==f'baseminerals:block/{name}',name + textures=assets/'textures/blocks' + if (textures/'ore_overlays'/f'{name}.png').is_file(): + assert model['parent']=='baseminerals:block/ore_overlay',name + assert (assets/'models/block/ore_overlay.json').is_file(),name + assert model['textures']=={ + 'particle':'minecraft:blocks/stone','base':'minecraft:blocks/stone', + 'overlay':f'baseminerals:blocks/ore_overlays/{name}' + },name + if not (source_textures/f'{name}.png').is_file(): + assert not (textures/f'{name}.png').exists(),f'{name}: stale solid texture' + else: + assert (textures/f'{name}.png').is_file(),f'{name}: missing solid texture' + assert model['parent']=='block/cube_all',name + assert model['textures']=={'all':f'baseminerals:blocks/{name}'},name language=dict(line.split('=',1) for line in (assets/'lang/en_US.lang').read_text().splitlines() if '=' in line and not line.startswith('#')) for name in ['charcoal','lithium','niter','phosphorus','potash','salt','saltpeter','silicon','sulfur']: assert 'config.baseminerals.enable_'+name in language,name diff --git a/tools/validation/legacy110/runtime-probe/review/minerals/ClientChecks.java b/tools/validation/legacy110/runtime-probe/review/minerals/ClientChecks.java index c2e3f3e..f33da47 100644 --- a/tools/validation/legacy110/runtime-probe/review/minerals/ClientChecks.java +++ b/tools/validation/legacy110/runtime-probe/review/minerals/ClientChecks.java @@ -66,6 +66,7 @@ public static final class Checks { if(!"baseminerals".equals(item.getRegistryName().getResourceDomain()))continue; require(mc.getRenderItem().getItemModelMesher().getItemModel(new ItemStack(item))!=mc.getRenderItem().getItemModelMesher().getModelManager().getMissingModel(),"Missing baked item: "+item.getRegistryName()); } + OreModelChecks.verify(mc); check=mc.getIntegratedServer().addScheduledTask(()->{try{BaseMineralsProbe.run(mc.getIntegratedServer());}catch(Exception e){throw new RuntimeException(e);}}); } else if(state==3&&check!=null&&check.isDone()) { check.get();screenshot(mc,"integrated-world.png");integrated=mc.getIntegratedServer();integrated.initiateShutdown();state=4; diff --git a/tools/validation/legacy110/runtime-probe/review/minerals/OreModelChecks.java b/tools/validation/legacy110/runtime-probe/review/minerals/OreModelChecks.java new file mode 100644 index 0000000..dc46ed6 --- /dev/null +++ b/tools/validation/legacy110/runtime-probe/review/minerals/OreModelChecks.java @@ -0,0 +1,86 @@ +package review.minerals; + +import com.mcmoddev.lib.init.Materials; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.List; +import net.minecraft.block.Block; +import net.minecraft.block.state.IBlockState; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.block.model.BakedQuad; +import net.minecraft.client.renderer.block.model.IBakedModel; +import net.minecraft.client.resources.IResource; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.util.BlockRenderLayer; +import net.minecraft.util.EnumFacing; +import net.minecraft.util.ResourceLocation; +import net.minecraftforge.fml.common.registry.ForgeRegistries; +import net.minecraftforge.oredict.OreDictionary; +import zone.moddev.mc.baseminerals.util.Config; + +import static review.minerals.ClientChecks.require; + +/** Checks the texture and geometry of the baked block and inventory models. */ +final class OreModelChecks { + static void verify(Minecraft minecraft) throws IOException { + int overlays = 0; + int solids = 0; + for (String material : BaseMineralsProbe.ORES) { + if (!Config.isEnabled(material)) continue; + String name = material + "_ore"; + ResourceLocation id = new ResourceLocation("baseminerals", name); + Block ore = ForgeRegistries.BLOCKS.getValue(id); + require(ore != null && id.equals(ore.getRegistryName()), "Wrong ore block ID: " + id); + Item item = Item.getItemFromBlock(ore); + require(id.equals(item.getRegistryName()), "Wrong ore item ID: " + id); + String oreKey = "ore" + Materials.getMaterialByName(material).getCapitalizedName(); + require(OreDictionary.getOres(oreKey).stream().anyMatch(stack -> stack.getItem() == item), + "Missing Ore Dictionary entry: " + oreKey); + require(ore.getBlockLayer() == BlockRenderLayer.CUTOUT_MIPPED, "Wrong ore render layer: " + id); + + String overlay = "baseminerals:blocks/ore_overlays/" + name; + boolean layered = hasOverlay(minecraft, name); + String base = layered ? "minecraft:blocks/stone" : "baseminerals:blocks/" + name; + IBlockState state = ore.getDefaultState(); + IBakedModel placed = minecraft.getBlockRendererDispatcher().getBlockModelShapes().getModelForState(state); + IBakedModel inventory = minecraft.getRenderItem().getItemModelMesher().getItemModel(new ItemStack(item)); + verifyModel(placed, state, base, layered ? overlay : null); + verifyModel(inventory, null, base, layered ? overlay : null); + if (layered) overlays++; + else solids++; + } + System.out.println("BASE_MINERALS_ORE_MODELS_PASS solids=" + solids + " overlays=" + overlays); + } + + private static boolean hasOverlay(Minecraft minecraft, String name) throws IOException { + ResourceLocation path = new ResourceLocation("baseminerals", "textures/blocks/ore_overlays/" + name + ".png"); + try (IResource resource = minecraft.getResourceManager().getResource(path)) { + return true; + } catch (FileNotFoundException missing) { + return false; + } + } + + private static void verifyModel(IBakedModel model, IBlockState state, String base, String overlay) { + require(base.equals(model.getParticleTexture().getIconName()), "Wrong ore particles: " + base); + for (EnumFacing face : EnumFacing.values()) { + List quads = model.getQuads(state, face, 0); + require(quads.size() == (overlay == null ? 1 : 2), "Wrong ore face count: " + base + " " + face); + verifyFace(quads, face, base, 0.0F); + if (overlay != null) verifyFace(quads, face, overlay, 0.05F / 16.0F); + } + } + + private static void verifyFace(List quads, EnumFacing face, String texture, float offset) { + BakedQuad quad = quads.stream().filter(candidate -> texture.equals(candidate.getSprite().getIconName())) + .findFirst().orElseThrow(() -> new AssertionError("Missing ore sprite: " + texture)); + int axis = face.getAxis().ordinal(); + float expected = face.getAxisDirection() == EnumFacing.AxisDirection.POSITIVE ? 1.0F + offset : -offset; + int[] data = quad.getVertexData(); + for (int vertex = 0; vertex < 4; vertex++) { + float depth = Float.intBitsToFloat(data[vertex * (data.length / 4) + axis]); + require(Math.abs(depth - expected) < 0.0001F, "Wrong ore face depth: " + texture + " " + face); + } + } +}